@adversity/coding-tool-x 3.1.0 → 3.1.2

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 (142) hide show
  1. package/CHANGELOG.md +39 -18
  2. package/README.md +8 -8
  3. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  4. package/dist/web/assets/ConfigTemplates-DvcbKKdS.js +1 -0
  5. package/dist/web/assets/Home-BJKPCBuk.css +1 -0
  6. package/dist/web/assets/Home-Cw-F_Wnu.js +1 -0
  7. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  8. package/dist/web/assets/PluginManager-jy_4GVxI.js +1 -0
  9. package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
  10. package/dist/web/assets/ProjectList-Df1-NcNr.js +1 -0
  11. package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
  12. package/dist/web/assets/SessionList-UWcZtC2r.js +1 -0
  13. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  14. package/dist/web/assets/SkillManager-IRdseMKB.js +1 -0
  15. package/dist/web/assets/Terminal-BasTyDut.js +1 -0
  16. package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
  17. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  18. package/dist/web/assets/WorkspaceManager-D-D2kK1V.js +1 -0
  19. package/dist/web/assets/icons-kcfLIMBB.js +1 -0
  20. package/dist/web/assets/index-CoB3zF0K.css +1 -0
  21. package/dist/web/assets/index-CryrSLv8.js +2 -0
  22. package/dist/web/assets/markdown-BfC0goYb.css +10 -0
  23. package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
  24. package/dist/web/assets/naive-ui-CSrLusZZ.js +1 -0
  25. package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
  26. package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
  27. package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
  28. package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
  29. package/dist/web/index.html +8 -6
  30. package/package.json +4 -2
  31. package/src/commands/channels.js +48 -1
  32. package/src/commands/cli-type.js +4 -2
  33. package/src/commands/daemon.js +81 -12
  34. package/src/commands/doctor.js +10 -9
  35. package/src/commands/list.js +1 -1
  36. package/src/commands/logs.js +6 -4
  37. package/src/commands/port-config.js +24 -4
  38. package/src/commands/proxy-control.js +12 -6
  39. package/src/commands/search.js +1 -1
  40. package/src/commands/security.js +3 -2
  41. package/src/commands/stats.js +226 -52
  42. package/src/commands/switch.js +1 -1
  43. package/src/commands/toggle-proxy.js +31 -6
  44. package/src/commands/update.js +97 -0
  45. package/src/commands/workspace.js +1 -1
  46. package/src/config/default.js +41 -2
  47. package/src/config/loader.js +74 -8
  48. package/src/config/model-metadata.js +415 -0
  49. package/src/config/model-pricing.js +23 -93
  50. package/src/config/paths.js +105 -33
  51. package/src/index.js +64 -3
  52. package/src/plugins/constants.js +3 -2
  53. package/src/plugins/plugin-api.js +1 -1
  54. package/src/reset-config.js +4 -2
  55. package/src/server/api/agents.js +57 -14
  56. package/src/server/api/channels.js +112 -33
  57. package/src/server/api/codex-channels.js +111 -18
  58. package/src/server/api/codex-proxy.js +14 -8
  59. package/src/server/api/commands.js +71 -18
  60. package/src/server/api/config-export.js +0 -6
  61. package/src/server/api/config-registry.js +11 -3
  62. package/src/server/api/config.js +376 -5
  63. package/src/server/api/convert.js +133 -0
  64. package/src/server/api/dashboard.js +22 -6
  65. package/src/server/api/gemini-channels.js +107 -18
  66. package/src/server/api/gemini-proxy.js +14 -8
  67. package/src/server/api/gemini-sessions.js +1 -1
  68. package/src/server/api/health-check.js +4 -3
  69. package/src/server/api/mcp.js +3 -3
  70. package/src/server/api/opencode-channels.js +497 -0
  71. package/src/server/api/opencode-projects.js +99 -0
  72. package/src/server/api/opencode-proxy.js +207 -0
  73. package/src/server/api/opencode-sessions.js +345 -0
  74. package/src/server/api/opencode-statistics.js +57 -0
  75. package/src/server/api/plugins.js +66 -19
  76. package/src/server/api/prompts.js +2 -2
  77. package/src/server/api/proxy.js +7 -4
  78. package/src/server/api/sessions.js +3 -0
  79. package/src/server/api/settings.js +111 -0
  80. package/src/server/api/skills.js +69 -18
  81. package/src/server/api/workspaces.js +78 -6
  82. package/src/server/codex-proxy-server.js +36 -22
  83. package/src/server/dev-server.js +1 -1
  84. package/src/server/gemini-proxy-server.js +21 -7
  85. package/src/server/index.js +174 -58
  86. package/src/server/opencode-proxy-server.js +5486 -0
  87. package/src/server/proxy-server.js +33 -22
  88. package/src/server/services/agents-service.js +61 -24
  89. package/src/server/services/channel-scheduler.js +9 -5
  90. package/src/server/services/channels.js +64 -37
  91. package/src/server/services/codex-channels.js +56 -43
  92. package/src/server/services/codex-sessions.js +105 -6
  93. package/src/server/services/codex-settings-manager.js +271 -49
  94. package/src/server/services/codex-statistics-service.js +2 -2
  95. package/src/server/services/commands-service.js +84 -25
  96. package/src/server/services/config-export-service.js +7 -45
  97. package/src/server/services/config-registry-service.js +63 -17
  98. package/src/server/services/config-sync-manager.js +160 -7
  99. package/src/server/services/config-templates-service.js +204 -51
  100. package/src/server/services/env-checker.js +50 -13
  101. package/src/server/services/env-manager.js +155 -19
  102. package/src/server/services/favorites.js +5 -3
  103. package/src/server/services/gemini-channels.js +33 -44
  104. package/src/server/services/gemini-statistics-service.js +2 -2
  105. package/src/server/services/mcp-service.js +350 -9
  106. package/src/server/services/model-detector.js +707 -221
  107. package/src/server/services/network-access.js +80 -0
  108. package/src/server/services/opencode-channels.js +208 -0
  109. package/src/server/services/opencode-gateway-converter.js +639 -0
  110. package/src/server/services/opencode-sessions.js +931 -0
  111. package/src/server/services/opencode-settings-manager.js +478 -0
  112. package/src/server/services/opencode-statistics-service.js +255 -0
  113. package/src/server/services/plugins-service.js +479 -22
  114. package/src/server/services/prompts-service.js +53 -11
  115. package/src/server/services/proxy-runtime.js +1 -1
  116. package/src/server/services/repo-scanner-base.js +1 -1
  117. package/src/server/services/response-decoder.js +21 -0
  118. package/src/server/services/security-config.js +1 -1
  119. package/src/server/services/session-cache.js +1 -1
  120. package/src/server/services/skill-service.js +300 -46
  121. package/src/server/services/speed-test.js +464 -186
  122. package/src/server/services/statistics-service.js +2 -2
  123. package/src/server/services/terminal-commands.js +10 -3
  124. package/src/server/services/terminal-config.js +1 -1
  125. package/src/server/services/ui-config.js +1 -1
  126. package/src/server/services/workspace-service.js +57 -100
  127. package/src/server/websocket-server.js +156 -8
  128. package/src/ui/menu.js +49 -40
  129. package/src/utils/port-helper.js +22 -8
  130. package/src/utils/session.js +5 -4
  131. package/dist/web/assets/icons-CO_2OFES.js +0 -1
  132. package/dist/web/assets/index-DI8QOi-E.js +0 -14
  133. package/dist/web/assets/index-uLHGdeZh.css +0 -41
  134. package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
  135. package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
  136. package/src/server/api/oauth.js +0 -294
  137. package/src/server/api/permissions.js +0 -385
  138. package/src/server/config/oauth-providers.js +0 -68
  139. package/src/server/services/oauth-callback-server.js +0 -284
  140. package/src/server/services/oauth-service.js +0 -378
  141. package/src/server/services/oauth-token-storage.js +0 -135
  142. package/src/server/services/permission-templates-service.js +0 -308
