@adversity/coding-tool-x 3.0.5 → 3.1.0

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 (34) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/web/assets/{icons-BlzwYoRU.js → icons-CO_2OFES.js} +1 -1
  3. package/dist/web/assets/index-DI8QOi-E.js +14 -0
  4. package/dist/web/assets/index-uLHGdeZh.css +41 -0
  5. package/dist/web/assets/{naive-ui-B1TP-0TP.js → naive-ui-B1re3c-e.js} +1 -1
  6. package/dist/web/index.html +4 -4
  7. package/package.json +1 -1
  8. package/src/commands/daemon.js +11 -1
  9. package/src/commands/ui.js +8 -1
  10. package/src/index.js +3 -1
  11. package/src/server/api/channels.js +3 -0
  12. package/src/server/api/codex-channels.js +40 -0
  13. package/src/server/api/config-registry.js +341 -0
  14. package/src/server/api/gemini-channels.js +40 -0
  15. package/src/server/api/oauth.js +294 -0
  16. package/src/server/api/permissions.js +30 -15
  17. package/src/server/codex-proxy-server.js +30 -4
  18. package/src/server/config/oauth-providers.js +68 -0
  19. package/src/server/gemini-proxy-server.js +64 -2
  20. package/src/server/index.js +15 -3
  21. package/src/server/proxy-server.js +31 -4
  22. package/src/server/services/channels.js +33 -2
  23. package/src/server/services/codex-channels.js +35 -4
  24. package/src/server/services/config-registry-service.js +762 -0
  25. package/src/server/services/config-sync-manager.js +456 -0
  26. package/src/server/services/config-templates-service.js +38 -3
  27. package/src/server/services/gemini-channels.js +40 -1
  28. package/src/server/services/model-detector.js +116 -23
  29. package/src/server/services/oauth-callback-server.js +284 -0
  30. package/src/server/services/oauth-service.js +378 -0
  31. package/src/server/services/oauth-token-storage.js +135 -0
  32. package/src/server/services/permission-templates-service.js +0 -31
  33. package/dist/web/assets/index-19ZPjh5b.css +0 -41
  34. package/dist/web/assets/index-B4w1yh7H.js +0 -14
@@ -13,6 +13,7 @@ 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
15
  const { CLAUDE_MODEL_PRICING } = require('../config/model-pricing');
16
+ const { getEffectiveApiKey } = require('./services/channels');
16
17
 
17
18
  let proxyServer = null;
18
19
  let proxyApp = null;
@@ -21,6 +22,10 @@ let currentPort = null;
21
22
  // 用于存储每个请求的元数据(用于 WebSocket 日志)
22
23
  const requestMetadata = new Map();
23
24
 
25
+ // 用于缓存已打印过的模型重定向规则,避免重复打印
26
+ // 格式: { channelId: { "originalModel": "redirectedModel", ... } }
27
+ const printedRedirectCache = new Map();
28
+
24
29
  const CLAUDE_BASE_PRICING = DEFAULT_CONFIG.pricing.claude;
25
30
  const ONE_MILLION = 1000000;
26
31
 
@@ -181,9 +186,10 @@ async function startProxyServer(options = {}) {
181
186
  });
182
187
 
183
188
  proxyReq.removeHeader('x-api-key');
184
- proxyReq.setHeader('x-api-key', selectedChannel.apiKey);
189
+ const effectiveKey = getEffectiveApiKey(selectedChannel);
190
+ proxyReq.setHeader('x-api-key', effectiveKey);
185
191
  proxyReq.removeHeader('authorization');
186
- proxyReq.setHeader('authorization', `Bearer ${selectedChannel.apiKey}`);
192
+ proxyReq.setHeader('authorization', `Bearer ${effectiveKey}`);
187
193
 
