Files
IC-Coder-Plugin/src/utils/messageHandler.ts
Roe-xin 208c24682b feat: 实现试用用户欢迎引导和过期检测功能
- 新增试用用户首次登录欢迎弹窗,展示使用教程
- 新增试用期过期检测服务和过期提醒弹窗
- 从 JWT token 中提取 ispluginTrial 标识判断用户类型
- 试用用户跳过邀请码验证流程
- 在消息发送前检查试用期是否过期
- 新增 ExpiredPanel 和 WelcomePanel 面板组件
- 新增 expiredModal 和 welcomeModal 视图组件
- 优化用户登录流程,根据用户类型显示不同引导
2026-02-26 15:42:18 +08:00

1228 lines
35 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { isTokenExpired } from "./jwtUtils";
import {
checkBalanceBeforeSend,
fetchBalance,
} from "../services/creditsService";
import { optimizePrompt } from "../services/promptOptimizeService";
import { NotificationService } from "../services/notificationService";
import { TrialExpirationService } from "../services/trialExpirationService";
import type { RunMode, ServiceTier } from "../types/api";
/** 是否使用后端服务(可通过配置控制) */
let useBackendService = true;
/** 当前对话会话 */
let currentSession: DialogSession | null = null;
/** 最后一个活跃的 taskId用于压缩等操作 */
let lastTaskId: string | null = null;
/**
* 处理用户消息
*/
export async function handleUserMessage(
panel: vscode.WebviewPanel,
text: string,
extensionPath?: string,
mode?: RunMode,
serviceTier?: ServiceTier // 新增:服务等级参数
) {
console.log("收到用户消息:", text);
// 检查 token 是否过期
const context = (panel as any).__context;
if (context) {
// 从 session 中获取 token
let token: string | undefined;
try {
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
token = session?.accessToken;
} catch (error) {
console.warn("[MessageHandler] 获取 session 失败:", error);
}
if (!token) {
console.warn("[MessageHandler] 未登录,阻止发送");
// 保存待发送的消息
await context.globalState.update('pendingMessage', {
text,
mode,
serviceTier,
timestamp: Date.now()
});
// 显示弹窗提示
const action = await vscode.window.showWarningMessage(
'请先登录后再发送消息',
'立即登录'
);
if (action === '立即登录') {
vscode.commands.executeCommand("ic-coder.login");
}
// 恢复输入状态
panel.webview.postMessage({
command: "updateSegments",
segments: [],
isComplete: true,
});
return;
}
if (isTokenExpired(token)) {
console.warn("[MessageHandler] Token 已过期,阻止发送");
// 保存待发送的消息
await context.globalState.update('pendingMessage', {
text,
mode,
serviceTier,
timestamp: Date.now()
});
// 清除过期的 session
await context.globalState.update('icCoderSessions', []);
await context.globalState.update('icCoderUserInfo', undefined);
// 显示弹窗提示
const action = await vscode.window.showWarningMessage(
'登录已过期,请重新登录',
'立即登录'
);
if (action === '立即登录') {
vscode.commands.executeCommand("ic-coder.login");
}
// 恢复输入状态
panel.webview.postMessage({
command: "updateSegments",
segments: [],
isComplete: true,
});
return;
}
// 检查试用期是否过期
const trialService = new TrialExpirationService(context, panel);
const isExpired = await trialService.checkExpiration();
if (isExpired) {
console.warn("[MessageHandler] 试用期已过期,阻止发送");
// 恢复输入状态
panel.webview.postMessage({
command: "updateSegments",
segments: [],
isComplete: true,
});
return;
}
}
// 记录用户消息到历史(允许失败,不阻塞主流程)
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/memberCenter")
);
}
// 恢复输入状态
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();
// 创建会话dialogManager 会自动处理旧会话的中止)
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) => {
console.log("[MessageHandler] 对话完成, 段落数:", segments.length);
// 先保存到历史记录(优先级最高,确保数据不丢失)
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);
console.log("[MessageHandler] AI响应已保存到历史记录");
} catch (error) {
console.error("[MessageHandler] 保存AI响应历史失败:", error);
}
// 对话完成后重新获取余额(因为已经消耗了 Credits
try {
console.log("[MessageHandler] 对话完成,重新获取余额...");
const newBalance = await fetchBalance();
if (newBalance !== null) {
console.log("[MessageHandler] 余额已更新:", newBalance);
}
} catch (error) {
console.error("[MessageHandler] 获取余额失败:", error);
}
// 尝试更新面板(如果面板已关闭,这些操作会失败,但不影响数据保存)
try {
// 隐藏状态栏
panel.webview.postMessage({
command: "hideStatus",
});
// 最后一次发送完整的段落
const result = await panel.webview.postMessage({
command: "updateSegments",
segments: segments,
isComplete: true,
});
console.log("[MessageHandler] postMessage 返回值:", result);
// 发送系统通知 - AI 响应完成
const notificationService = NotificationService.getInstance();
notificationService.success(
'IC Coder - AI 响应完成',
'您的问题已得到回复,点击查看详情',
() => {
// 点击通知时聚焦到面板
panel.reveal();
}
);
} catch (error) {
console.warn("[MessageHandler] 更新面板失败(面板可能已关闭):", error);
}
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}`);
// 发送系统通知
const notificationService = NotificationService.getInstance();
notificationService.success(
'IC Coder - 文件创建',
`文件已创建: ${path.basename(filePath)}`,
() => {
vscode.commands.executeCommand('vscode.open', vscode.Uri.file(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}`);
// 发送系统通知
const notificationService = NotificationService.getInstance();
notificationService.info(
'IC Coder - 文件更新',
`文件已更新: ${path.basename(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}`);
// 发送系统通知
const notificationService = NotificationService.getInstance();
notificationService.success(
'IC Coder - 仿真完成',
`VCD 文件已生成: ${fileName}`,
() => {
// 点击通知时打开 VCD 查看器
vscode.commands.executeCommand('ic-coder.openVCDViewer', result.vcdFilePath);
}
);
} 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 文件生成失败");
// 发送系统通知
const notificationService = NotificationService.getInstance();
notificationService.error(
'IC Coder - 仿真失败',
'VCD 文件生成失败,请查看错误信息',
() => {
// 点击通知时聚焦到面板
panel.reveal();
}
);
}
} catch (error) {
const errorMsg = `❌ 生成 VCD 文件时出错: ${
error instanceof Error ? error.message : "未知错误"
}`;
panel.webview.postMessage({
command: "receiveMessage",
text: errorMsg,
});
vscode.window.showErrorMessage(errorMsg);
// 发送系统通知
const notificationService = NotificationService.getInstance();
notificationService.error(
'IC Coder - 仿真错误',
error instanceof Error ? error.message : '生成 VCD 文件时出错',
() => {
panel.reveal();
}
);
}
}
/**
* 处理提示词优化请求
*/
export async function handleOptimizePrompt(
panel: vscode.WebviewPanel,
prompt: string
): Promise<void> {
console.log("[MessageHandler] ========== 收到提示词优化请求 ==========");
console.log("[MessageHandler] prompt:", prompt);
console.log("[MessageHandler] prompt 长度:", prompt?.length);
try {
console.log("[MessageHandler] 开始调用 optimizePrompt...");
const optimized = await optimizePrompt(prompt);
console.log("[MessageHandler] 优化成功,结果:", optimized);
panel.webview.postMessage({
command: "optimizeResult",
success: true,
optimizedPrompt: optimized,
});
} catch (error) {
const errorMsg = error instanceof Error ? error.message : "优化失败";
console.error("[MessageHandler] 提示词优化失败:", errorMsg);
panel.webview.postMessage({
command: "optimizeResult",
success: false,
error: errorMsg,
});
vscode.window.showErrorMessage(`提示词优化失败: ${errorMsg}`);
}
}