mu-agent-bridge 1.0.73 → 1.0.78

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 (152) hide show
  1. package/README.md +12 -8
  2. package/dist/src-ts/cli.js +67 -37
  3. package/dist/src-ts/control-server.js +7 -28
  4. package/dist/src-ts/core-updater.js +141 -33
  5. package/dist/src-ts/daemon-process.js +2 -1
  6. package/dist/src-ts/dsh-agent.js +7 -2
  7. package/dist/src-ts/http-agent.js +36 -0
  8. package/dist/src-ts/plugin-manager.js +185 -58
  9. package/dist/src-ts/plugins/abweb/index.js +1 -1
  10. package/dist/src-ts/plugins/abweb/public/app.js +8 -6
  11. package/dist/src-ts/plugins/abweb/public/index.html +3 -3
  12. package/dist/src-ts/plugins/aun/attachments.js +3 -21
  13. package/dist/src-ts/plugins/aun/index.js +89 -11
  14. package/dist/src-ts/plugins/aun/messages.js +1 -1
  15. package/dist/src-ts/plugins/aun/public/app.js +2 -1
  16. package/dist/src-ts/plugins/aun/public/index.html +1 -1
  17. package/dist/src-ts/plugins/cdp-draft.js +5 -2
  18. package/dist/src-ts/plugins/chatgpt/index.js +3 -2
  19. package/dist/src-ts/plugins/claude-cli/index.js +1 -1
  20. package/dist/src-ts/plugins/desktop-launch.js +44 -3
  21. package/dist/src-ts/plugins/doubao/cdp-client.js +231 -45
  22. package/dist/src-ts/plugins/doubao/index.js +119 -7
  23. package/dist/src-ts/plugins/doubao/native-task.js +134 -0
  24. package/dist/src-ts/plugins/dsh/cdp-client.js +334 -0
  25. package/dist/src-ts/plugins/dsh/index.js +102 -23
  26. package/dist/src-ts/plugins/dsh/session-history.js +31 -3
  27. package/dist/src-ts/plugins/feishu/index.js +1 -1
  28. package/dist/src-ts/plugins/personal-assistant/index.js +29 -8
  29. package/dist/src-ts/plugins/personal-assistant/model-client.js +20 -6
  30. package/dist/src-ts/plugins/personal-assistant/session-memory.js +124 -0
  31. package/dist/src-ts/plugins/personal-assistant/tools.js +4 -3
  32. package/dist/src-ts/plugins/qwenwork/cdp-client.js +43 -7
  33. package/dist/src-ts/plugins/qwenwork/index.js +52 -18
  34. package/dist/src-ts/plugins/workbuddy/cdp-client.js +349 -83
  35. package/dist/src-ts/plugins/workbuddy/index.js +24 -12
  36. package/dist/src-ts/plugins/zcode/cdp-client.js +500 -0
  37. package/dist/src-ts/plugins/zcode/index.js +97 -0
  38. package/dist/src-ts/session-management.js +27 -8
  39. package/dist/src-ts/version-manager.js +6 -3
  40. package/dist/tests-ts/abweb-model-config.test.js +44 -41
  41. package/dist/tests-ts/aun-attachments.test.js +19 -17
  42. package/dist/tests-ts/aun-default-owner-page.test.js +2 -1
  43. package/dist/tests-ts/aun-messages.test.js +14 -0
  44. package/dist/tests-ts/aun-plugin.test.js +207 -19
  45. package/dist/tests-ts/bundled-package.test.js +1 -1
  46. package/dist/tests-ts/cdp-draft-recovery.test.js +182 -3
  47. package/dist/tests-ts/chatgpt-plugin.test.js +19 -0
  48. package/dist/tests-ts/control-server.test.js +20 -0
  49. package/dist/tests-ts/core-updater.test.js +215 -46
  50. package/dist/tests-ts/daemon-process.test.js +126 -1
  51. package/dist/tests-ts/desktop-launch.test.js +73 -0
  52. package/dist/tests-ts/doubao-native-task.test.js +237 -0
  53. package/dist/tests-ts/doubao-plugin.test.js +688 -19
  54. package/dist/tests-ts/dsh-agent.test.js +91 -19
  55. package/dist/tests-ts/dsh-cdp.test.js +148 -0
  56. package/dist/tests-ts/dsh-history.test.js +37 -0
  57. package/dist/tests-ts/feishu-plugin.test.js +1 -1
  58. package/dist/tests-ts/http-agent.test.js +18 -0
  59. package/dist/tests-ts/local-cli.test.js +29 -2
  60. package/dist/tests-ts/model-provider-cancellation.test.js +2 -0
  61. package/dist/tests-ts/native-session-management.test.js +8 -7
  62. package/dist/tests-ts/personal-assistant-coordination.test.js +16 -0
  63. package/dist/tests-ts/personal-assistant-extensions.test.js +7 -1
  64. package/dist/tests-ts/personal-assistant-memory.test.js +117 -0
  65. package/dist/tests-ts/personal-assistant.test.js +277 -3
  66. package/dist/tests-ts/platform-integration.test.js +60 -1
  67. package/dist/tests-ts/plugin-manager.test.js +78 -6
  68. package/dist/tests-ts/plugin-upgrade-deadline.test.js +354 -0
  69. package/dist/tests-ts/publish-packages.test.js +1 -1
  70. package/dist/tests-ts/qwenwork-plugin.test.js +30 -0
  71. package/dist/tests-ts/qwenwork-reconnect.test.js +195 -0
  72. package/dist/tests-ts/qwenwork-sse.test.js +33 -0
  73. package/dist/tests-ts/session-access-defaults.test.js +112 -0
  74. package/dist/tests-ts/session-management.test.js +8 -8
  75. package/dist/tests-ts/version-manager.test.js +40 -0
  76. package/dist/tests-ts/workbuddy-cdp-recovery.test.js +311 -0
  77. package/dist/tests-ts/workbuddy-plugin.test.js +356 -13
  78. package/dist/tests-ts/zcode-cdp.test.js +894 -0
  79. package/dist/tests-ts/zcode-plugin.test.js +396 -0
  80. package/package.json +1 -1
  81. package/plugins/abweb/dist/src-ts/plugins/abweb/index.js +1 -1
  82. package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.js +8 -6
  83. package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.ts +7 -5
  84. package/plugins/abweb/dist/src-ts/plugins/abweb/public/index.html +3 -3
  85. package/plugins/abweb/package.json +1 -1
  86. package/plugins/abweb/plugin.json +1 -1
  87. package/plugins/aun/config.schema.json +1 -1
  88. package/plugins/aun/dist/src-ts/plugins/aun/attachments.js +3 -21
  89. package/plugins/aun/dist/src-ts/plugins/aun/index.js +89 -11
  90. package/plugins/aun/dist/src-ts/plugins/aun/messages.js +1 -1
  91. package/plugins/aun/dist/src-ts/plugins/aun/public/app.js +2 -1
  92. package/plugins/aun/dist/src-ts/plugins/aun/public/index.html +1 -1
  93. package/plugins/aun/package.json +1 -1
  94. package/plugins/aun/plugin.json +1 -1
  95. package/plugins/chatgpt/dist/src-ts/plugins/cdp-draft.js +5 -2
  96. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +3 -2
  97. package/plugins/chatgpt/dist/src-ts/plugins/desktop-launch.js +44 -3
  98. package/plugins/claude-cli/dist/src-ts/plugins/claude-cli/index.js +1 -1
  99. package/plugins/claude-cli/package.json +1 -1
  100. package/plugins/claude-cli/plugin.json +1 -1
  101. package/plugins/doubao/config.schema.json +2 -1
  102. package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +44 -3
  103. package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +231 -45
  104. package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +119 -7
  105. package/plugins/doubao/dist/src-ts/plugins/doubao/native-task.js +134 -0
  106. package/plugins/doubao/package.json +1 -1
  107. package/plugins/doubao/plugin.json +1 -1
  108. package/plugins/dsh/config.schema.json +5 -2
  109. package/plugins/dsh/dist/src-ts/dsh-agent.js +7 -2
  110. package/plugins/dsh/dist/src-ts/plugins/desktop-launch.js +44 -3
  111. package/plugins/dsh/dist/src-ts/plugins/dsh/cdp-client.js +334 -0
  112. package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +102 -23
  113. package/plugins/dsh/dist/src-ts/plugins/dsh/session-history.js +31 -3
  114. package/plugins/dsh/package.json +1 -1
  115. package/plugins/dsh/plugin.json +1 -1
  116. package/plugins/feishu/dist/src-ts/plugins/feishu/index.js +1 -1
  117. package/plugins/feishu/package.json +1 -1
  118. package/plugins/feishu/plugin.json +1 -1
  119. package/plugins/personal-assistant/config.schema.json +3 -2
  120. package/plugins/personal-assistant/dist/src-ts/attachments.js +62 -0
  121. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/index.js +29 -8
  122. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/model-client.js +20 -6
  123. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/session-memory.js +124 -0
  124. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/tools.js +4 -3
  125. package/plugins/personal-assistant/package.json +2 -2
  126. package/plugins/personal-assistant/plugin.json +1 -1
  127. package/plugins/qwenwork/dist/src-ts/plugins/cdp-draft.js +5 -2
  128. package/plugins/qwenwork/dist/src-ts/plugins/desktop-launch.js +44 -3
  129. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +43 -7
  130. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +52 -18
  131. package/plugins/qwenwork/package.json +1 -1
  132. package/plugins/qwenwork/plugin.json +1 -1
  133. package/plugins/workbuddy/config.schema.json +1 -1
  134. package/plugins/workbuddy/dist/src-ts/plugins/desktop-launch.js +44 -3
  135. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +349 -83
  136. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +24 -12
  137. package/plugins/workbuddy/package.json +1 -1
  138. package/plugins/workbuddy/plugin.json +1 -1
  139. package/plugins/zcode/config.schema.json +13 -0
  140. package/plugins/zcode/dist/src-ts/agent.js +69 -0
  141. package/plugins/zcode/dist/src-ts/attachments.js +62 -0
  142. package/plugins/zcode/dist/src-ts/plugin.js +13 -0
  143. package/plugins/zcode/dist/src-ts/plugins/desktop-launch.js +238 -0
  144. package/plugins/zcode/dist/src-ts/plugins/workbuddy/cdp-client.js +896 -0
  145. package/plugins/zcode/dist/src-ts/plugins/workbuddy/cli-client.js +39 -0
  146. package/plugins/zcode/dist/src-ts/plugins/workbuddy/desktop-launch.js +10 -0
  147. package/plugins/zcode/dist/src-ts/plugins/workbuddy/index.js +567 -0
  148. package/plugins/zcode/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
  149. package/plugins/zcode/dist/src-ts/plugins/zcode/cdp-client.js +500 -0
  150. package/plugins/zcode/dist/src-ts/plugins/zcode/index.js +97 -0
  151. package/plugins/zcode/package.json +16 -0
  152. package/plugins/zcode/plugin.json +11 -0
