coding-tool-x 3.7.3 → 3.7.5
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-BqDjJUJl.js → Analytics-DnnufrQi.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-DV2KFuC4.js → ConfigTemplates-BTVhMSfe.js} +1 -1
- package/dist/web/assets/Home-CqnBXFxj.css +1 -0
- package/dist/web/assets/Home-D03PEREx.js +1 -0
- package/dist/web/assets/{PluginManager-Bd3hhc5A.js → PluginManager-BiMqEiyQ.js} +1 -1
- package/dist/web/assets/{ProjectList-BB4Whns8.js → ProjectList-C6BA3V49.js} +1 -1
- package/dist/web/assets/{ProjectList-D3a0oRum.css → ProjectList-DQ5ZCJO2.css} +1 -1
- package/dist/web/assets/SessionList--9Bl6EhV.css +1 -0
- package/dist/web/assets/SessionList-DChG04jz.js +1 -0
- package/dist/web/assets/{SkillManager-DQjRXOFg.js → SkillManager-DAy8k5CL.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-CZeXnxu1.js → WorkspaceManager-Bs2D6R9j.js} +1 -1
- package/dist/web/assets/index-DnDBHGCC.js +2 -0
- package/dist/web/assets/index-U3TOywtB.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/config/paths.js +2 -0
- package/src/server/api/codex-projects.js +22 -9
- package/src/server/api/codex-sessions.js +30 -21
- package/src/server/api/dashboard.js +174 -94
- package/src/server/api/gemini-projects.js +20 -8
- package/src/server/api/gemini-sessions.js +28 -26
- package/src/server/api/omp-projects.js +19 -7
- package/src/server/api/omp-sessions.js +31 -17
- package/src/server/api/opencode-projects.js +19 -8
- package/src/server/api/opencode-sessions.js +28 -21
- package/src/server/api/projects.js +18 -22
- package/src/server/api/sessions.js +32 -16
- package/src/server/services/channel-balance.js +52 -5
- 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/omp-sessions.js +31 -11
- package/src/server/services/opencode-sessions.js +9 -6
- package/src/server/services/project-snapshots.js +52 -0
- package/src/server/services/session-snapshot-worker.js +158 -0
- package/src/server/services/session-snapshots.js +137 -0
- package/src/server/services/sessions.js +6 -4
- package/src/server/services/snapshot-cache.js +361 -0
- package/dist/web/assets/Home-D7hovkk9.js +0 -1
- package/dist/web/assets/Home-Jom_ZcdY.css +0 -1
- package/dist/web/assets/SessionList-CvurKSRK.js +0 -1
- package/dist/web/assets/SessionList-D12muXvx.css +0 -1
- package/dist/web/assets/index-BovYCFsd.js +0 -2
- package/dist/web/assets/index-kQCC5dlu.css +0 -1
|
@@ -1,11 +1,17 @@
|
|
|
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
|
|
8
7
|
} = require('../services/opencode-sessions');
|
|
8
|
+
const {
|
|
9
|
+
emptyProjectList,
|
|
10
|
+
getProjectListSnapshot,
|
|
11
|
+
invalidateProjectSnapshots
|
|
12
|
+
} = require('../services/project-snapshots');
|
|
13
|
+
const { invalidateSessionSnapshots } = require('../services/session-snapshots');
|
|
14
|
+
const { runDashboardSnapshotWorker } = require('../services/dashboard-snapshot-worker');
|
|
9
15
|
|
|
10
16
|
function isNotFoundError(error) {
|
|
11
17
|
return !!(error && error.message === 'Project not found');
|
|
@@ -16,22 +22,24 @@ module.exports = (config) => {
|
|
|
16
22
|
* GET /api/opencode/projects
|
|
17
23
|
* 获取所有 OpenCode 项目列表
|
|
18
24
|
*/
|
|
19
|
-
router.get('/', (req, res) => {
|
|
25
|
+
router.get('/', async (req, res) => {
|
|
20
26
|
try {
|
|
21
27
|
if (!isOpenCodeInstalled()) {
|
|
22
28
|
return res.json({
|
|
23
29
|
projects: [],
|
|
24
30
|
currentProject: null,
|
|
25
|
-
error: 'OpenCode CLI not installed or not found'
|
|
31
|
+
error: 'OpenCode CLI not installed or not found',
|
|
32
|
+
meta: { generatedAt: new Date().toISOString(), stale: false, refreshing: false, fallback: true, error: null }
|
|
26
33
|
});
|
|
27
34
|
}
|
|
28
35
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
const force = req.query?.fresh === '1';
|
|
37
|
+
const snapshot = await getProjectListSnapshot('opencode', {
|
|
38
|
+
fallbackValue: emptyProjectList(null),
|
|
39
|
+
force,
|
|
40
|
+
refresh: () => runDashboardSnapshotWorker('projects', 'opencode', config, { force })
|
|
34
41
|
});
|
|
42
|
+
res.json({ ...snapshot.value, meta: snapshot.meta });
|
|
35
43
|
} catch (err) {
|
|
36
44
|
console.error('[OpenCode API] Failed to get projects:', err);
|
|
37
45
|
|
|
@@ -65,6 +73,7 @@ module.exports = (config) => {
|
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
saveProjectOrder(order);
|
|
76
|
+
invalidateProjectSnapshots('opencode');
|
|
68
77
|
res.json({ success: true });
|
|
69
78
|
} catch (err) {
|
|
70
79
|
console.error('[OpenCode API] Failed to save project order:', err);
|
|
@@ -84,6 +93,8 @@ module.exports = (config) => {
|
|
|
84
93
|
|
|
85
94
|
const { projectName } = req.params;
|
|
86
95
|
const result = deleteProject(projectName);
|
|
96
|
+
invalidateProjectSnapshots('opencode');
|
|
97
|
+
invalidateSessionSnapshots('opencode', projectName);
|
|
87
98
|
res.json(result);
|
|
88
99
|
} catch (err) {
|
|
89
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,33 +1,25 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const router = express.Router();
|
|
3
|
-
const {
|
|
3
|
+
const { saveProjectOrder, deleteProject } = require('../services/sessions');
|
|
4
|
+
const {
|
|
5
|
+
emptyProjectList,
|
|
6
|
+
getProjectListSnapshot,
|
|
7
|
+
invalidateProjectSnapshots
|
|
8
|
+
} = require('../services/project-snapshots');
|
|
9
|
+
const { invalidateSessionSnapshots } = require('../services/session-snapshots');
|
|
10
|
+
const { runDashboardSnapshotWorker } = require('../services/dashboard-snapshot-worker');
|
|
4
11
|
|
|
5
12
|
module.exports = (config) => {
|
|
6
13
|
// GET /api/projects - Get all projects with stats
|
|
7
14
|
router.get('/', async (req, res) => {
|
|
8
15
|
try {
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (order && order.length > 0) {
|
|
15
|
-
const orderMap = new Map(order.map((name, idx) => [name, idx]));
|
|
16
|
-
sortedProjects = [...projects].sort((a, b) => {
|
|
17
|
-
const aIdx = orderMap.has(a.name) ? orderMap.get(a.name) : 999999;
|
|
18
|
-
const bIdx = orderMap.has(b.name) ? orderMap.get(b.name) : 999999;
|
|
19
|
-
if (aIdx === bIdx) {
|
|
20
|
-
// Both are new, sort by lastUsed
|
|
21
|
-
return (b.lastUsed || 0) - (a.lastUsed || 0);
|
|
22
|
-
}
|
|
23
|
-
return aIdx - bIdx;
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
res.json({
|
|
28
|
-
projects: sortedProjects,
|
|
29
|
-
currentProject: config.currentProject || (sortedProjects[0] ? sortedProjects[0].name : null)
|
|
16
|
+
const force = req.query?.fresh === '1';
|
|
17
|
+
const snapshot = await getProjectListSnapshot('claude', {
|
|
18
|
+
fallbackValue: emptyProjectList(config.currentProject || null),
|
|
19
|
+
force,
|
|
20
|
+
refresh: () => runDashboardSnapshotWorker('projects', 'claude', config, { force })
|
|
30
21
|
});
|
|
22
|
+
res.json({ ...snapshot.value, meta: snapshot.meta });
|
|
31
23
|
} catch (error) {
|
|
32
24
|
console.error('Error fetching projects:', error);
|
|
33
25
|
res.status(500).json({ error: error.message });
|
|
@@ -42,6 +34,7 @@ module.exports = (config) => {
|
|
|
42
34
|
return res.status(400).json({ error: 'Order must be an array' });
|
|
43
35
|
}
|
|
44
36
|
saveProjectOrder(config, order);
|
|
37
|
+
invalidateProjectSnapshots('claude');
|
|
45
38
|
res.json({ success: true });
|
|
46
39
|
} catch (error) {
|
|
47
40
|
console.error('Error saving project order:', error);
|
|
@@ -96,6 +89,7 @@ module.exports = (config) => {
|
|
|
96
89
|
'utf8'
|
|
97
90
|
);
|
|
98
91
|
|
|
92
|
+
invalidateProjectSnapshots('claude');
|
|
99
93
|
res.json({
|
|
100
94
|
success: true,
|
|
101
95
|
projectName,
|
|
@@ -113,6 +107,8 @@ module.exports = (config) => {
|
|
|
113
107
|
try {
|
|
114
108
|
const { projectName } = req.params;
|
|
115
109
|
deleteProject(config, projectName);
|
|
110
|
+
invalidateProjectSnapshots('claude');
|
|
111
|
+
invalidateSessionSnapshots('claude', projectName);
|
|
116
112
|
res.json({ success: true });
|
|
117
113
|
} catch (error) {
|
|
118
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);
|
|
@@ -6,6 +6,11 @@ const path = require('path');
|
|
|
6
6
|
const { URL } = require('url');
|
|
7
7
|
const { PATHS } = require('../../config/paths');
|
|
8
8
|
const { loadUIConfig } = require('./ui-config');
|
|
9
|
+
const {
|
|
10
|
+
getSnapshot,
|
|
11
|
+
refreshSnapshot,
|
|
12
|
+
invalidateSnapshot
|
|
13
|
+
} = require('./snapshot-cache');
|
|
9
14
|
|
|
10
15
|
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
11
16
|
const REQUEST_TIMEOUT_MS = 8000;
|
|
@@ -1489,6 +1494,14 @@ function buildCacheKey(source, channel) {
|
|
|
1489
1494
|
].join(':');
|
|
1490
1495
|
}
|
|
1491
1496
|
|
|
1497
|
+
function buildBalanceSnapshotKey(source, channel) {
|
|
1498
|
+
return `channel-balances:${buildCacheKey(source, channel)}`;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
function makeFallbackBalanceSnapshot(currentTime = Date.now()) {
|
|
1502
|
+
return makeHiddenSnapshot(null, { stale: true, updatedAt: nowIso(currentTime) });
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1492
1505
|
async function refreshChannelBalanceSnapshot(source, channel, options = {}) {
|
|
1493
1506
|
const force = options.force === true;
|
|
1494
1507
|
const currentTime = options.now || Date.now();
|
|
@@ -1588,6 +1601,7 @@ function clearChannelBalanceCache(source, channel) {
|
|
|
1588
1601
|
let strategyChanged = false;
|
|
1589
1602
|
|
|
1590
1603
|
balanceCache.delete(exactKey);
|
|
1604
|
+
invalidateSnapshot(`channel-balances:${exactKey}`);
|
|
1591
1605
|
for (const key of Array.from(balanceCache.keys())) {
|
|
1592
1606
|
if (key.startsWith(prefix)) {
|
|
1593
1607
|
balanceCache.delete(key);
|
|
@@ -1665,19 +1679,45 @@ async function mapWithConcurrency(items, limit, worker) {
|
|
|
1665
1679
|
async function getChannelBalances(source, options = {}) {
|
|
1666
1680
|
const normalizedSource = validateSource(source);
|
|
1667
1681
|
if (!isBalanceDisplayEnabled()) {
|
|
1668
|
-
return {
|
|
1682
|
+
return {
|
|
1683
|
+
enabled: false,
|
|
1684
|
+
source: normalizedSource,
|
|
1685
|
+
balances: {},
|
|
1686
|
+
refreshing: false,
|
|
1687
|
+
updatedAt: null,
|
|
1688
|
+
stale: false
|
|
1689
|
+
};
|
|
1669
1690
|
}
|
|
1670
1691
|
|
|
1671
1692
|
const channels = getEnabledBalanceChannels(normalizedSource);
|
|
1693
|
+
const currentTime = options.now || Date.now();
|
|
1672
1694
|
const entries = await mapWithConcurrency(channels, 4, async (channel) => {
|
|
1673
|
-
const snapshot = await
|
|
1674
|
-
|
|
1695
|
+
const snapshot = await getSnapshot(buildBalanceSnapshotKey(normalizedSource, channel), {
|
|
1696
|
+
ttlMs: CACHE_TTL_MS,
|
|
1697
|
+
fallbackValue: makeFallbackBalanceSnapshot(currentTime),
|
|
1698
|
+
refresh: () => refreshChannelBalanceSnapshot(normalizedSource, channel, { ...options, force: true, now: Date.now() })
|
|
1699
|
+
});
|
|
1700
|
+
const value = {
|
|
1701
|
+
...snapshot.value,
|
|
1702
|
+
stale: Boolean(snapshot.value?.stale || snapshot.meta.stale)
|
|
1703
|
+
};
|
|
1704
|
+
return [channel.id, value, snapshot.meta];
|
|
1675
1705
|
});
|
|
1676
1706
|
|
|
1707
|
+
const metas = entries.map((entry) => entry[2]).filter(Boolean);
|
|
1708
|
+
const updatedAt = metas
|
|
1709
|
+
.map(meta => meta.generatedAt)
|
|
1710
|
+
.filter(Boolean)
|
|
1711
|
+
.sort()
|
|
1712
|
+
.pop() || null;
|
|
1713
|
+
|
|
1677
1714
|
return {
|
|
1678
1715
|
enabled: true,
|
|
1679
1716
|
source: normalizedSource,
|
|
1680
|
-
balances: Object.fromEntries(entries.filter(([id]) => id))
|
|
1717
|
+
balances: Object.fromEntries(entries.filter(([id]) => id).map(([id, snapshot]) => [id, snapshot])),
|
|
1718
|
+
refreshing: metas.some(meta => meta.refreshing),
|
|
1719
|
+
updatedAt,
|
|
1720
|
+
stale: metas.some(meta => meta.stale)
|
|
1681
1721
|
};
|
|
1682
1722
|
}
|
|
1683
1723
|
|
|
@@ -1695,7 +1735,12 @@ async function refreshChannelBalance(source, channelId) {
|
|
|
1695
1735
|
throw error;
|
|
1696
1736
|
}
|
|
1697
1737
|
|
|
1698
|
-
const
|
|
1738
|
+
const entry = await refreshSnapshot(
|
|
1739
|
+
buildBalanceSnapshotKey(normalizedSource, channel),
|
|
1740
|
+
() => refreshChannelBalanceSnapshot(normalizedSource, channel, { force: true }),
|
|
1741
|
+
{ bypassInflight: true }
|
|
1742
|
+
);
|
|
1743
|
+
const balance = entry.value;
|
|
1699
1744
|
return {
|
|
1700
1745
|
enabled: true,
|
|
1701
1746
|
source: normalizedSource,
|
|
@@ -1707,6 +1752,7 @@ async function refreshChannelBalance(source, channelId) {
|
|
|
1707
1752
|
function clearBalanceCache() {
|
|
1708
1753
|
balanceCache.clear();
|
|
1709
1754
|
balanceStrategyCache.clear();
|
|
1755
|
+
invalidateSnapshot('channel-balances:');
|
|
1710
1756
|
strategyCacheLoaded = false;
|
|
1711
1757
|
}
|
|
1712
1758
|
|
|
@@ -1739,6 +1785,7 @@ module.exports = {
|
|
|
1739
1785
|
build88CodeSubscriptionSnapshot,
|
|
1740
1786
|
buildNewCliDashboardSnapshot,
|
|
1741
1787
|
buildBalanceProbeStrategies,
|
|
1788
|
+
buildBalanceSnapshotKey,
|
|
1742
1789
|
getEnabledBalanceChannels,
|
|
1743
1790
|
runBalanceStrategy,
|
|
1744
1791
|
refreshChannelBalanceSnapshot,
|
|
@@ -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
|
|