@@ -0,0 +1,931 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+ const { execFileSync } = require('child_process');
5
+ const { NATIVE_PATHS, PATHS } = require('../../config/paths');
6
+
7
+ /**
8
+ * OpenCode 会话服务
9
+ * 读取 OpenCode SQLite 会话数据
10
+ */
11
+
12
+ const PROJECT_ORDER_FILE = path.join(PATHS.base, 'opencode-project-order.json');
13
+ const SESSION_ORDER_FILE = path.join(PATHS.base, 'opencode-session-order.json');
14
+ const OPENCODE_DB_PATH = path.join(NATIVE_PATHS.opencode.data, 'opencode.db');
15
+ const COUNTS_CACHE_TTL_MS = 30 * 1000;
16
+ const EMPTY_COUNTS = Object.freeze({ projectCount: 0, sessionCount: 0 });
17
+
18
+ let countsCache = {
19
+ expiresAt: 0,
20
+ value: EMPTY_COUNTS
21
+ };
22
+
23
+ function ensureParentDir(filePath) {
24
+ const dir = path.dirname(filePath);
25
+ if (!fs.existsSync(dir)) {
26
+ fs.mkdirSync(dir, { recursive: true });
27
+ }
28
+ }
29
+
30
+ function readJsonSafe(filePath, fallback) {
31
+ if (!filePath || !fs.existsSync(filePath)) {
32
+ return fallback;
33
+ }
34
+ try {
35
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
36
+ } catch (err) {
37
+ return fallback;
38
+ }
39
+ }
40
+
41
+ function writeJsonSafe(filePath, data) {
42
+ ensureParentDir(filePath);
43
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
44
+ }
45
+
46
+ function sortByOrder(items, order, fallbackCompare) {
47
+ const fallbackSorted = [...items].sort(fallbackCompare);
48
+ if (!Array.isArray(order) || order.length === 0) {
49
+ return fallbackSorted;
50
+ }
51
+
52
+ const orderMap = new Map(order.map((name, idx) => [name, idx]));
53
+ return fallbackSorted.sort((a, b) => {
54
+ const aIndex = orderMap.has(a.name) ? orderMap.get(a.name) : Number.MAX_SAFE_INTEGER;
55
+ const bIndex = orderMap.has(b.name) ? orderMap.get(b.name) : Number.MAX_SAFE_INTEGER;
56
+ if (aIndex === bIndex) {
57
+ return fallbackCompare(a, b);
58
+ }
59
+ return aIndex - bIndex;
60
+ });
61
+ }
62
+
63
+ function parseJsonMaybe(raw, fallback = null) {
64
+ if (typeof raw !== 'string') {
65
+ return fallback;
66
+ }
67
+ try {
68
+ return JSON.parse(raw);
69
+ } catch (err) {
70
+ return fallback;
71
+ }
72
+ }
73
+
74
+ function extractTextContent(content) {
75
+ if (typeof content === 'string') {
76
+ return content;
77
+ }
78
+ if (Array.isArray(content)) {
79
+ return content
80
+ .filter(item => item && item.type === 'text' && typeof item.text === 'string')
81
+ .map(item => item.text)
82
+ .join('\n')
83
+ .trim();
84
+ }
85
+ return '';
86
+ }
87
+
88
+ function extractTextFromPartData(partData) {
89
+ if (!partData || typeof partData !== 'object') {
90
+ return '';
91
+ }
92
+
93
+ if (typeof partData.text === 'string' && partData.text.trim()) {
94
+ return partData.text.trim();
95
+ }
96
+
97
+ if (typeof partData.content === 'string' && partData.content.trim()) {
98
+ return partData.content.trim();
99
+ }
100
+
101
+ if (Array.isArray(partData.content)) {
102
+ return partData.content
103
+ .filter(item => item && item.type === 'text' && typeof item.text === 'string')
104
+ .map(item => item.text)
105
+ .join('\n')
106
+ .trim();
107
+ }
108
+
109
+ return '';
110
+ }
111
+
112
+ function extractTextFromMessageData(messageData) {
113
+ if (!messageData || typeof messageData !== 'object') {
114
+ return '';
115
+ }
116
+
117
+ const contentText = extractTextContent(messageData.content);
118
+ if (contentText) {
119
+ return contentText;
120
+ }
121
+
122
+ if (typeof messageData.text === 'string' && messageData.text.trim()) {
123
+ return messageData.text.trim();
124
+ }
125
+
126
+ return '';
127
+ }
128
+
129
+ function normalizeTimestampMs(input) {
130
+ const value = Number(input);
131
+ if (!Number.isFinite(value) || value <= 0) {
132
+ return null;
133
+ }
134
+ return value > 1e12 ? value : value * 1000;
135
+ }
136
+
137
+ function toIsoTime(input) {
138
+ const ts = normalizeTimestampMs(input);
139
+ if (!ts) {
140
+ return null;
141
+ }
142
+ try {
143
+ return new Date(ts).toISOString();
144
+ } catch (err) {
145
+ return null;
146
+ }
147
+ }
148
+
149
+ function sqlQuote(value) {
150
+ if (value === null || value === undefined) {
151
+ return 'NULL';
152
+ }
153
+ if (typeof value === 'number') {
154
+ return Number.isFinite(value) ? String(Math.trunc(value)) : 'NULL';
155
+ }
156
+ if (typeof value === 'boolean') {
157
+ return value ? '1' : '0';
158
+ }
159
+ return `'${String(value).replace(/'/g, "''")}'`;
160
+ }
161
+
162
+ function runSqliteQuery(sql) {
163
+ if (!isOpenCodeInstalled()) {
164
+ return [];
165
+ }
166
+
167
+ try {
168
+ const output = execFileSync('sqlite3', ['-json', OPENCODE_DB_PATH, sql], {
169
+ encoding: 'utf8',
170
+ stdio: ['ignore', 'pipe', 'pipe'],
171
+ maxBuffer: 10 * 1024 * 1024
172
+ }).trim();
173
+
174
+ if (!output) {
175
+ return [];
176
+ }
177
+
178
+ const parsed = JSON.parse(output);
179
+ return Array.isArray(parsed) ? parsed : [];
180
+ } catch (err) {
181
+ console.error('[OpenCode Sessions] SQLite query failed:', err.message);
182
+ return [];
183
+ }
184
+ }
185
+
186
+ function runSqliteExec(sql) {
187
+ if (!isOpenCodeInstalled()) {
188
+ throw new Error('OpenCode CLI not installed');
189
+ }
190
+
191
+ execFileSync('sqlite3', [OPENCODE_DB_PATH, sql], {
192
+ encoding: 'utf8',
193
+ stdio: ['ignore', 'pipe', 'pipe'],
194
+ maxBuffer: 10 * 1024 * 1024
195
+ });
196
+ }
197
+
198
+ function buildContext(text, keyword, contextLength = 35) {
199
+ if (!text || !keyword) {
200
+ return null;
201
+ }
202
+
203
+ const parsedContextLength = Number(contextLength);
204
+ const safeContextLength = Number.isFinite(parsedContextLength) && parsedContextLength >= 0
205
+ ? parsedContextLength
206
+ : 35;
207
+
208
+ const lowerText = String(text).toLowerCase();
209
+ const lowerKeyword = String(keyword).toLowerCase();
210
+ const index = lowerText.indexOf(lowerKeyword);
211
+ if (index === -1) {
212
+ return null;
213
+ }
214
+
215
+ const start = Math.max(0, index - safeContextLength);
216
+ const end = Math.min(lowerText.length, index + lowerKeyword.length + safeContextLength);
217
+ let context = String(text).slice(start, end);
218
+ if (start > 0) context = `...${context}`;
219
+ if (end < String(text).length) context = `${context}...`;
220
+ return context;
221
+ }
222
+
223
+ // 检查 OpenCode 是否安装
224
+ function isOpenCodeInstalled() {
225
+ return fs.existsSync(OPENCODE_DB_PATH);
226
+ }
227
+
228
+ // 获取 OpenCode 数据目录
229
+ function getOpenCodeDataDir() {
230
+ return NATIVE_PATHS.opencode.data;
231
+ }
232
+
233
+ // 兼容导出:保留旧路径函数
234
+ function getSessionsDir() {
235
+ return path.join(getOpenCodeDataDir(), 'storage', 'session');
236
+ }
237
+
238
+ // 兼容导出:保留旧路径函数
239
+ function getProjectsDir() {
240
+ return path.join(getOpenCodeDataDir(), 'storage', 'project');
241
+ }
242
+
243
+ function getProjectOrder() {
244
+ const order = readJsonSafe(PROJECT_ORDER_FILE, []);
245
+ return Array.isArray(order) ? order : [];
246
+ }
247
+
248
+ function saveProjectOrder(order) {
249
+ if (!Array.isArray(order)) {
250
+ throw new Error('order must be an array');
251
+ }
252
+ writeJsonSafe(PROJECT_ORDER_FILE, order);
253
+ return { success: true };
254
+ }
255
+
256
+ function getSessionOrderMap() {
257
+ const map = readJsonSafe(SESSION_ORDER_FILE, {});
258
+ return map && typeof map === 'object' ? map : {};
259
+ }
260
+
261
+ function saveSessionOrderMap(map) {
262
+ writeJsonSafe(SESSION_ORDER_FILE, map);
263
+ }
264
+
265
+ function getSessionOrder(projectId) {
266
+ const map = getSessionOrderMap();
267
+ const order = map[projectId];
268
+ return Array.isArray(order) ? order : [];
269
+ }
270
+
271
+ function saveSessionOrder(projectId, order) {
272
+ if (!projectId) {
273
+ throw new Error('projectId is required');
274
+ }
275
+ if (!Array.isArray(order)) {
276
+ throw new Error('order must be an array');
277
+ }
278
+
279
+ const map = getSessionOrderMap();
280
+ map[projectId] = order;
281
+ saveSessionOrderMap(map);
282
+ return { success: true };
283
+ }
284
+
285
+ function removeSessionFromOrder(projectId, sessionId) {
286
+ const map = getSessionOrderMap();
287
+ if (!Array.isArray(map[projectId])) {
288
+ return;
289
+ }
290
+ map[projectId] = map[projectId].filter(id => id !== sessionId);
291
+ saveSessionOrderMap(map);
292
+ }
293
+
294
+ function removeProjectFromOrder(projectId) {
295
+ const currentOrder = getProjectOrder().filter(name => name !== projectId);
296
+ saveProjectOrder(currentOrder);
297
+
298
+ const map = getSessionOrderMap();
299
+ if (map[projectId] !== undefined) {
300
+ delete map[projectId];
301
+ saveSessionOrderMap(map);
302
+ }
303
+ }
304
+
305
+ function invalidateProjectAndSessionCountsCache() {
306
+ countsCache.expiresAt = 0;
307
+ }
308
+
309
+ function queryProjectAndSessionCounts() {
310
+ const rows = runSqliteQuery(`
311
+ SELECT
312
+ (SELECT COUNT(*) FROM project) AS project_count,
313
+ (SELECT COUNT(*) FROM session WHERE time_archived IS NULL) AS session_count
314
+ `);
315
+
316
+ const row = rows[0] || {};
317
+ return {
318
+ projectCount: Number(row.project_count) || 0,
319
+ sessionCount: Number(row.session_count) || 0
320
+ };
321
+ }
322
+
323
+ function getProjectRows() {
324
+ return runSqliteQuery(`
325
+ SELECT
326
+ p.id,
327
+ p.worktree,
328
+ p.name,
329
+ p.time_created,
330
+ p.time_updated,
331
+ COALESCE(s.session_count, 0) AS session_count
332
+ FROM project p
333
+ LEFT JOIN (
334
+ SELECT project_id, COUNT(*) AS session_count
335
+ FROM session
336
+ WHERE time_archived IS NULL
337
+ GROUP BY project_id
338
+ ) s ON s.project_id = p.id
339
+ `);
340
+ }
341
+
342
+ function getSessionRowsByProjectId(projectId) {
343
+ return runSqliteQuery(`
344
+ SELECT
345
+ s.id,
346
+ s.project_id,
347
+ s.parent_id,
348
+ s.slug,
349
+ s.directory,
350
+ s.title,
351
+ s.version,
352
+ s.share_url,
353
+ s.summary_additions,
354
+ s.summary_deletions,
355
+ s.summary_files,
356
+ s.summary_diffs,
357
+ s.revert,
358
+ s.permission,
359
+ s.time_created,
360
+ s.time_updated,
361
+ s.time_compacting,
362
+ s.time_archived
363
+ FROM session s
364
+ WHERE s.project_id = ${sqlQuote(projectId)}
365
+ AND s.time_archived IS NULL
366
+ ORDER BY s.time_updated DESC
367
+ `);
368
+ }
369
+
370
+ function getSessionRowById(sessionId) {
371
+ const rows = runSqliteQuery(`
372
+ SELECT
373
+ s.id,
374
+ s.project_id,
375
+ s.parent_id,
376
+ s.slug,
377
+ s.directory,
378
+ s.title,
379
+ s.version,
380
+ s.share_url,
381
+ s.summary_additions,
382
+ s.summary_deletions,
383
+ s.summary_files,
384
+ s.summary_diffs,
385
+ s.revert,
386
+ s.permission,
387
+ s.time_created,
388
+ s.time_updated,
389
+ s.time_compacting,
390
+ s.time_archived
391
+ FROM session s
392
+ WHERE s.id = ${sqlQuote(sessionId)}
393
+ LIMIT 1
394
+ `);
395
+
396
+ return rows[0] || null;
397
+ }
398
+
399
+ function getMessageRowsBySessionId(sessionId) {
400
+ return runSqliteQuery(`
401
+ SELECT
402
+ id,
403
+ session_id,
404
+ time_created,
405
+ time_updated,
406
+ data
407
+ FROM message
408
+ WHERE session_id = ${sqlQuote(sessionId)}
409
+ ORDER BY time_created ASC
410
+ `);
411
+ }
412
+
413
+ function getPartRowsBySessionId(sessionId) {
414
+ return runSqliteQuery(`
415
+ SELECT
416
+ id,
417
+ message_id,
418
+ session_id,
419
+ time_created,
420
+ time_updated,
421
+ data
422
+ FROM part
423
+ WHERE session_id = ${sqlQuote(sessionId)}
424
+ ORDER BY time_created ASC
425
+ `);
426
+ }
427
+
428
+ function normalizeSession(session, projectId = null) {
429
+ return {
430
+ sessionId: session.id,
431
+ projectName: projectId || session.project_id,
432
+ mtime: toIsoTime(session.time_updated) || new Date().toISOString(),
433
+ size: 0,
434
+ filePath: '',
435
+ gitBranch: null,
436
+ firstMessage: session.title || session.slug || null,
437
+ forkedFrom: null,
438
+ directory: session.directory,
439
+ slug: session.slug,
440
+ source: 'opencode'
441
+ };
442
+ }
443
+
444
+ // 获取所有项目
445
+ function getProjects() {
446
+ const projects = getProjectRows().map((project) => ({
447
+ name: project.id,
448
+ displayName: project.name || project.id,
449
+ fullPath: project.worktree || '/',
450
+ path: project.worktree || '/',
451
+ sessionCount: Number(project.session_count) || 0,
452
+ lastUsed: Number(project.time_updated) || Number(project.time_created) || 0,
453
+ source: 'opencode'
454
+ }));
455
+
456
+ return sortByOrder(
457
+ projects,
458
+ getProjectOrder(),
459
+ (a, b) => (b.lastUsed || 0) - (a.lastUsed || 0)
460
+ );
461
+ }
462
+
463
+ // 根据项目ID获取会话列表
464
+ function getSessionsByProjectId(projectId) {
465
+ const sessions = getSessionRowsByProjectId(projectId).map(session => normalizeSession(session, projectId));
466
+ const order = getSessionOrder(projectId);
467
+
468
+ const fallbackSorted = sessions.sort(
469
+ (a, b) => new Date(b.mtime).getTime() - new Date(a.mtime).getTime()
470
+ );
471
+
472
+ if (order.length === 0) {
473
+ return fallbackSorted;
474
+ }
475
+
476
+ const orderMap = new Map(order.map((id, idx) => [id, idx]));
477
+ return [...fallbackSorted].sort((a, b) => {
478
+ const aIndex = orderMap.has(a.sessionId) ? orderMap.get(a.sessionId) : Number.MAX_SAFE_INTEGER;
479
+ const bIndex = orderMap.has(b.sessionId) ? orderMap.get(b.sessionId) : Number.MAX_SAFE_INTEGER;
480
+ if (aIndex === bIndex) {
481
+ return new Date(b.mtime).getTime() - new Date(a.mtime).getTime();
482
+ }
483
+ return aIndex - bIndex;
484
+ });
485
+ }
486
+
487
+ // 根据项目名获取会话列表
488
+ function getSessionsByProject(projectName) {
489
+ return getSessionsByProjectId(projectName);
490
+ }
491
+
492
+ function getSessionLocation(sessionId) {
493
+ const session = getSessionRowById(sessionId);
494
+ if (!session) {
495
+ return null;
496
+ }
497
+ return {
498
+ projectId: session.project_id,
499
+ sessionData: session
500
+ };
501
+ }
502
+
503
+ // 根据会话ID获取会话详情
504
+ function getSessionById(sessionId) {
505
+ const location = getSessionLocation(sessionId);
506
+ if (!location) {
507
+ return null;
508
+ }
509
+
510
+ return normalizeSession(location.sessionData, location.projectId);
511
+ }
512
+
513
+ function buildSessionMessages(sessionId) {
514
+ const messages = getMessageRowsBySessionId(sessionId);
515
+ const parts = getPartRowsBySessionId(sessionId);
516
+
517
+ const partsByMessageId = new Map();
518
+ for (const part of parts) {
519
+ if (!partsByMessageId.has(part.message_id)) {
520
+ partsByMessageId.set(part.message_id, []);
521
+ }
522
+ partsByMessageId.get(part.message_id).push(part);
523
+ }
524
+
525
+ const converted = [];
526
+
527
+ for (const row of messages) {
528
+ const messageData = parseJsonMaybe(row.data, {});
529
+ const role = messageData?.role;
530
+ if (role !== 'user' && role !== 'assistant') {
531
+ continue;
532
+ }
533
+
534
+ const messageParts = partsByMessageId.get(row.id) || [];
535
+ const partTexts = [];
536
+ for (const part of messageParts) {
537
+ const partData = parseJsonMaybe(part.data, null);
538
+ const text = extractTextFromPartData(partData);
539
+ if (text) {
540
+ partTexts.push(text);
541
+ }
542
+ }
543
+
544
+ const fallbackText = extractTextFromMessageData(messageData);
545
+ const content = partTexts.join('\n').trim() || fallbackText || '[空消息]';
546
+
547
+ const timestamp = toIsoTime(
548
+ messageData?.time?.created || row.time_created || row.time_updated
549
+ );
550
+
551
+ converted.push({
552
+ type: role,
553
+ role,
554
+ content,
555
+ timestamp,
556
+ model: role === 'assistant'
557
+ ? (messageData?.model?.modelID || messageData?.modelID || messageData?.model || 'opencode')
558
+ : null
559
+ });
560
+ }
561
+
562
+ return converted;
563
+ }
564
+
565
+ function getSessionMessages(sessionId) {
566
+ const location = getSessionLocation(sessionId);
567
+ if (!location) {
568
+ throw new Error('Session not found');
569
+ }
570
+
571
+ return buildSessionMessages(sessionId).map(({ type, content, timestamp, model }) => ({
572
+ type,
573
+ content,
574
+ timestamp,
575
+ model
576
+ }));
577
+ }
578
+
579
+ // 获取项目和会话数量统计
580
+ function getProjectAndSessionCounts() {
581
+ const now = Date.now();
582
+ if (countsCache.expiresAt > now) {
583
+ return countsCache.value;
584
+ }
585
+
586
+ try {
587
+ const counts = queryProjectAndSessionCounts();
588
+ countsCache = {
589
+ value: counts,
590
+ expiresAt: now + COUNTS_CACHE_TTL_MS
591
+ };
592
+ return counts;
593
+ } catch (err) {
594
+ console.error('[OpenCode Sessions] Failed to get counts:', err);
595
+ return countsCache.value || EMPTY_COUNTS;
596
+ }
597
+ }
598
+
599
+ function getRecentSessions(limit = 5) {
600
+ const projects = getProjects();
601
+ const { loadAliases } = require('./alias');
602
+ const { getForkRelations } = require('./sessions');
603
+ const aliases = loadAliases();
604
+ const forkRelations = getForkRelations();
605
+ const allSessions = [];
606
+
607
+ for (const project of projects) {
608
+ const sessions = getSessionsByProjectId(project.name);
609
+ sessions.forEach(session => {
610
+ allSessions.push({
611
+ ...session,
612
+ alias: aliases[session.sessionId] || null,
613
+ forkedFrom: forkRelations[session.sessionId] || null,
614
+ projectName: project.name,
615
+ projectDisplayName: project.displayName,
616
+ projectFullPath: project.fullPath
617
+ });
618
+ });
619
+ }
620
+
621
+ return allSessions
622
+ .sort((a, b) => new Date(b.mtime).getTime() - new Date(a.mtime).getTime())
623
+ .slice(0, limit);
624
+ }
625
+
626
+ // 删除会话
627
+ function deleteSession(sessionId) {
628
+ const location = getSessionLocation(sessionId);
629
+ if (!location) {
630
+ throw new Error('Session not found');
631
+ }
632
+
633
+ runSqliteExec(`
634
+ PRAGMA foreign_keys = ON;
635
+ DELETE FROM session WHERE id = ${sqlQuote(sessionId)};
636
+ `);
637
+
638
+ try {
639
+ const { deleteAlias } = require('./alias');
640
+ deleteAlias(sessionId);
641
+ } catch (err) {
642
+ // ignore alias cleanup errors
643
+ }
644
+
645
+ removeSessionFromOrder(location.projectId, sessionId);
646
+
647
+ try {
648
+ const { getForkRelations, saveForkRelations } = require('./sessions');
649
+ const relations = getForkRelations();
650
+ delete relations[sessionId];
651
+ Object.keys(relations).forEach((key) => {
652
+ if (relations[key] === sessionId) {
653
+ delete relations[key];
654
+ }
655
+ });
656
+ saveForkRelations(relations);
657
+ } catch (err) {
658
+ // ignore fork relation cleanup errors
659
+ }
660
+
661
+ invalidateProjectAndSessionCountsCache();
662
+ return { success: true, projectName: location.projectId, sessionId };
663
+ }
664
+
665
+ function forkSession(sessionId) {
666
+ const location = getSessionLocation(sessionId);
667
+ if (!location) {
668
+ throw new Error('Session not found');
669
+ }
670
+
671
+ const source = location.sessionData;
672
+ const messages = getMessageRowsBySessionId(sessionId);
673
+ const parts = getPartRowsBySessionId(sessionId);
674
+ const now = Date.now();
675
+ const newSessionId = `ses_${crypto.randomUUID().replace(/-/g, '')}`;
676
+
677
+ const messageIdMap = new Map();
678
+ for (const message of messages) {
679
+ messageIdMap.set(message.id, `msg_${crypto.randomUUID().replace(/-/g, '')}`);
680
+ }
681
+
682
+ const statements = [];
683
+ statements.push('PRAGMA foreign_keys = ON;');
684
+ statements.push('BEGIN IMMEDIATE;');
685
+
686
+ statements.push(`
687
+ INSERT INTO session (
688
+ id, project_id, parent_id, slug, directory, title, version, share_url,
689
+ summary_additions, summary_deletions, summary_files, summary_diffs,
690
+ revert, permission, time_created, time_updated, time_compacting, time_archived
691
+ ) VALUES (
692
+ ${sqlQuote(newSessionId)},
693
+ ${sqlQuote(source.project_id)},
694
+ ${sqlQuote(source.parent_id)},
695
+ ${sqlQuote(source.slug)},
696
+ ${sqlQuote(source.directory)},
697
+ ${sqlQuote(source.title)},
698
+ ${sqlQuote(source.version)},
699
+ ${sqlQuote(source.share_url)},
700
+ ${sqlQuote(source.summary_additions)},
701
+ ${sqlQuote(source.summary_deletions)},
702
+ ${sqlQuote(source.summary_files)},
703
+ ${sqlQuote(source.summary_diffs)},
704
+ ${sqlQuote(source.revert)},
705
+ ${sqlQuote(source.permission)},
706
+ ${sqlQuote(now)},
707
+ ${sqlQuote(now)},
708
+ ${sqlQuote(source.time_compacting)},
709
+ NULL
710
+ );
711
+ `);
712
+
713
+ for (const message of messages) {
714
+ const newMessageId = messageIdMap.get(message.id);
715
+ const messageData = parseJsonMaybe(message.data, null);
716
+
717
+ let serializedData = message.data;
718
+ if (messageData && typeof messageData === 'object') {
719
+ if (typeof messageData.parentID === 'string' && messageIdMap.has(messageData.parentID)) {
720
+ messageData.parentID = messageIdMap.get(messageData.parentID);
721
+ }
722
+ if (typeof messageData.id === 'string') {
723
+ messageData.id = newMessageId;
724
+ }
725
+ serializedData = JSON.stringify(messageData);
726
+ }
727
+
728
+ statements.push(`
729
+ INSERT INTO message (id, session_id, time_created, time_updated, data)
730
+ VALUES (
731
+ ${sqlQuote(newMessageId)},
732
+ ${sqlQuote(newSessionId)},
733
+ ${sqlQuote(message.time_created)},
734
+ ${sqlQuote(message.time_updated)},
735
+ ${sqlQuote(serializedData)}
736
+ );
737
+ `);
738
+ }
739
+
740
+ for (const part of parts) {
741
+ const newPartId = `prt_${crypto.randomUUID().replace(/-/g, '')}`;
742
+ const targetMessageId = messageIdMap.get(part.message_id);
743
+ if (!targetMessageId) {
744
+ continue;
745
+ }
746
+
747
+ statements.push(`
748
+ INSERT INTO part (id, message_id, session_id, time_created, time_updated, data)
749
+ VALUES (
750
+ ${sqlQuote(newPartId)},
751
+ ${sqlQuote(targetMessageId)},
752
+ ${sqlQuote(newSessionId)},
753
+ ${sqlQuote(part.time_created)},
754
+ ${sqlQuote(part.time_updated)},
755
+ ${sqlQuote(part.data)}
756
+ );
757
+ `);
758
+ }
759
+
760
+ statements.push('COMMIT;');
761
+
762
+ runSqliteExec(statements.join('\n'));
763
+
764
+ try {
765
+ const { getForkRelations, saveForkRelations } = require('./sessions');
766
+ const relations = getForkRelations();
767
+ relations[newSessionId] = sessionId;
768
+ saveForkRelations(relations);
769
+ } catch (err) {
770
+ // ignore fork relation save errors
771
+ }
772
+
773
+ const existingOrder = getSessionOrder(location.projectId);
774
+ saveSessionOrder(location.projectId, [newSessionId, ...existingOrder.filter(id => id !== newSessionId)]);
775
+
776
+ invalidateProjectAndSessionCountsCache();
777
+ return {
778
+ success: true,
779
+ newSessionId,
780
+ forkedFrom: sessionId,
781
+ projectName: location.projectId,
782
+ newFilePath: null
783
+ };
784
+ }
785
+
786
+ function deleteProject(projectId) {
787
+ const projectRows = runSqliteQuery(`
788
+ SELECT id FROM project WHERE id = ${sqlQuote(projectId)} LIMIT 1
789
+ `);
790
+
791
+ if (projectRows.length === 0) {
792
+ throw new Error('Project not found');
793
+ }
794
+
795
+ const sessionRows = runSqliteQuery(`
796
+ SELECT id FROM session WHERE project_id = ${sqlQuote(projectId)}
797
+ `);
798
+
799
+ const deletedSessionIds = sessionRows.map(row => row.id);
800
+
801
+ for (const sessionId of deletedSessionIds) {
802
+ try {
803
+ const { deleteAlias } = require('./alias');
804
+ deleteAlias(sessionId);
805
+ } catch (err) {
806
+ // ignore alias cleanup errors
807
+ }
808
+ }
809
+
810
+ runSqliteExec(`
811
+ PRAGMA foreign_keys = ON;
812
+ DELETE FROM project WHERE id = ${sqlQuote(projectId)};
813
+ `);
814
+
815
+ removeProjectFromOrder(projectId);
816
+
817
+ try {
818
+ const { getForkRelations, saveForkRelations } = require('./sessions');
819
+ const deletedSet = new Set(deletedSessionIds);
820
+ const relations = getForkRelations();
821
+ Object.keys(relations).forEach((key) => {
822
+ if (deletedSet.has(key) || deletedSet.has(relations[key])) {
823
+ delete relations[key];
824
+ }
825
+ });
826
+ saveForkRelations(relations);
827
+ } catch (err) {
828
+ // ignore relation cleanup errors
829
+ }
830
+
831
+ invalidateProjectAndSessionCountsCache();
832
+ return {
833
+ success: true,
834
+ projectName: projectId,
835
+ deletedCount: deletedSessionIds.length
836
+ };
837
+ }
838
+
839
+ // 搜索会话
840
+ function searchSessions(keyword, contextLength = 35, projectFilter = null) {
841
+ if (!keyword || !String(keyword).trim()) {
842
+ return [];
843
+ }
844
+
845
+ const searchKeyword = String(keyword).trim();
846
+ const projects = getProjects();
847
+ const { loadAliases } = require('./alias');
848
+ const aliases = loadAliases();
849
+ const results = [];
850
+
851
+ for (const project of projects) {
852
+ if (projectFilter && project.name !== projectFilter) {
853
+ continue;
854
+ }
855
+
856
+ const sessions = getSessionsByProjectId(project.name);
857
+ for (const session of sessions) {
858
+ const matches = [];
859
+
860
+ const quickChecks = [
861
+ session.sessionId,
862
+ session.firstMessage,
863
+ session.slug,
864
+ session.directory
865
+ ];
866
+
867
+ for (const text of quickChecks) {
868
+ const context = buildContext(text, searchKeyword, contextLength);
869
+ if (context) {
870
+ matches.push({
871
+ role: 'assistant',
872
+ context,
873
+ timestamp: session.mtime
874
+ });
875
+ }
876
+ }
877
+
878
+ const sessionMessages = buildSessionMessages(session.sessionId);
879
+ for (const message of sessionMessages) {
880
+ const context = buildContext(message.content, searchKeyword, contextLength);
881
+ if (!context) {
882
+ continue;
883
+ }
884
+
885
+ matches.push({
886
+ role: message.role,
887
+ context,
888
+ timestamp: message.timestamp
889
+ });
890
+ }
891
+
892
+ if (matches.length > 0) {
893
+ results.push({
894
+ sessionId: session.sessionId,
895
+ projectName: project.name,
896
+ projectDisplayName: project.displayName,
897
+ projectFullPath: project.fullPath,
898
+ alias: aliases[session.sessionId] || null,
899
+ matchCount: matches.length,
900
+ matches: matches.slice(0, 5),
901
+ source: 'opencode'
902
+ });
903
+ }
904
+ }
905
+ }
906
+
907
+ return results.sort((a, b) => b.matchCount - a.matchCount);
908
+ }
909
+
910
+ module.exports = {
911
+ isOpenCodeInstalled,
912
+ getOpenCodeDataDir,
913
+ getSessionsDir,
914
+ getProjectsDir,
915
+ getProjects,
916
+ getProjectOrder,
917
+ saveProjectOrder,
918
+ getSessionsByProject,
919
+ getSessionsByProjectId,
920
+ getSessionById,
921
+ getSessionMessages,
922
+ getRecentSessions,
923
+ normalizeSession,
924
+ getProjectAndSessionCounts,
925
+ deleteSession,
926
+ deleteProject,
927
+ forkSession,
928
+ getSessionOrder,
929
+ saveSessionOrder,
930
+ searchSessions
931
+ };