yangjinhaowife 4 hari lalu
melakukan
34c629fe7c

+ 24 - 0
.gitignore

@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 3 - 0
.vscode/extensions.json

@@ -0,0 +1,3 @@
+{
+  "recommendations": ["Vue.volar"]
+}

+ 2139 - 0
AI智能面试平台完整架构设计.md

@@ -0,0 +1,2139 @@
+# AI智能面试平台完整架构设计
+
+## 1. 系统架构概述
+
+### 1.1 整体架构模式
+
+**推荐架构:微服务架构 + 事件驱动架构**
+
+**选择理由:**
+- **可扩展性**:不同模块(简历处理、AI面试、分析报告)可独立扩展
+- **技术多样性**:AI模块可使用Python,业务逻辑可使用Java/Node.js
+- **容错性**:单个服务故障不影响整体系统
+- **团队协作**:不同团队可并行开发不同服务
+- **部署灵活性**:支持灰度发布和快速回滚
+
+**架构层次:**
+```
+┌─────────────────────────────────────────────────────────────┐
+│                    API Gateway (Kong/Nginx)                 │
+├─────────────────────────────────────────────────────────────┤
+│  用户服务  │  简历服务  │  面试服务  │  分析服务  │  企业服务  │
+├─────────────────────────────────────────────────────────────┤
+│              消息队列 (Apache Kafka)                        │
+├─────────────────────────────────────────────────────────────┤
+│    PostgreSQL  │  MongoDB  │  Redis  │  Elasticsearch      │
+├─────────────────────────────────────────────────────────────┤
+│              基础设施层 (Kubernetes + Docker)                │
+└─────────────────────────────────────────────────────────────┘
+```
+
+### 1.2 关键组件与交互图
+
+**核心微服务:**
+
+1. **用户认证服务 (Auth Service)**
+   - 用户注册、登录、权限管理
+   - JWT Token生成与验证
+   - OAuth2.0第三方登录集成
+
+2. **简历处理服务 (Resume Service)**
+   - 简历文件上传与存储
+   - AI简历解析与结构化
+   - 简历搜索与匹配
+
+3. **面试服务 (Interview Service)**
+   - AI模拟面试流程管理
+   - 实时音视频处理
+   - 面试问题生成与管理
+
+4. **分析服务 (Analysis Service)**
+   - 面试音视频分析
+   - 评估报告生成
+   - 数据统计与洞察
+
+5. **企业管理服务 (Company Service)**
+   - 职位发布与管理
+   - 候选人管理
+   - 招聘流程管理
+
+6. **通知服务 (Notification Service)**
+   - 邮件、短信通知
+   - 实时消息推送
+   - 消息模板管理
+
+**服务交互流程:**
+```
+用户登录 → 认证服务 → 获取Token
+简历上传 → 简历服务 → 消息队列 → AI解析服务
+开始面试 → 面试服务 → AI服务 → 实时交互
+面试结束 → 分析服务 → 生成报告 → 通知服务
+```
+
+### 1.3 系统边界与外部接口
+
+**外部服务集成:**
+- **云存储**:阿里云OSS/AWS S3(文件存储)
+- **AI服务**:讯飞星火/OpenAI API(自然语言处理)
+- **音视频**:阿里云RTC/腾讯云TRTC(实时通信)
+- **邮件服务**:阿里云邮件推送/SendGrid
+- **短信服务**:阿里云短信服务/Twilio
+- **第三方登录**:微信、钉钉、LinkedIn OAuth
+- **支付服务**:支付宝、微信支付(企业版付费功能)
+
+## 2. 前端架构
+
+### 2.1 推荐框架与库
+
+**主框架:Vue 3 + TypeScript**
+
+**选择理由:**
+- **学习曲线平缓**:相比React更容易上手
+- **性能优秀**:Composition API提供更好的逻辑复用
+- **生态完善**:Vue生态系统成熟,插件丰富
+- **TypeScript支持**:原生支持,类型安全
+- **渐进式**:可以逐步引入,适合团队技能水平
+
+**技术栈:**
+```
+Vue 3.4+ + TypeScript 5.0+
+Vite 5.0+ (构建工具)
+Vue Router 4.0+ (路由管理)
+Pinia 2.0+ (状态管理)
+Element Plus (UI组件库)
+Tailwind CSS (样式框架)
+```
+
+### 2.2 状态管理策略
+
+**推荐:Pinia**
+
+**优势:**
+- **TypeScript友好**:完全支持类型推断
+- **模块化**:天然支持模块化状态管理
+- **DevTools支持**:优秀的调试体验
+- **轻量级**:相比Vuex更简洁
+
+**状态结构设计:**
+```typescript
+// stores/auth.ts - 用户认证状态
+export const useAuthStore = defineStore('auth', {
+  state: () => ({
+    user: null as User | null,
+    token: localStorage.getItem('token'),
+    permissions: [] as string[]
+  })
+})
+
+// stores/interview.ts - 面试状态
+export const useInterviewStore = defineStore('interview', {
+  state: () => ({
+    currentInterview: null as Interview | null,
+    questions: [] as Question[],
+    answers: [] as Answer[],
+    status: 'idle' as InterviewStatus
+  })
+})
+
+// stores/resume.ts - 简历状态
+export const useResumeStore = defineStore('resume', {
+  state: () => ({
+    resumes: [] as Resume[],
+    currentResume: null as Resume | null,
+    uploadProgress: 0
+  })
+})
+```
+
+### 2.3 UI组件库建议
+
+**主要选择:Element Plus**
+
+**理由:**
+- **Vue 3原生支持**:专为Vue 3设计
+- **组件丰富**:覆盖大部分业务场景
+- **定制性强**:支持主题定制
+- **文档完善**:中文文档,易于使用
+- **企业级**:适合B端应用
+
+**补充方案:**
+- **Tailwind CSS**:原子化CSS,快速样式开发
+- **Headless UI**:无样式组件,最大化定制
+- **自定义组件**:特殊业务组件(如AI面试界面)
+
+### 2.4 模块组织与性能优化
+
+**项目结构:**
+```
+src/
+├── components/          # 通用组件
+│   ├── common/         # 基础组件
+│   ├── business/       # 业务组件
+│   └── layout/         # 布局组件
+├── views/              # 页面组件
+│   ├── auth/          # 认证相关页面
+│   ├── interview/     # 面试相关页面
+│   ├── resume/        # 简历相关页面
+│   └── company/       # 企业管理页面
+├── stores/             # Pinia状态管理
+├── composables/        # 组合式函数
+├── utils/              # 工具函数
+├── api/                # API接口
+├── types/              # TypeScript类型定义
+└── assets/             # 静态资源
+```
+
+**性能优化措施:**
+
+1. **代码分割与懒加载**
+```typescript
+// 路由级别懒加载
+const Interview = () => import('@/views/interview/InterviewView.vue')
+const Resume = () => import('@/views/resume/ResumeView.vue')
+
+// 组件级别懒加载
+const HeavyComponent = defineAsyncComponent(() => 
+  import('@/components/HeavyComponent.vue')
+)
+```
+
+2. **资源优化**
+- **图片懒加载**:使用Intersection Observer
+- **CDN加速**:静态资源使用CDN
+- **Gzip压缩**:服务器端启用压缩
+- **缓存策略**:合理设置缓存头
+
+3. **渲染优化**
+- **虚拟滚动**:大列表使用虚拟滚动
+- **防抖节流**:搜索、输入事件优化
+- **Keep-alive**:缓存不活跃组件
+
+## 3. 后端架构
+
+### 3.1 推荐技术栈
+
+**主要技术栈:Java + Spring Boot**
+
+**选择理由:**
+- **企业级成熟**:Spring生态系统完善
+- **性能优秀**:JVM优化,高并发处理能力强
+- **社区活跃**:丰富的第三方库和解决方案
+- **团队技能**:Java开发人员相对容易招聘
+- **微服务支持**:Spring Cloud提供完整微服务解决方案
+
+**技术组合:**
+```
+Java 17 + Spring Boot 3.2+
+Spring Cloud 2023.0+ (微服务框架)
+Spring Security 6.0+ (安全框架)
+Spring Data JPA (数据访问)
+MyBatis Plus (ORM增强)
+Redis (缓存)
+Apache Kafka (消息队列)
+Docker + Kubernetes (容器化)
+```
+
+**备选方案:Node.js + NestJS**
+- **适用场景**:团队JavaScript技能强,需要快速开发
+- **优势**:前后端技术栈统一,开发效率高
+- **劣势**:大规模并发处理相对较弱
+
+### 3.2 API设计原则
+
+**推荐:RESTful API + GraphQL混合**
+
+**RESTful API用于:**
+- 标准CRUD操作
+- 文件上传下载
+- 认证授权
+
+**GraphQL用于:**
+- 复杂数据查询
+- 前端灵活数据获取
+- 减少网络请求次数
+
+**API设计规范:**
+```
+# RESTful API示例
+GET    /api/v1/users              # 获取用户列表
+POST   /api/v1/users              # 创建用户
+GET    /api/v1/users/{id}         # 获取特定用户
+PUT    /api/v1/users/{id}         # 更新用户
+DELETE /api/v1/users/{id}         # 删除用户
+
+# 业务API示例
+POST   /api/v1/resumes/upload     # 简历上传
+POST   /api/v1/interviews/start   # 开始面试
+GET    /api/v1/interviews/{id}/analysis # 获取面试分析
+```
+
+**API响应格式标准化:**
+```json
+{
+  "code": 200,
+  "message": "success",
+  "data": {
+    // 实际数据
+  },
+  "timestamp": "2024-01-15T10:30:00Z",
+  "traceId": "abc123def456"
+}
+```
+
+### 3.3 身份验证与授权
+
+**推荐方案:JWT + OAuth 2.0**
+
+**JWT Token设计:**
+```json
+{
+  "sub": "user123",
+  "name": "张三",
+  "role": "candidate",
+  "permissions": ["resume:read", "interview:create"],
+  "exp": 1642234567,
+  "iat": 1642148167
+}
+```
+
+**权限控制模型:**
+```
+用户(User) → 角色(Role) → 权限(Permission)
+
+角色定义:
+- CANDIDATE: 求职者
+- HR: 人力资源
+- ADMIN: 系统管理员
+- COMPANY_ADMIN: 企业管理员
+
+权限示例:
+- resume:read, resume:write
+- interview:create, interview:manage
+- company:manage, user:manage
+```
+
+**安全实现:**
+```java
+@RestController
+@RequestMapping("/api/v1/interviews")
+@PreAuthorize("hasRole('CANDIDATE') or hasRole('HR')")
+public class InterviewController {
+    
+    @PostMapping("/start")
+    @PreAuthorize("hasPermission('interview', 'create')")
+    public ResponseEntity<Interview> startInterview(@RequestBody StartInterviewRequest request) {
+        // 面试开始逻辑
+    }
+}
+```
+
+### 3.4 业务逻辑组织
+
+**推荐:领域驱动设计(DDD) + 分层架构**
+
+**分层结构:**
+```
+┌─────────────────────────────────────┐
+│           Presentation Layer        │  # Controller, DTO
+├─────────────────────────────────────┤
+│           Application Layer         │  # Service, Use Cases
+├─────────────────────────────────────┤
+│             Domain Layer            │  # Entity, Domain Service
+├─────────────────────────────────────┤
+│          Infrastructure Layer       │  # Repository, External APIs
+└─────────────────────────────────────┘
+```
+
+**领域模型示例:**
+```java
+// 面试领域
+@Entity
+public class Interview {
+    private InterviewId id;
+    private CandidateId candidateId;
+    private JobId jobId;
+    private InterviewStatus status;
+    private List<Question> questions;
+    private List<Answer> answers;
+    
+    // 领域方法
+    public void start() {
+        if (this.status != InterviewStatus.SCHEDULED) {
+            throw new IllegalStateException("Interview cannot be started");
+        }
+        this.status = InterviewStatus.IN_PROGRESS;
+        // 发布领域事件
+        DomainEvents.publish(new InterviewStartedEvent(this.id));
+    }
+}
+
+// 简历领域
+@Entity
+public class Resume {
+    private ResumeId id;
+    private CandidateId candidateId;
+    private PersonalInfo personalInfo;
+    private List<WorkExperience> workExperiences;
+    private List<Skill> skills;
+    
+    public MatchScore calculateMatchScore(JobRequirement requirement) {
+        // 简历匹配算法
+    }
+}
+```
+
+### 3.5 异步任务处理
+
+**推荐:Apache Kafka + Spring Cloud Stream**
+
+**消息队列设计:**
+```
+Topic设计:
+- resume-uploaded: 简历上传事件
+- resume-parsed: 简历解析完成事件
+- interview-started: 面试开始事件
+- interview-completed: 面试完成事件
+- analysis-requested: 分析请求事件
+- notification-requested: 通知请求事件
+```
+
+**异步处理示例:**
+```java
+// 简历上传后异步解析
+@EventListener
+public class ResumeEventHandler {
+    
+    @KafkaListener(topics = "resume-uploaded")
+    public void handleResumeUploaded(ResumeUploadedEvent event) {
+        // 异步调用AI解析服务
+        aiParsingService.parseResumeAsync(event.getResumeId());
+    }
+    
+    @KafkaListener(topics = "interview-completed")
+    public void handleInterviewCompleted(InterviewCompletedEvent event) {
+        // 异步生成分析报告
+        analysisService.generateReportAsync(event.getInterviewId());
+        
+        // 发送通知
+        notificationService.sendInterviewCompletionNotification(event);
+    }
+}
+```
+
+**任务队列优先级设计:**
+```
+高优先级:用户认证、面试实时交互
+中优先级:简历解析、报告生成
+低优先级:数据统计、日志处理
+```
+
+## 4. 深度数据架构
+
+### 4.1 数据库类型选择
+
+**混合数据库架构**
+
+**关系型数据库:PostgreSQL 15+**
+- **用途**:用户信息、企业信息、职位信息、面试记录
+- **优势**:ACID特性、复杂查询、数据一致性
+- **选择理由**:相比MySQL,PostgreSQL在JSON支持、全文搜索、扩展性方面更优秀
+
+**文档数据库:MongoDB 7.0+**
+- **用途**:简历结构化数据、面试分析结果、非结构化日志
+- **优势**:灵活schema、水平扩展、JSON原生支持
+- **选择理由**:简历数据结构多样,MongoDB更适合存储和查询
+
+**搜索引擎:Elasticsearch 8.0+**
+- **用途**:简历全文搜索、职位匹配、日志分析
+- **优势**:强大的全文搜索、实时分析、分布式架构
+
+**缓存数据库:Redis 7.0+**
+- **用途**:会话存储、热点数据缓存、分布式锁
+- **优势**:高性能、丰富数据结构、持久化支持
+
+**CAP理论权衡:**
+- **用户核心数据**:选择CP(一致性+分区容错),使用PostgreSQL
+- **搜索和分析**:选择AP(可用性+分区容错),使用Elasticsearch
+- **缓存数据**:选择AP,使用Redis集群
+
+### 4.2 详细数据模型设计
+
+**PostgreSQL核心实体设计:**
+
+```sql
+-- 用户表
+CREATE TABLE users (
+    id BIGSERIAL PRIMARY KEY,
+    uuid UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(),
+    email VARCHAR(255) UNIQUE NOT NULL,
+    password_hash VARCHAR(255) NOT NULL,
+    user_type VARCHAR(20) NOT NULL CHECK (user_type IN ('candidate', 'hr', 'admin')),
+    profile JSONB,
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    deleted_at TIMESTAMP WITH TIME ZONE
+);
+
+-- 企业表
+CREATE TABLE companies (
+    id BIGSERIAL PRIMARY KEY,
+    uuid UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(),
+    name VARCHAR(255) NOT NULL,
+    industry VARCHAR(100),
+    size_range VARCHAR(50),
+    description TEXT,
+    logo_url VARCHAR(500),
+    website VARCHAR(255),
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+
+-- 职位表
+CREATE TABLE jobs (
+    id BIGSERIAL PRIMARY KEY,
+    uuid UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(),
+    company_id BIGINT NOT NULL REFERENCES companies(id),
+    title VARCHAR(255) NOT NULL,
+    description TEXT,
+    requirements JSONB, -- 技能要求、经验要求等
+    salary_range JSONB, -- {"min": 10000, "max": 20000, "currency": "CNY"}
+    location VARCHAR(255),
+    job_type VARCHAR(50), -- full-time, part-time, contract
+    status VARCHAR(20) DEFAULT 'active',
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+
+-- 简历表(基础信息)
+CREATE TABLE resumes (
+    id BIGSERIAL PRIMARY KEY,
+    uuid UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(),
+    candidate_id BIGINT NOT NULL REFERENCES users(id),
+    original_filename VARCHAR(255),
+    file_url VARCHAR(500),
+    file_type VARCHAR(20),
+    file_size BIGINT,
+    parsing_status VARCHAR(20) DEFAULT 'pending', -- pending, processing, completed, failed
+    parsed_data_id VARCHAR(100), -- MongoDB文档ID
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+
+-- 面试表
+CREATE TABLE interviews (
+    id BIGSERIAL PRIMARY KEY,
+    uuid UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(),
+    job_id BIGINT NOT NULL REFERENCES jobs(id),
+    candidate_id BIGINT NOT NULL REFERENCES users(id),
+    resume_id BIGINT REFERENCES resumes(id),
+    status VARCHAR(20) DEFAULT 'scheduled', -- scheduled, in_progress, completed, cancelled
+    scheduled_at TIMESTAMP WITH TIME ZONE,
+    started_at TIMESTAMP WITH TIME ZONE,
+    completed_at TIMESTAMP WITH TIME ZONE,
+    duration_seconds INTEGER,
+    video_url VARCHAR(500),
+    audio_url VARCHAR(500),
+    transcript_data_id VARCHAR(100), -- MongoDB文档ID
+    analysis_data_id VARCHAR(100), -- MongoDB文档ID
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+
+-- 面试问题表
+CREATE TABLE interview_questions (
+    id BIGSERIAL PRIMARY KEY,
+    interview_id BIGINT NOT NULL REFERENCES interviews(id),
+    question_text TEXT NOT NULL,
+    question_type VARCHAR(50), -- behavioral, technical, situational
+    asked_at TIMESTAMP WITH TIME ZONE,
+    answer_text TEXT,
+    answer_duration_seconds INTEGER,
+    ai_score DECIMAL(3,2), -- 0.00-1.00
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+```
+
+**MongoDB文档结构设计:**
+
+```javascript
+// 简历解析数据集合
+db.parsed_resumes.insertOne({
+  _id: ObjectId(),
+  resume_id: "uuid-from-postgresql",
+  personal_info: {
+    name: "张三",
+    email: "zhangsan@example.com",
+    phone: "+86-13800138000",
+    location: "北京市朝阳区",
+    birth_date: "1990-01-01",
+    gender: "male"
+  },
+  work_experiences: [
+    {
+      company: "阿里巴巴",
+      position: "高级Java开发工程师",
+      start_date: "2020-03-01",
+      end_date: "2023-12-31",
+      description: "负责电商平台后端开发...",
+      skills_used: ["Java", "Spring Boot", "MySQL", "Redis"]
+    }
+  ],
+  education: [
+    {
+      school: "清华大学",
+      degree: "本科",
+      major: "计算机科学与技术",
+      start_date: "2016-09-01",
+      end_date: "2020-06-30",
+      gpa: 3.8
+    }
+  ],
+  skills: [
+    {
+      category: "编程语言",
+      items: [
+        {"name": "Java", "level": "expert", "years": 5},
+        {"name": "Python", "level": "intermediate", "years": 2}
+      ]
+    }
+  ],
+  projects: [
+    {
+      name: "电商推荐系统",
+      description: "基于机器学习的商品推荐系统",
+      technologies: ["Python", "TensorFlow", "Redis"],
+      start_date: "2022-01-01",
+      end_date: "2022-06-30"
+    }
+  ],
+  parsing_metadata: {
+    parsed_at: new Date(),
+    parser_version: "v2.1.0",
+    confidence_score: 0.95,
+    extracted_keywords: ["Java", "Spring Boot", "微服务", "高并发"]
+  }
+});
+
+// 面试分析数据集合
+db.interview_analysis.insertOne({
+  _id: ObjectId(),
+  interview_id: "uuid-from-postgresql",
+  overall_score: 0.78,
+  analysis_dimensions: {
+    technical_competency: {
+      score: 0.82,
+      details: {
+        keyword_coverage: 0.85,
+        technical_depth: 0.80,
+        problem_solving: 0.78
+      }
+    },
+    communication_skills: {
+      score: 0.75,
+      details: {
+        fluency: 0.80,
+        clarity: 0.72,
+        confidence: 0.73
+      }
+    },
+    behavioral_assessment: {
+      score: 0.77,
+      details: {
+        leadership: 0.75,
+        teamwork: 0.80,
+        adaptability: 0.76
+      }
+    }
+  },
+  question_analysis: [
+    {
+      question_id: "q1",
+      question_text: "请介绍一下你的项目经验",
+      answer_analysis: {
+        duration_seconds: 120,
+        word_count: 180,
+        technical_keywords: ["微服务", "Spring Cloud", "Docker"],
+        sentiment_score: 0.8,
+        confidence_level: 0.75
+      }
+    }
+  ],
+  recommendations: [
+    "候选人技术能力较强,建议进入下一轮面试",
+    "沟通表达能力有待提升,可考虑提供相关培训"
+  ],
+  analyzed_at: new Date(),
+  analyzer_version: "v1.5.0"
+});
+```
+
+### 4.3 规范化与反规范化
+
+**关系型数据规范化(第三范式):**
+- **用户表**:避免冗余的个人信息
+- **企业-职位关系**:通过外键关联,避免企业信息重复
+- **面试-问题关系**:一对多关系,问题独立存储
+
+**性能优化的反规范化策略:**
+
+1. **冗余常用字段**
+```sql
+-- 在面试表中冗余候选人姓名和职位标题
+ALTER TABLE interviews ADD COLUMN candidate_name VARCHAR(100);
+ALTER TABLE interviews ADD COLUMN job_title VARCHAR(255);
+
+-- 通过触发器保持数据同步
+CREATE OR REPLACE FUNCTION sync_interview_denormalized_data()
+RETURNS TRIGGER AS $$
+BEGIN
+    UPDATE interviews 
+    SET candidate_name = (SELECT profile->>'name' FROM users WHERE id = NEW.candidate_id),
+        job_title = (SELECT title FROM jobs WHERE id = NEW.job_id)
+    WHERE id = NEW.id;
+    RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+```
+
+2. **预计算聚合数据**
+```sql
+-- 企业统计表
+CREATE TABLE company_statistics (
+    company_id BIGINT PRIMARY KEY REFERENCES companies(id),
+    total_jobs INTEGER DEFAULT 0,
+    active_jobs INTEGER DEFAULT 0,
+    total_interviews INTEGER DEFAULT 0,
+    avg_interview_score DECIMAL(3,2),
+    last_updated TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+```
+
+### 4.4 查询优化与缓存
+
+**索引策略:**
+
+```sql
+-- 复合索引用于常见查询
+CREATE INDEX idx_interviews_candidate_status ON interviews(candidate_id, status);
+CREATE INDEX idx_jobs_company_status ON jobs(company_id, status);
+CREATE INDEX idx_resumes_candidate_parsing ON resumes(candidate_id, parsing_status);
+
+-- 部分索引用于特定条件
+CREATE INDEX idx_active_jobs ON jobs(company_id) WHERE status = 'active';
+CREATE INDEX idx_completed_interviews ON interviews(job_id, completed_at) 
+    WHERE status = 'completed';
+
+-- JSONB字段索引
+CREATE INDEX idx_user_profile_gin ON users USING GIN(profile);
+CREATE INDEX idx_job_requirements_gin ON jobs USING GIN(requirements);
+
+-- 全文搜索索引
+CREATE INDEX idx_jobs_fulltext ON jobs USING GIN(
+    to_tsvector('chinese', title || ' ' || description)
+);
+```
+
+**查询优化示例:**
+
+```sql
+-- 优化前:简历匹配查询
+SELECT r.*, u.profile->>'name' as candidate_name
+FROM resumes r
+JOIN users u ON r.candidate_id = u.id
+WHERE r.parsing_status = 'completed'
+AND EXISTS (
+    SELECT 1 FROM parsed_resumes pr 
+    WHERE pr.resume_id = r.uuid::text
+    AND pr.skills @> '[{"name": "Java"}]'
+);
+
+-- 优化后:使用物化视图
+CREATE MATERIALIZED VIEW resume_search_view AS
+SELECT 
+    r.id,
+    r.uuid,
+    r.candidate_id,
+    u.profile->>'name' as candidate_name,
+    pr.skills,
+    pr.work_experiences,
+    pr.parsing_metadata->>'extracted_keywords' as keywords
+FROM resumes r
+JOIN users u ON r.candidate_id = u.id
+JOIN parsed_resumes pr ON pr.resume_id = r.uuid::text
+WHERE r.parsing_status = 'completed';
+
+CREATE INDEX idx_resume_search_skills ON resume_search_view USING GIN(skills);
+```
+
+**多级缓存策略:**
+
+```
+L1缓存(应用内存):
+- 用户会话信息(30分钟)
+- 常用配置数据(1小时)
+- 热点职位信息(15分钟)
+
+L2缓存(Redis):
+- 用户详细信息(2小时)
+- 简历解析结果(24小时)
+- 搜索结果(30分钟)
+- 面试分析报告(永久,手动失效)
+
+L3缓存(CDN):
+- 静态资源(图片、CSS、JS)
+- 公开的企业信息页面
+```
+
+**Redis缓存设计:**
+
+```java
+@Service
+public class CacheService {
+    
+    // 用户信息缓存
+    @Cacheable(value = "user", key = "#userId", unless = "#result == null")
+    public User getUserById(Long userId) {
+        return userRepository.findById(userId).orElse(null);
+    }
+    
+    // 简历搜索结果缓存
+    @Cacheable(value = "resume_search", key = "#searchKey")
+    public List<Resume> searchResumes(String searchKey, SearchCriteria criteria) {
+        return resumeSearchService.search(searchKey, criteria);
+    }
+    
+    // 分布式锁防止缓存击穿
+    @RedisLock(key = "interview_analysis:#{#interviewId}", waitTime = 5, leaseTime = 30)
+    public InterviewAnalysis getOrGenerateAnalysis(String interviewId) {
+        // 先查缓存,没有则生成
+    }
+}
+```
+
+### 4.5 可扩展性设计
+
+**数据库扩展策略:**
+
+1. **读写分离**
+```
+Master(写):处理所有写操作
+Slave1(读):处理用户查询、简历搜索
+Slave2(读):处理报表查询、数据分析
+Slave3(读):处理面试相关查询
+```
+
+2. **垂直分库**
+```
+user_db:用户、认证相关表
+company_db:企业、职位相关表
+resume_db:简历相关表
+interview_db:面试相关表
+analytics_db:分析、报表相关表
+```
+
+3. **水平分片策略**
+
+**用户表分片(按用户ID):**
+```sql
+-- 分片键:user_id % 16
+user_shard_0: user_id % 16 = 0
+user_shard_1: user_id % 16 = 1
+...
+user_shard_15: user_id % 16 = 15
+```
+
+**面试表分片(按时间):**
+```sql
+-- 按月分片
+interview_2024_01: created_at >= '2024-01-01' AND created_at < '2024-02-01'
+interview_2024_02: created_at >= '2024-02-01' AND created_at < '2024-03-01'
+```
+
+**MongoDB分片配置:**
+```javascript
+// 启用分片
+sh.enableSharding("interview_platform")
+
+// 简历数据按候选人ID分片
+sh.shardCollection(
+    "interview_platform.parsed_resumes",
+    { "resume_id": "hashed" }
+)
+
+// 面试分析按面试ID分片
+sh.shardCollection(
+    "interview_platform.interview_analysis",
+    { "interview_id": "hashed" }
+)
+```
+
+### 4.6 备份与恢复
+
+**备份策略:**
+
+1. **PostgreSQL备份**
+```bash
+# 全量备份(每日凌晨2点)
+#!/bin/bash
+DATE=$(date +%Y%m%d)
+DATABASE="interview_platform"
+BACKUP_DIR="/backup/postgresql"
+
+pg_dump -h localhost -U postgres -d $DATABASE | \
+gzip > $BACKUP_DIR/full_backup_$DATE.sql.gz
+
+# 上传到云存储
+aws s3 cp $BACKUP_DIR/full_backup_$DATE.sql.gz \
+s3://backup-bucket/postgresql/
+
+# 保留30天备份
+find $BACKUP_DIR -name "full_backup_*.sql.gz" -mtime +30 -delete
+```
+
+2. **增量备份(WAL归档)**
+```bash
+# postgresql.conf配置
+wal_level = replica
+archive_mode = on
+archive_command = 'cp %p /backup/wal_archive/%f'
+max_wal_senders = 3
+
+# 增量备份脚本
+pg_basebackup -h localhost -D /backup/base_backup -U replicator -v -P
+```
+
+3. **MongoDB备份**
+```bash
+# 全量备份
+mongodump --host mongodb-cluster --authenticationDatabase admin \
+--username backup_user --password backup_pass \
+--out /backup/mongodb/$(date +%Y%m%d)
+
+# 压缩并上传
+tar -czf /backup/mongodb_$(date +%Y%m%d).tar.gz \
+/backup/mongodb/$(date +%Y%m%d)
+
+aws s3 cp /backup/mongodb_$(date +%Y%m%d).tar.gz \
+s3://backup-bucket/mongodb/
+```
+
+**恢复策略:**
+
+1. **点时间恢复(PITR)**
+```bash
+# 恢复到指定时间点
+pg_ctl stop -D /var/lib/postgresql/data
+rm -rf /var/lib/postgresql/data/*
+
+# 恢复基础备份
+tar -xzf /backup/base_backup.tar.gz -C /var/lib/postgresql/data
+
+# 配置恢复
+echo "restore_command = 'cp /backup/wal_archive/%f %p'" >> \
+/var/lib/postgresql/data/recovery.conf
+echo "recovery_target_time = '2024-01-15 14:30:00'" >> \
+/var/lib/postgresql/data/recovery.conf
+
+pg_ctl start -D /var/lib/postgresql/data
+```
+
+2. **灾难恢复计划**
+
+**RTO(恢复时间目标):4小时**
+**RPO(恢复点目标):1小时**
+
+**恢复优先级:**
+```
+1. 用户认证服务(15分钟内)
+2. 核心业务数据库(1小时内)
+3. 文件存储服务(2小时内)
+4. 分析和报表服务(4小时内)
+```
+
+**异地容灾:**
+- **主站点**:阿里云华东1(杭州)
+- **备站点**:阿里云华北2(北京)
+- **数据同步**:实时主从复制 + 每日异地备份
+
+## 5. 基础设施与部署架构
+
+### 5.1 部署环境
+
+**推荐云服务商:阿里云**
+
+**选择理由:**
+- **本土优势**:国内访问速度快,合规性好
+- **产品完整**:提供完整的云原生解决方案
+- **AI服务**:丰富的AI和机器学习服务
+- **成本效益**:相比AWS在国内使用成本更低
+- **技术支持**:中文技术支持,响应及时
+
+**基础设施规划:**
+
+```
+生产环境架构:
+┌─────────────────────────────────────────────────────────────┐
+│                        CDN (阿里云CDN)                       │
+├─────────────────────────────────────────────────────────────┤
+│                    负载均衡 (SLB)                           │
+├─────────────────────────────────────────────────────────────┤
+│  API网关集群    │    Web服务集群    │    AI服务集群        │
+│  (Kong/Nginx)   │   (Spring Boot)   │   (Python/FastAPI)  │
+├─────────────────────────────────────────────────────────────┤
+│              Kubernetes集群 (ACK)                          │
+├─────────────────────────────────────────────────────────────┤
+│ PostgreSQL集群 │ MongoDB集群 │ Redis集群 │ Elasticsearch │
+│    (RDS)       │    (自建)    │   (Tair)  │    (自建)     │
+├─────────────────────────────────────────────────────────────┤
+│              对象存储 (OSS) + 文件存储 (NAS)                │
+└─────────────────────────────────────────────────────────────┘
+```
+
+**环境规划:**
+
+1. **开发环境(DEV)**
+   - **规模**:单节点,资源共享
+   - **用途**:开发人员日常开发测试
+   - **配置**:2核4GB ECS * 3台
+
+2. **测试环境(TEST)**
+   - **规模**:小规模集群
+   - **用途**:功能测试、集成测试
+   - **配置**:4核8GB ECS * 5台
+
+3. **预生产环境(STAGING)**
+   - **规模**:生产环境缩小版
+   - **用途**:性能测试、用户验收测试
+   - **配置**:8核16GB ECS * 8台
+
+4. **生产环境(PROD)**
+   - **规模**:高可用集群
+   - **用途**:正式对外服务
+   - **配置**:16核32GB ECS * 20台
+
+### 5.2 容器化与编排
+
+**Docker容器化策略:**
+
+**基础镜像标准化:**
+```dockerfile
+# Java应用基础镜像
+FROM openjdk:17-jre-slim
+
+# 安装必要工具
+RUN apt-get update && apt-get install -y \
+    curl \
+    wget \
+    telnet \
+    && rm -rf /var/lib/apt/lists/*
+
+# 创建应用用户
+RUN groupadd -r appuser && useradd -r -g appuser appuser
+
+# 设置工作目录
+WORKDIR /app
+
+# 复制应用
+COPY target/*.jar app.jar
+
+# 设置权限
+RUN chown -R appuser:appuser /app
+USER appuser
+
+# 健康检查
+HEALTHCHECK --interval=30s --timeout=3s --start-period=60s \
+  CMD curl -f http://localhost:8080/actuator/health || exit 1
+
+# 启动应用
+ENTRYPOINT ["java", "-jar", "app.jar"]
+```
+
+**Kubernetes部署配置:**
+
+```yaml
+# 用户服务部署
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: user-service
+  namespace: interview-platform
+spec:
+  replicas: 3
+  selector:
+    matchLabels:
+      app: user-service
+  template:
+    metadata:
+      labels:
+        app: user-service
+    spec:
+      containers:
+      - name: user-service
+        image: registry.cn-hangzhou.aliyuncs.com/interview/user-service:v1.0.0
+        ports:
+        - containerPort: 8080
+        env:
+        - name: SPRING_PROFILES_ACTIVE
+          value: "prod"
+        - name: DB_HOST
+          valueFrom:
+            secretKeyRef:
+              name: db-secret
+              key: host
+        resources:
+          requests:
+            memory: "512Mi"
+            cpu: "250m"
+          limits:
+            memory: "1Gi"
+            cpu: "500m"
+        livenessProbe:
+          httpGet:
+            path: /actuator/health
+            port: 8080
+          initialDelaySeconds: 60
+          periodSeconds: 30
+        readinessProbe:
+          httpGet:
+            path: /actuator/health/readiness
+            port: 8080
+          initialDelaySeconds: 30
+          periodSeconds: 10
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: user-service
+  namespace: interview-platform
+spec:
+  selector:
+    app: user-service
+  ports:
+  - port: 80
+    targetPort: 8080
+  type: ClusterIP
+```
+
+**Helm Chart管理:**
+
+```yaml
+# Chart.yaml
+apiVersion: v2
+name: interview-platform
+description: AI Interview Platform Helm Chart
+type: application
+version: 1.0.0
+appVersion: "1.0.0"
+
+# values.yaml
+global:
+  registry: registry.cn-hangzhou.aliyuncs.com/interview
+  namespace: interview-platform
+
+services:
+  userService:
+    enabled: true
+    replicas: 3
+    image:
+      tag: v1.0.0
+    resources:
+      requests:
+        memory: 512Mi
+        cpu: 250m
+      limits:
+        memory: 1Gi
+        cpu: 500m
+
+  resumeService:
+    enabled: true
+    replicas: 2
+    image:
+      tag: v1.0.0
+
+database:
+  postgresql:
+    host: rm-xxxxxxxx.mysql.rds.aliyuncs.com
+    port: 5432
+    database: interview_platform
+
+redis:
+  host: r-xxxxxxxx.redis.rds.aliyuncs.com
+  port: 6379
+```
+
+### 5.3 CI/CD流程
+
+**GitLab CI/CD Pipeline:**
+
+```yaml
+# .gitlab-ci.yml
+stages:
+  - test
+  - build
+  - security-scan
+  - deploy-dev
+  - deploy-test
+  - deploy-staging
+  - deploy-prod
+
+variables:
+  MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
+  DOCKER_REGISTRY: "registry.cn-hangzhou.aliyuncs.com/interview"
+
+cache:
+  paths:
+    - .m2/repository/
+    - node_modules/
+
+# 单元测试
+unit-test:
+  stage: test
+  image: openjdk:17
+  script:
+    - ./mvnw clean test
+    - ./mvnw jacoco:report
+  artifacts:
+    reports:
+      junit:
+        - target/surefire-reports/TEST-*.xml
+      coverage_report:
+        coverage_format: jacoco
+        path: target/site/jacoco/jacoco.xml
+  coverage: '/Total.*?([0-9]{1,3})%/'
+
+# 代码质量检查
+code-quality:
+  stage: test
+  image: sonarsource/sonar-scanner-cli:latest
+  script:
+    - sonar-scanner
+      -Dsonar.projectKey=$CI_PROJECT_NAME
+      -Dsonar.sources=src/main
+      -Dsonar.tests=src/test
+      -Dsonar.java.binaries=target/classes
+      -Dsonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml
+  only:
+    - main
+    - develop
+
+# 构建Docker镜像
+build-image:
+  stage: build
+  image: docker:latest
+  services:
+    - docker:dind
+  before_script:
+    - docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD $DOCKER_REGISTRY
+  script:
+    - ./mvnw clean package -DskipTests
+    - docker build -t $DOCKER_REGISTRY/$CI_PROJECT_NAME:$CI_COMMIT_SHA .
+    - docker push $DOCKER_REGISTRY/$CI_PROJECT_NAME:$CI_COMMIT_SHA
+    - docker tag $DOCKER_REGISTRY/$CI_PROJECT_NAME:$CI_COMMIT_SHA $DOCKER_REGISTRY/$CI_PROJECT_NAME:latest
+    - docker push $DOCKER_REGISTRY/$CI_PROJECT_NAME:latest
+
+# 安全扫描
+security-scan:
+  stage: security-scan
+  image: aquasec/trivy:latest
+  script:
+    - trivy image --exit-code 1 --severity HIGH,CRITICAL $DOCKER_REGISTRY/$CI_PROJECT_NAME:$CI_COMMIT_SHA
+  allow_failure: true
+
+# 部署到开发环境
+deploy-dev:
+  stage: deploy-dev
+  image: bitnami/kubectl:latest
+  script:
+    - kubectl config use-context dev-cluster
+    - helm upgrade --install $CI_PROJECT_NAME-dev ./helm-chart \
+        --namespace interview-platform-dev \
+        --set image.tag=$CI_COMMIT_SHA \
+        --set environment=dev
+  environment:
+    name: development
+    url: https://dev.interview-platform.com
+  only:
+    - develop
+
+# 部署到生产环境
+deploy-prod:
+  stage: deploy-prod
+  image: bitnami/kubectl:latest
+  script:
+    - kubectl config use-context prod-cluster
+    - helm upgrade --install $CI_PROJECT_NAME ./helm-chart \
+        --namespace interview-platform \
+        --set image.tag=$CI_COMMIT_SHA \
+        --set environment=prod
+  environment:
+    name: production
+    url: https://www.interview-platform.com
+  when: manual
+  only:
+    - main
+```
+
+**部署策略:**
+
+1. **蓝绿部署**
+```yaml
+# 蓝绿部署脚本
+apiVersion: argoproj.io/v1alpha1
+kind: Rollout
+metadata:
+  name: user-service
+spec:
+  replicas: 5
+  strategy:
+    blueGreen:
+      activeService: user-service-active
+      previewService: user-service-preview
+      autoPromotionEnabled: false
+      scaleDownDelaySeconds: 30
+      prePromotionAnalysis:
+        templates:
+        - templateName: success-rate
+        args:
+        - name: service-name
+          value: user-service-preview
+      postPromotionAnalysis:
+        templates:
+        - templateName: success-rate
+        args:
+        - name: service-name
+          value: user-service-active
+```
+
+2. **金丝雀发布**
+```yaml
+apiVersion: argoproj.io/v1alpha1
+kind: Rollout
+metadata:
+  name: resume-service
+spec:
+  replicas: 10
+  strategy:
+    canary:
+      steps:
+      - setWeight: 10
+      - pause: {duration: 5m}
+      - setWeight: 30
+      - pause: {duration: 10m}
+      - setWeight: 50
+      - pause: {duration: 15m}
+      - setWeight: 100
+      canaryService: resume-service-canary
+      stableService: resume-service-stable
+```
+
+### 5.4 监控与警报
+
+**监控架构:**
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│                    Grafana Dashboard                        │
+├─────────────────────────────────────────────────────────────┤
+│              Prometheus + AlertManager                     │
+├─────────────────────────────────────────────────────────────┤
+│  Node Exporter │ App Metrics │ DB Exporter │ Custom Metrics │
+├─────────────────────────────────────────────────────────────┤
+│              ELK Stack (日志聚合分析)                       │
+├─────────────────────────────────────────────────────────────┤
+│              Jaeger (分布式链路追踪)                        │
+└─────────────────────────────────────────────────────────────┘
+```
+
+**Prometheus配置:**
+
+```yaml
+# prometheus.yml
+global:
+  scrape_interval: 15s
+  evaluation_interval: 15s
+
+rule_files:
+  - "alert_rules.yml"
+
+alerting:
+  alertmanagers:
+    - static_configs:
+        - targets:
+          - alertmanager:9093
+
+scrape_configs:
+  # Kubernetes API Server
+  - job_name: 'kubernetes-apiservers'
+    kubernetes_sd_configs:
+    - role: endpoints
+    scheme: https
+    tls_config:
+      ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
+    bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
+    relabel_configs:
+    - source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name]
+      action: keep
+      regex: default;kubernetes;https
+
+  # 应用服务监控
+  - job_name: 'interview-services'
+    kubernetes_sd_configs:
+    - role: endpoints
+      namespaces:
+        names:
+        - interview-platform
+    relabel_configs:
+    - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape]
+      action: keep
+      regex: true
+    - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path]
+      action: replace
+      target_label: __metrics_path__
+      regex: (.+)
+```
+
+**告警规则:**
+
+```yaml
+# alert_rules.yml
+groups:
+- name: interview-platform-alerts
+  rules:
+  # 服务可用性告警
+  - alert: ServiceDown
+    expr: up{job="interview-services"} == 0
+    for: 1m
+    labels:
+      severity: critical
+    annotations:
+      summary: "Service {{ $labels.instance }} is down"
+      description: "{{ $labels.instance }} has been down for more than 1 minute."
+
+  # 高错误率告警
+  - alert: HighErrorRate
+    expr: |
+      (
+        rate(http_requests_total{status=~"5.."}[5m]) /
+        rate(http_requests_total[5m])
+      ) > 0.05
+    for: 5m
+    labels:
+      severity: warning
+    annotations:
+      summary: "High error rate on {{ $labels.instance }}"
+      description: "Error rate is {{ $value | humanizePercentage }} for {{ $labels.instance }}"
+
+  # 高延迟告警
+  - alert: HighLatency
+    expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 0.5
+    for: 5m
+    labels:
+      severity: warning
+    annotations:
+      summary: "High latency on {{ $labels.instance }}"
+      description: "95th percentile latency is {{ $value }}s for {{ $labels.instance }}"
+
+  # 数据库连接告警
+  - alert: DatabaseConnectionHigh
+    expr: pg_stat_activity_count > 80
+    for: 2m
+    labels:
+      severity: warning
+    annotations:
+      summary: "High database connections"
+      description: "Database has {{ $value }} active connections"
+
+  # 内存使用告警
+  - alert: HighMemoryUsage
+    expr: (node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes > 0.85
+    for: 5m
+    labels:
+      severity: warning
+    annotations:
+      summary: "High memory usage on {{ $labels.instance }}"
+      description: "Memory usage is {{ $value | humanizePercentage }}"
+```
+
+**Grafana仪表板:**
+
+```json
+{
+  "dashboard": {
+    "title": "Interview Platform Overview",
+    "panels": [
+      {
+        "title": "Service Health",
+        "type": "stat",
+        "targets": [
+          {
+            "expr": "up{job=\"interview-services\"}",
+            "legendFormat": "{{ instance }}"
+          }
+        ]
+      },
+      {
+        "title": "Request Rate",
+        "type": "graph",
+        "targets": [
+          {
+            "expr": "rate(http_requests_total[5m])",
+            "legendFormat": "{{ service }}"
+          }
+        ]
+      },
+      {
+        "title": "Response Time",
+        "type": "graph",
+        "targets": [
+          {
+            "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))",
+            "legendFormat": "{{ service }}"
+          }
+        ]
+      },
+      {
+        "title": "Error Rate",
+        "type": "graph",
+        "targets": [
+          {
+            "expr": "rate(http_requests_total{status=~\"5..\"}[5m]) / rate(http_requests_total[5m])",
+            "legendFormat": "{{ service }}"
+          }
+        ]
+      }
+    ]
+  }
+}
+```
+
+**日志聚合(ELK Stack):**
+
+```yaml
+# logstash配置
+input {
+  beats {
+    port => 5044
+  }
+}
+
+filter {
+  if [fields][service] {
+    mutate {
+      add_field => { "service_name" => "%{[fields][service]}" }
+    }
+  }
+  
+  # 解析Java应用日志
+  if [service_name] =~ /.*-service/ {
+    grok {
+      match => { 
+        "message" => "%{TIMESTAMP_ISO8601:timestamp} %{LOGLEVEL:level} \[%{DATA:thread}\] %{DATA:logger} - %{GREEDYDATA:log_message}"
+      }
+    }
+    
+    date {
+      match => [ "timestamp", "yyyy-MM-dd HH:mm:ss.SSS" ]
+    }
+  }
+}
+
+output {
+  elasticsearch {
+    hosts => ["elasticsearch:9200"]
+    index => "interview-platform-%{+YYYY.MM.dd}"
+  }
+}
+```
+
+## 6. 安全架构
+
+### 6.1 数据安全
+
+**传输中加密(TLS):**
+
+```nginx
+# Nginx TLS配置
+server {
+    listen 443 ssl http2;
+    server_name api.interview-platform.com;
+    
+    # TLS证书配置
+    ssl_certificate /etc/ssl/certs/interview-platform.crt;
+    ssl_certificate_key /etc/ssl/private/interview-platform.key;
+    
+    # TLS安全配置
+    ssl_protocols TLSv1.2 TLSv1.3;
+    ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384;
+    ssl_prefer_server_ciphers off;
+    ssl_session_cache shared:SSL:10m;
+    ssl_session_timeout 10m;
+    
+    # HSTS
+    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+    
+    # 其他安全头
+    add_header X-Frame-Options DENY;
+    add_header X-Content-Type-Options nosniff;
+    add_header X-XSS-Protection "1; mode=block";
+    add_header Referrer-Policy "strict-origin-when-cross-origin";
+    
+    location / {
+        proxy_pass http://backend-cluster;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+    }
+}
+```
+
+**静态数据加密:**
+
+```java
+// 数据库字段加密
+@Entity
+public class User {
+    @Id
+    private Long id;
+    
+    @Column(name = "email")
+    private String email;
+    
+    // 敏感字段加密存储
+    @Convert(converter = EncryptedStringConverter.class)
+    @Column(name = "phone")
+    private String phone;
+    
+    @Convert(converter = EncryptedStringConverter.class)
+    @Column(name = "id_card")
+    private String idCard;
+}
+
+// 加密转换器
+@Component
+public class EncryptedStringConverter implements AttributeConverter<String, String> {
+    
+    @Autowired
+    private EncryptionService encryptionService;
+    
+    @Override
+    public String convertToDatabaseColumn(String attribute) {
+        return encryptionService.encrypt(attribute);
+    }
+    
+    @Override
+    public String convertToEntityAttribute(String dbData) {
+        return encryptionService.decrypt(dbData);
+    }
+}
+```
+
+**文件存储加密:**
+
+```java
+// 简历文件加密存储
+@Service
+public class ResumeStorageService {
+    
+    public String uploadResume(MultipartFile file, String candidateId) {
+        try {
+            // 生成唯一文件名
+            String fileName = generateSecureFileName(file.getOriginalFilename());
+            
+            // 加密文件内容
+            byte[] encryptedContent = encryptionService.encryptFile(file.getBytes());
+            
+            // 上传到OSS
+            String objectKey = String.format("resumes/%s/%s", candidateId, fileName);
+            ossClient.putObject(bucketName, objectKey, new ByteArrayInputStream(encryptedContent));
+            
+            // 记录文件元数据
+            ResumeFile resumeFile = new ResumeFile();
+            resumeFile.setCandidateId(candidateId);
+            resumeFile.setFileName(fileName);
+            resumeFile.setObjectKey(objectKey);
+            resumeFile.setEncrypted(true);
+            resumeFileRepository.save(resumeFile);
+            
+            return objectKey;
+        } catch (Exception e) {
+            throw new StorageException("Failed to upload resume", e);
+        }
+    }
+}
+```
+
+### 6.2 威胁防护
+
+**API安全防护:**
+
+```java
+// SQL注入防护
+@Repository
+public class UserRepository {
+    
+    // 使用参数化查询
+    @Query("SELECT u FROM User u WHERE u.email = :email AND u.status = :status")
+    Optional<User> findByEmailAndStatus(@Param("email") String email, @Param("status") String status);
+    
+    // 避免动态SQL拼接
+    public List<User> searchUsers(UserSearchCriteria criteria) {
+        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
+        CriteriaQuery<User> query = cb.createQuery(User.class);
+        Root<User> root = query.from(User.class);
+        
+        List<Predicate> predicates = new ArrayList<>();
+        
+        if (StringUtils.hasText(criteria.getName())) {
+            predicates.add(cb.like(root.get("name"), "%" + criteria.getName() + "%"));
+        }
+        
+        if (StringUtils.hasText(criteria.getEmail())) {
+            predicates.add(cb.equal(root.get("email"), criteria.getEmail()));
+        }
+        
+        query.where(predicates.toArray(new Predicate[0]));
+        return entityManager.createQuery(query).getResultList();
+    }
+}
+
+// XSS防护
+@Component
+public class XssFilter implements Filter {
+    
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+            throws IOException, ServletException {
+        
+        XssHttpServletRequestWrapper wrappedRequest = new XssHttpServletRequestWrapper(
+            (HttpServletRequest) request);
+        chain.doFilter(wrappedRequest, response);
+    }
+}
+
+public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
+    
+    public XssHttpServletRequestWrapper(HttpServletRequest request) {
+        super(request);
+    }
+    
+    @Override
+    public String getParameter(String parameter) {
+        String value = super.getParameter(parameter);
+        return cleanXSS(value);
+    }
+    
+    private String cleanXSS(String value) {
+        if (value == null) {
+            return null;
+        }
+        
+        // 移除潜在的XSS攻击代码
+        value = value.replaceAll("<script[^>]*>.*?</script>", "");
+        value = value.replaceAll("javascript:", "");
+        value = value.replaceAll("onload\\s*=", "");
+        value = value.replaceAll("onclick\\s*=", "");
+        
+        return value;
+    }
+}
+
+// CSRF防护
+@Configuration
+@EnableWebSecurity
+public class SecurityConfig {
+    
+    @Bean
+    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+        http
+            .csrf(csrf -> csrf
+                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
+                .ignoringRequestMatchers("/api/public/**")
+            )
+            .sessionManagement(session -> session
+                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+            );
+        
+        return http.build();
+    }
+}
+```
+
+**API限流防护:**
+
+```java
+// Redis实现的令牌桶限流
+@Component
+public class RateLimitService {
+    
+    @Autowired
+    private RedisTemplate<String, String> redisTemplate;
+    
+    public boolean isAllowed(String key, int limit, int windowSeconds) {
+        String script = 
+            "local key = KEYS[1]\n" +
+            "local limit = tonumber(ARGV[1])\n" +
+            "local window = tonumber(ARGV[2])\n" +
+            "local current = redis.call('GET', key)\n" +
+            "if current == false then\n" +
+            "    redis.call('SET', key, 1)\n" +
+            "    redis.call('EXPIRE', key, window)\n" +
+            "    return 1\n" +
+            "else\n" +
+            "    if tonumber(current) < limit then\n" +
+            "        return redis.call('INCR', key)\n" +
+            "    else\n" +
+            "        return 0\n" +
+            "    end\n" +
+            "end";
+        
+        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
+        redisScript.setScriptText(script);
+        redisScript.setResultType(Long.class);
+        
+        Long result = redisTemplate.execute(redisScript, 
+            Collections.singletonList(key), 
+            String.valueOf(limit), 
+            String.valueOf(windowSeconds));
+        
+        return result != null && result > 0;
+    }
+}
+
+// 限流注解
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RateLimit {
+    int value() default 100; // 每分钟请求次数
+    int window() default 60; // 时间窗口(秒)
+    String key() default ""; // 限流key
+}
+
+// 限流切面
+@Aspect
+@Component
+public class RateLimitAspect {
+    
+    @Autowired
+    private RateLimitService rateLimitService;
+    
+    @Around("@annotation(rateLimit)")
+    public Object around(ProceedingJoinPoint point, RateLimit rateLimit) throws Throwable {
+        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
+            .currentRequestAttributes()).getRequest();
+        
+        String key = generateKey(request, rateLimit.key());
+        
+        if (!rateLimitService.isAllowed(key, rateLimit.value(), rateLimit.window())) {
+            throw new RateLimitExceededException("Rate limit exceeded");
+        }
+        
+        return point.proceed();
+    }
+    
+    private String generateKey(HttpServletRequest request, String customKey) {
+        if (StringUtils.hasText(customKey)) {
+            return customKey;
+        }
+        
+        String userKey = getUserIdentifier(request);
+        String uri = request.getRequestURI();
+        return String.format("rate_limit:%s:%s", userKey, uri);
+    }
+}
+```
+
+### 6.3 合规性考虑
+
+**个人信息保护法合规:**
+
+```java
+// 数据脱敏服务
+@Service
+public class DataMaskingService {
+    
+    // 手机号脱敏
+    public String maskPhone(String phone) {
+        if (StringUtils.isEmpty(phone) || phone.length() < 7) {
+            return phone;
+        }
+        return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4);
+    }
+    
+    // 邮箱脱敏
+    public String maskEmail(String email) {
+        if (StringUtils.isEmpty(email) || !email.contains("@")) {
+            return email;
+        }
+        String[] parts = email.split("@");
+        String username = parts[0];
+        if (username.length() <= 2) {
+            return email;
+        }
+        return username.substring(0, 2) + "***@" + parts[1];
+    }
+    
+    // 身份证脱敏
+    public String maskIdCard(String idCard) {
+        if (StringUtils.isEmpty(idCard) || idCard.length() < 8) {
+            return idCard;
+        }
+        return idCard.substring(0, 4) + "**********" + idCard.substring(idCard.length() - 4);
+    }
+}
+
+// 数据访问审计
+@Entity
+@Table(name = "data_access_logs")
+public class DataAccessLog {
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+    
+    @Column(name = "user_id")
+    private Long userId;
+    
+    @Column(name = "resource_type")
+    private String resourceType; // resume, interview, user_profile
+    
+    @Column(name = "resource_id")
+    private String resourceId;
+    
+    @Column(name = "action")
+    private String action; // read, write, delete
+    
+    @Column(name = "ip_address")
+    private String ipAddress;
+    
+    @Column(name = "user_agent")
+    private String userAgent;
+    
+    @Column(name = "access_time")
+    private LocalDateTime accessTime;
+    
+    @Column(name = "purpose")
+    private String purpose; // 访问目的
+}
+
+// 审计切面
+@Aspect
+@Component
+public class DataAccessAuditAspect {
+    
+    @Autowired
+    private DataAccessLogRepository auditRepository;
+    
+    @AfterReturning("@annotation(auditDataAccess)")
+    public void auditDataAccess(JoinPoint joinPoint, AuditDataAccess auditDataAccess) {
+        HttpServletRequest request = getCurrentRequest();
+        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+        
+        DataAccessLog log = new DataAccessLog();
+        log.setUserId(getCurrentUserId(auth));
+        log.setResourceType(auditDataAccess.resourceType());
+        log.setAction(auditDataAccess.action());
+        log.setIpAddress(getClientIpAddress(request));
+        log.setUserAgent(request.getHeader("User-Agent"));
+        log.setAccessTime(LocalDateTime.now());
+        log.setPurpose(auditDataAccess.purpose());
+        
+        auditRepository.save(log);
+    }
+}
+```
+
+**数据保留和删除策略:**
+
+```java
+// 数据生命周期管理
+@Service
+public class DataLifecycleService {
+    
+    // 用户数据删除(用户注销账户时)
+    @Transactional
+    public void deleteUserData(Long userId) {
+        // 1. 匿名化简历数据(保留用于算法训练)
+        anonymizeUserResumes(userId);
+        
+        // 2. 删除面试录音录像
+        deleteInterviewMediaFiles(userId);
+        
+        // 3. 删除个人身份信息
+        deletePersonalIdentifiableInfo(userId);
+        
+        // 4. 保留必要的业务数据(匿名化)
+        anonymizeBusinessData(userId);
+        
+        // 5. 记录删除日志
+        logDataDeletion(userId);
+    }
+    
+    // 定期清理过期数据
+    @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
+    public void cleanupExpiredData() {
+        // 删除90天前的访问日志
+        dataAccessLogRepository.deleteByAccessTimeBefore(
+            LocalDateTime.now().minusDays(90));
+        
+        // 删除1年前的临时文件
+        cleanupTemporaryFiles(LocalDateTime.now().minusYears(1));
+        
+        // 匿名化6个月前的面试数据
+        anonymizeOldInterviewData(LocalDateTime.now().minusMonths(6));
+    }
+}
+```
+
+## 7. 技术栈总结
+
+### 7.1 技术组合一览表
+
+| 层次 | 技术选择 | 版本 | 主要用途 | 备选方案 |
+|------|----------|------|----------|----------|
+| **前端框架** | Vue 3 + TypeScript | 3.4+ | 用户界面开发 | React 18, Angular 17 |
+| **状态管理** | Pinia | 2.1+ | 前端状态管理 | Vuex, Redux Toolkit |
+| **UI组件库** | Element Plus | 2.4+ | UI组件 | Ant Design Vue, Quasar |
+| **构建工具** | Vite | 5.0+ | 前端构建 | Webpack, Rollup |
+| **样式框架** | Tailwind CSS | 3.4+ | 样式开发 | Bootstrap, Bulma |
+| **后端框架** | Spring Boot | 3.2+ | 业务逻辑 | NestJS, Django, FastAPI |
+| **微服务** | Spring Cloud | 2023.0+ | 微服务治理 | Dubbo, gRPC |
+| **API网关** | Kong | 3.4+ | API管理 | Nginx, Zuul, Envoy |
+| **关系数据库** | PostgreSQL | 15+ | 结构化数据 | MySQL 8.0, Oracle |
+| **文档数据库** | MongoDB | 7.0+ | 非结构化数据 | CouchDB, Amazon DynamoDB |
+| **搜索引擎** | Elasticsearch | 8.0+ | 全文搜索 | Solr, Amazon OpenSearch |
+| **缓存** | Redis | 7.0+ | 缓存和会话 | Memcached, Hazelcast |
+| **消息队列** | Apache Kafka | 3.6+ | 异步消息 | RabbitMQ, Apache Pulsar |
+| **容器化** | Docker | 24.0+ | 应用容器化 | Podman, containerd |
+| **容器编排** | Kubernetes | 1.28+ | 容器编排 | Docker Swarm, Nomad |
+| **CI/CD** | GitLab CI | 16.0+ | 持续集成 | Jenkins, GitHub Actions |
+| **监控** | Prometheus + Grafana | 2.47+ / 10.0+ | 系统监控 | Zabbix, DataDog |
+| **日志** | ELK Stack | 8.0+ | 日志聚合 | Fluentd + InfluxDB |
+| **链路追踪** | Jaeger | 1.50+ | 分布式追踪 | Zipkin, SkyWalking |
+| **云服务商** | 阿里云 | - | 基础设施 | AWS, 腾讯云, 华为云 |
+
+### 7.2 优缺点与备选方案
+
+**核心技术选择分析:**
+
+#### 前端技术栈
+
+**Vue 3 + TypeScript**
+- **优点**:
+  - 学习曲线平缓,团队上手快
+  - Composition API提供更好的逻辑复用
+  - TypeScript支持优秀,类型安全
+  - 生态系统成熟,插件丰富
+  - 性能优秀,包体积小
+
+- **缺点**:
+  - 相比React,大型企业采用率较低
+  - 某些第三方库可能优先支持React
+  - 移动端开发需要额外方案
+
+- **备选方案**:
+  - **React 18 + TypeScript**:适合团队有React经验,生态更丰富
+  - **Angular 17**:适合大型企业项目,内置功能完整
+
+#### 后端技术栈
+
+**Java + Spring Boot**
+- **优点**:
+  - 企业级成熟度高,稳定可靠
+  - 性能优秀,JVM优化充分
+  - 微服务生态完善
+  - 人才储备充足
+  - 安全性和合规性支持好
+
+- **缺点**:
+  - 开发效率相对较低
+  - 内存占用较大
+  - 启动时间较长
+
+- **备选方案**:
+  - **Node.js + NestJS**:适合前端团队,开发效率高
+  - **Python + FastAPI**:适合AI算法集成,开发快速
+  - **Go + Gin**:适合高并发场景,性能优秀
+
+#### 数据库选择
+
+**PostgreSQL + MongoDB混合架构**
+- **优点**:
+  - PostgreSQL:ACID特性强,复杂查询支持好
+  - MongoDB:灵活schema,水平扩展容易
+  - 各自发挥优势,互补性强
+
+- **缺点**:
+  - 运维复杂度增加
+  - 数据一致性管理困难
+  - 团队需要掌握多种技术
+
+- **备选方案**:
+  - **纯PostgreSQL**:简化架构,JSON支持较好
+  - **MySQL + Redis**:传统方案,生态成熟
+  - **云原生数据库**:如阿里云PolarDB,运维简单
+
+#### 部署架构
+
+**Kubernetes + Docker**
+- **优点**:
+  - 云原生标准,可移植性强
+  - 自动扩缩容,高可用性
+  - 生态丰富,工具完善
+  - 适合微服务架构
+
+- **缺点**:
+  - 学习曲线陡峭
+  - 运维复杂度高
+  - 资源开销较大
+
+- **备选方案**:
+  - **Serverless架构**:如阿里云函数计算,运维简单
+  - **传统虚拟机**:技术成熟,团队熟悉
+  - **容器云服务**:如阿里云容器服务,降低运维复杂度
+
+**技术选型建议:**
+
+1. **初创团队**:选择Vue + Node.js + MongoDB + Serverless,快速迭代
+2. **中型企业**:选择Vue + Java + PostgreSQL + Kubernetes,平衡性能和开发效率
+3. **大型企业**:选择React + Java + 混合数据库 + 完整微服务,注重稳定性和可扩展性
+
+**分阶段实施策略:**
+
+**第一阶段(MVP)**:
+- 前端:Vue 3 + Element Plus
+- 后端:Spring Boot单体应用
+- 数据库:PostgreSQL
+- 部署:传统云服务器
+
+**第二阶段(扩展)**:
+- 引入Redis缓存
+- 添加MongoDB存储非结构化数据
+- 容器化部署
+- 引入CI/CD
+
+**第三阶段(微服务)**:
+- 拆分微服务
+- 引入Kubernetes
+- 完善监控和日志
+- 引入消息队列
+
+**第四阶段(优化)**:
+- 性能优化
+- 安全加固
+- 多云部署
+- AI能力增强
+
+---
+
+## 总结
+
+本架构设计为AI智能面试平台提供了一套完整、可扩展、安全的技术解决方案。通过微服务架构、云原生技术和现代化的开发运维体系,能够支撑从初期1000家企业到未来百万级用户的业务增长。
+
+关键设计原则:
+1. **可扩展性优先**:支持水平扩展和垂直扩展
+2. **安全性保障**:全方位的安全防护和合规性支持
+3. **高可用性**:多层次的容错和恢复机制
+4. **开发效率**:现代化的开发工具链和自动化流程
+5. **成本控制**:合理的资源配置和优化策略
+
+该架构设计不仅满足当前业务需求,更为未来的技术演进和业务扩展预留了充分的空间。

+ 1956 - 0
AI智能面试平台架构设计.md

@@ -0,0 +1,1956 @@
+# AI智能面试平台架构设计
+
+## 1. 系统架构概述
+
+### 1.1 整体架构模式
+
+**选择:微服务架构 + 云原生设计**
+
+**选择理由:**
+- **可扩展性**:支持百万级用户的水平扩展需求
+- **高可用性**:单个服务故障不影响整体系统
+- **技术多样性**:不同服务可选择最适合的技术栈
+- **团队协作**:支持多团队并行开发
+- **部署灵活性**:支持独立部署和版本管理
+
+**架构特点:**
+- 采用API网关统一入口
+- 服务间通过消息队列异步通信
+- 使用容器化部署和Kubernetes编排
+- 实现服务发现和负载均衡
+
+### 1.2 关键组件与交互图
+
+```
+┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
+│   Web前端       │    │   移动端App     │    │   管理后台      │
+│   (Vue.js)      │    │   (React Native)│    │   (React)       │
+└─────────┬───────┘    └─────────┬───────┘    └─────────┬───────┘
+          │                      │                      │
+          └──────────────────────┼──────────────────────┘
+                                 │
+                    ┌─────────────┴─────────────┐
+                    │      API网关              │
+                    │   (Kong/Nginx Gateway)    │
+                    └─────────────┬─────────────┘
+                                 │
+        ┌────────────────────────┼────────────────────────┐
+        │                       │                       │
+┌───────▼───────┐    ┌─────────▼─────────┐    ┌───────▼───────┐
+│   用户服务     │    │    面试服务       │    │   企业服务     │
+│ (User Service) │    │(Interview Service)│    │(Company Service)│
+└───────┬───────┘    └─────────┬─────────┘    └───────┬───────┘
+        │                      │                      │
+┌───────▼───────┐    ┌─────────▼─────────┐    ┌───────▼───────┐
+│   简历服务     │    │    AI分析服务     │    │   通知服务     │
+│(Resume Service)│    │ (AI Analysis)     │    │(Notification)  │
+└───────┬───────┘    └─────────┬─────────┘    └───────┬───────┘
+        │                      │                      │
+        └──────────────────────┼──────────────────────┘
+                               │
+                    ┌─────────▼─────────┐
+                    │    消息队列       │
+                    │  (Apache Kafka)   │
+                    └───────────────────┘
+```
+
+**核心服务说明:**
+- **用户服务**:用户注册、登录、权限管理
+- **企业服务**:企业信息、职位管理
+- **简历服务**:简历上传、解析、存储
+- **面试服务**:AI面试流程控制、视频处理
+- **AI分析服务**:简历筛选、面试分析、报告生成
+- **通知服务**:邮件、短信、站内消息
+
+### 1.3 系统边界与外部接口
+
+**外部服务集成:**
+- **云存储服务**:阿里云OSS/AWS S3(简历文件、视频存储)
+- **AI服务**:讯飞星火/OpenAI API(自然语言处理)
+- **视频处理**:阿里云视频点播(视频转码、截图)
+- **邮件服务**:阿里云邮件推送/SendGrid
+- **短信服务**:阿里云短信服务
+- **第三方登录**:微信、钉钉、LinkedIn OAuth
+- **支付服务**:支付宝、微信支付
+
+## 2. 前端架构
+
+### 2.1 推荐框架与库
+
+**选择:Vue.js 3 + TypeScript**
+
+**选择理由:**
+- **学习曲线平缓**:团队快速上手
+- **生态完善**:丰富的组件库和工具链
+- **性能优秀**:Composition API和响应式系统
+- **TypeScript支持**:类型安全和开发体验
+- **社区活跃**:持续更新和维护
+
+**技术栈:**
+```javascript
+// 核心框架
+Vue.js 3.3+
+TypeScript 5.0+
+Vite 4.0+
+
+// 路由和状态管理
+Vue Router 4
+Pinia
+
+// UI组件库
+Element Plus
+Tailwind CSS
+
+// 工具库
+Axios (HTTP客户端)
+Day.js (日期处理)
+Lodash (工具函数)
+```
+
+### 2.2 状态管理策略
+
+**选择:Pinia**
+
+**优势:**
+- Vue 3官方推荐
+- TypeScript原生支持
+- 模块化设计
+- 开发工具支持
+
+**状态结构:**
+```typescript
+// stores/user.ts
+export const useUserStore = defineStore('user', {
+  state: () => ({
+    currentUser: null,
+    isAuthenticated: false,
+    permissions: []
+  })
+})
+
+// stores/interview.ts
+export const useInterviewStore = defineStore('interview', {
+  state: () => ({
+    currentInterview: null,
+    questions: [],
+    answers: [],
+    status: 'idle'
+  })
+})
+```
+
+### 2.3 UI组件库建议
+
+**主要选择:Element Plus + Tailwind CSS**
+
+**理由:**
+- **Element Plus**:企业级组件库,功能完整
+- **Tailwind CSS**:原子化CSS,快速样式开发
+- **自定义组件**:业务特定组件的封装
+
+### 2.4 模块组织与性能优化
+
+**项目结构:**
+```
+src/
+├── components/          # 通用组件
+│   ├── common/         # 基础组件
+│   ├── business/       # 业务组件
+│   └── layout/         # 布局组件
+├── views/              # 页面组件
+│   ├── auth/          # 认证相关
+│   ├── interview/     # 面试相关
+│   ├── resume/        # 简历相关
+│   └── dashboard/     # 仪表板
+├── stores/             # 状态管理
+├── composables/        # 组合式函数
+├── utils/              # 工具函数
+├── types/              # TypeScript类型
+└── assets/             # 静态资源
+```
+
+**性能优化措施:**
+- **代码分割**:路由级别的懒加载
+- **组件懒加载**:大型组件按需加载
+- **资源优化**:图片压缩、CDN加速
+- **缓存策略**:HTTP缓存、本地存储
+- **虚拟滚动**:大列表性能优化
+
+## 3. 后端架构
+
+### 3.1 推荐技术栈
+
+**选择:Node.js + NestJS + TypeScript**
+
+**选择理由:**
+- **统一语言栈**:前后端使用相同语言,降低学习成本
+- **高性能**:事件驱动、非阻塞I/O
+- **生态丰富**:NPM包生态完善
+- **微服务友好**:轻量级、易于容器化
+- **实时通信**:WebSocket支持优秀
+
+**技术栈详情:**
+```typescript
+// 核心框架
+Node.js 18+
+NestJS 10+
+TypeScript 5.0+
+
+// 数据库ORM
+TypeORM / Prisma
+
+// 验证和安全
+class-validator
+Passport.js
+JWT
+
+// 消息队列
+Bull (Redis-based)
+
+// 文档和测试
+Swagger
+Jest
+```
+
+**备选方案:**
+- **Java + Spring Boot**:企业级应用首选,生态成熟
+- **Python + FastAPI**:AI集成友好,开发效率高
+- **Go + Gin**:高性能、低资源消耗
+
+### 3.2 API设计原则
+
+**选择:RESTful API + GraphQL混合**
+
+**RESTful API用于:**
+- CRUD操作
+- 文件上传下载
+- 第三方集成
+
+**GraphQL用于:**
+- 复杂数据查询
+- 前端数据聚合
+- 实时订阅
+
+**API设计规范:**
+```typescript
+// RESTful API示例
+GET    /api/v1/users/:id
+POST   /api/v1/users
+PUT    /api/v1/users/:id
+DELETE /api/v1/users/:id
+
+// GraphQL Schema示例
+type User {
+  id: ID!
+  email: String!
+  profile: UserProfile
+  interviews: [Interview!]!
+}
+
+type Query {
+  user(id: ID!): User
+  interviews(filter: InterviewFilter): [Interview!]!
+}
+```
+
+### 3.3 身份验证与授权
+
+**设计方案:JWT + RBAC**
+
+**认证流程:**
+```typescript
+// JWT Token结构
+interface JWTPayload {
+  userId: string
+  email: string
+  roles: string[]
+  permissions: string[]
+  exp: number
+}
+
+// 权限控制装饰器
+@UseGuards(JwtAuthGuard, RolesGuard)
+@Roles('hr', 'admin')
+@Controller('interviews')
+export class InterviewController {
+  @Post()
+  @RequirePermissions('interview:create')
+  createInterview(@Body() dto: CreateInterviewDto) {
+    // 创建面试逻辑
+  }
+}
+```
+
+**安全措施:**
+- **Token刷新机制**:Access Token + Refresh Token
+- **多因素认证**:短信验证码、邮箱验证
+- **设备管理**:登录设备记录和管理
+- **异常检测**:异常登录行为监控
+
+### 3.4 业务逻辑组织
+
+**采用:领域驱动设计(DDD) + 分层架构**
+
+**分层结构:**
+```typescript
+// 领域层 (Domain Layer)
+export class Interview {
+  constructor(
+    private readonly id: InterviewId,
+    private readonly candidateId: UserId,
+    private readonly jobId: JobId,
+    private status: InterviewStatus
+  ) {}
+
+  public start(): void {
+    if (this.status !== InterviewStatus.SCHEDULED) {
+      throw new Error('Interview cannot be started');
+    }
+    this.status = InterviewStatus.IN_PROGRESS;
+  }
+}
+
+// 应用层 (Application Layer)
+@Injectable()
+export class InterviewService {
+  constructor(
+    private readonly interviewRepo: InterviewRepository,
+    private readonly aiService: AIAnalysisService
+  ) {}
+
+  async conductInterview(command: ConductInterviewCommand): Promise<void> {
+    const interview = await this.interviewRepo.findById(command.interviewId);
+    interview.start();
+    await this.interviewRepo.save(interview);
+    
+    // 发布领域事件
+    this.eventBus.publish(new InterviewStartedEvent(interview.id));
+  }
+}
+
+// 基础设施层 (Infrastructure Layer)
+@Injectable()
+export class TypeOrmInterviewRepository implements InterviewRepository {
+  constructor(
+    @InjectRepository(InterviewEntity)
+    private readonly repo: Repository<InterviewEntity>
+  ) {}
+
+  async findById(id: InterviewId): Promise<Interview> {
+    const entity = await this.repo.findOne({ where: { id: id.value } });
+    return InterviewMapper.toDomain(entity);
+  }
+}
+```
+
+### 3.5 异步任务处理
+
+**选择:Redis + Bull队列**
+
+**队列设计:**
+```typescript
+// 队列定义
+export enum QueueNames {
+  RESUME_PROCESSING = 'resume-processing',
+  VIDEO_ANALYSIS = 'video-analysis',
+  EMAIL_NOTIFICATION = 'email-notification',
+  REPORT_GENERATION = 'report-generation'
+}
+
+// 任务处理器
+@Processor(QueueNames.RESUME_PROCESSING)
+export class ResumeProcessor {
+  @Process('parse-resume')
+  async parseResume(job: Job<ParseResumeData>) {
+    const { resumeId, fileUrl } = job.data;
+    
+    // 1. 下载文件
+    const fileBuffer = await this.fileService.download(fileUrl);
+    
+    // 2. AI解析
+    const parsedData = await this.aiService.parseResume(fileBuffer);
+    
+    // 3. 保存结果
+    await this.resumeService.updateParsedData(resumeId, parsedData);
+    
+    // 4. 更新进度
+    job.progress(100);
+  }
+}
+
+// 任务调度
+@Injectable()
+export class TaskScheduler {
+  constructor(
+    @InjectQueue(QueueNames.RESUME_PROCESSING)
+    private resumeQueue: Queue
+  ) {}
+
+  async scheduleResumeProcessing(resumeId: string, fileUrl: string) {
+    await this.resumeQueue.add('parse-resume', {
+      resumeId,
+      fileUrl
+    }, {
+      attempts: 3,
+      backoff: 'exponential',
+      delay: 2000
+    });
+  }
+}
+```
+
+## 4. 深度数据架构
+
+### 4.1 数据库类型选择
+
+**混合架构设计**
+
+**关系型数据库:PostgreSQL**
+- **用途**:用户信息、企业数据、职位信息、面试记录
+- **优势**:ACID特性、复杂查询、数据一致性
+- **版本**:PostgreSQL 15+
+
+**文档数据库:MongoDB**
+- **用途**:简历解析结果、面试分析报告、日志数据
+- **优势**:灵活schema、水平扩展、JSON原生支持
+- **版本**:MongoDB 6.0+
+
+**搜索引擎:Elasticsearch**
+- **用途**:简历全文搜索、职位匹配、数据分析
+- **优势**:全文搜索、实时分析、高性能
+- **版本**:Elasticsearch 8.0+
+
+**缓存数据库:Redis**
+- **用途**:会话存储、热点数据缓存、消息队列
+- **优势**:高性能、丰富数据结构、持久化
+- **版本**:Redis 7.0+
+
+**CAP理论权衡:**
+- **用户核心数据**:选择CP(一致性+分区容错),使用PostgreSQL
+- **搜索和分析**:选择AP(可用性+分区容错),使用Elasticsearch
+- **缓存数据**:选择AP,使用Redis集群
+
+### 4.2 详细数据模型设计
+
+**PostgreSQL核心实体设计:**
+
+```sql
+-- 用户表
+CREATE TABLE users (
+    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+    email VARCHAR(255) UNIQUE NOT NULL,
+    password_hash VARCHAR(255) NOT NULL,
+    user_type VARCHAR(20) NOT NULL CHECK (user_type IN ('candidate', 'hr', 'admin')),
+    status VARCHAR(20) DEFAULT 'active',
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+    last_login_at TIMESTAMP WITH TIME ZONE
+);
+
+-- 企业表
+CREATE TABLE companies (
+    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+    name VARCHAR(255) NOT NULL,
+    industry VARCHAR(100),
+    size_range VARCHAR(50),
+    website VARCHAR(255),
+    description TEXT,
+    logo_url VARCHAR(500),
+    status VARCHAR(20) DEFAULT 'active',
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+);
+
+-- 职位表
+CREATE TABLE jobs (
+    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+    company_id UUID NOT NULL REFERENCES companies(id),
+    title VARCHAR(255) NOT NULL,
+    description TEXT NOT NULL,
+    requirements TEXT,
+    salary_min INTEGER,
+    salary_max INTEGER,
+    location VARCHAR(255),
+    employment_type VARCHAR(50),
+    experience_level VARCHAR(50),
+    skills JSONB,
+    status VARCHAR(20) DEFAULT 'active',
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+);
+
+-- 简历表
+CREATE TABLE resumes (
+    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+    user_id UUID NOT NULL REFERENCES users(id),
+    original_filename VARCHAR(255),
+    file_url VARCHAR(500),
+    file_size INTEGER,
+    mime_type VARCHAR(100),
+    parsing_status VARCHAR(20) DEFAULT 'pending',
+    parsed_data_id VARCHAR(100), -- MongoDB文档ID
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+);
+
+-- 面试表
+CREATE TABLE interviews (
+    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+    job_id UUID NOT NULL REFERENCES jobs(id),
+    candidate_id UUID NOT NULL REFERENCES users(id),
+    hr_id UUID NOT NULL REFERENCES users(id),
+    type VARCHAR(20) NOT NULL CHECK (type IN ('ai', 'human', 'hybrid')),
+    status VARCHAR(20) DEFAULT 'scheduled',
+    scheduled_at TIMESTAMP WITH TIME ZONE,
+    started_at TIMESTAMP WITH TIME ZONE,
+    completed_at TIMESTAMP WITH TIME ZONE,
+    duration_minutes INTEGER,
+    video_url VARCHAR(500),
+    analysis_result_id VARCHAR(100), -- MongoDB文档ID
+    score INTEGER CHECK (score >= 0 AND score <= 100),
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+);
+
+-- 面试问题表
+CREATE TABLE interview_questions (
+    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+    interview_id UUID NOT NULL REFERENCES interviews(id),
+    question_text TEXT NOT NULL,
+    question_type VARCHAR(50),
+    order_index INTEGER NOT NULL,
+    asked_at TIMESTAMP WITH TIME ZONE,
+    answer_text TEXT,
+    answer_duration_seconds INTEGER,
+    answer_score INTEGER CHECK (answer_score >= 0 AND answer_score <= 100),
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+);
+```
+
+**MongoDB文档结构:**
+
+```javascript
+// 简历解析结果集合
+db.parsed_resumes.insertOne({
+  _id: ObjectId(),
+  resume_id: "uuid",
+  parsing_version: "1.0",
+  parsed_at: new Date(),
+  personal_info: {
+    name: "张三",
+    email: "zhangsan@example.com",
+    phone: "+86-138-0000-0000",
+    location: "北京市朝阳区",
+    birth_date: "1990-01-01",
+    gender: "male"
+  },
+  education: [{
+    institution: "清华大学",
+    degree: "本科",
+    major: "计算机科学与技术",
+    start_date: "2008-09",
+    end_date: "2012-06",
+    gpa: 3.8
+  }],
+  work_experience: [{
+    company: "阿里巴巴",
+    position: "高级软件工程师",
+    start_date: "2020-03",
+    end_date: "2023-08",
+    description: "负责电商平台后端开发",
+    achievements: ["优化系统性能提升30%", "主导微服务架构改造"]
+  }],
+  skills: {
+    programming_languages: ["Java", "Python", "JavaScript"],
+    frameworks: ["Spring Boot", "React", "Vue.js"],
+    databases: ["MySQL", "Redis", "MongoDB"],
+    tools: ["Git", "Docker", "Kubernetes"]
+  },
+  projects: [{
+    name: "电商推荐系统",
+    description: "基于机器学习的商品推荐系统",
+    technologies: ["Python", "TensorFlow", "Redis"],
+    start_date: "2022-01",
+    end_date: "2022-06"
+  }],
+  certifications: [{
+    name: "AWS认证解决方案架构师",
+    issuer: "Amazon",
+    issue_date: "2022-03",
+    expiry_date: "2025-03"
+  }],
+  languages: [{
+    language: "英语",
+    proficiency: "流利"
+  }],
+  extracted_keywords: ["Java", "微服务", "高并发", "分布式系统"],
+  confidence_score: 0.95
+});
+
+// 面试分析结果集合
+db.interview_analysis.insertOne({
+  _id: ObjectId(),
+  interview_id: "uuid",
+  analysis_version: "2.0",
+  analyzed_at: new Date(),
+  overall_score: 85,
+  dimensions: {
+    technical_skills: {
+      score: 88,
+      details: {
+        programming_knowledge: 90,
+        system_design: 85,
+        problem_solving: 90
+      }
+    },
+    communication: {
+      score: 82,
+      details: {
+        clarity: 85,
+        fluency: 80,
+        confidence: 80
+      }
+    },
+    behavioral: {
+      score: 85,
+      details: {
+        teamwork: 88,
+        leadership: 82,
+        adaptability: 85
+      }
+    }
+  },
+  question_analysis: [{
+    question_id: "uuid",
+    question_text: "请介绍一下你的项目经验",
+    answer_analysis: {
+      content_relevance: 0.9,
+      technical_depth: 0.85,
+      communication_clarity: 0.8,
+      keywords_mentioned: ["微服务", "高并发", "Redis"],
+      sentiment_score: 0.7,
+      confidence_level: 0.8
+    },
+    transcription: "我在阿里巴巴主要负责...",
+    duration_seconds: 120
+  }],
+  video_analysis: {
+    facial_expressions: {
+      confidence_avg: 0.75,
+      engagement_avg: 0.8,
+      stress_indicators: 0.2
+    },
+    voice_analysis: {
+      pace_consistency: 0.8,
+      volume_stability: 0.85,
+      filler_words_count: 5
+    }
+  },
+  recommendations: [
+    "候选人技术能力强,建议进入下一轮",
+    "沟通表达可以更加简洁明了",
+    "对系统设计有深入理解"
+  ],
+  red_flags: [],
+  processing_metadata: {
+    ai_model_version: "gpt-4",
+    processing_time_ms: 15000,
+    confidence_threshold: 0.7
+  }
+});
+```
+
+### 4.3 规范化与反规范化
+
+**关系型数据规范化(第三范式):**
+- **用户表**:符合3NF,避免数据冗余
+- **企业-职位关系**:通过外键关联,保持数据一致性
+- **面试-问题关系**:一对多关系,规范化存储
+
+**性能优化的反规范化:**
+```sql
+-- 在jobs表中冗余company_name,避免频繁JOIN
+ALTER TABLE jobs ADD COLUMN company_name VARCHAR(255);
+
+-- 在interviews表中冗余candidate_name和job_title
+ALTER TABLE interviews ADD COLUMN candidate_name VARCHAR(255);
+ALTER TABLE interviews ADD COLUMN job_title VARCHAR(255);
+
+-- 创建物化视图用于复杂查询
+CREATE MATERIALIZED VIEW interview_summary AS
+SELECT 
+    i.id,
+    i.status,
+    i.score,
+    u.email as candidate_email,
+    j.title as job_title,
+    c.name as company_name,
+    i.completed_at
+FROM interviews i
+JOIN users u ON i.candidate_id = u.id
+JOIN jobs j ON i.job_id = j.id
+JOIN companies c ON j.company_id = c.id;
+```
+
+### 4.4 查询优化与缓存
+
+**索引策略:**
+```sql
+-- 复合索引用于常见查询
+CREATE INDEX idx_interviews_status_date ON interviews(status, created_at);
+CREATE INDEX idx_jobs_company_status ON jobs(company_id, status);
+CREATE INDEX idx_resumes_user_status ON resumes(user_id, parsing_status);
+
+-- 部分索引用于特定条件
+CREATE INDEX idx_active_jobs ON jobs(created_at) WHERE status = 'active';
+
+-- 表达式索引
+CREATE INDEX idx_users_email_lower ON users(LOWER(email));
+
+-- JSONB索引用于技能搜索
+CREATE INDEX idx_jobs_skills_gin ON jobs USING GIN(skills);
+```
+
+**多级缓存策略:**
+
+```typescript
+// L1缓存:应用内存缓存
+@Injectable()
+export class CacheService {
+  private readonly memoryCache = new Map<string, any>();
+  
+  // L2缓存:Redis缓存
+  constructor(
+    @Inject('REDIS_CLIENT') private redis: Redis
+  ) {}
+  
+  async get<T>(key: string): Promise<T | null> {
+    // 1. 检查内存缓存
+    if (this.memoryCache.has(key)) {
+      return this.memoryCache.get(key);
+    }
+    
+    // 2. 检查Redis缓存
+    const cached = await this.redis.get(key);
+    if (cached) {
+      const data = JSON.parse(cached);
+      this.memoryCache.set(key, data);
+      return data;
+    }
+    
+    return null;
+  }
+  
+  async set(key: string, value: any, ttl: number = 3600): Promise<void> {
+    // 同时设置内存和Redis缓存
+    this.memoryCache.set(key, value);
+    await this.redis.setex(key, ttl, JSON.stringify(value));
+  }
+}
+
+// 缓存装饰器
+export function Cacheable(ttl: number = 3600) {
+  return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
+    const method = descriptor.value;
+    
+    descriptor.value = async function (...args: any[]) {
+      const cacheKey = `${target.constructor.name}:${propertyName}:${JSON.stringify(args)}`;
+      const cacheService = this.cacheService;
+      
+      let result = await cacheService.get(cacheKey);
+      if (!result) {
+        result = await method.apply(this, args);
+        await cacheService.set(cacheKey, result, ttl);
+      }
+      
+      return result;
+    };
+  };
+}
+
+// 使用示例
+@Injectable()
+export class JobService {
+  @Cacheable(1800) // 30分钟缓存
+  async getActiveJobs(companyId: string): Promise<Job[]> {
+    return this.jobRepository.findActiveByCompany(companyId);
+  }
+}
+```
+
+### 4.5 可扩展性设计
+
+**数据库分片策略:**
+
+```typescript
+// 用户数据按用户ID分片
+class UserShardingStrategy {
+  getShardKey(userId: string): string {
+    const hash = crypto.createHash('md5').update(userId).digest('hex');
+    const shardIndex = parseInt(hash.substring(0, 2), 16) % 4;
+    return `user_shard_${shardIndex}`;
+  }
+}
+
+// 面试数据按时间分片
+class InterviewShardingStrategy {
+  getShardKey(date: Date): string {
+    const year = date.getFullYear();
+    const month = date.getMonth() + 1;
+    return `interview_${year}_${month.toString().padStart(2, '0')}`;
+  }
+}
+
+// 分片路由器
+@Injectable()
+export class ShardingRouter {
+  private readonly shards = new Map<string, DataSource>();
+  
+  constructor(
+    private userStrategy: UserShardingStrategy,
+    private interviewStrategy: InterviewShardingStrategy
+  ) {
+    this.initializeShards();
+  }
+  
+  getUserShard(userId: string): DataSource {
+    const shardKey = this.userStrategy.getShardKey(userId);
+    return this.shards.get(shardKey)!;
+  }
+  
+  getInterviewShard(date: Date): DataSource {
+    const shardKey = this.interviewStrategy.getShardKey(date);
+    return this.shards.get(shardKey)!;
+  }
+}
+```
+
+**读写分离配置:**
+```typescript
+// 数据源配置
+@Module({
+  imports: [
+    TypeOrmModule.forRoot({
+      name: 'master',
+      type: 'postgres',
+      host: 'master.db.example.com',
+      replication: {
+        master: {
+          host: 'master.db.example.com',
+          port: 5432,
+          username: 'master_user',
+          password: 'master_pass',
+          database: 'interview_platform'
+        },
+        slaves: [
+          {
+            host: 'slave1.db.example.com',
+            port: 5432,
+            username: 'slave_user',
+            password: 'slave_pass',
+            database: 'interview_platform'
+          },
+          {
+            host: 'slave2.db.example.com',
+            port: 5432,
+            username: 'slave_user',
+            password: 'slave_pass',
+            database: 'interview_platform'
+          }
+        ]
+      }
+    })
+  ]
+})
+export class DatabaseModule {}
+```
+
+### 4.6 备份与恢复
+
+**备份策略:**
+
+```bash
+#!/bin/bash
+# 全量备份脚本
+BACKUP_DIR="/backup/$(date +%Y%m%d)"
+mkdir -p $BACKUP_DIR
+
+# PostgreSQL备份
+pg_dump -h $PG_HOST -U $PG_USER -d $PG_DATABASE | gzip > $BACKUP_DIR/postgres_full.sql.gz
+
+# MongoDB备份
+mongodump --host $MONGO_HOST --db $MONGO_DATABASE --out $BACKUP_DIR/mongodb/
+
+# Redis备份
+redis-cli --rdb $BACKUP_DIR/redis_dump.rdb
+
+# 上传到云存储
+aws s3 sync $BACKUP_DIR s3://backup-bucket/$(date +%Y%m%d)/
+
+# 清理本地备份(保留7天)
+find /backup -type d -mtime +7 -exec rm -rf {} +
+```
+
+**增量备份:**
+```bash
+#!/bin/bash
+# PostgreSQL WAL归档
+archive_command = 'aws s3 cp %p s3://wal-archive-bucket/%f'
+
+# MongoDB Oplog备份
+mongodump --host $MONGO_HOST --db local --collection oplog.rs --query '{"ts": {"$gte": Timestamp('$(date -d "1 hour ago" +%s)', 0)}}' --out $BACKUP_DIR/oplog/
+```
+
+**恢复策略:**
+```bash
+#!/bin/bash
+# 点时间恢复(PITR)
+function restore_to_point_in_time() {
+  local target_time=$1
+  
+  # 1. 恢复基础备份
+  pg_restore -h $PG_HOST -U $PG_USER -d $PG_DATABASE $BACKUP_DIR/postgres_full.sql.gz
+  
+  # 2. 应用WAL日志到指定时间点
+  pg_ctl start -D $PGDATA
+  psql -c "SELECT pg_wal_replay_resume();"
+  
+  # 3. 验证数据一致性
+  psql -c "SELECT count(*) FROM users;"
+}
+```
+
+## 5. 基础设施与部署架构
+
+### 5.1 部署环境
+
+**推荐:阿里云 + Kubernetes**
+
+**选择理由:**
+- **本土化优势**:国内访问速度快,合规性好
+- **生态完整**:提供全套云原生服务
+- **成本效益**:相比AWS更具价格优势
+- **技术支持**:中文技术支持,响应及时
+
+**云服务选择:**
+```yaml
+# 核心服务
+Compute: 
+  - ECS (弹性计算服务)
+  - ACK (容器服务Kubernetes版)
+  - Serverless App Engine
+
+Storage:
+  - OSS (对象存储)
+  - NAS (文件存储)
+  - CPFS (并行文件系统)
+
+Database:
+  - RDS PostgreSQL
+  - MongoDB Atlas
+  - Redis Enterprise
+  - AnalyticDB (数据仓库)
+
+Networking:
+  - VPC (专有网络)
+  - SLB (负载均衡)
+  - CDN (内容分发)
+  - API Gateway
+
+Security:
+  - WAF (Web应用防火墙)
+  - Anti-DDoS
+  - SSL证书服务
+  - RAM (访问控制)
+
+Monitoring:
+  - CloudMonitor
+  - Log Service
+  - Application Real-Time Monitoring Service (ARMS)
+```
+
+### 5.2 容器化与编排
+
+**Docker容器化:**
+
+```dockerfile
+# Node.js应用Dockerfile
+FROM node:18-alpine AS builder
+
+WORKDIR /app
+COPY package*.json ./
+RUN npm ci --only=production
+
+FROM node:18-alpine AS runtime
+WORKDIR /app
+
+# 创建非root用户
+RUN addgroup -g 1001 -S nodejs
+RUN adduser -S nextjs -u 1001
+
+COPY --from=builder /app/node_modules ./node_modules
+COPY --chown=nextjs:nodejs . .
+
+USER nextjs
+EXPOSE 3000
+
+HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
+  CMD curl -f http://localhost:3000/health || exit 1
+
+CMD ["npm", "start"]
+```
+
+**Kubernetes部署配置:**
+
+```yaml
+# deployment.yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: interview-api
+  namespace: production
+spec:
+  replicas: 3
+  strategy:
+    type: RollingUpdate
+    rollingUpdate:
+      maxSurge: 1
+      maxUnavailable: 0
+  selector:
+    matchLabels:
+      app: interview-api
+  template:
+    metadata:
+      labels:
+        app: interview-api
+    spec:
+      containers:
+      - name: api
+        image: registry.cn-hangzhou.aliyuncs.com/interview/api:v1.0.0
+        ports:
+        - containerPort: 3000
+        env:
+        - name: NODE_ENV
+          value: "production"
+        - name: DATABASE_URL
+          valueFrom:
+            secretKeyRef:
+              name: db-secret
+              key: url
+        resources:
+          requests:
+            memory: "256Mi"
+            cpu: "250m"
+          limits:
+            memory: "512Mi"
+            cpu: "500m"
+        livenessProbe:
+          httpGet:
+            path: /health
+            port: 3000
+          initialDelaySeconds: 30
+          periodSeconds: 10
+        readinessProbe:
+          httpGet:
+            path: /ready
+            port: 3000
+          initialDelaySeconds: 5
+          periodSeconds: 5
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: interview-api-service
+spec:
+  selector:
+    app: interview-api
+  ports:
+  - protocol: TCP
+    port: 80
+    targetPort: 3000
+  type: ClusterIP
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: interview-api-ingress
+  annotations:
+    kubernetes.io/ingress.class: nginx
+    cert-manager.io/cluster-issuer: letsencrypt-prod
+    nginx.ingress.kubernetes.io/rate-limit: "100"
+spec:
+  tls:
+  - hosts:
+    - api.interview-platform.com
+    secretName: api-tls
+  rules:
+  - host: api.interview-platform.com
+    http:
+      paths:
+      - path: /
+        pathType: Prefix
+        backend:
+          service:
+            name: interview-api-service
+            port:
+              number: 80
+```
+
+**Helm Chart配置:**
+
+```yaml
+# values.yaml
+replicaCount: 3
+
+image:
+  repository: registry.cn-hangzhou.aliyuncs.com/interview/api
+  tag: "v1.0.0"
+  pullPolicy: IfNotPresent
+
+service:
+  type: ClusterIP
+  port: 80
+
+ingress:
+  enabled: true
+  className: nginx
+  annotations:
+    cert-manager.io/cluster-issuer: letsencrypt-prod
+  hosts:
+    - host: api.interview-platform.com
+      paths:
+        - path: /
+          pathType: Prefix
+  tls:
+    - secretName: api-tls
+      hosts:
+        - api.interview-platform.com
+
+resources:
+  limits:
+    cpu: 500m
+    memory: 512Mi
+  requests:
+    cpu: 250m
+    memory: 256Mi
+
+autoscaling:
+  enabled: true
+  minReplicas: 3
+  maxReplicas: 10
+  targetCPUUtilizationPercentage: 80
+  targetMemoryUtilizationPercentage: 80
+
+env:
+  NODE_ENV: production
+  LOG_LEVEL: info
+
+secrets:
+  database:
+    url: "postgresql://user:pass@host:5432/db"
+  redis:
+    url: "redis://redis-cluster:6379"
+  jwt:
+    secret: "your-jwt-secret"
+```
+
+### 5.3 CI/CD流程
+
+**GitLab CI/CD Pipeline:**
+
+```yaml
+# .gitlab-ci.yml
+stages:
+  - test
+  - build
+  - security
+  - deploy-staging
+  - deploy-production
+
+variables:
+  DOCKER_REGISTRY: registry.cn-hangzhou.aliyuncs.com
+  IMAGE_NAME: interview/api
+  KUBECONFIG_FILE: $KUBECONFIG_STAGING
+
+# 测试阶段
+test:unit:
+  stage: test
+  image: node:18-alpine
+  script:
+    - npm ci
+    - npm run test:unit
+    - npm run test:coverage
+  coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'
+  artifacts:
+    reports:
+      coverage_report:
+        coverage_format: cobertura
+        path: coverage/cobertura-coverage.xml
+
+test:integration:
+  stage: test
+  image: node:18-alpine
+  services:
+    - postgres:13
+    - redis:7-alpine
+  variables:
+    POSTGRES_DB: test_db
+    POSTGRES_USER: test_user
+    POSTGRES_PASSWORD: test_pass
+  script:
+    - npm ci
+    - npm run test:integration
+
+# 构建阶段
+build:docker:
+  stage: build
+  image: docker:20.10.16
+  services:
+    - docker:20.10.16-dind
+  before_script:
+    - echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin $DOCKER_REGISTRY
+  script:
+    - docker build -t $DOCKER_REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA .
+    - docker build -t $DOCKER_REGISTRY/$IMAGE_NAME:latest .
+    - docker push $DOCKER_REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA
+    - docker push $DOCKER_REGISTRY/$IMAGE_NAME:latest
+  only:
+    - main
+    - develop
+
+# 安全扫描
+security:container-scan:
+  stage: security
+  image: aquasec/trivy:latest
+  script:
+    - trivy image --exit-code 0 --severity HIGH,CRITICAL $DOCKER_REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA
+  allow_failure: true
+
+security:sast:
+  stage: security
+  image: node:18-alpine
+  script:
+    - npm ci
+    - npm audit --audit-level high
+    - npx eslint . --ext .ts,.js --format gitlab
+  artifacts:
+    reports:
+      sast: gl-sast-report.json
+
+# 部署到测试环境
+deploy:staging:
+  stage: deploy-staging
+  image: bitnami/kubectl:latest
+  script:
+    - echo $KUBECONFIG_STAGING | base64 -d > kubeconfig
+    - export KUBECONFIG=kubeconfig
+    - helm upgrade --install interview-api-staging ./helm-chart \
+        --namespace staging \
+        --set image.tag=$CI_COMMIT_SHA \
+        --set ingress.hosts[0].host=staging-api.interview-platform.com \
+        --values ./helm-chart/values-staging.yaml
+  environment:
+    name: staging
+    url: https://staging-api.interview-platform.com
+  only:
+    - develop
+
+# 部署到生产环境
+deploy:production:
+  stage: deploy-production
+  image: bitnami/kubectl:latest
+  script:
+    - echo $KUBECONFIG_PRODUCTION | base64 -d > kubeconfig
+    - export KUBECONFIG=kubeconfig
+    - helm upgrade --install interview-api ./helm-chart \
+        --namespace production \
+        --set image.tag=$CI_COMMIT_SHA \
+        --values ./helm-chart/values-production.yaml
+  environment:
+    name: production
+    url: https://api.interview-platform.com
+  when: manual
+  only:
+    - main
+```
+
+### 5.4 监控与警报
+
+**Prometheus + Grafana监控栈:**
+
+```yaml
+# prometheus-config.yaml
+global:
+  scrape_interval: 15s
+  evaluation_interval: 15s
+
+rule_files:
+  - "alert_rules.yml"
+
+alerting:
+  alertmanagers:
+    - static_configs:
+        - targets:
+          - alertmanager:9093
+
+scrape_configs:
+  - job_name: 'kubernetes-apiservers'
+    kubernetes_sd_configs:
+    - role: endpoints
+    scheme: https
+    tls_config:
+      ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
+    bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
+    relabel_configs:
+    - source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name]
+      action: keep
+      regex: default;kubernetes;https
+
+  - job_name: 'interview-api'
+    kubernetes_sd_configs:
+    - role: pod
+    relabel_configs:
+    - source_labels: [__meta_kubernetes_pod_label_app]
+      action: keep
+      regex: interview-api
+    - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
+      action: keep
+      regex: true
+    - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
+      action: replace
+      target_label: __metrics_path__
+      regex: (.+)
+```
+
+**告警规则:**
+
+```yaml
+# alert_rules.yml
+groups:
+- name: interview-platform
+  rules:
+  - alert: HighErrorRate
+    expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.1
+    for: 5m
+    labels:
+      severity: critical
+    annotations:
+      summary: "High error rate detected"
+      description: "Error rate is {{ $value }} errors per second"
+
+  - alert: HighResponseTime
+    expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 1
+    for: 5m
+    labels:
+      severity: warning
+    annotations:
+      summary: "High response time detected"
+      description: "95th percentile response time is {{ $value }} seconds"
+
+  - alert: DatabaseConnectionHigh
+    expr: pg_stat_activity_count > 80
+    for: 2m
+    labels:
+      severity: warning
+    annotations:
+      summary: "High database connection count"
+      description: "Database has {{ $value }} active connections"
+
+  - alert: PodCrashLooping
+    expr: rate(kube_pod_container_status_restarts_total[15m]) > 0
+    for: 5m
+    labels:
+      severity: critical
+    annotations:
+      summary: "Pod is crash looping"
+      description: "Pod {{ $labels.pod }} in namespace {{ $labels.namespace }} is crash looping"
+```
+
+**Grafana仪表板配置:**
+
+```json
+{
+  "dashboard": {
+    "title": "Interview Platform Overview",
+    "panels": [
+      {
+        "title": "Request Rate",
+        "type": "graph",
+        "targets": [
+          {
+            "expr": "rate(http_requests_total[5m])",
+            "legendFormat": "{{method}} {{status}}"
+          }
+        ]
+      },
+      {
+        "title": "Response Time",
+        "type": "graph",
+        "targets": [
+          {
+            "expr": "histogram_quantile(0.50, rate(http_request_duration_seconds_bucket[5m]))",
+            "legendFormat": "50th percentile"
+          },
+          {
+            "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))",
+            "legendFormat": "95th percentile"
+          }
+        ]
+      },
+      {
+        "title": "Database Performance",
+        "type": "graph",
+        "targets": [
+          {
+            "expr": "pg_stat_database_tup_fetched",
+            "legendFormat": "Rows fetched"
+          },
+          {
+            "expr": "pg_stat_database_tup_inserted",
+            "legendFormat": "Rows inserted"
+          }
+        ]
+      }
+    ]
+  }
+}
+```
+
+## 6. 安全架构
+
+### 6.1 数据安全
+
+**传输加密(TLS):**
+
+```nginx
+# Nginx TLS配置
+server {
+    listen 443 ssl http2;
+    server_name api.interview-platform.com;
+    
+    # TLS证书配置
+    ssl_certificate /etc/ssl/certs/api.interview-platform.com.crt;
+    ssl_certificate_key /etc/ssl/private/api.interview-platform.com.key;
+    
+    # TLS安全配置
+    ssl_protocols TLSv1.2 TLSv1.3;
+    ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
+    ssl_prefer_server_ciphers off;
+    ssl_session_cache shared:SSL:10m;
+    ssl_session_timeout 10m;
+    
+    # HSTS
+    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+    
+    # 其他安全头
+    add_header X-Frame-Options DENY;
+    add_header X-Content-Type-Options nosniff;
+    add_header X-XSS-Protection "1; mode=block";
+    add_header Referrer-Policy "strict-origin-when-cross-origin";
+    
+    location / {
+        proxy_pass http://interview-api-service;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+    }
+}
+```
+
+**存储加密:**
+
+```typescript
+// 数据库字段加密
+@Entity()
+export class User {
+  @PrimaryGeneratedColumn('uuid')
+  id: string;
+  
+  @Column()
+  email: string;
+  
+  @Column({ transformer: new EncryptionTransformer() })
+  phone: string; // 加密存储手机号
+  
+  @Column({ transformer: new EncryptionTransformer() })
+  idCard: string; // 加密存储身份证号
+}
+
+// 加密转换器
+export class EncryptionTransformer implements ValueTransformer {
+  private readonly algorithm = 'aes-256-gcm';
+  private readonly key = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex');
+  
+  to(value: string): string {
+    if (!value) return value;
+    
+    const iv = crypto.randomBytes(16);
+    const cipher = crypto.createCipher(this.algorithm, this.key);
+    cipher.setAAD(Buffer.from('interview-platform'));
+    
+    let encrypted = cipher.update(value, 'utf8', 'hex');
+    encrypted += cipher.final('hex');
+    
+    const authTag = cipher.getAuthTag();
+    
+    return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted;
+  }
+  
+  from(value: string): string {
+    if (!value) return value;
+    
+    const [ivHex, authTagHex, encrypted] = value.split(':');
+    const iv = Buffer.from(ivHex, 'hex');
+    const authTag = Buffer.from(authTagHex, 'hex');
+    
+    const decipher = crypto.createDecipher(this.algorithm, this.key);
+    decipher.setAAD(Buffer.from('interview-platform'));
+    decipher.setAuthTag(authTag);
+    
+    let decrypted = decipher.update(encrypted, 'hex', 'utf8');
+    decrypted += decipher.final('utf8');
+    
+    return decrypted;
+  }
+}
+```
+
+### 6.2 威胁防护
+
+**API安全中间件:**
+
+```typescript
+// 速率限制
+@Injectable()
+export class RateLimitMiddleware implements NestMiddleware {
+  private readonly limiter = rateLimit({
+    windowMs: 15 * 60 * 1000, // 15分钟
+    max: 100, // 最多100个请求
+    message: 'Too many requests from this IP',
+    standardHeaders: true,
+    legacyHeaders: false,
+  });
+  
+  use(req: Request, res: Response, next: NextFunction) {
+    this.limiter(req, res, next);
+  }
+}
+
+// SQL注入防护
+@Injectable()
+export class SqlInjectionGuard implements CanActivate {
+  private readonly sqlInjectionPatterns = [
+    /('|(\-\-)|(;)|(\||\|)|(\*|\*))/i,
+    /(exec(\s|\+)+(s|x)p\w+)/i,
+    /union.*select/i,
+    /insert.*into/i,
+    /delete.*from/i,
+    /update.*set/i,
+    /drop.*table/i
+  ];
+  
+  canActivate(context: ExecutionContext): boolean {
+    const request = context.switchToHttp().getRequest();
+    const { query, body, params } = request;
+    
+    const allInputs = { ...query, ...body, ...params };
+    
+    for (const [key, value] of Object.entries(allInputs)) {
+      if (typeof value === 'string' && this.containsSqlInjection(value)) {
+        throw new BadRequestException(`Potential SQL injection detected in ${key}`);
+      }
+    }
+    
+    return true;
+  }
+  
+  private containsSqlInjection(input: string): boolean {
+    return this.sqlInjectionPatterns.some(pattern => pattern.test(input));
+  }
+}
+
+// XSS防护
+@Injectable()
+export class XssProtectionInterceptor implements NestInterceptor {
+  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
+    const request = context.switchToHttp().getRequest();
+    
+    // 清理请求数据
+    this.sanitizeObject(request.body);
+    this.sanitizeObject(request.query);
+    
+    return next.handle().pipe(
+      map(data => {
+        // 清理响应数据
+        return this.sanitizeObject(data);
+      })
+    );
+  }
+  
+  private sanitizeObject(obj: any): any {
+    if (typeof obj === 'string') {
+      return DOMPurify.sanitize(obj);
+    }
+    
+    if (Array.isArray(obj)) {
+      return obj.map(item => this.sanitizeObject(item));
+    }
+    
+    if (obj && typeof obj === 'object') {
+      const sanitized = {};
+      for (const [key, value] of Object.entries(obj)) {
+        sanitized[key] = this.sanitizeObject(value);
+      }
+      return sanitized;
+    }
+    
+    return obj;
+  }
+}
+
+// CSRF防护
+@Injectable()
+export class CsrfProtectionMiddleware implements NestMiddleware {
+  use(req: Request, res: Response, next: NextFunction) {
+    // 验证CSRF Token
+    const token = req.headers['x-csrf-token'] || req.body._token;
+    const sessionToken = req.session?.csrfToken;
+    
+    if (req.method !== 'GET' && token !== sessionToken) {
+      throw new ForbiddenException('Invalid CSRF token');
+    }
+    
+    next();
+  }
+}
+```
+
+**API访问控制:**
+
+```typescript
+// API密钥验证
+@Injectable()
+export class ApiKeyGuard implements CanActivate {
+  constructor(private readonly apiKeyService: ApiKeyService) {}
+  
+  async canActivate(context: ExecutionContext): Promise<boolean> {
+    const request = context.switchToHttp().getRequest();
+    const apiKey = request.headers['x-api-key'];
+    
+    if (!apiKey) {
+      throw new UnauthorizedException('API key required');
+    }
+    
+    const isValid = await this.apiKeyService.validateKey(apiKey);
+    if (!isValid) {
+      throw new UnauthorizedException('Invalid API key');
+    }
+    
+    // 记录API使用情况
+    await this.apiKeyService.recordUsage(apiKey, request.path);
+    
+    return true;
+  }
+}
+
+// IP白名单
+@Injectable()
+export class IpWhitelistGuard implements CanActivate {
+  private readonly allowedIps = new Set([
+    '192.168.1.0/24',
+    '10.0.0.0/8',
+    '172.16.0.0/12'
+  ]);
+  
+  canActivate(context: ExecutionContext): boolean {
+    const request = context.switchToHttp().getRequest();
+    const clientIp = request.ip || request.connection.remoteAddress;
+    
+    if (!this.isIpAllowed(clientIp)) {
+      throw new ForbiddenException('IP address not allowed');
+    }
+    
+    return true;
+  }
+  
+  private isIpAllowed(ip: string): boolean {
+    return Array.from(this.allowedIps).some(range => {
+      return this.ipInRange(ip, range);
+    });
+  }
+}
+```
+
+### 6.3 合规性考虑
+
+**数据隐私保护:**
+
+```typescript
+// 个人信息保护法合规
+@Injectable()
+export class PrivacyComplianceService {
+  // 数据最小化原则
+  async collectUserData(userData: Partial<User>): Promise<User> {
+    // 只收集必要的数据字段
+    const allowedFields = ['email', 'name', 'phone'];
+    const filteredData = Object.keys(userData)
+      .filter(key => allowedFields.includes(key))
+      .reduce((obj, key) => {
+        obj[key] = userData[key];
+        return obj;
+      }, {});
+    
+    return this.userService.create(filteredData);
+  }
+  
+  // 用户同意管理
+  async recordConsent(userId: string, consentType: string): Promise<void> {
+    await this.consentService.record({
+      userId,
+      consentType,
+      timestamp: new Date(),
+      ipAddress: this.request.ip,
+      userAgent: this.request.headers['user-agent']
+    });
+  }
+  
+  // 数据删除权(被遗忘权)
+  async deleteUserData(userId: string): Promise<void> {
+    // 1. 删除个人身份信息
+    await this.userService.anonymize(userId);
+    
+    // 2. 删除关联数据
+    await this.resumeService.deleteByUser(userId);
+    await this.interviewService.anonymizeByUser(userId);
+    
+    // 3. 记录删除操作
+    await this.auditService.logDataDeletion(userId);
+  }
+  
+  // 数据导出权
+  async exportUserData(userId: string): Promise<any> {
+    const userData = await this.userService.findById(userId);
+    const resumes = await this.resumeService.findByUser(userId);
+    const interviews = await this.interviewService.findByUser(userId);
+    
+    return {
+      personal_info: userData,
+      resumes: resumes,
+      interviews: interviews,
+      exported_at: new Date().toISOString()
+    };
+  }
+}
+```
+
+**审计日志:**
+
+```typescript
+@Injectable()
+export class AuditService {
+  async logUserAction(action: AuditAction): Promise<void> {
+    await this.auditRepository.save({
+      userId: action.userId,
+      action: action.type,
+      resource: action.resource,
+      details: action.details,
+      ipAddress: action.ipAddress,
+      userAgent: action.userAgent,
+      timestamp: new Date()
+    });
+  }
+  
+  // 敏感操作审计
+  @AuditLog('DATA_ACCESS')
+  async accessSensitiveData(userId: string, dataType: string): Promise<any> {
+    // 记录敏感数据访问
+    return this.dataService.getSensitiveData(userId, dataType);
+  }
+}
+
+// 审计装饰器
+export function AuditLog(actionType: string) {
+  return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
+    const method = descriptor.value;
+    
+    descriptor.value = async function (...args: any[]) {
+      const result = await method.apply(this, args);
+      
+      // 记录审计日志
+      await this.auditService.logUserAction({
+        type: actionType,
+        resource: `${target.constructor.name}.${propertyName}`,
+        details: { args: args.slice(0, 2) }, // 只记录前两个参数
+        userId: this.getCurrentUserId(),
+        ipAddress: this.getClientIp(),
+        userAgent: this.getUserAgent()
+      });
+      
+      return result;
+    };
+  };
+}
+```
+
+## 7. 技术栈总结
+
+### 7.1 技术组合一览表
+
+| 层级 | 技术选择 | 版本 | 用途 | 备选方案 |
+|------|----------|------|------|----------|
+| **前端框架** | Vue.js | 3.3+ | 用户界面开发 | React 18, Angular 16 |
+| **前端语言** | TypeScript | 5.0+ | 类型安全开发 | JavaScript ES2022 |
+| **前端构建** | Vite | 4.0+ | 快速构建打包 | Webpack 5, Rollup |
+| **状态管理** | Pinia | 2.1+ | 应用状态管理 | Vuex 4, Zustand |
+| **UI组件库** | Element Plus | 2.3+ | 企业级组件 | Ant Design Vue, Vuetify |
+| **CSS框架** | Tailwind CSS | 3.3+ | 原子化样式 | Bootstrap 5, Bulma |
+| **后端框架** | NestJS | 10+ | 企业级Node.js框架 | Express.js, Fastify |
+| **后端语言** | Node.js | 18+ | 服务端JavaScript | Java 17, Python 3.11, Go 1.20 |
+| **ORM框架** | TypeORM | 0.3+ | 数据库对象映射 | Prisma, Sequelize |
+| **主数据库** | PostgreSQL | 15+ | 关系型数据存储 | MySQL 8.0, MariaDB 10.6 |
+| **文档数据库** | MongoDB | 6.0+ | 非结构化数据 | CouchDB, Amazon DocumentDB |
+| **搜索引擎** | Elasticsearch | 8.0+ | 全文搜索分析 | Apache Solr, Algolia |
+| **缓存数据库** | Redis | 7.0+ | 高性能缓存 | Memcached, Hazelcast |
+| **消息队列** | Apache Kafka | 3.4+ | 分布式消息流 | RabbitMQ, Apache Pulsar |
+| **任务队列** | Bull | 4.10+ | 异步任务处理 | Agenda, Kue |
+| **容器化** | Docker | 20.10+ | 应用容器化 | Podman, containerd |
+| **容器编排** | Kubernetes | 1.27+ | 容器集群管理 | Docker Swarm, Nomad |
+| **服务网格** | Istio | 1.18+ | 微服务通信 | Linkerd, Consul Connect |
+| **API网关** | Kong | 3.3+ | API统一入口 | Nginx Gateway, Zuul |
+| **监控系统** | Prometheus | 2.45+ | 指标监控收集 | InfluxDB, Datadog |
+| **可视化** | Grafana | 10.0+ | 监控数据可视化 | Kibana, New Relic |
+| **日志管理** | ELK Stack | 8.8+ | 日志收集分析 | Fluentd + InfluxDB |
+| **CI/CD** | GitLab CI | 16.0+ | 持续集成部署 | Jenkins, GitHub Actions |
+| **云服务商** | 阿里云 | - | 云基础设施 | AWS, Azure, 腾讯云 |
+| **CDN** | 阿里云CDN | - | 内容分发网络 | CloudFlare, AWS CloudFront |
+| **对象存储** | 阿里云OSS | - | 文件对象存储 | AWS S3, 腾讯云COS |
+
+### 7.2 优缺点与备选方案
+
+**核心技术选择分析:**
+
+#### Vue.js vs React vs Angular
+
+**Vue.js (推荐)**
+- ✅ 学习曲线平缓,团队快速上手
+- ✅ 中文文档完善,社区活跃
+- ✅ 性能优秀,包体积小
+- ✅ TypeScript支持良好
+- ❌ 企业级生态相对较小
+- ❌ 大型项目架构指导较少
+
+**React (备选)**
+- ✅ 生态最丰富,社区最大
+- ✅ 企业采用率最高
+- ✅ 性能优化方案成熟
+- ❌ 学习曲线较陡峭
+- ❌ 配置复杂度较高
+
+**Angular (备选)**
+- ✅ 企业级框架,功能完整
+- ✅ TypeScript原生支持
+- ✅ 架构规范性强
+- ❌ 学习成本最高
+- ❌ 包体积较大
+
+#### Node.js vs Java vs Python vs Go
+
+**Node.js (推荐)**
+- ✅ 前后端技术栈统一
+- ✅ 开发效率高
+- ✅ 实时应用支持优秀
+- ✅ 微服务架构友好
+- ❌ CPU密集型任务性能较差
+- ❌ 单线程模型限制
+
+**Java Spring Boot (备选)**
+- ✅ 企业级应用首选
+- ✅ 生态最成熟
+- ✅ 性能稳定可靠
+- ✅ 人才储备充足
+- ❌ 开发效率相对较低
+- ❌ 内存占用较大
+
+**Python FastAPI (备选)**
+- ✅ AI集成最友好
+- ✅ 开发效率极高
+- ✅ 数据处理能力强
+- ❌ 性能相对较低
+- ❌ 并发处理能力有限
+
+**Go (备选)**
+- ✅ 性能最优秀
+- ✅ 并发处理能力强
+- ✅ 部署简单
+- ❌ 生态相对较小
+- ❌ 学习成本较高
+
+#### PostgreSQL vs MySQL vs MongoDB
+
+**PostgreSQL (推荐)**
+- ✅ 功能最丰富
+- ✅ JSON支持优秀
+- ✅ 扩展性强
+- ✅ 数据一致性保证
+- ❌ 学习成本较高
+- ❌ 运维复杂度较大
+
+**MySQL (备选)**
+- ✅ 使用最广泛
+- ✅ 运维成本低
+- ✅ 性能优秀
+- ❌ 功能相对简单
+- ❌ JSON支持较弱
+
+**MongoDB (混合使用)**
+- ✅ 灵活的文档模型
+- ✅ 水平扩展能力强
+- ✅ 开发效率高
+- ❌ 数据一致性较弱
+- ❌ 复杂查询能力有限
+
+**最终推荐架构组合:**
+
+```
+前端:Vue.js 3 + TypeScript + Vite + Pinia + Element Plus
+后端:Node.js + NestJS + TypeScript + TypeORM
+数据:PostgreSQL + MongoDB + Redis + Elasticsearch
+基础设施:Docker + Kubernetes + 阿里云
+监控:Prometheus + Grafana + ELK
+```
+
+**适用场景:**
+- ✅ 中小型技术团队(10-50人)
+- ✅ 快速迭代开发需求
+- ✅ 实时交互应用
+- ✅ AI功能集成
+- ✅ 云原生部署
+
+**不适用场景:**
+- ❌ 超大规模企业级应用(建议Java)
+- ❌ CPU密集型计算(建议Go/C++)
+- ❌ 强一致性金融应用(建议Java)
+- ❌ 传统企业环境(建议.NET)
+
+---
+
+## 总结
+
+本架构设计为AI智能面试平台提供了一套完整、可扩展、安全的技术解决方案。通过微服务架构、云原生部署、多数据库混合使用等现代化技术栈,能够支撑百万级用户的业务需求,同时保证系统的高可用性、高性能和高安全性。
+
+**关键优势:**
+1. **技术先进性**:采用最新的云原生技术栈
+2. **可扩展性**:支持水平扩展和业务增长
+3. **开发效率**:统一技术栈,提高开发效率
+4. **运维友好**:容器化部署,自动化运维
+5. **安全合规**:全面的安全防护和合规设计
+
+**实施建议:**
+1. **分阶段实施**:先实现核心功能,再逐步完善
+2. **技术选型验证**:通过POC验证关键技术选择
+3. **团队培训**:确保团队掌握相关技术栈
+4. **监控先行**:优先建立监控和日志体系
+5. **安全优先**:从设计阶段就考虑安全因素
+
+该架构设计为AI智能面试平台的成功实施提供了坚实的技术基础。

+ 5 - 0
README.md

@@ -0,0 +1,5 @@
+# Vue 3 + TypeScript + Vite
+
+This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
+
+Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

+ 13 - 0
index.html

@@ -0,0 +1,13 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Digital Human AI Interview Application</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 3218 - 0
package-lock.json

@@ -0,0 +1,3218 @@
+{
+  "name": "digital-human-interview",
+  "version": "0.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "digital-human-interview",
+      "version": "0.0.0",
+      "dependencies": {
+        "@types/crypto-js": "^4.2.2",
+        "crypto-js": "^4.2.0",
+        "lottie-web": "^5.12.2",
+        "socket.io-client": "^4.7.2",
+        "video.js": "^8.6.1",
+        "vue": "^3.4.38",
+        "vue-router": "^4.2.5"
+      },
+      "devDependencies": {
+        "@vitejs/plugin-vue": "^5.1.3",
+        "autoprefixer": "^10.4.16",
+        "postcss": "^8.4.31",
+        "tailwindcss": "^3.3.5",
+        "typescript": "^5.5.3",
+        "vite": "^5.4.2",
+        "vue-tsc": "^2.1.4"
+      }
+    },
+    "node_modules/@alloc/quick-lru": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+      "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
+      "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.27.7",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.7.tgz",
+      "integrity": "sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.27.7"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/runtime": {
+      "version": "7.27.6",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
+      "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.27.7",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.7.tgz",
+      "integrity": "sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.27.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+      "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+      "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+      "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+      "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+      "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+      "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+      "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+      "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+      "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+      "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+      "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+      "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+      "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+      "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+      "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+      "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+      "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+      "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+      "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@isaacs/cliui": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+      "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^5.1.2",
+        "string-width-cjs": "npm:string-width@^4.2.0",
+        "strip-ansi": "^7.0.1",
+        "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+        "wrap-ansi": "^8.1.0",
+        "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.8",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
+      "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/set-array": "^1.2.1",
+        "@jridgewell/sourcemap-codec": "^1.4.10",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/set-array": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+      "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+      "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.25",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+      "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@pkgjs/parseargs": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+      "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.44.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.1.tgz",
+      "integrity": "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.44.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.1.tgz",
+      "integrity": "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.44.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.1.tgz",
+      "integrity": "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.44.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.1.tgz",
+      "integrity": "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.44.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.1.tgz",
+      "integrity": "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.44.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.1.tgz",
+      "integrity": "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.44.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.1.tgz",
+      "integrity": "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.44.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.1.tgz",
+      "integrity": "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.44.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.1.tgz",
+      "integrity": "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.44.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.1.tgz",
+      "integrity": "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+      "version": "4.44.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.1.tgz",
+      "integrity": "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+      "version": "4.44.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.1.tgz",
+      "integrity": "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.44.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.1.tgz",
+      "integrity": "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.44.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.1.tgz",
+      "integrity": "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.44.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.1.tgz",
+      "integrity": "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.44.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.1.tgz",
+      "integrity": "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.44.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.1.tgz",
+      "integrity": "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.44.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.1.tgz",
+      "integrity": "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.44.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.1.tgz",
+      "integrity": "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.44.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.1.tgz",
+      "integrity": "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@socket.io/component-emitter": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+      "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
+      "license": "MIT"
+    },
+    "node_modules/@types/crypto-js": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmmirror.com/@types/crypto-js/-/crypto-js-4.2.2.tgz",
+      "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==",
+      "license": "MIT"
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@videojs/http-streaming": {
+      "version": "3.17.0",
+      "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.17.0.tgz",
+      "integrity": "sha512-Ch1P3tvvIEezeZXyK11UfWgp4cWKX4vIhZ30baN/lRinqdbakZ5hiAI3pGjRy3d+q/Epyc8Csz5xMdKNNGYpcw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "@videojs/vhs-utils": "^4.1.1",
+        "aes-decrypter": "^4.0.2",
+        "global": "^4.4.0",
+        "m3u8-parser": "^7.2.0",
+        "mpd-parser": "^1.3.1",
+        "mux.js": "7.1.0",
+        "video.js": "^7 || ^8"
+      },
+      "engines": {
+        "node": ">=8",
+        "npm": ">=5"
+      },
+      "peerDependencies": {
+        "video.js": "^8.19.0"
+      }
+    },
+    "node_modules/@videojs/vhs-utils": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.1.1.tgz",
+      "integrity": "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "global": "^4.4.0"
+      },
+      "engines": {
+        "node": ">=8",
+        "npm": ">=5"
+      }
+    },
+    "node_modules/@videojs/xhr": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.7.0.tgz",
+      "integrity": "sha512-giab+EVRanChIupZK7gXjHy90y3nncA2phIOyG3Ne5fvpiMJzvqYwiTOnEVW2S4CoYcuKJkomat7bMXA/UoUZQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.5.5",
+        "global": "~4.4.0",
+        "is-function": "^1.0.1"
+      }
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "5.2.4",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
+      "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "peerDependencies": {
+        "vite": "^5.0.0 || ^6.0.0",
+        "vue": "^3.2.25"
+      }
+    },
+    "node_modules/@volar/language-core": {
+      "version": "2.4.15",
+      "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz",
+      "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/source-map": "2.4.15"
+      }
+    },
+    "node_modules/@volar/source-map": {
+      "version": "2.4.15",
+      "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz",
+      "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@volar/typescript": {
+      "version": "2.4.15",
+      "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz",
+      "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "2.4.15",
+        "path-browserify": "^1.0.1",
+        "vscode-uri": "^3.0.8"
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.17.tgz",
+      "integrity": "sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.27.5",
+        "@vue/shared": "3.5.17",
+        "entities": "^4.5.0",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.17.tgz",
+      "integrity": "sha512-+2UgfLKoaNLhgfhV5Ihnk6wB4ljyW1/7wUIog2puUqajiC29Lp5R/IKDdkebh9jTbTogTbsgB+OY9cEWzG95JQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.17",
+        "@vue/shared": "3.5.17"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.17.tgz",
+      "integrity": "sha512-rQQxbRJMgTqwRugtjw0cnyQv9cP4/4BxWfTdRBkqsTfLOHWykLzbOc3C4GGzAmdMDxhzU/1Ija5bTjMVrddqww==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.27.5",
+        "@vue/compiler-core": "3.5.17",
+        "@vue/compiler-dom": "3.5.17",
+        "@vue/compiler-ssr": "3.5.17",
+        "@vue/shared": "3.5.17",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.17",
+        "postcss": "^8.5.6",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.17.tgz",
+      "integrity": "sha512-hkDbA0Q20ZzGgpj5uZjb9rBzQtIHLS78mMilwrlpWk2Ep37DYntUz0PonQ6kr113vfOEdM+zTBuJDaceNIW0tQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.17",
+        "@vue/shared": "3.5.17"
+      }
+    },
+    "node_modules/@vue/compiler-vue2": {
+      "version": "2.7.16",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
+      "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "de-indent": "^1.0.2",
+        "he": "^1.2.0"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "6.6.4",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+      "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+      "license": "MIT"
+    },
+    "node_modules/@vue/language-core": {
+      "version": "2.2.10",
+      "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.10.tgz",
+      "integrity": "sha512-+yNoYx6XIKuAO8Mqh1vGytu8jkFEOH5C8iOv3i8Z/65A7x9iAOXA97Q+PqZ3nlm2lxf5rOJuIGI/wDtx/riNYw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "~2.4.11",
+        "@vue/compiler-dom": "^3.5.0",
+        "@vue/compiler-vue2": "^2.7.16",
+        "@vue/shared": "^3.5.0",
+        "alien-signals": "^1.0.3",
+        "minimatch": "^9.0.3",
+        "muggle-string": "^0.4.1",
+        "path-browserify": "^1.0.1"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.17.tgz",
+      "integrity": "sha512-l/rmw2STIscWi7SNJp708FK4Kofs97zc/5aEPQh4bOsReD/8ICuBcEmS7KGwDj5ODQLYWVN2lNibKJL1z5b+Lw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/shared": "3.5.17"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.17.tgz",
+      "integrity": "sha512-QQLXa20dHg1R0ri4bjKeGFKEkJA7MMBxrKo2G+gJikmumRS7PTD4BOU9FKrDQWMKowz7frJJGqBffYMgQYS96Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.17",
+        "@vue/shared": "3.5.17"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.17.tgz",
+      "integrity": "sha512-8El0M60TcwZ1QMz4/os2MdlQECgGoVHPuLnQBU3m9h3gdNRW9xRmI8iLS4t/22OQlOE6aJvNNlBiCzPHur4H9g==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.17",
+        "@vue/runtime-core": "3.5.17",
+        "@vue/shared": "3.5.17",
+        "csstype": "^3.1.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.17.tgz",
+      "integrity": "sha512-BOHhm8HalujY6lmC3DbqF6uXN/K00uWiEeF22LfEsm9Q93XeJ/plHTepGwf6tqFcF7GA5oGSSAAUock3VvzaCA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.17",
+        "@vue/shared": "3.5.17"
+      },
+      "peerDependencies": {
+        "vue": "3.5.17"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.17.tgz",
+      "integrity": "sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg==",
+      "license": "MIT"
+    },
+    "node_modules/@xmldom/xmldom": {
+      "version": "0.8.10",
+      "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
+      "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/aes-decrypter": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-4.0.2.tgz",
+      "integrity": "sha512-lc+/9s6iJvuaRe5qDlMTpCFjnwpkeOXp8qP3oiZ5jsj1MRg+SBVUmmICrhxHvc8OELSmc+fEyyxAuppY6hrWzw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "@videojs/vhs-utils": "^4.1.1",
+        "global": "^4.4.0",
+        "pkcs7": "^1.0.4"
+      }
+    },
+    "node_modules/alien-signals": {
+      "version": "1.0.13",
+      "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
+      "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/ansi-regex": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+      "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+      "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/any-promise": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+      "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/anymatch": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/arg": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+      "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/autoprefixer": {
+      "version": "10.4.21",
+      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
+      "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "browserslist": "^4.24.4",
+        "caniuse-lite": "^1.0.30001702",
+        "fraction.js": "^4.3.7",
+        "normalize-range": "^0.1.2",
+        "picocolors": "^1.1.1",
+        "postcss-value-parser": "^4.2.0"
+      },
+      "bin": {
+        "autoprefixer": "bin/autoprefixer"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/binary-extensions": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+      "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/brace-expansion": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+      "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fill-range": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/browserslist": {
+      "version": "4.25.1",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz",
+      "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "caniuse-lite": "^1.0.30001726",
+        "electron-to-chromium": "^1.5.173",
+        "node-releases": "^2.0.19",
+        "update-browserslist-db": "^1.1.3"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/camelcase-css": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+      "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001726",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz",
+      "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "CC-BY-4.0"
+    },
+    "node_modules/chokidar": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+      "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      },
+      "engines": {
+        "node": ">= 8.10.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/chokidar/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/commander": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+      "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/crypto-js": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz",
+      "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
+      "license": "MIT"
+    },
+    "node_modules/cssesc": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "cssesc": "bin/cssesc"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+      "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+      "license": "MIT"
+    },
+    "node_modules/de-indent": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
+      "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/debug": {
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/didyoumean": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+      "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/dlv": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+      "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/dom-walk": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
+      "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
+    },
+    "node_modules/eastasianwidth": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.177",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.177.tgz",
+      "integrity": "sha512-7EH2G59nLsEMj97fpDuvVcYi6lwTcM1xuWw3PssD8xzboAW7zj7iB3COEEEATUfjLHrs5uKBLQT03V/8URx06g==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/engine.io-client": {
+      "version": "6.6.3",
+      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
+      "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
+      "license": "MIT",
+      "dependencies": {
+        "@socket.io/component-emitter": "~3.1.0",
+        "debug": "~4.3.1",
+        "engine.io-parser": "~5.2.1",
+        "ws": "~8.17.1",
+        "xmlhttprequest-ssl": "~2.1.1"
+      }
+    },
+    "node_modules/engine.io-parser": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+      "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/entities": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+      "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+      "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.21.5",
+        "@esbuild/android-arm": "0.21.5",
+        "@esbuild/android-arm64": "0.21.5",
+        "@esbuild/android-x64": "0.21.5",
+        "@esbuild/darwin-arm64": "0.21.5",
+        "@esbuild/darwin-x64": "0.21.5",
+        "@esbuild/freebsd-arm64": "0.21.5",
+        "@esbuild/freebsd-x64": "0.21.5",
+        "@esbuild/linux-arm": "0.21.5",
+        "@esbuild/linux-arm64": "0.21.5",
+        "@esbuild/linux-ia32": "0.21.5",
+        "@esbuild/linux-loong64": "0.21.5",
+        "@esbuild/linux-mips64el": "0.21.5",
+        "@esbuild/linux-ppc64": "0.21.5",
+        "@esbuild/linux-riscv64": "0.21.5",
+        "@esbuild/linux-s390x": "0.21.5",
+        "@esbuild/linux-x64": "0.21.5",
+        "@esbuild/netbsd-x64": "0.21.5",
+        "@esbuild/openbsd-x64": "0.21.5",
+        "@esbuild/sunos-x64": "0.21.5",
+        "@esbuild/win32-arm64": "0.21.5",
+        "@esbuild/win32-ia32": "0.21.5",
+        "@esbuild/win32-x64": "0.21.5"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "license": "MIT"
+    },
+    "node_modules/fast-glob": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+      "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.8"
+      },
+      "engines": {
+        "node": ">=8.6.0"
+      }
+    },
+    "node_modules/fast-glob/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fastq": {
+      "version": "1.19.1",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
+      "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/foreground-child": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+      "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "cross-spawn": "^7.0.6",
+        "signal-exit": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/fraction.js": {
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
+      "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "type": "patreon",
+        "url": "https://github.com/sponsors/rawify"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/glob": {
+      "version": "10.4.5",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+      "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^3.1.2",
+        "minimatch": "^9.0.4",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^1.11.1"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/glob-parent": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/global": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
+      "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
+      "license": "MIT",
+      "dependencies": {
+        "min-document": "^2.19.0",
+        "process": "^0.11.10"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/he": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "he": "bin/he"
+      }
+    },
+    "node_modules/is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-core-module": {
+      "version": "2.16.1",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+      "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-function": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz",
+      "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==",
+      "license": "MIT"
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/jackspeak": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+      "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      },
+      "optionalDependencies": {
+        "@pkgjs/parseargs": "^0.11.0"
+      }
+    },
+    "node_modules/jiti": {
+      "version": "1.21.7",
+      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+      "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "jiti": "bin/jiti.js"
+      }
+    },
+    "node_modules/lilconfig": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+      "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antonk52"
+      }
+    },
+    "node_modules/lines-and-columns": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lottie-web": {
+      "version": "5.13.0",
+      "resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.13.0.tgz",
+      "integrity": "sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==",
+      "license": "MIT"
+    },
+    "node_modules/lru-cache": {
+      "version": "10.4.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/m3u8-parser": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-7.2.0.tgz",
+      "integrity": "sha512-CRatFqpjVtMiMaKXxNvuI3I++vUumIXVVT/JpCpdU/FynV/ceVw1qpPyyBNindL+JlPMSesx+WX1QJaZEJSaMQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "@videojs/vhs-utils": "^4.1.1",
+        "global": "^4.4.0"
+      }
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.17",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+      "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0"
+      }
+    },
+    "node_modules/merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/min-document": {
+      "version": "2.19.0",
+      "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",
+      "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==",
+      "dependencies": {
+        "dom-walk": "^0.1.0"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/minipass": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+      "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/mpd-parser": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.3.1.tgz",
+      "integrity": "sha512-1FuyEWI5k2HcmhS1HkKnUAQV7yFPfXPht2DnRRGtoiiAAW+ESTbtEXIDpRkwdU+XyrQuwrIym7UkoPKsZ0SyFw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "@videojs/vhs-utils": "^4.0.0",
+        "@xmldom/xmldom": "^0.8.3",
+        "global": "^4.4.0"
+      },
+      "bin": {
+        "mpd-to-m3u8-json": "bin/parse.js"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/muggle-string": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
+      "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/mux.js": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.1.0.tgz",
+      "integrity": "sha512-NTxawK/BBELJrYsZThEulyUMDVlLizKdxyAsMuzoCD1eFj97BVaA8D/CvKsKu6FOLYkFojN5CbM9h++ZTZtknA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@babel/runtime": "^7.11.2",
+        "global": "^4.4.0"
+      },
+      "bin": {
+        "muxjs-transmux": "bin/transmux.js"
+      },
+      "engines": {
+        "node": ">=8",
+        "npm": ">=5"
+      }
+    },
+    "node_modules/mz": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+      "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "any-promise": "^1.0.0",
+        "object-assign": "^4.0.1",
+        "thenify-all": "^1.0.0"
+      }
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.19",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
+      "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/normalize-range": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+      "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-hash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+      "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/package-json-from-dist": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+      "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+      "dev": true,
+      "license": "BlueOak-1.0.0"
+    },
+    "node_modules/path-browserify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+      "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/path-scurry": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+      "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^10.2.0",
+        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pify": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+      "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/pirates": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+      "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/pkcs7": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz",
+      "integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@babel/runtime": "^7.5.5"
+      },
+      "bin": {
+        "pkcs7": "bin/cli.js"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.6",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+      "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/postcss-import": {
+      "version": "15.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+      "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "postcss-value-parser": "^4.0.0",
+        "read-cache": "^1.0.0",
+        "resolve": "^1.1.7"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.0.0"
+      }
+    },
+    "node_modules/postcss-js": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
+      "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "camelcase-css": "^2.0.1"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >= 16"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/postcss/"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4.21"
+      }
+    },
+    "node_modules/postcss-load-config": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
+      "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "lilconfig": "^3.0.0",
+        "yaml": "^2.3.4"
+      },
+      "engines": {
+        "node": ">= 14"
+      },
+      "peerDependencies": {
+        "postcss": ">=8.0.9",
+        "ts-node": ">=9.0.0"
+      },
+      "peerDependenciesMeta": {
+        "postcss": {
+          "optional": true
+        },
+        "ts-node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/postcss-nested": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+      "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "postcss-selector-parser": "^6.1.1"
+      },
+      "engines": {
+        "node": ">=12.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.2.14"
+      }
+    },
+    "node_modules/postcss-selector-parser": {
+      "version": "6.1.2",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+      "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cssesc": "^3.0.0",
+        "util-deprecate": "^1.0.2"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/postcss-value-parser": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+      "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/process": {
+      "version": "0.11.10",
+      "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+      "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6.0"
+      }
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/read-cache": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+      "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "pify": "^2.3.0"
+      }
+    },
+    "node_modules/readdirp": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "picomatch": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=8.10.0"
+      }
+    },
+    "node_modules/resolve": {
+      "version": "1.22.10",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
+      "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-core-module": "^2.16.0",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/reusify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+      "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/rollup": {
+      "version": "4.44.1",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.1.tgz",
+      "integrity": "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.8"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.44.1",
+        "@rollup/rollup-android-arm64": "4.44.1",
+        "@rollup/rollup-darwin-arm64": "4.44.1",
+        "@rollup/rollup-darwin-x64": "4.44.1",
+        "@rollup/rollup-freebsd-arm64": "4.44.1",
+        "@rollup/rollup-freebsd-x64": "4.44.1",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.44.1",
+        "@rollup/rollup-linux-arm-musleabihf": "4.44.1",
+        "@rollup/rollup-linux-arm64-gnu": "4.44.1",
+        "@rollup/rollup-linux-arm64-musl": "4.44.1",
+        "@rollup/rollup-linux-loongarch64-gnu": "4.44.1",
+        "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1",
+        "@rollup/rollup-linux-riscv64-gnu": "4.44.1",
+        "@rollup/rollup-linux-riscv64-musl": "4.44.1",
+        "@rollup/rollup-linux-s390x-gnu": "4.44.1",
+        "@rollup/rollup-linux-x64-gnu": "4.44.1",
+        "@rollup/rollup-linux-x64-musl": "4.44.1",
+        "@rollup/rollup-win32-arm64-msvc": "4.44.1",
+        "@rollup/rollup-win32-ia32-msvc": "4.44.1",
+        "@rollup/rollup-win32-x64-msvc": "4.44.1",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/socket.io-client": {
+      "version": "4.8.1",
+      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
+      "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@socket.io/component-emitter": "~3.1.0",
+        "debug": "~4.3.2",
+        "engine.io-client": "~6.6.1",
+        "socket.io-parser": "~4.2.4"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/socket.io-parser": {
+      "version": "4.2.4",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
+      "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
+      "license": "MIT",
+      "dependencies": {
+        "@socket.io/component-emitter": "~3.1.0",
+        "debug": "~4.3.1"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+      "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/string-width-cjs": {
+      "name": "string-width",
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/string-width-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+      "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/strip-ansi-cjs": {
+      "name": "strip-ansi",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/sucrase": {
+      "version": "3.35.0",
+      "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
+      "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.2",
+        "commander": "^4.0.0",
+        "glob": "^10.3.10",
+        "lines-and-columns": "^1.1.6",
+        "mz": "^2.7.0",
+        "pirates": "^4.0.1",
+        "ts-interface-checker": "^0.1.9"
+      },
+      "bin": {
+        "sucrase": "bin/sucrase",
+        "sucrase-node": "bin/sucrase-node"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/tailwindcss": {
+      "version": "3.4.17",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
+      "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@alloc/quick-lru": "^5.2.0",
+        "arg": "^5.0.2",
+        "chokidar": "^3.6.0",
+        "didyoumean": "^1.2.2",
+        "dlv": "^1.1.3",
+        "fast-glob": "^3.3.2",
+        "glob-parent": "^6.0.2",
+        "is-glob": "^4.0.3",
+        "jiti": "^1.21.6",
+        "lilconfig": "^3.1.3",
+        "micromatch": "^4.0.8",
+        "normalize-path": "^3.0.0",
+        "object-hash": "^3.0.0",
+        "picocolors": "^1.1.1",
+        "postcss": "^8.4.47",
+        "postcss-import": "^15.1.0",
+        "postcss-js": "^4.0.1",
+        "postcss-load-config": "^4.0.2",
+        "postcss-nested": "^6.2.0",
+        "postcss-selector-parser": "^6.1.2",
+        "resolve": "^1.22.8",
+        "sucrase": "^3.35.0"
+      },
+      "bin": {
+        "tailwind": "lib/cli.js",
+        "tailwindcss": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/thenify": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+      "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "any-promise": "^1.0.0"
+      }
+    },
+    "node_modules/thenify-all": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+      "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "thenify": ">= 3.1.0 < 4"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/ts-interface-checker": {
+      "version": "0.1.13",
+      "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+      "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/typescript": {
+      "version": "5.8.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
+      "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+      "devOptional": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/update-browserslist-db": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
+      "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.1"
+      },
+      "bin": {
+        "update-browserslist-db": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/video.js": {
+      "version": "8.23.3",
+      "resolved": "https://registry.npmjs.org/video.js/-/video.js-8.23.3.tgz",
+      "integrity": "sha512-Toe0VLlDZcUhiaWfcePS1OEdT3ATfktm0hk/PELfD7zUoPDHeT+cJf/wZmCy5M5eGVwtGUg25RWPCj1L/1XufA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "@videojs/http-streaming": "^3.17.0",
+        "@videojs/vhs-utils": "^4.1.1",
+        "@videojs/xhr": "2.7.0",
+        "aes-decrypter": "^4.0.2",
+        "global": "4.4.0",
+        "m3u8-parser": "^7.2.0",
+        "mpd-parser": "^1.3.1",
+        "mux.js": "^7.0.1",
+        "videojs-contrib-quality-levels": "4.1.0",
+        "videojs-font": "4.2.0",
+        "videojs-vtt.js": "0.15.5"
+      }
+    },
+    "node_modules/videojs-contrib-quality-levels": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-4.1.0.tgz",
+      "integrity": "sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "global": "^4.4.0"
+      },
+      "engines": {
+        "node": ">=16",
+        "npm": ">=8"
+      },
+      "peerDependencies": {
+        "video.js": "^8"
+      }
+    },
+    "node_modules/videojs-font": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-4.2.0.tgz",
+      "integrity": "sha512-YPq+wiKoGy2/M7ccjmlvwi58z2xsykkkfNMyIg4xb7EZQQNwB71hcSsB3o75CqQV7/y5lXkXhI/rsGAS7jfEmQ==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/videojs-vtt.js": {
+      "version": "0.15.5",
+      "resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz",
+      "integrity": "sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "global": "^4.3.1"
+      }
+    },
+    "node_modules/vite": {
+      "version": "5.4.19",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz",
+      "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.21.3",
+        "postcss": "^8.4.43",
+        "rollup": "^4.20.0"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^18.0.0 || >=20.0.0",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "sass-embedded": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.4.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vscode-uri": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
+      "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/vue": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.17.tgz",
+      "integrity": "sha512-LbHV3xPN9BeljML+Xctq4lbz2lVHCR6DtbpTf5XIO6gugpXUN49j2QQPcMj086r9+AkJ0FfUT8xjulKKBkkr9g==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.17",
+        "@vue/compiler-sfc": "3.5.17",
+        "@vue/runtime-dom": "3.5.17",
+        "@vue/server-renderer": "3.5.17",
+        "@vue/shared": "3.5.17"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-router": {
+      "version": "4.5.1",
+      "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
+      "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^6.6.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "vue": "^3.2.0"
+      }
+    },
+    "node_modules/vue-tsc": {
+      "version": "2.2.10",
+      "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.10.tgz",
+      "integrity": "sha512-jWZ1xSaNbabEV3whpIDMbjVSVawjAyW+x1n3JeGQo7S0uv2n9F/JMgWW90tGWNFRKya4YwKMZgCtr0vRAM7DeQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/typescript": "~2.4.11",
+        "@vue/language-core": "2.2.10"
+      },
+      "bin": {
+        "vue-tsc": "bin/vue-tsc.js"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.0.0"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/wrap-ansi": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+      "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.1.0",
+        "string-width": "^5.0.1",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs": {
+      "name": "wrap-ansi",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ws": {
+      "version": "8.17.1",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+      "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/xmlhttprequest-ssl": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
+      "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/yaml": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
+      "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "yaml": "bin.mjs"
+      },
+      "engines": {
+        "node": ">= 14.6"
+      }
+    }
+  }
+}

+ 29 - 0
package.json

@@ -0,0 +1,29 @@
+{
+  "name": "digital-human-interview",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vue-tsc -b && vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@types/crypto-js": "^4.2.2",
+    "crypto-js": "^4.2.0",
+    "lottie-web": "^5.12.2",
+    "socket.io-client": "^4.7.2",
+    "video.js": "^8.6.1",
+    "vue": "^3.4.38",
+    "vue-router": "^4.2.5"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^5.1.3",
+    "autoprefixer": "^10.4.16",
+    "postcss": "^8.4.31",
+    "tailwindcss": "^3.3.5",
+    "typescript": "^5.5.3",
+    "vite": "^5.4.2",
+    "vue-tsc": "^2.1.4"
+  }
+}

+ 6 - 0
postcss.config.js

@@ -0,0 +1,6 @@
+export default {
+  plugins: {
+    tailwindcss: {},
+    autoprefixer: {},
+  },
+}

+ 1 - 0
projectv1

@@ -0,0 +1 @@
+Subproject commit 37aa102a27c594de57fb49e4c42db56d63f7970b

+ 1 - 0
public/vite.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

+ 13 - 0
src/App.vue

@@ -0,0 +1,13 @@
+<template>
+  <div id="app" class="min-h-screen bg-gray-50">
+    <router-view />
+  </div>
+</template>
+
+<script setup lang="ts">
+// Root component - handles global routing
+</script>
+
+<style scoped>
+/* Global app styles are handled in style.css */
+</style>

+ 8 - 0
src/assets/logo.svg

@@ -0,0 +1,8 @@
+<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <rect width="100" height="100" rx="20" fill="#3B82F6"/>
+  <circle cx="50" cy="35" r="12" fill="white"/>
+  <rect x="35" y="55" width="30" height="35" rx="4" fill="white"/>
+  <circle cx="42" cy="28" r="2" fill="#3B82F6"/>
+  <circle cx="58" cy="28" r="2" fill="#3B82F6"/>
+  <ellipse cx="50" cy="42" rx="4" ry="2" fill="#3B82F6"/>
+</svg>

+ 1 - 0
src/assets/vue.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

+ 412 - 0
src/components/DigitalHumanPlayer.vue

@@ -0,0 +1,412 @@
+<template>
+  <div class="relative w-full max-w-md mx-auto">
+    <!-- Lottie动画容器 -->
+    <div
+      ref="lottieContainer"
+      class="relative aspect-[3/4] bg-gradient-to-br from-blue-100 to-purple-100 rounded-2xl overflow-hidden shadow-2xl"
+    >
+      <!-- 状态覆盖层 -->
+      <div class="absolute inset-0 flex items-center justify-center">
+        <!-- 倾听状态指示器 -->
+        <div v-if="status === 'LISTENING'" class="text-gray-700 text-center">
+          <div class="w-16 h-16 border-4 border-blue-400 border-opacity-50 rounded-full mb-4 mx-auto animate-pulse"></div>
+          <p class="text-sm font-medium">正在倾听...</p>
+        </div>
+        
+        <!-- 思考状态指示器 -->
+        <div v-if="status === 'THINKING'" class="text-gray-700 text-center">
+          <div class="w-16 h-16 border-4 border-amber-400 border-t-transparent rounded-full mb-4 mx-auto animate-spin"></div>
+          <p class="text-sm font-medium">思考中...</p>
+        </div>
+
+        <!-- 数字人头像 -->
+        <div v-if="status === 'ASKING' || status === 'GENERATING_LIP_SYNC'" class="text-center">
+          <div class="w-32 h-32 bg-blue-500 rounded-full mb-4 mx-auto flex items-center justify-center relative overflow-hidden">
+            <!-- 简单的嘴巴动画 -->
+            <div class="mouth-animation">
+              <div class="mouth" :class="{ 'talking': isTalking }"></div>
+            </div>
+            <!-- 眼睛 -->
+            <div class="absolute top-8 left-10 w-3 h-3 bg-white rounded-full"></div>
+            <div class="absolute top-8 right-10 w-3 h-3 bg-white rounded-full"></div>
+          </div>
+          <p v-if="status === 'ASKING'" class="text-sm font-medium text-gray-700">正在提问...</p>
+          <p v-if="status === 'GENERATING_LIP_SYNC'" class="text-sm font-medium text-gray-700">正在回复...</p>
+        </div>
+      </div>
+    </div>
+
+    <!-- 加载状态 -->
+    <div
+      v-if="isLoading"
+      class="absolute inset-0 flex items-center justify-center bg-gray-800 bg-opacity-50 rounded-2xl"
+    >
+      <div class="text-white text-center">
+        <div class="w-8 h-8 border-2 border-white border-t-transparent rounded-full animate-spin mb-4 mx-auto"></div>
+        <p class="text-sm">加载中...</p>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
+import lottie from 'lottie-web'
+
+// Props定义
+interface Props {
+  status: 'ASKING' | 'LISTENING' | 'THINKING' | 'GENERATING_LIP_SYNC'
+  lottieJson?: any
+  audioStream: string[] // 接收解码后的音频块数组
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  status: 'ASKING',
+  lottieJson: null,
+  audioStream: () => []
+})
+
+// 组件引用
+const lottieContainer = ref<HTMLDivElement>()
+
+// 状态
+const isLoading = ref(false)
+const isTalking = ref(false)
+
+// Lottie实例
+let lottieInstance: any = null
+let talkingInterval: NodeJS.Timeout | null = null
+
+// Web Audio API 相关
+let audioContext: AudioContext | null = null
+let audioQueue: ArrayBuffer[] = []
+let isPlaying = ref(false)
+let currentGainNode: GainNode | null = null
+
+// 初始化 Web Audio API
+const initAudioContext = async () => {
+  if (!audioContext) {
+    try {
+      audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
+      
+      // 创建主音量控制节点
+      currentGainNode = audioContext.createGain()
+      currentGainNode.connect(audioContext.destination)
+      currentGainNode.gain.value = 0.8 // 设置音量为80%
+      
+      console.log('🎵 Audio context initialized successfully')
+    } catch (error) {
+      console.error('Failed to initialize audio context:', error)
+    }
+  }
+}
+
+/**
+ * 监听音频流的变化
+ */
+watch(() => props.audioStream, (newStream) => {
+  if (newStream && newStream.length > 0) {
+    console.log('🎵 Received new audio stream chunks:', newStream.length)
+    processAudioStream(newStream)
+  }
+}, { deep: true })
+
+/**
+ * 监听状态变化
+ */
+watch(() => props.status, async (newStatus) => {
+  console.log('🎭 Digital human status changed to:', newStatus)
+  
+  switch (newStatus) {
+    case 'ASKING':
+      stopTalkingAnimation()
+      break
+    case 'LISTENING':
+      stopTalkingAnimation()
+      break
+    case 'THINKING':
+      stopTalkingAnimation()
+      break
+    case 'GENERATING_LIP_SYNC':
+      await initAudioContext()
+      startTalkingAnimation()
+      break
+  }
+})
+
+/**
+ * 处理音频流数据
+ */
+const processAudioStream = async (audioStream: string[]) => {
+  if (!audioContext) {
+    await initAudioContext()
+  }
+  
+  try {
+    // 将Base64字符串转换为ArrayBuffer
+    for (const audioChunk of audioStream) {
+      const arrayBuffer = base64ToArrayBuffer(audioChunk)
+      audioQueue.push(arrayBuffer)
+    }
+    
+    // 如果当前没有播放音频,开始播放队列
+    if (!isPlaying.value) {
+      playAudioQueue()
+    }
+  } catch (error) {
+    console.error('Error processing audio stream:', error)
+  }
+}
+
+/**
+ * 将Base64字符串转换为ArrayBuffer
+ */
+const base64ToArrayBuffer = (base64: string): ArrayBuffer => {
+  const binaryString = window.atob(base64)
+  const bytes = new Uint8Array(binaryString.length)
+  for (let i = 0; i < binaryString.length; i++) {
+    bytes[i] = binaryString.charCodeAt(i)
+  }
+  return bytes.buffer
+}
+
+/**
+ * 播放音频队列
+ */
+const playAudioQueue = async () => {
+  if (audioQueue.length === 0 || !audioContext || !currentGainNode) {
+    isPlaying.value = false
+    return
+  }
+
+  isPlaying.value = true
+  const bufferToPlay = audioQueue.shift()!
+  
+  try {
+    const audioBuffer = await audioContext.decodeAudioData(bufferToPlay.slice(0))
+    const source = audioContext.createBufferSource()
+    source.buffer = audioBuffer
+    source.connect(currentGainNode)
+    
+    source.onended = () => {
+      // 播放完一个块后,继续播放下一个
+      playAudioQueue()
+    }
+    
+    source.start()
+    console.log('🎵 Playing audio chunk, queue length:', audioQueue.length)
+    
+  } catch (error) {
+    console.error('音频解码错误:', error)
+    // 即使解码失败,也尝试播放下一个
+    playAudioQueue()
+  }
+}
+
+/**
+ * 停止音频播放
+ */
+const stopAudioPlayback = () => {
+  // 清空音频队列
+  audioQueue = []
+  isPlaying.value = false
+  console.log('🎵 Audio playback stopped')
+}
+
+/**
+ * 设置音量
+ */
+const setVolume = (volume: number) => {
+  if (currentGainNode) {
+    currentGainNode.gain.value = Math.max(0, Math.min(1, volume))
+  }
+}
+
+/**
+ * 初始化Lottie动画
+ */
+const initLottieAnimation = async () => {
+  if (!lottieContainer.value || !props.lottieJson) return
+  
+  try {
+    // 清除之前的动画实例
+    if (lottieInstance) {
+      lottieInstance.destroy()
+      lottieInstance = null
+    }
+
+    // 等待DOM更新
+    await nextTick()
+    
+    // 创建Lottie动画实例
+    lottieInstance = lottie.loadAnimation({
+      container: lottieContainer.value,
+      renderer: 'svg',
+      loop: true,
+      autoplay: false,
+      animationData: props.lottieJson
+    })
+    
+    console.log('🎭 Lottie animation initialized')
+    
+  } catch (error) {
+    console.error('Error initializing Lottie animation:', error)
+  }
+}
+
+/**
+ * 播放Lottie动画
+ */
+const playLottieAnimation = () => {
+  if (lottieInstance) {
+    lottieInstance.play()
+    console.log('🎭 Lottie animation started')
+  }
+}
+
+/**
+ * 停止Lottie动画
+ */
+const stopLottieAnimation = () => {
+  if (lottieInstance) {
+    lottieInstance.stop()
+    console.log('🎭 Lottie animation stopped')
+  }
+}
+
+/**
+ * 开始说话动画
+ */
+const startTalkingAnimation = () => {
+  isTalking.value = true
+  
+  // 播放Lottie动画(如果有)
+  playLottieAnimation()
+  
+  // 模拟嘴巴动画
+  talkingInterval = setInterval(() => {
+    isTalking.value = !isTalking.value
+  }, 150)
+  
+  console.log('🎭 Talking animation started')
+}
+
+/**
+ * 停止说话动画
+ */
+const stopTalkingAnimation = () => {
+  if (talkingInterval) {
+    clearInterval(talkingInterval)
+    talkingInterval = null
+  }
+  isTalking.value = false
+  
+  // 停止Lottie动画
+  stopLottieAnimation()
+  
+  console.log('🎭 Talking animation stopped')
+}
+
+/**
+ * 重置组件状态
+ */
+const resetComponent = () => {
+  stopAudioPlayback()
+  stopTalkingAnimation()
+  isLoading.value = false
+  console.log('🎭 Component reset')
+}
+
+/**
+ * 组件挂载时初始化
+ */
+onMounted(async () => {
+  console.log('🎭 DigitalHumanPlayer mounted')
+  
+  // 初始化音频上下文
+  await initAudioContext()
+  
+  // 如果有Lottie JSON数据,初始化动画
+  if (props.lottieJson) {
+    await initLottieAnimation()
+  }
+})
+
+/**
+ * 组件卸载时清理
+ */
+onUnmounted(() => {
+  console.log('🎭 DigitalHumanPlayer unmounted')
+  
+  // 重置组件状态
+  resetComponent()
+  
+  // 清理Lottie实例
+  if (lottieInstance) {
+    lottieInstance.destroy()
+    lottieInstance = null
+  }
+  
+  // 清理定时器
+  if (talkingInterval) {
+    clearInterval(talkingInterval)
+    talkingInterval = null
+  }
+  
+  // 关闭音频上下文
+  if (audioContext && audioContext.state !== 'closed') {
+    audioContext.close()
+    audioContext = null
+  }
+})
+
+// 暴露组件方法给父组件
+defineExpose({
+  setVolume,
+  stopAudioPlayback,
+  resetComponent,
+  startTalkingAnimation,
+  stopTalkingAnimation
+})
+</script>
+
+<style scoped>
+/* 嘴巴动画样式 */
+.mouth-animation {
+  position: relative;
+  width: 40px;
+  height: 30px;
+}
+
+.mouth {
+  width: 100%;
+  height: 100%;
+  background-color: #1f2937;
+  border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
+  transition: all 0.15s ease-in-out;
+}
+
+.mouth.talking {
+  border-radius: 50% 50% 50% 50% / 30% 30% 70% 70%;
+  transform: scaleY(1.3);
+}
+
+/* 视频样式优化 */
+.video-js {
+  width: 100% !important;
+  height: 100% !important;
+}
+
+.video-js .vjs-poster {
+  background-size: cover;
+  background-position: center;
+}
+
+/* 响应式设计 */
+@media (max-width: 640px) {
+  .mouth-animation {
+    width: 30px;
+    height: 22px;
+  }
+}
+</style>

+ 261 - 0
src/components/FallbackInput.vue

@@ -0,0 +1,261 @@
+<template>
+  <div class="fixed inset-0 bg-black bg-opacity-50 flex items-end justify-center z-50">
+    <!-- 输入面板 -->
+    <div class="bg-white rounded-t-3xl w-full max-w-md mx-4 mb-0 p-6 shadow-2xl">
+      
+      <!-- 拖拽指示器 -->
+      <div class="w-12 h-1 bg-gray-300 rounded-full mx-auto mb-4"></div>
+      
+      <!-- 标题 -->
+      <div class="text-center mb-4">
+        <h3 class="text-lg font-semibold text-gray-900">文字输入</h3>
+        <p class="text-sm text-gray-600">请输入您的回答</p>
+      </div>
+      
+      <!-- 文本输入区域 -->
+      <div class="mb-4">
+        <textarea
+          ref="textareaRef"
+          v-model="inputText"
+          :placeholder="placeholder"
+          :maxlength="maxLength"
+          class="w-full h-32 p-3 border border-gray-300 rounded-xl resize-none focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+          @input="handleInput"
+          @keydown="handleKeydown"
+        ></textarea>
+        
+        <!-- 字数统计 -->
+        <div class="flex justify-between items-center mt-2">
+          <span class="text-xs text-gray-500">
+            {{ inputText.length }} / {{ maxLength }}
+          </span>
+          <span v-if="inputText.length > maxLength * 0.8" class="text-xs text-amber-600">
+            建议控制在{{ Math.floor(maxLength * 0.8) }}字以内
+          </span>
+        </div>
+      </div>
+      
+      <!-- 快捷回复建议 -->
+      <div v-if="showSuggestions && suggestions.length > 0" class="mb-4">
+        <p class="text-sm text-gray-600 mb-2">快捷回复:</p>
+        <div class="flex flex-wrap gap-2">
+          <button
+            v-for="suggestion in suggestions"
+            :key="suggestion"
+            @click="applySuggestion(suggestion)"
+            class="text-xs bg-gray-100 hover:bg-gray-200 text-gray-700 px-3 py-1 rounded-full transition-colors"
+          >
+            {{ suggestion }}
+          </button>
+        </div>
+      </div>
+      
+      <!-- 操作按钮 -->
+      <div class="flex space-x-3">
+        <button
+          @click="$emit('close')"
+          class="flex-1 bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium py-3 px-4 rounded-xl transition-colors"
+        >
+          取消
+        </button>
+        <button
+          @click="submitText"
+          :disabled="!canSubmit"
+          :class="[
+            'flex-1 font-medium py-3 px-4 rounded-xl transition-colors',
+            canSubmit
+              ? 'bg-blue-600 hover:bg-blue-700 text-white'
+              : 'bg-gray-300 text-gray-500 cursor-not-allowed'
+          ]"
+        >
+          {{ submitButtonText }}
+        </button>
+      </div>
+      
+      <!-- 提示信息 -->
+      <div class="mt-3 text-center">
+        <p class="text-xs text-gray-500">
+          回车键快速提交,Shift+回车换行
+        </p>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted, nextTick } from 'vue'
+
+// Props定义
+interface Props {
+  placeholder?: string
+  maxLength?: number
+  showSuggestions?: boolean
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  placeholder: '请输入您的回答...',
+  maxLength: 500,
+  showSuggestions: true
+})
+
+// Events定义
+interface Emits {
+  (e: 'submit-text', text: string): void
+  (e: 'close'): void
+}
+
+const emit = defineEmits<Emits>()
+
+// 组件状态
+const inputText = ref('')
+const textareaRef = ref<HTMLTextAreaElement>()
+
+// 快捷回复建议
+const suggestions = ref([
+  '我认为这个职位很适合我',
+  '我有相关的工作经验',
+  '我对这个行业很感兴趣',
+  '我希望能够学习和成长',
+  '我相信我能胜任这个工作',
+  '我期待加入您的团队'
+])
+
+// 计算属性
+const canSubmit = computed(() => {
+  return inputText.value.trim().length > 0 && inputText.value.length <= props.maxLength
+})
+
+const submitButtonText = computed(() => {
+  if (inputText.value.trim().length === 0) {
+    return '请输入内容'
+  }
+  if (inputText.value.length > props.maxLength) {
+    return '内容过长'
+  }
+  return '提交回答'
+})
+
+/**
+ * 处理输入事件
+ */
+const handleInput = () => {
+  // 自动调整文本框高度
+  if (textareaRef.value) {
+    textareaRef.value.style.height = 'auto'
+    textareaRef.value.style.height = Math.min(textareaRef.value.scrollHeight, 200) + 'px'
+  }
+}
+
+/**
+ * 处理键盘事件
+ */
+const handleKeydown = (event: KeyboardEvent) => {
+  // 回车键提交 (Shift+回车换行)
+  if (event.key === 'Enter' && !event.shiftKey) {
+    event.preventDefault()
+    if (canSubmit.value) {
+      submitText()
+    }
+  }
+  
+  // ESC键关闭
+  if (event.key === 'Escape') {
+    emit('close')
+  }
+}
+
+/**
+ * 应用快捷回复建议
+ */
+const applySuggestion = (suggestion: string) => {
+  if (inputText.value.trim()) {
+    inputText.value += ',' + suggestion
+  } else {
+    inputText.value = suggestion
+  }
+  
+  // 聚焦到文本框末尾
+  nextTick(() => {
+    if (textareaRef.value) {
+      textareaRef.value.focus()
+      textareaRef.value.setSelectionRange(inputText.value.length, inputText.value.length)
+    }
+  })
+}
+
+/**
+ * 提交文本
+ */
+const submitText = () => {
+  if (!canSubmit.value) return
+  
+  const text = inputText.value.trim()
+  console.log('⌨️ Submitting text:', text)
+  
+  emit('submit-text', text)
+  
+  // 清空输入
+  inputText.value = ''
+}
+
+/**
+ * 组件挂载时聚焦到输入框
+ */
+onMounted(() => {
+  nextTick(() => {
+    if (textareaRef.value) {
+      textareaRef.value.focus()
+    }
+  })
+})
+</script>
+
+<style scoped>
+/* 输入面板动画 */
+.bg-white {
+  animation: slideUp 0.3s ease-out;
+}
+
+@keyframes slideUp {
+  from {
+    transform: translateY(100%);
+    opacity: 0;
+  }
+  to {
+    transform: translateY(0);
+    opacity: 1;
+  }
+}
+
+/* 文本框滚动条样式 */
+textarea::-webkit-scrollbar {
+  width: 4px;
+}
+
+textarea::-webkit-scrollbar-track {
+  background: #f1f5f9;
+  border-radius: 2px;
+}
+
+textarea::-webkit-scrollbar-thumb {
+  background: #cbd5e1;
+  border-radius: 2px;
+}
+
+textarea::-webkit-scrollbar-thumb:hover {
+  background: #94a3b8;
+}
+
+/* 响应式设计 */
+@media (max-width: 640px) {
+  .mx-4 {
+    margin-left: 0;
+    margin-right: 0;
+  }
+  
+  .rounded-t-3xl {
+    border-top-left-radius: 1rem;
+    border-top-right-radius: 1rem;
+  }
+}
+</style>

+ 41 - 0
src/components/HelloWorld.vue

@@ -0,0 +1,41 @@
+<script setup lang="ts">
+import { ref } from 'vue'
+
+defineProps<{ msg: string }>()
+
+const count = ref(0)
+</script>
+
+<template>
+  <h1>{{ msg }}</h1>
+
+  <div class="card">
+    <button type="button" @click="count++">count is {{ count }}</button>
+    <p>
+      Edit
+      <code>components/HelloWorld.vue</code> to test HMR
+    </p>
+  </div>
+
+  <p>
+    Check out
+    <a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
+      >create-vue</a
+    >, the official Vue + Vite starter
+  </p>
+  <p>
+    Learn more about IDE Support for Vue in the
+    <a
+      href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
+      target="_blank"
+      >Vue Docs Scaling up Guide</a
+    >.
+  </p>
+  <p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
+</template>
+
+<style scoped>
+.read-the-docs {
+  color: #888;
+}
+</style>

+ 170 - 0
src/components/StatusBar.vue

@@ -0,0 +1,170 @@
+<template>
+  <div class="bg-gray-900 text-white px-4 py-3 flex items-center justify-between shadow-lg">
+    <!-- 左侧: 问题进度 -->
+    <div class="flex items-center space-x-2">
+      <svg class="w-5 h-5 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
+              d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
+      </svg>
+      <span class="text-sm font-medium">
+        问题 {{ currentQuestion }} / {{ totalQuestions }}
+      </span>
+    </div>
+
+    <!-- 中间: 倒计时 -->
+    <div class="flex items-center space-x-2">
+      <svg class="w-5 h-5 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
+              d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
+      </svg>
+      <span 
+        :class="[
+          'text-sm font-mono font-medium',
+          timeWarning ? 'text-red-400 animate-pulse' : 'text-white'
+        ]"
+      >
+        {{ formattedCountdown }}
+      </span>
+    </div>
+
+    <!-- 右侧: 网络状态 -->
+    <div class="flex items-center space-x-2">
+      <div class="flex items-center space-x-1">
+        <!-- 网络信号强度指示器 -->
+        <div class="flex items-end space-x-0.5">
+          <div 
+            :class="[
+              'w-1 h-2 rounded-sm',
+              networkStatusColor
+            ]"
+          ></div>
+          <div 
+            :class="[
+              'w-1 h-3 rounded-sm',
+              networkStatus === 'good' ? networkStatusColor : 'bg-gray-600'
+            ]"
+          ></div>
+          <div 
+            :class="[
+              'w-1 h-4 rounded-sm',
+              networkStatus === 'good' ? networkStatusColor : 'bg-gray-600'
+            ]"
+          ></div>
+          <div 
+            :class="[
+              'w-1 h-5 rounded-sm',
+              networkStatus === 'good' ? networkStatusColor : 'bg-gray-600'
+            ]"
+          ></div>
+        </div>
+        
+        <!-- 网络状态圆点 -->
+        <div 
+          :class="[
+            'w-2 h-2 rounded-full ml-2',
+            networkStatusColor,
+            networkStatus === 'good' ? 'animate-pulse' : ''
+          ]"
+        ></div>
+      </div>
+      
+      <!-- 网络状态文本 (仅在移动端隐藏) -->
+      <span class="text-xs text-gray-300 hidden sm:inline">
+        {{ networkStatusText }}
+      </span>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+
+// Props定义
+interface Props {
+  currentQuestion: number
+  totalQuestions: number
+  countdown: number // 剩余秒数
+  networkStatus: 'good' | 'poor' | 'disconnected'
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  currentQuestion: 1,
+  totalQuestions: 4,
+  countdown: 900,
+  networkStatus: 'good'
+})
+
+// 格式化倒计时显示
+const formattedCountdown = computed(() => {
+  const minutes = Math.floor(props.countdown / 60)
+  const seconds = props.countdown % 60
+  return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
+})
+
+// 时间警告状态 (剩余时间少于2分钟时)
+const timeWarning = computed(() => {
+  return props.countdown <= 120 // 2分钟
+})
+
+// 网络状态颜色
+const networkStatusColor = computed(() => {
+  switch (props.networkStatus) {
+    case 'good':
+      return 'bg-green-500'
+    case 'poor':
+      return 'bg-amber-500'
+    case 'disconnected':
+      return 'bg-red-500'
+    default:
+      return 'bg-gray-500'
+  }
+})
+
+// 网络状态文本
+const networkStatusText = computed(() => {
+  switch (props.networkStatus) {
+    case 'good':
+      return '网络良好'
+    case 'poor':
+      return '网络不稳定'
+    case 'disconnected':
+      return '网络断开'
+    default:
+      return '未知状态'
+  }
+})
+</script>
+
+<style scoped>
+/* 状态栏固定在顶部 */
+.bg-gray-900 {
+  position: sticky;
+  top: 0;
+  z-index: 40;
+}
+
+/* 响应式设计 */
+@media (max-width: 640px) {
+  .space-x-2 {
+    gap: 0.25rem;
+  }
+  
+  .text-sm {
+    font-size: 0.75rem;
+  }
+}
+
+/* 网络信号动画 */
+@keyframes signal-pulse {
+  0%, 100% {
+    opacity: 1;
+  }
+  50% {
+    opacity: 0.5;
+  }
+}
+
+.animate-pulse {
+  animation: signal-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+}
+</style>

+ 402 - 0
src/components/VoiceInputHandler.vue

@@ -0,0 +1,402 @@
+<template>
+  <div class="fixed bottom-8 left-1/2 transform -translate-x-1/2 z-50">
+    <div class="flex flex-col items-center space-y-4">
+      
+      <!-- 麦克风按钮 -->
+      <button
+        @click="toggleRecording"
+        :disabled="isProcessing"
+        :class="[
+          'w-16 h-16 rounded-full flex items-center justify-center transition-all duration-300 shadow-lg',
+          isRecording 
+            ? 'bg-red-500 hover:bg-red-600 recording-pulse' 
+            : 'bg-blue-600 hover:bg-blue-700',
+          isProcessing && 'opacity-50 cursor-not-allowed'
+        ]"
+      >
+        <svg 
+          :class="['w-8 h-8 text-white transition-transform duration-200', isRecording && 'scale-110']"
+          fill="none" 
+          stroke="currentColor" 
+          viewBox="0 0 24 24"
+        >
+          <path 
+            v-if="!isRecording"
+            stroke-linecap="round" 
+            stroke-linejoin="round" 
+            stroke-width="2" 
+            d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" 
+          />
+          <path 
+            v-else
+            stroke-linecap="round" 
+            stroke-linejoin="round" 
+            stroke-width="2" 
+            d="M21 12a9 9 0 11-6.219-8.56" 
+          />
+        </svg>
+      </button>
+
+      <!-- 录音状态指示器 -->
+      <div class="flex flex-col items-center space-y-2">
+        <!-- 音频波形可视化 -->
+        <div v-if="isRecording" class="flex items-center space-x-1">
+          <div 
+            v-for="i in 7" 
+            :key="i"
+            :class="[
+              'w-1 bg-red-500 rounded-full wave-animation',
+              getWaveHeight(i)
+            ]"
+            :style="{ animationDelay: `${i * 0.1}s` }"
+          ></div>
+        </div>
+
+        <!-- 状态文本 -->
+        <div class="text-center">
+          <p v-if="!isRecording && !isProcessing" class="text-white text-sm font-medium">
+            点击开始录音
+          </p>
+          <p v-else-if="isRecording" class="text-red-300 text-sm font-medium animate-pulse">
+            正在录音...
+          </p>
+          <p v-else-if="isProcessing" class="text-blue-300 text-sm font-medium">
+            处理中...
+          </p>
+        </div>
+
+        <!-- 录音时长 -->
+        <div v-if="isRecording" class="text-white text-xs">
+          {{ formatTime(recordingDuration) }}
+        </div>
+      </div>
+
+      <!-- 提示文本 -->
+      <div v-if="!isRecording" class="text-center max-w-xs">
+        <p class="text-gray-300 text-xs">
+          长按录音或点击开关录音模式
+        </p>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted, inject } from 'vue'
+
+// Events定义
+interface Emits {
+  (e: 'on-transcript-update', transcript: string): void
+  (e: 'on-speech-end', finalTranscript: string): void
+}
+
+const emit = defineEmits<Emits>()
+
+// 从父组件注入WebSocket发送方法
+const sendToDigitalHuman = inject<(data: any) => void>('sendToDigitalHuman')
+
+// 组件状态
+const isRecording = ref(false)
+const isProcessing = ref(false)
+const recordingDuration = ref(0)
+const hasPermission = ref(false)
+
+// 音频相关
+let mediaRecorder: MediaRecorder | null = null
+let audioStream: MediaStream | null = null
+let audioChunks: Blob[] = []
+
+// 定时器
+let durationTimer: NodeJS.Timeout | null = null
+
+/**
+ * 初始化麦克风权限
+ */
+const initMicrophone = async () => {
+  try {
+    audioStream = await navigator.mediaDevices.getUserMedia({
+      audio: {
+        sampleRate: 16000,
+        channelCount: 1,
+        echoCancellation: true,
+        noiseSuppression: true
+      }
+    })
+    hasPermission.value = true
+    console.log('🎤 Microphone permission granted')
+  } catch (error) {
+    console.error('🎤 Microphone permission denied:', error)
+    hasPermission.value = false
+  }
+}
+
+/**
+ * 切换录音状态
+ */
+const toggleRecording = async () => {
+  if (isProcessing.value) return
+  
+  if (!hasPermission.value) {
+    await initMicrophone()
+    if (!hasPermission.value) return
+  }
+  
+  if (isRecording.value) {
+    stopRecording()
+  } else {
+    startRecording()
+  }
+}
+
+/**
+ * 开始录音
+ */
+const startRecording = async () => {
+  if (!audioStream) {
+    await initMicrophone()
+    if (!audioStream) return
+  }
+  
+  console.log('🎤 Starting recording...')
+  isRecording.value = true
+  recordingDuration.value = 0
+  audioChunks = []
+  
+  // 创建MediaRecorder
+  mediaRecorder = new MediaRecorder(audioStream, {
+    mimeType: 'audio/webm;codecs=opus'
+  })
+  
+  // 处理录音数据
+  mediaRecorder.ondataavailable = (event) => {
+    if (event.data.size > 0) {
+      audioChunks.push(event.data)
+      // 实时发送音频数据
+      sendAudioChunk(event.data)
+    }
+  }
+  
+  // 录音结束处理
+  mediaRecorder.onstop = () => {
+    processRecordedAudio()
+  }
+  
+  // 开始录音
+  mediaRecorder.start(100) // 每100ms收集一次数据
+  
+  // 开始录音时长计时
+  durationTimer = setInterval(() => {
+    recordingDuration.value++
+  }, 1000)
+}
+
+/**
+ * 停止录音
+ */
+const stopRecording = () => {
+  console.log('🎤 Stopping recording...')
+  isRecording.value = false
+  isProcessing.value = true
+  
+  if (mediaRecorder && mediaRecorder.state !== 'inactive') {
+    mediaRecorder.stop()
+  }
+  
+  // 清理定时器
+  if (durationTimer) {
+    clearInterval(durationTimer)
+    durationTimer = null
+  }
+}
+
+/**
+ * 发送音频块到服务器
+ */
+const sendAudioChunk = async (audioBlob: Blob) => {
+  if (!sendToDigitalHuman) {
+    console.warn('🎤 WebSocket send function not available')
+    return
+  }
+  
+  try {
+    const arrayBuffer = await audioBlob.arrayBuffer()
+    const base64Audio = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)))
+    
+    // 发送符合数字人API格式的音频数据
+    const audioData = {
+      header: {
+        app_id: '379db9f6',
+        uid: `user_${Date.now()}`,
+        status: 1, // 中间状态
+        stmid: String(Date.now())
+      },
+      payload: {
+        audio: {
+          encoding: 'raw',
+          sample_rate: 16000,
+          channels: 1,
+          bit_depth: 16,
+          status: 1, // 音频开始/中间状态
+          audio: base64Audio
+        }
+      }
+    }
+    
+    sendToDigitalHuman(audioData)
+  } catch (error) {
+    console.error('🎤 Error sending audio chunk:', error)
+  }
+}
+
+/**
+ * 处理录音完成的音频
+ */
+const processRecordedAudio = async () => {
+  if (!sendToDigitalHuman) {
+    console.warn('🎤 WebSocket send function not available')
+    isProcessing.value = false
+    return
+  }
+  
+  try {
+    // 发送音频结束信号
+    const audioEndData = {
+      header: {
+        app_id: '379db9f6',
+        uid: `user_${Date.now()}`,
+        status: 2, // 结束状态
+        stmid: String(Date.now())
+      },
+      payload: {
+        audio: {
+          encoding: 'raw',
+          sample_rate: 16000,
+          channels: 1,
+          bit_depth: 16,
+          status: 2, // 音频结束状态
+          audio: '' // 空音频表示结束
+        }
+      }
+    }
+    
+    sendToDigitalHuman(audioEndData)
+    console.log('🎤 Audio recording completed and sent to server')
+    
+    // 触发语音结束事件
+    emit('on-speech-end', '用户语音输入完成')
+    
+    // 重置状态
+    setTimeout(() => {
+      isProcessing.value = false
+      recordingDuration.value = 0
+    }, 500)
+    
+  } catch (error) {
+    console.error('🎤 Error processing recorded audio:', error)
+    isProcessing.value = false
+    recordingDuration.value = 0
+  }
+}
+
+/**
+ * 格式化时间显示
+ */
+const formatTime = (seconds: number): string => {
+  const mins = Math.floor(seconds / 60)
+  const secs = seconds % 60
+  return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
+}
+
+/**
+ * 获取音频波形高度类
+ */
+const getWaveHeight = (index: number): string => {
+  const heights = ['h-2', 'h-4', 'h-6', 'h-8', 'h-6', 'h-4', 'h-2']
+  return heights[index - 1] || 'h-2'
+}
+
+/**
+ * 处理长按事件
+ */
+const handleTouchStart = () => {
+  if (!isRecording.value && !isProcessing.value) {
+    startRecording()
+  }
+}
+
+const handleTouchEnd = () => {
+  if (isRecording.value) {
+    stopRecording()
+  }
+}
+
+/**
+ * 清理音频资源
+ */
+const cleanupAudio = () => {
+  if (mediaRecorder && mediaRecorder.state !== 'inactive') {
+    mediaRecorder.stop()
+  }
+  
+  if (audioStream) {
+    audioStream.getTracks().forEach(track => track.stop())
+    audioStream = null
+  }
+  
+  if (durationTimer) {
+    clearInterval(durationTimer)
+    durationTimer = null
+  }
+  
+  mediaRecorder = null
+  audioChunks = []
+  hasPermission.value = false
+}
+
+/**
+ * 组件挂载时添加事件监听
+ */
+onMounted(() => {
+  console.log('🎤 VoiceInputHandler mounted')
+  
+  // 添加长按支持
+  const button = document.querySelector('button')
+  if (button) {
+    button.addEventListener('touchstart', handleTouchStart, { passive: true })
+    button.addEventListener('touchend', handleTouchEnd, { passive: true })
+  }
+})
+
+/**
+ * 组件卸载时清理
+ */
+onUnmounted(() => {
+  console.log('🎤 VoiceInputHandler unmounted')
+  
+  // 清理音频资源
+  cleanupAudio()
+  
+  // 移除事件监听
+  const button = document.querySelector('button')
+  if (button) {
+    button.removeEventListener('touchstart', handleTouchStart)
+    button.removeEventListener('touchend', handleTouchEnd)
+  }
+})
+</script>
+
+<style scoped>
+/* 录音脉冲动画已在全局样式中定义 */
+
+/* 音频波形动画 */
+.wave-animation {
+  animation: wave 1.5s ease-in-out infinite;
+}
+
+/* 确保按钮在小屏幕上可见 */
+@media (max-width: 640px) {
+  .fixed.bottom-8 {
+    bottom: 6rem;
+  }
+}
+</style>

+ 292 - 0
src/composables/useDigitalHuman.ts

@@ -0,0 +1,292 @@
+import { ref, onUnmounted } from 'vue';
+import CryptoJS from 'crypto-js';
+
+// 定义从服务器接收到的数据的结构
+interface ServerResponse {
+  header: {
+    code: number;
+    message: string;
+    status: number;
+    sid?: string;
+  };
+  payload?: {
+    choices?: {
+      status: number;
+      seq: number;
+      text: Array<{
+        content: string;
+        role: string;
+        index: number;
+      }>;
+    };
+    usage?: {
+      text: {
+        question_tokens: number;
+        prompt_tokens: number;
+        completion_tokens: number;
+        total_tokens: number;
+      };
+    };
+  };
+}
+
+// 连接状态枚举
+enum ConnectionStatus {
+  DISCONNECTED = 'disconnected',
+  CONNECTING = 'connecting',
+  CONNECTED = 'connected',
+  RECONNECTING = 'reconnecting',
+  ERROR = 'error'
+}
+
+// 配置接口
+interface DigitalHumanConfig {
+  appId: string;
+  apiSecret: string;
+  apiKey: string;
+  maxReconnectAttempts?: number;
+  reconnectInterval?: number;
+  heartbeatInterval?: number;
+}
+
+// 默认配置
+const defaultConfig: DigitalHumanConfig = {
+  appId: '379db9f6',
+  apiSecret: 'YTJmZTgwY2ZkMWVjMzE5NGY2ZTA4Nzk1',
+  apiKey: 'b75ae5315729c32e4d38785aac8e26d0',
+  maxReconnectAttempts: 5,
+  reconnectInterval: 3000,
+  heartbeatInterval: 30000
+}
+
+export function useDigitalHuman(onMessage?: (data: ServerResponse) => void, config: Partial<DigitalHumanConfig> = {}) {
+  const finalConfig = { ...defaultConfig, ...config };
+  
+  const ws = ref<WebSocket | null>(null);
+  const connectionStatus = ref<ConnectionStatus>(ConnectionStatus.DISCONNECTED);
+  const isConnected = ref(false);
+  const reconnectAttempts = ref(0);
+  const lastError = ref<string | null>(null);
+  
+  // 定时器
+  let reconnectTimer: NodeJS.Timeout | null = null;
+  let heartbeatTimer: NodeJS.Timeout | null = null;
+
+  /**
+   * 生成鉴权URL
+   */
+  const generateAuthUrl = (): string => {
+    const host = 'spark-api.xf-yun.com';
+    const path = '/v3.5/chat';  // 使用星火3.5版本
+    const date = new Date().toUTCString();
+    
+    // 生成签名字符串
+    const signatureOrigin = `host: ${host}\ndate: ${date}\nGET ${path} HTTP/1.1`;
+    
+    // 使用HMAC-SHA256生成签名
+    const signature = CryptoJS.HmacSHA256(signatureOrigin, finalConfig.apiSecret);
+    const signatureBase64 = CryptoJS.enc.Base64.stringify(signature);
+    
+    // 生成authorization字符串
+    const authorizationOrigin = `api_key="${finalConfig.apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signatureBase64}"`;
+    const authorization = btoa(authorizationOrigin);
+    
+    // 构建WebSocket URL
+    const params = new URLSearchParams({
+      authorization,
+      date,
+      host
+    });
+    
+    return `wss://${host}${path}?${params.toString()}`;
+  };
+
+  /**
+   * 清理定时器
+   */
+  const clearTimers = () => {
+    if (reconnectTimer) {
+      clearTimeout(reconnectTimer);
+      reconnectTimer = null;
+    }
+    if (heartbeatTimer) {
+      clearInterval(heartbeatTimer);
+      heartbeatTimer = null;
+    }
+  };
+
+  /**
+   * 启动心跳检测
+   */
+  const startHeartbeat = () => {
+    clearTimers();
+    heartbeatTimer = setInterval(() => {
+      if (ws.value && ws.value.readyState === WebSocket.OPEN) {
+        send({ type: 'ping' });
+      }
+    }, finalConfig.heartbeatInterval!);
+  };
+
+  /**
+   * 处理重连
+   */
+  const handleReconnect = () => {
+    if (reconnectAttempts.value >= finalConfig.maxReconnectAttempts!) {
+      console.error('🔌 Maximum reconnection attempts reached');
+      connectionStatus.value = ConnectionStatus.ERROR;
+      lastError.value = 'Maximum reconnection attempts reached';
+      return;
+    }
+
+    reconnectAttempts.value++;
+    connectionStatus.value = ConnectionStatus.RECONNECTING;
+    
+    console.log(`🔌 Attempting to reconnect (${reconnectAttempts.value}/${finalConfig.maxReconnectAttempts})...`);
+    
+    reconnectTimer = setTimeout(() => {
+      connect();
+    }, finalConfig.reconnectInterval!);
+  };
+
+  /**
+   * 连接到服务器
+   */
+  const connect = () => {
+    if (connectionStatus.value === ConnectionStatus.CONNECTING || 
+        connectionStatus.value === ConnectionStatus.CONNECTED) {
+      return;
+    }
+
+    try {
+      connectionStatus.value = ConnectionStatus.CONNECTING;
+      lastError.value = null;
+      
+      const wsUrl = generateAuthUrl();
+      console.log('🔌 Connecting to WebSocket...', wsUrl);
+      
+      ws.value = new WebSocket(wsUrl);
+
+      ws.value.onopen = () => {
+        console.log('🔌 WebSocket connected successfully');
+        connectionStatus.value = ConnectionStatus.CONNECTED;
+        isConnected.value = true;
+        reconnectAttempts.value = 0;
+        lastError.value = null;
+        
+        // 启动心跳检测
+        startHeartbeat();
+      };
+
+      ws.value.onmessage = (event) => {
+        try {
+          const data: ServerResponse = JSON.parse(event.data);
+          
+          // 处理心跳响应
+          if (data.header?.message === 'pong') {
+            return;
+          }
+          
+          // 调用回调函数处理消息
+          if (onMessage) {
+            onMessage(data);
+          }
+        } catch (error) {
+          console.error('🔌 Error parsing message:', error);
+          lastError.value = 'Message parsing error';
+        }
+      };
+
+      ws.value.onerror = (error) => {
+        console.error('🔌 WebSocket error:', error);
+        connectionStatus.value = ConnectionStatus.ERROR;
+        isConnected.value = false;
+        lastError.value = 'WebSocket connection error';
+      };
+
+      ws.value.onclose = (event) => {
+        console.log('🔌 WebSocket connection closed:', event.code, event.reason);
+        connectionStatus.value = ConnectionStatus.DISCONNECTED;
+        isConnected.value = false;
+        clearTimers();
+        
+        // 如果不是主动关闭,尝试重连
+        if (event.code !== 1000 && reconnectAttempts.value < finalConfig.maxReconnectAttempts!) {
+          handleReconnect();
+        }
+      };
+      
+    } catch (error) {
+      console.error('🔌 Error creating WebSocket connection:', error);
+      connectionStatus.value = ConnectionStatus.ERROR;
+      lastError.value = 'Failed to create WebSocket connection';
+    }
+  };
+
+  /**
+   * 发送数据
+   */
+  const send = (data: object) => {
+    if (ws.value && ws.value.readyState === WebSocket.OPEN) {
+      try {
+        ws.value.send(JSON.stringify(data));
+        return true;
+      } catch (error) {
+        console.error('🔌 Error sending data:', error);
+        lastError.value = 'Failed to send data';
+        return false;
+      }
+    } else {
+      console.warn('🔌 WebSocket is not connected, cannot send data');
+      // 如果连接断开,尝试重连
+      if (connectionStatus.value === ConnectionStatus.DISCONNECTED) {
+        connect();
+      }
+      return false;
+    }
+  };
+  
+  /**
+   * 关闭连接
+   */
+  const disconnect = () => {
+    console.log('🔌 Disconnecting WebSocket...');
+    clearTimers();
+    reconnectAttempts.value = finalConfig.maxReconnectAttempts!; // 防止自动重连
+    
+    if (ws.value) {
+      ws.value.close(1000, 'Manual disconnect');
+      ws.value = null;
+    }
+    
+    connectionStatus.value = ConnectionStatus.DISCONNECTED;
+    isConnected.value = false;
+  };
+
+  /**
+   * 重置连接状态
+   */
+  const reset = () => {
+    disconnect();
+    reconnectAttempts.value = 0;
+    lastError.value = null;
+  };
+
+  // 组件卸载时清理资源
+  onUnmounted(() => {
+    disconnect();
+  });
+
+  return {
+    // 状态
+    isConnected,
+    connectionStatus,
+    lastError,
+    reconnectAttempts,
+    
+    // 方法
+    connect,
+    send,
+    disconnect,
+    reset
+  };
+}

