mu-agent-bridge 1.0.34 → 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.
Files changed (89) hide show
  1. package/README.md +4 -2
  2. package/dist/src-ts/channel-sessions.js +90 -24
  3. package/dist/src-ts/control-server.js +48 -7
  4. package/dist/src-ts/dsh-agent.js +141 -46
  5. package/dist/src-ts/headless-account.patch.yml +8 -1
  6. package/dist/src-ts/plugin-manager.js +69 -3
  7. package/dist/src-ts/plugins/aun/attachments.js +44 -13
  8. package/dist/src-ts/plugins/aun/index.js +5 -6
  9. package/dist/src-ts/plugins/chatgpt/cdp-client.js +342 -9
  10. package/dist/src-ts/plugins/chatgpt/index.js +127 -7
  11. package/dist/src-ts/plugins/desktop-launch.js +19 -0
  12. package/dist/src-ts/plugins/doubao/attachments.js +42 -0
  13. package/dist/src-ts/plugins/doubao/cdp-client.js +530 -48
  14. package/dist/src-ts/plugins/doubao/index.js +114 -25
  15. package/dist/src-ts/plugins/doubao/session-history.js +123 -0
  16. package/dist/src-ts/plugins/dsh/index.js +18 -2
  17. package/dist/src-ts/plugins/dsh/session-history.js +371 -0
  18. package/dist/src-ts/plugins/qwenwork/cdp-client.js +271 -10
  19. package/dist/src-ts/plugins/qwenwork/index.js +42 -3
  20. package/dist/src-ts/plugins/qwenwork/session-history.js +245 -0
  21. package/dist/src-ts/plugins/workbuddy/cdp-client.js +42 -2
  22. package/dist/src-ts/plugins/workbuddy/index.js +23 -3
  23. package/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
  24. package/dist/src-ts/session-archive.js +509 -0
  25. package/dist/src-ts/session-history.js +2 -0
  26. package/dist/src-ts/session-management.js +665 -0
  27. package/dist/src-ts/session-summary.js +171 -0
  28. package/dist/tests-ts/aun-attachments.test.js +41 -3
  29. package/dist/tests-ts/aun-plugin.test.js +28 -5
  30. package/dist/tests-ts/bundled-package.test.js +3 -1
  31. package/dist/tests-ts/channel-sessions.test.js +255 -2
  32. package/dist/tests-ts/chatgpt-history.test.js +197 -0
  33. package/dist/tests-ts/chatgpt-plugin.test.js +279 -2
  34. package/dist/tests-ts/control-server.test.js +39 -0
  35. package/dist/tests-ts/desktop-launch.test.js +41 -0
  36. package/dist/tests-ts/doubao-history.test.js +172 -0
  37. package/dist/tests-ts/doubao-plugin.test.js +549 -2
  38. package/dist/tests-ts/dsh-agent.test.js +199 -0
  39. package/dist/tests-ts/dsh-history.test.js +165 -0
  40. package/dist/tests-ts/native-session-management.test.js +195 -0
  41. package/dist/tests-ts/plugin-manager.test.js +354 -4
  42. package/dist/tests-ts/qwenwork-history.test.js +339 -0
  43. package/dist/tests-ts/qwenwork-images.test.js +166 -0
  44. package/dist/tests-ts/qwenwork-plugin.test.js +40 -1
  45. package/dist/tests-ts/session-archive.test.js +422 -0
  46. package/dist/tests-ts/session-management.test.js +323 -0
  47. package/dist/tests-ts/session-summary.test.js +199 -0
  48. package/dist/tests-ts/workbuddy-history.test.js +256 -0
  49. package/dist/tests-ts/workbuddy-plugin.test.js +43 -0
  50. package/package.json +1 -1
  51. package/plugins/aun/dist/src-ts/plugins/aun/attachments.js +44 -13
  52. package/plugins/aun/dist/src-ts/plugins/aun/index.js +5 -6
  53. package/plugins/aun/package.json +1 -1
  54. package/plugins/aun/plugin.json +1 -1
  55. package/plugins/chatgpt/dist/src-ts/attachments.js +62 -0
  56. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +342 -9
  57. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +127 -7
  58. package/plugins/chatgpt/package.json +1 -1
  59. package/plugins/chatgpt/plugin.json +14 -1
  60. package/plugins/doubao/config.schema.json +3 -0
  61. package/plugins/doubao/dist/src-ts/attachments.js +62 -0
  62. package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +19 -0
  63. package/plugins/doubao/dist/src-ts/plugins/doubao/attachments.js +42 -0
  64. package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +530 -48
  65. package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +114 -25
  66. package/plugins/doubao/dist/src-ts/plugins/doubao/session-history.js +123 -0
  67. package/plugins/doubao/package.json +1 -1
  68. package/plugins/doubao/plugin.json +1 -1
  69. package/plugins/dsh/config.schema.json +2 -1
  70. package/plugins/dsh/dist/src-ts/attachments.js +62 -0
  71. package/plugins/dsh/dist/src-ts/dsh-agent.js +141 -46
  72. package/plugins/dsh/dist/src-ts/headless-account.patch.yml +8 -1
  73. package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +18 -2
  74. package/plugins/dsh/dist/src-ts/plugins/dsh/session-history.js +371 -0
  75. package/plugins/dsh/package.json +5 -2
  76. package/plugins/dsh/plugin.json +1 -1
  77. package/plugins/qwenwork/dist/src-ts/attachments.js +62 -0
  78. package/plugins/qwenwork/dist/src-ts/plugins/desktop-launch.js +19 -0
  79. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +271 -10
  80. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +42 -3
  81. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/session-history.js +245 -0
  82. package/plugins/qwenwork/package.json +1 -1
  83. package/plugins/qwenwork/plugin.json +1 -1
  84. package/plugins/workbuddy/config.schema.json +1 -1
  85. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +42 -2
  86. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +23 -3
  87. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
  88. package/plugins/workbuddy/package.json +1 -1
  89. package/plugins/workbuddy/plugin.json +1 -1
