mu-agent-bridge 1.0.34 → 1.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +4 -2
  2. package/dist/src-ts/channel-sessions.js +90 -24
  3. package/dist/src-ts/control-server.js +48 -7
  4. package/dist/src-ts/dsh-agent.js +141 -46
  5. package/dist/src-ts/headless-account.patch.yml +8 -1
  6. package/dist/src-ts/plugin-manager.js +69 -3
  7. package/dist/src-ts/plugins/aun/attachments.js +44 -13
  8. package/dist/src-ts/plugins/aun/index.js +5 -6
  9. package/dist/src-ts/plugins/chatgpt/cdp-client.js +342 -9
  10. package/dist/src-ts/plugins/chatgpt/index.js +127 -7
  11. package/dist/src-ts/plugins/desktop-launch.js +19 -0
  12. package/dist/src-ts/plugins/doubao/attachments.js +42 -0
  13. package/dist/src-ts/plugins/doubao/cdp-client.js +530 -48
  14. package/dist/src-ts/plugins/doubao/index.js +114 -25
  15. package/dist/src-ts/plugins/doubao/session-history.js +123 -0
  16. package/dist/src-ts/plugins/dsh/index.js +18 -2
  17. package/dist/src-ts/plugins/dsh/session-history.js +371 -0
  18. package/dist/src-ts/plugins/qwenwork/cdp-client.js +271 -10
  19. package/dist/src-ts/plugins/qwenwork/index.js +42 -3
  20. package/dist/src-ts/plugins/qwenwork/session-history.js +245 -0
  21. package/dist/src-ts/plugins/workbuddy/cdp-client.js +42 -2
  22. package/dist/src-ts/plugins/workbuddy/index.js +23 -3
  23. package/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
  24. package/dist/src-ts/session-archive.js +509 -0
  25. package/dist/src-ts/session-history.js +2 -0
  26. package/dist/src-ts/session-management.js +665 -0
  27. package/dist/src-ts/session-summary.js +171 -0
  28. package/dist/tests-ts/aun-attachments.test.js +41 -3
  29. package/dist/tests-ts/aun-plugin.test.js +28 -5
  30. package/dist/tests-ts/bundled-package.test.js +3 -1
  31. package/dist/tests-ts/channel-sessions.test.js +255 -2
  32. package/dist/tests-ts/chatgpt-history.test.js +197 -0
  33. package/dist/tests-ts/chatgpt-plugin.test.js +279 -2
  34. package/dist/tests-ts/control-server.test.js +39 -0
  35. package/dist/tests-ts/desktop-launch.test.js +41 -0
  36. package/dist/tests-ts/doubao-history.test.js +172 -0
  37. package/dist/tests-ts/doubao-plugin.test.js +549 -2
  38. package/dist/tests-ts/dsh-agent.test.js +199 -0
  39. package/dist/tests-ts/dsh-history.test.js +165 -0
  40. package/dist/tests-ts/native-session-management.test.js +195 -0
  41. package/dist/tests-ts/plugin-manager.test.js +354 -4
  42. package/dist/tests-ts/qwenwork-history.test.js +339 -0
  43. package/dist/tests-ts/qwenwork-images.test.js +166 -0
  44. package/dist/tests-ts/qwenwork-plugin.test.js +40 -1
  45. package/dist/tests-ts/session-archive.test.js +422 -0
  46. package/dist/tests-ts/session-management.test.js +323 -0
  47. package/dist/tests-ts/session-summary.test.js +199 -0
  48. package/dist/tests-ts/workbuddy-history.test.js +256 -0
  49. package/dist/tests-ts/workbuddy-plugin.test.js +43 -0
  50. package/package.json +1 -1
  51. package/plugins/aun/dist/src-ts/plugins/aun/attachments.js +44 -13
  52. package/plugins/aun/dist/src-ts/plugins/aun/index.js +5 -6
  53. package/plugins/aun/package.json +1 -1
  54. package/plugins/aun/plugin.json +1 -1
  55. package/plugins/chatgpt/dist/src-ts/attachments.js +62 -0
  56. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +342 -9
  57. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +127 -7
  58. package/plugins/chatgpt/package.json +1 -1
  59. package/plugins/chatgpt/plugin.json +14 -1
  60. package/plugins/doubao/config.schema.json +3 -0
  61. package/plugins/doubao/dist/src-ts/attachments.js +62 -0
  62. package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +19 -0
  63. package/plugins/doubao/dist/src-ts/plugins/doubao/attachments.js +42 -0
  64. package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +530 -48
  65. package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +114 -25
  66. package/plugins/doubao/dist/src-ts/plugins/doubao/session-history.js +123 -0
  67. package/plugins/doubao/package.json +1 -1
  68. package/plugins/doubao/plugin.json +1 -1
  69. package/plugins/dsh/config.schema.json +2 -1
  70. package/plugins/dsh/dist/src-ts/attachments.js +62 -0
  71. package/plugins/dsh/dist/src-ts/dsh-agent.js +141 -46
  72. package/plugins/dsh/dist/src-ts/headless-account.patch.yml +8 -1
  73. package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +18 -2
  74. package/plugins/dsh/dist/src-ts/plugins/dsh/session-history.js +371 -0
  75. package/plugins/dsh/package.json +5 -2
  76. package/plugins/dsh/plugin.json +1 -1
  77. package/plugins/qwenwork/dist/src-ts/attachments.js +62 -0
  78. package/plugins/qwenwork/dist/src-ts/plugins/desktop-launch.js +19 -0
  79. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +271 -10
  80. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +42 -3
  81. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/session-history.js +245 -0
  82. package/plugins/qwenwork/package.json +1 -1
  83. package/plugins/qwenwork/plugin.json +1 -1
  84. package/plugins/workbuddy/config.schema.json +1 -1
  85. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +42 -2
  86. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +23 -3
  87. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
  88. package/plugins/workbuddy/package.json +1 -1
  89. package/plugins/workbuddy/plugin.json +1 -1
