coding-tool-x 3.2.2 → 3.3.1

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 (30) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/web/assets/{Analytics-COVBIlMT.js → Analytics-BskCbia_.js} +1 -1
  3. package/dist/web/assets/{ConfigTemplates-CwCbgetE.js → ConfigTemplates-B4X3rgfY.js} +1 -1
  4. package/dist/web/assets/{Home-CgMMTGxS.js → Home-DHYMMKOU.js} +1 -1
  5. package/dist/web/assets/{PluginManager-DQ4B002M.js → PluginManager-D_LoULGH.js} +1 -1
  6. package/dist/web/assets/{ProjectList-BT99XzrL.js → ProjectList-DiV4Qwa1.js} +1 -1
  7. package/dist/web/assets/{SessionList-ButOecT4.js → SessionList-B24o0wiX.js} +1 -1
  8. package/dist/web/assets/{SkillManager-e2C5kuhp.js → SkillManager-B9Rnuaig.js} +1 -1
  9. package/dist/web/assets/{WorkspaceManager-Dh5Rzjkr.js → WorkspaceManager-BkL2l5J9.js} +1 -1
  10. package/dist/web/assets/icons-B29onFfZ.js +1 -0
  11. package/dist/web/assets/index-C5j22icm.css +1 -0
  12. package/dist/web/assets/index-ZttxvTKw.js +2 -0
  13. package/dist/web/assets/{naive-ui-DlpKk-8M.js → naive-ui-CxpuzdjU.js} +1 -1
  14. package/dist/web/index.html +4 -4
  15. package/package.json +1 -1
  16. package/src/server/api/opencode-channels.js +30 -2
  17. package/src/server/opencode-proxy-server.js +16 -116
  18. package/src/server/proxy-server.js +2 -10
  19. package/src/server/services/channels.js +7 -5
  20. package/src/server/services/codex-channels.js +7 -5
  21. package/src/server/services/codex-settings-manager.js +13 -0
  22. package/src/server/services/config-templates-service.js +28 -22
  23. package/src/server/services/gemini-channels.js +7 -5
  24. package/src/server/services/mcp-service.js +22 -1
  25. package/src/server/services/request-logger.js +190 -0
  26. package/src/server/services/speed-test.js +17 -108
  27. package/src/utils/port-helper.js +26 -5
  28. package/dist/web/assets/icons-DRrXwWZi.js +0 -1
  29. package/dist/web/assets/index-CwGg4bbn.css +0 -1
  30. package/dist/web/assets/index-j56-PHWL.js +0 -2
@@ -122,9 +122,199 @@ function createApiRequestLogger() {
122
122
  };
123
123
  }
124
124
 
