@adversity/coding-tool-x 2.5.1 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/web/assets/icons-BlzwYoRU.js +1 -0
- package/dist/web/assets/{index-DZjidyED.js → index-AtwYwBZD.js} +2 -2
- package/dist/web/assets/index-BNHWEpD4.css +41 -0
- package/dist/web/assets/{naive-ui-sh0u_0bf.js → naive-ui-BcSq2wzw.js} +1 -1
- package/dist/web/assets/{vendors-CzcvkTIS.js → vendors-D2HHw_aW.js} +1 -1
- package/dist/web/assets/{vue-vendor-CEeI-Azr.js → vue-vendor-6JaYHOiI.js} +1 -1
- package/dist/web/index.html +6 -6
- package/package.json +3 -1
- package/src/commands/plugin.js +585 -0
- package/src/config/default.js +22 -3
- package/src/config/loader.js +6 -1
- package/src/index.js +229 -1
- package/src/server/api/dashboard.js +4 -3
- package/src/server/api/mcp.js +63 -0
- package/src/server/api/plugins.js +416 -0
- package/src/server/api/sessions.js +4 -4
- package/src/server/index.js +1 -0
- package/src/server/proxy-server.js +6 -3
- package/src/server/services/mcp-client.js +775 -0
- package/src/server/services/mcp-service.js +203 -0
- package/src/server/services/model-detector.js +350 -0
- package/src/server/services/plugins-service.js +677 -0
- package/src/server/services/pty-manager.js +65 -2
- package/src/server/services/sessions.js +72 -16
- package/src/server/services/speed-test.js +68 -37
- package/src/server/utils/pricing.js +32 -1
- package/src/ui/menu.js +1 -0
- package/dist/web/assets/icons-BALJo7bE.js +0 -1
- package/dist/web/assets/index-CvHZsWbE.css +0 -41
|
@@ -48,6 +48,56 @@ class PtyManager {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
resolveWorkingDirectory(cwd) {
|
|
52
|
+
const fallback = os.homedir();
|
|
53
|
+
if (typeof cwd !== 'string') {
|
|
54
|
+
return fallback;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const trimmed = cwd.trim();
|
|
58
|
+
if (!trimmed) {
|
|
59
|
+
return fallback;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let normalized = trimmed;
|
|
63
|
+
|
|
64
|
+
// 展开 ~
|
|
65
|
+
if (normalized === '~') {
|
|
66
|
+
normalized = os.homedir();
|
|
67
|
+
} else if (normalized.startsWith('~/') || normalized.startsWith('~\\')) {
|
|
68
|
+
normalized = path.join(os.homedir(), normalized.slice(2));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 先尝试直接使用(支持相对路径)
|
|
72
|
+
try {
|
|
73
|
+
if (fs.existsSync(normalized) && fs.statSync(normalized).isDirectory()) {
|
|
74
|
+
return path.isAbsolute(normalized) ? normalized : path.resolve(process.cwd(), normalized);
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
// 忽略错误,继续尝试其他候选路径
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 相对路径:优先按进程 cwd 解析,其次按用户 home 解析(用于 .codex 这类隐藏目录)
|
|
81
|
+
if (!path.isAbsolute(normalized)) {
|
|
82
|
+
const candidates = [
|
|
83
|
+
path.resolve(process.cwd(), normalized),
|
|
84
|
+
path.resolve(os.homedir(), normalized)
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
for (const candidate of candidates) {
|
|
88
|
+
try {
|
|
89
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
|
90
|
+
return candidate;
|
|
91
|
+
}
|
|
92
|
+
} catch (err) {
|
|
93
|
+
// 忽略错误
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return normalized;
|
|
99
|
+
}
|
|
100
|
+
|
|
51
101
|
createScreen(cols, rows) {
|
|
52
102
|
if (!HeadlessTerminal) {
|
|
53
103
|
return null;
|
|
@@ -193,7 +243,7 @@ class PtyManager {
|
|
|
193
243
|
throw new Error(`Cannot create terminal: ${errMsg}`);
|
|
194
244
|
}
|
|
195
245
|
|
|
196
|
-
|
|
246
|
+
let {
|
|
197
247
|
cwd = os.homedir(),
|
|
198
248
|
cols = 120,
|
|
199
249
|
rows = 30,
|
|
@@ -212,7 +262,20 @@ class PtyManager {
|
|
|
212
262
|
console.error('[PTY]', error);
|
|
213
263
|
throw new Error(error);
|
|
214
264
|
}
|
|
215
|
-
|
|
265
|
+
|
|
266
|
+
const originalCwd = cwd;
|
|
267
|
+
cwd = this.resolveWorkingDirectory(cwd);
|
|
268
|
+
if (originalCwd !== cwd) {
|
|
269
|
+
console.log(`[PTY] Resolved cwd: ${originalCwd} -> ${cwd}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory()) {
|
|
274
|
+
const error = `Working directory not found: ${cwd}`;
|
|
275
|
+
console.error('[PTY]', error);
|
|
276
|
+
throw new Error(error);
|
|
277
|
+
}
|
|
278
|
+
} catch (err) {
|
|
216
279
|
const error = `Working directory not found: ${cwd}`;
|
|
217
280
|
console.error('[PTY]', error);
|
|
218
281
|
throw new Error(error);
|
|
@@ -754,8 +754,8 @@ function searchSessions(config, projectName, keyword, contextLength = 15) {
|
|
|
754
754
|
}
|
|
755
755
|
|
|
756
756
|
// Get recent sessions across all projects
|
|
757
|
-
function getRecentSessions(config, limit = 5) {
|
|
758
|
-
const projects = getProjects(config);
|
|
757
|
+
async function getRecentSessions(config, limit = 5) {
|
|
758
|
+
const projects = await getProjects(config);
|
|
759
759
|
const allSessions = [];
|
|
760
760
|
const forkRelations = getForkRelations();
|
|
761
761
|
const aliases = loadAliases();
|
|
@@ -797,24 +797,80 @@ function getRecentSessions(config, limit = 5) {
|
|
|
797
797
|
}
|
|
798
798
|
|
|
799
799
|
// Search sessions across all projects
|
|
800
|
-
function searchSessionsAcrossProjects(config, keyword, contextLength = 35) {
|
|
801
|
-
const projects = getProjects(config);
|
|
800
|
+
async function searchSessionsAcrossProjects(config, keyword, contextLength = 35) {
|
|
802
801
|
const allResults = [];
|
|
803
802
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
const
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
803
|
+
try {
|
|
804
|
+
// Search in Claude projects
|
|
805
|
+
const claudeProjects = await getProjects(config);
|
|
806
|
+
claudeProjects.forEach(projectName => {
|
|
807
|
+
const projectResults = searchSessions(config, projectName, keyword, contextLength);
|
|
808
|
+
const { projectName: displayName, fullPath } = parseRealProjectPath(projectName);
|
|
809
|
+
|
|
810
|
+
// Add project info to each result
|
|
811
|
+
projectResults.forEach(result => {
|
|
812
|
+
allResults.push({
|
|
813
|
+
...result,
|
|
814
|
+
projectName: projectName,
|
|
815
|
+
projectDisplayName: displayName,
|
|
816
|
+
projectFullPath: fullPath,
|
|
817
|
+
channel: 'claude'
|
|
818
|
+
});
|
|
815
819
|
});
|
|
816
820
|
});
|
|
817
|
-
})
|
|
821
|
+
} catch (error) {
|
|
822
|
+
console.error('Error searching Claude projects:', error);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
try {
|
|
826
|
+
// Search in Codex projects
|
|
827
|
+
const codexProjectsDir = path.join(os.homedir(), '.codex', 'projects');
|
|
828
|
+
if (fs.existsSync(codexProjectsDir)) {
|
|
829
|
+
const codexConfig = { ...config, projectsDir: codexProjectsDir };
|
|
830
|
+
const codexProjects = await getProjects(codexConfig);
|
|
831
|
+
codexProjects.forEach(projectName => {
|
|
832
|
+
const projectResults = searchSessions(codexConfig, projectName, keyword, contextLength);
|
|
833
|
+
const { projectName: displayName, fullPath } = parseRealProjectPath(projectName);
|
|
834
|
+
|
|
835
|
+
projectResults.forEach(result => {
|
|
836
|
+
allResults.push({
|
|
837
|
+
...result,
|
|
838
|
+
projectName: projectName,
|
|
839
|
+
projectDisplayName: displayName,
|
|
840
|
+
projectFullPath: fullPath,
|
|
841
|
+
channel: 'codex'
|
|
842
|
+
});
|
|
843
|
+
});
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
} catch (error) {
|
|
847
|
+
console.error('Error searching Codex projects:', error);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
try {
|
|
851
|
+
// Search in Gemini projects
|
|
852
|
+
const geminiProjectsDir = path.join(os.homedir(), '.gemini', 'projects');
|
|
853
|
+
if (fs.existsSync(geminiProjectsDir)) {
|
|
854
|
+
const geminiConfig = { ...config, projectsDir: geminiProjectsDir };
|
|
855
|
+
const geminiProjects = await getProjects(geminiConfig);
|
|
856
|
+
geminiProjects.forEach(projectName => {
|
|
857
|
+
const projectResults = searchSessions(geminiConfig, projectName, keyword, contextLength);
|
|
858
|
+
const { projectName: displayName, fullPath } = parseRealProjectPath(projectName);
|
|
859
|
+
|
|
860
|
+
projectResults.forEach(result => {
|
|
861
|
+
allResults.push({
|
|
862
|
+
...result,
|
|
863
|
+
projectName: projectName,
|
|
864
|
+
projectDisplayName: displayName,
|
|
865
|
+
projectFullPath: fullPath,
|
|
866
|
+
channel: 'gemini'
|
|
867
|
+
});
|
|
868
|
+
});
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
} catch (error) {
|
|
872
|
+
console.error('Error searching Gemini projects:', error);
|
|
873
|
+
}
|
|
818
874
|
|
|
819
875
|
// Sort by match count
|
|
820
876
|
allResults.sort((a, b) => b.matchCount - a.matchCount);
|
|
@@ -9,6 +9,7 @@ const http = require('http');
|
|
|
9
9
|
const { URL } = require('url');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const fs = require('fs');
|
|
12
|
+
const { probeModelAvailability } = require('./model-detector');
|
|
12
13
|
|
|
13
14
|
// 测试结果缓存
|
|
14
15
|
const testResultsCache = new Map();
|
|
@@ -75,7 +76,7 @@ async function testChannelSpeed(channel, timeout = DEFAULT_TIMEOUT, channelType
|
|
|
75
76
|
|
|
76
77
|
// 直接测试 API 功能(发送测试消息)
|
|
77
78
|
// 不再单独测试网络连通性,因为直接 GET base_url 可能返回 404
|
|
78
|
-
const apiResult = await testAPIFunctionality(testUrl, channel.apiKey, sanitizedTimeout, channelType, channel.model);
|
|
79
|
+
const apiResult = await testAPIFunctionality(testUrl, channel.apiKey, sanitizedTimeout, channelType, channel.model, channel);
|
|
79
80
|
|
|
80
81
|
const success = apiResult.success;
|
|
81
82
|
const networkOk = apiResult.latency !== null; // 如果有延迟数据,说明网络是通的
|
|
@@ -90,7 +91,10 @@ async function testChannelSpeed(channel, timeout = DEFAULT_TIMEOUT, channelType
|
|
|
90
91
|
statusCode: apiResult.statusCode || null,
|
|
91
92
|
error: success ? null : (apiResult.error || '测试失败'),
|
|
92
93
|
latency: apiResult.latency || null, // 无论成功失败都保留延迟数据
|
|
93
|
-
testedAt: Date.now()
|
|
94
|
+
testedAt: Date.now(),
|
|
95
|
+
testedModel: apiResult.testedModel,
|
|
96
|
+
availableModels: apiResult.availableModels,
|
|
97
|
+
modelDetectionMethod: apiResult.modelDetectionMethod
|
|
94
98
|
};
|
|
95
99
|
|
|
96
100
|
testResultsCache.set(channel.id, finalResult);
|
|
@@ -189,18 +193,38 @@ function testNetworkConnectivity(url, apiKey, timeout) {
|
|
|
189
193
|
* @param {number} timeout - 超时时间
|
|
190
194
|
* @param {string} channelType - 渠道类型:'claude' | 'codex' | 'gemini'
|
|
191
195
|
* @param {string} model - 模型名称(可选,用于 Gemini)
|
|
196
|
+
* @param {Object} channel - 完整渠道配置(用于模型检测)
|
|
192
197
|
*/
|
|
193
|
-
function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'claude', model = null) {
|
|
198
|
+
async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'claude', model = null, channel = null) {
|
|
199
|
+
// Probe model availability if channel is provided
|
|
200
|
+
let modelProbe = null;
|
|
201
|
+
if (channel) {
|
|
202
|
+
try {
|
|
203
|
+
modelProbe = await probeModelAvailability(channel, channelType);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error('[SpeedTest] Model detection failed:', error.message);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
194
209
|
return new Promise((resolve) => {
|
|
195
210
|
const startTime = Date.now();
|
|
196
211
|
const parsedUrl = new URL(baseUrl);
|
|
197
212
|
const isHttps = parsedUrl.protocol === 'https:';
|
|
198
213
|
const httpModule = isHttps ? https : http;
|
|
199
214
|
|
|
215
|
+
// Helper to create result object with model info
|
|
216
|
+
const createResult = (result) => ({
|
|
217
|
+
...result,
|
|
218
|
+
testedModel: testModel,
|
|
219
|
+
availableModels: modelProbe?.availableModels,
|
|
220
|
+
modelDetectionMethod: modelProbe?.cached ? 'cached' : 'probed'
|
|
221
|
+
});
|
|
222
|
+
|
|
200
223
|
// 根据渠道类型确定 API 路径和请求格式
|
|
201
224
|
let apiPath;
|
|
202
225
|
let requestBody;
|
|
203
226
|
let headers;
|
|
227
|
+
let testModel = null; // Track which model is actually being tested
|
|
204
228
|
|
|
205
229
|
// Claude 渠道使用 Anthropic 格式
|
|
206
230
|
if (channelType === 'claude') {
|
|
@@ -214,10 +238,11 @@ function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'claude',
|
|
|
214
238
|
|
|
215
239
|
// 使用 Claude Code 的请求格式
|
|
216
240
|
// user_id 必须符合特定格式: user_xxx_account__session_xxx
|
|
217
|
-
//
|
|
241
|
+
// 优先使用模型检测结果,否则回退到 claude-sonnet-4-20250514
|
|
242
|
+
testModel = modelProbe?.preferredTestModel || 'claude-sonnet-4-20250514';
|
|
218
243
|
const sessionId = Math.random().toString(36).substring(2, 15);
|
|
219
244
|
requestBody = JSON.stringify({
|
|
220
|
-
model:
|
|
245
|
+
model: testModel,
|
|
221
246
|
max_tokens: 10,
|
|
222
247
|
stream: true,
|
|
223
248
|
messages: [{ role: 'user', content: [{ type: 'text', text: 'Hi' }] }],
|
|
@@ -249,12 +274,18 @@ function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'claude',
|
|
|
249
274
|
const template = JSON.parse(fs.readFileSync(CODEX_REQUEST_TEMPLATE_PATH, 'utf-8'));
|
|
250
275
|
// 生成新的 prompt_cache_key
|
|
251
276
|
template.prompt_cache_key = `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
277
|
+
// 使用模型检测结果更新模型(如果有)
|
|
278
|
+
if (modelProbe?.preferredTestModel) {
|
|
279
|
+
template.model = modelProbe.preferredTestModel;
|
|
280
|
+
}
|
|
281
|
+
testModel = template.model; // Track the model being used
|
|
252
282
|
requestBody = JSON.stringify(template);
|
|
253
283
|
} catch (err) {
|
|
254
284
|
console.error('[SpeedTest] Failed to load Codex template:', err.message);
|
|
255
285
|
// 降级使用简化版本(可能会失败)
|
|
286
|
+
testModel = modelProbe?.preferredTestModel || 'gpt-5-codex';
|
|
256
287
|
requestBody = JSON.stringify({
|
|
257
|
-
model:
|
|
288
|
+
model: testModel,
|
|
258
289
|
instructions: 'You are Codex.',
|
|
259
290
|
input: [{ type: 'message', role: 'user', content: [{ type: 'input_text', text: 'ping' }] }],
|
|
260
291
|
max_output_tokens: 10,
|
|
@@ -274,10 +305,10 @@ function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'claude',
|
|
|
274
305
|
if (!apiPath.endsWith('/chat/completions')) {
|
|
275
306
|
apiPath = apiPath + (apiPath.endsWith('/v1') ? '/chat/completions' : '/v1/chat/completions');
|
|
276
307
|
}
|
|
277
|
-
//
|
|
278
|
-
|
|
308
|
+
// 优先使用模型检测结果,其次使用渠道配置的模型,最后默认使用 gemini-2.5-pro
|
|
309
|
+
testModel = modelProbe?.preferredTestModel || model || 'gemini-2.5-pro';
|
|
279
310
|
requestBody = JSON.stringify({
|
|
280
|
-
model:
|
|
311
|
+
model: testModel,
|
|
281
312
|
max_tokens: 10,
|
|
282
313
|
messages: [{ role: 'user', content: 'Hi' }]
|
|
283
314
|
});
|
|
@@ -339,24 +370,24 @@ function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'claude',
|
|
|
339
370
|
resolved = true;
|
|
340
371
|
const latency = Date.now() - startTime;
|
|
341
372
|
req.destroy();
|
|
342
|
-
resolve({
|
|
373
|
+
resolve(createResult({
|
|
343
374
|
success: true,
|
|
344
375
|
latency,
|
|
345
376
|
error: null,
|
|
346
377
|
statusCode: res.statusCode
|
|
347
|
-
});
|
|
378
|
+
}));
|
|
348
379
|
} else if (chunkStr.includes('"detail"') || chunkStr.includes('"error"')) {
|
|
349
380
|
// 流式响应中的错误
|
|
350
381
|
resolved = true;
|
|
351
382
|
const latency = Date.now() - startTime;
|
|
352
383
|
req.destroy();
|
|
353
384
|
const errMsg = parseErrorMessage(chunkStr) || '流式响应错误';
|
|
354
|
-
resolve({
|
|
385
|
+
resolve(createResult({
|
|
355
386
|
success: false,
|
|
356
387
|
latency,
|
|
357
388
|
error: errMsg,
|
|
358
389
|
statusCode: res.statusCode
|
|
359
|
-
});
|
|
390
|
+
}));
|
|
360
391
|
}
|
|
361
392
|
}
|
|
362
393
|
});
|
|
@@ -372,106 +403,106 @@ function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'claude',
|
|
|
372
403
|
const errMsg = parseErrorMessage(data);
|
|
373
404
|
if (errMsg && (errMsg.includes('error') || errMsg.includes('Error') ||
|
|
374
405
|
errMsg.includes('失败') || errMsg.includes('错误'))) {
|
|
375
|
-
resolve({
|
|
406
|
+
resolve(createResult({
|
|
376
407
|
success: false,
|
|
377
408
|
latency,
|
|
378
409
|
error: errMsg,
|
|
379
410
|
statusCode: res.statusCode
|
|
380
|
-
});
|
|
411
|
+
}));
|
|
381
412
|
} else {
|
|
382
413
|
// 真正的成功响应
|
|
383
|
-
resolve({
|
|
414
|
+
resolve(createResult({
|
|
384
415
|
success: true,
|
|
385
416
|
latency,
|
|
386
417
|
error: null,
|
|
387
418
|
statusCode: res.statusCode
|
|
388
|
-
});
|
|
419
|
+
}));
|
|
389
420
|
}
|
|
390
421
|
} else if (res.statusCode === 401) {
|
|
391
|
-
resolve({
|
|
422
|
+
resolve(createResult({
|
|
392
423
|
success: false,
|
|
393
424
|
latency,
|
|
394
425
|
error: 'API Key 无效或已过期',
|
|
395
426
|
statusCode: res.statusCode
|
|
396
|
-
});
|
|
427
|
+
}));
|
|
397
428
|
} else if (res.statusCode === 403) {
|
|
398
|
-
resolve({
|
|
429
|
+
resolve(createResult({
|
|
399
430
|
success: false,
|
|
400
431
|
latency,
|
|
401
432
|
error: 'API Key 权限不足',
|
|
402
433
|
statusCode: res.statusCode
|
|
403
|
-
});
|
|
434
|
+
}));
|
|
404
435
|
} else if (res.statusCode === 429) {
|
|
405
436
|
// 请求过多 - 标记为失败
|
|
406
437
|
const errMsg = parseErrorMessage(data) || '请求过多,服务限流中';
|
|
407
|
-
resolve({
|
|
438
|
+
resolve(createResult({
|
|
408
439
|
success: false,
|
|
409
440
|
latency,
|
|
410
441
|
error: errMsg,
|
|
411
442
|
statusCode: res.statusCode
|
|
412
|
-
});
|
|
443
|
+
}));
|
|
413
444
|
} else if (res.statusCode === 503 || res.statusCode === 529) {
|
|
414
445
|
// 服务暂时不可用/过载 - 标记为失败
|
|
415
446
|
const errMsg = parseErrorMessage(data) || (res.statusCode === 503 ? '服务暂时不可用' : '服务过载');
|
|
416
|
-
resolve({
|
|
447
|
+
resolve(createResult({
|
|
417
448
|
success: false,
|
|
418
449
|
latency,
|
|
419
450
|
error: errMsg,
|
|
420
451
|
statusCode: res.statusCode
|
|
421
|
-
});
|
|
452
|
+
}));
|
|
422
453
|
} else if (res.statusCode === 402) {
|
|
423
|
-
resolve({
|
|
454
|
+
resolve(createResult({
|
|
424
455
|
success: false,
|
|
425
456
|
latency,
|
|
426
457
|
error: '账户余额不足',
|
|
427
458
|
statusCode: res.statusCode
|
|
428
|
-
});
|
|
459
|
+
}));
|
|
429
460
|
} else if (res.statusCode === 400) {
|
|
430
461
|
// 请求参数错误
|
|
431
462
|
const errMsg = parseErrorMessage(data) || '请求参数错误';
|
|
432
|
-
resolve({
|
|
463
|
+
resolve(createResult({
|
|
433
464
|
success: false,
|
|
434
465
|
latency,
|
|
435
466
|
error: errMsg,
|
|
436
467
|
statusCode: res.statusCode
|
|
437
|
-
});
|
|
468
|
+
}));
|
|
438
469
|
} else if (res.statusCode >= 500) {
|
|
439
470
|
// 5xx 服务器错误
|
|
440
471
|
const errMsg = parseErrorMessage(data) || `服务器错误 (${res.statusCode})`;
|
|
441
|
-
resolve({
|
|
472
|
+
resolve(createResult({
|
|
442
473
|
success: false,
|
|
443
474
|
latency,
|
|
444
475
|
error: errMsg,
|
|
445
476
|
statusCode: res.statusCode
|
|
446
|
-
});
|
|
477
|
+
}));
|
|
447
478
|
} else {
|
|
448
479
|
// 其他错误
|
|
449
480
|
const errMsg = parseErrorMessage(data) || `HTTP ${res.statusCode}`;
|
|
450
|
-
resolve({
|
|
481
|
+
resolve(createResult({
|
|
451
482
|
success: false,
|
|
452
483
|
latency,
|
|
453
484
|
error: errMsg,
|
|
454
485
|
statusCode: res.statusCode
|
|
455
|
-
});
|
|
486
|
+
}));
|
|
456
487
|
}
|
|
457
488
|
});
|
|
458
489
|
});
|
|
459
490
|
|
|
460
491
|
req.on('error', (error) => {
|
|
461
|
-
resolve({
|
|
492
|
+
resolve(createResult({
|
|
462
493
|
success: false,
|
|
463
494
|
latency: null,
|
|
464
495
|
error: error.message || '请求失败'
|
|
465
|
-
});
|
|
496
|
+
}));
|
|
466
497
|
});
|
|
467
498
|
|
|
468
499
|
req.on('timeout', () => {
|
|
469
500
|
req.destroy();
|
|
470
|
-
resolve({
|
|
501
|
+
resolve(createResult({
|
|
471
502
|
success: false,
|
|
472
503
|
latency: null,
|
|
473
504
|
error: 'API 请求超时'
|
|
474
|
-
});
|
|
505
|
+
}));
|
|
475
506
|
});
|
|
476
507
|
|
|
477
508
|
req.write(requestBody);
|
|
@@ -36,6 +36,37 @@ function resolvePricing(toolKey, modelPricing = {}, defaultPricing = {}) {
|
|
|
36
36
|
return base;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
function resolveModelPricing(toolKey, model, hardcodedPricing = {}, defaultPricing = {}) {
|
|
40
|
+
const config = getPricingConfig(toolKey);
|
|
41
|
+
|
|
42
|
+
// 1. Check per-model config
|
|
43
|
+
const modelConfig = config?.models?.[model];
|
|
44
|
+
if (modelConfig && modelConfig.mode === 'custom') {
|
|
45
|
+
const result = { ...hardcodedPricing };
|
|
46
|
+
RATE_KEYS.forEach((key) => {
|
|
47
|
+
if (typeof modelConfig[key] === 'number' && Number.isFinite(modelConfig[key])) {
|
|
48
|
+
result[key] = modelConfig[key];
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 2. Fall back to tool-level config
|
|
55
|
+
if (config && config.mode === 'custom') {
|
|
56
|
+
const result = { ...hardcodedPricing };
|
|
57
|
+
RATE_KEYS.forEach((key) => {
|
|
58
|
+
if (typeof config[key] === 'number' && Number.isFinite(config[key])) {
|
|
59
|
+
result[key] = config[key];
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 3. Use hardcoded pricing
|
|
66
|
+
return { ...defaultPricing, ...hardcodedPricing };
|
|
67
|
+
}
|
|
68
|
+
|
|
39
69
|
module.exports = {
|
|
40
|
-
resolvePricing
|
|
70
|
+
resolvePricing,
|
|
71
|
+
resolveModelPricing
|
|
41
72
|
};
|
package/src/ui/menu.js
CHANGED
|
@@ -113,6 +113,7 @@ async function showMainMenu(config) {
|
|
|
113
113
|
{ name: chalk.cyan('查看调度状态'), value: 'channel-status' },
|
|
114
114
|
{ name: chalk.cyan(`是否开启动态切换 (${proxyStatusText})`), value: 'toggle-proxy' },
|
|
115
115
|
{ name: chalk.cyan('添加渠道'), value: 'add-channel' },
|
|
116
|
+
{ name: chalk.blue('插件管理'), value: 'plugin-menu' },
|
|
116
117
|
new inquirer.Separator(chalk.gray('─'.repeat(14))),
|
|
117
118
|
{ name: chalk.magenta('配置端口'), value: 'port-config' },
|
|
118
119
|
{ name: chalk.yellow('恢复默认配置'), value: 'reset' },
|