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
@@ -1,12 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DoubaoCdpClient = 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;
7
7
  exports.cleanDoubaoReply = cleanDoubaoReply;
8
+ exports.downloadDoubaoArtifact = downloadDoubaoArtifact;
8
9
  exports.downloadDoubaoImage = downloadDoubaoImage;
9
10
  const node_crypto_1 = require("node:crypto");
11
+ const attachments_1 = require("../../attachments");
12
+ const attachments_2 = require("./attachments");
10
13
  function doubaoStreamErrorMessage(data) {
11
14
  let code = "";
12
15
  let message = "";
@@ -14,6 +17,17 @@ function doubaoStreamErrorMessage(data) {
14
17
  const value = JSON.parse(data);
15
18
  code = String(value.error_code ?? value.code ?? value.error?.error_code ?? value.error?.code ?? "");
16
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
+ }
17
31
  }
18
32
  catch { /* 平台可能返回非 JSON 错误 */ }
19
33
  if (message.length > 300)
@@ -23,16 +37,26 @@ function doubaoStreamErrorMessage(data) {
23
37
  return `豆包平台返回错误${code ? `(错误码 ${code})` : ""}:${message}`;
24
38
  return `豆包平台返回流式错误${code ? `(错误码 ${code})` : ""}`;
25
39
  }
26
- 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") };
27
41
  function sleep(ms, signal) {
28
42
  if (signal?.aborted)
29
43
  return Promise.reject(new Error("请求已取消"));
30
44
  return new Promise((resolve, reject) => { const timer = setTimeout(resolve, ms); signal?.addEventListener("abort", () => { clearTimeout(timer); reject(new Error("请求已取消")); }, { once: true }); });
31
45
  }
32
- function requestBody(text, conversationId, botId, pluginList = []) {
33
- 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 };
46
+ function requestBody(text, conversationId, botId, pluginList = [], attachments = []) {
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 };
34
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: [] };
35
- 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: [{ local_message_id: (0, node_crypto_1.randomUUID)(), content_block: [block], message_status: 0 }], 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" } });
50
+ const messages = [{ local_message_id: (0, node_crypto_1.randomUUID)(), content_block: [block], message_status: 0 }];
51
+ if (attachments.length)
52
+ messages.unshift({ local_message_id: (0, node_crypto_1.randomUUID)(), content_block: [{ ...block, block_type: 10052, block_id: (0, node_crypto_1.randomUUID)(), content: { attachment_block: { attachments: attachments.map(item => ({
53
+ type: item.type === "image" ? 1 : 3, identifier: item.localKey,
54
+ ...(item.type === "image" ? { image: { name: item.fileName, uri: item.fileKey, image_ori: { url: "", width: item.imageWidth, height: item.imageHeight, format: item.fileName.split(".").pop()?.toLowerCase(), url_formats: {} } } }
55
+ : { file: { uri: item.fileKey, url: "", file_type: 0, name: item.fileName, size: item.size } }),
56
+ parse_state: item.type === "image" ? 0 : 1, review_state: 1, upload_status: 1, progress: 100, src: "",
57
+ })) }, pc_event_block: "" } }], message_status: 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" } });
36
60
  }
37
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; }`;
38
62
  function mergeDoubaoReply(current, next) {
@@ -71,25 +95,91 @@ exports.responseImagesScript = `(value) => {
71
95
  };
72
96
  visit(value); return [...urls];
