@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
@@ -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);
@@ -13,9 +13,18 @@ const { getSchedulerState } = require('../services/channel-scheduler');
13
13
  const { getChannelHealthStatus, resetChannelHealth } = require('../services/channel-health');
14
14
  const { broadcastSchedulerState, broadcastLog } = require('../websocket-server');
15
15
  const { isCodexInstalled } = require('../services/codex-config');
16
- const { testChannelSpeed, testMultipleChannels, getLatencyLevel } = require('../services/speed-test');
16
+ const {
17
+ testChannelSpeed,
18
+ getLatencyLevel,
19
+ sanitizeBatchConcurrency,
20
+ runWithConcurrencyLimit
21
+ } = require('../services/speed-test');
17
22
  const { clearCodexRedirectCache } = require('../codex-proxy-server');
18
- const { fetchModelsFromProvider } = require('../services/model-detector');
23
+ const {
24
+ fetchModelsFromProvider,
25
+ probeModelAvailability,
26
+ } = require('../services/model-detector');
27
+ const CODEX_GATEWAY_SOURCE_TYPE = 'codex';
19
28
 
20
29
  module.exports = (config) => {
21
30
  /**
@@ -58,11 +67,50 @@ module.exports = (config) => {
58
67
  return res.status(404).json({ error: '渠道不存在' });
59
68
  }
60
69
 
61
- // Codex 渠道大多数是 OpenAI 兼容的
62
- const result = await fetchModelsFromProvider(channel, 'openai_compatible');
70
+ const gatewaySourceType = CODEX_GATEWAY_SOURCE_TYPE;
71
+ const listResult = await fetchModelsFromProvider(channel, 'openai_compatible');
72
+ const listedModels = Array.isArray(listResult.models) ? listResult.models : [];
73
+ let result;
74
+
75
+ if (listedModels.length > 0) {
76
+ result = listResult;
77
+ } else {
78
+ const usingConfiguredProbe = !!listResult.disabledByConfig;
79
+ const probe = await probeModelAvailability(channel, 'codex');
80
+ const probedModels = Array.isArray(probe.availableModels) ? probe.availableModels : [];
81
+
82
+ if (probedModels.length > 0) {
83
+ result = {
84
+ models: probedModels,
85
+ supported: true,
86
+ cached: !!probe.cached,
87
+ fallbackUsed: false,
88
+ lastChecked: probe.lastChecked || listResult.lastChecked || new Date().toISOString(),
89
+ error: null,
90
+ errorHint: listResult.error
91
+ ? (usingConfiguredProbe
92
+ ? '已按设置跳过 /v1/models,使用默认模型探测结果'
93
+ : '模型列表接口不可用,已自动切换为模型探测结果')
94
+ : null
95
+ };
96
+ } else {
97
+ result = {
98
+ models: [],
99
+ supported: false,
100
+ cached: !!probe.cached || !!listResult.cached,
101
+ fallbackUsed: false,
102
+ lastChecked: probe.lastChecked || listResult.lastChecked || new Date().toISOString(),
103
+ error: listResult.error || '无法探测可用模型',
104
+ errorHint: listResult.errorHint || (usingConfiguredProbe
105
+ ? '已按设置跳过 /v1/models,且默认模型探测无可用结果'
106
+ : '模型列表接口不可用且模型探测无可用结果')
107
+ };
108
+ }
109
+ }
63
110
 
64
111
  res.json({
65
112
  channelId: id,
113
+ gatewaySourceType,
66
114
  models: result.models,
67
115
  supported: result.supported,
68
116
  cached: result.cached,
@@ -91,10 +139,27 @@ module.exports = (config) => {
91
139
  return res.status(404).json({ error: 'Codex CLI not installed' });
92
140
  }
93
141
 
94
- const { name, providerKey, baseUrl, apiKey, websiteUrl, enabled, weight, maxConcurrency } = req.body;
142
+ const {
143
+ name,
144
+ providerKey,
145
+ baseUrl,
146
+ apiKey,
147
+ websiteUrl,
148
+ enabled,
149
+ weight,
150
+ maxConcurrency,
151
+ modelRedirects,
152
+ speedTestModel,
153
+ presetId,
154
+ gatewaySourceType
155
+ } = req.body;
156
+
157
+ if (!name || !providerKey || !baseUrl) {
158
+ return res.status(400).json({ error: 'Missing required fields: name, providerKey, baseUrl' });
159
+ }
95
160
 
96
- if (!name || !providerKey || !baseUrl || !apiKey) {
97
- return res.status(400).json({ error: 'Missing required fields' });
161
+ if (!apiKey) {
162
+ return res.status(400).json({ error: 'Missing required fields: apiKey' });
98
163
  }
99
164
 
100
165
  // wireApi 固定为 'responses' (OpenAI Responses API 格式)
@@ -102,7 +167,11 @@ module.exports = (config) => {
102
167
  websiteUrl,
103
168
  enabled,
104
169
  weight,
105
- maxConcurrency
170
+ maxConcurrency,
171
+ modelRedirects: modelRedirects || [],
172
+ speedTestModel: speedTestModel || null,
173
+ presetId: presetId || null,
174
+ gatewaySourceType
106
175
  });
107
176
  res.json(channel);
108
177
  broadcastSchedulerState('codex', getSchedulerState('codex'));
@@ -140,14 +209,14 @@ module.exports = (config) => {
140
209
  * DELETE /api/codex/channels/:channelId
141
210
  * 删除渠道
142
211
  */
143
- router.delete('/:channelId', (req, res) => {
212
+ router.delete('/:channelId', async (req, res) => {
144
213
  try {
145
214
  if (!isCodexInstalled()) {
146
215
  return res.status(404).json({ error: 'Codex CLI not installed' });
147
216
  }
148
217
 
149
218
  const { channelId } = req.params;
150
- const result = deleteChannel(channelId);
219
+ const result = await deleteChannel(channelId);
151
220
  res.json(result);
152
221
  broadcastSchedulerState('codex', getSchedulerState('codex'));
153
222
  } catch (err) {
@@ -217,8 +286,10 @@ module.exports = (config) => {
217
286
  return res.status(404).json({ error: '渠道不存在' });
218
287
  }
219
288
 
220
- const result = await testChannelSpeed(channel, timeout, 'codex');
289
+ const speedTestType = CODEX_GATEWAY_SOURCE_TYPE;
290
+ const result = await testChannelSpeed(channel, timeout, speedTestType);
221
291
  result.level = getLatencyLevel(result.latency);
292
+ result.gatewaySourceType = speedTestType;
222
293
 
223
294
  res.json(result);
224
295
  } catch (error) {
@@ -237,18 +308,37 @@ module.exports = (config) => {
237
308
  return res.json({ results: [], message: 'Codex CLI not installed' });
238
309
  }
239
310
 
240
- const { timeout = 10000 } = req.body;
311
+ const { timeout = 10000, concurrency } = req.body || {};
241
312
  const data = getChannels();
242
313
  const channels = data.channels || [];
314
+ const safeConcurrency = sanitizeBatchConcurrency(concurrency);
243
315
 
244
316
  if (channels.length === 0) {
245
317
  return res.json({ results: [], message: '没有可测试的渠道' });
246
318
  }
247
319
 
248
- // Codex 渠道使用 'codex' 类型
249
- const results = await testMultipleChannels(channels, timeout, 'codex');
250
- results.forEach(r => {
251
- r.level = getLatencyLevel(r.latency);
320
+ const results = await runWithConcurrencyLimit(
321
+ channels,
322
+ safeConcurrency,
323
+ async channel => {
324
+ const speedTestType = CODEX_GATEWAY_SOURCE_TYPE;
325
+ const result = await testChannelSpeed(channel, timeout, speedTestType);
326
+ result.level = getLatencyLevel(result.latency);
327
+ result.gatewaySourceType = speedTestType;
328
+ return result;
329
+ }
330
+ );
331
+
332
+ // 成功在前,成功结果按延迟升序
333
+ results.sort((a, b) => {
334
+ if (a.success && !b.success) return -1;
335
+ if (!a.success && b.success) return 1;
336
+ if (a.success && b.success) {
337
+ const aLatency = (a.latency === null || a.latency === undefined) ? Infinity : a.latency;
338
+ const bLatency = (b.latency === null || b.latency === undefined) ? Infinity : b.latency;
339
+ return aLatency - bLatency;
340
+ }
341
+ return 0;
252
342
  });
253
343
 
254
344
  res.json({
@@ -257,7 +347,8 @@ module.exports = (config) => {
257
347
  total: results.length,
258
348
  success: results.filter(r => r.success).length,
259
349
  failed: results.filter(r => !r.success).length,
260
- avgLatency: calculateAvgLatency(results)
350
+ avgLatency: calculateAvgLatency(results),
351
+ concurrency: safeConcurrency
261
352
  }
262
353
  });
263
354
  } catch (error) {
@@ -345,7 +436,9 @@ module.exports = (config) => {
345
436
 
346
437
  // 计算平均延迟
347
438
  function calculateAvgLatency(results) {
348
- const successResults = results.filter(r => r.success && r.latency);
439
+ const successResults = results.filter(
440
+ r => r.success && r.latency !== null && r.latency !== undefined
441
+ );
349
442
  if (successResults.length === 0) return null;
350
443
  const sum = successResults.reduce((acc, r) => acc + r.latency, 0);
351
444
  return Math.round(sum / successResults.length);
@@ -15,9 +15,9 @@ const {
15
15
  } = require('../services/codex-settings-manager');
16
16
  const { getChannels, getEnabledChannels } = require('../services/codex-channels');
17
17
  const { clearAllLogs } = require('../websocket-server');
18
+ const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
18
19
  const fs = require('fs');
19
20
  const path = require('path');
20
- const os = require('os');
21
21
 
22
22
  function sanitizeChannel(channel) {
23
23
  if (!channel) return null;
@@ -32,14 +32,24 @@ function sanitizeChannel(channel) {
32
32
 
33
33
  // 保存激活渠道ID
34
34
  function saveActiveChannelId(channelId) {
35
- const dir = path.join(os.homedir(), '.claude', 'cc-tool');
35
+ ensureStorageDirMigrated();
36
+ const filePath = PATHS.activeChannel.codex;
37
+ const dir = path.dirname(filePath);
36
38
  if (!fs.existsSync(dir)) {
37
39
  fs.mkdirSync(dir, { recursive: true });
38
40
  }
39
- const filePath = path.join(dir, 'codex-active-channel.json');
40
41
  fs.writeFileSync(filePath, JSON.stringify({ activeChannelId: channelId }, null, 2), 'utf8');
41
42
  }
42
43
 
44
+ function removeActiveChannelFile() {
45
+ ensureStorageDirMigrated();
46
+ const filePath = PATHS.activeChannel.codex;
47
+ if (fs.existsSync(filePath)) {
48
+ fs.unlinkSync(filePath);
49
+ console.log('[Codex Proxy] Removed codex-active-channel.json');
50
+ }
51
+ }
52
+
43
53
  // 获取代理状态
44
54
  router.get('/status', (req, res) => {
45
55
  try {
@@ -147,11 +157,7 @@ router.post('/stop', async (req, res) => {
147
157
  console.log('[Codex Proxy] Restored settings from backup');
148
158
 
149
159
  // 删除 active-channel.json
150
- const activeChannelPath = path.join(os.homedir(), '.claude', 'cc-tool', 'codex-active-channel.json');
151
- if (fs.existsSync(activeChannelPath)) {
152
- fs.unlinkSync(activeChannelPath);
153
- console.log('[Codex Proxy] Removed codex-active-channel.json');
154
- }
160
+ removeActiveChannelFile();
155
161
 
156
162
  const response = {
157
163
  success: true,