Files
CamTalk/docs/01-架构设计.md

503 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 架构设计
## 项目概述
CamTalk 是一款**多模态实时 AI 视觉对话助手**。用户通过摄像头和麦克风与 AI 交互AI 理解视觉场景和语音输入后,以文字和语音形式给出自然回应。
核心挑战在于三个维度之间的张力:
| 维度 | 关键问题 |
|------|---------|
| 视觉理解 | 如何准确理解摄像头画面中的人物、物体、场景? |
| 语音交互 | 如何让对话像真人交流一样自然、低延迟? |
| 成本控制 | 实时视频流 + LLM 推理,如何避免账单爆炸? |
## 系统架构
三层架构:**前端做轻量预处理,后端做智能编排,云端 AI 服务按需调用**。
```mermaid
graph TB
subgraph Browser["浏览器客户端"]
UI["UI 渲染层<br/>React 18 + TypeScript"]
Edge["边缘预处理层<br/>VAD / 关键帧检测"]
Media["媒体采集层<br/>Camera / Microphone"]
end
subgraph Gateway["Go 网关"]
WS["WebSocket Handler<br/>连接管理 / 消息分发"]
Session["Session Manager<br/>会话状态 / 对话历史"]
Orch["AI Orchestrator<br/>Eino Graph 声明式编排"]
Auth["Auth 模块<br/>JWT / bcrypt"]
REST["REST API<br/>健康检查 / 对话管理"]
Store["Store 层<br/>Repository 接口"]
end
subgraph AI["云端 AI 服务"]
STT["STT<br/>Deepgram / MiMo ASR"]
LLM["LLM<br/>GPT-4o / 通义千问"]
TTS["TTS<br/>OpenAI TTS / MiMo TTS"]
end
subgraph Storage["存储层"]
Mem["Memory<br/>进程内缓存"]
Redis["Redis<br/>会话状态"]
PG["PostgreSQL<br/>持久化存储"]
end
Media --> Edge
Edge -->|"query (image+audio)"| WS
UI <-->|"WebSocket"| WS
WS --> Session
WS --> Orch
Orch --> STT
Orch --> LLM
Orch --> TTS
Session --> Store
Store --> Mem
Store --> Redis
Store --> PG
REST --> Session
WS --> Auth
```
> 为什么单独加一层 Go 网关,而不是让前端直连 AI API1API Key 安全性2统一的速率限制和成本管控3多模型路由逻辑集中在一处便于维护。
## 核心交互流程
一次完整的"用户提问 → AI 回答"流程:
```mermaid
sequenceDiagram
participant B as 浏览器
participant G as Go 网关Eino Graph
participant S as STT
participant L as LLMChatModel
participant T as TTS
B->>B: VAD 检测到语音结束
B->>G: query {image, audio}
Note over G: EinoOrchestrator 启动 Graph.Stream()
G->>S: STT Lambda音频 → 文本
S-->>G: 识别文本
G-->>B: stt_result {text}
G->>G: History Lambda组装提示词 + 历史 + 多模态消息
G->>L: ChatModel Node流式推理
loop LLM 流式输出Callback OnEndWithStreamOutput
L-->>G: token delta
G-->>B: llm_chunk {delta}
end
G->>G: Msg2Str + Splitter Lambda句子切分
G->>T: TTS Lambda逐句合成
T-->>G: 音频 chunk
G-->>B: tts_audio {audio}
G->>G: Done Lambda发送完成通知
G-->>B: llm_done {full_text, tokens}
G-->>B: tts_audio {final: true}
```
**关键优化**Eino Graph 以 Stream 模式运行ChatModel 的 token 流通过 Callback 的 `OnEndWithStreamOutput` 实时推送到客户端(`llm_chunk`),同时 Splitter 节点将 token 流切分为句子TTS 节点逐句合成并推送音频。LLM 文本流和 TTS 音频流**并行推送**,用户感知延迟大幅降低。
## 技术栈
### 前端
| 技术 | 选型 | 选择理由 |
|------|------|---------|
| 框架 | React 18 + TypeScript | 组件化开发,类型安全,生态成熟 |
| 构建 | Vite | 开发热更新快,构建产物小 |
| 实时通信 | WebSocket原生 API + 自封装连接管理 | 浏览器原生支持,封装心跳/重连/消息分发 |
| 语音检测 | @ricky0123/vad-web | 基于 WebRTC VAD纯前端零延迟 |
| 媒体采集 | MediaDevices API | 浏览器原生摄像头/麦克风访问 |
### 后端
| 技术 | 选型 | 选择理由 |
|------|------|---------|
| 语言 | Go | 高并发 goroutine 模型,适合长连接管理 |
| HTTP 框架 | Gin | 高性能 HTTP 路由,中间件生态成熟 |
| WebSocket | gorilla/websocket | Go 生态最成熟的 WebSocket 库 |
| 会话存储 | Memory / Redis / PostgreSQL 三级存储 | 进程内存零依赖Redis 支持多实例PG 持久化。TieredManager 自动降级 |
| AI 编排 | CloudWeGo Eino Graph | 声明式 DAG 编排Stream 模式Callback AOP |
| 持久化存储 | PostgreSQL | 对话历史、用户数据、会话元数据 |
| 配置管理 | Viper + godotenv | 支持 YAML + .env + 环境变量覆盖 |
| 日志 | Zap | 高性能结构化日志 |
### AI 服务
| 能力 | 默认方案 | 备选方案 |
|------|---------|---------|
| 多模态 LLM | DashScope qwen3-vl-plus | GPT-4o 等 OpenAI 兼容模型 |
| 语音识别 STT | MiMo ASR小米 | Deepgram |
| 语音合成 TTS | MiMo TTS小米 | OpenAI TTS |
> Go 网关的 AI 服务层统一封装不同服务商的调用接口,通过配置切换 provider。LLM 通过 Eino 框架的 `eino-ext/components/model/openai` 组件接入,支持任何 OpenAI 兼容接口。
## 后端模块
```mermaid
graph LR
subgraph Entry["入口层"]
Main["main.go<br/>依赖注入 / 启动"]
end
subgraph Transport["传输层"]
WSH["WebSocket Handler<br/>连接管理 / 认证"]
APH["REST API Handlers<br/>Auth / Conversation / Health"]
end
subgraph Business["业务层"]
SM["Session Manager<br/>会话生命周期"]
ORCH["EinoOrchestrator<br/>Eino Graph 编排"]
AS["Auth Service<br/>注册/登录/刷新/登出"]
end
subgraph Eino_Layer["Eino 编排层"]
PG["PipelineGraph<br/>7 节点 DAG"]
CB["Callback Handler<br/>LLM token 推送"]
ST["PipelineState<br/>跨节点状态"]
end
subgraph AI_Layer["AI 服务层"]
STT_S["STT Service<br/>MiMo / Deepgram"]
LLM_S["ChatModel<br/>eino-ext OpenAI 兼容"]
TTS_S["TTS Service<br/>MiMo / OpenAI"]
end
subgraph Data["数据层"]
UR["UserRepository"]
MR["MessageRepository"]
SR["SessionRepository"]
end
Main --> WSH
Main --> APH
Main --> SM
Main --> ORCH
Main --> AS
WSH --> SM
WSH --> ORCH
APH --> SM
APH --> AS
ORCH --> PG
PG --> CB
PG --> ST
PG --> STT_S
PG --> LLM_S
PG --> TTS_S
SM --> MR
SM --> SR
AS --> UR
```
| 模块 | 职责 |
|------|------|
| WebSocket Handler | 管理客户端连接生命周期JWT 认证conversation_id 恢复,单播消息推送 |
| Session Manager | 维护用户会话状态、对话历史。三级存储Memory → Redis → PostgreSQL30 分钟 TTLWrite-Through 到 PG |
| Eino 编排层 | 基于 CloudWeGo Eino Graph 的声明式 AI 编排。7 节点 DAGSTT→History→ChatModel→Msg2Str→Splitter→TTS→DoneStream 模式调用Callback 实现 LLM token 实时推送 |
| AI Orchestrator | `EinoOrchestrator` 适配器,包装 Eino Graph 实现 `Orchestrator` 接口。context 取消 + 超时控制 |
| AI Service Layer | AI 服务抽象层,多 provider 支持Deepgram/MiMo/OpenAI 等) |
| Auth | 用户认证与授权。JWT (HS256) 双 token 轮转bcrypt 密码哈希Gin 中间件 |
| Store | 持久化存储层。UserRepository / MessageRepository / SessionRepository内存 + PostgreSQL 双实现 |
| REST API | 健康检查、认证、对话管理端点 |
| Logger | Zap 结构化日志 |
| Models | 数据模型定义 |
| Migrations | 数据库版本化迁移,嵌入式 SQL 文件自动执行 |
| Model Router | 根据请求类型选择 AI 模型(待实现) |
| Rate Limiter | 令牌桶限流。详细设计见 [令牌桶限流设计](./13-令牌桶限流设计.md) |
## 前端组件
| 组件 | 职责 |
|------|------|
| LandingPage | 未登录时的着陆页(营销展示),内嵌 LoginModal 登录/注册弹窗 |
| AuthPage | 登录/注册表单(备用,已被 LandingPage + LoginModal 替代) |
| CameraManager | 摄像头流采集 |
| MicManager | 麦克风音频采集 |
| EdgeProcessor | VAD + 关键帧检测Canvas 像素比较) |
| WebSocketManager | WS 连接生命周期管理 |
| ChatPanel | 消息展示、流式回复、文本输入、场景选择 |
| VideoPreview | 摄像头画面预览 |
| SessionSidebar | 左侧抽屉式对话列表(搜索、重命名、删除、时间分组) |
| ConfigPanel | 右侧抽屉式配置面板主题、TTS 开关、detail level、语言、场景、登出 |
| Toast | 轻量通知提示3 秒自动消失) |
核心 Hook`useVisionSession()` 封装一次完整的视觉对话会话摄像头、VAD、WebSocket、消息状态、认证、场景模式`useSessionList()` 通过 REST API 管理对话列表 CRUD列表、创建、删除、重命名、加载消息
### 前端会话状态模型(三态)
前端 UI 存在三个会话状态,由 `isConnected``isCameraOn` 联合决定:
```
┌──────────┐ startSession() ┌──────────┐
│ initial │ ──────────────────→ │ video │
│ 初始态 │ │ 视频通话 │
└──────────┘ └──────────┘
↑ │
│ stopSession() stopVideo()
│ │
│ ▼
│ ┌──────────┐
└──────────────────────── │ textOnly │
│ 文字对话 │
└──────────┘
startSession()
┌──────────┐
│ video │
└──────────┘
```
| 状态 | 条件 | WebSocket | 摄像头 | 消息 | 文字输入 |
|------|------|-----------|--------|------|---------|
| `initial` | `!isConnected && messages.length === 0` | 断开 | 关闭 | 空 | 可用(自动连接) |
| `video` | `isConnected && isCameraOn` | 连接 | 开启 | 有 | 可用 |
| `textOnly` | `isConnected && !isCameraOn` | 连接 | 关闭 | 保留 | 可用 |
- **`stopVideo()`**:停止摄像头/麦克风/VAD保持 WebSocket 连接和消息历史,用户可继续文字对话
- **`stopSession()`**:完全断开 WebSocket、清空消息、重置状态回到初始态
## 数据库设计
### ER 关系
```mermaid
erDiagram
users ||--o{ sessions : "1:N"
users ||--o{ refresh_tokens : "1:N"
sessions ||--o{ messages : "1:N"
users {
uuid id PK
varchar username UK
varchar password_hash
timestamptz created_at
timestamptz updated_at
}
sessions {
uuid id PK
uuid user_id FK
varchar title
jsonb config
timestamptz created_at
timestamptz updated_at
}
messages {
bigserial id PK
uuid session_id FK
varchar role
text content
integer tokens_used
timestamptz created_at
}
refresh_tokens {
bigserial id PK
uuid user_id FK
varchar token_hash UK
timestamptz expires_at
timestamptz created_at
}
```
### 表结构
```sql
-- 用户表
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR(64) NOT NULL UNIQUE,
password_hash VARCHAR(256) NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- 会话表
CREATE TABLE sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title VARCHAR(128) DEFAULT '新对话',
config JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- 消息表
CREATE TABLE messages (
id BIGSERIAL PRIMARY KEY,
session_id UUID NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
role VARCHAR(16) NOT NULL,
content TEXT NOT NULL,
tokens_used INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT now()
);
-- 刷新令牌表
CREATE TABLE refresh_tokens (
id BIGSERIAL PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash VARCHAR(256) NOT NULL UNIQUE,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
```
### 存储策略
| 场景 | 存储方案 | 说明 |
|------|---------|------|
| 默认 | Memory进程内 | 零依赖快速启动。MemoryManager 支持 Write-Through 到 PG |
| 持久化 | Memory + PostgreSQL | 通过 `storage.persistence.enabled: true` 启用MemoryManager 注入 PG Repository |
| 多实例 | Redis独立 | 通过配置切换到 RedisManager适合多实例部署 |
| 三级存储 | TieredManager | L1 Memory → L2 Redis → L3 PostgreSQL自动降级 |
**三级存储架构**`TieredManager`
```
TieredManager
├── L1: Memory进程内缓存微秒级读写
├── L2: Redis分布式缓存毫秒级读写
└── L3: PostgreSQL持久化存储冷数据
```
- **读取路径**L1 → L2 → L3逐级回源命中后向上回填
- **写入路径**L1 → L2同步 → L3异步
- **健康检查**:后台 goroutine 每 30 秒 ping Redis故障时自动降级为 L1+L3 模式
- **冷热分离**L1/L2 存"热数据"当前对话上下文L3 存"冷数据"(历史记录)
## 认证设计
采用 **JWT 双 token 轮转认证机制**,结合 bcrypt 密码哈希和 Refresh Token Rotation 安全策略。详细设计见 [鉴权体系设计](./12-鉴权体系设计.md)。
### 核心组件
| 组件 | 职责 |
|------|------|
| TokenManager | JWT 生成与验证HS256 算法) |
| AuthService | 认证业务逻辑(注册/登录/刷新/登出) |
| AuthMiddleware | Gin 中间件,校验 access_token 并注入用户信息 |
| PasswordUtil | bcrypt 密码哈希cost=10 |
### 认证流程
```mermaid
sequenceDiagram
participant C as 客户端
participant G as Go 网关
participant DB as PostgreSQL
Note over C,DB: 注册流程
C->>G: POST /api/auth/register {username, password}
G->>G: bcrypt hash 密码
G->>DB: INSERT users
G->>G: 生成 access_token + refresh_token
G->>DB: 存 SHA256(refresh_token)
G-->>C: {user, access_token, refresh_token}
Note over C,DB: 登录流程
C->>G: POST /api/auth/login {username, password}
G->>DB: 查 users by username
G->>G: bcrypt.CompareHashAndPassword
G->>G: 生成 token pair
G->>DB: 存 SHA256(refresh_token)
G-->>C: {user, access_token, refresh_token}
Note over C,DB: Token 刷新(轮转)
C->>G: POST /api/auth/refresh {refresh_token}
G->>G: 校验签名和过期
G->>DB: 验证 hash 存在
G->>DB: 撤销旧 refresh_token
G->>G: 生成新 token pair
G->>DB: 存新 refresh_token hash
G-->>C: {access_token, refresh_token}
```
### Token 策略
- **access_token**15 分钟有效,用于 API 认证和 WebSocket 连接
- **refresh_token**7 天有效,用于刷新 access_token
- **Refresh Token Rotation**:每次 refresh 都生成新的 token pair旧 refresh_token 立即失效
- **复用检测**:检测到已删除的 refresh_token 被复用时,吊销该用户的所有 refresh_token
### 安全机制
1. **密码安全**bcrypt 算法cost=10自动生成盐值防彩虹表攻击
2. **Token 安全**
- access_token 短有效期15 分钟),降低泄露风险
- refresh_token 使用 SHA256 哈希存储,不存储原始 token
- Refresh Token Rotation 防重放攻击
- 复用检测 + 自动吊销机制
3. **传输安全**HTTPS 强制CORS 限制HttpOnly Cookie 存储 refresh_token
4. **防攻击策略**
- 防暴力破解:可选速率限制
- 防枚举攻击:统一错误信息
- 防 Token 泄露:复用检测 + 自动吊销
### WebSocket 认证
连接地址:`ws://host/ws?token=<access_token>&conversation_id=<uuid>`
- HTTP Upgrade 前校验 token
- 校验失败返回 401 Unauthorized
- 校验成功后user_id 和 username 注入到连接上下文
### 配置
```yaml
auth:
jwt_secret: "" # JWT 签名密钥(必须通过 CAMTALK_AUTH_JWT_SECRET 环境变量设置)
access_ttl: 15 # access_token 有效期(分钟)
refresh_ttl: 10080 # refresh_token 有效期分钟7天
```
> **安全要求**`JWT_SECRET` **必须**通过环境变量设置,不能写入配置文件。生产环境使用 `openssl rand -hex 32` 生成随机密钥。
## 部署架构
```mermaid
graph TB
User["用户浏览器"] --> Nginx
subgraph Nginx["Nginx 反向代理"]
Static["/ → 前端静态资源"]
API["/api/* → Go Gateway"]
WS_Proxy["/ws → Go Gateway"]
end
subgraph Gateway_Pool["Go Gateway 实例"]
G1["Gateway-1"]
G2["Gateway-2"]
GN["Gateway-N"]
end
Nginx --> G1
Nginx --> G2
Nginx --> GN
G1 --> Redis
G2 --> Redis
GN --> Redis
G1 --> PG_DB["PostgreSQL"]
G2 --> PG_DB
GN --> PG_DB
G1 --> AI_Services["AI Services外部 API"]
G2 --> AI_Services
GN --> AI_Services
```
**跨域策略**Nginx 将前端(`/`、REST API`/api/*`、WebSocket`/ws`)统一反代到同一域名,浏览器无跨域问题。
**开发环境**:前端 Vite :5173 通过 `server.proxy` 转发 `/ws``/api` 到后端 :8080无需硬编码端口。