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,197 @@
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 node_vm_1 = require("node:vm");
9
+ const chatgpt_1 = require("../src-ts/plugins/chatgpt");
10
+ const logger = { info() { }, warn() { }, error() { } };
11
+ const active = { id: "local:one", host: "h", title: "当前任务" };
12
+ const sourceId = (host = active.host, id = active.id) => JSON.stringify([host, id]);
13
+ function history(plugin) {
14
+ const provider = plugin.sessionHistory;
15
+ strict_1.default.ok(provider, "插件应提供原生历史能力描述");
16
+ return provider;
17
+ }
18
+ function fixture() {
19
+ let connected = true, generating = false;
20
+ let selected = active;
21
+ let sessions = Array.from({ length: 55 }, (_, i) => ({ ...active, id: i ? `local:${i}` : active.id }));
22
+ let turns = Array.from({ length: 105 }, (_, i) => ({ role: i % 2 ? "assistant" : "user", text: `正文${i}` }));
23
+ let readCount = 0;
24
+ const forbidden = () => { strict_1.default.fail("历史读取不得发送、切换或新建会话"); };
25
+ const client = {
26
+ connect: async () => undefined, close() { }, connected: () => connected,
27
+ direct: async () => forbidden(), switchSession: async () => forbidden(), createSession: async () => forbidden(),
28
+ listSessions: async () => sessions, activeSession: async () => selected,
29
+ historyBusy: async () => generating,
30
+ readSessionHistory: async () => { readCount++; return { supported: true, turns, complete: false, missingReasons: [] }; },
31
+ };
32
+ const plugin = new chatgpt_1.ChatgptPlugin({ client });
33
+ return { plugin, client, setConnected: (value) => connected = value, setGenerating: (value) => generating = value,
34
+ setSelected: (value) => selected = value, setTurns: (value) => turns = value,
35
+ setSessions: (value) => sessions = value, reads: () => readCount };
36
+ }
37
+ (0, node_test_1.default)("ChatGPT 原生历史列举已加载侧栏并分页,读取当前会话正文且永不走发送路径", async () => {
38
+ const f = fixture();
39
+ await f.plugin.activate({ config: {}, logger });
40
+ const p = history(f.plugin);
41
+ strict_1.default.equal(p.mode, "sidebar");
42
+ const first = await p.listSessions({ limit: 999 });
43
+ strict_1.default.equal(first.supported, true);
44
+ if (!first.supported)
45
+ return;
46
+ strict_1.default.equal(first.sessions.length, 50);
47
+ strict_1.default.equal(first.sessions[0].id, sourceId());
48
+ strict_1.default.equal(first.complete, false);
49
+ strict_1.default.ok(first.missingReasons.some(reason => /侧栏/.test(reason)));
50
+ const second = await p.listSessions({ limit: 50, cursor: first.nextCursor });
51
+ strict_1.default.equal(second.supported && second.sessions.length, 5);
52
+ const body = await p.readSession({ sessionId: sourceId(), limit: 999 });
53
+ strict_1.default.equal(body.supported, true);
54
+ if (!body.supported)
55
+ return;
56
+ strict_1.default.equal(body.turns.length, 100);
57
+ strict_1.default.equal(body.complete, false);
58
+ const tail = await p.readSession({ sessionId: sourceId(), limit: 100, cursor: body.nextCursor });
59
+ strict_1.default.equal(tail.supported && tail.turns.length, 5);
60
+ strict_1.default.equal(tail.supported && tail.nextCursor, undefined);
61
+ const other = await p.readSession({ sessionId: sourceId("h", "local:1"), limit: 100 });
62
+ strict_1.default.equal(other.supported, false);
63
+ strict_1.default.equal(f.reads(), 2);
64
+ await f.plugin.deactivate();
65
+ });
66
+ (0, node_test_1.default)("ChatGPT 旧客户端明确不支持,scope 隔离端点且不含私钥路径", async () => {
67
+ const client = { connect: async () => undefined, close() { }, connected: () => true, direct: async () => "" };
68
+ const local = new chatgpt_1.ChatgptPlugin({ client });
69
+ const remote = new chatgpt_1.ChatgptPlugin({ client });
70
+ await local.activate({ config: {}, logger });
71
+ await remote.activate({ config: { sshHost: "server", sshUser: "user", sshKeyPath: "/秘密/私钥" }, logger });
72
+ strict_1.default.equal(history(local).mode, "unsupported");
73
+ strict_1.default.equal((await history(local).listSessions({ limit: 50 })).supported, false);
74
+ strict_1.default.notEqual(history(local).scope, history(remote).scope);
75
+ strict_1.default.ok(!history(remote).scope.includes("秘密"));
76
+ await local.deactivate();
77
+ await remote.deactivate();
78
+ });
79
+ (0, node_test_1.default)("ChatGPT 历史读取拒绝断线、生成中、未知身份和伪造游标,超限不截断", async () => {
80
+ const f = fixture();
81
+ await f.plugin.activate({ config: {}, logger });
82
+ const p = history(f.plugin);
83
+ f.setConnected(false);
84
+ strict_1.default.equal((await p.listSessions({ limit: 50 })).supported, false);
85
+ f.setConnected(true);
86
+ f.setGenerating(true);
87
+ strict_1.default.equal((await p.readSession({ sessionId: sourceId(), limit: 100 })).supported, false);
88
+ strict_1.default.equal((await p.listSessions({ limit: 50 })).supported, false);
89
+ f.setGenerating(false);
90
+ strict_1.default.equal((await p.readSession({ sessionId: sourceId("foreign-host"), limit: 100 })).supported, false);
91
+ f.setSelected(undefined);
92
+ strict_1.default.equal((await p.readSession({ sessionId: sourceId(), limit: 100 })).supported, false);
93
+ f.setSelected(active);
94
+ await strict_1.default.rejects(p.listSessions({ limit: 50, cursor: "1" }), /游标/);
95
+ await strict_1.default.rejects(p.readSession({ sessionId: sourceId(), limit: 0 }), /limit|条数/);
96
+ f.setTurns([{ role: "assistant", text: "长".repeat(60_001) }]);
97
+ await strict_1.default.rejects(p.readSession({ sessionId: sourceId(), limit: 100 }), /60,?000|60000|上限/);
98
+ strict_1.default.equal(f.reads(), 1);
99
+ await f.plugin.deactivate();
100
+ });
101
+ (0, node_test_1.default)("ChatGPT 翻页拒绝内容变化与跨查询游标,读取前后活动身份必须一致", async () => {
102
+ const f = fixture();
103
+ await f.plugin.activate({ config: {}, logger });
104
+ const p = history(f.plugin);
105
+ const page = await p.listSessions({ limit: 1 });
106
+ strict_1.default.ok(page.supported && page.nextCursor);
107
+ if (!page.supported)
108
+ return;
109
+ await strict_1.default.rejects(p.readSession({ sessionId: sourceId(), limit: 100, cursor: page.nextCursor }), /游标/);
110
+ f.setSessions([{ ...active, title: "列表已经变化" }]);
111
+ await strict_1.default.rejects(p.listSessions({ limit: 50, cursor: page.nextCursor }), /变化|游标/);
112
+ f.client.readSessionHistory = async () => { f.setSelected({ ...active, host: "other" }); return { supported: true, turns: [{ role: "assistant", text: "不应泄漏" }], complete: false, missingReasons: [] }; };
113
+ const changed = await p.readSession({ sessionId: sourceId(), limit: 100 });
114
+ strict_1.default.equal(changed.supported, false);
115
+ await f.plugin.deactivate();
116
+ });
117
+ (0, node_test_1.default)("ChatGPT 普通发送和会话控制共用忙碌边界,历史请求立即拒绝且不插队", async () => {
118
+ for (const operation of ["handle", "control"]) {
119
+ const f = fixture();
120
+ let release;
121
+ const pending = new Promise(resolve => release = resolve);
122
+ if (operation === "handle")
123
+ f.client.direct = async () => { await pending; return "完成"; };
124
+ else
125
+ f.client.createSession = async () => { await pending; return undefined; };
126
+ await f.plugin.activate({ config: {}, logger });
127
+ const running = operation === "handle" ? f.plugin.handle({ task: "执行" }) : f.plugin.controlSession({ operation: "new", sessionId: "s" });
128
+ strict_1.default.equal((await history(f.plugin).listSessions({ limit: 50 })).supported, false);
129
+ strict_1.default.equal(f.reads(), 0);
130
+ release();
131
+ await running;
132
+ strict_1.default.equal((await history(f.plugin).listSessions({ limit: 50 })).supported, true);
133
+ await f.plugin.deactivate();
134
+ }
135
+ });
136
+ (0, node_test_1.default)("ChatGPT CDP 历史脚本只读真实正文选择器,排除推理工具草稿和未确认助手内容", async () => {
137
+ const client = new chatgpt_1.ChatgptCdpClient();
138
+ const annotation = (conversation, target) => ({ getAttribute: (name) => name.endsWith("conversation") ? conversation : target });
139
+ const user = { innerText: "问题\n展开", querySelector: () => ({ innerText: "问题" }), closest: () => null };
140
+ const assistant = (text, conversation = "one", identified = true) => ({
141
+ innerText: text, closest: (selector) => selector.includes("data-response-annotation") && identified ? annotation(conversation, "m") : null,
142
+ querySelector: () => null,
143
+ });
144
+ const turn = {
145
+ getAttribute: () => "turn-one",
146
+ closest: () => ({ getAttribute: (name) => name.endsWith("host-id") ? active.host : name.endsWith("thread-id") ? active.id : active.title }),
147
+ querySelectorAll: (selector) => selector === "[data-user-message-bubble]" ? [user]
148
+ : selector === '[data-markdown-text-style="assistant-message"]' ? [assistant("最终答复"), assistant("其他会话", "other"), assistant("未确认", "one", false)] : [],
149
+ };
150
+ let generating = false;
151
+ const expressions = [];
152
+ const document = {
153
+ querySelector: (selector) => selector.includes("sidebar-thread-selected") ? { getAttribute: (name) => name.endsWith("host-id") ? active.host : name.endsWith("thread-id") ? active.id : active.title }
154
+ : selector.includes("Stop") && generating ? {} : null,
155
+ querySelectorAll: (selector) => selector === '[data-chatgpt-conversation-turn="true"]' ? [turn] : [],
156
+ };
157
+ Object.assign(client, { connected: () => true, evaluate: async (expression) => {
158
+ expressions.push(expression);
159
+ strict_1.default.doesNotMatch(expression, /\.click\(|\.focus\(|Input\.|fetch\(|import\(|workspaceFiles|innerText\s*=/);
160
+ return (0, node_vm_1.runInNewContext)(expression, { document });
161
+ } });
162
+ const read = () => client.readSessionHistory(active);
163
+ const result = await read();
164
+ strict_1.default.equal(result.supported, true);
165
+ strict_1.default.deepEqual(JSON.parse(JSON.stringify(result.turns)).map((turn) => [turn.role, turn.text]), [["user", "问题"], ["assistant", "最终答复"]]);
166
+ strict_1.default.ok(expressions.some(value => value.includes("data-markdown-text-tone")));
167
+ generating = true;
168
+ strict_1.default.equal((await read()).supported, false);
169
+ generating = false;
170
+ strict_1.default.equal((await client.readSessionHistory({ ...active, host: "forged" })).supported, false);
171
+ });
172
+ (0, node_test_1.default)("ChatGPT 历史正文缺少专用用户正文或 turn 身份时跳过,不采集附件与控件文案", async () => {
173
+ const client = new chatgpt_1.ChatgptCdpClient();
174
+ const activeSession = { id: "local:one", host: "h", title: "当前任务" };
175
+ const messages = [
176
+ { id: "turn-valid", body: null },
177
+ { id: null, body: { innerText: "不可信正文" } },
178
+ ];
179
+ const document = {
180
+ querySelector: (selector) => selector.includes("sidebar-thread-selected") ? {
181
+ getAttribute: (name) => name.endsWith("host-id") ? "h" : name.endsWith("thread-id") ? "local:one" : "当前任务",
182
+ } : null,
183
+ querySelectorAll: (selector) => selector === '[data-chatgpt-conversation-turn="true"]' ? messages.map(message => ({
184
+ getAttribute: (name) => name === "data-chatgpt-conversation-turn-id" ? message.id : null,
185
+ querySelectorAll: (nested) => nested === "[data-user-message-bubble]" ? [{
186
+ innerText: "附件秘密 + 控件文案",
187
+ querySelector: () => message.body,
188
+ }] : [],
189
+ })) : [],
190
+ };
191
+ Object.assign(client, { connected: () => true, evaluate: async (expression) => (0, node_vm_1.runInNewContext)(expression, { document }) });
192
+ const result = await client.readSessionHistory(activeSession);
193
+ strict_1.default.equal(result.supported, true);
194
+ strict_1.default.equal(result.turns.length, 0);
195
+ strict_1.default.ok(result.missingReasons.some(reason => /专用正文/.test(reason)), JSON.stringify(result.missingReasons));
196
+ strict_1.default.ok(result.missingReasons.some(reason => /身份/.test(reason)));
197
+ });
@@ -268,7 +268,7 @@ const chatgpt_1 = require("../src-ts/plugins/chatgpt");
268
268
  states++;
269
269
  value = JSON.stringify(states === 1
270
270
  ? { reply: "", active: true }
271
- : { reply: "本轮回复", active: false });
271
+ : { reply: "本轮回复", active: false, artifacts: [{ conversationId: "current", messageId: "new" }], hasArtifacts: false });
272
272
  }
273
273
  else if (expression.includes("data-chatgpt-conversation-turn-id") && expression.startsWith("JSON.stringify"))
274
274
  value = "[]";
@@ -295,7 +295,7 @@ const chatgpt_1 = require("../src-ts/plugins/chatgpt");
295
295
  await client.connect();
296
296
  try {
297
297
  strict_1.default.equal(await client.direct("你好", 5_000), "本轮回复");
298
- strict_1.default.equal(sentText, "你好");
298
+ strict_1.default.ok(sentText.startsWith("你好 附件回传约定:"));
299
299
  strict_1.default.ok(expressions.some((expression) => expression.includes("data-codex-composer")));
300
300
  strict_1.default.ok(expressions.some((expression) => expression.includes("data-chatgpt-conversation-turn-id")));
301
301
  strict_1.default.ok(expressions.some((expression) => expression.includes("data-user-message-bubble")));
@@ -483,3 +483,280 @@ const chatgpt_1 = require("../src-ts/plugins/chatgpt");
483
483
  await rm(dir, { recursive: true, force: true });
484
484
  }
485
485
  });
