mu-agent-bridge 1.0.35 → 1.0.40

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 (60) hide show
  1. package/README.md +2 -0
  2. package/dist/src-ts/channel-sessions.js +90 -24
  3. package/dist/src-ts/cli.js +1 -1
  4. package/dist/src-ts/control-server.js +48 -7
  5. package/dist/src-ts/plugin-manager.js +47 -0
  6. package/dist/src-ts/plugins/chatgpt/cdp-client.js +72 -0
  7. package/dist/src-ts/plugins/chatgpt/index.js +122 -4
  8. package/dist/src-ts/plugins/doubao/cdp-client.js +243 -18
  9. package/dist/src-ts/plugins/doubao/index.js +81 -5
  10. package/dist/src-ts/plugins/doubao/session-history.js +123 -0
  11. package/dist/src-ts/plugins/dsh/index.js +16 -2
  12. package/dist/src-ts/plugins/dsh/session-history.js +371 -0
  13. package/dist/src-ts/plugins/qwenwork/cdp-client.js +8 -0
  14. package/dist/src-ts/plugins/qwenwork/index.js +12 -1
  15. package/dist/src-ts/plugins/qwenwork/session-history.js +245 -0
  16. package/dist/src-ts/plugins/workbuddy/cdp-client.js +33 -0
  17. package/dist/src-ts/plugins/workbuddy/index.js +21 -1
  18. package/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
  19. package/dist/src-ts/session-archive.js +509 -0
  20. package/dist/src-ts/session-history.js +2 -0
  21. package/dist/src-ts/session-management.js +665 -0
  22. package/dist/src-ts/session-summary.js +171 -0
  23. package/dist/tests-ts/channel-sessions.test.js +233 -2
  24. package/dist/tests-ts/chatgpt-history.test.js +197 -0
  25. package/dist/tests-ts/control-server.test.js +39 -0
  26. package/dist/tests-ts/doubao-history.test.js +172 -0
  27. package/dist/tests-ts/doubao-plugin.test.js +124 -3
  28. package/dist/tests-ts/dsh-history.test.js +165 -0
  29. package/dist/tests-ts/native-session-management.test.js +195 -0
  30. package/dist/tests-ts/plugin-manager.test.js +254 -3
  31. package/dist/tests-ts/qwenwork-history.test.js +339 -0
  32. package/dist/tests-ts/session-archive.test.js +422 -0
  33. package/dist/tests-ts/session-management.test.js +323 -0
  34. package/dist/tests-ts/session-summary.test.js +199 -0
  35. package/dist/tests-ts/workbuddy-history.test.js +256 -0
  36. package/package.json +1 -1
  37. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +72 -0
  38. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +122 -4
  39. package/plugins/chatgpt/package.json +1 -1
  40. package/plugins/chatgpt/plugin.json +14 -1
  41. package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +243 -18
  42. package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +81 -5
  43. package/plugins/doubao/dist/src-ts/plugins/doubao/session-history.js +123 -0
  44. package/plugins/doubao/package.json +1 -1
  45. package/plugins/doubao/plugin.json +1 -1
  46. package/plugins/dsh/config.schema.json +2 -1
  47. package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +16 -2
  48. package/plugins/dsh/dist/src-ts/plugins/dsh/session-history.js +371 -0
  49. package/plugins/dsh/package.json +1 -1
  50. package/plugins/dsh/plugin.json +1 -1
  51. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +8 -0
  52. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +12 -1
  53. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/session-history.js +245 -0
  54. package/plugins/qwenwork/package.json +1 -1
  55. package/plugins/qwenwork/plugin.json +1 -1
  56. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +33 -0
  57. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +21 -1
  58. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
  59. package/plugins/workbuddy/package.json +1 -1
  60. package/plugins/workbuddy/plugin.json +1 -1
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DoubaoCdpClient = exports.currentResponseArtifactsScript = exports.responseArtifactsScript = exports.responseImagesScript = exports.responseTextScript = void 0;
3
+ exports.doubaoHistoryReadPageScript = exports.doubaoHistoryPageScript = exports.DoubaoCdpClient = exports.currentResponseArtifactsScript = exports.responseArtifactsScript = exports.responseImagesScript = exports.responseTextScript = void 0;
4
4
  exports.doubaoStreamErrorMessage = doubaoStreamErrorMessage;
5
5
  exports.requestBody = requestBody;
6
6
  exports.mergeDoubaoReply = mergeDoubaoReply;
@@ -17,6 +17,17 @@ function doubaoStreamErrorMessage(data) {
17
17
  const value = JSON.parse(data);
18
18
  code = String(value.error_code ?? value.code ?? value.error?.error_code ?? value.error?.code ?? "");
19
19
  message = String(value.error_msg ?? value.message ?? value.error?.message ?? "").trim();
20
+ let decision = value.extra?.decision;
21
+ if (typeof decision === "string") {
22
+ try {
23
+ decision = JSON.parse(decision);
24
+ }
25
+ catch { /* 无法解析时保留平台原文 */ }
26
+ }
27
+ if (decision && typeof decision === "object" && decision.type === "verify") {
28
+ const verification = decision.subtype === "slide" ? "滑块验证" : "安全验证";
29
+ return `豆包触发风控${code ? `(错误码 ${code})` : ""}:需要${verification},请在豆包桌面端完成验证后重新发送;请求未自动重试`;
30
+ }
20
31
  }
21
32
  catch { /* 平台可能返回非 JSON 错误 */ }
22
33
  if (message.length > 300)
@@ -26,14 +37,15 @@ function doubaoStreamErrorMessage(data) {
26
37
  return `豆包平台返回错误${code ? `(错误码 ${code})` : ""}:${message}`;
27
38
  return `豆包平台返回流式错误${code ? `(错误码 ${code})` : ""}`;
28
39
  }
29
- const defaults = { port: Number(process.env.DOUBAO_CDP_PORT ?? "9227"), conversationId: process.env.DOUBAO_CONVERSATION_ID ?? "759450172544514", botId: process.env.DOUBAO_BOT_ID ?? "7338286299411103781", timeoutMs: Number(process.env.DOUBAO_TIMEOUT_MS ?? "180000") };
40
+ const defaults = { port: Number(process.env.DOUBAO_CDP_PORT ?? "9227"), conversationId: process.env.DOUBAO_CONVERSATION_ID ?? "", botId: process.env.DOUBAO_BOT_ID ?? "7338286299411103781", timeoutMs: Number(process.env.DOUBAO_TIMEOUT_MS ?? "180000") };
30
41
  function sleep(ms, signal) {
31
42
  if (signal?.aborted)
32
43
  return Promise.reject(new Error("请求已取消"));
33
44
  return new Promise((resolve, reject) => { const timer = setTimeout(resolve, ms); signal?.addEventListener("abort", () => { clearTimeout(timer); reject(new Error("请求已取消")); }, { once: true }); });
34
45
  }
