@adversity/coding-tool-x 3.0.6 → 3.1.1

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 (133) hide show
  1. package/CHANGELOG.md +38 -18
  2. package/README.md +8 -8
  3. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  4. package/dist/web/assets/ConfigTemplates-ZrK_s7ma.js +1 -0
  5. package/dist/web/assets/Home-B8YfhZ3c.js +1 -0
  6. package/dist/web/assets/Home-Di2qsylF.css +1 -0
  7. package/dist/web/assets/PluginManager-BD7QUZbU.js +1 -0
  8. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  9. package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
  10. package/dist/web/assets/ProjectList-DRb1DuHV.js +1 -0
  11. package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
  12. package/dist/web/assets/SessionList-lZ0LKzfT.js +1 -0
  13. package/dist/web/assets/SkillManager-C1xG5B4Q.js +1 -0
  14. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  15. package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
  16. package/dist/web/assets/Terminal-DksBo_lM.js +1 -0
  17. package/dist/web/assets/WorkspaceManager-Burx7XOo.js +1 -0
  18. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  19. package/dist/web/assets/icons-kcfLIMBB.js +1 -0
  20. package/dist/web/assets/index-Ufv5rCa5.css +1 -0
  21. package/dist/web/assets/index-lAkrRC3h.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 +92 -13
  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/ui.js +8 -1
  45. package/src/commands/update.js +97 -0
  46. package/src/commands/workspace.js +1 -1
  47. package/src/config/default.js +39 -2
  48. package/src/config/loader.js +74 -8
  49. package/src/config/paths.js +105 -33
  50. package/src/index.js +67 -4
  51. package/src/plugins/constants.js +3 -2
  52. package/src/plugins/plugin-api.js +1 -1
  53. package/src/reset-config.js +4 -2
  54. package/src/server/api/agents.js +57 -14
  55. package/src/server/api/channels.js +112 -33
  56. package/src/server/api/codex-channels.js +111 -18
  57. package/src/server/api/codex-proxy.js +14 -8
  58. package/src/server/api/commands.js +71 -18
  59. package/src/server/api/config-export.js +0 -6
  60. package/src/server/api/config-registry.js +11 -3
  61. package/src/server/api/config.js +376 -5
  62. package/src/server/api/convert.js +133 -0
  63. package/src/server/api/dashboard.js +22 -6
  64. package/src/server/api/gemini-channels.js +107 -18
  65. package/src/server/api/gemini-proxy.js +14 -8
  66. package/src/server/api/gemini-sessions.js +1 -1
  67. package/src/server/api/health-check.js +4 -3
  68. package/src/server/api/mcp.js +3 -3
  69. package/src/server/api/opencode-channels.js +419 -0
  70. package/src/server/api/opencode-projects.js +99 -0
  71. package/src/server/api/opencode-proxy.js +198 -0
  72. package/src/server/api/opencode-sessions.js +403 -0
  73. package/src/server/api/opencode-statistics.js +57 -0
  74. package/src/server/api/plugins.js +66 -19
  75. package/src/server/api/prompts.js +2 -2
  76. package/src/server/api/proxy.js +7 -4
  77. package/src/server/api/sessions.js +3 -0
  78. package/src/server/api/skills.js +69 -18
  79. package/src/server/api/workspaces.js +78 -6
  80. package/src/server/codex-proxy-server.js +32 -19
  81. package/src/server/dev-server.js +1 -1
  82. package/src/server/gemini-proxy-server.js +17 -3
  83. package/src/server/index.js +164 -48
  84. package/src/server/opencode-proxy-server.js +4375 -0
  85. package/src/server/proxy-server.js +30 -19
  86. package/src/server/services/agents-service.js +61 -24
  87. package/src/server/services/channel-scheduler.js +9 -5
  88. package/src/server/services/channels.js +70 -12
  89. package/src/server/services/codex-channels.js +61 -23
  90. package/src/server/services/codex-settings-manager.js +271 -49
  91. package/src/server/services/codex-statistics-service.js +2 -2
  92. package/src/server/services/commands-service.js +84 -25
  93. package/src/server/services/config-export-service.js +7 -45
  94. package/src/server/services/config-registry-service.js +63 -17
  95. package/src/server/services/config-sync-manager.js +160 -7
  96. package/src/server/services/config-templates-service.js +204 -51
  97. package/src/server/services/env-checker.js +26 -12
  98. package/src/server/services/env-manager.js +126 -18
  99. package/src/server/services/favorites.js +5 -3
  100. package/src/server/services/gemini-channels.js +37 -15
  101. package/src/server/services/gemini-statistics-service.js +2 -2
  102. package/src/server/services/mcp-service.js +350 -9
  103. package/src/server/services/model-detector.js +707 -221
  104. package/src/server/services/network-access.js +80 -0
  105. package/src/server/services/opencode-channels.js +206 -0
  106. package/src/server/services/opencode-gateway-converter.js +639 -0
  107. package/src/server/services/opencode-sessions.js +663 -0
  108. package/src/server/services/opencode-settings-manager.js +342 -0
  109. package/src/server/services/opencode-statistics-service.js +255 -0
  110. package/src/server/services/plugins-service.js +479 -22
  111. package/src/server/services/prompts-service.js +53 -11
  112. package/src/server/services/proxy-runtime.js +1 -1
  113. package/src/server/services/repo-scanner-base.js +1 -1
  114. package/src/server/services/security-config.js +1 -1
  115. package/src/server/services/session-cache.js +1 -1
  116. package/src/server/services/skill-service.js +300 -46
  117. package/src/server/services/speed-test.js +464 -186
  118. package/src/server/services/statistics-service.js +2 -2
  119. package/src/server/services/terminal-commands.js +10 -3
  120. package/src/server/services/terminal-config.js +1 -1
  121. package/src/server/services/ui-config.js +1 -1
  122. package/src/server/services/workspace-service.js +57 -100
  123. package/src/server/websocket-server.js +132 -3
  124. package/src/ui/menu.js +49 -40
  125. package/src/utils/port-helper.js +22 -8
  126. package/src/utils/session.js +5 -4
  127. package/dist/web/assets/icons-BxudHPiX.js +0 -1
  128. package/dist/web/assets/index-D2VfwJBa.js +0 -14
  129. package/dist/web/assets/index-oXBzu0bd.css +0 -41
  130. package/dist/web/assets/naive-ui-DT-Uur8K.js +0 -1
  131. package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
  132. package/src/server/api/permissions.js +0 -385
  133. package/src/server/services/permission-templates-service.js +0 -308
