coding-tool-x 3.7.2 → 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-D5kLOhUo.js → Analytics-BmPIW7oK.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-D_-16blp.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-Cv_rv8Dh.js → PluginManager-CCJ7mo35.js} +1 -1
- package/dist/web/assets/{ProjectList-H_m4Tx4E.js → ProjectList-DVUs3o8x.js} +1 -1
- package/dist/web/assets/SessionList-DwssBydw.js +1 -0
- package/dist/web/assets/{SkillManager-CRav3m2a.js → SkillManager-DmOTHcQ7.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-B3f8R4EF.js → WorkspaceManager-DLVj0zlu.js} +1 -1
- package/dist/web/assets/{icons-Dop5j7Bf.js → icons-nGdSKQn2.js} +1 -1
- package/dist/web/assets/index-BjdNYwSs.js +2 -0
- package/dist/web/assets/index-kQCC5dlu.css +1 -0
- package/dist/web/index.html +3 -3
- package/package.json +1 -1
- package/src/config/paths.js +2 -0
- package/src/server/api/channels.js +14 -1
- package/src/server/api/codex-channels.js +21 -1
- package/src/server/api/codex-projects.js +25 -8
- package/src/server/api/dashboard.js +167 -75
- package/src/server/api/gemini-channels.js +21 -1
- package/src/server/api/gemini-projects.js +23 -7
- package/src/server/api/omp-channels.js +17 -1
- package/src/server/api/omp-projects.js +23 -6
- package/src/server/api/opencode-channels.js +21 -1
- 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/channel-sync-utils.js +368 -0
- package/src/server/services/channels.js +119 -0
- package/src/server/services/codex-channels.js +125 -0
- package/src/server/services/gemini-channels.js +123 -0
- package/src/server/services/omp-channels.js +299 -2
- package/src/server/services/omp-settings-manager.js +2 -0
- package/src/server/services/opencode-channels.js +172 -4
- package/src/server/services/project-snapshots.js +52 -0
- package/src/server/services/snapshot-cache.js +295 -0
- package/dist/web/assets/Home-DM_56Fyr.js +0 -1
- package/dist/web/assets/Home-Jom_ZcdY.css +0 -1
- package/dist/web/assets/SessionList-DcUQvsbR.js +0 -1
- package/dist/web/assets/index-Bnni1zPN.js +0 -2
- package/dist/web/assets/index-CfMuHG8u.css +0 -1
|
@@ -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) {
|
|
@@ -6,7 +6,8 @@ const {
|
|
|
6
6
|
updateChannel,
|
|
7
7
|
deleteChannel,
|
|
8
8
|
getEnabledChannels,
|
|
9
|
-
saveChannelOrder
|
|
9
|
+
saveChannelOrder,
|
|
10
|
+
syncCurrentGeminiChannel
|
|
10
11
|
} = require('../services/gemini-channels');
|
|
11
12
|
const { getSchedulerState } = require('../services/channel-scheduler');
|
|
12
13
|
const { getChannelHealthStatus, resetChannelHealth } = require('../services/channel-health');
|
|
@@ -53,6 +54,25 @@ module.exports = (config) => {
|
|
|
53
54
|
}
|
|
54
55
|
});
|
|
55
56
|
|
|
57
|
+
/**
|
|
58
|
+
* POST /api/gemini/channels/sync-current
|
|
59
|
+
* 将当前 Gemini 原生/ctx 配置同步到渠道列表
|
|
60
|
+
*/
|
|
61
|
+
router.post('/sync-current', (req, res) => {
|
|
62
|
+
try {
|
|
63
|
+
if (!isGeminiInstalled()) {
|
|
64
|
+
return res.status(404).json({ error: 'Gemini CLI not installed' });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const result = syncCurrentGeminiChannel();
|
|
68
|
+
res.json(result);
|
|
69
|
+
broadcastSchedulerState('gemini', getSchedulerState('gemini'));
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error('[Gemini Channels API] Failed to sync current channel:', err);
|
|
72
|
+
res.status(500).json({ error: err.message });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
56
76
|
/**
|
|
57
77
|
* GET /api/gemini/channels/:id/models
|
|
58
78
|
* 获取渠道可用模型列表
|
|
@@ -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) {
|
|
@@ -5,7 +5,8 @@ const {
|
|
|
5
5
|
createChannel,
|
|
6
6
|
updateChannel,
|
|
7
7
|
deleteChannel,
|
|
8
|
-
saveChannelOrder
|
|
8
|
+
saveChannelOrder,
|
|
9
|
+
syncCurrentOmpChannel
|
|
9
10
|
} = require('../services/omp-channels');
|
|
10
11
|
const { isOmpInstalled } = require('../services/omp-sessions');
|
|
11
12
|
const { getSchedulerState } = require('../services/channel-scheduler');
|
|
@@ -171,6 +172,21 @@ module.exports = () => {
|
|
|
171
172
|
}
|
|
172
173
|
});
|
|
173
174
|
|
|
175
|
+
router.post('/sync-current', (req, res) => {
|
|
176
|
+
try {
|
|
177
|
+
if (!isOmpInstalled()) {
|
|
178
|
+
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const result = syncCurrentOmpChannel();
|
|
182
|
+
res.json(result);
|
|
183
|
+
broadcastSchedulerState('omp', getSchedulerState('omp'));
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.error('[OMP Channels API] Failed to sync current channel:', err);
|
|
186
|
+
res.status(500).json({ error: err.message });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
174
190
|
router.post('/probe-models', async (req, res) => {
|
|
175
191
|
try {
|
|
176
192
|
const {
|
|
@@ -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)) {
|
|
@@ -5,7 +5,8 @@ const {
|
|
|
5
5
|
createChannel,
|
|
6
6
|
updateChannel,
|
|
7
7
|
deleteChannel,
|
|
8
|
-
saveChannelOrder
|
|
8
|
+
saveChannelOrder,
|
|
9
|
+
syncCurrentOpenCodeChannel
|
|
9
10
|
} = require('../services/opencode-channels');
|
|
10
11
|
const { isOpenCodeInstalled } = require('../services/opencode-sessions');
|
|
11
12
|
const { getSchedulerState } = require('../services/channel-scheduler');
|
|
@@ -131,6 +132,25 @@ module.exports = (config) => {
|
|
|
131
132
|
}
|
|
132
133
|
});
|
|
133
134
|
|
|
135
|
+
/**
|
|
136
|
+
* POST /api/opencode/channels/sync-current
|
|
137
|
+
* 将当前 OpenCode 原生/ctx 配置同步到渠道列表
|
|
138
|
+
*/
|
|
139
|
+
router.post('/sync-current', (req, res) => {
|
|
140
|
+
try {
|
|
141
|
+
if (!isOpenCodeInstalled()) {
|
|
142
|
+
return res.status(404).json({ error: 'OpenCode CLI not installed' });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const result = syncCurrentOpenCodeChannel();
|
|
146
|
+
res.json(result);
|
|
147
|
+
broadcastSchedulerState('opencode', getSchedulerState('opencode'));
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.error('[OpenCode Channels API] Failed to sync current channel:', err);
|
|
150
|
+
res.status(500).json({ error: err.message });
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
134
154
|
/**
|
|
135
155
|
* POST /api/opencode/channels/probe-models
|
|
136
156
|
* 用临时配置(新建渠道时)获取模型列表,无需 channelId
|
|
@@ -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);
|