@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,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
|
+
};
|