DeepSeek-V4 AI 智能助手(容器化部署)

647 阅读9分钟

AI 智能助手(deepseek-V4)

线上地址:http://150.158.150.46:8080

接口文档:http://150.158.150.46:8080/docs

本项目是一个 H5 / 小程序通用的 AI 对话系统,采用前后端同仓库维护:

  • 前端:ui/,Vue 3 + TypeScript + Vite 的 H5 聊天界面。
  • 后端:backend/,Express + SQLite 的 API 服务。
  • 后台能力:Redis 限流、BullMQ 异步任务、whisper.cpp 本地语音转文字。
  • 接口文档:后端运行后访问 /docs/docs.json

预览效果

截屏2026-05-09 13.53.51.png

核心能力

  • H5 自动登录,支持 accessToken + refreshToken 双 token 无感刷新。
  • SSE 流式 AI 对话,支持 deltathinkingdone 事件。
  • 会话列表、历史消息、删除会话、消息点赞、助手消息重新生成。
  • 图片上传与持久化展示。
  • 语音录制、前端标准化 WAV、后端异步转文字。
  • Redis 固定窗口限流,保护登录、AI 和语音接口。
  • BullMQ 后台队列,处理语音识别和临时文件清理任务。

技术栈

技术
前端Vue 3、TypeScript、Vite、Element Plus、Sass、markdown-it、recordrtc
后端Node.js ESM、Express 5、SQLite3、JWT、Multer、OpenAI SDK
中间件Redis、BullMQ
语音Browser MediaRecorder / Web Audio API、whisper.cpp
文档Swagger UI

项目结构

.
├── ui/                         # H5 前端
│   ├── src/App.vue             # 页面入口
│   ├── src/components/         # 聊天 UI 组件
│   ├── src/hook/               # 鉴权、会话、流式响应、录音、滚动逻辑
│   ├── src/utils/api.ts        # API 路径与后端基地址
│   ├── src/utils/request.ts    # 普通请求、token 注入、401 刷新重试
│   └── src/utils/streamRequest.ts # SSE 请求与流式 401 刷新重试
├── backend/                    # Express API 服务
│   ├── clients/                # OpenAI / DeepSeek 客户端
│   ├── controllers/            # HTTP 入参与响应
│   ├── docs/swagger/           # Swagger 注释定义
│   ├── middleware/             # 鉴权、限流、上传、whisper 封装
│   ├── queues/                 # Redis / BullMQ 队列定义
│   ├── repositories/           # SQLite 数据访问
│   ├── routes/                 # 路由挂载
│   ├── services/               # 业务编排
│   ├── workers/                # BullMQ worker 入口
│   ├── db.js                   # SQLite 初始化
│   └── index.js                # 服务入口
├── Dockerfile.frontend         # 前端生产镜像
├── Dockerfile.backend          # 后端生产镜像
├── docker-compose.yml          # Docker Compose 单机部署编排
├── docker.env.example          # Docker 部署环境变量模板
├── nginx.conf                  # 前端容器 Nginx 配置
├── DEPLOYMENT.md               # Docker 部署详细文档
├── AGENTS.md                   # 协作与运行约定
└── package.json                # monorepo 脚本

当前架构流程图

flowchart TD
  User["H5 / 小程序用户"] --> UI["ui: Vue 3 H5 聊天界面"]

  UI --> AuthHook["useChatAuth<br/>恢复登录态 / H5 登录"]
  UI --> ChatHook["useChatConversation<br/>会话与消息流程"]
  UI --> RecordHook["useChatRecording<br/>录音与语音上传"]

  AuthHook --> Request["request.ts<br/>accessToken 注入<br/>401 自动刷新重试"]
  ChatHook --> StreamReq["streamRequest.ts<br/>SSE 请求<br/>401 自动刷新重试"]
  RecordHook --> Request

  Request --> API["backend: Express API"]
  StreamReq --> API

  API --> Routes["routes<br/>路由与中间件顺序"]
  Routes --> Auth["authMiddleware<br/>校验 accessToken"]
  Routes --> RateLimit["rateLimit<br/>Redis 固定窗口限流"]
  Routes --> Upload["Multer<br/>图片 / 音频上传"]

  Auth --> Controllers["controllers<br/>参数与响应"]
  RateLimit --> Controllers
  Upload --> Controllers
  Controllers --> Services["services<br/>业务编排"]

  Services --> SQLite[("SQLite<br/>users / sessions / chat_records")]
  Services --> Model["OpenAI 兼容模型服务"]
  Services --> BullMQ["BullMQ 队列<br/>speech / cleanup"]
  BullMQ --> Redis[("Redis<br/>队列状态 / 限流计数")]
  Redis --> Workers["workers<br/>后台消费任务"]
  Workers --> Whisper["whisper.cpp<br/>语音转文字"]
  Workers --> Files["uploads / temp<br/>文件存储与清理"]

  Model --> SSE["SSE 流<br/>delta / thinking / done"]
  SSE --> UI

