mu-agent-bridge 1.0.28 → 1.0.30

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 (63) hide show
  1. package/README.md +6 -4
  2. package/dist/src-ts/cli-commands.js +172 -0
  3. package/dist/src-ts/cli.js +29 -14
  4. package/dist/src-ts/control-server.js +1 -1
  5. package/dist/src-ts/plugin-manager.js +17 -3
  6. package/dist/src-ts/plugins/abweb/index.js +1 -1
  7. package/dist/src-ts/plugins/abweb/public/app.js +20 -1
  8. package/dist/src-ts/plugins/abweb/public/index.html +3 -0
  9. package/dist/src-ts/plugins/abweb/public/style.css +4 -0
  10. package/dist/src-ts/plugins/aun/certs/root.crt +13 -0
  11. package/dist/src-ts/plugins/aun/evolcore.js +2 -1
  12. package/dist/src-ts/plugins/aun/index.js +369 -23
  13. package/dist/src-ts/plugins/aun/public/app.js +27 -7
  14. package/dist/src-ts/plugins/aun/public/index.html +1 -1
  15. package/dist/src-ts/plugins/desktop-launch.js +70 -0
  16. package/dist/src-ts/plugins/doubao/cdp-client.js +51 -8
  17. package/dist/src-ts/plugins/doubao/index.js +24 -5
  18. package/dist/src-ts/plugins/feishu/index.js +111 -23
  19. package/dist/src-ts/plugins/qwenwork/index.js +19 -2
  20. package/dist/tests-ts/abweb-login.test.js +1 -0
  21. package/dist/tests-ts/aun-plugin.test.js +658 -22
  22. package/dist/tests-ts/channel-feedback.test.js +99 -0
  23. package/dist/tests-ts/cli-commands.test.js +305 -0
  24. package/dist/tests-ts/control-server.test.js +3 -0
  25. package/dist/tests-ts/desktop-launch.test.js +39 -0
  26. package/dist/tests-ts/doubao-plugin.test.js +44 -0
  27. package/dist/tests-ts/feishu-plugin.test.js +187 -0
  28. package/dist/tests-ts/platform-integration.test.js +1 -1
  29. package/dist/tests-ts/plugin-cli-integration.test.js +111 -0
  30. package/dist/tests-ts/plugin-manager.test.js +37 -0
  31. package/package.json +1 -1
  32. package/plugins/abweb/dist/src-ts/plugins/abweb/index.js +1 -1
  33. package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.js +20 -1
  34. package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.ts +16 -1
  35. package/plugins/abweb/dist/src-ts/plugins/abweb/public/index.html +3 -0
  36. package/plugins/abweb/dist/src-ts/plugins/abweb/public/style.css +4 -0
  37. package/plugins/abweb/package.json +1 -1
  38. package/plugins/abweb/plugin.json +1 -1
  39. package/plugins/aun/dist/src-ts/plugins/aun/certs/root.crt +13 -0
  40. package/plugins/aun/dist/src-ts/plugins/aun/evolcore.js +2 -1
  41. package/plugins/aun/dist/src-ts/plugins/aun/index.js +369 -23
  42. package/plugins/aun/dist/src-ts/plugins/aun/public/app.js +27 -7
  43. package/plugins/aun/dist/src-ts/plugins/aun/public/index.html +1 -1
  44. package/plugins/aun/package.json +1 -1
  45. package/plugins/aun/plugin.json +3 -2
  46. package/plugins/chatgpt/package.json +1 -1
  47. package/plugins/chatgpt/plugin.json +1 -1
  48. package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +70 -0
  49. package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +51 -8
  50. package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +24 -5
  51. package/plugins/doubao/package.json +1 -1
  52. package/plugins/doubao/plugin.json +1 -1
  53. package/plugins/dsh/package.json +1 -1
  54. package/plugins/dsh/plugin.json +1 -1
  55. package/plugins/feishu/dist/src-ts/plugins/feishu/index.js +111 -23
  56. package/plugins/feishu/package.json +1 -1
  57. package/plugins/feishu/plugin.json +3 -2
  58. package/plugins/qwenwork/dist/src-ts/plugins/desktop-launch.js +70 -0
  59. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +19 -2
  60. package/plugins/qwenwork/package.json +1 -1
  61. package/plugins/qwenwork/plugin.json +1 -1
  62. package/plugins/workbuddy/package.json +1 -1
  63. package/plugins/workbuddy/plugin.json +1 -1
