@adversity/coding-tool-x 2.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 (125) hide show
  1. package/CHANGELOG.md +333 -0
  2. package/LICENSE +21 -0
  3. package/README.md +404 -0
  4. package/bin/ctx.js +8 -0
  5. package/dist/web/assets/index-D1AYlFLZ.js +3220 -0
  6. package/dist/web/assets/index-aL3cKxSK.css +41 -0
  7. package/dist/web/favicon.ico +0 -0
  8. package/dist/web/index.html +14 -0
  9. package/dist/web/logo.png +0 -0
  10. package/docs/CHANGELOG.md +582 -0
  11. package/docs/DIRECTORY_MIGRATION.md +112 -0
  12. package/docs/PROJECT_STRUCTURE.md +396 -0
  13. package/docs/bannel.png +0 -0
  14. package/docs/home.png +0 -0
  15. package/docs/logo.png +0 -0
  16. package/docs/multi-channel-load-balancing.md +249 -0
  17. package/package.json +73 -0
  18. package/src/commands/channels.js +504 -0
  19. package/src/commands/cli-type.js +99 -0
  20. package/src/commands/daemon.js +286 -0
  21. package/src/commands/doctor.js +332 -0
  22. package/src/commands/list.js +222 -0
  23. package/src/commands/logs.js +259 -0
  24. package/src/commands/port-config.js +115 -0
  25. package/src/commands/proxy-control.js +258 -0
  26. package/src/commands/proxy.js +152 -0
  27. package/src/commands/resume.js +137 -0
  28. package/src/commands/search.js +190 -0
  29. package/src/commands/stats.js +224 -0
  30. package/src/commands/switch.js +48 -0
  31. package/src/commands/toggle-proxy.js +222 -0
  32. package/src/commands/ui.js +92 -0
  33. package/src/commands/workspace.js +454 -0
  34. package/src/config/default.js +40 -0
  35. package/src/config/loader.js +75 -0
  36. package/src/config/paths.js +121 -0
  37. package/src/index.js +373 -0
  38. package/src/reset-config.js +92 -0
  39. package/src/server/api/agents.js +248 -0
  40. package/src/server/api/aliases.js +36 -0
  41. package/src/server/api/channels.js +258 -0
  42. package/src/server/api/claude-hooks.js +480 -0
  43. package/src/server/api/codex-channels.js +312 -0
  44. package/src/server/api/codex-projects.js +91 -0
  45. package/src/server/api/codex-proxy.js +182 -0
  46. package/src/server/api/codex-sessions.js +491 -0
  47. package/src/server/api/codex-statistics.js +57 -0
  48. package/src/server/api/commands.js +245 -0
  49. package/src/server/api/config-templates.js +182 -0
  50. package/src/server/api/config.js +147 -0
  51. package/src/server/api/convert.js +127 -0
  52. package/src/server/api/dashboard.js +125 -0
  53. package/src/server/api/env.js +144 -0
  54. package/src/server/api/favorites.js +77 -0
  55. package/src/server/api/gemini-channels.js +261 -0
  56. package/src/server/api/gemini-projects.js +91 -0
  57. package/src/server/api/gemini-proxy.js +160 -0
  58. package/src/server/api/gemini-sessions.js +397 -0
  59. package/src/server/api/gemini-statistics.js +57 -0
  60. package/src/server/api/health-check.js +118 -0
  61. package/src/server/api/mcp.js +336 -0
  62. package/src/server/api/pm2-autostart.js +269 -0
  63. package/src/server/api/projects.js +124 -0
  64. package/src/server/api/prompts.js +279 -0
  65. package/src/server/api/proxy.js +235 -0
  66. package/src/server/api/rules.js +271 -0
  67. package/src/server/api/sessions.js +595 -0
  68. package/src/server/api/settings.js +61 -0
  69. package/src/server/api/skills.js +305 -0
  70. package/src/server/api/statistics.js +91 -0
  71. package/src/server/api/terminal.js +202 -0
  72. package/src/server/api/ui-config.js +64 -0
  73. package/src/server/api/workspaces.js +407 -0
  74. package/src/server/codex-proxy-server.js +538 -0
  75. package/src/server/dev-server.js +26 -0
  76. package/src/server/gemini-proxy-server.js +518 -0
  77. package/src/server/index.js +305 -0
  78. package/src/server/proxy-server.js +469 -0
  79. package/src/server/services/agents-service.js +354 -0
  80. package/src/server/services/alias.js +71 -0
  81. package/src/server/services/channel-health.js +234 -0
  82. package/src/server/services/channel-scheduler.js +234 -0
  83. package/src/server/services/channels.js +347 -0
  84. package/src/server/services/codex-channels.js +625 -0
  85. package/src/server/services/codex-config.js +90 -0
  86. package/src/server/services/codex-parser.js +322 -0
  87. package/src/server/services/codex-sessions.js +665 -0
  88. package/src/server/services/codex-settings-manager.js +397 -0
  89. package/src/server/services/codex-speed-test-template.json +24 -0
  90. package/src/server/services/codex-statistics-service.js +255 -0
  91. package/src/server/services/commands-service.js +360 -0
  92. package/src/server/services/config-templates-service.js +732 -0
  93. package/src/server/services/env-checker.js +307 -0
  94. package/src/server/services/env-manager.js +300 -0
  95. package/src/server/services/favorites.js +163 -0
  96. package/src/server/services/gemini-channels.js +333 -0
  97. package/src/server/services/gemini-config.js +73 -0
  98. package/src/server/services/gemini-sessions.js +689 -0
  99. package/src/server/services/gemini-settings-manager.js +263 -0
  100. package/src/server/services/gemini-statistics-service.js +253 -0
  101. package/src/server/services/health-check.js +399 -0
  102. package/src/server/services/mcp-service.js +1188 -0
  103. package/src/server/services/prompts-service.js +492 -0
  104. package/src/server/services/proxy-runtime.js +79 -0
  105. package/src/server/services/pty-manager.js +435 -0
  106. package/src/server/services/rules-service.js +401 -0
  107. package/src/server/services/session-cache.js +127 -0
  108. package/src/server/services/session-converter.js +577 -0
  109. package/src/server/services/sessions.js +757 -0
  110. package/src/server/services/settings-manager.js +163 -0
  111. package/src/server/services/skill-service.js +965 -0
  112. package/src/server/services/speed-test.js +545 -0
  113. package/src/server/services/statistics-service.js +386 -0
  114. package/src/server/services/terminal-commands.js +155 -0
  115. package/src/server/services/terminal-config.js +140 -0
  116. package/src/server/services/terminal-detector.js +306 -0
  117. package/src/server/services/ui-config.js +130 -0
  118. package/src/server/services/workspace-service.js +662 -0
  119. package/src/server/utils/pricing.js +41 -0
  120. package/src/server/websocket-server.js +557 -0
  121. package/src/ui/menu.js +129 -0
  122. package/src/ui/prompts.js +100 -0
  123. package/src/utils/format.js +43 -0
  124. package/src/utils/port-helper.js +94 -0
  125. package/src/utils/session.js +239 -0
