@adversity/coding-tool-x 2.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 (125) hide show
  1. package/CHANGELOG.md +333 -0
  2. package/LICENSE +21 -0
  3. package/README.md +404 -0
  4. package/bin/ctx.js +8 -0
  5. package/dist/web/assets/index-D1AYlFLZ.js +3220 -0
  6. package/dist/web/assets/index-aL3cKxSK.css +41 -0
  7. package/dist/web/favicon.ico +0 -0
  8. package/dist/web/index.html +14 -0
  9. package/dist/web/logo.png +0 -0
  10. package/docs/CHANGELOG.md +582 -0
  11. package/docs/DIRECTORY_MIGRATION.md +112 -0
  12. package/docs/PROJECT_STRUCTURE.md +396 -0
  13. package/docs/bannel.png +0 -0
  14. package/docs/home.png +0 -0
  15. package/docs/logo.png +0 -0
  16. package/docs/multi-channel-load-balancing.md +249 -0
  17. package/package.json +73 -0
  18. package/src/commands/channels.js +504 -0
  19. package/src/commands/cli-type.js +99 -0
  20. package/src/commands/daemon.js +286 -0
  21. package/src/commands/doctor.js +332 -0
  22. package/src/commands/list.js +222 -0
  23. package/src/commands/logs.js +259 -0
  24. package/src/commands/port-config.js +115 -0
  25. package/src/commands/proxy-control.js +258 -0
  26. package/src/commands/proxy.js +152 -0
  27. package/src/commands/resume.js +137 -0
  28. package/src/commands/search.js +190 -0
  29. package/src/commands/stats.js +224 -0
  30. package/src/commands/switch.js +48 -0
  31. package/src/commands/toggle-proxy.js +222 -0
  32. package/src/commands/ui.js +92 -0
  33. package/src/commands/workspace.js +454 -0
  34. package/src/config/default.js +40 -0
  35. package/src/config/loader.js +75 -0
  36. package/src/config/paths.js +121 -0
  37. package/src/index.js +373 -0
  38. package/src/reset-config.js +92 -0
  39. package/src/server/api/agents.js +248 -0
  40. package/src/server/api/aliases.js +36 -0
  41. package/src/server/api/channels.js +258 -0
  42. package/src/server/api/claude-hooks.js +480 -0
  43. package/src/server/api/codex-channels.js +312 -0
  44. package/src/server/api/codex-projects.js +91 -0
  45. package/src/server/api/codex-proxy.js +182 -0
  46. package/src/server/api/codex-sessions.js +491 -0
  47. package/src/server/api/codex-statistics.js +57 -0
  48. package/src/server/api/commands.js +245 -0
  49. package/src/server/api/config-templates.js +182 -0
  50. package/src/server/api/config.js +147 -0
  51. package/src/server/api/convert.js +127 -0
  52. package/src/server/api/dashboard.js +125 -0
  53. package/src/server/api/env.js +144 -0
  54. package/src/server/api/favorites.js +77 -0
  55. package/src/server/api/gemini-channels.js +261 -0
  56. package/src/server/api/gemini-projects.js +91 -0
  57. package/src/server/api/gemini-proxy.js +160 -0
  58. package/src/server/api/gemini-sessions.js +397 -0
  59. package/src/server/api/gemini-statistics.js +57 -0
  60. package/src/server/api/health-check.js +118 -0
  61. package/src/server/api/mcp.js +336 -0
  62. package/src/server/api/pm2-autostart.js +269 -0
  63. package/src/server/api/projects.js +124 -0
  64. package/src/server/api/prompts.js +279 -0
  65. package/src/server/api/proxy.js +235 -0
  66. package/src/server/api/rules.js +271 -0
  67. package/src/server/api/sessions.js +595 -0
  68. package/src/server/api/settings.js +61 -0
  69. package/src/server/api/skills.js +305 -0
  70. package/src/server/api/statistics.js +91 -0
  71. package/src/server/api/terminal.js +202 -0
  72. package/src/server/api/ui-config.js +64 -0
  73. package/src/server/api/workspaces.js +407 -0
  74. package/src/server/codex-proxy-server.js +538 -0
  75. package/src/server/dev-server.js +26 -0
  76. package/src/server/gemini-proxy-server.js +518 -0
  77. package/src/server/index.js +305 -0
  78. package/src/server/proxy-server.js +469 -0
  79. package/src/server/services/agents-service.js +354 -0
  80. package/src/server/services/alias.js +71 -0
  81. package/src/server/services/channel-health.js +234 -0
  82. package/src/server/services/channel-scheduler.js +234 -0
  83. package/src/server/services/channels.js +347 -0
  84. package/src/server/services/codex-channels.js +625 -0
  85. package/src/server/services/codex-config.js +90 -0
  86. package/src/server/services/codex-parser.js +322 -0
  87. package/src/server/services/codex-sessions.js +665 -0
  88. package/src/server/services/codex-settings-manager.js +397 -0
  89. package/src/server/services/codex-speed-test-template.json +24 -0
  90. package/src/server/services/codex-statistics-service.js +255 -0
  91. package/src/server/services/commands-service.js +360 -0
  92. package/src/server/services/config-templates-service.js +732 -0
  93. package/src/server/services/env-checker.js +307 -0
  94. package/src/server/services/env-manager.js +300 -0
  95. package/src/server/services/favorites.js +163 -0
  96. package/src/server/services/gemini-channels.js +333 -0
  97. package/src/server/services/gemini-config.js +73 -0
  98. package/src/server/services/gemini-sessions.js +689 -0
  99. package/src/server/services/gemini-settings-manager.js +263 -0
  100. package/src/server/services/gemini-statistics-service.js +253 -0
  101. package/src/server/services/health-check.js +399 -0
  102. package/src/server/services/mcp-service.js +1188 -0
  103. package/src/server/services/prompts-service.js +492 -0
  104. package/src/server/services/proxy-runtime.js +79 -0
  105. package/src/server/services/pty-manager.js +435 -0
  106. package/src/server/services/rules-service.js +401 -0
  107. package/src/server/services/session-cache.js +127 -0
  108. package/src/server/services/session-converter.js +577 -0
  109. package/src/server/services/sessions.js +757 -0
  110. package/src/server/services/settings-manager.js +163 -0
  111. package/src/server/services/skill-service.js +965 -0
  112. package/src/server/services/speed-test.js +545 -0
  113. package/src/server/services/statistics-service.js +386 -0
  114. package/src/server/services/terminal-commands.js +155 -0
  115. package/src/server/services/terminal-config.js +140 -0
  116. package/src/server/services/terminal-detector.js +306 -0
  117. package/src/server/services/ui-config.js +130 -0
  118. package/src/server/services/workspace-service.js +662 -0
  119. package/src/server/utils/pricing.js +41 -0
  120. package/src/server/websocket-server.js +557 -0
  121. package/src/ui/menu.js +129 -0
  122. package/src/ui/prompts.js +100 -0
  123. package/src/utils/format.js +43 -0
  124. package/src/utils/port-helper.js +94 -0
  125. package/src/utils/session.js +239 -0
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Agents API 路由
3
+ *
4
+ * 管理 Claude Code 自定义代理
5
+ */
6
+
7
+ const express = require('express');
8
+ const { AgentsService } = require('../services/agents-service');
9
+
10
+ const router = express.Router();
11
+ const agentsService = new AgentsService();
12
+
13
+ /**
14
+ * 获取代理列表
15
+ * GET /api/agents
16
+ * Query: projectPath - 项目路径(可选,用于获取项目级代理)
17
+ */
18
+ router.get('/', (req, res) => {
19
+ try {
20
+ const { projectPath } = req.query;
21
+ const result = agentsService.listAgents(projectPath || null);
22
+
23
+ res.json({
24
+ success: true,
25
+ ...result
26
+ });
27
+ } catch (err) {
28
+ console.error('[Agents API] List agents error:', err);
29
+ res.status(500).json({
30
+ success: false,
31
+ message: err.message
32
+ });
33
+ }
34
+ });
35
+
36
+ /**
37
+ * 获取代理统计
38
+ * GET /api/agents/stats
39
+ */
40
+ router.get('/stats', (req, res) => {
41
+ try {
42
+ const { projectPath } = req.query;
43
+ const stats = agentsService.getStats(projectPath || null);
44
+
45
+ res.json({
46
+ success: true,
47
+ ...stats
48
+ });
49
+ } catch (err) {
50
+ console.error('[Agents API] Get stats error:', err);
51
+ res.status(500).json({
52
+ success: false,
53
+ message: err.message
54
+ });
55
+ }
56
+ });
57
+
58
+ /**
59
+ * 获取单个代理详情
60
+ * GET /api/agents/:scope/:fileName
61
+ */
62
+ router.get('/:scope/:fileName', (req, res) => {
63
+ try {
64
+ const { scope, fileName } = req.params;
65
+ const { projectPath } = req.query;
66
+
67
+ if (!['user', 'project'].includes(scope)) {
68
+ return res.status(400).json({
69
+ success: false,
70
+ message: '无效的 scope,必须是 user 或 project'
71
+ });
72
+ }
73
+
74
+ if (scope === 'project' && !projectPath) {
75
+ return res.status(400).json({
76
+ success: false,
77
+ message: '获取项目级代理需要提供 projectPath'
78
+ });
79
+ }
80
+
81
+ const agent = agentsService.getAgent(fileName, scope, projectPath || null);
82
+
83
+ if (!agent) {
84
+ return res.status(404).json({
85
+ success: false,
86
+ message: `代理 "${fileName}" 不存在`
87
+ });
88
+ }
89
+
90
+ res.json({
91
+ success: true,
92
+ agent
93
+ });
94
+ } catch (err) {
95
+ console.error('[Agents API] Get agent error:', err);
96
+ res.status(500).json({
97
+ success: false,
98
+ message: err.message
99
+ });
100
+ }
101
+ });
102
+
103
+ /**
104
+ * 创建代理
105
+ * POST /api/agents
106
+ * Body: { fileName, scope, projectPath?, name, description, tools?, model?, permissionMode?, skills?, systemPrompt? }
107
+ */
108
+ router.post('/', (req, res) => {
109
+ try {
110
+ const { fileName, scope, projectPath, name, description, tools, model, permissionMode, skills, systemPrompt } = req.body;
111
+
112
+ if (!fileName) {
113
+ return res.status(400).json({
114
+ success: false,
115
+ message: '代理文件名不能为空'
116
+ });
117
+ }
118
+
119
+ if (!['user', 'project'].includes(scope)) {
120
+ return res.status(400).json({
121
+ success: false,
122
+ message: '无效的 scope,必须是 user 或 project'
123
+ });
124
+ }
125
+
126
+ if (scope === 'project' && !projectPath) {
127
+ return res.status(400).json({
128
+ success: false,
129
+ message: '创建项目级代理需要提供 projectPath'
130
+ });
131
+ }
132
+
133
+ const agent = agentsService.createAgent({
134
+ fileName,
135
+ scope,
136
+ projectPath: projectPath || null,
137
+ name: name || fileName,
138
+ description: description || '',
139
+ tools: tools || '',
140
+ model: model || '',
141
+ permissionMode: permissionMode || '',
142
+ skills: skills || '',
143
+ systemPrompt: systemPrompt || ''
144
+ });
145
+
146
+ res.json({
147
+ success: true,
148
+ agent,
149
+ message: '代理创建成功'
150
+ });
151
+ } catch (err) {
152
+ console.error('[Agents API] Create agent error:', err);
153
+ res.status(500).json({
154
+ success: false,
155
+ message: err.message
156
+ });
157
+ }
158
+ });
159
+
160
+ /**
161
+ * 更新代理
162
+ * PUT /api/agents/:scope/:fileName
163
+ */
164
+ router.put('/:scope/:fileName', (req, res) => {
165
+ try {
166
+ const { scope, fileName } = req.params;
167
+ const { projectPath, name, description, tools, model, permissionMode, skills, systemPrompt } = req.body;
168
+
169
+ if (!['user', 'project'].includes(scope)) {
170
+ return res.status(400).json({
171
+ success: false,
172
+ message: '无效的 scope,必须是 user 或 project'
173
+ });
174
+ }
175
+
176
+ if (scope === 'project' && !projectPath) {
177
+ return res.status(400).json({
178
+ success: false,
179
+ message: '更新项目级代理需要提供 projectPath'
180
+ });
181
+ }
182
+
183
+ const agent = agentsService.updateAgent({
184
+ fileName,
185
+ scope,
186
+ projectPath: projectPath || null,
187
+ name: name || fileName,
188
+ description: description || '',
189
+ tools: tools || '',
190
+ model: model || '',
191
+ permissionMode: permissionMode || '',
192
+ skills: skills || '',
193
+ systemPrompt: systemPrompt || ''
194
+ });
195
+
196
+ res.json({
197
+ success: true,
198
+ agent,
199
+ message: '代理更新成功'
200
+ });
201
+ } catch (err) {
202
+ console.error('[Agents API] Update agent error:', err);
203
+ res.status(500).json({
204
+ success: false,
205
+ message: err.message
206
+ });
207
+ }
208
+ });
209
+
210
+ /**
211
+ * 删除代理
212
+ * DELETE /api/agents/:scope/:fileName
213
+ */
214
+ router.delete('/:scope/:fileName', (req, res) => {
215
+ try {
216
+ const { scope, fileName } = req.params;
217
+ const { projectPath } = req.query;
218
+
219
+ if (!['user', 'project'].includes(scope)) {
220
+ return res.status(400).json({
221
+ success: false,
222
+ message: '无效的 scope,必须是 user 或 project'
223
+ });
224
+ }
225
+
226
+ if (scope === 'project' && !projectPath) {
227
+ return res.status(400).json({
228
+ success: false,
229
+ message: '删除项目级代理需要提供 projectPath'
230
+ });
231
+ }
232
+
233
+ const result = agentsService.deleteAgent(fileName, scope, projectPath || null);
234
+
235
+ res.json({
236
+ success: result.success,
237
+ message: result.message
238
+ });
239
+ } catch (err) {
240
+ console.error('[Agents API] Delete agent error:', err);
241
+ res.status(500).json({
242
+ success: false,
243
+ message: err.message
244
+ });
245
+ }
246
+ });
247
+
248
+ module.exports = router;
@@ -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,258 @@
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
+ } = require('../services/channels');
12
+ const { getSchedulerState } = require('../services/channel-scheduler');
13
+ const { getChannelHealthStatus, getAllChannelHealthStatus, resetChannelHealth } = require('../services/channel-health');
14
+ const { testChannelSpeed, testMultipleChannels, getLatencyLevel } = require('../services/speed-test');
15
+ const { broadcastLog, broadcastProxyState, broadcastSchedulerState } = require('../websocket-server');
16
+
17
+ // GET /api/channels - Get all channels with health status
18
+ router.get('/', (req, res) => {
19
+ try {
20
+ const channels = getAllChannels();
21
+ // 为每个渠道附加健康状态
22
+ const channelsWithHealth = channels.map(ch => ({
23
+ ...ch,
24
+ health: getChannelHealthStatus(ch.id)
25
+ }));
26
+ res.json({ channels: channelsWithHealth });
27
+ } catch (error) {
28
+ console.error('Error fetching channels:', error);
29
+ res.status(500).json({ error: error.message });
30
+ }
31
+ });
32
+
33
+ router.get('/pool/status', (req, res) => {
34
+ try {
35
+ const source = req.query.source || 'claude';
36
+ const scheduler = getSchedulerState(source);
37
+ res.json({ source, scheduler });
38
+ } catch (error) {
39
+ res.status(500).json({ error: error.message });
40
+ }
41
+ });
42
+
43
+ // GET /api/channels/current - Get current settings
44
+ router.get('/current', (req, res) => {
45
+ try {
46
+ const settings = getCurrentSettings();
47
+ const channels = getAllChannels();
48
+ let currentChannel = null;
49
+
50
+ if (settings) {
51
+ currentChannel = channels.find(ch =>
52
+ ch.baseUrl === settings.baseUrl && ch.apiKey === settings.apiKey
53
+ );
54
+ }
55
+
56
+ res.json({ channel: currentChannel, settings });
57
+ } catch (error) {
58
+ console.error('Error fetching current settings:', error);
59
+ res.status(500).json({ error: error.message });
60
+ }
61
+ });
62
+
63
+ // POST /api/channels - Create new channel
64
+ router.post('/', (req, res) => {
65
+ try {
66
+ const { name, baseUrl, apiKey, websiteUrl, enabled, weight, maxConcurrency } = req.body;
67
+
68
+ if (!name || !baseUrl || !apiKey) {
69
+ return res.status(400).json({ error: 'Missing required fields' });
70
+ }
71
+
72
+ const channel = createChannel(name, baseUrl, apiKey, websiteUrl, {
73
+ enabled,
74
+ weight,
75
+ maxConcurrency
76
+ });
77
+ res.json({ channel });
78
+ broadcastSchedulerState('claude', getSchedulerState('claude'));
79
+ } catch (error) {
80
+ console.error('Error creating channel:', error);
81
+ res.status(500).json({ error: error.message });
82
+ }
83
+ });
84
+
85
+ // GET /api/channels/best-for-restore - Get best channel for restore (must be before /:id)
86
+ router.get('/best-for-restore', (req, res) => {
87
+ try {
88
+ const channel = getBestChannelForRestore();
89
+ res.json({ channel });
90
+ } catch (error) {
91
+ console.error('Error getting best channel for restore:', error);
92
+ res.status(500).json({ error: error.message });
93
+ }
94
+ });
95
+
96
+ // PUT /api/channels/:id - Update channel
97
+ router.put('/:id', (req, res) => {
98
+ try {
99
+ const { id } = req.params;
100
+ const updates = req.body;
101
+
102
+ const channel = updateChannel(id, updates);
103
+ res.json({ channel });
104
+ broadcastSchedulerState('claude', getSchedulerState('claude'));
105
+ } catch (error) {
106
+ console.error('Error updating channel:', error);
107
+ res.status(500).json({ error: error.message });
108
+ }
109
+ });
110
+
111
+ // DELETE /api/channels/:id - Delete channel
112
+ router.delete('/:id', (req, res) => {
113
+ try {
114
+ const { id } = req.params;
115
+ const result = deleteChannel(id);
116
+ res.json(result);
117
+ broadcastSchedulerState('claude', getSchedulerState('claude'));
118
+ } catch (error) {
119
+ console.error('Error deleting channel:', error);
120
+ res.status(500).json({ error: error.message });
121
+ }
122
+ });
123
+
124
+ router.post('/:id/apply-to-settings', async (req, res) => {
125
+ try {
126
+ const { id } = req.params;
127
+ const channel = applyChannelToSettings(id);
128
+
129
+ // Check if proxy is running
130
+ const { getProxyStatus } = require('../proxy-server');
131
+ const proxyStatus = getProxyStatus();
132
+
133
+ broadcastLog({
134
+ type: 'action',
135
+ action: 'apply_settings',
136
+ message: `已将 (${channel.name}) 渠道写入配置文件中`,
137
+ channelName: channel.name,
138
+ timestamp: Date.now(),
139
+ source: 'claude'
140
+ });
141
+
142
+ // Stop proxy if running
143
+ if (proxyStatus && proxyStatus.running) {
144
+ console.log(`Proxy is running, stopping to apply channel settings: ${channel.name}`);
145
+
146
+ // Stop proxy and restore backup
147
+ const { stopProxyServer } = require('../proxy-server');
148
+ await stopProxyServer({ clearStartTime: false });
149
+
150
+ console.log(`✅ 已停���动态切换,默认使用当前渠道`);
151
+ broadcastLog({
152
+ type: 'action',
153
+ action: 'stop_proxy',
154
+ message: `已停止动态切换,默认使用当前渠道`,
155
+ timestamp: Date.now(),
156
+ source: 'claude'
157
+ });
158
+
159
+ // 广播代理状态更新,通知前端代理已停止
160
+ const { broadcastProxyState } = require('../websocket-server');
161
+ broadcastProxyState('claude', {
162
+ running: false,
163
+ port: null,
164
+ runtime: null,
165
+ startTime: null
166
+ }, null, getAllChannels());
167
+ }
168
+
169
+ res.json({
170
+ message: `已将 (${channel.name}) 渠道写入配置文件中`,
171
+ channel
172
+ });
173
+ } catch (error) {
174
+ console.error('Error applying channel to settings:', error);
175
+ res.status(500).json({ error: error.message });
176
+ }
177
+ });
178
+
179
+ // POST /api/channels/:id/reset-health - Reset channel health status
180
+ router.post('/:id/reset-health', (req, res) => {
181
+ try {
182
+ const { id } = req.params;
183
+ resetChannelHealth(id, 'claude');
184
+ res.json({
185
+ success: true,
186
+ message: '渠道健康状态已重置',
187
+ health: getChannelHealthStatus(id)
188
+ });
189
+ } catch (error) {
190
+ console.error('Error resetting channel health:', error);
191
+ res.status(500).json({ error: error.message });
192
+ }
193
+ });
194
+
195
+ // POST /api/channels/:id/speed-test - Test single channel speed
196
+ router.post('/:id/speed-test', async (req, res) => {
197
+ try {
198
+ const { id } = req.params;
199
+ const { timeout = 10000 } = req.body;
200
+ const channels = getAllChannels();
201
+ const channel = channels.find(ch => ch.id === id);
202
+
203
+ if (!channel) {
204
+ return res.status(404).json({ error: '渠道不存在' });
205
+ }
206
+
207
+ // Claude 渠道使用 'claude' 类型
208
+ const result = await testChannelSpeed(channel, timeout, 'claude');
209
+ result.level = getLatencyLevel(result.latency);
210
+
211
+ res.json(result);
212
+ } catch (error) {
213
+ console.error('Error testing channel speed:', error);
214
+ res.status(500).json({ error: error.message });
215
+ }
216
+ });
217
+
218
+ // POST /api/channels/speed-test-all - Test all channels speed
219
+ router.post('/speed-test-all', async (req, res) => {
220
+ try {
221
+ const { timeout = 10000 } = req.body;
222
+ const channels = getAllChannels();
223
+
224
+ if (channels.length === 0) {
225
+ return res.json({ results: [], message: '没有可测试的渠道' });
226
+ }
227
+
228
+ // Claude 渠道使用 'claude' 类型
229
+ const results = await testMultipleChannels(channels, timeout, 'claude');
230
+ // 添加延迟等级
231
+ results.forEach(r => {
232
+ r.level = getLatencyLevel(r.latency);
233
+ });
234
+
235
+ res.json({
236
+ results,
237
+ summary: {
238
+ total: results.length,
239
+ success: results.filter(r => r.success).length,
240
+ failed: results.filter(r => !r.success).length,
241
+ avgLatency: calculateAvgLatency(results)
242
+ }
243
+ });
244
+ } catch (error) {
245
+ console.error('Error testing all channels speed:', error);
246
+ res.status(500).json({ error: error.message });
247
+ }
248
+ });
249
+
250
+ // 计算平均延迟
251
+ function calculateAvgLatency(results) {
252
+ const successResults = results.filter(r => r.success && r.latency);
253
+ if (successResults.length === 0) return null;
254
+ const sum = successResults.reduce((acc, r) => acc + r.latency, 0);
255
+ return Math.round(sum / successResults.length);
256
+ }
257
+
258
+ module.exports = router;