coding-tool-x 3.6.6 → 3.6.8

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 (56) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/web/assets/{Analytics-CcGKjDkP.js → Analytics-C57ZRDq5.js} +1 -1
  3. package/dist/web/assets/{ConfigTemplates-DPOa1w5D.js → ConfigTemplates-B3qqqL05.js} +1 -1
  4. package/dist/web/assets/Home-CG5XrTNc.js +1 -0
  5. package/dist/web/assets/Home-CIua-b-o.css +1 -0
  6. package/dist/web/assets/{PluginManager-BVbHUdey.js → PluginManager-BhX0LUzr.js} +1 -1
  7. package/dist/web/assets/{ProjectList-BQrz2FKo.js → ProjectList-BQ01OZn9.js} +1 -1
  8. package/dist/web/assets/{SessionList-CRGer64T.js → SessionList-DPv2IOXG.js} +1 -1
  9. package/dist/web/assets/{SkillManager-BhJKtWYM.js → SkillManager-D5CfQm2O.js} +1 -1
  10. package/dist/web/assets/{WorkspaceManager-pc6tpF3m.js → WorkspaceManager-Ce8ggteK.js} +1 -1
  11. package/dist/web/assets/index-CpnFNWqC.js +2 -0
  12. package/dist/web/assets/index-DJ9VFidd.css +1 -0
  13. package/dist/web/assets/{naive-ui-DM897mIP.js → naive-ui-kcWDeJwl.js} +1 -1
  14. package/dist/web/index.html +3 -3
  15. package/package.json +1 -1
  16. package/src/commands/stats.js +1 -1
  17. package/src/config/channel-preset-websites.js +2 -1
  18. package/src/config/paths.js +8 -0
  19. package/src/server/api/agents.js +16 -54
  20. package/src/server/api/claude-hooks.js +1 -1
  21. package/src/server/api/claude-statistics.js +57 -0
  22. package/src/server/api/commands.js +16 -54
  23. package/src/server/api/dashboard.js +2 -2
  24. package/src/server/api/gemini-channels.js +2 -0
  25. package/src/server/api/hooks.js +4 -1
  26. package/src/server/api/plugins.js +40 -65
  27. package/src/server/api/skills.js +21 -77
  28. package/src/server/api/validation-errors.js +34 -0
  29. package/src/server/gemini-proxy-server.js +205 -21
  30. package/src/server/index.js +1 -0
  31. package/src/server/services/agents-service.js +166 -75
  32. package/src/server/services/base/base-channel-service.js +15 -0
  33. package/src/server/services/base/proxy-utils.js +6 -3
  34. package/src/server/services/base/response-usage-parser.js +44 -0
  35. package/src/server/services/channel-balance.js +86 -1
  36. package/src/server/services/claude-statistics-service.js +139 -0
  37. package/src/server/services/commands-service.js +203 -121
  38. package/src/server/services/config-artifact-paths.js +116 -0
  39. package/src/server/services/config-export-service.js +7 -6
  40. package/src/server/services/config-registry-service.js +2 -2
  41. package/src/server/services/config-sync-manager.js +45 -11
  42. package/src/server/services/config-templates-service.js +21 -19
  43. package/src/server/services/format-converter.js +42 -4
  44. package/src/server/services/gemini-channels.js +30 -1
  45. package/src/server/services/gemini-sessions.js +347 -61
  46. package/src/server/services/mcp-client.js +236 -49
  47. package/src/server/services/mcp-service.js +51 -64
  48. package/src/server/services/notification-hooks.js +659 -88
  49. package/src/server/services/opencode-channels.js +15 -0
  50. package/src/server/services/plugins-service.js +771 -63
  51. package/src/server/services/repo-scanner-base.js +6 -29
  52. package/src/server/services/skill-service.js +187 -63
  53. package/dist/web/assets/Home-BP1LxGtp.js +0 -1
  54. package/dist/web/assets/Home-BeGkC4bb.css +0 -1
  55. package/dist/web/assets/index-0isNzXNY.css +0 -1
  56. package/dist/web/assets/index-BsdIJwLf.js +0 -2
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Agents 服务
3
3
  *
