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,245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QwenworkSessionHistoryProvider = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const MAX_SESSIONS = 1000;
|
|
6
|
+
const MAX_MESSAGE_PAGES = 20;
|
|
7
|
+
const MESSAGE_PAGE_SIZE = 100;
|
|
8
|
+
const MAX_READ_TURNS = MAX_MESSAGE_PAGES * MESSAGE_PAGE_SIZE;
|
|
9
|
+
function object(value) {
|
|
10
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
11
|
+
}
|
|
12
|
+
function rows(value) {
|
|
13
|
+
if (!Array.isArray(value))
|
|
14
|
+
return { rows: [], malformed: true };
|
|
15
|
+
const valid = value.filter((item) => typeof object(item)?.id === "string");
|
|
16
|
+
return { rows: valid, malformed: valid.length !== value.length };
|
|
17
|
+
}
|
|
18
|
+
function asTime(value) {
|
|
19
|
+
const time = value instanceof Date ? value.toISOString() : typeof value === "string" ? value : undefined;
|
|
20
|
+
return time && Number.isFinite(Date.parse(time)) ? new Date(time).toISOString() : undefined;
|
|
21
|
+
}
|
|
22
|
+
function subChats(value) {
|
|
23
|
+
const chat = object(value);
|
|
24
|
+
if (!Array.isArray(chat?.subChats))
|
|
25
|
+
return { rows: [], malformed: true };
|
|
26
|
+
const valid = chat.subChats.filter((item) => typeof object(item)?.id === "string");
|
|
27
|
+
return { rows: valid, malformed: valid.length !== chat.subChats.length };
|
|
28
|
+
}
|
|
29
|
+
function sessionFromChat(chat) {
|
|
30
|
+
const updatedAt = asTime(chat.updatedAt);
|
|
31
|
+
return { id: chat.id, title: typeof chat.name === "string" && chat.name.trim() ? chat.name.trim() : "未命名会话", ...(updatedAt ? { updatedAt } : {}) };
|
|
32
|
+
}
|
|
33
|
+
function fingerprint(value) {
|
|
34
|
+
return (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(value)).digest("hex");
|
|
35
|
+
}
|
|
36
|
+
/** 千问桌面端内置数据库的只读 tRPC 会话接口。 */
|
|
37
|
+
class QwenworkSessionHistoryProvider {
|
|
38
|
+
mode = "all";
|
|
39
|
+
scope;
|
|
40
|
+
limitation = "读取千问桌面端可列出的任务及其 sub-chat;每个会话最多读取 20 页消息。";
|
|
41
|
+
unavailableReason;
|
|
42
|
+
query;
|
|
43
|
+
cursors = new Map();
|
|
44
|
+
constructor(options) {
|
|
45
|
+
this.scope = options.scope;
|
|
46
|
+
this.unavailableReason = options.unavailableReason;
|
|
47
|
+
this.query = options.query;
|
|
48
|
+
}
|
|
49
|
+
async listSessions(query) {
|
|
50
|
+
if (!this.available())
|
|
51
|
+
return { supported: false, reason: this.unavailableReason() };
|
|
52
|
+
const cursor = this.resolveCursor(query.cursor, "list");
|
|
53
|
+
if (!cursor)
|
|
54
|
+
return { supported: false, reason: "千问会话目录游标无效或已过期,请重新列举" };
|
|
55
|
+
try {
|
|
56
|
+
const [active, archived, deleted] = await Promise.all([
|
|
57
|
+
this.query("chats.list", {}), this.query("chats.listArchived", {}), this.query("chats.listDeleted", {}),
|
|
58
|
+
]);
|
|
59
|
+
const parsedLists = [rows(active), rows(archived), rows(deleted)];
|
|
60
|
+
if (parsedLists.some(result => result.malformed && result.rows.length === 0))
|
|
61
|
+
return { supported: false, reason: "千问会话目录返回了无法识别的结构" };
|
|
62
|
+
const combined = new Map();
|
|
63
|
+
for (const chat of parsedLists.flatMap(result => result.rows))
|
|
64
|
+
combined.set(chat.id, chat);
|
|
65
|
+
const all = [...combined.values()].sort((a, b) => (asTime(b.updatedAt) ?? "").localeCompare(asTime(a.updatedAt) ?? ""));
|
|
66
|
+
const dataFingerprint = fingerprint(all);
|
|
67
|
+
if (query.cursor && cursor.fingerprint !== dataFingerprint)
|
|
68
|
+
return { supported: false, reason: "千问会话目录在分页期间发生变化,请重新列举" };
|
|
69
|
+
const capReached = all.length > MAX_SESSIONS;
|
|
70
|
+
const limited = all.slice(0, MAX_SESSIONS);
|
|
71
|
+
const limit = Math.max(1, Math.min(100, Math.floor(query.limit) || 50));
|
|
72
|
+
const page = limited.slice(cursor.offset, cursor.offset + limit);
|
|
73
|
+
const nextOffset = cursor.offset + page.length;
|
|
74
|
+
const nextCursor = nextOffset < limited.length ? this.createCursor({ kind: "list", offset: nextOffset, fingerprint: dataFingerprint }) : undefined;
|
|
75
|
+
const missingReasons = [];
|
|
76
|
+
if (parsedLists.some(result => result.malformed))
|
|
77
|
+
missingReasons.push("千问会话目录含有无法识别的条目");
|
|
78
|
+
if (capReached)
|
|
79
|
+
missingReasons.push(`千问会话超过 ${MAX_SESSIONS} 项读取上限`);
|
|
80
|
+
if (page.some(chat => !asTime(chat.updatedAt)))
|
|
81
|
+
missingReasons.push("部分会话缺少有效更新时间,无法判断是否属于今天");
|
|
82
|
+
return {
|
|
83
|
+
supported: true,
|
|
84
|
+
sessions: page.map(sessionFromChat),
|
|
85
|
+
...(nextCursor ? { nextCursor } : {}),
|
|
86
|
+
complete: !capReached && missingReasons.length === 0,
|
|
87
|
+
missingReasons,
|
|
88
|
+
readAt: new Date().toISOString(),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return { supported: false, reason: "千问会话目录只读查询失败" };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async readSession(query) {
|
|
96
|
+
if (!this.available())
|
|
97
|
+
return { supported: false, reason: this.unavailableReason() };
|
|
98
|
+
const since = query.since === undefined ? undefined : asTime(query.since);
|
|
99
|
+
if (query.since !== undefined && !since)
|
|
100
|
+
return { supported: false, reason: "千问 since 时间格式无效" };
|
|
101
|
+
const cursor = this.resolveCursor(query.cursor, "read", query.sessionId, since);
|
|
102
|
+
if (!cursor)
|
|
103
|
+
return { supported: false, reason: "千问消息游标无效或已过期,请重新读取会话" };
|
|
104
|
+
try {
|
|
105
|
+
const rawChat = await this.query("chats.get", { id: query.sessionId });
|
|
106
|
+
const chat = object(rawChat);
|
|
107
|
+
if (!chat || chat.id !== query.sessionId)
|
|
108
|
+
return { supported: false, reason: "千问会话不存在或已不可读取" };
|
|
109
|
+
const parsedChildren = subChats(chat);
|
|
110
|
+
const children = parsedChildren.rows;
|
|
111
|
+
const missingReasons = [];
|
|
112
|
+
if (parsedChildren.malformed)
|
|
113
|
+
missingReasons.push("千问 sub-chat 目录缺失或含有无法识别的条目");
|
|
114
|
+
const turns = [];
|
|
115
|
+
const fingerprintMessages = [];
|
|
116
|
+
for (const child of children) {
|
|
117
|
+
let beforeSequence;
|
|
118
|
+
let pageCount = 0;
|
|
119
|
+
let moreBefore = false;
|
|
120
|
+
while (pageCount < MAX_MESSAGE_PAGES) {
|
|
121
|
+
const input = { subChatId: child.id, limit: MESSAGE_PAGE_SIZE, ...(beforeSequence === undefined ? {} : { beforeSequence }) };
|
|
122
|
+
const result = object(await this.query("chats.listBySubChat", input));
|
|
123
|
+
const messages = Array.isArray(result?.messages) ? result.messages : undefined;
|
|
124
|
+
if (!messages) {
|
|
125
|
+
missingReasons.push(`sub-chat ${child.id} 消息列表格式无效`);
|
|
126
|
+
fingerprintMessages.push({ subChatId: child.id, malformed: true, response: result });
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
fingerprintMessages.push({ subChatId: child.id, beforeSequence, result });
|
|
130
|
+
for (const rawMessage of messages) {
|
|
131
|
+
const message = object(rawMessage);
|
|
132
|
+
if (!message) {
|
|
133
|
+
missingReasons.push(`sub-chat ${child.id} 含有无法识别的消息`);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (message.role !== "user" && message.role !== "assistant")
|
|
137
|
+
continue;
|
|
138
|
+
const time = asTime(message.createdAt);
|
|
139
|
+
if (!time)
|
|
140
|
+
missingReasons.push(`消息 ${String(message.id ?? "未知 ID")} 缺少有效创建时间`);
|
|
141
|
+
if (since && time && Date.parse(time) < Date.parse(since))
|
|
142
|
+
continue;
|
|
143
|
+
if (!Array.isArray(message.parts)) {
|
|
144
|
+
missingReasons.push(`消息 ${String(message.id ?? "未知 ID")} 的 parts 格式无效`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const visibleParts = [];
|
|
148
|
+
let malformedParts = false;
|
|
149
|
+
for (const rawPart of message.parts) {
|
|
150
|
+
const part = object(rawPart);
|
|
151
|
+
if (!part || typeof part.type !== "string") {
|
|
152
|
+
malformedParts = true;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (part.type === "text") {
|
|
156
|
+
if (typeof part.text !== "string")
|
|
157
|
+
malformedParts = true;
|
|
158
|
+
else
|
|
159
|
+
visibleParts.push(part.text);
|
|
160
|
+
}
|
|
161
|
+
else if (part.type === "data-file") {
|
|
162
|
+
visibleParts.push(`[附件:${typeof part.filename === "string" ? part.filename : "附件"}]`);
|
|
163
|
+
}
|
|
164
|
+
else if (part.type === "artifact") {
|
|
165
|
+
visibleParts.push(`[文件产物:${typeof part.fileName === "string" ? part.fileName : typeof part.mediaType === "string" ? part.mediaType : "产物"}]`);
|
|
166
|
+
}
|
|
167
|
+
else if (part.type.startsWith("tool-") || ["reasoning", "tool-call", "tool-Thinking", "thinking"].includes(part.type)) {
|
|
168
|
+
// 思考过程和工具调用属于执行细节,不作为用户可见正文。
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
malformedParts = true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (malformedParts)
|
|
175
|
+
missingReasons.push(`消息 ${String(message.id ?? "未知 ID")} 含有无法识别的 parts`);
|
|
176
|
+
const text = visibleParts.join("\n").trim();
|
|
177
|
+
if (!text)
|
|
178
|
+
continue;
|
|
179
|
+
turns.push({ role: message.role, text, ...(typeof message.id === "string" ? { id: message.id } : {}), ...(time ? { time } : {}) });
|
|
180
|
+
}
|
|
181
|
+
pageCount++;
|
|
182
|
+
moreBefore = result?.hasMoreBefore === true;
|
|
183
|
+
if (!moreBefore)
|
|
184
|
+
break;
|
|
185
|
+
const oldest = Number(result?.oldestSequence);
|
|
186
|
+
if (!Number.isSafeInteger(oldest) || oldest < 0 || beforeSequence === oldest) {
|
|
187
|
+
missingReasons.push(`sub-chat ${child.id} 消息分页游标无效`);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
beforeSequence = oldest;
|
|
191
|
+
}
|
|
192
|
+
if (pageCount === MAX_MESSAGE_PAGES && moreBefore)
|
|
193
|
+
missingReasons.push(`sub-chat ${child.id} 超过 ${MAX_MESSAGE_PAGES} 页读取上限`);
|
|
194
|
+
}
|
|
195
|
+
const unique = new Map();
|
|
196
|
+
for (const turn of turns)
|
|
197
|
+
unique.set(turn.id ?? `${turn.role}:${turn.time ?? ""}:${turn.text}`, turn);
|
|
198
|
+
const allTurns = [...unique.values()].sort((a, b) => (a.time ?? "").localeCompare(b.time ?? "") || (a.id ?? "").localeCompare(b.id ?? ""));
|
|
199
|
+
if (allTurns.length > MAX_READ_TURNS)
|
|
200
|
+
missingReasons.push(`千问会话消息超过 ${MAX_READ_TURNS} 条读取上限`);
|
|
201
|
+
const limitedTurns = allTurns.slice(0, MAX_READ_TURNS);
|
|
202
|
+
const dataFingerprint = fingerprint({ chat, messages: fingerprintMessages });
|
|
203
|
+
if (query.cursor && cursor.fingerprint !== dataFingerprint)
|
|
204
|
+
return { supported: false, reason: "千问会话正文在分页期间发生变化,请重新读取会话" };
|
|
205
|
+
const limit = Math.max(1, Math.min(200, Math.floor(query.limit) || 100));
|
|
206
|
+
const pageTurns = limitedTurns.slice(cursor.offset, cursor.offset + limit);
|
|
207
|
+
const nextOffset = cursor.offset + pageTurns.length;
|
|
208
|
+
const nextCursor = nextOffset < limitedTurns.length ? this.createCursor({ kind: "read", offset: nextOffset, fingerprint: dataFingerprint, sessionId: query.sessionId, ...(since ? { since } : {}) }) : undefined;
|
|
209
|
+
return {
|
|
210
|
+
supported: true,
|
|
211
|
+
session: sessionFromChat(chat),
|
|
212
|
+
turns: pageTurns,
|
|
213
|
+
...(nextCursor ? { nextCursor } : {}),
|
|
214
|
+
complete: missingReasons.length === 0,
|
|
215
|
+
missingReasons: [...new Set(missingReasons)],
|
|
216
|
+
readAt: new Date().toISOString(),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return { supported: false, reason: "千问会话正文只读查询失败" };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
resolveCursor(token, kind, sessionId, since) {
|
|
224
|
+
const now = Date.now();
|
|
225
|
+
for (const [key, value] of this.cursors)
|
|
226
|
+
if (value.expires <= now)
|
|
227
|
+
this.cursors.delete(key);
|
|
228
|
+
if (token === undefined)
|
|
229
|
+
return { kind, offset: 0, fingerprint: "", ...(sessionId ? { sessionId } : {}), ...(since ? { since } : {}), expires: now + 5 * 60_000 };
|
|
230
|
+
const cursor = this.cursors.get(token);
|
|
231
|
+
if (!cursor || cursor.kind !== kind || cursor.sessionId !== sessionId || cursor.since !== since)
|
|
232
|
+
return undefined;
|
|
233
|
+
this.cursors.delete(token);
|
|
234
|
+
return cursor;
|
|
235
|
+
}
|
|
236
|
+
createCursor(cursor) {
|
|
237
|
+
if (this.cursors.size >= 1000)
|
|
238
|
+
this.cursors.delete(this.cursors.keys().next().value);
|
|
239
|
+
const token = `qwen:${(0, node_crypto_1.randomUUID)()}`;
|
|
240
|
+
this.cursors.set(token, { ...cursor, expires: Date.now() + 5 * 60_000 });
|
|
241
|
+
return token;
|
|
242
|
+
}
|
|
243
|
+
available() { return !this.unavailableReason(); }
|
|
244
|
+
}
|
|
245
|
+
exports.QwenworkSessionHistoryProvider = QwenworkSessionHistoryProvider;
|
|
@@ -34,6 +34,39 @@ class WorkbuddyCdpClient {
|
|
|
34
34
|
const value = await this.evaluate(`([...document.querySelectorAll('[data-conversation-id]')].find(item => String(item.className).includes('_selected_')))?.getAttribute('data-conversation-id')`);
|
|
35
35
|
return typeof value === "string" && value.trim() ? value : undefined;
|
|
36
36
|
}
|
|
37
|
+
async readHistorySnapshot() {
|
|
38
|
+
return await this.evaluate(`(() => {
|
|
39
|
+
const selected = [...document.querySelectorAll('[data-conversation-id]')].find(item => String(item.className).includes('_selected_'));
|
|
40
|
+
const conversationId = selected?.getAttribute('data-conversation-id') || undefined;
|
|
41
|
+
const messages = [...document.querySelectorAll('.cr-agent[data-message-id]')].flatMap(element => {
|
|
42
|
+
const id = element.getAttribute('data-message-id');
|
|
43
|
+
const user = element.querySelector('[data-user-content]');
|
|
44
|
+
const assistant = element.querySelector('[data-assistant-content]');
|
|
45
|
+
if (!id || (!user && !assistant)) return [];
|
|
46
|
+
const content = (user || assistant).cloneNode(true);
|
|
47
|
+
content.querySelectorAll('.cr-reasoning,[class*="reasoning"],[data-testid*="reasoning"],[class*="tool"],[data-testid*="tool"],[class*="artifact"],[data-testid*="artifact"],[class*="attachment"],[data-testid*="attachment"],[data-slate-placeholder="true"],[data-slate-zero-width]').forEach(node => node.remove());
|
|
48
|
+
const finished = Boolean(element.querySelector('[data-testid="conversation-finished-footer"]'));
|
|
49
|
+
const text = content.innerText?.trim() || content.textContent?.trim() || '';
|
|
50
|
+
if (!user && !finished) return [];
|
|
51
|
+
return [{ id, role: user ? 'user' : 'assistant', text, complete: true }];
|
|
52
|
+
});
|
|
53
|
+
const first = document.querySelector('.cr-agent[data-message-id]');
|
|
54
|
+
const scroll = first?.parentElement;
|
|
55
|
+
const missingReasons = [];
|
|
56
|
+
const waitingChoice = document.querySelector(${JSON.stringify(interactionOptionSelector)});
|
|
57
|
+
if (waitingChoice) return { conversationId, messages: [], missingReasons, unavailableReason: 'WorkBuddy 正在等待用户选择,暂不读取历史' };
|
|
58
|
+
if (scroll && scroll.scrollHeight > scroll.clientHeight && scroll.scrollTop > 0) missingReasons.push('消息容器当前不在顶部,较早消息可能未加载。');
|
|
59
|
+
else if (scroll && scroll.scrollHeight > scroll.clientHeight && scroll.scrollTop === 0) missingReasons.push('消息容器可能采用虚拟列表,无法证实未渲染消息范围。');
|
|
60
|
+
if (!messages.length) missingReasons.push('当前 DOM 没有带稳定 ID 的用户或助手消息。');
|
|
61
|
+
return { conversationId, messages, missingReasons };
|
|
62
|
+
})()`);
|
|
63
|
+
}
|
|
64
|
+
async historyAvailability() {
|
|
65
|
+
return await this.evaluate(`(() => ({
|
|
66
|
+
conversationId: [...document.querySelectorAll('[data-conversation-id]')].find(item => String(item.className).includes('_selected_'))?.getAttribute('data-conversation-id') || undefined,
|
|
67
|
+
waiting: Boolean(document.querySelector(${JSON.stringify(interactionOptionSelector)})),
|
|
68
|
+
}))()`);
|
|
69
|
+
}
|
|
37
70
|
async connect() {
|
|
38
71
|
if (this.socket)
|
|
39
72
|
return;
|
|
@@ -12,8 +12,9 @@ const attachments_1 = require("../../attachments");
|
|
|
12
12
|
const plugin_1 = require("../../plugin");
|
|
13
13
|
const cdp_client_1 = require("./cdp-client");
|
|
14
14
|
const cli_client_1 = require("./cli-client");
|
|
15
|
+
const session_history_1 = require("./session-history");
|
|
15
16
|
exports.workbuddyManifest = {
|
|
16
|
-
id: "workbuddy", version: "1.1.
|
|
17
|
+
id: "workbuddy", version: "1.1.13", displayName: "WorkBuddy", type: "agent",
|
|
17
18
|
entry: "dist/index.js", apiVersion: "1", process: "in-process",
|
|
18
19
|
capabilities: ["chat", "health"], configSchema: "config.schema.json",
|
|
19
20
|
};
|
|
@@ -50,6 +51,7 @@ class WorkbuddyPlugin {
|
|
|
50
51
|
expertSession = false;
|
|
51
52
|
activeExpert;
|
|
52
53
|
sessionOperations = [];
|
|
54
|
+
sessionHistory;
|
|
53
55
|
constructor(options = {}) {
|
|
54
56
|
this.options = options;
|
|
55
57
|
}
|
|
@@ -78,6 +80,24 @@ class WorkbuddyPlugin {
|
|
|
78
80
|
savedBinding = saved.active;
|
|
79
81
|
}
|
|
80
82
|
this.sessionOperations = [];
|
|
83
|
+
this.sessionHistory = new session_history_1.WorkbuddySessionHistoryProvider({
|
|
84
|
+
scope: `workbuddy:cdp:${(0, plugin_1.resolveRuntimePort)(config, context.runtime, "workbuddy", "WORKBUDDY_CDP_PORT", this.options.cdpPort ?? 9229)}`,
|
|
85
|
+
supported: () => this.transport !== "cli" && !(this.transport !== "cdp" && this.cliPath) && Boolean(this.client?.connected() && this.client.conversationId && this.client.readHistorySnapshot && this.client.historyAvailability),
|
|
86
|
+
client: () => this.client,
|
|
87
|
+
run: async (work) => {
|
|
88
|
+
if (this.transport === "cli" || (this.transport !== "cdp" && this.cliPath))
|
|
89
|
+
return { supported: false, reason: "WorkBuddy CLI 模式不支持原生历史读取" };
|
|
90
|
+
if (!this.client?.connected() || !this.client.conversationId || !this.client.readHistorySnapshot || !this.client.historyAvailability)
|
|
91
|
+
return { supported: false, reason: "WorkBuddy 当前客户端未连接或不提供只读历史接口" };
|
|
92
|
+
if (this.pending || this.client?.pendingInteractionSessionId?.())
|
|
93
|
+
return { supported: false, reason: "WorkBuddy 当前忙或正在等待交互,暂不读取历史" };
|
|
94
|
+
return this.enqueue(async () => {
|
|
95
|
+
if (this.client?.pendingInteractionSessionId?.())
|
|
96
|
+
return { supported: false, reason: "WorkBuddy 正在等待交互,暂不读取历史" };
|
|
97
|
+
return work();
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
});
|
|
81
101
|
if (this.transport === "cli") {
|
|
82
102
|
if (!this.cliPath)
|
|
83
103
|
throw new Error("未找到 CodeBuddy CLI,请配置 cliPath");
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WorkbuddySessionHistoryProvider = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const unsupported = (reason) => ({ supported: false, reason });
|
|
6
|
+
class WorkbuddySessionHistoryProvider {
|
|
7
|
+
options;
|
|
8
|
+
scope;
|
|
9
|
+
limitation = "仅支持配置实例中当前已选任务的已加载完成正文;不枚举、切换任务,无法证明完整历史。";
|
|
10
|
+
listedSessionId;
|
|
11
|
+
snapshot;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.options = options;
|
|
14
|
+
this.scope = options.scope;
|
|
15
|
+
}
|
|
16
|
+
get mode() { return this.options.supported() ? "current" : "unsupported"; }
|
|
17
|
+
async listSessions(query) {
|
|
18
|
+
const invalid = this.validateQuery(query, 50);
|
|
19
|
+
if (invalid)
|
|
20
|
+
return unsupported(invalid);
|
|
21
|
+
if (query.cursor !== undefined)
|
|
22
|
+
return unsupported("WorkBuddy 当前任务列表没有后续页");
|
|
23
|
+
return this.options.run(async () => {
|
|
24
|
+
this.listedSessionId = undefined;
|
|
25
|
+
this.snapshot = undefined;
|
|
26
|
+
const client = this.options.client();
|
|
27
|
+
const state = await client.historyAvailability();
|
|
28
|
+
if (state.waiting)
|
|
29
|
+
return unsupported("WorkBuddy 正在等待用户选择,暂不读取历史");
|
|
30
|
+
const before = state.conversationId;
|
|
31
|
+
const after = await client.conversationId();
|
|
32
|
+
if (!before || before !== after)
|
|
33
|
+
return unsupported("WorkBuddy 当前任务身份不可核验或读取期间已切换");
|
|
34
|
+
this.listedSessionId = before;
|
|
35
|
+
return { supported: true, sessions: [{ id: before, title: "WorkBuddy 当前任务" }], complete: false, missingReasons: [this.limitation], readAt: new Date().toISOString() };
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async readSession(query) {
|
|
39
|
+
const invalid = this.validateQuery(query, 100);
|
|
40
|
+
if (invalid)
|
|
41
|
+
return unsupported(invalid);
|
|
42
|
+
if (!this.listedSessionId || query.sessionId !== this.listedSessionId)
|
|
43
|
+
return unsupported("WorkBuddy 仅支持读取本实例刚列出的当前任务");
|
|
44
|
+
return this.options.run(async () => {
|
|
45
|
+
const client = this.options.client();
|
|
46
|
+
const before = await client.conversationId();
|
|
47
|
+
if (!before || before !== query.sessionId) {
|
|
48
|
+
this.snapshot = undefined;
|
|
49
|
+
return unsupported("WorkBuddy 当前任务已切换,请重新列出任务");
|
|
50
|
+
}
|
|
51
|
+
const current = await client.historyAvailability();
|
|
52
|
+
if (current.waiting) {
|
|
53
|
+
this.snapshot = undefined;
|
|
54
|
+
return unsupported("WorkBuddy 正在等待用户选择,暂不读取历史");
|
|
55
|
+
}
|
|
56
|
+
if (current.conversationId !== before) {
|
|
57
|
+
this.snapshot = undefined;
|
|
58
|
+
return unsupported("WorkBuddy 读取前后任务身份发生变化");
|
|
59
|
+
}
|
|
60
|
+
let state = this.snapshot;
|
|
61
|
+
if (query.cursor !== undefined) {
|
|
62
|
+
if (!state || state.sessionId !== before || !state.cursor || query.cursor !== state.cursor)
|
|
63
|
+
return unsupported("WorkBuddy 历史游标无效或已过期");
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
this.snapshot = undefined;
|
|
67
|
+
const raw = await client.readHistorySnapshot();
|
|
68
|
+
if (raw.unavailableReason)
|
|
69
|
+
return unsupported(raw.unavailableReason);
|
|
70
|
+
if (raw.conversationId !== before)
|
|
71
|
+
return unsupported("WorkBuddy 正文与当前任务身份不一致");
|
|
72
|
+
if (!Array.isArray(raw.messages) || !Array.isArray(raw.missingReasons) || raw.missingReasons.some(reason => typeof reason !== "string"))
|
|
73
|
+
return unsupported("WorkBuddy 历史快照格式无效");
|
|
74
|
+
const turns = [];
|
|
75
|
+
const missingReasons = [this.limitation, ...raw.missingReasons];
|
|
76
|
+
const ids = new Set();
|
|
77
|
+
let characters = 0;
|
|
78
|
+
for (const message of raw.messages) {
|
|
79
|
+
if (!message || typeof message.id !== "string" || !message.id.trim() || typeof message.text !== "string" || (message.role !== "user" && message.role !== "assistant")) {
|
|
80
|
+
missingReasons.push("部分消息缺少稳定 ID 或可核验角色,已排除。");
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (message.complete !== true) {
|
|
84
|
+
missingReasons.push("当前任务存在未完成消息,已排除未完成正文。");
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (ids.has(message.id))
|
|
88
|
+
return unsupported("WorkBuddy 历史出现重复消息 ID,无法核验正文");
|
|
89
|
+
ids.add(message.id);
|
|
90
|
+
const text = message.text.trim();
|
|
91
|
+
if (!text)
|
|
92
|
+
continue;
|
|
93
|
+
characters += text.length;
|
|
94
|
+
if (characters > 60_000)
|
|
95
|
+
return unsupported("WorkBuddy 历史正文超过 60000 字符,拒绝读取而不截断");
|
|
96
|
+
turns.push({ id: message.id, role: message.role, text });
|
|
97
|
+
}
|
|
98
|
+
if (!turns.some(turn => turn.role === "user"))
|
|
99
|
+
missingReasons.push("当前 DOM 没有可核验的用户消息,不能证明完整对话。");
|
|
100
|
+
state = { sessionId: before, turns, missingReasons: [...new Set(missingReasons)], readAt: new Date().toISOString(), offset: 0 };
|
|
101
|
+
}
|
|
102
|
+
if (await client.conversationId() !== before) {
|
|
103
|
+
this.snapshot = undefined;
|
|
104
|
+
return unsupported("WorkBuddy 读取前后任务身份发生变化");
|
|
105
|
+
}
|
|
106
|
+
const turns = state.turns.slice(state.offset, state.offset + query.limit);
|
|
107
|
+
state.offset += turns.length;
|
|
108
|
+
state.cursor = state.offset < state.turns.length ? (0, node_crypto_1.randomUUID)() : undefined;
|
|
109
|
+
this.snapshot = state;
|
|
110
|
+
return { supported: true, session: { id: before, title: "WorkBuddy 当前任务" }, turns, ...(state.cursor ? { nextCursor: state.cursor } : {}), complete: false, missingReasons: state.missingReasons, readAt: state.readAt };
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
validateQuery(query, maximum) {
|
|
114
|
+
if (!Number.isInteger(query.limit) || query.limit < 1 || query.limit > maximum)
|
|
115
|
+
return `WorkBuddy 每页数量必须为 1–${maximum}`;
|
|
116
|
+
if (query.cursor !== undefined && (typeof query.cursor !== "string" || !query.cursor.trim()))
|
|
117
|
+
return "WorkBuddy 历史游标无效";
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
exports.WorkbuddySessionHistoryProvider = WorkbuddySessionHistoryProvider;
|