mu-agent-bridge 1.0.40 → 1.0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/src-ts/agent-availability.js +2 -1
- package/dist/src-ts/cli-commands.js +6 -2
- package/dist/src-ts/control-server.js +6 -0
- package/dist/src-ts/daemon-process.js +3 -2
- package/dist/src-ts/plugin-manager.js +17 -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 +42 -6
- package/dist/src-ts/plugins/desktop-launch.js +93 -2
- package/dist/src-ts/plugins/doubao/index.js +14 -12
- 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/plugins/workbuddy/cdp-client.js +3 -1
- package/dist/src-ts/plugins/workbuddy/index.js +30 -19
- package/dist/src-ts/secret-input.js +47 -0
- package/dist/src-ts/session-management.js +34 -13
- package/dist/tests-ts/agent-availability.test.js +15 -1
- 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/control-server.test.js +20 -0
- package/dist/tests-ts/daemon-process.test.js +52 -0
- package/dist/tests-ts/desktop-launch.test.js +98 -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 +122 -0
- package/dist/tests-ts/plugin-manager.test.js +21 -0
- package/dist/tests-ts/session-management.test.js +38 -4
- package/dist/tests-ts/workbuddy-plugin.test.js +7 -0
- package/package.json +3 -1
- package/plugins/aun/dist/src-ts/plugins/aun/index.js +1 -1
- package/plugins/chatgpt/config.schema.json +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 +42 -6
- package/plugins/chatgpt/dist/src-ts/plugins/desktop-launch.js +191 -0
- package/plugins/chatgpt/package.json +1 -1
- package/plugins/chatgpt/plugin.json +1 -14
- package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +93 -2
- package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +14 -12
- 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
- package/plugins/qwenwork/dist/src-ts/plugins/desktop-launch.js +93 -2
- package/plugins/workbuddy/dist/src-ts/plugins/desktop-launch.js +191 -0
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +3 -1
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +30 -19
- package/plugins/workbuddy/package.json +1 -1
- package/plugins/workbuddy/plugin.json +1 -1
package/README.md
CHANGED
|
@@ -146,3 +146,19 @@ Windows 路径时,可设置 `DSH_INSTALL_DIR`。
|
|
|
146
146
|
## AUN 身份管理
|
|
147
147
|
|
|
148
148
|
创建、连接、关联和 AUN 通信统一由独立 `agent-bridge-aun` 插件完成。安装并启动 AUN 后,在 ABWeb 的“通道 → AUN”创建或选择身份,再将已连接 AID 关联到已启动的 Agent;用 `mab status` 查看状态。Owner AID 仅作本地归属配置,不代表 AUN Owner 关系已激活。完整步骤见 [AUN 插件与 AID 身份管理](docs/aun-plugin-identities.md);给 Codex、Claude Code 直接执行的 CLI 说明见 [使用说明](使用说明.md)。
|
|
149
|
+
|
|
150
|
+
### 飞书凭据与办公 Agent 关联
|
|
151
|
+
|
|
152
|
+
在 ABWeb 的“飞书通道 → 手动配置凭据”中填写 App ID、App Secret,并选择“关联办公 Agent”。Secret 使用密码框显示;已保存的密钥保持掩码,不通过状态接口返回。WorkBuddy、千问办公、豆包根据运行后台的机器上是否存在可执行文件检测,文件缺失或插件未就绪的选项置灰。自定义安装位置请在对应插件配置中填写 `executablePath`。
|
|
153
|
+
|
|
154
|
+
也可以使用 CLI,Secret 在命令执行后隐藏输入,不写在命令参数中:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
mab feishu configure cli_你的应用ID --agent workbuddy
|
|
158
|
+
mab feishu agents
|
|
159
|
+
mab feishu list
|
|
160
|
+
mab feishu bind cli_你的应用ID workbuddy
|
|
161
|
+
mab feishu unbind cli_你的应用ID
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`configure` 保存凭据及可选关联后,需要在 ABWeb 重载飞书插件以连接机器人。非交互终端请使用网页密码框;不要把 Secret 写进命令行、脚本或仓库。
|
|
@@ -7,7 +7,8 @@ async function agentAvailability(id, config, status) {
|
|
|
7
7
|
const configured = typeof config.executablePath === "string" ? config.executablePath : undefined;
|
|
8
8
|
const executable = id === "workbuddy" ? (0, workbuddy_1.findExecutable)(configured)
|
|
9
9
|
: id === "qwenwork" ? await (0, desktop_launch_1.findWindowsExecutable)("QwenWorkCN.exe", configured)
|
|
10
|
-
: id === "doubao" ? await (0, desktop_launch_1.findWindowsExecutable)("Doubao.exe", configured)
|
|
10
|
+
: id === "doubao" ? await (0, desktop_launch_1.findWindowsExecutable)("Doubao.exe", configured)
|
|
11
|
+
: id === "dsh" ? await (0, desktop_launch_1.findWindowsExecutable)("DeepSeek Harness.exe", configured) : true;
|
|
11
12
|
if (!executable)
|
|
12
13
|
return { available: false, reason: "未找到可执行文件" };
|
|
13
14
|
return { available: status === "ready", reason: status === "ready" ? "" : "插件未就绪" };
|
|
@@ -5,6 +5,7 @@ exports.resolveLocalControlHost = resolveLocalControlHost;
|
|
|
5
5
|
exports.controlUrl = controlUrl;
|
|
6
6
|
exports.runPluginCommand = runPluginCommand;
|
|
7
7
|
exports.runCoreHelp = runCoreHelp;
|
|
8
|
+
const secret_input_1 = require("./secret-input");
|
|
8
9
|
const node_http_1 = require("node:http");
|
|
9
10
|
const MAX_BODY_BYTES = 1024 * 1024;
|
|
10
11
|
const PLUGIN_ID = /^[a-z][a-z0-9-]{1,63}$/;
|
|
@@ -108,6 +109,7 @@ function commandManifest(value, plugin) {
|
|
|
108
109
|
|| !Array.isArray(value.commands) || value.commands.some((command) => !object(command)
|
|
109
110
|
|| typeof command.name !== "string" || !/^[a-z][a-z0-9-]*$/.test(command.name)
|
|
110
111
|
|| typeof command.summary !== "string" || !command.summary.trim() || typeof command.usage !== "string" || !command.usage.trim()
|
|
112
|
+
|| (command.secretPrompt !== undefined && (typeof command.secretPrompt !== "string" || !command.secretPrompt.trim()))
|
|
111
113
|
|| (command.examples !== undefined && (!Array.isArray(command.examples) || command.examples.some((example) => typeof example !== "string"))))) {
|
|
112
114
|
throw new CliError("插件命令清单协议无效(需要 protocolVersion: 1)");
|
|
113
115
|
}
|
|
@@ -126,7 +128,7 @@ async function runPluginCommand(id, args, port, options = {}) {
|
|
|
126
128
|
throw new CliError(`插件“${id}”未安装;请先到 ABWeb 安装插件`, 2);
|
|
127
129
|
if (!plugin.capabilities.includes("cli"))
|
|
128
130
|
throw new CliError(`插件“${plugin.displayName}”未提供 CLI 命令;请升级对应插件`, 2);
|
|
129
|
-
if (plugin.status
|
|
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
|
|
151
|
+
const prompt = manifest.commands.find((command) => command.name === argv[0])?.secretPrompt;
|
|
152
|
+
const secret = prompt ? await (options.readSecret ?? secret_input_1.readSecret)(prompt) : undefined;
|
|
153
|
+
const executed = await requestJson(port, path, options, { protocolVersion: 1, argv, ...(secret === undefined ? {} : { secret }) });
|
|
150
154
|
const result = executed.value;
|
|
151
155
|
if (!object(result) || typeof result.ok !== "boolean" || typeof result.message !== "string"
|
|
152
156
|
|| (executed.status >= 200 && executed.status < 300 ? !result.ok : result.ok)
|
|
@@ -251,6 +251,8 @@ class ControlServer {
|
|
|
251
251
|
const result = await identity.create(body.aid, body.ownerAid);
|
|
252
252
|
return this.json(res, result.status === "等待 Owner 确认" ? 202 : result.status === "existing" ? 200 : 201, result);
|
|
253
253
|
}
|
|
254
|
+
if (method === "GET" && url.pathname === "/api/agents/availability")
|
|
255
|
+
return this.json(res, 200, { agents: await this.options.manager.availableAgents() });
|
|
254
256
|
if (method === "GET" && url.pathname === "/api/plugins")
|
|
255
257
|
return this.json(res, 200, { ok: true, plugins: this.options.manager.list() });
|
|
256
258
|
if (method === "GET" && url.pathname === "/api/plugins/catalog")
|
|
@@ -271,6 +273,10 @@ class ControlServer {
|
|
|
271
273
|
await this.options.manager.reload(id);
|
|
272
274
|
return this.json(res, 200, { ok: true });
|
|
273
275
|
}
|
|
276
|
+
if (method === "POST" && parts[3] === "restart-desktop") {
|
|
277
|
+
await this.options.manager.restartDesktop(id);
|
|
278
|
+
return this.json(res, 200, { ok: true });
|
|
279
|
+
}
|
|
274
280
|
if (method === "POST" && parts[3] === "update") {
|
|
275
281
|
const body = await this.body(req);
|
|
276
282
|
return this.json(res, 200, { ok: true, plugin: await this.options.manager.upgradePackage(id, body.packageName, body.version) });
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.isDaemonCommand = isDaemonCommand;
|
|
4
4
|
/** 只识别直接运行本安装 CLI 的后台进程,避免旧地址文件中的 PID 被其他程序复用。 */
|
|
5
5
|
function isDaemonCommand(commandLine, cli) {
|
|
6
|
-
const
|
|
7
|
-
|
|
6
|
+
const normalize = (value) => value.replace(/[\\/]+/g, "/").toLowerCase();
|
|
7
|
+
const args = [...commandLine.matchAll(/"([^"]*)"|(\S+)/g)].map((match) => normalize(match[1] ?? match[2]));
|
|
8
|
+
return args[1] === normalize(cli) && ["start", "--serve"].includes(args[2]);
|
|
8
9
|
}
|
|
@@ -21,6 +21,8 @@ const channel_sessions_1 = require("./channel-sessions");
|
|
|
21
21
|
const session_management_1 = require("./session-management");
|
|
22
22
|
const _2020_1 = __importDefault(require("ajv/dist/2020"));
|
|
23
23
|
const attachments_1 = require("./attachments");
|
|
24
|
+
const desktop_launch_1 = require("./plugins/desktop-launch");
|
|
25
|
+
const workbuddy_1 = require("./plugins/workbuddy");
|
|
24
26
|
const logger = { info() { }, warn() { }, error() { } };
|
|
25
27
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
26
28
|
class PluginManager {
|
|
@@ -385,7 +387,7 @@ class PluginManager {
|
|
|
385
387
|
if (trustedChannel && (typeof message.peerId !== "string" || !message.peerId.trim() || [message.peerId, message.recipientId, message.chatId, message.threadId].some(value => value !== undefined && (typeof value !== "string" || value.length > 512 || /[\u0000-\u001f*]/.test(value)))))
|
|
386
388
|
throw new agent_1.AgentError("通道来源标识无效");
|
|
387
389
|
const target = message.agentPlugin || "qwenwork";
|
|
388
|
-
if (/^\/sessions(?:\s|$)/.test(message.text.trimStart())) {
|
|
390
|
+
if (/^\/sessions(?:\s|$)/.test(message.text.trimStart()) || (0, session_management_1.isDailySummaryRequest)(message.text)) {
|
|
389
391
|
if (!trustedChannel)
|
|
390
392
|
throw new agent_1.AgentError("当前通道不支持会话管理");
|
|
391
393
|
const current = await this.sessions.current(message, target);
|
|
@@ -507,6 +509,20 @@ class PluginManager {
|
|
|
507
509
|
await this.logs.flush();
|
|
508
510
|
}
|
|
509
511
|
async reload(id) { await this.stop(id, "reload"); await this.start(id); }
|
|
512
|
+
async restartDesktop(id) {
|
|
513
|
+
if (id !== "workbuddy")
|
|
514
|
+
throw new agent_1.AgentError(`不支持桌面重启:${id}`);
|
|
515
|
+
const config = await this.config(id);
|
|
516
|
+
if (config.transport !== "cdp")
|
|
517
|
+
throw new agent_1.AgentError("请先将 WorkBuddy 设置为 CDP 模式");
|
|
518
|
+
const executable = (0, workbuddy_1.findExecutable)(typeof config.executablePath === "string" ? config.executablePath : undefined);
|
|
519
|
+
if (!executable)
|
|
520
|
+
throw new agent_1.AgentError("未找到 WorkBuddy 桌面程序,无法重启");
|
|
521
|
+
await this.stop(id);
|
|
522
|
+
const processIds = await (0, desktop_launch_1.terminateWindowsDesktop)(executable);
|
|
523
|
+
this.log("info", "后台已清理 WorkBuddy 旧进程,将以 CDP 模式启动", { processIds, executable }, id);
|
|
524
|
+
await this.start(id);
|
|
525
|
+
}
|
|
510
526
|
async uninstall(id) {
|
|
511
527
|
const loaded = this.plugins.get(id);
|
|
512
528
|
if (!loaded)
|
|
@@ -671,7 +671,7 @@ class AunPlugin {
|
|
|
671
671
|
method,
|
|
672
672
|
headers: { accept: "application/json", ...(body === undefined ? {} : { "content-type": "application/json" }) },
|
|
673
673
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
674
|
-
signal: AbortSignal.timeout(5000),
|
|
674
|
+
signal: AbortSignal.timeout(method === "PATCH" ? 120_000 : 5000),
|
|
675
675
|
redirect: "error",
|
|
676
676
|
});
|
|
677
677
|
}
|
|
@@ -111,7 +111,8 @@ class ChatgptCdpClient {
|
|
|
111
111
|
if (selected?.getAttribute('data-app-action-sidebar-thread-id') !== ${JSON.stringify(session.id)}
|
|
112
112
|
|| selected?.getAttribute('data-app-action-sidebar-thread-host-id') !== ${JSON.stringify(session.host)}) return { supported: false, reason: '读取期间活动会话身份发生变化' };
|
|
113
113
|
if (document.querySelector(${JSON.stringify(stopButtonSelector)})) return { supported: false, reason: 'ChatGPT 正在生成回复,暂不读取历史' };
|
|
114
|
-
const
|
|
114
|
+
const legacyTurns = [...document.querySelectorAll(${JSON.stringify(exports.turnSelector)})];
|
|
115
|
+
const turns = legacyTurns.length ? legacyTurns : [...document.querySelectorAll('[data-content-search-turn-key]')];
|
|
115
116
|
const output = [];
|
|
116
117
|
const missingReasons = [];
|
|
117
118
|
let chars = 0;
|
|
@@ -121,7 +122,7 @@ class ChatgptCdpClient {
|
|
|
121
122
|
output.push({ role, text });
|
|
122
123
|
};
|
|
123
124
|
for (const turn of turns) {
|
|
124
|
-
const turnId = turn.getAttribute(${JSON.stringify(turnIdAttribute)});
|
|
125
|
+
const turnId = turn.getAttribute(${JSON.stringify(turnIdAttribute)}) || turn.getAttribute('data-content-search-turn-key');
|
|
125
126
|
if (!turnId) {
|
|
126
127
|
missingReasons.push('部分消息缺少可验证的 turn 身份');
|
|
127
128
|
continue;
|
|
@@ -4,9 +4,12 @@ exports.ChatgptCdpClient = exports.ChatgptPlugin = exports.chatgptManifest = voi
|
|
|
4
4
|
const node_crypto_1 = require("node:crypto");
|
|
5
5
|
const cdp_client_1 = require("./cdp-client");
|
|
6
6
|
const session_store_1 = require("./session-store");
|
|
7
|
+
const desktop_launch_1 = require("../desktop-launch");
|
|
8
|
+
const node_path_1 = require("node:path");
|
|
9
|
+
const node_os_1 = require("node:os");
|
|
7
10
|
exports.chatgptManifest = {
|
|
8
11
|
id: "chatgpt",
|
|
9
|
-
version: "1.0.
|
|
12
|
+
version: "1.0.10",
|
|
10
13
|
displayName: "ChatGPT(Codex)",
|
|
11
14
|
type: "agent",
|
|
12
15
|
entry: "dist/index.js",
|
|
@@ -35,6 +38,8 @@ class ChatgptPlugin {
|
|
|
35
38
|
historyScope = "";
|
|
36
39
|
historyGeneration = 0;
|
|
37
40
|
historyProvider;
|
|
41
|
+
ownedProcess;
|
|
42
|
+
reconnect;
|
|
38
43
|
constructor(options = {}) {
|
|
39
44
|
this.options = options;
|
|
40
45
|
}
|
|
@@ -87,21 +92,52 @@ class ChatgptPlugin {
|
|
|
87
92
|
timeoutMs: typeof config.timeoutMs === "number" && config.timeoutMs > 0 ? config.timeoutMs : 30_000,
|
|
88
93
|
ssh,
|
|
89
94
|
});
|
|
95
|
+
const desktopExecutable = !ssh && config.autoStart !== false && !this.options.client && process.platform === "win32"
|
|
96
|
+
? await (0, desktop_launch_1.findChatgptExecutable)(typeof config.executablePath === "string" ? config.executablePath : undefined)
|
|
97
|
+
: undefined;
|
|
98
|
+
const desktopProfile = (0, node_path_1.join)(context.dataDir ?? (0, node_path_1.join)((0, node_os_1.homedir)(), ".agent-bridge", "chatgpt"), "desktop-profile");
|
|
99
|
+
const desktopArgs = [`--remote-debugging-port=${port(config.localCdpPort, 9230)}`];
|
|
100
|
+
const startupTimeoutMs = typeof config.startupTimeoutMs === "number" ? config.startupTimeoutMs : 30_000;
|
|
101
|
+
this.reconnect = async () => {
|
|
102
|
+
try {
|
|
103
|
+
await this.client.connect();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
catch (first) {
|
|
107
|
+
if (!desktopExecutable)
|
|
108
|
+
throw first;
|
|
109
|
+
await (0, desktop_launch_1.restartOwnedAndWait)(this.ownedProcess, desktopExecutable, desktopArgs, desktopProfile, () => this.client.connect(), startupTimeoutMs, child => { this.ownedProcess = child; });
|
|
110
|
+
}
|
|
111
|
+
};
|
|
90
112
|
this.sessionOperations = this.client.createSession ? ["new"] : [];
|
|
91
113
|
try {
|
|
92
114
|
await this.client.connect();
|
|
93
115
|
}
|
|
94
116
|
catch (error) {
|
|
95
|
-
|
|
117
|
+
let connectionError = error;
|
|
118
|
+
if (desktopExecutable) {
|
|
119
|
+
try {
|
|
120
|
+
await (0, desktop_launch_1.launchManagedAndWait)(desktopExecutable, desktopArgs, desktopProfile, () => this.client.connect(), startupTimeoutMs, child => { this.ownedProcess = child; });
|
|
121
|
+
this.failure = undefined;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
catch (launchError) {
|
|
125
|
+
connectionError = launchError;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
this.failure = connectionError instanceof Error ? connectionError.message : String(connectionError);
|
|
96
129
|
context.logger.warn(this.failure);
|
|
97
130
|
if (!this.store.sessions.length)
|
|
98
|
-
throw
|
|
131
|
+
throw connectionError;
|
|
99
132
|
}
|
|
100
133
|
}
|
|
101
134
|
async deactivate() {
|
|
102
135
|
await this.queue;
|
|
103
136
|
await this.store?.drain();
|
|
104
137
|
this.client?.close();
|
|
138
|
+
await (0, desktop_launch_1.terminateOwnedProcess)(this.ownedProcess).catch(() => undefined);
|
|
139
|
+
this.ownedProcess = undefined;
|
|
140
|
+
this.reconnect = undefined;
|
|
105
141
|
this.client = undefined;
|
|
106
142
|
this.remote = false;
|
|
107
143
|
}
|
|
@@ -246,7 +282,7 @@ class ChatgptPlugin {
|
|
|
246
282
|
}
|
|
247
283
|
async synchronize() {
|
|
248
284
|
if (!this.client.connected())
|
|
249
|
-
await this.client.connect();
|
|
285
|
+
await (this.reconnect?.() ?? this.client.connect());
|
|
250
286
|
if (!this.client.listSessions)
|
|
251
287
|
throw new Error("当前 ChatGPT 客户端不支持会话列表");
|
|
252
288
|
this.store.synchronize(await this.client.listSessions());
|
|
@@ -254,7 +290,7 @@ class ChatgptPlugin {
|
|
|
254
290
|
}
|
|
255
291
|
async select(session) {
|
|
256
292
|
if (!this.client.connected())
|
|
257
|
-
await this.client.connect();
|
|
293
|
+
await (this.reconnect?.() ?? this.client.connect());
|
|
258
294
|
if (!this.client.switchSession)
|
|
259
295
|
throw new Error("当前 ChatGPT 客户端不支持会话切换");
|
|
260
296
|
const active = await this.client.activeSession?.();
|
|
@@ -321,7 +357,7 @@ class ChatgptPlugin {
|
|
|
321
357
|
await this.select(selected);
|
|
322
358
|
}
|
|
323
359
|
if (!this.client.connected())
|
|
324
|
-
await this.client.connect();
|
|
360
|
+
await (this.reconnect?.() ?? this.client.connect());
|
|
325
361
|
selected ??= await this.client.activeSession?.();
|
|
326
362
|
const message = explicit?.[2]?.trim() || request.task;
|
|
327
363
|
const reply = await this.client.direct(message, request.timeoutMs, request.attachments);
|
|
@@ -3,10 +3,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.findWindowsExecutable = findWindowsExecutable;
|
|
4
4
|
exports.findExecutableInTree = findExecutableInTree;
|
|
5
5
|
exports.launchAndWait = launchAndWait;
|
|
6
|
+
exports.launchManagedAndWait = launchManagedAndWait;
|
|
7
|
+
exports.restartOwnedAndWait = restartOwnedAndWait;
|
|
8
|
+
exports.terminateWindowsDesktop = terminateWindowsDesktop;
|
|
9
|
+
exports.terminateOwnedProcess = terminateOwnedProcess;
|
|
10
|
+
exports.findChatgptExecutable = findChatgptExecutable;
|
|
6
11
|
exports.waitFor = waitFor;
|
|
12
|
+
const node_child_process_1 = require("node:child_process");
|
|
7
13
|
const promises_1 = require("node:fs/promises");
|
|
8
14
|
const node_fs_1 = require("node:fs");
|
|
9
15
|
const node_path_1 = require("node:path");
|
|
16
|
+
const node_util_1 = require("node:util");
|
|
17
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
10
18
|
const versionDirectory = /^(?:app[-_ ]?)?v?\d+(?:[.-]\d+)+$/i;
|
|
11
19
|
/** 在 Windows 常见安装目录中查找桌面程序,避免依赖单一盘符。 */
|
|
12
20
|
async function findWindowsExecutable(fileName, configured) {
|
|
@@ -31,8 +39,11 @@ async function findWindowsExecutable(fileName, configured) {
|
|
|
31
39
|
}
|
|
32
40
|
if (process.platform !== "win32")
|
|
33
41
|
return undefined;
|
|
34
|
-
const pattern = /doubao|豆包|qwen|qwwork
|
|
42
|
+
const pattern = /doubao|豆包|qwen|qwwork|千问|deepseek|harness|dsh/i;
|
|
35
43
|
const roots = [...new Set([
|
|
44
|
+
process.env.DSH_INSTALL_DIR,
|
|
45
|
+
"D:\\Programs\\DeepSeek Harness",
|
|
46
|
+
"D:\\DSH",
|
|
36
47
|
process.env.LOCALAPPDATA && (0, node_path_1.join)(process.env.LOCALAPPDATA, "Programs"),
|
|
37
48
|
process.env.ProgramFiles,
|
|
38
49
|
process.env["ProgramFiles(x86)"],
|
|
@@ -83,6 +94,87 @@ async function launchAndWait(executable, args, connect, timeoutMs, launch = (fil
|
|
|
83
94
|
child.unref();
|
|
84
95
|
return waitFor(connect, timeoutMs, 250);
|
|
85
96
|
}
|
|
97
|
+
async function launchManagedAndWait(executable, args, profileDirectory, connect, timeoutMs, onProcess, launch = (file, argv) => (0, node_child_process_1.spawn)(file, argv, { detached: true, stdio: "ignore", windowsHide: false })) {
|
|
98
|
+
if (profileDirectory)
|
|
99
|
+
await (0, promises_1.mkdir)(profileDirectory, { recursive: true });
|
|
100
|
+
const child = launch(executable, profileDirectory ? [...args, `--user-data-dir=${profileDirectory}`] : args);
|
|
101
|
+
child.unref();
|
|
102
|
+
onProcess(child);
|
|
103
|
+
try {
|
|
104
|
+
return await waitFor(connect, timeoutMs, 250);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
await terminateOwnedProcess(child).catch(() => undefined);
|
|
108
|
+
onProcess(undefined);
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function restartOwnedAndWait(child, executable, args, profileDirectory, connect, timeoutMs, onProcess) {
|
|
113
|
+
await terminateOwnedProcess(child);
|
|
114
|
+
onProcess(undefined);
|
|
115
|
+
return launchManagedAndWait(executable, args, profileDirectory, connect, timeoutMs, onProcess);
|
|
116
|
+
}
|
|
117
|
+
/** 仅在用户确认重启后清理同一用户、同一安装路径的桌面进程。 */
|
|
118
|
+
async function terminateWindowsDesktop(executable) {
|
|
119
|
+
if (process.platform !== "win32")
|
|
120
|
+
return [];
|
|
121
|
+
const script = `
|
|
122
|
+
$ErrorActionPreference = 'Stop'
|
|
123
|
+
$target = '${(0, node_path_1.resolve)(executable).replace(/'/g, "''")}'
|
|
124
|
+
$sid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
125
|
+
$processes = @(Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -ieq $target } | Where-Object { (Invoke-CimMethod -InputObject $_ -MethodName GetOwnerSid).Sid -eq $sid })
|
|
126
|
+
$ids = @($processes.ProcessId)
|
|
127
|
+
$roots = @($processes | Where-Object { $ids -notcontains $_.ParentProcessId })
|
|
128
|
+
foreach ($item in $roots) {
|
|
129
|
+
$current = Get-CimInstance Win32_Process -Filter "ProcessId=$($item.ProcessId)"
|
|
130
|
+
if (-not $current -or $current.CreationDate -ne $item.CreationDate) { continue }
|
|
131
|
+
& taskkill.exe /PID $item.ProcessId /T /F | Out-Null
|
|
132
|
+
if ($LASTEXITCODE -ne 0 -and (Get-Process -Id $item.ProcessId -ErrorAction SilentlyContinue)) { throw "无法结束桌面进程 $($item.ProcessId)" }
|
|
133
|
+
$remaining = Get-Process -Id $item.ProcessId -ErrorAction SilentlyContinue
|
|
134
|
+
if ($remaining -and -not $remaining.WaitForExit(10000)) { throw "等待桌面进程退出超时 $($item.ProcessId)" }
|
|
135
|
+
}
|
|
136
|
+
ConvertTo-Json -Compress -InputObject @($roots | ForEach-Object { [int]$_.ProcessId })
|
|
137
|
+
`;
|
|
138
|
+
const { stdout } = await execFileAsync("powershell.exe", ["-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { timeout: 30_000, windowsHide: true });
|
|
139
|
+
return JSON.parse(stdout.trim());
|
|
140
|
+
}
|
|
141
|
+
async function terminateOwnedProcess(child, platform = process.platform, run = async (file, args) => execFileAsync(file, args, { timeout: 10_000 })) {
|
|
142
|
+
const pid = child?.pid;
|
|
143
|
+
if (!Number.isSafeInteger(pid) || !pid || child?.exitCode !== null && child?.exitCode !== undefined || child?.signalCode)
|
|
144
|
+
return;
|
|
145
|
+
if (platform === "win32") {
|
|
146
|
+
try {
|
|
147
|
+
await run("taskkill.exe", ["/PID", String(pid), "/T", "/F"]);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (!child.exitCode && !child.signalCode && error.code !== "ESRCH")
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
try {
|
|
156
|
+
process.kill(pid, "SIGTERM");
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
if (error.code !== "ESRCH")
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function findChatgptExecutable(configured) {
|
|
165
|
+
if (configured?.trim() && (0, node_fs_1.existsSync)((0, node_path_1.resolve)(configured)))
|
|
166
|
+
return (0, node_path_1.resolve)(configured);
|
|
167
|
+
if (process.platform !== "win32")
|
|
168
|
+
return undefined;
|
|
169
|
+
const script = "$candidates = foreach ($package in (Get-AppxPackage | Where-Object { $_.Name -like 'OpenAI.Codex*' })) { $appDir = Join-Path $package.InstallLocation 'app'; $path = Join-Path $appDir 'ChatGPT.exe'; if (-not (Test-Path -LiteralPath $path)) { $path = Get-ChildItem -LiteralPath $appDir -Filter 'ChatGPT*.exe' -File -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName }; if ($path) { [pscustomobject]@{ Version = [version]$package.Version; Executable = $path } } }; $candidate = $candidates | Sort-Object Version -Descending | Select-Object -First 1; if ($candidate) { $candidate.Executable }";
|
|
170
|
+
try {
|
|
171
|
+
const { stdout } = await execFileAsync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { timeout: 10_000 });
|
|
172
|
+
return stdout.trim().split(/\r?\n/).at(-1)?.trim() || undefined;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
86
178
|
async function waitFor(action, timeoutMs, intervalMs = 500) {
|
|
87
179
|
const deadline = Date.now() + timeoutMs;
|
|
88
180
|
let last;
|
|
@@ -97,4 +189,3 @@ async function waitFor(action, timeoutMs, intervalMs = 500) {
|
|
|
97
189
|
}
|
|
98
190
|
throw last instanceof Error ? last : new Error("桌面程序启动超时");
|
|
99
191
|
}
|
|
100
|
-
const node_child_process_1 = require("node:child_process");
|
|
@@ -7,6 +7,7 @@ const cdp_client_1 = require("./cdp-client");
|
|
|
7
7
|
const session_history_1 = require("./session-history");
|
|
8
8
|
const plugin_1 = require("../../plugin");
|
|
9
9
|
const desktop_launch_1 = require("../desktop-launch");
|
|
10
|
+
const node_os_1 = require("node:os");
|
|
10
11
|
exports.doubaoManifest = {
|
|
11
12
|
id: "doubao",
|
|
12
13
|
version: "1.0.20",
|
|
@@ -35,6 +36,7 @@ class DoubaoPlugin {
|
|
|
35
36
|
}
|
|
36
37
|
disposer;
|
|
37
38
|
reconnect;
|
|
39
|
+
ownedProcess;
|
|
38
40
|
async activate(context) {
|
|
39
41
|
this.dataDir = context.dataDir;
|
|
40
42
|
this.bindings.clear();
|
|
@@ -68,20 +70,18 @@ class DoubaoPlugin {
|
|
|
68
70
|
}
|
|
69
71
|
catch (first) {
|
|
70
72
|
if (context.config.autoStart !== false && !this.injectedClient && process.platform === "win32") {
|
|
71
|
-
const port = (0, plugin_1.resolveRuntimePort)(context.config, context.runtime, "doubao", "DOUBAO_CDP_PORT", 9227);
|
|
72
73
|
const executable = await (0, desktop_launch_1.findWindowsExecutable)("Doubao.exe", typeof context.config.executablePath === "string" ? context.config.executablePath : undefined);
|
|
73
|
-
if (executable)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
74
|
+
if (!executable)
|
|
75
|
+
throw new Error("未找到 Doubao.exe,请配置 executablePath");
|
|
76
|
+
const port = (0, plugin_1.resolveRuntimePort)(context.config, context.runtime, "doubao", "DOUBAO_CDP_PORT", 9227);
|
|
77
|
+
context.logger?.warn?.(`正在恢复豆包:${executable},等待 CDP ${port}`);
|
|
78
|
+
try {
|
|
79
|
+
await (0, desktop_launch_1.restartOwnedAndWait)(this.ownedProcess, executable, [`--remote-debugging-port=${port}`], (0, node_path_1.join)(this.dataDir ?? (0, node_path_1.join)((0, node_os_1.homedir)(), ".agent-bridge", "doubao"), "desktop-profile"), () => this.client.connect(), typeof context.config.startupTimeoutMs === "number" ? context.config.startupTimeoutMs : 30_000, child => { this.ownedProcess = child; });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
first = new Error(`已启动豆包,但 CDP ${port} 未就绪:${error instanceof Error ? error.message : String(error)}`);
|
|
82
84
|
}
|
|
83
|
-
else
|
|
84
|
-
first = new Error("未找到 Doubao.exe,请配置 executablePath");
|
|
85
85
|
}
|
|
86
86
|
throw first;
|
|
87
87
|
}
|
|
@@ -106,6 +106,8 @@ class DoubaoPlugin {
|
|
|
106
106
|
this.disposer = undefined;
|
|
107
107
|
this.reconnect = undefined;
|
|
108
108
|
await this.client?.close();
|
|
109
|
+
await (0, desktop_launch_1.terminateOwnedProcess)(this.ownedProcess).catch(() => undefined);
|
|
110
|
+
this.ownedProcess = undefined;
|
|
109
111
|
this.client = undefined;
|
|
110
112
|
this.state = "stopped";
|
|
111
113
|
}
|
|
@@ -11,6 +11,7 @@ const commands = [
|
|
|
11
11
|
{ name: "status", summary: "查看飞书通道状态", usage: "mab feishu status" },
|
|
12
12
|
{ name: "list", summary: "列出机器人连接状态和关联 Agent", usage: "mab feishu list" },
|
|
13
13
|
{ name: "agents", summary: "列出已启动的 Agent 插件", usage: "mab feishu agents" },
|
|
14
|
+
{ name: "configure", summary: "保存机器人凭据并关联办公 Agent", usage: "mab feishu configure <app-id> [--agent <plugin-id>]", secretPrompt: "请输入飞书 App Secret" },
|
|
14
15
|
{ name: "bind", summary: "将已连接的机器人关联到 Agent", usage: "mab feishu bind <bot-id> <plugin-id>" },
|
|
15
16
|
{ name: "unbind", summary: "解除机器人关联并保留凭据", usage: "mab feishu unbind <bot-id>" },
|
|
16
17
|
];
|
|
@@ -149,8 +150,10 @@ class FeishuPlugin {
|
|
|
149
150
|
}
|
|
150
151
|
if (body.protocolVersion !== 1 || !Array.isArray(body.argv) || !body.argv.every((arg) => typeof arg === "string"))
|
|
151
152
|
return this.json(response, 400, { ok: false, message: "命令协议版本或 argv 无效" });
|
|
152
|
-
const [command, appId,
|
|
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>
|