@@ -37,17 +37,38 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  const strict_1 = __importDefault(require("node:assert/strict"));
40
- const node_test_1 = __importDefault(require("node:test"));
40
+ const node_test_1 = __importStar(require("node:test"));
41
41
  const node_vm_1 = require("node:vm");
42
+ const childProcess = require("node:child_process");
42
43
  const promises_1 = require("node:fs/promises");
44
+ const node_fs_1 = require("node:fs");
43
45
  const node_os_1 = require("node:os");
44
46
  const node_path_1 = require("node:path");
47
+ const fsPromises = require("node:fs/promises");
45
48
  const doubao = __importStar(require("../src-ts/plugins/doubao/cdp-client"));
46
49
  const doubao_1 = require("../src-ts/plugins/doubao");
47
50
  const cdp_client_1 = require("../src-ts/plugins/doubao/cdp-client");
48
51
  const attachments_1 = require("../src-ts/plugins/doubao/attachments");
52
+ const desktopLaunch = __importStar(require("../src-ts/plugins/desktop-launch"));
49
53
  const imageUrl = "https://p3-flow-imagex-sign.byteimg.com/tos-cn-i-a9rns2rl98/rc_gen_image/test.png?signature=test";
50
54
  const png = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII=", "base64");
55
+ (0, node_test_1.default)("豆包升级清理保留桌面,普通停止仍关闭桥接持有的进程", async (t) => {
56
+ const terminated = [];
57
+ t.mock.method(desktopLaunch, "terminateOwnedProcess", async (child) => { if (child)
58
+ terminated.push(child); });
59
+ let closed = 0;
60
+ const plugin = new doubao_1.DoubaoPlugin({ connect: async () => undefined, close: async () => { closed++; }, connected: () => true, direct: async () => "回复" });
61
+ const owned = { pid: 12345 };
62
+ await plugin.activate({ config: {} });
63
+ plugin.ownedProcess = owned;
64
+ await plugin.deactivate("reload", { preserveDesktop: true });
65
+ strict_1.default.equal(closed, 1);
66
+ strict_1.default.deepEqual(terminated, []);
67
+ await plugin.activate({ config: {} });
68
+ plugin.ownedProcess = owned;
69
+ await plugin.deactivate();
70
+ strict_1.default.deepEqual(terminated, [owned]);
71
+ });
51
72
  function deferred() {
52
73
  let resolve;
53
74
  let reject;
@@ -145,13 +166,13 @@ function deferred() {
145
166
  }
146
167
  });
147
168
  (0, node_test_1.default)("豆包在访问页面前明确拒绝视频与不支持的文件类型", async () => {
148
- const client = new cdp_client_1.DoubaoCdpClient();
169
+ const client = new cdp_client_1.DoubaoCdpClient({ mode: "chat" });
149
170
  for (const [filename, contentType, error] of [["clip.mp4", "video/mp4", /不支持视频/], ["archive.zip", "application/zip", /不支持.*文件类型/]]) {
150
171
  await strict_1.default.rejects(client.complete("读取附件", undefined, [{ kind: "file", data: Buffer.from("data"), filename, contentType }]), error);
151
172
  }
152
173
  });
153
174
  (0, node_test_1.default)("豆包上传和清理同时失败时保留两个错误", async () => {
154
- const client = new cdp_client_1.DoubaoCdpClient({ timeoutMs: 3000 });
175
+ const client = new cdp_client_1.DoubaoCdpClient({ mode: "chat", timeoutMs: 3000 });
155
176
  let uploadName = "";
156
177
  client.evaluate = async (expression) => {
157
178
  if (expression.includes("const transfer = new DataTransfer")) {
@@ -170,7 +191,7 @@ function deferred() {
170
191
  });
171
192
  });
172
193
  (0, node_test_1.default)("豆包上传启动阶段异常也清理本次附件", async () => {
173
- const client = new cdp_client_1.DoubaoCdpClient();
194
+ const client = new cdp_client_1.DoubaoCdpClient({ mode: "chat" });
174
195
  let cleaned = false;
175
196
  client.evaluate = async (expression) => {
176
197
  if (expression.includes("names.includes")) {
@@ -206,6 +227,39 @@ function deferred() {
206
227
  strict_1.default.deepEqual(extract({ block_type: 2074, is_finish: true, content_obj: { creations: [{ type: 2, video: { status: 4, vid: "failed" } }] } }), { artifacts: [], pending: false, failed: true });
207
228
  strict_1.default.deepEqual(extract(null), { artifacts: [], pending: false });
208
229
  });
230
+ (0, node_test_1.default)("豆包本地电脑文件操作块只回传工作区内的真实文件", async (t) => {
231
+ const workspace = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "mab-doubao-files-"));
232
+ const filename = "倍距点数轴题.docx";
233
+ const filePath = (0, node_path_1.join)(workspace, filename);
234
+ const data = Buffer.from("word-file-content");
235
+ await (0, promises_1.writeFile)(filePath, data);
236
+ t.after(() => (0, promises_1.rm)(workspace, { recursive: true, force: true }));
237
+ const extract = new Function(`return (${doubao.responseArtifactsScript})`)();
238
+ const result = extract([
239
+ { block_type: 10019, is_finish: true, content: { file_operation_block: { file_type: "diff", path: filePath } } },
240
+ { block_type: 10019, is_finish: true, content: { file_operation_block: { file_type: "shell", path: filePath } } },
241
+ ], workspace);
242
+ strict_1.default.deepEqual(result, { artifacts: [{ kind: "file", filename, path: filePath, workspace }], pending: false });
243
+ const attachment = await doubao.downloadDoubaoArtifact(result.artifacts[0]);
244
+ strict_1.default.equal(attachment.kind, "file");
245
+ strict_1.default.equal(attachment.filename, filename);
246
+ strict_1.default.deepEqual(attachment.data, data);
247
+ const outside = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "mab-doubao-outside-"));
248
+ const outsidePath = (0, node_path_1.join)(outside, "outside.docx");
249
+ await (0, promises_1.writeFile)(outsidePath, data);
250
+ t.after(() => (0, promises_1.rm)(outside, { recursive: true, force: true }));
251
+ await strict_1.default.rejects(doubao.downloadDoubaoArtifact({ ...result.artifacts[0], path: outsidePath }), /工作区/);
252
+ });
253
+ (0, node_test_1.default)("豆包本地电脑富媒体卡片采集完成原图,忽略工具预览和未完成卡片", () => {
254
+ const extract = new Function(`return (${doubao.responseArtifactsScript})`)();
255
+ const image = { status: 2, image_ori: { url: imageUrl }, image_thumb: { url: "https://example.com/thumb.png" } };
256
+ const card = { block_type: 10084, is_finish: true, content: { rich_media_layout_block: { media: [{ type: 6, creation: { type: 1, image } }, { type: 0 }] } } };
257
+ strict_1.default.deepEqual(extract([
258
+ { block_type: 10010, is_finish: true, content: { gen_image_block: { images: [{ uri: "https://example.com/preview", full_size_url: ["https://example.com/preview"] }] } } },
259
+ card, card,
260
+ ]), { artifacts: [{ kind: "image", url: imageUrl }], pending: false });
261
+ strict_1.default.deepEqual(extract({ ...card, is_finish: false }), { artifacts: [], pending: true });
262
+ });
209
263
  (0, node_test_1.default)("豆包异步产物只匹配同会话、本轮回复或原始问题引用", () => {
210
264
  const file = { block_type: 10020, is_finish: true, content_obj: { name: "result.txt", uri: "own-file" } };
211
265
  const question = "59158521582734851";
@@ -273,7 +327,7 @@ function deferred() {
273
327
  return new Response(JSON.stringify({ code: 0, data: { play_infos: [{ main: videoUrl }] } }));
274
328
  },
275
329
  };
276
- const client = new cdp_client_1.DoubaoCdpClient({ conversationId: "own", timeoutMs: 5000, fetchImpl: async (input) => new Response(String(input) === imageUrl ? png : String(input) === videoUrl ? mp4 : "marker", { headers: { "content-type": String(input) === imageUrl ? "image/png" : String(input) === videoUrl ? "video/mp4" : "text/plain" } }) });
330
+ const client = new cdp_client_1.DoubaoCdpClient({ mode: "chat", conversationId: "own", timeoutMs: 5000, fetchImpl: async (input) => new Response(String(input) === imageUrl ? png : String(input) === videoUrl ? mp4 : "marker", { headers: { "content-type": String(input) === imageUrl ? "image/png" : String(input) === videoUrl ? "video/mp4" : "text/plain" } }) });
277
331
  client.connect = async () => { };
278
332
  client.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, page);
