@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,665 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { getCodexDir } = require('./codex-config');
4
+ const { parseSession, parseSessionMeta, extractSessionMeta, readJSONL } = require('./codex-parser');
5
+
6
+ /**
7
+ * 获取会话目录
8
+ */
9
+ function getSessionsDir() {
10
+ return path.join(getCodexDir(), 'sessions');
11
+ }
12
+
13
+ /**
14
+ * 递归扫描目录查找所有会话文件
15
+ * @param {string} dir - 目录路径
16
+ * @returns {Array} 会话文件路径数组
17
+ */
18
+ function scanDirectoryRecursive(dir) {
19
+ const results = [];
20
+
21
+ if (!fs.existsSync(dir)) {
22
+ return results;
23
+ }
24
+
25
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
26
+
27
+ for (const entry of entries) {
28
+ const fullPath = path.join(dir, entry.name);
29
+
30
+ if (entry.isDirectory()) {
31
+ // 递归扫描子目录
32
+ results.push(...scanDirectoryRecursive(fullPath));
33
+ } else if (entry.isFile() && entry.name.match(/^rollout-.*\.jsonl$/)) {
34
+ // 匹配会话文件
35
+ results.push(fullPath);
36
+ }
37
+ }
38
+
39
+ return results;
40
+ }
41
+
42
+ /**
43
+ * 扫描所有会话文件
44
+ * @returns {Array} 会话文件路径数组
45
+ */
46
+ function scanSessionFiles() {
47
+ const sessionsDir = getSessionsDir();
48
+ const files = scanDirectoryRecursive(sessionsDir);
49
+
50
+ return files.map(filePath => {
51
+ const filename = path.basename(filePath);
52
+ // Codex 文件名格式:rollout-YYYY-MM-DDTHH-MM-SS-uuid.jsonl
53
+ // 时间戳:19个字符(2025-11-22T12-34-56)
54
+ const match = filename.match(/rollout-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2})-([\w-]+)\.jsonl/);
55
+
56
+ if (!match) return null;
57
+
58
+ return {
59
+ filePath,
60
+ timestamp: match[1],
61
+ sessionId: match[2],
62
+ date: match[1].split('T')[0]
63
+ };
64
+ }).filter(Boolean);
65
+ }
66
+
67
+ /**
68
+ * 获取所有会话(轻量级,仅元数据)
69
+ * @returns {Array} 会话对象数组
70
+ */
71
+ function getAllSessions() {
72
+ const files = scanSessionFiles();
73
+
74
+ return files.map(file => {
75
+ // 使用轻量级解析,只获取元数据
76
+ const session = parseSessionMeta(file.filePath);
77
+
78
+ if (!session) return null;
79
+
80
+ return {
81
+ ...session,
82
+ sessionId: file.sessionId,
83
+ date: file.date
84
+ };
85
+ }).filter(Boolean);
86
+ }
87
+
88
+ /**
89
+ * 归一化会话数据为 Claude Code 格式
90
+ * @param {Object} codexSession - Codex 会话对象
91
+ * @returns {Object} 归一化后的会话对象
92
+ */
93
+ function normalizeSession(codexSession) {
94
+ const { meta, sessionId, preview, filePath } = codexSession;
95
+
96
+ // 获取文件大小和修改时间
97
+ let size = 0;
98
+ let mtime = meta.timestamp;
99
+ try {
100
+ if (filePath && fs.existsSync(filePath)) {
101
+ const stats = fs.statSync(filePath);
102
+ size = stats.size;
103
+ mtime = stats.mtime.toISOString();
104
+ }
105
+ } catch (err) {
106
+ // 忽略错误
107
+ }
108
+
109
+ return {
110
+ sessionId,
111
+ mtime,
112
+ size,
113
+ filePath: filePath || '',
114
+ gitBranch: meta.git?.branch || null,
115
+ firstMessage: preview || null,
116
+ forkedFrom: null, // Codex 不支持 fork
117
+
118
+ // 额外的 Codex 特有字段(前端可能需要)
119
+ source: 'codex'
120
+ };
121
+ }
122
+
123
+ /**
124
+ * 聚合项目列表
125
+ * @returns {Array} 项目对象数组
126
+ */
127
+ function getProjects() {
128
+ const sessions = getAllSessions();
129
+ const projectMap = new Map();
130
+
131
+ sessions.forEach(session => {
132
+ const meta = session.meta;
133
+
134
+ // 优先使用 Git 仓库名,否则使用 cwd 的最后一级目录
135
+ let projectName;
136
+ let projectPath = meta.cwd;
137
+
138
+ if (meta.git?.repositoryUrl) {
139
+ // 从 Git URL 提取项目名
140
+ const repoUrl = meta.git.repositoryUrl;
141
+ projectName = repoUrl.split('/').pop().replace('.git', '');
142
+ } else {
143
+ // 使用目录名
144
+ projectName = path.basename(meta.cwd);
145
+ }
146
+
147
+ if (!projectMap.has(projectName)) {
148
+ projectMap.set(projectName, {
149
+ name: projectName,
150
+ displayName: projectName,
151
+ fullPath: projectPath,
152
+ path: projectPath,
153
+ gitRepo: meta.git?.repositoryUrl,
154
+ branch: meta.git?.branch,
155
+ sessions: [],
156
+ sessionCount: 0,
157
+ lastUsed: null,
158
+ source: 'codex'
159
+ });
160
+ }
161
+
162
+ const project = projectMap.get(projectName);
163
+ project.sessions.push(session);
164
+ project.sessionCount++;
165
+
166
+ // 更新最后活动时间
167
+ const sessionTime = new Date(session.meta.timestamp).getTime();
168
+ if (!project.lastUsed || sessionTime > project.lastUsed) {
169
+ project.lastUsed = sessionTime;
170
+ }
171
+ });
172
+
173
+ // 获取保存的排序
174
+ const savedOrder = getProjectOrder();
175
+ const projects = Array.from(projectMap.values());
176
+
177
+ // 应用保存的排序
178
+ if (savedOrder.length > 0) {
179
+ const ordered = [];
180
+ const projectsMap = new Map(projects.map(p => [p.name, p]));
181
+
182
+ // 按保存的顺序添加项目
183
+ for (const projectName of savedOrder) {
184
+ if (projectsMap.has(projectName)) {
185
+ ordered.push(projectsMap.get(projectName));
186
+ projectsMap.delete(projectName);
187
+ }
188
+ }
189
+
190
+ // 添加剩余的新项目(不在保存顺序中的)
191
+ ordered.push(...projectsMap.values());
192
+ return ordered;
193
+ }
194
+
195
+ // 默认按最后活动时间排序
196
+ return projects.sort((a, b) => b.lastUsed - a.lastUsed);
197
+ }
198
+
199
+ /**
200
+ * 根据项目名获取会话列表(归一化格式)
201
+ * @param {string} projectName - 项目名称
202
+ * @returns {Array} 归一化的会话数组
203
+ */
204
+ function getSessionsByProject(projectName) {
205
+ const sessions = getAllSessions();
206
+
207
+ // 获取 fork 关系
208
+ const { getForkRelations } = require('./sessions');
209
+ const forkRelations = getForkRelations();
210
+
211
+ // 获取保存的排序
212
+ const savedOrder = getSessionOrder(projectName);
213
+
214
+ // 过滤并归一化会话
215
+ const filteredSessions = sessions
216
+ .filter(session => {
217
+ // 根据 Git 仓库名或目录名匹配
218
+ let sessionProjectName;
219
+ if (session.meta.git?.repositoryUrl) {
220
+ sessionProjectName = session.meta.git.repositoryUrl.split('/').pop().replace('.git', '');
221
+ } else {
222
+ sessionProjectName = path.basename(session.meta.cwd);
223
+ }
224
+ return sessionProjectName === projectName;
225
+ })
226
+ .map(session => {
227
+ const normalized = normalizeSession(session);
228
+ // 添加 fork 关系
229
+ normalized.forkedFrom = forkRelations[normalized.sessionId] || null;
230
+ return normalized;
231
+ });
232
+
233
+ // 应用保存的排序
234
+ let orderedSessions = filteredSessions;
235
+ if (savedOrder.length > 0) {
236
+ const ordered = [];
237
+ const sessionMap = new Map(filteredSessions.map(s => [s.sessionId, s]));
238
+
239
+ // 提取新会话(不在保存顺序中的),按时间倒序排列
240
+ const newSessions = [];
241
+ for (const sessionId of savedOrder) {
242
+ if (sessionMap.has(sessionId)) {
243
+ sessionMap.delete(sessionId);
244
+ }
245
+ }
246
+ newSessions.push(...sessionMap.values());
247
+ newSessions.sort((a, b) => {
248
+ return new Date(b.mtime).getTime() - new Date(a.mtime).getTime();
249
+ });
250
+
251
+ // 新会话在前,旧会话在后(按保存顺序)
252
+ ordered.push(...newSessions);
253
+
254
+ for (const sessionId of savedOrder) {
255
+ const session = filteredSessions.find(s => s.sessionId === sessionId);
256
+ if (session) {
257
+ ordered.push(session);
258
+ }
259
+ }
260
+
261
+ orderedSessions = ordered;
262
+ } else {
263
+ // 默认按时间倒序
264
+ orderedSessions.sort((a, b) => {
265
+ return new Date(b.mtime).getTime() - new Date(a.mtime).getTime();
266
+ });
267
+ }
268
+
269
+ return orderedSessions;
270
+ }
271
+
272
+ /**
273
+ * 根据 sessionId 获取会话(归一化格式)
274
+ * @param {string} sessionId - 会话 ID
275
+ * @returns {Object|null} 归一化的会话对象
276
+ */
277
+ function getSessionById(sessionId) {
278
+ const files = scanSessionFiles();
279
+ const file = files.find(f => f.sessionId === sessionId);
280
+
281
+ if (!file) {
282
+ return null;
283
+ }
284
+
285
+ const session = parseSession(file.filePath);
286
+ if (!session) {
287
+ return null;
288
+ }
289
+
290
+ return {
291
+ ...normalizeSession(session),
292
+ messages: session.messages, // 包含完整消息
293
+ filePath: file.filePath
294
+ };
295
+ }
296
+
297
+ /**
298
+ * 搜索会话(全局)
299
+ * @param {string} keyword - 搜索关键词
300
+ * @returns {Array} 搜索结果
301
+ */
302
+ function searchSessions(keyword) {
303
+ const files = scanSessionFiles();
304
+ const results = [];
305
+
306
+ files.forEach(file => {
307
+ // 使用完整解析获取消息内容
308
+ const session = parseSession(file.filePath);
309
+
310
+ if (!session || !session.messages || !Array.isArray(session.messages)) {
311
+ return;
312
+ }
313
+
314
+ session.messages.forEach((message, index) => {
315
+ if (message.role !== 'user' && message.role !== 'assistant') {
316
+ return;
317
+ }
318
+
319
+ const content = (message.content || '').toLowerCase();
320
+ const keywordLower = keyword.toLowerCase();
321
+
322
+ if (content.includes(keywordLower)) {
323
+ // 提取上下文
324
+ const startIndex = Math.max(0, content.indexOf(keywordLower) - 50);
325
+ const endIndex = Math.min(content.length, content.indexOf(keywordLower) + keyword.length + 50);
326
+ const context = content.substring(startIndex, endIndex);
327
+
328
+ // 确定项目名
329
+ let projectName;
330
+ if (session.meta?.git?.repositoryUrl) {
331
+ projectName = session.meta.git.repositoryUrl.split('/').pop().replace('.git', '');
332
+ } else if (session.meta?.cwd) {
333
+ projectName = path.basename(session.meta.cwd);
334
+ } else {
335
+ projectName = 'Unknown';
336
+ }
337
+
338
+ results.push({
339
+ sessionId: file.sessionId,
340
+ projectName,
341
+ messageIndex: index,
342
+ role: message.role,
343
+ context: (startIndex > 0 ? '...' : '') + context + (endIndex < content.length ? '...' : ''),
344
+ timestamp: message.timestamp,
345
+ source: 'codex'
346
+ });
347
+ }
348
+ });
349
+ });
350
+
351
+ return results;
352
+ }
353
+
354
+ /**
355
+ * 删除项目(删除项目下所有会话)
356
+ * @param {string} projectName - 项目名称
357
+ * @returns {Object} 删除结果 { success: true, deletedCount: number }
358
+ */
359
+ function deleteProject(projectName) {
360
+ const sessions = getAllSessions();
361
+
362
+ // 找到该项目下的所有会话
363
+ const projectSessions = sessions.filter(session => {
364
+ let sessionProjectName;
365
+ if (session.meta.git?.repositoryUrl) {
366
+ sessionProjectName = session.meta.git.repositoryUrl.split('/').pop().replace('.git', '');
367
+ } else {
368
+ sessionProjectName = path.basename(session.meta.cwd);
369
+ }
370
+ return sessionProjectName === projectName;
371
+ });
372
+
373
+ if (projectSessions.length === 0) {
374
+ throw new Error('Project not found or has no sessions');
375
+ }
376
+
377
+ // 删除所有会话文件
378
+ let deletedCount = 0;
379
+ const { getForkRelations, saveForkRelations } = require('./sessions');
380
+ const { deleteAlias } = require('./alias');
381
+ const forkRelations = getForkRelations();
382
+ let forkRelationsModified = false;
383
+
384
+ projectSessions.forEach(session => {
385
+ try {
386
+ // 删除会话文件
387
+ if (fs.existsSync(session.filePath)) {
388
+ fs.unlinkSync(session.filePath);
389
+ deletedCount++;
390
+ }
391
+
392
+ // 清理 fork 关系
393
+ if (forkRelations[session.sessionId]) {
394
+ delete forkRelations[session.sessionId];
395
+ forkRelationsModified = true;
396
+ }
397
+
398
+ // 清理指向该会话的 fork 关系
399
+ Object.keys(forkRelations).forEach(key => {
400
+ if (forkRelations[key] === session.sessionId) {
401
+ delete forkRelations[key];
402
+ forkRelationsModified = true;
403
+ }
404
+ });
405
+
406
+ // 清理别名
407
+ try {
408
+ deleteAlias(session.sessionId);
409
+ } catch (err) {
410
+ // 忽略别名不存在的错误
411
+ }
412
+ } catch (err) {
413
+ console.error(`[Codex] Failed to delete session ${session.sessionId}:`, err.message);
414
+ }
415
+ });
416
+
417
+ // 保存清理后的 fork 关系
418
+ if (forkRelationsModified) {
419
+ saveForkRelations(forkRelations);
420
+ }
421
+
422
+ // 清理项目排序配置
423
+ try {
424
+ const currentOrder = getProjectOrder();
425
+ const newOrder = currentOrder.filter(name => name !== projectName);
426
+ if (newOrder.length !== currentOrder.length) {
427
+ saveProjectOrder(newOrder);
428
+ }
429
+ } catch (err) {
430
+ console.error('[Codex] Failed to clean project order:', err.message);
431
+ }
432
+
433
+ // 清理会话排序配置
434
+ try {
435
+ saveSessionOrder(projectName, []);
436
+ } catch (err) {
437
+ console.error('[Codex] Failed to clean session order:', err.message);
438
+ }
439
+
440
+ return { success: true, deletedCount };
441
+ }
442
+
443
+ /**
444
+ * 获取最近的会话(跨项目)
445
+ * @param {number} limit - 返回数量限制,默认 5
446
+ * @returns {Array} 最近会话数组
447
+ */
448
+ function getRecentSessions(limit = 5) {
449
+ const sessions = getAllSessions();
450
+
451
+ // 获取 fork 关系和别名
452
+ const { getForkRelations } = require('./sessions');
453
+ const { loadAliases } = require('./alias');
454
+ const forkRelations = getForkRelations();
455
+ const aliases = loadAliases();
456
+
457
+ // 归一化所有会话
458
+ const allNormalizedSessions = sessions.map(session => {
459
+ const normalized = normalizeSession(session);
460
+
461
+ // 添加项目信息
462
+ let projectName;
463
+ let projectPath = session.meta.cwd;
464
+
465
+ if (session.meta.git?.repositoryUrl) {
466
+ projectName = session.meta.git.repositoryUrl.split('/').pop().replace('.git', '');
467
+ } else {
468
+ projectName = path.basename(session.meta.cwd);
469
+ }
470
+
471
+ return {
472
+ ...normalized,
473
+ forkedFrom: forkRelations[normalized.sessionId] || null,
474
+ alias: aliases[normalized.sessionId] || null,
475
+ projectName: projectName,
476
+ projectDisplayName: projectName,
477
+ projectFullPath: projectPath
478
+ };
479
+ });
480
+
481
+ // 按 mtime 倒序排序,取前 N 个
482
+ return allNormalizedSessions
483
+ .sort((a, b) => new Date(b.mtime).getTime() - new Date(a.mtime).getTime())
484
+ .slice(0, limit);
485
+ }
486
+
487
+ /**
488
+ * 删除一个会话
489
+ * @param {string} sessionId - 会话 ID
490
+ * @returns {Object} 删除结果 { success: true }
491
+ */
492
+ function deleteSession(sessionId) {
493
+ const files = scanSessionFiles();
494
+ const targetFile = files.find(f => f.sessionId === sessionId);
495
+
496
+ if (!targetFile) {
497
+ throw new Error('Session not found');
498
+ }
499
+
500
+ // 删除会话文件
501
+ fs.unlinkSync(targetFile.filePath);
502
+
503
+ // 清理 fork 关系
504
+ const { getForkRelations, saveForkRelations } = require('./sessions');
505
+ const forkRelations = getForkRelations();
506
+
507
+ // 删除作为源的 fork 关系
508
+ delete forkRelations[sessionId];
509
+
510
+ // 删除所有指向该会话的 fork 关系
511
+ Object.keys(forkRelations).forEach(key => {
512
+ if (forkRelations[key] === sessionId) {
513
+ delete forkRelations[key];
514
+ }
515
+ });
516
+
517
+ saveForkRelations(forkRelations);
518
+
519
+ // 清理别名
520
+ const { deleteAlias } = require('./alias');
521
+ try {
522
+ deleteAlias(sessionId);
523
+ } catch (err) {
524
+ // 忽略别名不存在的错误
525
+ }
526
+
527
+ return { success: true };
528
+ }
529
+
530
+ /**
531
+ * Fork 一个会话(创建副本)
532
+ * @param {string} sessionId - 原会话 ID
533
+ * @returns {Object} Fork 结果 { newSessionId, forkedFrom }
534
+ */
535
+ function forkSession(sessionId) {
536
+ const files = scanSessionFiles();
537
+ const sourceFile = files.find(f => f.sessionId === sessionId);
538
+
539
+ if (!sourceFile) {
540
+ throw new Error('Session not found');
541
+ }
542
+
543
+ // 读取原会话文件内容
544
+ const content = fs.readFileSync(sourceFile.filePath, 'utf8');
545
+
546
+ // 生成新的 session ID (使用 crypto.randomUUID 生成 v4 UUID)
547
+ const crypto = require('crypto');
548
+ const newSessionId = crypto.randomUUID();
549
+
550
+ // 生成新的时间戳(Codex 格式:YYYY-MM-DDTHH-MM-SS)
551
+ const now = new Date();
552
+ const timestamp = now.toISOString()
553
+ .replace(/\.\d{3}Z$/, '') // 移除毫秒和 Z
554
+ .replace(/:/g, '-'); // 将冒号替换为破折号
555
+
556
+ // 生成新文件路径(按当前日期组织)
557
+ const year = now.getFullYear();
558
+ const month = String(now.getMonth() + 1).padStart(2, '0');
559
+ const day = String(now.getDate()).padStart(2, '0');
560
+
561
+ const targetDir = path.join(getSessionsDir(), String(year), month, day);
562
+
563
+ // 确保目标目录存在
564
+ if (!fs.existsSync(targetDir)) {
565
+ fs.mkdirSync(targetDir, { recursive: true });
566
+ }
567
+
568
+ const newFileName = `rollout-${timestamp}-${newSessionId}.jsonl`;
569
+ const newFilePath = path.join(targetDir, newFileName);
570
+
571
+ // 写入新文件
572
+ fs.writeFileSync(newFilePath, content, 'utf8');
573
+
574
+ // 保存 fork 关系(复用 Claude Code 的 fork 关系存储)
575
+ const { getForkRelations, saveForkRelations } = require('./sessions');
576
+ const forkRelations = getForkRelations();
577
+ forkRelations[newSessionId] = sessionId;
578
+ saveForkRelations(forkRelations);
579
+
580
+ return {
581
+ newSessionId,
582
+ forkedFrom: sessionId,
583
+ newFilePath
584
+ };
585
+ }
586
+
587
+ /**
588
+ * 获取会话排序(按项目)
589
+ * @param {string} projectName - 项目名称
590
+ * @returns {Array} 会话 ID 数组
591
+ */
592
+ function getSessionOrder(projectName) {
593
+ const { getSessionOrder: getClaudeSessionOrder } = require('./sessions');
594
+ // 复用 Claude Code 的排序存储,使用 "codex-" 前缀区分
595
+ return getClaudeSessionOrder(`codex-${projectName}`);
596
+ }
597
+
598
+ /**
599
+ * 保存会话排序
600
+ * @param {string} projectName - 项目名称
601
+ * @param {Array} order - 会话 ID 数组
602
+ */
603
+ function saveSessionOrder(projectName, order) {
604
+ const { saveSessionOrder: saveClaudeSessionOrder } = require('./sessions');
605
+ // 复用 Claude Code 的排序存储,使用 "codex-" 前缀区分
606
+ saveClaudeSessionOrder(`codex-${projectName}`, order);
607
+ }
608
+
609
+ /**
610
+ * 获取项目排序
611
+ * @returns {Array} 项目名称数组
612
+ */
613
+ function getProjectOrder() {
614
+ const { getProjectOrder: getClaudeProjectOrder } = require('./sessions');
615
+ const { getCodexDir } = require('./codex-config');
616
+ // 复用 Claude Code 的排序存储,使用特殊的配置对象标识 Codex
617
+ return getClaudeProjectOrder({ projectsDir: getCodexDir() });
618
+ }
619
+
620
+ /**
621
+ * 保存项目排序
622
+ * @param {Array} order - 项目名称数组
623
+ */
624
+ function saveProjectOrder(order) {
625
+ const { saveProjectOrder: saveClaudeProjectOrder } = require('./sessions');
626
+ const { getCodexDir } = require('./codex-config');
627
+ // 复用 Claude Code 的排序存储
628
+ saveClaudeProjectOrder({ projectsDir: getCodexDir() }, order);
629
+ }
630
+
631
+ /**
632
+ * 获取 Codex 项目与会话数量(用于仪表盘轻量统计)
633
+ */
634
+ function getProjectAndSessionCounts() {
635
+ try {
636
+ const projects = getProjects();
637
+ const sessions = scanSessionFiles();
638
+ return {
639
+ projectCount: projects.length,
640
+ sessionCount: sessions.length
641
+ };
642
+ } catch (err) {
643
+ return { projectCount: 0, sessionCount: 0 };
644
+ }
645
+ }
646
+
647
+ module.exports = {
648
+ getSessionsDir,
649
+ scanSessionFiles,
650
+ getAllSessions,
651
+ getProjects,
652
+ getSessionsByProject,
653
+ getSessionById,
654
+ searchSessions,
655
+ normalizeSession,
656
+ forkSession,
657
+ deleteSession,
658
+ deleteProject,
659
+ getRecentSessions,
660
+ getSessionOrder,
661
+ saveSessionOrder,
662
+ getProjectOrder,
663
+ saveProjectOrder,
664
+ getProjectAndSessionCounts
665
+ };