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; break;
// 新增:中止对话 // 新增:中止对话
case "abortDialog": case "abortDialog":
abortCurrentDialog(); void abortCurrentDialog();
break; break;
// 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送) // 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送)
case "planAction": 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 { 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'; import { ChatHistoryManager } from '../utils/chatHistoryManager';
/** /**
@ -159,30 +159,111 @@ export class DialogSession {
* 加载知识图谱数据 * 加载知识图谱数据
* 从 .iccoder/knowledge.json 读取 * 从 .iccoder/knowledge.json 读取
*/ */
private loadKnowledgeData(): string | null { private async loadKnowledgeData(): Promise<string | null> {
console.log('[DialogSession] loadKnowledgeData 开始执行'); console.log('[DialogSession] loadKnowledgeData 开始执行');
const workspaceFolders = vscode.workspace.workspaceFolders; // 等待 workspaceFolders 就绪(首次打开窗口/首次触发命令时可能为空)
const workspaceFolders = await this.waitForWorkspaceFolders();
if (!workspaceFolders || workspaceFolders.length === 0) { if (!workspaceFolders || workspaceFolders.length === 0) {
console.log('[DialogSession] 没有工作区文件夹'); console.log('[DialogSession] 没有工作区文件夹');
return null; return null;
} }
const workspacePath = workspaceFolders[0].uri.fsPath; // 多根工作区场景:优先读取实际存在 knowledge.json 的根目录
const knowledgePath = path.join(workspacePath, '.iccoder', 'knowledge.json'); for (const folder of this.getWorkspaceFolderCandidates(workspaceFolders)) {
console.log('[DialogSession] 知识图谱路径:', knowledgePath); const knowledgeUri = vscode.Uri.joinPath(folder.uri, '.iccoder', 'knowledge.json');
console.log('[DialogSession] 知识图谱 URI:', knowledgeUri.toString());
try { try {
const exists = fs.existsSync(knowledgePath); const content = await this.readTextFileWithRetry(knowledgeUri, 5);
console.log('[DialogSession] 文件存在:', exists); if (!content) {
continue;
}
if (exists) { // 基础校验 + 清洗:避免偶发读取到半截内容导致后端反序列化失败
const content = fs.readFileSync(knowledgePath, 'utf-8'); try {
console.log('[DialogSession] 加载知识图谱成功, 长度:', content.length); const parsed = JSON.parse(content) as any;
return content;
// 兼容:后端 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));
} }
} catch (error) {
console.warn('[DialogSession] 加载知识图谱失败:', error);
} }
return null; return null;
@ -253,7 +334,7 @@ export class DialogSession {
const newMessages = historyManager.getNewMessagesSinceCompaction(); const newMessages = historyManager.getNewMessagesSinceCompaction();
// 加载知识图谱数据 // 加载知识图谱数据
const knowledgeData = this.loadKnowledgeData(); const knowledgeData = await this.loadKnowledgeData();
console.log('[DialogSession] knowledgeData 加载结果:', knowledgeData ? `${knowledgeData.length} 字符` : 'null'); console.log('[DialogSession] knowledgeData 加载结果:', knowledgeData ? `${knowledgeData.length} 字符` : 'null');
const request: DialogRequest = { const request: DialogRequest = {
@ -554,6 +635,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

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

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 事件数据

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(); dialogManager.abortCurrentSession();
currentSession = null; currentSession = null;
} }

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

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