1270 lines
38 KiB
TypeScript
1270 lines
38 KiB
TypeScript
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";
|
||
|
||
/**
|
||
* 获取会员等级图标 URI
|
||
*/
|
||
function getTierIconUri(
|
||
webview: vscode.Webview,
|
||
context: vscode.ExtensionContext,
|
||
tierCode?: string,
|
||
): string | undefined {
|
||
if (!tierCode) {
|
||
return undefined;
|
||
}
|
||
|
||
const tierIconMap: Record<string, string> = {
|
||
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,
|
||
) {
|
||
// 检查 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,
|
||
});
|
||
}
|
||
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, // 默认显示在旁边,但可以指定
|
||
{
|
||
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 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,
|
||
"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",
|
||
),
|
||
);
|
||
|
||
// 获取二维码图片URI
|
||
const qrCodeUri = panel.webview.asWebviewUri(
|
||
vscode.Uri.joinPath(
|
||
context.extensionUri,
|
||
"src",
|
||
"assets",
|
||
"QRCode",
|
||
"wx.png",
|
||
),
|
||
);
|
||
|
||
// 获取Logo URI
|
||
const logoUri = panel.webview.asWebviewUri(
|
||
vscode.Uri.joinPath(context.extensionUri, "media", "homepage-logo.png"),
|
||
);
|
||
|
||
// 设置HTML内容
|
||
panel.webview.html = getWebviewContent(
|
||
iconUri.toString(),
|
||
autoIconUri.toString(),
|
||
liteIconUri.toString(),
|
||
syIconUri.toString(),
|
||
maxIconUri.toString(),
|
||
qrCodeUri.toString(),
|
||
logoUri.toString(),
|
||
);
|
||
|
||
// 获取并发送用户信息到 webview
|
||
try {
|
||
// 优先使用缓存的用户信息
|
||
let userInfo = getCachedUserInfo();
|
||
|
||
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",
|
||
text: pendingMessage.text,
|
||
mode: pendingMessage.mode,
|
||
serviceTier: pendingMessage.serviceTier,
|
||
});
|
||
}, 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,
|
||
);
|
||
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 "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();
|
||
const hasWelcomed = context.globalState.get("pluginTrialWelcomed");
|
||
|
||
console.log("[ICHelperPanel] 用户信息:", userInfo);
|
||
console.log("[ICHelperPanel] isPluginTrial:", userInfo?.isPluginTrial);
|
||
console.log("[ICHelperPanel] pluginTrialExpiresAt:", userInfo?.pluginTrialExpiresAt);
|
||
console.log("[ICHelperPanel] hasWelcomed:", hasWelcomed);
|
||
|
||
if (userInfo?.isPluginTrial === true) {
|
||
// 如果有过期时间,检查是否过期
|
||
if (userInfo.pluginTrialExpiresAt) {
|
||
const now = Date.now();
|
||
const isExpired = now >= userInfo.pluginTrialExpiresAt;
|
||
console.log("[ICHelperPanel] 是否过期:", isExpired);
|
||
|
||
if (isExpired) {
|
||
console.log("[ICHelperPanel] 试用已过期,不显示欢迎弹窗");
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 未过期或无过期时间,且未显示过欢迎弹窗
|
||
if (!hasWelcomed) {
|
||
await context.globalState.update("pluginTrialWelcomed", true);
|
||
console.log("[ICHelperPanel] ✅ 发送 showWelcomeModal 命令到前端");
|
||
panel.webview.postMessage({
|
||
command: "showWelcomeModal",
|
||
});
|
||
} else {
|
||
console.log("[ICHelperPanel] 已显示过欢迎弹窗");
|
||
}
|
||
} 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;
|
||
}
|
||
},
|
||
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}`);
|
||
}
|
||
}
|