Merge remote-tracking branch 'refs/remotes/origin/merge/merge' into feat/back-to-front
# Conflicts: # src/config/settings.ts # src/services/icCoderAuthProvider.ts
This commit is contained in:
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 |
@ -19,6 +19,8 @@ export interface IccoderConfig {
|
||||
backendUrl: string;
|
||||
/** 登录页面地址 */
|
||||
loginUrl: string;
|
||||
/** 后端服务地址(strangeLoop) */
|
||||
backendUrlStrongeLoop: string;
|
||||
/** 请求超时时间(毫秒) */
|
||||
timeout: number;
|
||||
/** 用户ID(临时使用,后续对接认证) */
|
||||
@ -32,6 +34,7 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
||||
/** 本地开发环境 */
|
||||
dev: {
|
||||
backendUrl: "http://localhost:2233",
|
||||
backendUrlStrongeLoop: "http://192.168.1.108:2029",
|
||||
loginUrl: "http://localhost/login",
|
||||
timeout: 300000,
|
||||
userId: "default-user",
|
||||
@ -40,6 +43,7 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
||||
/** 测试服务器环境 */
|
||||
test: {
|
||||
backendUrl: "http://192.168.1.108:2233",
|
||||
backendUrlStrongeLoop: "http://192.168.1.108:2029",
|
||||
loginUrl: "http://192.168.1.108:2005/login",
|
||||
timeout: 60000,
|
||||
userId: "default-user",
|
||||
@ -48,6 +52,7 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
||||
/** 生产环境 */
|
||||
prod: {
|
||||
backendUrl: "https://api.iccoder.com",
|
||||
backendUrlStrongeLoop: "http://api.iccoder.com:2029",
|
||||
loginUrl: "https://iccoder.com/login",
|
||||
timeout: 60000,
|
||||
userId: "default-user",
|
||||
@ -80,3 +85,15 @@ export function getApiUrl(path: string): string {
|
||||
const apiPath = path.startsWith("/") ? path : `/${path}`;
|
||||
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
|
||||
*/
|
||||
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 { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider";
|
||||
import { VCDFileServer } from "./services/vcdFileServer";
|
||||
import { initUserService } from "./services/userService";
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
console.log("🎉 IC Coder 插件已激活!");
|
||||
|
||||
// 初始化用户服务
|
||||
initUserService(context);
|
||||
|
||||
// 初始化 VCD 文件服务器
|
||||
const vcdFileServer = new VCDFileServer();
|
||||
const vcdFileServer = new VCDFileServer(context.extensionUri);
|
||||
vcdFileServer.start().then((port) => {
|
||||
console.log(`VCD 文件服务器已启动,端口: ${port}`);
|
||||
}).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(
|
||||
"ic-coder.login",
|
||||
@ -182,6 +219,7 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
openPanelCommand,
|
||||
openChatCommand,
|
||||
openVCDViewerCommand,
|
||||
openVCDViewerInBrowserCommand,
|
||||
loginCommand,
|
||||
logoutCommand,
|
||||
// TODO: 等待重新实现这些命令
|
||||
|
||||
@ -18,6 +18,44 @@ import { compactDialog } from "../services/apiClient";
|
||||
import { VCDViewerPanel } from "./VCDViewerPanel";
|
||||
import { ChatHistoryManager } from "../utils/chatHistoryManager";
|
||||
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 助手面板
|
||||
@ -62,7 +100,7 @@ export async function showICHelperPanel(
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [
|
||||
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
|
||||
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(
|
||||
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(
|
||||
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(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png")
|
||||
vscode.Uri.joinPath(
|
||||
context.extensionUri,
|
||||
"src",
|
||||
"assets",
|
||||
"model",
|
||||
"Max.png"
|
||||
)
|
||||
);
|
||||
|
||||
// 设置HTML内容
|
||||
@ -108,6 +170,52 @@ export async function showICHelperPanel(
|
||||
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(
|
||||
async (message) => {
|
||||
@ -142,7 +250,7 @@ export async function showICHelperPanel(
|
||||
historyManager.switchToPanelTask(panelId);
|
||||
|
||||
// 显示进度条
|
||||
panel.webview.postMessage({ type: 'showProgress' });
|
||||
panel.webview.postMessage({ type: "showProgress" });
|
||||
|
||||
handleUserMessage(
|
||||
panel,
|
||||
@ -176,10 +284,12 @@ export async function showICHelperPanel(
|
||||
vscode.window.showInformationMessage(message.text);
|
||||
break;
|
||||
case "openWaveformViewer":
|
||||
// 打开波形查看器 - 使用 vscode.open 触发自定义编辑器
|
||||
// 在新列中打开波形查看器
|
||||
if (message.vcdFilePath) {
|
||||
const vcdUri = vscode.Uri.file(message.vcdFilePath);
|
||||
vscode.commands.executeCommand('vscode.open', vcdUri);
|
||||
vscode.commands.executeCommand(
|
||||
"ic-coder.openVCDViewer",
|
||||
message.vcdFilePath
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "getVCDInfo":
|
||||
@ -322,7 +432,11 @@ export async function showICHelperPanel(
|
||||
try {
|
||||
const items = fs.readdirSync(dir, { withFileTypes: true });
|
||||
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 relativePath = path.relative(baseDir, fullPath);
|
||||
folders.push({ path: fullPath, relativePath });
|
||||
@ -351,7 +465,7 @@ export async function showICHelperPanel(
|
||||
canSelectMany: true,
|
||||
openLabel: "选择图片",
|
||||
filters: {
|
||||
"图片文件": ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp"],
|
||||
图片文件: ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp"],
|
||||
},
|
||||
});
|
||||
if (imageUris && imageUris.length > 0) {
|
||||
@ -371,8 +485,8 @@ export async function showICHelperPanel(
|
||||
canSelectMany: true,
|
||||
openLabel: "选择文档",
|
||||
filters: {
|
||||
"文档文件": ["pdf", "doc", "docx", "txt", "md"],
|
||||
"所有文件": ["*"],
|
||||
文档文件: ["pdf", "doc", "docx", "txt", "md"],
|
||||
所有文件: ["*"],
|
||||
},
|
||||
});
|
||||
if (docUris && docUris.length > 0) {
|
||||
@ -408,6 +522,29 @@ export async function showICHelperPanel(
|
||||
hasWorkspace: hasWorkspace,
|
||||
});
|
||||
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,
|
||||
|
||||
@ -107,7 +107,8 @@ export class VCDViewerPanel {
|
||||
* 创建或显示 VCD 查看器面板
|
||||
*/
|
||||
public static createOrShow(extensionUri: vscode.Uri, vcdFilePath?: string, vcdFileServer?: VCDFileServer) {
|
||||
const column = vscode.ViewColumn.One;
|
||||
// 在当前活动编辑器旁边打开新列
|
||||
const column = vscode.ViewColumn.Beside;
|
||||
|
||||
// 如果已经有面板打开,则显示它
|
||||
if (VCDViewerPanel.currentPanel) {
|
||||
|
||||
@ -6,7 +6,7 @@ import * as https from 'https';
|
||||
import * as http from 'http';
|
||||
import { URL } from 'url';
|
||||
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 请求选项
|
||||
@ -213,3 +213,14 @@ export function createSystemErrorResult(id: number, code: number, message: strin
|
||||
error: { code, message }
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* GET /system/user/getInfo
|
||||
*/
|
||||
export async function getUserInfo(): Promise<UserInfoResponse> {
|
||||
console.log('[API] 获取用户信息');
|
||||
return request<UserInfoResponse>('/system/user/getInfo', {
|
||||
method: 'GET'
|
||||
});
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import * as vscode from "vscode";
|
||||
import * as http from "http";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import { onTokenReceived, type UserInfo, clearUserInfo } from "./userService";
|
||||
import { getConfig } from "../config/settings";
|
||||
|
||||
/**
|
||||
@ -62,13 +63,16 @@ export class ICCoderAuthenticationProvider
|
||||
try {
|
||||
const token = await this.login();
|
||||
|
||||
// 获取到 token 后立即调用用户信息接口
|
||||
const userInfo = await onTokenReceived(token);
|
||||
|
||||
// 创建会话
|
||||
const session: vscode.AuthenticationSession = {
|
||||
id: this.generateSessionId(),
|
||||
accessToken: token,
|
||||
account: {
|
||||
id: "iccoder-user",
|
||||
label: "IC Coder 用户",
|
||||
id: userInfo?.userId || "iccoder-user",
|
||||
label: userInfo?.nickname || userInfo?.username || "IC Coder 用户",
|
||||
},
|
||||
scopes: [...scopes],
|
||||
};
|
||||
@ -109,6 +113,9 @@ export class ICCoderAuthenticationProvider
|
||||
this._sessions.splice(sessionIndex, 1);
|
||||
await this.saveSessions();
|
||||
|
||||
// 清除用户信息缓存
|
||||
await clearUserInfo();
|
||||
|
||||
// 触发会话变化事件
|
||||
this._onDidChangeSessions.fire({
|
||||
added: [],
|
||||
|
||||
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 fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as vscode from "vscode";
|
||||
|
||||
/**
|
||||
* VCD 文件 HTTP 服务器
|
||||
@ -10,6 +11,11 @@ export class VCDFileServer {
|
||||
private server: http.Server | null = null;
|
||||
private port: number = 0;
|
||||
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}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取波形查看器 URL
|
||||
*/
|
||||
public getViewerUrl(fileId: string): string {
|
||||
return `http://127.0.0.1:${this.port}/viewer/${fileId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文件 ID
|
||||
*/
|
||||
@ -101,7 +114,53 @@ export class VCDFileServer {
|
||||
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\/(.+)$/);
|
||||
if (!match) {
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
@ -142,4 +201,300 @@ export class VCDFileServer {
|
||||
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>`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -385,6 +385,96 @@ export interface ToolConfirmResponse {
|
||||
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;
|
||||
}
|
||||
|
||||
// ============== 辅助类型 ==============
|
||||
|
||||
/** 后端工具名称 */
|
||||
|
||||
@ -98,24 +98,24 @@ export function getAgentCardStyles(): string {
|
||||
}
|
||||
/* 低调显示的工具调用样式 */
|
||||
.agent-step.low-profile {
|
||||
opacity: 0.5;
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
opacity: 0.85;
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
background: transparent;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.agent-step.low-profile .step-icon {
|
||||
opacity: 0.4;
|
||||
font-size: 10px;
|
||||
opacity: 0.8;
|
||||
font-size: 12px;
|
||||
}
|
||||
.agent-step.low-profile .step-name {
|
||||
font-weight: 300;
|
||||
font-weight: 400;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
opacity: 0.7;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.agent-step.low-profile .step-result {
|
||||
opacity: 0.6;
|
||||
font-size: 9px;
|
||||
opacity: 0.85;
|
||||
font-size: 11px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@ -1,3 +1,10 @@
|
||||
import {
|
||||
getUserInfoComponentContent,
|
||||
getUserInfoComponentStyles,
|
||||
getUserInfoComponentScript,
|
||||
} from "./userInfoComponent";
|
||||
import { userAvatarIconSvg } from "../constants/toolIcons";
|
||||
|
||||
/**
|
||||
* 获取会话历史栏的 HTML 内容
|
||||
*/
|
||||
@ -6,7 +13,7 @@ export function getConversationHistoryBarContent(): string {
|
||||
<div class="conversation-history-bar">
|
||||
<div class="history-dropdown-container">
|
||||
<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">
|
||||
<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>
|
||||
@ -19,11 +26,20 @@ export function getConversationHistoryBarContent(): string {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-actions">
|
||||
<button class="new-conversation-button" onclick="createNewConversation()" title="新建对话">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor"/>
|
||||
</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>
|
||||
`;
|
||||
}
|
||||
@ -49,13 +65,59 @@ export function getConversationHistoryBarStyles(): string {
|
||||
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 {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
background: transparent;
|
||||
color: var(--vscode-input-foreground);
|
||||
color: var(--vscode-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
@ -64,7 +126,7 @@ export function getConversationHistoryBarStyles(): string {
|
||||
}
|
||||
|
||||
.history-dropdown-button:hover {
|
||||
opacity: 0.8;
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
.dropdown-label {
|
||||
@ -163,7 +225,7 @@ export function getConversationHistoryBarStyles(): string {
|
||||
background: transparent;
|
||||
color: var(--vscode-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -173,11 +235,12 @@ export function getConversationHistoryBarStyles(): string {
|
||||
}
|
||||
|
||||
.new-conversation-button:hover {
|
||||
opacity: 0.7;
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.new-conversation-button:active {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.new-conversation-button svg {
|
||||
@ -210,6 +273,29 @@ export function getConversationHistoryBarStyles(): string {
|
||||
*/
|
||||
export function getConversationHistoryBarScript(): string {
|
||||
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 currentConversationId = null;
|
||||
|
||||
@ -760,16 +760,39 @@ export function getMessageAreaScript(): string {
|
||||
return toolNameMap[toolName] || toolName;
|
||||
}
|
||||
|
||||
// 自动滚动控制标志
|
||||
let shouldAutoScroll = true;
|
||||
let lastScrollHeight = 0;
|
||||
|
||||
// 检查用户是否在底部附近(允许50px的误差)
|
||||
function isUserNearBottom() {
|
||||
const threshold = 50;
|
||||
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() {
|
||||
if (isUserNearBottom()) {
|
||||
if (shouldAutoScroll) {
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
lastScrollHeight = messagesEl.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ export function getProgressBarContent(): string {
|
||||
<span class="step-number">1</span>
|
||||
<span class="step-check">✓</span>
|
||||
</div>
|
||||
<div class="step-label">Spec设计文档</div>
|
||||
<div class="step-label">Spec</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-line"></div>
|
||||
@ -35,7 +35,7 @@ export function getProgressBarContent(): string {
|
||||
<span class="step-number">2</span>
|
||||
<span class="step-check">✓</span>
|
||||
</div>
|
||||
<div class="step-label">Design代码编写</div>
|
||||
<div class="step-label">Design</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-line"></div>
|
||||
@ -45,7 +45,7 @@ export function getProgressBarContent(): string {
|
||||
<span class="step-number">3</span>
|
||||
<span class="step-check">✓</span>
|
||||
</div>
|
||||
<div class="step-label">Sim仿真检查</div>
|
||||
<div class="step-label">Simulation</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-line"></div>
|
||||
@ -55,7 +55,7 @@ export function getProgressBarContent(): string {
|
||||
<span class="step-number">4</span>
|
||||
<span class="step-check">✓</span>
|
||||
</div>
|
||||
<div class="step-label">Done完成</div>
|
||||
<div class="step-label">Done</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();
|
||||
});
|
||||
}
|
||||
});
|
||||
`;
|
||||
}
|
||||
@ -347,7 +347,7 @@ export function getWaveformPreviewScript(): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开完整波形查看器
|
||||
* 打开完整波形查看器(在新列中)
|
||||
*/
|
||||
function openFullWaveform(vcdFilePath) {
|
||||
vscode.postMessage({
|
||||
|
||||
@ -585,6 +585,27 @@ export function getWebviewContent(
|
||||
}
|
||||
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':
|
||||
// 重置分段消息容器(停止对话时调用)
|
||||
console.log('[WebView] 重置分段消息容器');
|
||||
@ -611,6 +632,21 @@ export function getWebviewContent(
|
||||
}
|
||||
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':
|
||||
// 渲染迷你波形预览信息
|
||||
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()}
|
||||
${getAgentCardScript()}
|
||||
${getWaveformPreviewScript()}
|
||||
|
||||
Reference in New Issue
Block a user