18 KiB
CLAUDE.md
本文件为 Claude Code (claude.ai/code) 在本仓库中工作时提供指引。
项目概述
CamTalk 是一款多模态实时 AI 视觉对话助手。用户通过摄像头和麦克风与 AI 交互,AI 理解视觉场景和语音输入后,以文字和语音形式给出自然回应。
文档优先原则: 执行任何开发任务前,先读取
docs/下的相关设计文档(架构、接口、技术选型等),以文档为最高依据。代码实现应与文档一致;若有偏差,优先更新文档(尤其是接口文档)。docs/Eino/下有完整的 Eino 框架文档(~75 个 markdown 文件),可作为参考。
核心设计文档:
| 文档 | 内容 |
|---|---|
docs/01-架构设计.md |
三层架构、技术栈、数据库设计、部署方案 |
docs/02-接口文档.md |
WebSocket 协议、REST API、AI 服务层、编排器、配置管理 |
docs/03-技术选型.md |
AI 服务栈、持久化层、前端边缘处理选型 |
docs/04-用户故事.md |
用户场景与优先级 |
docs/05-语音交互.md |
VAD → STT → LLM → TTS 全链路 |
docs/06-视觉理解.md |
帧采样、关键帧检测、多模态输入 |
docs/07-成本控制.md |
采样策略、端云协同、模型分级 |
docs/08-功能创意.md |
功能创意与规划 |
docs/09-技术名词解释.md |
术语定义(VAD/STT/TTS/Token/JWT 等) |
docs/10-Eino重构方案.md |
Eino Graph 迁移方案与决策记录 |
docs/11-Eino框架技术文档.md |
Eino 框架使用指南 |
docs/12-鉴权体系设计.md |
JWT 双 token 轮转详细设计 |
docs/13-令牌桶限流设计.md |
令牌桶限流详细设计(需同步实现) |
架构
三层系统:
- 浏览器客户端(React 18 + TypeScript, Vite)—— 媒体采集、边缘预处理(VAD 通过
@ricky0123/vad-web、关键帧检测通过 Canvas 像素比较)、UI 渲染。核心 Hook:useVisionSession() - Go 网关(Gin, gorilla/websocket, Viper, Zap)—— WebSocket 服务器、会话管理、AI 编排(基于 CloudWeGo Eino Graph)。每个 WebSocket 连接一个 goroutine。
- 云端 AI 服务 —— 通过 OpenAI 兼容接口可灵活切换。默认:DashScope qwen3-vl-plus(LLM)、MiMo ASR(STT)、MiMo TTS(TTS)。仅通过 Go 网关访问,浏览器不直连。
关键模式:AI 编排基于 Eino Graph 声明式 DAG(6 节点线性流水线:START → STT → History → ChatModel → Msg2Str → Splitter → TTS → Done → END),LLM token 通过 Callback 实时推送,TTS 逐句合成并行推送,最小化感知延迟。
Eino Graph 节点详解
| 节点 | 类型 | 文件 | 职责 |
|---|---|---|---|
| STT | InvokableLambda |
backend/internal/eino/nodes_stt.go |
语音识别或文本直通(text-only 跳过 STT) |
| History | InvokableLambda |
backend/internal/eino/nodes_history.go |
构建 System Prompt + 对话历史 + 用户输入 + 图像 |
| ChatModel | ChatModel 节点 | backend/internal/eino/graph.go |
调用 DashScope qwen3-vl-plus(OpenAI 兼容协议) |
| Msg2Str | TransformableLambda |
backend/internal/eino/nodes_splitter.go |
将 ChatModel 流式 Message 转为字符串流 |
| Splitter | TransformableLambda |
backend/internal/eino/nodes_splitter.go |
按句子分隔符(。!?\n.!?)拆分文本流 |
| TTS | TransformableLambda |
backend/internal/eino/nodes_tts.go |
逐句合成语音并推送 tts_audio |
| Done | InvokableLambda |
backend/internal/eino/nodes_done.go |
发送 llm_done、收集最终输出 |
跨节点状态:PipelineState(backend/internal/eino/state.go),通过 context.WithValue 在节点间传递 FullResponse、TranscribedText、TokenUsage、SessionID、RequestID。
Callback:BuildCallbackHandler(backend/internal/eino/callback.go)挂载到 ChatModel 的 OnEndWithStreamOutput,每收到一个 LLM token 立即通过 sender.SendLLMChunk() 推送到客户端。
适配器:EinoOrchestrator(backend/internal/eino/adapter.go)包装 Graph,实现 orchestrator.Orchestrator 接口,负责解码 Base64 图像/音频、构建输入、注入上下文、运行流式推理、持久化消息。
会话存储(TieredManager)
三级存储:L1 Memory → L2 Redis → L3 PostgreSQL(backend/internal/session/tiered.go)
- 读路径:L1 命中直接返回;未命中尝试 L2 Redis → 回填 L1;L3 通过 L1 的
FindByID降级读取 - 写路径:L1 同步写入 → L2 同步写(失败 soft-warn)→ L3 异步 goroutine 写(使用
context.Background()防止请求取消丢失) - 降级:后台协程每 30 秒 ping Redis,Redis 不可用时自动跳过 L2 操作;恢复后自动重新启用
- TTL:Session 默认 30 分钟,MaxHistory 20 条;L1 后台协程每分钟清理过期 session
Repository 接口模式:UserRepository、MessageRepository、SessionRepository,均有 PostgreSQL 和内存双实现。
鉴权
JWT 双 token 轮转认证(HMAC-SHA256):
- Access Token:默认 120 分钟 TTL,Bearer header 传递
- Refresh Token:默认 7 天 TTL,带 jti(UUID),Hash 存储在 Redis/PostgreSQL
- 轮转:Refresh 时旧 token hash 删除,新 pair 生成;若 JWT 有效但 DB hash 缺失 → 判定为重放攻击 → 吊销该用户所有 refresh token
CachedUserRepository(backend/internal/store/cached_user.go):装饰器模式,Redis 缓存 refresh token hash,Read-Through / Write-Through,Redis 故障软降级
技术栈
| 层级 | 技术 |
|---|---|
| 前端 | React 18, TypeScript, Vite, @ricky0123/vad-web, onnxruntime-web |
| 后端 | Go 1.25+ (go.mod 最低要求; Dockerfile 构建用 golang:1.26-alpine), Gin, gorilla/websocket, Viper, Zap |
| AI 编排 | CloudWeGo Eino Graph(声明式 DAG 编排) |
| LLM | DashScope qwen3-vl-plus(默认,通过 eino-ext OpenAI ChatModel 接入) |
| STT | MiMo ASR(默认) / Deepgram |
| TTS | MiMo TTS(默认) / OpenAI TTS |
| 存储 | PostgreSQL 15 + Redis 7(通过 TieredManager 三级存储) |
构建与运行命令
# 前端
cd frontend && npm install
npm run dev # Vite 开发服务器(含 /ws、/api 代理到 localhost:8080)
npm run build # 生产构建(tsc -b && vite build)
npm run lint # ESLint 检查(flat config, TypeScript strict)
# 后端
cd backend && go mod download
go run ./cmd/server # 启动网关,监听 :8080
go build -o bin/camtalk ./cmd/server
go test ./... # 运行所有测试
go test -run TestName ./path # 运行单个测试
go vet ./... # 静态分析
# Docker 部署(生产环境)
./deploy.sh build # 构建所有镜像
./deploy.sh up # 启动 4 个服务
./deploy.sh restart # down + up
./deploy.sh logs [service] # 查看日志
./deploy.sh status # 查看服务状态
注意:前端目前没有测试基础设施(无 vitest 配置、无测试文件)。后端使用
testing+testify(assert/require/mock)测试,编译期接口检查var _ Interface = (*Impl)(nil)。
配置环境切换
项目通过 APP_ENV 环境变量控制配置文件加载:
-
本地开发(默认):
APP_ENV=dev→ 加载config/config.dev.yaml- Debug 日志、关闭限流、允许所有 CORS
- 使用
backend/.env中的远程 Redis/PostgreSQL 地址
-
生产部署:
APP_ENV=prod→ 加载config/config.prod.yaml- Info/JSON 日志、启用限流、严格 CORS 白名单
- Docker Compose 自动设置,使用容器内网地址
配置优先级:环境变量 > config.{env}.yaml > config.yaml > 默认值
手动切换环境(测试用):
cd backend
APP_ENV=prod go run ./cmd/server # 本地测试生产配置
APP_ENV=dev go run ./cmd/server # 显式指定开发配置
配置系统
配置文件:backend/config/config.yaml(基础配置),可被 config/config.{env}.yaml 覆盖。
优先级(从低到高):默认值 → config.yaml → config.{env}.yaml(由 APP_ENV 环境变量决定加载哪个 env 特定文件)→ .env 文件 → 环境变量
主要 CAMTALK_ 环境变量(模板见 backend/.env.example):
| 变量 | 用途 |
|---|---|
APP_ENV |
运行环境(dev/prod),决定加载 config.{env}.yaml |
CAMTALK_AI_STT_API_KEY |
STT API Key |
CAMTALK_AI_LLM_API_KEY |
LLM API Key |
CAMTALK_AI_TTS_API_KEY |
TTS API Key |
CAMTALK_AUTH_JWT_SECRET |
JWT 签名密钥 |
CAMTALK_STORAGE_DSN |
PostgreSQL 连接串 |
CAMTALK_STORAGE_REDIS_ENABLED |
启用 Redis(true/false) |
CAMTALK_STORAGE_PERSISTENCE_ENABLED |
启用 PostgreSQL(true/false) |
CAMTALK_REDIS_ADDR |
Redis 地址 |
CAMTALK_REDIS_PASSWORD |
Redis 密码 |
最小启动(至少需要一个 AI 服务的 API Key):
CAMTALK_AI_LLM_API_KEY=sk-xxx CAMTALK_AI_STT_API_KEY=xxx go run ./cmd/server
Docker 部署
docker-compose.yml 定义 4 个服务(camtalk-net 桥接网络):
| 服务 | 镜像/构建 | 端口 | 说明 |
|---|---|---|---|
frontend |
构建 ./frontend/Dockerfile(node:22-alpine → nginx:stable-alpine) |
9000:80 | React SPA,反向代理 /api 和 /ws 到 backend |
backend |
构建 ./backend/Dockerfile(golang:1.26-alpine → alpine:3.20) |
内部 8080 | Go 网关,静态链接二进制 -ldflags="-s -w" |
postgres |
postgres:15-alpine |
内部 5432 | 数据库 camtalk,挂载 ./backend/migrations/ 到 initdb |
redis |
redis:7-alpine |
内部 6379 | 会话缓存,AOF 持久化 |
后端容器依赖 postgres + redis 健康检查通过后启动。所有服务 restart: unless-stopped。密钥通过 --env-file /opt/camtalk/.env 注入。
前端 nginx 特殊配置:设置 Cross-Origin-Opener-Policy 和 Cross-Origin-Embedder-Policy 头(SharedArrayBuffer 需要,ONNX WASM 推理依赖)。
CI/CD
使用 Gitea Actions(.gitea/workflows/deploy.yml),自托管 runner(标签 aliyun)。
触发条件:push 到 main 或 v2 分支。流程:rsync 代码到 /root/camtalk,执行 deploy.sh build → deploy.sh restart。
数据库迁移
嵌入式 SQL 迁移系统(backend/internal/store/migrate.go),SQL 文件在 backend/migrations/:
| 迁移 | 内容 |
|---|---|
001_users |
users 表(UUID PK)+ refresh_tokens 表(FK → users) |
002_messages |
messages 表(BIGSERIAL PK, session_id UUID, 游标分页索引) |
003_sessions |
sessions 表(UUID PK, user_id UUID, config JSONB, 时间排序索引) |
迁移文件通过 Go 1.16+ //go:embed 嵌入二进制,启动时自动执行。通过 schema_migrations 表追踪版本,已应用的迁移跳过。同时挂载到 PostgreSQL 容器的 /docker-entrypoint-initdb.d 作为备用初始化路径。
WebSocket 协议
端点:ws://localhost:8080/ws?token=<access_token>&conversation_id=<uuid>
所有消息为 JSON 文本帧,统一信封格式 {type, request_id?, timestamp?}。完整契约见 docs/02-接口文档.md。
认证:WebSocket 连接通过 query param token(Access Token)认证,不走 HTTP Authorization header。服务端在升级时校验 JWT,失败返回 401。
客户端 → 服务端:query(图像 Base64 + 音频 Base64)、config、interrupt、ping
服务端 → 客户端:connected、stt_result、llm_chunk、llm_done、tts_audio、error、pong
心跳:客户端每 30 秒 ping,服务端 60 秒无 ping 断开连接。 重连:指数退避 + 抖动 —— 1s, 2s, 4s, 8s… 最大 30s。
前端 WebSocket 实现:CamTalkWebSocket 单例类(frontend/src/lib/websocket.ts),基于订阅模式(onMessage/onStatusChange 返回取消订阅函数),自动处理心跳和重连。
REST API(辅助)
GET /api/health— 健康检查(版本、运行时间、活跃会话数)POST /api/auth/register— 注册POST /api/auth/login— 登录POST /api/auth/refresh— 刷新 TokenPOST /api/auth/logout— 登出GET /api/conversations— 对话列表POST /api/conversations— 创建对话GET/PATCH/DELETE /api/conversations/:id— 对话详情/改标题/删除GET /api/conversations/:id/messages— 获取对话消息(游标分页)POST/DELETE /api/sessions— 会话管理
错误码
INVALID_MESSAGE、SESSION_NOT_FOUND、RATE_LIMITED、IMAGE_TOO_LARGE、AUDIO_TOO_SHORT、LLM_TIMEOUT、LLM_ERROR、STT_ERROR、TTS_ERROR、INTERNAL_ERROR、USERNAME_TAKEN、INVALID_CREDENTIALS、INVALID_TOKEN、INVALID_INPUT
前端组件结构
| 组件 | 职责 |
|---|---|
LandingPage |
未登录时的着陆页,内嵌 LoginModal 登录/注册弹窗 |
AuthPage |
登录/注册表单(备用) |
CameraManager |
摄像头流采集(useCamera hook:640x480, facingMode: environment) |
MicManager |
麦克风音频采集(useMicrophone hook:16kHz 单声道) |
EdgeProcessor |
VAD(useVAD hook:@ricky0123/vad-web)+ 关键帧检测(Canvas 像素比较,160x120 降采样) |
WebSocketManager |
WebSocket 连接生命周期管理(桥接 wsClient 单例到 React 状态) |
ChatPanel |
消息展示、流式回复、文本输入、场景选择(5 种场景卡片) |
VideoPreview |
摄像头画面预览(forwardRef <video>) |
SessionSidebar |
左侧抽屉式对话列表(搜索、重命名、删除、时间分组) |
ConfigPanel |
右侧抽屉式配置面板(主题、TTS、语言、场景、登出) |
Toast |
轻量通知提示(3 秒自动消失) |
核心 Hook:
useVisionSession()— 封装一次完整的视觉对话会话(~500 行),管理摄像头、麦克风、VAD、WebSocket、消息状态、TTS 播放、两种模式(dialogue / observation)useSessionList()— 对话列表 CRUD(通过 REST API),乐观更新useObservationMode()— 定期帧差异检测(5 秒间隔),相似度 < 0.85 时触发回调
关键 lib 文件:
frontend/src/lib/websocket.ts—CamTalkWebSocket单例,心跳 + 指数退避重连frontend/src/lib/auth.tsx—AuthProvider上下文,JWT 解码 + 自动刷新调度(exp 前 60 秒)frontend/src/lib/api.ts— REST 客户端,自动 Bearer header,并发安全 401 拦截 + token 刷新 + 重试frontend/src/lib/ttsPlayer.ts—TTSPlayer类,流式 TTS 音频逐句排队播放frontend/src/lib/scenarios.ts— 5 种对话场景定义(free_chat, interviewer, english_teacher, debate, interpreter)frontend/src/lib/i18n/— 国际化,3 种语言(zh-CN 默认/fallback, en-US, ja-JP),扁平常量 mapfrontend/src/lib/storage.ts— localStorage 封装(config, tokens, user info)frontend/src/lib/audio.ts— 音频编码工具(浏览器采集 → Base64 PCM)frontend/src/lib/sampling.ts— 混合采样策略(定时低频 + 事件高频,实现见docs/07-成本控制.md)frontend/src/lib/errors.ts— 错误码到用户友好文案的映射frontend/src/lib/toast.ts— Toast 全局状态管理(error/warning/info,3 秒自动消失)frontend/src/types/index.ts— 所有 TypeScript 类型定义(WebSocket 消息可辨识联合类型、场景、配置等)
Vite 构建细节
frontend/vite.config.ts 包含:
- 自定义
serve-vad-assets插件,在开发/构建时自动从node_modules复制 VAD 模型文件(silero_vad_legacy.onnx、silero_vad_v5.onnx、vad.worklet.bundle.min.js)和 ONNX Runtime WASM 文件到public/。开发服务器中间件确保.wasm和.mjs文件返回正确的 MIME type。 - 开发代理:
/ws→ws://localhost:8080、/api→http://localhost:8080,前端开发时无需配置额外环境变量。
后端模块结构
| 模块 | 职责 |
|---|---|
| WebSocket Handler | 连接管理、JWT 认证(query param token)、消息分发(query/config/interrupt/ping) |
| Session Manager | 会话状态、对话历史(三级存储:Memory/Redis/PostgreSQL,30 分钟 TTL) |
| Eino 编排层 | 基于 Eino Graph 的声明式 AI 编排(6 节点线性 DAG + Callback) |
| AI Orchestrator | EinoOrchestrator 适配器,包装 Graph 实现 Orchestrator 接口 |
| AI Service Layer | AI 服务抽象层(STT/TTS 多 provider 接口,LLM 通过 eino-ext ChatModel) |
| Auth | JWT 双 token 轮转认证,bcrypt 密码哈希,Gin 中间件 |
| Store | 持久化存储层(UserRepository/MessageRepository/SessionRepository,PG + 内存 + Redis 缓存装饰器) |
| REST API | 健康检查、认证、对话管理(Gin 路由组) |
| Models | 数据模型 + WebSocket 消息类型定义 |
| Migrations | 嵌入式 SQL 版本化迁移 |
| Rate Limiter | 按用户的令牌桶速率限制(docs/13-令牌桶限流设计.md,backend/internal/ratelimit/,实现中) |
测试模式:后端使用 testing + testify(assert、require、mock)。mock 模式包括:mockSender(实现 orchestrator.Sender 接口)、httptest.Server(模拟 AI 服务 HTTP API)、MockOrchestrator(模拟完整编排管道)。WebSocket 集成测试使用 httptest.Server + gorilla/websocket.Dialer。
编码规范
- Go:遵循标准 Go 规范。所有 AI 调用使用
context.Context做取消/超时。并发 map 访问使用sync.RWMutex。结构体标签用json:"snake_case"。编译期接口检查var _ Interface = (*Impl)(nil)。 - TypeScript:严格模式(
strict: true)。所有数据模型用接口定义。WebSocket 消息类型用可辨识联合类型(type字段)。verbatimModuleSyntax: true(强制import type)。未使用变量以_前缀忽略。 - CORS 处理:禁止在后端代码和配置文件(
config/*.yaml)中进行任何 CORS 配置。跨域由代理层统一处理:开发环境通过frontend/vite.config.ts中的 proxy 配置(/ws、/api代理到localhost:8080),生产环境通过 Nginx 反向代理(frontend/nginx.conf)。 - 提交信息:Conventional Commits 格式,描述用中文。示例:
feat: 添加 WebSocket 连接管理、fix: 修复心跳超时判断、docs: 更新接口文档 - 禁止自动 push:除非用户明确要求。
- 文档优先:实现功能前先读取
docs/下的相关设计文档。实现与文档不一致时,优先更新docs/下的接口文档。