@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,577 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const crypto = require('crypto');
5
+ const { getSessionById: getClaudeSessionById } = require('./sessions');
6
+ const { getSessionById: getCodexSessionById } = require('./codex-sessions');
7
+ const { getSessionById: getGeminiSessionById, getProjectPath } = require('./gemini-sessions');
8
+ const { readJSONL, parseSession: parseCodexFull } = require('./codex-parser');
9
+
10
+ /**
11
+ * 统一中间格式
12
+ * @typedef {Object} UnifiedSession
13
+ * @property {string} sessionId - 会话 ID
14
+ * @property {string} cwd - 工作目录
15
+ * @property {string|null} gitBranch - Git 分支
16
+ * @property {string} startTime - 开始时间(ISO 格式)
17
+ * @property {Array<UnifiedMessage>} messages - 消息列表
18
+ * @property {Object} metadata - 元数据
19
+ */
20
+
21
+ /**
22
+ * 统一消息格式
23
+ * @typedef {Object} UnifiedMessage
24
+ * @property {string} role - 角色 (user|assistant|system)
25
+ * @property {string} content - 内容
26
+ * @property {string} timestamp - 时间戳(ISO 格式)
27
+ */
28
+
29
+ /**
30
+ * 将 Claude Code 会话解析为统一格式
31
+ * @param {string} filePath - Claude 会话文件路径
32
+ * @returns {UnifiedSession}
33
+ */
34
+ function parseClaudeToUnified(filePath) {
35
+ const content = fs.readFileSync(filePath, 'utf8');
36
+ const lines = content.split('\n').filter(line => line.trim());
37
+
38
+ const messages = [];
39
+ let cwd = null;
40
+ let gitBranch = null;
41
+ let sessionId = null;
42
+ let startTime = null;
43
+
44
+ for (const line of lines) {
45
+ try {
46
+ const entry = JSON.parse(line);
47
+
48
+ // 提取元数据
49
+ if (entry.cwd && !cwd) {
50
+ cwd = entry.cwd;
51
+ }
52
+ if (entry.gitBranch && !gitBranch) {
53
+ gitBranch = entry.gitBranch;
54
+ }
55
+ if (entry.sessionId && !sessionId) {
56
+ sessionId = entry.sessionId;
57
+ }
58
+
59
+ // 解析消息
60
+ if (entry.type === 'user' && entry.message) {
61
+ messages.push({
62
+ role: 'user',
63
+ content: entry.message.content || '',
64
+ timestamp: entry.timestamp || new Date().toISOString()
65
+ });
66
+ if (!startTime) {
67
+ startTime = entry.timestamp;
68
+ }
69
+ } else if (entry.type === 'assistant' && entry.message) {
70
+ messages.push({
71
+ role: 'assistant',
72
+ content: entry.message.content || '',
73
+ timestamp: entry.timestamp || new Date().toISOString()
74
+ });
75
+ } else if (entry.type === 'summary') {
76
+ messages.push({
77
+ role: 'system',
78
+ content: entry.summary || '',
79
+ timestamp: entry.timestamp || new Date().toISOString()
80
+ });
81
+ }
82
+ // 忽略 file-history-snapshot 等其他类型
83
+ } catch (err) {
84
+ // 跳过无效行
85
+ }
86
+ }
87
+
88
+ return {
89
+ sessionId: sessionId || crypto.randomUUID(),
90
+ cwd: cwd || process.cwd(),
91
+ gitBranch,
92
+ startTime: startTime || new Date().toISOString(),
93
+ messages,
94
+ metadata: {
95
+ source: 'claude',
96
+ originalPath: filePath
97
+ }
98
+ };
99
+ }
100
+
101
+ /**
102
+ * 将 Codex 会话解析为统一格式
103
+ * @param {string} filePath - Codex 会话文件路径
104
+ * @returns {UnifiedSession}
105
+ */
106
+ function parseCodexToUnified(filePath) {
107
+ const session = parseCodexFull(filePath);
108
+
109
+ if (!session) {
110
+ throw new Error('Failed to parse Codex session');
111
+ }
112
+
113
+ const messages = [];
114
+ let sessionId = null;
115
+ let cwd = null;
116
+ let gitBranch = null;
117
+ let startTime = null;
118
+
119
+ // 从元数据提取信息
120
+ if (session.meta) {
121
+ sessionId = session.meta.sessionId || session.sessionId;
122
+ cwd = session.meta.cwd;
123
+ gitBranch = session.meta.git?.branch || null;
124
+ startTime = session.meta.timestamp;
125
+ }
126
+
127
+ // 转换消息
128
+ if (session.messages && Array.isArray(session.messages)) {
129
+ session.messages.forEach(msg => {
130
+ if (msg.role === 'user' || msg.role === 'assistant') {
131
+ messages.push({
132
+ role: msg.role,
133
+ content: msg.content || '',
134
+ timestamp: msg.timestamp || new Date().toISOString()
135
+ });
136
+ }
137
+ });
138
+ }
139
+
140
+ return {
141
+ sessionId: sessionId || crypto.randomUUID(),
142
+ cwd: cwd || process.cwd(),
143
+ gitBranch,
144
+ startTime: startTime || new Date().toISOString(),
145
+ messages,
146
+ metadata: {
147
+ source: 'codex',
148
+ originalPath: filePath,
149
+ git: session.meta?.git
150
+ }
151
+ };
152
+ }
153
+
154
+ /**
155
+ * 将 Gemini 会话解析为统一格式
156
+ * @param {string} filePath - Gemini 会话文件路径
157
+ * @returns {UnifiedSession}
158
+ */
159
+ function parseGeminiToUnified(filePath) {
160
+ const content = fs.readFileSync(filePath, 'utf8');
161
+ const session = JSON.parse(content);
162
+
163
+ const messages = [];
164
+
165
+ // 转换消息
166
+ if (session.messages && Array.isArray(session.messages)) {
167
+ session.messages.forEach(msg => {
168
+ if (msg.type === 'user') {
169
+ messages.push({
170
+ role: 'user',
171
+ content: msg.content || '',
172
+ timestamp: msg.timestamp || new Date().toISOString()
173
+ });
174
+ } else if (msg.type === 'assistant' || msg.type === 'gemini') {
175
+ messages.push({
176
+ role: 'assistant',
177
+ content: msg.content || '',
178
+ timestamp: msg.timestamp || new Date().toISOString()
179
+ });
180
+ } else if (msg.type === 'info') {
181
+ messages.push({
182
+ role: 'system',
183
+ content: msg.content || '',
184
+ timestamp: msg.timestamp || new Date().toISOString()
185
+ });
186
+ }
187
+ });
188
+ }
189
+
190
+ // 尝试从 projectHash 获取实际路径
191
+ const projectPath = getProjectPath(session.projectHash);
192
+
193
+ return {
194
+ sessionId: session.sessionId || crypto.randomUUID(),
195
+ cwd: projectPath || os.homedir(),
196
+ gitBranch: null, // Gemini 不记录 git branch
197
+ startTime: session.startTime || new Date().toISOString(),
198
+ messages,
199
+ metadata: {
200
+ source: 'gemini',
201
+ originalPath: filePath,
202
+ projectHash: session.projectHash,
203
+ model: session.messages?.[0]?.model
204
+ }
205
+ };
206
+ }
207
+
208
+ /**
209
+ * 从统一格式生成 Claude Code 会话
210
+ * @param {UnifiedSession} unified - 统一格式会话
211
+ * @param {string} targetPath - 目标文件路径
212
+ * @returns {string} 生成的文件路径
213
+ */
214
+ function generateClaudeFromUnified(unified, targetPath) {
215
+ const lines = [];
216
+ const version = '1.0.24'; // Claude Code 版本
217
+
218
+ // 为每条消息生成 JSONL 行
219
+ unified.messages.forEach(msg => {
220
+ if (msg.role === 'user') {
221
+ lines.push(JSON.stringify({
222
+ type: 'user',
223
+ message: {
224
+ role: 'user',
225
+ content: msg.content
226
+ },
227
+ cwd: unified.cwd,
228
+ sessionId: unified.sessionId,
229
+ gitBranch: unified.gitBranch,
230
+ version,
231
+ timestamp: msg.timestamp
232
+ }));
233
+ } else if (msg.role === 'assistant') {
234
+ lines.push(JSON.stringify({
235
+ type: 'assistant',
236
+ message: {
237
+ role: 'assistant',
238
+ content: msg.content
239
+ },
240
+ sessionId: unified.sessionId,
241
+ timestamp: msg.timestamp
242
+ }));
243
+ } else if (msg.role === 'system') {
244
+ lines.push(JSON.stringify({
245
+ type: 'summary',
246
+ summary: msg.content,
247
+ timestamp: msg.timestamp
248
+ }));
249
+ }
250
+ });
251
+
252
+ // 确保目标目录存在
253
+ const dir = path.dirname(targetPath);
254
+ if (!fs.existsSync(dir)) {
255
+ fs.mkdirSync(dir, { recursive: true });
256
+ }
257
+
258
+ // 写入文件
259
+ fs.writeFileSync(targetPath, lines.join('\n') + '\n', 'utf8');
260
+
261
+ return targetPath;
262
+ }
263
+
264
+ /**
265
+ * 从统一格式生成 Codex 会话
266
+ * @param {UnifiedSession} unified - 统一格式会话
267
+ * @param {string} targetPath - 目标文件路径
268
+ * @returns {string} 生成的文件路径
269
+ */
270
+ function generateCodexFromUnified(unified, targetPath) {
271
+ const lines = [];
272
+
273
+ // 生成 session_start 事件
274
+ lines.push(JSON.stringify({
275
+ type: 'event',
276
+ event: {
277
+ type: 'session_start',
278
+ session_id: unified.sessionId,
279
+ cwd: unified.cwd,
280
+ git: unified.gitBranch ? {
281
+ branch: unified.gitBranch,
282
+ repositoryUrl: unified.metadata.git?.repositoryUrl || null
283
+ } : null
284
+ },
285
+ timestamp: unified.startTime
286
+ }));
287
+
288
+ // 生成消息
289
+ unified.messages.forEach(msg => {
290
+ if (msg.role === 'user' || msg.role === 'assistant') {
291
+ lines.push(JSON.stringify({
292
+ type: 'message',
293
+ message: {
294
+ role: msg.role,
295
+ content: msg.content
296
+ },
297
+ timestamp: msg.timestamp
298
+ }));
299
+ }
300
+ // 忽略 system 消息(Codex 不支持)
301
+ });
302
+
303
+ // 确保目标目录存在
304
+ const dir = path.dirname(targetPath);
305
+ if (!fs.existsSync(dir)) {
306
+ fs.mkdirSync(dir, { recursive: true });
307
+ }
308
+
309
+ // 写入文件
310
+ fs.writeFileSync(targetPath, lines.join('\n') + '\n', 'utf8');
311
+
312
+ return targetPath;
313
+ }
314
+
315
+ /**
316
+ * 从统一格式生成 Gemini 会话
317
+ * @param {UnifiedSession} unified - 统一格式会话
318
+ * @param {string} targetPath - 目标文件路径
319
+ * @returns {string} 生成的文件路径
320
+ */
321
+ function generateGeminiFromUnified(unified, targetPath) {
322
+ // 计算 projectHash(如果有 cwd)
323
+ let projectHash;
324
+ if (unified.metadata.projectHash) {
325
+ projectHash = unified.metadata.projectHash;
326
+ } else {
327
+ projectHash = crypto.createHash('sha256').update(unified.cwd).digest('hex');
328
+ }
329
+
330
+ // 转换消息
331
+ const messages = unified.messages.map(msg => {
332
+ const geminiMsg = {
333
+ type: msg.role === 'user' ? 'user' : msg.role === 'assistant' ? 'assistant' : 'info',
334
+ content: msg.content,
335
+ timestamp: msg.timestamp
336
+ };
337
+
338
+ // 如果是 assistant 消息,添加 model 信息
339
+ if (msg.role === 'assistant') {
340
+ geminiMsg.model = unified.metadata.model || 'gemini-2.5-pro';
341
+ }
342
+
343
+ return geminiMsg;
344
+ });
345
+
346
+ // 计算最后更新时间
347
+ const lastUpdated = messages.length > 0
348
+ ? messages[messages.length - 1].timestamp
349
+ : unified.startTime;
350
+
351
+ const session = {
352
+ sessionId: unified.sessionId,
353
+ projectHash,
354
+ startTime: unified.startTime,
355
+ lastUpdated,
356
+ messages
357
+ };
358
+
359
+ // 确保目标目录存在
360
+ const dir = path.dirname(targetPath);
361
+ if (!fs.existsSync(dir)) {
362
+ fs.mkdirSync(dir, { recursive: true });
363
+ }
364
+
365
+ // 写入文件
366
+ fs.writeFileSync(targetPath, JSON.stringify(session, null, 2), 'utf8');
367
+
368
+ return targetPath;
369
+ }
370
+
371
+ /**
372
+ * 生成目标路径
373
+ * @param {string} targetType - 目标格式 (claude|codex|gemini)
374
+ * @param {UnifiedSession} unified - 统一格式会话
375
+ * @param {Object} options - 选项
376
+ * @returns {string} 目标文件路径
377
+ */
378
+ function generateTargetPath(targetType, unified, options = {}) {
379
+ const newSessionId = options.sessionId || crypto.randomUUID();
380
+
381
+ if (targetType === 'claude') {
382
+ // Claude: ~/.claude/projects/{encoded-path}/{uuid}.jsonl
383
+ const projectsDir = path.join(os.homedir(), '.claude', 'projects');
384
+ const encodedPath = options.targetProject
385
+ ? encodePath(options.targetProject)
386
+ : encodePath(unified.cwd);
387
+
388
+ return path.join(projectsDir, encodedPath, `${newSessionId}.jsonl`);
389
+ } else if (targetType === 'codex') {
390
+ // Codex: ~/.codex/sessions/{YYYY}/{MM}/{DD}/rollout-{timestamp}-{uuid}.jsonl
391
+ const sessionsDir = path.join(os.homedir(), '.codex', 'sessions');
392
+ const now = new Date();
393
+ const year = now.getFullYear();
394
+ const month = String(now.getMonth() + 1).padStart(2, '0');
395
+ const day = String(now.getDate()).padStart(2, '0');
396
+ const timestamp = now.toISOString()
397
+ .replace(/\.\d{3}Z$/, '')
398
+ .replace(/:/g, '-');
399
+
400
+ return path.join(sessionsDir, String(year), month, day,
401
+ `rollout-${timestamp}-${newSessionId}.jsonl`);
402
+ } else if (targetType === 'gemini') {
403
+ // Gemini: ~/.gemini/tmp/{project_hash}/chats/session-{timestamp}-{shortId}.json
404
+ const geminiDir = path.join(os.homedir(), '.gemini', 'tmp');
405
+ const projectHash = unified.metadata.projectHash ||
406
+ crypto.createHash('sha256').update(unified.cwd).digest('hex');
407
+ const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '');
408
+ const shortId = crypto.randomBytes(4).toString('hex');
409
+
410
+ return path.join(geminiDir, projectHash, 'chats',
411
+ `session-${timestamp}-${shortId}.json`);
412
+ } else {
413
+ throw new Error(`Unsupported target type: ${targetType}`);
414
+ }
415
+ }
416
+
417
+ /**
418
+ * 编码路径(Claude Code 格式)
419
+ * @param {string} realPath - 真实路径
420
+ * @returns {string} 编码后的路径
421
+ */
422
+ function encodePath(realPath) {
423
+ // macOS/Linux: /Users/user/project -> -Users-user-project
424
+ // Windows: C:\Users\user\project -> C--Users-user-project
425
+ if (process.platform === 'win32') {
426
+ return realPath.replace(/\\/g, '-').replace(/:/g, '--');
427
+ } else {
428
+ return realPath.replace(/\//g, '-');
429
+ }
430
+ }
431
+
432
+ /**
433
+ * 主转换函数
434
+ * @param {string} sourceType - 源格式 (claude|codex|gemini)
435
+ * @param {string} targetType - 目标格式 (claude|codex|gemini)
436
+ * @param {string} sessionId - 会话 ID
437
+ * @param {Object} options - 选项
438
+ * @returns {Object} 转换结果
439
+ */
440
+ async function convertSession(sourceType, targetType, sessionId, options = {}) {
441
+ // 验证参数
442
+ const validTypes = ['claude', 'codex', 'gemini'];
443
+ if (!validTypes.includes(sourceType)) {
444
+ throw new Error(`Invalid source type: ${sourceType}`);
445
+ }
446
+ if (!validTypes.includes(targetType)) {
447
+ throw new Error(`Invalid target type: ${targetType}`);
448
+ }
449
+ if (sourceType === targetType) {
450
+ throw new Error('Source and target types must be different');
451
+ }
452
+
453
+ // 1. 读取源会话
454
+ let sourceFilePath;
455
+ let sourceSession;
456
+
457
+ if (sourceType === 'claude') {
458
+ const { getConfig } = require('../config/default');
459
+ const config = getConfig();
460
+ const sessions = require('./sessions');
461
+ const allSessions = require('../../utils/session').getAllSessions(config);
462
+ sourceSession = allSessions.find(s => s.sessionId === sessionId);
463
+ if (!sourceSession) {
464
+ throw new Error('Source session not found');
465
+ }
466
+ sourceFilePath = sourceSession.filePath;
467
+ } else if (sourceType === 'codex') {
468
+ sourceSession = getCodexSessionById(sessionId);
469
+ if (!sourceSession) {
470
+ throw new Error('Source session not found');
471
+ }
472
+ sourceFilePath = sourceSession.filePath;
473
+ } else if (sourceType === 'gemini') {
474
+ sourceSession = getGeminiSessionById(sessionId);
475
+ if (!sourceSession) {
476
+ throw new Error('Source session not found');
477
+ }
478
+ sourceFilePath = sourceSession.filePath;
479
+ }
480
+
481
+ // 2. 解析为统一格式
482
+ let unified;
483
+ if (sourceType === 'claude') {
484
+ unified = parseClaudeToUnified(sourceFilePath);
485
+ } else if (sourceType === 'codex') {
486
+ unified = parseCodexToUnified(sourceFilePath);
487
+ } else if (sourceType === 'gemini') {
488
+ unified = parseGeminiToUnified(sourceFilePath);
489
+ }
490
+
491
+ // 3. 生成目标路径
492
+ const targetPath = generateTargetPath(targetType, unified, options);
493
+
494
+ // 4. 生成目标格式
495
+ let generatedPath;
496
+ if (targetType === 'claude') {
497
+ generatedPath = generateClaudeFromUnified(unified, targetPath);
498
+ } else if (targetType === 'codex') {
499
+ generatedPath = generateCodexFromUnified(unified, targetPath);
500
+ } else if (targetType === 'gemini') {
501
+ generatedPath = generateGeminiFromUnified(unified, targetPath);
502
+ }
503
+
504
+ return {
505
+ success: true,
506
+ sourceType,
507
+ targetType,
508
+ sourceSessionId: sessionId,
509
+ targetPath: generatedPath,
510
+ targetSessionId: path.basename(generatedPath, path.extname(generatedPath)).split('-').pop(),
511
+ messageCount: unified.messages.length
512
+ };
513
+ }
514
+
515
+ /**
516
+ * 预览转换(不写入文件)
517
+ * @param {string} sourceType - 源格式
518
+ * @param {string} sessionId - 会话 ID
519
+ * @returns {Object} 预览数据
520
+ */
521
+ async function previewConversion(sourceType, sessionId) {
522
+ let sourceFilePath;
523
+
524
+ if (sourceType === 'claude') {
525
+ const { getConfig } = require('../config/default');
526
+ const config = getConfig();
527
+ const allSessions = require('../../utils/session').getAllSessions(config);
528
+ const sourceSession = allSessions.find(s => s.sessionId === sessionId);
529
+ if (!sourceSession) {
530
+ throw new Error('Source session not found');
531
+ }
532
+ sourceFilePath = sourceSession.filePath;
533
+ } else if (sourceType === 'codex') {
534
+ const sourceSession = getCodexSessionById(sessionId);
535
+ if (!sourceSession) {
536
+ throw new Error('Source session not found');
537
+ }
538
+ sourceFilePath = sourceSession.filePath;
539
+ } else if (sourceType === 'gemini') {
540
+ const sourceSession = getGeminiSessionById(sessionId);
541
+ if (!sourceSession) {
542
+ throw new Error('Source session not found');
543
+ }
544
+ sourceFilePath = sourceSession.filePath;
545
+ }
546
+
547
+ // 解析为统一格式
548
+ let unified;
549
+ if (sourceType === 'claude') {
550
+ unified = parseClaudeToUnified(sourceFilePath);
551
+ } else if (sourceType === 'codex') {
552
+ unified = parseCodexToUnified(sourceFilePath);
553
+ } else if (sourceType === 'gemini') {
554
+ unified = parseGeminiToUnified(sourceFilePath);
555
+ }
556
+
557
+ return {
558
+ sessionId: unified.sessionId,
559
+ cwd: unified.cwd,
560
+ gitBranch: unified.gitBranch,
561
+ startTime: unified.startTime,
562
+ messageCount: unified.messages.length,
563
+ messages: unified.messages.slice(0, 5), // 只返回前 5 条消息作为预览
564
+ metadata: unified.metadata
565
+ };
566
+ }
567
+
568
+ module.exports = {
569
+ convertSession,
570
+ previewConversion,
571
+ parseClaudeToUnified,
572
+ parseCodexToUnified,
573
+ parseGeminiToUnified,
574
+ generateClaudeFromUnified,
575
+ generateCodexFromUnified,
576
+ generateGeminiFromUnified
577
+ };