mu-agent-bridge 1.0.41 → 1.0.42

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.
@@ -7,7 +7,8 @@ async function agentAvailability(id, config, status) {
7
7
  const configured = typeof config.executablePath === "string" ? config.executablePath : undefined;
8
8
  const executable = id === "workbuddy" ? (0, workbuddy_1.findExecutable)(configured)
9
9
  : id === "qwenwork" ? await (0, desktop_launch_1.findWindowsExecutable)("QwenWorkCN.exe", configured)
10
- : id === "doubao" ? await (0, desktop_launch_1.findWindowsExecutable)("Doubao.exe", configured) : true;
10
+ : id === "doubao" ? await (0, desktop_launch_1.findWindowsExecutable)("Doubao.exe", configured)
11
+ : id === "dsh" ? await (0, desktop_launch_1.findWindowsExecutable)("DeepSeek Harness.exe", configured) : true;
11
12
  if (!executable)
12
13
  return { available: false, reason: "未找到可执行文件" };
13
14
  return { available: status === "ready", reason: status === "ready" ? "" : "插件未就绪" };
@@ -273,6 +273,10 @@ class ControlServer {
273
273
  await this.options.manager.reload(id);
274
274
  return this.json(res, 200, { ok: true });
275
275
  }
276
+ if (method === "POST" && parts[3] === "restart-desktop") {
277
+ await this.options.manager.restartDesktop(id);
278
+ return this.json(res, 200, { ok: true });
279
+ }
276
280
  if (method === "POST" && parts[3] === "update") {
277
281
  const body = await this.body(req);
278
282
  return this.json(res, 200, { ok: true, plugin: await this.options.manager.upgradePackage(id, body.packageName, body.version) });
@@ -21,6 +21,8 @@ const channel_sessions_1 = require("./channel-sessions");
21
21
  const session_management_1 = require("./session-management");
22
22
  const _2020_1 = __importDefault(require("ajv/dist/2020"));
23
23
  const attachments_1 = require("./attachments");
24
+ const desktop_launch_1 = require("./plugins/desktop-launch");
25
+ const workbuddy_1 = require("./plugins/workbuddy");
24
26
  const logger = { info() { }, warn() { }, error() { } };
25
27
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
26
28
  class PluginManager {
@@ -507,6 +509,20 @@ class PluginManager {
507
509
  await this.logs.flush();
508
510
  }
509
511
  async reload(id) { await this.stop(id, "reload"); await this.start(id); }
512
+ async restartDesktop(id) {
513
+ if (id !== "workbuddy")
514
+ throw new agent_1.AgentError(`不支持桌面重启:${id}`);
515
+ const config = await this.config(id);
516
+ if (config.transport !== "cdp")
517
+ throw new agent_1.AgentError("请先将 WorkBuddy 设置为 CDP 模式");
518
+ const executable = (0, workbuddy_1.findExecutable)(typeof config.executablePath === "string" ? config.executablePath : undefined);
519
+ if (!executable)
520
+ throw new agent_1.AgentError("未找到 WorkBuddy 桌面程序,无法重启");
521
+ await this.stop(id);
522
+ const processIds = await (0, desktop_launch_1.terminateWindowsDesktop)(executable);
523
+ this.log("info", "后台已清理 WorkBuddy 旧进程,将以 CDP 模式启动", { processIds, executable }, id);
524
+ await this.start(id);
525
+ }
510
526
  async uninstall(id) {
511
527
  const loaded = this.plugins.get(id);
512
528
  if (!loaded)
@@ -4,6 +4,9 @@ exports.ChatgptCdpClient = exports.ChatgptPlugin = exports.chatgptManifest = voi
4
4
  const node_crypto_1 = require("node:crypto");
5
5
  const cdp_client_1 = require("./cdp-client");
6
6
  const session_store_1 = require("./session-store");
7
+ const desktop_launch_1 = require("../desktop-launch");
8
+ const node_path_1 = require("node:path");
9
+ const node_os_1 = require("node:os");
7
10
  exports.chatgptManifest = {
8
11
  id: "chatgpt",
9
12
  version: "1.0.10",
@@ -35,6 +38,8 @@ class ChatgptPlugin {
35
38
  historyScope = "";
36
39
  historyGeneration = 0;
37
40
  historyProvider;
41
+ ownedProcess;
42
+ reconnect;
38
43
  constructor(options = {}) {
39
44
  this.options = options;
40
45
  }
@@ -87,21 +92,52 @@ class ChatgptPlugin {
87
92
  timeoutMs: typeof config.timeoutMs === "number" && config.timeoutMs > 0 ? config.timeoutMs : 30_000,
88
93
  ssh,
89
94
  });
95
+ const desktopExecutable = !ssh && config.autoStart !== false && !this.options.client && process.platform === "win32"
96
+ ? await (0, desktop_launch_1.findChatgptExecutable)(typeof config.executablePath === "string" ? config.executablePath : undefined)
97
+ : undefined;
98
+ const desktopProfile = (0, node_path_1.join)(context.dataDir ?? (0, node_path_1.join)((0, node_os_1.homedir)(), ".agent-bridge", "chatgpt"), "desktop-profile");
99
+ const desktopArgs = [`--remote-debugging-port=${port(config.localCdpPort, 9230)}`];
100
+ const startupTimeoutMs = typeof config.startupTimeoutMs === "number" ? config.startupTimeoutMs : 30_000;
101
+ this.reconnect = async () => {
102
+ try {
103
+ await this.client.connect();
104
+ return;
105
+ }
106
+ catch (first) {
107
+ if (!desktopExecutable)
108
+ throw first;
109
+ await (0, desktop_launch_1.restartOwnedAndWait)(this.ownedProcess, desktopExecutable, desktopArgs, desktopProfile, () => this.client.connect(), startupTimeoutMs, child => { this.ownedProcess = child; });
110
+ }
111
+ };
90
112
  this.sessionOperations = this.client.createSession ? ["new"] : [];
91
113
  try {
92
114
  await this.client.connect();
93
115
  }
94
116
  catch (error) {
95
- this.failure = error instanceof Error ? error.message : String(error);
117
+ let connectionError = error;
118
+ if (desktopExecutable) {
119
+ try {
120
+ await (0, desktop_launch_1.launchManagedAndWait)(desktopExecutable, desktopArgs, desktopProfile, () => this.client.connect(), startupTimeoutMs, child => { this.ownedProcess = child; });
121
+ this.failure = undefined;
122
+ return;
123
+ }
124
+ catch (launchError) {
125
+ connectionError = launchError;
126
+ }
127
+ }
128
+ this.failure = connectionError instanceof Error ? connectionError.message : String(connectionError);
96
129
  context.logger.warn(this.failure);
97
130
  if (!this.store.sessions.length)
98
- throw error;
131
+ throw connectionError;
99
132
  }
100
133
  }
101
134
  async deactivate() {
102
135
  await this.queue;
103
136
  await this.store?.drain();
104
137
  this.client?.close();
138
+ await (0, desktop_launch_1.terminateOwnedProcess)(this.ownedProcess).catch(() => undefined);
139
+ this.ownedProcess = undefined;
140
+ this.reconnect = undefined;
105
141
  this.client = undefined;
106
142
  this.remote = false;
107
143
  }
@@ -246,7 +282,7 @@ class ChatgptPlugin {
246
282
  }
247
283
  async synchronize() {
248
284
  if (!this.client.connected())
249
- await this.client.connect();
285
+ await (this.reconnect?.() ?? this.client.connect());
250
286
  if (!this.client.listSessions)
251
287
  throw new Error("当前 ChatGPT 客户端不支持会话列表");
252
288
  this.store.synchronize(await this.client.listSessions());
@@ -254,7 +290,7 @@ class ChatgptPlugin {
254
290
  }
255
291
  async select(session) {
256
292
  if (!this.client.connected())
257
- await this.client.connect();
293
+ await (this.reconnect?.() ?? this.client.connect());
258
294
  if (!this.client.switchSession)
259
295
  throw new Error("当前 ChatGPT 客户端不支持会话切换");
260
296
  const active = await this.client.activeSession?.();
@@ -321,7 +357,7 @@ class ChatgptPlugin {
321
357
  await this.select(selected);
322
358
  }
323
359
  if (!this.client.connected())
324
- await this.client.connect();
360
+ await (this.reconnect?.() ?? this.client.connect());
325
361
  selected ??= await this.client.activeSession?.();
326
362
  const message = explicit?.[2]?.trim() || request.task;
327
363
  const reply = await this.client.direct(message, request.timeoutMs, request.attachments);
@@ -3,10 +3,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.findWindowsExecutable = findWindowsExecutable;
4
4
  exports.findExecutableInTree = findExecutableInTree;
5
5
  exports.launchAndWait = launchAndWait;
6
+ exports.launchManagedAndWait = launchManagedAndWait;
7
+ exports.restartOwnedAndWait = restartOwnedAndWait;
8
+ exports.terminateWindowsDesktop = terminateWindowsDesktop;
9
+ exports.terminateOwnedProcess = terminateOwnedProcess;
10
+ exports.findChatgptExecutable = findChatgptExecutable;
6
11
  exports.waitFor = waitFor;
12
+ const node_child_process_1 = require("node:child_process");
7
13
  const promises_1 = require("node:fs/promises");
8
14
  const node_fs_1 = require("node:fs");
9
15
  const node_path_1 = require("node:path");
16
+ const node_util_1 = require("node:util");
17
+ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
10
18
  const versionDirectory = /^(?:app[-_ ]?)?v?\d+(?:[.-]\d+)+$/i;
11
19
  /** 在 Windows 常见安装目录中查找桌面程序,避免依赖单一盘符。 */
12
20
  async function findWindowsExecutable(fileName, configured) {
@@ -31,8 +39,11 @@ async function findWindowsExecutable(fileName, configured) {
31
39
  }
32
40
  if (process.platform !== "win32")
33
41
  return undefined;
34
- const pattern = /doubao|豆包|qwen|qwwork|千问/i;
42
+ const pattern = /doubao|豆包|qwen|qwwork|千问|deepseek|harness|dsh/i;
35
43
  const roots = [...new Set([
44
+ process.env.DSH_INSTALL_DIR,
45
+ "D:\\Programs\\DeepSeek Harness",
46
+ "D:\\DSH",
36
47
  process.env.LOCALAPPDATA && (0, node_path_1.join)(process.env.LOCALAPPDATA, "Programs"),
37
48
  process.env.ProgramFiles,
38
49
  process.env["ProgramFiles(x86)"],
@@ -83,6 +94,87 @@ async function launchAndWait(executable, args, connect, timeoutMs, launch = (fil
83
94
  child.unref();
84
95
  return waitFor(connect, timeoutMs, 250);
85
96
  }
97
+ async function launchManagedAndWait(executable, args, profileDirectory, connect, timeoutMs, onProcess, launch = (file, argv) => (0, node_child_process_1.spawn)(file, argv, { detached: true, stdio: "ignore", windowsHide: false })) {
98
+ if (profileDirectory)
99
+ await (0, promises_1.mkdir)(profileDirectory, { recursive: true });
100
+ const child = launch(executable, profileDirectory ? [...args, `--user-data-dir=${profileDirectory}`] : args);
101
+ child.unref();
102
+ onProcess(child);
103
+ try {
104
+ return await waitFor(connect, timeoutMs, 250);
105
+ }
106
+ catch (error) {
107
+ await terminateOwnedProcess(child).catch(() => undefined);
108
+ onProcess(undefined);
109
+ throw error;
110
+ }
111
+ }
112
+ async function restartOwnedAndWait(child, executable, args, profileDirectory, connect, timeoutMs, onProcess) {
113
+ await terminateOwnedProcess(child);
114
+ onProcess(undefined);
115
+ return launchManagedAndWait(executable, args, profileDirectory, connect, timeoutMs, onProcess);
116
+ }
117
+ /** 仅在用户确认重启后清理同一用户、同一安装路径的桌面进程。 */
118
+ async function terminateWindowsDesktop(executable) {
119
+ if (process.platform !== "win32")
120
+ return [];
121
+ const script = `
122
+ $ErrorActionPreference = 'Stop'
123
+ $target = '${(0, node_path_1.resolve)(executable).replace(/'/g, "''")}'
124
+ $sid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
125
+ $processes = @(Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -ieq $target } | Where-Object { (Invoke-CimMethod -InputObject $_ -MethodName GetOwnerSid).Sid -eq $sid })
126
+ $ids = @($processes.ProcessId)
127
+ $roots = @($processes | Where-Object { $ids -notcontains $_.ParentProcessId })
128
+ foreach ($item in $roots) {
129
+ $current = Get-CimInstance Win32_Process -Filter "ProcessId=$($item.ProcessId)"
130
+ if (-not $current -or $current.CreationDate -ne $item.CreationDate) { continue }
131
+ & taskkill.exe /PID $item.ProcessId /T /F | Out-Null
132
+ if ($LASTEXITCODE -ne 0 -and (Get-Process -Id $item.ProcessId -ErrorAction SilentlyContinue)) { throw "无法结束桌面进程 $($item.ProcessId)" }
133
+ $remaining = Get-Process -Id $item.ProcessId -ErrorAction SilentlyContinue
134
+ if ($remaining -and -not $remaining.WaitForExit(10000)) { throw "等待桌面进程退出超时 $($item.ProcessId)" }
135
+ }
136
+ ConvertTo-Json -Compress -InputObject @($roots | ForEach-Object { [int]$_.ProcessId })
137
+ `;
138
+ const { stdout } = await execFileAsync("powershell.exe", ["-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { timeout: 30_000, windowsHide: true });
139
+ return JSON.parse(stdout.trim());
140
+ }
141
+ async function terminateOwnedProcess(child, platform = process.platform, run = async (file, args) => execFileAsync(file, args, { timeout: 10_000 })) {
142
+ const pid = child?.pid;
143
+ if (!Number.isSafeInteger(pid) || !pid || child?.exitCode !== null && child?.exitCode !== undefined || child?.signalCode)
144
+ return;
145
+ if (platform === "win32") {
146
+ try {
147
+ await run("taskkill.exe", ["/PID", String(pid), "/T", "/F"]);
148
+ }
149
+ catch (error) {
150
+ if (!child.exitCode && !child.signalCode && error.code !== "ESRCH")
151
+ throw error;
152
+ }
153
+ }
154
+ else {
155
+ try {
156
+ process.kill(pid, "SIGTERM");
157
+ }
158
+ catch (error) {
159
+ if (error.code !== "ESRCH")
160
+ throw error;
161
+ }
162
+ }
163
+ }
164
+ async function findChatgptExecutable(configured) {
165
+ if (configured?.trim() && (0, node_fs_1.existsSync)((0, node_path_1.resolve)(configured)))
166
+ return (0, node_path_1.resolve)(configured);
167
+ if (process.platform !== "win32")
168
+ return undefined;
169
+ const script = "$candidates = foreach ($package in (Get-AppxPackage | Where-Object { $_.Name -like 'OpenAI.Codex*' })) { $appDir = Join-Path $package.InstallLocation 'app'; $path = Join-Path $appDir 'ChatGPT.exe'; if (-not (Test-Path -LiteralPath $path)) { $path = Get-ChildItem -LiteralPath $appDir -Filter 'ChatGPT*.exe' -File -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName }; if ($path) { [pscustomobject]@{ Version = [version]$package.Version; Executable = $path } } }; $candidate = $candidates | Sort-Object Version -Descending | Select-Object -First 1; if ($candidate) { $candidate.Executable }";
170
+ try {
171
+ const { stdout } = await execFileAsync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { timeout: 10_000 });
172
+ return stdout.trim().split(/\r?\n/).at(-1)?.trim() || undefined;
173
+ }
174
+ catch {
175
+ return undefined;
176
+ }
177
+ }
86
178
  async function waitFor(action, timeoutMs, intervalMs = 500) {
87
179
  const deadline = Date.now() + timeoutMs;
88
180
  let last;
@@ -97,4 +189,3 @@ async function waitFor(action, timeoutMs, intervalMs = 500) {
97
189
  }
98
190
  throw last instanceof Error ? last : new Error("桌面程序启动超时");
99
191
  }
100
- const node_child_process_1 = require("node:child_process");
@@ -7,6 +7,7 @@ const cdp_client_1 = require("./cdp-client");
7
7
  const session_history_1 = require("./session-history");
8
8
  const plugin_1 = require("../../plugin");
9
9
  const desktop_launch_1 = require("../desktop-launch");
10
+ const node_os_1 = require("node:os");
10
11
  exports.doubaoManifest = {
11
12
  id: "doubao",
12
13
  version: "1.0.20",
@@ -35,6 +36,7 @@ class DoubaoPlugin {
35
36
  }
36
37
  disposer;
37
38
  reconnect;
39
+ ownedProcess;
38
40
  async activate(context) {
39
41
  this.dataDir = context.dataDir;
40
42
  this.bindings.clear();
@@ -68,20 +70,18 @@ class DoubaoPlugin {
68
70
  }
69
71
  catch (first) {
70
72
  if (context.config.autoStart !== false && !this.injectedClient && process.platform === "win32") {
71
- const port = (0, plugin_1.resolveRuntimePort)(context.config, context.runtime, "doubao", "DOUBAO_CDP_PORT", 9227);
72
73
  const executable = await (0, desktop_launch_1.findWindowsExecutable)("Doubao.exe", typeof context.config.executablePath === "string" ? context.config.executablePath : undefined);
73
- if (executable) {
74
- context.logger?.warn?.(`正在启动豆包:${executable},等待 CDP ${port}`);
75
- try {
76
- await (0, desktop_launch_1.launchAndWait)(executable, [`--remote-debugging-port=${port}`], () => this.client.connect(), typeof context.config.startupTimeoutMs === "number" ? context.config.startupTimeoutMs : 30_000);
77
- return;
78
- }
79
- catch (error) {
80
- first = new Error(`已启动豆包,但 CDP ${port} 未就绪:${error instanceof Error ? error.message : String(error)}`);
81
- }
74
+ if (!executable)
75
+ throw new Error("未找到 Doubao.exe,请配置 executablePath");
76
+ const port = (0, plugin_1.resolveRuntimePort)(context.config, context.runtime, "doubao", "DOUBAO_CDP_PORT", 9227);
77
+ context.logger?.warn?.(`正在恢复豆包:${executable},等待 CDP ${port}`);
78
+ try {
79
+ await (0, desktop_launch_1.restartOwnedAndWait)(this.ownedProcess, executable, [`--remote-debugging-port=${port}`], (0, node_path_1.join)(this.dataDir ?? (0, node_path_1.join)((0, node_os_1.homedir)(), ".agent-bridge", "doubao"), "desktop-profile"), () => this.client.connect(), typeof context.config.startupTimeoutMs === "number" ? context.config.startupTimeoutMs : 30_000, child => { this.ownedProcess = child; });
80
+ return;
81
+ }
82
+ catch (error) {
83
+ first = new Error(`已启动豆包,但 CDP ${port} 未就绪:${error instanceof Error ? error.message : String(error)}`);
82
84
  }
83
- else
84
- first = new Error("未找到 Doubao.exe,请配置 executablePath");
85
85
  }
86
86
  throw first;
87
87
  }
@@ -106,6 +106,8 @@ class DoubaoPlugin {
106
106
  this.disposer = undefined;
107
107
  this.reconnect = undefined;
108
108
  await this.client?.close();
109
+ await (0, desktop_launch_1.terminateOwnedProcess)(this.ownedProcess).catch(() => undefined);
110
+ this.ownedProcess = undefined;
109
111
  this.client = undefined;
110
112
  this.state = "stopped";
111
113
  }
@@ -91,7 +91,9 @@ class WorkbuddyCdpClient {
91
91
  pending.reject(new Error("WorkBuddy CDP 已关闭")); this.pending.clear(); }
92
92
  async createTask() {
93
93
  const snapshot = `(() => { const selected = [...document.querySelectorAll('[data-conversation-id]')].find((item) => String(item.className).includes('_selected_')); const editor = document.querySelector(${JSON.stringify(exports.editorSelector)}); const text = editor?.cloneNode(true); text?.querySelectorAll('[data-slate-placeholder="true"],[data-slate-zero-width]').forEach((item) => item.remove()); return { url: location.href, conversationId: selected?.getAttribute('data-conversation-id') ?? null, selected: Boolean(selected), messages: document.querySelectorAll('.cr-agent[data-message-id]').length, editor: text?.textContent?.replace(/[\\uFEFF\\u200B]/g, '').trim() ?? null, home: Boolean(editor?.querySelector('[data-slate-placeholder="true"]')) }; })()`;
94
- const before = await this.evaluate(snapshot);
94
+ const before = await this.evaluate(`(() => { const state = ${snapshot}; const loginRequired = [...document.querySelectorAll('a,button,[role="button"]')].some(item => /^(?:登录|立即登录|sign in)$/i.test(item.textContent?.trim() ?? '')); return { ...state, loginRequired }; })()`);
95
+ if (before.loginRequired)
96
+ throw new Error("WorkBuddy 尚未登录,请先在桌面端登录后重试");
95
97
  if (!await this.evaluate(`(() => { const button = [...document.querySelectorAll('button,[role="button"]')].find((item) => item.textContent?.trim() === '新建任务'); if (!button || button.disabled || button.getAttribute('aria-disabled') === 'true') return false; button.click(); return true; })()`))
96
98
  throw new Error("WorkBuddy 新建任务入口未找到或不可用");
97
99
  // WorkBuddy 首页不分配任务 ID;此时仅接受无选中任务的空白首页。
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WorkbuddyPlugin = exports.workbuddyManifest = void 0;
4
4
  exports.extractExpertQuery = extractExpertQuery;
5
5
  exports.findExecutable = findExecutable;
6
- const node_child_process_1 = require("node:child_process");
7
6
  const node_fs_1 = require("node:fs");
8
7
  const promises_1 = require("node:fs/promises");
9
8
  const node_os_1 = require("node:os");
@@ -13,6 +12,7 @@ const plugin_1 = require("../../plugin");
13
12
  const cdp_client_1 = require("./cdp-client");
14
13
  const cli_client_1 = require("./cli-client");
15
14
  const session_history_1 = require("./session-history");
15
+ const desktop_launch_1 = require("../desktop-launch");
16
16
  exports.workbuddyManifest = {
17
17
  id: "workbuddy", version: "1.1.13", displayName: "WorkBuddy", type: "agent",
18
18
  entry: "dist/index.js", apiVersion: "1", process: "in-process",
@@ -28,13 +28,19 @@ function findExecutable(configured) {
28
28
  if (configured)
29
29
  return (0, node_fs_1.existsSync)((0, node_path_1.resolve)(configured)) ? (0, node_path_1.resolve)(configured) : undefined;
30
30
  const local = process.env.LOCALAPPDATA ?? (0, node_path_1.join)((0, node_os_1.homedir)(), "AppData", "Local");
31
- return [(0, node_path_1.join)(local, "Programs", "WorkBuddy", "WorkBuddy.exe"), "D:\\Programs\\WorkBuddy\\WorkBuddy.exe"].find(node_fs_1.existsSync);
31
+ return [
32
+ (0, node_path_1.join)(local, "Programs", "WorkBuddy", "WorkBuddy.exe"),
33
+ (0, node_path_1.join)(local, "Programs", "WorkBuddyAI", "WorkBuddyAI.exe"),
34
+ "D:\\Programs\\WorkBuddy\\WorkBuddy.exe",
35
+ "D:\\Programs\\WorkBuddyAI\\WorkBuddyAI.exe",
36
+ ].find(node_fs_1.existsSync);
32
37
  }
33
38
  class WorkbuddyPlugin {
34
39
  options;
35
40
  manifest = exports.workbuddyManifest;
36
41
  client;
37
42
  process;
43
+ reconnect;
38
44
  cliPath;
39
45
  model;
40
46
  transport = "auto";
@@ -125,10 +131,11 @@ class WorkbuddyPlugin {
125
131
  async deactivate(_reason) {
126
132
  await this.queue;
127
133
  await this.sessionWrites.catch(() => { });
134
+ await (0, desktop_launch_1.terminateOwnedProcess)(this.process).catch(() => undefined);
128
135
  this.client?.close();
129
136
  this.client = undefined;
130
- // WorkBuddy 是用户桌面应用,插件停止时不结束桌面进程。
131
137
  this.process = undefined;
138
+ this.reconnect = undefined;
132
139
  }
133
140
  async health() {
134
141
  if (this.client?.connected())
@@ -165,6 +172,8 @@ class WorkbuddyPlugin {
165
172
  throw new Error("WorkBuddy 新会话缺少 sessionId");
166
173
  if (this.transport === "cli" || (this.transport !== "cdp" && this.cliPath) || !this.client?.createTask)
167
174
  throw new Error("WorkBuddy 当前传输不支持新建会话");
175
+ if (!this.client.connected())
176
+ await this.reconnect?.();
168
177
  if (!this.client.connected())
169
178
  throw new Error("WorkBuddy CDP 未连接");
170
179
  return this.enqueue(async () => {
@@ -181,6 +190,8 @@ class WorkbuddyPlugin {
181
190
  const hasAttachments = Array.isArray(request.attachments) && request.attachments.length > 0;
182
191
  if (hasAttachments && !this.attachmentInput)
183
192
  throw new Error("WorkBuddy 当前传输不支持附件输入");
193
+ if (!usesCli && !this.client?.connected())
194
+ await this.reconnect?.();
184
195
  const pendingInteractionSessionId = this.client?.pendingInteractionSessionId?.();
185
196
  if (!usesCli && pendingInteractionSessionId && pendingInteractionSessionId !== request.sessionId)
186
197
  throw new Error("WorkBuddy 正在等待另一个会话的选择");
@@ -304,6 +315,21 @@ class WorkbuddyPlugin {
304
315
  return run;
305
316
  }
306
317
  async connectOrStart(config, logger, port) {
318
+ const executable = findExecutable(typeof config.executablePath === "string" ? config.executablePath : this.options.executablePath);
319
+ // 沿用 WorkBuddy 的默认用户目录,保留用户在桌面端的登录状态。
320
+ const profile = undefined;
321
+ const timeoutMs = typeof config.startupTimeoutMs === "number" ? config.startupTimeoutMs : 30_000;
322
+ this.reconnect = async () => {
323
+ try {
324
+ await this.client.connect();
325
+ return;
326
+ }
327
+ catch (first) {
328
+ if (config.autoStart === false || this.options.client || !executable)
329
+ throw first;
330
+ await (0, desktop_launch_1.restartOwnedAndWait)(this.process, executable, [`--remote-debugging-port=${port}`], profile, () => this.client.connect(), timeoutMs, child => { this.process = child; });
331
+ }
332
+ };
307
333
  try {
308
334
  await this.client.connect();
309
335
  return;
@@ -311,25 +337,10 @@ class WorkbuddyPlugin {
311
337
  catch (first) {
312
338
  if (config.autoStart === false || this.options.client)
313
339
  throw first;
314
- const executable = findExecutable(typeof config.executablePath === "string" ? config.executablePath : this.options.executablePath);
315
340
  if (!executable)
316
341
  throw new Error("未找到 WorkBuddy.exe,请配置 executablePath");
317
- this.process = (0, node_child_process_1.spawn)(executable, [`--remote-debugging-port=${port}`], { detached: true, stdio: "ignore", windowsHide: false });
318
- this.process.unref();
319
342
  logger.info(`WorkBuddy 已启动,等待 CDP ${port}`);
320
- const deadline = Date.now() + (typeof config.startupTimeoutMs === "number" ? config.startupTimeoutMs : 30_000);
321
- let last = first;
322
- while (Date.now() < deadline) {
323
- await new Promise((resolve) => setTimeout(resolve, 500));
324
- try {
325
- await this.client.connect();
326
- return;
327
- }
328
- catch (error) {
329
- last = error;
330
- }
331
- }
332
- throw last instanceof Error ? last : new Error("WorkBuddy CDP 启动超时");
343
+ await (0, desktop_launch_1.launchManagedAndWait)(executable, [`--remote-debugging-port=${port}`], profile, () => this.client.connect(), timeoutMs, child => { this.process = child; });
333
344
  }
334
345
  }
335
346
  }
@@ -9,19 +9,33 @@ const node_os_1 = require("node:os");
9
9
  const node_path_1 = require("node:path");
10
10
  const node_test_1 = __importDefault(require("node:test"));
11
11
  const agent_availability_1 = require("../src-ts/agent-availability");
12
+ const workbuddy_1 = require("../src-ts/plugins/workbuddy");
12
13
  (0, node_test_1.default)("办公 Agent 根据可执行文件和就绪状态决定是否可选", async () => {
13
14
  const dir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "office-agent-"));
15
+ const previousLocalAppData = process.env.LOCALAPPDATA;
14
16
  try {
15
17
  const file = (0, node_path_1.join)(dir, "WorkBuddy.exe");
18
+ const dshFile = (0, node_path_1.join)(dir, "DeepSeek Harness.exe");
16
19
  strict_1.default.deepEqual(await (0, agent_availability_1.agentAvailability)("workbuddy", { executablePath: file }, "ready"), { available: false, reason: "未找到可执行文件" });
17
20
  await (0, promises_1.writeFile)(file, "测试程序");
18
21
  strict_1.default.deepEqual(await (0, agent_availability_1.agentAvailability)("workbuddy", { executablePath: file }, "ready"), { available: true, reason: "" });
22
+ const workbuddyAi = (0, node_path_1.join)(dir, "Programs", "WorkBuddyAI", "WorkBuddyAI.exe");
23
+ await (0, promises_1.mkdir)((0, node_path_1.join)(dir, "Programs", "WorkBuddyAI"), { recursive: true });
24
+ await (0, promises_1.writeFile)(workbuddyAi, "测试程序");
25
+ process.env.LOCALAPPDATA = dir;
26
+ strict_1.default.equal((0, workbuddy_1.findExecutable)(), workbuddyAi);
19
27
  strict_1.default.equal((await (0, agent_availability_1.agentAvailability)("workbuddy", { executablePath: file }, "failed")).available, false);
20
28
  strict_1.default.equal((await (0, agent_availability_1.agentAvailability)("qwenwork", { executablePath: (0, node_path_1.join)(dir, "missing.exe") }, "ready")).available, false);
21
29
  strict_1.default.equal((await (0, agent_availability_1.agentAvailability)("doubao", { executablePath: file }, "ready")).available, true);
22
- strict_1.default.equal((await (0, agent_availability_1.agentAvailability)("dsh", {}, "ready")).available, true);
30
+ strict_1.default.deepEqual(await (0, agent_availability_1.agentAvailability)("dsh", {}, "ready"), { available: false, reason: "未找到可执行文件" });
31
+ await (0, promises_1.writeFile)(dshFile, "测试程序");
32
+ strict_1.default.deepEqual(await (0, agent_availability_1.agentAvailability)("dsh", { executablePath: dshFile }, "ready"), { available: true, reason: "" });
23
33
  }
24
34
  finally {
35
+ if (previousLocalAppData === undefined)
36
+ delete process.env.LOCALAPPDATA;
37
+ else
38
+ process.env.LOCALAPPDATA = previousLocalAppData;
25
39
  await (0, promises_1.rm)(dir, { recursive: true, force: true });
26
40
  }
27
41
  });
@@ -48,6 +48,26 @@ const node_os_1 = require("node:os");
48
48
  const node_path_1 = require("node:path");
49
49
  const node_crypto_1 = require("node:crypto");
50
50
  const plugin_manager_1 = require("../src-ts/plugin-manager");
51
+ (0, node_test_1.default)("桌面重启请求交给 mab 后台执行并保留失败原因", async () => {
52
+ const calls = [];
53
+ const manager = { restartDesktop: async (id) => { calls.push(id); if (id !== "workbuddy")
54
+ throw new Error("不支持桌面重启"); } };
55
+ const server = new control_server_1.ControlServer({ manager, host: "127.0.0.1", port: 0 });
56
+ const address = await server.start();
57
+ const base = `http://${address.host}:${address.port}`;
58
+ try {
59
+ const response = await fetch(`${base}/api/plugins/workbuddy/restart-desktop`, { method: "POST" });
60
+ strict_1.default.equal(response.status, 200);
61
+ strict_1.default.deepEqual(await response.json(), { ok: true });
62
+ const failed = await fetch(`${base}/api/plugins/other/restart-desktop`, { method: "POST" });
63
+ strict_1.default.equal(failed.status, 400);
64
+ strict_1.default.deepEqual(await failed.json(), { error: "不支持桌面重启" });
65
+ strict_1.default.deepEqual(calls, ["workbuddy", "other"]);
66
+ }
67
+ finally {
68
+ await server.stop();
69
+ }
70
+ });
51
71
  (0, node_test_1.default)("关联 EvolCore AID 时配置相应的 Agent 路由", async () => {
52
72
  const calls = [];
53
73
  await (0, evolcore_1.configureAidForPlugin)("dsh", "test.agentid.cn", 26107, async (args) => { calls.push(args); return { ok: true }; });
@@ -1,10 +1,44 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
5
38
  Object.defineProperty(exports, "__esModule", { value: true });
6
39
  const strict_1 = __importDefault(require("node:assert/strict"));
7
40
  const promises_1 = require("node:fs/promises");
41
+ const node_child_process_1 = require("node:child_process");
8
42
  const node_os_1 = require("node:os");
9
43
  const node_path_1 = require("node:path");
10
44
  const node_test_1 = __importDefault(require("node:test"));
@@ -77,6 +111,70 @@ const desktop_launch_1 = require("../src-ts/plugins/desktop-launch");
77
111
  strict_1.default.equal(launches, 1);
78
112
  strict_1.default.equal(attempts, 3);
79
113
  });
114
+ (0, node_test_1.default)("结束桌面程序时只按插件保存的 PID 结束进程树", async () => {
115
+ const calls = [];
116
+ await (0, desktop_launch_1.terminateOwnedProcess)({ pid: 3210, exitCode: null, signalCode: null, unref() { } }, "win32", async (file, args) => { calls.push({ file, args }); });
117
+ await (0, desktop_launch_1.terminateOwnedProcess)(undefined, "win32", async (file, args) => { calls.push({ file, args }); });
118
+ strict_1.default.deepEqual(calls, [{ file: "taskkill.exe", args: ["/PID", "3210", "/T", "/F"] }]);
119
+ });
120
+ (0, node_test_1.default)("管理式启动增加独立用户目录并只记录本次启动进程", async () => {
121
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "desktop-profile-"));
122
+ let ownedPid;
123
+ let launchArgs = [];
124
+ try {
125
+ const result = await (await Promise.resolve().then(() => __importStar(require("../src-ts/plugins/desktop-launch")))).launchManagedAndWait("client.exe", ["--remote-debugging-port=9229"], (0, node_path_1.join)(root, "profile"), async () => "connected", 1000, child => { ownedPid = child?.pid; }, (_file, args) => { launchArgs = args; return { pid: 4567, exitCode: null, signalCode: null, unref() { } }; });
126
+ strict_1.default.equal(result, "connected");
127
+ strict_1.default.equal(ownedPid, 4567);
128
+ strict_1.default.deepEqual(launchArgs, ["--remote-debugging-port=9229", `--user-data-dir=${(0, node_path_1.join)(root, "profile")}`]);
129
+ }
130
+ finally {
131
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
132
+ }
133
+ });
134
+ (0, node_test_1.default)("使用客户端原有用户目录时,管理式启动不覆盖用户数据目录", async () => {
135
+ const { launchManagedAndWait } = await Promise.resolve().then(() => __importStar(require("../src-ts/plugins/desktop-launch")));
136
+ let args = [];
137
+ const result = await launchManagedAndWait("WorkBuddyAI.exe", ["--remote-debugging-port=9229"], undefined, async () => "connected", 1000, () => { }, (_file, argv) => {
138
+ args = argv;
139
+ return { pid: 1234, exitCode: null, signalCode: null, unref() { } };
140
+ });
141
+ strict_1.default.equal(result, "connected");
142
+ strict_1.default.deepEqual(args, ["--remote-debugging-port=9229"]);
143
+ });
144
+ (0, node_test_1.default)("Windows 后台清理已有 CDP 桌面进程,保留不同安装路径的同名程序", { skip: process.platform !== "win32" }, async () => {
145
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "mab-kill-desktop-"));
146
+ const children = [];
147
+ try {
148
+ for (const folder of ["selected", "other"]) {
149
+ await (0, promises_1.mkdir)((0, node_path_1.join)(root, folder));
150
+ const executable = (0, node_path_1.join)(root, folder, "WorkBuddyAI.exe");
151
+ await (0, promises_1.copyFile)((0, node_path_1.join)(process.env.SystemRoot, "System32", "cmd.exe"), executable);
152
+ const child = (0, node_child_process_1.spawn)(executable, ["/d", "/s", "/c", "ping -n 60 127.0.0.1 >nul & rem --remote-debugging-port=9229"], { windowsHide: true, stdio: "ignore" });
153
+ await new Promise((resolve, reject) => { child.once("spawn", resolve); child.once("error", reject); });
154
+ children.push(child);
155
+ }
156
+ const pids = await (0, desktop_launch_1.terminateWindowsDesktop)((0, node_path_1.join)(root, "selected", "WorkBuddyAI.exe"));
157
+ strict_1.default.deepEqual(pids, [children[0].pid]);
158
+ process.kill(children[1].pid, 0);
159
+ strict_1.default.deepEqual(await (0, desktop_launch_1.terminateWindowsDesktop)((0, node_path_1.join)(root, "selected", "WorkBuddyAI.exe")), []);
160
+ }
161
+ finally {
162
+ for (const child of children)
163
+ await (0, desktop_launch_1.terminateOwnedProcess)(child);
164
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
165
+ }
166
+ });
167
+ (0, node_test_1.default)("ChatGPT 可执行文件支持显式配置路径", async () => {
168
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "chatgpt-executable-"));
169
+ try {
170
+ const executable = (0, node_path_1.join)(root, "ChatGPT.exe");
171
+ await (0, promises_1.writeFile)(executable, "");
172
+ strict_1.default.equal(await (await Promise.resolve().then(() => __importStar(require("../src-ts/plugins/desktop-launch")))).findChatgptExecutable(executable), executable);
173
+ }
174
+ finally {
175
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
176
+ }
177
+ });
80
178
  (0, node_test_1.default)("豆包模糊查找版本目录,升级后自动避开旧版和上层启动器", async () => {
81
179
  const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "doubao-upgrade-"));
82
180
  try {