├── 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 |
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 |

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 |
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 | 
303 | 
--------------------------------------------------------------------------------
/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 |
67 |
71 |
72 |
73 |
78 |
82 |
83 |
84 |
85 |
86 | }
90 | size="large"
91 | >
92 | 保存配置
93 |
94 | }
97 | size="large"
98 | >
99 | 清除配置
100 |
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 |
143 |
147 |
148 |
149 |
154 |
158 |
159 |
160 |
165 |
169 |
170 |
171 |
176 |
183 |
184 |
185 |
190 |
198 |
199 |
200 |
201 | }
205 | size="large"
206 | >
207 | 保存配置
208 |
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
--------------------------------------------------------------------------------