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.
- package/README.md +16 -0
- package/dist/src-ts/cli-commands.js +6 -2
- package/dist/src-ts/cli.js +1 -1
- package/dist/src-ts/control-server.js +2 -0
- package/dist/src-ts/daemon-process.js +3 -2
- package/dist/src-ts/plugin-manager.js +1 -1
- package/dist/src-ts/plugins/aun/index.js +1 -1
- package/dist/src-ts/plugins/chatgpt/cdp-client.js +3 -2
- package/dist/src-ts/plugins/chatgpt/index.js +1 -1
- package/dist/src-ts/plugins/feishu/index.js +21 -8
- package/dist/src-ts/plugins/feishu/public/app.js +20 -8
- package/dist/src-ts/plugins/feishu/public/index.html +1 -1
- package/dist/src-ts/secret-input.js +47 -0
- package/dist/src-ts/session-management.js +34 -13
- package/dist/tests-ts/aun-plugin.test.js +5 -0
- package/dist/tests-ts/chatgpt-history.test.js +33 -0
- package/dist/tests-ts/cli-commands.test.js +58 -0
- package/dist/tests-ts/daemon-process.test.js +52 -0
- package/dist/tests-ts/feishu-page.test.js +10 -3
- package/dist/tests-ts/feishu-plugin.test.js +24 -1
- package/dist/tests-ts/installer-assets.test.js +99 -0
- package/dist/tests-ts/plugin-manager.test.js +3 -0
- package/dist/tests-ts/session-management.test.js +38 -4
- package/package.json +3 -1
- package/plugins/aun/dist/src-ts/plugins/aun/index.js +1 -1
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +3 -2
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +1 -1
- package/plugins/chatgpt/package.json +1 -1
- package/plugins/chatgpt/plugin.json +1 -14
- package/plugins/feishu/dist/src-ts/plugins/feishu/index.js +21 -8
- package/plugins/feishu/dist/src-ts/plugins/feishu/public/app.js +20 -8
- 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" }] } : {
|
|
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("
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
+
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.
|
|
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,
|
|
153
|
-
const
|
|
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
|
|
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
|
-
|
|
184
|
+
const entries = command === "configure" ? data.agents : data.plugins;
|
|
185
|
+
if (!Array.isArray(entries))
|
|
180
186
|
throw new Error("控制 API 响应无效");
|
|
181
|
-
agents =
|
|
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,
|
|
33
|
-
const agents =
|
|
34
|
-
const
|
|
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", ""), ...
|
|
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
|
-
|
|
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 {
|
|
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>
|