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

@ -1,9 +1,15 @@
/** /**
* 配置管理 * 配置管理
* 后端地址已预配置,用户无需手动设置 * 支持 dev本地开发和 test测试服务器两种环境
*/ */
import * as vscode from "vscode"; import * as vscode from "vscode";
/** 环境类型 */
type Environment = "dev" | "test" | "prod";
/** 当前环境 - 修改这里切换环境 */
const CURRENT_ENV: Environment = "dev";
/** 配置项接口 */ /** 配置项接口 */
export interface IccoderConfig { export interface IccoderConfig {
/** 后端服务地址 */ /** 后端服务地址 */
@ -14,19 +20,40 @@ export interface IccoderConfig {
userId: string; userId: string;
} }
/** 默认配置(预配置,不暴露给用户) */ /** 环境配置 */
const DEFAULT_CONFIG: IccoderConfig = { const ENV_CONFIG: Record<Environment, IccoderConfig> = {
/** 本地开发环境 */
dev: {
backendUrl: "http://localhost:2233",
timeout: 60000,
userId: "default-user",
},
/** 测试服务器环境 */
test: {
backendUrl: "http://192.168.1.108:2233", backendUrl: "http://192.168.1.108:2233",
timeout: 60000, timeout: 60000,
userId: "default-user", 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 { 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 { export function getApiUrl(path: string): string {
const { backendUrl } = getConfig(); const { backendUrl } = getConfig();
// 确保 URL 格式正确
const baseUrl = backendUrl.endsWith("/") const baseUrl = backendUrl.endsWith("/")
? backendUrl.slice(0, -1) ? backendUrl.slice(0, -1)
: backendUrl; : backendUrl;

View File

@ -12,7 +12,9 @@ import {
handlePlanAction, handlePlanAction,
setPendingPlanExecution, setPendingPlanExecution,
getCurrentTaskId, getCurrentTaskId,
setLastTaskId,
} from "../utils/messageHandler"; } from "../utils/messageHandler";
import { compactDialog } from "../services/apiClient";
import { VCDViewerPanel } from "./VCDViewerPanel"; import { VCDViewerPanel } from "./VCDViewerPanel";
import { ChatHistoryManager } from "../utils/chatHistoryManager"; import { ChatHistoryManager } from "../utils/chatHistoryManager";
import { MessageType } from "../types/chatHistory"; import { MessageType } from "../types/chatHistory";
@ -193,7 +195,40 @@ export async function showICHelperPanel(
break; break;
// 新增:中止对话 // 新增:中止对话
case "abortDialog": 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; break;
// 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送) // 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送)
case "planAction": case "planAction":
@ -528,6 +563,9 @@ async function selectConversation(
return; return;
} }
// 设置 lastTaskId用于压缩等操作
setLastTaskId(taskId);
// 更新面板的任务映射,确保后续对话保存到正确的任务中 // 更新面板的任务映射,确保后续对话保存到正确的任务中
const panelId = (panel as any).__uniqueId; const panelId = (panel as any).__uniqueId;
historyManager.setPanelTask(panelId, taskId, workspacePath); historyManager.setPanelTask(panelId, taskId, workspacePath);

View File

@ -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<StopDialogResponse> {
console.log(`[API] 停止对话: taskId=${taskId}`);
return request<StopDialogResponse>('/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<CompactDialogResponse> {
console.log(`[API] 压缩对话: taskId=${taskId}`);
return request<CompactDialogResponse>('/api/dialog/compact', {
method: 'POST',
body: { taskId }
});
}
/** /**
* 创建成功的工具结果 * 创建成功的工具结果
*/ */

View File

@ -3,12 +3,15 @@
* 整合 SSE 通信、工具执行、用户交互 * 整合 SSE 通信、工具执行、用户交互
*/ */
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import { startStreamDialog, generateTaskId, SSEController, SSECallbacks } from './sseHandler'; import { startStreamDialog, generateTaskId, SSEController, SSECallbacks } from './sseHandler';
import { executeToolCall, createToolExecutorContext, ToolExecutorContext } from './toolExecutor'; import { executeToolCall, createToolExecutorContext, ToolExecutorContext } from './toolExecutor';
import { userInteractionManager } from './userInteraction'; import { userInteractionManager } from './userInteraction';
import { getConfig } from '../config/settings'; import { getConfig } from '../config/settings';
import type { DialogRequest, ToolCallRequest, AskUserEvent, RunMode, ToolConfirmEvent, PlanConfirmEvent } from '../types/api'; 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; onError?: (message: string) => void;
/** 通知消息 */ /** 通知消息 */
onNotification?: (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; return this.isActive;
} }
/**
* 加载知识图谱数据
* 从 .iccoder/knowledge.json 读取
*/
private async loadKnowledgeData(): Promise<string | null> {
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<readonly vscode.WorkspaceFolder[] | undefined> {
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<string | null> {
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; this.currentTextSegment = null;
const config = getConfig(); 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 = { const request: DialogRequest = {
taskId: this.taskId, taskId: this.taskId,
message, message,
userId: config.userId, 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 = { const sseCallbacks: SSECallbacks = {
onTextDelta: (data) => { onTextDelta: (data) => {
this.accumulatedText += data.text; this.accumulatedText += data.text;
@ -420,6 +555,12 @@ export class DialogSession {
onComplete: (data) => { onComplete: (data) => {
this.isActive = false; this.isActive = false;
this.finalizeTextSegment(); this.finalizeTextSegment();
// 追踪 AI 消息(用于后端重启后恢复)
if (this.accumulatedText) {
historyManager.trackAiMessage(this.accumulatedText);
}
// 发送所有段落 // 发送所有段落
callbacks.onComplete?.(this.segments); 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: () => { onOpen: () => {
console.log('[DialogSession] SSE 连接已建立'); console.log('[DialogSession] SSE 连接已建立');
}, },
@ -530,6 +682,25 @@ export class DialogSession {
} }
this.isActive = false; this.isActive = false;
userInteractionManager.cancelAll(); userInteractionManager.cancelAll();
// 通知后端停止处理
stopDialog(this.taskId).catch(err => {
console.warn('[DialogSession] 停止对话请求失败:', err);
});
}
/**
* 获取当前的消息段落(用于中止时保存)
*/
getSegments(): MessageSegment[] {
return this.segments;
}
/**
* 获取累积的文本内容
*/
getAccumulatedText(): string {
return this.accumulatedText;
} }
/** /**

View File

@ -27,8 +27,10 @@ import type {
AgentStartEvent, AgentStartEvent,
AgentProgressEvent, AgentProgressEvent,
AgentCompleteEvent, AgentCompleteEvent,
AgentErrorEvent AgentErrorEvent,
ContextUsageEvent
} from '../types/api'; } from '../types/api';
import type { MemoryCompactedEvent } from '../types/memory';
/** /**
* SSE 事件回调接口 * SSE 事件回调接口
@ -68,6 +70,10 @@ export interface SSECallbacks {
onAgentComplete?: (data: AgentCompleteEvent) => void; onAgentComplete?: (data: AgentCompleteEvent) => void;
/** 子智能体错误 */ /** 子智能体错误 */
onAgentError?: (data: AgentErrorEvent) => void; onAgentError?: (data: AgentErrorEvent) => void;
/** 记忆压缩完成 */
onMemoryCompacted?: (data: MemoryCompactedEvent) => void;
/** 上下文使用量更新 */
onContextUsage?: (data: ContextUsageEvent) => void;
/** 连接打开 */ /** 连接打开 */
onOpen?: () => void; onOpen?: () => void;
/** 连接关闭 */ /** 连接关闭 */
@ -319,6 +325,12 @@ function dispatchEvent(
case 'agent_error': case 'agent_error':
callbacks.onAgentError?.(data as AgentErrorEvent); callbacks.onAgentError?.(data as AgentErrorEvent);
break; break;
case 'memory_compacted':
callbacks.onMemoryCompacted?.(data as MemoryCompactedEvent);
break;
case 'context_usage':
callbacks.onContextUsage?.(data as ContextUsageEvent);
break;
default: default:
console.log(`[SSE] 未知事件类型: ${eventType}`, data); console.log(`[SSE] 未知事件类型: ${eventType}`, data);
} }

View File

@ -313,22 +313,19 @@ async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string
* 保存知识图谱到 .iccoder/knowledge.json * 保存知识图谱到 .iccoder/knowledge.json
*/ */
async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> { async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
const workspaceFolders = vscode.workspace.workspaceFolders; const workspaceFolder = getWorkspaceFolder();
if (!workspaceFolders || workspaceFolders.length === 0) { if (!workspaceFolder) {
throw new Error('请先打开一个工作区'); throw new Error('请先打开一个工作区');
} }
const workspacePath = workspaceFolders[0].uri.fsPath; const iccoderDirUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder');
const iccoderDir = path.join(workspacePath, '.iccoder'); const knowledgeUri = vscode.Uri.joinPath(iccoderDirUri, 'knowledge.json');
const knowledgePath = path.join(iccoderDir, 'knowledge.json');
// 确保 .iccoder 目录存在 // 确保 .iccoder 目录存在(兼容远程/虚拟工作区)
if (!fs.existsSync(iccoderDir)) { await vscode.workspace.fs.createDirectory(iccoderDirUri);
fs.mkdirSync(iccoderDir, { recursive: true });
}
// 写入知识图谱 // 写入知识图谱UTF-8
fs.writeFileSync(knowledgePath, args.data, 'utf-8'); await vscode.workspace.fs.writeFile(knowledgeUri, Buffer.from(args.data || '', 'utf-8'));
return `知识图谱已保存: .iccoder/knowledge.json`; return `知识图谱已保存: .iccoder/knowledge.json`;
} }
@ -338,21 +335,36 @@ async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
* 从 .iccoder/knowledge.json 加载知识图谱 * 从 .iccoder/knowledge.json 加载知识图谱
*/ */
async function executeKnowledgeLoad(): Promise<string> { async function executeKnowledgeLoad(): Promise<string> {
const workspaceFolders = vscode.workspace.workspaceFolders; const workspaceFolder = getWorkspaceFolder();
if (!workspaceFolders || workspaceFolders.length === 0) { if (!workspaceFolder) {
throw new Error('请先打开一个工作区'); throw new Error('请先打开一个工作区');
} }
const workspacePath = workspaceFolders[0].uri.fsPath; const knowledgeUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder', 'knowledge.json');
const knowledgePath = path.join(workspacePath, '.iccoder', 'knowledge.json');
// 如果文件不存在,返回空图谱 try {
if (!fs.existsSync(knowledgePath)) { const bytes = await vscode.workspace.fs.readFile(knowledgeUri);
return JSON.stringify({ directed: true, nodes: [], links: [] }); 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;
}
} }
const content = fs.readFileSync(knowledgePath, 'utf-8'); function getWorkspaceFolder(): vscode.WorkspaceFolder | undefined {
return content; const folders = vscode.workspace.workspaceFolders;
if (!folders || folders.length === 0) {
return undefined;
}
const activeUri = vscode.window.activeTextEditor?.document?.uri;
const activeFolder = activeUri ? vscode.workspace.getWorkspaceFolder(activeUri) : undefined;
return activeFolder ?? folders[0];
} }
/** /**

View File

@ -32,6 +32,13 @@ export class UserInteractionManager {
this.webviewPanel = panel; this.webviewPanel = panel;
} }
/**
* 获取 WebView 面板
*/
getWebviewPanel(): vscode.WebviewPanel | null {
return this.webviewPanel;
}
/** /**
* 处理 ask_user 事件 * 处理 ask_user 事件
* @param event ask_user 事件数据 * @param event ask_user 事件数据
@ -60,13 +67,13 @@ export class UserInteractionManager {
reject reject
}); });
// 设置超时(5分钟 // 设置超时(2小时
setTimeout(() => { setTimeout(() => {
if (this.pendingQuestions.has(askId)) { if (this.pendingQuestions.has(askId)) {
this.pendingQuestions.delete(askId); this.pendingQuestions.delete(askId);
reject(new Error('用户回答超时')); reject(new Error('用户回答超时'));
} }
}, 300000); }, 7200000);
}); });
} }

View File

@ -3,6 +3,8 @@
* 对应后端 IC Coder Backend 的接口格式 * 对应后端 IC Coder Backend 的接口格式
*/ */
import { CompactedMemory, CompactedMessage } from './memory';
// ============== 对话请求/响应 ============== // ============== 对话请求/响应 ==============
/** /**
@ -27,6 +29,12 @@ export interface DialogRequest {
userId: string; userId: string;
/** 运行模式 */ /** 运行模式 */
mode: RunMode; mode: RunMode;
/** 压缩后的记忆数据(用于后端重启后恢复) */
compactedData?: CompactedMemory;
/** 压缩后产生的新消息 */
newMessages?: CompactedMessage[];
/** 知识图谱数据JSON 字符串,用于恢复知识图谱) */
knowledgeData?: string;
} }
// ============== SSE 事件类型 ============== // ============== SSE 事件类型 ==============
@ -45,6 +53,8 @@ export type SSEEventType =
| 'agent_progress' // 子智能体进度 | 'agent_progress' // 子智能体进度
| 'agent_complete' // 子智能体完成 | 'agent_complete' // 子智能体完成
| 'agent_error' // 子智能体错误 | 'agent_error' // 子智能体错误
| 'memory_compacted' // 记忆压缩完成
| 'context_usage' // 上下文使用量
| 'complete' // 对话完成 | 'complete' // 对话完成
| 'error' // 错误 | 'error' // 错误
| 'warning' // 警告 | 'warning' // 警告
@ -172,6 +182,13 @@ export interface AgentErrorEvent {
timestamp: number; timestamp: number;
} }
/** context_usage 事件数据 */
export interface ContextUsageEvent {
currentTokens: number;
maxTokens: number;
percentage: number;
}
// ============== 工具调用协议 (MCP 格式) ============== // ============== 工具调用协议 (MCP 格式) ==============
/** /**

View File

@ -5,7 +5,8 @@ export enum MessageType {
SYSTEM = "SYSTEM", SYSTEM = "SYSTEM",
USER = "USER", USER = "USER",
AI = "AI", 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字符串 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;
/** /**
* 对话轮次元数据 * 对话轮次元数据

42
src/types/memory.ts Normal file
View File

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

View File

@ -9,8 +9,10 @@ import {
UserMessage, UserMessage,
AiMessage, AiMessage,
SystemMessage, SystemMessage,
ToolExecutionResultMessage ToolExecutionResultMessage,
CompactionSummaryMessage
} from '../types/chatHistory'; } from '../types/chatHistory';
import { CompactedMemory, CompactedMessage } from '../types/memory';
/** /**
* 会话历史管理器 * 会话历史管理器
@ -23,6 +25,8 @@ export class ChatHistoryManager {
private currentProjectPath: string | null = null; private currentProjectPath: string | null = null;
// 存储每个面板的任务信息taskId 和 projectPath // 存储每个面板的任务信息taskId 和 projectPath
private panelTaskMap: Map<string, { taskId: string; projectPath: string }> = new Map(); private panelTaskMap: Map<string, { taskId: string; projectPath: string }> = new Map();
// 追踪压缩后产生的新消息
private newMessagesSinceCompaction: CompactedMessage[] = [];
private constructor() { private constructor() {
// 设置存储路径: ~/.iccoder // 设置存储路径: ~/.iccoder
@ -690,4 +694,203 @@ export class ChatHistoryManager {
hasMore: end < total 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; let currentSession: DialogSession | null = null;
/** 最后一个活跃的 taskId用于压缩等操作 */
let lastTaskId: string | null = null;
/** 待执行的计划Plan 模式确认后自动执行) */ /** 待执行的计划Plan 模式确认后自动执行) */
let pendingPlanExecution: { let pendingPlanExecution: {
panel: vscode.WebviewPanel; panel: vscode.WebviewPanel;
@ -127,6 +130,8 @@ async function handleUserMessageWithBackend(
// 创建或复用会话 // 创建或复用会话
if (!currentSession || !currentSession.active) { if (!currentSession || !currentSession.active) {
currentSession = dialogManager.createSession(extensionPath, reuseTaskId); currentSession = dialogManager.createSession(extensionPath, reuseTaskId);
// 保存 taskId 用于后续操作(如压缩)
lastTaskId = currentSession.getTaskId();
if (reuseTaskId) { if (reuseTaskId) {
console.log("[MessageHandler] 复用 taskId 创建会话:", reuseTaskId); console.log("[MessageHandler] 复用 taskId 创建会话:", reuseTaskId);
} }
@ -273,6 +278,16 @@ async function handleUserMessageWithBackend(
onNotification: (message) => { onNotification: (message) => {
vscode.window.showInformationMessage(message); vscode.window.showInformationMessage(message);
}, },
onContextUsage: (data) => {
// 发送上下文使用量到 WebView
panel.webview.postMessage({
command: "contextUsage",
currentTokens: data.currentTokens,
maxTokens: data.maxTokens,
percentage: data.percentage,
});
},
}, },
mode 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(); dialogManager.abortCurrentSession();
currentSession = null; currentSession = null;
} }
@ -304,7 +347,15 @@ export function abortCurrentDialog(): void {
* 获取当前会话的 taskId * 获取当前会话的 taskId
*/ */
export function getCurrentTaskId(): string | null { 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);
} }
/** /**

View File

@ -90,7 +90,7 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
break; break;
// 新增:中止对话 // 新增:中止对话
case "abortDialog": case "abortDialog":
abortCurrentDialog(); void abortCurrentDialog();
break; break;
} }
}, },

View File

@ -562,6 +562,19 @@ export function getWebviewContent(iconUri?: string): string {
} }
break; break;
case 'resetSegmentedMessage':
// 重置分段消息容器(停止对话时调用)
console.log('[WebView] 重置分段消息容器');
currentSegmentedMessage = null;
break;
case 'contextUsage':
// 更新上下文使用量显示
if (typeof updateContextDisplay === 'function') {
updateContextDisplay(message.currentTokens, message.maxTokens);
}
break;
case 'hideLoading': case 'hideLoading':
// 隐藏加载指示器 // 隐藏加载指示器
hideLoadingIndicator(); hideLoadingIndicator();