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.
Files changed (41) hide show
  1. package/dist/web/assets/{Analytics-DEjfL5Jx.js → Analytics-CbGxotgz.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-DkRL_-tf.js → ConfigTemplates-oP6nrFEb.js} +1 -1
  3. package/dist/web/assets/{Home-CF-L640I.js → Home-DMntmEvh.js} +1 -1
  4. package/dist/web/assets/{PluginManager-BzNYTdNB.js → PluginManager-BUC_c7nH.js} +1 -1
  5. package/dist/web/assets/{ProjectList-C0-JgHMM.js → ProjectList-CW8J49n7.js} +1 -1
  6. package/dist/web/assets/{SessionList-CkZUdX5N.js → SessionList-7lYnF92v.js} +1 -1
  7. package/dist/web/assets/{SkillManager-Cak0-4d4.js → SkillManager-Cs08216i.js} +1 -1
  8. package/dist/web/assets/{WorkspaceManager-CGDJzwEr.js → WorkspaceManager-CY-oGtyB.js} +1 -1
  9. package/dist/web/assets/{index-Dz7v9OM0.css → index-5qy5NMIP.css} +1 -1
  10. package/dist/web/assets/index-ClCqKpvX.js +2 -0
  11. package/dist/web/index.html +2 -2
  12. package/package.json +6 -2
  13. package/src/commands/doctor.js +2 -2
  14. package/src/commands/resume.js +1 -0
  15. package/src/commands/update.js +2 -1
  16. package/src/plugins/plugin-installer.js +1 -0
  17. package/src/server/api/claude-hooks.js +2 -3
  18. package/src/server/api/workspaces.js +2 -1
  19. package/src/server/codex-proxy-server.js +4 -92
  20. package/src/server/gemini-proxy-server.js +5 -28
  21. package/src/server/opencode-proxy-server.js +3 -93
  22. package/src/server/proxy-server.js +2 -57
  23. package/src/server/services/base/base-channel-service.js +247 -0
  24. package/src/server/services/base/proxy-utils.js +152 -0
  25. package/src/server/services/channel-health.js +30 -19
  26. package/src/server/services/channels.js +125 -293
  27. package/src/server/services/codex-channels.js +148 -513
  28. package/src/server/services/codex-env-manager.js +81 -21
  29. package/src/server/services/codex-settings-manager.js +20 -5
  30. package/src/server/services/gemini-channels.js +2 -7
  31. package/src/server/services/mcp-client.js +2 -1
  32. package/src/server/services/notification-hooks.js +9 -8
  33. package/src/server/services/oauth-credentials-service.js +12 -2
  34. package/src/server/services/opencode-channels.js +7 -9
  35. package/src/server/services/opencode-sessions.js +4 -2
  36. package/src/server/services/plugins-service.js +2 -1
  37. package/src/server/services/repo-scanner-base.js +1 -0
  38. package/src/server/services/skill-service.js +4 -2
  39. package/src/server/services/workspace-service.js +1 -0
  40. package/src/utils/port-helper.js +5 -5
  41. package/dist/web/assets/index-D_WItvHE.js +0 -2
@@ -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-D_WItvHE.js"></script>
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-Dz7v9OM0.css">
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.0",
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
- "nodemon": "^3.0.2"
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",
@@ -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+/);
@@ -106,6 +106,7 @@ async function resumeSession(config, sessionId, fork = false) {
106
106
  // 此时 ct 进程只是等待,不处理任何输入
107
107
  execSync(command, {
108
108
  stdio: 'inherit', // 完全继承 stdio,让 claude 控制终端
109
+ windowsHide: true,
109
110
  });
110
111
 
111
112
  // 恢复目录
@@ -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) => {
@@ -66,6 +66,7 @@ function execCommand(command, options = {}) {
66
66
  const output = execSync(command, {
67
67
  encoding: 'utf8',
68
68
  stdio: ['pipe', 'pipe', 'pipe'],
69
+ windowsHide: true,
69
70
  ...options
70
71
  });
71
72
  return { success: true, output: output.trim() };
@@ -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);
@@ -24,7 +24,8 @@ function validateBranchName(branchName) {
24
24
 
25
25
  try {
26
26
  execFileSync('git', ['check-ref-format', '--branch', normalized], {
27
- stdio: 'ignore'
27
+ stdio: 'ignore',
28
+ windowsHide: true
28
29
  });
29
30
  return { valid: true, normalized };
30
31
  } catch (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
- if (tier === 'opus' && modelConfig.opusModel) {
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
- function resolveGeminiTarget(baseUrl = '', requestPath = '') {
49
- let target = baseUrl || '';
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
- if (!originalModel) return originalModel;
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
  * 计算请求成本