279
333
  const result = await client.complete("生成产物");
@@ -300,7 +354,7 @@ function deferred() {
300
354
  return new Response(`event: STREAM_MSG_NOTIFY\ndata: ${JSON.stringify({ content: { content_block: text === "请求甲" ? [block] : [{ content: { text_block: { text: "第二轮回复" } } }] } })}\n\nevent: CHUNK_DELTA\ndata: ${JSON.stringify({ text: text === "请求甲" ? "第一轮回复" : "" })}\n\n`);
301
355
  },
302
356
  };
303
- const client = new cdp_client_1.DoubaoCdpClient({ conversationId: "test", timeoutMs: 5000, fetchImpl: async () => {
357
+ const client = new cdp_client_1.DoubaoCdpClient({ mode: "chat", conversationId: "test", timeoutMs: 5000, fetchImpl: async () => {
304
358
  downloadStarted.resolve();
305
359
  await downloadGate.promise;
306
360
  return new Response(png, { headers: { "content-type": "image/png" } });
@@ -334,6 +388,58 @@ function deferred() {
334
388
  strict_1.default.equal(results[0].attachments.length, 1);
335
389
  strict_1.default.equal(results[1].attachments, undefined);
336
390
  });
391
+ (0, node_test_1.default)("豆包本地电脑任务持续产生流式进度时超过静默超时仍等待最终回复", { timeout: 10_000 }, async () => {
392
+ const client = new cdp_client_1.DoubaoCdpClient({ timeoutMs: 1_100 });
393
+ let requestId = "", polls = 0, progressSequence = 0, completed = false;
394
+ client.connect = async () => { };
395
+ client.evaluate = async (expression) => {
396
+ if (expression.includes("buildGeneralAgentCompletionTaskParam")) {
397
+ requestId = expression.match(/"unique_key":"([^"]+)"/)?.[1] ?? "";
398
+ strict_1.default.ok(requestId);
399
+ setTimeout(() => { progressSequence = 1; }, 600);
400
+ setTimeout(() => { progressSequence = 2; }, 1_300);
401
+ setTimeout(() => { completed = true; }, 2_300);
402
+ return "started";
403
+ }
404
+ if (expression === "window.__mab_doubao_result") {
405
+ polls++;
406
+ return completed ? JSON.stringify({ requestId, ok: true, reply: "图片已生成", conversationId: "123" }) : null;
407
+ }
408
+ if (expression === "window.__mab_doubao_progress")
409
+ return { requestId, sequence: progressSequence };
410
+ };
411
+ const result = await client.complete("生成图片");
412
+ strict_1.default.equal(result.text, "图片已生成");
413
+ strict_1.default.equal(polls, 3);
414
+ });
415
+ for (const state of ["无输出", "进度不再变化", "其他请求的进度"]) {
416
+ (0, node_test_1.default)(`豆包本地电脑${state}不能持续延长等待`, async (t) => {
417
+ t.mock.timers.enable({ apis: ["setTimeout", "Date"] });
418
+ const client = new cdp_client_1.DoubaoCdpClient({ timeoutMs: 2_000 });
419
+ let requestId = "", polls = 0;
420
+ client.connect = async () => { };
421
+ client.evaluate = async (expression) => {
422
+ if (expression.includes("buildGeneralAgentCompletionTaskParam")) {
423
+ requestId = expression.match(/"unique_key":"([^"]+)"/)?.[1] ?? "";
424
+ strict_1.default.ok(requestId);
425
+ return "started";
426
+ }
427
+ if (expression === "window.__mab_doubao_result") {
428
+ polls++;
429
+ return null;
430
+ }
431
+ if (expression === "window.__mab_doubao_progress")
432
+ return { requestId: state === "其他请求的进度" ? "other-request" : requestId, sequence: state === "无输出" ? 0 : state === "进度不再变化" ? 1 : polls };
433
+ };
434
+ const result = strict_1.default.rejects(client.complete("等待本轮回复"), (error) => error.code === "DOUBAO_REPLY_TIMEOUT");
435
+ for (let i = 0; i < 5; i++) {
436
+ await new Promise(setImmediate);
437
+ t.mock.timers.tick(1_000);
438
+ }
439
+ await result;
440
+ strict_1.default.equal(polls, state === "进度不再变化" ? 3 : 2);
441
+ });
442
+ }
337
443
  for (const late of ["成功", "失败"]) {
338
444
  (0, node_test_1.default)(`豆包前轮超时后的晚${late}回调不覆盖下一轮结果`, { timeout: 10_000 }, async () => {
339
445
  const firstResponse = deferred(), nextResponse = deferred(), nextStarted = deferred();
@@ -344,7 +450,7 @@ for (const late of ["成功", "失败"]) {
344
450
  fetch: () => { if (++requests === 1)
345
451
  return firstResponse.promise; nextStarted.resolve(); return nextResponse.promise; },
346
452
  };
347
- const client = new cdp_client_1.DoubaoCdpClient({ conversationId: "test", timeoutMs: 20 });
453
+ const client = new cdp_client_1.DoubaoCdpClient({ mode: "chat", conversationId: "test", timeoutMs: 20 });
348
454
  client.connect = async () => { };
349
455
  client.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, page);
350
456
  await strict_1.default.rejects(client.complete("请求甲"), /等待豆包回复超时/);
@@ -369,6 +475,304 @@ for (const late of ["成功", "失败"]) {
369
475
  (0, node_test_1.default)("豆包流式合并不吞掉正文中重复出现的词", () => {
370
476
  strict_1.default.equal(doubao.mergeDoubaoReply("天空是蓝色。海水也是", "蓝色"), "天空是蓝色。海水也是蓝色");
371
477
  });
478
+ (0, node_test_1.default)("豆包预分配失败且未确认时复用原请求有限重试", { timeout: 10_000 }, async () => {
479
+ const client = new cdp_client_1.DoubaoCdpClient({ mode: "chat", timeoutMs: 4_000 });
480
+ const bodies = [];
481
+ let requests = 0;
482
+ const page = { window: {}, URL, AbortController, TextDecoder, navigator: { userAgent: "Chrome/147.0.0.0 SamanthaDoubao/2.32.6" }, performance: { getEntriesByType: () => [] }, fetch: async (_url, init) => {
483
+ bodies.push(String(init.body));
484
+ if (++requests === 1)
485
+ return new Response('event: STREAM_ERROR\ndata: {"error_code":718013001,"error_msg":"预分配会话位置信息失败"}\n\n');
486
+ return new Response('event: SSE_ACK\ndata: {"ack_client_meta":{"conversation_id":"123"}}\n\nevent: CHUNK_DELTA\ndata: {"text":"重试后回复"}\n\n');
487
+ } };
488
+ client.connect = async () => { };
489
+ client.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, page);
490
+ const result = await client.complete("重试请求");
491
+ strict_1.default.equal(result.text, "重试后回复");
492
+ strict_1.default.equal(requests, 2);
493
+ strict_1.default.equal(bodies[0], bodies[1]);
494
+ });
495
+ (0, node_test_1.default)("豆包预分配失败重试耗尽时保留平台错误码", { timeout: 10_000 }, async () => {
496
+ const client = new cdp_client_1.DoubaoCdpClient({ mode: "chat", timeoutMs: 4_000 });
497
+ let requests = 0;
498
+ const page = { window: {}, URL, AbortController, TextDecoder, navigator: { userAgent: "Chrome/147.0.0.0 SamanthaDoubao/2.32.6" }, performance: { getEntriesByType: () => [] }, fetch: async () => {
499
+ requests++;
500
+ return new Response('event: STREAM_ERROR\ndata: {"error_code":718013001,"error_msg":"预分配会话位置信息失败"}\n\n');
501
+ } };
502
+ client.connect = async () => { };
503
+ client.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, page);
504
+ await strict_1.default.rejects(client.complete("重试请求"), /718013001.*尝试 3 次/);
505
+ strict_1.default.equal(requests, 3);
506
+ });
507
+ (0, node_test_1.default)("豆包已确认或已有进展的流错误不自动重发", { timeout: 10_000 }, async () => {
508
+ for (const stream of [
509
+ 'event: SSE_ACK\ndata: {"ack_client_meta":{"conversation_id":"123"}}\n\nevent: STREAM_ERROR\ndata: {"error_code":718013001,"error_msg":"预分配会话位置信息失败"}\n\n',
510
+ 'event: CHUNK_DELTA\ndata: {"text":"已开始"}\n\nevent: STREAM_ERROR\ndata: {"error_code":718013001,"error_msg":"预分配会话位置信息失败"}\n\n',
511
+ 'event: STREAM_CHUNK\ndata: {"patch_op":[{"patch_value":{"content_block":[{"block_type":10020,"is_finish":false,"content":{"file_block":{"name":"生成中.pdf","uri":"pending"}}}]}}]}\n\nevent: STREAM_ERROR\ndata: {"error_code":718013001,"error_msg":"预分配会话位置信息失败"}\n\n',
512
+ ]) {
513
+ const client = new cdp_client_1.DoubaoCdpClient({ mode: "chat", timeoutMs: 2_000 });
514
+ let requests = 0;
515
+ const page = { window: {}, URL, AbortController, TextDecoder, navigator: { userAgent: "Chrome/147.0.0.0 SamanthaDoubao/2.32.6" }, performance: { getEntriesByType: () => [] }, fetch: async () => { requests++; return new Response(stream); } };
516
+ client.connect = async () => { };
517
+ client.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, page);
518
+ await strict_1.default.rejects(client.complete("不要重复发送"), /718013001/);
519
+ strict_1.default.equal(requests, 1);
520
+ }
521
+ });
522
+ (0, node_test_1.default)("豆包轮询期间 CDP 断开时重连后继续读取原结果且不重发", async () => {
523
+ const client = new cdp_client_1.DoubaoCdpClient({ mode: "chat", timeoutMs: 4_000 });
524
+ let connected = 0;
525
+ let disconnected = false;
526
+ let submissions = 0;
527
+ const page = { window: {}, URL, AbortController, TextDecoder, navigator: { userAgent: "Chrome/147.0.0.0 SamanthaDoubao/2.32.6" }, performance: { getEntriesByType: () => [] }, fetch: async () => {
528
+ submissions++;
529
+ return new Response('event: SSE_ACK\ndata: {"ack_client_meta":{"conversation_id":"456"}}\n\nevent: CHUNK_DELTA\ndata: {"text":"断线后继续回复"}\n\n');
530
+ } };
531
+ client.connect = async () => { connected++; };
532
+ client.evaluate = async (expression) => {
533
+ if (expression.includes("new AbortController()"))
534
+ disconnected = true;
535
+ if (expression === "window.__mab_doubao_result" && disconnected) {
536
+ disconnected = false;
537
+ throw new Error("豆包 CDP 已断开");
538
+ }
539
+ return (0, node_vm_1.runInNewContext)(expression, page);
540
+ };
541
+ const result = await client.complete("断线恢复");
542
+ strict_1.default.equal(result.text, "断线后继续回复");
543
+ strict_1.default.equal(connected, 2);
544
+ strict_1.default.equal(submissions, 1);
545
+ });
546
+ for (const recoveredState of ["已完成", "有新进度", "无新进度", "仍然断开"]) {
547
+ (0, node_test_1.default)(`豆包本地电脑流恢复跨过截止时间后核对原任务:${recoveredState}`, async (t) => {
548
+ t.mock.timers.enable({ apis: ["setTimeout", "Date"] });
549
+ const client = new cdp_client_1.DoubaoCdpClient({ timeoutMs: 2_000 });
550
+ let requestId = "", sequence = 0, submissions = 0, disconnected = false, reads = 0, connections = 0;
551
+ client.connect = async () => {
552
+ if (++connections > 1)
553
+ t.mock.timers.tick(2_500);
554
+ };
555
+ client.evaluate = async (expression) => {
556
+ if (expression.includes("buildGeneralAgentCompletionTaskParam")) {
557
+ submissions++;
558
+ requestId = expression.match(/"unique_key":"([^"]+)"/)?.[1] ?? "";
559
+ return "started";
560
+ }
561
+ if (expression === "window.__mab_doubao_request_id")
562
+ return requestId;
563
+ if (expression === "window.__mab_doubao_result") {
564
+ reads++;
565
+ if (!disconnected && reads === 2) {
566
+ disconnected = true;
567
+ throw new Error("豆包 CDP 已断开");
568
+ }
569
+ strict_1.default.ok(reads <= 4, "没有新进度时不得无限重连");
570
+ if (reads > 2 && recoveredState === "仍然断开")
571
+ throw new Error("豆包 CDP 已断开");
572
+ if (reads > 2 && recoveredState === "有新进度")
573
+ sequence = 1;
574
+ return reads > (recoveredState === "有新进度" ? 3 : 2) && (recoveredState === "已完成" || recoveredState === "有新进度")
575
+ ? JSON.stringify({ requestId, ok: true, reply: "断线期间完成的回复", conversationId: "123" }) : null;
576
+ }
577
+ if (expression === "window.__mab_doubao_progress")
578
+ return { requestId, sequence };
579
+ };
580
+ const running = client.complete("断线期间持续生成", undefined, undefined, "");
581
+ void running.catch(() => undefined);
582
+ for (let i = 0; i < 5; i++) {
583
+ await new Promise(setImmediate);
584
+ t.mock.timers.tick(1_000);
585
+ }
586
+ if (recoveredState === "已完成" || recoveredState === "有新进度")
587
+ strict_1.default.equal((await running).text, "断线期间完成的回复");
588
+ else
589
+ await strict_1.default.rejects(running, (error) => error.code === "DOUBAO_REPLY_TIMEOUT");
590
+ strict_1.default.equal(reads, recoveredState === "有新进度" ? 4 : 3, "恢复后应重新读取原请求结果");
591
+ strict_1.default.equal(connections, 2);
592
+ strict_1.default.equal(submissions, 1, "恢复不应重复提交任务");
593
+ });
594
+ }
595
+ (0, node_test_1.default)("豆包任务执行中 CDP 端口拒绝连接时恢复桌面并读取原任务,不重复提交", async () => {
596
+ const client = new cdp_client_1.DoubaoCdpClient({ mode: "chat", timeoutMs: 4_000, recoverCdp: async () => { recoveries++; connected = true; } });
597
+ let connected = false, connections = 0, recoveries = 0, disconnected = false, submissions = 0;
598
+ let requestId = "";
599
+ client.connected = () => connected;
600
+ client.connect = async () => {
601
+ connections++;
602
+ if (connections === 2)
603
+ throw Object.assign(new Error("fetch failed"), { cause: { code: "ECONNREFUSED" } });
604
+ connected = true;
605
+ };
606
+ client.evaluate = async (expression) => {
607
+ if (expression.includes("new AbortController()")) {
608
+ submissions++;
609
+ requestId = JSON.parse(expression.match(/window\.__mab_doubao_request_id = (.*?);/)?.[1] ?? '""');
610
+ disconnected = true;
611
+ return "started";
612
+ }
613
+ if (expression === "window.__mab_doubao_result" && disconnected) {
614
+ disconnected = false;
615
+ connected = false;
616
+ throw new Error("豆包 CDP 已断开");
617
+ }
618
+ if (expression === "window.__mab_doubao_request_id")
619
+ return requestId;
620
+ if (expression === "window.__mab_doubao_result")
621
+ return JSON.stringify({ ok: true, reply: "恢复后的原任务回复", conversationId: "123" });
622
+ };
623
+ strict_1.default.equal((await client.complete("执行中的任务")).text, "恢复后的原任务回复");
624
+ strict_1.default.equal(recoveries, 1);
625
+ strict_1.default.equal(submissions, 1);
626
+ });
627
+ (0, node_test_1.default)("豆包 CDP 命令超时但旧 socket 仍打开时重建连接并继续读取原任务", { timeout: 10_000 }, async () => {
628
+ const original = globalThis.WebSocket;
629
+ const sockets = [];
630
+ let discoveries = 0;
631
+ let submissions = 0;
632
+ let requestId = "";
633
+ class TestWebSocket {
634
+ url;
635
+ readyState = 0;
636
+ onopen;
637
+ onclose;
638
+ onmessage;
639
+ constructor(url) {
640
+ this.url = url;
641
+ sockets.push(this);
642
+ queueMicrotask(() => { this.readyState = 1; this.onopen?.(); });
643
+ }
644
+ send(raw) {
645
+ const request = JSON.parse(raw);
646
+ const expression = request.params?.expression ?? "";
647
+ if (expression.includes("new AbortController()")) {
648
+ submissions++;
649
+ requestId = JSON.parse(expression.match(/window\.__mab_doubao_request_id = (.*?);/)?.[1] ?? '""');
650
+ }
651
+ if (sockets[0] === this && expression === "window.__mab_doubao_result")
652
+ return;
653
+ const value = expression === "window.__mab_doubao_request_id"
654
+ ? requestId
655
+ : expression === "window.__mab_doubao_result"
656
+ ? JSON.stringify({ ok: true, reply: "超时后恢复的回复", conversationId: "456" })
657
+ : "started";
658
+ queueMicrotask(() => this.onmessage?.({ data: JSON.stringify({ id: request.id, result: { result: { value } } }) }));
659
+ }
660
+ close() { if (this.readyState === 3)
661
+ return; this.readyState = 3; this.onclose?.(); }
662
+ }
663
+ globalThis.WebSocket = TestWebSocket;
664
+ const client = new cdp_client_1.DoubaoCdpClient({ mode: "chat", timeoutMs: 2_500, fetchImpl: async () => new Response(JSON.stringify([
665
+ { type: "page", url: "doubao://doubao-chat/chat/test", webSocketDebuggerUrl: `ws://page-${++discoveries}` },
666
+ ])) });
667
+ const cdp = client.cdp.bind(client);
668
+ client.cdp = (method, params, timeoutMs = 50) => cdp(method, params, timeoutMs);
669
+ try {
670
+ strict_1.default.equal((await client.complete("超时后继续")).text, "超时后恢复的回复");
671
+ strict_1.default.equal(discoveries, 2);
672
+ strict_1.default.equal(sockets.length, 2);
673
+ strict_1.default.equal(submissions, 1);
674
+ }
675
+ finally {
676
+ await client.close();
677
+ globalThis.WebSocket = original;
678
+ }
679
+ });
680
+ (0, node_test_1.default)("豆包等待流结束时不重复注入请求", async () => {
681
+ const client = new cdp_client_1.DoubaoCdpClient({ mode: "chat", timeoutMs: 4_000 });
682
+ let submissions = 0, polls = 0;
683
+ client.connect = async () => { };
684
+ client.evaluate = async (expression) => {
685
+ if (expression.includes("new AbortController()")) {
686
+ submissions++;
687
+ return "started";
688
+ }
689
+ if (expression === "window.__mab_doubao_result" && ++polls >= 2)
690
+ return JSON.stringify({ ok: true, reply: "原任务回复", conversationId: "456" });
691
+ };
692
+ strict_1.default.equal((await client.complete("只提交一次")).text, "原任务回复");
693
+ strict_1.default.equal(submissions, 1);
694
+ });
695
+ (0, node_test_1.default)("豆包重连后原页面任务丢失时明确报告结果未知,不重发", async () => {
696
+ const client = new cdp_client_1.DoubaoCdpClient({ mode: "chat", timeoutMs: 4_000 });
697
+ let submissions = 0;
698
+ client.connect = async () => { };
699
+ client.evaluate = async (expression) => {
700
+ if (expression.includes("new AbortController()")) {
701
+ submissions++;
702
+ return "started";
703
+ }
704
+ if (expression === "window.__mab_doubao_result")
705
+ throw new Error("豆包 CDP 已断开");
706
+ };
707
+ await strict_1.default.rejects(client.complete("不能重复执行"), (error) => error.code === "DOUBAO_RESULT_UNKNOWN" && /原任务|页面/.test(error.message));
708
+ strict_1.default.equal(submissions, 1);
709
+ });
710
+ (0, node_test_1.default)("豆包 CDP 连接暂时失败会重试并记录恢复,耗尽时保留端口和原因", async () => {
711
+ const logs = [];
712
+ const client = new cdp_client_1.DoubaoCdpClient({ mode: "chat", port: 19227, timeoutMs: 4_000, logger: { warn: (message) => logs.push(message) } });
713
+ let attempts = 0;
714
+ client.openConnection = async () => { if (++attempts < 3)
715
+ throw Object.assign(new Error("fetch failed"), { cause: { code: "ECONNREFUSED" } }); };
716
+ await client.connect();
717
+ strict_1.default.equal(attempts, 3);
718
+ strict_1.default.ok(logs.some(message => /19227.*重试/.test(message)));
719
+ attempts = 0;
720
+ client.openConnection = async () => { attempts++; throw Object.assign(new Error("fetch failed"), { cause: { code: "ECONNREFUSED" } }); };
721
+ await strict_1.default.rejects(client.complete("未提交的任务"), (error) => error.code === "DOUBAO_CDP_UNAVAILABLE" && /19227.*ECONNREFUSED/.test(error.message));
722
+ strict_1.default.equal(attempts, 3);
723
+ });
724
+ (0, node_test_1.default)("豆包预分配失败遇到取消不再重试", async () => {
725
+ const controller = new AbortController();
726
+ const client = new cdp_client_1.DoubaoCdpClient({ mode: "chat", timeoutMs: 4_000 });
727
+ let submissions = 0;
728
+ const page = { window: {}, URL, AbortController, TextDecoder, navigator: { userAgent: "Chrome/147.0.0.0 SamanthaDoubao/2.32.6" }, performance: { getEntriesByType: () => [] }, fetch: async () => {
729
+ submissions++;
730
+ controller.abort();
731
+ return new Response('event: STREAM_ERROR\ndata: {"error_code":718013001,"error_msg":"预分配会话位置信息失败"}\n\n');
732
+ } };
733
+ client.connect = async () => { };
734
+ client.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, page);
735
+ await strict_1.default.rejects(client.complete("取消测试", controller.signal), /取消/);
736
+ strict_1.default.equal(submissions, 1);
737
+ });
738
+ (0, node_test_1.default)("豆包提交后 CDP 回执丢失时恢复原任务,不重复执行", async () => {
739
+ const client = new cdp_client_1.DoubaoCdpClient({ mode: "chat", timeoutMs: 4_000 });
740
+ let submissions = 0, connections = 0;
741
+ const page = { window: {}, URL, AbortController, TextDecoder, navigator: { userAgent: "Chrome/147.0.0.0 SamanthaDoubao/2.32.6" }, performance: { getEntriesByType: () => [] }, fetch: async () => {
742
+ submissions++;
743
+ return new Response('event: SSE_ACK\ndata: {"ack_client_meta":{"conversation_id":"456"}}\n\nevent: CHUNK_DELTA\ndata: {"text":"提交回执丢失后仍收到回复"}\n\n');
744
+ } };
745
+ client.connect = async () => { connections++; };
746
+ client.evaluate = async (expression) => {
747
+ const result = (0, node_vm_1.runInNewContext)(expression, page);
748
+ if (expression.includes("new AbortController()"))
749
+ throw new Error("豆包 CDP 已断开");
750
+ return result;
751
+ };
752
+ strict_1.default.equal((await client.complete("提交一次")).text, "提交回执丢失后仍收到回复");
753
+ strict_1.default.equal(submissions, 1);
754
+ strict_1.default.equal(connections, 2);
755
+ });
756
+ (0, node_test_1.default)("豆包风控和未知网络错误不重发任务,保留具体平台原因", async () => {
757
+ for (const failure of [
758
+ { error_code: 718013001, error_msg: "需要验证", extra: { decision: { type: "verify", subtype: "slide" } } },
759
+ { error_code: 12345, error_msg: "平台临时拒绝" },
760
+ undefined,
761
+ ]) {
762
+ const client = new cdp_client_1.DoubaoCdpClient({ mode: "chat", timeoutMs: 4_000 });
763
+ let submissions = 0;
764
+ const page = { window: {}, URL, AbortController, TextDecoder, navigator: { userAgent: "Chrome/147.0.0.0 SamanthaDoubao/2.32.6" }, performance: { getEntriesByType: () => [] }, fetch: async () => {
765
+ submissions++;
766
+ if (!failure)
767
+ throw new Error("fetch failed");
768
+ return new Response(`event: STREAM_ERROR\ndata: ${JSON.stringify(failure)}\n\n`);
769
+ } };
770
+ client.connect = async () => { };
771
+ client.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, page);
772
+ await strict_1.default.rejects(client.complete("不要重复执行"), (error) => /DOUBAO_(PLATFORM_ERROR|STREAM_FAILED)/.test(error.code) && /豆包平台流式响应/.test(error.stage));
773
+ strict_1.default.equal(submissions, 1);
774
+ }
775
+ });
372
776
  (0, node_test_1.default)("豆包完整回复解析 SSE 图片并保留增量正文中的重复词", async () => {
373
777
  const original = globalThis.WebSocket;
374
778
  const block = { block_type: 2074, is_finish: true, content: { creation_block: { creations: [{ image: { status: 2, image_ori: { url: imageUrl } } }] } } };
@@ -413,7 +817,7 @@ for (const late of ["成功", "失败"]) {
413
817
  }
414
818
  globalThis.WebSocket = TestWebSocket;
415
819
  let downloads = 0;
416
- const client = new cdp_client_1.DoubaoCdpClient({ fetchImpl: async (input) => {
820
+ const client = new cdp_client_1.DoubaoCdpClient({ mode: "chat", fetchImpl: async (input) => {
417
821
  if (String(input).endsWith("/json"))
418
822
  return new Response(JSON.stringify([{ type: "page", url: "doubao://doubao-chat/chat/test", webSocketDebuggerUrl: "ws://test" }]));
419
823
  strict_1.default.equal(String(input), imageUrl);
@@ -502,7 +906,7 @@ for (const late of ["成功", "失败"]) {
502
906
  });
503
907
  (0, node_test_1.default)("未配置豆包参数时保留默认 CDP 端口", async () => {
504
908
  let url = "";
505
- const client = new cdp_client_1.DoubaoCdpClient({ port: undefined, conversationId: undefined, botId: undefined, timeoutMs: undefined, fetchImpl: async (input) => { url = String(input); return new Response("[]"); } });
909
+ const client = new cdp_client_1.DoubaoCdpClient({ mode: "chat", port: undefined, conversationId: undefined, botId: undefined, timeoutMs: undefined, fetchImpl: async (input) => { url = String(input); return new Response("[]"); } });
506
910
  await strict_1.default.rejects(client.connect(), /未找到豆包/);
507
911
  strict_1.default.match(url, /:\d+\/json$/);
508
912
  });
@@ -552,6 +956,10 @@ for (const late of ["成功", "失败"]) {
552
956
  const existing = JSON.parse((0, cdp_client_1.requestBody)("继续", "123", "bot"));
553
957
  strict_1.default.equal(existing.option.need_create_conversation, false);
554
958
  strict_1.default.equal(existing.client_meta.local_conversation_id, undefined);
959
+ const localComputer = JSON.parse((0, cdp_client_1.requestBody)("执行", "123", "bot", [], [], "local-computer"));
960
+ strict_1.default.equal(localComputer.option.agent_mode, 1);
961
+ strict_1.default.equal(localComputer.option.aggregate_params.agent_mode, "1");
962
+ strict_1.default.equal(localComputer.ext.agent_mode, "1");
555
963
  });
556
964
  (0, node_test_1.default)("停止豆包客户端时向页面发送请求取消指令", async () => {
557
965
  const original = globalThis.WebSocket;
@@ -570,7 +978,7 @@ for (const late of ["成功", "失败"]) {
570
978
  }
571
979
  globalThis.WebSocket = TestWebSocket;
572
980
  try {
573
- const client = new cdp_client_1.DoubaoCdpClient({ fetchImpl: async () => new Response(JSON.stringify([{ type: "page", url: "https://www.doubao.com/chat/test", webSocketDebuggerUrl: "ws://test" }])) });
981
+ const client = new cdp_client_1.DoubaoCdpClient({ mode: "chat", fetchImpl: async () => new Response(JSON.stringify([{ type: "page", url: "https://www.doubao.com/chat/test", webSocketDebuggerUrl: "ws://test" }])) });
574
982
  await client.connect();
575
983
  const pending = client.direct("停止测试").catch((error) => String(error));
576
984
  await new Promise((resolve) => setImmediate(resolve));
@@ -607,7 +1015,7 @@ for (const late of ["成功", "失败"]) {
607
1015
  close() { this.readyState = 3; this.onclose?.(); }
608
1016
  }
609
1017
  globalThis.WebSocket = TestWebSocket;
610
- const client = new cdp_client_1.DoubaoCdpClient({ fetchImpl: async () => {
1018
+ const client = new cdp_client_1.DoubaoCdpClient({ mode: "chat", fetchImpl: async () => {
611
1019
  if (++discoveries === 2)
612
1020
  await secondDiscovery.promise;
613
1021
  return new Response(JSON.stringify([{ type: "page", url: "doubao://doubao-chat/chat/test", webSocketDebuggerUrl: "ws://test" }]));
@@ -663,7 +1071,7 @@ for (const late of ["成功", "失败"]) {
663
1071
  }
664
1072
  globalThis.WebSocket = TestWebSocket;
665
1073
  let discoveries = 0;
666
- const client = new cdp_client_1.DoubaoCdpClient({ timeoutMs: 2000, fetchImpl: async () => new Response(JSON.stringify([
1074
+ const client = new cdp_client_1.DoubaoCdpClient({ mode: "chat", timeoutMs: 2000, fetchImpl: async () => new Response(JSON.stringify([
667
1075
  { type: "page", url: "doubao://doubao-chat/chat/test", webSocketDebuggerUrl: `ws://page-${++discoveries}` },
668
1076
  ])) });
669
1077
  try {
@@ -703,6 +1111,264 @@ for (const late of ["成功", "失败"]) {
703
1111
  await plugin.deactivate();
704
1112
  }
705
1113
  });
1114
+ (0, node_test_1.default)("豆包自动启动复用默认用户目录,但不结束人工进程", async () => {
1115
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "doubao-default-profile-"));
1116
+ const executable = (0, node_path_1.join)(root, "Doubao.exe");
1117
+ const platform = Object.getOwnPropertyDescriptor(process, "platform");
1118
+ await (0, promises_1.writeFile)(executable, "");
1119
+ let connections = 0;
1120
+ let profile = "未捕获";
1121
+ const connect = node_test_1.mock.method(cdp_client_1.DoubaoCdpClient.prototype, "connect", async () => {
1122
+ connections++;
1123
+ if (connections === 1)
1124
+ throw Object.assign(new TypeError("fetch failed"), { cause: { code: "ECONNREFUSED" } });
1125
+ });
1126
+ let terminated = false;
1127
+ const terminate = node_test_1.mock.method(desktopLaunch, "terminateWindowsDesktop", async () => { terminated = true; return []; });
1128
+ const restart = node_test_1.mock.method(desktopLaunch, "restartOwnedAndWait", async (...args) => { profile = args[3]; });
1129
+ Object.defineProperty(process, "platform", { value: "win32" });
1130
+ const plugin = new doubao_1.DoubaoPlugin();
1131
+ try {
1132
+ await plugin.activate({ config: { executablePath: executable } });
1133
+ strict_1.default.equal(terminated, false);
1134
+ strict_1.default.equal(profile, undefined);
1135
+ }
1136
+ finally {
1137
+ await plugin.deactivate();
1138
+ restart.mock.restore();
1139
+ terminate.mock.restore();
1140
+ connect.mock.restore();
1141
+ Object.defineProperty(process, "platform", platform);
1142
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
1143
+ }
1144
+ });
1145
+ (0, node_test_1.default)("豆包非端口连接错误不结束人工进程", async () => {
1146
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "doubao-connect-error-"));
1147
+ const executable = (0, node_path_1.join)(root, "Doubao.exe");
1148
+ const platform = Object.getOwnPropertyDescriptor(process, "platform");
1149
+ await (0, promises_1.writeFile)(executable, "");
1150
+ let terminated = false;
1151
+ let restarted = false;
1152
+ const connect = node_test_1.mock.method(cdp_client_1.DoubaoCdpClient.prototype, "connect", async () => { throw new Error("豆包页面协议错误"); });
1153
+ const terminate = node_test_1.mock.method(desktopLaunch, "terminateWindowsDesktop", async () => { terminated = true; return []; });
1154
+ const restart = node_test_1.mock.method(desktopLaunch, "restartOwnedAndWait", async () => { restarted = true; });
1155
+ Object.defineProperty(process, "platform", { value: "win32" });
1156
+ const plugin = new doubao_1.DoubaoPlugin();
1157
+ try {
1158
+ await strict_1.default.rejects(plugin.activate({ config: { executablePath: executable } }), /豆包页面协议错误/);
1159
+ strict_1.default.equal(terminated, false);
1160
+ strict_1.default.equal(restarted, false);
1161
+ }
1162
+ finally {
1163
+ await plugin.deactivate();
1164
+ restart.mock.restore();
1165
+ terminate.mock.restore();
1166
+ connect.mock.restore();
1167
+ Object.defineProperty(process, "platform", platform);
1168
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
1169
+ }
1170
+ });
1171
+ (0, node_test_1.default)("豆包普通任务 CDP 端口拒绝连接时恢复桥接实例并继续原会话", async () => {
1172
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "doubao-cleanup-error-"));
1173
+ const executable = (0, node_path_1.join)(root, "Doubao.exe");
1174
+ const platform = Object.getOwnPropertyDescriptor(process, "platform");
1175
+ await (0, promises_1.writeFile)(executable, "");
1176
+ let connected = true, connections = 0, restarted = false;
1177
+ const connect = node_test_1.mock.method(cdp_client_1.DoubaoCdpClient.prototype, "connect", async () => { connections++; if (!connected)
1178
+ throw Object.assign(new Error("CDP 未连接"), { code: "ECONNREFUSED" }); });
1179
+ const connectionState = node_test_1.mock.method(cdp_client_1.DoubaoCdpClient.prototype, "connected", () => connected);
1180
+ const terminate = node_test_1.mock.method(desktopLaunch, "terminateWindowsDesktop", async () => { strict_1.default.fail("不能结束人工桌面"); return []; });
1181
+ const restart = node_test_1.mock.method(desktopLaunch, "restartOwnedAndWait", async (...args) => {
1182
+ strict_1.default.equal(args[0], undefined, "不得接管人工进程");
1183
+ restarted = true;
1184
+ connected = true;
1185
+ await args[4]();
1186
+ });
1187
+ const complete = node_test_1.mock.method(cdp_client_1.DoubaoCdpClient.prototype, "complete", async (_text, _signal, _attachments, conversationId) => {
1188
+ strict_1.default.ok(connected);
1189
+ strict_1.default.equal(conversationId, "123");
1190
+ return { text: "原会话已恢复", sessionId: "123" };
1191
+ });
1192
+ Object.defineProperty(process, "platform", { value: "win32" });
1193
+ const plugin = new doubao_1.DoubaoPlugin();
1194
+ try {
1195
+ await plugin.activate({ config: { executablePath: executable } });
1196
+ plugin.bindings.set("peer-a", "123");
1197
+ connected = false;
1198
+ strict_1.default.deepEqual(await plugin.handle({ task: "恢复原任务", sessionId: "peer-a" }), { text: "原会话已恢复", sessionId: "peer-a" });
1199
+ strict_1.default.equal(restarted, true);
1200
+ strict_1.default.equal(connections, 3);
1201
+ }
1202
+ finally {
1203
+ await plugin.deactivate();
1204
+ restart.mock.restore();
1205
+ terminate.mock.restore();
1206
+ connect.mock.restore();
1207
+ connectionState.mock.restore();
1208
+ complete.mock.restore();
1209
+ Object.defineProperty(process, "platform", platform);
1210
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
1211
+ }
1212
+ });
1213
+ (0, node_test_1.default)("豆包默认用户目录被占用时复制登录状态到桥接目录", async () => {
1214
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "doubao-profile-fallback-"));
1215
+ const executable = (0, node_path_1.join)(root, "Doubao.exe");
1216
+ const localAppData = (0, node_path_1.join)(root, "local-app-data");
1217
+ const dataDir = (0, node_path_1.join)(root, "state");
1218
+ const defaultProfile = (0, node_path_1.join)(localAppData, "Doubao", "User Data");
1219
+ const cookieFile = (0, node_path_1.join)(defaultProfile, "Default", "Network", "Cookies");
1220
+ const platform = Object.getOwnPropertyDescriptor(process, "platform");
1221
+ const previousLocalAppData = process.env.LOCALAPPDATA;
1222
+ await (0, promises_1.mkdir)((0, node_path_1.join)(defaultProfile, "Default", "Network"), { recursive: true });
1223
+ await (0, promises_1.writeFile)(executable, "");
1224
+ await (0, promises_1.writeFile)(cookieFile, "logged-in-cookie");
1225
+ process.env.LOCALAPPDATA = localAppData;
1226
+ let connections = 0;
1227
+ const profiles = [];
1228
+ const connect = node_test_1.mock.method(cdp_client_1.DoubaoCdpClient.prototype, "connect", async () => {
1229
+ connections++;
1230
+ if (connections === 1)
1231
+ throw Object.assign(new Error("CDP 未连接"), { code: "ECONNREFUSED" });
1232
+ });
1233
+ const restart = node_test_1.mock.method(desktopLaunch, "restartOwnedAndWait", async (...args) => {
1234
+ profiles.push(args[3]);
1235
+ if (profiles.length === 1)
1236
+ throw new Error("默认用户目录已被占用");
1237
+ });
1238
+ const terminate = node_test_1.mock.method(desktopLaunch, "terminateWindowsDesktop", async () => []);
1239
+ Object.defineProperty(process, "platform", { value: "win32" });
1240
+ const plugin = new doubao_1.DoubaoPlugin();
1241
+ try {
1242
+ await plugin.activate({ config: { executablePath: executable }, dataDir });
1243
+ strict_1.default.equal(profiles[0], undefined);
1244
+ strict_1.default.equal(profiles.length, 2);
1245
+ strict_1.default.equal(typeof profiles[1], "string");
1246
+ strict_1.default.equal(await (0, promises_1.readFile)((0, node_path_1.join)(String(profiles[1]), "Default", "Network", "Cookies"), "utf8"), "logged-in-cookie");
1247
+ }
1248
+ finally {
1249
+ await plugin.deactivate();
1250
+ terminate.mock.restore();
1251
+ restart.mock.restore();
1252
+ connect.mock.restore();
1253
+ Object.defineProperty(process, "platform", platform);
1254
+ if (previousLocalAppData === undefined)
1255
+ delete process.env.LOCALAPPDATA;
1256
+ else
1257
+ process.env.LOCALAPPDATA = previousLocalAppData;
1258
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
1259
+ }
1260
+ });
1261
+ (0, node_test_1.default)("豆包登录文件被人工进程锁定时仍完成桥接启动", async () => {
1262
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "doubao-locked-profile-"));
1263
+ const executable = (0, node_path_1.join)(root, "Doubao.exe");
1264
+ const localAppData = (0, node_path_1.join)(root, "local-app-data");
1265
+ const dataDir = (0, node_path_1.join)(root, "state");
1266
+ const defaultProfile = (0, node_path_1.join)(localAppData, "Doubao", "User Data");
1267
+ const platform = Object.getOwnPropertyDescriptor(process, "platform");
1268
+ const previousLocalAppData = process.env.LOCALAPPDATA;
1269
+ await (0, promises_1.mkdir)(defaultProfile, { recursive: true });
1270
+ await (0, promises_1.mkdir)((0, node_path_1.join)(dataDir, "desktop-profile"), { recursive: true });
1271
+ await (0, promises_1.writeFile)(executable, "");
1272
+ await (0, promises_1.writeFile)((0, node_path_1.join)(defaultProfile, "Local State"), "logged-in-state");
1273
+ await (0, promises_1.writeFile)((0, node_path_1.join)(dataDir, "desktop-profile", "Local State"), "existing-login-state");
1274
+ process.env.LOCALAPPDATA = localAppData;
1275
+ let connections = 0;
1276
+ const profiles = [];
1277
+ const connect = node_test_1.mock.method(cdp_client_1.DoubaoCdpClient.prototype, "connect", async () => {
1278
+ connections++;
1279
+ if (connections === 1)
1280
+ throw Object.assign(new Error("CDP 未连接"), { code: "ECONNREFUSED" });
1281
+ });
1282
+ const restart = node_test_1.mock.method(desktopLaunch, "restartOwnedAndWait", async (...args) => {
1283
+ profiles.push(args[3]);
1284
+ if (profiles.length === 1)
1285
+ throw new Error("默认用户目录已被占用");
1286
+ });
1287
+ const terminate = node_test_1.mock.method(desktopLaunch, "terminateWindowsDesktop", async () => []);
1288
+ const copy = node_test_1.mock.method(fsPromises, "cp", async () => {
1289
+ throw Object.assign(new Error("文件被占用"), { code: "EBUSY" });
1290
+ });
1291
+ Object.defineProperty(process, "platform", { value: "win32" });
1292
+ const plugin = new doubao_1.DoubaoPlugin();
1293
+ try {
1294
+ await plugin.activate({ config: { executablePath: executable }, dataDir });
1295
+ strict_1.default.equal(profiles.length, 2);
1296
+ strict_1.default.equal(await (0, promises_1.readFile)((0, node_path_1.join)(dataDir, "desktop-profile", "Local State"), "utf8"), "existing-login-state");
1297
+ }
1298
+ finally {
1299
+ await plugin.deactivate();
1300
+ copy.mock.restore();
1301
+ terminate.mock.restore();
1302
+ restart.mock.restore();
1303
+ connect.mock.restore();
1304
+ Object.defineProperty(process, "platform", platform);
1305
+ if (previousLocalAppData === undefined)
1306
+ delete process.env.LOCALAPPDATA;
1307
+ else
1308
+ process.env.LOCALAPPDATA = previousLocalAppData;
1309
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
1310
+ }
1311
+ });
1312
+ (0, node_test_1.default)("豆包登录文件可用备份读取时复制完整状态", async () => {
1313
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "doubao-backup-profile-"));
1314
+ const executable = (0, node_path_1.join)(root, "Doubao.exe");
1315
+ const localAppData = (0, node_path_1.join)(root, "local-app-data");
1316
+ const dataDir = (0, node_path_1.join)(root, "state");
1317
+ const defaultProfile = (0, node_path_1.join)(localAppData, "Doubao", "User Data");
1318
+ const platform = Object.getOwnPropertyDescriptor(process, "platform");
1319
+ const previousLocalAppData = process.env.LOCALAPPDATA;
1320
+ await (0, promises_1.mkdir)(defaultProfile, { recursive: true });
1321
+ await (0, promises_1.writeFile)(executable, "");
1322
+ await (0, promises_1.writeFile)((0, node_path_1.join)(defaultProfile, "Local State"), "logged-in-state");
1323
+ process.env.LOCALAPPDATA = localAppData;
1324
+ let connections = 0;
1325
+ const profiles = [];
1326
+ const connect = node_test_1.mock.method(cdp_client_1.DoubaoCdpClient.prototype, "connect", async () => {
1327
+ connections++;
1328
+ if (connections === 1)
1329
+ throw Object.assign(new Error("CDP 未连接"), { code: "ECONNREFUSED" });
1330
+ });
1331
+ const restart = node_test_1.mock.method(desktopLaunch, "restartOwnedAndWait", async (...args) => {
1332
+ profiles.push(args[3]);
1333
+ if (profiles.length === 1)
1334
+ throw new Error("默认用户目录已被占用");
1335
+ });
1336
+ const terminate = node_test_1.mock.method(desktopLaunch, "terminateWindowsDesktop", async () => []);
1337
+ const copy = node_test_1.mock.method(fsPromises, "cp", async () => {
1338
+ throw Object.assign(new Error("文件被占用"), { code: "EBUSY" });
1339
+ });
1340
+ const robocopy = node_test_1.mock.method(childProcess, "execFile", (file, args, _options, callback) => {
1341
+ strict_1.default.equal(file, "robocopy.exe");
1342
+ const source = args[0];
1343
+ const target = args[1];
1344
+ const name = args[2];
1345
+ (0, node_fs_1.mkdirSync)(target, { recursive: true });
1346
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(target, name), (0, node_fs_1.readFileSync)((0, node_path_1.join)(source, name)));
1347
+ callback(null);
1348
+ return {};
1349
+ });
1350
+ Object.defineProperty(process, "platform", { value: "win32" });
1351
+ const plugin = new doubao_1.DoubaoPlugin();
1352
+ try {
1353
+ await plugin.activate({ config: { executablePath: executable }, dataDir });
1354
+ strict_1.default.equal(profiles.length, 2);
1355
+ strict_1.default.equal(await (0, promises_1.readFile)((0, node_path_1.join)(dataDir, "desktop-profile", "Local State"), "utf8"), "logged-in-state");
1356
+ }
1357
+ finally {
1358
+ await plugin.deactivate();
1359
+ robocopy.mock.restore();
1360
+ copy.mock.restore();
1361
+ terminate.mock.restore();
1362
+ restart.mock.restore();
1363
+ connect.mock.restore();
1364
+ Object.defineProperty(process, "platform", platform);
1365
+ if (previousLocalAppData === undefined)
1366
+ delete process.env.LOCALAPPDATA;
1367
+ else
1368
+ process.env.LOCALAPPDATA = previousLocalAppData;
1369
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
1370
+ }
1371
+ });
706
1372
  (0, node_test_1.default)("豆包 /new 切换独立会话,重载后仍使用服务端会话标识", async () => {
707
1373
  const dataDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "doubao-sessions-"));
708
1374
  const targets = [];
@@ -807,26 +1473,29 @@ for (const late of ["成功", "失败"]) {
807
1473
  strict_1.default.deepEqual(before, ["模型提示"]);
808
1474
  strict_1.default.deepEqual(calls, ["模型提示", "普通任务"]);
809
1475
  });
810
- (0, node_test_1.default)("豆包模型断线时不调用可能重启桌面的恢复入口", async () => {
1476
+ (0, node_test_1.default)("豆包模型断线时优先恢复连接且不新建会话", async () => {
811
1477
  let connected = true;
812
1478
  let reconnects = 0;
813
1479
  let creates = 0;
1480
+ let connections = 0;
814
1481
  const plugin = new doubao_1.DoubaoPlugin({
815
- connect: async () => undefined, close: async () => undefined, connected: () => connected,
1482
+ connect: async () => { connections++; connected = true; }, close: async () => undefined, connected: () => connected,
816
1483
  direct: async () => "回复", createSession: async () => { creates++; }, complete: async () => ({ text: "回复", sessionId: "123" }),
817
1484
  });
818
1485
  await plugin.activate({ config: {} });
819
- plugin.reconnect = async () => { reconnects++; connected = true; };
1486
+ const reconnect = plugin.reconnect;
1487
+ plugin.reconnect = async () => { reconnects++; await reconnect(); };
820
1488
  connected = false;
821
- const error = await plugin.modelProvider.complete({ prompt: "模型提示", sessionId: "pa-offline", timeoutMs: 5000, signal: new AbortController().signal }).then(() => undefined, (value) => value);
1489
+ const result = await plugin.modelProvider.complete({ prompt: "模型提示", sessionId: "pa-offline", timeoutMs: 5000, signal: new AbortController().signal });
822
1490
  await plugin.deactivate();
823
- strict_1.default.ok(error);
824
- strict_1.default.equal(reconnects, 0);
1491
+ strict_1.default.equal(result.text, "回复");
1492
+ strict_1.default.equal(connections, 2);
1493
+ strict_1.default.equal(reconnects, 1);
825
1494
  strict_1.default.equal(creates, 0);
826
1495
  });
827
1496
  (0, node_test_1.default)("豆包取消后仍读取真实页面请求的迟到结果,不提前释放内部队列", async (t) => {
828
1497
  t.mock.timers.enable({ apis: ["setTimeout", "Date"] });
829
- const client = new cdp_client_1.DoubaoCdpClient({ timeoutMs: 10 });
1498
+ const client = new cdp_client_1.DoubaoCdpClient({ mode: "chat", timeoutMs: 10 });
830
1499
  const controller = new AbortController();
831
1500
  let polled = false;
832
1501
  let settled = false;