486
+ (0, node_test_1.default)("ChatGPT 转发输入附件并保留结构化输出附件", async () => {
487
+ const attachment = { kind: "file", data: new TextEncoder().encode("回归标记"), filename: "附件.txt", contentType: "text/plain" };
488
+ let received;
489
+ const client = { connect: async () => undefined, close() { }, connected: () => true, attachmentInput: true,
490
+ direct: async (_text, _timeout, attachments) => { received = attachments; return { text: "完成", attachments: [attachment] }; } };
491
+ const plugin = new chatgpt_1.ChatgptPlugin({ client });
492
+ await plugin.activate({ config: {}, logger: { info() { }, warn() { }, error() { } } });
493
+ try {
494
+ strict_1.default.equal(plugin.attachmentInput, true);
495
+ strict_1.default.deepEqual(await plugin.handle({ task: "复制附件", attachments: [attachment], sessionId: "media" }), { text: "完成", attachments: [attachment], sessionId: "media" });
496
+ strict_1.default.deepEqual(received, [attachment]);
497
+ }
498
+ finally {
499
+ await plugin.deactivate();
500
+ }
501
+ });
502
+ (0, node_test_1.default)("ChatGPT 附件链接只接受绝对本地路径并拒绝路径穿越", async () => {
503
+ const { chatgptAttachmentPath: parse } = await Promise.resolve().then(() => __importStar(require("../src-ts/plugins/chatgpt/cdp-client")));
504
+ const chatgptAttachmentPath = (href) => parse(href, "agent-bridge-chatgpt-test");
505
+ strict_1.default.equal(chatgptAttachmentPath("file:///tmp/agent-bridge-chatgpt-test/中文%20附件.mp4"), "/tmp/agent-bridge-chatgpt-test/中文 附件.mp4");
506
+ strict_1.default.equal(chatgptAttachmentPath("file:///C:/Users/test/AppData/Local/Temp/agent-bridge-chatgpt-test/output.png"), "C:/Users/test/AppData/Local/Temp/agent-bridge-chatgpt-test/output.png");
507
+ strict_1.default.equal(chatgptAttachmentPath("C:/Users/test/AppData/Local/Temp/agent-bridge-chatgpt-test/output.png"), "C:/Users/test/AppData/Local/Temp/agent-bridge-chatgpt-test/output.png");
508
+ strict_1.default.equal(chatgptAttachmentPath("sandbox:/tmp/agent-bridge-chatgpt-test/result.txt"), "/tmp/agent-bridge-chatgpt-test/result.txt");
509
+ for (const href of ["https://example.test/file", "file://server/share/file", "file:///tmp/../secret", "file:///tmp/%2e%2e/secret", "/tmp/%252e%252e/secret", "file:///tmp/file?token=secret", "file:///tmp/secret%00.txt", "file:///etc/passwd", "file:///home/user/.ssh/id_rsa", "../secret", "//server/file"]) {
510
+ strict_1.default.equal(chatgptAttachmentPath(href), undefined, href);
511
+ }
512
+ });
513
+ (0, node_test_1.default)("ChatGPT 按本轮会话和消息读取文件、图片、视频,先检查大小", async () => {
514
+ const { runInNewContext } = await Promise.resolve().then(() => __importStar(require("node:vm")));
515
+ const pngPath = "C:/Users/test/AppData/Local/Temp/agent-bridge-chatgpt-test/result.png";
516
+ const files = new Map([
517
+ ["/tmp/agent-bridge-chatgpt-test/result.txt", Buffer.from("附件标记")],
518
+ [pngPath, Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII=", "base64")],
519
+ ["/tmp/agent-bridge-chatgpt-test/result.mp4", Buffer.alloc(5 * 1024 * 1024, 0x5a)],
520
+ ]);
521
+ const calls = [];
522
+ let oversized = false;
523
+ let changedSize = false;
524
+ const requestIds = new Set();
525
+ const listeners = new Set();
526
+ const nativeWindow = {
527
+ addEventListener: (_name, receive) => listeners.add(receive),
528
+ removeEventListener: (_name, receive) => listeners.delete(receive),
529
+ electronBridge: { sendMessageFromView(request) {
530
+ const name = request.url.split("/").at(-1);
531
+ const params = JSON.parse(request.body);
532
+ strict_1.default.equal(name, "read-file-metadata");
533
+ strict_1.default.match(request.requestId, /^[a-f0-9-]{36}$/);
534
+ strict_1.default.equal(requestIds.has(request.requestId), false);
535
+ requestIds.add(request.requestId);
536
+ calls.push({ name, path: params.path });
537
+ strict_1.default.equal(params.hostId, "h");
538
+ const data = files.get(params.path);
539
+ const result = { isFile: true, createdAtMs: Date.now(), sizeBytes: oversized ? 33 * 1024 * 1024 : data.length };
540
+ queueMicrotask(() => listeners.forEach(receive => {
541
+ const rejected = { type: "fetch-response", requestId: request.requestId, responseType: "error", status: 432 };
542
+ receive({ source: {}, origin: "", data: rejected });
543
+ receive({ source: null, origin: "https://other.test", data: rejected });
544
+ receive({ source: null, origin: "", data: { ...rejected, requestId: "other-request" } });
545
+ receive({ source: requestIds.size % 2 ? null : nativeWindow, origin: "", data: { type: "fetch-response", requestId: request.requestId, responseType: "success", status: 200, bodyJsonString: JSON.stringify(result) } });
546
+ }));
547
+ } },
548
+ };
549
+ const node = (conversation, id, hrefs) => ({
550
+ closest: () => ({ getAttribute: (key) => key === "data-response-annotation-target" ? id : conversation }),
551
+ querySelectorAll: (selector) => hrefs.filter(href => typeof href === "string" || selector.includes("[data-file-reference][data-prompt-link-href]")).map(href => ({ getAttribute: (key) => typeof href === "string" ? key === "href" ? href : null : key === "data-prompt-link-href" ? href.prompt : null })),
552
+ });
553
+ const active = { id: "local:current", host: "h", title: "当前会话" };
554
+ const client = new chatgpt_1.ChatgptCdpClient({ timeoutMs: 100 });
555
+ client.activeSession = async () => active;
556
+ let selectedId = active.id;
557
+ let switchOnChunk = false;
558
+ const scope = {
559
+ window: nativeWindow, crypto, setTimeout, clearTimeout, URL,
560
+ fetch: async (url) => {
561
+ strict_1.default.equal(url, "https://chatgpt.test/assets/index-test.js");
562
+ return { text: async () => 'import "./rpc-test.js"' };
563
+ },
564
+ nativeImport: async (url) => {
565
+ strict_1.default.equal(url, "https://chatgpt.test/assets/rpc-test.js");
566
+ return { appServices: { workspaceFiles: { read: async (params) => {
567
+ calls.push({ name: "workspaceFiles.read", path: params.path });
568
+ strict_1.default.equal(params.hostId, "h");
569
+ strict_1.default.equal(params.representation, "blob");
570
+ const data = files.get(params.path);
571
+ return { blob: (changedSize ? Buffer.concat([data, Buffer.alloc(3)]) : data).toString("base64") };
572
+ } } } };
573
+ },
574
+ document: {
575
+ scripts: [{ src: "https://chatgpt.test/assets/index-test.js" }],
576
+ querySelector: () => ({ getAttribute: (key) => key.endsWith("host-id") ? "h" : selectedId }),
577
+ querySelectorAll: () => [node("current", "old", ["file:///tmp/history.txt"]), node("other", "new", ["file:///tmp/wrong-session.txt"]), node("current", "new", ["https://example.test/file", "file:///tmp/agent-bridge-chatgpt-test/result.txt", { prompt: pngPath }, "file:///tmp/agent-bridge-chatgpt-test/result.mp4"])],
578
+ },
579
+ };
580
+ let chunks = 0;
581
+ Object.assign(client, { evaluate: async (expression) => {
582
+ new Function(expression);
583
+ const result = await runInNewContext(expression.replace("await import(", "await nativeImport("), scope);
584
+ if (typeof result === "string") {
585
+ chunks++;
586
+ strict_1.default.ok(result.length <= 256 * 1024);
587
+ if (switchOnChunk && result.length === 256 * 1024)
588
+ selectedId = "local:other";
589
+ }
590
+ return result;
591
+ } });
592
+ const read = () => client.readArtifacts([{ conversationId: "current", messageId: "new" }], active, "agent-bridge-chatgpt-test", Date.now());
593
+ const output = await read();
594
+ strict_1.default.deepEqual(output.map(item => [item.filename, item.kind, item.contentType]), [["result.txt", "file", "text/plain"], ["result.png", "image", "image/png"], ["result.mp4", "file", "video/mp4"]]);
595
+ strict_1.default.deepEqual(output.map(item => Buffer.from(item.data)), [...files.values()]);
596
+ strict_1.default.deepEqual(calls.map(call => call.name), Array(3).fill(["read-file-metadata", "workspaceFiles.read"]).flat());
597
+ calls.length = 0;
598
+ oversized = true;
599
+ await strict_1.default.rejects(read(), /32 MiB/);
600
+ strict_1.default.deepEqual(calls.map(call => call.name), ["read-file-metadata"]);
601
+ oversized = false;
602
+ changedSize = true;
603
+ await strict_1.default.rejects(read(), /内容无效|大小或编码不一致/);
604
+ changedSize = false;
605
+ switchOnChunk = true;
606
+ await strict_1.default.rejects(read(), /会话已变化/);
607
+ strict_1.default.equal(listeners.size, 0);
608
+ strict_1.default.ok(chunks > 20);
609
+ strict_1.default.equal(Object.keys(scope).some(key => key.startsWith("__agentBridgeChatgptOutput_")), false);
610
+ });
611
+ (0, node_test_1.default)("ChatGPT 首轮占位会话仅在本轮消息已提交且助手会话唯一时读取附件", async () => {
612
+ const origin = { id: "local:client-new-thread:test", host: "h", title: "新会话" };
613
+ const first = "01a1590b-7a2a-7022-981c-34668be9ad8e";
614
+ const second = "11a1590b-7a2a-7022-981c-34668be9ad8e";
615
+ let active = origin;
616
+ const client = new chatgpt_1.ChatgptCdpClient();
617
+ client.activeSession = async () => active;
618
+ Object.assign(client, { evaluate: async () => [] });
619
+ const read = (messages, submitted) => client.readArtifacts(messages, origin, "agent-bridge-chatgpt-test", Date.now(), submitted);
620
+ strict_1.default.deepEqual(await read([{ conversationId: first, messageId: "new" }], true), []);
621
+ await strict_1.default.rejects(read([{ conversationId: first, messageId: "new" }], false), /身份尚未确认/);
622
+ await strict_1.default.rejects(read([{ conversationId: first, messageId: "new" }, { conversationId: second, messageId: "other" }], true), /身份尚未确认/);
623
+ active = { id: `local:${first}`, host: "h", title: "已切换会话" };
624
+ await strict_1.default.rejects(read([{ conversationId: first, messageId: "new" }], true), /会话已变化/);
625
+ });
626
+ (0, node_test_1.default)("ChatGPT 折叠长用户消息只按正文确认提交,忽略展开控件且拒绝其他正文", async () => {
627
+ const { runInNewContext } = await Promise.resolve().then(() => __importStar(require("node:vm")));
628
+ const client = new chatgpt_1.ChatgptCdpClient();
629
+ const origin = { id: "local:client-new-thread:test", host: "local", title: "验收" };
630
+ let sentText = "", polling = "", userText = "";
631
+ const bubble = {
632
+ get innerText() { return `${userText}\n\n…\nShow more`; },
633
+ querySelector: (selector) => selector === '[data-markdown-text-tone="user-message"]' ? { get innerText() { return userText; } } : null,
634
+ };
635
+ const scope = { document: {
636
+ querySelector: () => null,
637
+ querySelectorAll: (selector) => {
638
+ if (selector === '[data-user-message-bubble]')
639
+ return [bubble];
640
+ if (selector === '[data-markdown-text-style="assistant-message"]')
641
+ return [{
642
+ innerText: "本轮完成",
643
+ closest: () => ({ getAttribute: (key) => key.endsWith("conversation") ? "01a1590b-7a2a-7022-981c-34668be9ad8e" : "new" }),
644
+ querySelector: () => ({}),
645
+ }];
646
+ return [];
647
+ },
648
+ } };
649
+ Object.assign(client, {
650
+ cdp: async (_method, params) => { sentText = params.text; userText = sentText; },
651
+ activeSession: async () => origin,
652
+ readArtifacts: async (_messages, _origin, _directory, _startedAt, submitted) => {
653
+ strict_1.default.equal(submitted, true, "折叠控件不能影响本轮正文的精确匹配");
654
+ return [];
655
+ },
656
+ evaluate: async (expression) => {
657
+ if (expression.includes("findLast((item, index)")) {
658
+ polling = expression;
659
+ return runInNewContext(expression, scope);
660
+ }
661
+ if (expression.startsWith("JSON.stringify("))
662
+ return "[]";
663
+ if (expression.endsWith(".length"))
664
+ return 0;
665
+ return true;
666
+ },
667
+ });
668
+ strict_1.default.equal(await client.direct("这是需要折叠的长消息", 5000), "本轮完成");
669
+ strict_1.default.ok(sentText.includes("附件回传约定"));
670
+ userText = `${sentText} 其他内容`;
671
+ strict_1.default.equal(JSON.parse(runInNewContext(polling, scope)).submitted, false);
672
+ });
673
+ (0, node_test_1.default)("ChatGPT 输出附件拒绝异常分块长度且清理失败不遮蔽读取错误", async () => {
674
+ const active = { id: "local:current", host: "h", title: "当前会话" };
675
+ const client = new chatgpt_1.ChatgptCdpClient();
676
+ client.activeSession = async () => active;
677
+ let metadataCalls = 0;
678
+ Object.assign(client, { evaluate: async (expression) => {
679
+ if (expression.includes("const references = []"))
680
+ return ["/tmp/agent-bridge-chatgpt-test/result.txt"];
681
+ if (expression.startsWith("(async () =>")) {
682
+ metadataCalls++;
683
+ return { length: Number.MAX_SAFE_INTEGER, size: 1 };
684
+ }
685
+ if (expression.startsWith("delete globalThis"))
686
+ throw new Error("清理失败");
687
+ throw new Error("不应读取分块");
688
+ } });
689
+ const read = () => client.readArtifacts([{ conversationId: "current", messageId: "new" }], active, "agent-bridge-chatgpt-test", Date.now());
690
+ await strict_1.default.rejects(read(), error => {
691
+ strict_1.default.ok(error instanceof AggregateError);
692
+ strict_1.default.match(String(error.cause), /内容无效/);
693
+ strict_1.default.deepEqual(error.errors.map(String), ["Error: ChatGPT 输出附件内容无效", "Error: 清理失败"]);
694
+ return true;
695
+ });
696
+ strict_1.default.equal(metadataCalls, 1);
697
+ });
698
+ (0, node_test_1.default)("ChatGPT 原生附件投递脚本有效且保留临时文件路径", async () => {
699
+ const client = new chatgpt_1.ChatgptCdpClient();
700
+ let expression = "";
701
+ Object.assign(client, { evaluate: async (value) => { expression ||= value; new Function(value); return true; } });
702
+ await client.injectAttachments([{ kind: "file", filename: "中文.txt", contentType: "text/plain", data: Buffer.from("文件") }], "testSlot");
703
+ strict_1.default.match(expression, /createTemporaryFile/);
704
+ strict_1.default.match(expression, /Object\.defineProperty\(file, 'path'/);
705
+ strict_1.default.match(expression, /ClipboardEvent\('paste'/);
706
+ });
707
+ (0, node_test_1.default)("ChatGPT 同时发送和清理失败时保留原发送错误", async () => {
708
+ const client = new chatgpt_1.ChatgptCdpClient();
709
+ const primary = new Error("发送失败");
710
+ const cleanup = new Error("清理失败");
711
+ Object.assign(client, { directTurn: async () => { throw primary; }, evaluate: async () => { throw cleanup; } });
712
+ await strict_1.default.rejects(client.direct("测试", 100, [{ kind: "file", filename: "test.txt", contentType: "text/plain", data: Buffer.from("test") }]), error => {
713
+ strict_1.default.ok(error instanceof AggregateError);
714
+ strict_1.default.equal(error.cause, primary);
715
+ strict_1.default.deepEqual(error.errors, [primary, cleanup]);
716
+ return true;
717
+ });
718
+ });
719
+ (0, node_test_1.default)("ChatGPT 发送失败只清理本轮记录的附件按钮和临时文件", async () => {
720
+ const { runInNewContext } = await Promise.resolve().then(() => __importStar(require("node:vm")));
721
+ const client = new chatgpt_1.ChatgptCdpClient();
722
+ const removed = [];
723
+ const released = [];
724
+ let disconnected = false;
725
+ const scope = {};
726
+ Object.assign(client, {
727
+ directTurn: async (_text, _timeout, _items, slot) => {
728
+ scope[slot] = {
729
+ observer: { disconnect() { disconnected = true; } },
730
+ buttons: new Set([{ isConnected: true, click() { removed.push("本轮附件"); } }, { isConnected: false, click() { removed.push("已发送附件"); } }]),
731
+ paths: ["/tmp/本轮附件"],
732
+ services: { workspaceFiles: { releaseTemporaryFile: async ({ path }) => released.push(path) } },
733
+ };
734
+ throw new Error("上传后发送失败");
735
+ },
736
+ evaluate: (expression) => runInNewContext(expression, scope),
737
+ });
738
+ await strict_1.default.rejects(client.direct("测试", 100, [{ kind: "file", filename: "test.txt", contentType: "text/plain", data: Buffer.from("test") }]), /上传后发送失败/);
739
+ strict_1.default.deepEqual(removed, ["本轮附件"]);
740
+ strict_1.default.deepEqual(released, ["/tmp/本轮附件"]);
741
+ strict_1.default.equal(disconnected, true);
742
+ strict_1.default.equal(Object.keys(scope).length, 0);
743
+ });
744
+ (0, node_test_1.default)("ChatGPT 仅包含附件的回复也能结束等待", async () => {
745
+ const client = new chatgpt_1.ChatgptCdpClient();
746
+ const attachment = { kind: "file", filename: "result.mp4", contentType: "video/mp4", data: Buffer.from("视频") };
747
+ Object.assign(client, {
748
+ cdp: async () => undefined,
749
+ activeSession: async () => ({ id: "local:current", host: "h", title: "当前" }),
750
+ readArtifacts: async () => [attachment],
751
+ evaluate: async (expression) => {
752
+ if (expression.includes("findLast((item, index)"))
753
+ return JSON.stringify({ reply: "", active: false, artifacts: [{ conversationId: "current", messageId: "new" }], hasArtifacts: true });
754
+ if (expression.startsWith("JSON.stringify("))
755
+ return "[]";
756
+ if (expression.endsWith(".length"))
757
+ return 0;
758
+ return true;
759
+ },
760
+ });
761
+ strict_1.default.deepEqual(await client.direct("返回附件", 5000), { text: "", attachments: [attachment] });
762
+ });
@@ -438,3 +438,42 @@ const plugin_manager_1 = require("../src-ts/plugin-manager");
438
438
  }
439
439
  strict_1.default.deepEqual(calls, [{ task: "测试" }]);
440
440
  });
