mu-agent-bridge 1.0.4 → 1.0.12

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 (41) hide show
  1. package/README.md +16 -11
  2. package/dist/src-ts/cli.js +60 -25
  3. package/dist/src-ts/control-server.js +73 -94
  4. package/dist/src-ts/local-aids.js +26 -0
  5. package/dist/src-ts/log-store.js +64 -0
  6. package/dist/src-ts/plugin-manager.js +178 -37
  7. package/dist/src-ts/plugin-manifest.js +5 -1
  8. package/dist/src-ts/plugins/abweb/index.js +1 -1
  9. package/dist/src-ts/plugins/abweb/public/app.js +165 -104
  10. package/dist/src-ts/plugins/abweb/public/index.html +6 -5
  11. package/dist/src-ts/plugins/abweb/public/style.css +12 -12
  12. package/dist/src-ts/plugins/aun/evolcore.js +54 -0
  13. package/dist/src-ts/plugins/aun/index.js +252 -50
  14. package/dist/src-ts/plugins/aun/public/app.js +66 -0
  15. package/dist/src-ts/plugins/aun/public/index.html +14 -0
  16. package/dist/src-ts/plugins/aun/public/style.css +21 -0
  17. package/dist/src-ts/plugins/doubao/cdp-client.js +1 -1
  18. package/dist/src-ts/plugins/doubao/index.js +2 -2
  19. package/dist/src-ts/plugins/feishu/client.js +133 -0
  20. package/dist/src-ts/plugins/feishu/index.js +214 -0
  21. package/dist/src-ts/plugins/feishu/public/app.js +126 -0
  22. package/dist/src-ts/plugins/feishu/public/index.html +16 -0
  23. package/dist/src-ts/plugins/feishu/public/style.css +25 -0
  24. package/dist/src-ts/plugins/workbuddy/cdp-client.js +118 -0
  25. package/dist/src-ts/plugins/workbuddy/cli-client.js +39 -0
  26. package/dist/src-ts/plugins/workbuddy/index.js +139 -0
  27. package/dist/src-ts/version-manager.js +9 -1
  28. package/dist/tests-ts/abweb-plugin.test.js +19 -0
  29. package/dist/tests-ts/aun-plugin.test.js +108 -4
  30. package/dist/tests-ts/control-server.test.js +88 -6
  31. package/dist/tests-ts/doubao-plugin.test.js +14 -0
  32. package/dist/tests-ts/feishu-client.test.js +72 -0
  33. package/dist/tests-ts/feishu-page.test.js +47 -0
  34. package/dist/tests-ts/feishu-plugin.test.js +96 -0
  35. package/dist/tests-ts/log-store.test.js +27 -0
  36. package/dist/tests-ts/platform-integration.test.js +40 -1
  37. package/dist/tests-ts/plugin-catalog.test.js +8 -0
  38. package/dist/tests-ts/plugin-manager.test.js +62 -13
  39. package/dist/tests-ts/workbuddy-plugin.test.js +85 -0
  40. package/package.json +30 -6
  41. package/plugins/abweb/plugin.json +1 -1
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # mu-agent-bridge 动态插件平台
2
2
 
3
3
  运行时使用 TypeScript/Node.js,实现一个可动态发现、启动、停止、重载和安装插件的 daemon。
4
- 当前插件包括千问办公 `qwenwork`、DeepSeek Harness `dsh`、豆包 `doubao`,以及插件化管理后台
4
+ 当前插件包括千问办公 `qwenwork`、DeepSeek Harness `dsh`、豆包 `doubao`、基于 CodeBuddy CLI 的 `workbuddy`,以及插件化管理后台
5
5
  `abweb`。插件之间独立失败,管理 API 和 OpenAI 兼容接口由同一个本地控制服务提供。
6
6
 
7
7
  ## 快速开始
@@ -19,23 +19,22 @@ OpenAI 兼容 API 为 `/v1/models` 和 `/v1/chat/completions`。管理页面位
19
19
 
20
20
  安装后使用 `mab start` 启动。旧桥仍可使用 `17893`,不会与管理页面冲突。局域网访问时设置
21
21
  `AGENT_BRIDGE_HOST=0.0.0.0` 和 `AGENT_BRIDGE_TOKEN`,然后访问
