import * as vscode from "vscode"; import { getWebviewContent } from "../views/webviewContent"; import { handleUserMessage, insertCodeToEditor, handleReadFile, handleUpdateFile, handleRenameFile, handleReplaceInFile, handleUserAnswer, abortCurrentDialog, handlePlanAction, setPendingPlanExecution, getCurrentTaskId, setLastTaskId, } from "../utils/messageHandler"; import { compactDialog } from "../services/apiClient"; import { VCDViewerPanel } from "./VCDViewerPanel"; import { ChatHistoryManager } from "../utils/chatHistoryManager"; import { MessageType } from "../types/chatHistory"; import { getCachedUserInfo } from "../services/userService"; /** * 获取会员等级图标 URI */ function getTierIconUri( webview: vscode.Webview, context: vscode.ExtensionContext, tierCode?: string ): string | undefined { if (!tierCode) { return undefined; } const tierIconMap: Record = { 'BASIC': 'free.png', 'TRIAL': 'PRO-Try.png', 'ADVANCED': 'PRO.png', 'PROFESSIONAL': 'PRO+.png' }; const iconFile = tierIconMap[tierCode]; if (!iconFile) { return undefined; } const iconUri = webview.asWebviewUri( vscode.Uri.joinPath(context.extensionUri, 'src', 'assets', 'titleIcon', iconFile) ); return iconUri.toString(); } /** * 创建并显示 IC 助手面板 */ export async function showICHelperPanel( context: vscode.ExtensionContext, viewColumn?: vscode.ViewColumn ) { // 检查用户是否已登录 try { const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false, }); if (!session) { vscode.window .showWarningMessage("请先登录后再使用 IC Coder", "立即登录") .then((selection) => { if (selection === "立即登录") { vscode.commands.executeCommand("ic-coder.login"); } }); return; } } catch (error) { vscode.window .showWarningMessage("请先登录后再使用 IC Coder", "立即登录") .then((selection) => { if (selection === "立即登录") { vscode.commands.executeCommand("ic-coder.login"); } }); return; } // 创建WebView面板 const panel = vscode.window.createWebviewPanel( "icCoder", // 面板ID "IC Coder", // 面板标题 viewColumn || vscode.ViewColumn.Beside, // 默认显示在旁边,但可以指定 { enableScripts: true, retainContextWhenHidden: true, localResourceRoots: [ vscode.Uri.joinPath(context.extensionUri, "media"), vscode.Uri.joinPath(context.extensionUri, "src", "assets") ], } ); // 为面板生成唯一ID const panelId = `panel_${Date.now()}_${Math.random() .toString(36) .substr(2, 9)}`; (panel as any).__uniqueId = panelId; // 设置标签页图标 panel.iconPath = vscode.Uri.joinPath( context.extensionUri, "media", "icon.png" ); // 获取页面内图标URI const iconUri = panel.webview.asWebviewUri( vscode.Uri.joinPath(context.extensionUri, "media", "icon.png") ); // 获取模型图标URI const autoIconUri = panel.webview.asWebviewUri( vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Auto.png") ); const liteIconUri = panel.webview.asWebviewUri( vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "lite.png") ); const syIconUri = panel.webview.asWebviewUri( vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Sy.png") ); const maxIconUri = panel.webview.asWebviewUri( vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png") ); // 设置HTML内容 panel.webview.html = getWebviewContent( iconUri.toString(), autoIconUri.toString(), liteIconUri.toString(), syIconUri.toString(), maxIconUri.toString() ); // 获取并发送用户信息到 webview try { // 优先使用缓存的用户信息 let userInfo = getCachedUserInfo(); if (userInfo) { // 使用缓存的用户信息 console.log('[ICHelperPanel] 使用缓存的用户信息:', userInfo); const tierIconUrl = getTierIconUri(panel.webview, context, userInfo.membership?.tierCode); panel.webview.postMessage({ command: 'updateUserInfo', userInfo: { userId: userInfo.userId, nickname: userInfo.nickname, username: userInfo.username }, tierIconUrl: tierIconUrl }); } else { // 如果没有缓存,从 session 中获取 const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false, }); if (session) { console.log('[ICHelperPanel] 从 session 获取用户信息, account:', session.account); panel.webview.postMessage({ command: 'updateUserInfo', userInfo: { userId: session.account.id, nickname: session.account.label, username: session.account.label } }); } } } catch (error) { console.error('[ICHelperPanel] 获取用户信息失败:', error); } // 处理消息 panel.webview.onDidReceiveMessage( async (message) => { const historyManager = ChatHistoryManager.getInstance(); const panelId = (panel as any).__uniqueId; switch (message.command) { case "sendMessage": // 仅在用户发送消息时,确保面板有任务上下文 // 如果没有,则创建新任务(仅在首次发送消息时) if (!historyManager.getPanelTask(panelId)) { const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; if (workspacePath) { try { const taskMeta = await historyManager.createTask( workspacePath, "新对话" ); historyManager.setPanelTask( panelId, taskMeta.taskId, workspacePath ); } catch (error) { console.error("创建任务失败:", error); } } } // 切换到当前面板的任务上下文 historyManager.switchToPanelTask(panelId); // 显示进度条 panel.webview.postMessage({ type: 'showProgress' }); handleUserMessage( panel, message.text, context.extensionPath, message.mode, message.model // 传递服务等级 ); break; case "readFile": handleReadFile(panel, message.filePath); break; case "updateFile": handleUpdateFile(panel, message.filePath, message.content); break; case "renameFile": handleRenameFile(panel, message.oldPath, message.newPath); break; case "replaceInFile": handleReplaceInFile( panel, message.filePath, message.searchText, message.replaceText ); break; case "insertCode": insertCodeToEditor(message.code); break; case "showInfo": vscode.window.showInformationMessage(message.text); break; case "openWaveformViewer": // 在新列中打开波形查看器 if (message.vcdFilePath) { vscode.commands.executeCommand('ic-coder.openVCDViewer', message.vcdFilePath); } break; case "getVCDInfo": // 获取 VCD 文件信息 if (message.vcdFilePath && message.containerId) { getVCDFileInfo(panel, message.vcdFilePath, message.containerId); } break; case "createNewConversation": // 创建新会话 - 在当前编辑器组中打开新标签页 showICHelperPanel(context, panel.viewColumn); break; case "loadConversationHistory": // 加载会话历史(支持分页) loadConversationHistory( panel, message.offset || 0, message.limit || 10 ); break; case "selectConversation": // 选择会话 if (message.conversationId) { selectConversation( panel, message.conversationId, context.extensionPath ); } break; // 新增:处理用户回答 case "submitAnswer": handleUserAnswer( message.askId, message.selected, message.customInput ); break; // 新增:中止对话 case "abortDialog": void abortCurrentDialog(); break; // 新增:压缩会话 case "compressConversation": { const taskId = getCurrentTaskId(); if (taskId) { compactDialog(taskId) .then((result) => { if (result.success) { panel.webview.postMessage({ command: "receiveMessage", text: "✅ 会话压缩完成", }); } else { panel.webview.postMessage({ command: "receiveMessage", text: `❌ 压缩失败: ${result.error || "未知错误"}`, }); } }) .catch((err) => { panel.webview.postMessage({ command: "receiveMessage", text: `❌ 压缩失败: ${err.message || "网络错误"}`, }); }); } else { panel.webview.postMessage({ command: "receiveMessage", text: "❌ 没有活跃的会话", }); } } break; // 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送) case "planAction": if (message.action === "confirm") { // 确认执行:切换到 Agent 模式 panel.webview.postMessage({ command: "switchMode", mode: "agent", }); // 获取当前会话的 taskId,用于复用知识图谱数据 const taskId = getCurrentTaskId(); if (taskId) { // 设置待执行的计划,对话结束后自动执行(复用 taskId) setPendingPlanExecution( panel, message.planTitle || "计划", context.extensionPath, taskId ); } else { console.warn( "[ICHelperPanel] 无法获取当前 taskId,知识图谱数据可能丢失" ); } } break; // 添加文件上下文 - 显示工作区文件列表 case "addContextFile": { const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; if (!workspaceFolder) { vscode.window.showWarningMessage("请先打开一个工作区"); break; } // 获取工作区所有文件 const files = await vscode.workspace.findFiles( "**/*", "**/node_modules/**" ); panel.webview.postMessage({ command: "showWorkspaceFileList", files: files.map((uri) => ({ path: uri.fsPath, relativePath: vscode.workspace.asRelativePath(uri), })), }); } break; // 添加文件夹上下文 - 显示工作区文件夹列表 case "addContextFolder": { const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; if (!workspaceFolder) { vscode.window.showWarningMessage("请先打开一个工作区"); break; } // 获取工作区所有文件夹 const fs = require("fs"); const path = require("path"); const folders: Array<{ path: string; relativePath: string }> = []; function scanFolders(dir: string, baseDir: string) { try { const items = fs.readdirSync(dir, { withFileTypes: true }); for (const item of items) { if (item.isDirectory() && item.name !== "node_modules" && !item.name.startsWith(".")) { const fullPath = path.join(dir, item.name); const relativePath = path.relative(baseDir, fullPath); folders.push({ path: fullPath, relativePath }); scanFolders(fullPath, baseDir); } } } catch (error) { console.error("扫描文件夹失败:", error); } } scanFolders(workspaceFolder.uri.fsPath, workspaceFolder.uri.fsPath); panel.webview.postMessage({ command: "showWorkspaceFolderList", folders: folders, }); } break; // 添加图片上下文 case "addContextImage": { const imageUris = await vscode.window.showOpenDialog({ canSelectFiles: true, canSelectFolders: false, canSelectMany: true, openLabel: "选择图片", filters: { "图片文件": ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp"], }, }); if (imageUris && imageUris.length > 0) { panel.webview.postMessage({ command: "contextImagesSelected", images: imageUris.map((uri) => uri.fsPath), }); } } break; // 添加文档库上下文 case "addContextDocument": { const docUris = await vscode.window.showOpenDialog({ canSelectFiles: true, canSelectFolders: false, canSelectMany: true, openLabel: "选择文档", filters: { "文档文件": ["pdf", "doc", "docx", "txt", "md"], "所有文件": ["*"], }, }); if (docUris && docUris.length > 0) { panel.webview.postMessage({ command: "contextDocumentsSelected", documents: docUris.map((uri) => uri.fsPath), }); } } break; // 新增:检查工作区状态 case "checkWorkspace": const hasWorkspace = !!( vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0 ); if (!hasWorkspace) { // 弹窗提示用户需要打开工作区 vscode.window .showWarningMessage( "请先打开一个文件夹作为工作区,这样我就能更好地为您服务了 😊", "打开文件夹" ) .then((selection) => { if (selection === "打开文件夹") { vscode.commands.executeCommand("vscode.openFolder"); } }); } // 返回工作区状态给前端 panel.webview.postMessage({ command: "workspaceStatus", hasWorkspace: hasWorkspace, }); break; } }, undefined, context.subscriptions ); // 面板关闭时清理任务映射 panel.onDidDispose( () => { const historyManager = ChatHistoryManager.getInstance(); const panelId = (panel as any).__uniqueId; historyManager.removePanelTask(panelId); }, undefined, context.subscriptions ); } /** * 获取 VCD 文件信息 */ async function getVCDFileInfo( panel: vscode.WebviewPanel, vcdFilePath: string, containerId: string ) { try { const fs = require("fs"); const path = require("path"); // 检查文件是否存在 if (!fs.existsSync(vcdFilePath)) { panel.webview.postMessage({ command: "vcdInfo", containerId: containerId, vcdInfo: { signalCount: "N/A", timeRange: "N/A", fileSize: "N/A", error: "文件不存在", }, }); return; } // 获取文件大小 const stats = fs.statSync(vcdFilePath); const fileSizeKB = stats.size / 1024; const fileSize = fileSizeKB < 1024 ? `${fileSizeKB.toFixed(2)} KB` : `${(fileSizeKB / 1024).toFixed(2)} MB`; // 读取 VCD 文件内容 const content = fs.readFileSync(vcdFilePath, "utf-8"); // 解析信号数量 const varMatches = content.match(/\$var/g); const signalCount = varMatches ? varMatches.length : 0; // 解析时间范围 let timeRange = "N/A"; const timeMatch = content.match(/#(\d+)/g); if (timeMatch && timeMatch.length > 0) { const times = timeMatch.map((t: string) => parseInt(t.substring(1))); const minTime = Math.min(...times); const maxTime = Math.max(...times); timeRange = `${minTime} - ${maxTime}`; } // 解析前几个信号的真实数据 const signals = parseVCDSignals(content, 3); // 只解析前3个信号 // 发送信息回前端 panel.webview.postMessage({ command: "vcdInfo", containerId: containerId, vcdInfo: { signalCount: signalCount.toString(), timeRange: timeRange, fileSize: fileSize, signals: signals, // 添加真实信号数据 }, }); } catch (error) { console.error("获取 VCD 文件信息失败:", error); panel.webview.postMessage({ command: "vcdInfo", containerId: containerId, vcdInfo: { signalCount: "N/A", timeRange: "N/A", fileSize: "N/A", error: error instanceof Error ? error.message : "未知错误", }, }); } } /** * 解析 VCD 文件中的信号数据 */ function parseVCDSignals(content: string, maxSignals: number = 3) { const signals: Array<{ name: string; identifier: string; width: number; values: Array<{ time: number; value: string }>; }> = []; try { // 1. 解析信号定义部分 const varRegex = /\$var\s+(\w+)\s+(\d+)\s+(\S+)\s+([^\$]+?)\s+\$end/g; let match; const signalDefs: Array<{ name: string; identifier: string; width: number; }> = []; while ( (match = varRegex.exec(content)) !== null && signalDefs.length < maxSignals ) { const width = parseInt(match[2]); const identifier = match[3]; const name = match[4].trim(); signalDefs.push({ name, identifier, width }); } // 2. 找到数据变化部分的起始位置 const dumpvarsIndex = content.indexOf("$dumpvars"); if (dumpvarsIndex === -1) { return signals; } const dataSection = content.substring(dumpvarsIndex); // 3. 解析每个信号的值变化 for (const signalDef of signalDefs) { const values: Array<{ time: number; value: string }> = []; let currentTime = 0; // 分行处理数据 const lines = dataSection.split("\n"); for (const line of lines) { const trimmedLine = line.trim(); // 解析时间戳 if (trimmedLine.startsWith("#")) { currentTime = parseInt(trimmedLine.substring(1)); continue; } // 解析信号值变化 // 格式1: 单比特信号 "0!" 或 "1!" // 格式2: 多比特信号 "b1010 !" if (signalDef.width === 1) { // 单比特信号 const singleBitMatch = trimmedLine.match( new RegExp(`^([01xz])${signalDef.identifier}$`) ); if (singleBitMatch) { values.push({ time: currentTime, value: singleBitMatch[1] }); } } else { // 多比特信号 const multiBitMatch = trimmedLine.match( new RegExp(`^b([01xz]+)\\s+${signalDef.identifier}$`) ); if (multiBitMatch) { values.push({ time: currentTime, value: multiBitMatch[1] }); } } // 限制采样点数量,避免数据过多 if (values.length >= 50) { break; } } signals.push({ name: signalDef.name, identifier: signalDef.identifier, width: signalDef.width, values: values, }); } } catch (error) { console.error("解析 VCD 信号数据失败:", error); } return signals; } /** * 加载会话历史(支持分页) */ async function loadConversationHistory( panel: vscode.WebviewPanel, offset: number = 0, limit: number = 10 ) { try { const historyManager = ChatHistoryManager.getInstance(); const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; if (!workspacePath) { // 没有打开的工作区,返回空历史 panel.webview.postMessage({ command: "conversationHistory", items: [], total: 0, hasMore: false, }); return; } // 获取会话历史列表(支持分页) const result = await historyManager.getConversationHistoryList( workspacePath, offset, limit ); // 发送会话历史到前端 panel.webview.postMessage({ command: "conversationHistory", items: result.items, total: result.total, hasMore: result.hasMore, }); } catch (error) { console.error("加载会话历史失败:", error); // 发生错误时返回空历史 panel.webview.postMessage({ command: "conversationHistory", items: [], total: 0, hasMore: false, }); } } /** * 选择并加载指定的会话 */ async function selectConversation( panel: vscode.WebviewPanel, taskId: string, extensionPath: string ) { try { const historyManager = ChatHistoryManager.getInstance(); const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; if (!workspacePath) { vscode.window.showErrorMessage("没有打开的工作区"); return; } // 加载任务会话 const taskSession = await historyManager.loadTaskSession( workspacePath, taskId ); if (!taskSession) { vscode.window.showErrorMessage( `加载任务 ${taskId} 失败: 任务不存在或数据损坏` ); return; } // 切换到该任务 const switched = await historyManager.switchTask(workspacePath, taskId); if (!switched) { vscode.window.showErrorMessage(`切换到任务 ${taskId} 失败`); return; } // 设置 lastTaskId,用于压缩等操作 setLastTaskId(taskId); // 更新面板的任务映射,确保后续对话保存到正确的任务中 const panelId = (panel as any).__uniqueId; historyManager.setPanelTask(panelId, taskId, workspacePath); // 清空当前聊天界面 panel.webview.postMessage({ command: "clearChat", }); // 将会话历史消息转换为 segments 格式并发送到前端显示 const segments: any[] = []; let i = 0; while (i < taskSession.messages.length) { const message = taskSession.messages[i]; if (message.type === MessageType.USER) { // 用户消息 - 如果有累积的 segments,先发送 if (segments.length > 0) { panel.webview.postMessage({ command: "receiveSegments", segments: [...segments], }); segments.length = 0; } // 发送用户消息 const textContent = message.contents?.find((c) => c.type === "TEXT"); if (textContent && "text" in textContent) { panel.webview.postMessage({ command: "addUserMessage", text: textContent.text, }); } i++; } else if (message.type === MessageType.AI) { // AI消息 - 如果有 segments,直接使用 if (message.segments && message.segments.length > 0) { panel.webview.postMessage({ command: "receiveSegments", segments: message.segments, }); i++; } else { // 旧格式:需要转换为 segments // 收集连续的 AI 消息、工具调用和工具结果 if (message.text) { segments.push({ type: "text", content: message.text, }); } // 检查是否有工具调用 if ( message.toolExecutionRequests && message.toolExecutionRequests.length > 0 ) { for (const toolReq of message.toolExecutionRequests) { // 查找对应的工具执行结果 let toolResult = ""; if (i + 1 < taskSession.messages.length) { const nextMsg = taskSession.messages[i + 1]; if ( nextMsg.type === MessageType.TOOL_EXECUTION_RESULT && nextMsg.id === toolReq.id ) { toolResult = nextMsg.text; i++; // 跳过工具结果消息 } } segments.push({ type: "tool", toolName: toolReq.name, askId: toolReq.id, toolResult: toolResult, }); } } i++; // 继续收集后续的 AI 消息,直到遇到用户消息或有 segments 的 AI 消息 while (i < taskSession.messages.length) { const nextMsg = taskSession.messages[i]; if (nextMsg.type === MessageType.USER) { break; } if (nextMsg.type === MessageType.AI) { if (nextMsg.segments && nextMsg.segments.length > 0) { break; } if (nextMsg.text) { segments.push({ type: "text", content: nextMsg.text, }); } if ( nextMsg.toolExecutionRequests && nextMsg.toolExecutionRequests.length > 0 ) { for (const toolReq of nextMsg.toolExecutionRequests) { let toolResult = ""; if (i + 1 < taskSession.messages.length) { const resultMsg = taskSession.messages[i + 1]; if ( resultMsg.type === MessageType.TOOL_EXECUTION_RESULT && resultMsg.id === toolReq.id ) { toolResult = resultMsg.text; i++; // 跳过工具结果消息 } } segments.push({ type: "tool", toolName: toolReq.name, askId: toolReq.id, toolResult: toolResult, }); } } i++; } else if (nextMsg.type === MessageType.TOOL_EXECUTION_RESULT) { // 独立的工具结果(没有被上面处理的) i++; } else { i++; } } } } else { i++; } } // 发送剩余的 segments if (segments.length > 0) { panel.webview.postMessage({ command: "receiveSegments", segments: segments, }); } vscode.window.showInformationMessage( `已加载会话: ${taskSession.meta.taskName}` ); } catch (error) { console.error("选择会话失败:", error); vscode.window.showErrorMessage(`加载会话失败: ${error}`); } }