- creditsService.ts: 新增余额缓存和检测服务 - apiClient.ts: 新增 getCreditBalance() API 调用 - dialogService.ts: SSE credit_update 事件更新余额缓存 - messageHandler.ts: 发送消息前检测余额,低于5点阻止发送
1058 lines
31 KiB
TypeScript
1058 lines
31 KiB
TypeScript
import * as vscode from "vscode";
|
||
import * as path from "path";
|
||
import { readFileContent } from "./readFiles";
|
||
import {
|
||
createFile,
|
||
createOrOverwriteFile,
|
||
deleteFile,
|
||
updateFile,
|
||
renameFile,
|
||
replaceFile,
|
||
} from "./createFiles";
|
||
import {
|
||
generateVCD,
|
||
checkVerilogProject,
|
||
checkIverilogAvailable,
|
||
} from "./iverilogRunner";
|
||
import { ChatHistoryManager } from "./chatHistoryManager";
|
||
import { dialogManager, DialogSession } from "../services/dialogService";
|
||
import { userInteractionManager } from "../services/userInteraction";
|
||
import { healthCheck } from "../services/apiClient";
|
||
import { checkBalanceBeforeSend } from "../services/creditsService";
|
||
|
||
import type { RunMode, ServiceTier } from "../types/api";
|
||
|
||
/** 是否使用后端服务(可通过配置控制) */
|
||
let useBackendService = true;
|
||
|
||
/** 当前对话会话 */
|
||
let currentSession: DialogSession | null = null;
|
||
|
||
/** 最后一个活跃的 taskId(用于压缩等操作) */
|
||
let lastTaskId: string | null = null;
|
||
|
||
/** 待执行的计划(Plan 模式确认后自动执行) */
|
||
let pendingPlanExecution: {
|
||
panel: vscode.WebviewPanel;
|
||
planTitle: string;
|
||
extensionPath: string;
|
||
taskId: string; // 保存 taskId 以便复用
|
||
serviceTier?: ServiceTier; // 保存服务等级
|
||
} | null = null;
|
||
|
||
/**
|
||
* 设置待执行的计划(由 ICHelperPanel 调用)
|
||
*/
|
||
export function setPendingPlanExecution(
|
||
panel: vscode.WebviewPanel,
|
||
planTitle: string,
|
||
extensionPath: string,
|
||
taskId: string,
|
||
serviceTier?: ServiceTier
|
||
): void {
|
||
pendingPlanExecution = { panel, planTitle, extensionPath, taskId, serviceTier };
|
||
console.log("[MessageHandler] 设置待执行计划:", planTitle, "taskId:", taskId, "serviceTier:", serviceTier);
|
||
}
|
||
|
||
/**
|
||
* 处理用户消息
|
||
*/
|
||
export async function handleUserMessage(
|
||
panel: vscode.WebviewPanel,
|
||
text: string,
|
||
extensionPath?: string,
|
||
mode?: RunMode,
|
||
serviceTier?: ServiceTier // 新增:服务等级参数
|
||
) {
|
||
console.log("收到用户消息:", text);
|
||
|
||
// 记录用户消息到历史(允许失败,不阻塞主流程)
|
||
try {
|
||
const historyManager = ChatHistoryManager.getInstance();
|
||
await historyManager.addUserMessage(text);
|
||
} catch (error) {
|
||
console.warn("记录消息历史失败(可能没有打开工作区):", error);
|
||
}
|
||
|
||
// 设置 WebView 面板用于用户交互
|
||
userInteractionManager.setWebviewPanel(panel);
|
||
|
||
// 检查是否是 VCD 生成命令(本地处理)
|
||
if (isVCDGenerationCommand(text)) {
|
||
await handleVCDGeneration(panel, extensionPath || "");
|
||
return;
|
||
}
|
||
|
||
// 检查是否是文件操作命令(本地处理)
|
||
const fileOperation = parseFileOperation(text);
|
||
if (fileOperation) {
|
||
console.log("执行文件操作:", fileOperation.type, fileOperation.filePath);
|
||
await handleFileOperation(panel, fileOperation);
|
||
return;
|
||
}
|
||
|
||
// 发送前检测余额
|
||
const balanceCheck = await checkBalanceBeforeSend();
|
||
if (!balanceCheck.allowed) {
|
||
console.warn("[MessageHandler] 余额不足,阻止发送:", balanceCheck.message);
|
||
// 显示错误提示
|
||
const selection = await vscode.window.showWarningMessage(
|
||
balanceCheck.message || "资源点余额不足",
|
||
"去充值"
|
||
);
|
||
if (selection === "去充值") {
|
||
vscode.env.openExternal(vscode.Uri.parse("https://iccoder.com/recharge"));
|
||
}
|
||
// 恢复输入状态
|
||
panel.webview.postMessage({
|
||
command: "updateSegments",
|
||
segments: [],
|
||
isComplete: true,
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 尝试使用后端服务
|
||
if (useBackendService && extensionPath) {
|
||
try {
|
||
await handleUserMessageWithBackend(panel, text, extensionPath, mode, undefined, serviceTier);
|
||
return;
|
||
} catch (error) {
|
||
console.error("后端服务不可用:", error);
|
||
panel.webview.postMessage({
|
||
command: "updateStatus",
|
||
text: "后端服务不可用",
|
||
type: "error",
|
||
});
|
||
// 恢复输入状态
|
||
panel.webview.postMessage({
|
||
command: "updateSegments",
|
||
segments: [],
|
||
isComplete: true,
|
||
});
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 如果没有 extensionPath,显示错误
|
||
panel.webview.postMessage({
|
||
command: "updateStatus",
|
||
text: "无法处理消息:缺少必要参数",
|
||
type: "error",
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 使用后端服务处理用户消息
|
||
*/
|
||
async function handleUserMessageWithBackend(
|
||
panel: vscode.WebviewPanel,
|
||
text: string,
|
||
extensionPath: string,
|
||
mode?: RunMode,
|
||
reuseTaskId?: string, // 可选,复用现有 taskId(用于 Plan 模式确认后继续执行)
|
||
serviceTier?: ServiceTier // 新增:服务等级参数
|
||
): Promise<void> {
|
||
const historyManager = ChatHistoryManager.getInstance();
|
||
|
||
// 获取 historyManager 中的 taskId(由 ICHelperPanel 创建)
|
||
// 优先使用 reuseTaskId,其次使用 historyManager 的 taskId
|
||
const taskIdToUse = reuseTaskId || historyManager.getCurrentTaskId();
|
||
|
||
// 创建或复用会话
|
||
if (!currentSession || !currentSession.active) {
|
||
currentSession = dialogManager.createSession(extensionPath, taskIdToUse || undefined);
|
||
// 保存 taskId 用于后续操作(如压缩)
|
||
lastTaskId = currentSession.getTaskId();
|
||
console.log("[MessageHandler] 创建会话: taskId=", lastTaskId, "来源=", taskIdToUse ? "historyManager" : "新生成");
|
||
}
|
||
|
||
// 显示状态栏
|
||
panel.webview.postMessage({
|
||
command: "updateStatus",
|
||
text: "思考中...",
|
||
type: "thinking",
|
||
});
|
||
|
||
return new Promise((resolve, reject) => {
|
||
currentSession!.sendMessage(
|
||
text,
|
||
{
|
||
onText: (fullText, isStreaming) => {
|
||
// 不再单独处理文本,统一通过 onSegmentUpdate 处理
|
||
},
|
||
|
||
onSegmentUpdate: (segments) => {
|
||
// 实时发送段落更新,按后端返回顺序展示
|
||
panel.webview.postMessage({
|
||
command: "updateSegments",
|
||
segments: segments,
|
||
});
|
||
},
|
||
|
||
onToolStart: (toolName) => {
|
||
// 更新状态栏
|
||
panel.webview.postMessage({
|
||
command: "updateStatus",
|
||
text: `正在执行 ${toolName}...`,
|
||
type: "working",
|
||
});
|
||
},
|
||
|
||
onToolComplete: (toolName, result) => {
|
||
// 工具完成,不需要单独处理,通过 onSegmentUpdate 统一更新
|
||
},
|
||
|
||
onToolError: (toolName, error) => {
|
||
// 工具错误,不需要单独处理,通过 onSegmentUpdate 统一更新
|
||
},
|
||
|
||
onQuestion: (askId, question, options) => {
|
||
// 只更新状态栏,问题显示由 onSegmentUpdate 统一处理
|
||
panel.webview.postMessage({
|
||
command: "updateStatus",
|
||
text: "等待用户回答...",
|
||
type: "working",
|
||
});
|
||
},
|
||
|
||
onComplete: async (segments) => {
|
||
// 隐藏状态栏
|
||
panel.webview.postMessage({
|
||
command: "hideStatus",
|
||
});
|
||
|
||
// 最后一次发送完整的段落
|
||
console.log("[MessageHandler] 对话完成, 段落数:", segments.length);
|
||
|
||
const result = await panel.webview.postMessage({
|
||
command: "updateSegments",
|
||
segments: segments,
|
||
isComplete: true,
|
||
});
|
||
console.log("[MessageHandler] postMessage 返回值:", result);
|
||
|
||
// 保存完整的 segments 到历史记录
|
||
try {
|
||
// 将完整的 segments 保存到一条 AI 消息中
|
||
// 这样加载时可以完整还原对话样式
|
||
const textContent = segments
|
||
.filter((s) => s.type === "text" && s.content)
|
||
.map((s) => s.content)
|
||
.join("\n");
|
||
|
||
await historyManager.addAiMessage(textContent, undefined, segments);
|
||
} catch (error) {
|
||
console.warn("保存AI响应历史失败:", error);
|
||
}
|
||
|
||
// 检查是否有待执行的计划(Plan 模式确认后自动执行)
|
||
if (pendingPlanExecution) {
|
||
const {
|
||
panel: execPanel,
|
||
planTitle,
|
||
extensionPath: execPath,
|
||
taskId: reuseTaskId,
|
||
serviceTier: savedServiceTier,
|
||
} = pendingPlanExecution;
|
||
pendingPlanExecution = null;
|
||
console.log(
|
||
"[MessageHandler] 自动执行计划:",
|
||
planTitle,
|
||
"复用 taskId:",
|
||
reuseTaskId,
|
||
"serviceTier:",
|
||
savedServiceTier
|
||
);
|
||
|
||
// 延迟一小段时间确保当前对话完全结束
|
||
setTimeout(async () => {
|
||
try {
|
||
// 复用 taskId 创建新会话,确保知识图谱数据不丢失
|
||
await handleUserMessageWithBackend(
|
||
execPanel,
|
||
`请按照刚才的计划执行:${planTitle}`,
|
||
execPath,
|
||
"agent",
|
||
reuseTaskId, // 复用 Plan 模式的 taskId
|
||
savedServiceTier // 传递保存的服务等级
|
||
);
|
||
} catch (err) {
|
||
console.error("[MessageHandler] 自动执行计划失败:", err);
|
||
}
|
||
}, 500);
|
||
}
|
||
|
||
resolve();
|
||
},
|
||
|
||
onError: (message) => {
|
||
panel.webview.postMessage({
|
||
command: "hideLoading",
|
||
});
|
||
panel.webview.postMessage({
|
||
command: "receiveMessage",
|
||
text: `❌ 错误: ${message}`,
|
||
});
|
||
// 恢复输入状态
|
||
panel.webview.postMessage({
|
||
command: "updateSegments",
|
||
segments: [],
|
||
isComplete: true,
|
||
});
|
||
reject(new Error(message));
|
||
},
|
||
|
||
onNotification: (message) => {
|
||
vscode.window.showInformationMessage(message);
|
||
},
|
||
|
||
onContextUsage: (data) => {
|
||
// 发送上下文使用量到 WebView
|
||
panel.webview.postMessage({
|
||
command: "contextUsage",
|
||
currentTokens: data.currentTokens,
|
||
maxTokens: data.maxTokens,
|
||
percentage: data.percentage,
|
||
});
|
||
},
|
||
|
||
onPhaseProgress: (phaseId, status) => {
|
||
// 发送阶段进度更新到 WebView
|
||
// 映射 phaseId: sim -> simulation
|
||
const stepMap: Record<string, string> = {
|
||
spec: "spec",
|
||
design: "design",
|
||
sim: "simulation",
|
||
done: "done",
|
||
};
|
||
const step = stepMap[phaseId] || phaseId;
|
||
|
||
if (status === "current") {
|
||
// 显示进度条并更新到当前步骤
|
||
panel.webview.postMessage({ type: "showProgress" });
|
||
panel.webview.postMessage({ type: "updateProgress", step });
|
||
} else if (status === "completed") {
|
||
// 更新到下一步(或完成)
|
||
const steps = ["spec", "design", "simulation", "done"];
|
||
const currentIndex = steps.indexOf(step);
|
||
if (currentIndex < steps.length - 1) {
|
||
panel.webview.postMessage({
|
||
type: "updateProgress",
|
||
step: steps[currentIndex + 1],
|
||
});
|
||
} else {
|
||
panel.webview.postMessage({ type: "completeProgress" });
|
||
}
|
||
}
|
||
},
|
||
},
|
||
mode,
|
||
serviceTier // 传递服务等级
|
||
);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 处理用户回答(从 WebView 调用)
|
||
*/
|
||
export async function handleUserAnswer(
|
||
askId: string,
|
||
selected?: string[],
|
||
customInput?: string
|
||
): Promise<void> {
|
||
if (currentSession) {
|
||
await currentSession.submitAnswer(askId, selected, customInput);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 中止当前对话
|
||
*/
|
||
export async function abortCurrentDialog(): Promise<void> {
|
||
if (currentSession) {
|
||
// 保存当前已有的对话内容
|
||
const segments = currentSession.getSegments();
|
||
if (segments && segments.length > 0) {
|
||
try {
|
||
const historyManager = ChatHistoryManager.getInstance();
|
||
const textContent = segments
|
||
.filter((s) => s.type === "text" && s.content)
|
||
.map((s) => s.content)
|
||
.join("\n");
|
||
|
||
// 添加中止标记
|
||
const abortedContent = textContent + "\n\n[对话已被用户中止]";
|
||
await historyManager.addAiMessage(abortedContent, undefined, segments);
|
||
console.log("[MessageHandler] 已保存中止前的对话内容");
|
||
} catch (error) {
|
||
console.warn("[MessageHandler] 保存中止对话失败:", error);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 通知 WebView 重置分段消息容器
|
||
const panel = userInteractionManager.getWebviewPanel();
|
||
if (panel) {
|
||
panel.webview.postMessage({ command: "resetSegmentedMessage" });
|
||
console.log("[MessageHandler] 已发送重置分段消息命令");
|
||
}
|
||
|
||
dialogManager.abortCurrentSession();
|
||
currentSession = null;
|
||
}
|
||
|
||
/**
|
||
* 获取当前会话的 taskId
|
||
*/
|
||
export function getCurrentTaskId(): string | null {
|
||
return currentSession?.getTaskId() || lastTaskId;
|
||
}
|
||
|
||
/**
|
||
* 设置最后的 taskId(加载历史会话时调用)
|
||
*/
|
||
export function setLastTaskId(taskId: string): void {
|
||
lastTaskId = taskId;
|
||
console.log("[MessageHandler] 设置 lastTaskId:", taskId);
|
||
}
|
||
|
||
/**
|
||
* 处理计划操作(Plan 模式)
|
||
* @param panel WebView 面板
|
||
* @param action 操作类型:confirm/modify/cancel
|
||
* @param planTitle 计划标题
|
||
* @param extensionPath 扩展路径
|
||
*/
|
||
export async function handlePlanAction(
|
||
panel: vscode.WebviewPanel,
|
||
action: string,
|
||
planTitle: string,
|
||
extensionPath: string,
|
||
serviceTier?: ServiceTier
|
||
): Promise<void> {
|
||
console.log("[handlePlanAction] action:", action, "planTitle:", planTitle, "serviceTier:", serviceTier);
|
||
|
||
switch (action) {
|
||
case "confirm":
|
||
// 确认执行:切换到 Agent 模式并发送执行消息
|
||
panel.webview.postMessage({
|
||
command: "switchMode",
|
||
mode: "agent",
|
||
});
|
||
// 发送执行消息
|
||
await handleUserMessage(
|
||
panel,
|
||
`请按照刚才的计划执行:${planTitle}`,
|
||
extensionPath,
|
||
"agent",
|
||
serviceTier
|
||
);
|
||
break;
|
||
|
||
case "modify":
|
||
// 修改计划:提示用户输入修改建议
|
||
const modification = await vscode.window.showInputBox({
|
||
prompt: "请输入您对计划的修改建议",
|
||
placeHolder: "例如:第2步需要先检查文件是否存在...",
|
||
ignoreFocusOut: true,
|
||
});
|
||
if (modification) {
|
||
await handleUserMessage(
|
||
panel,
|
||
`请根据以下建议修改计划:${modification}`,
|
||
extensionPath,
|
||
"plan",
|
||
serviceTier
|
||
);
|
||
}
|
||
break;
|
||
|
||
case "cancel":
|
||
// 取消计划:通知用户
|
||
panel.webview.postMessage({
|
||
command: "addMessage",
|
||
text: "计划已取消。",
|
||
sender: "bot",
|
||
});
|
||
break;
|
||
|
||
default:
|
||
console.warn("[handlePlanAction] 未知操作:", action);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解析文件操作命令
|
||
*/
|
||
function parseFileOperation(text: string): {
|
||
type: "create" | "delete" | "read" | "update" | "rename" | "replace";
|
||
filePath: string;
|
||
content?: string;
|
||
newPath?: string;
|
||
searchText?: string;
|
||
replaceText?: string;
|
||
} | null {
|
||
const lowerText = text.toLowerCase().trim();
|
||
|
||
// 匹配创建文件:创建一个 xxx.ts 文件
|
||
const createMatch = lowerText.match(/创建(?:一个)?(.+?\.\w+)(?:文件)?/);
|
||
if (createMatch) {
|
||
const filePath = createMatch[1].trim();
|
||
return {
|
||
type: "create",
|
||
filePath: filePath,
|
||
content: getDefaultContent(filePath),
|
||
};
|
||
}
|
||
|
||
// 匹配删除文件:删除 xxx.ts 文件
|
||
const deleteMatch = lowerText.match(/删除(.+?\.\w+)(?:文件)?/);
|
||
if (deleteMatch) {
|
||
const filePath = deleteMatch[1].trim();
|
||
return {
|
||
type: "delete",
|
||
filePath: filePath,
|
||
};
|
||
}
|
||
|
||
// 匹配重命名文件:将 xxx.ts 重命名为 yyy.ts 或 把 xxx.ts 改名为 yyy.ts(优先匹配,避免被修改匹配)
|
||
const renameMatch = lowerText.match(
|
||
/(?:将|把)\s*(.+?\.\w+)\s*(?:重命名|改名)\s*(?:为|成)\s*(.+?\.\w+)/
|
||
);
|
||
if (renameMatch) {
|
||
const oldPath = renameMatch[1].trim();
|
||
const newPath = renameMatch[2].trim();
|
||
return {
|
||
type: "rename",
|
||
filePath: oldPath,
|
||
newPath: newPath,
|
||
};
|
||
}
|
||
|
||
// 匹配替换内容:支持多种格式
|
||
// 格式1: 在 xxx.ts 中将 "aaa" 替换为 "bbb"
|
||
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
|
||
// 格式3: 将 xxx.ts 文件 'aaa' 替换为 'bbb'
|
||
const replaceMatch1 = lowerText.match(
|
||
/在\s*(.+?\.\w+)\s*(?:文件)?中?\s*(?:将|把)\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/
|
||
);
|
||
if (replaceMatch1) {
|
||
const filePath = replaceMatch1[1].trim();
|
||
const searchText = replaceMatch1[2].trim();
|
||
const replaceText = replaceMatch1[3].trim();
|
||
return {
|
||
type: "replace",
|
||
filePath: filePath,
|
||
searchText: searchText,
|
||
replaceText: replaceText,
|
||
};
|
||
}
|
||
|
||
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
|
||
const replaceMatch2 = lowerText.match(
|
||
/(?:将|把)\s*(.+?\.\w+)\s*(?:文件)?\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/
|
||
);
|
||
if (replaceMatch2) {
|
||
const filePath = replaceMatch2[1].trim();
|
||
const searchText = replaceMatch2[2].trim();
|
||
const replaceText = replaceMatch2[3].trim();
|
||
return {
|
||
type: "replace",
|
||
filePath: filePath,
|
||
searchText: searchText,
|
||
replaceText: replaceText,
|
||
};
|
||
}
|
||
|
||
// 匹配读取文件:读取 xxx.ts 文件 或 打开 xxx.ts
|
||
const readMatch = lowerText.match(/(?:读取|打开)\s*(.+?\.\w+)\s*(?:文件)?/);
|
||
if (readMatch) {
|
||
const filePath = readMatch[1].trim();
|
||
return {
|
||
type: "read",
|
||
filePath: filePath,
|
||
};
|
||
}
|
||
|
||
// 匹配修改文件:修改 xxx.ts 文件(放在最后,避免误匹配)
|
||
const updateMatch = lowerText.match(/修改\s*(.+?\.\w+)\s*(?:文件)?/);
|
||
if (updateMatch) {
|
||
const filePath = updateMatch[1].trim();
|
||
return {
|
||
type: "update",
|
||
filePath: filePath,
|
||
};
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 处理文件操作
|
||
*/
|
||
async function handleFileOperation(
|
||
panel: vscode.WebviewPanel,
|
||
operation: {
|
||
type: "create" | "delete" | "read" | "update" | "rename" | "replace";
|
||
filePath: string;
|
||
content?: string;
|
||
newPath?: string;
|
||
searchText?: string;
|
||
replaceText?: string;
|
||
}
|
||
) {
|
||
const historyManager = ChatHistoryManager.getInstance();
|
||
|
||
try {
|
||
let responseText = "";
|
||
|
||
switch (operation.type) {
|
||
case "create":
|
||
await createFile(operation.filePath, operation.content || "");
|
||
responseText = `✅ 文件创建成功: ${operation.filePath}`;
|
||
panel.webview.postMessage({
|
||
command: "receiveMessage",
|
||
text: responseText,
|
||
});
|
||
vscode.window.showInformationMessage(
|
||
`文件创建成功: ${operation.filePath}`
|
||
);
|
||
await historyManager.addAiMessage(responseText);
|
||
break;
|
||
|
||
case "delete":
|
||
await deleteFile(operation.filePath);
|
||
responseText = `✅ 文件删除成功: ${operation.filePath}`;
|
||
panel.webview.postMessage({
|
||
command: "receiveMessage",
|
||
text: responseText,
|
||
});
|
||
vscode.window.showInformationMessage(
|
||
`文件删除成功: ${operation.filePath}`
|
||
);
|
||
await historyManager.addAiMessage(responseText);
|
||
break;
|
||
|
||
case "read":
|
||
const content = await readFileContent(operation.filePath);
|
||
panel.webview.postMessage({
|
||
command: "fileContent",
|
||
content: content,
|
||
filePath: operation.filePath,
|
||
});
|
||
await historyManager.addAiMessage(`读取文件: ${operation.filePath}`);
|
||
break;
|
||
|
||
case "update":
|
||
const currentContent = await readFileContent(operation.filePath);
|
||
panel.webview.postMessage({
|
||
command: "editFile",
|
||
content: currentContent,
|
||
filePath: operation.filePath,
|
||
});
|
||
await historyManager.addAiMessage(`编辑文件: ${operation.filePath}`);
|
||
break;
|
||
|
||
case "rename":
|
||
if (!operation.newPath) {
|
||
throw new Error("缺少新文件名");
|
||
}
|
||
await renameFile(operation.filePath, operation.newPath);
|
||
responseText = `✅ 文件重命名成功: ${operation.filePath} → ${operation.newPath}`;
|
||
panel.webview.postMessage({
|
||
command: "receiveMessage",
|
||
text: responseText,
|
||
});
|
||
vscode.window.showInformationMessage(
|
||
`文件重命名成功: ${operation.filePath} → ${operation.newPath}`
|
||
);
|
||
await historyManager.addAiMessage(responseText);
|
||
break;
|
||
|
||
case "replace":
|
||
if (!operation.searchText || !operation.replaceText) {
|
||
throw new Error("缺少替换内容");
|
||
}
|
||
await replaceFile(
|
||
operation.filePath,
|
||
operation.searchText,
|
||
operation.replaceText
|
||
);
|
||
responseText = `✅ 文件内容替换成功: ${operation.filePath}`;
|
||
panel.webview.postMessage({
|
||
command: "receiveMessage",
|
||
text: responseText,
|
||
});
|
||
vscode.window.showInformationMessage(
|
||
`文件内容替换成功: ${operation.filePath}`
|
||
);
|
||
await historyManager.addAiMessage(responseText);
|
||
break;
|
||
}
|
||
} catch (error) {
|
||
const errorMsg = error instanceof Error ? error.message : "操作失败";
|
||
panel.webview.postMessage({
|
||
command: "receiveMessage",
|
||
text: `❌ ${errorMsg}`,
|
||
});
|
||
vscode.window.showErrorMessage(errorMsg);
|
||
await historyManager.addAiMessage(`❌ ${errorMsg}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 根据文件扩展名生成默认内容
|
||
*/
|
||
function getDefaultContent(filePath: string): string {
|
||
const ext = filePath.split(".").pop()?.toLowerCase();
|
||
|
||
switch (ext) {
|
||
case "ts":
|
||
return `// ${filePath}\n\nexport {};\n`;
|
||
case "js":
|
||
return `// ${filePath}\n\n`;
|
||
case "json":
|
||
return "{\n \n}\n";
|
||
case "md":
|
||
return `# ${filePath}\n\n`;
|
||
case "html":
|
||
return `<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Document</title>
|
||
</head>
|
||
<body>
|
||
|
||
</body>
|
||
</html>`;
|
||
case "css":
|
||
return `/* ${filePath} */\n\n`;
|
||
case "v":
|
||
case "sv":
|
||
return `// ${filePath}\n\nmodule ${
|
||
filePath.split(".")[0]
|
||
} (\n \n);\n\nendmodule\n`;
|
||
default:
|
||
return "";
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理文件读取请求
|
||
*/
|
||
export async function handleReadFile(
|
||
panel: vscode.WebviewPanel,
|
||
filePath: string
|
||
) {
|
||
try {
|
||
const content = await readFileContent(filePath);
|
||
panel.webview.postMessage({
|
||
command: "fileContent",
|
||
content: content,
|
||
filePath: filePath,
|
||
});
|
||
} catch (error) {
|
||
panel.webview.postMessage({
|
||
command: "fileError",
|
||
error: error instanceof Error ? error.message : "读取文件失败",
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理文件创建请求
|
||
*/
|
||
export async function handleCreateFile(
|
||
panel: vscode.WebviewPanel,
|
||
filePath: string,
|
||
content: string,
|
||
overwrite: boolean = false //是否覆盖
|
||
) {
|
||
try {
|
||
if (overwrite) {
|
||
await createOrOverwriteFile(filePath, content);
|
||
} else {
|
||
await createFile(filePath, content);
|
||
}
|
||
|
||
panel.webview.postMessage({
|
||
command: "fileCreated",
|
||
filePath: filePath,
|
||
message: " 文件创建成功",
|
||
});
|
||
vscode.window.showInformationMessage(`文件创建成功: ${filePath}`);
|
||
} catch (error) {
|
||
panel.webview.postMessage({
|
||
command: "fileCreateError",
|
||
error: error instanceof Error ? error.message : "创建文件失败",
|
||
});
|
||
vscode.window.showErrorMessage(
|
||
`创建文件失败: ${error instanceof Error ? error.message : "未知错误"}`
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理文件更新请求
|
||
*/
|
||
export async function handleUpdateFile(
|
||
panel: vscode.WebviewPanel,
|
||
filePath: string,
|
||
content: string
|
||
) {
|
||
try {
|
||
await updateFile(filePath, content);
|
||
panel.webview.postMessage({
|
||
command: "fileUpdated",
|
||
filePath: filePath,
|
||
message: " 文件更新成功",
|
||
});
|
||
vscode.window.showInformationMessage(`文件更新成功: ${filePath}`);
|
||
} catch (error) {
|
||
panel.webview.postMessage({
|
||
command: "fileUpdateError",
|
||
error: error instanceof Error ? error.message : "更新文件失败",
|
||
});
|
||
vscode.window.showErrorMessage(
|
||
`更新文件失败: ${error instanceof Error ? error.message : "未知错误"}`
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理文件重命名请求
|
||
*/
|
||
export async function handleRenameFile(
|
||
panel: vscode.WebviewPanel,
|
||
oldPath: string,
|
||
newPath: string
|
||
) {
|
||
try {
|
||
await renameFile(oldPath, newPath);
|
||
panel.webview.postMessage({
|
||
command: "fileRenamed",
|
||
oldPath: oldPath,
|
||
newPath: newPath,
|
||
message: "文件重命名成功",
|
||
});
|
||
vscode.window.showInformationMessage(
|
||
`文件重命名成功: ${oldPath} → ${newPath}`
|
||
);
|
||
} catch (error) {
|
||
panel.webview.postMessage({
|
||
command: "fileRenameError",
|
||
error: error instanceof Error ? error.message : "重命名文件失败",
|
||
});
|
||
vscode.window.showErrorMessage(
|
||
`重命名文件失败: ${error instanceof Error ? error.message : "未知错误"}`
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理文件内容替换请求
|
||
*/
|
||
export async function handleReplaceInFile(
|
||
panel: vscode.WebviewPanel,
|
||
filePath: string,
|
||
searchText: string,
|
||
replaceText: string
|
||
) {
|
||
try {
|
||
await replaceFile(filePath, searchText, replaceText);
|
||
panel.webview.postMessage({
|
||
command: "fileReplaced",
|
||
filePath: filePath,
|
||
message: "文件内容替换成功",
|
||
});
|
||
vscode.window.showInformationMessage(`文件内容替换成功: ${filePath}`);
|
||
} catch (error) {
|
||
panel.webview.postMessage({
|
||
command: "fileReplaceError",
|
||
error: error instanceof Error ? error.message : "替换文件内容失败",
|
||
});
|
||
vscode.window.showErrorMessage(
|
||
`替换文件内容失败: ${error instanceof Error ? error.message : "未知错误"}`
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 将代码插入到编辑器
|
||
*/
|
||
export function insertCodeToEditor(code: string) {
|
||
const editor = vscode.window.activeTextEditor;
|
||
if (editor) {
|
||
editor.edit((editBuilder) => {
|
||
editBuilder.insert(editor.selection.active, code);
|
||
});
|
||
vscode.window.showInformationMessage("代码已插入");
|
||
} else {
|
||
vscode.window.showWarningMessage("请先打开一个编辑器");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查是否是 VCD 生成命令
|
||
*/
|
||
function isVCDGenerationCommand(text: string): boolean {
|
||
const lowerText = text.toLowerCase().trim();
|
||
|
||
// 匹配各种 VCD 生成命令
|
||
const vcdPatterns = [
|
||
/生成\s*vcd/,
|
||
/创建\s*vcd/,
|
||
/运行\s*仿真/,
|
||
/执行\s*仿真/,
|
||
/iverilog/,
|
||
/生成\s*波形/,
|
||
/仿真\s*生成/,
|
||
];
|
||
|
||
return vcdPatterns.some((pattern) => pattern.test(lowerText));
|
||
}
|
||
|
||
/**
|
||
* 处理 VCD 生成请求
|
||
*/
|
||
async function handleVCDGeneration(
|
||
panel: vscode.WebviewPanel,
|
||
extensionPath: string
|
||
) {
|
||
try {
|
||
// 获取当前工作区路径
|
||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||
panel.webview.postMessage({
|
||
command: "receiveMessage",
|
||
text: "❌ 请先打开一个工作区文件夹",
|
||
});
|
||
vscode.window.showErrorMessage("请先打开一个工作区文件夹");
|
||
return;
|
||
}
|
||
|
||
const projectPath = workspaceFolders[0].uri.fsPath;
|
||
|
||
// 发送开始消息
|
||
panel.webview.postMessage({
|
||
command: "receiveMessage",
|
||
text: "🔍 正在检查项目文件...",
|
||
});
|
||
|
||
// 1. 检查 iverilog 是否可用
|
||
const iverilogCheck = await checkIverilogAvailable(extensionPath);
|
||
if (!iverilogCheck.available) {
|
||
panel.webview.postMessage({
|
||
command: "receiveMessage",
|
||
text: `❌ ${iverilogCheck.message}\n\n请参考插件文档安装 iverilog 工具。`,
|
||
});
|
||
vscode.window.showErrorMessage(iverilogCheck.message);
|
||
return;
|
||
}
|
||
|
||
// 2. 检查项目文件完整性
|
||
const projectCheck = await checkVerilogProject(projectPath);
|
||
|
||
if (!projectCheck.isComplete) {
|
||
let errorMsg = "❌ 项目文件不完整:\n\n";
|
||
|
||
if (projectCheck.allVerilogFiles.length === 0) {
|
||
errorMsg += "• 未找到任何 Verilog 文件 (.v 或 .sv)\n";
|
||
} else {
|
||
errorMsg += `• 找到 ${projectCheck.allVerilogFiles.length} 个 Verilog 文件\n`;
|
||
|
||
if (!projectCheck.hasTopModule) {
|
||
errorMsg += "• ❌ 缺少顶层模块文件\n";
|
||
} else {
|
||
errorMsg += `• ✅ 顶层模块: ${projectCheck.topModuleFile}\n`;
|
||
}
|
||
|
||
if (!projectCheck.hasTestbench) {
|
||
errorMsg += "• ❌ 缺少 testbench 文件\n";
|
||
errorMsg +=
|
||
"\n提示: testbench 文件应包含 $dumpfile 和 $dumpvars 语句来生成 VCD 文件。\n";
|
||
} else {
|
||
errorMsg += `• ✅ Testbench: ${projectCheck.testbenchFile}\n`;
|
||
}
|
||
}
|
||
|
||
if (projectCheck.errors.length > 0) {
|
||
errorMsg += "\n错误信息:\n" + projectCheck.errors.join("\n");
|
||
}
|
||
|
||
panel.webview.postMessage({
|
||
command: "receiveMessage",
|
||
text: errorMsg,
|
||
});
|
||
vscode.window.showWarningMessage("项目文件不完整,无法生成 VCD");
|
||
return;
|
||
}
|
||
|
||
// 3. 显示项目检查结果
|
||
panel.webview.postMessage({
|
||
command: "receiveMessage",
|
||
text: `✅ 项目检查通过!\n\n找到 ${projectCheck.allVerilogFiles.length} 个 Verilog 文件\n• 顶层模块: ${projectCheck.topModuleFile}\n• Testbench: ${projectCheck.testbenchFile}\n\n🚀 开始编译和仿真...`,
|
||
});
|
||
|
||
// 4. 生成 VCD 文件
|
||
const result = await generateVCD(projectPath, extensionPath);
|
||
|
||
if (result.success) {
|
||
let successMsg = `✅ ${result.message}`;
|
||
|
||
if (result.stdout) {
|
||
successMsg += `\n\n仿真输出:\n${result.stdout}`;
|
||
}
|
||
|
||
// 发送带波形预览的消息
|
||
if (result.vcdFilePath) {
|
||
const fileName = path.basename(result.vcdFilePath);
|
||
panel.webview.postMessage({
|
||
command: "vcdGenerated",
|
||
text: successMsg,
|
||
vcdFilePath: result.vcdFilePath,
|
||
fileName: fileName,
|
||
});
|
||
|
||
vscode.window.showInformationMessage(`VCD 文件生成成功: ${fileName}`);
|
||
} else {
|
||
panel.webview.postMessage({
|
||
command: "receiveMessage",
|
||
text: successMsg,
|
||
});
|
||
}
|
||
} else {
|
||
let errorMsg = `❌ ${result.message}`;
|
||
|
||
if (result.stderr) {
|
||
errorMsg += `\n\n错误输出:\n${result.stderr}`;
|
||
}
|
||
|
||
if (result.stdout) {
|
||
errorMsg += `\n\n标准输出:\n${result.stdout}`;
|
||
}
|
||
|
||
panel.webview.postMessage({
|
||
command: "receiveMessage",
|
||
text: errorMsg,
|
||
});
|
||
|
||
vscode.window.showErrorMessage("VCD 文件生成失败");
|
||
}
|
||
} catch (error) {
|
||
const errorMsg = `❌ 生成 VCD 文件时出错: ${
|
||
error instanceof Error ? error.message : "未知错误"
|
||
}`;
|
||
|
||
panel.webview.postMessage({
|
||
command: "receiveMessage",
|
||
text: errorMsg,
|
||
});
|
||
|
||
vscode.window.showErrorMessage(errorMsg);
|
||
}
|
||
}
|