feat:搭建本地存储会话历史的框架

- 将会话历史存储在C:\Users\admin\.iccoder文件下
- 在里面又会创建多个文件夹进行存储
This commit is contained in:
Roe-xin
2025-12-15 15:19:36 +08:00
parent ab6d257df2
commit a1a526bb98
6 changed files with 1004 additions and 4 deletions

View File

@ -2,6 +2,7 @@ import * as vscode from "vscode";
import { ICViewProvider } from "./views/ICViewProvider";
import { showICHelperPanel } from "./panels/ICHelperPanel";
import { VCDViewerPanel } from "./panels/VCDViewerPanel";
import { ChatHistoryManager } from "./utils/chatHistoryManager";
export function activate(context: vscode.ExtensionContext) {
console.log("🎉 IC Coder 插件已激活!");
@ -50,6 +51,53 @@ export function activate(context: vscode.ExtensionContext) {
}
);
// 注册命令:查看会话历史
// TODO: 这些命令需要根据新的任务架构重新实现
// 暂时注释掉,等待重新实现
/*
const viewHistoryCommand = vscode.commands.registerCommand(
"ic-coder.viewHistory",
() => {
vscode.window.showInformationMessage("此功能正在重构中,敬请期待");
}
);
const newSessionCommand = vscode.commands.registerCommand(
"ic-coder.newSession",
() => {
vscode.window.showInformationMessage("此功能正在重构中,敬请期待");
}
);
const exportSessionCommand = vscode.commands.registerCommand(
"ic-coder.exportSession",
() => {
vscode.window.showInformationMessage("此功能正在重构中,敬请期待");
}
);
const deleteSessionCommand = vscode.commands.registerCommand(
"ic-coder.deleteSession",
() => {
vscode.window.showInformationMessage("此功能正在重构中,敬请期待");
}
);
const clearHistoryCommand = vscode.commands.registerCommand(
"ic-coder.clearHistory",
() => {
vscode.window.showInformationMessage("此功能正在重构中,敬请期待");
}
);
const searchSessionCommand = vscode.commands.registerCommand(
"ic-coder.searchSession",
() => {
vscode.window.showInformationMessage("此功能正在重构中,敬请期待");
}
);
*/
// 注册侧边栏视图
const viewProvider = new ICViewProvider(context.extensionUri);
const viewRegistration = vscode.window.registerWebviewViewProvider(
@ -62,6 +110,13 @@ export function activate(context: vscode.ExtensionContext) {
openPanelCommand,
openChatCommand,
openVCDViewerCommand,
// TODO: 等待重新实现这些命令
// viewHistoryCommand,
// newSessionCommand,
// exportSessionCommand,
// deleteSessionCommand,
// clearHistoryCommand,
// searchSessionCommand,
viewRegistration
);
}

115
src/types/chatHistory.ts Normal file
View File

@ -0,0 +1,115 @@
/**
* 消息类型枚举(对应 LangChain4j 格式)
*/
export enum MessageType {
SYSTEM = "SYSTEM",
USER = "USER",
AI = "AI",
TOOL_EXECUTION_RESULT = "TOOL_EXECUTION_RESULT"
}
/**
* 工具执行请求
*/
export interface ToolExecutionRequest {
id: string;
name: string;
arguments: string; // JSON字符串
}
/**
* 消息内容用于USER消息
*/
export interface MessageContent {
type: "TEXT";
text: string;
}
/**
* 基础消息接口
*/
export interface BaseMessage {
type: MessageType;
}
/**
* 系统消息
*/
export interface SystemMessage extends BaseMessage {
type: MessageType.SYSTEM;
text: string;
}
/**
* 用户消息
*/
export interface UserMessage extends BaseMessage {
type: MessageType.USER;
contents: MessageContent[];
}
/**
* AI消息
*/
export interface AiMessage extends BaseMessage {
type: MessageType.AI;
text?: string;
toolExecutionRequests?: ToolExecutionRequest[];
thinking?: string;
}
/**
* 工具执行结果消息
*/
export interface ToolExecutionResultMessage extends BaseMessage {
type: MessageType.TOOL_EXECUTION_RESULT;
id: string;
toolName: string;
text: string; // JSON字符串
}
/**
* 联合消息类型
*/
export type ChatMessage = SystemMessage | UserMessage | AiMessage | ToolExecutionResultMessage;
/**
* 对话轮次元数据
*/
export interface ConversationMeta {
turnId: number;
timestamp: string; // ISO 8601格式
usage?: {
inputTokens?: number;
outputTokens?: number;
totalTokens?: number;
};
model?: string;
duration?: number; // 响应耗时(秒)
}
/**
* 任务元数据meta.json
*/
export interface TaskMeta {
taskId: string;
taskName: string;
projectPath: string;
createdAt: string; // ISO 8601格式
updatedAt: string; // ISO 8601格式
stats: {
credits: number;
totalTokens: number;
inputTokens: number;
outputTokens: number;
};
}
/**
* 任务会话(包含所有相关数据)
*/
export interface TaskSession {
meta: TaskMeta;
messages: ChatMessage[]; // conversation.json的内容
conversationMeta: ConversationMeta[]; // conversation_meta.jsonl的内容
}

