feat:实现任务历史加载功能 - 完整还原对话样式

主要改进:
1. 实现selectConversation功能,支持点击任务历史列表加载会话
2. 优化会话存储格式,保存完整的segments信息(包括工具调用)
3. 添加旧格式到新格式的自动转换,兼容历史数据
4. 改进错误处理,自动清理无效的空任务目录
5. 优化路径编码逻辑,确保跨平台一致性
6. 前端支持clearChat、addUserMessage、addAiMessage命令

技术细节:
- 扩展AiMessage数据结构,添加segments字段
- 修改messageHandler保存逻辑,将完整segments保存到一条消息
- 实现loadTaskSession方法,加载指定任务的完整会话
- 添加自动清理机制,删除无效的空任务目录
This commit is contained in:
Roe-xin
2025-12-28 10:38:54 +08:00
parent 25a8ea5aa4
commit 9bdaf34471
7 changed files with 1276 additions and 53 deletions

View File

@ -11,6 +11,8 @@ import {
abortCurrentDialog,
} from "../utils/messageHandler";
import { VCDViewerPanel } from "./VCDViewerPanel";
import { ChatHistoryManager } from "../utils/chatHistoryManager";
import { MessageType } from "../types/chatHistory";
/**
* 创建并显示 IC 助手面板
@ -96,14 +98,14 @@ export function showICHelperPanel(
showICHelperPanel(context, panel.viewColumn);
break;
case "loadConversationHistory":
// 加载会话历史(暂未实现
panel.webview.postMessage({
command: "conversationHistory",
history: [],
});
// 加载会话历史(支持分页
loadConversationHistory(panel, message.offset || 0, message.limit || 10);
break;
case "selectConversation":
// 选择会话(暂未实现)
// 选择会话
if (message.conversationId) {
selectConversation(panel, message.conversationId, context.extensionPath);
}
break;
// 新增:处理用户回答
case "submitAnswer":
@ -302,3 +304,222 @@ function parseVCDSignals(content: string, maxSignals: number = 3) {
return signals;
}
/**
* 加载会话历史(支持分页)
*/
async function loadConversationHistory(
panel: vscode.WebviewPanel,
offset: number = 0,
limit: number = 10
) {
try {
const historyManager = ChatHistoryManager.getInstance();
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!workspacePath) {
// 没有打开的工作区,返回空历史
panel.webview.postMessage({
command: "conversationHistory",
items: [],
total: 0,
hasMore: false,
});
return;
}
// 获取会话历史列表(支持分页)
const result = await historyManager.getConversationHistoryList(
workspacePath,
offset,
limit
);
// 发送会话历史到前端
panel.webview.postMessage({
command: "conversationHistory",
items: result.items,
total: result.total,
hasMore: result.hasMore,
});
} catch (error) {
console.error("加载会话历史失败:", error);
// 发生错误时返回空历史
panel.webview.postMessage({
command: "conversationHistory",
items: [],
total: 0,
hasMore: false,
});
}
}
/**
* 选择并加载指定的会话
*/
async function selectConversation(
panel: vscode.WebviewPanel,
taskId: string,
extensionPath: string
) {
try {
const historyManager = ChatHistoryManager.getInstance();
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!workspacePath) {
vscode.window.showErrorMessage("没有打开的工作区");
return;
}
// 加载任务会话
const taskSession = await historyManager.loadTaskSession(workspacePath, taskId);
if (!taskSession) {
vscode.window.showErrorMessage(`加载任务 ${taskId} 失败: 任务不存在或数据损坏`);
return;
}
// 切换到该任务
const switched = await historyManager.switchTask(workspacePath, taskId);
if (!switched) {
vscode.window.showErrorMessage(`切换到任务 ${taskId} 失败`);
return;
}
// 清空当前聊天界面
panel.webview.postMessage({
command: "clearChat"
});
// 将会话历史消息转换为 segments 格式并发送到前端显示
const segments: any[] = [];
let i = 0;
while (i < taskSession.messages.length) {
const message = taskSession.messages[i];
if (message.type === MessageType.USER) {
// 用户消息 - 如果有累积的 segments先发送
if (segments.length > 0) {
panel.webview.postMessage({
command: "receiveSegments",
segments: [...segments]
});
segments.length = 0;
}
// 发送用户消息
const textContent = message.contents?.find(c => c.type === 'TEXT');
if (textContent && 'text' in textContent) {
panel.webview.postMessage({
command: "addUserMessage",
text: textContent.text
});
}
i++;
} else if (message.type === MessageType.AI) {
// AI消息 - 如果有 segments直接使用
if (message.segments && message.segments.length > 0) {
panel.webview.postMessage({
command: "receiveSegments",
segments: message.segments
});
i++;
} else {
// 旧格式:需要转换为 segments
// 收集连续的 AI 消息、工具调用和工具结果
if (message.text) {
segments.push({
type: 'text',
content: message.text
});
}
// 检查是否有工具调用
if (message.toolExecutionRequests && message.toolExecutionRequests.length > 0) {
for (const toolReq of message.toolExecutionRequests) {
// 查找对应的工具执行结果
let toolResult = '';
if (i + 1 < taskSession.messages.length) {
const nextMsg = taskSession.messages[i + 1];
if (nextMsg.type === MessageType.TOOL_EXECUTION_RESULT &&
nextMsg.id === toolReq.id) {
toolResult = nextMsg.text;
i++; // 跳过工具结果消息
}
}
segments.push({
type: 'tool',
toolName: toolReq.name,
askId: toolReq.id,
toolResult: toolResult
});
}
}
i++;
// 继续收集后续的 AI 消息,直到遇到用户消息或有 segments 的 AI 消息
while (i < taskSession.messages.length) {
const nextMsg = taskSession.messages[i];
if (nextMsg.type === MessageType.USER) {
break;
}
if (nextMsg.type === MessageType.AI) {
if (nextMsg.segments && nextMsg.segments.length > 0) {
break;
}
if (nextMsg.text) {
segments.push({
type: 'text',
content: nextMsg.text
});
}
if (nextMsg.toolExecutionRequests && nextMsg.toolExecutionRequests.length > 0) {
for (const toolReq of nextMsg.toolExecutionRequests) {
let toolResult = '';
if (i + 1 < taskSession.messages.length) {
const resultMsg = taskSession.messages[i + 1];
if (resultMsg.type === MessageType.TOOL_EXECUTION_RESULT &&
resultMsg.id === toolReq.id) {
toolResult = resultMsg.text;
i++; // 跳过工具结果消息
}
}
segments.push({
type: 'tool',
toolName: toolReq.name,
askId: toolReq.id,
toolResult: toolResult
});
}
}
i++;
} else if (nextMsg.type === MessageType.TOOL_EXECUTION_RESULT) {
// 独立的工具结果(没有被上面处理的)
i++;
} else {
i++;
}
}
}
} else {
i++;
}
}
// 发送剩余的 segments
if (segments.length > 0) {
panel.webview.postMessage({
command: "receiveSegments",
segments: segments
});
}
vscode.window.showInformationMessage(`已加载会话: ${taskSession.meta.taskName}`);
} catch (error) {
console.error("选择会话失败:", error);
vscode.window.showErrorMessage(`加载会话失败: ${error}`);
}
}

