diff --git a/src/extension.ts b/src/extension.ts
index ba00e75..50b32d2 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -310,6 +310,8 @@ export async function activate(context: vscode.ExtensionContext) {
logoutCommand,
changeInvitationCodeCommand,
testNotificationCommand,
+ testTrialUserCommand,
+ testExpiredUserCommand,
// TODO: 等待重新实现这些命令
// viewHistoryCommand,
// newSessionCommand,
diff --git a/src/panels/ExpiredPanel.ts b/src/panels/ExpiredPanel.ts
new file mode 100644
index 0000000..f0f7361
--- /dev/null
+++ b/src/panels/ExpiredPanel.ts
@@ -0,0 +1,78 @@
+/**
+ * 试用期到期提醒面板
+ * 功能:试用期到期时显示续费提示
+ * 依赖:vscode
+ * 使用场景:试用用户到期时显示
+ */
+
+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 天试用期已结束。
+ 如需继续使用,请联系我们获取正式版本。
+
+
+
+
+
+
+ `;
+ }
+}
diff --git a/src/panels/ICHelperPanel.ts b/src/panels/ICHelperPanel.ts
index 5d23714..9148914 100644
--- a/src/panels/ICHelperPanel.ts
+++ b/src/panels/ICHelperPanel.ts
@@ -426,12 +426,55 @@ export async function showICHelperPanel(
case "checkInvitationCode":
// 检查邀请码验证状态
{
- const { InvitationService } = require("../services/invitationService");
- const isVerified = await InvitationService.isVerified(context);
- panel.webview.postMessage({
- command: "invitationCodeStatus",
- verified: isVerified
- });
+ // 先检查是否是试用用户
+ const { getCachedUserInfo } = require("../services/userService");
+ const userInfo = getCachedUserInfo();
+
+ if (userInfo?.isPluginTrial === true) {
+ // 试用用户,跳过邀请码验证,直接返回已验证
+ console.log('[ICHelperPanel] 试用用户,跳过邀请码验证');
+ panel.webview.postMessage({
+ command: "invitationCodeStatus",
+ verified: true
+ });
+ } else {
+ // 正式用户,检查邀请码
+ const { InvitationService } = require("../services/invitationService");
+ const isVerified = await InvitationService.isVerified(context);
+ panel.webview.postMessage({
+ command: "invitationCodeStatus",
+ verified: isVerified
+ });
+ }
+ }
+ break;
+ case "checkWelcomeModal":
+ // 检查是否需要显示欢迎弹窗
+ {
+ console.log('[ICHelperPanel] 收到 checkWelcomeModal 消息');
+ const showWelcome = context.globalState.get('showWelcomeModal');
+ console.log('[ICHelperPanel] showWelcomeModal 标记值:', showWelcome);
+
+ if (showWelcome) {
+ // 清除标记并显示欢迎弹窗
+ await context.globalState.update('showWelcomeModal', undefined);
+ console.log('[ICHelperPanel] ✅ 发送 showWelcomeModal 命令到前端');
+ panel.webview.postMessage({
+ command: "showWelcomeModal"
+ });
+ } else {
+ console.log('[ICHelperPanel] showWelcomeModal 标记为 false,不显示弹窗');
+ }
+ }
+ break;
+ case "checkTrialExpiration":
+ // 检查试用期是否过期
+ {
+ console.log('[ICHelperPanel] 收到 checkTrialExpiration 消息');
+ const { TrialExpirationService } = require("../services/trialExpirationService");
+ const trialService = new TrialExpirationService(context, panel);
+ const isExpired = await trialService.checkExpiration();
+ console.log('[ICHelperPanel] 试用期过期状态:', isExpired);
}
break;
case "verifyInvitationCode":
diff --git a/src/panels/WelcomePanel.ts b/src/panels/WelcomePanel.ts
new file mode 100644
index 0000000..3f60842
--- /dev/null
+++ b/src/panels/WelcomePanel.ts
@@ -0,0 +1,153 @@
+/**
+ * 欢迎引导面板
+ * 功能:插件试用用户首次登录显示使用教程
+ * 依赖:vscode
+ * 使用场景:试用用户首次登录时显示
+ */
+
+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();
+
+ // 监听来自 webview 的消息
+ this._panel.webview.onDidReceiveMessage(
+ (message) => {
+ if (message.command === 'close') {
+ this._panel.dispose();
+ }
+ },
+ null,
+ this._disposables
+ );
+
+ // 监听关闭事件
+ 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();
+ }
+ }
+ }
+}
diff --git a/src/services/trialExpirationService.ts b/src/services/trialExpirationService.ts
new file mode 100644
index 0000000..eab2219
--- /dev/null
+++ b/src/services/trialExpirationService.ts
@@ -0,0 +1,62 @@
+/**
+ * 试用期过期检测服务
+ * 功能:检查插件试用用户是否过期
+ * 依赖:vscode, userService
+ * 使用场景:用户使用功能前检查是否过期
+ */
+
+import * as vscode from 'vscode';
+import { getCachedUserInfo } from './userService';
+
+export class TrialExpirationService {
+ private context: vscode.ExtensionContext;
+ private panel?: vscode.WebviewPanel;
+
+ constructor(context: vscode.ExtensionContext, panel?: vscode.WebviewPanel) {
+ this.context = context;
+ this.panel = panel;
+ }
+
+ /**
+ * 检查是否过期
+ * @returns true=已过期,false=未过期
+ */
+ public async checkExpiration(): Promise {
+ const userInfo = getCachedUserInfo();
+
+ // 不是插件试用用户,不需要检查
+ 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 {
+ // 通知前端显示过期弹窗
+ if (this.panel) {
+ this.panel.webview.postMessage({
+ command: 'showExpiredModal'
+ });
+ console.log('[TrialExpirationService] 已通知前端显示过期弹窗');
+ } else {
+ console.warn('[TrialExpirationService] panel 未提供,无法显示过期弹窗');
+ }
+ }
+}
diff --git a/src/services/userService.ts b/src/services/userService.ts
index eb33028..7b37292 100644
--- a/src/services/userService.ts
+++ b/src/services/userService.ts
@@ -9,6 +9,8 @@ import * as vscode from 'vscode';
import { getStrangeLoopApiUrl, getConfig } from '../config/settings';
import type { UserInfoResponse, MembershipResponse, MultiMembershipVO, MembershipItemVO } from '../types/api';
import { fetchBalanceWithToken, getCachedBalance } from './creditsService';
+import { getIsPluginTrialFromToken } from '../utils/jwtUtils';
+// 移除 WelcomePanel 导入,改用消息通知方式
/**
* HTTP 请求选项
@@ -117,6 +119,10 @@ export interface UserInfo {
};
// Credits 余额
credits?: number;
+ // 插件试用用户标识(从 JWT token 中提取)
+ isPluginTrial?: boolean;
+ // 试用到期时间(毫秒时间戳)
+ pluginTrialExpiresAt?: number;
}
/**
@@ -226,6 +232,10 @@ export async function onTokenReceived(token: string): Promise {
try {
console.log('[UserService] Token 已获取,正在获取用户信息、会员信息和余额...');
+ // 从 token 中提取 ispluginTrial 标识
+ const isPluginTrial = getIsPluginTrialFromToken(token);
+ console.log('[UserService] 从 token 中提取 ispluginTrial:', isPluginTrial);
+
// 并行获取用户信息、会员信息和余额
const [userInfo, membershipInfo, credits] = await Promise.all([
getUserInfo(token),
@@ -238,6 +248,12 @@ export async function onTokenReceived(token: string): Promise {
return null;
}
+ // 将 token 中的 ispluginTrial 标识添加到用户信息
+ if (isPluginTrial !== null) {
+ userInfo.isPluginTrial = isPluginTrial;
+ console.log('[UserService] 已将 ispluginTrial 添加到用户信息:', isPluginTrial);
+ }
+
// 添加 Credits 余额到用户信息
console.log('[UserService] 获取到的 Credits 余额:', credits);
if (credits !== null) {
@@ -313,6 +329,34 @@ export async function onTokenReceived(token: string): Promise {
// 保存到持久化存储
await saveUserInfo(userInfo);
+ // 判断是否是插件试用用户
+ console.log('[UserService] 检查用户类型,isPluginTrial:', userInfo.isPluginTrial);
+ console.log('[UserService] extensionContext 是否存在:', !!extensionContext);
+
+ if (userInfo.isPluginTrial === true) {
+ // 插件试用用户:标记需要显示欢迎弹窗
+ const hasWelcomed = extensionContext?.globalState.get('pluginTrialWelcomed');
+ console.log('[UserService] 是否已显示过欢迎弹窗:', hasWelcomed);
+
+ if (!hasWelcomed && extensionContext) {
+ // 设置标记,让聊天面板显示欢迎弹窗
+ await extensionContext.globalState.update('showWelcomeModal', true);
+ await extensionContext.globalState.update('pluginTrialWelcomed', true);
+ console.log('[UserService] ✅ 已设置欢迎弹窗标记 showWelcomeModal=true');
+
+ // 验证标记是否设置成功
+ const checkMark = extensionContext.globalState.get('showWelcomeModal');
+ console.log('[UserService] 验证标记:', checkMark);
+ } else if (!extensionContext) {
+ console.error('[UserService] ❌ extensionContext 为 null,无法设置标记');
+ } else {
+ console.log('[UserService] 已经显示过欢迎弹窗,跳过');
+ }
+ } else {
+ // 正式用户:显示邀请码弹窗(现有逻辑)
+ console.log('[UserService] 正式用户登录,将在面板中检查邀请码');
+ }
+
return userInfo;
} catch (error) {
console.error('[UserService] 获取用户信息失败:', error);
diff --git a/src/utils/jwtUtils.ts b/src/utils/jwtUtils.ts
index f3eb7b7..7858276 100644
--- a/src/utils/jwtUtils.ts
+++ b/src/utils/jwtUtils.ts
@@ -11,6 +11,7 @@ export interface JwtPayload {
user_id?: number; // 用户ID (下划线命名)
exp?: number; // 过期时间
iat?: number; // 签发时间
+ ispluginTrial?: boolean; // 是否是插件试用用户
[key: string]: unknown;
}
@@ -102,3 +103,24 @@ export function isTokenExpired(
return isExpired;
}
+
+/**
+ * 从 JWT token 中获取 ispluginTrial 标识
+ * @param token JWT token
+ * @returns true=插件试用用户,false=正式用户,null=无法判断
+ */
+export function getIsPluginTrialFromToken(token: string): boolean | null {
+ const payload = parseJwtPayload(token);
+ if (!payload) {
+ return null;
+ }
+
+ // 检查 ispluginTrial 字段
+ if (payload.ispluginTrial !== undefined) {
+ console.log("[JWT] 从 token 中获取到 ispluginTrial:", payload.ispluginTrial);
+ return payload.ispluginTrial === true;
+ }
+
+ console.log("[JWT] token 中没有 ispluginTrial 字段,判定为正式用户");
+ return false;
+}
diff --git a/src/utils/messageHandler.ts b/src/utils/messageHandler.ts
index f7647f1..46dd6bf 100644
--- a/src/utils/messageHandler.ts
+++ b/src/utils/messageHandler.ts
@@ -25,6 +25,7 @@ import {
} from "../services/creditsService";
import { optimizePrompt } from "../services/promptOptimizeService";
import { NotificationService } from "../services/notificationService";
+import { TrialExpirationService } from "../services/trialExpirationService";
import type { RunMode, ServiceTier } from "../types/api";
@@ -124,6 +125,21 @@ export async function handleUserMessage(
});
return;
}
+
+ // 检查试用期是否过期
+ const trialService = new TrialExpirationService(context, panel);
+ const isExpired = await trialService.checkExpiration();
+ if (isExpired) {
+ console.warn("[MessageHandler] 试用期已过期,阻止发送");
+
+ // 恢复输入状态
+ panel.webview.postMessage({
+ command: "updateSegments",
+ segments: [],
+ isComplete: true,
+ });
+ return;
+ }
}
// 记录用户消息到历史(允许失败,不阻塞主流程)
diff --git a/src/views/expiredModal.ts b/src/views/expiredModal.ts
new file mode 100644
index 0000000..4cfdd44
--- /dev/null
+++ b/src/views/expiredModal.ts
@@ -0,0 +1,218 @@
+/**
+ * 试用期过期弹窗
+ * 功能:在聊天面板内显示过期提醒模态窗口
+ * 依赖:无
+ * 使用场景:试用用户过期时在聊天面板内显示
+ */
+
+/**
+ * 获取过期弹窗的 HTML 内容
+ */
+export function getExpiredModalContent(logoUri?: string): string {
+ return `
+
+
+
+
+ ${logoUri ? `

` : ""}
+
+
+
+
+
如需继续使用,请联系我们获取正式版本。
+
+
+
+
+
+ `;
+}
+
+/**
+ * 获取过期弹窗的 CSS 样式
+ */
+export function getExpiredModalStyles(): string {
+ return `
+ /* 过期弹窗样式 */
+ .expired-modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 10000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 20px;
+ font-family: var(--vscode-font-family, "Segoe UI", Tahoma, Geneva, Verdana, sans-serif);
+ }
+
+ .expired-modal-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.7);
+ backdrop-filter: blur(5px);
+ animation: fadeIn 0.3s ease-out;
+ }
+
+ .expired-modal-content {
+ position: relative;
+ background: var(--vscode-editor-background);
+ border: 1px solid var(--vscode-widget-border);
+ border-radius: 12px;
+ width: 100%;
+ max-width: 450px;
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
+ animation: modalSlideIn 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
+ overflow: hidden;
+ }
+
+ .expired-logo-corner {
+ position: absolute;
+ top: 16px;
+ left: 24px;
+ height: 40px;
+ width: auto;
+ opacity: 0.9;
+ z-index: 10;
+ }
+
+ .expired-modal-header {
+ padding: 60px 32px 20px;
+ text-align: center;
+ }
+
+ .expired-icon {
+ font-size: 48px;
+ margin-bottom: 16px;
+ }
+
+ .expired-modal-header h2 {
+ margin: 0 0 12px;
+ font-size: 24px;
+ font-weight: 600;
+ color: var(--vscode-errorForeground);
+ }
+
+ .expired-modal-subtitle {
+ margin: 0;
+ font-size: 14px;
+ color: var(--vscode-descriptionForeground);
+ line-height: 1.5;
+ }
+
+ .expired-modal-body {
+ padding: 0 32px 32px;
+ text-align: center;
+ }
+
+ .expired-message {
+ font-size: 14px;
+ color: var(--vscode-descriptionForeground);
+ margin: 20px 0;
+ line-height: 1.6;
+ }
+
+ .expired-btn {
+ width: 100%;
+ padding: 12px 16px;
+ font-size: 14px;
+ font-weight: 600;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ background: var(--vscode-button-background);
+ color: var(--vscode-button-foreground);
+ transition: all 0.2s;
+ margin-top: 24px;
+ }
+
+ .expired-btn:hover {
+ background: var(--vscode-button-hoverBackground);
+ transform: translateY(-1px);
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
+ }
+
+ .expired-btn:active {
+ transform: translateY(0);
+ }
+ `;
+}
+
+/**
+ * 获取过期弹窗的 JavaScript 逻辑
+ */
+export function getExpiredModalScript(): string {
+ return `
+ // 过期弹窗逻辑
+ (function() {
+ const modal = document.getElementById('expiredModal');
+ const contactBtn = document.getElementById('expiredContactBtn');
+ const overlay = modal?.querySelector('.expired-modal-overlay');
+
+ // 显示过期弹窗
+ window.showExpiredModal = function() {
+ if (modal) {
+ modal.style.display = 'flex';
+ }
+ };
+
+ // 隐藏过期弹窗
+ window.hideExpiredModal = function() {
+ if (modal) {
+ modal.style.display = 'none';
+ }
+ };
+
+ // 点击"联系我们"按钮
+ if (contactBtn) {
+ contactBtn.addEventListener('click', function() {
+ // 可以打开联系页面
+ // window.open('https://iccoder.com/contact', '_blank');
+ hideExpiredModal();
+ });
+ }
+
+ // 点击遮罩层关闭弹窗
+ if (overlay) {
+ overlay.addEventListener('click', function() {
+ hideExpiredModal();
+ });
+ }
+
+ // 阻止点击弹窗内容时关闭
+ const content = modal?.querySelector('.expired-modal-content');
+ if (content) {
+ content.addEventListener('click', function(e) {
+ e.stopPropagation();
+ });
+ }
+
+ // 监听来自后端的消息
+ window.addEventListener('message', function(event) {
+ const message = event.data;
+ if (message.command === 'showExpiredModal') {
+ showExpiredModal();
+ }
+ });
+ })();
+ `;
+}
+
diff --git a/src/views/inputArea.ts b/src/views/inputArea.ts
index 0d01767..7ce0dfe 100644
--- a/src/views/inputArea.ts
+++ b/src/views/inputArea.ts
@@ -339,12 +339,14 @@ export function getInputAreaScript(): string {
if (messageInput) {
messageInput.addEventListener('input', autoResizeTextarea);
- // 监听点击事件,检测工作区状态和邀请码验证状态
+ // 监听点击事件,检测工作区状态、试用期过期和邀请码验证状态
messageInput.addEventListener('focus', () => {
if (!hasCheckedWorkspace) {
hasCheckedWorkspace = true;
vscode.postMessage({ command: 'checkWorkspace' });
}
+ // 检查试用期是否过期
+ vscode.postMessage({ command: 'checkTrialExpiration' });
// 检查邀请码验证状态
vscode.postMessage({ command: 'checkInvitationCode' });
});
diff --git a/src/views/webviewContent.ts b/src/views/webviewContent.ts
index a328076..5bb4900 100644
--- a/src/views/webviewContent.ts
+++ b/src/views/webviewContent.ts
@@ -30,6 +30,16 @@ import {
getInvitationModalStyles,
getInvitationModalScript,
} from "./invitationModal";
+import {
+ getWelcomeModalContent,
+ getWelcomeModalStyles,
+ getWelcomeModalScript,
+} from "./welcomeModal";
+import {
+ getExpiredModalContent,
+ getExpiredModalStyles,
+ getExpiredModalScript,
+} from "./expiredModal";
/**
* 获取 WebView 面板的 HTML 内容
*/
@@ -100,6 +110,8 @@ export function getWebviewContent(
${getProgressBarStyles()}
${getInputAreaStyles()}
${getInvitationModalStyles()}
+ ${getWelcomeModalStyles()}
+ ${getExpiredModalStyles()}
.file-editor-section {
margin-bottom: 15px;
@@ -466,6 +478,8 @@ export function getWebviewContent(
${getConversationHistoryBarContent()}
${getProgressBarContent()}
${getInvitationModalContent(qrCodeUri, logoUri)}
+ ${getWelcomeModalContent(logoUri)}
+ ${getExpiredModalContent(logoUri)}