feat:实现任务历史加载功能 - 完整还原对话样式
主要改进: 1. 实现selectConversation功能,支持点击任务历史列表加载会话 2. 优化会话存储格式,保存完整的segments信息(包括工具调用) 3. 添加旧格式到新格式的自动转换,兼容历史数据 4. 改进错误处理,自动清理无效的空任务目录 5. 优化路径编码逻辑,确保跨平台一致性 6. 前端支持clearChat、addUserMessage、addAiMessage命令 技术细节: - 扩展AiMessage数据结构,添加segments字段 - 修改messageHandler保存逻辑,将完整segments保存到一条消息 - 实现loadTaskSession方法,加载指定任务的完整会话 - 添加自动清理机制,删除无效的空任务目录
This commit is contained in:
@ -8,7 +8,8 @@ import {
|
||||
MessageType,
|
||||
UserMessage,
|
||||
AiMessage,
|
||||
SystemMessage
|
||||
SystemMessage,
|
||||
ToolExecutionResultMessage
|
||||
} from '../types/chatHistory';
|
||||
|
||||
/**
|
||||
@ -33,12 +34,13 @@ export class ChatHistoryManager {
|
||||
* 规则:
|
||||
* - 替换 \ 和 / 为 --
|
||||
* - 替换 : 为空
|
||||
* 例如:C:\Users\admin\Documents\Project -> C--Users-admin-Documents-Project
|
||||
* 例如:C:\Users\admin\Documents\Project -> C--Users--admin--Documents--Project
|
||||
*/
|
||||
private encodeProjectPath(projectPath: string): string {
|
||||
return projectPath
|
||||
.replace(/:/g, '') // 移除冒号
|
||||
.replace(/[/\\]/g, '--'); // 替换斜杠为 --
|
||||
.replace(/\\/g, '--') // 替换反斜杠为 --
|
||||
.replace(/\//g, '--') // 替换正斜杠为 --
|
||||
.replace(/:/g, ''); // 移除冒号
|
||||
}
|
||||
|
||||
/**
|
||||
@ -300,14 +302,15 @@ export class ChatHistoryManager {
|
||||
/**
|
||||
* 添加AI消息
|
||||
*/
|
||||
public async addAiMessage(text: string, toolRequests?: any[]): Promise<void> {
|
||||
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
|
||||
toolExecutionRequests: toolRequests,
|
||||
segments // 保存完整的 segments 信息
|
||||
};
|
||||
|
||||
messages.push(aiMessage);
|
||||
@ -468,6 +471,20 @@ export class ChatHistoryManager {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -512,4 +529,132 @@ export class ChatHistoryManager {
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -170,35 +170,16 @@ async function handleUserMessageWithBackend(
|
||||
});
|
||||
console.log('[MessageHandler] postMessage 返回值:', result);
|
||||
|
||||
// 按照 segments 顺序保存AI响应到历史记录
|
||||
// 保存完整的 segments 到历史记录
|
||||
try {
|
||||
// 遍历 segments,按顺序保存每个段落
|
||||
for (const segment of segments) {
|
||||
if (segment.type === 'text' && segment.content) {
|
||||
// 保存文本消息
|
||||
await historyManager.addAiMessage(segment.content);
|
||||
} else if (segment.type === 'tool') {
|
||||
// 保存工具调用请求(作为AI消息的一部分)
|
||||
const toolRequest = {
|
||||
id: segment.askId || `tool_${Date.now()}`,
|
||||
name: segment.toolName || '',
|
||||
arguments: ''
|
||||
};
|
||||
await historyManager.addAiMessage('', [toolRequest]);
|
||||
// 将完整的 segments 保存到一条 AI 消息中
|
||||
// 这样加载时可以完整还原对话样式
|
||||
const textContent = segments
|
||||
.filter(s => s.type === 'text' && s.content)
|
||||
.map(s => s.content)
|
||||
.join('\n');
|
||||
|
||||
// 如果有工具执行结果,保存工具执行结果
|
||||
if (segment.toolResult) {
|
||||
await historyManager.addToolExecutionResult(
|
||||
toolRequest.id,
|
||||
segment.toolName || '',
|
||||
segment.toolResult
|
||||
);
|
||||
}
|
||||
} else if (segment.type === 'question') {
|
||||
// 保存问题(作为AI消息)
|
||||
await historyManager.addAiMessage(segment.question || '');
|
||||
}
|
||||
}
|
||||
await historyManager.addAiMessage(textContent, undefined, segments);
|
||||
} catch (error) {
|
||||
console.warn("保存AI响应历史失败:", error);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user