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.
Files changed (185) hide show
  1. package/CHANGELOG.md +599 -0
  2. package/LICENSE +21 -0
  3. package/README.md +439 -0
  4. package/bin/ctx.js +8 -0
  5. package/dist/web/assets/Analytics-DN_YsnkW.js +39 -0
  6. package/dist/web/assets/Analytics-DuYvId7u.css +1 -0
  7. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  8. package/dist/web/assets/ConfigTemplates-DpXIMy0p.js +1 -0
  9. package/dist/web/assets/Home-38JTUlYt.js +1 -0
  10. package/dist/web/assets/Home-CjupSEWE.css +1 -0
  11. package/dist/web/assets/PluginManager-CX2tgq2H.js +1 -0
  12. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  13. package/dist/web/assets/ProjectList-C1lDcsn6.js +1 -0
  14. package/dist/web/assets/ProjectList-oJIyIRkP.css +1 -0
  15. package/dist/web/assets/SessionList-C55tjV7i.css +1 -0
  16. package/dist/web/assets/SessionList-CZ7T6rVx.js +1 -0
  17. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  18. package/dist/web/assets/SkillManager-DLN9f79y.js +1 -0
  19. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  20. package/dist/web/assets/WorkspaceManager-DxlHZkpZ.js +1 -0
  21. package/dist/web/assets/icons-DRrXwWZi.js +1 -0
  22. package/dist/web/assets/index-CetESrXw.css +1 -0
  23. package/dist/web/assets/index-Cfvn-2Gb.js +2 -0
  24. package/dist/web/assets/markdown-BfC0goYb.css +10 -0
  25. package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
  26. package/dist/web/assets/naive-ui-DlpKk-8M.js +1 -0
  27. package/dist/web/assets/vendors-DMjSfzlv.js +7 -0
  28. package/dist/web/assets/vue-vendor-DET08QYg.js +45 -0
  29. package/dist/web/favicon.ico +0 -0
  30. package/dist/web/index.html +20 -0
  31. package/dist/web/logo.png +0 -0
  32. package/docs/bannel.png +0 -0
  33. package/docs/home.png +0 -0
  34. package/docs/logo.png +0 -0
  35. package/docs/model-redirection.md +251 -0
  36. package/docs/multi-channel-load-balancing.md +249 -0
  37. package/package.json +80 -0
  38. package/src/commands/channels.js +551 -0
  39. package/src/commands/cli-type.js +101 -0
  40. package/src/commands/daemon.js +365 -0
  41. package/src/commands/doctor.js +333 -0
  42. package/src/commands/export-config.js +205 -0
  43. package/src/commands/list.js +222 -0
  44. package/src/commands/logs.js +261 -0
  45. package/src/commands/plugin.js +585 -0
  46. package/src/commands/port-config.js +135 -0
  47. package/src/commands/proxy-control.js +264 -0
  48. package/src/commands/proxy.js +152 -0
  49. package/src/commands/resume.js +137 -0
  50. package/src/commands/search.js +190 -0
  51. package/src/commands/security.js +37 -0
  52. package/src/commands/stats.js +398 -0
  53. package/src/commands/switch.js +48 -0
  54. package/src/commands/toggle-proxy.js +247 -0
  55. package/src/commands/ui.js +99 -0
  56. package/src/commands/update.js +97 -0
  57. package/src/commands/workspace.js +454 -0
  58. package/src/config/default.js +69 -0
  59. package/src/config/loader.js +149 -0
  60. package/src/config/model-metadata.js +167 -0
  61. package/src/config/model-metadata.json +125 -0
  62. package/src/config/model-pricing.js +35 -0
  63. package/src/config/paths.js +190 -0
  64. package/src/index.js +680 -0
  65. package/src/plugins/constants.js +15 -0
  66. package/src/plugins/event-bus.js +54 -0
  67. package/src/plugins/manifest-validator.js +129 -0
  68. package/src/plugins/plugin-api.js +128 -0
  69. package/src/plugins/plugin-installer.js +601 -0
  70. package/src/plugins/plugin-loader.js +229 -0
  71. package/src/plugins/plugin-manager.js +170 -0
  72. package/src/plugins/registry.js +152 -0
  73. package/src/plugins/schema/plugin-manifest.json +115 -0
  74. package/src/reset-config.js +94 -0
  75. package/src/server/api/agents.js +826 -0
  76. package/src/server/api/aliases.js +36 -0
  77. package/src/server/api/channels.js +368 -0
  78. package/src/server/api/claude-hooks.js +480 -0
  79. package/src/server/api/codex-channels.js +417 -0
  80. package/src/server/api/codex-projects.js +104 -0
  81. package/src/server/api/codex-proxy.js +195 -0
  82. package/src/server/api/codex-sessions.js +483 -0
  83. package/src/server/api/codex-statistics.js +57 -0
  84. package/src/server/api/commands.js +482 -0
  85. package/src/server/api/config-export.js +212 -0
  86. package/src/server/api/config-registry.js +357 -0
  87. package/src/server/api/config-sync.js +155 -0
  88. package/src/server/api/config-templates.js +248 -0
  89. package/src/server/api/config.js +521 -0
  90. package/src/server/api/convert.js +260 -0
  91. package/src/server/api/dashboard.js +142 -0
  92. package/src/server/api/env.js +144 -0
  93. package/src/server/api/favorites.js +77 -0
  94. package/src/server/api/gemini-channels.js +366 -0
  95. package/src/server/api/gemini-projects.js +91 -0
  96. package/src/server/api/gemini-proxy.js +173 -0
  97. package/src/server/api/gemini-sessions.js +376 -0
  98. package/src/server/api/gemini-statistics.js +57 -0
  99. package/src/server/api/health-check.js +31 -0
  100. package/src/server/api/mcp.js +399 -0
  101. package/src/server/api/opencode-channels.js +419 -0
  102. package/src/server/api/opencode-projects.js +99 -0
  103. package/src/server/api/opencode-proxy.js +207 -0
  104. package/src/server/api/opencode-sessions.js +327 -0
  105. package/src/server/api/opencode-statistics.js +57 -0
  106. package/src/server/api/plugins.js +463 -0
  107. package/src/server/api/pm2-autostart.js +269 -0
  108. package/src/server/api/projects.js +124 -0
  109. package/src/server/api/prompts.js +279 -0
  110. package/src/server/api/proxy.js +306 -0
  111. package/src/server/api/security.js +53 -0
  112. package/src/server/api/sessions.js +514 -0
  113. package/src/server/api/settings.js +142 -0
  114. package/src/server/api/skills.js +570 -0
  115. package/src/server/api/statistics.js +238 -0
  116. package/src/server/api/ui-config.js +64 -0
  117. package/src/server/api/workspaces.js +456 -0
  118. package/src/server/codex-proxy-server.js +681 -0
  119. package/src/server/dev-server.js +26 -0
  120. package/src/server/gemini-proxy-server.js +610 -0
  121. package/src/server/index.js +422 -0
  122. package/src/server/opencode-proxy-server.js +4771 -0
  123. package/src/server/proxy-server.js +669 -0
  124. package/src/server/services/agents-service.js +1137 -0
  125. package/src/server/services/alias.js +71 -0
  126. package/src/server/services/channel-health.js +234 -0
  127. package/src/server/services/channel-scheduler.js +240 -0
  128. package/src/server/services/channels.js +447 -0
  129. package/src/server/services/codex-channels.js +705 -0
  130. package/src/server/services/codex-config.js +90 -0
  131. package/src/server/services/codex-parser.js +322 -0
  132. package/src/server/services/codex-sessions.js +936 -0
  133. package/src/server/services/codex-settings-manager.js +619 -0
  134. package/src/server/services/codex-speed-test-template.json +24 -0
  135. package/src/server/services/codex-statistics-service.js +161 -0
  136. package/src/server/services/commands-service.js +574 -0
  137. package/src/server/services/config-export-service.js +1165 -0
  138. package/src/server/services/config-registry-service.js +828 -0
  139. package/src/server/services/config-sync-manager.js +941 -0
  140. package/src/server/services/config-sync-service.js +504 -0
  141. package/src/server/services/config-templates-service.js +913 -0
  142. package/src/server/services/enhanced-cache.js +196 -0
  143. package/src/server/services/env-checker.js +409 -0
  144. package/src/server/services/env-manager.js +436 -0
  145. package/src/server/services/favorites.js +165 -0
  146. package/src/server/services/format-converter.js +620 -0
  147. package/src/server/services/gemini-channels.js +459 -0
  148. package/src/server/services/gemini-config.js +73 -0
  149. package/src/server/services/gemini-sessions.js +689 -0
  150. package/src/server/services/gemini-settings-manager.js +263 -0
  151. package/src/server/services/gemini-statistics-service.js +157 -0
  152. package/src/server/services/health-check.js +85 -0
  153. package/src/server/services/mcp-client.js +790 -0
  154. package/src/server/services/mcp-service.js +1732 -0
  155. package/src/server/services/model-detector.js +1245 -0
  156. package/src/server/services/network-access.js +80 -0
  157. package/src/server/services/opencode-channels.js +366 -0
  158. package/src/server/services/opencode-gateway-adapters.js +1168 -0
  159. package/src/server/services/opencode-gateway-converter.js +639 -0
  160. package/src/server/services/opencode-sessions.js +931 -0
  161. package/src/server/services/opencode-settings-manager.js +478 -0
  162. package/src/server/services/opencode-statistics-service.js +161 -0
  163. package/src/server/services/plugins-service.js +1268 -0
  164. package/src/server/services/prompts-service.js +534 -0
  165. package/src/server/services/proxy-runtime.js +79 -0
  166. package/src/server/services/repo-scanner-base.js +708 -0
  167. package/src/server/services/request-logger.js +130 -0
  168. package/src/server/services/response-decoder.js +21 -0
  169. package/src/server/services/security-config.js +131 -0
  170. package/src/server/services/session-cache.js +127 -0
  171. package/src/server/services/session-converter.js +577 -0
  172. package/src/server/services/sessions.js +900 -0
  173. package/src/server/services/settings-manager.js +163 -0
  174. package/src/server/services/skill-service.js +1482 -0
  175. package/src/server/services/speed-test.js +1146 -0
  176. package/src/server/services/statistics-service.js +1043 -0
  177. package/src/server/services/ui-config.js +132 -0
  178. package/src/server/services/workspace-service.js +830 -0
  179. package/src/server/utils/pricing.js +73 -0
  180. package/src/server/websocket-server.js +513 -0
  181. package/src/ui/menu.js +139 -0
  182. package/src/ui/prompts.js +100 -0
  183. package/src/utils/format.js +43 -0
  184. package/src/utils/port-helper.js +108 -0
  185. 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;