mu-agent-bridge 1.0.35 → 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 (59) hide show
  1. package/README.md +2 -0
  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/plugin-manager.js +47 -0
  5. package/dist/src-ts/plugins/chatgpt/cdp-client.js +72 -0
  6. package/dist/src-ts/plugins/chatgpt/index.js +122 -4
  7. package/dist/src-ts/plugins/doubao/cdp-client.js +243 -18
  8. package/dist/src-ts/plugins/doubao/index.js +81 -5
  9. package/dist/src-ts/plugins/doubao/session-history.js +123 -0
  10. package/dist/src-ts/plugins/dsh/index.js +16 -2
  11. package/dist/src-ts/plugins/dsh/session-history.js +371 -0
  12. package/dist/src-ts/plugins/qwenwork/cdp-client.js +8 -0
  13. package/dist/src-ts/plugins/qwenwork/index.js +12 -1
  14. package/dist/src-ts/plugins/qwenwork/session-history.js +245 -0
  15. package/dist/src-ts/plugins/workbuddy/cdp-client.js +33 -0
  16. package/dist/src-ts/plugins/workbuddy/index.js +21 -1
  17. package/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
  18. package/dist/src-ts/session-archive.js +509 -0
  19. package/dist/src-ts/session-history.js +2 -0
  20. package/dist/src-ts/session-management.js +665 -0
  21. package/dist/src-ts/session-summary.js +171 -0
  22. package/dist/tests-ts/channel-sessions.test.js +233 -2
  23. package/dist/tests-ts/chatgpt-history.test.js +197 -0
  24. package/dist/tests-ts/control-server.test.js +39 -0
  25. package/dist/tests-ts/doubao-history.test.js +172 -0
  26. package/dist/tests-ts/doubao-plugin.test.js +124 -3
  27. package/dist/tests-ts/dsh-history.test.js +165 -0
  28. package/dist/tests-ts/native-session-management.test.js +195 -0
  29. package/dist/tests-ts/plugin-manager.test.js +254 -3
  30. package/dist/tests-ts/qwenwork-history.test.js +339 -0
  31. package/dist/tests-ts/session-archive.test.js +422 -0
  32. package/dist/tests-ts/session-management.test.js +323 -0
  33. package/dist/tests-ts/session-summary.test.js +199 -0
  34. package/dist/tests-ts/workbuddy-history.test.js +256 -0
  35. package/package.json +1 -1
  36. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +72 -0
  37. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +122 -4
  38. package/plugins/chatgpt/package.json +1 -1
  39. package/plugins/chatgpt/plugin.json +14 -1
  40. package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +243 -18
  41. package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +81 -5
  42. package/plugins/doubao/dist/src-ts/plugins/doubao/session-history.js +123 -0
  43. package/plugins/doubao/package.json +1 -1
  44. package/plugins/doubao/plugin.json +1 -1
  45. package/plugins/dsh/config.schema.json +2 -1
  46. package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +16 -2
  47. package/plugins/dsh/dist/src-ts/plugins/dsh/session-history.js +371 -0
  48. package/plugins/dsh/package.json +1 -1
  49. package/plugins/dsh/plugin.json +1 -1
  50. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +8 -0
  51. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +12 -1
  52. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/session-history.js +245 -0
  53. package/plugins/qwenwork/package.json +1 -1
  54. package/plugins/qwenwork/plugin.json +1 -1
  55. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +33 -0
  56. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +21 -1
  57. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
  58. package/plugins/workbuddy/package.json +1 -1
  59. package/plugins/workbuddy/plugin.json +1 -1
@@ -438,3 +438,42 @@ const plugin_manager_1 = require("../src-ts/plugin-manager");
438
438
  }
439
439
  strict_1.default.deepEqual(calls, [{ task: "测试" }]);
440
440
  });
