mu-agent-bridge 1.0.39 → 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 (32) hide show
  1. package/README.md +16 -0
  2. package/dist/src-ts/cli-commands.js +6 -2
  3. package/dist/src-ts/cli.js +1 -1
  4. package/dist/src-ts/control-server.js +2 -0
  5. package/dist/src-ts/daemon-process.js +3 -2
  6. package/dist/src-ts/plugin-manager.js +1 -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 +1 -1
  10. package/dist/src-ts/plugins/feishu/index.js +21 -8
  11. package/dist/src-ts/plugins/feishu/public/app.js +20 -8
  12. package/dist/src-ts/plugins/feishu/public/index.html +1 -1
  13. package/dist/src-ts/secret-input.js +47 -0
  14. package/dist/src-ts/session-management.js +34 -13
  15. package/dist/tests-ts/aun-plugin.test.js +5 -0
  16. package/dist/tests-ts/chatgpt-history.test.js +33 -0
  17. package/dist/tests-ts/cli-commands.test.js +58 -0
  18. package/dist/tests-ts/daemon-process.test.js +52 -0
  19. package/dist/tests-ts/feishu-page.test.js +10 -3
  20. package/dist/tests-ts/feishu-plugin.test.js +24 -1
  21. package/dist/tests-ts/installer-assets.test.js +99 -0
  22. package/dist/tests-ts/plugin-manager.test.js +3 -0
  23. package/dist/tests-ts/session-management.test.js +38 -4
  24. package/package.json +3 -1
  25. package/plugins/aun/dist/src-ts/plugins/aun/index.js +1 -1
  26. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +3 -2
  27. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +1 -1
  28. package/plugins/chatgpt/package.json +1 -1
  29. package/plugins/chatgpt/plugin.json +1 -14
  30. package/plugins/feishu/dist/src-ts/plugins/feishu/index.js +21 -8
  31. package/plugins/feishu/dist/src-ts/plugins/feishu/public/app.js +20 -8
  32. package/plugins/feishu/dist/src-ts/plugins/feishu/public/index.html +1 -1
@@ -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.39",
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>