新增功能: - 文档集创建:支持添加 .md/.txt/.v/.sv/.pdf 格式文件 - 智能限制:单文件 10MB,总容量 50MB,最多 1000 个文件 - 状态管理:实时显示索引状态(待索引/索引中/已索引) - 交互优化:支持文件预览、删除操作,带 loading 动画 技术实现: - 新增 docsetDialog 组件处理文档集对话框 - 新增 contextSettingsComponent 管理上下文设置 - 扩展 contextHelper 支持文档集 CRUD 操作 - 优化 messageRouter 处理文档集相关消息
483 lines
13 KiB
TypeScript
483 lines
13 KiB
TypeScript
/**
|
||
* 消息路由处理模块
|
||
* 功能:处理 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,
|
||
handleAddContextDocumentSet,
|
||
handleGetDocumentSetList,
|
||
} 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 "addContextDocumentSet":
|
||
await handleAddContextDocumentSet(panel);
|
||
break;
|
||
|
||
case "getDocumentSetList":
|
||
await handleGetDocumentSetList(panel);
|
||
break;
|
||
|
||
case "saveDocumentSet":
|
||
const { saveDocumentSet } = await import("./contextHelper");
|
||
saveDocumentSet(message.documents || []);
|
||
break;
|
||
|
||
case "openContextSettings":
|
||
panel.webview.postMessage({
|
||
command: "openSettingsTab",
|
||
tab: "context"
|
||
});
|
||
break;
|
||
|
||
case "selectFilesForDocset":
|
||
const uris = await vscode.window.showOpenDialog({
|
||
canSelectMany: true,
|
||
canSelectFiles: true,
|
||
canSelectFolders: false,
|
||
openLabel: "选择文件",
|
||
filters: {
|
||
"支持的文件": ["md", "txt", "v", "sv", "pdf"],
|
||
},
|
||
});
|
||
|
||
if (uris && uris.length > 0) {
|
||
const fs = require("fs");
|
||
const path = require("path");
|
||
const selectedFiles: Array<{ path: string; relativePath: string; size: number }> = [];
|
||
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
|
||
const MAX_TOTAL_SIZE = 50 * 1024 * 1024; // 50 MB
|
||
const MAX_FILES = 1000;
|
||
let totalSize = 0;
|
||
const errors: string[] = [];
|
||
|
||
for (const uri of uris) {
|
||
const filePath = uri.fsPath;
|
||
const ext = path.extname(filePath).toLowerCase();
|
||
|
||
if (![".md", ".txt", ".v", ".sv", ".pdf"].includes(ext)) {
|
||
errors.push(`文件 ${path.basename(filePath)} 格式不支持`);
|
||
continue;
|
||
}
|
||
|
||
try {
|
||
const stat = fs.statSync(filePath);
|
||
if (stat.size > MAX_FILE_SIZE) {
|
||
errors.push(`文件 ${path.basename(filePath)} 超过 10 MB`);
|
||
continue;
|
||
}
|
||
if (totalSize + stat.size > MAX_TOTAL_SIZE) {
|
||
errors.push("文档集总大小超过 50 MB");
|
||
break;
|
||
}
|
||
if (selectedFiles.length >= MAX_FILES) {
|
||
errors.push("文件数量超过 1000 个");
|
||
break;
|
||
}
|
||
|
||
selectedFiles.push({
|
||
path: filePath,
|
||
relativePath: vscode.workspace.asRelativePath(filePath),
|
||
size: stat.size,
|
||
});
|
||
totalSize += stat.size;
|
||
} catch (err) {
|
||
errors.push(`无法读取文件 ${path.basename(filePath)}`);
|
||
}
|
||
}
|
||
|
||
panel.webview.postMessage({
|
||
command: "filesSelectedForDocset",
|
||
files: selectedFiles,
|
||
errors: errors.length > 0 ? errors : undefined,
|
||
});
|
||
}
|
||
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;
|
||
}
|
||
}
|