125
+ const CLAUDE_TEMPLATE_PATH = path.join(CC_TOOL_DIR, 'claude-request-template.json');
126
+ const CLAUDE_TEMPLATE_MIN_SYSTEM_CHARS = 100;
127
+
128
+ const FALLBACK_CLAUDE_SYSTEM = Object.freeze([
129
+ {
130
+ type: 'text',
131
+ text: 'x-anthropic-billing-header: cc_version=2.1.59; cc_entrypoint=cli; cch=00000;'
132
+ },
133
+ {
134
+ type: 'text',
135
+ text: "You are Claude Code, Anthropic's official CLI for Claude."
136
+ }
137
+ ]);
138
+
139
+ const FALLBACK_CLAUDE_TOOLS = Object.freeze([
140
+ {
141
+ name: 'Task',
142
+ description: 'Launch a new agent to handle complex, multi-step tasks autonomously.',
143
+ input_schema: {
144
+ type: 'object',
145
+ properties: {
146
+ description: { type: 'string' },
147
+ prompt: { type: 'string' },
148
+ subagent_type: { type: 'string' },
149
+ model: { type: 'string' },
150
+ resume: { type: 'string' },
151
+ run_in_background: { type: 'boolean' },
152
+ max_turns: { type: 'integer' },
153
+ isolation: { type: 'string' }
154
+ },
155
+ required: ['description', 'prompt', 'subagent_type']
156
+ }
157
+ },
158
+ {
159
+ name: 'Bash',
160
+ description: 'Executes a given bash command and returns its output.',
161
+ input_schema: {
162
+ type: 'object',
163
+ properties: {
164
+ command: { type: 'string' },
165
+ timeout: { type: 'number' },
166
+ description: { type: 'string' },
167
+ run_in_background: { type: 'boolean' },
168
+ dangerouslyDisableSandbox: { type: 'boolean' }
169
+ },
170
+ required: ['command']
171
+ }
172
+ },
173
+ {
174
+ name: 'Glob',
175
+ description: 'Fast file pattern matching tool that works with any codebase size.',
176
+ input_schema: {
177
+ type: 'object',
178
+ properties: {
179
+ pattern: { type: 'string' },
180
+ path: { type: 'string' }
181
+ },
182
+ required: ['pattern']
183
+ }
184
+ },
185
+ {
186
+ name: 'Grep',
187
+ description: 'A powerful search tool built on ripgrep.',
188
+ input_schema: {
189
+ type: 'object',
190
+ properties: {
191
+ pattern: { type: 'string' },
192
+ path: { type: 'string' },
193
+ glob: { type: 'string' },
194
+ output_mode: { type: 'string' },
195
+ '-B': { type: 'number' },
196
+ '-A': { type: 'number' },
197
+ '-C': { type: 'number' },
198
+ context: { type: 'number' },
199
+ '-n': { type: 'boolean' },
200
+ '-i': { type: 'boolean' },
201
+ type: { type: 'string' },
202
+ head_limit: { type: 'number' },
203
+ offset: { type: 'number' },
204
+ multiline: { type: 'boolean' }
205
+ },
206
+ required: ['pattern']
207
+ }
208
+ },
209
+ {
210
+ name: 'Read',
211
+ description: 'Reads a file from the local filesystem.',
212
+ input_schema: {
213
+ type: 'object',
214
+ properties: {
215
+ file_path: { type: 'string' },
216
+ offset: { type: 'number' },
217
+ limit: { type: 'number' },
218
+ pages: { type: 'string' }
219
+ },
220
+ required: ['file_path']
221
+ }
222
+ },
223
+ {
224
+ name: 'Edit',
225
+ description: 'Performs exact string replacements in files.',
226
+ input_schema: {
227
+ type: 'object',
228
+ properties: {
229
+ file_path: { type: 'string' },
230
+ old_string: { type: 'string' },
231
+ new_string: { type: 'string' },
232
+ replace_all: { type: 'boolean' }
233
+ },
234
+ required: ['file_path', 'old_string', 'new_string']
235
+ }
236
+ },
237
+ {
238
+ name: 'Write',
239
+ description: 'Writes a file to the local filesystem.',
240
+ input_schema: {
241
+ type: 'object',
242
+ properties: {
243
+ file_path: { type: 'string' },
244
+ content: { type: 'string' }
245
+ },
246
+ required: ['file_path', 'content']
247
+ }
248
+ },
249
+ {
250
+ name: 'ToolSearch',
251
+ description: 'Search for or select deferred tools to make them available for use.',
252
+ input_schema: {
253
+ type: 'object',
254
+ properties: {
255
+ query: { type: 'string' },
256
+ max_results: { type: 'number' }
257
+ },
258
+ required: ['query', 'max_results']
259
+ }
260
+ }
261
+ ]);
262
+
263
+ /**
264
+ * 从 Claude 请求 body 提取模板(system/tools/userId),覆盖写入单一文件。
265
+ * 无需环境变量开关,默认始终执行(文件极小,仅保留最新一次有效模板)。
266
+ * @param {object} body - 请求 body 对象
267
+ */
268
+ function persistClaudeRequestTemplate(body) {
269
+ if (!body || typeof body !== 'object') return;
270
+
271
+ const system = Array.isArray(body.system) ? body.system : [];
272
+ const tools = Array.isArray(body.tools) ? body.tools : [];
273
+ const userId = (body.metadata && typeof body.metadata.user_id === 'string')
274
+ ? body.metadata.user_id
275
+ : '';
276
+
277
+ // 必须有 tools 且 system 有一定内容才算有效模板
278
+ if (tools.length === 0) return;
279
+ const systemCharCount = system.reduce((s, b) => s + (typeof b?.text === 'string' ? b.text.length : 0), 0);
280
+ if (systemCharCount < CLAUDE_TEMPLATE_MIN_SYSTEM_CHARS) return;
281
+
282
+ try {
283
+ ensureDir(CC_TOOL_DIR);
284
+ const template = { updatedAt: Date.now(), userId, system, tools };
285
+ fs.writeFile(CLAUDE_TEMPLATE_PATH, JSON.stringify(template), (err) => {
286
+ if (err) console.error('[request-logger] Failed to write claude-request-template.json:', err);
287
+ });
288
+ } catch (err) {
289
+ console.error('[request-logger] Failed to write claude-request-template.json:', err);
290
+ }
291
+ }
292
+
293
+ /**
294
+ * 读取 Claude 请求模板(同步)。
295
+ * 若文件不存在或无效,返回内置 fallback(含 billing header 占位符和完整工具 schema)。
296
+ * @returns {{ userId: string, system: Array, tools: Array }}
297
+ */
298
+ function loadClaudeRequestTemplate() {
299
+ try {
300
+ if (fs.existsSync(CLAUDE_TEMPLATE_PATH)) {
301
+ const raw = fs.readFileSync(CLAUDE_TEMPLATE_PATH, 'utf8');
302
+ const parsed = JSON.parse(raw);
303
+ if (Array.isArray(parsed.tools) && parsed.tools.length > 0 && Array.isArray(parsed.system)) {
304
+ return { userId: parsed.userId || '', system: parsed.system, tools: parsed.tools };
305
+ }
306
+ }
307
+ } catch {
308
+ // fall through to fallback
309
+ }
310
+ return { userId: '', system: [...FALLBACK_CLAUDE_SYSTEM], tools: [...FALLBACK_CLAUDE_TOOLS] };
311
+ }
312
+
125
313
  module.exports = {
126
314
  isProxyRequestLoggingEnabled,
127
315
  isApiRequestLoggingEnabled,
128
316
  persistProxyRequestSnapshot,
317
+ persistClaudeRequestTemplate,
318
+ loadClaudeRequestTemplate,
129
319
  createApiRequestLogger
130
320
  };
