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.
Files changed (44) hide show
  1. package/dist/web/assets/{Analytics-BqDjJUJl.js → Analytics-DnnufrQi.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-DV2KFuC4.js → ConfigTemplates-BTVhMSfe.js} +1 -1
  3. package/dist/web/assets/Home-CqnBXFxj.css +1 -0
  4. package/dist/web/assets/Home-D03PEREx.js +1 -0
  5. package/dist/web/assets/{PluginManager-Bd3hhc5A.js → PluginManager-BiMqEiyQ.js} +1 -1
  6. package/dist/web/assets/{ProjectList-BB4Whns8.js → ProjectList-C6BA3V49.js} +1 -1
  7. package/dist/web/assets/{ProjectList-D3a0oRum.css → ProjectList-DQ5ZCJO2.css} +1 -1
  8. package/dist/web/assets/SessionList--9Bl6EhV.css +1 -0
  9. package/dist/web/assets/SessionList-DChG04jz.js +1 -0
  10. package/dist/web/assets/{SkillManager-DQjRXOFg.js → SkillManager-DAy8k5CL.js} +1 -1
  11. package/dist/web/assets/{WorkspaceManager-CZeXnxu1.js → WorkspaceManager-Bs2D6R9j.js} +1 -1
  12. package/dist/web/assets/index-DnDBHGCC.js +2 -0
  13. package/dist/web/assets/index-U3TOywtB.css +1 -0
  14. package/dist/web/index.html +2 -2
  15. package/package.json +1 -1
  16. package/src/config/paths.js +2 -0
  17. package/src/server/api/codex-projects.js +22 -9
  18. package/src/server/api/codex-sessions.js +30 -21
  19. package/src/server/api/dashboard.js +174 -94
  20. package/src/server/api/gemini-projects.js +20 -8
  21. package/src/server/api/gemini-sessions.js +28 -26
  22. package/src/server/api/omp-projects.js +19 -7
  23. package/src/server/api/omp-sessions.js +31 -17
  24. package/src/server/api/opencode-projects.js +19 -8
  25. package/src/server/api/opencode-sessions.js +28 -21
  26. package/src/server/api/projects.js +18 -22
  27. package/src/server/api/sessions.js +32 -16
  28. package/src/server/services/channel-balance.js +52 -5
  29. package/src/server/services/codex-sessions.js +23 -10
  30. package/src/server/services/dashboard-snapshot-worker.js +216 -0
  31. package/src/server/services/gemini-sessions.js +15 -3
  32. package/src/server/services/omp-sessions.js +31 -11
  33. package/src/server/services/opencode-sessions.js +9 -6
  34. package/src/server/services/project-snapshots.js +52 -0
  35. package/src/server/services/session-snapshot-worker.js +158 -0
  36. package/src/server/services/session-snapshots.js +137 -0
  37. package/src/server/services/sessions.js +6 -4
  38. package/src/server/services/snapshot-cache.js +361 -0
  39. package/dist/web/assets/Home-D7hovkk9.js +0 -1
  40. package/dist/web/assets/Home-Jom_ZcdY.css +0 -1
  41. package/dist/web/assets/SessionList-CvurKSRK.js +0 -1
  42. package/dist/web/assets/SessionList-D12muXvx.css +0 -1
  43. package/dist/web/assets/index-BovYCFsd.js +0 -2
  44. 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 projects = getProjects();
30
-
31
- res.json({
32
- projects,
33
- currentProject: projects[0] ? projects[0].name : null
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 sessions = getSessionsByProject(projectName);
128
- const aliases = loadAliases();
129
- const projects = getProjects();
130
- const project = projects.find(p => p.name === projectName);
131
-
132
- // 计算总大小
133
- const totalSize = sessions.reduce((sum, session) => {
134
- return sum + (session.size || 0);
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 { getProjectsWithStats, saveProjectOrder, getProjectOrder, deleteProject } = require('../services/sessions');
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 projects = await getProjectsWithStats(config);
10
- const order = getProjectOrder(config);
11
-
12
- // Sort projects by saved order
13
- let sortedProjects = projects;
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 { getSessionsForProject, deleteSession, forkSession, saveSessionOrder, parseRealProjectPath, searchSessions, getRecentSessions, searchSessionsAcrossProjects, hasActualMessages } = require('../services/sessions');
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 result = await getSessionsForProject(config, projectName);
162
- const aliases = loadAliases();
163
-
164
- // Parse project path info
165
- const { fullPath, projectName: displayName } = parseRealProjectPath(projectName);
166
-
167
- res.json({
168
- sessions: result.sessions,
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 { enabled: false, source: normalizedSource, balances: {} };
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 refreshChannelBalanceSnapshot(normalizedSource, channel, options);
1674
- return [channel.id, snapshot];
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 balance = await refreshChannelBalanceSnapshot(normalizedSource, channel, { force: true });
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
- const cached = globalCache.get(CODEX_PROJECTS_CACHE_KEY);
232
- if (cached) {
233
- return cached;
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
- const cached = globalCache.get(cacheKey);
308
- if (cached) {
309
- return cached;
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 (countsCache.expiresAt > now) {
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