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.
- package/README.md +4 -2
- package/dist/src-ts/channel-sessions.js +90 -24
- package/dist/src-ts/control-server.js +48 -7
- package/dist/src-ts/dsh-agent.js +141 -46
- package/dist/src-ts/headless-account.patch.yml +8 -1
- package/dist/src-ts/plugin-manager.js +69 -3
- package/dist/src-ts/plugins/aun/attachments.js +44 -13
- package/dist/src-ts/plugins/aun/index.js +5 -6
- package/dist/src-ts/plugins/chatgpt/cdp-client.js +342 -9
- package/dist/src-ts/plugins/chatgpt/index.js +127 -7
- package/dist/src-ts/plugins/desktop-launch.js +19 -0
- package/dist/src-ts/plugins/doubao/attachments.js +42 -0
- package/dist/src-ts/plugins/doubao/cdp-client.js +530 -48
- package/dist/src-ts/plugins/doubao/index.js +114 -25
- package/dist/src-ts/plugins/doubao/session-history.js +123 -0
- package/dist/src-ts/plugins/dsh/index.js +18 -2
- package/dist/src-ts/plugins/dsh/session-history.js +371 -0
- package/dist/src-ts/plugins/qwenwork/cdp-client.js +271 -10
- package/dist/src-ts/plugins/qwenwork/index.js +42 -3
- package/dist/src-ts/plugins/qwenwork/session-history.js +245 -0
- package/dist/src-ts/plugins/workbuddy/cdp-client.js +42 -2
- package/dist/src-ts/plugins/workbuddy/index.js +23 -3
- package/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
- package/dist/src-ts/session-archive.js +509 -0
- package/dist/src-ts/session-history.js +2 -0
- package/dist/src-ts/session-management.js +665 -0
- package/dist/src-ts/session-summary.js +171 -0
- package/dist/tests-ts/aun-attachments.test.js +41 -3
- package/dist/tests-ts/aun-plugin.test.js +28 -5
- package/dist/tests-ts/bundled-package.test.js +3 -1
- package/dist/tests-ts/channel-sessions.test.js +255 -2
- package/dist/tests-ts/chatgpt-history.test.js +197 -0
- package/dist/tests-ts/chatgpt-plugin.test.js +279 -2
- package/dist/tests-ts/control-server.test.js +39 -0
- package/dist/tests-ts/desktop-launch.test.js +41 -0
- package/dist/tests-ts/doubao-history.test.js +172 -0
- package/dist/tests-ts/doubao-plugin.test.js +549 -2
- package/dist/tests-ts/dsh-agent.test.js +199 -0
- package/dist/tests-ts/dsh-history.test.js +165 -0
- package/dist/tests-ts/native-session-management.test.js +195 -0
- package/dist/tests-ts/plugin-manager.test.js +354 -4
- package/dist/tests-ts/qwenwork-history.test.js +339 -0
- package/dist/tests-ts/qwenwork-images.test.js +166 -0
- package/dist/tests-ts/qwenwork-plugin.test.js +40 -1
- package/dist/tests-ts/session-archive.test.js +422 -0
- package/dist/tests-ts/session-management.test.js +323 -0
- package/dist/tests-ts/session-summary.test.js +199 -0
- package/dist/tests-ts/workbuddy-history.test.js +256 -0
- package/dist/tests-ts/workbuddy-plugin.test.js +43 -0
- package/package.json +1 -1
- package/plugins/aun/dist/src-ts/plugins/aun/attachments.js +44 -13
- package/plugins/aun/dist/src-ts/plugins/aun/index.js +5 -6
- package/plugins/aun/package.json +1 -1
- package/plugins/aun/plugin.json +1 -1
- package/plugins/chatgpt/dist/src-ts/attachments.js +62 -0
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +342 -9
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +127 -7
- package/plugins/chatgpt/package.json +1 -1
- package/plugins/chatgpt/plugin.json +14 -1
- package/plugins/doubao/config.schema.json +3 -0
- package/plugins/doubao/dist/src-ts/attachments.js +62 -0
- package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +19 -0
- package/plugins/doubao/dist/src-ts/plugins/doubao/attachments.js +42 -0
- package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +530 -48
- package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +114 -25
- package/plugins/doubao/dist/src-ts/plugins/doubao/session-history.js +123 -0
- package/plugins/doubao/package.json +1 -1
- package/plugins/doubao/plugin.json +1 -1
- package/plugins/dsh/config.schema.json +2 -1
- package/plugins/dsh/dist/src-ts/attachments.js +62 -0
- package/plugins/dsh/dist/src-ts/dsh-agent.js +141 -46
- package/plugins/dsh/dist/src-ts/headless-account.patch.yml +8 -1
- package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +18 -2
- package/plugins/dsh/dist/src-ts/plugins/dsh/session-history.js +371 -0
- package/plugins/dsh/package.json +5 -2
- package/plugins/dsh/plugin.json +1 -1
- package/plugins/qwenwork/dist/src-ts/attachments.js +62 -0
- package/plugins/qwenwork/dist/src-ts/plugins/desktop-launch.js +19 -0
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +271 -10
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +42 -3
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/session-history.js +245 -0
- package/plugins/qwenwork/package.json +1 -1
- package/plugins/qwenwork/plugin.json +1 -1
- package/plugins/workbuddy/config.schema.json +1 -1
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +42 -2
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +23 -3
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
- package/plugins/workbuddy/package.json +1 -1
- 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 ?? "
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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(
|
|
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
|
-
|
|
85
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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) => {
|
|
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
|
|
163
|
-
await this.
|
|
164
|
-
const
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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 (
|
|
179
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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&
|
|
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®ion=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
|
-
|
|
645
|
+
const controller = new AbortController();
|
|
646
|
+
window.__mab_doubao_controller = controller;
|
|
209
647
|
const merge = ${mergeDoubaoReply.toString()};
|
|
210
648
|
(async () => {
|
|
211
649
|
try {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const
|
|
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 = []
|
|
221
|
-
const
|
|
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
|
-
|
|
225
|
-
|
|
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
|
}
|