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.
Files changed (42) hide show
  1. package/README.md +16 -11
  2. package/dist/src-ts/channel-sessions.js +68 -0
  3. package/dist/src-ts/cli.js +60 -25
  4. package/dist/src-ts/control-server.js +75 -94
  5. package/dist/src-ts/local-aids.js +26 -0
  6. package/dist/src-ts/log-store.js +64 -0
  7. package/dist/src-ts/plugin-manager.js +240 -39
  8. package/dist/src-ts/plugin-manifest.js +5 -1
  9. package/dist/src-ts/plugins/abweb/index.js +1 -1
  10. package/dist/src-ts/plugins/abweb/public/app.js +164 -100
  11. package/dist/src-ts/plugins/abweb/public/index.html +7 -6
  12. package/dist/src-ts/plugins/abweb/public/style.css +12 -13
  13. package/dist/src-ts/plugins/aun/evolcore.js +54 -0
  14. package/dist/src-ts/plugins/aun/index.js +253 -51
  15. package/dist/src-ts/plugins/aun/public/app.js +66 -0
  16. package/dist/src-ts/plugins/aun/public/index.html +14 -0
  17. package/dist/src-ts/plugins/aun/public/style.css +21 -0
  18. package/dist/src-ts/plugins/doubao/cdp-client.js +5 -4
  19. package/dist/src-ts/plugins/doubao/index.js +3 -2
  20. package/dist/src-ts/plugins/feishu/client.js +133 -0
  21. package/dist/src-ts/plugins/feishu/index.js +257 -0
  22. package/dist/src-ts/plugins/feishu/public/app.js +147 -0
  23. package/dist/src-ts/plugins/feishu/public/index.html +17 -0
  24. package/dist/src-ts/plugins/feishu/public/style.css +30 -0
  25. package/dist/src-ts/plugins/workbuddy/cdp-client.js +118 -0
  26. package/dist/src-ts/plugins/workbuddy/cli-client.js +39 -0
  27. package/dist/src-ts/plugins/workbuddy/index.js +139 -0
  28. package/dist/src-ts/version-manager.js +9 -1
  29. package/dist/tests-ts/abweb-plugin.test.js +13 -2
  30. package/dist/tests-ts/aun-plugin.test.js +108 -4
  31. package/dist/tests-ts/control-server.test.js +88 -6
  32. package/dist/tests-ts/doubao-plugin.test.js +18 -0
  33. package/dist/tests-ts/feishu-client.test.js +72 -0
  34. package/dist/tests-ts/feishu-page.test.js +53 -0
  35. package/dist/tests-ts/feishu-plugin.test.js +148 -0
  36. package/dist/tests-ts/log-store.test.js +27 -0
  37. package/dist/tests-ts/platform-integration.test.js +40 -1
  38. package/dist/tests-ts/plugin-catalog.test.js +8 -0
  39. package/dist/tests-ts/plugin-manager.test.js +219 -13
  40. package/dist/tests-ts/workbuddy-plugin.test.js +85 -0
  41. package/package.json +30 -6
  42. 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 staging = (0, node_path_1.join)(this.options.stateDir, "npm", `.staging-${packageName}-${Date.now()}`);
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
- const spec = version ? `${packageName}@${version}` : packageName;
86
- if (process.platform === "win32")
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
- this.plugins.delete(id);
117
- try {
118
- await this.discoverManagedPackages();
119
- await this.start(id);
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
- catch { /* 回滚后仍无法启动时保留原错误 */ }
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
- const response = await this.handle(target, { task: message.text, sessionId: `${message.channel}:${message.peerId}` });
217
- await message.reply(response.text);
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) { if (!this.plugins.has(id))
267
- throw new agent_1.AgentError(`unknown plugin: ${id}`); const dir = (0, node_path_1.join)(this.options.stateDir, id); await (0, promises_1.mkdir)(dir, { recursive: true }); await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "config.json"), JSON.stringify(config, null, 2)); }
268
- async setAid(id, aid) { await this.saveConfig(id, { ...await this.config(id), aid }); }
269
- async recordCreatedAid(aid) {
270
- const file = (0, node_path_1.join)(this.options.stateDir, "created-aids.json");
271
- await (0, promises_1.mkdir)(this.options.stateDir, { recursive: true });
272
- let aids = [];
273
- try {
274
- aids = JSON.parse(await (0, promises_1.readFile)(file, "utf8"));
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() {
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
- const aids = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(this.options.stateDir, "created-aids.json"), "utf8"));
283
- return Array.isArray(aids) ? aids.filter((aid) => typeof aid === "string") : [];
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
- return [];
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
- 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 } });
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
  }
@@ -5,7 +5,7 @@ const node_fs_1 = require("node:fs");
5
5
  const node_path_1 = require("node:path");
6
6
  const MANIFEST = {
7
7
  id: "abweb",
8
- version: "1.0.1",
8
+ version: "1.0.4",
9
9
  displayName: "Agent Bridge Web",
10
10
  entry: "dist/index.js",
11
11
  apiVersion: "1",