diff --git a/CLAUDE.md b/CLAUDE.md index 060d6c5..9780660 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,294 +1,73 @@ # CLAUDE.md -本文件为 Claude Code (claude.ai/code) 在本仓库中工作时提供指引。 +CamTalk — 多模态实时 AI 视觉对话助手(摄像头 + 麦克风 + 视觉 + 语音 AI) -## 项目概述 - -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` | 令牌桶限流详细设计(需同步实现) | +> **文档优先原则:** 开发前先读 `docs/` 设计文档,以文档为准;若代码与文档不一致,优先更新文档(尤其接口文档)。详细设计见 `docs/01-13` 系列文档。**注意**:`docs/Eino/` 框架文档内容庞大(~75 个文件),仅在需要了解 Eino Graph/节点/Callback 等框架细节时才读取。 ## 架构 -三层系统: +三层系统:前端(React + Vite)→ Go 网关(Gin + WebSocket + Eino Graph AI 编排)→ AI 服务(DashScope LLM, MiMo STT/TTS) -1. **浏览器客户端**(React 18 + TypeScript, Vite)—— 媒体采集、边缘预处理(VAD 通过 `@ricky0123/vad-web`、关键帧检测通过 Canvas 像素比较)、UI 渲染。核心 Hook:`useVisionSession()` -2. **Go 网关**(Gin, gorilla/websocket, Viper, Zap)—— WebSocket 服务器、会话管理、AI 编排(基于 CloudWeGo Eino Graph)。每个 WebSocket 连接一个 goroutine。 -3. **云端 AI 服务** —— 通过 OpenAI 兼容接口可灵活切换。默认:DashScope qwen3-vl-plus(LLM)、MiMo ASR(STT)、MiMo TTS(TTS)。仅通过 Go 网关访问,浏览器不直连。 +**AI 编排流水线**(Eino Graph 7 节点 DAG):`STT → History → ChatModel → Msg2Str → Splitter → TTS → Done`。LLM token 通过 Callback 实时推送,TTS 逐句并行合成。 -**关键模式**:AI 编排基于 Eino Graph 声明式 DAG(6 节点线性流水线:`START → STT → History → ChatModel → Msg2Str → Splitter → TTS → Done → END`),LLM token 通过 Callback 实时推送,TTS 逐句合成并行推送,最小化感知延迟。 +**会话存储**(TieredManager):L1 Memory → L2 Redis → L3 PostgreSQL 三级存储,30 分钟 TTL,Redis 故障自动降级。 -### 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 故障软降级 +**鉴权**:JWT 双 token 轮转(Access 120min + Refresh 7d),重放攻击检测(DB hash 校验),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 三级存储) | +前端:React 18 + TypeScript + Vite,VAD(@ricky0123/vad-web),ONNX Runtime +后端:Go 1.25+, Gin, WebSocket, Viper, Zap, CloudWeGo Eino Graph +AI:DashScope qwen3-vl-plus, MiMo ASR/TTS(可切换 Deepgram/OpenAI TTS) +存储:PostgreSQL 15 + Redis 7 -## 构建与运行命令 +## 快速启动 ```bash -# 前端 -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 # 查看服务状态 +# 前端:npm run dev(Vite,代理 /ws 和 /api 到 :8080) +# 后端:go run ./cmd/server(监听 :8080) +# 生产:./deploy.sh up(4 容器:frontend/backend/postgres/redis) ``` -> **注意**:前端目前没有测试基础设施(无 vitest 配置、无测试文件)。后端使用 `testing` + `testify`(assert/require/mock)测试,编译期接口检查 `var _ Interface = (*Impl)(nil)`。 +核心环境变量(`.env.example`):`CAMTALK_AI_LLM_API_KEY`, `CAMTALK_AI_STT_API_KEY`, `CAMTALK_AUTH_JWT_SECRET`, `CAMTALK_STORAGE_DSN` -## 配置环境切换 +配置优先级:环境变量 > `config.{APP_ENV}.yaml` > `config.yaml` +环境切换:`APP_ENV=dev|prod`(dev 默认,prod 启用限流 + 严格 CORS) -项目通过 `APP_ENV` 环境变量控制配置文件加载: +## 协议与 API -- **本地开发**(默认):`APP_ENV=dev` → 加载 `config/config.dev.yaml` - - Debug 日志、关闭限流、允许所有 CORS - - 使用 `backend/.env` 中的远程 Redis/PostgreSQL 地址 +**WebSocket**:`ws://localhost:8080/ws?token=&conversation_id=` +- 客户端:`query`(图像/音频 Base64), `config`, `interrupt`, `ping` +- 服务端:`connected`, `stt_result`, `llm_chunk`, `llm_done`, `tts_audio`, `error`, `pong` +- 心跳:客户端 30s ping,服务端 60s 超时断连;重连:指数退避 1s→30s +- 实现:`CamTalkWebSocket` 单例(`frontend/src/lib/websocket.ts`),订阅模式,自动重连 -- **生产部署**:`APP_ENV=prod` → 加载 `config/config.prod.yaml` - - Info/JSON 日志、启用限流、严格 CORS 白名单 - - Docker Compose 自动设置,使用容器内网地址 +**REST API**:`/api/auth/*`(注册/登录/刷新/登出),`/api/conversations/*`(CRUD + 消息分页),`/api/health` -**配置优先级**:环境变量 > config.{env}.yaml > config.yaml > 默认值 +**错误码**:`INVALID_MESSAGE`, `SESSION_NOT_FOUND`, `RATE_LIMITED`, `IMAGE_TOO_LARGE`, `LLM_TIMEOUT`, `STT/TTS/LLM_ERROR`, `INVALID_TOKEN`, 等 -**手动切换环境**(测试用): -```bash -cd backend -APP_ENV=prod go run ./cmd/server # 本地测试生产配置 -APP_ENV=dev go run ./cmd/server # 显式指定开发配置 -``` +## 关键文件路径 -## 配置系统 +**后端核心**: +- `backend/internal/eino/` — Graph 定义、节点、Callback、Adapter、State +- `backend/internal/session/tiered.go` — 三级会话存储 +- `backend/internal/store/` — Repository 实现(PG + 内存 + Redis 缓存) +- `backend/internal/ws/handler.go` — WebSocket 连接管理 +- `backend/migrations/` — SQL 迁移文件 -配置文件:`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): -```bash -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=&conversation_id=` - -所有消息为 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` — 刷新 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_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 `