@@ -6,9 +6,6 @@
6
6
 
7
7
  const https = require('https');
8
8
  const http = require('http');
9
- const fs = require('fs');
10
- const os = require('os');
11
- const path = require('path');
12
9
  const crypto = require('crypto');
13
10
  const { URL } = require('url');
14
11
  const { probeModelAvailability } = require('./model-detector');
@@ -16,6 +13,7 @@ const { getEffectiveApiKey: getClaudeEffectiveApiKey } = require('./channels');
16
13
  const { getEffectiveApiKey: getCodexEffectiveApiKey } = require('./codex-channels');
17
14
  const { getEffectiveApiKey: getGeminiEffectiveApiKey } = require('./gemini-channels');
18
15
  const { getEffectiveApiKey: getOpenCodeEffectiveApiKey } = require('./opencode-channels');
16
+ const { loadClaudeRequestTemplate } = require('./request-logger');
19
17
 
20
18
  // 测试结果缓存
21
19
  const testResultsCache = new Map();
@@ -35,16 +33,6 @@ const CLAUDE_ADVANCED_TOOL_USE_BETA = 'advanced-tool-use-2025-11-20';
35
33
  const ROUTE_OR_METHOD_MISMATCH_STATUS = new Set([404, 405, 501]);
36
34
  const CLAUDE_USER_ID_ACCOUNT_RE = /^user_([0-9a-f]{64})_account__session_[a-z0-9._-]+$/i;
37
35
  const CLAUDE_USER_ID_FULL_RE = /^user_[0-9a-f]{64}_account__session_[a-z0-9._-]+$/i;
38
- const DEFAULT_CLAUDE_CODE_TOOL_NAMES = Object.freeze([
39
- 'Task',
40
- 'Bash',
41
- 'Glob',
42
- 'Grep',
43
- 'Read',
44
- 'Edit',
45
- 'Write',
46
- 'ToolSearch'
47
- ]);
48
36
  let cachedClaudeAccountId = '';
49
37
  let cachedClaudeUserId = '';
50
38
 
@@ -195,96 +183,31 @@ function resolveClaudeAccountIdFromUserId(userId = '') {
195
183
  }
196
184
 
197
185
  function resolveClaudeAccountIdFromLogs() {
198
- const logsPath = path.join(os.homedir(), '.cc-tool', 'claude-requests.jsonl');
199
- if (!fs.existsSync(logsPath)) return '';
200
-
201
186
  try {
202
- const content = fs.readFileSync(logsPath, 'utf8');
203
- const lines = content.trim().split('\n');
204
- const accountIdCount = new Map();
205
-
206
- for (let index = lines.length - 1; index >= 0; index -= 1) {
207
- const line = lines[index].trim();
208
- if (!line) continue;
209
- try {
210
- const parsed = JSON.parse(line);
211
- const userId = parsed?.request?.body?.metadata?.user_id;
212
- const accountId = resolveClaudeAccountIdFromUserId(userId);
213
- if (accountId) {
214
- accountIdCount.set(accountId, (accountIdCount.get(accountId) || 0) + 1);
215
- }
216
- } catch {
217
- // ignore malformed line
218
- }
219
- }
220
-
221
- const ranked = Array.from(accountIdCount.entries())
222
- .filter(([accountId]) => accountId !== '0'.repeat(64))
223
- .sort((left, right) => right[1] - left[1]);
224
-
225
- if (ranked.length > 0) {
226
- return ranked[0][0];
227
- }
187
+ const template = loadClaudeRequestTemplate();
188
+ const userId = template?.userId || '';
189
+ const accountId = resolveClaudeAccountIdFromUserId(userId);
190
+ return (accountId && accountId !== '0'.repeat(64)) ? accountId : '';
228
191
  } catch {
229
- // ignore read error
192
+ return '';
230
193
  }
231
-
232
- return '';
233
194
  }
