@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.
- package/CHANGELOG.md +333 -0
- package/LICENSE +21 -0
- package/README.md +404 -0
- package/bin/ctx.js +8 -0
- package/dist/web/assets/index-D1AYlFLZ.js +3220 -0
- package/dist/web/assets/index-aL3cKxSK.css +41 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +14 -0
- package/dist/web/logo.png +0 -0
- package/docs/CHANGELOG.md +582 -0
- package/docs/DIRECTORY_MIGRATION.md +112 -0
- package/docs/PROJECT_STRUCTURE.md +396 -0
- package/docs/bannel.png +0 -0
- package/docs/home.png +0 -0
- package/docs/logo.png +0 -0
- package/docs/multi-channel-load-balancing.md +249 -0
- package/package.json +73 -0
- package/src/commands/channels.js +504 -0
- package/src/commands/cli-type.js +99 -0
- package/src/commands/daemon.js +286 -0
- package/src/commands/doctor.js +332 -0
- package/src/commands/list.js +222 -0
- package/src/commands/logs.js +259 -0
- package/src/commands/port-config.js +115 -0
- package/src/commands/proxy-control.js +258 -0
- package/src/commands/proxy.js +152 -0
- package/src/commands/resume.js +137 -0
- package/src/commands/search.js +190 -0
- package/src/commands/stats.js +224 -0
- package/src/commands/switch.js +48 -0
- package/src/commands/toggle-proxy.js +222 -0
- package/src/commands/ui.js +92 -0
- package/src/commands/workspace.js +454 -0
- package/src/config/default.js +40 -0
- package/src/config/loader.js +75 -0
- package/src/config/paths.js +121 -0
- package/src/index.js +373 -0
- package/src/reset-config.js +92 -0
- package/src/server/api/agents.js +248 -0
- package/src/server/api/aliases.js +36 -0
- package/src/server/api/channels.js +258 -0
- package/src/server/api/claude-hooks.js +480 -0
- package/src/server/api/codex-channels.js +312 -0
- package/src/server/api/codex-projects.js +91 -0
- package/src/server/api/codex-proxy.js +182 -0
- package/src/server/api/codex-sessions.js +491 -0
- package/src/server/api/codex-statistics.js +57 -0
- package/src/server/api/commands.js +245 -0
- package/src/server/api/config-templates.js +182 -0
- package/src/server/api/config.js +147 -0
- package/src/server/api/convert.js +127 -0
- package/src/server/api/dashboard.js +125 -0
- package/src/server/api/env.js +144 -0
- package/src/server/api/favorites.js +77 -0
- package/src/server/api/gemini-channels.js +261 -0
- package/src/server/api/gemini-projects.js +91 -0
- package/src/server/api/gemini-proxy.js +160 -0
- package/src/server/api/gemini-sessions.js +397 -0
- package/src/server/api/gemini-statistics.js +57 -0
- package/src/server/api/health-check.js +118 -0
- package/src/server/api/mcp.js +336 -0
- package/src/server/api/pm2-autostart.js +269 -0
- package/src/server/api/projects.js +124 -0
- package/src/server/api/prompts.js +279 -0
- package/src/server/api/proxy.js +235 -0
- package/src/server/api/rules.js +271 -0
- package/src/server/api/sessions.js +595 -0
- package/src/server/api/settings.js +61 -0
- package/src/server/api/skills.js +305 -0
- package/src/server/api/statistics.js +91 -0
- package/src/server/api/terminal.js +202 -0
- package/src/server/api/ui-config.js +64 -0
- package/src/server/api/workspaces.js +407 -0
- package/src/server/codex-proxy-server.js +538 -0
- package/src/server/dev-server.js +26 -0
- package/src/server/gemini-proxy-server.js +518 -0
- package/src/server/index.js +305 -0
- package/src/server/proxy-server.js +469 -0
- package/src/server/services/agents-service.js +354 -0
- package/src/server/services/alias.js +71 -0
- package/src/server/services/channel-health.js +234 -0
- package/src/server/services/channel-scheduler.js +234 -0
- package/src/server/services/channels.js +347 -0
- package/src/server/services/codex-channels.js +625 -0
- package/src/server/services/codex-config.js +90 -0
- package/src/server/services/codex-parser.js +322 -0
- package/src/server/services/codex-sessions.js +665 -0
- package/src/server/services/codex-settings-manager.js +397 -0
- package/src/server/services/codex-speed-test-template.json +24 -0
- package/src/server/services/codex-statistics-service.js +255 -0
- package/src/server/services/commands-service.js +360 -0
- package/src/server/services/config-templates-service.js +732 -0
- package/src/server/services/env-checker.js +307 -0
- package/src/server/services/env-manager.js +300 -0
- package/src/server/services/favorites.js +163 -0
- package/src/server/services/gemini-channels.js +333 -0
- package/src/server/services/gemini-config.js +73 -0
- package/src/server/services/gemini-sessions.js +689 -0
- package/src/server/services/gemini-settings-manager.js +263 -0
- package/src/server/services/gemini-statistics-service.js +253 -0
- package/src/server/services/health-check.js +399 -0
- package/src/server/services/mcp-service.js +1188 -0
- package/src/server/services/prompts-service.js +492 -0
- package/src/server/services/proxy-runtime.js +79 -0
- package/src/server/services/pty-manager.js +435 -0
- package/src/server/services/rules-service.js +401 -0
- package/src/server/services/session-cache.js +127 -0
- package/src/server/services/session-converter.js +577 -0
- package/src/server/services/sessions.js +757 -0
- package/src/server/services/settings-manager.js +163 -0
- package/src/server/services/skill-service.js +965 -0
- package/src/server/services/speed-test.js +545 -0
- package/src/server/services/statistics-service.js +386 -0
- package/src/server/services/terminal-commands.js +155 -0
- package/src/server/services/terminal-config.js +140 -0
- package/src/server/services/terminal-detector.js +306 -0
- package/src/server/services/ui-config.js +130 -0
- package/src/server/services/workspace-service.js +662 -0
- package/src/server/utils/pricing.js +41 -0
- package/src/server/websocket-server.js +557 -0
- package/src/ui/menu.js +129 -0
- package/src/ui/prompts.js +100 -0
- package/src/utils/format.js +43 -0
- package/src/utils/port-helper.js +94 -0
- package/src/utils/session.js +239 -0
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 速度测试服务
|
|
3
|
+
* 用于测试渠道 API 的响应延迟
|
|
4
|
+
* 参考 cc-switch 的实现方式
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const https = require('https');
|
|
8
|
+
const http = require('http');
|
|
9
|
+
const { URL } = require('url');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
|
|
13
|
+
// 测试结果缓存
|
|
14
|
+
const testResultsCache = new Map();
|
|
15
|
+
|
|
16
|
+
// Codex 请求体模板文件路径
|
|
17
|
+
const CODEX_REQUEST_TEMPLATE_PATH = path.join(__dirname, 'codex-speed-test-template.json');
|
|
18
|
+
|
|
19
|
+
// 超时配置(毫秒)
|
|
20
|
+
const DEFAULT_TIMEOUT = 15000;
|
|
21
|
+
const MIN_TIMEOUT = 5000;
|
|
22
|
+
const MAX_TIMEOUT = 60000;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 规范化超时时间
|
|
26
|
+
*/
|
|
27
|
+
function sanitizeTimeout(timeout) {
|
|
28
|
+
const ms = timeout || DEFAULT_TIMEOUT;
|
|
29
|
+
return Math.min(Math.max(ms, MIN_TIMEOUT), MAX_TIMEOUT);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 测试单个渠道的连接速度和 API 功能
|
|
34
|
+
* @param {Object} channel - 渠道配置
|
|
35
|
+
* @param {number} timeout - 超时时间(毫秒)
|
|
36
|
+
* @param {string} channelType - 渠道类型:'claude' | 'codex' | 'gemini'
|
|
37
|
+
* @returns {Promise<Object>} 测试结果
|
|
38
|
+
*/
|
|
39
|
+
async function testChannelSpeed(channel, timeout = DEFAULT_TIMEOUT, channelType = 'claude') {
|
|
40
|
+
const sanitizedTimeout = sanitizeTimeout(timeout);
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
if (!channel.baseUrl) {
|
|
44
|
+
return {
|
|
45
|
+
channelId: channel.id,
|
|
46
|
+
channelName: channel.name,
|
|
47
|
+
success: false,
|
|
48
|
+
networkOk: false,
|
|
49
|
+
apiOk: false,
|
|
50
|
+
error: 'URL 不能为空',
|
|
51
|
+
latency: null,
|
|
52
|
+
statusCode: null,
|
|
53
|
+
testedAt: Date.now()
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 规范化 URL(去除末尾斜杠)
|
|
58
|
+
let testUrl;
|
|
59
|
+
try {
|
|
60
|
+
const url = new URL(channel.baseUrl.trim());
|
|
61
|
+
testUrl = url.toString().replace(/\/+$/, '');
|
|
62
|
+
} catch (urlError) {
|
|
63
|
+
return {
|
|
64
|
+
channelId: channel.id,
|
|
65
|
+
channelName: channel.name,
|
|
66
|
+
success: false,
|
|
67
|
+
networkOk: false,
|
|
68
|
+
apiOk: false,
|
|
69
|
+
error: `URL 无效: ${urlError.message}`,
|
|
70
|
+
latency: null,
|
|
71
|
+
statusCode: null,
|
|
72
|
+
testedAt: Date.now()
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 直接测试 API 功能(发送测试消息)
|
|
77
|
+
// 不再单独测试网络连通性,因为直接 GET base_url 可能返回 404
|
|
78
|
+
const apiResult = await testAPIFunctionality(testUrl, channel.apiKey, sanitizedTimeout, channelType, channel.model);
|
|
79
|
+
|
|
80
|
+
const success = apiResult.success;
|
|
81
|
+
const networkOk = apiResult.latency !== null; // 如果有延迟数据,说明网络是通的
|
|
82
|
+
|
|
83
|
+
// 缓存结果
|
|
84
|
+
const finalResult = {
|
|
85
|
+
channelId: channel.id,
|
|
86
|
+
channelName: channel.name,
|
|
87
|
+
success,
|
|
88
|
+
networkOk,
|
|
89
|
+
apiOk: success,
|
|
90
|
+
statusCode: apiResult.statusCode || null,
|
|
91
|
+
error: success ? null : (apiResult.error || '测试失败'),
|
|
92
|
+
latency: apiResult.latency || null, // 无论成功失败都保留延迟数据
|
|
93
|
+
testedAt: Date.now()
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
testResultsCache.set(channel.id, finalResult);
|
|
97
|
+
|
|
98
|
+
return finalResult;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return {
|
|
101
|
+
channelId: channel.id,
|
|
102
|
+
channelName: channel.name,
|
|
103
|
+
success: false,
|
|
104
|
+
networkOk: false,
|
|
105
|
+
apiOk: false,
|
|
106
|
+
error: error.message || '连接失败',
|
|
107
|
+
latency: null,
|
|
108
|
+
statusCode: null,
|
|
109
|
+
testedAt: Date.now()
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 测试网络连通性(简单 GET 请求)
|
|
116
|
+
*/
|
|
117
|
+
function testNetworkConnectivity(url, apiKey, timeout) {
|
|
118
|
+
return new Promise((resolve) => {
|
|
119
|
+
const startTime = Date.now();
|
|
120
|
+
const parsedUrl = new URL(url);
|
|
121
|
+
const isHttps = parsedUrl.protocol === 'https:';
|
|
122
|
+
const httpModule = isHttps ? https : http;
|
|
123
|
+
|
|
124
|
+
const options = {
|
|
125
|
+
hostname: parsedUrl.hostname,
|
|
126
|
+
port: parsedUrl.port || (isHttps ? 443 : 80),
|
|
127
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
128
|
+
method: 'GET',
|
|
129
|
+
timeout,
|
|
130
|
+
headers: {
|
|
131
|
+
'Authorization': `Bearer ${apiKey || ''}`,
|
|
132
|
+
'Content-Type': 'application/json',
|
|
133
|
+
'User-Agent': 'Coding-Tool-SpeedTest/1.0'
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const req = httpModule.request(options, (res) => {
|
|
138
|
+
let data = '';
|
|
139
|
+
res.on('data', chunk => { data += chunk; });
|
|
140
|
+
res.on('end', () => {
|
|
141
|
+
const latency = Date.now() - startTime;
|
|
142
|
+
resolve({
|
|
143
|
+
statusCode: res.statusCode,
|
|
144
|
+
latency,
|
|
145
|
+
error: null
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
req.on('error', (error) => {
|
|
151
|
+
let errorMsg;
|
|
152
|
+
if (error.code === 'ECONNREFUSED') {
|
|
153
|
+
errorMsg = '连接被拒绝';
|
|
154
|
+
} else if (error.code === 'ETIMEDOUT') {
|
|
155
|
+
errorMsg = '连接超时';
|
|
156
|
+
} else if (error.code === 'ENOTFOUND') {
|
|
157
|
+
errorMsg = 'DNS 解析失败';
|
|
158
|
+
} else if (error.code === 'ECONNRESET') {
|
|
159
|
+
errorMsg = '连接被重置';
|
|
160
|
+
} else {
|
|
161
|
+
errorMsg = error.message || '连接失败';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
resolve({
|
|
165
|
+
statusCode: null,
|
|
166
|
+
latency: null,
|
|
167
|
+
error: errorMsg
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
req.on('timeout', () => {
|
|
172
|
+
req.destroy();
|
|
173
|
+
resolve({
|
|
174
|
+
statusCode: null,
|
|
175
|
+
latency: null,
|
|
176
|
+
error: '请求超时'
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
req.end();
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 测试 API 功能(发送真实的聊天请求)
|
|
186
|
+
* 根据渠道类型选择正确的 API 格式
|
|
187
|
+
* @param {string} baseUrl - 基础 URL
|
|
188
|
+
* @param {string} apiKey - API Key
|
|
189
|
+
* @param {number} timeout - 超时时间
|
|
190
|
+
* @param {string} channelType - 渠道类型:'claude' | 'codex' | 'gemini'
|
|
191
|
+
* @param {string} model - 模型名称(可选,用于 Gemini)
|
|
192
|
+
*/
|
|
193
|
+
function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'claude', model = null) {
|
|
194
|
+
return new Promise((resolve) => {
|
|
195
|
+
const startTime = Date.now();
|
|
196
|
+
const parsedUrl = new URL(baseUrl);
|
|
197
|
+
const isHttps = parsedUrl.protocol === 'https:';
|
|
198
|
+
const httpModule = isHttps ? https : http;
|
|
199
|
+
|
|
200
|
+
// 根据渠道类型确定 API 路径和请求格式
|
|
201
|
+
let apiPath;
|
|
202
|
+
let requestBody;
|
|
203
|
+
let headers;
|
|
204
|
+
|
|
205
|
+
// Claude 渠道使用 Anthropic 格式
|
|
206
|
+
if (channelType === 'claude') {
|
|
207
|
+
// Anthropic Messages API - 模拟 Claude Code 请求格式
|
|
208
|
+
apiPath = parsedUrl.pathname.replace(/\/$/, '');
|
|
209
|
+
if (!apiPath.endsWith('/messages')) {
|
|
210
|
+
apiPath = apiPath + (apiPath.endsWith('/v1') ? '/messages' : '/v1/messages');
|
|
211
|
+
}
|
|
212
|
+
// 添加 ?beta=true 查询参数
|
|
213
|
+
apiPath += '?beta=true';
|
|
214
|
+
|
|
215
|
+
// 使用 Claude Code 的请求格式
|
|
216
|
+
// user_id 必须符合特定格式: user_xxx_account__session_xxx
|
|
217
|
+
// 使用 claude-sonnet-4 模型测试,因为 haiku 可能没有配额
|
|
218
|
+
const sessionId = Math.random().toString(36).substring(2, 15);
|
|
219
|
+
requestBody = JSON.stringify({
|
|
220
|
+
model: 'claude-sonnet-4-20250514',
|
|
221
|
+
max_tokens: 10,
|
|
222
|
+
stream: true,
|
|
223
|
+
messages: [{ role: 'user', content: [{ type: 'text', text: 'Hi' }] }],
|
|
224
|
+
system: [{ type: 'text', text: "You are Claude Code, Anthropic's official CLI for Claude." }],
|
|
225
|
+
metadata: { user_id: `user_0000000000000000000000000000000000000000000000000000000000000000_account__session_${sessionId}` }
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
headers = {
|
|
229
|
+
'x-api-key': apiKey || '',
|
|
230
|
+
'Authorization': `Bearer ${apiKey || ''}`,
|
|
231
|
+
'anthropic-version': '2023-06-01',
|
|
232
|
+
'anthropic-beta': 'claude-code-20250219,interleaved-thinking-2025-05-14',
|
|
233
|
+
'anthropic-dangerous-direct-browser-access': 'true',
|
|
234
|
+
'x-app': 'cli',
|
|
235
|
+
'x-stainless-lang': 'js',
|
|
236
|
+
'x-stainless-runtime': 'node',
|
|
237
|
+
'Content-Type': 'application/json',
|
|
238
|
+
'User-Agent': 'claude-cli/2.0.53 (external, cli)'
|
|
239
|
+
};
|
|
240
|
+
} else if (channelType === 'codex') {
|
|
241
|
+
// Codex 使用 OpenAI Responses API 格式
|
|
242
|
+
// 路径: /v1/responses
|
|
243
|
+
apiPath = parsedUrl.pathname.replace(/\/$/, '');
|
|
244
|
+
if (!apiPath.endsWith('/responses')) {
|
|
245
|
+
apiPath = apiPath + (apiPath.endsWith('/v1') ? '/responses' : '/v1/responses');
|
|
246
|
+
}
|
|
247
|
+
// 从模板文件加载完整的 Codex 请求格式
|
|
248
|
+
try {
|
|
249
|
+
const template = JSON.parse(fs.readFileSync(CODEX_REQUEST_TEMPLATE_PATH, 'utf-8'));
|
|
250
|
+
// 生成新的 prompt_cache_key
|
|
251
|
+
template.prompt_cache_key = `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
252
|
+
requestBody = JSON.stringify(template);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
console.error('[SpeedTest] Failed to load Codex template:', err.message);
|
|
255
|
+
// 降级使用简化版本(可能会失败)
|
|
256
|
+
requestBody = JSON.stringify({
|
|
257
|
+
model: 'gpt-5-codex',
|
|
258
|
+
instructions: 'You are Codex.',
|
|
259
|
+
input: [{ type: 'message', role: 'user', content: [{ type: 'input_text', text: 'ping' }] }],
|
|
260
|
+
max_output_tokens: 10,
|
|
261
|
+
stream: false,
|
|
262
|
+
store: false
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
headers = {
|
|
266
|
+
'Authorization': `Bearer ${apiKey || ''}`,
|
|
267
|
+
'Content-Type': 'application/json',
|
|
268
|
+
'User-Agent': 'codex_cli_rs/0.65.0',
|
|
269
|
+
'openai-beta': 'responses=experimental'
|
|
270
|
+
};
|
|
271
|
+
} else if (channelType === 'gemini') {
|
|
272
|
+
// Gemini 也使用 OpenAI 兼容格式
|
|
273
|
+
apiPath = parsedUrl.pathname.replace(/\/$/, '');
|
|
274
|
+
if (!apiPath.endsWith('/chat/completions')) {
|
|
275
|
+
apiPath = apiPath + (apiPath.endsWith('/v1') ? '/chat/completions' : '/v1/chat/completions');
|
|
276
|
+
}
|
|
277
|
+
// 使用渠道配置的模型,如果没有则默认使用 gemini-2.5-pro
|
|
278
|
+
const geminiModel = model || 'gemini-2.5-pro';
|
|
279
|
+
requestBody = JSON.stringify({
|
|
280
|
+
model: geminiModel,
|
|
281
|
+
max_tokens: 10,
|
|
282
|
+
messages: [{ role: 'user', content: 'Hi' }]
|
|
283
|
+
});
|
|
284
|
+
headers = {
|
|
285
|
+
'Authorization': `Bearer ${apiKey || ''}`,
|
|
286
|
+
'Content-Type': 'application/json',
|
|
287
|
+
'User-Agent': 'Coding-Tool-SpeedTest/1.0'
|
|
288
|
+
};
|
|
289
|
+
} else {
|
|
290
|
+
// 默认使用 OpenAI 格式
|
|
291
|
+
apiPath = parsedUrl.pathname.replace(/\/$/, '');
|
|
292
|
+
if (!apiPath.endsWith('/chat/completions')) {
|
|
293
|
+
apiPath = apiPath + (apiPath.endsWith('/v1') ? '/chat/completions' : '/v1/chat/completions');
|
|
294
|
+
}
|
|
295
|
+
requestBody = JSON.stringify({
|
|
296
|
+
model: 'gpt-4o-mini',
|
|
297
|
+
max_tokens: 10,
|
|
298
|
+
messages: [{ role: 'user', content: 'Hi' }]
|
|
299
|
+
});
|
|
300
|
+
headers = {
|
|
301
|
+
'Authorization': `Bearer ${apiKey || ''}`,
|
|
302
|
+
'Content-Type': 'application/json',
|
|
303
|
+
'User-Agent': 'Coding-Tool-SpeedTest/1.0'
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const options = {
|
|
308
|
+
hostname: parsedUrl.hostname,
|
|
309
|
+
port: parsedUrl.port || (isHttps ? 443 : 80),
|
|
310
|
+
path: apiPath,
|
|
311
|
+
method: 'POST',
|
|
312
|
+
timeout,
|
|
313
|
+
headers
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const req = httpModule.request(options, (res) => {
|
|
317
|
+
let data = '';
|
|
318
|
+
let resolved = false;
|
|
319
|
+
const isStreamingResponse = channelType === 'codex'; // Codex 使用流式响应
|
|
320
|
+
|
|
321
|
+
// 解析响应体中的错误信息
|
|
322
|
+
const parseErrorMessage = (responseData) => {
|
|
323
|
+
try {
|
|
324
|
+
const errData = JSON.parse(responseData);
|
|
325
|
+
return errData.error?.message || errData.message || errData.detail || null;
|
|
326
|
+
} catch {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
res.on('data', chunk => {
|
|
332
|
+
data += chunk;
|
|
333
|
+
const chunkStr = chunk.toString();
|
|
334
|
+
|
|
335
|
+
// 对于流式响应(Codex),在收到第一个有效事件时立即返回成功
|
|
336
|
+
if (isStreamingResponse && !resolved && res.statusCode >= 200 && res.statusCode < 300) {
|
|
337
|
+
// 检查是否收到了 response.created 或 response.in_progress 事件
|
|
338
|
+
if (chunkStr.includes('response.created') || chunkStr.includes('response.in_progress')) {
|
|
339
|
+
resolved = true;
|
|
340
|
+
const latency = Date.now() - startTime;
|
|
341
|
+
req.destroy();
|
|
342
|
+
resolve({
|
|
343
|
+
success: true,
|
|
344
|
+
latency,
|
|
345
|
+
error: null,
|
|
346
|
+
statusCode: res.statusCode
|
|
347
|
+
});
|
|
348
|
+
} else if (chunkStr.includes('"detail"') || chunkStr.includes('"error"')) {
|
|
349
|
+
// 流式响应中的错误
|
|
350
|
+
resolved = true;
|
|
351
|
+
const latency = Date.now() - startTime;
|
|
352
|
+
req.destroy();
|
|
353
|
+
const errMsg = parseErrorMessage(chunkStr) || '流式响应错误';
|
|
354
|
+
resolve({
|
|
355
|
+
success: false,
|
|
356
|
+
latency,
|
|
357
|
+
error: errMsg,
|
|
358
|
+
statusCode: res.statusCode
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
res.on('end', () => {
|
|
365
|
+
if (resolved) return; // 已经处理过了
|
|
366
|
+
|
|
367
|
+
const latency = Date.now() - startTime;
|
|
368
|
+
|
|
369
|
+
// 严格判断:只有 2xx 且没有错误信息才算成功
|
|
370
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
371
|
+
// 检查响应体是否包含错误信息
|
|
372
|
+
const errMsg = parseErrorMessage(data);
|
|
373
|
+
if (errMsg && (errMsg.includes('error') || errMsg.includes('Error') ||
|
|
374
|
+
errMsg.includes('失败') || errMsg.includes('错误'))) {
|
|
375
|
+
resolve({
|
|
376
|
+
success: false,
|
|
377
|
+
latency,
|
|
378
|
+
error: errMsg,
|
|
379
|
+
statusCode: res.statusCode
|
|
380
|
+
});
|
|
381
|
+
} else {
|
|
382
|
+
// 真正的成功响应
|
|
383
|
+
resolve({
|
|
384
|
+
success: true,
|
|
385
|
+
latency,
|
|
386
|
+
error: null,
|
|
387
|
+
statusCode: res.statusCode
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
} else if (res.statusCode === 401) {
|
|
391
|
+
resolve({
|
|
392
|
+
success: false,
|
|
393
|
+
latency,
|
|
394
|
+
error: 'API Key 无效或已过期',
|
|
395
|
+
statusCode: res.statusCode
|
|
396
|
+
});
|
|
397
|
+
} else if (res.statusCode === 403) {
|
|
398
|
+
resolve({
|
|
399
|
+
success: false,
|
|
400
|
+
latency,
|
|
401
|
+
error: 'API Key 权限不足',
|
|
402
|
+
statusCode: res.statusCode
|
|
403
|
+
});
|
|
404
|
+
} else if (res.statusCode === 429) {
|
|
405
|
+
// 请求过多 - 标记为失败
|
|
406
|
+
const errMsg = parseErrorMessage(data) || '请求过多,服务限流中';
|
|
407
|
+
resolve({
|
|
408
|
+
success: false,
|
|
409
|
+
latency,
|
|
410
|
+
error: errMsg,
|
|
411
|
+
statusCode: res.statusCode
|
|
412
|
+
});
|
|
413
|
+
} else if (res.statusCode === 503 || res.statusCode === 529) {
|
|
414
|
+
// 服务暂时不可用/过载 - 标记为失败
|
|
415
|
+
const errMsg = parseErrorMessage(data) || (res.statusCode === 503 ? '服务暂时不可用' : '服务过载');
|
|
416
|
+
resolve({
|
|
417
|
+
success: false,
|
|
418
|
+
latency,
|
|
419
|
+
error: errMsg,
|
|
420
|
+
statusCode: res.statusCode
|
|
421
|
+
});
|
|
422
|
+
} else if (res.statusCode === 402) {
|
|
423
|
+
resolve({
|
|
424
|
+
success: false,
|
|
425
|
+
latency,
|
|
426
|
+
error: '账户余额不足',
|
|
427
|
+
statusCode: res.statusCode
|
|
428
|
+
});
|
|
429
|
+
} else if (res.statusCode === 400) {
|
|
430
|
+
// 请求参数错误
|
|
431
|
+
const errMsg = parseErrorMessage(data) || '请求参数错误';
|
|
432
|
+
resolve({
|
|
433
|
+
success: false,
|
|
434
|
+
latency,
|
|
435
|
+
error: errMsg,
|
|
436
|
+
statusCode: res.statusCode
|
|
437
|
+
});
|
|
438
|
+
} else if (res.statusCode >= 500) {
|
|
439
|
+
// 5xx 服务器错误
|
|
440
|
+
const errMsg = parseErrorMessage(data) || `服务器错误 (${res.statusCode})`;
|
|
441
|
+
resolve({
|
|
442
|
+
success: false,
|
|
443
|
+
latency,
|
|
444
|
+
error: errMsg,
|
|
445
|
+
statusCode: res.statusCode
|
|
446
|
+
});
|
|
447
|
+
} else {
|
|
448
|
+
// 其他错误
|
|
449
|
+
const errMsg = parseErrorMessage(data) || `HTTP ${res.statusCode}`;
|
|
450
|
+
resolve({
|
|
451
|
+
success: false,
|
|
452
|
+
latency,
|
|
453
|
+
error: errMsg,
|
|
454
|
+
statusCode: res.statusCode
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
req.on('error', (error) => {
|
|
461
|
+
resolve({
|
|
462
|
+
success: false,
|
|
463
|
+
latency: null,
|
|
464
|
+
error: error.message || '请求失败'
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
req.on('timeout', () => {
|
|
469
|
+
req.destroy();
|
|
470
|
+
resolve({
|
|
471
|
+
success: false,
|
|
472
|
+
latency: null,
|
|
473
|
+
error: 'API 请求超时'
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
req.write(requestBody);
|
|
478
|
+
req.end();
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* 批量测试多个渠道
|
|
484
|
+
* @param {Array} channels - 渠道列表
|
|
485
|
+
* @param {number} timeout - 超时时间
|
|
486
|
+
* @param {string} channelType - 渠道类型:'claude' | 'codex' | 'gemini'
|
|
487
|
+
* @returns {Promise<Array>} 测试结果列表
|
|
488
|
+
*/
|
|
489
|
+
async function testMultipleChannels(channels, timeout = DEFAULT_TIMEOUT, channelType = 'claude') {
|
|
490
|
+
const results = await Promise.all(
|
|
491
|
+
channels.map(channel => testChannelSpeed(channel, timeout, channelType))
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
// 按延迟排序(成功的在前,按延迟升序)
|
|
495
|
+
results.sort((a, b) => {
|
|
496
|
+
if (a.success && !b.success) return -1;
|
|
497
|
+
if (!a.success && b.success) return 1;
|
|
498
|
+
if (a.success && b.success) return (a.latency || Infinity) - (b.latency || Infinity);
|
|
499
|
+
return 0;
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
return results;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* 获取缓存的测试结果
|
|
507
|
+
* @param {string} channelId - 渠道 ID
|
|
508
|
+
* @returns {Object|null} 缓存的测试结果
|
|
509
|
+
*/
|
|
510
|
+
function getCachedResult(channelId) {
|
|
511
|
+
const cached = testResultsCache.get(channelId);
|
|
512
|
+
// 5 分钟内的缓存有效
|
|
513
|
+
if (cached && Date.now() - cached.testedAt < 5 * 60 * 1000) {
|
|
514
|
+
return cached;
|
|
515
|
+
}
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* 清除测试结果缓存
|
|
521
|
+
*/
|
|
522
|
+
function clearCache() {
|
|
523
|
+
testResultsCache.clear();
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* 获取延迟等级
|
|
528
|
+
* @param {number} latency - 延迟毫秒数
|
|
529
|
+
* @returns {string} 等级:excellent/good/fair/poor
|
|
530
|
+
*/
|
|
531
|
+
function getLatencyLevel(latency) {
|
|
532
|
+
if (!latency) return 'unknown';
|
|
533
|
+
if (latency < 300) return 'excellent'; // < 300ms 优秀
|
|
534
|
+
if (latency < 500) return 'good'; // < 500ms 良好
|
|
535
|
+
if (latency < 800) return 'fair'; // < 800ms 一般
|
|
536
|
+
return 'poor'; // >= 800ms 较差
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
module.exports = {
|
|
540
|
+
testChannelSpeed,
|
|
541
|
+
testMultipleChannels,
|
|
542
|
+
getCachedResult,
|
|
543
|
+
clearCache,
|
|
544
|
+
getLatencyLevel
|
|
545
|
+
};
|