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.
@@ -13,18 +13,27 @@ const node_util_1 = require("node:util");
13
13
  strict_1.default.match(stdout, /验证通过/);
14
14
  });
15
15
  (0, node_test_1.default)("Windows 安装包包括中文向导、四类 Agent 和 per-user 卸载定义", async () => {
16
- const [wxs, setup, launcher, build, qwenPlugin, workbuddyPlugin] = await Promise.all([
16
+ const [wxs, setup, launcher, build, uninstall, qwenPlugin, workbuddyPlugin] = await Promise.all([
17
17
  (0, promises_1.readFile)("installer/Product.wxs", "utf8"),
18
18
  (0, promises_1.readFile)("installer/setup.ps1", "utf8"),
19
- (0, promises_1.readFile)("installer/launch.cmd", "utf8"),
19
+ (0, promises_1.readFile)("installer/launch.vbs", "utf8"),
20
20
  (0, promises_1.readFile)("scripts/build-installer.ps1", "utf8"),
21
+ (0, promises_1.readFile)("installer/uninstall.ps1", "utf8"),
21
22
  (0, promises_1.readFile)("plugins/qwenwork/plugin.json", "utf8"),
22
23
  (0, promises_1.readFile)("plugins/workbuddy/plugin.json", "utf8"),
23
24
  ]);
24
25
  strict_1.default.match(wxs, /Scope="perUser"/);
25
26
  strict_1.default.match(wxs, /On="uninstall"/);
26
27
  strict_1.default.match(wxs, /LaunchWizard/);
28
+ strict_1.default.match(wxs, /launch\.vbs/);
29
+ strict_1.default.match(wxs, /Source="\$\(var\.SourceDir\)\\uninstall\.ps1"/);
30
+ strict_1.default.match(wxs, /<CustomAction Id="CleanupBridge"[^>]*Execute="deferred"[^>]*Return="ignore"/);
31
+ strict_1.default.match(wxs, /<Custom Action="CleanupBridge" Before="RemoveFiles" Condition="REMOVE=&quot;ALL&quot;"/);
32
+ strict_1.default.match(uninstall, /api\/plugins/);
33
+ strict_1.default.match(uninstall, /Stop-Process/);
34
+ strict_1.default.match(uninstall, /node_modules.*mu-agent-bridge/);
27
35
  strict_1.default.match(launcher, /setup\.ps1/);
36
+ strict_1.default.match(launcher, /WindowStyle Hidden/);
28
37
  strict_1.default.match(build, /ProductVersion=\$installerVersion/);
29
38
  strict_1.default.match(build, /\$packageVersion\.Build \+ 1/);
30
39
  strict_1.default.match(setup, /22\.16\.0/);
@@ -38,7 +47,13 @@ const node_util_1 = require("node:util");
38
47
  strict_1.default.doesNotMatch(setup, /mu-agent-bridge@\d+\.\d+\.\d+/);
39
48
  strict_1.default.match(setup, /api\/agents\/availability/);
40
49
  strict_1.default.match(setup, /function Find-AgentExecutable/);
41
- strict_1.default.match(setup, /Get-ChildItem -LiteralPath \$root -Filter \$app\.File -File -Recurse/);
50
+ strict_1.default.match(setup, /CheckedListBox/);
51
+ strict_1.default.match(setup, /DataGridViewComboBoxColumn/);
52
+ strict_1.default.match(setup, /\[void\]\$grid\.EndEdit\(\)/);
53
+ strict_1.default.match(setup, /\$grid\.Rows\.Add\(\$true, \$aid, \$null\)/);
54
+ strict_1.default.match(setup, /显示详细信息/);
55
+ strict_1.default.match(setup, /环境与桥接服务/);
56
+ strict_1.default.match(setup, /Get-ChildItem -LiteralPath \$root -Filter \$fileName -File -Recurse/);
42
57
  strict_1.default.match(setup, /api\/plugins\/\$agent\/config/);
43
58
  strict_1.default.match(setup, /TopMost = \$true/);
44
59
  strict_1.default.match(setup, /BringToFront\(\)/);
@@ -58,6 +73,10 @@ const node_util_1 = require("node:util");
58
73
  strict_1.default.equal(condition[1], "NOT REMOVE AND NOT UPGRADINGPRODUCTCODE");
59
74
  strict_1.default.match(wxs, /<InstallUISequence>\s*<Custom Action="LaunchWizard" After="ExecuteAction"/);
60
75
  });
76
+ (0, node_test_1.default)("Windows 卸载时先停止插件再结束 mab 后台", { skip: process.platform !== "win32" }, async () => {
77
+ const { stdout } = await (0, node_util_1.promisify)(node_child_process_1.execFile)("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "tests/windows-uninstall.test.ps1"]);
78
+ strict_1.default.match(stdout, /卸载清理验证通过/);
79
+ });
61
80
  (0, node_test_1.default)("向导后台启动服务并提供缺失客户端的下载和重试入口", async () => {
62
81
  const setup = await (0, promises_1.readFile)("installer/setup.ps1", "utf8");
63
82
  strict_1.default.doesNotMatch(setup, /& mab\.cmd start/);
@@ -72,17 +91,17 @@ const node_util_1 = require("node:util");
72
91
  const setup = await (0, promises_1.readFile)("installer/setup.ps1", "utf8");
73
92
  const selectAid = setup.indexOf("$aid = Resolve-SetupAid");
74
93
  const detect = setup.indexOf("$agents = Invoke-RestMethod");
75
- const bind = setup.indexOf("Invoke-AunCommand @('bind'");
94
+ const bind = setup.indexOf("Invoke-GuiBindingPlan @(");
76
95
  strict_1.default.ok(detect > 0 && detect < selectAid && selectAid < bind);
77
96
  strict_1.default.match(setup, /新 AID(示例:workbuddy11\.agentid\.cn)/);
78
97
  strict_1.default.match(setup, /Owner AID(示例:lihui7\.agentid\.cn)/);
79
98
  strict_1.default.match(setup, /\$readyAgents.Count -lt 4/);
80
- strict_1.default.match(setup, /\$InstalledIds -notcontains \$_.Id/);
99
+ strict_1.default.match(setup, /\$InstalledIds -contains \$_.Id/);
81
100
  strict_1.default.match(setup, /尚未就绪/);
82
101
  strict_1.default.match(setup, /AID 关联失败/);
83
102
  strict_1.default.match(setup, /setup\.log/);
84
103
  strict_1.default.match(setup, /Write-SetupLog/);
85
- strict_1.default.match(setup, /WorkBuddy 正在运行但未开启 CDP/);
104
+ strict_1.default.match(setup, /包括已有 CDP 进程/);
86
105
  strict_1.default.match(setup, /taskkill\.exe \/PID/);
87
106
  strict_1.default.match(setup, /function Stop-BridgeDaemons/);
88
107
  strict_1.default.match(setup, /api\/plugins\/aun\/commands/);
@@ -91,9 +110,13 @@ const node_util_1 = require("node:util");
91
110
  strict_1.default.match(setup, /确认改为 \$agent?输入 YES 继续\"\)\.Trim\(\) -ne 'YES'/);
92
111
  strict_1.default.doesNotMatch(setup, /-cne 'YES'/);
93
112
  strict_1.default.match(setup, /qwenwork\.cn\/download/);
94
- strict_1.default.match(setup, /检测到本机已有 AID/);
113
+ strict_1.default.match(setup, /选择本机已有 AID/);
95
114
  strict_1.default.match(setup, /IdentityConflictError/);
96
115
  strict_1.default.match(setup, /返回 AID 输入步骤/);
116
+ strict_1.default.match(setup, /MessageBox\]::Show\(\$script:WizardForm, \$message, 'AID 已注册'/);
97
117
  strict_1.default.match(setup, /transport = 'cdp'/);
98
118
  strict_1.default.match(setup, /workbuddy\/stop/);
119
+ strict_1.default.match(setup, /workbuddy\/restart-desktop/);
120
+ const prepare = setup.slice(setup.indexOf('function Initialize-WorkbuddyCdp'), setup.indexOf('function Invoke-GuiBindingPlan'));
121
+ strict_1.default.doesNotMatch(prepare, /taskkill|Stop-Process|cdpReady/);
99
122
  });
@@ -45,6 +45,24 @@ 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)("后台桌面重启先停止插件再启动,拒绝无关插件", { skip: process.platform === "win32" }, async () => {
49
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "mab-desktop-restart-"));
50
+ const manager = new plugin_manager_1.PluginManager({ pluginsDir: (0, node_path_1.join)(root, "plugins"), stateDir: (0, node_path_1.join)(root, "state") });
51
+ const calls = [];
52
+ manager.config = async () => ({ transport: "cdp", executablePath: (0, node_path_1.join)(root, "WorkBuddyAI.exe") });
53
+ manager.stop = async (id) => { calls.push(`stop:${id}`); };
54
+ manager.start = async (id) => { calls.push(`start:${id}`); };
55
+ try {
56
+ await (0, promises_1.writeFile)((0, node_path_1.join)(root, "WorkBuddyAI.exe"), "");
57
+ await manager.restartDesktop("workbuddy");
58
+ strict_1.default.deepEqual(calls, ["stop:workbuddy", "start:workbuddy"]);
59
+ await strict_1.default.rejects(() => manager.restartDesktop("other"), /不支持桌面重启/);
60
+ strict_1.default.equal(calls.length, 2);
61
+ }
62
+ finally {
63
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
64
+ }
65
+ });
48
66
  (0, node_test_1.default)("插件重载同时刷新内部依赖,其他插件的缓存保持不变", async () => {
49
67
  const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "mab-reload-dependency-"));
50
68
  const manager = new plugin_manager_1.PluginManager({ pluginsDir: (0, node_path_1.join)(root, "plugins"), stateDir: (0, node_path_1.join)(root, "state") });
@@ -8,6 +8,13 @@ const promises_1 = require("node:fs/promises");
8
8
  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
+ (0, node_test_1.default)("WorkBuddy 登录页阻止新建验证任务并提示先登录", async () => {
12
+ const client = new cdp_client_1.WorkbuddyCdpClient();
13
+ let calls = 0;
14
+ client.evaluate = async () => { calls++; return { loginRequired: true }; };
15
+ await strict_1.default.rejects(() => client.createTask(), /尚未登录.*桌面端登录/);
16
+ strict_1.default.equal(calls, 1);
17
+ });
11
18
  const node_vm_1 = require("node:vm");
12
19
  const workbuddy_1 = require("../src-ts/plugins/workbuddy");
13
20
  const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mu-agent-bridge",
3
- "version": "1.0.41",
3
+ "version": "1.0.42",
4
4
  "description": "将 AUN Agent 连接到本地桌面 AI 客户端的插件平台。",
5
5
  "main": "dist/src-ts/cli.js",
6
6
  "bin": {
@@ -1 +1 @@
1
- {"type":"object","properties":{"sshHost":{"type":"string","description":"Windows 主机地址"},"sshUser":{"type":"string","description":"SSH 用户名"},"sshPort":{"type":"integer","minimum":1,"maximum":65535,"default":22},"sshKeyPath":{"type":"string","description":"SSH 私钥路径"},"remoteCdpPort":{"type":"integer","minimum":1,"maximum":65535,"default":9230},"localCdpPort":{"type":"integer","minimum":1,"maximum":65535,"default":9230},"timeoutMs":{"type":"integer","minimum":1,"default":30000}},"additionalProperties":false}
1
+ {"type":"object","properties":{"sshHost":{"type":"string","description":"Windows 主机地址"},"sshUser":{"type":"string","description":"SSH 用户名"},"sshPort":{"type":"integer","minimum":1,"maximum":65535,"default":22},"sshKeyPath":{"type":"string","description":"SSH 私钥路径"},"remoteCdpPort":{"type":"integer","minimum":1,"maximum":65535,"default":9230},"localCdpPort":{"type":"integer","minimum":1,"maximum":65535,"default":9230},"executablePath":{"type":"string","description":"ChatGPT.exe 路径,省略时自动查找 Windows Store 安装版本"},"autoStart":{"type":"boolean","default":true,"description":"本机 CDP 未连接时自动启动 ChatGPT;配置 SSH 时不在 bridge 主机启动"},"startupTimeoutMs":{"type":"integer","minimum":1000,"default":30000},"timeoutMs":{"type":"integer","minimum":1,"default":30000}},"additionalProperties":false}
@@ -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);
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findWindowsExecutable = findWindowsExecutable;
4
+ exports.findExecutableInTree = findExecutableInTree;
5
+ exports.launchAndWait = launchAndWait;
6
+ exports.launchManagedAndWait = launchManagedAndWait;
7
+ exports.restartOwnedAndWait = restartOwnedAndWait;
8
+ exports.terminateWindowsDesktop = terminateWindowsDesktop;
9
+ exports.terminateOwnedProcess = terminateOwnedProcess;
10
+ exports.findChatgptExecutable = findChatgptExecutable;
11
+ exports.waitFor = waitFor;
12
+ const node_child_process_1 = require("node:child_process");
13
+ const promises_1 = require("node:fs/promises");
14
+ const node_fs_1 = require("node:fs");
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);
18
+ const versionDirectory = /^(?:app[-_ ]?)?v?\d+(?:[.-]\d+)+$/i;
19
+ /** 在 Windows 常见安装目录中查找桌面程序,避免依赖单一盘符。 */
20
+ async function findWindowsExecutable(fileName, configured) {
21
+ if (configured?.trim()) {
22
+ const configuredPath = (0, node_path_1.resolve)(configured);
23
+ if (fileName.toLowerCase() === "doubao.exe") {
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)))
26
+ root = (0, node_path_1.dirname)(root);
27
+ const latest = await findExecutableInTree(root, fileName, /^/, 3);
28
+ if (latest)
29
+ return latest;
30
+ }
31
+ // 千问升级会保留或删除旧版本目录,从同一安装位置重新定位新版。
32
+ if (fileName.toLowerCase() === "qwenworkcn.exe" && /^v?\d+(?:[.-]\d+)+$/i.test((0, node_path_1.basename)((0, node_path_1.dirname)(configuredPath)))) {
33
+ const latest = await findExecutableInTree((0, node_path_1.dirname)((0, node_path_1.dirname)(configuredPath)), fileName, /^/, 1);
34
+ if (latest)
35
+ return latest;
36
+ }
37
+ if ((0, node_fs_1.existsSync)(configuredPath))
38
+ return configuredPath;
39
+ }
40
+ if (process.platform !== "win32")
41
+ return undefined;
42
+ const pattern = /doubao|豆包|qwen|qwwork|千问|deepseek|harness|dsh/i;
43
+ const roots = [...new Set([
44
+ process.env.DSH_INSTALL_DIR,
45
+ "D:\\Programs\\DeepSeek Harness",
46
+ "D:\\DSH",
47
+ process.env.LOCALAPPDATA && (0, node_path_1.join)(process.env.LOCALAPPDATA, "Programs"),
48
+ process.env.ProgramFiles,
49
+ process.env["ProgramFiles(x86)"],
50
+ ...Array.from({ length: 24 }, (_, index) => `${String.fromCharCode(67 + index)}:\\`),
51
+ ].filter((value) => typeof value === "string" && (0, node_fs_1.existsSync)(value)))];
52
+ for (const root of roots) {
53
+ const found = await findExecutableInTree(root, fileName, pattern, 6);
54
+ if (found)
55
+ return found;
56
+ }
57
+ return undefined;
58
+ }
59
+ async function findExecutableInTree(directory, fileName, pattern, depth) {
60
+ return search(directory, fileName, pattern, depth, pattern.test(directory));
61
+ }
62
+ async function search(directory, fileName, pattern, depth, insideProduct) {
63
+ try {
64
+ const entries = await (0, promises_1.readdir)(directory, { withFileTypes: true });
65
+ // 豆包上层文件是启动器,优先查找新版目录或 app 中的实际程序。
66
+ 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 }));
69
+ for (const entry of versions) {
70
+ const found = await search((0, node_path_1.join)(directory, entry.name), fileName, pattern, depth - 1, true);
71
+ if (found)
72
+ return found;
73
+ }
74
+ }
75
+ for (const entry of entries)
76
+ if (entry.isFile() && insideProduct && entry.name.toLowerCase() === fileName.toLowerCase())
77
+ return (0, node_path_1.join)(directory, entry.name);
78
+ if (depth <= 0)
79
+ return undefined;
80
+ // 按数字比较版本目录,避免 1.2.5 排在 1.2.10 之前。
81
+ entries.sort((left, right) => right.name.localeCompare(left.name, "en", { numeric: true }));
82
+ for (const entry of entries)
83
+ if (entry.isDirectory() && !entry.name.startsWith(".") && (insideProduct || pattern.test(entry.name) || /^(?:Program Files(?: \(x86\))?|Programs)$/i.test(entry.name))) {
84
+ const found = await search((0, node_path_1.join)(directory, entry.name), fileName, pattern, depth - 1, insideProduct || pattern.test(entry.name));
85
+ if (found)
86
+ return found;
87
+ }
88
+ }
89
+ catch { /* 无权限或目录在扫描期间消失时继续查找 */ }
90
+ return undefined;
91
+ }
92
+ async function launchAndWait(executable, args, connect, timeoutMs, launch = (file, argv) => (0, node_child_process_1.spawn)(file, argv, { detached: true, stdio: "ignore", windowsHide: false })) {
93
+ const child = launch(executable, args);
94
+ child.unref();
95
+ return waitFor(connect, timeoutMs, 250);
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
+ }
178
+ async function waitFor(action, timeoutMs, intervalMs = 500) {
179
+ const deadline = Date.now() + timeoutMs;
180
+ let last;
181
+ while (Date.now() < deadline) {
182
+ try {
183
+ return await action();
184
+ }
185
+ catch (error) {
186
+ last = error;
187
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
188
+ }
189
+ }
190
+ throw last instanceof Error ? last : new Error("桌面程序启动超时");
191
+ }
@@ -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
  }