+ 380 - 0
src/composables/useInterview.ts

@@ -0,0 +1,380 @@
+import { ref, reactive, computed, watch } from 'vue'
+import { useDigitalHuman } from './useDigitalHuman';
+
+// 面试状态枚举
+export const InterviewStatus = {
+  IDLE: 'IDLE',
+  ASKING: 'ASKING',
+  LISTENING: 'LISTENING',
+  THINKING: 'THINKING',
+  GENERATING_LIP_SYNC: 'GENERATING_LIP_SYNC',
+  COMPLETED: 'COMPLETED'
+} as const
+
+export type InterviewStatusType = typeof InterviewStatus[keyof typeof InterviewStatus]
+
+// 面试问题类型定义
+export interface Question {
+  id: number
+  questionText: string
+  questionVideoUrl: string
+}
+
+// 面试状态类型定义
+export interface InterviewState {
+  status: InterviewStatusType
+  userAnswer: string
+  finalTranscript: string
+  isListening: boolean
+}
+
+/**
+ * 面试核心逻辑控制器
+ * 管理整个面试流程的状态和行为
+ */
+
+export function useInterview() {
+  // 新增:用于存储音频和文本流
+  const liveAudio = ref<string[]>([]);
+  const liveText = ref('');
+
+  // 处理服务器消息的回调函数
+  const handleServerMessage = (data: any) => {
+    console.log('📨 Received server message:', data);
+    
+    if (data.header.code !== 0) {
+      console.error("API 错误:", data.header.message);
+      return;
+    }
+
+    if (data.payload) {
+      // 处理讯飞星火API的对话响应
+      if (data.payload.choices && data.payload.choices.text) {
+        // 处理对话文本流
+        data.payload.choices.text.forEach((choice: any) => {
+          if (choice.content) {
+            liveText.value += choice.content;
+            console.log('💬 Received text chunk:', choice.content);
+          }
+        });
+        
+        // 当收到文本时,进入口型同步生成状态
+        if (interviewState.status === InterviewStatus.THINKING) {
+          interviewState.status = InterviewStatus.GENERATING_LIP_SYNC;
+        }
+      }
+      
+      // 检查是否为最后一条消息(status: 2表示结束)
+      if (data.header.status === 2) {
+        console.log('✅ AI response completed');
+        // AI回答完成,等待一段时间后进入下一个问题或结束面试
+        setTimeout(() => {
+          if (isLastQuestion.value) {
+            endInterview();
+          } else {
+            moveToNextQuestion();
+          }
+        }, 2000); // 等待2秒让用户看完回答
+      }
+    }
+  }
+  
+  // 模拟面试问题数据
+  const questions = ref<Question[]>([
+    {
+      id: 1,
+      questionText: "请简单介绍一下你自己。",
+      questionVideoUrl: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
+    },
+    {
+      id: 2,
+      questionText: "你为什么选择这个职位?",
+      questionVideoUrl: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4"
+    },
+    {
+      id: 3,
+      questionText: "请描述一下你的职业规划。",
+      questionVideoUrl: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4"
+    },
+    {
+      id: 4,
+      questionText: "你有什么问题想问我们吗?",
+      questionVideoUrl: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4"
+    }
+  ])
+
+  // 当前问题索引
+  const currentQuestionIndex = ref(0)
+
+  // 面试状态
+  const interviewState = reactive<InterviewState>({
+    status: InterviewStatus.IDLE,
+    userAnswer: '',
+    finalTranscript: '',
+    isListening: false
+  })
+
+  // 面试时长控制(15分钟 = 900秒)
+  const totalInterviewTime = 900
+  const remainingTime = ref(totalInterviewTime)
+  const isTimerRunning = ref(false)
+  let timerInterval: number | null = null
+
+  // 集成数字人WebSocket连接
+  const digitalHumanConfig = {
+    appId: '379db9f6',
+    apiSecret: 'YTJmZTgwY2ZkMWVjMzE5NGY2ZTA4Nzk1',
+    apiKey: 'b75ae5315729c32e4d38785aac8e26d0'
+  }
+  const { isConnected, connect, send, disconnect } = useDigitalHuman(handleServerMessage, digitalHumanConfig)
+
+  // 计算属性
+  const currentQuestion = computed(() => {
+    return questions.value[currentQuestionIndex.value]
+  })
+
+  const totalQuestions = computed(() => questions.value.length)
+
+  const isLastQuestion = computed(() => {
+    return currentQuestionIndex.value >= questions.value.length - 1
+  })
+
+  const formattedTime = computed(() => {
+    const minutes = Math.floor(remainingTime.value / 60)
+    const seconds = remainingTime.value % 60
+    return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
+  })
+
+  // 模拟Lottie动画数据
+  const mockLottieData = {
+    v: "5.7.4",
+    fr: 30,
+    ip: 0,
+    op: 90,
+    w: 400,
+    h: 400,
+    nm: "Talking Animation",
+    assets: [],
+    layers: [
+      {
+        ddd: 0,
+        ind: 1,
+        ty: 4,
+        nm: "Mouth",
+        sr: 1,
+        ks: {
+          o: { a: 0, k: 100 },
+          r: { a: 0, k: 0 },
+          p: { a: 0, k: [200, 200, 0] },
+          a: { a: 0, k: [0, 0, 0] },
+          s: { a: 0, k: [100, 100, 100] }
+        },
+        ao: 0,
+        shapes: [
+          {
+            ty: "el",
+            p: { a: 0, k: [0, 0] },
+            s: { a: 1, k: [
+              { t: 0, s: [20, 8] },
+              { t: 15, s: [25, 15] },
+              { t: 30, s: [20, 8] },
+              { t: 45, s: [30, 12] },
+              { t: 60, s: [20, 8] },
+              { t: 75, s: [25, 15] },
+              { t: 90, s: [20, 8] }
+            ]},
+            d: 1
+          }
+        ],
+        ip: 0,
+        op: 90,
+        st: 0,
+        bm: 0
+      }
+    ]
+  }
+
+  /**
+   * 提问函数
+   */
+  const askQuestion = () => {
+    interviewState.status = InterviewStatus.ASKING;
+    const question = currentQuestion.value;
+    if (!question) return;
+    
+    // 清空上一轮的音视频数据
+    liveAudio.value = [];
+    liveText.value = '';
+
+    // 发送问题给讯飞星火API(对话格式)
+    const questionData = {
+      header: {
+        app_id: digitalHumanConfig.appId,
+        uid: `user_${Date.now()}`
+      },
+      parameter: {
+        chat: {
+          domain: "generalv3.5",
+          temperature: 0.5,
+          max_tokens: 2048
+        }
+      },
+      payload: {
+        message: {
+          text: [
+            {
+              role: "system",
+              content: "你是一位专业的面试官,请根据求职者的回答进行深入追问和评估。"
+            },
+            {
+              role: "user",
+              content: question.questionText
+            }
+          ]
+        }
+      }
+    }
+    send(questionData);
+  };
+
+  /**
+   * 开始面试流程
+   */
+  const startInterview = () => {
+    console.log('🎯 Starting interview process...')
+    currentQuestionIndex.value = 0
+    startTimer()
+    
+    // 建立 WebSocket 连接
+    connect();
+    
+    // 监听连接状态,连接成功后开始提问
+    watch(isConnected, (newVal) => {
+      if (newVal) {
+        askQuestion(); // 连接成功后开始提问
+      }
+    });
+  }
+
+  /**
+   * 处理用户回答
+   * @param answer 用户的回答文本
+   */
+  const processUserAnswer = (answer: string) => {
+    console.log('🗣️ Processing user answer:', answer)
+    interviewState.userAnswer = answer
+    interviewState.finalTranscript = answer
+    interviewState.isListening = false
+    
+    // 进入思考状态
+    interviewState.status = InterviewStatus.THINKING
+    console.log('🤔 AI is thinking...')
+    
+    // 这里不需要手动发送,因为音频数据会通过VoiceInputHandler实时发送
+    // processUserAnswer主要用于处理文本输入的情况
+    console.log('User answer processed, waiting for AI response...')
+
+    // 后续的流程会由 handleServerMessage 中对 TTS 和 NLP 的处理来驱动
+    // 当收到完整回复后,会自动进入下一个问题或结束面试
+  }
+
+  /**
+   * 移动到下一个问题
+   */
+  const moveToNextQuestion = () => {
+    console.log('➡️ Moving to next question...')
+    currentQuestionIndex.value++
+    interviewState.userAnswer = ''
+    interviewState.finalTranscript = ''
+    
+    // 直接调用askQuestion开始新问题
+    askQuestion();
+  }
+
+  /**
+   * 结束面试
+   */
+  const endInterview = () => {
+    console.log('🎉 Interview completed!')
+    interviewState.status = InterviewStatus.COMPLETED
+    stopTimer()
+    // 断开WebSocket连接
+    disconnect()
+  }
+
+  /**
+   * 开始计时器
+   */
+  const startTimer = () => {
+    if (!isTimerRunning.value) {
+      isTimerRunning.value = true
+      timerInterval = setInterval(() => {
+        if (remainingTime.value > 0) {
+          remainingTime.value--
+        } else {
+          // 时间到,强制结束面试
+          endInterview()
+        }
+      }, 1000)
+    }
+  }
+
+  /**
+   * 停止计时器
+   */
+  const stopTimer = () => {
+    if (timerInterval) {
+      clearInterval(timerInterval)
+      timerInterval = null
+    }
+    isTimerRunning.value = false
+  }
+
+  /**
+   * 重置面试状态
+   */
+  const resetInterview = () => {
+    console.log('🔄 Resetting interview state...')
+    currentQuestionIndex.value = 0
+    interviewState.status = InterviewStatus.IDLE
+    interviewState.userAnswer = ''
+    interviewState.finalTranscript = ''
+    interviewState.isListening = false
+    remainingTime.value = totalInterviewTime
+    stopTimer()
+    // 断开WebSocket连接
+    disconnect()
+    // 清空音频和文本数据
+    liveAudio.value = []
+    liveText.value = ''
+  }
+
+  return {
+    // 状态
+    questions: questions.value,
+    currentQuestionIndex,
+    interviewState,
+    currentQuestion,
+    totalQuestions,
+    isLastQuestion,
+    remainingTime,
+    formattedTime,
+    isTimerRunning,
+    mockLottieData,
+    liveAudio,
+    liveText,
+    isConnected,
+    
+    // 方法
+    startInterview,
+    processUserAnswer,
+    moveToNextQuestion,
+    endInterview,
+    resetInterview,
+    startTimer,
+    stopTimer,
+    askQuestion,
+    send
+  }
+  
+}