22
- `http://<Windows 主机 IP>:26107/abweb/`。页面输入令牌后可管理插件;请只在可信网络使用。
23
- 已有 AID 从 EvolCore 的 `ec agent list/show` 读取,只显示当前 AUN 已连接且本机有私钥的身份,
24
- 禁用和断线身份不列出。管理页可指定新 AID 与 owner AID,调用 EvolCore 创建 Agent 和身份;
25
- 新身份连接成功后才会进入列表。
26
- “已连接 AID”按来源分为 EvolCore 和 mu-agent-bridge 两组;后者只包含通过本页面新建、
27
- 且当前已连接的 AID。分组由 mab 状态目录中的创建记录决定,不改变 EvolCore 的身份存储。
28
- 关联 DSH 或千问办公插件时,管理服务会将所选 AID 的 EvolCore 执行基座分别配置为本机
22
+ `http://<Windows 主机 IP>:26107/abweb/`。页面使用一次性配置码配对后管理插件;请只在可信网络使用。
23
+ 已有 AID 的连接状态由 AUN 插件查询,只有当前已连接的身份才能关联 Agent。
24
+ 核心可以只读扫描 EvolCore 本地身份;ABWeb 只展示已连接 AID,并按 EvolCore 与 mu-agent-bridge 分组,未连接身份不显示。
25
+ 创建新 AID 必须填写 Owner AID,并安装、启动 AUN 插件;创建与通信由该插件完成。
26
+ 已连接身份按来源分为 EvolCore 和 mu-agent-bridge 两组;后者保存在 AUN 插件自身的密钥库和配置中。
27
+ 关联 EvolCore 身份到 DSH 或千问办公时,AUN 插件会将所选身份的执行基座分别配置为本机
29
28
  `/dsh/v1` 或 `/v1` OpenAI 接口,然后重载该 Agent;AUN 入站消息由 EvolCore 转给插件,
30
29
  回复仍由 EvolCore 经 AUN 发送。该操作会覆盖所选 AID 当前的执行基座。
31
- 其他插件的 AID 关联目前只保存配置项,不自动改变 EvolCore 执行基座。
30
+ 其他 Agent 插件可关联 AUN 插件自己创建的身份,由通道直接转发来信和回复。
32
31
  Windows 可运行 `start-mab.ps1` 注册并启动登录时自动运行的计划任务;令牌保存在
33
32
  `%USERPROFILE%\.mab\token.txt`,页面输入该令牌即可使用管理 API。
34
33
 
35
34
  本机打开 `http://127.0.0.1:26107/abweb/` 无需配置码。局域网管理页可用一次性配置码配对。
36
35
  在 Windows 主机运行 `mab pair`,将显示的 6 位码输入
37
36
  abweb 的“配置码”栏;配对码 10 分钟有效,只能使用一次,连续输错 5 次后需重新执行
38
- `mab pair`。配对成功后浏览器在当前会话保存访问凭证。配置码只能从 Windows 本机的
37
+ `mab pair`。配对成功后登录有效期为 24 小时,浏览器关闭再打开仍可使用;过期后需重新配对。后台重启会使配对登录失效。本机免配对和手动配置的管理令牌按原有规则使用。配置码只能从 Windows 本机的
39
38
  `127.0.0.1` 接口取得,远程浏览器不能读取。
40
39
 
41
40
  环境变量:`AGENT_BRIDGE_PORT`、`AGENT_BRIDGE_HOST`、`AGENT_BRIDGE_TOKEN`、
@@ -52,6 +51,8 @@ abweb 的“配置码”栏;配对码 10 分钟有效,只能使用一次,
52
51
  升级失败会恢复旧插件并继续启动。Windows 的 `start-mab.ps1` 登录计划任务会在启动 daemon 前
53
52
  更新全局核心包,健康检查失败时恢复备份。全局单独安装的插件及 Linux 核心自动升级暂不受管理。
54
53
 
54
+ 插件更新策略可在 ABWeb 的“版本管理”中选择:默认“自动下载并安装”;“自动下载,手动安装”会把新版本缓存到状态目录并等待页面操作;“手动下载和安装”只检查版本。策略保存在 `state/update-policy.json`,插件下载缓存位于 `state/downloads`。
55
+
55
56
  运行时会发现核心包依赖和 `AGENT_BRIDGE_PLUGIN_PACKAGES` 中声明的 npm 包。插件包在自己的
56
57
  `package.json` 中声明 `agentBridge.plugin`,平台复用统一的 `activate`、`deactivate`、`health`、
57
58
  `handle` 生命周期。已加载插件可以通过管理 API 重载;新增 npm 包后重启 daemon 最可靠。
@@ -59,7 +60,7 @@ abweb 的“配置码”栏;配对码 10 分钟有效,只能使用一次,
59
60
  abweb 插件目录从 npm 搜索带 `mu-agent-bridge-plugin` 关键词的包,按包元数据中的
60
61
  `agentBridge.type` 展示 Agent、Channel 和管理插件;安装后以 `plugin.json` 的类型和能力
61
62
  再次校验,不根据包名推断类型。搜索结果有收录延迟,可在页面直接输入 `agent-bridge-*`
