@adversity/coding-tool-x 3.1.0 → 3.1.2

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 (142) hide show
  1. package/CHANGELOG.md +39 -18
  2. package/README.md +8 -8
  3. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  4. package/dist/web/assets/ConfigTemplates-DvcbKKdS.js +1 -0
  5. package/dist/web/assets/Home-BJKPCBuk.css +1 -0
  6. package/dist/web/assets/Home-Cw-F_Wnu.js +1 -0
  7. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  8. package/dist/web/assets/PluginManager-jy_4GVxI.js +1 -0
  9. package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
  10. package/dist/web/assets/ProjectList-Df1-NcNr.js +1 -0
  11. package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
  12. package/dist/web/assets/SessionList-UWcZtC2r.js +1 -0
  13. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  14. package/dist/web/assets/SkillManager-IRdseMKB.js +1 -0
  15. package/dist/web/assets/Terminal-BasTyDut.js +1 -0
  16. package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
  17. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  18. package/dist/web/assets/WorkspaceManager-D-D2kK1V.js +1 -0
  19. package/dist/web/assets/icons-kcfLIMBB.js +1 -0
  20. package/dist/web/assets/index-CoB3zF0K.css +1 -0
  21. package/dist/web/assets/index-CryrSLv8.js +2 -0
  22. package/dist/web/assets/markdown-BfC0goYb.css +10 -0
  23. package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
  24. package/dist/web/assets/naive-ui-CSrLusZZ.js +1 -0
  25. package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
  26. package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
  27. package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
  28. package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
  29. package/dist/web/index.html +8 -6
  30. package/package.json +4 -2
  31. package/src/commands/channels.js +48 -1
  32. package/src/commands/cli-type.js +4 -2
  33. package/src/commands/daemon.js +81 -12
  34. package/src/commands/doctor.js +10 -9
  35. package/src/commands/list.js +1 -1
  36. package/src/commands/logs.js +6 -4
  37. package/src/commands/port-config.js +24 -4
  38. package/src/commands/proxy-control.js +12 -6
  39. package/src/commands/search.js +1 -1
  40. package/src/commands/security.js +3 -2
  41. package/src/commands/stats.js +226 -52
  42. package/src/commands/switch.js +1 -1
  43. package/src/commands/toggle-proxy.js +31 -6
  44. package/src/commands/update.js +97 -0
  45. package/src/commands/workspace.js +1 -1
  46. package/src/config/default.js +41 -2
  47. package/src/config/loader.js +74 -8
  48. package/src/config/model-metadata.js +415 -0
  49. package/src/config/model-pricing.js +23 -93
  50. package/src/config/paths.js +105 -33
  51. package/src/index.js +64 -3
  52. package/src/plugins/constants.js +3 -2
  53. package/src/plugins/plugin-api.js +1 -1
  54. package/src/reset-config.js +4 -2
  55. package/src/server/api/agents.js +57 -14
  56. package/src/server/api/channels.js +112 -33
  57. package/src/server/api/codex-channels.js +111 -18
  58. package/src/server/api/codex-proxy.js +14 -8
  59. package/src/server/api/commands.js +71 -18
  60. package/src/server/api/config-export.js +0 -6
  61. package/src/server/api/config-registry.js +11 -3
  62. package/src/server/api/config.js +376 -5
  63. package/src/server/api/convert.js +133 -0
  64. package/src/server/api/dashboard.js +22 -6
  65. package/src/server/api/gemini-channels.js +107 -18
  66. package/src/server/api/gemini-proxy.js +14 -8
  67. package/src/server/api/gemini-sessions.js +1 -1
  68. package/src/server/api/health-check.js +4 -3
  69. package/src/server/api/mcp.js +3 -3
  70. package/src/server/api/opencode-channels.js +497 -0
  71. package/src/server/api/opencode-projects.js +99 -0
  72. package/src/server/api/opencode-proxy.js +207 -0
  73. package/src/server/api/opencode-sessions.js +345 -0
  74. package/src/server/api/opencode-statistics.js +57 -0
  75. package/src/server/api/plugins.js +66 -19
  76. package/src/server/api/prompts.js +2 -2
  77. package/src/server/api/proxy.js +7 -4
  78. package/src/server/api/sessions.js +3 -0
  79. package/src/server/api/settings.js +111 -0
  80. package/src/server/api/skills.js +69 -18
  81. package/src/server/api/workspaces.js +78 -6
  82. package/src/server/codex-proxy-server.js +36 -22
  83. package/src/server/dev-server.js +1 -1
  84. package/src/server/gemini-proxy-server.js +21 -7
  85. package/src/server/index.js +174 -58
  86. package/src/server/opencode-proxy-server.js +5486 -0
  87. package/src/server/proxy-server.js +33 -22
  88. package/src/server/services/agents-service.js +61 -24
  89. package/src/server/services/channel-scheduler.js +9 -5
  90. package/src/server/services/channels.js +64 -37
  91. package/src/server/services/codex-channels.js +56 -43
  92. package/src/server/services/codex-sessions.js +105 -6
  93. package/src/server/services/codex-settings-manager.js +271 -49
  94. package/src/server/services/codex-statistics-service.js +2 -2
  95. package/src/server/services/commands-service.js +84 -25
  96. package/src/server/services/config-export-service.js +7 -45
  97. package/src/server/services/config-registry-service.js +63 -17
  98. package/src/server/services/config-sync-manager.js +160 -7
  99. package/src/server/services/config-templates-service.js +204 -51
  100. package/src/server/services/env-checker.js +50 -13
  101. package/src/server/services/env-manager.js +155 -19
  102. package/src/server/services/favorites.js +5 -3
  103. package/src/server/services/gemini-channels.js +33 -44
  104. package/src/server/services/gemini-statistics-service.js +2 -2
  105. package/src/server/services/mcp-service.js +350 -9
  106. package/src/server/services/model-detector.js +707 -221
  107. package/src/server/services/network-access.js +80 -0
  108. package/src/server/services/opencode-channels.js +208 -0
  109. package/src/server/services/opencode-gateway-converter.js +639 -0
  110. package/src/server/services/opencode-sessions.js +931 -0
  111. package/src/server/services/opencode-settings-manager.js +478 -0
  112. package/src/server/services/opencode-statistics-service.js +255 -0
  113. package/src/server/services/plugins-service.js +479 -22
  114. package/src/server/services/prompts-service.js +53 -11
  115. package/src/server/services/proxy-runtime.js +1 -1
  116. package/src/server/services/repo-scanner-base.js +1 -1
  117. package/src/server/services/response-decoder.js +21 -0
  118. package/src/server/services/security-config.js +1 -1
  119. package/src/server/services/session-cache.js +1 -1
  120. package/src/server/services/skill-service.js +300 -46
  121. package/src/server/services/speed-test.js +464 -186
  122. package/src/server/services/statistics-service.js +2 -2
  123. package/src/server/services/terminal-commands.js +10 -3
  124. package/src/server/services/terminal-config.js +1 -1
  125. package/src/server/services/ui-config.js +1 -1
  126. package/src/server/services/workspace-service.js +57 -100
  127. package/src/server/websocket-server.js +156 -8
  128. package/src/ui/menu.js +49 -40
  129. package/src/utils/port-helper.js +22 -8
  130. package/src/utils/session.js +5 -4
  131. package/dist/web/assets/icons-CO_2OFES.js +0 -1
  132. package/dist/web/assets/index-DI8QOi-E.js +0 -14
  133. package/dist/web/assets/index-uLHGdeZh.css +0 -41
  134. package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
  135. package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
  136. package/src/server/api/oauth.js +0 -294
  137. package/src/server/api/permissions.js +0 -385
  138. package/src/server/config/oauth-providers.js +0 -68
  139. package/src/server/services/oauth-callback-server.js +0 -284
  140. package/src/server/services/oauth-service.js +0 -378
  141. package/src/server/services/oauth-token-storage.js +0 -135
  142. package/src/server/services/permission-templates-service.js +0 -308
