mu-agent-bridge 1.0.41 → 1.0.43

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.
Files changed (69) hide show
  1. package/README.md +4 -0
  2. package/dist/src-ts/agent-availability.js +2 -1
  3. package/dist/src-ts/control-server.js +4 -0
  4. package/dist/src-ts/headless-account.patch.yml +10 -0
  5. package/dist/src-ts/plugin-manager.js +16 -0
  6. package/dist/src-ts/plugin-manifest.js +3 -1
  7. package/dist/src-ts/plugins/abweb/public/app.js +34 -0
  8. package/dist/src-ts/plugins/abweb/public/index.html +10 -0
  9. package/dist/src-ts/plugins/abweb/public/style.css +3 -0
  10. package/dist/src-ts/plugins/aun/index.js +5 -5
  11. package/dist/src-ts/plugins/aun/public/app.js +1 -4
  12. package/dist/src-ts/plugins/aun/public/index.html +1 -1
  13. package/dist/src-ts/plugins/chatgpt/index.js +41 -5
  14. package/dist/src-ts/plugins/claude-cli/index.js +14 -0
  15. package/dist/src-ts/plugins/codex-cli/index.js +14 -0
  16. package/dist/src-ts/plugins/desktop-launch.js +93 -2
  17. package/dist/src-ts/plugins/doubao/index.js +14 -12
  18. package/dist/src-ts/plugins/local-cli/agent.js +321 -0
  19. package/dist/src-ts/plugins/workbuddy/cdp-client.js +3 -1
  20. package/dist/src-ts/plugins/workbuddy/desktop-launch.js +10 -0
  21. package/dist/src-ts/plugins/workbuddy/index.js +35 -21
  22. package/dist/tests-ts/abweb-cli.test.js +65 -0
  23. package/dist/tests-ts/agent-availability.test.js +15 -1
  24. package/dist/tests-ts/aun-default-owner-page.test.js +1 -1
  25. package/dist/tests-ts/aun-plugin.test.js +16 -6
  26. package/dist/tests-ts/bundled-package.test.js +1 -1
  27. package/dist/tests-ts/control-server.test.js +20 -0
  28. package/dist/tests-ts/desktop-launch.test.js +98 -0
  29. package/dist/tests-ts/installer-assets.test.js +30 -7
  30. package/dist/tests-ts/local-cli.test.js +170 -0
  31. package/dist/tests-ts/platform-integration.test.js +1 -1
  32. package/dist/tests-ts/plugin-manager.test.js +18 -0
  33. package/dist/tests-ts/workbuddy-plugin.test.js +63 -0
  34. package/package.json +1 -1
  35. package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.js +34 -0
  36. package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.ts +30 -1
  37. package/plugins/abweb/dist/src-ts/plugins/abweb/public/index.html +10 -0
  38. package/plugins/abweb/dist/src-ts/plugins/abweb/public/style.css +3 -0
  39. package/plugins/aun/config.schema.json +1 -1
  40. package/plugins/aun/dist/src-ts/plugins/aun/index.js +5 -5
  41. package/plugins/aun/dist/src-ts/plugins/aun/public/app.js +1 -4
  42. package/plugins/aun/dist/src-ts/plugins/aun/public/index.html +1 -1
  43. package/plugins/chatgpt/config.schema.json +1 -1
  44. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +41 -5
  45. package/plugins/chatgpt/dist/src-ts/plugins/desktop-launch.js +191 -0
  46. package/plugins/claude-cli/config.schema.json +37 -0
  47. package/plugins/claude-cli/dist/src-ts/agent.js +10 -0
  48. package/plugins/claude-cli/dist/src-ts/plugin.js +13 -0
  49. package/plugins/claude-cli/dist/src-ts/plugins/claude-cli/index.js +14 -0
  50. package/plugins/claude-cli/dist/src-ts/plugins/local-cli/agent.js +321 -0
  51. package/plugins/claude-cli/package.json +35 -0
  52. package/plugins/claude-cli/plugin.json +15 -0
  53. package/plugins/codex-cli/config.schema.json +37 -0
  54. package/plugins/codex-cli/dist/src-ts/agent.js +10 -0
  55. package/plugins/codex-cli/dist/src-ts/plugin.js +13 -0
  56. package/plugins/codex-cli/dist/src-ts/plugins/codex-cli/index.js +14 -0
  57. package/plugins/codex-cli/dist/src-ts/plugins/local-cli/agent.js +321 -0
  58. package/plugins/codex-cli/package.json +35 -0
  59. package/plugins/codex-cli/plugin.json +15 -0
  60. package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +93 -2
  61. package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +14 -12
  62. package/plugins/dsh/dist/src-ts/headless-account.patch.yml +10 -0
  63. package/plugins/qwenwork/dist/src-ts/plugins/desktop-launch.js +93 -2
  64. package/plugins/workbuddy/dist/src-ts/plugins/desktop-launch.js +191 -0
  65. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +3 -1
  66. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/desktop-launch.js +10 -0
  67. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +35 -21
  68. package/plugins/workbuddy/package.json +1 -1
  69. package/plugins/workbuddy/plugin.json +1 -1
