mu-agent-bridge 1.0.16 → 1.0.18
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 +6 -6
- package/dist/src-ts/cli.js +25 -3
- package/dist/src-ts/control-server.js +51 -5
- package/dist/src-ts/llprobe-qwen-bridge.js +1 -1
- package/dist/src-ts/plugin-manager.js +10 -2
- package/dist/src-ts/plugin.js +11 -0
- package/dist/src-ts/plugins/chatgpt/cdp-client.js +304 -0
- package/dist/src-ts/plugins/chatgpt/index.js +90 -0
- package/dist/src-ts/plugins/doubao/index.js +2 -1
- package/dist/src-ts/plugins/qwenwork/cdp-client.js +1 -1
- package/dist/src-ts/plugins/qwenwork/index.js +3 -2
- package/dist/src-ts/plugins/workbuddy/index.js +2 -1
- package/dist/tests-ts/bundled-package.test.js +1 -1
- package/dist/tests-ts/chatgpt-plugin.test.js +98 -0
- package/dist/tests-ts/control-server.test.js +16 -0
- package/dist/tests-ts/platform-integration.test.js +2 -2
- package/dist/tests-ts/qwenwork-plugin.test.js +17 -1
- package/package.json +1 -1
- package/plugins/chatgpt/config.schema.json +1 -0
- package/plugins/chatgpt/dist/src-ts/agent.js +10 -0
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +300 -0
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +90 -0
- package/plugins/chatgpt/plugin.json +1 -0
- package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +2 -1
- package/plugins/qwenwork/config.schema.json +1 -1
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +1 -1
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +3 -2
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +2 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# mu-agent-bridge 动态插件平台
|
|
2
2
|
|
|
3
3
|
运行时使用 TypeScript/Node.js,实现一个可动态发现、启动、停止、重载和安装插件的 daemon。
|
|
4
|
-
当前插件包括千问办公 `qwenwork`、DeepSeek Harness `dsh`、豆包 `doubao`、基于 CodeBuddy CLI 的 `workbuddy`,以及插件化管理后台
|
|
4
|
+
当前插件包括千问办公 `qwenwork`、DeepSeek Harness `dsh`、豆包 `doubao`、基于 CodeBuddy CLI 的 `workbuddy`、远程 ChatGPT/Codex CDP 插件 `chatgpt`,以及插件化管理后台
|
|
5
5
|
`abweb`。插件之间独立失败,管理 API 和 OpenAI 兼容接口由同一个本地控制服务提供。
|
|
6
6
|
|
|
7
7
|
## 快速开始
|
|
@@ -14,7 +14,7 @@ npm run start
|
|
|
14
14
|
|
|
15
15
|
默认监听 `http://127.0.0.1:26107`,管理页面为 `http://127.0.0.1:26107/abweb/`,管理 API 为 `/api/health`、`/api/plugins`,千问办公的
|
|
16
16
|
OpenAI 兼容 API 为 `/v1/models` 和 `/v1/chat/completions`。管理页面位于 `/abweb`。
|
|
17
|
-
千问办公插件使用 CDP `
|
|
17
|
+
千问办公插件使用 CDP `9228`;没有运行千问办公时,daemon 仍会启动,qwenwork 会显示为失败状态,
|
|
18
18
|
可以通过管理 API 重载。
|
|
19
19
|
|
|
20
20
|
安装后使用 `mab start` 启动。旧桥仍可使用 `17893`,不会与管理页面冲突。局域网访问时设置
|
|
@@ -40,14 +40,14 @@ abweb 的“配置码”栏;配对码 10 分钟有效,只能使用一次,
|
|
|
40
40
|
|
|
41
41
|
## npm 插件
|
|
42
42
|
|
|
43
|
-
默认包 `mu-agent-bridge` 已内置 abweb、AUN、千问办公、DSH
|
|
43
|
+
默认包 `mu-agent-bridge` 已内置 abweb、AUN、千问办公、DSH、豆包、飞书、WorkBuddy 和 ChatGPT/Codex 八个插件,直接安装即可使用:
|
|
44
44
|
|
|
45
45
|
```powershell
|
|
46
46
|
npm install -g mu-agent-bridge@latest
|
|
47
47
|
mab start
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
八个插件会从核心包的 `plugins/` 目录自动发现。插件仍分别发布为独立 npm 包,可用于单独测试、升级或替换版本。安装与发布说明见
|
|
51
51
|
[`docs/npm-publishing.md`](docs/npm-publishing.md)。
|
|
52
52
|
|
|
53
53
|
`mab start` 启动时会查询核心包和托管插件的 npm 最新版本,检查状态保存于 mab 状态目录的
|
|
@@ -72,7 +72,7 @@ abweb 插件目录从 npm 搜索带 `mu-agent-bridge-plugin` 关键词的包,
|
|
|
72
72
|
插件仍在 daemon 的同一个 Node.js 进程内运行。修改插件入口依赖后应重启 daemon,避免模块缓存
|
|
73
73
|
影响新代码。
|
|
74
74
|
|
|
75
|
-
npm
|
|
75
|
+
npm 尚未发布时,核心包内置的八个默认插件仍会出现在插件列表。abweb 的“安装本地插件”
|
|
76
76
|
可填写 **Windows 主机上**包含 `plugin.json` 的目录路径,例如编译后的
|
|
77
77
|
`C:\path\to\agent_bridge\packages\dsh`;安装后可在下拉框中启动。已内置的千问办公无需再次安装。
|
|
78
78
|
“安装 npm 插件”只适用于包已经公开发布的情况。npm 发布令牌仅供发布脚本向 npm 官网认证,
|
|
@@ -108,7 +108,7 @@ $env:CODEBUDDY_NPM_DIR = 'C:\path\to\node_modules\@tencent-ai\codebuddy-code'
|
|
|
108
108
|
|
|
109
109
|
千问工作区默认使用 `%APPDATA%\QwenWorkCN\workspace`,可用 `$env:QWEN_WORKSPACE`
|
|
110
110
|
覆盖;AUN 数据默认使用 `$env:EVOLCORE_HOME` 或用户目录下的 `.evolcore`。
|
|
111
|
-
千问 CDP 端口默认为 `
|
|
111
|
+
千问 CDP 端口默认为 `9228`,也可用 `$env:QWEN_CDP_PORT` 或 `$env:AGENT_BRIDGE_QWEN_CDP_PORT` 覆盖。守护进程注入端口优先级最高,用来覆盖插件配置中的冲突端口;之后依次读取插件配置、环境变量和默认值。守护进程监听端口被占用时,会按 `AGENT_BRIDGE_FALLBACK_PORTS` 或系统临时端口回退,并在启动日志中报告实际端口。豆包和 WorkBuddy 分别使用 `DOUBAO_CDP_PORT`、`WORKBUDDY_CDP_PORT`,也支持对应的 `AGENT_BRIDGE_*_CDP_PORT`。
|
|
112
112
|
|
|
113
113
|
## 自检与运行
|
|
114
114
|
|
package/dist/src-ts/cli.js
CHANGED
|
@@ -37,6 +37,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
37
37
|
const node_path_1 = require("node:path");
|
|
38
38
|
const node_http_1 = require("node:http");
|
|
39
39
|
const node_fs_1 = require("node:fs");
|
|
40
|
+
const promises_1 = require("node:fs/promises");
|
|
40
41
|
const node_os_1 = require("node:os");
|
|
41
42
|
const node_child_process_1 = require("node:child_process");
|
|
42
43
|
const node_util_1 = require("node:util");
|
|
@@ -70,8 +71,17 @@ async function selfTest(task) {
|
|
|
70
71
|
console.error(`session=${response.sessionId}`);
|
|
71
72
|
return 0;
|
|
72
73
|
}
|
|
74
|
+
async function daemonPort() {
|
|
75
|
+
try {
|
|
76
|
+
const address = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.resolve)(stateDirectory(), "daemon-address.json"), "utf8"));
|
|
77
|
+
if (Number.isInteger(address.port) && address.port > 0)
|
|
78
|
+
return address.port;
|
|
79
|
+
}
|
|
80
|
+
catch { /* 后台未写入地址时使用配置端口 */ }
|
|
81
|
+
return Number(process.env.AGENT_BRIDGE_PORT ?? "26107");
|
|
82
|
+
}
|
|
73
83
|
async function pair() {
|
|
74
|
-
const port =
|
|
84
|
+
const port = await daemonPort();
|
|
75
85
|
const result = await new Promise((resolve, reject) => {
|
|
76
86
|
const req = (0, node_http_1.request)({ host: "127.0.0.1", port, path: "/api/pair-code", method: "GET" }, (res) => {
|
|
77
87
|
let body = "";
|
|
@@ -110,7 +120,7 @@ async function update() {
|
|
|
110
120
|
return 0;
|
|
111
121
|
}
|
|
112
122
|
async function status() {
|
|
113
|
-
const port =
|
|
123
|
+
const port = await daemonPort();
|
|
114
124
|
const get = (path) => new Promise((resolve, reject) => {
|
|
115
125
|
const req = (0, node_http_1.request)({ host: "127.0.0.1", port, path, method: "GET" }, (res) => {
|
|
116
126
|
let body = "";
|
|
@@ -169,8 +179,14 @@ async function serve() {
|
|
|
169
179
|
if (host !== "127.0.0.1" && host !== "localhost" && host !== "::1" && !process.env.AGENT_BRIDGE_TOKEN) {
|
|
170
180
|
throw new Error("局域网监听必须设置 AGENT_BRIDGE_TOKEN");
|
|
171
181
|
}
|
|
172
|
-
const
|
|
182
|
+
const fallbackPorts = (process.env.AGENT_BRIDGE_FALLBACK_PORTS ?? "").split(",").map((value) => Number(value.trim())).filter((value) => Number.isInteger(value) && value >= 0);
|
|
183
|
+
if (port > 0 && !fallbackPorts.length)
|
|
184
|
+
fallbackPorts.push(0);
|
|
185
|
+
const server = new control_server_1.ControlServer({ manager, host, port, fallbackPorts, token: process.env.AGENT_BRIDGE_TOKEN });
|
|
173
186
|
const address = await server.start();
|
|
187
|
+
const addressFile = (0, node_path_1.resolve)(stateDirectory(), "daemon-address.json");
|
|
188
|
+
await (0, promises_1.mkdir)(stateDirectory(), { recursive: true });
|
|
189
|
+
await (0, promises_1.writeFile)(addressFile, JSON.stringify({ host: address.host, port: address.port, pid: process.pid }));
|
|
174
190
|
logger.info(`daemon listening on http://${address.host}:${address.port}`);
|
|
175
191
|
const order = ["abweb", ...manager.list().map((plugin) => plugin.id).filter((id) => id !== "abweb")];
|
|
176
192
|
for (const id of order) {
|
|
@@ -199,6 +215,12 @@ async function serve() {
|
|
|
199
215
|
}
|
|
200
216
|
}
|
|
201
217
|
await server.stop();
|
|
218
|
+
try {
|
|
219
|
+
const address = JSON.parse(await (0, promises_1.readFile)(addressFile, "utf8"));
|
|
220
|
+
if (address.pid === process.pid)
|
|
221
|
+
await (0, promises_1.rm)(addressFile, { force: true });
|
|
222
|
+
}
|
|
223
|
+
catch { /* 地址文件可能已被清理 */ }
|
|
202
224
|
clearInterval(updateTimer);
|
|
203
225
|
};
|
|
204
226
|
process.once("SIGINT", () => void shutdown().finally(() => process.exit(0)));
|
|
@@ -22,8 +22,36 @@ class ControlServer {
|
|
|
22
22
|
this.options = options;
|
|
23
23
|
this.server = (0, node_http_1.createServer)((req, res) => { void this.handle(req, res); });
|
|
24
24
|
}
|
|
25
|
-
async start() {
|
|
26
|
-
|
|
25
|
+
async start() {
|
|
26
|
+
const host = this.options.host ?? "127.0.0.1";
|
|
27
|
+
const candidates = [this.options.port ?? 0, ...(this.options.fallbackPorts ?? [])];
|
|
28
|
+
for (const port of candidates) {
|
|
29
|
+
try {
|
|
30
|
+
const address = await new Promise((resolve, reject) => {
|
|
31
|
+
const onError = (error) => { this.server.removeListener("listening", onListening); reject(error); };
|
|
32
|
+
const onListening = () => {
|
|
33
|
+
this.server.removeListener("error", onError);
|
|
34
|
+
const info = this.server.address();
|
|
35
|
+
if (!info || typeof info === "string") {
|
|
36
|
+
reject(new Error("后台监听地址无效"));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
resolve({ host, port: info.port });
|
|
40
|
+
};
|
|
41
|
+
this.server.once("error", onError);
|
|
42
|
+
this.server.once("listening", onListening);
|
|
43
|
+
this.server.listen(port, host);
|
|
44
|
+
});
|
|
45
|
+
this.address = address;
|
|
46
|
+
return address;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (error.code !== "EADDRINUSE" || port === candidates.at(-1))
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
throw new Error("后台监听失败");
|
|
54
|
+
}
|
|
27
55
|
async stop() { if (!this.server.listening)
|
|
28
56
|
return; await new Promise((resolve, reject) => this.server.close((error) => error ? reject(error) : resolve())); }
|
|
29
57
|
async handle(req, res) {
|
|
@@ -82,9 +110,27 @@ class ControlServer {
|
|
|
82
110
|
if (method === "POST" && url.pathname === "/api/update") {
|
|
83
111
|
this.options.manager.log("info", "已请求后台升级,准备重启 daemon", undefined, "后台升级");
|
|
84
112
|
if (process.platform === "win32") {
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
113
|
+
const logFile = `${process.env.APPDATA ?? process.cwd()}\\agent-bridge\\state\\core-update.log`;
|
|
114
|
+
const script = [
|
|
115
|
+
"$ErrorActionPreference='Continue'",
|
|
116
|
+
`$log = '${logFile.replace(/'/g, "''")}'`,
|
|
117
|
+
"$dir = Split-Path -Parent $log",
|
|
118
|
+
"New-Item -ItemType Directory -Force $dir | Out-Null",
|
|
119
|
+
"function Write-UpdateLog($text) { \"$(Get-Date -Format o) $text\" | Out-File -FilePath $log -Append -Encoding utf8 }",
|
|
120
|
+
"Start-Sleep -Seconds 3",
|
|
121
|
+
"Write-UpdateLog '停止旧 daemon'",
|
|
122
|
+
"schtasks.exe /End /TN mab 2>&1 | ForEach-Object { Write-UpdateLog $_ }",
|
|
123
|
+
"Start-Sleep -Seconds 2",
|
|
124
|
+
"Write-UpdateLog '开始安装 mu-agent-bridge@latest'",
|
|
125
|
+
"npm.cmd install -g mu-agent-bridge@latest --no-fund --no-audit 2>&1 | ForEach-Object { Write-UpdateLog $_ }",
|
|
126
|
+
"Write-UpdateLog '重新启动 mab 计划任务'",
|
|
127
|
+
"schtasks.exe /Run /TN mab 2>&1 | ForEach-Object { Write-UpdateLog $_ }",
|
|
128
|
+
"Write-UpdateLog '后台升级脚本结束'",
|
|
129
|
+
].join('; ');
|
|
130
|
+
setTimeout(() => {
|
|
131
|
+
const child = (0, node_child_process_1.spawn)("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], { detached: true, stdio: "ignore", windowsHide: true });
|
|
132
|
+
child.unref();
|
|
133
|
+
}, 1500).unref();
|
|
88
134
|
}
|
|
89
135
|
else {
|
|
90
136
|
return this.json(res, 501, { error: "当前平台未配置后台升级器" });
|
|
@@ -7,7 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
*/
|
|
8
8
|
const node_http_1 = require("node:http");
|
|
9
9
|
const node_crypto_1 = require("node:crypto");
|
|
10
|
-
const PORT = Number(process.env.QWEN_CDP_PORT ?? '
|
|
10
|
+
const PORT = Number(process.env.QWEN_CDP_PORT ?? '9228');
|
|
11
11
|
const SUBCHAT = process.env.QWEN_SUBCHAT ?? 'muj55ep9426gudjw';
|
|
12
12
|
const CHAT = process.env.QWEN_CHAT ?? 'muj55ep9u6wbv0eo';
|
|
13
13
|
const CWD = process.env.QWEN_WORKSPACE ?? `${process.env.APPDATA ?? ''}\\QwenWorkCN\\workspace`;
|
|
@@ -138,8 +138,11 @@ class PluginManager {
|
|
|
138
138
|
this.log("info", `开始升级插件:${id}`, { packageName, version }, "插件升级");
|
|
139
139
|
await (0, promises_1.mkdir)(staging, { recursive: true });
|
|
140
140
|
try {
|
|
141
|
-
if (!cached)
|
|
142
|
-
await this.downloadPackage(packageName, version)
|
|
141
|
+
if (!cached) {
|
|
142
|
+
const downloaded = await this.downloadPackage(packageName, version);
|
|
143
|
+
await (0, promises_1.cp)(downloaded, staging, { recursive: true, force: true });
|
|
144
|
+
await (0, promises_1.rm)(downloaded, { recursive: true, force: true });
|
|
145
|
+
}
|
|
143
146
|
const candidate = (0, node_path_1.join)(staging, "node_modules", packageName);
|
|
144
147
|
const meta = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(candidate, "package.json"), "utf8"));
|
|
145
148
|
if (meta.name !== packageName || !meta.version || (version && meta.version !== version))
|
|
@@ -307,6 +310,11 @@ class PluginManager {
|
|
|
307
310
|
}
|
|
308
311
|
},
|
|
309
312
|
},
|
|
313
|
+
runtime: { ports: {
|
|
314
|
+
qwenwork: Number(process.env.AGENT_BRIDGE_QWEN_CDP_PORT ?? process.env.QWEN_CDP_PORT ?? "9228"),
|
|
315
|
+
doubao: Number(process.env.AGENT_BRIDGE_DOUBAO_CDP_PORT ?? process.env.DOUBAO_CDP_PORT ?? "9227"),
|
|
316
|
+
workbuddy: Number(process.env.AGENT_BRIDGE_WORKBUDDY_CDP_PORT ?? process.env.WORKBUDDY_CDP_PORT ?? "9229"),
|
|
317
|
+
} },
|
|
310
318
|
});
|
|
311
319
|
loaded.instance = instance;
|
|
312
320
|
loaded.record.status = "ready";
|
package/dist/src-ts/plugin.js
CHANGED
|
@@ -1,2 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveRuntimePort = resolveRuntimePort;
|
|
4
|
+
function resolveRuntimePort(config, runtime, pluginId, envName, fallback) {
|
|
5
|
+
const injected = runtime?.ports?.[pluginId];
|
|
6
|
+
if (typeof injected === "number" && Number.isInteger(injected) && injected > 0)
|
|
7
|
+
return injected;
|
|
8
|
+
const configured = config.cdpPort;
|
|
9
|
+
if (typeof configured === "number" && Number.isInteger(configured) && configured > 0)
|
|
10
|
+
return configured;
|
|
11
|
+
const environment = Number(process.env[envName] ?? "");
|
|
12
|
+
return Number.isInteger(environment) && environment > 0 ? environment : fallback;
|
|
13
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ChatgptCdpClient = exports.userBubbleSelector = exports.turnSelector = exports.editorSelector = void 0;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
exports.editorSelector = '[data-codex-composer="true"][contenteditable="true"]';
|
|
6
|
+
exports.turnSelector = '[data-chatgpt-conversation-turn="true"]';
|
|
7
|
+
exports.userBubbleSelector = "[data-user-message-bubble]";
|
|
8
|
+
const assistantMessageSelector = '[data-markdown-text-style="assistant-message"]';
|
|
9
|
+
const turnIdAttribute = "data-chatgpt-conversation-turn-id";
|
|
10
|
+
const contentUnitSelector = "[data-content-search-unit-key]";
|
|
11
|
+
const sendButtonSelector = 'button[aria-label="Send"], button[aria-label="发送"]';
|
|
12
|
+
const stopButtonSelector = 'button[aria-label="Stop"], button[aria-label="停止"]';
|
|
13
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
class ChatgptCdpClient {
|
|
15
|
+
socket;
|
|
16
|
+
tunnel;
|
|
17
|
+
sequence = 0;
|
|
18
|
+
pending = new Map();
|
|
19
|
+
port;
|
|
20
|
+
timeoutMs;
|
|
21
|
+
fetchImpl;
|
|
22
|
+
webSocketFactory;
|
|
23
|
+
ssh;
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
this.port = options.port ?? options.ssh?.localPort ?? 9230;
|
|
26
|
+
this.timeoutMs = options.timeoutMs ?? 30_000;
|
|
27
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
28
|
+
this.webSocketFactory = options.webSocketFactory ?? ((url) => new WebSocket(url));
|
|
29
|
+
this.ssh = options.ssh;
|
|
30
|
+
}
|
|
31
|
+
connected() { return Boolean(this.socket); }
|
|
32
|
+
async connect() {
|
|
33
|
+
if (this.socket)
|
|
34
|
+
return;
|
|
35
|
+
try {
|
|
36
|
+
if (this.ssh) {
|
|
37
|
+
await this.startTunnel();
|
|
38
|
+
}
|
|
39
|
+
const page = await this.findPage();
|
|
40
|
+
if (!page.webSocketDebuggerUrl)
|
|
41
|
+
throw new Error("ChatGPT CDP target 缺少 WebSocket 地址");
|
|
42
|
+
const socketUrl = new URL(page.webSocketDebuggerUrl);
|
|
43
|
+
if (this.ssh) {
|
|
44
|
+
socketUrl.hostname = "127.0.0.1";
|
|
45
|
+
socketUrl.port = String(this.port);
|
|
46
|
+
}
|
|
47
|
+
const socket = this.webSocketFactory(socketUrl);
|
|
48
|
+
this.socket = socket;
|
|
49
|
+
let opened = false;
|
|
50
|
+
let rejectOpening;
|
|
51
|
+
socket.onmessage = (event) => this.onMessage(String(event.data));
|
|
52
|
+
socket.onclose = () => {
|
|
53
|
+
if (this.socket === socket) {
|
|
54
|
+
this.socket = undefined;
|
|
55
|
+
this.tunnel?.kill();
|
|
56
|
+
this.tunnel = undefined;
|
|
57
|
+
}
|
|
58
|
+
if (!opened)
|
|
59
|
+
rejectOpening?.(new Error("ChatGPT CDP WebSocket 连接已关闭"));
|
|
60
|
+
this.rejectPending(new Error("ChatGPT CDP 连接已关闭"));
|
|
61
|
+
};
|
|
62
|
+
await new Promise((resolve, reject) => {
|
|
63
|
+
rejectOpening = reject;
|
|
64
|
+
const timer = setTimeout(() => reject(new Error("ChatGPT CDP WebSocket 连接超时")), this.timeoutMs);
|
|
65
|
+
socket.onopen = () => {
|
|
66
|
+
opened = true;
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
resolve();
|
|
69
|
+
};
|
|
70
|
+
socket.onerror = () => {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
reject(new Error("ChatGPT CDP WebSocket 连接失败"));
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
this.close();
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async findPage() {
|
|
82
|
+
const deadline = Date.now() + this.timeoutMs;
|
|
83
|
+
let last;
|
|
84
|
+
while (Date.now() < deadline) {
|
|
85
|
+
try {
|
|
86
|
+
const response = await this.fetchImpl(`http://127.0.0.1:${this.port}/json`, {
|
|
87
|
+
signal: AbortSignal.timeout(Math.min(this.timeoutMs, 3_000)),
|
|
88
|
+
});
|
|
89
|
+
if (!response.ok)
|
|
90
|
+
throw new Error(`ChatGPT CDP /json HTTP ${response.status}`);
|
|
91
|
+
const targets = await response.json();
|
|
92
|
+
const page = targets.find((target) => target.type === "page" && (/chatgpt|codex/i.test(`${target.title} ${target.url}`)
|
|
93
|
+
|| target.url === "app://-/index.html"));
|
|
94
|
+
if (page?.webSocketDebuggerUrl)
|
|
95
|
+
return page;
|
|
96
|
+
last = new Error(`未找到 ChatGPT/Codex 页面 target(端口 ${this.port})`);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
last = error;
|
|
100
|
+
}
|
|
101
|
+
await sleep(500);
|
|
102
|
+
}
|
|
103
|
+
throw new Error(`ChatGPT CDP 连接超时:${last instanceof Error ? last.message : String(last ?? "端口不可达")}`, { cause: last });
|
|
104
|
+
}
|
|
105
|
+
close() {
|
|
106
|
+
this.socket?.close();
|
|
107
|
+
this.socket = undefined;
|
|
108
|
+
this.tunnel?.kill();
|
|
109
|
+
this.tunnel = undefined;
|
|
110
|
+
this.rejectPending(new Error("ChatGPT CDP 已关闭"));
|
|
111
|
+
}
|
|
112
|
+
async direct(text, timeoutMs = 240_000) {
|
|
113
|
+
await this.waitFor(`Boolean(document.querySelector(${JSON.stringify(exports.editorSelector)}))`, "ChatGPT 桌面端输入框未就绪");
|
|
114
|
+
const beforeTurnsJson = await this.evaluate(`JSON.stringify([...document.querySelectorAll(${JSON.stringify(exports.turnSelector)})].map(turn => ({ id: turn.getAttribute(${JSON.stringify(turnIdAttribute)}), users: turn.querySelectorAll(${JSON.stringify(exports.userBubbleSelector)}).length })))`);
|
|
115
|
+
const beforeTurns = JSON.parse(String(beforeTurnsJson || "[]"));
|
|
116
|
+
const beforeAssistantCount = Number(await this.evaluate(`document.querySelectorAll(${JSON.stringify(assistantMessageSelector)}).length`));
|
|
117
|
+
const ready = await this.evaluate(`(() => {
|
|
118
|
+
const editor = document.querySelector(${JSON.stringify(exports.editorSelector)});
|
|
119
|
+
if (!editor || editor.innerText.trim()) return false;
|
|
120
|
+
const findButton = () => {
|
|
121
|
+
for (let parent = editor; parent && parent !== document.body; parent = parent.parentElement) {
|
|
122
|
+
const buttons = [...parent.querySelectorAll(${JSON.stringify(sendButtonSelector)})];
|
|
123
|
+
if (buttons.length === 1) return buttons[0];
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
};
|
|
127
|
+
const button = findButton();
|
|
128
|
+
if (!button) return false;
|
|
129
|
+
editor.focus();
|
|
130
|
+
return document.activeElement === editor;
|
|
131
|
+
})()`);
|
|
132
|
+
if (!ready)
|
|
133
|
+
throw new Error("ChatGPT 输入框含有未发送内容,或桌面端发送按钮未就绪");
|
|
134
|
+
await this.cdp("Input.insertText", { text });
|
|
135
|
+
const inputMatches = await this.evaluate(`document.querySelector(${JSON.stringify(exports.editorSelector)})?.innerText.trim() === ${JSON.stringify(text.trim())}`);
|
|
136
|
+
if (!inputMatches)
|
|
137
|
+
throw new Error("ChatGPT 输入内容校验失败,未发送消息");
|
|
138
|
+
const sent = await this.evaluate(`(() => {
|
|
139
|
+
const editor = document.querySelector(${JSON.stringify(exports.editorSelector)});
|
|
140
|
+
const findButton = () => {
|
|
141
|
+
for (let parent = editor; parent && parent !== document.body; parent = parent.parentElement) {
|
|
142
|
+
const buttons = [...parent.querySelectorAll(${JSON.stringify(sendButtonSelector)})];
|
|
143
|
+
if (buttons.length === 1) return buttons[0];
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
};
|
|
147
|
+
const button = findButton();
|
|
148
|
+
if (!button || button.disabled) return false;
|
|
149
|
+
button.click();
|
|
150
|
+
return true;
|
|
151
|
+
})()`);
|
|
152
|
+
if (!sent)
|
|
153
|
+
throw new Error("ChatGPT 桌面端发送按钮不可用,未确认消息已发送");
|
|
154
|
+
const deadline = Date.now() + timeoutMs;
|
|
155
|
+
let previous = "";
|
|
156
|
+
let stable = 0;
|
|
157
|
+
while (Date.now() < deadline) {
|
|
158
|
+
await sleep(800);
|
|
159
|
+
const raw = await this.evaluate(`(() => {
|
|
160
|
+
const before = ${JSON.stringify(beforeTurns)};
|
|
161
|
+
const turns = [...document.querySelectorAll(${JSON.stringify(exports.turnSelector)})];
|
|
162
|
+
const turn = turns.findLast((item, index) => {
|
|
163
|
+
const id = item.getAttribute(${JSON.stringify(turnIdAttribute)});
|
|
164
|
+
const oldIndex = before.findIndex(entry => entry.id === id);
|
|
165
|
+
const old = oldIndex < 0 ? undefined : before[oldIndex];
|
|
166
|
+
const users = item.querySelectorAll(${JSON.stringify(exports.userBubbleSelector)});
|
|
167
|
+
const fresh = oldIndex < 0 || index >= before.length || users.length > (old?.users ?? 0);
|
|
168
|
+
return fresh && users.item(users.length - 1)?.innerText.trim() === ${JSON.stringify(text.trim())};
|
|
169
|
+
});
|
|
170
|
+
if (!turn) {
|
|
171
|
+
const assistants = [...document.querySelectorAll(${JSON.stringify(assistantMessageSelector)})];
|
|
172
|
+
const fallback = assistants.length > ${beforeAssistantCount}
|
|
173
|
+
? assistants.at(-1)?.innerText.trim() ?? ""
|
|
174
|
+
: "";
|
|
175
|
+
return JSON.stringify({ reply: fallback, active: Boolean(document.querySelector(${JSON.stringify(stopButtonSelector)})) });
|
|
176
|
+
}
|
|
177
|
+
const user = turn.querySelectorAll(${JSON.stringify(exports.userBubbleSelector)}).item(turn.querySelectorAll(${JSON.stringify(exports.userBubbleSelector)}).length - 1);
|
|
178
|
+
const unit = user?.closest(${JSON.stringify(contentUnitSelector)});
|
|
179
|
+
const units = [...turn.querySelectorAll(${JSON.stringify(contentUnitSelector)})];
|
|
180
|
+
const index = unit ? units.indexOf(unit) : -1;
|
|
181
|
+
const replies = index < 0 ? [] : units.slice(index + 1).map(item => item.innerText.trim()).filter(Boolean);
|
|
182
|
+
const legacyReplies = [...turn.querySelectorAll('[data-message-author-role="assistant"]')].map(item => item.innerText.trim()).filter(Boolean);
|
|
183
|
+
const reply = replies.at(-1) ?? legacyReplies.at(-1) ?? "";
|
|
184
|
+
const active = Boolean(document.querySelector(${JSON.stringify(stopButtonSelector)}));
|
|
185
|
+
return JSON.stringify({ reply, active });
|
|
186
|
+
})()`);
|
|
187
|
+
const state = JSON.parse(String(raw));
|
|
188
|
+
const reply = state.reply.trim();
|
|
189
|
+
stable = reply && reply === previous ? stable + 1 : 0;
|
|
190
|
+
previous = reply;
|
|
191
|
+
if (reply && stable >= 2 && !state.active)
|
|
192
|
+
return reply;
|
|
193
|
+
}
|
|
194
|
+
throw new Error("ChatGPT UI 回复超时");
|
|
195
|
+
}
|
|
196
|
+
async startTunnel() {
|
|
197
|
+
const ssh = this.ssh;
|
|
198
|
+
const args = [
|
|
199
|
+
"-N", "-o", "BatchMode=yes", "-o", "ExitOnForwardFailure=yes", "-o", "ConnectTimeout=10",
|
|
200
|
+
"-L", `${ssh.localPort}:127.0.0.1:${ssh.remotePort}`, "-p", String(ssh.port ?? 22),
|
|
201
|
+
];
|
|
202
|
+
if (ssh.keyPath)
|
|
203
|
+
args.push("-i", ssh.keyPath);
|
|
204
|
+
args.push(`${ssh.user}@${ssh.host}`);
|
|
205
|
+
this.tunnel = (0, node_child_process_1.spawn)("ssh", args, { stdio: "ignore", windowsHide: true });
|
|
206
|
+
const tunnel = this.tunnel;
|
|
207
|
+
await new Promise((resolve, reject) => {
|
|
208
|
+
let ready = false;
|
|
209
|
+
const fail = (error) => {
|
|
210
|
+
if (!ready)
|
|
211
|
+
reject(error);
|
|
212
|
+
else if (this.tunnel === tunnel) {
|
|
213
|
+
this.tunnel = undefined;
|
|
214
|
+
this.socket?.close();
|
|
215
|
+
this.socket = undefined;
|
|
216
|
+
this.rejectPending(error);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
const timer = setTimeout(() => {
|
|
220
|
+
ready = true;
|
|
221
|
+
resolve();
|
|
222
|
+
}, 500);
|
|
223
|
+
tunnel.once("error", (error) => {
|
|
224
|
+
clearTimeout(timer);
|
|
225
|
+
fail(error);
|
|
226
|
+
});
|
|
227
|
+
tunnel.once("exit", () => {
|
|
228
|
+
clearTimeout(timer);
|
|
229
|
+
fail(new Error("SSH CDP 隧道已关闭,请检查远程连接"));
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
async waitFor(expression, message) {
|
|
234
|
+
const deadline = Date.now() + this.timeoutMs;
|
|
235
|
+
while (Date.now() < deadline) {
|
|
236
|
+
if (await this.evaluate(expression))
|
|
237
|
+
return;
|
|
238
|
+
await sleep(200);
|
|
239
|
+
}
|
|
240
|
+
throw new Error(message);
|
|
241
|
+
}
|
|
242
|
+
rejectPending(error) {
|
|
243
|
+
for (const pending of this.pending.values())
|
|
244
|
+
pending.reject(error);
|
|
245
|
+
this.pending.clear();
|
|
246
|
+
}
|
|
247
|
+
onMessage(raw) {
|
|
248
|
+
let message;
|
|
249
|
+
try {
|
|
250
|
+
message = JSON.parse(raw);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (message.id === undefined)
|
|
256
|
+
return;
|
|
257
|
+
const pending = this.pending.get(message.id);
|
|
258
|
+
if (!pending)
|
|
259
|
+
return;
|
|
260
|
+
this.pending.delete(message.id);
|
|
261
|
+
if (message.error)
|
|
262
|
+
pending.reject(new Error(message.error.message ?? "CDP 调用失败"));
|
|
263
|
+
else
|
|
264
|
+
pending.resolve(message);
|
|
265
|
+
}
|
|
266
|
+
async cdp(method, params = {}) {
|
|
267
|
+
if (!this.socket)
|
|
268
|
+
throw new Error("ChatGPT CDP 尚未连接");
|
|
269
|
+
const id = ++this.sequence;
|
|
270
|
+
const socket = this.socket;
|
|
271
|
+
return new Promise((resolve, reject) => {
|
|
272
|
+
const timer = setTimeout(() => {
|
|
273
|
+
this.pending.delete(id);
|
|
274
|
+
reject(new Error(`CDP ${method} 超时`));
|
|
275
|
+
}, this.timeoutMs);
|
|
276
|
+
this.pending.set(id, {
|
|
277
|
+
resolve: (value) => {
|
|
278
|
+
clearTimeout(timer);
|
|
279
|
+
resolve(value);
|
|
280
|
+
},
|
|
281
|
+
reject: (error) => {
|
|
282
|
+
clearTimeout(timer);
|
|
283
|
+
reject(error);
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
try {
|
|
287
|
+
socket.send(JSON.stringify({ id, method, params }));
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
this.pending.delete(id);
|
|
291
|
+
clearTimeout(timer);
|
|
292
|
+
reject(error);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
async evaluate(expression) {
|
|
297
|
+
const response = await this.cdp("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true });
|
|
298
|
+
if (response.result?.exceptionDetails) {
|
|
299
|
+
throw new Error(`ChatGPT 页面 JS 异常:${JSON.stringify(response.result.exceptionDetails).slice(0, 300)}`);
|
|
300
|
+
}
|
|
301
|
+
return response.result?.result?.value;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
exports.ChatgptCdpClient = ChatgptCdpClient;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ChatgptCdpClient = exports.ChatgptPlugin = exports.chatgptManifest = void 0;
|
|
4
|
+
const cdp_client_1 = require("./cdp-client");
|
|
5
|
+
exports.chatgptManifest = {
|
|
6
|
+
id: "chatgpt",
|
|
7
|
+
version: "1.0.0",
|
|
8
|
+
displayName: "ChatGPT(Codex)",
|
|
9
|
+
type: "agent",
|
|
10
|
+
entry: "dist/index.js",
|
|
11
|
+
apiVersion: "1",
|
|
12
|
+
process: "in-process",
|
|
13
|
+
capabilities: ["chat", "health"],
|
|
14
|
+
configSchema: "config.schema.json",
|
|
15
|
+
};
|
|
16
|
+
function port(value, fallback) {
|
|
17
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 65_535
|
|
18
|
+
? value
|
|
19
|
+
: fallback;
|
|
20
|
+
}
|
|
21
|
+
class ChatgptPlugin {
|
|
22
|
+
options;
|
|
23
|
+
manifest = exports.chatgptManifest;
|
|
24
|
+
client;
|
|
25
|
+
failure;
|
|
26
|
+
remote = false;
|
|
27
|
+
queue = Promise.resolve();
|
|
28
|
+
constructor(options = {}) {
|
|
29
|
+
this.options = options;
|
|
30
|
+
}
|
|
31
|
+
async activate(context) {
|
|
32
|
+
const config = context.config;
|
|
33
|
+
const hasSshHost = config.sshHost !== undefined;
|
|
34
|
+
const hasSshUser = config.sshUser !== undefined;
|
|
35
|
+
if (hasSshHost !== hasSshUser || (hasSshHost && (typeof config.sshHost !== "string" || !config.sshHost.trim()
|
|
36
|
+
|| typeof config.sshUser !== "string" || !config.sshUser.trim()))) {
|
|
37
|
+
throw new Error("sshHost 和 sshUser 必须同时填写有效值");
|
|
38
|
+
}
|
|
39
|
+
if (config.sshKeyPath !== undefined && typeof config.sshKeyPath !== "string") {
|
|
40
|
+
throw new Error("sshKeyPath 必须是字符串");
|
|
41
|
+
}
|
|
42
|
+
const ssh = hasSshHost ? {
|
|
43
|
+
host: config.sshHost.trim(),
|
|
44
|
+
user: config.sshUser.trim(),
|
|
45
|
+
port: port(config.sshPort, 22),
|
|
46
|
+
keyPath: typeof config.sshKeyPath === "string" ? config.sshKeyPath : undefined,
|
|
47
|
+
remotePort: port(config.remoteCdpPort, 9230),
|
|
48
|
+
localPort: port(config.localCdpPort, 9230),
|
|
49
|
+
} : undefined;
|
|
50
|
+
this.remote = Boolean(ssh);
|
|
51
|
+
this.failure = undefined;
|
|
52
|
+
this.client = this.options.client ?? new cdp_client_1.ChatgptCdpClient({
|
|
53
|
+
port: port(config.localCdpPort, 9230),
|
|
54
|
+
timeoutMs: typeof config.timeoutMs === "number" && config.timeoutMs > 0 ? config.timeoutMs : 30_000,
|
|
55
|
+
ssh,
|
|
56
|
+
});
|
|
57
|
+
try {
|
|
58
|
+
await this.client.connect();
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
this.failure = error instanceof Error ? error.message : String(error);
|
|
62
|
+
context.logger.warn(this.failure);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async deactivate() {
|
|
67
|
+
await this.queue;
|
|
68
|
+
this.client?.close();
|
|
69
|
+
this.client = undefined;
|
|
70
|
+
this.remote = false;
|
|
71
|
+
}
|
|
72
|
+
async health() {
|
|
73
|
+
return this.client?.connected()
|
|
74
|
+
? { status: "ready", details: { transport: "cdp", remote: this.remote } }
|
|
75
|
+
: { status: "failed", message: this.failure ?? "ChatGPT CDP 未连接", details: { transport: "cdp", remote: this.remote } };
|
|
76
|
+
}
|
|
77
|
+
async handle(request) {
|
|
78
|
+
if (!request.task.trim())
|
|
79
|
+
throw new Error("消息内容不能为空");
|
|
80
|
+
if (!this.client?.connected())
|
|
81
|
+
throw new Error("ChatGPT CDP 未连接");
|
|
82
|
+
const run = this.queue.then(() => this.client.direct(request.task, request.timeoutMs));
|
|
83
|
+
this.queue = run.then(() => undefined, () => undefined);
|
|
84
|
+
return { text: await run, sessionId: request.sessionId };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
exports.ChatgptPlugin = ChatgptPlugin;
|
|
88
|
+
var cdp_client_2 = require("./cdp-client");
|
|
89
|
+
Object.defineProperty(exports, "ChatgptCdpClient", { enumerable: true, get: function () { return cdp_client_2.ChatgptCdpClient; } });
|
|
90
|
+
exports.default = ChatgptPlugin;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DoubaoPlugin = exports.doubaoManifest = void 0;
|
|
4
4
|
const cdp_client_1 = require("./cdp-client");
|
|
5
|
+
const plugin_1 = require("../../plugin");
|
|
5
6
|
exports.doubaoManifest = {
|
|
6
7
|
id: "doubao",
|
|
7
8
|
version: "1.0.4",
|
|
@@ -24,7 +25,7 @@ class DoubaoPlugin {
|
|
|
24
25
|
disposer;
|
|
25
26
|
async activate(context) {
|
|
26
27
|
this.client = this.injectedClient ?? new cdp_client_1.DoubaoCdpClient({
|
|
27
|
-
port:
|
|
28
|
+
port: (0, plugin_1.resolveRuntimePort)(context.config, context.runtime, "doubao", "DOUBAO_CDP_PORT", 9227),
|
|
28
29
|
conversationId: typeof context.config.conversationId === "string" ? context.config.conversationId : undefined,
|
|
29
30
|
botId: typeof context.config.botId === "string" ? context.config.botId : undefined,
|
|
30
31
|
pluginList: Array.isArray(context.config.pluginList) ? context.config.pluginList.filter((value) => typeof value === "string") : undefined,
|
|
@@ -27,7 +27,7 @@ class QwenCdpClient {
|
|
|
27
27
|
options;
|
|
28
28
|
constructor(options = {}) {
|
|
29
29
|
this.options = {
|
|
30
|
-
port: options.port ?? Number(process.env.QWEN_CDP_PORT ?? "
|
|
30
|
+
port: options.port ?? Number(process.env.QWEN_CDP_PORT ?? "9228"),
|
|
31
31
|
subChatId: options.subChatId ?? process.env.QWEN_SUBCHAT ?? "muj55ep9426gudjw",
|
|
32
32
|
chatId: options.chatId ?? process.env.QWEN_CHAT ?? "muj55ep9u6wbv0eo",
|
|
33
33
|
workspace: options.workspace ?? `${process.env.APPDATA ?? ""}\\QwenWorkCN\\workspace`,
|