+ 8 - 0
src/main.ts

@@ -0,0 +1,8 @@
+import { createApp } from 'vue'
+import './style.css'
+import App from './App.vue'
+import router from './router'
+
+const app = createApp(App)
+app.use(router)
+app.mount('#app')

+ 39 - 0
src/router.ts

@@ -0,0 +1,39 @@
+import { createRouter, createWebHistory } from 'vue-router'
+import WelcomeView from './views/WelcomeView.vue'
+import DeviceCheckView from './views/DeviceCheckView.vue'
+import InterviewView from './views/InterviewView.vue'
+import CompletionView from './views/CompletionView.vue'
+import TestView from './views/TestView.vue'
+
+const router = createRouter({
+  history: createWebHistory(),
+  routes: [
+    {
+      path: '/',
+      name: 'welcome',
+      component: WelcomeView
+    },
+    {
+      path: '/device-check',
+      name: 'device-check',
+      component: DeviceCheckView
+    },
+    {
+      path: '/interview',
+      name: 'interview',
+      component: InterviewView
+    },
+    {
+      path: '/completion',
+      name: 'completion',
+      component: CompletionView
+    },
+    {
+      path: '/test',
+      name: 'test',
+      component: TestView
+    }
+  ]
+})
+
+export default router

+ 96 - 0
src/style.css

