mu-agent-bridge 1.0.40 → 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.
Files changed (50) hide show
  1. package/README.md +16 -0
  2. package/dist/src-ts/agent-availability.js +2 -1
  3. package/dist/src-ts/cli-commands.js +6 -2
  4. package/dist/src-ts/control-server.js +6 -0
  5. package/dist/src-ts/daemon-process.js +3 -2
  6. package/dist/src-ts/plugin-manager.js +17 -1
  7. package/dist/src-ts/plugins/aun/index.js +1 -1
  8. package/dist/src-ts/plugins/chatgpt/cdp-client.js +3 -2
  9. package/dist/src-ts/plugins/chatgpt/index.js +42 -6
  10. package/dist/src-ts/plugins/desktop-launch.js +93 -2
  11. package/dist/src-ts/plugins/doubao/index.js +14 -12
  12. package/dist/src-ts/plugins/feishu/index.js +21 -8
  13. package/dist/src-ts/plugins/feishu/public/app.js +20 -8
  14. package/dist/src-ts/plugins/feishu/public/index.html +1 -1
  15. package/dist/src-ts/plugins/workbuddy/cdp-client.js +3 -1
  16. package/dist/src-ts/plugins/workbuddy/index.js +30 -19
  17. package/dist/src-ts/secret-input.js +47 -0
  18. package/dist/src-ts/session-management.js +34 -13
  19. package/dist/tests-ts/agent-availability.test.js +15 -1
  20. package/dist/tests-ts/aun-plugin.test.js +5 -0
  21. package/dist/tests-ts/chatgpt-history.test.js +33 -0
  22. package/dist/tests-ts/cli-commands.test.js +58 -0
  23. package/dist/tests-ts/control-server.test.js +20 -0
  24. package/dist/tests-ts/daemon-process.test.js +52 -0
  25. package/dist/tests-ts/desktop-launch.test.js +98 -0
  26. package/dist/tests-ts/feishu-page.test.js +10 -3
  27. package/dist/tests-ts/feishu-plugin.test.js +24 -1
  28. package/dist/tests-ts/installer-assets.test.js +122 -0
  29. package/dist/tests-ts/plugin-manager.test.js +21 -0
  30. package/dist/tests-ts/session-management.test.js +38 -4
  31. package/dist/tests-ts/workbuddy-plugin.test.js +7 -0
  32. package/package.json +3 -1
  33. package/plugins/aun/dist/src-ts/plugins/aun/index.js +1 -1
  34. package/plugins/chatgpt/config.schema.json +1 -1
  35. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +3 -2
  36. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +42 -6
  37. package/plugins/chatgpt/dist/src-ts/plugins/desktop-launch.js +191 -0
  38. package/plugins/chatgpt/package.json +1 -1
  39. package/plugins/chatgpt/plugin.json +1 -14
  40. package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +93 -2
  41. package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +14 -12
  42. package/plugins/feishu/dist/src-ts/plugins/feishu/index.js +21 -8
  43. package/plugins/feishu/dist/src-ts/plugins/feishu/public/app.js +20 -8
  44. package/plugins/feishu/dist/src-ts/plugins/feishu/public/index.html +1 -1
  45. package/plugins/qwenwork/dist/src-ts/plugins/desktop-launch.js +93 -2
  46. package/plugins/workbuddy/dist/src-ts/plugins/desktop-launch.js +191 -0
  47. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +3 -1
  48. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +30 -19
  49. package/plugins/workbuddy/package.json +1 -1
  50. package/plugins/workbuddy/plugin.json +1 -1
package/README.md CHANGED
@@ -146,3 +146,19 @@ Windows 路径时,可设置 `DSH_INSTALL_DIR`。
146
146
  ## AUN 身份管理
147
147
 
148
148
  创建、连接、关联和 AUN 通信统一由独立 `agent-bridge-aun` 插件完成。安装并启动 AUN 后,在 ABWeb 的“通道 → AUN”创建或选择身份,再将已连接 AID 关联到已启动的 Agent;用 `mab status` 查看状态。Owner AID 仅作本地归属配置,不代表 AUN Owner 关系已激活。完整步骤见 [AUN 插件与 AID 身份管理](docs/aun-plugin-identities.md);给 Codex、Claude Code 直接执行的 CLI 说明见 [使用说明](使用说明.md)。
