coding-tool-x 3.6.7 → 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.
- package/dist/web/assets/{Analytics-CKycBI0G.js → Analytics-C57ZRDq5.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-CACLQb8e.js → ConfigTemplates-B3qqqL05.js} +1 -1
- package/dist/web/assets/Home-CG5XrTNc.js +1 -0
- package/dist/web/assets/Home-CIua-b-o.css +1 -0
- package/dist/web/assets/{PluginManager-CQJl_uso.js → PluginManager-BhX0LUzr.js} +1 -1
- package/dist/web/assets/{ProjectList-DSkvQFEP.js → ProjectList-BQ01OZn9.js} +1 -1
- package/dist/web/assets/{SessionList-Czjs1GN1.js → SessionList-DPv2IOXG.js} +1 -1
- package/dist/web/assets/{SkillManager-Cg1xZ5Cv.js → SkillManager-D5CfQm2O.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-COjYeKvN.js → WorkspaceManager-Ce8ggteK.js} +1 -1
- package/dist/web/assets/index-CpnFNWqC.js +2 -0
- package/dist/web/assets/index-DJ9VFidd.css +1 -0
- package/dist/web/assets/{naive-ui-DM897mIP.js → naive-ui-kcWDeJwl.js} +1 -1
- package/dist/web/index.html +3 -3
- package/package.json +1 -1
- package/src/commands/stats.js +1 -1
- package/src/config/channel-preset-websites.js +2 -1
- package/src/config/paths.js +8 -0
- package/src/server/api/agents.js +16 -54
- package/src/server/api/claude-hooks.js +1 -1
- package/src/server/api/claude-statistics.js +57 -0
- package/src/server/api/commands.js +16 -54
- package/src/server/api/dashboard.js +2 -2
- package/src/server/api/gemini-channels.js +2 -0
- package/src/server/api/hooks.js +4 -1
- package/src/server/api/plugins.js +40 -65
- package/src/server/api/skills.js +21 -77
- package/src/server/api/validation-errors.js +34 -0
- package/src/server/gemini-proxy-server.js +205 -21
- package/src/server/index.js +1 -0
- package/src/server/services/agents-service.js +166 -75
- package/src/server/services/base/base-channel-service.js +15 -0
- package/src/server/services/base/response-usage-parser.js +44 -0
- package/src/server/services/channel-balance.js +86 -1
- package/src/server/services/claude-statistics-service.js +139 -0
- package/src/server/services/commands-service.js +203 -121
- package/src/server/services/config-artifact-paths.js +116 -0
- package/src/server/services/config-export-service.js +7 -6
- package/src/server/services/config-registry-service.js +2 -2
- package/src/server/services/config-sync-manager.js +45 -11
- package/src/server/services/config-templates-service.js +21 -19
- package/src/server/services/format-converter.js +42 -4
- package/src/server/services/gemini-channels.js +30 -1
- package/src/server/services/gemini-sessions.js +347 -61
- package/src/server/services/mcp-client.js +236 -49
- package/src/server/services/mcp-service.js +51 -64
- package/src/server/services/notification-hooks.js +659 -88
- package/src/server/services/opencode-channels.js +15 -0
- package/src/server/services/plugins-service.js +771 -63
- package/src/server/services/repo-scanner-base.js +6 -29
- package/src/server/services/skill-service.js +187 -63
- package/dist/web/assets/Home-BeGkC4bb.css +0 -1
- package/dist/web/assets/Home-Bk6sP5Ed.js +0 -1
- package/dist/web/assets/index-0isNzXNY.css +0 -1
- package/dist/web/assets/index-fWgvlZQx.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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
499
|
-
|
|
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(
|
|
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(
|
|
670
|
+
return this.getCodexAgent(safeFileName, scope);
|
|
648
671
|
}
|
|
649
672
|
|
|
650
|
-
const baseDir = scope
|
|
651
|
-
|
|
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 ||
|
|
665
|
-
fileName,
|
|
684
|
+
name: frontmatter.name || safeFileName,
|
|
685
|
+
fileName: safeFileName,
|
|
666
686
|
scope,
|
|
667
|
-
path:
|
|
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
|
|
699
|
-
|
|
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(`代理 "${
|
|
725
|
+
throw new Error(`代理 "${safeFileName}" 已存在`);
|
|
709
726
|
}
|
|
710
727
|
|
|
711
728
|
// 生成文件内容
|
|
712
|
-
const frontmatterData = { name: (name ||
|
|
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(
|
|
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
|
|
736
|
-
|
|
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(`代理 "${
|
|
756
|
+
throw new Error(`代理 "${safeFileName}" 不存在`);
|
|
743
757
|
}
|
|
744
758
|
|
|
745
759
|
// 生成文件内容
|
|
746
760
|
const frontmatterData = {
|
|
747
|
-
name: name ||
|
|
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(
|
|
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(
|
|
783
|
+
return this.deleteCodexAgent(safeFileName, scope);
|
|
770
784
|
}
|
|
771
785
|
|
|
772
|
-
const baseDir = scope
|
|
773
|
-
|
|
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(
|
|
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(`${
|
|
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
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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
|
};
|
|
@@ -80,6 +80,19 @@ function forgetBalanceStrategy(key) {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
function isRememberedHiddenStrategy(strategy) {
|
|
84
|
+
return strategy?.type === 'unsupported-balance'
|
|
85
|
+
|| strategy?.type === 'unavailable-balance';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function makeUnavailableBalanceStrategy(snapshot = {}, baseUrl = '') {
|
|
89
|
+
return {
|
|
90
|
+
type: 'unavailable-balance',
|
|
91
|
+
platform: snapshot.platform || null,
|
|
92
|
+
baseUrl: normalizeBaseUrl(baseUrl) || baseUrl || null
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
83
96
|
function nowIso(now = Date.now()) {
|
|
84
97
|
return new Date(now).toISOString();
|
|
85
98
|
}
|
|
@@ -1270,6 +1283,12 @@ async function runBalanceStrategy(strategy, token, channel = {}) {
|
|
|
1270
1283
|
strategy
|
|
1271
1284
|
};
|
|
1272
1285
|
}
|
|
1286
|
+
if (strategy.type === 'unavailable-balance') {
|
|
1287
|
+
return {
|
|
1288
|
+
snapshot: makeHiddenSnapshot(strategy.platform || null),
|
|
1289
|
+
strategy
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1273
1292
|
if (strategy.type === 'siliconflow-user-info') {
|
|
1274
1293
|
return {
|
|
1275
1294
|
snapshot: await runSiliconFlowUserInfoStrategy(strategy, token),
|
|
@@ -1292,6 +1311,7 @@ async function tryBalanceStrategy(strategy, token, channel = {}) {
|
|
|
1292
1311
|
|
|
1293
1312
|
async function probeBalanceFromBase(baseUrl, channel, token) {
|
|
1294
1313
|
let lastSnapshot = null;
|
|
1314
|
+
let lastHiddenStrategy = null;
|
|
1295
1315
|
for (const strategy of buildBalanceProbeStrategies(baseUrl, channel, token)) {
|
|
1296
1316
|
const result = await tryBalanceStrategy(strategy, token, channel);
|
|
1297
1317
|
if (result) return result;
|
|
@@ -1304,8 +1324,11 @@ async function probeBalanceFromBase(baseUrl, channel, token) {
|
|
|
1304
1324
|
: strategy.platform || null
|
|
1305
1325
|
);
|
|
1306
1326
|
}
|
|
1327
|
+
if (isRememberedHiddenStrategy(strategy)) {
|
|
1328
|
+
lastHiddenStrategy = strategy;
|
|
1329
|
+
}
|
|
1307
1330
|
}
|
|
1308
|
-
return lastSnapshot ? { snapshot: lastSnapshot, strategy:
|
|
1331
|
+
return lastSnapshot ? { snapshot: lastSnapshot, strategy: lastHiddenStrategy } : null;
|
|
1309
1332
|
}
|
|
1310
1333
|
|
|
1311
1334
|
function buildCacheKey(source, channel) {
|
|
@@ -1341,6 +1364,27 @@ async function refreshChannelBalanceSnapshot(source, channel, options = {}) {
|
|
|
1341
1364
|
loadPersistedStrategyCache();
|
|
1342
1365
|
const cachedStrategy = balanceStrategyCache.get(key);
|
|
1343
1366
|
if (cachedStrategy) {
|
|
1367
|
+
if (isRememberedHiddenStrategy(cachedStrategy)) {
|
|
1368
|
+
const strategyResult = await runBalanceStrategy(cachedStrategy, token, channel);
|
|
1369
|
+
const hiddenSnapshot = strategyResult?.snapshot || makeHiddenSnapshot(cachedStrategy.platform || null);
|
|
1370
|
+
if (cached?.snapshot?.visible) {
|
|
1371
|
+
const staleSnapshot = {
|
|
1372
|
+
...cached.snapshot,
|
|
1373
|
+
stale: true
|
|
1374
|
+
};
|
|
1375
|
+
balanceCache.set(key, {
|
|
1376
|
+
snapshot: staleSnapshot,
|
|
1377
|
+
expiresAt: currentTime + CACHE_TTL_MS
|
|
1378
|
+
});
|
|
1379
|
+
return staleSnapshot;
|
|
1380
|
+
}
|
|
1381
|
+
balanceCache.set(key, {
|
|
1382
|
+
snapshot: hiddenSnapshot,
|
|
1383
|
+
expiresAt: currentTime + CACHE_TTL_MS
|
|
1384
|
+
});
|
|
1385
|
+
return hiddenSnapshot;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1344
1388
|
const strategyResult = await tryBalanceStrategy(cachedStrategy, token, channel);
|
|
1345
1389
|
if (strategyResult?.snapshot?.visible) {
|
|
1346
1390
|
rememberBalanceStrategy(key, strategyResult.strategy);
|
|
@@ -1366,6 +1410,13 @@ async function refreshChannelBalanceSnapshot(source, channel, options = {}) {
|
|
|
1366
1410
|
lastSnapshot = result?.snapshot || makeHiddenSnapshot(null, { updatedAt: nowIso(currentTime) });
|
|
1367
1411
|
}
|
|
1368
1412
|
|
|
1413
|
+
const hiddenStrategy = lastSnapshot
|
|
1414
|
+
? makeUnavailableBalanceStrategy(lastSnapshot, bases[0])
|
|
1415
|
+
: null;
|
|
1416
|
+
if (hiddenStrategy) {
|
|
1417
|
+
rememberBalanceStrategy(key, hiddenStrategy);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1369
1420
|
if (cached?.snapshot?.visible) {
|
|
1370
1421
|
const staleSnapshot = {
|
|
1371
1422
|
...cached.snapshot,
|
|
@@ -1381,6 +1432,38 @@ async function refreshChannelBalanceSnapshot(source, channel, options = {}) {
|
|
|
1381
1432
|
return lastSnapshot || makeHiddenSnapshot(null, { updatedAt: nowIso(currentTime) });
|
|
1382
1433
|
}
|
|
1383
1434
|
|
|
1435
|
+
function clearChannelBalanceCache(source, channel) {
|
|
1436
|
+
const normalizedSource = validateSource(source);
|
|
1437
|
+
if (!channel) return;
|
|
1438
|
+
loadPersistedStrategyCache();
|
|
1439
|
+
|
|
1440
|
+
const exactKey = buildCacheKey(normalizedSource, channel);
|
|
1441
|
+
const identity = channel.id || channel.name || 'unknown';
|
|
1442
|
+
const prefix = `${normalizedSource}:${identity}:`;
|
|
1443
|
+
let strategyChanged = false;
|
|
1444
|
+
|
|
1445
|
+
balanceCache.delete(exactKey);
|
|
1446
|
+
for (const key of Array.from(balanceCache.keys())) {
|
|
1447
|
+
if (key.startsWith(prefix)) {
|
|
1448
|
+
balanceCache.delete(key);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
if (balanceStrategyCache.delete(exactKey)) {
|
|
1453
|
+
strategyChanged = true;
|
|
1454
|
+
}
|
|
1455
|
+
for (const key of Array.from(balanceStrategyCache.keys())) {
|
|
1456
|
+
if (key.startsWith(prefix)) {
|
|
1457
|
+
balanceStrategyCache.delete(key);
|
|
1458
|
+
strategyChanged = true;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
if (strategyChanged) {
|
|
1463
|
+
writePersistedStrategyCache();
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1384
1467
|
function getChannelsForSource(source) {
|
|
1385
1468
|
if (source === 'claude') {
|
|
1386
1469
|
return require('./channels').getAllChannels() || [];
|
|
@@ -1482,8 +1565,10 @@ function clearBalanceCache() {
|
|
|
1482
1565
|
module.exports = {
|
|
1483
1566
|
getChannelBalances,
|
|
1484
1567
|
refreshChannelBalance,
|
|
1568
|
+
clearChannelBalanceCache,
|
|
1485
1569
|
_test: {
|
|
1486
1570
|
clearBalanceCache,
|
|
1571
|
+
clearChannelBalanceCache,
|
|
1487
1572
|
normalizeBaseUrl,
|
|
1488
1573
|
buildBaseCandidates,
|
|
1489
1574
|
buildHubBalanceSnapshot,
|