mu-agent-bridge 1.0.64 → 1.0.66

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.
@@ -802,7 +802,8 @@ class PluginManager {
802
802
  async discoverDirectory(directory, label, manifestFile = (0, node_path_1.join)(directory, "plugin.json")) {
803
803
  try {
804
804
  const manifest = (0, plugin_manifest_1.validateManifest)(JSON.parse(await (0, promises_1.readFile)(manifestFile, "utf8")), directory);
805
- if (this.plugins.has(manifest.id))
805
+ const existing = this.plugins.get(manifest.id);
806
+ if (existing && (existing.instance || (0, node_path_1.resolve)(existing.record.path) === (0, node_path_1.resolve)(directory)))
806
807
  return;
807
808
  let packageName;
808
809
  let coreConstraint;
@@ -821,6 +822,20 @@ class PluginManager {
821
822
  const coreVersion = require((0, node_path_1.resolve)(__dirname, "../../package.json")).version;
822
823
  const error = coreConstraint && !(0, version_manager_1.satisfiesVersion)(coreVersion, coreConstraint) ? `核心版本 ${coreVersion} 不满足插件要求 ${coreConstraint}` : undefined;
823
824
  const 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 } : {}), ...(coreConstraint ? { coreConstraint } : {}), ...(error ? { error } : {}) };
825
+ if (existing) {
826
+ const versionOrder = (0, version_manager_1.compareVersions)(record.version, existing.record.version);
827
+ const candidateManaged = (0, node_path_1.resolve)(directory).startsWith(`${(0, node_path_1.resolve)(this.options.stateDir, "npm")}${node_path_1.sep}`);
828
+ const existingManaged = (0, node_path_1.resolve)(existing.record.path).startsWith(`${(0, node_path_1.resolve)(this.options.stateDir, "npm")}${node_path_1.sep}`);
829
+ // 新版本优先;同版本保留托管包,避免核心内置旧副本遮蔽显式安装包。
830
+ const existingFailed = existing.record.status === "failed";
831
+ const candidateFailed = record.status === "failed";
832
+ const keepExisting = candidateFailed !== existingFailed ? candidateFailed : versionOrder < 0 || (versionOrder === 0 && (!candidateManaged || existingManaged));
833
+ if (keepExisting) {
834
+ this.log("info", "忽略插件候选,保留当前可用版本", { id: manifest.id, ignoredPath: directory, ignoredVersion: record.version, selectedVersion: existing.record.version, reason: record.error }, "插件发现");
835
+ return;
836
+ }
837
+ this.log("info", "插件候选已替换", { id: manifest.id, ignoredPath: existing.record.path, ignoredVersion: existing.record.version, selectedVersion: record.version }, "插件发现");
838
+ }
824
839
  this.applyPluginTrust(record);
825
840
  this.plugins.set(manifest.id, { record });
826
841
  if (packageName && /^agent-bridge-[a-z0-9-]+$/.test(packageName)) {
@@ -331,7 +331,7 @@
331
331
  state.append(detail);
332
332
  }
333
333
  // 插件未就绪且声明了客户端下载地址时,给出获取桌面客户端的入口
334
- if (plugin.manifest?.clientDownloadUrl && plugin.status !== "ready") {
334
+ if (plugin.manifest?.clientDownloadUrl && plugin.error?.includes("未找到") && plugin.error.includes(".exe")) {
335
335
  const link = document.createElement("a");
336
336
  link.className = "plugin-id";
337
337
  link.href = plugin.manifest.clientDownloadUrl;
@@ -12,7 +12,7 @@ const agent_1 = require("../../agent");
12
12
  const messages_1 = require("./messages");
13
13
  const attachments_1 = require("./attachments");
14
14
  const manifest = {
15
- id: "aun", version: "1.0.33", type: "channel", displayName: "AUN 通道",
15
+ id: "aun", version: "1.0.34", type: "channel", displayName: "AUN 通道",
16
16
  entry: "dist/src-ts/plugins/aun/index.js", apiVersion: "1", process: "in-process",
17
17
  capabilities: ["channel", "channel.identity.create", "channel.receive", "channel.send", "health", "cli"],
18
18
  configSchema: "config.schema.json",
@@ -86,7 +86,7 @@ class AunPlugin {
86
86
  const known = new Set([...this.clients.keys(), ...this.listStoreAids()]);
87
87
  return {
88
88
  evolcore: [...known].filter((aid) => !own.has(aid)).sort(),
89
- muAgentBridge: [...this.clients.keys()].filter((aid) => own.has(aid)).sort(),
89
+ muAgentBridge: [...known].filter((aid) => own.has(aid)).sort(),
90
90
  localMuAgentBridge: [...own].filter((aid) => !known.has(aid)).sort(),
91
91
  };
92
92
  },
@@ -125,42 +125,29 @@ class AunPlugin {
125
125
  }
126
126
  },
127
127
  bind: async (pluginId, aid, _port) => {
128
+ if (!aid || !aidPattern.test(aid))
129
+ throw new Error("AID 格式无效,必须指定要关联的 AID");
128
130
  if (pluginId === "aun" || pluginId === "abweb")
129
131
  throw new Error("只能关联 Agent 插件");
132
+ if (this.context?.pluginStatus(pluginId) !== "ready")
133
+ throw new Error("目标 Agent 插件未启动");
130
134
  const previous = this.bindings;
131
135
  const previousTimes = this.bindingTimes;
132
- this.bindings = { ...previous };
133
- this.bindingTimes = { ...previousTimes };
136
+ const connection = this.clients.get(aid);
137
+ const hadIdentity = this.identities.has(aid);
138
+ const hadSubscription = Boolean(connection?.subscription);
134
139
  try {
135
- if (aid) {
136
- if (!aidPattern.test(aid))
137
- throw new Error("AID 格式无效");
138
- if (this.context?.pluginStatus(pluginId) !== "ready")
139
- throw new Error("目标 Agent 插件未启动");
140
- if (this.identities.has(aid) || this.legacyCreatedAids.has(aid)) {
141
- if (!this.clients.has(aid))
142
- await this.connectAid(aid);
143
- if (!this.clients.has(aid))
144
- throw new Error("AID 当前未连接");
145
- this.bindings[aid] = pluginId;
146
- }
147
- else {
148
- // 本地身份由插件直接接管,不再通过 ec 配置路由。
149
- const loaded = this.store.load(aid);
150
- if (!loaded.ok || !loaded.data)
151
- throw new Error("AID 不在本地密钥库中,无法关联");
152
- this.identities.add(aid);
153
- if (!this.clients.has(aid))
154
- await this.connectAid(aid);
155
- if (!this.clients.has(aid))
156
- throw new Error("AID 当前未连接");
157
- this.bindings[aid] = pluginId;
158
- }
159
- }
160
- else
161
- throw new Error("必须指定要关联的 AID");
162
- if (aid)
163
- this.bindingTimes[aid] = new Date().toISOString();
140
+ const loaded = this.store.load(aid);
141
+ if (!loaded.ok || !loaded.data)
142
+ throw new Error("AID 不在本地密钥库中,无法关联");
143
+ this.identities.add(aid);
144
+ if (!connection)
145
+ await this.connectAid(aid);
146
+ if (!this.clients.has(aid))
147
+ throw new Error("AID 当前未连接");
148
+ this.bindings = { ...previous, [aid]: pluginId };
149
+ this.bindingTimes = { ...previousTimes, [aid]: new Date().toISOString() };
150
+ this.subscribeAid(aid);
164
151
  await this.persist();
165
152
  }
166
153
  catch (error) {
@@ -168,6 +155,29 @@ class AunPlugin {
168
155
  this.bindingTimes = previousTimes;
169
156
  this.config.bindings = previous;
170
157
  this.config.binding_times = previousTimes;
158
+ if (!hadIdentity)
159
+ this.identities.delete(aid);
160
+ this.config.identities = [...this.identities].sort();
161
+ if (!hadSubscription) {
162
+ this.clients.get(aid)?.subscription?.unsubscribe();
163
+ if (connection)
164
+ connection.subscription = undefined;
165
+ }
166
+ if (!connection) {
167
+ try {
168
+ await this.clients.get(aid)?.client.close();
169
+ }
170
+ catch (closeError) {
171
+ this.context?.logger.warn("AUN 关联回滚关闭连接失败", { aid, error: safeError(closeError) });
172
+ }
173
+ finally {
174
+ this.clients.delete(aid);
175
+ if (!this.clients.size) {
176
+ this.status = "degraded";
177
+ this.healthMessage = "暂无已连接 AID";
178
+ }
179
+ }
180
+ }
171
181
  this.context?.logger.error("AUN 身份关联失败", { aid, pluginId, error: safeError(error) });
172
182
  throw error;
173
183
  }
@@ -178,6 +188,8 @@ class AunPlugin {
178
188
  throw new Error("AID 格式无效");
179
189
  if (this.bindings[aid] !== pluginId)
180
190
  throw new Error("指定 AID 当前未关联到该 Agent");
191
+ if ([...this.pendingMessages.keys()].some(key => JSON.parse(key)[0] === aid))
192
+ throw new Error("该 AID 正在处理消息,请等待任务完成后再解绑");
181
193
  const previous = this.bindings;
182
194
  const previousTimes = this.bindingTimes;
183
195
  this.bindings = { ...previous };
@@ -194,6 +206,21 @@ class AunPlugin {
194
206
  this.config.binding_times = previousTimes;
195
207
  throw error;
196
208
  }
209
+ const connection = this.clients.get(aid);
210
+ connection?.subscription?.unsubscribe();
211
+ try {
212
+ await connection?.client.close();
213
+ }
214
+ catch (error) {
215
+ this.context?.logger.warn("AUN 解绑已完成,关闭连接失败", { aid, pluginId, error: safeError(error) });
216
+ }
217
+ finally {
218
+ this.clients.delete(aid);
219
+ if (!this.clients.size) {
220
+ this.status = "degraded";
221
+ this.healthMessage = "暂无已连接 AID";
222
+ }
223
+ }
197
224
  this.context?.logger.info("AUN 单个身份关联已解除", { aid, pluginId });
198
225
  },
199
226
  };
@@ -207,6 +234,7 @@ class AunPlugin {
207
234
  async activate(context) {
208
235
  this.context = context;
209
236
  this.config = { ...context.config };
237
+ this.identities.clear();
210
238
  this.legacyCreatedAids.clear();
211
239
  this.createdAids.clear();
212
240
  try {
@@ -266,15 +294,25 @@ class AunPlugin {
266
294
  if (this.aid)
267
295
  this.identities.add(this.aid);
268
296
  this.acceptingMessages = true;
269
- for (const aid of this.identities)
270
- await this.connectAid(aid);
271
- // 密钥库中的其余 AID 也自动连接:不再依赖 ec 或 EvolCore 进程
272
- for (const aid of this.listStoreAids()) {
273
- this.identities.add(aid);
274
- if (!this.clients.has(aid))
297
+ // 只订阅明确关联 Agent 的身份,避免接管 EvolCore 或其它 AID 的消息。
298
+ for (const aid of Object.keys(this.bindings)) {
299
+ const target = this.bindings[aid];
300
+ if (!target || !context.pluginStatus(target)) {
301
+ context.logger.warn("AUN 跳过目标插件不存在的旧绑定,未连接身份", { aid, agentPlugin: target });
302
+ continue;
303
+ }
304
+ if (context.pluginStatus(target) !== "ready")
305
+ context.logger.warn("AUN 恢复已有显式绑定,目标插件尚未就绪;关联保持不变", { aid, agentPlugin: target, status: context.pluginStatus(target) });
306
+ if (aidPattern.test(aid))
275
307
  await this.connectAid(aid);
276
308
  }
277
309
  }
310
+ subscribeAid(aid) {
311
+ const connection = this.clients.get(aid);
312
+ if (!connection || connection.subscription || !this.bindings[aid])
313
+ return;
314
+ connection.subscription = connection.client.on("message.received", (payload) => this.onMessage(payload, aid));
315
+ }
278
316
  async connectAid(aid) {
279
317
  try {
280
318
  await this.prepareTrustRoots(aid);
@@ -282,7 +320,7 @@ class AunPlugin {
282
320
  if (!loaded.ok || !loaded.data)
283
321
  throw new Error(loaded.error?.message ?? "本地密钥库中不存在该 AID");
284
322
  const client = new this.sdk.AUNClient(loaded.data.aid);
285
- const subscription = this.identities.has(aid) || this.legacyCreatedAids.has(aid)
323
+ const subscription = this.bindings[aid]
286
324
  ? client.on("message.received", (payload) => this.onMessage(payload, aid))
287
325
  : undefined;
288
326
  try {
@@ -22,8 +22,11 @@ async function findWindowsExecutable(fileName, configured) {
22
22
  const configuredPath = (0, node_path_1.resolve)(configured);
23
23
  if (fileName.toLowerCase() === "doubao.exe") {
24
24
  let root = (0, node_path_1.dirname)(configuredPath);
25
- while (/^app$/i.test((0, node_path_1.basename)(root)) || versionDirectory.test((0, node_path_1.basename)(root)))
25
+ while (/^(?:app|launcher|doubao_update)$/i.test((0, node_path_1.basename)(root)) || versionDirectory.test((0, node_path_1.basename)(root)))
26
26
  root = (0, node_path_1.dirname)(root);
27
+ const updated = (0, node_path_1.join)(root, "Doubao_update", fileName);
28
+ if ((0, node_fs_1.existsSync)(updated))
29
+ return updated;
27
30
  const latest = await findExecutableInTree(root, fileName, /^/, 3);
28
31
  if (latest)
29
32
  return latest;
@@ -64,8 +67,11 @@ async function search(directory, fileName, pattern, depth, insideProduct) {
64
67
  const entries = await (0, promises_1.readdir)(directory, { withFileTypes: true });
65
68
  // 豆包上层文件是启动器,优先查找新版目录或 app 中的实际程序。
66
69
  if (insideProduct && depth > 0 && fileName.toLowerCase() === "doubao.exe") {
67
- const versions = entries.filter(entry => entry.isDirectory() && (versionDirectory.test(entry.name) || /^app$/i.test(entry.name)));
68
- versions.sort((left, right) => right.name.replace(/^\D+/, "").localeCompare(left.name.replace(/^\D+/, ""), "en", { numeric: true }));
70
+ const versions = entries.filter(entry => entry.isDirectory() && (versionDirectory.test(entry.name) || /^(?:app|doubao_update)$/i.test(entry.name)));
71
+ versions.sort((left, right) => {
72
+ const rank = (name) => /^doubao_update$/i.test(name) ? 2 : /^app$/i.test(name) ? 0 : 1;
73
+ return rank(right.name) - rank(left.name) || right.name.replace(/^\D+/, "").localeCompare(left.name.replace(/^\D+/, ""), "en", { numeric: true });
74
+ });
69
75
  for (const entry of versions) {
70
76
  const found = await search((0, node_path_1.join)(directory, entry.name), fileName, pattern, depth - 1, true);
71
77
  if (found)
@@ -73,7 +79,7 @@ async function search(directory, fileName, pattern, depth, insideProduct) {
73
79
  }
74
80
  }
75
81
  for (const entry of entries)
76
- if (entry.isFile() && insideProduct && entry.name.toLowerCase() === fileName.toLowerCase())
82
+ if (entry.isFile() && insideProduct && entry.name.toLowerCase() === fileName.toLowerCase() && !(fileName.toLowerCase() === "doubao.exe" && /^(?:launcher|doubao)$/i.test((0, node_path_1.basename)(directory))))
77
83
  return (0, node_path_1.join)(directory, entry.name);
78
84
  if (depth <= 0)
79
85
  return undefined;
@@ -16,7 +16,7 @@ const session_history_1 = require("./session-history");
16
16
  const desktop_launch_1 = require("../desktop-launch");
17
17
  const desktop_launch_2 = require("./desktop-launch");
18
18
  exports.workbuddyManifest = {
19
- id: "workbuddy", version: "1.1.28", displayName: "WorkBuddy", type: "agent",
19
+ id: "workbuddy", version: "1.1.29", displayName: "WorkBuddy", type: "agent",
20
20
  entry: "dist/index.js", apiVersion: "1", process: "in-process",
21
21
  capabilities: ["chat", "health"], configSchema: "config.schema.json",
22
22
  };
@@ -384,7 +384,19 @@ class WorkbuddyPlugin {
384
384
  this.activeDesktopConversationId = undefined;
385
385
  this.knownSessions.add(sessionId);
386
386
  await this.persistKnownSessions();
387
- await this.client.createTask(name);
387
+ try {
388
+ await this.client.createTask(name);
389
+ }
390
+ catch (error) {
391
+ this.knownSessions.delete(sessionId);
392
+ try {
393
+ await this.persistKnownSessions();
394
+ }
395
+ catch (rollbackError) {
396
+ throw new AggregateError([error, rollbackError], "WorkBuddy 新任务失败且会话记录回滚失败", { cause: error });
397
+ }
398
+ throw error;
399
+ }
388
400
  this.desktopSessionValid = true;
389
401
  this.expertSession = false;
390
402
  this.activeExpert = undefined;
@@ -63,3 +63,32 @@ class Element {
63
63
  strict_1.default.equal(saved.aid, undefined, "通过 PATCH 合并配置,不能改写通道关联 AID");
64
64
  strict_1.default.equal(node("#cli-config-dialog").open, false);
65
65
  });
66
+ (0, node_test_1.default)("客户端下载入口只在客户端可执行文件确实缺失时显示", async () => {
67
+ const script = await (0, promises_1.readFile)("src-ts/plugins/abweb/public/app.js", "utf8");
68
+ const render = async (error) => {
69
+ const nodes = new Map();
70
+ const node = (selector) => { if (!nodes.has(selector))
71
+ nodes.set(selector, new Element()); return nodes.get(selector); };
72
+ (0, node_vm_1.runInNewContext)(script, {
73
+ document: { querySelector: node, createElement: () => new Element(), hidden: false },
74
+ localStorage: { getItem: () => null, removeItem() { } }, window: { addEventListener() { } },
75
+ structuredClone, setInterval() { }, confirm: () => false,
76
+ fetch: async (path) => {
77
+ const values = {
78
+ "/api/health": { plugins: 1 },
79
+ "/api/plugins": { plugins: [{ id: "doubao", displayName: "豆包", type: "agent", version: "1.0.21", status: "failed", error, path: "plugins/doubao", manifest: { clientDownloadUrl: "https://www.doubao.com/chat/" } }] },
80
+ "/api/messages": { messages: [] }, "/api/versions": { plugins: [] },
81
+ "/api/update-policy": { mode: "auto-install" }, "/api/sessions": { sessions: [] },
82
+ "/api/plugin-trust": { config: { providers: {}, plugins: {} }, providers: [], entries: [] },
83
+ "/api/plugins/catalog?q=": { plugins: [] },
84
+ };
85
+ return { ok: true, status: 200, json: async () => values[path] ?? {} };
86
+ },
87
+ });
88
+ await new Promise(resolve => setImmediate(resolve));
89
+ const state = node("#plugins").children[0].children[3];
90
+ return state.children.some(child => child.textContent === "下载客户端");
91
+ };
92
+ strict_1.default.equal(await render("已启动豆包,但 CDP 9227 未就绪:fetch failed"), false);
93
+ strict_1.default.equal(await render("未找到 Doubao.exe,请配置 executablePath"), true);
94
+ });
@@ -86,9 +86,14 @@ async function runAunCommand(routes, response, argv) {
86
86
  close() { }
87
87
  },
88
88
  AUNClient: class {
89
+ aid;
89
90
  async connect() { }
90
91
  async close() { }
91
- on(_event, handler) { receive = handler; return { unsubscribe() { } }; }
92
+ constructor(aid) {
93
+ this.aid = aid;
94
+ }
95
+ on(_event, handler) { if (this.aid === "selected.agentid.cn")
96
+ receive = handler; return { unsubscribe() { } }; }
92
97
  async call(_method, params) { sent.push(params); }
93
98
  },
94
99
  };
@@ -142,7 +147,10 @@ async function withAunAttachmentPlugin(run, publish, config = { allow_from: "pee
142
147
  const plugin = new aun_1.AunPlugin({ sdk, ingress: { publish } });
143
148
  try {
144
149
  await plugin.activate({ dataDir: root, config, logger, callPlugin: async () => ({ text: "" }), pluginStatus: () => "ready", http: { register() { }, unregister() { } } });
145
- await run(async (payload) => { await handler(payload); }, sent, storage, logs);
150
+ if (!Object.keys(config.bindings ?? {}).length)
151
+ strict_1.default.equal(handler, undefined);
152
+ await run(async (payload) => { if (handler)
153
+ await handler(payload); }, sent, storage, logs, plugin);
146
154
  }
147
155
  finally {
148
156
  await plugin.deactivate("stop");
@@ -361,7 +369,7 @@ async function withAunAttachmentPlugin(run, publish, config = { allow_from: "pee
361
369
  await plugin.activate({ dataDir: root, config: { identities: [aids[0]] }, logger: { info() { }, warn() { }, error() { } }, callPlugin: async () => ({ text: "" }), pluginStatus: () => "ready", http: {
362
370
  register(route) { routes.set(`${route.method} ${route.path}`, route.handler); return () => undefined; }, unregister() { },
363
371
  } });
364
- strict_1.default.deepEqual(connects, aids);
372
+ strict_1.default.deepEqual(connects, []);
365
373
  for (const aid of aids) {
366
374
  const result = await runAunCommand(routes, commandResponse(), ["create", aid, "--owner", "owner.agentid.cn"]);
367
375
  strict_1.default.equal(result.statusCode, 200);
@@ -937,7 +945,7 @@ async function withAunAttachmentPlugin(run, publish, config = { allow_from: "pee
937
945
  const plugin = new aun_1.AunPlugin({ sdk, ingress: { publish: async () => undefined } });
938
946
  try {
939
947
  process.env.EVOLCORE_HOME = aunPath;
940
- await plugin.activate({ dataDir, config: {}, logger: { info() { }, warn() { }, error() { } }, callPlugin: async () => ({ text: "" }), pluginStatus: () => "ready", http: { register() { }, unregister() { } } });
948
+ await plugin.activate({ dataDir, config: { bindings: { "existing.agentid.cn": "workbuddy" } }, logger: { info() { }, warn() { }, error() { } }, callPlugin: async () => ({ text: "" }), pluginStatus: () => "ready", http: { register() { }, unregister() { } } });
941
949
  strict_1.default.deepEqual(connects, ["existing.agentid.cn"]);
942
950
  strict_1.default.deepEqual(subscriptions, ["existing.agentid.cn"]);
943
951
  strict_1.default.deepEqual(await plugin.identity.list(), { evolcore: ["existing.agentid.cn"], muAgentBridge: [], localMuAgentBridge: [] });
@@ -991,7 +999,41 @@ async function withAunAttachmentPlugin(run, publish, config = { allow_from: "pee
991
999
  await (0, promises_1.rm)(root, { recursive: true, force: true });
992
1000
  }
993
1001
  });
994
- (0, node_test_1.default)("共享密钥库中的 AID 自动连接并保留 EvolCore 来源", async () => {
1002
+ (0, node_test_1.default)("AUN 启动只连接显式绑定身份,旧 identities 不接管共享密钥库", async () => {
1003
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "aun-explicit-only-"));
1004
+ const connected = [];
1005
+ const closed = [];
1006
+ const sdk = {
1007
+ AIDStore: class {
1008
+ list() { return { ok: true, data: { identities: ["selected.agentid.cn", "external.agentid.cn"].map(aid => ({ aid })) } }; }
1009
+ load(aid) { return { ok: true, data: { aid } }; }
1010
+ close() { }
1011
+ },
1012
+ AUNClient: class {
1013
+ aid;
1014
+ constructor(aid) {
1015
+ this.aid = aid;
1016
+ }
1017
+ on() { return { unsubscribe() { } }; }
1018
+ async connect() { connected.push(this.aid); }
1019
+ async close() { closed.push(this.aid); }
1020
+ },
1021
+ };
1022
+ const plugin = new aun_1.AunPlugin({ sdk, ingress: { publish: async () => undefined } });
1023
+ try {
1024
+ await plugin.activate({ dataDir: root, config: { aid: "external.agentid.cn", identities: ["external.agentid.cn"], bindings: { "selected.agentid.cn": "workbuddy" } }, logger: { info() { }, warn() { }, error() { } }, callPlugin: async () => ({ text: "" }), pluginStatus: () => "ready", http: { register() { }, unregister() { } } });
1025
+ strict_1.default.deepEqual(connected, ["selected.agentid.cn"]);
1026
+ strict_1.default.ok((await plugin.identity.list()).evolcore.includes("external.agentid.cn"));
1027
+ await plugin.identity.unbind("workbuddy", "selected.agentid.cn", 26107);
1028
+ strict_1.default.deepEqual(closed, ["selected.agentid.cn"]);
1029
+ strict_1.default.deepEqual((await plugin.health()).details?.bindings, {});
1030
+ }
1031
+ finally {
1032
+ await plugin.deactivate("stop");
1033
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
1034
+ }
1035
+ });
1036
+ (0, node_test_1.default)("共享密钥库中的 AID 仅列出并保留 EvolCore 来源", async () => {
995
1037
  const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "aun-origin-"));
996
1038
  const dataDir = (0, node_path_1.join)(root, "aun");
997
1039
  await (0, promises_1.mkdir)(dataDir);
@@ -1046,14 +1088,14 @@ async function withAunAttachmentPlugin(run, publish, config = { allow_from: "pee
1046
1088
  try {
1047
1089
  await plugin.activate({ dataDir, config: { created_aids: ["created.agentid.cn"] }, logger: { info() { }, warn() { }, error() { } }, callPlugin: async () => ({ text: "" }), pluginStatus: () => "ready", http: { register() { }, unregister() { } } });
1048
1090
  strict_1.default.deepEqual(await plugin.identity.list(), { evolcore: ["shared.agentid.cn"], muAgentBridge: ["created.agentid.cn", "legacy.agentid.cn"], localMuAgentBridge: [] });
1049
- strict_1.default.deepEqual(subscriptions, storeIds);
1091
+ strict_1.default.deepEqual(subscriptions, []);
1050
1092
  await plugin.deactivate("stop");
1051
1093
  }
1052
1094
  finally {
1053
1095
  await (0, promises_1.rm)(root, { recursive: true, force: true });
1054
1096
  }
1055
1097
  });
1056
- (0, node_test_1.default)("默认密钥库指向共享 AUN 目录并自动连接其中全部 AID", async () => {
1098
+ (0, node_test_1.default)("默认共享 AUN 密钥库可列出全部 AID 但不自动连接", async () => {
1057
1099
  const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "aun-keystore-"));
1058
1100
  const dataDir = (0, node_path_1.join)(root, "aun");
1059
1101
  await (0, promises_1.mkdir)(dataDir);
@@ -1078,9 +1120,9 @@ async function withAunAttachmentPlugin(run, publish, config = { allow_from: "pee
1078
1120
  try {
1079
1121
  await plugin.activate({ dataDir, config: {}, logger: { info() { }, warn() { }, error() { } }, callPlugin: async () => ({ text: "" }), pluginStatus: () => "ready", http: { register() { }, unregister() { } } });
1080
1122
  strict_1.default.equal(plugin.path(), root);
1081
- // 密钥库中的全部 AID 在启动时自动连接,无需 ec 或手动关联
1123
+ // 共享密钥库中的身份可见,但未手动绑定时不建立连接。
1082
1124
  strict_1.default.deepEqual((await plugin.identity.list()).evolcore, ["devwin.agentid.cn", "qwenwork.agentid.cn"]);
1083
- strict_1.default.equal(clients.length, 2);
1125
+ strict_1.default.equal(clients.length, 0);
1084
1126
  }
1085
1127
  finally {
1086
1128
  delete process.env.AUN_HOME;
@@ -1137,18 +1179,18 @@ async function withAunAttachmentPlugin(run, publish, config = { allow_from: "pee
1137
1179
  const second = (async function* () { yield Buffer.from('{"aid":"second.agentid.pub","owner_aid":"owner.agentid.pub"}'); })();
1138
1180
  await route.handler(second, response);
1139
1181
  strict_1.default.equal(response.statusCode, 201);
1140
- // bridge、second 已连接;offline-store 在首次 activate 时即自动连接
1141
- strict_1.default.equal(clients.length, 3);
1182
+ // 只有显式创建的 bridge、second 建立连接,尚未订阅消息。
1183
+ strict_1.default.equal(clients.length, 2);
1142
1184
  // 关联不再调用 ec:AID 在本地密钥库即可直接绑定
1143
1185
  await plugin.identity.bind("dsh", "bridge.agentid.pub", 26107);
1144
1186
  const boundAt = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(root, "config.json"), "utf8")).binding_times["bridge.agentid.pub"];
1145
1187
  strict_1.default.ok(Number.isFinite(Date.parse(boundAt)));
1146
- // 密钥库中的 offline-store 已自动连接,绑定只更新关联记录
1188
+ // 首次绑定共享身份时才建立连接并订阅消息。
1147
1189
  await plugin.identity.bind("doubao", "offline-store.agentid.cn", 26107);
1148
1190
  strict_1.default.ok(JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(root, "config.json"), "utf8")).bindings["offline-store.agentid.cn"]);
1149
1191
  strict_1.default.equal(clients.length, 3);
1150
- // 自动连接时 offline-store 先建立连接,bridge 是第二个客户端
1151
- const bridgeClient = clients[1];
1192
+ // bridge 是本次显式创建的第一个客户端。
1193
+ const bridgeClient = clients[0];
1152
1194
  await bridgeClient.emit({ from: "peer.agentid.pub", message_id: "m1", payload: { text: "你好" } });
1153
1195
  strict_1.default.equal(received[0].agentPlugin, "dsh");
1154
1196
  await received[0].reply("收到");
@@ -1162,11 +1204,11 @@ async function withAunAttachmentPlugin(run, publish, config = { allow_from: "pee
1162
1204
  strict_1.default.equal(calls.filter((call) => call === "client.close").length, 3);
1163
1205
  strict_1.default.equal(calls.at(-1), "store.close");
1164
1206
  await plugin.activate({ dataDir: root, config: JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(root, "config.json"), "utf8")), logger: { info() { }, warn() { }, error() { } }, callPlugin: async () => ({ text: "" }), pluginStatus: () => "ready", http: { register() { }, unregister() { } } });
1165
- // 重启后 identities 已含全部三个 AID,各自恢复连接
1166
- strict_1.default.equal(clients.length, 6);
1207
+ // 重启后只恢复两个显式绑定的身份。
1208
+ strict_1.default.equal(clients.length, 5);
1167
1209
  const health = await plugin.health();
1168
- // 自动连接时 offline-store 先建链,顺序与其余 AID 不同
1169
- strict_1.default.deepEqual([...health.details?.aids].sort(), ["bridge.agentid.pub", "offline-store.agentid.cn", "second.agentid.pub"]);
1210
+ // 未绑定的 second 不恢复连接。
1211
+ strict_1.default.deepEqual([...health.details?.aids].sort(), ["bridge.agentid.pub", "offline-store.agentid.cn"]);
1170
1212
  strict_1.default.deepEqual(health.details?.bindings, { "bridge.agentid.pub": "dsh", "offline-store.agentid.cn": "doubao" });
1171
1213
  await routes.find((entry) => entry.path === "/api/plugins/aun/identities").handler({}, response);
1172
1214
  strict_1.default.equal(response.body.bindingTimes["bridge.agentid.pub"], boundAt);
@@ -1306,7 +1348,7 @@ async function withAunAttachmentPlugin(run, publish, config = { allow_from: "pee
1306
1348
  const plugin = new aun_1.AunPlugin({ sdk, ingress: { publish: async () => undefined } });
1307
1349
  try {
1308
1350
  globalThis.fetch = (async () => { fetchCalls++; throw new Error("不应从 Gateway 下载信任根"); });
1309
- await plugin.activate({ dataDir, config: { aid: "previous.agentid.cn", identities: ["previous.agentid.cn"], aun_path: aunPath }, logger: { info() { }, warn() { }, error() { } }, callPlugin: async () => ({ text: "" }), pluginStatus: () => "ready", http: { register() { }, unregister() { } } });
1351
+ await plugin.activate({ dataDir, config: { aid: "previous.agentid.cn", identities: ["previous.agentid.cn"], bindings: { "previous.agentid.cn": "workbuddy" }, aun_path: aunPath }, logger: { info() { }, warn() { }, error() { } }, callPlugin: async () => ({ text: "" }), pluginStatus: () => "ready", http: { register() { }, unregister() { } } });
1310
1352
  strict_1.default.equal(storeOptions.length, 2);
1311
1353
  strict_1.default.equal(storeOptions[1].rootCaPath, (0, node_path_1.join)(dataDir, "CA", "root", "root.crt"));
1312
1354
  strict_1.default.deepEqual(events, ["store.close", "client.connect"]);
@@ -1351,3 +1393,107 @@ async function withAunAttachmentPlugin(run, publish, config = { allow_from: "pee
1351
1393
  }
1352
1394
  }, async () => { throw new Error(`WORKBUDDY_ARTIFACT_READ_FAILED:读取文件 ${"目录/".repeat(120)};原因:ENOENT:产物文件已经删除;token=private-token`); });
1353
1395
  });
1396
+ (0, node_test_1.default)("AUN 绑定写盘失败清理新连接,解绑关闭失败仍删除连接并更新健康状态", async () => {
1397
+ for (const existingConnection of [false, true]) {
1398
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "aun-binding-cleanup-"));
1399
+ const aid = "cleanup.agentid.cn";
1400
+ let closed = 0;
1401
+ let subscriptions = 0;
1402
+ let closeFails = false;
1403
+ const warnings = [];
1404
+ const sdk = {
1405
+ AIDStore: class {
1406
+ list() { return { ok: true, data: { identities: [{ aid }] } }; }
1407
+ load() { return { ok: true, data: { aid } }; }
1408
+ close() { }
1409
+ },
1410
+ AUNClient: class {
1411
+ on() { subscriptions++; return { unsubscribe() { subscriptions--; } }; }
1412
+ async connect() { }
1413
+ async close() { closed++; if (closeFails)
1414
+ throw new Error("关闭失败"); }
1415
+ },
1416
+ };
1417
+ const plugin = new aun_1.AunPlugin({ sdk, ingress: { publish: async () => undefined } });
1418
+ try {
1419
+ await plugin.activate({ dataDir: root, config: {}, logger: { info() { }, warn(message, fields) { warnings.push(JSON.stringify([message, fields])); }, error() { } }, callPlugin: async () => ({ text: "" }), pluginStatus: () => "ready", http: { register() { }, unregister() { } } });
1420
+ if (existingConnection)
1421
+ await plugin.identity.create(aid, "owner.agentid.cn");
1422
+ await (0, promises_1.rm)((0, node_path_1.join)(root, "config.json"), { force: true });
1423
+ await (0, promises_1.mkdir)((0, node_path_1.join)(root, "config.json"));
1424
+ await strict_1.default.rejects(plugin.identity.bind("workbuddy", aid, 26107));
1425
+ strict_1.default.deepEqual((await plugin.health()).details?.bindings, {});
1426
+ strict_1.default.deepEqual((await plugin.health()).details?.aids, existingConnection ? [aid] : []);
1427
+ strict_1.default.equal(subscriptions, 0);
1428
+ strict_1.default.equal(closed, existingConnection ? 0 : 1);
1429
+ await (0, promises_1.rm)((0, node_path_1.join)(root, "config.json"), { recursive: true });
1430
+ await plugin.identity.bind("workbuddy", aid, 26107);
1431
+ strict_1.default.equal(subscriptions, 1);
1432
+ closeFails = true;
1433
+ await plugin.identity.unbind("workbuddy", aid, 26107);
1434
+ strict_1.default.match(warnings.join("\n"), /AUN.*关闭.*cleanup.agentid.cn.*关闭失败/);
1435
+ const health = await plugin.health();
1436
+ strict_1.default.deepEqual(health.details?.aids, []);
1437
+ strict_1.default.deepEqual(health.details?.bindings, {});
1438
+ strict_1.default.equal(health.details?.connected, false);
1439
+ strict_1.default.equal(health.message, "暂无已连接 AID");
1440
+ strict_1.default.equal(subscriptions, 0);
1441
+ }
1442
+ finally {
1443
+ await plugin.deactivate("stop");
1444
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
1445
+ }
1446
+ }
1447
+ });
1448
+ (0, node_test_1.default)("AUN 旧绑定目标不存在时保留配置并跳过连接,记录具体身份", async () => {
1449
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "aun-missing-target-"));
1450
+ const logs = [];
1451
+ let connects = 0;
1452
+ const sdk = {
1453
+ AIDStore: class {
1454
+ list() { return { ok: true, data: { identities: [{ aid: "external.agentid.cn" }] } }; }
1455
+ close() { }
1456
+ },
1457
+ AUNClient: class {
1458
+ async connect() { connects++; }
1459
+ },
1460
+ };
1461
+ const plugin = new aun_1.AunPlugin({ sdk, ingress: { publish: async () => undefined } });
1462
+ try {
1463
+ await plugin.activate({ dataDir: root, config: { bindings: { "external.agentid.cn": "qwenwork" } }, logger: { info() { }, warn(message, fields) { logs.push(JSON.stringify([message, fields])); }, error() { } }, callPlugin: async () => ({ text: "" }), pluginStatus: () => undefined, http: { register() { }, unregister() { } } });
1464
+ strict_1.default.equal(connects, 0);
1465
+ strict_1.default.deepEqual((await plugin.health()).details?.bindings, { "external.agentid.cn": "qwenwork" });
1466
+ strict_1.default.match(logs.join("\n"), /external.agentid.cn.*qwenwork/);
1467
+ }
1468
+ finally {
1469
+ await plugin.deactivate("stop");
1470
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
1471
+ }
1472
+ });
1473
+ (0, node_test_1.default)("AUN 有在途消息时拒绝解绑,最终回复完成后才能解除关联", async () => {
1474
+ let started;
1475
+ let release;
1476
+ const taskStarted = new Promise(resolve => { started = resolve; });
1477
+ const taskGate = new Promise(resolve => { release = resolve; });
1478
+ await withAunAttachmentPlugin(async (receive, sent, _storage, _logs, plugin) => {
1479
+ const task = receive({ from: "peer.agentid.cn", message_id: "unbind-busy", payload: { text: "执行任务" } });
1480
+ try {
1481
+ await taskStarted;
1482
+ await strict_1.default.rejects(plugin.identity.unbind("qwenwork", "local.agentid.cn", 26107), /正在处理消息.*完成后.*解绑/);
1483
+ strict_1.default.deepEqual((await plugin.health()).details?.bindings, { "local.agentid.cn": "qwenwork" });
1484
+ strict_1.default.equal((await plugin.health()).details?.connected, true);
1485
+ }
1486
+ finally {
1487
+ release();
1488
+ await task;
1489
+ }
1490
+ strict_1.default.equal(sent.at(-1).payload.text, "任务最终回复");
1491
+ await plugin.identity.unbind("qwenwork", "local.agentid.cn", 26107);
1492
+ strict_1.default.deepEqual((await plugin.health()).details?.bindings, {});
1493
+ strict_1.default.equal((await plugin.health()).details?.connected, false);
1494
+ }, async (message) => {
1495
+ started();
1496
+ await taskGate;
1497
+ await message.reply("任务最终回复");
1498
+ });
1499
+ });
@@ -216,3 +216,17 @@ const desktop_launch_1 = require("../src-ts/plugins/desktop-launch");
216
216
  await (0, promises_1.rm)(root, { recursive: true, force: true });
217
217
  }
218
218
  });
219
+ (0, node_test_1.default)("豆包配置指向 launcher 时改用同级实际客户端", async () => {
220
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "doubao-launcher-upgrade-"));
221
+ try {
222
+ const launcher = (0, node_path_1.join)(root, "Doubao_update", "launcher", "Doubao.exe");
223
+ const client = (0, node_path_1.join)(root, "Doubao_update", "Doubao.exe");
224
+ await (0, promises_1.mkdir)((0, node_path_1.join)(root, "Doubao_update", "launcher"), { recursive: true });
225
+ await (0, promises_1.writeFile)(launcher, "");
226
+ await (0, promises_1.writeFile)(client, "");
227
+ strict_1.default.equal(await (0, desktop_launch_1.findWindowsExecutable)("Doubao.exe", launcher), client);
228
+ }
229
+ finally {
230
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
231
+ }
232
+ });
@@ -8,6 +8,11 @@ const promises_1 = require("node:fs/promises");
8
8
  const node_test_1 = __importDefault(require("node:test"));
9
9
  const node_child_process_1 = require("node:child_process");
10
10
  const node_util_1 = require("node:util");
11
+ (0, node_test_1.default)("Windows 向导识别自定义安装目录并跳过 DSH 不支持的配置", { skip: process.platform !== "win32" }, async () => {
12
+ const { stdout } = await (0, node_util_1.promisify)(node_child_process_1.execFile)("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "tests/windows-installer-detection.test.ps1"]);
13
+ strict_1.default.match(stdout, /自定义安装目录检测验证通过/);
14
+ strict_1.default.match(stdout, /DSH GUI 配置验证通过/);
15
+ });
11
16
  (0, node_test_1.default)("Windows 安装向导隔离旧后台并固定 AUN 请求端口", { skip: process.platform !== "win32" }, async () => {
12
17
  const { stdout } = await (0, node_util_1.promisify)(node_child_process_1.execFile)("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "tests/windows-installer.test.ps1"]);
13
18
  strict_1.default.match(stdout, /验证通过/);
@@ -49,6 +54,15 @@ const node_util_1 = require("node:util");
49
54
  strict_1.default.doesNotMatch(setup, /mu-agent-bridge@\d+\.\d+\.\d+/);
50
55
  strict_1.default.match(setup, /api\/agents\/availability/);
51
56
  strict_1.default.match(setup, /function Find-AgentExecutable/);
57
+ strict_1.default.match(setup, /function Update-MabScheduledTask/);
58
+ strict_1.default.match(setup, /function Remove-OldBridgeInstallations/);
59
+ strict_1.default.match(setup, /function Read-SetupJsonFile/);
60
+ strict_1.default.match(setup, /ConvertFrom-Json -InputObject \$json/);
61
+ strict_1.default.doesNotMatch(setup, /Get-Content[^\n]*ConvertFrom-Json/);
62
+ strict_1.default.match(setup, /schtasks\.exe \/End \/TN mab/);
63
+ strict_1.default.doesNotMatch(setup, /Disable-ScheduledTask -TaskName 'mab'/);
64
+ strict_1.default.match(setup, /\.mab[\\\\]paths\.json/);
65
+ strict_1.default.match(setup, /清理旧 mab 目录/);
52
66
  strict_1.default.match(setup, /CheckedListBox/);
53
67
  strict_1.default.match(setup, /DataGridViewComboBoxColumn/);
54
68
  strict_1.default.match(setup, /\[void\]\$grid\.EndEdit\(\)/);
@@ -67,6 +81,8 @@ const node_util_1 = require("node:util");
67
81
  strict_1.default.match(setup, /TopMost = \$true/);
68
82
  strict_1.default.match(setup, /BringToFront\(\)/);
69
83
  strict_1.default.match(setup, /Invoke-AunCommand @\('list'\)/);
84
+ strict_1.default.match(setup, /自动解除未安装千问办公的残留绑定/);
85
+ strict_1.default.match(setup, /Invoke-AunCommand @\('unbind', \[string\]\$binding\.Name\)/);
70
86
  for (const id of ["qwenwork", "doubao", "workbuddy", "dsh"])
71
87
  strict_1.default.ok(setup.includes(id), "缺少 Agent " + id);
72
88
  strict_1.default.match(setup, /choices\[0\]\.message\.content/);
@@ -45,6 +45,35 @@ const node_http_1 = require("node:http");
45
45
  const plugin_manager_1 = require("../src-ts/plugin-manager");
46
46
  const version_manager_1 = require("../src-ts/version-manager");
47
47
  const channel_sessions_1 = require("../src-ts/channel-sessions");
48
+ (0, node_test_1.default)("启动时内置新版替代状态目录旧插件,保留较新及同版托管插件", async () => {
49
+ for (const [bundled, managed, expectedManaged] of [["1.0.31", "1.0.29", false], ["1.0.31", "1.0.32", true], ["1.0.31", "1.0.31", true], ["1.0.10", "1.0.9", false], ["1.0.9", "1.0.10", true]]) {
50
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "mab-plugin-precedence-"));
51
+ const bundledDir = (0, node_path_1.join)(root, "plugins", "aun");
52
+ const managedDir = (0, node_path_1.join)(root, "state", "npm", "agent-bridge-aun", "node_modules", "agent-bridge-aun");
53
+ try {
54
+ for (const [dir, version] of [[bundledDir, bundled], [managedDir, managed]]) {
55
+ await (0, promises_1.mkdir)(dir, { recursive: true });
56
+ await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "package.json"), JSON.stringify({ name: "agent-bridge-aun", version, agentBridge: { plugin: "./plugin.json" } }));
57
+ await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "plugin.json"), JSON.stringify({ id: "aun", version, entry: "index.js", apiVersion: "1", capabilities: [] }));
58
+ await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "index.js"), "module.exports={activate:async()=>{},deactivate:async()=>{},health:async()=>({status:'ready'})};");
59
+ }
60
+ const manager = new plugin_manager_1.PluginManager({ pluginsDir: (0, node_path_1.join)(root, "plugins"), stateDir: (0, node_path_1.join)(root, "state"), publisherResolver: async () => "m2lihui" });
61
+ await manager.discover();
62
+ strict_1.default.equal(manager.list()[0].path, expectedManaged ? managedDir : bundledDir);
63
+ strict_1.default.equal(manager.list()[0].version, expectedManaged ? managed : bundled);
64
+ strict_1.default.equal(manager.packageInfo("aun").managed, expectedManaged);
65
+ await manager.start("aun");
66
+ const instance = manager.get("aun").instance;
67
+ await manager.discover();
68
+ strict_1.default.equal(manager.get("aun").instance, instance);
69
+ strict_1.default.equal(manager.get("aun").record.status, "ready");
70
+ await manager.stop("aun", "stop");
71
+ }
72
+ finally {
73
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
74
+ }
75
+ }
76
+ });
48
77
  (0, node_test_1.default)("后台桌面重启先停止插件再启动,拒绝无关插件", { skip: process.platform === "win32" }, async () => {
49
78
  const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "mab-desktop-restart-"));
50
79
  const manager = new plugin_manager_1.PluginManager({ pluginsDir: (0, node_path_1.join)(root, "plugins"), stateDir: (0, node_path_1.join)(root, "state") });
@@ -1157,3 +1186,25 @@ const channel_sessions_1 = require("../src-ts/channel-sessions");
1157
1186
  await (0, promises_1.rm)(root, { recursive: true, force: true });
1158
1187
  }
1159
1188
  });
1189
+ (0, node_test_1.default)("不兼容的新托管候选不遮蔽可用内置插件", async () => {
1190
+ for (const managedFirst of [true, false]) {
1191
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "mab-compatible-choice-"));
1192
+ const manager = new plugin_manager_1.PluginManager({ pluginsDir: (0, node_path_1.join)(root, "plugins"), stateDir: (0, node_path_1.join)(root, "state"), publisherResolver: async () => "m2lihui" });
1193
+ try {
1194
+ const bundled = (0, node_path_1.join)(root, "plugins", "demo");
1195
+ const managed = (0, node_path_1.join)(root, "state", "npm", "agent-bridge-demo", "node_modules", "agent-bridge-demo");
1196
+ for (const [dir, version, requires] of [[bundled, "1.0.0", "*"], [managed, "2.0.0", ">=999.0.0"]]) {
1197
+ await (0, promises_1.mkdir)(dir, { recursive: true });
1198
+ await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "plugin.json"), JSON.stringify({ id: "demo", version, entry: "index.js", apiVersion: "1", capabilities: [] }));
1199
+ await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "package.json"), JSON.stringify({ name: "agent-bridge-demo", agentBridge: { requires: { "mu-agent-bridge": requires } } }));
1200
+ }
1201
+ for (const dir of managedFirst ? [managed, bundled] : [bundled, managed])
1202
+ await manager.discoverDirectory(dir, "demo");
1203
+ strict_1.default.equal(manager.get("demo").record.path, bundled);
1204
+ strict_1.default.equal(manager.get("demo").record.status, "discovered");
1205
+ }
1206
+ finally {
1207
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
1208
+ }
1209
+ }
1210
+ });
@@ -890,25 +890,22 @@ const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
890
890
  await plugin.handle({ task: "新会话", sessionId: "session-b" });
891
891
  strict_1.default.deepEqual(created, ["", ""]);
892
892
  });
893
- (0, node_test_1.default)("workbuddy 新建失败后拒绝在未知桌面状态续聊,旧会话不支持恢复", async () => {
894
- let shouldFail = false;
893
+ (0, node_test_1.default)("workbuddy 新建任务失败后同一会话可重试并恢复普通对话", async () => {
894
+ let shouldFail = true;
895
895
  let creates = 0;
896
+ const calls = [];
896
897
  const plugin = new workbuddy_1.WorkbuddyPlugin({ client: {
897
898
  connect: async () => undefined, close() { }, connected: () => true,
898
899
  createTask: async () => { creates++; if (shouldFail)
899
900
  throw new Error("创建失败"); },
900
- direct: async (task) => task,
901
+ direct: async (task) => { calls.push(task); return `回复:${task}`; },
901
902
  } });
902
903
  await plugin.activate({ config: { transport: "cdp" }, logger: { info() { }, warn() { }, error() { } } });
903
- await plugin.handle({ task: "a", sessionId: "session-a" });
904
- shouldFail = true;
905
- await strict_1.default.rejects(() => plugin.handle({ task: "b", sessionId: "session-b" }), /创建失败/);
906
- await strict_1.default.rejects(() => plugin.handle({ task: "继续", sessionId: "session-a" }), /不支持恢复旧 WorkBuddy 会话/);
904
+ await strict_1.default.rejects(() => plugin.handle({ task: "首次消息", sessionId: "session-b" }), /创建失败/);
907
905
  shouldFail = false;
908
- await strict_1.default.rejects(() => plugin.handle({ task: "重试", sessionId: "session-b" }), /不支持恢复旧 WorkBuddy 会话/);
909
- await plugin.handle({ task: "切换", sessionId: "session-c" });
910
- strict_1.default.equal(creates, 3);
911
- await strict_1.default.rejects(() => plugin.handle({ task: "旧会话", sessionId: "session-a" }), /不支持恢复旧 WorkBuddy 会话/);
906
+ strict_1.default.equal((await plugin.handle({ task: "重试普通对话", sessionId: "session-b" })).text, "回复:重试普通对话");
907
+ strict_1.default.equal(creates, 2);
908
+ strict_1.default.deepEqual(calls, ["重试普通对话"]);
912
909
  });
913
910
  (0, node_test_1.default)("workbuddy 执行中拒绝会话控制", async () => {
914
911
  let release;
@@ -1257,15 +1254,15 @@ const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
1257
1254
  await (0, promises_1.rm)(dataDir, { recursive: true, force: true });
1258
1255
  }
1259
1256
  });
1260
- (0, node_test_1.default)("workbuddy 新建失败的会话 ID 重启后仍禁止重试", async () => {
1257
+ (0, node_test_1.default)("workbuddy 新建失败的会话 ID 重启后仍可重试", async () => {
1261
1258
  const dataDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "wb-failed-session-"));
1262
1259
  let creates = 0;
1263
1260
  let fail = true;
1264
1261
  const client = {
1265
1262
  connect: async () => undefined, close() { }, connected: () => true,
1266
1263
  createTask: async () => {
1267
- const stored = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(dataDir, "known-sessions.json"), "utf8"));
1268
- strict_1.default.deepEqual(stored.sessionIds, ["failed-session"]);
1264
+ const stored = await (0, promises_1.readFile)((0, node_path_1.join)(dataDir, "known-sessions.json"), "utf8").then(JSON.parse).catch(() => ({ sessionIds: [] }));
1265
+ strict_1.default.equal(stored.sessionIds.includes("failed-session"), true);
1269
1266
  creates++;
1270
1267
  if (fail)
1271
1268
  throw new Error("创建失败");
@@ -1281,8 +1278,8 @@ const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
1281
1278
  fail = false;
1282
1279
  const second = new workbuddy_1.WorkbuddyPlugin({ client });
1283
1280
  await second.activate(context);
1284
- await strict_1.default.rejects(() => second.handle({ task: "重试", sessionId: "failed-session" }), /不支持恢复旧 WorkBuddy 会话/);
1285
- strict_1.default.equal(creates, 1);
1281
+ strict_1.default.equal((await second.handle({ task: "重试", sessionId: "failed-session" })).text, "重试");
1282
+ strict_1.default.equal(creates, 2);
1286
1283
  await second.deactivate("stop");
1287
1284
  }
1288
1285
  finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mu-agent-bridge",
3
- "version": "1.0.64",
3
+ "version": "1.0.66",
4
4
  "description": "将 AUN Agent 连接到本地桌面 AI 客户端的插件平台。",
5
5
  "main": "dist/src-ts/cli.js",
6
6
  "bin": {
@@ -331,7 +331,7 @@
331
331
  state.append(detail);
332
332
  }
333
333
  // 插件未就绪且声明了客户端下载地址时,给出获取桌面客户端的入口
334
- if (plugin.manifest?.clientDownloadUrl && plugin.status !== "ready") {
334
+ if (plugin.manifest?.clientDownloadUrl && plugin.error?.includes("未找到") && plugin.error.includes(".exe")) {
335
335
  const link = document.createElement("a");
336
336
  link.className = "plugin-id";
337
337
  link.href = plugin.manifest.clientDownloadUrl;
@@ -201,7 +201,7 @@
201
201
  if (plugin.error) { const detail = document.createElement("small"); detail.className = "error-text plugin-id"; detail.textContent = plugin.error; state.append(detail); }
202
202
  if (plugin.dependencyStatus?.length) { const detail = document.createElement("small"); detail.className = "error-text plugin-id"; detail.textContent = `依赖:${plugin.dependencyStatus.join(";")}`; state.append(detail); }
203
203
  // 插件未就绪且声明了客户端下载地址时,给出获取桌面客户端的入口
204
- if (plugin.manifest?.clientDownloadUrl && plugin.status !== "ready") {
204
+ if (plugin.manifest?.clientDownloadUrl && plugin.error?.includes("未找到") && plugin.error.includes(".exe")) {
205
205
  const link = document.createElement("a"); link.className = "plugin-id"; link.href = plugin.manifest.clientDownloadUrl; link.target = "_blank"; link.rel = "noreferrer noopener";
206
206
  link.textContent = "下载客户端"; state.append(link);
207
207
  }
@@ -12,7 +12,7 @@ const agent_1 = require("../../agent");
12
12
  const messages_1 = require("./messages");
13
13
  const attachments_1 = require("./attachments");
14
14
  const manifest = {
15
- id: "aun", version: "1.0.33", type: "channel", displayName: "AUN 通道",
15
+ id: "aun", version: "1.0.34", type: "channel", displayName: "AUN 通道",
16
16
  entry: "dist/src-ts/plugins/aun/index.js", apiVersion: "1", process: "in-process",
17
17
  capabilities: ["channel", "channel.identity.create", "channel.receive", "channel.send", "health", "cli"],
18
18
  configSchema: "config.schema.json",
@@ -86,7 +86,7 @@ class AunPlugin {
86
86
  const known = new Set([...this.clients.keys(), ...this.listStoreAids()]);
87
87
  return {
88
88
  evolcore: [...known].filter((aid) => !own.has(aid)).sort(),
89
- muAgentBridge: [...this.clients.keys()].filter((aid) => own.has(aid)).sort(),
89
+ muAgentBridge: [...known].filter((aid) => own.has(aid)).sort(),
90
90
  localMuAgentBridge: [...own].filter((aid) => !known.has(aid)).sort(),
91
91
  };
92
92
  },
@@ -125,42 +125,29 @@ class AunPlugin {
125
125
  }
126
126
  },
127
127
  bind: async (pluginId, aid, _port) => {
128
+ if (!aid || !aidPattern.test(aid))
129
+ throw new Error("AID 格式无效,必须指定要关联的 AID");
128
130
  if (pluginId === "aun" || pluginId === "abweb")
129
131
  throw new Error("只能关联 Agent 插件");
132
+ if (this.context?.pluginStatus(pluginId) !== "ready")
133
+ throw new Error("目标 Agent 插件未启动");
130
134
  const previous = this.bindings;
131
135
  const previousTimes = this.bindingTimes;
132
- this.bindings = { ...previous };
133
- this.bindingTimes = { ...previousTimes };
136
+ const connection = this.clients.get(aid);
137
+ const hadIdentity = this.identities.has(aid);
138
+ const hadSubscription = Boolean(connection?.subscription);
134
139
  try {
135
- if (aid) {
136
- if (!aidPattern.test(aid))
137
- throw new Error("AID 格式无效");
138
- if (this.context?.pluginStatus(pluginId) !== "ready")
139
- throw new Error("目标 Agent 插件未启动");
140
- if (this.identities.has(aid) || this.legacyCreatedAids.has(aid)) {
141
- if (!this.clients.has(aid))
142
- await this.connectAid(aid);
143
- if (!this.clients.has(aid))
144
- throw new Error("AID 当前未连接");
145
- this.bindings[aid] = pluginId;
146
- }
147
- else {
148
- // 本地身份由插件直接接管,不再通过 ec 配置路由。
149
- const loaded = this.store.load(aid);
150
- if (!loaded.ok || !loaded.data)
151
- throw new Error("AID 不在本地密钥库中,无法关联");
152
- this.identities.add(aid);
153
- if (!this.clients.has(aid))
154
- await this.connectAid(aid);
155
- if (!this.clients.has(aid))
156
- throw new Error("AID 当前未连接");
157
- this.bindings[aid] = pluginId;
158
- }
159
- }
160
- else
161
- throw new Error("必须指定要关联的 AID");
162
- if (aid)
163
- this.bindingTimes[aid] = new Date().toISOString();
140
+ const loaded = this.store.load(aid);
141
+ if (!loaded.ok || !loaded.data)
142
+ throw new Error("AID 不在本地密钥库中,无法关联");
143
+ this.identities.add(aid);
144
+ if (!connection)
145
+ await this.connectAid(aid);
146
+ if (!this.clients.has(aid))
147
+ throw new Error("AID 当前未连接");
148
+ this.bindings = { ...previous, [aid]: pluginId };
149
+ this.bindingTimes = { ...previousTimes, [aid]: new Date().toISOString() };
150
+ this.subscribeAid(aid);
164
151
  await this.persist();
165
152
  }
166
153
  catch (error) {
@@ -168,6 +155,29 @@ class AunPlugin {
168
155
  this.bindingTimes = previousTimes;
169
156
  this.config.bindings = previous;
170
157
  this.config.binding_times = previousTimes;
158
+ if (!hadIdentity)
159
+ this.identities.delete(aid);
160
+ this.config.identities = [...this.identities].sort();
161
+ if (!hadSubscription) {
162
+ this.clients.get(aid)?.subscription?.unsubscribe();
163
+ if (connection)
164
+ connection.subscription = undefined;
165
+ }
166
+ if (!connection) {
167
+ try {
168
+ await this.clients.get(aid)?.client.close();
169
+ }
170
+ catch (closeError) {
171
+ this.context?.logger.warn("AUN 关联回滚关闭连接失败", { aid, error: safeError(closeError) });
172
+ }
173
+ finally {
174
+ this.clients.delete(aid);
175
+ if (!this.clients.size) {
176
+ this.status = "degraded";
177
+ this.healthMessage = "暂无已连接 AID";
178
+ }
179
+ }
180
+ }
171
181
  this.context?.logger.error("AUN 身份关联失败", { aid, pluginId, error: safeError(error) });
172
182
  throw error;
173
183
  }
@@ -178,6 +188,8 @@ class AunPlugin {
178
188
  throw new Error("AID 格式无效");
179
189
  if (this.bindings[aid] !== pluginId)
180
190
  throw new Error("指定 AID 当前未关联到该 Agent");
191
+ if ([...this.pendingMessages.keys()].some(key => JSON.parse(key)[0] === aid))
192
+ throw new Error("该 AID 正在处理消息,请等待任务完成后再解绑");
181
193
  const previous = this.bindings;
182
194
  const previousTimes = this.bindingTimes;
183
195
  this.bindings = { ...previous };
@@ -194,6 +206,21 @@ class AunPlugin {
194
206
  this.config.binding_times = previousTimes;
195
207
  throw error;
196
208
  }
209
+ const connection = this.clients.get(aid);
210
+ connection?.subscription?.unsubscribe();
211
+ try {
212
+ await connection?.client.close();
213
+ }
214
+ catch (error) {
215
+ this.context?.logger.warn("AUN 解绑已完成,关闭连接失败", { aid, pluginId, error: safeError(error) });
216
+ }
217
+ finally {
218
+ this.clients.delete(aid);
219
+ if (!this.clients.size) {
220
+ this.status = "degraded";
221
+ this.healthMessage = "暂无已连接 AID";
222
+ }
223
+ }
197
224
  this.context?.logger.info("AUN 单个身份关联已解除", { aid, pluginId });
198
225
  },
199
226
  };
@@ -207,6 +234,7 @@ class AunPlugin {
207
234
  async activate(context) {
208
235
  this.context = context;
209
236
  this.config = { ...context.config };
237
+ this.identities.clear();
210
238
  this.legacyCreatedAids.clear();
211
239
  this.createdAids.clear();
212
240
  try {
@@ -266,15 +294,25 @@ class AunPlugin {
266
294
  if (this.aid)
267
295
  this.identities.add(this.aid);
268
296
  this.acceptingMessages = true;
269
- for (const aid of this.identities)
270
- await this.connectAid(aid);
271
- // 密钥库中的其余 AID 也自动连接:不再依赖 ec 或 EvolCore 进程
272
- for (const aid of this.listStoreAids()) {
273
- this.identities.add(aid);
274
- if (!this.clients.has(aid))
297
+ // 只订阅明确关联 Agent 的身份,避免接管 EvolCore 或其它 AID 的消息。
298
+ for (const aid of Object.keys(this.bindings)) {
299
+ const target = this.bindings[aid];
300
+ if (!target || !context.pluginStatus(target)) {
301
+ context.logger.warn("AUN 跳过目标插件不存在的旧绑定,未连接身份", { aid, agentPlugin: target });
302
+ continue;
303
+ }
304
+ if (context.pluginStatus(target) !== "ready")
305
+ context.logger.warn("AUN 恢复已有显式绑定,目标插件尚未就绪;关联保持不变", { aid, agentPlugin: target, status: context.pluginStatus(target) });
306
+ if (aidPattern.test(aid))
275
307
  await this.connectAid(aid);
276
308
  }
277
309
  }
310
+ subscribeAid(aid) {
311
+ const connection = this.clients.get(aid);
312
+ if (!connection || connection.subscription || !this.bindings[aid])
313
+ return;
314
+ connection.subscription = connection.client.on("message.received", (payload) => this.onMessage(payload, aid));
315
+ }
278
316
  async connectAid(aid) {
279
317
  try {
280
318
  await this.prepareTrustRoots(aid);
@@ -282,7 +320,7 @@ class AunPlugin {
282
320
  if (!loaded.ok || !loaded.data)
283
321
  throw new Error(loaded.error?.message ?? "本地密钥库中不存在该 AID");
284
322
  const client = new this.sdk.AUNClient(loaded.data.aid);
285
- const subscription = this.identities.has(aid) || this.legacyCreatedAids.has(aid)
323
+ const subscription = this.bindings[aid]
286
324
  ? client.on("message.received", (payload) => this.onMessage(payload, aid))
287
325
  : undefined;
288
326
  try {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-bridge-aun",
3
- "version": "1.0.33",
3
+ "version": "1.0.34",
4
4
  "description": "mu-agent-bridge 的 AUN 通道插件。",
5
5
  "keywords": [
6
6
  "mu-agent-bridge-plugin",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "aun",
3
- "version": "1.0.33",
3
+ "version": "1.0.34",
4
4
  "type": "channel",
5
5
  "displayName": "AUN 通道",
6
6
  "entry": "dist/src-ts/plugins/aun/index.js",
@@ -22,8 +22,11 @@ async function findWindowsExecutable(fileName, configured) {
22
22
  const configuredPath = (0, node_path_1.resolve)(configured);
23
23
  if (fileName.toLowerCase() === "doubao.exe") {
24
24
  let root = (0, node_path_1.dirname)(configuredPath);
25
- while (/^app$/i.test((0, node_path_1.basename)(root)) || versionDirectory.test((0, node_path_1.basename)(root)))
25
+ while (/^(?:app|launcher|doubao_update)$/i.test((0, node_path_1.basename)(root)) || versionDirectory.test((0, node_path_1.basename)(root)))
26
26
  root = (0, node_path_1.dirname)(root);
27
+ const updated = (0, node_path_1.join)(root, "Doubao_update", fileName);
28
+ if ((0, node_fs_1.existsSync)(updated))
29
+ return updated;
27
30
  const latest = await findExecutableInTree(root, fileName, /^/, 3);
28
31
  if (latest)
29
32
  return latest;
@@ -64,8 +67,11 @@ async function search(directory, fileName, pattern, depth, insideProduct) {
64
67
  const entries = await (0, promises_1.readdir)(directory, { withFileTypes: true });
65
68
  // 豆包上层文件是启动器,优先查找新版目录或 app 中的实际程序。
66
69
  if (insideProduct && depth > 0 && fileName.toLowerCase() === "doubao.exe") {
67
- const versions = entries.filter(entry => entry.isDirectory() && (versionDirectory.test(entry.name) || /^app$/i.test(entry.name)));
68
- versions.sort((left, right) => right.name.replace(/^\D+/, "").localeCompare(left.name.replace(/^\D+/, ""), "en", { numeric: true }));
70
+ const versions = entries.filter(entry => entry.isDirectory() && (versionDirectory.test(entry.name) || /^(?:app|doubao_update)$/i.test(entry.name)));
71
+ versions.sort((left, right) => {
72
+ const rank = (name) => /^doubao_update$/i.test(name) ? 2 : /^app$/i.test(name) ? 0 : 1;
73
+ return rank(right.name) - rank(left.name) || right.name.replace(/^\D+/, "").localeCompare(left.name.replace(/^\D+/, ""), "en", { numeric: true });
74
+ });
69
75
  for (const entry of versions) {
70
76
  const found = await search((0, node_path_1.join)(directory, entry.name), fileName, pattern, depth - 1, true);
71
77
  if (found)
@@ -73,7 +79,7 @@ async function search(directory, fileName, pattern, depth, insideProduct) {
73
79
  }
74
80
  }
75
81
  for (const entry of entries)
76
- if (entry.isFile() && insideProduct && entry.name.toLowerCase() === fileName.toLowerCase())
82
+ if (entry.isFile() && insideProduct && entry.name.toLowerCase() === fileName.toLowerCase() && !(fileName.toLowerCase() === "doubao.exe" && /^(?:launcher|doubao)$/i.test((0, node_path_1.basename)(directory))))
77
83
  return (0, node_path_1.join)(directory, entry.name);
78
84
  if (depth <= 0)
79
85
  return undefined;
@@ -22,8 +22,11 @@ async function findWindowsExecutable(fileName, configured) {
22
22
  const configuredPath = (0, node_path_1.resolve)(configured);
23
23
  if (fileName.toLowerCase() === "doubao.exe") {
24
24
  let root = (0, node_path_1.dirname)(configuredPath);
25
- while (/^app$/i.test((0, node_path_1.basename)(root)) || versionDirectory.test((0, node_path_1.basename)(root)))
25
+ while (/^(?:app|launcher|doubao_update)$/i.test((0, node_path_1.basename)(root)) || versionDirectory.test((0, node_path_1.basename)(root)))
26
26
  root = (0, node_path_1.dirname)(root);
27
+ const updated = (0, node_path_1.join)(root, "Doubao_update", fileName);
28
+ if ((0, node_fs_1.existsSync)(updated))
29
+ return updated;
27
30
  const latest = await findExecutableInTree(root, fileName, /^/, 3);
28
31
  if (latest)
29
32
  return latest;
@@ -64,8 +67,11 @@ async function search(directory, fileName, pattern, depth, insideProduct) {
64
67
  const entries = await (0, promises_1.readdir)(directory, { withFileTypes: true });
65
68
  // 豆包上层文件是启动器,优先查找新版目录或 app 中的实际程序。
66
69
  if (insideProduct && depth > 0 && fileName.toLowerCase() === "doubao.exe") {
67
- const versions = entries.filter(entry => entry.isDirectory() && (versionDirectory.test(entry.name) || /^app$/i.test(entry.name)));
68
- versions.sort((left, right) => right.name.replace(/^\D+/, "").localeCompare(left.name.replace(/^\D+/, ""), "en", { numeric: true }));
70
+ const versions = entries.filter(entry => entry.isDirectory() && (versionDirectory.test(entry.name) || /^(?:app|doubao_update)$/i.test(entry.name)));
71
+ versions.sort((left, right) => {
72
+ const rank = (name) => /^doubao_update$/i.test(name) ? 2 : /^app$/i.test(name) ? 0 : 1;
73
+ return rank(right.name) - rank(left.name) || right.name.replace(/^\D+/, "").localeCompare(left.name.replace(/^\D+/, ""), "en", { numeric: true });
74
+ });
69
75
  for (const entry of versions) {
70
76
  const found = await search((0, node_path_1.join)(directory, entry.name), fileName, pattern, depth - 1, true);
71
77
  if (found)
@@ -73,7 +79,7 @@ async function search(directory, fileName, pattern, depth, insideProduct) {
73
79
  }
74
80
  }
75
81
  for (const entry of entries)
76
- if (entry.isFile() && insideProduct && entry.name.toLowerCase() === fileName.toLowerCase())
82
+ if (entry.isFile() && insideProduct && entry.name.toLowerCase() === fileName.toLowerCase() && !(fileName.toLowerCase() === "doubao.exe" && /^(?:launcher|doubao)$/i.test((0, node_path_1.basename)(directory))))
77
83
  return (0, node_path_1.join)(directory, entry.name);
78
84
  if (depth <= 0)
79
85
  return undefined;
@@ -22,8 +22,11 @@ async function findWindowsExecutable(fileName, configured) {
22
22
  const configuredPath = (0, node_path_1.resolve)(configured);
23
23
  if (fileName.toLowerCase() === "doubao.exe") {
24
24
  let root = (0, node_path_1.dirname)(configuredPath);
25
- while (/^app$/i.test((0, node_path_1.basename)(root)) || versionDirectory.test((0, node_path_1.basename)(root)))
25
+ while (/^(?:app|launcher|doubao_update)$/i.test((0, node_path_1.basename)(root)) || versionDirectory.test((0, node_path_1.basename)(root)))
26
26
  root = (0, node_path_1.dirname)(root);
27
+ const updated = (0, node_path_1.join)(root, "Doubao_update", fileName);
28
+ if ((0, node_fs_1.existsSync)(updated))
29
+ return updated;
27
30
  const latest = await findExecutableInTree(root, fileName, /^/, 3);
28
31
  if (latest)
29
32
  return latest;
@@ -64,8 +67,11 @@ async function search(directory, fileName, pattern, depth, insideProduct) {
64
67
  const entries = await (0, promises_1.readdir)(directory, { withFileTypes: true });
65
68
  // 豆包上层文件是启动器,优先查找新版目录或 app 中的实际程序。
66
69
  if (insideProduct && depth > 0 && fileName.toLowerCase() === "doubao.exe") {
67
- const versions = entries.filter(entry => entry.isDirectory() && (versionDirectory.test(entry.name) || /^app$/i.test(entry.name)));
68
- versions.sort((left, right) => right.name.replace(/^\D+/, "").localeCompare(left.name.replace(/^\D+/, ""), "en", { numeric: true }));
70
+ const versions = entries.filter(entry => entry.isDirectory() && (versionDirectory.test(entry.name) || /^(?:app|doubao_update)$/i.test(entry.name)));
71
+ versions.sort((left, right) => {
72
+ const rank = (name) => /^doubao_update$/i.test(name) ? 2 : /^app$/i.test(name) ? 0 : 1;
73
+ return rank(right.name) - rank(left.name) || right.name.replace(/^\D+/, "").localeCompare(left.name.replace(/^\D+/, ""), "en", { numeric: true });
74
+ });
69
75
  for (const entry of versions) {
70
76
  const found = await search((0, node_path_1.join)(directory, entry.name), fileName, pattern, depth - 1, true);
71
77
  if (found)
@@ -73,7 +79,7 @@ async function search(directory, fileName, pattern, depth, insideProduct) {
73
79
  }
74
80
  }
75
81
  for (const entry of entries)
76
- if (entry.isFile() && insideProduct && entry.name.toLowerCase() === fileName.toLowerCase())
82
+ if (entry.isFile() && insideProduct && entry.name.toLowerCase() === fileName.toLowerCase() && !(fileName.toLowerCase() === "doubao.exe" && /^(?:launcher|doubao)$/i.test((0, node_path_1.basename)(directory))))
77
83
  return (0, node_path_1.join)(directory, entry.name);
78
84
  if (depth <= 0)
79
85
  return undefined;
@@ -22,8 +22,11 @@ async function findWindowsExecutable(fileName, configured) {
22
22
  const configuredPath = (0, node_path_1.resolve)(configured);
23
23
  if (fileName.toLowerCase() === "doubao.exe") {
24
24
  let root = (0, node_path_1.dirname)(configuredPath);
25
- while (/^app$/i.test((0, node_path_1.basename)(root)) || versionDirectory.test((0, node_path_1.basename)(root)))
25
+ while (/^(?:app|launcher|doubao_update)$/i.test((0, node_path_1.basename)(root)) || versionDirectory.test((0, node_path_1.basename)(root)))
26
26
  root = (0, node_path_1.dirname)(root);
27
+ const updated = (0, node_path_1.join)(root, "Doubao_update", fileName);
28
+ if ((0, node_fs_1.existsSync)(updated))
29
+ return updated;
27
30
  const latest = await findExecutableInTree(root, fileName, /^/, 3);
28
31
  if (latest)
29
32
  return latest;
@@ -64,8 +67,11 @@ async function search(directory, fileName, pattern, depth, insideProduct) {
64
67
  const entries = await (0, promises_1.readdir)(directory, { withFileTypes: true });
65
68
  // 豆包上层文件是启动器,优先查找新版目录或 app 中的实际程序。
66
69
  if (insideProduct && depth > 0 && fileName.toLowerCase() === "doubao.exe") {
67
- const versions = entries.filter(entry => entry.isDirectory() && (versionDirectory.test(entry.name) || /^app$/i.test(entry.name)));
68
- versions.sort((left, right) => right.name.replace(/^\D+/, "").localeCompare(left.name.replace(/^\D+/, ""), "en", { numeric: true }));
70
+ const versions = entries.filter(entry => entry.isDirectory() && (versionDirectory.test(entry.name) || /^(?:app|doubao_update)$/i.test(entry.name)));
71
+ versions.sort((left, right) => {
72
+ const rank = (name) => /^doubao_update$/i.test(name) ? 2 : /^app$/i.test(name) ? 0 : 1;
73
+ return rank(right.name) - rank(left.name) || right.name.replace(/^\D+/, "").localeCompare(left.name.replace(/^\D+/, ""), "en", { numeric: true });
74
+ });
69
75
  for (const entry of versions) {
70
76
  const found = await search((0, node_path_1.join)(directory, entry.name), fileName, pattern, depth - 1, true);
71
77
  if (found)
@@ -73,7 +79,7 @@ async function search(directory, fileName, pattern, depth, insideProduct) {
73
79
  }
74
80
  }
75
81
  for (const entry of entries)
76
- if (entry.isFile() && insideProduct && entry.name.toLowerCase() === fileName.toLowerCase())
82
+ if (entry.isFile() && insideProduct && entry.name.toLowerCase() === fileName.toLowerCase() && !(fileName.toLowerCase() === "doubao.exe" && /^(?:launcher|doubao)$/i.test((0, node_path_1.basename)(directory))))
77
83
  return (0, node_path_1.join)(directory, entry.name);
78
84
  if (depth <= 0)
79
85
  return undefined;
@@ -16,7 +16,7 @@ const session_history_1 = require("./session-history");
16
16
  const desktop_launch_1 = require("../desktop-launch");
17
17
  const desktop_launch_2 = require("./desktop-launch");
18
18
  exports.workbuddyManifest = {
19
- id: "workbuddy", version: "1.1.28", displayName: "WorkBuddy", type: "agent",
19
+ id: "workbuddy", version: "1.1.29", displayName: "WorkBuddy", type: "agent",
20
20
  entry: "dist/index.js", apiVersion: "1", process: "in-process",
21
21
  capabilities: ["chat", "health"], configSchema: "config.schema.json",
22
22
  };
@@ -384,7 +384,19 @@ class WorkbuddyPlugin {
384
384
  this.activeDesktopConversationId = undefined;
385
385
  this.knownSessions.add(sessionId);
386
386
  await this.persistKnownSessions();
387
- await this.client.createTask(name);
387
+ try {
388
+ await this.client.createTask(name);
389
+ }
390
+ catch (error) {
391
+ this.knownSessions.delete(sessionId);
392
+ try {
393
+ await this.persistKnownSessions();
394
+ }
395
+ catch (rollbackError) {
396
+ throw new AggregateError([error, rollbackError], "WorkBuddy 新任务失败且会话记录回滚失败", { cause: error });
397
+ }
398
+ throw error;
399
+ }
388
400
  this.desktopSessionValid = true;
389
401
  this.expertSession = false;
390
402
  this.activeExpert = undefined;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-bridge-workbuddy",
3
- "version": "1.1.28",
3
+ "version": "1.1.29",
4
4
  "description": "mu-agent-bridge 的 WorkBuddy 桌面端插件。",
5
5
  "keywords": [
6
6
  "mu-agent-bridge-plugin",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "workbuddy",
3
- "version": "1.1.28",
3
+ "version": "1.1.29",
4
4
  "type": "agent",
5
5
  "displayName": "WorkBuddy",
6
6
  "entry": "dist/src-ts/plugins/workbuddy/index.js",