mu-agent-bridge 1.0.42 → 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 (47) hide show
  1. package/README.md +4 -0
  2. package/dist/src-ts/headless-account.patch.yml +10 -0
  3. package/dist/src-ts/plugin-manifest.js +3 -1
  4. package/dist/src-ts/plugins/abweb/public/app.js +34 -0
  5. package/dist/src-ts/plugins/abweb/public/index.html +10 -0
  6. package/dist/src-ts/plugins/abweb/public/style.css +3 -0
  7. package/dist/src-ts/plugins/aun/index.js +5 -5
  8. package/dist/src-ts/plugins/aun/public/app.js +1 -4
  9. package/dist/src-ts/plugins/aun/public/index.html +1 -1
  10. package/dist/src-ts/plugins/claude-cli/index.js +14 -0
  11. package/dist/src-ts/plugins/codex-cli/index.js +14 -0
  12. package/dist/src-ts/plugins/local-cli/agent.js +321 -0
  13. package/dist/src-ts/plugins/workbuddy/desktop-launch.js +10 -0
  14. package/dist/src-ts/plugins/workbuddy/index.js +9 -6
  15. package/dist/tests-ts/abweb-cli.test.js +65 -0
  16. package/dist/tests-ts/aun-default-owner-page.test.js +1 -1
  17. package/dist/tests-ts/aun-plugin.test.js +16 -6
  18. package/dist/tests-ts/bundled-package.test.js +1 -1
  19. package/dist/tests-ts/local-cli.test.js +170 -0
  20. package/dist/tests-ts/platform-integration.test.js +1 -1
  21. package/dist/tests-ts/workbuddy-plugin.test.js +56 -0
  22. package/package.json +1 -1
  23. package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.js +34 -0
  24. package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.ts +30 -1
  25. package/plugins/abweb/dist/src-ts/plugins/abweb/public/index.html +10 -0
  26. package/plugins/abweb/dist/src-ts/plugins/abweb/public/style.css +3 -0
  27. package/plugins/aun/config.schema.json +1 -1
  28. package/plugins/aun/dist/src-ts/plugins/aun/index.js +5 -5
  29. package/plugins/aun/dist/src-ts/plugins/aun/public/app.js +1 -4
  30. package/plugins/aun/dist/src-ts/plugins/aun/public/index.html +1 -1
  31. package/plugins/claude-cli/config.schema.json +37 -0
  32. package/plugins/claude-cli/dist/src-ts/agent.js +10 -0
  33. package/plugins/claude-cli/dist/src-ts/plugin.js +13 -0
  34. package/plugins/claude-cli/dist/src-ts/plugins/claude-cli/index.js +14 -0
  35. package/plugins/claude-cli/dist/src-ts/plugins/local-cli/agent.js +321 -0
  36. package/plugins/claude-cli/package.json +35 -0
  37. package/plugins/claude-cli/plugin.json +15 -0
  38. package/plugins/codex-cli/config.schema.json +37 -0
  39. package/plugins/codex-cli/dist/src-ts/agent.js +10 -0
  40. package/plugins/codex-cli/dist/src-ts/plugin.js +13 -0
  41. package/plugins/codex-cli/dist/src-ts/plugins/codex-cli/index.js +14 -0
  42. package/plugins/codex-cli/dist/src-ts/plugins/local-cli/agent.js +321 -0
  43. package/plugins/codex-cli/package.json +35 -0
  44. package/plugins/codex-cli/plugin.json +15 -0
  45. package/plugins/dsh/dist/src-ts/headless-account.patch.yml +10 -0
  46. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/desktop-launch.js +10 -0
  47. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +9 -6