441
+ (0, node_test_1.default)("会话归档接口只允许本机或认证调用方,并限制配置请求大小", async () => {
442
+ const calls = [];
443
+ const sessionManagement = {
444
+ providers: () => [{ pluginId: "demo", scope: "local:one", mode: "current", limitation: "仅当前会话", status: "ready" }],
445
+ config: async () => ({ enabled: false, grants: [] }),
446
+ saveConfig: async (...args) => { calls.push(["config", ...args]); },
447
+ deleteSession: async (...args) => { calls.push(["session", ...args]); },
448
+ deleteSource: async (...args) => { calls.push(["source", ...args]); },
449
+ clear: async (...args) => { calls.push(["clear", ...args]); },
450
+ };
451
+ const server = new control_server_1.ControlServer({ manager: { sessionManagement, dispatchHttp: async () => false }, token: "local-secret" });
452
+ const invoke = async (method, path, body, remote = false, token) => {
453
+ const req = Object.assign(node_stream_1.Readable.from(body === undefined ? [] : [JSON.stringify(body)]), {
454
+ method, url: path, headers: token ? { authorization: `Bearer ${token}` } : {},
455
+ socket: { remoteAddress: remote ? "192.168.31.10" : "127.0.0.1" },
456
+ });
457
+ let result = "";
458
+ const res = { statusCode: 0, setHeader() { }, end(text) { result = text; } };
459
+ await server.handle(req, res);
460
+ return { status: res.statusCode, body: JSON.parse(result) };
461
+ };
462
+ strict_1.default.equal((await invoke("GET", "/api/session-history/providers", undefined, true)).status, 401);
463
+ strict_1.default.deepEqual((await invoke("GET", "/api/session-history/providers")).body, sessionManagement.providers());
464
+ strict_1.default.deepEqual((await invoke("GET", "/api/session-history/providers", undefined, true, "local-secret")).body, sessionManagement.providers());
465
+ strict_1.default.equal((await invoke("GET", "/api/session-history/config", undefined, true)).status, 401);
466
+ strict_1.default.deepEqual((await invoke("GET", "/api/session-history/config")).body, { enabled: false, grants: [] });
467
+ strict_1.default.equal((await invoke("PATCH", "/api/session-history/config", { enabled: true, grants: [] }, true, "local-secret")).status, 200);
468
+ strict_1.default.equal((await invoke("DELETE", "/api/session-history/sessions/session-1")).status, 200);
469
+ strict_1.default.equal((await invoke("POST", "/api/session-history/delete-source", { source: "source-1" })).status, 200);
470
+ strict_1.default.equal((await invoke("DELETE", "/api/session-history")).status, 200);
471
+ strict_1.default.equal((await invoke("POST", "/api/session-history/delete-source", { source: 42 })).status, 400);
472
+ strict_1.default.equal((await invoke("PATCH", "/api/session-history/config", { payload: "字".repeat(30_000) })).status, 413);
473
+ strict_1.default.deepEqual(calls.map((call) => call[0]), ["config", "session", "source", "clear"]);
474
+ strict_1.default.ok(!JSON.stringify(calls).includes("local-secret"));
475
+ const anonymousServer = new control_server_1.ControlServer({ manager: { sessionManagement } });
476
+ const response = { setHeader() { }, end() { } };
477
+ await anonymousServer.handle({ method: "DELETE", url: "/api/session-history", headers: {}, socket: { remoteAddress: "192.168.31.10" } }, response);
478
+ strict_1.default.equal(response.statusCode, 401);
479
+ });
@@ -77,3 +77,44 @@ const desktop_launch_1 = require("../src-ts/plugins/desktop-launch");
77
77
  strict_1.default.equal(launches, 1);
