Files
CamTalk/CLAUDE.md

18 KiB
Raw Blame History

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 令牌桶限流详细设计(需同步实现)

架构

三层系统:

  1. 浏览器客户端React 18 + TypeScript, Vite—— 媒体采集、边缘预处理VAD 通过 @ricky0123/vad-web、关键帧检测通过 Canvas 像素比较、UI 渲染。核心 HookuseVisionSession()
  2. Go 网关Gin, gorilla/websocket, Viper, Zap—— WebSocket 服务器、会话管理、AI 编排(基于 CloudWeGo Eino Graph。每个 WebSocket 连接一个 goroutine。
  3. 云端 AI 服务 —— 通过 OpenAI 兼容接口可灵活切换。默认DashScope qwen3-vl-plusLLM、MiMo ASRSTT、MiMo TTSTTS。仅通过 Go 网关访问,浏览器不直连。

关键模式AI 编排基于 Eino Graph 声明式 DAG6 节点线性流水线:START → STT → History → ChatModel → Msg2Str → Splitter → TTS → Done → ENDLLM 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-plusOpenAI 兼容协议)
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、收集最终输出

跨节点状态PipelineStatebackend/internal/eino/state.go),通过 context.WithValue 在节点间传递 FullResponse、TranscribedText、TokenUsage、SessionID、RequestID。

CallbackBuildCallbackHandlerbackend/internal/eino/callback.go)挂载到 ChatModel 的 OnEndWithStreamOutput,每收到一个 LLM token 立即通过 sender.SendLLMChunk() 推送到客户端。

适配器EinoOrchestratorbackend/internal/eino/adapter.go)包装 Graph实现 orchestrator.Orchestrator 接口,负责解码 Base64 图像/音频、构建输入、注入上下文、运行流式推理、持久化消息。

会话存储TieredManager

三级存储:L1 Memory → L2 Redis → L3 PostgreSQLbackend/internal/session/tiered.go

  • 读路径L1 命中直接返回;未命中尝试 L2 Redis → 回填 L1L3 通过 L1 的 FindByID 降级读取
  • 写路径L1 同步写入 → L2 同步写(失败 soft-warn→ L3 异步 goroutine 写(使用 context.Background() 防止请求取消丢失)
  • 降级:后台协程每 30 秒 ping RedisRedis 不可用时自动跳过 L2 操作;恢复后自动重新启用
  • TTLSession 默认 30 分钟MaxHistory 20 条L1 后台协程每分钟清理过期 session

Repository 接口模式:UserRepositoryMessageRepositorySessionRepository,均有 PostgreSQL 和内存双实现。

鉴权

JWT 双 token 轮转认证HMAC-SHA256

  • Access Token默认 120 分钟 TTLBearer header 传递
  • Refresh Token默认 7 天 TTL带 jtiUUIDHash 存储在 Redis/PostgreSQL
  • 轮转Refresh 时旧 token hash 删除,新 pair 生成;若 JWT 有效但 DB hash 缺失 → 判定为重放攻击 → 吊销该用户所有 refresh token
  • CachedUserRepositorybackend/internal/store/cached_user.go装饰器模式Redis 缓存 refresh token hashRead-Through / Write-ThroughRedis 故障软降级

技术栈

层级 技术
前端 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 + testifyassert/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.yamlconfig.{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 启用 Redistrue/false
CAMTALK_STORAGE_PERSISTENCE_ENABLED 启用 PostgreSQLtrue/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/Dockerfilenode:22-alpine → nginx:stable-alpine 9000:80 React SPA反向代理 /api 和 /ws 到 backend
backend 构建 ./backend/Dockerfilegolang: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-PolicyCross-Origin-Embedder-Policy 头(SharedArrayBuffer 需要ONNX WASM 推理依赖)。

CI/CD

使用 Gitea Actions.gitea/workflows/deploy.yml),自托管 runner标签 aliyun)。

触发条件push 到 mainv2 分支。流程rsync 代码到 /root/camtalk,执行 deploy.sh builddeploy.sh restart

数据库迁移

嵌入式 SQL 迁移系统(backend/internal/store/migrate.goSQL 文件在 backend/migrations/

迁移 内容
001_users usersUUID PK+ refresh_tokensFK → users
002_messages messagesBIGSERIAL PK, session_id UUID, 游标分页索引)
003_sessions sessionsUUID 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 tokenAccess Token认证不走 HTTP Authorization header。服务端在升级时校验 JWT失败返回 401。

客户端 → 服务端query(图像 Base64 + 音频 Base64configinterruptping 服务端 → 客户端connectedstt_resultllm_chunkllm_donetts_audioerrorpong

心跳:客户端每 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 — 刷新 Token
  • POST /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_MESSAGESESSION_NOT_FOUNDRATE_LIMITEDIMAGE_TOO_LARGEAUDIO_TOO_SHORTLLM_TIMEOUTLLM_ERRORSTT_ERRORTTS_ERRORINTERNAL_ERRORUSERNAME_TAKENINVALID_CREDENTIALSINVALID_TOKENINVALID_INPUT

前端组件结构

组件 职责
LandingPage 未登录时的着陆页,内嵌 LoginModal 登录/注册弹窗
AuthPage 登录/注册表单(备用)
CameraManager 摄像头流采集(useCamera hook640x480, facingMode: environment
MicManager 麦克风音频采集(useMicrophone hook16kHz 单声道)
EdgeProcessor VADuseVAD 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.tsCamTalkWebSocket 单例,心跳 + 指数退避重连
  • frontend/src/lib/auth.tsxAuthProvider 上下文JWT 解码 + 自动刷新调度exp 前 60 秒)
  • frontend/src/lib/api.ts — REST 客户端,自动 Bearer header并发安全 401 拦截 + token 刷新 + 重试
  • frontend/src/lib/ttsPlayer.tsTTSPlayer 类,流式 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扁平常量 map
  • frontend/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/info3 秒自动消失)
  • frontend/src/types/index.ts — 所有 TypeScript 类型定义WebSocket 消息可辨识联合类型、场景、配置等)

Vite 构建细节

frontend/vite.config.ts 包含:

  • 自定义 serve-vad-assets 插件,在开发/构建时自动从 node_modules 复制 VAD 模型文件(silero_vad_legacy.onnxsilero_vad_v5.onnxvad.worklet.bundle.min.js)和 ONNX Runtime WASM 文件到 public/。开发服务器中间件确保 .wasm.mjs 文件返回正确的 MIME type。
  • 开发代理:/wsws://localhost:8080/apihttp://localhost:8080,前端开发时无需配置额外环境变量。

后端模块结构

模块 职责
WebSocket Handler 连接管理、JWT 认证query param token、消息分发query/config/interrupt/ping
Session Manager 会话状态、对话历史三级存储Memory/Redis/PostgreSQL30 分钟 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/SessionRepositoryPG + 内存 + Redis 缓存装饰器)
REST API 健康检查、认证、对话管理Gin 路由组)
Models 数据模型 + WebSocket 消息类型定义
Migrations 嵌入式 SQL 版本化迁移
Rate Limiter 按用户的令牌桶速率限制(docs/13-令牌桶限流设计.mdbackend/internal/ratelimit/,实现中)

测试模式:后端使用 testing + testifyassertrequiremock。mock 模式包括:mockSender(实现 orchestrator.Sender 接口)、httptest.Server(模拟 AI 服务 HTTP APIMockOrchestrator模拟完整编排管道。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/ 下的接口文档。