@@ -0,0 +1,96 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+  font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+  line-height: 1.5;
+  font-weight: 400;
+  color-scheme: light;
+  color: #213547;
+  background-color: #f8fafc;
+  font-synthesis: none;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+body {
+  margin: 0;
+  min-width: 320px;
+  min-height: 100vh;
+  overflow-x: hidden;
+}
+
+#app {
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+}
+
+/* Custom scrollbar */
+::-webkit-scrollbar {
+  width: 6px;
+}
+
+::-webkit-scrollbar-track {
+  background: #f1f5f9;
+}
+
+::-webkit-scrollbar-thumb {
+  background: #cbd5e1;
+  border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+  background: #94a3b8;
+}
+
+/* Video.js custom styles */
+.video-js {
+  width: 100% !important;
+  height: 100% !important;
+  border-radius: 12px;
+  overflow: hidden;
+}
+
+.video-js .vjs-big-play-button {
+  display: none !important;
+}
+
+.video-js .vjs-control-bar {
+  display: none !important;
+}
+
+.video-js .vjs-poster {
+  background-size: cover;
+}
+
+/* Animation utilities */
+@keyframes recording-pulse {
+  0%, 100% {
+    opacity: 1;
+    transform: scale(1);
+  }
+  50% {
+    opacity: 0.8;
+    transform: scale(1.05);
+  }
+}
+
+.recording-pulse {
+  animation: recording-pulse 1.5s ease-in-out infinite;
+}
+
+@keyframes wave {
+  0%, 100% {
+    transform: scaleY(0.3);
+  }
+  50% {
+    transform: scaleY(1);
+  }
+}
+
+.wave-animation {
+  animation: wave 1.5s ease-in-out infinite;
+}