188
194
  if (!proxyReq.getHeader('anthropic-version')) {
189
195
  proxyReq.setHeader('anthropic-version', '2023-06-01');
@@ -225,7 +231,14 @@ async function startProxyServer(options = {}) {
225
231
  req.body.model = redirectedModel;
226
232
  // 更新 rawBody 以匹配修改后的 body
227
233
  req.rawBody = Buffer.from(JSON.stringify(req.body));
228
- console.log(`[Model Redirect] ${originalModel} → ${redirectedModel} (channel: ${channel.name})`);
234
+
235
+ // 只在重定向规则变化时打印日志(避免每次请求都打印)
236
+ const cachedRedirects = printedRedirectCache.get(channel.id) || {};
237
+ if (cachedRedirects[originalModel] !== redirectedModel) {
238
+ cachedRedirects[originalModel] = redirectedModel;
239
+ printedRedirectCache.set(channel.id, cachedRedirects);
240
+ console.log(`[Model Redirect] ${originalModel} → ${redirectedModel} (channel: ${channel.name})`);
241
+ }
229
242
  }
230
243
  }
231
244
 
@@ -526,8 +539,22 @@ function getProxyStatus() {
526
539
  };
527
540
  }
528
541
 
542
+ /**
543
+ * 清除指定渠道的模型重定向日志缓存
544
+ * 用于在渠道配置更新后触发重新打印日志
545
+ * @param {string} channelId - 渠道 ID
546
+ */
547
+ function clearRedirectCache(channelId) {
548
+ if (channelId) {
549
+ printedRedirectCache.delete(channelId);
550
+ } else {
551
+ printedRedirectCache.clear();
552
+ }
553
+ }
554
+
529
555
  module.exports = {
530
556
  startProxyServer,
531
557
  stopProxyServer,
532
- getProxyStatus
558
+ getProxyStatus,
559
+ clearRedirectCache
533
560
  };
@@ -65,6 +65,11 @@ function applyChannelDefaults(channel) {
65
65
  normalized.enabled = !!normalized.enabled;
66
66
  }
67
67
 
68
+ // OAuth 字段默认值(向后兼容)
69
+ if (!normalized.authType) {
70
+ normalized.authType = 'apiKey';
71
+ }
72
+
68
73
  normalized.weight = normalizeNumber(normalized.weight, 1, 100);
69
74
 
70
75
  if (normalized.maxConcurrency === undefined ||
@@ -189,7 +194,11 @@ function createChannel(name, baseUrl, apiKey, websiteUrl, extraConfig = {}) {
189
194
  modelConfig: extraConfig.modelConfig || null,
190
195
  modelRedirects: extraConfig.modelRedirects || [],
191
196
  proxyUrl: extraConfig.proxyUrl || '',
192
- speedTestModel: extraConfig.speedTestModel || null
197
+ speedTestModel: extraConfig.speedTestModel || null,
198
+ // OAuth 支持
199
+ authType: extraConfig.authType || 'apiKey',
200
+ oauthProvider: extraConfig.oauthProvider || null,
201
+ oauthTokenId: extraConfig.oauthTokenId || null
193
202
  });
194
203
 
195
204
  data.channels.push(newChannel);
@@ -381,6 +390,27 @@ function updateClaudeSettings(baseUrl, apiKey) {
381
390
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
382
391
  }
383
392
 