4
- * 管理 Claude/OpenCode 自定义代理的 CRUD 操作
4
+ * 管理 Claude/Codex/Gemini/OpenCode 自定义代理的 CRUD 操作
5
5
  * 支持从 GitHub 仓库扫描和安装代理
6
6
  */
7
7
 
@@ -13,15 +13,21 @@ const tomlStringify = require('@iarna/toml').stringify;
13
13
  const { RepoScannerBase } = require('./repo-scanner-base');
14
14
  const { NATIVE_PATHS } = require('../../config/paths');
15
15
  const { resolvePreferredHomeDir } = require('../../utils/home-dir');
16
+ const {
17
+ normalizeSafeFileStem,
18
+ normalizeSafeRelativePath,
19
+ resolveInsideRoot
20
+ } = require('./config-artifact-paths');
16
21
 
17
22
  // 默认仓库源
18
23
  const DEFAULT_REPOS = [];
19
- const SUPPORTED_PLATFORMS = ['claude', 'codex', 'opencode'];
24
+ const SUPPORTED_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode'];
20
25
  const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
21
26
  const CODEX_CONFIG_PATH = NATIVE_PATHS.codex.config;
22
27
  const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homedir());
23
28
  const CODEX_AGENTS_DIR = path.join(path.dirname(CODEX_CONFIG_PATH), 'agents');
24
29
  const CLAUDE_AGENTS_DIR = path.join(path.dirname(NATIVE_PATHS.claude.settings), 'agents');
30
+ const GEMINI_AGENTS_DIR = path.join(NATIVE_PATHS.gemini.dir, 'agents');
25
31
  const CODEX_CONFIG_MODES = new Set(['none', 'managed', 'custom']);
26
32
 
27
33
  const PLATFORM_CONFIG = {
@@ -47,6 +53,11 @@ const PLATFORM_CONFIG = {
47
53
  userAgentsDir: CODEX_AGENTS_DIR,
48
54
  projectAgentsDir: () => null,
49
55
  repoType: 'agents'
56
+ },
57
+ gemini: {
58
+ userAgentsDir: GEMINI_AGENTS_DIR,
59
+ projectAgentsDir: (projectPath) => path.join(projectPath, '.gemini', 'agents'),
60
+ repoType: 'gemini-agents'
50
61
  }
51
62
  };
52
63
 
@@ -59,39 +70,29 @@ function normalizePlatform(platform) {
59
70
  }
60
71
 
61
72
  function assertSafeAgentFileName(fileName) {
62
- if (typeof fileName !== 'string') {
63
- throw new Error('代理文件名必须是字符串');
64
- }
65
-
66
- const normalized = fileName.trim();
67
- if (!normalized) {
68
- throw new Error('代理文件名不能为空');
69
- }
70
-
71
- if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(normalized) || normalized.includes('..')) {
73
+ try {
74
+ return normalizeSafeFileStem(fileName, 'agent file name', {
75
+ allowDots: true,
76
+ pattern: /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/
77
+ });
78
+ } catch {
72
79
  throw new Error('代理文件名只能包含字母、数字、点号、横杠和下划线,且不能包含连续点');
73
80
  }
74
81
  }
75
82
 