+ 143 - 0
src/views/CompletionView.vue

@@ -0,0 +1,143 @@
+<template>
+  <div class="min-h-screen bg-gradient-to-br from-green-50 via-white to-blue-50 flex flex-col items-center justify-center px-4">
+    <div class="w-full max-w-md mx-auto text-center">
+      
+      <!-- 成功图标 -->
+      <div class="mb-8">
+        <div class="w-24 h-24 mx-auto mb-6 bg-green-100 rounded-full flex items-center justify-center">
+          <svg class="w-12 h-12 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
+                  d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
+          </svg>
+        </div>
+        <h1 class="text-3xl font-bold text-gray-900 mb-4">面试已完成</h1>
+        <p class="text-lg text-gray-600 mb-6">感谢您的参与!</p>
+      </div>
+
+      <!-- 面试总结卡片 -->
+      <div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 mb-8">
+        <h2 class="text-lg font-semibold text-gray-900 mb-4">面试总结</h2>
+        <div class="space-y-3">
+          <div class="flex justify-between items-center py-2 border-b border-gray-100">
+            <span class="text-sm text-gray-600">面试时长</span>
+            <span class="text-sm font-medium text-gray-900">{{ formatDuration(interviewDuration) }}</span>
+          </div>
+          <div class="flex justify-between items-center py-2 border-b border-gray-100">
+            <span class="text-sm text-gray-600">回答问题</span>
+            <span class="text-sm font-medium text-gray-900">{{ answeredQuestions }} 题</span>
+          </div>
+          <div class="flex justify-between items-center py-2 border-b border-gray-100">
+            <span class="text-sm text-gray-600">完成时间</span>
+            <span class="text-sm font-medium text-gray-900">{{ completionTime }}</span>
+          </div>
+          <div class="flex justify-between items-center py-2">
+            <span class="text-sm text-gray-600">面试状态</span>
+            <span class="text-sm font-medium text-green-600">已完成</span>
+          </div>
+        </div>
+      </div>
+
+      <!-- 后续步骤 -->
+      <div class="bg-blue-50 rounded-xl p-6 mb-8">
+        <h3 class="font-semibold text-blue-900 mb-3">后续步骤</h3>
+        <div class="space-y-2 text-sm text-blue-800">
+          <div class="flex items-center">
+            <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4" />
+            </svg>
+            面试结果将由AI系统自动分析
+          </div>
+          <div class="flex items-center">
+            <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4" />
+            </svg>
+            HR将在3个工作日内与您联系
+          </div>
+          <div class="flex items-center">
+            <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4" />
+            </svg>
+            请保持电话畅通,留意邮件通知
+          </div>
+        </div>
+      </div>
+
+      <!-- 操作按钮 -->
+      <div class="space-y-4">
+        <button
+          @click="restartInterview"
+          class="w-full bg-primary-600 hover:bg-primary-700 text-white font-semibold py-4 px-6 rounded-xl transition-colors duration-200 shadow-lg shadow-primary-200"
+        >
+          重新开始面试
+        </button>
+        
+        <button
+          @click="backToHome"
+          class="w-full bg-white hover:bg-gray-50 text-gray-700 font-semibold py-4 px-6 rounded-xl border border-gray-200 transition-colors duration-200"
+        >
+          返回首页
+        </button>
+      </div>
+
+      <!-- 联系信息 -->
+      <div class="mt-8 text-center">
+        <p class="text-sm text-gray-500 mb-2">如有疑问,请联系我们</p>
+        <div class="flex items-center justify-center space-x-4 text-sm">
+          <a href="mailto:hr@company.com" class="text-blue-600 hover:text-blue-700 transition-colors">
+            hr@company.com
+          </a>
+          <span class="text-gray-300">|</span>
+          <a href="tel:400-000-0000" class="text-blue-600 hover:text-blue-700 transition-colors">
+            400-000-0000
+          </a>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+
+const router = useRouter()
+
+// 模拟面试数据
+const interviewDuration = ref(0) // 面试持续时间(秒)
+const answeredQuestions = ref(4) // 回答的问题数
+const completionTime = ref('')
+
+// 计算面试时长
+const formatDuration = (seconds: number): string => {
+  const minutes = Math.floor(seconds / 60)
+  const remainingSeconds = seconds % 60
+  return `${minutes}分${remainingSeconds}秒`
+}
+
+// 重新开始面试
+const restartInterview = () => {
+  router.push('/device-check')
+}
+
+// 返回首页
+const backToHome = () => {
+  router.push('/')
+}
+
+// 组件挂载时设置数据
+onMounted(() => {
+  // 模拟面试持续时间(从15分钟倒计时中计算)
+  const totalTime = 15 * 60 // 15分钟总时长
+  const remainingTime = Math.floor(Math.random() * 300) + 200 // 随机剩余时间
+  interviewDuration.value = totalTime - remainingTime
+  
+  // 设置完成时间
+  completionTime.value = new Date().toLocaleString('zh-CN', {
+    year: 'numeric',
+    month: '2-digit',
+    day: '2-digit',
+    hour: '2-digit',
+    minute: '2-digit'
+  })
+})
+</script>