@@ -0,0 +1,321 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LocalCliAgent = void 0;
4
+ exports.resolveWindowsCliShim = resolveWindowsCliShim;
5
+ const node_child_process_1 = require("node:child_process");
6
+ const node_fs_1 = require("node:fs");
7
+ const promises_1 = require("node:fs/promises");
8
+ const node_path_1 = require("node:path");
9
+ const node_crypto_1 = require("node:crypto");
10
+ const nativeId = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
11
+ const maximumOutput = 4 * 1024 * 1024;
12
+ function diagnostic(value) {
13
+ return value.replace(/\b(?:Bearer\s+)[^\s"']+/gi, "Bearer [已隐藏]")
14
+ .replace(/((?:[\w-]*(?:token|secret|password|api[_-]?key)[\w-]*)["']?\s*[:=]\s*["']?)[^\s"',}]+/gi, "$1[已隐藏]")
15
+ .replace(/\b(?:sk-|npm_)[A-Za-z0-9_-]+/g, "[已隐藏]")
16
+ .replace(/https?:\/\/[^\s"']+/g, text => { try {
17
+ const url = new URL(text);
18
+ return `${url.origin}/[路径已隐藏]`;
19
+ }
20
+ catch {
21
+ return "[地址已隐藏]";
22
+ } })
23
+ .slice(0, 2000);
24
+ }
25
+ async function resolveWindowsCliShim(file, provider) {
26
+ const modules = (0, node_path_1.basename)((0, node_path_1.dirname)(file)) === ".bin" ? (0, node_path_1.dirname)((0, node_path_1.dirname)(file)) : (0, node_path_1.join)((0, node_path_1.dirname)(file), "node_modules");
27
+ const root = (0, node_path_1.join)(modules, ...(provider === "codex" ? ["@openai", "codex"] : ["@anthropic-ai", "claude-code"]));
28
+ const metadata = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(root, "package.json"), "utf8"));
29
+ const entry = typeof metadata.bin === "string" ? metadata.bin : metadata.bin?.[provider];
30
+ if (typeof entry !== "string" || !entry)
31
+ throw new Error("npm CLI 包未声明可执行入口");
32
+ const target = (0, node_path_1.resolve)(root, entry), path = (0, node_path_1.relative)(root, target);
33
+ if ((0, node_path_1.isAbsolute)(entry) || path === ".." || path.startsWith(`..${node_path_1.sep}`))
34
+ throw new Error("npm CLI 入口路径超出包目录");
35
+ if (!(await (0, promises_1.stat)(target)).isFile())
36
+ throw new Error("npm CLI 入口不是文件");
37
+ await (0, promises_1.access)(target, node_fs_1.constants.R_OK);
38
+ if ([".js", ".cjs", ".mjs"].includes((0, node_path_1.extname)(target)))
39
+ return { command: process.execPath, args: [target] };
40
+ if ((0, node_path_1.extname)(target).toLowerCase() !== ".exe")
41
+ throw new Error("npm CLI 入口必须是原生 exe 或 Node 脚本");
42
+ return { command: target, args: [] };
43
+ }
44
+ async function executable(provider, configured) {
45
+ const candidates = configured ? [(0, node_path_1.resolve)(configured)] : (process.env.PATH ?? "").split(node_path_1.delimiter).filter(Boolean)
46
+ .flatMap(directory => (process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""]).map(extension => (0, node_path_1.join)(directory, provider + extension)));
47
+ for (const file of candidates) {
48
+ try {
49
+ if (!(await (0, promises_1.stat)(file)).isFile())
50
+ continue;
51
+ if (process.platform === "win32" && [".cmd", ".bat"].includes((0, node_path_1.extname)(file).toLowerCase()))
52
+ return await resolveWindowsCliShim(file, provider);
53
+ if ([".js", ".cjs", ".mjs"].includes((0, node_path_1.extname)(file))) {
54
+ await (0, promises_1.access)(file, node_fs_1.constants.R_OK);
55
+ return { command: process.execPath, args: [file] };
56
+ }
57
+ await (0, promises_1.access)(file, process.platform === "win32" ? node_fs_1.constants.F_OK : node_fs_1.constants.X_OK);
58
+ return { command: file, args: [] };
59
+ }
60
+ catch (error) {
61
+ if (configured)
62
+ throw new Error(`无法使用 ${provider} CLI 程序:${diagnostic(String(error))};请检查路径、安装及 daemon 用户的执行权限`);
63
+ }
64
+ }
65
+ throw new Error(`未找到 ${provider} CLI 可执行程序,请安装并在 daemon 用户下登录,或配置 executablePath`);
66
+ }
67
+ class LocalCliAgent {
68
+ manifest;
69
+ provider;
70
+ sessionOperations = ["new"];
71
+ attachmentInput = false;
72
+ context;
73
+ command;
74
+ cwd = "";
75
+ timeoutMs = 240000;
76
+ ready = false;
77
+ message = "插件尚未启动";
78
+ version = "";
79
+ sessions = new Map();
80
+ writes = Promise.resolve();
81
+ pending = new Map();
82
+ children = new Map();
83
+ constructor(manifest, provider) {
84
+ this.manifest = manifest;
85
+ this.provider = provider;
86
+ }
87
+ async activate(context) {
88
+ this.context = context;
89
+ this.ready = false;
90
+ try {
91
+ const cwd = context.config.cwd;
92
+ if (typeof cwd !== "string" || !cwd.trim() || !(0, node_path_1.isAbsolute)(cwd))
93
+ throw new Error("请配置已存在的绝对工作目录 cwd");
94
+ if (!(await (0, promises_1.stat)(cwd)).isDirectory())
95
+ throw new Error("工作目录 cwd 不是目录");
96
+ this.cwd = cwd;
97
+ const timeout = context.config.timeoutMs ?? 240000;
98
+ if (typeof timeout !== "number" || !Number.isInteger(timeout) || timeout <= 0 || timeout > 2147483647)
99
+ throw new Error("timeoutMs 必须是有效的正整数毫秒数");
100
+ this.timeoutMs = timeout;
101
+ const configured = context.config.executablePath;
102
+ if (configured !== undefined && typeof configured !== "string")
103
+ throw new Error("executablePath 必须是路径字符串");
104
+ this.command = await executable(this.provider, typeof configured === "string" && configured.trim() ? configured : undefined);
105
+ this.version = (await this.run(["--version"], "", 10000)).stdout.trim();
106
+ if (!/\d+\.\d+\.\d+/.test(this.version))
107
+ throw new Error("无法识别 CLI 版本");
108
+ const help = (await this.run(this.provider === "codex" ? ["exec", "--help"] : ["--help"], "", 10000)).stdout;
109
+ if (this.provider === "codex" ? !help.includes("--json") || !help.includes("resume") : !help.includes("--output-format") || !help.includes("--resume"))
110
+ throw new Error("CLI 版本不支持所需的结构化输出和会话恢复,请升级 CLI");
111
+ this.sessions = new Map();
112
+ try {
113
+ const data = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(context.dataDir, "cli-sessions.json"), "utf8"));
114
+ if (data.version !== 1 || !Array.isArray(data.sessions) || data.sessions.some((entry) => !Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string" || typeof entry[1] !== "string" || !nativeId.test(entry[1])))
115
+ throw new Error("CLI 会话映射文件无效,请检查数据文件,不能安全恢复会话");
116
+ this.sessions = new Map(data.sessions);
117
+ }
118
+ catch (error) {
119
+ if (error.code !== "ENOENT")
120
+ throw error;
121
+ }
122
+ this.ready = true;
123
+ this.message = "";
124
+ context.logger.info("本地 CLI 插件已就绪", { plugin: this.manifest.id, version: diagnostic(this.version) });
125
+ }
126
+ catch (error) {
127
+ this.message = diagnostic(String(error));
128
+ throw new Error(this.message);
129
+ }
130
+ }
131
+ run(args, input, timeout) {
132
+ const command = this.command;
133
+ let child;
134
+ let stop;
135
+ const done = new Promise((accept, reject) => {
136
+ let stdout = "", stderr = "", size = 0, failure = "";
137
+ child = (0, node_child_process_1.spawn)(command.command, [...command.args, ...args], { cwd: this.cwd, env: process.env, shell: false, windowsHide: true, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"] });
138
+ const terminate = (reason) => {
139
+ if (failure)
140
+ return;
141
+ failure = reason;
142
+ if (!child.pid)
143
+ return;
144
+ if (process.platform === "win32")
145
+ (0, node_child_process_1.execFile)("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true }, () => { });
146
+ else {
147
+ try {
148
+ process.kill(-child.pid, "SIGKILL");
149
+ }
150
+ catch {
151
+ child.kill("SIGKILL");
152
+ }
153
+ }
154
+ };
155
+ stop = () => terminate("插件已停止,当前 CLI 任务已终止");
156
+ const timer = setTimeout(() => terminate(`CLI 调用超时(${timeout}ms),请检查登录、网络或权限审批`), timeout);
157
+ const cleanup = () => { clearTimeout(timer); this.children.delete(child); };
158
+ child.stdout.setEncoding("utf8");
159
+ child.stderr.setEncoding("utf8");
160
+ const collect = (value, error) => {
161
+ size += Buffer.byteLength(value);
162
+ if (size > maximumOutput) {
163
+ terminate("CLI 输出超过 4MB 限制");
164
+ return;
165
+ }
166
+ if (error)
167
+ stderr += value;
168
+ else
169
+ stdout += value;
170
+ };
171
+ child.stdout.on("data", value => collect(value, false));
172
+ child.stderr.on("data", value => collect(value, true));
173
+ child.on("error", error => { cleanup(); reject(new Error(`无法启动 ${this.provider} CLI:${diagnostic(String(error))}`)); });
174
+ child.on("close", (code, signal) => {
175
+ cleanup();
176
+ if (failure) {
177
+ reject(new Error(failure));
178
+ return;
179
+ }
180
+ if (code !== 0) {
181
+ const detail = diagnostic(stderr || stdout);
182
+ const hint = /login|authenticat|not logged|unauthorized|401/i.test(detail) ? "请检查 daemon 运行用户的 CLI 登录状态" : /permission|approval|denied|EACCES/i.test(detail) ? "权限审批受阻,请在本机 CLI 确认目录和工具权限" : "请检查 CLI 配置及网络";
183
+ reject(new Error(`${this.provider} CLI 退出码 ${code ?? signal}:${hint};${detail}`));
184
+ }
185
+ else
186
+ accept({ stdout, stderr });
187
+ });
188
+ child.stdin.on("error", () => { });
189
+ child.stdin.end(input);
190
+ });
191
+ this.children.set(child, { stop, done });
192
+ return done;
193
+ }
194
+ save(session, id) {
195
+ const operation = this.writes.catch(() => { }).then(async () => {
196
+ const next = new Map(this.sessions);
197
+ if (id)
198
+ next.set(session, id);
199
+ else
200
+ next.delete(session);
201
+ const directory = this.context.dataDir;
202
+ await (0, promises_1.mkdir)(directory, { recursive: true, mode: 0o700 });
203
+ const file = (0, node_path_1.join)(directory, "cli-sessions.json"), temporary = `${file}.${(0, node_crypto_1.randomUUID)()}.tmp`;
204
+ try {
205
+ await (0, promises_1.writeFile)(temporary, JSON.stringify({ version: 1, sessions: [...next] }), { flag: "wx", mode: 0o600 });
206
+ await (0, promises_1.rename)(temporary, file);
207
+ }
208
+ finally {
209
+ await (0, promises_1.rm)(temporary, { force: true });
210
+ }
211
+ this.sessions = next;
212
+ });
213
+ this.writes = operation;
214
+ return operation;
215
+ }
216
+ serial(key, operation) {
217
+ const next = (this.pending.get(key) ?? Promise.resolve()).catch(() => { }).then(operation);
218
+ this.pending.set(key, next);
219
+ return next.finally(() => { if (this.pending.get(key) === next)
220
+ this.pending.delete(key); });
221
+ }
222
+ requireReady() { if (!this.ready)
223
+ throw new Error(`${this.manifest.displayName} 插件未启动或已停止:${this.message}`); }
224
+ async handle(request) {
225
+ this.requireReady();
226
+ if (request.attachments?.length)
227
+ throw new Error(`${this.manifest.displayName} 本期不支持附件输入`);
228
+ if (!request.task?.trim())
229
+ throw new Error("CLI 任务文本不能为空");
230
+ const logical = request.sessionId || (0, node_crypto_1.randomUUID)();
231
+ return this.serial(logical, async () => {
232
+ this.requireReady();
233
+ const id = request.newSession ? undefined : this.sessions.get(logical);
234
+ const args = this.provider === "codex"
235
+ ? ["exec", ...(id ? ["resume", id] : []), "--json", "-"]
236
+ : ["--print", "--output-format", "json", ...(id ? ["--resume", id] : [])];
237
+ const timeout = request.timeoutMs ?? this.timeoutMs;
238
+ if (!Number.isInteger(timeout) || timeout <= 0 || timeout > 2147483647)
239
+ throw new Error("任务超时必须是有效的正整数毫秒数");
240
+ const started = Date.now();
241
+ this.context.logger.info("本地 CLI 任务开始", { plugin: this.manifest.id, sessionId: logical, resume: Boolean(id) });
242
+ try {
243
+ const output = await this.run(args, request.task, timeout);
244
+ const response = this.parse(output.stdout);
245
+ if (id && response.id !== id)
246
+ throw new Error("CLI 恢复返回的会话 ID 与指定会话不一致,保留原会话绑定");
247
+ await this.save(logical, response.id);
248
+ this.context.logger.info("本地 CLI 任务完成", { plugin: this.manifest.id, sessionId: logical, durationMs: Date.now() - started, exitCode: 0 });
249
+ return { text: response.text, sessionId: logical };
250
+ }
251
+ catch (error) {
252
+ const detail = diagnostic(String(error));
253
+ this.context.logger.error("本地 CLI 任务失败", { plugin: this.manifest.id, sessionId: logical, durationMs: Date.now() - started, error: detail });
254
+ throw new Error(`${this.manifest.displayName}:${detail}`);
255
+ }
256
+ });
257
+ }
258
+ parse(stdout) {
259
+ let id = "", text = "";
260
+ try {
261
+ if (this.provider === "codex") {
262
+ let completed = false, lastError = "";
263
+ const messages = [];
264
+ for (const line of stdout.split(/\r?\n/).filter(line => line.trim())) {
265
+ const event = JSON.parse(line);
266
+ if (event.type === "thread.started")
267
+ id = event.thread_id;
268
+ if (event.type === "item.completed" && event.item?.type === "agent_message" && typeof event.item.text === "string")
269
+ messages.push(event.item.text);
270
+ if (event.type === "turn.failed")
271
+ throw new Error(JSON.stringify(event.error ?? event));
272
+ // 中途错误后仍可能恢复,必须以最终完成事件判定成功。
273
+ if (event.type === "error") {
274
+ completed = false;
275
+ lastError = JSON.stringify(event.error ?? event);
276
+ }
277
+ if (event.type === "turn.completed")
278
+ completed = true;
279
+ }
280
+ if (!completed)
281
+ throw new Error(lastError || "缺少完成事件");
282
+ text = messages.join("\n\n");
283
+ }
284
+ else {
285
+ const event = JSON.parse(stdout);
286
+ if (event.is_error || event.type !== "result")
287
+ throw new Error(JSON.stringify(event.errors ?? event));
288
+ id = event.session_id;
289
+ text = event.result;
290
+ }
291
+ if (typeof id !== "string" || !nativeId.test(id))
292
+ throw new Error("缺少有效原生会话 ID");
293
+ if (typeof text !== "string" || !text.trim())
294
+ throw new Error("未返回有效回复文本");
295
+ return { id, text };
296
+ }
297
+ catch (error) {
298
+ throw new Error(`CLI 输出或会话无效:${diagnostic(String(error))}`);
299
+ }
300
+ }
301
+ async controlSession(request) {
302
+ this.requireReady();
303
+ return this.serial(request.sessionId, async () => {
304
+ this.requireReady();
305
+ await this.save(request.sessionId);
306
+ return { text: "已新建会话,下条消息将使用新的 CLI 会话", sessionId: (0, node_crypto_1.randomUUID)() };
307
+ });
308
+ }
309
+ async health() {
310
+ return { status: this.ready ? "ready" : "failed", ...(this.message ? { message: this.message } : {}), details: { executionMode: "cli", version: diagnostic(this.version), activeRequests: this.children.size } };
311
+ }
312
+ async deactivate(_reason) {
313
+ this.ready = false;
314
+ this.message = "插件已停止";
315
+ const children = [...this.children.values()];
316
+ children.forEach(child => child.stop());
317
+ await Promise.allSettled([...children.map(child => child.done), ...this.pending.values()]);
318
+ await this.writes.catch(() => { });
319
+ }
320
+ }
321
+ exports.LocalCliAgent = LocalCliAgent;
@@ -91,7 +91,9 @@ class WorkbuddyCdpClient {
91
91
  pending.reject(new Error("WorkBuddy CDP 已关闭")); this.pending.clear(); }
92
92
  async createTask() {
93
93
  const snapshot = `(() => { const selected = [...document.querySelectorAll('[data-conversation-id]')].find((item) => String(item.className).includes('_selected_')); const editor = document.querySelector(${JSON.stringify(exports.editorSelector)}); const text = editor?.cloneNode(true); text?.querySelectorAll('[data-slate-placeholder="true"],[data-slate-zero-width]').forEach((item) => item.remove()); return { url: location.href, conversationId: selected?.getAttribute('data-conversation-id') ?? null, selected: Boolean(selected), messages: document.querySelectorAll('.cr-agent[data-message-id]').length, editor: text?.textContent?.replace(/[\\uFEFF\\u200B]/g, '').trim() ?? null, home: Boolean(editor?.querySelector('[data-slate-placeholder="true"]')) }; })()`;
94
- const before = await this.evaluate(snapshot);
94
+ const before = await this.evaluate(`(() => { const state = ${snapshot}; const loginRequired = [...document.querySelectorAll('a,button,[role="button"]')].some(item => /^(?:登录|立即登录|sign in)$/i.test(item.textContent?.trim() ?? '')); return { ...state, loginRequired }; })()`);
95
+ if (before.loginRequired)
96
+ throw new Error("WorkBuddy 尚未登录,请先在桌面端登录后重试");
95
97
  if (!await this.evaluate(`(() => { const button = [...document.querySelectorAll('button,[role="button"]')].find((item) => item.textContent?.trim() === '新建任务'); if (!button || button.disabled || button.getAttribute('aria-disabled') === 'true') return false; button.click(); return true; })()`))
96
98
  throw new Error("WorkBuddy 新建任务入口未找到或不可用");
97
99
  // WorkBuddy 首页不分配任务 ID;此时仅接受无选中任务的空白首页。
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.launchWorkbuddyDesktop = launchWorkbuddyDesktop;
4
+ const node_child_process_1 = require("node:child_process");
5
+ function launchWorkbuddyDesktop(executable, args, profileDirectory, port) {
6
+ // WorkBuddy 会覆盖 Chromium 的用户目录参数,必须使用它自己的环境变量隔离实例。
7
+ const env = { ...process.env, WORKBUDDY_USER_DATA_DIR: profileDirectory, WORKBUDDY_REMOTE_DEBUGGING_PORT: String(port) };
8
+ delete env.ELECTRON_RUN_AS_NODE;
9
+ return (0, node_child_process_1.spawn)(executable, args, { env, detached: true, stdio: "ignore", windowsHide: false });
10
+ }
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WorkbuddyPlugin = exports.workbuddyManifest = void 0;
4
4
  exports.extractExpertQuery = extractExpertQuery;
5
5
  exports.findExecutable = findExecutable;
6
- const node_child_process_1 = require("node:child_process");
7
6
  const node_fs_1 = require("node:fs");
8
7
  const promises_1 = require("node:fs/promises");
9
8
  const node_os_1 = require("node:os");
@@ -13,6 +12,8 @@ const plugin_1 = require("../../plugin");
13
12
  const cdp_client_1 = require("./cdp-client");
14
13
  const cli_client_1 = require("./cli-client");
15
14
  const session_history_1 = require("./session-history");
15
+ const desktop_launch_1 = require("../desktop-launch");
16
+ const desktop_launch_2 = require("./desktop-launch");
16
17
  exports.workbuddyManifest = {
17
18
  id: "workbuddy", version: "1.1.13", displayName: "WorkBuddy", type: "agent",
18
19
  entry: "dist/index.js", apiVersion: "1", process: "in-process",
@@ -28,13 +29,19 @@ function findExecutable(configured) {
28
29
  if (configured)
29
30
  return (0, node_fs_1.existsSync)((0, node_path_1.resolve)(configured)) ? (0, node_path_1.resolve)(configured) : undefined;
30
31
  const local = process.env.LOCALAPPDATA ?? (0, node_path_1.join)((0, node_os_1.homedir)(), "AppData", "Local");
31
- return [(0, node_path_1.join)(local, "Programs", "WorkBuddy", "WorkBuddy.exe"), "D:\\Programs\\WorkBuddy\\WorkBuddy.exe"].find(node_fs_1.existsSync);
32
+ return [
33
+ (0, node_path_1.join)(local, "Programs", "WorkBuddy", "WorkBuddy.exe"),
34
+ (0, node_path_1.join)(local, "Programs", "WorkBuddyAI", "WorkBuddyAI.exe"),
35
+ "D:\\Programs\\WorkBuddy\\WorkBuddy.exe",
36
+ "D:\\Programs\\WorkBuddyAI\\WorkBuddyAI.exe",
37
+ ].find(node_fs_1.existsSync);
32
38
  }
33
39
  class WorkbuddyPlugin {
34
40
  options;
35
41
  manifest = exports.workbuddyManifest;
36
42
  client;
37
43
  process;
44
+ reconnect;
38
45
  cliPath;
39
46
  model;
40
47
  transport = "auto";
@@ -106,7 +113,7 @@ class WorkbuddyPlugin {
106
113
  const port = (0, plugin_1.resolveRuntimePort)(config, context.runtime, "workbuddy", "WORKBUDDY_CDP_PORT", this.options.cdpPort ?? 9229);
107
114
  this.client = this.options.client ?? new cdp_client_1.WorkbuddyCdpClient({ port });
108
115
  try {
109
- await this.connectOrStart(config, context.logger, port);
116
+ await this.connectOrStart(config, context.logger, port, context.dataDir);
110
117
  }
111
118
  catch (error) {
112
119
  this.failure = error instanceof Error ? error.message : String(error);
@@ -125,10 +132,11 @@ class WorkbuddyPlugin {
125
132
  async deactivate(_reason) {
126
133
  await this.queue;
127
134
  await this.sessionWrites.catch(() => { });
135
+ await (0, desktop_launch_1.terminateOwnedProcess)(this.process).catch(() => undefined);
128
136
  this.client?.close();
129
137
  this.client = undefined;
130
- // WorkBuddy 是用户桌面应用,插件停止时不结束桌面进程。
131
138
  this.process = undefined;
139
+ this.reconnect = undefined;
132
140
  }
133
141
  async health() {
134
142
  if (this.client?.connected())
@@ -165,6 +173,8 @@ class WorkbuddyPlugin {
165
173
  throw new Error("WorkBuddy 新会话缺少 sessionId");
166
174
  if (this.transport === "cli" || (this.transport !== "cdp" && this.cliPath) || !this.client?.createTask)
167
175
  throw new Error("WorkBuddy 当前传输不支持新建会话");
176
+ if (!this.client.connected())
177
+ await this.reconnect?.();
168
178
  if (!this.client.connected())
169
179
  throw new Error("WorkBuddy CDP 未连接");
170
180
  return this.enqueue(async () => {
@@ -181,6 +191,8 @@ class WorkbuddyPlugin {
181
191
  const hasAttachments = Array.isArray(request.attachments) && request.attachments.length > 0;
182
192
  if (hasAttachments && !this.attachmentInput)
183
193
  throw new Error("WorkBuddy 当前传输不支持附件输入");
194
+ if (!usesCli && !this.client?.connected())
195
+ await this.reconnect?.();
184
196
  const pendingInteractionSessionId = this.client?.pendingInteractionSessionId?.();
185
197
  if (!usesCli && pendingInteractionSessionId && pendingInteractionSessionId !== request.sessionId)
186
198
  throw new Error("WorkBuddy 正在等待另一个会话的选择");
@@ -303,7 +315,24 @@ class WorkbuddyPlugin {
303
315
  this.queue = run.then(() => { this.pending--; }, () => { this.pending--; });
304
316
  return run;
305
317
  }
306
- async connectOrStart(config, logger, port) {
318
+ async connectOrStart(config, logger, port, dataDir) {
319
+ const executable = findExecutable(typeof config.executablePath === "string" ? config.executablePath : this.options.executablePath);
320
+ const profile = (0, node_path_1.join)(dataDir ?? (0, node_path_1.join)((0, node_os_1.homedir)(), ".agent-bridge", "workbuddy"), "desktop");
321
+ const launch = (file, args) => (0, desktop_launch_2.launchWorkbuddyDesktop)(file, args, profile, port);
322
+ const timeoutMs = typeof config.startupTimeoutMs === "number" ? config.startupTimeoutMs : 30_000;
323
+ this.reconnect = async () => {
324
+ try {
325
+ await this.client.connect();
326
+ return;
327
+ }
328
+ catch (first) {
329
+ if (config.autoStart === false || this.options.client || !executable)
330
+ throw first;
331
+ await (0, desktop_launch_1.terminateOwnedProcess)(this.process);
332
+ this.process = undefined;
333
+ await (0, desktop_launch_1.launchManagedAndWait)(executable, [`--remote-debugging-port=${port}`], profile, () => this.client.connect(), timeoutMs, child => { this.process = child; }, launch);
334
+ }
335
+ };
307
336
  try {
308
337
  await this.client.connect();
309
338
  return;
@@ -311,25 +340,10 @@ class WorkbuddyPlugin {
311
340
  catch (first) {
312
341
  if (config.autoStart === false || this.options.client)
313
342
  throw first;
314
- const executable = findExecutable(typeof config.executablePath === "string" ? config.executablePath : this.options.executablePath);
315
343
  if (!executable)
316
344
  throw new Error("未找到 WorkBuddy.exe,请配置 executablePath");
317
- this.process = (0, node_child_process_1.spawn)(executable, [`--remote-debugging-port=${port}`], { detached: true, stdio: "ignore", windowsHide: false });
318
- this.process.unref();
319
345
  logger.info(`WorkBuddy 已启动,等待 CDP ${port}`);
320
- const deadline = Date.now() + (typeof config.startupTimeoutMs === "number" ? config.startupTimeoutMs : 30_000);
321
- let last = first;
322
- while (Date.now() < deadline) {
323
- await new Promise((resolve) => setTimeout(resolve, 500));
324
- try {
325
- await this.client.connect();
326
- return;
327
- }
328
- catch (error) {
329
- last = error;
330
- }
331
- }
332
- throw last instanceof Error ? last : new Error("WorkBuddy CDP 启动超时");
346
+ await (0, desktop_launch_1.launchManagedAndWait)(executable, [`--remote-debugging-port=${port}`], profile, () => this.client.connect(), timeoutMs, child => { this.process = child; }, launch);
333
347
  }
334
348
  }
335
349
  }
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const strict_1 = __importDefault(require("node:assert/strict"));
7
+ const promises_1 = require("node:fs/promises");
8
+ const node_vm_1 = require("node:vm");
9
+ const node_test_1 = __importDefault(require("node:test"));
10
+ class Element {
11
+ hidden = false;
12
+ open = false;
13
+ value = "";
14
+ textContent = "";
15
+ children = [];
16
+ options = [];
17
+ selectedIndex = 0;
18
+ onclick;
19
+ onsubmit;
20
+ append(...children) { this.children.push(...children); }
21
+ replaceChildren(...children) { this.children = children; }
22
+ setAttribute() { }
23
+ showModal() { this.open = true; }
24
+ close() { this.open = false; }
25
+ }
26
+ (0, node_test_1.default)("abweb CLI 配置入口读取、保存配置且保留通道关联", async () => {
27
+ const nodes = new Map();
28
+ const node = (selector) => { if (!nodes.has(selector))
29
+ nodes.set(selector, new Element()); return nodes.get(selector); };
30
+ let saved;
31
+ (0, node_vm_1.runInNewContext)(await (0, promises_1.readFile)("src-ts/plugins/abweb/public/app.js", "utf8"), {
32
+ document: { querySelector: node, createElement: () => new Element(), hidden: false },
33
+ localStorage: { getItem: () => null, removeItem() { } }, window: { addEventListener() { } },
34
+ structuredClone, setInterval() { }, confirm: () => false,
35
+ fetch: async (path, options) => {
36
+ let data = {};
37
+ if (path === "/api/plugins")
38
+ data = { plugins: [{ id: "codex-cli", displayName: "Codex CLI", type: "agent", version: "1.0.0", status: "installed", path: "plugins/codex-cli", manifest: { executionMode: "cli" } }] };
39
+ if (path.endsWith("/config")) {
40
+ if (options.method === "PATCH")
41
+ saved = JSON.parse(options.body);
42
+ data = { cwd: "/原目录", aid: "existing.agentid.cn" };
43
+ }
44
+ return { ok: true, status: 200, json: async () => data };
45
+ },
46
+ });
47
+ await new Promise(resolve => setImmediate(resolve));
48
+ const row = node("#plugins").children[0];
49
+ strict_1.default.match(row.children[1].textContent, /CLI/);
50
+ const configure = row.children.at(-1).children.find(child => child.textContent === "配置");
51
+ strict_1.default.ok(configure, "CLI 插件必须提供配置按钮");
52
+ await configure.onclick();
53
+ await new Promise(resolve => setImmediate(resolve));
54
+ strict_1.default.equal(node("#cli-config-cwd").value, "/原目录");
55
+ node("#cli-config-cwd").value = "/新目录";
56
+ node("#cli-config-executable").value = "/路径 空格/codex";
57
+ node("#cli-config-timeout").value = "240000";
58
+ node("#cli-config-form").onsubmit({ preventDefault() { } });
59
+ await new Promise(resolve => setImmediate(resolve));
60
+ strict_1.default.equal(saved.cwd, "/新目录");
61
+ strict_1.default.equal(saved.executablePath, "/路径 空格/codex");
62
+ strict_1.default.equal(saved.timeoutMs, 240000);
63
+ strict_1.default.equal(saved.aid, undefined, "通过 PATCH 合并配置,不能改写通道关联 AID");
64
+ strict_1.default.equal(node("#cli-config-dialog").open, false);
65
+ });
@@ -9,19 +9,33 @@ 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
11
  const agent_availability_1 = require("../src-ts/agent-availability");
12
+ const workbuddy_1 = require("../src-ts/plugins/workbuddy");
12
13
  (0, node_test_1.default)("办公 Agent 根据可执行文件和就绪状态决定是否可选", async () => {
13
14
  const dir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "office-agent-"));
15
+ const previousLocalAppData = process.env.LOCALAPPDATA;
14
16
  try {
15
17
  const file = (0, node_path_1.join)(dir, "WorkBuddy.exe");
18
+ const dshFile = (0, node_path_1.join)(dir, "DeepSeek Harness.exe");
16
19
  strict_1.default.deepEqual(await (0, agent_availability_1.agentAvailability)("workbuddy", { executablePath: file }, "ready"), { available: false, reason: "未找到可执行文件" });
17
20
  await (0, promises_1.writeFile)(file, "测试程序");
18
21
  strict_1.default.deepEqual(await (0, agent_availability_1.agentAvailability)("workbuddy", { executablePath: file }, "ready"), { available: true, reason: "" });
22
+ const workbuddyAi = (0, node_path_1.join)(dir, "Programs", "WorkBuddyAI", "WorkBuddyAI.exe");
23
+ await (0, promises_1.mkdir)((0, node_path_1.join)(dir, "Programs", "WorkBuddyAI"), { recursive: true });
24
+ await (0, promises_1.writeFile)(workbuddyAi, "测试程序");
25
+ process.env.LOCALAPPDATA = dir;
26
+ strict_1.default.equal((0, workbuddy_1.findExecutable)(), workbuddyAi);
19
27
  strict_1.default.equal((await (0, agent_availability_1.agentAvailability)("workbuddy", { executablePath: file }, "failed")).available, false);
20
28
  strict_1.default.equal((await (0, agent_availability_1.agentAvailability)("qwenwork", { executablePath: (0, node_path_1.join)(dir, "missing.exe") }, "ready")).available, false);
21
29
  strict_1.default.equal((await (0, agent_availability_1.agentAvailability)("doubao", { executablePath: file }, "ready")).available, true);
22
- strict_1.default.equal((await (0, agent_availability_1.agentAvailability)("dsh", {}, "ready")).available, true);
30
+ strict_1.default.deepEqual(await (0, agent_availability_1.agentAvailability)("dsh", {}, "ready"), { available: false, reason: "未找到可执行文件" });
31
+ await (0, promises_1.writeFile)(dshFile, "测试程序");
32
+ strict_1.default.deepEqual(await (0, agent_availability_1.agentAvailability)("dsh", { executablePath: dshFile }, "ready"), { available: true, reason: "" });
23
33
  }
24
34
  finally {
35
+ if (previousLocalAppData === undefined)
36
+ delete process.env.LOCALAPPDATA;
37
+ else
38
+ process.env.LOCALAPPDATA = previousLocalAppData;
25
39
  await (0, promises_1.rm)(dir, { recursive: true, force: true });
26
40
  }
27
41
  });
@@ -32,7 +32,7 @@ const flush = () => new Promise((resolve) => setImmediate(resolve));
32
32
  nodes.set(selector, new Element());
33
33
  return nodes.get(selector);
34
34
  };
35
- for (const [selector, keys] of [["#create-form", ["aid", "ownerAid"]], ["#config-form", ["agent_plugin", "allow_from", "aun_path", "encryption_seed"]]]) {
35
+ for (const [selector, keys] of [["#create-form", ["aid", "ownerAid"]], ["#config-form", ["allow_from", "aun_path", "encryption_seed"]]]) {
36
36
  for (const key of keys)
37
37
  node(selector).elements[key] = new Element();
38
38
  }