- api.ts: 新增 IverilogArgs 类型定义 - toolExecutor.ts: 新增 executeIverilog 函数,支持直接执行 iverilog 命令
526 lines
15 KiB
TypeScript
526 lines
15 KiB
TypeScript
/**
|
||
* 工具执行器
|
||
* 接收后端的 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, 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<void> {
|
||
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<string> {
|
||
const content = await readFileContent(args.path);
|
||
return content;
|
||
}
|
||
|
||
/**
|
||
* 执行 file_write 工具
|
||
*/
|
||
async function executeFileWrite(args: FileWriteArgs): Promise<string> {
|
||
await createOrOverwriteFile(args.path, args.content);
|
||
|
||
// 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<string> {
|
||
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}`);
|
||
}
|
||
|
||
// 删除文件
|
||
fs.unlinkSync(absolutePath);
|
||
|
||
// 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<string> {
|
||
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<string> {
|
||
// 检查 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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 执行 iverilog 工具
|
||
* 直接执行 iverilog 命令
|
||
*/
|
||
async function executeIverilog(
|
||
args: IverilogArgs,
|
||
context: ToolExecutorContext
|
||
): Promise<string> {
|
||
// 检查 iverilog 是否可用
|
||
const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
|
||
if (!iverilogCheck.available) {
|
||
throw new Error(`iverilog 不可用: ${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<string> {
|
||
// 获取工作区路径
|
||
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<string> {
|
||
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<string> {
|
||
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<string> {
|
||
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
|
||
};
|
||
}
|