@adversity/coding-tool-x 3.1.0 → 3.1.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 (142) hide show
  1. package/CHANGELOG.md +39 -18
  2. package/README.md +8 -8
  3. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  4. package/dist/web/assets/ConfigTemplates-DvcbKKdS.js +1 -0
  5. package/dist/web/assets/Home-BJKPCBuk.css +1 -0
  6. package/dist/web/assets/Home-Cw-F_Wnu.js +1 -0
  7. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  8. package/dist/web/assets/PluginManager-jy_4GVxI.js +1 -0
  9. package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
  10. package/dist/web/assets/ProjectList-Df1-NcNr.js +1 -0
  11. package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
  12. package/dist/web/assets/SessionList-UWcZtC2r.js +1 -0
  13. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  14. package/dist/web/assets/SkillManager-IRdseMKB.js +1 -0
  15. package/dist/web/assets/Terminal-BasTyDut.js +1 -0
  16. package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
  17. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  18. package/dist/web/assets/WorkspaceManager-D-D2kK1V.js +1 -0
  19. package/dist/web/assets/icons-kcfLIMBB.js +1 -0
  20. package/dist/web/assets/index-CoB3zF0K.css +1 -0
  21. package/dist/web/assets/index-CryrSLv8.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 +81 -12
  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/update.js +97 -0
  45. package/src/commands/workspace.js +1 -1
  46. package/src/config/default.js +41 -2
  47. package/src/config/loader.js +74 -8
  48. package/src/config/model-metadata.js +415 -0
  49. package/src/config/model-pricing.js +23 -93
  50. package/src/config/paths.js +105 -33
  51. package/src/index.js +64 -3
  52. package/src/plugins/constants.js +3 -2
  53. package/src/plugins/plugin-api.js +1 -1
  54. package/src/reset-config.js +4 -2
  55. package/src/server/api/agents.js +57 -14
  56. package/src/server/api/channels.js +112 -33
  57. package/src/server/api/codex-channels.js +111 -18
  58. package/src/server/api/codex-proxy.js +14 -8
  59. package/src/server/api/commands.js +71 -18
  60. package/src/server/api/config-export.js +0 -6
  61. package/src/server/api/config-registry.js +11 -3
  62. package/src/server/api/config.js +376 -5
  63. package/src/server/api/convert.js +133 -0
  64. package/src/server/api/dashboard.js +22 -6
  65. package/src/server/api/gemini-channels.js +107 -18
  66. package/src/server/api/gemini-proxy.js +14 -8
  67. package/src/server/api/gemini-sessions.js +1 -1
  68. package/src/server/api/health-check.js +4 -3
  69. package/src/server/api/mcp.js +3 -3
  70. package/src/server/api/opencode-channels.js +497 -0
  71. package/src/server/api/opencode-projects.js +99 -0
  72. package/src/server/api/opencode-proxy.js +207 -0
  73. package/src/server/api/opencode-sessions.js +345 -0
  74. package/src/server/api/opencode-statistics.js +57 -0
  75. package/src/server/api/plugins.js +66 -19
  76. package/src/server/api/prompts.js +2 -2
  77. package/src/server/api/proxy.js +7 -4
  78. package/src/server/api/sessions.js +3 -0
  79. package/src/server/api/settings.js +111 -0
  80. package/src/server/api/skills.js +69 -18
  81. package/src/server/api/workspaces.js +78 -6
  82. package/src/server/codex-proxy-server.js +36 -22
  83. package/src/server/dev-server.js +1 -1
  84. package/src/server/gemini-proxy-server.js +21 -7
  85. package/src/server/index.js +174 -58
  86. package/src/server/opencode-proxy-server.js +5486 -0
  87. package/src/server/proxy-server.js +33 -22
  88. package/src/server/services/agents-service.js +61 -24
  89. package/src/server/services/channel-scheduler.js +9 -5
  90. package/src/server/services/channels.js +64 -37
  91. package/src/server/services/codex-channels.js +56 -43
  92. package/src/server/services/codex-sessions.js +105 -6
  93. package/src/server/services/codex-settings-manager.js +271 -49
  94. package/src/server/services/codex-statistics-service.js +2 -2
  95. package/src/server/services/commands-service.js +84 -25
  96. package/src/server/services/config-export-service.js +7 -45
  97. package/src/server/services/config-registry-service.js +63 -17
  98. package/src/server/services/config-sync-manager.js +160 -7
  99. package/src/server/services/config-templates-service.js +204 -51
  100. package/src/server/services/env-checker.js +50 -13
  101. package/src/server/services/env-manager.js +155 -19
  102. package/src/server/services/favorites.js +5 -3
  103. package/src/server/services/gemini-channels.js +33 -44
  104. package/src/server/services/gemini-statistics-service.js +2 -2
  105. package/src/server/services/mcp-service.js +350 -9
  106. package/src/server/services/model-detector.js +707 -221
  107. package/src/server/services/network-access.js +80 -0
  108. package/src/server/services/opencode-channels.js +208 -0
  109. package/src/server/services/opencode-gateway-converter.js +639 -0
  110. package/src/server/services/opencode-sessions.js +931 -0
  111. package/src/server/services/opencode-settings-manager.js +478 -0
  112. package/src/server/services/opencode-statistics-service.js +255 -0
  113. package/src/server/services/plugins-service.js +479 -22
  114. package/src/server/services/prompts-service.js +53 -11
  115. package/src/server/services/proxy-runtime.js +1 -1
  116. package/src/server/services/repo-scanner-base.js +1 -1
  117. package/src/server/services/response-decoder.js +21 -0
  118. package/src/server/services/security-config.js +1 -1
  119. package/src/server/services/session-cache.js +1 -1
  120. package/src/server/services/skill-service.js +300 -46
  121. package/src/server/services/speed-test.js +464 -186
  122. package/src/server/services/statistics-service.js +2 -2
  123. package/src/server/services/terminal-commands.js +10 -3
  124. package/src/server/services/terminal-config.js +1 -1
  125. package/src/server/services/ui-config.js +1 -1
  126. package/src/server/services/workspace-service.js +57 -100
  127. package/src/server/websocket-server.js +156 -8
  128. package/src/ui/menu.js +49 -40
  129. package/src/utils/port-helper.js +22 -8
  130. package/src/utils/session.js +5 -4
  131. package/dist/web/assets/icons-CO_2OFES.js +0 -1
  132. package/dist/web/assets/index-DI8QOi-E.js +0 -14
  133. package/dist/web/assets/index-uLHGdeZh.css +0 -41
  134. package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
  135. package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
  136. package/src/server/api/oauth.js +0 -294
  137. package/src/server/api/permissions.js +0 -385
  138. package/src/server/config/oauth-providers.js +0 -68
  139. package/src/server/services/oauth-callback-server.js +0 -284
  140. package/src/server/services/oauth-service.js +0 -378
  141. package/src/server/services/oauth-token-storage.js +0 -135
  142. package/src/server/services/permission-templates-service.js +0 -308