package/README.md CHANGED
@@ -22,10 +22,12 @@ OpenAI 兼容 API 为 `/v1/models` 和 `/v1/chat/completions`。管理页面位
22
22
  `http://<Windows 主机 IP>:26107/abweb/`。页面使用一次性配置码配对后管理插件;请只在可信网络使用。
23
23
  如果 26107 被旧的 mab 进程占用,可执行 `mab restart`;Windows 会扫描并结束同一 CLI 安装路径的全部旧 daemon,并重新启动。`mab stop` 只停止 daemon,
24
24
  `mab update` 会结束旧 daemon、安装最新核心包并启动新版本。不会把自己的旧进程误当成新的 daemon,也不会因此默认改用临时端口。
25
- 已有 AID 的连接状态由 AUN 插件自己建立和查询,只有当前已连接的身份才能关联 Agent。
26
- AUN 插件通过 SDK 枚举 EvolCore 的本地 AID,并逐个建立连接;ABWeb 只展示已连接 AID,并按 EvolCore 与 mu-agent-bridge 分组,未连接身份不显示。状态检测不调用 `ec agent list/show`。
25
+ 已有 AID 的连接状态由 AUN 插件查询,只有当前已连接的身份才能关联 Agent。
26
+ 本机 AID 来自 AUN 插件自身的密钥库,不依赖 `ec` 命令,`ec stop` 后仍会显示。
27
+ 未配置 `aun_path` 时密钥库默认使用共享 AUN 目录(`AUN_HOME`/`EVOLCORE_HOME` 或
28
+ `~/.evolcore`),已注册的 AID 无需迁移;插件启动时自动连接密钥库中的全部 AID。
27
29
  创建新 AID 必须填写 Owner AID,并安装、启动 AUN 插件;创建与通信由该插件完成。
28
- 本工程创建的身份由 AUN 插件保持连接并收发消息;EvolCore 来源身份只用于连接状态检测,不订阅消息,避免重复收信。将 EvolCore 身份关联到 Agent 时,AUN 插件仍执行既有 `ec agent set/reload` 路由配置;该命令不用于检测连接状态。
30
+ 已连接身份保存在 AUN 插件自身的密钥库和配置中。关联 AID 到 Agent 时,由 AUN 插件直接保持连接并转发来信和回复,不再执行 `ec agent set/reload`。
29
31
  其他 Agent 插件可关联 AUN 插件自己创建的身份,由通道直接转发来信和回复。
30
32
  Windows 可运行 `start-mab.ps1` 注册并启动登录时自动运行的计划任务;令牌保存在
31
33
  `%USERPROFILE%\.mab\token.txt`,页面输入该令牌即可使用管理 API。
@@ -139,4 +141,4 @@ Windows 路径时,可设置 `DSH_INSTALL_DIR`。
139
141
 
140
142
  ## AUN 身份管理
141
143
 
