feat: 实现邀请码验证功能
## 功能概述 - 用户首次使用需验证邀请码才能发起对话 - 在输入框聚焦和点击示例时触发验证检查 - 使用弹窗形式展示邀请码输入界面,包含企业端用户提示和微信二维码 ## 主要变更 ### 新增文件 - `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 主题 - 支持回车键提交验证
This commit is contained in:
739
docs/invitation-code-design.md
Normal file
739
docs/invitation-code-design.md
Normal file
@ -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<boolean>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证邀请码
|
||||||
|
*/
|
||||||
|
static async verifyCode(code: string): Promise<boolean>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存验证状态
|
||||||
|
*/
|
||||||
|
static async saveVerificationStatus(
|
||||||
|
context: vscode.ExtensionContext,
|
||||||
|
code: string
|
||||||
|
): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除验证状态(用于退出登录)
|
||||||
|
*/
|
||||||
|
static async clearVerificationStatus(
|
||||||
|
context: vscode.ExtensionContext
|
||||||
|
): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示邀请码输入弹窗
|
||||||
|
*/
|
||||||
|
static async showInputDialog(): Promise<string | undefined>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 修改文件
|
||||||
|
|
||||||
|
**`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
|
||||||
|
<!-- 已验证 -->
|
||||||
|
<div class="invitation-status verified">
|
||||||
|
<span class="icon">✓</span>
|
||||||
|
<span>邀请码已验证</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 未验证 -->
|
||||||
|
<div class="invitation-status unverified">
|
||||||
|
<span class="icon">!</span>
|
||||||
|
<span>请先验证邀请码</span>
|
||||||
|
<button onclick="verifyInvitationCode()">立即验证</button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 五、安全考虑
|
||||||
|
|
||||||
|
### 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
|
||||||
@ -9,6 +9,7 @@ import { initUserService } from "./services/userService";
|
|||||||
import { initCreditsService } from "./services/creditsService";
|
import { initCreditsService } from "./services/creditsService";
|
||||||
import { isTokenExpired } from "./utils/jwtUtils";
|
import { isTokenExpired } from "./utils/jwtUtils";
|
||||||
import { NotificationService } from "./services/notificationService";
|
import { NotificationService } from "./services/notificationService";
|
||||||
|
import { InvitationService } from "./services/invitationService";
|
||||||
|
|
||||||
export async function activate(context: vscode.ExtensionContext) {
|
export async function activate(context: vscode.ExtensionContext) {
|
||||||
console.log("🎉 IC Coder 插件已激活!");
|
console.log("🎉 IC Coder 插件已激活!");
|
||||||
@ -187,6 +188,8 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
if (session) {
|
if (session) {
|
||||||
// 调用 authProvider 的 removeSession 方法
|
// 调用 authProvider 的 removeSession 方法
|
||||||
await authProvider.removeSession(session.id);
|
await authProvider.removeSession(session.id);
|
||||||
|
// 清除邀请码验证状态
|
||||||
|
await InvitationService.clearVerificationStatus(context);
|
||||||
} else {
|
} else {
|
||||||
vscode.window.showInformationMessage("当前未登录");
|
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(
|
const testNotificationCommand = vscode.commands.registerCommand(
|
||||||
"ic-coder.testNotification",
|
"ic-coder.testNotification",
|
||||||
@ -283,6 +303,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
openVCDViewerInBrowserCommand,
|
openVCDViewerInBrowserCommand,
|
||||||
loginCommand,
|
loginCommand,
|
||||||
logoutCommand,
|
logoutCommand,
|
||||||
|
changeInvitationCodeCommand,
|
||||||
testNotificationCommand,
|
testNotificationCommand,
|
||||||
// TODO: 等待重新实现这些命令
|
// TODO: 等待重新实现这些命令
|
||||||
// viewHistoryCommand,
|
// viewHistoryCommand,
|
||||||
|
|||||||
@ -396,6 +396,40 @@ export async function showICHelperPanel(
|
|||||||
// 退出登录
|
// 退出登录
|
||||||
vscode.commands.executeCommand("ic-coder.logout");
|
vscode.commands.executeCommand("ic-coder.logout");
|
||||||
break;
|
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":
|
case "openICCoder":
|
||||||
// 跳转到 IC Coder 官网
|
// 跳转到 IC Coder 官网
|
||||||
vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com"));
|
vscode.env.openExternal(vscode.Uri.parse("https://www.iccoder.com"));
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import * as https from 'https';
|
|||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
import { getApiUrl, getConfig } from '../config/settings';
|
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 请求选项
|
* HTTP 请求选项
|
||||||
@ -260,3 +260,27 @@ export async function getCreditBalance(userId: string): Promise<CreditBalanceRes
|
|||||||
timeout: 5000
|
timeout: 5000
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证邀请码
|
||||||
|
* POST /api/invitation/verify
|
||||||
|
*/
|
||||||
|
export async function verifyInvitationCode(code: string): Promise<InvitationVerifyResponse> {
|
||||||
|
console.log('[API] 验证邀请码');
|
||||||
|
const body: InvitationVerifyRequest = { code };
|
||||||
|
return request<InvitationVerifyResponse>('/api/invitation/verify', {
|
||||||
|
method: 'POST',
|
||||||
|
body
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询邀请码验证状态
|
||||||
|
* GET /api/invitation/status
|
||||||
|
*/
|
||||||
|
export async function checkInvitationStatus(): Promise<InvitationStatusResponse> {
|
||||||
|
console.log('[API] 查询邀请码验证状态');
|
||||||
|
return request<InvitationStatusResponse>('/api/invitation/status', {
|
||||||
|
method: 'GET'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
91
src/services/invitationService.ts
Normal file
91
src/services/invitationService.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* 邀请码验证服务
|
||||||
|
*/
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import { verifyInvitationCode, checkInvitationStatus } from './apiClient';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邀请码验证服务类
|
||||||
|
*/
|
||||||
|
export class InvitationService {
|
||||||
|
/**
|
||||||
|
* 检查用户是否已验证邀请码
|
||||||
|
*/
|
||||||
|
static async isVerified(context: vscode.ExtensionContext): Promise<boolean> {
|
||||||
|
// 【临时】使用本地验证,不调用后端
|
||||||
|
const localVerified = context.globalState.get<boolean>('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<void> {
|
||||||
|
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<void> {
|
||||||
|
await context.globalState.update('invitationCodeVerified', undefined);
|
||||||
|
await context.globalState.update('invitationCode', undefined);
|
||||||
|
await context.globalState.update('invitationVerifiedTime', undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示邀请码输入弹窗
|
||||||
|
*/
|
||||||
|
static async showInputDialog(): Promise<string | undefined> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -581,3 +581,49 @@ export type ToolArgs =
|
|||||||
| WaveformTraceArgs
|
| WaveformTraceArgs
|
||||||
| KnowledgeSaveArgs
|
| KnowledgeSaveArgs
|
||||||
| KnowledgeLoadArgs;
|
| 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -231,10 +231,10 @@ export function getExampleShowcaseScript(): string {
|
|||||||
|
|
||||||
// 直接发送示例消息
|
// 直接发送示例消息
|
||||||
function sendExample(index) {
|
function sendExample(index) {
|
||||||
// 先检查工作区
|
// 先检查邀请码验证状态
|
||||||
pendingExampleIndex = index;
|
pendingExampleIndex = index;
|
||||||
vscode.postMessage({
|
vscode.postMessage({
|
||||||
command: 'checkWorkspace'
|
command: 'checkInvitationCode'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -339,12 +339,14 @@ export function getInputAreaScript(): string {
|
|||||||
if (messageInput) {
|
if (messageInput) {
|
||||||
messageInput.addEventListener('input', autoResizeTextarea);
|
messageInput.addEventListener('input', autoResizeTextarea);
|
||||||
|
|
||||||
// 监听点击事件,检测工作区状态
|
// 监听点击事件,检测工作区状态和邀请码验证状态
|
||||||
messageInput.addEventListener('focus', () => {
|
messageInput.addEventListener('focus', () => {
|
||||||
if (!hasCheckedWorkspace) {
|
if (!hasCheckedWorkspace) {
|
||||||
hasCheckedWorkspace = true;
|
hasCheckedWorkspace = true;
|
||||||
vscode.postMessage({ command: 'checkWorkspace' });
|
vscode.postMessage({ command: 'checkWorkspace' });
|
||||||
}
|
}
|
||||||
|
// 检查邀请码验证状态
|
||||||
|
vscode.postMessage({ command: 'checkInvitationCode' });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 初始化时调整一次高度
|
// 初始化时调整一次高度
|
||||||
|
|||||||
321
src/views/invitationModal.ts
Normal file
321
src/views/invitationModal.ts
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
/**
|
||||||
|
* 邀请码验证弹窗
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取邀请码弹窗的 HTML 内容
|
||||||
|
*/
|
||||||
|
export function getInvitationModalContent(qrCodeUri?: string): string {
|
||||||
|
return `
|
||||||
|
<!-- 邀请码验证弹窗 -->
|
||||||
|
<div id="invitationModal" class="invitation-modal" style="display: none;">
|
||||||
|
<div class="invitation-modal-overlay"></div>
|
||||||
|
<div class="invitation-modal-content">
|
||||||
|
<div class="invitation-modal-header">
|
||||||
|
<h2>验证邀请码</h2>
|
||||||
|
<p class="invitation-modal-subtitle">仅供企业端用户和内部人员使用</p>
|
||||||
|
</div>
|
||||||
|
<div class="invitation-modal-body">
|
||||||
|
<div class="invitation-qrcode-section">
|
||||||
|
<p class="invitation-qrcode-text">欢迎企业端用户扫码添加微信获取邀请码</p>
|
||||||
|
<img src="${qrCodeUri}" alt="微信二维码" class="invitation-qrcode-image" />
|
||||||
|
</div>
|
||||||
|
<div class="invitation-input-section">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="invitationCodeInput"
|
||||||
|
class="invitation-code-input"
|
||||||
|
placeholder="请输入邀请码"
|
||||||
|
maxlength="20"
|
||||||
|
/>
|
||||||
|
<div id="invitationError" class="invitation-error" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="invitation-modal-footer">
|
||||||
|
<button id="invitationSubmitBtn" class="invitation-btn invitation-btn-primary">
|
||||||
|
验证
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取邀请码弹窗的 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 || '验证失败,请重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
}
|
||||||
@ -25,6 +25,11 @@ import {
|
|||||||
} from "./progressBar";
|
} from "./progressBar";
|
||||||
import { getHighlightJsLinks } from "../components/codeHighlight";
|
import { getHighlightJsLinks } from "../components/codeHighlight";
|
||||||
import { getCurrentEnv } from "../config/settings";
|
import { getCurrentEnv } from "../config/settings";
|
||||||
|
import {
|
||||||
|
getInvitationModalContent,
|
||||||
|
getInvitationModalStyles,
|
||||||
|
getInvitationModalScript,
|
||||||
|
} from "./invitationModal";
|
||||||
/**
|
/**
|
||||||
* 获取 WebView 面板的 HTML 内容
|
* 获取 WebView 面板的 HTML 内容
|
||||||
*/
|
*/
|
||||||
@ -93,6 +98,7 @@ export function getWebviewContent(
|
|||||||
${getConversationHistoryBarStyles()}
|
${getConversationHistoryBarStyles()}
|
||||||
${getProgressBarStyles()}
|
${getProgressBarStyles()}
|
||||||
${getInputAreaStyles()}
|
${getInputAreaStyles()}
|
||||||
|
${getInvitationModalStyles()}
|
||||||
|
|
||||||
.file-editor-section {
|
.file-editor-section {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
@ -398,6 +404,7 @@ export function getWebviewContent(
|
|||||||
<body>
|
<body>
|
||||||
${getConversationHistoryBarContent()}
|
${getConversationHistoryBarContent()}
|
||||||
${getProgressBarContent()}
|
${getProgressBarContent()}
|
||||||
|
${getInvitationModalContent(qrCodeUri)}
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div style="display: flex; align-items: center; justify-content: center; gap: 15px;">
|
<div style="display: flex; align-items: center; justify-content: center; gap: 15px;">
|
||||||
<img src="${iconUri}" alt="IC Coder" style="width: 48px; height: 48px;" />
|
<img src="${iconUri}" alt="IC Coder" style="width: 48px; height: 48px;" />
|
||||||
@ -802,6 +809,7 @@ export function getWebviewContent(
|
|||||||
${getConversationHistoryBarScript()}
|
${getConversationHistoryBarScript()}
|
||||||
${getProgressBarScript()}
|
${getProgressBarScript()}
|
||||||
${getInputAreaScript()}
|
${getInputAreaScript()}
|
||||||
|
${getInvitationModalScript()}
|
||||||
</script></body>
|
</script></body>
|
||||||
</html>`;
|
</html>`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user