@adversity/coding-tool-x 3.0.6 → 3.1.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 (133) hide show
  1. package/CHANGELOG.md +38 -18
  2. package/README.md +8 -8
  3. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  4. package/dist/web/assets/ConfigTemplates-ZrK_s7ma.js +1 -0
  5. package/dist/web/assets/Home-B8YfhZ3c.js +1 -0
  6. package/dist/web/assets/Home-Di2qsylF.css +1 -0
  7. package/dist/web/assets/PluginManager-BD7QUZbU.js +1 -0
  8. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  9. package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
  10. package/dist/web/assets/ProjectList-DRb1DuHV.js +1 -0
  11. package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
  12. package/dist/web/assets/SessionList-lZ0LKzfT.js +1 -0
  13. package/dist/web/assets/SkillManager-C1xG5B4Q.js +1 -0
  14. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  15. package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
  16. package/dist/web/assets/Terminal-DksBo_lM.js +1 -0
  17. package/dist/web/assets/WorkspaceManager-Burx7XOo.js +1 -0
  18. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  19. package/dist/web/assets/icons-kcfLIMBB.js +1 -0
  20. package/dist/web/assets/index-Ufv5rCa5.css +1 -0
  21. package/dist/web/assets/index-lAkrRC3h.js +2 -0
  22. package/dist/web/assets/markdown-BfC0goYb.css +10 -0
  23. package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
  24. package/dist/web/assets/naive-ui-CSrLusZZ.js +1 -0
  25. package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
  26. package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
  27. package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
  28. package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
  29. package/dist/web/index.html +8 -6
  30. package/package.json +4 -2
  31. package/src/commands/channels.js +48 -1
  32. package/src/commands/cli-type.js +4 -2
  33. package/src/commands/daemon.js +92 -13
  34. package/src/commands/doctor.js +10 -9
  35. package/src/commands/list.js +1 -1
  36. package/src/commands/logs.js +6 -4
  37. package/src/commands/port-config.js +24 -4
  38. package/src/commands/proxy-control.js +12 -6
  39. package/src/commands/search.js +1 -1
  40. package/src/commands/security.js +3 -2
  41. package/src/commands/stats.js +226 -52
  42. package/src/commands/switch.js +1 -1
  43. package/src/commands/toggle-proxy.js +31 -6
  44. package/src/commands/ui.js +8 -1
  45. package/src/commands/update.js +97 -0
  46. package/src/commands/workspace.js +1 -1
  47. package/src/config/default.js +39 -2
  48. package/src/config/loader.js +74 -8
  49. package/src/config/paths.js +105 -33
  50. package/src/index.js +67 -4
  51. package/src/plugins/constants.js +3 -2
  52. package/src/plugins/plugin-api.js +1 -1
  53. package/src/reset-config.js +4 -2
  54. package/src/server/api/agents.js +57 -14
  55. package/src/server/api/channels.js +112 -33
  56. package/src/server/api/codex-channels.js +111 -18
  57. package/src/server/api/codex-proxy.js +14 -8
  58. package/src/server/api/commands.js +71 -18
  59. package/src/server/api/config-export.js +0 -6
  60. package/src/server/api/config-registry.js +11 -3
  61. package/src/server/api/config.js +376 -5
  62. package/src/server/api/convert.js +133 -0
  63. package/src/server/api/dashboard.js +22 -6
  64. package/src/server/api/gemini-channels.js +107 -18
  65. package/src/server/api/gemini-proxy.js +14 -8
  66. package/src/server/api/gemini-sessions.js +1 -1
  67. package/src/server/api/health-check.js +4 -3
  68. package/src/server/api/mcp.js +3 -3
  69. package/src/server/api/opencode-channels.js +419 -0
  70. package/src/server/api/opencode-projects.js +99 -0
  71. package/src/server/api/opencode-proxy.js +198 -0
  72. package/src/server/api/opencode-sessions.js +403 -0
  73. package/src/server/api/opencode-statistics.js +57 -0
  74. package/src/server/api/plugins.js +66 -19
  75. package/src/server/api/prompts.js +2 -2
  76. package/src/server/api/proxy.js +7 -4
  77. package/src/server/api/sessions.js +3 -0
  78. package/src/server/api/skills.js +69 -18
  79. package/src/server/api/workspaces.js +78 -6
  80. package/src/server/codex-proxy-server.js +32 -19
  81. package/src/server/dev-server.js +1 -1
  82. package/src/server/gemini-proxy-server.js +17 -3
  83. package/src/server/index.js +164 -48
  84. package/src/server/opencode-proxy-server.js +4375 -0
  85. package/src/server/proxy-server.js +30 -19
  86. package/src/server/services/agents-service.js +61 -24
  87. package/src/server/services/channel-scheduler.js +9 -5
  88. package/src/server/services/channels.js +70 -12
  89. package/src/server/services/codex-channels.js +61 -23
  90. package/src/server/services/codex-settings-manager.js +271 -49
  91. package/src/server/services/codex-statistics-service.js +2 -2
  92. package/src/server/services/commands-service.js +84 -25
  93. package/src/server/services/config-export-service.js +7 -45
  94. package/src/server/services/config-registry-service.js +63 -17
  95. package/src/server/services/config-sync-manager.js +160 -7
  96. package/src/server/services/config-templates-service.js +204 -51
  97. package/src/server/services/env-checker.js +26 -12
  98. package/src/server/services/env-manager.js +126 -18
  99. package/src/server/services/favorites.js +5 -3
  100. package/src/server/services/gemini-channels.js +37 -15
  101. package/src/server/services/gemini-statistics-service.js +2 -2
  102. package/src/server/services/mcp-service.js +350 -9
  103. package/src/server/services/model-detector.js +707 -221
  104. package/src/server/services/network-access.js +80 -0
  105. package/src/server/services/opencode-channels.js +206 -0
  106. package/src/server/services/opencode-gateway-converter.js +639 -0
  107. package/src/server/services/opencode-sessions.js +663 -0
  108. package/src/server/services/opencode-settings-manager.js +342 -0
  109. package/src/server/services/opencode-statistics-service.js +255 -0
  110. package/src/server/services/plugins-service.js +479 -22
  111. package/src/server/services/prompts-service.js +53 -11
  112. package/src/server/services/proxy-runtime.js +1 -1
  113. package/src/server/services/repo-scanner-base.js +1 -1
  114. package/src/server/services/security-config.js +1 -1
  115. package/src/server/services/session-cache.js +1 -1
  116. package/src/server/services/skill-service.js +300 -46
  117. package/src/server/services/speed-test.js +464 -186
  118. package/src/server/services/statistics-service.js +2 -2
  119. package/src/server/services/terminal-commands.js +10 -3
  120. package/src/server/services/terminal-config.js +1 -1
  121. package/src/server/services/ui-config.js +1 -1
  122. package/src/server/services/workspace-service.js +57 -100
  123. package/src/server/websocket-server.js +132 -3
  124. package/src/ui/menu.js +49 -40
  125. package/src/utils/port-helper.js +22 -8
  126. package/src/utils/session.js +5 -4
  127. package/dist/web/assets/icons-BxudHPiX.js +0 -1
  128. package/dist/web/assets/index-D2VfwJBa.js +0 -14
  129. package/dist/web/assets/index-oXBzu0bd.css +0 -41
  130. package/dist/web/assets/naive-ui-DT-Uur8K.js +0 -1
  131. package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
  132. package/src/server/api/permissions.js +0 -385
  133. package/src/server/services/permission-templates-service.js +0 -308