234
195
 
235
196
  function resolveClaudeUserIdFromLogs() {
236
- const logsPath = path.join(os.homedir(), '.cc-tool', 'claude-requests.jsonl');
237
- if (!fs.existsSync(logsPath)) return '';
238
-
239
197
  try {
240
- const content = fs.readFileSync(logsPath, 'utf8');
241
- const lines = content.trim().split('\n');
242
- const userIdCount = new Map();
243
-
244
- for (let index = lines.length - 1; index >= 0; index -= 1) {
245
- const line = lines[index].trim();
246
- if (!line) continue;
247
- try {
248
- const parsed = JSON.parse(line);
249
- const userId = normalizeNonEmptyString(parsed?.request?.body?.metadata?.user_id);
250
- if (!userId || !CLAUDE_USER_ID_FULL_RE.test(userId)) continue;
251
- const accountId = resolveClaudeAccountIdFromUserId(userId);
252
- if (!accountId || accountId === '0'.repeat(64)) continue;
253
- userIdCount.set(userId, (userIdCount.get(userId) || 0) + 1);
254
- } catch {
255
- // ignore malformed line
256
- }
257
- }
258
-
259
- const ranked = Array.from(userIdCount.entries()).sort((left, right) => right[1] - left[1]);
260
- return ranked.length > 0 ? ranked[0][0] : '';
198
+ const template = loadClaudeRequestTemplate();
199
+ const userId = normalizeNonEmptyString(template?.userId || '');
200
+ if (!userId || !CLAUDE_USER_ID_FULL_RE.test(userId)) return '';
201
+ const accountId = resolveClaudeAccountIdFromUserId(userId);
202
+ if (!accountId || accountId === '0'.repeat(64)) return '';
203
+ return userId;
261
204
  } catch {
262
205
  return '';
263
206
  }
264
207
  }
265
208
 
266
209
  function resolveClaudeRequestTemplate() {
267
- const logsPath = path.join(os.homedir(), '.cc-tool', 'claude-requests.jsonl');
268
- if (!fs.existsSync(logsPath)) return null;
269
- try {
270
- const content = fs.readFileSync(logsPath, 'utf8');
271
- const lines = content.trim().split('\n');
272
- for (let i = lines.length - 1; i >= 0; i--) {
273
- const line = lines[i].trim();
274
- if (!line) continue;
275
- try {
276
- const parsed = JSON.parse(line);
277
- const body = parsed?.request?.body;
278
- if (!body || typeof body !== 'object') continue;
279
- const system = Array.isArray(body.system) ? body.system : [];
280
- const tools = Array.isArray(body.tools) ? body.tools : [];
281
- const hasBilling = system.some(b => typeof b?.text === 'string' && b.text.startsWith('x-anthropic-billing-header:'));
282
- if (!hasBilling || tools.length === 0) continue;
283
- return { system, tools };
284
- } catch { }
285
- }
286
- } catch { }
287
- return null;
210
+ return loadClaudeRequestTemplate();
288
211
  }
289
212
 
