From 032dd1b21507a77ef09767b641d989af4ebacc8d Mon Sep 17 00:00:00 2001 From: Roe-xin Date: Tue, 27 Jan 2026 14:40:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E9=82=80=E8=AF=B7?= =?UTF-8?q?=E7=A0=81=E9=AA=8C=E8=AF=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 功能概述 - 用户首次使用需验证邀请码才能发起对话 - 在输入框聚焦和点击示例时触发验证检查 - 使用弹窗形式展示邀请码输入界面,包含企业端用户提示和微信二维码 ## 主要变更 ### 新增文件 - `services/invitationService.ts`: 邀请码验证服务,处理验证逻辑和状态管理 - `views/invitationModal.ts`: 邀请码验证弹窗组件(HTML/CSS/JS) - `docs/invitation-code-design.md`: 邀请码功能设计文档 ### 修改文件 - `extension.ts`: 添加更换邀请码命令,退出登录时清除验证状态 - `panels/ICHelperPanel.ts`: 添加邀请码验证状态检查和验证消息处理 - `services/apiClient.ts`: 添加邀请码验证接口调用 - `types/api.ts`: 添加邀请码相关类型定义 - `views/inputArea.ts`: 输入框聚焦时触发邀请码验证检查 - `views/exampleShowcase.ts`: 点击示例时先检查邀请码验证状态 - `views/webviewContent.ts`: 集成邀请码弹窗到主界面 ## 技术实现 - 验证状态保存在 ExtensionContext.globalState 中 - 使用后端接口 POST /api/invitation/verify 进行验证 - 弹窗样式适配 VS Code 主题 - 支持回车键提交验证 --- docs/invitation-code-design.md | 739 ++++++++++++++++++++++++++++++ src/extension.ts | 21 + src/panels/ICHelperPanel.ts | 34 ++ src/services/apiClient.ts | 26 +- src/services/invitationService.ts | 91 ++++ src/types/api.ts | 46 ++ src/views/exampleShowcase.ts | 4 +- src/views/inputArea.ts | 4 +- src/views/invitationModal.ts | 321 +++++++++++++ src/views/webviewContent.ts | 8 + 10 files changed, 1290 insertions(+), 4 deletions(-) create mode 100644 docs/invitation-code-design.md create mode 100644 src/services/invitationService.ts create mode 100644 src/views/invitationModal.ts diff --git a/docs/invitation-code-design.md b/docs/invitation-code-design.md new file mode 100644 index 0000000..7b8d8f3 --- /dev/null +++ b/docs/invitation-code-design.md @@ -0,0 +1,739 @@ +# 邀请码验证功能设计方案 + +## 一、整体流程 + +``` +用户首次使用 → 检查邀请码状态 → 未验证则弹窗输入 → 后端验证 → 验证通过后可正常对话 +``` + +## 二、前端设计 + +### 2.1 邀请码状态管理 + +在 `ExtensionContext.globalState` 中存储邀请码验证状态: + +```typescript +// 存储结构 +{ + "invitationCodeVerified": true, + "invitationCode": "INVITE2024ABC", + "verifiedTime": "2024-01-20T10:30:00" +} +``` + +### 2.2 UI 交互流程 + +#### 弹窗输入邀请码 +使用 `vscode.window.showInputBox` 实现: + +```typescript +const invitationCode = await vscode.window.showInputBox({ + prompt: '请输入邀请码以继续使用 IC Coder', + placeHolder: '例如:INVITE2024ABC', + ignoreFocusOut: true, + validateInput: (value) => { + if (!value || value.trim().length === 0) { + return '邀请码不能为空'; + } + if (value.length < 6) { + return '邀请码格式不正确'; + } + return null; + } +}); +``` + +#### 验证结果提示 +- 成功:`vscode.window.showInformationMessage('邀请码验证成功!')` +- 失败:`vscode.window.showErrorMessage('邀请码无效或已过期,请重新输入')` + +### 2.3 验证时机 + +在以下场景触发邀请码验证: + +1. **用户首次发送消息时**(在 `handleUserMessage` 中检查) +2. **用户登录后**(在登录成功回调中检查) +3. **Token 过期重新登录后** + +### 2.4 前端验证流程图 + +``` +发送消息前检查 +├─ 检查是否已登录 +│ └─ 未登录 → 提示登录 +├─ 检查邀请码是否已验证 +│ ├─ 未验证 +│ │ ├─ 弹窗输入邀请码 +│ │ ├─ 调用后端验证接口 POST /api/invitation/verify +│ │ ├─ 验证成功 +│ │ │ ├─ 保存验证状态到 globalState +│ │ │ └─ 继续发送消息 +│ │ └─ 验证失败 +│ │ ├─ 显示错误提示 +│ │ └─ 阻止发送消息 +│ └─ 已验证 → 继续发送消息 +``` + +### 2.5 前端文件修改清单 + +#### 新增文件 + +**`src/services/invitationService.ts`** - 邀请码服务 +```typescript +/** + * 邀请码验证服务 + */ +export class InvitationService { + /** + * 检查用户是否已验证邀请码 + */ + static async isVerified(context: vscode.ExtensionContext): Promise + + /** + * 验证邀请码 + */ + static async verifyCode(code: string): Promise + + /** + * 保存验证状态 + */ + static async saveVerificationStatus( + context: vscode.ExtensionContext, + code: string + ): Promise + + /** + * 清除验证状态(用于退出登录) + */ + static async clearVerificationStatus( + context: vscode.ExtensionContext + ): Promise + + /** + * 显示邀请码输入弹窗 + */ + static async showInputDialog(): Promise +} +``` + +#### 修改文件 + +**`src/utils/messageHandler.ts`** +- 在 `handleUserMessage` 函数开头添加邀请码验证检查 + +**`src/services/apiClient.ts`** +- 添加 `verifyInvitationCode` 函数 +- 添加 `checkInvitationStatus` 函数 + +**`src/types/api.ts`** +- 添加邀请码相关类型定义 + +**`src/panels/ICHelperPanel.ts`** +- 在面板创建时检查邀请码状态(可选) + +**`src/extension.ts`** +- 在登录成功后检查邀请码状态 + +### 2.6 类型定义 + +在 `src/types/api.ts` 中添加: + +```typescript +// ============== 邀请码验证 ============== + +/** + * 邀请码验证请求 + * POST /api/invitation/verify + */ +export interface InvitationVerifyRequest { + /** 邀请码 */ + code: string; +} + +/** + * 邀请码验证响应 + */ +export interface InvitationVerifyResponse { + /** 响应代码 */ + code: number; + /** 响应消息 */ + msg: string; + /** 验证结果数据 */ + data?: { + /** 是否验证成功 */ + verified: boolean; + }; +} + +/** + * 邀请码状态响应 + * GET /api/invitation/status + */ +export interface InvitationStatusResponse { + /** 响应代码 */ + code: number; + /** 响应消息 */ + msg?: string; + /** 状态数据 */ + data?: { + /** 是否已验证 */ + verified: boolean; + /** 使用的邀请码 */ + invitationCode?: string; + /** 验证时间 */ + verifiedTime?: string; + }; +} +``` + +## 三、后端设计 + +### 3.1 数据库设计 + +#### 邀请码表 (invitation_codes) + +```sql +CREATE TABLE invitation_codes ( + id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', + code VARCHAR(32) UNIQUE NOT NULL COMMENT '邀请码', + max_uses INT DEFAULT 1 COMMENT '最大使用次数,-1表示无限制', + used_count INT DEFAULT 0 COMMENT '已使用次数', + expire_time DATETIME COMMENT '过期时间,NULL表示永不过期', + created_by BIGINT COMMENT '创建者用户ID', + created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + status TINYINT DEFAULT 1 COMMENT '状态:1-有效,0-禁用', + remark VARCHAR(500) COMMENT '备注', + INDEX idx_code (code), + INDEX idx_status (status), + INDEX idx_expire_time (expire_time) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='邀请码表'; +``` + +#### 用户邀请码关联表 (user_invitation) + +```sql +CREATE TABLE user_invitation ( + id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', + user_id BIGINT NOT NULL COMMENT '用户ID', + invitation_code VARCHAR(32) NOT NULL COMMENT '使用的邀请码', + verified_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '验证时间', + ip_address VARCHAR(50) COMMENT '验证时的IP地址', + UNIQUE KEY uk_user_id (user_id), + INDEX idx_invitation_code (invitation_code), + INDEX idx_verified_time (verified_time) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户邀请码关联表'; +``` + +### 3.2 API 接口设计 + +#### 3.2.1 验证邀请码 + +**接口地址**:`POST /api/invitation/verify` + +**请求头**: +``` +Authorization: Bearer {token} +Content-Type: application/json +``` + +**请求体**: +```json +{ + "code": "INVITE2024ABC" +} +``` + +**响应示例**: + +成功: +```json +{ + "code": 200, + "msg": "验证成功", + "data": { + "verified": true + } +} +``` + +失败: +```json +{ + "code": 400, + "msg": "邀请码无效或已过期" +} +``` + +```json +{ + "code": 400, + "msg": "邀请码使用次数已达上限" +} +``` + +```json +{ + "code": 400, + "msg": "您已验证过邀请码,无需重复验证" +} +``` + +#### 3.2.2 查询验证状态 + +**接口地址**:`GET /api/invitation/status` + +**请求头**: +``` +Authorization: Bearer {token} +``` + +**响应示例**: + +已验证: +```json +{ + "code": 200, + "msg": "success", + "data": { + "verified": true, + "invitationCode": "INVITE2024ABC", + "verifiedTime": "2024-01-20T10:30:00" + } +} +``` + +未验证: +```json +{ + "code": 200, + "msg": "success", + "data": { + "verified": false + } +} +``` + +#### 3.2.3 管理接口(管理员使用) + +**生成邀请码**:`POST /api/admin/invitation/generate` + +请求体: +```json +{ + "count": 10, + "maxUses": 1, + "expireTime": "2024-12-31T23:59:59", + "remark": "2024年1月批次" +} +``` + +响应: +```json +{ + "code": 200, + "msg": "生成成功", + "data": { + "codes": [ + "INVITE2024001", + "INVITE2024002", + "..." + ] + } +} +``` + +**查询邀请码列表**:`GET /api/admin/invitation/list` + +**禁用邀请码**:`PUT /api/admin/invitation/disable/{code}` + +**查询使用记录**:`GET /api/admin/invitation/usage/{code}` + +### 3.3 后端验证逻辑 + +#### 验证流程 + +```java +public boolean verifyInvitationCode(Long userId, String code) { + // 1. 检查邀请码是否存在 + InvitationCode invitationCode = invitationCodeMapper.selectByCode(code); + if (invitationCode == null) { + throw new BusinessException("邀请码不存在"); + } + + // 2. 检查邀请码状态 + if (invitationCode.getStatus() != 1) { + throw new BusinessException("邀请码已被禁用"); + } + + // 3. 检查是否过期 + if (invitationCode.getExpireTime() != null + && invitationCode.getExpireTime().before(new Date())) { + throw new BusinessException("邀请码已过期"); + } + + // 4. 检查使用次数 + if (invitationCode.getMaxUses() != -1 + && invitationCode.getUsedCount() >= invitationCode.getMaxUses()) { + throw new BusinessException("邀请码使用次数已达上限"); + } + + // 5. 检查用户是否已验证过 + UserInvitation existing = userInvitationMapper.selectByUserId(userId); + if (existing != null) { + throw new BusinessException("您已验证过邀请码,无需重复验证"); + } + + // 6. 创建用户验证记录 + UserInvitation userInvitation = new UserInvitation(); + userInvitation.setUserId(userId); + userInvitation.setInvitationCode(code); + userInvitation.setVerifiedTime(new Date()); + userInvitationMapper.insert(userInvitation); + + // 7. 增加邀请码使用次数 + invitationCodeMapper.incrementUsedCount(code); + + return true; +} +``` + +### 3.4 权限拦截 + +在对话接口中添加邀请码验证拦截: + +```java +@PostMapping("/dialog/stream") +public SseEmitter dialog(@RequestBody DialogRequest request) { + // 获取当前用户ID + Long userId = SecurityUtils.getUserId(); + + // 检查用户是否已验证邀请码 + if (!invitationService.isUserVerified(userId)) { + throw new BusinessException("请先验证邀请码后再使用对话功能"); + } + + // 继续处理对话请求 + // ... +} +``` + +或者使用拦截器统一处理: + +```java +@Component +public class InvitationInterceptor implements HandlerInterceptor { + + @Autowired + private InvitationService invitationService; + + @Override + public boolean preHandle(HttpServletRequest request, + HttpServletResponse response, + Object handler) throws Exception { + // 获取当前用户ID + Long userId = SecurityUtils.getUserId(); + + // 检查是否需要验证邀请码的接口 + String uri = request.getRequestURI(); + if (needsInvitationVerification(uri)) { + if (!invitationService.isUserVerified(userId)) { + throw new BusinessException("请先验证邀请码"); + } + } + + return true; + } + + private boolean needsInvitationVerification(String uri) { + // 需要验证邀请码的接口列表 + return uri.startsWith("/api/dialog/") + || uri.startsWith("/api/task/"); + } +} +``` + +### 3.5 后端文件清单 + +#### 实体类 +- `com.iccoder.entity.InvitationCode` - 邀请码实体 +- `com.iccoder.entity.UserInvitation` - 用户邀请码关联实体 + +#### Mapper +- `com.iccoder.mapper.InvitationCodeMapper` - 邀请码数据访问 +- `com.iccoder.mapper.UserInvitationMapper` - 用户邀请码关联数据访问 + +#### Service +- `com.iccoder.service.InvitationService` - 邀请码业务逻辑 +- `com.iccoder.service.impl.InvitationServiceImpl` - 实现类 + +#### Controller +- `com.iccoder.controller.InvitationController` - 邀请码接口 +- `com.iccoder.controller.admin.InvitationAdminController` - 管理接口 + +#### 拦截器 +- `com.iccoder.interceptor.InvitationInterceptor` - 邀请码验证拦截器 + +## 四、用户体验优化 + +### 4.1 首次使用引导 + +在用户首次打开聊天面板时,如果未验证邀请码,显示友好的引导信息: + +```typescript +// 在 ICHelperPanel.ts 中 +if (!await InvitationService.isVerified(context)) { + panel.webview.postMessage({ + command: 'showInvitationGuide', + message: '欢迎使用 IC Coder!请先输入邀请码以开始使用。' + }); +} +``` + +### 4.2 状态持久化 + +验证状态保存在 `globalState` 中,避免重复验证: + +```typescript +// 保存验证状态 +await context.globalState.update('invitationCodeVerified', true); +await context.globalState.update('invitationCode', code); +await context.globalState.update('verifiedTime', new Date().toISOString()); +``` + +### 4.3 错误提示优化 + +根据不同的错误类型,提供清晰的提示信息: + +| 错误类型 | 提示信息 | +|---------|---------| +| 邀请码不存在 | "邀请码不存在,请检查后重新输入" | +| 邀请码已过期 | "邀请码已过期,请联系管理员获取新的邀请码" | +| 使用次数已达上限 | "该邀请码使用次数已达上限,请使用其他邀请码" | +| 已验证过 | "您已验证过邀请码,无需重复验证" | +| 网络错误 | "网络连接失败,请检查网络后重试" | + +### 4.4 支持重新验证 + +提供命令允许用户更换邀请码: + +```typescript +// 在 extension.ts 中注册命令 +context.subscriptions.push( + vscode.commands.registerCommand('ic-coder.changeInvitationCode', async () => { + const confirm = await vscode.window.showWarningMessage( + '确定要更换邀请码吗?', + '确定', + '取消' + ); + + if (confirm === '确定') { + await InvitationService.clearVerificationStatus(context); + vscode.window.showInformationMessage('已清除邀请码,请重新验证'); + } + }) +); +``` + +### 4.5 Webview 中显示状态(可选) + +在聊天界面顶部显示邀请码验证状态: + +```html + +
+ + 邀请码已验证 +
+ + +
+ ! + 请先验证邀请码 + +
+``` + +## 五、安全考虑 + +### 5.1 邀请码生成规则 + +使用安全的随机算法生成邀请码: + +```java +public String generateInvitationCode() { + // 使用 UUID + 时间戳 + 随机数 + String uuid = UUID.randomUUID().toString().replace("-", ""); + String timestamp = String.valueOf(System.currentTimeMillis()); + String random = RandomStringUtils.randomAlphanumeric(6); + + // 组合并取前16位 + String combined = uuid + timestamp + random; + String code = DigestUtils.sha256Hex(combined).substring(0, 16).toUpperCase(); + + return "IC" + code; // 添加前缀,例如:IC3F2A9B1C4D5E6F +} +``` + +### 5.2 防暴力破解 + +限制验证频率,添加验证失败次数限制: + +```java +// 使用 Redis 记录验证失败次数 +String key = "invitation:fail:" + userId; +Integer failCount = redisTemplate.opsForValue().get(key); + +if (failCount != null && failCount >= 5) { + throw new BusinessException("验证失败次数过多,请1小时后再试"); +} + +// 验证失败时增加计数 +if (!verifySuccess) { + redisTemplate.opsForValue().increment(key); + redisTemplate.expire(key, 1, TimeUnit.HOURS); +} +``` + +### 5.3 Token 绑定 + +邀请码验证状态与用户 Token 绑定,退出登录时清除: + +```typescript +// 在退出登录时清除验证状态 +vscode.commands.registerCommand('ic-coder.logout', async () => { + // 清除 session + await clearSession(); + + // 清除邀请码验证状态 + await InvitationService.clearVerificationStatus(context); + + vscode.window.showInformationMessage('已退出登录'); +}); +``` + +### 5.4 日志记录 + +记录所有验证尝试,便于审计和分析: + +```java +@Slf4j +public class InvitationServiceImpl implements InvitationService { + + @Override + public boolean verifyInvitationCode(Long userId, String code) { + log.info("用户 {} 尝试验证邀请码: {}", userId, code); + + try { + // 验证逻辑 + // ... + + log.info("用户 {} 验证邀请码成功: {}", userId, code); + return true; + } catch (Exception e) { + log.warn("用户 {} 验证邀请码失败: {}, 原因: {}", + userId, code, e.getMessage()); + throw e; + } + } +} +``` + +### 5.5 敏感信息保护 + +- 邀请码在数据库中可以考虑加密存储(可选) +- API 响应中不暴露邀请码的详细信息(如剩余次数) +- 前端不缓存邀请码明文,只保存验证状态 + +## 六、实施步骤 + +### 阶段一:后端开发(优先) + +1. 创建数据库表 +2. 实现邀请码生成和管理功能 +3. 实现验证接口 +4. 添加权限拦截 +5. 测试接口功能 + +### 阶段二:前端开发 + +1. 添加类型定义 +2. 实现 `InvitationService` +3. 修改 `apiClient.ts` 添加接口调用 +4. 修改 `messageHandler.ts` 添加验证检查 +5. 测试完整流程 + +### 阶段三:联调测试 + +1. 前后端联调 +2. 测试各种异常场景 +3. 优化用户体验 +4. 性能测试 + +### 阶段四:上线部署 + +1. 生成初始邀请码 +2. 更新用户文档 +3. 灰度发布 +4. 监控运行状态 + +## 七、测试用例 + +### 7.1 正常流程测试 + +| 测试场景 | 预期结果 | +|---------|---------| +| 首次使用,输入有效邀请码 | 验证成功,可以正常对话 | +| 已验证用户再次打开面板 | 无需重复验证,直接使用 | +| 退出登录后重新登录 | 需要重新验证邀请码 | + +### 7.2 异常场景测试 + +| 测试场景 | 预期结果 | +|---------|---------| +| 输入不存在的邀请码 | 提示"邀请码不存在" | +| 输入已过期的邀请码 | 提示"邀请码已过期" | +| 输入使用次数已满的邀请码 | 提示"使用次数已达上限" | +| 已验证用户尝试再次验证 | 提示"已验证过,无需重复验证" | +| 网络断开时验证 | 提示"网络连接失败" | +| 连续输入错误邀请码5次 | 提示"验证失败次数过多,请稍后再试" | + +### 7.3 边界条件测试 + +| 测试场景 | 预期结果 | +|---------|---------| +| 邀请码为空 | 前端验证拦截,提示"邀请码不能为空" | +| 邀请码长度不足 | 前端验证拦截,提示"邀请码格式不正确" | +| 邀请码包含特殊字符 | 后端验证失败,提示"邀请码不存在" | +| 同一邀请码多人同时使用 | 使用数据库锁,确保不超过最大次数 | + +## 八、FAQ + +### Q1: 用户忘记邀请码怎么办? +A: 邀请码验证成功后,用户无需记住邀请码。如果需要查看,可以在设置中显示已验证的邀请码。 + +### Q2: 邀请码可以重复使用吗? +A: 取决于邀请码的 `maxUses` 设置。可以设置为 1(一次性)、N(限定次数)或 -1(无限制)。 + +### Q3: 如何批量生成邀请码? +A: 使用管理接口 `POST /api/admin/invitation/generate`,指定生成数量即可。 + +### Q4: 邀请码验证失败会影响登录吗? +A: 不会。邀请码验证是独立的,只影响对话功能的使用,不影响登录。 + +### Q5: 可以为不同用户群体设置不同的邀请码吗? +A: 可以。通过 `remark` 字段标记不同批次的邀请码,便于管理和统计。 + +## 九、后续优化方向 + +1. **邀请码分级**:不同等级的邀请码对应不同的权限(如对话次数、模型选择等) +2. **邀请奖励**:邀请他人使用可获得积分或额外权限 +3. **邀请统计**:统计每个邀请码的使用情况和用户活跃度 +4. **自动过期**:根据使用情况自动延长或缩短邀请码有效期 +5. **白名单机制**:特定用户可以免邀请码使用 + +--- + +**文档版本**:v1.0 +**最后更新**:2026-01-27 +**维护者**:IC Coder Team diff --git a/src/extension.ts b/src/extension.ts index 05491bc..cb9192a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -9,6 +9,7 @@ import { initUserService } from "./services/userService"; import { initCreditsService } from "./services/creditsService"; import { isTokenExpired } from "./utils/jwtUtils"; import { NotificationService } from "./services/notificationService"; +import { InvitationService } from "./services/invitationService"; export async function activate(context: vscode.ExtensionContext) { console.log("🎉 IC Coder 插件已激活!"); @@ -187,6 +188,8 @@ export async function activate(context: vscode.ExtensionContext) { if (session) { // 调用 authProvider 的 removeSession 方法 await authProvider.removeSession(session.id); + // 清除邀请码验证状态 + await InvitationService.clearVerificationStatus(context); } else { vscode.window.showInformationMessage("当前未登录"); } @@ -196,6 +199,23 @@ export async function activate(context: vscode.ExtensionContext) { } ); + // 注册命令:更换邀请码 + const changeInvitationCodeCommand = vscode.commands.registerCommand( + "ic-coder.changeInvitationCode", + async () => { + const confirm = await vscode.window.showWarningMessage( + '确定要更换邀请码吗?', + '确定', + '取消' + ); + + if (confirm === '确定') { + await InvitationService.clearVerificationStatus(context); + vscode.window.showInformationMessage('已清除邀请码,请重新验证'); + } + } + ); + // 注册命令:测试系统通知 const testNotificationCommand = vscode.commands.registerCommand( "ic-coder.testNotification", @@ -283,6 +303,7 @@ export async function activate(context: vscode.ExtensionContext) { openVCDViewerInBrowserCommand, loginCommand, logoutCommand, + changeInvitationCodeCommand, testNotificationCommand, // TODO: 等待重新实现这些命令 // viewHistoryCommand, diff --git a/src/panels/ICHelperPanel.ts b/src/panels/ICHelperPanel.ts index 9fc998e..62b5d35 100644 --- a/src/panels/ICHelperPanel.ts +++ b/src/panels/ICHelperPanel.ts @@ -396,6 +396,40 @@ export async function showICHelperPanel( // 退出登录 vscode.commands.executeCommand("ic-coder.logout"); break; + case "checkInvitationCode": + // 检查邀请码验证状态 + { + const { InvitationService } = require("../services/invitationService"); + const isVerified = await InvitationService.isVerified(context); + panel.webview.postMessage({ + command: "invitationCodeStatus", + verified: isVerified + }); + } + break; + case "verifyInvitationCode": + // 验证邀请码 + { + const { InvitationService } = require("../services/invitationService"); + const result = await InvitationService.verifyCode(message.code); + + if (result.success) { + // 验证成功,保存状态 + await InvitationService.saveVerificationStatus(context, message.code); + panel.webview.postMessage({ + command: "invitationCodeVerified", + success: true + }); + } else { + // 验证失败,返回错误信息 + panel.webview.postMessage({ + command: "invitationCodeVerified", + success: false, + message: result.message + }); + } + } + break; case "openICCoder": // 跳转到 IC Coder 官网 vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com")); diff --git a/src/services/apiClient.ts b/src/services/apiClient.ts index b918411..12db482 100644 --- a/src/services/apiClient.ts +++ b/src/services/apiClient.ts @@ -7,7 +7,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, UserInfoResponse } from '../types/api'; +import type { ToolCallResult, AnswerRequest, ToolResultResponse, AnswerResponse, ToolConfirmResponse, UserInfoResponse, InvitationVerifyRequest, InvitationVerifyResponse, InvitationStatusResponse } from '../types/api'; /** * HTTP 请求选项 @@ -260,3 +260,27 @@ export async function getCreditBalance(userId: string): Promise { + console.log('[API] 验证邀请码'); + const body: InvitationVerifyRequest = { code }; + return request('/api/invitation/verify', { + method: 'POST', + body + }); +} + +/** + * 查询邀请码验证状态 + * GET /api/invitation/status + */ +export async function checkInvitationStatus(): Promise { + console.log('[API] 查询邀请码验证状态'); + return request('/api/invitation/status', { + method: 'GET' + }); +} diff --git a/src/services/invitationService.ts b/src/services/invitationService.ts new file mode 100644 index 0000000..94435ca --- /dev/null +++ b/src/services/invitationService.ts @@ -0,0 +1,91 @@ +/** + * 邀请码验证服务 + */ +import * as vscode from 'vscode'; +import { verifyInvitationCode, checkInvitationStatus } from './apiClient'; + +/** + * 邀请码验证服务类 + */ +export class InvitationService { + /** + * 检查用户是否已验证邀请码 + */ + static async isVerified(context: vscode.ExtensionContext): Promise { + // 【临时】使用本地验证,不调用后端 + const localVerified = context.globalState.get('invitationCodeVerified'); + return localVerified || false; + } + + /** + * 验证邀请码 + */ + static async verifyCode(code: string): Promise<{ success: boolean; message: string }> { + try { + console.log('[InvitationService] 验证邀请码:', code); + const response = await verifyInvitationCode(code); + + if (response.code === 200 && response.data?.verified) { + return { + success: true, + message: response.msg || '验证成功' + }; + } else { + return { + success: false, + message: response.msg || '验证失败' + }; + } + } catch (error: any) { + console.error('[InvitationService] 验证邀请码失败:', error); + return { + success: false, + message: error.message || '网络连接失败,请检查网络后重试' + }; + } + } + + /** + * 保存验证状态到本地 + */ + static async saveVerificationStatus( + context: vscode.ExtensionContext, + code: string, + verifiedTime?: string + ): Promise { + await context.globalState.update('invitationCodeVerified', true); + await context.globalState.update('invitationCode', code); + await context.globalState.update('invitationVerifiedTime', verifiedTime || new Date().toISOString()); + } + + /** + * 清除验证状态(用于退出登录或更换邀请码) + */ + static async clearVerificationStatus(context: vscode.ExtensionContext): Promise { + await context.globalState.update('invitationCodeVerified', undefined); + await context.globalState.update('invitationCode', undefined); + await context.globalState.update('invitationVerifiedTime', undefined); + } + + /** + * 显示邀请码输入弹窗 + */ + static async showInputDialog(): Promise { + const code = await vscode.window.showInputBox({ + prompt: '请输入邀请码以继续使用 IC Coder', + placeHolder: '例如:INVITE2024ABC', + ignoreFocusOut: true, + validateInput: (value) => { + if (!value || value.trim().length === 0) { + return '邀请码不能为空'; + } + if (value.trim().length < 6) { + return '邀请码格式不正确'; + } + return null; + } + }); + + return code?.trim(); + } +} diff --git a/src/types/api.ts b/src/types/api.ts index 2dfe7de..d2ea3e5 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -581,3 +581,49 @@ export type ToolArgs = | WaveformTraceArgs | KnowledgeSaveArgs | KnowledgeLoadArgs; + +// ============== 邀请码验证 ============== + +/** + * 邀请码验证请求 + * POST /api/invitation/verify + */ +export interface InvitationVerifyRequest { + /** 邀请码 */ + code: string; +} + +/** + * 邀请码验证响应 + */ +export interface InvitationVerifyResponse { + /** 响应代码 */ + code: number; + /** 响应消息 */ + msg: string; + /** 验证结果数据 */ + data?: { + /** 是否验证成功 */ + verified: boolean; + }; +} + +/** + * 邀请码状态响应 + * GET /api/invitation/status + */ +export interface InvitationStatusResponse { + /** 响应代码 */ + code: number; + /** 响应消息 */ + msg?: string; + /** 状态数据 */ + data?: { + /** 是否已验证 */ + verified: boolean; + /** 使用的邀请码 */ + invitationCode?: string; + /** 验证时间 */ + verifiedTime?: string; + }; +} diff --git a/src/views/exampleShowcase.ts b/src/views/exampleShowcase.ts index 262d8fa..b457e11 100644 --- a/src/views/exampleShowcase.ts +++ b/src/views/exampleShowcase.ts @@ -231,10 +231,10 @@ export function getExampleShowcaseScript(): string { // 直接发送示例消息 function sendExample(index) { - // 先检查工作区 + // 先检查邀请码验证状态 pendingExampleIndex = index; vscode.postMessage({ - command: 'checkWorkspace' + command: 'checkInvitationCode' }); } diff --git a/src/views/inputArea.ts b/src/views/inputArea.ts index 4b6ab46..0d01767 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: 'checkInvitationCode' }); }); // 初始化时调整一次高度 diff --git a/src/views/invitationModal.ts b/src/views/invitationModal.ts new file mode 100644 index 0000000..b785d14 --- /dev/null +++ b/src/views/invitationModal.ts @@ -0,0 +1,321 @@ +/** + * 邀请码验证弹窗 + */ + +/** + * 获取邀请码弹窗的 HTML 内容 + */ +export function getInvitationModalContent(qrCodeUri?: string): string { + return ` + + + `; +} + +/** + * 获取邀请码弹窗的 CSS 样式 + */ +export function getInvitationModalStyles(): string { + return ` + /* 邀请码弹窗样式 */ + .invitation-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + } + + .invitation-modal-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4px); + } + + .invitation-modal-content { + position: relative; + background: var(--vscode-editor-background); + border: 1px solid var(--vscode-panel-border); + border-radius: 8px; + width: 90%; + max-width: 400px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + animation: modalSlideIn 0.3s ease-out; + } + + @keyframes modalSlideIn { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + .invitation-modal-header { + padding: 24px 24px 16px; + border-bottom: 1px solid var(--vscode-panel-border); + text-align: center; + } + + .invitation-modal-header h2 { + margin: 0; + font-size: 18px; + font-weight: 600; + color: var(--vscode-foreground); + } + + .invitation-modal-subtitle { + margin: 8px 0 0; + font-size: 13px; + color: var(--vscode-descriptionForeground); + } + + .invitation-modal-body { + padding: 24px; + } + + .invitation-qrcode-section { + text-align: center; + margin-bottom: 24px; + } + + .invitation-qrcode-text { + margin: 0 0 16px; + font-size: 13px; + color: var(--vscode-foreground); + line-height: 1.5; + } + + .invitation-qrcode-image { + width: 200px; + height: 200px; + border: 1px solid var(--vscode-panel-border); + border-radius: 8px; + background: #fff; + } + + .invitation-input-section { + margin-top: 24px; + } + + .invitation-code-input { + width: 100%; + padding: 10px 12px; + font-size: 14px; + border: 1px solid var(--vscode-input-border); + background: var(--vscode-input-background); + color: var(--vscode-input-foreground); + border-radius: 4px; + outline: none; + transition: border-color 0.2s; + box-sizing: border-box; + } + + .invitation-code-input:focus { + border-color: var(--vscode-focusBorder); + } + + .invitation-code-input::placeholder { + color: var(--vscode-input-placeholderForeground); + } + + .invitation-error { + margin-top: 12px; + padding: 8px 12px; + font-size: 13px; + color: var(--vscode-errorForeground); + background: var(--vscode-inputValidation-errorBackground); + border: 1px solid var(--vscode-inputValidation-errorBorder); + border-radius: 4px; + } + + .invitation-modal-footer { + padding: 16px 24px; + border-top: 1px solid var(--vscode-panel-border); + display: flex; + justify-content: flex-end; + } + + .invitation-btn { + padding: 8px 20px; + font-size: 13px; + border: none; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s; + outline: none; + } + + .invitation-btn-primary { + background: var(--vscode-button-background); + color: var(--vscode-button-foreground); + } + + .invitation-btn-primary:hover { + background: var(--vscode-button-hoverBackground); + } + + .invitation-btn-primary:active { + transform: scale(0.98); + } + + .invitation-btn-primary:disabled { + opacity: 0.5; + cursor: not-allowed; + } + `; +} + +/** + * 获取邀请码弹窗的 JavaScript 逻辑 + */ +export function getInvitationModalScript(): string { + return ` + // 邀请码弹窗逻辑 + (function() { + const modal = document.getElementById('invitationModal'); + const input = document.getElementById('invitationCodeInput'); + const submitBtn = document.getElementById('invitationSubmitBtn'); + const errorDiv = document.getElementById('invitationError'); + + // 显示邀请码弹窗 + window.showInvitationModal = function() { + modal.style.display = 'flex'; + setTimeout(() => { + input.focus(); + }, 100); + }; + + // 隐藏邀请码弹窗 + window.hideInvitationModal = function() { + modal.style.display = 'none'; + input.value = ''; + errorDiv.style.display = 'none'; + errorDiv.textContent = ''; + submitBtn.disabled = false; + }; + + // 显示错误信息 + window.showInvitationError = function(message) { + errorDiv.textContent = message; + errorDiv.style.display = 'block'; + submitBtn.disabled = false; + }; + + // 提交邀请码 + function submitInvitationCode() { + const code = input.value.trim(); + + if (!code) { + showInvitationError('邀请码不能为空'); + return; + } + + if (code.length < 6) { + showInvitationError('邀请码格式不正确'); + return; + } + + // 禁用按钮,防止重复提交 + submitBtn.disabled = true; + errorDiv.style.display = 'none'; + + // 发送验证请求到后端 + vscode.postMessage({ + command: 'verifyInvitationCode', + code: code + }); + } + + // 点击提交按钮 + submitBtn.addEventListener('click', submitInvitationCode); + + // 回车键提交 + input.addEventListener('keypress', function(e) { + if (e.key === 'Enter') { + submitInvitationCode(); + } + }); + + // 阻止点击弹窗内容时关闭 + document.querySelector('.invitation-modal-content').addEventListener('click', function(e) { + e.stopPropagation(); + }); + + // 监听来自后端的消息 + window.addEventListener('message', function(event) { + const message = event.data; + + // 处理邀请码验证状态 + if (message.command === 'invitationCodeStatus') { + if (!message.verified) { + // 未验证,显示弹窗 + showInvitationModal(); + } else { + // 已验证,继续执行待处理的操作 + if (typeof pendingExampleIndex !== 'undefined' && pendingExampleIndex >= 0) { + // 如果有待发送的示例,先检查工作区 + vscode.postMessage({ command: 'checkWorkspace' }); + } + } + } + + // 处理邀请码验证结果 + if (message.command === 'invitationCodeVerified') { + if (message.success) { + // 验证成功,隐藏弹窗 + hideInvitationModal(); + // 继续执行待处理的操作 + if (typeof pendingExampleIndex !== 'undefined' && pendingExampleIndex >= 0) { + // 如果有待发送的示例,先检查工作区 + vscode.postMessage({ command: 'checkWorkspace' }); + } + } else { + // 验证失败,显示错误信息 + showInvitationError(message.message || '验证失败,请重试'); + } + } + }); + })(); + `; +} diff --git a/src/views/webviewContent.ts b/src/views/webviewContent.ts index c26c470..818a41b 100644 --- a/src/views/webviewContent.ts +++ b/src/views/webviewContent.ts @@ -25,6 +25,11 @@ import { } from "./progressBar"; import { getHighlightJsLinks } from "../components/codeHighlight"; import { getCurrentEnv } from "../config/settings"; +import { + getInvitationModalContent, + getInvitationModalStyles, + getInvitationModalScript, +} from "./invitationModal"; /** * 获取 WebView 面板的 HTML 内容 */ @@ -93,6 +98,7 @@ export function getWebviewContent( ${getConversationHistoryBarStyles()} ${getProgressBarStyles()} ${getInputAreaStyles()} + ${getInvitationModalStyles()} .file-editor-section { margin-bottom: 15px; @@ -398,6 +404,7 @@ export function getWebviewContent( ${getConversationHistoryBarContent()} ${getProgressBarContent()} + ${getInvitationModalContent(qrCodeUri)}
IC Coder @@ -802,6 +809,7 @@ export function getWebviewContent( ${getConversationHistoryBarScript()} ${getProgressBarScript()} ${getInputAreaScript()} + ${getInvitationModalScript()} `; }