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
@@ -39,11 +39,128 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  const strict_1 = __importDefault(require("node:assert/strict"));
40
40
  const node_test_1 = __importDefault(require("node:test"));
41
41
  const node_vm_1 = require("node:vm");
42
+ const promises_1 = require("node:fs/promises");
43
+ const node_os_1 = require("node:os");
44
+ const node_path_1 = require("node:path");
42
45
  const doubao = __importStar(require("../src-ts/plugins/doubao/cdp-client"));
43
46
  const doubao_1 = require("../src-ts/plugins/doubao");
44
47
  const cdp_client_1 = require("../src-ts/plugins/doubao/cdp-client");
48
+ const attachments_1 = require("../src-ts/plugins/doubao/attachments");
45
49
  const imageUrl = "https://p3-flow-imagex-sign.byteimg.com/tos-cn-i-a9rns2rl98/rc_gen_image/test.png?signature=test";
46
50
  const png = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII=", "base64");
51
+ function deferred() {
52
+ let resolve;
53
+ let reject;
54
+ const promise = new Promise((done, fail) => { resolve = done; reject = fail; });
55
+ return { promise, resolve, reject };
56
+ }
57
+ (0, node_test_1.default)("豆包原生上传保护已有草稿,并只清理本次唯一文件名", () => {
58
+ const items = [{ name: "marker.txt", uploadName: "unique-marker.txt", type: "text/plain", size: 6, base64: "bWFya2Vy" }];
59
+ const input = { dispatchEvent() { strict_1.default.equal(this.files[0].name, "unique-marker.txt"); } };
60
+ let draft = "未发送正文";
61
+ let area;
62
+ const page = { document: { querySelector(selector) { return selector.includes("contenteditable") ? { textContent: draft } : selector.includes("attachment_area") ? area : input; } },
63
+ atob, Uint8Array, Event, File: class {
64
+ name;
65
+ constructor(_parts, name) {
66
+ this.name = name;
67
+ }
68
+ },
69
+ DataTransfer: class {
70
+ files = [];
71
+ items = { add: (file) => this.files.push(file) };
72
+ },
73
+ MouseEvent: class {
74
+ }, };
75
+ strict_1.default.match((0, node_vm_1.runInNewContext)((0, attachments_1.buildUploadInjection)(items), page), /未发送草稿/);
76
+ draft = "";
77
+ strict_1.default.equal((0, node_vm_1.runInNewContext)((0, attachments_1.buildUploadInjection)(items), page), "started");
78
+ strict_1.default.equal(Object.hasOwn(input, "files"), false);
79
+ const deleted = [];
80
+ area = { __reactFiberTest: { memoizedProps: { attachmentStates: [{ fileName: "unique-marker.txt" }, { fileName: "用户新上传.txt" }] } }, querySelectorAll: () => [0, 1].map(index => ({ dispatchEvent() { deleted.push(index); } })) };
81
+ strict_1.default.match((0, node_vm_1.runInNewContext)((0, attachments_1.buildUploadInjection)(items), page), /未发送草稿/);
82
+ (0, node_vm_1.runInNewContext)(`(${attachments_1.cleanupUploadScript})(["unique-marker.txt"])`, page);
83
+ strict_1.default.deepEqual(deleted, [0]);
84
+ });
85
+ (0, node_test_1.default)("豆包附件按原生附件块传递文件和图片,保留独立文本消息", () => {
86
+ const uploaded = [
87
+ { type: "file", fileName: "marker.txt", fileKey: "tos/file.txt", localKey: "file-key", size: 27, parseState: 1 },
88
+ { type: "image", fileName: "colors.png", fileKey: "tos/image.png", localKey: "image-key", size: 100, imageWidth: 64, imageHeight: 64 },
89
+ ];
90
+ const body = JSON.parse(cdp_client_1.requestBody("读取附件", "conversation", "bot", [], uploaded));
91
+ strict_1.default.equal(body.messages.length, 2);
92
+ const block = body.messages[0].content_block[0];
93
+ strict_1.default.equal(block.block_type, 10052);
94
+ const [file, image] = block.content.attachment_block.attachments;
95
+ strict_1.default.deepEqual(file.file, { uri: "tos/file.txt", url: "", file_type: 0, name: "marker.txt", size: 27 });
96
+ strict_1.default.equal(file.type, 3);
97
+ strict_1.default.equal(file.parse_state, 1);
98
+ strict_1.default.equal(image.type, 1);
99
+ strict_1.default.equal(image.image.uri, "tos/image.png");
100
+ strict_1.default.equal(image.image.image_ori.width, 64);
101
+ strict_1.default.equal(body.messages[1].content_block[0].content.text_block.text, "读取附件");
102
+ });
103
+ (0, node_test_1.default)("豆包插件声明附件输入并向完整客户端传递附件,旧客户端明确拒绝", async () => {
104
+ const attachment = { kind: "file", data: Buffer.from("marker"), filename: "marker.txt", contentType: "text/plain" };
105
+ let received;
106
+ const client = { async connect() { }, async close() { }, connected: () => true, direct: async () => "文本", complete: async (_text, _signal, items) => { received = items; return { text: "已读取" }; } };
107
+ const plugin = new doubao_1.DoubaoPlugin(client);
108
+ try {
109
+ await plugin.activate({ config: {} });
110
+ strict_1.default.equal(plugin.attachmentInput, true);
111
+ await plugin.handle({ task: "读取附件", attachments: [attachment] });
112
+ strict_1.default.deepEqual(received, [attachment]);
113
+ }
114
+ finally {
115
+ await plugin.deactivate();
116
+ }
117
+ const legacy = new doubao_1.DoubaoPlugin({ ...client, complete: undefined });
118
+ try {
119
+ await legacy.activate({ config: {} });
120
+ await strict_1.default.rejects(legacy.handle({ task: "读取附件", attachments: [attachment] }), /不支持附件/);
121
+ }
122
+ finally {
123
+ await legacy.deactivate();
124
+ }
125
+ });
126
+ (0, node_test_1.default)("豆包在访问页面前明确拒绝视频与不支持的文件类型", async () => {
127
+ const client = new cdp_client_1.DoubaoCdpClient();
128
+ for (const [filename, contentType, error] of [["clip.mp4", "video/mp4", /不支持视频/], ["archive.zip", "application/zip", /不支持.*文件类型/]]) {
129
+ await strict_1.default.rejects(client.complete("读取附件", undefined, [{ kind: "file", data: Buffer.from("data"), filename, contentType }]), error);
130
+ }
131
+ });
132
+ (0, node_test_1.default)("豆包上传和清理同时失败时保留两个错误", async () => {
133
+ const client = new cdp_client_1.DoubaoCdpClient({ timeoutMs: 3000 });
134
+ let uploadName = "";
135
+ client.evaluate = async (expression) => {
136
+ if (expression.includes("const transfer = new DataTransfer")) {
137
+ uploadName = expression.match(/"uploadName":"([^"]+)"/)[1];
138
+ return "started";
139
+ }
140
+ if (expression.includes("names.includes"))
141
+ throw new Error("清理测试失败");
142
+ return [{ fileName: uploadName, size: 6, status: "Error" }];
143
+ };
144
+ await strict_1.default.rejects(client.uploadAttachments([{ kind: "file", data: Buffer.from("marker"), filename: "marker.txt", contentType: "text/plain" }], new AbortController().signal), (error) => {
145
+ strict_1.default.ok(error instanceof AggregateError);
146
+ strict_1.default.match(String(error.errors[0]), /上传或解析失败/);
147
+ strict_1.default.match(String(error.errors[1]), /清理测试失败/);
148
+ return true;
149
+ });
150
+ });
151
+ (0, node_test_1.default)("豆包上传启动阶段异常也清理本次附件", async () => {
152
+ const client = new cdp_client_1.DoubaoCdpClient();
153
+ let cleaned = false;
154
+ client.evaluate = async (expression) => {
155
+ if (expression.includes("names.includes")) {
156
+ cleaned = true;
157
+ return "cleaned";
158
+ }
159
+ throw new Error("上传启动失败");
160
+ };
161
+ await strict_1.default.rejects(client.uploadAttachments([{ kind: "file", data: Buffer.from("marker"), filename: "marker.txt", contentType: "text/plain" }], new AbortController().signal), /上传启动失败/);
162
+ strict_1.default.equal(cleaned, true);
163
+ });
47
164
  (0, node_test_1.default)("豆包只提取生成完成的图片并去重,排除会话头像", () => {
48
165
  const extract = new Function(`return (${doubao.responseImagesScript})`)();
49
166
  const image = { status: 2, image_ori: { url: imageUrl, width: 2048, height: 2048 } };
@@ -51,6 +168,179 @@ const png = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR
51
168
  strict_1.default.deepEqual(extract({ avatar: { image_ori: { url: "avatar" } }, patch_op: [{ patch_value: { content_block: [block] } }] }), [imageUrl]);
52
169
  strict_1.default.deepEqual(extract(null), []);
53
170
  });
