From e77194628acf5904e565034081be31e63a4abf58 Mon Sep 17 00:00:00 2001 From: XiaoFeng <117837368+Fzhiyu1@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:42:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=A8=A1=E5=BC=8F=E4=BC=A0=E9=80=92?= =?UTF-8?q?=E5=92=8C=20API=20=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dialogService 接收并传递 mode 参数 - apiClient 构造带 mode 的请求 - messageHandler 从 WebView 消息获取 mode --- src/services/apiClient.ts | 14 +++- src/services/dialogService.ts | 134 ++++++++++++++++++++++++++++++++-- src/utils/messageHandler.ts | 131 +++++++++++++++++++++++++++++++-- 3 files changed, 265 insertions(+), 14 deletions(-) diff --git a/src/services/apiClient.ts b/src/services/apiClient.ts index 585f471..b8ab7b1 100644 --- a/src/services/apiClient.ts +++ b/src/services/apiClient.ts @@ -6,7 +6,7 @@ import * as https from 'https'; import * as http from 'http'; import { URL } from 'url'; import { getApiUrl, getConfig } from '../config/settings'; -import type { ToolCallResult, AnswerRequest, ToolResultResponse, AnswerResponse } from '../types/api'; +import type { ToolCallResult, AnswerRequest, ToolResultResponse, AnswerResponse, ToolConfirmResponse } from '../types/api'; /** * HTTP 请求选项 @@ -103,6 +103,18 @@ export async function submitAnswer(answer: AnswerRequest): Promise { + console.log(`[API] 提交工具确认: confirmId=${response.confirmId}, approved=${response.approved}`); + return request('/api/tool/confirm', { + method: 'POST', + body: response + }); +} + /** * 健康检查 * GET /api/dialog/health diff --git a/src/services/dialogService.ts b/src/services/dialogService.ts index 736730d..ec4551a 100644 --- a/src/services/dialogService.ts +++ b/src/services/dialogService.ts @@ -7,13 +7,14 @@ import { startStreamDialog, generateTaskId, SSEController, SSECallbacks } from ' import { executeToolCall, createToolExecutorContext, ToolExecutorContext } from './toolExecutor'; import { userInteractionManager } from './userInteraction'; import { getConfig } from '../config/settings'; -import type { DialogRequest, ToolCallRequest, AskUserEvent } from '../types/api'; +import type { DialogRequest, ToolCallRequest, AskUserEvent, RunMode, ToolConfirmEvent, PlanConfirmEvent } from '../types/api'; +import { submitToolConfirm, submitAnswer } from './apiClient'; /** * 消息段落类型 */ export interface MessageSegment { - type: 'text' | 'tool' | 'question' | 'agent'; + type: 'text' | 'tool' | 'question' | 'agent' | 'plan'; content?: string; toolName?: string; toolStatus?: 'running' | 'success' | 'error'; @@ -26,6 +27,10 @@ export interface MessageSegment { agentName?: string; agentStatus?: 'running' | 'completed' | 'error'; agentSteps?: AgentStep[]; + // 计划相关字段 + planTitle?: string; + planSteps?: string[]; + planSummary?: string; } /** @@ -51,6 +56,10 @@ export interface DialogCallbacks { onToolComplete?: (toolName: string, result: string) => void; /** 工具执行错误 */ onToolError?: (toolName: string, error: string) => void; + /** 工具确认请求(Ask 模式) */ + onToolConfirm?: (confirmId: number, toolName: string, toolInput: Record) => void; + /** 计划确认请求(Plan 模式) */ + onPlanConfirm?: (confirmId: number, title: string, steps: string[], summary: string) => void; /** 显示问题(ask_user) */ onQuestion?: (askId: string, question: string, options: string[]) => void; /** 实时更新段落(流式过程中) */ @@ -75,8 +84,9 @@ export class DialogSession { private segments: MessageSegment[] = []; private currentTextSegment: MessageSegment | null = null; - constructor(extensionPath: string) { - this.taskId = generateTaskId(); + constructor(extensionPath: string, existingTaskId?: string) { + // 支持复用现有 taskId(用于 Plan 模式确认后继续执行) + this.taskId = existingTaskId || generateTaskId(); this.toolContext = createToolExecutorContext(extensionPath); } @@ -142,12 +152,52 @@ export class DialogSession { return this.isActive; } + /** + * 获取工具操作描述(用于确认对话框) + */ + private getToolDescription(toolName: string, toolInput: Record): string { + const lines: string[] = []; + + switch (toolName) { + case 'file_write': + lines.push(`文件路径: ${toolInput.path || '未知'}`); + if (toolInput.content) { + const content = String(toolInput.content); + lines.push(`内容长度: ${content.length} 字符`); + lines.push(`内容预览: ${content.substring(0, 100)}${content.length > 100 ? '...' : ''}`); + } + break; + case 'file_delete': + lines.push(`删除文件: ${toolInput.path || '未知'}`); + break; + case 'syntax_check': + lines.push('执行语法检查'); + if (toolInput.code) { + const code = String(toolInput.code); + lines.push(`代码长度: ${code.length} 字符`); + } + break; + case 'simulation': + lines.push(`RTL文件: ${toolInput.rtlPath || '未知'}`); + lines.push(`TB文件: ${toolInput.tbPath || '未知'}`); + if (toolInput.duration) { + lines.push(`仿真时长: ${toolInput.duration}`); + } + break; + default: + lines.push(`参数: ${JSON.stringify(toolInput, null, 2)}`); + } + + return lines.join('\n'); + } + /** * 发送消息并开始流式对话 */ async sendMessage( message: string, - callbacks: DialogCallbacks + callbacks: DialogCallbacks, + mode?: RunMode ): Promise { if (this.isActive) { callbacks.onError?.('当前有对话正在进行中'); @@ -164,7 +214,7 @@ export class DialogSession { taskId: this.taskId, message, userId: config.userId, - toolMode: 'AGENT' + mode: mode || 'agent' }; const sseCallbacks: SSECallbacks = { @@ -248,6 +298,72 @@ export class DialogSession { callbacks.onSegmentUpdate?.(this.segments); }, + onToolConfirm: async (data: ToolConfirmEvent) => { + console.log('[DialogSession] onToolConfirm:', data.toolName, data.confirmId); + + // 调用回调通知 UI 显示确认对话框 + callbacks.onToolConfirm?.(data.confirmId, data.toolName, data.toolInput); + + // 使用 VSCode 快速选择框显示确认对话框 + const toolDescription = this.getToolDescription(data.toolName, data.toolInput); + const result = await vscode.window.showWarningMessage( + `确认执行操作: ${data.toolName}`, + { modal: true, detail: toolDescription }, + '确认执行', + '取消' + ); + + const approved = result === '确认执行'; + console.log('[DialogSession] 用户确认结果:', approved); + + // 发送确认响应到后端 + try { + await submitToolConfirm({ + confirmId: data.confirmId, + taskId: this.taskId, + approved + }); + } catch (error) { + console.error('[DialogSession] 发送确认响应失败:', error); + } + }, + + onPlanConfirm: async (data: PlanConfirmEvent) => { + console.log('[DialogSession] onPlanConfirm:', data.title); + + // 结束当前文本段落 + this.finalizeTextSegment(); + + const askId = `ask_${data.confirmId}`; + + // 添加计划段落到聊天界面(包含 askId 用于响应) + this.segments.push({ + type: 'plan', + askId: askId, + planTitle: data.title, + planSteps: data.steps, + planSummary: data.summary + }); + + // 实时发送段落更新 + callbacks.onSegmentUpdate?.(this.segments); + + // 注册问题到前端(类似 askUser),以便用户回答时能找到 + const planEvent = { + askId: askId, + question: `请确认执行计划:${data.title}`, + options: ['确认执行', '修改计划', '取消'] + }; + try { + await userInteractionManager.handleAskUser(planEvent as AskUserEvent, this.taskId); + } catch (error) { + console.error('[DialogSession] 处理计划确认失败:', error); + } + + // 调用回调通知 UI + callbacks.onPlanConfirm?.(data.confirmId, data.title, data.steps, data.summary); + }, + onAskUser: async (data: AskUserEvent) => { this.finalizeTextSegment(); this.segments.push({ @@ -402,13 +518,15 @@ class DialogManager { /** * 创建新会话 + * @param extensionPath 扩展路径 + * @param existingTaskId 可选,复用现有的 taskId(用于 Plan 模式确认后继续执行) */ - createSession(extensionPath: string): DialogSession { + createSession(extensionPath: string, existingTaskId?: string): DialogSession { // 如果有活跃会话,先中止 if (this.currentSession?.active) { this.currentSession.abort(); } - this.currentSession = new DialogSession(extensionPath); + this.currentSession = new DialogSession(extensionPath, existingTaskId); return this.currentSession; } diff --git a/src/utils/messageHandler.ts b/src/utils/messageHandler.ts index c9fd435..368abcf 100644 --- a/src/utils/messageHandler.ts +++ b/src/utils/messageHandler.ts @@ -19,19 +19,43 @@ import { dialogManager, DialogSession } from "../services/dialogService"; import { userInteractionManager } from "../services/userInteraction"; import { healthCheck } from "../services/apiClient"; +import type { RunMode } from '../types/api'; + /** 是否使用后端服务(可通过配置控制) */ let useBackendService = true; /** 当前对话会话 */ let currentSession: DialogSession | null = null; +/** 待执行的计划(Plan 模式确认后自动执行) */ +let pendingPlanExecution: { + panel: vscode.WebviewPanel; + planTitle: string; + extensionPath: string; + taskId: string; // 保存 taskId 以便复用 +} | null = null; + +/** + * 设置待执行的计划(由 ICHelperPanel 调用) + */ +export function setPendingPlanExecution( + panel: vscode.WebviewPanel, + planTitle: string, + extensionPath: string, + taskId: string +): void { + pendingPlanExecution = { panel, planTitle, extensionPath, taskId }; + console.log('[MessageHandler] 设置待执行计划:', planTitle, 'taskId:', taskId); +} + /** * 处理用户消息 */ export async function handleUserMessage( panel: vscode.WebviewPanel, text: string, - extensionPath?: string + extensionPath?: string, + mode?: RunMode ) { console.log("收到用户消息:", text); @@ -63,7 +87,7 @@ export async function handleUserMessage( // 尝试使用后端服务 if (useBackendService && extensionPath) { try { - await handleUserMessageWithBackend(panel, text, extensionPath); + await handleUserMessageWithBackend(panel, text, extensionPath, mode); return; } catch (error) { console.error("后端服务不可用,回退到本地模式:", error); @@ -97,11 +121,16 @@ export async function handleUserMessage( async function handleUserMessageWithBackend( panel: vscode.WebviewPanel, text: string, - extensionPath: string + extensionPath: string, + mode?: RunMode, + reuseTaskId?: string // 可选,复用现有 taskId(用于 Plan 模式确认后继续执行) ): Promise { // 创建或复用会话 if (!currentSession || !currentSession.active) { - currentSession = dialogManager.createSession(extensionPath); + currentSession = dialogManager.createSession(extensionPath, reuseTaskId); + if (reuseTaskId) { + console.log('[MessageHandler] 复用 taskId 创建会话:', reuseTaskId); + } } const historyManager = ChatHistoryManager.getInstance(); @@ -184,6 +213,29 @@ async function handleUserMessageWithBackend( console.warn("保存AI响应历史失败:", error); } + // 检查是否有待执行的计划(Plan 模式确认后自动执行) + if (pendingPlanExecution) { + const { panel: execPanel, planTitle, extensionPath: execPath, taskId: reuseTaskId } = pendingPlanExecution; + pendingPlanExecution = null; + console.log('[MessageHandler] 自动执行计划:', planTitle, '复用 taskId:', reuseTaskId); + + // 延迟一小段时间确保当前对话完全结束 + setTimeout(async () => { + try { + // 复用 taskId 创建新会话,确保知识图谱数据不丢失 + await handleUserMessageWithBackend( + execPanel, + `请按照刚才的计划执行:${planTitle}`, + execPath, + 'agent', + reuseTaskId // 复用 Plan 模式的 taskId + ); + } catch (err) { + console.error('[MessageHandler] 自动执行计划失败:', err); + } + }, 500); + } + resolve(); }, @@ -201,7 +253,7 @@ async function handleUserMessageWithBackend( onNotification: (message) => { vscode.window.showInformationMessage(message); }, - }); + }, mode); }); } @@ -226,6 +278,75 @@ export function abortCurrentDialog(): void { currentSession = null; } +/** + * 获取当前会话的 taskId + */ +export function getCurrentTaskId(): string | null { + return currentSession?.getTaskId() || null; +} + +/** + * 处理计划操作(Plan 模式) + * @param panel WebView 面板 + * @param action 操作类型:confirm/modify/cancel + * @param planTitle 计划标题 + * @param extensionPath 扩展路径 + */ +export async function handlePlanAction( + panel: vscode.WebviewPanel, + action: string, + planTitle: string, + extensionPath: string +): Promise { + console.log('[handlePlanAction] action:', action, 'planTitle:', planTitle); + + switch (action) { + case 'confirm': + // 确认执行:切换到 Agent 模式并发送执行消息 + panel.webview.postMessage({ + command: 'switchMode', + mode: 'agent' + }); + // 发送执行消息 + await handleUserMessage( + panel, + `请按照刚才的计划执行:${planTitle}`, + extensionPath, + 'agent' + ); + break; + + case 'modify': + // 修改计划:提示用户输入修改建议 + const modification = await vscode.window.showInputBox({ + prompt: '请输入您对计划的修改建议', + placeHolder: '例如:第2步需要先检查文件是否存在...', + ignoreFocusOut: true + }); + if (modification) { + await handleUserMessage( + panel, + `请根据以下建议修改计划:${modification}`, + extensionPath, + 'plan' + ); + } + break; + + case 'cancel': + // 取消计划:通知用户 + panel.webview.postMessage({ + command: 'addMessage', + text: '计划已取消。', + sender: 'bot' + }); + break; + + default: + console.warn('[handlePlanAction] 未知操作:', action); + } +} + /** * 解析文件操作命令 */