mu-agent-bridge 1.0.35 → 1.0.39
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 +2 -0
- package/dist/src-ts/channel-sessions.js +90 -24
- package/dist/src-ts/control-server.js +48 -7
- package/dist/src-ts/plugin-manager.js +47 -0
- package/dist/src-ts/plugins/chatgpt/cdp-client.js +72 -0
- package/dist/src-ts/plugins/chatgpt/index.js +122 -4
- package/dist/src-ts/plugins/doubao/cdp-client.js +243 -18
- package/dist/src-ts/plugins/doubao/index.js +81 -5
- package/dist/src-ts/plugins/doubao/session-history.js +123 -0
- package/dist/src-ts/plugins/dsh/index.js +16 -2
- package/dist/src-ts/plugins/dsh/session-history.js +371 -0
- package/dist/src-ts/plugins/qwenwork/cdp-client.js +8 -0
- package/dist/src-ts/plugins/qwenwork/index.js +12 -1
- package/dist/src-ts/plugins/qwenwork/session-history.js +245 -0
- package/dist/src-ts/plugins/workbuddy/cdp-client.js +33 -0
- package/dist/src-ts/plugins/workbuddy/index.js +21 -1
- package/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
- package/dist/src-ts/session-archive.js +509 -0
- package/dist/src-ts/session-history.js +2 -0
- package/dist/src-ts/session-management.js +665 -0
- package/dist/src-ts/session-summary.js +171 -0
- package/dist/tests-ts/channel-sessions.test.js +233 -2
- package/dist/tests-ts/chatgpt-history.test.js +197 -0
- package/dist/tests-ts/control-server.test.js +39 -0
- package/dist/tests-ts/doubao-history.test.js +172 -0
- package/dist/tests-ts/doubao-plugin.test.js +124 -3
- package/dist/tests-ts/dsh-history.test.js +165 -0
- package/dist/tests-ts/native-session-management.test.js +195 -0
- package/dist/tests-ts/plugin-manager.test.js +254 -3
- package/dist/tests-ts/qwenwork-history.test.js +339 -0
- package/dist/tests-ts/session-archive.test.js +422 -0
- package/dist/tests-ts/session-management.test.js +323 -0
- package/dist/tests-ts/session-summary.test.js +199 -0
- package/dist/tests-ts/workbuddy-history.test.js +256 -0
- package/package.json +1 -1
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +72 -0
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +122 -4
- package/plugins/chatgpt/package.json +1 -1
- package/plugins/chatgpt/plugin.json +14 -1
- package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +243 -18
- package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +81 -5
- package/plugins/doubao/dist/src-ts/plugins/doubao/session-history.js +123 -0
- package/plugins/doubao/package.json +1 -1
- package/plugins/doubao/plugin.json +1 -1
- package/plugins/dsh/config.schema.json +2 -1
- package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +16 -2
- package/plugins/dsh/dist/src-ts/plugins/dsh/session-history.js +371 -0
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/plugin.json +1 -1
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +8 -0
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +12 -1
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/session-history.js +245 -0
- package/plugins/qwenwork/package.json +1 -1
- package/plugins/qwenwork/plugin.json +1 -1
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +33 -0
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +21 -1
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
- package/plugins/workbuddy/package.json +1 -1
- package/plugins/workbuddy/plugin.json +1 -1
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
7
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
8
|
+
const promises_1 = require("node:fs/promises");
|
|
9
|
+
const node_path_1 = require("node:path");
|
|
10
|
+
const node_os_1 = require("node:os");
|
|
11
|
+
const node_vm_1 = require("node:vm");
|
|
12
|
+
const workbuddy_1 = require("../src-ts/plugins/workbuddy");
|
|
13
|
+
const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
|
|
14
|
+
const logger = { info() { }, warn() { }, error() { } };
|
|
15
|
+
const snapshot = () => ({ conversationId: "task-a", messages: [
|
|
16
|
+
{ id: "u1", role: "user", text: "问题", complete: true },
|
|
17
|
+
{ id: "a1", role: "assistant", text: "回答", complete: true },
|
|
18
|
+
{ id: "draft", role: "assistant", text: "草稿", complete: false },
|
|
19
|
+
{ id: "tool", role: "tool", text: "工具输出", complete: true },
|
|
20
|
+
{ role: "user", text: "没有稳定 ID", complete: true },
|
|
21
|
+
], missingReasons: [] });
|
|
22
|
+
async function fixture(overrides = {}, config = { transport: "cdp" }) {
|
|
23
|
+
const calls = [];
|
|
24
|
+
let activeId = "task-a";
|
|
25
|
+
const client = {
|
|
26
|
+
connect: async () => undefined, close() { }, connected: () => true,
|
|
27
|
+
conversationId: async () => activeId,
|
|
28
|
+
historyAvailability: async () => ({ conversationId: activeId, waiting: false }),
|
|
29
|
+
readHistorySnapshot: async () => { calls.push("read"); return snapshot(); },
|
|
30
|
+
direct: async () => { calls.push("direct"); return "普通回复"; },
|
|
31
|
+
complete: async () => { calls.push("complete"); return { text: "普通回复" }; },
|
|
32
|
+
createTask: async () => { calls.push("createTask"); },
|
|
33
|
+
...overrides,
|
|
34
|
+
};
|
|
35
|
+
const plugin = new workbuddy_1.WorkbuddyPlugin({ client });
|
|
36
|
+
await plugin.activate({ config, logger });
|
|
37
|
+
const history = plugin.sessionHistory;
|
|
38
|
+
strict_1.default.ok(history, "WorkBuddy 应公开受限原生历史 provider");
|
|
39
|
+
return { plugin, history, calls, client, setActiveId: (value) => { activeId = value; } };
|
|
40
|
+
}
|
|
41
|
+
(0, node_test_1.default)("WorkBuddy 原生历史只列出并读取当前任务,拒绝任意 ID 且不调用普通任务接口", async () => {
|
|
42
|
+
const { history, calls } = await fixture();
|
|
43
|
+
strict_1.default.equal(history.mode, "current");
|
|
44
|
+
const listed = await history.listSessions({ limit: 50 });
|
|
45
|
+
strict_1.default.equal(listed.supported, true);
|
|
46
|
+
if (!listed.supported)
|
|
47
|
+
return;
|
|
48
|
+
strict_1.default.deepEqual(listed.sessions.map(item => item.id), ["task-a"]);
|
|
49
|
+
strict_1.default.equal(listed.complete, false);
|
|
50
|
+
strict_1.default.equal((await history.readSession({ sessionId: "other", limit: 100 })).supported, false);
|
|
51
|
+
const read = await history.readSession({ sessionId: "task-a", limit: 100 });
|
|
52
|
+
strict_1.default.equal(read.supported, true);
|
|
53
|
+
if (!read.supported)
|
|
54
|
+
return;
|
|
55
|
+
strict_1.default.deepEqual(read.turns, [{ id: "u1", role: "user", text: "问题" }, { id: "a1", role: "assistant", text: "回答" }]);
|
|
56
|
+
strict_1.default.equal(read.complete, false);
|
|
57
|
+
strict_1.default.ok(read.missingReasons.some(reason => /完整|已加载/.test(reason)));
|
|
58
|
+
strict_1.default.deepEqual(calls, ["read"]);
|
|
59
|
+
});
|
|
60
|
+
(0, node_test_1.default)("WorkBuddy CLI、旧客户端、断线、等待选择与未列出的任务明确不支持读取", async () => {
|
|
61
|
+
const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "wb-history-cli-"));
|
|
62
|
+
try {
|
|
63
|
+
const cliPath = (0, node_path_1.join)(root, "codebuddy.cjs");
|
|
64
|
+
await (0, promises_1.writeFile)(cliPath, "throw Error('不应运行 CLI');");
|
|
65
|
+
const cli = await fixture({}, { transport: "cli", cliPath });
|
|
66
|
+
strict_1.default.equal(cli.history.mode, "unsupported");
|
|
67
|
+
strict_1.default.equal((await cli.history.listSessions({ limit: 50 })).supported, false);
|
|
68
|
+
strict_1.default.deepEqual(cli.calls, []);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
await (0, promises_1.rm)(root, { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
for (const overrides of [
|
|
74
|
+
{ readHistorySnapshot: undefined },
|
|
75
|
+
{ connected: () => false },
|
|
76
|
+
{ pendingInteractionSessionId: () => "logical-a" },
|
|
77
|
+
{ historyAvailability: async () => ({ conversationId: "task-a", waiting: true }) },
|
|
78
|
+
]) {
|
|
79
|
+
const { history, calls } = await fixture(overrides);
|
|
80
|
+
strict_1.default.equal((await history.listSessions({ limit: 50 })).supported, false);
|
|
81
|
+
strict_1.default.deepEqual(calls, []);
|
|
82
|
+
}
|
|
83
|
+
const { history, calls } = await fixture({ readHistorySnapshot: async () => ({ ...snapshot(), messages: [{ id: "a1", role: "assistant", text: "回复", complete: true }] }) });
|
|
84
|
+
const missingUser = await history.listSessions({ limit: 50 });
|
|
85
|
+
strict_1.default.equal(missingUser.supported, true);
|
|
86
|
+
if (!missingUser.supported)
|
|
87
|
+
return;
|
|
88
|
+
const result = await history.readSession({ sessionId: missingUser.sessions[0].id, limit: 100 });
|
|
89
|
+
strict_1.default.equal(result.supported, true);
|
|
90
|
+
if (result.supported) {
|
|
91
|
+
strict_1.default.equal(result.complete, false);
|
|
92
|
+
strict_1.default.ok(result.missingReasons.some(reason => /用户消息/.test(reason)));
|
|
93
|
+
}
|
|
94
|
+
strict_1.default.deepEqual(calls, []);
|
|
95
|
+
});
|
|
96
|
+
(0, node_test_1.default)("WorkBuddy 任务忙时立即拒绝历史读取,历史读取和 handle 共用队列", async () => {
|
|
97
|
+
let release;
|
|
98
|
+
const waiting = new Promise(resolve => { release = resolve; });
|
|
99
|
+
const { plugin, history } = await fixture({ complete: async () => { await waiting; return { text: "完成" }; } });
|
|
100
|
+
const request = plugin.handle({ task: "执行任务" });
|
|
101
|
+
const busy = await history.listSessions({ limit: 50 });
|
|
102
|
+
strict_1.default.equal(busy.supported, false);
|
|
103
|
+
if (!busy.supported)
|
|
104
|
+
strict_1.default.match(busy.reason, /忙/);
|
|
105
|
+
release();
|
|
106
|
+
await request;
|
|
107
|
+
let releaseRead;
|
|
108
|
+
let entered;
|
|
109
|
+
const reading = new Promise(resolve => { releaseRead = resolve; });
|
|
110
|
+
const started = new Promise(resolve => { entered = resolve; });
|
|
111
|
+
const next = await fixture({ readHistorySnapshot: async () => { entered(); await reading; return snapshot(); } });
|
|
112
|
+
await next.history.listSessions({ limit: 50 });
|
|
113
|
+
const read = next.history.readSession({ sessionId: "task-a", limit: 100 });
|
|
114
|
+
await started;
|
|
115
|
+
const handle = next.plugin.handle({ task: "普通任务" });
|
|
116
|
+
strict_1.default.deepEqual(next.calls, []);
|
|
117
|
+
releaseRead();
|
|
118
|
+
strict_1.default.equal((await read).supported, true);
|
|
119
|
+
await handle;
|
|
120
|
+
strict_1.default.deepEqual(next.calls, ["complete"]);
|
|
121
|
+
});
|
|
122
|
+
(0, node_test_1.default)("WorkBuddy 读取前后身份变化及快照任务不一致时拒绝", async () => {
|
|
123
|
+
const current = await fixture();
|
|
124
|
+
await current.history.listSessions({ limit: 50 });
|
|
125
|
+
current.setActiveId("task-b");
|
|
126
|
+
strict_1.default.equal((await current.history.readSession({ sessionId: "task-a", limit: 100 })).supported, false);
|
|
127
|
+
strict_1.default.deepEqual(current.calls, []);
|
|
128
|
+
let reads = 0;
|
|
129
|
+
const changed = await fixture({ conversationId: async () => ++reads <= 3 ? "task-a" : "task-b", historyAvailability: async () => ({ conversationId: ++reads <= 3 ? "task-a" : "task-b", waiting: false }) });
|
|
130
|
+
await changed.history.listSessions({ limit: 50 });
|
|
131
|
+
strict_1.default.equal((await changed.history.readSession({ sessionId: "task-a", limit: 100 })).supported, false);
|
|
132
|
+
const mismatch = await fixture({ readHistorySnapshot: async () => ({ ...snapshot(), conversationId: "task-b" }) });
|
|
133
|
+
await mismatch.history.listSessions({ limit: 50 });
|
|
134
|
+
strict_1.default.equal((await mismatch.history.readSession({ sessionId: "task-a", limit: 100 })).supported, false);
|
|
135
|
+
});
|
|
136
|
+
(0, node_test_1.default)("WorkBuddy 没有用户证据和被排除的未完成回复时保留明确缺失原因", async () => {
|
|
137
|
+
const value = await fixture({ readHistorySnapshot: async () => ({ ...snapshot(), messages: snapshot().messages.filter(item => item.role === "assistant" && item.complete), missingReasons: ["未完成助手回复已排除。"] }) });
|
|
138
|
+
const listed = await value.history.listSessions({ limit: 50 });
|
|
139
|
+
strict_1.default.equal(listed.supported, true);
|
|
140
|
+
if (!listed.supported)
|
|
141
|
+
return;
|
|
142
|
+
const result = await value.history.readSession({ sessionId: listed.sessions[0].id, limit: 100 });
|
|
143
|
+
strict_1.default.equal(result.supported, true);
|
|
144
|
+
if (!result.supported)
|
|
145
|
+
return;
|
|
146
|
+
strict_1.default.equal(result.complete, false);
|
|
147
|
+
strict_1.default.ok(result.missingReasons.some(reason => /用户/.test(reason)));
|
|
148
|
+
strict_1.default.ok(result.missingReasons.some(reason => /未完成/.test(reason)));
|
|
149
|
+
});
|
|
150
|
+
(0, node_test_1.default)("WorkBuddy 分页上限、不可伪造游标及 60000 字符限制", async () => {
|
|
151
|
+
const value = await fixture({ readHistorySnapshot: async () => ({ ...snapshot(), messages: Array.from({ length: 3 }, (_, index) => ({ id: `u${index}`, role: "user", text: `问题${index}`, complete: true })) }) });
|
|
152
|
+
strict_1.default.equal((await value.history.listSessions({ limit: 51 })).supported, false);
|
|
153
|
+
strict_1.default.equal((await value.history.readSession({ sessionId: "task-a", limit: 101 })).supported, false);
|
|
154
|
+
strict_1.default.equal((await value.history.readSession({ sessionId: "task-a", limit: 0 })).supported, false);
|
|
155
|
+
strict_1.default.equal((await value.history.readSession({ sessionId: "task-a", limit: 1, cursor: "task-b" })).supported, false);
|
|
156
|
+
const listed = await value.history.listSessions({ limit: 50 });
|
|
157
|
+
strict_1.default.equal(listed.supported, true);
|
|
158
|
+
if (!listed.supported)
|
|
159
|
+
return;
|
|
160
|
+
const first = await value.history.readSession({ sessionId: listed.sessions[0].id, limit: 1 });
|
|
161
|
+
strict_1.default.equal(first.supported, true);
|
|
162
|
+
if (!first.supported)
|
|
163
|
+
return;
|
|
164
|
+
strict_1.default.equal(first.turns.length, 1);
|
|
165
|
+
strict_1.default.ok(first.nextCursor);
|
|
166
|
+
strict_1.default.ok(!first.nextCursor.includes("task-a"));
|
|
167
|
+
const second = await value.history.readSession({ sessionId: listed.sessions[0].id, limit: 100, cursor: first.nextCursor });
|
|
168
|
+
strict_1.default.equal(second.supported, true);
|
|
169
|
+
if (second.supported)
|
|
170
|
+
strict_1.default.deepEqual(second.turns.map(turn => turn.id), ["u1", "u2"]);
|
|
171
|
+
strict_1.default.deepEqual(value.calls, []);
|
|
172
|
+
const oversized = await fixture({ readHistorySnapshot: async () => ({ ...snapshot(), messages: [{ id: "u1", role: "user", text: "字".repeat(60_001), complete: true }] }) });
|
|
173
|
+
const oversizedList = await oversized.history.listSessions({ limit: 50 });
|
|
174
|
+
strict_1.default.equal(oversizedList.supported, true);
|
|
175
|
+
if (!oversizedList.supported)
|
|
176
|
+
return;
|
|
177
|
+
const result = await oversized.history.readSession({ sessionId: oversizedList.sessions[0].id, limit: 100 });
|
|
178
|
+
strict_1.default.equal(result.supported, false);
|
|
179
|
+
if (!result.supported)
|
|
180
|
+
strict_1.default.match(result.reason, /60000/);
|
|
181
|
+
});
|
|
182
|
+
(0, node_test_1.default)("WorkBuddy CDP 历史 DOM 只读排除推理、工具、附件、草稿及未完成正文", async () => {
|
|
183
|
+
const client = new cdp_client_1.WorkbuddyCdpClient();
|
|
184
|
+
const read = client.readHistorySnapshot;
|
|
185
|
+
strict_1.default.equal(typeof read, "function", "CDP 应提供单独的只读历史快照方法");
|
|
186
|
+
const removed = [];
|
|
187
|
+
const body = (text) => ({ cloneNode: () => ({ textContent: text, querySelectorAll: (selector) => { removed.push(selector); return []; } }) });
|
|
188
|
+
const message = (id, role, text, finished) => ({
|
|
189
|
+
getAttribute: (name) => name === "data-message-id" ? id : null,
|
|
190
|
+
matches: (selector) => selector.includes(".cr-agent") && role === "assistant",
|
|
191
|
+
querySelector: (selector) => selector === '[data-user-content]' ? role === "user" ? body(text) : null : selector === '[data-assistant-content]' ? role === "assistant" ? body(text) : null : selector.includes("conversation-finished-footer") ? finished ? {} : null : null,
|
|
192
|
+
});
|
|
193
|
+
const selected = { className: "_selected_", getAttribute: () => "task-a" };
|
|
194
|
+
const expressions = [];
|
|
195
|
+
const internal = client;
|
|
196
|
+
internal.evaluate = async (expression) => {
|
|
197
|
+
expressions.push(expression);
|
|
198
|
+
return (0, node_vm_1.runInNewContext)(expression, { document: {
|
|
199
|
+
querySelectorAll: (selector) => selector === '[data-conversation-id]' ? [selected] : selector.includes("data-message-id") ? [message("u1", "user", "问题", true), message("a1", "assistant", "回答", true), message("draft", "assistant", "草稿", false)] : [],
|
|
200
|
+
querySelector: () => null,
|
|
201
|
+
} });
|
|
202
|
+
};
|
|
203
|
+
const result = await read.call(client);
|
|
204
|
+
strict_1.default.deepEqual(JSON.parse(JSON.stringify(result.messages)), [{ id: "u1", role: "user", text: "问题", complete: true }, { id: "a1", role: "assistant", text: "回答", complete: true }]);
|
|
205
|
+
strict_1.default.ok(removed.some(selector => /cr-reasoning/.test(selector) && /tool/.test(selector) && /artifact/.test(selector)));
|
|
206
|
+
strict_1.default.ok(expressions.every(expression => !/\.click\(|\.focus\(|fetch\(|Input\.|dispatchEvent|createTask|complete\(/.test(expression)));
|
|
207
|
+
internal.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, { document: {
|
|
208
|
+
querySelectorAll: (selector) => selector === '[data-conversation-id]' ? [selected] : [],
|
|
209
|
+
querySelector: (selector) => selector.includes("pending-sandbox-panel") ? {} : null,
|
|
210
|
+
} });
|
|
211
|
+
const waiting = await read.call(client);
|
|
212
|
+
strict_1.default.match(waiting.unavailableReason ?? "", /等待用户选择/);
|
|
213
|
+
});
|
|
214
|
+
(0, node_test_1.default)("WorkBuddy 当前页面存在未登记的等待选择时拒绝读取", async () => {
|
|
215
|
+
let waiting = false;
|
|
216
|
+
const value = await fixture({ historyAvailability: async () => ({ conversationId: "task-a", waiting }) });
|
|
217
|
+
const client = value.client;
|
|
218
|
+
const listed = await value.history.listSessions({ limit: 50 });
|
|
219
|
+
strict_1.default.equal(listed.supported, true);
|
|
220
|
+
if (!listed.supported)
|
|
221
|
+
return;
|
|
222
|
+
waiting = true;
|
|
223
|
+
const result = await value.history.readSession({ sessionId: listed.sessions[0].id, limit: 100 });
|
|
224
|
+
strict_1.default.equal(result.supported, false);
|
|
225
|
+
if (!result.supported)
|
|
226
|
+
strict_1.default.match(result.reason, /等待用户选择/);
|
|
227
|
+
});
|
|
228
|
+
(0, node_test_1.default)("WorkBuddy 页面已等待选择时不列出任务", async () => {
|
|
229
|
+
const value = await fixture({ historyAvailability: async () => ({ conversationId: "task-a", waiting: true }) });
|
|
230
|
+
const listed = await value.history.listSessions({ limit: 50 });
|
|
231
|
+
strict_1.default.equal(listed.supported, false);
|
|
232
|
+
if (!listed.supported)
|
|
233
|
+
strict_1.default.match(listed.reason, /等待用户选择/);
|
|
234
|
+
});
|
|
235
|
+
(0, node_test_1.default)("WorkBuddy 分页期间出现等待选择时拒绝续页并清除缓存", async () => {
|
|
236
|
+
let waiting = false;
|
|
237
|
+
const value = await fixture({
|
|
238
|
+
historyAvailability: async () => ({ conversationId: "task-a", waiting }),
|
|
239
|
+
readHistorySnapshot: async () => ({ ...snapshot(), messages: Array.from({ length: 2 }, (_, index) => ({ id: `u${index}`, role: "user", text: `问题${index}`, complete: true })) }),
|
|
240
|
+
});
|
|
241
|
+
const listed = await value.history.listSessions({ limit: 50 });
|
|
242
|
+
strict_1.default.equal(listed.supported, true);
|
|
243
|
+
if (!listed.supported)
|
|
244
|
+
return;
|
|
245
|
+
const first = await value.history.readSession({ sessionId: listed.sessions[0].id, limit: 1 });
|
|
246
|
+
strict_1.default.equal(first.supported, true);
|
|
247
|
+
if (!first.supported || !first.nextCursor)
|
|
248
|
+
return;
|
|
249
|
+
waiting = true;
|
|
250
|
+
const rejected = await value.history.readSession({ sessionId: listed.sessions[0].id, limit: 1, cursor: first.nextCursor });
|
|
251
|
+
strict_1.default.equal(rejected.supported, false);
|
|
252
|
+
if (!rejected.supported)
|
|
253
|
+
strict_1.default.match(rejected.reason, /等待用户选择/);
|
|
254
|
+
waiting = false;
|
|
255
|
+
strict_1.default.equal((await value.history.readSession({ sessionId: listed.sessions[0].id, limit: 1, cursor: first.nextCursor })).supported, false);
|
|
256
|
+
});
|
package/package.json
CHANGED
|
@@ -95,6 +95,78 @@ class ChatgptCdpClient {
|
|
|
95
95
|
})()`);
|
|
96
96
|
return (0, session_store_1.validSession)(entry) ? entry : undefined;
|
|
97
97
|
}
|
|
98
|
+
async historyBusy() {
|
|
99
|
+
return Boolean(await this.evaluate(`Boolean(document.querySelector(${JSON.stringify(stopButtonSelector)}))`));
|
|
100
|
+
}
|
|
101
|
+
async readSessionHistory(session) {
|
|
102
|
+
if (!this.connected())
|
|
103
|
+
return { supported: false, reason: "ChatGPT CDP 当前未连接" };
|
|
104
|
+
const before = await this.activeSession();
|
|
105
|
+
if (!before || before.id !== session.id || before.host !== session.host)
|
|
106
|
+
return { supported: false, reason: "当前活动会话身份与请求不一致" };
|
|
107
|
+
if (await this.historyBusy())
|
|
108
|
+
return { supported: false, reason: "ChatGPT 正在生成回复,暂不读取历史" };
|
|
109
|
+
const raw = await this.evaluate(`(() => {
|
|
110
|
+
const selected = document.querySelector('[data-app-action-sidebar-thread-selected="true"], [data-app-action-sidebar-thread-active="true"]');
|
|
111
|
+
if (selected?.getAttribute('data-app-action-sidebar-thread-id') !== ${JSON.stringify(session.id)}
|
|
112
|
+
|| selected?.getAttribute('data-app-action-sidebar-thread-host-id') !== ${JSON.stringify(session.host)}) return { supported: false, reason: '读取期间活动会话身份发生变化' };
|
|
113
|
+
if (document.querySelector(${JSON.stringify(stopButtonSelector)})) return { supported: false, reason: 'ChatGPT 正在生成回复,暂不读取历史' };
|
|
114
|
+
const turns = [...document.querySelectorAll(${JSON.stringify(exports.turnSelector)})];
|
|
115
|
+
const output = [];
|
|
116
|
+
const missingReasons = [];
|
|
117
|
+
let chars = 0;
|
|
118
|
+
const collect = (role, text) => {
|
|
119
|
+
chars += text.length;
|
|
120
|
+
if (chars > 60000) throw new Error('ChatGPT 会话正文超过 60,000 字符上限');
|
|
121
|
+
output.push({ role, text });
|
|
122
|
+
};
|
|
123
|
+
for (const turn of turns) {
|
|
124
|
+
const turnId = turn.getAttribute(${JSON.stringify(turnIdAttribute)});
|
|
125
|
+
if (!turnId) {
|
|
126
|
+
missingReasons.push('部分消息缺少可验证的 turn 身份');
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const users = [...turn.querySelectorAll(${JSON.stringify(exports.userBubbleSelector)})];
|
|
130
|
+
for (const item of users) {
|
|
131
|
+
const body = item.querySelector('[data-markdown-text-tone="user-message"]');
|
|
132
|
+
if (!body) {
|
|
133
|
+
missingReasons.push('部分用户消息缺少专用正文标记,已跳过');
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const text = body.innerText?.trim();
|
|
137
|
+
if (text) collect('user', text);
|
|
138
|
+
}
|
|
139
|
+
const assistants = [...turn.querySelectorAll(${JSON.stringify(assistantMessageSelector)})];
|
|
140
|
+
for (const item of assistants) {
|
|
141
|
+
const marker = item.closest('[data-response-annotation-target][data-response-annotation-conversation]');
|
|
142
|
+
const excluded = item.closest('[data-message-author-role="tool"], [data-message-author-role="system"], [data-message-channel="analysis"], [data-message-channel="commentary"], [data-reasoning], [data-is-streaming="true"], [data-streaming="true"], [aria-busy="true"]');
|
|
143
|
+
const text = item.innerText?.trim();
|
|
144
|
+
if (marker?.getAttribute('data-response-annotation-conversation') === ${JSON.stringify(session.id.replace(/^local:/, ""))}
|
|
145
|
+
&& marker.getAttribute('data-response-annotation-target') && !excluded && text) collect('assistant', text);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { supported: true, turns: output, missingReasons };
|
|
149
|
+
})()`);
|
|
150
|
+
const after = await this.activeSession();
|
|
151
|
+
if (!after || after.id !== session.id || after.host !== session.host)
|
|
152
|
+
return { supported: false, reason: "读取期间活动会话身份发生变化" };
|
|
153
|
+
if (await this.historyBusy())
|
|
154
|
+
return { supported: false, reason: "ChatGPT 正在生成回复,暂不读取历史" };
|
|
155
|
+
const result = raw;
|
|
156
|
+
if (result?.supported === false && typeof result.reason === "string")
|
|
157
|
+
return { supported: false, reason: result.reason };
|
|
158
|
+
if (!result || !Array.isArray(result.turns) || !result.turns.every((turn) => {
|
|
159
|
+
if (!turn || typeof turn !== "object")
|
|
160
|
+
return false;
|
|
161
|
+
const value = turn;
|
|
162
|
+
return (value.role === "user" || value.role === "assistant") && typeof value.text === "string";
|
|
163
|
+
})) {
|
|
164
|
+
return { supported: false, reason: "ChatGPT 会话正文格式无法验证" };
|
|
165
|
+
}
|
|
166
|
+
const missingReasons = Array.isArray(result.missingReasons) ? result.missingReasons.filter((item) => typeof item === "string") : [];
|
|
167
|
+
missingReasons.push("仅包含当前页面已加载的可验证正文,页面虚拟化或未加载历史导致完整性无法确认");
|
|
168
|
+
return { supported: true, turns: result.turns, complete: false, missingReasons };
|
|
169
|
+
}
|
|
98
170
|
async switchSession(session) {
|
|
99
171
|
await this.recoverDraft();
|
|
100
172
|
const locate = `[...document.querySelectorAll(${JSON.stringify(sidebarSelector)})].find(item =>
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ChatgptCdpClient = exports.ChatgptPlugin = exports.chatgptManifest = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
4
5
|
const cdp_client_1 = require("./cdp-client");
|
|
5
6
|
const session_store_1 = require("./session-store");
|
|
6
7
|
exports.chatgptManifest = {
|
|
7
8
|
id: "chatgpt",
|
|
8
|
-
version: "1.0.
|
|
9
|
+
version: "1.0.9",
|
|
9
10
|
displayName: "ChatGPT(Codex)",
|
|
10
11
|
type: "agent",
|
|
11
12
|
entry: "dist/index.js",
|
|
@@ -28,11 +29,27 @@ class ChatgptPlugin {
|
|
|
28
29
|
failure;
|
|
29
30
|
remote = false;
|
|
30
31
|
queue = Promise.resolve();
|
|
32
|
+
queuedOperations = 0;
|
|
31
33
|
store;
|
|
32
34
|
logger;
|
|
35
|
+
historyScope = "";
|
|
36
|
+
historyGeneration = 0;
|
|
37
|
+
historyProvider;
|
|
33
38
|
constructor(options = {}) {
|
|
34
39
|
this.options = options;
|
|
35
40
|
}
|
|
41
|
+
get sessionHistory() {
|
|
42
|
+
const client = this.client;
|
|
43
|
+
const unsupported = !client?.listSessions || !client.activeSession || !client.readSessionHistory;
|
|
44
|
+
const generation = this.historyGeneration;
|
|
45
|
+
return this.historyProvider ??= {
|
|
46
|
+
scope: this.historyScope,
|
|
47
|
+
mode: unsupported ? "unsupported" : "sidebar",
|
|
48
|
+
limitation: unsupported ? "当前 ChatGPT 客户端不提供只读历史接口" : "仅列举已加载侧栏;仅读取当前活动会话中已加载且可验证的正文,完整性可能受页面虚拟化影响",
|
|
49
|
+
listSessions: query => generation === this.historyGeneration ? this.listNativeSessions(query) : Promise.resolve({ supported: false, reason: "ChatGPT 插件实例已变化,请重新列举历史" }),
|
|
50
|
+
readSession: query => generation === this.historyGeneration ? this.readNativeSession(query) : Promise.resolve({ supported: false, reason: "ChatGPT 插件实例已变化,请重新列举历史" }),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
36
53
|
async activate(context) {
|
|
37
54
|
const config = context.config;
|
|
38
55
|
const hasSshHost = config.sshHost !== undefined;
|
|
@@ -53,6 +70,11 @@ class ChatgptPlugin {
|
|
|
53
70
|
localPort: port(config.localCdpPort, 9230),
|
|
54
71
|
} : undefined;
|
|
55
72
|
this.remote = Boolean(ssh);
|
|
73
|
+
this.historyGeneration++;
|
|
74
|
+
this.historyProvider = undefined;
|
|
75
|
+
this.historyScope = (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(ssh
|
|
76
|
+
? [ssh.host, ssh.user, ssh.port, ssh.remotePort]
|
|
77
|
+
: ["local", port(config.localCdpPort, 9230)])).digest("hex").slice(0, 24);
|
|
56
78
|
this.logger = context.logger;
|
|
57
79
|
this.store = new session_store_1.ChatgptSessionStore(context.dataDir, JSON.stringify(ssh
|
|
58
80
|
? [ssh.host, ssh.user, ssh.port, ssh.remotePort]
|
|
@@ -93,8 +115,7 @@ class ChatgptPlugin {
|
|
|
93
115
|
throw new Error("消息内容不能为空");
|
|
94
116
|
if (!this.client || !this.store)
|
|
95
117
|
throw new Error("ChatGPT 插件未启动");
|
|
96
|
-
const run = this.
|
|
97
|
-
this.queue = run.then(() => undefined, () => undefined);
|
|
118
|
+
const run = this.enqueue(() => this.run(request));
|
|
98
119
|
const result = await run;
|
|
99
120
|
return { ...(typeof result === "string" ? { text: result } : result), sessionId: request.sessionId };
|
|
100
121
|
}
|
|
@@ -106,7 +127,7 @@ class ChatgptPlugin {
|
|
|
106
127
|
throw new Error("ChatGPT 新会话缺少 sessionId");
|
|
107
128
|
if (!this.client?.createSession || !this.client.listSessions || !this.store)
|
|
108
129
|
throw new Error("ChatGPT 当前传输不支持新建会话");
|
|
109
|
-
const run = this.
|
|
130
|
+
const run = this.enqueue(async () => {
|
|
110
131
|
const existing = new Set((await this.client.listSessions()).map(item => JSON.stringify([item.host, item.id])));
|
|
111
132
|
const created = await this.client.createSession(request.name);
|
|
112
133
|
if (created) {
|
|
@@ -120,9 +141,106 @@ class ChatgptPlugin {
|
|
|
120
141
|
this.persist();
|
|
121
142
|
return { text: "ChatGPT 已创建新会话", sessionId };
|
|
122
143
|
});
|
|
144
|
+
return run;
|
|
145
|
+
}
|
|
146
|
+
enqueue(work) {
|
|
147
|
+
this.queuedOperations++;
|
|
148
|
+
const run = this.queue.then(work).finally(() => { this.queuedOperations--; });
|
|
123
149
|
this.queue = run.then(() => undefined, () => undefined);
|
|
124
150
|
return run;
|
|
125
151
|
}
|
|
152
|
+
async listNativeSessions(query) {
|
|
153
|
+
const client = this.client;
|
|
154
|
+
const reject = (reason) => ({ supported: false, reason });
|
|
155
|
+
if (!client?.listSessions || !client.activeSession || !client.readSessionHistory)
|
|
156
|
+
return reject("当前 ChatGPT 客户端不支持只读历史");
|
|
157
|
+
const listSessions = client.listSessions.bind(client);
|
|
158
|
+
if (!client.connected())
|
|
159
|
+
return reject("ChatGPT CDP 当前未连接");
|
|
160
|
+
if (this.queuedOperations)
|
|
161
|
+
return reject("ChatGPT 正在处理其他操作,历史读取已拒绝");
|
|
162
|
+
if (!Number.isInteger(query.limit) || query.limit < 1)
|
|
163
|
+
throw new Error("ChatGPT 历史页条数无效");
|
|
164
|
+
if (query.cursor !== undefined && !/^list:[a-f0-9]{64}:\d+$/.test(query.cursor))
|
|
165
|
+
throw new Error("ChatGPT 历史游标无效");
|
|
166
|
+
return this.enqueue(async () => {
|
|
167
|
+
if (!client.connected())
|
|
168
|
+
return reject("ChatGPT CDP 当前未连接");
|
|
169
|
+
if (await client.historyBusy?.())
|
|
170
|
+
return reject("ChatGPT 正在生成回复,暂不读取历史");
|
|
171
|
+
const entries = await listSessions();
|
|
172
|
+
const fingerprint = (0, node_crypto_1.createHash)("sha256").update(JSON.stringify([this.historyScope, this.historyGeneration, entries.map(({ host, id, title }) => [host, id, title])])).digest("hex");
|
|
173
|
+
const offset = query.cursor ? Number(query.cursor.split(":").at(-1)) : 0;
|
|
174
|
+
if (query.cursor !== undefined && !query.cursor.startsWith(`list:${fingerprint}:`))
|
|
175
|
+
throw new Error("ChatGPT 侧栏已变化,历史游标失效");
|
|
176
|
+
if (!Number.isSafeInteger(offset) || (query.cursor !== undefined && offset >= entries.length))
|
|
177
|
+
throw new Error("ChatGPT 历史游标位置无效");
|
|
178
|
+
const limit = Math.min(query.limit, 50);
|
|
179
|
+
const sessions = entries.slice(offset, offset + limit).map(item => ({ id: JSON.stringify([item.host, item.id]), title: item.title, ...(item.updatedAt ? { updatedAt: item.updatedAt } : {}) }));
|
|
180
|
+
return { supported: true, sessions, ...(offset + limit < entries.length ? { nextCursor: `list:${fingerprint}:${offset + limit}` } : {}), complete: false, missingReasons: ["仅包含 ChatGPT 当前已加载的侧栏项目,不代表全部历史会话"], readAt: new Date().toISOString() };
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
async readNativeSession(query) {
|
|
184
|
+
const client = this.client;
|
|
185
|
+
const reject = (reason) => ({ supported: false, reason });
|
|
186
|
+
if (!client?.listSessions || !client.activeSession || !client.readSessionHistory)
|
|
187
|
+
return reject("当前 ChatGPT 客户端不支持只读历史");
|
|
188
|
+
const listSessions = client.listSessions.bind(client);
|
|
189
|
+
const activeSession = client.activeSession.bind(client);
|
|
190
|
+
const readSessionHistory = client.readSessionHistory.bind(client);
|
|
191
|
+
if (!client.connected())
|
|
192
|
+
return reject("ChatGPT CDP 当前未连接");
|
|
193
|
+
if (this.queuedOperations)
|
|
194
|
+
return reject("ChatGPT 正在处理其他操作,历史读取已拒绝");
|
|
195
|
+
if (!Number.isInteger(query.limit) || query.limit < 1)
|
|
196
|
+
throw new Error("ChatGPT 历史页条数无效");
|
|
197
|
+
if (query.cursor !== undefined && !/^read:[a-f0-9]{64}:\d+$/.test(query.cursor))
|
|
198
|
+
throw new Error("ChatGPT 历史游标无效");
|
|
199
|
+
let requested;
|
|
200
|
+
try {
|
|
201
|
+
requested = JSON.parse(query.sessionId);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return reject("ChatGPT 会话 ID 无效");
|
|
205
|
+
}
|
|
206
|
+
if (!Array.isArray(requested) || requested.length !== 2 || requested.some(part => typeof part !== "string" || !part))
|
|
207
|
+
return reject("ChatGPT 会话 ID 无效");
|
|
208
|
+
return this.enqueue(async () => {
|
|
209
|
+
if (!client.connected())
|
|
210
|
+
return reject("ChatGPT CDP 当前未连接");
|
|
211
|
+
if (await client.historyBusy?.())
|
|
212
|
+
return reject("ChatGPT 正在生成回复,暂不读取历史");
|
|
213
|
+
const sessions = await listSessions();
|
|
214
|
+
const session = sessions.find(item => item.host === requested[0] && item.id === requested[1]);
|
|
215
|
+
if (!session)
|
|
216
|
+
return reject("目标会话不在当前 ChatGPT 侧栏中");
|
|
217
|
+
const active = await activeSession();
|
|
218
|
+
if (!active || active.host !== session.host || active.id !== session.id)
|
|
219
|
+
return reject("ChatGPT 只支持读取当前活动会话");
|
|
220
|
+
if (await client.historyBusy?.())
|
|
221
|
+
return reject("ChatGPT 正在生成回复,暂不读取历史");
|
|
222
|
+
const result = await readSessionHistory(session);
|
|
223
|
+
if (!result.supported)
|
|
224
|
+
return reject(result.reason);
|
|
225
|
+
const after = await activeSession();
|
|
226
|
+
if (!after || after.host !== session.host || after.id !== session.id)
|
|
227
|
+
return reject("读取期间活动会话身份发生变化");
|
|
228
|
+
const fingerprint = (0, node_crypto_1.createHash)("sha256").update(JSON.stringify([this.historyScope, this.historyGeneration, session.host, session.id, result.turns])).digest("hex");
|
|
229
|
+
const offset = query.cursor ? Number(query.cursor.split(":").at(-1)) : 0;
|
|
230
|
+
if (query.cursor !== undefined && !query.cursor.startsWith(`read:${fingerprint}:`))
|
|
231
|
+
throw new Error("ChatGPT 会话内容已变化,历史游标失效");
|
|
232
|
+
if (!Number.isSafeInteger(offset) || (query.cursor !== undefined && offset >= result.turns.length))
|
|
233
|
+
throw new Error("ChatGPT 历史游标位置无效");
|
|
234
|
+
const turns = result.turns.map(turn => ({ ...turn }));
|
|
235
|
+
const chars = turns.reduce((total, turn) => total + turn.text.length, 0);
|
|
236
|
+
if (chars > 60_000)
|
|
237
|
+
throw new Error("ChatGPT 会话正文超过 60,000 字符上限");
|
|
238
|
+
const limit = Math.min(query.limit, 100);
|
|
239
|
+
const pageTurns = turns.slice(offset, offset + limit);
|
|
240
|
+
const reasons = [...result.missingReasons, "仅包含当前页面已加载的可验证正文,页面虚拟化或未加载历史导致完整性无法确认"];
|
|
241
|
+
return { supported: true, session: { id: query.sessionId, title: session.title, ...(session.updatedAt ? { updatedAt: session.updatedAt } : {}) }, turns: pageTurns, ...(offset + limit < turns.length ? { nextCursor: `read:${fingerprint}:${offset + limit}` } : {}), complete: false, missingReasons: reasons, readAt: new Date().toISOString() };
|
|
242
|
+
});
|
|
243
|
+
}
|
|
126
244
|
persist() {
|
|
127
245
|
void this.store.save().catch(error => this.logger?.warn("ChatGPT 会话缓存保存失败", { error: String(error) }));
|
|
128
246
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"agent-bridge-chatgpt","version":"1.0.
|
|
1
|
+
{"name":"agent-bridge-chatgpt","version":"1.0.9","description":"agent-bridge 的 ChatGPT Codex CDP 插件。","keywords":["mu-agent-bridge-plugin","mu-agent-bridge-agent"],"main":"dist/src-ts/plugins/chatgpt/index.js","files":["dist/","plugin.json","config.schema.json","README.md"],"agentBridge":{"plugin":"./plugin.json","type":"agent"},"engines":{"node":">=22"}}
|
|
@@ -1 +1,14 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"id": "chatgpt",
|
|
3
|
+
"version": "1.0.9",
|
|
4
|
+
"type": "agent",
|
|
5
|
+
"displayName": "ChatGPT(Codex)",
|
|
6
|
+
"entry": "dist/src-ts/plugins/chatgpt/index.js",
|
|
7
|
+
"apiVersion": "1",
|
|
8
|
+
"process": "in-process",
|
|
9
|
+
"capabilities": [
|
|
10
|
+
"chat",
|
|
11
|
+
"health"
|
|
12
|
+
],
|
|
13
|
+
"configSchema": "config.schema.json"
|
|
14
|
+
}
|