441
+ (0, node_test_1.default)("会话归档接口只允许本机或认证调用方,并限制配置请求大小", async () => {
442
+ const calls = [];
443
+ const sessionManagement = {
444
+ providers: () => [{ pluginId: "demo", scope: "local:one", mode: "current", limitation: "仅当前会话", status: "ready" }],
445
+ config: async () => ({ enabled: false, grants: [] }),
446
+ saveConfig: async (...args) => { calls.push(["config", ...args]); },
447
+ deleteSession: async (...args) => { calls.push(["session", ...args]); },
448
+ deleteSource: async (...args) => { calls.push(["source", ...args]); },
449
+ clear: async (...args) => { calls.push(["clear", ...args]); },
450
+ };
451
+ const server = new control_server_1.ControlServer({ manager: { sessionManagement, dispatchHttp: async () => false }, token: "local-secret" });
452
+ const invoke = async (method, path, body, remote = false, token) => {
453
+ const req = Object.assign(node_stream_1.Readable.from(body === undefined ? [] : [JSON.stringify(body)]), {
454
+ method, url: path, headers: token ? { authorization: `Bearer ${token}` } : {},
455
+ socket: { remoteAddress: remote ? "192.168.31.10" : "127.0.0.1" },
456
+ });
457
+ let result = "";
458
+ const res = { statusCode: 0, setHeader() { }, end(text) { result = text; } };
459
+ await server.handle(req, res);
460
+ return { status: res.statusCode, body: JSON.parse(result) };
461
+ };
462
+ strict_1.default.equal((await invoke("GET", "/api/session-history/providers", undefined, true)).status, 401);
463
+ strict_1.default.deepEqual((await invoke("GET", "/api/session-history/providers")).body, sessionManagement.providers());
464
+ strict_1.default.deepEqual((await invoke("GET", "/api/session-history/providers", undefined, true, "local-secret")).body, sessionManagement.providers());
465
+ strict_1.default.equal((await invoke("GET", "/api/session-history/config", undefined, true)).status, 401);
466
+ strict_1.default.deepEqual((await invoke("GET", "/api/session-history/config")).body, { enabled: false, grants: [] });
467
+ strict_1.default.equal((await invoke("PATCH", "/api/session-history/config", { enabled: true, grants: [] }, true, "local-secret")).status, 200);
468
+ strict_1.default.equal((await invoke("DELETE", "/api/session-history/sessions/session-1")).status, 200);
469
+ strict_1.default.equal((await invoke("POST", "/api/session-history/delete-source", { source: "source-1" })).status, 200);
470
+ strict_1.default.equal((await invoke("DELETE", "/api/session-history")).status, 200);
471
+ strict_1.default.equal((await invoke("POST", "/api/session-history/delete-source", { source: 42 })).status, 400);
472
+ strict_1.default.equal((await invoke("PATCH", "/api/session-history/config", { payload: "字".repeat(30_000) })).status, 413);
473
+ strict_1.default.deepEqual(calls.map((call) => call[0]), ["config", "session", "source", "clear"]);
474
+ strict_1.default.ok(!JSON.stringify(calls).includes("local-secret"));
475
+ const anonymousServer = new control_server_1.ControlServer({ manager: { sessionManagement } });
476
+ const response = { setHeader() { }, end() { } };
477
+ await anonymousServer.handle({ method: "DELETE", url: "/api/session-history", headers: {}, socket: { remoteAddress: "192.168.31.10" } }, response);
478
+ strict_1.default.equal(response.statusCode, 401);
479
+ });
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const strict_1 = __importDefault(require("node:assert/strict"));
7
+ const node_test_1 = __importDefault(require("node:test"));
8
+ const node_vm_1 = require("node:vm");
9
+ const session_history_1 = require("../src-ts/plugins/doubao/session-history");
10
+ const cdp_client_1 = require("../src-ts/plugins/doubao/cdp-client");
11
+ const rawConversation = {
12
+ conversation_id: "conv-1", name: "今天的问题", create_time: "1792555702", update_time: "1792555770",
13
+ latest_index: "2", messages: [
14
+ { message_id: "user-1", user_type: 2, index_in_conv: "1", create_time: "1792555702", tts_content: "修复图片回传" },
15
+ { message_id: "assistant-1", user_type: 1, index_in_conv: "2", create_time: "1792555770", tts_content: "已修复并验证" },
16
+ ],
17
+ };
18
+ (0, node_test_1.default)("豆包只读历史脚本解析原生会话目录、Unix 时间与角色文本", async () => {
19
+ const require = (id) => {
20
+ strict_1.default.equal(id, 55807);
21
+ return { V: async (query) => {
22
+ strict_1.default.equal(query.conv_version, "cursor-1");
23
+ strict_1.default.equal(query.message_count_per_conv, 100);
24
+ return { code: 0, data: { cells: [{ conversation: rawConversation }], has_more: false, next_cursor: "" } };
25
+ } };
26
+ };
27
+ const chunks = [];
28
+ chunks.push = (...items) => {
29
+ for (const item of items) {
30
+ const install = item[2];
31
+ install(require);
32
+ }
33
+ return items.length;
34
+ };
35
+ const window = { "@flow-web/desktop:stable": chunks };
36
+ const execute = (0, node_vm_1.runInNewContext)(`(${cdp_client_1.doubaoHistoryPageScript})`, { window });
37
+ const page = await execute("cursor-1", 20);
38
+ strict_1.default.equal(page.complete, true);
39
+ strict_1.default.equal(JSON.stringify(page.conversations[0].messages.map(({ id, role, text }) => ({ id, role, text }))), JSON.stringify([
40
+ { id: "user-1", role: "user", text: "修复图片回传" },
41
+ { id: "assistant-1", role: "assistant", text: "已修复并验证" },
42
+ ]));
43
+ strict_1.default.equal(page.conversations[0].messages[0].time, new Date(1_792_555_702_000).toISOString());
44
+ strict_1.default.equal(JSON.stringify(page.missingReasons), "[]");
45
+ });
46
+ (0, node_test_1.default)("豆包历史 provider 使用原生正文游标分页并校验会话 ID", async () => {
47
+ let next;
48
+ const client = {
49
+ connected: () => true,
50
+ listHistory: async (cursor) => {
51
+ next = cursor;
52
+ return { conversations: [{ id: "conv-1", title: "会话", messages: [], truncated: true }], nextCursor: "next", complete: false, missingReasons: [] };
53
+ },
54
+ readHistory: async (id, cursor) => {
55
+ strict_1.default.equal(id, "conv-1");
56
+ strict_1.default.equal(cursor, undefined);
57
+ return { conversationId: id, messages: [{ id: "m-1", role: "user", text: "正文", time: new Date().toISOString() }], nextCursor: "42", hasMore: true, complete: true, missingReasons: [] };
58
+ },
59
+ };
60
+ const provider = new session_history_1.DoubaoSessionHistoryProvider({ scope: "doubao:9227", client: () => client });
61
+ strict_1.default.equal(provider.mode, "all");
62
+ const listed = await provider.listSessions({ limit: 20, cursor: "before" });
63
+ strict_1.default.equal(next, "before");
64
+ strict_1.default.equal(listed.supported && listed.nextCursor, "next");
65
+ strict_1.default.equal(listed.supported && listed.complete, false);
66
+ const result = await provider.readSession({ sessionId: "conv-1", limit: 100 });
67
+ strict_1.default.equal(result.supported && result.complete, true);
68
+ strict_1.default.equal(result.supported && result.nextCursor, "42");
69
+ strict_1.default.equal(result.supported && result.turns.length, 1);
70
+ });
71
+ (0, node_test_1.default)("豆包正文分页脚本使用原生单会话链游标并排除思考字段", async () => {
72
+ let nativeQuery;
73
+ const require = (id) => {
74
+ strict_1.default.equal(id, 77049);
75
+ return { J: async (query) => {
76
+ nativeQuery = query;
77
+ return { code: 0, data: { messages: [
78
+ { conversation_id: "conv-1", message_id: "m-1", user_type: 1, create_time: "1792555770", thinking_content: "不可见思考", content_block: [{ thinking_content: { text_block: { text: "不可见嵌套思考" } }, reasoning_block: { text_block: { text: "不可见推理" } }, text_block: { text: "可见正文" } }] },
79
+ ], has_more: true, next_index: "41" } };
80
+ } };
81
+ };
82
+ const chunks = [];
83
+ chunks.push = (...items) => { for (const item of items)
84
+ item[2](require); return items.length; };
85
+ const execute = (0, node_vm_1.runInNewContext)(`(${cdp_client_1.doubaoHistoryReadPageScript})`, { window: { "@flow-web/desktop:stable": chunks } });
86
+ const page = await execute("conv-1", "50", 50);
87
+ strict_1.default.equal(page.error, undefined);
88
+ strict_1.default.equal(nativeQuery?.conversation_id, "conv-1");
89
+ strict_1.default.equal(nativeQuery?.anchor_index, 50);
90
+ strict_1.default.equal(nativeQuery?.direction, 1);
91
+ strict_1.default.equal(JSON.stringify(page.messages.map(({ id, role, text }) => ({ id, role, text }))), JSON.stringify([{ id: "m-1", role: "assistant", text: "可见正文" }]));
92
+ strict_1.default.equal(page.nextCursor, "41");
93
+ strict_1.default.equal(page.complete, true);
94
+ });
95
+ (0, node_test_1.default)("豆包今日正文先按时间排除旧附件消息,未知时间仍报告不完整", async () => {
96
+ const messages = [
97
+ { conversation_id: "conv-1", message_id: "m-today", user_type: 1, create_time: "1792555770", tts_content: "今天的答复" },
98
+ { conversation_id: "conv-1", message_id: "m-old", user_type: 2, create_time: "1792497638", content_block: [{ content: { thinking_block: {}, creation_block: { creations: [{ image: {} }] } } }] },
99
+ ];
100
+ const require = (_id) => ({ J: async () => ({ code: 0, data: { messages, has_more: true, next_index: "41" } }) });
101
+ const chunks = [];
102
+ chunks.push = (...items) => { for (const item of items)
103
+ item[2](require); return items.length; };
104
+ const execute = (0, node_vm_1.runInNewContext)(`(${cdp_client_1.doubaoHistoryReadPageScript})`, { window: { "@flow-web/desktop:stable": chunks } });
105
+ const page = await execute("conv-1", "", 100, "2026-10-21T00:00:00+08:00");
106
+ strict_1.default.equal(page.complete, true);
107
+ strict_1.default.equal(page.reachedSince, true);
108
+ strict_1.default.equal(page.messages.length, 1);
109
+ strict_1.default.equal(page.messages[0].text, "今天的答复");
110
+ messages.push({ conversation_id: "conv-1", message_id: "m-unknown", user_type: 1, thinking_content: "仅思考" });
111
+ const incomplete = await execute("conv-1", "", 100, "2026-10-21T00:00:00+08:00");
112
+ strict_1.default.equal(incomplete.complete, false);
113
+ strict_1.default.ok(incomplete.missingReasons.some(reason => reason.includes("时间")));
114
+ });
115
+ (0, node_test_1.default)("豆包今日 provider 转交起点并在原生已越过起点时结束正文分页", async () => {
116
+ const since = "2026-10-21T00:00:00+08:00";
117
+ const client = {
118
+ connected: () => true,
119
+ listHistory: async () => ({ conversations: [{ id: "conv-1", title: "会话", messages: [], truncated: true }], complete: true, missingReasons: [] }),
120
+ readHistory: async (id, _cursor, _limit, nativeSince) => {
121
+ strict_1.default.equal(nativeSince, since);
122
+ return { conversationId: id, messages: [{ id: "m-1", role: "assistant", text: "今日答复", time: "2026-10-21T01:00:00Z" }], hasMore: true, nextCursor: "42", reachedSince: true, complete: true, missingReasons: [] };
123
+ },
124
+ };
125
+ const provider = new session_history_1.DoubaoSessionHistoryProvider({ scope: "doubao:9227", client: () => client });
126
+ await provider.listSessions({ limit: 20 });
127
+ const result = await provider.readSession({ sessionId: "conv-1", limit: 100, since });
128
+ strict_1.default.equal(result.supported && result.complete, true);
129
+ strict_1.default.equal(result.supported && result.nextCursor, undefined);
130
+ strict_1.default.equal(result.supported && result.turns.length, 1);
131
+ });
132
+ (0, node_test_1.default)("豆包历史脚本重复调用兼容 webpack 已安装 chunk 缓存", async () => {
133
+ const installed = new Set();
134
+ let calls = 0;
135
+ const require = (_id) => ({ V: async () => {
136
+ calls++;
137
+ return { code: 0, data: { cells: [{ conversation: rawConversation }], has_more: false } };
138
+ } });
139
+ const chunks = [];
140
+ chunks.push = (...items) => {
141
+ for (const item of items) {
142
+ const [ids, , install] = item;
143
+ // webpack 仅在存在未安装的 chunk 时执行 runtime callback。
144
+ if (ids.some(id => !installed.has(id)))
145
+ install(require);
146
+ for (const id of ids)
147
+ installed.add(id);
148
+ }
149
+ return items.length;
150
+ };
151
+ const execute = (0, node_vm_1.runInNewContext)(`(${cdp_client_1.doubaoHistoryPageScript})`, { window: { "@flow-web/desktop:stable": chunks } });
152
+ const first = await execute("", 20);
153
+ const second = await execute("", 20);
154
+ strict_1.default.equal(first.error, undefined);
155
+ strict_1.default.equal(second.error, undefined);
156
+ strict_1.default.equal(second.conversations?.length, 1);
157
+ strict_1.default.equal(calls, 2);
158
+ });
159
+ (0, node_test_1.default)("豆包历史目录 has_more 但游标缺失时报告不完整", async () => {
160
+ const require = (_id) => ({ V: async () => ({ code: 0, data: { cells: [], has_more: true, next_cursor: "" } }) });
161
+ const chunks = [];
162
+ chunks.push = (...items) => {
163
+ for (const item of items)
164
+ item[2](require);
165
+ return items.length;
166
+ };
167
+ const execute = (0, node_vm_1.runInNewContext)(`(${cdp_client_1.doubaoHistoryPageScript})`, { window: { "@flow-web/desktop:stable": chunks } });
168
+ const page = await execute("", 20);
169
+ strict_1.default.equal(page.complete, false);
170
+ strict_1.default.equal(page.nextCursor, undefined);
171
+ strict_1.default.ok(page.missingReasons.some(reason => reason.includes("游标")));
172
+ });
@@ -39,6 +39,9 @@ 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");
@@ -276,7 +279,7 @@ function deferred() {
276
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`);
277
280
  },
278
281
  };
279
- const client = new cdp_client_1.DoubaoCdpClient({ timeoutMs: 5000, fetchImpl: async () => {
282
+ const client = new cdp_client_1.DoubaoCdpClient({ conversationId: "test", timeoutMs: 5000, fetchImpl: async () => {
280
283
  downloadStarted.resolve();
281
284
  await downloadGate.promise;
282
285
  return new Response(png, { headers: { "content-type": "image/png" } });
@@ -320,7 +323,7 @@ for (const late of ["成功", "失败"]) {
320
323
  fetch: () => { if (++requests === 1)
321
324
  return firstResponse.promise; nextStarted.resolve(); return nextResponse.promise; },
322
325
  };
323
- const client = new cdp_client_1.DoubaoCdpClient({ timeoutMs: 20 });
326
+ const client = new cdp_client_1.DoubaoCdpClient({ conversationId: "test", timeoutMs: 20 });
324
327
  client.connect = async () => { };
325
328
  client.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, page);
326
329
  await strict_1.default.rejects(client.complete("请求甲"), /等待豆包回复超时/);
@@ -350,7 +353,16 @@ for (const late of ["成功", "失败"]) {
350
353
  const block = { block_type: 2074, is_finish: true, content: { creation_block: { creations: [{ image: { status: 2, image_ori: { url: imageUrl } } }] } } };
351
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`;
352
355
  const requests = [];
356
+ const bodies = [];
357
+ let draft = "", messages = 2;
353
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
+ },
354
366
  window: {}, navigator: { userAgent: "Chrome/147.0.0.0 SamanthaDoubao/2.32.6" },