62
- 包名安装。已安装插件按清单能力兼容旧版分类。AUN 通道插件包名为 `agent-bridge-aun`,安装后配置 AID 并启动即可;来信默认交给千问办公,也可通过 `agent_plugin` 指定其他已安装 Agent。
63
+ 包名安装。已安装插件按清单能力兼容旧版分类。AUN 通道插件包名为 `agent-bridge-aun`,安装后配置 AID 并启动即可;来信默认交给千问办公,也可通过 `agent_plugin` 指定其他已安装 Agent。`workbuddy` 插件包名为 `agent-bridge-workbuddy`,将 AUN 的 `agent_plugin` 设为 `workbuddy` 即可按对端保持 CodeBuddy 会话。默认问答使用独立的 `@tencent-ai/codebuddy-code` CLI,须单独登录;没有 CLI 时使用 WorkBuddy 桌面端 CDP,并默认自动启动桌面应用。CLI 未登录时不会自动切换到桌面端账号;可设置 `transport` 为 `cdp` 显式使用桌面端。
63
64
 
64
65
  插件仍在 daemon 的同一个 Node.js 进程内运行。修改插件入口依赖后应重启 daemon,避免模块缓存
65
66
  影响新代码。
@@ -124,3 +125,7 @@ npm run self-test -- hi
124
125
 
125
126
  Agent 通过 `AgentRegistry` 注册;DSH 也可以作为动态 `dsh` 插件加载。DeepSeek Harness 不在默认
126
127
  Windows 路径时,可设置 `DSH_INSTALL_DIR`。
128
+
129
+ ## AUN 身份管理
130
+
131
+ 创建、连接、关联和 AUN 通信统一由独立 `agent-bridge-aun` 插件完成。未安装并启动该插件时,ABWeb 禁用创建 AID;页面仅展示已连接身份。详细规则见 [AUN 插件与 AID 身份管理](docs/aun-plugin-identities.md)。
@@ -38,15 +38,19 @@ const node_path_1 = require("node:path");
38
38
  const node_http_1 = require("node:http");
39
39
  const node_fs_1 = require("node:fs");
40
40
  const node_os_1 = require("node:os");
41
+ const node_child_process_1 = require("node:child_process");
42
+ const node_util_1 = require("node:util");
41
43
  const control_server_1 = require("./control-server");
42
44
  const dsh_agent_1 = require("./dsh-agent");
43
45
  const registry_1 = require("./registry");
44
46
  const bridge_1 = require("./bridge");
45
47
  const plugin_manager_1 = require("./plugin-manager");
48
+ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
49
+ let activeManager;
46
50
  const logger = {
47
- info(message, fields) { console.log(`[agent-bridge] ${message}`, fields ?? ""); },
48
- warn(message, fields) { console.warn(`[agent-bridge] ${message}`, fields ?? ""); },
49
- error(message, fields) { console.error(`[agent-bridge] ${message}`, fields ?? ""); },
51
+ info(message, fields) { console.log(`[agent-bridge] ${message}`, fields ?? ""); activeManager?.log("info", message, fields, "mab"); },
52
+ warn(message, fields) { console.warn(`[agent-bridge] ${message}`, fields ?? ""); activeManager?.log("warn", message, fields, "mab"); },
53
+ error(message, fields) { console.error(`[agent-bridge] ${message}`, fields ?? ""); activeManager?.log("error", message, fields, "mab"); },
50
54
  };
