mu-agent-bridge 1.0.30 → 1.0.32
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 +17 -1
- package/dist/installer/mu-agent-bridge.msi +0 -0
- package/dist/src-ts/agent-availability.js +14 -0
- package/dist/src-ts/channel-sessions.js +28 -5
- package/dist/src-ts/cli-commands.js +6 -2
- package/dist/src-ts/control-server.js +13 -2
- package/dist/src-ts/daemon-process.js +3 -2
- package/dist/src-ts/dsh-agent.js +4 -2
- package/dist/src-ts/plugin-manager.js +53 -3
- package/dist/src-ts/plugins/aun/index.js +52 -7
- package/dist/src-ts/plugins/aun/public/index.html +1 -1
- package/dist/src-ts/plugins/chatgpt/cdp-client.js +31 -2
- package/dist/src-ts/plugins/chatgpt/index.js +37 -1
- package/dist/src-ts/plugins/chatgpt/session-store.js +6 -2
- package/dist/src-ts/plugins/desktop-launch.js +13 -2
- package/dist/src-ts/plugins/doubao/cdp-client.js +29 -3
- package/dist/src-ts/plugins/dsh/index.js +1 -0
- package/dist/src-ts/plugins/feishu/index.js +35 -10
- 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/qwenwork/cdp-client.js +105 -31
- package/dist/src-ts/plugins/qwenwork/index.js +201 -7
- package/dist/src-ts/plugins/qwenwork/openai-adapter.js +12 -2
- package/dist/src-ts/plugins/workbuddy/cdp-client.js +10 -4
- package/dist/src-ts/plugins/workbuddy/index.js +139 -7
- package/dist/src-ts/secret-input.js +47 -0
- package/dist/tests-ts/agent-availability.test.js +27 -0
- package/dist/tests-ts/aun-default-owner-page.test.js +81 -0
- package/dist/tests-ts/aun-plugin.test.js +152 -8
- package/dist/tests-ts/channel-sessions.test.js +189 -0
- package/dist/tests-ts/chatgpt-plugin.test.js +104 -0
- package/dist/tests-ts/cli-commands.test.js +58 -0
- package/dist/tests-ts/control-server.test.js +51 -0
- package/dist/tests-ts/daemon-process.test.js +1 -0
- package/dist/tests-ts/desktop-launch.test.js +40 -0
- package/dist/tests-ts/doubao-plugin.test.js +10 -0
- package/dist/tests-ts/dsh-agent.test.js +25 -0
- package/dist/tests-ts/feishu-page.test.js +10 -3
- package/dist/tests-ts/feishu-plugin.test.js +62 -1
- package/dist/tests-ts/installer-assets.test.js +84 -0
- package/dist/tests-ts/plugin-manager.test.js +170 -2
- package/dist/tests-ts/qwenwork-plugin.test.js +443 -0
- package/dist/tests-ts/qwenwork-sse.test.js +46 -0
- package/dist/tests-ts/workbuddy-plugin.test.js +278 -3
- package/package.json +2 -1
- package/plugins/aun/config.schema.json +1 -0
- package/plugins/aun/dist/src-ts/plugins/aun/index.js +52 -7
- package/plugins/aun/dist/src-ts/plugins/aun/public/index.html +1 -1
- package/plugins/aun/package.json +1 -1
- package/plugins/aun/plugin.json +1 -1
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +31 -2
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +37 -1
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/session-store.js +6 -2
- package/plugins/chatgpt/package.json +1 -1
- package/plugins/chatgpt/plugin.json +1 -1
- package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +13 -2
- package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +29 -3
- package/plugins/dsh/dist/src-ts/dsh-agent.js +4 -2
- package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +1 -0
- package/plugins/dsh/dist/src-ts/plugins/qwenwork/openai-adapter.js +12 -2
- package/plugins/feishu/dist/src-ts/plugins/feishu/index.js +35 -10
- 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/config.schema.json +3 -0
- package/plugins/qwenwork/dist/src-ts/plugins/desktop-launch.js +13 -2
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +105 -31
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +201 -7
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/openai-adapter.js +12 -2
- package/plugins/qwenwork/package.json +1 -1
- package/plugins/qwenwork/plugin.json +2 -2
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +10 -4
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +139 -7
- package/plugins/workbuddy/plugin.json +2 -2
package/README.md
CHANGED
|
@@ -141,4 +141,20 @@ Windows 路径时,可设置 `DSH_INSTALL_DIR`。
|
|
|
141
141
|
|
|
142
142
|
## AUN 身份管理
|
|
143
143
|
|
|
144
|
-
创建、连接、关联和 AUN 通信统一由独立 `agent-bridge-aun` 插件完成。安装并启动 AUN 后,在 ABWeb 的“通道 → AUN”创建或选择身份,再将已连接 AID 关联到已启动的 Agent;用 `mab status` 查看状态。Owner AID 仅作本地归属配置,不代表 AUN Owner 关系已激活。完整步骤见 [AUN 插件与 AID 身份管理](docs/aun-plugin-identities.md)。
|
|
144
|
+
创建、连接、关联和 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)。
|
|
145
|
+
|
|
146
|
+
### 飞书凭据与办公 Agent 关联
|
|
147
|
+
|
|
148
|
+
在 ABWeb 的“飞书通道 → 手动配置凭据”中填写 App ID、App Secret,并选择“关联办公 Agent”。Secret 使用密码框显示;已保存的密钥保持掩码,不通过状态接口返回。WorkBuddy、千问办公、豆包根据运行后台的机器上是否存在可执行文件检测,文件缺失或插件未就绪的选项置灰。自定义安装位置请在对应插件配置中填写 `executablePath`。
|
|
149
|
+
|
|
150
|
+
也可以使用 CLI,Secret 在命令执行后隐藏输入,不写在命令参数中:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
mab feishu configure cli_你的应用ID --agent workbuddy
|
|
154
|
+
mab feishu agents
|
|
155
|
+
mab feishu list
|
|
156
|
+
mab feishu bind cli_你的应用ID workbuddy
|
|
157
|
+
mab feishu unbind cli_你的应用ID
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`configure` 保存凭据及可选关联后,需要在 ABWeb 重载飞书插件以连接机器人。非交互终端请使用网页密码框;不要把 Secret 写进命令行、脚本或仓库。
|
|
Binary file
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.agentAvailability = agentAvailability;
|
|
4
|
+
const desktop_launch_1 = require("./plugins/desktop-launch");
|
|
5
|
+
const workbuddy_1 = require("./plugins/workbuddy");
|
|
6
|
+
async function agentAvailability(id, config, status) {
|
|
7
|
+
const configured = typeof config.executablePath === "string" ? config.executablePath : undefined;
|
|
8
|
+
const executable = id === "workbuddy" ? (0, workbuddy_1.findExecutable)(configured)
|
|
9
|
+
: id === "qwenwork" ? await (0, desktop_launch_1.findWindowsExecutable)("QwenWorkCN.exe", configured)
|
|
10
|
+
: id === "doubao" ? await (0, desktop_launch_1.findWindowsExecutable)("Doubao.exe", configured) : true;
|
|
11
|
+
if (!executable)
|
|
12
|
+
return { available: false, reason: "未找到可执行文件" };
|
|
13
|
+
return { available: status === "ready", reason: status === "ready" ? "" : "插件未就绪" };
|
|
14
|
+
}
|
|
@@ -4,6 +4,7 @@ exports.ChannelSessions = void 0;
|
|
|
4
4
|
const node_crypto_1 = require("node:crypto");
|
|
5
5
|
const promises_1 = require("node:fs/promises");
|
|
6
6
|
const node_path_1 = require("node:path");
|
|
7
|
+
const unsupportedCommands = /^(?:\/(?:s|session|slist|name|rename|del|status|stop|pause|resume|compact|fork|rewind|rw|repair|renew))(?:[ \t]+[^\r\n]*)?$/;
|
|
7
8
|
class ChannelSessions {
|
|
8
9
|
directory;
|
|
9
10
|
sessions = {};
|
|
@@ -21,7 +22,7 @@ class ChannelSessions {
|
|
|
21
22
|
throw new Error("通道会话文件格式无效");
|
|
22
23
|
for (const [key, value] of Object.entries(data)) {
|
|
23
24
|
const entry = value;
|
|
24
|
-
if (!entry || typeof entry.sessionId !== "string" || typeof entry.source !== "string" || typeof entry.agentPlugin !== "string" || typeof entry.updatedAt !== "string")
|
|
25
|
+
if (!entry || typeof entry.sessionId !== "string" || typeof entry.source !== "string" || typeof entry.agentPlugin !== "string" || typeof entry.updatedAt !== "string" || (entry.newSession !== undefined && typeof entry.newSession !== "boolean"))
|
|
25
26
|
throw new Error("通道会话记录无效");
|
|
26
27
|
this.sessions[key] = entry;
|
|
27
28
|
}
|
|
@@ -34,15 +35,37 @@ class ChannelSessions {
|
|
|
34
35
|
await this.loaded;
|
|
35
36
|
}
|
|
36
37
|
async list() { await this.load(); return Object.values(this.sessions).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); }
|
|
37
|
-
async run(message, agentPlugin, call) {
|
|
38
|
+
async run(message, agentPlugin, call, control) {
|
|
38
39
|
const source = JSON.stringify([message.channel, message.recipientId || "", message.chatId || "", message.peerId, message.threadId || ""]);
|
|
40
|
+
const command = message.text?.trim().match(/^\/new(?:[ \t]+([^\r\n]+))?$/);
|
|
41
|
+
const unsupported = message.text?.trim().match(unsupportedCommands);
|
|
42
|
+
if (unsupported)
|
|
43
|
+
return { text: `会话命令 ${unsupported[0].split(/[ \t]/, 1)[0]} 本期暂不支持` };
|
|
44
|
+
if (/^\/clear(?:[ \t]+[^\r\n]*)?$/.test(message.text?.trim() ?? ""))
|
|
45
|
+
return { text: "/clear 已移除,请使用 /new 新建会话" };
|
|
46
|
+
if (message.text?.trim() === "/help")
|
|
47
|
+
return { text: [
|
|
48
|
+
control ? "/new [名称]:新建并切换会话" : "/new [名称]:当前 Agent 插件不支持新建会话",
|
|
49
|
+
"话题中不支持 /new;/renew 及其他会话命令当前暂不支持",
|
|
50
|
+
"/clear 已移除,请使用 /new",
|
|
51
|
+
].join("\n") };
|
|
52
|
+
if (command && message.threadId)
|
|
53
|
+
return { text: "话题中不支持 /new,请使用 /renew 轮换话题会话(当前尚未支持)" };
|
|
54
|
+
if (command && !control)
|
|
55
|
+
return { text: "当前 Agent 插件不支持新建会话" };
|
|
56
|
+
if (command && this.pending.has(source))
|
|
57
|
+
throw new Error("当前会话正在执行任务,请等待完成后再新建会话");
|
|
39
58
|
const previous = this.pending.get(source) ?? Promise.resolve();
|
|
40
59
|
const turn = previous.catch(() => { }).then(async () => {
|
|
41
60
|
await this.load();
|
|
42
61
|
const entry = this.sessions[source];
|
|
43
|
-
const sessionId = entry?.agentPlugin === agentPlugin ? entry.sessionId : (0, node_crypto_1.randomUUID)();
|
|
44
|
-
const response =
|
|
45
|
-
|
|
62
|
+
const sessionId = !command && entry?.agentPlugin === agentPlugin ? entry.sessionId : (0, node_crypto_1.randomUUID)();
|
|
63
|
+
const response = command
|
|
64
|
+
? await control({ operation: "new", sessionId, ...(command[1]?.trim() ? { name: command[1].trim() } : {}) })
|
|
65
|
+
: await call(sessionId, !entry || entry.agentPlugin !== agentPlugin || entry.newSession === true);
|
|
66
|
+
if (command && (typeof response.sessionId !== "string" || !response.sessionId.trim()))
|
|
67
|
+
throw new Error("插件未返回有效的新会话标识,原会话绑定保持不变");
|
|
68
|
+
const next = { source, agentPlugin, sessionId: response.sessionId || sessionId, updatedAt: new Date().toISOString(), ...(command ? { newSession: true } : {}) };
|
|
46
69
|
const save = this.writes.catch(() => { }).then(async () => {
|
|
47
70
|
const snapshot = { ...this.sessions, [source]: next };
|
|
48
71
|
await (0, promises_1.mkdir)(this.directory, { recursive: true });
|
|
@@ -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)
|
|
@@ -132,12 +132,21 @@ class ControlServer {
|
|
|
132
132
|
return this.json(res, 200, { ok: true, token, expiresAt });
|
|
133
133
|
}
|
|
134
134
|
const authorization = req.headers.authorization;
|
|
135
|
-
const authorized = authorization === `Bearer ${this.options.token}` || (authorization?.startsWith("Bearer ") && (this.pairedTokens.get((0, node_crypto_1.createHash)("sha256").update(authorization.slice(7)).digest("hex")) ?? 0) > Date.now());
|
|
135
|
+
const authorized = (Boolean(this.options.token) && authorization === `Bearer ${this.options.token}`) || (authorization?.startsWith("Bearer ") && (this.pairedTokens.get((0, node_crypto_1.createHash)("sha256").update(authorization.slice(7)).digest("hex")) ?? 0) > Date.now());
|
|
136
136
|
if (this.options.token && !isLoopback(req.socket.remoteAddress) && !authorized && !(method === "GET" && url.pathname.startsWith("/abweb")))
|
|
137
137
|
return this.json(res, 401, { error: "unauthorized" });
|
|
138
138
|
if (parts[0] === "api" && parts[1] === "agents" && parts[2] && parts[3] === "v1") {
|
|
139
139
|
const id = decodeURIComponent(parts[2]);
|
|
140
|
-
|
|
140
|
+
if (req.headers["x-evolcore-session-id"] !== undefined && !authorized && !isLoopback(req.socket.remoteAddress))
|
|
141
|
+
return this.json(res, 401, { error: "会话身份仅允许已认证或本机调用方使用" });
|
|
142
|
+
const adapter = new openai_adapter_1.OpenAIAdapter(async (task, _signal, context) => {
|
|
143
|
+
if (!context)
|
|
144
|
+
return (await this.options.manager.handle(id, { task })).text;
|
|
145
|
+
// 客户端标识只在认证调用方或本机信任范围内生效,不能直接恢复其他入口的会话。
|
|
146
|
+
const caller = authorized ? (0, node_crypto_1.createHash)("sha256").update(authorization).digest("hex") : "loopback";
|
|
147
|
+
const sessionId = `openai:${(0, node_crypto_1.createHash)("sha256").update(JSON.stringify([caller, id, context.agentAid ?? "", context.peerId ?? "", context.sessionId])).digest("hex")}`;
|
|
148
|
+
return (await this.options.manager.handle(id, { task, sessionId })).text;
|
|
149
|
+
}, { model: id, prefix: `/api/agents/${encodeURIComponent(id)}/v1`, onExchange: (direction, text, peer, aid) => this.options.manager.recordMessage(id, direction, text, peer, aid) });
|
|
141
150
|
await adapter.handle(req, res);
|
|
142
151
|
return;
|
|
143
152
|
}
|
|
@@ -212,6 +221,8 @@ class ControlServer {
|
|
|
212
221
|
const result = await identity.create(body.aid, body.ownerAid);
|
|
213
222
|
return this.json(res, result.status === "等待 Owner 确认" ? 202 : result.status === "existing" ? 200 : 201, result);
|
|
214
223
|
}
|
|
224
|
+
if (method === "GET" && url.pathname === "/api/agents/availability")
|
|
225
|
+
return this.json(res, 200, { agents: await this.options.manager.availableAgents() });
|
|
215
226
|
if (method === "GET" && url.pathname === "/api/plugins")
|
|
216
227
|
return this.json(res, 200, { ok: true, plugins: this.options.manager.list() });
|
|
217
228
|
if (method === "GET" && url.pathname === "/api/plugins/catalog")
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.isDaemonCommand = isDaemonCommand;
|
|
4
4
|
/** 只识别直接运行本安装 CLI 的后台进程,避免旧地址文件中的 PID 被其他程序复用。 */
|
|
5
5
|
function isDaemonCommand(commandLine, cli) {
|
|
6
|
-
const
|
|
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
|
}
|
package/dist/src-ts/dsh-agent.js
CHANGED
|
@@ -18,7 +18,7 @@ class DshAgent {
|
|
|
18
18
|
if (!request.task.trim())
|
|
19
19
|
throw new agent_1.AgentError("DSH task must not be empty");
|
|
20
20
|
const args = ["--expose-internals", this.paths.cli, "--profile", "headless", "--patch", this.paths.patch, "--json"];
|
|
21
|
-
if (request.sessionId)
|
|
21
|
+
if (request.sessionId && !request.newSession)
|
|
22
22
|
args.push("--session-id", request.sessionId);
|
|
23
23
|
args.push("-");
|
|
24
24
|
const child = this.spawnProcess(this.paths.executable, args, {
|
|
@@ -29,7 +29,7 @@ class DshAgent {
|
|
|
29
29
|
child.stdin.end(request.task, "utf8");
|
|
30
30
|
let output = "";
|
|
31
31
|
let errors = "";
|
|
32
|
-
let sessionId = request.sessionId;
|
|
32
|
+
let sessionId = request.newSession ? undefined : request.sessionId;
|
|
33
33
|
let finalText = "";
|
|
34
34
|
child.stdout.setEncoding?.("utf8");
|
|
35
35
|
child.stderr.setEncoding?.("utf8");
|
|
@@ -64,6 +64,8 @@ class DshAgent {
|
|
|
64
64
|
});
|
|
65
65
|
if (!finalText)
|
|
66
66
|
throw new agent_1.AgentError("DSH returned no final text");
|
|
67
|
+
if (request.newSession && !sessionId)
|
|
68
|
+
throw new agent_1.AgentError("DSH 未返回真实新会话标识,无法确认后续续聊目标");
|
|
67
69
|
return { text: finalText, sessionId };
|
|
68
70
|
}
|
|
69
71
|
}
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.PluginManager = void 0;
|
|
7
|
+
const agent_availability_1 = require("./agent-availability");
|
|
7
8
|
const promises_1 = require("node:fs/promises");
|
|
8
9
|
const node_fs_1 = require("node:fs");
|
|
9
10
|
const node_module_1 = require("node:module");
|
|
@@ -24,6 +25,7 @@ class PluginManager {
|
|
|
24
25
|
options;
|
|
25
26
|
controlUrl;
|
|
26
27
|
plugins = new Map();
|
|
28
|
+
upgrades = new Map();
|
|
27
29
|
httpRoutes = new Map();
|
|
28
30
|
root;
|
|
29
31
|
messages = [];
|
|
@@ -160,6 +162,22 @@ class PluginManager {
|
|
|
160
162
|
})]);
|
|
161
163
|
}
|
|
162
164
|
async upgradePackage(id, packageName = `agent-bridge-${id}`, version) {
|
|
165
|
+
const pending = this.upgrades.get(id);
|
|
166
|
+
if (pending) {
|
|
167
|
+
if (pending.packageName === packageName && pending.version === version)
|
|
168
|
+
return pending.task;
|
|
169
|
+
throw new agent_1.AgentError("插件正在升级,请等待完成后再请求其他版本");
|
|
170
|
+
}
|
|
171
|
+
const task = this.performUpgrade(id, packageName, version);
|
|
172
|
+
this.upgrades.set(id, { packageName, version, task });
|
|
173
|
+
try {
|
|
174
|
+
return await task;
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
this.upgrades.delete(id);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async performUpgrade(id, packageName, version) {
|
|
163
181
|
if (!/^agent-bridge-[a-z0-9-]+$/.test(packageName))
|
|
164
182
|
throw new agent_1.AgentError("仅支持 agent-bridge-* npm 包名");
|
|
165
183
|
if (version && !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version))
|
|
@@ -232,6 +250,8 @@ class PluginManager {
|
|
|
232
250
|
get(id) { return this.plugins.get(id); }
|
|
233
251
|
recentMessages(id) { return this.messages.filter((item) => !id || item.pluginId === id).slice(-200).reverse(); }
|
|
234
252
|
recordMessage(pluginId, direction, text, peer, agentAid) {
|
|
253
|
+
if (direction === "error" && isNoisyAgentError(text))
|
|
254
|
+
return;
|
|
235
255
|
this.messages.push({ pluginId, direction, text: text.slice(0, 10000), time: new Date().toISOString(), ...(peer ? { peer } : {}), ...(agentAid ? { agentAid } : {}) });
|
|
236
256
|
if (this.messages.length > 200)
|
|
237
257
|
this.messages.shift();
|
|
@@ -357,7 +377,11 @@ class PluginManager {
|
|
|
357
377
|
const target = message.agentPlugin || "qwenwork";
|
|
358
378
|
this.recordMessage(target, "request", message.text, message.peerId);
|
|
359
379
|
try {
|
|
360
|
-
const
|
|
380
|
+
const instance = this.plugins.get(target)?.instance;
|
|
381
|
+
const control = instance?.sessionOperations?.includes("new")
|
|
382
|
+
? (request) => this.controlSession(target, request)
|
|
383
|
+
: undefined;
|
|
384
|
+
const response = await this.sessions.run(message, target, (sessionId, newSession) => this.handle(target, { task: message.text, sessionId, ...(newSession ? { newSession: true } : {}) }), control);
|
|
361
385
|
await message.reply(response.text);
|
|
362
386
|
this.recordMessage(target, "reply", response.text, message.peerId);
|
|
363
387
|
}
|
|
@@ -503,6 +527,12 @@ class PluginManager {
|
|
|
503
527
|
throw error;
|
|
504
528
|
throw new agent_1.AgentError(`插件配置校验失败:${error instanceof Error ? error.message : String(error)}`);
|
|
505
529
|
} }
|
|
530
|
+
async availableAgents() {
|
|
531
|
+
return Promise.all(this.list().filter((plugin) => plugin.type === "agent").map(async (plugin) => ({
|
|
532
|
+
id: plugin.id, displayName: plugin.displayName, status: plugin.status,
|
|
533
|
+
...await (0, agent_availability_1.agentAvailability)(plugin.id, await this.config(plugin.id), plugin.status),
|
|
534
|
+
})));
|
|
535
|
+
}
|
|
506
536
|
async health(id) { const loaded = this.plugins.get(id); if (!loaded)
|
|
507
537
|
throw new agent_1.AgentError(`unknown plugin: ${id}`); if (!loaded.instance)
|
|
508
538
|
return { status: loaded.record.status }; return loaded.instance.health(); }
|
|
@@ -517,6 +547,19 @@ class PluginManager {
|
|
|
517
547
|
}
|
|
518
548
|
return loaded.instance.handle(request);
|
|
519
549
|
}
|
|
550
|
+
async controlSession(id, request) {
|
|
551
|
+
const loaded = this.plugins.get(id);
|
|
552
|
+
if (!loaded?.instance || loaded.record.status !== "ready")
|
|
553
|
+
throw new agent_1.AgentError(`插件 ${id} 未就绪,无法控制会话`);
|
|
554
|
+
if (!loaded.instance.sessionOperations?.includes(request.operation))
|
|
555
|
+
throw new agent_1.AgentError(`插件 ${id} 不支持新建会话`);
|
|
556
|
+
const response = loaded.instance.controlSession
|
|
557
|
+
? await loaded.instance.controlSession(request)
|
|
558
|
+
: { text: `${loaded.record.displayName} 已创建新会话`, sessionId: request.sessionId };
|
|
559
|
+
if (typeof response.sessionId !== "string" || !response.sessionId.trim())
|
|
560
|
+
throw new agent_1.AgentError("插件未返回有效的新会话标识,原会话绑定保持不变");
|
|
561
|
+
return response;
|
|
562
|
+
}
|
|
520
563
|
async dispatchHttp(request, response) {
|
|
521
564
|
const method = (request.method ?? "GET").toUpperCase();
|
|
522
565
|
const pathname = new URL(request.url ?? "/", "http://localhost").pathname;
|
|
@@ -587,10 +630,13 @@ class PluginManager {
|
|
|
587
630
|
const packagedFile = (0, node_path_1.join)(record.path, record.manifest.entry);
|
|
588
631
|
const developmentFile = (0, node_path_1.resolve)(__dirname, "plugins", record.id, (0, node_path_1.basename)(record.manifest.entry));
|
|
589
632
|
const file = (0, node_fs_1.existsSync)(packagedFile) ? packagedFile : developmentFile;
|
|
590
|
-
//
|
|
633
|
+
// 同时刷新插件内部依赖,避免升级后仍运行旧版 CDP 客户端。
|
|
591
634
|
const req = (0, node_module_1.createRequire)(file);
|
|
592
635
|
const resolved = req.resolve(file);
|
|
593
|
-
|
|
636
|
+
const moduleRoot = (0, node_fs_1.existsSync)(packagedFile) ? (0, node_path_1.resolve)(record.path) : (0, node_path_1.dirname)(file);
|
|
637
|
+
for (const cached of Object.keys(req.cache))
|
|
638
|
+
if (cached === resolved || cached.startsWith(`${moduleRoot}${node_path_1.sep}`))
|
|
639
|
+
delete req.cache[cached];
|
|
594
640
|
const mod = req(resolved);
|
|
595
641
|
let value = mod.default ?? mod.plugin ?? mod;
|
|
596
642
|
// Entries may export a ready instance, a factory, or a class. Supporting all
|
|
@@ -819,6 +865,10 @@ class PluginManager {
|
|
|
819
865
|
} }
|
|
820
866
|
}
|
|
821
867
|
exports.PluginManager = PluginManager;
|
|
868
|
+
function isNoisyAgentError(text) {
|
|
869
|
+
const value = text.trim();
|
|
870
|
+
return /^(?:千问生成接口不可用(?:,已触发后台恢复)?|千问办公 UI 超时未返回回复|(?:API|接口)\s*(?:暂时)?不可用[。.!!]?)$/i.test(value);
|
|
871
|
+
}
|
|
822
872
|
function isUpdatePolicy(value) { return Boolean(value && typeof value === "object" && ["auto-install", "download-only", "manual"].includes(value.mode)); }
|
|
823
873
|
function pluginProcessName(id) {
|
|
824
874
|
return { qwenwork: "QwenWorkCN.exe", doubao: "Doubao.exe", workbuddy: "WorkBuddy.exe", chatgpt: "ChatGPT/Codex" }[id];
|
|
@@ -10,12 +10,13 @@ const node_os_1 = require("node:os");
|
|
|
10
10
|
const node_url_1 = require("node:url");
|
|
11
11
|
const messages_1 = require("./messages");
|
|
12
12
|
const manifest = {
|
|
13
|
-
id: "aun", version: "1.0.
|
|
13
|
+
id: "aun", version: "1.0.22", type: "channel", displayName: "AUN 通道",
|
|
14
14
|
entry: "dist/src-ts/plugins/aun/index.js", apiVersion: "1", process: "in-process",
|
|
15
15
|
capabilities: ["channel", "channel.identity.create", "channel.receive", "channel.send", "health", "cli"],
|
|
16
16
|
configSchema: "config.schema.json",
|
|
17
17
|
managementPage: "/abweb/channels/aun",
|
|
18
18
|
};
|
|
19
|
+
const newSessionCommand = /^\/new(?:[ \t]+[^\r\n]+)?$/;
|
|
19
20
|
async function loadSdk() {
|
|
20
21
|
const resolver = (0, node_module_1.createRequire)(__filename);
|
|
21
22
|
const dynamicImport = new Function("url", "return import(url)");
|
|
@@ -36,7 +37,8 @@ const cliCommands = [
|
|
|
36
37
|
{ name: "status", summary: "查看 AUN 通道状态", usage: "mab aun status" },
|
|
37
38
|
{ name: "list", summary: "查看已连接 AID 和 Agent 关联", usage: "mab aun list" },
|
|
38
39
|
{ name: "agents", summary: "列出当前已就绪的 Agent 插件", usage: "mab aun agents" },
|
|
39
|
-
{ name: "
|
|
40
|
+
{ name: "default-owner", summary: "查询或设置默认 Owner AID", usage: "mab aun default-owner [set <owner-aid>]", examples: ["mab aun default-owner", "mab aun default-owner set owner.agentid.cn"] },
|
|
41
|
+
{ name: "create", summary: "创建并连接 AID,可选关联 Agent", usage: "mab aun create <aid> [--owner <owner-aid>] [--agent <plugin-id>]", examples: ["mab aun create new.agentid.cn --owner owner.agentid.cn --agent qwenwork"] },
|
|
40
42
|
{ name: "bind", summary: "将 AID 关联到 Agent", usage: "mab aun bind <aid> <plugin-id>", examples: ["mab aun bind new.agentid.cn qwenwork"] },
|
|
41
43
|
{ name: "unbind", summary: "解除 Agent 与 AID 的关联", usage: "mab aun unbind <plugin-id>", examples: ["mab aun unbind qwenwork"] },
|
|
42
44
|
];
|
|
@@ -58,6 +60,7 @@ class AunPlugin {
|
|
|
58
60
|
clients = new Map();
|
|
59
61
|
identities = new Set();
|
|
60
62
|
legacyCreatedAids = new Set();
|
|
63
|
+
createdAids = new Set();
|
|
61
64
|
bindings = {};
|
|
62
65
|
bindingTimes = {};
|
|
63
66
|
creating = false;
|
|
@@ -75,10 +78,11 @@ class AunPlugin {
|
|
|
75
78
|
}
|
|
76
79
|
identity = {
|
|
77
80
|
list: async () => {
|
|
81
|
+
const own = new Set([...this.createdAids, ...this.legacyCreatedAids]);
|
|
78
82
|
return {
|
|
79
|
-
evolcore: [],
|
|
80
|
-
muAgentBridge: [...this.clients.keys()].sort(),
|
|
81
|
-
localMuAgentBridge: [...
|
|
83
|
+
evolcore: [...this.clients.keys()].filter((aid) => !own.has(aid)).sort(),
|
|
84
|
+
muAgentBridge: [...this.clients.keys()].filter((aid) => own.has(aid)).sort(),
|
|
85
|
+
localMuAgentBridge: [...own].filter((aid) => !this.clients.has(aid)).sort(),
|
|
82
86
|
};
|
|
83
87
|
},
|
|
84
88
|
create: async (aid, owner) => {
|
|
@@ -95,6 +99,8 @@ class AunPlugin {
|
|
|
95
99
|
const result = await this.store.register(aid);
|
|
96
100
|
if (!result.ok)
|
|
97
101
|
throw new Error(redact(result.error?.message ?? result.error?.code ?? "AID 注册失败"));
|
|
102
|
+
this.createdAids.add(aid);
|
|
103
|
+
await this.persist();
|
|
98
104
|
}
|
|
99
105
|
const loaded = this.store.load(aid);
|
|
100
106
|
if (!loaded.ok || !loaded.data)
|
|
@@ -180,6 +186,7 @@ class AunPlugin {
|
|
|
180
186
|
};
|
|
181
187
|
async persist() {
|
|
182
188
|
this.config.identities = [...this.identities].sort();
|
|
189
|
+
this.config.created_aids = [...new Set([...this.createdAids, ...this.legacyCreatedAids])].sort();
|
|
183
190
|
this.config.bindings = this.bindings;
|
|
184
191
|
this.config.binding_times = this.bindingTimes;
|
|
185
192
|
await (0, promises_1.writeFile)((0, node_path_1.join)(this.context.dataDir, "config.json"), `${JSON.stringify(this.config, null, 2)}\n`, { mode: 0o600 });
|
|
@@ -188,6 +195,7 @@ class AunPlugin {
|
|
|
188
195
|
this.context = context;
|
|
189
196
|
this.config = { ...context.config };
|
|
190
197
|
this.legacyCreatedAids.clear();
|
|
198
|
+
this.createdAids.clear();
|
|
191
199
|
try {
|
|
192
200
|
const created = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(context.dataDir, "..", "created-aids.json"), "utf8"));
|
|
193
201
|
if (Array.isArray(created))
|
|
@@ -199,6 +207,10 @@ class AunPlugin {
|
|
|
199
207
|
if (error.code !== "ENOENT")
|
|
200
208
|
context.logger.warn("读取旧版身份创建记录失败", { error: safeError(error) });
|
|
201
209
|
}
|
|
210
|
+
for (const aid of Array.isArray(this.config.created_aids) ? this.config.created_aids : []) {
|
|
211
|
+
if (typeof aid === "string" && aidPattern.test(aid))
|
|
212
|
+
this.createdAids.add(aid);
|
|
213
|
+
}
|
|
202
214
|
for (const key of ["aid", "owner_aid", "aun_path", "encryption_seed", "root_ca_path", "allow_from", "agent_plugin"]) {
|
|
203
215
|
if (this.config[key] !== undefined && typeof this.config[key] !== "string")
|
|
204
216
|
throw new Error(`${key} 必须是字符串`);
|
|
@@ -487,6 +499,8 @@ class AunPlugin {
|
|
|
487
499
|
const agents = await this.agents();
|
|
488
500
|
return { message: agents.length ? `当前已就绪的 Agent 插件:\n${agents.map((agent) => `${agent.id}(${agent.displayName})`).join("\n")}` : "暂无已就绪的 Agent 插件", data: agents };
|
|
489
501
|
}
|
|
502
|
+
if (command === "default-owner")
|
|
503
|
+
return this.defaultOwnerCommand(args);
|
|
490
504
|
if (command === "create")
|
|
491
505
|
return this.createCommand(args);
|
|
492
506
|
if (command === "bind" && args.length === 2) {
|
|
@@ -522,10 +536,29 @@ class AunPlugin {
|
|
|
522
536
|
}
|
|
523
537
|
return { message: `已解除 ${pluginId} 的 AID 关联`, data: { agent: pluginId, unboundAids } };
|
|
524
538
|
}
|
|
525
|
-
if (!["status", "list", "agents", "create", "bind", "unbind"].includes(command ?? ""))
|
|
539
|
+
if (!["status", "list", "agents", "default-owner", "create", "bind", "unbind"].includes(command ?? ""))
|
|
526
540
|
throw new AunCommandError(`未知 AUN 命令:${command ?? ""}`);
|
|
527
541
|
throw new AunCommandError(`AUN 命令参数无效:${command}`);
|
|
528
542
|
}
|
|
543
|
+
async defaultOwnerCommand(args) {
|
|
544
|
+
const owner = typeof this.config.owner_aid === "string" ? this.config.owner_aid.trim() : "";
|
|
545
|
+
if (!args.length)
|
|
546
|
+
return { message: owner ? `默认 Owner:${owner}` : "未配置默认 Owner", data: { owner_aid: owner || undefined } };
|
|
547
|
+
if (args.length !== 2 || args[0] !== "set")
|
|
548
|
+
throw new AunCommandError("AUN 默认 Owner 命令参数无效");
|
|
549
|
+
const nextOwner = args[1].trim();
|
|
550
|
+
if (!aidPattern.test(nextOwner))
|
|
551
|
+
throw new AunCommandError("Owner AID 格式无效");
|
|
552
|
+
this.config.owner_aid = nextOwner;
|
|
553
|
+
try {
|
|
554
|
+
await this.persist();
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
this.config.owner_aid = owner;
|
|
558
|
+
throw error;
|
|
559
|
+
}
|
|
560
|
+
return { message: "默认 Owner 已保存", data: { configured: true } };
|
|
561
|
+
}
|
|
529
562
|
async createCommand(args) {
|
|
530
563
|
const aid = args[0] ?? "";
|
|
531
564
|
if (!aidPattern.test(aid))
|
|
@@ -544,8 +577,10 @@ class AunPlugin {
|
|
|
544
577
|
else
|
|
545
578
|
throw new AunCommandError(`不支持的创建参数:${option}`);
|
|
546
579
|
}
|
|
580
|
+
if (!owner)
|
|
581
|
+
owner = typeof this.config.owner_aid === "string" ? this.config.owner_aid.trim() : "";
|
|
547
582
|
if (!owner || !aidPattern.test(owner))
|
|
548
|
-
throw new AunCommandError("需要有效的 --owner AID");
|
|
583
|
+
throw new AunCommandError("需要有效的 --owner AID,或先设置默认 Owner");
|
|
549
584
|
if (agent) {
|
|
550
585
|
this.validatePluginId(agent);
|
|
551
586
|
await this.requireAgent(agent);
|
|
@@ -703,12 +738,14 @@ class AunPlugin {
|
|
|
703
738
|
const message = (0, messages_1.normalizeAunMessage)(payload, aid, typeof this.config.allow_from === "string" ? this.config.allow_from : undefined);
|
|
704
739
|
if (!message || !this.seen.add(message.messageId ? `${aid}:${message.messageId}` : ""))
|
|
705
740
|
return;
|
|
741
|
+
let replyAttempted = false;
|
|
706
742
|
const channelMessage = {
|
|
707
743
|
channel: "aun", recipientId: aid, peerId: message.peerId, messageId: message.messageId, text: message.text, threadId: message.threadId,
|
|
708
744
|
agentPlugin: this.bindings[aid] || (typeof this.config.agent_plugin === "string" && this.config.agent_plugin.trim() ? this.config.agent_plugin.trim() : "qwenwork"),
|
|
709
745
|
reply: async (text) => {
|
|
710
746
|
if (!text.trim())
|
|
711
747
|
return;
|
|
748
|
+
replyAttempted = true;
|
|
712
749
|
const payload = { type: "text", text };
|
|
713
750
|
if (message.threadId)
|
|
714
751
|
payload.thread_id = message.threadId;
|
|
@@ -728,6 +765,14 @@ class AunPlugin {
|
|
|
728
765
|
}
|
|
729
766
|
catch (error) {
|
|
730
767
|
this.context?.logger.warn("AUN 消息处理失败", { error: safeError(error) });
|
|
768
|
+
if (!replyAttempted && newSessionCommand.test(message.text.trim())) {
|
|
769
|
+
try {
|
|
770
|
+
await channelMessage.reply(`新建会话失败:${safeError(error)}`);
|
|
771
|
+
}
|
|
772
|
+
catch (replyError) {
|
|
773
|
+
this.context?.logger.warn("AUN 错误回复失败", { error: safeError(replyError) });
|
|
774
|
+
}
|
|
775
|
+
}
|
|
731
776
|
}
|
|
732
777
|
}
|
|
733
778
|
isE2eeError(error) {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<header><h1>AUN 身份</h1><button id="refresh" type="button">刷新</button></header>
|
|
7
7
|
<p id="status" role="status"></p>
|
|
8
8
|
<div class="table-scroll"><table><thead><tr><th>AID</th><th>来源</th><th>已关联 Agent</th><th>操作</th></tr></thead><tbody id="identities"></tbody></table></div>
|
|
9
|
-
<form id="create-form"><h2>创建 AID</h2><div class="fields"><label>新 AID<input name="aid" required placeholder="例如:example.agentid.cn"></label><label>Owner AID<input name="ownerAid"
|
|
9
|
+
<form id="create-form"><h2>创建 AID</h2><div class="fields"><label>新 AID<input name="aid" required placeholder="例如:example.agentid.cn"></label><label>Owner AID<input name="ownerAid" placeholder="例如:owner.agentid.cn"><small class="hint">留空时使用已配置的默认 Owner;未配置时需填写。Owner 保存为本地归属配置;不代表 AUN 协议中的 Owner 关系已激活。</small></label></div><button type="submit">创建</button></form>
|
|
10
10
|
<form id="config-form"><h2>通道配置</h2><div class="fields"><label>默认 Agent<select name="agent_plugin"></select></label><label>允许的来源<input name="allow_from"></label><label>AUN 存储路径<input name="aun_path"></label><label>加密种子<input name="encryption_seed" type="password" autocomplete="new-password" placeholder="留空则不修改"></label></div><button type="submit">保存配置</button></form>
|
|
11
11
|
</main>
|
|
12
12
|
<script src="/abweb/channels/aun/app.js"></script>
|
|
@@ -13,6 +13,7 @@ const sendButtonSelector = 'button[aria-label="Send"], button[aria-label="发送
|
|
|
13
13
|
const stopButtonSelector = 'button[aria-label="Stop"], button[aria-label="停止"]';
|
|
14
14
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
15
|
const sidebarSelector = "[data-app-action-sidebar-thread-id]";
|
|
16
|
+
const newSessionSelector = 'button,[role="button"]';
|
|
16
17
|
class ChatgptCdpClient {
|
|
17
18
|
socket;
|
|
18
19
|
tunnel;
|
|
@@ -45,7 +46,7 @@ class ChatgptCdpClient {
|
|
|
45
46
|
}
|
|
46
47
|
async activeSession() {
|
|
47
48
|
const entry = await this.evaluate(`(() => {
|
|
48
|
-
const item = document.querySelector('[data-app-action-sidebar-thread-selected="true"]');
|
|
49
|
+
const item = document.querySelector('[data-app-action-sidebar-thread-selected="true"], [data-app-action-sidebar-thread-active="true"]');
|
|
49
50
|
return item ? {
|
|
50
51
|
id: item.getAttribute('data-app-action-sidebar-thread-id'),
|
|
51
52
|
host: item.getAttribute('data-app-action-sidebar-thread-host-id'),
|
|
@@ -72,10 +73,38 @@ class ChatgptCdpClient {
|
|
|
72
73
|
throw new Error("ChatGPT 侧栏中找不到目标会话");
|
|
73
74
|
await this.waitFor(`(() => {
|
|
74
75
|
const item = ${locate};
|
|
75
|
-
return item?.getAttribute('data-app-action-sidebar-thread-selected') === 'true'
|
|
76
|
+
return (item?.getAttribute('data-app-action-sidebar-thread-selected') === 'true' || item?.getAttribute('data-app-action-sidebar-thread-active') === 'true')
|
|
76
77
|
&& Boolean(document.querySelector(${JSON.stringify(exports.editorSelector)}));
|
|
77
78
|
})()`, "ChatGPT 会话切换未确认,未发送消息");
|
|
78
79
|
}
|
|
80
|
+
async createSession(_name) {
|
|
81
|
+
const before = await this.activeSession();
|
|
82
|
+
const state = await this.evaluate(`JSON.stringify({ editor: document.querySelector(${JSON.stringify(exports.editorSelector)})?.innerText ?? null, turns: document.querySelectorAll(${JSON.stringify(exports.turnSelector)}).length, generating: Boolean(document.querySelector(${JSON.stringify(stopButtonSelector)})) })`);
|
|
83
|
+
const initial = JSON.parse(String(state));
|
|
84
|
+
if (initial.editor?.trim() || initial.generating)
|
|
85
|
+
throw new Error("ChatGPT 当前有未发送内容或正在生成回复,不能新建会话");
|
|
86
|
+
const clicked = await this.evaluate(`(() => {
|
|
87
|
+
const labels = /^(?:新聊天|新对话|new chat|new conversation)$/i;
|
|
88
|
+
const item = [...document.querySelectorAll(${JSON.stringify(newSessionSelector)})].find((node) => {
|
|
89
|
+
const label = [node.getAttribute('aria-label'), node.getAttribute('title'), node.textContent].filter(Boolean).join(' ').trim();
|
|
90
|
+
return labels.test(label);
|
|
91
|
+
});
|
|
92
|
+
if (!item || item.getAttribute('disabled') !== null || item.getAttribute('aria-disabled') === 'true') return false;
|
|
93
|
+
item.click();
|
|
94
|
+
return true;
|
|
95
|
+
})()`);
|
|
96
|
+
if (!clicked)
|
|
97
|
+
throw new Error("ChatGPT 新聊天入口未找到或不可用");
|
|
98
|
+
await this.waitFor(`(() => {
|
|
99
|
+
const selected = document.querySelector('[data-app-action-sidebar-thread-selected="true"], [data-app-action-sidebar-thread-active="true"]');
|
|
100
|
+
const id = selected?.getAttribute('data-app-action-sidebar-thread-id');
|
|
101
|
+
const beforeId = ${JSON.stringify(before?.id ?? null)};
|
|
102
|
+
const editor = document.querySelector(${JSON.stringify(exports.editorSelector)});
|
|
103
|
+
const turns = document.querySelectorAll(${JSON.stringify(exports.turnSelector)}).length;
|
|
104
|
+
return Boolean(editor && !editor.innerText.trim() && turns === 0 && (!beforeId || id !== beforeId));
|
|
105
|
+
})()`, "ChatGPT 新会话状态校验失败");
|
|
106
|
+
return this.activeSession();
|
|
107
|
+
}
|
|
79
108
|
async connect() {
|
|
80
109
|
if (this.socket)
|
|
81
110
|
return;
|
|
@@ -5,7 +5,7 @@ const cdp_client_1 = require("./cdp-client");
|
|
|
5
5
|
const session_store_1 = require("./session-store");
|
|
6
6
|
exports.chatgptManifest = {
|
|
7
7
|
id: "chatgpt",
|
|
8
|
-
version: "1.0.
|
|
8
|
+
version: "1.0.3",
|
|
9
9
|
displayName: "ChatGPT(Codex)",
|
|
10
10
|
type: "agent",
|
|
11
11
|
entry: "dist/index.js",
|
|
@@ -22,6 +22,7 @@ function port(value, fallback) {
|
|
|
22
22
|
class ChatgptPlugin {
|
|
23
23
|
options;
|
|
24
24
|
manifest = exports.chatgptManifest;
|
|
25
|
+
sessionOperations = [];
|
|
25
26
|
client;
|
|
26
27
|
failure;
|
|
27
28
|
remote = false;
|
|
@@ -62,6 +63,7 @@ class ChatgptPlugin {
|
|
|
62
63
|
timeoutMs: typeof config.timeoutMs === "number" && config.timeoutMs > 0 ? config.timeoutMs : 30_000,
|
|
63
64
|
ssh,
|
|
64
65
|
});
|
|
66
|
+
this.sessionOperations = this.client.createSession ? ["new"] : [];
|
|
65
67
|
try {
|
|
66
68
|
await this.client.connect();
|
|
67
69
|
}
|
|
@@ -93,6 +95,31 @@ class ChatgptPlugin {
|
|
|
93
95
|
this.queue = run.then(() => undefined, () => undefined);
|
|
94
96
|
return { text: await run, sessionId: request.sessionId };
|
|
95
97
|
}
|
|
98
|
+
async controlSession(request) {
|
|
99
|
+
if (request.operation !== "new")
|
|
100
|
+
throw new Error(`ChatGPT 不支持会话操作:${String(request.operation)}`);
|
|
101
|
+
const sessionId = request.sessionId?.trim();
|
|
102
|
+
if (!sessionId)
|
|
103
|
+
throw new Error("ChatGPT 新会话缺少 sessionId");
|
|
104
|
+
if (!this.client?.createSession || !this.client.listSessions || !this.store)
|
|
105
|
+
throw new Error("ChatGPT 当前传输不支持新建会话");
|
|
106
|
+
const run = this.queue.then(async () => {
|
|
107
|
+
const existing = new Set((await this.client.listSessions()).map(item => JSON.stringify([item.host, item.id])));
|
|
108
|
+
const created = await this.client.createSession(request.name);
|
|
109
|
+
if (created) {
|
|
110
|
+
this.store.remember(created, "", "");
|
|
111
|
+
this.store.bindings[sessionId] = created;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
delete this.store.bindings[sessionId];
|
|
115
|
+
this.store.pendingBindings[sessionId] = [...existing];
|
|
116
|
+
}
|
|
117
|
+
this.persist();
|
|
118
|
+
return { text: "ChatGPT 已创建新会话", sessionId };
|
|
119
|
+
});
|
|
120
|
+
this.queue = run.then(() => undefined, () => undefined);
|
|
121
|
+
return run;
|
|
122
|
+
}
|
|
96
123
|
persist() {
|
|
97
124
|
void this.store.save().catch(error => this.logger?.warn("ChatGPT 会话缓存保存失败", { error: String(error) }));
|
|
98
125
|
}
|
|
@@ -177,6 +204,15 @@ class ChatgptPlugin {
|
|
|
177
204
|
selected ??= await this.client.activeSession?.();
|
|
178
205
|
const message = explicit?.[2]?.trim() || request.task;
|
|
179
206
|
const reply = await this.client.direct(message, request.timeoutMs);
|
|
207
|
+
const baseline = request.sessionId ? store.pendingBindings[request.sessionId] : undefined;
|
|
208
|
+
if (baseline) {
|
|
209
|
+
const existing = new Set(baseline);
|
|
210
|
+
const created = (await this.client.listSessions?.() ?? []).filter(item => !existing.has(JSON.stringify([item.host, item.id])));
|
|
211
|
+
if (created.length !== 1)
|
|
212
|
+
throw new Error(`ChatGPT 新消息已发送,但新会话标识无法唯一确认(找到 ${created.length} 个)`);
|
|
213
|
+
selected = created[0];
|
|
214
|
+
delete store.pendingBindings[request.sessionId];
|
|
215
|
+
}
|
|
180
216
|
if (selected) {
|
|
181
217
|
const current = await this.client.activeSession?.().catch(() => undefined);
|
|
182
218
|
const entry = current?.id === selected.id && current.host === selected.host ? current : selected;
|