coding-tool-x 3.7.3 → 3.7.4
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-BmPIW7oK.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-DV2KFuC4.js → ConfigTemplates-CG4j4ua4.js} +1 -1
- package/dist/web/assets/Home-B9yAAZrD.js +1 -0
- package/dist/web/assets/Home-DSTUSxrT.css +1 -0
- package/dist/web/assets/{PluginManager-Bd3hhc5A.js → PluginManager-CCJ7mo35.js} +1 -1
- package/dist/web/assets/{ProjectList-BB4Whns8.js → ProjectList-DVUs3o8x.js} +1 -1
- package/dist/web/assets/SessionList-DwssBydw.js +1 -0
- package/dist/web/assets/{SkillManager-DQjRXOFg.js → SkillManager-DmOTHcQ7.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-CZeXnxu1.js → WorkspaceManager-DLVj0zlu.js} +1 -1
- package/dist/web/assets/index-BjdNYwSs.js +2 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/src/config/paths.js +2 -0
- package/src/server/api/codex-projects.js +25 -8
- package/src/server/api/dashboard.js +167 -75
- package/src/server/api/gemini-projects.js +23 -7
- package/src/server/api/omp-projects.js +23 -6
- package/src/server/api/opencode-projects.js +23 -7
- package/src/server/api/projects.js +38 -21
- package/src/server/services/channel-balance.js +51 -5
- package/src/server/services/project-snapshots.js +52 -0
- package/src/server/services/snapshot-cache.js +295 -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/index-BovYCFsd.js +0 -2
package/dist/web/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<link rel="icon" href="/favicon.ico">
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
7
|
<title>coding-tool-x · AI 编程工作台</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-BjdNYwSs.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/markdown-BNJfEgCA.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vue-vendor-B5RqjEjQ.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/naive-ui-CVyvdmg1.js">
|
package/package.json
CHANGED
package/src/config/paths.js
CHANGED
|
@@ -432,6 +432,8 @@ const PATHS = {
|
|
|
432
432
|
},
|
|
433
433
|
|
|
434
434
|
// 运行时、缓存、备份
|
|
435
|
+
cache: CACHE_DIR,
|
|
436
|
+
snapshotCache: path.join(CACHE_DIR, 'snapshots'),
|
|
435
437
|
sessionCache: path.join(CACHE_DIR, 'session-cache.json'),
|
|
436
438
|
sessionHasCache: path.join(CACHE_DIR, 'session-has-cache.json'),
|
|
437
439
|
channelModels: path.join(CACHE_DIR, 'channel-models.json'),
|
|
@@ -2,6 +2,11 @@ const express = require('express');
|
|
|
2
2
|
const router = express.Router();
|
|
3
3
|
const { getProjects, saveProjectOrder, deleteProject } = require('../services/codex-sessions');
|
|
4
4
|
const { isCodexInstalled } = require('../services/codex-config');
|
|
5
|
+
const {
|
|
6
|
+
emptyProjectList,
|
|
7
|
+
getProjectListSnapshot,
|
|
8
|
+
invalidateProjectSnapshots
|
|
9
|
+
} = require('../services/project-snapshots');
|
|
5
10
|
|
|
6
11
|
const DEBUG_CODEX_PERF = process.env.DEBUG_CODEX_PERF === '1';
|
|
7
12
|
|
|
@@ -13,11 +18,19 @@ function logPerf(route, startMs, detail = '') {
|
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
module.exports = (config) => {
|
|
21
|
+
function buildProjectsPayload() {
|
|
22
|
+
const projects = getProjects();
|
|
23
|
+
return {
|
|
24
|
+
projects,
|
|
25
|
+
currentProject: projects[0] ? projects[0].name : null
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
16
29
|
/**
|
|
17
30
|
* GET /api/codex/projects
|
|
18
31
|
* 获取所有 Codex 项目列表
|
|
19
32
|
*/
|
|
20
|
-
router.get('/', (req, res) => {
|
|
33
|
+
router.get('/', async (req, res) => {
|
|
21
34
|
const startMs = Date.now();
|
|
22
35
|
try {
|
|
23
36
|
// 检查 Codex 是否安装
|
|
@@ -26,17 +39,19 @@ module.exports = (config) => {
|
|
|
26
39
|
return res.json({
|
|
27
40
|
projects: [],
|
|
28
41
|
currentProject: null,
|
|
29
|
-
error: 'Codex CLI not installed or not found'
|
|
42
|
+
error: 'Codex CLI not installed or not found',
|
|
43
|
+
meta: { generatedAt: new Date().toISOString(), stale: false, refreshing: false, fallback: true, error: null }
|
|
30
44
|
});
|
|
31
45
|
}
|
|
32
46
|
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
projects,
|
|
38
|
-
currentProject: projects[0] ? projects[0].name : null
|
|
47
|
+
const snapshot = await getProjectListSnapshot('codex', {
|
|
48
|
+
fallbackValue: emptyProjectList(null),
|
|
49
|
+
force: req.query?.fresh === '1',
|
|
50
|
+
refresh: buildProjectsPayload
|
|
39
51
|
});
|
|
52
|
+
logPerf('GET /api/codex/projects', startMs, `projects=${snapshot.value.projects.length} stale=${snapshot.meta.stale}`);
|
|
53
|
+
|
|
54
|
+
res.json({ ...snapshot.value, meta: snapshot.meta });
|
|
40
55
|
} catch (err) {
|
|
41
56
|
console.error('[Codex API] Failed to get projects:', err);
|
|
42
57
|
logPerf('GET /api/codex/projects', startMs, `error=${err.message}`);
|
|
@@ -72,6 +87,7 @@ module.exports = (config) => {
|
|
|
72
87
|
}
|
|
73
88
|
|
|
74
89
|
saveProjectOrder(order);
|
|
90
|
+
invalidateProjectSnapshots('codex');
|
|
75
91
|
|
|
76
92
|
res.json({ success: true });
|
|
77
93
|
} catch (err) {
|
|
@@ -92,6 +108,7 @@ module.exports = (config) => {
|
|
|
92
108
|
|
|
93
109
|
const { projectName } = req.params;
|
|
94
110
|
const result = deleteProject(projectName);
|
|
111
|
+
invalidateProjectSnapshots('codex');
|
|
95
112
|
|
|
96
113
|
res.json(result);
|
|
97
114
|
} catch (err) {
|
|
@@ -29,6 +29,108 @@ const { getTodayStatistics: getCodexTodayStatistics } = require('../services/cod
|
|
|
29
29
|
const { getTodayStatistics: getGeminiTodayStatistics } = require('../services/gemini-statistics-service');
|
|
30
30
|
const { getTodayStatistics: getOpenCodeTodayStatistics } = require('../services/opencode-statistics-service');
|
|
31
31
|
const { getTodayStatistics: getOmpTodayStatistics } = require('../services/omp-statistics-service');
|
|
32
|
+
const { getSnapshot } = require('../services/snapshot-cache');
|
|
33
|
+
const { projectCountKey } = require('../services/project-snapshots');
|
|
34
|
+
|
|
35
|
+
const SNAPSHOT_TTL_MS = 60 * 1000;
|
|
36
|
+
const EMPTY_COUNTS = { projectCount: 0, sessionCount: 0 };
|
|
37
|
+
const EMPTY_STATS = { requests: 0, tokens: 0, cost: 0, byModel: {}, byChannel: {} };
|
|
38
|
+
|
|
39
|
+
function safeRead(label, reader, fallback) {
|
|
40
|
+
try {
|
|
41
|
+
return reader();
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.warn(`[Dashboard] ${label} failed:`, error.message);
|
|
44
|
+
return fallback;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function channelSnapshotKey(source) {
|
|
49
|
+
return `dashboard:channels:${source}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function todayStatsSnapshotKey(source) {
|
|
53
|
+
return `dashboard:today-stats:${source}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeChannelTokens(tokens) {
|
|
57
|
+
if (tokens && typeof tokens === 'object') {
|
|
58
|
+
return Number(tokens.total ?? tokens.input ?? 0) || 0;
|
|
59
|
+
}
|
|
60
|
+
return Number(tokens) || 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeByChannel(byChannel = {}) {
|
|
64
|
+
if (!byChannel || typeof byChannel !== 'object') {
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return Object.fromEntries(Object.entries(byChannel).map(([channelId, item = {}]) => [
|
|
69
|
+
channelId,
|
|
70
|
+
{
|
|
71
|
+
...item,
|
|
72
|
+
requests: Number(item.requests) || 0,
|
|
73
|
+
tokens: normalizeChannelTokens(item.tokens),
|
|
74
|
+
cost: Number(item.cost) || 0
|
|
75
|
+
}
|
|
76
|
+
]));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 格式化统计数据:取 summary、byModel 和 byChannel 中的数据
|
|
80
|
+
function formatStats(stats) {
|
|
81
|
+
if (stats && stats.summary) {
|
|
82
|
+
return {
|
|
83
|
+
requests: stats.summary.requests || 0,
|
|
84
|
+
tokens: normalizeChannelTokens(stats.summary.tokens),
|
|
85
|
+
cost: stats.summary.cost || 0,
|
|
86
|
+
byModel: stats.byModel || {},
|
|
87
|
+
byChannel: normalizeByChannel(stats.byChannel || {})
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return { ...EMPTY_STATS };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function readDashboardSnapshot(key, fallbackValue, refresh) {
|
|
94
|
+
return getSnapshot(key, {
|
|
95
|
+
ttlMs: SNAPSHOT_TTL_MS,
|
|
96
|
+
fallbackValue,
|
|
97
|
+
refresh
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function summarizePart(partSnapshots = {}) {
|
|
102
|
+
const items = {};
|
|
103
|
+
let stale = false;
|
|
104
|
+
let refreshing = false;
|
|
105
|
+
let generatedAt = null;
|
|
106
|
+
|
|
107
|
+
Object.entries(partSnapshots).forEach(([source, snapshot]) => {
|
|
108
|
+
const meta = snapshot.meta || {};
|
|
109
|
+
items[source] = meta;
|
|
110
|
+
stale = stale || Boolean(meta.stale);
|
|
111
|
+
refreshing = refreshing || Boolean(meta.refreshing);
|
|
112
|
+
if (meta.generatedAt && (!generatedAt || meta.generatedAt > generatedAt)) {
|
|
113
|
+
generatedAt = meta.generatedAt;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
generatedAt,
|
|
119
|
+
stale,
|
|
120
|
+
refreshing,
|
|
121
|
+
items
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function dashboardMeta(parts) {
|
|
126
|
+
const partValues = Object.values(parts);
|
|
127
|
+
return {
|
|
128
|
+
generatedAt: new Date().toISOString(),
|
|
129
|
+
stale: partValues.some(part => part.stale),
|
|
130
|
+
refreshing: partValues.some(part => part.refreshing),
|
|
131
|
+
parts
|
|
132
|
+
};
|
|
133
|
+
}
|
|
32
134
|
|
|
33
135
|
/**
|
|
34
136
|
* GET /api/dashboard/init
|
|
@@ -38,20 +140,22 @@ router.get('/init', async (req, res) => {
|
|
|
38
140
|
try {
|
|
39
141
|
const config = loadConfig();
|
|
40
142
|
|
|
41
|
-
|
|
143
|
+
const uiConfig = safeRead('uiConfig', () => loadUIConfig(), {});
|
|
144
|
+
const favorites = safeRead('favorites', () => loadFavorites(), {});
|
|
145
|
+
const proxyStatus = {
|
|
146
|
+
claude: safeRead('claude proxy status', () => getProxyStatus(), {}),
|
|
147
|
+
codex: safeRead('codex proxy status', () => getCodexProxyStatus(), {}),
|
|
148
|
+
gemini: safeRead('gemini proxy status', () => getGeminiProxyStatus(), {}),
|
|
149
|
+
opencode: safeRead('opencode proxy status', () => getOpenCodeProxyStatus(), {}),
|
|
150
|
+
omp: safeRead('omp proxy status', () => getOmpProxyStatus(), {})
|
|
151
|
+
};
|
|
152
|
+
|
|
42
153
|
const [
|
|
43
|
-
uiConfig,
|
|
44
|
-
favorites,
|
|
45
154
|
claudeChannels,
|
|
46
155
|
codexChannels,
|
|
47
156
|
geminiChannels,
|
|
48
157
|
opencodeChannels,
|
|
49
158
|
ompChannels,
|
|
50
|
-
claudeProxyStatus,
|
|
51
|
-
codexProxyStatus,
|
|
52
|
-
geminiProxyStatus,
|
|
53
|
-
opencodeProxyStatus,
|
|
54
|
-
ompProxyStatus,
|
|
55
159
|
claudeTodayStats,
|
|
56
160
|
codexTodayStats,
|
|
57
161
|
geminiTodayStats,
|
|
@@ -63,52 +167,45 @@ router.get('/init', async (req, res) => {
|
|
|
63
167
|
opencodeCounts,
|
|
64
168
|
ompCounts
|
|
65
169
|
] = await Promise.all([
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
Promise.resolve(getCodexProxyStatus()),
|
|
82
|
-
Promise.resolve(getGeminiProxyStatus()),
|
|
83
|
-
Promise.resolve(getOpenCodeProxyStatus()),
|
|
84
|
-
Promise.resolve(getOmpProxyStatus()),
|
|
85
|
-
|
|
86
|
-
// Today Stats (所有平台)
|
|
87
|
-
Promise.resolve(getClaudeTodayStatistics()),
|
|
88
|
-
Promise.resolve(getCodexTodayStatistics()),
|
|
89
|
-
Promise.resolve(getGeminiTodayStatistics()),
|
|
90
|
-
Promise.resolve(getOpenCodeTodayStatistics()),
|
|
91
|
-
Promise.resolve(getOmpTodayStatistics()),
|
|
92
|
-
|
|
93
|
-
// 轻量级统计
|
|
94
|
-
Promise.resolve(getClaudeCounts(config)),
|
|
95
|
-
Promise.resolve(getCodexCounts()),
|
|
96
|
-
Promise.resolve(getGeminiCounts()),
|
|
97
|
-
Promise.resolve(getOpenCodeCounts()),
|
|
98
|
-
Promise.resolve(getOmpCounts())
|
|
170
|
+
readDashboardSnapshot(channelSnapshotKey('claude'), [], () => getAllChannels()),
|
|
171
|
+
readDashboardSnapshot(channelSnapshotKey('codex'), { channels: [] }, () => getCodexChannels()),
|
|
172
|
+
readDashboardSnapshot(channelSnapshotKey('gemini'), { channels: [] }, () => getGeminiChannels()),
|
|
173
|
+
readDashboardSnapshot(channelSnapshotKey('opencode'), { channels: [] }, () => getOpenCodeChannels()),
|
|
174
|
+
readDashboardSnapshot(channelSnapshotKey('omp'), { channels: [] }, () => getOmpChannels()),
|
|
175
|
+
readDashboardSnapshot(todayStatsSnapshotKey('claude'), null, () => getClaudeTodayStatistics()),
|
|
176
|
+
readDashboardSnapshot(todayStatsSnapshotKey('codex'), null, () => getCodexTodayStatistics()),
|
|
177
|
+
readDashboardSnapshot(todayStatsSnapshotKey('gemini'), null, () => getGeminiTodayStatistics()),
|
|
178
|
+
readDashboardSnapshot(todayStatsSnapshotKey('opencode'), null, () => getOpenCodeTodayStatistics()),
|
|
179
|
+
readDashboardSnapshot(todayStatsSnapshotKey('omp'), null, () => getOmpTodayStatistics()),
|
|
180
|
+
readDashboardSnapshot(projectCountKey('claude'), EMPTY_COUNTS, () => getClaudeCounts(config)),
|
|
181
|
+
readDashboardSnapshot(projectCountKey('codex'), EMPTY_COUNTS, () => getCodexCounts()),
|
|
182
|
+
readDashboardSnapshot(projectCountKey('gemini'), EMPTY_COUNTS, () => getGeminiCounts()),
|
|
183
|
+
readDashboardSnapshot(projectCountKey('opencode'), EMPTY_COUNTS, () => getOpenCodeCounts()),
|
|
184
|
+
readDashboardSnapshot(projectCountKey('omp'), EMPTY_COUNTS, () => getOmpCounts())
|
|
99
185
|
]);
|
|
100
186
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
187
|
+
const parts = {
|
|
188
|
+
counts: summarizePart({
|
|
189
|
+
claude: claudeCounts,
|
|
190
|
+
codex: codexCounts,
|
|
191
|
+
gemini: geminiCounts,
|
|
192
|
+
opencode: opencodeCounts,
|
|
193
|
+
omp: ompCounts
|
|
194
|
+
}),
|
|
195
|
+
todayStats: summarizePart({
|
|
196
|
+
claude: claudeTodayStats,
|
|
197
|
+
codex: codexTodayStats,
|
|
198
|
+
gemini: geminiTodayStats,
|
|
199
|
+
opencode: opencodeTodayStats,
|
|
200
|
+
omp: ompTodayStats
|
|
201
|
+
}),
|
|
202
|
+
channels: summarizePart({
|
|
203
|
+
claude: claudeChannels,
|
|
204
|
+
codex: codexChannels,
|
|
205
|
+
gemini: geminiChannels,
|
|
206
|
+
opencode: opencodeChannels,
|
|
207
|
+
omp: ompChannels
|
|
208
|
+
})
|
|
112
209
|
};
|
|
113
210
|
|
|
114
211
|
res.json({
|
|
@@ -117,33 +214,28 @@ router.get('/init', async (req, res) => {
|
|
|
117
214
|
uiConfig,
|
|
118
215
|
favorites,
|
|
119
216
|
channels: {
|
|
120
|
-
claude: claudeChannels,
|
|
121
|
-
codex: codexChannels,
|
|
122
|
-
gemini: geminiChannels,
|
|
123
|
-
opencode: opencodeChannels.channels || [],
|
|
124
|
-
omp: ompChannels.channels || []
|
|
125
|
-
},
|
|
126
|
-
proxyStatus: {
|
|
127
|
-
claude: claudeProxyStatus,
|
|
128
|
-
codex: codexProxyStatus,
|
|
129
|
-
gemini: geminiProxyStatus,
|
|
130
|
-
opencode: opencodeProxyStatus,
|
|
131
|
-
omp: ompProxyStatus
|
|
217
|
+
claude: claudeChannels.value,
|
|
218
|
+
codex: codexChannels.value,
|
|
219
|
+
gemini: geminiChannels.value,
|
|
220
|
+
opencode: opencodeChannels.value.channels || [],
|
|
221
|
+
omp: ompChannels.value.channels || []
|
|
132
222
|
},
|
|
223
|
+
proxyStatus,
|
|
133
224
|
counts: {
|
|
134
|
-
claude: claudeCounts ||
|
|
135
|
-
codex: codexCounts ||
|
|
136
|
-
gemini: geminiCounts ||
|
|
137
|
-
opencode: opencodeCounts ||
|
|
138
|
-
omp: ompCounts ||
|
|
225
|
+
claude: claudeCounts.value || EMPTY_COUNTS,
|
|
226
|
+
codex: codexCounts.value || EMPTY_COUNTS,
|
|
227
|
+
gemini: geminiCounts.value || EMPTY_COUNTS,
|
|
228
|
+
opencode: opencodeCounts.value || EMPTY_COUNTS,
|
|
229
|
+
omp: ompCounts.value || EMPTY_COUNTS
|
|
139
230
|
},
|
|
140
231
|
todayStats: {
|
|
141
|
-
claude: formatStats(claudeTodayStats),
|
|
142
|
-
codex: formatStats(codexTodayStats),
|
|
143
|
-
gemini: formatStats(geminiTodayStats),
|
|
144
|
-
opencode: formatStats(opencodeTodayStats),
|
|
145
|
-
omp: formatStats(ompTodayStats)
|
|
146
|
-
}
|
|
232
|
+
claude: formatStats(claudeTodayStats.value),
|
|
233
|
+
codex: formatStats(codexTodayStats.value),
|
|
234
|
+
gemini: formatStats(geminiTodayStats.value),
|
|
235
|
+
opencode: formatStats(opencodeTodayStats.value),
|
|
236
|
+
omp: formatStats(ompTodayStats.value)
|
|
237
|
+
},
|
|
238
|
+
meta: dashboardMeta(parts)
|
|
147
239
|
}
|
|
148
240
|
});
|
|
149
241
|
} catch (error) {
|
|
@@ -2,29 +2,43 @@ const express = require('express');
|
|
|
2
2
|
const router = express.Router();
|
|
3
3
|
const { getProjects, saveProjectOrder, deleteProject } = require('../services/gemini-sessions');
|
|
4
4
|
const { isGeminiInstalled } = require('../services/gemini-config');
|
|
5
|
+
const {
|
|
6
|
+
emptyProjectList,
|
|
7
|
+
getProjectListSnapshot,
|
|
8
|
+
invalidateProjectSnapshots
|
|
9
|
+
} = require('../services/project-snapshots');
|
|
5
10
|
|
|
6
11
|
module.exports = (config) => {
|
|
12
|
+
function buildProjectsPayload() {
|
|
13
|
+
const projects = getProjects();
|
|
14
|
+
return {
|
|
15
|
+
projects,
|
|
16
|
+
currentProject: projects[0] ? projects[0].name : null
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
7
20
|
/**
|
|
8
21
|
* GET /api/gemini/projects
|
|
9
22
|
* 获取所有 Gemini 项目列表
|
|
10
23
|
*/
|
|
11
|
-
router.get('/', (req, res) => {
|
|
24
|
+
router.get('/', async (req, res) => {
|
|
12
25
|
try {
|
|
13
26
|
// 检查 Gemini 是否安装
|
|
14
27
|
if (!isGeminiInstalled()) {
|
|
15
28
|
return res.json({
|
|
16
29
|
projects: [],
|
|
17
30
|
currentProject: null,
|
|
18
|
-
error: 'Gemini CLI not installed or not found'
|
|
31
|
+
error: 'Gemini CLI not installed or not found',
|
|
32
|
+
meta: { generatedAt: new Date().toISOString(), stale: false, refreshing: false, fallback: true, error: null }
|
|
19
33
|
});
|
|
20
34
|
}
|
|
21
35
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
currentProject: projects[0] ? projects[0].name : null
|
|
36
|
+
const snapshot = await getProjectListSnapshot('gemini', {
|
|
37
|
+
fallbackValue: emptyProjectList(null),
|
|
38
|
+
force: req.query?.fresh === '1',
|
|
39
|
+
refresh: buildProjectsPayload
|
|
27
40
|
});
|
|
41
|
+
res.json({ ...snapshot.value, meta: snapshot.meta });
|
|
28
42
|
} catch (err) {
|
|
29
43
|
console.error('[Gemini API] Failed to get projects:', err);
|
|
30
44
|
|
|
@@ -59,6 +73,7 @@ module.exports = (config) => {
|
|
|
59
73
|
}
|
|
60
74
|
|
|
61
75
|
saveProjectOrder(order);
|
|
76
|
+
invalidateProjectSnapshots('gemini');
|
|
62
77
|
|
|
63
78
|
res.json({ success: true });
|
|
64
79
|
} catch (err) {
|
|
@@ -79,6 +94,7 @@ module.exports = (config) => {
|
|
|
79
94
|
|
|
80
95
|
const { projectHash } = req.params;
|
|
81
96
|
const result = deleteProject(projectHash);
|
|
97
|
+
invalidateProjectSnapshots('gemini');
|
|
82
98
|
|
|
83
99
|
res.json(result);
|
|
84
100
|
} catch (err) {
|
|
@@ -6,27 +6,42 @@ const {
|
|
|
6
6
|
deleteProject,
|
|
7
7
|
isOmpInstalled
|
|
8
8
|
} = require('../services/omp-sessions');
|
|
9
|
+
const {
|
|
10
|
+
emptyProjectList,
|
|
11
|
+
getProjectListSnapshot,
|
|
12
|
+
invalidateProjectSnapshots
|
|
13
|
+
} = require('../services/project-snapshots');
|
|
9
14
|
|
|
10
15
|
function isNotFoundError(error) {
|
|
11
16
|
return !!(error && error.message === 'Project not found');
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
module.exports = () => {
|
|
15
|
-
|
|
20
|
+
function buildProjectsPayload() {
|
|
21
|
+
const projects = getProjects();
|
|
22
|
+
return {
|
|
23
|
+
projects,
|
|
24
|
+
currentProject: projects[0] ? projects[0].name : null
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
router.get('/', async (req, res) => {
|
|
16
29
|
try {
|
|
17
30
|
if (!isOmpInstalled()) {
|
|
18
31
|
return res.json({
|
|
19
32
|
projects: [],
|
|
20
33
|
currentProject: null,
|
|
21
|
-
error: 'OMP CLI not installed or not found'
|
|
34
|
+
error: 'OMP CLI not installed or not found',
|
|
35
|
+
meta: { generatedAt: new Date().toISOString(), stale: false, refreshing: false, fallback: true, error: null }
|
|
22
36
|
});
|
|
23
37
|
}
|
|
24
38
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
39
|
+
const snapshot = await getProjectListSnapshot('omp', {
|
|
40
|
+
fallbackValue: emptyProjectList(null),
|
|
41
|
+
force: req.query?.fresh === '1',
|
|
42
|
+
refresh: buildProjectsPayload
|
|
29
43
|
});
|
|
44
|
+
res.json({ ...snapshot.value, meta: snapshot.meta });
|
|
30
45
|
} catch (err) {
|
|
31
46
|
console.error('[OMP API] Failed to get projects:', err);
|
|
32
47
|
if (err.code === 'ENOENT') {
|
|
@@ -49,6 +64,7 @@ module.exports = () => {
|
|
|
49
64
|
return res.status(400).json({ error: 'order must be an array' });
|
|
50
65
|
}
|
|
51
66
|
saveProjectOrder(order);
|
|
67
|
+
invalidateProjectSnapshots('omp');
|
|
52
68
|
res.json({ success: true });
|
|
53
69
|
} catch (err) {
|
|
54
70
|
console.error('[OMP API] Failed to save project order:', err);
|
|
@@ -62,6 +78,7 @@ module.exports = () => {
|
|
|
62
78
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
63
79
|
}
|
|
64
80
|
const result = deleteProject(req.params.projectName);
|
|
81
|
+
invalidateProjectSnapshots('omp');
|
|
65
82
|
res.json(result);
|
|
66
83
|
} catch (err) {
|
|
67
84
|
if (isNotFoundError(err)) {
|
|
@@ -6,32 +6,46 @@ const {
|
|
|
6
6
|
deleteProject,
|
|
7
7
|
isOpenCodeInstalled
|
|
8
8
|
} = require('../services/opencode-sessions');
|
|
9
|
+
const {
|
|
10
|
+
emptyProjectList,
|
|
11
|
+
getProjectListSnapshot,
|
|
12
|
+
invalidateProjectSnapshots
|
|
13
|
+
} = require('../services/project-snapshots');
|
|
9
14
|
|
|
10
15
|
function isNotFoundError(error) {
|
|
11
16
|
return !!(error && error.message === 'Project not found');
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
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
|
+
|
|
15
28
|
/**
|
|
16
29
|
* GET /api/opencode/projects
|
|
17
30
|
* 获取所有 OpenCode 项目列表
|
|
18
31
|
*/
|
|
19
|
-
router.get('/', (req, res) => {
|
|
32
|
+
router.get('/', async (req, res) => {
|
|
20
33
|
try {
|
|
21
34
|
if (!isOpenCodeInstalled()) {
|
|
22
35
|
return res.json({
|
|
23
36
|
projects: [],
|
|
24
37
|
currentProject: null,
|
|
25
|
-
error: 'OpenCode CLI not installed or not found'
|
|
38
|
+
error: 'OpenCode CLI not installed or not found',
|
|
39
|
+
meta: { generatedAt: new Date().toISOString(), stale: false, refreshing: false, fallback: true, error: null }
|
|
26
40
|
});
|
|
27
41
|
}
|
|
28
42
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
currentProject: projects[0] ? projects[0].name : null
|
|
43
|
+
const snapshot = await getProjectListSnapshot('opencode', {
|
|
44
|
+
fallbackValue: emptyProjectList(null),
|
|
45
|
+
force: req.query?.fresh === '1',
|
|
46
|
+
refresh: buildProjectsPayload
|
|
34
47
|
});
|
|
48
|
+
res.json({ ...snapshot.value, meta: snapshot.meta });
|
|
35
49
|
} catch (err) {
|
|
36
50
|
console.error('[OpenCode API] Failed to get projects:', err);
|
|
37
51
|
|
|
@@ -65,6 +79,7 @@ module.exports = (config) => {
|
|
|
65
79
|
}
|
|
66
80
|
|
|
67
81
|
saveProjectOrder(order);
|
|
82
|
+
invalidateProjectSnapshots('opencode');
|
|
68
83
|
res.json({ success: true });
|
|
69
84
|
} catch (err) {
|
|
70
85
|
console.error('[OpenCode API] Failed to save project order:', err);
|
|
@@ -84,6 +99,7 @@ module.exports = (config) => {
|
|
|
84
99
|
|
|
85
100
|
const { projectName } = req.params;
|
|
86
101
|
const result = deleteProject(projectName);
|
|
102
|
+
invalidateProjectSnapshots('opencode');
|
|
87
103
|
res.json(result);
|
|
88
104
|
} catch (err) {
|
|
89
105
|
if (isNotFoundError(err)) {
|
|
@@ -1,33 +1,47 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const router = express.Router();
|
|
3
3
|
const { getProjectsWithStats, saveProjectOrder, getProjectOrder, deleteProject } = require('../services/sessions');
|
|
4
|
+
const {
|
|
5
|
+
emptyProjectList,
|
|
6
|
+
getProjectListSnapshot,
|
|
7
|
+
invalidateProjectSnapshots
|
|
8
|
+
} = require('../services/project-snapshots');
|
|
4
9
|
|
|
5
10
|
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
|
+
|
|
6
36
|
// GET /api/projects - Get all projects with stats
|
|
7
37
|
router.get('/', async (req, res) => {
|
|
8
38
|
try {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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)
|
|
39
|
+
const snapshot = await getProjectListSnapshot('claude', {
|
|
40
|
+
fallbackValue: emptyProjectList(config.currentProject || null),
|
|
41
|
+
force: req.query?.fresh === '1',
|
|
42
|
+
refresh: buildProjectsPayload
|
|
30
43
|
});
|
|
44
|
+
res.json({ ...snapshot.value, meta: snapshot.meta });
|
|
31
45
|
} catch (error) {
|
|
32
46
|
console.error('Error fetching projects:', error);
|
|
33
47
|
res.status(500).json({ error: error.message });
|
|
@@ -42,6 +56,7 @@ module.exports = (config) => {
|
|
|
42
56
|
return res.status(400).json({ error: 'Order must be an array' });
|
|
43
57
|
}
|
|
44
58
|
saveProjectOrder(config, order);
|
|
59
|
+
invalidateProjectSnapshots('claude');
|
|
45
60
|
res.json({ success: true });
|
|
46
61
|
} catch (error) {
|
|
47
62
|
console.error('Error saving project order:', error);
|
|
@@ -96,6 +111,7 @@ module.exports = (config) => {
|
|
|
96
111
|
'utf8'
|
|
97
112
|
);
|
|
98
113
|
|
|
114
|
+
invalidateProjectSnapshots('claude');
|
|
99
115
|
res.json({
|
|
100
116
|
success: true,
|
|
101
117
|
projectName,
|
|
@@ -113,6 +129,7 @@ module.exports = (config) => {
|
|
|
113
129
|
try {
|
|
114
130
|
const { projectName } = req.params;
|
|
115
131
|
deleteProject(config, projectName);
|
|
132
|
+
invalidateProjectSnapshots('claude');
|
|
116
133
|
res.json({ success: true });
|
|
117
134
|
} catch (error) {
|
|
118
135
|
console.error('Error deleting project:', error);
|