393
+ /**
394
+ * 获取渠道的有效 API Key
395
+ * 如果渠道使用 OAuth 认证,返回有效的 OAuth 令牌;否则返回静态 API Key
396
+ *
397
+ * @param {Object} channel - 渠道对象
398
+ * @returns {string|null} 有效的 API Key,OAuth 令牌无效/过期时返回 null
399
+ */
400
+ function getEffectiveApiKey(channel) {
401
+ if (channel.authType === 'oauth' && channel.oauthTokenId) {
402
+ const { getToken, isTokenExpired } = require('./oauth-token-storage');
403
+ const token = getToken(channel.oauthTokenId);
404
+ if (token && !isTokenExpired(token)) {
405
+ return token.accessToken;
406
+ }
407
+ // OAuth 令牌无效或已过期,返回 null(调用方应处理刷新或报错)
408
+ console.warn(`[Channels] OAuth token expired or not found for channel ${channel.name}`);
409
+ return null;
410
+ }
411
+ return channel.apiKey;
412
+ }
413
+
384
414
  module.exports = {
385
415
  getAllChannels,
386
416
  getCurrentSettings,
@@ -390,5 +420,6 @@ module.exports = {
390
420
  applyChannelToSettings,
391
421
  getBestChannelForRestore,
392
422
  updateClaudeSettings,
393
- updateClaudeSettingsWithModelConfig
423
+ updateClaudeSettingsWithModelConfig,
424
+ getEffectiveApiKey
394
425
  };
@@ -47,7 +47,10 @@ function loadChannels() {
47
47
  ...ch,
48
48
  enabled: ch.enabled !== false, // 默认启用
49
49
  weight: ch.weight || 1,
50
- maxConcurrency: ch.maxConcurrency || null
50
+ maxConcurrency: ch.maxConcurrency || null,
51
+ modelRedirects: ch.modelRedirects || [],
52
+ speedTestModel: ch.speedTestModel || null,
53
+ authType: ch.authType || 'apiKey' // 默认 API Key 认证
51
54
  }));
52
55
  }
53
56
  return data;
@@ -175,6 +178,11 @@ function createChannel(name, providerKey, baseUrl, apiKey, wireApi = 'responses'
175
178
  enabled: extraConfig.enabled !== false, // 默认启用
176
179
  weight: extraConfig.weight || 1,
177
180
  maxConcurrency: extraConfig.maxConcurrency || null,
181
+ modelRedirects: extraConfig.modelRedirects || [],
182
+ speedTestModel: extraConfig.speedTestModel || null,
183
+ authType: extraConfig.authType || 'apiKey',
184
+ oauthProvider: extraConfig.oauthProvider || null,
185
+ oauthTokenId: extraConfig.oauthTokenId || null,
178
186
  createdAt: Date.now(),
179
187
  updatedAt: Date.now()
180
188
  };
@@ -217,11 +225,13 @@ function updateChannel(channelId, updates) {
217
225
  }
218
226
  }
219
227
 
228
+ const merged = { ...oldChannel, ...updates };
220
229
  const newChannel = {
221
- ...oldChannel,
222
- ...updates,
230
+ ...merged,
223
231
  id: channelId, // 保持 ID 不变
224
232
  createdAt: oldChannel.createdAt, // 保持创建时间
233
+ modelRedirects: merged.modelRedirects || [],
234
+ speedTestModel: merged.speedTestModel !== undefined ? merged.speedTestModel : (oldChannel.speedTestModel || null),
225
235
  updatedAt: Date.now()
226
236
  };
227
237
 
@@ -643,6 +653,26 @@ try {
643
653
  console.warn('[Codex Channels] Auto sync env vars failed:', err.message);
644
654
  }
645
655
 
656
+ /**
657
+ * 获取渠道的有效 API Key
658
+ * 如果渠道使用 OAuth 认证,返回 OAuth 令牌;否则返回静态 API Key
659
+ *
660
+ * @param {Object} channel - 渠道对象
661
+ * @returns {string|null} 有效的 API Key
662
+ */
663
+ function getEffectiveApiKey(channel) {
664
+ if (channel.authType === 'oauth' && channel.oauthTokenId) {
665
+ const { getToken, isTokenExpired } = require('./oauth-token-storage');
666
+ const token = getToken(channel.oauthTokenId);
667
+ if (token && !isTokenExpired(token)) {
668
+ return token.accessToken;
669
+ }
670
+ // OAuth 令牌无效或已过期,返回 null
671
+ return null;
672
+ }
673
+ return channel.apiKey;
674
+ }
675
+
646
676
  module.exports = {
647
677
  getChannels,
648
678
  createChannel,
@@ -652,5 +682,6 @@ module.exports = {
652
682
  saveChannelOrder,
653
683
  syncAllChannelEnvVars,
654
684
  writeCodexConfigForMultiChannel,
655
- applyChannelToSettings
685
+ applyChannelToSettings,
686
+ getEffectiveApiKey
656
687
  };