分层约定

后端请求链路:

routes
  -> middleware(auth / rateLimit / upload)
  -> controllers
  -> services
  -> repositories / clients / queues
  • routes:只描述接口路径和中间件顺序,不写业务规则。
  • middleware:处理鉴权、限流、上传等横切能力。
  • controllers:做 HTTP 入参转换和响应格式。
  • services:承载业务编排,例如登录签发 token、创建会话、写消息、创建语音任务。
  • repositories:只封装 SQLite 读写。
  • queues:只封装 Redis / BullMQ 队列定义与任务投递。
  • workers:后台消费队列,执行耗时任务。

运行要求

  • Node.js >= 20.18.0
  • npm >= 10
  • 可用的 OpenAI 兼容模型服务
  • Redis 服务,用于限流和 BullMQ 队列
  • 如启用语音转写,需要本地可执行的 whisper.cpp

快速开始

根目录安装依赖:

npm install

启动前后端:

npm run dev:all

单独启动:

npm run dev:frontend
npm run dev:backend

启动后台 worker:

npm --prefix backend run worker:all

构建前端:

npm run build:frontend

生产启动后端:

npm run start:backend

默认地址:

  • 前端开发服务:http://localhost:5173
  • 后端 API:http://localhost:3000
  • Swagger:http://localhost:3000/docs

环境变量

后端 backend/.env

变量名必填说明
JWT_SECRETJWT 签名密钥
DB_FILESQLite 文件路径,支持相对路径或绝对路径
OPENAI_API_KEY模型服务 API Key
OPENAI_BASE_URLOpenAI 兼容服务地址
PORT默认 3000
HOST开发环境默认 0.0.0.0,生产环境默认 127.0.0.1
CORS_ORIGIN开发环境允许的跨域来源,多个用逗号分隔
WX_APP_ID小程序登录所需
WX_APP_SECRET小程序登录所需
REDIS_URL默认 redis://127.0.0.1:6379
WHISPER_ROOT默认 ./whisper.cpp
WHISPER_SAMPLE_PATH默认 ./sample-6s.wav
LOGIN_RATE_LIMIT_WINDOW_MS登录限流窗口,默认 600000
LOGIN_RATE_LIMIT_MAX登录限流窗口内最大请求数,默认 20
AI_RATE_LIMIT_WINDOW_MSAI 接口限流窗口,默认 60000
AI_RATE_LIMIT_MAXAI 接口限流窗口内最大请求数,默认 60
SPEECH_RATE_LIMIT_WINDOW_MS语音接口限流窗口,默认 3600000
SPEECH_RATE_LIMIT_MAX语音接口限流窗口内最大请求数,默认 30
CLEANUP_UPLOADS_EVERY_MS清理任务执行间隔,默认 3600000
UPLOAD_FILE_MAX_AGE_MS上传/临时文件最大保留时长,默认 86400000

前端 ui/.env.localui/.env.development

VITE_OPENAI_BASE_URL=http://localhost:3000

说明:变量名沿用历史命名,实际含义是后端 API 服务地址。

登录与鉴权

当前采用双 token:

  • accessToken:短期访问令牌,默认 15 分钟,只用于业务接口。
  • refreshToken:长期刷新令牌,默认 7 天,只用于 /api/user/refresh
  • token:后端响应中保留的兼容字段,值等于 accessToken
