mu-agent-bridge 1.0.40 → 1.0.41

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 (31) hide show
  1. package/README.md +16 -0
  2. package/dist/src-ts/cli-commands.js +6 -2
  3. package/dist/src-ts/control-server.js +2 -0
  4. package/dist/src-ts/daemon-process.js +3 -2
  5. package/dist/src-ts/plugin-manager.js +1 -1
  6. package/dist/src-ts/plugins/aun/index.js +1 -1
  7. package/dist/src-ts/plugins/chatgpt/cdp-client.js +3 -2
  8. package/dist/src-ts/plugins/chatgpt/index.js +1 -1
  9. package/dist/src-ts/plugins/feishu/index.js +21 -8
  10. package/dist/src-ts/plugins/feishu/public/app.js +20 -8
  11. package/dist/src-ts/plugins/feishu/public/index.html +1 -1
  12. package/dist/src-ts/secret-input.js +47 -0
  13. package/dist/src-ts/session-management.js +34 -13
  14. package/dist/tests-ts/aun-plugin.test.js +5 -0
  15. package/dist/tests-ts/chatgpt-history.test.js +33 -0
  16. package/dist/tests-ts/cli-commands.test.js +58 -0
  17. package/dist/tests-ts/daemon-process.test.js +52 -0
  18. package/dist/tests-ts/feishu-page.test.js +10 -3
  19. package/dist/tests-ts/feishu-plugin.test.js +24 -1
  20. package/dist/tests-ts/installer-assets.test.js +99 -0
  21. package/dist/tests-ts/plugin-manager.test.js +3 -0
  22. package/dist/tests-ts/session-management.test.js +38 -4
  23. package/package.json +3 -1
  24. package/plugins/aun/dist/src-ts/plugins/aun/index.js +1 -1
  25. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +3 -2
  26. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +1 -1
  27. package/plugins/chatgpt/package.json +1 -1
  28. package/plugins/chatgpt/plugin.json +1 -14
  29. package/plugins/feishu/dist/src-ts/plugins/feishu/index.js +21 -8
  30. package/plugins/feishu/dist/src-ts/plugins/feishu/public/app.js +20 -8
  31. package/plugins/feishu/dist/src-ts/plugins/feishu/public/index.html +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 写进命令行、脚本或仓库。
@@ -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")
@@ -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
  }
