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.
- 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 +165 -104
- package/dist/src-ts/plugins/abweb/public/index.html +6 -5
- package/dist/src-ts/plugins/abweb/public/style.css +12 -12
- 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 +19 -0
- 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
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.PluginManager = void 0;
|
|
4
7
|
const promises_1 = require("node:fs/promises");
|
|
@@ -11,6 +14,8 @@ const agent_1 = require("./agent");
|
|
|
11
14
|
const plugin_manifest_1 = require("./plugin-manifest");
|
|
12
15
|
const version_manager_1 = require("./version-manager");
|
|
13
16
|
const plugin_catalog_1 = require("./plugin-catalog");
|
|
17
|
+
const log_store_1 = require("./log-store");
|
|
18
|
+
const ajv_1 = __importDefault(require("ajv"));
|
|
14
19
|
const logger = { info() { }, warn() { }, error() { } };
|
|
15
20
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
16
21
|
class PluginManager {
|
|
@@ -20,11 +25,55 @@ class PluginManager {
|
|
|
20
25
|
root;
|
|
21
26
|
messages = [];
|
|
22
27
|
versions;
|
|
28
|
+
logs;
|
|
23
29
|
constructor(options) {
|
|
24
30
|
this.options = options;
|
|
25
31
|
this.root = (0, node_path_1.resolve)(options.pluginsDir);
|
|
26
32
|
this.versions = options.versions ?? new version_manager_1.VersionManager({ stateDir: options.stateDir });
|
|
33
|
+
this.logs = new log_store_1.LogStore((0, node_path_1.join)(options.stateDir, "logs"));
|
|
27
34
|
}
|
|
35
|
+
log(level, message, fields, source = "mab") { this.logs.append({ time: new Date().toISOString(), level, source, message, ...(fields ? { fields } : {}) }); }
|
|
36
|
+
async updatePolicy() { try {
|
|
37
|
+
const value = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(this.options.stateDir, "update-policy.json"), "utf8"));
|
|
38
|
+
return isUpdatePolicy(value) ? value : { mode: "auto-install" };
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return { mode: "auto-install" };
|
|
42
|
+
} }
|
|
43
|
+
async saveUpdatePolicy(policy) { if (!isUpdatePolicy(policy))
|
|
44
|
+
throw new agent_1.AgentError("更新策略无效"); await (0, promises_1.mkdir)(this.options.stateDir, { recursive: true }); await (0, promises_1.writeFile)((0, node_path_1.join)(this.options.stateDir, "update-policy.json"), JSON.stringify(policy, null, 2)); this.log("info", `插件更新策略已设置:${policy.mode}`, undefined, "升级策略"); }
|
|
45
|
+
async downloadPackage(packageName, version) {
|
|
46
|
+
if (!/^agent-bridge-[a-z0-9-]+$/.test(packageName))
|
|
47
|
+
throw new agent_1.AgentError("仅支持 agent-bridge-* npm 包名");
|
|
48
|
+
if (version && !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version))
|
|
49
|
+
throw new agent_1.AgentError("插件版本号无效");
|
|
50
|
+
const target = (0, node_path_1.join)(this.options.stateDir, "downloads", packageName, version ?? "latest");
|
|
51
|
+
await (0, promises_1.rm)(target, { recursive: true, force: true });
|
|
52
|
+
await (0, promises_1.mkdir)(target, { recursive: true });
|
|
53
|
+
this.log("info", `开始下载插件:${packageName}`, { version }, "插件升级");
|
|
54
|
+
try {
|
|
55
|
+
const spec = version ? `${packageName}@${version}` : packageName;
|
|
56
|
+
if (process.platform === "win32")
|
|
57
|
+
await execFileAsync("npm.cmd", ["install", "--prefix", target, "--no-save", "--ignore-scripts", spec], { shell: true });
|
|
58
|
+
else
|
|
59
|
+
await execFileAsync("npm", ["install", "--prefix", target, "--no-save", "--ignore-scripts", spec]);
|
|
60
|
+
const candidate = (0, node_path_1.join)(target, "node_modules", packageName);
|
|
61
|
+
const meta = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(candidate, "package.json"), "utf8"));
|
|
62
|
+
if (meta.name !== packageName || !meta.version || (version && meta.version !== version) || !meta.agentBridge?.plugin)
|
|
63
|
+
throw new agent_1.AgentError("npm 包声明无效");
|
|
64
|
+
const manifest = (0, plugin_manifest_1.validateManifest)(JSON.parse(await (0, promises_1.readFile)((0, node_path_1.resolve)(candidate, meta.agentBridge.plugin), "utf8")), candidate);
|
|
65
|
+
if (manifest.id !== packageName.slice("agent-bridge-".length) || manifest.version !== meta.version)
|
|
66
|
+
throw new agent_1.AgentError("npm 包与插件清单不匹配");
|
|
67
|
+
this.log("info", `插件下载完成:${manifest.id}`, { packageName, version: meta.version }, "插件升级");
|
|
68
|
+
return target;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
await (0, promises_1.rm)(target, { recursive: true, force: true });
|
|
72
|
+
this.log("error", `插件下载失败:${packageName}`, { error: error instanceof Error ? error.message : String(error) }, "插件升级");
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async cachedPackage(packageName, version) { const target = (0, node_path_1.join)(this.options.stateDir, "downloads", packageName, version ?? "latest"); return (0, node_fs_1.existsSync)((0, node_path_1.join)(target, "node_modules", packageName)) ? target : undefined; }
|
|
28
77
|
async discover() {
|
|
29
78
|
await (0, promises_1.mkdir)(this.root, { recursive: true });
|
|
30
79
|
await this.discoverNpmPackages();
|
|
@@ -78,15 +127,15 @@ class PluginManager {
|
|
|
78
127
|
if (!(0, node_path_1.resolve)(loaded.record.path).startsWith((0, node_path_1.resolve)(this.options.stateDir, "npm")))
|
|
79
128
|
throw new agent_1.AgentError("内置插件不能通过 npm 升级");
|
|
80
129
|
const prefix = (0, node_path_1.join)(this.options.stateDir, "npm", packageName);
|
|
81
|
-
const
|
|
130
|
+
const cached = await this.cachedPackage(packageName, version);
|
|
131
|
+
const staging = cached ?? (0, node_path_1.join)(this.options.stateDir, "npm", `.staging-${packageName}-${Date.now()}`);
|
|
82
132
|
const backup = `${prefix}.backup-${Date.now()}`;
|
|
133
|
+
let stopped = false;
|
|
134
|
+
this.log("info", `开始升级插件:${id}`, { packageName, version }, "插件升级");
|
|
83
135
|
await (0, promises_1.mkdir)(staging, { recursive: true });
|
|
84
136
|
try {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
await execFileAsync("npm.cmd", ["install", "--prefix", staging, "--no-save", "--ignore-scripts", spec]);
|
|
88
|
-
else
|
|
89
|
-
await execFileAsync("npm", ["install", "--prefix", staging, "--no-save", "--ignore-scripts", spec]);
|
|
137
|
+
if (!cached)
|
|
138
|
+
await this.downloadPackage(packageName, version).then((downloaded) => (0, promises_1.rename)(downloaded, staging));
|
|
90
139
|
const candidate = (0, node_path_1.join)(staging, "node_modules", packageName);
|
|
91
140
|
const meta = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(candidate, "package.json"), "utf8"));
|
|
92
141
|
if (meta.name !== packageName || !meta.version || (version && meta.version !== version))
|
|
@@ -97,6 +146,7 @@ class PluginManager {
|
|
|
97
146
|
if (manifest.id !== id || manifest.version !== meta.version)
|
|
98
147
|
throw new agent_1.AgentError("npm 包插件 ID 或版本不匹配");
|
|
99
148
|
await this.stop(id, "reload");
|
|
149
|
+
stopped = true;
|
|
100
150
|
await (0, promises_1.rename)(prefix, backup);
|
|
101
151
|
await (0, promises_1.rename)(staging, prefix);
|
|
102
152
|
this.plugins.delete(id);
|
|
@@ -105,6 +155,7 @@ class PluginManager {
|
|
|
105
155
|
const record = this.plugins.get(id).record;
|
|
106
156
|
await this.versions.checkPackage(packageName, record.version);
|
|
107
157
|
await (0, promises_1.rm)(backup, { recursive: true, force: true });
|
|
158
|
+
this.log("info", `插件升级成功:${id}`, { packageName, version: record.version }, "插件升级");
|
|
108
159
|
return record;
|
|
109
160
|
}
|
|
110
161
|
catch (error) {
|
|
@@ -113,12 +164,15 @@ class PluginManager {
|
|
|
113
164
|
await (0, promises_1.rm)(prefix, { recursive: true, force: true });
|
|
114
165
|
await (0, promises_1.rename)(backup, prefix);
|
|
115
166
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
167
|
+
if (stopped) {
|
|
168
|
+
this.plugins.delete(id);
|
|
169
|
+
try {
|
|
170
|
+
await this.discoverManagedPackages();
|
|
171
|
+
await this.start(id);
|
|
172
|
+
}
|
|
173
|
+
catch { /* 回滚后仍无法启动时保留原错误 */ }
|
|
120
174
|
}
|
|
121
|
-
|
|
175
|
+
this.log("error", `插件升级失败并已回滚:${id}`, { packageName, error: error instanceof Error ? error.message : String(error) }, "插件升级");
|
|
122
176
|
throw error;
|
|
123
177
|
}
|
|
124
178
|
}
|
|
@@ -139,9 +193,10 @@ class PluginManager {
|
|
|
139
193
|
if (await this.existsDir(prefix))
|
|
140
194
|
throw new agent_1.AgentError("插件包已经安装");
|
|
141
195
|
await (0, promises_1.mkdir)(prefix, { recursive: true });
|
|
196
|
+
this.log("info", `开始安装插件:${packageName}`, undefined, "插件安装");
|
|
142
197
|
try {
|
|
143
198
|
if (process.platform === "win32")
|
|
144
|
-
await execFileAsync("npm.cmd", ["install", "--prefix", prefix, "--no-save", "--ignore-scripts", packageName]);
|
|
199
|
+
await execFileAsync("npm.cmd", ["install", "--prefix", prefix, "--no-save", "--ignore-scripts", packageName], { shell: true });
|
|
145
200
|
else
|
|
146
201
|
await execFileAsync("npm", ["install", "--prefix", prefix, "--no-save", "--ignore-scripts", packageName]);
|
|
147
202
|
const directory = (0, node_path_1.join)(prefix, "node_modules", name);
|
|
@@ -158,10 +213,12 @@ class PluginManager {
|
|
|
158
213
|
const record = this.plugins.get(manifest.id)?.record;
|
|
159
214
|
if (!record)
|
|
160
215
|
throw new agent_1.AgentError("插件发现失败");
|
|
216
|
+
this.log("info", `插件安装成功:${manifest.id}`, { packageName, version: record.version }, "插件安装");
|
|
161
217
|
return record;
|
|
162
218
|
}
|
|
163
219
|
catch (error) {
|
|
164
220
|
await (0, promises_1.rm)(prefix, { recursive: true, force: true });
|
|
221
|
+
this.log("error", `插件安装失败:${packageName}`, { error: error instanceof Error ? error.message : String(error) }, "插件安装");
|
|
165
222
|
throw error;
|
|
166
223
|
}
|
|
167
224
|
}
|
|
@@ -182,6 +239,14 @@ class PluginManager {
|
|
|
182
239
|
throw new agent_1.AgentError(`unknown plugin: ${id}`);
|
|
183
240
|
if (loaded.instance && loaded.record.status === "ready")
|
|
184
241
|
return;
|
|
242
|
+
const compatibility = this.checkCompatibility(loaded.record);
|
|
243
|
+
loaded.record.compatibility = compatibility;
|
|
244
|
+
if (!compatibility.ok) {
|
|
245
|
+
loaded.record.status = "failed";
|
|
246
|
+
loaded.record.error = compatibility.reason;
|
|
247
|
+
this.log("error", `插件兼容性检查失败:${id}`, { reason: compatibility.reason }, "插件生命周期");
|
|
248
|
+
throw new agent_1.AgentError(compatibility.reason);
|
|
249
|
+
}
|
|
185
250
|
loaded.record.status = "starting";
|
|
186
251
|
try {
|
|
187
252
|
const instance = await this.load(loaded.record);
|
|
@@ -194,10 +259,10 @@ class PluginManager {
|
|
|
194
259
|
}
|
|
195
260
|
catch { /* defaults */ }
|
|
196
261
|
const pluginLogger = {
|
|
197
|
-
info: (message, fields) => { this.options.logger?.info(message, fields); if (fields?.direction && typeof fields.text === "string")
|
|
262
|
+
info: (message, fields) => { this.log("info", message, fields, id); this.options.logger?.info(message, fields); if (fields?.direction && typeof fields.text === "string")
|
|
198
263
|
this.recordMessage(id, fields.direction, fields.text, typeof fields.peer === "string" ? fields.peer : undefined, typeof fields.agentAid === "string" ? fields.agentAid : undefined); },
|
|
199
|
-
warn: (message, fields) => (this.options.logger ?? logger).warn(message, fields),
|
|
200
|
-
error: (message, fields) => (this.options.logger ?? logger).error(message, fields),
|
|
264
|
+
warn: (message, fields) => { this.log("warn", message, fields, id); (this.options.logger ?? logger).warn(message, fields); },
|
|
265
|
+
error: (message, fields) => { this.log("error", message, fields, id); (this.options.logger ?? logger).error(message, fields); },
|
|
201
266
|
};
|
|
202
267
|
await instance.activate({
|
|
203
268
|
dataDir,
|
|
@@ -213,17 +278,27 @@ class PluginManager {
|
|
|
213
278
|
ingress: {
|
|
214
279
|
publish: async (message) => {
|
|
215
280
|
const target = message.agentPlugin || "qwenwork";
|
|
216
|
-
|
|
217
|
-
|
|
281
|
+
this.recordMessage(target, "request", message.text, message.peerId);
|
|
282
|
+
try {
|
|
283
|
+
const response = await this.handle(target, { task: message.text, sessionId: `${message.channel}:${message.peerId}` });
|
|
284
|
+
await message.reply(response.text);
|
|
285
|
+
this.recordMessage(target, "reply", response.text, message.peerId);
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
this.recordMessage(target, "error", error instanceof Error ? error.message : String(error), message.peerId);
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
218
291
|
},
|
|
219
292
|
},
|
|
220
293
|
});
|
|
221
294
|
loaded.instance = instance;
|
|
222
295
|
loaded.record.status = "ready";
|
|
296
|
+
this.log("info", `插件已启动:${id}`, { version: loaded.record.version }, "插件生命周期");
|
|
223
297
|
}
|
|
224
298
|
catch (error) {
|
|
225
299
|
loaded.record.status = "failed";
|
|
226
300
|
loaded.record.error = error instanceof Error ? error.message : String(error);
|
|
301
|
+
this.log("error", `插件启动失败:${id}`, { error: loaded.record.error }, "插件生命周期");
|
|
227
302
|
throw error;
|
|
228
303
|
}
|
|
229
304
|
}
|
|
@@ -244,6 +319,8 @@ class PluginManager {
|
|
|
244
319
|
else
|
|
245
320
|
this.unregisterHttpRoutes(id);
|
|
246
321
|
loaded.record.status = "disabled";
|
|
322
|
+
this.log("info", `插件已停止:${id}`, { reason }, "插件生命周期");
|
|
323
|
+
await this.logs.flush();
|
|
247
324
|
}
|
|
248
325
|
async reload(id) { await this.stop(id, "reload"); await this.start(id); }
|
|
249
326
|
async uninstall(id) {
|
|
@@ -263,29 +340,33 @@ class PluginManager {
|
|
|
263
340
|
await (0, promises_1.rm)(isManaged ? managedPrefix : path, { recursive: true, force: true });
|
|
264
341
|
this.plugins.delete(id);
|
|
265
342
|
}
|
|
266
|
-
async saveConfig(id, config) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
await
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
catch { /* 首次记录 */ }
|
|
277
|
-
if (!aids.includes(aid))
|
|
278
|
-
await (0, promises_1.writeFile)(file, JSON.stringify([...aids, aid].sort(), null, 2));
|
|
279
|
-
}
|
|
280
|
-
async createdAids() {
|
|
343
|
+
async saveConfig(id, config) {
|
|
344
|
+
const loaded = this.plugins.get(id);
|
|
345
|
+
if (!loaded)
|
|
346
|
+
throw new agent_1.AgentError(`unknown plugin: ${id}`);
|
|
347
|
+
await this.validateConfig(loaded.record, config);
|
|
348
|
+
const previous = await this.config(id);
|
|
349
|
+
const dir = (0, node_path_1.join)(this.options.stateDir, id);
|
|
350
|
+
await (0, promises_1.mkdir)(dir, { recursive: true });
|
|
351
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "config.json"), JSON.stringify(config, null, 2));
|
|
352
|
+
const action = await this.reloadStrategy(loaded.record);
|
|
281
353
|
try {
|
|
282
|
-
|
|
283
|
-
|
|
354
|
+
if (action === "hot" && loaded.instance && typeof loaded.instance.reloadConfig === "function")
|
|
355
|
+
await loaded.instance.reloadConfig(config);
|
|
356
|
+
else if (action === "reload" && loaded.instance)
|
|
357
|
+
await this.reload(id);
|
|
358
|
+
else if (action === "hot" && loaded.instance)
|
|
359
|
+
throw new agent_1.AgentError("插件未提供 hot 配置重载接口");
|
|
360
|
+
this.log("info", `插件配置已保存:${id}`, { action }, "插件配置");
|
|
361
|
+
return { action, restartRequired: action === "restart" };
|
|
284
362
|
}
|
|
285
|
-
catch {
|
|
286
|
-
|
|
363
|
+
catch (error) {
|
|
364
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "config.json"), JSON.stringify(previous, null, 2));
|
|
365
|
+
this.log("error", `插件配置重载失败:${id}`, { action, error: error instanceof Error ? error.message : String(error) }, "插件配置");
|
|
366
|
+
throw error;
|
|
287
367
|
}
|
|
288
368
|
}
|
|
369
|
+
async setAid(id, aid) { await this.saveConfig(id, { ...await this.config(id), aid }); }
|
|
289
370
|
async config(id) { if (!this.plugins.has(id))
|
|
290
371
|
throw new agent_1.AgentError(`unknown plugin: ${id}`); try {
|
|
291
372
|
return JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(this.options.stateDir, id, "config.json"), "utf8"));
|
|
@@ -293,6 +374,29 @@ class PluginManager {
|
|
|
293
374
|
catch {
|
|
294
375
|
return {};
|
|
295
376
|
} }
|
|
377
|
+
async reloadStrategy(record) { if (!record.manifest.configSchema)
|
|
378
|
+
return "reload"; try {
|
|
379
|
+
const schema = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.resolve)(record.path, record.manifest.configSchema), "utf8"));
|
|
380
|
+
const value = schema["x-agent-bridge"]?.reload;
|
|
381
|
+
return value === "hot" || value === "restart" ? value : "reload";
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
return "reload";
|
|
385
|
+
} }
|
|
386
|
+
async validateConfig(record, config) { if (!config || typeof config !== "object" || Array.isArray(config))
|
|
387
|
+
throw new agent_1.AgentError("插件配置必须是对象"); if (!record.manifest.configSchema)
|
|
388
|
+
return; try {
|
|
389
|
+
const schema = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.resolve)(record.path, record.manifest.configSchema), "utf8"));
|
|
390
|
+
const { ["x-agent-bridge"]: _metadata, ...validationSchema } = schema;
|
|
391
|
+
const validator = new ajv_1.default({ allErrors: true }).compile(validationSchema);
|
|
392
|
+
if (!validator(config))
|
|
393
|
+
throw new agent_1.AgentError(`插件配置无效:${new ajv_1.default({ allErrors: true }).errorsText(validator.errors)}`);
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
if (error instanceof agent_1.AgentError)
|
|
397
|
+
throw error;
|
|
398
|
+
throw new agent_1.AgentError(`插件配置校验失败:${error instanceof Error ? error.message : String(error)}`);
|
|
399
|
+
} }
|
|
296
400
|
async health(id) { const loaded = this.plugins.get(id); if (!loaded)
|
|
297
401
|
throw new agent_1.AgentError(`unknown plugin: ${id}`); if (!loaded.instance)
|
|
298
402
|
return { status: loaded.record.status }; return loaded.instance.health(); }
|
|
@@ -450,12 +554,48 @@ class PluginManager {
|
|
|
450
554
|
const manifest = (0, plugin_manifest_1.validateManifest)(JSON.parse(await (0, promises_1.readFile)(manifestFile, "utf8")), directory);
|
|
451
555
|
if (this.plugins.has(manifest.id))
|
|
452
556
|
return;
|
|
453
|
-
|
|
557
|
+
let packageName;
|
|
558
|
+
let publisher;
|
|
559
|
+
let coreConstraint;
|
|
560
|
+
try {
|
|
561
|
+
const packageJson = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(directory, "package.json"), "utf8"));
|
|
562
|
+
packageName = packageJson.name;
|
|
563
|
+
publisher = packageJson._npmUser?.name ?? packageJson.maintainers?.[0]?.name;
|
|
564
|
+
coreConstraint = packageJson.agentBridge?.requires?.["mu-agent-bridge"];
|
|
565
|
+
const declared = packageJson.agentBridge?.requires?.plugins;
|
|
566
|
+
if (declared && JSON.stringify(declared) !== JSON.stringify(manifest.dependencies ?? {}))
|
|
567
|
+
throw new agent_1.AgentError("npm 包与插件清单的依赖声明冲突");
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
if (error instanceof agent_1.AgentError)
|
|
571
|
+
throw error; /* 内置插件没有 npm 元数据 */
|
|
572
|
+
}
|
|
573
|
+
if (coreConstraint) {
|
|
574
|
+
const coreVersion = require((0, node_path_1.resolve)(__dirname, "../../package.json")).version;
|
|
575
|
+
if (!(0, version_manager_1.satisfiesVersion)(coreVersion, coreConstraint))
|
|
576
|
+
throw new agent_1.AgentError(`核心版本 ${coreVersion} 不满足插件要求 ${coreConstraint}`);
|
|
577
|
+
}
|
|
578
|
+
const trustedNames = new Set((process.env.AGENT_BRIDGE_TRUSTED_PACKAGES ?? "").split(",").map((name) => name.trim()).filter(Boolean));
|
|
579
|
+
const trusted = Boolean(packageName && trustedNames.has(packageName));
|
|
580
|
+
this.plugins.set(manifest.id, { record: { id: manifest.id, version: manifest.version, type: manifest.type ?? "other", displayName: manifest.displayName ?? manifest.id, status: "discovered", capabilities: manifest.capabilities, manifest, path: directory, ...(packageName ? { packageName, publisher, trusted, autoUpdate: trusted } : {}), ...(coreConstraint ? { coreConstraint } : {}) } });
|
|
454
581
|
}
|
|
455
582
|
catch (error) {
|
|
456
583
|
throw new agent_1.AgentError(`invalid plugin ${label}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
457
584
|
}
|
|
458
585
|
}
|
|
586
|
+
checkCompatibility(record) {
|
|
587
|
+
const coreVersion = require((0, node_path_1.resolve)(__dirname, "../../package.json")).version;
|
|
588
|
+
if (record.coreConstraint && !(0, version_manager_1.satisfiesVersion)(coreVersion, record.coreConstraint))
|
|
589
|
+
return { ok: false, reason: `核心版本 ${coreVersion} 不满足插件要求 ${record.coreConstraint}` };
|
|
590
|
+
for (const [id, range] of Object.entries(record.manifest.dependencies ?? {})) {
|
|
591
|
+
const dependency = this.plugins.get(id)?.record;
|
|
592
|
+
if (!dependency)
|
|
593
|
+
return { ok: false, reason: `缺少插件依赖:${id}` };
|
|
594
|
+
if (!(0, version_manager_1.satisfiesVersion)(dependency.version, range))
|
|
595
|
+
return { ok: false, reason: `插件依赖 ${id}@${dependency.version} 不满足 ${range}` };
|
|
596
|
+
}
|
|
597
|
+
return { ok: true };
|
|
598
|
+
}
|
|
459
599
|
async exists(path) { try {
|
|
460
600
|
await (0, promises_1.readFile)((0, node_path_1.join)(path, "plugin.json"));
|
|
461
601
|
return true;
|
|
@@ -472,3 +612,4 @@ class PluginManager {
|
|
|
472
612
|
} }
|
|
473
613
|
}
|
|
474
614
|
exports.PluginManager = PluginManager;
|
|
615
|
+
function isUpdatePolicy(value) { return Boolean(value && typeof value === "object" && ["auto-install", "download-only", "manual"].includes(value.mode)); }
|
|
@@ -25,6 +25,10 @@ function validateManifest(value, baseDir) {
|
|
|
25
25
|
throw new agent_1.AgentError("插件类型与能力不匹配");
|
|
26
26
|
if (m.process && m.process !== "in-process" && m.process !== "isolated")
|
|
27
27
|
throw new agent_1.AgentError("invalid plugin process");
|
|
28
|
+
if (m.managementPage !== undefined && ((m.type ?? inferredType) !== "channel" || m.managementPage !== `/abweb/channels/${m.id}`))
|
|
29
|
+
throw new agent_1.AgentError("插件管理页面路径无效");
|
|
30
|
+
if (m.dependencies && (!m.dependencies || typeof m.dependencies !== "object" || Array.isArray(m.dependencies) || Object.entries(m.dependencies).some(([id, range]) => !/^[a-z][a-z0-9-]{1,63}$/.test(id) || typeof range !== "string" || !range.trim())))
|
|
31
|
+
throw new agent_1.AgentError("插件依赖声明无效");
|
|
28
32
|
if (baseDir) {
|
|
29
33
|
const root = (0, node_path_1.resolve)(baseDir);
|
|
30
34
|
const target = (0, node_path_1.resolve)(root, m.entry);
|
|
@@ -32,5 +36,5 @@ function validateManifest(value, baseDir) {
|
|
|
32
36
|
if ((0, node_path_1.isAbsolute)(m.entry) || rel.startsWith(`..${node_path_1.sep}`) || rel === "..")
|
|
33
37
|
throw new agent_1.AgentError("plugin entry path escapes plugin directory");
|
|
34
38
|
}
|
|
35
|
-
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 };
|
|
39
|
+
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 } : {}) };
|
|
36
40
|
}
|