coding-tool-x 3.2.0

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 (185) hide show
  1. package/CHANGELOG.md +599 -0
  2. package/LICENSE +21 -0
  3. package/README.md +439 -0
  4. package/bin/ctx.js +8 -0
  5. package/dist/web/assets/Analytics-DN_YsnkW.js +39 -0
  6. package/dist/web/assets/Analytics-DuYvId7u.css +1 -0
  7. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  8. package/dist/web/assets/ConfigTemplates-DpXIMy0p.js +1 -0
  9. package/dist/web/assets/Home-38JTUlYt.js +1 -0
  10. package/dist/web/assets/Home-CjupSEWE.css +1 -0
  11. package/dist/web/assets/PluginManager-CX2tgq2H.js +1 -0
  12. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  13. package/dist/web/assets/ProjectList-C1lDcsn6.js +1 -0
  14. package/dist/web/assets/ProjectList-oJIyIRkP.css +1 -0
  15. package/dist/web/assets/SessionList-C55tjV7i.css +1 -0
  16. package/dist/web/assets/SessionList-CZ7T6rVx.js +1 -0
  17. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  18. package/dist/web/assets/SkillManager-DLN9f79y.js +1 -0
  19. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  20. package/dist/web/assets/WorkspaceManager-DxlHZkpZ.js +1 -0
  21. package/dist/web/assets/icons-DRrXwWZi.js +1 -0
  22. package/dist/web/assets/index-CetESrXw.css +1 -0
  23. package/dist/web/assets/index-Cfvn-2Gb.js +2 -0
  24. package/dist/web/assets/markdown-BfC0goYb.css +10 -0
  25. package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
  26. package/dist/web/assets/naive-ui-DlpKk-8M.js +1 -0
  27. package/dist/web/assets/vendors-DMjSfzlv.js +7 -0
  28. package/dist/web/assets/vue-vendor-DET08QYg.js +45 -0
  29. package/dist/web/favicon.ico +0 -0
  30. package/dist/web/index.html +20 -0
  31. package/dist/web/logo.png +0 -0
  32. package/docs/bannel.png +0 -0
  33. package/docs/home.png +0 -0
  34. package/docs/logo.png +0 -0
  35. package/docs/model-redirection.md +251 -0
  36. package/docs/multi-channel-load-balancing.md +249 -0
  37. package/package.json +80 -0
  38. package/src/commands/channels.js +551 -0
  39. package/src/commands/cli-type.js +101 -0
  40. package/src/commands/daemon.js +365 -0
  41. package/src/commands/doctor.js +333 -0
  42. package/src/commands/export-config.js +205 -0
  43. package/src/commands/list.js +222 -0
  44. package/src/commands/logs.js +261 -0
  45. package/src/commands/plugin.js +585 -0
  46. package/src/commands/port-config.js +135 -0
  47. package/src/commands/proxy-control.js +264 -0
  48. package/src/commands/proxy.js +152 -0
  49. package/src/commands/resume.js +137 -0
  50. package/src/commands/search.js +190 -0
  51. package/src/commands/security.js +37 -0
  52. package/src/commands/stats.js +398 -0
  53. package/src/commands/switch.js +48 -0
  54. package/src/commands/toggle-proxy.js +247 -0
  55. package/src/commands/ui.js +99 -0
  56. package/src/commands/update.js +97 -0
  57. package/src/commands/workspace.js +454 -0
  58. package/src/config/default.js +69 -0
  59. package/src/config/loader.js +149 -0
  60. package/src/config/model-metadata.js +167 -0
  61. package/src/config/model-metadata.json +125 -0
  62. package/src/config/model-pricing.js +35 -0
  63. package/src/config/paths.js +190 -0
  64. package/src/index.js +680 -0
  65. package/src/plugins/constants.js +15 -0
  66. package/src/plugins/event-bus.js +54 -0
  67. package/src/plugins/manifest-validator.js +129 -0
  68. package/src/plugins/plugin-api.js +128 -0
  69. package/src/plugins/plugin-installer.js +601 -0
  70. package/src/plugins/plugin-loader.js +229 -0
  71. package/src/plugins/plugin-manager.js +170 -0
  72. package/src/plugins/registry.js +152 -0
  73. package/src/plugins/schema/plugin-manifest.json +115 -0
  74. package/src/reset-config.js +94 -0
  75. package/src/server/api/agents.js +826 -0
  76. package/src/server/api/aliases.js +36 -0
  77. package/src/server/api/channels.js +368 -0
  78. package/src/server/api/claude-hooks.js +480 -0
  79. package/src/server/api/codex-channels.js +417 -0
  80. package/src/server/api/codex-projects.js +104 -0
  81. package/src/server/api/codex-proxy.js +195 -0
  82. package/src/server/api/codex-sessions.js +483 -0
  83. package/src/server/api/codex-statistics.js +57 -0
  84. package/src/server/api/commands.js +482 -0
  85. package/src/server/api/config-export.js +212 -0
  86. package/src/server/api/config-registry.js +357 -0
  87. package/src/server/api/config-sync.js +155 -0
  88. package/src/server/api/config-templates.js +248 -0
  89. package/src/server/api/config.js +521 -0
  90. package/src/server/api/convert.js +260 -0
  91. package/src/server/api/dashboard.js +142 -0
  92. package/src/server/api/env.js +144 -0
  93. package/src/server/api/favorites.js +77 -0
  94. package/src/server/api/gemini-channels.js +366 -0
  95. package/src/server/api/gemini-projects.js +91 -0
  96. package/src/server/api/gemini-proxy.js +173 -0
  97. package/src/server/api/gemini-sessions.js +376 -0
  98. package/src/server/api/gemini-statistics.js +57 -0
  99. package/src/server/api/health-check.js +31 -0
  100. package/src/server/api/mcp.js +399 -0
  101. package/src/server/api/opencode-channels.js +419 -0
  102. package/src/server/api/opencode-projects.js +99 -0
  103. package/src/server/api/opencode-proxy.js +207 -0
  104. package/src/server/api/opencode-sessions.js +327 -0
  105. package/src/server/api/opencode-statistics.js +57 -0
  106. package/src/server/api/plugins.js +463 -0
  107. package/src/server/api/pm2-autostart.js +269 -0
  108. package/src/server/api/projects.js +124 -0
  109. package/src/server/api/prompts.js +279 -0
  110. package/src/server/api/proxy.js +306 -0
  111. package/src/server/api/security.js +53 -0
  112. package/src/server/api/sessions.js +514 -0
  113. package/src/server/api/settings.js +142 -0
  114. package/src/server/api/skills.js +570 -0
  115. package/src/server/api/statistics.js +238 -0
  116. package/src/server/api/ui-config.js +64 -0
  117. package/src/server/api/workspaces.js +456 -0
  118. package/src/server/codex-proxy-server.js +681 -0
  119. package/src/server/dev-server.js +26 -0
  120. package/src/server/gemini-proxy-server.js +610 -0
  121. package/src/server/index.js +422 -0
  122. package/src/server/opencode-proxy-server.js +4771 -0
  123. package/src/server/proxy-server.js +669 -0
  124. package/src/server/services/agents-service.js +1137 -0
  125. package/src/server/services/alias.js +71 -0
  126. package/src/server/services/channel-health.js +234 -0
  127. package/src/server/services/channel-scheduler.js +240 -0
  128. package/src/server/services/channels.js +447 -0
  129. package/src/server/services/codex-channels.js +705 -0
  130. package/src/server/services/codex-config.js +90 -0
  131. package/src/server/services/codex-parser.js +322 -0
  132. package/src/server/services/codex-sessions.js +936 -0
  133. package/src/server/services/codex-settings-manager.js +619 -0
  134. package/src/server/services/codex-speed-test-template.json +24 -0
  135. package/src/server/services/codex-statistics-service.js +161 -0
  136. package/src/server/services/commands-service.js +574 -0
  137. package/src/server/services/config-export-service.js +1165 -0
  138. package/src/server/services/config-registry-service.js +828 -0
  139. package/src/server/services/config-sync-manager.js +941 -0
  140. package/src/server/services/config-sync-service.js +504 -0
  141. package/src/server/services/config-templates-service.js +913 -0
  142. package/src/server/services/enhanced-cache.js +196 -0
  143. package/src/server/services/env-checker.js +409 -0
  144. package/src/server/services/env-manager.js +436 -0
  145. package/src/server/services/favorites.js +165 -0
  146. package/src/server/services/format-converter.js +620 -0
  147. package/src/server/services/gemini-channels.js +459 -0
  148. package/src/server/services/gemini-config.js +73 -0
  149. package/src/server/services/gemini-sessions.js +689 -0
  150. package/src/server/services/gemini-settings-manager.js +263 -0
  151. package/src/server/services/gemini-statistics-service.js +157 -0
  152. package/src/server/services/health-check.js +85 -0
  153. package/src/server/services/mcp-client.js +790 -0
  154. package/src/server/services/mcp-service.js +1732 -0
  155. package/src/server/services/model-detector.js +1245 -0
  156. package/src/server/services/network-access.js +80 -0
  157. package/src/server/services/opencode-channels.js +366 -0
  158. package/src/server/services/opencode-gateway-adapters.js +1168 -0
  159. package/src/server/services/opencode-gateway-converter.js +639 -0
  160. package/src/server/services/opencode-sessions.js +931 -0
  161. package/src/server/services/opencode-settings-manager.js +478 -0
  162. package/src/server/services/opencode-statistics-service.js +161 -0
  163. package/src/server/services/plugins-service.js +1268 -0
  164. package/src/server/services/prompts-service.js +534 -0
  165. package/src/server/services/proxy-runtime.js +79 -0
  166. package/src/server/services/repo-scanner-base.js +708 -0
  167. package/src/server/services/request-logger.js +130 -0
  168. package/src/server/services/response-decoder.js +21 -0
  169. package/src/server/services/security-config.js +131 -0
  170. package/src/server/services/session-cache.js +127 -0
  171. package/src/server/services/session-converter.js +577 -0
  172. package/src/server/services/sessions.js +900 -0
  173. package/src/server/services/settings-manager.js +163 -0
  174. package/src/server/services/skill-service.js +1482 -0
  175. package/src/server/services/speed-test.js +1146 -0
  176. package/src/server/services/statistics-service.js +1043 -0
  177. package/src/server/services/ui-config.js +132 -0
  178. package/src/server/services/workspace-service.js +830 -0
  179. package/src/server/utils/pricing.js +73 -0
  180. package/src/server/websocket-server.js +513 -0
  181. package/src/ui/menu.js +139 -0
  182. package/src/ui/prompts.js +100 -0
  183. package/src/utils/format.js +43 -0
  184. package/src/utils/port-helper.js +108 -0
  185. package/src/utils/session.js +240 -0
