Merge remote-tracking branch 'origin/feat/Plugin-front-end' into feat/back-to-front

This commit is contained in:
XiaoFeng
2025-12-30 09:42:23 +08:00
27 changed files with 6028 additions and 499 deletions

View File

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

View File

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