agent-bridge-doubao 0.0.0-stage → 1.0.0
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 -2
- package/config.schema.json +12 -0
- package/dist/src-ts/agent.js +10 -0
- package/dist/src-ts/plugins/doubao/cdp-client.js +93 -0
- package/dist/src-ts/plugins/doubao/index.js +83 -0
- package/package.json +21 -4
- package/plugin.json +10 -0
package/README.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 豆包插件
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
供 `mu-agent-bridge` 使用的豆包桌面插件。安装:`npm install -g agent-bridge-doubao`。运行前需要豆包桌面端开启 CDP 调试端口。
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "object",
|
|
3
|
+
"properties": {
|
|
4
|
+
"aid": { "type": "string", "description": "复用的 EvolCore 已有 AID" },
|
|
5
|
+
"cdpPort": { "type": "integer", "minimum": 1, "description": "豆包桌面端 CDP 调试端口,默认 9227" },
|
|
6
|
+
"conversationId": { "type": "string", "description": "豆包会话 ID" },
|
|
7
|
+
"botId": { "type": "string", "description": "豆包 bot ID" },
|
|
8
|
+
"timeoutMs": { "type": "integer", "minimum": 1000, "description": "等待回复超时时间(毫秒)" },
|
|
9
|
+
"token": { "type": "string", "secret": true }
|
|
10
|
+
},
|
|
11
|
+
"additionalProperties": false
|
|
12
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AgentError = void 0;
|
|
4
|
+
class AgentError extends Error {
|
|
5
|
+
constructor(message, options) {
|
|
6
|
+
super(message, options);
|
|
7
|
+
this.name = "AgentError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
exports.AgentError = AgentError;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DoubaoCdpClient = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const defaults = { port: Number(process.env.DOUBAO_CDP_PORT ?? "9227"), conversationId: process.env.DOUBAO_CONVERSATION_ID ?? "759450172544514", botId: process.env.DOUBAO_BOT_ID ?? "7338286299411103781", timeoutMs: Number(process.env.DOUBAO_TIMEOUT_MS ?? "180000") };
|
|
6
|
+
function sleep(ms, signal) {
|
|
7
|
+
if (signal?.aborted)
|
|
8
|
+
return Promise.reject(new Error("请求已取消"));
|
|
9
|
+
return new Promise((resolve, reject) => { const timer = setTimeout(resolve, ms); signal?.addEventListener("abort", () => { clearTimeout(timer); reject(new Error("请求已取消")); }, { once: true }); });
|
|
10
|
+
}
|
|
11
|
+
function requestBody(text, conversationId, botId) {
|
|
12
|
+
const general = { runtime_type: 0, agent_task_param: {}, agent_task_param_change: { runtime_changed: false, device_changed: false, sandbox_auth_type_changed: false }, project_id: "", need_modify_conversation: false };
|
|
13
|
+
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: [] };
|
|
14
|
+
return JSON.stringify({ client_meta: { conversation_id: conversationId, bot_id: botId, last_section_id: "", last_message_index: null, local_permissions: [{ permission_name: "ACCESS_COARSE_LOCATION", status: 1 }, { permission_name: "ACCESS_FINE_LOCATION", status: 1 }, { permission_name: "ACCESS_BACKGROUND_LOCATION", status: 1 }] }, messages: [{ local_message_id: (0, node_crypto_1.randomUUID)(), content_block: [block], message_status: 0 }], option: { send_message_scene: "", create_time_ms: Date.now(), collect_id: "", is_audio: false, answer_with_suggest: false, agent_mode: 2, tts_switch: false, need_deep_think: 0, click_clear_context: false, is_regen: false, is_replace: false, is_from_click_option: false, is_from_click_softlink: false, disable_sse_cache: false, scene_type: 0, unique_key: (0, node_crypto_1.randomUUID)(), start_seq: 0, need_create_conversation: false, regen_query_id: [], edit_query_id: [], message_from: 0, sse_recv_event_options: { support_chunk_delta: true }, support_lazy_fetch_stream: true, is_ai_playground: false, is_old_user: true, general_task_param: general, recovery_option: { is_recovery: false, req_create_time_sec: Math.floor(Date.now() / 1000), append_sse_event_scene: 0 }, message_storage_type: 0, related_deleted_message_ids: {}, connector_info_list: [], model_config: { model_item_key: "0", model_extra_params: {}, reasoning_effort: 5 }, aggregate_params: { mention_skill_list: "[]", mention_plugin_list: "[]", mention_ext: "[{}]", conversation_mode: "", mode_id: "1", model_item_key: "0", agent_mode: "2", reasoning_effort: "5", provider_id: "" } }, user_context: [], ext: { agent_mode: "2", use_deep_think: "0", general_task_param: JSON.stringify(general), collection_id: "", is_finish: "1", commerce_credit_config_enable: "0" } });
|
|
15
|
+
}
|
|
16
|
+
class DoubaoCdpClient {
|
|
17
|
+
socket;
|
|
18
|
+
sequence = 0;
|
|
19
|
+
pending = new Map();
|
|
20
|
+
options;
|
|
21
|
+
constructor(options = {}) { this.options = { ...defaults, ...options, fetchImpl: options.fetchImpl }; }
|
|
22
|
+
connected() { return Boolean(this.socket); }
|
|
23
|
+
async connect() {
|
|
24
|
+
if (this.socket)
|
|
25
|
+
return;
|
|
26
|
+
const response = await (this.options.fetchImpl ?? fetch)(`http://127.0.0.1:${this.options.port}/json`);
|
|
27
|
+
if (!response.ok)
|
|
28
|
+
throw new Error(`豆包 CDP /json HTTP ${response.status}`);
|
|
29
|
+
const targets = await response.json();
|
|
30
|
+
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/"));
|
|
31
|
+
if (!page?.webSocketDebuggerUrl)
|
|
32
|
+
throw new Error(`未找到豆包 chat 页面(CDP ${this.options.port})`);
|
|
33
|
+
const socket = new WebSocket(page.webSocketDebuggerUrl);
|
|
34
|
+
this.socket = socket;
|
|
35
|
+
socket.onmessage = event => this.onMessage(String(event.data));
|
|
36
|
+
await new Promise((resolve, reject) => { socket.onopen = () => resolve(); socket.onerror = () => reject(new Error("豆包 CDP WebSocket 连接失败")); });
|
|
37
|
+
}
|
|
38
|
+
close() { this.socket?.close(); this.socket = undefined; for (const pending of this.pending.values())
|
|
39
|
+
pending.reject(new Error("豆包 CDP 已关闭")); this.pending.clear(); }
|
|
40
|
+
async direct(text, signal) {
|
|
41
|
+
await this.evaluate(buildInjection(requestBody(text, this.options.conversationId, this.options.botId)));
|
|
42
|
+
const deadline = Date.now() + this.options.timeoutMs;
|
|
43
|
+
while (Date.now() < deadline) {
|
|
44
|
+
await sleep(1000, signal);
|
|
45
|
+
const result = await this.evaluate("window.__mab_doubao_result");
|
|
46
|
+
if (typeof result !== "string")
|
|
47
|
+
continue;
|
|
48
|
+
const parsed = JSON.parse(result);
|
|
49
|
+
if (!parsed.ok)
|
|
50
|
+
throw new Error(`豆包请求失败: ${parsed.reason ?? "未知错误"}`);
|
|
51
|
+
if (parsed.reply?.trim())
|
|
52
|
+
return parsed.reply.trim();
|
|
53
|
+
}
|
|
54
|
+
throw new Error("等待豆包回复超时");
|
|
55
|
+
}
|
|
56
|
+
onMessage(raw) { let message; try {
|
|
57
|
+
message = JSON.parse(raw);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return;
|
|
61
|
+
} if (message.id === undefined)
|
|
62
|
+
return; const pending = this.pending.get(message.id); if (!pending)
|
|
63
|
+
return; this.pending.delete(message.id); pending.resolve(message); }
|
|
64
|
+
async cdp(method, params = {}) { if (!this.socket)
|
|
65
|
+
throw new Error("豆包 CDP 尚未连接"); const id = ++this.sequence; const result = new Promise((resolve, reject) => this.pending.set(id, { resolve, reject })); this.socket.send(JSON.stringify({ id, method, params })); return Promise.race([result, sleep(this.options.timeoutMs).then(() => { throw new Error(`CDP ${method} 超时`); })]); }
|
|
66
|
+
async evaluate(expression) { const response = await this.cdp("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: false }); if (response.result?.exceptionDetails)
|
|
67
|
+
throw new Error(`豆包页面 JS 异常: ${JSON.stringify(response.result.exceptionDetails).slice(0, 300)}`); return response.result?.result?.value; }
|
|
68
|
+
}
|
|
69
|
+
exports.DoubaoCdpClient = DoubaoCdpClient;
|
|
70
|
+
function buildInjection(body) {
|
|
71
|
+
const url = "https://www.doubao.com/chat/completion?aid=582478&channel=win&chromium_version=147.0.7727.149&client_platform=pc_client&device_id=2750296938275162&device_platform=web&doubao_device_platform=desktop&doubao_pc_version=2.30.4&fp=verify_2750296938275162&language=zh&pc_version=2.30.4&pkg_type=release_version&real_aid=582478®ion=CN&runtime=web&runtime_version=3.37.7&samantha_web=1&sys_region=CN&tea_uuid=2750296938275162&tz_name=Asia%2FShanghai&use-olympus-account=1&version_code=20800&web_platform=desktop";
|
|
72
|
+
return String.raw `(() => {
|
|
73
|
+
window.__mab_doubao_result = null;
|
|
74
|
+
const merge = (a, b) => { if (!b || a.endsWith(b)) return a; const n = Math.min(a.length, b.length); let k = 0; for (let i = 1; i <= n; i++) if (a.endsWith(b.slice(0, i))) k = i; return a + (k >= 4 ? b.slice(k) : b); };
|
|
75
|
+
(async () => {
|
|
76
|
+
try {
|
|
77
|
+
let webId = '', tabId = '';
|
|
78
|
+
for (const entry of performance.getEntriesByType('resource')) { try { const u = new URL(entry.name); if (u.host === 'www.doubao.com') { webId ||= u.searchParams.get('web_id') || ''; tabId ||= u.searchParams.get('web_tab_id') || ''; } } catch {} if (webId && tabId) break; }
|
|
79
|
+
webId ||= '7689385646880933426'; tabId ||= '98f6fc8e-a1f7-4585-aa08-89b9a10aa451';
|
|
80
|
+
let requestUrl = ${JSON.stringify(url)} + '&web_id=' + encodeURIComponent(webId) + '&web_tab_id=' + encodeURIComponent(tabId);
|
|
81
|
+
for (const entry of performance.getEntriesByType('resource')) if (entry.name.includes('/chat/completion?')) requestUrl = entry.name;
|
|
82
|
+
const response = await fetch(requestUrl, { method: 'POST', headers: { 'content-type': 'application/json' }, credentials: 'include', body: ${JSON.stringify(body)} });
|
|
83
|
+
if (!response.ok) throw new Error('HTTP ' + response.status);
|
|
84
|
+
const reader = response.body?.getReader(); if (!reader) throw new Error('响应无 body');
|
|
85
|
+
const decoder = new TextDecoder(); let buffer = '', reply = '', events = [];
|
|
86
|
+
const consume = (block) => { const event = (block.match(/^event:\s*(.+)$/m) || [])[1] || ''; const data = (block.match(/^data:\s*(.+)$/m) || [])[1] || ''; if (event) events.push(event); if (event === 'STREAM_ERROR') throw new Error('STREAM_ERROR ' + data.slice(0, 500)); if (!data) return; try { const json = JSON.parse(data); if (event === 'STREAM_CHUNK') for (const patch of (json.patch_op || [])) reply = merge(reply, patch.patch_value?.tts_content || ''); if (event === 'CHUNK_DELTA' && typeof json.text === 'string') reply = merge(reply, json.text); if (event === 'STREAM_MSG_NOTIFY') for (const item of (json.content?.content_block || json.data?.content?.content_block || [])) reply = merge(reply, item.content?.text_block?.text || ''); if (event === 'SSE_REPLY_END') reply = merge(reply, json.msg_finish_attr?.brief || json.data?.msg_finish_attr?.brief || ''); } catch {} };
|
|
87
|
+
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); } }
|
|
88
|
+
if (buffer.trim()) consume(buffer);
|
|
89
|
+
window.__mab_doubao_result = JSON.stringify({ ok: Boolean(reply.trim()), reply, reason: reply.trim() ? '' : 'SSE 未提取到文本; events=' + events.slice(0, 20).join(',') });
|
|
90
|
+
} catch (error) { window.__mab_doubao_result = JSON.stringify({ ok: false, reason: String(error) }); }
|
|
91
|
+
})(); return 'started';
|
|
92
|
+
})()`;
|
|
93
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DoubaoPlugin = exports.doubaoManifest = void 0;
|
|
4
|
+
const cdp_client_1 = require("./cdp-client");
|
|
5
|
+
exports.doubaoManifest = {
|
|
6
|
+
id: "doubao",
|
|
7
|
+
version: "1.0.0",
|
|
8
|
+
displayName: "豆包",
|
|
9
|
+
entry: "dist/index.js",
|
|
10
|
+
apiVersion: "1",
|
|
11
|
+
process: "in-process",
|
|
12
|
+
capabilities: ["chat", "health"],
|
|
13
|
+
configSchema: "config.schema.json",
|
|
14
|
+
};
|
|
15
|
+
class DoubaoPlugin {
|
|
16
|
+
injectedClient;
|
|
17
|
+
manifest = exports.doubaoManifest;
|
|
18
|
+
client;
|
|
19
|
+
state = "stopped";
|
|
20
|
+
message = "";
|
|
21
|
+
constructor(injectedClient) {
|
|
22
|
+
this.injectedClient = injectedClient;
|
|
23
|
+
}
|
|
24
|
+
disposer;
|
|
25
|
+
async activate(context) {
|
|
26
|
+
this.client = this.injectedClient ?? new cdp_client_1.DoubaoCdpClient({
|
|
27
|
+
port: typeof context.config.cdpPort === "number" ? context.config.cdpPort : undefined,
|
|
28
|
+
conversationId: typeof context.config.conversationId === "string" ? context.config.conversationId : undefined,
|
|
29
|
+
botId: typeof context.config.botId === "string" ? context.config.botId : undefined,
|
|
30
|
+
timeoutMs: typeof context.config.timeoutMs === "number" ? context.config.timeoutMs : undefined,
|
|
31
|
+
});
|
|
32
|
+
try {
|
|
33
|
+
await this.client.connect();
|
|
34
|
+
if (context.http)
|
|
35
|
+
this.disposer = context.http.register({ method: "POST", path: "/v1/chat/completions", handler: (request, response) => this.handleHttp(request, response) }) || undefined;
|
|
36
|
+
this.state = "ready";
|
|
37
|
+
this.message = "";
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
this.state = "failed";
|
|
41
|
+
this.message = error instanceof Error ? error.message : String(error);
|
|
42
|
+
context.logger?.warn?.(this.message);
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async deactivate() {
|
|
47
|
+
this.disposer?.();
|
|
48
|
+
this.disposer = undefined;
|
|
49
|
+
this.client?.close();
|
|
50
|
+
this.client = undefined;
|
|
51
|
+
this.state = "stopped";
|
|
52
|
+
}
|
|
53
|
+
async health() {
|
|
54
|
+
return this.state === "ready" && this.client?.connected()
|
|
55
|
+
? { status: "ready", details: { adapter: "doubao", cdp: true } }
|
|
56
|
+
: { status: "degraded", message: this.message || "豆包 CDP 未连接", details: { adapter: "doubao", cdp: false } };
|
|
57
|
+
}
|
|
58
|
+
async handle(request) {
|
|
59
|
+
if (this.state !== "ready" || !this.client)
|
|
60
|
+
throw new Error("豆包插件未就绪");
|
|
61
|
+
return { text: await this.client.direct(request.task) };
|
|
62
|
+
}
|
|
63
|
+
async handleHttp(request, response) {
|
|
64
|
+
let raw = "";
|
|
65
|
+
for await (const chunk of request)
|
|
66
|
+
raw += chunk;
|
|
67
|
+
const payload = JSON.parse(raw || "{}");
|
|
68
|
+
const messages = Array.isArray(payload.messages) ? payload.messages : [];
|
|
69
|
+
const last = messages.at(-1);
|
|
70
|
+
const task = typeof last?.content === "string" ? last.content : "";
|
|
71
|
+
if (!task.trim()) {
|
|
72
|
+
response.statusCode = 400;
|
|
73
|
+
response.end(JSON.stringify({ error: "缺少用户消息" }));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const result = await this.handle({ task });
|
|
77
|
+
response.statusCode = 200;
|
|
78
|
+
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
79
|
+
response.end(JSON.stringify({ id: `doubao-${Date.now()}`, object: "chat.completion", model: payload.model ?? "doubao", choices: [{ index: 0, message: { role: "assistant", content: result.text }, finish_reason: "stop" }] }));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
exports.DoubaoPlugin = DoubaoPlugin;
|
|
83
|
+
exports.default = DoubaoPlugin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-bridge-doubao",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "agent-bridge 的豆包插件。",
|
|
5
|
+
"repository": { "type": "git", "url": "https://codeup.aliyun.com/6808da56fb3da14b899afd54/agent_bridge.git", "directory": "packages/doubao" },
|
|
6
|
+
"main": "dist/src-ts/plugins/doubao/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/",
|
|
9
|
+
"plugin.json",
|
|
10
|
+
"config.schema.json",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"agentBridge": {
|
|
14
|
+
"plugin": "./plugin.json"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public",
|
|
18
|
+
"registry": "https://registry.npmjs.org"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=22"
|
|
22
|
+
}
|
|
23
|
+
}
|
package/plugin.json
ADDED