290
213
  function resolveClaudePreferredUserId() {
@@ -347,17 +270,6 @@ function buildClaudeCodeUserId() {
347
270
  return `user_${accountId}_account__session_${sessionId}`;
348
271
  }
349
272
 
350
- function buildDefaultClaudeCodeTools() {
351
- return DEFAULT_CLAUDE_CODE_TOOL_NAMES.map(name => ({
352
- name,
353
- description: `${name} tool`,
354
- input_schema: {
355
- type: 'object',
356
- properties: {},
357
- additionalProperties: true
358
- }
359
- }));
360
- }
361
273
 
362
274
  function buildGeminiNativeGeneratePath(parsedUrl, model) {
363
275
  let pathname = parsedUrl.pathname.replace(/\/+$/, '');
@@ -755,12 +667,8 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
755
667
  : `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
756
668
  const userId = buildClaudeCodeUserId() || `user_${'0'.repeat(64)}_account__session_${sessionId}`;
757
669
  const template = resolveClaudeRequestTemplate();
758
- const systemBlocks = template?.system?.length > 0
759
- ? template.system
760
- : [{ type: 'text', text: "You are Claude Code, Anthropic's official CLI for Claude." }];
761
- const toolsToUse = template?.tools?.length > 0
762
- ? template.tools
763
- : buildDefaultClaudeCodeTools();
670
+ const systemBlocks = template.system;
671
+ const toolsToUse = template.tools;
764
672
  const requestPayload = {
765
673
  model: testModel,
766
674
  max_tokens: 10,
@@ -868,6 +776,7 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
868
776
  primaryRequestConfig = useCliFormat ? cliRequestConfig : nativeRequestConfig;
869
777
  fallbackRequestConfig = useCliFormat ? nativeRequestConfig : cliRequestConfig;
870
778
  } else {
779
+ testModel = modelProbe?.preferredTestModel || normalizeNonEmptyString(model) || 'gpt-4o-mini';
871
780
  let apiPath = parsedUrl.pathname.replace(/\/$/, '');
872
781
  if (!apiPath.endsWith('/chat/completions')) {
873
782
  apiPath = apiPath + (apiPath.endsWith('/v1') ? '/chat/completions' : '/v1/chat/completions');
@@ -875,7 +784,7 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
875
784
  primaryRequestConfig = {
876
785
  apiPath,
877
786
  requestBody: JSON.stringify({
878
- model: 'gpt-4o-mini',
787
+ model: testModel,
879
788
  max_tokens: 1,
880
789
  messages: [{ role: 'user', content: 'Hi' }]
881
790
  }),
@@ -40,17 +40,33 @@ function isPortInUse(port, host = '127.0.0.1') {
40
40
  }
41
41
 
42
42
  /**
43
- * 查找占用端口的进程PID
43
+ * 查找占用端口的进程PID(跨平台)
44
44
  */
45
45
  function findProcessByPort(port) {
46
+ const isWindows = process.platform === 'win32';
47
+ if (isWindows) {
48
+ try {
49
+ // Windows: netstat -ano 列出所有连接,findstr 过滤端口
50
+ const result = execSync(`netstat -ano | findstr ":${port} "`, { encoding: 'utf-8' });
51
+ const pids = new Set();
52
+ result.split('\n').forEach(line => {
53
+ // 格式: " TCP 0.0.0.0:9999 0.0.0.0:0 LISTENING 1234"
54
+ const match = line.trim().match(/\s+(\d+)\s*$/);
55
+ if (match) pids.add(match[1]);
56
+ });
57
+ return Array.from(pids).filter(pid => pid && pid !== '0');
58
+ } catch (e) {
59
+ return [];
60
+ }
61
+ }
62
+
46
63
  try {
47
64
  // macOS/Linux 使用 lsof
48
65
  const result = execSync(`lsof -ti :${port}`, { encoding: 'utf-8' }).trim();
49
66
  return result.split('\n').filter(pid => pid);
50
67
  } catch (err) {
51
- // 如果 lsof 失败,尝试使用其他命令
68
+ // 如果 lsof 失败,尝试使用 fuser(某些 Linux 系统)
52
69
  try {
53
- // 适用于某些 Linux 系统
54
70
  const result = execSync(`fuser ${port}/tcp 2>/dev/null`, { encoding: 'utf-8' }).trim();
55
71
  return result.split(/\s+/).filter(pid => pid);
56
72
  } catch (e) {
@@ -60,7 +76,7 @@ function findProcessByPort(port) {
60
76
  }
61
77
 
62
78
  /**
63
- * 杀掉占用端口的进程
79
+ * 杀掉占用端口的进程(跨平台)
64
80
  */
65
81
  function killProcessByPort(port) {
66
82
  try {
@@ -69,9 +85,14 @@ function killProcessByPort(port) {
69
85
  return false;
70
86
  }
71
87
 
88
+ const isWindows = process.platform === 'win32';
72
89
  pids.forEach(pid => {
73
90
  try {
74
- execSync(`kill -9 ${pid}`, { stdio: 'ignore' });
91
+ if (isWindows) {
92
+ execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore' });
93
+ } else {
94
+ execSync(`kill -9 ${pid}`, { stdio: 'ignore' });
95
+ }
75
96
  } catch (err) {
76
97
  // 忽略单个进程杀掉失败的错误
77
98
  }