@adversity/coding-tool-x 2.5.1 → 2.6.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 +14 -0
- package/dist/web/assets/icons-CNM9_Fh0.js +1 -0
- package/dist/web/assets/index-BcmuQT-z.css +41 -0
- package/dist/web/assets/{index-DZjidyED.js → index-Ej0MPDUI.js} +2 -2
- package/dist/web/index.html +3 -3
- 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 +276 -0
- 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 +177 -0
- package/src/server/services/pty-manager.js +65 -2
- 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
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugins Service
|
|
3
|
+
*
|
|
4
|
+
* Wraps the plugin system for API access
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { listPlugins, getPlugin, updatePlugin: updatePluginRegistry } = require('../../plugins/registry');
|
|
10
|
+
const { installPlugin: installPluginCore, uninstallPlugin: uninstallPluginCore } = require('../../plugins/plugin-installer');
|
|
11
|
+
const { initializePlugins, shutdownPlugins } = require('../../plugins/plugin-manager');
|
|
12
|
+
const { INSTALLED_DIR, CONFIG_DIR } = require('../../plugins/constants');
|
|
13
|
+
|
|
14
|
+
class PluginsService {
|
|
15
|
+
/**
|
|
16
|
+
* List all installed plugins with their status
|
|
17
|
+
* @returns {Object} { plugins: Array }
|
|
18
|
+
*/
|
|
19
|
+
listPlugins() {
|
|
20
|
+
const plugins = listPlugins();
|
|
21
|
+
|
|
22
|
+
// Enhance with additional info
|
|
23
|
+
const enhancedPlugins = plugins.map(plugin => {
|
|
24
|
+
const pluginDir = path.join(INSTALLED_DIR, plugin.name);
|
|
25
|
+
const manifestPath = path.join(pluginDir, 'plugin.json');
|
|
26
|
+
|
|
27
|
+
let manifest = null;
|
|
28
|
+
if (fs.existsSync(manifestPath)) {
|
|
29
|
+
try {
|
|
30
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
31
|
+
} catch (err) {
|
|
32
|
+
// Ignore parse errors
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
...plugin,
|
|
38
|
+
description: manifest?.description || '',
|
|
39
|
+
author: manifest?.author || '',
|
|
40
|
+
commands: manifest?.commands || [],
|
|
41
|
+
hooks: manifest?.hooks || []
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return { plugins: enhancedPlugins };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get single plugin details
|
|
50
|
+
* @param {string} name - Plugin name
|
|
51
|
+
* @returns {Object|null} Plugin details or null
|
|
52
|
+
*/
|
|
53
|
+
getPlugin(name) {
|
|
54
|
+
const plugin = getPlugin(name);
|
|
55
|
+
if (!plugin) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const pluginDir = path.join(INSTALLED_DIR, name);
|
|
60
|
+
const manifestPath = path.join(pluginDir, 'plugin.json');
|
|
61
|
+
|
|
62
|
+
let manifest = null;
|
|
63
|
+
if (fs.existsSync(manifestPath)) {
|
|
64
|
+
try {
|
|
65
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
66
|
+
} catch (err) {
|
|
67
|
+
// Ignore parse errors
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
name,
|
|
73
|
+
...plugin,
|
|
74
|
+
description: manifest?.description || '',
|
|
75
|
+
author: manifest?.author || '',
|
|
76
|
+
commands: manifest?.commands || [],
|
|
77
|
+
hooks: manifest?.hooks || [],
|
|
78
|
+
manifest
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Install plugin from Git URL
|
|
84
|
+
* @param {string} gitUrl - Git repository URL
|
|
85
|
+
* @returns {Promise<Object>} Installation result
|
|
86
|
+
*/
|
|
87
|
+
async installPlugin(gitUrl) {
|
|
88
|
+
return await installPluginCore(gitUrl);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Uninstall plugin
|
|
93
|
+
* @param {string} name - Plugin name
|
|
94
|
+
* @returns {Object} Uninstallation result
|
|
95
|
+
*/
|
|
96
|
+
uninstallPlugin(name) {
|
|
97
|
+
return uninstallPluginCore(name);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Toggle plugin enabled/disabled
|
|
102
|
+
* @param {string} name - Plugin name
|
|
103
|
+
* @param {boolean} enabled - Enable or disable
|
|
104
|
+
* @returns {Object} Updated plugin info
|
|
105
|
+
*/
|
|
106
|
+
togglePlugin(name, enabled) {
|
|
107
|
+
const plugin = getPlugin(name);
|
|
108
|
+
if (!plugin) {
|
|
109
|
+
throw new Error(`Plugin "${name}" not found`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
updatePluginRegistry(name, { enabled });
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
name,
|
|
116
|
+
...getPlugin(name)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Update plugin config
|
|
122
|
+
* @param {string} name - Plugin name
|
|
123
|
+
* @param {Object} config - Configuration object
|
|
124
|
+
* @returns {Object} Result
|
|
125
|
+
*/
|
|
126
|
+
updatePluginConfig(name, config) {
|
|
127
|
+
const plugin = getPlugin(name);
|
|
128
|
+
if (!plugin) {
|
|
129
|
+
throw new Error(`Plugin "${name}" not found`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const configFile = path.join(CONFIG_DIR, `${name}.json`);
|
|
133
|
+
|
|
134
|
+
// Ensure config directory exists
|
|
135
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
136
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fs.writeFileSync(configFile, JSON.stringify(config, null, 2), 'utf8');
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
success: true,
|
|
143
|
+
message: `Configuration updated for plugin "${name}"`
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get plugin repositories (for future use)
|
|
149
|
+
* @returns {Array} Empty array for now
|
|
150
|
+
*/
|
|
151
|
+
getRepos() {
|
|
152
|
+
// TODO: Implement plugin repository system
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Add repository (for future use)
|
|
158
|
+
* @param {Object} repo - Repository info
|
|
159
|
+
* @returns {Array} Updated repos list
|
|
160
|
+
*/
|
|
161
|
+
addRepo(repo) {
|
|
162
|
+
// TODO: Implement plugin repository system
|
|
163
|
+
throw new Error('Plugin repositories not yet implemented');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Remove repository (for future use)
|
|
168
|
+
* @param {string} id - Repository ID
|
|
169
|
+
* @returns {Array} Updated repos list
|
|
170
|
+
*/
|
|
171
|
+
removeRepo(id) {
|
|
172
|
+
// TODO: Implement plugin repository system
|
|
173
|
+
throw new Error('Plugin repositories not yet implemented');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = { PluginsService };
|
|
@@ -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);
|
|
@@ -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' },
|