From 6425496d2edeed784f12cc43d2be7bae324b618c Mon Sep 17 00:00:00 2001 From: zhuo <3231949183@qq.com> Date: Tue, 10 Mar 2026 18:56:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=A6=BB=E7=BA=BF=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E6=94=B9=E8=BF=9B=E5=92=8C=20SystemVerilog?= =?UTF-8?q?=20=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加离线模式仿真模拟:识别代码生成完成消息后自动模拟仿真流程 - 启用 iverilog SystemVerilog 2012 标准支持(-g2012) - 优化进度条显示逻辑 Co-Authored-By: Claude Sonnet 4.6 --- src/services/dialogService.ts | 2 + src/services/toolExecutor.ts | 27 ++++-- src/utils/iverilogRunner.ts | 152 ++++++++++++++++++++++++++++++---- src/utils/messageHandler.ts | 44 ++++++++++ src/utils/readFiles.ts | 56 +++++++++++++ 5 files changed, 256 insertions(+), 25 deletions(-) diff --git a/src/services/dialogService.ts b/src/services/dialogService.ts index 1e7c2c5..065a4c5 100644 --- a/src/services/dialogService.ts +++ b/src/services/dialogService.ts @@ -128,6 +128,8 @@ export class DialogSession { private currentTextSegment: MessageSegment | null = null; private completeCallback: ((segments: MessageSegment[]) => void) | null = null; // 保存完成回调,用于 abort 时触发 + private consecutiveToolErrors = 0; // 连续工具错误计数 + private readonly MAX_CONSECUTIVE_ERRORS = 5; // 最大连续错误次数 constructor(extensionPath: string, existingTaskId?: string) { // 支持复用现有 taskId(用于 Plan 模式确认后继续执行) diff --git a/src/services/toolExecutor.ts b/src/services/toolExecutor.ts index c61c54e..46a36a4 100644 --- a/src/services/toolExecutor.ts +++ b/src/services/toolExecutor.ts @@ -6,7 +6,7 @@ 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 { readFileContent, readDirectory, listDirectory } from "../utils/readFiles"; import { createOrOverwriteFile } from "../utils/createFiles"; import { resolveWorkspaceFilePath, showFileDiff } from "../utils/fileDiff"; import { changeTracker } from "./changeTracker"; @@ -126,8 +126,9 @@ export async function executeToolCall( // 提交成功结果 const result = createSuccessResult(callId, resultText); + console.log(`[ToolExecutor] 准备提交结果: ${toolName}, callId=${callId}`); await submitToolResult(result); - console.log(`[ToolExecutor] 工具执行成功: ${toolName}, callId=${callId}`); + console.log(`[ToolExecutor] 结果提交成功: ${toolName}, callId=${callId}`); } catch (error) { const errorMessage = error instanceof Error ? error.message : "未知错误"; console.error( @@ -136,8 +137,20 @@ export async function executeToolCall( ); // 提交错误结果 - const result = createBusinessErrorResult(callId, errorMessage); - await submitToolResult(result); + try { + const result = createBusinessErrorResult(callId, errorMessage); + console.log(`[ToolExecutor] 准备提交错误结果: ${toolName}, callId=${callId}`); + await submitToolResult(result); + console.log(`[ToolExecutor] 错误结果提交成功: ${toolName}, callId=${callId}`); + } catch (submitError) { + console.error( + `[ToolExecutor] 提交错误结果失败: ${toolName}, callId=${callId}`, + submitError, + ); + throw submitError; + } + // 重新抛出原始错误,让调用方知道工具执行失败 + throw error; } } @@ -274,10 +287,8 @@ 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 || "(目录为空)"; + const files = await listDirectory(dirPath, extensions); + return files.join("\n") || "(目录为空)"; } /** diff --git a/src/utils/iverilogRunner.ts b/src/utils/iverilogRunner.ts index ac0af58..4da95b8 100644 --- a/src/utils/iverilogRunner.ts +++ b/src/utils/iverilogRunner.ts @@ -201,6 +201,33 @@ async function findVerilogFiles(dir: string): Promise { return verilogFiles; } +/** + * 递归查找目录下所有 VCD 文件 + */ +async function findVcdFilesRecursive(dir: string): Promise { + const vcdFiles: string[] = []; + + async function searchDir(currentDir: string) { + const dirUri = vscode.Uri.file(currentDir); + const entries = await vscode.workspace.fs.readDirectory(dirUri); + + for (const [fileName, fileType] of entries) { + const filePath = path.join(currentDir, fileName); + + if (fileType === vscode.FileType.Directory) { + if (!fileName.startsWith(".") && fileName !== "node_modules") { + await searchDir(filePath); + } + } else if (fileType === vscode.FileType.File && fileName.endsWith(".vcd")) { + vcdFiles.push(filePath); + } + } + } + + await searchDir(dir); + return vcdFiles; +} + /** * 获取 iverilog 可执行文件路径 */ @@ -304,8 +331,8 @@ export async function generateVCD( IVERILOG_ROOT: path.join(extensionPath, "tools", "iverilog"), }; - // 5. 构建 iverilog 编译参数 - const compileArgs = ["-o", outputFile, ...projectCheck.allVerilogFiles]; + // 5. 构建 iverilog 编译参数(启用 SystemVerilog 2012 标准) + const compileArgs = ["-g2012", "-o", outputFile, ...projectCheck.allVerilogFiles]; console.log("执行编译命令:", iverilogPath, compileArgs.join(" ")); console.log("IVERILOG_ROOT:", env.IVERILOG_ROOT); @@ -317,7 +344,10 @@ export async function generateVCD( cwd: projectPath, env: env, }); + console.log("编译成功,stdout:", compileResult.stdout); + console.log("编译成功,stderr:", compileResult.stderr); } catch (error: any) { + console.error("编译失败:", error); return { success: false, message: `IC Coder编译器编译失败:\n${error.message}`, @@ -326,9 +356,20 @@ export async function generateVCD( }; } + // 6.1 检查 .vvp 文件是否生成 + const fs = require("fs"); + if (!fs.existsSync(outputFile)) { + return { + success: false, + message: `编译未生成 .vvp 文件: ${outputFile}`, + stderr: compileResult.stderr, + stdout: compileResult.stdout, + }; + } + console.log("已生成 .vvp 文件:", outputFile); + // 6.5. 删除 shebang 行(修复 Windows 上的 vvp 解析错误) try { - const fs = require("fs"); const vvpContent = fs.readFileSync(outputFile, "utf8"); const lines = vvpContent.split("\n"); @@ -336,9 +377,62 @@ export async function generateVCD( const cleanedContent = lines.slice(1).join("\n"); fs.writeFileSync(outputFile, cleanedContent, "utf8"); console.log("已删除 .vvp 文件的 shebang 行"); + } else { + console.log(".vvp 文件无 shebang 行,跳过"); } } catch (error) { - console.warn("删除 shebang 失败,继续执行:", error); + console.error("删除 shebang 失败:", error); + return { + success: false, + message: `处理 .vvp 文件失败: ${error instanceof Error ? error.message : "未知错误"}`, + }; + } + + // 6.6. 检查并创建 VCD 输出目录,并处理 Windows 路径问题 + try { + const tbPath = projectCheck.testbenchFile; + if (tbPath && fs.existsSync(tbPath)) { + const tbContent = fs.readFileSync(tbPath, "utf8"); + const dumpfileMatch = tbContent.match(/\$dumpfile\s*\(\s*["']([^"']+)["']\s*\)/); + if (dumpfileMatch) { + const vcdPath = dumpfileMatch[1]; + const vcdDir = path.dirname(vcdPath); + console.log(`testbench 中的 VCD 路径: ${vcdPath}`); + + if (vcdDir && vcdDir !== "." && vcdDir !== "") { + const vcdDirPath = path.join(projectPath, vcdDir); + console.log(`检查 VCD 目录: ${vcdDirPath}`); + if (!fs.existsSync(vcdDirPath)) { + fs.mkdirSync(vcdDirPath, { recursive: true }); + console.log(`已创建 VCD 输出目录: ${vcdDirPath}`); + } else { + console.log(`VCD 目录已存在: ${vcdDirPath}`); + } + + // Windows 兼容性:修改 .vvp 文件中的路径,将正斜杠替换为反斜杠 + if (process.platform === "win32" && vcdPath.includes("/")) { + const vvpContent = fs.readFileSync(outputFile, "utf8"); + const windowsPath = vcdPath.replace(/\//g, "\\\\"); + const modifiedContent = vvpContent.replace( + new RegExp(`"${vcdPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`, 'g'), + `"${windowsPath}"` + ); + fs.writeFileSync(outputFile, modifiedContent, "utf8"); + console.log(`已修正 VCD 路径: ${vcdPath} -> ${windowsPath}`); + } + } else { + console.log("VCD 文件在根目录,无需创建子目录"); + } + } else { + console.warn("testbench 中未找到 $dumpfile 语句"); + } + } + } catch (error) { + console.error("处理 VCD 路径失败:", error); + return { + success: false, + message: `处理 VCD 路径失败: ${error instanceof Error ? error.message : "未知错误"}`, + }; } // 7. 执行仿真生成 VCD @@ -351,7 +445,11 @@ export async function generateVCD( cwd: projectPath, env: env, }); + console.log("仿真执行完成"); + console.log("仿真 stdout:", simResult.stdout); + console.log("仿真 stderr:", simResult.stderr); } catch (error: any) { + console.error("仿真失败:", error); return { success: false, message: `VVP 仿真失败:\n${error.message}`, @@ -361,16 +459,38 @@ export async function generateVCD( } // 8. 查找生成的 VCD 文件 - const projectUri = vscode.Uri.file(projectPath); - const entries = await vscode.workspace.fs.readDirectory(projectUri); - const vcdFiles = entries - .filter( - ([fileName, fileType]) => - fileType === vscode.FileType.File && fileName.endsWith(".vcd"), - ) - .map(([fileName]) => fileName); + let vcdFile: string | null = null; - if (vcdFiles.length === 0) { + // 8.1 尝试从 testbench 中提取 VCD 路径 + try { + const fs = require("fs"); + const tbPath = projectCheck.testbenchFile; + if (tbPath && fs.existsSync(tbPath)) { + const tbContent = fs.readFileSync(tbPath, "utf8"); + const dumpfileMatch = tbContent.match(/\$dumpfile\s*\(\s*["']([^"']+)["']\s*\)/); + if (dumpfileMatch) { + const vcdPath = dumpfileMatch[1]; + const absoluteVcdPath = path.join(projectPath, vcdPath); + if (fs.existsSync(absoluteVcdPath)) { + vcdFile = absoluteVcdPath; + console.log(`找到 VCD 文件(从 testbench): ${vcdFile}`); + } + } + } + } catch (error) { + console.warn("从 testbench 提取 VCD 路径失败:", error); + } + + // 8.2 如果未找到,递归搜索项目目录 + if (!vcdFile) { + const foundFiles = await findVcdFilesRecursive(projectPath); + if (foundFiles.length > 0) { + vcdFile = foundFiles[0]; + console.log(`找到 VCD 文件(递归搜索): ${vcdFile}`); + } + } + + if (!vcdFile) { return { success: false, message: @@ -379,9 +499,6 @@ export async function generateVCD( }; } - // 使用找到的第一个 VCD 文件 - const vcdFile = path.join(projectPath, vcdFiles[0]); - // 9. 清理中间文件 try { const outputUri = vscode.Uri.file(outputFile); @@ -594,8 +711,9 @@ export async function generateMultiVCD( console.log(`[generateMultiVCD] 仿真模块: ${module.name} (${macroName})`); try { - // 编译(带宏定义) + // 编译(带宏定义,启用 SystemVerilog 2012 标准) const compileArgs = [ + "-g2012", `-D${macroName}`, "-o", outputFile, diff --git a/src/utils/messageHandler.ts b/src/utils/messageHandler.ts index b061cea..d5253b2 100644 --- a/src/utils/messageHandler.ts +++ b/src/utils/messageHandler.ts @@ -37,6 +37,9 @@ let currentSession: DialogSession | null = null; /** 最后一个活跃的 taskId(用于压缩等操作) */ let lastTaskId: string | null = null; +/** 离线模式仿真模拟标志(防止重复触发) */ +let offlineSimulationTriggered = false; + async function trackFileChange( filePath: string, oldContent: string, @@ -258,6 +261,8 @@ async function handleUserMessageWithBackend( ); // 保存 taskId 用于后续操作(如压缩) lastTaskId = currentSession.getTaskId(); + // 重置离线模式仿真标志(新会话开始) + offlineSimulationTriggered = false; console.log( "[MessageHandler] 创建会话: taskId=", lastTaskId, @@ -286,6 +291,45 @@ async function handleUserMessageWithBackend( command: "updateSegments", segments: segments, }); + + // 【离线部署模式】检测代码生成完成消息,模拟仿真流程 + if (!offlineSimulationTriggered) { + const hasCompletionMessage = segments.some(seg => + seg.type === 'text' && + seg.content?.includes('【代码生成完成】') && + seg.content?.includes('语法检查:已通过') + ); + + if (hasCompletionMessage) { + offlineSimulationTriggered = true; + console.log('[离线模式] 检测到代码生成完成,开始模拟仿真流程'); + + // 立即点亮 Simulation 阶段 + panel.webview.postMessage({ + type: "updateProgress", + step: "simulation" + }); + + // 随机延时 8-13 秒后完成仿真 + const simulationDelay = 8000 + Math.random() * 5000; + setTimeout(() => { + console.log('[离线模式] 模拟仿真完成,进入 Done 阶段'); + // Simulation 完成,进入 Done + panel.webview.postMessage({ + type: "updateProgress", + step: "done" + }); + + // 再延时 1 秒完成所有步骤 + setTimeout(() => { + console.log('[离线模式] 所有阶段完成'); + panel.webview.postMessage({ + type: "completeProgress" + }); + }, 1000); + }, simulationDelay); + } + } }, onToolStart: (toolName) => { diff --git a/src/utils/readFiles.ts b/src/utils/readFiles.ts index e92f8d7..255e701 100644 --- a/src/utils/readFiles.ts +++ b/src/utils/readFiles.ts @@ -129,6 +129,62 @@ export async function readDirectory( } } +/** + * 列出目录下的文件和文件夹(不读取内容,仅返回路径) + */ +export async function listDirectory( + dirPath: string, + extensions?: string[] +): Promise { + try { + // 如果是相对路径,转换为绝对路径 + let absolutePath = dirPath; + if (!path.isAbsolute(dirPath)) { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (workspaceFolders && workspaceFolders.length > 0) { + absolutePath = path.join(workspaceFolders[0].uri.fsPath, dirPath); + } + } + + const dirUri = vscode.Uri.file(absolutePath); + + // 检查目录是否存在 + try { + const stat = await vscode.workspace.fs.stat(dirUri); + if (stat.type !== vscode.FileType.Directory) { + throw new Error(`路径不是目录: ${absolutePath}`); + } + } catch (error) { + throw new Error(`目录不存在: ${absolutePath}`); + } + + // 读取目录内容 + const entries = await vscode.workspace.fs.readDirectory(dirUri); + const results: string[] = []; + + for (const [fileName, fileType] of entries) { + if (fileType === vscode.FileType.Directory) { + results.push(fileName + '/'); + } else if (fileType === vscode.FileType.File) { + // 扩展名过滤 + if (extensions && extensions.length > 0) { + const ext = path.extname(fileName); + // 规范化扩展名(支持 "v" 和 ".v" 两种格式) + const normalizedExts = extensions.map(e => e.startsWith('.') ? e : '.' + e); + if (!normalizedExts.includes(ext)) { + continue; + } + } + results.push(fileName); + } + } + + return results; + } catch (error) { + throw error; + } +} + /** * 获取文件信息 */