142
- 创建、连接、关联和 AUN 通信统一由独立 `agent-bridge-aun` 插件完成。未安装并启动该插件时,ABWeb 禁用创建 AID;页面仅展示已连接身份。详细规则见 [AUN 插件与 AID 身份管理](docs/aun-plugin-identities.md)。
144
+ 创建、连接、关联和 AUN 通信统一由独立 `agent-bridge-aun` 插件完成。安装并启动 AUN 后,在 ABWeb 的“通道 → AUN”创建或选择身份,再将已连接 AID 关联到已启动的 Agent;用 `mab status` 查看状态。Owner AID 仅作本地归属配置,不代表 AUN Owner 关系已激活。完整步骤见 [AUN 插件与 AID 身份管理](docs/aun-plugin-identities.md)。
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CORE_HELP = void 0;
4
+ exports.resolveLocalControlHost = resolveLocalControlHost;
5
+ exports.controlUrl = controlUrl;
6
+ exports.runPluginCommand = runPluginCommand;
7
+ exports.runCoreHelp = runCoreHelp;
8
+ const node_http_1 = require("node:http");
9
+ const MAX_BODY_BYTES = 1024 * 1024;
10
+ const PLUGIN_ID = /^[a-z][a-z0-9-]{1,63}$/;
11
+ const RESERVED_COMMANDS = new Set(["start", "stop", "restart", "status", "pair", "update", "help"]);
12
+ exports.CORE_HELP = "用法:mab --help\n mab start | stop | restart | update | status | pair\n mab --self-test <任务> | --serve\n mab <插件 ID> help [命令]\n mab <插件 ID> <命令> [参数...] [--json]\n";
13
+ class CliError extends Error {
14
+ exitCode;
15
+ constructor(message, exitCode = 1) {
16
+ super(message);
17
+ this.exitCode = exitCode;
18
+ }
19
+ }
20
+ function resolveLocalControlHost(host) {
21
+ if (host === "0.0.0.0")
22
+ return "127.0.0.1";
23
+ if (host === "::")
24
+ return "::1";
25
+ if (host === "127.0.0.1" || host === "::1" || host === "localhost")
26
+ return host;
27
+ throw new CliError("动态 CLI 仅支持本机 loopback 控制地址;请将 AGENT_BRIDGE_HOST 设置为 127.0.0.1、localhost、::1 或通配地址");
28
+ }
29
+ function controlUrl(host, port) {
30
+ const localHost = resolveLocalControlHost(host);
31
+ return `http://${localHost.includes(":") ? `[${localHost}]` : localHost}:${port}`;
32
+ }
33
+ function object(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
34
+ function extractOptions(args) {
35
+ const argv = [];
36
+ let json = false;
37
+ let terminated = false;
38
+ for (const argument of args) {
39
+ if (!terminated && argument === "--json") {
40
+ json = true;
41
+ continue;
42
+ }
43
+ if (argument === "--")
44
+ terminated = true;
45
+ argv.push(argument);
46
+ }
47
+ return { argv, json };
48
+ }
49
+ function output(result, exitCode, json) {
50
+ if (json)
51
+ return { exitCode, stdout: `${JSON.stringify(result)}\n`, stderr: "" };
52
+ return { exitCode, stdout: exitCode === 0 ? `${result.message}\n` : "", stderr: exitCode === 0 ? "" : `${result.message}\n` };
53
+ }
54
+ async function requestJson(port, path, options, payload) {
55
+ const body = payload === undefined ? undefined : JSON.stringify(payload);
56
+ if (body && Buffer.byteLength(body) > MAX_BODY_BYTES)
57
+ throw new CliError("命令请求超过 1 MiB 上限");
58
+ return new Promise((resolve, reject) => {
59
+ const headers = { accept: "application/json" };
60
+ const token = options.token ?? process.env.AGENT_BRIDGE_TOKEN;
61
+ if (token)
62
+ headers.authorization = `Bearer ${token}`;
63
+ if (body !== undefined) {
64
+ headers["content-type"] = "application/json";
65
+ headers["content-length"] = Buffer.byteLength(body);
66
+ }
67
+ const req = (0, node_http_1.request)({ host: resolveLocalControlHost(options.host ?? "127.0.0.1"), port, path, method: body === undefined ? "GET" : "POST", headers }, (res) => {
68
+ const chunks = [];
69
+ let bytes = 0;
70
+ res.on("data", (chunk) => {
71
+ bytes += chunk.length;
72
+ if (bytes > MAX_BODY_BYTES) {
73
+ req.destroy(new CliError("后台响应超过 1 MiB 上限"));
74
+ return;
75
+ }
76
+ chunks.push(chunk);
77
+ });
78
+ res.on("error", reject);
79
+ res.on("end", () => {
80
+ try {
81
+ resolve({ status: res.statusCode ?? 500, value: JSON.parse(Buffer.concat(chunks).toString("utf8")) });
82
+ }
83
+ catch {
84
+ reject(new CliError("后台响应不是有效 JSON"));
85
+ }
86
+ });
87
+ });
88
+ const timer = setTimeout(() => req.destroy(new CliError("连接后台超时;请检查 daemon 状态")), options.timeoutMs ?? 10_000);
89
+ req.on("close", () => clearTimeout(timer));
90
+ req.on("error", (error) => reject(error instanceof CliError ? error : new CliError(`后台无法访问;请先执行 mab start:${error.message}`)));
91
+ req.end(body);
92
+ });
93
+ }
94
+ async function discover(port, options) {
95
+ const { status, value } = await requestJson(port, "/api/plugins", options);
96
+ if (status < 200 || status >= 300)
97
+ throw new CliError(`插件发现失败(HTTP ${status})`);
98
+ if (!object(value) || !Array.isArray(value.plugins) || value.plugins.some((entry) => !object(entry)
99
+ || typeof entry.id !== "string" || !PLUGIN_ID.test(entry.id) || typeof entry.version !== "string"
100
+ || typeof entry.displayName !== "string" || typeof entry.status !== "string"
101
+ || !Array.isArray(entry.capabilities) || entry.capabilities.some((capability) => typeof capability !== "string"))) {
102
+ throw new CliError("后台插件清单格式无效");
103
+ }
104
+ return value.plugins;
105
+ }
106
+ function commandManifest(value, plugin) {
107
+ if (!object(value) || value.protocolVersion !== 1 || value.plugin !== plugin || typeof value.version !== "string"
108
+ || !Array.isArray(value.commands) || value.commands.some((command) => !object(command)
109
+ || typeof command.name !== "string" || !/^[a-z][a-z0-9-]*$/.test(command.name)
110
+ || typeof command.summary !== "string" || !command.summary.trim() || typeof command.usage !== "string" || !command.usage.trim()
111
+ || (command.examples !== undefined && (!Array.isArray(command.examples) || command.examples.some((example) => typeof example !== "string"))))) {
112
+ throw new CliError("插件命令清单协议无效(需要 protocolVersion: 1)");
113
+ }
114
+ if (new Set(value.commands.map((command) => command.name)).size !== value.commands.length)
115
+ throw new CliError("插件命令清单包含重复命令");
116
+ return value;
117
+ }
118
+ async function runPluginCommand(id, args, port, options = {}) {
119
+ const extracted = extractOptions(args);
120
+ const json = options.json ?? extracted.json;
121
+ try {
122
+ if (!PLUGIN_ID.test(id) || RESERVED_COMMANDS.has(id))
123
+ throw new CliError("插件 ID 无效或使用了核心保留命令", 2);
124
+ const plugin = (await discover(port, options)).find((entry) => entry.id === id);
125
+ if (!plugin)
126
+ throw new CliError(`插件“${id}”未安装;请先到 ABWeb 安装插件`, 2);
127
+ if (!plugin.capabilities.includes("cli"))
128
+ throw new CliError(`插件“${plugin.displayName}”未提供 CLI 命令;请升级对应插件`, 2);
129
+ if (plugin.status !== "ready")
130
+ throw new CliError(`插件“${plugin.displayName}”未就绪(${plugin.status})`, 2);
131
+ const path = `/api/plugins/${id}/commands`;
132
+ const response = await requestJson(port, path, options);
133
+ if (response.status < 200 || response.status >= 300)
134
+ throw new CliError(`插件命令清单读取失败(HTTP ${response.status})`);
135
+ const manifest = commandManifest(response.value, id);
136
+ const argv = extracted.argv;
137
+ if (argv.length === 0 || argv[0] === "help" || argv[0] === "--help") {
138
+ const helpArgs = argv.slice(argv[0] === "help" || argv[0] === "--help" ? 1 : 0);
139
+ if (helpArgs.length > 1)
140
+ throw new CliError(`用法:mab ${id} help [命令]`, 2);
141
+ const selected = helpArgs.length === 0 ? manifest.commands : manifest.commands.filter((command) => command.name === helpArgs[0]);
142
+ if (helpArgs.length > 0 && selected.length === 0)
143
+ throw new CliError(`插件“${id}”没有命令:${helpArgs[0]}`, 2);
144
+ const message = [`${plugin.displayName}(${manifest.version})`, ...selected.flatMap((command) => [
145
+ `${command.name}:${command.summary}`, ` ${command.usage}`, ...(command.examples ?? []).map((example) => ` 示例:${example}`),
146
+ ])].join("\n");
147
+ return output({ ok: true, message, data: { ...manifest, commands: selected } }, 0, json);
148
+ }
149
+ const executed = await requestJson(port, path, options, { protocolVersion: 1, argv });
150
+ const result = executed.value;
151
+ if (!object(result) || typeof result.ok !== "boolean" || typeof result.message !== "string"
152
+ || (executed.status >= 200 && executed.status < 300 ? !result.ok : result.ok)
153
+ || executed.status < 200 || (executed.status >= 300 && executed.status < 400)) {
154
+ throw new CliError("插件命令响应协议无效");
155
+ }
156
+ const exitCode = executed.status >= 500 ? 1 : executed.status >= 400 ? 2 : 0;
157
+ return output(result, exitCode, json);
158
+ }
159
+ catch (error) {
160
+ return output({ ok: false, message: error instanceof Error ? error.message : String(error) }, error instanceof CliError ? error.exitCode : 1, json);
161
+ }
162
+ }
163
+ async function runCoreHelp(port, options = {}) {
164
+ let help = exports.CORE_HELP;
165
+ try {
166
+ const plugins = (await discover(port, options)).filter((plugin) => plugin.capabilities.includes("cli") && !RESERVED_COMMANDS.has(plugin.id));
167
+ if (plugins.length)
168
+ help += `\n插件命令(mab <插件 ID> help 查看详情):\n${plugins.map((plugin) => ` ${plugin.id}:${plugin.displayName}(${plugin.status})`).join("\n")}\n`;
169
+ }
170
+ catch { /* 后台不可达时仍显示核心静态帮助 */ }
171
+ return { exitCode: 0, stdout: help, stderr: "" };
172
+ }
@@ -47,6 +47,7 @@ const registry_1 = require("./registry");
47
47
  const bridge_1 = require("./bridge");
48
48
  const plugin_manager_1 = require("./plugin-manager");
49
49
  const daemon_process_1 = require("./daemon-process");
50
+ const cli_commands_1 = require("./cli-commands");
50
51
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
51
52
  let activeManager;
52
53
  const logger = {
@@ -72,19 +73,19 @@ async function selfTest(task) {
72
73
  console.error(`session=${response.sessionId}`);
73
74
  return 0;
74
75
  }
75
- async function daemonPort() {
76
+ async function daemonAddress() {
76
77
  try {
77
78
  const address = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.resolve)(stateDirectory(), "daemon-address.json"), "utf8"));
78
79
  if (Number.isInteger(address.port) && address.port > 0)
79
- return address.port;
80
+ return { host: typeof address.host === "string" ? address.host : "127.0.0.1", port: address.port };
80
81
  }
81
82
  catch { /* 后台未写入地址时使用配置端口 */ }
82
- return Number(process.env.AGENT_BRIDGE_PORT ?? "26107");
83
+ return { host: process.env.AGENT_BRIDGE_HOST ?? "127.0.0.1", port: Number(process.env.AGENT_BRIDGE_PORT ?? "26107") };
83
84
  }