76
83
  function assertSafeRepoPath(repoPath) {
77
- if (typeof repoPath !== 'string' || !repoPath.trim()) {
78
- throw new Error('代理仓库路径不能为空');
79
- }
80
-
81
- const raw = repoPath.replace(/\\/g, '/').trim();
82
- const normalized = path.posix.normalize(raw).replace(/^(\.\/)+/, '');
83
- if (!normalized ||
84
- normalized === '.' ||
85
- normalized === '..' ||
86
- normalized.startsWith('../') ||
87
- normalized.includes('/../') ||
88
- path.posix.isAbsolute(normalized)) {
84
+ let normalized = '';
85
+ try {
86
+ normalized = normalizeSafeRelativePath(repoPath, 'agent repo path');
87
+ } catch {
89
88
  throw new Error('代理仓库路径不合法');
90
89
  }
91
90
 
92
91
  if (!normalized.endsWith('.md')) {
93
92
  throw new Error('代理仓库路径必须是 .md 文件');
94
93
  }
94
+
95
+ return normalized;
95
96
  }
96
97
 
97
98
  function assertSafeProjectPath(projectPath) {
@@ -158,7 +159,8 @@ function isPlainObject(value) {
158
159
  }
159
160
 
160
161
  function getCodexManagedAgentConfigPath(fileName) {
161
- return path.join(CODEX_AGENTS_DIR, `${fileName}.toml`);
162
+ const safeFileName = assertSafeAgentFileName(fileName);
163
+ return resolveInsideRoot(CODEX_AGENTS_DIR, `${safeFileName}.toml`, 'Codex agent config path');
162
164
  }
163
165
 
164
166
  function normalizeCodexConfigPath(configPath) {
@@ -495,8 +497,13 @@ class AgentsRepoScanner extends RepoScannerBase {
495
497
  * 检查代理是否已安装
496
498
  */
497
499
  isInstalled(fileName) {
498
- const fullPath = path.join(this.installDir, `${fileName}.md`);
499
- return fs.existsSync(fullPath);
500
+ try {
501
+ const safeFileName = assertSafeAgentFileName(fileName);
502
+ const fullPath = resolveInsideRoot(this.installDir, `${safeFileName}.md`, 'Agent path');
503
+ return fs.existsSync(fullPath);
504
+ } catch {
505
+ return false;
506
+ }
500
507
  }
501
508
 
502
509
  /**
@@ -510,8 +517,8 @@ class AgentsRepoScanner extends RepoScannerBase {
510
517
  * 安装代理
511
518
  */
512
519
  async installAgent(item) {
513
- assertSafeAgentFileName(item?.fileName);
514
- assertSafeRepoPath(item?.repoPath);
520
+ const safeFileName = assertSafeAgentFileName(item?.fileName);
521
+ const safeRepoPath = assertSafeRepoPath(item?.repoPath);
515
522
 
516
523
  const repo = {
517
524
  owner: item.repoOwner,
@@ -520,7 +527,7 @@ class AgentsRepoScanner extends RepoScannerBase {
520
527
  };
521
528
 
522
529
  // 代理安装到根目录,使用文件名
523
- return this.installFromRepo(item.repoPath, repo, `${item.fileName}.md`);
530
+ return this.installFromRepo(safeRepoPath, repo, `${safeFileName}.md`);
524
531
  }
525
532
  }
526
533
 
@@ -551,6 +558,22 @@ class AgentsService {
551
558
  return this.projectAgentsDir(safeProjectPath);
552
559
  }
553
560
 
561
+ _getBaseDir(scope, projectPath = null) {
562
+ return scope === 'user'
563
+ ? this.userAgentsDir
564
+ : this.getProjectAgentsDir(projectPath);
565
+ }
566
+
567
+ _resolveAgentFilePath(baseDir, fileName) {
568
+ if (!baseDir) {
569
+ throw new Error('项目路径不能为空');
570
+ }
571
+ const safeFileName = assertSafeAgentFileName(fileName);
572
+ const relativePath = `${safeFileName}.md`;
573
+ const fullPath = resolveInsideRoot(baseDir, relativePath, 'Agent path');
574
+ return { safeFileName, relativePath, fullPath };
575
+ }
576
+
554
577
  /**
555
578
  * 获取所有代理列表
556
579
  * @param {string} projectPath - 项目路径(可选,用于获取项目级代理)
@@ -641,17 +664,14 @@ class AgentsService {
641
664
  * 获取单个代理详情
642
665
  */
643
666
  getAgent(fileName, scope, projectPath = null) {
644
- assertSafeAgentFileName(fileName);
667
+ const safeFileName = assertSafeAgentFileName(fileName);
645
668
 
646
669
  if (this.platform === 'codex') {
647
- return this.getCodexAgent(fileName, scope);
670
+ return this.getCodexAgent(safeFileName, scope);
648
671
  }
649
672
 
650
- const baseDir = scope === 'user'
651
- ? this.userAgentsDir
652
- : this.getProjectAgentsDir(projectPath);
653
-
654
- const filePath = path.join(baseDir, `${fileName}.md`);
673
+ const baseDir = this._getBaseDir(scope, projectPath);
674
+ const { relativePath, fullPath: filePath } = this._resolveAgentFilePath(baseDir, safeFileName);
655
675
 
656
676
  if (!fs.existsSync(filePath)) {
657
677
  return null;
@@ -661,10 +681,10 @@ class AgentsService {
661
681
  const { frontmatter, body } = parseFrontmatter(content);
662
682
 
663
683
  return {
664
- name: frontmatter.name || fileName,
665
- fileName,
684
+ name: frontmatter.name || safeFileName,
685
+ fileName: safeFileName,
666
686
  scope,
667
- path: `${fileName}.md`,
687
+ path: relativePath,
668
688
  fullPath: filePath,
669
689
  description: frontmatter.description || '',
670
690
  tools: frontmatter.tools || '',
@@ -681,10 +701,10 @@ class AgentsService {
681
701
  * 创建代理
682
702
  */
683
703
  createAgent({ fileName, scope, projectPath, name, description, tools, model, permissionMode, skills, systemPrompt, configMode, configFile, configContent }) {
684
- assertSafeAgentFileName(fileName);
704
+ const safeFileName = assertSafeAgentFileName(fileName);
685
705
 
686
706
  if (this.platform === 'codex') {
687
- return this.createCodexAgent({ fileName, scope, description, model, configMode, configFile, configContent });
707
+ return this.createCodexAgent({ fileName: safeFileName, scope, description, model, configMode, configFile, configContent });
688
708
  }
689
709
 
690
710
  if (this.platform === 'claude' && (!name || !name.trim())) {
@@ -695,21 +715,18 @@ class AgentsService {
695
715
  throw new Error('代理描述不能为空');
696
716
  }
697
717
 
698
- const baseDir = scope === 'user'
699
- ? this.userAgentsDir
700
- : this.getProjectAgentsDir(projectPath);
718
+ const baseDir = this._getBaseDir(scope, projectPath);
719
+ const { fullPath: filePath } = this._resolveAgentFilePath(baseDir, safeFileName);
701
720
 
702
721
  ensureDir(baseDir);
703
722
 
704
- const filePath = path.join(baseDir, `${fileName}.md`);
705
-
706
723
  // 检查是否已存在
707
724
  if (fs.existsSync(filePath)) {
708
- throw new Error(`代理 "${fileName}" 已存在`);
725
+ throw new Error(`代理 "${safeFileName}" 已存在`);
709
726
  }
710
727
 
711
728
  // 生成文件内容
712
- const frontmatterData = { name: (name || fileName), description };
729
+ const frontmatterData = { name: (name || safeFileName), description };
713
730
  if (tools) frontmatterData.tools = tools;
714
731
  if (model) frontmatterData.model = model;
715
732
  if (permissionMode) frontmatterData.permissionMode = permissionMode;
@@ -719,32 +736,29 @@ class AgentsService {
719
736
 
720
737
  fs.writeFileSync(filePath, content, 'utf-8');
721
738
 
722
- return this.getAgent(fileName, scope, projectPath);
739
+ return this.getAgent(safeFileName, scope, projectPath);
723
740
  }
724
741
 
725
742
  /**
726
743
  * 更新代理
727
744
  */
728
745
  updateAgent({ fileName, scope, projectPath, name, description, tools, model, permissionMode, skills, systemPrompt, configMode, configFile, configContent }) {
729
- assertSafeAgentFileName(fileName);
746
+ const safeFileName = assertSafeAgentFileName(fileName);
730
747
 
731
748
  if (this.platform === 'codex') {
732
- return this.updateCodexAgent({ fileName, scope, description, model, configMode, configFile, configContent });
749
+ return this.updateCodexAgent({ fileName: safeFileName, scope, description, model, configMode, configFile, configContent });
733
750
  }
734
751
 
735
- const baseDir = scope === 'user'
736
- ? this.userAgentsDir
737
- : this.getProjectAgentsDir(projectPath);
738
-
739
- const filePath = path.join(baseDir, `${fileName}.md`);
752
+ const baseDir = this._getBaseDir(scope, projectPath);
753
+ const { fullPath: filePath } = this._resolveAgentFilePath(baseDir, safeFileName);
740
754
 
741
755
  if (!fs.existsSync(filePath)) {
742
- throw new Error(`代理 "${fileName}" 不存在`);
756
+ throw new Error(`代理 "${safeFileName}" 不存在`);
743
757
  }
744
758
 
745
759
  // 生成文件内容
746
760
  const frontmatterData = {
747
- name: name || fileName,
761
+ name: name || safeFileName,
748
762
  description: description || ''
749
763
  };
750
764
  if (tools) frontmatterData.tools = tools;
@@ -756,24 +770,21 @@ class AgentsService {
756
770
 
757
771
  fs.writeFileSync(filePath, content, 'utf-8');
758
772
 
759
- return this.getAgent(fileName, scope, projectPath);
773
+ return this.getAgent(safeFileName, scope, projectPath);
760
774
  }
761
775
 
762
776
  /**
763
777
  * 删除代理
764
778
  */
765
779
  deleteAgent(fileName, scope, projectPath = null) {
766
- assertSafeAgentFileName(fileName);
780
+ const safeFileName = assertSafeAgentFileName(fileName);
767
781
 
768
782
  if (this.platform === 'codex') {
769
- return this.deleteCodexAgent(fileName, scope);
783
+ return this.deleteCodexAgent(safeFileName, scope);
770
784
  }
771
785
 
772
- const baseDir = scope === 'user'
773
- ? this.userAgentsDir
774
- : this.getProjectAgentsDir(projectPath);
775
-
776
- const filePath = path.join(baseDir, `${fileName}.md`);
786
+ const baseDir = this._getBaseDir(scope, projectPath);
787
+ const { fullPath: filePath } = this._resolveAgentFilePath(baseDir, safeFileName);
777
788
 
778
789
  if (!fs.existsSync(filePath)) {
779
790
  return { success: false, message: '代理不存在' };
@@ -855,23 +866,27 @@ class AgentsService {
855
866
  if (!agent || typeof agent !== 'object') {
856
867
  throw new Error('无效的代理安装参数');
857
868
  }
858
- assertSafeAgentFileName(agent.fileName);
859
- assertSafeRepoPath(agent.repoPath);
860
- return this.repoScanner.installAgent(agent);
869
+ const safeFileName = assertSafeAgentFileName(agent.fileName);
870
+ const safeRepoPath = assertSafeRepoPath(agent.repoPath);
871
+ return this.repoScanner.installAgent({
872
+ ...agent,
873
+ fileName: safeFileName,
874
+ repoPath: safeRepoPath
875
+ });
861
876
  }
862
877
 
863
878
  /**
864
879
  * 卸载代理
865
880
  */
866
881
  uninstallAgent(fileName) {
867
- assertSafeAgentFileName(fileName);
868
- return this.repoScanner.uninstall(`${fileName}.md`);
882
+ const safeFileName = assertSafeAgentFileName(fileName);
883
+ return this.repoScanner.uninstall(`${safeFileName}.md`);
869
884
  }
870
885
 
871
886
  listCodexAgents() {
872
887
  const config = readCodexTomlConfig();
873
888
  const agentsTable = isPlainObject(config.agents) ? config.agents : {};
874
- const agents = [];
889
+ const agentsByName = new Map();
875
890
 
876
891
  for (const [key, value] of Object.entries(agentsTable)) {
877
892
  if (!isPlainObject(value)) {
@@ -901,7 +916,7 @@ class AgentsService {
901
916
  }
902
917
  }
903
918
 
904
- agents.push({
919
+ agentsByName.set(key, {
905
920
  name: key,
906
921
  fileName: key,
907
922
  scope: 'user',
@@ -918,10 +933,86 @@ class AgentsService {
918
933
  configMode,
919
934
  configReadError,
920
935
  resolvedConfigFile,
921
- updatedAt
936
+ updatedAt,
937
+ source: 'codex-config'
922
938
  });
923
939
  }
924
940
 
941
+ if (fs.existsSync(CODEX_AGENTS_DIR)) {
942
+ try {
943
+ const entries = fs.readdirSync(CODEX_AGENTS_DIR, { withFileTypes: true });
944
+ for (const entry of entries) {
945
+ if (!entry.isFile() || !entry.name.endsWith('.toml')) {
946
+ continue;
947
+ }
948
+
949
+ const fileName = entry.name.slice(0, -'.toml'.length);
950
+ try {
951
+ assertSafeAgentFileName(fileName);
952
+ } catch {
953
+ continue;
954
+ }
955
+
956
+ const configFilePath = path.join(CODEX_AGENTS_DIR, entry.name);
957
+ const parsedConfigFile = readCodexAgentConfigFile(configFilePath);
958
+ const existing = agentsByName.get(fileName);
959
+ const model = isPlainObject(parsedConfigFile.data) && typeof parsedConfigFile.data.model === 'string'
960
+ ? parsedConfigFile.data.model
961
+ : '';
962
+ const updatedAt = parsedConfigFile.updatedAt || fs.statSync(configFilePath).mtime.getTime();
963
+
964
+ if (existing) {
965
+ if (!existing.configFile) {
966
+ existing.configFile = configFilePath;
967
+ existing.configMode = inferCodexConfigMode(configFilePath);
968
+ existing.path = configFilePath;
969
+ existing.fullPath = configFilePath;
970
+ existing.resolvedConfigFile = configFilePath;
971
+ }
972
+ if (!existing.fullContent && parsedConfigFile.content) {
973
+ existing.fullContent = parsedConfigFile.content;
974
+ }
975
+ if (!existing.model && model) {
976
+ existing.model = model;
977
+ }
978
+ if (parsedConfigFile.error && !existing.configReadError) {
979
+ existing.configReadError = parsedConfigFile.error;
980
+ }
981
+ existing.updatedAt = Math.max(existing.updatedAt || 0, updatedAt);
982
+ existing.source = existing.source === 'codex-config'
983
+ ? 'codex-config+native-file'
984
+ : existing.source;
985
+ continue;
986
+ }
987
+
988
+ agentsByName.set(fileName, {
989
+ name: fileName,
990
+ fileName,
991
+ scope: 'user',
992
+ path: configFilePath,
993
+ fullPath: configFilePath,
994
+ description: '',
995
+ tools: '',
996
+ model,
997
+ permissionMode: '',
998
+ skills: '',
999
+ systemPrompt: '',
1000
+ fullContent: parsedConfigFile.content,
1001
+ configFile: configFilePath,
1002
+ configMode: inferCodexConfigMode(configFilePath),
1003
+ configReadError: parsedConfigFile.error || '',
1004
+ resolvedConfigFile: configFilePath,
1005
+ updatedAt,
1006
+ source: 'native-file'
1007
+ });
1008
+ }
1009
+ } catch (err) {
1010
+ console.warn('[AgentsService] Failed to scan Codex agents dir:', err.message);
1011
+ }
1012
+ }
1013
+
1014
+ const agents = Array.from(agentsByName.values());
1015
+
925
1016
  agents.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
926
1017
 
927
1018
  return {
@@ -13,6 +13,14 @@ const crypto = require('crypto');
13
13
  const { normalizeGatewaySourceType, normalizeNumber } = require('./proxy-utils');
14
14
  const { resolveChannelWebsiteUrl } = require('../../../config/channel-preset-websites');
15
15
 
16
+ function clearChannelBalanceCache(platform, channel) {
17
+ try {
18
+ require('../channel-balance').clearChannelBalanceCache(platform, channel);
19
+ } catch (_) {
20
+ // Balance cache invalidation is an optimization; channel updates must still succeed.
21
+ }
22
+ }
23
+
16
24
  class BaseChannelService {
17
25
  /**
18
26
  * @param {object} config
@@ -121,6 +129,9 @@ class BaseChannelService {
121
129
  }
122
130
 
123
131
  this.saveChannels(data);
132
+ if (oldChannel.enabled === false && nextChannel.enabled !== false) {
133
+ clearChannelBalanceCache(this.platform, nextChannel);
134
+ }
124
135
  this._onAfterUpdate(oldChannel, nextChannel, data.channels);
125
136
  return nextChannel;
126
137
  }
@@ -154,10 +165,14 @@ class BaseChannelService {
154
165
  }
155
166
 
156
167
  // 单渠道模式:只启用目标渠道
168
+ const wasEnabled = channel.enabled !== false;
157
169
  data.channels.forEach(ch => {
158
170
  ch.enabled = ch.id === channelId;
159
171
  });
160
172
  this.saveChannels(data);
173
+ if (!wasEnabled) {
174
+ clearChannelBalanceCache(this.platform, channel);
175
+ }
161
176
  this._applyToNativeSettings(channel);
162
177
  return channel;
163
178
  }
@@ -67,10 +67,10 @@ function redirectModel(originalModel, channel, options = {}) {
67
67
  }
68
68
 
69
69
  /**
70
- * 解析代理目标 URL,避免 /v1/v1 重复
70
+ * 解析代理目标 URL,避免 API 版本段重复
71
71
  *
72
- * 当 baseUrl 以 /v1 结尾且请求路径也以 /v1 开头时,
73
- * 去掉 baseUrl 的 /v1 后缀,因为 http-proxy 会自动拼接 req.url。
72
+ * 当 baseUrl 和请求路径都包含同一个 API 版本段时,去掉 baseUrl 的
73
+ * 版本后缀,因为 http-proxy 会自动拼接 req.url。
74
74
  *
75
75
  * @param {string} baseUrl - 渠道配置的 base_url
76
76
  * @param {string} requestPath - 请求路径
@@ -81,6 +81,9 @@ function resolveTargetUrl(baseUrl, requestPath) {
81
81
  if (target.endsWith('/')) {
82
82
  target = target.slice(0, -1);
83
83
  }
84
+ if (target.endsWith('/v1beta') && requestPath.startsWith('/v1beta')) {
85
+ return target.slice(0, -7);
86
+ }
84
87
  if (target.endsWith('/v1') && requestPath.startsWith('/v1')) {
85
88
  target = target.slice(0, -3);
86
89
  }
@@ -218,6 +218,48 @@ function parseNonStreamingUsage(parsed) {
218
218
  return parseSSEUsage(parsed, '');
219
219
  }
220
220
 
221
+ function splitSSEEvents(buffer = '') {
222
+ const normalized = String(buffer || '');
223
+ const events = [];
224
+ let cursor = 0;
225
+ const separator = /\r?\n\r?\n/g;
226
+ let match;
227
+
228
+ while ((match = separator.exec(normalized)) !== null) {
229
+ events.push(normalized.slice(cursor, match.index));
230
+ cursor = separator.lastIndex;
231
+ }
232
+
233
+ return {
234
+ events,
235
+ remainder: normalized.slice(cursor)
236
+ };
237
+ }
238
+
239
+ function parseSSEEventText(eventText = '') {
240
+ if (!String(eventText || '').trim()) {
241
+ return null;
242
+ }
243
+
244
+ let eventType = '';
245
+ const dataLines = [];
246
+
247
+ String(eventText).split(/\r?\n/).forEach((line) => {
248
+ if (line.startsWith('event:')) {
249
+ eventType = line.substring(6).trim();
250
+ } else if (line.startsWith('data:')) {
251
+ dataLines.push(line.substring(5).trim());
252
+ }
253
+ });
254
+
255
+ const data = dataLines.join('\n');
256
+ if (!data || data === '[DONE]') {
257
+ return null;
258
+ }
259
+
260
+ return { eventType, data };
261
+ }
262
+
221
263
  /**
222
264
  * 将解析结果合并到 tokenData 对象。
223
265
  *
@@ -260,6 +302,8 @@ function createTokenData() {
260
302
  module.exports = {
261
303
  parseSSEUsage,
262
304
  parseNonStreamingUsage,
305
+ splitSSEEvents,
306
+ parseSSEEventText,
263
307
  mergeUsageIntoTokenData,
264
308
  createTokenData
265
309
  };