agent-bridge-workbuddy 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 +4 -2
- package/config.schema.json +14 -0
- package/dist/src-ts/agent.js +10 -0
- package/dist/src-ts/plugins/workbuddy/cdp-client.js +69 -0
- package/dist/src-ts/plugins/workbuddy/cli-client.js +39 -0
- package/dist/src-ts/plugins/workbuddy/index.js +122 -0
- package/package.json +10 -4
- package/plugin.json +11 -0
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# WorkBuddy 插件
|
|
2
2
|
|
|
3
|
-
|
|
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`。
|
|
@@ -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,69 @@
|
|
|
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) {
|
|
39
|
+
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 })))`)));
|
|
40
|
+
const beforeIds = new Set(before.map((item) => item.id).filter((id) => Boolean(id)));
|
|
41
|
+
if (!await this.evaluate(`(() => { const element = document.querySelector(${JSON.stringify(exports.editorSelector)}); element?.focus(); return Boolean(element); })()`))
|
|
42
|
+
throw new Error("WorkBuddy 输入框未找到");
|
|
43
|
+
await this.cdp("Input.insertText", { text });
|
|
44
|
+
if (!await this.evaluate(`(() => { const button = document.querySelector('button[aria-label="发送"]'); if (!button || button.disabled) return false; button.click(); return true; })()`))
|
|
45
|
+
throw new Error("WorkBuddy 发送按钮不可用");
|
|
46
|
+
const deadline = Date.now() + timeoutMs;
|
|
47
|
+
while (Date.now() < deadline) {
|
|
48
|
+
await sleep(1_000);
|
|
49
|
+
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"]')) })))`)));
|
|
50
|
+
const reply = current.filter((item) => item.id && !beforeIds.has(item.id) && item.text?.trim() && item.finished).at(-1)?.text?.trim();
|
|
51
|
+
if (reply)
|
|
52
|
+
return reply;
|
|
53
|
+
}
|
|
54
|
+
throw new Error("WorkBuddy UI 回复超时");
|
|
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("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} 超时`); })]); }
|
|
66
|
+
async evaluate(expression) { const response = await this.cdp("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true }); if (response.result?.exceptionDetails)
|
|
67
|
+
throw new Error(`WorkBuddy 页面 JS 异常:${JSON.stringify(response.result.exceptionDetails).slice(0, 300)}`); return response.result?.result?.value; }
|
|
68
|
+
}
|
|
69
|
+
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,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WorkbuddyPlugin = exports.workbuddyManifest = void 0;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const node_fs_1 = require("node:fs");
|
|
6
|
+
const node_os_1 = require("node:os");
|
|
7
|
+
const node_path_1 = require("node:path");
|
|
8
|
+
const cdp_client_1 = require("./cdp-client");
|
|
9
|
+
const cli_client_1 = require("./cli-client");
|
|
10
|
+
exports.workbuddyManifest = {
|
|
11
|
+
id: "workbuddy", version: "1.0.0", displayName: "WorkBuddy", type: "agent",
|
|
12
|
+
entry: "dist/index.js", apiVersion: "1", process: "in-process",
|
|
13
|
+
capabilities: ["chat", "health"], configSchema: "config.schema.json",
|
|
14
|
+
};
|
|
15
|
+
function findExecutable(configured) {
|
|
16
|
+
if (configured)
|
|
17
|
+
return (0, node_fs_1.existsSync)((0, node_path_1.resolve)(configured)) ? (0, node_path_1.resolve)(configured) : undefined;
|
|
18
|
+
const local = process.env.LOCALAPPDATA ?? (0, node_path_1.join)((0, node_os_1.homedir)(), "AppData", "Local");
|
|
19
|
+
return [(0, node_path_1.join)(local, "Programs", "WorkBuddy", "WorkBuddy.exe"), "D:\\Programs\\WorkBuddy\\WorkBuddy.exe"].find(node_fs_1.existsSync);
|
|
20
|
+
}
|
|
21
|
+
class WorkbuddyPlugin {
|
|
22
|
+
options;
|
|
23
|
+
manifest = exports.workbuddyManifest;
|
|
24
|
+
client;
|
|
25
|
+
process;
|
|
26
|
+
cliPath;
|
|
27
|
+
model;
|
|
28
|
+
transport = "auto";
|
|
29
|
+
timeoutMs = 240_000;
|
|
30
|
+
queue = Promise.resolve();
|
|
31
|
+
failure;
|
|
32
|
+
constructor(options = {}) {
|
|
33
|
+
this.options = options;
|
|
34
|
+
}
|
|
35
|
+
async activate(context) {
|
|
36
|
+
const config = context.config;
|
|
37
|
+
if (config.transport !== undefined && !["auto", "cli", "cdp"].includes(String(config.transport)))
|
|
38
|
+
throw new Error("transport 只能是 auto、cli 或 cdp");
|
|
39
|
+
this.transport = config.transport ?? "auto";
|
|
40
|
+
this.timeoutMs = typeof config.timeoutMs === "number" && config.timeoutMs > 0 ? config.timeoutMs : 240_000;
|
|
41
|
+
this.model = typeof config.model === "string" && config.model.trim() ? config.model : undefined;
|
|
42
|
+
this.cliPath = (0, cli_client_1.findCli)(typeof config.cliPath === "string" ? config.cliPath : undefined);
|
|
43
|
+
this.failure = undefined;
|
|
44
|
+
if (this.transport === "cli") {
|
|
45
|
+
if (!this.cliPath)
|
|
46
|
+
throw new Error("未找到 CodeBuddy CLI,请配置 cliPath");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const port = typeof config.cdpPort === "number" ? config.cdpPort : this.options.cdpPort ?? 9229;
|
|
50
|
+
this.client = this.options.client ?? new cdp_client_1.WorkbuddyCdpClient({ port });
|
|
51
|
+
try {
|
|
52
|
+
await this.connectOrStart(config, context.logger, port);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
this.failure = error instanceof Error ? error.message : String(error);
|
|
56
|
+
this.client.close();
|
|
57
|
+
this.client = undefined;
|
|
58
|
+
if (this.transport === "cdp" || !this.cliPath)
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async deactivate(_reason) {
|
|
63
|
+
await this.queue;
|
|
64
|
+
this.client?.close();
|
|
65
|
+
this.client = undefined;
|
|
66
|
+
// WorkBuddy 是用户桌面应用,插件停止时不结束桌面进程。
|
|
67
|
+
this.process = undefined;
|
|
68
|
+
}
|
|
69
|
+
async health() {
|
|
70
|
+
if (this.client?.connected())
|
|
71
|
+
return { status: "ready", details: { transport: "cdp" } };
|
|
72
|
+
if (this.cliPath && (0, node_fs_1.existsSync)(this.cliPath))
|
|
73
|
+
return { status: "ready", details: { transport: "cli" } };
|
|
74
|
+
return { status: "failed", message: this.failure ?? "WorkBuddy 未连接", details: { transport: this.transport } };
|
|
75
|
+
}
|
|
76
|
+
async handle(request) {
|
|
77
|
+
if (!request.task.trim())
|
|
78
|
+
throw new Error("消息内容不能为空");
|
|
79
|
+
const run = this.queue.then(() => this.complete(request), () => this.complete(request));
|
|
80
|
+
this.queue = run.then(() => undefined, () => undefined);
|
|
81
|
+
return { text: await run, sessionId: request.sessionId };
|
|
82
|
+
}
|
|
83
|
+
async complete(request) {
|
|
84
|
+
const timeout = request.timeoutMs ?? this.timeoutMs;
|
|
85
|
+
if (this.cliPath && this.transport !== "cdp")
|
|
86
|
+
return (0, cli_client_1.callCli)(this.cliPath, request.task, request.sessionId ?? "local-test", timeout, this.model);
|
|
87
|
+
if (!this.client?.connected())
|
|
88
|
+
throw new Error("WorkBuddy CDP 未连接");
|
|
89
|
+
return this.client.direct(request.task, timeout);
|
|
90
|
+
}
|
|
91
|
+
async connectOrStart(config, logger, port) {
|
|
92
|
+
try {
|
|
93
|
+
await this.client.connect();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
catch (first) {
|
|
97
|
+
if (config.autoStart === false || this.options.client)
|
|
98
|
+
throw first;
|
|
99
|
+
const executable = findExecutable(typeof config.executablePath === "string" ? config.executablePath : this.options.executablePath);
|
|
100
|
+
if (!executable)
|
|
101
|
+
throw new Error("未找到 WorkBuddy.exe,请配置 executablePath");
|
|
102
|
+
this.process = (0, node_child_process_1.spawn)(executable, [`--remote-debugging-port=${port}`], { detached: true, stdio: "ignore", windowsHide: false });
|
|
103
|
+
this.process.unref();
|
|
104
|
+
logger.info(`WorkBuddy 已启动,等待 CDP ${port}`);
|
|
105
|
+
const deadline = Date.now() + (typeof config.startupTimeoutMs === "number" ? config.startupTimeoutMs : 30_000);
|
|
106
|
+
let last = first;
|
|
107
|
+
while (Date.now() < deadline) {
|
|
108
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
109
|
+
try {
|
|
110
|
+
await this.client.connect();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
last = error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
throw last instanceof Error ? last : new Error("WorkBuddy CDP 启动超时");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
exports.WorkbuddyPlugin = WorkbuddyPlugin;
|
|
122
|
+
exports.default = WorkbuddyPlugin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-bridge-workbuddy",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
}
|
|
3
|
+
"version": "1.0.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.0.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
|
+
}
|