coding-tool-x 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +599 -0
- package/LICENSE +21 -0
- package/README.md +439 -0
- package/bin/ctx.js +8 -0
- package/dist/web/assets/Analytics-DN_YsnkW.js +39 -0
- package/dist/web/assets/Analytics-DuYvId7u.css +1 -0
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-DpXIMy0p.js +1 -0
- package/dist/web/assets/Home-38JTUlYt.js +1 -0
- package/dist/web/assets/Home-CjupSEWE.css +1 -0
- package/dist/web/assets/PluginManager-CX2tgq2H.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/ProjectList-C1lDcsn6.js +1 -0
- package/dist/web/assets/ProjectList-oJIyIRkP.css +1 -0
- package/dist/web/assets/SessionList-C55tjV7i.css +1 -0
- package/dist/web/assets/SessionList-CZ7T6rVx.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/SkillManager-DLN9f79y.js +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/WorkspaceManager-DxlHZkpZ.js +1 -0
- package/dist/web/assets/icons-DRrXwWZi.js +1 -0
- package/dist/web/assets/index-CetESrXw.css +1 -0
- package/dist/web/assets/index-Cfvn-2Gb.js +2 -0
- package/dist/web/assets/markdown-BfC0goYb.css +10 -0
- package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
- package/dist/web/assets/naive-ui-DlpKk-8M.js +1 -0
- package/dist/web/assets/vendors-DMjSfzlv.js +7 -0
- package/dist/web/assets/vue-vendor-DET08QYg.js +45 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +20 -0
- package/dist/web/logo.png +0 -0
- package/docs/bannel.png +0 -0
- package/docs/home.png +0 -0
- package/docs/logo.png +0 -0
- package/docs/model-redirection.md +251 -0
- package/docs/multi-channel-load-balancing.md +249 -0
- package/package.json +80 -0
- package/src/commands/channels.js +551 -0
- package/src/commands/cli-type.js +101 -0
- package/src/commands/daemon.js +365 -0
- package/src/commands/doctor.js +333 -0
- package/src/commands/export-config.js +205 -0
- package/src/commands/list.js +222 -0
- package/src/commands/logs.js +261 -0
- package/src/commands/plugin.js +585 -0
- package/src/commands/port-config.js +135 -0
- package/src/commands/proxy-control.js +264 -0
- package/src/commands/proxy.js +152 -0
- package/src/commands/resume.js +137 -0
- package/src/commands/search.js +190 -0
- package/src/commands/security.js +37 -0
- package/src/commands/stats.js +398 -0
- package/src/commands/switch.js +48 -0
- package/src/commands/toggle-proxy.js +247 -0
- package/src/commands/ui.js +99 -0
- package/src/commands/update.js +97 -0
- package/src/commands/workspace.js +454 -0
- package/src/config/default.js +69 -0
- package/src/config/loader.js +149 -0
- package/src/config/model-metadata.js +167 -0
- package/src/config/model-metadata.json +125 -0
- package/src/config/model-pricing.js +35 -0
- package/src/config/paths.js +190 -0
- package/src/index.js +680 -0
- package/src/plugins/constants.js +15 -0
- package/src/plugins/event-bus.js +54 -0
- package/src/plugins/manifest-validator.js +129 -0
- package/src/plugins/plugin-api.js +128 -0
- package/src/plugins/plugin-installer.js +601 -0
- package/src/plugins/plugin-loader.js +229 -0
- package/src/plugins/plugin-manager.js +170 -0
- package/src/plugins/registry.js +152 -0
- package/src/plugins/schema/plugin-manifest.json +115 -0
- package/src/reset-config.js +94 -0
- package/src/server/api/agents.js +826 -0
- package/src/server/api/aliases.js +36 -0
- package/src/server/api/channels.js +368 -0
- package/src/server/api/claude-hooks.js +480 -0
- package/src/server/api/codex-channels.js +417 -0
- package/src/server/api/codex-projects.js +104 -0
- package/src/server/api/codex-proxy.js +195 -0
- package/src/server/api/codex-sessions.js +483 -0
- package/src/server/api/codex-statistics.js +57 -0
- package/src/server/api/commands.js +482 -0
- package/src/server/api/config-export.js +212 -0
- package/src/server/api/config-registry.js +357 -0
- package/src/server/api/config-sync.js +155 -0
- package/src/server/api/config-templates.js +248 -0
- package/src/server/api/config.js +521 -0
- package/src/server/api/convert.js +260 -0
- package/src/server/api/dashboard.js +142 -0
- package/src/server/api/env.js +144 -0
- package/src/server/api/favorites.js +77 -0
- package/src/server/api/gemini-channels.js +366 -0
- package/src/server/api/gemini-projects.js +91 -0
- package/src/server/api/gemini-proxy.js +173 -0
- package/src/server/api/gemini-sessions.js +376 -0
- package/src/server/api/gemini-statistics.js +57 -0
- package/src/server/api/health-check.js +31 -0
- package/src/server/api/mcp.js +399 -0
- package/src/server/api/opencode-channels.js +419 -0
- package/src/server/api/opencode-projects.js +99 -0
- package/src/server/api/opencode-proxy.js +207 -0
- package/src/server/api/opencode-sessions.js +327 -0
- package/src/server/api/opencode-statistics.js +57 -0
- package/src/server/api/plugins.js +463 -0
- package/src/server/api/pm2-autostart.js +269 -0
- package/src/server/api/projects.js +124 -0
- package/src/server/api/prompts.js +279 -0
- package/src/server/api/proxy.js +306 -0
- package/src/server/api/security.js +53 -0
- package/src/server/api/sessions.js +514 -0
- package/src/server/api/settings.js +142 -0
- package/src/server/api/skills.js +570 -0
- package/src/server/api/statistics.js +238 -0
- package/src/server/api/ui-config.js +64 -0
- package/src/server/api/workspaces.js +456 -0
- package/src/server/codex-proxy-server.js +681 -0
- package/src/server/dev-server.js +26 -0
- package/src/server/gemini-proxy-server.js +610 -0
- package/src/server/index.js +422 -0
- package/src/server/opencode-proxy-server.js +4771 -0
- package/src/server/proxy-server.js +669 -0
- package/src/server/services/agents-service.js +1137 -0
- package/src/server/services/alias.js +71 -0
- package/src/server/services/channel-health.js +234 -0
- package/src/server/services/channel-scheduler.js +240 -0
- package/src/server/services/channels.js +447 -0
- package/src/server/services/codex-channels.js +705 -0
- package/src/server/services/codex-config.js +90 -0
- package/src/server/services/codex-parser.js +322 -0
- package/src/server/services/codex-sessions.js +936 -0
- package/src/server/services/codex-settings-manager.js +619 -0
- package/src/server/services/codex-speed-test-template.json +24 -0
- package/src/server/services/codex-statistics-service.js +161 -0
- package/src/server/services/commands-service.js +574 -0
- package/src/server/services/config-export-service.js +1165 -0
- package/src/server/services/config-registry-service.js +828 -0
- package/src/server/services/config-sync-manager.js +941 -0
- package/src/server/services/config-sync-service.js +504 -0
- package/src/server/services/config-templates-service.js +913 -0
- package/src/server/services/enhanced-cache.js +196 -0
- package/src/server/services/env-checker.js +409 -0
- package/src/server/services/env-manager.js +436 -0
- package/src/server/services/favorites.js +165 -0
- package/src/server/services/format-converter.js +620 -0
- package/src/server/services/gemini-channels.js +459 -0
- package/src/server/services/gemini-config.js +73 -0
- package/src/server/services/gemini-sessions.js +689 -0
- package/src/server/services/gemini-settings-manager.js +263 -0
- package/src/server/services/gemini-statistics-service.js +157 -0
- package/src/server/services/health-check.js +85 -0
- package/src/server/services/mcp-client.js +790 -0
- package/src/server/services/mcp-service.js +1732 -0
- package/src/server/services/model-detector.js +1245 -0
- package/src/server/services/network-access.js +80 -0
- package/src/server/services/opencode-channels.js +366 -0
- package/src/server/services/opencode-gateway-adapters.js +1168 -0
- package/src/server/services/opencode-gateway-converter.js +639 -0
- package/src/server/services/opencode-sessions.js +931 -0
- package/src/server/services/opencode-settings-manager.js +478 -0
- package/src/server/services/opencode-statistics-service.js +161 -0
- package/src/server/services/plugins-service.js +1268 -0
- package/src/server/services/prompts-service.js +534 -0
- package/src/server/services/proxy-runtime.js +79 -0
- package/src/server/services/repo-scanner-base.js +708 -0
- package/src/server/services/request-logger.js +130 -0
- package/src/server/services/response-decoder.js +21 -0
- package/src/server/services/security-config.js +131 -0
- package/src/server/services/session-cache.js +127 -0
- package/src/server/services/session-converter.js +577 -0
- package/src/server/services/sessions.js +900 -0
- package/src/server/services/settings-manager.js +163 -0
- package/src/server/services/skill-service.js +1482 -0
- package/src/server/services/speed-test.js +1146 -0
- package/src/server/services/statistics-service.js +1043 -0
- package/src/server/services/ui-config.js +132 -0
- package/src/server/services/workspace-service.js +830 -0
- package/src/server/utils/pricing.js +73 -0
- package/src/server/websocket-server.js +513 -0
- package/src/ui/menu.js +139 -0
- package/src/ui/prompts.js +100 -0
- package/src/utils/format.js +43 -0
- package/src/utils/port-helper.js +108 -0
- package/src/utils/session.js +240 -0
|
@@ -0,0 +1,1137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agents 服务
|
|
3
|
+
*
|
|
4
|
+
* 管理 Claude/OpenCode 自定义代理的 CRUD 操作
|
|
5
|
+
* 支持从 GitHub 仓库扫描和安装代理
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const toml = require('toml');
|
|
12
|
+
const tomlStringify = require('@iarna/toml').stringify;
|
|
13
|
+
const { RepoScannerBase } = require('./repo-scanner-base');
|
|
14
|
+
const { NATIVE_PATHS } = require('../../config/paths');
|
|
15
|
+
|
|
16
|
+
// 默认仓库源
|
|
17
|
+
const DEFAULT_REPOS = [];
|
|
18
|
+
const SUPPORTED_PLATFORMS = ['claude', 'codex', 'opencode'];
|
|
19
|
+
const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
|
|
20
|
+
const CODEX_CONFIG_PATH = NATIVE_PATHS.codex.config;
|
|
21
|
+
const CODEX_AGENTS_DIR = path.join(os.homedir(), '.codex', 'agents');
|
|
22
|
+
const CODEX_CONFIG_MODES = new Set(['none', 'managed', 'custom']);
|
|
23
|
+
|
|
24
|
+
const PLATFORM_CONFIG = {
|
|
25
|
+
claude: {
|
|
26
|
+
userAgentsDir: path.join(os.homedir(), '.claude', 'agents'),
|
|
27
|
+
projectAgentsDir: (projectPath) => path.join(projectPath, '.claude', 'agents'),
|
|
28
|
+
repoType: 'agents'
|
|
29
|
+
},
|
|
30
|
+
opencode: {
|
|
31
|
+
userAgentsDir: path.join(OPENCODE_CONFIG_DIR, 'agents'),
|
|
32
|
+
legacyUserAgentsDir: path.join(OPENCODE_CONFIG_DIR, 'agent'),
|
|
33
|
+
projectAgentsDir: (projectPath) => {
|
|
34
|
+
const modern = path.join(projectPath, '.opencode', 'agents');
|
|
35
|
+
const legacy = path.join(projectPath, '.opencode', 'agent');
|
|
36
|
+
if (fs.existsSync(legacy) && !fs.existsSync(modern)) {
|
|
37
|
+
return legacy;
|
|
38
|
+
}
|
|
39
|
+
return modern;
|
|
40
|
+
},
|
|
41
|
+
repoType: 'opencode-agents'
|
|
42
|
+
},
|
|
43
|
+
codex: {
|
|
44
|
+
userAgentsDir: CODEX_AGENTS_DIR,
|
|
45
|
+
projectAgentsDir: () => null,
|
|
46
|
+
repoType: 'agents'
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function normalizePlatform(platform) {
|
|
51
|
+
const normalized = typeof platform === 'string' && platform.trim() ? platform.trim() : 'claude';
|
|
52
|
+
if (!SUPPORTED_PLATFORMS.includes(normalized)) {
|
|
53
|
+
throw new Error(`不支持的平台: ${platform}`);
|
|
54
|
+
}
|
|
55
|
+
return normalized;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function assertSafeAgentFileName(fileName) {
|
|
59
|
+
if (typeof fileName !== 'string') {
|
|
60
|
+
throw new Error('代理文件名必须是字符串');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const normalized = fileName.trim();
|
|
64
|
+
if (!normalized) {
|
|
65
|
+
throw new Error('代理文件名不能为空');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(normalized) || normalized.includes('..')) {
|
|
69
|
+
throw new Error('代理文件名只能包含字母、数字、点号、横杠和下划线,且不能包含连续点');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function assertSafeRepoPath(repoPath) {
|
|
74
|
+
if (typeof repoPath !== 'string' || !repoPath.trim()) {
|
|
75
|
+
throw new Error('代理仓库路径不能为空');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const raw = repoPath.replace(/\\/g, '/').trim();
|
|
79
|
+
const normalized = path.posix.normalize(raw).replace(/^(\.\/)+/, '');
|
|
80
|
+
if (!normalized ||
|
|
81
|
+
normalized === '.' ||
|
|
82
|
+
normalized === '..' ||
|
|
83
|
+
normalized.startsWith('../') ||
|
|
84
|
+
normalized.includes('/../') ||
|
|
85
|
+
path.posix.isAbsolute(normalized)) {
|
|
86
|
+
throw new Error('代理仓库路径不合法');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!normalized.endsWith('.md')) {
|
|
90
|
+
throw new Error('代理仓库路径必须是 .md 文件');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function assertSafeProjectPath(projectPath) {
|
|
95
|
+
if (typeof projectPath !== 'string' || !projectPath.trim()) {
|
|
96
|
+
throw new Error('projectPath 必须是非空字符串');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (projectPath.includes('\0')) {
|
|
100
|
+
throw new Error('projectPath 不合法');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const normalized = path.resolve(projectPath.trim());
|
|
104
|
+
if (!path.isAbsolute(normalized)) {
|
|
105
|
+
throw new Error('projectPath 必须是绝对路径');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!fs.existsSync(normalized)) {
|
|
109
|
+
throw new Error('projectPath 不存在');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const stat = fs.statSync(normalized);
|
|
113
|
+
if (!stat.isDirectory()) {
|
|
114
|
+
throw new Error('projectPath 必须是目录');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return fs.realpathSync(normalized);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 确保目录存在
|
|
122
|
+
*/
|
|
123
|
+
function ensureDir(dirPath) {
|
|
124
|
+
if (!fs.existsSync(dirPath)) {
|
|
125
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function writeFileAtomic(filePath, content) {
|
|
130
|
+
const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
131
|
+
fs.writeFileSync(tempPath, content, 'utf-8');
|
|
132
|
+
fs.renameSync(tempPath, filePath);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function readCodexTomlConfig() {
|
|
136
|
+
if (!fs.existsSync(CODEX_CONFIG_PATH)) {
|
|
137
|
+
return {};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const content = fs.readFileSync(CODEX_CONFIG_PATH, 'utf-8');
|
|
142
|
+
return toml.parse(content);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
throw new Error(`读取 Codex config.toml 失败: ${err.message}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function writeCodexTomlConfig(config) {
|
|
149
|
+
ensureDir(path.dirname(CODEX_CONFIG_PATH));
|
|
150
|
+
writeFileAtomic(CODEX_CONFIG_PATH, tomlStringify(config));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isPlainObject(value) {
|
|
154
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function getCodexManagedAgentConfigPath(fileName) {
|
|
158
|
+
return path.join(CODEX_AGENTS_DIR, `${fileName}.toml`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function normalizeCodexConfigPath(configPath) {
|
|
162
|
+
return typeof configPath === 'string' ? configPath.trim() : '';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function assertSafeCodexConfigPath(configPath) {
|
|
166
|
+
const normalized = normalizeCodexConfigPath(configPath);
|
|
167
|
+
if (!normalized) {
|
|
168
|
+
throw new Error('Codex 自定义 config_file 不能为空');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (normalized.includes('\0')) {
|
|
172
|
+
throw new Error('Codex 自定义 config_file 不合法');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (normalized.startsWith('~/')) {
|
|
176
|
+
const relative = path.posix.normalize(normalized.slice(2).replace(/\\/g, '/'));
|
|
177
|
+
if (!relative ||
|
|
178
|
+
relative === '.' ||
|
|
179
|
+
relative === '..' ||
|
|
180
|
+
relative.startsWith('../') ||
|
|
181
|
+
relative.includes('/../')) {
|
|
182
|
+
throw new Error('Codex 自定义 config_file 不合法');
|
|
183
|
+
}
|
|
184
|
+
return `~/${relative}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (path.isAbsolute(normalized)) {
|
|
188
|
+
return normalized;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const relative = path.posix.normalize(normalized.replace(/\\/g, '/')).replace(/^(\.\/)+/, '');
|
|
192
|
+
if (!relative ||
|
|
193
|
+
relative === '.' ||
|
|
194
|
+
relative === '..' ||
|
|
195
|
+
relative.startsWith('../') ||
|
|
196
|
+
relative.includes('/../') ||
|
|
197
|
+
path.posix.isAbsolute(relative)) {
|
|
198
|
+
throw new Error('Codex 自定义 config_file 不合法');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return relative;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function resolveCodexConfigPath(configPath) {
|
|
205
|
+
const normalized = normalizeCodexConfigPath(configPath);
|
|
206
|
+
if (!normalized) return '';
|
|
207
|
+
|
|
208
|
+
if (normalized.startsWith('~/')) {
|
|
209
|
+
return path.join(os.homedir(), normalized.slice(2));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (path.isAbsolute(normalized)) {
|
|
213
|
+
return normalized;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return path.resolve(path.dirname(CODEX_CONFIG_PATH), normalized);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function isManagedCodexConfigPath(configPath) {
|
|
220
|
+
const resolved = resolveCodexConfigPath(configPath);
|
|
221
|
+
if (!resolved) return false;
|
|
222
|
+
const managedRoot = path.resolve(CODEX_AGENTS_DIR) + path.sep;
|
|
223
|
+
return resolved.startsWith(managedRoot) || resolved === path.resolve(CODEX_AGENTS_DIR);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function getManagedCodexConfigResolvedPath(configPath) {
|
|
227
|
+
const normalized = normalizeCodexConfigPath(configPath);
|
|
228
|
+
if (!normalized || !isManagedCodexConfigPath(normalized)) {
|
|
229
|
+
return '';
|
|
230
|
+
}
|
|
231
|
+
return resolveCodexConfigPath(normalized);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function normalizeCodexConfigMode(mode) {
|
|
235
|
+
const normalized = typeof mode === 'string' ? mode.trim().toLowerCase() : '';
|
|
236
|
+
if (!normalized) {
|
|
237
|
+
return '';
|
|
238
|
+
}
|
|
239
|
+
if (!CODEX_CONFIG_MODES.has(normalized)) {
|
|
240
|
+
throw new Error(`不支持的 Codex configMode: ${mode}`);
|
|
241
|
+
}
|
|
242
|
+
return normalized;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function inferCodexConfigMode(configFile) {
|
|
246
|
+
const normalized = normalizeCodexConfigPath(configFile);
|
|
247
|
+
if (!normalized) return 'none';
|
|
248
|
+
return isManagedCodexConfigPath(normalized) ? 'managed' : 'custom';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function resolveCodexConfigContent({ configContent, model, fallbackContent = '' }) {
|
|
252
|
+
if (typeof configContent === 'string') {
|
|
253
|
+
return configContent;
|
|
254
|
+
}
|
|
255
|
+
const trimmedModel = typeof model === 'string' ? model.trim() : '';
|
|
256
|
+
if (trimmedModel) {
|
|
257
|
+
return tomlStringify({ model: trimmedModel });
|
|
258
|
+
}
|
|
259
|
+
return typeof fallbackContent === 'string' ? fallbackContent : '';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function readCodexAgentConfigFile(configFilePath) {
|
|
263
|
+
if (!configFilePath) {
|
|
264
|
+
return {
|
|
265
|
+
content: '',
|
|
266
|
+
data: null,
|
|
267
|
+
updatedAt: null,
|
|
268
|
+
error: null
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!fs.existsSync(configFilePath)) {
|
|
273
|
+
return {
|
|
274
|
+
content: '',
|
|
275
|
+
data: null,
|
|
276
|
+
updatedAt: null,
|
|
277
|
+
error: `配置文件不存在: ${configFilePath}`
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const content = fs.readFileSync(configFilePath, 'utf-8');
|
|
283
|
+
const updatedAt = fs.statSync(configFilePath).mtime.getTime();
|
|
284
|
+
try {
|
|
285
|
+
const data = toml.parse(content);
|
|
286
|
+
return {
|
|
287
|
+
content,
|
|
288
|
+
data,
|
|
289
|
+
updatedAt,
|
|
290
|
+
error: null
|
|
291
|
+
};
|
|
292
|
+
} catch (parseErr) {
|
|
293
|
+
return {
|
|
294
|
+
content,
|
|
295
|
+
data: null,
|
|
296
|
+
updatedAt,
|
|
297
|
+
error: `配置文件 TOML 解析失败: ${parseErr.message}`
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
} catch (err) {
|
|
301
|
+
return {
|
|
302
|
+
content: '',
|
|
303
|
+
data: null,
|
|
304
|
+
updatedAt: null,
|
|
305
|
+
error: `读取配置文件失败: ${err.message}`
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 解析 YAML frontmatter
|
|
312
|
+
*/
|
|
313
|
+
function parseFrontmatter(content) {
|
|
314
|
+
const result = {
|
|
315
|
+
frontmatter: {},
|
|
316
|
+
body: content
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// 移除 BOM
|
|
320
|
+
content = content.trim().replace(/^\uFEFF/, '');
|
|
321
|
+
|
|
322
|
+
// 解析 YAML frontmatter
|
|
323
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
|
|
324
|
+
if (!match) {
|
|
325
|
+
return result;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const frontmatterText = match[1];
|
|
329
|
+
result.body = match[2].trim();
|
|
330
|
+
|
|
331
|
+
// 简单解析 YAML(支持基本字段)
|
|
332
|
+
const lines = frontmatterText.split('\n');
|
|
333
|
+
for (const line of lines) {
|
|
334
|
+
const colonIndex = line.indexOf(':');
|
|
335
|
+
if (colonIndex === -1) continue;
|
|
336
|
+
|
|
337
|
+
const key = line.slice(0, colonIndex).trim();
|
|
338
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
339
|
+
|
|
340
|
+
// 去除引号
|
|
341
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
342
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
343
|
+
value = value.slice(1, -1);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
result.frontmatter[key] = value;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return result;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* 生成 frontmatter 字符串
|
|
354
|
+
*/
|
|
355
|
+
function generateFrontmatter(data, platform = 'claude') {
|
|
356
|
+
const lines = ['---'];
|
|
357
|
+
|
|
358
|
+
// Claude 下写入 name,OpenCode 以文件名作为 agent id
|
|
359
|
+
if (platform !== 'opencode' && data.name) {
|
|
360
|
+
lines.push(`name: ${data.name}`);
|
|
361
|
+
}
|
|
362
|
+
if (data.description) {
|
|
363
|
+
lines.push(`description: "${data.description}"`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 可选字段
|
|
367
|
+
if (data.tools) {
|
|
368
|
+
lines.push(`tools: ${data.tools}`);
|
|
369
|
+
}
|
|
370
|
+
if (data.model) {
|
|
371
|
+
lines.push(`model: ${data.model}`);
|
|
372
|
+
}
|
|
373
|
+
if (data.permissionMode) {
|
|
374
|
+
lines.push(`permissionMode: ${data.permissionMode}`);
|
|
375
|
+
}
|
|
376
|
+
if (data.skills) {
|
|
377
|
+
lines.push(`skills: ${data.skills}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
lines.push('---');
|
|
381
|
+
return lines.join('\n');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* 扫描目录获取代理文件(agents 约定为扁平目录)
|
|
386
|
+
*/
|
|
387
|
+
function scanAgentsDir(dir, basePath, scope) {
|
|
388
|
+
const agents = [];
|
|
389
|
+
|
|
390
|
+
if (!fs.existsSync(dir)) {
|
|
391
|
+
return agents;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
396
|
+
|
|
397
|
+
for (const entry of entries) {
|
|
398
|
+
const fullPath = path.join(dir, entry.name);
|
|
399
|
+
|
|
400
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
401
|
+
// 解析代理文件
|
|
402
|
+
try {
|
|
403
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
404
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
405
|
+
|
|
406
|
+
// 计算相对路径
|
|
407
|
+
const relativePath = path.relative(basePath, fullPath);
|
|
408
|
+
const fileName = entry.name.replace(/\.md$/, '');
|
|
409
|
+
|
|
410
|
+
agents.push({
|
|
411
|
+
name: frontmatter.name || fileName,
|
|
412
|
+
fileName,
|
|
413
|
+
scope,
|
|
414
|
+
path: relativePath,
|
|
415
|
+
fullPath,
|
|
416
|
+
description: frontmatter.description || '',
|
|
417
|
+
tools: frontmatter.tools || '',
|
|
418
|
+
model: frontmatter.model || '',
|
|
419
|
+
permissionMode: frontmatter.permissionMode || '',
|
|
420
|
+
skills: frontmatter.skills || '',
|
|
421
|
+
systemPrompt: body,
|
|
422
|
+
fullContent: content,
|
|
423
|
+
updatedAt: fs.statSync(fullPath).mtime.getTime()
|
|
424
|
+
});
|
|
425
|
+
} catch (err) {
|
|
426
|
+
console.warn(`[AgentsService] Failed to parse ${fullPath}:`, err.message);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
} catch (err) {
|
|
431
|
+
console.error(`[AgentsService] Failed to scan ${dir}:`, err.message);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return agents;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Agents 仓库扫描器
|
|
439
|
+
*/
|
|
440
|
+
class AgentsRepoScanner extends RepoScannerBase {
|
|
441
|
+
constructor(platform, installDir) {
|
|
442
|
+
super({
|
|
443
|
+
type: PLATFORM_CONFIG[platform]?.repoType || 'agents',
|
|
444
|
+
installDir,
|
|
445
|
+
markerFile: null, // 直接扫描 .md 文件
|
|
446
|
+
fileExtension: '.md',
|
|
447
|
+
defaultRepos: DEFAULT_REPOS
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* 获取并解析单个代理文件
|
|
453
|
+
*/
|
|
454
|
+
async fetchAndParseItem(file, repo, baseDir) {
|
|
455
|
+
try {
|
|
456
|
+
// 计算相对路径
|
|
457
|
+
const relativePath = baseDir ? file.path.slice(baseDir.length + 1) : file.path;
|
|
458
|
+
const fileName = path.basename(file.path, '.md');
|
|
459
|
+
|
|
460
|
+
// 获取文件内容
|
|
461
|
+
const content = await this.fetchRawContent(repo, file.path);
|
|
462
|
+
const { frontmatter, body } = this.parseFrontmatter(content);
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
key: `${repo.owner}/${repo.name}:${relativePath}`,
|
|
466
|
+
name: frontmatter.name || fileName,
|
|
467
|
+
fileName,
|
|
468
|
+
scope: 'remote',
|
|
469
|
+
path: relativePath,
|
|
470
|
+
repoPath: file.path,
|
|
471
|
+
description: frontmatter.description || '',
|
|
472
|
+
tools: frontmatter.tools || '',
|
|
473
|
+
model: frontmatter.model || '',
|
|
474
|
+
permissionMode: frontmatter.permissionMode || '',
|
|
475
|
+
skills: frontmatter.skills || '',
|
|
476
|
+
systemPrompt: body,
|
|
477
|
+
fullContent: content,
|
|
478
|
+
installed: this.isInstalled(fileName),
|
|
479
|
+
readmeUrl: `https://github.com/${repo.owner}/${repo.name}/blob/${repo.branch}/${file.path}`,
|
|
480
|
+
repoOwner: repo.owner,
|
|
481
|
+
repoName: repo.name,
|
|
482
|
+
repoBranch: repo.branch,
|
|
483
|
+
repoDirectory: repo.directory || ''
|
|
484
|
+
};
|
|
485
|
+
} catch (err) {
|
|
486
|
+
console.warn(`[AgentsRepoScanner] Parse agent ${file.path} error:`, err.message);
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* 检查代理是否已安装
|
|
493
|
+
*/
|
|
494
|
+
isInstalled(fileName) {
|
|
495
|
+
const fullPath = path.join(this.installDir, `${fileName}.md`);
|
|
496
|
+
return fs.existsSync(fullPath);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* 获取去重 key
|
|
501
|
+
*/
|
|
502
|
+
getDedupeKey(item) {
|
|
503
|
+
return item.fileName.toLowerCase();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* 安装代理
|
|
508
|
+
*/
|
|
509
|
+
async installAgent(item) {
|
|
510
|
+
assertSafeAgentFileName(item?.fileName);
|
|
511
|
+
assertSafeRepoPath(item?.repoPath);
|
|
512
|
+
|
|
513
|
+
const repo = {
|
|
514
|
+
owner: item.repoOwner,
|
|
515
|
+
name: item.repoName,
|
|
516
|
+
branch: item.repoBranch
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
// 代理安装到根目录,使用文件名
|
|
520
|
+
return this.installFromRepo(item.repoPath, repo, `${item.fileName}.md`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Agents 服务类
|
|
526
|
+
*/
|
|
527
|
+
class AgentsService {
|
|
528
|
+
constructor(platform = 'claude') {
|
|
529
|
+
this.platform = normalizePlatform(platform);
|
|
530
|
+
const config = PLATFORM_CONFIG[this.platform];
|
|
531
|
+
|
|
532
|
+
this.userAgentsDir = config.userAgentsDir;
|
|
533
|
+
if (this.platform === 'opencode') {
|
|
534
|
+
const legacyUserDir = config.legacyUserAgentsDir;
|
|
535
|
+
if (legacyUserDir && fs.existsSync(legacyUserDir) && !fs.existsSync(this.userAgentsDir)) {
|
|
536
|
+
this.userAgentsDir = legacyUserDir;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
this.projectAgentsDir = config.projectAgentsDir;
|
|
541
|
+
this.repoScanner = new AgentsRepoScanner(this.platform, this.userAgentsDir);
|
|
542
|
+
ensureDir(this.userAgentsDir);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
getProjectAgentsDir(projectPath) {
|
|
546
|
+
if (!projectPath) return null;
|
|
547
|
+
const safeProjectPath = assertSafeProjectPath(projectPath);
|
|
548
|
+
return this.projectAgentsDir(safeProjectPath);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* 获取所有代理列表
|
|
553
|
+
* @param {string} projectPath - 项目路径(可选,用于获取项目级代理)
|
|
554
|
+
*/
|
|
555
|
+
listAgents(projectPath = null) {
|
|
556
|
+
if (this.platform === 'codex') {
|
|
557
|
+
return this.listCodexAgents();
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const agents = [];
|
|
561
|
+
|
|
562
|
+
// 获取用户级代理
|
|
563
|
+
const userAgents = scanAgentsDir(this.userAgentsDir, this.userAgentsDir, 'user');
|
|
564
|
+
agents.push(...userAgents);
|
|
565
|
+
|
|
566
|
+
// 获取项目级代理(如果提供了项目路径)
|
|
567
|
+
if (projectPath) {
|
|
568
|
+
const projectAgentsDir = this.getProjectAgentsDir(projectPath);
|
|
569
|
+
const projectAgents = scanAgentsDir(projectAgentsDir, projectAgentsDir, 'project');
|
|
570
|
+
agents.push(...projectAgents);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// 按名称排序
|
|
574
|
+
agents.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
agents,
|
|
578
|
+
total: agents.length,
|
|
579
|
+
userCount: userAgents.length,
|
|
580
|
+
projectCount: agents.length - userAgents.length
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* 获取所有代理(包括远程仓库)
|
|
586
|
+
*/
|
|
587
|
+
async listAllAgents(projectPath = null, forceRefresh = false) {
|
|
588
|
+
if (this.platform === 'codex') {
|
|
589
|
+
const { agents, userCount, projectCount } = this.listCodexAgents();
|
|
590
|
+
return {
|
|
591
|
+
agents,
|
|
592
|
+
total: agents.length,
|
|
593
|
+
userCount,
|
|
594
|
+
projectCount,
|
|
595
|
+
remoteCount: 0
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// 获取本地代理
|
|
600
|
+
const { agents: localAgents, userCount, projectCount } = this.listAgents(projectPath);
|
|
601
|
+
|
|
602
|
+
// 获取远程代理
|
|
603
|
+
let remoteAgents = [];
|
|
604
|
+
try {
|
|
605
|
+
remoteAgents = await this.repoScanner.listRemoteItems(forceRefresh);
|
|
606
|
+
|
|
607
|
+
// 更新安装状态
|
|
608
|
+
for (const agent of remoteAgents) {
|
|
609
|
+
agent.installed = this.repoScanner.isInstalled(agent.fileName);
|
|
610
|
+
}
|
|
611
|
+
} catch (err) {
|
|
612
|
+
console.warn('[AgentsService] Failed to fetch remote agents:', err.message);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// 合并列表(本地优先)
|
|
616
|
+
const allAgents = [...localAgents];
|
|
617
|
+
const localKeys = new Set(localAgents.map(a => a.fileName.toLowerCase()));
|
|
618
|
+
|
|
619
|
+
for (const remote of remoteAgents) {
|
|
620
|
+
if (!localKeys.has(remote.fileName.toLowerCase())) {
|
|
621
|
+
allAgents.push(remote);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// 排序
|
|
626
|
+
allAgents.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
|
|
627
|
+
|
|
628
|
+
return {
|
|
629
|
+
agents: allAgents,
|
|
630
|
+
total: allAgents.length,
|
|
631
|
+
userCount,
|
|
632
|
+
projectCount,
|
|
633
|
+
remoteCount: remoteAgents.length
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* 获取单个代理详情
|
|
639
|
+
*/
|
|
640
|
+
getAgent(fileName, scope, projectPath = null) {
|
|
641
|
+
assertSafeAgentFileName(fileName);
|
|
642
|
+
|
|
643
|
+
if (this.platform === 'codex') {
|
|
644
|
+
return this.getCodexAgent(fileName, scope);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const baseDir = scope === 'user'
|
|
648
|
+
? this.userAgentsDir
|
|
649
|
+
: this.getProjectAgentsDir(projectPath);
|
|
650
|
+
|
|
651
|
+
const filePath = path.join(baseDir, `${fileName}.md`);
|
|
652
|
+
|
|
653
|
+
if (!fs.existsSync(filePath)) {
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
658
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
659
|
+
|
|
660
|
+
return {
|
|
661
|
+
name: frontmatter.name || fileName,
|
|
662
|
+
fileName,
|
|
663
|
+
scope,
|
|
664
|
+
path: `${fileName}.md`,
|
|
665
|
+
fullPath: filePath,
|
|
666
|
+
description: frontmatter.description || '',
|
|
667
|
+
tools: frontmatter.tools || '',
|
|
668
|
+
model: frontmatter.model || '',
|
|
669
|
+
permissionMode: frontmatter.permissionMode || '',
|
|
670
|
+
skills: frontmatter.skills || '',
|
|
671
|
+
systemPrompt: body,
|
|
672
|
+
fullContent: content,
|
|
673
|
+
updatedAt: fs.statSync(filePath).mtime.getTime()
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* 创建代理
|
|
679
|
+
*/
|
|
680
|
+
createAgent({ fileName, scope, projectPath, name, description, tools, model, permissionMode, skills, systemPrompt, configMode, configFile, configContent }) {
|
|
681
|
+
assertSafeAgentFileName(fileName);
|
|
682
|
+
|
|
683
|
+
if (this.platform === 'codex') {
|
|
684
|
+
return this.createCodexAgent({ fileName, scope, description, model, configMode, configFile, configContent });
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (this.platform === 'claude' && (!name || !name.trim())) {
|
|
688
|
+
throw new Error('代理名称不能为空');
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (!description || !description.trim()) {
|
|
692
|
+
throw new Error('代理描述不能为空');
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const baseDir = scope === 'user'
|
|
696
|
+
? this.userAgentsDir
|
|
697
|
+
: this.getProjectAgentsDir(projectPath);
|
|
698
|
+
|
|
699
|
+
ensureDir(baseDir);
|
|
700
|
+
|
|
701
|
+
const filePath = path.join(baseDir, `${fileName}.md`);
|
|
702
|
+
|
|
703
|
+
// 检查是否已存在
|
|
704
|
+
if (fs.existsSync(filePath)) {
|
|
705
|
+
throw new Error(`代理 "${fileName}" 已存在`);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// 生成文件内容
|
|
709
|
+
const frontmatterData = { name: (name || fileName), description };
|
|
710
|
+
if (tools) frontmatterData.tools = tools;
|
|
711
|
+
if (model) frontmatterData.model = model;
|
|
712
|
+
if (permissionMode) frontmatterData.permissionMode = permissionMode;
|
|
713
|
+
if (skills) frontmatterData.skills = skills;
|
|
714
|
+
|
|
715
|
+
const content = generateFrontmatter(frontmatterData, this.platform) + '\n\n' + (systemPrompt || '');
|
|
716
|
+
|
|
717
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
718
|
+
|
|
719
|
+
return this.getAgent(fileName, scope, projectPath);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* 更新代理
|
|
724
|
+
*/
|
|
725
|
+
updateAgent({ fileName, scope, projectPath, name, description, tools, model, permissionMode, skills, systemPrompt, configMode, configFile, configContent }) {
|
|
726
|
+
assertSafeAgentFileName(fileName);
|
|
727
|
+
|
|
728
|
+
if (this.platform === 'codex') {
|
|
729
|
+
return this.updateCodexAgent({ fileName, scope, description, model, configMode, configFile, configContent });
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const baseDir = scope === 'user'
|
|
733
|
+
? this.userAgentsDir
|
|
734
|
+
: this.getProjectAgentsDir(projectPath);
|
|
735
|
+
|
|
736
|
+
const filePath = path.join(baseDir, `${fileName}.md`);
|
|
737
|
+
|
|
738
|
+
if (!fs.existsSync(filePath)) {
|
|
739
|
+
throw new Error(`代理 "${fileName}" 不存在`);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// 生成文件内容
|
|
743
|
+
const frontmatterData = {
|
|
744
|
+
name: name || fileName,
|
|
745
|
+
description: description || ''
|
|
746
|
+
};
|
|
747
|
+
if (tools) frontmatterData.tools = tools;
|
|
748
|
+
if (model) frontmatterData.model = model;
|
|
749
|
+
if (permissionMode) frontmatterData.permissionMode = permissionMode;
|
|
750
|
+
if (skills) frontmatterData.skills = skills;
|
|
751
|
+
|
|
752
|
+
const content = generateFrontmatter(frontmatterData, this.platform) + '\n\n' + (systemPrompt || '');
|
|
753
|
+
|
|
754
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
755
|
+
|
|
756
|
+
return this.getAgent(fileName, scope, projectPath);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* 删除代理
|
|
761
|
+
*/
|
|
762
|
+
deleteAgent(fileName, scope, projectPath = null) {
|
|
763
|
+
assertSafeAgentFileName(fileName);
|
|
764
|
+
|
|
765
|
+
if (this.platform === 'codex') {
|
|
766
|
+
return this.deleteCodexAgent(fileName, scope);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const baseDir = scope === 'user'
|
|
770
|
+
? this.userAgentsDir
|
|
771
|
+
: this.getProjectAgentsDir(projectPath);
|
|
772
|
+
|
|
773
|
+
const filePath = path.join(baseDir, `${fileName}.md`);
|
|
774
|
+
|
|
775
|
+
if (!fs.existsSync(filePath)) {
|
|
776
|
+
return { success: false, message: '代理不存在' };
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
fs.unlinkSync(filePath);
|
|
780
|
+
|
|
781
|
+
return { success: true, message: '代理已删除' };
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* 获取统计信息
|
|
786
|
+
*/
|
|
787
|
+
getStats(projectPath = null) {
|
|
788
|
+
if (this.platform === 'codex') {
|
|
789
|
+
const { agents, userCount, projectCount } = this.listCodexAgents();
|
|
790
|
+
return {
|
|
791
|
+
total: agents.length,
|
|
792
|
+
userCount,
|
|
793
|
+
projectCount,
|
|
794
|
+
models: { default: agents.length }
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const { agents, userCount, projectCount } = this.listAgents(projectPath);
|
|
799
|
+
|
|
800
|
+
// 按模型分组
|
|
801
|
+
const models = {};
|
|
802
|
+
for (const agent of agents) {
|
|
803
|
+
const m = agent.model || 'default';
|
|
804
|
+
if (!models[m]) {
|
|
805
|
+
models[m] = 0;
|
|
806
|
+
}
|
|
807
|
+
models[m]++;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
return {
|
|
811
|
+
total: agents.length,
|
|
812
|
+
userCount,
|
|
813
|
+
projectCount,
|
|
814
|
+
models
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// ==================== 仓库管理 ====================
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* 获取仓库列表
|
|
822
|
+
*/
|
|
823
|
+
getRepos() {
|
|
824
|
+
return this.repoScanner.loadRepos();
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* 添加仓库
|
|
829
|
+
*/
|
|
830
|
+
addRepo(repo) {
|
|
831
|
+
return this.repoScanner.addRepo(repo);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* 删除仓库
|
|
836
|
+
*/
|
|
837
|
+
removeRepo(owner, name, directory = '') {
|
|
838
|
+
return this.repoScanner.removeRepo(owner, name, directory);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* 切换仓库启用状态
|
|
843
|
+
*/
|
|
844
|
+
toggleRepo(owner, name, directory = '', enabled) {
|
|
845
|
+
return this.repoScanner.toggleRepo(owner, name, directory, enabled);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* 从远程仓库安装代理
|
|
850
|
+
*/
|
|
851
|
+
async installFromRemote(agent) {
|
|
852
|
+
if (!agent || typeof agent !== 'object') {
|
|
853
|
+
throw new Error('无效的代理安装参数');
|
|
854
|
+
}
|
|
855
|
+
assertSafeAgentFileName(agent.fileName);
|
|
856
|
+
assertSafeRepoPath(agent.repoPath);
|
|
857
|
+
return this.repoScanner.installAgent(agent);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* 卸载代理
|
|
862
|
+
*/
|
|
863
|
+
uninstallAgent(fileName) {
|
|
864
|
+
assertSafeAgentFileName(fileName);
|
|
865
|
+
return this.repoScanner.uninstall(`${fileName}.md`);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
listCodexAgents() {
|
|
869
|
+
const config = readCodexTomlConfig();
|
|
870
|
+
const agentsTable = isPlainObject(config.agents) ? config.agents : {};
|
|
871
|
+
const agents = [];
|
|
872
|
+
|
|
873
|
+
for (const [key, value] of Object.entries(agentsTable)) {
|
|
874
|
+
if (!isPlainObject(value)) {
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const configFile = normalizeCodexConfigPath(value.config_file);
|
|
879
|
+
const resolvedConfigFile = resolveCodexConfigPath(configFile);
|
|
880
|
+
const configMode = inferCodexConfigMode(configFile);
|
|
881
|
+
const fullPath = configFile || `${key}.toml`;
|
|
882
|
+
let model = '';
|
|
883
|
+
let fullContent = '';
|
|
884
|
+
let configReadError = '';
|
|
885
|
+
let updatedAt = fs.existsSync(CODEX_CONFIG_PATH) ? fs.statSync(CODEX_CONFIG_PATH).mtime.getTime() : Date.now();
|
|
886
|
+
|
|
887
|
+
if (configFile) {
|
|
888
|
+
const parsedConfigFile = readCodexAgentConfigFile(resolvedConfigFile);
|
|
889
|
+
fullContent = parsedConfigFile.content;
|
|
890
|
+
if (isPlainObject(parsedConfigFile.data) && typeof parsedConfigFile.data.model === 'string') {
|
|
891
|
+
model = parsedConfigFile.data.model;
|
|
892
|
+
}
|
|
893
|
+
if (parsedConfigFile.updatedAt) {
|
|
894
|
+
updatedAt = Math.max(updatedAt, parsedConfigFile.updatedAt);
|
|
895
|
+
}
|
|
896
|
+
if (parsedConfigFile.error) {
|
|
897
|
+
configReadError = parsedConfigFile.error;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
agents.push({
|
|
902
|
+
name: key,
|
|
903
|
+
fileName: key,
|
|
904
|
+
scope: 'user',
|
|
905
|
+
path: fullPath,
|
|
906
|
+
fullPath,
|
|
907
|
+
description: value.description || '',
|
|
908
|
+
tools: '',
|
|
909
|
+
model,
|
|
910
|
+
permissionMode: '',
|
|
911
|
+
skills: '',
|
|
912
|
+
systemPrompt: '',
|
|
913
|
+
fullContent,
|
|
914
|
+
configFile,
|
|
915
|
+
configMode,
|
|
916
|
+
configReadError,
|
|
917
|
+
resolvedConfigFile,
|
|
918
|
+
updatedAt
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
agents.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
|
|
923
|
+
|
|
924
|
+
return {
|
|
925
|
+
agents,
|
|
926
|
+
total: agents.length,
|
|
927
|
+
userCount: agents.length,
|
|
928
|
+
projectCount: 0
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
getCodexAgent(fileName, scope) {
|
|
933
|
+
assertSafeAgentFileName(fileName);
|
|
934
|
+
|
|
935
|
+
if (scope !== 'user') {
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const { agents } = this.listCodexAgents();
|
|
940
|
+
return agents.find(agent => agent.fileName === fileName) || null;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
createCodexAgent({ fileName, scope, description, model, configMode, configFile, configContent }) {
|
|
944
|
+
assertSafeAgentFileName(fileName);
|
|
945
|
+
|
|
946
|
+
if (scope !== 'user') {
|
|
947
|
+
throw new Error('Codex 仅支持用户级代理');
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (!description || !description.trim()) {
|
|
951
|
+
throw new Error('代理描述不能为空');
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const config = readCodexTomlConfig();
|
|
955
|
+
config.features = isPlainObject(config.features) ? config.features : {};
|
|
956
|
+
config.features.multi_agent = true;
|
|
957
|
+
config.agents = isPlainObject(config.agents) ? config.agents : {};
|
|
958
|
+
|
|
959
|
+
if (Object.prototype.hasOwnProperty.call(config.agents, fileName)) {
|
|
960
|
+
if (isPlainObject(config.agents[fileName])) {
|
|
961
|
+
throw new Error(`代理 "${fileName}" 已存在`);
|
|
962
|
+
}
|
|
963
|
+
throw new Error(`代理文件名 "${fileName}" 与 Codex 全局 agents 配置冲突`);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const agentConfig = { description: description.trim() };
|
|
967
|
+
const normalizedMode = normalizeCodexConfigMode(configMode);
|
|
968
|
+
|
|
969
|
+
if (!normalizedMode) {
|
|
970
|
+
const trimmedModel = (model || '').trim();
|
|
971
|
+
if (trimmedModel) {
|
|
972
|
+
ensureDir(CODEX_AGENTS_DIR);
|
|
973
|
+
const configFilePath = getCodexManagedAgentConfigPath(fileName);
|
|
974
|
+
writeFileAtomic(configFilePath, tomlStringify({ model: trimmedModel }));
|
|
975
|
+
agentConfig.config_file = configFilePath;
|
|
976
|
+
}
|
|
977
|
+
} else if (normalizedMode === 'managed') {
|
|
978
|
+
ensureDir(CODEX_AGENTS_DIR);
|
|
979
|
+
const configFilePath = getCodexManagedAgentConfigPath(fileName);
|
|
980
|
+
const content = resolveCodexConfigContent({ configContent, model });
|
|
981
|
+
writeFileAtomic(configFilePath, content);
|
|
982
|
+
agentConfig.config_file = configFilePath;
|
|
983
|
+
} else if (normalizedMode === 'custom') {
|
|
984
|
+
const safeConfigFile = assertSafeCodexConfigPath(configFile);
|
|
985
|
+
const resolvedPath = resolveCodexConfigPath(safeConfigFile);
|
|
986
|
+
ensureDir(path.dirname(resolvedPath));
|
|
987
|
+
const content = resolveCodexConfigContent({ configContent, model });
|
|
988
|
+
writeFileAtomic(resolvedPath, content);
|
|
989
|
+
agentConfig.config_file = safeConfigFile;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
config.agents[fileName] = agentConfig;
|
|
993
|
+
writeCodexTomlConfig(config);
|
|
994
|
+
|
|
995
|
+
return this.getCodexAgent(fileName, scope);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
updateCodexAgent({ fileName, scope, description, model, configMode, configFile, configContent }) {
|
|
999
|
+
assertSafeAgentFileName(fileName);
|
|
1000
|
+
|
|
1001
|
+
if (scope !== 'user') {
|
|
1002
|
+
throw new Error('Codex 仅支持用户级代理');
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const config = readCodexTomlConfig();
|
|
1006
|
+
config.features = isPlainObject(config.features) ? config.features : {};
|
|
1007
|
+
config.features.multi_agent = true;
|
|
1008
|
+
config.agents = isPlainObject(config.agents) ? config.agents : {};
|
|
1009
|
+
|
|
1010
|
+
const existingAgent = config.agents[fileName];
|
|
1011
|
+
if (!isPlainObject(existingAgent)) {
|
|
1012
|
+
throw new Error(`代理 "${fileName}" 不存在`);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const agentConfig = { ...existingAgent };
|
|
1016
|
+
agentConfig.description = (description || '').trim();
|
|
1017
|
+
|
|
1018
|
+
const existingConfigFile = normalizeCodexConfigPath(agentConfig.config_file);
|
|
1019
|
+
const isExistingManagedConfig = isManagedCodexConfigPath(existingConfigFile);
|
|
1020
|
+
const resolvedExistingConfigFile = isExistingManagedConfig ? resolveCodexConfigPath(existingConfigFile) : '';
|
|
1021
|
+
const normalizedMode = normalizeCodexConfigMode(configMode);
|
|
1022
|
+
|
|
1023
|
+
if (!normalizedMode) {
|
|
1024
|
+
const trimmedModel = (model || '').trim();
|
|
1025
|
+
if (trimmedModel) {
|
|
1026
|
+
ensureDir(CODEX_AGENTS_DIR);
|
|
1027
|
+
const configFilePath = isExistingManagedConfig ? existingConfigFile : getCodexManagedAgentConfigPath(fileName);
|
|
1028
|
+
const resolvedConfigFilePath = resolveCodexConfigPath(configFilePath);
|
|
1029
|
+
const parsedConfigFile = readCodexAgentConfigFile(resolvedConfigFilePath);
|
|
1030
|
+
const configFileData = isPlainObject(parsedConfigFile?.data) ? parsedConfigFile.data : {};
|
|
1031
|
+
configFileData.model = trimmedModel;
|
|
1032
|
+
ensureDir(path.dirname(resolvedConfigFilePath));
|
|
1033
|
+
writeFileAtomic(resolvedConfigFilePath, tomlStringify(configFileData));
|
|
1034
|
+
agentConfig.config_file = configFilePath;
|
|
1035
|
+
} else if (isExistingManagedConfig) {
|
|
1036
|
+
delete agentConfig.config_file;
|
|
1037
|
+
if (resolvedExistingConfigFile && fs.existsSync(resolvedExistingConfigFile)) {
|
|
1038
|
+
fs.unlinkSync(resolvedExistingConfigFile);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
} else if (normalizedMode === 'none') {
|
|
1042
|
+
delete agentConfig.config_file;
|
|
1043
|
+
if (resolvedExistingConfigFile && fs.existsSync(resolvedExistingConfigFile)) {
|
|
1044
|
+
fs.unlinkSync(resolvedExistingConfigFile);
|
|
1045
|
+
}
|
|
1046
|
+
} else if (normalizedMode === 'managed') {
|
|
1047
|
+
ensureDir(CODEX_AGENTS_DIR);
|
|
1048
|
+
const configFilePath = getCodexManagedAgentConfigPath(fileName);
|
|
1049
|
+
const resolvedConfigFilePath = resolveCodexConfigPath(configFilePath);
|
|
1050
|
+
const existingManagedContent = fs.existsSync(resolvedConfigFilePath)
|
|
1051
|
+
? fs.readFileSync(resolvedConfigFilePath, 'utf-8')
|
|
1052
|
+
: '';
|
|
1053
|
+
const content = resolveCodexConfigContent({
|
|
1054
|
+
configContent,
|
|
1055
|
+
model,
|
|
1056
|
+
fallbackContent: existingManagedContent
|
|
1057
|
+
});
|
|
1058
|
+
ensureDir(path.dirname(resolvedConfigFilePath));
|
|
1059
|
+
writeFileAtomic(resolvedConfigFilePath, content);
|
|
1060
|
+
agentConfig.config_file = configFilePath;
|
|
1061
|
+
if (resolvedExistingConfigFile &&
|
|
1062
|
+
resolvedExistingConfigFile !== resolvedConfigFilePath &&
|
|
1063
|
+
fs.existsSync(resolvedExistingConfigFile)) {
|
|
1064
|
+
fs.unlinkSync(resolvedExistingConfigFile);
|
|
1065
|
+
}
|
|
1066
|
+
} else if (normalizedMode === 'custom') {
|
|
1067
|
+
let targetConfigPath = normalizeCodexConfigPath(configFile);
|
|
1068
|
+
if (!targetConfigPath) {
|
|
1069
|
+
if (existingConfigFile && !isExistingManagedConfig) {
|
|
1070
|
+
targetConfigPath = existingConfigFile;
|
|
1071
|
+
} else {
|
|
1072
|
+
throw new Error('custom 模式需要提供 configFile');
|
|
1073
|
+
}
|
|
1074
|
+
} else {
|
|
1075
|
+
targetConfigPath = assertSafeCodexConfigPath(targetConfigPath);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const resolvedTargetConfigPath = resolveCodexConfigPath(targetConfigPath);
|
|
1079
|
+
const existingContent = fs.existsSync(resolvedTargetConfigPath)
|
|
1080
|
+
? fs.readFileSync(resolvedTargetConfigPath, 'utf-8')
|
|
1081
|
+
: '';
|
|
1082
|
+
const content = resolveCodexConfigContent({
|
|
1083
|
+
configContent,
|
|
1084
|
+
model,
|
|
1085
|
+
fallbackContent: existingContent
|
|
1086
|
+
});
|
|
1087
|
+
ensureDir(path.dirname(resolvedTargetConfigPath));
|
|
1088
|
+
writeFileAtomic(resolvedTargetConfigPath, content);
|
|
1089
|
+
agentConfig.config_file = targetConfigPath;
|
|
1090
|
+
if (resolvedExistingConfigFile &&
|
|
1091
|
+
resolvedExistingConfigFile !== resolvedTargetConfigPath &&
|
|
1092
|
+
fs.existsSync(resolvedExistingConfigFile)) {
|
|
1093
|
+
fs.unlinkSync(resolvedExistingConfigFile);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
config.agents[fileName] = agentConfig;
|
|
1098
|
+
writeCodexTomlConfig(config);
|
|
1099
|
+
|
|
1100
|
+
return this.getCodexAgent(fileName, scope);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
deleteCodexAgent(fileName, scope) {
|
|
1104
|
+
assertSafeAgentFileName(fileName);
|
|
1105
|
+
|
|
1106
|
+
if (scope !== 'user') {
|
|
1107
|
+
throw new Error('Codex 仅支持用户级代理');
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
const config = readCodexTomlConfig();
|
|
1111
|
+
config.agents = isPlainObject(config.agents) ? config.agents : {};
|
|
1112
|
+
|
|
1113
|
+
const existingAgent = config.agents[fileName];
|
|
1114
|
+
if (!isPlainObject(existingAgent)) {
|
|
1115
|
+
return { success: false, message: '代理不存在' };
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
const existingConfigFile = normalizeCodexConfigPath(existingAgent.config_file);
|
|
1119
|
+
const resolvedExistingConfigFile = resolveCodexConfigPath(existingConfigFile);
|
|
1120
|
+
if (existingConfigFile &&
|
|
1121
|
+
isManagedCodexConfigPath(existingConfigFile) &&
|
|
1122
|
+
resolvedExistingConfigFile &&
|
|
1123
|
+
fs.existsSync(resolvedExistingConfigFile)) {
|
|
1124
|
+
fs.unlinkSync(resolvedExistingConfigFile);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
delete config.agents[fileName];
|
|
1128
|
+
writeCodexTomlConfig(config);
|
|
1129
|
+
|
|
1130
|
+
return { success: true, message: '代理已删除' };
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
module.exports = {
|
|
1135
|
+
AgentsService,
|
|
1136
|
+
DEFAULT_REPOS
|
|
1137
|
+
};
|