@@ -0,0 +1,36 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const { setAlias, deleteAlias } = require('../services/alias');
4
+
5
+ module.exports = () => {
6
+ // POST /api/aliases - Set alias for a session
7
+ router.post('/', (req, res) => {
8
+ try {
9
+ const { sessionId, alias } = req.body;
10
+
11
+ if (!sessionId || !alias) {
12
+ return res.status(400).json({ error: 'sessionId and alias are required' });
13
+ }
14
+
15
+ const aliases = setAlias(sessionId, alias);
16
+ res.json({ success: true, aliases });
17
+ } catch (error) {
18
+ console.error('Error setting alias:', error);
19
+ res.status(500).json({ error: error.message });
20
+ }
21
+ });
22
+
23
+ // DELETE /api/aliases/:sessionId - Delete alias
24
+ router.delete('/:sessionId', (req, res) => {
25
+ try {
26
+ const { sessionId } = req.params;
27
+ const aliases = deleteAlias(sessionId);
28
+ res.json({ success: true, aliases });
29
+ } catch (error) {
30
+ console.error('Error deleting alias:', error);
31
+ res.status(500).json({ error: error.message });
32
+ }
33
+ });
34
+
35
+ return router;
36
+ };
@@ -0,0 +1,368 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const {
4
+ getAllChannels,
5
+ applyChannelToSettings,
6
+ createChannel,
7
+ updateChannel,
8
+ deleteChannel,
9
+ getCurrentSettings,
10
+ getBestChannelForRestore,
11
+ updateClaudeSettingsWithModelConfig
12
+ } = require('../services/channels');
13
+ const { getSchedulerState } = require('../services/channel-scheduler');
14
+ const { getChannelHealthStatus, getAllChannelHealthStatus, resetChannelHealth } = require('../services/channel-health');
15
+ const {
16
+ testChannelSpeed,
17
+ getLatencyLevel,
18
+ sanitizeBatchConcurrency,
19
+ runWithConcurrencyLimit
20
+ } = require('../services/speed-test');
21
+ const { getDefaultSpeedTestModelByToolType } = require('../../config/model-metadata');
22
+ const { broadcastLog, broadcastProxyState, broadcastSchedulerState } = require('../websocket-server');
23
+ const { clearRedirectCache } = require('../proxy-server');
24
+ const CLAUDE_GATEWAY_SOURCE_TYPE = 'claude';
25
+
26
+ function getDefaultClaudeModel() {
27
+ return getDefaultSpeedTestModelByToolType('claude');
28
+ }
29
+
30
+ // GET /api/channels - Get all channels with health status
31
+ router.get('/', (req, res) => {
32
+ try {
33
+ const channels = getAllChannels();
34
+ // 为每个渠道附加健康状态
35
+ const channelsWithHealth = channels.map(ch => ({
36
+ ...ch,
37
+ health: getChannelHealthStatus(ch.id)
38
+ }));
39
+ res.json({ channels: channelsWithHealth });
40
+ } catch (error) {
41
+ console.error('Error fetching channels:', error);
42
+ res.status(500).json({ error: error.message });
43
+ }
44
+ });
45
+
46
+ router.get('/pool/status', (req, res) => {
47
+ try {
48
+ const source = req.query.source || 'claude';
49
+ const scheduler = getSchedulerState(source);
50
+ res.json({ source, scheduler });
51
+ } catch (error) {
52
+ res.status(500).json({ error: error.message });
53
+ }
54
+ });
55
+
56
+ // GET /api/channels/current - Get current settings
57
+ router.get('/current', (req, res) => {
58
+ try {
59
+ const settings = getCurrentSettings();
60
+ const channels = getAllChannels();
61
+ let currentChannel = null;
62
+
63
+ if (settings) {
64
+ currentChannel = channels.find(ch =>
65
+ ch.baseUrl === settings.baseUrl && ch.apiKey === settings.apiKey
66
+ );
67
+ }
68
+
69
+ res.json({ channel: currentChannel, settings });
70
+ } catch (error) {
71
+ console.error('Error fetching current settings:', error);
72
+ res.status(500).json({ error: error.message });
73
+ }
74
+ });
75
+
76
+ // POST /api/channels - Create new channel
77
+ router.post('/', (req, res) => {
78
+ try {
79
+ const {
80
+ name,
81
+ baseUrl,
82
+ apiKey,
83
+ websiteUrl,
84
+ enabled,
85
+ weight,
86
+ maxConcurrency,
87
+ presetId,
88
+ modelConfig,
89
+ modelRedirects,
90
+ proxyUrl,
91
+ speedTestModel,
92
+ gatewaySourceType
93
+ } = req.body;
94
+
95
+ if (!name || !baseUrl) {
96
+ return res.status(400).json({ error: 'Missing required fields: name, baseUrl' });
97
+ }
98
+
99
+ if (!apiKey) {
100
+ return res.status(400).json({ error: 'Missing required fields: apiKey' });
101
+ }
102
+
103
+ const channel = createChannel(name, baseUrl, apiKey, websiteUrl, {
104
+ enabled,
105
+ weight,
106
+ maxConcurrency,
107
+ presetId,
108
+ modelConfig,
109
+ modelRedirects: modelRedirects || [],
110
+ proxyUrl: proxyUrl || '',
111
+ speedTestModel: speedTestModel || null,
112
+ gatewaySourceType
113
+ });
114
+ res.json({ channel });
115
+ broadcastSchedulerState('claude', getSchedulerState('claude'));
116
+ } catch (error) {
117
+ console.error('Error creating channel:', error);
118
+ res.status(500).json({ error: error.message });
119
+ }
120
+ });
121
+
122
+ // GET /api/channels/best-for-restore - Get best channel for restore (must be before /:id)
123
+ router.get('/best-for-restore', (req, res) => {
124
+ try {
125
+ const channel = getBestChannelForRestore();
126
+ res.json({ channel });
127
+ } catch (error) {
128
+ console.error('Error getting best channel for restore:', error);
129
+ res.status(500).json({ error: error.message });
130
+ }
131
+ });
132
+
133
+ // PUT /api/channels/:id - Update channel
134
+ router.put('/:id', (req, res) => {
135
+ try {
136
+ const { id } = req.params;
137
+ const updates = req.body;
138
+
139
+ const channel = updateChannel(id, updates);
140
+ // 清除该渠道的模型重定向日志缓存,使下次请求时重新打印
141
+ clearRedirectCache(id);
142
+ res.json({ channel });
143
+ broadcastSchedulerState('claude', getSchedulerState('claude'));
144
+ } catch (error) {
145
+ console.error('Error updating channel:', error);
146
+ res.status(500).json({ error: error.message });
147
+ }
148
+ });
149
+
150
+ // DELETE /api/channels/:id - Delete channel
151
+ router.delete('/:id', async (req, res) => {
152
+ try {
153
+ const { id } = req.params;
154
+ const result = await deleteChannel(id);
155
+ res.json(result);
156
+ broadcastSchedulerState('claude', getSchedulerState('claude'));
157
+ } catch (error) {
158
+ console.error('Error deleting channel:', error);
159
+ res.status(500).json({ error: error.message });
160
+ }
161
+ });
162
+
163
+ router.post('/:id/apply-to-settings', async (req, res) => {
164
+ try {
165
+ const { id } = req.params;
166
+ const channel = applyChannelToSettings(id);
167
+
168
+ // Check if proxy is running
169
+ const { getProxyStatus } = require('../proxy-server');
170
+ const proxyStatus = getProxyStatus();
171
+
172
+ broadcastLog({
173
+ type: 'action',
174
+ action: 'apply_settings',
175
+ message: `已将 (${channel.name}) 渠道写入配置文件中`,
176
+ channelName: channel.name,
177
+ timestamp: Date.now(),
178
+ source: 'claude'
179
+ });
180
+
181
+ // Stop proxy if running
182
+ if (proxyStatus && proxyStatus.running) {
183
+ console.log(`Proxy is running, stopping to apply channel settings: ${channel.name}`);
184
+
185
+ // Stop proxy and restore backup
186
+ const { stopProxyServer } = require('../proxy-server');
187
+ await stopProxyServer({ clearStartTime: false });
188
+
189
+ // Re-apply channel settings after proxy stop to prevent race condition
190
+ // (stopProxyServer restores backup, then we overwrite it with current channel)
191
+ updateClaudeSettingsWithModelConfig(channel);
192
+
193
+ console.log(`✅ 已停���动态切换,默认使用当前渠道`);
194
+ broadcastLog({
195
+ type: 'action',
196
+ action: 'stop_proxy',
197
+ message: `已停止动态切换,默认使用当前渠道`,
198
+ timestamp: Date.now(),
199
+ source: 'claude'
200
+ });
201
+
202
+ // 广播代理状态更新,通知前端代理已停止
203
+ const { broadcastProxyState } = require('../websocket-server');
204
+ broadcastProxyState('claude', {
205
+ running: false,
206
+ port: null,
207
+ runtime: null,
208
+ startTime: null
209
+ }, null, getAllChannels());
210
+ }
211
+
212
+ res.json({
213
+ message: `已将 (${channel.name}) 渠道写入配置文件中`,
214
+ channel
215
+ });
216
+ } catch (error) {
217
+ console.error('Error applying channel to settings:', error);
218
+ res.status(500).json({ error: error.message });
219
+ }
220
+ });
221
+
222
+ // POST /api/channels/:id/reset-health - Reset channel health status
223
+ router.post('/:id/reset-health', (req, res) => {
224
+ try {
225
+ const { id } = req.params;
226
+ resetChannelHealth(id, 'claude');
227
+ res.json({
228
+ success: true,
229
+ message: '渠道健康状态已重置',
230
+ health: getChannelHealthStatus(id)
231
+ });
232
+ } catch (error) {
233
+ console.error('Error resetting channel health:', error);
234
+ res.status(500).json({ error: error.message });
235
+ }
236
+ });
237
+
238
+ // POST /api/channels/:id/speed-test - Test single channel speed
239
+ router.post('/:id/speed-test', async (req, res) => {
240
+ try {
241
+ const { id } = req.params;
242
+ const { timeout = 10000 } = req.body;
243
+ const channels = getAllChannels();
244
+ const channel = channels.find(ch => ch.id === id);
245
+
246
+ if (!channel) {
247
+ return res.status(404).json({ error: '渠道不存在' });
248
+ }
249
+
250
+ const speedTestType = CLAUDE_GATEWAY_SOURCE_TYPE;
251
+ const result = await testChannelSpeed(channel, timeout, speedTestType);
252
+ result.level = getLatencyLevel(result.latency);
253
+ result.gatewaySourceType = speedTestType;
254
+
255
+ res.json(result);
256
+ } catch (error) {
257
+ console.error('Error testing channel speed:', error);
258
+ res.status(500).json({ error: error.message });
259
+ }
260
+ });
261
+
262
+ // GET /api/channels/:id/models - Get available models for a channel
263
+ router.get('/:id/models', async (req, res) => {
264
+ try {
265
+ const { id } = req.params;
266
+
267
+ const channels = getAllChannels();
268
+ const channel = channels.find(ch => ch.id === id);
269
+
270
+ if (!channel) {
271
+ return res.status(404).json({ error: '渠道不存在' });
272
+ }
273
+
274
+ const gatewaySourceType = CLAUDE_GATEWAY_SOURCE_TYPE;
275
+ const models = [getDefaultClaudeModel()];
276
+ const now = new Date().toISOString();
277
+ const result = {
278
+ models,
279
+ supported: models.length > 0,
280
+ cached: false,
281
+ fallbackUsed: false,
282
+ lastChecked: now,
283
+ error: models.length > 0 ? null : '未配置默认模型列表',
284
+ errorHint: models.length > 0 ? null : '请在设置中配置 Claude 默认模型'
285
+ };
286
+
287
+ res.json({
288
+ channelId: id,
289
+ gatewaySourceType,
290
+ models: result.models,
291
+ supported: result.supported,
292
+ fallbackUsed: result.fallbackUsed,
293
+ cached: result.cached,
294
+ fetchedAt: result.lastChecked || new Date().toISOString(),
295
+ error: result.error,
296
+ errorHint: result.errorHint
297
+ });
298
+ } catch (error) {
299
+ console.error('Error fetching channel models:', error);
300
+ res.status(500).json({
301
+ error: '获取模型列表失败',
302
+ channelId: req.params.id
303
+ });
304
+ }
305
+ });
306
+
307
+ // POST /api/channels/speed-test-all - Test all channels speed
308
+ router.post('/speed-test-all', async (req, res) => {
309
+ try {
310
+ const { timeout = 10000, concurrency } = req.body || {};
311
+ const channels = getAllChannels();
312
+ const safeConcurrency = sanitizeBatchConcurrency(concurrency);
313
+
314
+ if (channels.length === 0) {
315
+ return res.json({ results: [], message: '没有可测试的渠道' });
316
+ }
317
+
318
+ const results = await runWithConcurrencyLimit(
319
+ channels,
320
+ safeConcurrency,
321
+ async channel => {
322
+ const speedTestType = CLAUDE_GATEWAY_SOURCE_TYPE;
323
+ const result = await testChannelSpeed(channel, timeout, speedTestType);
324
+ result.level = getLatencyLevel(result.latency);
325
+ result.gatewaySourceType = speedTestType;
326
+ return result;
327
+ }
328
+ );
329
+
330
+ // 与其他渠道保持一致:成功在前,成功结果按延迟升序
331
+ results.sort((a, b) => {
332
+ if (a.success && !b.success) return -1;
333
+ if (!a.success && b.success) return 1;
334
+ if (a.success && b.success) {
335
+ const aLatency = (a.latency === null || a.latency === undefined) ? Infinity : a.latency;
336
+ const bLatency = (b.latency === null || b.latency === undefined) ? Infinity : b.latency;
337
+ return aLatency - bLatency;
338
+ }
339
+ return 0;
340
+ });
341
+
342
+ res.json({
343
+ results,
344
+ summary: {
345
+ total: results.length,
346
+ success: results.filter(r => r.success).length,
347
+ failed: results.filter(r => !r.success).length,
348
+ avgLatency: calculateAvgLatency(results),
349
+ concurrency: safeConcurrency
350
+ }
351
+ });
352
+ } catch (error) {
353
+ console.error('Error testing all channels speed:', error);
354
+ res.status(500).json({ error: error.message });
355
+ }
356
+ });
357
+
358
+ // 计算平均延迟
359
+ function calculateAvgLatency(results) {
360
+ const successResults = results.filter(
361
+ r => r.success && r.latency !== null && r.latency !== undefined
362
+ );
363
+ if (successResults.length === 0) return null;
364
+ const sum = successResults.reduce((acc, r) => acc + r.latency, 0);
365
+ return Math.round(sum / successResults.length);
366
+ }
367
+
368
+ module.exports = router;