Merge remote-tracking branch 'origin/feat/Plugin-front-end' into feat/back-to-front
This commit is contained in:
@ -8,7 +8,8 @@ import {
|
||||
MessageType,
|
||||
UserMessage,
|
||||
AiMessage,
|
||||
SystemMessage
|
||||
SystemMessage,
|
||||
ToolExecutionResultMessage
|
||||
} from '../types/chatHistory';
|
||||
|
||||
/**
|
||||
@ -20,6 +21,8 @@ export class 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 constructor() {
|
||||
// 设置存储路径: ~/.iccoder
|
||||
@ -33,12 +36,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, ''); // 移除冒号
|
||||
}
|
||||
|
||||
/**
|
||||
@ -107,6 +111,43 @@ export class 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新任务
|
||||
*/
|
||||
@ -264,17 +305,11 @@ export class ChatHistoryManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保有当前任务,如果没有则自动创建
|
||||
* 确保有当前任务,如果没有则抛出错误
|
||||
*/
|
||||
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("没有打开的工作区,无法创建任务");
|
||||
}
|
||||
throw new Error("没有当前任务上下文,请确保已正确初始化面板任务");
|
||||
}
|
||||
}
|
||||
|
||||
@ -300,14 +335,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);
|
||||
@ -333,6 +369,24 @@ export class ChatHistoryManager {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录对话轮次元数据
|
||||
*/
|
||||
@ -450,6 +504,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -494,4 +562,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,6 +170,20 @@ async function handleUserMessageWithBackend(
|
||||
});
|
||||
console.log('[MessageHandler] postMessage 返回值:', result);
|
||||
|
||||
// 保存完整的 segments 到历史记录
|
||||
try {
|
||||
// 将完整的 segments 保存到一条 AI 消息中
|
||||
// 这样加载时可以完整还原对话样式
|
||||
const textContent = segments
|
||||
.filter(s => s.type === 'text' && s.content)
|
||||
.map(s => s.content)
|
||||
.join('\n');
|
||||
|
||||
await historyManager.addAiMessage(textContent, undefined, segments);
|
||||
} catch (error) {
|
||||
console.warn("保存AI响应历史失败:", error);
|
||||
}
|
||||
|
||||
resolve();
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user