- P0: 新增工具执行结果追踪(trackToolResult),防止后端重启丢失 - P1: 版本冲突检查改为从尾部扫描,与加载逻辑一致 - P1: projectPath为空时添加用户警告通知 - 追踪工具错误信息,保留失败记录
924 lines
27 KiB
TypeScript
924 lines
27 KiB
TypeScript
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<string, { taskId: string; projectPath: string }> = 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<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;
|
||
}
|
||
|
||
/**
|
||
* 为面板设置任务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<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) {
|
||
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[], segments?: any[]): Promise<void> {
|
||
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<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 addToolExecutionResult(id: string, toolName: string, result: string): Promise<void> {
|
||
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<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);
|
||
// 跳过无效的任务目录
|
||
// 尝试清理空目录
|
||
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<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;
|
||
}
|
||
|
||
/**
|
||
* 加载指定任务的会话内容
|
||
* @param projectPath 项目路径
|
||
* @param taskId 任务ID
|
||
* @returns 任务会话内容,如果任务不存在则返回null
|
||
*/
|
||
public async loadTaskSession(projectPath: string, taskId: string): Promise<TaskSession | null> {
|
||
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<void> {
|
||
// 尝试从多个来源获取 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<CompactedMemory | null> {
|
||
// 尝试从多个来源获取 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}`
|
||
});
|
||
}
|
||
}
|