diff --git a/src/panels/ICHelperPanel.ts b/src/panels/ICHelperPanel.ts index 7d9c114..daeeaef 100644 --- a/src/panels/ICHelperPanel.ts +++ b/src/panels/ICHelperPanel.ts @@ -193,7 +193,7 @@ export async function showICHelperPanel( break; // 新增:中止对话 case "abortDialog": - abortCurrentDialog(); + void abortCurrentDialog(); break; // 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送) case "planAction": diff --git a/src/services/apiClient.ts b/src/services/apiClient.ts index b8ab7b1..9538b1b 100644 --- a/src/services/apiClient.ts +++ b/src/services/apiClient.ts @@ -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 { + console.log(`[API] 停止对话: taskId=${taskId}`); + return request('/api/dialog/stop', { + method: 'POST', + body: { taskId } + }); +} + /** * 创建成功的工具结果 */ diff --git a/src/services/dialogService.ts b/src/services/dialogService.ts index 7478629..69aac6d 100644 --- a/src/services/dialogService.ts +++ b/src/services/dialogService.ts @@ -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,30 +159,111 @@ export class DialogSession { * 加载知识图谱数据 * 从 .iccoder/knowledge.json 读取 */ - private loadKnowledgeData(): string | null { + private async loadKnowledgeData(): Promise { 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); + try { + 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 { + for (let i = 0; i < 10; i++) { + const folders = vscode.workspace.workspaceFolders; + if (folders && folders.length > 0) { + return folders; + } + await new Promise(resolve => setTimeout(resolve, 100)); + } + return vscode.workspace.workspaceFolders; + } + + private getWorkspaceFolderCandidates( + workspaceFolders: readonly vscode.WorkspaceFolder[] + ): vscode.WorkspaceFolder[] { + const result: vscode.WorkspaceFolder[] = []; + + // 1) 当前激活文件所在的 workspace folder(如果有) + const activeUri = vscode.window.activeTextEditor?.document?.uri; + const activeFolder = activeUri ? vscode.workspace.getWorkspaceFolder(activeUri) : undefined; + if (activeFolder) { + result.push(activeFolder); + } + + // 2) 其它 workspace folders(去重) + for (const folder of workspaceFolders) { + if (!result.some(f => f.uri.toString() === folder.uri.toString())) { + result.push(folder); + } + } + + return result; + } + + private async readTextFileWithRetry(uri: vscode.Uri, maxAttempts: number): Promise { + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + const bytes = await vscode.workspace.fs.readFile(uri); + const text = Buffer.from(bytes).toString('utf-8'); + if (!text || !text.trim()) { + return null; + } + return text; + } catch (error) { + // 文件不存在:不是错误,直接返回 null + if (error instanceof vscode.FileSystemError && error.code === 'FileNotFound') { + return null; + } + + const retryable = + (error instanceof vscode.FileSystemError && error.code === 'Unavailable') || + (typeof (error as any)?.code === 'string' && ['EBUSY', 'EPERM', 'EACCES'].includes((error as any).code)); + + if (!retryable || attempt >= maxAttempts) { + throw error; + } + + const delayMs = 50 * attempt; + console.log(`[DialogSession] 读取知识图谱失败(可重试): attempt=${attempt}/${maxAttempts}, delay=${delayMs}ms`); + await new Promise(resolve => setTimeout(resolve, delayMs)); } - } catch (error) { - console.warn('[DialogSession] 加载知识图谱失败:', error); } 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; } /** diff --git a/src/services/toolExecutor.ts b/src/services/toolExecutor.ts index 51edda2..824a6b0 100644 --- a/src/services/toolExecutor.ts +++ b/src/services/toolExecutor.ts @@ -313,22 +313,19 @@ async function executeWaveformSummary(args: WaveformSummaryArgs): Promise { - const workspaceFolders = vscode.workspace.workspaceFolders; - if (!workspaceFolders || workspaceFolders.length === 0) { + const workspaceFolder = getWorkspaceFolder(); + if (!workspaceFolder) { throw new Error('请先打开一个工作区'); } - const workspacePath = workspaceFolders[0].uri.fsPath; - const iccoderDir = path.join(workspacePath, '.iccoder'); - const knowledgePath = path.join(iccoderDir, 'knowledge.json'); + const iccoderDirUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder'); + const knowledgeUri = vscode.Uri.joinPath(iccoderDirUri, 'knowledge.json'); - // 确保 .iccoder 目录存在 - if (!fs.existsSync(iccoderDir)) { - fs.mkdirSync(iccoderDir, { recursive: true }); - } + // 确保 .iccoder 目录存在(兼容远程/虚拟工作区) + await vscode.workspace.fs.createDirectory(iccoderDirUri); - // 写入知识图谱 - fs.writeFileSync(knowledgePath, args.data, 'utf-8'); + // 写入知识图谱(UTF-8) + await vscode.workspace.fs.writeFile(knowledgeUri, Buffer.from(args.data || '', 'utf-8')); return `知识图谱已保存: .iccoder/knowledge.json`; } @@ -338,21 +335,36 @@ async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise { * 从 .iccoder/knowledge.json 加载知识图谱 */ async function executeKnowledgeLoad(): Promise { - const workspaceFolders = vscode.workspace.workspaceFolders; - if (!workspaceFolders || workspaceFolders.length === 0) { + const workspaceFolder = getWorkspaceFolder(); + if (!workspaceFolder) { throw new Error('请先打开一个工作区'); } - const workspacePath = workspaceFolders[0].uri.fsPath; - const knowledgePath = path.join(workspacePath, '.iccoder', 'knowledge.json'); + const knowledgeUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder', 'knowledge.json'); - // 如果文件不存在,返回空图谱 - if (!fs.existsSync(knowledgePath)) { - return JSON.stringify({ directed: true, nodes: [], links: [] }); + try { + const bytes = await vscode.workspace.fs.readFile(knowledgeUri); + const content = Buffer.from(bytes).toString('utf-8'); + return content; + } catch (error) { + // 文件不存在:返回空图谱 + if (error instanceof vscode.FileSystemError && error.code === 'FileNotFound') { + // 与后端 KnowledgeGraph 结构保持一致(nodes/edges + nodeClass 多态字段) + return JSON.stringify({ taskId: '', version: 1, module: null, nodes: [], edges: [] }); + } + throw error; + } +} + +function getWorkspaceFolder(): vscode.WorkspaceFolder | undefined { + const folders = vscode.workspace.workspaceFolders; + if (!folders || folders.length === 0) { + return undefined; } - const content = fs.readFileSync(knowledgePath, 'utf-8'); - return content; + const activeUri = vscode.window.activeTextEditor?.document?.uri; + const activeFolder = activeUri ? vscode.workspace.getWorkspaceFolder(activeUri) : undefined; + return activeFolder ?? folders[0]; } /** diff --git a/src/services/userInteraction.ts b/src/services/userInteraction.ts index f5e9a21..ff1dcfb 100644 --- a/src/services/userInteraction.ts +++ b/src/services/userInteraction.ts @@ -32,6 +32,13 @@ export class UserInteractionManager { this.webviewPanel = panel; } + /** + * 获取 WebView 面板 + */ + getWebviewPanel(): vscode.WebviewPanel | null { + return this.webviewPanel; + } + /** * 处理 ask_user 事件 * @param event ask_user 事件数据 diff --git a/src/utils/messageHandler.ts b/src/utils/messageHandler.ts index 368abcf..ead218a 100644 --- a/src/utils/messageHandler.ts +++ b/src/utils/messageHandler.ts @@ -273,7 +273,35 @@ export async function handleUserAnswer( /** * 中止当前对话 */ -export function abortCurrentDialog(): void { +export async function abortCurrentDialog(): Promise { + if (currentSession) { + // 保存当前已有的对话内容 + const segments = currentSession.getSegments(); + if (segments && segments.length > 0) { + try { + const historyManager = ChatHistoryManager.getInstance(); + const textContent = segments + .filter(s => s.type === 'text' && s.content) + .map(s => s.content) + .join('\n'); + + // 添加中止标记 + const abortedContent = textContent + '\n\n[对话已被用户中止]'; + await historyManager.addAiMessage(abortedContent, undefined, segments); + console.log('[MessageHandler] 已保存中止前的对话内容'); + } catch (error) { + console.warn('[MessageHandler] 保存中止对话失败:', error); + } + } + } + + // 通知 WebView 重置分段消息容器 + const panel = userInteractionManager.getWebviewPanel(); + if (panel) { + panel.webview.postMessage({ command: 'resetSegmentedMessage' }); + console.log('[MessageHandler] 已发送重置分段消息命令'); + } + dialogManager.abortCurrentSession(); currentSession = null; } diff --git a/src/views/ICViewProvider.ts b/src/views/ICViewProvider.ts index 286ac2b..74fce75 100644 --- a/src/views/ICViewProvider.ts +++ b/src/views/ICViewProvider.ts @@ -90,7 +90,7 @@ export function showICHelperPanel(context: vscode.ExtensionContext) { break; // 新增:中止对话 case "abortDialog": - abortCurrentDialog(); + void abortCurrentDialog(); break; } }, diff --git a/src/views/webviewContent.ts b/src/views/webviewContent.ts index 691492a..25bc90f 100644 --- a/src/views/webviewContent.ts +++ b/src/views/webviewContent.ts @@ -565,6 +565,12 @@ export function getWebviewContent(iconUri?: string): string { } break; + case 'resetSegmentedMessage': + // 重置分段消息容器(停止对话时调用) + console.log('[WebView] 重置分段消息容器'); + currentSegmentedMessage = null; + break; + case 'hideLoading': // 隐藏加载指示器 hideLoadingIndicator();