View File

@ -56,6 +56,7 @@ export interface AiMessage extends BaseMessage {
text?: string;
toolExecutionRequests?: ToolExecutionRequest[];
thinking?: string;
segments?: any[]; // 保存完整的 segments 信息用于还原显示
}
/**

View File

@ -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
};
}
}

View File

@ -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);
}

View File

@ -111,6 +111,10 @@ export function getConversationHistoryBarStyles(): string {
cursor: pointer;
transition: background 0.2s ease;
border-bottom: 1px solid var(--vscode-panel-border);
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.history-item:last-child {
@ -124,15 +128,17 @@ export function getConversationHistoryBarStyles(): string {
.history-item-title {
font-size: 14px;
font-weight: 500;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
.history-item-time {
font-size: 12px;
opacity: 0.7;
white-space: nowrap;
flex-shrink: 0;
}
.history-empty {
@ -142,6 +148,14 @@ export function getConversationHistoryBarStyles(): string {
font-size: 14px;
}
.history-load-more {
padding: 12px 16px;
text-align: center;
color: var(--vscode-descriptionForeground);
font-size: 12px;
border-top: 1px solid var(--vscode-panel-border);
}
.new-conversation-button {
width: 36px;
height: 36px;
@ -199,6 +213,12 @@ export function getConversationHistoryBarScript(): string {
// 会话历史相关变量
let conversationHistory = [];
let currentConversationId = null;
let currentOffset = 0;
let totalHistory = 0;
let hasMoreHistory = false;
let isLoadingHistory = false;
const HISTORY_PAGE_SIZE = 10;
const MAX_HISTORY_ITEMS = 100;
// 切换历史记录下拉菜单
function toggleHistoryDropdown() {
@ -211,33 +231,90 @@ export function getConversationHistoryBarScript(): string {
} else {
menu.classList.add('active');
button.classList.add('active');
// 加载会话历史
loadConversationHistory();
// 重置并加载会话历史
resetAndLoadHistory();
}
}
// 加载会话历史
function loadConversationHistory() {
vscode.postMessage({ command: 'loadConversationHistory' });
// 重置并加载会话历史
function resetAndLoadHistory() {
conversationHistory = [];
currentOffset = 0;
totalHistory = 0;
hasMoreHistory = false;
const historyList = document.getElementById('historyList');
if (historyList) {
historyList.innerHTML = '<div class="history-empty">加载中...</div>';
}
loadMoreHistory();
}
// 渲染会话历史列表
function renderConversationHistory(history) {
conversationHistory = history;
const historyList = document.getElementById('historyList');
// 加载更多会话历史
function loadMoreHistory() {
if (isLoadingHistory || (currentOffset > 0 && !hasMoreHistory)) {
return;
}
if (!history || history.length === 0) {
// 检查是否已达到最大数量
if (currentOffset >= MAX_HISTORY_ITEMS) {
return;
}
isLoadingHistory = true;
vscode.postMessage({
command: 'loadConversationHistory',
offset: currentOffset,
limit: HISTORY_PAGE_SIZE
});
}
// 渲染会话历史列表(支持追加)
function renderConversationHistory(data) {
isLoadingHistory = false;
if (!data || !data.items) {
return;
}
// 追加新数据
conversationHistory = conversationHistory.concat(data.items);
totalHistory = data.total;
hasMoreHistory = data.hasMore;
currentOffset += data.items.length;
const historyList = document.getElementById('historyList');
if (!historyList) {
return;
}
// 如果没有任何历史记录
if (conversationHistory.length === 0) {
historyList.innerHTML = '<div class="history-empty">暂无会话历史</div>';
return;
}
historyList.innerHTML = history.map(item => \`
<div class="history-item"
onclick="selectConversation('\${item.id}')">
// 渲染所有历史记录
historyList.innerHTML = conversationHistory.map(item => \`
<div class="history-item" onclick="selectConversation('\${item.id}')">
<div class="history-item-title">\${item.title || '未命名会话'}</div>
<div class="history-item-time">\${formatTime(item.timestamp)}</div>
</div>
\`).join('');
// 如果还有更多数据,添加"加载更多"提示
if (hasMoreHistory && currentOffset < MAX_HISTORY_ITEMS) {
historyList.innerHTML += \`
<div class="history-load-more" id="loadMoreIndicator">
<span>滚动加载更多...</span>
</div>
\`;
} else if (currentOffset >= MAX_HISTORY_ITEMS && hasMoreHistory) {
historyList.innerHTML += \`
<div class="history-load-more">
<span>已显示最近 \${MAX_HISTORY_ITEMS} 条记录</span>
</div>
\`;
}
}
// 选择会话
@ -291,6 +368,22 @@ export function getConversationHistoryBarScript(): string {
});
}
// 监听下拉菜单滚动事件
const historyDropdownMenu = document.getElementById('historyDropdownMenu');
if (historyDropdownMenu) {
historyDropdownMenu.addEventListener('scroll', () => {
const menu = historyDropdownMenu;
const scrollTop = menu.scrollTop;
const scrollHeight = menu.scrollHeight;
const clientHeight = menu.clientHeight;
// 当滚动到距离底部 50px 时,加载更多
if (scrollHeight - scrollTop - clientHeight < 50) {
loadMoreHistory();
}
});
}
// 点击外部关闭下拉菜单
document.addEventListener('click', (event) => {
const container = document.querySelector('.history-dropdown-container');

View File

@ -525,6 +525,37 @@ export function getWebviewContent(iconUri?: string): string {
showQuestion(message.askId, message.question, message.options);
break;
case 'conversationHistory':
// 渲染会话历史列表(支持分页)
renderConversationHistory({
items: message.items || [],
total: message.total || 0,
hasMore: message.hasMore || false
});
break;
case 'clearChat':
// 清空聊天界面
const messagesContainer = document.getElementById('messages');
if (messagesContainer) {
messagesContainer.innerHTML = '';
}
break;
case 'addUserMessage':
// 添加用户消息
if (message.text) {
addMessage(message.text, 'user');
}
break;
case 'addAiMessage':
// 添加AI消息
if (message.text) {
addMessage(message.text, 'bot');
}
break;
default:
console.log('[WebView] 未处理的消息类型:', message.command);
}