51
55
  function stateDirectory() {
52
56
  if (process.env.AGENT_BRIDGE_STATE_DIR)
@@ -95,33 +99,27 @@ async function pair() {
95
99
  console.log(`配对码: ${result.code}(约 ${minutes} 分钟内有效,仅可使用一次)`);
96
100
  return 0;
97
101
  }
102
+ async function update() {
103
+ if (process.platform !== "win32") {
104
+ console.error("当前平台未配置后台升级器");
105
+ return 2;
106
+ }
107
+ await execFileAsync("npm.cmd", ["install", "-g", "mu-agent-bridge@latest", "--no-fund", "--no-audit"], { timeout: 120_000 });
108
+ await execFileAsync("schtasks.exe", ["/Run", "/TN", "mab"]);
109
+ console.log("核心已更新,已请求 mab 计划任务重新启动");
110
+ return 0;
111
+ }
98
112
  async function serve() {
99
113
  const manager = new plugin_manager_1.PluginManager({
100
114
  pluginsDir: (0, node_path_1.resolve)(process.env.AGENT_BRIDGE_PLUGINS_DIR ?? (0, node_path_1.resolve)(__dirname, "..", "..", "plugins")),
101
115
  stateDir: stateDirectory(),
102
116
  logger,
103
117
  });
118
+ activeManager = manager;
104
119
  await manager.discover();
105
120
  await manager.versions.load();
106
121
  const coreVersion = await Promise.resolve().then(() => __importStar(require("node:fs/promises"))).then(({ readFile }) => readFile((0, node_path_1.resolve)(__dirname, "../../package.json"), "utf8").then((value) => JSON.parse(value).version).catch(() => "0.0.0"));
107
- const checkedVersions = await Promise.all([manager.versions.checkPackage("mu-agent-bridge", coreVersion), ...manager.list().map((plugin) => {
108
- const info = manager.packageInfo(plugin.id);
109
- return info.packageName && info.managed ? manager.versions.checkPackage(info.packageName, plugin.version) : Promise.resolve(undefined);
110
- })]);
111
- for (const status of checkedVersions.slice(1)) {
112
- if (status?.state !== "update-available")
113
- continue;
114
- const plugin = manager.list().find((record) => manager.packageInfo(record.id).packageName === status.packageName);
115
- if (!plugin)
116
- continue;
117
- try {
118
- await manager.upgradePackage(plugin.id, status.packageName, status.latestVersion);
119
- logger.info(`插件已升级:${plugin.id}`, { version: status.latestVersion });
120
- }
121
- catch (error) {
122
- logger.error(`插件升级失败,保留旧版本:${plugin.id}`, { error: error instanceof Error ? error.message : String(error) });
123
- }
124
- }
122
+ await checkAndUpgradePlugins(manager);
125
123
  const port = Number(process.env.AGENT_BRIDGE_PORT ?? "26107");
126
124
  const host = process.env.AGENT_BRIDGE_HOST ?? "127.0.0.1";
127
125
  if (host !== "127.0.0.1" && host !== "localhost" && host !== "::1" && !process.env.AGENT_BRIDGE_TOKEN) {
@@ -140,6 +138,9 @@ async function serve() {
140
138
  logger.error(`plugin failed: ${id}`, { error: error instanceof Error ? error.message : String(error) });
141
139
  }
142
140
  }
141
+ const intervalMs = Math.max(60_000, Number(process.env.AGENT_BRIDGE_PLUGIN_UPDATE_INTERVAL_MS ?? 300_000) || 300_000);
142
+ const updateTimer = setInterval(() => void checkAndUpgradePlugins(manager), intervalMs);
143
+ updateTimer.unref?.();
143
144
  let stopping = false;
144
145
  const shutdown = async () => {
145
146
  if (stopping)
@@ -154,20 +155,54 @@ async function serve() {
154
155
  }
155
156
  }
156
157
  await server.stop();
158
+ clearInterval(updateTimer);
157
159
  };
158
160
  process.once("SIGINT", () => void shutdown().finally(() => process.exit(0)));
159
161
  process.once("SIGTERM", () => void shutdown().finally(() => process.exit(0)));
160
162
  await new Promise(() => undefined);
161
163
  return 0;
162
164
  }
165
+ async function checkAndUpgradePlugins(manager) {
166
+ try {
167
+ const policy = await manager.updatePolicy();
168
+ const statuses = await manager.checkPackageUpdates();
169
+ for (const status of statuses.slice(1)) {
170
+ if (status.state !== "update-available")
171
+ continue;
172
+ const plugin = manager.list().find((record) => record.packageName === status.packageName);
173
+ if (!plugin)
174
+ continue;
175
+ if (policy.mode === "manual" || (!plugin.autoUpdate && policy.mode === "auto-install")) {
176
+ manager.log("info", `第三方插件有可用更新:${plugin.id}`, { packageName: status.packageName, version: status.latestVersion }, "升级检查");
177
+ continue;
178
+ }
179
+ try {
180
+ if (policy.mode === "download-only") {
181
+ await manager.downloadPackage(status.packageName, status.latestVersion);
182
+ continue;
183
+ }
184
+ if (!plugin.autoUpdate) {
185
+ manager.log("info", `第三方插件有可用更新:${plugin.id}`, { packageName: status.packageName, version: status.latestVersion }, "升级检查");
186
+ continue;
187
+ }
188
+ await manager.upgradePackage(plugin.id, status.packageName, status.latestVersion);
189
+ logger.info(`插件已升级:${plugin.id}`, { version: status.latestVersion });
190
+ }
191
+ catch (error) {
192
+ logger.error(`插件升级失败,保留旧版本:${plugin.id}`, { error: error instanceof Error ? error.message : String(error) });
193
+ }
194
+ }
195
+ }
196
+ catch (error) {
197
+ logger.warn("插件更新检查失败", { error: error instanceof Error ? error.message : String(error) });
198
+ }
199
+ }
163
200
  async function main() {
164
201
  const args = process.argv.slice(2);
165
202
  if (args.includes("pair"))
166
203
  return pair();
167
- if (args[0] === "update" && args.includes("--startup")) {
168
- console.log("核心升级由 Windows 启动器执行;请更新全局 mu-agent-bridge 后重新启动。");
169
- return 0;
170
- }
204
+ if (args[0] === "update")
205
+ return update();
171
206
  const selfTestIndex = args.indexOf("--self-test");
172
207
  if (selfTestIndex >= 0)
173
208
  return selfTest(args[selfTestIndex + 1] ?? "hi");
@@ -1,63 +1,21 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ControlServer = void 0;
4
- exports.configureAidForPlugin = configureAidForPlugin;
5
- exports.listExistingAids = listExistingAids;
6
- exports.listConnectedAids = listConnectedAids;
3
+ exports.ControlServer = exports.listExistingAids = void 0;
7
4
  const node_http_1 = require("node:http");
8
- const promises_1 = require("node:fs/promises");
9
5
  const node_child_process_1 = require("node:child_process");
10
- const node_util_1 = require("node:util");
11
- const node_os_1 = require("node:os");
12
- const node_path_1 = require("node:path");
13
6
  const node_crypto_1 = require("node:crypto");
14
- const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
7
+ const local_aids_1 = require("./local-aids");
8
+ const openai_adapter_1 = require("./plugins/qwenwork/openai-adapter");
9
+ var local_aids_2 = require("./local-aids");
10
+ Object.defineProperty(exports, "listExistingAids", { enumerable: true, get: function () { return local_aids_2.listExistingAids; } });
15
11
  const AID_PATTERN = /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?){2,}$/;
