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,826 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agents API 路由
|
|
3
|
+
*
|
|
4
|
+
* 管理 Claude Code 自定义代理
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const express = require('express');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { AgentsService } = require('../services/agents-service');
|
|
12
|
+
const { PATHS } = require('../../config/paths');
|
|
13
|
+
|
|
14
|
+
const router = express.Router();
|
|
15
|
+
const SUPPORTED_PLATFORMS = ['claude', 'codex', 'opencode'];
|
|
16
|
+
const agentServices = new Map();
|
|
17
|
+
const DEFAULT_PROJECT_ALLOWED_ROOTS = [os.homedir(), process.cwd()];
|
|
18
|
+
|
|
19
|
+
function isSupportedPlatform(platform) {
|
|
20
|
+
return SUPPORTED_PLATFORMS.includes(platform);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getRawPlatform(req) {
|
|
24
|
+
const queryPlatform = typeof req.query?.platform === 'string' ? req.query.platform.trim() : '';
|
|
25
|
+
const bodyPlatform = typeof req.body?.platform === 'string' ? req.body.platform.trim() : '';
|
|
26
|
+
return queryPlatform || bodyPlatform || '';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolvePlatform(rawPlatform) {
|
|
30
|
+
return rawPlatform || 'claude';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getPlatform(req) {
|
|
34
|
+
return resolvePlatform(getRawPlatform(req));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getAgentsService(req) {
|
|
38
|
+
const platform = getPlatform(req);
|
|
39
|
+
if (!agentServices.has(platform)) {
|
|
40
|
+
agentServices.set(platform, new AgentsService(platform));
|
|
41
|
+
}
|
|
42
|
+
return { platform, service: agentServices.get(platform) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function validateScopeForPlatform(scope, platform, projectPath) {
|
|
46
|
+
if (!['user', 'project'].includes(scope)) {
|
|
47
|
+
return '无效的 scope,必须是 user 或 project';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (platform === 'codex' && scope !== 'user') {
|
|
51
|
+
return 'Codex 平台仅支持 user 作用域代理';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (scope === 'project' && !projectPath) {
|
|
55
|
+
return '项目级代理需要提供 projectPath';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function validateAgentFileName(fileName) {
|
|
62
|
+
if (typeof fileName !== 'string') {
|
|
63
|
+
return '代理文件名必须是字符串';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const normalized = fileName.trim();
|
|
67
|
+
if (!normalized) {
|
|
68
|
+
return '代理文件名不能为空';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(normalized) || normalized.includes('..')) {
|
|
72
|
+
return '代理文件名只能包含字母、数字、点号、横杠和下划线,且不能包含连续点';
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isCodexRepoOperationUnsupported(platform) {
|
|
78
|
+
return platform === 'codex';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function validateRepoPath(repoPath) {
|
|
82
|
+
if (typeof repoPath !== 'string' || !repoPath.trim()) {
|
|
83
|
+
return '代理仓库路径不能为空';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const raw = repoPath.replace(/\\/g, '/').trim();
|
|
87
|
+
const normalized = path.posix.normalize(raw).replace(/^(\.\/)+/, '');
|
|
88
|
+
if (!normalized ||
|
|
89
|
+
normalized === '.' ||
|
|
90
|
+
normalized === '..' ||
|
|
91
|
+
normalized.startsWith('../') ||
|
|
92
|
+
normalized.includes('/../') ||
|
|
93
|
+
path.posix.isAbsolute(normalized)) {
|
|
94
|
+
return '代理仓库路径不合法';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!normalized.endsWith('.md')) {
|
|
98
|
+
return '代理仓库路径必须是 .md 文件';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getAllowedProjectRoots() {
|
|
105
|
+
const roots = new Set(DEFAULT_PROJECT_ALLOWED_ROOTS.map(item => path.resolve(item)));
|
|
106
|
+
|
|
107
|
+
// 从工作区配置中扩展允许目录,避免误拦截外部磁盘/自定义根目录项目
|
|
108
|
+
try {
|
|
109
|
+
const workspaceConfigPath = path.join(PATHS.base, 'workspaces.json');
|
|
110
|
+
if (fs.existsSync(workspaceConfigPath)) {
|
|
111
|
+
const raw = fs.readFileSync(workspaceConfigPath, 'utf-8');
|
|
112
|
+
const parsed = JSON.parse(raw || '{}');
|
|
113
|
+
const workspaces = Array.isArray(parsed.workspaces) ? parsed.workspaces : [];
|
|
114
|
+
|
|
115
|
+
for (const workspace of workspaces) {
|
|
116
|
+
if (workspace && typeof workspace.path === 'string' && workspace.path.trim()) {
|
|
117
|
+
roots.add(path.resolve(workspace.path.trim()));
|
|
118
|
+
}
|
|
119
|
+
const projects = Array.isArray(workspace?.projects) ? workspace.projects : [];
|
|
120
|
+
for (const project of projects) {
|
|
121
|
+
if (project && typeof project.sourcePath === 'string' && project.sourcePath.trim()) {
|
|
122
|
+
roots.add(path.resolve(project.sourcePath.trim()));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch (err) {
|
|
128
|
+
// 忽略工作区配置读取失败,使用默认白名单继续
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const raw = typeof process.env.CC_TOOL_PROJECT_PATH_ALLOWLIST === 'string'
|
|
132
|
+
? process.env.CC_TOOL_PROJECT_PATH_ALLOWLIST
|
|
133
|
+
: '';
|
|
134
|
+
const configuredRoots = raw
|
|
135
|
+
.split(path.delimiter)
|
|
136
|
+
.map(item => item.trim())
|
|
137
|
+
.filter(Boolean);
|
|
138
|
+
|
|
139
|
+
if (configuredRoots.length > 0) {
|
|
140
|
+
for (const configuredRoot of configuredRoots) {
|
|
141
|
+
roots.add(path.resolve(configuredRoot));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return Array.from(roots);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isPathInside(basePath, targetPath) {
|
|
149
|
+
return targetPath === basePath || targetPath.startsWith(`${basePath}${path.sep}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function validateProjectPath(projectPath) {
|
|
153
|
+
if (typeof projectPath !== 'string' || !projectPath.trim()) {
|
|
154
|
+
return { error: 'projectPath 必须是非空字符串' };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (projectPath.includes('\0')) {
|
|
158
|
+
return { error: 'projectPath 不合法' };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const trimmed = projectPath.trim();
|
|
162
|
+
if (!path.isAbsolute(trimmed)) {
|
|
163
|
+
return { error: 'projectPath 必须是绝对路径' };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const resolvedPath = path.resolve(trimmed);
|
|
167
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
168
|
+
return { error: 'projectPath 不存在' };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const stat = fs.statSync(resolvedPath);
|
|
172
|
+
if (!stat.isDirectory()) {
|
|
173
|
+
return { error: 'projectPath 必须是目录' };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const realProjectPath = fs.realpathSync(resolvedPath);
|
|
177
|
+
const allowedRoots = getAllowedProjectRoots();
|
|
178
|
+
const isAllowed = allowedRoots.some((rootPath) => {
|
|
179
|
+
try {
|
|
180
|
+
const resolvedRoot = path.resolve(rootPath);
|
|
181
|
+
if (!fs.existsSync(resolvedRoot)) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
const realRootPath = fs.realpathSync(resolvedRoot);
|
|
185
|
+
return isPathInside(realRootPath, realProjectPath);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (!isAllowed) {
|
|
192
|
+
return { error: 'projectPath 不在允许的项目目录范围内' };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { projectPath: realProjectPath };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function normalizeProjectPathForScope(scope, projectPath) {
|
|
199
|
+
if (scope !== 'project') {
|
|
200
|
+
return { projectPath: null };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return validateProjectPath(projectPath);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function normalizeOptionalProjectPath(projectPath) {
|
|
207
|
+
if (projectPath == null || projectPath === '') {
|
|
208
|
+
return { projectPath: null };
|
|
209
|
+
}
|
|
210
|
+
return validateProjectPath(projectPath);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
router.use((req, res, next) => {
|
|
214
|
+
const rawPlatform = getRawPlatform(req);
|
|
215
|
+
if (rawPlatform && !isSupportedPlatform(rawPlatform)) {
|
|
216
|
+
return res.status(400).json({
|
|
217
|
+
success: false,
|
|
218
|
+
message: `不支持的平台: ${rawPlatform}`
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
next();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 获取代理列表
|
|
226
|
+
* GET /api/agents
|
|
227
|
+
* Query: projectPath - 项目路径(可选,用于获取项目级代理)
|
|
228
|
+
*/
|
|
229
|
+
router.get('/', (req, res) => {
|
|
230
|
+
try {
|
|
231
|
+
const { platform, service } = getAgentsService(req);
|
|
232
|
+
const { projectPath } = req.query;
|
|
233
|
+
const normalizedProjectPath = normalizeOptionalProjectPath(projectPath);
|
|
234
|
+
if (normalizedProjectPath.error) {
|
|
235
|
+
return res.status(400).json({
|
|
236
|
+
success: false,
|
|
237
|
+
message: normalizedProjectPath.error
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
const result = service.listAgents(normalizedProjectPath.projectPath);
|
|
241
|
+
|
|
242
|
+
res.json({
|
|
243
|
+
success: true,
|
|
244
|
+
platform,
|
|
245
|
+
...result
|
|
246
|
+
});
|
|
247
|
+
} catch (err) {
|
|
248
|
+
console.error('[Agents API] List agents error:', err);
|
|
249
|
+
res.status(500).json({
|
|
250
|
+
success: false,
|
|
251
|
+
message: err.message
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 获取代理统计
|
|
258
|
+
* GET /api/agents/stats
|
|
259
|
+
*/
|
|
260
|
+
router.get('/stats', (req, res) => {
|
|
261
|
+
try {
|
|
262
|
+
const { platform, service } = getAgentsService(req);
|
|
263
|
+
const { projectPath } = req.query;
|
|
264
|
+
const normalizedProjectPath = normalizeOptionalProjectPath(projectPath);
|
|
265
|
+
if (normalizedProjectPath.error) {
|
|
266
|
+
return res.status(400).json({
|
|
267
|
+
success: false,
|
|
268
|
+
message: normalizedProjectPath.error
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
const stats = service.getStats(normalizedProjectPath.projectPath);
|
|
272
|
+
|
|
273
|
+
res.json({
|
|
274
|
+
success: true,
|
|
275
|
+
platform,
|
|
276
|
+
...stats
|
|
277
|
+
});
|
|
278
|
+
} catch (err) {
|
|
279
|
+
console.error('[Agents API] Get stats error:', err);
|
|
280
|
+
res.status(500).json({
|
|
281
|
+
success: false,
|
|
282
|
+
message: err.message
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* 获取单个代理详情
|
|
289
|
+
* GET /api/agents/:scope/:fileName
|
|
290
|
+
*/
|
|
291
|
+
router.get('/:scope/:fileName', (req, res) => {
|
|
292
|
+
try {
|
|
293
|
+
const { platform, service } = getAgentsService(req);
|
|
294
|
+
const { scope, fileName } = req.params;
|
|
295
|
+
const { projectPath } = req.query;
|
|
296
|
+
|
|
297
|
+
const scopeError = validateScopeForPlatform(scope, platform, projectPath);
|
|
298
|
+
if (scopeError) {
|
|
299
|
+
return res.status(400).json({
|
|
300
|
+
success: false,
|
|
301
|
+
message: scopeError
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const fileNameError = validateAgentFileName(fileName);
|
|
306
|
+
if (fileNameError) {
|
|
307
|
+
return res.status(400).json({
|
|
308
|
+
success: false,
|
|
309
|
+
message: fileNameError
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const normalizedProjectPath = normalizeProjectPathForScope(scope, projectPath);
|
|
314
|
+
if (normalizedProjectPath.error) {
|
|
315
|
+
return res.status(400).json({
|
|
316
|
+
success: false,
|
|
317
|
+
message: normalizedProjectPath.error
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const agent = service.getAgent(fileName, scope, normalizedProjectPath.projectPath);
|
|
322
|
+
|
|
323
|
+
if (!agent) {
|
|
324
|
+
return res.status(404).json({
|
|
325
|
+
success: false,
|
|
326
|
+
message: `代理 "${fileName}" 不存在`
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
res.json({
|
|
331
|
+
success: true,
|
|
332
|
+
platform,
|
|
333
|
+
agent
|
|
334
|
+
});
|
|
335
|
+
} catch (err) {
|
|
336
|
+
console.error('[Agents API] Get agent error:', err);
|
|
337
|
+
res.status(500).json({
|
|
338
|
+
success: false,
|
|
339
|
+
message: err.message
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* 创建代理
|
|
346
|
+
* POST /api/agents
|
|
347
|
+
* Body: { fileName, scope, projectPath?, name, description, tools?, model?, permissionMode?, skills?, systemPrompt? }
|
|
348
|
+
*/
|
|
349
|
+
router.post('/', (req, res) => {
|
|
350
|
+
try {
|
|
351
|
+
const { platform, service } = getAgentsService(req);
|
|
352
|
+
const {
|
|
353
|
+
fileName,
|
|
354
|
+
scope,
|
|
355
|
+
projectPath,
|
|
356
|
+
name,
|
|
357
|
+
description,
|
|
358
|
+
tools,
|
|
359
|
+
model,
|
|
360
|
+
permissionMode,
|
|
361
|
+
skills,
|
|
362
|
+
systemPrompt,
|
|
363
|
+
configMode,
|
|
364
|
+
configFile,
|
|
365
|
+
configContent
|
|
366
|
+
} = req.body;
|
|
367
|
+
|
|
368
|
+
if (!fileName) {
|
|
369
|
+
return res.status(400).json({
|
|
370
|
+
success: false,
|
|
371
|
+
message: '代理文件名不能为空'
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const fileNameError = validateAgentFileName(fileName);
|
|
376
|
+
if (fileNameError) {
|
|
377
|
+
return res.status(400).json({
|
|
378
|
+
success: false,
|
|
379
|
+
message: fileNameError
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const scopeError = validateScopeForPlatform(scope, platform, projectPath);
|
|
384
|
+
if (scopeError) {
|
|
385
|
+
return res.status(400).json({
|
|
386
|
+
success: false,
|
|
387
|
+
message: scopeError
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const normalizedProjectPath = normalizeProjectPathForScope(scope, projectPath);
|
|
392
|
+
if (normalizedProjectPath.error) {
|
|
393
|
+
return res.status(400).json({
|
|
394
|
+
success: false,
|
|
395
|
+
message: normalizedProjectPath.error
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const agent = service.createAgent({
|
|
400
|
+
fileName,
|
|
401
|
+
scope,
|
|
402
|
+
projectPath: normalizedProjectPath.projectPath,
|
|
403
|
+
name: name || fileName,
|
|
404
|
+
description: description || '',
|
|
405
|
+
tools: tools || '',
|
|
406
|
+
model: model || '',
|
|
407
|
+
permissionMode: permissionMode || '',
|
|
408
|
+
skills: skills || '',
|
|
409
|
+
systemPrompt: systemPrompt || '',
|
|
410
|
+
configMode,
|
|
411
|
+
configFile,
|
|
412
|
+
configContent
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
res.json({
|
|
416
|
+
success: true,
|
|
417
|
+
platform,
|
|
418
|
+
agent,
|
|
419
|
+
message: '代理创建成功'
|
|
420
|
+
});
|
|
421
|
+
} catch (err) {
|
|
422
|
+
console.error('[Agents API] Create agent error:', err);
|
|
423
|
+
res.status(500).json({
|
|
424
|
+
success: false,
|
|
425
|
+
message: err.message
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* 更新代理
|
|
432
|
+
* PUT /api/agents/:scope/:fileName
|
|
433
|
+
*/
|
|
434
|
+
router.put('/:scope/:fileName', (req, res) => {
|
|
435
|
+
try {
|
|
436
|
+
const { platform, service } = getAgentsService(req);
|
|
437
|
+
const { scope, fileName } = req.params;
|
|
438
|
+
const {
|
|
439
|
+
projectPath,
|
|
440
|
+
name,
|
|
441
|
+
description,
|
|
442
|
+
tools,
|
|
443
|
+
model,
|
|
444
|
+
permissionMode,
|
|
445
|
+
skills,
|
|
446
|
+
systemPrompt,
|
|
447
|
+
configMode,
|
|
448
|
+
configFile,
|
|
449
|
+
configContent
|
|
450
|
+
} = req.body;
|
|
451
|
+
|
|
452
|
+
const scopeError = validateScopeForPlatform(scope, platform, projectPath);
|
|
453
|
+
if (scopeError) {
|
|
454
|
+
return res.status(400).json({
|
|
455
|
+
success: false,
|
|
456
|
+
message: scopeError
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const fileNameError = validateAgentFileName(fileName);
|
|
461
|
+
if (fileNameError) {
|
|
462
|
+
return res.status(400).json({
|
|
463
|
+
success: false,
|
|
464
|
+
message: fileNameError
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const normalizedProjectPath = normalizeProjectPathForScope(scope, projectPath);
|
|
469
|
+
if (normalizedProjectPath.error) {
|
|
470
|
+
return res.status(400).json({
|
|
471
|
+
success: false,
|
|
472
|
+
message: normalizedProjectPath.error
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const agent = service.updateAgent({
|
|
477
|
+
fileName,
|
|
478
|
+
scope,
|
|
479
|
+
projectPath: normalizedProjectPath.projectPath,
|
|
480
|
+
name: name || fileName,
|
|
481
|
+
description: description || '',
|
|
482
|
+
tools: tools || '',
|
|
483
|
+
model: model || '',
|
|
484
|
+
permissionMode: permissionMode || '',
|
|
485
|
+
skills: skills || '',
|
|
486
|
+
systemPrompt: systemPrompt || '',
|
|
487
|
+
configMode,
|
|
488
|
+
configFile,
|
|
489
|
+
configContent
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
res.json({
|
|
493
|
+
success: true,
|
|
494
|
+
platform,
|
|
495
|
+
agent,
|
|
496
|
+
message: '代理更新成功'
|
|
497
|
+
});
|
|
498
|
+
} catch (err) {
|
|
499
|
+
console.error('[Agents API] Update agent error:', err);
|
|
500
|
+
res.status(500).json({
|
|
501
|
+
success: false,
|
|
502
|
+
message: err.message
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* 删除代理
|
|
509
|
+
* DELETE /api/agents/:scope/:fileName
|
|
510
|
+
*/
|
|
511
|
+
router.delete('/:scope/:fileName', (req, res) => {
|
|
512
|
+
try {
|
|
513
|
+
const { platform, service } = getAgentsService(req);
|
|
514
|
+
const { scope, fileName } = req.params;
|
|
515
|
+
const { projectPath } = req.query;
|
|
516
|
+
|
|
517
|
+
const scopeError = validateScopeForPlatform(scope, platform, projectPath);
|
|
518
|
+
if (scopeError) {
|
|
519
|
+
return res.status(400).json({
|
|
520
|
+
success: false,
|
|
521
|
+
message: scopeError
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const normalizedProjectPath = normalizeProjectPathForScope(scope, projectPath);
|
|
526
|
+
if (normalizedProjectPath.error) {
|
|
527
|
+
return res.status(400).json({
|
|
528
|
+
success: false,
|
|
529
|
+
message: normalizedProjectPath.error
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const fileNameError = validateAgentFileName(fileName);
|
|
534
|
+
if (fileNameError) {
|
|
535
|
+
return res.status(400).json({
|
|
536
|
+
success: false,
|
|
537
|
+
message: fileNameError
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const result = service.deleteAgent(fileName, scope, normalizedProjectPath.projectPath);
|
|
542
|
+
|
|
543
|
+
res.json({
|
|
544
|
+
platform,
|
|
545
|
+
success: result.success,
|
|
546
|
+
message: result.message
|
|
547
|
+
});
|
|
548
|
+
} catch (err) {
|
|
549
|
+
console.error('[Agents API] Delete agent error:', err);
|
|
550
|
+
res.status(500).json({
|
|
551
|
+
success: false,
|
|
552
|
+
message: err.message
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// ==================== 仓库管理 API ====================
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* 获取所有代理(包括远程仓库)
|
|
561
|
+
* GET /api/agents/all
|
|
562
|
+
* Query: projectPath, refresh=1 强制刷新缓存
|
|
563
|
+
*/
|
|
564
|
+
router.get('/all', async (req, res) => {
|
|
565
|
+
try {
|
|
566
|
+
const { platform, service } = getAgentsService(req);
|
|
567
|
+
const { projectPath, refresh } = req.query;
|
|
568
|
+
const normalizedProjectPath = normalizeOptionalProjectPath(projectPath);
|
|
569
|
+
if (normalizedProjectPath.error) {
|
|
570
|
+
return res.status(400).json({
|
|
571
|
+
success: false,
|
|
572
|
+
message: normalizedProjectPath.error
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
const forceRefresh = refresh === '1';
|
|
576
|
+
const result = await service.listAllAgents(normalizedProjectPath.projectPath, forceRefresh);
|
|
577
|
+
|
|
578
|
+
res.json({
|
|
579
|
+
success: true,
|
|
580
|
+
platform,
|
|
581
|
+
...result
|
|
582
|
+
});
|
|
583
|
+
} catch (err) {
|
|
584
|
+
console.error('[Agents API] List all agents error:', err);
|
|
585
|
+
res.status(500).json({
|
|
586
|
+
success: false,
|
|
587
|
+
message: err.message
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* 获取仓库列表
|
|
594
|
+
* GET /api/agents/repos
|
|
595
|
+
*/
|
|
596
|
+
router.get('/repos', (req, res) => {
|
|
597
|
+
try {
|
|
598
|
+
const { platform, service } = getAgentsService(req);
|
|
599
|
+
if (isCodexRepoOperationUnsupported(platform)) {
|
|
600
|
+
return res.status(400).json({
|
|
601
|
+
success: false,
|
|
602
|
+
message: 'Codex 平台暂不支持远程仓库代理'
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
const repos = service.getRepos();
|
|
606
|
+
res.json({
|
|
607
|
+
success: true,
|
|
608
|
+
platform,
|
|
609
|
+
repos
|
|
610
|
+
});
|
|
611
|
+
} catch (err) {
|
|
612
|
+
console.error('[Agents API] Get repos error:', err);
|
|
613
|
+
res.status(500).json({
|
|
614
|
+
success: false,
|
|
615
|
+
message: err.message
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* 添加仓库
|
|
622
|
+
* POST /api/agents/repos
|
|
623
|
+
* Body: { owner, name, branch, directory, enabled }
|
|
624
|
+
*/
|
|
625
|
+
router.post('/repos', (req, res) => {
|
|
626
|
+
try {
|
|
627
|
+
const { platform, service } = getAgentsService(req);
|
|
628
|
+
if (isCodexRepoOperationUnsupported(platform)) {
|
|
629
|
+
return res.status(400).json({
|
|
630
|
+
success: false,
|
|
631
|
+
message: 'Codex 平台暂不支持远程仓库代理'
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
const { owner, name, branch = 'main', directory = '', enabled = true } = req.body;
|
|
635
|
+
|
|
636
|
+
if (!owner || !name) {
|
|
637
|
+
return res.status(400).json({
|
|
638
|
+
success: false,
|
|
639
|
+
message: 'Missing owner or name'
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const repos = service.addRepo({ owner, name, branch, directory, enabled });
|
|
644
|
+
|
|
645
|
+
res.json({
|
|
646
|
+
success: true,
|
|
647
|
+
platform,
|
|
648
|
+
repos
|
|
649
|
+
});
|
|
650
|
+
} catch (err) {
|
|
651
|
+
console.error('[Agents API] Add repo error:', err);
|
|
652
|
+
res.status(500).json({
|
|
653
|
+
success: false,
|
|
654
|
+
message: err.message
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* 删除仓库
|
|
661
|
+
* DELETE /api/agents/repos/:owner/:name
|
|
662
|
+
* Query: directory - 可选,子目录路径
|
|
663
|
+
*/
|
|
664
|
+
router.delete('/repos/:owner/:name', (req, res) => {
|
|
665
|
+
try {
|
|
666
|
+
const { platform, service } = getAgentsService(req);
|
|
667
|
+
if (isCodexRepoOperationUnsupported(platform)) {
|
|
668
|
+
return res.status(400).json({
|
|
669
|
+
success: false,
|
|
670
|
+
message: 'Codex 平台暂不支持远程仓库代理'
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
const { owner, name } = req.params;
|
|
674
|
+
const { directory = '' } = req.query;
|
|
675
|
+
const repos = service.removeRepo(owner, name, directory);
|
|
676
|
+
|
|
677
|
+
res.json({
|
|
678
|
+
success: true,
|
|
679
|
+
platform,
|
|
680
|
+
repos
|
|
681
|
+
});
|
|
682
|
+
} catch (err) {
|
|
683
|
+
console.error('[Agents API] Remove repo error:', err);
|
|
684
|
+
res.status(500).json({
|
|
685
|
+
success: false,
|
|
686
|
+
message: err.message
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* 切换仓库启用状态
|
|
693
|
+
* PUT /api/agents/repos/:owner/:name/toggle
|
|
694
|
+
* Body: { enabled, directory }
|
|
695
|
+
*/
|
|
696
|
+
router.put('/repos/:owner/:name/toggle', (req, res) => {
|
|
697
|
+
try {
|
|
698
|
+
const { platform, service } = getAgentsService(req);
|
|
699
|
+
if (isCodexRepoOperationUnsupported(platform)) {
|
|
700
|
+
return res.status(400).json({
|
|
701
|
+
success: false,
|
|
702
|
+
message: 'Codex 平台暂不支持远程仓库代理'
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
const { owner, name } = req.params;
|
|
706
|
+
const { enabled, directory = '' } = req.body;
|
|
707
|
+
|
|
708
|
+
const repos = service.toggleRepo(owner, name, directory, enabled);
|
|
709
|
+
|
|
710
|
+
res.json({
|
|
711
|
+
success: true,
|
|
712
|
+
platform,
|
|
713
|
+
repos
|
|
714
|
+
});
|
|
715
|
+
} catch (err) {
|
|
716
|
+
console.error('[Agents API] Toggle repo error:', err);
|
|
717
|
+
res.status(500).json({
|
|
718
|
+
success: false,
|
|
719
|
+
message: err.message
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* 从远程仓库安装代理
|
|
726
|
+
* POST /api/agents/install
|
|
727
|
+
* Body: agent object from listAllAgents
|
|
728
|
+
*/
|
|
729
|
+
router.post('/install', async (req, res) => {
|
|
730
|
+
try {
|
|
731
|
+
const { platform, service } = getAgentsService(req);
|
|
732
|
+
if (isCodexRepoOperationUnsupported(platform)) {
|
|
733
|
+
return res.status(400).json({
|
|
734
|
+
success: false,
|
|
735
|
+
message: 'Codex 平台暂不支持远程仓库代理安装'
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
const agent = req.body;
|
|
739
|
+
|
|
740
|
+
if (!agent || !agent.repoOwner || !agent.repoName) {
|
|
741
|
+
return res.status(400).json({
|
|
742
|
+
success: false,
|
|
743
|
+
message: 'Missing agent info or repo info'
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const fileNameError = validateAgentFileName(agent.fileName);
|
|
748
|
+
if (fileNameError) {
|
|
749
|
+
return res.status(400).json({
|
|
750
|
+
success: false,
|
|
751
|
+
message: fileNameError
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const repoPathError = validateRepoPath(agent.repoPath);
|
|
756
|
+
if (repoPathError) {
|
|
757
|
+
return res.status(400).json({
|
|
758
|
+
success: false,
|
|
759
|
+
message: repoPathError
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const result = await service.installFromRemote(agent);
|
|
764
|
+
|
|
765
|
+
res.json({
|
|
766
|
+
success: true,
|
|
767
|
+
platform,
|
|
768
|
+
...result
|
|
769
|
+
});
|
|
770
|
+
} catch (err) {
|
|
771
|
+
console.error('[Agents API] Install agent error:', err);
|
|
772
|
+
res.status(500).json({
|
|
773
|
+
success: false,
|
|
774
|
+
message: err.message
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* 卸载代理
|
|
781
|
+
* POST /api/agents/uninstall
|
|
782
|
+
* Body: { fileName } - 代理的文件名(不含扩展名)
|
|
783
|
+
*/
|
|
784
|
+
router.post('/uninstall', (req, res) => {
|
|
785
|
+
try {
|
|
786
|
+
const { platform, service } = getAgentsService(req);
|
|
787
|
+
if (isCodexRepoOperationUnsupported(platform)) {
|
|
788
|
+
return res.status(400).json({
|
|
789
|
+
success: false,
|
|
790
|
+
message: 'Codex 平台暂不支持远程仓库代理卸载'
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
const { fileName } = req.body;
|
|
794
|
+
|
|
795
|
+
if (!fileName) {
|
|
796
|
+
return res.status(400).json({
|
|
797
|
+
success: false,
|
|
798
|
+
message: 'Missing fileName'
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const fileNameError = validateAgentFileName(fileName);
|
|
803
|
+
if (fileNameError) {
|
|
804
|
+
return res.status(400).json({
|
|
805
|
+
success: false,
|
|
806
|
+
message: fileNameError
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const result = service.uninstallAgent(fileName);
|
|
811
|
+
|
|
812
|
+
res.json({
|
|
813
|
+
success: true,
|
|
814
|
+
platform,
|
|
815
|
+
...result
|
|
816
|
+
});
|
|
817
|
+
} catch (err) {
|
|
818
|
+
console.error('[Agents API] Uninstall agent error:', err);
|
|
819
|
+
res.status(500).json({
|
|
820
|
+
success: false,
|
|
821
|
+
message: err.message
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
module.exports = router;
|