@@ -0,0 +1,256 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const strict_1 = __importDefault(require("node:assert/strict"));
7
+ const node_test_1 = __importDefault(require("node:test"));
8
+ const promises_1 = require("node:fs/promises");
9
+ const node_path_1 = require("node:path");
10
+ const node_os_1 = require("node:os");
11
+ const node_vm_1 = require("node:vm");
12
+ const workbuddy_1 = require("../src-ts/plugins/workbuddy");
13
+ const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
14
+ const logger = { info() { }, warn() { }, error() { } };
15
+ const snapshot = () => ({ conversationId: "task-a", messages: [
16
+ { id: "u1", role: "user", text: "问题", complete: true },
17
+ { id: "a1", role: "assistant", text: "回答", complete: true },
18
+ { id: "draft", role: "assistant", text: "草稿", complete: false },
19
+ { id: "tool", role: "tool", text: "工具输出", complete: true },
20
+ { role: "user", text: "没有稳定 ID", complete: true },
21
+ ], missingReasons: [] });
22
+ async function fixture(overrides = {}, config = { transport: "cdp" }) {
23
+ const calls = [];
24
+ let activeId = "task-a";
25
+ const client = {
26
+ connect: async () => undefined, close() { }, connected: () => true,
27
+ conversationId: async () => activeId,
28
+ historyAvailability: async () => ({ conversationId: activeId, waiting: false }),
29
+ readHistorySnapshot: async () => { calls.push("read"); return snapshot(); },
30
+ direct: async () => { calls.push("direct"); return "普通回复"; },
31
+ complete: async () => { calls.push("complete"); return { text: "普通回复" }; },
32
+ createTask: async () => { calls.push("createTask"); },
33
+ ...overrides,
34
+ };
35
+ const plugin = new workbuddy_1.WorkbuddyPlugin({ client });
36
+ await plugin.activate({ config, logger });
37
+ const history = plugin.sessionHistory;
38
+ strict_1.default.ok(history, "WorkBuddy 应公开受限原生历史 provider");
39
+ return { plugin, history, calls, client, setActiveId: (value) => { activeId = value; } };
40
+ }
41
+ (0, node_test_1.default)("WorkBuddy 原生历史只列出并读取当前任务,拒绝任意 ID 且不调用普通任务接口", async () => {
42
+ const { history, calls } = await fixture();
43
+ strict_1.default.equal(history.mode, "current");
44
+ const listed = await history.listSessions({ limit: 50 });
45
+ strict_1.default.equal(listed.supported, true);
46
+ if (!listed.supported)
47
+ return;
48
+ strict_1.default.deepEqual(listed.sessions.map(item => item.id), ["task-a"]);
49
+ strict_1.default.equal(listed.complete, false);
50
+ strict_1.default.equal((await history.readSession({ sessionId: "other", limit: 100 })).supported, false);
51
+ const read = await history.readSession({ sessionId: "task-a", limit: 100 });
52
+ strict_1.default.equal(read.supported, true);
53
+ if (!read.supported)
54
+ return;
55
+ strict_1.default.deepEqual(read.turns, [{ id: "u1", role: "user", text: "问题" }, { id: "a1", role: "assistant", text: "回答" }]);
56
+ strict_1.default.equal(read.complete, false);
57
+ strict_1.default.ok(read.missingReasons.some(reason => /完整|已加载/.test(reason)));
58
+ strict_1.default.deepEqual(calls, ["read"]);
59
+ });
60
+ (0, node_test_1.default)("WorkBuddy CLI、旧客户端、断线、等待选择与未列出的任务明确不支持读取", async () => {
61
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "wb-history-cli-"));
62
+ try {
63
+ const cliPath = (0, node_path_1.join)(root, "codebuddy.cjs");
64
+ await (0, promises_1.writeFile)(cliPath, "throw Error('不应运行 CLI');");
65
+ const cli = await fixture({}, { transport: "cli", cliPath });
66
+ strict_1.default.equal(cli.history.mode, "unsupported");
67
+ strict_1.default.equal((await cli.history.listSessions({ limit: 50 })).supported, false);
68
+ strict_1.default.deepEqual(cli.calls, []);
69
+ }
70
+ finally {
71
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
72
+ }
73
+ for (const overrides of [
74
+ { readHistorySnapshot: undefined },
75
+ { connected: () => false },
76
+ { pendingInteractionSessionId: () => "logical-a" },
77
+ { historyAvailability: async () => ({ conversationId: "task-a", waiting: true }) },
78
+ ]) {
79
+ const { history, calls } = await fixture(overrides);
80
+ strict_1.default.equal((await history.listSessions({ limit: 50 })).supported, false);
81
+ strict_1.default.deepEqual(calls, []);
82
+ }
83
+ const { history, calls } = await fixture({ readHistorySnapshot: async () => ({ ...snapshot(), messages: [{ id: "a1", role: "assistant", text: "回复", complete: true }] }) });
84
+ const missingUser = await history.listSessions({ limit: 50 });
85
+ strict_1.default.equal(missingUser.supported, true);
86
+ if (!missingUser.supported)
87
+ return;
88
+ const result = await history.readSession({ sessionId: missingUser.sessions[0].id, limit: 100 });
89
+ strict_1.default.equal(result.supported, true);
90
+ if (result.supported) {
91
+ strict_1.default.equal(result.complete, false);
92
+ strict_1.default.ok(result.missingReasons.some(reason => /用户消息/.test(reason)));
93
+ }
94
+ strict_1.default.deepEqual(calls, []);
95
+ });
96
+ (0, node_test_1.default)("WorkBuddy 任务忙时立即拒绝历史读取,历史读取和 handle 共用队列", async () => {
97
+ let release;
98
+ const waiting = new Promise(resolve => { release = resolve; });
99
+ const { plugin, history } = await fixture({ complete: async () => { await waiting; return { text: "完成" }; } });
100
+ const request = plugin.handle({ task: "执行任务" });
101
+ const busy = await history.listSessions({ limit: 50 });
102
+ strict_1.default.equal(busy.supported, false);
103
+ if (!busy.supported)
104
+ strict_1.default.match(busy.reason, /忙/);
105
+ release();
106
+ await request;
107
+ let releaseRead;
108
+ let entered;
109
+ const reading = new Promise(resolve => { releaseRead = resolve; });
110
+ const started = new Promise(resolve => { entered = resolve; });
111
+ const next = await fixture({ readHistorySnapshot: async () => { entered(); await reading; return snapshot(); } });
112
+ await next.history.listSessions({ limit: 50 });
113
+ const read = next.history.readSession({ sessionId: "task-a", limit: 100 });
114
+ await started;
115
+ const handle = next.plugin.handle({ task: "普通任务" });
116
+ strict_1.default.deepEqual(next.calls, []);
117
+ releaseRead();
118
+ strict_1.default.equal((await read).supported, true);
119
+ await handle;
120
+ strict_1.default.deepEqual(next.calls, ["complete"]);
121
+ });
122
+ (0, node_test_1.default)("WorkBuddy 读取前后身份变化及快照任务不一致时拒绝", async () => {
123
+ const current = await fixture();
124
+ await current.history.listSessions({ limit: 50 });
125
+ current.setActiveId("task-b");
126
+ strict_1.default.equal((await current.history.readSession({ sessionId: "task-a", limit: 100 })).supported, false);
127
+ strict_1.default.deepEqual(current.calls, []);
128
+ let reads = 0;
129
+ const changed = await fixture({ conversationId: async () => ++reads <= 3 ? "task-a" : "task-b", historyAvailability: async () => ({ conversationId: ++reads <= 3 ? "task-a" : "task-b", waiting: false }) });
130
+ await changed.history.listSessions({ limit: 50 });
131
+ strict_1.default.equal((await changed.history.readSession({ sessionId: "task-a", limit: 100 })).supported, false);
132
+ const mismatch = await fixture({ readHistorySnapshot: async () => ({ ...snapshot(), conversationId: "task-b" }) });
133
+ await mismatch.history.listSessions({ limit: 50 });
134
+ strict_1.default.equal((await mismatch.history.readSession({ sessionId: "task-a", limit: 100 })).supported, false);
135
+ });
136
+ (0, node_test_1.default)("WorkBuddy 没有用户证据和被排除的未完成回复时保留明确缺失原因", async () => {
137
+ const value = await fixture({ readHistorySnapshot: async () => ({ ...snapshot(), messages: snapshot().messages.filter(item => item.role === "assistant" && item.complete), missingReasons: ["未完成助手回复已排除。"] }) });
138
+ const listed = await value.history.listSessions({ limit: 50 });
139
+ strict_1.default.equal(listed.supported, true);
140
+ if (!listed.supported)
141
+ return;
142
+ const result = await value.history.readSession({ sessionId: listed.sessions[0].id, limit: 100 });
143
+ strict_1.default.equal(result.supported, true);
144
+ if (!result.supported)
145
+ return;
146
+ strict_1.default.equal(result.complete, false);
147
+ strict_1.default.ok(result.missingReasons.some(reason => /用户/.test(reason)));
148
+ strict_1.default.ok(result.missingReasons.some(reason => /未完成/.test(reason)));
149
+ });
150
+ (0, node_test_1.default)("WorkBuddy 分页上限、不可伪造游标及 60000 字符限制", async () => {
151
+ const value = await fixture({ readHistorySnapshot: async () => ({ ...snapshot(), messages: Array.from({ length: 3 }, (_, index) => ({ id: `u${index}`, role: "user", text: `问题${index}`, complete: true })) }) });
152
+ strict_1.default.equal((await value.history.listSessions({ limit: 51 })).supported, false);
153
+ strict_1.default.equal((await value.history.readSession({ sessionId: "task-a", limit: 101 })).supported, false);
154
+ strict_1.default.equal((await value.history.readSession({ sessionId: "task-a", limit: 0 })).supported, false);
155
+ strict_1.default.equal((await value.history.readSession({ sessionId: "task-a", limit: 1, cursor: "task-b" })).supported, false);
156
+ const listed = await value.history.listSessions({ limit: 50 });
157
+ strict_1.default.equal(listed.supported, true);
158
+ if (!listed.supported)
159
+ return;
160
+ const first = await value.history.readSession({ sessionId: listed.sessions[0].id, limit: 1 });
161
+ strict_1.default.equal(first.supported, true);
162
+ if (!first.supported)
163
+ return;
164
+ strict_1.default.equal(first.turns.length, 1);
165
+ strict_1.default.ok(first.nextCursor);
166
+ strict_1.default.ok(!first.nextCursor.includes("task-a"));
167
+ const second = await value.history.readSession({ sessionId: listed.sessions[0].id, limit: 100, cursor: first.nextCursor });
168
+ strict_1.default.equal(second.supported, true);
169
+ if (second.supported)
170
+ strict_1.default.deepEqual(second.turns.map(turn => turn.id), ["u1", "u2"]);
171
+ strict_1.default.deepEqual(value.calls, []);
172
+ const oversized = await fixture({ readHistorySnapshot: async () => ({ ...snapshot(), messages: [{ id: "u1", role: "user", text: "字".repeat(60_001), complete: true }] }) });
173
+ const oversizedList = await oversized.history.listSessions({ limit: 50 });
174
+ strict_1.default.equal(oversizedList.supported, true);
175
+ if (!oversizedList.supported)
176
+ return;
177
+ const result = await oversized.history.readSession({ sessionId: oversizedList.sessions[0].id, limit: 100 });
178
+ strict_1.default.equal(result.supported, false);
179
+ if (!result.supported)
180
+ strict_1.default.match(result.reason, /60000/);
181
+ });
182
+ (0, node_test_1.default)("WorkBuddy CDP 历史 DOM 只读排除推理、工具、附件、草稿及未完成正文", async () => {
183
+ const client = new cdp_client_1.WorkbuddyCdpClient();
184
+ const read = client.readHistorySnapshot;
185
+ strict_1.default.equal(typeof read, "function", "CDP 应提供单独的只读历史快照方法");
186
+ const removed = [];
187
+ const body = (text) => ({ cloneNode: () => ({ textContent: text, querySelectorAll: (selector) => { removed.push(selector); return []; } }) });
188
+ const message = (id, role, text, finished) => ({
189
+ getAttribute: (name) => name === "data-message-id" ? id : null,
190
+ matches: (selector) => selector.includes(".cr-agent") && role === "assistant",
191
+ querySelector: (selector) => selector === '[data-user-content]' ? role === "user" ? body(text) : null : selector === '[data-assistant-content]' ? role === "assistant" ? body(text) : null : selector.includes("conversation-finished-footer") ? finished ? {} : null : null,
192
+ });
193
+ const selected = { className: "_selected_", getAttribute: () => "task-a" };
194
+ const expressions = [];
195
+ const internal = client;
196
+ internal.evaluate = async (expression) => {
197
+ expressions.push(expression);
198
+ return (0, node_vm_1.runInNewContext)(expression, { document: {
199
+ querySelectorAll: (selector) => selector === '[data-conversation-id]' ? [selected] : selector.includes("data-message-id") ? [message("u1", "user", "问题", true), message("a1", "assistant", "回答", true), message("draft", "assistant", "草稿", false)] : [],
200
+ querySelector: () => null,
201
+ } });
202
+ };
203
+ const result = await read.call(client);
204
+ strict_1.default.deepEqual(JSON.parse(JSON.stringify(result.messages)), [{ id: "u1", role: "user", text: "问题", complete: true }, { id: "a1", role: "assistant", text: "回答", complete: true }]);
205
+ strict_1.default.ok(removed.some(selector => /cr-reasoning/.test(selector) && /tool/.test(selector) && /artifact/.test(selector)));
206
+ strict_1.default.ok(expressions.every(expression => !/\.click\(|\.focus\(|fetch\(|Input\.|dispatchEvent|createTask|complete\(/.test(expression)));
207
+ internal.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, { document: {
208
+ querySelectorAll: (selector) => selector === '[data-conversation-id]' ? [selected] : [],
209
+ querySelector: (selector) => selector.includes("pending-sandbox-panel") ? {} : null,
210
+ } });
211
+ const waiting = await read.call(client);
212
+ strict_1.default.match(waiting.unavailableReason ?? "", /等待用户选择/);
213
+ });
214
+ (0, node_test_1.default)("WorkBuddy 当前页面存在未登记的等待选择时拒绝读取", async () => {
215
+ let waiting = false;
216
+ const value = await fixture({ historyAvailability: async () => ({ conversationId: "task-a", waiting }) });
217
+ const client = value.client;
218
+ const listed = await value.history.listSessions({ limit: 50 });
219
+ strict_1.default.equal(listed.supported, true);
220
+ if (!listed.supported)
221
+ return;
222
+ waiting = true;
223
+ const result = await value.history.readSession({ sessionId: listed.sessions[0].id, limit: 100 });
224
+ strict_1.default.equal(result.supported, false);
225
+ if (!result.supported)
226
+ strict_1.default.match(result.reason, /等待用户选择/);
227
+ });
228
+ (0, node_test_1.default)("WorkBuddy 页面已等待选择时不列出任务", async () => {
229
+ const value = await fixture({ historyAvailability: async () => ({ conversationId: "task-a", waiting: true }) });
230
+ const listed = await value.history.listSessions({ limit: 50 });
231
+ strict_1.default.equal(listed.supported, false);
232
+ if (!listed.supported)
233
+ strict_1.default.match(listed.reason, /等待用户选择/);
234
+ });
235
+ (0, node_test_1.default)("WorkBuddy 分页期间出现等待选择时拒绝续页并清除缓存", async () => {
236
+ let waiting = false;
237
+ const value = await fixture({
238
+ historyAvailability: async () => ({ conversationId: "task-a", waiting }),
239
+ readHistorySnapshot: async () => ({ ...snapshot(), messages: Array.from({ length: 2 }, (_, index) => ({ id: `u${index}`, role: "user", text: `问题${index}`, complete: true })) }),
240
+ });
241
+ const listed = await value.history.listSessions({ limit: 50 });
242
+ strict_1.default.equal(listed.supported, true);
243
+ if (!listed.supported)
244
+ return;
245
+ const first = await value.history.readSession({ sessionId: listed.sessions[0].id, limit: 1 });
246
+ strict_1.default.equal(first.supported, true);
247
+ if (!first.supported || !first.nextCursor)
248
+ return;
249
+ waiting = true;
250
+ const rejected = await value.history.readSession({ sessionId: listed.sessions[0].id, limit: 1, cursor: first.nextCursor });
251
+ strict_1.default.equal(rejected.supported, false);
252
+ if (!rejected.supported)
253
+ strict_1.default.match(rejected.reason, /等待用户选择/);
254
+ waiting = false;
255
+ strict_1.default.equal((await value.history.readSession({ sessionId: listed.sessions[0].id, limit: 1, cursor: first.nextCursor })).supported, false);
256
+ });
@@ -110,6 +110,42 @@ const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
110
110
  internal.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, { document: { querySelectorAll: () => [message] }, URL, Uint8Array, btoa, fetch: async () => new Response(null, { status: 404 }) });
111
111
  await strict_1.default.rejects(() => internal.readArtifacts(new Set(["new"])), /产物读取失败/);
112
112
  });
