docs: 按功能模块重构文档结构

- 新建 01-架构设计.md:合并项目概述+系统架构+持久化设计,含 Mermaid 架构图、模块图、时序图、ER 图、部署图
- 新建 02-接口文档.md:合并接口文档+持久化 API+用户模块 API,统一格式去重
- 重编号 03~09,去掉状态标注,规划中功能标记为待实现
- 删除 PLAN_BACKEND.md、PLAN_USER_MODULE.md 及冗余文档
This commit is contained in:
hhs
2026-06-19 15:31:52 +08:00
parent dca37f3e48
commit a04275cc76
14 changed files with 606 additions and 3290 deletions

389
docs/01-架构设计.md Normal file
View 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 API1API 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 分钟 TTLWrite-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无需硬编码端口。

View File

@@ -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

View File

@@ -1,256 +0,0 @@
# 系统架构
## 概述
三层架构:**前端做轻量预处理,后端做智能编排,云端 AI 服务按需调用**。在保证交互体验的同时控制成本。
## 三层架构
| 层级 | 职责 | 关键约束 |
|------|------|---------|
| **客户端(浏览器)** | 媒体采集、边缘预处理、UI 渲染 | 浏览器资源有限,模型需轻量 |
| **Go 网关** | 会话管理、AI 服务编排、流式管道 | 高并发、低延迟、状态管理 |
| **AI 服务** | LLM 推理、语音识别、语音合成 | 按量计费,需控制调用频率 |
> 为什么要单独加一层 Go 网关,而不是让前端直连 AI API1API 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 connectionJWT 认证conversation_id 恢复 | ✅ 已完成 |
| Session Manager | 维护用户会话状态、对话历史 | Memory默认/ Redis可切换30 分钟 TTLWrite-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 GatewayREST API
└── /ws → Go GatewayWebSocket
├── 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`

View File

@@ -4,25 +4,25 @@
本文档记录项目中各项技术的**选型过程、替代方案对比和决策理由**。技术选型没有"绝对正确",只有"更适合"。
**定位**:本文档记录各项技术的选型过程和决策理由。AI 服务栈、持久化层、认证系统均已实现并通过配置灵活切换。前端边缘处理已确定技术栈。
**定位**:本文档记录各项技术的选型过程和决策理由。
```
技术选型
├── AI 服务栈(✅ 已实现)
├── AI 服务栈
│ ├── STT: Deepgram默认 / MiMo ASR
│ ├── LLM: GPT-4o默认 / 通义千问等 OpenAI 兼容模型
│ └── TTS: OpenAI TTS默认 / MiMo TTS
├── 持久化层(✅ 已实现)
├── 持久化层
│ ├── 数据库: PostgreSQLpgx/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 @@
---
## 二、持久化层选型(已实现)
## 二、持久化层选型
### 数据特征分析

View File

@@ -61,7 +61,7 @@ vad.start();
方案选择:
- **OpenAI TTS**(默认):音质好,延迟中等,按字符计费,模型 tts-1
- **MiMo TTS**(小米):国产替代,通过配置切换
- **Edge TTS**规划中):微软免费方案,音质不错,延迟略高
- **Edge TTS**待实现):微软免费方案,音质不错,延迟略高
## 延迟优化要点

View File

@@ -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 开销

View File

@@ -1,751 +0,0 @@
# 持久化与用户系统设计
## 概述
本文档定义用户注册/登录、JWT 认证、对话历史持久化的完整设计方案。核心目标:**用户登录后可在对话列表中选择历史对话继续交谈**。
### 设计决策
| 决策项 | 选择 | 理由 |
|--------|------|------|
| 认证方式 | JWTaccess + 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_tokenDELETE
生成新的 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 签名和过期(不查 DBDB 校验由 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/内存(热) ←→ PostgreSQLwrite-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-throughAppendMessage 同时写 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` hooklocalStorage 持久化)
- [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`

View File

@@ -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 2Session 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` ListTTL 刷新,选配 |
| 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 3AI 服务层接口 + 实现 ✅
**目标**:定义并实现三个 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 4AI 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+abortLLM 超时→LLM_TIMEOUTTTS 失败→静默跳过 |
| 4.6 | Interrupt 支持 | 同上文件 | context cancel 触发所有流中止 |
| 4.7 | Orchestrator 测试 | `internal/orchestrator/pipeline_test.go` | mock 三个 AI service + mock sender验证完整流程、中断、错误降级 |
---
### Phase 5WS 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 6REST 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 7Rate 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

View File

@@ -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-技术名词解释** — 遇到不熟悉的名词时查阅