@adversity/coding-tool-x 3.1.1 → 3.1.3
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/CHANGELOG.md +41 -0
- package/dist/web/assets/Analytics-BIqc8Rin.css +1 -0
- package/dist/web/assets/Analytics-D2V09DHH.js +39 -0
- package/dist/web/assets/{ConfigTemplates-ZrK_s7ma.js → ConfigTemplates-Bf_11LhH.js} +1 -1
- package/dist/web/assets/Home-BRnW4FTS.js +1 -0
- package/dist/web/assets/Home-CyCIx4BA.css +1 -0
- package/dist/web/assets/{PluginManager-BD7QUZbU.js → PluginManager-B9J32GhW.js} +1 -1
- package/dist/web/assets/{ProjectList-DRb1DuHV.js → ProjectList-5a19MWJk.js} +1 -1
- package/dist/web/assets/SessionList-CXUr6S7w.css +1 -0
- package/dist/web/assets/SessionList-Cxg5bAdT.js +1 -0
- package/dist/web/assets/{SkillManager-C1xG5B4Q.js → SkillManager-CVBr0CLi.js} +1 -1
- package/dist/web/assets/{Terminal-DksBo_lM.js → Terminal-D2Xe_Q0H.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Burx7XOo.js → WorkspaceManager-C7dwV94C.js} +1 -1
- package/dist/web/assets/icons-BxcwoY5F.js +1 -0
- package/dist/web/assets/index-BS9RA6SN.js +2 -0
- package/dist/web/assets/index-DUNAVDGb.css +1 -0
- package/dist/web/assets/naive-ui-BIXcURHZ.js +1 -0
- package/dist/web/assets/{vendors-CO3Upi1d.js → vendors-i5CBGnlm.js} +1 -1
- package/dist/web/assets/{vue-vendor-DqyWIXEb.js → vue-vendor-PKd8utv_.js} +1 -1
- package/dist/web/index.html +6 -6
- package/package.json +1 -1
- package/src/config/default.js +7 -27
- package/src/config/loader.js +6 -3
- package/src/config/model-metadata.js +167 -0
- package/src/config/model-metadata.json +125 -0
- package/src/config/model-pricing.js +23 -93
- package/src/server/api/channels.js +16 -39
- package/src/server/api/codex-channels.js +15 -43
- package/src/server/api/commands.js +0 -77
- package/src/server/api/config.js +4 -1
- package/src/server/api/gemini-channels.js +16 -40
- package/src/server/api/opencode-channels.js +108 -56
- package/src/server/api/opencode-proxy.js +42 -33
- package/src/server/api/opencode-sessions.js +4 -69
- package/src/server/api/sessions.js +11 -68
- package/src/server/api/settings.js +138 -0
- package/src/server/api/skills.js +0 -44
- package/src/server/api/statistics.js +115 -1
- package/src/server/codex-proxy-server.js +32 -59
- package/src/server/gemini-proxy-server.js +21 -18
- package/src/server/index.js +13 -7
- package/src/server/opencode-proxy-server.js +1232 -197
- package/src/server/proxy-server.js +8 -8
- package/src/server/services/codex-sessions.js +105 -6
- package/src/server/services/commands-service.js +0 -29
- package/src/server/services/config-templates-service.js +38 -28
- package/src/server/services/env-checker.js +97 -9
- package/src/server/services/env-manager.js +29 -1
- package/src/server/services/opencode-channels.js +3 -1
- package/src/server/services/opencode-sessions.js +486 -218
- package/src/server/services/opencode-settings-manager.js +172 -36
- package/src/server/services/plugins-service.js +37 -28
- package/src/server/services/pty-manager.js +22 -18
- package/src/server/services/response-decoder.js +21 -0
- package/src/server/services/skill-service.js +1 -49
- package/src/server/services/speed-test.js +40 -3
- package/src/server/services/statistics-service.js +238 -1
- package/src/server/utils/pricing.js +51 -60
- package/src/server/websocket-server.js +24 -5
- package/dist/web/assets/Home-B8YfhZ3c.js +0 -1
- package/dist/web/assets/Home-Di2qsylF.css +0 -1
- package/dist/web/assets/SessionList-BGJWyneI.css +0 -1
- package/dist/web/assets/SessionList-lZ0LKzfT.js +0 -1
- package/dist/web/assets/icons-kcfLIMBB.js +0 -1
- package/dist/web/assets/index-Ufv5rCa5.css +0 -1
- package/dist/web/assets/index-lAkrRC3h.js +0 -2
- package/dist/web/assets/naive-ui-CSrLusZZ.js +0 -1
- package/src/server/api/convert.js +0 -260
- package/src/server/services/session-converter.js +0 -577
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const crypto = require('crypto');
|
|
4
|
+
const { execFileSync } = require('child_process');
|
|
4
5
|
const { NATIVE_PATHS, PATHS } = require('../../config/paths');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* OpenCode 会话服务
|
|
8
|
-
* 读取 OpenCode
|
|
9
|
+
* 读取 OpenCode SQLite 会话数据
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
const PROJECT_ORDER_FILE = path.join(PATHS.base, 'opencode-project-order.json');
|
|
12
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
|
+
};
|
|
13
22
|
|
|
14
23
|
function ensureParentDir(filePath) {
|
|
15
24
|
const dir = path.dirname(filePath);
|
|
@@ -34,27 +43,6 @@ function writeJsonSafe(filePath, data) {
|
|
|
34
43
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
35
44
|
}
|
|
36
45
|
|
|
37
|
-
function copyDirectoryRecursive(sourceDir, targetDir) {
|
|
38
|
-
if (!fs.existsSync(sourceDir)) {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (!fs.existsSync(targetDir)) {
|
|
43
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
|
47
|
-
for (const entry of entries) {
|
|
48
|
-
const sourcePath = path.join(sourceDir, entry.name);
|
|
49
|
-
const targetPath = path.join(targetDir, entry.name);
|
|
50
|
-
if (entry.isDirectory()) {
|
|
51
|
-
copyDirectoryRecursive(sourcePath, targetPath);
|
|
52
|
-
} else {
|
|
53
|
-
fs.copyFileSync(sourcePath, targetPath);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
46
|
function sortByOrder(items, order, fallbackCompare) {
|
|
59
47
|
const fallbackSorted = [...items].sort(fallbackCompare);
|
|
60
48
|
if (!Array.isArray(order) || order.length === 0) {
|
|
@@ -72,6 +60,17 @@ function sortByOrder(items, order, fallbackCompare) {
|
|
|
72
60
|
});
|
|
73
61
|
}
|
|
74
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
|
+
|
|
75
74
|
function extractTextContent(content) {
|
|
76
75
|
if (typeof content === 'string') {
|
|
77
76
|
return content;
|
|
@@ -86,6 +85,116 @@ function extractTextContent(content) {
|
|
|
86
85
|
return '';
|
|
87
86
|
}
|
|
88
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
|
+
|
|
89
198
|
function buildContext(text, keyword, contextLength = 35) {
|
|
90
199
|
if (!text || !keyword) {
|
|
91
200
|
return null;
|
|
@@ -96,24 +205,24 @@ function buildContext(text, keyword, contextLength = 35) {
|
|
|
96
205
|
? parsedContextLength
|
|
97
206
|
: 35;
|
|
98
207
|
|
|
99
|
-
const lowerText = text.toLowerCase();
|
|
100
|
-
const lowerKeyword = keyword.toLowerCase();
|
|
208
|
+
const lowerText = String(text).toLowerCase();
|
|
209
|
+
const lowerKeyword = String(keyword).toLowerCase();
|
|
101
210
|
const index = lowerText.indexOf(lowerKeyword);
|
|
102
211
|
if (index === -1) {
|
|
103
212
|
return null;
|
|
104
213
|
}
|
|
105
214
|
|
|
106
215
|
const start = Math.max(0, index - safeContextLength);
|
|
107
|
-
const end = Math.min(
|
|
108
|
-
let context = text.slice(start, end);
|
|
216
|
+
const end = Math.min(lowerText.length, index + lowerKeyword.length + safeContextLength);
|
|
217
|
+
let context = String(text).slice(start, end);
|
|
109
218
|
if (start > 0) context = `...${context}`;
|
|
110
|
-
if (end < text.length) context = `${context}...`;
|
|
219
|
+
if (end < String(text).length) context = `${context}...`;
|
|
111
220
|
return context;
|
|
112
221
|
}
|
|
113
222
|
|
|
114
223
|
// 检查 OpenCode 是否安装
|
|
115
224
|
function isOpenCodeInstalled() {
|
|
116
|
-
return fs.existsSync(
|
|
225
|
+
return fs.existsSync(OPENCODE_DB_PATH);
|
|
117
226
|
}
|
|
118
227
|
|
|
119
228
|
// 获取 OpenCode 数据目录
|
|
@@ -121,25 +230,16 @@ function getOpenCodeDataDir() {
|
|
|
121
230
|
return NATIVE_PATHS.opencode.data;
|
|
122
231
|
}
|
|
123
232
|
|
|
124
|
-
//
|
|
233
|
+
// 兼容导出:保留旧路径函数
|
|
125
234
|
function getSessionsDir() {
|
|
126
235
|
return path.join(getOpenCodeDataDir(), 'storage', 'session');
|
|
127
236
|
}
|
|
128
237
|
|
|
129
|
-
//
|
|
238
|
+
// 兼容导出:保留旧路径函数
|
|
130
239
|
function getProjectsDir() {
|
|
131
240
|
return path.join(getOpenCodeDataDir(), 'storage', 'project');
|
|
132
241
|
}
|
|
133
242
|
|
|
134
|
-
// 获取消息存储目录
|
|
135
|
-
function getMessagesRootDir() {
|
|
136
|
-
return NATIVE_PATHS.opencode.messages;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function getMessageDir(sessionId) {
|
|
140
|
-
return path.join(getMessagesRootDir(), sessionId);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
243
|
function getProjectOrder() {
|
|
144
244
|
const order = readJsonSafe(PROJECT_ORDER_FILE, []);
|
|
145
245
|
return Array.isArray(order) ? order : [];
|
|
@@ -202,43 +302,156 @@ function removeProjectFromOrder(projectId) {
|
|
|
202
302
|
}
|
|
203
303
|
}
|
|
204
304
|
|
|
205
|
-
function
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
return [];
|
|
209
|
-
}
|
|
305
|
+
function invalidateProjectAndSessionCountsCache() {
|
|
306
|
+
countsCache.expiresAt = 0;
|
|
307
|
+
}
|
|
210
308
|
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
+
};
|
|
222
442
|
}
|
|
223
443
|
|
|
224
444
|
// 获取所有项目
|
|
225
445
|
function getProjects() {
|
|
226
|
-
const projects =
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
fullPath: project.worktree || '/',
|
|
236
|
-
path: project.worktree || '/',
|
|
237
|
-
sessionCount: projectSessions.length,
|
|
238
|
-
lastUsed: project.time?.updated || project.time?.created || 0,
|
|
239
|
-
source: 'opencode'
|
|
240
|
-
});
|
|
241
|
-
}
|
|
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
|
+
}));
|
|
242
455
|
|
|
243
456
|
return sortByOrder(
|
|
244
457
|
projects,
|
|
@@ -249,30 +462,13 @@ function getProjects() {
|
|
|
249
462
|
|
|
250
463
|
// 根据项目ID获取会话列表
|
|
251
464
|
function getSessionsByProjectId(projectId) {
|
|
252
|
-
const
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
if (!fs.existsSync(sessionsDir)) {
|
|
256
|
-
return sessions;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const files = fs.readdirSync(sessionsDir).filter(file => file.endsWith('.json'));
|
|
260
|
-
for (const file of files) {
|
|
261
|
-
const filePath = path.join(sessionsDir, file);
|
|
262
|
-
try {
|
|
263
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
264
|
-
const session = JSON.parse(content);
|
|
265
|
-
sessions.push(normalizeSession(session, filePath, projectId));
|
|
266
|
-
} catch (err) {
|
|
267
|
-
console.error(`[OpenCode Sessions] Failed to parse session file ${file}:`, err);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
465
|
+
const sessions = getSessionRowsByProjectId(projectId).map(session => normalizeSession(session, projectId));
|
|
466
|
+
const order = getSessionOrder(projectId);
|
|
270
467
|
|
|
271
468
|
const fallbackSorted = sessions.sort(
|
|
272
469
|
(a, b) => new Date(b.mtime).getTime() - new Date(a.mtime).getTime()
|
|
273
470
|
);
|
|
274
471
|
|
|
275
|
-
const order = getSessionOrder(projectId);
|
|
276
472
|
if (order.length === 0) {
|
|
277
473
|
return fallbackSorted;
|
|
278
474
|
}
|
|
@@ -288,101 +484,115 @@ function getSessionsByProjectId(projectId) {
|
|
|
288
484
|
});
|
|
289
485
|
}
|
|
290
486
|
|
|
291
|
-
//
|
|
292
|
-
function
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
: new Date().toISOString();
|
|
487
|
+
// 根据项目名获取会话列表
|
|
488
|
+
function getSessionsByProject(projectName) {
|
|
489
|
+
return getSessionsByProjectId(projectName);
|
|
490
|
+
}
|
|
296
491
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
size = stats.size;
|
|
302
|
-
}
|
|
303
|
-
} catch (err) {
|
|
304
|
-
// 忽略错误
|
|
492
|
+
function getSessionLocation(sessionId) {
|
|
493
|
+
const session = getSessionRowById(sessionId);
|
|
494
|
+
if (!session) {
|
|
495
|
+
return null;
|
|
305
496
|
}
|
|
306
|
-
|
|
307
497
|
return {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
mtime,
|
|
311
|
-
size,
|
|
312
|
-
filePath: filePath || '',
|
|
313
|
-
gitBranch: null,
|
|
314
|
-
firstMessage: session.title || session.slug || null,
|
|
315
|
-
forkedFrom: null,
|
|
316
|
-
directory: session.directory,
|
|
317
|
-
slug: session.slug,
|
|
318
|
-
source: 'opencode'
|
|
498
|
+
projectId: session.project_id,
|
|
499
|
+
sessionData: session
|
|
319
500
|
};
|
|
320
501
|
}
|
|
321
502
|
|
|
322
|
-
//
|
|
323
|
-
function
|
|
324
|
-
|
|
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);
|
|
325
511
|
}
|
|
326
512
|
|
|
327
|
-
function
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
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);
|
|
331
523
|
}
|
|
332
524
|
|
|
333
|
-
const
|
|
334
|
-
for (const projectDir of projectDirs) {
|
|
335
|
-
if (!projectDir.isDirectory()) continue;
|
|
525
|
+
const converted = [];
|
|
336
526
|
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
return { projectId: projectDir.name, sessionPath: directPath, sessionData };
|
|
343
|
-
}
|
|
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;
|
|
344
532
|
}
|
|
345
533
|
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
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);
|
|
352
541
|
}
|
|
353
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
|
+
});
|
|
354
560
|
}
|
|
355
561
|
|
|
356
|
-
return
|
|
562
|
+
return converted;
|
|
357
563
|
}
|
|
358
564
|
|
|
359
|
-
|
|
360
|
-
function getSessionById(sessionId) {
|
|
565
|
+
function getSessionMessages(sessionId) {
|
|
361
566
|
const location = getSessionLocation(sessionId);
|
|
362
567
|
if (!location) {
|
|
363
|
-
|
|
568
|
+
throw new Error('Session not found');
|
|
364
569
|
}
|
|
365
570
|
|
|
366
|
-
return
|
|
571
|
+
return buildSessionMessages(sessionId).map(({ type, content, timestamp, model }) => ({
|
|
572
|
+
type,
|
|
573
|
+
content,
|
|
574
|
+
timestamp,
|
|
575
|
+
model
|
|
576
|
+
}));
|
|
367
577
|
}
|
|
368
578
|
|
|
369
579
|
// 获取项目和会话数量统计
|
|
370
580
|
function getProjectAndSessionCounts() {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
for (const project of projects) {
|
|
376
|
-
sessionCount += project.sessionCount || 0;
|
|
377
|
-
}
|
|
581
|
+
const now = Date.now();
|
|
582
|
+
if (countsCache.expiresAt > now) {
|
|
583
|
+
return countsCache.value;
|
|
584
|
+
}
|
|
378
585
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
586
|
+
try {
|
|
587
|
+
const counts = queryProjectAndSessionCounts();
|
|
588
|
+
countsCache = {
|
|
589
|
+
value: counts,
|
|
590
|
+
expiresAt: now + COUNTS_CACHE_TTL_MS
|
|
382
591
|
};
|
|
592
|
+
return counts;
|
|
383
593
|
} catch (err) {
|
|
384
594
|
console.error('[OpenCode Sessions] Failed to get counts:', err);
|
|
385
|
-
return
|
|
595
|
+
return countsCache.value || EMPTY_COUNTS;
|
|
386
596
|
}
|
|
387
597
|
}
|
|
388
598
|
|
|
@@ -420,12 +630,10 @@ function deleteSession(sessionId) {
|
|
|
420
630
|
throw new Error('Session not found');
|
|
421
631
|
}
|
|
422
632
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
fs.rmSync(messageDir, { recursive: true, force: true });
|
|
428
|
-
}
|
|
633
|
+
runSqliteExec(`
|
|
634
|
+
PRAGMA foreign_keys = ON;
|
|
635
|
+
DELETE FROM session WHERE id = ${sqlQuote(sessionId)};
|
|
636
|
+
`);
|
|
429
637
|
|
|
430
638
|
try {
|
|
431
639
|
const { deleteAlias } = require('./alias');
|
|
@@ -450,6 +658,7 @@ function deleteSession(sessionId) {
|
|
|
450
658
|
// ignore fork relation cleanup errors
|
|
451
659
|
}
|
|
452
660
|
|
|
661
|
+
invalidateProjectAndSessionCountsCache();
|
|
453
662
|
return { success: true, projectName: location.projectId, sessionId };
|
|
454
663
|
}
|
|
455
664
|
|
|
@@ -459,28 +668,99 @@ function forkSession(sessionId) {
|
|
|
459
668
|
throw new Error('Session not found');
|
|
460
669
|
}
|
|
461
670
|
|
|
462
|
-
const now = new Date().toISOString();
|
|
463
|
-
const newSessionId = crypto.randomUUID();
|
|
464
671
|
const source = location.sessionData;
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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);
|
|
472
726
|
}
|
|
473
|
-
};
|
|
474
727
|
|
|
475
|
-
|
|
476
|
-
|
|
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
|
+
}
|
|
477
746
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
+
`);
|
|
482
758
|
}
|
|
483
759
|
|
|
760
|
+
statements.push('COMMIT;');
|
|
761
|
+
|
|
762
|
+
runSqliteExec(statements.join('\n'));
|
|
763
|
+
|
|
484
764
|
try {
|
|
485
765
|
const { getForkRelations, saveForkRelations } = require('./sessions');
|
|
486
766
|
const relations = getForkRelations();
|
|
@@ -493,35 +773,32 @@ function forkSession(sessionId) {
|
|
|
493
773
|
const existingOrder = getSessionOrder(location.projectId);
|
|
494
774
|
saveSessionOrder(location.projectId, [newSessionId, ...existingOrder.filter(id => id !== newSessionId)]);
|
|
495
775
|
|
|
776
|
+
invalidateProjectAndSessionCountsCache();
|
|
496
777
|
return {
|
|
497
778
|
success: true,
|
|
498
779
|
newSessionId,
|
|
499
780
|
forkedFrom: sessionId,
|
|
500
781
|
projectName: location.projectId,
|
|
501
|
-
newFilePath:
|
|
782
|
+
newFilePath: null
|
|
502
783
|
};
|
|
503
784
|
}
|
|
504
785
|
|
|
505
786
|
function deleteProject(projectId) {
|
|
506
|
-
const
|
|
507
|
-
|
|
787
|
+
const projectRows = runSqliteQuery(`
|
|
788
|
+
SELECT id FROM project WHERE id = ${sqlQuote(projectId)} LIMIT 1
|
|
789
|
+
`);
|
|
790
|
+
|
|
791
|
+
if (projectRows.length === 0) {
|
|
508
792
|
throw new Error('Project not found');
|
|
509
793
|
}
|
|
510
794
|
|
|
511
|
-
const
|
|
512
|
-
|
|
795
|
+
const sessionRows = runSqliteQuery(`
|
|
796
|
+
SELECT id FROM session WHERE project_id = ${sqlQuote(projectId)}
|
|
797
|
+
`);
|
|
513
798
|
|
|
514
|
-
|
|
515
|
-
const sessionPath = path.join(projectSessionDir, file);
|
|
516
|
-
const session = readJsonSafe(sessionPath, null);
|
|
517
|
-
const sessionId = session?.id || path.basename(file, '.json');
|
|
518
|
-
deletedSessionIds.push(sessionId);
|
|
519
|
-
|
|
520
|
-
const messageDir = getMessageDir(sessionId);
|
|
521
|
-
if (fs.existsSync(messageDir)) {
|
|
522
|
-
fs.rmSync(messageDir, { recursive: true, force: true });
|
|
523
|
-
}
|
|
799
|
+
const deletedSessionIds = sessionRows.map(row => row.id);
|
|
524
800
|
|
|
801
|
+
for (const sessionId of deletedSessionIds) {
|
|
525
802
|
try {
|
|
526
803
|
const { deleteAlias } = require('./alias');
|
|
527
804
|
deleteAlias(sessionId);
|
|
@@ -530,14 +807,10 @@ function deleteProject(projectId) {
|
|
|
530
807
|
}
|
|
531
808
|
}
|
|
532
809
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
if (entry.data.id === projectId) {
|
|
538
|
-
fs.rmSync(entry.filePath, { force: true });
|
|
539
|
-
}
|
|
540
|
-
}
|
|
810
|
+
runSqliteExec(`
|
|
811
|
+
PRAGMA foreign_keys = ON;
|
|
812
|
+
DELETE FROM project WHERE id = ${sqlQuote(projectId)};
|
|
813
|
+
`);
|
|
541
814
|
|
|
542
815
|
removeProjectFromOrder(projectId);
|
|
543
816
|
|
|
@@ -555,6 +828,7 @@ function deleteProject(projectId) {
|
|
|
555
828
|
// ignore relation cleanup errors
|
|
556
829
|
}
|
|
557
830
|
|
|
831
|
+
invalidateProjectAndSessionCountsCache();
|
|
558
832
|
return {
|
|
559
833
|
success: true,
|
|
560
834
|
projectName: projectId,
|
|
@@ -589,6 +863,7 @@ function searchSessions(keyword, contextLength = 35, projectFilter = null) {
|
|
|
589
863
|
session.slug,
|
|
590
864
|
session.directory
|
|
591
865
|
];
|
|
866
|
+
|
|
592
867
|
for (const text of quickChecks) {
|
|
593
868
|
const context = buildContext(text, searchKeyword, contextLength);
|
|
594
869
|
if (context) {
|
|
@@ -600,26 +875,18 @@ function searchSessions(keyword, contextLength = 35, projectFilter = null) {
|
|
|
600
875
|
}
|
|
601
876
|
}
|
|
602
877
|
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
for (const messageFile of messageFiles) {
|
|
609
|
-
const messagePath = path.join(messageDir, messageFile);
|
|
610
|
-
const message = readJsonSafe(messagePath, null);
|
|
611
|
-
if (!message) continue;
|
|
612
|
-
|
|
613
|
-
const text = extractTextContent(message.content);
|
|
614
|
-
const context = buildContext(text, searchKeyword, contextLength);
|
|
615
|
-
if (!context) continue;
|
|
616
|
-
|
|
617
|
-
matches.push({
|
|
618
|
-
role: message.role === 'user' ? 'user' : 'assistant',
|
|
619
|
-
context,
|
|
620
|
-
timestamp: message.time?.created || null
|
|
621
|
-
});
|
|
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;
|
|
622
883
|
}
|
|
884
|
+
|
|
885
|
+
matches.push({
|
|
886
|
+
role: message.role,
|
|
887
|
+
context,
|
|
888
|
+
timestamp: message.timestamp
|
|
889
|
+
});
|
|
623
890
|
}
|
|
624
891
|
|
|
625
892
|
if (matches.length > 0) {
|
|
@@ -651,6 +918,7 @@ module.exports = {
|
|
|
651
918
|
getSessionsByProject,
|
|
652
919
|
getSessionsByProjectId,
|
|
653
920
|
getSessionById,
|
|
921
|
+
getSessionMessages,
|
|
654
922
|
getRecentSessions,
|
|
655
923
|
normalizeSession,
|
|
656
924
|
getProjectAndSessionCounts,
|