+ 237 - 0
src/views/DeviceCheckView.vue

@@ -0,0 +1,237 @@
+<template>
+  <div class="min-h-screen bg-gray-50 flex flex-col">
+    <!-- 顶部导航 -->
+    <div class="bg-white shadow-sm border-b border-gray-200">
+      <div class="max-w-2xl mx-auto px-4 py-4">
+        <div class="flex items-center justify-between">
+          <button
+            @click="goBack"
+            class="flex items-center text-gray-600 hover:text-gray-900 transition-colors"
+          >
+            <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
+            </svg>
+            返回
+          </button>
+          <h1 class="text-lg font-semibold text-gray-900">设备检测</h1>
+          <div class="w-16"></div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 主要内容 -->
+    <div class="flex-1 flex items-center justify-center px-4 py-8">
+      <div class="w-full max-w-md">
+        <!-- 检测步骤 -->
+        <div class="space-y-6">
+          
+          <!-- 权限检测 -->
+          <div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
+            <div class="flex items-center justify-between mb-4">
+              <div class="flex items-center">
+                <svg class="w-8 h-8 text-blue-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
+                        d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
+                </svg>
+                <div>
+                  <h3 class="font-semibold text-gray-900">设备权限</h3>
+                  <p class="text-sm text-gray-600">需要访问麦克风和摄像头</p>
+                </div>
+              </div>
+              <div class="flex items-center">
+                <div v-if="!permissionGranted" 
+                     class="w-3 h-3 bg-amber-400 rounded-full animate-pulse"></div>
+                <div v-else 
+                     class="w-3 h-3 bg-green-500 rounded-full"></div>
+              </div>
+            </div>
+            
+            <button
+              v-if="!permissionGranted"
+              @click="requestPermission"
+              class="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-4 rounded-lg transition-colors"
+            >
+              授权设备访问
+            </button>
+            <div v-else class="text-sm text-green-600 font-medium">
+              ✓ 权限已授权
+            </div>
+          </div>
+
+          <!-- 麦克风测试 -->
+          <div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
+            <div class="flex items-center justify-between mb-4">
+              <div class="flex items-center">
+                <svg class="w-8 h-8 text-blue-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
+                        d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" />
+                </svg>
+                <div>
+                  <h3 class="font-semibold text-gray-900">麦克风测试</h3>
+                  <p class="text-sm text-gray-600">检测音频输入是否正常</p>
+                </div>
+              </div>
+              <div class="flex items-center">
+                <div v-if="!microphoneOk" 
+                     class="w-3 h-3 bg-amber-400 rounded-full animate-pulse"></div>
+                <div v-else 
+                     class="w-3 h-3 bg-green-500 rounded-full"></div>
+              </div>
+            </div>
+            
+            <!-- 音量指示器 -->
+            <div class="mb-4" v-if="permissionGranted">
+              <div class="flex items-center space-x-1 mb-2">
+                <span class="text-sm text-gray-600">音量:</span>
+                <div class="flex space-x-1">
+                  <div v-for="i in 10" :key="i" 
+                       :class="['w-2 h-4 rounded-sm', i <= volumeLevel ? 'bg-green-500' : 'bg-gray-200']"></div>
+                </div>
+              </div>
+              <p class="text-xs text-gray-500">请说话测试麦克风</p>
+            </div>
+
+            <div v-if="microphoneOk" class="text-sm text-green-600 font-medium">
+              ✓ 麦克风工作正常
+            </div>
+          </div>
+
+          <!-- 网络检测 -->
+          <div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
+            <div class="flex items-center justify-between mb-4">
+              <div class="flex items-center">
+                <svg class="w-8 h-8 text-blue-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
+                        d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0" />
+                </svg>
+                <div>
+                  <h3 class="font-semibold text-gray-900">网络连接</h3>
+                  <p class="text-sm text-gray-600">检测网络状态和延迟</p>
+                </div>
+              </div>
+              <div class="flex items-center">
+                <div v-if="networkStatus === 'checking'"
+                     class="w-3 h-3 bg-amber-400 rounded-full animate-pulse"></div>
+                <div v-else-if="networkStatus === 'good'" 
+                     class="w-3 h-3 bg-green-500 rounded-full"></div>
+                <div v-else 
+                     class="w-3 h-3 bg-red-500 rounded-full"></div>
+              </div>
+            </div>
+            
+            <div class="text-sm font-medium">
+              <span v-if="networkStatus === 'checking'" class="text-amber-600">
+                检测中...
+              </span>
+              <span v-else-if="networkStatus === 'good'" class="text-green-600">
+                ✓ 网络良好
+              </span>
+              <span v-else class="text-red-600">
+                ✗ 网络异常
+              </span>
+            </div>
+          </div>
+        </div>
+
+        <!-- 完成按钮 -->
+        <div class="mt-8">
+          <button
+            v-if="allChecksCompleted"
+            @click="proceedToInterview"
+            class="w-full bg-green-600 hover:bg-green-700 text-white font-semibold py-4 px-6 rounded-xl transition-colors duration-200 shadow-lg"
+          >
+            检测完成,开始面试
+          </button>
+          <div v-else class="text-center text-gray-500 text-sm">
+            请完成所有设备检测步骤
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted, onUnmounted } from 'vue'
+import { useRouter } from 'vue-router'
+
+const router = useRouter()
+
+// 设备检测状态
+const permissionGranted = ref(false)
+const microphoneOk = ref(false)
+const networkStatus = ref<'checking' | 'good' | 'poor'>('checking')
+const volumeLevel = ref(0)
+
+// 定时器
+let volumeInterval: NodeJS.Timeout | null = null
+let networkInterval: NodeJS.Timeout | null = null
+
+// 计算属性
+const allChecksCompleted = computed(() => {
+  return permissionGranted.value && microphoneOk.value && networkStatus.value === 'good'
+})
+
+// 请求设备权限
+const requestPermission = async () => {
+  try {
+    // 模拟权限请求过程
+    await new Promise(resolve => setTimeout(resolve, 1000))
+    permissionGranted.value = true
+    
+    // 权限授权后开始麦克风测试
+    startMicrophoneTest()
+  } catch (error) {
+    console.error('Permission denied:', error)
+  }
+}
+
+// 开始麦克风测试
+const startMicrophoneTest = () => {
+  // 模拟音量波动
+  volumeInterval = setInterval(() => {
+    volumeLevel.value = Math.floor(Math.random() * 8) + 1
+  }, 200)
+  
+  // 3秒后确认麦克风正常
+  setTimeout(() => {
+    microphoneOk.value = true
+    if (volumeInterval) {
+      clearInterval(volumeInterval)
+      volumeInterval = null
+    }
+  }, 3000)
+}
+
+// 网络检测
+const checkNetwork = () => {
+  networkInterval = setTimeout(() => {
+    networkStatus.value = 'good'
+  }, 2000)
+}
+
+// 返回首页
+const goBack = () => {
+  router.push('/')
+}
+
+// 前往面试页面
+const proceedToInterview = () => {
+  router.push('/interview')
+}
+
+// 组件挂载时开始网络检测
+onMounted(() => {
+  checkNetwork()
+})
+
+// 组件卸载时清理定时器
+onUnmounted(() => {
+  if (volumeInterval) {
+    clearInterval(volumeInterval)
+  }
+  if (networkInterval) {
+    clearTimeout(networkInterval)
+  }
+})
+</script>