@@ -0,0 +1,371 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DshSessionHistoryProvider = void 0;
4
+ exports.decodeDshZstdFrames = decodeDshZstdFrames;
5
+ const node_crypto_1 = require("node:crypto");
6
+ const node_fs_1 = require("node:fs");
7
+ const promises_1 = require("node:fs/promises");
8
+ const node_os_1 = require("node:os");
9
+ const node_path_1 = require("node:path");
10
+ const node_zlib_1 = require("node:zlib");
11
+ const MAX_COMPRESSED_BYTES = 32 * 1024 * 1024;
12
+ const MAX_EXPANDED_BYTES = 128 * 1024 * 1024;
13
+ const MAX_SESSIONS = 10_000;
14
+ const MAX_TURNS = 100_000;
15
+ const KNOWN_EVENTS = new Set([
16
+ "agent-preset/selected", "agent/inbox/spliced", "approval/asked", "approval/decided", "approval/policy", "assistant/attempt", "assistant/message",
17
+ "command/done", "command/run", "compaction/end", "compaction/prune", "compaction/start", "compaction/summary", "deliverables/presented",
18
+ "developer/message", "feedback/message-delete", "feedback/message-put", "feedback/record", "goal/change", "hook/invoked", "hook/result",
19
+ "image/offload", "llm/retry", "llm/retry-started", "model/selection", "permission/preset", "plan/mode", "request/context", "request/header",
20
+ "sandbox/mode", "schedule/change", "session-log-deepseek/delivery-accepted", "session/end-seed", "session/title", "session/title-llm-request",
21
+ "step/end", "step/start", "subagent/catalog", "subagent/descriptor", "subagent/model-selection-policy", "system/message", "team/member",
22
+ "team/message/delivered", "team/message/queued", "team/task", "todo/write", "tool-workflow/agent-end", "tool-workflow/agent-start",
23
+ "tool-workflow/run-end", "tool-workflow/run-start", "tool/call", "tool/ptc-dispatch", "tool/ptc-dispatch-start", "tool/result", "turn/end",
24
+ "turn/start", "user/message", "web/deepseek-search-llm-request", "workspace/changes",
25
+ ]);
26
+ const unsupported = (reason) => ({ supported: false, reason });
27
+ const validTime = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && Number.isFinite(new Date(value).getTime());
28
+ /** DSH 将头部与每批事件编码为独立 zstd 帧;Node 的解压器只消费首帧,必须逐帧处理。 */
29
+ function decodeDshZstdFrames(bytes) {
30
+ const decoded = [];
31
+ let offset = 0, total = 0;
32
+ const requireBytes = (position, length) => { if (position + length > bytes.length)
33
+ throw new Error("DSH zstd 帧被截断"); };
34
+ while (offset < bytes.length) {
35
+ if (decoded.length > MAX_TURNS)
36
+ throw new Error("DSH zstd 帧数量超过读取上限");
37
+ const start = offset;
38
+ requireBytes(offset, 5);
39
+ if (bytes.readUInt32LE(offset) !== 0xfd2fb528)
40
+ throw new Error("DSH zstd 帧魔数无效");
41
+ const descriptor = bytes[offset + 4];
42
+ if (descriptor & 0x18)
43
+ throw new Error("DSH zstd 帧含保留标记");
44
+ const single = Boolean(descriptor & 0x20), sizeFlag = descriptor >>> 6, checksum = Boolean(descriptor & 4);
45
+ offset += 5 + (single ? 0 : 1) + [0, 1, 2, 4][descriptor & 3] + (sizeFlag ? [0, 2, 4, 8][sizeFlag] : single ? 1 : 0);
46
+ requireBytes(offset, 0);
47
+ let last = false;
48
+ while (!last) {
49
+ requireBytes(offset, 3);
50
+ const block = bytes.readUIntLE(offset, 3);
51
+ last = Boolean(block & 1);
52
+ const kind = (block >>> 1) & 3, size = block >>> 3;
53
+ if (kind === 3)
54
+ throw new Error("DSH zstd 块类型无效");
55
+ offset += 3 + (kind === 1 ? 1 : size);
56
+ requireBytes(offset, 0);
57
+ }
58
+ offset += checksum ? 4 : 0;
59
+ requireBytes(offset, 0);
60
+ const frame = (0, node_zlib_1.zstdDecompressSync)(bytes.subarray(start, offset), { maxOutputLength: MAX_EXPANDED_BYTES - total });
61
+ total += frame.length;
62
+ if (total > MAX_EXPANDED_BYTES)
63
+ throw new Error("DSH 解压日志超过读取上限");
64
+ decoded.push(frame);
65
+ }
66
+ return Buffer.concat(decoded, total);
67
+ }
68
+ class DshSessionHistoryProvider {
69
+ options;
70
+ scope;
71
+ limitation = "读取的是 DSH 官方 v4 持久化会话日志;实时尚未落盘的事件、未知格式与损坏日志会标为不完整。";
72
+ mode = "all";
73
+ root;
74
+ constructor(options = {}) {
75
+ this.options = options;
76
+ this.root = (0, node_path_1.resolve)(options.sessionsRoot ?? (0, node_path_1.join)(process.env.DSH_HOME || (0, node_path_1.join)((0, node_os_1.homedir)(), ".dsh"), "sessions"));
77
+ this.scope = `dsh:persisted:${(0, node_crypto_1.createHash)("sha256").update(this.root).digest("hex").slice(0, 24)}`;
78
+ }
79
+ async listSessions(query) {
80
+ if (this.options.unavailableReason?.())
81
+ return unsupported(this.options.unavailableReason());
82
+ const invalid = this.validate(query, 100);
83
+ if (invalid)
84
+ return unsupported(invalid);
85
+ const root = await this.realRoot();
86
+ if (typeof root !== "string")
87
+ return root;
88
+ const discovered = await this.discover(root);
89
+ const fingerprint = (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(discovered.entries.map(item => [item.id, item.generation, item.mtime]))).digest("hex");
90
+ const offset = this.cursor(query.cursor, fingerprint, discovered.entries.length, "dsh-list");
91
+ if (offset instanceof Error)
92
+ throw offset;
93
+ const sessions = discovered.entries.slice(offset, offset + Math.min(query.limit, 100)).map(item => ({ id: item.id, title: item.id, updatedAt: new Date(item.mtime).toISOString() }));
94
+ return {
95
+ supported: true,
96
+ sessions,
97
+ ...(offset + sessions.length < discovered.entries.length ? { nextCursor: `dsh-list:${fingerprint}:${offset + sessions.length}` } : {}),
98
+ complete: discovered.reasons.length === 0,
99
+ missingReasons: discovered.reasons,
100
+ readAt: new Date().toISOString(),
101
+ };
102
+ }
103
+ async readSession(query) {
104
+ if (this.options.unavailableReason?.())
105
+ return unsupported(this.options.unavailableReason());
106
+ const invalid = this.validate(query, 1000);
107
+ if (invalid)
108
+ return unsupported(invalid);
109
+ const since = this.parseSince(query.since);
110
+ if (since instanceof Error)
111
+ return unsupported(since.message);
112
+ const root = await this.realRoot();
113
+ if (typeof root !== "string")
114
+ return root;
115
+ const discovered = await this.discover(root);
116
+ const entry = discovered.entries.find(item => item.id === query.sessionId);
117
+ if (!entry)
118
+ return unsupported("DSH 会话不在当前可验证的持久化日志列表中");
119
+ const parsed = await this.readEntry(root, entry);
120
+ // 先完整解析并校验日志,再按时间筛选,避免过滤掩盖损坏或未知事件。
121
+ const filteredTurns = since === undefined ? parsed.turns : parsed.turns.filter(turn => turn.time !== undefined && Date.parse(turn.time) >= Date.parse(since));
122
+ const fingerprint = (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(["dsh-read", entry.id, entry.generation, entry.mtime, since ?? null, filteredTurns])).digest("hex");
123
+ const offset = this.cursor(query.cursor, fingerprint, filteredTurns.length, "dsh-read");
124
+ if (offset instanceof Error)
125
+ throw offset;
126
+ const turns = filteredTurns.slice(offset, offset + Math.min(query.limit, 1000));
127
+ return {
128
+ supported: true,
129
+ session: parsed.session,
130
+ turns,
131
+ ...(offset + turns.length < filteredTurns.length ? { nextCursor: `dsh-read:${fingerprint}:${offset + turns.length}` } : {}),
132
+ complete: parsed.complete,
133
+ missingReasons: parsed.reasons,
134
+ readAt: new Date().toISOString(),
135
+ };
136
+ }
137
+ async realRoot() {
138
+ try {
139
+ const resolved = await (0, promises_1.realpath)(this.root);
140
+ const info = await (0, promises_1.lstat)(this.root);
141
+ if (!info.isDirectory() || info.isSymbolicLink() || resolved !== this.root)
142
+ return unsupported("DSH 会话目录不是受信任的真实目录");
143
+ return resolved;
144
+ }
145
+ catch {
146
+ return unsupported("DSH 持久化会话目录不存在或不可读");
147
+ }
148
+ }
149
+ async discover(root) {
150
+ const entries = [];
151
+ const reasons = [];
152
+ let projects;
153
+ try {
154
+ projects = await (0, promises_1.readdir)(root, { withFileTypes: true });
155
+ }
156
+ catch {
157
+ return { entries, reasons: ["无法完整枚举 DSH 项目会话目录"] };
158
+ }
159
+ for (const project of projects) {
160
+ if (!project.isDirectory()) {
161
+ if (project.isSymbolicLink())
162
+ reasons.push("会话目录包含符号链接,已略过");
163
+ continue;
164
+ }
165
+ const projectPath = (0, node_path_1.join)(root, project.name);
166
+ if (!await this.inside(root, projectPath)) {
167
+ reasons.push("项目会话目录路径越界,已略过");
168
+ continue;
169
+ }
170
+ let directories;
171
+ try {
172
+ directories = await (0, promises_1.readdir)(projectPath, { withFileTypes: true });
173
+ }
174
+ catch {
175
+ reasons.push("部分项目会话目录不可读");
176
+ continue;
177
+ }
178
+ for (const directory of directories) {
179
+ if (!directory.isDirectory()) {
180
+ if (directory.isSymbolicLink())
181
+ reasons.push("会话目录包含符号链接,已略过");
182
+ continue;
183
+ }
184
+ const dir = (0, node_path_1.join)(projectPath, directory.name);
185
+ if (!await this.inside(root, dir)) {
186
+ reasons.push("会话文件目录路径越界,已略过");
187
+ continue;
188
+ }
189
+ let files;
190
+ try {
191
+ files = await (0, promises_1.readdir)(dir, { withFileTypes: true });
192
+ }
193
+ catch {
194
+ reasons.push("部分会话文件目录不可读");
195
+ continue;
196
+ }
197
+ const candidates = files.flatMap(file => {
198
+ const match = file.name.match(/^session(?:\.v(\d+))?\.jsonl(\.zstd)?$/);
199
+ return match ? [{ file, generation: match[1] ? Number(match[1]) : 0, compressed: Boolean(match[2]) }] : [];
200
+ }).sort((a, b) => b.generation - a.generation);
201
+ const canonical = candidates[0];
202
+ if (!canonical) {
203
+ if (files.some(file => file.name.startsWith("session.")))
204
+ reasons.push("会话目录使用无法识别的日志格式");
205
+ continue;
206
+ }
207
+ if (canonical.generation !== 4) {
208
+ reasons.push(`会话 ${directory.name} 使用不支持的 v${canonical.generation} 持久化代际`);
209
+ continue;
210
+ }
211
+ if (candidates.length > 1)
212
+ reasons.push(`会话 ${directory.name} 存在多个日志代际,无法确认规范版本`);
213
+ if (canonical.file.isSymbolicLink() || !canonical.file.isFile()) {
214
+ reasons.push(`会话 ${directory.name} 的日志不是普通文件`);
215
+ continue;
216
+ }
217
+ const path = (0, node_path_1.join)(dir, canonical.file.name);
218
+ if (!await this.inside(root, path)) {
219
+ reasons.push(`会话 ${directory.name} 的日志路径越界`);
220
+ continue;
221
+ }
222
+ try {
223
+ const stat = await (0, promises_1.lstat)(path);
224
+ if (stat.size > MAX_COMPRESSED_BYTES) {
225
+ reasons.push(`会话 ${directory.name} 超过压缩日志大小上限`);
226
+ continue;
227
+ }
228
+ entries.push({ path, id: directory.name, mtime: stat.mtimeMs, generation: canonical.generation, compressed: canonical.compressed });
229
+ }
230
+ catch {
231
+ reasons.push(`会话 ${directory.name} 日志无法读取`);
232
+ }
233
+ }
234
+ }
235
+ entries.sort((a, b) => b.mtime - a.mtime || a.id.localeCompare(b.id));
236
+ if (entries.length > MAX_SESSIONS) {
237
+ entries.length = MAX_SESSIONS;
238
+ reasons.push(`会话数超过 ${MAX_SESSIONS} 条上限`);
239
+ }
240
+ return { entries, reasons: [...new Set(reasons)] };
241
+ }
242
+ async inside(root, path) {
243
+ try {
244
+ const real = await (0, promises_1.realpath)(path);
245
+ const rel = (0, node_path_1.relative)(root, real);
246
+ return rel !== ".." && !rel.startsWith(`..${node_path_1.sep}`) && !(0, node_path_1.isAbsolute)(rel);
247
+ }
248
+ catch {
249
+ return false;
250
+ }
251
+ }
252
+ async readEntry(root, entry) {
253
+ const reasons = [];
254
+ const fail = (reason) => ({ session: { id: entry.id, title: entry.id }, turns: [], complete: false, reasons: [...new Set([...reasons, reason])] });
255
+ try {
256
+ if (!await this.inside(root, entry.path))
257
+ return fail("DSH 会话日志路径越界或为符号链接");
258
+ const before = await (0, promises_1.lstat)(entry.path);
259
+ if (!before.isFile() || before.isSymbolicLink() || before.size > MAX_COMPRESSED_BYTES)
260
+ return fail("DSH 会话日志不是普通文件或超过读取上限");
261
+ const handle = await (0, promises_1.open)(entry.path, node_fs_1.constants.O_RDONLY | (node_fs_1.constants.O_NOFOLLOW ?? 0));
262
+ let bytes;
263
+ try {
264
+ const opened = await handle.stat();
265
+ if (!opened.isFile() || opened.size > MAX_COMPRESSED_BYTES || opened.ino !== before.ino)
266
+ return fail("DSH 会话日志在打开期间发生变化");
267
+ bytes = await handle.readFile();
268
+ const after = await handle.stat();
269
+ if (opened.ino !== after.ino || opened.size !== after.size || opened.mtimeMs !== after.mtimeMs || bytes.byteLength !== after.size)
270
+ return fail("DSH 会话日志在读取期间发生变化");
271
+ }
272
+ finally {
273
+ await handle.close();
274
+ }
275
+ const data = entry.compressed ? decodeDshZstdFrames(bytes) : bytes;
276
+ if (data.byteLength > MAX_EXPANDED_BYTES)
277
+ return fail("DSH 解压日志超过读取上限");
278
+ const text = data.toString("utf8");
279
+ if (!text.endsWith("\n"))
280
+ return fail("DSH 会话日志最后一条记录不完整");
281
+ const rows = text.slice(0, -1).split("\n");
282
+ if (!rows.length || rows.length > MAX_TURNS + 1)
283
+ return fail("DSH 会话记录数量超过读取上限");
284
+ const header = JSON.parse(rows[0]);
285
+ if (header.type !== "session" || header.version !== 4 || header.id !== entry.id || !validTime(header.createdAt) || typeof header.isSeeded !== "boolean" || !Number.isSafeInteger(header.delegationDepth) || header.delegationDepth < 0)
286
+ return fail("DSH 会话头格式或身份无法验证");
287
+ const turns = [];
288
+ let complete = true;
289
+ for (let index = 1; index < rows.length; index++) {
290
+ let event;
291
+ try {
292
+ event = JSON.parse(rows[index]);
293
+ }
294
+ catch {
295
+ return fail("DSH 会话日志包含损坏记录");
296
+ }
297
+ if (!event || event.seq !== index - 1 || typeof event.type !== "string" || !validTime(event.time) || typeof event.data !== "object" || event.data === null)
298
+ return fail("DSH 会话事件序号、时间或结构无法验证");
299
+ if (!KNOWN_EVENTS.has(event.type) && event.ignorable !== true) {
300
+ complete = false;
301
+ reasons.push(`存在未知且不可安全忽略的 DSH 事件:${event.type}`);
302
+ continue;
303
+ }
304
+ if (event.type !== "user/message" && event.type !== "assistant/message")
305
+ continue;
306
+ const declaredRole = event.type === "user/message" ? event.data.role : event.data.message?.role;
307
+ if (declaredRole !== undefined && declaredRole !== (event.type === "user/message" ? "user" : "assistant")) {
308
+ complete = false;
309
+ reasons.push("DSH 消息事件与角色声明冲突,已排除");
310
+ continue;
311
+ }
312
+ const content = event.type === "user/message" ? event.data.content : event.data.message?.content;
313
+ if (!Array.isArray(content)) {
314
+ complete = false;
315
+ reasons.push("部分 DSH 消息缺少可验证正文");
316
+ continue;
317
+ }
318
+ const parts = [];
319
+ for (const block of content) {
320
+ if (!block || typeof block !== "object" || typeof block.type !== "string") {
321
+ complete = false;
322
+ reasons.push("部分 DSH 消息包含无法解释的正文块");
323
+ continue;
324
+ }
325
+ if (block.type === "reasoning" || block.type === "tool-call")
326
+ continue;
327
+ if (block.type === "text" && typeof block.text === "string")
328
+ parts.push(block.text);
329
+ else {
330
+ complete = false;
331
+ reasons.push(`DSH ${block.type} 正文块未纳入文本历史读取`);
332
+ }
333
+ }
334
+ const message = parts.join("\n").trim();
335
+ if (message)
336
+ turns.push({ id: `${entry.id}:${event.seq}`, role: event.type === "user/message" ? "user" : "assistant", text: message, time: new Date(event.time).toISOString() });
337
+ }
338
+ return { session: { id: entry.id, title: entry.id, updatedAt: new Date(before.mtimeMs).toISOString() }, turns, complete, reasons: [...new Set(reasons)] };
339
+ }
340
+ catch (error) {
341
+ return fail(error instanceof Error && /maximum output length|larger than/.test(error.message) ? "DSH 解压日志超过读取上限" : "DSH 会话日志无法按官方 v4 格式完整解析");
342
+ }
343
+ }
344
+ cursor(cursor, fingerprint, total, prefix) {
345
+ if (cursor === undefined)
346
+ return 0;
347
+ const match = cursor.match(/^(dsh-list|dsh-read):([a-f0-9]{64}):(\d+)$/);
348
+ if (!match || match[1] !== prefix || match[2] !== fingerprint)
349
+ return new Error("DSH 历史游标无效或内容已变化");
350
+ const offset = Number(match[3]);
351
+ return Number.isSafeInteger(offset) && offset >= 0 && offset < total ? offset : new Error("DSH 历史游标位置无效");
352
+ }
353
+ validate(query, max) {
354
+ if (!Number.isInteger(query.limit) || query.limit < 1 || query.limit > max)
355
+ return `DSH 每页数量必须为 1–${max}`;
356
+ if (query.cursor !== undefined && (typeof query.cursor !== "string" || !query.cursor.trim()))
357
+ return "DSH 历史游标无效";
358
+ return undefined;
359
+ }
360
+ parseSince(value) {
361
+ if (value === undefined)
362
+ return undefined;
363
+ if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})$/.test(value))
364
+ return new Error("DSH since 必须是 ISO 8601 时间");
365
+ const timestamp = Date.parse(value);
366
+ if (!Number.isFinite(timestamp))
367
+ return new Error("DSH since 必须是有效的 ISO 8601 时间");
368
+ return new Date(timestamp).toISOString();
369
+ }
370
+ }
371
+ exports.DshSessionHistoryProvider = DshSessionHistoryProvider;