diff --git a/docs/插件试用用户功能实现方案.md b/docs/插件试用用户功能实现方案.md
new file mode 100644
index 0000000..b585b2a
--- /dev/null
+++ b/docs/插件试用用户功能实现方案.md
@@ -0,0 +1,783 @@
+# 插件试用用户功能实现方案
+
+## 1. 方案概述
+
+**核心思路:**
+- Web 登录成功后只返回 token(保持现状)
+- 插件调用 `getUserInfo(token)` 时,后端返回的数据里包含标识字段
+- 前端根据该字段判断是否是插件试用用户
+- 插件试用用户:显示欢迎弹窗,不显示邀请码弹窗
+- 正式用户:显示邀请码弹窗(现有逻辑)
+
+---
+
+## 2. 后端需要做什么
+
+### 2.1 在用户信息接口中添加字段
+
+**接口:** `GET /system/user/getInfo`
+
+**现有响应:**
+```json
+{
+ "userId": "xxx",
+ "username": "testuser",
+ "nickname": "测试用户",
+ "email": "test@example.com"
+}
+```
+
+**新增字段:**
+
+**方案 :添加 isPluginTrial 字段**
+
+```json
+{
+ "userId": "xxx",
+ "username": "testuser",
+ "nickname": "测试用户",
+ "email": "test@example.com",
+ "isPluginTrial": true, // ← 新增:是否是插件试用用户
+ "pluginTrialExpiresAt": 1709654400000 // ← 新增:试用到期时间(毫秒时间戳)
+}
+```
+
+### 2.2 后端逻辑说明
+
+**判断逻辑:**
+```javascript
+// 伪代码
+function getUserInfo(userId) {
+ const user = db.users.findById(userId);
+
+ // 判断是否是插件试用用户(后端自己的逻辑)
+ const isPluginTrial = checkIfPluginTrialUser(user);
+
+ return {
+ userId: user.id,
+ username: user.username,
+ nickname: user.nickname,
+ email: user.email,
+ isPluginTrial: isPluginTrial,
+ pluginTrialExpiresAt: isPluginTrial ? user.trial_expires_at : null
+ };
+}
+```
+
+**Token 过期时间:**
+- 插件试用用户:JWT Token 设置 15 天过期
+- 正式用户:JWT Token 设置 30 天过期(或现有逻辑)
+
+---
+
+## 3. 前端需要修改的地方
+
+### 3.1 修改 UserInfo 接口
+
+**文件:** `src/services/userService.ts`
+
+**现有接口:**
+```typescript
+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;
+ };
+ credits?: number;
+}
+```
+
+**新增字段:**
+```typescript
+interface UserInfo {
+ // ... 现有字段
+ isPluginTrial?: boolean; // ← 新增:是否是插件试用用户
+ pluginTrialExpiresAt?: number; // ← 新增:试用到期时间(毫秒时间戳)
+ membership?: {
+ tierCode: string;
+ tierName: string;
+ tierLevel: number;
+ remainingDays?: number;
+ monthlyCredits?: number;
+ };
+ credits?: number;
+}
+```
+
+
+### 3.2 修改 onTokenReceived() 方法
+
+**文件:** `src/services/userService.ts`
+
+**现有代码位置:** 约 200 行左右
+
+**修改内容:**
+```typescript
+async onTokenReceived(token: string) {
+ // 现有逻辑:并行获取三类信息
+ const [userInfo, membershipInfo, credits] = await Promise.all([
+ getUserInfo(token),
+ getMembershipInfo(token),
+ fetchBalanceWithToken(token)
+ ]);
+
+ // 合并数据
+ const fullUserInfo = {
+ ...userInfo,
+ membership: membershipInfo,
+ credits: credits
+ };
+
+ // 保存到 globalState
+ await this.context.globalState.update('icCoderUserInfo', fullUserInfo);
+
+ // ========== 新增逻辑 ==========
+ // 判断是否是插件试用用户
+ if (fullUserInfo.isPluginTrial === true) {
+ // 插件试用用户:显示欢迎弹窗,不显示邀请码弹窗
+ await this.showWelcomePanel();
+ // 标记为已显示欢迎弹窗(避免重复显示)
+ await this.context.globalState.update('pluginTrialWelcomed', true);
+ } else {
+ // 正式用户:显示邀请码弹窗(现有逻辑)
+ await this.checkAndShowInvitationModal();
+ }
+ // ========== 新增逻辑结束 ==========
+
+ return fullUserInfo;
+}
+```
+
+
+### 3.3 新增欢迎弹窗面板
+
+**新建文件:** `src/panels/WelcomePanel.ts`
+
+```typescript
+/**
+ * 欢迎引导面板
+ * 功能:插件试用用户首次登录显示使用教程
+ */
+
+import * as vscode from 'vscode';
+
+export class WelcomePanel {
+ public static currentPanel: WelcomePanel | undefined;
+ private readonly _panel: vscode.WebviewPanel;
+ private _disposables: vscode.Disposable[] = [];
+
+ private constructor(panel: vscode.WebviewPanel) {
+ this._panel = panel;
+ this._panel.webview.html = this.getHtmlContent();
+
+ // 监听关闭事件
+ this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
+ }
+
+ public static render(context: vscode.ExtensionContext) {
+ // 避免重复显示
+ if (WelcomePanel.currentPanel) {
+ WelcomePanel.currentPanel._panel.reveal(vscode.ViewColumn.One);
+ return;
+ }
+
+ const panel = vscode.window.createWebviewPanel(
+ 'icCoderWelcome',
+ '欢迎使用 IC Coder',
+ vscode.ViewColumn.One,
+ {
+ enableScripts: true,
+ retainContextWhenHidden: true
+ }
+ );
+
+ WelcomePanel.currentPanel = new WelcomePanel(panel);
+ }
+
+ private getHtmlContent(): string {
+ return `
+
+
+
+
+
+ 欢迎使用 IC Coder
+
+
+
+ 🎉 欢迎使用 IC Coder!
+
+ 您已成功激活 15 天试用期,让我们开始探索 IC Coder 的强大功能吧!
+
+
+
+
📝 步骤 1:打开聊天面板
+
点击侧边栏的 IC Coder 图标,或使用命令面板搜索 "IC Coder: Open Chat"
+
+
+
+
💬 步骤 2:输入您的需求
+
描述您想要生成的 Verilog 代码或需要帮助的问题,AI 将为您提供专业的解决方案
+
+
+
+
🔬 步骤 3:运行仿真
+
使用 "生成 VCD" 命令运行 iverilog 仿真,并通过波形查看器查看仿真结果
+
+
+
+
+
+
+
+ `;
+ }
+
+ public dispose() {
+ WelcomePanel.currentPanel = undefined;
+ this._panel.dispose();
+ while (this._disposables.length) {
+ const disposable = this._disposables.pop();
+ if (disposable) {
+ disposable.dispose();
+ }
+ }
+ }
+}
+```
+
+
+### 3.4 新增过期提醒面板
+
+**新建文件:** `src/panels/ExpiredPanel.ts`
+
+```typescript
+/**
+ * 试用期到期提醒面板
+ * 功能:试用期到期时显示续费提示
+ */
+
+import * as vscode from 'vscode';
+
+export class ExpiredPanel {
+ public static render() {
+ const panel = vscode.window.createWebviewPanel(
+ 'icCoderExpired',
+ '试用期已到期',
+ vscode.ViewColumn.One,
+ { enableScripts: true }
+ );
+
+ panel.webview.html = this.getHtmlContent();
+ }
+
+ private static getHtmlContent(): string {
+ return `
+
+
+
+
+
+
+
+ ⏰ 您的试用期已到期
+ 感谢您使用 IC Coder!您的 15 天试用期已结束。
+ 如需继续使用,请联系我们获取正式版本。
+
+
+
+
+
+
+ `;
+ }
+}
+```
+
+
+### 3.5 新增过期检测服务
+
+**新建文件:** `src/services/trialExpirationService.ts`
+
+```typescript
+/**
+ * 试用期过期检测服务
+ * 功能:检查插件试用用户是否过期
+ */
+
+import * as vscode from 'vscode';
+import { getUserInfo } from './userService';
+import { ExpiredPanel } from '../panels/ExpiredPanel';
+
+export class TrialExpirationService {
+ private context: vscode.ExtensionContext;
+
+ constructor(context: vscode.ExtensionContext) {
+ this.context = context;
+ }
+
+ /**
+ * 检查是否过期
+ * @returns true=已过期,false=未过期
+ */
+ public async checkExpiration(): Promise {
+ const userInfo = await getUserInfo();
+
+ // 不是插件试用用户,不需要检查
+ if (!userInfo?.isPluginTrial) {
+ return false;
+ }
+
+ // 没有过期时间,不检查
+ if (!userInfo.pluginTrialExpiresAt) {
+ return false;
+ }
+
+ // 检查是否过期
+ const now = Date.now();
+ if (now >= userInfo.pluginTrialExpiresAt) {
+ // 已过期
+ await this.handleExpired();
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 处理过期逻辑
+ */
+ private async handleExpired(): Promise {
+ // 显示过期弹窗
+ ExpiredPanel.render();
+
+ // 清除本地数据(可选)
+ // await this.context.globalState.update('icCoderUserInfo', undefined);
+ // await this.context.globalState.update('icCoderSessions', undefined);
+ }
+}
+```
+
+
+### 3.6 在消息发送前检查过期
+
+**文件:** `src/utils/messageHandler.ts`
+
+**修改位置:** 在发送消息给后端之前添加过期检查
+
+```typescript
+import { TrialExpirationService } from '../services/trialExpirationService';
+
+// 在 handleUserMessage 或类似的消息处理函数中添加
+async function handleUserMessage(message: string, context: vscode.ExtensionContext) {
+ // ========== 新增:检查试用期是否过期 ==========
+ const trialService = new TrialExpirationService(context);
+ const isExpired = await trialService.checkExpiration();
+
+ if (isExpired) {
+ // 已过期,禁止使用
+ return {
+ success: false,
+ message: '您的试用期已到期,请联系我们获取正式版本'
+ };
+ }
+ // ========== 新增结束 ==========
+
+ // 现有的消息处理逻辑
+ // ...
+}
+```
+
+
+---
+
+## 4. 完整的实现流程
+
+### 4.1 登录流程(带过期检查)
+
+```
+1. 用户点击登录
+ ↓
+2. 打开浏览器,Web 端登录
+ ↓
+3. 重定向回插件:http://localhost:{port}/callback?token={token}
+ ↓
+4. 插件调用 onTokenReceived(token)
+ ↓
+5. 并行获取:getUserInfo + getMembershipInfo + Credits
+ ↓
+6. 后端返回 userInfo(包含 isPluginTrial 和 pluginTrialExpiresAt)
+ ↓
+7. 判断 isPluginTrial === true?
+ ├─ 是:显示欢迎弹窗,不显示邀请码弹窗
+ └─ 否:显示邀请码弹窗(现有逻辑)
+ ↓
+8. 保存用户信息到 globalState
+```
+
+### 4.2 使用功能时的过期检查
+
+```
+1. 用户发送消息/使用功能
+ ↓
+2. 调用 trialService.checkExpiration()
+ ↓
+3. 获取 userInfo,检查 isPluginTrial
+ ↓
+4. 如果是插件试用用户,检查 Date.now() >= pluginTrialExpiresAt?
+ ├─ 是:显示过期弹窗,禁止使用,返回 true
+ └─ 否:允许使用,返回 false
+ ↓
+5. 继续正常的消息处理流程
+```
+
+
+---
+
+## 5. ⚠️ 潜在 Bug 和注意事项
+
+### 5.1 时间同步问题
+
+**问题:** 前端使用 `Date.now()` 判断过期,如果用户本地时间不准确会导致误判
+
+**场景:**
+- 用户本地时间快了 1 天 → 提前显示过期弹窗
+- 用户本地时间慢了 1 天 → 过期后仍可使用
+
+**解决方案:**
+```typescript
+// 方案 1:每次使用前调用后端验证(推荐)
+async checkExpiration(): Promise {
+ try {
+ // 调用后端接口验证 Token 是否过期
+ const response = await fetch(`${API_BASE}/auth/verify`, {
+ headers: { 'Authorization': `Bearer ${token}` }
+ });
+
+ if (response.status === 401) {
+ // Token 过期
+ await this.handleExpired();
+ return true;
+ }
+ return false;
+ } catch (error) {
+ // 网络错误,使用本地时间判断
+ const userInfo = await getUserInfo();
+ return Date.now() >= (userInfo?.pluginTrialExpiresAt || 0);
+ }
+}
+```
+
+
+### 5.2 欢迎弹窗重复显示
+
+**问题:** 每次登录都显示欢迎弹窗,用户体验不好
+
+**场景:**
+- 试用用户第一次登录显示欢迎弹窗 ✅
+- 用户关闭插件后重新打开,又显示欢迎弹窗 ❌
+
+**解决方案:**
+```typescript
+async onTokenReceived(token: string) {
+ // ... 获取用户信息
+
+ if (fullUserInfo.isPluginTrial === true) {
+ // 检查是否已经显示过欢迎弹窗
+ const hasWelcomed = this.context.globalState.get('pluginTrialWelcomed');
+
+ if (!hasWelcomed) {
+ await this.showWelcomePanel();
+ await this.context.globalState.update('pluginTrialWelcomed', true);
+ }
+ } else {
+ await this.checkAndShowInvitationModal();
+ }
+}
+```
+
+
+### 5.3 isPluginTrial 字段类型不一致
+
+**问题:** 后端可能返回 `true`、`false`、`null`、`undefined`,前端判断时需要严格处理
+
+**场景:**
+```typescript
+// ❌ 错误写法
+if (userInfo.isPluginTrial) {
+ // undefined 会被判断为 false
+}
+
+// ✅ 正确写法
+if (userInfo.isPluginTrial === true) {
+ // 插件试用用户
+}
+```
+
+**建议:**
+- 后端统一返回 `true` 或 `false`,不要返回 `null`
+- 前端使用严格相等 `===` 判断
+
+
+### 5.4 Token 过期但前端未清除
+
+**问题:** Token 在后端已过期,但前端仍保存着过期的 Token
+
+**场景:**
+- 用户 15 天后打开插件
+- 前端尝试调用 API,后端返回 401
+- 前端没有处理 401,导致功能异常
+
+**解决方案:**
+```typescript
+// 在 apiClient.ts 中统一处理 401
+async function apiCall(url: string, options: any) {
+ const response = await fetch(url, options);
+
+ if (response.status === 401) {
+ // Token 过期,清除本地数据
+ await clearAllData();
+ ExpiredPanel.render();
+ throw new Error('Token expired');
+ }
+
+ return response.json();
+}
+```
+
+
+---
+
+## 6. 前后端对接清单
+
+### 6.1 后端需要提供
+
+**1. 修改 `GET /system/user/getInfo` 接口**
+- 新增字段:`isPluginTrial` (boolean)
+- 新增字段:`pluginTrialExpiresAt` (number, 毫秒时间戳)
+
+**2. Token 过期时间设置**
+- 插件试用用户:JWT Token 设置 15 天过期
+- 正式用户:保持现有逻辑
+
+**3. 测试账号**
+- 提供 1-2 个插件试用用户账号用于测试
+
+### 6.2 前端需要修改
+
+**1. 修改文件:**
+- `src/services/userService.ts` - 修改 UserInfo 接口和 onTokenReceived()
+- `src/utils/messageHandler.ts` - 添加过期检查
+
+**2. 新增文件:**
+- `src/panels/WelcomePanel.ts` - 欢迎弹窗
+- `src/panels/ExpiredPanel.ts` - 过期提醒弹窗
+- `src/services/trialExpirationService.ts` - 过期检测服务
+
+**3. globalState 新增存储键:**
+- `pluginTrialWelcomed` (boolean) - 是否已显示欢迎弹窗
+
+
+---
+
+## 7. 测试计划
+
+### 7.1 登录流程测试
+
+**测试用例 1:插件试用用户登录**
+- 使用插件试用用户账号登录
+- 验证是否显示欢迎弹窗
+- 验证是否不显示邀请码弹窗
+- 验证 userInfo 中 isPluginTrial === true
+
+**测试用例 2:正式用户登录**
+- 使用正式用户账号登录
+- 验证是否显示邀请码弹窗
+- 验证是否不显示欢迎弹窗
+- 验证 userInfo 中 isPluginTrial !== true
+
+**测试用例 3:欢迎弹窗不重复显示**
+- 插件试用用户登录后显示欢迎弹窗
+- 关闭插件重新打开
+- 验证不再显示欢迎弹窗
+
+
+### 7.2 过期检测测试
+
+**测试用例 4:未过期用户正常使用**
+- 插件试用用户登录(未过期)
+- 发送消息使用功能
+- 验证功能正常使用
+
+**测试用例 5:已过期用户禁止使用**
+- 修改本地时间到 15 天后(或修改 pluginTrialExpiresAt)
+- 尝试发送消息
+- 验证显示过期弹窗
+- 验证功能被禁用
+
+**测试用例 6:Token 过期处理**
+- 使用过期的 Token 调用 API
+- 验证后端返回 401
+- 验证前端显示过期弹窗
+
+
+---
+
+## 8. 总结
+
+### 8.1 核心改动点
+
+**后端(最小改动):**
+1. `GET /system/user/getInfo` 接口新增 2 个字段
+2. JWT Token 根据用户类型设置不同过期时间
+
+**前端(主要改动):**
+1. 修改 UserInfo 接口定义
+2. 修改 onTokenReceived() 添加判断逻辑
+3. 新增 3 个文件(欢迎面板、过期面板、过期检测服务)
+4. 在消息发送前添加过期检查
+
+### 8.2 关键判断逻辑
+
+```typescript
+// 登录后判断
+if (userInfo.isPluginTrial === true) {
+ showWelcomePanel(); // 显示欢迎弹窗
+} else {
+ showInvitationModal(); // 显示邀请码弹窗
+}
+
+// 使用前判断
+if (Date.now() >= userInfo.pluginTrialExpiresAt) {
+ showExpiredPanel(); // 显示过期弹窗
+ return; // 禁止使用
+}
+```
+
+
+### 8.3 必须注意的问题
+
+1. **时间同步问题** - 用户本地时间不准确导致误判,建议调用后端验证
+2. **isPluginTrial 严格判断** - 必须使用 `=== true` 判断
+3. **欢迎弹窗重复显示** - 使用 globalState 标记避免重复
+4. **Token 过期处理** - 在 apiClient 中统一处理 401 响应
+
+### 8.4 实现优先级
+
+**P0(必须实现):**
+1. 后端接口新增字段
+2. 前端 UserInfo 接口修改
+3. 前端 onTokenReceived() 判断逻辑
+4. 过期检测逻辑
+
+**P1(重要):**
+1. 欢迎弹窗
+2. 过期提醒弹窗
+
+**P2(优化):**
+1. 后端验证过期(避免时间同步问题)
+2. 更友好的过期提示
+
+---
+
+**文档完成!** 基于你现有的代码架构,这个方案改动最小,实现最简单。
+
diff --git a/src/panels/ICHelperPanel.ts b/src/panels/ICHelperPanel.ts
index 722b100..5d23714 100644
--- a/src/panels/ICHelperPanel.ts
+++ b/src/panels/ICHelperPanel.ts
@@ -20,6 +20,7 @@ import { ChatHistoryManager } from "../utils/chatHistoryManager";
import { MessageType } from "../types/chatHistory";
import { getCachedUserInfo } from "../services/userService";
import { isTokenExpired } from "../utils/jwtUtils";
+import { setBalanceUpdateCallback } from "../services/creditsService";
/**
* 获取会员等级图标 URI
@@ -222,6 +223,26 @@ export async function showICHelperPanel(
console.error('[ICHelperPanel] 获取用户信息失败:', error);
}
+ // 设置余额更新回调
+ setBalanceUpdateCallback((balance: number) => {
+ const userInfo = getCachedUserInfo();
+ if (userInfo) {
+ userInfo.credits = balance;
+ const tierIconUrl = getTierIconUri(panel.webview, context, userInfo.membership?.tierCode);
+ panel.webview.postMessage({
+ command: 'updateUserInfo',
+ userInfo: {
+ userId: userInfo.userId,
+ nickname: userInfo.nickname,
+ username: userInfo.username,
+ credits: balance,
+ membership: userInfo.membership
+ },
+ tierIconUrl: tierIconUrl
+ });
+ }
+ });
+
// 检查是否有待发送的消息
const pendingMessage = context.globalState.get('pendingMessage') as any;
if (pendingMessage) {
diff --git a/src/services/creditsService.ts b/src/services/creditsService.ts
index ae5d341..ad0f8de 100644
--- a/src/services/creditsService.ts
+++ b/src/services/creditsService.ts
@@ -25,6 +25,9 @@ const CACHE_TTL_MS = 5 * 60 * 1000;
/** ExtensionContext 用于持久化存储 */
let extensionContext: vscode.ExtensionContext | null = null;
+/** 余额更新回调函数 */
+let onBalanceUpdateCallback: ((balance: number) => void) | null = null;
+
/**
* 初始化 Credits 服务(设置 context)
*/
@@ -39,6 +42,13 @@ export function initCreditsService(context: vscode.ExtensionContext): void {
}
}
+/**
+ * 设置余额更新回调
+ */
+export function setBalanceUpdateCallback(callback: (balance: number) => void): void {
+ onBalanceUpdateCallback = callback;
+}
+
/**
* 保存余额到持久化存储
*/
@@ -60,6 +70,10 @@ export function updateCachedBalance(balance: number): void {
saveBalance(balance).catch(err => {
console.error('[CreditsService] 保存余额失败:', err);
});
+ // 通知前端更新余额显示
+ if (onBalanceUpdateCallback) {
+ onBalanceUpdateCallback(balance);
+ }
}
/**
diff --git a/src/views/planCard.ts b/src/views/planCard.ts
index 706ec37..d8dca73 100644
--- a/src/views/planCard.ts
+++ b/src/views/planCard.ts
@@ -720,7 +720,7 @@ export function getPlanCardScript(): string {
segmentDiv.innerHTML = \`
\${progressHtml}