@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,491 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const {
|
|
4
|
+
getSessionsByProject,
|
|
5
|
+
getSessionById,
|
|
6
|
+
searchSessions,
|
|
7
|
+
forkSession,
|
|
8
|
+
deleteSession,
|
|
9
|
+
getRecentSessions,
|
|
10
|
+
saveSessionOrder
|
|
11
|
+
} = require('../services/codex-sessions');
|
|
12
|
+
const { isCodexInstalled } = require('../services/codex-config');
|
|
13
|
+
const { loadAliases } = require('../services/alias');
|
|
14
|
+
|
|
15
|
+
module.exports = (config) => {
|
|
16
|
+
// ============================================
|
|
17
|
+
// 静态路由必须放在参数路由之前
|
|
18
|
+
// ============================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* GET /api/codex/sessions/search/global?keyword=xxx
|
|
22
|
+
* 全局搜索
|
|
23
|
+
*/
|
|
24
|
+
router.get('/search/global', (req, res) => {
|
|
25
|
+
try {
|
|
26
|
+
if (!isCodexInstalled()) {
|
|
27
|
+
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { keyword } = req.query;
|
|
31
|
+
|
|
32
|
+
if (!keyword) {
|
|
33
|
+
return res.status(400).json({ error: 'Keyword is required' });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const results = searchSessions(keyword);
|
|
37
|
+
|
|
38
|
+
// 按会话分组,统计每个会话的匹配数
|
|
39
|
+
const sessionMap = new Map();
|
|
40
|
+
results.forEach(match => {
|
|
41
|
+
if (!sessionMap.has(match.sessionId)) {
|
|
42
|
+
sessionMap.set(match.sessionId, {
|
|
43
|
+
sessionId: match.sessionId,
|
|
44
|
+
projectName: match.projectName,
|
|
45
|
+
matchCount: 0,
|
|
46
|
+
matches: []
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const session = sessionMap.get(match.sessionId);
|
|
50
|
+
session.matchCount++;
|
|
51
|
+
session.matches.push({
|
|
52
|
+
messageIndex: match.messageIndex,
|
|
53
|
+
role: match.role,
|
|
54
|
+
context: match.context,
|
|
55
|
+
timestamp: match.timestamp
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const sessions = Array.from(sessionMap.values());
|
|
60
|
+
|
|
61
|
+
res.json({
|
|
62
|
+
keyword,
|
|
63
|
+
totalMatches: results.length,
|
|
64
|
+
sessions,
|
|
65
|
+
source: 'codex'
|
|
66
|
+
});
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error('[Codex API] Failed to search sessions:', err);
|
|
69
|
+
res.status(500).json({ error: err.message });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* GET /api/codex/sessions/recent/list?limit=10
|
|
75
|
+
* 获取最近会话
|
|
76
|
+
*/
|
|
77
|
+
router.get('/recent/list', (req, res) => {
|
|
78
|
+
try {
|
|
79
|
+
if (!isCodexInstalled()) {
|
|
80
|
+
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const limit = parseInt(req.query.limit) || 5;
|
|
84
|
+
const sessions = getRecentSessions(limit);
|
|
85
|
+
|
|
86
|
+
res.json({
|
|
87
|
+
sessions,
|
|
88
|
+
source: 'codex'
|
|
89
|
+
});
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error('[Codex API] Failed to get recent sessions:', err);
|
|
92
|
+
res.status(500).json({ error: err.message });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ============================================
|
|
97
|
+
// 参数路由
|
|
98
|
+
// ============================================
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* GET /api/codex/sessions/:projectName
|
|
102
|
+
* 获取项目的所有会话
|
|
103
|
+
*/
|
|
104
|
+
router.get('/:projectName', (req, res) => {
|
|
105
|
+
try {
|
|
106
|
+
if (!isCodexInstalled()) {
|
|
107
|
+
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const { projectName } = req.params;
|
|
111
|
+
const sessions = getSessionsByProject(projectName);
|
|
112
|
+
|
|
113
|
+
// 计算总大小
|
|
114
|
+
const totalSize = sessions.reduce((sum, session) => {
|
|
115
|
+
return sum + (session.size || 0);
|
|
116
|
+
}, 0);
|
|
117
|
+
|
|
118
|
+
// 获取别名
|
|
119
|
+
const aliases = loadAliases();
|
|
120
|
+
|
|
121
|
+
res.json({
|
|
122
|
+
sessions,
|
|
123
|
+
totalSize,
|
|
124
|
+
aliases, // 返回所有别名
|
|
125
|
+
projectInfo: {
|
|
126
|
+
name: projectName,
|
|
127
|
+
fullPath: projectName,
|
|
128
|
+
displayName: projectName
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.error('[Codex API] Failed to get sessions:', err);
|
|
133
|
+
res.status(500).json({ error: err.message });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* GET /api/codex/sessions/:projectName/search
|
|
139
|
+
* 项目级搜索
|
|
140
|
+
*/
|
|
141
|
+
router.get('/:projectName/search', (req, res) => {
|
|
142
|
+
try {
|
|
143
|
+
if (!isCodexInstalled()) {
|
|
144
|
+
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const { projectName } = req.params;
|
|
148
|
+
const { keyword, context } = req.query;
|
|
149
|
+
|
|
150
|
+
if (!keyword) {
|
|
151
|
+
return res.status(400).json({ error: 'Keyword is required' });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 使用全局搜索,然后过滤项目
|
|
155
|
+
const allResults = searchSessions(keyword);
|
|
156
|
+
const filteredResults = allResults.filter(r => r.projectName === projectName);
|
|
157
|
+
|
|
158
|
+
// 按会话分组
|
|
159
|
+
const sessionMap = new Map();
|
|
160
|
+
filteredResults.forEach(match => {
|
|
161
|
+
if (!sessionMap.has(match.sessionId)) {
|
|
162
|
+
sessionMap.set(match.sessionId, {
|
|
163
|
+
sessionId: match.sessionId,
|
|
164
|
+
projectName: match.projectName,
|
|
165
|
+
matchCount: 0,
|
|
166
|
+
matches: []
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
const session = sessionMap.get(match.sessionId);
|
|
170
|
+
session.matchCount++;
|
|
171
|
+
session.matches.push({
|
|
172
|
+
messageIndex: match.messageIndex,
|
|
173
|
+
role: match.role,
|
|
174
|
+
context: match.context,
|
|
175
|
+
timestamp: match.timestamp
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const sessions = Array.from(sessionMap.values());
|
|
180
|
+
|
|
181
|
+
res.json({
|
|
182
|
+
keyword,
|
|
183
|
+
totalMatches: filteredResults.length,
|
|
184
|
+
sessions
|
|
185
|
+
});
|
|
186
|
+
} catch (err) {
|
|
187
|
+
console.error('[Codex API] Failed to search project sessions:', err);
|
|
188
|
+
res.status(500).json({ error: err.message });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* GET /api/codex/sessions/:projectName/:sessionId/messages
|
|
194
|
+
* 获取会话的消息列表
|
|
195
|
+
*/
|
|
196
|
+
router.get('/:projectName/:sessionId/messages', (req, res) => {
|
|
197
|
+
try {
|
|
198
|
+
if (!isCodexInstalled()) {
|
|
199
|
+
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const { sessionId } = req.params;
|
|
203
|
+
const { page = 1, limit = 20, order = 'desc' } = req.query;
|
|
204
|
+
|
|
205
|
+
const session = getSessionById(sessionId);
|
|
206
|
+
|
|
207
|
+
if (!session) {
|
|
208
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 转换消息格式为前端期望的格式
|
|
212
|
+
const convertedMessages = [];
|
|
213
|
+
|
|
214
|
+
for (const msg of session.messages) {
|
|
215
|
+
// 用户消息
|
|
216
|
+
if (msg.role === 'user') {
|
|
217
|
+
convertedMessages.push({
|
|
218
|
+
type: 'user',
|
|
219
|
+
content: msg.content || '[空消息]',
|
|
220
|
+
timestamp: msg.timestamp,
|
|
221
|
+
model: null
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
// 助手消息(普通回复)
|
|
225
|
+
else if (msg.role === 'assistant') {
|
|
226
|
+
convertedMessages.push({
|
|
227
|
+
type: 'assistant',
|
|
228
|
+
content: msg.content || '[空消息]',
|
|
229
|
+
timestamp: msg.timestamp,
|
|
230
|
+
model: session.provider || 'codex'
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
// 推理内容
|
|
234
|
+
else if (msg.role === 'reasoning') {
|
|
235
|
+
convertedMessages.push({
|
|
236
|
+
type: 'assistant',
|
|
237
|
+
content: `**[推理]**\n${msg.content || '[空推理]'}`,
|
|
238
|
+
timestamp: msg.timestamp,
|
|
239
|
+
model: session.provider || 'codex'
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
// 工具调用
|
|
243
|
+
else if (msg.role === 'tool_call') {
|
|
244
|
+
const argsStr = typeof msg.arguments === 'object'
|
|
245
|
+
? JSON.stringify(msg.arguments, null, 2)
|
|
246
|
+
: msg.arguments;
|
|
247
|
+
convertedMessages.push({
|
|
248
|
+
type: 'assistant',
|
|
249
|
+
content: `**[调用工具: ${msg.name}]**\n\`\`\`json\n${argsStr}\n\`\`\``,
|
|
250
|
+
timestamp: msg.timestamp,
|
|
251
|
+
model: session.provider || 'codex'
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
// 工具输出
|
|
255
|
+
else if (msg.role === 'tool_output') {
|
|
256
|
+
let outputStr = '';
|
|
257
|
+
if (typeof msg.output === 'object' && msg.output.output) {
|
|
258
|
+
// 标准格式:{ output: '...', metadata: {...} }
|
|
259
|
+
outputStr = msg.output.output;
|
|
260
|
+
if (msg.output.metadata) {
|
|
261
|
+
const meta = msg.output.metadata;
|
|
262
|
+
outputStr += `\n\n**[元数据]**\n- 退出码: ${meta.exit_code || 0}\n- 耗时: ${meta.duration_seconds || 0}s`;
|
|
263
|
+
}
|
|
264
|
+
} else if (typeof msg.output === 'string') {
|
|
265
|
+
outputStr = msg.output;
|
|
266
|
+
} else {
|
|
267
|
+
outputStr = JSON.stringify(msg.output, null, 2);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
convertedMessages.push({
|
|
271
|
+
type: 'user',
|
|
272
|
+
content: `**[工具结果]**\n\`\`\`\n${outputStr}\n\`\`\``,
|
|
273
|
+
timestamp: msg.timestamp,
|
|
274
|
+
model: null
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 分页处理
|
|
280
|
+
const pageNum = parseInt(page);
|
|
281
|
+
const limitNum = parseInt(limit);
|
|
282
|
+
|
|
283
|
+
// 排序
|
|
284
|
+
let messages = convertedMessages;
|
|
285
|
+
if (order === 'desc') {
|
|
286
|
+
messages = [...messages].reverse();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 分页
|
|
290
|
+
const totalMessages = messages.length;
|
|
291
|
+
const start = (pageNum - 1) * limitNum;
|
|
292
|
+
const end = start + limitNum;
|
|
293
|
+
const paginatedMessages = messages.slice(start, end);
|
|
294
|
+
|
|
295
|
+
res.json({
|
|
296
|
+
messages: paginatedMessages,
|
|
297
|
+
metadata: {
|
|
298
|
+
gitBranch: session.gitBranch,
|
|
299
|
+
gitRepository: session.gitRepository,
|
|
300
|
+
cwd: session.cwd,
|
|
301
|
+
provider: session.provider
|
|
302
|
+
},
|
|
303
|
+
pagination: {
|
|
304
|
+
page: pageNum,
|
|
305
|
+
limit: limitNum,
|
|
306
|
+
total: totalMessages,
|
|
307
|
+
hasMore: end < totalMessages
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
} catch (err) {
|
|
311
|
+
console.error('[Codex API] Failed to get session messages:', err);
|
|
312
|
+
res.status(500).json({ error: err.message });
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* DELETE /api/codex/sessions/:projectName/:sessionId
|
|
318
|
+
* 删除会话
|
|
319
|
+
*/
|
|
320
|
+
router.delete('/:projectName/:sessionId', (req, res) => {
|
|
321
|
+
try {
|
|
322
|
+
if (!isCodexInstalled()) {
|
|
323
|
+
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const { sessionId } = req.params;
|
|
327
|
+
const result = deleteSession(sessionId);
|
|
328
|
+
|
|
329
|
+
res.json(result);
|
|
330
|
+
} catch (err) {
|
|
331
|
+
console.error('[Codex API] Failed to delete session:', err);
|
|
332
|
+
res.status(500).json({ error: err.message });
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* POST /api/codex/sessions/:projectName/:sessionId/fork
|
|
338
|
+
* Fork 一个会话
|
|
339
|
+
*/
|
|
340
|
+
router.post('/:projectName/:sessionId/fork', (req, res) => {
|
|
341
|
+
try {
|
|
342
|
+
if (!isCodexInstalled()) {
|
|
343
|
+
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const { sessionId } = req.params;
|
|
347
|
+
const result = forkSession(sessionId);
|
|
348
|
+
|
|
349
|
+
res.json(result);
|
|
350
|
+
} catch (err) {
|
|
351
|
+
console.error('[Codex API] Failed to fork session:', err);
|
|
352
|
+
res.status(500).json({ error: err.message });
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* POST /api/codex/sessions/:projectName/order
|
|
358
|
+
* 保存会话排序
|
|
359
|
+
*/
|
|
360
|
+
router.post('/:projectName/order', (req, res) => {
|
|
361
|
+
try {
|
|
362
|
+
if (!isCodexInstalled()) {
|
|
363
|
+
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const { projectName } = req.params;
|
|
367
|
+
const { order } = req.body;
|
|
368
|
+
|
|
369
|
+
if (!Array.isArray(order)) {
|
|
370
|
+
return res.status(400).json({ error: 'order must be an array' });
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
saveSessionOrder(projectName, order);
|
|
374
|
+
|
|
375
|
+
res.json({ success: true });
|
|
376
|
+
} catch (err) {
|
|
377
|
+
console.error('[Codex API] Failed to save session order:', err);
|
|
378
|
+
res.status(500).json({ error: err.message });
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* POST /api/codex/sessions/:projectName/:sessionId/launch
|
|
384
|
+
* 启动会话(打开终端)
|
|
385
|
+
*/
|
|
386
|
+
router.post('/:projectName/:sessionId/launch', (req, res) => {
|
|
387
|
+
try {
|
|
388
|
+
if (!isCodexInstalled()) {
|
|
389
|
+
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const { sessionId } = req.params;
|
|
393
|
+
const { exec } = require('child_process');
|
|
394
|
+
const fs = require('fs');
|
|
395
|
+
const path = require('path');
|
|
396
|
+
|
|
397
|
+
// 获取会话详情
|
|
398
|
+
const session = getSessionById(sessionId);
|
|
399
|
+
|
|
400
|
+
if (!session) {
|
|
401
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// 从会话文件提取 cwd
|
|
405
|
+
let cwd = null;
|
|
406
|
+
try {
|
|
407
|
+
if (session.filePath && fs.existsSync(session.filePath)) {
|
|
408
|
+
const content = fs.readFileSync(session.filePath, 'utf8');
|
|
409
|
+
const firstLine = content.split('\n')[0];
|
|
410
|
+
if (firstLine) {
|
|
411
|
+
const json = JSON.parse(firstLine);
|
|
412
|
+
if (json.type === 'session_meta' && json.payload?.cwd) {
|
|
413
|
+
cwd = json.payload.cwd;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
} catch (e) {
|
|
418
|
+
console.warn('Unable to extract cwd from Codex session:', e.message);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (!cwd) {
|
|
422
|
+
return res.status(400).json({
|
|
423
|
+
error: 'Unable to determine working directory from session'
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// 获取别名
|
|
428
|
+
const { loadAliases } = require('../services/alias');
|
|
429
|
+
const aliases = loadAliases();
|
|
430
|
+
const alias = aliases[sessionId];
|
|
431
|
+
|
|
432
|
+
// 广播行为日志
|
|
433
|
+
const { broadcastLog } = require('../websocket-server');
|
|
434
|
+
broadcastLog({
|
|
435
|
+
type: 'action',
|
|
436
|
+
action: 'launch_codex_session',
|
|
437
|
+
message: `启动 Codex 会话 ${alias || sessionId.substring(0, 8)}`,
|
|
438
|
+
sessionId: sessionId,
|
|
439
|
+
alias: alias || null,
|
|
440
|
+
timestamp: Date.now()
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// 使用配置的终端工具启动
|
|
444
|
+
const { getTerminalLaunchCommand } = require('../services/terminal-config');
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
// Windows 路径需要转换为反斜杠格式
|
|
448
|
+
const normalizedCwd = process.platform === 'win32' ? cwd.replace(/\//g, '\\') : cwd;
|
|
449
|
+
|
|
450
|
+
// 获取启动命令(使用 sessionId 作为占位符)
|
|
451
|
+
const { command, terminalId, terminalName } = getTerminalLaunchCommand(normalizedCwd, sessionId);
|
|
452
|
+
|
|
453
|
+
// 将命令中的 'claude -r' 替换为 'codex resume'
|
|
454
|
+
const codexCommand = command
|
|
455
|
+
.replace(/claude\s+-r\s+/g, 'codex resume ')
|
|
456
|
+
.replace(/claude\s+--resume\s+/g, 'codex resume ');
|
|
457
|
+
|
|
458
|
+
console.log(`[Codex] Launching terminal: ${terminalName} (${terminalId})`);
|
|
459
|
+
console.log(`[Codex] Command: ${codexCommand}`);
|
|
460
|
+
|
|
461
|
+
// 异步执行命令,不等待结果
|
|
462
|
+
const shellOption = process.platform === 'win32' ? { shell: 'cmd.exe' } : { shell: true };
|
|
463
|
+
exec(codexCommand, shellOption, (error, stdout, stderr) => {
|
|
464
|
+
if (error) {
|
|
465
|
+
console.error(`[Codex] Failed to launch terminal ${terminalName}:`, error.message);
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// 立即返回成功响应
|
|
470
|
+
res.json({
|
|
471
|
+
success: true,
|
|
472
|
+
cwd,
|
|
473
|
+
sessionFile: session.filePath,
|
|
474
|
+
terminal: terminalName,
|
|
475
|
+
terminalId,
|
|
476
|
+
sessionId
|
|
477
|
+
});
|
|
478
|
+
} catch (terminalError) {
|
|
479
|
+
console.error('[Codex] Failed to get terminal command:', terminalError);
|
|
480
|
+
return res.status(500).json({
|
|
481
|
+
error: '无法启动终端:' + terminalError.message
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
} catch (err) {
|
|
485
|
+
console.error('[Codex API] Failed to launch session:', err);
|
|
486
|
+
res.status(500).json({ error: err.message });
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
return router;
|
|
491
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const {
|
|
4
|
+
getStatistics,
|
|
5
|
+
getDailyStatistics,
|
|
6
|
+
getTodayStatistics
|
|
7
|
+
} = require('../services/codex-statistics-service');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 获取 Codex 总体统计数据
|
|
11
|
+
* GET /api/codex/statistics/summary
|
|
12
|
+
*/
|
|
13
|
+
router.get('/summary', (req, res) => {
|
|
14
|
+
try {
|
|
15
|
+
const stats = getStatistics();
|
|
16
|
+
res.json(stats);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error('[Codex] Failed to get statistics:', error);
|
|
19
|
+
res.status(500).json({ error: 'Failed to get statistics' });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 获取 Codex 今日统计数据
|
|
25
|
+
* GET /api/codex/statistics/today
|
|
26
|
+
*/
|
|
27
|
+
router.get('/today', (req, res) => {
|
|
28
|
+
try {
|
|
29
|
+
const stats = getTodayStatistics();
|
|
30
|
+
res.json(stats);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('[Codex] Failed to get today statistics:', error);
|
|
33
|
+
res.status(500).json({ error: 'Failed to get today statistics' });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 获取 Codex 指定日期的统计数据
|
|
39
|
+
* GET /api/codex/statistics/daily/:date
|
|
40
|
+
*/
|
|
41
|
+
router.get('/daily/:date', (req, res) => {
|
|
42
|
+
try {
|
|
43
|
+
const { date } = req.params;
|
|
44
|
+
|
|
45
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
46
|
+
return res.status(400).json({ error: 'Invalid date format. Expected YYYY-MM-DD' });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const stats = getDailyStatistics(date);
|
|
50
|
+
res.json(stats);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('[Codex] Failed to get daily statistics:', error);
|
|
53
|
+
res.status(500).json({ error: 'Failed to get daily statistics' });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
module.exports = router;
|