mu-agent-bridge 1.0.22 → 1.0.23

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.
@@ -249,6 +249,16 @@ class PluginManager {
249
249
  throw new agent_1.AgentError(`unknown plugin: ${id}`);
250
250
  if (loaded.instance && loaded.record.status === "ready")
251
251
  return;
252
+ if (loaded.instance) {
253
+ try {
254
+ await loaded.instance.deactivate("reload");
255
+ }
256
+ catch (error) {
257
+ this.log("warn", `插件恢复前清理失败:${id}`, { error: error instanceof Error ? error.message : String(error) }, "插件生命周期");
258
+ }
259
+ this.unregisterHttpRoutes(id);
260
+ loaded.instance = undefined;
261
+ }
252
262
  if (path.has(id)) {
253
263
  const reason = `插件存在循环依赖:${[...path, id].join(" -> ")}`;
254
264
  loaded.record.status = "failed";
@@ -268,8 +278,9 @@ class PluginManager {
268
278
  for (const dependency of Object.keys(loaded.record.manifest.dependencies ?? {}))
269
279
  await this.startWithDependencies(dependency, next);
270
280
  loaded.record.status = "starting";
281
+ let instance;
271
282
  try {
272
- const instance = await this.load(loaded.record);
283
+ instance = await this.load(loaded.record);
273
284
  const dataDir = (0, node_path_1.join)(this.options.stateDir, id);
274
285
  await (0, promises_1.mkdir)(dataDir, { recursive: true });
275
286
  const configPath = (0, node_path_1.join)(dataDir, "config.json");
@@ -322,6 +333,16 @@ class PluginManager {
322
333
  this.log("info", `插件已启动:${id}`, { version: loaded.record.version }, "插件生命周期");
323
334
  }
324
335
  catch (error) {
336
+ if (instance) {
337
+ try {
338
+ await instance.deactivate("reload");
339
+ }
340
+ catch (cleanupError) {
341
+ this.log("warn", `插件启动失败后的清理失败:${id}`, { error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError) }, "插件生命周期");
342
+ }
343
+ this.unregisterHttpRoutes(id);
344
+ }
345
+ loaded.instance = undefined;
325
346
  loaded.record.status = "failed";
326
347
  loaded.record.error = error instanceof Error ? error.message : String(error);
327
348
  this.log("error", `插件启动失败:${id}`, { error: loaded.record.error }, "插件生命周期");
@@ -108,7 +108,7 @@
108
108
  item.type = "button";
109
109
  item.textContent = label;
110
110
  item.title = title;
111
- item.onclick = () => { void run().catch((cause) => error(String(cause))); };
111
+ item.onclick = () => { item.disabled = true; void run().catch((cause) => error(String(cause))).finally(() => { item.disabled = false; void load(false); }); };
112
112
  return item;
113
113
  }
114
114
  async function load(refreshCatalog = true) {
@@ -428,3 +428,41 @@ const channel_sessions_1 = require("../src-ts/channel-sessions");
428
428
  strict_1.default.equal(manifest.clientDownloadUrl, "https://tongyi.aliyun.com/");
429
429
  strict_1.default.throws(() => validateManifest({ id: "demo", version: "1.0.0", entry: "index.js", apiVersion: "1", capabilities: ["chat"], clientDownloadUrl: "file:///C:/QwenWorkCN.exe" }), /客户端下载地址/);
430
430
  });
431
+ (0, node_test_1.default)("启动失败清理实例和路由,再次启动及重载可以恢复", async () => {
432
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "mab-recovery-"));
433
+ try {
434
+ const dir = (0, node_path_1.join)(root, "recover");
435
+ await (0, promises_1.mkdir)(dir);
436
+ await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "plugin.json"), JSON.stringify({ id: "recover", version: "1.0.0", entry: "index.js", apiVersion: "1", capabilities: [] }));
437
+ await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "index.js"), `module.exports = class {
438
+ async activate(ctx) { this.ctx = ctx; ctx.http.register('GET','/partial',(_,res)=>res.end('ok')); if(globalThis.recoveryFail) throw new Error('桌面连接失败'); }
439
+ async deactivate() { globalThis.recoveryCleaned = (globalThis.recoveryCleaned || 0) + 1; }
440
+ async health() { return {status: globalThis.recoveryOffline ? 'failed' : 'ready'}; }
441
+ }`);
442
+ const manager = new plugin_manager_1.PluginManager({ pluginsDir: root, stateDir: (0, node_path_1.join)(root, "state") });
443
+ await manager.discover();
444
+ globalThis.recoveryFail = true;
445
+ await strict_1.default.rejects(manager.start("recover"), /桌面连接失败/);
446
+ strict_1.default.equal(manager.list()[0].status, "failed");
447
+ strict_1.default.equal(globalThis.recoveryCleaned, 1);
448
+ strict_1.default.equal(manager.get("recover")?.instance, undefined);
449
+ strict_1.default.equal(await manager.dispatchHttp({ method: "GET", url: "/partial" }, {}), false);
450
+ globalThis.recoveryFail = false;
451
+ await manager.start("recover");
452
+ strict_1.default.equal(manager.list()[0].status, "ready");
453
+ strict_1.default.equal(manager.list()[0].error, undefined);
454
+ await manager.stop("recover");
455
+ globalThis.recoveryFail = true;
456
+ await strict_1.default.rejects(manager.reload("recover"), /桌面连接失败/);
457
+ globalThis.recoveryFail = false;
458
+ globalThis.recoveryOffline = false;
459
+ await manager.reload("recover");
460
+ strict_1.default.equal(manager.list()[0].status, "ready");
461
+ await manager.stop("recover");
462
+ }
463
+ finally {
464
+ for (const key of ['recoveryFail', 'recoveryCleaned', 'recoveryOffline'])
465
+ delete globalThis[key];
466
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
467
+ }
468
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mu-agent-bridge",
3
- "version": "1.0.22",
3
+ "version": "1.0.23",
4
4
  "description": "将 AUN Agent 连接到本地桌面 AI 客户端的插件平台。",
5
5
  "main": "dist/src-ts/cli.js",
6
6
  "bin": {
@@ -108,7 +108,7 @@
108
108
  item.type = "button";
109
109
  item.textContent = label;
110
110
  item.title = title;
111
- item.onclick = () => { void run().catch((cause) => error(String(cause))); };
111
+ item.onclick = () => { item.disabled = true; void run().catch((cause) => error(String(cause))).finally(() => { item.disabled = false; void load(false); }); };
112
112
  return item;
113
113
  }
114
114
  async function load(refreshCatalog = true) {
@@ -75,7 +75,7 @@
75
75
  }
76
76
  function button(label: string, title: string, run: () => Promise<void>): HTMLButtonElement {
77
77
  const item = document.createElement("button"); item.type = "button"; item.textContent = label; item.title = title;
78
- item.onclick = () => { void run().catch((cause) => error(String(cause))); }; return item;
78
+ item.onclick = () => { item.disabled = true; void run().catch((cause) => error(String(cause))).finally(() => { item.disabled = false; void load(false); }); }; return item;
79
79
  }
80
80
  async function load(refreshCatalog = true): Promise<void> {
81
81
  if (loading) return;