Merge branch 'feat/back-to-front' into feat/plugin-front-end
This commit is contained in:
@ -9,8 +9,10 @@ import {
|
||||
UserMessage,
|
||||
AiMessage,
|
||||
SystemMessage,
|
||||
ToolExecutionResultMessage
|
||||
ToolExecutionResultMessage,
|
||||
CompactionSummaryMessage
|
||||
} from '../types/chatHistory';
|
||||
import { CompactedMemory, CompactedMessage } from '../types/memory';
|
||||
|
||||
/**
|
||||
* 会话历史管理器
|
||||
@ -23,6 +25,8 @@ export class ChatHistoryManager {
|
||||
private currentProjectPath: string | null = null;
|
||||
// 存储每个面板的任务信息(taskId 和 projectPath)
|
||||
private panelTaskMap: Map<string, { taskId: string; projectPath: string }> = new Map();
|
||||
// 追踪压缩后产生的新消息
|
||||
private newMessagesSinceCompaction: CompactedMessage[] = [];
|
||||
|
||||
private constructor() {
|
||||
// 设置存储路径: ~/.iccoder
|
||||
@ -690,4 +694,203 @@ export class ChatHistoryManager {
|
||||
hasMore: end < total
|
||||
};
|
||||
}
|
||||
|
||||
// ========== 压缩数据相关方法 ==========
|
||||
|
||||
/**
|
||||
* 保存压缩数据(存入 conversation.json 作为压缩摘要消息)
|
||||
*/
|
||||
public async saveCompactedData(compacted: CompactedMemory): Promise<void> {
|
||||
// 尝试从多个来源获取 projectPath
|
||||
let projectPath = this.currentProjectPath;
|
||||
|
||||
if (!projectPath) {
|
||||
for (const [, taskInfo] of this.panelTaskMap) {
|
||||
if (taskInfo.taskId === compacted.taskId) {
|
||||
projectPath = taskInfo.projectPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!projectPath) {
|
||||
console.error('[ChatHistoryManager] 无法保存压缩数据:projectPath 为空');
|
||||
return;
|
||||
}
|
||||
|
||||
// 读取现有对话历史
|
||||
const taskDir = this.getTaskDir(projectPath, compacted.taskId);
|
||||
const conversationPath = path.join(taskDir, 'conversation.json');
|
||||
let messages: ChatMessage[] = [];
|
||||
|
||||
try {
|
||||
const uri = vscode.Uri.file(conversationPath);
|
||||
const content = await vscode.workspace.fs.readFile(uri);
|
||||
messages = JSON.parse(Buffer.from(content).toString('utf-8'));
|
||||
} catch {
|
||||
// 文件不存在,使用空数组
|
||||
}
|
||||
|
||||
// 创建压缩摘要消息
|
||||
const summaryMessage: CompactionSummaryMessage = {
|
||||
type: MessageType.COMPACTION_SUMMARY,
|
||||
summary: compacted.summary,
|
||||
version: compacted.version,
|
||||
compactedAt: compacted.compactedAt,
|
||||
originalMessageCount: compacted.originalMessageCount,
|
||||
compactedMessageCount: compacted.compactedMessageCount
|
||||
};
|
||||
|
||||
// 添加到对话历史
|
||||
messages.push(summaryMessage);
|
||||
|
||||
// 保存
|
||||
const uri = vscode.Uri.file(conversationPath);
|
||||
const content = Buffer.from(JSON.stringify(messages, null, 2), 'utf-8');
|
||||
await vscode.workspace.fs.writeFile(uri, content);
|
||||
|
||||
// 重置新消息追踪
|
||||
this.newMessagesSinceCompaction = [];
|
||||
|
||||
console.log(`[ChatHistoryManager] 压缩摘要已保存到 conversation.json: taskId=${compacted.taskId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载压缩数据(从 conversation.json 构建)
|
||||
*/
|
||||
public async loadCompactedData(taskId: string): Promise<CompactedMemory | null> {
|
||||
// 尝试从多个来源获取 projectPath
|
||||
let projectPath = this.currentProjectPath;
|
||||
|
||||
if (!projectPath) {
|
||||
for (const [, taskInfo] of this.panelTaskMap) {
|
||||
if (taskInfo.taskId === taskId) {
|
||||
projectPath = taskInfo.projectPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!projectPath) {
|
||||
console.log('[ChatHistoryManager] loadCompactedData: projectPath 为空');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 读取 conversation.json
|
||||
const taskDir = this.getTaskDir(projectPath, taskId);
|
||||
const conversationPath = path.join(taskDir, 'conversation.json');
|
||||
|
||||
try {
|
||||
const uri = vscode.Uri.file(conversationPath);
|
||||
const content = await vscode.workspace.fs.readFile(uri);
|
||||
const messages: ChatMessage[] = JSON.parse(Buffer.from(content).toString('utf-8'));
|
||||
|
||||
if (messages.length === 0) {
|
||||
console.log('[ChatHistoryManager] conversation.json 为空');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 从 conversation.json 构建 CompactedMemory
|
||||
return this.buildCompactedMemoryFromConversation(taskId, messages);
|
||||
} catch {
|
||||
console.log('[ChatHistoryManager] conversation.json 不存在:', conversationPath);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 conversation.json 构建 CompactedMemory
|
||||
*/
|
||||
private buildCompactedMemoryFromConversation(taskId: string, messages: ChatMessage[]): CompactedMemory {
|
||||
// 查找最后一个压缩摘要消息
|
||||
let lastSummary: CompactionSummaryMessage | null = null;
|
||||
let summaryIndex = -1;
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].type === MessageType.COMPACTION_SUMMARY) {
|
||||
lastSummary = messages[i] as CompactionSummaryMessage;
|
||||
summaryIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取摘要后的消息(或全部消息)
|
||||
const recentMessages = summaryIndex >= 0
|
||||
? messages.slice(summaryIndex + 1)
|
||||
: messages;
|
||||
|
||||
// 转换为 CompactedMessage 格式
|
||||
const compactedMessages: CompactedMessage[] = recentMessages.map(msg => ({
|
||||
type: this.mapMessageType(msg.type),
|
||||
content: this.extractMessageContent(msg)
|
||||
}));
|
||||
|
||||
return {
|
||||
taskId,
|
||||
version: lastSummary?.version || Date.now(),
|
||||
compactedAt: lastSummary?.compactedAt || new Date().toISOString(),
|
||||
summary: lastSummary?.summary || '',
|
||||
recentMessages: compactedMessages,
|
||||
originalMessageCount: messages.length,
|
||||
compactedMessageCount: compactedMessages.length
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射消息类型
|
||||
*/
|
||||
private mapMessageType(type: MessageType): 'USER' | 'AI' | 'SYSTEM' | 'TOOL_RESULT' {
|
||||
switch (type) {
|
||||
case MessageType.USER: return 'USER';
|
||||
case MessageType.AI: return 'AI';
|
||||
case MessageType.SYSTEM: return 'SYSTEM';
|
||||
case MessageType.TOOL_EXECUTION_RESULT: return 'TOOL_RESULT';
|
||||
default: return 'USER';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取消息内容
|
||||
*/
|
||||
private extractMessageContent(msg: ChatMessage): string {
|
||||
switch (msg.type) {
|
||||
case MessageType.USER:
|
||||
return (msg as UserMessage).contents?.[0]?.text || '';
|
||||
case MessageType.AI:
|
||||
return (msg as AiMessage).text || '';
|
||||
case MessageType.SYSTEM:
|
||||
return (msg as SystemMessage).text || '';
|
||||
case MessageType.TOOL_EXECUTION_RESULT:
|
||||
return (msg as ToolExecutionResultMessage).text || '';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取压缩后产生的新消息
|
||||
*/
|
||||
public getNewMessagesSinceCompaction(): CompactedMessage[] {
|
||||
return this.newMessagesSinceCompaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* 追踪新消息(用户消息)
|
||||
*/
|
||||
public trackUserMessage(text: string): void {
|
||||
this.newMessagesSinceCompaction.push({
|
||||
type: 'USER',
|
||||
content: text
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 追踪新消息(AI消息)
|
||||
*/
|
||||
public trackAiMessage(text: string): void {
|
||||
this.newMessagesSinceCompaction.push({
|
||||
type: 'AI',
|
||||
content: text
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,9 @@ let useBackendService = true;
|
||||
/** 当前对话会话 */
|
||||
let currentSession: DialogSession | null = null;
|
||||
|
||||
/** 最后一个活跃的 taskId(用于压缩等操作) */
|
||||
let lastTaskId: string | null = null;
|
||||
|
||||
/** 待执行的计划(Plan 模式确认后自动执行) */
|
||||
let pendingPlanExecution: {
|
||||
panel: vscode.WebviewPanel;
|
||||
@ -127,6 +130,8 @@ async function handleUserMessageWithBackend(
|
||||
// 创建或复用会话
|
||||
if (!currentSession || !currentSession.active) {
|
||||
currentSession = dialogManager.createSession(extensionPath, reuseTaskId);
|
||||
// 保存 taskId 用于后续操作(如压缩)
|
||||
lastTaskId = currentSession.getTaskId();
|
||||
if (reuseTaskId) {
|
||||
console.log("[MessageHandler] 复用 taskId 创建会话:", reuseTaskId);
|
||||
}
|
||||
@ -273,6 +278,16 @@ async function handleUserMessageWithBackend(
|
||||
onNotification: (message) => {
|
||||
vscode.window.showInformationMessage(message);
|
||||
},
|
||||
|
||||
onContextUsage: (data) => {
|
||||
// 发送上下文使用量到 WebView
|
||||
panel.webview.postMessage({
|
||||
command: "contextUsage",
|
||||
currentTokens: data.currentTokens,
|
||||
maxTokens: data.maxTokens,
|
||||
percentage: data.percentage,
|
||||
});
|
||||
},
|
||||
},
|
||||
mode
|
||||
);
|
||||
@ -295,7 +310,35 @@ export async function handleUserAnswer(
|
||||
/**
|
||||
* 中止当前对话
|
||||
*/
|
||||
export function abortCurrentDialog(): void {
|
||||
export async function abortCurrentDialog(): Promise<void> {
|
||||
if (currentSession) {
|
||||
// 保存当前已有的对话内容
|
||||
const segments = currentSession.getSegments();
|
||||
if (segments && segments.length > 0) {
|
||||
try {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
const textContent = segments
|
||||
.filter((s) => s.type === "text" && s.content)
|
||||
.map((s) => s.content)
|
||||
.join("\n");
|
||||
|
||||
// 添加中止标记
|
||||
const abortedContent = textContent + "\n\n[对话已被用户中止]";
|
||||
await historyManager.addAiMessage(abortedContent, undefined, segments);
|
||||
console.log("[MessageHandler] 已保存中止前的对话内容");
|
||||
} catch (error) {
|
||||
console.warn("[MessageHandler] 保存中止对话失败:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 通知 WebView 重置分段消息容器
|
||||
const panel = userInteractionManager.getWebviewPanel();
|
||||
if (panel) {
|
||||
panel.webview.postMessage({ command: "resetSegmentedMessage" });
|
||||
console.log("[MessageHandler] 已发送重置分段消息命令");
|
||||
}
|
||||
|
||||
dialogManager.abortCurrentSession();
|
||||
currentSession = null;
|
||||
}
|
||||
@ -304,7 +347,15 @@ export function abortCurrentDialog(): void {
|
||||
* 获取当前会话的 taskId
|
||||
*/
|
||||
export function getCurrentTaskId(): string | null {
|
||||
return currentSession?.getTaskId() || null;
|
||||
return currentSession?.getTaskId() || lastTaskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最后的 taskId(加载历史会话时调用)
|
||||
*/
|
||||
export function setLastTaskId(taskId: string): void {
|
||||
lastTaskId = taskId;
|
||||
console.log("[MessageHandler] 设置 lastTaskId:", taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user