/** * 工具执行器 * 接收后端的 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 { resolveWorkspaceFilePath, showFileDiff } from "../utils/fileDiff"; import { changeTracker } from "./changeTracker"; import { generateVCD, checkIverilogAvailable, generateMultiVCD, DumpModule, } from "../utils/iverilogRunner"; import { analyzeVcdFile } from "../utils/vcdParser"; import { executeWaveformTrace, WaveformTraceArgs, } from "../utils/waveformTracer"; import { submitToolResult, createSuccessResult, createBusinessErrorResult, createSystemErrorResult, } from "./apiClient"; import type { ToolCallRequest, ToolName, FileReadArgs, FileWriteArgs, FileDeleteArgs, FileListArgs, SyntaxCheckArgs, IverilogArgs, SimulationArgs, WaveformSummaryArgs, KnowledgeSaveArgs, KnowledgeLoadArgs, } 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_delete": resultText = await executeFileDelete(args as unknown as FileDeleteArgs); 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 "iverilog": resultText = await executeIverilog( args as unknown as IverilogArgs, 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; case "waveform_trace": resultText = await executeWaveformTrace( args as unknown as WaveformTraceArgs, context, ); break; case "knowledge_save": resultText = await executeKnowledgeSave( args as unknown as KnowledgeSaveArgs, ); break; case "knowledge_load": resultText = await executeKnowledgeLoad(); 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 { const absolutePath = resolveWorkspaceFilePath(args.path); const existedBeforeWrite = fs.existsSync(absolutePath); const oldContent = existedBeforeWrite ? await readFileContent(args.path) : ""; await createOrOverwriteFile(args.path, args.content); // 记录文件变更 try { changeTracker.trackChange(args.path, oldContent, args.content); } catch (error) { console.warn("[ToolExecutor] 记录文件变更失败:", error); } // Verilog 文件添加知识图谱提示 const isVerilogFile = args.path.endsWith(".v") || args.path.endsWith(".sv"); if (isVerilogFile) { return `文件已写入: ${args.path}\n\n[提示] 如有新信号或规则,请更新知识图谱`; } return `文件已写入: ${args.path}`; } /** * 执行 file_delete 工具 * 删除指定路径的文件(带用户确认) */ async function executeFileDelete(args: FileDeleteArgs): Promise { const filePath = args.path; // 获取工作区路径 const workspaceFolders = vscode.workspace.workspaceFolders; if (!workspaceFolders || workspaceFolders.length === 0) { throw new Error("请先打开一个工作区"); } const workspacePath = workspaceFolders[0].uri.fsPath; // 解析文件路径(支持相对路径和绝对路径) const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(workspacePath, filePath); // 检查文件是否存在 if (!fs.existsSync(absolutePath)) { throw new Error(`文件不存在: ${filePath}`); } // 检查是否为文件(不允许删除目录) const stat = fs.statSync(absolutePath); if (stat.isDirectory()) { throw new Error(`不能删除目录,请指定文件路径: ${filePath}`); } // 验证文件路径在工作区内 const isInWorkspace = workspaceFolders.some((folder) => absolutePath.startsWith(folder.uri.fsPath), ); if (!isInWorkspace) { throw new Error("只能删除工作区内的文件"); } // 保护敏感文件 const protectedFiles = [ "package.json", "tsconfig.json", ".git", "node_modules", ]; const fileName = path.basename(absolutePath); if (protectedFiles.includes(fileName)) { throw new Error(`不允许删除系统文件: ${fileName}`); } // 弹出确认对话框 const confirmed = await vscode.window.showWarningMessage( `确定要删除文件吗?\n\n📄 ${path.basename(filePath)}\n📁 ${path.dirname(filePath)}`, { modal: true, // 模态对话框,阻止其他操作 detail: "⚠️ 文件将被移到回收站,可以恢复", }, "确定删除", "取消", ); // 用户取消或关闭对话框 if (confirmed !== "确定删除") { throw new Error("用户取消了删除操作"); } // 读取文件内容用于变更追踪 const oldContent = fs.readFileSync(absolutePath, "utf-8"); // 记录删除变更 const relativePath = path.relative(workspacePath, absolutePath); changeTracker.trackChange(relativePath, oldContent, ""); // 删除文件(移到回收站) const uri = vscode.Uri.file(absolutePath); await vscode.workspace.fs.delete(uri, { recursive: false, // 不是目录,设为 false useTrash: true, // 移到回收站而非永久删除 }); // Verilog 文件添加知识图谱提示 const isVerilogFile = filePath.endsWith(".v") || filePath.endsWith(".sv"); if (isVerilogFile) { return `文件已删除: ${filePath}\n\n[提示] 请删除知识图谱中相关节点`; } return `文件已删除: ${filePath}`; } /** * 执行 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(`IC Coder编译器不可用: ${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; } } /** * 执行 iverilog 工具 * 直接执行 iverilog 命令 */ async function executeIverilog( args: IverilogArgs, context: ToolExecutorContext, ): Promise { // 检查 iverilog 是否可用 const iverilogCheck = await checkIverilogAvailable(context.extensionPath); if (!iverilogCheck.available) { throw new Error(`IC Coder编译器不可用: ${iverilogCheck.message}`); } // 获取工作目录 const workspaceFolders = vscode.workspace.workspaceFolders; if (!workspaceFolders || workspaceFolders.length === 0) { throw new Error("没有打开的工作区"); } const projectPath = workspaceFolders[0].uri.fsPath; const workDir = args.workDir ? path.join(projectPath, args.workDir) : projectPath; // 解析参数 const iverilogPath = getIverilogPath(context.extensionPath); const cmdArgs = args.args.split(/\s+/).filter((a) => a.length > 0); const { spawn } = require("child_process"); return new Promise((resolve, reject) => { const child = spawn(iverilogPath, cmdArgs, { cwd: workDir, 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) => { const output = stderr || stdout || "(无输出)"; if (code === 0) { resolve(`执行成功\n${output}`); } else { resolve(`执行失败 (exit code: ${code})\n${output}`); } }); child.on("error", (error: Error) => { reject(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; // 检查是否有 dumpModules 参数(多 VCD 模式) if (args.dumpModules) { const modules = parseDumpModules(args.dumpModules); const vcdDir = args.vcdDir || "vcd"; const result = await generateMultiVCD( projectPath, context.extensionPath, args.tbPath, modules, vcdDir, ); if (result.success) { const vcdList = result.vcdFiles .map( (f) => `- ${f.moduleName}: ${f.success ? f.vcdPath : "失败 - " + f.error}`, ) .join("\n"); return `${result.message}\n\nVCD 文件列表:\n${vcdList}${result.stdout ? "\n\n仿真输出:" + result.stdout : ""}`; } else { throw new Error(result.message); } } // 原有单 VCD 逻辑 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); } } /** * 解析 dumpModules 参数 * 格式:name:path,name:path */ function parseDumpModules(dumpModules: string): DumpModule[] { return dumpModules.split(",").map((item) => { const [name, modulePath] = item.trim().split(":"); return { name: name.trim(), path: modulePath.trim() }; }); } /** * 执行 waveform_summary 工具 * 解析 VCD 文件并返回波形摘要 */ async function executeWaveformSummary( args: WaveformSummaryArgs, ): Promise { const { vcdPath, signals, checkpoints } = args; // 获取工作区路径 const workspaceFolders = vscode.workspace.workspaceFolders; if (!workspaceFolders || workspaceFolders.length === 0) { throw new Error("请先打开一个工作区"); } const workspacePath = workspaceFolders[0].uri.fsPath; // 解析 VCD 文件路径(支持相对路径) const absolutePath = path.isAbsolute(vcdPath) ? vcdPath : path.join(workspacePath, vcdPath); // 检查文件是否存在 if (!fs.existsSync(absolutePath)) { throw new Error(`VCD 文件不存在: ${vcdPath}`); } // 解析检查点时间 const checkpoint = checkpoints ? parseInt(checkpoints, 10) : undefined; // 调用 VCD 解析器 const result = analyzeVcdFile(absolutePath, signals, checkpoint); return result; } /** * 执行 knowledge_save 工具 * 保存知识图谱到 .iccoder/knowledge.json */ async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise { const workspaceFolder = getWorkspaceFolder(); if (!workspaceFolder) { throw new Error("请先打开一个工作区"); } const iccoderDirUri = vscode.Uri.joinPath(workspaceFolder.uri, ".iccoder"); const knowledgeUri = vscode.Uri.joinPath(iccoderDirUri, "knowledge.json"); // 确保 .iccoder 目录存在(兼容远程/虚拟工作区) await vscode.workspace.fs.createDirectory(iccoderDirUri); // 写入知识图谱(UTF-8) await vscode.workspace.fs.writeFile( knowledgeUri, Buffer.from(args.data || "", "utf-8"), ); return `知识图谱已保存: .iccoder/knowledge.json`; } /** * 执行 knowledge_load 工具 * 从 .iccoder/knowledge.json 加载知识图谱 */ async function executeKnowledgeLoad(): Promise { const workspaceFolder = getWorkspaceFolder(); if (!workspaceFolder) { throw new Error("请先打开一个工作区"); } const knowledgeUri = vscode.Uri.joinPath( workspaceFolder.uri, ".iccoder", "knowledge.json", ); 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 activeUri = vscode.window.activeTextEditor?.document?.uri; const activeFolder = activeUri ? vscode.workspace.getWorkspaceFolder(activeUri) : undefined; return activeFolder ?? folders[0]; } /** * 获取 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, }; }