78
78
  strict_1.default.equal(attempts, 3);
79
79
  });
80
+ (0, node_test_1.default)("豆包模糊查找版本目录,升级后自动避开旧版和上层启动器", async () => {
81
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "doubao-upgrade-"));
82
+ try {
83
+ const launcher = (0, node_path_1.join)(root, "Doubao.exe");
84
+ const oldPath = (0, node_path_1.join)(root, "app-2.32.6", "Doubao.exe");
85
+ const newPath = (0, node_path_1.join)(root, "app-2.32.10", "Doubao.exe");
86
+ const app = (0, node_path_1.join)(root, "app", "Doubao.exe");
87
+ for (const dir of ["app-2.32.6", "app-2.32.10"])
88
+ await (0, promises_1.mkdir)((0, node_path_1.join)(root, dir));
89
+ await (0, promises_1.mkdir)((0, node_path_1.join)(root, "app"));
90
+ for (const file of [launcher, oldPath, newPath, app])
91
+ await (0, promises_1.writeFile)(file, "");
92
+ strict_1.default.equal(await (0, desktop_launch_1.findWindowsExecutable)("Doubao.exe", oldPath), newPath);
93
+ strict_1.default.equal(await (0, desktop_launch_1.findWindowsExecutable)("Doubao.exe", launcher), newPath);
94
+ await (0, promises_1.rm)(oldPath);
95
+ strict_1.default.equal(await (0, desktop_launch_1.findWindowsExecutable)("Doubao.exe", oldPath), newPath);
96
+ await (0, promises_1.rm)(newPath);
97
+ strict_1.default.equal(await (0, desktop_launch_1.findWindowsExecutable)("Doubao.exe", oldPath), app);
98
+ strict_1.default.equal(await (0, desktop_launch_1.findExecutableInTree)(root, "Doubao.exe", /doubao/i, 3), app);
99
+ }
100
+ finally {
101
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
102
+ }
103
+ });
104
+ (0, node_test_1.default)("豆包旧配置包含版本和 app 两层时仍查找同安装位置的新版", async () => {
105
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "doubao-nested-upgrade-"));
106
+ try {
107
+ const oldPath = (0, node_path_1.join)(root, "2.32.6", "app", "Doubao.exe");
108
+ const newPath = (0, node_path_1.join)(root, "v2.32.10", "app", "Doubao.exe");
109
+ await (0, promises_1.mkdir)((0, node_path_1.join)(root, "2.32.6", "app"), { recursive: true });
110
+ await (0, promises_1.mkdir)((0, node_path_1.join)(root, "v2.32.10", "app"), { recursive: true });
111
+ await (0, promises_1.writeFile)(oldPath, "");
112
+ await (0, promises_1.writeFile)(newPath, "");
113
+ strict_1.default.equal(await (0, desktop_launch_1.findWindowsExecutable)("Doubao.exe", oldPath), newPath);
114
+ await (0, promises_1.rm)(oldPath);
115
+ strict_1.default.equal(await (0, desktop_launch_1.findWindowsExecutable)("Doubao.exe", oldPath), newPath);
116
+ }
117
+ finally {
118
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
119
+ }
120
+ });