@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.
- package/CHANGELOG.md +333 -0
- package/LICENSE +21 -0
- package/README.md +404 -0
- package/bin/ctx.js +8 -0
- package/dist/web/assets/index-D1AYlFLZ.js +3220 -0
- package/dist/web/assets/index-aL3cKxSK.css +41 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +14 -0
- package/dist/web/logo.png +0 -0
- package/docs/CHANGELOG.md +582 -0
- package/docs/DIRECTORY_MIGRATION.md +112 -0
- package/docs/PROJECT_STRUCTURE.md +396 -0
- package/docs/bannel.png +0 -0
- package/docs/home.png +0 -0
- package/docs/logo.png +0 -0
- package/docs/multi-channel-load-balancing.md +249 -0
- package/package.json +73 -0
- package/src/commands/channels.js +504 -0
- package/src/commands/cli-type.js +99 -0
- package/src/commands/daemon.js +286 -0
- package/src/commands/doctor.js +332 -0
- package/src/commands/list.js +222 -0
- package/src/commands/logs.js +259 -0
- package/src/commands/port-config.js +115 -0
- package/src/commands/proxy-control.js +258 -0
- package/src/commands/proxy.js +152 -0
- package/src/commands/resume.js +137 -0
- package/src/commands/search.js +190 -0
- package/src/commands/stats.js +224 -0
- package/src/commands/switch.js +48 -0
- package/src/commands/toggle-proxy.js +222 -0
- package/src/commands/ui.js +92 -0
- package/src/commands/workspace.js +454 -0
- package/src/config/default.js +40 -0
- package/src/config/loader.js +75 -0
- package/src/config/paths.js +121 -0
- package/src/index.js +373 -0
- package/src/reset-config.js +92 -0
- package/src/server/api/agents.js +248 -0
- package/src/server/api/aliases.js +36 -0
- package/src/server/api/channels.js +258 -0
- package/src/server/api/claude-hooks.js +480 -0
- package/src/server/api/codex-channels.js +312 -0
- package/src/server/api/codex-projects.js +91 -0
- package/src/server/api/codex-proxy.js +182 -0
- package/src/server/api/codex-sessions.js +491 -0
- package/src/server/api/codex-statistics.js +57 -0
- package/src/server/api/commands.js +245 -0
- package/src/server/api/config-templates.js +182 -0
- package/src/server/api/config.js +147 -0
- package/src/server/api/convert.js +127 -0
- package/src/server/api/dashboard.js +125 -0
- package/src/server/api/env.js +144 -0
- package/src/server/api/favorites.js +77 -0
- package/src/server/api/gemini-channels.js +261 -0
- package/src/server/api/gemini-projects.js +91 -0
- package/src/server/api/gemini-proxy.js +160 -0
- package/src/server/api/gemini-sessions.js +397 -0
- package/src/server/api/gemini-statistics.js +57 -0
- package/src/server/api/health-check.js +118 -0
- package/src/server/api/mcp.js +336 -0
- package/src/server/api/pm2-autostart.js +269 -0
- package/src/server/api/projects.js +124 -0
- package/src/server/api/prompts.js +279 -0
- package/src/server/api/proxy.js +235 -0
- package/src/server/api/rules.js +271 -0
- package/src/server/api/sessions.js +595 -0
- package/src/server/api/settings.js +61 -0
- package/src/server/api/skills.js +305 -0
- package/src/server/api/statistics.js +91 -0
- package/src/server/api/terminal.js +202 -0
- package/src/server/api/ui-config.js +64 -0
- package/src/server/api/workspaces.js +407 -0
- package/src/server/codex-proxy-server.js +538 -0
- package/src/server/dev-server.js +26 -0
- package/src/server/gemini-proxy-server.js +518 -0
- package/src/server/index.js +305 -0
- package/src/server/proxy-server.js +469 -0
- package/src/server/services/agents-service.js +354 -0
- package/src/server/services/alias.js +71 -0
- package/src/server/services/channel-health.js +234 -0
- package/src/server/services/channel-scheduler.js +234 -0
- package/src/server/services/channels.js +347 -0
- package/src/server/services/codex-channels.js +625 -0
- package/src/server/services/codex-config.js +90 -0
- package/src/server/services/codex-parser.js +322 -0
- package/src/server/services/codex-sessions.js +665 -0
- package/src/server/services/codex-settings-manager.js +397 -0
- package/src/server/services/codex-speed-test-template.json +24 -0
- package/src/server/services/codex-statistics-service.js +255 -0
- package/src/server/services/commands-service.js +360 -0
- package/src/server/services/config-templates-service.js +732 -0
- package/src/server/services/env-checker.js +307 -0
- package/src/server/services/env-manager.js +300 -0
- package/src/server/services/favorites.js +163 -0
- package/src/server/services/gemini-channels.js +333 -0
- package/src/server/services/gemini-config.js +73 -0
- package/src/server/services/gemini-sessions.js +689 -0
- package/src/server/services/gemini-settings-manager.js +263 -0
- package/src/server/services/gemini-statistics-service.js +253 -0
- package/src/server/services/health-check.js +399 -0
- package/src/server/services/mcp-service.js +1188 -0
- package/src/server/services/prompts-service.js +492 -0
- package/src/server/services/proxy-runtime.js +79 -0
- package/src/server/services/pty-manager.js +435 -0
- package/src/server/services/rules-service.js +401 -0
- package/src/server/services/session-cache.js +127 -0
- package/src/server/services/session-converter.js +577 -0
- package/src/server/services/sessions.js +757 -0
- package/src/server/services/settings-manager.js +163 -0
- package/src/server/services/skill-service.js +965 -0
- package/src/server/services/speed-test.js +545 -0
- package/src/server/services/statistics-service.js +386 -0
- package/src/server/services/terminal-commands.js +155 -0
- package/src/server/services/terminal-config.js +140 -0
- package/src/server/services/terminal-detector.js +306 -0
- package/src/server/services/ui-config.js +130 -0
- package/src/server/services/workspace-service.js +662 -0
- package/src/server/utils/pricing.js +41 -0
- package/src/server/websocket-server.js +557 -0
- package/src/ui/menu.js +129 -0
- package/src/ui/prompts.js +100 -0
- package/src/utils/format.js +43 -0
- package/src/utils/port-helper.js +94 -0
- 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;
|