@@ -0,0 +1,118 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const {
4
+ healthCheckAllProjects,
5
+ scanLegacySessionFiles,
6
+ migrateSessionFiles,
7
+ cleanLegacySessionFiles
8
+ } = require('../services/health-check');
9
+ const { getProjects } = require('../services/sessions');
10
+
11
+ module.exports = (config) => {
12
+ /**
13
+ * GET /api/health-check - 健康检查所有项目
14
+ */
15
+ router.get('/', (req, res) => {
16
+ try {
17
+ const projects = getProjects(config);
18
+ const result = healthCheckAllProjects(projects);
19
+
20
+ res.json({
21
+ success: true,
22
+ timestamp: new Date().toISOString(),
23
+ ...result
24
+ });
25
+ } catch (error) {
26
+ console.error('Health check failed:', error);
27
+ res.status(500).json({
28
+ success: false,
29
+ error: error.message
30
+ });
31
+ }
32
+ });
33
+
34
+ /**
35
+ * GET /api/health-check/scan-legacy - 扫描旧文件
36
+ */
37
+ router.get('/scan-legacy', (req, res) => {
38
+ try {
39
+ const result = scanLegacySessionFiles();
40
+
41
+ res.json({
42
+ success: true,
43
+ timestamp: new Date().toISOString(),
44
+ ...result
45
+ });
46
+ } catch (error) {
47
+ console.error('Legacy scan failed:', error);
48
+ res.status(500).json({
49
+ success: false,
50
+ error: error.message
51
+ });
52
+ }
53
+ });
54
+
55
+ /**
56
+ * POST /api/health-check/migrate-legacy - 迁移旧文件到正确位置
57
+ * Body:
58
+ * {
59
+ * "dryRun": boolean, // 是否只是预演
60
+ * "projectNames": string[] // 可选:指定要迁移的项目
61
+ * }
62
+ */
63
+ router.post('/migrate-legacy', (req, res) => {
64
+ try {
65
+ const { dryRun = false, projectNames = null } = req.body;
66
+
67
+ const result = migrateSessionFiles({
68
+ dryRun,
69
+ projectNames
70
+ });
71
+
72
+ res.json({
73
+ success: true,
74
+ timestamp: new Date().toISOString(),
75
+ ...result
76
+ });
77
+ } catch (error) {
78
+ console.error('Legacy migration failed:', error);
79
+ res.status(500).json({
80
+ success: false,
81
+ error: error.message
82
+ });
83
+ }
84
+ });
85
+
86
+ /**
87
+ * POST /api/health-check/clean-legacy - 清理旧文件
88
+ * Body:
89
+ * {
90
+ * "dryRun": boolean, // 是否只是预演
91
+ * "projectNames": string[] // 可选:指定要清理的项目
92
+ * }
93
+ */
94
+ router.post('/clean-legacy', (req, res) => {
95
+ try {
96
+ const { dryRun = false, projectNames = null } = req.body;
97
+
98
+ const result = cleanLegacySessionFiles({
99
+ dryRun,
100
+ projectNames
101
+ });
102
+
103
+ res.json({
104
+ success: true,
105
+ timestamp: new Date().toISOString(),
106
+ ...result
107
+ });
108
+ } catch (error) {
109
+ console.error('Legacy cleanup failed:', error);
110
+ res.status(500).json({
111
+ success: false,
112
+ error: error.message
113
+ });
114
+ }
115
+ });
116
+
117
+ return router;
118
+ };
@@ -0,0 +1,336 @@
1
+ /**
2
+ * MCP 服务器管理 API 路由
3
+ */
4
+
5
+ const express = require('express');
6
+ const router = express.Router();
7
+ const mcpService = require('../services/mcp-service');
8
+
9
+ /**
10
+ * GET /api/mcp/servers
11
+ * 获取所有 MCP 服务器
12
+ */
13
+ router.get('/servers', (req, res) => {
14
+ try {
15
+ const servers = mcpService.getAllServers();
16
+ res.json({
17
+ success: true,
18
+ servers
19
+ });
20
+ } catch (error) {
21
+ console.error('[MCP API] Get servers failed:', error);
22
+ res.status(500).json({
23
+ success: false,
24
+ error: error.message
25
+ });
26
+ }
27
+ });
28
+
29
+ /**
30
+ * GET /api/mcp/servers/:id
31
+ * 获取单个 MCP 服务器
32
+ */
33
+ router.get('/servers/:id', (req, res) => {
34
+ try {
35
+ const server = mcpService.getServer(req.params.id);
36
+ if (!server) {
37
+ return res.status(404).json({
38
+ success: false,
39
+ error: `MCP 服务器 "${req.params.id}" 不存在`
40
+ });
41
+ }
42
+ res.json({
43
+ success: true,
44
+ server
45
+ });
46
+ } catch (error) {
47
+ console.error('[MCP API] Get server failed:', error);
48
+ res.status(500).json({
49
+ success: false,
50
+ error: error.message
51
+ });
52
+ }
53
+ });
54
+
55
+ /**
56
+ * POST /api/mcp/servers
57
+ * 添加或更新 MCP 服务器
58
+ */
59
+ router.post('/servers', async (req, res) => {
60
+ try {
61
+ const server = req.body;
62
+
63
+ if (!server.id) {
64
+ return res.status(400).json({
65
+ success: false,
66
+ error: 'MCP 服务器 ID 不能为空'
67
+ });
68
+ }
69
+
70
+ if (!server.server) {
71
+ return res.status(400).json({
72
+ success: false,
73
+ error: '服务器配置不能为空'
74
+ });
75
+ }
76
+
77
+ const result = await mcpService.saveServer(server);
78
+ res.json({
79
+ success: true,
80
+ server: result
81
+ });
82
+ } catch (error) {
83
+ console.error('[MCP API] Save server failed:', error);
84
+ res.status(400).json({
85
+ success: false,
86
+ error: error.message
87
+ });
88
+ }
89
+ });
90
+
91
+ /**
92
+ * DELETE /api/mcp/servers/:id
93
+ * 删除 MCP 服务器
94
+ */
95
+ router.delete('/servers/:id', async (req, res) => {
96
+ try {
97
+ const deleted = await mcpService.deleteServer(req.params.id);
98
+ if (!deleted) {
99
+ return res.status(404).json({
100
+ success: false,
101
+ error: `MCP 服务器 "${req.params.id}" 不存在`
102
+ });
103
+ }
104
+ res.json({
105
+ success: true
106
+ });
107
+ } catch (error) {
108
+ console.error('[MCP API] Delete server failed:', error);
109
+ res.status(500).json({
110
+ success: false,
111
+ error: error.message
112
+ });
113
+ }
114
+ });
115
+
116
+ /**
117
+ * POST /api/mcp/servers/:id/toggle
118
+ * 切换 MCP 服务器在某平台的启用状态
119
+ */
120
+ router.post('/servers/:id/toggle', async (req, res) => {
121
+ try {
122
+ const { app, enabled } = req.body;
123
+
124
+ if (!app) {
125
+ return res.status(400).json({
126
+ success: false,
127
+ error: '必须指定平台 (app)'
128
+ });
129
+ }
130
+
131
+ if (typeof enabled !== 'boolean') {
132
+ return res.status(400).json({
133
+ success: false,
134
+ error: '必须指定启用状态 (enabled)'
135
+ });
136
+ }
137
+
138
+ const server = await mcpService.toggleServerApp(req.params.id, app, enabled);
139
+ res.json({
140
+ success: true,
141
+ server
142
+ });
143
+ } catch (error) {
144
+ console.error('[MCP API] Toggle server failed:', error);
145
+ res.status(400).json({
146
+ success: false,
147
+ error: error.message
148
+ });
149
+ }
150
+ });
151
+
152
+ /**
153
+ * GET /api/mcp/presets
154
+ * 获取 MCP 预设模板列表
155
+ */
156
+ router.get('/presets', (req, res) => {
157
+ try {
158
+ const presets = mcpService.getPresets();
159
+ res.json({
160
+ success: true,
161
+ presets
162
+ });
163
+ } catch (error) {
164
+ console.error('[MCP API] Get presets failed:', error);
165
+ res.status(500).json({
166
+ success: false,
167
+ error: error.message
168
+ });
169
+ }
170
+ });
171
+
172
+ /**
173
+ * POST /api/mcp/import/:platform
174
+ * 从指定平台导入 MCP 配置
175
+ */
176
+ router.post('/import/:platform', async (req, res) => {
177
+ try {
178
+ const { platform } = req.params;
179
+
180
+ if (!['claude', 'codex', 'gemini'].includes(platform)) {
181
+ return res.status(400).json({
182
+ success: false,
183
+ error: `无效的平台: ${platform}`
184
+ });
185
+ }
186
+
187
+ const count = await mcpService.importFromPlatform(platform);
188
+ res.json({
189
+ success: true,
190
+ imported: count,
191
+ message: count > 0
192
+ ? `成功从 ${platform} 导入 ${count} 个 MCP 服务器`
193
+ : `${platform} 没有可导入的 MCP 服务器`
194
+ });
195
+ } catch (error) {
196
+ console.error('[MCP API] Import failed:', error);
197
+ res.status(500).json({
198
+ success: false,
199
+ error: error.message
200
+ });
201
+ }
202
+ });
203
+
204
+ /**
205
+ * GET /api/mcp/stats
206
+ * 获取 MCP 统计信息
207
+ */
208
+ router.get('/stats', (req, res) => {
209
+ try {
210
+ const stats = mcpService.getStats();
211
+ res.json({
212
+ success: true,
213
+ stats
214
+ });
215
+ } catch (error) {
216
+ console.error('[MCP API] Get stats failed:', error);
217
+ res.status(500).json({
218
+ success: false,
219
+ error: error.message
220
+ });
221
+ }
222
+ });
223
+
224
+ /**
225
+ * POST /api/mcp/servers/:id/test
226
+ * 测试 MCP 服务器连接
227
+ */
228
+ router.post('/servers/:id/test', async (req, res) => {
229
+ try {
230
+ const result = await mcpService.testServer(req.params.id);
231
+
232
+ // 更新服务器状态
233
+ const status = result.success ? 'online' : 'error';
234
+ await mcpService.updateServerStatus(req.params.id, status);
235
+
236
+ res.json({
237
+ success: true,
238
+ result
239
+ });
240
+ } catch (error) {
241
+ console.error('[MCP API] Test server failed:', error);
242
+ res.status(500).json({
243
+ success: false,
244
+ error: error.message
245
+ });
246
+ }
247
+ });
248
+
249
+ /**
250
+ * POST /api/mcp/servers/order
251
+ * 更新服务器排序
252
+ */
253
+ router.post('/servers/order', (req, res) => {
254
+ try {
255
+ const { serverIds } = req.body;
256
+
257
+ if (!Array.isArray(serverIds)) {
258
+ return res.status(400).json({
259
+ success: false,
260
+ error: 'serverIds 必须是数组'
261
+ });
262
+ }
263
+
264
+ const servers = mcpService.updateServerOrder(serverIds);
265
+ res.json({
266
+ success: true,
267
+ servers
268
+ });
269
+ } catch (error) {
270
+ console.error('[MCP API] Update order failed:', error);
271
+ res.status(500).json({
272
+ success: false,
273
+ error: error.message
274
+ });
275
+ }
276
+ });
277
+
278
+ /**
279
+ * GET /api/mcp/export
280
+ * 导出 MCP 配置
281
+ */
282
+ router.get('/export', (req, res) => {
283
+ try {
284
+ const format = req.query.format || 'json';
285
+
286
+ if (!['json', 'claude', 'codex'].includes(format)) {
287
+ return res.status(400).json({
288
+ success: false,
289
+ error: `无效的导出格式: ${format}`
290
+ });
291
+ }
292
+
293
+ const result = mcpService.exportServers(format);
294
+ res.json({
295
+ success: true,
296
+ ...result
297
+ });
298
+ } catch (error) {
299
+ console.error('[MCP API] Export failed:', error);
300
+ res.status(500).json({
301
+ success: false,
302
+ error: error.message
303
+ });
304
+ }
305
+ });
306
+
307
+ /**
308
+ * GET /api/mcp/export/download
309
+ * 下载导出的配置文件
310
+ */
311
+ router.get('/export/download', (req, res) => {
312
+ try {
313
+ const format = req.query.format || 'json';
314
+
315
+ if (!['json', 'claude', 'codex'].includes(format)) {
316
+ return res.status(400).json({
317
+ success: false,
318
+ error: `无效的导出格式: ${format}`
319
+ });
320
+ }
321
+
322
+ const result = mcpService.exportServers(format);
323
+
324
+ res.setHeader('Content-Type', format === 'codex' ? 'application/toml' : 'application/json');
325
+ res.setHeader('Content-Disposition', `attachment; filename="${result.filename}"`);
326
+ res.send(result.content);
327
+ } catch (error) {
328
+ console.error('[MCP API] Export download failed:', error);
329
+ res.status(500).json({
330
+ success: false,
331
+ error: error.message
332
+ });
333
+ }
334
+ });
335
+
336
+ module.exports = router;
@@ -0,0 +1,269 @@
1
+ const express = require('express');
2
+ const { exec } = require('child_process');
3
+ const { promisify } = require('util');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const pm2 = require('pm2');
8
+
9
+ const execAsync = promisify(exec);
10
+
11
+ /**
12
+ * Check if PM2 autostart is enabled
13
+ * by looking for PM2 startup script in system
14
+ */
15
+ async function checkAutoStartStatus() {
16
+ try {
17
+ const platform = process.platform;
18
+
19
+ if (platform === 'darwin') {
20
+ // macOS - check for LaunchDaemon
21
+ const launchDaemonsPath = path.join(os.homedir(), 'Library/LaunchDaemons');
22
+ const pm2Files = fs.existsSync(launchDaemonsPath)
23
+ ? fs.readdirSync(launchDaemonsPath).filter(f => f.includes('pm2'))
24
+ : [];
25
+
26
+ return { enabled: pm2Files.length > 0, platform: 'darwin' };
27
+ } else if (platform === 'linux') {
28
+ // Linux - check for systemd service
29
+ const systemdPath = '/etc/systemd/system/pm2-root.service';
30
+ const userSystemdPath = path.join(os.homedir(), '.config/systemd/user/pm2-*.service');
31
+
32
+ const rootExists = fs.existsSync(systemdPath);
33
+ const userExists = fs.existsSync(path.join(os.homedir(), '.config/systemd/user')) &&
34
+ fs.readdirSync(path.join(os.homedir(), '.config/systemd/user')).some(f => f.includes('pm2'));
35
+
36
+ return { enabled: rootExists || userExists, platform: 'linux' };
37
+ } else if (platform === 'win32') {
38
+ // Windows - check for PM2 service in registry (simplified check)
39
+ // For now, assume Windows support via pm2 package manager
40
+ return { enabled: false, platform: 'win32', note: '暂不支持 Windows' };
41
+ }
42
+
43
+ return { enabled: false, platform };
44
+ } catch (err) {
45
+ console.error('Error checking autostart status:', err);
46
+ return { enabled: false, error: err.message };
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Enable PM2 autostart
52
+ * Runs: pm2 startup && pm2 save
53
+ */
54
+ async function enableAutoStart() {
55
+ return new Promise((resolve) => {
56
+ pm2.connect((err) => {
57
+ if (err) {
58
+ console.error('PM2 connect error:', err);
59
+ return resolve({
60
+ success: false,
61
+ message: '无法连接到 PM2:' + (err.message || '未知错误'),
62
+ error: err.message
63
+ });
64
+ }
65
+
66
+ // Get current process list
67
+ pm2.list((listErr, processes) => {
68
+ if (listErr) {
69
+ pm2.disconnect();
70
+ console.error('PM2 list error:', listErr);
71
+ return resolve({
72
+ success: false,
73
+ message: '无法获取 PM2 进程列表:' + (listErr.message || '未知错误')
74
+ });
75
+ }
76
+
77
+ // If no processes are running, we can't really set up autostart
78
+ if (!processes || processes.length === 0) {
79
+ pm2.disconnect();
80
+ return resolve({
81
+ success: false,
82
+ message: '暂无运行中的进程,无法启用开机自启。请先启动服务:ctx start'
83
+ });
84
+ }
85
+
86
+ // Save current process list
87
+ pm2.save((saveErr) => {
88
+ if (saveErr) {
89
+ pm2.disconnect();
90
+ console.error('PM2 save error:', saveErr);
91
+ return resolve({
92
+ success: false,
93
+ message: '无法保存 PM2 配置:' + (saveErr.message || '未知错误')
94
+ });
95
+ }
96
+
97
+ // Run startup command
98
+ const platform = process.platform;
99
+ const command = platform === 'darwin'
100
+ ? 'pm2 startup launchd -u $(whoami) --hp $(eval echo ~$(whoami))'
101
+ : platform === 'linux'
102
+ ? 'pm2 startup systemd -u $(whoami) --hp $(eval echo ~$(whoami))'
103
+ : 'pm2 startup';
104
+
105
+ console.log(`Running startup command: ${command}`);
106
+
107
+ exec(command, { shell: '/bin/bash', timeout: 30000 }, (execErr, stdout, stderr) => {
108
+ pm2.disconnect();
109
+
110
+ if (execErr) {
111
+ console.error('Startup command error:', execErr);
112
+ console.error('stderr:', stderr);
113
+
114
+ // Check if it's already enabled
115
+ if (stderr && stderr.includes('already')) {
116
+ return resolve({
117
+ success: true,
118
+ message: '开机自启已启用(或已存在)'
119
+ });
120
+ }
121
+
122
+ return resolve({
123
+ success: false,
124
+ message: '启用失败。' + (stderr || execErr.message || '请确保已安装 PM2 且有足够权限'),
125
+ error: execErr.message
126
+ });
127
+ }
128
+
129
+ console.log('Startup command output:', stdout);
130
+ return resolve({
131
+ success: true,
132
+ message: '开机自启已启用。重启电脑后自动启动'
133
+ });
134
+ });
135
+ });
136
+ });
137
+ });
138
+ });
139
+ }
140
+
141
+ /**
142
+ * Disable PM2 autostart
143
+ * Runs: pm2 unstartup
144
+ */
145
+ async function disableAutoStart() {
146
+ return new Promise((resolve) => {
147
+ pm2.connect((err) => {
148
+ if (err) {
149
+ console.error('PM2 connect error:', err);
150
+ return resolve({
151
+ success: false,
152
+ message: '无法连接到 PM2:' + (err.message || '未知错误')
153
+ });
154
+ }
155
+
156
+ // Run unstartup command
157
+ const platform = process.platform;
158
+ const command = platform === 'darwin'
159
+ ? 'pm2 unstartup launchd -u $(whoami)'
160
+ : platform === 'linux'
161
+ ? 'pm2 unstartup systemd -u $(whoami)'
162
+ : 'pm2 unstartup';
163
+
164
+ console.log(`Running unstartup command: ${command}`);
165
+
166
+ exec(command, { shell: '/bin/bash', timeout: 30000 }, (execErr, stdout, stderr) => {
167
+ pm2.disconnect();
168
+
169
+ if (execErr) {
170
+ console.error('Unstartup command error:', execErr);
171
+ console.error('stderr:', stderr);
172
+
173
+ // Check if it's not set up
174
+ if (stderr && stderr.includes('not set')) {
175
+ return resolve({
176
+ success: true,
177
+ message: '开机自启已禁用(或未启用)'
178
+ });
179
+ }
180
+
181
+ return resolve({
182
+ success: false,
183
+ message: '禁用失败。' + (stderr || execErr.message || '请确保已安装 PM2 且有足够权限'),
184
+ error: execErr.message
185
+ });
186
+ }
187
+
188
+ console.log('Unstartup command output:', stdout);
189
+ return resolve({
190
+ success: true,
191
+ message: '开机自启已禁用'
192
+ });
193
+ });
194
+ });
195
+ });
196
+ }
197
+
198
+ module.exports = () => {
199
+ const router = express.Router();
200
+
201
+ /**
202
+ * GET /api/pm2-autostart
203
+ * Get current PM2 autostart status
204
+ */
205
+ router.get('/', async (req, res) => {
206
+ try {
207
+ const status = await checkAutoStartStatus();
208
+ res.json({
209
+ success: true,
210
+ data: status
211
+ });
212
+ } catch (err) {
213
+ console.error('Failed to check autostart status:', err);
214
+ // 返回 200 状态码,让前端通过 success 字段判断
215
+ res.json({
216
+ success: false,
217
+ message: 'Failed to check autostart status: ' + err.message
218
+ });
219
+ }
220
+ });
221
+
222
+ /**
223
+ * POST /api/pm2-autostart
224
+ * Enable or disable PM2 autostart
225
+ * Body: { action: 'enable' | 'disable' }
226
+ */
227
+ router.post('/', async (req, res) => {
228
+ try {
229
+ const { action } = req.body;
230
+
231
+ if (!action || !['enable', 'disable'].includes(action)) {
232
+ return res.status(400).json({
233
+ success: false,
234
+ message: 'Invalid action. Must be "enable" or "disable"'
235
+ });
236
+ }
237
+
238
+ let result;
239
+ if (action === 'enable') {
240
+ result = await enableAutoStart();
241
+ } else {
242
+ result = await disableAutoStart();
243
+ }
244
+
245
+ if (result.success) {
246
+ res.json({
247
+ success: true,
248
+ message: result.message,
249
+ data: { action, enabled: action === 'enable' }
250
+ });
251
+ } else {
252
+ // 返回 200 状态码,让前端通过 success 字段判断
253
+ res.json({
254
+ success: false,
255
+ message: result.message
256
+ });
257
+ }
258
+ } catch (err) {
259
+ console.error('Failed to configure autostart:', err);
260
+ // 真正的服务器错误才返回 500
261
+ res.status(500).json({
262
+ success: false,
263
+ message: '服务器错误:' + err.message
264
+ });
265
+ }
266
+ });
267
+
268
+ return router;
269
+ };