+ 489 - 0
src/views/InterviewView.vue

@@ -0,0 +1,489 @@
+<template>
+  <div class="min-h-screen bg-gray-900 flex">
+    <!-- 侧边栏 - 对话历史 -->
+    <div 
+      :class="[
+        'bg-gray-800 border-r border-gray-700 flex flex-col transition-all duration-300 ease-in-out',
+        sidebarCollapsed ? 'w-0 overflow-hidden' : 'w-80'
+      ]"
+    >
+      <!-- 侧边栏头部 - 固定 -->
+      <div class="p-4 border-b border-gray-700 flex items-center justify-between flex-shrink-0">
+        <div>
+          <h2 class="text-white font-semibold text-lg">对话记录</h2>
+          <p class="text-gray-400 text-sm">面试问答历史</p>
+        </div>
+        
+        <!-- 收起按钮 -->
+        <button
+          @click="toggleSidebar"
+          class="text-gray-400 hover:text-white transition-colors p-1 rounded"
+          title="收起对话记录"
+        >
+          <svg 
+            class="w-5 h-5"
+            fill="none" 
+            stroke="currentColor" 
+            viewBox="0 0 24 24"
+          >
+            <path 
+              stroke-linecap="round" 
+              stroke-linejoin="round" 
+              stroke-width="2" 
+              d="M11 19l-7-7 7-7m8 14l-7-7 7-7" 
+            />
+          </svg>
+        </button>
+      </div>
+      
+      <!-- 对话历史列表 - 可滚动区域 -->
+      <div class="flex-1 overflow-y-auto">
+        <div class="p-4 space-y-4">
+          <div v-for="(conversation, index) in conversationHistory" :key="index" class="space-y-3">
+            <!-- AI问题 -->
+            <div class="flex items-start space-x-3">
+              <div class="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center flex-shrink-0">
+                <svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
+                        d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
+                </svg>
+              </div>
+              <div class="flex-1">
+                <div class="bg-blue-600 text-white p-3 rounded-lg rounded-tl-none">
+                  <p class="text-sm">{{ conversation.question }}</p>
+                </div>
+                <p class="text-xs text-gray-500 mt-1">AI面试官</p>
+              </div>
+            </div>
+            
+            <!-- 用户回答 -->
+            <div v-if="conversation.answer" class="flex items-start space-x-3 justify-end">
+              <div class="flex-1 text-right">
+                <div class="bg-green-600 text-white p-3 rounded-lg rounded-tr-none inline-block max-w-xs">
+                  <p class="text-sm text-left">{{ conversation.answer }}</p>
+                </div>
+                <p class="text-xs text-gray-500 mt-1">您的回答</p>
+              </div>
+              <div class="w-8 h-8 bg-green-600 rounded-full flex items-center justify-center flex-shrink-0">
+                <svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
+                        d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
+                </svg>
+              </div>
+            </div>
+          </div>
+          
+          <!-- 空状态 -->
+          <div v-if="conversationHistory.length === 0" class="text-center text-gray-500 py-8">
+            <svg class="w-12 h-12 mx-auto mb-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
+                    d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
+            </svg>
+            <p class="text-sm">对话记录将在这里显示</p>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 侧边栏展开按钮 - 完全收起时显示 -->
+    <div 
+      v-if="sidebarCollapsed"
+      class="fixed top-1/2 left-0 transform -translate-y-1/2 z-40"
+    >
+      <button
+        @click="toggleSidebar"
+        class="bg-gray-800 bg-opacity-90 hover:bg-opacity-100 text-white p-3 rounded-r-lg transition-all backdrop-blur-sm shadow-lg border-r border-t border-b border-gray-600"
+        title="展开对话记录"
+      >
+        <div class="flex flex-col items-center space-y-1">
+          <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
+          </svg>
+          <!-- 对话数量指示器 -->
+          <div v-if="conversationHistory.length > 0" class="bg-blue-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
+            {{ conversationHistory.length }}
+          </div>
+        </div>
+      </button>
+    </div>
+
+    <!-- 主要内容区域 - 固定布局 -->
+    <div class="flex-1 flex flex-col h-screen overflow-hidden">
+      <!-- 返回按钮 - 固定在左上角 -->
+      <div class="absolute top-4 left-4 z-40">
+        <button
+          @click="showExitConfirm = true"
+          class="bg-black bg-opacity-50 hover:bg-opacity-70 text-white text-sm px-3 py-2 rounded-lg transition-all flex items-center backdrop-blur-sm"
+        >
+          <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
+          </svg>
+          返回
+        </button>
+      </div>
+
+      <!-- 状态栏 - 固定 -->
+      <div class="flex-shrink-0">
+        <StatusBar
+          :current-question="currentQuestionIndex + 1"
+          :total-questions="totalQuestions"
+          :countdown="remainingTime"
+          :network-status="'good'"
+        />
+      </div>
+
+      <!-- 数字人播放器区域 - 固定大小,不滚动 -->
+      <div class="flex-1 flex items-center justify-center p-4 relative overflow-hidden">
+        <DigitalHumanPlayer
+          :status="interviewState.status"
+          :lottie-json="mockLottieData"
+          :audio-stream="liveAudio"
+        />
+
+        <!-- 当前问题显示气泡 - 相对于播放器定位 -->
+        <div v-if="showCurrentQuestion && currentQuestion" 
+             class="absolute top-4 left-4 right-4 z-30">
+          <div class="bg-blue-600 bg-opacity-95 text-white p-4 rounded-lg max-w-md mx-auto shadow-lg backdrop-blur-sm">
+            <div class="flex items-start justify-between">
+              <p class="text-sm font-medium flex-1">{{ currentQuestion.questionText }}</p>
+              <button @click="hideCurrentQuestion" class="ml-2 text-blue-200 hover:text-white">
+                <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
+                </svg>
+              </button>
+            </div>
+          </div>
+        </div>
+
+        <!-- 实时转录显示气泡 - 相对于播放器定位 -->
+        <div v-if="currentTranscript" 
+             class="absolute bottom-4 left-4 right-4 z-30">
+          <div class="bg-green-600 bg-opacity-95 text-white p-3 rounded-lg max-w-md mx-auto shadow-lg backdrop-blur-sm">
+            <div class="flex items-center">
+              <div class="w-2 h-2 bg-green-300 rounded-full animate-pulse mr-2"></div>
+              <p class="text-sm">{{ currentTranscript }}</p>
+            </div>
+          </div>
+        </div>
+
+        <!-- 实时AI文本显示气泡 - 相对于播放器定位 -->
+        <div v-if="liveText" 
+             class="absolute bottom-16 left-4 right-4 z-30">
+          <div class="bg-blue-600 bg-opacity-95 text-white p-3 rounded-lg max-w-md mx-auto shadow-lg backdrop-blur-sm">
+            <div class="flex items-center">
+              <div class="w-2 h-2 bg-blue-300 rounded-full animate-pulse mr-2"></div>
+              <p class="text-sm">{{ liveText }}</p>
+            </div>
+          </div>
+        </div>
+
+        <!-- 状态提示气泡 - 相对于播放器定位 -->
+        <div v-if="interviewState.status === 'THINKING'" 
+             class="absolute bottom-4 left-4 right-4 z-30">
+          <div class="bg-amber-600 bg-opacity-95 text-white p-3 rounded-lg max-w-md mx-auto text-center shadow-lg backdrop-blur-sm">
+            <div class="flex items-center justify-center">
+              <div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></div>
+              <p class="text-sm">AI正在思考中...</p>
+            </div>
+          </div>
+        </div>
+
+        <div v-if="interviewState.status === 'GENERATING_LIP_SYNC'" 
+             class="absolute bottom-4 left-4 right-4 z-30">
+          <div class="bg-purple-600 bg-opacity-95 text-white p-3 rounded-lg max-w-md mx-auto text-center shadow-lg backdrop-blur-sm">
+            <div class="flex items-center justify-center">
+              <div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></div>
+              <p class="text-sm">正在生成回复...</p>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 底部控制区域 - 固定在底部 -->
+      <div class="flex-shrink-0 relative">
+        <!-- 语音输入处理器 - 固定位置 -->
+        <VoiceInputHandler
+          v-if="interviewState.status === 'LISTENING'"
+          @on-transcript-update="handleTranscriptUpdate"
+          @on-speech-end="handleSpeechEnd"
+        />
+
+        <!-- 切换到文本输入的按钮 - 固定位置 -->
+        <div v-if="interviewState.status === 'LISTENING'" 
+             class="absolute bottom-4 right-4 z-30">
+          <button
+            @click="showFallbackInput = true"
+            class="bg-gray-800 hover:bg-gray-700 text-white text-sm px-3 py-2 rounded-lg transition-colors"
+          >
+            文字输入
+          </button>
+        </div>
+      </div>
+
+      <!-- 降级文本输入框 - 覆盖整个右侧区域 -->
+      <FallbackInput
+        v-if="showFallbackInput"
+        @submit-text="handleTextSubmit"
+        @close="showFallbackInput = false"
+      />
+    </div>
+
+    <!-- 退出确认弹窗 -->
+    <div v-if="showExitConfirm" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 px-4">
+      <div class="bg-white rounded-xl p-6 max-w-sm w-full mx-4">
+        <div class="text-center mb-6">
+          <div class="w-12 h-12 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-4">
+            <svg class="w-6 h-6 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
+                    d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
+            </svg>
+          </div>
+          <h3 class="text-lg font-semibold text-gray-900 mb-2">确认退出面试</h3>
+          <p class="text-sm text-gray-600">退出后当前面试进度将丢失,您确定要离开吗?</p>
+        </div>
+        
+        <div class="flex space-x-3">
+          <button
+            @click="showExitConfirm = false"
+            class="flex-1 bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium py-3 px-4 rounded-lg transition-colors"
+          >
+            继续面试
+          </button>
+          <button
+            @click="confirmExit"
+            class="flex-1 bg-red-600 hover:bg-red-700 text-white font-medium py-3 px-4 rounded-lg transition-colors"
+          >
+            确认退出
+          </button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted, watch, provide } from 'vue'
+import { useRouter } from 'vue-router'
+import { useInterview } from '../composables/useInterview'
+import StatusBar from '../components/StatusBar.vue'
+import DigitalHumanPlayer from '../components/DigitalHumanPlayer.vue'
+import VoiceInputHandler from '../components/VoiceInputHandler.vue'
+import FallbackInput from '../components/FallbackInput.vue'
+
+const router = useRouter()
+
+// 使用面试逻辑组合函数
+const {
+  interviewState,
+  currentQuestion,
+  currentQuestionIndex,
+  totalQuestions,
+  remainingTime,
+  formattedTime,
+  isTimerRunning,
+  mockLottieData,
+  liveAudio,
+  liveText,
+  startInterview,
+  processUserAnswer,
+  resetInterview,
+  isConnected,
+  send
+} = useInterview()
+
+// 为子组件提供WebSocket发送方法
+provide('sendToDigitalHuman', send)
+
+// 本地状态
+const currentTranscript = ref('')
+const showFallbackInput = ref(false)
+const showExitConfirm = ref(false)
+const showCurrentQuestion = ref(false)
+const sidebarCollapsed = ref(true) // 默认收起侧边栏
+
+// 对话历史记录
+const conversationHistory = ref<Array<{
+  question: string
+  answer?: string
+  timestamp: Date
+}>>([])
+
+// 定时器
+let questionTimer: NodeJS.Timeout | null = null
+let transcriptTimer: NodeJS.Timeout | null = null
+
+/**
+ * 切换侧边栏展开/收起状态
+ */
+const toggleSidebar = () => {
+  sidebarCollapsed.value = !sidebarCollapsed.value
+  console.log('📱 Sidebar toggled:', sidebarCollapsed.value ? 'collapsed' : 'expanded')
+}
+
+/**
+ * 处理实时转录更新
+ */
+const handleTranscriptUpdate = (transcript: string) => {
+  currentTranscript.value = transcript
+  console.log('📝 Transcript update:', transcript)
+  
+  // 清除之前的定时器
+  if (transcriptTimer) {
+    clearTimeout(transcriptTimer)
+  }
+  
+  // 5秒后自动隐藏转录文本
+  transcriptTimer = setTimeout(() => {
+    currentTranscript.value = ''
+  }, 5000)
+}
+
+/**
+ * 处理语音识别结束
+ */
+const handleSpeechEnd = (finalTranscript: string) => {
+  console.log('🎤 Speech ended:', finalTranscript)
+  currentTranscript.value = ''
+  
+  // 清除转录定时器
+  if (transcriptTimer) {
+    clearTimeout(transcriptTimer)
+  }
+  
+  // 更新对话历史
+  updateConversationHistory(finalTranscript)
+  
+  processUserAnswer(finalTranscript)
+}
+
+/**
+ * 处理文本输入提交
+ */
+const handleTextSubmit = (text: string) => {
+  console.log('⌨️ Text submitted:', text)
+  showFallbackInput.value = false
+  
+  // 更新对话历史
+  updateConversationHistory(text)
+  
+  processUserAnswer(text)
+}
+
+/**
+ * 更新对话历史记录
+ */
+const updateConversationHistory = (answer: string) => {
+  if (conversationHistory.value.length > 0) {
+    const lastConversation = conversationHistory.value[conversationHistory.value.length - 1]
+    if (!lastConversation.answer) {
+      lastConversation.answer = answer
+    }
+  }
+}
+
+/**
+ * 添加新问题到对话历史
+ */
+const addQuestionToHistory = (question: string) => {
+  conversationHistory.value.push({
+    question,
+    timestamp: new Date()
+  })
+}
+
+/**
+ * 显示当前问题气泡
+ */
+const showQuestionBubble = () => {
+  showCurrentQuestion.value = true
+  
+  // 清除之前的定时器
+  if (questionTimer) {
+    clearTimeout(questionTimer)
+  }
+  
+  // 8秒后自动隐藏问题气泡
+  questionTimer = setTimeout(() => {
+    showCurrentQuestion.value = false
+  }, 8000)
+}
+
+/**
+ * 手动隐藏当前问题气泡
+ */
+const hideCurrentQuestion = () => {
+  showCurrentQuestion.value = false
+  if (questionTimer) {
+    clearTimeout(questionTimer)
+  }
+}
+
+/**
+ * 确认退出面试
+ */
+const confirmExit = () => {
+  console.log('🚪 User confirmed exit from interview')
+  resetInterview()
+  router.push('/')
+}
+
+// 监听面试状态变化
+watch(() => interviewState.status, (newStatus) => {
+  console.log('📊 Interview status changed to:', newStatus)
+  
+  // 当开始提问时,显示问题气泡并添加到历史记录
+  if (newStatus === 'ASKING' && currentQuestion.value) {
+    showQuestionBubble()
+    addQuestionToHistory(currentQuestion.value.questionText)
+  }
+  
+  // 当面试完成时,导航到完成页面
+  if (newStatus === 'COMPLETED') {
+    setTimeout(() => {
+      router.push('/completion')
+    }, 1000)
+  }
+})
+
+// 监听剩余时间,当时间到时自动结束面试
+watch(() => remainingTime.value, (newTime) => {
+  if (newTime === 0) {
+    console.log('⏰ Time is up! Ending interview...')
+    router.push('/completion')
+  }
+})
+
+// 组件挂载时开始面试
+onMounted(() => {
+  console.log('🚀 InterviewView mounted, starting interview...')
+  startInterview()
+})
+
+// 组件卸载时重置面试状态和清理定时器
+onUnmounted(() => {
+  console.log('🔄 InterviewView unmounted, resetting interview...')
+  resetInterview()
+  
+  // 清理所有定时器
+  if (questionTimer) {
+    clearTimeout(questionTimer)
+  }
+  if (transcriptTimer) {
+    clearTimeout(transcriptTimer)
+  }
+})
+
+// 处理页面离开前的清理
+const handleBeforeUnload = () => {
+  resetInterview()
+}
+
+// 监听页面离开事件
+onMounted(() => {
+  window.addEventListener('beforeunload', handleBeforeUnload)
+})
+
+onUnmounted(() => {
+  window.removeEventListener('beforeunload', handleBeforeUnload)
+})
+</script>

