@adversity/coding-tool-x 3.0.3 → 3.0.5

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.
@@ -12,6 +12,7 @@ const { resolvePricing, resolveModelPricing } = require('./utils/pricing');
12
12
  const { recordRequest } = require('./services/statistics-service');
13
13
  const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('./services/proxy-runtime');
14
14
  const eventBus = require('../plugins/event-bus');
15
+ const { CLAUDE_MODEL_PRICING } = require('../config/model-pricing');
15
16
 
16
17
  let proxyServer = null;
17
18
  let proxyApp = null;
@@ -20,21 +21,67 @@ let currentPort = null;
20
21
  // 用于存储每个请求的元数据(用于 WebSocket 日志)
21
22
  const requestMetadata = new Map();
22
23
 
23
- // Claude API 定价(每百万 tokens 的价格,单位:美元)
24
- const PRICING = {
25
- 'claude-sonnet-4-5-20250929': { input: 3, output: 15, cacheCreation: 3.75, cacheRead: 0.30 },
26
- 'claude-sonnet-4-20250514': { input: 3, output: 15, cacheCreation: 3.75, cacheRead: 0.30 },
27
- 'claude-sonnet-3-5-20241022': { input: 3, output: 15, cacheCreation: 3.75, cacheRead: 0.30 },
28
- 'claude-sonnet-3-5-20240620': { input: 3, output: 15, cacheCreation: 3.75, cacheRead: 0.30 },
29
- 'claude-opus-4-20250514': { input: 15, output: 75, cacheCreation: 18.75, cacheRead: 1.50 },
30
- 'claude-opus-3-20240229': { input: 15, output: 75, cacheCreation: 18.75, cacheRead: 1.50 },
31
- 'claude-haiku-3-5-20241022': { input: 0.8, output: 4, cacheCreation: 1, cacheRead: 0.08 },
32
- 'claude-3-5-haiku-20241022': { input: 0.8, output: 4, cacheCreation: 1, cacheRead: 0.08 }
33
- };
34
-
35
24
  const CLAUDE_BASE_PRICING = DEFAULT_CONFIG.pricing.claude;
36
25
  const ONE_MILLION = 1000000;
37
26
 
27
+ /**
28
+ * 检测模型层级
29
+ * @param {string} modelName - 模型名称
30
+ * @returns {string|null} 模型层级 (opus/sonnet/haiku) 或 null
31
+ */
32
+ function detectModelTier(modelName) {
33
+ if (!modelName) return null;
34
+ const lower = modelName.toLowerCase();
35
+ if (lower.includes('opus')) return 'opus';
36
+ if (lower.includes('sonnet')) return 'sonnet';
37
+ if (lower.includes('haiku')) return 'haiku';
38
+ return null;
39
+ }
40
+
41
+ /**
42
+ * 应用模型重定向
43
+ * @param {string} originalModel - 原始模型名称
44
+ * @param {object} channel - 渠道对象,包含 modelConfig 和 modelRedirects
45
+ * @returns {string} 重定向后的模型名称
46
+ */
47
+ function redirectModel(originalModel, channel) {
48
+ if (!originalModel) return originalModel;
49
+
50
+ // 优先使用新的 modelRedirects 数组格式
51
+ const modelRedirects = channel?.modelRedirects;
52
+ if (Array.isArray(modelRedirects) && modelRedirects.length > 0) {
53
+ for (const rule of modelRedirects) {
54
+ if (rule.from && rule.to && rule.from === originalModel) {
55
+ return rule.to;
56
+ }
57
+ }
58
+ }
59
+
60
+ // 向后兼容:使用旧的 modelConfig 格式
61
+ const modelConfig = channel?.modelConfig;
62
+ if (!modelConfig) return originalModel;
63
+
64
+ const tier = detectModelTier(originalModel);
65
+
66
+ // 优先级:层级特定配置 > 通用模型覆盖
67
+ if (tier === 'opus' && modelConfig.opusModel) {
68
+ return modelConfig.opusModel;
69
+ }
70
+ if (tier === 'sonnet' && modelConfig.sonnetModel) {
71
+ return modelConfig.sonnetModel;
72
+ }
73
+ if (tier === 'haiku' && modelConfig.haikuModel) {
74
+ return modelConfig.haikuModel;
75
+ }
76
+
77
+ // 回退到通用模型覆盖
78
+ if (modelConfig.model) {
79
+ return modelConfig.model;
80
+ }
81
+
82
+ return originalModel;
83
+ }
84
+
38
85
  /**
39
86
  * 计算请求成本
40
87
  * @param {string} model - 模型名称
@@ -42,7 +89,7 @@ const ONE_MILLION = 1000000;
42
89
  * @returns {number} 成本(美元)
43
90
  */