149
+
150
+ ### 飞书凭据与办公 Agent 关联
151
+
152
+ 在 ABWeb 的“飞书通道 → 手动配置凭据”中填写 App ID、App Secret,并选择“关联办公 Agent”。Secret 使用密码框显示;已保存的密钥保持掩码,不通过状态接口返回。WorkBuddy、千问办公、豆包根据运行后台的机器上是否存在可执行文件检测,文件缺失或插件未就绪的选项置灰。自定义安装位置请在对应插件配置中填写 `executablePath`。
153
+
154
+ 也可以使用 CLI,Secret 在命令执行后隐藏输入,不写在命令参数中:
155
+
156
+ ```bash
157
+ mab feishu configure cli_你的应用ID --agent workbuddy
158
+ mab feishu agents
159
+ mab feishu list
160
+ mab feishu bind cli_你的应用ID workbuddy
161
+ mab feishu unbind cli_你的应用ID
162
+ ```
163
+
164
+ `configure` 保存凭据及可选关联后,需要在 ABWeb 重载飞书插件以连接机器人。非交互终端请使用网页密码框;不要把 Secret 写进命令行、脚本或仓库。
@@ -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" ? "" : "插件未就绪" };
@@ -5,6 +5,7 @@ exports.resolveLocalControlHost = resolveLocalControlHost;
5
5
  exports.controlUrl = controlUrl;
6
6
  exports.runPluginCommand = runPluginCommand;
7
7
  exports.runCoreHelp = runCoreHelp;
8
+ const secret_input_1 = require("./secret-input");
8
9
  const node_http_1 = require("node:http");
9
10
  const MAX_BODY_BYTES = 1024 * 1024;
10
11
  const PLUGIN_ID = /^[a-z][a-z0-9-]{1,63}$/;
@@ -108,6 +109,7 @@ function commandManifest(value, plugin) {
108
109
  || !Array.isArray(value.commands) || value.commands.some((command) => !object(command)
109
110
  || typeof command.name !== "string" || !/^[a-z][a-z0-9-]*$/.test(command.name)
110
111
  || typeof command.summary !== "string" || !command.summary.trim() || typeof command.usage !== "string" || !command.usage.trim()
112
+ || (command.secretPrompt !== undefined && (typeof command.secretPrompt !== "string" || !command.secretPrompt.trim()))
111
113
  || (command.examples !== undefined && (!Array.isArray(command.examples) || command.examples.some((example) => typeof example !== "string"))))) {
112
114
  throw new CliError("插件命令清单协议无效(需要 protocolVersion: 1)");
113
115
  }
@@ -126,7 +128,7 @@ async function runPluginCommand(id, args, port, options = {}) {
126
128
  throw new CliError(`插件“${id}”未安装;请先到 ABWeb 安装插件`, 2);
127
129
  if (!plugin.capabilities.includes("cli"))
128
130
  throw new CliError(`插件“${plugin.displayName}”未提供 CLI 命令;请升级对应插件`, 2);
129
- if (plugin.status !== "ready")
131
+ if (!["ready", "degraded"].includes(plugin.status))
130
132
  throw new CliError(`插件“${plugin.displayName}”未就绪(${plugin.status})`, 2);
131
133
  const path = `/api/plugins/${id}/commands`;
132
134
  const response = await requestJson(port, path, options);
@@ -146,7 +148,9 @@ async function runPluginCommand(id, args, port, options = {}) {
146
148
  ])].join("\n");
147
149
  return output({ ok: true, message, data: { ...manifest, commands: selected } }, 0, json);
148
150
  }
149
- const executed = await requestJson(port, path, options, { protocolVersion: 1, argv });
151
+ const prompt = manifest.commands.find((command) => command.name === argv[0])?.secretPrompt;
152
+ const secret = prompt ? await (options.readSecret ?? secret_input_1.readSecret)(prompt) : undefined;
153
+ const executed = await requestJson(port, path, options, { protocolVersion: 1, argv, ...(secret === undefined ? {} : { secret }) });
150
154
  const result = executed.value;
