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