agent-bridge-workbuddy 0.0.0-stage → 1.1.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 CHANGED
@@ -1,3 +1,7 @@
1
- # Temporary Holding Version
1
+ # WorkBuddy 插件
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 插件保留旧桥的 CodeBuddy CLI 调用,并支持 WorkBuddy 桌面端 CDP。默认 `auto` 优先使用 CLI;CLI 未登录时直接返回登录错误,不切换桌面端账号。没有 CLI 时才使用 CDP。未连接桌面端时默认自动启动 `WorkBuddy.exe`,CDP 默认端口为 `9229`。
4
+
5
+ 将本目录作为本地插件安装,或发布后安装 `agent-bridge-workbuddy`;在 AUN 通道配置中把 `agent_plugin` 设为 `workbuddy`。插件串行处理消息;CLI 按 AUN 对端续接会话,CDP 使用当前桌面会话。可用 `transport` 固定为 `cli` 或 `cdp`。
6
+
7
+ 当提示中以“只要、调用、使用、找、用”等表达指定专家(例如“只要 AI 短剧剧本专家,帮我写开场”)时,插件会切换到 WorkBuddy 桌面端,在专家中心按名称搜索并召唤唯一匹配的专家,再发送原始提示。专家请求需要桌面端 CDP;普通提示继续优先使用 CLI。简称没有唯一匹配时会提示用户指定完整专家名称。
@@ -0,0 +1,14 @@
1
+ {
2
+ "type": "object",
3
+ "properties": {
4
+ "transport": { "type": "string", "enum": ["auto", "cli", "cdp"], "default": "auto" },
5
+ "cliPath": { "type": "string", "description": "CodeBuddy CLI 路径" },
6
+ "model": { "type": "string", "description": "CLI 模型 ID" },
7
+ "executablePath": { "type": "string", "description": "WorkBuddy.exe 路径,省略时自动查找" },
8
+ "cdpPort": { "type": "integer", "minimum": 1, "maximum": 65535, "default": 9229 },
9
+ "autoStart": { "type": "boolean", "default": true, "description": "未连接时自动启动 WorkBuddy" },
10
+ "startupTimeoutMs": { "type": "integer", "minimum": 1000, "default": 30000 },
11
+ "timeoutMs": { "type": "integer", "minimum": 1, "default": 240000, "description": "单次回复超时时间(毫秒)" }
12
+ },
13
+ "additionalProperties": false
14
+ }
@@ -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,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;
package/package.json CHANGED
@@ -1,6 +1,12 @@
1
1
  {
2
2
  "name": "agent-bridge-workbuddy",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.0",
4
+ "description": "agent-bridge 的 CodeBuddy CLI 插件。",
5
+ "keywords": ["mu-agent-bridge-plugin", "mu-agent-bridge-agent"],
6
+ "repository": { "type": "git", "url": "https://codeup.aliyun.com/6808da56fb3da14b899afd54/agent_bridge.git", "directory": "packages/workbuddy" },
7
+ "main": "dist/src-ts/plugins/workbuddy/index.js",
8
+ "files": ["dist/", "plugin.json", "config.schema.json", "README.md"],
9
+ "agentBridge": { "plugin": "./plugin.json", "type": "agent" },
10
+ "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" },
11
+ "engines": { "node": ">=22" }
12
+ }
package/plugin.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "id": "workbuddy",
3
+ "version": "1.1.0",
4
+ "type": "agent",
5
+ "displayName": "WorkBuddy(CodeBuddy CLI)",
6
+ "entry": "dist/src-ts/plugins/workbuddy/index.js",
7
+ "apiVersion": "1",
8
+ "process": "in-process",
9
+ "capabilities": ["chat", "health"],
10
+ "configSchema": "config.schema.json"
11
+ }