@@ -0,0 +1,497 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const {
4
+ getChannels,
5
+ createChannel,
6
+ updateChannel,
7
+ deleteChannel,
8
+ saveChannelOrder
9
+ } = require('../services/opencode-channels');
10
+ const { isOpenCodeInstalled } = require('../services/opencode-sessions');
11
+ const { getSchedulerState } = require('../services/channel-scheduler');
12
+ const { getChannelHealthStatus, resetChannelHealth } = require('../services/channel-health');
13
+ const { broadcastSchedulerState } = require('../websocket-server');
14
+ const {
15
+ testChannelSpeed,
16
+ sanitizeBatchConcurrency,
17
+ runWithConcurrencyLimit
18
+ } = require('../services/speed-test');
19
+ const {
20
+ clearOpenCodeRedirectCache,
21
+ collectProxyModelList,
22
+ getOpenCodeProxyStatus
23
+ } = require('../opencode-proxy-server');
24
+ const { setProxyConfig } = require('../services/opencode-settings-manager');
25
+ const {
26
+ fetchModelsFromProvider,
27
+ probeModelAvailability,
28
+ clearCache
29
+ } = require('../services/model-detector');
30
+
31
+ module.exports = (config) => {
32
+ function uniqueModels(models = []) {
33
+ const seen = new Set();
34
+ const result = [];
35
+ models.forEach((model) => {
36
+ if (typeof model !== 'string') return;
37
+ const trimmed = model.trim();
38
+ if (!trimmed) return;
39
+ const key = trimmed.toLowerCase();
40
+ if (seen.has(key)) return;
41
+ seen.add(key);
42
+ result.push(trimmed);
43
+ });
44
+ return result;
45
+ }
46
+
47
+ function collectChannelPreferredModels(channel) {
48
+ const candidates = [];
49
+ if (!channel || typeof channel !== 'object') return candidates;
50
+
51
+ candidates.push(channel.model);
52
+ candidates.push(channel.speedTestModel);
53
+
54
+ const modelConfig = channel.modelConfig;
55
+ if (modelConfig && typeof modelConfig === 'object') {
56
+ candidates.push(modelConfig.model);
57
+ candidates.push(modelConfig.opusModel);
58
+ candidates.push(modelConfig.sonnetModel);
59
+ candidates.push(modelConfig.haikuModel);
60
+ }
61
+
62
+ if (Array.isArray(channel.modelRedirects)) {
63
+ channel.modelRedirects.forEach((rule) => {
64
+ candidates.push(rule?.from);
65
+ candidates.push(rule?.to);
66
+ });
67
+ }
68
+
69
+ return uniqueModels(candidates);
70
+ }
71
+
72
+ function resolveGatewaySourceType(channel) {
73
+ const value = String(channel?.gatewaySourceType || '').trim().toLowerCase();
74
+ if (value === 'claude') return 'claude';
75
+ if (value === 'gemini') return 'gemini';
76
+ return 'codex';
77
+ }
78
+
79
+ function mapGatewaySourceTypeToSpeedTestType(channel) {
80
+ return resolveGatewaySourceType(channel);
81
+ }
82
+
83
+ function isConverterPresetChannel(channel) {
84
+ const presetId = String(channel?.presetId || '').trim().toLowerCase();
85
+ return presetId === 'entry_claude' || presetId === 'entry_codex' || presetId === 'entry_gemini';
86
+ }
87
+
88
+ function refreshEditedChannelModelCache(channelId) {
89
+ if (!channelId) return;
90
+ clearCache(channelId);
91
+ }
92
+
93
+ async function syncOpenCodeProxyConfigByCache() {
94
+ const proxyStatus = getOpenCodeProxyStatus();
95
+ if (!proxyStatus?.running || !Number.isFinite(proxyStatus?.port)) {
96
+ return;
97
+ }
98
+
99
+ const channels = getChannels().channels || [];
100
+ const enabledChannels = channels.filter(ch => ch.enabled !== false);
101
+
102
+ // Collect per-channel model lists for per-channel provider generation
103
+ let detectedModels = [];
104
+ try {
105
+ detectedModels = await collectProxyModelList(enabledChannels, { useCacheOnly: true }) || [];
106
+ } catch (error) {
107
+ console.warn('[OpenCode Channels API] Failed to collect cached models while syncing proxy config:', error.message);
108
+ }
109
+
110
+ const channelPayloads = enabledChannels.map((ch) => {
111
+ let models;
112
+ if (Array.isArray(ch.allowedModels) && ch.allowedModels.length > 0) {
113
+ // User explicitly selected models for this channel
114
+ models = ch.allowedModels;
115
+ } else {
116
+ // Fall back to configured + detected models
117
+ models = uniqueModels([
118
+ ch.model,
119
+ ch.speedTestModel,
120
+ ...(Array.isArray(ch.modelRedirects)
121
+ ? ch.modelRedirects.flatMap(r => [r?.from, r?.to])
122
+ : []),
123
+ ...detectedModels
124
+ ]);
125
+ }
126
+ return {
127
+ name: ch.name,
128
+ providerKey: ch.providerKey || ch.name,
129
+ model: ch.model || null,
130
+ models
131
+ };
132
+ });
133
+
134
+ const currentChannel = enabledChannels[0];
135
+ const activeModel = currentChannel?.model || currentChannel?.speedTestModel || null;
136
+ setProxyConfig(proxyStatus.port, { channels: channelPayloads, model: activeModel });
137
+ }
138
+
139
+ async function refreshEditedChannelAndSyncProxy(channelId) {
140
+ try {
141
+ await refreshEditedChannelModelCache(channelId);
142
+ } catch (error) {
143
+ console.warn('[OpenCode Channels API] Refresh edited channel model cache failed:', error.message);
144
+ }
145
+
146
+ try {
147
+ await syncOpenCodeProxyConfigByCache();
148
+ } catch (error) {
149
+ console.warn('[OpenCode Channels API] Sync proxy config after channel edit failed:', error.message);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * GET /api/opencode/channels
155
+ * 获取所有 OpenCode 渠道
156
+ */
157
+ router.get('/', (req, res) => {
158
+ try {
159
+ if (!isOpenCodeInstalled()) {
160
+ return res.json({
161
+ channels: [],
162
+ installed: false,
163
+ error: 'OpenCode CLI not installed'
164
+ });
165
+ }
166
+ const data = getChannels();
167
+ const channelsWithHealth = (data.channels || []).map(ch => ({
168
+ ...ch,
169
+ health: getChannelHealthStatus(ch.id, 'opencode')
170
+ }));
171
+ res.json({ channels: channelsWithHealth, installed: true });
172
+ } catch (err) {
173
+ console.error('[OpenCode Channels API] Failed to get channels:', err);
174
+ res.status(500).json({ error: err.message });
175
+ }
176
+ });
177
+
178
+ /**
179
+ * GET /api/opencode/channels/enabled
180
+ * 获取所有已启用的 OpenCode 渠道
181
+ */
182
+ router.get('/enabled', (req, res) => {
183
+ try {
184
+ if (!isOpenCodeInstalled()) {
185
+ return res.json({
186
+ channels: [],
187
+ installed: false,
188
+ error: 'OpenCode CLI not installed'
189
+ });
190
+ }
191
+ const data = getChannels();
192
+ const enabledChannels = (data.channels || []).filter(ch => ch.enabled !== false);
193
+ const channelsWithHealth = enabledChannels.map(ch => ({
194
+ ...ch,
195
+ health: getChannelHealthStatus(ch.id, 'opencode')
196
+ }));
197
+ res.json({ channels: channelsWithHealth, installed: true });
198
+ } catch (err) {
199
+ console.error('[OpenCode Channels API] Failed to get enabled channels:', err);
200
+ res.status(500).json({ error: err.message });
201
+ }
202
+ });
203
+
204
+ /**
205
+ * GET /api/opencode/channels/:channelId/models
206
+ * 获取渠道可用模型列表
207
+ */
208
+ router.get('/:channelId/models', async (req, res) => {
209
+ try {
210
+ const { channelId } = req.params;
211
+ const channels = getChannels().channels || [];
212
+ const channel = channels.find(ch => ch.id === channelId);
213
+
214
+ if (!channel) {
215
+ return res.status(404).json({ error: 'Channel not found' });
216
+ }
217
+
218
+ const gatewaySourceType = resolveGatewaySourceType(channel);
219
+ const preferredModels = collectChannelPreferredModels(channel);
220
+ const listResult = await fetchModelsFromProvider(channel, 'openai_compatible');
221
+ const listedModels = Array.isArray(listResult.models) ? uniqueModels(listResult.models) : [];
222
+ const shouldProbeByDefault = !!listResult.disabledByConfig;
223
+ let result;
224
+
225
+ if (listedModels.length > 0) {
226
+ result = {
227
+ models: listedModels,
228
+ supported: true,
229
+ cached: !!listResult.cached,
230
+ fallbackUsed: false,
231
+ lastChecked: listResult.lastChecked || new Date().toISOString(),
232
+ error: null,
233
+ errorHint: null
234
+ };
235
+ } else if (shouldProbeByDefault || isConverterPresetChannel(channel)) {
236
+ const probe = await probeModelAvailability(channel, gatewaySourceType, {
237
+ stopOnFirstAvailable: false,
238
+ preferredModels
239
+ });
240
+ const probedModels = Array.isArray(probe.availableModels) ? uniqueModels(probe.availableModels) : [];
241
+
242
+ result = {
243
+ models: probedModels,
244
+ supported: probedModels.length > 0,
245
+ cached: !!probe.cached || !!listResult.cached,
246
+ fallbackUsed: false,
247
+ lastChecked: probe.lastChecked || listResult.lastChecked || new Date().toISOString(),
248
+ error: probedModels.length > 0 ? null : (listResult.error || '无法获取可用模型'),
249
+ errorHint: probedModels.length > 0
250
+ ? (shouldProbeByDefault ? '已按设置跳过 /v1/models,使用默认模型探测结果' : '模型列表接口不可用,已自动切换为模型探测结果')
251
+ : (listResult.errorHint || (shouldProbeByDefault
252
+ ? '已按设置跳过 /v1/models,且默认模型探测无可用结果'
253
+ : '模型列表接口不可用且模型探测无可用结果'))
254
+ };
255
+ } else {
256
+ // 非入口转换器渠道:只请求 /v1/models,失败则返回空列表
257
+ result = {
258
+ models: [],
259
+ supported: false,
260
+ cached: !!listResult.cached,
261
+ fallbackUsed: false,
262
+ lastChecked: listResult.lastChecked || new Date().toISOString(),
263
+ error: listResult.error || '该渠道未返回可用模型列表',
264
+ errorHint: listResult.errorHint || '此类型渠道不执行模型探测,请检查 /v1/models 接口'
265
+ };
266
+ }
267
+
268
+ res.json({
269
+ channelId: channelId,
270
+ gatewaySourceType,
271
+ models: result.models,
272
+ supported: result.supported,
273
+ cached: result.cached,
274
+ fallbackUsed: result.fallbackUsed,
275
+ fetchedAt: result.lastChecked || new Date().toISOString(),
276
+ error: result.error,
277
+ errorHint: result.errorHint
278
+ });
279
+ } catch (error) {
280
+ console.error('[OpenCode Channels API] Error fetching models:', error);
281
+ res.status(500).json({
282
+ error: 'Failed to fetch model list',
283
+ channelId: req.params.channelId
284
+ });
285
+ }
286
+ });
287
+
288
+ /**
289
+ * POST /api/opencode/channels
290
+ * 创建新渠道
291
+ */
292
+ router.post('/', async (req, res) => {
293
+ try {
294
+ const {
295
+ name,
296
+ baseUrl,
297
+ apiKey,
298
+ wireApi,
299
+ enabled,
300
+ weight,
301
+ maxConcurrency,
302
+ model,
303
+ gatewaySourceType,
304
+ modelRedirects,
305
+ speedTestModel,
306
+ presetId,
307
+ websiteUrl,
308
+ allowedModels
309
+ } = req.body;
310
+
311
+ if (!name || !baseUrl) {
312
+ return res.status(400).json({ error: 'Missing required fields: name and baseUrl' });
313
+ }
314
+
315
+ if (!apiKey) {
316
+ return res.status(400).json({ error: 'API Key is required' });
317
+ }
318
+
319
+ const channel = createChannel(name, baseUrl, apiKey, {
320
+ wireApi: wireApi || 'openai',
321
+ enabled,
322
+ weight,
323
+ maxConcurrency,
324
+ model,
325
+ gatewaySourceType,
326
+ modelRedirects: modelRedirects || [],
327
+ speedTestModel: speedTestModel || null,
328
+ presetId,
329
+ websiteUrl,
330
+ allowedModels: allowedModels || []
331
+ });
332
+
333
+ clearOpenCodeRedirectCache(channel.id);
334
+ await refreshEditedChannelAndSyncProxy(channel.id);
335
+ res.json(channel);
336
+ broadcastSchedulerState('opencode', getSchedulerState('opencode'));
337
+ } catch (err) {
338
+ console.error('[OpenCode Channels API] Failed to create channel:', err);
339
+ res.status(500).json({ error: err.message });
340
+ }
341
+ });
342
+
343
+ /**
344
+ * PUT /api/opencode/channels/:channelId
345
+ * 更新渠道
346
+ */
347
+ router.put('/:channelId', async (req, res) => {
348
+ try {
349
+ const { channelId } = req.params;
350
+ const updates = req.body;
351
+
352
+ const channel = updateChannel(channelId, updates);
353
+ clearOpenCodeRedirectCache(channelId);
354
+ await refreshEditedChannelAndSyncProxy(channelId);
355
+ res.json(channel);
356
+ broadcastSchedulerState('opencode', getSchedulerState('opencode'));
357
+ } catch (err) {
358
+ console.error('[OpenCode Channels API] Failed to update channel:', err);
359
+ res.status(500).json({ error: err.message });
360
+ }
361
+ });
362
+
363
+ /**
364
+ * DELETE /api/opencode/channels/:channelId
365
+ * 删除渠道
366
+ */
367
+ router.delete('/:channelId', async (req, res) => {
368
+ try {
369
+ const { channelId } = req.params;
370
+ const result = await deleteChannel(channelId);
371
+ clearOpenCodeRedirectCache(channelId);
372
+ await refreshEditedChannelAndSyncProxy(channelId);
373
+ res.json(result);
374
+ broadcastSchedulerState('opencode', getSchedulerState('opencode'));
375
+ } catch (err) {
376
+ console.error('[OpenCode Channels API] Failed to delete channel:', err);
377
+ res.status(500).json({ error: err.message });
378
+ }
379
+ });
380
+
381
+ /**
382
+ * POST /api/opencode/channels/order
383
+ * 保存渠道顺序
384
+ */
385
+ router.post('/order', (req, res) => {
386
+ try {
387
+ const { order } = req.body;
388
+ if (!Array.isArray(order)) {
389
+ return res.status(400).json({ error: 'Order must be an array' });
390
+ }
391
+ saveChannelOrder(order);
392
+ res.json({ success: true });
393
+ broadcastSchedulerState('opencode', getSchedulerState('opencode'));
394
+ } catch (err) {
395
+ console.error('[OpenCode Channels API] Failed to save order:', err);
396
+ res.status(500).json({ error: err.message });
397
+ }
398
+ });
399
+
400
+ /**
401
+ * POST /api/opencode/channels/:channelId/reset-health
402
+ * 重置渠道健康状态
403
+ */
404
+ router.post('/:channelId/reset-health', (req, res) => {
405
+ try {
406
+ const { channelId } = req.params;
407
+ resetChannelHealth(channelId, 'opencode');
408
+ res.json({ success: true });
409
+ broadcastSchedulerState('opencode', getSchedulerState('opencode'));
410
+ } catch (err) {
411
+ console.error('[OpenCode Channels API] Failed to reset health:', err);
412
+ res.status(500).json({ error: err.message });
413
+ }
414
+ });
415
+
416
+ /**
417
+ * POST /api/opencode/channels/:channelId/speed-test
418
+ * 测试渠道速度
419
+ */
420
+ router.post('/:channelId/speed-test', async (req, res) => {
421
+ try {
422
+ const { channelId } = req.params;
423
+ const { timeout = 20000 } = req.body;
424
+
425
+ const channels = getChannels().channels || [];
426
+ const channel = channels.find(ch => ch.id === channelId);
427
+
428
+ if (!channel) {
429
+ return res.status(404).json({ error: 'Channel not found' });
430
+ }
431
+
432
+ const speedTestType = mapGatewaySourceTypeToSpeedTestType(channel);
433
+ const result = await testChannelSpeed(channel, timeout, speedTestType);
434
+ res.json(result);
435
+ } catch (error) {
436
+ console.error('[OpenCode Channels API] Speed test failed:', error);
437
+ res.status(500).json({ error: error.message });
438
+ }
439
+ });
440
+
441
+ /**
442
+ * POST /api/opencode/channels/speed-test-all
443
+ * 测试所有渠道速度
444
+ */
445
+ router.post('/speed-test-all', async (req, res) => {
446
+ try {
447
+ const { timeout = 20000, concurrency } = req.body || {};
448
+ const channels = getChannels().channels || [];
449
+ const safeConcurrency = sanitizeBatchConcurrency(concurrency);
450
+
451
+ const results = await runWithConcurrencyLimit(
452
+ channels,
453
+ safeConcurrency,
454
+ channel => {
455
+ const speedTestType = mapGatewaySourceTypeToSpeedTestType(channel);
456
+ return testChannelSpeed(channel, timeout, speedTestType);
457
+ }
458
+ );
459
+
460
+ // 与 testMultipleChannels 保持一致的排序:成功在前,成功按延迟升序
461
+ results.sort((a, b) => {
462
+ if (a.success && !b.success) return -1;
463
+ if (!a.success && b.success) return 1;
464
+ if (a.success && b.success) {
465
+ const aLatency = (a.latency === null || a.latency === undefined) ? Infinity : a.latency;
466
+ const bLatency = (b.latency === null || b.latency === undefined) ? Infinity : b.latency;
467
+ return aLatency - bLatency;
468
+ }
469
+ return 0;
470
+ });
471
+
472
+ // 添加摘要统计
473
+ const successResults = results.filter(r => r.success);
474
+ const successWithLatency = successResults.filter(
475
+ r => r.latency !== null && r.latency !== undefined
476
+ );
477
+ const summary = {
478
+ total: results.length,
479
+ success: successResults.length,
480
+ failed: results.length - successResults.length,
481
+ avgLatency: successWithLatency.length > 0
482
+ ? Math.round(
483
+ successWithLatency.reduce((sum, r) => sum + r.latency, 0) / successWithLatency.length
484
+ )
485
+ : null,
486
+ concurrency: safeConcurrency
487
+ };
488
+
489
+ res.json({ results, summary });
490
+ } catch (error) {
491
+ console.error('[OpenCode Channels API] Speed test all failed:', error);
492
+ res.status(500).json({ error: error.message });
493
+ }
494
+ });
495
+
496
+ return router;
497
+ };
@@ -0,0 +1,99 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const {
4
+ getProjects,
5
+ saveProjectOrder,
6
+ deleteProject,
7
+ isOpenCodeInstalled
8
+ } = require('../services/opencode-sessions');
9
+
10
+ function isNotFoundError(error) {
11
+ return !!(error && error.message === 'Project not found');
12
+ }
13
+
14
+ module.exports = (config) => {
15
+ /**
16
+ * GET /api/opencode/projects
17
+ * 获取所有 OpenCode 项目列表
18
+ */
19
+ router.get('/', (req, res) => {
20
+ try {
21
+ if (!isOpenCodeInstalled()) {
22
+ return res.json({
23
+ projects: [],
24
+ currentProject: null,
25
+ error: 'OpenCode CLI not installed or not found'
26
+ });
27
+ }
28
+
29
+ const projects = getProjects();
30
+
31
+ res.json({
32
+ projects,
33
+ currentProject: projects[0] ? projects[0].name : null
34
+ });
35
+ } catch (err) {
36
+ console.error('[OpenCode API] Failed to get projects:', err);
37
+
38
+ if (err.code === 'ENOENT') {
39
+ return res.status(404).json({
40
+ error: 'OpenCode data directory not found',
41
+ projects: []
42
+ });
43
+ }
44
+
45
+ res.status(500).json({
46
+ error: err.message,
47
+ projects: []
48
+ });
49
+ }
50
+ });
51
+
52
+ /**
53
+ * POST /api/opencode/projects/order
54
+ * 保存项目排序
55
+ */
56
+ router.post('/order', (req, res) => {
57
+ try {
58
+ if (!isOpenCodeInstalled()) {
59
+ return res.status(404).json({ error: 'OpenCode CLI not installed' });
60
+ }
61
+
62
+ const { order } = req.body;
63
+ if (!Array.isArray(order)) {
64
+ return res.status(400).json({ error: 'order must be an array' });
65
+ }
66
+
67
+ saveProjectOrder(order);
68
+ res.json({ success: true });
69
+ } catch (err) {
70
+ console.error('[OpenCode API] Failed to save project order:', err);
71
+ res.status(500).json({ error: err.message });
72
+ }
73
+ });
74
+
75
+ /**
76
+ * DELETE /api/opencode/projects/:projectName
77
+ * 删除项目(删除项目下所有会话)
78
+ */
79
+ router.delete('/:projectName', (req, res) => {
80
+ try {
81
+ if (!isOpenCodeInstalled()) {
82
+ return res.status(404).json({ error: 'OpenCode CLI not installed' });
83
+ }
84
+
85
+ const { projectName } = req.params;
86
+ const result = deleteProject(projectName);
87
+ res.json(result);
88
+ } catch (err) {
89
+ if (isNotFoundError(err)) {
90
+ console.warn('[OpenCode API] Delete project target not found:', err.message);
91
+ return res.status(404).json({ error: err.message });
92
+ }
93
+ console.error('[OpenCode API] Failed to delete project:', err);
94
+ res.status(500).json({ error: err.message });
95
+ }
96
+ });
97
+
98
+ return router;
99
+ };