- apiClient 添加 stopDialog 接口 - dialogService 添加 getSegments/getAccumulatedText 方法 - dialogService.abort 调用后端停止接口 - messageHandler.abortCurrentDialog 保存中止前的对话内容 - userInteraction 添加 getWebviewPanel 方法 - webviewContent 添加 resetSegmentedMessage 命令处理 - 修复停止后新消息覆盖旧消息的问题
684 lines
20 KiB
TypeScript
684 lines
20 KiB
TypeScript
import * as vscode from "vscode";
|
||
import { getWebviewContent } from "../views/webviewContent";
|
||
import {
|
||
handleUserMessage,
|
||
insertCodeToEditor,
|
||
handleReadFile,
|
||
handleUpdateFile,
|
||
handleRenameFile,
|
||
handleReplaceInFile,
|
||
handleUserAnswer,
|
||
abortCurrentDialog,
|
||
handlePlanAction,
|
||
setPendingPlanExecution,
|
||
getCurrentTaskId,
|
||
} from "../utils/messageHandler";
|
||
import { VCDViewerPanel } from "./VCDViewerPanel";
|
||
import { ChatHistoryManager } from "../utils/chatHistoryManager";
|
||
import { MessageType } from "../types/chatHistory";
|
||
|
||
/**
|
||
* 创建并显示 IC 助手面板
|
||
*/
|
||
export async function showICHelperPanel(
|
||
context: vscode.ExtensionContext,
|
||
viewColumn?: vscode.ViewColumn
|
||
) {
|
||
// 检查用户是否已登录
|
||
try {
|
||
const session = await vscode.authentication.getSession("iccoder", [], {
|
||
createIfNone: false,
|
||
});
|
||
if (!session) {
|
||
vscode.window
|
||
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
||
.then((selection) => {
|
||
if (selection === "立即登录") {
|
||
vscode.commands.executeCommand("ic-coder.login");
|
||
}
|
||
});
|
||
return;
|
||
}
|
||
} catch (error) {
|
||
vscode.window
|
||
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
||
.then((selection) => {
|
||
if (selection === "立即登录") {
|
||
vscode.commands.executeCommand("ic-coder.login");
|
||
}
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 创建WebView面板
|
||
const panel = vscode.window.createWebviewPanel(
|
||
"icCoder", // 面板ID
|
||
"IC Coder", // 面板标题
|
||
viewColumn || vscode.ViewColumn.Beside, // 默认显示在旁边,但可以指定
|
||
{
|
||
enableScripts: true,
|
||
retainContextWhenHidden: true,
|
||
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, "media")],
|
||
}
|
||
);
|
||
|
||
// 为面板生成唯一ID
|
||
const panelId = `panel_${Date.now()}_${Math.random()
|
||
.toString(36)
|
||
.substr(2, 9)}`;
|
||
(panel as any).__uniqueId = panelId;
|
||
|
||
// 设置标签页图标
|
||
panel.iconPath = vscode.Uri.joinPath(
|
||
context.extensionUri,
|
||
"media",
|
||
"icon.png"
|
||
);
|
||
|
||
// 获取页面内图标URI
|
||
const iconUri = panel.webview.asWebviewUri(
|
||
vscode.Uri.joinPath(context.extensionUri, "media", "icon.png")
|
||
);
|
||
|
||
// 设置HTML内容
|
||
panel.webview.html = getWebviewContent(iconUri.toString());
|
||
|
||
// 处理消息
|
||
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);
|
||
|
||
handleUserMessage(
|
||
panel,
|
||
message.text,
|
||
context.extensionPath,
|
||
message.mode
|
||
);
|
||
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) {
|
||
VCDViewerPanel.createOrShow(
|
||
context.extensionUri,
|
||
message.vcdFilePath
|
||
);
|
||
}
|
||
break;
|
||
case "getVCDInfo":
|
||
// 获取 VCD 文件信息
|
||
if (message.vcdFilePath && message.containerId) {
|
||
getVCDFileInfo(panel, message.vcdFilePath, message.containerId);
|
||
}
|
||
break;
|
||
case "createNewConversation":
|
||
// 创建新会话 - 在当前编辑器组中打开新标签页
|
||
showICHelperPanel(context, panel.viewColumn);
|
||
break;
|
||
case "loadConversationHistory":
|
||
// 加载会话历史(支持分页)
|
||
loadConversationHistory(
|
||
panel,
|
||
message.offset || 0,
|
||
message.limit || 10
|
||
);
|
||
break;
|
||
case "selectConversation":
|
||
// 选择会话
|
||
if (message.conversationId) {
|
||
selectConversation(
|
||
panel,
|
||
message.conversationId,
|
||
context.extensionPath
|
||
);
|
||
}
|
||
break;
|
||
// 新增:处理用户回答
|
||
case "submitAnswer":
|
||
handleUserAnswer(
|
||
message.askId,
|
||
message.selected,
|
||
message.customInput
|
||
);
|
||
break;
|
||
// 新增:中止对话
|
||
case "abortDialog":
|
||
void abortCurrentDialog();
|
||
break;
|
||
// 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送)
|
||
case "planAction":
|
||
if (message.action === "confirm") {
|
||
// 确认执行:切换到 Agent 模式
|
||
panel.webview.postMessage({
|
||
command: "switchMode",
|
||
mode: "agent",
|
||
});
|
||
// 获取当前会话的 taskId,用于复用知识图谱数据
|
||
const taskId = getCurrentTaskId();
|
||
if (taskId) {
|
||
// 设置待执行的计划,对话结束后自动执行(复用 taskId)
|
||
setPendingPlanExecution(
|
||
panel,
|
||
message.planTitle || "计划",
|
||
context.extensionPath,
|
||
taskId
|
||
);
|
||
} else {
|
||
console.warn(
|
||
"[ICHelperPanel] 无法获取当前 taskId,知识图谱数据可能丢失"
|
||
);
|
||
}
|
||
}
|
||
break;
|
||
// 新增:检查工作区状态
|
||
case "checkWorkspace":
|
||
const hasWorkspace = !!(
|
||
vscode.workspace.workspaceFolders &&
|
||
vscode.workspace.workspaceFolders.length > 0
|
||
);
|
||
if (!hasWorkspace) {
|
||
// 弹窗提示用户需要打开工作区
|
||
vscode.window
|
||
.showWarningMessage(
|
||
"请先打开一个文件夹作为工作区,这样我就能更好地为您服务了 😊",
|
||
"打开文件夹"
|
||
)
|
||
.then((selection) => {
|
||
if (selection === "打开文件夹") {
|
||
vscode.commands.executeCommand("vscode.openFolder");
|
||
}
|
||
});
|
||
}
|
||
// 返回工作区状态给前端
|
||
panel.webview.postMessage({
|
||
command: "workspaceStatus",
|
||
hasWorkspace: hasWorkspace,
|
||
});
|
||
break;
|
||
}
|
||
},
|
||
undefined,
|
||
context.subscriptions
|
||
);
|
||
|
||
// 面板关闭时清理任务映射
|
||
panel.onDidDispose(
|
||
() => {
|
||
const historyManager = ChatHistoryManager.getInstance();
|
||
const panelId = (panel as any).__uniqueId;
|
||
historyManager.removePanelTask(panelId);
|
||
},
|
||
undefined,
|
||
context.subscriptions
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 获取 VCD 文件信息
|
||
*/
|
||
async function getVCDFileInfo(
|
||
panel: vscode.WebviewPanel,
|
||
vcdFilePath: string,
|
||
containerId: string
|
||
) {
|
||
try {
|
||
const fs = require("fs");
|
||
const path = require("path");
|
||
|
||
// 检查文件是否存在
|
||
if (!fs.existsSync(vcdFilePath)) {
|
||
panel.webview.postMessage({
|
||
command: "vcdInfo",
|
||
containerId: containerId,
|
||
vcdInfo: {
|
||
signalCount: "N/A",
|
||
timeRange: "N/A",
|
||
fileSize: "N/A",
|
||
error: "文件不存在",
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 获取文件大小
|
||
const stats = fs.statSync(vcdFilePath);
|
||
const fileSizeKB = stats.size / 1024;
|
||
const fileSize =
|
||
fileSizeKB < 1024
|
||
? `${fileSizeKB.toFixed(2)} KB`
|
||
: `${(fileSizeKB / 1024).toFixed(2)} MB`;
|
||
|
||
// 读取 VCD 文件内容
|
||
const content = fs.readFileSync(vcdFilePath, "utf-8");
|
||
|
||
// 解析信号数量
|
||
const varMatches = content.match(/\$var/g);
|
||
const signalCount = varMatches ? varMatches.length : 0;
|
||
|
||
// 解析时间范围
|
||
let timeRange = "N/A";
|
||
const timeMatch = content.match(/#(\d+)/g);
|
||
if (timeMatch && timeMatch.length > 0) {
|
||
const times = timeMatch.map((t: string) => parseInt(t.substring(1)));
|
||
const minTime = Math.min(...times);
|
||
const maxTime = Math.max(...times);
|
||
timeRange = `${minTime} - ${maxTime}`;
|
||
}
|
||
|
||
// 解析前几个信号的真实数据
|
||
const signals = parseVCDSignals(content, 3); // 只解析前3个信号
|
||
|
||
// 发送信息回前端
|
||
panel.webview.postMessage({
|
||
command: "vcdInfo",
|
||
containerId: containerId,
|
||
vcdInfo: {
|
||
signalCount: signalCount.toString(),
|
||
timeRange: timeRange,
|
||
fileSize: fileSize,
|
||
signals: signals, // 添加真实信号数据
|
||
},
|
||
});
|
||
} catch (error) {
|
||
console.error("获取 VCD 文件信息失败:", error);
|
||
panel.webview.postMessage({
|
||
command: "vcdInfo",
|
||
containerId: containerId,
|
||
vcdInfo: {
|
||
signalCount: "N/A",
|
||
timeRange: "N/A",
|
||
fileSize: "N/A",
|
||
error: error instanceof Error ? error.message : "未知错误",
|
||
},
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解析 VCD 文件中的信号数据
|
||
*/
|
||
function parseVCDSignals(content: string, maxSignals: number = 3) {
|
||
const signals: Array<{
|
||
name: string;
|
||
identifier: string;
|
||
width: number;
|
||
values: Array<{ time: number; value: string }>;
|
||
}> = [];
|
||
|
||
try {
|
||
// 1. 解析信号定义部分
|
||
const varRegex = /\$var\s+(\w+)\s+(\d+)\s+(\S+)\s+([^\$]+?)\s+\$end/g;
|
||
let match;
|
||
const signalDefs: Array<{
|
||
name: string;
|
||
identifier: string;
|
||
width: number;
|
||
}> = [];
|
||
|
||
while (
|
||
(match = varRegex.exec(content)) !== null &&
|
||
signalDefs.length < maxSignals
|
||
) {
|
||
const width = parseInt(match[2]);
|
||
const identifier = match[3];
|
||
const name = match[4].trim();
|
||
|
||
signalDefs.push({ name, identifier, width });
|
||
}
|
||
|
||
// 2. 找到数据变化部分的起始位置
|
||
const dumpvarsIndex = content.indexOf("$dumpvars");
|
||
if (dumpvarsIndex === -1) {
|
||
return signals;
|
||
}
|
||
|
||
const dataSection = content.substring(dumpvarsIndex);
|
||
|
||
// 3. 解析每个信号的值变化
|
||
for (const signalDef of signalDefs) {
|
||
const values: Array<{ time: number; value: string }> = [];
|
||
let currentTime = 0;
|
||
|
||
// 分行处理数据
|
||
const lines = dataSection.split("\n");
|
||
|
||
for (const line of lines) {
|
||
const trimmedLine = line.trim();
|
||
|
||
// 解析时间戳
|
||
if (trimmedLine.startsWith("#")) {
|
||
currentTime = parseInt(trimmedLine.substring(1));
|
||
continue;
|
||
}
|
||
|
||
// 解析信号值变化
|
||
// 格式1: 单比特信号 "0!" 或 "1!"
|
||
// 格式2: 多比特信号 "b1010 !"
|
||
if (signalDef.width === 1) {
|
||
// 单比特信号
|
||
const singleBitMatch = trimmedLine.match(
|
||
new RegExp(`^([01xz])${signalDef.identifier}$`)
|
||
);
|
||
if (singleBitMatch) {
|
||
values.push({ time: currentTime, value: singleBitMatch[1] });
|
||
}
|
||
} else {
|
||
// 多比特信号
|
||
const multiBitMatch = trimmedLine.match(
|
||
new RegExp(`^b([01xz]+)\\s+${signalDef.identifier}$`)
|
||
);
|
||
if (multiBitMatch) {
|
||
values.push({ time: currentTime, value: multiBitMatch[1] });
|
||
}
|
||
}
|
||
|
||
// 限制采样点数量,避免数据过多
|
||
if (values.length >= 50) {
|
||
break;
|
||
}
|
||
}
|
||
|
||
signals.push({
|
||
name: signalDef.name,
|
||
identifier: signalDef.identifier,
|
||
width: signalDef.width,
|
||
values: values,
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error("解析 VCD 信号数据失败:", error);
|
||
}
|
||
|
||
return signals;
|
||
}
|
||
|
||
/**
|
||
* 加载会话历史(支持分页)
|
||
*/
|
||
async function loadConversationHistory(
|
||
panel: vscode.WebviewPanel,
|
||
offset: number = 0,
|
||
limit: number = 10
|
||
) {
|
||
try {
|
||
const historyManager = ChatHistoryManager.getInstance();
|
||
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||
|
||
if (!workspacePath) {
|
||
// 没有打开的工作区,返回空历史
|
||
panel.webview.postMessage({
|
||
command: "conversationHistory",
|
||
items: [],
|
||
total: 0,
|
||
hasMore: false,
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 获取会话历史列表(支持分页)
|
||
const result = await historyManager.getConversationHistoryList(
|
||
workspacePath,
|
||
offset,
|
||
limit
|
||
);
|
||
|
||
// 发送会话历史到前端
|
||
panel.webview.postMessage({
|
||
command: "conversationHistory",
|
||
items: result.items,
|
||
total: result.total,
|
||
hasMore: result.hasMore,
|
||
});
|
||
} catch (error) {
|
||
console.error("加载会话历史失败:", error);
|
||
// 发生错误时返回空历史
|
||
panel.webview.postMessage({
|
||
command: "conversationHistory",
|
||
items: [],
|
||
total: 0,
|
||
hasMore: false,
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 选择并加载指定的会话
|
||
*/
|
||
async function selectConversation(
|
||
panel: vscode.WebviewPanel,
|
||
taskId: string,
|
||
extensionPath: string
|
||
) {
|
||
try {
|
||
const historyManager = ChatHistoryManager.getInstance();
|
||
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||
|
||
if (!workspacePath) {
|
||
vscode.window.showErrorMessage("没有打开的工作区");
|
||
return;
|
||
}
|
||
|
||
// 加载任务会话
|
||
const taskSession = await historyManager.loadTaskSession(
|
||
workspacePath,
|
||
taskId
|
||
);
|
||
|
||
if (!taskSession) {
|
||
vscode.window.showErrorMessage(
|
||
`加载任务 ${taskId} 失败: 任务不存在或数据损坏`
|
||
);
|
||
return;
|
||
}
|
||
|
||
// 切换到该任务
|
||
const switched = await historyManager.switchTask(workspacePath, taskId);
|
||
if (!switched) {
|
||
vscode.window.showErrorMessage(`切换到任务 ${taskId} 失败`);
|
||
return;
|
||
}
|
||
|
||
// 更新面板的任务映射,确保后续对话保存到正确的任务中
|
||
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}`);
|
||
}
|
||
}
|