@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.
- package/CHANGELOG.md +15 -0
- package/dist/web/assets/index-Bpjcdalh.js +14 -0
- package/dist/web/assets/{index-TjhcaFRe.css → index-CB782_71.css} +2 -2
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/config/default.js +2 -9
- package/src/config/model-pricing.js +105 -0
- package/src/server/api/workspaces.js +5 -3
- package/src/server/codex-proxy-server.js +53 -36
- package/src/server/proxy-server.js +2 -13
- package/src/server/services/mcp-client.js +17 -2
- package/src/server/services/model-detector.js +62 -15
- package/src/server/services/workspace-service.js +126 -5
- package/src/server/utils/pricing.js +13 -3
- package/dist/web/assets/index-DfPKAt9R.js +0 -14
package/dist/web/index.html
CHANGED
|
@@ -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-
|
|
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-
|
|
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
package/src/config/default.js
CHANGED
|
@@ -24,15 +24,8 @@ const DEFAULT_CONFIG = {
|
|
|
24
24
|
cacheCreation: 3.75,
|
|
25
25
|
cacheRead: 0.30,
|
|
26
26
|
models: {
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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);
|