fix: 修复对话停止和会话记忆保存问题

- apiClient 添加 stopDialog 接口
- dialogService 添加 getSegments/getAccumulatedText 方法
- dialogService.abort 调用后端停止接口
- messageHandler.abortCurrentDialog 保存中止前的对话内容
- userInteraction 添加 getWebviewPanel 方法
- webviewContent 添加 resetSegmentedMessage 命令处理
- 修复停止后新消息覆盖旧消息的问题
This commit is contained in:
XiaoFeng
2025-12-31 11:55:31 +08:00
parent 28b75e8475
commit 0f8674e1c7
8 changed files with 221 additions and 39 deletions

View File

@ -193,7 +193,7 @@ export async function showICHelperPanel(
break;
// 新增:中止对话
case "abortDialog":
abortCurrentDialog();
void abortCurrentDialog();
break;
// 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送)
case "planAction":

View File

@ -126,6 +126,35 @@ 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 }
});
}
/**
* 创建成功的工具结果
*/

View File

@ -10,7 +10,7 @@ import { executeToolCall, createToolExecutorContext, ToolExecutorContext } from
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';
/**
@ -159,31 +159,112 @@ export class DialogSession {
* 加载知识图谱数据
* 从 .iccoder/knowledge.json 读取
*/
private loadKnowledgeData(): string | null {
private async loadKnowledgeData(): Promise<string | null> {
console.log('[DialogSession] loadKnowledgeData 开始执行');
const workspaceFolders = vscode.workspace.workspaceFolders;
// 等待 workspaceFolders 就绪(首次打开窗口/首次触发命令时可能为空)
const workspaceFolders = await this.waitForWorkspaceFolders();
if (!workspaceFolders || workspaceFolders.length === 0) {
console.log('[DialogSession] 没有工作区文件夹');
return null;
}
const workspacePath = workspaceFolders[0].uri.fsPath;
const knowledgePath = path.join(workspacePath, '.iccoder', 'knowledge.json');
console.log('[DialogSession] 知识图谱路径:', knowledgePath);
// 多根工作区场景:优先读取实际存在 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 exists = fs.existsSync(knowledgePath);
console.log('[DialogSession] 文件存在:', exists);
const content = await this.readTextFileWithRetry(knowledgeUri, 5);
if (!content) {
continue;
}
if (exists) {
const content = fs.readFileSync(knowledgePath, 'utf-8');
console.log('[DialogSession] 加载知识图谱成功, 长度:', content.length);
return content;
// 基础校验 + 清洗:避免偶发读取到半截内容导致后端反序列化失败
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;
}
@ -253,7 +334,7 @@ export class DialogSession {
const newMessages = historyManager.getNewMessagesSinceCompaction();
// 加载知识图谱数据
const knowledgeData = this.loadKnowledgeData();
const knowledgeData = await this.loadKnowledgeData();
console.log('[DialogSession] knowledgeData 加载结果:', knowledgeData ? `${knowledgeData.length} 字符` : 'null');
const request: DialogRequest = {
@ -554,6 +635,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;
}
/**

View File

@ -313,22 +313,19 @@ async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string
* 保存知识图谱到 .iccoder/knowledge.json
*/
async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
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<string> {
* 从 .iccoder/knowledge.json 加载知识图谱
*/
async function executeKnowledgeLoad(): Promise<string> {
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];
}
/**

View File

@ -32,6 +32,13 @@ export class UserInteractionManager {
this.webviewPanel = panel;
}
/**
* 获取 WebView 面板
*/
getWebviewPanel(): vscode.WebviewPanel | null {
return this.webviewPanel;
}
/**
* 处理 ask_user 事件
* @param event ask_user 事件数据

View File

@ -273,7 +273,35 @@ export async function handleUserAnswer(
/**
* 中止当前对话
*/
export function abortCurrentDialog(): void {
export async function abortCurrentDialog(): Promise<void> {
if (currentSession) {
// 保存当前已有的对话内容
const segments = currentSession.getSegments();
if (segments && segments.length > 0) {
try {
const historyManager = ChatHistoryManager.getInstance();
const textContent = segments
.filter(s => s.type === 'text' && s.content)
.map(s => s.content)
.join('\n');
// 添加中止标记
const abortedContent = textContent + '\n\n[对话已被用户中止]';
await historyManager.addAiMessage(abortedContent, undefined, segments);
console.log('[MessageHandler] 已保存中止前的对话内容');
} catch (error) {
console.warn('[MessageHandler] 保存中止对话失败:', error);
}
}
}
// 通知 WebView 重置分段消息容器
const panel = userInteractionManager.getWebviewPanel();
if (panel) {
panel.webview.postMessage({ command: 'resetSegmentedMessage' });
console.log('[MessageHandler] 已发送重置分段消息命令');
}
dialogManager.abortCurrentSession();
currentSession = null;
}

View File

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

View File

@ -565,6 +565,12 @@ export function getWebviewContent(iconUri?: string): string {
}
break;
case 'resetSegmentedMessage':
// 重置分段消息容器(停止对话时调用)
console.log('[WebView] 重置分段消息容器');
currentSegmentedMessage = null;
break;
case 'hideLoading':
// 隐藏加载指示器
hideLoadingIndicator();