mu-agent-bridge 1.0.22 → 1.0.24
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 +4 -0
- package/dist/src-ts/cli.js +64 -4
- package/dist/src-ts/control-server.js +6 -0
- package/dist/src-ts/plugin-manager.js +166 -6
- package/dist/src-ts/plugins/abweb/public/app.js +75 -3
- package/dist/src-ts/plugins/abweb/public/index.html +1 -0
- package/dist/src-ts/plugins/abweb/public/style.css +3 -0
- package/dist/src-ts/plugins/chatgpt/cdp-client.js +47 -0
- package/dist/src-ts/plugins/chatgpt/index.js +110 -6
- package/dist/src-ts/plugins/chatgpt/session-store.js +91 -0
- package/dist/tests-ts/abweb-plugin.test.js +3 -0
- package/dist/tests-ts/chatgpt-plugin.test.js +209 -0
- package/dist/tests-ts/control-server.test.js +36 -0
- package/dist/tests-ts/plugin-manager.test.js +150 -0
- package/package.json +1 -1
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.js +75 -3
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.ts +40 -4
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/index.html +1 -0
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/style.css +3 -0
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +47 -0
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +110 -6
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/session-store.js +91 -0
- package/plugins/chatgpt/plugin.json +1 -1
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ChatgptCdpClient = exports.ChatgptPlugin = exports.chatgptManifest = void 0;
|
|
4
4
|
const cdp_client_1 = require("./cdp-client");
|
|
5
|
+
const session_store_1 = require("./session-store");
|
|
5
6
|
exports.chatgptManifest = {
|
|
6
7
|
id: "chatgpt",
|
|
7
|
-
version: "1.0.
|
|
8
|
+
version: "1.0.1",
|
|
8
9
|
displayName: "ChatGPT(Codex)",
|
|
9
10
|
type: "agent",
|
|
10
11
|
entry: "dist/index.js",
|
|
@@ -25,6 +26,8 @@ class ChatgptPlugin {
|
|
|
25
26
|
failure;
|
|
26
27
|
remote = false;
|
|
27
28
|
queue = Promise.resolve();
|
|
29
|
+
store;
|
|
30
|
+
logger;
|
|
28
31
|
constructor(options = {}) {
|
|
29
32
|
this.options = options;
|
|
30
33
|
}
|
|
@@ -48,6 +51,11 @@ class ChatgptPlugin {
|
|
|
48
51
|
localPort: port(config.localCdpPort, 9230),
|
|
49
52
|
} : undefined;
|
|
50
53
|
this.remote = Boolean(ssh);
|
|
54
|
+
this.logger = context.logger;
|
|
55
|
+
this.store = new session_store_1.ChatgptSessionStore(context.dataDir, JSON.stringify(ssh
|
|
56
|
+
? [ssh.host, ssh.user, ssh.port, ssh.remotePort]
|
|
57
|
+
: ["local", port(config.localCdpPort, 9230)]));
|
|
58
|
+
await this.store.load();
|
|
51
59
|
this.failure = undefined;
|
|
52
60
|
this.client = this.options.client ?? new cdp_client_1.ChatgptCdpClient({
|
|
53
61
|
port: port(config.localCdpPort, 9230),
|
|
@@ -60,11 +68,13 @@ class ChatgptPlugin {
|
|
|
60
68
|
catch (error) {
|
|
61
69
|
this.failure = error instanceof Error ? error.message : String(error);
|
|
62
70
|
context.logger.warn(this.failure);
|
|
63
|
-
|
|
71
|
+
if (!this.store.sessions.length)
|
|
72
|
+
throw error;
|
|
64
73
|
}
|
|
65
74
|
}
|
|
66
75
|
async deactivate() {
|
|
67
76
|
await this.queue;
|
|
77
|
+
await this.store?.drain();
|
|
68
78
|
this.client?.close();
|
|
69
79
|
this.client = undefined;
|
|
70
80
|
this.remote = false;
|
|
@@ -72,17 +82,111 @@ class ChatgptPlugin {
|
|
|
72
82
|
async health() {
|
|
73
83
|
return this.client?.connected()
|
|
74
84
|
? { status: "ready", details: { transport: "cdp", remote: this.remote } }
|
|
75
|
-
: { status: "failed", message: this.failure ?? "ChatGPT CDP 未连接", details: { transport: "cdp", remote: this.remote } };
|
|
85
|
+
: { status: this.client && this.store?.sessions.length ? "degraded" : "failed", message: this.failure ?? "ChatGPT CDP 未连接", details: { transport: "cdp", remote: this.remote } };
|
|
76
86
|
}
|
|
77
87
|
async handle(request) {
|
|
78
88
|
if (!request.task.trim())
|
|
79
89
|
throw new Error("消息内容不能为空");
|
|
80
|
-
if (!this.client
|
|
81
|
-
throw new Error("ChatGPT
|
|
82
|
-
const run = this.queue.then(() => this.
|
|
90
|
+
if (!this.client || !this.store)
|
|
91
|
+
throw new Error("ChatGPT 插件未启动");
|
|
92
|
+
const run = this.queue.then(() => this.run(request));
|
|
83
93
|
this.queue = run.then(() => undefined, () => undefined);
|
|
84
94
|
return { text: await run, sessionId: request.sessionId };
|
|
85
95
|
}
|
|
96
|
+
persist() {
|
|
97
|
+
void this.store.save().catch(error => this.logger?.warn("ChatGPT 会话缓存保存失败", { error: String(error) }));
|
|
98
|
+
}
|
|
99
|
+
async synchronize() {
|
|
100
|
+
if (!this.client.connected())
|
|
101
|
+
await this.client.connect();
|
|
102
|
+
if (!this.client.listSessions)
|
|
103
|
+
throw new Error("当前 ChatGPT 客户端不支持会话列表");
|
|
104
|
+
this.store.synchronize(await this.client.listSessions());
|
|
105
|
+
this.persist();
|
|
106
|
+
}
|
|
107
|
+
async select(session) {
|
|
108
|
+
if (!this.client.connected())
|
|
109
|
+
await this.client.connect();
|
|
110
|
+
if (!this.client.switchSession)
|
|
111
|
+
throw new Error("当前 ChatGPT 客户端不支持会话切换");
|
|
112
|
+
const active = await this.client.activeSession?.();
|
|
113
|
+
if (active?.id === session.id && active.host === session.host)
|
|
114
|
+
return;
|
|
115
|
+
try {
|
|
116
|
+
await this.client.switchSession(session);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
// 目标消失时只重新同步一次;不在其他会话中代替发送。
|
|
120
|
+
if (!String(error).includes("找不到目标会话"))
|
|
121
|
+
throw error;
|
|
122
|
+
await this.synchronize();
|
|
123
|
+
const updated = this.store.sessions.find(item => item.id === session.id && item.host === session.host);
|
|
124
|
+
if (!updated)
|
|
125
|
+
throw new Error("ChatGPT 目标会话已删除或不在当前侧栏,请刷新最近任务");
|
|
126
|
+
await this.client.switchSession(updated);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async run(request) {
|
|
130
|
+
const store = this.store;
|
|
131
|
+
const text = request.task.trim();
|
|
132
|
+
const refresh = /^(?:刷新最近任务|刷新任务列表|刷新最近会话)$/.test(text);
|
|
133
|
+
const listing = /^(?:最近任务|列出最近会话|有哪些已保存的 ChatGPT 任务)[??。]?$/.test(text);
|
|
134
|
+
if (refresh || listing) {
|
|
135
|
+
if (refresh || !store.fresh()) {
|
|
136
|
+
try {
|
|
137
|
+
await this.synchronize();
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (refresh || !store.sessions.length)
|
|
141
|
+
throw error;
|
|
142
|
+
return `远程暂不可用,以下是本地缓存(同步时间:${store.updatedAt}):\n${(0, session_store_1.formatSessions)(store.sessions)}`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return `最近会话(当前侧栏缓存;同步时间:${store.updatedAt}):\n${(0, session_store_1.formatSessions)(store.sessions)}`;
|
|
146
|
+
}
|
|
147
|
+
const explicit = text.match(/^切换会话\s*[::]\s*([^;;\n]+)(?:[;;\n]\s*([\s\S]+))?$/);
|
|
148
|
+
const natural = text.match(/^(?:继续处理|接着处理|继续任务|切换到)\s*(.+?)[。]?$/);
|
|
149
|
+
const query = explicit?.[1].trim() ?? natural?.[1].trim();
|
|
150
|
+
let selected;
|
|
151
|
+
if (query) {
|
|
152
|
+
if (!store.fresh())
|
|
153
|
+
await this.synchronize();
|
|
154
|
+
let matches = store.match(query);
|
|
155
|
+
if (!matches.length) {
|
|
156
|
+
await this.synchronize();
|
|
157
|
+
matches = store.match(query);
|
|
158
|
+
}
|
|
159
|
+
if (!matches.length)
|
|
160
|
+
return `没有找到会话:${query}。请发送“最近任务”查看可用标题。`;
|
|
161
|
+
if (matches.length > 1)
|
|
162
|
+
return `找到多个会话,请用完整标题或会话 ID 指定:\n${(0, session_store_1.formatSessions)(matches)}`;
|
|
163
|
+
selected = matches[0];
|
|
164
|
+
await this.select(selected);
|
|
165
|
+
if (request.sessionId)
|
|
166
|
+
store.bindings[request.sessionId] = selected;
|
|
167
|
+
this.persist();
|
|
168
|
+
if ((explicit && !explicit[2]) || text.startsWith("切换到"))
|
|
169
|
+
return `已切换到:${selected.title}。后续消息将继续此会话。`;
|
|
170
|
+
}
|
|
171
|
+
else if (request.sessionId && store.bindings[request.sessionId]) {
|
|
172
|
+
selected = store.bindings[request.sessionId];
|
|
173
|
+
await this.select(selected);
|
|
174
|
+
}
|
|
175
|
+
if (!this.client.connected())
|
|
176
|
+
await this.client.connect();
|
|
177
|
+
selected ??= await this.client.activeSession?.();
|
|
178
|
+
const message = explicit?.[2]?.trim() || request.task;
|
|
179
|
+
const reply = await this.client.direct(message, request.timeoutMs);
|
|
180
|
+
if (selected) {
|
|
181
|
+
const current = await this.client.activeSession?.().catch(() => undefined);
|
|
182
|
+
const entry = current?.id === selected.id && current.host === selected.host ? current : selected;
|
|
183
|
+
store.remember(entry, message, reply);
|
|
184
|
+
if (request.sessionId)
|
|
185
|
+
store.bindings[request.sessionId] = entry;
|
|
186
|
+
this.persist();
|
|
187
|
+
}
|
|
188
|
+
return reply;
|
|
189
|
+
}
|
|
86
190
|
}
|
|
87
191
|
exports.ChatgptPlugin = ChatgptPlugin;
|
|
88
192
|
var cdp_client_2 = require("./cdp-client");
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ChatgptSessionStore = void 0;
|
|
4
|
+
exports.validSession = validSession;
|
|
5
|
+
exports.formatSessions = formatSessions;
|
|
6
|
+
const promises_1 = require("node:fs/promises");
|
|
7
|
+
const node_path_1 = require("node:path");
|
|
8
|
+
function validSession(value) {
|
|
9
|
+
if (!value || typeof value !== "object")
|
|
10
|
+
return false;
|
|
11
|
+
const item = value;
|
|
12
|
+
return ["id", "host", "title"].every(key => typeof item[key] === "string" && Boolean(item[key]))
|
|
13
|
+
&& ["updatedAt", "lastUserMessage", "lastAssistantReply"].every(key => item[key] === undefined || typeof item[key] === "string");
|
|
14
|
+
}
|
|
15
|
+
class ChatgptSessionStore {
|
|
16
|
+
directory;
|
|
17
|
+
endpoint;
|
|
18
|
+
sessions = [];
|
|
19
|
+
bindings = {};
|
|
20
|
+
updatedAt = "";
|
|
21
|
+
writes = Promise.resolve();
|
|
22
|
+
constructor(directory, endpoint) {
|
|
23
|
+
this.directory = directory;
|
|
24
|
+
this.endpoint = endpoint;
|
|
25
|
+
}
|
|
26
|
+
async load() {
|
|
27
|
+
if (!this.directory)
|
|
28
|
+
return;
|
|
29
|
+
try {
|
|
30
|
+
const value = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(this.directory, "chatgpt-sessions.json"), "utf8"));
|
|
31
|
+
if (value.version !== 1 || value.endpoint !== this.endpoint || !Array.isArray(value.sessions)
|
|
32
|
+
|| !value.sessions.every(validSession) || typeof value.updatedAt !== "string"
|
|
33
|
+
|| !value.bindings || typeof value.bindings !== "object" || Array.isArray(value.bindings)
|
|
34
|
+
|| !Object.values(value.bindings).every(validSession))
|
|
35
|
+
return;
|
|
36
|
+
this.sessions = value.sessions.slice(0, 20);
|
|
37
|
+
this.bindings = Object.fromEntries(Object.entries(value.bindings));
|
|
38
|
+
this.updatedAt = value.updatedAt;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error.code !== "ENOENT" && !(error instanceof SyntaxError))
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
fresh() { return Date.now() - Date.parse(this.updatedAt) < 10 * 60_000; }
|
|
46
|
+
synchronize(entries) {
|
|
47
|
+
const previous = this.sessions;
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
this.sessions = entries.filter(validSession).filter(item => {
|
|
50
|
+
const key = JSON.stringify([item.host, item.id]);
|
|
51
|
+
if (seen.has(key))
|
|
52
|
+
return false;
|
|
53
|
+
seen.add(key);
|
|
54
|
+
return true;
|
|
55
|
+
}).map(item => ({ ...previous.find(old => old.id === item.id && old.host === item.host), ...item }))
|
|
56
|
+
.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? "")).slice(0, 20);
|
|
57
|
+
this.updatedAt = new Date().toISOString();
|
|
58
|
+
}
|
|
59
|
+
remember(entry, user, reply) {
|
|
60
|
+
const next = { ...entry, lastUserMessage: user.slice(0, 500), lastAssistantReply: reply.slice(0, 500), updatedAt: new Date().toISOString() };
|
|
61
|
+
this.sessions = [next, ...this.sessions.filter(old => old.id !== entry.id || old.host !== entry.host)].slice(0, 20);
|
|
62
|
+
}
|
|
63
|
+
match(query) {
|
|
64
|
+
const text = query.trim().toLocaleLowerCase();
|
|
65
|
+
if (!text)
|
|
66
|
+
return [];
|
|
67
|
+
const exact = this.sessions.filter(item => item.id === query || item.title.toLocaleLowerCase() === text);
|
|
68
|
+
return exact.length ? exact : this.sessions.filter(item => `${item.title}\n${item.lastUserMessage ?? ""}`.toLocaleLowerCase().includes(text));
|
|
69
|
+
}
|
|
70
|
+
save() {
|
|
71
|
+
const snapshot = { version: 1, endpoint: this.endpoint, updatedAt: this.updatedAt, sessions: this.sessions, bindings: this.bindings };
|
|
72
|
+
const text = `${JSON.stringify(snapshot, null, 2)}\n`;
|
|
73
|
+
const write = this.writes.catch(() => { }).then(async () => {
|
|
74
|
+
if (!this.directory)
|
|
75
|
+
return;
|
|
76
|
+
await (0, promises_1.mkdir)(this.directory, { recursive: true });
|
|
77
|
+
const file = (0, node_path_1.join)(this.directory, "chatgpt-sessions.json");
|
|
78
|
+
await (0, promises_1.writeFile)(`${file}.tmp`, text, { mode: 0o600 });
|
|
79
|
+
await (0, promises_1.rename)(`${file}.tmp`, file);
|
|
80
|
+
});
|
|
81
|
+
this.writes = write;
|
|
82
|
+
return write;
|
|
83
|
+
}
|
|
84
|
+
async drain() { await this.writes.catch(() => { }); }
|
|
85
|
+
}
|
|
86
|
+
exports.ChatgptSessionStore = ChatgptSessionStore;
|
|
87
|
+
function formatSessions(entries) {
|
|
88
|
+
if (!entries.length)
|
|
89
|
+
return "当前侧栏暂无可用会话。";
|
|
90
|
+
return entries.map((item, index) => `${index + 1}. ${item.title}\n 主机:${item.host};会话:${item.id}${item.updatedAt ? `;最近使用:${item.updatedAt}` : ""}`).join("\n");
|
|
91
|
+
}
|
|
@@ -56,6 +56,8 @@ function createContext(config = {}) {
|
|
|
56
56
|
strict_1.default.doesNotMatch(String(response.body), /id="plugin-picker"/);
|
|
57
57
|
strict_1.default.match(String(response.body), /id="channel-tabs"/);
|
|
58
58
|
strict_1.default.match(String(response.body), /id="channel-panel"/);
|
|
59
|
+
strict_1.default.match(String(response.body), /id="plugin-trust"/);
|
|
60
|
+
strict_1.default.match(String(response.body), /自动升级信任/);
|
|
59
61
|
strict_1.default.match(String(response.body), /id="pair-code"/);
|
|
60
62
|
strict_1.default.doesNotMatch(String(response.body), /id="access-token"|访问令牌/);
|
|
61
63
|
strict_1.default.doesNotMatch(String(response.body), /id="create-aid-form"/);
|
|
@@ -67,6 +69,7 @@ function createContext(config = {}) {
|
|
|
67
69
|
strict_1.default.match(String(script.body), /"已安装"/);
|
|
68
70
|
strict_1.default.match(String(script.body), /\/update/);
|
|
69
71
|
strict_1.default.match(String(script.body), /managementPage/);
|
|
72
|
+
strict_1.default.match(String(script.body), /\/api\/plugin-trust/);
|
|
70
73
|
strict_1.default.doesNotMatch(String(script.body), /mab-token/);
|
|
71
74
|
strict_1.default.match(String(script.body), /setInterval/);
|
|
72
75
|
strict_1.default.match(String(script.body), /300000/);
|
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
@@ -110,3 +143,179 @@ const chatgpt_1 = require("../src-ts/plugins/chatgpt");
|
|
|
110
143
|
client.close();
|
|
111
144
|
}
|
|
112
145
|
});
|
|
146
|
+
(0, node_test_1.default)("ChatGPT 最近任务走缓存,频道切换后后续消息回到绑定会话", async () => {
|
|
147
|
+
const { mkdtemp, rm } = await Promise.resolve().then(() => __importStar(require("node:fs/promises")));
|
|
148
|
+
const { tmpdir } = await Promise.resolve().then(() => __importStar(require("node:os")));
|
|
149
|
+
const { join } = await Promise.resolve().then(() => __importStar(require("node:path")));
|
|
150
|
+
const dataDir = await mkdtemp(join(tmpdir(), "chatgpt-memory-"));
|
|
151
|
+
const sessions = [
|
|
152
|
+
{ id: "s1", host: "h", title: "语音插件", updatedAt: "2026-10-13T00:00:00.000Z" },
|
|
153
|
+
{ id: "s2", host: "h", title: "远程测试", updatedAt: "2026-10-13T00:00:00.000Z" },
|
|
154
|
+
];
|
|
155
|
+
let selected = sessions[0];
|
|
156
|
+
let lists = 0;
|
|
157
|
+
const sent = [];
|
|
158
|
+
const switched = [];
|
|
159
|
+
const client = { connect: async () => { }, close() { }, connected: () => true,
|
|
160
|
+
listSessions: async () => { lists++; return sessions; },
|
|
161
|
+
activeSession: async () => selected,
|
|
162
|
+
switchSession: async (entry) => { selected = entry; switched.push(entry.id); },
|
|
163
|
+
direct: async (text) => { sent.push(`${selected.id}:${text}`); return "收到"; },
|
|
164
|
+
};
|
|
165
|
+
const plugin = new chatgpt_1.ChatgptPlugin({ client });
|
|
166
|
+
const context = { config: {}, dataDir, logger: { info() { }, warn() { }, error() { } } };
|
|
167
|
+
try {
|
|
168
|
+
await plugin.activate(context);
|
|
169
|
+
strict_1.default.match((await plugin.handle({ task: "最近任务", sessionId: "channel-a" })).text, /语音插件/);
|
|
170
|
+
await plugin.handle({ task: "最近任务", sessionId: "channel-a" });
|
|
171
|
+
strict_1.default.equal(lists, 1);
|
|
172
|
+
strict_1.default.deepEqual(sent, []);
|
|
173
|
+
strict_1.default.match((await plugin.handle({ task: "切换会话:远程测试", sessionId: "channel-a" })).text, /已切换/);
|
|
174
|
+
await plugin.handle({ task: "继续处理语音插件", sessionId: "channel-b" });
|
|
175
|
+
await plugin.handle({ task: "检查进度", sessionId: "channel-a" });
|
|
176
|
+
strict_1.default.equal(sent.at(-1), "s2:检查进度");
|
|
177
|
+
strict_1.default.ok(switched.includes("s2"));
|
|
178
|
+
await plugin.deactivate();
|
|
179
|
+
const second = new chatgpt_1.ChatgptPlugin({ client });
|
|
180
|
+
await second.activate(context);
|
|
181
|
+
await second.handle({ task: "最近任务" });
|
|
182
|
+
strict_1.default.equal(lists, 1);
|
|
183
|
+
await second.deactivate();
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
await rm(dataDir, { recursive: true, force: true });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
(0, node_test_1.default)("ChatGPT 切换歧义或失败时不发送消息,普通消息不按标题自动切换", async () => {
|
|
190
|
+
const sessions = [{ id: "a", host: "h", title: "语音接口" }, { id: "b", host: "h", title: "语音测试" }];
|
|
191
|
+
let sends = 0;
|
|
192
|
+
const client = { connect: async () => { }, close() { }, connected: () => true,
|
|
193
|
+
listSessions: async () => sessions, activeSession: async () => undefined,
|
|
194
|
+
switchSession: async () => { throw new Error("目标不可用"); },
|
|
195
|
+
direct: async () => { sends++; return "收到"; },
|
|
196
|
+
};
|
|
197
|
+
const plugin = new chatgpt_1.ChatgptPlugin({ client });
|
|
198
|
+
await plugin.activate({ config: {}, logger: { info() { }, warn() { }, error() { } } });
|
|
199
|
+
strict_1.default.match((await plugin.handle({ task: "继续处理语音" })).text, /多个/);
|
|
200
|
+
strict_1.default.equal(sends, 0);
|
|
201
|
+
await strict_1.default.rejects(plugin.handle({ task: "切换会话:语音接口" }), /不可用/);
|
|
202
|
+
strict_1.default.equal(sends, 0);
|
|
203
|
+
strict_1.default.equal((await plugin.handle({ task: "语音接口怎么写" })).text, "收到");
|
|
204
|
+
strict_1.default.equal(sends, 1);
|
|
205
|
+
await plugin.deactivate();
|
|
206
|
+
});
|
|
207
|
+
(0, node_test_1.default)("ChatGPT 固定 CDP 操作读取侧栏并按主机和 ID 切换,未发送内容阻止切换", async () => {
|
|
208
|
+
const { runInNewContext } = await Promise.resolve().then(() => __importStar(require("node:vm")));
|
|
209
|
+
const entries = [{ id: 'a"\\', host: "h1", title: "旧会话" }, { id: 'a"\\', host: "h2", title: "目标会话" }];
|
|
210
|
+
let selected = 0;
|
|
211
|
+
const editor = { innerText: "" };
|
|
212
|
+
const nodes = entries.map((entry, index) => ({
|
|
213
|
+
getAttribute: (key) => key.endsWith("selected") ? String(index === selected)
|
|
214
|
+
: key.endsWith("host-id") ? entry.host : key.endsWith("thread-id") ? entry.id : entry.title,
|
|
215
|
+
click: () => { selected = index; },
|
|
216
|
+
}));
|
|
217
|
+
const document = {
|
|
218
|
+
querySelectorAll: () => nodes,
|
|
219
|
+
querySelector: (selector) => selector.includes("selected") ? nodes[selected] : selector.includes("composer") ? editor : null,
|
|
220
|
+
};
|
|
221
|
+
let onMessage = null;
|
|
222
|
+
const client = new chatgpt_1.ChatgptCdpClient({
|
|
223
|
+
fetchImpl: async () => new Response(JSON.stringify([{ type: "page", url: "app://-/index.html", webSocketDebuggerUrl: "ws://localhost:9230/a" }])),
|
|
224
|
+
webSocketFactory: () => ({
|
|
225
|
+
set onmessage(fn) { onMessage = fn; },
|
|
226
|
+
set onopen(fn) { queueMicrotask(fn); },
|
|
227
|
+
set onclose(_) { }, set onerror(_) { }, close() { },
|
|
228
|
+
send(raw) {
|
|
229
|
+
const command = JSON.parse(raw);
|
|
230
|
+
let result;
|
|
231
|
+
try {
|
|
232
|
+
result = { result: { value: JSON.parse(JSON.stringify(runInNewContext(command.params.expression, { document }))) } };
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
result = { exceptionDetails: { text: String(error) } };
|
|
236
|
+
}
|
|
237
|
+
queueMicrotask(() => onMessage?.({ data: JSON.stringify({ id: command.id, result }) }));
|
|
238
|
+
},
|
|
239
|
+
}),
|
|
240
|
+
});
|
|
241
|
+
await client.connect();
|
|
242
|
+
try {
|
|
243
|
+
strict_1.default.deepEqual(await client.listSessions(), entries);
|
|
244
|
+
await client.switchSession(entries[1]);
|
|
245
|
+
strict_1.default.equal(selected, 1);
|
|
246
|
+
strict_1.default.deepEqual(await client.activeSession(), entries[1]);
|
|
247
|
+
editor.innerText = "未发送草稿";
|
|
248
|
+
await strict_1.default.rejects(client.switchSession(entries[0]), /未发送内容/);
|
|
249
|
+
strict_1.default.equal(selected, 1);
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
client.close();
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
(0, node_test_1.default)("ChatGPT 缓存限制容量、可恢复并按连接隔离,损坏或未知版本可重新生成", async () => {
|
|
256
|
+
const { ChatgptSessionStore } = await Promise.resolve().then(() => __importStar(require("../src-ts/plugins/chatgpt/session-store")));
|
|
257
|
+
const { mkdtemp, readFile, writeFile, rm } = await Promise.resolve().then(() => __importStar(require("node:fs/promises")));
|
|
258
|
+
const { tmpdir } = await Promise.resolve().then(() => __importStar(require("node:os")));
|
|
259
|
+
const { join } = await Promise.resolve().then(() => __importStar(require("node:path")));
|
|
260
|
+
const dir = await mkdtemp(join(tmpdir(), "chatgpt-store-"));
|
|
261
|
+
const file = join(dir, "chatgpt-sessions.json");
|
|
262
|
+
try {
|
|
263
|
+
const store = new ChatgptSessionStore(dir, "remote-a");
|
|
264
|
+
store.synchronize(Array.from({ length: 25 }, (_, i) => ({ id: String(i), host: "h", title: `任务${i}` })));
|
|
265
|
+
strict_1.default.equal(store.sessions.length, 20);
|
|
266
|
+
store.remember(store.sessions[2], "HELLO".repeat(200), "回复".repeat(500));
|
|
267
|
+
await store.save();
|
|
268
|
+
const loaded = new ChatgptSessionStore(dir, "remote-a");
|
|
269
|
+
await loaded.load();
|
|
270
|
+
strict_1.default.equal(loaded.sessions[0].id, "2");
|
|
271
|
+
strict_1.default.equal(loaded.sessions[0].lastUserMessage?.length, 500);
|
|
272
|
+
strict_1.default.equal(loaded.match("hello")[0].id, "2");
|
|
273
|
+
const isolated = new ChatgptSessionStore(dir, "remote-b");
|
|
274
|
+
await isolated.load();
|
|
275
|
+
strict_1.default.equal(isolated.sessions.length, 0);
|
|
276
|
+
const value = JSON.parse(await readFile(file, "utf8"));
|
|
277
|
+
await writeFile(file, JSON.stringify({ ...value, version: 999 }));
|
|
278
|
+
const future = new ChatgptSessionStore(dir, "remote-a");
|
|
279
|
+
await future.load();
|
|
280
|
+
strict_1.default.equal(future.sessions.length, 0);
|
|
281
|
+
await writeFile(file, "{broken");
|
|
282
|
+
const broken = new ChatgptSessionStore(dir, "remote-a");
|
|
283
|
+
await broken.load();
|
|
284
|
+
strict_1.default.equal(broken.sessions.length, 0);
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
await rm(dir, { recursive: true, force: true });
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
(0, node_test_1.default)("ChatGPT 远程断开后仍可查询已保存索引,刷新失败不覆盖缓存", async () => {
|
|
291
|
+
const { mkdtemp, rm } = await Promise.resolve().then(() => __importStar(require("node:fs/promises")));
|
|
292
|
+
const { tmpdir } = await Promise.resolve().then(() => __importStar(require("node:os")));
|
|
293
|
+
const { join } = await Promise.resolve().then(() => __importStar(require("node:path")));
|
|
294
|
+
const dir = await mkdtemp(join(tmpdir(), "chatgpt-offline-"));
|
|
295
|
+
let online = true;
|
|
296
|
+
let calls = 0;
|
|
297
|
+
const client = { connect: async () => { if (!online)
|
|
298
|
+
throw new Error("远程不可达"); },
|
|
299
|
+
close() { }, connected: () => online, direct: async () => "回复",
|
|
300
|
+
listSessions: async () => { calls++; return [{ id: "s", title: "记住的任务", host: "h" }]; },
|
|
301
|
+
};
|
|
302
|
+
const context = { config: {}, dataDir: dir, logger: { info() { }, warn() { }, error() { } } };
|
|
303
|
+
try {
|
|
304
|
+
const initial = new chatgpt_1.ChatgptPlugin({ client });
|
|
305
|
+
await initial.activate(context);
|
|
306
|
+
await initial.handle({ task: "最近任务" });
|
|
307
|
+
await initial.deactivate();
|
|
308
|
+
online = false;
|
|
309
|
+
const offline = new chatgpt_1.ChatgptPlugin({ client });
|
|
310
|
+
await offline.activate(context);
|
|
311
|
+
strict_1.default.equal((await offline.health()).status, "degraded");
|
|
312
|
+
strict_1.default.match((await offline.handle({ task: "最近任务" })).text, /记住的任务/);
|
|
313
|
+
strict_1.default.equal(calls, 1);
|
|
314
|
+
await strict_1.default.rejects(offline.handle({ task: "刷新最近任务" }), /不可达/);
|
|
315
|
+
strict_1.default.match((await offline.handle({ task: "最近任务" })).text, /记住的任务/);
|
|
316
|
+
await offline.deactivate();
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
await rm(dir, { recursive: true, force: true });
|
|
320
|
+
}
|
|
321
|
+
});
|
|
@@ -92,6 +92,42 @@ const plugin_manager_1 = require("../src-ts/plugin-manager");
|
|
|
92
92
|
await server.stop();
|
|
93
93
|
}
|
|
94
94
|
});
|
|
95
|
+
(0, node_test_1.default)("信任管理 API 支持查询、变更和拒绝无效配置", async () => {
|
|
96
|
+
const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "mab-trust-api-"));
|
|
97
|
+
const previous = process.env.AGENT_BRIDGE_TRUSTED_PACKAGES;
|
|
98
|
+
try {
|
|
99
|
+
process.env.AGENT_BRIDGE_TRUSTED_PACKAGES = "";
|
|
100
|
+
const dir = (0, node_path_1.join)(root, "plugins", "sample");
|
|
101
|
+
await (0, promises_1.mkdir)(dir, { recursive: true });
|
|
102
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "package.json"), JSON.stringify({ name: "agent-bridge-sample", version: "1.0.0", _npmUser: { name: "m2lihui" } }));
|
|
103
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(dir, "plugin.json"), JSON.stringify({ id: "sample", version: "1.0.0", entry: "index.js", apiVersion: "1", capabilities: [] }));
|
|
104
|
+
const manager = new plugin_manager_1.PluginManager({ pluginsDir: (0, node_path_1.join)(root, "plugins"), stateDir: (0, node_path_1.join)(root, "state"), publisherResolver: async () => "m2lihui" });
|
|
105
|
+
await manager.discover();
|
|
106
|
+
const server = new control_server_1.ControlServer({ manager, host: "127.0.0.1", port: 0 });
|
|
107
|
+
const address = await server.start();
|
|
108
|
+
try {
|
|
109
|
+
const base = `http://${address.host}:${address.port}`;
|
|
110
|
+
const initial = await (await fetch(`${base}/api/plugin-trust`)).json();
|
|
111
|
+
strict_1.default.equal(initial.entries.find((entry) => entry.id === "sample").trusted, true);
|
|
112
|
+
const saved = await fetch(`${base}/api/plugin-trust`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ providers: { m2lihui: "deny" }, plugins: { sample: "allow" } }) });
|
|
113
|
+
strict_1.default.equal(saved.status, 200);
|
|
114
|
+
const after = await (await fetch(`${base}/api/plugin-trust`)).json();
|
|
115
|
+
strict_1.default.equal(after.entries.find((entry) => entry.id === "sample").trusted, true);
|
|
116
|
+
strict_1.default.equal(after.entries.find((entry) => entry.id === "sample").pluginOverride, "allow");
|
|
117
|
+
strict_1.default.equal((await fetch(`${base}/api/plugin-trust`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ providers: { m2lihui: "sometimes" }, plugins: {} }) })).status, 400);
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
await server.stop();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
if (previous === undefined)
|
|
125
|
+
delete process.env.AGENT_BRIDGE_TRUSTED_PACKAGES;
|
|
126
|
+
else
|
|
127
|
+
process.env.AGENT_BRIDGE_TRUSTED_PACKAGES = previous;
|
|
128
|
+
await (0, promises_1.rm)(root, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
95
131
|
(0, node_test_1.default)("首选后台端口被占用时回退到可用端口", async () => {
|
|
96
132
|
const occupied = (0, node_net_1.createServer)();
|
|
97
133
|
await new Promise((resolve) => occupied.listen(0, "127.0.0.1", resolve));
|