44
91
  function calculateCost(model, tokens) {
45
- const hardcodedPricing = PRICING[model] || {};
92
+ const hardcodedPricing = CLAUDE_MODEL_PRICING[model] || {};
46
93
  const pricing = resolveModelPricing('claude', model, hardcodedPricing, CLAUDE_BASE_PRICING);
47
94
 
48
95
  const inputRate = typeof pricing.input === 'number' ? pricing.input : CLAUDE_BASE_PRICING.input;
@@ -168,6 +215,20 @@ async function startProxyServer(options = {}) {
168
215
 
169
216
  req.selectedChannel = channel;
170
217
  req.sessionId = sessionId || null;
218
+
219
+ // 应用模型重定向(当 proxy 开启时)
220
+ if (req.body && req.body.model) {
221
+ const originalModel = req.body.model;
222
+ const redirectedModel = redirectModel(originalModel, channel);
223
+
224
+ if (redirectedModel !== originalModel) {
225
+ req.body.model = redirectedModel;
226
+ // 更新 rawBody 以匹配修改后的 body
227
+ req.rawBody = Buffer.from(JSON.stringify(req.body));
228
+ console.log(`[Model Redirect] ${originalModel} → ${redirectedModel} (channel: ${channel.name})`);
229
+ }
230
+ }
231
+
171
232
  let released = false;
172
233
 
173
234
  const release = () => {
@@ -74,7 +74,9 @@ function refreshChannels(source = 'claude') {
74
74
  baseUrl: ch.baseUrl,
75
75
  apiKey: ch.apiKey,
76
76
  weight: Math.max(1, Number(ch.weight) || 1),
77
- maxConcurrency: ch.maxConcurrency ?? null
77
+ maxConcurrency: ch.maxConcurrency ?? null,
78
+ modelConfig: ch.modelConfig || null,
79
+ modelRedirects: ch.modelRedirects || []
78
80
  }));
79
81
 
80
82
  state.channels.forEach(ch => {
@@ -187,6 +187,7 @@ function createChannel(name, baseUrl, apiKey, websiteUrl, extraConfig = {}) {
187
187
  maxConcurrency: extraConfig.maxConcurrency,
188
188
  presetId: extraConfig.presetId || 'official',
189
189
  modelConfig: extraConfig.modelConfig || null,
190
+ modelRedirects: extraConfig.modelRedirects || [],
190
191
  proxyUrl: extraConfig.proxyUrl || '',
191
192
  speedTestModel: extraConfig.speedTestModel || null
192
193
  });
@@ -215,8 +216,10 @@ function updateChannel(id, updates) {
215
216
  enabled: merged.enabled,
216
217
  presetId: merged.presetId,
217
218
  modelConfig: merged.modelConfig,
219
+ modelRedirects: merged.modelRedirects || [],
218
220
  proxyUrl: merged.proxyUrl,
219
- speedTestModel: merged.speedTestModel
221
+ speedTestModel: merged.speedTestModel,
222
+ updatedAt: Date.now()
220
223
  });
221
224
 
222
225
  // Get proxy status
@@ -238,8 +238,15 @@ class McpClient extends EventEmitter {
238
238
  }, this._timeout);
239
239
 
