Files
IC-Coder-Plugin/src/utils/chatHistoryManager.ts
Roe-xin a1a526bb98 feat:搭建本地存储会话历史的框架
- 将会话历史存储在C:\Users\admin\.iccoder文件下
- 在里面又会创建多个文件夹进行存储
2025-12-15 15:19:36 +08:00

498 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as vscode from 'vscode';
import * as path from 'path';
import {
ChatMessage,
TaskMeta,
TaskSession,
ConversationMeta,
MessageType,
UserMessage,
AiMessage,
SystemMessage
} from '../types/chatHistory';
/**
* 会话历史管理器
* 按照设计文档实现:~/.iccoder/projects/{项目路径编码}/{taskId}/
*/
export class ChatHistoryManager {
private static instance: ChatHistoryManager;
private baseDir: string; // ~/.iccoder
private currentTaskId: string | null = null;
private currentProjectPath: string | null = null;
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, '--'); // 替换斜杠为 --
}
/**
* 生成任务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<void> {
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<void> {
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;
}
/**
* 创建新任务
*/
public async createTask(projectPath: string, taskName: string): Promise<TaskMeta> {
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<void> {
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<TaskMeta | null> {
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<void> {
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<ChatMessage[]> {
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<void> {
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<void> {
if (!this.currentTaskId || !this.currentProjectPath) {
// 获取当前工作区路径
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (workspacePath) {
await this.createTask(workspacePath, "默认任务");
} else {
throw new Error("没有打开的工作区,无法创建任务");
}
}
}
/**
* 添加用户消息
*/
public async addUserMessage(text: string): Promise<void> {
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[]): Promise<void> {
await this.ensureCurrentTask();
const messages = await this.loadConversation();
const aiMessage: AiMessage = {
type: MessageType.AI,
text,
toolExecutionRequests: toolRequests
};
messages.push(aiMessage);
await this.saveConversation(messages);
// 更新任务元数据
await this.updateTaskTimestamp();
}
/**
* 添加系统消息
*/
public async addSystemMessage(text: string): Promise<void> {
await this.ensureCurrentTask();
const messages = await this.loadConversation();
const systemMessage: SystemMessage = {
type: MessageType.SYSTEM,
text
};
messages.push(systemMessage);
await this.saveConversation(messages);
}
/**
* 记录对话轮次元数据
*/
public async recordTurnMeta(
turnId: number,
usage?: { inputTokens?: number; outputTokens?: number; totalTokens?: number },
model?: string,
duration?: number
): Promise<void> {
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<void> {
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<void> {
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<TaskSession | null> {
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<ConversationMeta[]> {
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<TaskMeta[]> {
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);
}
}
}
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<boolean> {
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;
}
}