coding-tool-x 3.8.6 → 3.8.8

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 (134) hide show
  1. package/dist/web/assets/{AgentsDrawer-Db9wAyaZ.js → AgentsDrawer-DRQuwi0m.js} +1 -1
  2. package/dist/web/assets/Analytics-Cf5KwEdj.js +1 -0
  3. package/dist/web/assets/AssetPathField-DyniM0pF.js +1 -0
  4. package/dist/web/assets/{ChatHistoryDrawer-CwzJHyNP.js → ChatHistoryDrawer-CrfS0KYV.js} +1 -1
  5. package/dist/web/assets/{CollapseItem-CTCfSiue.js → CollapseItem-ClMsJuUu.js} +1 -1
  6. package/dist/web/assets/{CommandsDrawer-vDghB1U9.js → CommandsDrawer-CzHelf56.js} +1 -1
  7. package/dist/web/assets/ConfigExportDrawer-BEZ8wTby.js +1 -0
  8. package/dist/web/assets/ConfigTemplates-Crg3xWe5.js +1 -0
  9. package/dist/web/assets/{ConfigTemplatesDrawer-CbsJp76X.js → ConfigTemplatesDrawer-BqM32Im7.js} +1 -1
  10. package/dist/web/assets/{ConfigTemplatesPanel-liMSTh7s.js → ConfigTemplatesPanel-lTdutC8H.js} +1 -1
  11. package/dist/web/assets/{Divider-D28g1gwj.js → Divider-DsdbkHrG.js} +1 -1
  12. package/dist/web/assets/{FavoritesDrawer-D6QSiYaL.js → FavoritesDrawer-DDyWUBJR.js} +1 -1
  13. package/dist/web/assets/Forward-DLCL7aMY.js +1 -0
  14. package/dist/web/assets/{GatewayConvertDrawer-RvET6iNG.js → GatewayConvertDrawer-Rigtt_5J.js} +1 -1
  15. package/dist/web/assets/Home-tCSKmSHV.js +1 -0
  16. package/dist/web/assets/{MarkdownEditor-CYmLGYju.js → MarkdownEditor-BMMUufSc.js} +1 -1
  17. package/dist/web/assets/McpDrawer-CmE-d7VW.js +1 -0
  18. package/dist/web/assets/{McpFormDrawer-BcTlgSpN.js → McpFormDrawer-ByvS18od.js} +1 -1
  19. package/dist/web/assets/{PluginManager-Dh2o0xkZ.js → PluginManager-C4kJlpVX.js} +1 -1
  20. package/dist/web/assets/{PluginsDrawer-deKQ5u3I.js → PluginsDrawer-05R4k15x.js} +1 -1
  21. package/dist/web/assets/PluginsPanel-BCoEs7Og.js +1 -0
  22. package/dist/web/assets/PluginsPanel-CzGMK-Ap.css +1 -0
  23. package/dist/web/assets/ProjectList-BJt1OIaI.js +1 -0
  24. package/dist/web/assets/ProjectList-DG3yu8xZ.css +1 -0
  25. package/dist/web/assets/PromptsDrawer-DnwBf_oW.js +1 -0
  26. package/dist/web/assets/RecentSessionsDrawer-B7d-38wH.js +1 -0
  27. package/dist/web/assets/RecentSessionsDrawer-yrAD4p1A.css +1 -0
  28. package/dist/web/assets/SessionCard-BUgjHQwX.js +1 -0
  29. package/dist/web/assets/SessionList-B-bXZRFk.js +1 -0
  30. package/dist/web/assets/SessionList-qG_XpPBJ.css +1 -0
  31. package/dist/web/assets/{SettingsDrawer-u_vwUuPS.js → SettingsDrawer-BITzjQLl.js} +1 -1
  32. package/dist/web/assets/SkillManager-D9jkNuRM.js +1 -0
  33. package/dist/web/assets/{SkillsDrawer-CkF3BpyC.js → SkillsDrawer-0Z4IefxC.js} +1 -1
  34. package/dist/web/assets/SkillsPanel-BKIVOj7n.js +1 -0
  35. package/dist/web/assets/{SkillsPanel-B3Nm5j32.css → SkillsPanel-C4ke56nF.css} +1 -1
  36. package/dist/web/assets/{SpeedTestDrawer-BgfG1Ked.js → SpeedTestDrawer-Bx1bYf5i.js} +1 -1
  37. package/dist/web/assets/Upload-CzC4rGI4.js +1 -0
  38. package/dist/web/assets/{WorkspaceDrawer-Br5CeCzL.js → WorkspaceDrawer-eujtWpoq.js} +1 -1
  39. package/dist/web/assets/{WorkspaceManager-B2O8-qIw.js → WorkspaceManager-CQ-8Ej68.js} +1 -1
  40. package/dist/web/assets/{WorkspacePanel-5oDKit3c.js → WorkspacePanel-Cql5qE2R.js} +1 -1
  41. package/dist/web/assets/headers-BqDmkMjy.js +1 -0
  42. package/dist/web/assets/{icons-B2Vg-IOC.js → icons-DmFe4b5j.js} +1 -1
  43. package/dist/web/assets/index-Cmmwj3Fx.js +2 -0
  44. package/dist/web/assets/{index-Bi1MQHYX.css → index-Cnbe696C.css} +1 -1
  45. package/dist/web/assets/{light-Cp0t7v32.js → light-0eavxNvt.js} +1 -1
  46. package/dist/web/assets/platforms-BmzzwnSw.js +1 -0
  47. package/dist/web/assets/platforms-DWXgmy2w.js +1 -0
  48. package/dist/web/assets/session-visibility-BAmxh3cK.js +1 -0
  49. package/dist/web/assets/sessions-D4yQtfKb.js +1 -0
  50. package/dist/web/assets/sessions-DqlbocLJ.js +1 -0
  51. package/dist/web/assets/use-message-DqCwefG6.js +1 -0
  52. package/dist/web/assets/{useResponsiveDrawer-jbmqZuJs.js → useResponsiveDrawer-DZd5rNk5.js} +1 -1
  53. package/dist/web/index.html +7 -7
  54. package/package.json +1 -1
  55. package/src/commands/search.js +17 -2
  56. package/src/config/paths.js +2 -0
  57. package/src/platforms/driver-registry.js +9 -1
  58. package/src/platforms/drivers/claude/session-history-adapter.js +72 -1
  59. package/src/platforms/drivers/claude/sessions-implementation.js +49 -7
  60. package/src/platforms/drivers/claude/sessions.js +30 -6
  61. package/src/platforms/drivers/codex/parser.js +57 -1
  62. package/src/platforms/drivers/codex/session-history-adapter.js +8 -2
  63. package/src/platforms/drivers/codex/sessions-implementation.js +25 -3
  64. package/src/platforms/drivers/codex/sessions.js +18 -1
  65. package/src/platforms/drivers/dsh/api-operations.js +396 -0
  66. package/src/platforms/drivers/dsh/channels-implementation.js +458 -0
  67. package/src/platforms/drivers/dsh/channels.js +191 -0
  68. package/src/platforms/drivers/dsh/common.js +571 -0
  69. package/src/platforms/drivers/dsh/native-config.js +93 -0
  70. package/src/platforms/drivers/dsh/native-logs.js +116 -0
  71. package/src/platforms/drivers/dsh/plugin-manager.js +117 -0
  72. package/src/platforms/drivers/dsh/projects.js +23 -0
  73. package/src/platforms/drivers/dsh/proxy-implementation.js +260 -0
  74. package/src/platforms/drivers/dsh/proxy.js +23 -0
  75. package/src/platforms/drivers/dsh/resources.js +346 -0
  76. package/src/platforms/drivers/dsh/sessions.js +587 -0
  77. package/src/platforms/drivers/dsh/statistics-implementation.js +238 -0
  78. package/src/platforms/drivers/dsh/statistics.js +24 -0
  79. package/src/platforms/drivers/gemini/session-history-adapter.js +49 -1
  80. package/src/platforms/drivers/gemini/sessions-implementation.js +46 -1
  81. package/src/platforms/drivers/gemini/sessions.js +2 -1
  82. package/src/platforms/drivers/omp/session-history-adapter.js +42 -1
  83. package/src/platforms/drivers/omp/sessions-implementation.js +51 -3
  84. package/src/platforms/drivers/omp/sessions.js +33 -2
  85. package/src/platforms/drivers/opencode/sessions-implementation.js +89 -9
  86. package/src/platforms/drivers/opencode/sessions.js +37 -1
  87. package/src/platforms/manifest-schema.js +43 -3
  88. package/src/platforms/manifests/dsh.json +542 -0
  89. package/src/platforms/path-resolver.js +2 -0
  90. package/src/platforms/platform-path-context.js +12 -2
  91. package/src/platforms/registry.js +16 -1
  92. package/src/platforms/runtime.js +5 -0
  93. package/src/platforms/session-history-adapters.js +1 -0
  94. package/src/server/api/platform-api-config.js +23 -2
  95. package/src/server/api/platform-route-factory.js +9 -2
  96. package/src/server/api/platforms.js +19 -1
  97. package/src/server/api/plugins.js +108 -13
  98. package/src/server/services/config-registry-service.js +10 -1
  99. package/src/server/services/config-templates-service.js +1 -0
  100. package/src/server/services/plugins-service.js +474 -103
  101. package/src/server/services/session-history-index.js +900 -140
  102. package/src/server/services/session-history-worker.js +3 -1
  103. package/src/server/services/skill-service.js +214 -7
  104. package/src/server/services/usage-log-utils.js +1 -0
  105. package/src/server/services/workspace-service.js +39 -10
  106. package/src/shared/channel-driver.js +80 -5
  107. package/src/shared/driver-factories/api.js +22 -6
  108. package/src/shared/driver-factories/projects.js +8 -0
  109. package/dist/web/assets/Analytics-C4gMzv7g.js +0 -1
  110. package/dist/web/assets/AssetPathField-Bd_nMFu3.js +0 -1
  111. package/dist/web/assets/ConfigExportDrawer-C-XDvlHc.js +0 -1
  112. package/dist/web/assets/ConfigTemplates-CYY8XRED.js +0 -1
  113. package/dist/web/assets/Home-P8eseZcf.js +0 -1
  114. package/dist/web/assets/McpDrawer-Ch3bkKIq.js +0 -1
  115. package/dist/web/assets/PluginsPanel-BofsgQgj.js +0 -1
  116. package/dist/web/assets/PluginsPanel-D6ZqITcR.css +0 -1
  117. package/dist/web/assets/ProjectList-677RLZ8O.css +0 -1
  118. package/dist/web/assets/ProjectList-DhSWJHTA.js +0 -1
  119. package/dist/web/assets/PromptsDrawer-Cfb2FeeU.js +0 -1
  120. package/dist/web/assets/RecentSessionsDrawer-3eXamJhr.js +0 -1
  121. package/dist/web/assets/RecentSessionsDrawer-CKFsOYDM.css +0 -1
  122. package/dist/web/assets/SessionCard-CW0eHURI.js +0 -1
  123. package/dist/web/assets/SessionList-BpYRz1cX.css +0 -1
  124. package/dist/web/assets/SessionList-DQfyDGWU.js +0 -1
  125. package/dist/web/assets/SkillManager-DHMprJLA.js +0 -1
  126. package/dist/web/assets/SkillsPanel-DWE4pFGx.js +0 -1
  127. package/dist/web/assets/Upload-CNwaGOws.js +0 -1
  128. package/dist/web/assets/headers-cf-QapUq.js +0 -1
  129. package/dist/web/assets/index-BMAUPq5K.js +0 -2
  130. package/dist/web/assets/platforms-CBkB45JP.js +0 -1
  131. package/dist/web/assets/platforms-FTpjxAT6.js +0 -1
  132. package/dist/web/assets/sessions-Cmy31slD.js +0 -1
  133. package/dist/web/assets/sessions-yC_oVVh8.js +0 -1
  134. package/dist/web/assets/use-message-DoLM8C1C.js +0 -1
