1 Commits

Author SHA1 Message Date
e61122449d feat:暂存修改+还未测试 2026-01-10 10:19:49 +08:00
19 changed files with 361 additions and 1599 deletions

29
.vscodeignore Normal file
View File

@ -0,0 +1,29 @@
# 排除开发文件
.vscode/**
.git/**
.gitignore
node_modules/**
src/**
**/*.ts
**/*.map
# 排除测试文件
test/**
**/*.test.js
# 排除文档
*.md
!README.md
# 排除 waveform_trace Python 源码(只保留 exe
tools/waveform_trace/src/**
tools/waveform_trace/build/**
tools/waveform_trace/dist/**
tools/waveform_trace/build.bat
tools/waveform_trace/build.sh
# 排除打包临时文件
**/__pycache__/**
**/*.pyc
**/*.pyo
**/*.spec

View File

@ -17,8 +17,6 @@ export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
export interface IccoderConfig { export interface IccoderConfig {
/** 后端服务地址 */ /** 后端服务地址 */
backendUrl: string; backendUrl: string;
/** 登录页面地址 */
loginUrl: string;
/** 后端服务地址strangeLoop */ /** 后端服务地址strangeLoop */
backendUrlStrongeLoop: string; backendUrlStrongeLoop: string;
/** 请求超时时间(毫秒) */ /** 请求超时时间(毫秒) */
@ -35,7 +33,6 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
dev: { dev: {
backendUrl: "http://localhost:2233", backendUrl: "http://localhost:2233",
backendUrlStrongeLoop: "http://192.168.1.108:2029", backendUrlStrongeLoop: "http://192.168.1.108:2029",
loginUrl: "http://localhost/login",
timeout: 300000, timeout: 300000,
userId: "default-user", userId: "default-user",
serviceTier: "max", // 默认使用 max serviceTier: "max", // 默认使用 max
@ -44,7 +41,6 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
test: { test: {
backendUrl: "http://192.168.1.108:2233", backendUrl: "http://192.168.1.108:2233",
backendUrlStrongeLoop: "http://192.168.1.108:2029", backendUrlStrongeLoop: "http://192.168.1.108:2029",
loginUrl: "http://192.168.1.108:2005/login",
timeout: 60000, timeout: 60000,
userId: "default-user", userId: "default-user",
serviceTier: "max", serviceTier: "max",
@ -53,7 +49,6 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
prod: { prod: {
backendUrl: "https://api.iccoder.com", backendUrl: "https://api.iccoder.com",
backendUrlStrongeLoop: "http://api.iccoder.com:2029", backendUrlStrongeLoop: "http://api.iccoder.com:2029",
loginUrl: "https://iccoder.com/login",
timeout: 60000, timeout: 60000,
userId: "default-user", userId: "default-user",
serviceTier: "auto", serviceTier: "auto",

View File

@ -10,6 +10,7 @@ import {
handleUserAnswer, handleUserAnswer,
abortCurrentDialog, abortCurrentDialog,
handlePlanAction, handlePlanAction,
setPendingPlanExecution,
getCurrentTaskId, getCurrentTaskId,
setLastTaskId, setLastTaskId,
} from "../utils/messageHandler"; } from "../utils/messageHandler";
@ -32,10 +33,10 @@ function getTierIconUri(
} }
const tierIconMap: Record<string, string> = { const tierIconMap: Record<string, string> = {
'BASIC': 'free.png', BASIC: "free.png",
'TRIAL': 'PRO-Try.png', TRIAL: "PRO-Try.png",
'ADVANCED': 'PRO.png', ADVANCED: "PRO.png",
'PROFESSIONAL': 'PRO+.png' PROFESSIONAL: "PRO+.png",
}; };
const iconFile = tierIconMap[tierCode]; const iconFile = tierIconMap[tierCode];
@ -44,7 +45,13 @@ function getTierIconUri(
} }
const iconUri = webview.asWebviewUri( const iconUri = webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, 'src', 'assets', 'titleIcon', iconFile) vscode.Uri.joinPath(
context.extensionUri,
"src",
"assets",
"titleIcon",
iconFile
)
); );
return iconUri.toString(); return iconUri.toString();
@ -93,7 +100,7 @@ export async function showICHelperPanel(
retainContextWhenHidden: true, retainContextWhenHidden: true,
localResourceRoots: [ localResourceRoots: [
vscode.Uri.joinPath(context.extensionUri, "media"), vscode.Uri.joinPath(context.extensionUri, "media"),
vscode.Uri.joinPath(context.extensionUri, "src", "assets") vscode.Uri.joinPath(context.extensionUri, "src", "assets"),
], ],
} }
); );
@ -118,16 +125,40 @@ export async function showICHelperPanel(
// 获取模型图标URI // 获取模型图标URI
const autoIconUri = panel.webview.asWebviewUri( const autoIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Auto.png") vscode.Uri.joinPath(
context.extensionUri,
"src",
"assets",
"model",
"Auto.png"
)
); );
const liteIconUri = panel.webview.asWebviewUri( const liteIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "lite.png") vscode.Uri.joinPath(
context.extensionUri,
"src",
"assets",
"model",
"lite.png"
)
); );
const syIconUri = panel.webview.asWebviewUri( const syIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Sy.png") vscode.Uri.joinPath(
context.extensionUri,
"src",
"assets",
"model",
"Sy.png"
)
); );
const maxIconUri = panel.webview.asWebviewUri( const maxIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png") vscode.Uri.joinPath(
context.extensionUri,
"src",
"assets",
"model",
"Max.png"
)
); );
// 设置HTML内容 // 设置HTML内容
@ -146,16 +177,20 @@ export async function showICHelperPanel(
if (userInfo) { if (userInfo) {
// 使用缓存的用户信息 // 使用缓存的用户信息
console.log('[ICHelperPanel] 使用缓存的用户信息:', userInfo); console.log("[ICHelperPanel] 使用缓存的用户信息:", userInfo);
const tierIconUrl = getTierIconUri(panel.webview, context, userInfo.membership?.tierCode); const tierIconUrl = getTierIconUri(
panel.webview,
context,
userInfo.membership?.tierCode
);
panel.webview.postMessage({ panel.webview.postMessage({
command: 'updateUserInfo', command: "updateUserInfo",
userInfo: { userInfo: {
userId: userInfo.userId, userId: userInfo.userId,
nickname: userInfo.nickname, nickname: userInfo.nickname,
username: userInfo.username username: userInfo.username,
}, },
tierIconUrl: tierIconUrl tierIconUrl: tierIconUrl,
}); });
} else { } else {
// 如果没有缓存,从 session 中获取 // 如果没有缓存,从 session 中获取
@ -163,19 +198,22 @@ export async function showICHelperPanel(
createIfNone: false, createIfNone: false,
}); });
if (session) { if (session) {
console.log('[ICHelperPanel] 从 session 获取用户信息, account:', session.account); console.log(
"[ICHelperPanel] 从 session 获取用户信息, account:",
session.account
);
panel.webview.postMessage({ panel.webview.postMessage({
command: 'updateUserInfo', command: "updateUserInfo",
userInfo: { userInfo: {
userId: session.account.id, userId: session.account.id,
nickname: session.account.label, nickname: session.account.label,
username: session.account.label username: session.account.label,
} },
}); });
} }
} }
} catch (error) { } catch (error) {
console.error('[ICHelperPanel] 获取用户信息失败:', error); console.error("[ICHelperPanel] 获取用户信息失败:", error);
} }
// 处理消息 // 处理消息
@ -212,7 +250,7 @@ export async function showICHelperPanel(
historyManager.switchToPanelTask(panelId); historyManager.switchToPanelTask(panelId);
// 显示进度条 // 显示进度条
panel.webview.postMessage({ type: 'showProgress' }); panel.webview.postMessage({ type: "showProgress" });
handleUserMessage( handleUserMessage(
panel, panel,
@ -248,7 +286,10 @@ export async function showICHelperPanel(
case "openWaveformViewer": case "openWaveformViewer":
// 在新列中打开波形查看器 // 在新列中打开波形查看器
if (message.vcdFilePath) { if (message.vcdFilePath) {
vscode.commands.executeCommand('ic-coder.openVCDViewer', message.vcdFilePath); vscode.commands.executeCommand(
"ic-coder.openVCDViewer",
message.vcdFilePath
);
} }
break; break;
case "getVCDInfo": case "getVCDInfo":
@ -281,7 +322,7 @@ export async function showICHelperPanel(
break; break;
// 新增:处理用户回答 // 新增:处理用户回答
case "submitAnswer": case "submitAnswer":
void handleUserAnswer( handleUserAnswer(
message.askId, message.askId,
message.selected, message.selected,
message.customInput message.customInput
@ -327,20 +368,26 @@ export async function showICHelperPanel(
// 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送) // 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送)
case "planAction": case "planAction":
if (message.action === "confirm") { if (message.action === "confirm") {
// 确认执行:切换到 Agent 模式UI 切换) // 确认执行:切换到 Agent 模式
panel.webview.postMessage({ panel.webview.postMessage({
command: "switchMode", command: "switchMode",
mode: "agent", mode: "agent",
}); });
// 注意:不再设置待执行计划;后端 LLM 会在同一对话中自动执行计划 // 获取当前会话的 taskId用于复用知识图谱数据
} else if (message.action === "modify" || message.action === "cancel") { const taskId = getCurrentTaskId();
void handlePlanAction( if (taskId) {
// 设置待执行的计划,对话结束后自动执行(复用 taskId
setPendingPlanExecution(
panel, panel,
message.action, message.planTitle || "计划",
message.planTitle || "",
context.extensionPath, context.extensionPath,
message.model taskId
); );
} else {
console.warn(
"[ICHelperPanel] 无法获取当前 taskId知识图谱数据可能丢失"
);
}
} }
break; break;
// 添加文件上下文 - 显示工作区文件列表 // 添加文件上下文 - 显示工作区文件列表
@ -385,7 +432,11 @@ export async function showICHelperPanel(
try { try {
const items = fs.readdirSync(dir, { withFileTypes: true }); const items = fs.readdirSync(dir, { withFileTypes: true });
for (const item of items) { for (const item of items) {
if (item.isDirectory() && item.name !== "node_modules" && !item.name.startsWith(".")) { if (
item.isDirectory() &&
item.name !== "node_modules" &&
!item.name.startsWith(".")
) {
const fullPath = path.join(dir, item.name); const fullPath = path.join(dir, item.name);
const relativePath = path.relative(baseDir, fullPath); const relativePath = path.relative(baseDir, fullPath);
folders.push({ path: fullPath, relativePath }); folders.push({ path: fullPath, relativePath });
@ -414,7 +465,7 @@ export async function showICHelperPanel(
canSelectMany: true, canSelectMany: true,
openLabel: "选择图片", openLabel: "选择图片",
filters: { filters: {
"图片文件": ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp"], : ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp"],
}, },
}); });
if (imageUris && imageUris.length > 0) { if (imageUris && imageUris.length > 0) {
@ -434,8 +485,8 @@ export async function showICHelperPanel(
canSelectMany: true, canSelectMany: true,
openLabel: "选择文档", openLabel: "选择文档",
filters: { filters: {
"文档文件": ["pdf", "doc", "docx", "txt", "md"], : ["pdf", "doc", "docx", "txt", "md"],
"所有文件": ["*"], : ["*"],
}, },
}); });
if (docUris && docUris.length > 0) { if (docUris && docUris.length > 0) {
@ -471,6 +522,29 @@ export async function showICHelperPanel(
hasWorkspace: hasWorkspace, hasWorkspace: hasWorkspace,
}); });
break; break;
// 新增:处理面板宽度不足
case "panelWidthInsufficient":
// 关闭面板
panel.dispose();
vscode.window.showWarningMessage(
"聊天面板宽度不足(最小 200px已自动关闭"
);
break;
}
},
undefined,
context.subscriptions
);
// 监听面板状态变化,检查宽度
panel.onDidChangeViewState(
(e) => {
if (e.webviewPanel.visible) {
// 请求前端检查宽度
panel.webview.postMessage({
command: "checkPanelWidth",
minWidth: 200,
});
} }
}, },
undefined, undefined,
@ -501,8 +575,12 @@ async function getVCDFileInfo(
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
console.log(`[getVCDFileInfo] 开始解析 VCD 文件: ${vcdFilePath}`);
console.log(`[getVCDFileInfo] containerId: ${containerId}`);
// 检查文件是否存在 // 检查文件是否存在
if (!fs.existsSync(vcdFilePath)) { if (!fs.existsSync(vcdFilePath)) {
console.error(`[getVCDFileInfo] 文件不存在: ${vcdFilePath}`);
panel.webview.postMessage({ panel.webview.postMessage({
command: "vcdInfo", command: "vcdInfo",
containerId: containerId, containerId: containerId,
@ -541,8 +619,14 @@ async function getVCDFileInfo(
timeRange = `${minTime} - ${maxTime}`; timeRange = `${minTime} - ${maxTime}`;
} }
// 解析前几个信号的真实数据 // 解析信号的真实数据
const signals = parseVCDSignals(content, 3); // 只解析前3个信号 // 增加到20个信号以便显示更多波形平衡性能和完整性
const signals = parseVCDSignals(content, 20);
console.log(`[getVCDFileInfo] 解析到 ${signals.length} 个有效信号`);
signals.forEach((sig, idx) => {
console.log(`[getVCDFileInfo] 信号${idx + 1}: ${sig.name}, 值变化数: ${sig.values.length}`);
});
// 发送信息回前端 // 发送信息回前端
panel.webview.postMessage({ panel.webview.postMessage({
@ -630,10 +714,13 @@ function parseVCDSignals(content: string, maxSignals: number = 3) {
// 解析信号值变化 // 解析信号值变化
// 格式1: 单比特信号 "0!" 或 "1!" // 格式1: 单比特信号 "0!" 或 "1!"
// 格式2: 多比特信号 "b1010 !" // 格式2: 多比特信号 "b1010 !"
// 转义标识符中的特殊字符
const escapedId = signalDef.identifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
if (signalDef.width === 1) { if (signalDef.width === 1) {
// 单比特信号 // 单比特信号
const singleBitMatch = trimmedLine.match( const singleBitMatch = trimmedLine.match(
new RegExp(`^([01xz])${signalDef.identifier}$`) new RegExp(`^([01xz])${escapedId}$`)
); );
if (singleBitMatch) { if (singleBitMatch) {
values.push({ time: currentTime, value: singleBitMatch[1] }); values.push({ time: currentTime, value: singleBitMatch[1] });
@ -641,7 +728,7 @@ function parseVCDSignals(content: string, maxSignals: number = 3) {
} else { } else {
// 多比特信号 // 多比特信号
const multiBitMatch = trimmedLine.match( const multiBitMatch = trimmedLine.match(
new RegExp(`^b([01xz]+)\\s+${signalDef.identifier}$`) new RegExp(`^b([01xz]+)\\s+${escapedId}$`)
); );
if (multiBitMatch) { if (multiBitMatch) {
values.push({ time: currentTime, value: multiBitMatch[1] }); values.push({ time: currentTime, value: multiBitMatch[1] });
@ -654,6 +741,8 @@ function parseVCDSignals(content: string, maxSignals: number = 3) {
} }
} }
// 只添加有值变化数据的信号
if (values.length > 0) {
signals.push({ signals.push({
name: signalDef.name, name: signalDef.name,
identifier: signalDef.identifier, identifier: signalDef.identifier,
@ -661,6 +750,7 @@ function parseVCDSignals(content: string, maxSignals: number = 3) {
values: values, values: values,
}); });
} }
}
} catch (error) { } catch (error) {
console.error("解析 VCD 信号数据失败:", error); console.error("解析 VCD 信号数据失败:", error);
} }

View File

@ -224,22 +224,3 @@ export async function getUserInfo(): Promise<UserInfoResponse> {
method: 'GET' method: 'GET'
}); });
} }
/** 余额查询响应 */
export interface CreditBalanceResponse {
success: boolean;
balance?: number;
error?: string;
}
/**
* 查询用户资源点余额
* GET /api/dialog/balance?userId=xxx
*/
export async function getCreditBalance(userId: string): Promise<CreditBalanceResponse> {
console.log('[API] 查询余额: userId=', userId);
return request<CreditBalanceResponse>(`/api/dialog/balance?userId=${userId}`, {
method: 'GET',
timeout: 5000
});
}

View File

@ -1,210 +0,0 @@
/**
* 资源点余额管理服务
* 负责缓存余额、主动查询、发送前检测
*/
import * as vscode from 'vscode';
import * as https from 'https';
import * as http from 'http';
import { URL } from 'url';
import { getStrangeLoopApiUrl } from '../config/settings';
import { getCachedUserInfo } from './userService';
/** 低余额阈值 */
const LOW_CREDIT_THRESHOLD = 5;
/** 缓存的余额 */
let cachedBalance: number | null = null;
/** 最后更新时间 */
let lastUpdateTime: number = 0;
/** 缓存有效期5分钟 */
const CACHE_TTL_MS = 5 * 60 * 1000;
/**
* 更新缓存的余额(从 SSE credit_update 事件调用)
*/
export function updateCachedBalance(balance: number): void {
cachedBalance = balance;
lastUpdateTime = Date.now();
console.log('[CreditsService] 余额已更新:', balance);
}
/**
* 获取缓存的余额
*/
export function getCachedBalance(): number | null {
return cachedBalance;
}
/**
* 检查缓存是否有效
*/
function isCacheValid(): boolean {
if (cachedBalance === null) return false;
return Date.now() - lastUpdateTime < CACHE_TTL_MS;
}
/**
* StrangeLoop 余额响应类型
*/
interface StrangeLoopBalanceResponse {
userId?: number;
availableCredits?: number;
totalCredits?: number;
error?: string;
message?: string;
}
/**
* 主动查询余额(直接调用 StrangeLoop 接口)
*/
export async function fetchBalance(): Promise<number | null> {
try {
// 获取 JWT token
const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
if (!session?.accessToken) {
console.warn('[CreditsService] 无法查询余额:未登录');
return null;
}
const token = session.accessToken;
console.log('[CreditsService] 开始查询余额token 长度:', token.length);
// 直接调用 StrangeLoop 的 /api/credit/balance 接口
const response = await callStrangeLoopBalance(token);
if (response.availableCredits !== undefined) {
const balance = response.availableCredits;
updateCachedBalance(balance);
console.log('[CreditsService] 余额查询成功:', balance);
return balance;
} else {
console.warn('[CreditsService] 查询余额失败:', response.error || response.message);
return null;
}
} catch (error) {
console.error('[CreditsService] 查询余额异常:', error);
return null;
}
}
/**
* 调用 StrangeLoop 余额接口
*/
async function callStrangeLoopBalance(token: string): Promise<StrangeLoopBalanceResponse> {
const urlStr = getStrangeLoopApiUrl('/api/credit/balance');
const url = new URL(urlStr);
const isHttps = url.protocol === 'https:';
const httpModule = isHttps ? https : http;
// 余额查询使用固定短超时,避免阻塞发送前检查
const BALANCE_TIMEOUT_MS = 5000;
const requestOptions: http.RequestOptions = {
hostname: url.hostname,
port: url.port || (isHttps ? 443 : 80),
path: url.pathname + url.search,
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
timeout: BALANCE_TIMEOUT_MS
};
return new Promise((resolve, reject) => {
const req = httpModule.request(requestOptions, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
console.log('[CreditsService] 响应状态码:', res.statusCode);
console.log('[CreditsService] 响应内容:', data);
try {
const json = JSON.parse(data);
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
resolve(json as StrangeLoopBalanceResponse);
} else if (res.statusCode === 401 || res.statusCode === 403) {
// 登录过期或无权限
resolve({ error: '登录已过期,请重新登录' });
} else {
resolve({ error: json.error || json.message || json.msg || `HTTP ${res.statusCode}` });
}
} catch (e) {
resolve({ error: `解析响应失败: ${data}` });
}
});
});
req.on('error', (error) => {
reject(error);
});
req.on('timeout', () => {
req.destroy();
reject(new Error('请求超时'));
});
req.end();
});
}
/**
* 获取当前余额(优先使用缓存,过期则主动查询)
*/
export async function getBalance(): Promise<number | null> {
if (isCacheValid()) {
return cachedBalance;
}
return await fetchBalance();
}
/**
* 检查余额是否足够发送消息
* @returns { allowed: boolean, balance: number | null, message?: string }
*/
export async function checkBalanceBeforeSend(): Promise<{
allowed: boolean;
balance: number | null;
message?: string;
}> {
const userInfo = getCachedUserInfo();
if (!userInfo) {
// 未登录,允许发送(后端会处理)
return { allowed: true, balance: null };
}
const balance = await getBalance();
if (balance === null) {
// 无法获取余额,允许发送(后端会处理)
console.warn('[CreditsService] 无法获取余额,允许发送');
return { allowed: true, balance: null };
}
if (balance < LOW_CREDIT_THRESHOLD) {
return {
allowed: false,
balance,
message: `资源点余额不足!当前余额 ${balance.toFixed(2)} 点,低于最低要求 ${LOW_CREDIT_THRESHOLD} 点。请充值后再试。`
};
}
return { allowed: true, balance };
}
/**
* 清除缓存(登出时调用)
*/
export function clearBalanceCache(): void {
cachedBalance = null;
lastUpdateTime = 0;
console.log('[CreditsService] 余额缓存已清除');
}

View File

@ -12,14 +12,12 @@ import { getConfig } from '../config/settings';
import type { DialogRequest, ToolCallRequest, AskUserEvent, RunMode, ServiceTier, ToolConfirmEvent, PlanConfirmEvent } from '../types/api'; import type { DialogRequest, ToolCallRequest, AskUserEvent, RunMode, ServiceTier, ToolConfirmEvent, PlanConfirmEvent } from '../types/api';
import { submitToolConfirm, submitAnswer, stopDialog } from './apiClient'; import { submitToolConfirm, submitAnswer, stopDialog } from './apiClient';
import { ChatHistoryManager } from '../utils/chatHistoryManager'; import { ChatHistoryManager } from '../utils/chatHistoryManager';
import { getUserIdFromToken } from '../utils/jwtUtils';
import { updateCachedBalance } from './creditsService';
/** /**
* 消息段落类型 * 消息段落类型
*/ */
export interface MessageSegment { export interface MessageSegment {
type: 'text' | 'tool' | 'question' | 'agent' | 'plan' | 'progress'; type: 'text' | 'tool' | 'question' | 'agent' | 'plan';
content?: string; content?: string;
toolName?: string; toolName?: string;
toolStatus?: 'running' | 'success' | 'error'; toolStatus?: 'running' | 'success' | 'error';
@ -34,11 +32,8 @@ export interface MessageSegment {
agentSteps?: AgentStep[]; agentSteps?: AgentStep[];
// 计划相关字段 // 计划相关字段
planTitle?: string; planTitle?: string;
planPhases?: import('../types/api').PlanPhase[];
planSteps?: string[]; planSteps?: string[];
planSummary?: string; planSummary?: string;
// 进度条相关字段(独立于 plan用于执行模式
progressPhases?: import('../types/api').PlanPhase[];
} }
/** /**
@ -67,7 +62,7 @@ export interface DialogCallbacks {
/** 工具确认请求Ask 模式) */ /** 工具确认请求Ask 模式) */
onToolConfirm?: (confirmId: number, toolName: string, toolInput: Record<string, unknown>) => void; onToolConfirm?: (confirmId: number, toolName: string, toolInput: Record<string, unknown>) => void;
/** 计划确认请求Plan 模式) */ /** 计划确认请求Plan 模式) */
onPlanConfirm?: (confirmId: number, title: string, phases: import('../types/api').PlanPhase[] | undefined, steps: string[] | undefined, summary: string) => void; onPlanConfirm?: (confirmId: number, title: string, steps: string[], summary: string) => void;
/** 显示问题ask_user */ /** 显示问题ask_user */
onQuestion?: (askId: string, question: string, options: string[]) => void; onQuestion?: (askId: string, question: string, options: string[]) => void;
/** 实时更新段落(流式过程中) */ /** 实时更新段落(流式过程中) */
@ -80,8 +75,6 @@ export interface DialogCallbacks {
onNotification?: (message: string) => void; onNotification?: (message: string) => void;
/** 上下文使用量更新 */ /** 上下文使用量更新 */
onContextUsage?: (data: { currentTokens: number; maxTokens: number; percentage: number }) => void; onContextUsage?: (data: { currentTokens: number; maxTokens: number; percentage: number }) => void;
/** 阶段进度更新 */
onPhaseProgress?: (phaseId: string, status: string) => void;
} }
/** /**
@ -93,10 +86,8 @@ export class DialogSession {
private toolContext: ToolExecutorContext; private toolContext: ToolExecutorContext;
private accumulatedText = ''; private accumulatedText = '';
private isActive = false; private isActive = false;
private hasCompleted = false; // 标记是否已收到 complete 事件
private segments: MessageSegment[] = []; private segments: MessageSegment[] = [];
private currentTextSegment: MessageSegment | null = null; private currentTextSegment: MessageSegment | null = null;
private completeCallback: ((segments: MessageSegment[]) => void) | null = null; // 保存完成回调,用于 abort 时触发
constructor(extensionPath: string, existingTaskId?: string) { constructor(extensionPath: string, existingTaskId?: string) {
// 支持复用现有 taskId用于 Plan 模式确认后继续执行) // 支持复用现有 taskId用于 Plan 模式确认后继续执行)
@ -334,35 +325,12 @@ export class DialogSession {
} }
this.isActive = true; this.isActive = true;
this.hasCompleted = false; // 重置完成标志
this.accumulatedText = ''; this.accumulatedText = '';
this.segments = []; this.segments = [];
this.currentTextSegment = null; this.currentTextSegment = null;
this.completeCallback = callbacks.onComplete || null; // 保存完成回调,用于 abort 时触发
const config = getConfig(); const config = getConfig();
// 从登录 session 获取真实 userId
let userId = config.userId; // 默认值
try {
console.log('[DialogSession] 尝试获取登录 session...');
const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
console.log('[DialogSession] session 结果:', session ? '已获取' : 'null/undefined');
if (session?.accessToken) {
console.log('[DialogSession] accessToken 长度:', session.accessToken.length);
const parsedUserId = getUserIdFromToken(session.accessToken);
console.log('[DialogSession] 解析的 userId:', parsedUserId);
if (parsedUserId) {
userId = parsedUserId;
console.log('[DialogSession] 使用真实 userId:', userId);
}
} else {
console.log('[DialogSession] 未获取到 accessToken使用默认 userId:', userId);
}
} catch (error) {
console.warn('[DialogSession] 获取登录 session 失败:', error);
}
// 获取压缩数据和新消息(用于后端重启后恢复) // 获取压缩数据和新消息(用于后端重启后恢复)
const historyManager = ChatHistoryManager.getInstance(); const historyManager = ChatHistoryManager.getInstance();
const compactedData = await historyManager.loadCompactedData(this.taskId); const compactedData = await historyManager.loadCompactedData(this.taskId);
@ -372,12 +340,10 @@ export class DialogSession {
const knowledgeData = await this.loadKnowledgeData(); const knowledgeData = await this.loadKnowledgeData();
console.log('[DialogSession] knowledgeData 加载结果:', knowledgeData ? `${knowledgeData.length} 字符` : 'null'); console.log('[DialogSession] knowledgeData 加载结果:', knowledgeData ? `${knowledgeData.length} 字符` : 'null');
console.log('[DialogSession] serviceTier 参数:', serviceTier, '-> 使用:', serviceTier || config.serviceTier);
const request: DialogRequest = { const request: DialogRequest = {
taskId: this.taskId, taskId: this.taskId,
message, message,
userId, userId: config.userId,
mode: mode || 'agent', mode: mode || 'agent',
serviceTier: serviceTier || config.serviceTier, // 优先使用传入的参数 serviceTier: serviceTier || config.serviceTier, // 优先使用传入的参数
compactedData: compactedData || undefined, compactedData: compactedData || undefined,
@ -460,8 +426,6 @@ export class DialogSession {
callbacks.onToolComplete?.(data.tool_name, data.result); callbacks.onToolComplete?.(data.tool_name, data.result);
// 实时发送段落更新 // 实时发送段落更新
callbacks.onSegmentUpdate?.(this.segments); callbacks.onSegmentUpdate?.(this.segments);
// 追踪工具执行结果(用于后端重启后恢复)
historyManager.trackToolResult(data.tool_name, data.result);
}, },
onToolError: (data) => { onToolError: (data) => {
@ -469,8 +433,6 @@ export class DialogSession {
callbacks.onToolError?.(data.tool_name, data.error); callbacks.onToolError?.(data.tool_name, data.error);
// 实时发送段落更新 // 实时发送段落更新
callbacks.onSegmentUpdate?.(this.segments); callbacks.onSegmentUpdate?.(this.segments);
// 追踪工具执行错误(用于后端重启后恢复)
historyManager.trackToolResult(data.tool_name, `[错误] ${data.error}`);
}, },
onToolConfirm: async (data: ToolConfirmEvent) => { onToolConfirm: async (data: ToolConfirmEvent) => {
@ -546,12 +508,10 @@ export class DialogSession {
const askId = `ask_${data.confirmId}`; const askId = `ask_${data.confirmId}`;
// 添加计划段落到聊天界面(包含 askId 用于响应) // 添加计划段落到聊天界面(包含 askId 用于响应)
// 支持新格式phases和旧格式steps
this.segments.push({ this.segments.push({
type: 'plan', type: 'plan',
askId: askId, askId: askId,
planTitle: data.title, planTitle: data.title,
planPhases: data.phases,
planSteps: data.steps, planSteps: data.steps,
planSummary: data.summary planSummary: data.summary
}); });
@ -572,108 +532,7 @@ export class DialogSession {
} }
// 调用回调通知 UI // 调用回调通知 UI
callbacks.onPlanConfirm?.(data.confirmId, data.title, data.phases, data.steps, data.summary); callbacks.onPlanConfirm?.(data.confirmId, data.title, data.steps, data.summary);
},
onPhaseProgress: (data: import('../types/api').PhaseProgressEvent) => {
console.log('[DialogSession] onPhaseProgress:', data.phaseId, data.status);
// 1. 尝试更新 plan segment兼容旧逻辑
for (let i = this.segments.length - 1; i >= 0; i--) {
const seg = this.segments[i];
if (seg.type === 'plan' && seg.planPhases) {
seg.planPhases = seg.planPhases.map(phase => {
if (phase.id === data.phaseId) {
return { ...phase, status: data.status };
}
return phase;
});
callbacks.onSegmentUpdate?.(this.segments);
break;
}
}
// 2. 通知外部更新独立进度条
callbacks.onPhaseProgress?.(data.phaseId, data.status);
},
onPlanStepAdd: (data: import('../types/api').PlanStepAddEvent) => {
console.log('[DialogSession] onPlanStepAdd:', data.phaseId, data.step);
for (let i = this.segments.length - 1; i >= 0; i--) {
const seg = this.segments[i];
if (seg.type === 'plan' && seg.planPhases) {
seg.planPhases = seg.planPhases.map(phase => {
if (phase.id === data.phaseId) {
const newSteps = [...(phase.steps || [])];
if (data.index >= 0 && data.index < newSteps.length) {
newSteps.splice(data.index, 0, data.step);
} else {
newSteps.push(data.step);
}
return { ...phase, steps: newSteps };
}
return phase;
});
break;
}
}
callbacks.onSegmentUpdate?.(this.segments);
},
onPlanStepRemove: (data: import('../types/api').PlanStepRemoveEvent) => {
console.log('[DialogSession] onPlanStepRemove:', data.phaseId, data.stepIndex);
for (let i = this.segments.length - 1; i >= 0; i--) {
const seg = this.segments[i];
if (seg.type === 'plan' && seg.planPhases) {
seg.planPhases = seg.planPhases.map(phase => {
if (phase.id === data.phaseId && phase.steps) {
const newSteps = [...phase.steps];
newSteps.splice(data.stepIndex, 1);
return { ...phase, steps: newSteps };
}
return phase;
});
break;
}
}
callbacks.onSegmentUpdate?.(this.segments);
},
onPlanStepUpdate: (data: import('../types/api').PlanStepUpdateEvent) => {
console.log('[DialogSession] onPlanStepUpdate:', data.phaseId, data.stepIndex);
for (let i = this.segments.length - 1; i >= 0; i--) {
const seg = this.segments[i];
if (seg.type === 'plan' && seg.planPhases) {
seg.planPhases = seg.planPhases.map(phase => {
if (phase.id === data.phaseId && phase.steps) {
const newSteps = [...phase.steps];
if (data.stepIndex >= 0 && data.stepIndex < newSteps.length) {
newSteps[data.stepIndex] = data.step;
}
return { ...phase, steps: newSteps };
}
return phase;
});
break;
}
}
callbacks.onSegmentUpdate?.(this.segments);
},
onPlanSummaryUpdate: (data: import('../types/api').PlanSummaryUpdateEvent) => {
console.log('[DialogSession] onPlanSummaryUpdate');
for (let i = this.segments.length - 1; i >= 0; i--) {
const seg = this.segments[i];
if (seg.type === 'plan') {
seg.planSummary = data.summary;
break;
}
}
callbacks.onSegmentUpdate?.(this.segments);
}, },
onAskUser: async (data: AskUserEvent) => { onAskUser: async (data: AskUserEvent) => {
@ -697,7 +556,6 @@ export class DialogSession {
onComplete: (data) => { onComplete: (data) => {
this.isActive = false; this.isActive = false;
this.hasCompleted = true; // 标记已收到 complete 事件
this.finalizeTextSegment(); this.finalizeTextSegment();
// 追踪 AI 消息(用于后端重启后恢复) // 追踪 AI 消息(用于后端重启后恢复)
@ -796,40 +654,12 @@ export class DialogSession {
callbacks.onContextUsage?.(data); callbacks.onContextUsage?.(data);
}, },
onCreditUpdate: (data) => {
console.log('[DialogSession] onCreditUpdate: 扣除', data.deductedCredits, '剩余', data.remainingCredits);
// 更新余额缓存
updateCachedBalance(data.remainingCredits);
// 资源点余额低于阈值时弹窗提醒
const LOW_CREDIT_THRESHOLD = 5;
if (data.remainingCredits < LOW_CREDIT_THRESHOLD) {
vscode.window.showWarningMessage(
`资源点余额不足!当前剩余 ${data.remainingCredits.toFixed(2)} 点,请及时充值。`,
'去充值'
).then(selection => {
if (selection === '去充值') {
// 打开充值页面
vscode.env.openExternal(vscode.Uri.parse('https://iccoder.com/recharge'));
}
});
}
},
onOpen: () => { onOpen: () => {
console.log('[DialogSession] SSE 连接已建立'); console.log('[DialogSession] SSE 连接已建立');
}, },
onClose: () => { onClose: () => {
console.log('[DialogSession] SSE 连接已关闭'); console.log('[DialogSession] SSE 连接已关闭');
// 如果没有收到 complete 事件,需要补充完成逻辑
if (!this.hasCompleted && this.isActive) {
console.log('[DialogSession] 未收到 complete 事件,补充完成处理');
this.finalizeTextSegment();
if (this.accumulatedText) {
historyManager.trackAiMessage(this.accumulatedText);
}
callbacks.onComplete?.(this.segments);
}
this.isActive = false; this.isActive = false;
} }
}; };
@ -848,25 +678,13 @@ export class DialogSession {
* 中止当前对话 * 中止当前对话
*/ */
abort(): void { abort(): void {
// 先标记完成,防止 onClose 重复触发
const wasActive = this.isActive;
this.hasCompleted = true;
this.isActive = false;
if (this.sseController) { if (this.sseController) {
this.sseController.abort(); this.sseController.abort();
this.sseController = null; this.sseController = null;
} }
this.isActive = false;
userInteractionManager.cancelAll(); userInteractionManager.cancelAll();
// 如果之前是活跃状态,触发完成回调以结束 Promise
if (wasActive && this.completeCallback) {
this.finalizeTextSegment();
console.log('[DialogSession] abort 触发完成回调');
this.completeCallback(this.segments);
this.completeCallback = null;
}
// 通知后端停止处理 // 通知后端停止处理
stopDialog(this.taskId).catch(err => { stopDialog(this.taskId).catch(err => {
console.warn('[DialogSession] 停止对话请求失败:', err); console.warn('[DialogSession] 停止对话请求失败:', err);
@ -931,7 +749,6 @@ class DialogManager {
*/ */
abortCurrentSession(): void { abortCurrentSession(): void {
this.currentSession?.abort(); this.currentSession?.abort();
this.currentSession = null; // 清空会话,确保下次创建新会话
} }
} }

View File

@ -3,7 +3,6 @@ import * as http from "http";
import * as path from "path"; import * as path from "path";
import * as fs from "fs"; import * as fs from "fs";
import { onTokenReceived, type UserInfo, clearUserInfo } from "./userService"; import { onTokenReceived, type UserInfo, clearUserInfo } from "./userService";
import { getConfig } from "../config/settings";
/** /**
* IC Coder Authentication Provider * IC Coder Authentication Provider
@ -14,6 +13,7 @@ export class ICCoderAuthenticationProvider
{ {
private static readonly AUTH_TYPE = "iccoder"; private static readonly AUTH_TYPE = "iccoder";
private static readonly AUTH_NAME = "IC Coder"; private static readonly AUTH_NAME = "IC Coder";
private static readonly LOGIN_URL = "http://192.168.1.108:2005/login";
private static loginServer: http.Server | null = null; private static loginServer: http.Server | null = null;
private static currentPort: number | null = null; private static currentPort: number | null = null;
@ -156,8 +156,9 @@ export class ICCoderAuthenticationProvider
// 构建登录 URL // 构建登录 URL
const callbackUrl = `http://localhost:${port}/callback`; const callbackUrl = `http://localhost:${port}/callback`;
const config = getConfig(); const loginUrl = `${
const loginUrl = `${config.loginUrl}?redirect_uri=${encodeURIComponent(callbackUrl)}`; ICCoderAuthenticationProvider.LOGIN_URL
}?redirect_uri=${encodeURIComponent(callbackUrl)}`;
console.log("🔐 登录服务器已启动,监听端口:", port); console.log("🔐 登录服务器已启动,监听端口:", port);
console.log("🌐 登录 URL:", loginUrl); console.log("🌐 登录 URL:", loginUrl);

View File

@ -28,8 +28,7 @@ import type {
AgentProgressEvent, AgentProgressEvent,
AgentCompleteEvent, AgentCompleteEvent,
AgentErrorEvent, AgentErrorEvent,
ContextUsageEvent, ContextUsageEvent
CreditUpdateEvent
} from '../types/api'; } from '../types/api';
import type { MemoryCompactedEvent } from '../types/memory'; import type { MemoryCompactedEvent } from '../types/memory';
@ -45,16 +44,6 @@ export interface SSECallbacks {
onToolConfirm?: (data: ToolConfirmEvent) => void; onToolConfirm?: (data: ToolConfirmEvent) => void;
/** 收到计划确认请求Plan 模式) */ /** 收到计划确认请求Plan 模式) */
onPlanConfirm?: (data: PlanConfirmEvent) => void; onPlanConfirm?: (data: PlanConfirmEvent) => void;
/** 阶段进度更新 */
onPhaseProgress?: (data: import('../types/api').PhaseProgressEvent) => void;
/** 添加计划步骤 */
onPlanStepAdd?: (data: import('../types/api').PlanStepAddEvent) => void;
/** 删除计划步骤 */
onPlanStepRemove?: (data: import('../types/api').PlanStepRemoveEvent) => void;
/** 更新计划步骤 */
onPlanStepUpdate?: (data: import('../types/api').PlanStepUpdateEvent) => void;
/** 更新计划摘要 */
onPlanSummaryUpdate?: (data: import('../types/api').PlanSummaryUpdateEvent) => void;
/** 工具开始执行 */ /** 工具开始执行 */
onToolStart?: (data: ToolStartEvent) => void; onToolStart?: (data: ToolStartEvent) => void;
/** 工具执行完成 */ /** 工具执行完成 */
@ -85,8 +74,6 @@ export interface SSECallbacks {
onMemoryCompacted?: (data: MemoryCompactedEvent) => void; onMemoryCompacted?: (data: MemoryCompactedEvent) => void;
/** 上下文使用量更新 */ /** 上下文使用量更新 */
onContextUsage?: (data: ContextUsageEvent) => void; onContextUsage?: (data: ContextUsageEvent) => void;
/** 资源点余额更新 */
onCreditUpdate?: (data: CreditUpdateEvent) => void;
/** 连接打开 */ /** 连接打开 */
onOpen?: () => void; onOpen?: () => void;
/** 连接关闭 */ /** 连接关闭 */
@ -299,21 +286,6 @@ function dispatchEvent(
case 'plan_confirm': case 'plan_confirm':
callbacks.onPlanConfirm?.(data as PlanConfirmEvent); callbacks.onPlanConfirm?.(data as PlanConfirmEvent);
break; break;
case 'phase_progress':
callbacks.onPhaseProgress?.(data as import('../types/api').PhaseProgressEvent);
break;
case 'plan_step_add':
callbacks.onPlanStepAdd?.(data as import('../types/api').PlanStepAddEvent);
break;
case 'plan_step_remove':
callbacks.onPlanStepRemove?.(data as import('../types/api').PlanStepRemoveEvent);
break;
case 'plan_step_update':
callbacks.onPlanStepUpdate?.(data as import('../types/api').PlanStepUpdateEvent);
break;
case 'plan_summary_update':
callbacks.onPlanSummaryUpdate?.(data as import('../types/api').PlanSummaryUpdateEvent);
break;
case 'tool_start': case 'tool_start':
callbacks.onToolStart?.(data as ToolStartEvent); callbacks.onToolStart?.(data as ToolStartEvent);
break; break;
@ -359,9 +331,6 @@ function dispatchEvent(
case 'context_usage': case 'context_usage':
callbacks.onContextUsage?.(data as ContextUsageEvent); callbacks.onContextUsage?.(data as ContextUsageEvent);
break; break;
case 'credit_update':
callbacks.onCreditUpdate?.(data as CreditUpdateEvent);
break;
case 'heartbeat': case 'heartbeat':
// 心跳事件:仅用于保持连接,不需要特殊处理 // 心跳事件:仅用于保持连接,不需要特殊处理
// Node.js req.setTimeout 会在收到数据时自动重置计时器 // Node.js req.setTimeout 会在收到数据时自动重置计时器

View File

@ -8,7 +8,7 @@ import * as os from 'os';
import * as fs from 'fs'; import * as fs from 'fs';
import { readFileContent, readDirectory } from '../utils/readFiles'; import { readFileContent, readDirectory } from '../utils/readFiles';
import { createOrOverwriteFile } from '../utils/createFiles'; import { createOrOverwriteFile } from '../utils/createFiles';
import { generateVCD, checkIverilogAvailable, generateMultiVCD, DumpModule } from '../utils/iverilogRunner'; import { generateVCD, checkIverilogAvailable } from '../utils/iverilogRunner';
import { analyzeVcdFile } from '../utils/vcdParser'; import { analyzeVcdFile } from '../utils/vcdParser';
import { executeWaveformTrace, WaveformTraceArgs } from '../utils/waveformTracer'; import { executeWaveformTrace, WaveformTraceArgs } from '../utils/waveformTracer';
import { import {
@ -25,7 +25,6 @@ import type {
FileDeleteArgs, FileDeleteArgs,
FileListArgs, FileListArgs,
SyntaxCheckArgs, SyntaxCheckArgs,
IverilogArgs,
SimulationArgs, SimulationArgs,
WaveformSummaryArgs, WaveformSummaryArgs,
KnowledgeSaveArgs, KnowledgeSaveArgs,
@ -76,9 +75,6 @@ export async function executeToolCall(
case 'syntax_check': case 'syntax_check':
resultText = await executeSyntaxCheck(args as unknown as SyntaxCheckArgs, context); resultText = await executeSyntaxCheck(args as unknown as SyntaxCheckArgs, context);
break; break;
case 'iverilog':
resultText = await executeIverilog(args as unknown as IverilogArgs, context);
break;
case 'simulation': case 'simulation':
resultText = await executeSimulation(args as unknown as SimulationArgs, context); resultText = await executeSimulation(args as unknown as SimulationArgs, context);
break; break;
@ -274,71 +270,6 @@ async function executeSyntaxCheck(
} }
} }
/**
* 执行 iverilog 工具
* 直接执行 iverilog 命令
*/
async function executeIverilog(
args: IverilogArgs,
context: ToolExecutorContext
): Promise<string> {
// 检查 iverilog 是否可用
const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
if (!iverilogCheck.available) {
throw new Error(`iverilog 不可用: ${iverilogCheck.message}`);
}
// 获取工作目录
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error('没有打开的工作区');
}
const projectPath = workspaceFolders[0].uri.fsPath;
const workDir = args.workDir
? path.join(projectPath, args.workDir)
: projectPath;
// 解析参数
const iverilogPath = getIverilogPath(context.extensionPath);
const cmdArgs = args.args.split(/\s+/).filter(a => a.length > 0);
const { spawn } = require('child_process');
return new Promise((resolve, reject) => {
const child = spawn(iverilogPath, cmdArgs, {
cwd: workDir,
env: {
...process.env,
IVERILOG_ROOT: path.join(context.extensionPath, 'tools', 'iverilog')
}
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (data: Buffer) => {
stdout += data.toString();
});
child.stderr.on('data', (data: Buffer) => {
stderr += data.toString();
});
child.on('close', (code: number) => {
const output = stderr || stdout || '(无输出)';
if (code === 0) {
resolve(`执行成功\n${output}`);
} else {
resolve(`执行失败 (exit code: ${code})\n${output}`);
}
});
child.on('error', (error: Error) => {
reject(error);
});
});
}
/** /**
* 执行 simulation 工具 * 执行 simulation 工具
*/ */
@ -354,30 +285,7 @@ async function executeSimulation(
const projectPath = workspaceFolders[0].uri.fsPath; const projectPath = workspaceFolders[0].uri.fsPath;
// 检查是否有 dumpModules 参数(多 VCD 模式) // 调用现有的 generateVCD 函数
if (args.dumpModules) {
const modules = parseDumpModules(args.dumpModules);
const vcdDir = args.vcdDir || 'vcd';
const result = await generateMultiVCD(
projectPath,
context.extensionPath,
args.tbPath,
modules,
vcdDir
);
if (result.success) {
const vcdList = result.vcdFiles
.map(f => `- ${f.moduleName}: ${f.success ? f.vcdPath : '失败 - ' + f.error}`)
.join('\n');
return `${result.message}\n\nVCD 文件列表:\n${vcdList}${result.stdout ? '\n\n仿真输出:' + result.stdout : ''}`;
} else {
throw new Error(result.message);
}
}
// 原有单 VCD 逻辑
const result = await generateVCD(projectPath, context.extensionPath); const result = await generateVCD(projectPath, context.extensionPath);
if (result.success) { if (result.success) {
@ -395,17 +303,6 @@ async function executeSimulation(
} }
} }
/**
* 解析 dumpModules 参数
* 格式name:path,name:path
*/
function parseDumpModules(dumpModules: string): DumpModule[] {
return dumpModules.split(',').map(item => {
const [name, modulePath] = item.trim().split(':');
return { name: name.trim(), path: modulePath.trim() };
});
}
/** /**
* 执行 waveform_summary 工具 * 执行 waveform_summary 工具
* 解析 VCD 文件并返回波形摘要 * 解析 VCD 文件并返回波形摘要

View File

@ -56,11 +56,6 @@ export type SSEEventType =
| "tool_call" // 客户端工具调用请求 | "tool_call" // 客户端工具调用请求
| "tool_confirm" // 工具确认请求Ask 模式) | "tool_confirm" // 工具确认请求Ask 模式)
| "plan_confirm" // 计划确认请求Plan 模式) | "plan_confirm" // 计划确认请求Plan 模式)
| "phase_progress" // 阶段进度更新
| "plan_step_add" // 添加计划步骤
| "plan_step_remove" // 删除计划步骤
| "plan_step_update" // 更新计划步骤
| "plan_summary_update" // 更新计划摘要
| "tool_start" // 工具开始执行 | "tool_start" // 工具开始执行
| "tool_complete" // 工具执行完成 | "tool_complete" // 工具执行完成
| "tool_error" // 工具执行错误 | "tool_error" // 工具执行错误
@ -71,7 +66,6 @@ export type SSEEventType =
| "agent_error" // 子智能体错误 | "agent_error" // 子智能体错误
| "memory_compacted" // 记忆压缩完成 | "memory_compacted" // 记忆压缩完成
| "context_usage" // 上下文使用量 | "context_usage" // 上下文使用量
| "credit_update" // 资源点余额更新
| "complete" // 对话完成 | "complete" // 对话完成
| "error" // 错误 | "error" // 错误
| "warning" // 警告 | "warning" // 警告
@ -114,83 +108,20 @@ export interface ToolConfirmEvent {
timestamp: number; timestamp: number;
} }
/** 计划步骤 */
export interface PlanStep {
/** 步骤名称 */
name: string;
/** 步骤描述 */
description?: string;
}
/** 计划阶段 */
export interface PlanPhase {
/** 阶段ID: spec/design/sim/done */
id: string;
/** 阶段名称 */
name: string;
/** 阶段状态: skipped/completed/current/pending */
status: string;
/** 跳过原因 */
reason?: string;
/** 阶段内的步骤 */
steps: PlanStep[];
}
/** plan_confirm 事件数据Plan 模式计划确认) */ /** plan_confirm 事件数据Plan 模式计划确认) */
export interface PlanConfirmEvent { export interface PlanConfirmEvent {
/** 确认ID */ /** 确认ID */
confirmId: number; confirmId: number;
/** 计划标题 */ /** 计划标题 */
title: string; title: string;
/** 四阶段计划列表(新格式) */ /** 执行步骤列表 */
phases?: PlanPhase[]; steps: string[];
/** 执行步骤列表(旧格式,兼容) */
steps?: string[];
/** 计划摘要 */ /** 计划摘要 */
summary: string; summary: string;
/** 时间戳 */ /** 时间戳 */
timestamp: number; timestamp: number;
} }
/** phase_progress 事件数据(阶段进度更新) */
export interface PhaseProgressEvent {
/** 阶段ID: spec/design/sim/done */
phaseId: string;
/** 状态: current/completed */
status: string;
/** 时间戳 */
timestamp: number;
}
/** plan_step_add 事件数据(添加计划步骤) */
export interface PlanStepAddEvent {
phaseId: string;
step: PlanStep;
index: number;
timestamp: number;
}
/** plan_step_remove 事件数据(删除计划步骤) */
export interface PlanStepRemoveEvent {
phaseId: string;
stepIndex: number;
timestamp: number;
}
/** plan_step_update 事件数据(更新计划步骤) */
export interface PlanStepUpdateEvent {
phaseId: string;
stepIndex: number;
step: PlanStep;
timestamp: number;
}
/** plan_summary_update 事件数据(更新计划摘要) */
export interface PlanSummaryUpdateEvent {
summary: string;
timestamp: number;
}
/** ask_user 事件数据 */ /** ask_user 事件数据 */
export interface AskUserEvent { export interface AskUserEvent {
askId: string; askId: string;
@ -270,12 +201,6 @@ export interface ContextUsageEvent {
percentage: number; percentage: number;
} }
/** credit_update 事件数据 */
export interface CreditUpdateEvent {
deductedCredits: number;
remainingCredits: number;
}
// ============== 工具调用协议 (MCP 格式) ============== // ============== 工具调用协议 (MCP 格式) ==============
/** /**
@ -484,7 +409,6 @@ export type ToolName =
| "file_delete" | "file_delete"
| "file_list" | "file_list"
| "syntax_check" | "syntax_check"
| "iverilog"
| "simulation" | "simulation"
| "waveform_summary" | "waveform_summary"
| "waveform_trace" | "waveform_trace"
@ -519,21 +443,11 @@ export interface SyntaxCheckArgs {
code: string; code: string;
} }
/** iverilog 工具参数 */
export interface IverilogArgs {
args: string;
workDir?: string;
}
/** simulation 工具参数 */ /** simulation 工具参数 */
export interface SimulationArgs { export interface SimulationArgs {
rtlPath: string; rtlPath: string;
tbPath: string; tbPath: string;
duration?: string; duration?: string;
/** 要dump的模块列表格式name:path,name:path */
dumpModules?: string;
/** VCD输出目录默认'vcd' */
vcdDir?: string;
} }
/** waveform_summary 工具参数 */ /** waveform_summary 工具参数 */
@ -573,7 +487,6 @@ export type ToolArgs =
| FileDeleteArgs | FileDeleteArgs
| FileListArgs | FileListArgs
| SyntaxCheckArgs | SyntaxCheckArgs
| IverilogArgs
| SimulationArgs | SimulationArgs
| WaveformSummaryArgs | WaveformSummaryArgs
| WaveformTraceArgs | WaveformTraceArgs

View File

@ -715,10 +715,6 @@ export class ChatHistoryManager {
if (!projectPath) { if (!projectPath) {
console.error('[ChatHistoryManager] 无法保存压缩数据projectPath 为空'); console.error('[ChatHistoryManager] 无法保存压缩数据projectPath 为空');
// 通知用户压缩数据保存失败
vscode.window.showWarningMessage(
'对话历史压缩数据保存失败:无法确定项目路径。后端重启后可能无法恢复完整对话历史。'
);
return; return;
} }
@ -735,19 +731,6 @@ export class ChatHistoryManager {
// 文件不存在,使用空数组 // 文件不存在,使用空数组
} }
// 版本检查:防止旧版本覆盖新版本(从尾部扫描,与加载逻辑一致)
let existingSummary: CompactionSummaryMessage | null = null;
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].type === MessageType.COMPACTION_SUMMARY) {
existingSummary = messages[i] as CompactionSummaryMessage;
break;
}
}
if (existingSummary && existingSummary.version >= compacted.version) {
console.log(`[ChatHistoryManager] 跳过旧版本压缩数据: 现有版本=${existingSummary.version}, 新版本=${compacted.version}`);
return;
}
// 创建压缩摘要消息 // 创建压缩摘要消息
const summaryMessage: CompactionSummaryMessage = { const summaryMessage: CompactionSummaryMessage = {
type: MessageType.COMPACTION_SUMMARY, type: MessageType.COMPACTION_SUMMARY,
@ -910,14 +893,4 @@ export class ChatHistoryManager {
content: text content: text
}); });
} }
/**
* 追踪新消息(工具执行结果)
*/
public trackToolResult(toolName: string, result: string): void {
this.newMessagesSinceCompaction.push({
type: 'TOOL_RESULT',
content: `[${toolName}] ${result}`
});
}
} }

View File

@ -413,193 +413,3 @@ export async function checkIverilogAvailable(
}; };
} }
} }
/**
* 要 dump 的模块定义
*/
export interface DumpModule {
name: string; // 模块名(用于 VCD 文件名和宏名)
path: string; // 实例路径(如 dut.u_tx
}
/**
* 多 VCD 生成结果
*/
export interface MultiVCDResult {
success: boolean;
vcdFiles: Array<{
moduleName: string;
vcdPath: string;
success: boolean;
error?: string;
}>;
message: string;
stdout?: string;
}
/**
* 在 testbench 中注入条件编译代码
* 将原有的 $dumpfile/$dumpvars 替换为条件编译版本
*/
function injectConditionalDump(
tbContent: string,
dumpModules: DumpModule[],
vcdDir: string
): string {
// 匹配 $dumpfile 和 $dumpvars 语句(可能跨多行)
const dumpPattern = /(\$dumpfile\s*\([^)]+\)\s*;[\s\S]*?\$dumpvars\s*\([^)]+\)\s*;)/g;
// 生成条件编译代码
const conditionalCode = generateConditionalDumpCode(dumpModules, vcdDir);
// 替换原有的 dump 语句
const modified = tbContent.replace(dumpPattern, conditionalCode);
// 如果没有找到匹配,尝试单独匹配 $dumpfile
if (modified === tbContent) {
const singleDumpPattern = /\$dumpfile\s*\([^)]+\)\s*;/g;
return tbContent.replace(singleDumpPattern, conditionalCode);
}
return modified;
}
/**
* 生成条件编译的 dump 代码
*/
function generateConditionalDumpCode(
dumpModules: DumpModule[],
vcdDir: string
): string {
if (dumpModules.length === 0) {
return '$dumpfile("output.vcd");\n $dumpvars(0, dut);';
}
const lines: string[] = [];
dumpModules.forEach((module, index) => {
const macroName = `DUMP_${module.name.toUpperCase()}`;
const vcdPath = `${vcdDir}/${module.name}.vcd`;
const directive = index === 0 ? '`ifdef' : '`elsif';
lines.push(`${directive} ${macroName}`);
lines.push(` $dumpfile("${vcdPath}");`);
lines.push(` $dumpvars(1, ${module.path});`);
});
// 添加默认分支(使用第一个模块)
lines.push('`else');
lines.push(` $dumpfile("${vcdDir}/${dumpModules[0].name}.vcd");`);
lines.push(` $dumpvars(1, ${dumpModules[0].path});`);
lines.push('`endif');
return lines.join('\n');
}
/**
* 生成多个 VCD 文件(为不同子模块)
*/
export async function generateMultiVCD(
projectPath: string,
extensionPath: string,
tbPath: string,
dumpModules: DumpModule[],
vcdDir: string = 'vcd'
): Promise<MultiVCDResult> {
const results: MultiVCDResult['vcdFiles'] = [];
let allStdout = '';
try {
// 1. 创建 vcd 目录
const vcdDirPath = path.join(projectPath, vcdDir);
const vcdDirUri = vscode.Uri.file(vcdDirPath);
try {
await vscode.workspace.fs.createDirectory(vcdDirUri);
} catch {
// 目录可能已存在
}
// 2. 读取原始 testbench
const tbFullPath = path.isAbsolute(tbPath) ? tbPath : path.join(projectPath, tbPath);
const tbUri = vscode.Uri.file(tbFullPath);
const tbBytes = await vscode.workspace.fs.readFile(tbUri);
const originalTb = Buffer.from(tbBytes).toString('utf-8');
// 3. 注入条件编译代码
const modifiedTb = injectConditionalDump(originalTb, dumpModules, vcdDir);
await vscode.workspace.fs.writeFile(tbUri, Buffer.from(modifiedTb, 'utf-8'));
console.log('[generateMultiVCD] Testbench 已修改,开始多次仿真...');
// 4. 获取工具路径
const iverilogPath = await getIverilogPath(extensionPath);
const vvpPath = await getVvpPath(extensionPath);
const env = {
...process.env,
IVERILOG_ROOT: path.join(extensionPath, "tools", "iverilog"),
};
// 5. 获取所有 Verilog 文件
const projectCheck = await checkVerilogProject(projectPath);
const outputFile = path.join(projectPath, "simulation.vvp");
// 6. 循环执行仿真
for (const module of dumpModules) {
const macroName = `DUMP_${module.name.toUpperCase()}`;
const vcdPath = path.join(vcdDirPath, `${module.name}.vcd`);
console.log(`[generateMultiVCD] 仿真模块: ${module.name} (${macroName})`);
try {
// 编译(带宏定义)
const compileArgs = [
`-D${macroName}`,
"-o", outputFile,
...projectCheck.allVerilogFiles
];
await execCommand(iverilogPath, compileArgs, { cwd: projectPath, env });
// 仿真
const simResult = await execCommand(vvpPath, [outputFile], { cwd: projectPath, env });
allStdout += `\n[${module.name}] ${simResult.stdout}`;
results.push({
moduleName: module.name,
vcdPath: vcdPath,
success: true
});
} catch (error: any) {
console.error(`[generateMultiVCD] 模块 ${module.name} 仿真失败:`, error.message);
results.push({
moduleName: module.name,
vcdPath: vcdPath,
success: false,
error: error.message
});
// 继续执行其他模块
}
}
// 7. 清理中间文件
try {
await vscode.workspace.fs.delete(vscode.Uri.file(outputFile));
} catch {
// 忽略
}
const successCount = results.filter(r => r.success).length;
return {
success: successCount > 0,
vcdFiles: results,
message: `生成完成:${successCount}/${dumpModules.length} 个 VCD 文件成功`,
stdout: allStdout
};
} catch (error) {
return {
success: false,
vcdFiles: results,
message: `生成多 VCD 文件失败: ${error instanceof Error ? error.message : '未知错误'}`
};
}
}

View File

@ -1,73 +0,0 @@
/**
* JWT 工具函数
*/
/**
* JWT Payload 接口
*/
export interface JwtPayload {
sub?: string; // subject (通常是 userId)
userId?: number; // 用户ID (驼峰命名)
user_id?: number; // 用户ID (下划线命名)
exp?: number; // 过期时间
iat?: number; // 签发时间
[key: string]: unknown;
}
/**
* 解析 JWT token 的 payload
* @param token JWT token
* @returns 解析后的 payload解析失败返回 null
*/
export function parseJwtPayload(token: string): JwtPayload | null {
try {
const parts = token.split('.');
if (parts.length !== 3) {
console.warn('[JWT] token 格式不正确期望3部分实际:', parts.length);
return null;
}
// payload 是第二部分base64url 编码
const payload = parts[1];
// base64url 转 base64
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
// 解码
const jsonStr = Buffer.from(base64, 'base64').toString('utf-8');
const parsed = JSON.parse(jsonStr);
console.log('[JWT] 解析成功, payload 字段:', Object.keys(parsed));
console.log('[JWT] payload 内容:', JSON.stringify(parsed));
return parsed;
} catch (error) {
console.error('[JWT] 解析失败:', error);
return null;
}
}
/**
* 从 JWT token 中获取用户ID
* @param token JWT token
* @returns 用户ID字符串获取失败返回 null
*/
export function getUserIdFromToken(token: string): string | null {
const payload = parseJwtPayload(token);
if (!payload) {
return null;
}
// 支持多种字段名user_id, userId, sub
if (payload.user_id !== undefined) {
return String(payload.user_id);
}
if (payload.userId !== undefined) {
return String(payload.userId);
}
if (payload.sub !== undefined) {
return String(payload.sub);
}
console.warn('[JWT] payload 中没有 user_id, userId 或 sub 字段');
return null;
}

View File

@ -18,7 +18,6 @@ import { ChatHistoryManager } from "./chatHistoryManager";
import { dialogManager, DialogSession } from "../services/dialogService"; import { dialogManager, DialogSession } from "../services/dialogService";
import { userInteractionManager } from "../services/userInteraction"; import { userInteractionManager } from "../services/userInteraction";
import { healthCheck } from "../services/apiClient"; import { healthCheck } from "../services/apiClient";
import { checkBalanceBeforeSend } from "../services/creditsService";
import type { RunMode, ServiceTier } from "../types/api"; import type { RunMode, ServiceTier } from "../types/api";
@ -31,6 +30,27 @@ let currentSession: DialogSession | null = null;
/** 最后一个活跃的 taskId用于压缩等操作 */ /** 最后一个活跃的 taskId用于压缩等操作 */
let lastTaskId: string | null = null; let lastTaskId: string | null = null;
/** 待执行的计划Plan 模式确认后自动执行) */
let pendingPlanExecution: {
panel: vscode.WebviewPanel;
planTitle: string;
extensionPath: string;
taskId: string; // 保存 taskId 以便复用
} | null = null;
/**
* 设置待执行的计划(由 ICHelperPanel 调用)
*/
export function setPendingPlanExecution(
panel: vscode.WebviewPanel,
planTitle: string,
extensionPath: string,
taskId: string
): void {
pendingPlanExecution = { panel, planTitle, extensionPath, taskId };
console.log("[MessageHandler] 设置待执行计划:", planTitle, "taskId:", taskId);
}
/** /**
* 处理用户消息 * 处理用户消息
*/ */
@ -68,31 +88,17 @@ export async function handleUserMessage(
return; return;
} }
// 发送前检测余额
const balanceCheck = await checkBalanceBeforeSend();
if (!balanceCheck.allowed) {
console.warn("[MessageHandler] 余额不足,阻止发送:", balanceCheck.message);
// 显示错误提示
const selection = await vscode.window.showWarningMessage(
balanceCheck.message || "资源点余额不足",
"去充值"
);
if (selection === "去充值") {
vscode.env.openExternal(vscode.Uri.parse("https://iccoder.com/recharge"));
}
// 恢复输入状态
panel.webview.postMessage({
command: "updateSegments",
segments: [],
isComplete: true,
});
return;
}
// 尝试使用后端服务 // 尝试使用后端服务
if (useBackendService && extensionPath) { if (useBackendService && extensionPath) {
try { try {
await handleUserMessageWithBackend(panel, text, extensionPath, mode, undefined, serviceTier); await handleUserMessageWithBackend(
panel,
text,
extensionPath,
mode,
undefined,
serviceTier
);
return; return;
} catch (error) { } catch (error) {
console.error("后端服务不可用:", error); console.error("后端服务不可用:", error);
@ -136,11 +142,21 @@ async function handleUserMessageWithBackend(
// 优先使用 reuseTaskId其次使用 historyManager 的 taskId // 优先使用 reuseTaskId其次使用 historyManager 的 taskId
const taskIdToUse = reuseTaskId || historyManager.getCurrentTaskId(); const taskIdToUse = reuseTaskId || historyManager.getCurrentTaskId();
// 创建会话dialogManager 会自动处理旧会话的中止) // 创建或复用会话
currentSession = dialogManager.createSession(extensionPath, taskIdToUse || undefined); if (!currentSession || !currentSession.active) {
currentSession = dialogManager.createSession(
extensionPath,
taskIdToUse || undefined
);
// 保存 taskId 用于后续操作(如压缩) // 保存 taskId 用于后续操作(如压缩)
lastTaskId = currentSession.getTaskId(); lastTaskId = currentSession.getTaskId();
console.log("[MessageHandler] 创建会话: taskId=", lastTaskId, "来源=", taskIdToUse ? "historyManager" : "新生成"); console.log(
"[MessageHandler] 创建会话: taskId=",
lastTaskId,
"来源=",
taskIdToUse ? "historyManager" : "新生成"
);
}
// 显示状态栏 // 显示状态栏
panel.webview.postMessage({ panel.webview.postMessage({
@ -221,6 +237,39 @@ async function handleUserMessageWithBackend(
console.warn("保存AI响应历史失败:", error); console.warn("保存AI响应历史失败:", error);
} }
// 检查是否有待执行的计划Plan 模式确认后自动执行)
if (pendingPlanExecution) {
const {
panel: execPanel,
planTitle,
extensionPath: execPath,
taskId: reuseTaskId,
} = pendingPlanExecution;
pendingPlanExecution = null;
console.log(
"[MessageHandler] 自动执行计划:",
planTitle,
"复用 taskId:",
reuseTaskId
);
// 延迟一小段时间确保当前对话完全结束
setTimeout(async () => {
try {
// 复用 taskId 创建新会话,确保知识图谱数据不丢失
await handleUserMessageWithBackend(
execPanel,
`请按照刚才的计划执行:${planTitle}`,
execPath,
"agent",
reuseTaskId // 复用 Plan 模式的 taskId
);
} catch (err) {
console.error("[MessageHandler] 自动执行计划失败:", err);
}
}, 500);
}
resolve(); resolve();
}, },
@ -254,36 +303,6 @@ async function handleUserMessageWithBackend(
percentage: data.percentage, percentage: data.percentage,
}); });
}, },
onPhaseProgress: (phaseId, status) => {
// 发送阶段进度更新到 WebView
// 映射 phaseId: sim -> simulation
const stepMap: Record<string, string> = {
spec: "spec",
design: "design",
sim: "simulation",
done: "done",
};
const step = stepMap[phaseId] || phaseId;
if (status === "current") {
// 显示进度条并更新到当前步骤
panel.webview.postMessage({ type: "showProgress" });
panel.webview.postMessage({ type: "updateProgress", step });
} else if (status === "completed") {
// 更新到下一步(或完成)
const steps = ["spec", "design", "simulation", "done"];
const currentIndex = steps.indexOf(step);
if (currentIndex < steps.length - 1) {
panel.webview.postMessage({
type: "updateProgress",
step: steps[currentIndex + 1],
});
} else {
panel.webview.postMessage({ type: "completeProgress" });
}
}
},
}, },
mode, mode,
serviceTier // 传递服务等级 serviceTier // 传递服务等级
@ -366,10 +385,9 @@ export async function handlePlanAction(
panel: vscode.WebviewPanel, panel: vscode.WebviewPanel,
action: string, action: string,
planTitle: string, planTitle: string,
extensionPath: string, extensionPath: string
serviceTier?: ServiceTier
): Promise<void> { ): Promise<void> {
console.log("[handlePlanAction] action:", action, "planTitle:", planTitle, "serviceTier:", serviceTier); console.log("[handlePlanAction] action:", action, "planTitle:", planTitle);
switch (action) { switch (action) {
case "confirm": case "confirm":
@ -383,8 +401,7 @@ export async function handlePlanAction(
panel, panel,
`请按照刚才的计划执行:${planTitle}`, `请按照刚才的计划执行:${planTitle}`,
extensionPath, extensionPath,
"agent", "agent"
serviceTier
); );
break; break;
@ -400,8 +417,7 @@ export async function handlePlanAction(
panel, panel,
`请根据以下建议修改计划:${modification}`, `请根据以下建议修改计划:${modification}`,
extensionPath, extensionPath,
"plan", "plan"
serviceTier
); );
} }
break; break;