113
+ (0, node_test_1.default)("workbuddy 不回传视频产物,保留图片和普通附件", async () => {
114
+ const client = new cdp_client_1.WorkbuddyCdpClient();
115
+ const fetched = [];
116
+ let cancelled = false;
117
+ const cards = ["file:///C:/movie.MP4", "file:///C:/movie.bin", "file:///C:/image.png"].map(url => ({ getAttribute: () => url }));
118
+ const message = { getAttribute: () => "new", querySelector: () => null, querySelectorAll: () => cards };
119
+ const internal = client;
120
+ internal.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, {
121
+ document: { querySelectorAll: () => [message] }, URL, Uint8Array, btoa,
122
+ fetch: async (url) => {
123
+ fetched.push(url);
124
+ if (url.endsWith('.bin'))
125
+ return { ok: true, headers: { get: () => 'video/mp4' }, body: { cancel: async () => { cancelled = true; }, getReader() { throw new Error('不应读取视频'); } } };
126
+ return new Response(new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10, 1]));
127
+ },
128
+ });
129
+ strict_1.default.deepEqual((await internal.readArtifacts(new Set(['new']))).map(item => item.filename), ['image.png']);
130
+ strict_1.default.deepEqual(fetched, ['file:///C:/movie.bin', 'file:///C:/image.png']);
131
+ strict_1.default.equal(cancelled, true);
132
+ });
133
+ (0, node_test_1.default)("workbuddy 默认允许长任务等待一小时并遵守显式超时", async () => {
134
+ const timeouts = [];
135
+ const plugin = new workbuddy_1.WorkbuddyPlugin({ client: {
136
+ connect: async () => undefined, close() { }, connected: () => true,
137
+ direct: async () => '', complete: async (request) => { timeouts.push(request.timeoutMs); return { text: '已完成' }; },
138
+ } });
139
+ await plugin.activate({ config: { transport: 'cdp' }, logger: { info() { }, warn() { }, error() { } } });
140
+ try {
141
+ await plugin.handle({ task: '生成视频' });
142
+ await plugin.handle({ task: '继续', timeoutMs: 1234 });
143
+ strict_1.default.deepEqual(timeouts, [3_600_000, 1234]);
144
+ }
145
+ finally {
146
+ await plugin.deactivate('stop');
147
+ }
148
+ });
113
149
  (0, node_test_1.default)("workbuddy CDP 读取当前产物面板隐藏文件时排除旧轮次与其他任务", async () => {
114
150
  const client = new cdp_client_1.WorkbuddyCdpClient();
115
151
  const fetched = [];
@@ -710,10 +746,17 @@ const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
710
746
  const client = new cdp_client_1.WorkbuddyCdpClient({ timeoutMs: 1 });
711
747
  const evaluator = client;
712
748
  evaluator.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, { document, location: { href: "app://workbuddy/task/old" } });
749
+ evaluator.pendingInteraction = { prompt: "请选择项目", options: ["项目甲"], sessionId: "old-session" };
750
+ evaluator.pendingInteractionCleanup = ["C:/Temp/旧附件.png"];
751
+ const cleaned = [];
752
+ evaluator.cleanupTemporaryFiles = async (paths) => { cleaned.push(...paths); };
713
753
  if (succeeds)
714
754
  await client.createTask();
715
755
  else
716
756
  await strict_1.default.rejects(() => client.createTask(), /新任务状态校验失败/);
757
+ strict_1.default.equal(client.pendingInteractionSessionId(), succeeds ? undefined : "old-session");
758
+ strict_1.default.deepEqual(cleaned, succeeds ? ["C:/Temp/旧附件.png"] : []);
759
+ strict_1.default.deepEqual(evaluator.pendingInteractionCleanup, succeeds ? undefined : ["C:/Temp/旧附件.png"]);
717
760
  }
