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,482 @@
1
+ /**
2
+ * Commands API 路由
3
+ *
4
+ * 管理 Claude Code 自定义命令
5
+ */
6
+
7
+ const express = require('express');
8
+ const { CommandsService } = require('../services/commands-service');
9
+
10
+ const router = express.Router();
11
+ const SUPPORTED_PLATFORMS = ['claude', 'opencode'];
12
+ const commandServices = new Map();
13
+
14
+ function resolvePlatform(rawPlatform) {
15
+ return SUPPORTED_PLATFORMS.includes(rawPlatform) ? rawPlatform : 'claude';
16
+ }
17
+
18
+ function getPlatform(req) {
19
+ return resolvePlatform(req.query?.platform || req.body?.platform);
20
+ }
21
+
22
+ function getCommandsService(req) {
23
+ const platform = getPlatform(req);
24
+ if (!commandServices.has(platform)) {
25
+ commandServices.set(platform, new CommandsService(platform));
26
+ }
27
+ return { platform, service: commandServices.get(platform) };
28
+ }
29
+
30
+ /**
31
+ * 获取命令列表
32
+ * GET /api/commands
33
+ * Query: projectPath - 项目路径(可选,用于获取项目级命令)
34
+ */
35
+ router.get('/', (req, res) => {
36
+ try {
37
+ const { platform, service } = getCommandsService(req);
38
+ const { projectPath } = req.query;
39
+ const result = service.listCommands(projectPath || null);
40
+
41
+ res.json({
42
+ success: true,
43
+ platform,
44
+ ...result
45
+ });
46
+ } catch (err) {
47
+ console.error('[Commands API] List commands error:', err);
48
+ res.status(500).json({
49
+ success: false,
50
+ message: err.message
51
+ });
52
+ }
53
+ });
54
+
55
+ /**
56
+ * 获取命令统计
57
+ * GET /api/commands/stats
58
+ */
59
+ router.get('/stats', (req, res) => {
60
+ try {
61
+ const { platform, service } = getCommandsService(req);
62
+ const { projectPath } = req.query;
63
+ const stats = service.getStats(projectPath || null);
64
+
65
+ res.json({
66
+ success: true,
67
+ platform,
68
+ ...stats
69
+ });
70
+ } catch (err) {
71
+ console.error('[Commands API] Get stats error:', err);
72
+ res.status(500).json({
73
+ success: false,
74
+ message: err.message
75
+ });
76
+ }
77
+ });
78
+
79
+ /**
80
+ * 获取单个命令详情
81
+ * GET /api/commands/:scope/:name
82
+ * GET /api/commands/:scope/ns/:namespace/:name
83
+ */
84
+ router.get('/:scope/:name', (req, res) => {
85
+ try {
86
+ const { platform, service } = getCommandsService(req);
87
+ const { scope, name } = req.params;
88
+ const { projectPath, namespace } = req.query;
89
+
90
+ if (!['user', 'project'].includes(scope)) {
91
+ return res.status(400).json({
92
+ success: false,
93
+ message: '无效的 scope,必须是 user 或 project'
94
+ });
95
+ }
96
+
97
+ if (scope === 'project' && !projectPath) {
98
+ return res.status(400).json({
99
+ success: false,
100
+ message: '获取项目级命令需要提供 projectPath'
101
+ });
102
+ }
103
+
104
+ const command = service.getCommand(name, scope, projectPath || null, namespace || null);
105
+
106
+ if (!command) {
107
+ return res.status(404).json({
108
+ success: false,
109
+ message: `命令 "${name}" 不存在`
110
+ });
111
+ }
112
+
113
+ res.json({
114
+ success: true,
115
+ platform,
116
+ command
117
+ });
118
+ } catch (err) {
119
+ console.error('[Commands API] Get command error:', err);
120
+ res.status(500).json({
121
+ success: false,
122
+ message: err.message
123
+ });
124
+ }
125
+ });
126
+
127
+ /**
128
+ * 创建命令
129
+ * POST /api/commands
130
+ * Body: { name, scope, projectPath?, namespace?, description?, allowedTools?, argumentHint?, body }
131
+ */
132
+ router.post('/', (req, res) => {
133
+ try {
134
+ const { platform, service } = getCommandsService(req);
135
+ const { name, scope, projectPath, namespace, description, allowedTools, argumentHint, agent, model, subtask, body } = req.body;
136
+
137
+ if (!name) {
138
+ return res.status(400).json({
139
+ success: false,
140
+ message: '命令名称不能为空'
141
+ });
142
+ }
143
+
144
+ if (!['user', 'project'].includes(scope)) {
145
+ return res.status(400).json({
146
+ success: false,
147
+ message: '无效的 scope,必须是 user 或 project'
148
+ });
149
+ }
150
+
151
+ if (scope === 'project' && !projectPath) {
152
+ return res.status(400).json({
153
+ success: false,
154
+ message: '创建项目级命令需要提供 projectPath'
155
+ });
156
+ }
157
+
158
+ const command = service.createCommand({
159
+ name,
160
+ scope,
161
+ projectPath: projectPath || null,
162
+ namespace: namespace || null,
163
+ description: description || '',
164
+ allowedTools: allowedTools || '',
165
+ argumentHint: argumentHint || '',
166
+ agent: agent || '',
167
+ model: model || '',
168
+ subtask: typeof subtask === 'boolean' ? subtask : undefined,
169
+ body: body || ''
170
+ });
171
+
172
+ res.json({
173
+ success: true,
174
+ platform,
175
+ command,
176
+ message: '命令创建成功'
177
+ });
178
+ } catch (err) {
179
+ console.error('[Commands API] Create command error:', err);
180
+ res.status(500).json({
181
+ success: false,
182
+ message: err.message
183
+ });
184
+ }
185
+ });
186
+
187
+ /**
188
+ * 更新命令
189
+ * PUT /api/commands/:scope/:name
190
+ */
191
+ router.put('/:scope/:name', (req, res) => {
192
+ try {
193
+ const { platform, service } = getCommandsService(req);
194
+ const { scope, name } = req.params;
195
+ const { projectPath, namespace, description, allowedTools, argumentHint, agent, model, subtask, body } = req.body;
196
+
197
+ if (!['user', 'project'].includes(scope)) {
198
+ return res.status(400).json({
199
+ success: false,
200
+ message: '无效的 scope,必须是 user 或 project'
201
+ });
202
+ }
203
+
204
+ if (scope === 'project' && !projectPath) {
205
+ return res.status(400).json({
206
+ success: false,
207
+ message: '更新项目级命令需要提供 projectPath'
208
+ });
209
+ }
210
+
211
+ const command = service.updateCommand({
212
+ name,
213
+ scope,
214
+ projectPath: projectPath || null,
215
+ namespace: namespace || null,
216
+ description: description || '',
217
+ allowedTools: allowedTools || '',
218
+ argumentHint: argumentHint || '',
219
+ agent: agent || '',
220
+ model: model || '',
221
+ subtask: typeof subtask === 'boolean' ? subtask : undefined,
222
+ body: body || ''
223
+ });
224
+
225
+ res.json({
226
+ success: true,
227
+ platform,
228
+ command,
229
+ message: '命令更新成功'
230
+ });
231
+ } catch (err) {
232
+ console.error('[Commands API] Update command error:', err);
233
+ res.status(500).json({
234
+ success: false,
235
+ message: err.message
236
+ });
237
+ }
238
+ });
239
+
240
+ /**
241
+ * 删除命令
242
+ * DELETE /api/commands/:scope/:name
243
+ */
244
+ router.delete('/:scope/:name', (req, res) => {
245
+ try {
246
+ const { platform, service } = getCommandsService(req);
247
+ const { scope, name } = req.params;
248
+ const { projectPath, namespace } = req.query;
249
+
250
+ if (!['user', 'project'].includes(scope)) {
251
+ return res.status(400).json({
252
+ success: false,
253
+ message: '无效的 scope,必须是 user 或 project'
254
+ });
255
+ }
256
+
257
+ if (scope === 'project' && !projectPath) {
258
+ return res.status(400).json({
259
+ success: false,
260
+ message: '删除项目级命令需要提供 projectPath'
261
+ });
262
+ }
263
+
264
+ const result = service.deleteCommand(name, scope, projectPath || null, namespace || null);
265
+
266
+ res.json({
267
+ platform,
268
+ success: result.success,
269
+ message: result.message
270
+ });
271
+ } catch (err) {
272
+ console.error('[Commands API] Delete command error:', err);
273
+ res.status(500).json({
274
+ success: false,
275
+ message: err.message
276
+ });
277
+ }
278
+ });
279
+
280
+ // ==================== 仓库管理 API ====================
281
+
282
+ /**
283
+ * 获取所有命令(包括远程仓库)
284
+ * GET /api/commands/all
285
+ * Query: projectPath, refresh=1 强制刷新缓存
286
+ */
287
+ router.get('/all', async (req, res) => {
288
+ try {
289
+ const { platform, service } = getCommandsService(req);
290
+ const { projectPath, refresh } = req.query;
291
+ const forceRefresh = refresh === '1';
292
+ const result = await service.listAllCommands(projectPath || null, forceRefresh);
293
+
294
+ res.json({
295
+ success: true,
296
+ platform,
297
+ ...result
298
+ });
299
+ } catch (err) {
300
+ console.error('[Commands API] List all commands error:', err);
301
+ res.status(500).json({
302
+ success: false,
303
+ message: err.message
304
+ });
305
+ }
306
+ });
307
+
308
+ /**
309
+ * 获取仓库列表
310
+ * GET /api/commands/repos
311
+ */
312
+ router.get('/repos', (req, res) => {
313
+ try {
314
+ const { platform, service } = getCommandsService(req);
315
+ const repos = service.getRepos();
316
+ res.json({
317
+ success: true,
318
+ platform,
319
+ repos
320
+ });
321
+ } catch (err) {
322
+ console.error('[Commands API] Get repos error:', err);
323
+ res.status(500).json({
324
+ success: false,
325
+ message: err.message
326
+ });
327
+ }
328
+ });
329
+
330
+ /**
331
+ * 添加仓库
332
+ * POST /api/commands/repos
333
+ * Body: { owner, name, branch, directory, enabled }
334
+ */
335
+ router.post('/repos', (req, res) => {
336
+ try {
337
+ const { platform, service } = getCommandsService(req);
338
+ const { owner, name, branch = 'main', directory = '', enabled = true } = req.body;
339
+
340
+ if (!owner || !name) {
341
+ return res.status(400).json({
342
+ success: false,
343
+ message: 'Missing owner or name'
344
+ });
345
+ }
346
+
347
+ const repos = service.addRepo({ owner, name, branch, directory, enabled });
348
+
349
+ res.json({
350
+ success: true,
351
+ platform,
352
+ repos
353
+ });
354
+ } catch (err) {
355
+ console.error('[Commands API] Add repo error:', err);
356
+ res.status(500).json({
357
+ success: false,
358
+ message: err.message
359
+ });
360
+ }
361
+ });
362
+
363
+ /**
364
+ * 删除仓库
365
+ * DELETE /api/commands/repos/:owner/:name
366
+ * Query: directory - 可选,子目录路径
367
+ */
368
+ router.delete('/repos/:owner/:name', (req, res) => {
369
+ try {
370
+ const { platform, service } = getCommandsService(req);
371
+ const { owner, name } = req.params;
372
+ const { directory = '' } = req.query;
373
+ const repos = service.removeRepo(owner, name, directory);
374
+
375
+ res.json({
376
+ success: true,
377
+ platform,
378
+ repos
379
+ });
380
+ } catch (err) {
381
+ console.error('[Commands API] Remove repo error:', err);
382
+ res.status(500).json({
383
+ success: false,
384
+ message: err.message
385
+ });
386
+ }
387
+ });
388
+
389
+ /**
390
+ * 切换仓库启用状态
391
+ * PUT /api/commands/repos/:owner/:name/toggle
392
+ * Body: { enabled, directory }
393
+ */
394
+ router.put('/repos/:owner/:name/toggle', (req, res) => {
395
+ try {
396
+ const { platform, service } = getCommandsService(req);
397
+ const { owner, name } = req.params;
398
+ const { enabled, directory = '' } = req.body;
399
+
400
+ const repos = service.toggleRepo(owner, name, directory, enabled);
401
+
402
+ res.json({
403
+ success: true,
404
+ platform,
405
+ repos
406
+ });
407
+ } catch (err) {
408
+ console.error('[Commands API] Toggle repo error:', err);
409
+ res.status(500).json({
410
+ success: false,
411
+ message: err.message
412
+ });
413
+ }
414
+ });
415
+
416
+ /**
417
+ * 从远程仓库安装命令
418
+ * POST /api/commands/install
419
+ * Body: command object from listAllCommands
420
+ */
421
+ router.post('/install', async (req, res) => {
422
+ try {
423
+ const { platform, service } = getCommandsService(req);
424
+ const command = req.body;
425
+
426
+ if (!command || !command.repoOwner || !command.repoName) {
427
+ return res.status(400).json({
428
+ success: false,
429
+ message: 'Missing command info or repo info'
430
+ });
431
+ }
432
+
433
+ const result = await service.installFromRemote(command);
434
+
435
+ res.json({
436
+ success: true,
437
+ platform,
438
+ ...result
439
+ });
440
+ } catch (err) {
441
+ console.error('[Commands API] Install command error:', err);
442
+ res.status(500).json({
443
+ success: false,
444
+ message: err.message
445
+ });
446
+ }
447
+ });
448
+
449
+ /**
450
+ * 卸载命令
451
+ * POST /api/commands/uninstall
452
+ * Body: { path } - 命令的相对路径
453
+ */
454
+ router.post('/uninstall', (req, res) => {
455
+ try {
456
+ const { platform, service } = getCommandsService(req);
457
+ const { path } = req.body;
458
+
459
+ if (!path) {
460
+ return res.status(400).json({
461
+ success: false,
462
+ message: 'Missing path'
463
+ });
464
+ }
465
+
466
+ const result = service.uninstallCommand(path);
467
+
468
+ res.json({
469
+ success: true,
470
+ platform,
471
+ ...result
472
+ });
473
+ } catch (err) {
474
+ console.error('[Commands API] Uninstall command error:', err);
475
+ res.status(500).json({
476
+ success: false,
477
+ message: err.message
478
+ });
479
+ }
480
+ });
481
+
482
+ module.exports = router;
@@ -0,0 +1,212 @@
1
+ /**
2
+ * 配置导出/导入 API 路由
3
+ */
4
+
5
+ const express = require('express');
6
+ const configExportService = require('../services/config-export-service');
7
+ const AdmZip = require('adm-zip');
8
+
9
+ const router = express.Router();
10
+
11
+ function parseConfigZip(buffer) {
12
+ const zip = new AdmZip(buffer);
13
+ const entry = zip.getEntry('config.json');
14
+ if (!entry) {
15
+ throw new Error('配置包缺少 config.json');
16
+ }
17
+ const content = entry.getData().toString('utf8');
18
+ return JSON.parse(content);
19
+ }
20
+
21
+ function buildPreviewSummary(data) {
22
+ return {
23
+ version: data.version,
24
+ exportedAt: data.exportedAt,
25
+ counts: {
26
+ configTemplates: (data.data.configTemplates || []).length,
27
+ channels: (data.data.channels || []).length,
28
+ plugins: (data.data.plugins || []).length
29
+ },
30
+ items: {
31
+ configTemplates: (data.data.configTemplates || []).map(t => ({
32
+ id: t.id,
33
+ name: t.name,
34
+ description: t.description
35
+ })),
36
+ channels: (data.data.channels || []).map(c => ({
37
+ id: c.id,
38
+ name: c.name,
39
+ type: c.type
40
+ })),
41
+ plugins: (data.data.plugins || []).map(p => ({
42
+ name: p.name,
43
+ type: p.type,
44
+ version: p.version
45
+ }))
46
+ }
47
+ };
48
+ }
49
+
50
+ /**
51
+ * 导出所有配置
52
+ * GET /api/config-export
53
+ */
54
+ router.get('/', (req, res) => {
55
+ try {
56
+ const format = (req.query.format || 'json').toLowerCase();
57
+ const result = format === 'zip'
58
+ ? configExportService.exportAllConfigsZip()
59
+ : configExportService.exportAllConfigs();
60
+
61
+ if (result.success) {
62
+ if (format === 'zip') {
63
+ // 设置响应头,触发文件下载
64
+ const filename = result.filename || `ctx-config-${new Date().toISOString().split('T')[0]}.zip`;
65
+ res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
66
+ res.setHeader('Content-Type', 'application/zip');
67
+ res.send(result.data);
68
+ } else {
69
+ // 设置响应头,触发文件下载
70
+ const filename = `ctx-config-${new Date().toISOString().split('T')[0]}.json`;
71
+ res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
72
+ res.setHeader('Content-Type', 'application/json');
73
+ res.json(result.data);
74
+ }
75
+ } else {
76
+ res.status(500).json({
77
+ success: false,
78
+ message: result.message
79
+ });
80
+ }
81
+ } catch (err) {
82
+ console.error('[ConfigExport API] 导出失败:', err);
83
+ res.status(500).json({
84
+ success: false,
85
+ message: err.message
86
+ });
87
+ }
88
+ });
89
+
90
+ /**
91
+ * 导入配置
92
+ * POST /api/config-export/import
93
+ * Body: { data: {...}, overwrite: boolean }
94
+ */
95
+ router.post('/import', (req, res) => {
96
+ try {
97
+ const { data, overwrite = false } = req.body;
98
+
99
+ if (!data) {
100
+ return res.status(400).json({
101
+ success: false,
102
+ message: '缺少导入数据'
103
+ });
104
+ }
105
+
106
+ const result = configExportService.importConfigs(data, { overwrite });
107
+
108
+ res.json(result);
109
+ } catch (err) {
110
+ console.error('[ConfigExport API] 导入失败:', err);
111
+ res.status(500).json({
112
+ success: false,
113
+ message: err.message
114
+ });
115
+ }
116
+ });
117
+
118
+ /**
119
+ * 导入 ZIP 配置
120
+ * POST /api/config-export/import-zip
121
+ */
122
+ router.post('/import-zip', express.raw({ type: ['application/zip', 'application/octet-stream'], limit: '100mb' }), (req, res) => {
123
+ try {
124
+ const overwrite = req.query.overwrite === 'true';
125
+ const buffer = req.body;
126
+
127
+ if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
128
+ return res.status(400).json({
129
+ success: false,
130
+ message: '缺少 ZIP 文件内容'
131
+ });
132
+ }
133
+
134
+ const data = parseConfigZip(buffer);
135
+ const result = configExportService.importConfigs(data, { overwrite });
136
+ res.json(result);
137
+ } catch (err) {
138
+ console.error('[ConfigExport API] 导入 ZIP 失败:', err);
139
+ res.status(500).json({
140
+ success: false,
141
+ message: err.message
142
+ });
143
+ }
144
+ });
145
+
146
+ /**
147
+ * 预览导入配置(不实际导入)
148
+ * POST /api/config-export/preview
149
+ */
150
+ router.post('/preview', (req, res) => {
151
+ try {
152
+ const { data } = req.body;
153
+
154
+ if (!data || !data.data) {
155
+ return res.status(400).json({
156
+ success: false,
157
+ message: '无效的导入数据格式'
158
+ });
159
+ }
160
+
161
+ const summary = buildPreviewSummary(data);
162
+
163
+ res.json({
164
+ success: true,
165
+ data: summary
166
+ });
167
+ } catch (err) {
168
+ console.error('[ConfigExport API] 预览失败:', err);
169
+ res.status(500).json({
170
+ success: false,
171
+ message: err.message
172
+ });
173
+ }
174
+ });
175
+
176
+ /**
177
+ * 预览 ZIP 导入配置(不实际导入)
178
+ * POST /api/config-export/preview-zip
179
+ */
180
+ router.post('/preview-zip', express.raw({ type: ['application/zip', 'application/octet-stream'], limit: '100mb' }), (req, res) => {
181
+ try {
182
+ const buffer = req.body;
183
+ if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
184
+ return res.status(400).json({
185
+ success: false,
186
+ message: '缺少 ZIP 文件内容'
187
+ });
188
+ }
189
+
190
+ const data = parseConfigZip(buffer);
191
+ if (!data || !data.data) {
192
+ return res.status(400).json({
193
+ success: false,
194
+ message: '无效的导入数据格式'
195
+ });
196
+ }
197
+
198
+ const summary = buildPreviewSummary(data);
199
+ res.json({
200
+ success: true,
201
+ data: summary
202
+ });
203
+ } catch (err) {
204
+ console.error('[ConfigExport API] 预览 ZIP 失败:', err);
205
+ res.status(500).json({
206
+ success: false,
207
+ message: err.message
208
+ });
209
+ }
210
+ });
211
+
212
+ module.exports = router;