240
240
  try {
241
+ // 确保 PATH 不被覆盖,优先使用用户提供的 env,但保留 PATH
242
+ const mergedEnv = { ...process.env, ...env };
243
+ // 如果用户提供的 env 中有 PATH,将其追加到系统 PATH 前面
244
+ if (env && env.PATH && process.env.PATH) {
245
+ mergedEnv.PATH = `${env.PATH}:${process.env.PATH}`;
246
+ }
247
+
241
248
  this._child = spawn(command, args, {
242
- env: { ...process.env, ...env },
249
+ env: mergedEnv,
243
250
  stdio: ['pipe', 'pipe', 'pipe'],
244
251
  cwd: cwd || process.cwd()
245
252
  });
@@ -267,7 +274,15 @@ class McpClient extends EventEmitter {
267
274
 
268
275
  this._child.on('error', (err) => {
269
276
  if (err.code === 'ENOENT') {
270
- settle(new McpClientError(`Command "${command}" not found. Ensure it is installed and in PATH.`));
277
+ const pathHint = mergedEnv.PATH
278
+ ? `\n Current PATH: ${mergedEnv.PATH.split(':').slice(0, 5).join(':')}\n (showing first 5 entries)`
279
+ : '\n PATH is not set!';
280
+ settle(new McpClientError(
281
+ `Command "${command}" not found. Please check:\n` +
282
+ ` 1. Is "${command}" installed?\n` +
283
+ ` 2. Try using absolute path (e.g., /usr/bin/node or $(which ${command}))\n` +
284
+ ` 3. Check your PATH environment variable${pathHint}`
285
+ ));
271
286
  } else {
272
287
  settle(new McpClientError(`Failed to start process: ${err.message}`));
273
288
  }
@@ -13,11 +13,13 @@ const { URL } = require('url');
13
13
  // Model priority by channel type
14
14
  const MODEL_PRIORITY = {
15
15
  claude: [
16
- 'claude-haiku-3-5-20241022',
17
- 'claude-3-5-haiku-20241022',
18
- 'claude-sonnet-4-20250514',
16
+ 'claude-opus-4-5-20250929',
19
17
  'claude-sonnet-4-5-20250929',
20
- 'claude-opus-4-20250514'
18
+ 'claude-haiku-4-5-20250929',
19
+ 'claude-sonnet-4-20250514',
20
+ 'claude-opus-4-20250514',
21
+ 'claude-haiku-3-5-20241022',
22
+ 'claude-3-5-haiku-20241022'
21
23
  ],
22
24
  codex: ['gpt-4o-mini', 'gpt-4o', 'gpt-5-codex', 'o3'],
23
25
  gemini: ['gemini-2.5-flash', 'gemini-2.5-pro']
@@ -183,10 +183,80 @@ function createWorkspace(options) {
183
183
 
184
184
  const workspaceProjects = [];
185
185
 
186
+ /**
187
+ * 验证 worktree 分支冲突
188
+ * @param {Array} projects - 项目列表
189
+ * @returns {Array} 冲突列表 [{repo, branch, projects: [index1, index2]}]
190
+ */
191
+ function validateWorktreeBranches(projects) {
192
+ const repoMap = new Map();
193
+ const conflicts = [];
194
+
195
+ for (let i = 0; i < projects.length; i++) {
196
+ const proj = projects[i];
197
+ const { sourcePath, branch, createWorktree } = proj;
198
+
199
+ // Skip non-worktree projects
200
+ const isGit = isGitRepo(sourcePath);
201
+ const useWorktree = createWorktree !== undefined ? createWorktree : isGit;
202
+ if (!useWorktree || !isGit) continue;
203
+
204
+ // Resolve to absolute path to handle symlinks
205
+ const resolvedPath = fs.realpathSync(sourcePath);
206
+
207
+ // Determine target branch
208
+ let targetBranch = branch;
209
+ if (!targetBranch) {
210
+ try {
211
+ targetBranch = execSync('git rev-parse --abbrev-ref HEAD', {
212
+ cwd: sourcePath,
213
+ encoding: 'utf8'
214
+ }).trim();
215
+ } catch (e) {
216
+ targetBranch = 'main';
217
+ }
218
+ }
219
+
220
+ // Check for conflicts
221
+ if (!repoMap.has(resolvedPath)) {
222
+ repoMap.set(resolvedPath, new Map());
223
+ }
224
+
225
+ const branches = repoMap.get(resolvedPath);
226
+ if (branches.has(targetBranch)) {
227
+ const conflictingIndex = branches.get(targetBranch);
228
+ conflicts.push({
229
+ repo: resolvedPath,
230
+ branch: targetBranch,
231
+ projects: [conflictingIndex, i]
232
+ });
233
+ } else {
234
+ branches.set(targetBranch, i);
235
+ }
236
+ }
237
+
238
+ return conflicts;
239
+ }
240
+
241
+ // Validate branch uniqueness for worktree mode
242
+ const branchConflicts = validateWorktreeBranches(projects);
243
+ if (branchConflicts.length > 0) {
244
+ const conflict = branchConflicts[0];
245
+ const projectNames = conflict.projects.map(i => projects[i].name || `项目${i + 1}`).join(', ');
246
+ throw new Error(
247
+ `无法创建工作区:分支 '${conflict.branch}' 在同一仓库中被多个项目使用 (${projectNames})。\n` +
248
+ `仓库路径: ${conflict.repo}\n\n` +
249
+ `Git worktree 不允许在同一仓库的多个工作树中检出相同的分支。\n\n` +
250
+ `解决方案:\n` +
251
+ `1. 为不同项目指定不同的分支名\n` +
252
+ `2. 或者禁用其中一个项目的 worktree 模式(设置 createWorktree: false)`
253
+ );
254
+ }
255
+
186
256
  try {
187
257
  // 处理每个项目
188
258
  for (const proj of projects) {
189
- const { sourcePath, name: linkName, branch } = proj;
259
+ const { sourcePath, name: linkName, branch, baseBranch } = proj;
190
260
  // useWorktree: 未指定时,Git 仓库默认 true,非 Git 仓库默认 false
191
261
  const isGit = isGitRepo(sourcePath);
192
262
  const useWorktree = proj.createWorktree !== undefined ? proj.createWorktree : isGit;
@@ -248,7 +318,15 @@ function createWorkspace(options) {
248
318
  } catch (error) {
249
319
  // 如果分支不存在,尝试创建新分支
250
320
  try {
251
- execSync(`git worktree add "${worktreePath}" -b "${targetBranch}"`, {
321
+ // 构建 git worktree add 命令
322
+ let worktreeCmd = `git worktree add "${worktreePath}" -b "${targetBranch}"`;
323
+
324
+ // 如果指定了基础分支,添加到命令中
325
+ if (baseBranch && baseBranch.trim()) {
326
+ worktreeCmd += ` "${baseBranch}"`;
327
+ }
328
+
329
+ execSync(worktreeCmd, {
252
330
  cwd: sourcePath,
253
331
  stdio: 'pipe'
254
332
  });
@@ -258,7 +336,16 @@ function createWorkspace(options) {
258
336
  path: worktreePath
259
337
  });
260
338
  } catch (err) {
261
- // worktree 创建失败,回退到软链接模式
339
+ // Check if it's a "branch already checked out" error
340
+ if (err.message.includes('already checked out')) {
341
+ throw new Error(
342
+ `无法创建 worktree:分支 '${targetBranch}' 已在其他工作树中检出。\n` +
343
+ `错误详情: ${err.message}\n\n` +
344
+ `提示:请为此项目指定不同的分支名,或禁用 worktree 模式。`
345
+ );
346
+ }
347
+
348
+ // For other errors, provide clear message but allow fallback
262
349
  console.warn(`创建 worktree 失败,使用软链接: ${err.message}`);
263
350
  targetPath = sourcePath;
264
351
  worktrees = getGitWorktrees(sourcePath);
@@ -472,7 +559,7 @@ function addProjectToWorkspace(workspaceId, projectConfig) {
472
559
  throw new Error('工作区不存在');
473
560
  }
474
561
 
475
- const { sourcePath, name: linkName, branch } = projectConfig;
562
+ const { sourcePath, name: linkName, branch, baseBranch } = projectConfig;
476
563
  // useWorktree: 未指定时,Git 仓库默认 true,非 Git 仓库默认 false
477
564
  const isGit = isGitRepo(sourcePath);
478
565
  const useWorktree = projectConfig.createWorktree !== undefined ? projectConfig.createWorktree : isGit;
@@ -525,14 +612,48 @@ function addProjectToWorkspace(workspaceId, projectConfig) {
525
612
  targetPath = worktreePath;
526
613
  worktrees.push({ branch: targetBranch, path: worktreePath });
527
614
  } catch (error) {
615
+ // Check if branch is already checked out elsewhere
616
+ if (error.message && error.message.includes('already checked out')) {
617
+ throw new Error(
618
+ `无法添加项目:分支 '${targetBranch}' 已在其他工作树中检出。\n` +
619
+ `仓库路径: ${sourcePath}\n\n` +
620
+ `Git worktree 不允许在同一仓库的多个工作树中检出相同的分支。\n\n` +
621
+ `解决方案:\n` +
622
+ `1. 指定不同的分支名\n` +
623
+ `2. 或者禁用 worktree 模式(设置 createWorktree: false)`
624
+ );
625
+ }
626
+
627
+ // Branch doesn't exist, try creating it
528
628
  try {
529
- execSync(`git worktree add "${worktreePath}" -b "${targetBranch}"`, {
629
+ // 构建 git worktree add 命令
630
+ let worktreeCmd = `git worktree add "${worktreePath}" -b "${targetBranch}"`;
631
+
632
+ // 如果指定了基础分支,添加到命令中
633
+ if (baseBranch && baseBranch.trim()) {
634
+ worktreeCmd += ` "${baseBranch}"`;
635
+ }
636
+
637
+ execSync(worktreeCmd, {
530
638
  cwd: sourcePath,
531
639
  stdio: 'pipe'
532
640
  });
533
641
  targetPath = worktreePath;
534
642
  worktrees.push({ branch: targetBranch, path: worktreePath });
535
643
  } catch (err) {
644
+ // Check for "already checked out" error in create branch attempt
645
+ if (err.message && err.message.includes('already checked out')) {
646
+ throw new Error(
647
+ `无法添加项目:分支 '${targetBranch}' 已在其他工作树中检出。\n` +
648
+ `仓库路径: ${sourcePath}\n\n` +
649
+ `Git worktree 不允许在同一仓库的多个工作树中检出相同的分支。\n\n` +
650
+ `解决方案:\n` +
651
+ `1. 指定不同的分支名\n` +
652
+ `2. 或者禁用 worktree 模式(设置 createWorktree: false)`
653
+ );
654
+ }
655
+
656
+ // Other errors: fall back to symlink mode
536
657
  console.warn(`创建 worktree 失败,使用软链接: ${err.message}`);
537
658
  targetPath = sourcePath;
538
659
  worktrees = getGitWorktrees(sourcePath);
@@ -1,5 +1,6 @@
1
1
  const { loadConfig } = require('../../config/loader');
2
2
  const DEFAULT_CONFIG = require('../../config/default');
3
+ const { CLAUDE_MODEL_PRICING, CLAUDE_MODEL_ALIASES } = require('../../config/model-pricing');
3
4
 
4
5
  const RATE_KEYS = ['input', 'output', 'cacheCreation', 'cacheRead', 'cached', 'reasoning'];
5
6
 
@@ -39,7 +40,7 @@ function resolvePricing(toolKey, modelPricing = {}, defaultPricing = {}) {
39
40
  function resolveModelPricing(toolKey, model, hardcodedPricing = {}, defaultPricing = {}) {
40
41
  const config = getPricingConfig(toolKey);
41
42
 
42
- // 1. Check per-model config
43
+ // 1. Check user custom config for specific model first
43
44
  const modelConfig = config?.models?.[model];
44
45
  if (modelConfig && modelConfig.mode === 'custom') {
45
46
  const result = { ...hardcodedPricing };
@@ -51,7 +52,7 @@ function resolveModelPricing(toolKey, model, hardcodedPricing = {}, defaultPrici
51
52
  return result;
52
53
  }
53
54
 
54
- // 2. Fall back to tool-level config
55
+ // 2. Check user custom config for tool-level
55
56
  if (config && config.mode === 'custom') {
56
57
  const result = { ...hardcodedPricing };
57
58
  RATE_KEYS.forEach((key) => {
@@ -62,7 +63,16 @@ function resolveModelPricing(toolKey, model, hardcodedPricing = {}, defaultPrici
62
63
  return result;
63
64
  }
64
65
 
65
- // 3. Use hardcoded pricing
66
+ // 3. Use centralized hardcoded pricing for known models (mode: 'auto')
67
+ // Normalize model name using aliases
68
+ const normalizedModel = CLAUDE_MODEL_ALIASES[model] || model;
69
+ const centralizedPricing = CLAUDE_MODEL_PRICING[normalizedModel];
70
+
71
+ if (centralizedPricing) {
72
+ return { ...defaultPricing, ...centralizedPricing };
73
+ }
74
+
75
+ // 4. Fall back to base pricing for unknown models
66
76
  return { ...defaultPricing, ...hardcodedPricing };
67
77
  }
68
78