coding-tool-x 3.7.4 → 3.7.6
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/dist/web/assets/{Analytics-BmPIW7oK.js → Analytics-wCRLv26m.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-CG4j4ua4.js → ConfigTemplates-aa1blEHh.js} +1 -1
- package/dist/web/assets/Home-CqnBXFxj.css +1 -0
- package/dist/web/assets/Home-eIxO50-k.js +1 -0
- package/dist/web/assets/{PluginManager-CCJ7mo35.js → PluginManager-BAwGFNjN.js} +1 -1
- package/dist/web/assets/ProjectList-CRiAEixW.js +1 -0
- package/dist/web/assets/ProjectList-Dw2omgOH.css +1 -0
- package/dist/web/assets/SessionList-BQ0FW6Gd.js +1 -0
- package/dist/web/assets/SessionList-kYJ6-Ies.css +1 -0
- package/dist/web/assets/{SkillManager-DmOTHcQ7.js → SkillManager-Bhzs1O5S.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-DLVj0zlu.js → WorkspaceManager-CLdX-pTT.js} +1 -1
- package/dist/web/assets/index-B_nFqixU.css +1 -0
- package/dist/web/assets/index-ClMNA9Hf.js +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/server/api/codex-projects.js +7 -11
- package/src/server/api/codex-sessions.js +30 -21
- package/src/server/api/dashboard.js +24 -36
- package/src/server/api/gemini-projects.js +7 -11
- package/src/server/api/gemini-sessions.js +28 -26
- package/src/server/api/omp-channels.js +50 -7
- package/src/server/api/omp-projects.js +6 -11
- package/src/server/api/omp-sessions.js +31 -17
- package/src/server/api/opencode-projects.js +6 -11
- package/src/server/api/opencode-sessions.js +28 -21
- package/src/server/api/projects.js +7 -28
- package/src/server/api/sessions.js +32 -16
- package/src/server/services/channel-balance.js +2 -1
- package/src/server/services/codex-sessions.js +23 -10
- package/src/server/services/dashboard-snapshot-worker.js +216 -0
- package/src/server/services/gemini-sessions.js +15 -3
- package/src/server/services/native-oauth-adapters.js +2 -1
- package/src/server/services/omp-auth-provider-worker.js +29 -0
- package/src/server/services/omp-auth-providers.js +137 -11
- package/src/server/services/omp-config.js +7 -4
- package/src/server/services/omp-sessions.js +38 -14
- package/src/server/services/opencode-sessions.js +9 -6
- package/src/server/services/plugins-service.js +2 -1
- package/src/server/services/project-snapshots.js +8 -1
- package/src/server/services/session-snapshot-worker.js +158 -0
- package/src/server/services/session-snapshots.js +145 -0
- package/src/server/services/sessions.js +6 -4
- package/src/server/services/snapshot-cache.js +123 -13
- package/dist/web/assets/Home-B9yAAZrD.js +0 -1
- package/dist/web/assets/Home-DSTUSxrT.css +0 -1
- package/dist/web/assets/ProjectList-D3a0oRum.css +0 -1
- package/dist/web/assets/ProjectList-DVUs3o8x.js +0 -1
- package/dist/web/assets/SessionList-D12muXvx.css +0 -1
- package/dist/web/assets/SessionList-DwssBydw.js +0 -1
- package/dist/web/assets/index-BjdNYwSs.js +0 -2
- package/dist/web/assets/index-kQCC5dlu.css +0 -1
|
@@ -16,6 +16,14 @@ const {
|
|
|
16
16
|
} = require('../services/omp-sessions');
|
|
17
17
|
const { loadAliases } = require('../services/alias');
|
|
18
18
|
const { broadcastLog } = require('../websocket-server');
|
|
19
|
+
const {
|
|
20
|
+
defaultProjectInfo,
|
|
21
|
+
emptySessionList,
|
|
22
|
+
getSessionListSnapshot,
|
|
23
|
+
invalidateSessionSnapshots,
|
|
24
|
+
runSessionSnapshotWorker
|
|
25
|
+
} = require('../services/session-snapshots');
|
|
26
|
+
const { invalidateProjectSnapshots } = require('../services/project-snapshots');
|
|
19
27
|
|
|
20
28
|
function isNotFoundError(error) {
|
|
21
29
|
if (!error || !error.message) return false;
|
|
@@ -81,27 +89,22 @@ module.exports = () => {
|
|
|
81
89
|
}
|
|
82
90
|
});
|
|
83
91
|
|
|
84
|
-
router.get('/:projectName', (req, res) => {
|
|
92
|
+
router.get('/:projectName', async (req, res) => {
|
|
85
93
|
try {
|
|
86
94
|
if (!isOmpInstalled()) {
|
|
87
95
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
88
96
|
}
|
|
89
97
|
const { projectName } = req.params;
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
projectInfo: {
|
|
99
|
-
name: projectName,
|
|
100
|
-
fullPath: project?.fullPath || project?.path || projectName,
|
|
101
|
-
path: project?.path || projectName,
|
|
102
|
-
displayName: project?.displayName || projectName
|
|
103
|
-
}
|
|
98
|
+
const force = req.query?.fresh === '1';
|
|
99
|
+
const snapshot = await getSessionListSnapshot('omp', projectName, {
|
|
100
|
+
fallbackValue: emptySessionList(projectName, {
|
|
101
|
+
aliases: loadAliases(),
|
|
102
|
+
projectInfo: defaultProjectInfo(projectName)
|
|
103
|
+
}),
|
|
104
|
+
force,
|
|
105
|
+
refresh: () => runSessionSnapshotWorker('omp', projectName, {}, { force })
|
|
104
106
|
});
|
|
107
|
+
res.json({ ...snapshot.value, meta: snapshot.meta });
|
|
105
108
|
} catch (err) {
|
|
106
109
|
console.error('[OMP API] Failed to get sessions:', err);
|
|
107
110
|
res.status(500).json({ error: err.message });
|
|
@@ -206,7 +209,10 @@ module.exports = () => {
|
|
|
206
209
|
if (!isOmpInstalled()) {
|
|
207
210
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
208
211
|
}
|
|
209
|
-
|
|
212
|
+
const result = deleteSession(req.params.sessionId);
|
|
213
|
+
invalidateSessionSnapshots('omp', req.params.projectName);
|
|
214
|
+
invalidateProjectSnapshots('omp');
|
|
215
|
+
res.json(result);
|
|
210
216
|
} catch (err) {
|
|
211
217
|
if (isNotFoundError(err)) {
|
|
212
218
|
return res.status(404).json({ error: err.message });
|
|
@@ -244,6 +250,10 @@ module.exports = () => {
|
|
|
244
250
|
failed.push({ sessionId, error: err.message });
|
|
245
251
|
}
|
|
246
252
|
});
|
|
253
|
+
if (deletedSessionIds.length > 0) {
|
|
254
|
+
invalidateSessionSnapshots('omp', req.params.projectName);
|
|
255
|
+
invalidateProjectSnapshots('omp');
|
|
256
|
+
}
|
|
247
257
|
res.json({
|
|
248
258
|
success: failed.length === 0,
|
|
249
259
|
requestedCount: uniqueSessionIds.length,
|
|
@@ -262,7 +272,10 @@ module.exports = () => {
|
|
|
262
272
|
if (!isOmpInstalled()) {
|
|
263
273
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
264
274
|
}
|
|
265
|
-
|
|
275
|
+
const result = forkSession(req.params.sessionId, req.body || {});
|
|
276
|
+
invalidateSessionSnapshots('omp', req.params.projectName);
|
|
277
|
+
invalidateProjectSnapshots('omp');
|
|
278
|
+
res.json(result);
|
|
266
279
|
} catch (err) {
|
|
267
280
|
if (isNotFoundError(err)) {
|
|
268
281
|
return res.status(404).json({ error: err.message });
|
|
@@ -282,6 +295,7 @@ module.exports = () => {
|
|
|
282
295
|
return res.status(400).json({ error: 'order must be an array' });
|
|
283
296
|
}
|
|
284
297
|
saveSessionOrder(req.params.projectName, order);
|
|
298
|
+
invalidateSessionSnapshots('omp', req.params.projectName);
|
|
285
299
|
res.json({ success: true });
|
|
286
300
|
} catch (err) {
|
|
287
301
|
console.error('[OMP API] Failed to save session order:', err);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const router = express.Router();
|
|
3
3
|
const {
|
|
4
|
-
getProjects,
|
|
5
4
|
saveProjectOrder,
|
|
6
5
|
deleteProject,
|
|
7
6
|
isOpenCodeInstalled
|
|
@@ -11,20 +10,14 @@ const {
|
|
|
11
10
|
getProjectListSnapshot,
|
|
12
11
|
invalidateProjectSnapshots
|
|
13
12
|
} = require('../services/project-snapshots');
|
|
13
|
+
const { invalidateSessionSnapshots } = require('../services/session-snapshots');
|
|
14
|
+
const { runDashboardSnapshotWorker } = require('../services/dashboard-snapshot-worker');
|
|
14
15
|
|
|
15
16
|
function isNotFoundError(error) {
|
|
16
17
|
return !!(error && error.message === 'Project not found');
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
module.exports = (config) => {
|
|
20
|
-
function buildProjectsPayload() {
|
|
21
|
-
const projects = getProjects();
|
|
22
|
-
return {
|
|
23
|
-
projects,
|
|
24
|
-
currentProject: projects[0] ? projects[0].name : null
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
21
|
/**
|
|
29
22
|
* GET /api/opencode/projects
|
|
30
23
|
* 获取所有 OpenCode 项目列表
|
|
@@ -40,10 +33,11 @@ module.exports = (config) => {
|
|
|
40
33
|
});
|
|
41
34
|
}
|
|
42
35
|
|
|
36
|
+
const force = req.query?.fresh === '1';
|
|
43
37
|
const snapshot = await getProjectListSnapshot('opencode', {
|
|
44
38
|
fallbackValue: emptyProjectList(null),
|
|
45
|
-
force
|
|
46
|
-
refresh:
|
|
39
|
+
force,
|
|
40
|
+
refresh: () => runDashboardSnapshotWorker('projects', 'opencode', config, { force })
|
|
47
41
|
});
|
|
48
42
|
res.json({ ...snapshot.value, meta: snapshot.meta });
|
|
49
43
|
} catch (err) {
|
|
@@ -100,6 +94,7 @@ module.exports = (config) => {
|
|
|
100
94
|
const { projectName } = req.params;
|
|
101
95
|
const result = deleteProject(projectName);
|
|
102
96
|
invalidateProjectSnapshots('opencode');
|
|
97
|
+
invalidateSessionSnapshots('opencode', projectName);
|
|
103
98
|
res.json(result);
|
|
104
99
|
} catch (err) {
|
|
105
100
|
if (isNotFoundError(err)) {
|
|
@@ -15,6 +15,14 @@ const {
|
|
|
15
15
|
const { loadAliases } = require('../services/alias');
|
|
16
16
|
const { broadcastLog } = require('../websocket-server');
|
|
17
17
|
const { HOME_DIR } = require('../../config/paths');
|
|
18
|
+
const {
|
|
19
|
+
defaultProjectInfo,
|
|
20
|
+
emptySessionList,
|
|
21
|
+
getSessionListSnapshot,
|
|
22
|
+
invalidateSessionSnapshots,
|
|
23
|
+
runSessionSnapshotWorker
|
|
24
|
+
} = require('../services/session-snapshots');
|
|
25
|
+
const { invalidateProjectSnapshots } = require('../services/project-snapshots');
|
|
18
26
|
|
|
19
27
|
function isNotFoundError(error) {
|
|
20
28
|
if (!error || !error.message) {
|
|
@@ -117,34 +125,23 @@ module.exports = (config) => {
|
|
|
117
125
|
* GET /api/opencode/sessions/:projectName
|
|
118
126
|
* 获取项目的所有会话
|
|
119
127
|
*/
|
|
120
|
-
router.get('/:projectName', (req, res) => {
|
|
128
|
+
router.get('/:projectName', async (req, res) => {
|
|
121
129
|
try {
|
|
122
130
|
if (!isOpenCodeInstalled()) {
|
|
123
131
|
return res.status(404).json({ error: 'OpenCode CLI not installed' });
|
|
124
132
|
}
|
|
125
133
|
|
|
126
134
|
const { projectName } = req.params;
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}, 0);
|
|
136
|
-
|
|
137
|
-
res.json({
|
|
138
|
-
sessions,
|
|
139
|
-
totalSize,
|
|
140
|
-
aliases,
|
|
141
|
-
projectInfo: {
|
|
142
|
-
name: projectName,
|
|
143
|
-
fullPath: project?.fullPath || projectName,
|
|
144
|
-
path: project?.path || projectName,
|
|
145
|
-
displayName: project?.displayName || projectName
|
|
146
|
-
}
|
|
135
|
+
const force = req.query?.fresh === '1';
|
|
136
|
+
const snapshot = await getSessionListSnapshot('opencode', projectName, {
|
|
137
|
+
fallbackValue: emptySessionList(projectName, {
|
|
138
|
+
aliases: loadAliases(),
|
|
139
|
+
projectInfo: defaultProjectInfo(projectName)
|
|
140
|
+
}),
|
|
141
|
+
force,
|
|
142
|
+
refresh: () => runSessionSnapshotWorker('opencode', projectName, {}, { force })
|
|
147
143
|
});
|
|
144
|
+
res.json({ ...snapshot.value, meta: snapshot.meta });
|
|
148
145
|
} catch (err) {
|
|
149
146
|
console.error('[OpenCode API] Failed to get sessions:', err);
|
|
150
147
|
res.status(500).json({ error: err.message });
|
|
@@ -294,6 +291,8 @@ module.exports = (config) => {
|
|
|
294
291
|
|
|
295
292
|
const { sessionId } = req.params;
|
|
296
293
|
const result = deleteSession(sessionId);
|
|
294
|
+
invalidateSessionSnapshots('opencode', req.params.projectName);
|
|
295
|
+
invalidateProjectSnapshots('opencode');
|
|
297
296
|
|
|
298
297
|
res.json(result);
|
|
299
298
|
} catch (err) {
|
|
@@ -347,6 +346,11 @@ module.exports = (config) => {
|
|
|
347
346
|
}
|
|
348
347
|
});
|
|
349
348
|
|
|
349
|
+
if (deletedSessionIds.length > 0) {
|
|
350
|
+
invalidateSessionSnapshots('opencode', req.params.projectName);
|
|
351
|
+
invalidateProjectSnapshots('opencode');
|
|
352
|
+
}
|
|
353
|
+
|
|
350
354
|
res.json({
|
|
351
355
|
success: failed.length === 0,
|
|
352
356
|
requestedCount: uniqueSessionIds.length,
|
|
@@ -372,6 +376,8 @@ module.exports = (config) => {
|
|
|
372
376
|
|
|
373
377
|
const { sessionId } = req.params;
|
|
374
378
|
const result = forkSession(sessionId);
|
|
379
|
+
invalidateSessionSnapshots('opencode', req.params.projectName);
|
|
380
|
+
invalidateProjectSnapshots('opencode');
|
|
375
381
|
res.json(result);
|
|
376
382
|
} catch (err) {
|
|
377
383
|
if (isNotFoundError(err)) {
|
|
@@ -401,6 +407,7 @@ module.exports = (config) => {
|
|
|
401
407
|
}
|
|
402
408
|
|
|
403
409
|
saveSessionOrder(projectName, order);
|
|
410
|
+
invalidateSessionSnapshots('opencode', projectName);
|
|
404
411
|
res.json({ success: true });
|
|
405
412
|
} catch (err) {
|
|
406
413
|
console.error('[OpenCode API] Failed to save session order:', err);
|
|
@@ -1,45 +1,23 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const router = express.Router();
|
|
3
|
-
const {
|
|
3
|
+
const { saveProjectOrder, deleteProject } = require('../services/sessions');
|
|
4
4
|
const {
|
|
5
5
|
emptyProjectList,
|
|
6
6
|
getProjectListSnapshot,
|
|
7
7
|
invalidateProjectSnapshots
|
|
8
8
|
} = require('../services/project-snapshots');
|
|
9
|
+
const { invalidateSessionSnapshots } = require('../services/session-snapshots');
|
|
10
|
+
const { runDashboardSnapshotWorker } = require('../services/dashboard-snapshot-worker');
|
|
9
11
|
|
|
10
12
|
module.exports = (config) => {
|
|
11
|
-
async function buildProjectsPayload() {
|
|
12
|
-
const projects = await getProjectsWithStats(config);
|
|
13
|
-
const order = getProjectOrder(config);
|
|
14
|
-
|
|
15
|
-
// Sort projects by saved order
|
|
16
|
-
let sortedProjects = projects;
|
|
17
|
-
if (order && order.length > 0) {
|
|
18
|
-
const orderMap = new Map(order.map((name, idx) => [name, idx]));
|
|
19
|
-
sortedProjects = [...projects].sort((a, b) => {
|
|
20
|
-
const aIdx = orderMap.has(a.name) ? orderMap.get(a.name) : 999999;
|
|
21
|
-
const bIdx = orderMap.has(b.name) ? orderMap.get(b.name) : 999999;
|
|
22
|
-
if (aIdx === bIdx) {
|
|
23
|
-
// Both are new, sort by lastUsed
|
|
24
|
-
return (b.lastUsed || 0) - (a.lastUsed || 0);
|
|
25
|
-
}
|
|
26
|
-
return aIdx - bIdx;
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
projects: sortedProjects,
|
|
32
|
-
currentProject: config.currentProject || (sortedProjects[0] ? sortedProjects[0].name : null)
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
13
|
// GET /api/projects - Get all projects with stats
|
|
37
14
|
router.get('/', async (req, res) => {
|
|
38
15
|
try {
|
|
16
|
+
const force = req.query?.fresh === '1';
|
|
39
17
|
const snapshot = await getProjectListSnapshot('claude', {
|
|
40
18
|
fallbackValue: emptyProjectList(config.currentProject || null),
|
|
41
|
-
force
|
|
42
|
-
refresh:
|
|
19
|
+
force,
|
|
20
|
+
refresh: () => runDashboardSnapshotWorker('projects', 'claude', config, { force })
|
|
43
21
|
});
|
|
44
22
|
res.json({ ...snapshot.value, meta: snapshot.meta });
|
|
45
23
|
} catch (error) {
|
|
@@ -130,6 +108,7 @@ module.exports = (config) => {
|
|
|
130
108
|
const { projectName } = req.params;
|
|
131
109
|
deleteProject(config, projectName);
|
|
132
110
|
invalidateProjectSnapshots('claude');
|
|
111
|
+
invalidateSessionSnapshots('claude', projectName);
|
|
133
112
|
res.json({ success: true });
|
|
134
113
|
} catch (error) {
|
|
135
114
|
console.error('Error deleting project:', error);
|
|
@@ -3,10 +3,18 @@ const router = express.Router();
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const readline = require('readline');
|
|
6
|
-
const {
|
|
6
|
+
const { deleteSession, forkSession, saveSessionOrder, parseRealProjectPath, searchSessions, getRecentSessions, searchSessionsAcrossProjects, hasActualMessages } = require('../services/sessions');
|
|
7
7
|
const { loadAliases } = require('../services/alias');
|
|
8
8
|
const { broadcastLog } = require('../websocket-server');
|
|
9
9
|
const { NATIVE_PATHS } = require('../../config/paths');
|
|
10
|
+
const {
|
|
11
|
+
defaultProjectInfo,
|
|
12
|
+
emptySessionList,
|
|
13
|
+
getSessionListSnapshot,
|
|
14
|
+
invalidateSessionSnapshots,
|
|
15
|
+
runSessionSnapshotWorker
|
|
16
|
+
} = require('../services/session-snapshots');
|
|
17
|
+
const { invalidateProjectSnapshots } = require('../services/project-snapshots');
|
|
10
18
|
const CLAUDE_PROJECTS_DIR = NATIVE_PATHS.claude.projects;
|
|
11
19
|
|
|
12
20
|
function resolveClaudeSessionFile(projectName, sessionId, fullPath) {
|
|
@@ -158,22 +166,16 @@ module.exports = (config) => {
|
|
|
158
166
|
router.get('/:projectName', async (req, res) => {
|
|
159
167
|
try {
|
|
160
168
|
const { projectName } = req.params;
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
totalSize: result.totalSize,
|
|
170
|
-
aliases,
|
|
171
|
-
projectInfo: {
|
|
172
|
-
name: projectName,
|
|
173
|
-
displayName,
|
|
174
|
-
fullPath
|
|
175
|
-
}
|
|
169
|
+
const force = req.query?.fresh === '1';
|
|
170
|
+
const snapshot = await getSessionListSnapshot('claude', projectName, {
|
|
171
|
+
fallbackValue: emptySessionList(projectName, {
|
|
172
|
+
aliases: loadAliases(),
|
|
173
|
+
projectInfo: defaultProjectInfo(projectName)
|
|
174
|
+
}),
|
|
175
|
+
force,
|
|
176
|
+
refresh: () => runSessionSnapshotWorker('claude', projectName, config, { force })
|
|
176
177
|
});
|
|
178
|
+
res.json({ ...snapshot.value, meta: snapshot.meta });
|
|
177
179
|
} catch (error) {
|
|
178
180
|
console.error('Error fetching sessions:', error);
|
|
179
181
|
res.status(500).json({ error: error.message });
|
|
@@ -185,6 +187,8 @@ module.exports = (config) => {
|
|
|
185
187
|
try {
|
|
186
188
|
const { projectName, sessionId } = req.params;
|
|
187
189
|
const result = deleteSession(config, projectName, sessionId);
|
|
190
|
+
invalidateSessionSnapshots('claude', projectName);
|
|
191
|
+
invalidateProjectSnapshots('claude');
|
|
188
192
|
res.json(result);
|
|
189
193
|
} catch (error) {
|
|
190
194
|
console.error('Error deleting session:', error);
|
|
@@ -228,6 +232,11 @@ module.exports = (config) => {
|
|
|
228
232
|
}
|
|
229
233
|
});
|
|
230
234
|
|
|
235
|
+
if (deletedSessionIds.length > 0) {
|
|
236
|
+
invalidateSessionSnapshots('claude', projectName);
|
|
237
|
+
invalidateProjectSnapshots('claude');
|
|
238
|
+
}
|
|
239
|
+
|
|
231
240
|
res.json({
|
|
232
241
|
success: failed.length === 0,
|
|
233
242
|
requestedCount: uniqueSessionIds.length,
|
|
@@ -246,6 +255,8 @@ module.exports = (config) => {
|
|
|
246
255
|
try {
|
|
247
256
|
const { projectName, sessionId } = req.params;
|
|
248
257
|
const result = forkSession(config, projectName, sessionId, normalizeForkOptions(req.body));
|
|
258
|
+
invalidateSessionSnapshots('claude', projectName);
|
|
259
|
+
invalidateProjectSnapshots('claude');
|
|
249
260
|
res.json(result);
|
|
250
261
|
} catch (error) {
|
|
251
262
|
console.error('Error forking session:', error);
|
|
@@ -330,6 +341,10 @@ module.exports = (config) => {
|
|
|
330
341
|
timestamp: Date.now()
|
|
331
342
|
});
|
|
332
343
|
|
|
344
|
+
const snapshotSource = toolType === 'codex' || toolType === 'gemini' ? toolType : 'claude';
|
|
345
|
+
invalidateSessionSnapshots(snapshotSource, projectName);
|
|
346
|
+
invalidateProjectSnapshots(snapshotSource);
|
|
347
|
+
|
|
333
348
|
res.json({
|
|
334
349
|
success: true,
|
|
335
350
|
sessionId: newSessionId,
|
|
@@ -352,6 +367,7 @@ module.exports = (config) => {
|
|
|
352
367
|
return res.status(400).json({ error: 'order must be an array' });
|
|
353
368
|
}
|
|
354
369
|
saveSessionOrder(projectName, order);
|
|
370
|
+
invalidateSessionSnapshots('claude', projectName);
|
|
355
371
|
res.json({ success: true });
|
|
356
372
|
} catch (error) {
|
|
357
373
|
console.error('Error saving session order:', error);
|
|
@@ -1737,7 +1737,8 @@ async function refreshChannelBalance(source, channelId) {
|
|
|
1737
1737
|
|
|
1738
1738
|
const entry = await refreshSnapshot(
|
|
1739
1739
|
buildBalanceSnapshotKey(normalizedSource, channel),
|
|
1740
|
-
() => refreshChannelBalanceSnapshot(normalizedSource, channel, { force: true })
|
|
1740
|
+
() => refreshChannelBalanceSnapshot(normalizedSource, channel, { force: true }),
|
|
1741
|
+
{ bypassInflight: true }
|
|
1741
1742
|
);
|
|
1742
1743
|
const balance = entry.value;
|
|
1743
1744
|
return {
|
|
@@ -227,10 +227,14 @@ function normalizeSession(codexSession) {
|
|
|
227
227
|
* 聚合项目列表
|
|
228
228
|
* @returns {Array} 项目对象数组
|
|
229
229
|
*/
|
|
230
|
-
function getProjects() {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
230
|
+
function getProjects(options = {}) {
|
|
231
|
+
if (options.force) {
|
|
232
|
+
invalidateCodexSessionCaches();
|
|
233
|
+
} else {
|
|
234
|
+
const cached = globalCache.get(CODEX_PROJECTS_CACHE_KEY);
|
|
235
|
+
if (cached) {
|
|
236
|
+
return cached;
|
|
237
|
+
}
|
|
234
238
|
}
|
|
235
239
|
|
|
236
240
|
const sessions = getAllSessions();
|
|
@@ -302,11 +306,15 @@ function getProjects() {
|
|
|
302
306
|
* @param {string} projectName - 项目名称
|
|
303
307
|
* @returns {Array} 归一化的会话数组
|
|
304
308
|
*/
|
|
305
|
-
function getSessionsByProject(projectName) {
|
|
309
|
+
function getSessionsByProject(projectName, options = {}) {
|
|
306
310
|
const cacheKey = getCodexSessionsCacheKey(projectName);
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
311
|
+
if (options.force) {
|
|
312
|
+
invalidateCodexSessionCaches({ projectName });
|
|
313
|
+
} else {
|
|
314
|
+
const cached = globalCache.get(cacheKey);
|
|
315
|
+
if (cached) {
|
|
316
|
+
return cached;
|
|
317
|
+
}
|
|
310
318
|
}
|
|
311
319
|
|
|
312
320
|
const sessions = getAllSessions();
|
|
@@ -975,9 +983,14 @@ function calculateProjectAndSessionCounts() {
|
|
|
975
983
|
/**
|
|
976
984
|
* 获取 Codex 项目与会话数量(用于仪表盘轻量统计)
|
|
977
985
|
*/
|
|
978
|
-
function getProjectAndSessionCounts() {
|
|
986
|
+
function getProjectAndSessionCounts(options = {}) {
|
|
979
987
|
const now = Date.now();
|
|
980
|
-
if (
|
|
988
|
+
if (options.force) {
|
|
989
|
+
countsCache.expiresAt = 0;
|
|
990
|
+
scanFilesCache.expiresAt = 0;
|
|
991
|
+
sessionFileIndexCache.expiresAt = 0;
|
|
992
|
+
}
|
|
993
|
+
if (!options.force && countsCache.expiresAt > now) {
|
|
981
994
|
return countsCache.value;
|
|
982
995
|
}
|
|
983
996
|
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { fork } = require('child_process');
|
|
4
|
+
|
|
5
|
+
const DASHBOARD_SNAPSHOT_WORKER_TIMEOUT_MS = 180 * 1000;
|
|
6
|
+
|
|
7
|
+
function sortClaudeProjects(projects, order = []) {
|
|
8
|
+
if (!Array.isArray(order) || order.length === 0) {
|
|
9
|
+
return projects;
|
|
10
|
+
}
|
|
11
|
+
const orderMap = new Map(order.map((name, idx) => [name, idx]));
|
|
12
|
+
return [...projects].sort((a, b) => {
|
|
13
|
+
const aIdx = orderMap.has(a.name) ? orderMap.get(a.name) : 999999;
|
|
14
|
+
const bIdx = orderMap.has(b.name) ? orderMap.get(b.name) : 999999;
|
|
15
|
+
if (aIdx === bIdx) {
|
|
16
|
+
return (b.lastUsed || 0) - (a.lastUsed || 0);
|
|
17
|
+
}
|
|
18
|
+
return aIdx - bIdx;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function buildProjectsPayload(source, config = {}, options = {}) {
|
|
23
|
+
switch (source) {
|
|
24
|
+
case 'claude': {
|
|
25
|
+
const { getProjectsWithStats, getProjectOrder } = require('./sessions');
|
|
26
|
+
const projects = await getProjectsWithStats(config, { force: options.force === true });
|
|
27
|
+
const sortedProjects = sortClaudeProjects(projects, getProjectOrder(config));
|
|
28
|
+
return {
|
|
29
|
+
projects: sortedProjects,
|
|
30
|
+
currentProject: config.currentProject || (sortedProjects[0] ? sortedProjects[0].name : null)
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
case 'codex': {
|
|
34
|
+
const { getProjects } = require('./codex-sessions');
|
|
35
|
+
const projects = getProjects({ force: options.force === true });
|
|
36
|
+
return { projects, currentProject: projects[0] ? projects[0].name : null };
|
|
37
|
+
}
|
|
38
|
+
case 'gemini': {
|
|
39
|
+
const { getProjects } = require('./gemini-sessions');
|
|
40
|
+
const projects = getProjects({ force: options.force === true });
|
|
41
|
+
return { projects, currentProject: projects[0] ? projects[0].name : null };
|
|
42
|
+
}
|
|
43
|
+
case 'opencode': {
|
|
44
|
+
const { getProjects } = require('./opencode-sessions');
|
|
45
|
+
const projects = getProjects({ force: options.force === true });
|
|
46
|
+
return { projects, currentProject: projects[0] ? projects[0].name : null };
|
|
47
|
+
}
|
|
48
|
+
case 'omp': {
|
|
49
|
+
const { getProjects } = require('./omp-sessions');
|
|
50
|
+
const projects = getProjects({ force: options.force === true });
|
|
51
|
+
return { projects, currentProject: projects[0] ? projects[0].name : null };
|
|
52
|
+
}
|
|
53
|
+
default:
|
|
54
|
+
throw new Error(`Unsupported project snapshot source: ${source}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildCountsPayload(source, config = {}, options = {}) {
|
|
59
|
+
switch (source) {
|
|
60
|
+
case 'claude':
|
|
61
|
+
return require('./sessions').getProjectAndSessionCounts(config, { force: options.force === true });
|
|
62
|
+
case 'codex':
|
|
63
|
+
return require('./codex-sessions').getProjectAndSessionCounts({ force: options.force === true });
|
|
64
|
+
case 'gemini':
|
|
65
|
+
return require('./gemini-sessions').getProjectAndSessionCounts({ force: options.force === true });
|
|
66
|
+
case 'opencode':
|
|
67
|
+
return require('./opencode-sessions').getProjectAndSessionCounts({ force: options.force === true });
|
|
68
|
+
case 'omp':
|
|
69
|
+
return require('./omp-sessions').getProjectAndSessionCounts({ force: options.force === true });
|
|
70
|
+
default:
|
|
71
|
+
throw new Error(`Unsupported counts snapshot source: ${source}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildTodayStatsPayload(source) {
|
|
76
|
+
switch (source) {
|
|
77
|
+
case 'claude':
|
|
78
|
+
return require('./claude-statistics-service').getTodayStatistics();
|
|
79
|
+
case 'codex':
|
|
80
|
+
return require('./codex-statistics-service').getTodayStatistics();
|
|
81
|
+
case 'gemini':
|
|
82
|
+
return require('./gemini-statistics-service').getTodayStatistics();
|
|
83
|
+
case 'opencode':
|
|
84
|
+
return require('./opencode-statistics-service').getTodayStatistics();
|
|
85
|
+
case 'omp':
|
|
86
|
+
return require('./omp-statistics-service').getTodayStatistics();
|
|
87
|
+
default:
|
|
88
|
+
throw new Error(`Unsupported today stats snapshot source: ${source}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function buildChannelsPayload(source) {
|
|
93
|
+
switch (source) {
|
|
94
|
+
case 'claude':
|
|
95
|
+
return require('./channels').getAllChannels();
|
|
96
|
+
case 'codex':
|
|
97
|
+
return require('./codex-channels').getChannels();
|
|
98
|
+
case 'gemini':
|
|
99
|
+
return require('./gemini-channels').getChannels();
|
|
100
|
+
case 'opencode':
|
|
101
|
+
return require('./opencode-channels').getChannels();
|
|
102
|
+
case 'omp':
|
|
103
|
+
return require('./omp-channels').getChannels();
|
|
104
|
+
default:
|
|
105
|
+
throw new Error(`Unsupported channels snapshot source: ${source}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function buildPayload({ kind, source, config, options }) {
|
|
110
|
+
const snapshotOptions = options || {};
|
|
111
|
+
switch (kind) {
|
|
112
|
+
case 'projects':
|
|
113
|
+
return buildProjectsPayload(source, config || {}, snapshotOptions);
|
|
114
|
+
case 'counts':
|
|
115
|
+
return buildCountsPayload(source, config || {}, snapshotOptions);
|
|
116
|
+
case 'todayStats':
|
|
117
|
+
return buildTodayStatsPayload(source);
|
|
118
|
+
case 'channels':
|
|
119
|
+
return buildChannelsPayload(source);
|
|
120
|
+
default:
|
|
121
|
+
throw new Error(`Unsupported dashboard snapshot kind: ${kind}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function runDashboardSnapshotWorker(kind, source, config = {}, options = {}) {
|
|
126
|
+
if (process.env.NODE_ENV === 'test') {
|
|
127
|
+
return Promise.resolve(buildPayload({ kind, source, config, options }));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const child = fork(__filename, [], {
|
|
132
|
+
stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
|
|
133
|
+
env: {
|
|
134
|
+
...process.env,
|
|
135
|
+
CC_TOOL_DASHBOARD_SNAPSHOT_WORKER: '1'
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
let settled = false;
|
|
140
|
+
let stderr = '';
|
|
141
|
+
let timeout = null;
|
|
142
|
+
|
|
143
|
+
const finish = (error, value) => {
|
|
144
|
+
if (settled) return;
|
|
145
|
+
settled = true;
|
|
146
|
+
if (timeout) {
|
|
147
|
+
clearTimeout(timeout);
|
|
148
|
+
}
|
|
149
|
+
child.removeAllListeners();
|
|
150
|
+
if (!child.killed) {
|
|
151
|
+
child.kill();
|
|
152
|
+
}
|
|
153
|
+
if (error) {
|
|
154
|
+
reject(error);
|
|
155
|
+
} else {
|
|
156
|
+
resolve(value);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
timeout = setTimeout(() => {
|
|
161
|
+
finish(new Error(`Dashboard snapshot refresh timed out for ${kind}/${source}`));
|
|
162
|
+
}, DASHBOARD_SNAPSHOT_WORKER_TIMEOUT_MS);
|
|
163
|
+
|
|
164
|
+
child.stderr.on('data', (chunk) => {
|
|
165
|
+
stderr += chunk.toString();
|
|
166
|
+
if (stderr.length > 4096) {
|
|
167
|
+
stderr = stderr.slice(-4096);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
child.on('message', (message) => {
|
|
172
|
+
if (!message || typeof message !== 'object') return;
|
|
173
|
+
if (message.ok) {
|
|
174
|
+
finish(null, message.value);
|
|
175
|
+
} else {
|
|
176
|
+
finish(new Error(message.error || `Dashboard snapshot refresh failed for ${kind}/${source}`));
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
child.on('error', (error) => finish(error));
|
|
181
|
+
child.on('exit', (code, signal) => {
|
|
182
|
+
if (settled) return;
|
|
183
|
+
const suffix = stderr ? `: ${stderr.trim()}` : '';
|
|
184
|
+
finish(new Error(`Dashboard snapshot worker exited (${code || signal || 'unknown'})${suffix}`));
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
child.send({ kind, source, config, options });
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function attachWorkerHandler() {
|
|
192
|
+
process.on('message', async (message) => {
|
|
193
|
+
try {
|
|
194
|
+
const value = await buildPayload(message || {});
|
|
195
|
+
if (process.send) {
|
|
196
|
+
process.send({ ok: true, value }, () => process.exit(0));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (process.send) {
|
|
201
|
+
process.send({ ok: false, error: error?.message || String(error) }, () => process.exit(1));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
process.exit(0);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (process.env.CC_TOOL_DASHBOARD_SNAPSHOT_WORKER === '1' || require.main === module) {
|
|
210
|
+
attachWorkerHandler();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
buildPayload,
|
|
215
|
+
runDashboardSnapshotWorker
|
|
216
|
+
};
|