docs: 按功能模块重构文档结构
- 新建 01-架构设计.md:合并项目概述+系统架构+持久化设计,含 Mermaid 架构图、模块图、时序图、ER 图、部署图 - 新建 02-接口文档.md:合并接口文档+持久化 API+用户模块 API,统一格式去重 - 重编号 03~09,去掉状态标注,规划中功能标记为待实现 - 删除 PLAN_BACKEND.md、PLAN_USER_MODULE.md 及冗余文档
This commit is contained in:
389
docs/01-架构设计.md
Normal file
389
docs/01-架构设计.md
Normal file
@@ -0,0 +1,389 @@
|
||||
# 架构设计
|
||||
|
||||
## 项目概述
|
||||
|
||||
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/>STT→LLM→TTS 流式并行"]
|
||||
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 API?1)API Key 安全性;2)统一的速率限制和成本管控;3)多模型路由逻辑集中在一处便于维护。
|
||||
|
||||
## 核心交互流程
|
||||
|
||||
一次完整的"用户提问 → AI 回答"流程:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant B as 浏览器
|
||||
participant G as Go 网关
|
||||
participant S as STT
|
||||
participant L as LLM
|
||||
participant T as TTS
|
||||
|
||||
B->>B: VAD 检测到语音结束
|
||||
B->>G: query {image, audio}
|
||||
G->>S: 音频流
|
||||
S-->>G: 流式文本
|
||||
G-->>B: stt_result {text}
|
||||
|
||||
G->>L: [图像 + 文本 + 上下文]
|
||||
loop LLM 流式输出
|
||||
L-->>G: token delta
|
||||
G-->>B: llm_chunk {delta}
|
||||
end
|
||||
G-->>B: llm_done {full_text, tokens}
|
||||
|
||||
par LLM 输出的同时
|
||||
G->>G: 句子切分器检测到完整句子
|
||||
G->>T: 句子文本
|
||||
T-->>G: 音频 chunk
|
||||
G-->>B: tts_audio {audio}
|
||||
end
|
||||
G-->>B: tts_audio {final: true}
|
||||
```
|
||||
|
||||
**关键优化**:LLM 文本流和 TTS 音频流**并行推送**——客户端先逐 token 展示文字,同时 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 | 进程内存零依赖,Redis 支持多实例部署 |
|
||||
| 持久化存储 | PostgreSQL | 对话历史、用户数据、会话元数据 |
|
||||
| 配置管理 | Viper + godotenv | 支持 YAML + .env + 环境变量覆盖 |
|
||||
| 日志 | Zap | 高性能结构化日志 |
|
||||
|
||||
### AI 服务
|
||||
|
||||
| 能力 | 默认方案 | 备选方案 |
|
||||
|------|---------|---------|
|
||||
| 多模态 LLM | GPT-4o | 通义千问等 OpenAI 兼容模型 |
|
||||
| 语音识别 STT | Deepgram | MiMo ASR(小米) |
|
||||
| 语音合成 TTS | OpenAI TTS | MiMo TTS(小米) |
|
||||
|
||||
> Go 网关的 AI 服务层统一封装不同服务商的调用接口,通过配置切换 provider。
|
||||
|
||||
## 后端模块
|
||||
|
||||
```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["Orchestrator<br/>STT→LLM→TTS 编排"]
|
||||
AS["Auth Service<br/>注册/登录/刷新/登出"]
|
||||
end
|
||||
|
||||
subgraph AI_Layer["AI 服务层"]
|
||||
STT_S["STT Service<br/>Deepgram / MiMo"]
|
||||
LLM_S["LLM Service<br/>OpenAI 兼容"]
|
||||
TTS_S["TTS Service<br/>OpenAI / MiMo"]
|
||||
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 --> STT_S
|
||||
ORCH --> LLM_S
|
||||
ORCH --> TTS_S
|
||||
SM --> MR
|
||||
SM --> SR
|
||||
AS --> UR
|
||||
```
|
||||
|
||||
| 模块 | 职责 |
|
||||
|------|------|
|
||||
| WebSocket Handler | 管理客户端连接生命周期,JWT 认证,conversation_id 恢复,单播消息推送 |
|
||||
| Session Manager | 维护用户会话状态、对话历史。Memory(默认)/ Redis(可切换),30 分钟 TTL,Write-Through 到 PG |
|
||||
| AI Orchestrator | 编排 STT→LLM→TTS 流式并行管道,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 | 令牌桶限流(待实现) |
|
||||
|
||||
## 前端组件
|
||||
|
||||
| 组件 | 职责 |
|
||||
|------|------|
|
||||
| AuthPage | 登录/注册表单 |
|
||||
| CameraManager | 摄像头流采集 |
|
||||
| MicManager | 麦克风音频采集 |
|
||||
| EdgeProcessor | VAD + 关键帧检测(Canvas 像素比较) |
|
||||
| WebSocketManager | WS 连接生命周期管理 |
|
||||
| ChatPanel | 消息展示、流式回复、文本输入、场景选择 |
|
||||
| VideoPreview | 摄像头画面预览 |
|
||||
| SessionSidebar | 左侧抽屉式对话列表(搜索、重命名、删除) |
|
||||
| ConfigPanel | 右侧抽屉式配置面板(主题、TTS 开关、detail level、语言、场景、账户) |
|
||||
| Toast | 轻量通知提示(3 秒自动消失) |
|
||||
|
||||
核心 Hook:`useVisionSession()` 封装一次完整的视觉对话会话(摄像头、VAD、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.driver: postgres` 启用,MemoryManager 注入 PG Repository |
|
||||
| 多实例 | Redis(独立) | 通过配置切换到 RedisManager,适合多实例部署 |
|
||||
|
||||
冷热分离:Redis/Memory 存"热数据"(当前对话上下文,微秒级读写),PostgreSQL 存"冷数据"(历史记录)。MemoryManager 的 Write-Through 机制确保每次 AppendMessage 同时写入 PG,重启后可从 PG 恢复会话。
|
||||
|
||||
## 认证设计
|
||||
|
||||
```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 分钟有效,refresh_token 7 天有效。refresh 时旧 token 失效(轮转),防止重放攻击。
|
||||
|
||||
**WebSocket 认证**:连接地址 `ws://host/ws?token=<access_token>&conversation_id=<uuid>`。HTTP Upgrade 前校验 token,失败返回 401。
|
||||
|
||||
## 部署架构
|
||||
|
||||
```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,无需硬编码端口。
|
||||
@@ -1,28 +0,0 @@
|
||||
# 项目概述
|
||||
|
||||
## 概述
|
||||
|
||||
开发一款**多模态实时对话应用**——通过摄像头与麦克风捕获用户的视觉场景与语音输入,由 AI 理解并给出自然、流畅的回应。
|
||||
|
||||
核心挑战在于三个维度之间的张力:
|
||||
|
||||
| 维度 | 关键问题 | 详见 |
|
||||
|------|---------|------|
|
||||
| 视觉理解 | 如何准确理解摄像头画面中的人物、物体、场景? | `07-视觉理解.md` |
|
||||
| 语音交互 | 如何让对话像真人交流一样自然、低延迟? | `06-语音交互.md` |
|
||||
| 成本控制 | 实时视频流 + LLM 推理,如何避免账单爆炸? | `08-成本控制.md` |
|
||||
|
||||
> 提升视觉精度意味着更高分辨率和更频繁的采样,但这会直接推高带宽和推理成本。架构设计需要在三者之间做好取舍。
|
||||
|
||||
## 项目目标
|
||||
|
||||
1. **用户故事规划**:明确"AI 能看、能听、能说"需要覆盖哪些场景 → `05-用户故事.md`
|
||||
2. **成本控制策略**:从架构设计层面融入运营成本意识 → `08-成本控制.md`
|
||||
|
||||
## 交付物
|
||||
|
||||
- 可运行的应用程序(摄像头 + 麦克风 → AI 回应)
|
||||
- 设计文档,覆盖:
|
||||
- 计划实现 vs 最终实现的用户故事
|
||||
- 成本控制技巧的构思 vs 实际采用的方案
|
||||
- 项目架构设计与技术选型
|
||||
File diff suppressed because it is too large
Load Diff
256
docs/02-系统架构.md
256
docs/02-系统架构.md
@@ -1,256 +0,0 @@
|
||||
# 系统架构
|
||||
|
||||
## 概述
|
||||
|
||||
三层架构:**前端做轻量预处理,后端做智能编排,云端 AI 服务按需调用**。在保证交互体验的同时控制成本。
|
||||
|
||||
## 三层架构
|
||||
|
||||
| 层级 | 职责 | 关键约束 |
|
||||
|------|------|---------|
|
||||
| **客户端(浏览器)** | 媒体采集、边缘预处理、UI 渲染 | 浏览器资源有限,模型需轻量 |
|
||||
| **Go 网关** | 会话管理、AI 服务编排、流式管道 | 高并发、低延迟、状态管理 |
|
||||
| **AI 服务** | LLM 推理、语音识别、语音合成 | 按量计费,需控制调用频率 |
|
||||
|
||||
> 为什么要单独加一层 Go 网关,而不是让前端直连 AI API?1)API Key 安全性;2)统一的速率限制和成本管控;3)多模型路由逻辑集中在一处便于维护。
|
||||
|
||||
## 技术栈
|
||||
|
||||
### 前端
|
||||
|
||||
| 技术 | 选型 | 选择理由 |
|
||||
|------|------|---------|
|
||||
| 框架 | React 18 + TypeScript | 组件化开发,类型安全,生态成熟 |
|
||||
| 构建 | Vite | 开发热更新快,构建产物小 |
|
||||
| 实时通信 | WebSocket(原生 API) + 自封装连接管理 | 浏览器原生支持,封装心跳/重连/消息分发 |
|
||||
| 边缘推理 | ONNX Runtime Web | 浏览器端跑轻量模型(VAD、关键帧检测) |
|
||||
| 语音检测 | @ricky0123/vad-web | 基于 WebRTC VAD,纯前端零延迟 |
|
||||
| 媒体采集 | MediaDevices API | 浏览器原生摄像头/麦克风访问 |
|
||||
|
||||
### 后端
|
||||
|
||||
| 技术 | 选型 | 选择理由 |
|
||||
|------|------|---------|
|
||||
| 语言 | Go | 高并发 goroutine 模型,适合长连接管理 |
|
||||
| HTTP 框架 | Gin | 高性能 HTTP 路由,中间件生态成熟 |
|
||||
| WebSocket | gorilla/websocket | Go 生态最成熟的 WebSocket 库 |
|
||||
| 会话存储 | Redis(已实现) / Memory(默认) | 高速 KV 存储,Memory 为默认实现,Redis 已实现可通过配置切换 |
|
||||
| 持久化存储 | PostgreSQL(已实现) | 对话历史、用户数据、会话持久化。MemoryManager 支持 Write-Through 到 PG |
|
||||
| 配置管理 | Viper + godotenv | 支持 YAML + .env + 环境变量覆盖,详见 `03-接口文档.md` 第六章 |
|
||||
| 日志 | Zap | 高性能结构化日志 |
|
||||
|
||||
### AI 服务
|
||||
|
||||
| 能力 | 主选方案 | 备选方案 | 选型考量 |
|
||||
|------|---------|---------|---------|
|
||||
| 多模态 LLM | GPT-4o(默认) | 通义千问等 OpenAI 兼容模型 | 通过 OpenAI 兼容接口,可灵活切换 |
|
||||
| 语音识别 STT | Deepgram(默认) | MiMo ASR(小米) | 支持多 provider 切换 |
|
||||
| 语音合成 TTS | OpenAI TTS(默认) | MiMo TTS(小米) | 支持多 provider 切换 |
|
||||
|
||||
> 不必绑定单一厂商。Go 网关的 AI 服务层统一封装不同服务商的调用接口,通过配置切换 provider。
|
||||
|
||||
## 核心交互流程
|
||||
|
||||
一次完整的"用户提问 → AI 回答"流程:
|
||||
|
||||
```
|
||||
Browser Go Gateway STT LLM TTS
|
||||
| | | | |
|
||||
|-- VAD 检测到语音结束 --->| | | |
|
||||
| | | | |
|
||||
|-- [音频+图像] -------->| | | |
|
||||
| |--- 音频流 ------->| | |
|
||||
| |<-- 流式文本 ------| | |
|
||||
| | | | |
|
||||
| |--- [图像+文本+上下文] -------->| |
|
||||
| |<-- 流式回答文本 --------------| |
|
||||
|<-- 推送回答文本 --------| | | |
|
||||
| |--- 回答文本 ---------------------------->|
|
||||
| |<-- 流式音频 --------------------------------|
|
||||
|<-- 推送音频流 ----------| | | |
|
||||
| | | | |
|
||||
|-> 播放音频 + 渲染文字 | | | |
|
||||
```
|
||||
|
||||
**关键优化**:LLM 文本流和 TTS 音频流是**并行推送**的——客户端先展示文字,同时开始播放语音,用户感知延迟大幅降低。
|
||||
|
||||
## 后端模块
|
||||
|
||||
| 模块 | 职责 | 关键实现 | 状态 |
|
||||
|------|------|---------|------|
|
||||
| WebSocket Handler | 管理客户端连接生命周期,单播消息推送 | goroutine per connection,JWT 认证,conversation_id 恢复 | ✅ 已完成 |
|
||||
| Session Manager | 维护用户会话状态、对话历史 | Memory(默认)/ Redis(可切换),30 分钟 TTL,Write-Through 到 PG(详见 `03-接口文档.md` 第五章) | ✅ 已完成 |
|
||||
| AI Orchestrator | 编排 STT→LLM→TTS 流式并行管道 | context 取消 + 超时控制 + 句子切分 | ✅ 已完成 |
|
||||
| AI Service Layer | AI 服务抽象层(STT/LLM/TTS) | 多 provider 支持(Deepgram/MiMo/OpenAI 等) | ✅ 已完成 |
|
||||
| Auth | 用户认证与授权 | JWT (HS256) 双 token 轮转,bcrypt 密码哈希,Gin 中间件 | ✅ 已完成 |
|
||||
| Store | 持久化存储层 | UserRepository / MessageRepository / SessionRepository,内存 + PostgreSQL 双实现 | ✅ 已完成 |
|
||||
| REST API | 健康检查、认证、对话管理端点 | Gin 路由,输入校验,权限校验 | ✅ 已完成 |
|
||||
| Error Handler | 统一错误码定义与发送 | 错误码枚举 | ✅ 已完成 |
|
||||
| Logger | 日志初始化封装 | Zap 结构化日志 | ✅ 已完成 |
|
||||
| Models | 数据模型定义 | WebSocket 消息、会话、配置、用户等 | ✅ 已完成 |
|
||||
| Migrations | 数据库版本化迁移 | 嵌入式 SQL 文件,自动执行,版本跟踪 | ✅ 已完成 |
|
||||
| Model Router | 根据请求类型选择 AI 模型 | 规则引擎 + 成本阈值 | 📋 规划中 |
|
||||
| Rate Limiter | 防止单用户过度消耗 API 额度 | 令牌桶算法 | 📋 规划中 |
|
||||
|
||||
AI Orchestrator 核心接口(`internal/orchestrator/orchestrator.go`):
|
||||
|
||||
```go
|
||||
// Orchestrator AI 编排器接口。
|
||||
type Orchestrator interface {
|
||||
ProcessQuery(ctx context.Context, sessionID string, req models.WsQuery,
|
||||
history []models.Message, sender Sender) error
|
||||
}
|
||||
```
|
||||
|
||||
Pipeline 实现(`internal/orchestrator/pipeline.go`)流程:
|
||||
1. Base64 解码音频/图片
|
||||
2. 调用 `stt.Recognize()` → 发送 `stt_result`
|
||||
3. 调用 `llm.ChatStream()` 获取流式输出,goroutine 消费 token → 发送 `llm_chunk` + 句子切分
|
||||
4. 另一 goroutine 从句子 channel 读取 → 调用 `tts.SynthesizeStream()` → 发送 `tts_audio`
|
||||
5. 流结束 → 发送 `llm_done`
|
||||
6. TTS 失败静默跳过,STT/LLM 失败发送对应 error 消息
|
||||
|
||||
> **关键优化**:LLM 文本流和 TTS 音频流**并行推送**——客户端先逐 token 展示文字,同时 TTS 逐句子合成并推送音频,用户感知延迟大幅降低。详细的 AI 服务层接口和编排策略见 `03-接口文档.md` 第三、四章。
|
||||
|
||||
## 前端组件
|
||||
|
||||
| 组件 | 职责 |
|
||||
|------|------|
|
||||
| AuthPage | 登录/注册表单,前端校验,Tab 切换 |
|
||||
| CameraManager | 摄像头流采集 |
|
||||
| MicManager | 麦克风音频采集 |
|
||||
| EdgeProcessor | VAD + 关键帧检测(Canvas 像素比较) |
|
||||
| WebSocketManager | WS 连接生命周期管理 |
|
||||
| ChatPanel | 消息展示、流式回复、文本输入、场景选择 |
|
||||
| VideoPreview | 摄像头画面预览 |
|
||||
| SessionSidebar | 左侧抽屉式对话列表(搜索、重命名、删除) |
|
||||
| ConfigPanel | 右侧抽屉式配置面板(主题、TTS 开关、detail level、语言、场景、账户) |
|
||||
| Toast | 轻量通知提示(3 秒自动消失) |
|
||||
|
||||
核心 Hook:`useVisionSession()` 封装一次完整的视觉对话会话(摄像头、VAD、WebSocket、消息状态、认证、场景模式)。
|
||||
|
||||
```typescript
|
||||
// useVisionSession 核心职责(简化示意)
|
||||
function useVisionSession() {
|
||||
// 组合:useCamera + useMicrophone + useVAD + useWebSocketManager + useObservationMode
|
||||
// 管理:消息状态、流式回复、处理标志、配置、统计、模式
|
||||
|
||||
// VAD onSpeechEnd: 捕获帧 + 音频 → 发送 query 消息
|
||||
// 服务端消息处理:stt_result / llm_chunk / llm_done / tts_audio / error
|
||||
// 文本输入:sendTextMessage() 支持手动输入文字(跳过 STT)
|
||||
// 场景模式:config 消息支持 scenario 字段(free_chat / interviewer / english_teacher 等)
|
||||
// 打断:interrupt() 发送中断消息 + 停止 TTS + 保存部分回复
|
||||
// 认证:WebSocket 连接携带 JWT token,支持 conversation_id 恢复历史对话
|
||||
}
|
||||
```
|
||||
|
||||
## 存储策略(分阶段)
|
||||
|
||||
| 阶段 | 存储方案 | 持久化内容 | 理由 |
|
||||
|------|---------|-----------|------|
|
||||
| 当前默认 | Memory(进程内) | 会话状态 + 对话历史 | 零依赖,快速启动。MemoryManager 支持 Write-Through 到 PG |
|
||||
| 已实现 | Memory + PostgreSQL | 用户数据、对话历史、会话元数据 | 通过 `storage.driver: postgres` 启用,MemoryManager 注入 PG Repository |
|
||||
| 已实现 | Redis(独立) | 会话状态 + 对话历史 | 通过配置切换到 RedisManager,适合多实例部署 |
|
||||
|
||||
冷热分离:Redis/Memory 存"热数据"(当前对话上下文,微秒级读写),PostgreSQL 存"冷数据"(历史记录)。MemoryManager 的 Write-Through 机制确保每次 AppendMessage 同时写入 PG,重启后可从 PG 恢复会话。
|
||||
|
||||
### PostgreSQL 表设计(已实现)
|
||||
|
||||
实际迁移文件位于 `backend/migrations/`,通过 `go:embed` 嵌入,启动时自动执行:
|
||||
|
||||
```sql
|
||||
-- 001_users.up.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 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()
|
||||
);
|
||||
|
||||
-- 002_messages.up.sql
|
||||
CREATE TABLE messages (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
session_id UUID NOT NULL,
|
||||
role VARCHAR(16) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
tokens_used INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
-- 003_sessions.up.sql
|
||||
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()
|
||||
);
|
||||
```
|
||||
|
||||
## 部署架构
|
||||
|
||||
```
|
||||
用户浏览器
|
||||
↓
|
||||
Nginx(同源反代 + 负载均衡)
|
||||
├── / → 前端静态资源(CDN 或本地 dist)
|
||||
├── /api/* → Go Gateway(REST API)
|
||||
└── /ws → Go Gateway(WebSocket)
|
||||
├── Gateway-1 ──→ Redis
|
||||
├── Gateway-2 ──→ Redis
|
||||
└── Gateway-N ──→ AI Services(外部 API)
|
||||
```
|
||||
|
||||
**跨域策略**:Nginx 将前端和后端统一到同一域名下,浏览器无跨域问题。
|
||||
|
||||
### Nginx 配置
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name camtalk.example.com;
|
||||
|
||||
# 前端静态资源
|
||||
location / {
|
||||
root /var/www/camtalk/dist;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# REST API 反代
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# WebSocket 反代
|
||||
location /ws {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_read_timeout 86400s; # 长连接超时 24h
|
||||
proxy_send_timeout 86400s;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> WebSocket 是长连接,Nginx 必须配置 `Upgrade` 和 `Connection` 头。`proxy_read_timeout` 需要覆盖心跳间隔(客户端 30s ping),否则 Nginx 会主动断开空闲连接。
|
||||
|
||||
### 开发环境
|
||||
|
||||
开发时前端(Vite :5173)和后端(Gin :8080)不同端口。前端 WebSocket 地址基于 `window.location.host` 动态构建,通过 Vite `server.proxy` 转发到后端,无需硬编码端口。
|
||||
|
||||
`vite.config.ts` 中配置了 `/ws`(WebSocket)和 `/api`(REST)的代理,目标为 `http://localhost:8080`。
|
||||
@@ -4,25 +4,25 @@
|
||||
|
||||
本文档记录项目中各项技术的**选型过程、替代方案对比和决策理由**。技术选型没有"绝对正确",只有"更适合"。
|
||||
|
||||
**定位**:本文档记录各项技术的选型过程和决策理由。AI 服务栈、持久化层、认证系统均已实现并通过配置灵活切换。前端边缘处理已确定技术栈。
|
||||
**定位**:本文档记录各项技术的选型过程和决策理由。
|
||||
|
||||
```
|
||||
技术选型
|
||||
├── AI 服务栈(✅ 已实现)
|
||||
├── AI 服务栈
|
||||
│ ├── STT: Deepgram(默认) / MiMo ASR
|
||||
│ ├── LLM: GPT-4o(默认) / 通义千问等 OpenAI 兼容模型
|
||||
│ └── TTS: OpenAI TTS(默认) / MiMo TTS
|
||||
├── 持久化层(✅ 已实现)
|
||||
├── 持久化层
|
||||
│ ├── 数据库: PostgreSQL(pgx/v5,手写 SQL)
|
||||
│ ├── 迁移: 嵌入式 SQL 文件,自动执行
|
||||
│ └── 存储模式: Memory(默认)+ Write-Through 到 PG / Redis(可切换)
|
||||
├── 认证与用户系统(✅ 已实现)
|
||||
├── 认证与用户系统
|
||||
│ ├── 认证方案: JWT (HS256), access 15min + refresh 7day
|
||||
│ ├── JWT 库: golang-jwt/jwt/v5
|
||||
│ ├── 密码哈希: bcrypt
|
||||
│ ├── 数据库驱动: pgx/v5(手写 SQL,不用 ORM)
|
||||
│ └── 前端 Token 存储: localStorage
|
||||
└── 前端边缘处理层(✅ 已实现)
|
||||
└── 前端边缘处理层
|
||||
├── 关键帧检测: Canvas 像素比较(160x120 降采样)
|
||||
├── 语音检测: @ricky0123/vad-web
|
||||
└── 媒体采集: MediaDevices API
|
||||
@@ -64,7 +64,7 @@
|
||||
|
||||
---
|
||||
|
||||
## 二、持久化层选型(已实现)
|
||||
## 二、持久化层选型
|
||||
|
||||
### 数据特征分析
|
||||
|
||||
@@ -61,7 +61,7 @@ vad.start();
|
||||
方案选择:
|
||||
- **OpenAI TTS**(默认):音质好,延迟中等,按字符计费,模型 tts-1
|
||||
- **MiMo TTS**(小米):国产替代,通过配置切换
|
||||
- **Edge TTS**(规划中):微软免费方案,音质不错,延迟略高
|
||||
- **Edge TTS**(待实现):微软免费方案,音质不错,延迟略高
|
||||
|
||||
## 延迟优化要点
|
||||
|
||||
@@ -40,11 +40,11 @@ const ACTIVE_INTERVAL = 1000; // 用户说话时 1 秒一帧
|
||||
不是所有计算都需要上云。可前置到客户端的计算:
|
||||
|
||||
- **VAD 语音检测**:浏览器端完成,减少无效音频上传(节省 ~70% 带宽)
|
||||
- **人脸/物体检测**(规划中):用 ONNX Runtime 跑轻量模型(如 YOLOv8-nano ~6MB,推理 ~30ms),只在检测到新物体时触发 LLM。当前 MVP 使用 Canvas 像素比较做关键帧检测
|
||||
- **人脸/物体检测**(待实现):用 ONNX Runtime 跑轻量模型(如 YOLOv8-nano ~6MB,推理 ~30ms),只在检测到新物体时触发 LLM。当前 MVP 使用 Canvas 像素比较做关键帧检测
|
||||
- **重复画面过滤**:计算帧间相似度,对话模式 similarity > 0.9 跳过,观察模式 similarity < 0.85 触发
|
||||
- **敏感内容过滤**(规划中):NSFW 检测前置,避免无效 API 调用
|
||||
- **敏感内容过滤**(待实现):NSFW 检测前置,避免无效 API 调用
|
||||
|
||||
## 策略三:模型分级——用对模型做对事(规划中)
|
||||
## 策略三:模型分级——用对模型做对事(待实现)
|
||||
|
||||
不是每个问题都需要最贵的模型:
|
||||
|
||||
@@ -57,8 +57,8 @@ const ACTIVE_INTERVAL = 1000; // 用户说话时 1 秒一帧
|
||||
|
||||
> 当前 MVP 阶段使用单一模型(默认 GPT-4o),模型分级路由为未来优化方向。通过配置 `ai.llm.model` 可手动切换模型。
|
||||
|
||||
## 策略四:缓存与复用(规划中)
|
||||
## 策略四:缓存与复用(待实现)
|
||||
|
||||
- **语义缓存**(规划中):相似问题直接返回缓存结果(如反复问"这是什么")
|
||||
- **语义缓存**(待实现):相似问题直接返回缓存结果(如反复问"这是什么")
|
||||
- **上下文复用**:连续对话中,未变化的图像不必重复发送(已通过重复画面过滤实现)
|
||||
- **对话历史裁剪**:前端按 `MAX_HISTORY_ROUNDS = 10` 裁剪,后端按 `defaultHistorySize = 20` 裁剪,限制每轮的固定 token 开销
|
||||
@@ -1,751 +0,0 @@
|
||||
# 持久化与用户系统设计
|
||||
|
||||
## 概述
|
||||
|
||||
本文档定义用户注册/登录、JWT 认证、对话历史持久化的完整设计方案。核心目标:**用户登录后可在对话列表中选择历史对话继续交谈**。
|
||||
|
||||
### 设计决策
|
||||
|
||||
| 决策项 | 选择 | 理由 |
|
||||
|--------|------|------|
|
||||
| 认证方式 | JWT(access + refresh 双 token) | 无状态,适合分布式部署 |
|
||||
| 注册方式 | 用户名 + 密码 | MVP 最简方案 |
|
||||
| 密码存储 | bcrypt hash | 行业标准,抗彩虹表 |
|
||||
| 对话恢复 | 对话列表选择 | 用户可见所有历史对话,自主选择继续或新建 |
|
||||
| 对话标题 | 自动取首条用户消息前 20 字符 | 零成本,自然可读 |
|
||||
| 图像持久化 | 不存储 | 节省空间,文字历史已足够 |
|
||||
| 登录后行为 | 先选对话,再进聊天 | 明确的入口,避免困惑 |
|
||||
| WS 认证 | URL query 参数 `?token=xxx` | HTTP Upgrade 无法带 Authorization header |
|
||||
| Token 策略 | access 15min + refresh 7day | 安全性与体验平衡 |
|
||||
|
||||
---
|
||||
|
||||
## 一、数据库设计
|
||||
|
||||
### 1.1 ER 关系
|
||||
|
||||
```
|
||||
users 1──N sessions 1──N messages
|
||||
│
|
||||
└── refresh_tokens (1──N, token 轮转管理)
|
||||
```
|
||||
|
||||
### 1.2 表结构
|
||||
|
||||
```sql
|
||||
-- 用户表
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
username VARCHAR(64) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(256) NOT NULL, -- bcrypt hash
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_username ON users(username);
|
||||
|
||||
-- 会话(对话)表
|
||||
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 '新对话',
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_sessions_user_id ON sessions(user_id, updated_at DESC);
|
||||
|
||||
-- 消息表
|
||||
CREATE TABLE messages (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
session_id UUID NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
||||
role VARCHAR(16) NOT NULL, -- "user" | "assistant"
|
||||
content TEXT NOT NULL,
|
||||
tokens_used INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_messages_session_id ON messages(session_id, id);
|
||||
|
||||
-- 刷新令牌表
|
||||
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, -- SHA256(refresh_token)
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_refresh_tokens_user ON refresh_tokens(user_id);
|
||||
CREATE INDEX idx_refresh_tokens_hash ON refresh_tokens(token_hash);
|
||||
```
|
||||
|
||||
### 1.3 与现有设计的差异
|
||||
|
||||
| 变更 | 原设计(`02-系统架构.md`) | 新设计 | 理由 |
|
||||
|------|--------------------------|--------|------|
|
||||
| `sessions.user_id` | `NOT NULL` 无外键 | `REFERENCES users(id) ON DELETE CASCADE` | 关联用户,级联删除 |
|
||||
| `sessions.title` | 无 | `VARCHAR(128) DEFAULT '新对话'` | 对话列表展示 |
|
||||
| `messages.image_url` | 有 | 移除 | 不存储图像 |
|
||||
| `usage_daily` | 有 | MVP 暂不实现 | 按需后加 |
|
||||
| 新增 `users` | 无 | 新增 | 用户系统核心 |
|
||||
| 新增 `refresh_tokens` | 无 | 新增 | JWT refresh 机制 |
|
||||
|
||||
---
|
||||
|
||||
## 二、JWT 认证设计
|
||||
|
||||
### 2.1 Token 结构
|
||||
|
||||
**access_token**:
|
||||
- payload: `{user_id, username, exp (15min), iat, iss: "camtalk"}`
|
||||
- 签名算法: HS256(对称密钥,从配置读取)
|
||||
- 存储位置: 前端 localStorage
|
||||
|
||||
**refresh_token**:
|
||||
- payload: `{user_id, token_id (UUID), exp (7day), iat, iss: "camtalk"}`
|
||||
- 存储位置: 前端 localStorage + 数据库 `refresh_tokens` 表(存 SHA256 hash)
|
||||
|
||||
### 2.2 认证流程
|
||||
|
||||
#### 注册
|
||||
|
||||
```
|
||||
用户 ──POST /api/auth/register──> 检查 username 唯一性
|
||||
bcrypt hash 密码
|
||||
INSERT users
|
||||
↓
|
||||
生成 access_token + refresh_token
|
||||
存 SHA256(refresh_token) 到 DB
|
||||
↓
|
||||
返回 {user, access_token, refresh_token}
|
||||
```
|
||||
|
||||
#### 登录
|
||||
|
||||
```
|
||||
用户 ──POST /api/auth/login──> 查 users 表 by username
|
||||
bcrypt.CompareHashAndPassword
|
||||
↓
|
||||
生成 access_token + refresh_token
|
||||
存 SHA256(refresh_token) 到 DB
|
||||
↓
|
||||
返回 {user, access_token, refresh_token}
|
||||
```
|
||||
|
||||
#### 刷新
|
||||
|
||||
```
|
||||
用户 ──POST /api/auth/refresh──> 校验 refresh_token 签名和过期
|
||||
查 DB 验证 hash 存在
|
||||
↓
|
||||
撤销旧 refresh_token(DELETE)
|
||||
生成新的 access + refresh
|
||||
存新 refresh_token hash
|
||||
↓
|
||||
返回 {access_token, refresh_token}
|
||||
```
|
||||
|
||||
#### 登出
|
||||
|
||||
```
|
||||
用户 ──POST /api/auth/logout──> 撤销 refresh_token (DELETE from DB)
|
||||
前端清除 localStorage
|
||||
```
|
||||
|
||||
### 2.3 Go 实现接口
|
||||
|
||||
```go
|
||||
// internal/auth/jwt.go
|
||||
|
||||
type Claims struct {
|
||||
UserID string `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
type TokenManager struct {
|
||||
secret []byte
|
||||
accessTTL time.Duration // 15min
|
||||
refreshTTL time.Duration // 7day
|
||||
}
|
||||
|
||||
// GeneratePair 生成 access + refresh token 对。
|
||||
func (tm *TokenManager) GeneratePair(userID, username string) (access, refresh string, err error)
|
||||
|
||||
// ValidateAccess 校验 access_token,返回 Claims。
|
||||
func (tm *TokenManager) ValidateAccess(tokenStr string) (*Claims, error)
|
||||
|
||||
// ValidateRefresh 校验 refresh_token 签名和过期(不查 DB,DB 校验由 service 层负责)。
|
||||
func (tm *TokenManager) ValidateRefresh(tokenStr string) (*Claims, error)
|
||||
|
||||
// HashToken 计算 token 的 SHA256 hash(用于 DB 存储)。
|
||||
func HashToken(token string) string
|
||||
```
|
||||
|
||||
```go
|
||||
// internal/auth/middleware.go
|
||||
|
||||
// AuthMiddleware Gin 中间件:从 Authorization: Bearer <token> 提取并校验。
|
||||
// 校验通过后将 Claims 写入 gin.Context。
|
||||
func AuthMiddleware(tm *TokenManager) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
auth := c.GetHeader("Authorization")
|
||||
if !strings.HasPrefix(auth, "Bearer ") {
|
||||
c.AbortWithStatusJSON(401, gin.H{"error": "missing token"})
|
||||
return
|
||||
}
|
||||
claims, err := tm.ValidateAccess(strings.TrimPrefix(auth, "Bearer "))
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(401, gin.H{"error": "invalid token"})
|
||||
return
|
||||
}
|
||||
c.Set("claims", claims)
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、REST API 设计
|
||||
|
||||
### 3.1 认证 API(新增)
|
||||
|
||||
#### 注册
|
||||
|
||||
```
|
||||
POST /api/auth/register
|
||||
Content-Type: application/json
|
||||
|
||||
{"username": "alice", "password": "s3cret123"}
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
// 201 Created
|
||||
{
|
||||
"user": {"id": "uuid", "username": "alice", "created_at": "2026-06-14T10:00:00Z"},
|
||||
"access_token": "eyJ...",
|
||||
"refresh_token": "eyJ..."
|
||||
}
|
||||
```
|
||||
|
||||
错误码:`USERNAME_TAKEN`(409)、`INVALID_INPUT`(400,用户名/密码格式不合规)
|
||||
|
||||
#### 登录
|
||||
|
||||
```
|
||||
POST /api/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{"username": "alice", "password": "s3cret123"}
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
// 200 OK
|
||||
{
|
||||
"user": {"id": "uuid", "username": "alice"},
|
||||
"access_token": "eyJ...",
|
||||
"refresh_token": "eyJ..."
|
||||
}
|
||||
```
|
||||
|
||||
错误码:`INVALID_CREDENTIALS`(401)
|
||||
|
||||
#### 刷新 Token
|
||||
|
||||
```
|
||||
POST /api/auth/refresh
|
||||
Content-Type: application/json
|
||||
|
||||
{"refresh_token": "eyJ..."}
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
// 200 OK
|
||||
{
|
||||
"access_token": "eyJ...",
|
||||
"refresh_token": "eyJ..."
|
||||
}
|
||||
```
|
||||
|
||||
错误码:`INVALID_TOKEN`(401)
|
||||
|
||||
#### 登出
|
||||
|
||||
```
|
||||
POST /api/auth/logout
|
||||
Authorization: Bearer <access_token>
|
||||
Content-Type: application/json
|
||||
|
||||
{"refresh_token": "eyJ..."}
|
||||
```
|
||||
|
||||
响应:`204 No Content`
|
||||
|
||||
### 3.2 对话管理 API(新增)
|
||||
|
||||
所有端点需要 `Authorization: Bearer <access_token>` header。
|
||||
|
||||
#### 获取对话列表
|
||||
|
||||
```
|
||||
GET /api/conversations?page=1&size=20
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
// 200 OK
|
||||
{
|
||||
"conversations": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"title": "这是一朵红色的玫瑰花",
|
||||
"last_message": "它看起来很美丽。",
|
||||
"message_count": 6,
|
||||
"updated_at": "2026-06-14T10:30:00Z"
|
||||
}
|
||||
],
|
||||
"total": 42,
|
||||
"page": 1,
|
||||
"size": 20
|
||||
}
|
||||
```
|
||||
|
||||
#### 创建新对话
|
||||
|
||||
```
|
||||
POST /api/conversations
|
||||
Content-Type: application/json
|
||||
|
||||
{}
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
// 201 Created
|
||||
{
|
||||
"id": "uuid",
|
||||
"title": "新对话",
|
||||
"created_at": "2026-06-14T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取对话详情
|
||||
|
||||
```
|
||||
GET /api/conversations/:id
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
// 200 OK
|
||||
{
|
||||
"id": "uuid",
|
||||
"title": "这是一朵红色的玫瑰花",
|
||||
"created_at": "2026-06-14T10:00:00Z",
|
||||
"updated_at": "2026-06-14T10:30:00Z",
|
||||
"config": {"tts_enabled": true, "detail_level": "low", "language": "zh-CN"}
|
||||
}
|
||||
```
|
||||
|
||||
#### 更新对话标题
|
||||
|
||||
```
|
||||
PATCH /api/conversations/:id
|
||||
Content-Type: application/json
|
||||
|
||||
{"title": "新的标题"}
|
||||
```
|
||||
|
||||
响应:`200 OK` + 更新后的对话详情
|
||||
|
||||
#### 删除对话
|
||||
|
||||
```
|
||||
DELETE /api/conversations/:id
|
||||
```
|
||||
|
||||
响应:`204 No Content`(级联删除 messages)
|
||||
|
||||
#### 获取对话历史消息
|
||||
|
||||
```
|
||||
GET /api/conversations/:id/messages?limit=50&before=<message_id>
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
// 200 OK
|
||||
{
|
||||
"messages": [
|
||||
{"id": 1, "role": "user", "content": "这是什么花?", "created_at": "..."},
|
||||
{"id": 2, "role": "assistant", "content": "这是一朵红色的玫瑰。", "tokens_used": 42, "created_at": "..."}
|
||||
],
|
||||
"has_more": false
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 现有 API 变更
|
||||
|
||||
| 端点 | 变更 |
|
||||
|------|------|
|
||||
| `GET /api/health` | 不变 |
|
||||
| `POST /api/sessions` | **废弃**,使用 `POST /api/conversations` 替代 |
|
||||
| `DELETE /api/sessions/{id}` | **废弃**,使用 `DELETE /api/conversations/:id` 替代 |
|
||||
|
||||
### 3.4 新增错误码
|
||||
|
||||
| 错误码 | HTTP 状态 | 含义 |
|
||||
|--------|-----------|------|
|
||||
| `USERNAME_TAKEN` | 409 | 用户名已被注册 |
|
||||
| `INVALID_CREDENTIALS` | 401 | 用户名或密码错误 |
|
||||
| `INVALID_TOKEN` | 401 | JWT 无效或已过期 |
|
||||
| `INVALID_INPUT` | 400 | 请求参数不合规(用户名/密码长度等) |
|
||||
|
||||
---
|
||||
|
||||
## 四、Session Manager 改造
|
||||
|
||||
### 4.1 接口扩展
|
||||
|
||||
```go
|
||||
// internal/session/manager.go
|
||||
|
||||
type Manager interface {
|
||||
// ===== 原有方法(签名变更) =====
|
||||
|
||||
// Create 创建新会话,关联 user_id。
|
||||
Create(ctx context.Context, userID string, config models.SessionConfig) (string, error)
|
||||
|
||||
Get(ctx context.Context, sessionID string) (*models.Session, error)
|
||||
UpdateConfig(ctx context.Context, sessionID string, patch models.SessionConfigPatch) error
|
||||
GetHistory(ctx context.Context, sessionID string, limit int) ([]models.Message, error)
|
||||
AppendMessage(ctx context.Context, sessionID string, msg models.Message) error
|
||||
SetActiveRequest(ctx context.Context, sessionID string, requestID string) error
|
||||
GetActiveRequestID(ctx context.Context, sessionID string) (string, error)
|
||||
ClearActiveRequest(ctx context.Context, sessionID string) error
|
||||
Touch(ctx context.Context, sessionID string) error
|
||||
Destroy(ctx context.Context, sessionID string) error
|
||||
ActiveCount() int
|
||||
|
||||
// ===== 新增方法 =====
|
||||
|
||||
// ListByUser 获取用户的对话列表(分页)。
|
||||
ListByUser(ctx context.Context, userID string, page, size int) ([]ConversationSummary, int, error)
|
||||
|
||||
// UpdateTitle 更新对话标题。
|
||||
UpdateTitle(ctx context.Context, sessionID string, title string) error
|
||||
|
||||
// LoadFromDB 从 PostgreSQL 加载历史消息到热存储(Redis/内存)。
|
||||
// 用户选择历史对话继续交谈时调用。
|
||||
LoadFromDB(ctx context.Context, sessionID string) error
|
||||
}
|
||||
|
||||
// ConversationSummary 对话列表项。
|
||||
type ConversationSummary struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
LastMessage string `json:"last_message"`
|
||||
MessageCount int `json:"message_count"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Model 变更
|
||||
|
||||
```go
|
||||
// internal/models/models.go
|
||||
|
||||
type Session struct {
|
||||
ID string `json:"session_id"`
|
||||
UserID string `json:"user_id"` // 新增
|
||||
Title string `json:"title"` // 新增
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"` // 新增
|
||||
Config SessionConfig `json:"config"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
PasswordHash string `json:"-"` // 不序列化到 JSON
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 冷热数据策略
|
||||
|
||||
```
|
||||
当前活跃会话: Redis/内存(热) ←→ PostgreSQL(冷,write-through)
|
||||
历史会话加载: PostgreSQL → Redis/内存(按需恢复)
|
||||
```
|
||||
|
||||
**Write-through 保证持久化**:每次 `AppendMessage` 同时写入 PostgreSQL,确保服务重启不丢数据。
|
||||
|
||||
**历史对话恢复流程**:
|
||||
1. 用户从对话列表选择一个历史对话
|
||||
2. 前端带 `conversation_id` 建立 WebSocket 连接
|
||||
3. 后端调用 `sessionManager.LoadFromDB(conversationID)` 将历史消息从 PostgreSQL 加载到 Redis/内存
|
||||
4. 后续对话正常走热存储路径
|
||||
|
||||
---
|
||||
|
||||
## 五、WebSocket 认证集成
|
||||
|
||||
### 5.1 连接流程
|
||||
|
||||
```
|
||||
前端 后端
|
||||
| |
|
||||
|-- WS /ws?token=<access> ---->|
|
||||
| &conversation_id=<uuid> |
|
||||
| |-- 校验 access_token
|
||||
| |-- 校验 conversation_id 归属
|
||||
| |-- LoadFromDB(如果是历史对话)
|
||||
| |-- 创建新 session(如果 conversation_id 为空)
|
||||
|<-- connected {session_id} ---|
|
||||
| |
|
||||
|-- query {image, audio} ----->| (正常对话流程)
|
||||
```
|
||||
|
||||
### 5.2 Go 实现
|
||||
|
||||
```go
|
||||
// internal/ws/handler.go
|
||||
|
||||
func (h *Handler) HandleWS(c *gin.Context) {
|
||||
// 1. 提取并校验 access_token
|
||||
tokenStr := c.Query("token")
|
||||
if tokenStr == "" {
|
||||
c.JSON(401, gin.H{"error": "missing token"})
|
||||
return
|
||||
}
|
||||
claims, err := h.tokenManager.ValidateAccess(tokenStr)
|
||||
if err != nil {
|
||||
c.JSON(401, gin.H{"error": "invalid token"})
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 提取 conversation_id(可选)
|
||||
conversationID := c.Query("conversation_id")
|
||||
|
||||
// 3. 升级 WebSocket
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 获取或创建 session
|
||||
var sessionID string
|
||||
if conversationID != "" {
|
||||
// 验证该对话属于当前用户
|
||||
sess, err := h.sessionMgr.Get(c, conversationID)
|
||||
if err != nil || sess.UserID != claims.UserID {
|
||||
conn.WriteJSON(models.WsError{Type: "error", Code: "SESSION_NOT_FOUND"})
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
// 加载历史到热存储
|
||||
h.sessionMgr.LoadFromDB(c, conversationID)
|
||||
sessionID = conversationID
|
||||
} else {
|
||||
// 创建新对话
|
||||
sessionID, _ = h.sessionMgr.Create(c, claims.UserID, models.DefaultConfig())
|
||||
}
|
||||
|
||||
// 5. 进入正常 WS 处理循环
|
||||
h.handleSession(conn, sessionID, claims.UserID)
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 前端连接方式
|
||||
|
||||
```typescript
|
||||
// WebSocket 连接
|
||||
const ws = new WebSocket(
|
||||
`wss://${window.location.host}/ws?token=${accessToken}&conversation_id=${selectedConvId || ''}`
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、前端设计概要
|
||||
|
||||
### 6.1 页面路由
|
||||
|
||||
```
|
||||
/ → 未登录重定向到 /login
|
||||
/login → AuthPage(登录/注册表单)
|
||||
/chat → 主界面(需登录)
|
||||
/chat/:id → 主界面,自动加载指定对话
|
||||
```
|
||||
|
||||
### 6.2 组件结构
|
||||
|
||||
```
|
||||
App
|
||||
├── AuthPage ← 新增:登录/注册
|
||||
└── ChatLayout(需登录)
|
||||
├── ConversationList ← 新增:侧边栏对话列表
|
||||
│ ├── 对话项(标题、最后消息、时间)
|
||||
│ ├── 新建对话按钮
|
||||
│ └── 删除对话按钮
|
||||
├── ChatPanel ← 现有,需适配多对话
|
||||
├── VideoPreview ← 现有
|
||||
├── MicManager ← 现有
|
||||
└── ConfigPanel ← 现有
|
||||
```
|
||||
|
||||
### 6.3 新增 Hook
|
||||
|
||||
```typescript
|
||||
// useAuth — 认证状态管理
|
||||
function useAuth() {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const login = async (username: string, password: string) => { ... };
|
||||
const register = async (username: string, password: string) => { ... };
|
||||
const logout = async () => { ... };
|
||||
const refreshToken = async () => { ... };
|
||||
|
||||
// 请求拦截器:自动附加 Authorization header
|
||||
// 401 时自动尝试 refresh,失败则跳转登录
|
||||
|
||||
return { user, loading, login, register, logout };
|
||||
}
|
||||
|
||||
// useConversations — 对话列表管理
|
||||
function useConversations() {
|
||||
const [conversations, setConversations] = useState<ConversationSummary[]>([]);
|
||||
const [currentId, setCurrentId] = useState<string | null>(null);
|
||||
|
||||
const fetchList = async (page?: number) => { ... };
|
||||
const createNew = async () => { ... };
|
||||
const deleteConv = async (id: string) => { ... };
|
||||
const renameConv = async (id: string, title: string) => { ... };
|
||||
const selectConv = (id: string) => { setCurrentId(id); };
|
||||
|
||||
return { conversations, currentId, fetchList, createNew, deleteConv, renameConv, selectConv };
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 对话标题自动生成
|
||||
|
||||
```go
|
||||
// 内部逻辑:首条 user 消息的前 20 个字符作为 title
|
||||
func generateTitle(firstMessage string) string {
|
||||
runes := []rune(firstMessage)
|
||||
if len(runes) > 20 {
|
||||
return string(runes[:20]) + "…"
|
||||
}
|
||||
return firstMessage
|
||||
}
|
||||
```
|
||||
|
||||
在 `AppendMessage` 时,如果 session 的 title 仍为 "新对话",自动更新为 `generateTitle(msg.Content)`。
|
||||
|
||||
---
|
||||
|
||||
## 七、配置扩展
|
||||
|
||||
### 7.1 Go 配置结构体
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
App AppConfig `mapstructure:"app"`
|
||||
Server ServerConfig `mapstructure:"server"`
|
||||
Auth AuthConfig `mapstructure:"auth"` // 新增
|
||||
Redis RedisConfig `mapstructure:"redis"`
|
||||
AI AIConfig `mapstructure:"ai"`
|
||||
Storage StorageConfig `mapstructure:"storage"`
|
||||
Log LogConfig `mapstructure:"log"`
|
||||
}
|
||||
|
||||
type AuthConfig struct {
|
||||
JWTSecret string `mapstructure:"jwt_secret"` // 必须通过环境变量设置
|
||||
AccessTTL int `mapstructure:"access_ttl"` // 分钟,默认 15
|
||||
RefreshTTL int `mapstructure:"refresh_ttl"` // 分钟,默认 10080 (7天)
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 配置文件示例
|
||||
|
||||
```yaml
|
||||
# config.yaml
|
||||
auth:
|
||||
access_ttl: 15 # 分钟
|
||||
refresh_ttl: 10080 # 7天
|
||||
|
||||
storage:
|
||||
driver: "memory" # "memory" | "postgres"
|
||||
dsn: ""
|
||||
```
|
||||
|
||||
### 7.3 环境变量
|
||||
|
||||
| 配置项 | 环境变量 | 说明 |
|
||||
|--------|---------|------|
|
||||
| `auth.jwt_secret` | `CAMTALK_AUTH_JWT_SECRET` | **必须设置**,JWT 签名密钥 |
|
||||
| `auth.access_ttl` | `CAMTALK_AUTH_ACCESS_TTL` | access_token 有效期(分钟) |
|
||||
| `auth.refresh_ttl` | `CAMTALK_AUTH_REFRESH_TTL` | refresh_token 有效期(分钟) |
|
||||
| `storage.driver` | `CAMTALK_STORAGE_DRIVER` | `"memory"` 或 `"postgres"` |
|
||||
| `storage.dsn` | `CAMTALK_STORAGE_DSN` | PostgreSQL 连接串 |
|
||||
|
||||
---
|
||||
|
||||
## 八、实施阶段
|
||||
|
||||
### Phase 1:用户认证系统 ✅
|
||||
|
||||
- [x] 数据库 schema 迁移脚本(users, refresh_tokens 表)— `migrations/001_users.up.sql`
|
||||
- [x] `internal/auth/` 包:TokenManager, bcrypt 工具, JWT 中间件
|
||||
- [x] `internal/store/user.go`:UserRepository 接口 + PostgreSQL 实现 + 内存实现
|
||||
- [x] REST API:`/api/auth/register`, `/api/auth/login`, `/api/auth/refresh`, `/api/auth/logout`
|
||||
- [x] 单元测试 — `jwt_test.go`, `service_test.go`, `auth_test.go`, `user_test.go`
|
||||
|
||||
### Phase 2:对话 CRUD + 消息持久化 ✅
|
||||
|
||||
- [x] 数据库 schema 迁移脚本(sessions, messages 表)— `migrations/002_messages.up.sql`, `003_sessions.up.sql`
|
||||
- [x] `internal/store/message.go`:MessageRepository 接口 + PostgreSQL 实现
|
||||
- [x] `internal/store/session.go`:SessionRepository 接口 + PostgreSQL 实现
|
||||
- [x] Session Manager 扩展:Create 绑定 user_id, ListByUser, UpdateTitle
|
||||
- [x] REST API:`/api/conversations` CRUD + `/api/conversations/:id/messages`
|
||||
- [x] Write-through:AppendMessage 同时写 PostgreSQL
|
||||
|
||||
### Phase 3:对话历史恢复 ✅
|
||||
|
||||
- [x] MemoryManager 支持从 PG 透明恢复会话(Get 时自动 LoadFromDB)
|
||||
- [x] 对话标题自动生成逻辑(首条 user 消息前 20 字符)
|
||||
- [x] REST API:对话详情、历史消息查询(游标分页)
|
||||
|
||||
### Phase 4:前端集成 ✅
|
||||
|
||||
- [x] `useAuth` hook + AuthProvider(自动附加 token、自动 refresh)
|
||||
- [x] `AuthPage` 组件(登录/注册表单)
|
||||
- [x] `SessionSidebar` 组件(对话列表、搜索、重命名、删除)
|
||||
- [x] `useSessionList` hook(localStorage 持久化)
|
||||
- [x] 路由守卫:未登录重定向到 AuthPage
|
||||
- [x] WebSocket 连接带 token + conversation_id
|
||||
- [x] `useVisionSession` 适配多对话切换
|
||||
|
||||
### Phase 5:配置与收尾 ✅
|
||||
|
||||
- [x] 配置结构体扩展(AuthConfig, SessionConfig, StorageConfig)
|
||||
- [x] config.yaml 更新
|
||||
- [x] 数据库迁移嵌入式自动执行(`go:embed`)
|
||||
- [x] 集成测试 — 122 个测试函数覆盖所有模块
|
||||
- [x] 更新 `02-系统架构.md` 和 `03-接口文档.md`
|
||||
@@ -1,223 +0,0 @@
|
||||
# CamTalk 后端完善计划
|
||||
|
||||
> **✅ 状态:全部完成。** 所有 Phase 已实现并通过测试(约 122 个测试函数)。本文档保留作为历史参考。
|
||||
|
||||
## Context
|
||||
|
||||
后端当前是一个骨架:`main.go` 启动 Gin 服务器,`ws/handler.go` 实现了 WebSocket 连接生命周期和消息分发,`models/models.go` 定义了所有协议消息类型,`config/config.go` 实现了 Viper 配置加载。但所有业务逻辑都是 TODO 桩——没有 Session Manager、没有 AI 服务客户端、没有编排层、没有日志/错误工具、没有测试。前端已基本完成,正在等待后端提供真实的 AI 管道。
|
||||
|
||||
**目标**:按设计文档(`docs/03-接口文档.md` 为最高依据)逐步填充所有业务模块,使端到端的 STT → LLM → TTS 流式管道可用。
|
||||
|
||||
---
|
||||
|
||||
## 分阶段实施
|
||||
|
||||
### Phase 1:基础设施(logger、errors、config 接入、graceful shutdown) ✅
|
||||
|
||||
**目标**:为后续模块提供日志、错误码、配置等基础能力,替换 `main.go` 中的硬编码值。
|
||||
|
||||
| # | 任务 | 文件 | 说明 |
|
||||
|---|------|------|------|
|
||||
| 1.1 | 实现 Zap 日志封装 | `internal/logger/logger.go` | 提供 `Init(level, format)` 和全局 `*zap.SugaredLogger`,替换所有 `log.Printf` |
|
||||
| 1.2 | 实现错误码常量 + WS 错误发送工具 | `internal/errors/codes.go` | 10 个错误码常量 + `SendWSError(client, code, requestID, err)` |
|
||||
| 1.3 | main.go 接入 config.Load() | `cmd/server/main.go` | 用 `cfg.Server.Host:Port` 替换硬编码 `:8080`,初始化 logger |
|
||||
| 1.4 | 添加 graceful shutdown | `cmd/server/main.go` | `signal.NotifyContext` + `http.Server.Shutdown`,10s drain |
|
||||
| 1.5 | 添加 .gitignore | `backend/.gitignore` | 排除 `server` 二进制、`.env`、`tmp/` |
|
||||
|
||||
> **CORS**:不在此处实现,生产环境由 Nginx 反向代理统一处理跨域。
|
||||
|
||||
---
|
||||
|
||||
### Phase 2:Session Manager ✅
|
||||
|
||||
**目标**:实现会话生命周期管理,让 WS handler 能追踪会话、存储对话历史。
|
||||
|
||||
| # | 任务 | 文件 | 说明 |
|
||||
|---|------|------|------|
|
||||
| 2.1 | 定义 SessionManager 接口 | `internal/session/manager.go` | 方法:`Create`, `Get`, `UpdateConfig`, `GetHistory`, `AppendMessage`, `SetActiveRequest`, `ClearActiveRequest`, `Touch`, `Destroy` |
|
||||
| 2.2 | 实现内存版 SessionManager | `internal/session/memory.go` | `sync.RWMutex` + `map[string]*sessionEntry`,TTL 30 分钟,历史上限 20 条 |
|
||||
| 2.3 | 实现 Redis 版 SessionManager | `internal/session/redis.go` | `session:{id}:meta` Hash + `session:{id}:history` List,TTL 刷新,选配 |
|
||||
| 2.4 | 编写 Session Manager 测试 | `internal/session/memory_test.go` | 覆盖 Create/Get/Expire/Destroy/AppendMessage/History 上限 |
|
||||
| 2.5 | WS handler 接入 SessionManager | `internal/ws/handler.go` | `ServeWS` 接收 `session.Manager` 参数;`connected` 消息后创建会话;`query` 时 Touch + SetActiveRequest;`config` 时 UpdateConfig;断开时不销毁(自然过期) |
|
||||
|
||||
---
|
||||
|
||||
### Phase 3:AI 服务层接口 + 实现 ✅
|
||||
|
||||
**目标**:定义并实现三个 AI 服务客户端,每个服务一个独立包。
|
||||
|
||||
| # | 任务 | 文件 | 说明 |
|
||||
|---|------|------|------|
|
||||
| **3a. STT** | | | |
|
||||
| 3.1 | STT 接口定义 | `internal/ai/stt/stt.go` | `Service` 接口:`Recognize(ctx, audio []byte, opts Options) (string, error)`。`Options`: Encoding, SampleRate, Language |
|
||||
| 3.2 | Deepgram 实现 | `internal/ai/stt/deepgram.go` | WebSocket 连接 `wss://api.deepgram.com/v1/listen`,发送 PCM 音频,接收转录结果,5s 超时 |
|
||||
| 3.3 | STT 测试(mock) | `internal/ai/stt/deepgram_test.go` | httptest/WebSocket mock,验证连接、发送、超时 |
|
||||
| **3b. LLM** | | | |
|
||||
| 3.4 | LLM 接口定义 | `internal/ai/llm/llm.go` | `Service` 接口:`ChatStream(ctx, req Request) (<-chan Chunk, error)`。`Request`: Image, Text, History, Language。`Chunk`: Delta, Done, TokensUsed, Model |
|
||||
| 3.5 | OpenAI 实现 | `internal/ai/llm/openai.go` | `POST /v1/chat/completions` + `stream: true`,SSE 解析,10s 超时,image 以 `data:image/jpeg;base64,...` 传入 |
|
||||
| 3.6 | System Prompt 定义 | `internal/ai/llm/prompt.go` | 中文视觉助手提示词,根据 Language/DetailLevel 动态构建 |
|
||||
| 3.7 | LLM 测试(mock) | `internal/ai/llm/openai_test.go` | httptest mock SSE 流,验证流式解析、超时、错误处理 |
|
||||
| **3c. TTS** | | | |
|
||||
| 3.8 | TTS 接口定义 | `internal/ai/tts/tts.go` | `Service` 接口:`SynthesizeStream(ctx, textStream <-chan string, opts Options) (<-chan Chunk, error)`。`Chunk`: Audio []byte, IsLast |
|
||||
| 3.9 | OpenAI 实现 | `internal/ai/tts/openai.go` | `POST /v1/audio/speech` 模型 `tts-1`,逐句发送,返回 MP3 流,5s/句超时 |
|
||||
| 3.10 | TTS 测试(mock) | `internal/ai/tts/openai_test.go` | httptest mock,验证逐句合成、超时 |
|
||||
|
||||
---
|
||||
|
||||
### Phase 4:AI Orchestrator(核心编排) ✅
|
||||
|
||||
**目标**:实现 STT → LLM → TTS 流式并行管道,这是后端最关键的业务逻辑。
|
||||
|
||||
| # | 任务 | 文件 | 说明 |
|
||||
|---|------|------|------|
|
||||
| 4.1 | Orchestrator 接口 | `internal/orchestrator/orchestrator.go` | `ProcessQuery(ctx, sessionID, req, history, sender)` — 接收查询并执行管道 |
|
||||
| 4.2 | Sender 接口 | `internal/orchestrator/sender.go` | 抽象 WS 推送:`SendSTTResult`, `SendLLMChunk`, `SendLLMDone`, `SendTTSAudio`, `SendError`,便于测试 |
|
||||
| 4.3 | 管道实现 | `internal/orchestrator/pipeline.go` | ① `stt.Recognize()` → 发送 `stt_result` ② `llm.ChatStream()` 并行消费 token → 发送 `llm_chunk` + 句子切分 → channel ③ `tts.SynthesizeStream()` 从 channel 读取 → 发送 `tts_audio` ④ 流结束 → 发送 `llm_done` |
|
||||
| 4.4 | 句子切分器 | `internal/orchestrator/splitter.go` | 按 `。!?\n.!?` 切分,buffer size 4 channel |
|
||||
| 4.5 | 错误降级 | 同上文件 | STT 失败→STT_ERROR+abort;LLM 超时→LLM_TIMEOUT;TTS 失败→静默跳过 |
|
||||
| 4.6 | Interrupt 支持 | 同上文件 | context cancel 触发所有流中止 |
|
||||
| 4.7 | Orchestrator 测试 | `internal/orchestrator/pipeline_test.go` | mock 三个 AI service + mock sender,验证完整流程、中断、错误降级 |
|
||||
|
||||
---
|
||||
|
||||
### Phase 5:WS Handler 完整接入 ✅
|
||||
|
||||
**目标**:将 Session Manager + Orchestrator 串入 WebSocket handler,实现端到端消息处理。
|
||||
|
||||
| # | 任务 | 文件 | 说明 |
|
||||
|---|------|------|------|
|
||||
| 5.1 | Client 扩展 | `internal/ws/handler.go` | 添加 `session.Manager`、`orchestrator.Orchestrator`、`context.CancelFunc`(用于 interrupt) |
|
||||
| 5.2 | query 处理 | 同上 | 解码 audio Base64 → `stt.Recognize` 的输入;Touch 会话;设置 active request;启动 `orchestrator.ProcessQuery` goroutine |
|
||||
| 5.3 | config 处理 | 同上 | 调用 `session.UpdateConfig()` |
|
||||
| 5.4 | interrupt 处理 | 同上 | 查找 active request 的 cancel func,调用 `cancel()`,ClearActiveRequest |
|
||||
| 5.5 | Disconnect 处理 | 同上 | 取消当前活跃请求(如有),不销毁会话 |
|
||||
|
||||
---
|
||||
|
||||
### Phase 6:REST API 补全 ✅
|
||||
|
||||
**目标**:补全设计文档中的 REST 端点。
|
||||
|
||||
| # | 任务 | 文件 | 说明 |
|
||||
|---|------|------|------|
|
||||
| 6.1 | Session 路由 | `internal/api/session.go` | `POST /api/sessions` 创建会话,`DELETE /api/sessions/:id` 销毁会话 |
|
||||
| 6.2 | Health 更新 | `cmd/server/main.go` | 从 SessionManager 获取 `active_sessions` 真实值 |
|
||||
| 6.3 | 路由注册 | `cmd/server/main.go` | 统一注册 REST + WS 路由,注入依赖 |
|
||||
|
||||
---
|
||||
|
||||
### Phase 7:Rate Limiter + Model Router(可选/MVP 后) 📋
|
||||
|
||||
**目标**:防止滥用 + 智能模型选择,MVP 可简化或跳过。
|
||||
|
||||
| # | 任务 | 文件 | 说明 |
|
||||
|---|------|------|------|
|
||||
| 7.1 | 令牌桶 Rate Limiter | `internal/middleware/ratelimit.go` | `golang.org/x/time/rate` 或自实现,按 session ID 限流 |
|
||||
| 7.2 | Rate Limiter 中间件 | `internal/middleware/ratelimit.go` | 在 WS query 路径上检查,超限返回 `RATE_LIMITED` |
|
||||
| 7.3 | Model Router | `internal/ai/router.go` | 规则引擎:简单识别→GPT-4o-mini,深度分析→GPT-4o,暂不实现 o1 |
|
||||
|
||||
---
|
||||
|
||||
### Phase 8:集成测试 + 文档同步 ✅
|
||||
|
||||
| # | 任务 | 文件 | 说明 |
|
||||
|---|------|------|------|
|
||||
| 8.1 | WS 集成测试 | `internal/ws/handler_test.go` | 启动 Gin test server + gorilla websocket client,验证完整 query→stt_result→llm_chunk→llm_done→tts_audio 流程 |
|
||||
| 8.2 | 文档同步 | `docs/03-接口文档.md` | 代码实现与文档有偏差时更新文档 |
|
||||
| 8.3 | go.sum 清理 | `backend/` | `go mod tidy` 清理无用依赖 |
|
||||
|
||||
---
|
||||
|
||||
## 关键文件清单
|
||||
|
||||
```
|
||||
backend/
|
||||
cmd/server/main.go ← Phase 1.3, 1.4, 1.5, 6.2, 6.3
|
||||
internal/
|
||||
config/config.go ← 已完成,Phase 1.3 接入
|
||||
logger/logger.go ← Phase 1.1(新建)
|
||||
errors/codes.go ← Phase 1.2(新建)
|
||||
models/models.go ← 已完成,可能小幅扩展
|
||||
session/
|
||||
manager.go ← Phase 2.1(新建)
|
||||
memory.go ← Phase 2.2(新建)
|
||||
redis.go ← Phase 2.3(新建)
|
||||
memory_test.go ← Phase 2.4(新建)
|
||||
ai/
|
||||
stt/
|
||||
stt.go ← Phase 3.1(新建)
|
||||
deepgram.go ← Phase 3.2(新建)
|
||||
deepgram_test.go ← Phase 3.3(新建)
|
||||
llm/
|
||||
llm.go ← Phase 3.4(新建)
|
||||
openai.go ← Phase 3.5(新建)
|
||||
prompt.go ← Phase 3.6(新建)
|
||||
openai_test.go ← Phase 3.7(新建)
|
||||
tts/
|
||||
tts.go ← Phase 3.8(新建)
|
||||
openai.go ← Phase 3.9(新建)
|
||||
openai_test.go ← Phase 3.10(新建)
|
||||
router.go ← Phase 7.3(新建)
|
||||
orchestrator/
|
||||
orchestrator.go ← Phase 4.1(新建)
|
||||
sender.go ← Phase 4.2(新建)
|
||||
pipeline.go ← Phase 4.3, 4.4, 4.5, 4.6(新建)
|
||||
pipeline_test.go ← Phase 4.7(新建)
|
||||
api/
|
||||
session.go ← Phase 6.1(新建)
|
||||
middleware/
|
||||
ratelimit.go ← Phase 7.1, 7.2(新建)
|
||||
ws/
|
||||
handler.go ← Phase 5.1-5.5(修改)
|
||||
handler_test.go ← Phase 8.1(新建)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 新增依赖
|
||||
|
||||
| 包 | 用途 | Phase |
|
||||
|----|------|-------|
|
||||
| `go.uber.org/zap` | 结构化日志 | 1 |
|
||||
| `github.com/redis/go-redis/v9` | Redis 客户端 | 2.3 |
|
||||
| `github.com/gorilla/websocket` | 已有,Deepgram WS 也复用 | 3.2 |
|
||||
|
||||
---
|
||||
|
||||
## 执行顺序与依赖关系
|
||||
|
||||
```
|
||||
Phase 1 (基础设施)
|
||||
↓
|
||||
Phase 2 (Session Manager)
|
||||
↓
|
||||
Phase 3 (AI 服务层) ← 可与 Phase 2 并行开发
|
||||
↓
|
||||
Phase 4 (Orchestrator) ← 依赖 Phase 2 + 3
|
||||
↓
|
||||
Phase 5 (WS Handler 接入) ← 依赖 Phase 4
|
||||
↓
|
||||
Phase 6 (REST API) ← 依赖 Phase 2
|
||||
↓
|
||||
Phase 7 (Rate Limiter + Router) ← 独立,可推后
|
||||
↓
|
||||
Phase 8 (集成测试 + 文档)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验证方案
|
||||
|
||||
1. **单元测试**:每个模块独立测试,mock 外部依赖(AI API、Redis)
|
||||
2. **集成测试**:`httptest` 启动 Gin server,用 gorilla/websocket 客户端模拟完整 query 流程
|
||||
3. **端到端手动测试**:启动后端 → 打开前端 → 摄像头+麦克风对话 → 验证 stt_result / llm_chunk / tts_audio 消息流
|
||||
4. **go vet + go test ./...** 通过
|
||||
|
||||
---
|
||||
|
||||
## 设计文档参考
|
||||
|
||||
- 接口规范(最高优先级):`docs/03-接口文档.md`
|
||||
- 系统架构:`docs/02-系统架构.md`
|
||||
- 技术选型:`docs/04-技术选型.md`
|
||||
- 成本控制:`docs/08-成本控制.md`
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,46 +1,26 @@
|
||||
# CamTalk 设计文档
|
||||
|
||||
CamTalk 是一款多模态实时 AI 视觉对话助手。用户通过摄像头和麦克风与 AI 交互,AI 理解视觉场景和语音输入后给出自然回应。
|
||||
CamTalk 是一款多模态实时 AI 视觉对话助手。用户通过摄像头和麦克风与 AI 交互,AI 理解视觉场景和语音输入后,以文字和语音形式给出自然回应。
|
||||
|
||||
## 文档索引
|
||||
|
||||
| 文档 | 说明 | 状态 |
|
||||
|------|------|------|
|
||||
| [01-项目概述](01-项目概述.md) | 项目目标、核心挑战、交付物 | ✅ 与代码一致 |
|
||||
| [02-系统架构](02-系统架构.md) | 三层架构、技术栈、核心交互流程、前后端模块、存储策略、部署架构 | ✅ 已更新 |
|
||||
| [03-接口文档](03-接口文档.md) | WebSocket 协议、REST API、AI 服务层接口、编排器设计、Session Manager、配置管理(Viper)、数据模型、错误码、连接管理(**实现时首先阅读**) | ✅ 已更新 |
|
||||
| [04-技术选型](04-技术选型.md) | 持久化层(PostgreSQL)、认证系统和前端边缘处理层的选型对比与决策理由 | ✅ 已更新 |
|
||||
| [05-用户故事](05-用户故事.md) | P0/P1/P2 用户故事、验收标准、优先级决策依据 | ✅ 与代码一致 |
|
||||
| [06-语音交互](06-语音交互.md) | VAD → STT → LLM → TTS 全链路、延迟优化 | ✅ 与代码一致 |
|
||||
| [07-视觉理解](07-视觉理解.md) | 帧采样策略、图像编码、多模态 LLM 输入机制 | ✅ 与代码一致 |
|
||||
| [08-成本控制](08-成本控制.md) | 智能采样、端云协同、模型分级、缓存复用 | ✅ 与代码一致 |
|
||||
| [09-技术名词解释](09-技术名词解释.md) | 前端/后端/AI 服务技术名词简明解释 | ✅ 与代码一致 |
|
||||
| [10-功能创意](10-功能创意.md) | 未来功能创意清单 | 📋 愿景 |
|
||||
| [11-持久化与用户系统设计](11-持久化与用户系统设计.md) | 用户认证、JWT、对话持久化的完整设计方案 | ✅ 已全部实现 |
|
||||
| [PLAN_BACKEND.md](PLAN_BACKEND.md) | 后端 AI 管道构建计划(Session Manager → AI 服务 → Orchestrator) | ✅ 已全部完成 |
|
||||
| [PLAN_USER_MODULE.md](PLAN_USER_MODULE.md) | 后端用户模块构建计划(Auth → 对话 CRUD → 消息持久化) | ✅ 已全部完成 |
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [01-架构设计](01-架构设计.md) | 系统架构、技术栈、模块设计、数据库、部署架构(含 Mermaid 图) |
|
||||
| [02-接口文档](02-接口文档.md) | WebSocket 协议、REST API、AI 服务层、编排器、Session Manager、配置管理、数据模型、错误码 |
|
||||
| [03-技术选型](03-技术选型.md) | 各技术的选型对比与决策理由 |
|
||||
| [04-用户故事](04-用户故事.md) | P0/P1/P2 用户故事、验收标准 |
|
||||
| [05-语音交互](05-语音交互.md) | VAD → STT → LLM → TTS 全链路、延迟优化 |
|
||||
| [06-视觉理解](06-视觉理解.md) | 帧采样策略、图像编码、多模态 LLM 输入机制 |
|
||||
| [07-成本控制](07-成本控制.md) | 智能采样、端云协同、模型分级、缓存复用 |
|
||||
| [08-功能创意](08-功能创意.md) | 未来功能创意清单 |
|
||||
| [09-技术名词解释](09-技术名词解释.md) | 前端/后端/AI 服务技术名词简明解释 |
|
||||
|
||||
## 推荐阅读顺序
|
||||
|
||||
1. **01-项目概述** — 了解项目目标
|
||||
2. **02-系统架构** — 理解三层架构和技术栈全貌
|
||||
3. **03-接口文档** — 前后端通信契约,实现时的最高依据
|
||||
4. **04-技术选型** — 了解为什么选这些技术
|
||||
5. **05-用户故事** — 明确功能优先级
|
||||
6. **06~08** — 各技术领域的详细设计
|
||||
7. **09-技术名词解释** — 遇到不熟悉的名词时查阅
|
||||
8. **11-持久化与用户系统设计** — 用户认证和持久化的详细设计
|
||||
|
||||
## 实现状态总览
|
||||
|
||||
前后端代码已全部实现,无 TODO/FIXME 桩代码。后端约 122 个测试函数覆盖所有模块。
|
||||
|
||||
| 层级 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| 前端 | ✅ 已完成 | 10 个组件、3 个 Hook、10 个库模块、i18n 三语言 |
|
||||
| 后端 AI 管道 | ✅ 已完成 | STT/LLM/TTS 多 provider、Orchestrator 流式并行 |
|
||||
| 后端用户系统 | ✅ 已完成 | JWT 认证、用户注册登录、对话 CRUD、消息持久化 |
|
||||
| 后端存储层 | ✅ 已完成 | Memory + PostgreSQL + Redis 三种实现 |
|
||||
| 数据库迁移 | ✅ 已完成 | 3 个版本化迁移脚本,嵌入式自动执行 |
|
||||
| Model Router | 📋 规划中 | 按问题复杂度选择模型 |
|
||||
| Rate Limiter | 📋 规划中 | 令牌桶限流 |
|
||||
1. **01-架构设计** — 理解三层架构、技术栈和模块全貌
|
||||
2. **02-接口文档** — 前后端通信契约,实现时的最高依据
|
||||
3. **03-技术选型** — 了解为什么选这些技术
|
||||
4. **04-用户故事** — 明确功能优先级
|
||||
5. **05~07** — 各技术领域的详细设计
|
||||
6. **09-技术名词解释** — 遇到不熟悉的名词时查阅
|
||||
|
||||
Reference in New Issue
Block a user