mu-agent-bridge 1.0.65 → 1.0.67

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.
@@ -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;
@@ -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.30", 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
  };
@@ -364,7 +364,15 @@ class WorkbuddyPlugin {
364
364
  if (this.knownSessions.has(sessionId)) {
365
365
  const binding = this.sessionBindings.get(sessionId);
366
366
  const conversationId = binding?.conversationId ?? (this.recoverableSessionId === sessionId ? this.recoverableConversationId : undefined);
367
- if (!conversationId || !this.client.activateConversation)
367
+ if (!conversationId) {
368
+ // 旧版本可能在新任务校验前写入会话 ID,无法恢复时允许重新创建。
369
+ this.knownSessions.delete(sessionId);
370
+ this.sessionBindings.delete(sessionId);
371
+ await this.persistKnownSessions();
372
+ await this.createSession(sessionId);
373
+ return;
374
+ }
375
+ if (!this.client.activateConversation)
368
376
  throw new Error("WorkBuddy 不支持恢复旧 WorkBuddy 会话");
369
377
  await this.client.activateConversation(conversationId);
370
378
  this.activeSessionId = sessionId;
@@ -384,7 +392,19 @@ class WorkbuddyPlugin {
384
392
  this.activeDesktopConversationId = undefined;
385
393
  this.knownSessions.add(sessionId);
386
394
  await this.persistKnownSessions();
387
- await this.client.createTask(name);
395
+ try {
396
+ await this.client.createTask(name);
397
+ }
398
+ catch (error) {
399
+ this.knownSessions.delete(sessionId);
400
+ try {
401
+ await this.persistKnownSessions();
402
+ }
403
+ catch (rollbackError) {
404
+ throw new AggregateError([error, rollbackError], "WorkBuddy 新任务失败且会话记录回滚失败", { cause: error });
405
+ }
406
+ throw error;
407
+ }
388
408
  this.desktopSessionValid = true;
389
409
  this.expertSession = false;
390
410
  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
+ });
@@ -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\(\)/);
@@ -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;
@@ -1165,7 +1162,7 @@ const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
1165
1162
  await strict_1.default.rejects(() => plugin.handle({ task: "调用文案写作专家改写" }), /sessionId|会话/);
1166
1163
  strict_1.default.equal(calls, 1);
1167
1164
  });
1168
- (0, node_test_1.default)("workbuddy 跨插件重启持久化已使用的会话 ID", async () => {
1165
+ (0, node_test_1.default)("workbuddy 跨插件重启清理没有桌面绑定的旧会话并重新创建", async () => {
1169
1166
  const dataDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "wb-sessions-"));
1170
1167
  let creates = 0;
1171
1168
  const client = {
@@ -1180,8 +1177,8 @@ const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
1180
1177
  await first.deactivate("reload");
1181
1178
  const second = new workbuddy_1.WorkbuddyPlugin({ client });
1182
1179
  await second.activate(context);
1183
- await strict_1.default.rejects(() => second.handle({ task: "恢复旧会话", sessionId: "session-a" }), /不支持恢复旧 WorkBuddy 会话/);
1184
- strict_1.default.equal(creates, 1);
1180
+ strict_1.default.equal((await second.handle({ task: "恢复旧会话", sessionId: "session-a" })).text, "恢复旧会话");
1181
+ strict_1.default.equal(creates, 2);
1185
1182
  await second.deactivate("stop");
1186
1183
  }
1187
1184
  finally {
@@ -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.65",
3
+ "version": "1.0.67",
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
  }
@@ -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.30", 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
  };
@@ -364,7 +364,15 @@ class WorkbuddyPlugin {
364
364
  if (this.knownSessions.has(sessionId)) {
365
365
  const binding = this.sessionBindings.get(sessionId);
366
366
  const conversationId = binding?.conversationId ?? (this.recoverableSessionId === sessionId ? this.recoverableConversationId : undefined);
367
- if (!conversationId || !this.client.activateConversation)
367
+ if (!conversationId) {
368
+ // 旧版本可能在新任务校验前写入会话 ID,无法恢复时允许重新创建。
369
+ this.knownSessions.delete(sessionId);
370
+ this.sessionBindings.delete(sessionId);
371
+ await this.persistKnownSessions();
372
+ await this.createSession(sessionId);
373
+ return;
374
+ }
375
+ if (!this.client.activateConversation)
368
376
  throw new Error("WorkBuddy 不支持恢复旧 WorkBuddy 会话");
369
377
  await this.client.activateConversation(conversationId);
370
378
  this.activeSessionId = sessionId;
@@ -384,7 +392,19 @@ class WorkbuddyPlugin {
384
392
  this.activeDesktopConversationId = undefined;
385
393
  this.knownSessions.add(sessionId);
386
394
  await this.persistKnownSessions();
387
- await this.client.createTask(name);
395
+ try {
396
+ await this.client.createTask(name);
397
+ }
398
+ catch (error) {
399
+ this.knownSessions.delete(sessionId);
400
+ try {
401
+ await this.persistKnownSessions();
402
+ }
403
+ catch (rollbackError) {
404
+ throw new AggregateError([error, rollbackError], "WorkBuddy 新任务失败且会话记录回滚失败", { cause: error });
405
+ }
406
+ throw error;
407
+ }
388
408
  this.desktopSessionValid = true;
389
409
  this.expertSession = false;
390
410
  this.activeExpert = undefined;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-bridge-workbuddy",
3
- "version": "1.1.28",
3
+ "version": "1.1.30",
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.30",
4
4
  "type": "agent",
5
5
  "displayName": "WorkBuddy",
6
6
  "entry": "dist/src-ts/plugins/workbuddy/index.js",