Compare commits
9 Commits
4a7af49fea
...
feat/detai
| Author | SHA1 | Date | |
|---|---|---|---|
| e61122449d | |||
| 4687c3faa6 | |||
| feff8ea4d3 | |||
| 6abec8c7b7 | |||
| f9b3699bda | |||
| 8da1177bf3 | |||
| a85a044a9b | |||
| c58e3603de | |||
| 940584e1ea |
BIN
src/assets/titleIcon/PRO+.png
Normal file
BIN
src/assets/titleIcon/PRO+.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
src/assets/titleIcon/PRO-Try.png
Normal file
BIN
src/assets/titleIcon/PRO-Try.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 302 KiB |
BIN
src/assets/titleIcon/PRO.png
Normal file
BIN
src/assets/titleIcon/PRO.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
src/assets/titleIcon/free.png
Normal file
BIN
src/assets/titleIcon/free.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
@ -8,7 +8,7 @@ import * as vscode from "vscode";
|
|||||||
type Environment = "dev" | "test" | "prod";
|
type Environment = "dev" | "test" | "prod";
|
||||||
|
|
||||||
/** 当前环境 - 修改这里切换环境 */
|
/** 当前环境 - 修改这里切换环境 */
|
||||||
const CURRENT_ENV: Environment = "dev";
|
const CURRENT_ENV: Environment = "test";
|
||||||
|
|
||||||
/** 服务等级类型 */
|
/** 服务等级类型 */
|
||||||
export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
|
export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
|
||||||
@ -17,8 +17,8 @@ export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
|
|||||||
export interface IccoderConfig {
|
export interface IccoderConfig {
|
||||||
/** 后端服务地址 */
|
/** 后端服务地址 */
|
||||||
backendUrl: string;
|
backendUrl: string;
|
||||||
/** 登录页面地址 */
|
/** 后端服务地址(strangeLoop) */
|
||||||
loginUrl: string;
|
backendUrlStrongeLoop: string;
|
||||||
/** 请求超时时间(毫秒) */
|
/** 请求超时时间(毫秒) */
|
||||||
timeout: number;
|
timeout: number;
|
||||||
/** 用户ID(临时使用,后续对接认证) */
|
/** 用户ID(临时使用,后续对接认证) */
|
||||||
@ -32,7 +32,7 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
|||||||
/** 本地开发环境 */
|
/** 本地开发环境 */
|
||||||
dev: {
|
dev: {
|
||||||
backendUrl: "http://localhost:2233",
|
backendUrl: "http://localhost:2233",
|
||||||
loginUrl: "http://localhost/login",
|
backendUrlStrongeLoop: "http://192.168.1.108:2029",
|
||||||
timeout: 300000,
|
timeout: 300000,
|
||||||
userId: "default-user",
|
userId: "default-user",
|
||||||
serviceTier: "max", // 默认使用 max
|
serviceTier: "max", // 默认使用 max
|
||||||
@ -40,7 +40,7 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
|||||||
/** 测试服务器环境 */
|
/** 测试服务器环境 */
|
||||||
test: {
|
test: {
|
||||||
backendUrl: "http://192.168.1.108:2233",
|
backendUrl: "http://192.168.1.108:2233",
|
||||||
loginUrl: "http://192.168.1.108:2005/login",
|
backendUrlStrongeLoop: "http://192.168.1.108:2029",
|
||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
userId: "default-user",
|
userId: "default-user",
|
||||||
serviceTier: "max",
|
serviceTier: "max",
|
||||||
@ -48,7 +48,7 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
|||||||
/** 生产环境 */
|
/** 生产环境 */
|
||||||
prod: {
|
prod: {
|
||||||
backendUrl: "https://api.iccoder.com",
|
backendUrl: "https://api.iccoder.com",
|
||||||
loginUrl: "https://iccoder.com/login",
|
backendUrlStrongeLoop: "http://api.iccoder.com:2029",
|
||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
userId: "default-user",
|
userId: "default-user",
|
||||||
serviceTier: "auto",
|
serviceTier: "auto",
|
||||||
@ -80,3 +80,15 @@ export function getApiUrl(path: string): string {
|
|||||||
const apiPath = path.startsWith("/") ? path : `/${path}`;
|
const apiPath = path.startsWith("/") ? path : `/${path}`;
|
||||||
return `${baseUrl}${apiPath}`;
|
return `${baseUrl}${apiPath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 StrangeLoop 服务 API 地址(用于用户信息等)
|
||||||
|
*/
|
||||||
|
export function getStrangeLoopApiUrl(path: string): string {
|
||||||
|
const { backendUrlStrongeLoop } = getConfig();
|
||||||
|
const baseUrl = backendUrlStrongeLoop.endsWith("/")
|
||||||
|
? backendUrlStrongeLoop.slice(0, -1)
|
||||||
|
: backendUrlStrongeLoop;
|
||||||
|
const apiPath = path.startsWith("/") ? path : `/${path}`;
|
||||||
|
return `${baseUrl}${apiPath}`;
|
||||||
|
}
|
||||||
|
|||||||
@ -175,3 +175,8 @@ export const stateTransitionIconSvg = `
|
|||||||
* 用户提问图标 SVG
|
* 用户提问图标 SVG
|
||||||
*/
|
*/
|
||||||
export const userQuestionIconSvg = `<svg t="1767869230062" class="icon" viewBox="0 0 1068 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4819" width="14" height="14"><path d="M563.645217 578.782609c2.537739-35.350261 6.322087-58.189913 11.397566-68.518957 7.568696-15.449043 24.175304-34.370783 49.775304-56.631652 35.172174-30.72 58.546087-53.960348 70.121739-69.810087 11.575652-15.805217 17.408-36.418783 17.408-61.885217 0-41.939478-15.805217-76.399304-47.37113-103.379479-31.610435-26.980174-73.638957-40.470261-126.130087-40.47026-56.765217 0-101.376 15.760696-133.921392 47.282086C372.424348 256.934957 356.173913 298.562783 356.173913 350.386087h71.145739c1.335652-31.165217 6.811826-55.02887 16.384-71.590957 17.051826-29.740522 47.86087-44.610783 92.338087-44.610782 35.973565 0 61.796174 8.637217 77.378783 25.911652 15.582609 17.274435 23.373913 37.665391 23.373913 61.128348 0 16.784696-5.342609 32.990609-16.027826 48.573217-5.787826 8.904348-13.534609 17.363478-23.151305 25.555478l-31.966608 28.40487c-30.675478 27.113739-50.487652 51.155478-59.570087 72.125217-6.054957 13.979826-10.551652 41.627826-13.579131 82.899479h71.145739z m15.137392 89.043478a44.521739 44.521739 0 1 0-89.043479 0 44.521739 44.521739 0 0 0 89.043479 0z" fill="#8a8a8a" p-id="4820"></path><path d="M934.912 0h-801.391304a133.565217 133.565217 0 0 0-133.565218 133.565217v623.304348l0.222609 7.835826A133.565217 133.565217 0 0 0 133.565217 890.434783h222.608696v89.043478a44.521739 44.521739 0 0 0 64.556522 39.713391L675.661913 890.434783h259.294609a133.565217 133.565217 0 0 0 133.565217-133.565218V133.565217a133.565217 133.565217 0 0 0-133.565217-133.565217z m-801.391304 89.043478h801.391304a44.521739 44.521739 0 0 1 44.521739 44.521739v623.304348a44.521739 44.521739 0 0 1-44.521739 44.521739h-269.801739a44.521739 44.521739 0 0 0-20.034783 4.763826l-199.902608 100.930783V845.913043a44.521739 44.521739 0 0 0-44.52174-44.521739h-267.130434a44.521739 44.521739 0 0 1-44.521739-44.521739V133.565217a44.521739 44.521739 0 0 1 44.521739-44.521739z" fill="#8a8a8a" p-id="4821"></path></svg>`;
|
export const userQuestionIconSvg = `<svg t="1767869230062" class="icon" viewBox="0 0 1068 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4819" width="14" height="14"><path d="M563.645217 578.782609c2.537739-35.350261 6.322087-58.189913 11.397566-68.518957 7.568696-15.449043 24.175304-34.370783 49.775304-56.631652 35.172174-30.72 58.546087-53.960348 70.121739-69.810087 11.575652-15.805217 17.408-36.418783 17.408-61.885217 0-41.939478-15.805217-76.399304-47.37113-103.379479-31.610435-26.980174-73.638957-40.470261-126.130087-40.47026-56.765217 0-101.376 15.760696-133.921392 47.282086C372.424348 256.934957 356.173913 298.562783 356.173913 350.386087h71.145739c1.335652-31.165217 6.811826-55.02887 16.384-71.590957 17.051826-29.740522 47.86087-44.610783 92.338087-44.610782 35.973565 0 61.796174 8.637217 77.378783 25.911652 15.582609 17.274435 23.373913 37.665391 23.373913 61.128348 0 16.784696-5.342609 32.990609-16.027826 48.573217-5.787826 8.904348-13.534609 17.363478-23.151305 25.555478l-31.966608 28.40487c-30.675478 27.113739-50.487652 51.155478-59.570087 72.125217-6.054957 13.979826-10.551652 41.627826-13.579131 82.899479h71.145739z m15.137392 89.043478a44.521739 44.521739 0 1 0-89.043479 0 44.521739 44.521739 0 0 0 89.043479 0z" fill="#8a8a8a" p-id="4820"></path><path d="M934.912 0h-801.391304a133.565217 133.565217 0 0 0-133.565218 133.565217v623.304348l0.222609 7.835826A133.565217 133.565217 0 0 0 133.565217 890.434783h222.608696v89.043478a44.521739 44.521739 0 0 0 64.556522 39.713391L675.661913 890.434783h259.294609a133.565217 133.565217 0 0 0 133.565217-133.565218V133.565217a133.565217 133.565217 0 0 0-133.565217-133.565217z m-801.391304 89.043478h801.391304a44.521739 44.521739 0 0 1 44.521739 44.521739v623.304348a44.521739 44.521739 0 0 1-44.521739 44.521739h-269.801739a44.521739 44.521739 0 0 0-20.034783 4.763826l-199.902608 100.930783V845.913043a44.521739 44.521739 0 0 0-44.52174-44.521739h-267.130434a44.521739 44.521739 0 0 1-44.521739-44.521739V133.565217a44.521739 44.521739 0 0 1 44.521739-44.521739z" fill="#8a8a8a" p-id="4821"></path></svg>`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户头像图标 SVG
|
||||||
|
*/
|
||||||
|
export const userAvatarIconSvg = `<svg t="1767947405083" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4661" width="16" height="16"><path d="M515.541449 7.082899c-280.359429 0-508.458551 228.120391-508.458551 508.458551s228.120391 508.458551 508.458551 508.458551 508.458551-228.120391 508.458551-508.458551S795.900879 7.082899 515.541449 7.082899zM515.541449 981.864196c-257.132626 0-466.301477-209.190121-466.301477-466.322747 0-257.132626 209.168851-466.322747 466.301477-466.322747s466.301477 209.190121 466.301477 466.322747S772.674075 981.864196 515.541449 981.864196zM614.574414 524.177056 614.574414 524.177056c47.751075-31.96876 79.230625-86.398604 79.230625-148.187857 0-98.437405-79.804915-178.24232-178.24232-178.24232-98.437405 0-178.24232 79.804915-178.24232 178.24232 0 61.810523 31.479551 116.219097 79.251895 148.187857-100.266622 39.519598-171.244501 137.170014-171.244501 251.453545 0 0.23397 0 0.446669 0.02127 0.659369 0 0.04254-0.02127 0.10635-0.02127 0.14889 0 15.612155 12.65563 28.246516 28.267786 28.246516 15.590885 0 21.886796-12.63436 21.886796-28.246516 0-0.340319-0.08508-0.659369-0.10635-1.020958 0.10635-118.005774 102.159649-219.995264 220.207964-219.995264 118.112124 0 220.207964 102.095839 220.207964 220.207964 0 0.14889-1.467628 29.054774 21.971875 29.054774 15.505806 0 28.076356-12.57055 28.076356-28.055086 0-0.06381-0.02127-0.12762-0.02127-0.2127 0-0.25524 0.02127-0.510479 0.02127-0.786989C785.797645 661.34707 714.798496 563.696654 614.574414 524.177056zM515.541449 510.734437c-74.402343 0-134.723968-60.321625-134.723968-134.723968 0-74.423613 60.321625-134.723968 134.723968-134.723968 74.423613 0 134.723968 60.321625 134.723968 134.723968S589.943792 510.734437 515.541449 510.734437z" fill="currentColor" p-id="4662"></path></svg>`;
|
||||||
|
|||||||
@ -5,12 +5,16 @@ import { VCDViewerPanel, VCDViewerEditorProvider } from "./panels/VCDViewerPanel
|
|||||||
import { ChatHistoryManager } from "./utils/chatHistoryManager";
|
import { ChatHistoryManager } from "./utils/chatHistoryManager";
|
||||||
import { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider";
|
import { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider";
|
||||||
import { VCDFileServer } from "./services/vcdFileServer";
|
import { VCDFileServer } from "./services/vcdFileServer";
|
||||||
|
import { initUserService } from "./services/userService";
|
||||||
|
|
||||||
export function activate(context: vscode.ExtensionContext) {
|
export function activate(context: vscode.ExtensionContext) {
|
||||||
console.log("🎉 IC Coder 插件已激活!");
|
console.log("🎉 IC Coder 插件已激活!");
|
||||||
|
|
||||||
|
// 初始化用户服务
|
||||||
|
initUserService(context);
|
||||||
|
|
||||||
// 初始化 VCD 文件服务器
|
// 初始化 VCD 文件服务器
|
||||||
const vcdFileServer = new VCDFileServer();
|
const vcdFileServer = new VCDFileServer(context.extensionUri);
|
||||||
vcdFileServer.start().then((port) => {
|
vcdFileServer.start().then((port) => {
|
||||||
console.log(`VCD 文件服务器已启动,端口: ${port}`);
|
console.log(`VCD 文件服务器已启动,端口: ${port}`);
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
@ -86,6 +90,39 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 注册命令:在浏览器中打开 VCD 波形查看器
|
||||||
|
const openVCDViewerInBrowserCommand = vscode.commands.registerCommand(
|
||||||
|
"ic-coder.openVCDViewerInBrowser",
|
||||||
|
async (vcdFilePath?: string) => {
|
||||||
|
if (!vcdFilePath) {
|
||||||
|
const fileUri = await vscode.window.showOpenDialog({
|
||||||
|
canSelectFiles: true,
|
||||||
|
canSelectFolders: false,
|
||||||
|
canSelectMany: false,
|
||||||
|
filters: {
|
||||||
|
"VCD 文件": ["vcd"],
|
||||||
|
"所有文件": ["*"],
|
||||||
|
},
|
||||||
|
title: "选择 VCD 文件",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fileUri && fileUri[0]) {
|
||||||
|
vcdFilePath = fileUri[0].fsPath;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册文件到服务器
|
||||||
|
const fileId = vcdFileServer.registerFile(vcdFilePath);
|
||||||
|
const viewerUrl = vcdFileServer.getViewerUrl(fileId);
|
||||||
|
|
||||||
|
// 在默认浏览器中打开
|
||||||
|
vscode.env.openExternal(vscode.Uri.parse(viewerUrl));
|
||||||
|
vscode.window.showInformationMessage(`波形查看器已在浏览器中打开`);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// 注册命令:用户登录
|
// 注册命令:用户登录
|
||||||
const loginCommand = vscode.commands.registerCommand(
|
const loginCommand = vscode.commands.registerCommand(
|
||||||
"ic-coder.login",
|
"ic-coder.login",
|
||||||
@ -182,6 +219,7 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
openPanelCommand,
|
openPanelCommand,
|
||||||
openChatCommand,
|
openChatCommand,
|
||||||
openVCDViewerCommand,
|
openVCDViewerCommand,
|
||||||
|
openVCDViewerInBrowserCommand,
|
||||||
loginCommand,
|
loginCommand,
|
||||||
logoutCommand,
|
logoutCommand,
|
||||||
// TODO: 等待重新实现这些命令
|
// TODO: 等待重新实现这些命令
|
||||||
|
|||||||
@ -18,6 +18,44 @@ import { compactDialog } from "../services/apiClient";
|
|||||||
import { VCDViewerPanel } from "./VCDViewerPanel";
|
import { VCDViewerPanel } from "./VCDViewerPanel";
|
||||||
import { ChatHistoryManager } from "../utils/chatHistoryManager";
|
import { ChatHistoryManager } from "../utils/chatHistoryManager";
|
||||||
import { MessageType } from "../types/chatHistory";
|
import { MessageType } from "../types/chatHistory";
|
||||||
|
import { getCachedUserInfo } from "../services/userService";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会员等级图标 URI
|
||||||
|
*/
|
||||||
|
function getTierIconUri(
|
||||||
|
webview: vscode.Webview,
|
||||||
|
context: vscode.ExtensionContext,
|
||||||
|
tierCode?: string
|
||||||
|
): string | undefined {
|
||||||
|
if (!tierCode) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tierIconMap: Record<string, string> = {
|
||||||
|
BASIC: "free.png",
|
||||||
|
TRIAL: "PRO-Try.png",
|
||||||
|
ADVANCED: "PRO.png",
|
||||||
|
PROFESSIONAL: "PRO+.png",
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconFile = tierIconMap[tierCode];
|
||||||
|
if (!iconFile) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconUri = webview.asWebviewUri(
|
||||||
|
vscode.Uri.joinPath(
|
||||||
|
context.extensionUri,
|
||||||
|
"src",
|
||||||
|
"assets",
|
||||||
|
"titleIcon",
|
||||||
|
iconFile
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return iconUri.toString();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建并显示 IC 助手面板
|
* 创建并显示 IC 助手面板
|
||||||
@ -62,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"),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -87,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内容
|
||||||
@ -108,6 +170,52 @@ export async function showICHelperPanel(
|
|||||||
maxIconUri.toString()
|
maxIconUri.toString()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 获取并发送用户信息到 webview
|
||||||
|
try {
|
||||||
|
// 优先使用缓存的用户信息
|
||||||
|
let userInfo = getCachedUserInfo();
|
||||||
|
|
||||||
|
if (userInfo) {
|
||||||
|
// 使用缓存的用户信息
|
||||||
|
console.log("[ICHelperPanel] 使用缓存的用户信息:", userInfo);
|
||||||
|
const tierIconUrl = getTierIconUri(
|
||||||
|
panel.webview,
|
||||||
|
context,
|
||||||
|
userInfo.membership?.tierCode
|
||||||
|
);
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "updateUserInfo",
|
||||||
|
userInfo: {
|
||||||
|
userId: userInfo.userId,
|
||||||
|
nickname: userInfo.nickname,
|
||||||
|
username: userInfo.username,
|
||||||
|
},
|
||||||
|
tierIconUrl: tierIconUrl,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 如果没有缓存,从 session 中获取
|
||||||
|
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||||
|
createIfNone: false,
|
||||||
|
});
|
||||||
|
if (session) {
|
||||||
|
console.log(
|
||||||
|
"[ICHelperPanel] 从 session 获取用户信息, account:",
|
||||||
|
session.account
|
||||||
|
);
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "updateUserInfo",
|
||||||
|
userInfo: {
|
||||||
|
userId: session.account.id,
|
||||||
|
nickname: session.account.label,
|
||||||
|
username: session.account.label,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[ICHelperPanel] 获取用户信息失败:", error);
|
||||||
|
}
|
||||||
|
|
||||||
// 处理消息
|
// 处理消息
|
||||||
panel.webview.onDidReceiveMessage(
|
panel.webview.onDidReceiveMessage(
|
||||||
async (message) => {
|
async (message) => {
|
||||||
@ -142,14 +250,14 @@ export async function showICHelperPanel(
|
|||||||
historyManager.switchToPanelTask(panelId);
|
historyManager.switchToPanelTask(panelId);
|
||||||
|
|
||||||
// 显示进度条
|
// 显示进度条
|
||||||
panel.webview.postMessage({ type: 'showProgress' });
|
panel.webview.postMessage({ type: "showProgress" });
|
||||||
|
|
||||||
handleUserMessage(
|
handleUserMessage(
|
||||||
panel,
|
panel,
|
||||||
message.text,
|
message.text,
|
||||||
context.extensionPath,
|
context.extensionPath,
|
||||||
message.mode,
|
message.mode,
|
||||||
message.model // 传递服务等级
|
message.model // 传递服务等级
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "readFile":
|
case "readFile":
|
||||||
@ -176,10 +284,12 @@ export async function showICHelperPanel(
|
|||||||
vscode.window.showInformationMessage(message.text);
|
vscode.window.showInformationMessage(message.text);
|
||||||
break;
|
break;
|
||||||
case "openWaveformViewer":
|
case "openWaveformViewer":
|
||||||
// 打开波形查看器 - 使用 vscode.open 触发自定义编辑器
|
// 在新列中打开波形查看器
|
||||||
if (message.vcdFilePath) {
|
if (message.vcdFilePath) {
|
||||||
const vcdUri = vscode.Uri.file(message.vcdFilePath);
|
vscode.commands.executeCommand(
|
||||||
vscode.commands.executeCommand('vscode.open', vcdUri);
|
"ic-coder.openVCDViewer",
|
||||||
|
message.vcdFilePath
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "getVCDInfo":
|
case "getVCDInfo":
|
||||||
@ -322,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 });
|
||||||
@ -351,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) {
|
||||||
@ -371,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) {
|
||||||
@ -408,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,
|
||||||
@ -438,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,
|
||||||
@ -478,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({
|
||||||
@ -567,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] });
|
||||||
@ -578,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] });
|
||||||
@ -591,12 +741,15 @@ function parseVCDSignals(content: string, maxSignals: number = 3) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
signals.push({
|
// 只添加有值变化数据的信号
|
||||||
name: signalDef.name,
|
if (values.length > 0) {
|
||||||
identifier: signalDef.identifier,
|
signals.push({
|
||||||
width: signalDef.width,
|
name: signalDef.name,
|
||||||
values: values,
|
identifier: signalDef.identifier,
|
||||||
});
|
width: signalDef.width,
|
||||||
|
values: values,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("解析 VCD 信号数据失败:", error);
|
console.error("解析 VCD 信号数据失败:", error);
|
||||||
|
|||||||
@ -107,7 +107,8 @@ export class VCDViewerPanel {
|
|||||||
* 创建或显示 VCD 查看器面板
|
* 创建或显示 VCD 查看器面板
|
||||||
*/
|
*/
|
||||||
public static createOrShow(extensionUri: vscode.Uri, vcdFilePath?: string, vcdFileServer?: VCDFileServer) {
|
public static createOrShow(extensionUri: vscode.Uri, vcdFilePath?: string, vcdFileServer?: VCDFileServer) {
|
||||||
const column = vscode.ViewColumn.One;
|
// 在当前活动编辑器旁边打开新列
|
||||||
|
const column = vscode.ViewColumn.Beside;
|
||||||
|
|
||||||
// 如果已经有面板打开,则显示它
|
// 如果已经有面板打开,则显示它
|
||||||
if (VCDViewerPanel.currentPanel) {
|
if (VCDViewerPanel.currentPanel) {
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import * as https from 'https';
|
|||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
import { getApiUrl, getConfig } from '../config/settings';
|
import { getApiUrl, getConfig } from '../config/settings';
|
||||||
import type { ToolCallResult, AnswerRequest, ToolResultResponse, AnswerResponse, ToolConfirmResponse } from '../types/api';
|
import type { ToolCallResult, AnswerRequest, ToolResultResponse, AnswerResponse, ToolConfirmResponse, UserInfoResponse } from '../types/api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP 请求选项
|
* HTTP 请求选项
|
||||||
@ -213,3 +213,14 @@ export function createSystemErrorResult(id: number, code: number, message: strin
|
|||||||
error: { code, message }
|
error: { code, message }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
* GET /system/user/getInfo
|
||||||
|
*/
|
||||||
|
export async function getUserInfo(): Promise<UserInfoResponse> {
|
||||||
|
console.log('[API] 获取用户信息');
|
||||||
|
return request<UserInfoResponse>('/system/user/getInfo', {
|
||||||
|
method: 'GET'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -12,13 +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';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息段落类型
|
* 消息段落类型
|
||||||
*/
|
*/
|
||||||
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';
|
||||||
@ -33,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[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -66,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;
|
||||||
/** 实时更新段落(流式过程中) */
|
/** 实时更新段落(流式过程中) */
|
||||||
@ -79,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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -92,7 +86,6 @@ 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;
|
||||||
|
|
||||||
@ -332,34 +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;
|
||||||
|
|
||||||
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,7 +343,7 @@ export class DialogSession {
|
|||||||
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,
|
||||||
@ -537,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
|
||||||
});
|
});
|
||||||
@ -563,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) => {
|
||||||
@ -688,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 消息(用于后端重启后恢复)
|
||||||
@ -787,38 +654,12 @@ export class DialogSession {
|
|||||||
callbacks.onContextUsage?.(data);
|
callbacks.onContextUsage?.(data);
|
||||||
},
|
},
|
||||||
|
|
||||||
onCreditUpdate: (data) => {
|
|
||||||
console.log('[DialogSession] onCreditUpdate: 扣除', data.deductedCredits, '剩余', 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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import * as vscode from "vscode";
|
|||||||
import * as http from "http";
|
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 { getConfig } from "../config/settings";
|
import { onTokenReceived, type UserInfo, clearUserInfo } from "./userService";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IC Coder Authentication Provider
|
* IC Coder Authentication Provider
|
||||||
@ -13,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;
|
||||||
|
|
||||||
@ -62,13 +63,16 @@ export class ICCoderAuthenticationProvider
|
|||||||
try {
|
try {
|
||||||
const token = await this.login();
|
const token = await this.login();
|
||||||
|
|
||||||
|
// 获取到 token 后立即调用用户信息接口
|
||||||
|
const userInfo = await onTokenReceived(token);
|
||||||
|
|
||||||
// 创建会话
|
// 创建会话
|
||||||
const session: vscode.AuthenticationSession = {
|
const session: vscode.AuthenticationSession = {
|
||||||
id: this.generateSessionId(),
|
id: this.generateSessionId(),
|
||||||
accessToken: token,
|
accessToken: token,
|
||||||
account: {
|
account: {
|
||||||
id: "iccoder-user",
|
id: userInfo?.userId || "iccoder-user",
|
||||||
label: "IC Coder 用户",
|
label: userInfo?.nickname || userInfo?.username || "IC Coder 用户",
|
||||||
},
|
},
|
||||||
scopes: [...scopes],
|
scopes: [...scopes],
|
||||||
};
|
};
|
||||||
@ -109,6 +113,9 @@ export class ICCoderAuthenticationProvider
|
|||||||
this._sessions.splice(sessionIndex, 1);
|
this._sessions.splice(sessionIndex, 1);
|
||||||
await this.saveSessions();
|
await this.saveSessions();
|
||||||
|
|
||||||
|
// 清除用户信息缓存
|
||||||
|
await clearUserInfo();
|
||||||
|
|
||||||
// 触发会话变化事件
|
// 触发会话变化事件
|
||||||
this._onDidChangeSessions.fire({
|
this._onDidChangeSessions.fire({
|
||||||
added: [],
|
added: [],
|
||||||
@ -149,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);
|
||||||
|
|||||||
@ -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 会在收到数据时自动重置计时器
|
||||||
|
|||||||
@ -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 {
|
||||||
@ -285,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) {
|
||||||
@ -326,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 文件并返回波形摘要
|
||||||
|
|||||||
345
src/services/userService.ts
Normal file
345
src/services/userService.ts
Normal file
@ -0,0 +1,345 @@
|
|||||||
|
/**
|
||||||
|
* 用户服务
|
||||||
|
* 管理用户信息和认证相关的 API 调用
|
||||||
|
*/
|
||||||
|
import * as https from 'https';
|
||||||
|
import * as http from 'http';
|
||||||
|
import { URL } from 'url';
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import { getStrangeLoopApiUrl, getConfig } from '../config/settings';
|
||||||
|
import type { UserInfoResponse, MembershipResponse, MultiMembershipVO, MembershipItemVO } from '../types/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP 请求选项
|
||||||
|
*/
|
||||||
|
interface RequestOptions {
|
||||||
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
body?: unknown;
|
||||||
|
timeout?: number;
|
||||||
|
token?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送 HTTP 请求(带 token)
|
||||||
|
*/
|
||||||
|
async function request<T>(path: string, options: RequestOptions): Promise<T> {
|
||||||
|
const url = new URL(getStrangeLoopApiUrl(path));
|
||||||
|
const { timeout } = getConfig();
|
||||||
|
|
||||||
|
const isHttps = url.protocol === 'https:';
|
||||||
|
const httpModule = isHttps ? https : http;
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果有 token,添加到请求头
|
||||||
|
if (options.token) {
|
||||||
|
headers['Authorization'] = `Bearer ${options.token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestOptions: http.RequestOptions = {
|
||||||
|
hostname: url.hostname,
|
||||||
|
port: url.port || (isHttps ? 443 : 80),
|
||||||
|
path: url.pathname + url.search,
|
||||||
|
method: options.method,
|
||||||
|
headers,
|
||||||
|
timeout: options.timeout || timeout
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const req = httpModule.request(requestOptions, (res) => {
|
||||||
|
let data = '';
|
||||||
|
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('end', () => {
|
||||||
|
console.log(`[HTTP] 响应状态码: ${res.statusCode}`);
|
||||||
|
console.log(`[HTTP] 响应内容: ${data}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(data);
|
||||||
|
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
|
resolve(json as T);
|
||||||
|
} else {
|
||||||
|
reject(new Error(json.error || json.message || json.msg || `HTTP ${res.statusCode}`));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 如果不是 JSON,直接返回原始内容
|
||||||
|
reject(new Error(`解析响应失败 (${res.statusCode}): ${data}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('timeout', () => {
|
||||||
|
req.destroy();
|
||||||
|
reject(new Error('请求超时'));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.body) {
|
||||||
|
req.write(JSON.stringify(options.body));
|
||||||
|
}
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户信息数据结构(实际返回的数据)
|
||||||
|
*/
|
||||||
|
export interface UserInfo {
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
nickname: string;
|
||||||
|
email?: string;
|
||||||
|
phonenumber?: string;
|
||||||
|
avatar?: string;
|
||||||
|
roles?: string[];
|
||||||
|
permissions?: string[];
|
||||||
|
createTime?: string;
|
||||||
|
loginDate?: string;
|
||||||
|
// 会员信息
|
||||||
|
membership?: {
|
||||||
|
tierCode: string;
|
||||||
|
tierName: string;
|
||||||
|
tierLevel: number;
|
||||||
|
remainingDays?: number;
|
||||||
|
monthlyCredits?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
* GET /system/user/getInfo
|
||||||
|
*/
|
||||||
|
export async function getUserInfo(token: string): Promise<UserInfo | null> {
|
||||||
|
const apiPath = '/system/user/getInfo';
|
||||||
|
const fullUrl = getStrangeLoopApiUrl(apiPath);
|
||||||
|
console.log('[UserService] 获取用户信息');
|
||||||
|
console.log('[UserService] 请求地址:', fullUrl);
|
||||||
|
console.log('[UserService] Token:', token ? '已提供' : '未提供');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await request<UserInfoResponse>(apiPath, {
|
||||||
|
method: 'GET',
|
||||||
|
token
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理响应数据 - 检查 code 是否为 200
|
||||||
|
if (response.code === 200 && response.user) {
|
||||||
|
const user = response.user;
|
||||||
|
return {
|
||||||
|
userId: String(user.userId),
|
||||||
|
username: user.userName,
|
||||||
|
nickname: user.nickName,
|
||||||
|
email: user.email,
|
||||||
|
phonenumber: user.phonenumber,
|
||||||
|
avatar: user.avatar,
|
||||||
|
roles: response.roles,
|
||||||
|
permissions: response.permissions,
|
||||||
|
createTime: user.createTime,
|
||||||
|
loginDate: user.loginDate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('[UserService] 获取用户信息失败:', response);
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[UserService] 请求失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户会员信息
|
||||||
|
* GET /strangeloop/api/membership/current
|
||||||
|
*/
|
||||||
|
export async function getMembershipInfo(token: string): Promise<MultiMembershipVO | null> {
|
||||||
|
const apiPath = '/strangeloop/api/membership/current';
|
||||||
|
const fullUrl = getStrangeLoopApiUrl(apiPath);
|
||||||
|
console.log('[UserService] 获取会员信息');
|
||||||
|
console.log('[UserService] 请求地址:', fullUrl);
|
||||||
|
console.log('[UserService] Token:', token ? '已提供' : '未提供');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await request<MembershipResponse>(apiPath, {
|
||||||
|
method: 'GET',
|
||||||
|
token
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理响应数据 - 检查 code 是否为 200
|
||||||
|
if (response.code === 200 && response.data) {
|
||||||
|
console.log('[UserService] 会员信息获取成功:', response.data);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('[UserService] 获取会员信息失败:', response);
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[UserService] 请求会员信息失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员等级映射
|
||||||
|
*/
|
||||||
|
const TIER_LEVEL_MAP: Record<string, number> = {
|
||||||
|
'BASIC': 1,
|
||||||
|
'TRIAL': 2,
|
||||||
|
'ADVANCED': 3,
|
||||||
|
'PROFESSIONAL': 4
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最高等级的会员信息
|
||||||
|
*/
|
||||||
|
function getHighestTierMembership(allMemberships?: MembershipItemVO[]): MembershipItemVO | null {
|
||||||
|
if (!allMemberships || allMemberships.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按等级排序,获取最高等级
|
||||||
|
return allMemberships.reduce((highest, current) => {
|
||||||
|
const currentLevel = TIER_LEVEL_MAP[current.tierCode] || 0;
|
||||||
|
const highestLevel = TIER_LEVEL_MAP[highest.tierCode] || 0;
|
||||||
|
return currentLevel > highestLevel ? current : highest;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当获取到 token 时自动调用此函数
|
||||||
|
* 用于在登录成功后立即获取用户信息
|
||||||
|
*/
|
||||||
|
export async function onTokenReceived(token: string): Promise<UserInfo | null> {
|
||||||
|
try {
|
||||||
|
console.log('[UserService] Token 已获取,正在获取用户信息和会员信息...');
|
||||||
|
|
||||||
|
// 并行获取用户信息和会员信息
|
||||||
|
const [userInfo, membershipInfo] = await Promise.all([
|
||||||
|
getUserInfo(token),
|
||||||
|
getMembershipInfo(token)
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!userInfo) {
|
||||||
|
console.warn('[UserService] 未能获取到用户信息');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打印用户信息到控制台
|
||||||
|
console.log('='.repeat(60));
|
||||||
|
console.log('用户信息详情:');
|
||||||
|
console.log('='.repeat(60));
|
||||||
|
console.log(`用户ID: ${userInfo.userId}`);
|
||||||
|
console.log(`用户名: ${userInfo.username}`);
|
||||||
|
console.log(`昵称: ${userInfo.nickname}`);
|
||||||
|
if (userInfo.email) {
|
||||||
|
console.log(`邮箱: ${userInfo.email}`);
|
||||||
|
}
|
||||||
|
if (userInfo.phonenumber) {
|
||||||
|
console.log(`手机号: ${userInfo.phonenumber}`);
|
||||||
|
}
|
||||||
|
if (userInfo.avatar) {
|
||||||
|
console.log(`头像: ${userInfo.avatar}`);
|
||||||
|
}
|
||||||
|
if (userInfo.roles && userInfo.roles.length > 0) {
|
||||||
|
console.log(`角色: ${userInfo.roles.join(', ')}`);
|
||||||
|
}
|
||||||
|
if (userInfo.permissions && userInfo.permissions.length > 0) {
|
||||||
|
console.log(`权限: ${userInfo.permissions.join(', ')}`);
|
||||||
|
}
|
||||||
|
if (userInfo.createTime) {
|
||||||
|
console.log(`创建时间: ${userInfo.createTime}`);
|
||||||
|
}
|
||||||
|
if (userInfo.loginDate) {
|
||||||
|
console.log(`最后登录: ${userInfo.loginDate}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打印会员信息 - 从 allMemberships 中获取最高等级
|
||||||
|
if (membershipInfo && membershipInfo.allMemberships) {
|
||||||
|
const highestTier = getHighestTierMembership(membershipInfo.allMemberships);
|
||||||
|
|
||||||
|
if (highestTier) {
|
||||||
|
console.log('');
|
||||||
|
console.log('会员信息:');
|
||||||
|
console.log(`会员等级: ${highestTier.tierName} (${highestTier.tierCode})`);
|
||||||
|
console.log(`等级层级: ${highestTier.tierLevel}`);
|
||||||
|
console.log(`剩余天数: ${highestTier.remainingDays === -1 ? '永久' : highestTier.remainingDays + '天'}`);
|
||||||
|
console.log(`月度积分: ${highestTier.monthlyCredits}`);
|
||||||
|
|
||||||
|
// 将最高等级会员信息合并到用户信息中
|
||||||
|
userInfo.membership = {
|
||||||
|
tierCode: highestTier.tierCode,
|
||||||
|
tierName: highestTier.tierName,
|
||||||
|
tierLevel: highestTier.tierLevel,
|
||||||
|
remainingDays: highestTier.remainingDays,
|
||||||
|
monthlyCredits: highestTier.monthlyCredits
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('='.repeat(60));
|
||||||
|
|
||||||
|
// 保存到持久化存储
|
||||||
|
await saveUserInfo(userInfo);
|
||||||
|
|
||||||
|
return userInfo;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[UserService] 获取用户信息失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 持久化存储 ==============
|
||||||
|
|
||||||
|
let extensionContext: vscode.ExtensionContext | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化用户服务(设置 context)
|
||||||
|
*/
|
||||||
|
export function initUserService(context: vscode.ExtensionContext): void {
|
||||||
|
extensionContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存用户信息到持久化存储
|
||||||
|
*/
|
||||||
|
export async function saveUserInfo(userInfo: UserInfo): Promise<void> {
|
||||||
|
if (!extensionContext) {
|
||||||
|
console.warn('[UserService] ExtensionContext 未初始化');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await extensionContext.globalState.update('icCoderUserInfo', userInfo);
|
||||||
|
console.log('[UserService] 用户信息已保存到持久化存储');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从持久化存储获取用户信息
|
||||||
|
*/
|
||||||
|
export function getCachedUserInfo(): UserInfo | null {
|
||||||
|
if (!extensionContext) {
|
||||||
|
console.warn('[UserService] ExtensionContext 未初始化');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return extensionContext.globalState.get<UserInfo>('icCoderUserInfo') || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除持久化存储的用户信息
|
||||||
|
*/
|
||||||
|
export async function clearUserInfo(): Promise<void> {
|
||||||
|
if (!extensionContext) {
|
||||||
|
console.warn('[UserService] ExtensionContext 未初始化');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await extensionContext.globalState.update('icCoderUserInfo', undefined);
|
||||||
|
console.log('[UserService] 用户信息已清除');
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import * as http from "http";
|
import * as http from "http";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
import * as vscode from "vscode";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* VCD 文件 HTTP 服务器
|
* VCD 文件 HTTP 服务器
|
||||||
@ -10,6 +11,11 @@ export class VCDFileServer {
|
|||||||
private server: http.Server | null = null;
|
private server: http.Server | null = null;
|
||||||
private port: number = 0;
|
private port: number = 0;
|
||||||
private vcdFiles: Map<string, string> = new Map(); // fileId -> filePath
|
private vcdFiles: Map<string, string> = new Map(); // fileId -> filePath
|
||||||
|
private extensionUri: vscode.Uri;
|
||||||
|
|
||||||
|
constructor(extensionUri: vscode.Uri) {
|
||||||
|
this.extensionUri = extensionUri;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 启动服务器
|
* 启动服务器
|
||||||
@ -73,6 +79,13 @@ export class VCDFileServer {
|
|||||||
return `http://127.0.0.1:${this.port}/vcd/${fileId}`;
|
return `http://127.0.0.1:${this.port}/vcd/${fileId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取波形查看器 URL
|
||||||
|
*/
|
||||||
|
public getViewerUrl(fileId: string): string {
|
||||||
|
return `http://127.0.0.1:${this.port}/viewer/${fileId}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成文件 ID
|
* 生成文件 ID
|
||||||
*/
|
*/
|
||||||
@ -101,7 +114,53 @@ export class VCDFileServer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析 URL,提取文件 ID
|
// 路由处理
|
||||||
|
if (url.startsWith("/viewer/")) {
|
||||||
|
this.handleViewerRequest(url, res);
|
||||||
|
} else if (url.startsWith("/vcd/")) {
|
||||||
|
this.handleVcdFileRequest(url, res);
|
||||||
|
} else if (url.startsWith("/static/")) {
|
||||||
|
this.handleStaticFileRequest(url, res);
|
||||||
|
} else {
|
||||||
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||||
|
res.end("Not Found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理查看器页面请求
|
||||||
|
*/
|
||||||
|
private handleViewerRequest(url: string, res: http.ServerResponse): void {
|
||||||
|
const match = url.match(/^\/viewer\/(.+)$/);
|
||||||
|
if (!match) {
|
||||||
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||||
|
res.end("Not Found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileId = match[1];
|
||||||
|
const filePath = this.vcdFiles.get(fileId);
|
||||||
|
|
||||||
|
if (!filePath) {
|
||||||
|
console.error(`[VCDFileServer] 文件 ID 不存在: ${fileId}`);
|
||||||
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||||
|
res.end("File Not Found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成 HTML 页面
|
||||||
|
const html = this.generateViewerHtml(fileId, filePath);
|
||||||
|
res.writeHead(200, {
|
||||||
|
"Content-Type": "text/html; charset=utf-8",
|
||||||
|
"Content-Length": Buffer.byteLength(html),
|
||||||
|
});
|
||||||
|
res.end(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 VCD 文件请求
|
||||||
|
*/
|
||||||
|
private handleVcdFileRequest(url: string, res: http.ServerResponse): void {
|
||||||
const match = url.match(/^\/vcd\/(.+)$/);
|
const match = url.match(/^\/vcd\/(.+)$/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||||
@ -142,4 +201,300 @@ export class VCDFileServer {
|
|||||||
res.end("Internal Server Error");
|
res.end("Internal Server Error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理静态文件请求(Surfer 资源)
|
||||||
|
*/
|
||||||
|
private handleStaticFileRequest(url: string, res: http.ServerResponse): void {
|
||||||
|
const match = url.match(/^\/static\/(.+)$/);
|
||||||
|
if (!match) {
|
||||||
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||||
|
res.end("Not Found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = match[1];
|
||||||
|
const filePath = path.join(this.extensionUri.fsPath, "media", "surfer", fileName);
|
||||||
|
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
console.error(`[VCDFileServer] 静态文件不存在: ${filePath}`);
|
||||||
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||||
|
res.end("File Not Found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileContent = fs.readFileSync(filePath);
|
||||||
|
const contentType = this.getContentType(fileName);
|
||||||
|
res.writeHead(200, {
|
||||||
|
"Content-Type": contentType,
|
||||||
|
"Content-Length": fileContent.length,
|
||||||
|
});
|
||||||
|
res.end(fileContent);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[VCDFileServer] 读取静态文件失败:`, error);
|
||||||
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
||||||
|
res.end("Internal Server Error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件的 Content-Type
|
||||||
|
*/
|
||||||
|
private getContentType(fileName: string): string {
|
||||||
|
const ext = path.extname(fileName).toLowerCase();
|
||||||
|
const contentTypes: { [key: string]: string } = {
|
||||||
|
".js": "application/javascript",
|
||||||
|
".wasm": "application/wasm",
|
||||||
|
".html": "text/html",
|
||||||
|
".css": "text/css",
|
||||||
|
};
|
||||||
|
return contentTypes[ext] || "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 VCD 文件获取根模块及其直接子模块名称
|
||||||
|
*/
|
||||||
|
private parseVcdRootScope(vcdFilePath: string): string[] {
|
||||||
|
try {
|
||||||
|
const buffer = fs.readFileSync(vcdFilePath, { encoding: 'utf8' });
|
||||||
|
const lines = buffer.split('\n');
|
||||||
|
|
||||||
|
const scopeNames: string[] = [];
|
||||||
|
let scopeDepth = 0;
|
||||||
|
const scopeStack: string[] = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
if (trimmed.startsWith('$enddefinitions')) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopeMatch = trimmed.match(/^\$scope\s+(\w+)\s+(\w+)/);
|
||||||
|
if (scopeMatch) {
|
||||||
|
const scopeType = scopeMatch[1];
|
||||||
|
const scopeName = scopeMatch[2];
|
||||||
|
|
||||||
|
if (scopeDepth === 0 && scopeType === 'module') {
|
||||||
|
scopeStack.push(scopeName);
|
||||||
|
} else if (scopeDepth === 1 && scopeType === 'module') {
|
||||||
|
const fullPath = [...scopeStack, scopeName];
|
||||||
|
scopeNames.push(fullPath.join('.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
scopeDepth++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.startsWith('$upscope')) {
|
||||||
|
scopeDepth--;
|
||||||
|
if (scopeDepth === 0) {
|
||||||
|
scopeStack.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return scopeNames;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[VCDFileServer] 解析 VCD 文件失败:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成波形查看器 HTML 页面
|
||||||
|
*/
|
||||||
|
private generateViewerHtml(fileId: string, vcdFilePath: string): string {
|
||||||
|
const vcdUrl = this.getFileUrl(fileId);
|
||||||
|
const fileName = path.basename(vcdFilePath);
|
||||||
|
const scopeNames = this.parseVcdRootScope(vcdFilePath);
|
||||||
|
const scopeNamesJson = JSON.stringify(scopeNames);
|
||||||
|
|
||||||
|
const htmlPart1 = this.getHtmlPart1(fileName);
|
||||||
|
const htmlPart2 = this.getHtmlPart2(vcdUrl, scopeNamesJson);
|
||||||
|
const htmlPart3 = this.getHtmlPart3();
|
||||||
|
|
||||||
|
return htmlPart1 + htmlPart2 + htmlPart3;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getHtmlPart1(fileName: string): string {
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||||
|
<title>Surfer 波形查看器 - ${fileName}</title>
|
||||||
|
<script>
|
||||||
|
window.surferReady = false;
|
||||||
|
window.pendingVcdData = null;
|
||||||
|
|
||||||
|
function on_surfer_error(msg) {
|
||||||
|
console.log("Surfer error:", msg);
|
||||||
|
document.getElementById("error_message").innerHTML = msg;
|
||||||
|
document.getElementById("error_container").style.display = "block";
|
||||||
|
}
|
||||||
|
window.on_surfer_error = on_surfer_error;
|
||||||
|
</script>
|
||||||
|
<script type="module">
|
||||||
|
console.log('[Browser] 开始初始化 Surfer...');
|
||||||
|
import init from '/static/surfer.js';
|
||||||
|
await init({module_or_path: '/static/surfer_bg.wasm'});
|
||||||
|
console.log('[Browser] Surfer WASM 已加载');
|
||||||
|
|
||||||
|
import {WebHandle, inject_message, id_of_name, draw_text_arrow} from '/static/surfer.js';
|
||||||
|
window.inject_message = inject_message;
|
||||||
|
window.id_of_name = id_of_name;
|
||||||
|
window.draw_text_arrow = draw_text_arrow;
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
window.surferReady = true;
|
||||||
|
console.log('[Browser] Surfer 已完全初始化并准备就绪');
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.inject_message(JSON.stringify("ToggleLogs"));
|
||||||
|
console.log('[Browser] 已发送关闭日志面板命令');
|
||||||
|
} catch (e) {
|
||||||
|
console.log('[Browser] 关闭日志面板失败:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.pendingVcdData) {
|
||||||
|
console.log('[Browser] 发现待处理的 VCD 数据,立即加载');
|
||||||
|
loadVcdUrl(window.pendingVcdData);
|
||||||
|
window.pendingVcdData = null;
|
||||||
|
}
|
||||||
|
</script>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getHtmlPart2(vcdUrl: string, scopeNamesJson: string): string {
|
||||||
|
return `
|
||||||
|
<script>
|
||||||
|
function loadVcdUrl(data) {
|
||||||
|
try {
|
||||||
|
console.log('[Browser] ========== 开始加载 VCD URL ==========');
|
||||||
|
console.log('[Browser] URL:', data.url);
|
||||||
|
console.log('[Browser] Scope names from VCD:', data.scopeNames);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('[Browser] 通过 postMessage 发送 LoadUrl 命令');
|
||||||
|
window.postMessage({
|
||||||
|
command: 'LoadUrl',
|
||||||
|
url: data.url
|
||||||
|
}, '*');
|
||||||
|
console.log('[Browser] ✅ 已发送 LoadUrl 命令');
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
console.log('[Browser] 尝试自动添加所有信号');
|
||||||
|
let scopeNamesToTry = [];
|
||||||
|
|
||||||
|
if (data.scopeNames && data.scopeNames.length > 0) {
|
||||||
|
scopeNamesToTry = data.scopeNames.map(path => path.split('.'));
|
||||||
|
console.log('[Browser] 使用解析的作用域名称:', scopeNamesToTry);
|
||||||
|
} else {
|
||||||
|
scopeNamesToTry = [['top'], ['testbench'], ['tb'], ['test'], ['dut']];
|
||||||
|
console.log('[Browser] 使用回退作用域名称');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < scopeNamesToTry.length; i++) {
|
||||||
|
const scopeName = scopeNamesToTry[i];
|
||||||
|
try {
|
||||||
|
const addScopeMsg = {
|
||||||
|
"AddScope": [
|
||||||
|
{"strs": scopeName, "id": {"Wellen": i + 1}},
|
||||||
|
true
|
||||||
|
]
|
||||||
|
};
|
||||||
|
window.inject_message(JSON.stringify(addScopeMsg));
|
||||||
|
console.log('[Browser] 已发送 AddScope: ' + scopeName.join('.'));
|
||||||
|
} catch (e) {
|
||||||
|
console.log('[Browser] AddScope 失败: ' + scopeName.join('.'), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
window.inject_message(JSON.stringify("ZoomToFit"));
|
||||||
|
console.log('[Browser] 已发送 ZoomToFit 命令');
|
||||||
|
} catch (e) {
|
||||||
|
console.log('[Browser] ZoomToFit 失败:', e);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Browser] 添加信号失败:', e);
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
|
}, 100);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Browser] ❌ 加载 VCD 失败:', error);
|
||||||
|
on_surfer_error(error.message + '\\n' + error.stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.loadVcdUrl = loadVcdUrl;
|
||||||
|
|
||||||
|
// 页面加载完成后自动加载 VCD
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const vcdData = {
|
||||||
|
url: '${vcdUrl}',
|
||||||
|
scopeNames: ${scopeNamesJson}
|
||||||
|
};
|
||||||
|
if (window.surferReady) {
|
||||||
|
loadVcdUrl(vcdData);
|
||||||
|
} else {
|
||||||
|
window.pendingVcdData = vcdData;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getHtmlPart3(): string {
|
||||||
|
return `
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
canvas {
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
#error_container {
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
margin: 0px auto;
|
||||||
|
max-width: 980px;
|
||||||
|
color: #f48771;
|
||||||
|
background-color: #5a1d1d;
|
||||||
|
position: relative;
|
||||||
|
height: 90%;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
#error_message {
|
||||||
|
overflow: scroll;
|
||||||
|
white-space: break-spaces;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="the_canvas_id"></canvas>
|
||||||
|
<div id="error_container" style="display: none;">
|
||||||
|
<h3>❌ Surfer 加载失败</h3>
|
||||||
|
<code id="error_message"></code>
|
||||||
|
</div>
|
||||||
|
<script src="/static/integration.js"></script>
|
||||||
|
<script>
|
||||||
|
register_message_listener();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
173
src/types/api.ts
173
src/types/api.ts
@ -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 格式) ==============
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -385,6 +310,96 @@ export interface ToolConfirmResponse {
|
|||||||
approved: boolean;
|
approved: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============== 用户信息 ==============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户信息响应
|
||||||
|
* GET /system/user/getInfo
|
||||||
|
*/
|
||||||
|
export interface UserInfoResponse {
|
||||||
|
/** 响应消息 */
|
||||||
|
msg: string;
|
||||||
|
/** 响应代码 (200 表示成功) */
|
||||||
|
code: number;
|
||||||
|
/** 权限列表 */
|
||||||
|
permissions: string[];
|
||||||
|
/** 角色列表 */
|
||||||
|
roles: string[];
|
||||||
|
/** 是否默认修改密码 */
|
||||||
|
isDefaultModifyPwd: boolean;
|
||||||
|
/** 密码是否过期 */
|
||||||
|
isPasswordExpired: boolean;
|
||||||
|
/** 用户信息 */
|
||||||
|
user: {
|
||||||
|
userId: number;
|
||||||
|
userName: string;
|
||||||
|
nickName: string;
|
||||||
|
email?: string;
|
||||||
|
phonenumber?: string;
|
||||||
|
sex?: string;
|
||||||
|
avatar?: string;
|
||||||
|
status?: string;
|
||||||
|
createTime?: string;
|
||||||
|
loginDate?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 会员信息 ==============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员单条记录
|
||||||
|
*/
|
||||||
|
export interface MembershipItemVO {
|
||||||
|
membershipId: number | null;
|
||||||
|
tierCode: string;
|
||||||
|
tierName: string;
|
||||||
|
tierLevel: number;
|
||||||
|
expireTime: string | null;
|
||||||
|
remainingDays: number;
|
||||||
|
permanent: boolean;
|
||||||
|
nextGrantTime: string | null;
|
||||||
|
lastGrantTime: string | null;
|
||||||
|
grantCycle: number;
|
||||||
|
totalGranted: number;
|
||||||
|
monthlyCredits: number;
|
||||||
|
teamSeat: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户会员信息
|
||||||
|
*/
|
||||||
|
export interface UserMembershipVO {
|
||||||
|
userId: number;
|
||||||
|
tierCode: string;
|
||||||
|
tierName: string;
|
||||||
|
tierLevel: number;
|
||||||
|
allowedModelCombinations: string[];
|
||||||
|
description?: string;
|
||||||
|
createdTime?: string;
|
||||||
|
updatedTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多会员信息响应
|
||||||
|
*/
|
||||||
|
export interface MultiMembershipVO extends UserMembershipVO {
|
||||||
|
displayTier?: MembershipItemVO;
|
||||||
|
allMemberships?: MembershipItemVO[];
|
||||||
|
totalMonthlyCredits?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员信息响应
|
||||||
|
* GET /strangeloop/api/membership/current
|
||||||
|
*/
|
||||||
|
export interface MembershipResponse {
|
||||||
|
code: number;
|
||||||
|
msg?: string;
|
||||||
|
message?: string;
|
||||||
|
data?: MultiMembershipVO;
|
||||||
|
}
|
||||||
|
|
||||||
// ============== 辅助类型 ==============
|
// ============== 辅助类型 ==============
|
||||||
|
|
||||||
/** 后端工具名称 */
|
/** 后端工具名称 */
|
||||||
@ -433,10 +448,6 @@ 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 工具参数 */
|
||||||
|
|||||||
@ -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 : '未知错误'}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -59,7 +59,7 @@ export async function handleUserMessage(
|
|||||||
text: string,
|
text: string,
|
||||||
extensionPath?: string,
|
extensionPath?: string,
|
||||||
mode?: RunMode,
|
mode?: RunMode,
|
||||||
serviceTier?: ServiceTier // 新增:服务等级参数
|
serviceTier?: ServiceTier // 新增:服务等级参数
|
||||||
) {
|
) {
|
||||||
console.log("收到用户消息:", text);
|
console.log("收到用户消息:", text);
|
||||||
|
|
||||||
@ -91,7 +91,14 @@ export async function handleUserMessage(
|
|||||||
// 尝试使用后端服务
|
// 尝试使用后端服务
|
||||||
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);
|
||||||
@ -127,7 +134,7 @@ async function handleUserMessageWithBackend(
|
|||||||
extensionPath: string,
|
extensionPath: string,
|
||||||
mode?: RunMode,
|
mode?: RunMode,
|
||||||
reuseTaskId?: string, // 可选,复用现有 taskId(用于 Plan 模式确认后继续执行)
|
reuseTaskId?: string, // 可选,复用现有 taskId(用于 Plan 模式确认后继续执行)
|
||||||
serviceTier?: ServiceTier // 新增:服务等级参数
|
serviceTier?: ServiceTier // 新增:服务等级参数
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const historyManager = ChatHistoryManager.getInstance();
|
const historyManager = ChatHistoryManager.getInstance();
|
||||||
|
|
||||||
@ -137,10 +144,18 @@ async function handleUserMessageWithBackend(
|
|||||||
|
|
||||||
// 创建或复用会话
|
// 创建或复用会话
|
||||||
if (!currentSession || !currentSession.active) {
|
if (!currentSession || !currentSession.active) {
|
||||||
currentSession = dialogManager.createSession(extensionPath, taskIdToUse || undefined);
|
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" : "新生成"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示状态栏
|
// 显示状态栏
|
||||||
@ -288,39 +303,9 @@ 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 // 传递服务等级
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -98,24 +98,24 @@ export function getAgentCardStyles(): string {
|
|||||||
}
|
}
|
||||||
/* 低调显示的工具调用样式 */
|
/* 低调显示的工具调用样式 */
|
||||||
.agent-step.low-profile {
|
.agent-step.low-profile {
|
||||||
opacity: 0.5;
|
opacity: 0.85;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
padding: 2px 6px;
|
padding: 4px 8px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
.agent-step.low-profile .step-icon {
|
.agent-step.low-profile .step-icon {
|
||||||
opacity: 0.4;
|
opacity: 0.8;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.agent-step.low-profile .step-name {
|
.agent-step.low-profile .step-name {
|
||||||
font-weight: 300;
|
font-weight: 400;
|
||||||
color: var(--vscode-descriptionForeground);
|
color: var(--vscode-descriptionForeground);
|
||||||
opacity: 0.7;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
.agent-step.low-profile .step-result {
|
.agent-step.low-profile .step-result {
|
||||||
opacity: 0.6;
|
opacity: 0.85;
|
||||||
font-size: 9px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,10 @@
|
|||||||
|
import {
|
||||||
|
getUserInfoComponentContent,
|
||||||
|
getUserInfoComponentStyles,
|
||||||
|
getUserInfoComponentScript,
|
||||||
|
} from "./userInfoComponent";
|
||||||
|
import { userAvatarIconSvg } from "../constants/toolIcons";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取会话历史栏的 HTML 内容
|
* 获取会话历史栏的 HTML 内容
|
||||||
*/
|
*/
|
||||||
@ -6,7 +13,7 @@ export function getConversationHistoryBarContent(): string {
|
|||||||
<div class="conversation-history-bar">
|
<div class="conversation-history-bar">
|
||||||
<div class="history-dropdown-container">
|
<div class="history-dropdown-container">
|
||||||
<button class="history-dropdown-button" onclick="toggleHistoryDropdown()">
|
<button class="history-dropdown-button" onclick="toggleHistoryDropdown()">
|
||||||
<span class="dropdown-label">Past Conversations</span>
|
<span class="dropdown-label">历史对话</span>
|
||||||
<svg class="dropdown-icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
<svg class="dropdown-icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3 0.1-12.7-6.4-12.7z" fill="currentColor"/>
|
<path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3 0.1-12.7-6.4-12.7z" fill="currentColor"/>
|
||||||
</svg>
|
</svg>
|
||||||
@ -19,11 +26,20 @@ export function getConversationHistoryBarContent(): string {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="new-conversation-button" onclick="createNewConversation()" title="新建对话">
|
<div class="right-actions">
|
||||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
<button class="new-conversation-button" onclick="createNewConversation()" title="新建对话">
|
||||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor"/>
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
</svg>
|
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor"/>
|
||||||
</button>
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="user-info-container">
|
||||||
|
<button class="user-avatar-icon-button" id="userAvatarIconButton" style="display: none;" title="查看用户信息" onclick="openUserDetailModal()">
|
||||||
|
${userAvatarIconSvg}
|
||||||
|
</button>
|
||||||
|
${getUserInfoComponentContent()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -49,13 +65,59 @@ export function getConversationHistoryBarStyles(): string {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.right-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar-icon-button {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar-icon-button:hover {
|
||||||
|
background: var(--vscode-toolbar-hoverBackground);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar-icon-button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar-icon-button.active {
|
||||||
|
background: var(--vscode-toolbar-hoverBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar-icon-button svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
${getUserInfoComponentStyles()}
|
||||||
|
|
||||||
.history-dropdown-button {
|
.history-dropdown-button {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--vscode-input-foreground);
|
color: var(--vscode-foreground);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -64,7 +126,7 @@ export function getConversationHistoryBarStyles(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.history-dropdown-button:hover {
|
.history-dropdown-button:hover {
|
||||||
opacity: 0.8;
|
background: var(--vscode-toolbar-hoverBackground);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-label {
|
.dropdown-label {
|
||||||
@ -163,7 +225,7 @@ export function getConversationHistoryBarStyles(): string {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--vscode-foreground);
|
color: var(--vscode-foreground);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 50%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -173,11 +235,12 @@ export function getConversationHistoryBarStyles(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.new-conversation-button:hover {
|
.new-conversation-button:hover {
|
||||||
opacity: 0.7;
|
background: var(--vscode-toolbar-hoverBackground);
|
||||||
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-conversation-button:active {
|
.new-conversation-button:active {
|
||||||
opacity: 0.5;
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-conversation-button svg {
|
.new-conversation-button svg {
|
||||||
@ -210,6 +273,29 @@ export function getConversationHistoryBarStyles(): string {
|
|||||||
*/
|
*/
|
||||||
export function getConversationHistoryBarScript(): string {
|
export function getConversationHistoryBarScript(): string {
|
||||||
return `
|
return `
|
||||||
|
${getUserInfoComponentScript()}
|
||||||
|
|
||||||
|
// 更新用户头像图标按钮显示
|
||||||
|
function updateUserAvatarIconButton(userInfo) {
|
||||||
|
const userAvatarIconButton = document.getElementById('userAvatarIconButton');
|
||||||
|
|
||||||
|
if (userInfo && userInfo.nickname) {
|
||||||
|
// 显示用户头像图标按钮
|
||||||
|
if (userAvatarIconButton) {
|
||||||
|
userAvatarIconButton.style.display = 'flex';
|
||||||
|
}
|
||||||
|
// 同时更新用户详情弹窗的数据
|
||||||
|
if (typeof updateUserInfoDisplay === 'function') {
|
||||||
|
updateUserInfoDisplay(userInfo);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 隐藏用户头像图标按钮
|
||||||
|
if (userAvatarIconButton) {
|
||||||
|
userAvatarIconButton.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 会话历史相关变量
|
// 会话历史相关变量
|
||||||
let conversationHistory = [];
|
let conversationHistory = [];
|
||||||
let currentConversationId = null;
|
let currentConversationId = null;
|
||||||
|
|||||||
@ -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 = {
|
||||||
@ -760,16 +737,39 @@ export function getMessageAreaScript(): string {
|
|||||||
return toolNameMap[toolName] || toolName;
|
return toolNameMap[toolName] || toolName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 自动滚动控制标志
|
||||||
|
let shouldAutoScroll = true;
|
||||||
|
let lastScrollHeight = 0;
|
||||||
|
|
||||||
// 检查用户是否在底部附近(允许50px的误差)
|
// 检查用户是否在底部附近(允许50px的误差)
|
||||||
function isUserNearBottom() {
|
function isUserNearBottom() {
|
||||||
const threshold = 50;
|
const threshold = 50;
|
||||||
return messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight < threshold;
|
return messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight < threshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 智能滚动:只有用户在底部附近时才自动滚动
|
// 监听用户滚动行为
|
||||||
|
messagesEl.addEventListener('scroll', () => {
|
||||||
|
const isAtBottom = isUserNearBottom();
|
||||||
|
|
||||||
|
// 如果用户滚动到底部,恢复自动滚动
|
||||||
|
if (isAtBottom) {
|
||||||
|
shouldAutoScroll = true;
|
||||||
|
} else {
|
||||||
|
// 只有当内容高度没有变化时,才认为是用户主动滚动
|
||||||
|
// 如果内容高度变化了,说明是因为新内容导致的位置变化,不应该停止自动滚动
|
||||||
|
if (messagesEl.scrollHeight === lastScrollHeight) {
|
||||||
|
shouldAutoScroll = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastScrollHeight = messagesEl.scrollHeight;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 智能滚动:只有在允许自动滚动时才滚动到底部
|
||||||
function smartScrollToBottom() {
|
function smartScrollToBottom() {
|
||||||
if (isUserNearBottom()) {
|
if (shouldAutoScroll) {
|
||||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||||
|
lastScrollHeight = messagesEl.scrollHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1057,30 +1057,19 @@ 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);
|
let vcdPath = segment.vcdFilePath;
|
||||||
|
if (!vcdPath && segment.toolResult) {
|
||||||
if (vcdPaths.length > 0) {
|
const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/);
|
||||||
// 多 VCD 模式:为每个文件创建预览
|
if (match && match[1]) {
|
||||||
vcdPaths.forEach(vcdInfo => {
|
vcdPath = match[1].trim();
|
||||||
const waveformPreview = createWaveformPreview(vcdInfo.path, vcdInfo.name);
|
|
||||||
segmentDiv.appendChild(waveformPreview);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 单 VCD 模式(兼容旧逻辑)
|
|
||||||
let vcdPath = segment.vcdFilePath;
|
|
||||||
if (!vcdPath && segment.toolResult) {
|
|
||||||
const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/);
|
|
||||||
if (match && match[1]) {
|
|
||||||
vcdPath = match[1].trim();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (vcdPath) {
|
if (vcdPath) {
|
||||||
const fileName = segment.fileName || vcdPath.split(/[\\\\\/]/).pop() || 'waveform.vcd';
|
const fileName = segment.fileName || vcdPath.split(/[\\\\\/]/).pop() || 'waveform.vcd';
|
||||||
const waveformPreview = createWaveformPreview(vcdPath, fileName);
|
const waveformPreview = createWaveformPreview(vcdPath, fileName);
|
||||||
segmentDiv.appendChild(waveformPreview);
|
segmentDiv.appendChild(waveformPreview);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1315,30 +1304,19 @@ 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);
|
let vcdPath = segment.vcdFilePath;
|
||||||
|
if (!vcdPath && segment.toolResult) {
|
||||||
if (vcdPaths.length > 0) {
|
const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/);
|
||||||
// 多 VCD 模式:为每个文件创建预览
|
if (match && match[1]) {
|
||||||
vcdPaths.forEach(vcdInfo => {
|
vcdPath = match[1].trim();
|
||||||
const waveformPreview = createWaveformPreview(vcdInfo.path, vcdInfo.name);
|
|
||||||
segmentDiv.appendChild(waveformPreview);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 单 VCD 模式(兼容旧逻辑)
|
|
||||||
let vcdPath = segment.vcdFilePath;
|
|
||||||
if (!vcdPath && segment.toolResult) {
|
|
||||||
const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/);
|
|
||||||
if (match && match[1]) {
|
|
||||||
vcdPath = match[1].trim();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (vcdPath) {
|
if (vcdPath) {
|
||||||
const fileName = segment.fileName || vcdPath.split(/[\\\\\/]/).pop() || 'waveform.vcd';
|
const fileName = segment.fileName || vcdPath.split(/[\\\\\/]/).pop() || 'waveform.vcd';
|
||||||
const waveformPreview = createWaveformPreview(vcdPath, fileName);
|
const waveformPreview = createWaveformPreview(vcdPath, fileName);
|
||||||
segmentDiv.appendChild(waveformPreview);
|
segmentDiv.appendChild(waveformPreview);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>');
|
|
||||||
|
|
||||||
// 代码块 (\`\`\`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>
|
||||||
@ -677,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">
|
||||||
@ -697,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>
|
||||||
|
|||||||
@ -25,7 +25,7 @@ export function getProgressBarContent(): string {
|
|||||||
<span class="step-number">1</span>
|
<span class="step-number">1</span>
|
||||||
<span class="step-check">✓</span>
|
<span class="step-check">✓</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="step-label">Spec设计文档</div>
|
<div class="step-label">Spec</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="progress-line"></div>
|
<div class="progress-line"></div>
|
||||||
@ -35,7 +35,7 @@ export function getProgressBarContent(): string {
|
|||||||
<span class="step-number">2</span>
|
<span class="step-number">2</span>
|
||||||
<span class="step-check">✓</span>
|
<span class="step-check">✓</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="step-label">Design代码编写</div>
|
<div class="step-label">Design</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="progress-line"></div>
|
<div class="progress-line"></div>
|
||||||
@ -45,7 +45,7 @@ export function getProgressBarContent(): string {
|
|||||||
<span class="step-number">3</span>
|
<span class="step-number">3</span>
|
||||||
<span class="step-check">✓</span>
|
<span class="step-check">✓</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="step-label">Sim仿真检查</div>
|
<div class="step-label">Simulation</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="progress-line"></div>
|
<div class="progress-line"></div>
|
||||||
@ -55,7 +55,7 @@ export function getProgressBarContent(): string {
|
|||||||
<span class="step-number">4</span>
|
<span class="step-number">4</span>
|
||||||
<span class="step-check">✓</span>
|
<span class="step-check">✓</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="step-label">Done完成</div>
|
<div class="step-label">Done</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
287
src/views/userInfoComponent.ts
Normal file
287
src/views/userInfoComponent.ts
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
/**
|
||||||
|
* 用户信息组件
|
||||||
|
* 包含用户头像、昵称、会员等级等信息
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息组件的 HTML 内容
|
||||||
|
* 只包含用户详情下拉面板,不包含触发按钮
|
||||||
|
*/
|
||||||
|
export function getUserInfoComponentContent(): string {
|
||||||
|
return `
|
||||||
|
<div class="user-info-wrapper">
|
||||||
|
<!-- 用户详情下拉面板 -->
|
||||||
|
<div class="user-detail-dropdown" id="userDetailDropdown">
|
||||||
|
<div class="user-detail-content">
|
||||||
|
<div class="user-detail-header">
|
||||||
|
<div class="user-avatar-small">
|
||||||
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="user-name-tier">
|
||||||
|
<div class="user-detail-name" id="userDetailName">加载中...</div>
|
||||||
|
<img class="tier-icon-inline" id="tierIconInline" style="display: none;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="user-detail-body">
|
||||||
|
<div class="user-detail-item">
|
||||||
|
<span class="detail-label">剩余 Credits</span>
|
||||||
|
<span class="detail-value" id="creditsDetail">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息组件的 CSS 样式
|
||||||
|
*/
|
||||||
|
export function getUserInfoComponentStyles(): string {
|
||||||
|
return `
|
||||||
|
.user-info-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 用户详情下拉面板 */
|
||||||
|
.user-detail-dropdown {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 8px);
|
||||||
|
right: 0;
|
||||||
|
z-index: 10000;
|
||||||
|
min-width: 250px;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-detail-dropdown.active {
|
||||||
|
display: block;
|
||||||
|
animation: dropdownSlideIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dropdownSlideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-detail-content {
|
||||||
|
background: var(--vscode-sideBar-background);
|
||||||
|
border: 1px solid var(--vscode-widget-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-detail-header {
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
background: linear-gradient(135deg, rgba(0, 122, 204, 0.1) 0%, rgba(88, 166, 255, 0.05) 100%);
|
||||||
|
border-bottom: 1px solid var(--vscode-widget-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar-small {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: linear-gradient(135deg, #007acc 0%, #58a6ff 100%);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 122, 204, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar-small svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name-tier {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-detail-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tier-icon-inline {
|
||||||
|
height: 26px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-detail-body {
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--vscode-sideBar-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-detail-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--vscode-widget-border);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-detail-item:hover {
|
||||||
|
background: var(--vscode-list-hoverBackground);
|
||||||
|
border-color: rgba(0, 122, 204, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-detail-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tier-icon-large {
|
||||||
|
height: 20px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tier-icon {
|
||||||
|
width: 110px;
|
||||||
|
height: 35px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息组件的 JavaScript 脚本
|
||||||
|
*/
|
||||||
|
export function getUserInfoComponentScript(): string {
|
||||||
|
return `
|
||||||
|
// 用户信息数据
|
||||||
|
let currentUserInfo = null;
|
||||||
|
|
||||||
|
// 切换用户详情下拉面板
|
||||||
|
function openUserDetailModal() {
|
||||||
|
const dropdown = document.getElementById('userDetailDropdown');
|
||||||
|
const userButton = document.getElementById('userAvatarIconButton');
|
||||||
|
|
||||||
|
if (dropdown) {
|
||||||
|
const isActive = dropdown.classList.contains('active');
|
||||||
|
if (isActive) {
|
||||||
|
dropdown.classList.remove('active');
|
||||||
|
if (userButton) {
|
||||||
|
userButton.classList.remove('active');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dropdown.classList.add('active');
|
||||||
|
if (userButton) {
|
||||||
|
userButton.classList.add('active');
|
||||||
|
}
|
||||||
|
// 更新下拉面板中的用户信息
|
||||||
|
updateUserDetailModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭用户详情下拉面板
|
||||||
|
function closeUserDetailModal() {
|
||||||
|
const dropdown = document.getElementById('userDetailDropdown');
|
||||||
|
const userButton = document.getElementById('userAvatarIconButton');
|
||||||
|
|
||||||
|
if (dropdown) {
|
||||||
|
dropdown.classList.remove('active');
|
||||||
|
}
|
||||||
|
if (userButton) {
|
||||||
|
userButton.classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户详情下拉面板内容
|
||||||
|
function updateUserDetailModal() {
|
||||||
|
if (!currentUserInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户名
|
||||||
|
const userDetailName = document.getElementById('userDetailName');
|
||||||
|
if (userDetailName) {
|
||||||
|
userDetailName.textContent = currentUserInfo.nickname || '未知用户';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新会员等级图标(显示在用户名旁边)
|
||||||
|
const tierIconInline = document.getElementById('tierIconInline');
|
||||||
|
if (tierIconInline && currentUserInfo.tierIconUrl) {
|
||||||
|
tierIconInline.src = currentUserInfo.tierIconUrl;
|
||||||
|
tierIconInline.style.display = 'block';
|
||||||
|
} else if (tierIconInline) {
|
||||||
|
tierIconInline.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新剩余 Credits
|
||||||
|
const creditsDetail = document.getElementById('creditsDetail');
|
||||||
|
if (creditsDetail) {
|
||||||
|
creditsDetail.textContent = currentUserInfo.credits !== undefined ? currentUserInfo.credits.toString() : '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户信息显示
|
||||||
|
function updateUserInfoDisplay(userInfo) {
|
||||||
|
currentUserInfo = userInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定下拉面板事件
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// 点击页面其他地方关闭下拉面板
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
const dropdown = document.getElementById('userDetailDropdown');
|
||||||
|
const userButton = document.getElementById('userAvatarIconButton');
|
||||||
|
|
||||||
|
if (dropdown && dropdown.classList.contains('active')) {
|
||||||
|
// 如果点击的不是用户按钮和下拉面板内容,则关闭
|
||||||
|
if (!userButton?.contains(e.target) && !dropdown.contains(e.target)) {
|
||||||
|
closeUserDetailModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 阻止下拉面板内容点击事件冒泡
|
||||||
|
const dropdownContent = document.querySelector('.user-detail-content');
|
||||||
|
if (dropdownContent) {
|
||||||
|
dropdownContent.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
`;
|
||||||
|
}
|
||||||
@ -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] 波形渲染完成');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -347,7 +356,7 @@ export function getWaveformPreviewScript(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打开完整波形查看器
|
* 打开完整波形查看器(在新列中)
|
||||||
*/
|
*/
|
||||||
function openFullWaveform(vcdFilePath) {
|
function openFullWaveform(vcdFilePath) {
|
||||||
vscode.postMessage({
|
vscode.postMessage({
|
||||||
|
|||||||
@ -585,6 +585,27 @@ export function getWebviewContent(
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'updateUserInfo':
|
||||||
|
// 更新用户信息
|
||||||
|
console.log('[WebView] 收到用户信息:', message.userInfo);
|
||||||
|
if (message.userInfo) {
|
||||||
|
const userInfoData = {
|
||||||
|
nickname: message.userInfo.nickname || message.userInfo.username || '用户',
|
||||||
|
userId: message.userInfo.userId || message.userInfo.id,
|
||||||
|
tierName: message.userInfo.tierName,
|
||||||
|
tierIconUrl: message.tierIconUrl,
|
||||||
|
registerTime: message.userInfo.registerTime || message.userInfo.createdAt
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('[WebView] 显示用户信息:', userInfoData);
|
||||||
|
|
||||||
|
// 调用更新用户头像图标按钮的函数
|
||||||
|
if (typeof updateUserAvatarIconButton === 'function') {
|
||||||
|
updateUserAvatarIconButton(userInfoData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case 'resetSegmentedMessage':
|
case 'resetSegmentedMessage':
|
||||||
// 重置分段消息容器(停止对话时调用)
|
// 重置分段消息容器(停止对话时调用)
|
||||||
console.log('[WebView] 重置分段消息容器');
|
console.log('[WebView] 重置分段消息容器');
|
||||||
@ -611,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 {
|
||||||
@ -721,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()}
|
||||||
|
|||||||
Reference in New Issue
Block a user