diff --git a/src/assets/titleIcon/PRO+.png b/src/assets/titleIcon/PRO+.png new file mode 100644 index 0000000..0ed3620 Binary files /dev/null and b/src/assets/titleIcon/PRO+.png differ diff --git a/src/assets/titleIcon/PRO-Try.png b/src/assets/titleIcon/PRO-Try.png new file mode 100644 index 0000000..ba7fe75 Binary files /dev/null and b/src/assets/titleIcon/PRO-Try.png differ diff --git a/src/assets/titleIcon/PRO.png b/src/assets/titleIcon/PRO.png new file mode 100644 index 0000000..a6d6dfc Binary files /dev/null and b/src/assets/titleIcon/PRO.png differ diff --git a/src/assets/titleIcon/free.png b/src/assets/titleIcon/free.png new file mode 100644 index 0000000..d7c0614 Binary files /dev/null and b/src/assets/titleIcon/free.png differ diff --git a/src/config/settings.ts b/src/config/settings.ts index 40eda58..2829944 100644 --- a/src/config/settings.ts +++ b/src/config/settings.ts @@ -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 = { /** 本地开发环境 */ 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 = { /** 测试服务器环境 */ 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 = { /** 生产环境 */ 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}`; +} diff --git a/src/constants/toolIcons.ts b/src/constants/toolIcons.ts index f8ad1a7..f551a16 100644 --- a/src/constants/toolIcons.ts +++ b/src/constants/toolIcons.ts @@ -175,3 +175,8 @@ export const stateTransitionIconSvg = ` * 用户提问图标 SVG */ export const userQuestionIconSvg = ``; + +/** + * 用户头像图标 SVG + */ +export const userAvatarIconSvg = ``; diff --git a/src/extension.ts b/src/extension.ts index 3603e8a..e1c67b4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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: 等待重新实现这些命令 diff --git a/src/panels/ICHelperPanel.ts b/src/panels/ICHelperPanel.ts index 2d7978e..04f966f 100644 --- a/src/panels/ICHelperPanel.ts +++ b/src/panels/ICHelperPanel.ts @@ -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 = { + 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,14 +250,14 @@ export async function showICHelperPanel( historyManager.switchToPanelTask(panelId); // 显示进度条 - panel.webview.postMessage({ type: 'showProgress' }); + panel.webview.postMessage({ type: "showProgress" }); handleUserMessage( panel, message.text, context.extensionPath, message.mode, - message.model // 传递服务等级 + message.model // 传递服务等级 ); break; case "readFile": @@ -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, diff --git a/src/panels/VCDViewerPanel.ts b/src/panels/VCDViewerPanel.ts index f2d7455..477c4a0 100644 --- a/src/panels/VCDViewerPanel.ts +++ b/src/panels/VCDViewerPanel.ts @@ -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) { diff --git a/src/services/apiClient.ts b/src/services/apiClient.ts index bbc312d..c94d577 100644 --- a/src/services/apiClient.ts +++ b/src/services/apiClient.ts @@ -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 { + console.log('[API] 获取用户信息'); + return request('/system/user/getInfo', { + method: 'GET' + }); +} diff --git a/src/services/icCoderAuthProvider.ts b/src/services/icCoderAuthProvider.ts index 05157c0..ce8e2f4 100644 --- a/src/services/icCoderAuthProvider.ts +++ b/src/services/icCoderAuthProvider.ts @@ -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: [], diff --git a/src/services/userService.ts b/src/services/userService.ts new file mode 100644 index 0000000..c02a2e6 --- /dev/null +++ b/src/services/userService.ts @@ -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; + body?: unknown; + timeout?: number; + token?: string; +} + +/** + * 发送 HTTP 请求(带 token) + */ +async function request(path: string, options: RequestOptions): Promise { + const url = new URL(getStrangeLoopApiUrl(path)); + const { timeout } = getConfig(); + + const isHttps = url.protocol === 'https:'; + const httpModule = isHttps ? https : http; + + const headers: Record = { + '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 { + 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(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 { + 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(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 = { + '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 { + 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 { + 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('icCoderUserInfo') || null; +} + +/** + * 清除持久化存储的用户信息 + */ +export async function clearUserInfo(): Promise { + if (!extensionContext) { + console.warn('[UserService] ExtensionContext 未初始化'); + return; + } + await extensionContext.globalState.update('icCoderUserInfo', undefined); + console.log('[UserService] 用户信息已清除'); +} diff --git a/src/services/vcdFileServer.ts b/src/services/vcdFileServer.ts index 1afff27..4bcdfca 100644 --- a/src/services/vcdFileServer.ts +++ b/src/services/vcdFileServer.ts @@ -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 = 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 ` + + + + + Surfer 波形查看器 - ${fileName} + + `; + } + + private getHtmlPart2(vcdUrl: string, scopeNamesJson: string): string { + return ` + `; + } + + private getHtmlPart3(): string { + return ` + + + + + + + + +`; + } } diff --git a/src/types/api.ts b/src/types/api.ts index e432aee..d68968e 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -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; +} + // ============== 辅助类型 ============== /** 后端工具名称 */ diff --git a/src/views/agentCard.ts b/src/views/agentCard.ts index 2dbd82e..0f6eabd 100644 --- a/src/views/agentCard.ts +++ b/src/views/agentCard.ts @@ -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; } `; } diff --git a/src/views/conversationHistoryBar.ts b/src/views/conversationHistoryBar.ts index b429620..1e8cde0 100644 --- a/src/views/conversationHistoryBar.ts +++ b/src/views/conversationHistoryBar.ts @@ -1,3 +1,10 @@ +import { + getUserInfoComponentContent, + getUserInfoComponentStyles, + getUserInfoComponentScript, +} from "./userInfoComponent"; +import { userAvatarIconSvg } from "../constants/toolIcons"; + /** * 获取会话历史栏的 HTML 内容 */ @@ -6,7 +13,7 @@ export function getConversationHistoryBarContent(): string {
- +
+ + + +
`; } @@ -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; diff --git a/src/views/messageArea.ts b/src/views/messageArea.ts index 843bd82..8350e2c 100644 --- a/src/views/messageArea.ts +++ b/src/views/messageArea.ts @@ -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; } } diff --git a/src/views/progressBar.ts b/src/views/progressBar.ts index 2646761..9dc1621 100644 --- a/src/views/progressBar.ts +++ b/src/views/progressBar.ts @@ -25,7 +25,7 @@ export function getProgressBarContent(): string { 1 -
Spec设计文档
+
Spec
@@ -35,7 +35,7 @@ export function getProgressBarContent(): string { 2 -
Design代码编写
+
Design
@@ -45,7 +45,7 @@ export function getProgressBarContent(): string { 3 -
Sim仿真检查
+
Simulation
@@ -55,7 +55,7 @@ export function getProgressBarContent(): string { 4 -
Done完成
+
Done
diff --git a/src/views/userInfoComponent.ts b/src/views/userInfoComponent.ts new file mode 100644 index 0000000..444ad59 --- /dev/null +++ b/src/views/userInfoComponent.ts @@ -0,0 +1,287 @@ +/** + * 用户信息组件 + * 包含用户头像、昵称、会员等级等信息 + */ + +/** + * 获取用户信息组件的 HTML 内容 + * 只包含用户详情下拉面板,不包含触发按钮 + */ +export function getUserInfoComponentContent(): string { + return ` + + `; +} + +/** + * 获取用户信息组件的 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(); + }); + } + }); + `; +} diff --git a/src/views/waveformPreviewContent.ts b/src/views/waveformPreviewContent.ts index 5bc1586..73135ea 100644 --- a/src/views/waveformPreviewContent.ts +++ b/src/views/waveformPreviewContent.ts @@ -347,7 +347,7 @@ export function getWaveformPreviewScript(): string { } /** - * 打开完整波形查看器 + * 打开完整波形查看器(在新列中) */ function openFullWaveform(vcdFilePath) { vscode.postMessage({ diff --git a/src/views/webviewContent.ts b/src/views/webviewContent.ts index 90eda69..7fe2fea 100644 --- a/src/views/webviewContent.ts +++ b/src/views/webviewContent.ts @@ -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()}