+ 213 - 0
src/views/TestView.vue

@@ -0,0 +1,213 @@
+<template>
+  <div class="test-container">
+    <h1>WebSocket连接测试</h1>
+    
+    <div class="status-section">
+      <h2>连接状态</h2>
+      <p>状态: <span :class="statusClass">{{ connectionStatus }}</span></p>
+      <p>是否连接: {{ isConnected ? '是' : '否' }}</p>
+      <p v-if="lastError" class="error">错误: {{ lastError }}</p>
+    </div>
+    
+    <div class="controls">
+      <button @click="testConnect" :disabled="isConnected">连接</button>
+      <button @click="testDisconnect" :disabled="!isConnected">断开</button>
+      <button @click="testSendMessage" :disabled="!isConnected">发送测试消息</button>
+    </div>
+    
+    <div class="messages-section">
+      <h2>消息日志</h2>
+      <div class="messages" ref="messagesContainer">
+        <div v-for="(message, index) in messages" :key="index" class="message">
+          <span class="timestamp">{{ message.timestamp }}</span>
+          <span class="type" :class="message.type">{{ message.type }}</span>
+          <span class="content">{{ message.content }}</span>
+        </div>
+      </div>
+      <button @click="clearMessages">清空日志</button>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, nextTick } from 'vue'
+import { useDigitalHuman } from '../composables/useDigitalHuman'
+
+interface Message {
+  timestamp: string
+  type: 'sent' | 'received' | 'error' | 'info'
+  content: string
+}
+
+const messages = ref<Message[]>([])
+const messagesContainer = ref<HTMLElement>()
+
+const addMessage = (type: Message['type'], content: string) => {
+  messages.value.push({
+    timestamp: new Date().toLocaleTimeString(),
+    type,
+    content
+  })
+  
+  nextTick(() => {
+    if (messagesContainer.value) {
+      messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
+    }
+  })
+}
+
+const handleServerMessage = (data: any) => {
+  addMessage('received', JSON.stringify(data, null, 2))
+}
+
+const { isConnected, connect, disconnect, send, connectionStatus, lastError } = useDigitalHuman(handleServerMessage)
+
+const statusClass = computed(() => {
+  switch (connectionStatus.value) {
+    case 'connected': return 'status-connected'
+    case 'connecting': return 'status-connecting'
+    case 'reconnecting': return 'status-reconnecting'
+    case 'error': return 'status-error'
+    default: return 'status-disconnected'
+  }
+})
+
+const testConnect = () => {
+  addMessage('info', '尝试连接WebSocket...')
+  connect()
+}
+
+const testDisconnect = () => {
+  addMessage('info', '断开WebSocket连接...')
+  disconnect()
+}
+
+const testSendMessage = () => {
+  const testMessage = {
+    header: {
+      app_id: '379db9f6',
+      uid: `test_user_${Date.now()}`
+    },
+    parameter: {
+      chat: {
+        domain: "generalv3.5",
+        temperature: 0.5,
+        max_tokens: 2048
+      }
+    },
+    payload: {
+      message: {
+        text: [
+          {
+            role: "user",
+            content: "你好,这是一个测试消息"
+          }
+        ]
+      }
+    }
+  }
+  
+  addMessage('sent', JSON.stringify(testMessage, null, 2))
+  send(testMessage)
+}
+
+const clearMessages = () => {
+  messages.value = []
+}
+</script>
+
+<style scoped>
+.test-container {
+  max-width: 1200px;
+  margin: 0 auto;
+  padding: 20px;
+  font-family: Arial, sans-serif;
+}
+
+.status-section {
+  background: #f5f5f5;
+  padding: 15px;
+  border-radius: 8px;
+  margin-bottom: 20px;
+}
+
+.status-connected { color: #4CAF50; font-weight: bold; }
+.status-connecting { color: #FF9800; font-weight: bold; }
+.status-reconnecting { color: #2196F3; font-weight: bold; }
+.status-error { color: #F44336; font-weight: bold; }
+.status-disconnected { color: #9E9E9E; font-weight: bold; }
+
+.error {
+  color: #F44336;
+  font-weight: bold;
+}
+
+.controls {
+  margin-bottom: 20px;
+}
+
+.controls button {
+  margin-right: 10px;
+  padding: 10px 20px;
+  border: none;
+  border-radius: 4px;
+  background: #2196F3;
+  color: white;
+  cursor: pointer;
+}
+
+.controls button:disabled {
+  background: #ccc;
+  cursor: not-allowed;
+}
+
+.controls button:hover:not(:disabled) {
+  background: #1976D2;
+}
+
+.messages-section {
+  border: 1px solid #ddd;
+  border-radius: 8px;
+  padding: 15px;
+}
+
+.messages {
+  height: 400px;
+  overflow-y: auto;
+  border: 1px solid #eee;
+  padding: 10px;
+  margin-bottom: 10px;
+  background: #fafafa;
+  font-family: 'Courier New', monospace;
+  font-size: 12px;
+}
+
+.message {
+  margin-bottom: 8px;
+  padding: 5px;
+  border-radius: 4px;
+}
+
+.timestamp {
+  color: #666;
+  margin-right: 10px;
+}
+
+.type {
+  font-weight: bold;
+  margin-right: 10px;
+  padding: 2px 6px;
+  border-radius: 3px;
+  font-size: 10px;
+}
+
+.type.sent { background: #E3F2FD; color: #1976D2; }
+.type.received { background: #E8F5E8; color: #388E3C; }
+.type.error { background: #FFEBEE; color: #D32F2F; }
+.type.info { background: #FFF3E0; color: #F57C00; }
+
+.content {
+  white-space: pre-wrap;
+  word-break: break-all;
+}
+</style>

+ 93 - 0
src/views/WelcomeView.vue

@@ -0,0 +1,93 @@
+<template>
+  <div class="min-h-screen bg-gradient-to-br from-blue-50 via-white to-blue-50 flex flex-col items-center justify-center px-4">
+    <!-- 主要内容区域 -->
+    <div class="w-full max-w-md mx-auto text-center">
+      
+      <!-- 公司Logo区域 -->
+      <div class="mb-8">
+        <div class="w-20 h-20 mx-auto mb-4 bg-primary-100 rounded-full flex items-center justify-center">
+          <svg class="w-10 h-10 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
+                  d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
+          </svg>
+        </div>
+        <h1 class="text-3xl font-bold text-gray-900 mb-2">AI数字人面试</h1>
+        <p class="text-gray-600 mb-2">智能化面试体验</p>
+        <div class="flex items-center justify-center text-sm text-gray-500">
+          <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
+                  d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
+          </svg>
+          预计时长: 约15分钟
+        </div>
+      </div>
+
+      <!-- 功能介绍卡片 -->
+      <div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 mb-8">
+        <h2 class="text-lg font-semibold text-gray-900 mb-4">面试流程</h2>
+        <div class="space-y-3">
+          <div class="flex items-center text-sm text-gray-600">
+            <div class="w-6 h-6 bg-blue-100 rounded-full flex items-center justify-center mr-3">
+              <span class="text-xs font-semibold text-blue-600">1</span>
+            </div>
+            设备检测与权限确认
+          </div>
+          <div class="flex items-center text-sm text-gray-600">
+            <div class="w-6 h-6 bg-blue-100 rounded-full flex items-center justify-center mr-3">
+              <span class="text-xs font-semibold text-blue-600">2</span>
+            </div>
+            AI数字人提问与互动
+          </div>
+          <div class="flex items-center text-sm text-gray-600">
+            <div class="w-6 h-6 bg-blue-100 rounded-full flex items-center justify-center mr-3">
+              <span class="text-xs font-semibold text-blue-600">3</span>
+            </div>
+            语音识别与智能分析
+          </div>
+          <div class="flex items-center text-sm text-gray-600">
+            <div class="w-6 h-6 bg-blue-100 rounded-full flex items-center justify-center mr-3">
+              <span class="text-xs font-semibold text-blue-600">4</span>
+            </div>
+            面试结果与反馈
+          </div>
+        </div>
+      </div>
+
+      <!-- 操作按钮 -->
+      <div class="space-y-4">
+        <button
+          @click="startInterview"
+          class="w-full bg-primary-600 hover:bg-primary-700 text-white font-semibold py-4 px-6 rounded-xl transition-colors duration-200 shadow-lg shadow-primary-200 hover:shadow-primary-300"
+        >
+          开始面试
+        </button>
+        
+        <button
+          @click="checkDevice"
+          class="w-full bg-white hover:bg-gray-50 text-gray-700 font-semibold py-4 px-6 rounded-xl border border-gray-200 transition-colors duration-200"
+        >
+          设备检测
+        </button>
+      </div>
+
+      <!-- 底部提示 -->
+      <div class="mt-8 text-xs text-gray-500">
+        <p>请确保网络连接稳定,并在安静的环境中进行面试</p>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useRouter } from 'vue-router'
+
+const router = useRouter()
+
+const startInterview = () => {
+  router.push('/interview')
+}
+
+const checkDevice = () => {
+  router.push('/device-check')
+}
+</script>

+ 1 - 0
src/vite-env.d.ts

@@ -0,0 +1 @@
+/// <reference types="vite/client" />

+ 48 - 0
tailwind.config.js

@@ -0,0 +1,48 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+  content: [
+    "./index.html",
+    "./src/**/*.{vue,js,ts,jsx,tsx}",
+  ],
+  theme: {
+    extend: {
+      colors: {
+        primary: {
+          50: '#eff6ff',
+          100: '#dbeafe',
+          500: '#3b82f6',
+          600: '#2563eb',
+          700: '#1d4ed8',
+        },
+        success: {
+          50: '#f0fdf4',
+          100: '#dcfce7',
+          500: '#22c55e',
+          600: '#16a34a',
+        },
+        warning: {
+          50: '#fffbeb',
+          100: '#fef3c7',
+          500: '#f59e0b',
+          600: '#d97706',
+        },
+        error: {
+          50: '#fef2f2',
+          100: '#fee2e2',
+          500: '#ef4444',
+          600: '#dc2626',
+        }
+      },
+      spacing: {
+        '18': '4.5rem',
+        '88': '22rem',
+        '128': '32rem',
+      },
+      animation: {
+        'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
+        'bounce-slow': 'bounce 2s infinite',
+      }
+    },
+  },
+  plugins: [],
+}

+ 24 - 0
tsconfig.app.json

@@ -0,0 +1,24 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "useDefineForClassFields": true,
+    "module": "ESNext",
+    "lib": ["ES2020", "DOM", "DOM.Iterable"],
+    "skipLibCheck": true,
+
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "isolatedModules": true,
+    "moduleDetection": "force",
+    "noEmit": true,
+    "jsx": "preserve",
+
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true
+  },
+  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
+}

+ 7 - 0
tsconfig.json

@@ -0,0 +1,7 @@
+{
+  "files": [],
+  "references": [
+    { "path": "./tsconfig.app.json" },
+    { "path": "./tsconfig.node.json" }
+  ]
+}

+ 22 - 0
tsconfig.node.json

@@ -0,0 +1,22 @@
+{
+  "compilerOptions": {
+    "target": "ES2022",
+    "lib": ["ES2023"],
+    "module": "ESNext",
+    "skipLibCheck": true,
+
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "isolatedModules": true,
+    "moduleDetection": "force",
+    "noEmit": true,
+
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true
+  },
+  "include": ["vite.config.ts"]
+}

+ 7 - 0
vite.config.ts

@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [vue()],
+})