diff --git a/src/services/dialogService.ts b/src/services/dialogService.ts new file mode 100644 index 0000000..7bfa9fa --- /dev/null +++ b/src/services/dialogService.ts @@ -0,0 +1,224 @@ +/** + * 对话服务 + * 整合 SSE 通信、工具执行、用户交互 + */ +import * as vscode from 'vscode'; +import { startStreamDialog, generateTaskId, SSEController, SSECallbacks } from './sseHandler'; +import { executeToolCall, createToolExecutorContext, ToolExecutorContext } from './toolExecutor'; +import { userInteractionManager } from './userInteraction'; +import { getConfig } from '../config/settings'; +import type { DialogRequest, ToolCallRequest, AskUserEvent } from '../types/api'; + +/** + * 对话回调接口 + */ +export interface DialogCallbacks { + /** 收到文本(可能多次调用,流式) */ + onText?: (text: string, isStreaming: boolean) => void; + /** 工具开始执行 */ + onToolStart?: (toolName: string) => void; + /** 工具执行完成 */ + onToolComplete?: (toolName: string, result: string) => void; + /** 工具执行错误 */ + onToolError?: (toolName: string, error: string) => void; + /** 显示问题(ask_user) */ + onQuestion?: (askId: string, question: string, options: string[]) => void; + /** 对话完成 */ + onComplete?: () => void; + /** 错误 */ + onError?: (message: string) => void; + /** 通知消息 */ + onNotification?: (message: string) => void; +} + +/** + * 对话会话 + */ +export class DialogSession { + private taskId: string; + private sseController: SSEController | null = null; + private toolContext: ToolExecutorContext; + private accumulatedText = ''; + private isActive = false; + + constructor(extensionPath: string) { + this.taskId = generateTaskId(); + this.toolContext = createToolExecutorContext(extensionPath); + } + + /** + * 获取任务ID + */ + getTaskId(): string { + return this.taskId; + } + + /** + * 是否活跃 + */ + get active(): boolean { + return this.isActive; + } + + /** + * 发送消息并开始流式对话 + */ + async sendMessage( + message: string, + callbacks: DialogCallbacks + ): Promise { + if (this.isActive) { + callbacks.onError?.('当前有对话正在进行中'); + return; + } + + this.isActive = true; + this.accumulatedText = ''; + + const config = getConfig(); + const request: DialogRequest = { + taskId: this.taskId, + message, + userId: config.userId, + toolMode: 'AGENT' + }; + + const sseCallbacks: SSECallbacks = { + onTextDelta: (data) => { + this.accumulatedText += data.text; + console.log('[DialogSession] onTextDelta, 累积文本长度:', this.accumulatedText.length); + callbacks.onText?.(this.accumulatedText, true); + }, + + onToolCall: async (data: ToolCallRequest) => { + callbacks.onToolStart?.(data.params.name); + try { + await executeToolCall(data, this.toolContext); + callbacks.onToolComplete?.(data.params.name, '执行完成'); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : '未知错误'; + callbacks.onToolError?.(data.params.name, errorMsg); + } + }, + + onToolStart: (data) => { + callbacks.onToolStart?.(data.tool_name); + }, + + onToolComplete: (data) => { + callbacks.onToolComplete?.(data.tool_name, data.result); + }, + + onToolError: (data) => { + callbacks.onToolError?.(data.tool_name, data.error); + }, + + onAskUser: async (data: AskUserEvent) => { + callbacks.onQuestion?.(data.askId, data.question, data.options); + try { + await userInteractionManager.handleAskUser(data, this.taskId); + } catch (error) { + console.error('[DialogSession] 处理用户问题失败:', error); + } + }, + + onComplete: (data) => { + this.isActive = false; + // 发送最终文本(非流式) + if (this.accumulatedText) { + callbacks.onText?.(this.accumulatedText, false); + } + callbacks.onComplete?.(); + }, + + onError: (data) => { + this.isActive = false; + callbacks.onError?.(data.message); + }, + + onWarning: (data) => { + callbacks.onNotification?.(`⚠️ ${data.message}`); + }, + + onNotification: (data) => { + callbacks.onNotification?.(data.message); + }, + + onOpen: () => { + console.log('[DialogSession] SSE 连接已建立'); + }, + + onClose: () => { + console.log('[DialogSession] SSE 连接已关闭'); + this.isActive = false; + } + }; + + try { + this.sseController = await startStreamDialog(request, sseCallbacks); + } catch (error) { + this.isActive = false; + const errorMsg = error instanceof Error ? error.message : '连接失败'; + callbacks.onError?.(errorMsg); + throw error; + } + } + + /** + * 中止当前对话 + */ + abort(): void { + if (this.sseController) { + this.sseController.abort(); + this.sseController = null; + } + this.isActive = false; + userInteractionManager.cancelAll(); + } + + /** + * 提交用户回答 + */ + async submitAnswer( + askId: string, + selected?: string[], + customInput?: string + ): Promise { + await userInteractionManager.receiveAnswer(askId, selected, customInput); + } +} + +/** + * 全局对话会话管理 + */ +class DialogManager { + private currentSession: DialogSession | null = null; + + /** + * 创建新会话 + */ + createSession(extensionPath: string): DialogSession { + // 如果有活跃会话,先中止 + if (this.currentSession?.active) { + this.currentSession.abort(); + } + this.currentSession = new DialogSession(extensionPath); + return this.currentSession; + } + + /** + * 获取当前会话 + */ + getCurrentSession(): DialogSession | null { + return this.currentSession; + } + + /** + * 中止当前会话 + */ + abortCurrentSession(): void { + this.currentSession?.abort(); + } +} + +export const dialogManager = new DialogManager(); diff --git a/src/services/toolExecutor.ts b/src/services/toolExecutor.ts new file mode 100644 index 0000000..b424d63 --- /dev/null +++ b/src/services/toolExecutor.ts @@ -0,0 +1,272 @@ +/** + * 工具执行器 + * 接收后端的 tool_call 事件,执行本地工具,返回结果 + */ +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as os from 'os'; +import * as fs from 'fs'; +import { readFileContent, readDirectory } from '../utils/readFiles'; +import { createOrOverwriteFile } from '../utils/createFiles'; +import { generateVCD, checkIverilogAvailable } from '../utils/iverilogRunner'; +import { + submitToolResult, + createSuccessResult, + createBusinessErrorResult, + createSystemErrorResult +} from './apiClient'; +import type { + ToolCallRequest, + ToolName, + FileReadArgs, + FileWriteArgs, + FileListArgs, + SyntaxCheckArgs, + SimulationArgs, + WaveformSummaryArgs +} from '../types/api'; + +/** + * 工具执行器上下文 + */ +export interface ToolExecutorContext { + /** 扩展路径(用于 iverilog) */ + extensionPath: string; + /** 工作区路径 */ + workspacePath: string; +} + +/** + * 执行工具调用 + * @param request 工具调用请求 + * @param context 执行上下文 + */ +export async function executeToolCall( + request: ToolCallRequest, + context: ToolExecutorContext +): Promise { + const toolName = request.params.name as ToolName; + const args = request.params.arguments; + const callId = request.id; + + console.log(`[ToolExecutor] 执行工具: ${toolName}, callId=${callId}`, args); + + try { + let resultText: string; + + switch (toolName) { + case 'file_read': + resultText = await executeFileRead(args as unknown as FileReadArgs); + break; + case 'file_write': + resultText = await executeFileWrite(args as unknown as FileWriteArgs); + break; + case 'file_list': + resultText = await executeFileList(args as unknown as FileListArgs); + break; + case 'syntax_check': + resultText = await executeSyntaxCheck(args as unknown as SyntaxCheckArgs, context); + break; + case 'simulation': + resultText = await executeSimulation(args as unknown as SimulationArgs, context); + break; + case 'waveform_summary': + resultText = await executeWaveformSummary(args as unknown as WaveformSummaryArgs); + break; + default: + throw new Error(`未知工具: ${toolName}`); + } + + // 提交成功结果 + const result = createSuccessResult(callId, resultText); + await submitToolResult(result); + console.log(`[ToolExecutor] 工具执行成功: ${toolName}, callId=${callId}`); + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : '未知错误'; + console.error(`[ToolExecutor] 工具执行失败: ${toolName}, callId=${callId}`, error); + + // 提交错误结果 + const result = createBusinessErrorResult(callId, errorMessage); + await submitToolResult(result); + } +} + +/** + * 执行 file_read 工具 + */ +async function executeFileRead(args: FileReadArgs): Promise { + const content = await readFileContent(args.path); + return content; +} + +/** + * 执行 file_write 工具 + */ +async function executeFileWrite(args: FileWriteArgs): Promise { + await createOrOverwriteFile(args.path, args.content); + return `文件已写入: ${args.path}`; +} + +/** + * 执行 file_list 工具 + */ +async function executeFileList(args: FileListArgs): Promise { + const dirPath = args.path || '.'; + const extensions = args.extension ? [args.extension] : undefined; + + const files = await readDirectory(dirPath, extensions); + const fileList = files.map(f => f.path).join('\n'); + + return fileList || '(目录为空)'; +} + +/** + * 执行 syntax_check 工具 + * 将代码写入临时文件,调用 iverilog 检查语法 + */ +async function executeSyntaxCheck( + args: SyntaxCheckArgs, + context: ToolExecutorContext +): Promise { + // 检查 iverilog 是否可用 + const iverilogCheck = await checkIverilogAvailable(context.extensionPath); + if (!iverilogCheck.available) { + throw new Error(`iverilog 不可用: ${iverilogCheck.message}`); + } + + // 创建临时文件 + const tempDir = os.tmpdir(); + const tempFile = path.join(tempDir, `iccoder_syntax_${Date.now()}.v`); + + try { + // 写入代码到临时文件 + fs.writeFileSync(tempFile, args.code, 'utf-8'); + + // 调用 iverilog 进行语法检查 + const { spawn } = require('child_process'); + const iverilogPath = getIverilogPath(context.extensionPath); + + return new Promise((resolve, reject) => { + const child = spawn(iverilogPath, ['-t', 'null', tempFile], { + cwd: tempDir, + env: { + ...process.env, + IVERILOG_ROOT: path.join(context.extensionPath, 'tools', 'iverilog') + } + }); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (data: Buffer) => { + stdout += data.toString(); + }); + + child.stderr.on('data', (data: Buffer) => { + stderr += data.toString(); + }); + + child.on('close', (code: number) => { + // 清理临时文件 + try { + fs.unlinkSync(tempFile); + } catch (e) { + // 忽略清理错误 + } + + if (code === 0) { + resolve('语法检查通过,无错误。'); + } else { + resolve(`语法检查发现错误:\n${stderr || stdout}`); + } + }); + + child.on('error', (error: Error) => { + try { + fs.unlinkSync(tempFile); + } catch (e) { + // 忽略清理错误 + } + reject(error); + }); + }); + + } catch (error) { + // 确保清理临时文件 + try { + fs.unlinkSync(tempFile); + } catch (e) { + // 忽略 + } + throw error; + } +} + +/** + * 执行 simulation 工具 + */ +async function executeSimulation( + args: SimulationArgs, + context: ToolExecutorContext +): Promise { + // 获取工作区路径 + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) { + throw new Error('请先打开一个工作区'); + } + + const projectPath = workspaceFolders[0].uri.fsPath; + + // 调用现有的 generateVCD 函数 + const result = await generateVCD(projectPath, context.extensionPath); + + if (result.success) { + let message = result.message; + if (result.stdout) { + message += `\n\n仿真输出:\n${result.stdout}`; + } + return message; + } else { + let errorMessage = result.message; + if (result.stderr) { + errorMessage += `\n\n错误输出:\n${result.stderr}`; + } + throw new Error(errorMessage); + } +} + +/** + * 执行 waveform_summary 工具 + * TODO: 实现 VCD 波形分析 + */ +async function executeWaveformSummary(args: WaveformSummaryArgs): Promise { + // TODO: 使用 vcdrom/vcd-stream 解析 VCD 文件 + // 目前返回一个占位响应 + return `波形分析功能暂未实现。\n请求参数:\n- VCD文件: ${args.vcdPath}\n- 信号: ${args.signals}\n- 检查点: ${args.checkpoints || '无'}`; +} + +/** + * 获取 iverilog 路径 + */ +function getIverilogPath(extensionPath: string): string { + const platform = process.platform; + if (platform === 'win32') { + return path.join(extensionPath, 'tools', 'iverilog', 'bin', 'iverilog.exe'); + } else { + return path.join(extensionPath, 'tools', 'iverilog', 'bin', 'iverilog'); + } +} + +/** + * 创建工具执行器上下文 + */ +export function createToolExecutorContext(extensionPath: string): ToolExecutorContext { + const workspaceFolders = vscode.workspace.workspaceFolders; + const workspacePath = workspaceFolders?.[0]?.uri.fsPath || ''; + + return { + extensionPath, + workspacePath + }; +} diff --git a/src/services/userInteraction.ts b/src/services/userInteraction.ts new file mode 100644 index 0000000..f22455d --- /dev/null +++ b/src/services/userInteraction.ts @@ -0,0 +1,154 @@ +/** + * 用户交互处理器 + * 处理 ask_user 事件,通过 WebView 显示问题并收集用户回答 + */ +import * as vscode from 'vscode'; +import { submitAnswer } from './apiClient'; +import type { AskUserEvent, AnswerRequest } from '../types/api'; + +/** + * 待处理的用户问题 + */ +interface PendingQuestion { + askId: string; + taskId: string; + question: string; + options: string[]; + resolve: (answer: string) => void; + reject: (error: Error) => void; +} + +/** + * 用户交互管理器 + */ +export class UserInteractionManager { + private pendingQuestions = new Map(); + private webviewPanel: vscode.WebviewPanel | null = null; + + /** + * 设置 WebView 面板(用于发送消息) + */ + setWebviewPanel(panel: vscode.WebviewPanel): void { + this.webviewPanel = panel; + } + + /** + * 处理 ask_user 事件 + * @param event ask_user 事件数据 + * @param taskId 当前任务ID + */ + async handleAskUser(event: AskUserEvent, taskId: string): Promise { + const { askId, question, options } = event; + + console.log(`[UserInteraction] 收到问题: askId=${askId}, question=${question}`); + + // 通过 WebView 显示问题 + if (this.webviewPanel) { + this.webviewPanel.webview.postMessage({ + command: 'showQuestion', + askId, + question, + options + }); + } + + // 创建 Promise 等待用户回答 + return new Promise((resolve, reject) => { + this.pendingQuestions.set(askId, { + askId, + taskId, + question, + options, + resolve: (answer: string) => { + this.submitUserAnswer(askId, taskId, answer) + .then(() => resolve()) + .catch(reject); + }, + reject + }); + + // 设置超时(5分钟) + setTimeout(() => { + if (this.pendingQuestions.has(askId)) { + this.pendingQuestions.delete(askId); + reject(new Error('用户回答超时')); + } + }, 300000); + }); + } + + /** + * 处理用户提交的回答(从 WebView 调用) + * @param askId 问题ID + * @param selected 选中的选项 + * @param customInput 自定义输入 + */ + async receiveAnswer( + askId: string, + selected?: string[], + customInput?: string + ): Promise { + const pending = this.pendingQuestions.get(askId); + if (!pending) { + console.warn(`[UserInteraction] 问题不存在或已超时: askId=${askId}`); + return; + } + + // 构建答案 + const answer = customInput || selected?.join(', ') || ''; + + console.log(`[UserInteraction] 收到用户回答: askId=${askId}, answer=${answer}`); + + // 移除待处理问题 + this.pendingQuestions.delete(askId); + + // 触发 resolve + pending.resolve(answer); + } + + /** + * 提交用户回答到后端 + */ + private async submitUserAnswer( + askId: string, + taskId: string, + answer: string + ): Promise { + const request: AnswerRequest = { + askId, + taskId, + customInput: answer + }; + + try { + const response = await submitAnswer(request); + if (!response.success) { + throw new Error(response.error || '提交回答失败'); + } + console.log(`[UserInteraction] 回答已提交: askId=${askId}`); + } catch (error) { + console.error(`[UserInteraction] 提交回答失败: askId=${askId}`, error); + throw error; + } + } + + /** + * 取消所有待处理的问题 + */ + cancelAll(): void { + for (const [askId, pending] of this.pendingQuestions) { + pending.reject(new Error('用户交互已取消')); + } + this.pendingQuestions.clear(); + } + + /** + * 检查是否有待处理的问题 + */ + hasPendingQuestions(): boolean { + return this.pendingQuestions.size > 0; + } +} + +// 全局实例 +export const userInteractionManager = new UserInteractionManager();