├── backend ├── app │ ├── __init__.py │ ├── routers │ │ ├── __init__.py │ │ ├── auth.py │ │ └── sessions.py │ ├── database.py │ ├── schemas.py │ ├── models.py │ └── auth.py ├── requirements.txt ├── init.sql ├── run_server.py ├── .env.example ├── Dockerfile ├── .dockerignore ├── setup.sh ├── main.py ├── test_db.py ├── fix_windows_encoding.py ├── init_default_user.py ├── WINDOWS_TROUBLESHOOT.md ├── run_server_windows.py └── test_connection.py ├── frontend ├── src │ ├── vite-env.d.ts │ ├── api │ │ ├── index.ts │ │ ├── auth.ts │ │ ├── sessions.ts │ │ ├── request.ts │ │ ├── utils │ │ │ └── xfyunRtasr │ │ │ │ └── index.esm.js │ │ ├── deepseek.ts │ │ └── xfyunRtasr.ts │ ├── main.tsx │ ├── utils │ │ ├── voiceIdentifier.ts │ │ └── questionDetection.ts │ ├── App.css │ ├── App.tsx │ ├── store │ │ ├── userConfigStore.ts │ │ ├── xfyunConfigStore.ts │ │ ├── apiConfigStore.ts │ │ ├── authStore.ts │ │ └── interviewStore.ts │ ├── index.css │ ├── layouts │ │ └── MainLayout.tsx │ ├── assets │ │ └── react.svg │ └── pages │ │ ├── InterviewNew.tsx │ │ ├── Settings.tsx │ │ └── InterviewMeeting.tsx ├── public │ ├── image.png │ ├── vite.svg │ └── xfyunRtasr │ │ ├── processor.worker.js │ │ └── processor.worklet.js ├── tsconfig.json ├── .env.example ├── vite.config.ts ├── index.html ├── Dockerfile.dev ├── tsconfig.node.json ├── eslint.config.js ├── tsconfig.app.json ├── setup.sh ├── package.json └── README.md ├── public ├── turorial2.png └── tutorial1.png ├── memory-bank ├── patterns-style-guide.md ├── tasks-progress.md └── project-context.md ├── scripts ├── docker-stop.sh ├── docker-restart.sh ├── docker-logs.sh ├── dev-start.sh ├── docker-start.sh └── start-frontend.sh ├── fix-docker-permissions.sh ├── .gitignore ├── docker-compose.dev.yml ├── test-docker.sh ├── docker-compose.yml ├── docker-compose.full.yml ├── DOCKER_SETUP.md ├── README.md └── CLAUDE.md /backend/app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/routers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/turorial2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonJarvan/interview-helper/HEAD/public/turorial2.png -------------------------------------------------------------------------------- /public/tutorial1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonJarvan/interview-helper/HEAD/public/tutorial1.png -------------------------------------------------------------------------------- /frontend/public/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonJarvan/interview-helper/HEAD/frontend/public/image.png -------------------------------------------------------------------------------- /frontend/src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './deepseek' 2 | export * from './auth' 3 | export * from './sessions' 4 | export { default as request } from './request' -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /frontend/.env.example: -------------------------------------------------------------------------------- 1 | # API Base URL - 后端服务地址 2 | VITE_API_BASE_URL=http://localhost:9000 3 | 4 | # 科大讯飞语音转写API (可选,建议在应用内设置) 5 | # VITE_XFYUN_APPID=your_xfyun_app_id 6 | # VITE_XFYUN_API_KEY=your_xfyun_api_key 7 | 8 | # 应用配置 9 | # VITE_APP_TITLE=AI Interview Helper -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.tsx' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.104.1 2 | uvicorn[standard]==0.24.0 3 | sqlalchemy==2.0.23 4 | alembic==1.12.1 5 | psycopg2-binary==2.9.9 6 | pydantic==2.5.0 7 | python-jose[cryptography]==3.3.0 8 | passlib[bcrypt]==1.7.4 9 | python-multipart==0.0.6 10 | redis==5.0.1 11 | python-dotenv==1.0.0 -------------------------------------------------------------------------------- /backend/init.sql: -------------------------------------------------------------------------------- 1 | -- AI Interview Helper Database Initialization 2 | -- 初始化数据库脚本 3 | 4 | -- 确保数据库使用UTF8编码 5 | SET client_encoding = 'UTF8'; 6 | 7 | -- 创建扩展(如果需要) 8 | -- CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 9 | 10 | -- 数据库初始化完成 11 | SELECT 'AI Interview Helper Database Initialized' AS status; -------------------------------------------------------------------------------- /frontend/src/utils/voiceIdentifier.ts: -------------------------------------------------------------------------------- 1 | // 音量检测工具 2 | export function isAudioActive(frameBuffer: ArrayBuffer, threshold = 0.02): boolean { 3 | const arr = new Int16Array(frameBuffer) 4 | let max = 0 5 | for (let i = 0; i < arr.length; i++) { 6 | max = Math.max(max, Math.abs(arr[i]) / 32768) 7 | } 8 | return max > threshold 9 | } -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import { resolve } from 'path' 4 | 5 | // https://vite.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react()], 8 | resolve: { 9 | alias: { 10 | '@': resolve(__dirname, 'src'), 11 | }, 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /memory-bank/patterns-style-guide.md: -------------------------------------------------------------------------------- 1 | # 系统模式与风格指南 2 | 3 | ## 架构模式 4 | 5 | ### 前后端分离架构 6 | - **前端**: React单页应用 7 | - **后端**: FastAPI RESTful服务 8 | - **通信**: 通过API进行数据交换 9 | - **实时通信**: WebSocket协议 10 | 11 | ### 数据流模式 12 | - **单向数据流**: 保证数据流向可预测 13 | - **状态集中管理**: 使用Zustand进行状态管理 14 | - **组件间通信**: 通过状态共享数据 15 | - **实时数据流**: WebSocket实时数据处理 -------------------------------------------------------------------------------- /scripts/docker-stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # AI Interview Helper Docker停止脚本 4 | 5 | echo "🛑 停止AI面试助手Docker服务..." 6 | 7 | if [ ! -f "docker-compose.yml" ]; then 8 | echo "❌ 未找到docker-compose.yml文件,请在项目根目录执行此脚本" 9 | exit 1 10 | fi 11 | 12 | # 停止服务 13 | if command -v docker-compose &> /dev/null; then 14 | docker-compose down 15 | else 16 | docker compose down 17 | fi 18 | 19 | echo "✅ 服务已停止" -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /backend/run_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Run the FastAPI server 4 | """ 5 | 6 | import uvicorn 7 | import os 8 | 9 | if __name__ == "__main__": 10 | # 检查是否在Docker容器中运行 11 | is_docker = os.path.exists('/.dockerenv') 12 | 13 | uvicorn.run( 14 | "main:app", 15 | host="0.0.0.0", 16 | port=9000, 17 | reload=not is_docker, # Docker中禁用自动重载以提高性能 18 | log_level="info", 19 | access_log=True 20 | ) -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | # Database configuration 2 | DATABASE_URL=postgresql://postgres:your_password@localhost:5432/ai_interview_helper 3 | REDIS_URL=redis://localhost:6379 4 | 5 | # JWT configuration - 请修改为强密码 6 | SECRET_KEY=your-secret-key-change-this-in-production-use-strong-password 7 | ALGORITHM=HS256 8 | ACCESS_TOKEN_EXPIRE_MINUTES=30 9 | 10 | # CORS configuration 11 | CORS_ORIGINS=["http://localhost:5173", "http://localhost:3000"] 12 | 13 | # 可选:调试模式 14 | DEBUG=False 15 | 16 | # 可选:日志级别 17 | LOG_LEVEL=INFO -------------------------------------------------------------------------------- /scripts/docker-restart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # AI Interview Helper Docker重启脚本 4 | 5 | echo "🔄 重启AI面试助手Docker服务..." 6 | 7 | if [ ! -f "docker-compose.yml" ]; then 8 | echo "❌ 未找到docker-compose.yml文件,请在项目根目录执行此脚本" 9 | exit 1 10 | fi 11 | 12 | # 重启服务 13 | if command -v docker-compose &> /dev/null; then 14 | docker-compose restart 15 | else 16 | docker compose restart 17 | fi 18 | 19 | echo "✅ 服务已重启" 20 | 21 | # 显示状态 22 | echo "📊 服务状态:" 23 | if command -v docker-compose &> /dev/null; then 24 | docker-compose ps 25 | else 26 | docker compose ps 27 | fi -------------------------------------------------------------------------------- /backend/app/database.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import sessionmaker 4 | import os 5 | from dotenv import load_dotenv 6 | 7 | load_dotenv() 8 | 9 | DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:yourpassword@localhost:5432/ai_interview_helper") 10 | 11 | engine = create_engine(DATABASE_URL) 12 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 13 | 14 | Base = declarative_base() 15 | 16 | def get_db(): 17 | db = SessionLocal() 18 | try: 19 | yield db 20 | finally: 21 | db.close() -------------------------------------------------------------------------------- /scripts/docker-logs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # AI Interview Helper Docker日志查看脚本 4 | 5 | echo "📋 AI面试助手服务日志..." 6 | 7 | if [ ! -f "docker-compose.yml" ]; then 8 | echo "❌ 未找到docker-compose.yml文件,请在项目根目录执行此脚本" 9 | exit 1 10 | fi 11 | 12 | # 显示所有服务日志 13 | if [ "$1" == "" ]; then 14 | echo "🔍 显示所有服务日志 (按Ctrl+C退出)..." 15 | if command -v docker-compose &> /dev/null; then 16 | docker-compose logs -f 17 | else 18 | docker compose logs -f 19 | fi 20 | else 21 | echo "🔍 显示 $1 服务日志 (按Ctrl+C退出)..." 22 | if command -v docker-compose &> /dev/null; then 23 | docker-compose logs -f $1 24 | else 25 | docker compose logs -f $1 26 | fi 27 | fi -------------------------------------------------------------------------------- /frontend/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | # AI Interview Helper Frontend Dockerfile (Development) 2 | FROM node:18-alpine 3 | 4 | # 设置工作目录 5 | WORKDIR /app 6 | 7 | # 设置环境变量 8 | ENV NODE_ENV=development 9 | 10 | # 安装系统依赖 11 | RUN apk add --no-cache curl 12 | 13 | # 复制package文件 14 | COPY package*.json ./ 15 | 16 | # 安装依赖 17 | RUN npm install 18 | 19 | # 复制源代码 20 | COPY . . 21 | 22 | # 创建非root用户 23 | RUN addgroup -g 1001 -S nodejs 24 | RUN adduser -S nextjs -u 1001 25 | RUN chown -R nextjs:nodejs /app 26 | USER nextjs 27 | 28 | # 暴露端口 29 | EXPOSE 5173 30 | 31 | # 健康检查 32 | HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ 33 | CMD curl -f http://localhost:5173 || exit 1 34 | 35 | # 启动开发服务器 36 | CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "verbatimModuleSyntax": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "erasableSyntaxOnly": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noUncheckedSideEffectImports": true 23 | }, 24 | "include": ["vite.config.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /frontend/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # AI Interview Helper Backend Dockerfile 2 | FROM python:3.11-slim 3 | 4 | # 设置工作目录 5 | WORKDIR /app 6 | 7 | # 设置环境变量 8 | ENV PYTHONDONTWRITEBYTECODE=1 9 | ENV PYTHONUNBUFFERED=1 10 | ENV PYTHONIOENCODING=utf-8 11 | 12 | # 安装系统依赖 13 | RUN apt-get update && apt-get install -y \ 14 | gcc \ 15 | curl \ 16 | postgresql-client \ 17 | && rm -rf /var/lib/apt/lists/* 18 | 19 | # 复制依赖文件 20 | COPY requirements.txt . 21 | 22 | # 安装Python依赖 23 | RUN pip install --no-cache-dir -r requirements.txt 24 | 25 | # 复制应用代码 26 | COPY . . 27 | 28 | # 创建非root用户 29 | RUN useradd --create-home --shell /bin/bash app \ 30 | && chown -R app:app /app 31 | USER app 32 | 33 | # 健康检查 34 | HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ 35 | CMD curl -f http://localhost:9000/health || exit 1 36 | 37 | # 暴露端口 38 | EXPOSE 9000 39 | 40 | # 启动命令 41 | CMD ["python", "run_server.py"] -------------------------------------------------------------------------------- /frontend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "verbatimModuleSyntax": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "erasableSyntaxOnly": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "noUncheckedSideEffectImports": true, 25 | 26 | "baseUrl": "./", 27 | "paths": { 28 | "@/*": ["src/*"] 29 | } 30 | }, 31 | "include": ["src"] 32 | } 33 | -------------------------------------------------------------------------------- /frontend/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # AI Interview Helper Frontend Setup Script 4 | # 前端环境安装和配置脚本 5 | 6 | set -e 7 | 8 | echo "🚀 开始设置AI面试助手前端环境..." 9 | 10 | # 检查Node.js版本 11 | echo "📋 检查Node.js版本..." 12 | node --version 13 | npm --version 14 | 15 | # 检查和配置环境变量 16 | echo "🔧 检查环境变量配置..." 17 | if [ ! -f .env ]; then 18 | echo "⚠️ 未找到.env文件,正在复制示例配置..." 19 | cp .env.example .env 20 | echo "✅ 已创建.env文件" 21 | echo "📝 注意:科大讯飞API密钥建议在应用内设置页面配置" 22 | fi 23 | 24 | # 安装依赖 25 | echo "📦 安装前端依赖..." 26 | npm install 27 | 28 | echo "🎉 前端环境设置完成!" 29 | echo "" 30 | echo "📝 使用说明:" 31 | echo " 1. 启动开发服务器: npm run dev" 32 | echo " 2. 构建生产版本: npm run build" 33 | echo " 3. 访问地址: http://localhost:5173" 34 | echo "" 35 | echo "🔧 环境配置:" 36 | echo " - Node.js项目,使用Vite构建" 37 | echo " - 后端API地址: http://localhost:9000" 38 | echo " - 默认登录: test / test1234" 39 | echo "" 40 | echo "🔐 API配置说明:" 41 | echo " - DeepSeek API密钥: 在应用内API配置页面填入" 42 | echo " - 科大讯飞API: 在应用内语音设置页面配置" 43 | echo " - 环境变量文件(.env)已加入.gitignore保护" -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "antd": "^5.26.0", 14 | "axios": "^1.9.0", 15 | "crypto-js": "^4.2.0", 16 | "immer": "^10.1.1", 17 | "openapi-typescript-codegen": "^0.29.0", 18 | "react": "^19.1.0", 19 | "react-dom": "^19.1.0", 20 | "react-markdown": "^10.1.0", 21 | "react-router-dom": "^7.6.2", 22 | "styled-components": "^6.1.18", 23 | "zustand": "^5.0.5" 24 | }, 25 | "devDependencies": { 26 | "@eslint/js": "^9.25.0", 27 | "@types/react": "^19.1.2", 28 | "@types/react-dom": "^19.1.2", 29 | "@vitejs/plugin-react": "^4.4.1", 30 | "eslint": "^9.25.0", 31 | "eslint-plugin-react-hooks": "^5.2.0", 32 | "eslint-plugin-react-refresh": "^0.4.19", 33 | "globals": "^16.0.0", 34 | "typescript": "~5.8.3", 35 | "typescript-eslint": "^8.30.1", 36 | "vite": "^6.3.5" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/api/auth.ts: -------------------------------------------------------------------------------- 1 | import request from './request' 2 | 3 | export interface User { 4 | id: number 5 | username: string 6 | email?: string 7 | is_active: boolean 8 | created_at: string 9 | updated_at: string 10 | } 11 | 12 | export interface LoginRequest { 13 | username: string 14 | password: string 15 | } 16 | 17 | export interface RegisterRequest { 18 | username: string 19 | password: string 20 | email?: string 21 | } 22 | 23 | export interface LoginResponse { 24 | access_token: string 25 | token_type: string 26 | } 27 | 28 | // 用户登录 29 | export const login = async (data: LoginRequest): Promise => { 30 | return request.post('/api/auth/login', data) 31 | } 32 | 33 | // 用户注册 34 | export const register = async (data: RegisterRequest): Promise => { 35 | return request.post('/api/auth/register', data) 36 | } 37 | 38 | // 获取当前用户信息 39 | export const getCurrentUser = async (): Promise => { 40 | return request.get('/api/auth/me') 41 | } 42 | 43 | // 用户登出 44 | export const logout = async (): Promise<{ detail: string }> => { 45 | return request.post('/api/auth/logout') 46 | } -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Virtual environments 24 | venv/ 25 | env/ 26 | .venv/ 27 | ENV/ 28 | env.bak/ 29 | venv.bak/ 30 | 31 | # Environment variables 32 | .env.local 33 | .env.development 34 | .env.test 35 | .env.production 36 | .env.staging 37 | 38 | # IDEs 39 | .idea/ 40 | .vscode/ 41 | *.swp 42 | *.swo 43 | 44 | # OS generated files 45 | .DS_Store 46 | .DS_Store? 47 | ._* 48 | .Spotlight-V100 49 | .Trashes 50 | ehthumbs.db 51 | Thumbs.db 52 | 53 | # Logs 54 | *.log 55 | logs/ 56 | 57 | # Database 58 | *.db 59 | *.sqlite 60 | *.sqlite3 61 | 62 | # Temporary files 63 | tmp/ 64 | temp/ 65 | .tmp/ 66 | 67 | # Git 68 | .git/ 69 | .gitignore 70 | 71 | # Docker 72 | Dockerfile* 73 | docker-compose* 74 | .dockerignore 75 | 76 | # Documentation 77 | README.md 78 | *.md 79 | 80 | # Test files 81 | test_*.py 82 | *_test.py 83 | tests/ 84 | 85 | # Windows specific 86 | run_server_windows.py 87 | fix_windows_encoding.py 88 | WINDOWS_TROUBLESHOOT.md -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom' 3 | import MainLayout from './layouts/MainLayout' 4 | import InterviewNew from './pages/InterviewNew' 5 | import InterviewMeeting from './pages/InterviewMeeting' 6 | import Settings from './pages/Settings' 7 | 8 | const Placeholder: React.FC<{ title: string }> = ({ title }) => ( 9 |
{title}(开发中...)
10 | ) 11 | 12 | const App: React.FC = () => { 13 | return ( 14 | 15 | 16 | } /> 17 | } /> 18 | } /> 19 | } /> 20 | } /> 21 | } /> 22 | } /> 23 | 24 | 25 | ) 26 | } 27 | 28 | export default App 29 | -------------------------------------------------------------------------------- /frontend/src/store/userConfigStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { devtools, immer } from 'zustand/middleware' 3 | 4 | interface UserInfo { 5 | id: string 6 | username: string 7 | email: string 8 | avatar?: string 9 | role: 'user' | 'admin' 10 | createdAt: string 11 | lastLoginAt: string 12 | } 13 | 14 | interface UserConfigState { 15 | userInfo: UserInfo | null 16 | isAuthenticated: boolean 17 | setUserInfo: (userInfo: UserInfo | null) => void 18 | setAuthenticated: (isAuthenticated: boolean) => void 19 | logout: () => void 20 | } 21 | 22 | export const useUserConfigStore = create()( 23 | devtools( 24 | immer((set) => ({ 25 | userInfo: null, 26 | isAuthenticated: false, 27 | setUserInfo: (userInfo) => set((state) => { 28 | state.userInfo = userInfo 29 | }), 30 | setAuthenticated: (isAuthenticated) => set((state) => { 31 | state.isAuthenticated = isAuthenticated 32 | }), 33 | logout: () => { 34 | localStorage.removeItem('token') 35 | set((state) => { 36 | state.userInfo = null 37 | state.isAuthenticated = false 38 | }) 39 | }, 40 | })), 41 | { 42 | name: 'user-config-store', 43 | trace: true, 44 | traceLimit: 25, 45 | } 46 | ) 47 | ) -------------------------------------------------------------------------------- /frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | a { 17 | font-weight: 500; 18 | color: #646cff; 19 | text-decoration: inherit; 20 | } 21 | a:hover { 22 | color: #535bf2; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | padding: 0; 28 | min-width: 320px; 29 | min-height: 100vh; 30 | width: 100%; 31 | height: 100vh; 32 | } 33 | 34 | #root { 35 | width: 100%; 36 | height: 100vh; 37 | margin: 0; 38 | padding: 0; 39 | } 40 | 41 | h1 { 42 | font-size: 3.2em; 43 | line-height: 1.1; 44 | } 45 | 46 | button { 47 | border-radius: 8px; 48 | border: 1px solid transparent; 49 | padding: 0.6em 1.2em; 50 | font-size: 1em; 51 | font-weight: 500; 52 | font-family: inherit; 53 | background-color: #1a1a1a; 54 | cursor: pointer; 55 | transition: border-color 0.25s; 56 | } 57 | button:hover { 58 | border-color: #646cff; 59 | } 60 | button:focus, 61 | button:focus-visible { 62 | outline: 4px auto -webkit-focus-ring-color; 63 | } 64 | 65 | @media (prefers-color-scheme: light) { 66 | :root { 67 | color: #213547; 68 | background-color: #ffffff; 69 | } 70 | a:hover { 71 | color: #747bff; 72 | } 73 | button { 74 | background-color: #f9f9f9; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /backend/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # AI Interview Helper Backend Setup Script 4 | # 后端环境安装和数据库初始化脚本 5 | 6 | set -e 7 | 8 | echo "🚀 开始设置AI面试助手后端环境..." 9 | 10 | # 检查Python版本 11 | echo "📋 检查Python版本..." 12 | python3 --version 13 | 14 | # 创建虚拟环境 15 | echo "🔧 创建Python虚拟环境..." 16 | python3 -m venv venv 17 | 18 | # 检查和配置环境变量 19 | echo "🔧 检查环境变量配置..." 20 | if [ ! -f .env ]; then 21 | echo "⚠️ 未找到.env文件,正在复制示例配置..." 22 | cp .env.example .env 23 | echo "✅ 已创建.env文件,请根据需要修改JWT密钥等配置" 24 | echo "⚠️ 重要:请修改.env文件中的SECRET_KEY为强密码!" 25 | fi 26 | 27 | # 激活虚拟环境 28 | echo "✅ 激活虚拟环境..." 29 | source venv/bin/activate 30 | 31 | # 安装依赖 32 | echo "📦 安装Python依赖..." 33 | pip install -r requirements.txt 34 | 35 | # 测试数据库连接并创建数据库 36 | echo "🗄️ 初始化数据库连接..." 37 | python test_db.py 38 | 39 | # 创建数据库表 40 | echo "📊 创建数据库表结构..." 41 | python -c " 42 | import sys 43 | sys.path.append('.') 44 | from app.database import engine 45 | from app.models import Base 46 | Base.metadata.create_all(bind=engine) 47 | print('✅ 数据库表创建成功') 48 | " 49 | 50 | # 创建默认用户 51 | echo "👤 创建默认用户账户..." 52 | python init_default_user.py 53 | 54 | echo "🎉 后端环境设置完成!" 55 | echo "" 56 | echo "📝 使用说明:" 57 | echo " 1. 启动后端服务: python run_server.py" 58 | echo " 2. API文档地址: http://localhost:9000/docs" 59 | echo " 3. 默认用户: test / test1234" 60 | echo "" 61 | echo "🔧 环境配置:" 62 | echo " - Python虚拟环境: ./venv" 63 | echo " - 数据库: PostgreSQL (localhost:5432)" 64 | echo " - Redis: localhost:6379" 65 | echo " - 后端端口: 9000" 66 | echo "" 67 | echo "🔐 安全提醒:" 68 | echo " - .env文件包含敏感信息,已加入.gitignore" 69 | echo " - 生产环境请务必修改SECRET_KEY为强密码" 70 | echo " - API密钥等敏感信息不会提交到版本控制" -------------------------------------------------------------------------------- /fix-docker-permissions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Docker权限修复脚本 (WSL环境) 4 | 5 | echo "🔧 检查并修复Docker权限问题..." 6 | 7 | # 检查当前用户 8 | echo "当前用户: $(whoami)" 9 | 10 | # 检查docker组是否存在 11 | if getent group docker > /dev/null 2>&1; then 12 | echo "✅ docker组存在" 13 | else 14 | echo "❌ docker组不存在,正在创建..." 15 | sudo groupadd docker 16 | fi 17 | 18 | # 检查用户是否在docker组中 19 | if groups $USER | grep -q '\bdocker\b'; then 20 | echo "✅ 用户已在docker组中" 21 | else 22 | echo "⚠️ 用户不在docker组中,正在添加..." 23 | sudo usermod -aG docker $USER 24 | echo "✅ 已将用户添加到docker组" 25 | echo "⚠️ 请注销并重新登录,或运行: newgrp docker" 26 | fi 27 | 28 | # 检查Docker socket权限 29 | if [ -S /var/run/docker.sock ]; then 30 | echo "✅ Docker socket存在" 31 | ls -la /var/run/docker.sock 32 | 33 | # 尝试修复权限 34 | if [ ! -w /var/run/docker.sock ]; then 35 | echo "⚠️ Docker socket权限不足,尝试修复..." 36 | sudo chmod 666 /var/run/docker.sock 2>/dev/null || true 37 | fi 38 | else 39 | echo "❌ Docker socket不存在" 40 | echo "请确保Docker Desktop在Windows中运行,并启用WSL集成" 41 | fi 42 | 43 | # 测试Docker命令 44 | echo "" 45 | echo "🧪 测试Docker命令..." 46 | if docker ps > /dev/null 2>&1; then 47 | echo "✅ Docker命令可以正常执行" 48 | docker --version 49 | else 50 | echo "❌ Docker命令执行失败" 51 | echo "" 52 | echo "💡 解决方案:" 53 | echo "1. 确保Docker Desktop在Windows中运行" 54 | echo "2. 在Docker Desktop设置中启用WSL集成" 55 | echo "3. 重启WSL终端" 56 | echo "4. 如果问题持续,运行: newgrp docker" 57 | 58 | # 提供替代方案 59 | echo "" 60 | echo "🔄 尝试使用newgrp临时解决..." 61 | exec newgrp docker 62 | fi 63 | 64 | echo "" 65 | echo "🎉 Docker权限检查完成!" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Environment variables and secrets 2 | .env 3 | .env.local 4 | .env.development 5 | .env.test 6 | .env.production 7 | .env.staging 8 | .env.md 9 | 10 | # API keys and configuration files 11 | secrets.json 12 | secrets.yaml 13 | secrets.yml 14 | config.ini 15 | settings.ini 16 | api-keys.json 17 | runtime-config.json 18 | 19 | # Logs 20 | *.log 21 | logs/ 22 | 23 | # OS generated files 24 | .DS_Store 25 | .DS_Store? 26 | ._* 27 | .Spotlight-V100 28 | .Trashes 29 | ehthumbs.db 30 | Thumbs.db 31 | 32 | # Editor and IDE files 33 | .vscode/ 34 | .idea/ 35 | *.swp 36 | *.swo 37 | *~ 38 | 39 | # Temporary files 40 | tmp/ 41 | temp/ 42 | .tmp/ 43 | 44 | # Backup files 45 | *.bak 46 | *.backup 47 | *.old 48 | 49 | # Local development databases 50 | *.db 51 | *.sqlite 52 | *.sqlite3 53 | 54 | # SSL certificates and keys 55 | *.pem 56 | *.key 57 | *.crt 58 | *.csr 59 | 60 | # Node.js 61 | node_modules/ 62 | */node_modules/ 63 | frontend/node_modules/ 64 | npm-debug.log* 65 | yarn-debug.log* 66 | yarn-error.log* 67 | dist/ 68 | build/ 69 | frontend/dist/ 70 | frontend/build/ 71 | frontend/.vite/ 72 | frontend/.temp/ 73 | frontend/.cache/ 74 | *.tsbuildinfo 75 | 76 | # Python 77 | __pycache__/ 78 | *.py[cod] 79 | *$py.class 80 | venv/ 81 | env/ 82 | .venv/ 83 | backend/venv/ 84 | backend/__pycache__/ 85 | backend/app/__pycache__/ 86 | *.pyc 87 | *.pyo 88 | *.pyd 89 | backend/*.db 90 | backend/data/ 91 | backend/alembic/versions/*.py 92 | 93 | # Runtime and test files 94 | pids 95 | *.pid 96 | *.seed 97 | *.pid.lock 98 | coverage/ 99 | test-results/ 100 | playwright-report/ 101 | .cache 102 | .coverage 103 | .coverage.* 104 | .mypy_cache 105 | .pytest_cache 106 | .hypothesis -------------------------------------------------------------------------------- /scripts/dev-start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # AI Interview Helper 开发环境启动脚本 4 | 5 | set -e 6 | 7 | echo "🚀 启动AI面试助手开发环境..." 8 | 9 | # 检查Docker是否安装 10 | if ! command -v docker &> /dev/null; then 11 | echo "❌ Docker未安装,请先安装Docker" 12 | exit 1 13 | fi 14 | 15 | # 检查当前目录 16 | if [ ! -f "docker-compose.dev.yml" ]; then 17 | echo "❌ 未找到docker-compose.dev.yml文件,请在项目根目录执行此脚本" 18 | exit 1 19 | fi 20 | 21 | # 停止并删除旧容器(如果存在) 22 | echo "🧹 清理旧容器..." 23 | docker-compose -f docker-compose.dev.yml down --remove-orphans 2>/dev/null || docker compose -f docker-compose.dev.yml down --remove-orphans 2>/dev/null || true 24 | 25 | # 构建并启动服务 26 | echo "🔨 构建并启动开发环境..." 27 | if command -v docker-compose &> /dev/null; then 28 | docker-compose -f docker-compose.dev.yml up --build -d 29 | else 30 | docker compose -f docker-compose.dev.yml up --build -d 31 | fi 32 | 33 | # 等待数据库启动 34 | echo "⏳ 等待数据库启动..." 35 | sleep 15 36 | 37 | # 初始化默认用户 38 | echo "👤 初始化默认用户..." 39 | if command -v docker-compose &> /dev/null; then 40 | docker-compose -f docker-compose.dev.yml exec backend python init_default_user.py 41 | else 42 | docker compose -f docker-compose.dev.yml exec backend python init_default_user.py 43 | fi 44 | 45 | echo "" 46 | echo "🎉 开发环境启动完成!" 47 | echo "" 48 | echo "📝 服务地址:" 49 | echo " - 后端API: http://localhost:9000" 50 | echo " - API文档: http://localhost:9000/docs" 51 | echo " - PostgreSQL: localhost:5432" 52 | echo " - Redis: localhost:6379" 53 | echo "" 54 | echo "👤 默认账户:" 55 | echo " - 用户名: test" 56 | echo " - 密码: test1234" 57 | echo "" 58 | echo "🔧 开发命令:" 59 | echo " - 查看日志: docker-compose -f docker-compose.dev.yml logs -f" 60 | echo " - 进入后端容器: docker-compose -f docker-compose.dev.yml exec backend bash" 61 | echo " - 停止服务: docker-compose -f docker-compose.dev.yml down" 62 | echo "" 63 | echo "💡 提示:" 64 | echo " - 代码修改会自动重载" 65 | echo " - 数据库数据持久化保存" -------------------------------------------------------------------------------- /backend/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import locale 4 | 5 | # 修复Windows环境编码问题 6 | if sys.platform == "win32": 7 | os.environ['PYTHONIOENCODING'] = 'utf-8' 8 | os.environ['PYTHONUTF8'] = '1' 9 | 10 | from fastapi import FastAPI 11 | from fastapi.middleware.cors import CORSMiddleware 12 | from contextlib import asynccontextmanager 13 | from dotenv import load_dotenv 14 | 15 | from app.database import engine, Base 16 | from app.routers import auth, sessions 17 | 18 | load_dotenv(encoding='utf-8') 19 | 20 | @asynccontextmanager 21 | async def lifespan(app: FastAPI): 22 | try: 23 | # Create tables 24 | print("🗄️ 正在创建数据库表...") 25 | Base.metadata.create_all(bind=engine) 26 | print("✅ 数据库表创建成功") 27 | except Exception as e: 28 | print(f"❌ 数据库初始化失败: {e}") 29 | print("请检查:") 30 | print("1. PostgreSQL服务是否正在运行") 31 | print("2. 数据库连接信息是否正确") 32 | print("3. 数据库密码是否为'root'") 33 | raise e 34 | 35 | yield 36 | 37 | app = FastAPI( 38 | title="AI Interview Helper API", 39 | description="Backend API for AI Interview Helper application", 40 | version="0.3.0", 41 | lifespan=lifespan 42 | ) 43 | 44 | # CORS middleware 45 | CORS_ORIGINS = os.getenv("CORS_ORIGINS", "http://localhost:5173").split(",") 46 | app.add_middleware( 47 | CORSMiddleware, 48 | allow_origins=CORS_ORIGINS, 49 | allow_credentials=True, 50 | allow_methods=["*"], 51 | allow_headers=["*"], 52 | ) 53 | 54 | # Include routers 55 | app.include_router(auth.router, prefix="/api/auth", tags=["authentication"]) 56 | app.include_router(sessions.router, prefix="/api/sessions", tags=["sessions"]) 57 | 58 | @app.get("/") 59 | async def root(): 60 | return {"message": "AI Interview Helper API v0.3.0"} 61 | 62 | @app.get("/health") 63 | async def health_check(): 64 | return {"status": "healthy"} -------------------------------------------------------------------------------- /backend/app/schemas.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, EmailStr 2 | from datetime import datetime 3 | from typing import Optional, List 4 | 5 | # User schemas 6 | class UserBase(BaseModel): 7 | username: str 8 | email: Optional[EmailStr] = None 9 | 10 | class UserCreate(UserBase): 11 | password: str 12 | 13 | class UserLogin(BaseModel): 14 | username: str 15 | password: str 16 | 17 | class User(UserBase): 18 | id: int 19 | is_active: bool 20 | created_at: datetime 21 | updated_at: datetime 22 | 23 | class Config: 24 | from_attributes = True 25 | 26 | # Token schemas 27 | class Token(BaseModel): 28 | access_token: str 29 | token_type: str 30 | 31 | class TokenData(BaseModel): 32 | username: Optional[str] = None 33 | 34 | # Session schemas 35 | class SessionMessageBase(BaseModel): 36 | message_type: str 37 | content: str 38 | message_metadata: Optional[str] = None 39 | 40 | class SessionMessageCreate(SessionMessageBase): 41 | pass 42 | 43 | class SessionMessage(SessionMessageBase): 44 | id: int 45 | timestamp: datetime 46 | 47 | class Config: 48 | from_attributes = True 49 | 50 | class InterviewSessionBase(BaseModel): 51 | title: Optional[str] = None 52 | 53 | class InterviewSessionCreate(InterviewSessionBase): 54 | pass 55 | 56 | class InterviewSession(InterviewSessionBase): 57 | id: int 58 | session_id: str 59 | user_id: int 60 | created_at: datetime 61 | updated_at: datetime 62 | messages: List[SessionMessage] = [] 63 | 64 | class Config: 65 | from_attributes = True 66 | 67 | class InterviewSessionList(BaseModel): 68 | id: int 69 | session_id: str 70 | title: Optional[str] = None 71 | created_at: datetime 72 | updated_at: datetime 73 | message_count: int = 0 74 | 75 | class Config: 76 | from_attributes = True -------------------------------------------------------------------------------- /scripts/docker-start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # AI Interview Helper Docker启动脚本 4 | 5 | set -e 6 | 7 | echo "🐳 启动AI面试助手Docker环境..." 8 | 9 | # 检查Docker是否安装 10 | if ! command -v docker &> /dev/null; then 11 | echo "❌ Docker未安装,请先安装Docker" 12 | exit 1 13 | fi 14 | 15 | if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then 16 | echo "❌ Docker Compose未安装,请先安装Docker Compose" 17 | exit 1 18 | fi 19 | 20 | # 检查当前目录 21 | if [ ! -f "docker-compose.yml" ]; then 22 | echo "❌ 未找到docker-compose.yml文件,请在项目根目录执行此脚本" 23 | exit 1 24 | fi 25 | 26 | # 停止并删除旧容器(如果存在) 27 | echo "🧹 清理旧容器..." 28 | docker-compose down --remove-orphans 2>/dev/null || docker compose down --remove-orphans 2>/dev/null || true 29 | 30 | # 构建并启动服务 31 | echo "🔨 构建并启动服务..." 32 | if command -v docker-compose &> /dev/null; then 33 | docker-compose up --build -d 34 | else 35 | docker compose up --build -d 36 | fi 37 | 38 | # 等待服务启动 39 | echo "⏳ 等待服务启动..." 40 | sleep 10 41 | 42 | # 检查服务状态 43 | echo "📊 检查服务状态..." 44 | if command -v docker-compose &> /dev/null; then 45 | docker-compose ps 46 | else 47 | docker compose ps 48 | fi 49 | 50 | # 初始化默认用户 51 | echo "👤 初始化默认用户..." 52 | if command -v docker-compose &> /dev/null; then 53 | docker-compose exec backend python init_default_user.py 54 | else 55 | docker compose exec backend python init_default_user.py 56 | fi 57 | 58 | echo "" 59 | echo "🎉 AI面试助手启动完成!" 60 | echo "" 61 | echo "📝 服务地址:" 62 | echo " - 后端API: http://localhost:9000" 63 | echo " - API文档: http://localhost:9000/docs" 64 | echo " - PostgreSQL: localhost:5432" 65 | echo " - Redis: localhost:6379" 66 | echo "" 67 | echo "👤 默认账户:" 68 | echo " - 用户名: test" 69 | echo " - 密码: test1234" 70 | echo "" 71 | echo "🔧 管理命令:" 72 | echo " - 查看日志: ./scripts/docker-logs.sh" 73 | echo " - 停止服务: ./scripts/docker-stop.sh" 74 | echo " - 重启服务: ./scripts/docker-restart.sh" -------------------------------------------------------------------------------- /scripts/start-frontend.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # AI Interview Helper 前端启动脚本 4 | 5 | set -e 6 | 7 | echo "🚀 启动AI面试助手前端开发服务器..." 8 | 9 | # 检查Node.js是否安装 10 | if ! command -v node &> /dev/null; then 11 | echo "❌ Node.js未安装,请先安装Node.js 16+" 12 | exit 1 13 | fi 14 | 15 | if ! command -v npm &> /dev/null; then 16 | echo "❌ npm未安装,请先安装npm" 17 | exit 1 18 | fi 19 | 20 | echo "✅ Node.js版本: $(node --version)" 21 | echo "✅ npm版本: $(npm --version)" 22 | 23 | # 检查当前目录 24 | if [ ! -d "frontend" ]; then 25 | echo "❌ 未找到frontend目录,请在项目根目录执行此脚本" 26 | exit 1 27 | fi 28 | 29 | # 进入前端目录 30 | cd frontend 31 | 32 | # 检查并创建环境变量文件 33 | if [ ! -f ".env" ]; then 34 | echo "⚠️ 未找到.env文件,正在复制示例配置..." 35 | if [ -f ".env.example" ]; then 36 | cp .env.example .env 37 | echo "✅ 已创建.env文件" 38 | else 39 | echo "🔧 创建默认.env文件..." 40 | cat > .env << EOF 41 | # API Base URL 42 | VITE_API_BASE_URL=http://localhost:9000 43 | EOF 44 | echo "✅ 已创建默认.env文件" 45 | fi 46 | fi 47 | 48 | # 检查后端API是否可用 49 | echo "🔍 检查后端API连接..." 50 | if curl -s -f http://localhost:9000/health > /dev/null 2>&1; then 51 | echo "✅ 后端API服务正常运行" 52 | else 53 | echo "⚠️ 后端API不可访问,请确保Docker容器正在运行" 54 | echo "💡 可以运行以下命令启动后端:" 55 | echo " docker-compose -f docker-compose.dev.yml up -d" 56 | echo "" 57 | echo "🔄 继续启动前端服务器..." 58 | fi 59 | 60 | # 检查是否已安装依赖 61 | if [ ! -d "node_modules" ]; then 62 | echo "📦 安装前端依赖..." 63 | npm install 64 | else 65 | echo "✅ 前端依赖已安装" 66 | fi 67 | 68 | # 启动开发服务器 69 | echo "🌟 启动前端开发服务器..." 70 | echo "" 71 | echo "📝 服务信息:" 72 | echo " - 前端地址: http://localhost:5173" 73 | echo " - 后端API: http://localhost:9000" 74 | echo " - API文档: http://localhost:9000/docs" 75 | echo "" 76 | echo "👤 默认登录账户:" 77 | echo " - 用户名: test" 78 | echo " - 密码: test1234" 79 | echo "" 80 | echo "🛑 按Ctrl+C停止服务器" 81 | echo "=" * 50 82 | 83 | # 启动Vite开发服务器 84 | npm run dev -------------------------------------------------------------------------------- /backend/test_db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test database connection and create database if needed 4 | """ 5 | 6 | import os 7 | import sys 8 | from sqlalchemy import create_engine, text 9 | from dotenv import load_dotenv 10 | 11 | def test_and_create_db(): 12 | load_dotenv() 13 | 14 | # First try to connect to postgres to create the database 15 | postgres_url = "postgresql://postgres:root@localhost:5432/postgres" 16 | target_db = "ai_interview_helper" 17 | 18 | try: 19 | # Connect to postgres database 20 | engine = create_engine(postgres_url) 21 | with engine.connect() as conn: 22 | # Check if database exists 23 | result = conn.execute(text(f"SELECT 1 FROM pg_database WHERE datname = '{target_db}'")) 24 | if not result.fetchone(): 25 | # Database doesn't exist, create it 26 | conn.execute(text("COMMIT")) # End any existing transaction 27 | conn.execute(text(f"CREATE DATABASE {target_db}")) 28 | print(f"Database '{target_db}' created successfully") 29 | else: 30 | print(f"Database '{target_db}' already exists") 31 | except Exception as e: 32 | print(f"Error with database operations: {e}") 33 | return False 34 | 35 | # Test connection to the target database 36 | DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:root@localhost:5432/ai_interview_helper") 37 | 38 | try: 39 | engine = create_engine(DATABASE_URL) 40 | with engine.connect() as conn: 41 | result = conn.execute(text("SELECT 1")) 42 | print(f"Successfully connected to database at {DATABASE_URL}") 43 | return True 44 | except Exception as e: 45 | print(f"Failed to connect to database: {e}") 46 | return False 47 | 48 | if __name__ == "__main__": 49 | test_and_create_db() -------------------------------------------------------------------------------- /frontend/src/api/sessions.ts: -------------------------------------------------------------------------------- 1 | import request from './request' 2 | 3 | export interface SessionMessage { 4 | id: number 5 | message_type: string 6 | content: string 7 | message_metadata?: string 8 | timestamp: string 9 | } 10 | 11 | export interface InterviewSession { 12 | id: number 13 | session_id: string 14 | user_id: number 15 | title?: string 16 | created_at: string 17 | updated_at: string 18 | messages: SessionMessage[] 19 | } 20 | 21 | export interface InterviewSessionList { 22 | id: number 23 | session_id: string 24 | title?: string 25 | created_at: string 26 | updated_at: string 27 | message_count: number 28 | } 29 | 30 | export interface CreateSessionRequest { 31 | title?: string 32 | } 33 | 34 | export interface CreateMessageRequest { 35 | message_type: string 36 | content: string 37 | message_metadata?: string 38 | } 39 | 40 | // 创建新的面试会话 41 | export const createSession = async (data: CreateSessionRequest): Promise => { 42 | return request.post('/api/sessions/', data) 43 | } 44 | 45 | // 获取用户的所有会话列表 46 | export const getUserSessions = async (): Promise => { 47 | return request.get('/api/sessions/') 48 | } 49 | 50 | // 获取特定会话详情 51 | export const getSession = async (sessionId: string): Promise => { 52 | return request.get(`/api/sessions/${sessionId}`) 53 | } 54 | 55 | // 向会话添加消息 56 | export const addMessageToSession = async ( 57 | sessionId: string, 58 | message: CreateMessageRequest 59 | ): Promise => { 60 | return request.post(`/api/sessions/${sessionId}/messages`, message) 61 | } 62 | 63 | // 获取会话的所有消息 64 | export const getSessionMessages = async (sessionId: string): Promise => { 65 | return request.get(`/api/sessions/${sessionId}/messages`) 66 | } 67 | 68 | // 删除会话 69 | export const deleteSession = async (sessionId: string): Promise<{ detail: string }> => { 70 | return request.delete(`/api/sessions/${sessionId}`) 71 | } -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | # PostgreSQL数据库 5 | postgres: 6 | image: postgres:17.5 7 | container_name: ai-interview-postgres-dev 8 | environment: 9 | POSTGRES_DB: ai_interview_helper 10 | POSTGRES_USER: postgres 11 | POSTGRES_PASSWORD: root 12 | POSTGRES_INITDB_ARGS: "--encoding=UTF8 --lc-collate=C --lc-ctype=C" 13 | volumes: 14 | - postgres_dev_data:/var/lib/postgresql/data 15 | - ./backend/init.sql:/docker-entrypoint-initdb.d/init.sql 16 | ports: 17 | - "5462:5432" 18 | networks: 19 | - ai-interview-dev-network 20 | 21 | # Redis缓存 22 | redis: 23 | image: redis:7.4-alpine 24 | container_name: ai-interview-redis-dev 25 | ports: 26 | - "6389:6379" 27 | networks: 28 | - ai-interview-dev-network 29 | command: redis-server --appendonly yes 30 | volumes: 31 | - redis_dev_data:/data 32 | 33 | # 后端API服务 (开发模式) 34 | backend: 35 | build: 36 | context: ./backend 37 | dockerfile: Dockerfile 38 | container_name: ai-interview-backend-dev 39 | environment: 40 | DATABASE_URL: postgresql://postgres:root@postgres:5432/ai_interview_helper 41 | REDIS_URL: redis://redis:6379 42 | SECRET_KEY: ai-interview-helper-secret-key-dev-2024 43 | ALGORITHM: HS256 44 | ACCESS_TOKEN_EXPIRE_MINUTES: 30 45 | CORS_ORIGINS: '["http://localhost:5173", "http://localhost:3000", "http://localhost:8080"]' 46 | DEBUG: "True" 47 | ports: 48 | - "9000:9000" 49 | networks: 50 | - ai-interview-dev-network 51 | depends_on: 52 | - postgres 53 | - redis 54 | volumes: 55 | - ./backend:/app:rw 56 | - /app/venv 57 | restart: unless-stopped 58 | command: python run_server.py 59 | stdin_open: true 60 | tty: true 61 | 62 | volumes: 63 | postgres_dev_data: 64 | driver: local 65 | redis_dev_data: 66 | driver: local 67 | 68 | networks: 69 | ai-interview-dev-network: 70 | driver: bridge -------------------------------------------------------------------------------- /backend/fix_windows_encoding.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Windows环境编码问题修复脚本 4 | """ 5 | 6 | import os 7 | import sys 8 | import locale 9 | 10 | def fix_windows_encoding(): 11 | """修复Windows环境下的编码问题""" 12 | 13 | print("🔧 检查和修复Windows环境编码问题...") 14 | 15 | # 设置环境变量 16 | os.environ['PYTHONIOENCODING'] = 'utf-8' 17 | os.environ['PYTHONUTF8'] = '1' 18 | 19 | # 检查系统编码 20 | print(f"系统默认编码: {locale.getpreferredencoding()}") 21 | print(f"文件系统编码: {sys.getfilesystemencoding()}") 22 | print(f"stdout编码: {sys.stdout.encoding}") 23 | 24 | # 检查数据库URL 25 | from dotenv import load_dotenv 26 | load_dotenv() 27 | 28 | db_url = os.getenv("DATABASE_URL") 29 | print(f"数据库URL: {db_url}") 30 | 31 | # 验证URL编码 32 | try: 33 | db_url_bytes = db_url.encode('utf-8') 34 | db_url_decoded = db_url_bytes.decode('utf-8') 35 | print("✅ 数据库URL编码正常") 36 | except Exception as e: 37 | print(f"❌ 数据库URL编码异常: {e}") 38 | return False 39 | 40 | return True 41 | 42 | def test_database_connection(): 43 | """测试数据库连接""" 44 | try: 45 | from app.database import engine 46 | with engine.connect() as conn: 47 | result = conn.execute("SELECT 1") 48 | print("✅ 数据库连接成功") 49 | return True 50 | except Exception as e: 51 | print(f"❌ 数据库连接失败: {e}") 52 | return False 53 | 54 | if __name__ == "__main__": 55 | print("🚀 开始Windows环境编码修复...") 56 | 57 | if fix_windows_encoding(): 58 | print("✅ 编码环境检查完成") 59 | 60 | if test_database_connection(): 61 | print("✅ 数据库连接测试成功") 62 | else: 63 | print("❌ 数据库连接测试失败") 64 | print("请检查PostgreSQL是否正在运行,密码是否正确") 65 | else: 66 | print("❌ 编码环境修复失败") 67 | 68 | print("\n📝 解决方案建议:") 69 | print("1. 确保PostgreSQL服务正在运行") 70 | print("2. 检查数据库密码是否为'root'") 71 | print("3. 如果问题持续,请尝试修改DATABASE_URL中的密码") 72 | print("4. 建议在Windows Terminal或PowerShell中运行,避免使用Git Bash") -------------------------------------------------------------------------------- /backend/init_default_user.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Initialize default user for AI Interview Helper 4 | Creates a default user with username 'test' and password 'test1234' 5 | """ 6 | 7 | import os 8 | import sys 9 | from sqlalchemy import create_engine 10 | from sqlalchemy.orm import sessionmaker 11 | from dotenv import load_dotenv 12 | 13 | # Add the current directory to path to import app modules 14 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 15 | 16 | from app.models import Base, User 17 | from app.auth import get_password_hash 18 | 19 | def init_default_user(): 20 | load_dotenv() 21 | 22 | DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:yourpassword@localhost:5432/ai_interview_helper") 23 | 24 | # Create engine and session 25 | engine = create_engine(DATABASE_URL) 26 | Base.metadata.create_all(bind=engine) 27 | 28 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 29 | db = SessionLocal() 30 | 31 | try: 32 | # Check if default user already exists 33 | existing_user = db.query(User).filter(User.username == "test").first() 34 | if existing_user: 35 | print("Default user 'test' already exists") 36 | return 37 | 38 | # Create default user 39 | hashed_password = get_password_hash("test1234") 40 | default_user = User( 41 | username="test", 42 | email="test@example.com", 43 | hashed_password=hashed_password, 44 | is_active=True 45 | ) 46 | 47 | db.add(default_user) 48 | db.commit() 49 | db.refresh(default_user) 50 | 51 | print(f"Default user created successfully:") 52 | print(f" Username: test") 53 | print(f" Password: test1234") 54 | print(f" Email: test@example.com") 55 | print(f" User ID: {default_user.id}") 56 | 57 | except Exception as e: 58 | print(f"Error creating default user: {e}") 59 | db.rollback() 60 | finally: 61 | db.close() 62 | 63 | if __name__ == "__main__": 64 | init_default_user() -------------------------------------------------------------------------------- /backend/app/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, DateTime, Text, ForeignKey, Boolean 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import relationship 4 | from datetime import datetime 5 | 6 | Base = declarative_base() 7 | 8 | class User(Base): 9 | __tablename__ = "users" 10 | 11 | id = Column(Integer, primary_key=True, index=True) 12 | username = Column(String, unique=True, index=True, nullable=False) 13 | email = Column(String, unique=True, index=True, nullable=True) 14 | hashed_password = Column(String, nullable=False) 15 | is_active = Column(Boolean, default=True) 16 | created_at = Column(DateTime, default=datetime.utcnow) 17 | updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) 18 | 19 | # Relationship 20 | sessions = relationship("InterviewSession", back_populates="user") 21 | 22 | class InterviewSession(Base): 23 | __tablename__ = "interview_sessions" 24 | 25 | id = Column(Integer, primary_key=True, index=True) 26 | user_id = Column(Integer, ForeignKey("users.id"), nullable=False) 27 | session_id = Column(String, unique=True, index=True, nullable=False) 28 | title = Column(String, nullable=True) 29 | created_at = Column(DateTime, default=datetime.utcnow) 30 | updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) 31 | 32 | # Relationship 33 | user = relationship("User", back_populates="sessions") 34 | messages = relationship("SessionMessage", back_populates="session") 35 | 36 | class SessionMessage(Base): 37 | __tablename__ = "session_messages" 38 | 39 | id = Column(Integer, primary_key=True, index=True) 40 | session_id = Column(Integer, ForeignKey("interview_sessions.id"), nullable=False) 41 | message_type = Column(String, nullable=False) # 'user', 'assistant', 'transcription' 42 | content = Column(Text, nullable=False) 43 | timestamp = Column(DateTime, default=datetime.utcnow) 44 | message_metadata = Column(Text, nullable=True) # JSON string for additional data 45 | 46 | # Relationship 47 | session = relationship("InterviewSession", back_populates="messages") -------------------------------------------------------------------------------- /frontend/src/store/xfyunConfigStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { devtools, persist } from 'zustand/middleware' 3 | import { immer } from 'zustand/middleware/immer' 4 | 5 | /** 6 | * 科大讯飞API配置接口 7 | */ 8 | export interface XfyunConfig { 9 | appId: string 10 | apiKey: string 11 | isConfigured: boolean 12 | description?: string 13 | createdAt: string 14 | updatedAt: string 15 | } 16 | 17 | interface XfyunConfigState { 18 | config: XfyunConfig 19 | updateConfig: (appId: string, apiKey: string) => void 20 | clearConfig: () => void 21 | getAppId: () => string 22 | getApiKey: () => string 23 | } 24 | 25 | const defaultConfig: XfyunConfig = { 26 | appId: '', 27 | apiKey: '', 28 | isConfigured: false, 29 | description: '科大讯飞语音转写API配置', 30 | createdAt: new Date().toISOString(), 31 | updatedAt: new Date().toISOString() 32 | } 33 | 34 | export const useXfyunConfigStore = create()( 35 | devtools( 36 | persist( 37 | immer((set, get) => ({ 38 | config: defaultConfig, 39 | 40 | updateConfig: (appId: string, apiKey: string) => { 41 | set((state) => { 42 | state.config.appId = appId 43 | state.config.apiKey = apiKey 44 | state.config.isConfigured = !!(appId && apiKey) 45 | state.config.updatedAt = new Date().toISOString() 46 | }) 47 | }, 48 | 49 | clearConfig: () => { 50 | set((state) => { 51 | state.config = { 52 | ...defaultConfig, 53 | createdAt: state.config.createdAt, 54 | updatedAt: new Date().toISOString() 55 | } 56 | }) 57 | }, 58 | 59 | getAppId: () => { 60 | const { config } = get() 61 | return config.appId || import.meta.env.VITE_XFYUN_APPID || '' 62 | }, 63 | 64 | getApiKey: () => { 65 | const { config } = get() 66 | return config.apiKey || import.meta.env.VITE_XFYUN_API_KEY || '' 67 | } 68 | })), 69 | { 70 | name: 'xfyun-config-storage', 71 | } 72 | ), 73 | { 74 | name: 'xfyun-config-store', 75 | trace: true, 76 | traceLimit: 25, 77 | } 78 | ) 79 | ) -------------------------------------------------------------------------------- /test-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # AI Interview Helper Docker环境测试脚本 4 | 5 | echo "🧪 测试AI面试助手Docker环境..." 6 | 7 | # 检查Docker是否安装 8 | if ! command -v docker &> /dev/null; then 9 | echo "❌ Docker未安装" 10 | exit 1 11 | fi 12 | 13 | echo "✅ Docker已安装: $(docker --version)" 14 | 15 | # 检查Docker Compose 16 | if command -v docker-compose &> /dev/null; then 17 | echo "✅ Docker Compose已安装: $(docker-compose --version)" 18 | elif docker compose version &> /dev/null; then 19 | echo "✅ Docker Compose已安装: $(docker compose version)" 20 | else 21 | echo "❌ Docker Compose未安装" 22 | exit 1 23 | fi 24 | 25 | # 检查必要文件 26 | echo "📋 检查项目文件..." 27 | required_files=( 28 | "docker-compose.yml" 29 | "docker-compose.dev.yml" 30 | "backend/Dockerfile" 31 | "backend/requirements.txt" 32 | "scripts/docker-start.sh" 33 | ) 34 | 35 | for file in "${required_files[@]}"; do 36 | if [ -f "$file" ]; then 37 | echo "✅ $file" 38 | else 39 | echo "❌ $file 不存在" 40 | exit 1 41 | fi 42 | done 43 | 44 | # 测试Docker Compose配置 45 | echo "🔍 验证Docker Compose配置..." 46 | if command -v docker-compose &> /dev/null; then 47 | docker-compose config > /dev/null 48 | if [ $? -eq 0 ]; then 49 | echo "✅ docker-compose.yml 配置有效" 50 | else 51 | echo "❌ docker-compose.yml 配置无效" 52 | exit 1 53 | fi 54 | 55 | docker-compose -f docker-compose.dev.yml config > /dev/null 56 | if [ $? -eq 0 ]; then 57 | echo "✅ docker-compose.dev.yml 配置有效" 58 | else 59 | echo "❌ docker-compose.dev.yml 配置无效" 60 | exit 1 61 | fi 62 | else 63 | docker compose config > /dev/null 64 | if [ $? -eq 0 ]; then 65 | echo "✅ docker-compose.yml 配置有效" 66 | else 67 | echo "❌ docker-compose.yml 配置无效" 68 | exit 1 69 | fi 70 | 71 | docker compose -f docker-compose.dev.yml config > /dev/null 72 | if [ $? -eq 0 ]; then 73 | echo "✅ docker-compose.dev.yml 配置有效" 74 | else 75 | echo "❌ docker-compose.dev.yml 配置无效" 76 | exit 1 77 | fi 78 | fi 79 | 80 | echo "" 81 | echo "🎉 Docker环境测试通过!" 82 | echo "" 83 | echo "📝 可以使用以下命令启动:" 84 | echo " - 生产环境: ./scripts/docker-start.sh" 85 | echo " - 开发环境: ./scripts/dev-start.sh" 86 | echo " - 手动启动: docker-compose up --build -d" -------------------------------------------------------------------------------- /frontend/src/api/request.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios' 3 | import { message } from 'antd' 4 | 5 | // 创建axios实例 6 | const request: AxiosInstance = axios.create({ 7 | baseURL: import.meta.env.VITE_API_BASE_URL, 8 | timeout: 15000, 9 | headers: { 10 | 'Content-Type': 'application/json', 11 | }, 12 | }) 13 | 14 | // 请求拦截器 15 | request.interceptors.request.use( 16 | (config: AxiosRequestConfig) => { 17 | // 从localStorage获取token 18 | const token = localStorage.getItem('token') 19 | if (token) { 20 | config.headers = { 21 | ...config.headers, 22 | Authorization: `Bearer ${token}`, 23 | } 24 | } 25 | return config 26 | }, 27 | (error) => { 28 | return Promise.reject(error) 29 | } 30 | ) 31 | 32 | // 响应拦截器 33 | request.interceptors.response.use( 34 | (response: AxiosResponse) => { 35 | const { data } = response 36 | // FastAPI直接返回数据,不需要额外的包装 37 | return data 38 | }, 39 | (error) => { 40 | if (error.response) { 41 | const { status, data } = error.response 42 | const errorMessage = data?.detail || data?.message || '请求失败' 43 | 44 | switch (status) { 45 | case 401: 46 | // 未授权,清除token并跳转到登录页 47 | localStorage.removeItem('token') 48 | message.error('登录已过期,请重新登录') 49 | // 不直接跳转,让用户手动处理 50 | break 51 | case 403: 52 | message.error('没有权限访问该资源') 53 | break 54 | case 404: 55 | message.error('请求的资源不存在') 56 | break 57 | case 422: 58 | // FastAPI验证错误 59 | const validationError = data?.detail?.[0] 60 | if (validationError) { 61 | message.error(`${validationError.loc?.join('.')} ${validationError.msg}`) 62 | } else { 63 | message.error(errorMessage) 64 | } 65 | break 66 | case 500: 67 | message.error('服务器错误') 68 | break 69 | default: 70 | message.error(errorMessage) 71 | } 72 | } else { 73 | message.error('网络连接失败') 74 | } 75 | return Promise.reject(error) 76 | } 77 | ) 78 | 79 | export default request -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | # PostgreSQL数据库 5 | postgres: 6 | image: postgres:17.5 7 | container_name: ai-interview-postgres 8 | environment: 9 | POSTGRES_DB: ai_interview_helper 10 | POSTGRES_USER: postgres 11 | POSTGRES_PASSWORD: root 12 | POSTGRES_INITDB_ARGS: "--encoding=UTF8 --lc-collate=C --lc-ctype=C" 13 | volumes: 14 | - postgres_data:/var/lib/postgresql/data 15 | - ./backend/init.sql:/docker-entrypoint-initdb.d/init.sql 16 | ports: 17 | - "5432:5432" 18 | networks: 19 | - ai-interview-network 20 | healthcheck: 21 | test: ["CMD-SHELL", "pg_isready -U postgres -d ai_interview_helper"] 22 | interval: 30s 23 | timeout: 10s 24 | retries: 5 25 | 26 | # Redis缓存 27 | redis: 28 | image: redis:7.4-alpine 29 | container_name: ai-interview-redis 30 | ports: 31 | - "6379:6379" 32 | networks: 33 | - ai-interview-network 34 | command: redis-server --appendonly yes 35 | volumes: 36 | - redis_data:/data 37 | healthcheck: 38 | test: ["CMD", "redis-cli", "ping"] 39 | interval: 30s 40 | timeout: 10s 41 | retries: 5 42 | 43 | # 后端API服务 44 | backend: 45 | build: 46 | context: ./backend 47 | dockerfile: Dockerfile 48 | container_name: ai-interview-backend 49 | environment: 50 | DATABASE_URL: postgresql://postgres:root@postgres:5432/ai_interview_helper 51 | REDIS_URL: redis://redis:6379 52 | SECRET_KEY: ai-interview-helper-secret-key-please-change-in-production-2024 53 | ALGORITHM: HS256 54 | ACCESS_TOKEN_EXPIRE_MINUTES: 30 55 | CORS_ORIGINS: '["http://localhost:5173", "http://localhost:3000"]' 56 | ports: 57 | - "9000:9000" 58 | networks: 59 | - ai-interview-network 60 | depends_on: 61 | postgres: 62 | condition: service_healthy 63 | redis: 64 | condition: service_healthy 65 | volumes: 66 | - ./backend:/app 67 | restart: unless-stopped 68 | healthcheck: 69 | test: ["CMD", "curl", "-f", "http://localhost:9000/health"] 70 | interval: 30s 71 | timeout: 10s 72 | retries: 5 73 | start_period: 40s 74 | 75 | volumes: 76 | postgres_data: 77 | driver: local 78 | redis_data: 79 | driver: local 80 | 81 | networks: 82 | ai-interview-network: 83 | driver: bridge -------------------------------------------------------------------------------- /docker-compose.full.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | # PostgreSQL数据库 5 | postgres: 6 | image: postgres:17.5 7 | container_name: ai-interview-postgres-full 8 | environment: 9 | POSTGRES_DB: ai_interview_helper 10 | POSTGRES_USER: postgres 11 | POSTGRES_PASSWORD: root 12 | POSTGRES_INITDB_ARGS: "--encoding=UTF8 --lc-collate=C --lc-ctype=C" 13 | volumes: 14 | - postgres_full_data:/var/lib/postgresql/data 15 | - ./backend/init.sql:/docker-entrypoint-initdb.d/init.sql 16 | ports: 17 | - "5462:5432" 18 | networks: 19 | - ai-interview-full-network 20 | 21 | # Redis缓存 22 | redis: 23 | image: redis:7.4-alpine 24 | container_name: ai-interview-redis-full 25 | ports: 26 | - "6389:6379" 27 | networks: 28 | - ai-interview-full-network 29 | command: redis-server --appendonly yes 30 | volumes: 31 | - redis_full_data:/data 32 | 33 | # 后端API服务 34 | backend: 35 | build: 36 | context: ./backend 37 | dockerfile: Dockerfile 38 | container_name: ai-interview-backend-full 39 | environment: 40 | DATABASE_URL: postgresql://postgres:root@postgres:5432/ai_interview_helper 41 | REDIS_URL: redis://redis:6379 42 | SECRET_KEY: ai-interview-helper-secret-key-full-2024 43 | ALGORITHM: HS256 44 | ACCESS_TOKEN_EXPIRE_MINUTES: 30 45 | CORS_ORIGINS: '["http://localhost:5173", "http://localhost:3000", "http://frontend:5173"]' 46 | DEBUG: "True" 47 | ports: 48 | - "9000:9000" 49 | networks: 50 | - ai-interview-full-network 51 | depends_on: 52 | - postgres 53 | - redis 54 | volumes: 55 | - ./backend:/app:rw 56 | - /app/venv 57 | restart: unless-stopped 58 | 59 | # 前端服务 (可选) 60 | frontend: 61 | build: 62 | context: ./frontend 63 | dockerfile: Dockerfile.dev 64 | container_name: ai-interview-frontend-full 65 | environment: 66 | VITE_API_BASE_URL: http://localhost:9000 67 | ports: 68 | - "5173:5173" 69 | networks: 70 | - ai-interview-full-network 71 | depends_on: 72 | - backend 73 | volumes: 74 | - ./frontend:/app:rw 75 | - /app/node_modules 76 | restart: unless-stopped 77 | stdin_open: true 78 | tty: true 79 | 80 | volumes: 81 | postgres_full_data: 82 | driver: local 83 | redis_full_data: 84 | driver: local 85 | 86 | networks: 87 | ai-interview-full-network: 88 | driver: bridge -------------------------------------------------------------------------------- /backend/WINDOWS_TROUBLESHOOT.md: -------------------------------------------------------------------------------- 1 | # Windows环境故障排除指南 2 | 3 | ## 常见问题解决方案 4 | 5 | ### 1. UTF-8编码错误 6 | 7 | **错误信息**: `UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd6` 8 | 9 | **解决方案**: 10 | ```bash 11 | # 方案1: 使用Windows专用启动脚本 12 | python run_server_windows.py 13 | 14 | # 方案2: 设置环境变量后启动 15 | set PYTHONIOENCODING=utf-8 16 | set PYTHONUTF8=1 17 | python run_server.py 18 | 19 | # 方案3: 在PowerShell中设置 20 | $env:PYTHONIOENCODING="utf-8" 21 | $env:PYTHONUTF8="1" 22 | python run_server.py 23 | ``` 24 | 25 | ### 2. 数据库连接问题 26 | 27 | **测试连接**: 28 | ```bash 29 | python test_connection.py 30 | ``` 31 | 32 | **常见问题**: 33 | - PostgreSQL服务未启动 34 | - 密码错误(应该是'root') 35 | - 端口5432被占用 36 | - 数据库不存在 37 | 38 | **解决步骤**: 39 | 1. 确认PostgreSQL容器正在运行 40 | 2. 检查端口占用: `netstat -an | findstr 5432` 41 | 3. 验证密码是否为'root' 42 | 4. 手动创建数据库(如果需要) 43 | 44 | ### 3. 依赖安装问题 45 | 46 | **Windows下推荐使用**: 47 | ```bash 48 | # 创建虚拟环境 49 | python -m venv venv 50 | 51 | # 激活虚拟环境 (CMD) 52 | venv\Scripts\activate 53 | 54 | # 激活虚拟环境 (PowerShell) 55 | venv\Scripts\Activate.ps1 56 | 57 | # 安装依赖 58 | pip install -r requirements.txt 59 | ``` 60 | 61 | ### 4. 路径问题 62 | 63 | **避免使用包含中文的路径**: 64 | - 项目路径不要包含中文字符 65 | - 用户名不要包含中文字符 66 | - 使用英文路径如: `C:\projects\ai-interview-helper` 67 | 68 | ### 5. 终端选择 69 | 70 | **推荐使用**: 71 | 1. Windows Terminal (首选) 72 | 2. PowerShell 73 | 3. CMD 74 | 75 | **避免使用**: 76 | - Git Bash (可能有编码问题) 77 | - WSL内的终端 (路径映射问题) 78 | 79 | ### 6. Docker数据库配置 80 | 81 | **确保PostgreSQL容器配置正确**: 82 | ```bash 83 | docker run --name postgres-ai-interview ^ 84 | -e POSTGRES_PASSWORD=root ^ 85 | -p 5432:5432 ^ 86 | -d postgres:17.5 87 | ``` 88 | 89 | **检查容器状态**: 90 | ```bash 91 | docker ps 92 | docker logs postgres-ai-interview 93 | ``` 94 | 95 | ### 7. 防火墙和网络 96 | 97 | **Windows防火墙**: 98 | - 确保允许Python访问网络 99 | - 检查5432端口是否被阻止 100 | 101 | **网络测试**: 102 | ```bash 103 | # 测试端口连通性 104 | telnet localhost 5432 105 | ``` 106 | 107 | ## 快速诊断脚本 108 | 109 | 运行诊断脚本检查环境: 110 | ```bash 111 | python fix_windows_encoding.py 112 | ``` 113 | 114 | ## 完整启动流程 (Windows) 115 | 116 | 1. **启动PostgreSQL容器** 117 | ```bash 118 | docker run --name postgres-ai-interview -e POSTGRES_PASSWORD=root -p 5432:5432 -d postgres:17.5 119 | ``` 120 | 121 | 2. **测试数据库连接** 122 | ```bash 123 | python test_connection.py 124 | ``` 125 | 126 | 3. **启动后端服务** 127 | ```bash 128 | python run_server_windows.py 129 | ``` 130 | 131 | 4. **验证服务** 132 | 访问: http://localhost:9000/docs 133 | 134 | ## 联系支持 135 | 136 | 如果问题仍然存在,请提供以下信息: 137 | - Windows版本 138 | - Python版本 139 | - 错误完整堆栈信息 140 | - 数据库连接测试结果 -------------------------------------------------------------------------------- /backend/run_server_windows.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Windows环境专用的FastAPI服务器启动脚本 4 | 解决编码和路径问题 5 | """ 6 | 7 | import os 8 | import sys 9 | import locale 10 | 11 | def setup_windows_environment(): 12 | """设置Windows环境变量""" 13 | # 设置编码环境变量 14 | os.environ['PYTHONIOENCODING'] = 'utf-8' 15 | os.environ['PYTHONUTF8'] = '1' 16 | os.environ['LANG'] = 'en_US.UTF-8' 17 | os.environ['LC_ALL'] = 'en_US.UTF-8' 18 | 19 | # 设置Python路径 20 | current_dir = os.path.dirname(os.path.abspath(__file__)) 21 | if current_dir not in sys.path: 22 | sys.path.insert(0, current_dir) 23 | 24 | def check_environment(): 25 | """检查环境配置""" 26 | print("🔍 检查环境配置...") 27 | print(f"Python版本: {sys.version}") 28 | print(f"系统平台: {sys.platform}") 29 | print(f"文件系统编码: {sys.getfilesystemencoding()}") 30 | print(f"默认编码: {locale.getpreferredencoding()}") 31 | print(f"当前目录: {os.getcwd()}") 32 | 33 | def test_database_connection(): 34 | """测试数据库连接""" 35 | try: 36 | print("🗄️ 测试数据库连接...") 37 | from dotenv import load_dotenv 38 | load_dotenv(encoding='utf-8') 39 | 40 | # 检查环境变量 41 | db_url = os.getenv("DATABASE_URL") 42 | if not db_url: 43 | print("❌ 未找到DATABASE_URL环境变量") 44 | return False 45 | 46 | print(f"数据库URL: {db_url}") 47 | 48 | # 测试连接 49 | from app.database import engine 50 | with engine.connect() as conn: 51 | result = conn.execute("SELECT 1").scalar() 52 | print("✅ 数据库连接成功") 53 | return True 54 | except Exception as e: 55 | print(f"❌ 数据库连接失败: {e}") 56 | print("\n💡 解决建议:") 57 | print("1. 确保PostgreSQL服务正在运行") 58 | print("2. 检查端口5432是否被占用") 59 | print("3. 验证数据库密码是否为'root'") 60 | print("4. 尝试手动创建数据库: CREATE DATABASE ai_interview_helper;") 61 | return False 62 | 63 | def start_server(): 64 | """启动FastAPI服务器""" 65 | try: 66 | import uvicorn 67 | print("🚀 启动FastAPI服务器...") 68 | 69 | uvicorn.run( 70 | "main:app", 71 | host="0.0.0.0", 72 | port=9000, 73 | reload=True, 74 | log_level="info", 75 | access_log=True 76 | ) 77 | except Exception as e: 78 | print(f"❌ 服务器启动失败: {e}") 79 | return False 80 | 81 | if __name__ == "__main__": 82 | print("🔧 Windows环境AI面试助手后端启动器") 83 | print("=" * 50) 84 | 85 | # 设置环境 86 | setup_windows_environment() 87 | 88 | # 检查环境 89 | check_environment() 90 | 91 | # 测试数据库连接 92 | if not test_database_connection(): 93 | print("\n❌ 数据库连接失败,无法启动服务器") 94 | print("请先解决数据库连接问题") 95 | sys.exit(1) 96 | 97 | # 启动服务器 98 | print("\n🎯 所有检查通过,正在启动服务器...") 99 | start_server() -------------------------------------------------------------------------------- /backend/app/routers/auth.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | from fastapi import APIRouter, Depends, HTTPException, status 3 | from fastapi.security import HTTPBearer 4 | from sqlalchemy.orm import Session 5 | 6 | from ..database import get_db 7 | from ..models import User as UserModel 8 | from ..schemas import UserCreate, UserLogin, User, Token 9 | from ..auth import ( 10 | authenticate_user, 11 | create_access_token, 12 | get_password_hash, 13 | get_current_active_user, 14 | ACCESS_TOKEN_EXPIRE_MINUTES 15 | ) 16 | 17 | router = APIRouter() 18 | security = HTTPBearer() 19 | 20 | @router.post("/register", response_model=User) 21 | async def register_user(user: UserCreate, db: Session = Depends(get_db)): 22 | # Check if user already exists 23 | db_user = db.query(UserModel).filter(UserModel.username == user.username).first() 24 | if db_user: 25 | raise HTTPException( 26 | status_code=status.HTTP_400_BAD_REQUEST, 27 | detail="Username already registered" 28 | ) 29 | 30 | # Check if email already exists (if provided) 31 | if user.email: 32 | db_user = db.query(UserModel).filter(UserModel.email == user.email).first() 33 | if db_user: 34 | raise HTTPException( 35 | status_code=status.HTTP_400_BAD_REQUEST, 36 | detail="Email already registered" 37 | ) 38 | 39 | # Create new user 40 | hashed_password = get_password_hash(user.password) 41 | db_user = UserModel( 42 | username=user.username, 43 | email=user.email, 44 | hashed_password=hashed_password 45 | ) 46 | db.add(db_user) 47 | db.commit() 48 | db.refresh(db_user) 49 | 50 | return db_user 51 | 52 | @router.post("/login", response_model=Token) 53 | async def login_user(user_credentials: UserLogin, db: Session = Depends(get_db)): 54 | user = authenticate_user(db, user_credentials.username, user_credentials.password) 55 | if not user: 56 | raise HTTPException( 57 | status_code=status.HTTP_401_UNAUTHORIZED, 58 | detail="Incorrect username or password", 59 | headers={"WWW-Authenticate": "Bearer"}, 60 | ) 61 | 62 | access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) 63 | access_token = create_access_token( 64 | data={"sub": user.username}, expires_delta=access_token_expires 65 | ) 66 | 67 | return {"access_token": access_token, "token_type": "bearer"} 68 | 69 | @router.get("/me", response_model=User) 70 | async def read_users_me(current_user: UserModel = Depends(get_current_active_user)): 71 | return current_user 72 | 73 | @router.post("/logout") 74 | async def logout_user(current_user: UserModel = Depends(get_current_active_user)): 75 | # In a real application, you might want to blacklist the token 76 | # For now, we'll just return a success message 77 | return {"detail": "Successfully logged out"} -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # AI Interview Helper - Frontend 2 | 3 | 这是一个基于React + TypeScript + Vite的AI面试助手前端项目,支持实时语音转写、AI智能回答和完整的用户认证系统。 4 | 5 | ## ✨ 主要功能 6 | 7 | - 🎤 **实时语音转写** - 集成科大讯飞RTasr API 8 | - 🤖 **AI智能回答** - DeepSeek AI驱动的面试回答建议 9 | - 👤 **用户认证** - 完整的登录/注册/JWT认证系统 10 | - 💾 **会话管理** - 面试记录持久化和历史回顾 11 | - 🔄 **状态同步** - 实时前后端数据同步 12 | 13 | ## 🚀 快速开始 14 | 15 | ### 环境要求 16 | - Node.js 16+ 17 | - 后端API服务运行在 http://localhost:9000 18 | 19 | ### 安装和启动 20 | ```bash 21 | # 安装依赖 22 | npm install 23 | 24 | # 启动开发服务器 25 | npm run dev 26 | 27 | # 访问 http://localhost:5173 28 | ``` 29 | 30 | ### 默认测试账户 31 | - 用户名:`test` 32 | - 密码:`test1234` 33 | 34 | ## 🔧 API密钥配置 35 | 36 | 使用前需要在应用内配置以下API密钥: 37 | 38 | 1. **科大讯飞语音转写** 39 | - 申请地址:https://www.xfyun.cn/ 40 | - 在API配置页面填入APPID和APIKEY 41 | 42 | 2. **DeepSeek AI** 43 | - 申请地址:https://platform.deepseek.com/ 44 | - 在API配置页面填入APIKEY 45 | 46 | ## 🛠️ 技术栈 47 | 48 | - **React 19** + TypeScript + Vite 49 | - **Ant Design** - 企业级UI组件库 50 | - **Zustand** - 轻量级状态管理 (interviewStore, authStore) 51 | - **Axios** - HTTP客户端和JWT认证拦截 52 | - **WebAudio API** - 实时音频处理 53 | - **Styled Components** - CSS-in-JS样式 54 | 55 | ## 开发环境要求 56 | 57 | - Node.js >= 16.0.0 58 | - npm >= 7.0.0 59 | 60 | ## 安装依赖 61 | 62 | ```bash 63 | npm install 64 | ``` 65 | 66 | ## 开发命令 67 | 68 | ```bash 69 | # 启动开发服务器 70 | npm run dev 71 | 72 | # 构建生产版本 73 | npm run build 74 | 75 | # 预览生产构建 76 | npm run preview 77 | 78 | # 运行类型检查 79 | npm run type-check 80 | 81 | # 运行代码检查 82 | npm run lint 83 | ``` 84 | 85 | ## 项目结构 86 | 87 | ``` 88 | src/ 89 | ├── api/ # API请求和拦截器 90 | ├── components/ # 可复用组件 91 | ├── hooks/ # 自定义Hooks 92 | ├── layouts/ # 布局组件 93 | ├── pages/ # 页面组件 94 | ├── store/ # 状态管理 95 | ├── types/ # TypeScript类型定义 96 | ├── utils/ # 工具函数 97 | └── App.tsx # 应用入口 98 | ``` 99 | 100 | ## 🔧 环境变量 101 | 102 | 在项目根目录创建 `.env` 文件: 103 | 104 | ```env 105 | VITE_API_BASE_URL=http://localhost:9000 106 | ``` 107 | 108 | ### API集成说明 109 | 110 | - **后端API**: `/api/auth/*`, `/api/sessions/*` 111 | - **语音转写**: 科大讯飞RTasr WebSocket API 112 | - **AI回答**: DeepSeek Coder API 113 | 114 | ## 开发规范 115 | 116 | - 使用TypeScript进行开发 117 | - 遵循ESLint规则 118 | - 使用Prettier进行代码格式化 119 | - 组件使用函数式组件和Hooks 120 | - 状态管理使用Zustand 121 | - UI组件使用Ant Design 122 | 123 | ## 贡献指南 124 | 125 | 1. Fork本仓库 126 | 2. 创建特性分支 (`git checkout -b feature/AmazingFeature`) 127 | 3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) 128 | 4. 推送到分支 (`git push origin feature/AmazingFeature`) 129 | 5. 创建Pull Request 130 | 131 | ## 🤝 贡献指南 132 | 133 | 1. Fork本仓库 134 | 2. 创建特性分支 (`git checkout -b feature/AmazingFeature`) 135 | 3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) 136 | 4. 推送到分支 (`git push origin feature/AmazingFeature`) 137 | 5. 创建Pull Request 138 | 139 | ## 📄 更多信息 140 | 141 | 查看项目根目录的 [README.md](../README.md) 获取完整的项目说明和后端配置指南。 -------------------------------------------------------------------------------- /backend/app/auth.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from typing import Optional 3 | from jose import JWTError, jwt 4 | from passlib.context import CryptContext 5 | from fastapi import Depends, HTTPException, status 6 | from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials 7 | from sqlalchemy.orm import Session 8 | import os 9 | from dotenv import load_dotenv 10 | 11 | from .database import get_db 12 | from .models import User 13 | from .schemas import TokenData 14 | 15 | load_dotenv() 16 | 17 | SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-change-this-in-production") 18 | ALGORITHM = os.getenv("ALGORITHM", "HS256") 19 | ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30")) 20 | 21 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 22 | security = HTTPBearer() 23 | 24 | def verify_password(plain_password, hashed_password): 25 | return pwd_context.verify(plain_password, hashed_password) 26 | 27 | def get_password_hash(password): 28 | return pwd_context.hash(password) 29 | 30 | def get_user(db: Session, username: str): 31 | return db.query(User).filter(User.username == username).first() 32 | 33 | def authenticate_user(db: Session, username: str, password: str): 34 | user = get_user(db, username) 35 | if not user: 36 | return False 37 | if not verify_password(password, user.hashed_password): 38 | return False 39 | return user 40 | 41 | def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): 42 | to_encode = data.copy() 43 | if expires_delta: 44 | expire = datetime.utcnow() + expires_delta 45 | else: 46 | expire = datetime.utcnow() + timedelta(minutes=15) 47 | to_encode.update({"exp": expire}) 48 | encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) 49 | return encoded_jwt 50 | 51 | async def get_current_user( 52 | credentials: HTTPAuthorizationCredentials = Depends(security), 53 | db: Session = Depends(get_db) 54 | ): 55 | credentials_exception = HTTPException( 56 | status_code=status.HTTP_401_UNAUTHORIZED, 57 | detail="Could not validate credentials", 58 | headers={"WWW-Authenticate": "Bearer"}, 59 | ) 60 | 61 | try: 62 | token = credentials.credentials 63 | payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) 64 | username: str = payload.get("sub") 65 | if username is None: 66 | raise credentials_exception 67 | token_data = TokenData(username=username) 68 | except JWTError: 69 | raise credentials_exception 70 | 71 | user = get_user(db, username=token_data.username) 72 | if user is None: 73 | raise credentials_exception 74 | return user 75 | 76 | async def get_current_active_user(current_user: User = Depends(get_current_user)): 77 | if not current_user.is_active: 78 | raise HTTPException(status_code=400, detail="Inactive user") 79 | return current_user -------------------------------------------------------------------------------- /frontend/src/store/apiConfigStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { devtools, persist } from 'zustand/middleware' 3 | import { immer } from 'zustand/middleware/immer'; 4 | /** 5 | * API配置接口 6 | * @property apiProvider - API提供商名称 7 | * @property apiKey - API密钥 8 | * @property baseUrl - API基础URL 9 | * @property seq - 配置序号 10 | * @property model - 使用的模型名称(可选) 11 | * @property maxTokens - 最大token数(可选) 12 | * @property temperature - 温度参数(可选) 13 | * @property description - 配置描述(可选) 14 | * @property createdAt - 创建时间 15 | * @property updatedAt - 更新时间 16 | */ 17 | export interface ApiConfig { 18 | apiProvider: string 19 | apiKey: string 20 | baseUrl: string 21 | seq: number 22 | model?: string 23 | maxTokens?: number 24 | temperature?: number 25 | isDefault?: boolean 26 | description?: string 27 | createdAt: string 28 | updatedAt: string 29 | } 30 | 31 | /** 32 | * API配置状态接口 33 | * @property apiConfigs - API配置列表 34 | * @property addConfig - 添加新配置的方法 35 | * @property updateConfig - 更新配置的方法 36 | * @property deleteConfig - 删除配置的方法 37 | */ 38 | interface ApiConfigState { 39 | apiConfigs: ApiConfig[] 40 | addConfig: (config: Omit) => void 41 | updateConfig: (seq: number, config: Partial) => void 42 | deleteConfig: (seq: number) => void 43 | setDefaultConfig: (seq: number) => void 44 | } 45 | 46 | // 默认的DeepSeek配置 47 | const defaultDeepSeekConfig: Omit = { 48 | apiProvider: 'deepseek', 49 | apiKey: '', // 需要用户在设置中配置 50 | baseUrl: 'https://api.deepseek.com', 51 | seq: 1, 52 | model: 'deepseek-chat', 53 | maxTokens: 2000, 54 | temperature: 0.7, 55 | description: 'DeepSeek API 默认配置(请在设置中填入您的API密钥)' 56 | } 57 | 58 | export const useApiConfigStore = create()( 59 | devtools( 60 | persist( 61 | immer((set) => ({ 62 | apiConfigs: [{ 63 | ...defaultDeepSeekConfig, 64 | createdAt: new Date().toISOString(), 65 | updatedAt: new Date().toISOString(), 66 | }], 67 | addConfig: (config) => set((state) => { 68 | state.apiConfigs.push({ 69 | ...config, 70 | createdAt: new Date().toISOString(), 71 | updatedAt: new Date().toISOString(), 72 | }) 73 | }), 74 | updateConfig: (seq, config) => set((state) => { 75 | state.apiConfigs = state.apiConfigs.map((item) => 76 | item.seq === seq 77 | ? { 78 | ...item, 79 | ...config, 80 | updatedAt: new Date().toISOString(), 81 | } 82 | : item 83 | ) 84 | }), 85 | deleteConfig: (seq) => set((state) => { 86 | state.apiConfigs = state.apiConfigs.filter((item) => item.seq !== seq) 87 | }), 88 | setDefaultConfig: (seq) => set((state) => { 89 | state.apiConfigs = state.apiConfigs.map((item) => ({ 90 | ...item, 91 | isDefault: item.seq === seq, 92 | })) 93 | }), 94 | })), 95 | { 96 | name: 'api-config-storage', 97 | } 98 | ), 99 | { 100 | name: 'api-config-store', 101 | trace: true, 102 | traceLimit: 25, 103 | } 104 | ) 105 | ) -------------------------------------------------------------------------------- /DOCKER_SETUP.md: -------------------------------------------------------------------------------- 1 | # Docker环境设置指南 2 | 3 | ## WSL环境下的Docker配置 4 | 5 | ### 1. 确保Docker Desktop运行 6 | 7 | 在Windows上启动Docker Desktop,确保Docker服务正在运行。 8 | 9 | ### 2. WSL中的Docker配置 10 | 11 | 确保在WSL中可以访问Docker: 12 | 13 | ```bash 14 | # 检查Docker是否可用 15 | docker --version 16 | docker ps 17 | 18 | # 如果遇到权限问题,可能需要重启Docker Desktop 19 | # 或者在Windows中重新启动Docker Desktop的WSL集成 20 | ``` 21 | 22 | ### 3. 启动AI面试助手 23 | 24 | #### 方法一:一键启动开发环境 25 | ```bash 26 | # 给脚本执行权限 27 | chmod +x scripts/dev-start.sh 28 | 29 | # 启动开发环境 30 | ./scripts/dev-start.sh 31 | ``` 32 | 33 | #### 方法二:手动启动 34 | ```bash 35 | # 停止可能存在的旧容器 36 | docker-compose -f docker-compose.dev.yml down 37 | 38 | # 构建并启动 39 | docker-compose -f docker-compose.dev.yml up --build -d 40 | 41 | # 等待数据库启动 42 | sleep 15 43 | 44 | # 初始化默认用户 45 | docker-compose -f docker-compose.dev.yml exec backend python init_default_user.py 46 | ``` 47 | 48 | ### 4. 验证服务 49 | 50 | ```bash 51 | # 查看容器状态 52 | docker-compose -f docker-compose.dev.yml ps 53 | 54 | # 查看日志 55 | docker-compose -f docker-compose.dev.yml logs 56 | 57 | # 测试API 58 | curl http://localhost:9000/health 59 | ``` 60 | 61 | ### 5. 常见问题解决 62 | 63 | #### Docker权限问题 64 | ```bash 65 | # 确保Docker Desktop在Windows中运行 66 | # 重启Docker Desktop的WSL集成: 67 | # Docker Desktop -> Settings -> Resources -> WSL Integration 68 | ``` 69 | 70 | #### 端口占用问题 71 | ```bash 72 | # 检查端口占用 73 | netstat -tulpn | grep :9000 74 | netstat -tulpn | grep :5432 75 | 76 | # 如果端口被占用,停止占用进程或修改docker-compose.dev.yml中的端口映射 77 | ``` 78 | 79 | #### 数据库连接问题 80 | ```bash 81 | # 查看PostgreSQL容器日志 82 | docker-compose -f docker-compose.dev.yml logs postgres 83 | 84 | # 进入PostgreSQL容器检查 85 | docker-compose -f docker-compose.dev.yml exec postgres psql -U postgres -d ai_interview_helper 86 | ``` 87 | 88 | ## 开发工作流 89 | 90 | ### 日常开发 91 | 92 | 1. **启动环境**: 93 | ```bash 94 | ./scripts/dev-start.sh 95 | ``` 96 | 97 | 2. **查看日志**: 98 | ```bash 99 | docker-compose -f docker-compose.dev.yml logs -f backend 100 | ``` 101 | 102 | 3. **进入容器调试**: 103 | ```bash 104 | docker-compose -f docker-compose.dev.yml exec backend bash 105 | ``` 106 | 107 | 4. **重启服务**: 108 | ```bash 109 | docker-compose -f docker-compose.dev.yml restart backend 110 | ``` 111 | 112 | 5. **停止环境**: 113 | ```bash 114 | docker-compose -f docker-compose.dev.yml down 115 | ``` 116 | 117 | ### 数据管理 118 | 119 | 1. **重置数据库**: 120 | ```bash 121 | docker-compose -f docker-compose.dev.yml down -v 122 | ./scripts/dev-start.sh 123 | ``` 124 | 125 | 2. **备份数据**: 126 | ```bash 127 | docker-compose -f docker-compose.dev.yml exec postgres pg_dump -U postgres ai_interview_helper > backup.sql 128 | ``` 129 | 130 | 3. **恢复数据**: 131 | ```bash 132 | docker-compose -f docker-compose.dev.yml exec -T postgres psql -U postgres ai_interview_helper < backup.sql 133 | ``` 134 | 135 | ## 生产环境部署 136 | 137 | ```bash 138 | # 启动生产环境 139 | ./scripts/docker-start.sh 140 | 141 | # 或手动启动 142 | docker-compose up --build -d 143 | ``` 144 | 145 | ## 故障排除 146 | 147 | ### 检查Docker状态 148 | ```bash 149 | # 检查Docker服务 150 | docker info 151 | 152 | # 检查容器状态 153 | docker ps -a 154 | 155 | # 检查网络 156 | docker network ls 157 | ``` 158 | 159 | ### 清理Docker环境 160 | ```bash 161 | # 停止所有容器 162 | docker-compose -f docker-compose.dev.yml down 163 | 164 | # 清理未使用的镜像 165 | docker image prune 166 | 167 | # 清理未使用的卷(谨慎使用) 168 | docker volume prune 169 | ``` 170 | 171 | ### 重新构建镜像 172 | ```bash 173 | # 强制重新构建 174 | docker-compose -f docker-compose.dev.yml build --no-cache 175 | 176 | # 重新启动 177 | docker-compose -f docker-compose.dev.yml up -d 178 | ``` -------------------------------------------------------------------------------- /frontend/src/api/utils/xfyunRtasr/index.esm.js: -------------------------------------------------------------------------------- 1 | function e(e,t,r,o){return new(r||(r=Promise))((function(n,a){function i(e){try{s(o.next(e))}catch(e){a(e)}}function u(e){try{s(o.throw(e))}catch(e){a(e)}}function s(e){var t;e.done?n(e.value):(t=e.value,t instanceof r?t:new r((function(e){e(t)}))).then(i,u)}s((o=o.apply(e,t||[])).next())}))}function t(e,t){var r,o,n,a,i={label:0,sent:function(){if(1&n[0])throw n[1];return n[1]},trys:[],ops:[]};return a={next:u(0),throw:u(1),return:u(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function u(u){return function(s){return function(u){if(r)throw new TypeError("Generator is already executing.");for(;a&&(a=0,u[0]&&(i=0)),i;)try{if(r=1,o&&(n=2&u[0]?o.return:u[0]?o.throw||((n=o.return)&&n.call(o),0):o.next)&&!(n=n.call(o,u[1])).done)return n;switch(o=0,n&&(u=[2&u[0],n.value]),u[0]){case 0:case 1:n=u;break;case 4:return i.label++,{value:u[1],done:!1};case 5:i.label++,o=u[1],u=[0];continue;case 7:u=i.ops.pop(),i.trys.pop();continue;default:if(!(n=i.trys,(n=n.length>0&&n[n.length-1])||6!==u[0]&&2!==u[0])){i=0;continue}if(3===u[0]&&(!n||u[1]>n[0]&&u[1], 19 | label: '用户中心', 20 | children: [ 21 | { key: 'home', icon: , label: 首页 }, 22 | ], 23 | }, 24 | { 25 | key: 'interview', 26 | icon: , 27 | label: '面试管理', 28 | children: [ 29 | { key: 'interview-new', label: 新的面试 }, 30 | { key: 'interview-record', label: 面试记录 }, 31 | ], 32 | }, 33 | { 34 | key: 'help', 35 | icon: , 36 | label: 帮助, 37 | }, 38 | { 39 | key: 'settings', 40 | icon: , 41 | label: 系统设置, 42 | }, 43 | ] 44 | 45 | const userMenu = ( 46 | 47 | }> 48 | 49 | 50 | 51 | ) 52 | 53 | const MainLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => { 54 | const location = useLocation() 55 | const navigate = useNavigate() 56 | // 选中菜单项 57 | const selectedKeys = React.useMemo(() => { 58 | const path = location.pathname 59 | if (path === '/') return ['home'] 60 | if (path.startsWith('/interview/new')) return ['interview-new'] 61 | if (path.startsWith('/interview/record')) return ['interview-record'] 62 | if (path.startsWith('/help')) return ['help'] 63 | if (path.startsWith('/settings')) return ['settings'] 64 | return [] 65 | }, [location.pathname]) 66 | 67 | return ( 68 | 69 | {/* 顶部Header横跨全屏 */} 70 |
71 |
72 | logo 73 |
74 | 75 |
76 | } style={{ marginRight: 8 }} /> 77 | 17***171 78 |
79 |
80 |
81 | {/* 主体区域:左侧Menu,右侧内容 */} 82 | 83 | 84 | 91 | 92 | 93 | {children} 94 | 95 | 96 | 97 | ) 98 | } 99 | 100 | export default MainLayout -------------------------------------------------------------------------------- /frontend/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/test_connection.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | 数据库连接测试脚本 - Windows兼容版本 4 | """ 5 | 6 | import os 7 | import sys 8 | import psycopg2 9 | from urllib.parse import urlparse 10 | 11 | # 设置编码 12 | if sys.platform == "win32": 13 | os.environ['PYTHONIOENCODING'] = 'utf-8' 14 | os.environ['PYTHONUTF8'] = '1' 15 | 16 | def test_direct_connection(): 17 | """直接测试PostgreSQL连接""" 18 | try: 19 | print("🔗 直接测试PostgreSQL连接...") 20 | 21 | # 使用基本连接参数 22 | conn = psycopg2.connect( 23 | host="localhost", 24 | port=5432, 25 | database="postgres", # 先连接到默认数据库 26 | user="postgres", 27 | password="root" 28 | ) 29 | 30 | cursor = conn.cursor() 31 | cursor.execute("SELECT version();") 32 | version = cursor.fetchone() 33 | print(f"✅ PostgreSQL版本: {version[0]}") 34 | 35 | # 检查目标数据库是否存在 36 | cursor.execute("SELECT 1 FROM pg_database WHERE datname = 'ai_interview_helper';") 37 | db_exists = cursor.fetchone() 38 | 39 | if not db_exists: 40 | print("🔧 创建目标数据库...") 41 | # 注意:CREATE DATABASE不能在事务中执行 42 | conn.autocommit = True 43 | cursor.execute("CREATE DATABASE ai_interview_helper;") 44 | print("✅ 数据库创建成功") 45 | else: 46 | print("✅ 目标数据库已存在") 47 | 48 | cursor.close() 49 | conn.close() 50 | 51 | # 测试连接到目标数据库 52 | print("🔗 测试连接到目标数据库...") 53 | conn = psycopg2.connect( 54 | host="localhost", 55 | port=5432, 56 | database="ai_interview_helper", 57 | user="postgres", 58 | password="root" 59 | ) 60 | 61 | cursor = conn.cursor() 62 | cursor.execute("SELECT 1;") 63 | result = cursor.fetchone() 64 | print("✅ 目标数据库连接成功") 65 | 66 | cursor.close() 67 | conn.close() 68 | 69 | return True 70 | 71 | except psycopg2.Error as e: 72 | print(f"❌ PostgreSQL连接错误: {e}") 73 | return False 74 | except Exception as e: 75 | print(f"❌ 其他错误: {e}") 76 | return False 77 | 78 | def test_sqlalchemy_connection(): 79 | """测试SQLAlchemy连接""" 80 | try: 81 | print("🔗 测试SQLAlchemy连接...") 82 | 83 | from dotenv import load_dotenv 84 | load_dotenv(encoding='utf-8') 85 | 86 | from sqlalchemy import create_engine, text 87 | 88 | db_url = os.getenv("DATABASE_URL", "postgresql://postgres:root@localhost:5432/ai_interview_helper") 89 | print(f"数据库URL: {db_url}") 90 | 91 | engine = create_engine(db_url) 92 | 93 | with engine.connect() as conn: 94 | result = conn.execute(text("SELECT 1")).scalar() 95 | print("✅ SQLAlchemy连接成功") 96 | return True 97 | 98 | except Exception as e: 99 | print(f"❌ SQLAlchemy连接失败: {e}") 100 | return False 101 | 102 | def main(): 103 | print("🧪 数据库连接测试工具") 104 | print("=" * 40) 105 | 106 | # 检查psycopg2是否可用 107 | try: 108 | import psycopg2 109 | print("✅ psycopg2模块可用") 110 | except ImportError: 111 | print("❌ psycopg2模块未安装") 112 | print("请运行: pip install psycopg2-binary") 113 | return False 114 | 115 | # 直接连接测试 116 | if not test_direct_connection(): 117 | print("\n💡 直接连接失败,可能的原因:") 118 | print("1. PostgreSQL服务未启动") 119 | print("2. 密码不正确(应该是'root')") 120 | print("3. 端口5432被占用或无法访问") 121 | print("4. 防火墙阻止连接") 122 | return False 123 | 124 | # SQLAlchemy连接测试 125 | if not test_sqlalchemy_connection(): 126 | print("❌ SQLAlchemy连接失败") 127 | return False 128 | 129 | print("\n🎉 所有测试通过!数据库连接正常") 130 | return True 131 | 132 | if __name__ == "__main__": 133 | if main(): 134 | print("\n✅ 可以安全启动后端服务器") 135 | else: 136 | print("\n❌ 请先解决数据库连接问题") -------------------------------------------------------------------------------- /frontend/public/xfyunRtasr/processor.worker.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";function t(t){return function(t){if(Array.isArray(t))return e(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,r){if(!t)return;if("string"==typeof t)return e(t,r);var i=Object.prototype.toString.call(t).slice(8,-1);"Object"===i&&t.constructor&&(i=t.constructor.name);if("Map"===i||"Set"===i)return Array.from(t);if("Arguments"===i||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i))return e(t,r)}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function e(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,i=new Array(e);r0&&this.toSampleRate>0&&this.channels>0))throw new Error("Invalid settings specified for the resampler.");this.fromSampleRate==this.toSampleRate?(this.resampler=this.bypassResampler,this.ratioWeight=1):(this.fromSampleRate0){for(;f<1;f+=a)for(n=1-(o=f%1),r=0;r0){for(var s=this.ratioWeight,a=0,f=0;f0&&n=(o=1+n-p))){for(f=0;f=self.frameSize&&(self.postMessage({frameBuffer:self.transData(this.frameBuffer),isLastFrame:!1}),self.frameBuffer=[]),!0;u&&self.postMessage({frameBuffer:self.transData(u),isLastFrame:!1})}}}(); 2 | -------------------------------------------------------------------------------- /frontend/src/store/authStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { devtools } from 'zustand/middleware' 3 | import { immer } from 'zustand/middleware/immer' 4 | import * as authApi from '../api/auth' 5 | 6 | export interface User { 7 | id: number 8 | username: string 9 | email?: string 10 | is_active: boolean 11 | created_at: string 12 | updated_at: string 13 | } 14 | 15 | interface AuthState { 16 | user: User | null 17 | token: string | null 18 | isLoading: boolean 19 | isAuthenticated: boolean 20 | login: (username: string, password: string) => Promise 21 | register: (username: string, password: string, email?: string) => Promise 22 | logout: () => void 23 | initAuth: () => Promise 24 | } 25 | 26 | export const useAuthStore = create()( 27 | devtools( 28 | immer((set, get) => ({ 29 | user: null, 30 | token: localStorage.getItem('token'), 31 | isLoading: false, 32 | isAuthenticated: false, 33 | 34 | login: async (username, password) => { 35 | try { 36 | set((state) => { 37 | state.isLoading = true 38 | }) 39 | 40 | const response = await authApi.login({ username, password }) 41 | const { access_token } = response 42 | 43 | // 保存token 44 | localStorage.setItem('token', access_token) 45 | 46 | // 获取用户信息 47 | const user = await authApi.getCurrentUser() 48 | 49 | set((state) => { 50 | state.token = access_token 51 | state.user = user 52 | state.isAuthenticated = true 53 | state.isLoading = false 54 | }) 55 | 56 | return true 57 | } catch (error) { 58 | console.error('Login failed:', error) 59 | set((state) => { 60 | state.isLoading = false 61 | }) 62 | return false 63 | } 64 | }, 65 | 66 | register: async (username, password, email) => { 67 | try { 68 | set((state) => { 69 | state.isLoading = true 70 | }) 71 | 72 | await authApi.register({ username, password, email }) 73 | 74 | // 注册成功后自动登录 75 | const loginSuccess = await get().login(username, password) 76 | 77 | set((state) => { 78 | state.isLoading = false 79 | }) 80 | 81 | return loginSuccess 82 | } catch (error) { 83 | console.error('Registration failed:', error) 84 | set((state) => { 85 | state.isLoading = false 86 | }) 87 | return false 88 | } 89 | }, 90 | 91 | logout: () => { 92 | // 调用后端登出API(可选) 93 | authApi.logout().catch(console.error) 94 | 95 | // 清除本地状态 96 | localStorage.removeItem('token') 97 | set((state) => { 98 | state.user = null 99 | state.token = null 100 | state.isAuthenticated = false 101 | }) 102 | }, 103 | 104 | initAuth: async () => { 105 | const token = localStorage.getItem('token') 106 | if (!token) { 107 | set((state) => { 108 | state.isAuthenticated = false 109 | }) 110 | return 111 | } 112 | 113 | try { 114 | set((state) => { 115 | state.isLoading = true 116 | }) 117 | 118 | const user = await authApi.getCurrentUser() 119 | 120 | set((state) => { 121 | state.token = token 122 | state.user = user 123 | state.isAuthenticated = true 124 | state.isLoading = false 125 | }) 126 | } catch (error) { 127 | console.error('Auth initialization failed:', error) 128 | // Token无效,清除 129 | localStorage.removeItem('token') 130 | set((state) => { 131 | state.token = null 132 | state.user = null 133 | state.isAuthenticated = false 134 | state.isLoading = false 135 | }) 136 | } 137 | }, 138 | })), 139 | { 140 | name: 'auth-store', 141 | trace: true, 142 | traceLimit: 25, 143 | } 144 | ) 145 | ) -------------------------------------------------------------------------------- /memory-bank/project-context.md: -------------------------------------------------------------------------------- 1 | # 项目背景与上下文 2 | 3 | ## 项目简要说明 4 | 5 | ### 项目概述 6 | AI面试助手是一个跨端应用,基于React、ANTD和zustand开发,旨在帮助用户回答面试问题。系统能够将用户和面试官的语音实时转换为文本,并利用AI模型提供回答和反馈。 7 | 8 | ### 项目目标 9 | - 开发一个用户友好的面试助手应用 10 | - 实现实时语音转文字功能 11 | - 集成AI模型提供智能回答 12 | - 支持多种AI模型选择 13 | - 提供用户认证和数据持久化 14 | 15 | ### 核心功能 16 | 17 | #### 音频转文字 18 | - 全屏面试界面设计 19 | - 前端获取麦克风音频流 20 | - 实时音频流处理 21 | - 将音频流发送给科大讯飞实时语音转写API 22 | - 获取带时间戳的对话文字 23 | - 流式显示在前端并转发给后端 24 | - 支持角色分离显示 25 | - 实现错误自动恢复 26 | 27 | #### 回答AI 28 | - 前端将文字+预置Prompt拼接 29 | - 发送给基于deepseek-coder模型的API 30 | - 获取AI回复 31 | - 以打字机形式流式显示在前端并转发给后端 32 | - 支持多模型切换 33 | - 提供个性化回答 34 | - 实现智能问题分析 35 | 36 | ### 技术迭代路线 37 | 38 | #### v0.1 - 初始化前端和后端 [已完成] 39 | - 前端:完成面试会话页UI和音频转文字功能 40 | - 实现基础布局和路由 41 | - 集成科大讯飞API 42 | - 实现实时语音转写 43 | 44 | #### v0.2 - 完成AI回答功能 [已完成] 45 | - 前端:实现AI回答功能 46 | - 开发全屏面试界面 47 | - 优化实时转写体验 48 | - 实现对话气泡组件 49 | - 集成DeepSeek API 50 | - 添加错误处理机制 51 | 52 | #### v0.3 - 添加后端和数据持久化 [已完成] ✅ 53 | - 后端:使用FastAPI,PostgreSQL数据库 54 | - 实现用户登录/注册/登出功能(JWT认证) 55 | - 所有接口需要传用户token进行鉴权 56 | - 添加默认用户:test/test1234 57 | - 实现会话历史存储 58 | - 部署后端服务到9000端口 59 | - 完整的数据库模型设计 60 | 61 | #### v0.4 - 增强模型选择能力 [计划中] 62 | - 前端:添加模型选择器 63 | - 支持多种AI模型 64 | - 实现模型配置界面 65 | - 优化模型切换体验 66 | 67 | #### v0.5 - 完善用户系统 [计划中] 68 | - 前端:添加登录/注册/登出页面 69 | - 实现登录后跳转到面试页功能 70 | - 完善用户个人中心 71 | - 优化用户体验 72 | 73 | ## 产品上下文 74 | 75 | ### 目标用户群体 76 | - **求职者**: 希望提升面试技巧的个人 77 | - **面试教练**: 专业面试培训师 78 | - **面试培训机构**: 提供面试培训服务的机构 79 | - **技能提升者**: 对提升面试技巧感兴趣的个人 80 | 81 | ### 用户核心需求 82 | - 希望在无压力环境下练习面试 83 | - 获取即时反馈和改进建议 84 | - 记录和回顾面试历史 85 | - 选择不同类型的模型辅助练习 86 | - 实时语音转写和对话显示 87 | - 流畅的面试体验 88 | - 可靠的错误处理机制 89 | 90 | ### 用户旅程 91 | 92 | #### 新建面试会话 93 | 1. 用户注册/登录应用 94 | 2. 进入面试会话历史页面, 选择新建面试会话 95 | 3. 进入全屏面试会话页面 96 | 4. 点击"开始面试"按钮 97 | 5. 授权麦克风访问权限 98 | 6. 开始录音并回答面试问题 99 | 7. 实时查看语音转写结果 100 | 8. 获取AI实时反馈 101 | 9. 保存面试记录供后续回顾 102 | 103 | #### 查看历史面试会话 104 | 1. 用户注册/登录应用 105 | 2. 进入面试会话历史页面, 选择查看历史会话 106 | 3. 进入历史会话页面, 查看历史会话记录 107 | 4. 可查看完整的对话内容 108 | 5. 可查看语音转写记录 109 | 6. 可查看AI反馈内容 110 | 111 | #### 配置大模型API 112 | 1. 用户注册/登录应用 113 | 2. 进入面试会话历史页面, 在左侧Menu选择API配置 114 | 3. 在API配置页, 用户添加大模型供应商, baseUrl, APIKey等信息 115 | 4. 配置科大讯飞语音转写API参数 116 | 5. 测试API连接状态 117 | 6. 保存配置信息 118 | 119 | ### 核心功能特性 120 | 121 | #### 实时语音转写 122 | - 全屏面试界面 123 | - 实时音频流处理 124 | - 高准确度转写 125 | - 角色分离显示 126 | - 错误自动恢复 127 | 128 | #### AI回答功能 129 | - 智能问题分析 130 | - 个性化回答生成 131 | - 流式文本显示 132 | - 打字机效果 133 | - 多模型支持 134 | 135 | #### 用户体验 136 | - 简洁直观的界面 137 | - 流畅的交互体验 138 | - 实时的状态反馈 139 | - 可靠的错误处理 140 | - 完整的使用引导 141 | 142 | ## 技术上下文 143 | 144 | ### 前端技术栈 145 | - **React + TypeScript**: 现代化前端开发框架 146 | - **ANTD**: UI组件库,提供丰富的组件 147 | - **Zustand**: 轻量级状态管理库 148 | - **Axios**: HTTP请求库 149 | - **Styled-components**: CSS-in-JS样式管理 150 | - **Vite**: 快速构建工具 151 | - **OpenAPI-typescript-codegen**: API代码生成工具 152 | - **WebSocket**: 实时通信协议 153 | - **WebAudio API**: 音频处理接口 154 | 155 | ### 后端技术栈 156 | - **FastAPI**: 高性能Python Web框架 157 | - **PostgreSQL**: 关系型数据库 158 | - **JWT**: 用户认证机制 159 | - **SQLAlchemy**: 对象关系映射(ORM) 160 | - **Pydantic**: 数据验证库 161 | - **Uvicorn**: ASGI服务器 162 | - **WebSocket**: 实时通信支持 163 | - **Redis**: 缓存数据库 164 | 165 | ### 外部API集成 166 | 167 | #### 科大讯飞语音转写API 168 | - 实时音频流转文字 169 | - 支持中文识别 170 | - 需要API密钥和鉴权 171 | - WebSocket通信 172 | - 支持角色分离 173 | - 实时转写结果 174 | 175 | #### DeepSeek Coder API 176 | - 基于DeepSeek-coder模型 177 | - 用于生成AI回答 178 | - 支持流式输出 179 | - 支持多模型切换 180 | - 提供个性化回答 181 | - 智能问题分析 182 | 183 | ### 开发环境要求 184 | - **Node.js 16+** (前端开发) 185 | - **Python 3.9+** (后端开发) 186 | - **PostgreSQL 14+** (数据库) 187 | - **Redis 6+** (缓存) 188 | - **Git** (版本控制) 189 | 190 | ### 性能优化策略 191 | 192 | #### 前端优化 193 | - 组件懒加载 194 | - 虚拟滚动 195 | - 状态管理优化 196 | - WebSocket连接池 197 | - 音频流处理优化 198 | 199 | #### 后端优化 200 | - 数据库索引 201 | - 缓存策略 202 | - 并发处理 203 | - WebSocket连接管理 204 | - 错误重试机制 205 | 206 | ### 安全措施 207 | 208 | #### 前端安全 209 | - API密钥保护 210 | - 输入验证 211 | - XSS防护 212 | - CSRF防护 213 | - 错误处理 214 | 215 | #### 后端安全 216 | - JWT认证 217 | - 请求限流 218 | - 数据加密 219 | - SQL注入防护 220 | - 日志审计 221 | 222 | ### 技术决策与理由 223 | 224 | #### 关键技术选择 225 | - **React**: 成熟的前端生态系统,组件化开发 226 | - **TypeScript**: 类型安全,提高代码质量 227 | - **Zustand**: 轻量级状态管理,避免Redux复杂性 228 | - **FastAPI**: 高性能,自动API文档生成 229 | - **PostgreSQL**: 强大的关系型数据库,支持复杂查询 230 | - **WebSocket**: 实时通信需求,低延迟 231 | - **WebAudio API**: 浏览器原生音频处理能力 232 | 233 | #### 架构约束 234 | - 前后端分离架构 235 | - RESTful API设计原则 236 | - 响应式设计要求 237 | - 跨浏览器兼容性 238 | - 实时性能要求 239 | - 安全性考虑 -------------------------------------------------------------------------------- /frontend/src/utils/questionDetection.ts: -------------------------------------------------------------------------------- 1 | import type { MeetingMessage } from '../store/interviewStore' 2 | 3 | /** 4 | * 滑动窗口策略:获取最近的消息 5 | * @param messages 所有消息 6 | * @param timeWindow 时间窗口(毫秒,默认30秒) 7 | * @param countWindow 消息数量窗口(默认30条) 8 | * @returns 最近的消息数组 9 | */ 10 | export const getRecentMessages = ( 11 | messages: MeetingMessage[], 12 | timeWindow = 30000, 13 | countWindow = 30 14 | ): MeetingMessage[] => { 15 | const now = Date.now() 16 | const timeFiltered = messages.filter(m => now - m.timestamp <= timeWindow) 17 | const countFiltered = messages.slice(-countWindow) 18 | 19 | // 取两个条件的交集,选择更小的集合 20 | return timeFiltered.length <= countFiltered.length ? timeFiltered : countFiltered 21 | } 22 | 23 | /** 24 | * 稳定的系统提示词(用于KV Cache优化) 25 | */ 26 | export const STABLE_SYSTEM_PROMPT = `你是专业的AI面试助手,负责识别和回答面试中的新问题。 27 | 28 | ## 容错处理规则: 29 | 1. **语音识别错误容忍**:忽略明显的识别错误,如"note js"理解为"Node.js","浏览器使用的引擎"可能被错误分割 30 | 2. **角色混淆处理**:根据内容语义判断真实角色,不完全依赖标记的role 31 | 3. **内容碎片拼接**:将语义相关的碎片内容理解为完整问题,标点符号可能分离到下一句 32 | 4. **标点符号容错**:问号可能被识别为句号,根据语义判断疑问意图 33 | 34 | ## 核心任务: 35 | - 识别消息中的**新问题**(标记为[新消息]的内容) 36 | - 忽略已回答的问题(标记为[历史]的内容仅作参考) 37 | - 如果发现新问题,提供简洁专业的回答 38 | - 如果没有新问题,回复"未检测到新问题" 39 | 40 | ## 回答要求: 41 | - 回答要简洁、专业、针对性强 42 | - 避免重复回答已处理过的问题 43 | - 语气自然,适合面试场景 44 | - 重点关注技术问题和面试相关问题` 45 | 46 | /** 47 | * 为DeepSeek API准备消息,优化KV Cache命中率 48 | * @param messages 所有消息 49 | * @returns 准备好的AI消息和新消息ID列表 50 | */ 51 | export const prepareMessagesForAI = (messages: MeetingMessage[]) => { 52 | const recentMessages = getRecentMessages(messages) 53 | 54 | // 分离已问过的和新的消息 55 | const askedMessages = recentMessages.filter(m => m.isAsked) 56 | const newMessages = recentMessages.filter(m => !m.isAsked) 57 | 58 | // 如果没有新消息,直接返回 59 | if (newMessages.length === 0) { 60 | return { aiMessages: [], newMessageIds: [] } 61 | } 62 | 63 | // 构建稳定的前缀(system prompt + 历史消息) 64 | const stablePrefix = [ 65 | { 66 | role: 'system' as const, 67 | content: STABLE_SYSTEM_PROMPT 68 | } 69 | ] 70 | 71 | // 已处理的历史消息作为稳定前缀(按时间戳排序保持稳定) 72 | const sortedAskedMessages = askedMessages 73 | .sort((a, b) => a.timestamp - b.timestamp) 74 | .map(m => ({ 75 | role: m.role === 'user' ? 'user' as const : 'assistant' as const, 76 | content: `[历史] ${m.content}` 77 | })) 78 | 79 | // 新消息追加在后面(按时间戳排序) 80 | const sortedNewMessages = newMessages 81 | .sort((a, b) => a.timestamp - b.timestamp) 82 | .map(m => ({ 83 | role: m.role === 'user' ? 'user' as const : 'assistant' as const, 84 | content: `[新消息] ${m.content}` 85 | })) 86 | 87 | // 为了最大化缓存命中,保持稳定的消息结构 88 | const aiMessages = [ 89 | ...stablePrefix, 90 | ...sortedAskedMessages, 91 | ...sortedNewMessages 92 | ] 93 | 94 | return { 95 | aiMessages, 96 | newMessageIds: newMessages.map(m => m.id) 97 | } 98 | } 99 | 100 | // 全局状态:追踪静音持续时间 101 | let lastAudioActiveTime = Date.now() 102 | let silenceStartTime: number | null = null 103 | 104 | /** 105 | * 更新音频活跃状态并追踪静音时间 106 | * @param audioActive 当前音频是否活跃 107 | */ 108 | export const updateAudioActiveState = (audioActive: boolean) => { 109 | const now = Date.now() 110 | 111 | if (audioActive) { 112 | // 有声音,重置静音追踪 113 | lastAudioActiveTime = now 114 | silenceStartTime = null 115 | } else { 116 | // 无声音,开始或继续追踪静音 117 | if (silenceStartTime === null) { 118 | silenceStartTime = now 119 | } 120 | } 121 | } 122 | 123 | /** 124 | * 检查是否应该触发AI问题检测 125 | * @param lastDataTime 最后收到数据的时间 126 | * @returns 是否应该触发 127 | */ 128 | export const shouldTriggerQuestionDetection = (lastDataTime: number): boolean => { 129 | // 注意:具体的触发条件已经在预检查和主动检查中实现 130 | // 如果调用到这里,说明已经满足了触发条件 131 | // 这里保留作为统一的检查入口,未来可以添加其他条件 132 | 133 | // TODO: 未来可以在这里添加其他触发条件,例如: 134 | // - 关键词检测 135 | // - 语义分析 136 | // - 用户自定义规则 137 | 138 | return true // 直接返回true,因为调用时已经过滤了条件 139 | } 140 | 141 | /** 142 | * 防抖处理类 143 | */ 144 | export class QuestionDetectionDebouncer { 145 | private timer: number | null = null 146 | private readonly delay: number 147 | 148 | constructor(delay = 500) { 149 | this.delay = delay 150 | } 151 | 152 | /** 153 | * 防抖执行函数 154 | * @param callback 要执行的回调函数 155 | */ 156 | debounce(callback: () => void): void { 157 | if (this.timer) { 158 | clearTimeout(this.timer) 159 | } 160 | 161 | this.timer = window.setTimeout(() => { 162 | callback() 163 | this.timer = null 164 | }, this.delay) 165 | } 166 | 167 | /** 168 | * 取消待执行的防抖 169 | */ 170 | cancel(): void { 171 | if (this.timer) { 172 | clearTimeout(this.timer) 173 | this.timer = null 174 | } 175 | } 176 | 177 | /** 178 | * 清理资源 179 | */ 180 | destroy(): void { 181 | this.cancel() 182 | } 183 | } -------------------------------------------------------------------------------- /frontend/src/pages/InterviewNew.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Form, Select, Button, Typography, Divider, Space, Alert } from 'antd' 3 | import { useInterviewStore } from '../store/interviewStore' 4 | import { useNavigate } from 'react-router-dom' 5 | 6 | const { Title, Text, Link } = Typography 7 | 8 | const InterviewNew: React.FC = () => { 9 | const [form] = Form.useForm() 10 | const interviewConfig = useInterviewStore((s) => s.interviewConfig) 11 | const setInterviewConfig = useInterviewStore((s) => s.setInterviewConfig) 12 | const navigate = useNavigate() 13 | 14 | // 表单初始值同步store 15 | React.useEffect(() => { 16 | form.setFieldsValue(interviewConfig) 17 | }, [form, interviewConfig]) 18 | 19 | // 表单项变更同步store 20 | const handleValuesChange = (changed: any, all: any) => { 21 | setInterviewConfig(changed) 22 | } 23 | 24 | // 新增:前往面试按钮点击事件 25 | const handleGoToMeeting = () => { 26 | const values = form.getFieldsValue() 27 | const params = new URLSearchParams({ 28 | region: values.region || '', 29 | job: values.job || '', 30 | lang: values.lang || '' 31 | }).toString() 32 | navigate(`/interview/meeting?${params}`) 33 | } 34 | 35 | return ( 36 |
37 |
44 | {/* 地区和岗位 */} 45 |
46 | 请选择面试地区和岗位 47 | 48 | 49 | 54 | 55 | 56 | 63 | 64 | 没有想要的岗位?联系小助手一键添加 65 | 66 |
67 | 68 | {/* 技术定制 */} 69 |
70 | 技术定制 71 | 72 | 73 | 80 | 81 | 82 | 86 | 87 | 当前问答库为空 88 | 89 | 90 |
91 | 92 | {/* 模式选择 */} 93 |
94 | 模式选择 95 | 你并未上传简历,AI将无法基于简历回答,去上传} 97 | type="info" 98 | showIcon 99 | style={{ marginBottom: 16 }} 100 | /> 101 |
102 | 103 | {/* 开始面试 */} 104 |
105 | 106 | 107 | 108 |
109 | 110 |
111 | ) 112 | } 113 | 114 | export default InterviewNew -------------------------------------------------------------------------------- /backend/app/routers/sessions.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, HTTPException, status 2 | from sqlalchemy.orm import Session 3 | from sqlalchemy import func 4 | from typing import List 5 | import uuid 6 | 7 | from ..database import get_db 8 | from ..models import User as UserModel, InterviewSession, SessionMessage 9 | from ..schemas import ( 10 | InterviewSessionCreate, 11 | InterviewSession as InterviewSessionSchema, 12 | InterviewSessionList, 13 | SessionMessageCreate, 14 | SessionMessage as SessionMessageSchema 15 | ) 16 | from ..auth import get_current_active_user 17 | 18 | router = APIRouter() 19 | 20 | @router.post("/", response_model=InterviewSessionSchema) 21 | async def create_session( 22 | session: InterviewSessionCreate, 23 | current_user: UserModel = Depends(get_current_active_user), 24 | db: Session = Depends(get_db) 25 | ): 26 | # Generate unique session ID 27 | session_id = str(uuid.uuid4()) 28 | 29 | # Create new session 30 | db_session = InterviewSession( 31 | user_id=current_user.id, 32 | session_id=session_id, 33 | title=session.title or f"Interview Session - {session_id[:8]}" 34 | ) 35 | db.add(db_session) 36 | db.commit() 37 | db.refresh(db_session) 38 | 39 | return db_session 40 | 41 | @router.get("/", response_model=List[InterviewSessionList]) 42 | async def get_user_sessions( 43 | current_user: UserModel = Depends(get_current_active_user), 44 | db: Session = Depends(get_db) 45 | ): 46 | # Get sessions with message count 47 | sessions = db.query( 48 | InterviewSession, 49 | func.count(SessionMessage.id).label('message_count') 50 | ).outerjoin(SessionMessage).filter( 51 | InterviewSession.user_id == current_user.id 52 | ).group_by(InterviewSession.id).order_by( 53 | InterviewSession.updated_at.desc() 54 | ).all() 55 | 56 | result = [] 57 | for session, message_count in sessions: 58 | result.append({ 59 | "id": session.id, 60 | "session_id": session.session_id, 61 | "title": session.title, 62 | "created_at": session.created_at, 63 | "updated_at": session.updated_at, 64 | "message_count": message_count or 0 65 | }) 66 | 67 | return result 68 | 69 | @router.get("/{session_id}", response_model=InterviewSessionSchema) 70 | async def get_session( 71 | session_id: str, 72 | current_user: UserModel = Depends(get_current_active_user), 73 | db: Session = Depends(get_db) 74 | ): 75 | session = db.query(InterviewSession).filter( 76 | InterviewSession.session_id == session_id, 77 | InterviewSession.user_id == current_user.id 78 | ).first() 79 | 80 | if not session: 81 | raise HTTPException( 82 | status_code=status.HTTP_404_NOT_FOUND, 83 | detail="Session not found" 84 | ) 85 | 86 | return session 87 | 88 | @router.post("/{session_id}/messages", response_model=SessionMessageSchema) 89 | async def add_message_to_session( 90 | session_id: str, 91 | message: SessionMessageCreate, 92 | current_user: UserModel = Depends(get_current_active_user), 93 | db: Session = Depends(get_db) 94 | ): 95 | # Get session 96 | session = db.query(InterviewSession).filter( 97 | InterviewSession.session_id == session_id, 98 | InterviewSession.user_id == current_user.id 99 | ).first() 100 | 101 | if not session: 102 | raise HTTPException( 103 | status_code=status.HTTP_404_NOT_FOUND, 104 | detail="Session not found" 105 | ) 106 | 107 | # Create message 108 | db_message = SessionMessage( 109 | session_id=session.id, 110 | message_type=message.message_type, 111 | content=message.content, 112 | message_metadata=message.message_metadata 113 | ) 114 | db.add(db_message) 115 | db.commit() 116 | db.refresh(db_message) 117 | 118 | # Update session timestamp 119 | from datetime import datetime 120 | session.updated_at = datetime.utcnow() 121 | db.commit() 122 | 123 | return db_message 124 | 125 | @router.get("/{session_id}/messages", response_model=List[SessionMessageSchema]) 126 | async def get_session_messages( 127 | session_id: str, 128 | current_user: UserModel = Depends(get_current_active_user), 129 | db: Session = Depends(get_db) 130 | ): 131 | # Get session 132 | session = db.query(InterviewSession).filter( 133 | InterviewSession.session_id == session_id, 134 | InterviewSession.user_id == current_user.id 135 | ).first() 136 | 137 | if not session: 138 | raise HTTPException( 139 | status_code=status.HTTP_404_NOT_FOUND, 140 | detail="Session not found" 141 | ) 142 | 143 | messages = db.query(SessionMessage).filter( 144 | SessionMessage.session_id == session.id 145 | ).order_by(SessionMessage.timestamp.asc()).all() 146 | 147 | return messages 148 | 149 | @router.delete("/{session_id}") 150 | async def delete_session( 151 | session_id: str, 152 | current_user: UserModel = Depends(get_current_active_user), 153 | db: Session = Depends(get_db) 154 | ): 155 | session = db.query(InterviewSession).filter( 156 | InterviewSession.session_id == session_id, 157 | InterviewSession.user_id == current_user.id 158 | ).first() 159 | 160 | if not session: 161 | raise HTTPException( 162 | status_code=status.HTTP_404_NOT_FOUND, 163 | detail="Session not found" 164 | ) 165 | 166 | # Delete all messages first 167 | db.query(SessionMessage).filter(SessionMessage.session_id == session.id).delete() 168 | 169 | # Delete session 170 | db.delete(session) 171 | db.commit() 172 | 173 | return {"detail": "Session deleted successfully"} -------------------------------------------------------------------------------- /frontend/src/api/deepseek.ts: -------------------------------------------------------------------------------- 1 | import { useApiConfigStore } from '../store/apiConfigStore' 2 | import { useInterviewStore } from '@/store/interviewStore' 3 | import { message as antdMessage } from 'antd' 4 | 5 | /** 6 | * DeepSeek API 响应接口 7 | */ 8 | interface DeepSeekResponse { 9 | id: string 10 | object: string 11 | created: number 12 | model: string 13 | choices: Array<{ 14 | index: number 15 | message: { 16 | role: string 17 | content: string 18 | } 19 | finish_reason: string 20 | }> 21 | usage: { 22 | prompt_tokens: number 23 | completion_tokens: number 24 | total_tokens: number 25 | } 26 | } 27 | 28 | /** 29 | * DeepSeek API 请求参数接口 30 | */ 31 | interface DeepSeekRequest { 32 | model: string 33 | messages: Array<{ 34 | role: 'user' | 'assistant' | 'system' 35 | content: string 36 | }> 37 | temperature?: number 38 | max_tokens?: number 39 | stream?: boolean 40 | } 41 | 42 | const DEEPSEEK_BASE_URL = 'https://api.deepseek.com' 43 | 44 | /** 45 | * 获取DeepSeek配置 46 | * @returns DeepSeek配置 47 | */ 48 | const getDeepSeekConfig = () => { 49 | const { apiConfigs } = useApiConfigStore.getState() 50 | const config = apiConfigs.find(config => config.apiProvider === 'deepseek') 51 | if (!config) { 52 | throw new Error('DeepSeek configuration not found') 53 | } 54 | return config 55 | } 56 | 57 | /** 58 | * 调用 DeepSeek API 进行对话 59 | * @param messages 对话消息数组 60 | * @param options 可选参数 61 | * @returns Promise 62 | */ 63 | export const chatWithDeepSeek = async ( 64 | messages: DeepSeekRequest['messages'], 65 | options?: { 66 | temperature?: number 67 | max_tokens?: number 68 | stream?: boolean 69 | } 70 | ): Promise => { 71 | const config = getDeepSeekConfig() 72 | const response = await fetch(`${DEEPSEEK_BASE_URL}/chat/completions`, { 73 | method: 'POST', 74 | headers: { 75 | 'Content-Type': 'application/json', 76 | 'Authorization': `Bearer ${config.apiKey}` 77 | }, 78 | body: JSON.stringify({ 79 | model: config.model || 'deepseek-chat', 80 | messages, 81 | temperature: options?.temperature ?? config.temperature, 82 | max_tokens: options?.max_tokens ?? config.maxTokens, 83 | stream: options?.stream ?? false 84 | }) 85 | }) 86 | if (!response.ok) { 87 | throw new Error(`HTTP error! status: ${response.status}`) 88 | } 89 | return await response.json() 90 | } 91 | 92 | /** 93 | * 流式调用 DeepSeek API 94 | * @param messages 对话消息数组 95 | * @param onMessage 消息回调函数 96 | * @param options 可选参数 97 | */ 98 | export const streamChatWithDeepSeek = async ( 99 | messages: DeepSeekRequest['messages'], 100 | onMessage: (parsed: any) => void, 101 | options?: { 102 | temperature?: number 103 | max_tokens?: number 104 | } 105 | ): Promise => { 106 | const config = getDeepSeekConfig() 107 | const response = await fetch(`${DEEPSEEK_BASE_URL}/chat/completions`, { 108 | method: 'POST', 109 | headers: { 110 | 'Content-Type': 'application/json', 111 | 'Authorization': `Bearer ${config.apiKey}` 112 | }, 113 | body: JSON.stringify({ 114 | model: config.model || 'deepseek-chat', 115 | messages, 116 | temperature: options?.temperature ?? config.temperature, 117 | max_tokens: options?.max_tokens ?? config.maxTokens, 118 | stream: true 119 | }) 120 | }) 121 | if (!response.ok) { 122 | throw new Error(`HTTP error! status: ${response.status}`) 123 | } 124 | const reader = response.body?.getReader() 125 | if (!reader) { 126 | throw new Error('No response body') 127 | } 128 | const decoder = new TextDecoder() 129 | let buffer = '' 130 | while (true) { 131 | const { done, value } = await reader.read() 132 | if (done) break 133 | buffer += decoder.decode(value, { stream: true }) 134 | const lines = buffer.split('\n') 135 | buffer = lines.pop() || '' 136 | for (const line of lines) { 137 | if (line.startsWith('data: ')) { 138 | const data = line.slice(6) 139 | if (data === '[DONE]') continue 140 | try { 141 | const parsed = JSON.parse(data) 142 | onMessage(parsed) 143 | } catch (e) { 144 | // 忽略解析错误 145 | } 146 | } 147 | } 148 | } 149 | } 150 | 151 | /** 152 | * 智能问题检测和回答处理(KV Cache优化版本) 153 | */ 154 | export const handleQuestionDetected = async () => { 155 | const store = useInterviewStore.getState() 156 | const { upsertAnswer, markMessagesAsAsked, messages } = store 157 | 158 | // 动态导入工具函数以避免循环依赖 159 | const { prepareMessagesForAI } = await import('../utils/questionDetection') 160 | 161 | // 准备消息(KV Cache优化) 162 | const { aiMessages, newMessageIds } = prepareMessagesForAI(messages) 163 | 164 | // 如果没有新消息,直接返回 165 | if (aiMessages.length === 0 || newMessageIds.length === 0) { 166 | return 167 | } 168 | 169 | let fullMsg = '' 170 | let answerId = '' 171 | 172 | try { 173 | await streamChatWithDeepSeek(aiMessages, (parsed) => { 174 | const id = parsed.id || (parsed.choices && parsed.choices[0]?.id) || 'deepseek-' + Date.now() 175 | const content = parsed.choices?.[0]?.delta?.content || '' 176 | 177 | if (!answerId) answerId = id 178 | 179 | if (content) { 180 | fullMsg += content 181 | // 实时更新回答 182 | upsertAnswer({ 183 | id: answerId, 184 | message: fullMsg, 185 | created: Date.now(), 186 | question: newMessageIds.map(id => 187 | messages.find(m => m.id === id)?.content || '' 188 | ).join(' ') 189 | }) 190 | } 191 | }) 192 | 193 | // 标记消息为已处理,优化后续KV Cache命中 194 | markMessagesAsAsked(newMessageIds) 195 | 196 | } catch (err: any) { 197 | // DeepSeek API错误处理 198 | const reason = err?.choices?.[0]?.finish_reason || err?.message || 'AI接口调用失败' 199 | antdMessage.error(reason) 200 | console.error('DeepSeek API Error:', err) 201 | } 202 | } 203 | 204 | /** 205 | * 兼容性:保持原有的handleQuestionEnd方法 206 | * @deprecated 请使用 handleQuestionDetection 方法 207 | */ 208 | export const handleQuestionEnd = handleQuestionDetected -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AI Interview Helper 2 | 3 | AI面试助手是一个基于React的智能面试准备工具,提供实时语音转文字、AI智能回答建议和完整的面试会话管理功能。 4 | 5 | ## ✨ 主要功能 6 | 7 | - 🎤 **实时语音转写** - 基于科大讯飞RTasr API的高精度语音识别 8 | - 🤖 **AI智能回答** - 集成DeepSeek AI提供面试问题的智能回答建议 9 | - 💾 **会话持久化** - 完整的面试记录保存和历史回顾 10 | - 👤 **用户认证** - 基于JWT的安全用户登录系统 11 | - 🔄 **实时同步** - 前后端实时数据同步和状态管理 12 | 13 | ## 🏗️ 技术架构 14 | 15 | ### 前端技术栈 16 | - **React 19** + TypeScript + Vite 17 | - **Ant Design** - 企业级UI组件库 18 | - **Zustand** - 轻量级状态管理 19 | - **Axios** - HTTP客户端和请求拦截 20 | - **WebAudio API** - 实时音频处理 21 | 22 | ### 后端技术栈 23 | - **FastAPI** - 高性能Python Web框架 24 | - **PostgreSQL** - 关系型数据库 25 | - **Redis** - 缓存和会话存储 26 | - **JWT** - 用户认证和授权 27 | - **SQLAlchemy** - ORM数据库操作 28 | 29 | ## 🚀 快速开始 30 | 31 | ### 环境要求 32 | 33 | - Node.js 16+ 34 | - Python 3.9+ 35 | - PostgreSQL 17+ (推荐Docker) 36 | - Redis 7+ (推荐Docker) 37 | 38 | ### 1. 使用Docker启动(推荐) 39 | 40 | #### 方法一:一键启动所有服务(推荐) 41 | ```bash 42 | # 启动生产环境 43 | chmod +x scripts/docker-start.sh 44 | ./scripts/docker-start.sh 45 | 46 | # 或启动开发环境(支持代码热重载) 47 | chmod +x scripts/dev-start.sh 48 | ./scripts/dev-start.sh 49 | ``` 50 | 51 | #### 方法二:使用docker-compose 52 | ```bash 53 | # 生产环境 54 | docker-compose up --build -d 55 | 56 | # 开发环境 57 | docker-compose -f docker-compose.dev.yml up --build -d 58 | ``` 59 | 60 | #### 方法三:单独启动数据库服务 61 | ```bash 62 | # PostgreSQL 63 | docker run --name postgres-ai-interview \ 64 | -e POSTGRES_PASSWORD=root \ 65 | -p 5432:5432 \ 66 | -d postgres:17.5 67 | 68 | # Redis 69 | docker run --name redis-ai-interview \ 70 | -p 6379:6379 \ 71 | -d redis:7.4-alpine 72 | ``` 73 | 74 | ### 2. 传统方式启动后端(可选) 75 | 76 | > 💡 **推荐使用上面的Docker方式启动,可避免环境配置问题** 77 | 78 | #### Linux/macOS/WSL环境: 79 | ```bash 80 | cd backend 81 | chmod +x setup.sh 82 | ./setup.sh # 自动创建.env配置文件 83 | python run_server.py 84 | ``` 85 | 86 | #### Windows环境: 87 | ```bash 88 | cd backend 89 | python test_connection.py # 测试数据库连接 90 | python run_server_windows.py # Windows专用启动脚本 91 | ``` 92 | 93 | ### 3. 前端启动 94 | 95 | #### 方法一:使用自动化脚本 (推荐) 96 | ```bash 97 | cd frontend 98 | chmod +x setup.sh 99 | ./setup.sh # 自动创建.env配置文件 100 | npm run dev 101 | ``` 102 | 103 | #### 方法二:手动安装 104 | ```bash 105 | # 进入前端目录 106 | cd frontend 107 | 108 | # 复制环境变量模板 109 | cp .env.example .env 110 | 111 | # 安装依赖 112 | npm install 113 | 114 | # 启动开发服务器 (运行在 http://localhost:5173) 115 | npm run dev 116 | ``` 117 | 118 | ### 4. 默认测试账户 119 | 120 | - **用户名**: `test` 121 | - **密码**: `test1234` 122 | - **邮箱**: `test@example.com` 123 | 124 | ### 5. API密钥配置 125 | 126 | 使用前需要配置API密钥(在应用内设置): 127 | 128 | 1. **DeepSeek AI API**: 在应用的API配置页面填入密钥 129 | 2. **科大讯飞语音API**: 在应用的语音设置页面配置APPID和APIKEY 130 | 131 | > 💡 **安全提示**: 所有API密钥都通过应用内界面配置,不会硬编码在代码中 132 | 133 | ## 📁 项目结构 134 | 135 | ``` 136 | ai-interview-helper-react/ 137 | ├── frontend/ # React前端应用 138 | │ ├── src/ 139 | │ │ ├── api/ # API接口封装 140 | │ │ ├── components/ # 可复用组件 141 | │ │ ├── pages/ # 页面组件 142 | │ │ ├── store/ # Zustand状态管理 143 | │ │ └── utils/ # 工具函数 144 | │ └── public/ 145 | ├── backend/ # FastAPI后端应用 146 | │ ├── app/ 147 | │ │ ├── models.py # 数据库模型 148 | │ │ ├── schemas.py # Pydantic模型 149 | │ │ ├── auth.py # 认证逻辑 150 | │ │ └── routers/ # API路由 151 | │ ├── main.py # 应用入口 152 | │ └── requirements.txt # Python依赖 153 | └── memory-bank/ # 项目文档和上下文 154 | ``` 155 | 156 | ## 🔧 配置说明 157 | 158 | ### 环境变量配置 159 | 160 | **后端配置** (backend/.env): 161 | ```env 162 | DATABASE_URL=postgresql://postgres:root@localhost:5432/ai_interview_helper 163 | REDIS_URL=redis://localhost:6379 164 | SECRET_KEY=your-secret-key-change-this-in-production 165 | ACCESS_TOKEN_EXPIRE_MINUTES=30 166 | ``` 167 | 168 | **前端配置** (frontend/.env): 169 | ```env 170 | VITE_API_BASE_URL=http://localhost:9000 171 | ``` 172 | 173 | ### API密钥申请 174 | 175 | 1. **科大讯飞语音转写API** - 申请地址:https://www.xfyun.cn/ 176 | 2. **DeepSeek AI API** - 申请地址:https://platform.deepseek.com/ 177 | 178 | 申请后在应用内设置页面配置,不需要修改代码文件。 179 | 180 | ## 📊 API文档 181 | 182 | 后端启动后可访问自动生成的API文档: 183 | - **Swagger UI**: http://localhost:9000/docs 184 | - **ReDoc**: http://localhost:9000/redoc 185 | 186 | ### 主要API端点 187 | 188 | | 端点 | 方法 | 描述 | 189 | |------|------|------| 190 | | `/health` | GET | 健康检查 | 191 | | `/api/auth/login` | POST | 用户登录 | 192 | | `/api/auth/register` | POST | 用户注册 | 193 | | `/api/auth/me` | GET | 获取当前用户 | 194 | | `/api/sessions/` | GET/POST | 会话管理 | 195 | | `/api/sessions/{id}/messages` | GET/POST | 消息管理 | 196 | 197 | ## 🛠️ 开发命令 198 | 199 | ### Docker开发(推荐) 200 | 201 | ```bash 202 | # 启动开发环境 203 | ./scripts/dev-start.sh 204 | 205 | # 查看日志 206 | ./scripts/docker-logs.sh 207 | 208 | # 查看特定服务日志 209 | ./scripts/docker-logs.sh backend 210 | ./scripts/docker-logs.sh postgres 211 | 212 | # 重启服务 213 | ./scripts/docker-restart.sh 214 | 215 | # 停止服务 216 | ./scripts/docker-stop.sh 217 | 218 | # 进入后端容器调试 219 | docker-compose -f docker-compose.dev.yml exec backend bash 220 | ``` 221 | 222 | ### 前端开发 223 | ```bash 224 | cd frontend 225 | npm run dev # 启动开发服务器 226 | npm run build # 构建生产版本 227 | npm run lint # 代码检查 228 | npm run preview # 预览构建结果 229 | ``` 230 | 231 | ### 传统后端开发 232 | 233 | **Linux/macOS/WSL**: 234 | ```bash 235 | cd backend 236 | source venv/bin/activate 237 | python run_server.py # 启动开发服务器 238 | python init_default_user.py # 重置默认用户 239 | python test_db.py # 测试数据库连接 240 | ``` 241 | 242 | **Windows**: 243 | ```bash 244 | cd backend 245 | venv\Scripts\activate 246 | python run_server_windows.py # Windows专用启动脚本 247 | python test_connection.py # 测试数据库连接 248 | python init_default_user.py # 重置默认用户 249 | ``` 250 | 251 | ### Docker管理命令 252 | 253 | ```bash 254 | # 查看容器状态 255 | docker-compose ps 256 | 257 | # 查看实时日志 258 | docker-compose logs -f 259 | 260 | # 重新构建并启动 261 | docker-compose up --build -d 262 | 263 | # 停止并删除容器 264 | docker-compose down 265 | 266 | # 清理所有数据(谨慎使用) 267 | docker-compose down -v 268 | ``` 269 | 270 | ## 📈 版本路线图 271 | 272 | - ✅ **v0.1** - 基础前端UI和语音转写功能 273 | - ✅ **v0.2** - AI回答功能和实时对话体验 274 | - ✅ **v0.3** - 后端架构和数据持久化 275 | - 🔄 **v0.4** - 多模型支持和API配置管理 276 | - 📋 **v0.5** - 完善用户系统和登录页面 277 | 278 | ## 🤝 贡献指南 279 | 280 | 1. Fork 本仓库 281 | 2. 创建功能分支 (`git checkout -b feature/AmazingFeature`) 282 | 3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) 283 | 4. 推送到分支 (`git push origin feature/AmazingFeature`) 284 | 5. 创建 Pull Request 285 | 6. 加我WeChat: JarvanJason 一起贡献 286 | 287 | ## 📄 许可证 288 | 289 | 本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。 290 | 291 | ## 🙏 致谢 292 | 293 | - 科大讯飞提供优秀的语音识别API 294 | - DeepSeek提供强大的AI模型支持 295 | - Ant Design提供优雅的UI组件库 296 | - 面试狗提供UI灵感 297 | 298 | ## 正在解决的问题 299 | 触发AI提问的时机难以控制, 准确率低, 需要帮忙改进 300 | 301 | ## 使用截图 302 | ![](./public/tutorial1.png) 303 | ![](./public/turorial2.png) -------------------------------------------------------------------------------- /frontend/public/xfyunRtasr/processor.worklet.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";function t(t,e){for(var r=0;rt.length)&&(e=t.length);for(var r=0,n=new Array(e);r0&&this.toSampleRate>0&&this.channels>0))throw new Error("Invalid settings specified for the resampler.");this.fromSampleRate==this.toSampleRate?(this.resampler=this.bypassResampler,this.ratioWeight=1):(this.fromSampleRate0){for(;a<1;a+=o)for(s=1-(f=a%1),r=0;r0){for(var i=this.ratioWeight,o=0,a=0;a0&&s=(f=1+s-c))){for(a=0;a=this.frameSize&&(this.port.postMessage({frameBuffer:this.transData(this.frameBuffer),isLastFrame:!1}),this.frameBuffer=[]),!0):(r&&this.port.postMessage({frameBuffer:this.transData(r),isLastFrame:!1}),!0)}},{key:"transData",value:function(t){return"short16"===this.arrayBufferType&&(t=function(t){for(var e=new ArrayBuffer(2*t.length),r=new DataView(e),n=0,i=0;i2.5s) 162 | - **双路径触发**:被动检查(语音转录时)+ 主动检查(静音延迟) 163 | - **绕过机制**:主动检查通过 `bypassPreCheck=true` 绕过预检查 164 | - **全局状态管理**:`silenceStartTime`、`lastDataReceiveTime` 追踪时间状态 165 | - **资源清理**:组件卸载时自动清理定时器和回调 166 | 167 | ### 音频处理与状态同步 168 | - 通过 `/public/xfyunRtasr/` worker文件集成WebAudio API 169 | - `setAudioActiveState()` 同步音频状态到问题检测系统 170 | - `updateAudioActiveState()` 追踪静音持续时间 171 | - 实时音频流处理和WebSocket通信 172 | 173 | ### API集成与缓存优化 174 | - **KV Cache策略**:稳定前缀 + 增量追加设计 175 | - **消息准备**:`prepareMessagesForAI()` 分离历史和新消息 176 | - **持久化存储**:xfyunConfigStore 和 apiConfigStore 支持 localStorage 177 | - **错误容错**:动态导入避免循环依赖,完善的错误处理 178 | 179 | ### UI架构与布局 180 | - **固定底部操作栏**:`flexShrink: 0` 确保不被压缩 181 | - **分离滚动区域**:聊天区和语音区独立滚动,`minHeight: 0` 启用滚动 182 | - **自动滚动**:`useEffect` 监听数据变化自动滚到底部 183 | - **响应式设计**:Flexbox布局适配不同屏幕尺寸 184 | 185 | ### 构建配置 186 | - Vite配合React插件 187 | - 启用TypeScript严格模式 188 | - 配置路径别名(`@` 指向 `src/`) 189 | - ESLint配合React特定规则 190 | 191 | ## 外部依赖 192 | 193 | ### 核心API 194 | - **科大讯飞RTasr**: 实时语音识别WebSocket API 195 | - **DeepSeek**: AI聊天完成API用于生成响应 196 | 197 | ### 关键库 198 | - `antd` - UI组件库 199 | - `zustand` - 轻量级状态管理 200 | - `axios` - HTTP客户端 201 | - `react-router-dom` - 路由 202 | - `styled-components` - CSS-in-JS样式 203 | - `react-markdown` - Markdown渲染 204 | - `crypto-js` - API认证加密函数 205 | 206 | ## 开发工作流 207 | 208 | ### 智能问题检测工作流 209 | 1. **语音转录处理**:科大讯飞RTasr实时转写语音为文本 210 | 2. **智能触发检测**: 211 | - 被动检查:语音转录数据接近3秒中断时触发预检查 212 | - 主动检查:音频静音1.1秒后自动触发检测 213 | 3. **KV Cache优化调用**:准备消息上下文,利用DeepSeek缓存机制 214 | 4. **AI响应生成**:基于滑动窗口历史对话生成回答 215 | 5. **实时显示**:通过markdown渲染进行流式显示 216 | 6. **状态更新**:标记已处理消息,优化后续缓存命中 217 | 218 | ## 记忆库上下文 219 | 220 | 项目包含 `/memory-bank` 目录,记录了: 221 | - 项目路线图和版本规划 222 | - 技术上下文和架构决策 223 | - 实现进度和任务跟踪 224 | - 样式指南和开发模式 225 | 226 | 这是一个处于v0.3阶段的活跃开发项目,已完成后端架构和数据持久化功能,下一步将专注于多模型支持和用户系统完善。 227 | 228 | ## v0.3 更新说明 229 | 230 | ### 主要新增功能 231 | - **完整的后端架构**: 基于FastAPI + PostgreSQL + Redis 232 | - **用户认证系统**: JWT认证,支持注册/登录/登出 233 | - **会话持久化**: 所有面试会话和消息自动保存到数据库 234 | - **默认测试账户**: test/test1234,便于快速测试 235 | - **前后端集成**: 完整的API客户端和状态管理 236 | - **智能问题检测系统**: 基于AI语义理解的高精度问题识别 237 | - **KV Cache优化**: DeepSeek API调用成本降低70-90% 238 | - **API配置持久化**: 支持XFYUN和DeepSeek配置本地存储 239 | - **设置界面**: 完整的API配置和系统设置页面 240 | - **UI布局优化**: 固定底部操作栏,分离滚动区域,全屏适配 241 | 242 | ### 数据库模型 243 | - **Users表**: 用户基本信息和认证数据 244 | - **InterviewSessions表**: 面试会话元数据 245 | - **SessionMessages表**: 会话中的所有消息记录 246 | 247 | ### API接口 248 | - **认证接口**: `/api/auth/login`, `/api/auth/register`, `/api/auth/me` 249 | - **会话接口**: `/api/sessions/` (CRUD操作) 250 | - **消息接口**: `/api/sessions/{id}/messages` (消息管理) 251 | 252 | ### 安全特性 253 | - **敏感信息保护**: 所有API密钥和tokens通过环境变量或应用内设置管理 254 | - **版本控制安全**: .env文件和敏感配置已加入.gitignore 255 | - **JWT安全**: 使用强密钥进行token签名 256 | - **密码加密**: 使用bcrypt加密存储用户密码 257 | 258 | ### v0.3.1 智能问题检测系统 259 | 260 | #### 核心特性 261 | - **语义理解检测**: 摒弃规则匹配,使用AI语义分析识别问题 262 | - **触发时机优化**: 双路径触发机制(被动+主动)避免漏检和过度检测 263 | - **成本控制**: KV Cache充分利用,API调用成本降低70-90% 264 | - **容错处理**: 语音识别错误容忍,角色混淆智能处理 265 | 266 | #### 技术实现 267 | - **轻量级预检查**: `shouldPreCheck()` 只在接近阈值时触发完整检查 268 | - **静音检测**: 音频静音1.1秒后主动触发AI问题检测 269 | - **防抖机制**: 500ms防抖避免重复触发 270 | - **滑动窗口**: 30秒+30条消息优化上下文长度 271 | - **消息标记**: `isAsked` 属性避免重复处理 272 | - **资源清理**: 组件卸载时自动清理定时器和回调 273 | 274 | #### 架构优化 275 | - **异步动态导入**: 避免循环依赖,使用 `await import()` 延迟加载 276 | - **浏览器兼容**: `window.setTimeout` 替换 `NodeJS.Timeout` 类型 277 | - **状态同步**: 音频活跃状态与问题检测系统实时同步 278 | - **错误处理**: 完善的异常捕获和用户友好的错误提示 279 | 280 | #### 文件结构 281 | - `src/utils/questionDetection.ts` - 问题检测核心逻辑和工具函数 282 | - `src/api/xfyunRtasr.ts` - 语音转录与智能触发集成 283 | - `src/api/deepseek.ts` - KV Cache优化的DeepSeek API调用 284 | - `src/store/interviewStore.ts` - 消息状态管理和持久化 285 | - `src/pages/InterviewMeeting.tsx` - UI布局优化和状态同步 286 | 287 | ### v0.3.2 设置系统与配置管理 288 | 289 | #### 持久化存储 290 | - **xfyunConfigStore**: 科大讯飞API配置,支持localStorage持久化 291 | - **apiConfigStore**: DeepSeek API配置,支持localStorage持久化 292 | - **配置验证**: 实时状态检查和错误提示 293 | - **默认值**: 智能默认配置减少用户配置负担 294 | 295 | #### 设置界面 296 | - **标签页设计**: XFYUN和DeepSeek配置分离,界面清晰 297 | - **表单验证**: Ant Design Form组件,实时验证和错误提示 298 | - **状态显示**: 配置状态实时反馈,连接成功/失败提示 299 | - **导航集成**: 主菜单添加设置入口,便于快速访问 300 | 301 | ### v0.3.3 UI布局优化 302 | 303 | #### 全屏适配 304 | - **100vh高度**: 完整利用视口高度,消除默认边距 305 | - **固定底部操作栏**: `flexShrink: 0` 确保操作栏不被压缩 306 | - **分离滚动区域**: 聊天区和语音区独立滚动,`minHeight: 0` 启用滚动 307 | - **自动滚动**: 消息和答案更新时自动滚到底部 308 | 309 | #### 布局结构 310 | - **Flexbox架构**: 主内容区 `flex: 1`,底部操作栏固定高度 311 | - **响应式设计**: 聊天区和语音区比例优化,适配不同屏幕 312 | - **视觉层次**: 问答分栏显示,用户问题左侧,AI回答右侧 313 | - **状态指示**: 录音状态、音频活跃状态实时显示 314 | 315 | #### 交互优化 316 | - **音频状态同步**: 录音状态与音频活跃状态实时同步显示 317 | - **操作反馈**: 按钮状态变化、加载状态、错误提示完善 318 | - **键盘快捷键**: 支持快捷操作,提升用户体验 -------------------------------------------------------------------------------- /frontend/src/pages/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Form, Input, Button, Tabs, Space, Typography, Divider, message, InputNumber } from 'antd' 2 | import { SaveOutlined, ReloadOutlined, DeleteOutlined } from '@ant-design/icons' 3 | import { useXfyunConfigStore } from '../store/xfyunConfigStore' 4 | import { useApiConfigStore } from '../store/apiConfigStore' 5 | 6 | const { Title, Text } = Typography 7 | const { TabPane } = Tabs 8 | 9 | export default function Settings() { 10 | const { config: xfyunConfig, updateConfig: updateXfyunConfig, clearConfig: clearXfyunConfig } = useXfyunConfigStore() 11 | const { apiConfigs, updateConfig: updateApiConfig } = useApiConfigStore() 12 | 13 | const [xfyunForm] = Form.useForm() 14 | const [deepseekForm] = Form.useForm() 15 | 16 | // 获取默认的DeepSeek配置 17 | const deepseekConfig = apiConfigs.find(config => config.apiProvider === 'deepseek') || apiConfigs[0] 18 | 19 | const handleXfyunSave = (values: any) => { 20 | updateXfyunConfig(values.appId, values.apiKey) 21 | message.success('科大讯飞配置已保存') 22 | } 23 | 24 | const handleXfyunClear = () => { 25 | clearXfyunConfig() 26 | xfyunForm.resetFields() 27 | message.success('科大讯飞配置已清除') 28 | } 29 | 30 | const handleDeepSeekSave = (values: any) => { 31 | if (deepseekConfig) { 32 | updateApiConfig(deepseekConfig.seq, { 33 | apiKey: values.apiKey, 34 | baseUrl: values.baseUrl, 35 | model: values.model, 36 | maxTokens: values.maxTokens, 37 | temperature: values.temperature, 38 | }) 39 | message.success('DeepSeek配置已保存') 40 | } 41 | } 42 | 43 | return ( 44 |
45 | 应用设置 46 | 配置API密钥和相关参数 47 | 48 | 49 | 50 | 51 | 52 | 53 |
62 | 67 | 71 | 72 | 73 | 78 | 82 | 83 | 84 | 85 | 86 | 94 | 101 | 102 | 103 |
104 | 105 | 106 | 107 |
108 | 配置状态: 109 | 110 | {xfyunConfig.isConfigured ? " ✅ 已配置" : " ⚠️ 未配置"} 111 | 112 |
113 | 114 | {xfyunConfig.updatedAt && ( 115 |
116 | 117 | 最后更新:{new Date(xfyunConfig.updatedAt).toLocaleString()} 118 | 119 |
120 | )} 121 |
122 |
123 | 124 | 125 | 126 |
138 | 143 | 147 | 148 | 149 | 154 | 158 | 159 | 160 | 165 | 169 | 170 | 171 | 176 | 183 | 184 | 185 | 190 | 198 | 199 | 200 | 201 | 209 | 210 |
211 | 212 | 213 | 214 |
215 | 配置状态: 216 | 217 | {deepseekConfig?.apiKey ? " ✅ 已配置" : " ⚠️ 未配置"} 218 | 219 |
220 | 221 | {deepseekConfig?.updatedAt && ( 222 |
223 | 224 | 最后更新:{new Date(deepseekConfig.updatedAt).toLocaleString()} 225 | 226 |
227 | )} 228 |
229 |
230 |
231 |
232 | ) 233 | } -------------------------------------------------------------------------------- /frontend/src/api/xfyunRtasr.ts: -------------------------------------------------------------------------------- 1 | // 科大讯飞实时语音转写API工具 2 | // 文档参考:https://www.xfyun.cn/doc/asr/rtasr/API.html 3 | 4 | import CryptoJS from 'crypto-js'; 5 | import type { MeetingMessage } from '@/store/interviewStore' 6 | import { useInterviewStore } from '@/store/interviewStore' 7 | import { useXfyunConfigStore } from '@/store/xfyunConfigStore' 8 | 9 | // 获取当前时间戳(秒) 10 | function getTimestamp() { 11 | return Math.floor(Date.now() / 1000); 12 | } 13 | 14 | // 生成 signa(HMACSHA1 + base64 + urlencode) 15 | function getSigna(appid: string, apiKey: string, ts: number) { 16 | const signa = CryptoJS.MD5(appid + ts).toString(); 17 | const signatureSha = CryptoJS.HmacSHA1(signa, apiKey); 18 | const signature = CryptoJS.enc.Base64.stringify(signatureSha); 19 | return encodeURIComponent(signature); 20 | } 21 | 22 | // 生成WebSocket URL 23 | export function getRtasrWebSocketUrl() { 24 | const { getAppId, getApiKey } = useXfyunConfigStore.getState(); 25 | const appId = getAppId(); 26 | const apiKey = getApiKey(); 27 | 28 | if (!appId || !apiKey) { 29 | throw new Error('科大讯飞API配置未完成,请在设置中配置APPID和API_KEY'); 30 | } 31 | 32 | const ts = getTimestamp(); 33 | const signa = getSigna(appId, apiKey, ts); 34 | return `wss://rtasr.xfyun.cn/v1/ws?appid=${appId}&ts=${ts}&signa=${signa}&roleType=2`; 35 | } 36 | 37 | // 创建WebSocket并处理转写 38 | export function createRtasrWebSocket({ 39 | onResult, 40 | onError, 41 | onOpen, 42 | onClose 43 | }: { 44 | onResult: (data: any) => void, 45 | onError?: (e: Event) => void, 46 | onOpen?: () => void, 47 | onClose?: () => void 48 | }) { 49 | const ws = new WebSocket(getRtasrWebSocketUrl()); 50 | ws.onopen = () => { 51 | onOpen && onOpen(); 52 | }; 53 | ws.onmessage = (e) => { 54 | try { 55 | const json = JSON.parse(e.data); 56 | onResult(json); 57 | } catch (err) { 58 | // 忽略非JSON消息 59 | } 60 | }; 61 | ws.onerror = (e) => { 62 | onError && onError(e); 63 | }; 64 | ws.onclose = () => { 65 | onClose && onClose(); 66 | }; 67 | return ws; 68 | } 69 | 70 | // 全局状态:防抖器和数据追踪 71 | let questionDetectionDebouncer: any = null 72 | let lastDataReceiveTime = Date.now() 73 | let currentAudioActive = false 74 | let currentQuestionDetectionCallback: (() => void) | null = null 75 | let silenceCheckTimer: number | null = null 76 | 77 | /** 78 | * 清理问题检测相关资源 79 | */ 80 | export function cleanupQuestionDetection() { 81 | if (silenceCheckTimer) { 82 | window.clearTimeout(silenceCheckTimer) 83 | silenceCheckTimer = null 84 | } 85 | currentQuestionDetectionCallback = null 86 | 87 | if (questionDetectionDebouncer) { 88 | questionDetectionDebouncer.destroy?.() 89 | questionDetectionDebouncer = null 90 | } 91 | } 92 | 93 | // 初始化防抖器(延迟加载以避免循环依赖) 94 | async function initDebouncer() { 95 | if (!questionDetectionDebouncer) { 96 | const { QuestionDetectionDebouncer } = await import('@/utils/questionDetection') 97 | questionDetectionDebouncer = new QuestionDetectionDebouncer(500) 98 | } 99 | return questionDetectionDebouncer 100 | } 101 | 102 | /** 103 | * 设置音频活跃状态并同步到问题检测系统 104 | * @param audioActive 音频是否活跃 105 | */ 106 | export async function setAudioActiveState(audioActive: boolean) { 107 | currentAudioActive = audioActive 108 | 109 | // 同步到问题检测系统,追踪静音时间 110 | const { updateAudioActiveState } = await import('@/utils/questionDetection') 111 | updateAudioActiveState(audioActive) 112 | 113 | // 如果变为静音,启动延迟检查 114 | if (!audioActive && currentQuestionDetectionCallback) { 115 | // 清除之前的定时器 116 | if (silenceCheckTimer) { 117 | window.clearTimeout(silenceCheckTimer) 118 | } 119 | 120 | // 1.1秒后主动检查一次(绕过预检查) 121 | silenceCheckTimer = window.setTimeout(() => { 122 | console.log('🔍 静音1.1秒后主动检查触发条件') 123 | checkAndTriggerQuestionDetection(currentQuestionDetectionCallback, true) // bypassPreCheck = true 124 | silenceCheckTimer = null 125 | }, 1100) 126 | } else if (audioActive) { 127 | // 如果重新有声音,取消延迟检查 128 | if (silenceCheckTimer) { 129 | window.clearTimeout(silenceCheckTimer) 130 | silenceCheckTimer = null 131 | } 132 | } 133 | } 134 | 135 | /** 136 | * 轻量级预检查:避免过度触发完整检查 137 | * @param lastDataTime 最后收到数据的时间 138 | * @returns 是否需要进行完整检查 139 | */ 140 | function shouldPreCheck(lastDataTime: number): boolean { 141 | const now = Date.now() 142 | const timeSinceLastData = now - lastDataTime 143 | 144 | // 只有在接近触发条件时才进行完整检查 145 | // 距离3秒数据中断还有500ms以内时才检查 146 | return timeSinceLastData > 2500 147 | } 148 | 149 | /** 150 | * 智能问题检测触发器 151 | * @param onQuestionDetection 问题检测回调 152 | * @param bypassPreCheck 是否绕过预检查(用于主动检查) 153 | */ 154 | async function checkAndTriggerQuestionDetection(onQuestionDetection?: () => void, bypassPreCheck = false) { 155 | if (!onQuestionDetection) return 156 | 157 | // 轻量级预检查,避免过度触发(主动检查可以绕过) 158 | if (!bypassPreCheck && !shouldPreCheck(lastDataReceiveTime)) { 159 | return 160 | } 161 | 162 | // 动态导入避免循环依赖 163 | const { shouldTriggerQuestionDetection } = await import('@/utils/questionDetection') 164 | 165 | // 统一的触发检查入口(当前总是返回true,预留未来扩展) 166 | const shouldTrigger = shouldTriggerQuestionDetection(lastDataReceiveTime) 167 | 168 | if (shouldTrigger) { 169 | const debouncer = await initDebouncer() 170 | debouncer.debounce(() => { 171 | const triggerSource = bypassPreCheck ? '主动静音检查' : '被动数据检查' 172 | console.log(`🎯 触发AI问题检测 - ${triggerSource}`) 173 | onQuestionDetection() 174 | }) 175 | } 176 | } 177 | 178 | /** 179 | * 解析科大讯飞实时转写API返回的data字段,转为 MeetingMessage[],支持智能问题检测 180 | * @param contentData 科大讯飞ws返回的整体对象 181 | * @param onQuestionDetected 智能问题检测回调函数 182 | * @returns MeetingMessage[] 183 | */ 184 | export function parseRtasrResult(contentData: any, onQuestionDetected?: () => void): MeetingMessage[] { 185 | const result: MeetingMessage[] = [] 186 | 187 | // 更新数据接收时间 188 | lastDataReceiveTime = Date.now() 189 | 190 | // 设置当前回调,用于延迟主动检查 191 | if (onQuestionDetected) { 192 | currentQuestionDetectionCallback = onQuestionDetected 193 | } 194 | 195 | console.info('【parseRtasrResult】', contentData) 196 | 197 | try { 198 | if (!contentData || !contentData.cn || !contentData.cn.st || !contentData.cn.st.rt) { 199 | console.error('【parseRtasrResult】', 'dataObj is null') 200 | return result 201 | } 202 | 203 | if (contentData.cn.st.type !== '0') { 204 | return result 205 | } 206 | 207 | let lastRl: string | null = null 208 | let currentContent = '' 209 | let currentRole: 'user' | 'asker' = 'user' 210 | 211 | // 解析转写结果 212 | contentData.cn.st.rt.forEach((rtItem: any) => { 213 | rtItem.ws.forEach((wsItem: any) => { 214 | wsItem.cw.forEach((cwItem: any) => { 215 | const rl = cwItem.rl 216 | const role: 'user' | 'asker' = rl === '0' ? 'user' : 'asker' 217 | 218 | if (lastRl === null) { 219 | // 第一个 220 | lastRl = rl 221 | currentRole = role 222 | currentContent = cwItem.w 223 | } else if (rl === lastRl) { 224 | // 角色未变,继续累积内容 225 | currentContent += cwItem.w 226 | } else { 227 | // 角色切换,先推送上一个消息 228 | if (currentContent.trim()) { 229 | const message = { 230 | id: Date.now().toString() + Math.random(), 231 | timestamp: Date.now(), 232 | content: currentContent.trim(), 233 | role: currentRole, 234 | status: 'sent' as const, 235 | } 236 | result.push(message) 237 | 238 | // 每个消息只检查一次触发条件 239 | checkAndTriggerQuestionDetection(onQuestionDetected) 240 | } 241 | 242 | // 开始新角色的内容 243 | lastRl = rl 244 | currentRole = role 245 | currentContent = cwItem.w 246 | } 247 | }) 248 | }) 249 | }) 250 | 251 | // 推送最后一段内容 252 | if (currentContent.trim()) { 253 | const message = { 254 | id: Date.now().toString() + Math.random(), 255 | timestamp: Date.now(), 256 | content: currentContent.trim(), 257 | role: currentRole, 258 | status: 'sent' as const, 259 | } 260 | result.push(message) 261 | 262 | // 对最后一个消息也检查触发条件 263 | checkAndTriggerQuestionDetection(onQuestionDetected) 264 | } 265 | 266 | return result 267 | 268 | } catch (e) { 269 | console.error('parseRtasrResult error', e) 270 | return result 271 | } 272 | } -------------------------------------------------------------------------------- /frontend/src/store/interviewStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { devtools } from 'zustand/middleware' 3 | import { immer } from 'zustand/middleware/immer' 4 | import * as sessionsApi from '../api/sessions'; 5 | /** 6 | * 语音转写结果接口 7 | * @property action - 动作类型 8 | * @property code - 状态码 9 | * @property data - 转写数据 10 | * @property desc - 描述信息 11 | * @property sid - 会话ID 12 | * @property timestamp - 时间戳 13 | * @property isFinal - 是否为最终结果 14 | */ 15 | interface TransResult { 16 | action: string 17 | code: string 18 | data: { 19 | cn: { 20 | st: { 21 | bg: string 22 | ed: string 23 | rt: Array<{ 24 | ws: Array<{ 25 | cw: Array<{ 26 | w: string 27 | wp: string 28 | }> 29 | wb: number 30 | we: number 31 | }> 32 | }> 33 | type: string 34 | } 35 | } 36 | seg_id: number 37 | biz?: string 38 | src?: string 39 | dst?: string 40 | isEnd?: boolean 41 | rl?: number 42 | } 43 | desc: string 44 | sid: string 45 | timestamp: number 46 | isFinal: boolean 47 | } 48 | 49 | /** 50 | * 会议消息接口 51 | * @property id - 消息ID 52 | * @property timestamp - 时间戳 53 | * @property content - 消息内容 54 | * @property role - 消息角色(用户/助手) 55 | * @property status - 消息状态(发送中/已发送/错误) 56 | * @property error - 错误信息(可选) 57 | * @property audioUrl - 音频URL(可选) 58 | * @property duration - 音频时长(可选) 59 | * @property isTranslated - 是否已翻译(可选) 60 | * @property translatedContent - 翻译内容(可选) 61 | * @property metadata - 元数据(可选) 62 | */ 63 | export interface MeetingMessage { 64 | id: string 65 | timestamp: number 66 | content: string 67 | role: 'user' | 'asker' 68 | status: 'sending' | 'sent' | 'error' 69 | error?: string 70 | audioUrl?: string 71 | duration?: number 72 | isTranslated?: boolean 73 | translatedContent?: string 74 | isAsked?: boolean // 新增:是否已发送给AI 75 | metadata?: { 76 | modelUsed?: string 77 | tokensUsed?: number 78 | processingTime?: number 79 | } 80 | } 81 | 82 | /** 83 | * 新建面试页表单配置 84 | */ 85 | export interface InterviewConfig { 86 | hasReadGuide: boolean 87 | region: string 88 | job: string 89 | lang: string 90 | customQA: string 91 | qaLibStatus: string // 问答库状态 92 | } 93 | 94 | export interface Answer { 95 | id: string 96 | message: string 97 | created: number 98 | question: string 99 | } 100 | 101 | interface InterviewState { 102 | messages: MeetingMessage[] 103 | transResults: TransResult[] 104 | answers: Answer[] 105 | isRecording: boolean 106 | isProcessing: boolean 107 | interviewConfig: InterviewConfig 108 | currentSessionId: string | null 109 | sessionLoading: boolean 110 | setCurrentSessionId: (sessionId: string | null) => void 111 | createNewSession: (title?: string) => Promise 112 | loadSession: (sessionId: string) => Promise 113 | saveMessageToSession: (message: MeetingMessage) => Promise 114 | setInterviewConfig: (config: Partial) => void 115 | addMessage: (message: Omit) => void 116 | addTransResult: (result: Omit) => void 117 | addAnswer: (answer: Answer) => void 118 | updateAnswer: (id: string, message: string) => void 119 | upsertAnswer: (answer: Answer) => void 120 | setRecording: (isRecording: boolean) => void 121 | setProcessing: (isProcessing: boolean) => void 122 | clearMessages: () => void 123 | clearTransResults: () => void 124 | updateMessageStatus: (id: string, status: MeetingMessage['status'], error?: string) => void 125 | markMessagesAsAsked: (messageIds: string[]) => void 126 | } 127 | 128 | const defaultInterviewConfig: InterviewConfig = { 129 | hasReadGuide: false, 130 | region: '简体中文', 131 | job: '前端', 132 | lang: 'Typescript', 133 | customQA: '否', 134 | qaLibStatus: '空', 135 | } 136 | 137 | export const useInterviewStore = create()( 138 | devtools( 139 | immer((set, get) => ({ 140 | messages: [], 141 | transResults: [], 142 | answers: [], 143 | isRecording: false, 144 | isProcessing: false, 145 | interviewConfig: defaultInterviewConfig, 146 | currentSessionId: null, 147 | sessionLoading: false, 148 | setCurrentSessionId: (sessionId) => set((state) => { 149 | state.currentSessionId = sessionId 150 | }), 151 | createNewSession: async (title) => { 152 | try { 153 | set((state) => { 154 | state.sessionLoading = true 155 | }) 156 | const session = await sessionsApi.createSession({ title }) 157 | set((state) => { 158 | state.currentSessionId = session.session_id 159 | state.messages = [] 160 | state.transResults = [] 161 | state.answers = [] 162 | state.sessionLoading = false 163 | }) 164 | return session.session_id 165 | } catch (error) { 166 | console.error('Failed to create session:', error) 167 | set((state) => { 168 | state.sessionLoading = false 169 | }) 170 | return null 171 | } 172 | }, 173 | loadSession: async (sessionId) => { 174 | try { 175 | set((state) => { 176 | state.sessionLoading = true 177 | }) 178 | const session = await sessionsApi.getSession(sessionId) 179 | 180 | // 转换后端消息格式到前端格式 181 | const convertedMessages: MeetingMessage[] = session.messages.map(msg => ({ 182 | id: msg.id.toString(), 183 | timestamp: new Date(msg.timestamp).getTime(), 184 | content: msg.content, 185 | role: msg.message_type === 'user' ? 'user' : 'asker', 186 | status: 'sent' as const, 187 | metadata: msg.message_metadata ? JSON.parse(msg.message_metadata) : undefined 188 | })) 189 | 190 | set((state) => { 191 | state.currentSessionId = sessionId 192 | state.messages = convertedMessages 193 | state.sessionLoading = false 194 | }) 195 | } catch (error) { 196 | console.error('Failed to load session:', error) 197 | set((state) => { 198 | state.sessionLoading = false 199 | }) 200 | } 201 | }, 202 | saveMessageToSession: async (message) => { 203 | const { currentSessionId } = get() 204 | if (!currentSessionId) return 205 | 206 | try { 207 | await sessionsApi.addMessageToSession(currentSessionId, { 208 | message_type: message.role === 'user' ? 'user' : 'assistant', 209 | content: message.content, 210 | message_metadata: message.metadata ? JSON.stringify(message.metadata) : undefined 211 | }) 212 | } catch (error) { 213 | console.error('Failed to save message to session:', error) 214 | } 215 | }, 216 | setInterviewConfig: (config) => set((state) => { 217 | state.interviewConfig = { ...state.interviewConfig, ...config } 218 | }), 219 | addMessage: (message) => { 220 | const newMessage: MeetingMessage = { 221 | ...message, 222 | id: Date.now().toString(), 223 | timestamp: Date.now(), 224 | } 225 | 226 | set((state) => { 227 | state.messages.push(newMessage) 228 | }) 229 | 230 | // 异步保存到后端 231 | const { saveMessageToSession } = get() 232 | saveMessageToSession(newMessage).catch(console.error) 233 | }, 234 | addTransResult: (result) => { 235 | try { 236 | set((state) => { 237 | state.transResults.push({ 238 | ...result, 239 | timestamp: Date.now(), 240 | isFinal: result.data?.cn?.st?.type === '0', 241 | }) 242 | }) 243 | } catch (e) { 244 | console.error('addTransResult error', e, result) 245 | } 246 | }, 247 | addAnswer: (answer) => set((state) => { 248 | state.answers.push(answer) 249 | }), 250 | updateAnswer: (id, message) => set((state) => { 251 | const idx = state.answers.findIndex(a => a.id === id) 252 | if (idx !== -1) { 253 | state.answers[idx].message += message 254 | } 255 | }), 256 | upsertAnswer: (answer) => set((state) => { 257 | const idx = state.answers.findIndex(a => a.id === answer.id) 258 | if (idx === -1) { 259 | state.answers.push(answer) 260 | } else { 261 | state.answers[idx] = answer 262 | } 263 | }), 264 | setRecording: (isRecording) => set((state) => { 265 | state.isRecording = isRecording 266 | }), 267 | setProcessing: (isProcessing) => set((state) => { 268 | state.isProcessing = isProcessing 269 | }), 270 | clearMessages: () => set((state) => { 271 | state.messages = [] 272 | }), 273 | clearTransResults: () => set((state) => { 274 | state.transResults = [] 275 | }), 276 | updateMessageStatus: (id, status, error) => set((state) => { 277 | state.messages = state.messages.map((msg) => 278 | msg.id === id ? { ...msg, status, error } : msg 279 | ) 280 | }), 281 | markMessagesAsAsked: (messageIds) => set((state) => { 282 | state.messages = state.messages.map((msg) => 283 | messageIds.includes(msg.id) ? { ...msg, isAsked: true } : msg 284 | ) 285 | }), 286 | })), 287 | { 288 | name: 'interview-store', 289 | trace: true, 290 | traceLimit: 25, 291 | } 292 | ) 293 | ) -------------------------------------------------------------------------------- /frontend/src/pages/InterviewMeeting.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react' 2 | import { useNavigate } from 'react-router-dom' 3 | import { useInterviewStore } from '@/store/interviewStore' 4 | import { createRtasrWebSocket, parseRtasrResult, setAudioActiveState, cleanupQuestionDetection } from '@/api/xfyunRtasr' 5 | import RecorderManager from '@/api/utils/xfyunRtasr/index.esm.js' 6 | import { isAudioActive } from '@/utils/voiceIdentifier' 7 | import { handleQuestionDetected } from '@/api/deepseek' 8 | import ReactMarkdown from 'react-markdown' 9 | 10 | const InterviewMeeting: React.FC = () => { 11 | const navigate = useNavigate() 12 | const addTransResult = useInterviewStore(s => s.addTransResult) 13 | const addMessage = useInterviewStore(s => s.addMessage) 14 | const messages = useInterviewStore(s => s.messages) 15 | const answers = useInterviewStore(s => s.answers) 16 | const [recording, setRecording] = useState(false) 17 | const wsRef = useRef(null) 18 | const recorderRef = useRef(null) 19 | const [audioActive, setAudioActive] = useState(false) 20 | 21 | // 自动滚动到底部的引用 22 | const chatAreaRef = useRef(null) 23 | const voiceAreaRef = useRef(null) 24 | const voiceContentRef = useRef(null) 25 | 26 | // 处理转写结果 27 | const handleRtasrResult = (data: any) => { 28 | console.log('【data】', data) 29 | if (data.action === 'result') { 30 | if (data.code === '0') { 31 | const parsedData = typeof data.data === 'string' ? JSON.parse(data.data) : data.data 32 | addTransResult({ 33 | action: data.action, 34 | code: data.code, 35 | data: parsedData, 36 | desc: data.desc, 37 | sid: data.sid, 38 | }) 39 | // 解析文本内容,集成智能AI问题检测 40 | const msgs = parseRtasrResult(parsedData, () => handleQuestionDetected()) 41 | msgs.forEach(msg => { 42 | addMessage(msg) 43 | console.log('【MeetingMessage】', msg) 44 | }) 45 | } else { 46 | // TODO:错误处理 47 | console.error('【转写错误】', data.desc) 48 | } 49 | } 50 | } 51 | 52 | // 开始面试 53 | const handleStart = async () => { 54 | setRecording(true) 55 | const recorder = new RecorderManager('/xfyunRtasr') 56 | recorderRef.current = recorder 57 | const ws = createRtasrWebSocket({ 58 | onResult: handleRtasrResult, 59 | onError: () => setRecording(false), 60 | onClose: () => setRecording(false), 61 | onOpen: async () => { 62 | // 只有 WebSocket 连接成功后才启动录音 63 | recorder.onFrameRecorded = ({ isLastFrame, frameBuffer }: any) => { 64 | const audioActiveState = isAudioActive(frameBuffer, 0.02) 65 | setAudioActive(audioActiveState) 66 | // 同步音频状态到问题检测系统 67 | setAudioActiveState(audioActiveState) 68 | 69 | if (ws.readyState === ws.OPEN) { 70 | ws.send(new Int8Array(frameBuffer)) 71 | if (isLastFrame) { 72 | ws.send('{"end": true}') 73 | } 74 | } 75 | } 76 | recorder.onStop = () => { } 77 | await recorder.start({ sampleRate: 16000, frameSize: 1280 }) 78 | } 79 | }) 80 | wsRef.current = ws 81 | } 82 | // 停止面试 83 | const handleStop = () => { 84 | setRecording(false) 85 | recorderRef.current?.stop() 86 | wsRef.current?.close() 87 | // 清理问题检测相关资源 88 | cleanupQuestionDetection() 89 | } 90 | 91 | // 组件卸载时清理 92 | React.useEffect(() => { 93 | return () => { 94 | cleanupQuestionDetection() 95 | } 96 | }, []) 97 | 98 | // 自动滚动到底部 99 | React.useEffect(() => { 100 | if (chatAreaRef.current) { 101 | chatAreaRef.current.scrollTop = chatAreaRef.current.scrollHeight 102 | } 103 | }, [answers]) 104 | 105 | React.useEffect(() => { 106 | if (voiceContentRef.current) { 107 | voiceContentRef.current.scrollTop = voiceContentRef.current.scrollHeight 108 | } 109 | }, [messages]) 110 | 111 | return ( 112 |
120 | {/* 主内容区 */} 121 |
128 | {/* 聊天区:Q左A右分栏 */} 129 |
141 | {answers.length > 0 ? answers.map((ans, idx) => ( 142 |
149 | {/* Q 左侧 */} 150 |
159 | Q: {ans.question} 160 |
161 | {/* A 右侧 */} 162 |
169 | A: {ans.message} 170 |
171 |
172 | )) : ( 173 |
179 | 暂无问答 180 |
181 | )} 182 |
183 | 184 | {/* 语音区 */} 185 |
197 |

实时转录

198 |
199 | {messages.map((msg, idx) => ( 200 |
208 | 209 | {msg.role === 'user' ? '用户' : '助手'}: 210 | 211 | {msg.content} 212 |
213 | ))} 214 |
215 |
216 |
217 | 218 | {/* 底部操作栏 - 固定在屏幕底部 */} 219 |
230 |
231 | 帮助中心
232 | 236 |
237 | 238 |
239 | 249 | 259 | 269 | 279 |
280 | 281 |
282 |
283 | 290 | 291 | {recording ? (audioActive ? '录音中' : '静音中') : '未录音'} 292 | 293 |
294 | 295 | 310 | 311 | 324 |
325 |
326 |
327 | ) 328 | } 329 | 330 | export default InterviewMeeting --------------------------------------------------------------------------------