diff --git a/src/panels/ICHelperPanel.ts b/src/panels/ICHelperPanel.ts index ec28085..bf31d94 100644 --- a/src/panels/ICHelperPanel.ts +++ b/src/panels/ICHelperPanel.ts @@ -1,138 +1,90 @@ +/** + * IC 助手面板主组件 + * 功能:创建和管理 IC Coder 聊天面板 + * 依赖:vscode, webviewContent, helpers + * 使用场景:用户打开 IC Coder 聊天界面 + */ import * as vscode from "vscode"; import { getWebviewContent } from "../views/webviewContent"; -import { - handleUserMessage, - insertCodeToEditor, - handleReadFile, - handleUpdateFile, - handleRenameFile, - handleReplaceInFile, - handleUserAnswer, - abortCurrentDialog, - handleOptimizePrompt, - handlePlanAction, - getCurrentTaskId, - setLastTaskId, - handleAcceptChange, - handleRejectChange, - startChangeSession, - handleOpenFileDiff, -} 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"; -import { isTokenExpired } from "../utils/jwtUtils"; -import { setBalanceUpdateCallback } from "../services/creditsService"; +import { checkAuthAndPromptLogin } from "./helpers/authHelper"; +import { + sendUserInfoToWebview, + setupBalanceUpdateCallback, +} from "./helpers/userInfoHelper"; +import { handleWebviewMessage } from "./helpers/messageRouter"; -/** - * 获取会员等级图标 URI - */ -function getTierIconUri( +function getIconUris( 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, - "dist", - "assets", - "titleIcon", - iconFile, +) { + return { + icon: webview.asWebviewUri( + vscode.Uri.joinPath(context.extensionUri, "media", "icon.png"), ), - ); - - return iconUri.toString(); + auto: webview.asWebviewUri( + vscode.Uri.joinPath( + context.extensionUri, + "dist", + "assets", + "model", + "Auto.png", + ), + ), + lite: webview.asWebviewUri( + vscode.Uri.joinPath( + context.extensionUri, + "dist", + "assets", + "model", + "lite.png", + ), + ), + sy: webview.asWebviewUri( + vscode.Uri.joinPath( + context.extensionUri, + "dist", + "assets", + "model", + "Sy.png", + ), + ), + max: webview.asWebviewUri( + vscode.Uri.joinPath( + context.extensionUri, + "dist", + "assets", + "model", + "Max.png", + ), + ), + qrCode: webview.asWebviewUri( + vscode.Uri.joinPath( + context.extensionUri, + "dist", + "assets", + "QRCode", + "wx.png", + ), + ), + logo: webview.asWebviewUri( + vscode.Uri.joinPath(context.extensionUri, "media", "homepage-logo.png"), + ), + }; } -/** - * 创建并显示 IC 助手面板 - */ export async function showICHelperPanel( context: vscode.ExtensionContext, viewColumn?: vscode.ViewColumn, ) { - // 检查 token 是否过期 - let token: string | undefined; - try { - const session = await vscode.authentication.getSession("iccoder", [], { - createIfNone: false, - }); - token = session?.accessToken; - } catch (error) { - console.warn("[ICHelperPanel] 获取 session 失败:", error); - } - - if (token && isTokenExpired(token)) { - // 清除过期的 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", { - forceReauth: true, - }); - } + if (!(await checkAuthAndPromptLogin(context))) { return; } - // 检查用户是否已登录 - 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", { - forceReauth: true, - }); - } - }); - return; - } - } catch (error) { - vscode.window - .showWarningMessage("请先登录后再使用 IC Coder", "立即登录") - .then((selection) => { - if (selection === "立即登录") { - vscode.commands.executeCommand("ic-coder.login", { - forceReauth: true, - }); - } - }); - return; - } - - // 创建WebView面板 const panel = vscode.window.createWebviewPanel( - "icCoder", // 面板ID - "IC Coder", // 面板标题 - viewColumn || vscode.ViewColumn.Beside, // 默认显示在旁边,但可以指定 + "icCoder", + "IC Coder", + viewColumn || vscode.ViewColumn.Beside, { enableScripts: true, retainContextWhenHidden: true, @@ -143,177 +95,37 @@ export async function showICHelperPanel( }, ); - // 保存 panel 引用到全局 (global as any).currentICHelperPanel = panel; - // 为面板生成唯一ID const panelId = `panel_${Date.now()}_${Math.random() .toString(36) .substr(2, 9)}`; (panel as any).__uniqueId = panelId; (panel as any).__context = context; - // 设置标签页图标 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, - "dist", - "assets", - "model", - "Auto.png", - ), - ); - const liteIconUri = panel.webview.asWebviewUri( - vscode.Uri.joinPath( - context.extensionUri, - "dist", - "assets", - "model", - "lite.png", - ), - ); - const syIconUri = panel.webview.asWebviewUri( - vscode.Uri.joinPath( - context.extensionUri, - "dist", - "assets", - "model", - "Sy.png", - ), - ); - const maxIconUri = panel.webview.asWebviewUri( - vscode.Uri.joinPath( - context.extensionUri, - "dist", - "assets", - "model", - "Max.png", - ), - ); - - // 获取二维码图片URI - const qrCodeUri = panel.webview.asWebviewUri( - vscode.Uri.joinPath( - context.extensionUri, - "dist", - "assets", - "QRCode", - "wx.png", - ), - ); - - // 获取Logo URI - const logoUri = panel.webview.asWebviewUri( - vscode.Uri.joinPath(context.extensionUri, "media", "homepage-logo.png"), - ); - - // 设置HTML内容 + const icons = getIconUris(panel.webview, context); panel.webview.html = getWebviewContent( - iconUri.toString(), - autoIconUri.toString(), - liteIconUri.toString(), - syIconUri.toString(), - maxIconUri.toString(), - qrCodeUri.toString(), - logoUri.toString(), + icons.icon.toString(), + icons.auto.toString(), + icons.lite.toString(), + icons.sy.toString(), + icons.max.toString(), + icons.qrCode.toString(), + icons.logo.toString(), ); - // 获取并发送用户信息到 webview - try { - // 优先使用缓存的用户信息 - let userInfo = getCachedUserInfo(); + await sendUserInfoToWebview(panel, context); + setupBalanceUpdateCallback(panel, context); - if (userInfo) { - // 使用缓存的用户信息 - console.log("[ICHelperPanel] 使用缓存的用户信息:", userInfo); - console.log("[ICHelperPanel] Credits 余额:", userInfo.credits); - const tierIconUrl = getTierIconUri( - panel.webview, - context, - userInfo.membership?.tierCode, - ); - const messageData = { - command: "updateUserInfo", - userInfo: { - userId: userInfo.userId, - nickname: userInfo.nickname, - username: userInfo.username, - credits: userInfo.credits, - membership: userInfo.membership, - }, - tierIconUrl: tierIconUrl, - }; - console.log("[ICHelperPanel] 发送用户信息到前端:", messageData); - panel.webview.postMessage(messageData); - } 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); - } - - // 设置余额更新回调 - setBalanceUpdateCallback((balance: number) => { - const userInfo = getCachedUserInfo(); - if (userInfo) { - userInfo.credits = balance; - const tierIconUrl = getTierIconUri( - panel.webview, - context, - userInfo.membership?.tierCode, - ); - panel.webview.postMessage({ - command: "updateUserInfo", - userInfo: { - userId: userInfo.userId, - nickname: userInfo.nickname, - username: userInfo.username, - credits: balance, - membership: userInfo.membership, - }, - tierIconUrl: tierIconUrl, - }); - } - }); - - // 检查是否有待发送的消息 const pendingMessage = context.globalState.get("pendingMessage") as any; if (pendingMessage) { - console.log("[ICHelperPanel] 检测到待发送消息,准备自动发送"); - - // 清除待发送消息 await context.globalState.update("pendingMessage", undefined); - - // 延迟发送,确保面板已完全初始化 setTimeout(() => { panel.webview.postMessage({ command: "autoSendMessage", @@ -324,593 +136,14 @@ export async function showICHelperPanel( }, 500); } - // 处理消息 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); - - // 启动变更追踪会话 - const sessionId = `session_${panelId}_${Date.now()}`; - startChangeSession(sessionId); - - // 显示进度条 - panel.webview.postMessage({ type: "showProgress" }); - - handleUserMessage( - panel, - message.text, - context.extensionPath, - message.mode, - message.model, // 传递服务等级 - message.contextItems, // 传递上下文项 - ); - 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": - void handleUserAnswer( - message.askId, - message.selected, - message.customInput, - message.answers - ); - 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; - case "optimizePrompt": - if (typeof message.prompt === "string") { - void handleOptimizePrompt(panel, message.prompt); - } else { - panel.webview.postMessage({ - command: "optimizeResult", - success: false, - error: "提示词为空或格式错误", - }); - } - break; - case "logout": - // 退出登录 - vscode.commands.executeCommand("ic-coder.logout"); - break; - case "openFile": - // 打开文件 - if (message.filePath) { - const path = require('path'); - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - const fullPath = path.isAbsolute(message.filePath) || !workspaceFolder - ? message.filePath - : vscode.Uri.joinPath(workspaceFolder.uri, message.filePath).fsPath; - vscode.workspace.openTextDocument(fullPath).then(doc => { - vscode.window.showTextDocument(doc); - }); - } - break; - case "openFileWithSelection": - // 打开文件并选中代码 - if (message.filePath) { - const path = require('path'); - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - const fullPath = path.isAbsolute(message.filePath) || !workspaceFolder - ? message.filePath - : vscode.Uri.joinPath(workspaceFolder.uri, message.filePath).fsPath; - vscode.workspace.openTextDocument(fullPath).then(doc => { - vscode.window.showTextDocument(doc).then(editor => { - const start = new vscode.Position(message.startLine - 1, 0); - const end = new vscode.Position(message.endLine - 1, doc.lineAt(message.endLine - 1).text.length); - editor.selection = new vscode.Selection(start, end); - editor.revealRange(new vscode.Range(start, end)); - }); - }); - } - break; - case "openFilePathTag": - // 打开文件路径标签(智能查找) - if (message.filePath) { - const path = require('path'); - const fs = require('fs'); - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - - let fullPath = message.filePath; - - // 如果是相对路径且工作区存在 - if (!path.isAbsolute(message.filePath) && workspaceFolder) { - const candidatePath = vscode.Uri.joinPath(workspaceFolder.uri, message.filePath).fsPath; - // 检查文件是否存在 - if (fs.existsSync(candidatePath)) { - fullPath = candidatePath; - } else { - // 尝试在工作区中搜索该文件 - const fileName = path.basename(message.filePath); - const files = await vscode.workspace.findFiles(`**/${fileName}`, '**/node_modules/**', 1); - if (files.length > 0) { - fullPath = files[0].fsPath; - } - } - } - - if (message.startLine && message.endLine) { - vscode.workspace.openTextDocument(fullPath).then(doc => { - vscode.window.showTextDocument(doc).then(editor => { - const start = new vscode.Position(message.startLine - 1, 0); - const end = new vscode.Position(message.endLine - 1, doc.lineAt(message.endLine - 1).text.length); - editor.selection = new vscode.Selection(start, end); - editor.revealRange(new vscode.Range(start, end)); - }); - }); - } else { - vscode.workspace.openTextDocument(fullPath).then(doc => { - vscode.window.showTextDocument(doc); - }); - } - } - break; - case "acceptChange": - // 采纳变更 - if (message.changeId) { - await handleAcceptChange(panel, message.changeId); - } - break; - case "rejectChange": - // 拒绝变更 - if (message.changeId) { - await handleRejectChange(panel, message.changeId); - } - break; - case "openFileDiff": - // 打开文件 diff - if (message.changeId) { - await handleOpenFileDiff(panel, message.changeId); - } - break; - case "checkInvitationCode": - // 检查邀请码验证状态 - { - // 先检查是否是试用用户 - const { getCachedUserInfo } = require("../services/userService"); - const userInfo = getCachedUserInfo(); - - if (userInfo?.isPluginTrial === true) { - // 试用用户,跳过邀请码验证,直接返回已验证 - console.log("[ICHelperPanel] 试用用户,跳过邀请码验证"); - panel.webview.postMessage({ - command: "invitationCodeStatus", - verified: true, - }); - } else { - // 正式用户,检查邀请码 - const { - InvitationService, - } = require("../services/invitationService"); - const isVerified = await InvitationService.isVerified(context); - panel.webview.postMessage({ - command: "invitationCodeStatus", - verified: isVerified, - }); - } - } - break; - case "checkWelcomeModal": - // 检查是否需要显示欢迎弹窗 - { - console.log("[ICHelperPanel] 收到 checkWelcomeModal 消息"); - const userInfo = getCachedUserInfo(); - - console.log("[ICHelperPanel] 用户信息:", userInfo); - console.log("[ICHelperPanel] isPluginTrial:", userInfo?.isPluginTrial); - console.log("[ICHelperPanel] pluginTrialExpiresAt:", userInfo?.pluginTrialExpiresAt); - - if (userInfo?.isPluginTrial === true) { - // undefined 表示无效,不显示 - if (userInfo.pluginTrialExpiresAt === undefined) { - console.log("[ICHelperPanel] pluginTrialExpiresAt 未设置,不显示欢迎弹窗"); - break; - } - - // null 表示长期有效,显示弹窗 - // 有值则检查是否过期 - if (userInfo.pluginTrialExpiresAt !== null) { - const now = Date.now(); - const isExpired = now >= userInfo.pluginTrialExpiresAt; - console.log("[ICHelperPanel] 是否过期:", isExpired); - - if (isExpired) { - console.log("[ICHelperPanel] 试用已过期,不显示欢迎弹窗"); - break; - } - } - - // 未过期或长期有效(null),显示欢迎弹窗 - console.log("[ICHelperPanel] ✅ 发送 showWelcomeModal 命令到前端"); - panel.webview.postMessage({ - command: "showWelcomeModal", - }); - } else { - console.log("[ICHelperPanel] 非试用用户"); - } - } - break; - case "checkTrialExpiration": - // 检查试用期是否过期 - { - console.log("[ICHelperPanel] 收到 checkTrialExpiration 消息"); - const { - TrialExpirationService, - } = require("../services/trialExpirationService"); - const trialService = new TrialExpirationService(context, panel); - const isExpired = await trialService.checkExpiration(); - console.log("[ICHelperPanel] 试用期过期状态:", isExpired); - } - break; - case "verifyInvitationCode": - // 验证邀请码 - { - const { - InvitationService, - } = require("../services/invitationService"); - const result = await InvitationService.verifyCode(message.code); - - if (result.success) { - // 验证成功,保存状态 - await InvitationService.saveVerificationStatus( - context, - message.code, - ); - panel.webview.postMessage({ - command: "invitationCodeVerified", - success: true, - }); - // 延迟显示欢迎弹窗,确保邀请码弹窗已关闭 - setTimeout(() => { - panel.webview.postMessage({ - command: "showNdtWelcomeModal", - }); - }, 300); - } else { - // 验证失败,返回错误信息 - panel.webview.postMessage({ - command: "invitationCodeVerified", - success: false, - message: result.message, - }); - } - } - break; - case "openICCoder": - // 跳转到 IC Coder 官网 - vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com")); - break; - case "openTutorial": - // 打开使用教程 - vscode.env.openExternal( - vscode.Uri.parse( - "https://www.iccoder.com/guides/quick-start/first-task-plugin", - ), - ); - break; - case "openUserManual": - // 打开用户手册 - vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com")); - break; - case "openUserFeedback": - // 打开用户反馈二维码弹窗 - panel.webview.postMessage({ - command: "showFeedbackQRCode", - }); - break; - // 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送) - case "planAction": - if (message.action === "confirm") { - // 确认执行:切换到 Agent 模式(UI 切换) - panel.webview.postMessage({ - command: "switchMode", - mode: "agent", - }); - // 注意:不再设置待执行计划;后端 LLM 会在同一对话中自动执行计划 - } else if ( - message.action === "modify" || - message.action === "cancel" - ) { - void handlePlanAction( - panel, - message.action, - message.planTitle || "", - context.extensionPath, - message.model, - ); - } - 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 "openFile": - { - let filePath = message.filePath; - if (filePath) { - // 如果是相对路径,转换为绝对路径 - if (!require("path").isAbsolute(filePath)) { - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - if (workspaceFolder) { - filePath = require("path").join(workspaceFolder.uri.fsPath, filePath); - } - } - const uri = vscode.Uri.file(filePath); - vscode.window.showTextDocument(uri); - } - } - 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; - case "openExternalUrl": - // 打开外部链接 - if (message.url) { - vscode.env.openExternal(vscode.Uri.parse(message.url)); - } - break; - case "openICCoder": - // 打开 IC Coder 官网 - vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com")); - break; - case "logout": - // 退出登录(前端已有确认对话框) - vscode.commands.executeCommand("ic-coder.logout"); - break; - } + await handleWebviewMessage(message, panel, context); }, undefined, context.subscriptions, ); - // 面板关闭时清理任务映射 panel.onDidDispose( () => { const historyManager = ChatHistoryManager.getInstance(); @@ -921,425 +154,3 @@ export async function showICHelperPanel( 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}`); - } -} diff --git a/src/panels/helpers/authHelper.ts b/src/panels/helpers/authHelper.ts new file mode 100644 index 0000000..c5ba440 --- /dev/null +++ b/src/panels/helpers/authHelper.ts @@ -0,0 +1,66 @@ +/** + * 认证辅助模块 + * 功能:处理用户登录状态检查和 token 验证 + * 依赖:vscode, jwtUtils + * 使用场景:面板初始化时验证用户登录状态 + */ +import * as vscode from "vscode"; +import { isTokenExpired } from "../../utils/jwtUtils"; + +export async function checkAuthAndPromptLogin( + context: vscode.ExtensionContext, +): Promise { + let token: string | undefined; + try { + const session = await vscode.authentication.getSession("iccoder", [], { + createIfNone: false, + }); + token = session?.accessToken; + } catch (error) { + console.warn("[AuthHelper] 获取 session 失败:", error); + } + + if (token && isTokenExpired(token)) { + 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", { forceReauth: true }); + } + return false; + } + + 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", { + forceReauth: true, + }); + } + }); + return false; + } + } catch (error) { + vscode.window + .showWarningMessage("请先登录后再使用 IC Coder", "立即登录") + .then((selection) => { + if (selection === "立即登录") { + vscode.commands.executeCommand("ic-coder.login", { + forceReauth: true, + }); + } + }); + return false; + } + + return true; +} diff --git a/src/panels/helpers/contextHelper.ts b/src/panels/helpers/contextHelper.ts new file mode 100644 index 0000000..91e6ca2 --- /dev/null +++ b/src/panels/helpers/contextHelper.ts @@ -0,0 +1,104 @@ +/** + * 上下文管理模块 + * 功能:处理文件、文件夹、图片、文档上下文添加 + * 依赖:vscode, fs, path + * 使用场景:用户添加上下文项时 + */ +import * as vscode from "vscode"; + +export async function handleAddContextFile(panel: vscode.WebviewPanel) { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder) { + vscode.window.showWarningMessage("请先打开一个工作区"); + return; + } + + 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), + })), + }); +} + +export async function handleAddContextFolder(panel: vscode.WebviewPanel) { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder) { + vscode.window.showWarningMessage("请先打开一个工作区"); + return; + } + + 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, + }); +} + +export async function handleAddContextImage(panel: vscode.WebviewPanel) { + 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), + }); + } +} + +export async function handleAddContextDocument(panel: vscode.WebviewPanel) { + 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), + }); + } +} diff --git a/src/panels/helpers/conversationHelper.ts b/src/panels/helpers/conversationHelper.ts new file mode 100644 index 0000000..6280f8a --- /dev/null +++ b/src/panels/helpers/conversationHelper.ts @@ -0,0 +1,219 @@ +/** + * 会话历史管理模块 + * 功能:加载和选择会话历史 + * 依赖:vscode, chatHistoryManager, messageHandler + * 使用场景:会话历史列表和切换 + */ +import * as vscode from "vscode"; +import { ChatHistoryManager } from "../../utils/chatHistoryManager"; +import { MessageType } from "../../types/chatHistory"; +import { setLastTaskId } from "../../utils/messageHandler"; + +export 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, + }); + } +} + +export 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; + } + + setLastTaskId(taskId); + + const panelId = (panel as any).__uniqueId; + historyManager.setPanelTask(panelId, taskId, workspacePath); + + panel.webview.postMessage({ command: "clearChat" }); + + const segments: any[] = []; + let i = 0; + + while (i < taskSession.messages.length) { + const message = taskSession.messages[i]; + + if (message.type === MessageType.USER) { + 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) { + if (message.segments && message.segments.length > 0) { + panel.webview.postMessage({ + command: "receiveSegments", + segments: message.segments, + }); + i++; + } else { + 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++; + + 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++; + } + } + + 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}`); + } +} diff --git a/src/panels/helpers/fileHelper.ts b/src/panels/helpers/fileHelper.ts new file mode 100644 index 0000000..4c046ae --- /dev/null +++ b/src/panels/helpers/fileHelper.ts @@ -0,0 +1,78 @@ +/** + * 文件操作辅助模块 + * 功能:处理文件打开、选择等操作 + * 依赖:vscode, fs, path + * 使用场景:打开文件、跳转到代码位置 + */ +import * as vscode from "vscode"; + +export async function openFile(filePath: string) { + const path = require("path"); + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + const fullPath = + path.isAbsolute(filePath) || !workspaceFolder + ? filePath + : vscode.Uri.joinPath(workspaceFolder.uri, filePath).fsPath; + const doc = await vscode.workspace.openTextDocument(fullPath); + await vscode.window.showTextDocument(doc); +} + +export async function openFileWithSelection( + filePath: string, + startLine: number, + endLine: number, +) { + const path = require("path"); + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + const fullPath = + path.isAbsolute(filePath) || !workspaceFolder + ? filePath + : vscode.Uri.joinPath(workspaceFolder.uri, filePath).fsPath; + const doc = await vscode.workspace.openTextDocument(fullPath); + const editor = await vscode.window.showTextDocument(doc); + const start = new vscode.Position(startLine - 1, 0); + const end = new vscode.Position( + endLine - 1, + doc.lineAt(endLine - 1).text.length, + ); + editor.selection = new vscode.Selection(start, end); + editor.revealRange(new vscode.Range(start, end)); +} + +export async function openFilePathTag( + filePath: string, + startLine?: number, + endLine?: number, +) { + const path = require("path"); + const fs = require("fs"); + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + + let fullPath = filePath; + + if (!path.isAbsolute(filePath) && workspaceFolder) { + const candidatePath = vscode.Uri.joinPath( + workspaceFolder.uri, + filePath, + ).fsPath; + if (fs.existsSync(candidatePath)) { + fullPath = candidatePath; + } else { + const fileName = path.basename(filePath); + const files = await vscode.workspace.findFiles( + `**/${fileName}`, + "**/node_modules/**", + 1, + ); + if (files.length > 0) { + fullPath = files[0].fsPath; + } + } + } + + if (startLine && endLine) { + await openFileWithSelection(fullPath, startLine, endLine); + } else { + await openFile(fullPath); + } +} diff --git a/src/panels/helpers/messageRouter.ts b/src/panels/helpers/messageRouter.ts new file mode 100644 index 0000000..2f78904 --- /dev/null +++ b/src/panels/helpers/messageRouter.ts @@ -0,0 +1,396 @@ +/** + * 消息路由处理模块 + * 功能:处理 webview 消息的路由分发 + * 依赖:各个 helper 模块和 messageHandler + * 使用场景:webview 消息接收时 + */ +import * as vscode from "vscode"; +import { + handleUserMessage, + insertCodeToEditor, + handleReadFile, + handleUpdateFile, + handleRenameFile, + handleReplaceInFile, + handleUserAnswer, + abortCurrentDialog, + handleOptimizePrompt, + handlePlanAction, + getCurrentTaskId, + handleAcceptChange, + handleRejectChange, + handleOpenFileDiff, + startChangeSession, +} from "../../utils/messageHandler"; +import { compactDialog } from "../../services/apiClient"; +import { ChatHistoryManager } from "../../utils/chatHistoryManager"; +import { getCachedUserInfo } from "../../services/userService"; +import { loadConversationHistory, selectConversation } from "./conversationHelper"; +import { getVCDFileInfo } from "./vcdHelper"; +import { + handleAddContextFile, + handleAddContextFolder, + handleAddContextImage, + handleAddContextDocument, +} from "./contextHelper"; +import { openFile, openFileWithSelection, openFilePathTag } from "./fileHelper"; + +export async function handleWebviewMessage( + message: any, + panel: vscode.WebviewPanel, + context: vscode.ExtensionContext, +) { + 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); + const sessionId = `session_${panelId}_${Date.now()}`; + startChangeSession(sessionId); + panel.webview.postMessage({ type: "showProgress" }); + + handleUserMessage( + panel, + message.text, + context.extensionPath, + message.mode, + message.model, + message.contextItems, + ); + 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": + if (message.vcdFilePath && message.containerId) { + getVCDFileInfo(panel, message.vcdFilePath, message.containerId); + } + break; + + case "createNewConversation": + const { showICHelperPanel } = require("../ICHelperPanel"); + 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": + void handleUserAnswer( + message.askId, + message.selected, + message.customInput, + message.answers, + ); + break; + + case "abortDialog": + void abortCurrentDialog(); + break; + + case "compressConversation": + { + const taskId = getCurrentTaskId(); + if (taskId) { + compactDialog(taskId) + .then((result) => { + panel.webview.postMessage({ + command: "receiveMessage", + text: result.success + ? "✅ 会话压缩完成" + : `❌ 压缩失败: ${result.error || "未知错误"}`, + }); + }) + .catch((err) => { + panel.webview.postMessage({ + command: "receiveMessage", + text: `❌ 压缩失败: ${err.message || "网络错误"}`, + }); + }); + } else { + panel.webview.postMessage({ + command: "receiveMessage", + text: "❌ 没有活跃的会话", + }); + } + } + break; + + case "optimizePrompt": + if (typeof message.prompt === "string") { + void handleOptimizePrompt(panel, message.prompt); + } else { + panel.webview.postMessage({ + command: "optimizeResult", + success: false, + error: "提示词为空或格式错误", + }); + } + break; + + case "logout": + vscode.commands.executeCommand("ic-coder.logout"); + break; + + case "openFile": + if (message.filePath) { + await openFile(message.filePath); + } + break; + + case "openFileWithSelection": + if (message.filePath) { + await openFileWithSelection( + message.filePath, + message.startLine, + message.endLine, + ); + } + break; + + case "openFilePathTag": + if (message.filePath) { + await openFilePathTag( + message.filePath, + message.startLine, + message.endLine, + ); + } + break; + + case "acceptChange": + if (message.changeId) { + await handleAcceptChange(panel, message.changeId); + } + break; + + case "rejectChange": + if (message.changeId) { + await handleRejectChange(panel, message.changeId); + } + break; + + case "openFileDiff": + if (message.changeId) { + await handleOpenFileDiff(panel, message.changeId); + } + break; + + case "checkInvitationCode": + { + const userInfo = getCachedUserInfo(); + if (userInfo?.isPluginTrial === true) { + panel.webview.postMessage({ + command: "invitationCodeStatus", + verified: true, + }); + } else { + const { InvitationService } = require("../../services/invitationService"); + const isVerified = await InvitationService.isVerified(context); + panel.webview.postMessage({ + command: "invitationCodeStatus", + verified: isVerified, + }); + } + } + break; + + case "checkWelcomeModal": + { + const userInfo = getCachedUserInfo(); + if (userInfo?.isPluginTrial === true) { + if (userInfo.pluginTrialExpiresAt === undefined) { + break; + } + if (userInfo.pluginTrialExpiresAt !== null) { + const now = Date.now(); + const isExpired = now >= userInfo.pluginTrialExpiresAt; + if (isExpired) { + break; + } + } + panel.webview.postMessage({ command: "showWelcomeModal" }); + } + } + break; + + case "checkTrialExpiration": + { + const { TrialExpirationService } = require("../../services/trialExpirationService"); + const trialService = new TrialExpirationService(context, panel); + await trialService.checkExpiration(); + } + break; + + case "verifyInvitationCode": + { + const { InvitationService } = require("../../services/invitationService"); + const result = await InvitationService.verifyCode(message.code); + + if (result.success) { + await InvitationService.saveVerificationStatus(context, message.code); + panel.webview.postMessage({ + command: "invitationCodeVerified", + success: true, + }); + setTimeout(() => { + panel.webview.postMessage({ command: "showNdtWelcomeModal" }); + }, 300); + } else { + panel.webview.postMessage({ + command: "invitationCodeVerified", + success: false, + message: result.message, + }); + } + } + break; + + case "openICCoder": + vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com")); + break; + + case "openTutorial": + vscode.env.openExternal( + vscode.Uri.parse( + "https://www.iccoder.com/guides/quick-start/first-task-plugin", + ), + ); + break; + + case "openUserManual": + vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com")); + break; + + case "openUserFeedback": + panel.webview.postMessage({ command: "showFeedbackQRCode" }); + break; + + case "planAction": + if (message.action === "confirm") { + panel.webview.postMessage({ command: "switchMode", mode: "agent" }); + } else if (message.action === "modify" || message.action === "cancel") { + void handlePlanAction( + panel, + message.action, + message.planTitle || "", + context.extensionPath, + message.model, + ); + } + break; + + case "addContextFile": + await handleAddContextFile(panel); + break; + + case "addContextFolder": + await handleAddContextFolder(panel); + break; + + case "addContextImage": + await handleAddContextImage(panel); + break; + + case "addContextDocument": + await handleAddContextDocument(panel); + 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; + + case "openExternalUrl": + if (message.url) { + vscode.env.openExternal(vscode.Uri.parse(message.url)); + } + break; + } +} diff --git a/src/panels/helpers/userInfoHelper.ts b/src/panels/helpers/userInfoHelper.ts new file mode 100644 index 0000000..becf1d8 --- /dev/null +++ b/src/panels/helpers/userInfoHelper.ts @@ -0,0 +1,116 @@ +/** + * 用户信息辅助模块 + * 功能:管理用户信息的获取、更新和发送 + * 依赖:vscode, userService, creditsService + * 使用场景:面板初始化和余额更新时 + */ +import * as vscode from "vscode"; +import { getCachedUserInfo } from "../../services/userService"; +import { setBalanceUpdateCallback } from "../../services/creditsService"; + +export 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, + "dist", + "assets", + "titleIcon", + iconFile, + ), + ); + + return iconUri.toString(); +} + +export async function sendUserInfoToWebview( + panel: vscode.WebviewPanel, + context: vscode.ExtensionContext, +) { + try { + let userInfo = getCachedUserInfo(); + + if (userInfo) { + console.log("[UserInfoHelper] 使用缓存的用户信息:", 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, + credits: userInfo.credits, + membership: userInfo.membership, + }, + tierIconUrl: tierIconUrl, + }); + } else { + const session = await vscode.authentication.getSession("iccoder", [], { + createIfNone: false, + }); + if (session) { + panel.webview.postMessage({ + command: "updateUserInfo", + userInfo: { + userId: session.account.id, + nickname: session.account.label, + username: session.account.label, + }, + }); + } + } + } catch (error) { + console.error("[UserInfoHelper] 获取用户信息失败:", error); + } +} + +export function setupBalanceUpdateCallback( + panel: vscode.WebviewPanel, + context: vscode.ExtensionContext, +) { + setBalanceUpdateCallback((balance: number) => { + const userInfo = getCachedUserInfo(); + if (userInfo) { + userInfo.credits = balance; + const tierIconUrl = getTierIconUri( + panel.webview, + context, + userInfo.membership?.tierCode, + ); + panel.webview.postMessage({ + command: "updateUserInfo", + userInfo: { + userId: userInfo.userId, + nickname: userInfo.nickname, + username: userInfo.username, + credits: balance, + membership: userInfo.membership, + }, + tierIconUrl: tierIconUrl, + }); + } + }); +} diff --git a/src/panels/helpers/vcdHelper.ts b/src/panels/helpers/vcdHelper.ts new file mode 100644 index 0000000..6480bd4 --- /dev/null +++ b/src/panels/helpers/vcdHelper.ts @@ -0,0 +1,158 @@ +/** + * VCD 文件处理模块 + * 功能:VCD 文件信息获取和信号解析 + * 依赖:vscode, fs + * 使用场景:波形查看器相关功能 + */ +import * as vscode from "vscode"; + +export async function getVCDFileInfo( + panel: vscode.WebviewPanel, + vcdFilePath: string, + containerId: string, +) { + try { + const fs = require("fs"); + + 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`; + + 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); + + 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 : "未知错误", + }, + }); + } +} + +function parseVCDSignals(content: string, maxSignals: number = 3) { + const signals: Array<{ + name: string; + identifier: string; + width: number; + values: Array<{ time: number; value: string }>; + }> = []; + + try { + 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 }); + } + + const dumpvarsIndex = content.indexOf("$dumpvars"); + if (dumpvarsIndex === -1) { + return signals; + } + + const dataSection = content.substring(dumpvarsIndex); + + 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; + } + + 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; +}