@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.
@@ -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
- const {
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
- if (!fs.existsSync(cwd)) {
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
- // 使用 claude-sonnet-4 模型测试,因为 haiku 可能没有配额
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: 'claude-sonnet-4-20250514',
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: 'gpt-5-codex',
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
- // 使用渠道配置的模型,如果没有则默认使用 gemini-2.5-pro
278
- const geminiModel = model || 'gemini-2.5-pro';
308
+ // 优先使用模型检测结果,其次使用渠道配置的模型,最后默认使用 gemini-2.5-pro
309
+ testModel = modelProbe?.preferredTestModel || model || 'gemini-2.5-pro';
279
310
  requestBody = JSON.stringify({
280
- model: geminiModel,
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' },