diff --git a/src/services/toolExecutor.ts b/src/services/toolExecutor.ts index 4e31a39..930e5e6 100644 --- a/src/services/toolExecutor.ts +++ b/src/services/toolExecutor.ts @@ -2,23 +2,31 @@ * 工具执行器 * 接收后端的 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 * 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'; + createSystemErrorResult, +} from "./apiClient"; import type { ToolCallRequest, ToolName, @@ -31,8 +39,8 @@ import type { SimulationArgs, WaveformSummaryArgs, KnowledgeSaveArgs, - KnowledgeLoadArgs -} from '../types/api'; + KnowledgeLoadArgs, +} from "../types/api"; /** * 工具执行器上下文 @@ -51,7 +59,7 @@ export interface ToolExecutorContext { */ export async function executeToolCall( request: ToolCallRequest, - context: ToolExecutorContext + context: ToolExecutorContext, ): Promise { const toolName = request.params.name as ToolName; const args = request.params.arguments; @@ -63,37 +71,53 @@ export async function executeToolCall( let resultText: string; switch (toolName) { - case 'file_read': + case "file_read": resultText = await executeFileRead(args as unknown as FileReadArgs); break; - case 'file_write': + case "file_write": resultText = await executeFileWrite(args as unknown as FileWriteArgs); break; - case 'file_delete': + case "file_delete": resultText = await executeFileDelete(args as unknown as FileDeleteArgs); break; - case 'file_list': + case "file_list": resultText = await executeFileList(args as unknown as FileListArgs); break; - case 'syntax_check': - resultText = await executeSyntaxCheck(args as unknown as SyntaxCheckArgs, context); + case "syntax_check": + resultText = await executeSyntaxCheck( + args as unknown as SyntaxCheckArgs, + context, + ); break; - case 'iverilog': - resultText = await executeIverilog(args as unknown as IverilogArgs, context); + case "iverilog": + resultText = await executeIverilog( + args as unknown as IverilogArgs, + context, + ); break; - case 'simulation': - resultText = await executeSimulation(args as unknown as SimulationArgs, context); + case "simulation": + resultText = await executeSimulation( + args as unknown as SimulationArgs, + context, + ); break; - case 'waveform_summary': - resultText = await executeWaveformSummary(args as unknown as WaveformSummaryArgs); + case "waveform_summary": + resultText = await executeWaveformSummary( + args as unknown as WaveformSummaryArgs, + ); break; - case 'waveform_trace': - resultText = await executeWaveformTrace(args as unknown as WaveformTraceArgs, context); + case "waveform_trace": + resultText = await executeWaveformTrace( + args as unknown as WaveformTraceArgs, + context, + ); break; - case 'knowledge_save': - resultText = await executeKnowledgeSave(args as unknown as KnowledgeSaveArgs); + case "knowledge_save": + resultText = await executeKnowledgeSave( + args as unknown as KnowledgeSaveArgs, + ); break; - case 'knowledge_load': + case "knowledge_load": resultText = await executeKnowledgeLoad(); break; default: @@ -104,10 +128,12 @@ export async function executeToolCall( 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 errorMessage = error instanceof Error ? error.message : "未知错误"; + console.error( + `[ToolExecutor] 工具执行失败: ${toolName}, callId=${callId}`, + error, + ); // 提交错误结果 const result = createBusinessErrorResult(callId, errorMessage); @@ -129,7 +155,7 @@ async function executeFileRead(args: FileReadArgs): Promise { async function executeFileWrite(args: FileWriteArgs): Promise { const absolutePath = resolveWorkspaceFilePath(args.path); const existedBeforeWrite = fs.existsSync(absolutePath); - const oldContent = existedBeforeWrite ? await readFileContent(args.path) : ''; + const oldContent = existedBeforeWrite ? await readFileContent(args.path) : ""; await createOrOverwriteFile(args.path, args.content); @@ -137,11 +163,11 @@ async function executeFileWrite(args: FileWriteArgs): Promise { try { changeTracker.trackChange(args.path, oldContent, args.content); } catch (error) { - console.warn('[ToolExecutor] 记录文件变更失败:', error); + console.warn("[ToolExecutor] 记录文件变更失败:", error); } // Verilog 文件添加知识图谱提示 - const isVerilogFile = args.path.endsWith('.v') || args.path.endsWith('.sv'); + const isVerilogFile = args.path.endsWith(".v") || args.path.endsWith(".sv"); if (isVerilogFile) { return `文件已写入: ${args.path}\n\n[提示] 如有新信号或规则,请更新知识图谱`; } @@ -151,7 +177,7 @@ async function executeFileWrite(args: FileWriteArgs): Promise { /** * 执行 file_delete 工具 - * 删除指定路径的文件 + * 删除指定路径的文件(带用户确认) */ async function executeFileDelete(args: FileDeleteArgs): Promise { const filePath = args.path; @@ -159,7 +185,7 @@ async function executeFileDelete(args: FileDeleteArgs): Promise { // 获取工作区路径 const workspaceFolders = vscode.workspace.workspaceFolders; if (!workspaceFolders || workspaceFolders.length === 0) { - throw new Error('请先打开一个工作区'); + throw new Error("请先打开一个工作区"); } const workspacePath = workspaceFolders[0].uri.fsPath; @@ -180,18 +206,60 @@ async function executeFileDelete(args: FileDeleteArgs): Promise { 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 oldContent = fs.readFileSync(absolutePath, "utf-8"); // 记录删除变更 const relativePath = path.relative(workspacePath, absolutePath); - changeTracker.trackChange(relativePath, oldContent, ''); + changeTracker.trackChange(relativePath, oldContent, ""); - // 删除文件 - fs.unlinkSync(absolutePath); + // 删除文件(移到回收站) + 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'); + const isVerilogFile = filePath.endsWith(".v") || filePath.endsWith(".sv"); if (isVerilogFile) { return `文件已删除: ${filePath}\n\n[提示] 请删除知识图谱中相关节点`; } @@ -203,13 +271,13 @@ async function executeFileDelete(args: FileDeleteArgs): Promise { * 执行 file_list 工具 */ async function executeFileList(args: FileListArgs): Promise { - const dirPath = args.path || '.'; + 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'); + const fileList = files.map((f) => f.path).join("\n"); - return fileList || '(目录为空)'; + return fileList || "(目录为空)"; } /** @@ -218,7 +286,7 @@ async function executeFileList(args: FileListArgs): Promise { */ async function executeSyntaxCheck( args: SyntaxCheckArgs, - context: ToolExecutorContext + context: ToolExecutorContext, ): Promise { // 检查 iverilog 是否可用 const iverilogCheck = await checkIverilogAvailable(context.extensionPath); @@ -232,33 +300,33 @@ async function executeSyntaxCheck( try { // 写入代码到临时文件 - fs.writeFileSync(tempFile, args.code, 'utf-8'); + fs.writeFileSync(tempFile, args.code, "utf-8"); // 调用 iverilog 进行语法检查 - const { spawn } = require('child_process'); + const { spawn } = require("child_process"); const iverilogPath = getIverilogPath(context.extensionPath); return new Promise((resolve, reject) => { - const child = spawn(iverilogPath, ['-t', 'null', tempFile], { + const child = spawn(iverilogPath, ["-t", "null", tempFile], { cwd: tempDir, env: { ...process.env, - IVERILOG_ROOT: path.join(context.extensionPath, 'tools', 'iverilog') - } + IVERILOG_ROOT: path.join(context.extensionPath, "tools", "iverilog"), + }, }); - let stdout = ''; - let stderr = ''; + let stdout = ""; + let stderr = ""; - child.stdout.on('data', (data: Buffer) => { + child.stdout.on("data", (data: Buffer) => { stdout += data.toString(); }); - child.stderr.on('data', (data: Buffer) => { + child.stderr.on("data", (data: Buffer) => { stderr += data.toString(); }); - child.on('close', (code: number) => { + child.on("close", (code: number) => { // 清理临时文件 try { fs.unlinkSync(tempFile); @@ -267,13 +335,13 @@ async function executeSyntaxCheck( } if (code === 0) { - resolve('语法检查通过,无错误。'); + resolve("语法检查通过,无错误。"); } else { resolve(`语法检查发现错误:\n${stderr || stdout}`); } }); - child.on('error', (error: Error) => { + child.on("error", (error: Error) => { try { fs.unlinkSync(tempFile); } catch (e) { @@ -282,7 +350,6 @@ async function executeSyntaxCheck( reject(error); }); }); - } catch (error) { // 确保清理临时文件 try { @@ -300,7 +367,7 @@ async function executeSyntaxCheck( */ async function executeIverilog( args: IverilogArgs, - context: ToolExecutorContext + context: ToolExecutorContext, ): Promise { // 检查 iverilog 是否可用 const iverilogCheck = await checkIverilogAvailable(context.extensionPath); @@ -311,7 +378,7 @@ async function executeIverilog( // 获取工作目录 const workspaceFolders = vscode.workspace.workspaceFolders; if (!workspaceFolders || workspaceFolders.length === 0) { - throw new Error('没有打开的工作区'); + throw new Error("没有打开的工作区"); } const projectPath = workspaceFolders[0].uri.fsPath; const workDir = args.workDir @@ -320,32 +387,32 @@ async function executeIverilog( // 解析参数 const iverilogPath = getIverilogPath(context.extensionPath); - const cmdArgs = args.args.split(/\s+/).filter(a => a.length > 0); + const cmdArgs = args.args.split(/\s+/).filter((a) => a.length > 0); - const { spawn } = require('child_process'); + 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') - } + IVERILOG_ROOT: path.join(context.extensionPath, "tools", "iverilog"), + }, }); - let stdout = ''; - let stderr = ''; + let stdout = ""; + let stderr = ""; - child.stdout.on('data', (data: Buffer) => { + child.stdout.on("data", (data: Buffer) => { stdout += data.toString(); }); - child.stderr.on('data', (data: Buffer) => { + child.stderr.on("data", (data: Buffer) => { stderr += data.toString(); }); - child.on('close', (code: number) => { - const output = stderr || stdout || '(无输出)'; + child.on("close", (code: number) => { + const output = stderr || stdout || "(无输出)"; if (code === 0) { resolve(`执行成功\n${output}`); } else { @@ -353,7 +420,7 @@ async function executeIverilog( } }); - child.on('error', (error: Error) => { + child.on("error", (error: Error) => { reject(error); }); }); @@ -364,12 +431,12 @@ async function executeIverilog( */ async function executeSimulation( args: SimulationArgs, - context: ToolExecutorContext + context: ToolExecutorContext, ): Promise { // 获取工作区路径 const workspaceFolders = vscode.workspace.workspaceFolders; if (!workspaceFolders || workspaceFolders.length === 0) { - throw new Error('请先打开一个工作区'); + throw new Error("请先打开一个工作区"); } const projectPath = workspaceFolders[0].uri.fsPath; @@ -377,21 +444,24 @@ async function executeSimulation( // 检查是否有 dumpModules 参数(多 VCD 模式) if (args.dumpModules) { const modules = parseDumpModules(args.dumpModules); - const vcdDir = args.vcdDir || 'vcd'; + const vcdDir = args.vcdDir || "vcd"; const result = await generateMultiVCD( projectPath, context.extensionPath, args.tbPath, modules, - vcdDir + 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 : ''}`; + .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); } @@ -420,8 +490,8 @@ async function executeSimulation( * 格式:name:path,name:path */ function parseDumpModules(dumpModules: string): DumpModule[] { - return dumpModules.split(',').map(item => { - const [name, modulePath] = item.trim().split(':'); + return dumpModules.split(",").map((item) => { + const [name, modulePath] = item.trim().split(":"); return { name: name.trim(), path: modulePath.trim() }; }); } @@ -430,13 +500,15 @@ function parseDumpModules(dumpModules: string): DumpModule[] { * 执行 waveform_summary 工具 * 解析 VCD 文件并返回波形摘要 */ -async function executeWaveformSummary(args: WaveformSummaryArgs): Promise { +async function executeWaveformSummary( + args: WaveformSummaryArgs, +): Promise { const { vcdPath, signals, checkpoints } = args; // 获取工作区路径 const workspaceFolders = vscode.workspace.workspaceFolders; if (!workspaceFolders || workspaceFolders.length === 0) { - throw new Error('请先打开一个工作区'); + throw new Error("请先打开一个工作区"); } const workspacePath = workspaceFolders[0].uri.fsPath; @@ -467,17 +539,20 @@ async function executeWaveformSummary(args: WaveformSummaryArgs): Promise { const workspaceFolder = getWorkspaceFolder(); if (!workspaceFolder) { - throw new Error('请先打开一个工作区'); + throw new Error("请先打开一个工作区"); } - const iccoderDirUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder'); - const knowledgeUri = vscode.Uri.joinPath(iccoderDirUri, 'knowledge.json'); + 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')); + await vscode.workspace.fs.writeFile( + knowledgeUri, + Buffer.from(args.data || "", "utf-8"), + ); return `知识图谱已保存: .iccoder/knowledge.json`; } @@ -489,20 +564,33 @@ async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise { async function executeKnowledgeLoad(): Promise { const workspaceFolder = getWorkspaceFolder(); if (!workspaceFolder) { - throw new Error('请先打开一个工作区'); + throw new Error("请先打开一个工作区"); } - const knowledgeUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder', 'knowledge.json'); + 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'); + const content = Buffer.from(bytes).toString("utf-8"); return content; } catch (error) { // 文件不存在:返回空图谱 - if (error instanceof vscode.FileSystemError && error.code === 'FileNotFound') { + if ( + error instanceof vscode.FileSystemError && + error.code === "FileNotFound" + ) { // 与后端 KnowledgeGraph 结构保持一致(nodes/edges + nodeClass 多态字段) - return JSON.stringify({ taskId: '', version: 1, module: null, nodes: [], edges: [] }); + return JSON.stringify({ + taskId: "", + version: 1, + module: null, + nodes: [], + edges: [], + }); } throw error; } @@ -515,7 +603,9 @@ function getWorkspaceFolder(): vscode.WorkspaceFolder | undefined { } const activeUri = vscode.window.activeTextEditor?.document?.uri; - const activeFolder = activeUri ? vscode.workspace.getWorkspaceFolder(activeUri) : undefined; + const activeFolder = activeUri + ? vscode.workspace.getWorkspaceFolder(activeUri) + : undefined; return activeFolder ?? folders[0]; } @@ -524,22 +614,24 @@ function getWorkspaceFolder(): vscode.WorkspaceFolder | undefined { */ function getIverilogPath(extensionPath: string): string { const platform = process.platform; - if (platform === 'win32') { - return path.join(extensionPath, 'tools', 'iverilog', 'bin', 'iverilog.exe'); + if (platform === "win32") { + return path.join(extensionPath, "tools", "iverilog", "bin", "iverilog.exe"); } else { - return path.join(extensionPath, 'tools', 'iverilog', 'bin', 'iverilog'); + return path.join(extensionPath, "tools", "iverilog", "bin", "iverilog"); } } /** * 创建工具执行器上下文 */ -export function createToolExecutorContext(extensionPath: string): ToolExecutorContext { +export function createToolExecutorContext( + extensionPath: string, +): ToolExecutorContext { const workspaceFolders = vscode.workspace.workspaceFolders; - const workspacePath = workspaceFolders?.[0]?.uri.fsPath || ''; + const workspacePath = workspaceFolders?.[0]?.uri.fsPath || ""; return { extensionPath, - workspacePath + workspacePath, }; }