View File

@ -303,7 +303,6 @@ export function getConversationHistoryBarScript(): string {
let totalHistory = 0; let totalHistory = 0;
let hasMoreHistory = false; let hasMoreHistory = false;
let isLoadingHistory = false; let isLoadingHistory = false;
let currentLoadRequestId = 0; // 请求 ID用于防止并发加载
const HISTORY_PAGE_SIZE = 10; const HISTORY_PAGE_SIZE = 10;
const MAX_HISTORY_ITEMS = 100; const MAX_HISTORY_ITEMS = 100;
@ -347,15 +346,11 @@ export function getConversationHistoryBarScript(): string {
return; return;
} }
// 生成新的请求 ID用于防止并发加载
const requestId = ++currentLoadRequestId;
isLoadingHistory = true; isLoadingHistory = true;
vscode.postMessage({ vscode.postMessage({
command: 'loadConversationHistory', command: 'loadConversationHistory',
offset: currentOffset, offset: currentOffset,
limit: HISTORY_PAGE_SIZE, limit: HISTORY_PAGE_SIZE
requestId: requestId
}); });
} }
@ -367,19 +362,11 @@ export function getConversationHistoryBarScript(): string {
return; return;
} }
// 追加新数据(去重) // 追加新数据
const existingIds = new Set(conversationHistory.map(item => item.id)); conversationHistory = conversationHistory.concat(data.items);
const newItems = [];
for (const item of data.items) {
if (!existingIds.has(item.id)) {
existingIds.add(item.id);
newItems.push(item);
}
}
conversationHistory = conversationHistory.concat(newItems);
totalHistory = data.total; totalHistory = data.total;
hasMoreHistory = data.hasMore; hasMoreHistory = data.hasMore;
currentOffset = conversationHistory.length; currentOffset += data.items.length;
const historyList = document.getElementById('historyList'); const historyList = document.getElementById('historyList');
if (!historyList) { if (!historyList) {
@ -467,10 +454,9 @@ export function getConversationHistoryBarScript(): string {
}); });
} }
// 监听下拉菜单滚动事件(防止重复注册) // 监听下拉菜单滚动事件
const historyDropdownMenu = document.getElementById('historyDropdownMenu'); const historyDropdownMenu = document.getElementById('historyDropdownMenu');
if (historyDropdownMenu && !historyDropdownMenu._scrollListenerAdded) { if (historyDropdownMenu) {
historyDropdownMenu._scrollListenerAdded = true;
historyDropdownMenu.addEventListener('scroll', () => { historyDropdownMenu.addEventListener('scroll', () => {
const menu = historyDropdownMenu; const menu = historyDropdownMenu;
const scrollTop = menu.scrollTop; const scrollTop = menu.scrollTop;

View File

@ -675,29 +675,6 @@ export function getMessageAreaScript(): string {
${getPlanCardScript()} ${getPlanCardScript()}
// 解析多 VCD 文件路径
function parseMultiVcdPaths(toolResult) {
if (!toolResult) return [];
const result = String(toolResult);
// 匹配 "- moduleName: path" 格式
const vcdListMatch = result.match(/VCD 文件列表:[\\s\\S]*?(?=\\n\\n|$)/);
if (!vcdListMatch) return [];
const paths = [];
const lineRegex = /- (\\w+): ([^\\n]+)/g;
let match;
while ((match = lineRegex.exec(vcdListMatch[0])) !== null) {
const name = match[1];
const pathOrError = match[2].trim();
// 跳过失败的条目
if (!pathOrError.startsWith('失败')) {
paths.push({ name: name + '.vcd', path: pathOrError });
}
}
return paths;
}
// 获取工具图标 // 获取工具图标
function getToolIcon(toolName) { function getToolIcon(toolName) {
const iconMap = { const iconMap = {
@ -1080,17 +1057,7 @@ export function getMessageAreaScript(): string {
// 如果是仿真工具且成功完成,尝试添加波形预览 // 如果是仿真工具且成功完成,尝试添加波形预览
if (segment.toolName === 'simulation' && segment.toolStatus === 'success') { if (segment.toolName === 'simulation' && segment.toolStatus === 'success') {
// 尝试解析多个 VCD 文件(多 VCD 模式) // 优先使用显式提供的路径,否则从结果文本中解析
const vcdPaths = parseMultiVcdPaths(segment.toolResult);
if (vcdPaths.length > 0) {
// 多 VCD 模式:为每个文件创建预览
vcdPaths.forEach(vcdInfo => {
const waveformPreview = createWaveformPreview(vcdInfo.path, vcdInfo.name);
segmentDiv.appendChild(waveformPreview);
});
} else {
// 单 VCD 模式(兼容旧逻辑)
let vcdPath = segment.vcdFilePath; let vcdPath = segment.vcdFilePath;
if (!vcdPath && segment.toolResult) { if (!vcdPath && segment.toolResult) {
const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/); const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/);
@ -1105,7 +1072,6 @@ export function getMessageAreaScript(): string {
segmentDiv.appendChild(waveformPreview); segmentDiv.appendChild(waveformPreview);
} }
} }
}
// 添加折叠/展开事件监听 // 添加折叠/展开事件监听
if (shouldCollapse) { if (shouldCollapse) {
@ -1338,17 +1304,7 @@ export function getMessageAreaScript(): string {
// 如果是仿真工具且成功完成,尝试添加波形预览 // 如果是仿真工具且成功完成,尝试添加波形预览
if (segment.toolName === 'simulation' && segment.toolStatus === 'success') { if (segment.toolName === 'simulation' && segment.toolStatus === 'success') {
// 尝试解析多个 VCD 文件(多 VCD 模式) // 优先使用显式提供的路径,否则从结果文本中解析
const vcdPaths = parseMultiVcdPaths(segment.toolResult);
if (vcdPaths.length > 0) {
// 多 VCD 模式:为每个文件创建预览
vcdPaths.forEach(vcdInfo => {
const waveformPreview = createWaveformPreview(vcdInfo.path, vcdInfo.name);
segmentDiv.appendChild(waveformPreview);
});
} else {
// 单 VCD 模式(兼容旧逻辑)
let vcdPath = segment.vcdFilePath; let vcdPath = segment.vcdFilePath;
if (!vcdPath && segment.toolResult) { if (!vcdPath && segment.toolResult) {
const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/); const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/);
@ -1363,7 +1319,6 @@ export function getMessageAreaScript(): string {
segmentDiv.appendChild(waveformPreview); segmentDiv.appendChild(waveformPreview);
} }
} }
}
// 添加折叠/展开事件监听 // 添加折叠/展开事件监听
if (shouldCollapse) { if (shouldCollapse) {

View File

@ -4,7 +4,6 @@
* 功能说明: * 功能说明:
* - 显示执行计划的卡片界面 * - 显示执行计划的卡片界面
* - 包含计划标题、摘要和步骤列表 * - 包含计划标题、摘要和步骤列表
* - 摘要支持 Markdown 格式渲染
* - 提供确认执行、修改计划、取消等操作按钮 * - 提供确认执行、修改计划、取消等操作按钮
*/ */
@ -44,62 +43,11 @@ export function getPlanCardStyles(): string {
padding: 16px; padding: 16px;
} }
.plan-summary { .plan-summary {
color: var(--vscode-foreground); color: var(--vscode-descriptionForeground);
margin-bottom: 12px; margin-bottom: 12px;
font-size: 13px; font-size: 13px;
line-height: 1.6; line-height: 1.5;
} }
/* Markdown 渲染样式 */
.plan-summary h1, .plan-summary h2, .plan-summary h3, .plan-summary h4 {
margin: 16px 0 8px 0;
font-weight: 600;
color: var(--vscode-foreground);
}
.plan-summary h1 { font-size: 18px; border-bottom: 1px solid var(--vscode-input-border); padding-bottom: 6px; }
.plan-summary h2 { font-size: 16px; }
.plan-summary h3 { font-size: 14px; }
.plan-summary h4 { font-size: 13px; }
.plan-summary p { margin: 8px 0; }
.plan-summary ul, .plan-summary ol {
margin: 8px 0;
padding-left: 24px;
}
.plan-summary li { margin: 4px 0; }
.plan-summary code {
background: var(--vscode-textCodeBlock-background);
padding: 2px 6px;
border-radius: 3px;
font-family: var(--vscode-editor-font-family);
font-size: 12px;
}
.plan-summary pre {
background: var(--vscode-textCodeBlock-background);
padding: 12px;
border-radius: 4px;
overflow-x: auto;
margin: 8px 0;
}
.plan-summary pre code {
background: none;
padding: 0;
}
.plan-summary table {
border-collapse: collapse;
width: 100%;
margin: 8px 0;
font-size: 12px;
}
.plan-summary th, .plan-summary td {
border: 1px solid var(--vscode-input-border);
padding: 6px 10px;
text-align: left;
}
.plan-summary th {
background: var(--vscode-sideBar-background);
font-weight: 600;
}
.plan-summary strong { font-weight: 600; }
.plan-summary em { font-style: italic; }
.plan-steps { .plan-steps {
font-size: 13px; font-size: 13px;
} }
@ -110,15 +58,6 @@ export function getPlanCardStyles(): string {
border-radius: 4px; border-radius: 4px;
line-height: 1.5; line-height: 1.5;
} }
.plan-step strong {
color: var(--vscode-textLink-foreground);
}
.step-details {
margin-top: 4px;
font-size: 12px;
color: var(--vscode-descriptionForeground);
line-height: 1.4;
}
.plan-step:last-child { .plan-step:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
@ -211,168 +150,6 @@ export function getPlanCardStyles(): string {
.plan-actions .custom-submit:hover { .plan-actions .custom-submit:hover {
background: var(--vscode-button-hoverBackground); background: var(--vscode-button-hoverBackground);
} }
/* 阶段进度条样式 */
.phase-progress {
display: flex;
align-items: center;
padding: 12px 16px;
background: var(--vscode-sideBar-background);
border-bottom: 1px solid var(--vscode-input-border);
}
.phase-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--vscode-descriptionForeground);
}
.phase-item.current {
color: var(--vscode-textLink-foreground);
font-weight: 600;
}
.phase-item.completed {
color: #4caf50;
}
.phase-item.skipped {
color: var(--vscode-descriptionForeground);
opacity: 0.6;
}
.phase-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--vscode-input-border);
flex-shrink: 0;
}
.phase-dot.current {
background: var(--vscode-textLink-foreground);
box-shadow: 0 0 0 3px rgba(0, 122, 204, 0.2);
}
.phase-dot.completed {
background: #4caf50;
}
.phase-dot.skipped {
background: var(--vscode-descriptionForeground);
opacity: 0.5;
}
.phase-line {
flex: 1;
height: 2px;
background: var(--vscode-input-border);
margin: 0 8px;
}
.phase-line.completed {
background: #4caf50;
}
/* 阶段列表样式 */
.plan-phases {
font-size: 13px;
}
.plan-phase {
margin-bottom: 12px;
border: 1px solid var(--vscode-input-border);
border-radius: 6px;
overflow: hidden;
}
.plan-phase:last-child {
margin-bottom: 0;
}
.phase-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
background: var(--vscode-list-hoverBackground);
cursor: pointer;
user-select: none;
}
.phase-header:hover {
background: var(--vscode-list-activeSelectionBackground);
}
.phase-toggle {
font-size: 10px;
color: var(--vscode-descriptionForeground);
transition: transform 0.2s;
}
.phase-toggle.expanded {
transform: rotate(90deg);
}
.phase-name {
flex: 1;
font-weight: 500;
}
.phase-status {
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
background: var(--vscode-badge-background);
color: var(--vscode-badge-foreground);
}
.phase-status.current {
background: var(--vscode-textLink-foreground);
color: white;
}
.phase-status.skipped {
background: var(--vscode-descriptionForeground);
opacity: 0.6;
}
.phase-status.completed {
background: #4caf50;
color: white;
}
.phase-content {
padding: 0 12px;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
}
.phase-content.expanded {
padding: 12px;
max-height: 500px;
}
.phase-reason {
font-size: 12px;
color: var(--vscode-descriptionForeground);
font-style: italic;
margin-bottom: 8px;
}
.phase-steps {
margin: 0;
padding: 0;
list-style: none;
}
.phase-step-item {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 6px 0;
border-bottom: 1px solid var(--vscode-input-border);
}
.phase-step-item:last-child {
border-bottom: none;
}
.phase-step-checkbox {
width: 14px;
height: 14px;
border: 2px solid var(--vscode-textLink-foreground);
border-radius: 3px;
flex-shrink: 0;
margin-top: 2px;
}
.phase-step-text {
flex: 1;
}
.phase-step-name {
font-weight: 500;
color: var(--vscode-foreground);
}
.phase-step-desc {
font-size: 12px;
color: var(--vscode-descriptionForeground);
margin-top: 2px;
}
`; `;
} }
@ -381,200 +158,6 @@ export function getPlanCardStyles(): string {
*/ */
export function getPlanCardScript(): string { export function getPlanCardScript(): string {
return ` return `
// 简单的 Markdown 渲染函数
function renderPlanMarkdown(text) {
if (!text) return '';
let html = text;
// 转义 HTML 特殊字符(保留换行)
html = html.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
// 代码块 (\`\`\`code\`\`\`)
html = html.replace(/\\x60\\x60\\x60([\\s\\S]*?)\\x60\\x60\\x60/g, '<pre><code>$1</code></pre>');
// 行内代码 (\`code\`)
html = html.replace(/\\x60([^\\x60]+)\\x60/g, '<code>$1</code>');
// 表格处理
html = html.replace(/^\\|(.+)\\|\\s*\\n\\|[-:\\s|]+\\|\\s*\\n((?:\\|.+\\|\\s*\\n?)+)/gm, function(match, header, body) {
const headers = header.split('|').map(h => h.trim()).filter(h => h);
const rows = body.trim().split('\\n').map(row =>
row.split('|').map(cell => cell.trim()).filter(cell => cell)
);
let table = '<table><thead><tr>';
headers.forEach(h => table += '<th>' + h + '</th>');
table += '</tr></thead><tbody>';
rows.forEach(row => {
table += '<tr>';
row.forEach(cell => table += '<td>' + cell + '</td>');
table += '</tr>';
});
table += '</tbody></table>';
return table;
});
// 标题
html = html.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
// 粗体和斜体
html = html.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');
html = html.replace(/\\*(.+?)\\*/g, '<em>$1</em>');
// 无序列表
html = html.replace(/^[\\s]*[-*] (.+)$/gm, '<li>$1</li>');
html = html.replace(/(<li>.*<\\/li>\\n?)+/g, '<ul>$&</ul>');
// 有序列表
html = html.replace(/^[\\s]*\\d+\\. (.+)$/gm, '<li>$1</li>');
// 段落(连续的非空行)
html = html.replace(/^(?!<[hupolt]|$)(.+)$/gm, '<p>$1</p>');
// 清理多余的空行
html = html.replace(/<p><\\/p>/g, '');
html = html.replace(/\\n{2,}/g, '\\n');
return html;
}
// 解析并渲染步骤列表
function renderPlanSteps(steps) {
if (!steps || steps.length === 0) return '';
// 尝试解析 JSON 格式的步骤
let parsedSteps = steps;
// 如果是单个字符串且看起来像 JSON 数组,尝试解析
if (steps.length === 1 && typeof steps[0] === 'string') {
const str = steps[0].trim();
if (str.startsWith('[') && str.endsWith(']')) {
try {
parsedSteps = JSON.parse(str);
} catch (e) {
// 解析失败,保持原样
}
}
}
return parsedSteps.map((step, i) => {
// 如果是对象,格式化显示
if (typeof step === 'object' && step !== null) {
const name = step.name || step.id || ('步骤 ' + (i + 1));
const desc = step.description || '';
const inputs = step.inputs || '';
const outputs = step.outputs || '';
const logic = step.logic || '';
let content = '<strong>' + name + '</strong>';
if (desc) content += '' + desc;
let details = [];
if (inputs) details.push('输入: ' + inputs);
if (outputs) details.push('输出: ' + outputs);
if (logic) details.push('逻辑: ' + logic);
if (details.length > 0) {
content += '<div class="step-details">' + details.join(' | ') + '</div>';
}
return '<div class="plan-step"><span class="step-checkbox"></span>' + content + '</div>';
}
// 普通字符串
return '<div class="plan-step"><span class="step-checkbox"></span> ' + step + '</div>';
}).join('');
}
// 渲染阶段进度条
function renderPhaseProgress(phases) {
if (!phases || phases.length === 0) return '';
const phaseNames = { spec: 'Spec', design: 'Design', sim: 'Sim', done: 'Done' };
let html = '<div class="phase-progress">';
phases.forEach((phase, i) => {
const name = phaseNames[phase.id] || phase.name || phase.id;
const status = phase.status || 'pending';
html += \`<div class="phase-item \${status}">
<span class="phase-dot \${status}"></span>
<span>\${name}</span>
</div>\`;
// 添加连接线(最后一个不加)
if (i < phases.length - 1) {
const lineStatus = (status === 'completed' || status === 'skipped') ? 'completed' : '';
html += \`<div class="phase-line \${lineStatus}"></div>\`;
}
});
html += '</div>';
return html;
}
// 渲染阶段列表(两级结构)
function renderPlanPhases(phases) {
if (!phases || phases.length === 0) return '';
const statusLabels = {
skipped: '跳过',
completed: '已完成',
current: '当前',
pending: '待执行'
};
return phases.map((phase, i) => {
const status = phase.status || 'pending';
const statusLabel = statusLabels[status] || status;
const isExpanded = status === 'current';
const hasSteps = phase.steps && phase.steps.length > 0;
const hasReason = phase.reason && status === 'skipped';
let stepsHtml = '';
if (phase.steps && phase.steps.length > 0) {
stepsHtml = phase.steps.map(step => \`
<li class="phase-step-item">
<span class="phase-step-checkbox"></span>
<div class="phase-step-text">
<div class="phase-step-name">\${step.name || ''}</div>
\${step.description ? \`<div class="phase-step-desc">\${step.description}</div>\` : ''}
</div>
</li>
\`).join('');
}
return \`
<div class="plan-phase" data-phase-id="\${phase.id}">
<div class="phase-header" onclick="togglePhase(this)">
<span class="phase-toggle \${isExpanded ? 'expanded' : ''}">▶</span>
<span class="phase-name">\${phase.name || phase.id}</span>
<span class="phase-status \${status}">\${statusLabel}</span>
</div>
<div class="phase-content \${isExpanded ? 'expanded' : ''}">
\${hasReason ? \`<div class="phase-reason">\${phase.reason}</div>\` : ''}
\${hasSteps ? \`<ul class="phase-steps">\${stepsHtml}</ul>\` : ''}
\${!hasSteps && !hasReason ? '<div class="phase-reason">暂无步骤</div>' : ''}
</div>
</div>
\`;
}).join('');
}
// 切换阶段展开/折叠
function togglePhase(header) {
const toggle = header.querySelector('.phase-toggle');
const content = header.nextElementSibling;
toggle.classList.toggle('expanded');
content.classList.toggle('expanded');
}
// 渲染计划卡片(在 updateSegmentsRealtime 中使用) // 渲染计划卡片(在 updateSegmentsRealtime 中使用)
function renderPlanCardInSegment(segment, segmentDiv, answeredQuestions) { function renderPlanCardInSegment(segment, segmentDiv, answeredQuestions) {
segmentDiv.className += ' segment-plan'; segmentDiv.className += ' segment-plan';
@ -587,15 +170,9 @@ export function getPlanCardScript(): string {
segmentDiv.classList.add('answered'); segmentDiv.classList.add('answered');
} }
// 判断是否有 phases新格式还是 steps旧格式 const stepsHtml = (segment.planSteps || []).map((step, i) =>
const hasPhases = segment.planPhases && segment.planPhases.length > 0; \`<div class="plan-step"><span class="step-checkbox"></span> \${step}</div>\`
).join('');
// 渲染阶段进度条和阶段列表(新格式)
const progressHtml = hasPhases ? renderPhaseProgress(segment.planPhases) : '';
const phasesHtml = hasPhases ? renderPlanPhases(segment.planPhases) : '';
// 兼容旧格式:渲染步骤列表
const stepsHtml = !hasPhases ? renderPlanSteps(segment.planSteps || []) : '';
// 选项按钮 // 选项按钮
const options = ['确认执行', '修改计划', '取消']; const options = ['确认执行', '修改计划', '取消'];
@ -604,19 +181,15 @@ export function getPlanCardScript(): string {
return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`; return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`;
}).join(''); }).join('');
// 渲染 Markdown 格式的摘要
const summaryHtml = renderPlanMarkdown(segment.planSummary || '');
segmentDiv.innerHTML = \` segmentDiv.innerHTML = \`
<div class="plan-card"> <div class="plan-card">
<div class="plan-header"> <div class="plan-header">
<span class="plan-icon">${plannerIconSvg}</span> <span class="plan-icon">${plannerIconSvg}</span>
<span class="plan-title">\${segment.planTitle || '执行计划'}</span> <span class="plan-title">\${segment.planTitle || '执行计划'}</span>
</div> </div>
\${progressHtml}
<div class="plan-body"> <div class="plan-body">
<div class="plan-summary">\${summaryHtml}</div> <div class="plan-summary">\${segment.planSummary || ''}</div>
\${hasPhases ? \`<div class="plan-phases">\${phasesHtml}</div>\` : \`<div class="plan-steps">\${stepsHtml}</div>\`} <div class="plan-steps">\${stepsHtml}</div>
</div> </div>
<div class="plan-actions"> <div class="plan-actions">
<div class="question-options" data-ask-id="\${segment.askId}">\${optionsHtml}</div> <div class="question-options" data-ask-id="\${segment.askId}">\${optionsHtml}</div>
@ -646,8 +219,7 @@ export function getPlanCardScript(): string {
vscode.postMessage({ vscode.postMessage({
command: 'planAction', command: 'planAction',
action: actionMap[option] || option, action: actionMap[option] || option,
planTitle: segment.planTitle, planTitle: segment.planTitle
model: getCurrentModel()
}); });
}); });
}); });
@ -678,19 +250,9 @@ export function getPlanCardScript(): string {
// 渲染计划卡片(在 renderSegments 中使用) // 渲染计划卡片(在 renderSegments 中使用)
function renderPlanCardStatic(segment, segmentDiv) { function renderPlanCardStatic(segment, segmentDiv) {
segmentDiv.className += ' segment-plan'; segmentDiv.className += ' segment-plan';
const stepsHtml = (segment.planSteps || []).map((step, i) =>
// 判断是否有 phases新格式还是 steps旧格式 \`<div class="plan-step"><span class="step-checkbox"></span> \${step}</div>\`
const hasPhases = segment.planPhases && segment.planPhases.length > 0; ).join('');
// 渲染阶段进度条和阶段列表(新格式)
const progressHtml = hasPhases ? renderPhaseProgress(segment.planPhases) : '';
const phasesHtml = hasPhases ? renderPlanPhases(segment.planPhases) : '';
// 兼容旧格式:渲染步骤列表
const stepsHtml = !hasPhases ? renderPlanSteps(segment.planSteps || []) : '';
// 渲染 Markdown 格式的摘要
const summaryHtml = renderPlanMarkdown(segment.planSummary || '');
segmentDiv.innerHTML = \` segmentDiv.innerHTML = \`
<div class="plan-card"> <div class="plan-card">
@ -698,10 +260,9 @@ export function getPlanCardScript(): string {
<span class="plan-icon">📋</span> <span class="plan-icon">📋</span>
<span class="plan-title">\${segment.planTitle || '执行计划'}</span> <span class="plan-title">\${segment.planTitle || '执行计划'}</span>
</div> </div>
\${progressHtml}
<div class="plan-body"> <div class="plan-body">
<div class="plan-summary">\${summaryHtml}</div> <div class="plan-summary">\${segment.planSummary || ''}</div>
\${hasPhases ? \`<div class="plan-phases">\${phasesHtml}</div>\` : \`<div class="plan-steps">\${stepsHtml}</div>\`} <div class="plan-steps">\${stepsHtml}</div>
</div> </div>
<div class="plan-actions"> <div class="plan-actions">
<button class="plan-btn plan-btn-confirm" data-action="confirm">确认执行</button> <button class="plan-btn plan-btn-confirm" data-action="confirm">确认执行</button>
@ -721,8 +282,7 @@ export function getPlanCardScript(): string {
vscode.postMessage({ vscode.postMessage({
command: 'planAction', command: 'planAction',
action: action, action: action,
planTitle: segment.planTitle, planTitle: segment.planTitle
model: getCurrentModel()
}); });
}); });
}); });

View File

@ -218,8 +218,16 @@ export function getWaveformPreviewScript(): string {
* 渲染波形预览信息 * 渲染波形预览信息
*/ */
function renderWaveformInfo(containerId, vcdInfo) { function renderWaveformInfo(containerId, vcdInfo) {
console.log('[renderWaveformInfo] 开始渲染波形, containerId:', containerId);
console.log('[renderWaveformInfo] vcdInfo:', vcdInfo);
const container = document.getElementById(containerId); const container = document.getElementById(containerId);
if (!container) return; if (!container) {
console.error('[renderWaveformInfo] 找不到容器:', containerId);
return;
}
console.log('[renderWaveformInfo] 找到容器,信号数量:', vcdInfo.signals?.length || 0);
// 清空容器 // 清空容器
container.innerHTML = ''; container.innerHTML = '';
@ -229,6 +237,7 @@ export function getWaveformPreviewScript(): string {
waveformSvg.innerHTML = drawRealWaveform(vcdInfo.signals || []); waveformSvg.innerHTML = drawRealWaveform(vcdInfo.signals || []);
container.appendChild(waveformSvg); container.appendChild(waveformSvg);
console.log('[renderWaveformInfo] 波形渲染完成');
} }
/** /**

View File

@ -632,6 +632,21 @@ export function getWebviewContent(
} }
break; break;
case 'checkPanelWidth':
// 检查面板宽度
const minWidth = message.minWidth || 200;
const currentWidth = document.body.clientWidth;
console.log('[WebView] 检查面板宽度:', currentWidth, '最小宽度:', minWidth);
if (currentWidth < minWidth) {
// 宽度不足,通知后端关闭面板
vscode.postMessage({
command: 'panelWidthInsufficient',
currentWidth: currentWidth,
minWidth: minWidth
});
}
break;
case 'vcdInfo': case 'vcdInfo':
// 渲染迷你波形预览信息 // 渲染迷你波形预览信息
try { try {
@ -742,6 +757,35 @@ export function getWebviewContent(
} }
}); });
// 监听窗口大小变化,检查面板宽度
let resizeTimer;
const MIN_PANEL_WIDTH = 500;
function checkPanelWidth() {
const currentWidth = document.body.clientWidth;
if (currentWidth < MIN_PANEL_WIDTH) {
console.log('[WebView] 面板宽度不足:', currentWidth, 'px最小要求:', MIN_PANEL_WIDTH, 'px');
vscode.postMessage({
command: 'panelWidthInsufficient',
currentWidth: currentWidth,
minWidth: MIN_PANEL_WIDTH
});
}
}
window.addEventListener('resize', () => {
// 使用防抖,避免频繁检查
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
checkPanelWidth();
}, 300);
});
// 初始加载时也检查一次
setTimeout(() => {
checkPanelWidth();
}, 500);
${getMessageAreaScript()} ${getMessageAreaScript()}
${getAgentCardScript()} ${getAgentCardScript()}
${getWaveformPreviewScript()} ${getWaveformPreviewScript()}