sequenceDiagram
  participant UI as H5 前端
  participant API as Express API

  UI->>UI: 读取 URL / localStorage 中的 accessToken 和 refreshToken
  alt accessToken 有效
    UI->>API: GET /api/user/info<br/>Authorization: Bearer accessToken
    API-->>UI: 用户信息
  else accessToken 缺失或临近过期,且 refreshToken 存在
    UI->>API: POST /api/user/refresh<br/>{ refreshToken }
    API-->>UI: 新 accessToken + refreshToken
    UI->>API: GET /api/user/info
  else 无有效登录态
    UI->>API: POST /api/user/login/h5
    API-->>UI: accessToken + refreshToken
  end

  UI->>API: 业务请求携带 accessToken
  API-->>UI: accessToken 过期时返回 401
  UI->>API: 自动 refresh 并重试原请求一次

H5 默认调试登录账号在 ui/src/hook/useChatAuth.ts 中维护:

{
  "username": "h5_test",
  "password": "pass123"
}

生产环境应替换为正式登录入口。

API 总览

用户接口

方法路径鉴权说明
POST/api/user/login小程序登录
POST/api/user/login/h5H5 账号密码登录,不存在则自动注册
GET/api/user/infoaccessToken获取当前用户信息
POST/api/user/refreshrefreshToken刷新 accessToken 与 refreshToken

AI 与会话接口

方法路径鉴权说明
POST/api/ai/chataccessToken正式聊天接口,支持 JSON 和图片上传
GET/api/ai/sessionsaccessToken获取当前用户会话列表
GET/api/ai/sessions/:id/messagesaccessToken获取会话消息
POST/api/ai/sessions/:id/deleteaccessToken删除会话及其消息
GET/api/ai/modelsaccessToken获取模型列表
POST/api/ai/messages/:id/likeaccessToken切换消息点赞状态
POST/api/ai/messages/:id/regenerateaccessToken重新生成指定助手消息
POST/api/ai/speech-to-text/jobsaccessToken创建异步音频转文字任务
GET/api/ai/speech-to-text/jobs/:idaccessToken查询异步音频转文字任务

主要业务流程

对话

  1. 前端发送文本或图片消息到 /api/ai/chat
  2. 后端鉴权、限流并处理上传文件。
  3. 未传 sessionId 时创建新会话。
  4. 用户消息写入 chat_records
  5. 后端调用 OpenAI 兼容模型服务。
  6. 流式模式下通过 SSE 返回 deltathinkingdone
  7. 结束后将助手完整回复写回数据库。

文本请求示例:

{
  "messages": [
    {
      "role": "user",
      "type": "text",
      "content": "你好"
    }
  ],
  "sessionId": 1,
  "stream": true,
  "model": "deepseek-v4-flash"
}

图片请求使用 multipart/form-data

  • messages:JSON 字符串
  • image:图片文件
  • sessionId:可选
  • streamtrue
  • model:模型名

语音转文字

前端录音
  -> 标准化为 WAV(PCM / 16bit / 单声道 / 16kHz)
  -> POST /api/ai/speech-to-text/jobs
  -> authMiddleware 校验用户
  -> speechRateLimit 使用 Redis 限流
  -> upload.single('audio') 保存音频
  -> BullMQ speech-transcription 队列记录任务
  -> speechWorker 调用 whisper.cpp 转写
  -> GET /api/ai/speech-to-text/jobs/:id 查询结果

前端录音策略:

  • 优先使用 MediaRecorder
  • 不支持或初始化失败时回退到 Web Audio API。
  • 无论浏览器输出 webm、ogg 还是 wav,最终都会标准化成 16kHz WAV。
  • 标准化放在前端完成,减少后端对 ffmpeg 等转码工具的依赖。

限流

当前限流使用 Redis 固定时间窗口:

rate:{业务前缀}:{用户或IP}:{窗口编号}

每次请求通过 Redis INCR 原子递增计数,第一次写入时设置过期时间。超过阈值返回 429。Redis 短暂不可用时限流降级放行,避免限流组件故障导致核心接口整体不可用。

BullMQ 队列

  • speech-transcription:处理语音识别,避免请求线程阻塞。
  • cleanup-uploads:定时清理上传文件和临时文件。
  • Redis 负责保存队列任务状态、重试信息和限流计数。

数据存储

SQLite 主要表:

