2 Commits

Author SHA1 Message Date
b9dc631bf7 Merge branch 'feat/ningDeShiDai' of https://git.pengyejiatu.com/pengyejiatu/Ic-coder-plugin into feat/ningDeShiDai 2026-03-10 19:01:38 +08:00
6425496d2e feat: 离线部署模式改进和 SystemVerilog 支持
- 添加离线模式仿真模拟:识别代码生成完成消息后自动模拟仿真流程
- 启用 iverilog SystemVerilog 2012 标准支持(-g2012)
- 优化进度条显示逻辑

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 18:56:53 +08:00
5 changed files with 256 additions and 25 deletions

View File

@ -128,6 +128,8 @@ export class DialogSession {
private currentTextSegment: MessageSegment | null = null; private currentTextSegment: MessageSegment | null = null;
private completeCallback: ((segments: MessageSegment[]) => void) | null = private completeCallback: ((segments: MessageSegment[]) => void) | null =
null; // 保存完成回调,用于 abort 时触发 null; // 保存完成回调,用于 abort 时触发
private consecutiveToolErrors = 0; // 连续工具错误计数
private readonly MAX_CONSECUTIVE_ERRORS = 5; // 最大连续错误次数
constructor(extensionPath: string, existingTaskId?: string) { constructor(extensionPath: string, existingTaskId?: string) {
// 支持复用现有 taskId用于 Plan 模式确认后继续执行) // 支持复用现有 taskId用于 Plan 模式确认后继续执行)

View File

@ -6,7 +6,7 @@ import * as vscode from "vscode";
import * as path from "path"; import * as path from "path";
import * as os from "os"; import * as os from "os";
import * as fs from "fs"; import * as fs from "fs";
import { readFileContent, readDirectory } from "../utils/readFiles"; import { readFileContent, readDirectory, listDirectory } from "../utils/readFiles";
import { createOrOverwriteFile } from "../utils/createFiles"; import { createOrOverwriteFile } from "../utils/createFiles";
import { resolveWorkspaceFilePath, showFileDiff } from "../utils/fileDiff"; import { resolveWorkspaceFilePath, showFileDiff } from "../utils/fileDiff";
import { changeTracker } from "./changeTracker"; import { changeTracker } from "./changeTracker";
@ -126,8 +126,9 @@ export async function executeToolCall(
// 提交成功结果 // 提交成功结果
const result = createSuccessResult(callId, resultText); const result = createSuccessResult(callId, resultText);
console.log(`[ToolExecutor] 准备提交结果: ${toolName}, callId=${callId}`);
await submitToolResult(result); await submitToolResult(result);
console.log(`[ToolExecutor] 工具执行成功: ${toolName}, callId=${callId}`); console.log(`[ToolExecutor] 结果提交成功: ${toolName}, callId=${callId}`);
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : "未知错误"; const errorMessage = error instanceof Error ? error.message : "未知错误";
console.error( console.error(
@ -136,8 +137,20 @@ export async function executeToolCall(
); );
// 提交错误结果 // 提交错误结果
const result = createBusinessErrorResult(callId, errorMessage); try {
await submitToolResult(result); 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<string> {
const dirPath = args.path || "."; const dirPath = args.path || ".";
const extensions = args.extension ? [args.extension] : undefined; const extensions = args.extension ? [args.extension] : undefined;
const files = await readDirectory(dirPath, extensions); const files = await listDirectory(dirPath, extensions);
const fileList = files.map((f) => f.path).join("\n"); return files.join("\n") || "(目录为空)";
return fileList || "(目录为空)";
} }
/** /**

View File

@ -201,6 +201,33 @@ async function findVerilogFiles(dir: string): Promise<string[]> {
return verilogFiles; return verilogFiles;
} }
/**
* 递归查找目录下所有 VCD 文件
*/
async function findVcdFilesRecursive(dir: string): Promise<string[]> {
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 可执行文件路径 * 获取 iverilog 可执行文件路径
*/ */
@ -304,8 +331,8 @@ export async function generateVCD(
IVERILOG_ROOT: path.join(extensionPath, "tools", "iverilog"), IVERILOG_ROOT: path.join(extensionPath, "tools", "iverilog"),
}; };
// 5. 构建 iverilog 编译参数 // 5. 构建 iverilog 编译参数(启用 SystemVerilog 2012 标准)
const compileArgs = ["-o", outputFile, ...projectCheck.allVerilogFiles]; const compileArgs = ["-g2012", "-o", outputFile, ...projectCheck.allVerilogFiles];
console.log("执行编译命令:", iverilogPath, compileArgs.join(" ")); console.log("执行编译命令:", iverilogPath, compileArgs.join(" "));
console.log("IVERILOG_ROOT:", env.IVERILOG_ROOT); console.log("IVERILOG_ROOT:", env.IVERILOG_ROOT);
@ -317,7 +344,10 @@ export async function generateVCD(
cwd: projectPath, cwd: projectPath,
env: env, env: env,
}); });
console.log("编译成功stdout:", compileResult.stdout);
console.log("编译成功stderr:", compileResult.stderr);
} catch (error: any) { } catch (error: any) {
console.error("编译失败:", error);
return { return {
success: false, success: false,
message: `IC Coder编译器编译失败:\n${error.message}`, 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 解析错误) // 6.5. 删除 shebang 行(修复 Windows 上的 vvp 解析错误)
try { try {
const fs = require("fs");
const vvpContent = fs.readFileSync(outputFile, "utf8"); const vvpContent = fs.readFileSync(outputFile, "utf8");
const lines = vvpContent.split("\n"); const lines = vvpContent.split("\n");
@ -336,9 +377,62 @@ export async function generateVCD(
const cleanedContent = lines.slice(1).join("\n"); const cleanedContent = lines.slice(1).join("\n");
fs.writeFileSync(outputFile, cleanedContent, "utf8"); fs.writeFileSync(outputFile, cleanedContent, "utf8");
console.log("已删除 .vvp 文件的 shebang 行"); console.log("已删除 .vvp 文件的 shebang 行");
} else {
console.log(".vvp 文件无 shebang 行,跳过");
} }
} catch (error) { } 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 // 7. 执行仿真生成 VCD
@ -351,7 +445,11 @@ export async function generateVCD(
cwd: projectPath, cwd: projectPath,
env: env, env: env,
}); });
console.log("仿真执行完成");
console.log("仿真 stdout:", simResult.stdout);
console.log("仿真 stderr:", simResult.stderr);
} catch (error: any) { } catch (error: any) {
console.error("仿真失败:", error);
return { return {
success: false, success: false,
message: `VVP 仿真失败:\n${error.message}`, message: `VVP 仿真失败:\n${error.message}`,
@ -361,16 +459,38 @@ export async function generateVCD(
} }
// 8. 查找生成的 VCD 文件 // 8. 查找生成的 VCD 文件
const projectUri = vscode.Uri.file(projectPath); let vcdFile: string | null = null;
const entries = await vscode.workspace.fs.readDirectory(projectUri);
const vcdFiles = entries
.filter(
([fileName, fileType]) =>
fileType === vscode.FileType.File && fileName.endsWith(".vcd"),
)
.map(([fileName]) => fileName);
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 { return {
success: false, success: false,
message: message:
@ -379,9 +499,6 @@ export async function generateVCD(
}; };
} }
// 使用找到的第一个 VCD 文件
const vcdFile = path.join(projectPath, vcdFiles[0]);
// 9. 清理中间文件 // 9. 清理中间文件
try { try {
const outputUri = vscode.Uri.file(outputFile); const outputUri = vscode.Uri.file(outputFile);
@ -594,8 +711,9 @@ export async function generateMultiVCD(
console.log(`[generateMultiVCD] 仿真模块: ${module.name} (${macroName})`); console.log(`[generateMultiVCD] 仿真模块: ${module.name} (${macroName})`);
try { try {
// 编译(带宏定义) // 编译(带宏定义,启用 SystemVerilog 2012 标准
const compileArgs = [ const compileArgs = [
"-g2012",
`-D${macroName}`, `-D${macroName}`,
"-o", "-o",
outputFile, outputFile,

View File

@ -37,6 +37,9 @@ let currentSession: DialogSession | null = null;
/** 最后一个活跃的 taskId用于压缩等操作 */ /** 最后一个活跃的 taskId用于压缩等操作 */
let lastTaskId: string | null = null; let lastTaskId: string | null = null;
/** 离线模式仿真模拟标志(防止重复触发) */
let offlineSimulationTriggered = false;
async function trackFileChange( async function trackFileChange(
filePath: string, filePath: string,
oldContent: string, oldContent: string,
@ -258,6 +261,8 @@ async function handleUserMessageWithBackend(
); );
// 保存 taskId 用于后续操作(如压缩) // 保存 taskId 用于后续操作(如压缩)
lastTaskId = currentSession.getTaskId(); lastTaskId = currentSession.getTaskId();
// 重置离线模式仿真标志(新会话开始)
offlineSimulationTriggered = false;
console.log( console.log(
"[MessageHandler] 创建会话: taskId=", "[MessageHandler] 创建会话: taskId=",
lastTaskId, lastTaskId,
@ -294,6 +299,45 @@ async function handleUserMessageWithBackend(
command: "updateSegments", command: "updateSegments",
segments: filteredSegments, segments: filteredSegments,
}); });
// 【离线部署模式】检测代码生成完成消息,模拟仿真流程
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) => { onToolStart: (toolName) => {

View File

@ -129,6 +129,62 @@ export async function readDirectory(
} }
} }
/**
* 列出目录下的文件和文件夹(不读取内容,仅返回路径)
*/
export async function listDirectory(
dirPath: string,
extensions?: string[]
): Promise<string[]> {
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;
}
}
/** /**
* 获取文件信息 * 获取文件信息
*/ */