import * as vscode from 'vscode'; import * as path from 'path'; import { ChatMessage, TaskMeta, TaskSession, ConversationMeta, MessageType, UserMessage, AiMessage, SystemMessage, ToolExecutionResultMessage, CompactionSummaryMessage } from '../types/chatHistory'; import { CompactedMemory, CompactedMessage } from '../types/memory'; /** * 会话历史管理器 * 按照设计文档实现:~/.iccoder/projects/{项目路径编码}/{taskId}/ */ export class ChatHistoryManager { private static instance: ChatHistoryManager; private baseDir: string; // ~/.iccoder private currentTaskId: string | null = null; private currentProjectPath: string | null = null; // 存储每个面板的任务信息(taskId 和 projectPath) private panelTaskMap: Map = new Map(); // 追踪压缩后产生的新消息 private newMessagesSinceCompaction: CompactedMessage[] = []; private constructor() { // 设置存储路径: ~/.iccoder const userHome = process.env.USERPROFILE || process.env.HOME || ''; this.baseDir = path.join(userHome, '.iccoder'); this.ensureBaseDir(); } /** * 项目路径编码 * 规则: * - 替换 \ 和 / 为 -- * - 替换 : 为空 * 例如:C:\Users\admin\Documents\Project -> C--Users--admin--Documents--Project */ private encodeProjectPath(projectPath: string): string { return projectPath .replace(/\\/g, '--') // 替换反斜杠为 -- .replace(/\//g, '--') // 替换正斜杠为 -- .replace(/:/g, ''); // 移除冒号 } /** * 生成任务ID * 格式:task_{date}_{sequence} */ private generateTaskId(): string { const date = new Date().toISOString().split('T')[0].replace(/-/g, ''); const sequence = Math.random().toString(36).substr(2, 6); return `task_${date}_${sequence}`; } /** * 获取任务目录路径 */ private getTaskDir(projectPath: string, taskId: string): string { const encodedPath = this.encodeProjectPath(projectPath); return path.join(this.baseDir, 'projects', encodedPath, taskId); } /** * 确保基础目录存在 */ private async ensureBaseDir(): Promise { try { const uri = vscode.Uri.file(this.baseDir); try { await vscode.workspace.fs.stat(uri); } catch { // 目录不存在,创建它 await vscode.workspace.fs.createDirectory(uri); console.log(`创建存储目录: ${this.baseDir}`); } } catch (error) { console.error("创建存储目录失败:", error); vscode.window.showErrorMessage("创建会话历史存储目录失败"); } } /** * 确保任务目录存在 */ private async ensureTaskDir(taskDir: string): Promise { try { const uri = vscode.Uri.file(taskDir); try { await vscode.workspace.fs.stat(uri); } catch { // 目录不存在,创建它 await vscode.workspace.fs.createDirectory(uri); console.log(`创建任务目录: ${taskDir}`); } } catch (error) { console.error("创建任务目录失败:", error); throw error; } } /** * 获取单例实例 */ public static getInstance(): ChatHistoryManager { if (!ChatHistoryManager.instance) { ChatHistoryManager.instance = new ChatHistoryManager(); } return ChatHistoryManager.instance; } /** * 为面板设置任务ID */ public setPanelTask(panelId: string, taskId: string, projectPath: string): void { this.panelTaskMap.set(panelId, { taskId, projectPath }); this.currentTaskId = taskId; this.currentProjectPath = projectPath; } /** * 获取面板的任务ID */ public getPanelTask(panelId: string): string | null { const taskInfo = this.panelTaskMap.get(panelId); return taskInfo ? taskInfo.taskId : null; } /** * 切换到指定面板的任务上下文 */ public switchToPanelTask(panelId: string): boolean { const taskInfo = this.panelTaskMap.get(panelId); if (taskInfo) { this.currentTaskId = taskInfo.taskId; this.currentProjectPath = taskInfo.projectPath; return true; } return false; } /** * 移除面板的任务映射 */ public removePanelTask(panelId: string): void { this.panelTaskMap.delete(panelId); } /** * 创建新任务 */ public async createTask(projectPath: string, taskName: string): Promise { const taskId = this.generateTaskId(); const now = new Date().toISOString(); const meta: TaskMeta = { taskId, taskName, projectPath, createdAt: now, updatedAt: now, stats: { credits: 0, totalTokens: 0, inputTokens: 0, outputTokens: 0 } }; this.currentTaskId = taskId; this.currentProjectPath = projectPath; // 创建任务目录 const taskDir = this.getTaskDir(projectPath, taskId); await this.ensureTaskDir(taskDir); // 保存 meta.json await this.saveTaskMeta(meta); // 初始化空的 conversation.json await this.saveConversation([]); return meta; } /** * 保存任务元数据 */ private async saveTaskMeta(meta: TaskMeta): Promise { if (!this.currentTaskId || !this.currentProjectPath) { throw new Error("没有当前任务"); } const taskDir = this.getTaskDir(this.currentProjectPath, this.currentTaskId); const metaPath = path.join(taskDir, 'meta.json'); try { const uri = vscode.Uri.file(metaPath); const content = Buffer.from(JSON.stringify(meta, null, 2), 'utf-8'); await vscode.workspace.fs.writeFile(uri, content); } catch (error) { console.error("保存任务元数据失败:", error); throw error; } } /** * 加载任务元数据 */ private async loadTaskMeta(): Promise { if (!this.currentTaskId || !this.currentProjectPath) { return null; } const taskDir = this.getTaskDir(this.currentProjectPath, this.currentTaskId); const metaPath = path.join(taskDir, 'meta.json'); try { const uri = vscode.Uri.file(metaPath); const content = await vscode.workspace.fs.readFile(uri); const data = Buffer.from(content).toString('utf-8'); return JSON.parse(data); } catch (error) { // 文件不存在或读取失败 return null; } } /** * 保存对话历史(conversation.json) */ private async saveConversation(messages: ChatMessage[]): Promise { if (!this.currentTaskId || !this.currentProjectPath) { throw new Error("没有当前任务"); } const taskDir = this.getTaskDir(this.currentProjectPath, this.currentTaskId); const conversationPath = path.join(taskDir, 'conversation.json'); try { const uri = vscode.Uri.file(conversationPath); const content = Buffer.from(JSON.stringify(messages, null, 2), 'utf-8'); await vscode.workspace.fs.writeFile(uri, content); } catch (error) { console.error("保存对话历史失败:", error); throw error; } } /** * 加载对话历史 */ private async loadConversation(): Promise { if (!this.currentTaskId || !this.currentProjectPath) { return []; } const taskDir = this.getTaskDir(this.currentProjectPath, this.currentTaskId); const conversationPath = path.join(taskDir, 'conversation.json'); try { const uri = vscode.Uri.file(conversationPath); const content = await vscode.workspace.fs.readFile(uri); const data = Buffer.from(content).toString('utf-8'); return JSON.parse(data); } catch (error) { // 文件不存在或读取失败 return []; } } /** * 追加对话元数据(conversation_meta.jsonl) */ private async appendConversationMeta(meta: ConversationMeta): Promise { if (!this.currentTaskId || !this.currentProjectPath) { throw new Error("没有当前任务"); } const taskDir = this.getTaskDir(this.currentProjectPath, this.currentTaskId); const metaPath = path.join(taskDir, 'conversation_meta.jsonl'); try { const uri = vscode.Uri.file(metaPath); const line = JSON.stringify(meta) + '\n'; // 读取现有内容 let existingContent = ''; try { const content = await vscode.workspace.fs.readFile(uri); existingContent = Buffer.from(content).toString('utf-8'); } catch { // 文件不存在,忽略错误 } // 追加新内容 const newContent = existingContent + line; await vscode.workspace.fs.writeFile(uri, Buffer.from(newContent, 'utf-8')); } catch (error) { console.error("追加对话元数据失败:", error); throw error; } } /** * 确保有当前任务,如果没有则抛出错误 */ private async ensureCurrentTask(): Promise { if (!this.currentTaskId || !this.currentProjectPath) { throw new Error("没有当前任务上下文,请确保已正确初始化面板任务"); } } /** * 添加用户消息 */ public async addUserMessage(text: string): Promise { await this.ensureCurrentTask(); const messages = await this.loadConversation(); const userMessage: UserMessage = { type: MessageType.USER, contents: [{ type: "TEXT", text }] }; messages.push(userMessage); await this.saveConversation(messages); // 更新任务元数据 await this.updateTaskTimestamp(); } /** * 添加AI消息 */ public async addAiMessage(text: string, toolRequests?: any[], segments?: any[]): Promise { await this.ensureCurrentTask(); const messages = await this.loadConversation(); const aiMessage: AiMessage = { type: MessageType.AI, text, toolExecutionRequests: toolRequests, segments // 保存完整的 segments 信息 }; messages.push(aiMessage); await this.saveConversation(messages); // 更新任务元数据 await this.updateTaskTimestamp(); } /** * 添加系统消息 */ public async addSystemMessage(text: string): Promise { await this.ensureCurrentTask(); const messages = await this.loadConversation(); const systemMessage: SystemMessage = { type: MessageType.SYSTEM, text }; messages.push(systemMessage); await this.saveConversation(messages); } /** * 添加工具执行结果消息 */ public async addToolExecutionResult(id: string, toolName: string, result: string): Promise { await this.ensureCurrentTask(); const messages = await this.loadConversation(); const toolResultMessage: ToolExecutionResultMessage = { type: MessageType.TOOL_EXECUTION_RESULT, id, toolName, text: result }; messages.push(toolResultMessage); await this.saveConversation(messages); } /** * 记录对话轮次元数据 */ public async recordTurnMeta( turnId: number, usage?: { inputTokens?: number; outputTokens?: number; totalTokens?: number }, model?: string, duration?: number ): Promise { const meta: ConversationMeta = { turnId, timestamp: new Date().toISOString(), usage, model, duration }; await this.appendConversationMeta(meta); // 更新任务统计 if (usage) { await this.updateTaskStats(usage); } } /** * 更新任务时间戳 */ private async updateTaskTimestamp(): Promise { const meta = await this.loadTaskMeta(); if (meta) { meta.updatedAt = new Date().toISOString(); await this.saveTaskMeta(meta); } } /** * 更新任务统计 */ private async updateTaskStats(usage: { inputTokens?: number; outputTokens?: number; totalTokens?: number }): Promise { const meta = await this.loadTaskMeta(); if (meta) { meta.stats.inputTokens += usage.inputTokens || 0; meta.stats.outputTokens += usage.outputTokens || 0; meta.stats.totalTokens += usage.totalTokens || 0; meta.updatedAt = new Date().toISOString(); await this.saveTaskMeta(meta); } } /** * 获取当前任务会话 */ public async getCurrentTaskSession(): Promise { const meta = await this.loadTaskMeta(); if (!meta) { return null; } const messages = await this.loadConversation(); const conversationMeta = await this.loadConversationMeta(); return { meta, messages, conversationMeta }; } /** * 加载对话元数据 */ private async loadConversationMeta(): Promise { if (!this.currentTaskId || !this.currentProjectPath) { return []; } const taskDir = this.getTaskDir(this.currentProjectPath, this.currentTaskId); const metaPath = path.join(taskDir, 'conversation_meta.jsonl'); try { const uri = vscode.Uri.file(metaPath); const content = await vscode.workspace.fs.readFile(uri); const data = Buffer.from(content).toString('utf-8'); return data .split('\n') .filter(line => line.trim()) .map(line => JSON.parse(line)); } catch (error) { // 文件不存在或读取失败 return []; } } /** * 列出项目的所有任务 */ public async listProjectTasks(projectPath: string): Promise { const encodedPath = this.encodeProjectPath(projectPath); const projectDir = path.join(this.baseDir, 'projects', encodedPath); try { const uri = vscode.Uri.file(projectDir); const entries = await vscode.workspace.fs.readDirectory(uri); const tasks: TaskMeta[] = []; for (const [taskId, type] of entries) { if (type === vscode.FileType.Directory) { const metaPath = path.join(projectDir, taskId, 'meta.json'); try { const metaUri = vscode.Uri.file(metaPath); const content = await vscode.workspace.fs.readFile(metaUri); const data = Buffer.from(content).toString('utf-8'); tasks.push(JSON.parse(data)); } catch (error) { console.error(`加载任务 ${taskId} 失败:`, error); // 跳过无效的任务目录 // 尝试清理空目录 try { const taskDirUri = vscode.Uri.file(path.join(projectDir, taskId)); const taskDirEntries = await vscode.workspace.fs.readDirectory(taskDirUri); if (taskDirEntries.length === 0) { // 目录为空,删除它 await vscode.workspace.fs.delete(taskDirUri, { recursive: false }); console.log(`已清理空任务目录: ${taskId}`); } } catch (cleanupError) { // 清理失败,忽略错误 console.warn(`清理任务目录 ${taskId} 失败:`, cleanupError); } } } } return tasks.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() ); } catch (error) { // 目录不存在 return []; } } /** * 切换到指定任务 */ public async switchTask(projectPath: string, taskId: string): Promise { const taskDir = this.getTaskDir(projectPath, taskId); const metaPath = path.join(taskDir, 'meta.json'); try { const uri = vscode.Uri.file(metaPath); await vscode.workspace.fs.stat(uri); this.currentProjectPath = projectPath; this.currentTaskId = taskId; return true; } catch { return false; } } /** * 获取当前任务ID */ public getCurrentTaskId(): string | null { return this.currentTaskId; } /** * 获取基础目录 */ public getBaseDir(): string { return this.baseDir; } /** * 加载指定任务的会话内容 * @param projectPath 项目路径 * @param taskId 任务ID * @returns 任务会话内容,如果任务不存在则返回null */ public async loadTaskSession(projectPath: string, taskId: string): Promise { const taskDir = this.getTaskDir(projectPath, taskId); const metaPath = path.join(taskDir, 'meta.json'); try { // 检查任务是否存在 const metaUri = vscode.Uri.file(metaPath); const metaContent = await vscode.workspace.fs.readFile(metaUri); const meta: TaskMeta = JSON.parse(Buffer.from(metaContent).toString('utf-8')); // 读取会话内容 const conversationPath = path.join(taskDir, 'conversation.json'); let messages: ChatMessage[] = []; try { const conversationUri = vscode.Uri.file(conversationPath); const conversationContent = await vscode.workspace.fs.readFile(conversationUri); messages = JSON.parse(Buffer.from(conversationContent).toString('utf-8')); } catch { // 会话文件不存在,使用空数组 } // 读取会话元数据 const conversationMetaPath = path.join(taskDir, 'conversation_meta.jsonl'); let conversationMeta: ConversationMeta[] = []; try { const metaUri = vscode.Uri.file(conversationMetaPath); const content = await vscode.workspace.fs.readFile(metaUri); const data = Buffer.from(content).toString('utf-8'); conversationMeta = data .split('\n') .filter(line => line.trim()) .map(line => JSON.parse(line)); } catch { // 元数据文件不存在,使用空数组 } return { meta, messages, conversationMeta }; } catch (error) { console.error(`加载任务 ${taskId} 的会话失败:`, error); return null; } } /** * 获取会话历史列表(支持分页) * 返回格式:{ id: taskId, title: 第一句用户消息, timestamp: 创建时间 } * @param projectPath 项目路径 * @param offset 偏移量(从第几条开始,默认0) * @param limit 每页数量(默认10条) * @returns { items: 历史列表, total: 总数, hasMore: 是否还有更多 } */ public async getConversationHistoryList( projectPath: string, offset: number = 0, limit: number = 10 ): Promise<{ items: Array<{ id: string; title: string; timestamp: string }>; total: number; hasMore: boolean; }> { const tasks = await this.listProjectTasks(projectPath); const total = tasks.length; const historyList: Array<{ id: string; title: string; timestamp: string }> = []; // 计算分页范围 const start = offset; const end = Math.min(offset + limit, total); const limitedTasks = tasks.slice(start, end); for (const task of limitedTasks) { // 读取该任务的 conversation.json 获取第一句用户消息 const taskDir = this.getTaskDir(task.projectPath, task.taskId); const conversationPath = path.join(taskDir, 'conversation.json'); try { const uri = vscode.Uri.file(conversationPath); const content = await vscode.workspace.fs.readFile(uri); const data = Buffer.from(content).toString('utf-8'); const messages: ChatMessage[] = JSON.parse(data); // 找到第一条用户消息 const firstUserMessage = messages.find(msg => msg.type === MessageType.USER) as UserMessage | undefined; let title = '未命名会话'; if (firstUserMessage && firstUserMessage.contents && firstUserMessage.contents.length > 0) { const textContent = firstUserMessage.contents.find(c => c.type === 'TEXT'); if (textContent && 'text' in textContent) { // 截取前50个字符作为标题 title = textContent.text.length > 50 ? textContent.text.substring(0, 50) + '...' : textContent.text; } } historyList.push({ id: task.taskId, title, timestamp: task.createdAt }); } catch (error) { console.error(`读取任务 ${task.taskId} 的会话历史失败:`, error); // 如果读取失败,使用任务名称作为标题 historyList.push({ id: task.taskId, title: task.taskName || '未命名会话', timestamp: task.createdAt }); } } // 返回分页结果 return { items: historyList, total, hasMore: end < total }; } // ========== 压缩数据相关方法 ========== /** * 保存压缩数据(存入 conversation.json 作为压缩摘要消息) */ public async saveCompactedData(compacted: CompactedMemory): Promise { // 尝试从多个来源获取 projectPath let projectPath = this.currentProjectPath; if (!projectPath) { for (const [, taskInfo] of this.panelTaskMap) { if (taskInfo.taskId === compacted.taskId) { projectPath = taskInfo.projectPath; break; } } } if (!projectPath) { console.error('[ChatHistoryManager] 无法保存压缩数据:projectPath 为空'); // 通知用户压缩数据保存失败 vscode.window.showWarningMessage( '对话历史压缩数据保存失败:无法确定项目路径。后端重启后可能无法恢复完整对话历史。' ); return; } // 读取现有对话历史 const taskDir = this.getTaskDir(projectPath, compacted.taskId); const conversationPath = path.join(taskDir, 'conversation.json'); let messages: ChatMessage[] = []; try { const uri = vscode.Uri.file(conversationPath); const content = await vscode.workspace.fs.readFile(uri); messages = JSON.parse(Buffer.from(content).toString('utf-8')); } catch { // 文件不存在,使用空数组 } // 版本检查:防止旧版本覆盖新版本(从尾部扫描,与加载逻辑一致) let existingSummary: CompactionSummaryMessage | null = null; for (let i = messages.length - 1; i >= 0; i--) { if (messages[i].type === MessageType.COMPACTION_SUMMARY) { existingSummary = messages[i] as CompactionSummaryMessage; break; } } if (existingSummary && existingSummary.version >= compacted.version) { console.log(`[ChatHistoryManager] 跳过旧版本压缩数据: 现有版本=${existingSummary.version}, 新版本=${compacted.version}`); return; } // 创建压缩摘要消息 const summaryMessage: CompactionSummaryMessage = { type: MessageType.COMPACTION_SUMMARY, summary: compacted.summary, version: compacted.version, compactedAt: compacted.compactedAt, originalMessageCount: compacted.originalMessageCount, compactedMessageCount: compacted.compactedMessageCount }; // 添加到对话历史 messages.push(summaryMessage); // 保存 const uri = vscode.Uri.file(conversationPath); const content = Buffer.from(JSON.stringify(messages, null, 2), 'utf-8'); await vscode.workspace.fs.writeFile(uri, content); // 重置新消息追踪 this.newMessagesSinceCompaction = []; console.log(`[ChatHistoryManager] 压缩摘要已保存到 conversation.json: taskId=${compacted.taskId}`); } /** * 加载压缩数据(从 conversation.json 构建) */ public async loadCompactedData(taskId: string): Promise { // 尝试从多个来源获取 projectPath let projectPath = this.currentProjectPath; if (!projectPath) { for (const [, taskInfo] of this.panelTaskMap) { if (taskInfo.taskId === taskId) { projectPath = taskInfo.projectPath; break; } } } if (!projectPath) { console.log('[ChatHistoryManager] loadCompactedData: projectPath 为空'); return null; } // 读取 conversation.json const taskDir = this.getTaskDir(projectPath, taskId); const conversationPath = path.join(taskDir, 'conversation.json'); try { const uri = vscode.Uri.file(conversationPath); const content = await vscode.workspace.fs.readFile(uri); const messages: ChatMessage[] = JSON.parse(Buffer.from(content).toString('utf-8')); if (messages.length === 0) { console.log('[ChatHistoryManager] conversation.json 为空'); return null; } // 从 conversation.json 构建 CompactedMemory return this.buildCompactedMemoryFromConversation(taskId, messages); } catch { console.log('[ChatHistoryManager] conversation.json 不存在:', conversationPath); return null; } } /** * 从 conversation.json 构建 CompactedMemory */ private buildCompactedMemoryFromConversation(taskId: string, messages: ChatMessage[]): CompactedMemory { // 查找最后一个压缩摘要消息 let lastSummary: CompactionSummaryMessage | null = null; let summaryIndex = -1; for (let i = messages.length - 1; i >= 0; i--) { if (messages[i].type === MessageType.COMPACTION_SUMMARY) { lastSummary = messages[i] as CompactionSummaryMessage; summaryIndex = i; break; } } // 获取摘要后的消息(或全部消息) const recentMessages = summaryIndex >= 0 ? messages.slice(summaryIndex + 1) : messages; // 转换为 CompactedMessage 格式 const compactedMessages: CompactedMessage[] = recentMessages.map(msg => ({ type: this.mapMessageType(msg.type), content: this.extractMessageContent(msg) })); return { taskId, version: lastSummary?.version || Date.now(), compactedAt: lastSummary?.compactedAt || new Date().toISOString(), summary: lastSummary?.summary || '', recentMessages: compactedMessages, originalMessageCount: messages.length, compactedMessageCount: compactedMessages.length }; } /** * 映射消息类型 */ private mapMessageType(type: MessageType): 'USER' | 'AI' | 'SYSTEM' | 'TOOL_RESULT' { switch (type) { case MessageType.USER: return 'USER'; case MessageType.AI: return 'AI'; case MessageType.SYSTEM: return 'SYSTEM'; case MessageType.TOOL_EXECUTION_RESULT: return 'TOOL_RESULT'; default: return 'USER'; } } /** * 提取消息内容 */ private extractMessageContent(msg: ChatMessage): string { switch (msg.type) { case MessageType.USER: return (msg as UserMessage).contents?.[0]?.text || ''; case MessageType.AI: return (msg as AiMessage).text || ''; case MessageType.SYSTEM: return (msg as SystemMessage).text || ''; case MessageType.TOOL_EXECUTION_RESULT: return (msg as ToolExecutionResultMessage).text || ''; default: return ''; } } /** * 获取压缩后产生的新消息 */ public getNewMessagesSinceCompaction(): CompactedMessage[] { return this.newMessagesSinceCompaction; } /** * 追踪新消息(用户消息) */ public trackUserMessage(text: string): void { this.newMessagesSinceCompaction.push({ type: 'USER', content: text }); } /** * 追踪新消息(AI消息) */ public trackAiMessage(text: string): void { this.newMessagesSinceCompaction.push({ type: 'AI', content: text }); } /** * 追踪新消息(工具执行结果) */ public trackToolResult(toolName: string, result: string): void { this.newMessagesSinceCompaction.push({ type: 'TOOL_RESULT', content: `[${toolName}] ${result}` }); } }