151
155
  if (!object(result) || typeof result.ok !== "boolean" || typeof result.message !== "string"
152
156
  || (executed.status >= 200 && executed.status < 300 ? !result.ok : result.ok)
@@ -251,6 +251,8 @@ class ControlServer {
251
251
  const result = await identity.create(body.aid, body.ownerAid);
252
252
  return this.json(res, result.status === "等待 Owner 确认" ? 202 : result.status === "existing" ? 200 : 201, result);
253
253
  }
254
+ if (method === "GET" && url.pathname === "/api/agents/availability")
255
+ return this.json(res, 200, { agents: await this.options.manager.availableAgents() });
254
256
  if (method === "GET" && url.pathname === "/api/plugins")
255
257
  return this.json(res, 200, { ok: true, plugins: this.options.manager.list() });
256
258
  if (method === "GET" && url.pathname === "/api/plugins/catalog")
@@ -271,6 +273,10 @@ class ControlServer {
271
273
  await this.options.manager.reload(id);
272
274
  return this.json(res, 200, { ok: true });
273
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
+ }
274
280
  if (method === "POST" && parts[3] === "update") {
275
281
  const body = await this.body(req);
276
282
  return this.json(res, 200, { ok: true, plugin: await this.options.manager.upgradePackage(id, body.packageName, body.version) });
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.isDaemonCommand = isDaemonCommand;
4
4
  /** 只识别直接运行本安装 CLI 的后台进程,避免旧地址文件中的 PID 被其他程序复用。 */
5
5
  function isDaemonCommand(commandLine, cli) {
6
- const args = [...commandLine.matchAll(/"([^"]*)"|(\S+)/g)].map((match) => (match[1] ?? match[2]).replaceAll("\\", "/").toLowerCase());
7
- return args[1] === cli.replaceAll("\\", "/").toLowerCase() && ["start", "--serve"].includes(args[2]);
6
+ const normalize = (value) => value.replace(/[\\/]+/g, "/").toLowerCase();
7
+ const args = [...commandLine.matchAll(/"([^"]*)"|(\S+)/g)].map((match) => normalize(match[1] ?? match[2]));
8
+ return args[1] === normalize(cli) && ["start", "--serve"].includes(args[2]);
8
9
  }
@@ -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 {
@@ -385,7 +387,7 @@ class PluginManager {
385
387
  if (trustedChannel && (typeof message.peerId !== "string" || !message.peerId.trim() || [message.peerId, message.recipientId, message.chatId, message.threadId].some(value => value !== undefined && (typeof value !== "string" || value.length > 512 || /[\u0000-\u001f*]/.test(value)))))
386
388
  throw new agent_1.AgentError("通道来源标识无效");
387
389
  const target = message.agentPlugin || "qwenwork";
388
- if (/^\/sessions(?:\s|$)/.test(message.text.trimStart())) {
390
+ if (/^\/sessions(?:\s|$)/.test(message.text.trimStart()) || (0, session_management_1.isDailySummaryRequest)(message.text)) {
389
391
  if (!trustedChannel)
390
392
  throw new agent_1.AgentError("当前通道不支持会话管理");
391
393
  const current = await this.sessions.current(message, target);
@@ -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)
@@ -671,7 +671,7 @@ class AunPlugin {
671
671
  method,
672
672
  headers: { accept: "application/json", ...(body === undefined ? {} : { "content-type": "application/json" }) },
673
673
  ...(body === undefined ? {} : { body: JSON.stringify(body) }),
674
- signal: AbortSignal.timeout(5000),
674
+ signal: AbortSignal.timeout(method === "PATCH" ? 120_000 : 5000),
675
675
  redirect: "error",
676
676
  });
677
677
  }
@@ -111,7 +111,8 @@ class ChatgptCdpClient {
111
111
  if (selected?.getAttribute('data-app-action-sidebar-thread-id') !== ${JSON.stringify(session.id)}
112
112
  || selected?.getAttribute('data-app-action-sidebar-thread-host-id') !== ${JSON.stringify(session.host)}) return { supported: false, reason: '读取期间活动会话身份发生变化' };
113
113
  if (document.querySelector(${JSON.stringify(stopButtonSelector)})) return { supported: false, reason: 'ChatGPT 正在生成回复,暂不读取历史' };
114
- const turns = [...document.querySelectorAll(${JSON.stringify(exports.turnSelector)})];
114
+ const legacyTurns = [...document.querySelectorAll(${JSON.stringify(exports.turnSelector)})];
115
+ const turns = legacyTurns.length ? legacyTurns : [...document.querySelectorAll('[data-content-search-turn-key]')];
115
116
  const output = [];
116
117
  const missingReasons = [];
117
118
  let chars = 0;
@@ -121,7 +122,7 @@ class ChatgptCdpClient {
121
122
  output.push({ role, text });
122
123
  };
123
124
  for (const turn of turns) {
124
- const turnId = turn.getAttribute(${JSON.stringify(turnIdAttribute)});
125
+ const turnId = turn.getAttribute(${JSON.stringify(turnIdAttribute)}) || turn.getAttribute('data-content-search-turn-key');
125
126
  if (!turnId) {
126
127
  missingReasons.push('部分消息缺少可验证的 turn 身份');
127
128
  continue;
@@ -4,9 +4,12 @@ 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
- version: "1.0.9",
12
+ version: "1.0.10",
10
13
  displayName: "ChatGPT(Codex)",
11
14
  type: "agent",
12
15
  entry: "dist/index.js",
@@ -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
  }
@@ -11,6 +11,7 @@ const commands = [
11
11
  { name: "status", summary: "查看飞书通道状态", usage: "mab feishu status" },
12
12
  { name: "list", summary: "列出机器人连接状态和关联 Agent", usage: "mab feishu list" },
13
13
  { name: "agents", summary: "列出已启动的 Agent 插件", usage: "mab feishu agents" },
14
+ { name: "configure", summary: "保存机器人凭据并关联办公 Agent", usage: "mab feishu configure <app-id> [--agent <plugin-id>]", secretPrompt: "请输入飞书 App Secret" },
14
15
  { name: "bind", summary: "将已连接的机器人关联到 Agent", usage: "mab feishu bind <bot-id> <plugin-id>" },
15
16
  { name: "unbind", summary: "解除机器人关联并保留凭据", usage: "mab feishu unbind <bot-id>" },
16
17
  ];
@@ -149,8 +150,10 @@ class FeishuPlugin {
149
150
  }
150
151
  if (body.protocolVersion !== 1 || !Array.isArray(body.argv) || !body.argv.every((arg) => typeof arg === "string"))
151
152
  return this.json(response, 400, { ok: false, message: "命令协议版本或 argv 无效" });
152
- const [command, appId, pluginId] = body.argv;
153
- const expectedLength = command === "bind" ? 3 : command === "unbind" ? 2 : 1;
153
+ const [command, appId, pluginArg] = body.argv;
154
+ const pluginId = command === "configure" && body.argv[2] === "--agent" ? body.argv[3] : pluginArg;
155
+ const secret = typeof body.secret === "string" ? body.secret.trim() : "";
156
+ const expectedLength = command === "bind" ? 3 : command === "unbind" ? 2 : command === "configure" ? (body.argv.length === 4 ? 4 : 2) : 1;
154
157
  if (!commands.some((item) => item.name === command) || body.argv.length !== expectedLength || body.argv.some((arg) => !arg.trim()))
155
158
  return this.json(response, 400, { ok: false, message: "命令或参数无效,请查看 mab feishu help" });
156
159
  if (command === "status") {
@@ -161,34 +164,44 @@ class FeishuPlugin {
161
164
  const bots = this.bots.map((bot) => ({ app_id: bot.app_id, status: this.botStates.get(bot.app_id)?.status || "degraded", pluginId: this.bindings[bot.app_id] || "" }));
162
165
  return this.json(response, 200, { ok: true, message: bots.map((bot) => `${bot.app_id}:${bot.status},Agent:${bot.pluginId || "未关联"}`).join("\n") || "尚未配置飞书机器人", data: bots });
163
166
  }
164
- if (command !== "agents" && !this.bots.some((bot) => bot.app_id === appId))
167
+ if (command === "configure" && (!appId || !secret || body.argv.length === 4 && (body.argv[2] !== "--agent" || !pluginId)))
168
+ return this.json(response, 400, { ok: false, message: "用法:mab feishu configure <app-id> [--agent <plugin-id>],Secret 必须输入" });
169
+ if (command !== "configure" && command !== "agents" && !this.bots.some((bot) => bot.app_id === appId))
165
170
  return this.json(response, 400, { ok: false, message: "飞书机器人标识不存在" });
166
171
  if (command === "bind" && this.botStates.get(appId)?.status !== "ready")
167
172
  return this.json(response, 400, { ok: false, message: "飞书机器人当前未连接" });
168
- if (command === "agents" || command === "bind") {
173
+ if (command === "agents" || command === "bind" || command === "configure" && pluginId) {
169
174
  const runtime = this.context?.runtime;
170
175
  const controlUrl = runtime && "controlUrl" in runtime ? runtime.controlUrl : undefined;
171
176
  if (typeof controlUrl !== "string" || !controlUrl)
172
177
  return this.json(response, 503, { ok: false, message: "运行时未提供控制 API 地址,请升级核心并重启后台" });
173
178
  let agents;
174
179
  try {
175
- const result = await fetch(new URL("/api/plugins", controlUrl), { signal: AbortSignal.timeout(5000) });
180
+ const result = await fetch(new URL(command === "configure" ? "/api/agents/availability" : "/api/plugins", controlUrl), { signal: AbortSignal.timeout(5000) });
176
181
  if (!result.ok)
177
182
  throw new Error("控制 API 请求失败");
178
183
  const data = await result.json();
179
- if (!Array.isArray(data.plugins))
184
+ const entries = command === "configure" ? data.agents : data.plugins;
185
+ if (!Array.isArray(entries))
180
186
  throw new Error("控制 API 响应无效");
181
- agents = data.plugins.filter((item) => item && item.type === "agent" && item.status === "ready" && typeof item.id === "string").map((item) => ({ id: item.id }));
187
+ agents = entries.filter((item) => item && typeof item.id === "string" && (command === "configure" ? item.available === true : item.type === "agent" && item.status === "ready")).map((item) => ({ id: item.id }));
182
188
  }
183
189
  catch {
184
190
  return this.json(response, 502, { ok: false, message: "无法读取当前 Agent 插件,请检查后台控制 API" });
185
191
  }
186
192
  if (command === "agents")
187
193
  return this.json(response, 200, { ok: true, message: agents.map((agent) => agent.id).join("\n") || "没有已启动的 Agent 插件", data: agents });
188
- if (!agents.some((agent) => agent.id === pluginId))
194
+ if (pluginId && !agents.some((agent) => agent.id === pluginId))
189
195
  return this.json(response, 400, { ok: false, message: "目标 Agent 插件未启动或类型无效" });
190
196
  }
191
197
  try {
198
+ if (command === "configure") {
199
+ this.saveBot(appId, secret);
200
+ await this.persist();
201
+ if (pluginId)
202
+ await this.bindBot(appId, pluginId);
203
+ return this.json(response, 200, { ok: true, message: "飞书凭据已保存,请重载飞书插件以连接机器人", data: { app_id: appId, ...(pluginId ? { pluginId } : {}) } });
204
+ }
192
205
  await this.bindBot(appId, command === "bind" ? pluginId : "");
193
206
  this.json(response, 200, { ok: true, message: command === "bind" ? "飞书机器人已关联 Agent" : "飞书机器人已解除 Agent 关联", data: { app_id: appId, ...(command === "bind" ? { pluginId } : {}) } });
194
207
  }
@@ -29,9 +29,9 @@
29
29
  if (loading) return;
30
30
  loading = true;
31
31
  try {
32
- const [state, plugins] = await Promise.all([request("/api/plugins/feishu/setup/status"), request("/api/plugins")]);
33
- const agents = (plugins.plugins || []).filter((plugin) => plugin.type === "agent" && plugin.status === "ready");
34
- const options = agents.map((agent) => new Option(agent.displayName || agent.id, agent.id));
32
+ const [state, availability] = await Promise.all([request("/api/plugins/feishu/setup/status"), request("/api/agents/availability")]);
33
+ const agents = availability.agents || [];
34
+ const agentOptions = () => agents.map((agent) => { const option = new Option(`${agent.displayName || agent.id}(${agent.available ? "可用" : agent.reason})`, agent.id); option.disabled = !agent.available; return option; });
35
35
  bots = state.bots || [];
36
36
  botTable.replaceChildren();
37
37
  for (const bot of [...bots].sort((a, b) => Number(Boolean(b.pluginId)) - Number(Boolean(a.pluginId)) || Number(b.status === "ready") - Number(a.status === "ready") || (Date.parse(b.binding_time) || 0) - (Date.parse(a.binding_time) || 0))) {
@@ -39,18 +39,20 @@
39
39
  for (const text of [bot.app_id, bot.status === "ready" ? "已连接" : bot.message || "未连接", bot.pluginId || "未关联", bot.binding_time ? new Date(bot.binding_time).toLocaleString() : "—"]) { const cell = document.createElement("td"); cell.textContent = text; row.append(cell); }
40
40
  const controls = document.createElement("td");
41
41
  const select = document.createElement("select"); select.setAttribute("aria-label", `${bot.app_id} 关联 Agent`);
42
- select.append(new Option("选择 Agent", ""), ...agents.map((agent) => new Option(agent.displayName || agent.id, agent.id))); select.value = bot.pluginId || "";
42
+ select.append(new Option("选择 Agent", ""), ...agentOptions()); select.value = bot.pluginId || "";
43
43
  function button(text, action) { const button = document.createElement("button"); button.type = "button"; button.textContent = text; button.onclick = async () => { button.disabled = true; try { await action(); } catch (error) { show(String(error), true); } finally { button.disabled = false; } }; return button; }
44
- async function bind(pluginId) { await request("/api/plugins/feishu/bind", "POST", { app_id: bot.app_id, pluginId }); await load(false); show(`${bot.app_id} ${pluginId ? "已关联 " + pluginId : "已解除关联"}`); }
45
- controls.append(select, button("关联", () => bind(select.value)), button("解除", () => bind("")), button("编辑凭据", async () => { credentials.elements.app_id.value = bot.app_id; savedAppId = bot.app_id; secretMasked = true; credentials.elements.app_secret.value = "********"; credentials.elements.app_secret.required = false; }));
44
+ async function bind(pluginId) { if (pluginId && !agents.find((agent) => agent.id === pluginId)?.available) throw new Error("目标办公 Agent 不可用"); await request("/api/plugins/feishu/bind", "POST", { app_id: bot.app_id, pluginId }); await load(false); show(`${bot.app_id} ${pluginId ? "已关联 " + pluginId : "已解除关联"}`); }
45
+ controls.append(select, button("关联", () => bind(select.value)), button("解除", () => bind("")), button("编辑凭据", async () => { credentials.elements.app_id.value = bot.app_id; credentials.elements.agent_plugin.value = bot.pluginId || ""; savedAppId = bot.app_id; secretMasked = true; credentials.elements.app_secret.value = "********"; credentials.elements.app_secret.required = false; }));
46
46
  row.append(controls); botTable.append(row);
47
47
  }
48
48
  if (refreshForm) {
49
- settings.elements.agent_plugin.replaceChildren(new Option("默认 Agent", ""), ...options);
49
+ credentials.elements.agent_plugin.replaceChildren(new Option("保留原有关联", ""), ...agentOptions());
50
+ settings.elements.agent_plugin.replaceChildren(new Option("默认 Agent", ""), ...agentOptions());
50
51
  settings.elements.agent_plugin.value = state.agent_plugin || "";
51
52
  settings.elements.allow_from.value = state.allow_from || "";
52
53
  settings.elements.allow_chats.value = state.allow_chats || "";
53
54
  credentials.elements.app_id.value = state.app_id || "";
55
+ credentials.elements.agent_plugin.value = state.bindings?.[credentials.elements.app_id.value] || "";
54
56
  savedAppId = state.configured ? state.app_id : "";
55
57
  secretMasked = state.configured;
56
58
  credentials.elements.app_secret.value = secretMasked ? "********" : "";
@@ -120,14 +122,24 @@
120
122
  credentials.elements.app_id.oninput = () => {
121
123
 
122
124
  const changed = credentials.elements.app_id.value.trim() !== savedAppId;
125
+ if (changed) credentials.elements.agent_plugin.value = "";
123
126
  if (changed && secretMasked) { credentials.elements.app_secret.value = ""; secretMasked = false; }
124
127
  credentials.elements.app_secret.required = changed;
125
128
  };
126
129
  credentials.elements.app_secret.oninput = () => { secretMasked = false; };
127
130
  credentials.elements.app_secret.addEventListener("invalid", () => show("首次配置或更换机器人时,App ID 和 App Secret 都需要填写", true));
131
+ function selectedAgentUnavailable() { return credentials.elements.agent_plugin.selectedOptions?.[0]?.disabled || false; }
128
132
  credentials.onsubmit = async (event) => {
129
133
  event.preventDefault(); const button = credentials.querySelector("button"); button.disabled = true; show("正在保存凭据并连接飞书");
130
- try { await request("/api/plugins/feishu/bot", "POST", { app_id: credentials.elements.app_id.value.trim(), app_secret: secretMasked ? "" : credentials.elements.app_secret.value }); savedAppId = credentials.elements.app_id.value.trim(); secretMasked = true; credentials.elements.app_secret.value = "********"; await reconnect(); show("凭据已保存,机器人已连接"); }
134
+ try {
135
+ const agentPlugin = credentials.elements.agent_plugin.value;
136
+ if (agentPlugin && selectedAgentUnavailable()) throw new Error("目标办公 Agent 不可用");
137
+ await request("/api/plugins/feishu/bot", "POST", { app_id: credentials.elements.app_id.value.trim(), app_secret: secretMasked ? "" : credentials.elements.app_secret.value });
138
+ savedAppId = credentials.elements.app_id.value.trim();
139
+ secretMasked = true; credentials.elements.app_secret.value = "********";
140
+ if (agentPlugin) await request("/api/plugins/feishu/bind", "POST", { app_id: savedAppId, pluginId: agentPlugin });
141
+ await reconnect(); show("凭据已保存,机器人已连接");
142
+ }
131
143
  catch (error) { show(String(error), true); }
132
144
  finally { button.disabled = false; }
133
145
  };
@@ -8,7 +8,7 @@
8
8
  <dl><div><dt>连接状态</dt><dd id="connection">未配置</dd></div><div><dt>App ID</dt><dd id="app-id">未配置</dd></div><div><dt>关联 Agent</dt><dd id="bound-agent">未关联</dd></div></dl>
9
9
  <section><h2>机器人关联</h2><div class="table-wrap"><table><thead><tr><th>App ID</th><th>连接状态</th><th>关联 Agent</th><th>最近关联时间</th><th>操作</th></tr></thead><tbody id="bots"></tbody></table></div></section>
10
10
  <section><h2>创建设备授权</h2><div class="actions"><button id="begin" type="button">创建飞书机器人</button><a id="authorization-link" target="_blank" rel="noopener noreferrer" hidden>打开飞书创建链接</a></div><div id="authorization-details" hidden><img id="authorization-qr" width="256" height="256" alt="飞书机器人创建授权二维码"><p id="authorization-url"></p></div><p id="authorization-state" role="status"></p></section>
11
- <form id="credentials"><h2>手动配置凭据</h2><div class="fields"><label>App ID<input name="app_id" required autocomplete="off"></label><label>App Secret<input name="app_secret" type="password" required autocomplete="new-password"></label></div><button type="submit">保存并连接</button></form>
11
+ <form id="credentials"><h2>手动配置凭据</h2><div class="fields"><label>App ID<input name="app_id" required autocomplete="off"></label><label>App Secret<input name="app_secret" type="password" required autocomplete="new-password"></label><label>关联办公 Agent<select name="agent_plugin"></select></label></div><button type="submit">保存并连接</button></form>
12
12
 
13
13
  <form id="settings"><h2>通道配置</h2><div class="fields"><label>默认 Agent<select name="agent_plugin"></select></label><label>允许的 open_id<input name="allow_from"></label><label>允许的 chat_id<input name="allow_chats"></label></div><button type="submit">保存配置</button></form>
14
14
  </main>