84
85
  async function pair() {
85
- const port = await daemonPort();
86
+ const { host, port } = await daemonAddress();
86
87
  const result = await new Promise((resolve, reject) => {
87
- const req = (0, node_http_1.request)({ host: "127.0.0.1", port, path: "/api/pair-code", method: "GET" }, (res) => {
88
+ const req = (0, node_http_1.request)({ host: (0, cli_commands_1.resolveLocalControlHost)(host), port, path: "/api/pair-code", method: "GET" }, (res) => {
88
89
  let body = "";
89
90
  res.setEncoding("utf8");
90
91
  res.on("data", (chunk) => body += chunk);
@@ -181,9 +182,9 @@ async function restart() {
181
182
  return 0;
182
183
  }
183
184
  async function status() {
184
- const port = await daemonPort();
185
+ const { host, port } = await daemonAddress();
185
186
  const get = (path) => new Promise((resolve, reject) => {
186
- const req = (0, node_http_1.request)({ host: "127.0.0.1", port, path, method: "GET" }, (res) => {
187
+ const req = (0, node_http_1.request)({ host: (0, cli_commands_1.resolveLocalControlHost)(host), port, path, method: "GET" }, (res) => {
187
188
  let body = "";
188
189
  res.setEncoding("utf8");
189
190
  res.on("data", (chunk) => body += chunk);
@@ -243,6 +244,12 @@ async function serve() {
243
244
  const fallbackPorts = (process.env.AGENT_BRIDGE_FALLBACK_PORTS ?? "").split(",").map((value) => Number(value.trim())).filter((value) => Number.isInteger(value) && value >= 0);
244
245
  const server = new control_server_1.ControlServer({ manager, host, port, fallbackPorts, token: process.env.AGENT_BRIDGE_TOKEN, sessionFile: (0, node_path_1.resolve)(stateDirectory(), "paired-sessions.json") });
245
246
  const address = await server.start();
247
+ try {
248
+ manager.setControlUrl((0, cli_commands_1.controlUrl)(address.host, address.port));
249
+ }
250
+ catch {
251
+ logger.warn("当前监听地址仅提供网络 API,插件动态 CLI 需要本机 loopback 或通配监听地址");
252
+ }
246
253
  const addressFile = (0, node_path_1.resolve)(stateDirectory(), "daemon-address.json");
247
254
  await (0, promises_1.mkdir)(stateDirectory(), { recursive: true });
248
255
  await (0, promises_1.writeFile)(addressFile, JSON.stringify({ host: address.host, port: address.port, pid: process.pid }));
@@ -324,7 +331,7 @@ async function checkAndUpgradePlugins(manager) {
324
331
  }
325
332
  async function main() {
326
333
  const args = process.argv.slice(2);
327
- if (args.includes("pair"))
334
+ if (args[0] === "pair")
328
335
  return pair();
329
336
  if (args[0] === "status")
330
337
  return status();
@@ -334,15 +341,23 @@ async function main() {
334
341
  return restart();
335
342
  if (args[0] === "update")
336
343
  return update();
337
- const selfTestIndex = args.indexOf("--self-test");
338
- if (selfTestIndex >= 0)
339
- return selfTest(args[selfTestIndex + 1] ?? "hi");
340
- if (args.includes("--serve") || args.includes("start")) {
344
+ if (args[0] === "--self-test")
345
+ return selfTest(args[1] ?? "hi");
346
+ if (args[0] === "--serve" || args[0] === "start") {
341
347
  await stopDaemon(false);
342
348
  return serve();
343
349
  }
344
- console.error("用法:mab start | mab stop | mab restart | mab update | mab status | mab --self-test <任务>");
345
- return 2;
350
+ if (!args[0]) {
351
+ process.stderr.write(cli_commands_1.CORE_HELP);
352
+ return 2;
353
+ }
354
+ const address = await daemonAddress();
355
+ const result = args[0] === "--help" || args[0] === "help"
356
+ ? await (0, cli_commands_1.runCoreHelp)(address.port, { host: address.host, timeoutMs: 1000 })
357
+ : await (0, cli_commands_1.runPluginCommand)(args[0], args.slice(1), address.port, { host: address.host });
358
+ process.stdout.write(result.stdout);
359
+ process.stderr.write(result.stderr);
360
+ return result.exitCode;
346
361
  }
347
362
  main().then((code) => { process.exitCode = code; }).catch((error) => {
348
363
  console.error(error instanceof Error ? error.stack ?? error.message : error);
@@ -54,7 +54,7 @@ class ControlServer {
54
54
  reject(new Error("后台监听地址无效"));
55
55
  return;
56
56
  }
57
- resolve({ host, port: info.port });
57
+ resolve({ host: info.address, port: info.port });
58
58
  };
59
59
  this.server.once("error", onError);
60
60
  this.server.once("listening", onListening);
@@ -22,6 +22,7 @@ const logger = { info() { }, warn() { }, error() { } };
22
22
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
23
23
  class PluginManager {
24
24
  options;
25
+ controlUrl;
25
26
  plugins = new Map();
26
27
  httpRoutes = new Map();
27
28
  root;
@@ -41,6 +42,7 @@ class PluginManager {
41
42
  this.sessions = new channel_sessions_1.ChannelSessions(options.stateDir);
42
43
  }
43
44
  log(level, message, fields, source = "mab") { this.logs.append({ time: new Date().toISOString(), level, source, message, ...(fields ? { fields } : {}) }); }
45
+ setControlUrl(url) { this.controlUrl = url; }
44
46
  async updatePolicy() { try {
45
47
  const value = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(this.options.stateDir, "update-policy.json"), "utf8"));
46
48
  return isUpdatePolicy(value) ? value : { mode: "auto-install" };
@@ -365,7 +367,7 @@ class PluginManager {
365
367
  }
366
368
  },
367
369
  },
368
- runtime: { ports: {
370
+ runtime: { controlUrl: this.controlUrl, ports: {
369
371
  qwenwork: Number(process.env.AGENT_BRIDGE_QWEN_CDP_PORT ?? process.env.QWEN_CDP_PORT ?? "9228"),
370
372
  doubao: Number(process.env.AGENT_BRIDGE_DOUBAO_CDP_PORT ?? process.env.DOUBAO_CDP_PORT ?? "9227"),
371
373
  workbuddy: Number(process.env.AGENT_BRIDGE_WORKBUDDY_CDP_PORT ?? process.env.WORKBUDDY_CDP_PORT ?? "9229"),
@@ -504,8 +506,17 @@ class PluginManager {
504
506
  async health(id) { const loaded = this.plugins.get(id); if (!loaded)
505
507
  throw new agent_1.AgentError(`unknown plugin: ${id}`); if (!loaded.instance)
506
508
  return { status: loaded.record.status }; return loaded.instance.health(); }
507
- async handle(id, request) { const loaded = this.plugins.get(id); if (!loaded?.instance || loaded.record.status !== "ready")
508
- throw new agent_1.AgentError(`plugin not ready: ${id}`); return loaded.instance.handle(request); }
509
+ async handle(id, request) {
510
+ const loaded = this.plugins.get(id);
511
+ if (!loaded?.instance || loaded.record.status !== "ready") {
512
+ const name = loaded?.record.displayName ?? id;
513
+ const processName = loaded ? pluginProcessName(loaded.record.id) : undefined;
514
+ const processText = processName ? `目标进程 ${processName} 不可用;` : "";
515
+ const reason = loaded?.record.error ? `启动错误:${loaded.record.error}` : `当前状态:${loaded?.record.status ?? "未发现"}`;
516
+ throw new agent_1.AgentError(`插件“${name}”无法调用,插件未就绪:${processText}${reason}`);
517
+ }
518
+ return loaded.instance.handle(request);
519
+ }
509
520
  async dispatchHttp(request, response) {
510
521
  const method = (request.method ?? "GET").toUpperCase();
511
522
  const pathname = new URL(request.url ?? "/", "http://localhost").pathname;
@@ -809,6 +820,9 @@ class PluginManager {
809
820
  }
810
821
  exports.PluginManager = PluginManager;
811
822
  function isUpdatePolicy(value) { return Boolean(value && typeof value === "object" && ["auto-install", "download-only", "manual"].includes(value.mode)); }
823
+ function pluginProcessName(id) {
824
+ return { qwenwork: "QwenWorkCN.exe", doubao: "Doubao.exe", workbuddy: "WorkBuddy.exe", chatgpt: "ChatGPT/Codex" }[id];
825
+ }
812
826
  function defaultPluginTrustConfig() { return { providers: { m2lihui: "allow" }, plugins: {} }; }
813
827
  function mergeDefaultPluginTrustConfig(value) { return { providers: { m2lihui: "allow", ...value.providers }, plugins: { ...value.plugins } }; }
814
828
  function normalizeProvider(value) { return value.trim().toLowerCase(); }
@@ -5,7 +5,7 @@ const node_fs_1 = require("node:fs");
5
5
  const node_path_1 = require("node:path");
6
6
  const MANIFEST = {
7
7
  id: "abweb",
8
- version: "1.0.6",
8
+ version: "1.0.7",
9
9
  displayName: "Agent Bridge Web",
10
10
  entry: "dist/index.js",
11
11
  apiVersion: "1",
@@ -35,6 +35,25 @@
35
35
  let loading = false;
36
36
  const installingPackages = new Set();
37
37
  function error(message) { errorNode.textContent = message; errorNode.hidden = !message; }
38
+ function showResult(title, message, failed) {
39
+ node("#operation-result-title").textContent = title;
40
+ const detail = node("#operation-result-message");
41
+ detail.textContent = message;
42
+ detail.className = failed ? "error" : "";
43
+ const dialog = node("#operation-result");
44
+ if (!dialog.open)
45
+ dialog.showModal();
46
+ }
47
+ node("#operation-result-close").onclick = () => node("#operation-result").close();
48
+ window.addEventListener("message", (event) => {
49
+ const frame = channelPanel.querySelector("iframe.channel-frame");
50
+ if (event.origin !== window.location.origin || !frame || event.source !== frame.contentWindow)
51
+ return;
52
+ const result = event.data;
53
+ if (!result || result.type !== "mab:operation-result" || typeof result.title !== "string" || typeof result.message !== "string" || typeof result.failed !== "boolean")
54
+ return;
55
+ showResult(result.title, result.message, result.failed);
56
+ });
38
57
  async function loadCatalog() {
39
58
  const result = await request(`/api/plugins/catalog?q=${encodeURIComponent(packageSearchNode.value.trim())}`);
40
59
  const items = result.plugins ?? [];
@@ -113,7 +132,7 @@
113
132
  item.type = "button";
114
133
  item.textContent = label;
115
134
  item.title = title;
116
- item.onclick = () => { item.disabled = true; void run().catch((cause) => error(String(cause))).finally(() => { item.disabled = false; void load(false); }); };
135
+ item.onclick = () => { item.disabled = true; void run().catch((cause) => { const message = cause instanceof Error ? cause.message : String(cause); error(message); showResult(`${title}失败`, message, true); }).finally(() => { item.disabled = false; void load(false); }); };
117
136
  return item;
118
137
  }
119
138
  async function load(refreshCatalog = true) {
@@ -21,6 +21,9 @@
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="operation-result" aria-labelledby="operation-result-title" aria-describedby="operation-result-message">
25
+ <h2 id="operation-result-title">操作结果</h2><p id="operation-result-message"></p><button id="operation-result-close" type="button" autofocus>确定</button>
26
+ </dialog>
24
27
  <script src="/abweb/app.js"></script>
25
28
  </body>
26
29
  </html>
@@ -56,3 +56,7 @@ th { color: #566761; font-weight: 600; background: #edf1ef; }
56
56
  th:nth-child(1) { width: 145px; } th:nth-child(2) { width: 100px; } th:nth-child(3), th:nth-child(4) { width: 145px; } th:nth-child(5) { width: 62px; } th:nth-child(7) { width: 140px; }
57
57
  .message-error td:last-child { color: #a83427; }
58
58
  @media (max-width: 620px) { .shell { padding: 16px; } .section-heading { align-items: flex-start; flex-direction: column; } form, form input { width: 100%; } }
59
+
60
+ #operation-result { width: min(520px, calc(100% - 32px)); border: 1px solid #bfc9c5; border-radius: 8px; padding: 24px; }
61
+ #operation-result::backdrop { background: rgb(0 0 0 / 35%); }
62
+ #operation-result-message { white-space: pre-wrap; overflow-wrap: anywhere; margin: 16px 0; }
@@ -0,0 +1,13 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIB/TCCAYOgAwIBAgIUI/WLfQXFR9KpvBlhnvJFIAtylNQwCgYIKoZIzj0EAwMw
3
+ KzEUMBIGA1UEAwwLQVVOIFJvb3QgQ0ExEzARBgNVBAoMCkFnZW50VW5pb24wIBcN
4
+ MjYwMzE3MDE1NDUyWhgPMjA1NjAzMDkwMTU5NTJaMCsxFDASBgNVBAMMC0FVTiBS
5
+ b290IENBMRMwEQYDVQQKDApBZ2VudFVuaW9uMHYwEAYHKoZIzj0CAQYFK4EEACID
6
+ YgAEbuQMkmsHjhtwsgoFhfgC4514k2IMQVECNeBtxiwiEe5OTY0D7MvqwtEzz67r
7
+ 1jo2OXOuA+BajSW/ZOw89HH2zrWIshw2qVo5WP4ELnrKk/dipfiJBm9IrJXsO0kN
8
+ ope4o2YwZDASBgNVHRMBAf8ECDAGAQH/AgECMA4GA1UdDwEB/wQEAwIBhjAdBgNV
9
+ HQ4EFgQULmOUMJCtK8VatDpoo/iigMIu3hwwHwYDVR0jBBgwFoAULmOUMJCtK8Va
10
+ tDpoo/iigMIu3hwwCgYIKoZIzj0EAwMDaAAwZQIwPcby4jt32sKANZhUuFaOcWiy
11
+ V8rgbF0pF+ROrlg+9JdQ3yRyGWTU2VRNyzd+FlNbAjEAgVKYzN6mpxnVfjk8dHYH
12
+ vh6o6LY9oqw8rdZ/2Q1k1eUSaFxykTZQ9ipR84GIGXrc
13
+ -----END CERTIFICATE-----
@@ -26,7 +26,8 @@ async function configureAidForPlugin(pluginId, aid, port, run = runEc) {
26
26
  return;
27
27
  if (!/^[a-z0-9_-]+$/.test(pluginId))
28
28
  throw new Error("插件 ID 格式无效");
29
- const path = pluginId === "dsh" ? "/dsh/v1" : pluginId === "qwenwork" ? "/v1" : `/api/agents/${pluginId}/v1`;
29
+ // 通过 daemon 的统一插件代理访问所有 Agent;否则 qwenwork 的裸 /v1 路由在有插件 HTTP 注册表时不存在。
30
+ const path = pluginId === "dsh" ? "/dsh/v1" : `/api/agents/${pluginId}/v1`;
30
31
  for (const [key, value] of [
31
32
  ["baseagents.ecagent.baseUrl", `http://127.0.0.1:${port}${path}`],
32
33
  ["baseagents.ecagent.apiKey", "mab-local"],