mu-agent-bridge 1.0.5 → 1.0.13
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/channel-sessions.js +68 -0
- package/dist/src-ts/cli.js +60 -25
- package/dist/src-ts/control-server.js +75 -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 +240 -39
- 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 +164 -100
- package/dist/src-ts/plugins/abweb/public/index.html +7 -6
- 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 +253 -51
- 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 +5 -4
- package/dist/src-ts/plugins/doubao/index.js +3 -2
- package/dist/src-ts/plugins/feishu/client.js +133 -0
- package/dist/src-ts/plugins/feishu/index.js +257 -0
- package/dist/src-ts/plugins/feishu/public/app.js +147 -0
- package/dist/src-ts/plugins/feishu/public/index.html +17 -0
- package/dist/src-ts/plugins/feishu/public/style.css +30 -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 +18 -0
- package/dist/tests-ts/feishu-client.test.js +72 -0
- package/dist/tests-ts/feishu-page.test.js +53 -0
- package/dist/tests-ts/feishu-plugin.test.js +148 -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 +219 -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,9 @@ 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 channel_sessions_1 = require("./channel-sessions");
|
|
19
|
+
const _2020_1 = __importDefault(require("ajv/dist/2020"));
|
|
14
20
|
const logger = { info() { }, warn() { }, error() { } };
|
|
15
21
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
16
22
|
class PluginManager {
|
|
@@ -20,11 +26,58 @@ class PluginManager {
|
|
|
20
26
|
root;
|
|
21
27
|
messages = [];
|
|
22
28
|
versions;
|
|
29
|
+
logs;
|
|
30
|
+
sessions;
|
|
23
31
|
constructor(options) {
|
|
24
32
|
this.options = options;
|
|
25
33
|
this.root = (0, node_path_1.resolve)(options.pluginsDir);
|
|
26
34
|
this.versions = options.versions ?? new version_manager_1.VersionManager({ stateDir: options.stateDir });
|
|
35
|
+
this.logs = new log_store_1.LogStore((0, node_path_1.join)(options.stateDir, "logs"));
|
|
36
|
+
this.sessions = new channel_sessions_1.ChannelSessions(options.stateDir);
|
|
27
37
|
}
|
|
38
|
+
log(level, message, fields, source = "mab") { this.logs.append({ time: new Date().toISOString(), level, source, message, ...(fields ? { fields } : {}) }); }
|
|
39
|
+
async updatePolicy() { try {
|
|
40
|
+
const value = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(this.options.stateDir, "update-policy.json"), "utf8"));
|
|
41
|
+
return isUpdatePolicy(value) ? value : { mode: "auto-install" };
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return { mode: "auto-install" };
|
|
45
|
+
} }
|
|
46
|
+
async saveUpdatePolicy(policy) { if (!isUpdatePolicy(policy))
|
|
47
|
+
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, "升级策略"); }
|
|
48
|
+
async downloadPackage(packageName, version) {
|
|
49
|
+
if (!/^agent-bridge-[a-z0-9-]+$/.test(packageName))
|
|
50
|
+
throw new agent_1.AgentError("仅支持 agent-bridge-* npm 包名");
|
|
51
|
+
if (version && !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version))
|
|
52
|
+
throw new agent_1.AgentError("插件版本号无效");
|
|
53
|
+
const target = (0, node_path_1.join)(this.options.stateDir, "downloads", packageName, version ?? "latest");
|
|
54
|
+
await (0, promises_1.rm)(target, { recursive: true, force: true });
|
|
55
|
+
await (0, promises_1.mkdir)(target, { recursive: true });
|
|
56
|
+
this.log("info", `开始下载插件:${packageName}`, { version }, "插件升级");
|
|
57
|
+
try {
|
|
58
|
+
const spec = version ? `${packageName}@${version}` : packageName;
|
|
59
|
+
if (process.platform === "win32")
|
|
60
|
+
await execFileAsync("npm.cmd", ["install", "--prefix", target, "--no-save", "--ignore-scripts", spec], { shell: true });
|
|
61
|
+
else
|
|
62
|
+
await execFileAsync("npm", ["install", "--prefix", target, "--no-save", "--ignore-scripts", spec]);
|
|
63
|
+
const candidate = (0, node_path_1.join)(target, "node_modules", packageName);
|
|
64
|
+
const meta = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(candidate, "package.json"), "utf8"));
|
|
65
|
+
if (meta.name !== packageName || !meta.version || (version && meta.version !== version) || !meta.agentBridge?.plugin)
|
|
66
|
+
throw new agent_1.AgentError("npm 包声明无效");
|
|
67
|
+
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);
|
|
68
|
+
if (manifest.id !== packageName.slice("agent-bridge-".length) || manifest.version !== meta.version)
|
|
69
|
+
throw new agent_1.AgentError("npm 包与插件清单不匹配");
|
|
70
|
+
this.validateCandidate(manifest, meta.agentBridge);
|
|
71
|
+
this.log("info", `插件下载完成:${manifest.id}`, { packageName, version: meta.version }, "插件升级");
|
|
72
|
+
return target;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
await (0, promises_1.rm)(target, { recursive: true, force: true });
|
|
76
|
+
this.log("error", `插件下载失败:${packageName}`, { error: error instanceof Error ? error.message : String(error) }, "插件升级");
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
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
81
|
async discover() {
|
|
29
82
|
await (0, promises_1.mkdir)(this.root, { recursive: true });
|
|
30
83
|
await this.discoverNpmPackages();
|
|
@@ -48,7 +101,7 @@ class PluginManager {
|
|
|
48
101
|
}
|
|
49
102
|
return this.list();
|
|
50
103
|
}
|
|
51
|
-
list() { return [...this.plugins.values()].map(({ record }) => ({ ...record, capabilities: [...record.capabilities] })); }
|
|
104
|
+
list() { return [...this.plugins.values()].map(({ record }) => ({ ...record, capabilities: [...record.capabilities], dependencyStatus: this.dependencyStatus(record) })); }
|
|
52
105
|
packageInfo(id) {
|
|
53
106
|
const record = this.plugins.get(id)?.record;
|
|
54
107
|
if (!record)
|
|
@@ -78,15 +131,15 @@ class PluginManager {
|
|
|
78
131
|
if (!(0, node_path_1.resolve)(loaded.record.path).startsWith((0, node_path_1.resolve)(this.options.stateDir, "npm")))
|
|
79
132
|
throw new agent_1.AgentError("内置插件不能通过 npm 升级");
|
|
80
133
|
const prefix = (0, node_path_1.join)(this.options.stateDir, "npm", packageName);
|
|
81
|
-
const
|
|
134
|
+
const cached = await this.cachedPackage(packageName, version);
|
|
135
|
+
const staging = cached ?? (0, node_path_1.join)(this.options.stateDir, "npm", `.staging-${packageName}-${Date.now()}`);
|
|
82
136
|
const backup = `${prefix}.backup-${Date.now()}`;
|
|
137
|
+
let stopped = false;
|
|
138
|
+
this.log("info", `开始升级插件:${id}`, { packageName, version }, "插件升级");
|
|
83
139
|
await (0, promises_1.mkdir)(staging, { recursive: true });
|
|
84
140
|
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]);
|
|
141
|
+
if (!cached)
|
|
142
|
+
await this.downloadPackage(packageName, version).then((downloaded) => (0, promises_1.rename)(downloaded, staging));
|
|
90
143
|
const candidate = (0, node_path_1.join)(staging, "node_modules", packageName);
|
|
91
144
|
const meta = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(candidate, "package.json"), "utf8"));
|
|
92
145
|
if (meta.name !== packageName || !meta.version || (version && meta.version !== version))
|
|
@@ -96,7 +149,9 @@ class PluginManager {
|
|
|
96
149
|
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);
|
|
97
150
|
if (manifest.id !== id || manifest.version !== meta.version)
|
|
98
151
|
throw new agent_1.AgentError("npm 包插件 ID 或版本不匹配");
|
|
152
|
+
this.validateCandidate(manifest, meta.agentBridge);
|
|
99
153
|
await this.stop(id, "reload");
|
|
154
|
+
stopped = true;
|
|
100
155
|
await (0, promises_1.rename)(prefix, backup);
|
|
101
156
|
await (0, promises_1.rename)(staging, prefix);
|
|
102
157
|
this.plugins.delete(id);
|
|
@@ -105,6 +160,7 @@ class PluginManager {
|
|
|
105
160
|
const record = this.plugins.get(id).record;
|
|
106
161
|
await this.versions.checkPackage(packageName, record.version);
|
|
107
162
|
await (0, promises_1.rm)(backup, { recursive: true, force: true });
|
|
163
|
+
this.log("info", `插件升级成功:${id}`, { packageName, version: record.version }, "插件升级");
|
|
108
164
|
return record;
|
|
109
165
|
}
|
|
110
166
|
catch (error) {
|
|
@@ -113,12 +169,15 @@ class PluginManager {
|
|
|
113
169
|
await (0, promises_1.rm)(prefix, { recursive: true, force: true });
|
|
114
170
|
await (0, promises_1.rename)(backup, prefix);
|
|
115
171
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
172
|
+
if (stopped) {
|
|
173
|
+
this.plugins.delete(id);
|
|
174
|
+
try {
|
|
175
|
+
await this.discoverManagedPackages();
|
|
176
|
+
await this.start(id);
|
|
177
|
+
}
|
|
178
|
+
catch { /* 回滚后仍无法启动时保留原错误 */ }
|
|
120
179
|
}
|
|
121
|
-
|
|
180
|
+
this.log("error", `插件升级失败并已回滚:${id}`, { packageName, error: error instanceof Error ? error.message : String(error) }, "插件升级");
|
|
122
181
|
throw error;
|
|
123
182
|
}
|
|
124
183
|
}
|
|
@@ -139,9 +198,10 @@ class PluginManager {
|
|
|
139
198
|
if (await this.existsDir(prefix))
|
|
140
199
|
throw new agent_1.AgentError("插件包已经安装");
|
|
141
200
|
await (0, promises_1.mkdir)(prefix, { recursive: true });
|
|
201
|
+
this.log("info", `开始安装插件:${packageName}`, undefined, "插件安装");
|
|
142
202
|
try {
|
|
143
203
|
if (process.platform === "win32")
|
|
144
|
-
await execFileAsync("npm.cmd", ["install", "--prefix", prefix, "--no-save", "--ignore-scripts", packageName]);
|
|
204
|
+
await execFileAsync("npm.cmd", ["install", "--prefix", prefix, "--no-save", "--ignore-scripts", packageName], { shell: true });
|
|
145
205
|
else
|
|
146
206
|
await execFileAsync("npm", ["install", "--prefix", prefix, "--no-save", "--ignore-scripts", packageName]);
|
|
147
207
|
const directory = (0, node_path_1.join)(prefix, "node_modules", name);
|
|
@@ -154,14 +214,17 @@ class PluginManager {
|
|
|
154
214
|
const manifest = (0, plugin_manifest_1.validateManifest)(JSON.parse(await (0, promises_1.readFile)(manifestFile, "utf8")), directory);
|
|
155
215
|
if (manifest.id !== name.slice("agent-bridge-".length) || manifest.version !== meta.version || (meta.agentBridge.type && meta.agentBridge.type !== manifest.type))
|
|
156
216
|
throw new agent_1.AgentError("npm 包与插件清单不匹配");
|
|
217
|
+
this.validateCandidate(manifest, meta.agentBridge);
|
|
157
218
|
await this.discoverDirectory(directory, name, manifestFile);
|
|
158
219
|
const record = this.plugins.get(manifest.id)?.record;
|
|
159
220
|
if (!record)
|
|
160
221
|
throw new agent_1.AgentError("插件发现失败");
|
|
222
|
+
this.log("info", `插件安装成功:${manifest.id}`, { packageName, version: record.version }, "插件安装");
|
|
161
223
|
return record;
|
|
162
224
|
}
|
|
163
225
|
catch (error) {
|
|
164
226
|
await (0, promises_1.rm)(prefix, { recursive: true, force: true });
|
|
227
|
+
this.log("error", `插件安装失败:${packageName}`, { error: error instanceof Error ? error.message : String(error) }, "插件安装");
|
|
165
228
|
throw error;
|
|
166
229
|
}
|
|
167
230
|
}
|
|
@@ -176,12 +239,31 @@ class PluginManager {
|
|
|
176
239
|
await this.discover();
|
|
177
240
|
return this.plugins.get(manifest.id).record;
|
|
178
241
|
}
|
|
179
|
-
async start(id) {
|
|
242
|
+
async start(id) { await this.startWithDependencies(id, new Set()); }
|
|
243
|
+
async startWithDependencies(id, path) {
|
|
180
244
|
const loaded = this.plugins.get(id);
|
|
181
245
|
if (!loaded)
|
|
182
246
|
throw new agent_1.AgentError(`unknown plugin: ${id}`);
|
|
183
247
|
if (loaded.instance && loaded.record.status === "ready")
|
|
184
248
|
return;
|
|
249
|
+
if (path.has(id)) {
|
|
250
|
+
const reason = `插件存在循环依赖:${[...path, id].join(" -> ")}`;
|
|
251
|
+
loaded.record.status = "failed";
|
|
252
|
+
loaded.record.error = reason;
|
|
253
|
+
throw new agent_1.AgentError(reason);
|
|
254
|
+
}
|
|
255
|
+
const compatibility = this.checkCompatibility(loaded.record);
|
|
256
|
+
loaded.record.compatibility = compatibility;
|
|
257
|
+
if (!compatibility.ok) {
|
|
258
|
+
loaded.record.status = "failed";
|
|
259
|
+
loaded.record.error = compatibility.reason;
|
|
260
|
+
this.log("error", `插件兼容性检查失败:${id}`, { reason: compatibility.reason }, "插件生命周期");
|
|
261
|
+
throw new agent_1.AgentError(compatibility.reason);
|
|
262
|
+
}
|
|
263
|
+
const next = new Set(path);
|
|
264
|
+
next.add(id);
|
|
265
|
+
for (const dependency of Object.keys(loaded.record.manifest.dependencies ?? {}))
|
|
266
|
+
await this.startWithDependencies(dependency, next);
|
|
185
267
|
loaded.record.status = "starting";
|
|
186
268
|
try {
|
|
187
269
|
const instance = await this.load(loaded.record);
|
|
@@ -194,10 +276,10 @@ class PluginManager {
|
|
|
194
276
|
}
|
|
195
277
|
catch { /* defaults */ }
|
|
196
278
|
const pluginLogger = {
|
|
197
|
-
info: (message, fields) => { this.options.logger?.info(message, fields); if (fields?.direction && typeof fields.text === "string")
|
|
279
|
+
info: (message, fields) => { this.log("info", message, fields, id); this.options.logger?.info(message, fields); if (fields?.direction && typeof fields.text === "string")
|
|
198
280
|
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),
|
|
281
|
+
warn: (message, fields) => { this.log("warn", message, fields, id); (this.options.logger ?? logger).warn(message, fields); },
|
|
282
|
+
error: (message, fields) => { this.log("error", message, fields, id); (this.options.logger ?? logger).error(message, fields); },
|
|
201
283
|
};
|
|
202
284
|
await instance.activate({
|
|
203
285
|
dataDir,
|
|
@@ -213,17 +295,28 @@ class PluginManager {
|
|
|
213
295
|
ingress: {
|
|
214
296
|
publish: async (message) => {
|
|
215
297
|
const target = message.agentPlugin || "qwenwork";
|
|
216
|
-
|
|
217
|
-
|
|
298
|
+
this.recordMessage(target, "request", message.text, message.peerId);
|
|
299
|
+
try {
|
|
300
|
+
const response = await this.sessions.run(message, target, (sessionId) => this.handle(target, { task: message.text, sessionId }));
|
|
301
|
+
await message.reply(response.text);
|
|
302
|
+
this.recordMessage(target, "reply", response.text, message.peerId);
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
this.recordMessage(target, "error", error instanceof Error ? error.message : String(error), message.peerId);
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
218
308
|
},
|
|
219
309
|
},
|
|
220
310
|
});
|
|
221
311
|
loaded.instance = instance;
|
|
222
312
|
loaded.record.status = "ready";
|
|
313
|
+
loaded.record.error = undefined;
|
|
314
|
+
this.log("info", `插件已启动:${id}`, { version: loaded.record.version }, "插件生命周期");
|
|
223
315
|
}
|
|
224
316
|
catch (error) {
|
|
225
317
|
loaded.record.status = "failed";
|
|
226
318
|
loaded.record.error = error instanceof Error ? error.message : String(error);
|
|
319
|
+
this.log("error", `插件启动失败:${id}`, { error: loaded.record.error }, "插件生命周期");
|
|
227
320
|
throw error;
|
|
228
321
|
}
|
|
229
322
|
}
|
|
@@ -244,12 +337,17 @@ class PluginManager {
|
|
|
244
337
|
else
|
|
245
338
|
this.unregisterHttpRoutes(id);
|
|
246
339
|
loaded.record.status = "disabled";
|
|
340
|
+
this.log("info", `插件已停止:${id}`, { reason }, "插件生命周期");
|
|
341
|
+
await this.logs.flush();
|
|
247
342
|
}
|
|
248
343
|
async reload(id) { await this.stop(id, "reload"); await this.start(id); }
|
|
249
344
|
async uninstall(id) {
|
|
250
345
|
const loaded = this.plugins.get(id);
|
|
251
346
|
if (!loaded)
|
|
252
347
|
throw new agent_1.AgentError(`unknown plugin: ${id}`);
|
|
348
|
+
const users = [...this.plugins.values()].filter(({ record }) => Object.prototype.hasOwnProperty.call(record.manifest.dependencies ?? {}, id)).map(({ record }) => record.id);
|
|
349
|
+
if (users.length)
|
|
350
|
+
throw new agent_1.AgentError(`插件 ${id} 仍被依赖:${users.join("、")}`);
|
|
253
351
|
const path = (0, node_path_1.resolve)(loaded.record.path);
|
|
254
352
|
const packageName = (0, node_path_1.basename)(path);
|
|
255
353
|
const managedPrefix = (0, node_path_1.resolve)(this.options.stateDir, "npm", packageName);
|
|
@@ -263,29 +361,42 @@ class PluginManager {
|
|
|
263
361
|
await (0, promises_1.rm)(isManaged ? managedPrefix : path, { recursive: true, force: true });
|
|
264
362
|
this.plugins.delete(id);
|
|
265
363
|
}
|
|
266
|
-
async saveConfig(id, config) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
async createdAids() {
|
|
364
|
+
async saveConfig(id, config) {
|
|
365
|
+
const loaded = this.plugins.get(id);
|
|
366
|
+
if (!loaded)
|
|
367
|
+
throw new agent_1.AgentError(`unknown plugin: ${id}`);
|
|
368
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
369
|
+
throw new agent_1.AgentError("插件配置必须是对象");
|
|
370
|
+
const previous = await this.config(id);
|
|
371
|
+
const merged = { ...previous, ...config };
|
|
372
|
+
await this.validateConfig(loaded.record, merged);
|
|
373
|
+
const dir = (0, node_path_1.join)(this.options.stateDir, id);
|
|
374
|
+
await (0, promises_1.mkdir)(dir, { recursive: true });
|
|
375
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "config.json"), JSON.stringify(merged, null, 2));
|
|
376
|
+
const action = await this.reloadStrategy(loaded.record);
|
|
281
377
|
try {
|
|
282
|
-
|
|
283
|
-
|
|
378
|
+
if (action === "hot" && loaded.instance && typeof loaded.instance.reloadConfig === "function")
|
|
379
|
+
await loaded.instance.reloadConfig(merged);
|
|
380
|
+
else if (action === "reload" && loaded.instance)
|
|
381
|
+
await this.reload(id);
|
|
382
|
+
else if (action === "hot" && loaded.instance)
|
|
383
|
+
throw new agent_1.AgentError("插件未提供 hot 配置重载接口");
|
|
384
|
+
this.log("info", `插件配置已保存:${id}`, { action }, "插件配置");
|
|
385
|
+
return { action, restartRequired: action === "restart" };
|
|
284
386
|
}
|
|
285
|
-
catch {
|
|
286
|
-
|
|
387
|
+
catch (error) {
|
|
388
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "config.json"), JSON.stringify(previous, null, 2));
|
|
389
|
+
if (action === "reload" && !loaded.instance) {
|
|
390
|
+
try {
|
|
391
|
+
await this.start(id);
|
|
392
|
+
}
|
|
393
|
+
catch { /* 保留原始错误 */ }
|
|
394
|
+
}
|
|
395
|
+
this.log("error", `插件配置重载失败:${id}`, { action, error: error instanceof Error ? error.message : String(error) }, "插件配置");
|
|
396
|
+
throw error;
|
|
287
397
|
}
|
|
288
398
|
}
|
|
399
|
+
async setAid(id, aid) { await this.saveConfig(id, { ...await this.config(id), aid }); }
|
|
289
400
|
async config(id) { if (!this.plugins.has(id))
|
|
290
401
|
throw new agent_1.AgentError(`unknown plugin: ${id}`); try {
|
|
291
402
|
return JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(this.options.stateDir, id, "config.json"), "utf8"));
|
|
@@ -293,6 +404,30 @@ class PluginManager {
|
|
|
293
404
|
catch {
|
|
294
405
|
return {};
|
|
295
406
|
} }
|
|
407
|
+
async reloadStrategy(record) { if (!record.manifest.configSchema)
|
|
408
|
+
return "reload"; try {
|
|
409
|
+
const schema = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.resolve)(record.path, record.manifest.configSchema), "utf8"));
|
|
410
|
+
const value = schema["x-agent-bridge"]?.reload;
|
|
411
|
+
return value === "hot" || value === "restart" ? value : "reload";
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
return "reload";
|
|
415
|
+
} }
|
|
416
|
+
async validateConfig(record, config) { if (!config || typeof config !== "object" || Array.isArray(config))
|
|
417
|
+
throw new agent_1.AgentError("插件配置必须是对象"); if (!record.manifest.configSchema)
|
|
418
|
+
return; try {
|
|
419
|
+
const schema = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.resolve)(record.path, record.manifest.configSchema), "utf8"));
|
|
420
|
+
const { ["x-agent-bridge"]: _metadata, ...validationSchema } = schema;
|
|
421
|
+
const ajv = new _2020_1.default({ allErrors: true, strict: false });
|
|
422
|
+
const validator = ajv.compile(validationSchema);
|
|
423
|
+
if (!validator(config))
|
|
424
|
+
throw new agent_1.AgentError(`插件配置无效:${ajv.errorsText(validator.errors)}`);
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
if (error instanceof agent_1.AgentError)
|
|
428
|
+
throw error;
|
|
429
|
+
throw new agent_1.AgentError(`插件配置校验失败:${error instanceof Error ? error.message : String(error)}`);
|
|
430
|
+
} }
|
|
296
431
|
async health(id) { const loaded = this.plugins.get(id); if (!loaded)
|
|
297
432
|
throw new agent_1.AgentError(`unknown plugin: ${id}`); if (!loaded.instance)
|
|
298
433
|
return { status: loaded.record.status }; return loaded.instance.health(); }
|
|
@@ -450,12 +585,77 @@ class PluginManager {
|
|
|
450
585
|
const manifest = (0, plugin_manifest_1.validateManifest)(JSON.parse(await (0, promises_1.readFile)(manifestFile, "utf8")), directory);
|
|
451
586
|
if (this.plugins.has(manifest.id))
|
|
452
587
|
return;
|
|
453
|
-
|
|
588
|
+
let packageName;
|
|
589
|
+
let publisher;
|
|
590
|
+
let coreConstraint;
|
|
591
|
+
try {
|
|
592
|
+
const packageJson = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(directory, "package.json"), "utf8"));
|
|
593
|
+
packageName = packageJson.name;
|
|
594
|
+
publisher = packageJson._npmUser?.name ?? packageJson.maintainers?.[0]?.name;
|
|
595
|
+
coreConstraint = packageJson.agentBridge?.requires?.["mu-agent-bridge"];
|
|
596
|
+
const declared = packageJson.agentBridge?.requires?.plugins;
|
|
597
|
+
if (declared && JSON.stringify(declared) !== JSON.stringify(manifest.dependencies ?? {}))
|
|
598
|
+
throw new agent_1.AgentError("npm 包与插件清单的依赖声明冲突");
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
if (error instanceof agent_1.AgentError)
|
|
602
|
+
throw error; /* 内置插件没有 npm 元数据 */
|
|
603
|
+
}
|
|
604
|
+
const coreVersion = require((0, node_path_1.resolve)(__dirname, "../../package.json")).version;
|
|
605
|
+
const error = coreConstraint && !(0, version_manager_1.satisfiesVersion)(coreVersion, coreConstraint) ? `核心版本 ${coreVersion} 不满足插件要求 ${coreConstraint}` : undefined;
|
|
606
|
+
const trustedNames = new Set((process.env.AGENT_BRIDGE_TRUSTED_PACKAGES ?? "").split(",").map((name) => name.trim()).filter(Boolean));
|
|
607
|
+
const trusted = Boolean(packageName && trustedNames.has(packageName));
|
|
608
|
+
this.plugins.set(manifest.id, { record: { id: manifest.id, version: manifest.version, type: manifest.type ?? "other", displayName: manifest.displayName ?? manifest.id, status: error ? "failed" : "discovered", capabilities: manifest.capabilities, manifest, path: directory, ...(packageName ? { packageName, publisher, trusted, autoUpdate: trusted } : {}), ...(coreConstraint ? { coreConstraint } : {}), ...(error ? { error } : {}) } });
|
|
454
609
|
}
|
|
455
610
|
catch (error) {
|
|
456
611
|
throw new agent_1.AgentError(`invalid plugin ${label}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
457
612
|
}
|
|
458
613
|
}
|
|
614
|
+
checkCompatibility(record) {
|
|
615
|
+
const coreVersion = require((0, node_path_1.resolve)(__dirname, "../../package.json")).version;
|
|
616
|
+
if (record.coreConstraint && !(0, version_manager_1.satisfiesVersion)(coreVersion, record.coreConstraint))
|
|
617
|
+
return { ok: false, reason: `核心版本 ${coreVersion} 不满足插件要求 ${record.coreConstraint}` };
|
|
618
|
+
for (const [id, range] of Object.entries(record.manifest.dependencies ?? {})) {
|
|
619
|
+
const dependency = this.plugins.get(id)?.record;
|
|
620
|
+
if (!dependency)
|
|
621
|
+
return { ok: false, reason: `缺少插件依赖:${id}` };
|
|
622
|
+
if (!(0, version_manager_1.satisfiesVersion)(dependency.version, range))
|
|
623
|
+
return { ok: false, reason: `插件依赖 ${id}@${dependency.version} 不满足 ${range}` };
|
|
624
|
+
}
|
|
625
|
+
return { ok: true };
|
|
626
|
+
}
|
|
627
|
+
validateCandidate(manifest, metadata) {
|
|
628
|
+
const declared = metadata?.requires?.plugins;
|
|
629
|
+
if (declared && (Object.keys(declared).length !== Object.keys(manifest.dependencies ?? {}).length || Object.entries(declared).some(([id, range]) => manifest.dependencies?.[id] !== range)))
|
|
630
|
+
throw new agent_1.AgentError("npm 包与插件清单的依赖声明冲突");
|
|
631
|
+
const compatibility = this.checkCompatibility({ id: manifest.id, version: manifest.version, type: manifest.type ?? "other", displayName: manifest.displayName ?? manifest.id, status: "discovered", capabilities: manifest.capabilities, manifest, path: "", coreConstraint: metadata?.requires?.["mu-agent-bridge"] });
|
|
632
|
+
if (!compatibility.ok)
|
|
633
|
+
throw new agent_1.AgentError(compatibility.reason);
|
|
634
|
+
for (const { record } of this.plugins.values()) {
|
|
635
|
+
const range = record.manifest.dependencies?.[manifest.id];
|
|
636
|
+
if (range && !(0, version_manager_1.satisfiesVersion)(manifest.version, range))
|
|
637
|
+
throw new agent_1.AgentError(`插件 ${record.id} 要求 ${manifest.id}@${range},候选版本为 ${manifest.version}`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
dependencyStatus(record) {
|
|
641
|
+
const statuses = [];
|
|
642
|
+
for (const [id, range] of Object.entries(record.manifest.dependencies ?? {})) {
|
|
643
|
+
const dependency = this.plugins.get(id)?.record;
|
|
644
|
+
if (!dependency)
|
|
645
|
+
statuses.push(`缺少 ${id}`);
|
|
646
|
+
else if (!(0, version_manager_1.satisfiesVersion)(dependency.version, range))
|
|
647
|
+
statuses.push(`${id}@${dependency.version} 不满足 ${range}`);
|
|
648
|
+
}
|
|
649
|
+
if (this.hasCycle(record.id))
|
|
650
|
+
statuses.push("存在循环依赖");
|
|
651
|
+
return statuses;
|
|
652
|
+
}
|
|
653
|
+
hasCycle(id) {
|
|
654
|
+
const visit = (current, path) => { if (path.has(current))
|
|
655
|
+
return true; const record = this.plugins.get(current)?.record; if (!record)
|
|
656
|
+
return false; const next = new Set(path); next.add(current); return Object.keys(record.manifest.dependencies ?? {}).some((dependency) => visit(dependency, next)); };
|
|
657
|
+
return visit(id, new Set());
|
|
658
|
+
}
|
|
459
659
|
async exists(path) { try {
|
|
460
660
|
await (0, promises_1.readFile)((0, node_path_1.join)(path, "plugin.json"));
|
|
461
661
|
return true;
|
|
@@ -472,3 +672,4 @@ class PluginManager {
|
|
|
472
672
|
} }
|
|
473
673
|
}
|
|
474
674
|
exports.PluginManager = PluginManager;
|
|
675
|
+
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
|
}
|