coding-tool-x 3.3.7 → 3.3.9

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 (89) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +253 -326
  3. package/dist/web/assets/{Analytics-IW6eAy9u.js → Analytics-D6LzK9hk.js} +1 -1
  4. package/dist/web/assets/{ConfigTemplates-BPtkTMSc.js → ConfigTemplates-BUDYuxRi.js} +1 -1
  5. package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
  6. package/dist/web/assets/Home-D7KX7iF8.js +1 -0
  7. package/dist/web/assets/{PluginManager-BGx9MSDV.js → PluginManager-DTgQ--vB.js} +1 -1
  8. package/dist/web/assets/{ProjectList-BCn-mrCx.js → ProjectList-DMCiGmCT.js} +1 -1
  9. package/dist/web/assets/{SessionList-CzLfebJQ.js → SessionList-CRBsdVRe.js} +1 -1
  10. package/dist/web/assets/{SkillManager-CXz2vBQx.js → SkillManager-DMwx2Q4k.js} +1 -1
  11. package/dist/web/assets/{WorkspaceManager-CHtgMfKc.js → WorkspaceManager-DapB4ljL.js} +1 -1
  12. package/dist/web/assets/{icons-B29onFfZ.js → icons-B5Pl4lrD.js} +1 -1
  13. package/dist/web/assets/index-CL-qpoJ_.js +2 -0
  14. package/dist/web/assets/index-D_5dRFOL.css +1 -0
  15. package/dist/web/assets/{markdown-C9MYpaSi.js → markdown-DyTJGI4N.js} +1 -1
  16. package/dist/web/assets/{naive-ui-CxpuzdjU.js → naive-ui-Bdxp09n2.js} +1 -1
  17. package/dist/web/assets/{vendors-DMjSfzlv.js → vendors-CKPV1OAU.js} +2 -2
  18. package/dist/web/assets/{vue-vendor-DET08QYg.js → vue-vendor-3bf-fPGP.js} +1 -1
  19. package/dist/web/index.html +7 -7
  20. package/docs/home.png +0 -0
  21. package/package.json +14 -5
  22. package/src/commands/daemon.js +3 -2
  23. package/src/commands/security.js +1 -2
  24. package/src/commands/toggle-proxy.js +100 -5
  25. package/src/config/paths.js +718 -90
  26. package/src/server/api/agents.js +1 -1
  27. package/src/server/api/channels.js +9 -0
  28. package/src/server/api/claude-hooks.js +13 -8
  29. package/src/server/api/codex-channels.js +9 -0
  30. package/src/server/api/codex-proxy.js +27 -15
  31. package/src/server/api/gemini-proxy.js +22 -11
  32. package/src/server/api/hooks.js +45 -0
  33. package/src/server/api/oauth-credentials.js +163 -0
  34. package/src/server/api/opencode-proxy.js +22 -10
  35. package/src/server/api/plugins.js +2 -1
  36. package/src/server/api/proxy.js +39 -44
  37. package/src/server/api/skills.js +91 -13
  38. package/src/server/api/ui-config.js +5 -0
  39. package/src/server/codex-proxy-server.js +90 -70
  40. package/src/server/gemini-proxy-server.js +107 -88
  41. package/src/server/index.js +2 -0
  42. package/src/server/opencode-proxy-server.js +381 -225
  43. package/src/server/proxy-server.js +86 -60
  44. package/src/server/services/alias.js +3 -3
  45. package/src/server/services/channels.js +21 -24
  46. package/src/server/services/codex-channels.js +158 -255
  47. package/src/server/services/codex-config.js +2 -5
  48. package/src/server/services/codex-env-manager.js +423 -0
  49. package/src/server/services/codex-settings-manager.js +21 -357
  50. package/src/server/services/codex-statistics-service.js +3 -27
  51. package/src/server/services/config-export-service.js +43 -9
  52. package/src/server/services/config-registry-service.js +3 -2
  53. package/src/server/services/config-sync-manager.js +1 -1
  54. package/src/server/services/favorites.js +4 -3
  55. package/src/server/services/gemini-channels.js +14 -12
  56. package/src/server/services/gemini-statistics-service.js +3 -25
  57. package/src/server/services/mcp-service.js +35 -19
  58. package/src/server/services/model-detector.js +4 -3
  59. package/src/server/services/native-keychain.js +243 -0
  60. package/src/server/services/native-oauth-adapters.js +891 -0
  61. package/src/server/services/network-access.js +39 -1
  62. package/src/server/services/notification-hooks.js +951 -0
  63. package/src/server/services/oauth-credentials-service.js +786 -0
  64. package/src/server/services/oauth-utils.js +49 -0
  65. package/src/server/services/opencode-channels.js +19 -15
  66. package/src/server/services/opencode-sessions.js +2 -2
  67. package/src/server/services/opencode-settings-manager.js +169 -16
  68. package/src/server/services/opencode-statistics-service.js +3 -27
  69. package/src/server/services/plugins-service.js +115 -15
  70. package/src/server/services/prompts-service.js +2 -3
  71. package/src/server/services/proxy-log-helper.js +242 -0
  72. package/src/server/services/proxy-runtime.js +6 -4
  73. package/src/server/services/repo-scanner-base.js +12 -4
  74. package/src/server/services/request-logger.js +7 -7
  75. package/src/server/services/security-config.js +4 -4
  76. package/src/server/services/session-cache.js +2 -2
  77. package/src/server/services/sessions.js +2 -2
  78. package/src/server/services/settings-manager.js +13 -0
  79. package/src/server/services/skill-service.js +867 -368
  80. package/src/server/services/statistics-service.js +5 -5
  81. package/src/server/services/ui-config.js +4 -3
  82. package/src/server/services/workspace-service.js +1 -1
  83. package/src/server/websocket-server.js +5 -4
  84. package/dist/web/assets/Home-BsSioaaB.css +0 -1
  85. package/dist/web/assets/Home-obifg_9E.js +0 -1
  86. package/dist/web/assets/index-C7LPdVsN.js +0 -2
  87. package/dist/web/assets/index-eEmjZKWP.css +0 -1
  88. package/docs/bannel.png +0 -0
  89. package/docs/model-redirection.md +0 -251