@@ -8,7 +8,24 @@ const express = require('express');
8
8
  const { AgentsService } = require('../services/agents-service');
9
9
 
10
10
  const router = express.Router();
11
- const agentsService = new AgentsService();
11
+ const SUPPORTED_PLATFORMS = ['claude', 'opencode'];
12
+ const agentServices = new Map();
13
+
14
+ function resolvePlatform(rawPlatform) {
15
+ return SUPPORTED_PLATFORMS.includes(rawPlatform) ? rawPlatform : 'claude';
16
+ }
17
+
18
+ function getPlatform(req) {
19
+ return resolvePlatform(req.query?.platform || req.body?.platform);
20
+ }
21
+
22
+ function getAgentsService(req) {
23
+ const platform = getPlatform(req);
24
+ if (!agentServices.has(platform)) {
25
+ agentServices.set(platform, new AgentsService(platform));
26
+ }
27
+ return { platform, service: agentServices.get(platform) };
28
+ }
12
29
 
13
30
  /**
14
31
  * 获取代理列表
@@ -17,11 +34,13 @@ const agentsService = new AgentsService();
17
34
  */
18
35
  router.get('/', (req, res) => {
19
36
  try {
37
+ const { platform, service } = getAgentsService(req);
20
38
  const { projectPath } = req.query;
21
- const result = agentsService.listAgents(projectPath || null);
39
+ const result = service.listAgents(projectPath || null);
22
40
 
23
41
  res.json({
24
42
  success: true,
43
+ platform,
25
44
  ...result
26
45
  });
27
46
  } catch (err) {
@@ -39,11 +58,13 @@ router.get('/', (req, res) => {
39
58
  */
40
59
  router.get('/stats', (req, res) => {
41
60
  try {
61
+ const { platform, service } = getAgentsService(req);
42
62
  const { projectPath } = req.query;
43
- const stats = agentsService.getStats(projectPath || null);
63
+ const stats = service.getStats(projectPath || null);
44
64
 
45
65
  res.json({
46
66
  success: true,
67
+ platform,
47
68
  ...stats
48
69
  });
49
70
  } catch (err) {
@@ -61,6 +82,7 @@ router.get('/stats', (req, res) => {
61
82
  */
62
83
  router.get('/:scope/:fileName', (req, res) => {
63
84
  try {
85
+ const { platform, service } = getAgentsService(req);
64
86
  const { scope, fileName } = req.params;
65
87
  const { projectPath } = req.query;
66
88
 
@@ -78,7 +100,7 @@ router.get('/:scope/:fileName', (req, res) => {
78
100
  });
79
101
  }
80
102
 
81
- const agent = agentsService.getAgent(fileName, scope, projectPath || null);
103
+ const agent = service.getAgent(fileName, scope, projectPath || null);
82
104
 
83
105
  if (!agent) {
84
106
  return res.status(404).json({
@@ -89,6 +111,7 @@ router.get('/:scope/:fileName', (req, res) => {
89
111
 
90
112
  res.json({
91
113
  success: true,
114
+ platform,
92
115
  agent
93
116
  });
94
117
  } catch (err) {
@@ -107,6 +130,7 @@ router.get('/:scope/:fileName', (req, res) => {
107
130
  */
108
131
  router.post('/', (req, res) => {
109
132
  try {
133
+ const { platform, service } = getAgentsService(req);
110
134
  const { fileName, scope, projectPath, name, description, tools, model, permissionMode, skills, systemPrompt } = req.body;
111
135
 
112
136
  if (!fileName) {
@@ -130,7 +154,7 @@ router.post('/', (req, res) => {
130
154
  });
131
155
  }
132
156
 
133
- const agent = agentsService.createAgent({
157
+ const agent = service.createAgent({
134
158
  fileName,
135
159
  scope,
136
160
  projectPath: projectPath || null,
@@ -145,6 +169,7 @@ router.post('/', (req, res) => {
145
169
 
146
170
  res.json({
147
171
  success: true,
172
+ platform,
148
173
  agent,
149
174
  message: '代理创建成功'
150
175
  });
@@ -163,6 +188,7 @@ router.post('/', (req, res) => {
163
188
  */
164
189
  router.put('/:scope/:fileName', (req, res) => {
165
190
  try {
191
+ const { platform, service } = getAgentsService(req);
166
192
  const { scope, fileName } = req.params;
167
193
  const { projectPath, name, description, tools, model, permissionMode, skills, systemPrompt } = req.body;
168
194
 
@@ -180,7 +206,7 @@ router.put('/:scope/:fileName', (req, res) => {
180
206
  });
181
207
  }
182
208
 
183
- const agent = agentsService.updateAgent({
209
+ const agent = service.updateAgent({
184
210
  fileName,
185
211
  scope,
186
212
  projectPath: projectPath || null,
@@ -195,6 +221,7 @@ router.put('/:scope/:fileName', (req, res) => {
195
221
 
196
222
  res.json({
197
223
  success: true,
224
+ platform,
198
225
  agent,
199
226
  message: '代理更新成功'
200
227
  });
@@ -213,6 +240,7 @@ router.put('/:scope/:fileName', (req, res) => {
213
240
  */
214
241
  router.delete('/:scope/:fileName', (req, res) => {
215
242
  try {
243
+ const { platform, service } = getAgentsService(req);
216
244
  const { scope, fileName } = req.params;
217
245
  const { projectPath } = req.query;
218
246
 
@@ -230,9 +258,10 @@ router.delete('/:scope/:fileName', (req, res) => {
230
258
  });
231
259
  }
232
260
 
233
- const result = agentsService.deleteAgent(fileName, scope, projectPath || null);
261
+ const result = service.deleteAgent(fileName, scope, projectPath || null);
234
262
 
235
263
  res.json({
264
+ platform,
236
265
  success: result.success,
237
266
  message: result.message
238
267
  });
@@ -254,12 +283,14 @@ router.delete('/:scope/:fileName', (req, res) => {
254
283
  */
255
284
  router.get('/all', async (req, res) => {
256
285
  try {
286
+ const { platform, service } = getAgentsService(req);
257
287
  const { projectPath, refresh } = req.query;
258
288
  const forceRefresh = refresh === '1';
259
- const result = await agentsService.listAllAgents(projectPath || null, forceRefresh);
289
+ const result = await service.listAllAgents(projectPath || null, forceRefresh);
260
290
 
261
291
  res.json({
262
292
  success: true,
293
+ platform,
263
294
  ...result
264
295
  });
265
296
  } catch (err) {
@@ -277,9 +308,11 @@ router.get('/all', async (req, res) => {
277
308
  */
278
309
  router.get('/repos', (req, res) => {
279
310
  try {
280
- const repos = agentsService.getRepos();
311
+ const { platform, service } = getAgentsService(req);
312
+ const repos = service.getRepos();
281
313
  res.json({
282
314
  success: true,
315
+ platform,
283
316
  repos
284
317
  });
285
318
  } catch (err) {
@@ -298,6 +331,7 @@ router.get('/repos', (req, res) => {
298
331
  */
299
332
  router.post('/repos', (req, res) => {
300
333
  try {
334
+ const { platform, service } = getAgentsService(req);
301
335
  const { owner, name, branch = 'main', directory = '', enabled = true } = req.body;
302
336
 
303
337
  if (!owner || !name) {
@@ -307,10 +341,11 @@ router.post('/repos', (req, res) => {
307
341
  });
308
342
  }
309
343
 
310
- const repos = agentsService.addRepo({ owner, name, branch, directory, enabled });
344
+ const repos = service.addRepo({ owner, name, branch, directory, enabled });
311
345
 
312
346
  res.json({
313
347
  success: true,
348
+ platform,
314
349
  repos
315
350
  });
316
351
  } catch (err) {
@@ -329,12 +364,14 @@ router.post('/repos', (req, res) => {
329
364
  */
330
365
  router.delete('/repos/:owner/:name', (req, res) => {
331
366
  try {
367
+ const { platform, service } = getAgentsService(req);
332
368
  const { owner, name } = req.params;
333
369
  const { directory = '' } = req.query;
334
- const repos = agentsService.removeRepo(owner, name, directory);
370
+ const repos = service.removeRepo(owner, name, directory);
335
371
 
336
372
  res.json({
337
373
  success: true,
374
+ platform,
338
375
  repos
339
376
  });
340
377
  } catch (err) {
@@ -353,13 +390,15 @@ router.delete('/repos/:owner/:name', (req, res) => {
353
390
  */
354
391
  router.put('/repos/:owner/:name/toggle', (req, res) => {
355
392
  try {
393
+ const { platform, service } = getAgentsService(req);
356
394
  const { owner, name } = req.params;
357
395
  const { enabled, directory = '' } = req.body;
358
396
 
359
- const repos = agentsService.toggleRepo(owner, name, directory, enabled);
397
+ const repos = service.toggleRepo(owner, name, directory, enabled);
360
398
 
361
399
  res.json({
362
400
  success: true,
401
+ platform,
363
402
  repos
364
403
  });
365
404
  } catch (err) {
@@ -378,6 +417,7 @@ router.put('/repos/:owner/:name/toggle', (req, res) => {
378
417
  */
379
418
  router.post('/install', async (req, res) => {
380
419
  try {
420
+ const { platform, service } = getAgentsService(req);
381
421
  const agent = req.body;
382
422
 
383
423
  if (!agent || !agent.repoOwner || !agent.repoName) {
@@ -387,10 +427,11 @@ router.post('/install', async (req, res) => {
387
427
  });
388
428
  }
389
429
 
390
- const result = await agentsService.installFromRemote(agent);
430
+ const result = await service.installFromRemote(agent);
391
431
 
392
432
  res.json({
393
433
  success: true,
434
+ platform,
394
435
  ...result
395
436
  });
396
437
  } catch (err) {
@@ -409,6 +450,7 @@ router.post('/install', async (req, res) => {
409
450
  */
410
451
  router.post('/uninstall', (req, res) => {
411
452
  try {
453
+ const { platform, service } = getAgentsService(req);
412
454
  const { fileName } = req.body;
413
455
 
414
456
  if (!fileName) {
@@ -418,10 +460,11 @@ router.post('/uninstall', (req, res) => {
418
460
  });
419
461
  }
420
462
 
421
- const result = agentsService.uninstallAgent(fileName);
463
+ const result = service.uninstallAgent(fileName);
422
464
 
423
465
  res.json({
424
466
  success: true,
467
+ platform,
425
468
  ...result
426
469
  });
427
470
  } catch (err) {
@@ -12,10 +12,19 @@ const {
12
12
  } = require('../services/channels');
13
13
  const { getSchedulerState } = require('../services/channel-scheduler');
14
14
  const { getChannelHealthStatus, getAllChannelHealthStatus, resetChannelHealth } = require('../services/channel-health');
15
- const { testChannelSpeed, testMultipleChannels, getLatencyLevel } = require('../services/speed-test');
16
- const { fetchModelsFromProvider } = require('../services/model-detector');
15
+ const {
16
+ testChannelSpeed,
17
+ getLatencyLevel,
18
+ sanitizeBatchConcurrency,
19
+ runWithConcurrencyLimit
20
+ } = require('../services/speed-test');
21
+ const {
22
+ probeModelAvailability,
23
+ fetchModelsFromProvider
24
+ } = require('../services/model-detector');
17
25
  const { broadcastLog, broadcastProxyState, broadcastSchedulerState } = require('../websocket-server');
18
26
  const { clearRedirectCache } = require('../proxy-server');
27
+ const CLAUDE_GATEWAY_SOURCE_TYPE = 'claude';
19
28
 
20
29
  // GET /api/channels - Get all channels with health status
21
30
  router.get('/', (req, res) => {
@@ -66,16 +75,40 @@ router.get('/current', (req, res) => {
66
75
  // POST /api/channels - Create new channel
67
76
  router.post('/', (req, res) => {
68
77
  try {
69
- const { name, baseUrl, apiKey, websiteUrl, enabled, weight, maxConcurrency } = req.body;
78
+ const {
79
+ name,
80
+ baseUrl,
81
+ apiKey,
82
+ websiteUrl,
83
+ enabled,
84
+ weight,
85
+ maxConcurrency,
86
+ presetId,
87
+ modelConfig,
88
+ modelRedirects,
89
+ proxyUrl,
90
+ speedTestModel,
91
+ gatewaySourceType
92
+ } = req.body;
93
+
94
+ if (!name || !baseUrl) {
95
+ return res.status(400).json({ error: 'Missing required fields: name, baseUrl' });
96
+ }
70
97
 
71
- if (!name || !baseUrl || !apiKey) {
72
- return res.status(400).json({ error: 'Missing required fields' });
98
+ if (!apiKey) {
99
+ return res.status(400).json({ error: 'Missing required fields: apiKey' });
73
100
  }
74
101
 
75
102
  const channel = createChannel(name, baseUrl, apiKey, websiteUrl, {
76
103
  enabled,
77
104
  weight,
78
- maxConcurrency
105
+ maxConcurrency,
106
+ presetId,
107
+ modelConfig,
108
+ modelRedirects: modelRedirects || [],
109
+ proxyUrl: proxyUrl || '',
110
+ speedTestModel: speedTestModel || null,
111
+ gatewaySourceType
79
112
  });
80
113
  res.json({ channel });
81
114
  broadcastSchedulerState('claude', getSchedulerState('claude'));
@@ -114,10 +147,10 @@ router.put('/:id', (req, res) => {
114
147
  });
115
148
 
116
149
  // DELETE /api/channels/:id - Delete channel
117
- router.delete('/:id', (req, res) => {
150
+ router.delete('/:id', async (req, res) => {
118
151
  try {
119
152
  const { id } = req.params;
120
- const result = deleteChannel(id);
153
+ const result = await deleteChannel(id);
121
154
  res.json(result);
122
155
  broadcastSchedulerState('claude', getSchedulerState('claude'));
123
156
  } catch (error) {
@@ -213,9 +246,10 @@ router.post('/:id/speed-test', async (req, res) => {
213
246
  return res.status(404).json({ error: '渠道不存在' });
214
247
  }
215
248
 
216
- // Claude 渠道使用 'claude' 类型
217
- const result = await testChannelSpeed(channel, timeout, 'claude');
249
+ const speedTestType = CLAUDE_GATEWAY_SOURCE_TYPE;
250
+ const result = await testChannelSpeed(channel, timeout, speedTestType);
218
251
  result.level = getLatencyLevel(result.latency);
252
+ result.gatewaySourceType = speedTestType;
219
253
 
220
254
  res.json(result);
221
255
  } catch (error) {
@@ -228,12 +262,6 @@ router.post('/:id/speed-test', async (req, res) => {
228
262
  router.get('/:id/models', async (req, res) => {
229
263
  try {
230
264
  const { id } = req.params;
231
- const VALID_CHANNEL_TYPES = ['claude', 'codex', 'gemini', 'openai_compatible'];
232
- const { type = 'claude' } = req.query;
233
-
234
- if (!VALID_CHANNEL_TYPES.includes(type)) {
235
- return res.status(400).json({ error: 'Invalid channel type', channelId: id });
236
- }
237
265
 
238
266
  const channels = getAllChannels();
239
267
  const channel = channels.find(ch => ch.id === id);
@@ -242,23 +270,53 @@ router.get('/:id/models', async (req, res) => {
242
270
  return res.status(404).json({ error: '渠道不存在' });
243
271
  }
244
272
 
245
- // If no type specified or type is 'claude', auto-detect
246
- let channelType = type;
247
- if (!type || type === 'claude') {
248
- const { detectChannelType } = require('../services/model-detector');
249
- channelType = detectChannelType(channel);
250
- console.log(`[API] Auto-detected channel type: ${channelType} for ${channel.name}`);
273
+ const gatewaySourceType = CLAUDE_GATEWAY_SOURCE_TYPE;
274
+ const listResult = await fetchModelsFromProvider(channel, 'openai_compatible');
275
+ const listedModels = Array.isArray(listResult.models) ? listResult.models : [];
276
+ let result;
277
+
278
+ if (listedModels.length > 0) {
279
+ result = {
280
+ models: listedModels,
281
+ supported: true,
282
+ cached: !!listResult.cached,
283
+ fallbackUsed: false,
284
+ lastChecked: listResult.lastChecked || new Date().toISOString(),
285
+ error: null,
286
+ errorHint: null
287
+ };
288
+ } else {
289
+ const usingConfiguredProbe = !!listResult.disabledByConfig;
290
+ const probe = await probeModelAvailability(channel, gatewaySourceType, {
291
+ stopOnFirstAvailable: false
292
+ });
293
+ const probedModels = Array.isArray(probe.availableModels) ? probe.availableModels : [];
294
+
295
+ result = {
296
+ models: probedModels,
297
+ supported: probedModels.length > 0,
298
+ cached: !!probe.cached || !!listResult.cached,
299
+ fallbackUsed: probedModels.length > 0,
300
+ lastChecked: probe.lastChecked || listResult.lastChecked || new Date().toISOString(),
301
+ error: probedModels.length > 0 ? null : (listResult.error || '无法获取可用模型'),
302
+ errorHint: probedModels.length > 0
303
+ ? (usingConfiguredProbe ? '已按设置跳过 /v1/models,使用默认模型探测结果' : '模型列表接口不可用,已使用模型探测结果')
304
+ : (listResult.errorHint || (usingConfiguredProbe
305
+ ? '已按设置跳过 /v1/models,且默认模型探测无可用结果'
306
+ : '模型列表接口不可用且模型探测无可用结果'))
307
+ };
251
308
  }
252
309
 
253
- const result = await fetchModelsFromProvider(channel, channelType);
254
-
255
310
  res.json({
256
311
  channelId: id,
312
+ gatewaySourceType,
257
313
  models: result.models,
258
314
  supported: result.supported,
315
+ fallbackUsed: result.fallbackUsed,
259
316
  cached: result.cached,
260
317
  fetchedAt: result.lastChecked || new Date().toISOString(),
261
- error: result.error
318
+ error: result.error,
319
+ errorHint: result.errorHint
262
320
  });
263
321
  } catch (error) {
264
322
  console.error('Error fetching channel models:', error);
@@ -272,18 +330,36 @@ router.get('/:id/models', async (req, res) => {
272
330
  // POST /api/channels/speed-test-all - Test all channels speed
273
331
  router.post('/speed-test-all', async (req, res) => {
274
332
  try {
275
- const { timeout = 10000 } = req.body;
333
+ const { timeout = 10000, concurrency } = req.body || {};
276
334
  const channels = getAllChannels();
335
+ const safeConcurrency = sanitizeBatchConcurrency(concurrency);
277
336
 
278
337
  if (channels.length === 0) {
279
338
  return res.json({ results: [], message: '没有可测试的渠道' });
280
339
  }
281
340
 
282
- // Claude 渠道使用 'claude' 类型
283
- const results = await testMultipleChannels(channels, timeout, 'claude');
284
- // 添加延迟等级
285
- results.forEach(r => {
286
- r.level = getLatencyLevel(r.latency);
341
+ const results = await runWithConcurrencyLimit(
342
+ channels,
343
+ safeConcurrency,
344
+ async channel => {
345
+ const speedTestType = CLAUDE_GATEWAY_SOURCE_TYPE;
346
+ const result = await testChannelSpeed(channel, timeout, speedTestType);
347
+ result.level = getLatencyLevel(result.latency);
348
+ result.gatewaySourceType = speedTestType;
349
+ return result;
350
+ }
351
+ );
352
+
353
+ // 与其他渠道保持一致:成功在前,成功结果按延迟升序
354
+ results.sort((a, b) => {
355
+ if (a.success && !b.success) return -1;
356
+ if (!a.success && b.success) return 1;
357
+ if (a.success && b.success) {
358
+ const aLatency = (a.latency === null || a.latency === undefined) ? Infinity : a.latency;
359
+ const bLatency = (b.latency === null || b.latency === undefined) ? Infinity : b.latency;
360
+ return aLatency - bLatency;
361
+ }
362
+ return 0;
287
363
  });
288
364
 
289
365
  res.json({
@@ -292,7 +368,8 @@ router.post('/speed-test-all', async (req, res) => {
292
368
  total: results.length,
293
369
  success: results.filter(r => r.success).length,
294
370
  failed: results.filter(r => !r.success).length,
295
- avgLatency: calculateAvgLatency(results)
371
+ avgLatency: calculateAvgLatency(results),
372
+ concurrency: safeConcurrency
296
373
  }
297
374
  });
298
375
  } catch (error) {
@@ -303,7 +380,9 @@ router.post('/speed-test-all', async (req, res) => {
303
380
 
304
381
  // 计算平均延迟
305
382
  function calculateAvgLatency(results) {
306
- const successResults = results.filter(r => r.success && r.latency);
383
+ const successResults = results.filter(
384
+ r => r.success && r.latency !== null && r.latency !== undefined
385
+ );
307
386
  if (successResults.length === 0) return null;
308
387
  const sum = successResults.reduce((acc, r) => acc + r.latency, 0);
309
388
  return Math.round(sum / successResults.length);