mu-agent-bridge 1.0.21 → 1.0.23
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/dist/src-ts/plugin-manager.js +22 -1
- package/dist/src-ts/plugins/abweb/public/app.js +1 -1
- package/dist/src-ts/plugins/chatgpt/cdp-client.js +11 -2
- package/dist/tests-ts/chatgpt-plugin.test.js +14 -0
- package/dist/tests-ts/plugin-manager.test.js +38 -0
- package/package.json +1 -1
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.js +1 -1
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.ts +1 -1
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +11 -2
|
@@ -249,6 +249,16 @@ class PluginManager {
|
|
|
249
249
|
throw new agent_1.AgentError(`unknown plugin: ${id}`);
|
|
250
250
|
if (loaded.instance && loaded.record.status === "ready")
|
|
251
251
|
return;
|
|
252
|
+
if (loaded.instance) {
|
|
253
|
+
try {
|
|
254
|
+
await loaded.instance.deactivate("reload");
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
this.log("warn", `插件恢复前清理失败:${id}`, { error: error instanceof Error ? error.message : String(error) }, "插件生命周期");
|
|
258
|
+
}
|
|
259
|
+
this.unregisterHttpRoutes(id);
|
|
260
|
+
loaded.instance = undefined;
|
|
261
|
+
}
|
|
252
262
|
if (path.has(id)) {
|
|
253
263
|
const reason = `插件存在循环依赖:${[...path, id].join(" -> ")}`;
|
|
254
264
|
loaded.record.status = "failed";
|
|
@@ -268,8 +278,9 @@ class PluginManager {
|
|
|
268
278
|
for (const dependency of Object.keys(loaded.record.manifest.dependencies ?? {}))
|
|
269
279
|
await this.startWithDependencies(dependency, next);
|
|
270
280
|
loaded.record.status = "starting";
|
|
281
|
+
let instance;
|
|
271
282
|
try {
|
|
272
|
-
|
|
283
|
+
instance = await this.load(loaded.record);
|
|
273
284
|
const dataDir = (0, node_path_1.join)(this.options.stateDir, id);
|
|
274
285
|
await (0, promises_1.mkdir)(dataDir, { recursive: true });
|
|
275
286
|
const configPath = (0, node_path_1.join)(dataDir, "config.json");
|
|
@@ -322,6 +333,16 @@ class PluginManager {
|
|
|
322
333
|
this.log("info", `插件已启动:${id}`, { version: loaded.record.version }, "插件生命周期");
|
|
323
334
|
}
|
|
324
335
|
catch (error) {
|
|
336
|
+
if (instance) {
|
|
337
|
+
try {
|
|
338
|
+
await instance.deactivate("reload");
|
|
339
|
+
}
|
|
340
|
+
catch (cleanupError) {
|
|
341
|
+
this.log("warn", `插件启动失败后的清理失败:${id}`, { error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError) }, "插件生命周期");
|
|
342
|
+
}
|
|
343
|
+
this.unregisterHttpRoutes(id);
|
|
344
|
+
}
|
|
345
|
+
loaded.instance = undefined;
|
|
325
346
|
loaded.record.status = "failed";
|
|
326
347
|
loaded.record.error = error instanceof Error ? error.message : String(error);
|
|
327
348
|
this.log("error", `插件启动失败:${id}`, { error: loaded.record.error }, "插件生命周期");
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
item.type = "button";
|
|
109
109
|
item.textContent = label;
|
|
110
110
|
item.title = title;
|
|
111
|
-
item.onclick = () => { void run().catch((cause) => error(String(cause))); };
|
|
111
|
+
item.onclick = () => { item.disabled = true; void run().catch((cause) => error(String(cause))).finally(() => { item.disabled = false; void load(false); }); };
|
|
112
112
|
return item;
|
|
113
113
|
}
|
|
114
114
|
async function load(refreshCatalog = true) {
|
|
@@ -5,6 +5,7 @@ const node_child_process_1 = require("node:child_process");
|
|
|
5
5
|
exports.editorSelector = '[data-codex-composer="true"][contenteditable="true"]';
|
|
6
6
|
exports.turnSelector = '[data-chatgpt-conversation-turn="true"]';
|
|
7
7
|
exports.userBubbleSelector = "[data-user-message-bubble]";
|
|
8
|
+
const assistantMessageSelector = '[data-markdown-text-style="assistant-message"]';
|
|
8
9
|
const turnIdAttribute = "data-chatgpt-conversation-turn-id";
|
|
9
10
|
const contentUnitSelector = "[data-content-search-unit-key]";
|
|
10
11
|
const sendButtonSelector = 'button[aria-label="Send"], button[aria-label="发送"]';
|
|
@@ -88,7 +89,8 @@ class ChatgptCdpClient {
|
|
|
88
89
|
if (!response.ok)
|
|
89
90
|
throw new Error(`ChatGPT CDP /json HTTP ${response.status}`);
|
|
90
91
|
const targets = await response.json();
|
|
91
|
-
const page = targets.find((target) => target.type === "page" && /chatgpt|codex/i.test(`${target.title} ${target.url}`)
|
|
92
|
+
const page = targets.find((target) => target.type === "page" && (/chatgpt|codex/i.test(`${target.title} ${target.url}`)
|
|
93
|
+
|| target.url === "app://-/index.html"));
|
|
92
94
|
if (page?.webSocketDebuggerUrl)
|
|
93
95
|
return page;
|
|
94
96
|
last = new Error(`未找到 ChatGPT/Codex 页面 target(端口 ${this.port})`);
|
|
@@ -111,6 +113,7 @@ class ChatgptCdpClient {
|
|
|
111
113
|
await this.waitFor(`Boolean(document.querySelector(${JSON.stringify(exports.editorSelector)}))`, "ChatGPT 桌面端输入框未就绪");
|
|
112
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 })))`);
|
|
113
115
|
const beforeTurns = JSON.parse(String(beforeTurnsJson || "[]"));
|
|
116
|
+
const beforeAssistantCount = Number(await this.evaluate(`document.querySelectorAll(${JSON.stringify(assistantMessageSelector)}).length`));
|
|
114
117
|
const ready = await this.evaluate(`(() => {
|
|
115
118
|
const editor = document.querySelector(${JSON.stringify(exports.editorSelector)});
|
|
116
119
|
if (!editor || editor.innerText.trim()) return false;
|
|
@@ -164,7 +167,13 @@ class ChatgptCdpClient {
|
|
|
164
167
|
const fresh = oldIndex < 0 || index >= before.length || users.length > (old?.users ?? 0);
|
|
165
168
|
return fresh && users.item(users.length - 1)?.innerText.trim() === ${JSON.stringify(text.trim())};
|
|
166
169
|
});
|
|
167
|
-
if (!turn)
|
|
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
|
+
}
|
|
168
177
|
const user = turn.querySelectorAll(${JSON.stringify(exports.userBubbleSelector)}).item(turn.querySelectorAll(${JSON.stringify(exports.userBubbleSelector)}).length - 1);
|
|
169
178
|
const unit = user?.closest(${JSON.stringify(contentUnitSelector)});
|
|
170
179
|
const units = [...turn.querySelectorAll(${JSON.stringify(contentUnitSelector)})];
|
|
@@ -35,6 +35,20 @@ const chatgpt_1 = require("../src-ts/plugins/chatgpt");
|
|
|
35
35
|
strict_1.default.equal(client.connected(), true);
|
|
36
36
|
client.close();
|
|
37
37
|
});
|
|
38
|
+
(0, node_test_1.default)("ChatGPT/Codex 桌面端使用 app 页面 target 时可以连接", async () => {
|
|
39
|
+
let connectedUrl = "";
|
|
40
|
+
const client = new chatgpt_1.ChatgptCdpClient({
|
|
41
|
+
timeoutMs: 100,
|
|
42
|
+
fetchImpl: async () => new Response(JSON.stringify([{ type: "page", title: "请只回复:CDP连通测试成功", url: "app://-/index.html", webSocketDebuggerUrl: "ws://127.0.0.1:9230/devtools/page/2" }])),
|
|
43
|
+
webSocketFactory: (url) => {
|
|
44
|
+
connectedUrl = url.toString();
|
|
45
|
+
return { set onmessage(_) { }, set onclose(_) { }, set onerror(_) { }, set onopen(handler) { queueMicrotask(handler); }, close() { }, send() { } };
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
await client.connect();
|
|
49
|
+
strict_1.default.equal(connectedUrl, "ws://127.0.0.1:9230/devtools/page/2");
|
|
50
|
+
client.close();
|
|
51
|
+
});
|
|
38
52
|
(0, node_test_1.default)("ChatGPT CDP 只返回本轮新用户消息之后的回复", async () => {
|
|
39
53
|
let messageHandler;
|
|
40
54
|
let sentText = "";
|
|
@@ -428,3 +428,41 @@ const channel_sessions_1 = require("../src-ts/channel-sessions");
|
|
|
428
428
|
strict_1.default.equal(manifest.clientDownloadUrl, "https://tongyi.aliyun.com/");
|
|
429
429
|
strict_1.default.throws(() => validateManifest({ id: "demo", version: "1.0.0", entry: "index.js", apiVersion: "1", capabilities: ["chat"], clientDownloadUrl: "file:///C:/QwenWorkCN.exe" }), /客户端下载地址/);
|
|
430
430
|
});
|
|
431
|
+
(0, node_test_1.default)("启动失败清理实例和路由,再次启动及重载可以恢复", async () => {
|
|
432
|
+
const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "mab-recovery-"));
|
|
433
|
+
try {
|
|
434
|
+
const dir = (0, node_path_1.join)(root, "recover");
|
|
435
|
+
await (0, promises_1.mkdir)(dir);
|
|
436
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "plugin.json"), JSON.stringify({ id: "recover", version: "1.0.0", entry: "index.js", apiVersion: "1", capabilities: [] }));
|
|
437
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "index.js"), `module.exports = class {
|
|
438
|
+
async activate(ctx) { this.ctx = ctx; ctx.http.register('GET','/partial',(_,res)=>res.end('ok')); if(globalThis.recoveryFail) throw new Error('桌面连接失败'); }
|
|
439
|
+
async deactivate() { globalThis.recoveryCleaned = (globalThis.recoveryCleaned || 0) + 1; }
|
|
440
|
+
async health() { return {status: globalThis.recoveryOffline ? 'failed' : 'ready'}; }
|
|
441
|
+
}`);
|
|
442
|
+
const manager = new plugin_manager_1.PluginManager({ pluginsDir: root, stateDir: (0, node_path_1.join)(root, "state") });
|
|
443
|
+
await manager.discover();
|
|
444
|
+
globalThis.recoveryFail = true;
|
|
445
|
+
await strict_1.default.rejects(manager.start("recover"), /桌面连接失败/);
|
|
446
|
+
strict_1.default.equal(manager.list()[0].status, "failed");
|
|
447
|
+
strict_1.default.equal(globalThis.recoveryCleaned, 1);
|
|
448
|
+
strict_1.default.equal(manager.get("recover")?.instance, undefined);
|
|
449
|
+
strict_1.default.equal(await manager.dispatchHttp({ method: "GET", url: "/partial" }, {}), false);
|
|
450
|
+
globalThis.recoveryFail = false;
|
|
451
|
+
await manager.start("recover");
|
|
452
|
+
strict_1.default.equal(manager.list()[0].status, "ready");
|
|
453
|
+
strict_1.default.equal(manager.list()[0].error, undefined);
|
|
454
|
+
await manager.stop("recover");
|
|
455
|
+
globalThis.recoveryFail = true;
|
|
456
|
+
await strict_1.default.rejects(manager.reload("recover"), /桌面连接失败/);
|
|
457
|
+
globalThis.recoveryFail = false;
|
|
458
|
+
globalThis.recoveryOffline = false;
|
|
459
|
+
await manager.reload("recover");
|
|
460
|
+
strict_1.default.equal(manager.list()[0].status, "ready");
|
|
461
|
+
await manager.stop("recover");
|
|
462
|
+
}
|
|
463
|
+
finally {
|
|
464
|
+
for (const key of ['recoveryFail', 'recoveryCleaned', 'recoveryOffline'])
|
|
465
|
+
delete globalThis[key];
|
|
466
|
+
await (0, promises_1.rm)(root, { recursive: true, force: true });
|
|
467
|
+
}
|
|
468
|
+
});
|
package/package.json
CHANGED
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
item.type = "button";
|
|
109
109
|
item.textContent = label;
|
|
110
110
|
item.title = title;
|
|
111
|
-
item.onclick = () => { void run().catch((cause) => error(String(cause))); };
|
|
111
|
+
item.onclick = () => { item.disabled = true; void run().catch((cause) => error(String(cause))).finally(() => { item.disabled = false; void load(false); }); };
|
|
112
112
|
return item;
|
|
113
113
|
}
|
|
114
114
|
async function load(refreshCatalog = true) {
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
}
|
|
76
76
|
function button(label: string, title: string, run: () => Promise<void>): HTMLButtonElement {
|
|
77
77
|
const item = document.createElement("button"); item.type = "button"; item.textContent = label; item.title = title;
|
|
78
|
-
item.onclick = () => { void run().catch((cause) => error(String(cause))); }; return item;
|
|
78
|
+
item.onclick = () => { item.disabled = true; void run().catch((cause) => error(String(cause))).finally(() => { item.disabled = false; void load(false); }); }; return item;
|
|
79
79
|
}
|
|
80
80
|
async function load(refreshCatalog = true): Promise<void> {
|
|
81
81
|
if (loading) return;
|
|
@@ -5,6 +5,7 @@ const node_child_process_1 = require("node:child_process");
|
|
|
5
5
|
exports.editorSelector = '[data-codex-composer="true"][contenteditable="true"]';
|
|
6
6
|
exports.turnSelector = '[data-chatgpt-conversation-turn="true"]';
|
|
7
7
|
exports.userBubbleSelector = "[data-user-message-bubble]";
|
|
8
|
+
const assistantMessageSelector = '[data-markdown-text-style="assistant-message"]';
|
|
8
9
|
const turnIdAttribute = "data-chatgpt-conversation-turn-id";
|
|
9
10
|
const contentUnitSelector = "[data-content-search-unit-key]";
|
|
10
11
|
const sendButtonSelector = 'button[aria-label="Send"], button[aria-label="发送"]';
|
|
@@ -88,7 +89,8 @@ class ChatgptCdpClient {
|
|
|
88
89
|
if (!response.ok)
|
|
89
90
|
throw new Error(`ChatGPT CDP /json HTTP ${response.status}`);
|
|
90
91
|
const targets = await response.json();
|
|
91
|
-
const page = targets.find((target) => target.type === "page" && /chatgpt|codex/i.test(`${target.title} ${target.url}`)
|
|
92
|
+
const page = targets.find((target) => target.type === "page" && (/chatgpt|codex/i.test(`${target.title} ${target.url}`)
|
|
93
|
+
|| target.url === "app://-/index.html"));
|
|
92
94
|
if (page?.webSocketDebuggerUrl)
|
|
93
95
|
return page;
|
|
94
96
|
last = new Error(`未找到 ChatGPT/Codex 页面 target(端口 ${this.port})`);
|
|
@@ -111,6 +113,7 @@ class ChatgptCdpClient {
|
|
|
111
113
|
await this.waitFor(`Boolean(document.querySelector(${JSON.stringify(exports.editorSelector)}))`, "ChatGPT 桌面端输入框未就绪");
|
|
112
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 })))`);
|
|
113
115
|
const beforeTurns = JSON.parse(String(beforeTurnsJson || "[]"));
|
|
116
|
+
const beforeAssistantCount = Number(await this.evaluate(`document.querySelectorAll(${JSON.stringify(assistantMessageSelector)}).length`));
|
|
114
117
|
const ready = await this.evaluate(`(() => {
|
|
115
118
|
const editor = document.querySelector(${JSON.stringify(exports.editorSelector)});
|
|
116
119
|
if (!editor || editor.innerText.trim()) return false;
|
|
@@ -164,7 +167,13 @@ class ChatgptCdpClient {
|
|
|
164
167
|
const fresh = oldIndex < 0 || index >= before.length || users.length > (old?.users ?? 0);
|
|
165
168
|
return fresh && users.item(users.length - 1)?.innerText.trim() === ${JSON.stringify(text.trim())};
|
|
166
169
|
});
|
|
167
|
-
if (!turn)
|
|
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
|
+
}
|
|
168
177
|
const user = turn.querySelectorAll(${JSON.stringify(exports.userBubbleSelector)}).item(turn.querySelectorAll(${JSON.stringify(exports.userBubbleSelector)}).length - 1);
|
|
169
178
|
const unit = user?.closest(${JSON.stringify(contentUnitSelector)});
|
|
170
179
|
const units = [...turn.querySelectorAll(${JSON.stringify(contentUnitSelector)})];
|