35
46
  function requestBody(text, conversationId, botId, pluginList = [], attachments = []) {
36
- const general = { runtime_type: 0, agent_task_param: {}, agent_task_param_change: { runtime_changed: false, device_changed: false, sandbox_auth_type_changed: false }, project_id: "", need_modify_conversation: false };
47
+ const fresh = !conversationId;
48
+ const general = fresh ? { server_do_compare: true } : { runtime_type: 0, agent_task_param: {}, agent_task_param_change: { runtime_changed: false, device_changed: false, sandbox_auth_type_changed: false }, project_id: "", need_modify_conversation: false };
37
49
  const block = { block_type: 10000, content: { text_block: { text, icon_url: "", icon_url_dark: "", summary: "" }, pc_event_block: "" }, block_id: (0, node_crypto_1.randomUUID)(), parent_id: "", meta_info: [], append_fields: [] };
38
50
  const messages = [{ local_message_id: (0, node_crypto_1.randomUUID)(), content_block: [block], message_status: 0 }];
39
51
  if (attachments.length)
@@ -43,7 +55,8 @@ function requestBody(text, conversationId, botId, pluginList = [], attachments =
43
55
  : { file: { uri: item.fileKey, url: "", file_type: 0, name: item.fileName, size: item.size } }),
44
56
  parse_state: item.type === "image" ? 0 : 1, review_state: 1, upload_status: 1, progress: 100, src: "",
45
57
  })) }, pc_event_block: "" } }], message_status: 0 });
46
- return JSON.stringify({ client_meta: { conversation_id: conversationId, bot_id: botId, last_section_id: "", last_message_index: null, local_permissions: [{ permission_name: "ACCESS_COARSE_LOCATION", status: 1 }, { permission_name: "ACCESS_FINE_LOCATION", status: 1 }, { permission_name: "ACCESS_BACKGROUND_LOCATION", status: 1 }] }, messages, option: { send_message_scene: "", create_time_ms: Date.now(), collect_id: "", is_audio: false, answer_with_suggest: false, agent_mode: 2, tts_switch: false, need_deep_think: 0, click_clear_context: false, is_regen: false, is_replace: false, is_from_click_option: false, is_from_click_softlink: false, disable_sse_cache: false, scene_type: 0, unique_key: (0, node_crypto_1.randomUUID)(), start_seq: 0, need_create_conversation: false, regen_query_id: [], edit_query_id: [], message_from: 0, sse_recv_event_options: { support_chunk_delta: true }, support_lazy_fetch_stream: true, is_ai_playground: false, is_old_user: true, general_task_param: general, recovery_option: { is_recovery: false, req_create_time_sec: Math.floor(Date.now() / 1000), append_sse_event_scene: 0 }, message_storage_type: 0, related_deleted_message_ids: {}, connector_info_list: [], model_config: { model_item_key: "0", model_extra_params: {}, reasoning_effort: 5 }, aggregate_params: { mention_skill_list: "[]", mention_plugin_list: JSON.stringify(pluginList), mention_ext: "[{}]", conversation_mode: "", mode_id: "1", model_item_key: "0", agent_mode: "2", reasoning_effort: "5", provider_id: "" } }, user_context: [], ext: { agent_mode: "2", use_deep_think: "0", general_task_param: JSON.stringify(general), collection_id: "", is_finish: "1", commerce_credit_config_enable: "0" } });
58
+ const localConversationId = `local_${Date.now()}${Math.floor(Math.random() * 1000).toString().padStart(3, "0")}`;
59
+ return JSON.stringify({ client_meta: { ...(fresh ? { local_conversation_id: localConversationId } : {}), conversation_id: conversationId, bot_id: botId, last_section_id: "", last_message_index: null, local_permissions: [{ permission_name: "ACCESS_COARSE_LOCATION", status: 1 }, { permission_name: "ACCESS_FINE_LOCATION", status: 1 }, { permission_name: "ACCESS_BACKGROUND_LOCATION", status: 1 }] }, messages, option: { send_message_scene: "", create_time_ms: Date.now(), collect_id: "", is_audio: false, answer_with_suggest: false, agent_mode: 2, tts_switch: false, need_deep_think: 0, click_clear_context: false, from_suggest: false, is_regen: false, is_replace: false, is_from_click_option: false, is_from_click_softlink: false, disable_sse_cache: false, select_text_action: "", is_select_text: false, resend_for_regen: false, scene_type: 0, unique_key: (0, node_crypto_1.randomUUID)(), start_seq: 0, need_create_conversation: fresh, ...(fresh ? { conversation_init_option: { need_ack_conversation: true }, conversation_init_ext: { model_item_key: "0", reasoning_effort: "3", mode_id: "1" } } : {}), regen_query_id: [], edit_query_id: [], regen_instruction: "", no_replace_for_regen: false, message_from: 0, shared_app_name: "", shared_app_id: "", sse_recv_event_options: { support_chunk_delta: true }, support_lazy_fetch_stream: true, is_ai_playground: false, is_old_user: true, general_task_param: general, recovery_option: { is_recovery: false, req_create_time_sec: Math.floor(Date.now() / 1000), append_sse_event_scene: 0 }, message_storage_type: 0, related_deleted_message_ids: {}, connector_info_list: [], model_config: { model_item_key: "0", model_extra_params: {}, reasoning_effort: fresh ? 3 : 5 }, aggregate_params: { mention_skill_list: "[]", mention_plugin_list: JSON.stringify(pluginList), mention_ext: "[{}]", conversation_mode: fresh ? "1" : "", mode_id: "1", model_item_key: "0", agent_mode: "2", reasoning_effort: fresh ? "3" : "5", provider_id: "" }, ...(fresh ? { conversation_mode: 1 } : {}) }, user_context: [], ext: { ...(fresh ? { sub_conv_firstmet_type: "1", conversation_init_option: JSON.stringify({ need_ack_conversation: true }) } : {}), agent_mode: "2", use_deep_think: "0", general_task_param: JSON.stringify(general), collection_id: "", is_finish: "1", commerce_credit_config_enable: "0" } });
47
60
  }