73
97
  }`;
74
- async function downloadDoubaoImage(address, fetchImpl = fetch, signal) {
75
- const url = new URL(address);
76
- if (url.protocol !== "https:" || !/^p\d+-flow-imagex-sign\.byteimg\.com$/.test(url.hostname) || url.username || url.password || (url.port && url.port !== "443"))
77
- throw new Error("豆包图片地址无效");
98
+ // 只读取平台原生产物块,不将正文链接、封面或本地路径当作附件。
99
+ exports.responseArtifactsScript = `(value) => {
100
+ const artifacts = [], seen = new Set(); let pending = false, failed = false;
101
+ const add = (item) => { const key = JSON.stringify(item); if (!seen.has(key)) { seen.add(key); artifacts.push(item); } };
102
+ const visit = (item) => {
103
+ if (Array.isArray(item)) { for (const child of item) visit(child); return; }
104
+ if (!item || typeof item !== 'object') return;
105
+ const type = Number(item.block_type), content = item.content || item.content_obj;
106
+ if (type === 2074) {
107
+ for (const creation of (content?.creation_block || content)?.creations || []) {
108
+ const image = creation.image, video = creation.video;
109
+ if (image?.status === 2 && item.is_finish === true && image.image_ori?.url) add({ kind: 'image', url: image.image_ori.url });
110
+ if (image && (image.status === 1 || item.is_finish === false)) pending = true;
111
+ if (video?.status === 3 && item.is_finish === true) {
112
+ if (video.vid || video.download_url) add({ kind: 'video', ...(video.vid ? { vid: video.vid } : {}), ...(video.download_url ? { url: video.download_url } : {}) });
113
+ } else if (video && (video.status === 1 || video.status === 2 || item.is_finish === false)) pending = true;
114
+ if (video?.status === 4) failed = true;
115
+ }
116
+ }
117
+ if (type === 10020) {
118
+ const file = content?.file_block || content;
119
+ if (file?.name && (file.uri || file.url)) {
120
+ if (item.is_finish === true) add({ kind: 'file', filename: file.name, ...(file.uri ? { uri: file.uri } : {}), ...(file.url ? { url: file.url } : {}) });
121
+ else pending = true;
122
+ }
123
+ }
124
+ for (const child of Object.values(item)) visit(child);
125
+ };
126
+ visit(value); return { artifacts, pending, ...(failed ? { failed: true } : {}) };
127
+ }`;
128
+ // 异步生成会另发消息,必须通过本轮问题引用关联,不能拿当前页面最近的任意媒体。
129
+ exports.currentResponseArtifactsScript = String.raw `(conversationId, questionIds, messageIds) => {
130
+ if (location.pathname !== '/chat/' + conversationId) return { artifacts: [], pending: true };
131
+ const messages = new Map();
132
+ for (const el of document.querySelectorAll('[data-testid=receive_message]')) {
133
+ const key = Object.keys(el).find(key => key.startsWith('__reactFiber'));
134
+ for (let fiber = el[key], index = 0; fiber && index < 18; fiber = fiber.return, index++) {
135
+ const props = fiber.memoizedProps;
136
+ const message = [props?.message, props?.data, props?.item, props?.value].find(value => value?.message_id && value.conversation_id === conversationId);
137
+ if (!message) continue;
138
+ // 原生引用 ID 可能超出安全整数范围,从原始字符串提取以免 JSON.parse 舍入。
139
+ const reference = message.ext?.reference_info;
140
+ const match = typeof reference === 'string' ? reference.match(/"referenced_message_id"\s*:\s*(?:"([^"\n]+)"|(\d+))/) : null;
141
+ const questionId = match?.[1] || match?.[2];
142
+ if (messageIds.includes(message.message_id) || questionIds.includes(questionId)) messages.set(message.message_id, message.content_blocks_v2 || message.content_blocks || message.content_block || []);
143
+ break;
144
+ }
145
+ }
146
+ return (${exports.responseArtifactsScript})([...messages.values()]);
147
+ }`;
148
+ async function downloadDoubaoArtifact(item, fetchImpl = fetch, signal, budget = attachments_1.MAX_ATTACHMENT_BYTES) {
149
+ const label = item.kind === "image" ? "图片" : item.kind === "video" ? "视频" : "文件";
150
+ let url;
151
+ try {
152
+ url = new URL(item.url ?? "");
153
+ }
154
+ catch {
155
+ throw new Error(`豆包${label}地址无效`);
156
+ }
157
+ const trusted = item.kind === "image" ? /^p\d+-flow-imagex-sign\.byteimg\.com$/.test(url.hostname)
158
+ : item.kind === "video" ? /^v\d+-(?:vdl\.doubao\.com|show\.douyinvod\.com)$/.test(url.hostname)
159
+ : /^p\d+-flow-sign\.byteimg\.com$/.test(url.hostname);
160
+ if (url.protocol !== "https:" || !trusted || url.username || url.password || (url.port && url.port !== "443"))
161
+ throw new Error(`豆包${label}地址无效`);
162
+ const filename = item.kind === "file" ? (0, attachments_1.attachmentFilename)(item.filename ?? "") : "";
163
+ const limit = Math.min(attachments_1.MAX_ATTACHMENT_BYTES, budget);
164
+ if (limit <= 0)
165
+ throw new Error("豆包附件总大小过大(上限 64 MiB)");
78
166
  const response = await fetchImpl(url.href, { redirect: "error", signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(60_000)]) : AbortSignal.timeout(60_000) });
79
167
  if (!response.ok)
80
- throw new Error(`下载豆包图片 HTTP ${response.status}`);
168
+ throw new Error(`下载豆包${label} HTTP ${response.status}`);
81
169
  const contentType = response.headers.get("content-type")?.split(";", 1)[0].trim().toLowerCase() ?? "";
82
170
  const extension = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp" }[contentType];
83
- if (!extension)
84
- throw new Error("豆包图片类型不支持");
85
- const limit = 32 * 1024 * 1024;
171
+ if ((item.kind === "image" && !extension) || (item.kind === "video" && contentType !== "video/mp4") || !/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(contentType)) {
172
+ await response.body?.cancel();
173
+ throw new Error(`豆包${label}类型不支持`);
174
+ }
175
+ const oversized = () => new Error(`豆包${label}过大(单项上限 32 MiB,总计上限 64 MiB)`);
86
176
  if (Number(response.headers.get("content-length")) > limit) {
87
177
  await response.body?.cancel();
88
- throw new Error("豆包图片过大(上限 32 MiB)");
178
+ throw oversized();
89
179
  }
90
180
  const reader = response.body?.getReader();
91
181
  if (!reader)
92
- throw new Error("豆包图片内容为空");
182
+ throw new Error(`豆包${label}内容为空`);
93
183
  const chunks = [];
94
184
  let size = 0;
95
185
  try {
@@ -100,7 +190,7 @@ async function downloadDoubaoImage(address, fetchImpl = fetch, signal) {
100
190
  size += value.byteLength;
101
191
  if (size > limit) {
102
192
  await reader.cancel();
103
- throw new Error("豆包图片过大(上限 32 MiB)");
193
+ throw oversized();
104
194
  }
105
195
  chunks.push(value);
106
196
  }
@@ -109,24 +199,47 @@ async function downloadDoubaoImage(address, fetchImpl = fetch, signal) {
109
199
  reader.releaseLock();
110
200
  }
111
201
  const data = Buffer.concat(chunks);
112
- const valid = contentType === "image/png" ? data.subarray(0, 8).toString("hex") === "89504e470d0a1a0a"
113
- : contentType === "image/jpeg" ? data.subarray(0, 3).toString("hex") === "ffd8ff"
114
- : data.subarray(0, 4).toString() === "RIFF" && data.subarray(8, 12).toString() === "WEBP";
115
- if (!valid)
202
+ if (item.kind === "image" && (0, attachments_1.detectImageContentType)(data) !== contentType)
116
203
  throw new Error("豆包图片内容与类型不符");
117
- return { data, contentType, filename: `doubao-${(0, node_crypto_1.randomUUID)()}.${extension}` };
204
+ if (contentType === "video/mp4" && (data.length < 12 || data.subarray(4, 8).toString() !== "ftyp"))
205
+ throw new Error("豆包视频内容与类型不符");
206
+ const result = { kind: item.kind === "image" ? "image" : "file", data, contentType, filename: filename || `doubao-${(0, node_crypto_1.randomUUID)()}.${item.kind === "image" ? extension : "mp4"}` };
207
+ (0, attachments_1.validateAttachments)([result]);
208
+ return result;
209
+ }
210
+ async function downloadDoubaoImage(address, fetchImpl = fetch, signal) {
211
+ const { kind: _kind, ...image } = await downloadDoubaoArtifact({ kind: "image", url: address }, fetchImpl, signal);
212
+ return image;
118
213
  }
119
214
  class DoubaoCdpClient {
120
215
  socket;
216
+ connecting;
217
+ queue = Promise.resolve();
121
218
  sequence = 0;
219
+ conversationId = "";
122
220
  closeController = new AbortController();
123
221
  pending = new Map();
124
222
  options;
125
- 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 }; }
126
- connected() { return Boolean(this.socket); }
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; }
224
+ connected() { return this.socket?.readyState === 1; }
127
225
  async connect() {
128
- if (this.socket)
226
+ if (this.closeController.signal.aborted)
227
+ throw new Error("豆包 CDP 已关闭");
228
+ if (this.connected())
129
229
  return;
230
+ if (this.connecting)
231
+ return this.connecting;
232
+ const opening = this.openConnection();
233
+ this.connecting = opening;
234
+ try {
235
+ await opening;
236
+ }
237
+ finally {
238
+ if (this.connecting === opening)
239
+ this.connecting = undefined;
240
+ }
241
+ }
242
+ async openConnection() {
130
243
  const response = await (this.options.fetchImpl ?? fetch)(`http://127.0.0.1:${this.options.port}/json`);