package/README.md CHANGED
@@ -162,3 +162,7 @@ mab feishu unbind cli_你的应用ID
162
162
  ```
163
163
 
164
164
  `configure` 保存凭据及可选关联后,需要在 ABWeb 重载飞书插件以连接机器人。非交互终端请使用网页密码框;不要把 Secret 写进命令行、脚本或仓库。
165
+
166
+ ### 本地 CLI Agent
167
+
168
+ 新增两个独立插件:`agent-bridge-codex-cli`(Codex CLI)与 `agent-bridge-claude-cli`(Claude Code CLI),随核心默认打包。支持在 abweb 配置、启停及通过通道关联 AID,沿用本机 CLI 的登录和默认模型。详见 [本地 CLI 插件使用说明](docs/local-cli-agents.md)。
@@ -1,4 +1,14 @@
1
1
  # 复用 DeepSeek Harness 桌面端登录账号的模型。
2
+ # 新版 headless 默认禁用凭据及账号服务,桥接需要显式恢复才能复用桌面登录。
3
+ - id: credentials
4
+ disabled: false
5
+
6
+ - id: authorization
7
+ disabled: false
8
+
9
+ - id: deepseek-account
10
+ disabled: false
11
+
2
12
  - id: agent-default-model
3
13
  config:
4
14
  provider: deepseek-account
@@ -21,6 +21,8 @@ function validateManifest(value, baseDir) {
21
21
  throw new agent_1.AgentError("invalid plugin capabilities");
22
22
  if (m.type && !["agent", "channel", "management", "other"].includes(m.type))
23
23
  throw new agent_1.AgentError("invalid plugin type");
24
+ if (m.executionMode !== undefined && m.executionMode !== "cli" && m.executionMode !== "cdp")
25
+ throw new agent_1.AgentError("插件运行方式无效");
24
26
  const inferredType = m.capabilities.includes("channel") ? "channel" : m.capabilities.includes("chat") ? "agent" : m.capabilities.includes("management") ? "management" : "other";
25
27
  if (m.type && inferredType !== "other" && m.type !== inferredType)
26
28
  throw new agent_1.AgentError("插件类型与能力不匹配");
@@ -39,5 +41,5 @@ function validateManifest(value, baseDir) {
39
41
  if ((0, node_path_1.isAbsolute)(m.entry) || rel.startsWith(`..${node_path_1.sep}`) || rel === "..")
40
42
  throw new agent_1.AgentError("plugin entry path escapes plugin directory");
41
43
  }
42
- return { id: m.id, version: m.version, displayName: m.displayName ?? m.id, type: m.type ?? inferredType, entry: m.entry, apiVersion: m.apiVersion, process: m.process ?? "in-process", capabilities: [...m.capabilities], configSchema: m.configSchema, routes: m.routes, ...(m.dependencies ? { dependencies: { ...m.dependencies } } : {}), ...(m.managementPage ? { managementPage: m.managementPage } : {}), ...(m.clientDownloadUrl ? { clientDownloadUrl: m.clientDownloadUrl } : {}) };
44
+ return { id: m.id, version: m.version, displayName: m.displayName ?? m.id, type: m.type ?? inferredType, entry: m.entry, apiVersion: m.apiVersion, process: m.process ?? "in-process", capabilities: [...m.capabilities], ...(m.executionMode ? { executionMode: m.executionMode } : {}), configSchema: m.configSchema, routes: m.routes, ...(m.dependencies ? { dependencies: { ...m.dependencies } } : {}), ...(m.managementPage ? { managementPage: m.managementPage } : {}), ...(m.clientDownloadUrl ? { clientDownloadUrl: m.clientDownloadUrl } : {}) };
43
45
  }
@@ -308,6 +308,8 @@
308
308
  const status = document.createElement("span");
309
309
  status.className = `status status-${plugin.status}`;
310
310
  status.textContent = plugin.status;
311
+ if (plugin.type === "agent")
312
+ type.textContent += ` · ${{ cli: "CLI", cdp: "CDP" }[plugin.manifest?.executionMode ?? ""] ?? "未声明"}`;
311
313
  const dependencies = Object.entries(plugin.manifest?.dependencies ?? {});
312
314
  if (dependencies.length) {
313
315
  const detail = document.createElement("small");
@@ -340,6 +342,8 @@
340
342
  }
341
343
  const controls = document.createElement("td");
342
344
  controls.className = "controls";
345
+ if (plugin.manifest?.executionMode === "cli")
346
+ controls.append(button("配置", `配置 ${plugin.id}`, () => configureCli(plugin)));
343
347
  if (pluginVersions[plugin.id]?.state === "update-available" && plugin.path.includes("node_modules")) {
344
348
  controlsUpgrade(plugin.id);
345
349
  controls.append(button("下载", `下载 ${plugin.id}`, async () => { await request(`/api/plugins/${encodeURIComponent(plugin.id)}/download`, "POST", { packageName: plugin.packageName, version: pluginVersions[plugin.id]?.latestVersion }); await load(); }));
@@ -367,6 +371,36 @@
367
371
  pluginsNode.append(row);
368
372
  }
369
373
  }
374
+ let configuringCli = "";
375
+ async function configureCli(plugin) {
376
+ const config = await request(`/api/plugins/${encodeURIComponent(plugin.id)}/config`);
377
+ configuringCli = plugin.id;
378
+ node("#cli-config-title").textContent = `${plugin.displayName} 配置`;
379
+ node("#cli-config-cwd").value = config.cwd ?? "";
380
+ node("#cli-config-executable").value = config.executablePath ?? "";
381
+ node("#cli-config-timeout").value = String(config.timeoutMs ?? 240000);
382
+ node("#cli-config-error").hidden = true;
383
+ node("#cli-config-dialog").showModal();
384
+ }
385
+ node("#cli-config-cancel").onclick = () => node("#cli-config-dialog").close();
386
+ node("#cli-config-form").onsubmit = event => {
387
+ event.preventDefault();
388
+ const save = node("#cli-config-save");
389
+ save.disabled = true;
390
+ void request(`/api/plugins/${encodeURIComponent(configuringCli)}/config`, "PATCH", {
391
+ cwd: node("#cli-config-cwd").value.trim(),
392
+ executablePath: node("#cli-config-executable").value.trim(),
393
+ timeoutMs: Number(node("#cli-config-timeout").value),
394
+ }).then(() => {
395
+ node("#cli-config-dialog").close();
396
+ showResult("CLI 配置已保存", "请启动插件,然后在 Channel 设置中关联 AID。", false);
397
+ return load(false);
398
+ }).catch(cause => {
399
+ const detail = node("#cli-config-error");
400
+ detail.textContent = String(cause);
401
+ detail.hidden = false;
402
+ }).finally(() => { save.disabled = false; });
403
+ };
370
404
  function renderMessages(messages) {
371
405
  messagesNode.replaceChildren();
372
406
  messagesEmpty.hidden = messages.length > 0;
@@ -21,6 +21,16 @@
21
21
  <div id="logs-panel" role="tabpanel" aria-labelledby="logs-tab" hidden><section><div class="section-heading"><h2>运行日志</h2><button id="logs-refresh" type="button">刷新日志</button></div><div class="table-scroll"><table><thead><tr><th>时间</th><th>级别</th><th>来源</th><th>消息</th><th>详细字段</th></tr></thead><tbody id="logs"></tbody></table></div><p id="logs-empty" class="muted" hidden>暂无运行日志</p><div class="section-heading"><button id="logs-prev" type="button">上一页</button><span id="logs-page" class="muted">第 1 页</span><button id="logs-next" type="button">下一页</button></div></section></div>
22
22
  <p id="error" class="error" role="alert" hidden></p>
23
23
  </main>
24
+ <dialog id="cli-config-dialog" aria-labelledby="cli-config-title">
25
+ <h2 id="cli-config-title">CLI 配置</h2><p class="muted">沿用后台运行用户的 CLI 登录、默认模型及权限。AID 请在 Channel 设置关联。</p>
26
+ <form id="cli-config-form">
27
+ <label for="cli-config-cwd">工作目录(必填,绝对路径)</label><input id="cli-config-cwd" required placeholder="C:\Users\用户名\projects\demo">
28
+ <label for="cli-config-executable">CLI 程序路径(可选,留空使用 PATH)</label><input id="cli-config-executable" placeholder="例如 C:\Users\用户名\AppData\Roaming\npm\codex.cmd">
29
+ <label for="cli-config-timeout">任务超时(毫秒)</label><input id="cli-config-timeout" type="number" min="1" max="2147483647" required value="240000">
30
+ <p id="cli-config-error" class="error" role="alert" hidden></p>
31
+ <div><button id="cli-config-save" type="submit">保存配置</button><button id="cli-config-cancel" type="button">取消</button></div>
32
+ </form>
33
+ </dialog>
24
34
  <dialog id="operation-result" aria-labelledby="operation-result-title" aria-describedby="operation-result-message">
25
35
  <h2 id="operation-result-title">操作结果</h2><p id="operation-result-message"></p><button id="operation-result-close" type="button" autofocus>确定</button>
26
36
  </dialog>
@@ -60,3 +60,6 @@ th:nth-child(1) { width: 145px; } th:nth-child(2) { width: 100px; } th:nth-child
60
60
  #operation-result { width: min(520px, calc(100% - 32px)); border: 1px solid #bfc9c5; border-radius: 8px; padding: 24px; }
61
61
  #operation-result::backdrop { background: rgb(0 0 0 / 35%); }
62
62
  #operation-result-message { white-space: pre-wrap; overflow-wrap: anywhere; margin: 16px 0; }
63
+
64
+ #cli-config-form { display: grid; min-width: min(70vw, 480px); }
65
+ #cli-config-form input { width: 100%; }
@@ -214,7 +214,7 @@ class AunPlugin {
214
214
  if (typeof aid === "string" && aidPattern.test(aid))
215
215
  this.createdAids.add(aid);
216
216
  }
217
- for (const key of ["aid", "owner_aid", "aun_path", "encryption_seed", "root_ca_path", "allow_from", "agent_plugin"]) {
217
+ for (const key of ["aid", "owner_aid", "aun_path", "encryption_seed", "root_ca_path", "allow_from"]) {
218
218
  if (this.config[key] !== undefined && typeof this.config[key] !== "string")
219
219
  throw new Error(`${key} 必须是字符串`);
220
220
  }
@@ -419,12 +419,12 @@ class AunPlugin {
419
419
  throw new Error("请求内容过大");
420
420
  }
421
421
  const body = JSON.parse(raw || "{}");
422
- for (const key of ["agent_plugin", "allow_from", "aun_path"])
422
+ for (const key of ["allow_from", "aun_path"])
423
423
  if (typeof body[key] !== "string")
424
424
  return this.json(response, 400, { error: `${key} 必须是字符串` });
425
425
  if (body.encryption_seed !== undefined && typeof body.encryption_seed !== "string")
426
426
  return this.json(response, 400, { error: "encryption_seed 必须是字符串" });
427
- this.config = { ...this.config, agent_plugin: body.agent_plugin, allow_from: body.allow_from, aun_path: body.aun_path, ...(body.encryption_seed ? { encryption_seed: body.encryption_seed } : {}) };
427
+ this.config = { ...this.config, allow_from: body.allow_from, aun_path: body.aun_path, ...(body.encryption_seed ? { encryption_seed: body.encryption_seed } : {}) };
428
428
  await this.persist();
429
429
  this.context?.logger.info("AUN 通道配置已保存");
430
430
  this.json(response, 200, { ok: true });
@@ -741,7 +741,7 @@ class AunPlugin {
741
741
  }
742
742
  };
743
743
  async onMessage(payload, aid) {
744
- if (!this.acceptingMessages)
744
+ if (!this.acceptingMessages || !this.bindings[aid])
745
745
  return;
746
746
  const message = (0, messages_1.normalizeAunMessage)(payload, aid, typeof this.config.allow_from === "string" ? this.config.allow_from : undefined);
747
747
  if (!message || !this.seen.add(message.messageId ? `${aid}:${message.messageId}` : ""))
@@ -793,7 +793,7 @@ class AunPlugin {
793
793
  const channelMessage = {
794
794
  channel: "aun", recipientId: aid, peerId: message.peerId, messageId: message.messageId, text: message.text, threadId: message.threadId,
795
795
  ...(attachments.length ? { attachments } : {}),
796
- agentPlugin: this.bindings[aid] || (typeof this.config.agent_plugin === "string" && this.config.agent_plugin.trim() ? this.config.agent_plugin.trim() : "qwenwork"),
796
+ agentPlugin: this.bindings[aid],
797
797
  reply: async (text) => {
798
798
  if (!text.trim())
799
799
  return;
@@ -29,10 +29,7 @@
29
29
  agents = (plugins.plugins || []).filter((plugin) => plugin.type === "agent");
30
30
  if (refreshForm) createForm.elements.ownerAid.value = config.owner_aid || "";
31
31
  await Promise.all(agents.map(async (agent) => { agent.configAid = (await request(`/api/plugins/${encodeURIComponent(agent.id)}/config`)).aid || ""; }));
32
- const target = configForm.elements.agent_plugin;
33
32
  if (refreshForm) {
34
- target.replaceChildren(new Option("默认 Agent", ""), ...agents.map((agent) => new Option(agent.displayName || agent.id, agent.id)));
35
- target.value = config.agent_plugin || "";
36
33
  for (const key of ["allow_from", "aun_path"]) configForm.elements[key].value = config[key] || "";
37
34
  configForm.elements.encryption_seed.value = "";
38
35
  }
@@ -76,7 +73,7 @@
76
73
  return load();
77
74
  }).catch((error) => notify("创建 AID 失败", errorMessage(error), true));
78
75
  };
79
- configForm.onsubmit = (event) => { event.preventDefault(); const changes = { agent_plugin: configForm.elements.agent_plugin.value, allow_from: configForm.elements.allow_from.value, aun_path: configForm.elements.aun_path.value };
76
+ configForm.onsubmit = (event) => { event.preventDefault(); const changes = { allow_from: configForm.elements.allow_from.value, aun_path: configForm.elements.aun_path.value };
80
77
  if (configForm.elements.encryption_seed.value) changes.encryption_seed = configForm.elements.encryption_seed.value;
81
78
  void request("/api/plugins/aun/settings", "POST", changes).then(() => request("/api/plugins/aun/reload", "POST")).then(() => load()).then(() => notify("保存成功", "配置已保存")).catch((error) => notify("保存配置失败", errorMessage(error), true));
82
79
  };
@@ -7,7 +7,7 @@
7
7
  <p id="status" role="status"></p>
8
8
  <div class="table-scroll"><table><thead><tr><th>AID</th><th>来源</th><th>已关联 Agent</th><th>操作</th></tr></thead><tbody id="identities"></tbody></table></div>
9
9
  <form id="create-form"><h2>创建 AID</h2><div class="fields"><label>新 AID<input name="aid" required placeholder="例如:example.agentid.cn"></label><label>Owner AID<input name="ownerAid" placeholder="例如:owner.agentid.cn"><small class="hint">留空时使用已配置的默认 Owner;未配置时需填写。Owner 保存为本地归属配置;不代表 AUN 协议中的 Owner 关系已激活。</small></label></div><button type="submit">创建</button></form>
10
- <form id="config-form"><h2>通道配置</h2><div class="fields"><label>默认 Agent<select name="agent_plugin"></select></label><label>允许的来源<input name="allow_from"></label><label>AUN 存储路径<input name="aun_path"></label><label>加密种子<input name="encryption_seed" type="password" autocomplete="new-password" placeholder="留空则不修改"></label></div><button type="submit">保存配置</button></form>
10
+ <form id="config-form"><h2>通道配置</h2><div class="fields"><label>允许的来源<input name="allow_from"></label><label>AUN 存储路径<input name="aun_path"></label><label>加密种子<input name="encryption_seed" type="password" autocomplete="new-password" placeholder="留空则不修改"></label></div><button type="submit">保存配置</button></form>
11
11
  </main>
12
12
  <script src="/abweb/channels/aun/app.js"></script>
13
13
  </body>
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ClaudeCliPlugin = exports.manifest = void 0;
4
+ const agent_1 = require("../local-cli/agent");
5
+ exports.manifest = {
6
+ id: "claude-cli", version: "1.0.0", displayName: "Claude Code CLI", type: "agent", executionMode: "cli",
7
+ entry: "dist/src-ts/plugins/claude-cli/index.js", apiVersion: "1", process: "in-process",
8
+ capabilities: ["chat", "health"], configSchema: "config.schema.json",
9
+ };
10
+ class ClaudeCliPlugin extends agent_1.LocalCliAgent {
11
+ constructor() { super(exports.manifest, "claude"); }
12
+ }
13
+ exports.ClaudeCliPlugin = ClaudeCliPlugin;
14
+ exports.default = ClaudeCliPlugin;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CodexCliPlugin = exports.manifest = void 0;
4
+ const agent_1 = require("../local-cli/agent");
5
+ exports.manifest = {
6
+ id: "codex-cli", version: "1.0.0", displayName: "Codex CLI", type: "agent", executionMode: "cli",
7
+ entry: "dist/src-ts/plugins/codex-cli/index.js", apiVersion: "1", process: "in-process",
8
+ capabilities: ["chat", "health"], configSchema: "config.schema.json",
9
+ };
10
+ class CodexCliPlugin extends agent_1.LocalCliAgent {
11
+ constructor() { super(exports.manifest, "codex"); }
12
+ }
13
+ exports.CodexCliPlugin = CodexCliPlugin;
14
+ exports.default = CodexCliPlugin;
@@ -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;
@@ -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
+ }
@@ -13,6 +13,7 @@ const cdp_client_1 = require("./cdp-client");
13
13
  const cli_client_1 = require("./cli-client");
14
14
  const session_history_1 = require("./session-history");
15
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",
@@ -112,7 +113,7 @@ class WorkbuddyPlugin {
112
113
  const port = (0, plugin_1.resolveRuntimePort)(config, context.runtime, "workbuddy", "WORKBUDDY_CDP_PORT", this.options.cdpPort ?? 9229);
113
114
  this.client = this.options.client ?? new cdp_client_1.WorkbuddyCdpClient({ port });
114
115
  try {
115
- await this.connectOrStart(config, context.logger, port);
116
+ await this.connectOrStart(config, context.logger, port, context.dataDir);
116
117
  }
117
118
  catch (error) {
118
119
  this.failure = error instanceof Error ? error.message : String(error);
@@ -314,10 +315,10 @@ class WorkbuddyPlugin {
314
315
  this.queue = run.then(() => { this.pending--; }, () => { this.pending--; });
315
316
  return run;
316
317
  }
317
- async connectOrStart(config, logger, port) {
318
+ async connectOrStart(config, logger, port, dataDir) {
318
319
  const executable = findExecutable(typeof config.executablePath === "string" ? config.executablePath : this.options.executablePath);
319
- // 沿用 WorkBuddy 的默认用户目录,保留用户在桌面端的登录状态。
320
- const profile = undefined;
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);
321
322
  const timeoutMs = typeof config.startupTimeoutMs === "number" ? config.startupTimeoutMs : 30_000;
322
323
  this.reconnect = async () => {
323
324
  try {
@@ -327,7 +328,9 @@ class WorkbuddyPlugin {
327
328
  catch (first) {
328
329
  if (config.autoStart === false || this.options.client || !executable)
329
330
  throw first;
330
- await (0, desktop_launch_1.restartOwnedAndWait)(this.process, executable, [`--remote-debugging-port=${port}`], profile, () => this.client.connect(), timeoutMs, child => { this.process = child; });
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);
331
334
  }
332
335
  };
333
336
  try {
@@ -340,7 +343,7 @@ class WorkbuddyPlugin {
340
343
  if (!executable)
341
344
  throw new Error("未找到 WorkBuddy.exe,请配置 executablePath");
342
345
  logger.info(`WorkBuddy 已启动,等待 CDP ${port}`);
343
- await (0, desktop_launch_1.launchManagedAndWait)(executable, [`--remote-debugging-port=${port}`], profile, () => this.client.connect(), timeoutMs, child => { this.process = child; });
346
+ await (0, desktop_launch_1.launchManagedAndWait)(executable, [`--remote-debugging-port=${port}`], profile, () => this.client.connect(), timeoutMs, child => { this.process = child; }, launch);
344
347
  }
345
348
  }
346
349
  }