Merge branch 'feat/back-to-front' into feat/plugin-front-end

This commit is contained in:
Roe-xin
2025-12-31 19:00:23 +08:00
14 changed files with 695 additions and 41 deletions

View File

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

View File

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