355
367
  performance: { getEntriesByType: () => [
356
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" },
@@ -358,7 +370,7 @@ for (const late of ["成功", "失败"]) {
358
370
  { name: "https://evil.example/chat/completion?device_id=untrusted" },
359
371
  ] },
360
372
  URL, AbortController, TextDecoder,
361
- fetch: async (address) => { requests.push(new URL(address)); return new Response(stream); },
373
+ fetch: async (address, options) => { requests.push(new URL(address)); bodies.push(JSON.parse(options.body)); return new Response(stream); },
362
374
  };
363
375
  class TestWebSocket {
364
376
  readyState = 1;
@@ -412,6 +424,24 @@ for (const late of ["成功", "失败"]) {
412
424
  strict_1.default.equal(requests.at(-1)?.searchParams.get("pc_version"), "2.33.0");
413
425
  strict_1.default.equal(requests.at(-1)?.searchParams.get("chromium_version"), "148.0.1.2");
414
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);
415
445
  }
416
446
  finally {
417
447
  await client.close();
@@ -483,6 +513,25 @@ for (const late of ["成功", "失败"]) {
483
513
  (0, node_test_1.default)("豆包错误兼容旧版 code 字段", () => {
484
514
  strict_1.default.equal((0, cdp_client_1.doubaoStreamErrorMessage)(JSON.stringify({ code: "E_LIMIT" })), "豆包平台返回流式错误(错误码 E_LIMIT)");
485
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
+ });
486
535
  (0, node_test_1.default)("停止豆包客户端时向页面发送请求取消指令", async () => {
487
536
  const original = globalThis.WebSocket;
488
537
  const messages = [];
@@ -633,3 +682,75 @@ for (const late of ["成功", "失败"]) {
633
682
  await plugin.deactivate();
634
683
  }
635
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
+ });
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const strict_1 = __importDefault(require("node:assert/strict"));
7
+ const promises_1 = require("node:fs/promises");
8
+ const node_os_1 = require("node:os");
9
+ const node_path_1 = require("node:path");
10
+ const node_zlib_1 = require("node:zlib");
11
+ const node_test_1 = __importDefault(require("node:test"));
12
+ const session_history_1 = require("../src-ts/plugins/dsh/session-history");
13
+ async function fixture() {
14
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "dsh-history-"));
15
+ const provider = new session_history_1.DshSessionHistoryProvider({ sessionsRoot: (0, node_path_1.join)(root, "sessions") });
16
+ async function session(id, events, options = {}) {
17
+ const directory = (0, node_path_1.join)(root, "sessions", "--project--", `session-${id}`);
18
+ await (0, promises_1.mkdir)(directory, { recursive: true });
19
+ const header = { type: "session", version: 4, id: `session-${id}`, createdAt: 1792512000000, cwd: "C:\\project", isSeeded: false, delegationDepth: 0 };
20
+ const content = [header, ...events].map(value => JSON.stringify(value)).join("\n") + "\n";
21
+ // DSH 的持久化器把头部和每次追加事件分别压成独立帧,不能只解首帧。
22
+ const bytes = options.compressed === false ? Buffer.from(content) : Buffer.concat(content.trimEnd().split("\n").map(line => (0, node_zlib_1.zstdCompressSync)(Buffer.from(`${line}\n`))));
23
+ await (0, promises_1.writeFile)((0, node_path_1.join)(directory, `session.v${options.generation ?? "4"}.jsonl${options.compressed === false ? "" : ".zstd"}`), bytes);
24
+ }
25
+ return { root, provider, session, close: () => (0, promises_1.rm)(root, { recursive: true, force: true }) };
26
+ }
27
+ const event = (seq, type, data, time = "2026-10-21T02:00:00.000Z") => ({ seq, type, time: Date.parse(time), data });
28
+ (0, node_test_1.default)("DSH 原生日志枚举全部持久会话并读取用户/助手正文与事件时间", async () => {
29
+ const f = await fixture();
30
+ try {
31
+ await f.session("one", [
32
+ event(0, "user/message", { content: [{ type: "text", text: "今天的问题" }] }),
33
+ event(1, "assistant/message", { message: { content: [{ type: "reasoning", text: "思考不回传" }, { type: "text", text: "已经解决" }] } }, "2026-10-21T02:01:00.000Z"),
34
+ ]);
35
+ await f.session("two", [event(0, "user/message", { content: [{ type: "text", text: "另一会话" }] })], { compressed: false });
36
+ const list = await f.provider.listSessions({ limit: 1 });
37
+ strict_1.default.equal(list.supported, true);
38
+ if (!list.supported)
39
+ return;
40
+ strict_1.default.equal(f.provider.mode, "all");
41
+ strict_1.default.equal(list.complete, true);
42
+ strict_1.default.ok(list.nextCursor);
43
+ const page = await f.provider.listSessions({ limit: 10, cursor: list.nextCursor });
44
+ strict_1.default.equal(page.supported && page.sessions.length, 1);
45
+ const body = await f.provider.readSession({ sessionId: "session-one", limit: 10 });
46
+ strict_1.default.equal(body.supported, true);
47
+ if (!body.supported)
48
+ return;
49
+ strict_1.default.deepEqual(body.turns.map(turn => [turn.role, turn.text, turn.time]), [
50
+ ["user", "今天的问题", "2026-10-21T02:00:00.000Z"],
51
+ ["assistant", "已经解决", "2026-10-21T02:01:00.000Z"],
52
+ ]);
53
+ strict_1.default.equal(body.complete, true);
54
+ }
55
+ finally {
56
+ await f.close();
57
+ }
58
+ });
59
+ (0, node_test_1.default)("DSH 遇到未知事件、未知代际或越界游标时显式报告不完整/拒绝", async () => {
60
+ const f = await fixture();
61
+ try {
62
+ await f.session("known", [event(0, "user/message", { content: [{ type: "text", text: "保留正文" }] })]);
63
+ await f.session("future", [event(0, "user/message", { content: [{ type: "text", text: "不读取" }] })], { generation: "5" });
64
+ const list = await f.provider.listSessions({ limit: 10 });
65
+ strict_1.default.equal(list.supported && list.complete, false);
66
+ strict_1.default.equal(list.supported && list.missingReasons.some(reason => /代际/.test(reason)), true);
67
+ const body = await f.provider.readSession({ sessionId: "session-known", limit: 10 });
68
+ strict_1.default.equal(body.supported && body.complete, true);
69
+ await strict_1.default.rejects(f.provider.listSessions({ limit: 1, cursor: "fake" }), /游标/);
70
+ }
71
+ finally {
72
+ await f.close();
73
+ }
74
+ });
75
+ (0, node_test_1.default)("DSH 拒绝损坏/超大/角色不明的日志,不静默声称完整", async () => {
76
+ const f = await fixture();
77
+ try {
78
+ await f.session("bad", [event(1, "user/message", { content: [{ type: "text", text: "错序" }] })]);
79
+ const list = await f.provider.listSessions({ limit: 10 });
80
+ strict_1.default.equal(list.supported && list.complete, true);
81
+ const body = await f.provider.readSession({ sessionId: "session-bad", limit: 10 });
82
+ strict_1.default.equal(body.supported, true);
83
+ if (body.supported) {
84
+ strict_1.default.equal(body.complete, false);
85
+ strict_1.default.equal(body.turns.length, 0);
86
+ }
87
+ }
88
+ finally {
89
+ await f.close();
90
+ }
91
+ });
92
+ (0, node_test_1.default)("DSH 分页游标绑定日志快照,目录中的符号链接不会被读取", async () => {
93
+ const f = await fixture();
94
+ const outside = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "dsh-history-outside-"));
95
+ try {
96
+ await f.session("one", [event(0, "user/message", { content: [{ type: "text", text: "第一条" }] }), event(1, "assistant/message", { message: { role: "assistant", content: [{ type: "text", text: "第二条" }] } })]);
97
+ const first = await f.provider.readSession({ sessionId: "session-one", limit: 1 });
98
+ strict_1.default.ok(first.supported && first.nextCursor);
99
+ if (!first.supported)
100
+ return;
101
+ await f.session("one", [event(0, "user/message", { content: [{ type: "text", text: "内容变化" }] })]);
102
+ await strict_1.default.rejects(f.provider.readSession({ sessionId: "session-one", limit: 1, cursor: first.nextCursor }), /游标/);
103
+ await (0, promises_1.symlink)(outside, (0, node_path_1.join)(f.root, "sessions", "外部目录"), process.platform === "win32" ? "junction" : "dir");
104
+ const listed = await f.provider.listSessions({ limit: 100 });
105
+ strict_1.default.equal(listed.supported && listed.complete, false);
106
+ strict_1.default.ok(listed.supported && listed.missingReasons.some(reason => /符号链接/.test(reason)));
107
+ strict_1.default.equal((await f.provider.readSession({ sessionId: "../../外部目录", limit: 10 })).supported, false);
108
+ }
109
+ finally {
110
+ await f.close();
111
+ await (0, promises_1.rm)(outside, { recursive: true, force: true });
112
+ }
113
+ });
114
+ (0, node_test_1.default)("DSH 多帧解压拒绝截断帧和非 zstd 尾部,避免只读首帧后声称完整", () => {
115
+ const first = (0, node_zlib_1.zstdCompressSync)(Buffer.from("第一帧\n")), second = (0, node_zlib_1.zstdCompressSync)(Buffer.from("第二帧\n"));
116
+ strict_1.default.equal((0, session_history_1.decodeDshZstdFrames)(Buffer.concat([first, second])).toString(), "第一帧\n第二帧\n");
117
+ strict_1.default.throws(() => (0, session_history_1.decodeDshZstdFrames)(Buffer.concat([first, second.subarray(0, -1)])), /截断/);
118
+ strict_1.default.throws(() => (0, session_history_1.decodeDshZstdFrames)(Buffer.concat([first, Buffer.from("invalid")])), /魔数/);
119
+ });
120
+ (0, node_test_1.default)("DSH since 在完整解析后筛选正文,且游标绑定时间范围和读取类型", async () => {
121
+ const f = await fixture();
122
+ try {
123
+ await f.session("since", [
124
+ event(0, "user/message", { content: [{ type: "text", text: "较早消息" }] }, "2026-10-20T23:59:59.000Z"),
125
+ event(1, "assistant/message", { message: { content: [{ type: "text", text: "今天回复" }] } }, "2026-10-21T00:00:00.000Z"),
126
+ event(2, "user/message", { content: [{ type: "text", text: "今天问题" }] }, "2026-10-21T01:00:00.000Z"),
127
+ ]);
128
+ const page = await f.provider.readSession({ sessionId: "session-since", limit: 1, since: "2026-10-21T00:00:00+00:00" });
129
+ strict_1.default.equal(page.supported, true);
130
+ if (!page.supported)
131
+ return;
132
+ strict_1.default.deepEqual(page.turns.map(turn => turn.text), ["今天回复"]);
133
+ strict_1.default.ok(page.nextCursor);
134
+ await strict_1.default.rejects(f.provider.readSession({ sessionId: "session-since", limit: 1, since: "2026-10-21T00:00:01Z", cursor: page.nextCursor }), /游标/);
135
+ await strict_1.default.rejects(f.provider.listSessions({ limit: 1, cursor: page.nextCursor }), /游标/);
136
+ const all = await f.provider.readSession({ sessionId: "session-since", limit: 10 });
137
+ strict_1.default.equal(all.supported && all.turns.length, 3);
138
+ const invalid = await f.provider.readSession({ sessionId: "session-since", limit: 10, since: "今天" });
139
+ strict_1.default.equal(invalid.supported, false);
140
+ if (!invalid.supported)
141
+ strict_1.default.match(invalid.reason, /ISO/);
142
+ }
143
+ finally {
144
+ await f.close();
145
+ }
146
+ });
147
+ (0, node_test_1.default)("DSH since 过滤旧消息后不足一页的末页不生成尾游标", async () => {
148
+ const f = await fixture();
149
+ try {
150
+ await f.session("since-last", [
151
+ event(0, "user/message", { content: [{ type: "text", text: "昨天的问题" }] }, "2026-10-20T23:59:59.000Z"),
152
+ event(1, "user/message", { content: [{ type: "text", text: "今天的问题" }] }, "2026-10-21T00:00:00.000Z"),
153
+ ]);
154
+ const page = await f.provider.readSession({ sessionId: "session-since-last", limit: 10, since: "2026-10-21T00:00:00Z" });
155
+ strict_1.default.equal(page.supported, true);
156
+ if (!page.supported)
157
+ return;
158
+ strict_1.default.deepEqual(page.turns.map(turn => turn.text), ["今天的问题"]);
159
+ strict_1.default.equal(page.nextCursor, undefined);
160
+ strict_1.default.equal(page.complete, true);
161
+ }
162
+ finally {
163
+ await f.close();
164
+ }
165
+ });