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:
@ -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>`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user