@adversity/coding-tool-x 3.0.2 → 3.0.4

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.
@@ -5,12 +5,12 @@
5
5
  <link rel="icon" href="/favicon.ico">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
7
  <title>CC-TOOL - ClaudeCode增强工作助手</title>
8
- <script type="module" crossorigin src="/assets/index-DfPKAt9R.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-Bpjcdalh.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/vue-vendor-6JaYHOiI.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/vendors-D2HHw_aW.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/icons-BlzwYoRU.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/naive-ui-B1TP-0TP.js">
13
- <link rel="stylesheet" crossorigin href="/assets/index-TjhcaFRe.css">
13
+ <link rel="stylesheet" crossorigin href="/assets/index-CB782_71.css">
14
14
  </head>
15
15
  <body>
16
16
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adversity/coding-tool-x",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
4
4
  "description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -24,15 +24,8 @@ const DEFAULT_CONFIG = {
24
24
  cacheCreation: 3.75,
25
25
  cacheRead: 0.30,
26
26
  models: {
27
- 'claude-sonnet-4-20250514': { mode: 'auto' },
28
- 'claude-haiku-3-5-20241022': {
29
- mode: 'custom',
30
- input: 0.8,
31
- output: 4,
32
- cacheCreation: 1,
33
- cacheRead: 0.08
34
- },
35
- 'claude-opus-4-20250514': { mode: 'auto' }
27
+ // All models use centralized pricing from src/config/model-pricing.js
28
+ // Add custom entries here only if you need to override official pricing
36
29
  }
37
30
  },
