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
|
@@ -5,30 +5,119 @@ const { loadConfig } = require('../../config/loader');
|
|
|
5
5
|
// Services
|
|
6
6
|
const { loadUIConfig } = require('../services/ui-config');
|
|
7
7
|
const { loadFavorites } = require('../services/favorites');
|
|
8
|
-
const { getAllChannels } = require('../services/channels');
|
|
9
8
|
const { getProxyStatus } = require('../proxy-server');
|
|
10
9
|
const { getCodexProxyStatus } = require('../codex-proxy-server');
|
|
11
10
|
const { getGeminiProxyStatus } = require('../gemini-proxy-server');
|
|
12
11
|
const { getOpenCodeProxyStatus } = require('../opencode-proxy-server');
|
|
13
12
|
const { getOmpProxyStatus } = require('../omp-proxy-server');
|
|
14
|
-
const {
|
|
15
|
-
const {
|
|
16
|
-
const {
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
13
|
+
const { getSnapshot } = require('../services/snapshot-cache');
|
|
14
|
+
const { projectCountKey } = require('../services/project-snapshots');
|
|
15
|
+
const { runDashboardSnapshotWorker } = require('../services/dashboard-snapshot-worker');
|
|
16
|
+
|
|
17
|
+
const SNAPSHOT_TTL_MS = 60 * 1000;
|
|
18
|
+
const DASHBOARD_SNAPSHOT_DEFER_MS = 0;
|
|
19
|
+
const EMPTY_COUNTS = { projectCount: 0, sessionCount: 0 };
|
|
20
|
+
const EMPTY_STATS = { requests: 0, tokens: 0, cost: 0, byModel: {}, byChannel: {} };
|
|
21
|
+
|
|
22
|
+
function safeRead(label, reader, fallback) {
|
|
23
|
+
try {
|
|
24
|
+
return reader();
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.warn(`[Dashboard] ${label} failed:`, error.message);
|
|
27
|
+
return fallback;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function channelSnapshotKey(source) {
|
|
32
|
+
return `dashboard:channels:${source}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function todayStatsSnapshotKey(source) {
|
|
36
|
+
return `dashboard:today-stats:${source}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeChannelTokens(tokens) {
|
|
40
|
+
if (tokens && typeof tokens === 'object') {
|
|
41
|
+
return Number(tokens.total ?? tokens.input ?? 0) || 0;
|
|
42
|
+
}
|
|
43
|
+
return Number(tokens) || 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeByChannel(byChannel = {}) {
|
|
47
|
+
if (!byChannel || typeof byChannel !== 'object') {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return Object.fromEntries(Object.entries(byChannel).map(([channelId, item = {}]) => [
|
|
52
|
+
channelId,
|
|
53
|
+
{
|
|
54
|
+
...item,
|
|
55
|
+
requests: Number(item.requests) || 0,
|
|
56
|
+
tokens: normalizeChannelTokens(item.tokens),
|
|
57
|
+
cost: Number(item.cost) || 0
|
|
58
|
+
}
|
|
59
|
+
]));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 格式化统计数据:取 summary、byModel 和 byChannel 中的数据
|
|
63
|
+
function formatStats(stats) {
|
|
64
|
+
if (stats && stats.summary) {
|
|
65
|
+
return {
|
|
66
|
+
requests: stats.summary.requests || 0,
|
|
67
|
+
tokens: normalizeChannelTokens(stats.summary.tokens),
|
|
68
|
+
cost: stats.summary.cost || 0,
|
|
69
|
+
byModel: stats.byModel || {},
|
|
70
|
+
byChannel: normalizeByChannel(stats.byChannel || {})
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return { ...EMPTY_STATS };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function readDashboardSnapshot(kind, source, key, fallbackValue, config = {}, options = {}) {
|
|
77
|
+
const force = options.force === true;
|
|
78
|
+
return getSnapshot(key, {
|
|
79
|
+
ttlMs: SNAPSHOT_TTL_MS,
|
|
80
|
+
fallbackValue,
|
|
81
|
+
force,
|
|
82
|
+
staleWhileForce: force,
|
|
83
|
+
deferMs: DASHBOARD_SNAPSHOT_DEFER_MS,
|
|
84
|
+
refresh: () => runDashboardSnapshotWorker(kind, source, config, { force })
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function summarizePart(partSnapshots = {}) {
|
|
89
|
+
const items = {};
|
|
90
|
+
let stale = false;
|
|
91
|
+
let refreshing = false;
|
|
92
|
+
let generatedAt = null;
|
|
93
|
+
|
|
94
|
+
Object.entries(partSnapshots).forEach(([source, snapshot]) => {
|
|
95
|
+
const meta = snapshot.meta || {};
|
|
96
|
+
items[source] = meta;
|
|
97
|
+
stale = stale || Boolean(meta.stale);
|
|
98
|
+
refreshing = refreshing || Boolean(meta.refreshing);
|
|
99
|
+
if (meta.generatedAt && (!generatedAt || meta.generatedAt > generatedAt)) {
|
|
100
|
+
generatedAt = meta.generatedAt;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
generatedAt,
|
|
106
|
+
stale,
|
|
107
|
+
refreshing,
|
|
108
|
+
items
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function dashboardMeta(parts) {
|
|
113
|
+
const partValues = Object.values(parts);
|
|
114
|
+
return {
|
|
115
|
+
generatedAt: new Date().toISOString(),
|
|
116
|
+
stale: partValues.some(part => part.stale),
|
|
117
|
+
refreshing: partValues.some(part => part.refreshing),
|
|
118
|
+
parts
|
|
119
|
+
};
|
|
120
|
+
}
|
|
32
121
|
|
|
33
122
|
/**
|
|
34
123
|
* GET /api/dashboard/init
|
|
@@ -37,21 +126,24 @@ const { getTodayStatistics: getOmpTodayStatistics } = require('../services/omp-s
|
|
|
37
126
|
router.get('/init', async (req, res) => {
|
|
38
127
|
try {
|
|
39
128
|
const config = loadConfig();
|
|
129
|
+
const force = req.query?.fresh === '1' || req.query?.force === '1';
|
|
130
|
+
|
|
131
|
+
const uiConfig = safeRead('uiConfig', () => loadUIConfig(), {});
|
|
132
|
+
const favorites = safeRead('favorites', () => loadFavorites(), {});
|
|
133
|
+
const proxyStatus = {
|
|
134
|
+
claude: safeRead('claude proxy status', () => getProxyStatus(), {}),
|
|
135
|
+
codex: safeRead('codex proxy status', () => getCodexProxyStatus(), {}),
|
|
136
|
+
gemini: safeRead('gemini proxy status', () => getGeminiProxyStatus(), {}),
|
|
137
|
+
opencode: safeRead('opencode proxy status', () => getOpenCodeProxyStatus(), {}),
|
|
138
|
+
omp: safeRead('omp proxy status', () => getOmpProxyStatus(), {})
|
|
139
|
+
};
|
|
40
140
|
|
|
41
|
-
// 并行获取所有数据
|
|
42
141
|
const [
|
|
43
|
-
uiConfig,
|
|
44
|
-
favorites,
|
|
45
142
|
claudeChannels,
|
|
46
143
|
codexChannels,
|
|
47
144
|
geminiChannels,
|
|
48
145
|
opencodeChannels,
|
|
49
146
|
ompChannels,
|
|
50
|
-
claudeProxyStatus,
|
|
51
|
-
codexProxyStatus,
|
|
52
|
-
geminiProxyStatus,
|
|
53
|
-
opencodeProxyStatus,
|
|
54
|
-
ompProxyStatus,
|
|
55
147
|
claudeTodayStats,
|
|
56
148
|
codexTodayStats,
|
|
57
149
|
geminiTodayStats,
|
|
@@ -63,52 +155,45 @@ router.get('/init', async (req, res) => {
|
|
|
63
155
|
opencodeCounts,
|
|
64
156
|
ompCounts
|
|
65
157
|
] = 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())
|
|
158
|
+
readDashboardSnapshot('channels', 'claude', channelSnapshotKey('claude'), [], config, { force }),
|
|
159
|
+
readDashboardSnapshot('channels', 'codex', channelSnapshotKey('codex'), { channels: [] }, config, { force }),
|
|
160
|
+
readDashboardSnapshot('channels', 'gemini', channelSnapshotKey('gemini'), { channels: [] }, config, { force }),
|
|
161
|
+
readDashboardSnapshot('channels', 'opencode', channelSnapshotKey('opencode'), { channels: [] }, config, { force }),
|
|
162
|
+
readDashboardSnapshot('channels', 'omp', channelSnapshotKey('omp'), { channels: [] }, config, { force }),
|
|
163
|
+
readDashboardSnapshot('todayStats', 'claude', todayStatsSnapshotKey('claude'), null, config, { force }),
|
|
164
|
+
readDashboardSnapshot('todayStats', 'codex', todayStatsSnapshotKey('codex'), null, config, { force }),
|
|
165
|
+
readDashboardSnapshot('todayStats', 'gemini', todayStatsSnapshotKey('gemini'), null, config, { force }),
|
|
166
|
+
readDashboardSnapshot('todayStats', 'opencode', todayStatsSnapshotKey('opencode'), null, config, { force }),
|
|
167
|
+
readDashboardSnapshot('todayStats', 'omp', todayStatsSnapshotKey('omp'), null, config, { force }),
|
|
168
|
+
readDashboardSnapshot('counts', 'claude', projectCountKey('claude'), EMPTY_COUNTS, config, { force }),
|
|
169
|
+
readDashboardSnapshot('counts', 'codex', projectCountKey('codex'), EMPTY_COUNTS, config, { force }),
|
|
170
|
+
readDashboardSnapshot('counts', 'gemini', projectCountKey('gemini'), EMPTY_COUNTS, config, { force }),
|
|
171
|
+
readDashboardSnapshot('counts', 'opencode', projectCountKey('opencode'), EMPTY_COUNTS, config, { force }),
|
|
172
|
+
readDashboardSnapshot('counts', 'omp', projectCountKey('omp'), EMPTY_COUNTS, config, { force })
|
|
99
173
|
]);
|
|
100
174
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
175
|
+
const parts = {
|
|
176
|
+
counts: summarizePart({
|
|
177
|
+
claude: claudeCounts,
|
|
178
|
+
codex: codexCounts,
|
|
179
|
+
gemini: geminiCounts,
|
|
180
|
+
opencode: opencodeCounts,
|
|
181
|
+
omp: ompCounts
|
|
182
|
+
}),
|
|
183
|
+
todayStats: summarizePart({
|
|
184
|
+
claude: claudeTodayStats,
|
|
185
|
+
codex: codexTodayStats,
|
|
186
|
+
gemini: geminiTodayStats,
|
|
187
|
+
opencode: opencodeTodayStats,
|
|
188
|
+
omp: ompTodayStats
|
|
189
|
+
}),
|
|
190
|
+
channels: summarizePart({
|
|
191
|
+
claude: claudeChannels,
|
|
192
|
+
codex: codexChannels,
|
|
193
|
+
gemini: geminiChannels,
|
|
194
|
+
opencode: opencodeChannels,
|
|
195
|
+
omp: ompChannels
|
|
196
|
+
})
|
|
112
197
|
};
|
|
113
198
|
|
|
114
199
|
res.json({
|
|
@@ -117,33 +202,28 @@ router.get('/init', async (req, res) => {
|
|
|
117
202
|
uiConfig,
|
|
118
203
|
favorites,
|
|
119
204
|
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
|
|
205
|
+
claude: claudeChannels.value,
|
|
206
|
+
codex: codexChannels.value,
|
|
207
|
+
gemini: geminiChannels.value,
|
|
208
|
+
opencode: opencodeChannels.value.channels || [],
|
|
209
|
+
omp: ompChannels.value.channels || []
|
|
132
210
|
},
|
|
211
|
+
proxyStatus,
|
|
133
212
|
counts: {
|
|
134
|
-
claude: claudeCounts ||
|
|
135
|
-
codex: codexCounts ||
|
|
136
|
-
gemini: geminiCounts ||
|
|
137
|
-
opencode: opencodeCounts ||
|
|
138
|
-
omp: ompCounts ||
|
|
213
|
+
claude: claudeCounts.value || EMPTY_COUNTS,
|
|
214
|
+
codex: codexCounts.value || EMPTY_COUNTS,
|
|
215
|
+
gemini: geminiCounts.value || EMPTY_COUNTS,
|
|
216
|
+
opencode: opencodeCounts.value || EMPTY_COUNTS,
|
|
217
|
+
omp: ompCounts.value || EMPTY_COUNTS
|
|
139
218
|
},
|
|
140
219
|
todayStats: {
|
|
141
|
-
claude: formatStats(claudeTodayStats),
|
|
142
|
-
codex: formatStats(codexTodayStats),
|
|
143
|
-
gemini: formatStats(geminiTodayStats),
|
|
144
|
-
opencode: formatStats(opencodeTodayStats),
|
|
145
|
-
omp: formatStats(ompTodayStats)
|
|
146
|
-
}
|
|
220
|
+
claude: formatStats(claudeTodayStats.value),
|
|
221
|
+
codex: formatStats(codexTodayStats.value),
|
|
222
|
+
gemini: formatStats(geminiTodayStats.value),
|
|
223
|
+
opencode: formatStats(opencodeTodayStats.value),
|
|
224
|
+
omp: formatStats(ompTodayStats.value)
|
|
225
|
+
},
|
|
226
|
+
meta: dashboardMeta(parts)
|
|
147
227
|
}
|
|
148
228
|
});
|
|
149
229
|
} catch (error) {
|
|
@@ -1,30 +1,39 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const router = express.Router();
|
|
3
|
-
const {
|
|
3
|
+
const { 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');
|
|
10
|
+
const { invalidateSessionSnapshots } = require('../services/session-snapshots');
|
|
11
|
+
const { runDashboardSnapshotWorker } = require('../services/dashboard-snapshot-worker');
|
|
5
12
|
|
|
6
13
|
module.exports = (config) => {
|
|
7
14
|
/**
|
|
8
15
|
* GET /api/gemini/projects
|
|
9
16
|
* 获取所有 Gemini 项目列表
|
|
10
17
|
*/
|
|
11
|
-
router.get('/', (req, res) => {
|
|
18
|
+
router.get('/', async (req, res) => {
|
|
12
19
|
try {
|
|
13
20
|
// 检查 Gemini 是否安装
|
|
14
21
|
if (!isGeminiInstalled()) {
|
|
15
22
|
return res.json({
|
|
16
23
|
projects: [],
|
|
17
24
|
currentProject: null,
|
|
18
|
-
error: 'Gemini CLI not installed or not found'
|
|
25
|
+
error: 'Gemini CLI not installed or not found',
|
|
26
|
+
meta: { generatedAt: new Date().toISOString(), stale: false, refreshing: false, fallback: true, error: null }
|
|
19
27
|
});
|
|
20
28
|
}
|
|
21
29
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
const force = req.query?.fresh === '1';
|
|
31
|
+
const snapshot = await getProjectListSnapshot('gemini', {
|
|
32
|
+
fallbackValue: emptyProjectList(null),
|
|
33
|
+
force,
|
|
34
|
+
refresh: () => runDashboardSnapshotWorker('projects', 'gemini', config, { force })
|
|
27
35
|
});
|
|
36
|
+
res.json({ ...snapshot.value, meta: snapshot.meta });
|
|
28
37
|
} catch (err) {
|
|
29
38
|
console.error('[Gemini API] Failed to get projects:', err);
|
|
30
39
|
|
|
@@ -59,6 +68,7 @@ module.exports = (config) => {
|
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
saveProjectOrder(order);
|
|
71
|
+
invalidateProjectSnapshots('gemini');
|
|
62
72
|
|
|
63
73
|
res.json({ success: true });
|
|
64
74
|
} catch (err) {
|
|
@@ -79,6 +89,8 @@ module.exports = (config) => {
|
|
|
79
89
|
|
|
80
90
|
const { projectHash } = req.params;
|
|
81
91
|
const result = deleteProject(projectHash);
|
|
92
|
+
invalidateProjectSnapshots('gemini');
|
|
93
|
+
invalidateSessionSnapshots('gemini', projectHash);
|
|
82
94
|
|
|
83
95
|
res.json(result);
|
|
84
96
|
} catch (err) {
|
|
@@ -13,6 +13,14 @@ const {
|
|
|
13
13
|
} = require('../services/gemini-sessions');
|
|
14
14
|
const { isGeminiInstalled } = require('../services/gemini-config');
|
|
15
15
|
const { loadAliases } = require('../services/alias');
|
|
16
|
+
const {
|
|
17
|
+
defaultProjectInfo,
|
|
18
|
+
emptySessionList,
|
|
19
|
+
getSessionListSnapshot,
|
|
20
|
+
invalidateSessionSnapshots,
|
|
21
|
+
runSessionSnapshotWorker
|
|
22
|
+
} = require('../services/session-snapshots');
|
|
23
|
+
const { invalidateProjectSnapshots } = require('../services/project-snapshots');
|
|
16
24
|
|
|
17
25
|
function normalizeForkOptions(body = {}) {
|
|
18
26
|
const alias = typeof body.alias === 'string' ? body.alias.trim() : '';
|
|
@@ -90,39 +98,23 @@ module.exports = (config) => {
|
|
|
90
98
|
* GET /api/gemini/sessions/:projectHash
|
|
91
99
|
* 获取项目的所有会话
|
|
92
100
|
*/
|
|
93
|
-
router.get('/:projectHash', (req, res) => {
|
|
101
|
+
router.get('/:projectHash', async (req, res) => {
|
|
94
102
|
try {
|
|
95
103
|
if (!isGeminiInstalled()) {
|
|
96
104
|
return res.status(404).json({ error: 'Gemini CLI not installed' });
|
|
97
105
|
}
|
|
98
106
|
|
|
99
107
|
const { projectHash } = req.params;
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const aliases = loadAliases();
|
|
109
|
-
|
|
110
|
-
// 使用彩虹表解析真实路径
|
|
111
|
-
const realPath = getProjectPath(projectHash);
|
|
112
|
-
const path = require('path');
|
|
113
|
-
const displayName = realPath ? path.basename(realPath) : `Project ${projectHash.substring(0, 8)}`;
|
|
114
|
-
|
|
115
|
-
res.json({
|
|
116
|
-
sessions,
|
|
117
|
-
totalSize,
|
|
118
|
-
aliases,
|
|
119
|
-
projectInfo: {
|
|
120
|
-
name: projectHash,
|
|
121
|
-
fullPath: realPath || projectHash,
|
|
122
|
-
path: realPath || projectHash,
|
|
123
|
-
displayName
|
|
124
|
-
}
|
|
108
|
+
const force = req.query?.fresh === '1';
|
|
109
|
+
const snapshot = await getSessionListSnapshot('gemini', projectHash, {
|
|
110
|
+
fallbackValue: emptySessionList(projectHash, {
|
|
111
|
+
aliases: loadAliases(),
|
|
112
|
+
projectInfo: defaultProjectInfo(projectHash)
|
|
113
|
+
}),
|
|
114
|
+
force,
|
|
115
|
+
refresh: () => runSessionSnapshotWorker('gemini', projectHash, {}, { force })
|
|
125
116
|
});
|
|
117
|
+
res.json({ ...snapshot.value, meta: snapshot.meta });
|
|
126
118
|
} catch (err) {
|
|
127
119
|
console.error('[Gemini API] Failed to get sessions:', err);
|
|
128
120
|
res.status(500).json({ error: err.message });
|
|
@@ -328,6 +320,8 @@ module.exports = (config) => {
|
|
|
328
320
|
|
|
329
321
|
const { sessionId } = req.params;
|
|
330
322
|
const result = deleteSession(sessionId);
|
|
323
|
+
invalidateSessionSnapshots('gemini', req.params.projectHash);
|
|
324
|
+
invalidateProjectSnapshots('gemini');
|
|
331
325
|
|
|
332
326
|
res.json(result);
|
|
333
327
|
} catch (err) {
|
|
@@ -377,6 +371,11 @@ module.exports = (config) => {
|
|
|
377
371
|
}
|
|
378
372
|
});
|
|
379
373
|
|
|
374
|
+
if (deletedSessionIds.length > 0) {
|
|
375
|
+
invalidateSessionSnapshots('gemini', req.params.projectHash);
|
|
376
|
+
invalidateProjectSnapshots('gemini');
|
|
377
|
+
}
|
|
378
|
+
|
|
380
379
|
res.json({
|
|
381
380
|
success: failed.length === 0,
|
|
382
381
|
requestedCount: uniqueSessionIds.length,
|
|
@@ -402,6 +401,8 @@ module.exports = (config) => {
|
|
|
402
401
|
|
|
403
402
|
const { sessionId } = req.params;
|
|
404
403
|
const result = forkSession(sessionId, normalizeForkOptions(req.body));
|
|
404
|
+
invalidateSessionSnapshots('gemini', req.params.projectHash);
|
|
405
|
+
invalidateProjectSnapshots('gemini');
|
|
405
406
|
|
|
406
407
|
res.json(result);
|
|
407
408
|
} catch (err) {
|
|
@@ -428,6 +429,7 @@ module.exports = (config) => {
|
|
|
428
429
|
}
|
|
429
430
|
|
|
430
431
|
saveSessionOrder(projectHash, order);
|
|
432
|
+
invalidateSessionSnapshots('gemini', projectHash);
|
|
431
433
|
|
|
432
434
|
res.json({ success: true });
|
|
433
435
|
} catch (err) {
|
|
@@ -1,32 +1,41 @@
|
|
|
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
|
isOmpInstalled
|
|
8
7
|
} = require('../services/omp-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');
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
module.exports = () => {
|
|
15
|
-
router.get('/', (req, res) => {
|
|
21
|
+
router.get('/', async (req, res) => {
|
|
16
22
|
try {
|
|
17
23
|
if (!isOmpInstalled()) {
|
|
18
24
|
return res.json({
|
|
19
25
|
projects: [],
|
|
20
26
|
currentProject: null,
|
|
21
|
-
error: 'OMP CLI not installed or not found'
|
|
27
|
+
error: 'OMP CLI not installed or not found',
|
|
28
|
+
meta: { generatedAt: new Date().toISOString(), stale: false, refreshing: false, fallback: true, error: null }
|
|
22
29
|
});
|
|
23
30
|
}
|
|
24
31
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
const force = req.query?.fresh === '1';
|
|
33
|
+
const snapshot = await getProjectListSnapshot('omp', {
|
|
34
|
+
fallbackValue: emptyProjectList(null),
|
|
35
|
+
force,
|
|
36
|
+
refresh: () => runDashboardSnapshotWorker('projects', 'omp', {}, { force })
|
|
29
37
|
});
|
|
38
|
+
res.json({ ...snapshot.value, meta: snapshot.meta });
|
|
30
39
|
} catch (err) {
|
|
31
40
|
console.error('[OMP API] Failed to get projects:', err);
|
|
32
41
|
if (err.code === 'ENOENT') {
|
|
@@ -49,6 +58,7 @@ module.exports = () => {
|
|
|
49
58
|
return res.status(400).json({ error: 'order must be an array' });
|
|
50
59
|
}
|
|
51
60
|
saveProjectOrder(order);
|
|
61
|
+
invalidateProjectSnapshots('omp');
|
|
52
62
|
res.json({ success: true });
|
|
53
63
|
} catch (err) {
|
|
54
64
|
console.error('[OMP API] Failed to save project order:', err);
|
|
@@ -62,6 +72,8 @@ module.exports = () => {
|
|
|
62
72
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
63
73
|
}
|
|
64
74
|
const result = deleteProject(req.params.projectName);
|
|
75
|
+
invalidateProjectSnapshots('omp');
|
|
76
|
+
invalidateSessionSnapshots('omp', req.params.projectName);
|
|
65
77
|
res.json(result);
|
|
66
78
|
} catch (err) {
|
|
67
79
|
if (isNotFoundError(err)) {
|
|
@@ -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);
|