Files
CamTalk/CLAUDE.md
hhs 7adf81c6e5 feat: 集成限流器到服务
- main.go 初始化限流器(根据 Redis 可用性选择内存/Redis 实现)
- WebSocket handler 添加 query 消息限流(按 userID)
- Auth API 添加登录/注册限流(按 IP)
- refresh 和 logout 不限流(避免影响正常用户操作)
- 修复所有测试(传递 nil limiter 参数)
- 所有测试通过(包括 ws 和 api 集成测试)
2026-06-21 00:00:24 +08:00

273 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 渲染。核心 Hook`useVisionSession()`
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 → 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-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`、收集最终输出 |
**跨节点状态**`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 → 回填 L1L3 通过 L1 的 `FindByID` 降级读取
- **写路径**L1 同步写入 → L2 同步写(失败 soft-warn→ L3 异步 goroutine 写(使用 `context.Background()` 防止请求取消丢失)
- **降级**:后台协程每 30 秒 ping RedisRedis 不可用时自动跳过 L2 操作;恢复后自动重新启用
- **TTL**Session 默认 30 分钟MaxHistory 20 条L1 后台协程每分钟清理过期 session
Repository 接口模式:`UserRepository``MessageRepository``SessionRepository`,均有 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
- `CachedUserRepository``backend/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 三级存储) |
## 构建与运行命令
```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 # 查看服务状态
```
> **注意**:前端目前没有测试基础设施(无 vitest 配置、无测试文件)。后端使用 `testing` + `testify`assert/require/mock测试编译期接口检查 `var _ Interface = (*Impl)(nil)`。
## 配置系统
配置文件:`backend/config.yaml`(默认值),可被 `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` | 启用 Redistrue/false |
| `CAMTALK_STORAGE_PERSISTENCE_ENABLED` | 启用 PostgreSQLtrue/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=<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` — 刷新 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` hook640x480, facingMode: environment |
| `MicManager` | 麦克风音频采集(`useMicrophone` hook16kHz 单声道) |
| `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扁平常量 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.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/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-令牌桶限流设计.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`)。未使用变量以 `_` 前缀忽略。
- **提交信息**Conventional Commits 格式,描述用中文。示例:`feat: 添加 WebSocket 连接管理``fix: 修复心跳超时判断``docs: 更新接口文档`
- **禁止自动 push**:除非用户明确要求。
- **文档优先**:实现功能前先读取 `docs/` 下的相关设计文档。实现与文档不一致时,优先更新 `docs/` 下的接口文档。