718
761
  });
719
762
  (0, node_test_1.default)("workbuddy CDP 新建接受旧任务退回无 ID 落地页及已在落地页的启动", async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mu-agent-bridge",
3
- "version": "1.0.34",
3
+ "version": "1.0.39",
4
4
  "description": "将 AUN Agent 连接到本地桌面 AI 客户端的插件平台。",
5
5
  "main": "dist/src-ts/cli.js",
6
6
  "bin": {
@@ -1,10 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AunDownloadUrlError = void 0;
3
4
  exports.parseAunAttachment = parseAunAttachment;
4
5
  exports.downloadAunAttachments = downloadAunAttachments;
5
6
  exports.downloadAunAttachment = downloadAunAttachment;
6
7
  exports.uploadAunAttachments = uploadAunAttachments;
7
8
  const node_crypto_1 = require("node:crypto");
9
+ const node_util_1 = require("node:util");
8
10
  const attachments_1 = require("../../attachments");
9
11
  const aidPattern = /^[a-z0-9][a-z0-9_-]{2,63}\.[a-z0-9-]+(?:\.[a-z0-9-]+)+$/;
10
12
  const mimePattern = /^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+$/;
@@ -12,6 +14,21 @@ const imageTypes = new Set(["image/png", "image/jpeg", "image/webp"]);
12
14
  function digest(data) { return (0, node_crypto_1.createHash)("sha256").update(data).digest("hex"); }
13
15
  function validKey(key) { return /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(key) && !key.split("/").some((part) => part === "." || part === ".."); }
14
16
  function validHash(value) { return /^[a-f0-9]{64}$/i.test(value); }
17
+ function normalizedMime(value) {
18
+ if (value === undefined)
19
+ return;
20
+ if (typeof value !== "string" || /[\u0000-\u001f\u007f]/.test(value))
21
+ throw new Error("AUN 附件 MIME 类型无效");
22
+ if (!value.trim())
23
+ return;
24
+ try {
25
+ const type = new node_util_1.MIMEType(value).essence;
26
+ if (mimePattern.test(type))
27
+ return type;
28
+ }
29
+ catch { /* 统一报告无效类型。 */ }
30
+ throw new Error("AUN 附件 MIME 类型无效");
31
+ }
15
32
  function validateReference(ref) {
16
33
  if (!aidPattern.test(ref.ownerAid) || !/^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/.test(ref.bucket) || !validKey(ref.objectKey))
17
34
  throw new Error("AUN 附件引用路径无效");
@@ -43,11 +60,9 @@ function parseAunAttachment(value) {
43
60
  const bucket = item.bucket === undefined ? "default" : typeof item.bucket === "string" ? item.bucket : "";
44
61
  const filename = (0, attachments_1.attachmentFilename)(typeof item.filename === "string" ? item.filename : objectKey);
45
62
  const ref = { ownerAid, bucket, objectKey, filename };
46
- if (item.content_type !== undefined) {
47
- if (typeof item.content_type !== "string")
48
- throw new Error("AUN 附件 MIME 类型无效");
49
- ref.contentType = item.content_type.toLowerCase();
50
- }
63
+ const contentType = normalizedMime(item.content_type);
64
+ if (contentType)
65
+ ref.contentType = contentType;
51
66
  if (item.size_bytes !== undefined) {
52
67
  if (typeof item.size_bytes !== "number")
53
68
  throw new Error("AUN 附件大小无效");
@@ -81,10 +96,8 @@ function checkedMetadata(meta, ref) {
81
96
  if ([meta.owner, meta.owner_aid].some((owner) => owner && owner !== ref.ownerAid) || meta.bucket && meta.bucket !== ref.bucket
82
97
  || [meta.path, meta.object_key].some((path) => path && path !== `/${ref.objectKey}` && path !== ref.objectKey))
83
98
  throw new Error("AUN 附件元数据引用不一致");
84
- const rawType = metadataValue(meta, "contentType", "content_type");
85
- if (rawType && (typeof rawType !== "string" || !mimePattern.test(rawType)))
86
- throw new Error("AUN 附件元数据 MIME 类型无效");
87
- const contentType = typeof rawType === "string" && rawType ? rawType.toLowerCase() : ref.contentType ?? "application/octet-stream";
99
+ const rawType = normalizedMime(metadataValue(meta, "contentType", "content_type"));
100
+ const contentType = rawType ?? ref.contentType ?? "application/octet-stream";
88
101
  if (ref.contentType && rawType && ref.contentType.toLowerCase() !== contentType)
89
102
  throw new Error("AUN 附件 MIME 类型校验失败");
90
103
  const sha256 = typeof meta.sha256 === "string" && meta.sha256 ? meta.sha256.toLowerCase() : undefined;
@@ -162,9 +175,20 @@ async function downloadAunAttachments(client, refs) {
162
175
  (0, attachments_1.validateAttachments)(result);
163
176
  return result;
164
177
  }
178
+ class AunDownloadUrlError extends Error {
179
+ diagnostic;
180
+ constructor(url, reason) {
181
+ super("AUN 下载地址无效");
182
+ this.name = "AunDownloadUrlError";
183
+ this.diagnostic = { protocol: url.protocol, hostname: url.hostname, port: url.port, reason };
184
+ }
185
+ }
186
+ exports.AunDownloadUrlError = AunDownloadUrlError;
165
187
  function checkedDownloadUrl(url) {
166
- if (url.protocol !== "https:" || url.username || url.password || url.port || url.hash || !url.hostname.includes(".") || /^(?:localhost|127\.|0\.|10\.|192\.168\.|169\.254\.|172\.(?:1[6-9]|2\d|3[01])\.)/.test(url.hostname) || url.hostname.includes(":"))
167
- throw new Error("AUN 下载地址无效");
188
+ const storagePort = url.port === "9470" && /^storage\.[a-z0-9-]+(?:\.[a-z0-9-]+)+$/i.test(url.hostname);
189
+ const reason = url.protocol !== "https:" ? "协议" : url.username || url.password ? "凭据" : url.port && !storagePort ? "端口" : url.hash ? "片段" : !url.hostname.includes(".") ? "主机名" : /^(?:localhost|127\.|0\.|10\.|192\.168\.|169\.254\.|172\.(?:1[6-9]|2\d|3[01])\.)/.test(url.hostname) ? "本机地址" : url.hostname.includes(":") ? "主机名" : undefined;
190
+ if (reason)
191
+ throw new AunDownloadUrlError(url, reason);
168
192
  return url;
169
193
  }
170
194
  async function readDownloadTicket(value, expectedSize) {
@@ -224,9 +248,16 @@ async function uploadAunAttachments(client, ownerAid, peerAid, items) {
224
248
  const filename = (0, attachments_1.attachmentFilename)(item.filename);
225
249
  const objectKey = `shared/${(0, node_crypto_1.randomUUID)()}/${filename.replace(/[^A-Za-z0-9._-]/g, "_")}`;
226
250
  const sha256 = digest(item.data);
227
- const ref = { ownerAid, bucket: "default", objectKey, filename, contentType: item.contentType.toLowerCase(), sizeBytes: item.data.byteLength, sha256 };
228
- const uploaded = await client.storage.writeBytes(`/${objectKey}`, item.data, { contentType: item.contentType, public: false, overwrite: false });
251
+ const contentType = item.contentType.toLowerCase();
252
+ // 视频播放器不携带 AID 凭据,沿用 EvolCore 的标准可读对象上传方式。
253
+ const video = contentType.startsWith("video/");
254
+ const ref = { ownerAid, bucket: "default", objectKey, filename, contentType, sizeBytes: item.data.byteLength, sha256 };
255
+ const uploaded = await client.storage.writeBytes(`/${objectKey}`, item.data, { contentType, public: video, overwrite: video });
229
256
  checkedMetadata({ ...uploaded, size: uploaded.size ?? uploaded.size_bytes ?? item.data.byteLength }, ref);
257
+ if (video) {
258
+ result.push({ owner_aid: ownerAid, object_key: objectKey, filename, content_type: contentType, size_bytes: item.data.byteLength, sha256 });
259
+ continue;
260
+ }
230
261
  const share = await client.storage.lowlevel.createShareLink({ owner: ownerAid, bucket: "default", objectKey, allowedAids: [peerAid], expireInSeconds: 86400 });
231
262
  if (share.owner_aid && share.owner_aid !== ownerAid || share.bucket && share.bucket !== "default" || share.object_key && share.object_key !== objectKey)
232
263
  throw new Error("AUN 附件分享对象不一致");
@@ -11,7 +11,7 @@ const node_url_1 = require("node:url");
11
11
  const messages_1 = require("./messages");
12
12
  const attachments_1 = require("./attachments");
13
13
  const manifest = {
14
- id: "aun", version: "1.0.23", type: "channel", displayName: "AUN 通道",
14
+ id: "aun", version: "1.0.30", type: "channel", displayName: "AUN 通道",
15
15
  entry: "dist/src-ts/plugins/aun/index.js", apiVersion: "1", process: "in-process",
16
16
  capabilities: ["channel", "channel.identity.create", "channel.receive", "channel.send", "health", "cli"],
17
17
  configSchema: "config.schema.json",
@@ -783,7 +783,7 @@ class AunPlugin {
783
783
  }
784
784
  }
785
785
  catch (error) {
786
- this.context?.logger.warn("AUN 附件下载失败", { error: safeError(error) });
786
+ this.context?.logger.warn("AUN 附件下载失败", { error: safeError(error), ...(error instanceof attachments_1.AunDownloadUrlError ? { downloadUrl: error.diagnostic } : {}) });
787
787
  try {
788
788
  await this.sendText(aid, message.peerId, `附件接收失败:${safeError(error)}`, message.threadId);
789
789
  }
@@ -817,15 +817,14 @@ class AunPlugin {
817
817
  }
818
818
  const groups = new Map();
819
819
  for (const [index, item] of items.entries()) {
820
- const kind = item.kind;
821
- const ref = refs[index];
820
+ const kind = item.kind === "file" && item.contentType.toLowerCase().startsWith("video/") ? "video" : item.kind;
822
821
  const group = groups.get(kind) ?? [];
823
- group.push(ref);
822
+ group.push(refs[index]);
824
823
  groups.set(kind, group);
825
824
  }
826
825
  groupsCount = groups.size;
827
826
  for (const [kind, group] of groups) {
828
- const payload = { type: kind, text: text || (kind === "image" ? "图片已生成。" : "文件已生成。"), attachments: group };
827
+ const payload = { type: kind, text: text || (kind === "image" ? "图片已生成。" : kind === "video" ? "视频已生成。" : "文件已生成。"), attachments: group };
829
828
  if (message.threadId)
830
829
  payload.thread_id = message.threadId;
831
830
  if (message.messageId)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-bridge-aun",
3
- "version": "1.0.23",
3
+ "version": "1.0.30",
4
4
  "description": "mu-agent-bridge 的 AUN 通道插件。",
5
5
  "keywords": [
6
6
  "mu-agent-bridge-plugin",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "aun",
3
- "version": "1.0.23",
3
+ "version": "1.0.30",
4
4
  "type": "channel",
5
5
  "displayName": "AUN 通道",
6
6
  "entry": "dist/src-ts/plugins/aun/index.js",
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_TOTAL_ATTACHMENT_BYTES = exports.MAX_ATTACHMENTS = exports.MAX_ATTACHMENT_BYTES = void 0;
4
+ exports.detectImageContentType = detectImageContentType;
5
+ exports.attachmentFilename = attachmentFilename;
6
+ exports.validateAttachment = validateAttachment;
7
+ exports.validateAttachments = validateAttachments;
8
+ exports.asAttachments = asAttachments;
9
+ exports.MAX_ATTACHMENT_BYTES = 32 * 1024 * 1024;
10
+ exports.MAX_ATTACHMENTS = 8;
11
+ exports.MAX_TOTAL_ATTACHMENT_BYTES = 64 * 1024 * 1024;
12
+ function detectImageContentType(data) {
13
+ if (data.length >= 8 && data.slice(0, 8).every((value, index) => value === [137, 80, 78, 71, 13, 10, 26, 10][index]))
14
+ return "image/png";
15
+ if (data.length >= 3 && data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff)
16
+ return "image/jpeg";
17
+ if (data.length >= 12 && String.fromCharCode(...data.slice(0, 4)) === "RIFF" && String.fromCharCode(...data.slice(8, 12)) === "WEBP")
18
+ return "image/webp";
19
+ return undefined;
20
+ }
21
+ function attachmentFilename(filename) {
22
+ if (typeof filename !== "string" || /[\u0000-\u001f\u007f]/.test(filename))
23
+ throw new Error("附件文件名无效");
24
+ const name = filename.split(/[\\/]/).pop() ?? "";
25
+ if (!name.trim() || name === "." || name === "..")
26
+ throw new Error("附件文件名无效");
27
+ return name;
28
+ }
29
+ function validateAttachment(item) {
30
+ if (!item || !(item.data instanceof Uint8Array) || item.data.byteLength === 0)
31
+ throw new Error("附件内容不能为空");
32
+ if (item.data.byteLength > exports.MAX_ATTACHMENT_BYTES)
33
+ throw new Error("单个附件不能超过 32 MiB");
34
+ attachmentFilename(item.filename);
35
+ if (item.kind !== "image" && item.kind !== "file")
36
+ throw new Error("附件类型无效");
37
+ if (typeof item.contentType !== "string" || !/^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+$/.test(item.contentType))
38
+ throw new Error("附件 MIME 类型无效");
39
+ if (item.kind === "image") {
40
+ const detected = detectImageContentType(item.data);
41
+ if (!detected || detected !== item.contentType.toLowerCase())
42
+ throw new Error("图片内容与 MIME 类型不一致");
43
+ }
44
+ }
45
+ function validateAttachments(items) {
46
+ if (!items)
47
+ return;
48
+ if (!Array.isArray(items))
49
+ throw new Error("附件列表无效");
50
+ if (items.length > exports.MAX_ATTACHMENTS)
51
+ throw new Error("单条消息最多包含 8 个附件");
52
+ let total = 0;
53
+ for (const item of items) {
54
+ validateAttachment(item);
55
+ total += item.data.byteLength;
56
+ }
57
+ if (total > exports.MAX_TOTAL_ATTACHMENT_BYTES)
58
+ throw new Error("单条消息附件总大小不能超过 64 MiB");
59
+ }
60
+ function asAttachments(images) {
61
+ return (images ?? []).map((image) => ({ ...image, kind: "image" }));
62
+ }