16
- async function runEc(args) {
17
- const command = process.platform === "win32" ? process.env.ComSpec ?? "cmd.exe" : "ec";
18
- const argv = process.platform === "win32" ? ["/d", "/s", "/c", `ec ${args.join(" ")}`] : args;
19
- try {
20
- const { stdout } = await execFileAsync(command, argv, { timeout: 120_000, maxBuffer: 1024 * 1024 });
21
- return JSON.parse(stdout);
22
- }
23
- catch (error) {
24
- const stdout = error.stdout;
25
- if (stdout) {
26
- try {
27
- return JSON.parse(stdout);
28
- }
29
- catch { /* 保留原始命令错误 */ }
30
- }
31
- throw error;
32
- }
33
- }
34
- async function configureAidForPlugin(pluginId, aid, port, run = runEc) {
35
- if (!aid || (pluginId !== "dsh" && pluginId !== "qwenwork"))
36
- return;
37
- const model = pluginId === "dsh" ? "dsh" : "qwenwork";
38
- const path = pluginId === "dsh" ? "/dsh/v1" : "/v1";
39
- const settings = [
40
- ["baseagents.ecagent.baseUrl", `http://127.0.0.1:${port}${path}`],
41
- ["baseagents.ecagent.apiKey", "mab-local"],
42
- ["baseagents.ecagent.model", model],
43
- ["active_baseagent", "ecagent"],
44
- ];
45
- for (const [key, value] of settings) {
46
- const result = await run(["agent", "set", aid, key, value, "--format", "json"]);
47
- if (!result?.ok)
48
- throw new Error(`配置 AID ${aid} 失败:${key}`);
49
- }
50
- const reloaded = await run(["agent", "reload", aid, "--format", "json"]);
51
- if (!reloaded?.ok)
52
- throw new Error(`重载 AID ${aid} 失败:${reloaded?.error ?? reloaded?.code ?? "未知错误"}`);
53
- }
54
12
  class ControlServer {
55
13
  options;
56
14
  server;
57
15
  address;
58
16
  pairingCode = String((0, node_crypto_1.randomInt)(0, 1_000_000)).padStart(6, "0");
59
17
  pairingExpiresAt = Date.now() + 10 * 60 * 1000;
60
- pairedTokens = new Set();
18
+ pairedTokens = new Map();
61
19
  pairingUsed = false;
62
20
  pairingFailures = 0;
63
21
  constructor(options) {
@@ -94,35 +52,67 @@ class ControlServer {
94
52
  }
95
53
  this.pairingUsed = true;
96
54
  const token = (0, node_crypto_1.randomBytes)(32).toString("hex");
97
- this.pairedTokens.add(token);
98
- return this.json(res, 200, { ok: true, token });
55
+ const expiresAt = Date.now() + 24 * 60 * 60 * 1000;
56
+ for (const [storedToken, expiry] of this.pairedTokens)
57
+ if (expiry <= Date.now())
58
+ this.pairedTokens.delete(storedToken);
59
+ this.pairedTokens.set(token, expiresAt);
60
+ return this.json(res, 200, { ok: true, token, expiresAt });
99
61
  }
100
62
  const authorization = req.headers.authorization;
101
- const authorized = authorization === `Bearer ${this.options.token}` || (authorization?.startsWith("Bearer ") && this.pairedTokens.has(authorization.slice(7)));
63
+ const authorized = authorization === `Bearer ${this.options.token}` || (authorization?.startsWith("Bearer ") && (this.pairedTokens.get(authorization.slice(7)) ?? 0) > Date.now());
102
64
  if (this.options.token && !isLoopback(req.socket.remoteAddress) && !authorized && !(method === "GET" && url.pathname.startsWith("/abweb")))
103
65
  return this.json(res, 401, { error: "unauthorized" });
66
+ if (parts[0] === "api" && parts[1] === "agents" && parts[2] && parts[3] === "v1") {
67
+ const id = decodeURIComponent(parts[2]);
68
+ const adapter = new openai_adapter_1.OpenAIAdapter(async (task) => (await this.options.manager.handle(id, { task })).text, { model: id, prefix: `/api/agents/${encodeURIComponent(id)}/v1`, onExchange: (direction, text, peer, aid) => this.options.manager.recordMessage(id, direction, text, peer, aid) });
69
+ await adapter.handle(req, res);
70
+ return;
71
+ }
104
72
  if (method === "GET" && url.pathname === "/api/health")
105
73
  return this.json(res, 200, { status: "ok", plugins: this.options.manager.list().length });
106
74
  if (method === "GET" && url.pathname === "/api/versions")
107
75
  return this.json(res, 200, { core: this.options.manager.versions.get("mu-agent-bridge") ?? null, plugins: this.options.manager.list().map((plugin) => ({ ...plugin, versionStatus: this.options.manager.versions.get(`agent-bridge-${plugin.id}`) ?? null })) });
76
+ if (method === "GET" && url.pathname === "/api/update-policy")
77
+ return this.json(res, 200, await this.options.manager.updatePolicy());
78
+ if (method === "PATCH" && url.pathname === "/api/update-policy")
79
+ return this.json(res, 200, await this.options.manager.saveUpdatePolicy(await this.body(req)).then(() => ({ ok: true })));
108
80
  if (method === "POST" && url.pathname === "/api/versions/check")
109
81
  return this.json(res, 200, { versions: await this.options.manager.checkPackageUpdates() });
82
+ if (method === "POST" && url.pathname === "/api/update") {
83
+ this.options.manager.log("info", "已请求后台升级,准备重启 daemon", undefined, "后台升级");
84
+ if (process.platform === "win32") {
85
+ const script = "$ErrorActionPreference='Continue'; Start-Sleep -Seconds 2; schtasks.exe /End /TN mab | Out-Null; npm.cmd install -g mu-agent-bridge@latest --no-fund --no-audit; schtasks.exe /Run /TN mab | Out-Null";
86
+ const child = (0, node_child_process_1.spawn)("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], { detached: true, stdio: "ignore", windowsHide: true });
87
+ child.unref();
88
+ }
89
+ else {
90
+ return this.json(res, 501, { error: "当前平台未配置后台升级器" });
91
+ }
92
+ return this.json(res, 202, { ok: true, message: "后台正在升级,服务将自动重启" });
93
+ }
110
94
  if (method === "GET" && url.pathname === "/api/messages")
111
95
  return this.json(res, 200, { messages: this.options.manager.recentMessages() });
96
+ if (method === "GET" && url.pathname === "/api/logs") {
97
+ const page = Number(url.searchParams.get("page") ?? "1");
98
+ const pageSize = Number(url.searchParams.get("pageSize") ?? "100");
99
+ return this.json(res, 200, await this.options.manager.logs.page(page, pageSize));
100
+ }
112
101
  if (method === "GET" && url.pathname === "/api/aids") {
113
- const aids = await listConnectedAids();
114
- const created = new Set(await this.options.manager.createdAids?.() ?? []);
115
- return this.json(res, 200, { aids, groups: { evolcore: aids.filter((aid) => !created.has(aid)), muAgentBridge: aids.filter((aid) => created.has(aid)) } });
102
+ const identity = this.identity();
103
+ const groups = identity ? await identity.list() : { evolcore: [], muAgentBridge: [] };
104
+ const aids = [...new Set([...groups.evolcore, ...groups.muAgentBridge])];
105
+ return this.json(res, 200, { aids, groups, localAids: (await (0, local_aids_1.listExistingAids)()).filter((aid) => !aids.includes(aid)), canCreate: Boolean(identity), message: identity ? "" : "请先安装并启动 AUN 插件,才能创建或关联 AID" });
116
106
  }
117
107
  if (method === "POST" && url.pathname === "/api/aids") {
118
108
  const body = await this.body(req);
119
109
  if (typeof body.aid !== "string" || !AID_PATTERN.test(body.aid) || typeof body.ownerAid !== "string" || !AID_PATTERN.test(body.ownerAid))
120
110
  return this.json(res, 400, { error: "需要有效的 AID 和 owner AID" });
121
- const project = (0, node_path_1.join)((0, node_os_1.homedir)(), ".mab", "projects", body.aid);
122
- const result = await runEc(["agent", "new", body.aid, "--non-interactive", "--owner", body.ownerAid, "--project", project, "--name", body.aid, "--description", `mu-agent-bridge 的 ${body.aid} Agent`, "--format", "json"]);
123
- if (result.ok)
124
- await this.options.manager.recordCreatedAid(body.aid);
125
- return this.json(res, result.ok ? 201 : 400, result);
111
+ const identity = this.identity();
112
+ if (!identity)
113
+ return this.json(res, 409, { error: "请先安装并启动 AUN 插件,才能创建 AID" });
114
+ const result = await identity.create(body.aid, body.ownerAid);
115
+ return this.json(res, result.status === "等待 Owner 确认" ? 202 : result.status === "existing" ? 200 : 201, result);
126
116
  }
127
117
  if (method === "GET" && url.pathname === "/api/plugins")
128
118
  return this.json(res, 200, { ok: true, plugins: this.options.manager.list() });
@@ -148,6 +138,10 @@ class ControlServer {
148
138
  const body = await this.body(req);
149
139
  return this.json(res, 200, { ok: true, plugin: await this.options.manager.upgradePackage(id, body.packageName, body.version) });
150
140
  }
141
+ if (method === "POST" && parts[3] === "download") {
142
+ const body = await this.body(req);
143
+ return this.json(res, 200, { ok: true, path: await this.options.manager.downloadPackage(body.packageName ?? `agent-bridge-${id}`, body.version) });
144
+ }
151
145
  if (method === "DELETE" && parts.length === 3) {
152
146
  await this.options.manager.uninstall(id);
153
147
  return this.json(res, 200, { ok: true });
@@ -156,17 +150,27 @@ class ControlServer {
156
150
  return this.json(res, 200, maskSecrets(await this.options.manager.config(id)));
157
151
  if (method === "PATCH" && parts[3] === "aid") {
158
152
  const body = await this.body(req);
159
- if (typeof body.aid !== "string" || (body.aid && !(await listConnectedAids()).includes(body.aid)))
160
- return this.json(res, 400, { error: "AID 当前未连接" });
153
+ if (typeof body.aid !== "string" || (body.aid && !AID_PATTERN.test(body.aid)))
154
+ return this.json(res, 400, { error: "AID 格式无效" });
161
155
  if (body.aid && (id === "dsh" || id === "qwenwork") && this.options.manager.get(id)?.record.status !== "ready")
162
156
  return this.json(res, 409, { error: "插件未启动" });
163
- await configureAidForPlugin(id, body.aid, this.address.port);
164
- await this.options.manager.setAid(id, body.aid);
157
+ const identity = this.identity();
158
+ if (body.aid && !identity)
159
+ return this.json(res, 409, { error: "请先安装并启动 AUN 插件,才能关联 AID" });
160
+ try {
161
+ if (identity)
162
+ await identity.bind(id, body.aid, this.address.port);
163
+ await this.options.manager.setAid(id, body.aid);
164
+ this.options.manager.log("info", "AID 关联已保存", { aid: body.aid, pluginId: id });
165
+ }
166
+ catch (error) {
167
+ this.options.manager.log("error", "AID 关联失败", { aid: body.aid, pluginId: id, error: error instanceof Error ? error.message : String(error) });
168
+ throw error;
169
+ }
165
170
  return this.json(res, 200, { ok: true });
166
171
  }
167
172
  if (method === "PATCH" && parts[3] === "config") {
168
- await this.options.manager.saveConfig(id, await this.body(req));
169
- return this.json(res, 200, { ok: true });
173
+ return this.json(res, 200, { ok: true, ...(await this.options.manager.saveConfig(id, await this.body(req))) });
170
174
  }
171
175
  if (method === "GET" && parts[3] === "health")
172
176
  return this.json(res, 200, await this.options.manager.health(id));
@@ -190,6 +194,10 @@ class ControlServer {
190
194
  return this.json(res, status, { error: error instanceof Error ? error.message : String(error) });
191
195
  }
192
196
  }
197
+ identity() {
198
+ const loaded = this.options.manager.get("aun");
199
+ return loaded?.record.status === "ready" ? loaded.instance?.identity : undefined;
200
+ }
193
201
  async body(req) { let raw = ""; for await (const chunk of req)
194
202
  raw += chunk; if (!raw)
195
203
  return {}; try {
@@ -204,35 +212,6 @@ exports.ControlServer = ControlServer;
204
212
  function isLoopback(address) {
205
213
  return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
206
214
  }
207
- async function listExistingAids() {
208
- const root = (0, node_path_1.resolve)(process.env.EVOLCORE_HOME ?? (0, node_path_1.join)((0, node_os_1.homedir)(), ".evolcore"), "AIDs");
209
- let names;
210
- try {
211
- names = await (0, promises_1.readdir)(root);
212
- }
213
- catch {
214
- return [];
215
- }
216
- const available = await Promise.all(names.filter((name) => /^[a-zA-Z0-9.-]+$/.test(name)).map(async (name) => {
217
- try {
218
- await (0, promises_1.access)((0, node_path_1.join)(root, name, "private", "key.json"));
219
- return name;
220
- }
221
- catch {
222
- return undefined;
223
- }
224
- }));
225
- return available.filter((name) => Boolean(name)).sort();
226
- }
227
- async function listConnectedAids(run = runEc) {
228
- const result = await run(["agent", "list", "--format", "json"]);
229
- if (!result.ok || !Array.isArray(result.agents))
230
- throw new Error("读取 EvolCore Agent 列表失败");
231
- const candidates = result.agents.filter((agent) => agent.status === "running" && typeof agent.aid === "string" && AID_PATTERN.test(agent.aid) && agent.channels?.some((channel) => channel.startsWith("aun#")));
232
- const details = await Promise.all(candidates.map((agent) => run(["agent", "show", agent.aid, "--format", "json"]).catch(() => null)));
233
- const local = new Set(await listExistingAids());
234
- return details.filter((detail) => detail?.ok && detail.status === "running" && detail.connection?.status === "connected" && local.has(detail.aid)).map((detail) => detail.aid).sort();
235
- }
236
215
  function maskSecrets(value) {
237
216
  if (Array.isArray(value))
238
217
  return value.map(maskSecrets);
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.listExistingAids = listExistingAids;
4
+ const promises_1 = require("node:fs/promises");
5
+ const node_os_1 = require("node:os");
6
+ const node_path_1 = require("node:path");
7
+ async function listExistingAids() {
8
+ const root = (0, node_path_1.resolve)(process.env.EVOLCORE_HOME ?? (0, node_path_1.join)((0, node_os_1.homedir)(), ".evolcore"), "AIDs");
9
+ let names;
10
+ try {
11
+ names = await (0, promises_1.readdir)(root);
12
+ }
13
+ catch {
14
+ return [];
15
+ }
16
+ const available = await Promise.all(names.filter((name) => /^[a-zA-Z0-9.-]+$/.test(name)).map(async (name) => {
17
+ try {
18
+ await (0, promises_1.access)((0, node_path_1.join)(root, name, "private", "key.json"));
19
+ return name;
20
+ }
21
+ catch {
22
+ return undefined;
23
+ }
24
+ }));
25
+ return available.filter((name) => Boolean(name)).sort();
26
+ }
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LogStore = void 0;
4
+ const promises_1 = require("node:fs/promises");
5
+ const node_path_1 = require("node:path");
6
+ const MAX_BYTES = 500 * 1024 * 1024;
7
+ class LogStore {
8
+ directory;
9
+ maxBytes;
10
+ queue = Promise.resolve();
11
+ constructor(directory, maxBytes = MAX_BYTES) {
12
+ this.directory = directory;
13
+ this.maxBytes = maxBytes;
14
+ }
15
+ append(entry) {
16
+ this.queue = this.queue.catch(() => undefined).then(async () => {
17
+ await (0, promises_1.mkdir)(this.directory, { recursive: true });
18
+ const file = (0, node_path_1.join)(this.directory, "mab.log");
19
+ const line = `${JSON.stringify(entry)}\n`;
20
+ let size = 0;
21
+ try {
22
+ size = (await (0, promises_1.stat)(file)).size;
23
+ }
24
+ catch { /* 首次写入 */ }
25
+ if (size + Buffer.byteLength(line) > this.maxBytes / 2) {
26
+ const archive = (0, node_path_1.join)(this.directory, "mab.log.1");
27
+ try {
28
+ await (0, promises_1.unlink)(archive);
29
+ }
30
+ catch { /* 无归档 */ }
31
+ try {
32
+ await (0, promises_1.rename)(file, archive);
33
+ }
34
+ catch { /* 并发首次写入 */ }
35
+ }
36
+ await (0, promises_1.appendFile)(file, line, "utf8");
37
+ });
38
+ }
39
+ async flush() { await this.queue; }
40
+ async page(page = 1, pageSize = 100) {
41
+ const size = Math.min(100, Math.max(1, Math.floor(pageSize) || 100));
42
+ const current = Math.max(1, Math.floor(page) || 1);
43
+ const files = [(0, node_path_1.join)(this.directory, "mab.log.1"), (0, node_path_1.join)(this.directory, "mab.log")];
44
+ const entries = [];
45
+ for (const file of files) {
46
+ try {
47
+ const text = await (0, promises_1.readFile)(file, "utf8");
48
+ for (const line of text.split("\n")) {
49
+ if (!line.trim())
50
+ continue;
51
+ try {
52
+ entries.push(JSON.parse(line));
53
+ }
54
+ catch { /* 忽略损坏行 */ }
55
+ }
56
+ }
57
+ catch { /* 文件尚未创建 */ }
58
+ }
59
+ entries.sort((a, b) => b.time.localeCompare(a.time));
60
+ const start = (current - 1) * size;
61
+ return { logs: entries.slice(start, start + size), page: current, pageSize: size, total: entries.length, hasNext: start + size < entries.length };
62
+ }
63
+ }
64
+ exports.LogStore = LogStore;