mu-agent-bridge 1.0.5 → 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.
- package/README.md +16 -11
- package/dist/src-ts/cli.js +60 -25
- package/dist/src-ts/control-server.js +73 -94
- package/dist/src-ts/local-aids.js +26 -0
- package/dist/src-ts/log-store.js +64 -0
- package/dist/src-ts/plugin-manager.js +178 -37
- package/dist/src-ts/plugin-manifest.js +5 -1
- package/dist/src-ts/plugins/abweb/index.js +1 -1
- package/dist/src-ts/plugins/abweb/public/app.js +141 -101
- package/dist/src-ts/plugins/abweb/public/index.html +6 -5
- package/dist/src-ts/plugins/abweb/public/style.css +12 -13
- package/dist/src-ts/plugins/aun/evolcore.js +54 -0
- package/dist/src-ts/plugins/aun/index.js +252 -50
- package/dist/src-ts/plugins/aun/public/app.js +66 -0
- package/dist/src-ts/plugins/aun/public/index.html +14 -0
- package/dist/src-ts/plugins/aun/public/style.css +21 -0
- package/dist/src-ts/plugins/doubao/cdp-client.js +1 -1
- package/dist/src-ts/plugins/doubao/index.js +2 -2
- package/dist/src-ts/plugins/feishu/client.js +133 -0
- package/dist/src-ts/plugins/feishu/index.js +214 -0
- package/dist/src-ts/plugins/feishu/public/app.js +126 -0
- package/dist/src-ts/plugins/feishu/public/index.html +16 -0
- package/dist/src-ts/plugins/feishu/public/style.css +25 -0
- package/dist/src-ts/plugins/workbuddy/cdp-client.js +118 -0
- package/dist/src-ts/plugins/workbuddy/cli-client.js +39 -0
- package/dist/src-ts/plugins/workbuddy/index.js +139 -0
- package/dist/src-ts/version-manager.js +9 -1
- package/dist/tests-ts/abweb-plugin.test.js +13 -2
- package/dist/tests-ts/aun-plugin.test.js +108 -4
- package/dist/tests-ts/control-server.test.js +88 -6
- package/dist/tests-ts/doubao-plugin.test.js +14 -0
- package/dist/tests-ts/feishu-client.test.js +72 -0
- package/dist/tests-ts/feishu-page.test.js +47 -0
- package/dist/tests-ts/feishu-plugin.test.js +96 -0
- package/dist/tests-ts/log-store.test.js +27 -0
- package/dist/tests-ts/platform-integration.test.js +40 -1
- package/dist/tests-ts/plugin-catalog.test.js +8 -0
- package/dist/tests-ts/plugin-manager.test.js +62 -13
- package/dist/tests-ts/workbuddy-plugin.test.js +85 -0
- package/package.json +30 -6
- 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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
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)。
|
package/dist/src-ts/cli.js
CHANGED
|
@@ -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
|
-
|
|
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"
|
|
168
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
98
|
-
|
|
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.
|
|
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
|
|
114
|
-
const
|
|
115
|
-
|
|
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
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return this.json(res, 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 && !
|
|
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
|
-
|
|
164
|
-
|
|
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;
|