171
+ (0, node_test_1.default)("豆包只收集本轮原生块中完成的图片、文件和视频,不读取正文地址或本地路径", () => {
172
+ const extract = new Function(`return (${doubao.responseArtifactsScript})`)();
173
+ const image = { type: 1, id: "image-1", image: { status: 2, image_ori: { url: imageUrl } } };
174
+ const video = { type: 2, id: "video-1", video: { status: 3, vid: "video-id", download_url: "" } };
175
+ const imageBlock = { block_type: 2074, is_finish: true, content: { creation_block: { creations: [image, video, video, { ...video, id: "pending", video: { status: 2, vid: "pending-id" } }] } } };
176
+ const fileBlock = { block_type: 10020, block_id: "file-1", is_finish: true, content: { file_block: { name: "output.txt", uri: "tos/output.txt", url: "", path: "C:/private/output.txt", type: "txt" } } };
177
+ const result = extract({ patch_op: [{ patch_value: { content_block: [imageBlock, fileBlock, fileBlock, { ...fileBlock, is_finish: false }, { ...fileBlock, content: { file_block: { name: "private.txt", path: "C:/private/private.txt" } } }, { block_type: 10000, content: { text_block: { text: "https://example.com/fake.mp4" } } }] } }] });
178
+ strict_1.default.deepEqual(result, {
179
+ artifacts: [{ kind: "image", url: imageUrl }, { kind: "video", vid: "video-id" }, { kind: "file", filename: "output.txt", uri: "tos/output.txt" }],
180
+ pending: true,
181
+ });
182
+ strict_1.default.deepEqual(extract({ block_type: 10020, is_finish: true, content_obj: { name: "second.csv", url: "https://example.com/second.csv", type: "csv" } }).artifacts, [{ kind: "file", filename: "second.csv", url: "https://example.com/second.csv" }]);
183
+ strict_1.default.deepEqual(extract({ ...imageBlock, is_finish: false }).artifacts, []);
184
+ strict_1.default.deepEqual(extract({ ...fileBlock, is_finish: false }), { artifacts: [], pending: true });
185
+ 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 });
186
+ strict_1.default.deepEqual(extract(null), { artifacts: [], pending: false });
187
+ });
188
+ (0, node_test_1.default)("豆包异步产物只匹配同会话、本轮回复或原始问题引用", () => {
189
+ const file = { block_type: 10020, is_finish: true, content_obj: { name: "result.txt", uri: "own-file" } };
190
+ const question = "59158521582734851";
191
+ const messages = [
192
+ { message_id: "old", conversation_id: "own", ext: { reference_info: '{"referenced_message_id":1}' }, content_block: [file] },
193
+ { message_id: "other", conversation_id: "other", ext: { reference_info: `{"referenced_message_id":${question}}` }, content_block: [file] },
194
+ { message_id: "later", conversation_id: "own", ext: { reference_info: `{"referenced_message_id":${question}}` }, content_block: [file] },
195
+ ];
196
+ const page = { location: { pathname: "/chat/own" }, document: { querySelectorAll: () => messages.map(message => ({ __reactFiberTest: { memoizedProps: { message } } })) } };
197
+ const result = (0, node_vm_1.runInNewContext)(`(${doubao.currentResponseArtifactsScript})("own", ["${question}"], [])`, page);
198
+ strict_1.default.deepEqual(JSON.parse(JSON.stringify(result)), { artifacts: [{ kind: "file", filename: "result.txt", uri: "own-file" }], pending: false });
199
+ const unmatched = (0, node_vm_1.runInNewContext)(`(${doubao.currentResponseArtifactsScript})("own", ["different"], [])`, page);
200
+ strict_1.default.deepEqual(JSON.parse(JSON.stringify(unmatched)), { artifacts: [], pending: false });
201
+ });
202
+ (0, node_test_1.default)("豆包文件与视频下载限制可信域名、类型、字节和总预算", async () => {
203
+ const download = doubao.downloadDoubaoArtifact;
204
+ strict_1.default.equal(typeof download, "function");
205
+ const videoUrl = "https://v11-vdl.doubao.com/test.mp4";
206
+ const fileUrl = "https://p3-flow-sign.byteimg.com/test.txt";
207
+ const mp4 = Buffer.from("000000186674797069736f6d0000020069736f6d6d703431", "hex");
208
+ const fetchVideo = async (_url, init) => { strict_1.default.equal(init.redirect, "error"); return new Response(mp4, { headers: { "content-type": "video/mp4" } }); };
209
+ const video = await download({ kind: "video", url: videoUrl }, fetchVideo);
210
+ strict_1.default.equal(video.kind, "file");
211
+ strict_1.default.equal(video.contentType, "video/mp4");
212
+ strict_1.default.deepEqual(video.data, mp4);
213
+ strict_1.default.match(video.filename, /\.mp4$/);
214
+ const file = await download({ kind: "file", url: fileUrl, filename: "../result.txt" }, async () => new Response("marker", { headers: { "content-type": "text/plain;charset=utf-8" } }));
215
+ strict_1.default.equal(file.filename, "result.txt");
216
+ strict_1.default.equal(file.contentType, "text/plain");
217
+ for (const url of ["http://127.0.0.1/test", "https://example.com/test", "https://v11-vdl.doubao.com.evil.com/test", "https://user:pass@p3-flow-sign.byteimg.com/test", "https://p3-flow-sign.byteimg.com:444/test"]) {
218
+ await strict_1.default.rejects(download({ kind: "file", url, filename: "result.txt" }, () => { strict_1.default.fail("非法地址不得请求"); }), /地址/);
219
+ }
220
+ await strict_1.default.rejects(download({ kind: "video", url: videoUrl }, async () => new Response("html", { headers: { "content-type": "video/mp4" } })), /内容与类型/);
221
+ await strict_1.default.rejects(download({ kind: "video", url: videoUrl }, async () => new Response(mp4, { headers: { "content-type": "text/html" } })), /类型/);
222
+ await strict_1.default.rejects(download({ kind: "file", url: fileUrl, filename: "result.txt" }, async () => new Response("marker", { headers: { "content-type": "text/plain", "content-length": "40000000" } })), /过大/);
223
+ await strict_1.default.rejects(download({ kind: "file", url: fileUrl, filename: "result.txt" }, async () => new Response("marker", { headers: { "content-type": "text/plain" } }), undefined, 3), /过大/);
224
+ });
225
+ (0, node_test_1.default)("豆包完整回复等待本轮异步视频,并将图片、文件、视频统一回传", async () => {
226
+ const videoUrl = "https://v26-show.douyinvod.com/video.mp4", fileUrl = "https://p3-flow-sign.byteimg.com/output.txt";
227
+ const mp4 = Buffer.from("000000186674797069736f6d0000020069736f6d6d703431", "hex");
228
+ const image = { block_type: 2074, is_finish: true, content: { creation_block: { creations: [{ type: 1, image: { status: 2, image_ori: { url: imageUrl } } }] } } };
229
+ const file = { block_type: 10020, is_finish: true, content: { file_block: { uri: "own-file", name: "output.txt" } } };
230
+ const video = { block_type: 2074, is_finish: true, content_obj: { creations: [{ type: 2, video: { status: 3, vid: "own-video" } }] } };
231
+ const stream = [
232
+ ["STREAM_MSG_NOTIFY", { meta: { conversation_id: "other" }, content: { content_block: [{ ...file, content: { file_block: { uri: "other-file", name: "other.txt" } } }] } }],
233
+ ["SSE_ACK", { ack_client_meta: { conversation_id: "own" }, query_list: [{ question_id: "59158521582734851" }] }],
234
+ ["STREAM_MSG_NOTIFY", { meta: { message_id: "reply", conversation_id: "own", user_type: 2 }, content: { content_block: [image, file], ext: { has_video_gen: "1" }, tts_content: "已提交生成" } }],
235
+ ["SSE_REPLY_END", {}],
236
+ ].map(([event, data]) => `event: ${event}\ndata: ${JSON.stringify(data).replace(/"question_id":"(\d+)"/g, '"question_id":$1')}\n\n`).join("");
237
+ const calls = [];
238
+ const message = { message_id: "later", conversation_id: "own", ext: { reference_info: '{"referenced_message_id":59158521582734851}' }, content_block: [video] };
239
+ const page = { window: {}, navigator: { userAgent: "Chrome/147.0.0.0 SamanthaDoubao/2.32.6" }, location: { pathname: "/chat/own" }, performance: { getEntriesByType: () => [] }, URL, AbortController, TextDecoder,
240
+ document: { querySelectorAll: () => [{ __reactFiberTest: { memoizedProps: { message } } }] },
241
+ fetch: async (input, init) => {
242
+ const path = new URL(input).pathname;
243
+ if (path === "/chat/completion")
244
+ return new Response(stream);
245
+ calls.push(path);
246
+ if (path === "/alice/message/get_file_url") {
247
+ strict_1.default.deepEqual(JSON.parse(String(init.body)), { uris: ["own-file"], type: "file" });
248
+ return new Response(JSON.stringify({ code: 0, data: { file_urls: [{ uri: "own-file", main_url: fileUrl }] } }));
249
+ }
250
+ strict_1.default.equal(path, "/samantha/video/get_play_info");
251
+ strict_1.default.deepEqual(JSON.parse(String(init.body)), { vid: "own-video" });
252
+ return new Response(JSON.stringify({ code: 0, data: { play_infos: [{ main: videoUrl }] } }));
253
+ },
254
+ };
255
+ 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" } }) });
256
+ client.connect = async () => { };
257
+ client.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, page);
258
+ const result = await client.complete("生成产物");
259
+ strict_1.default.equal(result.images.length, 1);
260
+ strict_1.default.deepEqual(result.attachments.map((item) => item.contentType).sort(), ["image/png", "text/plain", "video/mp4"]);
261
+ strict_1.default.deepEqual(calls.sort(), ["/alice/message/get_file_url", "/samantha/video/get_play_info"]);
262
+ });
263
+ (0, node_test_1.default)("豆包并发请求串行完成上传、回复和下载后再开始下一轮", { timeout: 10_000 }, async () => {
264
+ const uploadStarted = deferred(), uploadGate = deferred();
265
+ const requestStarted = deferred(), requestGate = deferred();
266
+ const downloadStarted = deferred(), downloadGate = deferred();
267
+ const uploads = [], requests = [];
268
+ const block = { block_type: 2074, is_finish: true, content: { creation_block: { creations: [{ image: { status: 2, image_ori: { url: imageUrl } } }] } } };
269
+ const page = {
270
+ window: {}, URL, AbortController, TextDecoder,
271
+ navigator: { userAgent: "Chrome/147.0.0.0 SamanthaDoubao/2.32.6" }, performance: { getEntriesByType: () => [] },
272
+ fetch: async (_url, init) => {
273
+ const text = JSON.parse(String(init.body)).messages.at(-1).content_block[0].content.text_block.text;
274
+ requests.push(text);
275
+ if (text === "请求甲") {
276
+ requestStarted.resolve();
277
+ await requestGate.promise;
278
+ }
279
+ 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`);
280
+ },
281
+ };
282
+ const client = new cdp_client_1.DoubaoCdpClient({ conversationId: "test", timeoutMs: 5000, fetchImpl: async () => {
283
+ downloadStarted.resolve();
284
+ await downloadGate.promise;
285
+ return new Response(png, { headers: { "content-type": "image/png" } });
286
+ } });
287
+ client.connect = async () => { };
288
+ client.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, page);
289
+ client.uploadAttachments = async (items) => {
290
+ uploads.push(items[0].filename);
291
+ if (items[0].filename === "甲.txt") {
292
+ uploadStarted.resolve();
293
+ await uploadGate.promise;
294
+ }
295
+ return [];
296
+ };
297
+ const attachment = (filename) => [{ kind: "file", data: Buffer.from("marker"), contentType: "text/plain", filename }];
298
+ const first = client.complete("请求甲", undefined, attachment("甲.txt"));
299
+ await uploadStarted.promise;
300
+ const second = client.complete("请求乙", undefined, attachment("乙.txt"));
301
+ await new Promise(setImmediate);
302
+ const duringUpload = { uploads: [...uploads], requests: [...requests] };
303
+ uploadGate.resolve();
304
+ await requestStarted.promise;
305
+ requestGate.resolve();
306
+ await downloadStarted.promise;
307
+ const duringDownload = { uploads: [...uploads], requests: [...requests] };
308
+ downloadGate.resolve();
309
+ const results = await Promise.all([first, second]);
310
+ strict_1.default.deepEqual(duringUpload, { uploads: ["甲.txt"], requests: [] });
311
+ strict_1.default.deepEqual(duringDownload, { uploads: ["甲.txt"], requests: ["请求甲"] });
312
+ strict_1.default.deepEqual(results.map(item => item.text), ["第一轮回复", "第二轮回复"]);
313
+ strict_1.default.equal(results[0].attachments.length, 1);
314
+ strict_1.default.equal(results[1].attachments, undefined);
315
+ });
316
+ for (const late of ["成功", "失败"]) {
317
+ (0, node_test_1.default)(`豆包前轮超时后的晚${late}回调不覆盖下一轮结果`, { timeout: 10_000 }, async () => {
318
+ const firstResponse = deferred(), nextResponse = deferred(), nextStarted = deferred();
319
+ let requests = 0;
320
+ const page = {
321
+ window: {}, URL, AbortController, TextDecoder,
322
+ navigator: { userAgent: "Chrome/147.0.0.0 SamanthaDoubao/2.32.6" }, performance: { getEntriesByType: () => [] },
323
+ fetch: () => { if (++requests === 1)
324
+ return firstResponse.promise; nextStarted.resolve(); return nextResponse.promise; },
325
+ };
326
+ const client = new cdp_client_1.DoubaoCdpClient({ conversationId: "test", timeoutMs: 20 });
327
+ client.connect = async () => { };
328
+ client.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, page);
329
+ await strict_1.default.rejects(client.complete("请求甲"), /等待豆包回复超时/);
330
+ const next = client.complete("请求乙");
331
+ await nextStarted.promise;
332
+ if (late === "成功")
333
+ firstResponse.resolve(new Response('event: CHUNK_DELTA\ndata: {"text":"过期回复"}\n\n'));
334
+ else
335
+ firstResponse.reject(new Error("过期错误"));
336
+ await new Promise(setImmediate);
337
+ const resultAfterLateCallback = page.window.__mab_doubao_result;
338
+ nextResponse.resolve(new Response('event: CHUNK_DELTA\ndata: {"text":"本轮回复"}\n\n'));
339
+ const result = await next;
340
+ strict_1.default.equal(resultAfterLateCallback, null);
341
+ strict_1.default.equal(result.text, "本轮回复");
342
+ });
343
+ }
54
344
  (0, node_test_1.default)("豆包图片结果仍清理回复中重复的用户问题", () => {
55
345
  strict_1.default.equal(doubao.cleanDoubaoReply("生成宇航员橘猫图片已为你生成。", "生成宇航员橘猫图片"), "已为你生成。");
56
346
  strict_1.default.equal(doubao.cleanDoubaoReply("已生成图片。", "生成宇航员橘猫图片"), "已生成图片。");
@@ -62,8 +352,28 @@ const png = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR
62
352
  const original = globalThis.WebSocket;
63
353
  const block = { block_type: 2074, is_finish: true, content: { creation_block: { creations: [{ image: { status: 2, image_ori: { url: imageUrl } } }] } } };
64
354
  let stream = `event: STREAM_CHUNK\ndata: ${JSON.stringify({ patch_op: [{ patch_value: { content_block: [block], tts_content: "生成宇航员橘猫图片已为你生成。" } }] })}\n\nevent: SSE_REPLY_END\ndata: {}\n\n`;
65
- const page = { window: {}, performance: { getEntriesByType: () => [] }, URL, AbortController, TextDecoder, fetch: async () => new Response(stream) };
355
+ const requests = [];
356
+ const bodies = [];
357
+ let draft = "", messages = 2;
358
+ const page = {
359
+ location: { pathname: "/chat/123", href: "chrome://doubao-chat/chat/123" },
360
+ document: {
361
+ querySelector: (selector) => selector.includes("create_conversation_button")
362
+ ? { getAttribute() { return null; }, click() { page.location.pathname = "/chat"; messages = 0; } }
363
+ : { get innerText() { return draft; } },
364
+ querySelectorAll: () => Array.from({ length: messages }),
365
+ },
366
+ window: {}, navigator: { userAgent: "Chrome/147.0.0.0 SamanthaDoubao/2.32.6" },
367
+ performance: { getEntriesByType: () => [
368
+ { name: "https://www.doubao.com/settings?pc_version=2.32.6&device_id=current-device&tea_uuid=current-device&web_id=current-web&web_tab_id=current-tab" },
369
+ { name: "https://www.doubao.com/chat/completion?pc_version=2.30.4&device_id=old-device" },
370
+ { name: "https://evil.example/chat/completion?device_id=untrusted" },
371
+ ] },
372
+ URL, AbortController, TextDecoder,
373
+ fetch: async (address, options) => { requests.push(new URL(address)); bodies.push(JSON.parse(options.body)); return new Response(stream); },
374
+ };
66
375
  class TestWebSocket {
376
+ readyState = 1;
67
377
  onopen;
68
378
  onmessage;
69
379
  constructor() { queueMicrotask(() => this.onopen?.()); }
@@ -98,6 +408,12 @@ const png = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR
98
408
  strict_1.default.deepEqual(Buffer.from(result.images[0].data), png);
99
409
  strict_1.default.equal(result.images[0].contentType, "image/png");
100
410
  strict_1.default.equal(downloads, 1);
411
+ strict_1.default.equal(requests[0].origin, "https://www.doubao.com");
412
+ strict_1.default.equal(requests[0].searchParams.get("pc_version"), "2.32.6");
413
+ strict_1.default.equal(requests[0].searchParams.get("doubao_pc_version"), "2.32.6");
414
+ strict_1.default.equal(requests[0].searchParams.get("device_id"), "current-device");
415
+ strict_1.default.equal(requests[0].searchParams.get("web_id"), "current-web");
416
+ page.navigator.userAgent = "Chrome/148.0.1.2 SamanthaDoubao/2.33.0";
101
417
  const chunks = ["天空是蓝色。海水也是", "蓝色", ",这是光散射的结果。"];
102
418
  for (const event of ["CHUNK_DELTA", "STREAM_CHUNK"]) {
103
419
  stream = chunks.map(text => `event: ${event}\ndata: ${JSON.stringify(event === "CHUNK_DELTA" ? { text } : { patch_op: [{ patch_value: { tts_content: text } }] })}\n\n`).join("");
@@ -105,7 +421,27 @@ const png = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR
105
421
  const reply = await client.complete("请继续");
106
422
  strict_1.default.equal(reply.text, chunks.join(""), event);
107
423
  strict_1.default.equal(reply.images?.length, 1);
424
+ strict_1.default.equal(requests.at(-1)?.searchParams.get("pc_version"), "2.33.0");
425
+ strict_1.default.equal(requests.at(-1)?.searchParams.get("chromium_version"), "148.0.1.2");
108
426
  }
427
+ strict_1.default.equal(typeof client.createSession, "function");
428
+ draft = "用户尚未发送的内容";
429
+ await strict_1.default.rejects(client.createSession(), /未发送内容/);
430
+ draft = "";
431
+ const count = bodies.length;
432
+ await client.createSession();
433
+ strict_1.default.equal(page.location.pathname, "/chat");
434
+ strict_1.default.equal(bodies.length, count, "新建入口不发送占位消息");
435
+ stream = 'event: SSE_ACK\ndata: {"ack_client_meta":{"conversation_id":"456"}}\n\nevent: CHUNK_DELTA\ndata: {"text":"新会话已回复"}\n\n';
436
+ const fresh = await client.complete("新会话首条消息", undefined, undefined, "");
437
+ strict_1.default.equal(fresh.sessionId, "456");
438
+ strict_1.default.equal(bodies.at(-1).client_meta.conversation_id, "");
439
+ strict_1.default.match(bodies.at(-1).client_meta.local_conversation_id, /^local_/);
440
+ strict_1.default.equal(bodies.at(-1).option.need_create_conversation, true);
441
+ strict_1.default.equal(bodies.at(-1).option.conversation_init_option.need_ack_conversation, true);
442
+ await client.complete("继续回复", undefined, undefined, fresh.sessionId);
443
+ strict_1.default.equal(bodies.at(-1).client_meta.conversation_id, "456");
444
+ strict_1.default.equal(bodies.at(-1).option.need_create_conversation, false);
109
445
  }
110
446
  finally {
111
447
  await client.close();
@@ -177,10 +513,30 @@ const png = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR
177
513
  (0, node_test_1.default)("豆包错误兼容旧版 code 字段", () => {
178
514
  strict_1.default.equal((0, cdp_client_1.doubaoStreamErrorMessage)(JSON.stringify({ code: "E_LIMIT" })), "豆包平台返回流式错误(错误码 E_LIMIT)");
179
515
  });
516
+ (0, node_test_1.default)("豆包风控滑块验证优先于 rate limited 文案,且不泄露验证令牌", () => {
517
+ const decision = { type: "verify", subtype: "slide", detail: "private-challenge", log_id: "private-log" };
518
+ for (const value of [decision, JSON.stringify(decision)]) {
519
+ const message = (0, cdp_client_1.doubaoStreamErrorMessage)(JSON.stringify({ error_code: 710022004, error_msg: "rate limited", extra: { decision: value } }));
520
+ strict_1.default.match(message, /710022004.*滑块验证.*豆包桌面端/);
521
+ strict_1.default.doesNotMatch(message, /限流|rate limited|private-/);
522
+ }
523
+ strict_1.default.doesNotThrow(() => (0, cdp_client_1.doubaoStreamErrorMessage)('{"extra":{"decision":"无效 JSON"}}'));
524
+ });
525
+ (0, node_test_1.default)("豆包新建会话协议区分空会话和已有会话", () => {
526
+ const fresh = JSON.parse((0, cdp_client_1.requestBody)("你好", "", "bot"));
527
+ strict_1.default.equal(fresh.option.need_create_conversation, true);
528
+ strict_1.default.equal(fresh.option.conversation_init_option.need_ack_conversation, true);
529
+ strict_1.default.match(fresh.client_meta.local_conversation_id, /^local_/);
530
+ strict_1.default.notEqual(fresh.client_meta.local_conversation_id, JSON.parse((0, cdp_client_1.requestBody)("你好", "", "bot")).client_meta.local_conversation_id);
531
+ const existing = JSON.parse((0, cdp_client_1.requestBody)("继续", "123", "bot"));
532
+ strict_1.default.equal(existing.option.need_create_conversation, false);
533
+ strict_1.default.equal(existing.client_meta.local_conversation_id, undefined);
534
+ });
180
535
  (0, node_test_1.default)("停止豆包客户端时向页面发送请求取消指令", async () => {
181
536
  const original = globalThis.WebSocket;
182
537
  const messages = [];
183
538
  class TestWebSocket {
539
+ readyState = 1;
184
540
  onopen;
185
541
  onmessage;
186
542
  constructor(_url) { queueMicrotask(() => this.onopen?.()); }
@@ -198,7 +554,7 @@ const png = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR
198
554
  const pending = client.direct("停止测试").catch((error) => String(error));
199
555
  await new Promise((resolve) => setImmediate(resolve));
200
556
  await client.cancel();
201
- strict_1.default.match(messages.map((message) => JSON.parse(message).params?.expression ?? "").join("\n"), /new AbortController\(\).*signal: window\.__mab_doubao_controller\.signal/s);
557
+ strict_1.default.match(messages.map((message) => JSON.parse(message).params?.expression ?? "").join("\n"), /new AbortController\(\).*signal: controller\.signal/s);
202
558
  strict_1.default.match(messages.at(-1) ?? "", /__mab_doubao_controller\?\.abort\(\)/);
203
559
  strict_1.default.match(await pending, /请求已取消/);
204
560
  client.close();
@@ -207,3 +563,194 @@ const png = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR
207
563
  globalThis.WebSocket = original;
208
564
  }
209
565
  });
566
+ (0, node_test_1.default)("豆包并发连接复用同一次页面发现和 WebSocket,活动命令保持可用", async () => {
567
+ const original = globalThis.WebSocket;
568
+ const sockets = [], secondDiscovery = deferred();
569
+ let discoveries = 0, finishActive;
570
+ class TestWebSocket {
571
+ readyState = 0;
572
+ onopen;
573
+ onclose;
574
+ onmessage;
575
+ constructor(_url) { sockets.push(this); if (sockets.length === 1)
576
+ queueMicrotask(() => this.open()); }
577
+ open() { this.readyState = 1; this.onopen?.(); }
578
+ send(raw) {
579
+ const request = JSON.parse(raw);
580
+ const reply = () => this.onmessage?.({ data: JSON.stringify({ id: request.id, result: { result: { value: "活动页面回复" } } }) });
581
+ if (request.params.expression === "活动命令")
582
+ finishActive = reply;
583
+ else
584
+ queueMicrotask(reply);
585
+ }
586
+ close() { this.readyState = 3; this.onclose?.(); }
587
+ }
588
+ globalThis.WebSocket = TestWebSocket;
589
+ const client = new cdp_client_1.DoubaoCdpClient({ fetchImpl: async () => {
590
+ if (++discoveries === 2)
591
+ await secondDiscovery.promise;
592
+ return new Response(JSON.stringify([{ type: "page", url: "doubao://doubao-chat/chat/test", webSocketDebuggerUrl: "ws://test" }]));
593
+ } });
594
+ try {
595
+ const first = client.connect(), second = client.connect();
596
+ await first;
597
+ const active = client.evaluate("活动命令");
598
+ secondDiscovery.resolve();
599
+ await new Promise(setImmediate);
600
+ const readyDuringReconnect = client.connected();
601
+ const followup = client.evaluate("后续命令").catch((error) => error.message);
602
+ sockets[1]?.open();
603
+ await second;
604
+ finishActive();
605
+ strict_1.default.equal(await active, "活动页面回复");
606
+ strict_1.default.equal(await followup, "活动页面回复");
607
+ strict_1.default.equal(readyDuringReconnect, true);
608
+ strict_1.default.equal(discoveries, 1);
609
+ strict_1.default.equal(sockets.length, 1);
610
+ }
611
+ finally {
612
+ await client.close();
613
+ globalThis.WebSocket = original;
614
+ }
615
+ });
616
+ (0, node_test_1.default)("豆包更新断开 CDP 后清理待响应命令,下次请求连接新页面", async () => {
617
+ const original = globalThis.WebSocket;
618
+ const sockets = [];
619
+ const prompts = [];
620
+ class TestWebSocket {
621
+ url;
622
+ readyState = 0;
623
+ onopen;
624
+ onclose;
625
+ onmessage;
626
+ constructor(url) {
627
+ this.url = url;
628
+ sockets.push(this);
629
+ queueMicrotask(() => { this.readyState = 1; this.onopen?.(); });
630
+ }
631
+ send(raw) {
632
+ const request = JSON.parse(raw);
633
+ const expression = request.params.expression;
634
+ if (expression === "等待旧页面")
635
+ return;
636
+ if (expression.includes("new AbortController()"))
637
+ prompts.push(expression);
638
+ const value = expression === "window.__mab_doubao_result" ? JSON.stringify({ ok: true, reply: "连接已恢复" }) : "started";
639
+ queueMicrotask(() => this.onmessage?.({ data: JSON.stringify({ id: request.id, result: { result: { value } } }) }));
640
+ }
641
+ close() { this.readyState = 3; this.onclose?.(); }
642
+ }
643
+ globalThis.WebSocket = TestWebSocket;
644
+ let discoveries = 0;
645
+ const client = new cdp_client_1.DoubaoCdpClient({ timeoutMs: 2000, fetchImpl: async () => new Response(JSON.stringify([
646
+ { type: "page", url: "doubao://doubao-chat/chat/test", webSocketDebuggerUrl: `ws://page-${++discoveries}` },
647
+ ])) });
648
+ try {
649
+ await client.connect();
650
+ const pending = client.evaluate("等待旧页面");
651
+ const rejected = strict_1.default.rejects(pending, /豆包 CDP 已断开/);
652
+ sockets[0].close();
653
+ strict_1.default.equal(client.connected(), false);
654
+ await rejected;
655
+ strict_1.default.equal(await client.direct("更新后验证"), "连接已恢复");
656
+ strict_1.default.equal(sockets[1].url, "ws://page-2");
657
+ strict_1.default.equal(client.connected(), true);
658
+ strict_1.default.equal(prompts.length, 1);
659
+ }
660
+ finally {
661
+ await client.close();
662
+ globalThis.WebSocket = original;
663
+ }
664
+ });
665
+ (0, node_test_1.default)("豆包插件在下一次任务前重新执行连接和启动流程", async () => {
666
+ let connected = false, connections = 0;
667
+ const client = {
668
+ async connect() { connections++; connected = true; },
669
+ async close() { connected = false; }, connected: () => connected,
670
+ async direct() { strict_1.default.ok(connected, "发送前应恢复连接"); return "新版已连接"; },
671
+ };
672
+ const plugin = new doubao_1.DoubaoPlugin(client);
673
+ try {
674
+ await plugin.activate({ config: {} });
675
+ connected = false;
676
+ strict_1.default.equal((await plugin.health()).status, "degraded");
677
+ strict_1.default.deepEqual(await plugin.handle({ task: "升级后测试" }), { text: "新版已连接" });
678
+ strict_1.default.equal(connections, 2);
679
+ strict_1.default.equal((await plugin.health()).status, "ready");
680
+ }
681
+ finally {
682
+ await plugin.deactivate();
683
+ }
684
+ });
685
+ (0, node_test_1.default)("豆包 /new 切换独立会话,重载后仍使用服务端会话标识", async () => {
686
+ const dataDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "doubao-sessions-"));
687
+ const targets = [];
688
+ let created = 0, failCreate = false;
689
+ const client = {
690
+ async connect() { }, async close() { }, connected: () => true, async direct() { return "旧接口"; },
691
+ async createSession() { if (failCreate)
692
+ throw new Error("未发送内容"); created++; },
693
+ async complete(_text, _signal, _attachments, conversationId) {
694
+ targets.push(conversationId);
695
+ return { text: "回复", sessionId: conversationId || String(100 + targets.length) };
696
+ },
697
+ };
698
+ let plugin = new doubao_1.DoubaoPlugin(client);
699
+ try {
700
+ await plugin.activate({ config: {}, dataDir });
701
+ strict_1.default.deepEqual(plugin.sessionOperations, ["new"]);
702
+ const first = await plugin.handle({ task: "首条消息", sessionId: "peer-a" });
703
+ strict_1.default.equal(first.sessionId, "peer-a");
704
+ strict_1.default.equal(targets.at(-1), "");
705
+ await plugin.handle({ task: "继续", sessionId: "peer-a" });
706
+ strict_1.default.equal(targets.at(-1), "101");
707
+ strict_1.default.deepEqual(await plugin.controlSession({ operation: "new", sessionId: "peer-b", name: "新会话" }), { text: "豆包已创建新会话", sessionId: "peer-b" });
708
+ strict_1.default.equal(created, 1);
709
+ await plugin.deactivate();
710
+ plugin = new doubao_1.DoubaoPlugin(client);
711
+ await plugin.activate({ config: {}, dataDir });
712
+ await plugin.handle({ task: "新会话消息", sessionId: "peer-b", newSession: true });
713
+ strict_1.default.equal(targets.at(-1), "");
714
+ await plugin.handle({ task: "继续新会话", sessionId: "peer-b" });
715
+ strict_1.default.equal(targets.at(-1), "103");
716
+ await plugin.handle({ task: "回到旧会话", sessionId: "peer-a" });
717
+ strict_1.default.equal(targets.at(-1), "101");
718
+ failCreate = true;
719
+ await strict_1.default.rejects(plugin.controlSession({ operation: "new", sessionId: "peer-c" }), /未发送内容/);
720
+ await plugin.handle({ task: "失败后继续原会话", sessionId: "peer-b" });
721
+ strict_1.default.equal(targets.at(-1), "103");
722
+ const saved = await (0, promises_1.readFile)((0, node_path_1.join)(dataDir, "sessions.json"), "utf8");
723
+ strict_1.default.doesNotMatch(saved, /首条消息|新会话消息|回到旧会话/);
724
+ }
725
+ finally {
726
+ await plugin.deactivate();
727
+ await (0, promises_1.rm)(dataDir, { recursive: true, force: true });
728
+ }
729
+ });
730
+ (0, node_test_1.default)("豆包跨来源请求串行执行,忙时不允许新建会话", async () => {
731
+ let release;
732
+ const blocked = new Promise(resolve => { release = resolve; });
733
+ let started;
734
+ const running = new Promise(resolve => { started = resolve; });
735
+ let calls = 0, active = 0, peak = 0;
736
+ const plugin = new doubao_1.DoubaoPlugin({
737
+ async connect() { }, async close() { }, connected: () => true, async direct() { return ""; }, async createSession() { },
738
+ async complete() { calls++; active++; peak = Math.max(peak, active); started(); if (calls === 1)
739
+ await blocked; active--; return { text: "回复", sessionId: String(calls) }; },
740
+ });
741
+ try {
742
+ await plugin.activate({ config: {} });
743
+ const first = plugin.handle({ task: "一", sessionId: "a" });
744
+ const second = plugin.handle({ task: "二", sessionId: "b" });
745
+ await running;
746
+ await strict_1.default.rejects(plugin.controlSession({ operation: "new", sessionId: "c" }), /当前忙/);
747
+ release();
748
+ await Promise.all([first, second]);
749
+ strict_1.default.equal(peak, 1);
750
+ strict_1.default.equal(calls, 2);
751
+ }
752
+ finally {
753
+ release();
754
+ await plugin.deactivate();
755
+ }
756
+ });