coding-tool-x 3.4.0 → 3.4.2
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/dist/web/assets/{Analytics-DEjfL5Jx.js → Analytics-CbGxotgz.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-DkRL_-tf.js → ConfigTemplates-oP6nrFEb.js} +1 -1
- package/dist/web/assets/{Home-CF-L640I.js → Home-DMntmEvh.js} +1 -1
- package/dist/web/assets/{PluginManager-BzNYTdNB.js → PluginManager-BUC_c7nH.js} +1 -1
- package/dist/web/assets/{ProjectList-C0-JgHMM.js → ProjectList-CW8J49n7.js} +1 -1
- package/dist/web/assets/{SessionList-CkZUdX5N.js → SessionList-7lYnF92v.js} +1 -1
- package/dist/web/assets/{SkillManager-Cak0-4d4.js → SkillManager-Cs08216i.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-CGDJzwEr.js → WorkspaceManager-CY-oGtyB.js} +1 -1
- package/dist/web/assets/{index-Dz7v9OM0.css → index-5qy5NMIP.css} +1 -1
- package/dist/web/assets/index-ClCqKpvX.js +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +6 -2
- package/src/commands/doctor.js +2 -2
- package/src/commands/resume.js +1 -0
- package/src/commands/update.js +2 -1
- package/src/plugins/plugin-installer.js +1 -0
- package/src/server/api/claude-hooks.js +2 -3
- package/src/server/api/workspaces.js +2 -1
- package/src/server/codex-proxy-server.js +4 -92
- package/src/server/gemini-proxy-server.js +5 -28
- package/src/server/opencode-proxy-server.js +3 -93
- package/src/server/proxy-server.js +2 -57
- package/src/server/services/base/base-channel-service.js +247 -0
- package/src/server/services/base/proxy-utils.js +152 -0
- package/src/server/services/channel-health.js +30 -19
- package/src/server/services/channels.js +125 -293
- package/src/server/services/codex-channels.js +148 -513
- package/src/server/services/codex-env-manager.js +81 -21
- package/src/server/services/codex-settings-manager.js +20 -5
- package/src/server/services/gemini-channels.js +2 -7
- package/src/server/services/mcp-client.js +2 -1
- package/src/server/services/notification-hooks.js +9 -8
- package/src/server/services/oauth-credentials-service.js +12 -2
- package/src/server/services/opencode-channels.js +7 -9
- package/src/server/services/opencode-sessions.js +4 -2
- package/src/server/services/plugins-service.js +2 -1
- package/src/server/services/repo-scanner-base.js +1 -0
- package/src/server/services/skill-service.js +4 -2
- package/src/server/services/workspace-service.js +1 -0
- package/src/utils/port-helper.js +5 -5
- package/dist/web/assets/index-D_WItvHE.js +0 -2
package/dist/web/index.html
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
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-ClCqKpvX.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/markdown-DyTJGI4N.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vue-vendor-3bf-fPGP.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/vendors-CKPV1OAU.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/naive-ui-Bdxp09n2.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/icons-B5Pl4lrD.js">
|
|
14
14
|
<link rel="stylesheet" crossorigin href="/assets/markdown-BfC0goYb.css">
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-5qy5NMIP.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="app"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coding-tool-x",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.2",
|
|
4
4
|
"description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
"test:codex-agents": "node scripts/test-codex-agents.js",
|
|
15
15
|
"test:skills": "node scripts/test-skill-providers.js",
|
|
16
16
|
"test:plugins-market": "node scripts/test-plugin-market-cache.js",
|
|
17
|
+
"test:unit": "vitest run",
|
|
18
|
+
"test:unit:watch": "vitest",
|
|
17
19
|
"test:windows": "node scripts/test-windows-regression.js",
|
|
18
20
|
"benchmark:codex": "node scripts/benchmark-codex-loading.js",
|
|
19
21
|
"build:web": "cd src/web && npm run build",
|
|
@@ -77,7 +79,9 @@
|
|
|
77
79
|
"systeminformation": "^5.31.4"
|
|
78
80
|
},
|
|
79
81
|
"devDependencies": {
|
|
80
|
-
"
|
|
82
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
83
|
+
"nodemon": "^3.0.2",
|
|
84
|
+
"vitest": "^4.1.0"
|
|
81
85
|
},
|
|
82
86
|
"repository": {
|
|
83
87
|
"type": "git",
|
package/src/commands/doctor.js
CHANGED
|
@@ -262,7 +262,7 @@ async function checkProcessStatus() {
|
|
|
262
262
|
|
|
263
263
|
// 检查是否有 PM2 进程
|
|
264
264
|
try {
|
|
265
|
-
const { stdout } = await execAsync('pm2 list');
|
|
265
|
+
const { stdout } = await execAsync('pm2 list', { windowsHide: true });
|
|
266
266
|
if (stdout.includes('cc-tool')) {
|
|
267
267
|
return {
|
|
268
268
|
name: '进程状态',
|
|
@@ -295,7 +295,7 @@ async function checkProcessStatus() {
|
|
|
295
295
|
*/
|
|
296
296
|
async function checkDiskSpace() {
|
|
297
297
|
try {
|
|
298
|
-
const { stdout } = await execAsync('df -h ~');
|
|
298
|
+
const { stdout } = await execAsync('df -h ~', { windowsHide: true });
|
|
299
299
|
const lines = stdout.trim().split('\n');
|
|
300
300
|
if (lines.length > 1) {
|
|
301
301
|
const parts = lines[1].split(/\s+/);
|
package/src/commands/resume.js
CHANGED
package/src/commands/update.js
CHANGED
|
@@ -26,7 +26,8 @@ function runNpmInstall(packageName, version) {
|
|
|
26
26
|
return new Promise((resolve, reject) => {
|
|
27
27
|
const npmCommand = resolveNpmCommand();
|
|
28
28
|
const child = spawn(npmCommand, ['install', '-g', `${packageName}@${version}`], {
|
|
29
|
-
stdio: 'inherit'
|
|
29
|
+
stdio: 'inherit',
|
|
30
|
+
windowsHide: true
|
|
30
31
|
});
|
|
31
32
|
|
|
32
33
|
child.on('error', (err) => {
|
|
@@ -132,7 +132,7 @@ const timestamp = new Date().toLocaleString('zh-CN');
|
|
|
132
132
|
const cmd = generateSystemNotificationCommand(systemNotification.type);
|
|
133
133
|
script += `// 系统通知
|
|
134
134
|
try {
|
|
135
|
-
execSync(${JSON.stringify(cmd)}, { stdio: 'ignore' });
|
|
135
|
+
execSync(${JSON.stringify(cmd)}, { stdio: 'ignore', windowsHide: true });
|
|
136
136
|
} catch (e) {
|
|
137
137
|
console.error('系统通知失败:', e.message);
|
|
138
138
|
}
|
|
@@ -542,8 +542,7 @@ router.post('/test', (req, res) => {
|
|
|
542
542
|
// 测试系统通知
|
|
543
543
|
const command = generateSystemNotificationCommand(type || 'notification');
|
|
544
544
|
const { execSync } = require('child_process');
|
|
545
|
-
execSync(command, { stdio: 'ignore' });
|
|
546
|
-
res.json({ success: true, message: '系统测试通知已发送' });
|
|
545
|
+
execSync(command, { stdio: 'ignore', windowsHide: true });
|
|
547
546
|
}
|
|
548
547
|
} catch (error) {
|
|
549
548
|
console.error('Error testing notification:', error);
|
|
@@ -14,6 +14,7 @@ const { createDecodedStream } = require('./services/response-decoder');
|
|
|
14
14
|
const { getEffectiveApiKey } = require('./services/codex-channels');
|
|
15
15
|
const { persistProxyRequestSnapshot } = require('./services/request-logger');
|
|
16
16
|
const { publishUsageLog, publishFailureLog } = require('./services/proxy-log-helper');
|
|
17
|
+
const { redirectModel, resolveTargetUrl } = require('./services/base/proxy-utils');
|
|
17
18
|
|
|
18
19
|
let proxyServer = null;
|
|
19
20
|
let proxyApp = null;
|
|
@@ -46,99 +47,10 @@ const PRICING = {
|
|
|
46
47
|
const CODEX_BASE_PRICING = DEFAULT_CONFIG.pricing.codex;
|
|
47
48
|
const ONE_MILLION = 1000000;
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
* 检测模型层级
|
|
51
|
-
* @param {string} modelName - 模型名称
|
|
52
|
-
* @returns {string|null} 模型层级 (opus/sonnet/haiku) 或 null
|
|
53
|
-
*/
|
|
54
|
-
function detectModelTier(modelName) {
|
|
55
|
-
if (!modelName) return null;
|
|
56
|
-
const lower = modelName.toLowerCase();
|
|
57
|
-
if (lower.includes('opus')) return 'opus';
|
|
58
|
-
if (lower.includes('sonnet')) return 'sonnet';
|
|
59
|
-
if (lower.includes('haiku')) return 'haiku';
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* 应用模型重定向
|
|
65
|
-
* @param {string} originalModel - 原始模型名称
|
|
66
|
-
* @param {object} channel - 渠道对象,包含 modelConfig 和 modelRedirects
|
|
67
|
-
* @returns {string} 重定向后的模型名称
|
|
68
|
-
*/
|
|
69
|
-
function redirectModel(originalModel, channel) {
|
|
70
|
-
if (!originalModel) return originalModel;
|
|
71
|
-
|
|
72
|
-
// 优先使用新的 modelRedirects 数组格式
|
|
73
|
-
const modelRedirects = channel?.modelRedirects;
|
|
74
|
-
if (Array.isArray(modelRedirects) && modelRedirects.length > 0) {
|
|
75
|
-
for (const rule of modelRedirects) {
|
|
76
|
-
if (rule.from && rule.to && rule.from === originalModel) {
|
|
77
|
-
return rule.to;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// 向后兼容:使用旧的 modelConfig 格式
|
|
83
|
-
const modelConfig = channel?.modelConfig;
|
|
84
|
-
if (!modelConfig) return originalModel;
|
|
85
|
-
|
|
86
|
-
const tier = detectModelTier(originalModel);
|
|
50
|
+
// detectModelTier, redirectModel, resolveTargetUrl imported from services/base/proxy-utils
|
|
87
51
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return modelConfig.opusModel;
|
|
91
|
-
}
|
|
92
|
-
if (tier === 'sonnet' && modelConfig.sonnetModel) {
|
|
93
|
-
return modelConfig.sonnetModel;
|
|
94
|
-
}
|
|
95
|
-
if (tier === 'haiku' && modelConfig.haikuModel) {
|
|
96
|
-
return modelConfig.haikuModel;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// 回退到通用模型覆盖
|
|
100
|
-
if (modelConfig.model) {
|
|
101
|
-
return modelConfig.model;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return originalModel;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* 解析 Codex 代理目标 URL
|
|
109
|
-
*
|
|
110
|
-
* Codex CLI 发送请求到我们的代理时,请求路径格式:
|
|
111
|
-
* - /v1/responses (OpenAI Responses API)
|
|
112
|
-
* - /v1/chat/completions (OpenAI Chat Completions API)
|
|
113
|
-
*
|
|
114
|
-
* 渠道配置的 base_url 可能是:
|
|
115
|
-
* - https://api.openai.com/v1
|
|
116
|
-
* - https://example.com/openai/v1
|
|
117
|
-
* - https://example.com
|
|
118
|
-
*
|
|
119
|
-
* 最终转发目标示例:
|
|
120
|
-
* - base_url: https://example.com/openai/v1, path: /v1/responses
|
|
121
|
-
* -> target: https://example.com/openai, 最终: https://example.com/openai/v1/responses
|
|
122
|
-
*
|
|
123
|
-
* 这个函数返回要传给 http-proxy 的 target,http-proxy 会自动拼接 req.url
|
|
124
|
-
*/
|
|
125
|
-
function resolveCodexTarget(baseUrl = '', requestPath = '') {
|
|
126
|
-
let target = baseUrl || '';
|
|
127
|
-
|
|
128
|
-
// 移除末尾斜杠
|
|
129
|
-
if (target.endsWith('/')) {
|
|
130
|
-
target = target.slice(0, -1);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// 核心逻辑:避免 /v1/v1 重复
|
|
134
|
-
// 如果 base_url 以 /v1 结尾,且请求路径以 /v1 开头,去掉 base_url 的 /v1
|
|
135
|
-
// 因为 http-proxy 会将 requestPath 追加到 target 后面
|
|
136
|
-
if (target.endsWith('/v1') && requestPath.startsWith('/v1')) {
|
|
137
|
-
target = target.slice(0, -3);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return target;
|
|
141
|
-
}
|
|
52
|
+
// resolveCodexTarget replaced by resolveTargetUrl from proxy-utils
|
|
53
|
+
const resolveCodexTarget = resolveTargetUrl;
|
|
142
54
|
|
|
143
55
|
/**
|
|
144
56
|
* 计算请求成本
|
|
@@ -14,6 +14,7 @@ const { createDecodedStream } = require('./services/response-decoder');
|
|
|
14
14
|
const { getEffectiveApiKey } = require('./services/gemini-channels');
|
|
15
15
|
const { persistProxyRequestSnapshot } = require('./services/request-logger');
|
|
16
16
|
const { publishUsageLog, publishFailureLog } = require('./services/proxy-log-helper');
|
|
17
|
+
const { redirectModel: redirectModelBase, resolveTargetUrl } = require('./services/base/proxy-utils');
|
|
17
18
|
|
|
18
19
|
let proxyServer = null;
|
|
19
20
|
let proxyApp = null;
|
|
@@ -45,36 +46,12 @@ const PRICING = {
|
|
|
45
46
|
const GEMINI_BASE_PRICING = DEFAULT_CONFIG.pricing.gemini;
|
|
46
47
|
const ONE_MILLION = 1000000;
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (target.endsWith('/')) {
|
|
51
|
-
target = target.slice(0, -1);
|
|
52
|
-
}
|
|
53
|
-
if (target.endsWith('/v1') && requestPath.startsWith('/v1')) {
|
|
54
|
-
target = target.slice(0, -3);
|
|
55
|
-
}
|
|
56
|
-
return target;
|
|
57
|
-
}
|
|
49
|
+
// resolveGeminiTarget replaced by resolveTargetUrl from proxy-utils
|
|
50
|
+
const resolveGeminiTarget = resolveTargetUrl;
|
|
58
51
|
|
|
59
|
-
|
|
60
|
-
* 应用模型重定向(精确匹配)
|
|
61
|
-
* @param {string} originalModel - 原始模型名称
|
|
62
|
-
* @param {object} channel - 渠道对象,包含 modelRedirects 数组
|
|
63
|
-
* @returns {string} 重定向后的模型名称
|
|
64
|
-
*/
|
|
52
|
+
// Gemini uses exact-match only redirect (no tier fallback)
|
|
65
53
|
function redirectModel(originalModel, channel) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const modelRedirects = channel?.modelRedirects;
|
|
69
|
-
if (Array.isArray(modelRedirects) && modelRedirects.length > 0) {
|
|
70
|
-
for (const rule of modelRedirects) {
|
|
71
|
-
if (rule.from && rule.to && rule.from === originalModel) {
|
|
72
|
-
return rule.to;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return originalModel;
|
|
54
|
+
return redirectModelBase(originalModel, channel, { useTierFallback: false });
|
|
78
55
|
}
|
|
79
56
|
|
|
80
57
|
/**
|
|
@@ -21,6 +21,7 @@ const { getEnabledChannels, getEffectiveApiKey } = require('./services/opencode-
|
|
|
21
21
|
const { persistProxyRequestSnapshot, loadClaudeRequestTemplate } = require('./services/request-logger');
|
|
22
22
|
const { probeModelAvailability, fetchModelsFromProvider } = require('./services/model-detector');
|
|
23
23
|
const { publishUsageLog, publishFailureLog } = require('./services/proxy-log-helper');
|
|
24
|
+
const { redirectModel, resolveTargetUrl } = require('./services/base/proxy-utils');
|
|
24
25
|
|
|
25
26
|
let proxyServer = null;
|
|
26
27
|
let proxyApp = null;
|
|
@@ -92,99 +93,8 @@ let cachedClaudeUserId = '';
|
|
|
92
93
|
let cachedClaudeRequestTemplate = null;
|
|
93
94
|
let cachedClaudeRequestTemplateAt = 0;
|
|
94
95
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
* @param {string} modelName - 模型名称
|
|
98
|
-
* @returns {string|null} 模型层级 (opus/sonnet/haiku) 或 null
|
|
99
|
-
*/
|
|
100
|
-
function detectModelTier(modelName) {
|
|
101
|
-
if (!modelName) return null;
|
|
102
|
-
const lower = modelName.toLowerCase();
|
|
103
|
-
if (lower.includes('opus')) return 'opus';
|
|
104
|
-
if (lower.includes('sonnet')) return 'sonnet';
|
|
105
|
-
if (lower.includes('haiku')) return 'haiku';
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* 应用模型重定向
|
|
111
|
-
* @param {string} originalModel - 原始模型名称
|
|
112
|
-
* @param {object} channel - 渠道对象,包含 modelConfig 和 modelRedirects
|
|
113
|
-
* @returns {string} 重定向后的模型名称
|
|
114
|
-
*/
|
|
115
|
-
function redirectModel(originalModel, channel) {
|
|
116
|
-
if (!originalModel) return originalModel;
|
|
117
|
-
|
|
118
|
-
// 优先使用新的 modelRedirects 数组格式
|
|
119
|
-
const modelRedirects = channel?.modelRedirects;
|
|
120
|
-
if (Array.isArray(modelRedirects) && modelRedirects.length > 0) {
|
|
121
|
-
for (const rule of modelRedirects) {
|
|
122
|
-
if (rule.from && rule.to && rule.from === originalModel) {
|
|
123
|
-
return rule.to;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// 向后兼容:使用旧的 modelConfig 格式
|
|
129
|
-
const modelConfig = channel?.modelConfig;
|
|
130
|
-
if (!modelConfig) return originalModel;
|
|
131
|
-
|
|
132
|
-
const tier = detectModelTier(originalModel);
|
|
133
|
-
|
|
134
|
-
// 优先级:层级特定配置 > 通用模型覆盖
|
|
135
|
-
if (tier === 'opus' && modelConfig.opusModel) {
|
|
136
|
-
return modelConfig.opusModel;
|
|
137
|
-
}
|
|
138
|
-
if (tier === 'sonnet' && modelConfig.sonnetModel) {
|
|
139
|
-
return modelConfig.sonnetModel;
|
|
140
|
-
}
|
|
141
|
-
if (tier === 'haiku' && modelConfig.haikuModel) {
|
|
142
|
-
return modelConfig.haikuModel;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// 回退到通用模型覆盖
|
|
146
|
-
if (modelConfig.model) {
|
|
147
|
-
return modelConfig.model;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return originalModel;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* 解析 OpenCode 代理目标 URL
|
|
155
|
-
*
|
|
156
|
-
* OpenCode CLI 发送请求到我们的代理时,请求路径格式:
|
|
157
|
-
* - /v1/responses (OpenAI Responses API)
|
|
158
|
-
* - /v1/chat/completions (OpenAI Chat Completions API)
|
|
159
|
-
*
|
|
160
|
-
* 渠道配置的 base_url 可能是:
|
|
161
|
-
* - https://api.openai.com/v1
|
|
162
|
-
* - https://example.com/openai/v1
|
|
163
|
-
* - https://example.com
|
|
164
|
-
*
|
|
165
|
-
* 最终转发目标示例:
|
|
166
|
-
* - base_url: https://example.com/openai/v1, path: /v1/responses
|
|
167
|
-
* -> target: https://example.com/openai, 最终: https://example.com/openai/v1/responses
|
|
168
|
-
*
|
|
169
|
-
* 这个函数返回要传给 http-proxy 的 target,http-proxy 会自动拼接 req.url
|
|
170
|
-
*/
|
|
171
|
-
function resolveOpenCodeTarget(baseUrl = '', requestPath = '') {
|
|
172
|
-
let target = baseUrl || '';
|
|
173
|
-
|
|
174
|
-
// 移除末尾斜杠
|
|
175
|
-
if (target.endsWith('/')) {
|
|
176
|
-
target = target.slice(0, -1);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// 核心逻辑:避免 /v1/v1 重复
|
|
180
|
-
// 如果 base_url 以 /v1 结尾,且请求路径以 /v1 开头,去掉 base_url 的 /v1
|
|
181
|
-
// 因为 http-proxy 会将 requestPath 追加到 target 后面
|
|
182
|
-
if (target.endsWith('/v1') && requestPath.startsWith('/v1')) {
|
|
183
|
-
target = target.slice(0, -3);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return target;
|
|
187
|
-
}
|
|
96
|
+
// detectModelTier, redirectModel, resolveTargetUrl imported from services/base/proxy-utils
|
|
97
|
+
const resolveOpenCodeTarget = resolveTargetUrl;
|
|
188
98
|
|
|
189
99
|
/**
|
|
190
100
|
* 计算请求成本
|
|
@@ -19,6 +19,7 @@ const eventBus = require('../plugins/event-bus');
|
|
|
19
19
|
const { getEffectiveApiKey } = require('./services/channels');
|
|
20
20
|
const { persistProxyRequestSnapshot, persistClaudeRequestTemplate } = require('./services/request-logger');
|
|
21
21
|
const { publishUsageLog, publishFailureLog } = require('./services/proxy-log-helper');
|
|
22
|
+
const { redirectModel } = require('./services/base/proxy-utils');
|
|
22
23
|
|
|
23
24
|
let proxyServer = null;
|
|
24
25
|
let proxyApp = null;
|
|
@@ -34,63 +35,7 @@ const printedRedirectCache = new Map();
|
|
|
34
35
|
const CLAUDE_BASE_PRICING = DEFAULT_CONFIG.pricing.claude;
|
|
35
36
|
const ONE_MILLION = 1000000;
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
* 检测模型层级
|
|
39
|
-
* @param {string} modelName - 模型名称
|
|
40
|
-
* @returns {string|null} 模型层级 (opus/sonnet/haiku) 或 null
|
|
41
|
-
*/
|
|
42
|
-
function detectModelTier(modelName) {
|
|
43
|
-
if (!modelName) return null;
|
|
44
|
-
const lower = modelName.toLowerCase();
|
|
45
|
-
if (lower.includes('opus')) return 'opus';
|
|
46
|
-
if (lower.includes('sonnet')) return 'sonnet';
|
|
47
|
-
if (lower.includes('haiku')) return 'haiku';
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* 应用模型重定向
|
|
53
|
-
* @param {string} originalModel - 原始模型名称
|
|
54
|
-
* @param {object} channel - 渠道对象,包含 modelConfig 和 modelRedirects
|
|
55
|
-
* @returns {string} 重定向后的模型名称
|
|
56
|
-
*/
|
|
57
|
-
function redirectModel(originalModel, channel) {
|
|
58
|
-
if (!originalModel) return originalModel;
|
|
59
|
-
|
|
60
|
-
// 优先使用新的 modelRedirects 数组格式
|
|
61
|
-
const modelRedirects = channel?.modelRedirects;
|
|
62
|
-
if (Array.isArray(modelRedirects) && modelRedirects.length > 0) {
|
|
63
|
-
for (const rule of modelRedirects) {
|
|
64
|
-
if (rule.from && rule.to && rule.from === originalModel) {
|
|
65
|
-
return rule.to;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// 向后兼容:使用旧的 modelConfig 格式
|
|
71
|
-
const modelConfig = channel?.modelConfig;
|
|
72
|
-
if (!modelConfig) return originalModel;
|
|
73
|
-
|
|
74
|
-
const tier = detectModelTier(originalModel);
|
|
75
|
-
|
|
76
|
-
// 优先级:层级特定配置 > 通用模型覆盖
|
|
77
|
-
if (tier === 'opus' && modelConfig.opusModel) {
|
|
78
|
-
return modelConfig.opusModel;
|
|
79
|
-
}
|
|
80
|
-
if (tier === 'sonnet' && modelConfig.sonnetModel) {
|
|
81
|
-
return modelConfig.sonnetModel;
|
|
82
|
-
}
|
|
83
|
-
if (tier === 'haiku' && modelConfig.haikuModel) {
|
|
84
|
-
return modelConfig.haikuModel;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// 回退到通用模型覆盖
|
|
88
|
-
if (modelConfig.model) {
|
|
89
|
-
return modelConfig.model;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return originalModel;
|
|
93
|
-
}
|
|
38
|
+
// detectModelTier and redirectModel imported from services/base/proxy-utils
|
|
94
39
|
|
|
95
40
|
/**
|
|
96
41
|
* 计算请求成本
|