@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,595 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const readline = require('readline');
7
+ const { getSessionsForProject, deleteSession, forkSession, saveSessionOrder, parseRealProjectPath, searchSessions, getRecentSessions, searchSessionsAcrossProjects, hasActualMessages } = require('../services/sessions');
8
+ const { loadAliases } = require('../services/alias');
9
+ const { broadcastLog } = require('../websocket-server');
10
+
11
+ module.exports = (config) => {
12
+ // GET /api/sessions/search/global - Search sessions across all projects
13
+ router.get('/search/global', (req, res) => {
14
+ try {
15
+ const { keyword, context } = req.query;
16
+
17
+ if (!keyword) {
18
+ return res.status(400).json({ error: 'Keyword is required' });
19
+ }
20
+
21
+ const contextLength = context ? parseInt(context) : 35;
22
+ const results = searchSessionsAcrossProjects(config, keyword, contextLength);
23
+
24
+ res.json({
25
+ keyword,
26
+ totalMatches: results.reduce((sum, r) => sum + r.matchCount, 0),
27
+ sessions: results
28
+ });
29
+ } catch (error) {
30
+ console.error('Error searching sessions globally:', error);
31
+ res.status(500).json({ error: error.message });
32
+ }
33
+ });
34
+
35
+ // GET /api/sessions/recent - Get recent sessions across all projects
36
+ router.get('/recent/list', (req, res) => {
37
+ try {
38
+ const limit = parseInt(req.query.limit) || 5;
39
+ const sessions = getRecentSessions(config, limit);
40
+ res.json({ sessions });
41
+ } catch (error) {
42
+ console.error('Error fetching recent sessions:', error);
43
+ res.status(500).json({ error: error.message });
44
+ }
45
+ });
46
+
47
+ // GET /api/sessions/:projectName - Get sessions for a project
48
+ router.get('/:projectName', (req, res) => {
49
+ try {
50
+ const { projectName } = req.params;
51
+ const result = getSessionsForProject(config, projectName);
52
+ const aliases = loadAliases();
53
+
54
+ // Parse project path info
55
+ const { fullPath, projectName: displayName } = parseRealProjectPath(projectName);
56
+
57
+ res.json({
58
+ sessions: result.sessions,
59
+ totalSize: result.totalSize,
60
+ aliases,
61
+ projectInfo: {
62
+ name: projectName,
63
+ displayName,
64
+ fullPath
65
+ }
66
+ });
67
+ } catch (error) {
68
+ console.error('Error fetching sessions:', error);
69
+ res.status(500).json({ error: error.message });
70
+ }
71
+ });
72
+
73
+ // DELETE /api/sessions/:projectName/:sessionId - Delete a session
74
+ router.delete('/:projectName/:sessionId', (req, res) => {
75
+ try {
76
+ const { projectName, sessionId } = req.params;
77
+ const result = deleteSession(config, projectName, sessionId);
78
+ res.json(result);
79
+ } catch (error) {
80
+ console.error('Error deleting session:', error);
81
+ res.status(500).json({ error: error.message });
82
+ }
83
+ });
84
+
85
+ // POST /api/sessions/:projectName/:sessionId/fork - Fork a session
86
+ router.post('/:projectName/:sessionId/fork', (req, res) => {
87
+ try {
88
+ const { projectName, sessionId } = req.params;
89
+ const result = forkSession(config, projectName, sessionId);
90
+ res.json(result);
91
+ } catch (error) {
92
+ console.error('Error forking session:', error);
93
+ res.status(500).json({ error: error.message });
94
+ }
95
+ });
96
+
97
+ // POST /api/sessions/:projectName/create - Create a new session
98
+ router.post('/:projectName/create', (req, res) => {
99
+ try {
100
+ const { projectName } = req.params;
101
+ const { toolType = 'claude' } = req.body; // 'claude', 'codex', 或 'gemini'
102
+ const crypto = require('crypto');
103
+
104
+ // 解析项目路径
105
+ const { fullPath } = parseRealProjectPath(projectName);
106
+
107
+ // 生成新的 session ID
108
+ const newSessionId = crypto.randomUUID();
109
+
110
+ // 根据工具类型决定会话文件路径
111
+ let sessionDir, sessionFile;
112
+
113
+ if (toolType === 'claude') {
114
+ // Claude Code: 直接创建在项目的 .claude/sessions/ 目录(与 Claude Code 默认行为一致)
115
+ sessionDir = path.join(fullPath, '.claude', 'sessions');
116
+ sessionFile = path.join(sessionDir, `${newSessionId}.jsonl`);
117
+ } else if (toolType === 'codex') {
118
+ // Codex: ~/.codex/sessions/YYYY/MM/DD/{sessionId}.jsonl
119
+ const now = new Date();
120
+ const year = now.getFullYear();
121
+ const month = String(now.getMonth() + 1).padStart(2, '0');
122
+ const day = String(now.getDate()).padStart(2, '0');
123
+ sessionDir = path.join(os.homedir(), '.codex', 'sessions', String(year), month, day);
124
+ sessionFile = path.join(sessionDir, `${newSessionId}.jsonl`);
125
+ } else if (toolType === 'gemini') {
126
+ // Gemini: ~/.gemini/tmp/{hash}/chats/{sessionId}.json
127
+ const pathHash = crypto.createHash('sha256').update(fullPath).digest('hex');
128
+ sessionDir = path.join(os.homedir(), '.gemini', 'tmp', pathHash, 'chats');
129
+ sessionFile = path.join(sessionDir, `${newSessionId}.json`);
130
+ } else {
131
+ return res.status(400).json({ error: 'Invalid toolType. Must be claude, codex, or gemini' });
132
+ }
133
+
134
+ // 确保目录存在
135
+ if (!fs.existsSync(sessionDir)) {
136
+ fs.mkdirSync(sessionDir, { recursive: true });
137
+ }
138
+
139
+ // 创建初始化会话文件
140
+ const timestamp = new Date().toISOString();
141
+ let initialContent;
142
+
143
+ if (toolType === 'gemini') {
144
+ // Gemini 使用 JSON 格式
145
+ initialContent = JSON.stringify({
146
+ id: newSessionId,
147
+ projectPath: fullPath,
148
+ createdAt: timestamp,
149
+ messages: []
150
+ }, null, 2);
151
+ } else {
152
+ // Claude 和 Codex 使用 JSONL 格式
153
+ const metadata = {
154
+ type: 'metadata',
155
+ cwd: fullPath,
156
+ gitBranch: null,
157
+ timestamp: timestamp
158
+ };
159
+ initialContent = JSON.stringify(metadata) + '\n';
160
+ }
161
+
162
+ fs.writeFileSync(sessionFile, initialContent, 'utf8');
163
+
164
+ // 广播日志
165
+ broadcastLog({
166
+ type: 'action',
167
+ action: 'create_session',
168
+ message: `创建新会话: ${newSessionId.substring(0, 8)} (${toolType})`,
169
+ sessionId: newSessionId,
170
+ tool: toolType,
171
+ timestamp: Date.now()
172
+ });
173
+
174
+ res.json({
175
+ success: true,
176
+ sessionId: newSessionId,
177
+ sessionFile,
178
+ toolType,
179
+ projectName
180
+ });
181
+ } catch (error) {
182
+ console.error('Error creating session:', error);
183
+ res.status(500).json({ error: error.message });
184
+ }
185
+ });
186
+
187
+ // POST /api/sessions/:projectName/order - Save session order
188
+ router.post('/:projectName/order', (req, res) => {
189
+ try {
190
+ const { projectName } = req.params;
191
+ const { order } = req.body;
192
+ saveSessionOrder(projectName, order);
193
+ res.json({ success: true });
194
+ } catch (error) {
195
+ console.error('Error saving session order:', error);
196
+ res.status(500).json({ error: error.message });
197
+ }
198
+ });
199
+
200
+ // GET /api/sessions/:projectName/search - Search sessions content
201
+ router.get('/:projectName/search', (req, res) => {
202
+ try {
203
+ const { projectName } = req.params;
204
+ const { keyword, context } = req.query;
205
+
206
+ if (!keyword) {
207
+ return res.status(400).json({ error: 'Keyword is required' });
208
+ }
209
+
210
+ const contextLength = context ? parseInt(context) : 15;
211
+ const results = searchSessions(config, projectName, keyword, contextLength);
212
+
213
+ res.json({
214
+ keyword,
215
+ totalMatches: results.reduce((sum, r) => sum + r.matchCount, 0),
216
+ sessions: results
217
+ });
218
+ } catch (error) {
219
+ console.error('Error searching sessions:', error);
220
+ res.status(500).json({ error: error.message });
221
+ }
222
+ });
223
+
224
+ // GET /api/sessions/:projectName/:sessionId/messages - Get session messages with pagination
225
+ router.get('/:projectName/:sessionId/messages', async (req, res) => {
226
+ try {
227
+ const { projectName, sessionId } = req.params;
228
+ const { page = 1, limit = 20, order = 'desc' } = req.query;
229
+
230
+ console.log(`[Messages API] Request for ${projectName}/${sessionId}, page=${page}, limit=${limit}`);
231
+
232
+ const pageNum = parseInt(page);
233
+ const limitNum = parseInt(limit);
234
+
235
+ // Parse real project path
236
+ const { fullPath } = parseRealProjectPath(projectName);
237
+ console.log(`[Messages API] Parsed project path: ${fullPath}`);
238
+
239
+ // Try to find session file
240
+ let sessionFile = null;
241
+ const possiblePaths = [
242
+ path.join(fullPath, '.claude', 'sessions', sessionId + '.jsonl'),
243
+ path.join(os.homedir(), '.claude', 'projects', projectName, sessionId + '.jsonl')
244
+ ];
245
+
246
+ console.log(`[Messages API] Trying paths:`, possiblePaths);
247
+
248
+ for (const testPath of possiblePaths) {
249
+ if (fs.existsSync(testPath)) {
250
+ sessionFile = testPath;
251
+ console.log(`[Messages API] Found session file: ${sessionFile}`);
252
+ break;
253
+ }
254
+ }
255
+
256
+ if (!sessionFile) {
257
+ console.error(`[Messages API] Session file not found for: ${sessionId}`);
258
+ return res.status(404).json({
259
+ error: `Session file not found: ${sessionId}`,
260
+ triedPaths: possiblePaths
261
+ });
262
+ }
263
+
264
+ // Check if session has actual messages (not just file-history-snapshots)
265
+ if (!hasActualMessages(sessionFile)) {
266
+ console.warn(`[Messages API] Session ${sessionId} has no actual messages (only file-history-snapshots)`);
267
+ return res.status(404).json({
268
+ error: `Session has no conversation messages: ${sessionId}`,
269
+ reason: 'This session contains only file history snapshots, not actual conversation data'
270
+ });
271
+ }
272
+
273
+ // Read and parse session file
274
+ const allMessages = [];
275
+ const metadata = {};
276
+
277
+ const stream = fs.createReadStream(sessionFile, { encoding: 'utf8' });
278
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
279
+
280
+ try {
281
+ for await (const line of rl) {
282
+ if (!line.trim()) continue;
283
+ try {
284
+ const json = JSON.parse(line);
285
+
286
+ if (json.type === 'summary' && json.summary) {
287
+ metadata.summary = json.summary;
288
+ }
289
+ if (json.gitBranch) {
290
+ metadata.gitBranch = json.gitBranch;
291
+ }
292
+ if (json.cwd) {
293
+ metadata.cwd = json.cwd;
294
+ }
295
+
296
+ if (json.type === 'user' || json.type === 'assistant') {
297
+ const message = {
298
+ type: json.type,
299
+ content: null,
300
+ timestamp: json.timestamp || null,
301
+ model: json.model || null
302
+ };
303
+
304
+ if (json.type === 'user') {
305
+ if (typeof json.message?.content === 'string') {
306
+ message.content = json.message.content;
307
+ } else if (Array.isArray(json.message?.content)) {
308
+ const parts = [];
309
+ for (const item of json.message.content) {
310
+ if (item.type === 'text' && item.text) {
311
+ parts.push(item.text);
312
+ } else if (item.type === 'tool_result') {
313
+ const resultContent = typeof item.content === 'string'
314
+ ? item.content
315
+ : JSON.stringify(item.content, null, 2);
316
+ parts.push(`**[工具结果]**\n\`\`\`\n${resultContent}\n\`\`\``);
317
+ } else if (item.type === 'image') {
318
+ parts.push('[图片]');
319
+ }
320
+ }
321
+ message.content = parts.join('\n\n') || '[工具交互]';
322
+ }
323
+ } else if (json.type === 'assistant') {
324
+ if (Array.isArray(json.message?.content)) {
325
+ const parts = [];
326
+ for (const item of json.message.content) {
327
+ if (item.type === 'text' && item.text) {
328
+ parts.push(item.text);
329
+ } else if (item.type === 'tool_use') {
330
+ const inputStr = JSON.stringify(item.input, null, 2);
331
+ parts.push(`**[调用工具: ${item.name}]**\n\`\`\`json\n${inputStr}\n\`\`\``);
332
+ } else if (item.type === 'thinking' && item.thinking) {
333
+ parts.push(`**[思考]**\n${item.thinking}`);
334
+ }
335
+ }
336
+ message.content = parts.join('\n\n') || '[处理中...]';
337
+ } else if (typeof json.message?.content === 'string') {
338
+ message.content = json.message.content;
339
+ }
340
+ }
341
+
342
+ if (message.content && message.content !== 'Warmup') {
343
+ allMessages.push(message);
344
+ }
345
+ }
346
+ } catch (err) {
347
+ // Skip invalid lines
348
+ }
349
+ }
350
+ } finally {
351
+ rl.close();
352
+ stream.destroy();
353
+ }
354
+
355
+ // Sort messages (desc = newest first)
356
+ if (order === 'desc') {
357
+ allMessages.reverse();
358
+ }
359
+
360
+ console.log(`[Messages API] Parsed ${allMessages.length} total messages`);
361
+
362
+ // Pagination
363
+ const total = allMessages.length;
364
+ const startIndex = (pageNum - 1) * limitNum;
365
+ const endIndex = startIndex + limitNum;
366
+ const messages = allMessages.slice(startIndex, endIndex);
367
+ const hasMore = endIndex < total;
368
+
369
+ console.log(`[Messages API] Returning ${messages.length} messages (page ${pageNum}, total ${total})`);
370
+
371
+ res.json({
372
+ messages,
373
+ metadata,
374
+ pagination: {
375
+ page: pageNum,
376
+ limit: limitNum,
377
+ total,
378
+ hasMore
379
+ }
380
+ });
381
+ } catch (error) {
382
+ console.error('Error fetching session messages:', error);
383
+ res.status(500).json({ error: error.message });
384
+ }
385
+ });
386
+
387
+ // POST /api/sessions/:projectName/:sessionId/launch - Launch terminal with session
388
+ router.post('/:projectName/:sessionId/launch', async (req, res) => {
389
+ try {
390
+ const { projectName, sessionId } = req.params;
391
+ const { targetTool } = req.body; // 'claude', 'codex', 或 'gemini'
392
+ const { exec } = require('child_process');
393
+ const path = require('path');
394
+ const fs = require('fs');
395
+ const os = require('os');
396
+
397
+ // Parse real project path (important for cross-project sessions)
398
+ const { fullPath } = parseRealProjectPath(projectName);
399
+
400
+ const projectSessionsDir = path.join(fullPath, '.claude', 'sessions');
401
+ const projectSessionFile = path.join(projectSessionsDir, sessionId + '.jsonl');
402
+
403
+ // Try to find session file in multiple possible locations
404
+ let sessionFile = null;
405
+ const possiblePaths = [
406
+ projectSessionFile,
407
+ // Location 2: User's .claude/projects directory (ClaudeCode default)
408
+ path.join(os.homedir(), '.claude', 'projects', projectName, sessionId + '.jsonl')
409
+ ];
410
+
411
+ for (const testPath of possiblePaths) {
412
+ if (fs.existsSync(testPath)) {
413
+ sessionFile = testPath;
414
+ break;
415
+ }
416
+ }
417
+
418
+ // 如果会话只存在于全局目录,则复制到项目的 .claude/sessions 目录,避免 claude -r 找不到文件
419
+ if (sessionFile && sessionFile !== projectSessionFile) {
420
+ try {
421
+ if (!fs.existsSync(projectSessionsDir)) {
422
+ fs.mkdirSync(projectSessionsDir, { recursive: true });
423
+ }
424
+ fs.copyFileSync(sessionFile, projectSessionFile);
425
+ sessionFile = projectSessionFile;
426
+ } catch (copyError) {
427
+ console.warn('Failed to sync session file to project directory:', copyError.message);
428
+ }
429
+ }
430
+
431
+ if (!sessionFile) {
432
+ console.error(`Session file not found in any location for session: ${sessionId}`);
433
+ console.error('Tried paths:', possiblePaths);
434
+ return res.status(404).json({
435
+ error: `No conversation found with session ID: ${sessionId}`,
436
+ details: `Tried locations: ${possiblePaths.join(', ')}`
437
+ });
438
+ }
439
+
440
+ // 判断会话来源类型
441
+ let sourceType = 'claude'; // 默认
442
+ if (sessionFile.includes('/.codex/') || sessionFile.includes('\\.codex\\')) {
443
+ sourceType = 'codex';
444
+ } else if (sessionFile.includes('/.gemini/') || sessionFile.includes('\\.gemini\\')) {
445
+ sourceType = 'gemini';
446
+ }
447
+
448
+ // 如果指定了 targetTool 且与 sourceType 不同,则需要转换
449
+ let finalSessionFile = sessionFile;
450
+ let finalSessionId = sessionId;
451
+
452
+ if (targetTool && targetTool !== sourceType) {
453
+ console.log(`跨工具启动:${sourceType} -> ${targetTool},会话 ${sessionId}`);
454
+
455
+ try {
456
+ const { convertSession } = require('../services/session-converter');
457
+
458
+ // 执行转换
459
+ const convertResult = await convertSession(
460
+ sourceType,
461
+ targetTool,
462
+ sessionId,
463
+ {
464
+ sourcePath: sessionFile,
465
+ preserveTimestamps: true,
466
+ targetProjectPath: fullPath
467
+ }
468
+ );
469
+
470
+ if (convertResult.success) {
471
+ finalSessionFile = convertResult.targetPath;
472
+ finalSessionId = convertResult.targetSessionId;
473
+ console.log(`转换成功:${finalSessionFile}`);
474
+
475
+ // 广播转换日志
476
+ broadcastLog({
477
+ type: 'action',
478
+ action: 'auto_convert_session',
479
+ message: `自动转换会话:${sourceType} -> ${targetTool}`,
480
+ sessionId: finalSessionId,
481
+ timestamp: Date.now()
482
+ });
483
+ } else {
484
+ return res.status(500).json({
485
+ error: '会话转换失败:' + (convertResult.error || '未知错误')
486
+ });
487
+ }
488
+ } catch (convertError) {
489
+ console.error('会话转换出错:', convertError);
490
+ return res.status(500).json({
491
+ error: '会话转换出错:' + convertError.message
492
+ });
493
+ }
494
+ }
495
+
496
+ // Extract working directory from session file
497
+ let cwd = fullPath; // Default to project directory
498
+ try {
499
+ const content = fs.readFileSync(finalSessionFile, 'utf8');
500
+ const firstLine = content.split('\n')[0];
501
+ if (firstLine) {
502
+ const json = JSON.parse(firstLine);
503
+ if (json.cwd) {
504
+ cwd = json.cwd;
505
+ }
506
+ }
507
+ } catch (e) {
508
+ console.warn('Unable to extract cwd from session, using project path:', e.message);
509
+ }
510
+
511
+ // 确保会话文件在 cwd 的 .claude/sessions/ 目录下
512
+ // 这样 claude -r 才能找到文件
513
+ const cwdSessionsDir = path.join(cwd, '.claude', 'sessions');
514
+ const cwdSessionFile = path.join(cwdSessionsDir, finalSessionId + '.jsonl');
515
+
516
+ // 如果会话文件不在 cwd 的 sessions 目录,复制过去
517
+ if (finalSessionFile !== cwdSessionFile && !fs.existsSync(cwdSessionFile)) {
518
+ try {
519
+ if (!fs.existsSync(cwdSessionsDir)) {
520
+ fs.mkdirSync(cwdSessionsDir, { recursive: true });
521
+ }
522
+ fs.copyFileSync(finalSessionFile, cwdSessionFile);
523
+ console.log(`[Launch] Copied session to cwd: ${cwdSessionFile}`);
524
+ } catch (copyError) {
525
+ console.warn('[Launch] Failed to copy session file to cwd:', copyError.message);
526
+ // 如果复制失败,尝试更新 cwd 为项目目录
527
+ if (fs.existsSync(projectSessionsDir)) {
528
+ cwd = fullPath;
529
+ console.log(`[Launch] Fallback to project directory: ${cwd}`);
530
+ }
531
+ }
532
+ }
533
+
534
+ // Get alias
535
+ const aliases = loadAliases();
536
+ const alias = aliases[finalSessionId];
537
+
538
+ // 广播行为日志
539
+ broadcastLog({
540
+ type: 'action',
541
+ action: 'launch_session',
542
+ message: `启动会话 ${alias || finalSessionId.substring(0, 8)} (${targetTool || sourceType})`,
543
+ sessionId: finalSessionId,
544
+ alias: alias || null,
545
+ tool: targetTool || sourceType,
546
+ timestamp: Date.now()
547
+ });
548
+
549
+ // 使用配置的终端工具启动
550
+ const { getTerminalLaunchCommand } = require('../services/terminal-config');
551
+
552
+ try {
553
+ // Windows 路径需要转换为反斜杠格式
554
+ const normalizedCwd = process.platform === 'win32' ? cwd.replace(/\//g, '\\') : cwd;
555
+
556
+ // 获取启动命令(需要传入 targetTool)
557
+ const { command, terminalId, terminalName } = getTerminalLaunchCommand(
558
+ normalizedCwd,
559
+ finalSessionId,
560
+ targetTool || sourceType
561
+ );
562
+
563
+ console.log(`Launching terminal: ${terminalName} (${terminalId})`);
564
+ console.log(`Command: ${command}`);
565
+
566
+ // 异步执行命令,不等待结果
567
+ const shellOption = process.platform === 'win32' ? { shell: 'cmd.exe' } : { shell: true };
568
+ exec(command, shellOption, (error, stdout, stderr) => {
569
+ if (error) {
570
+ console.error(`Failed to launch terminal ${terminalName}:`, error.message);
571
+ }
572
+ });
573
+
574
+ // 立即返回成功响应
575
+ res.json({
576
+ success: true,
577
+ cwd,
578
+ sessionFile,
579
+ terminal: terminalName,
580
+ terminalId
581
+ });
582
+ } catch (terminalError) {
583
+ console.error('Failed to get terminal command:', terminalError);
584
+ return res.status(500).json({
585
+ error: '无法启动终端:' + terminalError.message
586
+ });
587
+ }
588
+ } catch (error) {
589
+ console.error('Error launching terminal:', error);
590
+ res.status(500).json({ error: error.message });
591
+ }
592
+ });
593
+
594
+ return router;
595
+ };
@@ -0,0 +1,61 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const { detectAvailableTerminals } = require('../services/terminal-detector');
4
+ const { loadTerminalConfig, saveTerminalConfig, getSelectedTerminal } = require('../services/terminal-config');
5
+
6
+ // GET /api/settings/terminals - 获取可用终端列表
7
+ router.get('/terminals', (req, res) => {
8
+ try {
9
+ const availableTerminals = detectAvailableTerminals();
10
+ const config = loadTerminalConfig();
11
+
12
+ res.json({
13
+ available: availableTerminals,
14
+ selected: config.selectedTerminal,
15
+ customCommand: config.customCommand
16
+ });
17
+ } catch (error) {
18
+ console.error('Error getting terminals:', error);
19
+ res.status(500).json({ error: error.message });
20
+ }
21
+ });
22
+
23
+ // GET /api/settings/terminal-config - 获取当前终端配置
24
+ router.get('/terminal-config', (req, res) => {
25
+ try {
26
+ const config = loadTerminalConfig();
27
+ const selectedTerminal = getSelectedTerminal();
28
+
29
+ res.json({
30
+ config,
31
+ selectedTerminal
32
+ });
33
+ } catch (error) {
34
+ console.error('Error getting terminal config:', error);
35
+ res.status(500).json({ error: error.message });
36
+ }
37
+ });
38
+
39
+ // POST /api/settings/terminal-config - 保存终端配置
40
+ router.post('/terminal-config', (req, res) => {
41
+ try {
42
+ const { selectedTerminal, customCommand } = req.body;
43
+
44
+ const config = {
45
+ selectedTerminal: selectedTerminal || null,
46
+ customCommand: customCommand || null
47
+ };
48
+
49
+ saveTerminalConfig(config);
50
+
51
+ res.json({
52
+ success: true,
53
+ config
54
+ });
55
+ } catch (error) {
56
+ console.error('Error saving terminal config:', error);
57
+ res.status(500).json({ error: error.message });
58
+ }
59
+ });
60
+
61
+ module.exports = router;