mu-agent-bridge 1.0.5 → 1.0.13
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 +16 -11
- package/dist/src-ts/channel-sessions.js +68 -0
- package/dist/src-ts/cli.js +60 -25
- package/dist/src-ts/control-server.js +75 -94
- package/dist/src-ts/local-aids.js +26 -0
- package/dist/src-ts/log-store.js +64 -0
- package/dist/src-ts/plugin-manager.js +240 -39
- package/dist/src-ts/plugin-manifest.js +5 -1
- package/dist/src-ts/plugins/abweb/index.js +1 -1
- package/dist/src-ts/plugins/abweb/public/app.js +164 -100
- package/dist/src-ts/plugins/abweb/public/index.html +7 -6
- package/dist/src-ts/plugins/abweb/public/style.css +12 -13
- package/dist/src-ts/plugins/aun/evolcore.js +54 -0
- package/dist/src-ts/plugins/aun/index.js +253 -51
- package/dist/src-ts/plugins/aun/public/app.js +66 -0
- package/dist/src-ts/plugins/aun/public/index.html +14 -0
- package/dist/src-ts/plugins/aun/public/style.css +21 -0
- package/dist/src-ts/plugins/doubao/cdp-client.js +5 -4
- package/dist/src-ts/plugins/doubao/index.js +3 -2
- package/dist/src-ts/plugins/feishu/client.js +133 -0
- package/dist/src-ts/plugins/feishu/index.js +257 -0
- package/dist/src-ts/plugins/feishu/public/app.js +147 -0
- package/dist/src-ts/plugins/feishu/public/index.html +17 -0
- package/dist/src-ts/plugins/feishu/public/style.css +30 -0
- package/dist/src-ts/plugins/workbuddy/cdp-client.js +118 -0
- package/dist/src-ts/plugins/workbuddy/cli-client.js +39 -0
- package/dist/src-ts/plugins/workbuddy/index.js +139 -0
- package/dist/src-ts/version-manager.js +9 -1
- package/dist/tests-ts/abweb-plugin.test.js +13 -2
- package/dist/tests-ts/aun-plugin.test.js +108 -4
- package/dist/tests-ts/control-server.test.js +88 -6
- package/dist/tests-ts/doubao-plugin.test.js +18 -0
- package/dist/tests-ts/feishu-client.test.js +72 -0
- package/dist/tests-ts/feishu-page.test.js +53 -0
- package/dist/tests-ts/feishu-plugin.test.js +148 -0
- package/dist/tests-ts/log-store.test.js +27 -0
- package/dist/tests-ts/platform-integration.test.js +40 -1
- package/dist/tests-ts/plugin-catalog.test.js +8 -0
- package/dist/tests-ts/plugin-manager.test.js +219 -13
- package/dist/tests-ts/workbuddy-plugin.test.js +85 -0
- package/package.json +30 -6
- package/plugins/abweb/plugin.json +1 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WorkbuddyCdpClient = exports.assistantSelector = exports.editorSelector = void 0;
|
|
4
|
+
function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
|
|
5
|
+
exports.editorSelector = '[data-slate-editor][contenteditable="true"]';
|
|
6
|
+
exports.assistantSelector = '.cr-agent[data-message-id] [data-assistant-content]';
|
|
7
|
+
class WorkbuddyCdpClient {
|
|
8
|
+
socket;
|
|
9
|
+
sequence = 0;
|
|
10
|
+
pending = new Map();
|
|
11
|
+
port;
|
|
12
|
+
timeoutMs;
|
|
13
|
+
fetchImpl;
|
|
14
|
+
constructor(options = {}) { this.port = options.port ?? Number(process.env.WORKBUDDY_CDP_PORT ?? "9229"); this.timeoutMs = options.timeoutMs ?? 30_000; this.fetchImpl = options.fetchImpl ?? fetch; }
|
|
15
|
+
connected() { return Boolean(this.socket); }
|
|
16
|
+
async connect() {
|
|
17
|
+
if (this.socket)
|
|
18
|
+
return;
|
|
19
|
+
const response = await this.fetchImpl(`http://127.0.0.1:${this.port}/json`);
|
|
20
|
+
if (!response.ok)
|
|
21
|
+
throw new Error(`WorkBuddy CDP /json HTTP ${response.status}`);
|
|
22
|
+
const targets = await response.json();
|
|
23
|
+
const page = targets.find((target) => target.type === "page" && /workbuddy/i.test(`${target.title} ${target.url}`));
|
|
24
|
+
if (!page?.webSocketDebuggerUrl)
|
|
25
|
+
throw new Error(`未找到 WorkBuddy 页面 target(端口 ${this.port})`);
|
|
26
|
+
this.socket = new WebSocket(page.webSocketDebuggerUrl);
|
|
27
|
+
this.socket.onmessage = (event) => this.onMessage(String(event.data));
|
|
28
|
+
try {
|
|
29
|
+
await new Promise((resolve, reject) => { const socket = this.socket; socket.onopen = () => resolve(); socket.onerror = () => reject(new Error("WorkBuddy CDP WebSocket 连接失败")); });
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
this.close();
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
close() { this.socket?.close(); this.socket = undefined; for (const pending of this.pending.values())
|
|
37
|
+
pending.reject(new Error("WorkBuddy CDP 已关闭")); this.pending.clear(); }
|
|
38
|
+
async direct(text, timeoutMs = 240_000, expert, newTask = false) {
|
|
39
|
+
if (expert)
|
|
40
|
+
await this.selectExpert(expert);
|
|
41
|
+
else if (newTask) {
|
|
42
|
+
if (!await this.evaluate(`(() => { const button = [...document.querySelectorAll('button,[role="button"]')].find((item) => item.textContent?.trim() === '新建任务'); button?.click(); return Boolean(button); })()`))
|
|
43
|
+
throw new Error("WorkBuddy 新建任务入口未找到");
|
|
44
|
+
}
|
|
45
|
+
await this.waitFor(`Boolean(document.querySelector(${JSON.stringify(exports.editorSelector)}))`, "WorkBuddy 输入框未就绪");
|
|
46
|
+
const before = JSON.parse(String(await this.evaluate(`JSON.stringify([...document.querySelectorAll(${JSON.stringify(exports.assistantSelector)})].map(element => ({ id: element.closest('.cr-agent')?.getAttribute('data-message-id'), text: element.innerText })))`)));
|
|
47
|
+
const beforeIds = new Set(before.map((item) => item.id).filter((id) => Boolean(id)));
|
|
48
|
+
if (!await this.evaluate(`(() => { const element = document.querySelector(${JSON.stringify(exports.editorSelector)}); if (!element) return false; element.focus(); const selection = window.getSelection(); const range = document.createRange(); range.selectNodeContents(element); selection?.removeAllRanges(); selection?.addRange(range); return true; })()`))
|
|
49
|
+
throw new Error("WorkBuddy 输入框未找到");
|
|
50
|
+
await this.cdp("Input.dispatchKeyEvent", { type: "keyDown", key: "Backspace", code: "Backspace" });
|
|
51
|
+
await this.cdp("Input.dispatchKeyEvent", { type: "keyUp", key: "Backspace", code: "Backspace" });
|
|
52
|
+
await this.cdp("Input.insertText", { text });
|
|
53
|
+
if (!await this.evaluate(`document.querySelector(${JSON.stringify(exports.editorSelector)})?.innerText.trim() === ${JSON.stringify(text.trim())}`))
|
|
54
|
+
throw new Error("WorkBuddy 输入内容校验失败,未发送消息");
|
|
55
|
+
if (!await this.evaluate(`(() => { const button = document.querySelector('button[aria-label="发送"]'); if (!button || button.disabled) return false; button.click(); return true; })()`))
|
|
56
|
+
throw new Error("WorkBuddy 发送按钮不可用");
|
|
57
|
+
const deadline = Date.now() + timeoutMs;
|
|
58
|
+
while (Date.now() < deadline) {
|
|
59
|
+
await sleep(1_000);
|
|
60
|
+
const current = JSON.parse(String(await this.evaluate(`JSON.stringify([...document.querySelectorAll('.cr-agent[data-message-id]')].map(element => ({ id: element.getAttribute('data-message-id'), text: element.querySelector('[data-assistant-content]')?.innerText, finished: Boolean(element.querySelector('[data-testid="conversation-finished-footer"]')) })))`)));
|
|
61
|
+
const reply = current.filter((item) => item.id && !beforeIds.has(item.id) && item.text?.trim() && item.finished).at(-1)?.text?.trim();
|
|
62
|
+
if (reply)
|
|
63
|
+
return reply;
|
|
64
|
+
}
|
|
65
|
+
throw new Error("WorkBuddy UI 回复超时");
|
|
66
|
+
}
|
|
67
|
+
async selectExpert(name) {
|
|
68
|
+
if (!await this.evaluate(`(() => { const element = [...document.querySelectorAll('button,[role="button"],a')].find((item) => item.textContent?.trim() === '专家·技能·连接器'); if (!element) return false; element.click(); return true; })()`))
|
|
69
|
+
throw new Error("WorkBuddy 专家入口未找到");
|
|
70
|
+
await this.waitFor(`Boolean(document.querySelector('input[placeholder="搜索专家"]'))`, "WorkBuddy 专家搜索框未就绪");
|
|
71
|
+
if (!await this.evaluate(`(() => { const element = document.querySelector('input[placeholder="搜索专家"]'); if (!element) return false; element.focus(); return true; })()`))
|
|
72
|
+
throw new Error("WorkBuddy 专家搜索框未找到");
|
|
73
|
+
for (const query of [...new Set([name, name.replace(/专家(?:团)?$/, "")])]) {
|
|
74
|
+
if (!await this.evaluate(`(() => { const element = document.querySelector('input[placeholder="搜索专家"]'); if (!element) return false; const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set; setter?.call(element, ''); element.dispatchEvent(new Event('input', { bubbles: true })); element.focus(); return true; })()`))
|
|
75
|
+
throw new Error("WorkBuddy 专家搜索框未找到");
|
|
76
|
+
await this.cdp("Input.insertText", { text: query });
|
|
77
|
+
await this.waitFor(`(() => { const input = document.querySelector('input[placeholder="搜索专家"]'); const cards = document.querySelectorAll('article[role="button"] button[data-track-id="expert_summon_click"]'); return input?.value === ${JSON.stringify(query)} && cards.length > 0; })()`, "WorkBuddy 专家搜索超时");
|
|
78
|
+
const result = await this.evaluate(`(() => {
|
|
79
|
+
const cards = [...document.querySelectorAll('article[role="button"]')].filter(card => card.querySelector('button[data-track-id="expert_summon_click"]'));
|
|
80
|
+
const title = card => card.querySelector('[class*="_role_"]')?.textContent?.trim() || card.querySelector('img')?.alt || card.innerText.split('\\n').filter(Boolean)[1];
|
|
81
|
+
const exact = cards.filter(card => title(card) === ${JSON.stringify(name)});
|
|
82
|
+
const alias = cards.filter(card => card.querySelector('img')?.alt === ${JSON.stringify(name)} || card.querySelector('button[data-track-id="expert_summon_click"]')?.getAttribute('aria-label') === ${JSON.stringify(`召唤${name}`)});
|
|
83
|
+
const related = cards.filter(card => title(card)?.includes(${JSON.stringify(name)}) || ${JSON.stringify(name)}.includes(title(card) || ''));
|
|
84
|
+
const matches = exact.length ? exact : related.length === 1 ? related : alias.length === 1 ? alias : related;
|
|
85
|
+
if (matches.length !== 1) return { names: matches.map(title) };
|
|
86
|
+
const button = matches[0].querySelector('button[data-track-id="expert_summon_click"]');
|
|
87
|
+
button.click(); return { selected: true };
|
|
88
|
+
})()`);
|
|
89
|
+
if (result.selected)
|
|
90
|
+
return;
|
|
91
|
+
if (result.names?.length)
|
|
92
|
+
throw new Error(`WorkBuddy 专家匹配不唯一,请指定完整名称:${result.names.join("、")}`);
|
|
93
|
+
}
|
|
94
|
+
throw new Error(`未找到 WorkBuddy 专家:${name}`);
|
|
95
|
+
}
|
|
96
|
+
async waitFor(expression, message) {
|
|
97
|
+
const deadline = Date.now() + this.timeoutMs;
|
|
98
|
+
while (Date.now() < deadline) {
|
|
99
|
+
if (await this.evaluate(expression))
|
|
100
|
+
return;
|
|
101
|
+
await sleep(200);
|
|
102
|
+
}
|
|
103
|
+
throw new Error(message);
|
|
104
|
+
}
|
|
105
|
+
onMessage(raw) { let message; try {
|
|
106
|
+
message = JSON.parse(raw);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return;
|
|
110
|
+
} if (message.id === undefined)
|
|
111
|
+
return; const pending = this.pending.get(message.id); if (!pending)
|
|
112
|
+
return; this.pending.delete(message.id); pending.resolve(message); }
|
|
113
|
+
async cdp(method, params = {}) { if (!this.socket)
|
|
114
|
+
throw new Error("WorkBuddy 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.timeoutMs).then(() => { throw new Error(`CDP ${method} 超时`); })]); }
|
|
115
|
+
async evaluate(expression) { const response = await this.cdp("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true }); if (response.result?.exceptionDetails)
|
|
116
|
+
throw new Error(`WorkBuddy 页面 JS 异常:${JSON.stringify(response.result.exceptionDetails).slice(0, 300)}`); return response.result?.result?.value; }
|
|
117
|
+
}
|
|
118
|
+
exports.WorkbuddyCdpClient = WorkbuddyCdpClient;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findCli = findCli;
|
|
4
|
+
exports.callCli = callCli;
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
const node_fs_1 = require("node:fs");
|
|
7
|
+
const node_os_1 = require("node:os");
|
|
8
|
+
const node_path_1 = require("node:path");
|
|
9
|
+
const node_util_1 = require("node:util");
|
|
10
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
11
|
+
function findCli(configured) {
|
|
12
|
+
if (configured)
|
|
13
|
+
return (0, node_fs_1.existsSync)((0, node_path_1.resolve)(configured)) ? (0, node_path_1.resolve)(configured) : undefined;
|
|
14
|
+
const roots = [process.env.CODEBUDDY_NPM_DIR, (0, node_path_1.join)(process.env.APPDATA ?? (0, node_path_1.join)((0, node_os_1.homedir)(), "AppData", "Roaming"), "npm", "node_modules", "@tencent-ai", "codebuddy-code"), (0, node_path_1.join)((0, node_os_1.homedir)(), ".npm", "node_modules", "@tencent-ai", "codebuddy-code")];
|
|
15
|
+
return roots.filter((root) => Boolean(root)).map((root) => (0, node_path_1.join)(root, "bin", "codebuddy")).find(node_fs_1.existsSync);
|
|
16
|
+
}
|
|
17
|
+
async function callCli(cliPath, text, sessionId, timeoutMs, model) {
|
|
18
|
+
const id = (sessionId.replace(/[^A-Za-z0-9_-]/g, "-").replace(/^-+|-+$/g, "") || "wb").slice(0, 80);
|
|
19
|
+
const args = [cliPath, "-p", text, "--session-id", id, "--output-format", "text"];
|
|
20
|
+
if (model)
|
|
21
|
+
args.push("--model", model);
|
|
22
|
+
let stdout;
|
|
23
|
+
let stderr;
|
|
24
|
+
try {
|
|
25
|
+
({ stdout, stderr } = await execFileAsync(process.execPath, args, { cwd: (0, node_path_1.dirname)((0, node_path_1.dirname)(cliPath)), timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024, encoding: "utf8", windowsHide: true }));
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
const failure = error;
|
|
29
|
+
if (failure.killed)
|
|
30
|
+
throw new Error("CodeBuddy 回复超时");
|
|
31
|
+
throw new Error(`CodeBuddy 调用失败:${failure.stderr?.trim().slice(-300) || failure.message}`);
|
|
32
|
+
}
|
|
33
|
+
if (/Authentication required\. Please use \/login command to sign in to your account/.test(`${stdout}\n${stderr}`))
|
|
34
|
+
throw new Error("CodeBuddy CLI 尚未登录,请先执行 /login");
|
|
35
|
+
const reply = stdout.split(/\r?\n/).filter((line) => !(line.startsWith("[") && line.slice(0, 40).includes("]["))).join("\n").trim();
|
|
36
|
+
if (!reply)
|
|
37
|
+
throw new Error("CodeBuddy 无输出");
|
|
38
|
+
return reply;
|
|
39
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WorkbuddyPlugin = exports.workbuddyManifest = void 0;
|
|
4
|
+
exports.extractExpertQuery = extractExpertQuery;
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
const node_fs_1 = require("node:fs");
|
|
7
|
+
const node_os_1 = require("node:os");
|
|
8
|
+
const node_path_1 = require("node:path");
|
|
9
|
+
const cdp_client_1 = require("./cdp-client");
|
|
10
|
+
const cli_client_1 = require("./cli-client");
|
|
11
|
+
exports.workbuddyManifest = {
|
|
12
|
+
id: "workbuddy", version: "1.1.0", displayName: "WorkBuddy", type: "agent",
|
|
13
|
+
entry: "dist/index.js", apiVersion: "1", process: "in-process",
|
|
14
|
+
capabilities: ["chat", "health"], configSchema: "config.schema.json",
|
|
15
|
+
};
|
|
16
|
+
function extractExpertQuery(task) {
|
|
17
|
+
const text = task.normalize("NFKC");
|
|
18
|
+
const match = text.match(/^(?:请)?(?:只要|调用|使用|帮我找|找|用|选择|切换到)\s*[“「"]?([^,。!?,!?\n“「"]{2,30}?专家(?:团)?)(?!技能)/)
|
|
19
|
+
?? text.match(/^([A-Za-z\u4e00-\u9fff]{2,24}?专家(?:团)?)(?=[,。!,!::\s]|$)/);
|
|
20
|
+
return match?.[1]?.trim() || undefined;
|
|
21
|
+
}
|
|
22
|
+
function findExecutable(configured) {
|
|
23
|
+
if (configured)
|
|
24
|
+
return (0, node_fs_1.existsSync)((0, node_path_1.resolve)(configured)) ? (0, node_path_1.resolve)(configured) : undefined;
|
|
25
|
+
const local = process.env.LOCALAPPDATA ?? (0, node_path_1.join)((0, node_os_1.homedir)(), "AppData", "Local");
|
|
26
|
+
return [(0, node_path_1.join)(local, "Programs", "WorkBuddy", "WorkBuddy.exe"), "D:\\Programs\\WorkBuddy\\WorkBuddy.exe"].find(node_fs_1.existsSync);
|
|
27
|
+
}
|
|
28
|
+
class WorkbuddyPlugin {
|
|
29
|
+
options;
|
|
30
|
+
manifest = exports.workbuddyManifest;
|
|
31
|
+
client;
|
|
32
|
+
process;
|
|
33
|
+
cliPath;
|
|
34
|
+
model;
|
|
35
|
+
transport = "auto";
|
|
36
|
+
timeoutMs = 240_000;
|
|
37
|
+
queue = Promise.resolve();
|
|
38
|
+
failure;
|
|
39
|
+
expertSession = false;
|
|
40
|
+
constructor(options = {}) {
|
|
41
|
+
this.options = options;
|
|
42
|
+
}
|
|
43
|
+
async activate(context) {
|
|
44
|
+
const config = context.config;
|
|
45
|
+
if (config.transport !== undefined && !["auto", "cli", "cdp"].includes(String(config.transport)))
|
|
46
|
+
throw new Error("transport 只能是 auto、cli 或 cdp");
|
|
47
|
+
this.transport = config.transport ?? "auto";
|
|
48
|
+
this.timeoutMs = typeof config.timeoutMs === "number" && config.timeoutMs > 0 ? config.timeoutMs : 240_000;
|
|
49
|
+
this.model = typeof config.model === "string" && config.model.trim() ? config.model : undefined;
|
|
50
|
+
this.cliPath = (0, cli_client_1.findCli)(typeof config.cliPath === "string" ? config.cliPath : undefined);
|
|
51
|
+
this.failure = undefined;
|
|
52
|
+
if (this.transport === "cli") {
|
|
53
|
+
if (!this.cliPath)
|
|
54
|
+
throw new Error("未找到 CodeBuddy CLI,请配置 cliPath");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const port = typeof config.cdpPort === "number" ? config.cdpPort : this.options.cdpPort ?? 9229;
|
|
58
|
+
this.client = this.options.client ?? new cdp_client_1.WorkbuddyCdpClient({ port });
|
|
59
|
+
try {
|
|
60
|
+
await this.connectOrStart(config, context.logger, port);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
this.failure = error instanceof Error ? error.message : String(error);
|
|
64
|
+
this.client.close();
|
|
65
|
+
this.client = undefined;
|
|
66
|
+
if (this.transport === "cdp" || !this.cliPath)
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async deactivate(_reason) {
|
|
71
|
+
await this.queue;
|
|
72
|
+
this.client?.close();
|
|
73
|
+
this.client = undefined;
|
|
74
|
+
// WorkBuddy 是用户桌面应用,插件停止时不结束桌面进程。
|
|
75
|
+
this.process = undefined;
|
|
76
|
+
}
|
|
77
|
+
async health() {
|
|
78
|
+
if (this.client?.connected())
|
|
79
|
+
return { status: "ready", details: { transport: "cdp" } };
|
|
80
|
+
if (this.cliPath && (0, node_fs_1.existsSync)(this.cliPath))
|
|
81
|
+
return { status: "ready", details: { transport: "cli" } };
|
|
82
|
+
return { status: "failed", message: this.failure ?? "WorkBuddy 未连接", details: { transport: this.transport } };
|
|
83
|
+
}
|
|
84
|
+
async handle(request) {
|
|
85
|
+
if (!request.task.trim())
|
|
86
|
+
throw new Error("消息内容不能为空");
|
|
87
|
+
const run = this.queue.then(() => this.complete(request), () => this.complete(request));
|
|
88
|
+
this.queue = run.then(() => undefined, () => undefined);
|
|
89
|
+
return { text: await run, sessionId: request.sessionId };
|
|
90
|
+
}
|
|
91
|
+
async complete(request) {
|
|
92
|
+
const timeout = request.timeoutMs ?? this.timeoutMs;
|
|
93
|
+
const expert = extractExpertQuery(request.task);
|
|
94
|
+
if (expert) {
|
|
95
|
+
if (!this.client?.connected())
|
|
96
|
+
throw new Error(`专家请求需要已连接的 WorkBuddy 桌面端:${expert}`);
|
|
97
|
+
this.expertSession = true;
|
|
98
|
+
return this.client.direct(request.task, timeout, expert);
|
|
99
|
+
}
|
|
100
|
+
if (this.cliPath && this.transport !== "cdp")
|
|
101
|
+
return (0, cli_client_1.callCli)(this.cliPath, request.task, request.sessionId ?? "local-test", timeout, this.model);
|
|
102
|
+
if (!this.client?.connected())
|
|
103
|
+
throw new Error("WorkBuddy CDP 未连接");
|
|
104
|
+
const reset = this.expertSession;
|
|
105
|
+
this.expertSession = false;
|
|
106
|
+
return this.client.direct(request.task, timeout, undefined, reset);
|
|
107
|
+
}
|
|
108
|
+
async connectOrStart(config, logger, port) {
|
|
109
|
+
try {
|
|
110
|
+
await this.client.connect();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
catch (first) {
|
|
114
|
+
if (config.autoStart === false || this.options.client)
|
|
115
|
+
throw first;
|
|
116
|
+
const executable = findExecutable(typeof config.executablePath === "string" ? config.executablePath : this.options.executablePath);
|
|
117
|
+
if (!executable)
|
|
118
|
+
throw new Error("未找到 WorkBuddy.exe,请配置 executablePath");
|
|
119
|
+
this.process = (0, node_child_process_1.spawn)(executable, [`--remote-debugging-port=${port}`], { detached: true, stdio: "ignore", windowsHide: false });
|
|
120
|
+
this.process.unref();
|
|
121
|
+
logger.info(`WorkBuddy 已启动,等待 CDP ${port}`);
|
|
122
|
+
const deadline = Date.now() + (typeof config.startupTimeoutMs === "number" ? config.startupTimeoutMs : 30_000);
|
|
123
|
+
let last = first;
|
|
124
|
+
while (Date.now() < deadline) {
|
|
125
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
126
|
+
try {
|
|
127
|
+
await this.client.connect();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
last = error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
throw last instanceof Error ? last : new Error("WorkBuddy CDP 启动超时");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
exports.WorkbuddyPlugin = WorkbuddyPlugin;
|
|
139
|
+
exports.default = WorkbuddyPlugin;
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.VersionManager = void 0;
|
|
4
7
|
exports.compareVersions = compareVersions;
|
|
8
|
+
exports.satisfiesVersion = satisfiesVersion;
|
|
5
9
|
const promises_1 = require("node:fs/promises");
|
|
6
10
|
const node_path_1 = require("node:path");
|
|
7
11
|
const node_child_process_1 = require("node:child_process");
|
|
8
12
|
const node_util_1 = require("node:util");
|
|
13
|
+
const semver_1 = __importDefault(require("semver"));
|
|
9
14
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
10
15
|
function compareVersions(a, b) {
|
|
11
16
|
const pa = parseVersion(a), pb = parseVersion(b);
|
|
@@ -22,6 +27,9 @@ function compareVersions(a, b) {
|
|
|
22
27
|
return -1;
|
|
23
28
|
return pa.pre.localeCompare(pb.pre, "en", { numeric: true });
|
|
24
29
|
}
|
|
30
|
+
function satisfiesVersion(version, constraint) {
|
|
31
|
+
return Boolean(semver_1.default.valid(version) && semver_1.default.validRange(constraint) && semver_1.default.satisfies(version, constraint));
|
|
32
|
+
}
|
|
25
33
|
function parseVersion(value) {
|
|
26
34
|
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(value);
|
|
27
35
|
return match ? { n: [Number(match[1]), Number(match[2]), Number(match[3])], pre: match[4] } : undefined;
|
|
@@ -68,6 +76,6 @@ class VersionManager {
|
|
|
68
76
|
this.saveTask = this.saveTask.catch(() => undefined).then(async () => { await (0, promises_1.mkdir)(this.options.stateDir, { recursive: true }); await (0, promises_1.writeFile)((0, node_path_1.join)(this.options.stateDir, "versions.json"), JSON.stringify(this.list(), null, 2)); });
|
|
69
77
|
await this.saveTask;
|
|
70
78
|
}
|
|
71
|
-
async queryNpm(args) { return (await execFileAsync(process.platform === "win32" ?
|
|
79
|
+
async queryNpm(args) { return (await execFileAsync(process.platform === "win32" ? "npm.cmd" : "npm", args, { timeout: 30_000, maxBuffer: 1024 * 1024, ...(process.platform === "win32" ? { shell: true } : {}) })).stdout; }
|
|
72
80
|
}
|
|
73
81
|
exports.VersionManager = VersionManager;
|
|
@@ -50,8 +50,15 @@ function createContext(config = {}) {
|
|
|
50
50
|
strict_1.default.match(String(response.body), /通信记录/);
|
|
51
51
|
strict_1.default.match(String(response.body), /<th scope="col">来源 Peer<\/th>/);
|
|
52
52
|
strict_1.default.match(String(response.body), /id="catalog" class="catalog" aria-live="polite"/);
|
|
53
|
-
strict_1.default.match(String(response.body),
|
|
54
|
-
strict_1.default.match(String(response.body), /<
|
|
53
|
+
strict_1.default.match(String(response.body), /id="plugins"/);
|
|
54
|
+
strict_1.default.match(String(response.body), /<details id="installed-plugins" open><summary><h2>已安装插件<\/h2><\/summary>/);
|
|
55
|
+
strict_1.default.match(String(response.body), /<th scope="col">名称<\/th>/);
|
|
56
|
+
strict_1.default.doesNotMatch(String(response.body), /id="plugin-picker"/);
|
|
57
|
+
strict_1.default.match(String(response.body), /id="channel-tabs"/);
|
|
58
|
+
strict_1.default.match(String(response.body), /id="channel-panel"/);
|
|
59
|
+
strict_1.default.match(String(response.body), /id="pair-code"/);
|
|
60
|
+
strict_1.default.doesNotMatch(String(response.body), /id="access-token"|访问令牌/);
|
|
61
|
+
strict_1.default.doesNotMatch(String(response.body), /id="create-aid-form"/);
|
|
55
62
|
strict_1.default.doesNotMatch(String(response.body), /id="package-name"/);
|
|
56
63
|
const scriptHandler = routes.get("/abweb/app.js");
|
|
57
64
|
strict_1.default.ok(scriptHandler);
|
|
@@ -59,6 +66,10 @@ function createContext(config = {}) {
|
|
|
59
66
|
strict_1.default.match(String(script.body), /"升级"/);
|
|
60
67
|
strict_1.default.match(String(script.body), /"已安装"/);
|
|
61
68
|
strict_1.default.match(String(script.body), /\/update/);
|
|
69
|
+
strict_1.default.match(String(script.body), /managementPage/);
|
|
70
|
+
strict_1.default.doesNotMatch(String(script.body), /mab-token/);
|
|
71
|
+
strict_1.default.match(String(script.body), /setInterval/);
|
|
72
|
+
strict_1.default.match(String(script.body), /300000/);
|
|
62
73
|
strict_1.default.equal(response.headers["content-type"], "text/html; charset=utf-8");
|
|
63
74
|
});
|
|
64
75
|
(0, node_test_1.default)("abweb returns 404 for unknown static assets", async () => {
|
|
@@ -8,14 +8,87 @@ const promises_1 = require("node:fs/promises");
|
|
|
8
8
|
const node_os_1 = require("node:os");
|
|
9
9
|
const node_path_1 = require("node:path");
|
|
10
10
|
const node_test_1 = __importDefault(require("node:test"));
|
|
11
|
+
const node_vm_1 = require("node:vm");
|
|
11
12
|
const aun_1 = require("../src-ts/plugins/aun");
|
|
13
|
+
(0, node_test_1.default)("AUN 页面按关联状态、来源和最近关联时间排列已连接身份", async () => {
|
|
14
|
+
const node = () => ({ children: [], elements: new Proxy({}, { get: () => ({ value: "", replaceChildren() { } }) }), append(...items) { this.children.push(...items); }, replaceChildren(...items) { this.children = items; }, setAttribute() { } });
|
|
15
|
+
const elements = new Map();
|
|
16
|
+
const identities = {
|
|
17
|
+
groups: { muAgentBridge: ["offline-order.agentid.cn", "old.agentid.cn", "new.agentid.cn", "unknown.agentid.cn"], evolcore: ["external.agentid.cn"] },
|
|
18
|
+
bindings: { "old.agentid.cn": "dsh", "new.agentid.cn": "doubao", "unknown.agentid.cn": "workbuddy" },
|
|
19
|
+
bindingTimes: { "old.agentid.cn": "2026-10-09T00:00:00.000Z", "new.agentid.cn": "2026-10-10T00:00:00.000Z", "external.agentid.cn": "2026-10-11T00:00:00.000Z" },
|
|
20
|
+
};
|
|
21
|
+
const agents = [{ id: "qwenwork", type: "agent" }];
|
|
22
|
+
(0, node_vm_1.runInNewContext)(await (0, promises_1.readFile)((0, node_path_1.join)(__dirname, "../src-ts/plugins/aun/public/app.js"), "utf8"), {
|
|
23
|
+
document: { querySelector(selector) { if (!elements.has(selector))
|
|
24
|
+
elements.set(selector, node()); return elements.get(selector); }, createElement: node },
|
|
25
|
+
Option: function () { return node(); }, localStorage: { getItem() { return null; } }, setInterval() { },
|
|
26
|
+
fetch: async (path) => ({ ok: true, json: async () => path.endsWith("/identities") ? identities : path === "/api/plugins" ? { plugins: agents } : path.includes("qwenwork") ? { aid: "external.agentid.cn" } : {} }),
|
|
27
|
+
});
|
|
28
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
29
|
+
strict_1.default.deepEqual(elements.get("#identities").children.map((row) => row.children[0].textContent), ["new.agentid.cn", "old.agentid.cn", "unknown.agentid.cn", "external.agentid.cn", "offline-order.agentid.cn"]);
|
|
30
|
+
});
|
|
31
|
+
(0, node_test_1.default)("AUN 设置页及身份接口由插件注册并随停用注销", async () => {
|
|
32
|
+
const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "aun-management-"));
|
|
33
|
+
const routes = new Map();
|
|
34
|
+
const sdk = { AIDStore: class {
|
|
35
|
+
close() { }
|
|
36
|
+
} };
|
|
37
|
+
const plugin = new aun_1.AunPlugin({ sdk, connectedAids: async () => ["external.agentid.cn"], ingress: { publish: async () => undefined } });
|
|
38
|
+
try {
|
|
39
|
+
await plugin.activate({ dataDir: root, config: { owner_aid: "owner.agentid.cn", bindings: { "external.agentid.cn": "qwenwork" } }, logger: { info() { }, warn() { }, error() { } }, callPlugin: async () => ({ text: "" }), pluginStatus: () => "ready", http: {
|
|
40
|
+
register(route) { routes.set(`${route.method} ${route.path}`, route.handler); return () => routes.delete(`${route.method} ${route.path}`); }, unregister() { },
|
|
41
|
+
} });
|
|
42
|
+
strict_1.default.ok(routes.has("GET /abweb/channels/aun"));
|
|
43
|
+
strict_1.default.ok(routes.has("GET /api/plugins/aun/identities"));
|
|
44
|
+
strict_1.default.ok(routes.has("POST /api/plugins/aun/bind"));
|
|
45
|
+
strict_1.default.ok(routes.has("POST /api/plugins/aun/settings"));
|
|
46
|
+
const response = { statusCode: 0, setHeader() { }, end(value) { this.body = value; } };
|
|
47
|
+
await routes.get("GET /abweb/channels/aun")({ url: "/abweb/channels/aun" }, response);
|
|
48
|
+
strict_1.default.equal(response.statusCode, 200);
|
|
49
|
+
strict_1.default.match(String(response.body), /AUN 身份/);
|
|
50
|
+
await routes.get("GET /api/plugins/aun/identities")({}, response);
|
|
51
|
+
strict_1.default.deepEqual(JSON.parse(response.body).groups.evolcore, ["external.agentid.cn"]);
|
|
52
|
+
strict_1.default.deepEqual(JSON.parse(response.body).bindingTimes, {});
|
|
53
|
+
await routes.get("POST /api/plugins/aun/settings")((async function* () { yield Buffer.from(JSON.stringify({ agent_plugin: "dsh", allow_from: "peer.agentid.cn", aun_path: "" })); })(), response);
|
|
54
|
+
const saved = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(root, "config.json"), "utf8"));
|
|
55
|
+
strict_1.default.equal(saved.owner_aid, "owner.agentid.cn");
|
|
56
|
+
strict_1.default.deepEqual(saved.bindings, { "external.agentid.cn": "qwenwork" });
|
|
57
|
+
strict_1.default.equal(saved.agent_plugin, "dsh");
|
|
58
|
+
await plugin.deactivate("stop");
|
|
59
|
+
strict_1.default.equal(routes.size, 0);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
await (0, promises_1.rm)(root, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
(0, node_test_1.default)("旧版创建身份按来源归入本工程且只显示已连接身份", async () => {
|
|
66
|
+
const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "aun-origin-"));
|
|
67
|
+
const dataDir = (0, node_path_1.join)(root, "aun");
|
|
68
|
+
await (0, promises_1.mkdir)(dataDir);
|
|
69
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(root, "created-aids.json"), JSON.stringify(["doubao-lih.agentid.cn", "dshlihui.agentid.cn", "offline.agentid.cn"]));
|
|
70
|
+
const sdk = { AIDStore: class {
|
|
71
|
+
close() { }
|
|
72
|
+
} };
|
|
73
|
+
const plugin = new aun_1.AunPlugin({ sdk, connectedAids: async () => ["doubao-lih.agentid.cn", "dshlihui.agentid.cn", "external.agentid.cn"], ingress: { publish: async () => undefined } });
|
|
74
|
+
try {
|
|
75
|
+
await plugin.activate({ dataDir, config: {}, logger: { info() { }, warn() { }, error() { } }, callPlugin: async () => ({ text: "" }), pluginStatus: () => "ready", http: { register() { }, unregister() { } } });
|
|
76
|
+
strict_1.default.deepEqual(await plugin.identity.list(), { evolcore: ["external.agentid.cn"], muAgentBridge: ["doubao-lih.agentid.cn", "dshlihui.agentid.cn"], localMuAgentBridge: [] });
|
|
77
|
+
await plugin.deactivate("stop");
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
await (0, promises_1.rm)(root, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
12
83
|
(0, node_test_1.default)("AUN 插件注册 AID 并将加密回复发回来源 Peer", async () => {
|
|
13
84
|
const calls = [];
|
|
85
|
+
let registration = { ok: true, data: {} };
|
|
86
|
+
let loadOk = true;
|
|
14
87
|
const clients = [];
|
|
15
88
|
const sdk = {
|
|
16
89
|
AIDStore: class {
|
|
17
|
-
async register(input) { calls.push(`register:${input.aid}:${input.owner}`); return
|
|
18
|
-
load(aid) { return { ok: true, data: { aid } }; }
|
|
90
|
+
async register(input) { calls.push(`register:${input.aid}:${input.owner}`); return registration; }
|
|
91
|
+
load(aid) { return loadOk ? { ok: true, data: { aid } } : { ok: false, error: { message: "本地身份不存在" } }; }
|
|
19
92
|
close() { calls.push("store.close"); }
|
|
20
93
|
},
|
|
21
94
|
AUNClient: class {
|
|
@@ -41,12 +114,43 @@ const aun_1 = require("../src-ts/plugins/aun");
|
|
|
41
114
|
await route.handler(request, response);
|
|
42
115
|
strict_1.default.equal(response.statusCode, 201);
|
|
43
116
|
strict_1.default.equal(JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(root, "config.json"), "utf8")).aid, "bridge.agentid.pub");
|
|
117
|
+
const second = (async function* () { yield Buffer.from('{"aid":"second.agentid.pub","owner_aid":"owner.agentid.pub"}'); })();
|
|
118
|
+
await route.handler(second, response);
|
|
119
|
+
strict_1.default.equal(response.statusCode, 201);
|
|
120
|
+
strict_1.default.equal(clients.length, 2);
|
|
121
|
+
await plugin.identity.bind("dsh", "bridge.agentid.pub", 26107);
|
|
122
|
+
const boundAt = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(root, "config.json"), "utf8")).binding_times["bridge.agentid.pub"];
|
|
123
|
+
strict_1.default.ok(Number.isFinite(Date.parse(boundAt)));
|
|
44
124
|
clients[0].emit({ from: "peer.agentid.pub", message_id: "m1", payload: { text: "你好" } });
|
|
45
|
-
strict_1.default.equal(received[0].agentPlugin, "
|
|
125
|
+
strict_1.default.equal(received[0].agentPlugin, "dsh");
|
|
46
126
|
await received[0].reply("收到");
|
|
47
127
|
strict_1.default.ok(calls.includes("message.send:peer.agentid.pub:true"));
|
|
128
|
+
clients[0].emit({ from: "peer.agentid.pub", payload: { text: "无 ID 消息一" } });
|
|
129
|
+
clients[0].emit({ from: "peer.agentid.pub", payload: { text: "无 ID 消息二" } });
|
|
130
|
+
strict_1.default.equal(received.length, 3);
|
|
131
|
+
clients[0].emit({ from: "peer.agentid.pub", message_id: "m1", payload: { text: "重复消息" } });
|
|
132
|
+
strict_1.default.equal(received.length, 3);
|
|
133
|
+
await plugin.deactivate("stop");
|
|
134
|
+
strict_1.default.equal(calls.filter((call) => call === "client.close").length, 2);
|
|
135
|
+
strict_1.default.equal(calls.at(-1), "store.close");
|
|
136
|
+
await plugin.activate({ dataDir: root, config: JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(root, "config.json"), "utf8")), logger: { info() { }, warn() { }, error() { } }, callPlugin: async () => ({ text: "" }), pluginStatus: () => "ready", http: { register() { }, unregister() { } } });
|
|
137
|
+
const health = await plugin.health();
|
|
138
|
+
strict_1.default.deepEqual(health.details?.aids, ["bridge.agentid.pub", "second.agentid.pub"]);
|
|
139
|
+
strict_1.default.deepEqual(health.details?.bindings, { "bridge.agentid.pub": "dsh" });
|
|
140
|
+
await routes.find((entry) => entry.path === "/api/plugins/aun/identities").handler({}, response);
|
|
141
|
+
strict_1.default.equal(response.body.bindingTimes["bridge.agentid.pub"], boundAt);
|
|
142
|
+
await strict_1.default.rejects(plugin.identity.bind("dsh", "bad", 26107), /AID 格式无效/);
|
|
143
|
+
await routes.find((entry) => entry.path === "/api/plugins/aun/identities").handler({}, response);
|
|
144
|
+
strict_1.default.equal(response.body.bindingTimes["bridge.agentid.pub"], boundAt);
|
|
145
|
+
registration = { ok: true, data: { state: "owner_pending", operation_id: "op1" } };
|
|
146
|
+
strict_1.default.deepEqual(await plugin.identity.create("pending.agentid.pub", "owner.agentid.pub"), { aid: "pending.agentid.pub", status: "等待 Owner 确认", operation_id: "op1" });
|
|
147
|
+
strict_1.default.equal((await plugin.health()).details?.aids instanceof Array, true);
|
|
148
|
+
registration = { ok: false, error: { message: "注册失败" } };
|
|
149
|
+
await strict_1.default.rejects(plugin.identity.create("failed.agentid.pub", "owner.agentid.pub"), /注册失败/);
|
|
150
|
+
registration = { ok: true, data: {} };
|
|
151
|
+
loadOk = false;
|
|
152
|
+
await strict_1.default.rejects(plugin.identity.create("missing.agentid.pub", "owner.agentid.pub"), /本地身份不存在/);
|
|
48
153
|
await plugin.deactivate("stop");
|
|
49
|
-
strict_1.default.deepEqual(calls.slice(-2), ["client.close", "store.close"]);
|
|
50
154
|
}
|
|
51
155
|
finally {
|
|
52
156
|
await (0, promises_1.rm)(root, { recursive: true, force: true });
|