Merge branch 'feat/back-to-front' into feat/plugin-front-end
This commit is contained in:
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建成功的工具结果
|
* 创建成功的工具结果
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWorkspaceFolder(): vscode.WorkspaceFolder | undefined {
|
||||||
|
const folders = vscode.workspace.workspaceFolders;
|
||||||
|
if (!folders || folders.length === 0) {
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = fs.readFileSync(knowledgePath, 'utf-8');
|
const activeUri = vscode.window.activeTextEditor?.document?.uri;
|
||||||
return content;
|
const activeFolder = activeUri ? vscode.workspace.getWorkspaceFolder(activeUri) : undefined;
|
||||||
|
return activeFolder ?? folders[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 格式) ==============
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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
42
src/types/memory.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -90,7 +90,7 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
|||||||
break;
|
break;
|
||||||
// 新增:中止对话
|
// 新增:中止对话
|
||||||
case "abortDialog":
|
case "abortDialog":
|
||||||
abortCurrentDialog();
|
void abortCurrentDialog();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user