38
31
  codex: {
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Centralized Claude Model Pricing
3
+ *
4
+ * Official Anthropic pricing as of 2026-02-02
5
+ * Source: https://claude.com/pricing
6
+ *
7
+ * All prices in USD per million tokens
8
+ */
9
+
10
+ const CLAUDE_MODEL_PRICING = {
11
+ // Claude 4.5 (current generation)
12
+ 'claude-opus-4-5-20250929': {
13
+ input: 5,
14
+ output: 25,
15
+ cacheCreation: 6.25,
16
+ cacheRead: 0.50
17
+ },
18
+ 'claude-sonnet-4-5-20250929': {
19
+ input: 3,
20
+ output: 15,
21
+ cacheCreation: 3.75,
22
+ cacheRead: 0.30
23
+ },
24
+ 'claude-haiku-4-5-20250929': {
25
+ input: 1,
26
+ output: 5,
27
+ cacheCreation: 1.25,
28
+ cacheRead: 0.10
29
+ },
30
+
31
+ // Claude 4 (previous generation)
32
+ 'claude-opus-4-20250514': {
33
+ input: 5,
34
+ output: 25,
35
+ cacheCreation: 6.25,
36
+ cacheRead: 0.50
37
+ },
38
+ 'claude-sonnet-4-20250514': {
39
+ input: 3,
40
+ output: 15,
41
+ cacheCreation: 3.75,
42
+ cacheRead: 0.30
43
+ },
44
+
45
+ // Claude 3.5
46
+ 'claude-haiku-3-5-20241022': {
47
+ input: 1,
48
+ output: 5,
49
+ cacheCreation: 1.25,
50
+ cacheRead: 0.10
51
+ },
52
+ 'claude-3-5-haiku-20241022': {
53
+ input: 1,
54
+ output: 5,
55
+ cacheCreation: 1.25,
56
+ cacheRead: 0.10
57
+ },
58
+ 'claude-sonnet-3-5-20241022': {
59
+ input: 3,
60
+ output: 15,
61
+ cacheCreation: 3.75,
62
+ cacheRead: 0.30
63
+ },
64
+ 'claude-sonnet-3-5-20240620': {
65
+ input: 3,
66
+ output: 15,
67
+ cacheCreation: 3.75,
68
+ cacheRead: 0.30
69
+ },
70
+
71
+ // Claude 3 (legacy)
72
+ 'claude-opus-3-20240229': {
73
+ input: 15,
74
+ output: 75,
75
+ cacheCreation: 18.75,
76
+ cacheRead: 1.50
77
+ },
78
+ 'claude-3-opus-20240229': {
79
+ input: 15,
80
+ output: 75,
81
+ cacheCreation: 18.75,
82
+ cacheRead: 1.50
83
+ }
84
+ };
85
+
86
+ /**
87
+ * Model name aliases for normalization
88
+ * Maps short names to full model identifiers
89
+ */
90
+ const CLAUDE_MODEL_ALIASES = {
91
+ 'claude-opus-4-5': 'claude-opus-4-5-20250929',
92
+ 'claude-sonnet-4-5': 'claude-sonnet-4-5-20250929',
93
+ 'claude-haiku-4-5': 'claude-haiku-4-5-20250929',
94
+ 'claude-opus-4': 'claude-opus-4-20250514',
95
+ 'claude-sonnet-4': 'claude-sonnet-4-20250514',
96
+ 'claude-haiku-3-5': 'claude-haiku-3-5-20241022',
97
+ 'claude-sonnet-3-5': 'claude-sonnet-3-5-20241022',
98
+ 'claude-opus-3': 'claude-opus-3-20240229'
99
+ };
100
+
101
+ module.exports = {
102
+ CLAUDE_MODEL_PRICING,
103
+ CLAUDE_MODEL_ALIASES,
104
+ PRICING_LAST_UPDATED: '2026-02-02'
105
+ };
@@ -278,13 +278,14 @@ router.put('/:id/last-used', (req, res) => {
278
278
  * sourcePath: string,
279
279
  * name?: string,
280
280
  * createWorktree?: boolean,
281
- * branch?: string
281
+ * branch?: string,
282
+ * baseBranch?: string
282
283
  * }
283
284
  */
284
285
  router.post('/:id/projects', (req, res) => {
285
286
  try {
286
287
  const { id } = req.params;
287
- const { sourcePath, name, createWorktree, branch } = req.body;
288
+ const { sourcePath, name, createWorktree, branch, baseBranch } = req.body;
288
289
 
289
290
  if (!sourcePath || !sourcePath.trim()) {
290
291
  return res.status(400).json({
@@ -304,7 +305,8 @@ router.post('/:id/projects', (req, res) => {
304
305
  sourcePath,
305
306
  name,
306
307
  createWorktree,
307
- branch
308
+ branch,
309
+ baseBranch
308
310
  });
309
311
 
310
312
  res.json({
@@ -11,6 +11,7 @@ const { resolvePricing } = require('./utils/pricing');
11
11
  const { recordRequest: recordCodexRequest } = require('./services/codex-statistics-service');
12
12
  const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('./services/proxy-runtime');
13
13
  const { getEnabledChannels, writeCodexConfigForMultiChannel } = require('./services/codex-channels');
14
+ const { CLAUDE_MODEL_PRICING } = require('../config/model-pricing');
14
15
 
15
16
  let proxyServer = null;
16
17
  let proxyApp = null;
@@ -20,6 +21,7 @@ let currentPort = null;
20
21
  const requestMetadata = new Map();
21
22
 
22
23
  // OpenAI 模型定价(每百万 tokens 的价格,单位:美元)
24
+ // Claude 模型使用 config/model-pricing.js 中的集中定价
23
25
  const PRICING = {
24
26
  'gpt-4o': { input: 2.5, output: 10 },
25
27
  'gpt-4o-2024-11-20': { input: 2.5, output: 10 },
@@ -32,13 +34,7 @@ const PRICING = {
32
34
  'o1-pro': { input: 150, output: 600 },
33
35
  'o3': { input: 10, output: 40 },
34
36
  'o3-mini': { input: 1.1, output: 4.4 },
35
- 'o4-mini': { input: 1.1, output: 4.4 },
36
- // Claude 模型(通过 OpenAI 格式访问)
37
- 'claude-sonnet-4-5-20250929': { input: 3, output: 15 },
38
- 'claude-sonnet-4-20250514': { input: 3, output: 15 },
39
- 'claude-opus-4-20250514': { input: 15, output: 75 },
40
- 'claude-3-5-sonnet-20241022': { input: 3, output: 15 },
41
- 'claude-3-5-haiku-20241022': { input: 0.8, output: 4 }
37
+ 'o4-mini': { input: 1.1, output: 4.4 }
42
38
  };
43
39
 
44
40
  const CODEX_BASE_PRICING = DEFAULT_CONFIG.pricing.codex;
@@ -84,35 +80,56 @@ function resolveCodexTarget(baseUrl = '', requestPath = '') {
84
80
  * 计算请求成本
85
81
  */
86
82
  function calculateCost(model, tokens) {
87
- // 尝试精确匹配
88
- let pricing = PRICING[model];
89
-
90
- // 如果没有精确匹配,尝试模糊匹配
91
- if (!pricing) {
92
- const modelLower = model.toLowerCase();
93
- if (modelLower.includes('gpt-4o-mini')) {
94
- pricing = PRICING['gpt-4o-mini'];
95
- } else if (modelLower.includes('gpt-4o')) {
96
- pricing = PRICING['gpt-4o'];
97
- } else if (modelLower.includes('gpt-4')) {
98
- pricing = PRICING['gpt-4'];
99
- } else if (modelLower.includes('gpt-3.5')) {
100
- pricing = PRICING['gpt-3.5-turbo'];
101
- } else if (modelLower.includes('o1-mini')) {
102
- pricing = PRICING['o1-mini'];
103
- } else if (modelLower.includes('o1-pro')) {
104
- pricing = PRICING['o1-pro'];
105
- } else if (modelLower.includes('o1')) {
106
- pricing = PRICING['o1'];
107
- } else if (modelLower.includes('o3-mini')) {
108
- pricing = PRICING['o3-mini'];
109
- } else if (modelLower.includes('o3')) {
110
- pricing = PRICING['o3'];
111
- } else if (modelLower.includes('o4-mini')) {
112
- pricing = PRICING['o4-mini'];
113
- } else if (modelLower.includes('claude')) {
114
- // Claude 模型默认使用 Sonnet 定价
115
- pricing = PRICING['claude-sonnet-4-5-20250929'];
83
+ let pricing;
84
+
85
+ // 首先检查是否是 Claude 模型,使用集中定价
86
+ if (model.startsWith('claude-') || model.toLowerCase().includes('claude')) {
87
+ pricing = CLAUDE_MODEL_PRICING[model];
88
+
89
+ // 如果没有精确匹配,尝试模糊匹配 Claude 模型
90
+ if (!pricing) {
91
+ const modelLower = model.toLowerCase();
92
+ // 查找最接近的 Claude 模型
93
+ for (const [key, value] of Object.entries(CLAUDE_MODEL_PRICING)) {
94
+ if (key.toLowerCase().includes(modelLower) || modelLower.includes(key.toLowerCase())) {
95
+ pricing = value;
96
+ break;
97
+ }
98
+ }
99
+ }
100
+
101
+ // 如果仍然没有找到,使用默认 Sonnet 定价
102
+ if (!pricing) {
103
+ pricing = CLAUDE_MODEL_PRICING['claude-sonnet-4-5-20250929'];
104
+ }
105
+ } else {
106
+ // Claude 模型,使用 PRICING 对象(OpenAI 等)
107
+ pricing = PRICING[model];
108
+
109
+ // 如果没有精确匹配,尝试模糊匹配
110
+ if (!pricing) {
111
+ const modelLower = model.toLowerCase();
112
+ if (modelLower.includes('gpt-4o-mini')) {
113
+ pricing = PRICING['gpt-4o-mini'];
114
+ } else if (modelLower.includes('gpt-4o')) {
115
+ pricing = PRICING['gpt-4o'];
116
+ } else if (modelLower.includes('gpt-4')) {
117
+ pricing = PRICING['gpt-4'];
118
+ } else if (modelLower.includes('gpt-3.5')) {
119
+ pricing = PRICING['gpt-3.5-turbo'];
120
+ } else if (modelLower.includes('o1-mini')) {
121
+ pricing = PRICING['o1-mini'];
122
+ } else if (modelLower.includes('o1-pro')) {
123
+ pricing = PRICING['o1-pro'];
124
+ } else if (modelLower.includes('o1')) {
125
+ pricing = PRICING['o1'];
126
+ } else if (modelLower.includes('o3-mini')) {
127
+ pricing = PRICING['o3-mini'];
128
+ } else if (modelLower.includes('o3')) {
129
+ pricing = PRICING['o3'];
130
+ } else if (modelLower.includes('o4-mini')) {
131
+ pricing = PRICING['o4-mini'];
132
+ }
116
133
  }
117
134
  }
118
135
 
@@ -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,18 +21,6 @@ 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
 
@@ -42,7 +31,7 @@ const ONE_MILLION = 1000000;
42
31
  * @returns {number} 成本(美元)
43
32
  */
44
33
  function calculateCost(model, tokens) {
45
- const hardcodedPricing = PRICING[model] || {};
34
+ const hardcodedPricing = CLAUDE_MODEL_PRICING[model] || {};
46
35
  const pricing = resolveModelPricing('claude', model, hardcodedPricing, CLAUDE_BASE_PRICING);
47
36
 
48
37
  const inputRate = typeof pricing.input === 'number' ? pricing.input : CLAUDE_BASE_PRICING.input;
@@ -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']
@@ -530,14 +532,53 @@ async function fetchModelsFromProvider(channel, channelType) {
530
532
  });
531
533
  }
532
534
  } else if (res.statusCode === 401 || res.statusCode === 403) {
533
- console.error(`[ModelDetector] Authentication failed for ${channel.name}: ${res.statusCode}`);
534
- resolve({
535
- models: [],
536
- supported: true,
537
- cached: false,
538
- fallbackUsed: true,
539
- error: `Authentication failed: ${res.statusCode}`
540
- });
535
+ // Check if it's a Cloudflare protection issue
536
+ const bodyLower = data.toLowerCase();
537
+ const isCloudflare = bodyLower.includes('cloudflare') || bodyLower.includes('challenge') || bodyLower.includes('cf-ray');
538
+
539
+ let errorMessage;
540
+ let errorHint;
541
+
542
+ if (isCloudflare) {
543
+ errorMessage = 'Cloudflare 防护拦截,已使用默认模型';
544
+ errorHint = '该 API 端点受 Cloudflare 保护,已自动使用默认模型 claude-sonnet-4-5';
545
+ console.warn(`[ModelDetector] Cloudflare protection detected for ${channel.name}, using default model`);
546
+ resolve({
547
+ models: ['claude-sonnet-4-5'],
548
+ supported: true,
549
+ cached: false,
550
+ fallbackUsed: true,
551
+ error: errorMessage,
552
+ errorHint: errorHint,
553
+ statusCode: res.statusCode
554
+ });
555
+ } else if (res.statusCode === 401) {
556
+ errorMessage = 'API 密钥认证失败';
557
+ errorHint = '请检查 API 密钥是否正确配置';
558
+ console.error(`[ModelDetector] Authentication failed for ${channel.name}: ${res.statusCode} - ${errorMessage}`);
559
+ resolve({
560
+ models: [],
561
+ supported: true,
562
+ cached: false,
563
+ fallbackUsed: true,
564
+ error: errorMessage,
565
+ errorHint: errorHint,
566
+ statusCode: res.statusCode
567
+ });
568
+ } else {
569
+ errorMessage = '访问被拒绝';
570
+ errorHint = '请检查 API 密钥权限或联系服务提供商';
571
+ console.error(`[ModelDetector] Access denied for ${channel.name}: ${res.statusCode} - ${errorMessage}`);
572
+ resolve({
573
+ models: [],
574
+ supported: true,
575
+ cached: false,
576
+ fallbackUsed: true,
577
+ error: errorMessage,
578
+ errorHint: errorHint,
579
+ statusCode: res.statusCode
580
+ });
581
+ }
541
582
  } else if (res.statusCode === 404) {
542
583
  console.warn(`[ModelDetector] Model list endpoint not found for ${channel.name}`);
543
584
  resolve({
@@ -545,7 +586,9 @@ async function fetchModelsFromProvider(channel, channelType) {
545
586
  supported: false,
546
587
  cached: false,
547
588
  fallbackUsed: true,
548
- error: 'Endpoint not found (404)'
589
+ error: '模型列表端点不存在',
590
+ errorHint: '该 API 可能不支持 /v1/models 接口,请手动输入模型名称',
591
+ statusCode: 404
549
592
  });
550
593
  } else if (res.statusCode === 429) {
551
594
  console.warn(`[ModelDetector] Rate limited for ${channel.name}`);
@@ -554,7 +597,9 @@ async function fetchModelsFromProvider(channel, channelType) {
554
597
  supported: true,
555
598
  cached: false,
556
599
  fallbackUsed: true,
557
- error: 'Rate limited (429)'
600
+ error: '请求频率限制',
601
+ errorHint: '请稍后再试或联系服务提供商提高限额',
602
+ statusCode: 429
558
603
  });
559
604
  } else {
560
605
  console.error(`[ModelDetector] Unexpected status ${res.statusCode} for ${channel.name}`);
@@ -563,7 +608,9 @@ async function fetchModelsFromProvider(channel, channelType) {
563
608
  supported: true,
564
609
  cached: false,
565
610
  fallbackUsed: true,
566
- error: `HTTP ${res.statusCode}`
611
+ error: `HTTP 错误 ${res.statusCode}`,
612
+ errorHint: '请检查 API 端点配置或联系服务提供商',
613
+ statusCode: res.statusCode
567
614
  });
568
615
  }
569
616
  });
@@ -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);