@@ -0,0 +1,587 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const zlib = require('zlib');
6
+ const { resolvePaths } = require('./common');
7
+
8
+ const GENERATION_PATTERN = /^session(?:\.v(\d+))?\.jsonl(\.zstd)?$/;
9
+ const ZSTD_MAGIC = 0xFD2FB528;
10
+
11
+ function projectNameFor(cwd) {
12
+ if (!cwd) return '__no-cwd__';
13
+ const encoded = Buffer.from(String(cwd), 'utf8').toString('base64url');
14
+ return `dsh-${encoded}`;
15
+ }
16
+
17
+ function textFromContent(content) {
18
+ if (typeof content === 'string') return content;
19
+ if (!Array.isArray(content)) return '';
20
+ return content.map(item => {
21
+ if (!item || typeof item !== 'object') return '';
22
+ if (item.type === 'text') return item.text || '';
23
+ if (item.type === 'image') return '[图片]';
24
+ return '';
25
+ }).filter(Boolean).join('\n\n').trim();
26
+ }
27
+
28
+ function zstdFrameEnd(input, start) {
29
+ if (input.length - start < 5) return null;
30
+ if (input.readUInt32LE(start) !== ZSTD_MAGIC) {
31
+ throw new Error('DSH session log contains an invalid Zstandard frame');
32
+ }
33
+
34
+ let offset = start + 4;
35
+ const descriptor = input[offset++];
36
+ if ((descriptor & 0x18) !== 0) throw new Error('DSH session log contains a reserved Zstandard frame flag');
37
+ const contentSizeFlag = descriptor >>> 6;
38
+ const singleSegment = (descriptor & 0x20) !== 0;
39
+ const checksum = (descriptor & 0x04) !== 0;
40
+ const dictionaryFlag = descriptor & 0x03;
41
+ const dictionaryBytes = dictionaryFlag === 3 ? 4 : dictionaryFlag;
42
+ const contentSizeBytes = contentSizeFlag === 0
43
+ ? (singleSegment ? 1 : 0)
44
+ : 1 << contentSizeFlag;
45
+ const headerBytes = (singleSegment ? 0 : 1) + dictionaryBytes + contentSizeBytes;
46
+ if (input.length - offset < headerBytes) return null;
47
+ offset += headerBytes;
48
+
49
+ let lastBlock = false;
50
+ while (!lastBlock) {
51
+ if (input.length - offset < 3) return null;
52
+ const blockHeader = input.readUIntLE(offset, 3);
53
+ offset += 3;
54
+ lastBlock = (blockHeader & 1) !== 0;
55
+ const blockType = (blockHeader >>> 1) & 0x03;
56
+ const blockSize = blockHeader >>> 3;
57
+ if (blockType === 0x03) throw new Error('DSH session log contains a reserved Zstandard block type');
58
+ const payloadBytes = blockType === 0x01 ? 1 : blockSize;
59
+ if (input.length - offset < payloadBytes) return null;
60
+ offset += payloadBytes;
61
+ }
62
+ if (checksum) {
63
+ if (input.length - offset < 4) return null;
64
+ offset += 4;
65
+ }
66
+ return offset;
67
+ }
68
+
69
+ function decompressZstdFrames(input) {
70
+ const chunks = [];
71
+ let offset = 0;
72
+ while (offset < input.length) {
73
+ const end = zstdFrameEnd(input, offset);
74
+ if (end === null) throw new Error('DSH session log has a truncated Zstandard frame');
75
+ chunks.push(zlib.zstdDecompressSync(input.subarray(offset, end)));
76
+ offset = end;
77
+ }
78
+ return Buffer.concat(chunks);
79
+ }
80
+
81
+ /**
82
+ * Read only the beginning of a framed zstd log. DSH normally emits one
83
+ * frame per JSONL record, so decoding complete frames until the first 64 KiB
84
+ * is enough for a list summary without materializing the whole transcript.
85
+ */
86
+ function readZstdPrefix(filePath, maxOutput = 64 * 1024, maxInput = 4 * 1024 * 1024) {
87
+ if (typeof zlib.zstdDecompressSync !== 'function') {
88
+ const error = new Error('当前 Node.js 不支持读取 DSH zstd session logs');
89
+ error.code = 'DSH_ZSTD_UNSUPPORTED';
90
+ throw error;
91
+ }
92
+
93
+ let fd;
94
+ try {
95
+ fd = fs.openSync(filePath, 'r');
96
+ const chunks = [];
97
+ let buffered = Buffer.alloc(0);
98
+ let outputSize = 0;
99
+ let inputSize = 0;
100
+ const chunkSize = 64 * 1024;
101
+ const outputChunks = [];
102
+ let eof = false;
103
+
104
+ while (!eof && inputSize < maxInput && outputSize < maxOutput) {
105
+ const readSize = Math.min(chunkSize, maxInput - inputSize);
106
+ const buffer = Buffer.alloc(readSize);
107
+ const bytesRead = fs.readSync(fd, buffer, 0, readSize, inputSize);
108
+ if (bytesRead === 0) {
109
+ eof = true;
110
+ } else {
111
+ inputSize += bytesRead;
112
+ chunks.push(buffer.subarray(0, bytesRead));
113
+ buffered = Buffer.concat(chunks);
114
+ }
115
+
116
+ let offset = 0;
117
+ while (offset < buffered.length && outputSize < maxOutput) {
118
+ const end = zstdFrameEnd(buffered, offset);
119
+ if (end === null) break;
120
+ const decoded = zlib.zstdDecompressSync(buffered.subarray(offset, end));
121
+ outputChunks.push(decoded);
122
+ outputSize += decoded.length;
123
+ offset = end;
124
+ }
125
+
126
+ if (offset > 0) {
127
+ // Keep only the incomplete trailing frame before reading more input.
128
+ const remainder = buffered.subarray(offset);
129
+ chunks.length = 0;
130
+ if (remainder.length > 0) chunks.push(remainder);
131
+ buffered = remainder;
132
+ }
133
+ if (bytesRead < readSize) eof = true;
134
+ }
135
+
136
+ return Buffer.concat(outputChunks).subarray(0, maxOutput);
137
+ } finally {
138
+ if (fd !== undefined) {
139
+ try { fs.closeSync(fd); } catch (_) {}
140
+ }
141
+ }
142
+ }
143
+
144
+ function eventText(event) {
145
+ const data = event?.data || {};
146
+ if (event?.type === 'user/message' || event?.type === 'assistant/message') {
147
+ return textFromContent(event.type === 'user/message' ? data.content : data.message?.content);
148
+ }
149
+ if (event?.type === 'tool/result') return textFromContent(data.message?.content || data.content || data.result);
150
+ if (event?.type === 'tool/call') return textFromContent(data.arguments || data.input || data.message?.content);
151
+ return '';
152
+ }
153
+
154
+ function decodeArtifact(filePath) {
155
+ let bytes = fs.readFileSync(filePath);
156
+ if (filePath.endsWith('.zstd')) {
157
+ if (typeof zlib.zstdDecompressSync !== 'function') {
158
+ const error = new Error('当前 Node.js 不支持读取 DSH zstd session logs');
159
+ error.code = 'DSH_ZSTD_UNSUPPORTED';
160
+ throw error;
161
+ }
162
+ bytes = decompressZstdFrames(bytes);
163
+ }
164
+ const records = [];
165
+ for (const line of bytes.toString('utf8').split(/\r?\n/)) {
166
+ if (!line.trim()) continue;
167
+ try {
168
+ records.push(JSON.parse(line));
169
+ } catch (error) {
170
+ const wrapped = new Error(`无法解析 DSH session log: ${path.basename(filePath)}`);
171
+ wrapped.code = 'DSH_SESSION_CORRUPT';
172
+ wrapped.cause = error;
173
+ throw wrapped;
174
+ }
175
+ }
176
+ const header = records[0];
177
+ if (!header || header.type !== 'session' || typeof header.id !== 'string') {
178
+ const error = new Error(`DSH session log 缺少有效 header: ${path.basename(filePath)}`);
179
+ error.code = 'DSH_SESSION_INVALID_HEADER';
180
+ throw error;
181
+ }
182
+ return { header, events: records.slice(1) };
183
+ }
184
+
185
+ function decodeArtifactHeaderBytes(bytes) {
186
+ const lines = bytes.toString('utf8').split(/\r?\n/);
187
+ const header = JSON.parse(lines.shift() || '');
188
+ if (!header || header.type !== 'session' || typeof header.id !== 'string') {
189
+ throw new Error('DSH session log missing a valid header');
190
+ }
191
+ let firstMessage = null;
192
+ for (const line of lines) {
193
+ if (!line.trim()) continue;
194
+ try {
195
+ const event = JSON.parse(line);
196
+ if (event.type === 'user/message' && event.data?.content) {
197
+ firstMessage = textFromContent(event.data.content) || null;
198
+ if (firstMessage) break;
199
+ }
200
+ } catch (_) {
201
+ // The bounded prefix can end in the middle of a JSON record.
202
+ }
203
+ }
204
+ return { header, events: [], firstMessage };
205
+ }
206
+
207
+ function decodeArtifactHeader(filePath) {
208
+ if (filePath.endsWith('.zstd')) {
209
+ return decodeArtifactHeaderBytes(readZstdPrefix(filePath));
210
+ }
211
+ let fd;
212
+ try {
213
+ fd = fs.openSync(filePath, 'r');
214
+ const buffer = Buffer.alloc(64 * 1024);
215
+ const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
216
+ return decodeArtifactHeaderBytes(buffer.subarray(0, bytesRead));
217
+ } finally {
218
+ if (fd !== undefined) {
219
+ try { fs.closeSync(fd); } catch (_) {}
220
+ }
221
+ }
222
+ }
223
+
224
+ function selectArtifact(sessionDir) {
225
+ let entries;
226
+ try {
227
+ entries = fs.readdirSync(sessionDir, { withFileTypes: true });
228
+ } catch (_) {
229
+ return null;
230
+ }
231
+ const candidates = entries.map(entry => {
232
+ if (!entry.isFile()) return null;
233
+ const match = GENERATION_PATTERN.exec(entry.name);
234
+ if (!match) return null;
235
+ return {
236
+ path: path.join(sessionDir, entry.name),
237
+ version: match[1] ? Number(match[1]) : 0,
238
+ compressed: Boolean(match[2])
239
+ };
240
+ }).filter(Boolean);
241
+ candidates.sort((left, right) => right.version - left.version || Number(right.compressed) - Number(left.compressed));
242
+ return candidates[0] || null;
243
+ }
244
+
245
+ function listArtifacts(context = {}, { includeEvents = true } = {}) {
246
+ const root = resolvePaths(context).sessions;
247
+ if (!fs.existsSync(root)) return [];
248
+ const artifacts = [];
249
+ for (const projectEntry of fs.readdirSync(root, { withFileTypes: true })) {
250
+ if (!projectEntry.isDirectory() || projectEntry.isSymbolicLink()) continue;
251
+ const projectDir = path.join(root, projectEntry.name);
252
+ for (const sessionEntry of fs.readdirSync(projectDir, { withFileTypes: true })) {
253
+ if (!sessionEntry.isDirectory() || sessionEntry.isSymbolicLink()) continue;
254
+ const sessionDir = path.join(projectDir, sessionEntry.name);
255
+ const artifact = selectArtifact(sessionDir);
256
+ if (!artifact) continue;
257
+ try {
258
+ const stat = fs.statSync(artifact.path);
259
+ const parsed = includeEvents
260
+ ? decodeArtifact(artifact.path)
261
+ : decodeArtifactHeader(artifact.path);
262
+ const cwd = typeof parsed.header.cwd === 'string' ? parsed.header.cwd : null;
263
+ const events = parsed.events;
264
+ const user = events.find(event => event.type === 'user/message' && event.data?.content);
265
+ artifacts.push({
266
+ ...artifact,
267
+ projectDir: projectEntry.name,
268
+ sessionDir,
269
+ filePath: artifact.path,
270
+ size: stat.size,
271
+ mtimeMs: stat.mtimeMs,
272
+ header: parsed.header,
273
+ events,
274
+ cwd,
275
+ projectName: projectNameFor(cwd),
276
+ firstMessage: parsed.firstMessage || textFromContent(user?.data?.content) || null
277
+ });
278
+ } catch (error) {
279
+ artifacts.push({
280
+ ...artifact,
281
+ projectDir: projectEntry.name,
282
+ sessionDir,
283
+ filePath: artifact.path,
284
+ size: 0,
285
+ mtimeMs: 0,
286
+ cwd: null,
287
+ projectName: '__invalid__',
288
+ error: error.message,
289
+ errorCode: error.code || 'DSH_SESSION_READ_FAILED'
290
+ });
291
+ }
292
+ }
293
+ }
294
+ return artifacts;
295
+ }
296
+
297
+ function displayName(cwd) {
298
+ if (!cwd) return '未指定工作目录';
299
+ return path.basename(cwd) || cwd;
300
+ }
301
+
302
+ function summary(artifact) {
303
+ const header = artifact.header || {};
304
+ return {
305
+ sessionId: header.id || null,
306
+ projectName: artifact.projectName,
307
+ projectDisplayName: displayName(artifact.cwd),
308
+ projectFullPath: artifact.cwd,
309
+ cwd: artifact.cwd,
310
+ parentSession: header.parentSession || null,
311
+ origin: header.origin || null,
312
+ createdAt: header.createdAt || null,
313
+ updatedAt: artifact.mtimeMs,
314
+ mtime: artifact.mtimeMs,
315
+ size: artifact.size,
316
+ filePath: artifact.filePath,
317
+ firstMessage: artifact.firstMessage,
318
+ readOnly: true,
319
+ running: null,
320
+ error: artifact.error || null,
321
+ errorCode: artifact.errorCode || null
322
+ };
323
+ }
324
+
325
+ function normalizeMessage(event, sessionId, index, userMessageNumber) {
326
+ const data = event?.data || {};
327
+ let type = null;
328
+ let content = '';
329
+ let role = null;
330
+ let model = data.message?.source?.model || data.model || null;
331
+ let provider = data.message?.source?.provider || data.provider || null;
332
+ if (event.type === 'user/message') {
333
+ type = 'user';
334
+ role = 'user';
335
+ content = data.content;
336
+ } else if (event.type === 'assistant/message') {
337
+ type = 'assistant';
338
+ role = 'assistant';
339
+ content = textFromContent(data.message?.content);
340
+ } else if (event.type === 'tool/result') {
341
+ type = 'assistant';
342
+ role = 'assistant';
343
+ content = `**[工具结果]**\n\`\`\`\n${textFromContent(data.content || data.result || data.message?.content)}\n\`\`\``;
344
+ } else {
345
+ return null;
346
+ }
347
+ if (!content || (typeof content === 'string' && !content.trim())) return null;
348
+ const timestamp = Number.isFinite(event.time) ? event.time : null;
349
+ const message = {
350
+ messageId: data.message?.id || data.id || `${sessionId}:${event.seq ?? index}`,
351
+ role,
352
+ type,
353
+ subtype: event.type === 'tool/result' ? 'tool_result' : null,
354
+ content,
355
+ timestamp,
356
+ model,
357
+ provider,
358
+ userMessageNumber: type === 'user' ? userMessageNumber + 1 : null,
359
+ extra: { dshEventType: event.type, seq: event.seq ?? index }
360
+ };
361
+ return message;
362
+ }
363
+
364
+ function createDriver(context = {}) {
365
+ const read = () => listArtifacts(context, { includeEvents: false }).filter(artifact => artifact.projectName !== '__invalid__');
366
+ const readFull = () => listArtifacts(context, { includeEvents: true }).filter(artifact => artifact.projectName !== '__invalid__');
367
+ const fullSessionCache = new Map();
368
+ const getById = sessionId => {
369
+ const summaryArtifact = read().find(artifact => artifact.header?.id === sessionId);
370
+ if (!summaryArtifact) {
371
+ fullSessionCache.delete(sessionId);
372
+ return null;
373
+ }
374
+ const cached = fullSessionCache.get(sessionId);
375
+ if (cached
376
+ && cached.filePath === summaryArtifact.filePath
377
+ && cached.size === summaryArtifact.size
378
+ && cached.mtimeMs === summaryArtifact.mtimeMs) {
379
+ return cached;
380
+ }
381
+ const parsed = decodeArtifact(summaryArtifact.filePath);
382
+ const artifact = {
383
+ ...summaryArtifact,
384
+ header: parsed.header,
385
+ events: parsed.events
386
+ };
387
+ fullSessionCache.set(sessionId, artifact);
388
+ return artifact;
389
+ };
390
+ const readProject = projectName => read().filter(artifact => artifact.projectName === projectName);
391
+ const readFullProject = projectName => readFull().filter(artifact => artifact.projectName === projectName);
392
+ const listProjects = () => {
393
+ const map = new Map();
394
+ for (const artifact of read()) {
395
+ const current = map.get(artifact.projectName) || {
396
+ name: artifact.projectName,
397
+ displayName: displayName(artifact.cwd),
398
+ fullPath: artifact.cwd,
399
+ path: artifact.cwd,
400
+ sessionCount: 0,
401
+ lastModified: 0,
402
+ readOnly: true
403
+ };
404
+ current.sessionCount += 1;
405
+ current.lastModified = Math.max(current.lastModified, artifact.mtimeMs || 0);
406
+ map.set(artifact.projectName, current);
407
+ }
408
+ return [...map.values()].sort((left, right) => right.lastModified - left.lastModified || left.displayName.localeCompare(right.displayName));
409
+ };
410
+ const listProjectsPage = (options = {}) => {
411
+ const page = Math.max(1, Number.parseInt(options.page, 10) || 1);
412
+ const limit = Math.max(1, Math.min(100, Number.parseInt(options.limit, 10) || 20));
413
+ const query = String(options.q || options.search || '').trim().toLowerCase();
414
+ const all = listProjects().filter(project => !query
415
+ || project.name.toLowerCase().includes(query)
416
+ || project.displayName.toLowerCase().includes(query)
417
+ || String(project.fullPath || '').toLowerCase().includes(query));
418
+ const projects = all
419
+ .sort((left, right) => right.lastModified - left.lastModified || left.name.localeCompare(right.name))
420
+ .slice((page - 1) * limit, page * limit);
421
+ return {
422
+ projects,
423
+ currentProject: null,
424
+ pagination: { page, limit, total: all.length, hasMore: page * limit < all.length }
425
+ };
426
+ };
427
+ const requestOptions = (value, fallback = {}) => value && typeof value === 'object' && !Array.isArray(value)
428
+ ? value
429
+ : (fallback || {});
430
+ const listSessionsForProject = projectName => readProject(projectName).map(summary).sort((left, right) => right.updatedAt - left.updatedAt || left.sessionId.localeCompare(right.sessionId));
431
+ const listSessionsPage = (projectNameOrRequest, options = {}) => {
432
+ const request = requestOptions(projectNameOrRequest, options);
433
+ const projectName = request.params?.projectName || (typeof projectNameOrRequest === 'string' ? projectNameOrRequest : '');
434
+ const page = Math.max(1, Number.parseInt(request.query?.page ?? request.page, 10) || 1);
435
+ const limit = Math.max(1, Math.min(100, Number.parseInt(request.query?.limit ?? request.limit, 10) || 20));
436
+ const query = String(request.query?.q ?? request.q ?? '').trim().toLowerCase();
437
+ const all = listSessionsForProject(projectName).filter(session => !query
438
+ || session.sessionId.toLowerCase().includes(query)
439
+ || String(session.firstMessage || '').toLowerCase().includes(query)
440
+ || String(session.projectFullPath || '').toLowerCase().includes(query));
441
+ const sessions = all.slice((page - 1) * limit, page * limit);
442
+ return {
443
+ sessions,
444
+ totalSize: all.reduce((sum, session) => sum + (Number(session.size) || 0), 0),
445
+ projectInfo: { sessionCount: all.length, totalSize: all.reduce((sum, session) => sum + (Number(session.size) || 0), 0) },
446
+ pagination: { page, limit, total: all.length, hasMore: page * limit < all.length }
447
+ };
448
+ };
449
+ const recentSessions = (limit = 5) => read().map(summary).sort((left, right) => right.updatedAt - left.updatedAt).slice(0, Math.max(1, limit));
450
+ const searchProject = (projectName, keyword, contextLength = 15) => {
451
+ const needle = String(keyword || '').toLowerCase();
452
+ return readFullProject(projectName).flatMap(artifact => {
453
+ const matches = artifact.events.map((event, index) => ({ event, index, text: eventText(event) }))
454
+ .filter(entry => entry.text && entry.text.toLowerCase().includes(needle))
455
+ .map(entry => ({ line: entry.index + 2, text: entry.text.slice(0, Math.max(20, contextLength * 20)) }));
456
+ return matches.length ? [{ ...summary(artifact), matchCount: matches.length, matches }] : [];
457
+ });
458
+ };
459
+ const searchAllProjects = (keyword, limit = 35, projectName = null) => {
460
+ const all = readFull().filter(artifact => !projectName || artifact.projectName === projectName).flatMap(artifact => {
461
+ const matches = artifact.events.map((event, index) => ({ event, index, text: eventText(event) }))
462
+ .filter(entry => entry.text && entry.text.toLowerCase().includes(String(keyword || '').toLowerCase()))
463
+ .map(entry => ({ line: entry.index + 2, text: entry.text.slice(0, Math.max(20, Number(limit) || 35) * 20) }));
464
+ return matches.length ? [{ ...summary(artifact), matchCount: matches.length, matches }] : [];
465
+ });
466
+ return all.sort((left, right) => right.updatedAt - left.updatedAt || left.sessionId.localeCompare(right.sessionId)).slice(0, Math.max(1, Number(limit) || 35));
467
+ };
468
+ const messagesForSession = (sessionId, options = {}) => {
469
+ const artifact = getById(sessionId);
470
+ if (!artifact) return null;
471
+ const mapped = [];
472
+ let userCount = 0;
473
+ for (const [index, event] of artifact.events.entries()) {
474
+ const message = normalizeMessage(event, sessionId, index, userCount);
475
+ if (!message) continue;
476
+ if (message.type === 'user') userCount += 1;
477
+ mapped.push(message);
478
+ }
479
+ const page = Math.max(1, Number(options.page) || 1);
480
+ const limit = Math.max(1, Math.min(200, Number(options.limit) || 20));
481
+ const order = options.order === 'asc' ? 'asc' : 'desc';
482
+ const ordered = order === 'desc' ? [...mapped].reverse() : mapped;
483
+ const offset = (page - 1) * limit;
484
+ return {
485
+ messages: ordered.slice(offset, offset + limit),
486
+ metadata: {
487
+ sessionId,
488
+ projectName: artifact.projectName,
489
+ projectFullPath: artifact.cwd,
490
+ createdAt: artifact.header.createdAt,
491
+ readOnly: true
492
+ },
493
+ pagination: { page, limit, total: ordered.length, hasMore: offset + limit < ordered.length }
494
+ };
495
+ };
496
+ const outlineForSession = sessionId => {
497
+ const artifact = getById(sessionId);
498
+ if (!artifact) return null;
499
+ let userMessageNumber = 0;
500
+ const items = [];
501
+ for (const [index, event] of artifact.events.entries()) {
502
+ if (event.type !== 'user/message') continue;
503
+ userMessageNumber += 1;
504
+ const preview = textFromContent(event.data?.content);
505
+ items.push({ userMessageNumber, preview: preview.slice(0, 200), timestamp: Number.isFinite(event.time) ? event.time : null, index });
506
+ }
507
+ return { sessionId, items };
508
+ };
509
+ const statusForSession = sessionId => {
510
+ const artifact = read().find(item => item.header?.id === sessionId) || null;
511
+ if (!artifact) return null;
512
+ return { sessionId, running: null, readOnly: true, lastModified: artifact.mtimeMs, size: artifact.size };
513
+ };
514
+ const unsupported = operation => () => ({
515
+ status: 'unsupported',
516
+ platform: 'dsh',
517
+ capability: 'sessions',
518
+ operation
519
+ });
520
+
521
+ return {
522
+ platform: 'dsh',
523
+ capability: 'sessions',
524
+ listSessions(requestOrProjectName, options = {}) {
525
+ const request = requestOptions(requestOrProjectName, options);
526
+ const projectName = request.params?.projectName || (typeof requestOrProjectName === 'string' ? requestOrProjectName : '');
527
+ return listSessionsForProject(projectName);
528
+ },
529
+ listSessionsPage,
530
+ recent(requestOrLimit, options = {}) {
531
+ const request = requestOptions(requestOrLimit, options);
532
+ const limit = request.query?.limit || (typeof requestOrLimit === 'number' ? requestOrLimit : 5);
533
+ return recentSessions(limit);
534
+ },
535
+ search(requestOrProjectName, keyword, contextLength = 15, options = {}) {
536
+ if (requestOrProjectName && typeof requestOrProjectName === 'object') {
537
+ const request = requestOrProjectName;
538
+ return searchProject(request.params?.projectName, request.query?.keyword || request.query?.q || '', Number(request.query?.context) || 15);
539
+ }
540
+ return searchProject(requestOrProjectName, keyword, contextLength);
541
+ },
542
+ searchAcrossProjects(requestOrKeyword, limit = 35, options = {}) {
543
+ if (requestOrKeyword && typeof requestOrKeyword === 'object') {
544
+ const request = requestOrKeyword;
545
+ return searchAllProjects(
546
+ request.query?.keyword || request.query?.q || '',
547
+ Number(request.query?.limit) || 35,
548
+ request.query?.projectName || request.query?.project || null
549
+ );
550
+ }
551
+ return searchAllProjects(requestOrKeyword, limit, options.projectName || options.project || null);
552
+ },
553
+ messages(requestOrSessionId, options = {}) {
554
+ if (requestOrSessionId && typeof requestOrSessionId === 'object') {
555
+ const request = requestOrSessionId;
556
+ return messagesForSession(request.params?.sessionId, {
557
+ ...request.query,
558
+ page: request.query?.page,
559
+ limit: request.query?.limit,
560
+ order: request.query?.order
561
+ });
562
+ }
563
+ return messagesForSession(requestOrSessionId, options);
564
+ },
565
+ outline(requestOrSessionId) {
566
+ const sessionId = requestOrSessionId && typeof requestOrSessionId === 'object'
567
+ ? requestOrSessionId.params?.sessionId
568
+ : requestOrSessionId;
569
+ return outlineForSession(sessionId);
570
+ },
571
+ status(requestOrSessionId) {
572
+ const sessionId = requestOrSessionId && typeof requestOrSessionId === 'object'
573
+ ? requestOrSessionId.params?.sessionId
574
+ : requestOrSessionId;
575
+ return statusForSession(sessionId);
576
+ },
577
+ listProjects,
578
+ listProjectsPage,
579
+ delete: unsupported('delete'),
580
+ batchDelete: unsupported('batchDelete'),
581
+ fork: unsupported('fork'),
582
+ saveSessionOrder: unsupported('saveSessionOrder'),
583
+ launch: unsupported('launch')
584
+ };
585
+ }
586
+
587
+ module.exports = { createDriver, projectNameFor, decodeArtifact, listArtifacts, textFromContent };