131
244
  if (!response.ok)
132
245
  throw new Error(`豆包 CDP /json HTTP ${response.status}`);
@@ -134,10 +247,26 @@ class DoubaoCdpClient {
134
247
  const page = targets.find(t => t.type === "page" && (t.url ?? "").includes(`/chat/${this.options.conversationId}`)) ?? targets.find(t => t.type === "page" && (t.url ?? "").includes("doubao-chat/chat")) ?? targets.find(t => t.type === "page" && (t.url ?? "").includes("/chat/"));
135
248
  if (!page?.webSocketDebuggerUrl)
136
249
  throw new Error(`未找到豆包 chat 页面(CDP ${this.options.port})`);
250
+ if (this.closeController.signal.aborted)
251
+ throw new Error("豆包 CDP 已关闭");
252
+ this.conversationId = this.options.conversationId || (page.url ?? "").match(/\/chat\/([^/?#]+)/)?.[1] || "";
137
253
  const socket = new WebSocket(page.webSocketDebuggerUrl);
138
254
  this.socket = socket;
139
255
  socket.onmessage = event => this.onMessage(String(event.data));
140
- await new Promise((resolve, reject) => { socket.onopen = () => resolve(); socket.onerror = () => reject(new Error("豆包 CDP WebSocket 连接失败")); });
256
+ await new Promise((resolve, reject) => {
257
+ const disconnect = (error) => {
258
+ if (this.socket === socket) {
259
+ this.socket = undefined;
260
+ for (const pending of this.pending.values())
261
+ pending.reject(error);
262
+ this.pending.clear();
263
+ }
264
+ reject(error);
265
+ };
266
+ socket.onopen = () => resolve();
267
+ socket.onclose = () => disconnect(new Error("豆包 CDP 已断开"));
268
+ socket.onerror = () => { disconnect(new Error("豆包 CDP WebSocket 连接失败")); socket.close(); };
269
+ });
141
270
  }
142
271
  async cancel() {
143
272
  this.closeController.abort();
@@ -159,27 +288,152 @@ class DoubaoCdpClient {
159
288
  async direct(text, signal) {
160
289
  return (await this.complete(text, signal)).text;
161
290
  }
162
- async complete(text, signal) {
163
- await this.evaluate(buildInjection(requestBody(text, this.options.conversationId, this.options.botId, this.options.pluginList)));
164
- const deadline = Date.now() + this.options.timeoutMs;
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));
337
+ this.queue = run.then(() => undefined, () => undefined);
338
+ return run;
339
+ }
340
+ async runComplete(text, signal, attachments, conversationId) {
341
+ (0, attachments_1.validateAttachments)(attachments);
342
+ for (const item of attachments ?? []) {
343
+ if (item.contentType.toLowerCase().startsWith("video/"))
344
+ throw new Error("豆包桌面端不支持视频附件输入");
345
+ if (!/\.(pdf|txt|csv|docx?|xlsx?|pptx?|md|mobi|epub|png|jpe?g|webp|py|java|js|ts|c|cpp|h|hpp|cc|cxx|html|css|php|rb|pl|sh|bash|swift|kt|kts|ktm|go|dart|scala|cs|aspx|xaml|vue|mod|proto|tsx|jsx|rs|json|yaml|yml|xml|env|config|properties|ini|toml|plist|markdown|adoc|feature|sum|bat|cmd|ps1|vbs|lua|vmx|vbox|dockerfile)$/i.test((0, attachments_1.attachmentFilename)(item.filename)))
346
+ throw new Error("豆包桌面端不支持此文件类型");
347
+ }
165
348
  const requestSignal = signal ? AbortSignal.any([signal, this.closeController.signal]) : this.closeController.signal;
349
+ if (requestSignal.aborted)
350
+ throw new Error("请求已取消");
351
+ await this.connect();
352
+ if (requestSignal.aborted)
353
+ throw new Error("请求已取消");
354
+ const target = conversationId ?? this.conversationId;
355
+ const uploaded = attachments?.length ? await this.uploadAttachments(attachments, requestSignal) : [];
356
+ await this.evaluate(buildInjection(requestBody(text, target, this.options.botId, this.options.pluginList, uploaded)));
357
+ const deadline = Date.now() + this.options.timeoutMs;
166
358
  while (Date.now() < deadline) {
167
359
  await sleep(1000, requestSignal);
168
360
  const result = await this.evaluate("window.__mab_doubao_result");
169
361
  if (typeof result !== "string")
170
362
  continue;
171
363
  const parsed = JSON.parse(result);
364
+ if (parsed.failed)
365
+ throw new Error("豆包媒体生成失败");
172
366
  if (!parsed.ok)
173
367
  throw new Error(`豆包请求失败: ${parsed.reason ?? "未知错误"}`);
174
- const images = [];
175
- for (const address of new Set(parsed.images ?? []))
176
- images.push(await downloadDoubaoImage(address, this.options.fetchImpl, requestSignal));
368
+ let artifacts = parsed.artifacts ?? [];
369
+ if (parsed.pending || parsed.expectVideo) {
370
+ const current = await this.evaluate(`(${exports.currentResponseArtifactsScript})(${JSON.stringify(parsed.conversationId || target)}, ${JSON.stringify(parsed.questionIds ?? [])}, ${JSON.stringify(parsed.messageIds ?? [])})`);
371
+ if (current.failed)
372
+ throw new Error("豆包媒体生成失败");
373
+ if (current.pending || !current.artifacts.length || (parsed.expectVideo && !current.artifacts.some(item => item.kind === "video")))
374
+ continue;
375
+ artifacts = [...artifacts, ...current.artifacts];
376
+ }
377
+ const unique = [...new Map(artifacts.map(item => [`${item.kind}:${item.vid || item.uri || item.url}`, item])).values()];
378
+ if (unique.length > attachments_1.MAX_ATTACHMENTS)
379
+ throw new Error("豆包单条消息最多返回 8 个附件");
380
+ const resolved = unique.some(item => !item.url) ? await this.evaluate(buildResolveArtifactsInjection(unique), true) : unique;
381
+ const output = [];
382
+ let total = 0;
383
+ for (const item of resolved) {
384
+ const downloaded = await downloadDoubaoArtifact(item, this.options.fetchImpl, requestSignal, attachments_1.MAX_TOTAL_ATTACHMENT_BYTES - total);
385
+ total += downloaded.data.byteLength;
386
+ output.push(downloaded);
387
+ }
388
+ (0, attachments_1.validateAttachments)(output);
389
+ const images = output.filter(item => item.kind === "image").map(({ kind: _kind, ...image }) => image);
177
390
  const reply = cleanDoubaoReply(parsed.reply ?? "", text);
178
- if (images.length || reply)
179
- return { text: reply || "图片已生成。", ...(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
+ }
180
398
  }
181
399
  throw new Error("等待豆包回复超时");
182
400
  }
401
+ async uploadAttachments(attachments, signal) {
402
+ const token = (0, node_crypto_1.randomUUID)();
403
+ const items = attachments.map((item, index) => ({ name: (0, attachments_1.attachmentFilename)(item.filename), uploadName: `${token}-${index}-${(0, attachments_1.attachmentFilename)(item.filename)}`, type: item.contentType, size: item.data.byteLength, base64: Buffer.from(item.data).toString("base64") }));
404
+ let uploadError;
405
+ try {
406
+ const started = await this.evaluate((0, attachments_2.buildUploadInjection)(items));
407
+ if (started !== "started")
408
+ throw new Error(String(started));
409
+ const deadline = Date.now() + Math.min(this.options.timeoutMs, 90_000);
410
+ while (Date.now() < deadline) {
411
+ await sleep(500, signal);
412
+ const states = await this.evaluate(`(${attachments_2.uploadStateScript})()`);
413
+ const own = items.map(item => states.find(state => state.fileName === item.uploadName && state.size === item.size));
414
+ if (own.some(item => item && /error|fail/i.test(item.status ?? "")))
415
+ throw new Error("豆包附件上传或解析失败");
416
+ if (own.every(item => item?.fileKey && item.status === "Normal" && (item.type === "image" || item.parseState === 1))) {
417
+ return own.map((item, index) => ({ ...item, fileName: items[index].name }));
418
+ }
419
+ }
420
+ throw new Error("等待豆包附件上传或解析超时");
421
+ }
422
+ catch (error) {
423
+ uploadError = error;
424
+ throw error;
425
+ }
426
+ finally {
427
+ try {
428
+ await this.evaluate(`(${attachments_2.cleanupUploadScript})(${JSON.stringify(items.map(item => item.uploadName))})`);
429
+ }
430
+ catch (cleanupError) {
431
+ if (uploadError)
432
+ throw new AggregateError([uploadError, cleanupError], "豆包附件上传失败,且清理附件草稿失败");
433
+ throw cleanupError;
434
+ }
435
+ }
436
+ }
183
437
  onMessage(raw) { let message; try {
184
438
  message = JSON.parse(raw);
185
439
  }
@@ -188,41 +442,269 @@ class DoubaoCdpClient {
188
442
  } if (message.id === undefined)
189
443
  return; const pending = this.pending.get(message.id); if (!pending)
190
444
  return; this.pending.delete(message.id); pending.resolve(message); }
191
- async cdp(method, params = {}, timeoutMs = this.options.timeoutMs) { if (!this.socket)
445
+ async cdp(method, params = {}, timeoutMs = this.options.timeoutMs) { const socket = this.socket; if (!socket || !this.connected())
192
446
  throw new Error("豆包 CDP 尚未连接"); const id = ++this.sequence; const result = new Promise((resolve, reject) => this.pending.set(id, { resolve, reject })); let timer; try {
193
- this.socket.send(JSON.stringify({ id, method, params }));
447
+ socket.send(JSON.stringify({ id, method, params }));
194
448
  return await Promise.race([result, new Promise((_, reject) => { timer = setTimeout(() => { this.pending.delete(id); reject(new Error(`CDP ${method} 超时`)); }, timeoutMs); })]);
195
449
  }
196
450
  finally {
197
451
  clearTimeout(timer);
198
452
  } }
199
- async evaluate(expression) { const response = await this.cdp("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: false }); if (response.result?.exceptionDetails)
453
+ async evaluate(expression, awaitPromise = false) { const response = await this.cdp("Runtime.evaluate", { expression, returnByValue: true, awaitPromise }); if (response.result?.exceptionDetails)
200
454
  throw new Error(`豆包页面 JS 异常: ${JSON.stringify(response.result.exceptionDetails).slice(0, 300)}`); return response.result?.result?.value; }
201
455
  }
202
456
  exports.DoubaoCdpClient = DoubaoCdpClient;
457
+ function buildResolveArtifactsInjection(artifacts) {
458
+ return `(async () => {
459
+ const controller = window.__mab_doubao_controller;
460
+ const requestUrl = window.__mab_doubao_request_url;
461
+ const result = [];
462
+ for (const item of ${JSON.stringify(artifacts)}) {
463
+ if (item.url) { result.push(item); continue; }
464
+ const path = item.kind === 'video' ? '/samantha/video/get_play_info' : '/alice/message/get_file_url';
465
+ const url = new URL(requestUrl);
466
+ if (url.origin !== 'https://www.doubao.com') throw new Error('豆包产物接口地址无效');
467
+ url.pathname = path; url.hash = '';
468
+ const body = item.kind === 'video' ? { vid: item.vid } : { uris: [item.uri], type: 'file' };
469
+ const response = await fetch(url.href, { method: 'POST', headers: { 'content-type': 'application/json' }, credentials: 'include', body: JSON.stringify(body), signal: controller.signal });
470
+ if (!response.ok) throw new Error('豆包产物地址解析 HTTP ' + response.status);
471
+ const parsed = await response.json();
472
+ if (parsed.code !== 0) throw new Error('豆包产物地址解析失败');
473
+ const info = item.kind === 'video' ? parsed.data?.play_infos?.[0] : parsed.data?.file_urls?.find(value => value.uri === item.uri);
474
+ const address = item.kind === 'video' ? info?.main || info?.backup : info?.main_url || info?.back_url;
475
+ if (typeof address !== 'string' || !address) throw new Error('豆包产物缺少下载地址');
476
+ result.push({ ...item, url: address });
477
+ }
478
+ return result;
479
+ })()`;
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
+ }`;
203
640
  function buildInjection(body) {
204
- const url = "https://www.doubao.com/chat/completion?aid=582478&channel=win&chromium_version=147.0.7727.149&client_platform=pc_client&device_id=2750296938275162&device_platform=web&doubao_device_platform=desktop&doubao_pc_version=2.30.4&fp=verify_2750296938275162&language=zh&pc_version=2.30.4&pkg_type=release_version&real_aid=582478&region=CN&runtime=web&runtime_version=3.37.7&samantha_web=1&sys_region=CN&tea_uuid=2750296938275162&tz_name=Asia%2FShanghai&use-olympus-account=1&version_code=20800&web_platform=desktop";
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";
205
642
  return String.raw `(() => {
206
643
  window.__mab_doubao_result = null;
207
644
  window.__mab_doubao_controller?.abort();
208
- window.__mab_doubao_controller = new AbortController();
645
+ const controller = new AbortController();
646
+ window.__mab_doubao_controller = controller;
209
647
  const merge = ${mergeDoubaoReply.toString()};
210
648
  (async () => {
211
649
  try {
212
- let webId = '', tabId = '';
213
- for (const entry of performance.getEntriesByType('resource')) { try { const u = new URL(entry.name); if (u.host === 'www.doubao.com') { webId ||= u.searchParams.get('web_id') || ''; tabId ||= u.searchParams.get('web_tab_id') || ''; } } catch {} if (webId && tabId) break; }
214
- webId ||= '7689385646880933426'; tabId ||= '98f6fc8e-a1f7-4585-aa08-89b9a10aa451';
215
- let requestUrl = ${JSON.stringify(url)} + '&web_id=' + encodeURIComponent(webId) + '&web_tab_id=' + encodeURIComponent(tabId);
216
- for (const entry of performance.getEntriesByType('resource')) if (entry.name.includes('/chat/completion?')) requestUrl = entry.name;
217
- const response = await fetch(requestUrl, { method: 'POST', headers: { 'content-type': 'application/json' }, credentials: 'include', body: ${JSON.stringify(body)}, signal: window.__mab_doubao_controller.signal });
650
+ const requestUrl = new URL(${JSON.stringify(url)});
651
+ const resources = [];
652
+ for (const entry of performance.getEntriesByType('resource')) { try { const u = new URL(entry.name); if (u.origin === requestUrl.origin) resources.push(u); } catch {} }
653
+ resources.reverse();
654
+ // 优先读取客户端原生请求,避免复用插件上次注入的旧版参数。
655
+ const sources = [...resources.filter(u => u.pathname !== '/chat/completion'), ...resources.filter(u => u.pathname === '/chat/completion')];
656
+ for (const key of [...requestUrl.searchParams.keys(), 'device_id', 'tea_uuid', 'fp', 'pc_version', 'doubao_pc_version', 'chromium_version', 'runtime_version', 'web_id', 'web_tab_id']) {
657
+ const value = sources.find(u => u.searchParams.get(key))?.searchParams.get(key);
658
+ if (value) requestUrl.searchParams.set(key, value);
659
+ }
660
+ const userAgent = navigator.userAgent;
661
+ const version = userAgent.match(/SamanthaDoubao\/([\d.]+)/i)?.[1];
662
+ const chromium = userAgent.match(/Chrome\/([\d.]+)/i)?.[1];
663
+ if (version) { requestUrl.searchParams.set('pc_version', version); requestUrl.searchParams.set('doubao_pc_version', version); }
664
+ if (chromium) requestUrl.searchParams.set('chromium_version', chromium);
665
+ window.__mab_doubao_request_url = requestUrl.href;
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 });
218
667
  if (!response.ok) throw new Error('HTTP ' + response.status);
219
668
  const reader = response.body?.getReader(); if (!reader) throw new Error('响应无 body');
220
- const decoder = new TextDecoder(); let buffer = '', reply = '', events = []; const images = new Set();
221
- const consume = (block) => { const event = (block.match(/^event:\s*(.+)$/m) || [])[1] || ''; const data = (block.match(/^data:\s*(.+)$/m) || [])[1] || ''; if (event) events.push(event); if (event === 'STREAM_ERROR') throw new Error((${doubaoStreamErrorMessage.toString()})(data)); if (!data) return; try { const json = JSON.parse(data); if (['STREAM_CHUNK', 'STREAM_MSG_NOTIFY', 'FULL_MSG_NOTIFY'].includes(event)) for (const url of (${exports.responseImagesScript})(json)) images.add(url); if (event === 'STREAM_CHUNK') for (const patch of (json.patch_op || [])) reply = merge(reply, patch.patch_value?.tts_content || ''); if (event === 'CHUNK_DELTA' && typeof json.text === 'string') reply = merge(reply, json.text); if (event === 'STREAM_MSG_NOTIFY' || event === 'FULL_MSG_NOTIFY') for (const text of (${exports.responseTextScript})(json)) reply = merge(reply, text); if (event === 'SSE_REPLY_END') reply = merge(reply, json.msg_finish_attr?.brief || json.data?.msg_finish_attr?.brief || ''); } catch {} };
669
+ const decoder = new TextDecoder(); let buffer = '', reply = '', events = [], expectVideo = false, conversationId = ${JSON.stringify(JSON.parse(body).client_meta.conversation_id)};
670
+ const blocks = new Map(), questionIds = new Set(), messageIds = new Set();
671
+ const collect = (value) => {
672
+ if (!value || typeof value !== 'object') return;
673
+ if (value.ext?.has_video_gen === '1' || value.ext?.has_video_gen === 1) expectVideo = true;
674
+ if ([2074, 10020].includes(Number(value.block_type))) {
675
+ const key = value.block_id || value.id || value.block_type + ':' + JSON.stringify(value.content || value.content_obj);
676
+ const previous = blocks.get(key) || {};
677
+ blocks.set(key, { ...previous, ...value, ...(value.content ? { content: { ...previous.content, ...value.content } } : {}) });
678
+ }
679
+ for (const child of Object.values(value)) collect(child);
680
+ };
681
+ const consume = (block) => {
682
+ const event = (block.match(/^event:\s*(.+)$/m) || [])[1] || '', data = (block.match(/^data:\s*(.+)$/m) || [])[1] || '';
683
+ if (event) events.push(event);
684
+ if (event === 'STREAM_ERROR') throw new Error((${doubaoStreamErrorMessage.toString()})(data));
685
+ if (!data) return;
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;
688
+ const conversation = json.meta?.conversation_id || json.message?.conversation_id || json.ack_client_meta?.conversation_id;
689
+ if (conversation && conversation !== conversationId) return;
690
+ collect(json);
691
+ if (event === 'SSE_ACK' && json.ack_client_meta?.conversation_id === conversationId) {
692
+ // question_id 可能超过 JS 安全整数范围,必须从原始 JSON 文本提取。
693
+ for (const match of data.matchAll(/"question_id"\s*:\s*(?:"([^"\n]+)"|(\d+))/g)) questionIds.add(match[1] || match[2]);
694
+ }
695
+ const meta = json.meta || json.message;
696
+ if (meta?.conversation_id === conversationId && meta.user_type === 2 && meta.message_id) messageIds.add(String(meta.message_id));
697
+ if (event === 'STREAM_CHUNK') for (const patch of (json.patch_op || [])) reply = merge(reply, patch.patch_value?.tts_content || '');
698
+ if (event === 'CHUNK_DELTA' && typeof json.text === 'string') reply = merge(reply, json.text);
699
+ if (event === 'STREAM_MSG_NOTIFY' || event === 'FULL_MSG_NOTIFY') for (const text of (${exports.responseTextScript})(json)) reply = merge(reply, text);
700
+ if (event === 'SSE_REPLY_END') reply = merge(reply, json.msg_finish_attr?.brief || json.data?.msg_finish_attr?.brief || '');
701
+ };
222
702
  for (;;) { const item = await reader.read(); if (item.done) break; buffer += decoder.decode(item.value, { stream: true }).replace(/\r\n/g, '\n'); let index; while ((index = buffer.indexOf('\n\n')) >= 0) { consume(buffer.slice(0, index)); buffer = buffer.slice(index + 2); } }
223
703
  if (buffer.trim()) consume(buffer);
224
- window.__mab_doubao_result = JSON.stringify({ ok: Boolean(reply.trim() || images.size), reply, images: [...images], reason: reply.trim() || images.size ? '' : 'SSE 未提取到文本或图片; events=' + events.slice(0, 20).join(',') });
225
- } catch (error) { window.__mab_doubao_result = JSON.stringify({ ok: false, reason: String(error) }); }
704
+ const media = (${exports.responseArtifactsScript})([...blocks.values()]);
705
+ const ok = Boolean(reply.trim() || media.artifacts.length || media.pending || expectVideo);
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(',') });
707
+ } catch (error) { if (window.__mab_doubao_controller === controller) window.__mab_doubao_result = JSON.stringify({ ok: false, reason: String(error) }); }
226
708
  })(); return 'started';
227
709
  })()`;
228
710
  }