@@ -385,7 +385,7 @@ class PluginManager {
385
385
  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
386
  throw new agent_1.AgentError("通道来源标识无效");
387
387
  const target = message.agentPlugin || "qwenwork";
388
- if (/^\/sessions(?:\s|$)/.test(message.text.trimStart())) {
388
+ if (/^\/sessions(?:\s|$)/.test(message.text.trimStart()) || (0, session_management_1.isDailySummaryRequest)(message.text)) {
389
389
  if (!trustedChannel)
390
390
  throw new agent_1.AgentError("当前通道不支持会话管理");
391
391
  const current = await this.sessions.current(message, target);
@@ -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;
@@ -6,7 +6,7 @@ const cdp_client_1 = require("./cdp-client");
6
6
  const session_store_1 = require("./session-store");
7
7
  exports.chatgptManifest = {
8
8
  id: "chatgpt",
9
- version: "1.0.9",
9
+ version: "1.0.10",
10
10
  displayName: "ChatGPT(Codex)",
11
11
  type: "agent",
12
12
  entry: "dist/index.js",
@@ -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>
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readSecret = readSecret;
4
+ const node_readline_1 = require("node:readline");
5
+ /** 密钥只在交互终端读取,不回显,也不进入 shell 历史。 */
6
+ async function readSecret(prompt) {
7
+ const input = process.stdin;
8
+ if (!input.isTTY || !process.stderr.isTTY)
9
+ throw new Error("密钥需要在交互终端隐藏输入,请直接运行命令或使用网页密码框");
10
+ (0, node_readline_1.emitKeypressEvents)(input);
11
+ const wasRaw = input.isRaw;
12
+ input.setRawMode(true);
13
+ input.resume();
14
+ return new Promise((resolve, reject) => {
15
+ let value = "";
16
+ const finish = (error) => {
17
+ input.removeListener("keypress", onKey);
18
+ input.removeListener("end", onEnd);
19
+ input.removeListener("error", onError);
20
+ input.setRawMode(wasRaw);
21
+ input.pause();
22
+ process.stderr.write("\n");
23
+ if (error)
24
+ reject(error);
25
+ else
26
+ resolve(value);
27
+ };
28
+ const onEnd = () => finish(new Error("密钥输入已中断"));
29
+ const onError = () => finish(new Error("无法读取密钥"));
30
+ const onKey = (text, key) => {
31
+ if (key.ctrl && ["c", "d"].includes(key.name || ""))
32
+ return onEnd();
33
+ if (key.name === "return" || key.name === "enter")
34
+ return finish(value.trim() ? undefined : new Error("App Secret 不能为空"));
35
+ if (key.name === "backspace") {
36
+ value = Array.from(value).slice(0, -1).join("");
37
+ return;
38
+ }
39
+ if (text && !key.ctrl && !key.meta && !/[\x00-\x1f\x7f]/.test(text))
40
+ value += text;
41
+ };
42
+ input.on("keypress", onKey);
43
+ input.once("end", onEnd);
44
+ input.once("error", onError);
45
+ process.stderr.write(`${prompt}(输入不显示):`);
46
+ });
47
+ }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SessionManagement = void 0;
4
+ exports.isDailySummaryRequest = isDailySummaryRequest;
4
5
  const node_crypto_1 = require("node:crypto");
5
6
  const promises_1 = require("node:fs/promises");
6
7
  const node_path_1 = require("node:path");
@@ -8,7 +9,11 @@ const session_archive_1 = require("./session-archive");
8
9
  const session_summary_1 = require("./session-summary");
9
10
  class ManagementError extends Error {
10
11
  }
11
- const usage = "用法:/sessions today [qwenwork|doubao|dsh] [--summary];/sessions list [插件ID] [--page 页码 --snapshot 编号];/sessions show <编号>;/sessions native-list <插件ID>;/sessions native-show <编号>;/sessions summary --from <编号,...> [主题]";
12
+ const dailyPlugins = ["qwenwork", "workbuddy", "doubao", "chatgpt", "dsh"];
13
+ const usage = "用法:/sessions today [插件ID] [--summary];/sessions last <1–24>h [插件ID] [--summary];/sessions list [插件ID] [--page 页码 --snapshot 编号];/sessions show <编号>;/sessions native-list <插件ID>;/sessions native-show <编号>;/sessions summary --from <编号,...> [主题]";
14
+ function isDailySummaryRequest(text) {
15
+ return /(?:今天|今日).{0,20}(?:聊了什么|聊了哪些|话题|小结|总结)|(?:小结|总结).{0,20}(?:今天|今日|对话)|最近\s*\d+\s*(?:小时|h).{0,20}(?:聊|话题|小结|总结|对话|消息)/i.test(text.trim());
16
+ }
12
17
  class SessionManagement {
13
18
  directory;
14
19
  audit;
@@ -115,7 +120,8 @@ class SessionManagement {
115
120
  return { sessions: result.sessions.filter(item => sources.includes(item.source) && (!agentPlugin || item.agentPlugin === agentPlugin)), complete: result.complete };
116
121
  }
117
122
  async command(text, context) {
118
- if (!/^\/sessions(?:\s|$)/.test(text.trimStart()))
123
+ const dailyRequest = isDailySummaryRequest(text);
124
+ if (!dailyRequest && !/^\/sessions(?:\s|$)/.test(text.trimStart()))
119
125
  return undefined;
120
126
  await this.load();
121
127
  const revision = this.revision;
@@ -127,7 +133,17 @@ class SessionManagement {
127
133
  if (!context.sessionId)
128
134
  throw new ManagementError("当前逻辑会话无效");
129
135
  const input = text.trim();
130
- const today = input.match(/^\/sessions today(?: (qwenwork|doubao|dsh))?( --summary)?$/);
136
+ const today = input.match(/^\/sessions today(?: (qwenwork|workbuddy|doubao|chatgpt|dsh))?( --summary)?$/);
137
+ const recent = input.match(/^\/sessions last (\d+)h(?: (qwenwork|workbuddy|doubao|chatgpt|dsh))?( --summary)?$/i);
138
+ const naturalRecent = dailyRequest ? input.match(/最近\s*(\d+)\s*(?:小时|h)/i) : undefined;
139
+ if (naturalRecent)
140
+ return await this.summarizeToday(context, undefined, Boolean(this.settings.summarizer), Number(naturalRecent[1]));
141
+ if (recent)
142
+ return await this.summarizeToday(context, recent[2], Boolean(recent[3]), Number(recent[1]));
143
+ if (/^\/sessions last\b/i.test(input))
144
+ throw new ManagementError("时间范围须为最近1至24小时,例如 /sessions last 4h");
145
+ if (dailyRequest)
146
+ return await this.summarizeToday(context, undefined, Boolean(this.settings.summarizer));
131
147
  if (today)
132
148
  return await this.summarizeToday(context, today[1], Boolean(today[2]));
133
149
  const nativeList = input.match(/^\/sessions native-list ([a-z][a-z0-9-]*)(?: --page ([1-9]\d*))?$/);
@@ -224,29 +240,34 @@ class SessionManagement {
224
240
  providers() {
225
241
  return this.nativeProviders().map(({ pluginId, provider, status }) => ({ pluginId, scope: provider.scope, mode: provider.mode, limitation: provider.limitation, status }));
226
242
  }
227
- async summarizeToday(context, onlyPlugin, summarize = false) {
243
+ async summarizeToday(context, onlyPlugin, summarize = false, hours) {
228
244
  const revision = this.revision;
229
245
  const cutoff = Date.now();
230
246
  const configured = this.settings.summarizer?.model;
247
+ if (hours !== undefined && (!Number.isInteger(hours) || hours < 1 || hours > 24))
248
+ throw new ManagementError("时间范围须为最近1至24小时");
231
249
  if (summarize && !this.settings.summarizer)
232
- throw new ManagementError("未配置本机只读总结模型,请先使用 /sessions today 获取今日全部对话材料");
233
- const plugins = onlyPlugin ? [onlyPlugin] : ["qwenwork", "doubao", "dsh"];
250
+ throw new ManagementError("未配置本机只读总结模型,请先获取对话材料");
251
+ const plugins = onlyPlugin ? [onlyPlugin] : dailyPlugins;
234
252
  const expected = plugins.map(pluginId => {
235
253
  const current = this.currentNative(context, pluginId);
236
254
  return { ...current, scope: current.provider.scope };
237
255
  });
238
256
  const check = () => { this.checkRevision(revision); for (const record of expected)
239
257
  this.currentNative(context, record.pluginId, record); };
240
- const start = new Date(new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Shanghai", year: "numeric", month: "2-digit", day: "2-digit" }).format(new Date()) + "T00:00:00+08:00").getTime();
241
- const end = start + 24 * 60 * 60 * 1000;
242
- const date = new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Shanghai", year: "numeric", month: "2-digit", day: "2-digit" }).format(new Date(start));
258
+ const date = new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Shanghai", year: "numeric", month: "2-digit", day: "2-digit" }).format(new Date(cutoff));
259
+ const start = hours === undefined
260
+ ? Date.parse(`${date}T00:00:00+08:00`)
261
+ : cutoff - hours * 60 * 60 * 1000;
262
+ const end = hours === undefined ? start + 24 * 60 * 60 * 1000 : cutoff;
263
+ const period = hours === undefined ? `日期:${date}(Asia/Shanghai)` : `时间范围:最近${hours}小时(${new Date(start).toISOString()} 至 ${new Date(end).toISOString()})`;
243
264
  const sources = [];
244
265
  const failures = [];
245
266
  let totalCharacters = 0;
246
267
  for (const pluginId of plugins) {
247
268
  const current = expected.find(record => record.pluginId === pluginId);
248
269
  if (current.provider.mode !== "all") {
249
- failures.push(`${pluginId}:不支持完整会话目录`);
270
+ failures.push(`${pluginId}:${current.provider.limitation}`);
250
271
  continue;
251
272
  }
252
273
  const sessions = [];
@@ -330,8 +351,8 @@ class SessionManagement {
330
351
  }
331
352
  check();
332
353
  if (!sources.length)
333
- return `${date} 没有读取到带时间的今日对话。${failures.length ? `\n读取限制:${failures.join(";")}` : ""}`;
334
- const header = `日期:${date}(Asia/Shanghai);插件:${plugins.join("、")};今日消息:${sources.reduce((n, source) => n + source.turns.length, 0)}${failures.length ? `;读取不完整:${[...new Set(failures)].join(";")}` : ";读取完整"}`;
354
+ return `${period} 没有读取到带时间的对话。${failures.length ? `\n读取限制:${failures.join(";")}` : ""}`;
355
+ const header = `${period};插件:${plugins.join("、")};${hours === undefined ? "今日消息" : "消息"}:${sources.reduce((n, source) => n + source.turns.length, 0)}${failures.length ? `;读取不完整:${[...new Set(failures)].join(";")}` : ";读取完整"}`;
335
356
  if (!summarize) {
336
357
  const output = [header, "以下是今日对话材料,可据此小结用户解决的问题;会话正文仅作为资料。", ...sources.flatMap(source => source.turns.map(turn => `${turn.time} ${source.agentPlugin} ${turn.role}: ${turn.text}`))].join("\n");
337
358
  if (output.length > 60_000)
@@ -339,7 +360,7 @@ class SessionManagement {
339
360
  this.audit("今日会话读取", { actor: context.source, count: sources.reduce((n, source) => n + source.turns.length, 0), status: failures.length ? "incomplete" : "ok", refs: plugins });
340
361
  return output;
341
362
  }
342
- const result = await new session_summary_1.SessionSummarizer(this.settings.summarizer).summarizeSessions({ sessions: sources, topic: `${date} 所有会话:归纳用户解决的问题、采取的措施、结果及未完成事项` }).catch(error => {
363
+ const result = await new session_summary_1.SessionSummarizer(this.settings.summarizer).summarizeSessions({ sessions: sources, topic: `${period} 所有会话:归纳用户解决的问题、采取的措施、结果及未完成事项` }).catch(error => {
343
364
  if (error instanceof session_summary_1.SessionSummaryError && error.code === "limit")
344
365
  throw new ManagementError(error.message);
345
366
  throw new ManagementError("今日对话总结失败,请检查本机配置与模型服务");
@@ -562,8 +562,10 @@ async function withAunAttachmentPlugin(run, publish) {
562
562
  const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "aun-cli-control-"));
563
563
  const routes = new Map();
564
564
  const requests = [];
565
+ const timeoutDurations = [];
565
566
  const logs = [];
566
567
  const originalFetch = globalThis.fetch;
568
+ const originalTimeout = AbortSignal.timeout;
567
569
  let currentAid = "old.agentid.cn";
568
570
  const sdk = {
569
571
  AIDStore: class {
@@ -581,6 +583,7 @@ async function withAunAttachmentPlugin(run, publish) {
581
583
  };
582
584
  const plugin = new aun_1.AunPlugin({ sdk, ingress: { publish: async () => undefined } });
583
585
  try {
586
+ AbortSignal.timeout = ((milliseconds) => { timeoutDurations.push(milliseconds); return originalTimeout(milliseconds); });
584
587
  globalThis.fetch = (async (input, init) => {
585
588
  const url = String(input);
586
589
  requests.push({ url, method: init?.method ?? "GET", body: typeof init?.body === "string" ? init.body : undefined, signal: init?.signal ?? undefined });
@@ -623,6 +626,7 @@ async function withAunAttachmentPlugin(run, publish) {
623
626
  ]);
624
627
  strict_1.default.ok(requests.every(({ url }) => new URL(url).hostname === "127.0.0.1"));
625
628
  strict_1.default.ok(requests.every(({ signal }) => signal instanceof AbortSignal));
629
+ strict_1.default.deepEqual([...new Set(timeoutDurations)], [5000, 120_000]);
626
630
  strict_1.default.ok(JSON.stringify(created.body).includes("new.agentid.cn"));
627
631
  strict_1.default.doesNotMatch(JSON.stringify(created.body), /owner\.agentid\.cn/);
628
632
  strict_1.default.doesNotMatch(logs.join("\n"), /owner\.agentid\.cn/);
@@ -630,6 +634,7 @@ async function withAunAttachmentPlugin(run, publish) {
630
634
  }
631
635
  finally {
632
636
  globalThis.fetch = originalFetch;
637
+ AbortSignal.timeout = originalTimeout;
633
638
  await (0, promises_1.rm)(root, { recursive: true, force: true });
634
639
  }
635
640
  });
@@ -195,3 +195,36 @@ function fixture() {
195
195
  strict_1.default.ok(result.missingReasons.some(reason => /专用正文/.test(reason)), JSON.stringify(result.missingReasons));
196
196
  strict_1.default.ok(result.missingReasons.some(reason => /身份/.test(reason)));
197
197
  });
198
+ (0, node_test_1.default)("ChatGPT 新版桌面通过搜索轮次身份读取用户正文与当前会话最终答复", async () => {
199
+ const client = new chatgpt_1.ChatgptCdpClient();
200
+ const user = { querySelector: () => ({ innerText: "回一句:你很帅" }) };
201
+ const assistant = {
202
+ innerText: "你很帅",
203
+ closest: (selector) => selector.includes("data-response-annotation") ? {
204
+ getAttribute: (name) => name.endsWith("conversation") ? "one" : "message-one",
205
+ } : null,
206
+ };
207
+ const turn = {
208
+ getAttribute: (name) => name === "data-content-search-turn-key" ? "turn-one" : null,
209
+ querySelectorAll: (selector) => selector === "[data-user-message-bubble]" ? [user]
210
+ : selector === '[data-markdown-text-style="assistant-message"]' ? [assistant] : [],
211
+ };
212
+ const document = {
213
+ querySelector: (selector) => selector.includes("sidebar-thread-selected") ? {
214
+ getAttribute: (name) => name.endsWith("host-id") ? active.host : name.endsWith("thread-id") ? active.id : active.title,
215
+ } : null,
216
+ querySelectorAll: (selector) => selector === "[data-content-search-turn-key]" ? [turn] : [],
217
+ };
218
+ Object.assign(client, { connected: () => true, evaluate: async (expression) => {
219
+ strict_1.default.doesNotMatch(expression, /\.click\(|\.focus\(|Input\.|fetch\(|import\(|innerText\s*=/);
220
+ return (0, node_vm_1.runInNewContext)(expression, { document });
221
+ } });
222
+ const result = await client.readSessionHistory(active);
223
+ strict_1.default.equal(result.supported, true);
224
+ if (!result.supported)
225
+ return;
226
+ strict_1.default.deepEqual(JSON.parse(JSON.stringify(result.turns)), [
227
+ { role: "user", text: "回一句:你很帅" },
228
+ { role: "assistant", text: "你很帅" },
229
+ ]);
230
+ });
@@ -1,4 +1,37 @@
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
  };
@@ -303,3 +336,28 @@ function standardServer(onPost = (_request, response) => json(response, commandR
303
336
  await responseLimit.close();
304
337
  }
305
338
  });
339
+ (0, node_test_1.default)("插件密钥通过隐藏输入和独立字段传递,不进入 argv 或输出", async () => {
340
+ let payload;
341
+ const server = await listen((request, response) => {
342
+ if (request.url === "/api/plugins")
343
+ return json(response, { plugins: [{ ...plugin, status: "degraded" }] });
344
+ if (request.method === "GET")
345
+ return json(response, { ...commands, commands: [{ name: "configure", summary: "配置凭据", usage: "mab demo configure", secretPrompt: "App Secret" }] });
346
+ void readBody(request).then((body) => { payload = JSON.parse(body); json(response, { ok: true, message: "已保存" }); });
347
+ });
348
+ try {
349
+ const result = await (0, cli_commands_1.runPluginCommand)("demo", ["configure"], server.port, { readSecret: async () => "测试密钥" });
350
+ strict_1.default.equal(result.exitCode, 0);
351
+ strict_1.default.deepEqual(payload.argv, ["configure"]);
352
+ strict_1.default.equal(payload.secret, "测试密钥");
353
+ strict_1.default.doesNotMatch(result.stdout + result.stderr, /测试密钥/);
354
+ }
355
+ finally {
356
+ await server.close();
357
+ }
358
+ });
359
+ (0, node_test_1.default)("非交互终端拒绝读取密钥", async () => {
360
+ const { readSecret } = await Promise.resolve().then(() => __importStar(require("../src-ts/secret-input")));
361
+ if (!process.stdin.isTTY || !process.stderr.isTTY)
362
+ await strict_1.default.rejects(readSecret("App Secret"), /交互终端/);
363
+ });
@@ -4,12 +4,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const strict_1 = __importDefault(require("node:assert/strict"));
7
+ const node_child_process_1 = require("node:child_process");
8
+ const node_events_1 = require("node:events");
9
+ const promises_1 = require("node:fs/promises");
10
+ const node_os_1 = require("node:os");
11
+ const node_path_1 = require("node:path");
7
12
  const node_test_1 = __importDefault(require("node:test"));
8
13
  const daemon_process_1 = require("../src-ts/daemon-process");
9
14
  const cli = "C:\\Users\\molian\\AppData\\Roaming\\npm\\node_modules\\mu-agent-bridge\\dist\\src-ts\\cli.js";
10
15
  (0, node_test_1.default)("识别同一安装的全部旧 daemon,兼容 Windows 路径与引号", () => {
11
16
  strict_1.default.equal((0, daemon_process_1.isDaemonCommand)(`"C:\\Program Files\\nodejs\\node.exe" ${cli.replaceAll("\\", "/")} start`, cli), true);
12
17
  strict_1.default.equal((0, daemon_process_1.isDaemonCommand)(`node.exe "${cli}" --serve`, cli), true);
18
+ strict_1.default.equal((0, daemon_process_1.isDaemonCommand)(`node.exe "${cli.replaceAll("\\", "\\\\")}" start`, cli), true);
13
19
  });
14
20
  (0, node_test_1.default)("清理进程时保留 CLI 操作、其他安装及普通 Node 程序", () => {
15
21
  for (const args of ["restart", "stop", "update", "status"])
@@ -18,3 +24,49 @@ const cli = "C:\\Users\\molian\\AppData\\Roaming\\npm\\node_modules\\mu-agent-br
18
24
  strict_1.default.equal((0, daemon_process_1.isDaemonCommand)(`node.exe -e "${cli} start"`, cli), false);
19
25
  strict_1.default.equal((0, daemon_process_1.isDaemonCommand)(`node.exe "${cli}.backup" start`, cli), false);
20
26
  });
27
+ (0, node_test_1.default)("插件更新一直等待时,本地 API 和 AUN 仍已就绪", { skip: process.platform === "win32", timeout: 15_000 }, async () => {
28
+ // Windows 的真实进程清理另行验证,避免测试误结束同路径下的用户后台。
29
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "mab-startup-"));
30
+ const plugin = (0, node_path_1.join)(root, "plugins", "aun");
31
+ const state = (0, node_path_1.join)(root, "state");
32
+ const preload = (0, node_path_1.join)(root, "hold-updates.cjs");
33
+ let child;
34
+ try {
35
+ await (0, promises_1.mkdir)(plugin, { recursive: true });
36
+ await (0, promises_1.writeFile)((0, node_path_1.join)(plugin, "plugin.json"), JSON.stringify({ id: "aun", version: "1.0.0", type: "channel", entry: "index.cjs", apiVersion: "1", capabilities: [] }));
37
+ await (0, promises_1.writeFile)((0, node_path_1.join)(plugin, "index.cjs"), "module.exports={activate:async()=>{},deactivate:async()=>{},health:async()=>({status:'ready'})};");
38
+ await (0, promises_1.writeFile)(preload, `require(${JSON.stringify((0, node_path_1.resolve)(__dirname, "../src-ts/plugin-manager.js"))}).PluginManager.prototype.checkPackageUpdates=async()=>{console.log('UPDATE_BLOCKED');return new Promise(()=>{});};`);
39
+ child = (0, node_child_process_1.spawn)(process.execPath, ["--require", preload, (0, node_path_1.resolve)(__dirname, "../src-ts/cli.js"), "start"], {
40
+ env: { ...process.env, AGENT_BRIDGE_STATE_DIR: state, AGENT_BRIDGE_PLUGINS_DIR: (0, node_path_1.join)(root, "plugins"), AGENT_BRIDGE_HOST: "127.0.0.1", AGENT_BRIDGE_PORT: "0", AGENT_BRIDGE_TOKEN: "" },
41
+ stdio: ["ignore", "pipe", "pipe"],
42
+ });
43
+ let output = "";
44
+ child.stderr.on("data", chunk => { output += chunk; });
45
+ await new Promise((done, reject) => {
46
+ const timer = setTimeout(() => reject(new Error(`未进入更新检查:${output}`)), 5000);
47
+ child.stdout.on("data", chunk => {
48
+ output += chunk;
49
+ if (output.includes("UPDATE_BLOCKED")) {
50
+ clearTimeout(timer);
51
+ done();
52
+ }
53
+ });
54
+ child.once("error", error => { clearTimeout(timer); reject(error); });
55
+ child.once("exit", code => { clearTimeout(timer); reject(new Error(`后台提前退出:${code},${output}`)); });
56
+ });
57
+ const address = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(state, "daemon-address.json"), "utf8"));
58
+ strict_1.default.equal(address.pid, child.pid);
59
+ const response = await fetch(`http://127.0.0.1:${address.port}/api/plugins`, { signal: AbortSignal.timeout(2000) });
60
+ strict_1.default.equal(response.status, 200);
61
+ const plugins = (await response.json()).plugins;
62
+ strict_1.default.equal(plugins.find((entry) => entry.id === "aun")?.status, "ready");
63
+ }
64
+ finally {
65
+ if (child?.pid && child.exitCode === null && child.signalCode === null) {
66
+ const exited = (0, node_events_1.once)(child, "exit");
67
+ child.kill("SIGKILL");
68
+ await exited;
69
+ }
70
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
71
+ }
72
+ });
@@ -12,7 +12,7 @@ const node_vm_1 = require("node:vm");
12
12
  const element = () => ({ children: [], value: "", required: false, placeholder: "", textContent: "", addEventListener() { }, setAttribute() { }, append(...items) { this.children.push(...items); }, replaceChildren(...items) { this.children = items; } });
13
13
  const field = element;
14
14
  const button = { disabled: false };
15
- const credentials = { elements: { app_id: field(), app_secret: field() }, querySelector: () => button };
15
+ const credentials = { elements: { app_id: field(), app_secret: field(), agent_plugin: field() }, querySelector: () => button };
16
16
  const nodes = {
17
17
  "#credentials": credentials,
18
18
  "#binding": { elements: { pluginId: field() } },
@@ -25,15 +25,18 @@ const node_vm_1 = require("node:vm");
25
25
  document: { querySelector: (id) => nodes[id] ??= element(), createElement: element },
26
26
  localStorage: { getItem: () => null }, sessionStorage: { getItem: () => null },
27
27
  window: { addEventListener() { } }, AbortSignal, setTimeout, clearTimeout, setInterval() { }, clearInterval() { },
28
- Option: function () { },
28
+ Option: function (text, value) { this.text = text; this.value = value; },
29
29
  fetch: async (path, init) => {
30
30
  if (path.endsWith("/bot") || path.endsWith("/bind"))
31
31
  submitted.push(JSON.parse(init.body));
32
- return { ok: true, json: async () => path.endsWith("/status") ? { app_id: "cli_1", configured: true, status, message: status === "ready" ? "已连接" : "连接失败", bots: [{ app_id: "cli_1", configured: true, status, message: "连接失败" }, { app_id: "cli_2", configured: true, status: "ready", pluginId: "dsh", binding_time: "2026-10-10T10:00:00Z" }] } : { plugins: [] } };
32
+ return { ok: true, json: async () => path.endsWith("/status") ? { app_id: "cli_1", configured: true, status, message: status === "ready" ? "已连接" : "连接失败", bots: [{ app_id: "cli_1", configured: true, status, message: "连接失败" }, { app_id: "cli_2", configured: true, status: "ready", pluginId: "dsh", binding_time: "2026-10-10T10:00:00Z" }] } : { agents: [{ id: "dsh", displayName: "DSH", available: true }, { id: "workbuddy", displayName: "WorkBuddy", available: false, reason: "未找到可执行文件" }] } };
33
33
  },
34
34
  });
35
35
  await new Promise((resolve) => setImmediate(resolve));
36
36
  strict_1.default.equal(nodes["#bots"].children.length, 2);
37
+ strict_1.default.equal(credentials.elements.agent_plugin.children[2].disabled, true);
38
+ strict_1.default.match(credentials.elements.agent_plugin.children[2].text, /未找到可执行文件/);
39
+ strict_1.default.equal(credentials.elements.agent_plugin.children[1].disabled, false);
37
40
  strict_1.default.equal(nodes["#bots"].children[0].children[0].textContent, "cli_2");
38
41
  await nodes["#bots"].children[0].children[4].children[1].onclick();
39
42
  strict_1.default.equal(submitted[0].app_id, "cli_2");
@@ -42,6 +45,10 @@ const node_vm_1 = require("node:vm");
42
45
  await credentials.onsubmit({ preventDefault() { } });
43
46
  strict_1.default.equal(submitted[1].app_secret, "");
44
47
  strict_1.default.equal(credentials.elements.app_secret.value, "********");
48
+ credentials.elements.agent_plugin.value = "dsh";
49
+ await credentials.onsubmit({ preventDefault() { } });
50
+ strict_1.default.equal(submitted.at(-1).pluginId, "dsh");
51
+ strict_1.default.equal(submitted.at(-1).app_id, "cli_1");
45
52
  status = "degraded";
46
53
  await credentials.onsubmit({ preventDefault() { } });
47
54
  strict_1.default.match(nodes["#notice"].textContent, /连接失败/);
@@ -229,7 +229,7 @@ const feishu_1 = require("../src-ts/plugins/feishu");
229
229
  strict_1.default.equal(typeof postCommands, "function");
230
230
  await getCommands({}, response);
231
231
  const manifest = JSON.parse(response.body);
232
- strict_1.default.deepEqual(manifest.commands.map((command) => command.name), ["status", "list", "agents", "bind", "unbind"]);
232
+ strict_1.default.deepEqual(manifest.commands.map((command) => command.name), ["status", "list", "agents", "configure", "bind", "unbind"]);
233
233
  strict_1.default.equal(manifest.protocolVersion, 1);
234
234
  strict_1.default.equal(manifest.version, "1.0.5");
235
235
  const run = async (...argv) => {
@@ -371,3 +371,26 @@ const feishu_1 = require("../src-ts/plugins/feishu");
371
371
  await (0, promises_1.rm)(state, { recursive: true, force: true });
372
372
  }
373
373
  });
374
+ (0, node_test_1.default)("飞书 CLI 凭据配置保存密钥但不在响应中返回,并可同时关联 Agent", async () => {
375
+ const state = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "feishu-cli-configure-"));
376
+ const routes = new Map();
377
+ const originalFetch = globalThis.fetch;
378
+ const plugin = new feishu_1.FeishuPlugin({ client: { async start() { }, close() { } }, ingress: { publish: async () => undefined } });
379
+ try {
380
+ globalThis.fetch = async () => new Response(JSON.stringify({ agents: [{ id: "workbuddy", available: true }] }), { status: 200 });
381
+ await plugin.activate({ dataDir: state, config: {}, logger: { info() { }, warn() { }, error() { } }, http: { register(route) { routes.set(`${route.method} ${route.path}`, route.handler); return () => routes.delete(`${route.method} ${route.path}`); }, unregister() { } }, callPlugin: async () => ({ text: "" }), pluginStatus: () => "ready", runtime: { controlUrl: "http://127.0.0.1:1" } });
382
+ const handler = routes.get("POST /api/plugins/feishu/commands");
383
+ const response = { statusCode: 0, setHeader() { }, end(value) { this.body = value; } };
384
+ await handler(node_stream_1.Readable.from([JSON.stringify({ protocolVersion: 1, argv: ["configure", "cli_secretless", "--agent", "workbuddy"], secret: "never-return-this" })]), response);
385
+ strict_1.default.equal(response.statusCode, 200);
386
+ strict_1.default.doesNotMatch(response.body, /never-return-this/);
387
+ const saved = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(state, "config.json"), "utf8"));
388
+ strict_1.default.equal(saved.bots[0].app_secret, "never-return-this");
389
+ strict_1.default.equal(saved.bindings.cli_secretless, "workbuddy");
390
+ }
391
+ finally {
392
+ globalThis.fetch = originalFetch;
393
+ await plugin.deactivate("stop");
394
+ await (0, promises_1.rm)(state, { recursive: true, force: true });
395
+ }
396
+ });
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const strict_1 = __importDefault(require("node:assert/strict"));
7
+ const promises_1 = require("node:fs/promises");
8
+ const node_test_1 = __importDefault(require("node:test"));
9
+ const node_child_process_1 = require("node:child_process");
10
+ const node_util_1 = require("node:util");
11
+ (0, node_test_1.default)("Windows 安装向导隔离旧后台并固定 AUN 请求端口", { skip: process.platform !== "win32" }, async () => {
12
+ const { stdout } = await (0, node_util_1.promisify)(node_child_process_1.execFile)("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "tests/windows-installer.test.ps1"]);
13
+ strict_1.default.match(stdout, /验证通过/);
14
+ });
15
+ (0, node_test_1.default)("Windows 安装包包括中文向导、四类 Agent 和 per-user 卸载定义", async () => {
16
+ const [wxs, setup, launcher, build, qwenPlugin, workbuddyPlugin] = await Promise.all([
17
+ (0, promises_1.readFile)("installer/Product.wxs", "utf8"),
18
+ (0, promises_1.readFile)("installer/setup.ps1", "utf8"),
19
+ (0, promises_1.readFile)("installer/launch.cmd", "utf8"),
20
+ (0, promises_1.readFile)("scripts/build-installer.ps1", "utf8"),
21
+ (0, promises_1.readFile)("plugins/qwenwork/plugin.json", "utf8"),
22
+ (0, promises_1.readFile)("plugins/workbuddy/plugin.json", "utf8"),
23
+ ]);
24
+ strict_1.default.match(wxs, /Scope="perUser"/);
25
+ strict_1.default.match(wxs, /On="uninstall"/);
26
+ strict_1.default.match(wxs, /LaunchWizard/);
27
+ strict_1.default.match(launcher, /setup\.ps1/);
28
+ strict_1.default.match(build, /ProductVersion=\$installerVersion/);
29
+ strict_1.default.match(build, /\$packageVersion\.Build \+ 1/);
30
+ strict_1.default.match(setup, /22\.16\.0/);
31
+ strict_1.default.match(setup, /\$activeNodeVersion -lt \[version\]'22\.16\.0'/);
32
+ strict_1.default.match(setup, /node-v\$version-win-x64\.zip/);
33
+ strict_1.default.match(setup, /Environment\]::SetEnvironmentVariable\('Path', \$userPath, 'User'\)/);
34
+ strict_1.default.match(setup, /Get-Command npm\.cmd/);
35
+ strict_1.default.match(setup, /\$npmCommand\.Source prefix --global/);
36
+ strict_1.default.match(setup, /@\(\$nodeDirectory, \$npmDirectory, \$npmPrefix\)/);
37
+ strict_1.default.match(setup, /mu-agent-bridge@latest --engine-strict/);
38
+ strict_1.default.doesNotMatch(setup, /mu-agent-bridge@\d+\.\d+\.\d+/);
39
+ strict_1.default.match(setup, /api\/agents\/availability/);
40
+ strict_1.default.match(setup, /function Find-AgentExecutable/);
41
+ strict_1.default.match(setup, /Get-ChildItem -LiteralPath \$root -Filter \$app\.File -File -Recurse/);
42
+ strict_1.default.match(setup, /api\/plugins\/\$agent\/config/);
43
+ strict_1.default.match(setup, /TopMost = \$true/);
44
+ strict_1.default.match(setup, /BringToFront\(\)/);
45
+ strict_1.default.match(setup, /Invoke-AunCommand @\('list'\)/);
46
+ for (const id of ["qwenwork", "doubao", "workbuddy", "dsh"])
47
+ strict_1.default.ok(setup.includes(id), "缺少 Agent " + id);
48
+ strict_1.default.match(setup, /choices\[0\]\.message\.content/);
49
+ strict_1.default.match(setup, /existingBinding/);
50
+ strict_1.default.match(qwenPlugin, /https:\/\/qwenwork\.cn\/download/);
51
+ strict_1.default.match(workbuddyPlugin, /"displayName": "WorkBuddy"/);
52
+ strict_1.default.match(workbuddyPlugin, /https:\/\/www\.workbuddy\.ai\//);
53
+ });
54
+ (0, node_test_1.default)("MSI 重复安装时打开向导,卸载和静默运行时跳过", async () => {
55
+ const wxs = await (0, promises_1.readFile)("installer/Product.wxs", "utf8");
56
+ const condition = wxs.match(/<Custom Action="LaunchWizard"[^>]*Condition="([^"]+)"/);
57
+ strict_1.default.ok(condition, "缺少启动向导的执行条件");
58
+ strict_1.default.equal(condition[1], "NOT REMOVE AND NOT UPGRADINGPRODUCTCODE");
59
+ strict_1.default.match(wxs, /<InstallUISequence>\s*<Custom Action="LaunchWizard" After="ExecuteAction"/);
60
+ });
61
+ (0, node_test_1.default)("向导后台启动服务并提供缺失客户端的下载和重试入口", async () => {
62
+ const setup = await (0, promises_1.readFile)("installer/setup.ps1", "utf8");
63
+ strict_1.default.doesNotMatch(setup, /& mab\.cmd start/);
64
+ strict_1.default.match(setup, /Start-Process/);
65
+ strict_1.default.match(setup, /AddSeconds\(180\)/);
66
+ strict_1.default.match(setup, /System.Windows.Forms.LinkLabel/);
67
+ strict_1.default.match(setup, /重新检测/);
68
+ for (const domain of ["qwenwork.cn/download", "www.doubao.com", "www.workbuddy.ai", "github.com/deepseek-ai/deepseek-harness"])
69
+ strict_1.default.ok(setup.includes(domain));
70
+ });
71
+ (0, node_test_1.default)("先检测并显示缺失 App,再配置 AID,最后单独绑定 Agent", async () => {
72
+ const setup = await (0, promises_1.readFile)("installer/setup.ps1", "utf8");
73
+ const selectAid = setup.indexOf("$aid = Resolve-SetupAid");
74
+ const detect = setup.indexOf("$agents = Invoke-RestMethod");
75
+ const bind = setup.indexOf("Invoke-AunCommand @('bind'");
76
+ strict_1.default.ok(detect > 0 && detect < selectAid && selectAid < bind);
77
+ strict_1.default.match(setup, /新 AID(示例:workbuddy11\.agentid\.cn)/);
78
+ strict_1.default.match(setup, /Owner AID(示例:lihui7\.agentid\.cn)/);
79
+ strict_1.default.match(setup, /\$readyAgents.Count -lt 4/);
80
+ strict_1.default.match(setup, /\$InstalledIds -notcontains \$_.Id/);
81
+ strict_1.default.match(setup, /尚未就绪/);
82
+ strict_1.default.match(setup, /AID 关联失败/);
83
+ strict_1.default.match(setup, /setup\.log/);
84
+ strict_1.default.match(setup, /Write-SetupLog/);
85
+ strict_1.default.match(setup, /WorkBuddy 正在运行但未开启 CDP/);
86
+ strict_1.default.match(setup, /taskkill\.exe \/PID/);
87
+ strict_1.default.match(setup, /function Stop-BridgeDaemons/);
88
+ strict_1.default.match(setup, /api\/plugins\/aun\/commands/);
89
+ strict_1.default.doesNotMatch(setup, /Invoke-Mab/);
90
+ strict_1.default.match(setup, /Read-Host '输入 YES 关闭 WorkBuddy 并以 CDP 模式重启'\)\.Trim\(\) -ne 'YES'/);
91
+ strict_1.default.match(setup, /确认改为 \$agent?输入 YES 继续\"\)\.Trim\(\) -ne 'YES'/);
92
+ strict_1.default.doesNotMatch(setup, /-cne 'YES'/);
93
+ strict_1.default.match(setup, /qwenwork\.cn\/download/);
94
+ strict_1.default.match(setup, /检测到本机已有 AID/);
95
+ strict_1.default.match(setup, /IdentityConflictError/);
96
+ strict_1.default.match(setup, /返回 AID 输入步骤/);
97
+ strict_1.default.match(setup, /transport = 'cdp'/);
98
+ strict_1.default.match(setup, /workbuddy\/stop/);
99
+ });
@@ -928,6 +928,9 @@ const channel_sessions_1 = require("../src-ts/channel-sessions");
928
928
  strict_1.default.equal(globalThis.historyCalls, before);
929
929
  strict_1.default.equal((await manager.sessionManagement.archive.listSessions()).sessions.length, 1);
930
930
  const admin = await manager.sessions.current(message("", "admin"), "agent");
931
+ await ingress.publish(message("最近 4 小时有哪些话题", "admin"));
932
+ strict_1.default.match(replies.at(-1), /无权/);
933
+ strict_1.default.equal(globalThis.historyCalls, before);
931
934
  await manager.sessionManagement.saveConfig({ enabled: true, grants: [{ managerSource: admin.source, managerSessionId: admin.logicalSessionId, sources: [archived[0].source] }], summarizer: { endpoint: `http://127.0.0.1:${summaryServer.address().port}/v1/chat/completions`, model: "read-only" } });
932
935
  await ingress.publish(message("/sessions list", "admin"));
933
936
  const ref = replies.at(-1).split("\n")[0].split(" ")[0];
@@ -132,11 +132,11 @@ const source = (peer) => JSON.stringify(["aun", "local", "", peer, ""]);
132
132
  const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "mab-today-summary-"));
133
133
  const today = new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Shanghai", year: "numeric", month: "2-digit", day: "2-digit" }).format(new Date());
134
134
  const start = Date.parse(`${today}T00:00:00+08:00`);
135
- const plugins = ["qwenwork", "doubao", "dsh"];
135
+ const plugins = ["qwenwork", "doubao", "dsh", "workbuddy", "chatgpt"];
136
136
  const providers = plugins.map(pluginId => ({
137
137
  pluginId, status: "ready", identity: {},
138
138
  provider: {
139
- scope: `${pluginId}:scope`, mode: "all", limitation: "全量只读",
139
+ scope: `${pluginId}:scope`, mode: pluginId === "workbuddy" || pluginId === "chatgpt" ? "current" : "all", limitation: pluginId === "workbuddy" ? "无法证明完整历史" : pluginId === "chatgpt" ? "页面虚拟化影响" : "全量只读",
140
140
  async listSessions() { return { supported: true, sessions: [{ id: `${pluginId}-session`, title: pluginId, updatedAt: new Date(start - 1).toISOString() }], complete: true, missingReasons: [], readAt: new Date().toISOString() }; },
141
141
  async readSession(query) {
142
142
  return { supported: true, session: { id: query.sessionId, title: pluginId }, turns: [
@@ -158,10 +158,12 @@ const source = (peer) => JSON.stringify(["aun", "local", "", peer, ""]);
158
158
  await new Promise(resolve => server.listen(0, "127.0.0.1", resolve));
159
159
  try {
160
160
  await service.saveConfig({ enabled: false, grants: [], nativeGrants: [{ managerSource: source("admin"), managerSessionId: "manager", providers: providers.map(item => ({ pluginId: item.pluginId, scope: item.provider.scope })) }], summarizer: { endpoint: `http://127.0.0.1:${server.address().port}/v1/chat/completions`, model: "test" } });
161
- const result = await service.command("/sessions today --summary", { source: source("admin"), sessionId: "manager" });
161
+ const result = await service.command("今天有哪些话题聊了,给个小结", { source: source("admin"), sessionId: "manager" });
162
162
  strict_1.default.ok(result.includes(today));
163
163
  strict_1.default.match(result, /今日三个插件的问题均已解决/);
164
- for (const plugin of plugins)
164
+ strict_1.default.match(result, /workbuddy:.*无法证明完整历史/);
165
+ strict_1.default.match(result, /chatgpt:.*虚拟化影响/);
166
+ for (const plugin of ["qwenwork", "doubao", "dsh"])
165
167
  strict_1.default.match(received, new RegExp(`${plugin} 今天已解决`));
166
168
  strict_1.default.ok(!received.includes("昨天旧内容"));
167
169
  const raw = await service.command("/sessions today", { source: source("admin"), sessionId: "manager" });
@@ -217,6 +219,38 @@ const source = (peer) => JSON.stringify(["aun", "local", "", peer, ""]);
217
219
  await (0, promises_1.rm)(root, { recursive: true, force: true });
218
220
  }
219
221
  });
222
+ (0, node_test_1.default)("按最近整小时与自然语言筛选消息时间", async () => {
223
+ const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "mab-recent-history-"));
224
+ const providers = ["qwenwork", "workbuddy", "doubao", "chatgpt", "dsh"].map(pluginId => {
225
+ const provider = {
226
+ scope: `${pluginId}:scope`, mode: pluginId === "workbuddy" || pluginId === "chatgpt" ? "current" : "all", limitation: "只读",
227
+ async listSessions() { return { supported: true, sessions: [{ id: "recent", title: "最近会话" }], complete: true, missingReasons: [], readAt: new Date().toISOString() }; },
228
+ async readSession(query) {
229
+ const start = Date.parse(query.since);
230
+ return { supported: true, session: { id: "recent", title: "最近会话" }, turns: [
231
+ { role: "user", text: "窗口内消息", time: new Date(start + 60_000).toISOString() },
232
+ { role: "assistant", text: "窗口外消息", time: new Date(start - 60_000).toISOString() },
233
+ ], complete: true, missingReasons: [], readAt: new Date().toISOString() };
234
+ },
235
+ };
236
+ return { pluginId, provider, identity: provider, status: "ready" };
237
+ });
238
+ const service = new session_management_1.SessionManagement(root, undefined, undefined, () => providers);
239
+ try {
240
+ await service.saveConfig({ enabled: false, grants: [], nativeGrants: [{ managerSource: source("admin"), managerSessionId: "manager", providers: providers.map(item => ({ pluginId: item.pluginId, scope: item.provider.scope })) }] });
241
+ const context = { source: source("admin"), sessionId: "manager" };
242
+ const fourHours = await service.command("/sessions last 4h qwenwork", context);
243
+ strict_1.default.match(fourHours, /窗口内消息/);
244
+ strict_1.default.ok(!fourHours.includes("窗口外消息"));
245
+ const oneHour = await service.command("最近 1 小时有哪些话题", context);
246
+ strict_1.default.match(oneHour, /窗口内消息/);
247
+ strict_1.default.ok(!oneHour.includes("窗口外消息"));
248
+ strict_1.default.match(await service.command("/sessions last 25h", context) ?? "", /1至24小时/);
249
+ }
250
+ finally {
251
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
252
+ }
253
+ });
220
254
  (0, node_test_1.default)("读取期间撤销授权会阻止旧快照发往总结端点", async () => {
221
255
  const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "mab-management-revoke-"));
222
256
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mu-agent-bridge",
3
- "version": "1.0.40",
3
+ "version": "1.0.41",
4
4
  "description": "将 AUN Agent 连接到本地桌面 AI 客户端的插件平台。",
5
5
  "main": "dist/src-ts/cli.js",
6
6
  "bin": {
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "dist/",
12
+ "!dist/installer/**",
12
13
  "plugins/",
13
14
  "README.md"
14
15
  ],
@@ -30,6 +31,7 @@
30
31
  "scripts": {
31
32
  "build": "tsc && node scripts/build-web.mjs",
32
33
  "build:packages": "npm run build && node scripts/build-packages.mjs",
34
+ "build:installer": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/build-installer.ps1",
33
35
  "publish:packages": "node scripts/publish-packages.mjs",
34
36
  "test": "npm run build:packages && node --test dist/tests-ts/*.test.js",
35
37
  "self-test": "npm run build && node dist/src-ts/cli.js --self-test",
@@ -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;
@@ -6,7 +6,7 @@ const cdp_client_1 = require("./cdp-client");
6
6
  const session_store_1 = require("./session-store");
7
7
  exports.chatgptManifest = {
8
8
  id: "chatgpt",
9
- version: "1.0.9",
9
+ version: "1.0.10",
10
10
  displayName: "ChatGPT(Codex)",
11
11
  type: "agent",
12
12
  entry: "dist/index.js",
@@ -1 +1 @@
1
- {"name":"agent-bridge-chatgpt","version":"1.0.9","description":"agent-bridge 的 ChatGPT Codex CDP 插件。","keywords":["mu-agent-bridge-plugin","mu-agent-bridge-agent"],"main":"dist/src-ts/plugins/chatgpt/index.js","files":["dist/","plugin.json","config.schema.json","README.md"],"agentBridge":{"plugin":"./plugin.json","type":"agent"},"engines":{"node":">=22"}}
1
+ {"name":"agent-bridge-chatgpt","version":"1.0.10","description":"agent-bridge 的 ChatGPT Codex CDP 插件。","keywords":["mu-agent-bridge-plugin","mu-agent-bridge-agent"],"main":"dist/src-ts/plugins/chatgpt/index.js","files":["dist/","plugin.json","config.schema.json","README.md"],"agentBridge":{"plugin":"./plugin.json","type":"agent"},"engines":{"node":">=22"}}
@@ -1,14 +1 @@
1
- {
2
- "id": "chatgpt",
3
- "version": "1.0.9",
4
- "type": "agent",
5
- "displayName": "ChatGPT(Codex)",
6
- "entry": "dist/src-ts/plugins/chatgpt/index.js",
7
- "apiVersion": "1",
8
- "process": "in-process",
9
- "capabilities": [
10
- "chat",
11
- "health"
12
- ],
13
- "configSchema": "config.schema.json"
14
- }
1
+ {"id":"chatgpt","version":"1.0.10","type":"agent","displayName":"ChatGPT(Codex)","entry":"dist/src-ts/plugins/chatgpt/index.js","apiVersion":"1","process":"in-process","capabilities":["chat","health"],"configSchema":"config.schema.json"}
@@ -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>