@@ -18,6 +18,7 @@ const { createDecodedStream } = require('./services/response-decoder');
18
18
  const eventBus = require('../plugins/event-bus');
19
19
  const { getEffectiveApiKey } = require('./services/channels');
20
20
  const { persistProxyRequestSnapshot, persistClaudeRequestTemplate } = require('./services/request-logger');
21
+ const { publishUsageLog, publishFailureLog } = require('./services/proxy-log-helper');
21
22
 
22
23
  let proxyServer = null;
23
24
  let proxyApp = null;
@@ -313,20 +314,20 @@ async function startProxyServer(options = {}) {
313
314
  const effectiveKey = getEffectiveApiKey(channel);
314
315
  if (!effectiveKey) {
315
316
  release();
317
+ publishFailureLog({
318
+ source: 'claude',
319
+ channel: channel.name,
320
+ message: 'API key not configured or expired. Please update your channel key.',
321
+ statusCode: 401,
322
+ stage: 'preflight',
323
+ broadcastLog
324
+ });
316
325
  return res.status(401).json({
317
326
  error: 'API key not configured or expired. Please update your channel key.',
318
327
  type: 'authentication_error'
319
328
  });
320
329
  }
321
330
  req.effectiveApiKey = effectiveKey;
322
-
323
- const now = new Date();
324
- const time = now.toLocaleTimeString('zh-CN', {
325
- hour12: false,
326
- hour: '2-digit',
327
- minute: '2-digit',
328
- second: '2-digit'
329
- });
330
331
  const requestSnapshot = serializeFullClaudeRequest(req);
331
332
  persistClaudeRequestSnapshot({
332
333
  timestamp: Date.now(),
@@ -373,6 +374,21 @@ async function startProxyServer(options = {}) {
373
374
  if (err) {
374
375
  // 记录请求失败
375
376
  recordFailure(channel.id, 'claude', err);
377
+ const metadata = requestMetadata.get(req) || {
378
+ id: null,
379
+ channel: channel.name,
380
+ channelId: channel.id,
381
+ startTime: Date.now()
382
+ };
383
+ publishFailureLog({
384
+ source: 'claude',
385
+ metadata,
386
+ message: err.message,
387
+ error: err,
388
+ statusCode: 502,
389
+ stage: 'proxy_web',
390
+ broadcastLog
391
+ });
376
392
  console.error('Proxy error:', err);
377
393
  if (res && !res.headersSent) {
378
394
  res.status(502).json({
@@ -384,6 +400,13 @@ async function startProxyServer(options = {}) {
384
400
  });
385
401
  } catch (error) {
386
402
  console.error('Channel allocation error:', error);
403
+ publishFailureLog({
404
+ source: 'claude',
405
+ message: error.message || '所有渠道暂时不可用',
406
+ statusCode: 503,
407
+ stage: 'allocate_channel',
408
+ broadcastLog
409
+ });
387
410
  if (!res.headersSent) {
388
411
  res.status(503).json({
389
412
  error: error.message || '所有渠道暂时不可用',
@@ -425,8 +448,34 @@ async function startProxyServer(options = {}) {
425
448
  cacheRead: 0,
426
449
  model: ''
427
450
  };
451
+ let usageRecorded = false;
428
452
  const parsedStream = createDecodedStream(proxyRes);
429
453
 
454
+ function recordUsageIfReady() {
455
+ if (usageRecorded) return false;
456
+
457
+ const result = publishUsageLog({
458
+ source: 'claude',
459
+ metadata,
460
+ model: tokenData.model,
461
+ tokens: {
462
+ input: tokenData.inputTokens,
463
+ output: tokenData.outputTokens,
464
+ cacheCreation: tokenData.cacheCreation,
465
+ cacheRead: tokenData.cacheRead
466
+ },
467
+ calculateCost,
468
+ broadcastLog,
469
+ recordRequest,
470
+ recordSuccess,
471
+ allowBroadcast: !isResponseClosed
472
+ });
473
+
474
+ if (!result) return false;
475
+ usageRecorded = true;
476
+ return true;
477
+ }
478
+
430
479
  parsedStream.on('data', (chunk) => {
431
480
  if (isResponseClosed) return;
432
481
 
@@ -474,57 +523,8 @@ async function startProxyServer(options = {}) {
474
523
  }
475
524
  }
476
525
 
477
- if (eventType === 'message_delta' && parsed.usage) {
478
- const now = new Date();
479
- const time = now.toLocaleTimeString('zh-CN', {
480
- hour12: false,
481
- hour: '2-digit',
482
- minute: '2-digit',
483
- second: '2-digit'
484
- });
485
-
486
- const tokens = {
487
- input: tokenData.inputTokens,
488
- output: tokenData.outputTokens,
489
- cacheCreation: tokenData.cacheCreation,
490
- cacheRead: tokenData.cacheRead,
491
- total: tokenData.inputTokens + tokenData.outputTokens + tokenData.cacheCreation + tokenData.cacheRead
492
- };
493
- const cost = calculateCost(tokenData.model, tokens);
494
-
495
- if (!isResponseClosed) {
496
- broadcastLog({
497
- type: 'log',
498
- id: metadata.id,
499
- time: time,
500
- channel: metadata.channel,
501
- model: tokenData.model,
502
- inputTokens: tokenData.inputTokens,
503
- outputTokens: tokenData.outputTokens,
504
- cacheCreation: tokenData.cacheCreation,
505
- cacheRead: tokenData.cacheRead,
506
- cost: cost,
507
- source: 'claude'
508
- });
509
- }
510
-
511
- const duration = Date.now() - metadata.startTime;
512
-
513
- recordRequest({
514
- id: metadata.id,
515
- timestamp: new Date(metadata.startTime).toISOString(),
516
- toolType: 'claude-code',
517
- channel: metadata.channel,
518
- channelId: metadata.channelId,
519
- model: tokenData.model,
520
- tokens: tokens,
521
- duration: duration,
522
- success: true,
523
- cost: cost
524
- });
525
-
526
- // 记录请求成功(用于健康检查)
527
- recordSuccess(metadata.channelId, 'claude');
526
+ if (eventType === 'message_stop') {
527
+ recordUsageIfReady();
528
528
  }
529
529
  } catch (err) {
530
530
  }
@@ -540,7 +540,10 @@ async function startProxyServer(options = {}) {
540
540
  }
541
541
  };
542
542
 
543
- parsedStream.on('end', finalize);
543
+ parsedStream.on('end', () => {
544
+ recordUsageIfReady();
545
+ finalize();
546
+ });
544
547
 
545
548
  parsedStream.on('error', (err) => {
546
549
  if (err.code !== 'EPIPE' && err.code !== 'ECONNRESET') {
@@ -550,6 +553,15 @@ async function startProxyServer(options = {}) {
550
553
  if (metadata && metadata.channelId) {
551
554
  recordFailure(metadata.channelId, 'claude', err);
552
555
  }
556
+ publishFailureLog({
557
+ source: 'claude',
558
+ metadata,
559
+ message: err.message,
560
+ error: err,
561
+ statusCode: proxyRes.statusCode,
562
+ stage: 'response_stream',
563
+ broadcastLog
564
+ });
553
565
  isResponseClosed = true;
554
566
  finalize();
555
567
  });
@@ -561,6 +573,20 @@ async function startProxyServer(options = {}) {
561
573
  if (req && req.selectedChannel && req.selectedChannel.id) {
562
574
  recordFailure(req.selectedChannel.id, 'claude', err);
563
575
  }
576
+ const metadata = req ? requestMetadata.get(req) : null;
577
+ publishFailureLog({
578
+ source: 'claude',
579
+ metadata: metadata || {
580
+ channel: req?.selectedChannel?.name,
581
+ channelId: req?.selectedChannel?.id,
582
+ model: req?.body?.model
583
+ },
584
+ message: err.message,
585
+ error: err,
586
+ statusCode: 502,
587
+ stage: 'proxy',
588
+ broadcastLog
589
+ });
564
590
  if (res && !res.headersSent) {
565
591
  res.status(502).json({
566
592
  error: 'Proxy error: ' + err.message,
@@ -2,13 +2,13 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { PATHS } = require('../../config/paths');
4
4
 
5
- const ALIAS_DIR = PATHS.base;
6
5
  const ALIAS_FILE = PATHS.aliases;
7
6
 
8
7
  // Ensure alias directory exists
9
8
  function ensureAliasDir() {
10
- if (!fs.existsSync(ALIAS_DIR)) {
11
- fs.mkdirSync(ALIAS_DIR, { recursive: true });
9
+ const dir = path.dirname(ALIAS_FILE);
10
+ if (!fs.existsSync(dir)) {
11
+ fs.mkdirSync(dir, { recursive: true });
12
12
  }
13
13
  }
14
14
 
@@ -1,9 +1,11 @@
1
1
  const fs = require('fs');
2
+ const path = require('path');
2
3
  const { isProxyConfig } = require('./settings-manager');
3
4
  const { PATHS, NATIVE_PATHS } = require('../../config/paths');
5
+ const { clearNativeOAuth } = require('./native-oauth-adapters');
4
6
 
5
7
  function getChannelsFilePath() {
6
- const dir = PATHS.base;
8
+ const dir = path.dirname(PATHS.channels.claude);
7
9
  if (!fs.existsSync(dir)) {
8
10
  fs.mkdirSync(dir, { recursive: true });
9
11
  }
@@ -11,7 +13,7 @@ function getChannelsFilePath() {
11
13
  }
12
14
 
13
15
  function getActiveChannelIdPath() {
14
- const dir = PATHS.base;
16
+ const dir = path.dirname(PATHS.activeChannel.claude);
15
17
  if (!fs.existsSync(dir)) {
16
18
  fs.mkdirSync(dir, { recursive: true });
17
19
  }
@@ -165,11 +167,13 @@ function getCurrentSettings() {
165
167
  return null;
166
168
  }
167
169
  const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
170
+ const nativeOAuth = require('./native-oauth-adapters').readNativeOAuth('claude');
168
171
 
169
172
  let baseUrl = settings.env?.ANTHROPIC_BASE_URL || '';
170
- let apiKey = settings.env?.ANTHROPIC_API_KEY ||
171
- settings.env?.ANTHROPIC_AUTH_TOKEN ||
172
- '';
173
+ let apiKey = settings.env?.ANTHROPIC_API_KEY || '';
174
+ if (!apiKey && !nativeOAuth) {
175
+ apiKey = settings.env?.ANTHROPIC_AUTH_TOKEN || '';
176
+ }
173
177
 
174
178
  if (!apiKey && settings.apiKeyHelper) {
175
179
  apiKey = extractApiKeyFromHelper(settings.apiKeyHelper);
@@ -288,14 +292,6 @@ function updateChannel(id, updates) {
288
292
  console.log(`[Single-channel mode] Enabled "${nextChannel.name}", disabled all others`);
289
293
  }
290
294
 
291
- // Prevent disabling last enabled channel when proxy is OFF
292
- if (!isProxyRunning && !nextChannel.enabled && oldChannel.enabled) {
293
- const enabledCount = data.channels.filter(ch => ch.enabled).length;
294
- if (enabledCount === 0) {
295
- throw new Error('无法禁用最后一个启用的渠道。请先启用其他渠道或启动动态切换。');
296
- }
297
- }
298
-
299
295
  saveChannels(data);
300
296
 
301
297
  // Sync settings.json only when proxy is OFF.
@@ -341,6 +337,7 @@ function applyChannelToSettings(id) {
341
337
  }
342
338
 
343
339
  function updateClaudeSettingsWithModelConfig(channel) {
340
+ clearNativeOAuth('claude');
344
341
  const settingsPath = getClaudeSettingsPath();
345
342
 
346
343
  let settings = {};
@@ -354,17 +351,10 @@ function updateClaudeSettingsWithModelConfig(channel) {
354
351
 
355
352
  const { baseUrl, apiKey, modelConfig, presetId, proxyUrl } = channel;
356
353
 
357
- const useAuthToken = settings.env.ANTHROPIC_AUTH_TOKEN !== undefined;
358
- const useApiKey = settings.env.ANTHROPIC_API_KEY !== undefined;
359
-
360
354
  settings.env.ANTHROPIC_BASE_URL = baseUrl;
361
-
362
- if (useAuthToken || (!useAuthToken && !useApiKey)) {
363
- settings.env.ANTHROPIC_AUTH_TOKEN = apiKey;
364
- delete settings.env.ANTHROPIC_API_KEY;
365
- } else {
366
- settings.env.ANTHROPIC_API_KEY = apiKey;
367
- }
355
+ settings.env.ANTHROPIC_API_KEY = apiKey;
356
+ delete settings.env.ANTHROPIC_AUTH_TOKEN;
357
+ delete settings.env.CLAUDE_CODE_OAUTH_TOKEN;
368
358
 
369
359
  if (presetId && presetId !== 'official' && modelConfig) {
370
360
  if (modelConfig.model) {
@@ -433,6 +423,12 @@ function getEffectiveApiKey(channel) {
433
423
  return channel.apiKey || null;
434
424
  }
435
425
 
426
+ function disableAllChannels() {
427
+ const data = loadChannels();
428
+ data.channels.forEach(ch => { ch.enabled = false; });
429
+ saveChannels(data);
430
+ }
431
+
436
432
  module.exports = {
437
433
  getAllChannels,
438
434
  getCurrentChannel,
@@ -444,5 +440,6 @@ module.exports = {
444
440
  getBestChannelForRestore,
445
441
  updateClaudeSettings,
446
442
  updateClaudeSettingsWithModelConfig,
447
- getEffectiveApiKey
443
+ getEffectiveApiKey,
444
+ disableAllChannels
448
445
  };