diff --git a/src/config/settings.ts b/src/config/settings.ts index 15ce17d..442ebc3 100644 --- a/src/config/settings.ts +++ b/src/config/settings.ts @@ -1,9 +1,15 @@ /** * 配置管理 - * 后端地址已预配置,用户无需手动设置 + * 支持 dev(本地开发)和 test(测试服务器)两种环境 */ import * as vscode from "vscode"; +/** 环境类型 */ +type Environment = "dev" | "test" | "prod"; + +/** 当前环境 - 修改这里切换环境 */ +const CURRENT_ENV: Environment = "dev"; + /** 配置项接口 */ export interface IccoderConfig { /** 后端服务地址 */ @@ -14,19 +20,40 @@ export interface IccoderConfig { userId: string; } -/** 默认配置(预配置,不暴露给用户) */ -const DEFAULT_CONFIG: IccoderConfig = { - backendUrl: "http://192.168.1.108:2233", - timeout: 60000, - userId: "default-user", +/** 环境配置 */ +const ENV_CONFIG: Record = { + /** 本地开发环境 */ + dev: { + backendUrl: "http://localhost:2233", + timeout: 60000, + userId: "default-user", + }, + /** 测试服务器环境 */ + test: { + backendUrl: "http://192.168.1.108:2233", + timeout: 60000, + userId: "default-user", + }, + /** 生产环境 */ + prod: { + backendUrl: "https://api.iccoder.com", // TODO: 替换为实际生产地址 + timeout: 60000, + userId: "default-user", + }, }; +/** + * 获取当前环境 + */ +export function getCurrentEnv(): Environment { + return CURRENT_ENV; +} + /** * 获取配置项 - * 直接返回预配置的值,用户无需手动配置 */ export function getConfig(): IccoderConfig { - return { ...DEFAULT_CONFIG }; + return { ...ENV_CONFIG[CURRENT_ENV] }; } /** @@ -34,7 +61,6 @@ export function getConfig(): IccoderConfig { */ export function getApiUrl(path: string): string { const { backendUrl } = getConfig(); - // 确保 URL 格式正确 const baseUrl = backendUrl.endsWith("/") ? backendUrl.slice(0, -1) : backendUrl; diff --git a/src/panels/ICHelperPanel.ts b/src/panels/ICHelperPanel.ts index 7d9c114..df0a0b0 100644 --- a/src/panels/ICHelperPanel.ts +++ b/src/panels/ICHelperPanel.ts @@ -12,7 +12,9 @@ import { handlePlanAction, setPendingPlanExecution, getCurrentTaskId, + setLastTaskId, } from "../utils/messageHandler"; +import { compactDialog } from "../services/apiClient"; import { VCDViewerPanel } from "./VCDViewerPanel"; import { ChatHistoryManager } from "../utils/chatHistoryManager"; import { MessageType } from "../types/chatHistory"; @@ -193,7 +195,40 @@ export async function showICHelperPanel( break; // 新增:中止对话 case "abortDialog": - abortCurrentDialog(); + void abortCurrentDialog(); + break; + // 新增:压缩会话 + case "compressConversation": + { + const taskId = getCurrentTaskId(); + if (taskId) { + compactDialog(taskId) + .then((result) => { + if (result.success) { + panel.webview.postMessage({ + command: "receiveMessage", + text: "✅ 会话压缩完成", + }); + } else { + panel.webview.postMessage({ + command: "receiveMessage", + text: `❌ 压缩失败: ${result.error || "未知错误"}`, + }); + } + }) + .catch((err) => { + panel.webview.postMessage({ + command: "receiveMessage", + text: `❌ 压缩失败: ${err.message || "网络错误"}`, + }); + }); + } else { + panel.webview.postMessage({ + command: "receiveMessage", + text: "❌ 没有活跃的会话", + }); + } + } break; // 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送) case "planAction": @@ -528,6 +563,9 @@ async function selectConversation( return; } + // 设置 lastTaskId,用于压缩等操作 + setLastTaskId(taskId); + // 更新面板的任务映射,确保后续对话保存到正确的任务中 const panelId = (panel as any).__uniqueId; historyManager.setPanelTask(panelId, taskId, workspacePath); diff --git a/src/services/apiClient.ts b/src/services/apiClient.ts index b8ab7b1..bbc312d 100644 --- a/src/services/apiClient.ts +++ b/src/services/apiClient.ts @@ -126,6 +126,55 @@ export async function healthCheck(): Promise<{ status: string }> { }); } +/** + * 停止对话请求 + */ +export interface StopDialogRequest { + taskId: string; +} + +/** + * 停止对话响应 + */ +export interface StopDialogResponse { + success: boolean; + taskId: string; + message?: string; + error?: string; +} + +/** + * 停止对话 + * POST /api/dialog/stop + */ +export async function stopDialog(taskId: string): Promise { + console.log(`[API] 停止对话: taskId=${taskId}`); + return request('/api/dialog/stop', { + method: 'POST', + body: { taskId } + }); +} + +/** 压缩对话响应 */ +export interface CompactDialogResponse { + success: boolean; + taskId: string; + message?: string; + error?: string; +} + +/** + * 手动压缩对话历史 + * POST /api/dialog/compact + */ +export async function compactDialog(taskId: string): Promise { + console.log(`[API] 压缩对话: taskId=${taskId}`); + return request('/api/dialog/compact', { + method: 'POST', + body: { taskId } + }); +} + /** * 创建成功的工具结果 */ diff --git a/src/services/dialogService.ts b/src/services/dialogService.ts index 4d304f7..3f6b6ca 100644 --- a/src/services/dialogService.ts +++ b/src/services/dialogService.ts @@ -3,12 +3,15 @@ * 整合 SSE 通信、工具执行、用户交互 */ import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; import { startStreamDialog, generateTaskId, SSEController, SSECallbacks } from './sseHandler'; import { executeToolCall, createToolExecutorContext, ToolExecutorContext } from './toolExecutor'; import { userInteractionManager } from './userInteraction'; import { getConfig } from '../config/settings'; import type { DialogRequest, ToolCallRequest, AskUserEvent, RunMode, ToolConfirmEvent, PlanConfirmEvent } from '../types/api'; -import { submitToolConfirm, submitAnswer } from './apiClient'; +import { submitToolConfirm, submitAnswer, stopDialog } from './apiClient'; +import { ChatHistoryManager } from '../utils/chatHistoryManager'; /** * 消息段落类型 @@ -70,6 +73,8 @@ export interface DialogCallbacks { onError?: (message: string) => void; /** 通知消息 */ onNotification?: (message: string) => void; + /** 上下文使用量更新 */ + onContextUsage?: (data: { currentTokens: number; maxTokens: number; percentage: number }) => void; } /** @@ -152,6 +157,120 @@ export class DialogSession { return this.isActive; } + /** + * 加载知识图谱数据 + * 从 .iccoder/knowledge.json 读取 + */ + private async loadKnowledgeData(): Promise { + console.log('[DialogSession] loadKnowledgeData 开始执行'); + + // 等待 workspaceFolders 就绪(首次打开窗口/首次触发命令时可能为空) + const workspaceFolders = await this.waitForWorkspaceFolders(); + if (!workspaceFolders || workspaceFolders.length === 0) { + console.log('[DialogSession] 没有工作区文件夹'); + return null; + } + + // 多根工作区场景:优先读取实际存在 knowledge.json 的根目录 + for (const folder of this.getWorkspaceFolderCandidates(workspaceFolders)) { + const knowledgeUri = vscode.Uri.joinPath(folder.uri, '.iccoder', 'knowledge.json'); + console.log('[DialogSession] 知识图谱 URI:', knowledgeUri.toString()); + + try { + const content = await this.readTextFileWithRetry(knowledgeUri, 5); + if (!content) { + continue; + } + + // 基础校验 + 清洗:避免偶发读取到半截内容导致后端反序列化失败 + try { + const parsed = JSON.parse(content) as any; + + // 兼容:后端 KnowledgeGraph.isEmpty() 可能被序列化为 "empty",老后端反序列化会失败 + if (parsed && typeof parsed === 'object' && 'empty' in parsed) { + delete parsed.empty; + } + + const sanitized = JSON.stringify(parsed); + console.log('[DialogSession] 知识图谱已清洗, sanitizedLen:', sanitized.length); + return sanitized; + } catch (e) { + console.warn('[DialogSession] 知识图谱 JSON 解析失败,跳过本次读取:', e); + continue; + } + } catch (error) { + console.warn('[DialogSession] 加载知识图谱失败:', error); + } + } + + return null; + } + + private async waitForWorkspaceFolders(): Promise { + for (let i = 0; i < 10; i++) { + const folders = vscode.workspace.workspaceFolders; + if (folders && folders.length > 0) { + return folders; + } + await new Promise(resolve => setTimeout(resolve, 100)); + } + return vscode.workspace.workspaceFolders; + } + + private getWorkspaceFolderCandidates( + workspaceFolders: readonly vscode.WorkspaceFolder[] + ): vscode.WorkspaceFolder[] { + const result: vscode.WorkspaceFolder[] = []; + + // 1) 当前激活文件所在的 workspace folder(如果有) + const activeUri = vscode.window.activeTextEditor?.document?.uri; + const activeFolder = activeUri ? vscode.workspace.getWorkspaceFolder(activeUri) : undefined; + if (activeFolder) { + result.push(activeFolder); + } + + // 2) 其它 workspace folders(去重) + for (const folder of workspaceFolders) { + if (!result.some(f => f.uri.toString() === folder.uri.toString())) { + result.push(folder); + } + } + + return result; + } + + private async readTextFileWithRetry(uri: vscode.Uri, maxAttempts: number): Promise { + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + const bytes = await vscode.workspace.fs.readFile(uri); + const text = Buffer.from(bytes).toString('utf-8'); + if (!text || !text.trim()) { + return null; + } + return text; + } catch (error) { + // 文件不存在:不是错误,直接返回 null + if (error instanceof vscode.FileSystemError && error.code === 'FileNotFound') { + return null; + } + + const retryable = + (error instanceof vscode.FileSystemError && error.code === 'Unavailable') || + (typeof (error as any)?.code === 'string' && ['EBUSY', 'EPERM', 'EACCES'].includes((error as any).code)); + + if (!retryable || attempt >= maxAttempts) { + throw error; + } + + const delayMs = 50 * attempt; + console.log(`[DialogSession] 读取知识图谱失败(可重试): attempt=${attempt}/${maxAttempts}, delay=${delayMs}ms`); + await new Promise(resolve => setTimeout(resolve, delayMs)); + } + } + + return null; + } + /** * 获取工具操作描述(用于确认对话框) */ @@ -210,13 +329,29 @@ export class DialogSession { this.currentTextSegment = null; const config = getConfig(); + + // 获取压缩数据和新消息(用于后端重启后恢复) + const historyManager = ChatHistoryManager.getInstance(); + const compactedData = await historyManager.loadCompactedData(this.taskId); + const newMessages = historyManager.getNewMessagesSinceCompaction(); + + // 加载知识图谱数据 + const knowledgeData = await this.loadKnowledgeData(); + console.log('[DialogSession] knowledgeData 加载结果:', knowledgeData ? `${knowledgeData.length} 字符` : 'null'); + const request: DialogRequest = { taskId: this.taskId, message, userId: config.userId, - mode: mode || 'agent' + mode: mode || 'agent', + compactedData: compactedData || undefined, + newMessages: newMessages.length > 0 ? newMessages : undefined, + knowledgeData: knowledgeData || undefined }; + // 追踪用户消息 + historyManager.trackUserMessage(message); + const sseCallbacks: SSECallbacks = { onTextDelta: (data) => { this.accumulatedText += data.text; @@ -420,6 +555,12 @@ export class DialogSession { onComplete: (data) => { this.isActive = false; this.finalizeTextSegment(); + + // 追踪 AI 消息(用于后端重启后恢复) + if (this.accumulatedText) { + historyManager.trackAiMessage(this.accumulatedText); + } + // 发送所有段落 callbacks.onComplete?.(this.segments); }, @@ -500,6 +641,17 @@ export class DialogSession { } }, + onMemoryCompacted: async (data) => { + console.log('[DialogSession] onMemoryCompacted:', data.taskId); + // 保存压缩数据到本地 + await historyManager.saveCompactedData(data.compactedData); + }, + + onContextUsage: (data) => { + console.log('[DialogSession] onContextUsage:', data.currentTokens, '/', data.maxTokens); + callbacks.onContextUsage?.(data); + }, + onOpen: () => { console.log('[DialogSession] SSE 连接已建立'); }, @@ -530,6 +682,25 @@ export class DialogSession { } this.isActive = false; userInteractionManager.cancelAll(); + + // 通知后端停止处理 + stopDialog(this.taskId).catch(err => { + console.warn('[DialogSession] 停止对话请求失败:', err); + }); + } + + /** + * 获取当前的消息段落(用于中止时保存) + */ + getSegments(): MessageSegment[] { + return this.segments; + } + + /** + * 获取累积的文本内容 + */ + getAccumulatedText(): string { + return this.accumulatedText; } /** diff --git a/src/services/sseHandler.ts b/src/services/sseHandler.ts index d150c1f..d2c94bf 100644 --- a/src/services/sseHandler.ts +++ b/src/services/sseHandler.ts @@ -27,8 +27,10 @@ import type { AgentStartEvent, AgentProgressEvent, AgentCompleteEvent, - AgentErrorEvent + AgentErrorEvent, + ContextUsageEvent } from '../types/api'; +import type { MemoryCompactedEvent } from '../types/memory'; /** * SSE 事件回调接口 @@ -68,6 +70,10 @@ export interface SSECallbacks { onAgentComplete?: (data: AgentCompleteEvent) => void; /** 子智能体错误 */ onAgentError?: (data: AgentErrorEvent) => void; + /** 记忆压缩完成 */ + onMemoryCompacted?: (data: MemoryCompactedEvent) => void; + /** 上下文使用量更新 */ + onContextUsage?: (data: ContextUsageEvent) => void; /** 连接打开 */ onOpen?: () => void; /** 连接关闭 */ @@ -319,6 +325,12 @@ function dispatchEvent( case 'agent_error': callbacks.onAgentError?.(data as AgentErrorEvent); break; + case 'memory_compacted': + callbacks.onMemoryCompacted?.(data as MemoryCompactedEvent); + break; + case 'context_usage': + callbacks.onContextUsage?.(data as ContextUsageEvent); + break; default: console.log(`[SSE] 未知事件类型: ${eventType}`, data); } diff --git a/src/services/toolExecutor.ts b/src/services/toolExecutor.ts index 51edda2..824a6b0 100644 --- a/src/services/toolExecutor.ts +++ b/src/services/toolExecutor.ts @@ -313,22 +313,19 @@ async function executeWaveformSummary(args: WaveformSummaryArgs): Promise { - const workspaceFolders = vscode.workspace.workspaceFolders; - if (!workspaceFolders || workspaceFolders.length === 0) { + const workspaceFolder = getWorkspaceFolder(); + if (!workspaceFolder) { throw new Error('请先打开一个工作区'); } - const workspacePath = workspaceFolders[0].uri.fsPath; - const iccoderDir = path.join(workspacePath, '.iccoder'); - const knowledgePath = path.join(iccoderDir, 'knowledge.json'); + const iccoderDirUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder'); + const knowledgeUri = vscode.Uri.joinPath(iccoderDirUri, 'knowledge.json'); - // 确保 .iccoder 目录存在 - if (!fs.existsSync(iccoderDir)) { - fs.mkdirSync(iccoderDir, { recursive: true }); - } + // 确保 .iccoder 目录存在(兼容远程/虚拟工作区) + await vscode.workspace.fs.createDirectory(iccoderDirUri); - // 写入知识图谱 - fs.writeFileSync(knowledgePath, args.data, 'utf-8'); + // 写入知识图谱(UTF-8) + await vscode.workspace.fs.writeFile(knowledgeUri, Buffer.from(args.data || '', 'utf-8')); return `知识图谱已保存: .iccoder/knowledge.json`; } @@ -338,21 +335,36 @@ async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise { * 从 .iccoder/knowledge.json 加载知识图谱 */ async function executeKnowledgeLoad(): Promise { - const workspaceFolders = vscode.workspace.workspaceFolders; - if (!workspaceFolders || workspaceFolders.length === 0) { + const workspaceFolder = getWorkspaceFolder(); + if (!workspaceFolder) { throw new Error('请先打开一个工作区'); } - const workspacePath = workspaceFolders[0].uri.fsPath; - const knowledgePath = path.join(workspacePath, '.iccoder', 'knowledge.json'); + const knowledgeUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder', 'knowledge.json'); - // 如果文件不存在,返回空图谱 - if (!fs.existsSync(knowledgePath)) { - return JSON.stringify({ directed: true, nodes: [], links: [] }); + try { + const bytes = await vscode.workspace.fs.readFile(knowledgeUri); + const content = Buffer.from(bytes).toString('utf-8'); + return content; + } catch (error) { + // 文件不存在:返回空图谱 + if (error instanceof vscode.FileSystemError && error.code === 'FileNotFound') { + // 与后端 KnowledgeGraph 结构保持一致(nodes/edges + nodeClass 多态字段) + return JSON.stringify({ taskId: '', version: 1, module: null, nodes: [], edges: [] }); + } + throw error; + } +} + +function getWorkspaceFolder(): vscode.WorkspaceFolder | undefined { + const folders = vscode.workspace.workspaceFolders; + if (!folders || folders.length === 0) { + return undefined; } - const content = fs.readFileSync(knowledgePath, 'utf-8'); - return content; + const activeUri = vscode.window.activeTextEditor?.document?.uri; + const activeFolder = activeUri ? vscode.workspace.getWorkspaceFolder(activeUri) : undefined; + return activeFolder ?? folders[0]; } /** diff --git a/src/services/userInteraction.ts b/src/services/userInteraction.ts index 0879224..a5fea5a 100644 --- a/src/services/userInteraction.ts +++ b/src/services/userInteraction.ts @@ -32,6 +32,13 @@ export class UserInteractionManager { this.webviewPanel = panel; } + /** + * 获取 WebView 面板 + */ + getWebviewPanel(): vscode.WebviewPanel | null { + return this.webviewPanel; + } + /** * 处理 ask_user 事件 * @param event ask_user 事件数据 @@ -60,13 +67,13 @@ export class UserInteractionManager { reject }); - // 设置超时(5分钟) + // 设置超时(2小时) setTimeout(() => { if (this.pendingQuestions.has(askId)) { this.pendingQuestions.delete(askId); reject(new Error('用户回答超时')); } - }, 300000); + }, 7200000); }); } diff --git a/src/types/api.ts b/src/types/api.ts index 7f3d227..991f300 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -3,6 +3,8 @@ * 对应后端 IC Coder Backend 的接口格式 */ +import { CompactedMemory, CompactedMessage } from './memory'; + // ============== 对话请求/响应 ============== /** @@ -27,6 +29,12 @@ export interface DialogRequest { userId: string; /** 运行模式 */ mode: RunMode; + /** 压缩后的记忆数据(用于后端重启后恢复) */ + compactedData?: CompactedMemory; + /** 压缩后产生的新消息 */ + newMessages?: CompactedMessage[]; + /** 知识图谱数据(JSON 字符串,用于恢复知识图谱) */ + knowledgeData?: string; } // ============== SSE 事件类型 ============== @@ -45,6 +53,8 @@ export type SSEEventType = | 'agent_progress' // 子智能体进度 | 'agent_complete' // 子智能体完成 | 'agent_error' // 子智能体错误 + | 'memory_compacted' // 记忆压缩完成 + | 'context_usage' // 上下文使用量 | 'complete' // 对话完成 | 'error' // 错误 | 'warning' // 警告 @@ -172,6 +182,13 @@ export interface AgentErrorEvent { timestamp: number; } +/** context_usage 事件数据 */ +export interface ContextUsageEvent { + currentTokens: number; + maxTokens: number; + percentage: number; +} + // ============== 工具调用协议 (MCP 格式) ============== /** diff --git a/src/types/chatHistory.ts b/src/types/chatHistory.ts index ebb33dd..d0c4750 100644 --- a/src/types/chatHistory.ts +++ b/src/types/chatHistory.ts @@ -5,7 +5,8 @@ export enum MessageType { SYSTEM = "SYSTEM", USER = "USER", AI = "AI", - TOOL_EXECUTION_RESULT = "TOOL_EXECUTION_RESULT" + TOOL_EXECUTION_RESULT = "TOOL_EXECUTION_RESULT", + COMPACTION_SUMMARY = "COMPACTION_SUMMARY" // 压缩摘要 } /** @@ -69,10 +70,22 @@ export interface ToolExecutionResultMessage extends BaseMessage { text: string; // JSON字符串 } +/** + * 压缩摘要消息 + */ +export interface CompactionSummaryMessage extends BaseMessage { + type: MessageType.COMPACTION_SUMMARY; + summary: string; + version: number; + compactedAt: string; + originalMessageCount: number; + compactedMessageCount: number; +} + /** * 联合消息类型 */ -export type ChatMessage = SystemMessage | UserMessage | AiMessage | ToolExecutionResultMessage; +export type ChatMessage = SystemMessage | UserMessage | AiMessage | ToolExecutionResultMessage | CompactionSummaryMessage; /** * 对话轮次元数据 diff --git a/src/types/memory.ts b/src/types/memory.ts new file mode 100644 index 0000000..31844ad --- /dev/null +++ b/src/types/memory.ts @@ -0,0 +1,42 @@ +/** + * 压缩记忆相关类型定义 + */ + +/** + * 压缩后的记忆数据 + */ +export interface CompactedMemory { + taskId: string; + version: number; + compactedAt: string; + summary: string; + recentMessages: CompactedMessage[]; + originalMessageCount: number; + compactedMessageCount: number; +} + +/** + * 压缩消息格式 + */ +export interface CompactedMessage { + type: 'USER' | 'AI' | 'SYSTEM' | 'TOOL_RESULT'; + content: string; + toolCall?: ToolCallInfo; +} + +/** + * 工具调用信息 + */ +export interface ToolCallInfo { + toolName: string; + toolInput: string; + toolOutput?: string; +} + +/** + * 记忆压缩 SSE 事件 + */ +export interface MemoryCompactedEvent { + taskId: string; + compactedData: CompactedMemory; +} diff --git a/src/utils/chatHistoryManager.ts b/src/utils/chatHistoryManager.ts index 17609e1..9f702d4 100644 --- a/src/utils/chatHistoryManager.ts +++ b/src/utils/chatHistoryManager.ts @@ -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 = 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 { + // 尝试从多个来源获取 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 { + // 尝试从多个来源获取 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 + }); + } } diff --git a/src/utils/messageHandler.ts b/src/utils/messageHandler.ts index ff72b6a..a173fb6 100644 --- a/src/utils/messageHandler.ts +++ b/src/utils/messageHandler.ts @@ -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 { + 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); } /** diff --git a/src/views/ICViewProvider.ts b/src/views/ICViewProvider.ts index 286ac2b..74fce75 100644 --- a/src/views/ICViewProvider.ts +++ b/src/views/ICViewProvider.ts @@ -90,7 +90,7 @@ export function showICHelperPanel(context: vscode.ExtensionContext) { break; // 新增:中止对话 case "abortDialog": - abortCurrentDialog(); + void abortCurrentDialog(); break; } }, diff --git a/src/views/webviewContent.ts b/src/views/webviewContent.ts index 3b63b1a..2eb90e6 100644 --- a/src/views/webviewContent.ts +++ b/src/views/webviewContent.ts @@ -562,6 +562,19 @@ export function getWebviewContent(iconUri?: string): string { } break; + case 'resetSegmentedMessage': + // 重置分段消息容器(停止对话时调用) + console.log('[WebView] 重置分段消息容器'); + currentSegmentedMessage = null; + break; + + case 'contextUsage': + // 更新上下文使用量显示 + if (typeof updateContextDisplay === 'function') { + updateContextDisplay(message.currentTokens, message.maxTokens); + } + break; + case 'hideLoading': // 隐藏加载指示器 hideLoadingIndicator();