@@ -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;
@@ -159,7 +160,7 @@ async function startProxyServer(options = {}) {
159
160
 
160
161
  try {
161
162
  const config = loadConfig();
162
- const port = config.ports?.proxy || 10088;
163
+ const port = config.ports?.proxy || 20088;
163
164
  currentPort = port;
164
165
 
165
166
  proxyApp = express();
@@ -185,9 +186,10 @@ async function startProxyServer(options = {}) {
185
186
  });
186
187
 
187
188
  proxyReq.removeHeader('x-api-key');
188
- proxyReq.setHeader('x-api-key', selectedChannel.apiKey);
189
+ const effectiveKey = req.effectiveApiKey;
190
+ proxyReq.setHeader('x-api-key', effectiveKey);
189
191
  proxyReq.removeHeader('authorization');
190
- proxyReq.setHeader('authorization', `Bearer ${selectedChannel.apiKey}`);
192
+ proxyReq.setHeader('authorization', `Bearer ${effectiveKey}`);
191
193
 
192
194
  if (!proxyReq.getHeader('anthropic-version')) {
193
195
  proxyReq.setHeader('anthropic-version', '2023-06-01');
@@ -219,6 +221,30 @@ async function startProxyServer(options = {}) {
219
221
 
220
222
  req.selectedChannel = channel;
221
223
  req.sessionId = sessionId || null;
224
+ let released = false;
225
+
226
+ const release = () => {
227
+ if (released) return;
228
+ released = true;
229
+ releaseChannel(channel.id, 'claude');
230
+ // 广播调度状态(请求结束)
231
+ broadcastSchedulerState('claude', getSchedulerState('claude'));
232
+ };
233
+
234
+ req.__releaseChannel = release;
235
+
236
+ res.on('close', release);
237
+ res.on('error', release);
238
+
239
+ const effectiveKey = getEffectiveApiKey(channel);
240
+ if (!effectiveKey) {
241
+ release();
242
+ return res.status(401).json({
243
+ error: 'API key not configured or expired. Please update your channel key.',
244
+ type: 'authentication_error'
245
+ });
246
+ }
247
+ req.effectiveApiKey = effectiveKey;
222
248
 
223
249
  // 应用模型重定向(当 proxy 开启时)
224
250
  if (req.body && req.body.model) {
@@ -240,21 +266,6 @@ async function startProxyServer(options = {}) {
240
266
  }
241
267
  }
242
268
 
243
- let released = false;
244
-
245
- const release = () => {
246
- if (released) return;
247
- released = true;
248
- releaseChannel(channel.id, 'claude');
249
- // 广播调度状态(请求结束)
250
- broadcastSchedulerState('claude', getSchedulerState('claude'));
251
- };
252
-
253
- req.__releaseChannel = release;
254
-
255
- res.on('close', release);
256
- res.on('error', release);
257
-
258
269
  const proxyOptions = {
259
270
  target: channel.baseUrl,
260
271
  changeOrigin: true,
@@ -531,7 +542,7 @@ function getProxyStatus() {
531
542
  return {
532
543
  running: !!proxyServer,
533
544
  port: currentPort,
534
- defaultPort: config.ports?.proxy || 10088,
545
+ defaultPort: config.ports?.proxy || 20088,
535
546
  startTime,
536
547
  runtime
537
548
  };
@@ -1,11 +1,7 @@
1
1
  /**
2
2
  * Agents 服务
3
3
  *
4
- * 管理 Claude Code 自定义代理的 CRUD 操作
5
- * 代理目录:
6
- * - 用户级: ~/.claude/agents/
7
- * - 项目级: .claude/agents/
8
- *
4
+ * 管理 Claude/OpenCode 自定义代理的 CRUD 操作
9
5
  * 支持从 GitHub 仓库扫描和安装代理
10
6
  */
11
7
 
@@ -13,12 +9,37 @@ const fs = require('fs');
13
9
  const path = require('path');
14
10
  const os = require('os');
15
11
  const { RepoScannerBase } = require('./repo-scanner-base');
16
-
17
- // 代理目录路径
18
- const USER_AGENTS_DIR = path.join(os.homedir(), '.claude', 'agents');
12
+ const { NATIVE_PATHS } = require('../../config/paths');
19
13
 
20
14
  // 默认仓库源
21
15
  const DEFAULT_REPOS = [];
16
+ const SUPPORTED_PLATFORMS = ['claude', 'opencode'];
17
+ const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
18
+
19
+ const PLATFORM_CONFIG = {
20
+ claude: {
21
+ userAgentsDir: path.join(os.homedir(), '.claude', 'agents'),
22
+ projectAgentsDir: (projectPath) => path.join(projectPath, '.claude', 'agents'),
23
+ repoType: 'agents'
24
+ },
25
+ opencode: {
26
+ userAgentsDir: path.join(OPENCODE_CONFIG_DIR, 'agents'),
27
+ legacyUserAgentsDir: path.join(OPENCODE_CONFIG_DIR, 'agent'),
28
+ projectAgentsDir: (projectPath) => {
29
+ const modern = path.join(projectPath, '.opencode', 'agents');
30
+ const legacy = path.join(projectPath, '.opencode', 'agent');
31
+ if (fs.existsSync(legacy) && !fs.existsSync(modern)) {
32
+ return legacy;
33
+ }
34
+ return modern;
35
+ },
36
+ repoType: 'opencode-agents'
37
+ }
38
+ };
39
+
40
+ function normalizePlatform(platform) {
41
+ return SUPPORTED_PLATFORMS.includes(platform) ? platform : 'claude';
42
+ }
22
43
 
23
44
  /**
24
45
  * 确保目录存在
@@ -74,11 +95,11 @@ function parseFrontmatter(content) {
74
95
  /**
75
96
  * 生成 frontmatter 字符串
76
97
  */
77
- function generateFrontmatter(data) {
98
+ function generateFrontmatter(data, platform = 'claude') {
78
99
  const lines = ['---'];
79
100
 
80
- // 必需字段
81
- if (data.name) {
101
+ // Claude 下写入 name,OpenCode 以文件名作为 agent id
102
+ if (platform !== 'opencode' && data.name) {
82
103
  lines.push(`name: ${data.name}`);
83
104
  }
84
105
  if (data.description) {
@@ -164,10 +185,10 @@ function scanAgentsDir(dir, basePath, scope) {
164
185
  * Agents 仓库扫描器
165
186
  */
166
187
  class AgentsRepoScanner extends RepoScannerBase {
167
- constructor() {
188
+ constructor(platform, installDir) {
168
189
  super({
169
- type: 'agents',
170
- installDir: USER_AGENTS_DIR,
190
+ type: PLATFORM_CONFIG[platform]?.repoType || 'agents',
191
+ installDir,
171
192
  markerFile: null, // 直接扫描 .md 文件
172
193
  fileExtension: '.md',
173
194
  defaultRepos: DEFAULT_REPOS
@@ -248,12 +269,28 @@ class AgentsRepoScanner extends RepoScannerBase {
248
269
  * Agents 服务类
249
270
  */
250
271
  class AgentsService {
251
- constructor() {
252
- this.userAgentsDir = USER_AGENTS_DIR;
253
- this.repoScanner = new AgentsRepoScanner();
272
+ constructor(platform = 'claude') {
273
+ this.platform = normalizePlatform(platform);
274
+ const config = PLATFORM_CONFIG[this.platform];
275
+
276
+ this.userAgentsDir = config.userAgentsDir;
277
+ if (this.platform === 'opencode') {
278
+ const legacyUserDir = config.legacyUserAgentsDir;
279
+ if (legacyUserDir && fs.existsSync(legacyUserDir) && !fs.existsSync(this.userAgentsDir)) {
280
+ this.userAgentsDir = legacyUserDir;
281
+ }
282
+ }
283
+
284
+ this.projectAgentsDir = config.projectAgentsDir;
285
+ this.repoScanner = new AgentsRepoScanner(this.platform, this.userAgentsDir);
254
286
  ensureDir(this.userAgentsDir);
255
287
  }
256
288
 
289
+ getProjectAgentsDir(projectPath) {
290
+ if (!projectPath) return null;
291
+ return this.projectAgentsDir(projectPath);
292
+ }
293
+
257
294
  /**
258
295
  * 获取所有代理列表
259
296
  * @param {string} projectPath - 项目路径(可选,用于获取项目级代理)
@@ -267,7 +304,7 @@ class AgentsService {
267
304
 
268
305
  // 获取项目级代理(如果提供了项目路径)
269
306
  if (projectPath) {
270
- const projectAgentsDir = path.join(projectPath, '.claude', 'agents');
307
+ const projectAgentsDir = this.getProjectAgentsDir(projectPath);
271
308
  const projectAgents = scanAgentsDir(projectAgentsDir, projectAgentsDir, 'project');
272
309
  agents.push(...projectAgents);
273
310
  }
@@ -331,7 +368,7 @@ class AgentsService {
331
368
  getAgent(fileName, scope, projectPath = null) {
332
369
  const baseDir = scope === 'user'
333
370
  ? this.userAgentsDir
334
- : path.join(projectPath, '.claude', 'agents');
371
+ : this.getProjectAgentsDir(projectPath);
335
372
 
336
373
  const filePath = path.join(baseDir, `${fileName}.md`);
337
374
 
@@ -382,7 +419,7 @@ class AgentsService {
382
419
 
383
420
  const baseDir = scope === 'user'
384
421
  ? this.userAgentsDir
385
- : path.join(projectPath, '.claude', 'agents');
422
+ : this.getProjectAgentsDir(projectPath);
386
423
 
387
424
  ensureDir(baseDir);
388
425
 
@@ -400,7 +437,7 @@ class AgentsService {
400
437
  if (permissionMode) frontmatterData.permissionMode = permissionMode;
401
438
  if (skills) frontmatterData.skills = skills;
402
439
 
403
- const content = generateFrontmatter(frontmatterData) + '\n\n' + (systemPrompt || '');
440
+ const content = generateFrontmatter(frontmatterData, this.platform) + '\n\n' + (systemPrompt || '');
404
441
 
405
442
  fs.writeFileSync(filePath, content, 'utf-8');
406
443
 
@@ -413,7 +450,7 @@ class AgentsService {
413
450
  updateAgent({ fileName, scope, projectPath, name, description, tools, model, permissionMode, skills, systemPrompt }) {
414
451
  const baseDir = scope === 'user'
415
452
  ? this.userAgentsDir
416
- : path.join(projectPath, '.claude', 'agents');
453
+ : this.getProjectAgentsDir(projectPath);
417
454
 
418
455
  const filePath = path.join(baseDir, `${fileName}.md`);
419
456
 
@@ -431,7 +468,7 @@ class AgentsService {
431
468
  if (permissionMode) frontmatterData.permissionMode = permissionMode;
432
469
  if (skills) frontmatterData.skills = skills;
433
470
 
434
- const content = generateFrontmatter(frontmatterData) + '\n\n' + (systemPrompt || '');
471
+ const content = generateFrontmatter(frontmatterData, this.platform) + '\n\n' + (systemPrompt || '');
435
472
 
436
473
  fs.writeFileSync(filePath, content, 'utf-8');
437
474
 
@@ -444,7 +481,7 @@ class AgentsService {
444
481
  deleteAgent(fileName, scope, projectPath = null) {
445
482
  const baseDir = scope === 'user'
446
483
  ? this.userAgentsDir
447
- : path.join(projectPath, '.claude', 'agents');
484
+ : this.getProjectAgentsDir(projectPath);
448
485
 
449
486
  const filePath = path.join(baseDir, `${fileName}.md`);
450
487
 
@@ -1,6 +1,7 @@
1
1
  const { getAllChannels } = require('./channels');
2
2
  const { getChannels: getCodexChannels } = require('./codex-channels');
3
3
  const { getChannels: getGeminiChannels } = require('./gemini-channels');
4
+ const { getChannels: getOpenCodeChannels } = require('./opencode-channels');
4
5
  const { isChannelAvailable, getChannelHealthStatus, setOnChannelFrozen } = require('./channel-health');
5
6
 
6
7
  const channelProviders = {
@@ -12,6 +13,10 @@ const channelProviders = {
12
13
  gemini: () => {
13
14
  const data = getGeminiChannels();
14
15
  return Array.isArray(data?.channels) ? data.channels : [];
16
+ },
17
+ opencode: () => {
18
+ const data = getOpenCodeChannels();
19
+ return Array.isArray(data?.channels) ? data.channels : [];
15
20
  }
16
21
  };
17
22
 
@@ -27,7 +32,8 @@ function createState() {
27
32
  const schedulerStates = {
28
33
  claude: createState(),
29
34
  codex: createState(),
30
- gemini: createState()
35
+ gemini: createState(),
36
+ opencode: createState()
31
37
  };
32
38
 
33
39
  function getState(source = 'claude') {
@@ -69,10 +75,8 @@ function refreshChannels(source = 'claude') {
69
75
  state.channels = raw
70
76
  .filter(ch => ch.enabled !== false)
71
77
  .map(ch => ({
72
- id: ch.id,
73
- name: ch.name,
74
- baseUrl: ch.baseUrl,
75
- apiKey: ch.apiKey,
78
+ // 保留渠道完整字段,避免 proxy 等运行时配置在调度层丢失
79
+ ...ch,
76
80
  weight: Math.max(1, Number(ch.weight) || 1),
77
81
  maxConcurrency: ch.maxConcurrency ?? null,
78
82
  modelConfig: ch.modelConfig || null,
@@ -4,7 +4,7 @@ const os = require('os');
4
4
  const { isProxyConfig } = require('./settings-manager');
5
5
 
6
6
  function getChannelsFilePath() {
7
- const dir = path.join(os.homedir(), '.claude', 'cc-tool');
7
+ const dir = path.join(os.homedir(), '.cc-tool');
8
8
  if (!fs.existsSync(dir)) {
9
9
  fs.mkdirSync(dir, { recursive: true });
10
10
  }
@@ -12,7 +12,7 @@ function getChannelsFilePath() {
12
12
  }
13
13
 
14
14
  function getActiveChannelIdPath() {
15
- const dir = path.join(os.homedir(), '.claude', 'cc-tool');
15
+ const dir = path.join(os.homedir(), '.cc-tool');
16
16
  if (!fs.existsSync(dir)) {
17
17
  fs.mkdirSync(dir, { recursive: true });
18
18
  }
@@ -57,6 +57,38 @@ function normalizeNumber(value, defaultValue, max = null) {
57
57
  return num;
58
58
  }
59
59
 
60
+ function normalizeGatewaySourceType(value, fallback = 'claude') {
61
+ const normalized = String(value || '').trim().toLowerCase();
62
+ if (normalized === 'claude') return 'claude';
63
+ if (normalized === 'codex') return 'codex';
64
+ if (normalized === 'gemini') return 'gemini';
65
+ return fallback;
66
+ }
67
+
68
+ function extractApiKeyFromHelper(apiKeyHelper) {
69
+ if (typeof apiKeyHelper !== 'string' || !apiKeyHelper.trim()) {
70
+ return '';
71
+ }
72
+
73
+ const helper = apiKeyHelper.trim();
74
+ let match = helper.match(/^echo\s+["']([^"']+)["']$/);
75
+ if (match && match[1]) {
76
+ return match[1];
77
+ }
78
+
79
+ match = helper.match(/^printf\s+["'][^"']*["']\s+["']([^"']+)["']$/);
80
+ if (match && match[1]) {
81
+ return match[1];
82
+ }
83
+
84
+ return '';
85
+ }
86
+
87
+ function buildApiKeyHelperCommand() {
88
+ // 避免把明文 API Key 写入可执行命令,降低注入风险
89
+ return 'printf "%s" "${ANTHROPIC_AUTH_TOKEN:-${ANTHROPIC_API_KEY:-}}"';
90
+ }
91
+
60
92
  function applyChannelDefaults(channel) {
61
93
  const normalized = { ...channel };
62
94
  if (normalized.enabled === undefined) {
@@ -75,6 +107,8 @@ function applyChannelDefaults(channel) {
75
107
  normalized.maxConcurrency = normalizeNumber(normalized.maxConcurrency, 1, 100);
76
108
  }
77
109
 
110
+ normalized.gatewaySourceType = normalizeGatewaySourceType(normalized.gatewaySourceType, 'claude');
111
+
78
112
  return normalized;
79
113
  }
80
114
 
@@ -139,10 +173,7 @@ function getCurrentSettings() {
139
173
  '';
140
174
 
141
175
  if (!apiKey && settings.apiKeyHelper) {
142
- const match = settings.apiKeyHelper.match(/['"]([^'"]+)['"]/);
143
- if (match && match[1]) {
144
- apiKey = match[1];
145
- }
176
+ apiKey = extractApiKeyFromHelper(settings.apiKeyHelper);
146
177
  }
147
178
 
148
179
  if (!baseUrl && !apiKey) {
@@ -173,6 +204,23 @@ function getAllChannels() {
173
204
  return data.channels;
174
205
  }
175
206
 
207
+ function getCurrentChannel() {
208
+ const channels = getAllChannels();
209
+ if (!Array.isArray(channels) || channels.length === 0) {
210
+ return null;
211
+ }
212
+
213
+ const activeChannelId = loadActiveChannelId();
214
+ if (activeChannelId) {
215
+ const matched = channels.find(ch => ch.id === activeChannelId);
216
+ if (matched) {
217
+ return matched;
218
+ }
219
+ }
220
+
221
+ return channels.find(ch => ch.enabled !== false) || channels[0];
222
+ }
223
+
176
224
  function createChannel(name, baseUrl, apiKey, websiteUrl, extraConfig = {}) {
177
225
  const data = loadChannels();
178
226
  const newChannel = applyChannelDefaults({
@@ -189,7 +237,8 @@ function createChannel(name, baseUrl, apiKey, websiteUrl, extraConfig = {}) {
189
237
  modelConfig: extraConfig.modelConfig || null,
190
238
  modelRedirects: extraConfig.modelRedirects || [],
191
239
  proxyUrl: extraConfig.proxyUrl || '',
192
- speedTestModel: extraConfig.speedTestModel || null
240
+ speedTestModel: extraConfig.speedTestModel || null,
241
+ gatewaySourceType: normalizeGatewaySourceType(extraConfig.gatewaySourceType, 'claude')
193
242
  });
194
243
 
195
244
  data.channels.push(newChannel);
@@ -209,7 +258,7 @@ function updateChannel(id, updates) {
209
258
  const oldChannel = { ...data.channels[index] };
210
259
 
211
260
  const merged = { ...data.channels[index], ...updates };
212
- data.channels[index] = applyChannelDefaults({
261
+ const nextChannel = applyChannelDefaults({
213
262
  ...merged,
214
263
  weight: merged.weight,
215
264
  maxConcurrency: merged.maxConcurrency,
@@ -219,8 +268,10 @@ function updateChannel(id, updates) {
219
268
  modelRedirects: merged.modelRedirects || [],
220
269
  proxyUrl: merged.proxyUrl,
221
270
  speedTestModel: merged.speedTestModel,
271
+ gatewaySourceType: normalizeGatewaySourceType(merged.gatewaySourceType, 'claude'),
222
272
  updatedAt: Date.now()
223
273
  });
274
+ data.channels[index] = nextChannel;
224
275
 
225
276
  // Get proxy status
226
277
  const { getProxyStatus } = require('../proxy-server');
@@ -264,7 +315,7 @@ function updateChannel(id, updates) {
264
315
  return data.channels[index];
265
316
  }
266
317
 
267
- function deleteChannel(id) {
318
+ async function deleteChannel(id) {
268
319
  const data = loadChannels();
269
320
  const index = data.channels.findIndex(ch => ch.id === id);
270
321
 
@@ -274,6 +325,7 @@ function deleteChannel(id) {
274
325
 
275
326
  data.channels.splice(index, 1);
276
327
  saveChannels(data);
328
+
277
329
  return { success: true };
278
330
  }
279
331
 
@@ -347,7 +399,7 @@ function updateClaudeSettingsWithModelConfig(channel) {
347
399
  delete settings.env.NO_PROXY;
348
400
  }
349
401
 
350
- settings.apiKeyHelper = `echo '${apiKey}'`;
402
+ settings.apiKeyHelper = buildApiKeyHelperCommand();
351
403
 
352
404
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
353
405
  }
@@ -376,13 +428,18 @@ function updateClaudeSettings(baseUrl, apiKey) {
376
428
  settings.env.ANTHROPIC_API_KEY = apiKey;
377
429
  }
378
430
 
379
- settings.apiKeyHelper = `echo '${apiKey}'`;
431
+ settings.apiKeyHelper = buildApiKeyHelperCommand();
380
432
 
381
433
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
382
434
  }
383
435
 
436
+ function getEffectiveApiKey(channel) {
437
+ return channel.apiKey || null;
438
+ }
439
+
384
440
  module.exports = {
385
441
  getAllChannels,
442
+ getCurrentChannel,
386
443
  getCurrentSettings,
387
444
  createChannel,
388
445
  updateChannel,
@@ -390,5 +447,6 @@ module.exports = {
390
447
  applyChannelToSettings,
391
448
  getBestChannelForRestore,
392
449
  updateClaudeSettings,
393
- updateClaudeSettingsWithModelConfig
450
+ updateClaudeSettingsWithModelConfig,
451
+ getEffectiveApiKey
394
452
  };
@@ -20,9 +20,17 @@ const { injectEnvToShell, removeEnvFromShell, isProxyConfig } = require('./codex
20
20
  * - 使用 weight 和 maxConcurrency 控制负载均衡
21
21
  */
22
22
 
23
+ function normalizeGatewaySourceType(value, fallback = 'codex') {
24
+ const normalized = String(value || '').trim().toLowerCase();
25
+ if (normalized === 'claude') return 'claude';
26
+ if (normalized === 'codex') return 'codex';
27
+ if (normalized === 'gemini') return 'gemini';
28
+ return fallback;
29
+ }
30
+
23
31
  // 获取渠道存储文件路径
24
32
  function getChannelsFilePath() {
25
- const ccToolDir = path.join(os.homedir(), '.claude', 'cc-tool');
33
+ const ccToolDir = path.join(os.homedir(), '.cc-tool');
26
34
  if (!fs.existsSync(ccToolDir)) {
27
35
  fs.mkdirSync(ccToolDir, { recursive: true });
28
36
  }
@@ -43,14 +51,17 @@ function loadChannels() {
43
51
  const data = JSON.parse(content);
44
52
  // 确保渠道有 enabled 字段(兼容旧数据)
45
53
  if (data.channels) {
46
- data.channels = data.channels.map(ch => ({
47
- ...ch,
48
- enabled: ch.enabled !== false, // 默认启用
49
- weight: ch.weight || 1,
50
- maxConcurrency: ch.maxConcurrency || null,
51
- modelRedirects: ch.modelRedirects || [],
52
- speedTestModel: ch.speedTestModel || null
53
- }));
54
+ data.channels = data.channels.map(ch => {
55
+ return {
56
+ ...ch,
57
+ enabled: ch.enabled !== false, // 默认启用
58
+ weight: ch.weight || 1,
59
+ maxConcurrency: ch.maxConcurrency || null,
60
+ modelRedirects: ch.modelRedirects || [],
61
+ speedTestModel: ch.speedTestModel || null,
62
+ gatewaySourceType: normalizeGatewaySourceType(ch.gatewaySourceType, 'codex')
63
+ };
64
+ });
54
65
  }
55
66
  return data;
56
67
  } catch (err) {
@@ -110,6 +121,7 @@ function initializeFromConfig() {
110
121
  enabled: config.model_provider === providerKey, // 当前激活的渠道启用
111
122
  weight: 1,
112
123
  maxConcurrency: null,
124
+ gatewaySourceType: 'codex',
113
125
  createdAt: Date.now(),
114
126
  updatedAt: Date.now()
115
127
  });
@@ -179,6 +191,7 @@ function createChannel(name, providerKey, baseUrl, apiKey, wireApi = 'responses'
179
191
  maxConcurrency: extraConfig.maxConcurrency || null,
180
192
  modelRedirects: extraConfig.modelRedirects || [],
181
193
  speedTestModel: extraConfig.speedTestModel || null,
194
+ gatewaySourceType: normalizeGatewaySourceType(extraConfig.gatewaySourceType, 'codex'),
182
195
  createdAt: Date.now(),
183
196
  updatedAt: Date.now()
184
197
  };
@@ -187,8 +200,8 @@ function createChannel(name, providerKey, baseUrl, apiKey, wireApi = 'responses'
187
200
  saveChannels(data);
188
201
 
189
202
  // 注入该渠道的环境变量(用于直接使用 codex 命令)
190
- if (apiKey && envKey) {
191
- const injectResult = injectEnvToShell(envKey, apiKey);
203
+ if (newChannel.enabled !== false && newChannel.apiKey && envKey) {
204
+ const injectResult = injectEnvToShell(envKey, newChannel.apiKey);
192
205
  if (injectResult.success) {
193
206
  console.log(`[Codex Channels] Environment variable ${envKey} injected for new channel`);
194
207
  } else {
@@ -228,6 +241,7 @@ function updateChannel(channelId, updates) {
228
241
  createdAt: oldChannel.createdAt, // 保持创建时间
229
242
  modelRedirects: merged.modelRedirects || [],
230
243
  speedTestModel: merged.speedTestModel !== undefined ? merged.speedTestModel : (oldChannel.speedTestModel || null),
244
+ gatewaySourceType: normalizeGatewaySourceType(merged.gatewaySourceType, 'codex'),
231
245
  updatedAt: Date.now()
232
246
  };
233
247
 
@@ -269,19 +283,23 @@ function updateChannel(channelId, updates) {
269
283
  // 如果 envKey 或 apiKey 变化,需要更新环境变量
270
284
  const oldEnvKey = oldChannel.envKey;
271
285
  const newEnvKey = newChannel.envKey;
272
- const oldApiKey = oldChannel.apiKey;
273
286
  const newApiKey = newChannel.apiKey;
274
-
275
- // 如果 envKey 改变,删除旧的,注入新的
276
- if (oldEnvKey !== newEnvKey && oldEnvKey) {
287
+ const shouldRemoveOldEnv =
288
+ !!oldEnvKey && (
289
+ oldEnvKey !== newEnvKey ||
290
+ !newApiKey ||
291
+ newChannel.enabled === false
292
+ );
293
+
294
+ // 禁用或 key 变化时都要清理旧环境变量,避免残留
295
+ if (shouldRemoveOldEnv) {
277
296
  const removeResult = removeEnvFromShell(oldEnvKey);
278
297
  if (removeResult.success) {
279
298
  console.log(`[Codex Channels] Old environment variable ${oldEnvKey} removed`);
280
299
  }
281
300
  }
282
301
 
283
- // 如果有新的 API Key,注入到环境变量
284
- if (newApiKey && newEnvKey) {
302
+ if (newChannel.enabled !== false && newApiKey && newEnvKey) {
285
303
  const injectResult = injectEnvToShell(newEnvKey, newApiKey);
286
304
  if (injectResult.success) {
287
305
  console.log(`[Codex Channels] Environment variable ${newEnvKey} updated`);
@@ -295,7 +313,7 @@ function updateChannel(channelId, updates) {
295
313
  }
296
314
 
297
315
  // 删除渠道
298
- function deleteChannel(channelId) {
316
+ async function deleteChannel(channelId) {
299
317
  const data = loadChannels();
300
318
 
301
319
  const index = data.channels.findIndex(c => c.id === channelId);
@@ -397,7 +415,7 @@ function writeCodexConfigForMultiChannel(allChannels) {
397
415
  // 回退默认的代理配置(使用默认端口),确保 provider 存在
398
416
  config.model_providers['cc-proxy'] = {
399
417
  name: 'cc-proxy',
400
- base_url: 'http://127.0.0.1:10089/v1',
418
+ base_url: 'http://127.0.0.1:20089/v1',
401
419
  wire_api: 'responses',
402
420
  env_key: 'CC_PROXY_KEY'
403
421
  };
@@ -448,6 +466,12 @@ ${tomlContent}`;
448
466
  }
449
467
 
450
468
  // 更新所有渠道的 API Key
469
+ for (const channel of allChannels) {
470
+ if (channel.envKey && !channel.apiKey) {
471
+ delete auth[channel.envKey];
472
+ }
473
+ }
474
+
451
475
  for (const channel of allChannels) {
452
476
  if (channel.apiKey) {
453
477
  auth[channel.envKey] = channel.apiKey;
@@ -508,7 +532,10 @@ function syncAllChannelEnvVars() {
508
532
  const results = [];
509
533
 
510
534
  for (const channel of channels) {
511
- if (channel.apiKey && channel.envKey) {
535
+ if (!channel.envKey) continue;
536
+
537
+ const shouldInject = channel.enabled !== false && !!channel.apiKey;
538
+ if (shouldInject) {
512
539
  const injectResult = injectEnvToShell(channel.envKey, channel.apiKey);
513
540
  if (injectResult.success) {
514
541
  syncedCount++;
@@ -516,7 +543,11 @@ function syncAllChannelEnvVars() {
516
543
  } else {
517
544
  results.push({ envKey: channel.envKey, success: false, error: injectResult.error });
518
545
  }
546
+ continue;
519
547
  }
548
+
549
+ // 清理已停用或缺失 key 的渠道环境变量,避免残留
550
+ removeEnvFromShell(channel.envKey);
520
551
  }
521
552
 
522
553
  console.log(`[Codex Channels] Synced ${syncedCount} environment variables`);
@@ -620,19 +651,21 @@ ${tomlContent}`;
620
651
  }
621
652
  }
622
653
 
623
- // 添加当前渠道的 API Key
624
654
  if (channel.apiKey && channel.envKey) {
625
655
  auth[channel.envKey] = channel.apiKey;
656
+ } else if (channel.envKey) {
657
+ delete auth[channel.envKey];
626
658
  }
627
659
 
628
660
  fs.writeFileSync(authPath, JSON.stringify(auth, null, 2), 'utf8');
629
661
 
630
- // 注入环境变量到 shell 配置文件
631
662
  if (channel.apiKey && channel.envKey) {
632
663
  const injectResult = injectEnvToShell(channel.envKey, channel.apiKey);
633
664
  if (injectResult.success) {
634
665
  console.log(`[Codex Channels] Environment variable ${channel.envKey} injected`);
635
666
  }
667
+ } else if (channel.envKey) {
668
+ removeEnvFromShell(channel.envKey);
636
669
  }
637
670
 
638
671
  return channel;
@@ -649,6 +682,10 @@ try {
649
682
  console.warn('[Codex Channels] Auto sync env vars failed:', err.message);
650
683
  }
651
684
 
685
+ function getEffectiveApiKey(channel) {
686
+ return channel.apiKey || null;
687
+ }
688
+
652
689
  module.exports = {
653
690
  getChannels,
654
691
  createChannel,
@@ -658,5 +695,6 @@ module.exports = {
658
695
  saveChannelOrder,
659
696
  syncAllChannelEnvVars,
660
697
  writeCodexConfigForMultiChannel,
661
- applyChannelToSettings
698
+ applyChannelToSettings,
699
+ getEffectiveApiKey
662
700
  };