View File

@ -0,0 +1,497 @@
import * as vscode from 'vscode';
import * as path from 'path';
import {
ChatMessage,
TaskMeta,
TaskSession,
ConversationMeta,
MessageType,
UserMessage,
AiMessage,
SystemMessage
} from '../types/chatHistory';
/**
* 会话历史管理器
* 按照设计文档实现:~/.iccoder/projects/{项目路径编码}/{taskId}/
*/
export class ChatHistoryManager {
private static instance: ChatHistoryManager;
private baseDir: string; // ~/.iccoder
private currentTaskId: string | null = null;
private currentProjectPath: string | null = null;
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, '--'); // 替换斜杠为 --
}
/**
* 生成任务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;
}
/**
* 创建新任务
*/
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) {
// 获取当前工作区路径
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (workspacePath) {
await this.createTask(workspacePath, "默认任务");
} else {
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[]): Promise<void> {
await this.ensureCurrentTask();
const messages = await this.loadConversation();
const aiMessage: AiMessage = {
type: MessageType.AI,
text,
toolExecutionRequests: toolRequests
};
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 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);
}
}
}
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;
}
}

View File

@ -13,6 +13,7 @@ import {
checkVerilogProject,
checkIverilogAvailable,
} from "./iverilogRunner";
import { ChatHistoryManager } from "./chatHistoryManager";
/**
* 处理用户消息
@ -24,6 +25,10 @@ export async function handleUserMessage(
) {
console.log("收到用户消息:", text);
// 记录用户消息到历史
const historyManager = ChatHistoryManager.getInstance();
await historyManager.addUserMessage(text);
// 检查是否是 VCD 生成命令
if (isVCDGenerationCommand(text)) {
await handleVCDGeneration(panel, extensionPath || "");
@ -44,6 +49,10 @@ export async function handleUserMessage(
// 普通消息处理
console.log("作为普通消息处理");
const reply = getMockReply(text);
// 记录助手回复到历史
await historyManager.addAiMessage(reply);
setTimeout(() => {
panel.webview.postMessage({
command: "receiveMessage",
@ -166,28 +175,36 @@ async function handleFileOperation(
replaceText?: string;
}
) {
const historyManager = ChatHistoryManager.getInstance();
try {
let responseText = "";
switch (operation.type) {
case "create":
await createFile(operation.filePath, operation.content || "");
responseText = `✅ 文件创建成功: ${operation.filePath}`;
panel.webview.postMessage({
command: "receiveMessage",
text: `✅ 文件创建成功: ${operation.filePath}`,
text: responseText,
});
vscode.window.showInformationMessage(
`文件创建成功: ${operation.filePath}`
);
await historyManager.addAiMessage(responseText);
break;
case "delete":
await deleteFile(operation.filePath);
responseText = `✅ 文件删除成功: ${operation.filePath}`;
panel.webview.postMessage({
command: "receiveMessage",
text: `✅ 文件删除成功: ${operation.filePath}`,
text: responseText,
});
vscode.window.showInformationMessage(
`文件删除成功: ${operation.filePath}`
);
await historyManager.addAiMessage(responseText);
break;
case "read":
@ -197,6 +214,7 @@ async function handleFileOperation(
content: content,
filePath: operation.filePath,
});
await historyManager.addAiMessage(`读取文件: ${operation.filePath}`);
break;
case "update":
@ -206,6 +224,7 @@ async function handleFileOperation(
content: currentContent,
filePath: operation.filePath,
});
await historyManager.addAiMessage(`编辑文件: ${operation.filePath}`);
break;
case "rename":
@ -213,13 +232,15 @@ async function handleFileOperation(
throw new Error("缺少新文件名");
}
await renameFile(operation.filePath, operation.newPath);
responseText = `✅ 文件重命名成功: ${operation.filePath}${operation.newPath}`;
panel.webview.postMessage({
command: "receiveMessage",
text: `✅ 文件重命名成功: ${operation.filePath}${operation.newPath}`,
text: responseText,
});
vscode.window.showInformationMessage(
`文件重命名成功: ${operation.filePath}${operation.newPath}`
);
await historyManager.addAiMessage(responseText);
break;
case "replace":
@ -231,13 +252,15 @@ async function handleFileOperation(
operation.searchText,
operation.replaceText
);
responseText = `✅ 文件内容替换成功: ${operation.filePath}`;
panel.webview.postMessage({
command: "receiveMessage",
text: `✅ 文件内容替换成功: ${operation.filePath}`,
text: responseText,
});
vscode.window.showInformationMessage(
`文件内容替换成功: ${operation.filePath}`
);
await historyManager.addAiMessage(responseText);
break;
}
} catch (error) {
@ -247,6 +270,7 @@ async function handleFileOperation(
text: `${errorMsg}`,
});
vscode.window.showErrorMessage(errorMsg);
await historyManager.addAiMessage(`${errorMsg}`);
}
}