说明
users用户账号、昵称、头像、默认模型、最后登录时间
sessions会话标题、所属用户、创建与更新时间
chat_records消息内容、角色、类型、媒体路径、推理内容、点赞状态

whisper.cpp 准备

如果需要启用语音识别,在 backend 目录执行:

git clone https://github.com/ggerganov/whisper.cpp.git
cd whisper.cpp
bash ./models/download-ggml-model.sh tiny
mkdir build
cd build
cmake ..
make -j4

默认要求:

  • 可执行文件:backend/whisper.cpp/build/bin/whisper-cli
  • 模型文件:backend/whisper.cpp/models/ggml-tiny.bin

如放在其他目录,需要显式配置 WHISPER_ROOT

Docker Compose 部署

当前项目支持 Docker Compose 单机容器化部署,适合直接部署到 Linux 云服务器。

部署后主要包含三个容器:

frontend  # Nginx + 前端静态文件 + /api 反向代理
backend   # Express API 服务,容器内部监听 3000
redis     # Redis,用于限流和 BullMQ

可选 worker:

worker    # BullMQ 后台任务,处理语音转文字和清理任务

生产访问链路:

浏览器
  -> 服务器公网 8080 端口
  -> frontend 容器 Nginx:80
  -> /api 转发到 backend 容器:3000
  -> backend 访问 redis:6379 和 SQLite volume

默认只有 8080 暴露给公网,后端 3000 和 Redis 6379 只在 Docker 内部网络访问。

1. 准备服务器

服务器需要安装:

  • Git
  • Docker
  • Docker Compose v2

Ubuntu 示例:

sudo apt update
sudo apt install -y git docker.io docker-compose-v2
docker compose version

2. 配置环境变量

在项目根目录复制 Docker 环境变量模板:

cp docker.env.example docker.env
nano docker.env

至少需要配置:

JWT_SECRET=换成一串足够长的随机字符串
OPENAI_API_KEY=你的AI接口KEY
OPENAI_BASE_URL=你的AI接口BaseURL
WX_APP_ID=你的微信APPID
WX_APP_SECRET=你的微信密钥
SWAGGER_SERVER_URL=http://你的服务器IP:8080
CORS_ORIGIN=*

以下变量由 docker-compose.yml 注入,通常不需要写进 docker.env

NODE_ENV=production
HOST=0.0.0.0
PORT=3000
DB_FILE=/app/data/wechat-mini.db
REDIS_URL=redis://redis:6379

前端 Docker 生产部署下 VITE_OPENAI_BASE_URL 默认为空,表示前端请求同域 /api,再由 Nginx 转发到后端。

3. 构建并启动

docker compose up -d --build

检查容器:

docker compose ps

查看日志:

docker compose logs --tail=100 backend
docker compose logs --tail=100 frontend

访问地址:

  • 前端:http://服务器IP:8080
  • Swagger:http://服务器IP:8080/docs
  • OpenAPI JSON:http://服务器IP:8080/docs.json

如果浏览器无法访问,需要在云服务器安全组或防火墙中放行 8080 端口。

4. 启动 worker

如需启用语音转文字和后台清理任务:

docker compose --profile worker up -d --build

5. 更新部署

git pull
docker compose up -d --build

SQLite 数据、上传文件和 Redis 数据保存在 Docker volume 中。不要随意执行:

docker compose down -v

该命令会删除 volume,可能导致数据库和上传文件丢失。

更完整的 Docker 部署步骤、端口说明、国内服务器构建慢处理和排障说明见 DEPLOYMENT.md

部署要点

  • 前端生产构建产物在 ui/dist
  • 建议前端和后端挂在同一域名下,通过 Nginx 反向代理 /api/uploads
  • SSE 需要关闭代理缓冲,并放宽读写超时。
  • 生产环境默认不启用 CORS,跨域部署时需要明确配置网关或 CORS_ORIGIN
  • SQLite 适合单机部署;并发和数据量增长后应评估迁移到 MySQL / PostgreSQL。

Nginx 关键配置示例:

location / {
    try_files $uri $uri/ /index.html;
}

location /api/ {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
    proxy_buffering off;
    add_header X-Accel-Buffering no;
}

location /uploads/ {
    proxy_pass http://127.0.0.1:3000;
    proxy_set_header Host $host;
}