lcch-cli 1.0.1
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/README.md +847 -0
- package/dist/backup.js +142 -0
- package/dist/export.js +1211 -0
- package/dist/index.js +3015 -0
- package/dist/scanner.js +1789 -0
- package/dist/types.js +5 -0
- package/package.json +49 -0
package/dist/export.js
ADDED
|
@@ -0,0 +1,1211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 会话导出模块 - 负责导出项目会话到文件
|
|
4
|
+
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.readSessionMessages = readSessionMessages;
|
|
40
|
+
exports.readProjectSessions = readProjectSessions;
|
|
41
|
+
exports.generateExportFileName = generateExportFileName;
|
|
42
|
+
exports.exportProjectSessions = exportProjectSessions;
|
|
43
|
+
exports.getDefaultOutputDir = getDefaultOutputDir;
|
|
44
|
+
exports.formatFileSize = formatFileSize;
|
|
45
|
+
const fs = __importStar(require("fs-extra"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const os = __importStar(require("os"));
|
|
48
|
+
/**
|
|
49
|
+
* 读取单个会话的消息
|
|
50
|
+
*/
|
|
51
|
+
async function readSessionMessages(projectPath, sessionId) {
|
|
52
|
+
const normalizedPath = projectPath.replace(/\//g, '-');
|
|
53
|
+
const filePath = path.join(os.homedir(), '.claude', 'projects', normalizedPath, `${sessionId}.jsonl`);
|
|
54
|
+
if (!await fs.pathExists(filePath)) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
58
|
+
const lines = content.trim().split('\n').filter(line => line.trim());
|
|
59
|
+
const messages = [];
|
|
60
|
+
for (const line of lines) {
|
|
61
|
+
try {
|
|
62
|
+
const record = JSON.parse(line);
|
|
63
|
+
messages.push(record);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// 忽略解析失败的行
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return messages;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 读取项目的所有会话
|
|
73
|
+
*/
|
|
74
|
+
async function readProjectSessions(projectPath) {
|
|
75
|
+
const normalizedPath = projectPath.replace(/\//g, '-');
|
|
76
|
+
const projectDir = path.join(os.homedir(), '.claude', 'projects', normalizedPath);
|
|
77
|
+
if (!await fs.pathExists(projectDir)) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
const entries = await fs.readdir(projectDir, { withFileTypes: true });
|
|
81
|
+
const sessions = [];
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
if (entry.isFile() && entry.name.endsWith('.jsonl')) {
|
|
84
|
+
const sessionId = entry.name.replace('.jsonl', '');
|
|
85
|
+
const messages = await readSessionMessages(projectPath, sessionId);
|
|
86
|
+
if (messages.length > 0) {
|
|
87
|
+
sessions.push({ sessionId, messages });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// 按会话ID排序
|
|
92
|
+
sessions.sort((a, b) => a.sessionId.localeCompare(b.sessionId));
|
|
93
|
+
return sessions;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 生成唯一的导出文件名
|
|
97
|
+
*/
|
|
98
|
+
function generateExportFileName(projectPath, format, usedNames) {
|
|
99
|
+
const baseName = path.basename(projectPath);
|
|
100
|
+
const ext = format === 'json' ? 'json' : format === 'html' ? 'html' : format === 'text' ? 'txt' : 'md';
|
|
101
|
+
const suffix = '-sessions';
|
|
102
|
+
let fileName = `${baseName}${suffix}.${ext}`;
|
|
103
|
+
if (!usedNames.has(fileName)) {
|
|
104
|
+
usedNames.add(fileName);
|
|
105
|
+
return fileName;
|
|
106
|
+
}
|
|
107
|
+
let counter = 1;
|
|
108
|
+
while (usedNames.has(`${baseName}-${counter}${suffix}.${ext}`)) {
|
|
109
|
+
counter++;
|
|
110
|
+
}
|
|
111
|
+
fileName = `${baseName}-${counter}${suffix}.${ext}`;
|
|
112
|
+
usedNames.add(fileName);
|
|
113
|
+
return fileName;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* 格式化日期时间
|
|
117
|
+
*/
|
|
118
|
+
function formatDateTime(timestamp) {
|
|
119
|
+
if (!timestamp)
|
|
120
|
+
return '未知时间';
|
|
121
|
+
const date = new Date(timestamp);
|
|
122
|
+
return date.toLocaleString('zh-CN', {
|
|
123
|
+
year: 'numeric',
|
|
124
|
+
month: '2-digit',
|
|
125
|
+
day: '2-digit',
|
|
126
|
+
hour: '2-digit',
|
|
127
|
+
minute: '2-digit',
|
|
128
|
+
second: '2-digit',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* 提取消息文本内容
|
|
133
|
+
*/
|
|
134
|
+
function extractMessageText(content) {
|
|
135
|
+
if (!content)
|
|
136
|
+
return '';
|
|
137
|
+
if (typeof content === 'string') {
|
|
138
|
+
return content;
|
|
139
|
+
}
|
|
140
|
+
if (Array.isArray(content)) {
|
|
141
|
+
return content
|
|
142
|
+
.map((item) => {
|
|
143
|
+
if (typeof item === 'string')
|
|
144
|
+
return item;
|
|
145
|
+
if (item && typeof item === 'object') {
|
|
146
|
+
const obj = item;
|
|
147
|
+
// 处理 text 类型
|
|
148
|
+
if (obj.type === 'text' && obj.text) {
|
|
149
|
+
return String(obj.text);
|
|
150
|
+
}
|
|
151
|
+
// 处理 tool_use 类型
|
|
152
|
+
if (obj.type === 'tool_use') {
|
|
153
|
+
const toolName = obj.name || '未知工具';
|
|
154
|
+
const toolInput = obj.input || {};
|
|
155
|
+
const toolInputStr = JSON.stringify(toolInput, null, 2);
|
|
156
|
+
return `🔧 使用工具: ${toolName}\n参数:\n${toolInputStr}`;
|
|
157
|
+
}
|
|
158
|
+
// 处理 tool_result 类型
|
|
159
|
+
if (obj.type === 'tool_result') {
|
|
160
|
+
const toolContent = obj.content;
|
|
161
|
+
const toolError = obj.is_error ? '❌ 错误' : '';
|
|
162
|
+
if (typeof toolContent === 'string') {
|
|
163
|
+
return `${toolError ? toolError + '\n' : ''}📋 工具结果:\n${toolContent}`;
|
|
164
|
+
}
|
|
165
|
+
else if (toolContent) {
|
|
166
|
+
return `${toolError ? toolError + '\n' : ''}📋 工具结果:\n${JSON.stringify(toolContent, null, 2)}`;
|
|
167
|
+
}
|
|
168
|
+
return `${toolError || '📋 工具结果'}`;
|
|
169
|
+
}
|
|
170
|
+
// 处理 thinking 类型
|
|
171
|
+
if (obj.type === 'thinking') {
|
|
172
|
+
const thinking = obj.thinking || '';
|
|
173
|
+
const signature = obj.signature || '';
|
|
174
|
+
let result = '💭 思考过程';
|
|
175
|
+
if (thinking) {
|
|
176
|
+
result += `:\n${thinking}`;
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
// 处理 redacted_thinking 类型
|
|
181
|
+
if (obj.type === 'redacted_thinking') {
|
|
182
|
+
return '🔒 [思考内容已隐藏]';
|
|
183
|
+
}
|
|
184
|
+
// 处理 file_history_snapshot 类型
|
|
185
|
+
if (obj.type === 'file-history-snapshot') {
|
|
186
|
+
return '📸 文件历史快照';
|
|
187
|
+
}
|
|
188
|
+
// 处理其他带 text 字段的类型
|
|
189
|
+
if (obj.text) {
|
|
190
|
+
return String(obj.text);
|
|
191
|
+
}
|
|
192
|
+
// 处理带 name 字段的类型
|
|
193
|
+
if (obj.name) {
|
|
194
|
+
return `[${obj.name}]`;
|
|
195
|
+
}
|
|
196
|
+
// 默认返回类型名
|
|
197
|
+
if (obj.type) {
|
|
198
|
+
return `[${obj.type}]`;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return '';
|
|
202
|
+
})
|
|
203
|
+
.filter(Boolean)
|
|
204
|
+
.join('\n');
|
|
205
|
+
}
|
|
206
|
+
if (typeof content === 'object' && content !== null) {
|
|
207
|
+
const obj = content;
|
|
208
|
+
if (obj.text)
|
|
209
|
+
return String(obj.text);
|
|
210
|
+
if (obj.type) {
|
|
211
|
+
// 对单个对象也进行类型处理
|
|
212
|
+
if (obj.type === 'tool_use') {
|
|
213
|
+
return `🔧 使用工具: ${obj.name || '未知工具'}`;
|
|
214
|
+
}
|
|
215
|
+
if (obj.type === 'tool_result') {
|
|
216
|
+
return '📋 工具结果';
|
|
217
|
+
}
|
|
218
|
+
return `[${obj.type}]`;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return String(content);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
/**
|
|
225
|
+
* 过滤单条消息的内容块
|
|
226
|
+
*/
|
|
227
|
+
function filterMessageContent(content, options) {
|
|
228
|
+
// 如果没有内容类型过滤条件,则返回全部
|
|
229
|
+
if (options.contentTypes.length === 0) {
|
|
230
|
+
return content;
|
|
231
|
+
}
|
|
232
|
+
return content.filter(item => {
|
|
233
|
+
if (!item || typeof item !== 'object')
|
|
234
|
+
return false;
|
|
235
|
+
return options.contentTypes.includes(item.type);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* 过滤消息列表
|
|
240
|
+
*/
|
|
241
|
+
function filterMessages(messages, options) {
|
|
242
|
+
// 如果没有角色和内容类型过滤条件,则返回全部
|
|
243
|
+
if (options.roles.length === 0 && options.contentTypes.length === 0) {
|
|
244
|
+
return messages;
|
|
245
|
+
}
|
|
246
|
+
return messages.map(msg => {
|
|
247
|
+
// 如果指定了角色过滤
|
|
248
|
+
if (options.roles.length > 0) {
|
|
249
|
+
const msgRole = msg.message?.role;
|
|
250
|
+
if (msgRole && !options.roles.includes(msgRole)) {
|
|
251
|
+
return { ...msg, message: undefined }; // 移除消息内容但保留记录
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// 如果指定了内容类型过滤
|
|
255
|
+
if (options.contentTypes.length > 0 && msg.message?.content) {
|
|
256
|
+
return {
|
|
257
|
+
...msg,
|
|
258
|
+
message: {
|
|
259
|
+
...msg.message,
|
|
260
|
+
content: filterMessageContent(msg.message.content, options),
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
return msg;
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* 转换为 JSON 格式
|
|
269
|
+
*/
|
|
270
|
+
function convertToJson(projectPath, sessions, options = { sessions: [], roles: [], contentTypes: [] }) {
|
|
271
|
+
// 对每个会话的消息进行过滤
|
|
272
|
+
const filteredSessions = sessions.map(session => ({
|
|
273
|
+
...session,
|
|
274
|
+
messages: filterMessages(session.messages, options),
|
|
275
|
+
}));
|
|
276
|
+
const exportData = {
|
|
277
|
+
projectPath,
|
|
278
|
+
exportedAt: new Date().toISOString(),
|
|
279
|
+
sessionCount: sessions.length,
|
|
280
|
+
sessions: filteredSessions.map(session => ({
|
|
281
|
+
sessionId: session.sessionId,
|
|
282
|
+
messageCount: session.messages.length,
|
|
283
|
+
messages: session.messages,
|
|
284
|
+
})),
|
|
285
|
+
};
|
|
286
|
+
return JSON.stringify(exportData, null, 2);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* 转换为 Markdown 格式
|
|
290
|
+
*/
|
|
291
|
+
function convertToMarkdown(projectPath, sessions, options = { sessions: [], roles: [], contentTypes: [] }) {
|
|
292
|
+
// 对每个会话的消息进行过滤
|
|
293
|
+
const filteredSessions = sessions.map(session => ({
|
|
294
|
+
...session,
|
|
295
|
+
messages: filterMessages(session.messages, options),
|
|
296
|
+
}));
|
|
297
|
+
const projectName = path.basename(projectPath);
|
|
298
|
+
const exportedAt = formatDateTime(new Date().toISOString());
|
|
299
|
+
let md = `# ${projectName} 会话导出\n\n`;
|
|
300
|
+
md += `**项目路径**: ${projectPath}\n\n`;
|
|
301
|
+
md += `**导出时间**: ${exportedAt}\n\n`;
|
|
302
|
+
md += `**会话数量**: ${sessions.length}\n\n`;
|
|
303
|
+
md += `---\n\n`;
|
|
304
|
+
for (let i = 0; i < filteredSessions.length; i++) {
|
|
305
|
+
const session = filteredSessions[i];
|
|
306
|
+
const isLastSession = i === filteredSessions.length - 1;
|
|
307
|
+
md += `## Session ${i + 1}: ${session.sessionId}\n\n`;
|
|
308
|
+
// 计算会话时间范围
|
|
309
|
+
const timestamps = session.messages
|
|
310
|
+
.map(m => m.timestamp)
|
|
311
|
+
.filter(Boolean)
|
|
312
|
+
.map(t => new Date(t).getTime());
|
|
313
|
+
if (timestamps.length > 0) {
|
|
314
|
+
const minTime = Math.min(...timestamps);
|
|
315
|
+
const maxTime = Math.max(...timestamps);
|
|
316
|
+
md += `**会话时间**: ${formatDateTime(minTime)} - ${formatDateTime(maxTime)}\n\n`;
|
|
317
|
+
md += `**消息数量**: ${session.messages.length}\n\n`;
|
|
318
|
+
}
|
|
319
|
+
// 写入消息
|
|
320
|
+
for (const message of session.messages) {
|
|
321
|
+
const time = formatDateTime(message.timestamp);
|
|
322
|
+
const role = message.message?.role === 'user' ? 'User' : 'Assistant';
|
|
323
|
+
md += `### ${time} - ${role}\n\n`;
|
|
324
|
+
const content = message.message?.content;
|
|
325
|
+
if (content) {
|
|
326
|
+
const text = extractMessageText(content);
|
|
327
|
+
if (text) {
|
|
328
|
+
// 如果是代码块,保持格式
|
|
329
|
+
md += `${text}\n\n`;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// 显示其他字段信息(如果有)
|
|
333
|
+
if (message.type && message.type !== 'message') {
|
|
334
|
+
md += `*类型: ${message.type}*\n\n`;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (!isLastSession) {
|
|
338
|
+
md += `---\n\n`;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return md;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* 将消息内容分类提取(支持工具操作合并)
|
|
345
|
+
*/
|
|
346
|
+
function categorizeMessageContent(content) {
|
|
347
|
+
const result = {
|
|
348
|
+
texts: [],
|
|
349
|
+
toolOperations: new Map(),
|
|
350
|
+
toolResults: new Map(),
|
|
351
|
+
thinkings: [],
|
|
352
|
+
others: [],
|
|
353
|
+
};
|
|
354
|
+
if (!content)
|
|
355
|
+
return result;
|
|
356
|
+
const items = Array.isArray(content) ? content : [content];
|
|
357
|
+
for (const item of items) {
|
|
358
|
+
if (typeof item === 'string') {
|
|
359
|
+
if (item.trim())
|
|
360
|
+
result.texts.push(item);
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (item && typeof item === 'object') {
|
|
364
|
+
const obj = item;
|
|
365
|
+
const type = obj.type;
|
|
366
|
+
switch (type) {
|
|
367
|
+
case 'text':
|
|
368
|
+
if (obj.text)
|
|
369
|
+
result.texts.push(String(obj.text));
|
|
370
|
+
break;
|
|
371
|
+
case 'tool_use': {
|
|
372
|
+
const toolName = String(obj.name || '未知工具');
|
|
373
|
+
const toolId = obj.id ? String(obj.id) : '';
|
|
374
|
+
const toolInput = obj.input || {};
|
|
375
|
+
const operation = {
|
|
376
|
+
id: toolId,
|
|
377
|
+
name: toolName,
|
|
378
|
+
input: toolInput,
|
|
379
|
+
inputStr: JSON.stringify(toolInput),
|
|
380
|
+
};
|
|
381
|
+
if (!result.toolOperations.has(toolName)) {
|
|
382
|
+
result.toolOperations.set(toolName, []);
|
|
383
|
+
}
|
|
384
|
+
result.toolOperations.get(toolName).push(operation);
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
case 'tool_result': {
|
|
388
|
+
const toolUseId = obj.tool_use_id ? String(obj.tool_use_id) : '';
|
|
389
|
+
const toolContent = obj.content;
|
|
390
|
+
const isError = !!obj.is_error;
|
|
391
|
+
// 从 toolUseId 提取工具名称(通常是 toolu_xxx 格式)
|
|
392
|
+
let toolName = '未知工具';
|
|
393
|
+
if (toolUseId) {
|
|
394
|
+
// 尝试匹配工具名称(如果有的话)
|
|
395
|
+
const idParts = toolUseId.split(':');
|
|
396
|
+
if (idParts.length > 1) {
|
|
397
|
+
toolName = idParts[0];
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const toolResult = {
|
|
401
|
+
toolUseId,
|
|
402
|
+
content: toolContent,
|
|
403
|
+
isError,
|
|
404
|
+
};
|
|
405
|
+
if (!result.toolResults.has(toolName)) {
|
|
406
|
+
result.toolResults.set(toolName, []);
|
|
407
|
+
}
|
|
408
|
+
result.toolResults.get(toolName).push(toolResult);
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
case 'thinking':
|
|
412
|
+
if (obj.thinking) {
|
|
413
|
+
result.thinkings.push('💭 思考过程');
|
|
414
|
+
result.thinkings.push(...String(obj.thinking).split('\n'));
|
|
415
|
+
}
|
|
416
|
+
break;
|
|
417
|
+
case 'redacted_thinking':
|
|
418
|
+
result.thinkings.push('🔒 [思考内容已隐藏]');
|
|
419
|
+
break;
|
|
420
|
+
case 'file_history_snapshot':
|
|
421
|
+
result.others.push('📸 文件历史快照');
|
|
422
|
+
break;
|
|
423
|
+
default:
|
|
424
|
+
if (obj.text) {
|
|
425
|
+
result.texts.push(String(obj.text));
|
|
426
|
+
}
|
|
427
|
+
else if (type) {
|
|
428
|
+
result.others.push(`[${type}]`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return result;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* 格式化工具操作为紧凑列表
|
|
437
|
+
*/
|
|
438
|
+
function formatToolOperations(toolOperations, contentWidth) {
|
|
439
|
+
const lines = [];
|
|
440
|
+
let totalCount = 0;
|
|
441
|
+
for (const [toolName, operations] of toolOperations) {
|
|
442
|
+
totalCount += operations.length;
|
|
443
|
+
}
|
|
444
|
+
lines.push(`🔧 共 ${totalCount} 个工具操作`);
|
|
445
|
+
for (const [toolName, operations] of toolOperations) {
|
|
446
|
+
lines.push(`├─ ${toolName} (${operations.length}) ─${'─'.repeat(Math.max(0, contentWidth - toolName.length - 15))}`);
|
|
447
|
+
for (let i = 0; i < operations.length; i++) {
|
|
448
|
+
const op = operations[i];
|
|
449
|
+
const isLast = i === operations.length - 1;
|
|
450
|
+
const prefix = isLast ? '└─' : '├─';
|
|
451
|
+
// 提取关键参数
|
|
452
|
+
const inputSummary = summarizeToolInput(toolName, op.input);
|
|
453
|
+
const shortId = op.id.split(':').pop() || op.id.substring(0, 8);
|
|
454
|
+
const line = `${prefix} [${shortId}] ${inputSummary}`;
|
|
455
|
+
lines.push(line.length > contentWidth ? line.substring(0, contentWidth - 3) + '...' : line);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return lines;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* 格式化工具结果为紧凑列表
|
|
462
|
+
*/
|
|
463
|
+
function formatToolResults(toolResults, contentWidth) {
|
|
464
|
+
const lines = [];
|
|
465
|
+
let totalCount = 0;
|
|
466
|
+
for (const [toolName, results] of toolResults) {
|
|
467
|
+
totalCount += results.length;
|
|
468
|
+
}
|
|
469
|
+
lines.push(`📋 共 ${totalCount} 个工具结果`);
|
|
470
|
+
for (const [toolName, results] of toolResults) {
|
|
471
|
+
lines.push(`├─ ${toolName} (${results.length}) ─${'─'.repeat(Math.max(0, contentWidth - toolName.length - 15))}`);
|
|
472
|
+
for (let i = 0; i < results.length; i++) {
|
|
473
|
+
const result = results[i];
|
|
474
|
+
const isLast = i === results.length - 1;
|
|
475
|
+
const prefix = isLast ? '└─' : '├─';
|
|
476
|
+
const shortId = result.toolUseId.split(':').pop() || result.toolUseId.substring(0, 8);
|
|
477
|
+
// 处理错误标记
|
|
478
|
+
const errorMark = result.isError ? '❌ ' : '';
|
|
479
|
+
// 提取结果摘要
|
|
480
|
+
const resultSummary = summarizeToolResult(result.content);
|
|
481
|
+
const line = `${prefix} ${errorMark}[${shortId}] ${resultSummary}`;
|
|
482
|
+
if (line.length > contentWidth) {
|
|
483
|
+
lines.push(line.substring(0, contentWidth - 3) + '...');
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
lines.push(line);
|
|
487
|
+
}
|
|
488
|
+
// 如果有详细内容且是错误,显示更多行
|
|
489
|
+
if (result.isError && typeof result.content === 'string') {
|
|
490
|
+
const contentLines = result.content.split('\n').slice(0, 3); // 最多显示3行
|
|
491
|
+
for (let j = 0; j < contentLines.length; j++) {
|
|
492
|
+
const contentLine = contentLines[j];
|
|
493
|
+
const isLastContent = j === contentLines.length - 1;
|
|
494
|
+
const contentPrefix = isLast && isLastContent ? '└─' : '│ ';
|
|
495
|
+
const formatted = `${contentPrefix} ${contentLine}`;
|
|
496
|
+
lines.push(formatted.length > contentWidth ? formatted.substring(0, contentWidth - 3) + '...' : formatted);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return lines;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* 摘要化工具输入
|
|
505
|
+
*/
|
|
506
|
+
function summarizeToolInput(toolName, input) {
|
|
507
|
+
const inputStr = JSON.stringify(input);
|
|
508
|
+
// 针对常见工具类型的摘要
|
|
509
|
+
switch (toolName.toLowerCase()) {
|
|
510
|
+
case 'read':
|
|
511
|
+
const filePath = input.file_path || input.path || '';
|
|
512
|
+
return filePath ? `读取: ${path.basename(String(filePath))}` : '读取文件';
|
|
513
|
+
case 'write':
|
|
514
|
+
const writePath = input.file_path || input.path || '';
|
|
515
|
+
return writePath ? `写入: ${path.basename(String(writePath))}` : '写入文件';
|
|
516
|
+
case 'glob':
|
|
517
|
+
const pattern = input.pattern || '';
|
|
518
|
+
return pattern ? `匹配: ${pattern}` : '匹配文件';
|
|
519
|
+
case 'grep':
|
|
520
|
+
const query = input.pattern || input.query || '';
|
|
521
|
+
return query ? `搜索: ${query}` : '搜索内容';
|
|
522
|
+
case 'bash':
|
|
523
|
+
const command = input.command || '';
|
|
524
|
+
const shortCmd = String(command).split(' ')[0];
|
|
525
|
+
return shortCmd ? `执行: ${shortCmd}` : '执行命令';
|
|
526
|
+
case 'edit':
|
|
527
|
+
const editPath = input.file_path || input.path || '';
|
|
528
|
+
return editPath ? `编辑: ${path.basename(String(editPath))}` : '编辑文件';
|
|
529
|
+
case 'webfetch':
|
|
530
|
+
const url = input.url || '';
|
|
531
|
+
return url ? `获取: ${String(url).substring(0, 30)}...` : '获取网页';
|
|
532
|
+
default:
|
|
533
|
+
// 对于未知工具,显示前两个参数
|
|
534
|
+
const keys = Object.keys(input).slice(0, 2);
|
|
535
|
+
if (keys.length === 0)
|
|
536
|
+
return '';
|
|
537
|
+
return keys.map(k => `${k}: ${String(input[k]).substring(0, 20)}`).join(', ');
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* 摘要化工具结果
|
|
542
|
+
*/
|
|
543
|
+
function summarizeToolResult(content) {
|
|
544
|
+
if (!content)
|
|
545
|
+
return '空结果';
|
|
546
|
+
if (typeof content === 'string') {
|
|
547
|
+
// 如果内容是文件列表
|
|
548
|
+
if (content.includes('\n') && content.split('\n').length > 1) {
|
|
549
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
550
|
+
return `${lines.length} 行内容`;
|
|
551
|
+
}
|
|
552
|
+
// 如果是单行内容
|
|
553
|
+
const summary = content.substring(0, 40);
|
|
554
|
+
return summary.length < content.length ? summary + '...' : summary;
|
|
555
|
+
}
|
|
556
|
+
if (typeof content === 'object') {
|
|
557
|
+
// 尝试提取有用信息
|
|
558
|
+
const obj = content;
|
|
559
|
+
if (obj.count !== undefined)
|
|
560
|
+
return `${obj.count} 项`;
|
|
561
|
+
if (obj.length !== undefined)
|
|
562
|
+
return `${obj.length} 项`;
|
|
563
|
+
if (obj.status)
|
|
564
|
+
return String(obj.status);
|
|
565
|
+
if (obj.result)
|
|
566
|
+
return JSON.stringify(obj.result).substring(0, 40);
|
|
567
|
+
return JSON.stringify(content).substring(0, 40);
|
|
568
|
+
}
|
|
569
|
+
return String(content).substring(0, 40);
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* 转换为纯文本格式(优雅版,使用 Unicode 符号,合并显示同一消息块)
|
|
573
|
+
*/
|
|
574
|
+
function convertToText(projectPath, sessions, options = { sessions: [], roles: [], contentTypes: [] }) {
|
|
575
|
+
// 对每个会话的消息进行过滤
|
|
576
|
+
const filteredSessions = sessions.map(session => ({
|
|
577
|
+
...session,
|
|
578
|
+
messages: filterMessages(session.messages, options),
|
|
579
|
+
}));
|
|
580
|
+
const projectName = path.basename(projectPath);
|
|
581
|
+
const exportedAt = formatDateTime(new Date().toISOString());
|
|
582
|
+
const totalMessages = filteredSessions.reduce((sum, s) => sum + s.messages.length, 0);
|
|
583
|
+
// 计算标题宽度
|
|
584
|
+
const titleWidth = Math.max(60, projectName.length * 2 + 4);
|
|
585
|
+
let text = '';
|
|
586
|
+
// 顶部装饰线
|
|
587
|
+
text += '═'.repeat(titleWidth) + '\n';
|
|
588
|
+
text += `📁 ${projectName}\n`;
|
|
589
|
+
text += '═'.repeat(titleWidth) + '\n\n';
|
|
590
|
+
// 项目信息
|
|
591
|
+
text += `📂 ${projectPath}\n`;
|
|
592
|
+
text += `📅 导出时间: ${exportedAt}\n`;
|
|
593
|
+
text += `💬 ${sessions.length} 个会话, ${totalMessages} 条消息\n\n`;
|
|
594
|
+
// 分隔线
|
|
595
|
+
text += '─'.repeat(titleWidth) + '\n\n';
|
|
596
|
+
for (let i = 0; i < filteredSessions.length; i++) {
|
|
597
|
+
const session = filteredSessions[i];
|
|
598
|
+
const isLastSession = i === filteredSessions.length - 1;
|
|
599
|
+
// 会话标题
|
|
600
|
+
text += `🔷 会话 ${i + 1} │ ${session.sessionId}\n`;
|
|
601
|
+
// 计算会话时间范围
|
|
602
|
+
const timestamps = session.messages
|
|
603
|
+
.map(m => m.timestamp)
|
|
604
|
+
.filter(Boolean)
|
|
605
|
+
.map(t => new Date(t).getTime());
|
|
606
|
+
if (timestamps.length > 0) {
|
|
607
|
+
const minTime = Math.min(...timestamps);
|
|
608
|
+
const maxTime = Math.max(...timestamps);
|
|
609
|
+
const minTimeStr = formatDateTime(minTime);
|
|
610
|
+
const maxTimeStr = formatDateTime(maxTime);
|
|
611
|
+
const maxTimeOnly = maxTimeStr.split(' ')[1] || maxTimeStr;
|
|
612
|
+
text += ` 🕐 ${minTimeStr} ~ ${maxTimeOnly} │ ${session.messages.length} 条消息\n`;
|
|
613
|
+
}
|
|
614
|
+
text += '\n';
|
|
615
|
+
text += '─'.repeat(titleWidth) + '\n\n';
|
|
616
|
+
let currentBlock = null;
|
|
617
|
+
const blocks = [];
|
|
618
|
+
for (const message of session.messages) {
|
|
619
|
+
const role = message.message?.role === 'user' ? 'user' : 'assistant';
|
|
620
|
+
const time = formatDateTime(message.timestamp);
|
|
621
|
+
const timeOnly = time.split(' ')[1] || time;
|
|
622
|
+
if (!currentBlock || currentBlock.role !== role) {
|
|
623
|
+
currentBlock = { role, time: timeOnly, messages: [] };
|
|
624
|
+
blocks.push(currentBlock);
|
|
625
|
+
}
|
|
626
|
+
currentBlock.messages.push(message);
|
|
627
|
+
}
|
|
628
|
+
// 输出合并后的消息块
|
|
629
|
+
for (let b = 0; b < blocks.length; b++) {
|
|
630
|
+
const block = blocks[b];
|
|
631
|
+
const isUser = block.role === 'user';
|
|
632
|
+
const roleName = isUser ? '用户' : '助手';
|
|
633
|
+
const icon = isUser ? '👤' : '🤖';
|
|
634
|
+
// 消息块标题
|
|
635
|
+
text += `${icon} ${roleName} ${block.time}\n`;
|
|
636
|
+
// 收集各类内容
|
|
637
|
+
const allTexts = [];
|
|
638
|
+
const allToolOperations = new Map();
|
|
639
|
+
const allToolResults = new Map();
|
|
640
|
+
const allThinkings = [];
|
|
641
|
+
const allOthers = [];
|
|
642
|
+
for (const message of block.messages) {
|
|
643
|
+
const content = message.message?.content;
|
|
644
|
+
if (content) {
|
|
645
|
+
const categorized = categorizeMessageContent(content);
|
|
646
|
+
allTexts.push(...categorized.texts);
|
|
647
|
+
// 合并工具操作
|
|
648
|
+
for (const [toolName, operations] of categorized.toolOperations) {
|
|
649
|
+
if (!allToolOperations.has(toolName)) {
|
|
650
|
+
allToolOperations.set(toolName, []);
|
|
651
|
+
}
|
|
652
|
+
allToolOperations.get(toolName).push(...operations);
|
|
653
|
+
}
|
|
654
|
+
// 合并工具结果
|
|
655
|
+
for (const [toolName, results] of categorized.toolResults) {
|
|
656
|
+
if (!allToolResults.has(toolName)) {
|
|
657
|
+
allToolResults.set(toolName, []);
|
|
658
|
+
}
|
|
659
|
+
allToolResults.get(toolName).push(...results);
|
|
660
|
+
}
|
|
661
|
+
allThinkings.push(...categorized.thinkings);
|
|
662
|
+
allOthers.push(...categorized.others);
|
|
663
|
+
}
|
|
664
|
+
// 收集其他类型信息
|
|
665
|
+
if (message.type && message.type !== 'message') {
|
|
666
|
+
allOthers.push(`📌 类型: ${message.type}`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
// 按顺序输出各类内容
|
|
670
|
+
const contentWidth = titleWidth - 6;
|
|
671
|
+
// 1. 思考过程
|
|
672
|
+
if (allThinkings.length > 0) {
|
|
673
|
+
text += ` ┌─💭 思考 ─${'─'.repeat(Math.max(0, contentWidth - 8))}┐\n`;
|
|
674
|
+
for (const line of allThinkings) {
|
|
675
|
+
const truncated = line.length > contentWidth - 4
|
|
676
|
+
? line.substring(0, contentWidth - 7) + '...'
|
|
677
|
+
: line;
|
|
678
|
+
text += ` │ ${truncated.padEnd(contentWidth - 4)}│\n`;
|
|
679
|
+
}
|
|
680
|
+
text += ` └${'─'.repeat(Math.max(0, contentWidth - 2))}┘\n\n`;
|
|
681
|
+
}
|
|
682
|
+
// 2. 文本内容
|
|
683
|
+
if (allTexts.length > 0) {
|
|
684
|
+
const hasTools = allToolOperations.size > 0 || allToolResults.size > 0;
|
|
685
|
+
if (hasTools) {
|
|
686
|
+
text += ` ┌─💬 消息 ─${'─'.repeat(Math.max(0, contentWidth - 8))}┐\n`;
|
|
687
|
+
}
|
|
688
|
+
for (const line of allTexts) {
|
|
689
|
+
const lines = line.split('\n');
|
|
690
|
+
for (const l of lines) {
|
|
691
|
+
const truncated = l.length > contentWidth - 4
|
|
692
|
+
? l.substring(0, contentWidth - 7) + '...'
|
|
693
|
+
: l;
|
|
694
|
+
if (hasTools) {
|
|
695
|
+
text += ` │ ${truncated.padEnd(contentWidth - 4)}│\n`;
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
text += ` ${truncated}\n`;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (hasTools) {
|
|
703
|
+
text += ` └${'─'.repeat(Math.max(0, contentWidth - 2))}┘\n\n`;
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
text += '\n';
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
// 3. 工具调用(合并显示)
|
|
710
|
+
if (allToolOperations.size > 0) {
|
|
711
|
+
const formattedTools = formatToolOperations(allToolOperations, contentWidth - 4);
|
|
712
|
+
text += ` ┌─🔧 工具调用 ─${'─'.repeat(Math.max(0, contentWidth - 11))}┐\n`;
|
|
713
|
+
for (const line of formattedTools) {
|
|
714
|
+
const displayLine = line.startsWith('🔧') || line.startsWith('├─') || line.startsWith('└─')
|
|
715
|
+
? line
|
|
716
|
+
: `│ ${line}`;
|
|
717
|
+
const truncated = displayLine.length > contentWidth - 4
|
|
718
|
+
? displayLine.substring(0, contentWidth - 7) + '...'
|
|
719
|
+
: displayLine;
|
|
720
|
+
text += ` │ ${truncated.padEnd(contentWidth - 4)}│\n`;
|
|
721
|
+
}
|
|
722
|
+
text += ` └${'─'.repeat(Math.max(0, contentWidth - 2))}┘\n\n`;
|
|
723
|
+
}
|
|
724
|
+
// 4. 工具结果(合并显示)
|
|
725
|
+
if (allToolResults.size > 0) {
|
|
726
|
+
const formattedResults = formatToolResults(allToolResults, contentWidth - 4);
|
|
727
|
+
text += ` ┌─📋 工具结果 ─${'─'.repeat(Math.max(0, contentWidth - 11))}┐\n`;
|
|
728
|
+
for (const line of formattedResults) {
|
|
729
|
+
const displayLine = line.startsWith('📋') || line.startsWith('├─') || line.startsWith('└─')
|
|
730
|
+
? line
|
|
731
|
+
: `│ ${line}`;
|
|
732
|
+
const truncated = displayLine.length > contentWidth - 4
|
|
733
|
+
? displayLine.substring(0, contentWidth - 7) + '...'
|
|
734
|
+
: displayLine;
|
|
735
|
+
text += ` │ ${truncated.padEnd(contentWidth - 4)}│\n`;
|
|
736
|
+
}
|
|
737
|
+
text += ` └${'─'.repeat(Math.max(0, contentWidth - 2))}┘\n\n`;
|
|
738
|
+
}
|
|
739
|
+
// 5. 其他类型
|
|
740
|
+
if (allOthers.length > 0) {
|
|
741
|
+
text += ` ┌─📎 其他 ─${'─'.repeat(Math.max(0, contentWidth - 9))}┐\n`;
|
|
742
|
+
for (const line of allOthers) {
|
|
743
|
+
const truncated = line.length > contentWidth - 4
|
|
744
|
+
? line.substring(0, contentWidth - 7) + '...'
|
|
745
|
+
: line;
|
|
746
|
+
text += ` │ ${truncated.padEnd(contentWidth - 4)}│\n`;
|
|
747
|
+
}
|
|
748
|
+
text += ` └${'─'.repeat(Math.max(0, contentWidth - 2))}┘\n\n`;
|
|
749
|
+
}
|
|
750
|
+
// 消息块分隔
|
|
751
|
+
if (b < blocks.length - 1) {
|
|
752
|
+
text += ` ···\n\n`;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
// 会话分隔线
|
|
756
|
+
if (!isLastSession) {
|
|
757
|
+
text += '═'.repeat(titleWidth) + '\n\n';
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
// 底部装饰线
|
|
761
|
+
text += '═'.repeat(titleWidth) + '\n';
|
|
762
|
+
return text;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* 转换为 HTML 交互式格式
|
|
766
|
+
*/
|
|
767
|
+
function convertToHtml(projectPath, sessions, options = { sessions: [], roles: [], contentTypes: [] }) {
|
|
768
|
+
// 对每个会话的消息进行过滤
|
|
769
|
+
const filteredSessions = sessions.map(session => ({
|
|
770
|
+
...session,
|
|
771
|
+
messages: filterMessages(session.messages, options),
|
|
772
|
+
}));
|
|
773
|
+
const projectName = path.basename(projectPath);
|
|
774
|
+
const exportedAt = new Date().toISOString();
|
|
775
|
+
const totalMessages = filteredSessions.reduce((sum, s) => sum + s.messages.length, 0);
|
|
776
|
+
let html = `<!DOCTYPE html>
|
|
777
|
+
<html lang="zh-CN">
|
|
778
|
+
<head>
|
|
779
|
+
<meta charset="UTF-8">
|
|
780
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
781
|
+
<title>${escapeHtml(projectName)} - 会话导出</title>
|
|
782
|
+
<style>
|
|
783
|
+
* {
|
|
784
|
+
box-sizing: border-box;
|
|
785
|
+
margin: 0;
|
|
786
|
+
padding: 0;
|
|
787
|
+
}
|
|
788
|
+
body {
|
|
789
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
790
|
+
line-height: 1.6;
|
|
791
|
+
color: #333;
|
|
792
|
+
background: #f5f5f5;
|
|
793
|
+
padding: 20px;
|
|
794
|
+
}
|
|
795
|
+
.container {
|
|
796
|
+
max-width: 900px;
|
|
797
|
+
margin: 0 auto;
|
|
798
|
+
background: white;
|
|
799
|
+
border-radius: 8px;
|
|
800
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
801
|
+
overflow: hidden;
|
|
802
|
+
}
|
|
803
|
+
.header {
|
|
804
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
805
|
+
color: white;
|
|
806
|
+
padding: 30px;
|
|
807
|
+
}
|
|
808
|
+
.header h1 {
|
|
809
|
+
font-size: 24px;
|
|
810
|
+
margin-bottom: 10px;
|
|
811
|
+
}
|
|
812
|
+
.header .meta {
|
|
813
|
+
opacity: 0.9;
|
|
814
|
+
font-size: 14px;
|
|
815
|
+
}
|
|
816
|
+
.stats {
|
|
817
|
+
display: flex;
|
|
818
|
+
gap: 20px;
|
|
819
|
+
margin-top: 15px;
|
|
820
|
+
}
|
|
821
|
+
.stats .stat {
|
|
822
|
+
background: rgba(255,255,255,0.2);
|
|
823
|
+
padding: 8px 15px;
|
|
824
|
+
border-radius: 20px;
|
|
825
|
+
font-size: 14px;
|
|
826
|
+
}
|
|
827
|
+
.session {
|
|
828
|
+
border-bottom: 1px solid #eee;
|
|
829
|
+
}
|
|
830
|
+
.session:last-child {
|
|
831
|
+
border-bottom: none;
|
|
832
|
+
}
|
|
833
|
+
.session-header {
|
|
834
|
+
background: #f8f9fa;
|
|
835
|
+
padding: 15px 20px;
|
|
836
|
+
cursor: pointer;
|
|
837
|
+
display: flex;
|
|
838
|
+
justify-content: space-between;
|
|
839
|
+
align-items: center;
|
|
840
|
+
transition: background 0.2s;
|
|
841
|
+
}
|
|
842
|
+
.session-header:hover {
|
|
843
|
+
background: #e9ecef;
|
|
844
|
+
}
|
|
845
|
+
.session-title {
|
|
846
|
+
display: flex;
|
|
847
|
+
align-items: center;
|
|
848
|
+
gap: 10px;
|
|
849
|
+
}
|
|
850
|
+
.session-title .icon {
|
|
851
|
+
width: 30px;
|
|
852
|
+
height: 30px;
|
|
853
|
+
background: #667eea;
|
|
854
|
+
color: white;
|
|
855
|
+
border-radius: 50%;
|
|
856
|
+
display: flex;
|
|
857
|
+
align-items: center;
|
|
858
|
+
justify-content: center;
|
|
859
|
+
font-weight: bold;
|
|
860
|
+
}
|
|
861
|
+
.session-title .session-id {
|
|
862
|
+
color: #666;
|
|
863
|
+
font-size: 14px;
|
|
864
|
+
}
|
|
865
|
+
.session-title .time-range {
|
|
866
|
+
color: #999;
|
|
867
|
+
font-size: 12px;
|
|
868
|
+
}
|
|
869
|
+
.session-title .message-count {
|
|
870
|
+
background: #e9ecef;
|
|
871
|
+
padding: 2px 8px;
|
|
872
|
+
border-radius: 10px;
|
|
873
|
+
font-size: 12px;
|
|
874
|
+
color: #666;
|
|
875
|
+
}
|
|
876
|
+
.toggle-icon {
|
|
877
|
+
color: #999;
|
|
878
|
+
transition: transform 0.3s;
|
|
879
|
+
}
|
|
880
|
+
.session.collapsed .toggle-icon {
|
|
881
|
+
transform: rotate(-90deg);
|
|
882
|
+
}
|
|
883
|
+
.session.collapsed .session-content {
|
|
884
|
+
display: none;
|
|
885
|
+
}
|
|
886
|
+
.session-content {
|
|
887
|
+
padding: 20px;
|
|
888
|
+
}
|
|
889
|
+
.message {
|
|
890
|
+
margin-bottom: 20px;
|
|
891
|
+
padding: 15px;
|
|
892
|
+
border-radius: 8px;
|
|
893
|
+
background: #f8f9fa;
|
|
894
|
+
}
|
|
895
|
+
.message.user {
|
|
896
|
+
background: #e3f2fd;
|
|
897
|
+
margin-left: 40px;
|
|
898
|
+
}
|
|
899
|
+
.message.assistant {
|
|
900
|
+
background: #f3e5f5;
|
|
901
|
+
margin-right: 40px;
|
|
902
|
+
}
|
|
903
|
+
.message-header {
|
|
904
|
+
display: flex;
|
|
905
|
+
justify-content: space-between;
|
|
906
|
+
margin-bottom: 10px;
|
|
907
|
+
font-size: 12px;
|
|
908
|
+
color: #666;
|
|
909
|
+
}
|
|
910
|
+
.message-header .role {
|
|
911
|
+
font-weight: bold;
|
|
912
|
+
}
|
|
913
|
+
.message-content {
|
|
914
|
+
white-space: pre-wrap;
|
|
915
|
+
word-break: break-word;
|
|
916
|
+
}
|
|
917
|
+
.tool-use {
|
|
918
|
+
background: #fff3e0;
|
|
919
|
+
border: 1px solid #ffb74d;
|
|
920
|
+
border-radius: 6px;
|
|
921
|
+
padding: 10px;
|
|
922
|
+
margin: 10px 0;
|
|
923
|
+
font-family: monospace;
|
|
924
|
+
font-size: 13px;
|
|
925
|
+
}
|
|
926
|
+
.tool-result {
|
|
927
|
+
background: #e8f5e9;
|
|
928
|
+
border: 1px solid #81c784;
|
|
929
|
+
border-radius: 6px;
|
|
930
|
+
padding: 10px;
|
|
931
|
+
margin: 10px 0;
|
|
932
|
+
font-family: monospace;
|
|
933
|
+
font-size: 13px;
|
|
934
|
+
max-height: 200px;
|
|
935
|
+
overflow-y: auto;
|
|
936
|
+
}
|
|
937
|
+
.tool-result.error {
|
|
938
|
+
background: #ffebee;
|
|
939
|
+
border-color: #ef5350;
|
|
940
|
+
}
|
|
941
|
+
.thinking {
|
|
942
|
+
background: #f5f5f5;
|
|
943
|
+
border-left: 3px solid #9e9e9e;
|
|
944
|
+
padding: 10px;
|
|
945
|
+
margin: 10px 0;
|
|
946
|
+
font-style: italic;
|
|
947
|
+
color: #666;
|
|
948
|
+
}
|
|
949
|
+
code {
|
|
950
|
+
background: #f5f5f5;
|
|
951
|
+
padding: 2px 6px;
|
|
952
|
+
border-radius: 3px;
|
|
953
|
+
font-size: 13px;
|
|
954
|
+
}
|
|
955
|
+
pre {
|
|
956
|
+
background: #263238;
|
|
957
|
+
color: #fff;
|
|
958
|
+
padding: 15px;
|
|
959
|
+
border-radius: 6px;
|
|
960
|
+
overflow-x: auto;
|
|
961
|
+
font-size: 13px;
|
|
962
|
+
}
|
|
963
|
+
pre code {
|
|
964
|
+
background: none;
|
|
965
|
+
padding: 0;
|
|
966
|
+
color: inherit;
|
|
967
|
+
}
|
|
968
|
+
.session-divider {
|
|
969
|
+
height: 1px;
|
|
970
|
+
background: linear-gradient(to right, transparent, #ddd, transparent);
|
|
971
|
+
margin: 20px 0;
|
|
972
|
+
}
|
|
973
|
+
</style>
|
|
974
|
+
</head>
|
|
975
|
+
<body>
|
|
976
|
+
<div class="container">
|
|
977
|
+
<div class="header">
|
|
978
|
+
<h1>📁 ${escapeHtml(projectName)}</h1>
|
|
979
|
+
<div class="meta">
|
|
980
|
+
<div>📂 ${escapeHtml(projectPath)}</div>
|
|
981
|
+
<div>📅 导出时间: ${formatDateTime(new Date(exportedAt).getTime())}</div>
|
|
982
|
+
</div>
|
|
983
|
+
<div class="stats">
|
|
984
|
+
<div class="stat">💬 ${filteredSessions.length} 个会话</div>
|
|
985
|
+
<div class="stat">📝 ${totalMessages} 条消息</div>
|
|
986
|
+
</div>
|
|
987
|
+
</div>
|
|
988
|
+
`;
|
|
989
|
+
// 添加会话
|
|
990
|
+
for (let i = 0; i < filteredSessions.length; i++) {
|
|
991
|
+
const session = filteredSessions[i];
|
|
992
|
+
// 计算会话时间范围
|
|
993
|
+
const timestamps = session.messages
|
|
994
|
+
.map(m => m.timestamp)
|
|
995
|
+
.filter(Boolean)
|
|
996
|
+
.map(t => new Date(t).getTime());
|
|
997
|
+
let timeRange = '';
|
|
998
|
+
if (timestamps.length > 0) {
|
|
999
|
+
const minTime = formatDateTime(Math.min(...timestamps));
|
|
1000
|
+
const maxTime = formatDateTime(Math.max(...timestamps));
|
|
1001
|
+
timeRange = `${minTime.split(' ')[0]} ~ ${maxTime.split(' ')[0]}`;
|
|
1002
|
+
}
|
|
1003
|
+
html += `
|
|
1004
|
+
<div class="session" id="session-${i}">
|
|
1005
|
+
<div class="session-header" onclick="toggleSession(${i})">
|
|
1006
|
+
<div class="session-title">
|
|
1007
|
+
<div class="icon">${i + 1}</div>
|
|
1008
|
+
<div>
|
|
1009
|
+
<div class="session-id">${escapeHtml(session.sessionId)}</div>
|
|
1010
|
+
<div class="time-range">${escapeHtml(timeRange)}</div>
|
|
1011
|
+
</div>
|
|
1012
|
+
<div class="message-count">${session.messages.length} 条消息</div>
|
|
1013
|
+
</div>
|
|
1014
|
+
<div class="toggle-icon">▼</div>
|
|
1015
|
+
</div>
|
|
1016
|
+
<div class="session-content">
|
|
1017
|
+
`;
|
|
1018
|
+
// 添加消息
|
|
1019
|
+
for (const message of session.messages) {
|
|
1020
|
+
const role = message.message?.role === 'user' ? 'user' : 'assistant';
|
|
1021
|
+
const roleName = role === 'user' ? '用户' : '助手';
|
|
1022
|
+
const time = formatDateTime(message.timestamp);
|
|
1023
|
+
html += `
|
|
1024
|
+
<div class="message ${role}">
|
|
1025
|
+
<div class="message-header">
|
|
1026
|
+
<span class="role">${roleName}</span>
|
|
1027
|
+
<span class="time">${escapeHtml(time)}</span>
|
|
1028
|
+
</div>
|
|
1029
|
+
<div class="message-content">
|
|
1030
|
+
`;
|
|
1031
|
+
const content = message.message?.content;
|
|
1032
|
+
if (content) {
|
|
1033
|
+
html += formatHtmlContent(content);
|
|
1034
|
+
}
|
|
1035
|
+
html += `
|
|
1036
|
+
</div>
|
|
1037
|
+
</div>
|
|
1038
|
+
`;
|
|
1039
|
+
}
|
|
1040
|
+
html += `
|
|
1041
|
+
</div>
|
|
1042
|
+
</div>
|
|
1043
|
+
`;
|
|
1044
|
+
}
|
|
1045
|
+
html += `
|
|
1046
|
+
</div>
|
|
1047
|
+
<script>
|
|
1048
|
+
function toggleSession(index) {
|
|
1049
|
+
const session = document.getElementById('session-' + index);
|
|
1050
|
+
session.classList.toggle('collapsed');
|
|
1051
|
+
}
|
|
1052
|
+
</script>
|
|
1053
|
+
</body>
|
|
1054
|
+
</html>
|
|
1055
|
+
`;
|
|
1056
|
+
return html;
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* 转义 HTML 特殊字符
|
|
1060
|
+
*/
|
|
1061
|
+
function escapeHtml(text) {
|
|
1062
|
+
const map = {
|
|
1063
|
+
'&': '&',
|
|
1064
|
+
'<': '<',
|
|
1065
|
+
'>': '>',
|
|
1066
|
+
'"': '"',
|
|
1067
|
+
"'": ''',
|
|
1068
|
+
};
|
|
1069
|
+
return text.replace(/[&<>"']/g, m => map[m]);
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* 格式化 HTML 内容
|
|
1073
|
+
*/
|
|
1074
|
+
function formatHtmlContent(content) {
|
|
1075
|
+
if (!content)
|
|
1076
|
+
return '';
|
|
1077
|
+
if (typeof content === 'string') {
|
|
1078
|
+
return escapeHtml(content);
|
|
1079
|
+
}
|
|
1080
|
+
if (Array.isArray(content)) {
|
|
1081
|
+
return content
|
|
1082
|
+
.map((item) => {
|
|
1083
|
+
if (typeof item === 'string') {
|
|
1084
|
+
return escapeHtml(item);
|
|
1085
|
+
}
|
|
1086
|
+
if (item && typeof item === 'object') {
|
|
1087
|
+
const obj = item;
|
|
1088
|
+
const type = obj.type;
|
|
1089
|
+
switch (type) {
|
|
1090
|
+
case 'text':
|
|
1091
|
+
return obj.text ? `<p>${escapeHtml(String(obj.text))}</p>` : '';
|
|
1092
|
+
case 'tool_use': {
|
|
1093
|
+
const toolName = obj.name || '未知工具';
|
|
1094
|
+
const toolInput = obj.input || {};
|
|
1095
|
+
return `
|
|
1096
|
+
<div class="tool-use">
|
|
1097
|
+
<strong>🔧 工具: ${escapeHtml(String(toolName))}</strong>
|
|
1098
|
+
<pre><code>${escapeHtml(JSON.stringify(toolInput, null, 2))}</code></pre>
|
|
1099
|
+
</div>
|
|
1100
|
+
`;
|
|
1101
|
+
}
|
|
1102
|
+
case 'tool_result': {
|
|
1103
|
+
const toolContent = obj.content;
|
|
1104
|
+
const isError = !!obj.is_error;
|
|
1105
|
+
let contentStr = '';
|
|
1106
|
+
if (typeof toolContent === 'string') {
|
|
1107
|
+
contentStr = escapeHtml(toolContent);
|
|
1108
|
+
}
|
|
1109
|
+
else if (toolContent) {
|
|
1110
|
+
contentStr = `<pre><code>${escapeHtml(JSON.stringify(toolContent, null, 2))}</code></pre>`;
|
|
1111
|
+
}
|
|
1112
|
+
return `
|
|
1113
|
+
<div class="tool-result ${isError ? 'error' : ''}">
|
|
1114
|
+
<strong>📋 工具结果 ${isError ? '❌ 错误' : ''}</strong>
|
|
1115
|
+
${contentStr}
|
|
1116
|
+
</div>
|
|
1117
|
+
`;
|
|
1118
|
+
}
|
|
1119
|
+
case 'thinking':
|
|
1120
|
+
return obj.thinking
|
|
1121
|
+
? `<div class="thinking">💭 ${escapeHtml(String(obj.thinking))}</div>`
|
|
1122
|
+
: '';
|
|
1123
|
+
case 'redacted_thinking':
|
|
1124
|
+
return '<div class="thinking">🔒 [思考内容已隐藏]</div>';
|
|
1125
|
+
case 'file-history-snapshot':
|
|
1126
|
+
return '<div>📸 文件历史快照</div>';
|
|
1127
|
+
default:
|
|
1128
|
+
return obj.text ? `<p>${escapeHtml(String(obj.text))}</p>` : '';
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return '';
|
|
1132
|
+
})
|
|
1133
|
+
.join('\n');
|
|
1134
|
+
}
|
|
1135
|
+
return '';
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* 过滤会话列表
|
|
1139
|
+
*/
|
|
1140
|
+
function filterSessions(sessions, options) {
|
|
1141
|
+
// 如果指定了会话ID,则只导出指定的会话
|
|
1142
|
+
let filteredSessions = sessions;
|
|
1143
|
+
if (options.sessions.length > 0) {
|
|
1144
|
+
const sessionSet = new Set(options.sessions);
|
|
1145
|
+
filteredSessions = sessions.filter(s => sessionSet.has(s.sessionId));
|
|
1146
|
+
}
|
|
1147
|
+
return filteredSessions;
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* 导出单个项目的会话
|
|
1151
|
+
*/
|
|
1152
|
+
async function exportProjectSessions(projectPath, format, outputDir, usedNames, options = { sessions: [], roles: [], contentTypes: [] }) {
|
|
1153
|
+
// 读取项目的所有会话
|
|
1154
|
+
let sessions = await readProjectSessions(projectPath);
|
|
1155
|
+
if (sessions.length === 0) {
|
|
1156
|
+
return null;
|
|
1157
|
+
}
|
|
1158
|
+
// 根据选项过滤会话
|
|
1159
|
+
sessions = filterSessions(sessions, options);
|
|
1160
|
+
if (sessions.length === 0) {
|
|
1161
|
+
return null;
|
|
1162
|
+
}
|
|
1163
|
+
// 生成文件名(处理新格式的文件扩展名)
|
|
1164
|
+
const fileName = generateExportFileName(projectPath, format, usedNames);
|
|
1165
|
+
const fullPath = path.join(outputDir, fileName);
|
|
1166
|
+
// 生成导出内容
|
|
1167
|
+
let content;
|
|
1168
|
+
let messageCount = 0;
|
|
1169
|
+
if (format === 'json') {
|
|
1170
|
+
content = convertToJson(projectPath, sessions, options);
|
|
1171
|
+
messageCount = sessions.reduce((sum, s) => sum + s.messages.length, 0);
|
|
1172
|
+
}
|
|
1173
|
+
else if (format === 'html') {
|
|
1174
|
+
content = convertToHtml(projectPath, sessions, options);
|
|
1175
|
+
messageCount = sessions.reduce((sum, s) => sum + s.messages.length, 0);
|
|
1176
|
+
}
|
|
1177
|
+
else if (format === 'text') {
|
|
1178
|
+
content = convertToText(projectPath, sessions, options);
|
|
1179
|
+
messageCount = sessions.reduce((sum, s) => sum + s.messages.length, 0);
|
|
1180
|
+
}
|
|
1181
|
+
else {
|
|
1182
|
+
content = convertToMarkdown(projectPath, sessions, options);
|
|
1183
|
+
messageCount = sessions.reduce((sum, s) => sum + s.messages.length, 0);
|
|
1184
|
+
}
|
|
1185
|
+
// 写入文件
|
|
1186
|
+
await fs.writeFile(fullPath, content, 'utf-8');
|
|
1187
|
+
return {
|
|
1188
|
+
projectPath,
|
|
1189
|
+
fileName,
|
|
1190
|
+
fullPath,
|
|
1191
|
+
sessionCount: sessions.length,
|
|
1192
|
+
messageCount,
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* 获取默认输出目录
|
|
1197
|
+
*/
|
|
1198
|
+
function getDefaultOutputDir() {
|
|
1199
|
+
return path.join(os.homedir(), 'Desktop', 'lcch-exports');
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* 格式化文件大小
|
|
1203
|
+
*/
|
|
1204
|
+
function formatFileSize(bytes) {
|
|
1205
|
+
if (bytes === 0)
|
|
1206
|
+
return '0 B';
|
|
1207
|
+
const k = 1024;
|
|
1208
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
1209
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1210
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
1211
|
+
}
|