@@ -11,6 +11,7 @@ const DEFAULT_CONFIG = require('../config/default');
11
11
  const { resolvePricing, resolveModelPricing } = require('./utils/pricing');
12
12
  const { recordRequest } = require('./services/statistics-service');
13
13
  const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('./services/proxy-runtime');
14
+ const { createDecodedStream } = require('./services/response-decoder');
14
15
  const eventBus = require('../plugins/event-bus');
15
16
  const { CLAUDE_MODEL_PRICING } = require('../config/model-pricing');
16
17
  const { getEffectiveApiKey } = require('./services/channels');
@@ -160,7 +161,7 @@ async function startProxyServer(options = {}) {
160
161
 
161
162
  try {
162
163
  const config = loadConfig();
163
- const port = config.ports?.proxy || 10088;
164
+ const port = config.ports?.proxy || 20088;
164
165
  currentPort = port;
165
166
 
166
167
  proxyApp = express();
@@ -186,7 +187,7 @@ async function startProxyServer(options = {}) {
186
187
  });
187
188
 
188
189
  proxyReq.removeHeader('x-api-key');
189
- const effectiveKey = getEffectiveApiKey(selectedChannel);
190
+ const effectiveKey = req.effectiveApiKey;
190
191
  proxyReq.setHeader('x-api-key', effectiveKey);
191
192
  proxyReq.removeHeader('authorization');
192
193
  proxyReq.setHeader('authorization', `Bearer ${effectiveKey}`);
@@ -221,6 +222,30 @@ async function startProxyServer(options = {}) {
221
222
 
222
223
  req.selectedChannel = channel;
223
224
  req.sessionId = sessionId || null;
225
+ let released = false;
226
+
227
+ const release = () => {
228
+ if (released) return;
229
+ released = true;
230
+ releaseChannel(channel.id, 'claude');
231
+ // 广播调度状态(请求结束)
232
+ broadcastSchedulerState('claude', getSchedulerState('claude'));
233
+ };
234
+
235
+ req.__releaseChannel = release;
236
+
237
+ res.on('close', release);
238
+ res.on('error', release);
239
+
240
+ const effectiveKey = getEffectiveApiKey(channel);
241
+ if (!effectiveKey) {
242
+ release();
243
+ return res.status(401).json({
244
+ error: 'API key not configured or expired. Please update your channel key.',
245
+ type: 'authentication_error'
246
+ });
247
+ }
248
+ req.effectiveApiKey = effectiveKey;
224
249
 
225
250
  // 应用模型重定向(当 proxy 开启时)
226
251
  if (req.body && req.body.model) {
@@ -242,21 +267,6 @@ async function startProxyServer(options = {}) {
242
267
  }
243
268
  }
244
269
 
245
- let released = false;
246
-
247
- const release = () => {
248
- if (released) return;
249
- released = true;
250
- releaseChannel(channel.id, 'claude');
251
- // 广播调度状态(请求结束)
252
- broadcastSchedulerState('claude', getSchedulerState('claude'));
253
- };
254
-
255
- req.__releaseChannel = release;
256
-
257
- res.on('close', release);
258
- res.on('error', release);
259
-
260
270
  const proxyOptions = {
261
271
  target: channel.baseUrl,
262
272
  changeOrigin: true,
@@ -325,11 +335,12 @@ async function startProxyServer(options = {}) {
325
335
  cacheRead: 0,
326
336
  model: ''
327
337
  };
338
+ const parsedStream = createDecodedStream(proxyRes);
328
339
 
329
- proxyRes.on('data', (chunk) => {
340
+ parsedStream.on('data', (chunk) => {
330
341
  if (isResponseClosed) return;
331
342
 
332
- buffer += chunk.toString();
343
+ buffer += chunk.toString('utf8');
333
344
 
334
345
  const events = buffer.split('\n\n');
335
346
  buffer = events.pop() || '';
@@ -439,9 +450,9 @@ async function startProxyServer(options = {}) {
439
450
  }
440
451
  };
441
452
 
442
- proxyRes.on('end', finalize);
453
+ parsedStream.on('end', finalize);
443
454
 
444
- proxyRes.on('error', (err) => {
455
+ parsedStream.on('error', (err) => {
445
456
  if (err.code !== 'EPIPE' && err.code !== 'ECONNRESET') {
446
457
  console.error('Proxy response error:', err);
447
458
  }
@@ -533,7 +544,7 @@ function getProxyStatus() {
533
544
  return {
534
545
  running: !!proxyServer,
535
546
  port: currentPort,
536
- defaultPort: config.ports?.proxy || 10088,
547
+ defaultPort: config.ports?.proxy || 20088,
537
548
  startTime,
538
549
  runtime
539
550
  };
@@ -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) {
@@ -65,11 +97,6 @@ function applyChannelDefaults(channel) {
65
97
  normalized.enabled = !!normalized.enabled;
66
98
  }
67
99
 
68
- // OAuth 字段默认值(向后兼容)
69
- if (!normalized.authType) {
70
- normalized.authType = 'apiKey';
71
- }
72
-
73
100
  normalized.weight = normalizeNumber(normalized.weight, 1, 100);
74
101
 
75
102
  if (normalized.maxConcurrency === undefined ||
@@ -80,6 +107,8 @@ function applyChannelDefaults(channel) {
80
107
  normalized.maxConcurrency = normalizeNumber(normalized.maxConcurrency, 1, 100);
81
108
  }
82
109
 
110
+ normalized.gatewaySourceType = normalizeGatewaySourceType(normalized.gatewaySourceType, 'claude');
111
+
83
112
  return normalized;
84
113
  }
85
114
 
@@ -144,10 +173,7 @@ function getCurrentSettings() {
144
173
  '';
145
174
 
146
175
  if (!apiKey && settings.apiKeyHelper) {
147
- const match = settings.apiKeyHelper.match(/['"]([^'"]+)['"]/);
148
- if (match && match[1]) {
149
- apiKey = match[1];
150
- }
176
+ apiKey = extractApiKeyFromHelper(settings.apiKeyHelper);
151
177
  }
152
178
 
153
179
  if (!baseUrl && !apiKey) {
@@ -178,6 +204,23 @@ function getAllChannels() {
178
204
  return data.channels;
179
205
  }
180
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
+
181
224
  function createChannel(name, baseUrl, apiKey, websiteUrl, extraConfig = {}) {
182
225
  const data = loadChannels();
183
226
  const newChannel = applyChannelDefaults({
@@ -195,10 +238,7 @@ function createChannel(name, baseUrl, apiKey, websiteUrl, extraConfig = {}) {
195
238
  modelRedirects: extraConfig.modelRedirects || [],
196
239
  proxyUrl: extraConfig.proxyUrl || '',
197
240
  speedTestModel: extraConfig.speedTestModel || null,
198
- // OAuth 支持
199
- authType: extraConfig.authType || 'apiKey',
200
- oauthProvider: extraConfig.oauthProvider || null,
201
- oauthTokenId: extraConfig.oauthTokenId || null
241
+ gatewaySourceType: normalizeGatewaySourceType(extraConfig.gatewaySourceType, 'claude')
202
242
  });
203
243
 
204
244
  data.channels.push(newChannel);
@@ -218,7 +258,7 @@ function updateChannel(id, updates) {
218
258
  const oldChannel = { ...data.channels[index] };
219
259
 
220
260
  const merged = { ...data.channels[index], ...updates };
221
- data.channels[index] = applyChannelDefaults({
261
+ const nextChannel = applyChannelDefaults({
222
262
  ...merged,
223
263
  weight: merged.weight,
224
264
  maxConcurrency: merged.maxConcurrency,
@@ -228,8 +268,10 @@ function updateChannel(id, updates) {
228
268
  modelRedirects: merged.modelRedirects || [],
229
269
  proxyUrl: merged.proxyUrl,
230
270
  speedTestModel: merged.speedTestModel,
271
+ gatewaySourceType: normalizeGatewaySourceType(merged.gatewaySourceType, 'claude'),
231
272
  updatedAt: Date.now()
232
273
  });
274
+ data.channels[index] = nextChannel;
233
275
 
234
276
  // Get proxy status
235
277
  const { getProxyStatus } = require('../proxy-server');
@@ -273,7 +315,7 @@ function updateChannel(id, updates) {
273
315
  return data.channels[index];
274
316
  }
275
317
 
276
- function deleteChannel(id) {
318
+ async function deleteChannel(id) {
277
319
  const data = loadChannels();
278
320
  const index = data.channels.findIndex(ch => ch.id === id);
279
321
 
@@ -283,6 +325,7 @@ function deleteChannel(id) {
283
325
 
284
326
  data.channels.splice(index, 1);
285
327
  saveChannels(data);
328
+
286
329
  return { success: true };
287
330
  }
288
331
 
@@ -356,7 +399,7 @@ function updateClaudeSettingsWithModelConfig(channel) {
356
399
  delete settings.env.NO_PROXY;
357
400
  }
358
401
 
359
- settings.apiKeyHelper = `echo '${apiKey}'`;
402
+ settings.apiKeyHelper = buildApiKeyHelperCommand();
360
403
 
361
404
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
362
405
  }
@@ -385,34 +428,18 @@ function updateClaudeSettings(baseUrl, apiKey) {
385
428
  settings.env.ANTHROPIC_API_KEY = apiKey;
386
429
  }
387
430
 
388
- settings.apiKeyHelper = `echo '${apiKey}'`;
431
+ settings.apiKeyHelper = buildApiKeyHelperCommand();
389
432
 
390
433
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
391
434
  }
392
435
 
393
- /**
394
- * 获取渠道的有效 API Key
395
- * 如果渠道使用 OAuth 认证,返回有效的 OAuth 令牌;否则返回静态 API Key
396
- *
397
- * @param {Object} channel - 渠道对象
398
- * @returns {string|null} 有效的 API Key,OAuth 令牌无效/过期时返回 null
399
- */
400
436
  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;
437
+ return channel.apiKey || null;
412
438
  }
413
439
 
414
440
  module.exports = {
415
441
  getAllChannels,
442
+ getCurrentChannel,
416
443
  getCurrentSettings,
417
444
  createChannel,
418
445
  updateChannel,