48
61
  exports.responseTextScript = `(value) => { const texts = []; const visit = (item) => { if (Array.isArray(item)) { for (const child of item) visit(child); return; } if (!item || typeof item !== 'object') return; if (item.text_block && typeof item.text_block.text === 'string') texts.push(item.text_block.text); for (const child of Object.values(item)) visit(child); }; visit(value); return texts; }`;
49
62
  function mergeDoubaoReply(current, next) {
@@ -203,10 +216,11 @@ class DoubaoCdpClient {
203
216
  connecting;
204
217
  queue = Promise.resolve();
205
218
  sequence = 0;
219
+ conversationId = "";
206
220
  closeController = new AbortController();
207
221
  pending = new Map();
208
222
  options;
209
- constructor(options = {}) { this.options = { port: options.port ?? defaults.port, conversationId: options.conversationId ?? defaults.conversationId, botId: options.botId ?? defaults.botId, pluginList: options.pluginList ?? [], timeoutMs: options.timeoutMs ?? defaults.timeoutMs, fetchImpl: options.fetchImpl }; }
223
+ constructor(options = {}) { this.options = { port: options.port ?? defaults.port, conversationId: options.conversationId ?? defaults.conversationId, botId: options.botId ?? defaults.botId, pluginList: options.pluginList ?? [], timeoutMs: options.timeoutMs ?? defaults.timeoutMs, fetchImpl: options.fetchImpl }; this.conversationId = this.options.conversationId; }
210
224
  connected() { return this.socket?.readyState === 1; }
211
225
  async connect() {
212
226
  if (this.closeController.signal.aborted)
@@ -235,6 +249,7 @@ class DoubaoCdpClient {
235
249
  throw new Error(`未找到豆包 chat 页面(CDP ${this.options.port})`);
236
250
  if (this.closeController.signal.aborted)
237
251
  throw new Error("豆包 CDP 已关闭");
252
+ this.conversationId = this.options.conversationId || (page.url ?? "").match(/\/chat\/([^/?#]+)/)?.[1] || "";
238
253
  const socket = new WebSocket(page.webSocketDebuggerUrl);
239
254
  this.socket = socket;
240
255
  socket.onmessage = event => this.onMessage(String(event.data));
@@ -273,12 +288,56 @@ class DoubaoCdpClient {
273
288
  async direct(text, signal) {
274
289
  return (await this.complete(text, signal)).text;
275
290
  }
276
- async complete(text, signal, attachments) {
277
- const run = this.queue.then(() => this.runComplete(text, signal, attachments));
291
+ async listHistory(cursor, limit) {
292
+ await this.connect();
293
+ const expression = `(${exports.doubaoHistoryPageScript})(${JSON.stringify(cursor ?? "")}, ${limit})`;
294
+ const raw = await this.evaluate(expression, true);
295
+ if (!raw || typeof raw !== "object")
296
+ throw new Error("豆包历史接口返回格式无效");
297
+ const result = raw;
298
+ if (result.error)
299
+ throw new Error(result.error);
300
+ return result;
301
+ }
302
+ async readHistory(conversationId, cursor, limit, since) {
303
+ await this.connect();
304
+ const expression = `(${exports.doubaoHistoryReadPageScript})(${JSON.stringify(conversationId)}, ${JSON.stringify(cursor ?? "")}, ${limit}, ${JSON.stringify(since ?? "")})`;
305
+ const raw = await this.evaluate(expression, true);
306
+ if (!raw || typeof raw !== "object")
307
+ throw new Error("豆包会话正文接口返回格式无效");
308
+ const result = raw;
309
+ if (result.error)
310
+ throw new Error(result.error);
311
+ return result;
312
+ }
313
+ async createSession(_name) {
314
+ await this.connect();
315
+ const snapshot = `(() => ({ blank: location.pathname.replace(/\\/+$/, '') === '/chat', input: document.querySelector('[data-testid="chat_input_input"]')?.innerText ?? null, messages: document.querySelectorAll('[data-testid="send_message"],[data-testid="receive_message"]').length }))()`;
316
+ const before = await this.evaluate(snapshot);
317
+ if (before.input?.trim())
318
+ throw new Error("豆包输入框含有未发送内容,不能新建会话");
319
+ if (!before.blank || before.messages) {
320
+ const clicked = await this.evaluate(`(() => { const button = document.querySelector('[data-testid="create_conversation_button"] [role="button"]'); if (!button || button.getAttribute('aria-disabled') === 'true') return false; button.click(); return true; })()`);
321
+ if (clicked !== true)
322
+ throw new Error("豆包新对话入口未找到或不可用");
323
+ }
324
+ const deadline = Date.now() + Math.min(this.options.timeoutMs, 10_000);
325
+ while (Date.now() < deadline) {
326
+ const state = await this.evaluate(snapshot);
327
+ if (state.blank && state.input !== null && !state.input.trim() && state.messages === 0) {
328
+ this.conversationId = "";
329
+ return;
330
+ }
331
+ await sleep(100, this.closeController.signal);
332
+ }
333
+ throw new Error("豆包新会话状态校验失败");
334
+ }
335
+ async complete(text, signal, attachments, conversationId) {
336
+ const run = this.queue.then(() => this.runComplete(text, signal, attachments, conversationId));
278
337
  this.queue = run.then(() => undefined, () => undefined);
279
338
  return run;
280
339
  }
281
- async runComplete(text, signal, attachments) {
340
+ async runComplete(text, signal, attachments, conversationId) {
282
341
  (0, attachments_1.validateAttachments)(attachments);
283
342
  for (const item of attachments ?? []) {
284
343
  if (item.contentType.toLowerCase().startsWith("video/"))
@@ -292,8 +351,9 @@ class DoubaoCdpClient {
292
351
  await this.connect();
293
352
  if (requestSignal.aborted)
294
353
  throw new Error("请求已取消");
354
+ const target = conversationId ?? this.conversationId;
295
355
  const uploaded = attachments?.length ? await this.uploadAttachments(attachments, requestSignal) : [];
296
- await this.evaluate(buildInjection(requestBody(text, this.options.conversationId, this.options.botId, this.options.pluginList, uploaded)));
356
+ await this.evaluate(buildInjection(requestBody(text, target, this.options.botId, this.options.pluginList, uploaded)));
297
357
  const deadline = Date.now() + this.options.timeoutMs;
298
358
  while (Date.now() < deadline) {
299
359
  await sleep(1000, requestSignal);
@@ -307,7 +367,7 @@ class DoubaoCdpClient {
307
367
  throw new Error(`豆包请求失败: ${parsed.reason ?? "未知错误"}`);
308
368
  let artifacts = parsed.artifacts ?? [];
309
369
  if (parsed.pending || parsed.expectVideo) {
310
- const current = await this.evaluate(`(${exports.currentResponseArtifactsScript})(${JSON.stringify(this.options.conversationId)}, ${JSON.stringify(parsed.questionIds ?? [])}, ${JSON.stringify(parsed.messageIds ?? [])})`);
370
+ const current = await this.evaluate(`(${exports.currentResponseArtifactsScript})(${JSON.stringify(parsed.conversationId || target)}, ${JSON.stringify(parsed.questionIds ?? [])}, ${JSON.stringify(parsed.messageIds ?? [])})`);
311
371
  if (current.failed)
312
372
  throw new Error("豆包媒体生成失败");
313
373
  if (current.pending || !current.artifacts.length || (parsed.expectVideo && !current.artifacts.some(item => item.kind === "video")))
@@ -328,8 +388,13 @@ class DoubaoCdpClient {
328
388
  (0, attachments_1.validateAttachments)(output);
329
389
  const images = output.filter(item => item.kind === "image").map(({ kind: _kind, ...image }) => image);
330
390
  const reply = cleanDoubaoReply(parsed.reply ?? "", text);
331
- if (output.length || reply)
332
- return { text: reply || "附件已生成。", ...(output.length ? { attachments: output } : {}), ...(images.length ? { images } : {}) };
391
+ if (output.length || reply) {
392
+ const backendId = parsed.conversationId || target;
393
+ if (!backendId)
394
+ throw new Error("豆包未返回新会话标识,不能绑定会话");
395
+ this.conversationId = backendId;
396
+ return { text: reply || "附件已生成。", sessionId: backendId, ...(output.length ? { attachments: output } : {}), ...(images.length ? { images } : {}) };
397
+ }
333
398
  }
334
399
  throw new Error("等待豆包回复超时");
335
400
  }
@@ -413,6 +478,165 @@ function buildResolveArtifactsInjection(artifacts) {
413
478
  return result;
414
479
  })()`;
415
480
  }
481
+ /** 复用豆包页面已加载的只读原生请求,不打开会话或发送任务。 */
482
+ exports.doubaoHistoryPageScript = String.raw `async (cursor, limit) => {
483
+ try {
484
+ const chunks = window["@flow-web/desktop:stable"];
485
+ if (!Array.isArray(chunks)) return { error: '未找到豆包历史模块' };
486
+ // webpack 对同一 chunk 只执行一次 runtime,每次读取必须使用新的注册标识。
487
+ window.__mab_doubao_history_sequence = (Number(window.__mab_doubao_history_sequence) || 0) + 1;
488
+ let require;
489
+ const chunkId = 'mab-readonly-history-' + window.__mab_doubao_history_sequence;
490
+ chunks.push([[chunkId], {}, runtimeRequire => { require = runtimeRequire; }]);
491
+ if (typeof require !== 'function') return { error: '豆包历史模块未就绪' };
492
+ const result = await require(55807).V({
493
+ conv_version: cursor || '', limit,
494
+ message_count_per_conv: 100,
495
+ option: { not_need_message: false, need_complete_conversation: true, exclude_archive: false, only_archive: false }
496
+ });
497
+ if (!result || Number(result.code) !== 0 || !result.data || !Array.isArray(result.data.cells)) {
498
+ return { error: '豆包历史目录读取失败:' + String(result?.msg || result?.code || '返回数据无效') };
499
+ }
500
+ const textOf = message => {
501
+ if (typeof message.tts_content === 'string' && message.tts_content.trim()) return message.tts_content.trim();
502
+ if (typeof message.content === 'string' && message.content.trim()) return message.content.trim();
503
+ const texts = [];
504
+ const visit = value => {
505
+ if (Array.isArray(value)) { for (const item of value) visit(item); return; }
506
+ if (!value || typeof value !== 'object') return;
507
+ if (typeof value.text_block?.text === 'string' && value.text_block.text.trim()) texts.push(value.text_block.text.trim());
508
+ for (const [key, child] of Object.entries(value)) {
509
+ if (/think|reasoning/i.test(key)) continue;
510
+ visit(child);
511
+ }
512
+ };
513
+ visit(message.content_block);
514
+ return [...new Set(texts)].join('\n').trim();
515
+ };
516
+ const isoTime = value => {
517
+ const number = Number(value);
518
+ if (!Number.isFinite(number) || number <= 0) return undefined;
519
+ const date = new Date(number < 1e12 ? number * 1000 : number);
520
+ return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
521
+ };
522
+ const missingReasons = [];
523
+ if (result.data.has_more === true && !result.data.next_cursor) missingReasons.push('豆包历史目录还有后续会话但缺少分页游标,无法完成全部目录读取。');
524
+ const conversations = [];
525
+ for (const cell of result.data.cells) {
526
+ const conversation = cell?.conversation;
527
+ if (!conversation || typeof conversation.conversation_id !== 'string' || !conversation.conversation_id) {
528
+ missingReasons.push('豆包历史目录含无法识别的会话条目。');
529
+ continue;
530
+ }
531
+ const rawMessages = Array.isArray(conversation.messages) ? conversation.messages : [];
532
+ const maxIndex = Math.max(0, ...rawMessages.map(item => Number(item?.index_in_conv) || 0));
533
+ const latestIndex = Number(conversation.latest_index) || 0;
534
+ const truncated = rawMessages.length >= 95 || (latestIndex > 0 && maxIndex < latestIndex);
535
+ const messages = [];
536
+ let incomplete = false;
537
+ for (const item of rawMessages) {
538
+ const role = Number(item?.user_type) === 2 ? 'user' : Number(item?.user_type) === 1 ? 'assistant' : undefined;
539
+ const text = textOf(item || {});
540
+ const time = isoTime(item?.create_time || item?.update_time);
541
+ if (!role || typeof item?.message_id !== 'string' || !item.message_id || !text || !time) {
542
+ incomplete = true;
543
+ continue;
544
+ }
545
+ messages.push({ id: item.message_id, role, text, time });
546
+ }
547
+ conversations.push({
548
+ id: conversation.conversation_id,
549
+ title: typeof conversation.name === 'string' && conversation.name.trim() ? conversation.name.trim() : '豆包会话',
550
+ createdAt: isoTime(conversation.create_time),
551
+ updatedAt: isoTime(conversation.update_time),
552
+ messages,
553
+ truncated: truncated || incomplete
554
+ });
555
+ }
556
+ return {
557
+ conversations,
558
+ ...(result.data.has_more === true && result.data.next_cursor ? { nextCursor: String(result.data.next_cursor) } : {}),
559
+ complete: missingReasons.length === 0,
560
+ missingReasons: [...new Set(missingReasons)]
561
+ };
562
+ } catch (error) {
563
+ return { error: '豆包历史读取失败,请检查桌面端历史接口是否可用' };
564
+ }
565
+ }`;
566
+ /** 通过豆包原生单会话链接口只读分页,不切换当前会话。 */
567
+ exports.doubaoHistoryReadPageScript = String.raw `async (conversationId, cursor, limit, since) => {
568
+ try {
569
+ const chunks = window["@flow-web/desktop:stable"];
570
+ if (!Array.isArray(chunks)) return { error: '未找到豆包会话历史模块' };
571
+ window.__mab_doubao_history_sequence = (Number(window.__mab_doubao_history_sequence) || 0) + 1;
572
+ let require;
573
+ chunks.push([['mab-readonly-history-' + window.__mab_doubao_history_sequence], {}, runtimeRequire => { require = runtimeRequire; }]);
574
+ if (typeof require !== 'function') return { error: '豆包会话历史模块未就绪' };
575
+ const anchor = cursor ? Number(cursor) : Number.MAX_SAFE_INTEGER;
576
+ if (!Number.isSafeInteger(anchor) || anchor < 0 || !Number.isInteger(limit) || limit < 1 || limit > 100) return { error: '豆包会话历史分页参数无效' };
577
+ const sinceMs = since ? Date.parse(since) : undefined;
578
+ if (since && !Number.isFinite(sinceMs)) return { error: '豆包会话历史起始时间无效' };
579
+ const result = await require(77049).J({
580
+ conversation_id: conversationId, anchor_index: anchor, conversation_type: 3,
581
+ direction: 1, limit, ext: {}, filter: { index_list: [] },
582
+ evaluate_ab_params: '', evaluate_common_params: ''
583
+ });
584
+ const data = result?.data;
585
+ if (Number(result?.code) !== 0 || !data || !Array.isArray(data.messages)) return { error: '豆包单会话历史接口返回格式无效' };
586
+ const conversations = new Set(data.messages.map(item => item?.conversation_id).filter(Boolean));
587
+ if (conversations.size > 1 || (conversations.size === 1 && !conversations.has(conversationId))) return { error: '豆包单会话历史返回了不匹配的会话' };
588
+ const isoTime = value => {
589
+ const number = Number(value);
590
+ if (!Number.isFinite(number) || number <= 0) return undefined;
591
+ const date = new Date(number < 1e12 ? number * 1000 : number);
592
+ return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
593
+ };
594
+ const textOf = message => {
595
+ if (typeof message.tts_content === 'string' && message.tts_content.trim()) return message.tts_content.trim();
596
+ if (typeof message.content === 'string' && message.content.trim()) return message.content.trim();
597
+ const texts = [];
598
+ const visit = value => {
599
+ if (Array.isArray(value)) { for (const item of value) visit(item); return; }
600
+ if (!value || typeof value !== 'object') return;
601
+ if (typeof value.text_block?.text === 'string' && value.text_block.text.trim()) texts.push(value.text_block.text.trim());
602
+ for (const [key, child] of Object.entries(value)) {
603
+ if (/think|reasoning/i.test(key)) continue;
604
+ visit(child);
605
+ }
606
+ };
607
+ visit(message.content_block);
608
+ return [...new Set(texts)].join('\n').trim();
609
+ };
610
+ const messages = [];
611
+ const missingReasons = [];
612
+ let previousTime = Infinity, reachedSince = false;
613
+ for (const item of data.messages) {
614
+ const time = isoTime(item?.create_time || item?.update_time);
615
+ if (!time) {
616
+ missingReasons.push('豆包会话历史包含缺少有效时间的消息,无法确认其所属日期。');
617
+ continue;
618
+ }
619
+ const timeMs = Date.parse(time);
620
+ if (timeMs > previousTime) missingReasons.push('豆包会话历史消息时间未按倒序排列,无法确认日期范围完整性。');
621
+ previousTime = timeMs;
622
+ // 起点页可能含有昨天的附件或其他非文本消息;它们不属于本次日期范围。
623
+ if (sinceMs !== undefined && timeMs < sinceMs) { reachedSince = true; continue; }
624
+ const role = Number(item?.user_type) === 2 ? 'user' : Number(item?.user_type) === 1 ? 'assistant' : undefined;
625
+ const text = textOf(item || {});
626
+ if (!role || typeof item?.message_id !== 'string' || !item.message_id || !text || !time) {
627
+ missingReasons.push('豆包会话历史包含无法识别的消息结构、正文或时间。');
628
+ continue;
629
+ }
630
+ messages.push({ id: item.message_id, role, text, time });
631
+ }
632
+ const hasMore = data.has_more === true;
633
+ const nextCursor = hasMore && data.next_index !== undefined && String(data.next_index) !== String(anchor) ? String(data.next_index) : undefined;
634
+ if (hasMore && !nextCursor) missingReasons.push('豆包会话历史还有后续消息但缺少有效分页游标。');
635
+ return { conversationId, messages, ...(nextCursor ? { nextCursor } : {}), hasMore, reachedSince: reachedSince && missingReasons.length === 0, complete: missingReasons.length === 0 && (!hasMore || Boolean(nextCursor)), missingReasons: [...new Set(missingReasons)] };
636
+ } catch (error) {
637
+ return { error: '豆包会话历史读取失败,请检查桌面端历史接口是否可用' };
638
+ }
639
+ }`;
416
640
  function buildInjection(body) {
417
641
  const url = "https://www.doubao.com/chat/completion?aid=582478&channel=win&client_platform=pc_client&device_platform=web&doubao_device_platform=desktop&language=zh&pkg_type=release_version&real_aid=582478&region=CN&runtime=web&samantha_web=1&sys_region=CN&tz_name=Asia%2FShanghai&use-olympus-account=1&version_code=20800&web_platform=desktop";
418
642
  return String.raw `(() => {
@@ -439,10 +663,10 @@ function buildInjection(body) {
439
663
  if (version) { requestUrl.searchParams.set('pc_version', version); requestUrl.searchParams.set('doubao_pc_version', version); }
440
664
  if (chromium) requestUrl.searchParams.set('chromium_version', chromium);
441
665
  window.__mab_doubao_request_url = requestUrl.href;
442
- const response = await fetch(requestUrl.href, { method: 'POST', headers: { 'content-type': 'application/json' }, credentials: 'include', body: ${JSON.stringify(body)}, signal: controller.signal });
666
+ const response = await fetch(requestUrl.href, { method: 'POST', headers: { 'content-type': 'application/json', ...(JSON.parse(${JSON.stringify(body)}).client_meta.conversation_id ? {} : { 'Agw-Js-Conv': 'str, str' }) }, credentials: 'include', body: ${JSON.stringify(body)}, signal: controller.signal });
443
667
  if (!response.ok) throw new Error('HTTP ' + response.status);
444
668
  const reader = response.body?.getReader(); if (!reader) throw new Error('响应无 body');
445
- const decoder = new TextDecoder(); let buffer = '', reply = '', events = [], expectVideo = false;
669
+ const decoder = new TextDecoder(); let buffer = '', reply = '', events = [], expectVideo = false, conversationId = ${JSON.stringify(JSON.parse(body).client_meta.conversation_id)};
446
670
  const blocks = new Map(), questionIds = new Set(), messageIds = new Set();
447
671
  const collect = (value) => {
448
672
  if (!value || typeof value !== 'object') return;
@@ -460,15 +684,16 @@ function buildInjection(body) {
460
684
  if (event === 'STREAM_ERROR') throw new Error((${doubaoStreamErrorMessage.toString()})(data));
461
685
  if (!data) return;
462
686
  let json; try { json = JSON.parse(data); } catch { return; }
687
+ if (event === 'SSE_ACK' && !conversationId && typeof json.ack_client_meta?.conversation_id === 'string' && /^\d+$/.test(json.ack_client_meta.conversation_id)) conversationId = json.ack_client_meta.conversation_id;
463
688
  const conversation = json.meta?.conversation_id || json.message?.conversation_id || json.ack_client_meta?.conversation_id;
464
- if (conversation && conversation !== ${JSON.stringify(JSON.parse(body).client_meta.conversation_id)}) return;
689
+ if (conversation && conversation !== conversationId) return;
465
690
  collect(json);
466
- if (event === 'SSE_ACK' && json.ack_client_meta?.conversation_id === ${JSON.stringify(JSON.parse(body).client_meta.conversation_id)}) {
691
+ if (event === 'SSE_ACK' && json.ack_client_meta?.conversation_id === conversationId) {
467
692
  // question_id 可能超过 JS 安全整数范围,必须从原始 JSON 文本提取。
468
693
  for (const match of data.matchAll(/"question_id"\s*:\s*(?:"([^"\n]+)"|(\d+))/g)) questionIds.add(match[1] || match[2]);
469
694
  }
470
695
  const meta = json.meta || json.message;
471
- if (meta?.conversation_id === ${JSON.stringify(JSON.parse(body).client_meta.conversation_id)} && meta.user_type === 2 && meta.message_id) messageIds.add(String(meta.message_id));
696
+ if (meta?.conversation_id === conversationId && meta.user_type === 2 && meta.message_id) messageIds.add(String(meta.message_id));
472
697
  if (event === 'STREAM_CHUNK') for (const patch of (json.patch_op || [])) reply = merge(reply, patch.patch_value?.tts_content || '');
473
698
  if (event === 'CHUNK_DELTA' && typeof json.text === 'string') reply = merge(reply, json.text);
474
699
  if (event === 'STREAM_MSG_NOTIFY' || event === 'FULL_MSG_NOTIFY') for (const text of (${exports.responseTextScript})(json)) reply = merge(reply, text);
@@ -478,7 +703,7 @@ function buildInjection(body) {
478
703
  if (buffer.trim()) consume(buffer);
479
704
  const media = (${exports.responseArtifactsScript})([...blocks.values()]);
480
705
  const ok = Boolean(reply.trim() || media.artifacts.length || media.pending || expectVideo);
481
- if (window.__mab_doubao_controller === controller) window.__mab_doubao_result = JSON.stringify({ ok, reply, ...media, expectVideo, questionIds: [...questionIds], messageIds: [...messageIds], reason: ok ? '' : 'SSE 未提取到文本或附件; events=' + events.slice(0, 20).join(',') });
706
+ if (window.__mab_doubao_controller === controller) window.__mab_doubao_result = JSON.stringify({ ok, reply, conversationId, ...media, expectVideo, questionIds: [...questionIds], messageIds: [...messageIds], reason: ok ? '' : 'SSE 未提取到文本或附件; events=' + events.slice(0, 20).join(',') });
482
707
  } catch (error) { if (window.__mab_doubao_controller === controller) window.__mab_doubao_result = JSON.stringify({ ok: false, reason: String(error) }); }
483
708
  })(); return 'started';
484
709
  })()`;
@@ -1,12 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DoubaoPlugin = exports.doubaoManifest = void 0;
4
+ const promises_1 = require("node:fs/promises");
5
+ const node_path_1 = require("node:path");
4
6
  const cdp_client_1 = require("./cdp-client");
7
+ const session_history_1 = require("./session-history");
5
8
  const plugin_1 = require("../../plugin");
6
9
  const desktop_launch_1 = require("../desktop-launch");
7
10
  exports.doubaoManifest = {
8
11
  id: "doubao",
9
- version: "1.0.16",
12
+ version: "1.0.20",
10
13
  displayName: "豆包",
11
14
  entry: "dist/index.js",
12
15
  apiVersion: "1",
@@ -18,15 +21,35 @@ class DoubaoPlugin {
18
21
  injectedClient;
19
22
  manifest = exports.doubaoManifest;
20
23
  attachmentInput = true;
24
+ sessionOperations = [];
25
+ sessionHistory;
21
26
  client;
22
27
  state = "stopped";
23
28
  message = "";
29
+ queue = Promise.resolve();
30
+ pending = 0;
31
+ bindings = new Map();
32
+ dataDir;
24
33
  constructor(injectedClient) {
25
34
  this.injectedClient = injectedClient;
26
35
  }
27
36
  disposer;
28
37
  reconnect;
29
38
  async activate(context) {
39
+ this.dataDir = context.dataDir;
40
+ this.bindings.clear();
41
+ if (this.dataDir) {
42
+ try {
43
+ const saved = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(this.dataDir, "sessions.json"), "utf8"));
44
+ if (!saved || typeof saved !== "object" || Array.isArray(saved) || Object.values(saved).some(value => typeof value !== "string" || !/^\d*$/.test(value)))
45
+ throw new Error("豆包会话绑定记录无效");
46
+ this.bindings = new Map(Object.entries(saved));
47
+ }
48
+ catch (error) {
49
+ if (error.code !== "ENOENT")
50
+ throw error;
51
+ }
52
+ }
30
53
  this.client = this.injectedClient ?? new cdp_client_1.DoubaoCdpClient({
31
54
  port: (0, plugin_1.resolveRuntimePort)(context.config, context.runtime, "doubao", "DOUBAO_CDP_PORT", 9227),
32
55
  conversationId: typeof context.config.conversationId === "string" ? context.config.conversationId : undefined,
@@ -34,6 +57,11 @@ class DoubaoPlugin {
34
57
  pluginList: Array.isArray(context.config.pluginList) ? context.config.pluginList.filter((value) => typeof value === "string") : undefined,
35
58
  timeoutMs: typeof context.config.timeoutMs === "number" ? context.config.timeoutMs : undefined,
36
59
  });
60
+ this.sessionHistory = new session_history_1.DoubaoSessionHistoryProvider({
61
+ scope: `doubao:${(0, plugin_1.resolveRuntimePort)(context.config, context.runtime, "doubao", "DOUBAO_CDP_PORT", 9227)}`,
62
+ client: () => this.client,
63
+ });
64
+ this.sessionOperations = this.client.createSession && this.client.complete ? ["new"] : [];
37
65
  this.reconnect = async () => {
38
66
  try {
39
67
  await this.client.connect();
@@ -73,6 +101,7 @@ class DoubaoPlugin {
73
101
  }
74
102
  }
75
103
  async deactivate() {
104
+ await this.queue;
76
105
  this.disposer?.();
77
106
  this.disposer = undefined;
78
107
  this.reconnect = undefined;
@@ -85,14 +114,61 @@ class DoubaoPlugin {
85
114
  ? { status: "ready", details: { adapter: "doubao", cdp: true } }
86
115
  : { status: "degraded", message: this.message || "豆包 CDP 未连接", details: { adapter: "doubao", cdp: false } };
87
116
  }
88
- async handle(request) {
117
+ async controlSession(request) {
118
+ if (request.operation !== "new")
119
+ throw new Error(`豆包不支持会话操作:${String(request.operation)}`);
120
+ if (!request.sessionId?.trim())
121
+ throw new Error("豆包新会话缺少 sessionId");
122
+ if (this.pending)
123
+ throw new Error("豆包当前忙,无法创建新会话");
124
+ if (!this.sessionOperations.includes("new"))
125
+ throw new Error("豆包当前传输不支持新建会话");
126
+ return this.enqueue(async () => {
127
+ await this.ensureConnected();
128
+ await this.client.createSession(request.name);
129
+ await this.remember(request.sessionId, "");
130
+ return { text: "豆包已创建新会话", sessionId: request.sessionId };
131
+ });
132
+ }
133
+ async ensureConnected() {
89
134
  if (this.state !== "ready" || !this.client)
90
135
  throw new Error("豆包插件未就绪");
91
- if (request.attachments?.length && !this.client.complete)
92
- throw new Error("当前豆包客户端不支持附件输入");
93
136
  if (!this.client.connected())
94
137
  await this.reconnect?.();
95
- return this.client.complete ? this.client.complete(request.task, undefined, request.attachments) : { text: await this.client.direct(request.task) };
138
+ }
139
+ async handle(request) {
140
+ return this.enqueue(async () => {
141
+ await this.ensureConnected();
142
+ const client = this.client;
143
+ if (request.attachments?.length && !client.complete)
144
+ throw new Error("当前豆包客户端不支持附件输入");
145
+ if (!client.complete)
146
+ return { text: await client.direct(request.task) };
147
+ const conversationId = request.sessionId ? this.bindings.get(request.sessionId) ?? "" : undefined;
148
+ const response = await client.complete(request.task, undefined, request.attachments, conversationId);
149
+ if (!request.sessionId)
150
+ return response;
151
+ if (!response.sessionId || !/^\d+$/.test(response.sessionId))
152
+ throw new Error("豆包未返回有效的服务端会话标识");
153
+ await this.remember(request.sessionId, response.sessionId);
154
+ return { ...response, sessionId: request.sessionId };
155
+ });
156
+ }
157
+ enqueue(task) {
158
+ this.pending++;
159
+ const run = this.queue.then(task);
160
+ this.queue = run.then(() => undefined, () => undefined);
161
+ return run.finally(() => { this.pending--; });
162
+ }
163
+ async remember(sessionId, conversationId) {
164
+ const next = new Map(this.bindings).set(sessionId, conversationId);
165
+ if (this.dataDir) {
166
+ await (0, promises_1.mkdir)(this.dataDir, { recursive: true });
167
+ const file = (0, node_path_1.join)(this.dataDir, "sessions.json");
168
+ await (0, promises_1.writeFile)(`${file}.tmp`, `${JSON.stringify(Object.fromEntries(next), null, 2)}\n`, { mode: 0o600 });
169
+ await (0, promises_1.rename)(`${file}.tmp`, file);
170
+ }
171
+ this.bindings = next;
96
172
  }
97
173
  async handleHttp(request, response) {
98
174
  let raw = "";
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DoubaoSessionHistoryProvider = void 0;
4
+ const unsupported = (reason) => ({ supported: false, reason });
5
+ class DoubaoSessionHistoryProvider {
6
+ options;
7
+ mode = "all";
8
+ limitation = "豆包通过原生历史目录和单会话消息链分页只读读取对话正文。";
9
+ listed = new Map();
10
+ listedSessions = new Map();
11
+ readAt = "";
12
+ snapshotAt = 0;
13
+ constructor(options) {
14
+ this.options = options;
15
+ }
16
+ get scope() { return this.options.scope; }
17
+ async listSessions(query) {
18
+ if (!Number.isInteger(query.limit) || query.limit < 1 || query.limit > 50)
19
+ return unsupported("豆包每页会话数量必须为 1–50");
20
+ if (query.cursor !== undefined && (typeof query.cursor !== "string" || !query.cursor.trim()))
21
+ return unsupported("豆包会话目录游标无效");
22
+ const client = this.options.client();
23
+ if (!client?.connected() || !client.listHistory)
24
+ return unsupported("豆包 CDP 未连接或当前客户端不提供只读历史目录接口");
25
+ try {
26
+ const page = await client.listHistory(query.cursor, query.limit);
27
+ if (!Array.isArray(page.conversations) || typeof page.complete !== "boolean" || !Array.isArray(page.missingReasons))
28
+ return unsupported("豆包会话目录格式无效");
29
+ if (query.cursor === undefined) {
30
+ this.listed.clear();
31
+ this.listedSessions.clear();
32
+ }
33
+ this.readAt = new Date().toISOString();
34
+ this.snapshotAt = Date.now();
35
+ for (const conversation of page.conversations) {
36
+ if (!conversation || typeof conversation.id !== "string" || !conversation.id || !Array.isArray(conversation.messages))
37
+ return unsupported("豆包会话目录包含无效条目");
38
+ this.listed.set(conversation.id, conversation);
39
+ this.listedSessions.set(conversation.id, { id: conversation.id, title: conversation.title, ...(conversation.updatedAt ? { updatedAt: conversation.updatedAt } : {}) });
40
+ }
41
+ const missingReasons = [...new Set(page.missingReasons)];
42
+ return {
43
+ supported: true,
44
+ sessions: page.conversations.map(item => ({ id: item.id, title: item.title, ...(item.updatedAt ? { updatedAt: item.updatedAt } : {}) })),
45
+ ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
46
+ complete: page.complete,
47
+ missingReasons,
48
+ readAt: this.readAt,
49
+ };
50
+ }
51
+ catch (error) {
52
+ return unsupported("豆包历史目录读取失败,请检查桌面端接口或版本兼容性");
53
+ }
54
+ }
55
+ async readSession(query) {
56
+ if (!Number.isInteger(query.limit) || query.limit < 1 || query.limit > 100)
57
+ return unsupported("豆包每页消息数量必须为 1–100");
58
+ const since = query.since;
59
+ const sinceMs = since === undefined ? undefined : Date.parse(since);
60
+ if (since !== undefined && !Number.isFinite(sinceMs))
61
+ return unsupported("豆包会话正文起始时间无效");
62
+ let nativeCursor;
63
+ if (query.cursor !== undefined) {
64
+ const bound = query.cursor.match(/^db-read:(\d+):([1-9]\d*)$/);
65
+ if (bound) {
66
+ if (sinceMs !== Number(bound[1]))
67
+ return unsupported("豆包会话正文游标与起始时间不匹配");
68
+ nativeCursor = bound[2];
69
+ }
70
+ else if (/^[1-9]\d*$/.test(query.cursor) && sinceMs === undefined)
71
+ nativeCursor = query.cursor;
72
+ else
73
+ return unsupported("豆包会话正文游标无效");
74
+ }
75
+ const session = this.listedSessions.get(query.sessionId);
76
+ if (!session)
77
+ return unsupported("请先分页列出豆包会话目录,再读取该会话");
78
+ if (Date.now() - this.snapshotAt > 120_000) {
79
+ this.listed.clear();
80
+ this.listedSessions.clear();
81
+ return unsupported("豆包历史快照已过期,请重新列出会话目录");
82
+ }
83
+ const client = this.options.client();
84
+ if (!client?.connected() || !client.readHistory)
85
+ return unsupported("豆包 CDP 未连接或当前客户端不提供只读会话正文分页接口");
86
+ let page;
87
+ try {
88
+ page = await client.readHistory(query.sessionId, nativeCursor, query.limit, since);
89
+ }
90
+ catch {
91
+ return unsupported("豆包会话正文读取失败,请检查桌面端接口或版本兼容性");
92
+ }
93
+ if (page.conversationId !== query.sessionId || !Array.isArray(page.messages) || typeof page.hasMore !== "boolean" || typeof page.complete !== "boolean" || !Array.isArray(page.missingReasons))
94
+ return unsupported("豆包会话正文格式或会话身份无效");
95
+ if (page.nextCursor !== undefined && (!/^[1-9]\d*$/.test(page.nextCursor) || page.nextCursor === nativeCursor))
96
+ return unsupported("豆包会话正文返回无效或重复游标");
97
+ const pageTimes = page.messages.map(message => message.time ? Date.parse(message.time) : NaN).filter(Number.isFinite);
98
+ const oldest = pageTimes.length ? Math.min(...pageTimes) : undefined;
99
+ const reachedSince = sinceMs !== undefined && (page.reachedSince === true || oldest !== undefined && oldest < sinceMs);
100
+ const visible = sinceMs === undefined ? page.messages : page.messages.filter(message => !message.time || Date.parse(message.time) >= sinceMs);
101
+ const turns = visible.map(message => ({
102
+ ...(message.id ? { id: message.id } : {}),
103
+ role: message.role,
104
+ text: message.text,
105
+ ...(message.time ? { time: message.time } : {}),
106
+ }));
107
+ const missingReasons = [...page.missingReasons];
108
+ if (page.hasMore && !page.nextCursor && !reachedSince)
109
+ missingReasons.push("豆包会话正文仍有后续消息,但分页游标缺失。");
110
+ if (!turns.every(turn => Boolean(turn.id && turn.time)))
111
+ missingReasons.push("部分豆包消息缺少稳定标识或发送时间,无法确认正文完整性。");
112
+ return {
113
+ supported: true,
114
+ session,
115
+ turns,
116
+ ...(page.nextCursor && !reachedSince ? { nextCursor: sinceMs === undefined ? page.nextCursor : `db-read:${sinceMs}:${page.nextCursor}` } : {}),
117
+ complete: page.complete && (!page.hasMore || Boolean(page.nextCursor) || reachedSince) && missingReasons.length === 0,
118
+ missingReasons: [...new Set(missingReasons)],
119
+ readAt: this.readAt,
120
+ };
121
+ }
122
+ }
123
+ exports.DoubaoSessionHistoryProvider = DoubaoSessionHistoryProvider;