docs: 文档与代码一致性检查与修复

- 修复心跳 Bug:应用层 ping 不更新 lastPong,60 秒后连接会被错误断开
- 03-接口文档:audio/mpeg→audio/mp3、STTConfig/TTSConfig 补充 Model 字段、
  APP_ENV 环境变量名修正、配置搜索路径补充、.env 加载说明、Vite proxy 说明修正
- 02-系统架构:补充 ConfigPanel/Toast 组件、Model Router/Rate Limiter 标注规划中、
  补充 Gin 框架、MVP 存储改为 Memory、AI 服务 provider 更新、Orchestrator 伪代码对齐
- 04-技术选型:新增 AI 服务栈选型章节(STT/LLM/TTS)、PostgreSQL 标注规划中
- 06-语音交互:VAD 参数名修正、STT 改为一次性识别描述、音频编码格式补充
- 07-视觉理解:关键帧检测代码改为 TypeScript、分辨率修正、阈值逻辑统一
- 08-成本控制:变量名修正、未实现功能标注规划中、对话历史裁剪策略补充
- CLAUDE.md:同步更新技术栈、模块结构、存储策略描述
This commit is contained in:
hhs
2026-06-14 08:52:36 +08:00
parent a6ce6d9c4f
commit 6e0c67e1cb
8 changed files with 171 additions and 170 deletions

View File

@@ -73,7 +73,7 @@ interface ConfigMessage {
```typescript
interface InterruptMessage {
type: "interrupt";
request_id?: string; // 可选,指定打断哪次请求
request_id?: string; // 可选,当前实现不使用此字段,服务端始终取消当前活跃请求
}
```
@@ -143,7 +143,7 @@ interface TTSAudioMessage {
type: "tts_audio";
request_id: string;
audio: string; // Base64 编码的音频片段
mime_type: string; // "audio/mpeg"
mime_type: string; // "audio/mp3"
is_last: boolean; // 是否为最后一片
}
```
@@ -152,7 +152,7 @@ interface TTSAudioMessage {
| 属性 | 值 | 说明 |
|------|------|------|
| 编码 | `audio/mpeg`MP3 | 浏览器 `<audio>` 原生支持OpenAI TTS 默认输出 |
| 编码 | `audio/mp3`MP3 | 浏览器 `<audio>` 原生支持OpenAI TTS 默认输出 |
| 采样率 | 24kHz | OpenAI TTS 默认 |
| 声道 | 单声道 | 语音不需要立体声 |
| 传输 | Base64 编码的 MP3 片段 | 每个 `tts_audio` 消息携带一个句子的音频 |
@@ -165,7 +165,7 @@ interface TTSAudioMessage {
1. **排队播放**:收到 `tts_audio` 时,将 Base64 解码为 Blob URL 并加入播放队列。第一片到达即开始播放,后续片段在 `onended` 回调中自动衔接。
2. **错误容错**:单个片段播放失败时跳过,继续播放队列中下一个,不中断整个回复。
3. **打断清理**:收到 `interrupt` 消息或用户触发打断时,清空播放队列并释放所有 Blob URL。
4. **类型锁定**`mime_type` 字段固定为 `"audio/mpeg"`,前端解码时直接使用,无需运行时判断。
4. **类型锁定**`mime_type` 字段固定为 `"audio/mp3"`,前端解码时直接使用,无需运行时判断。
```typescript
// 前端播放器伪代码
@@ -173,7 +173,7 @@ class AudioPlayer {
private queue: string[] = []; // Blob URL 队列
enqueue(base64: string) {
const url = decodeBase64Audio(base64, "audio/mpeg");
const url = decodeBase64Audio(base64, "audio/mp3");
this.queue.push(url);
if (this.queue.length === 1) this.playNext(); // 第一片到了就开始播
}
@@ -293,7 +293,7 @@ DELETE /api/sessions/{session_id}
## 三、AI 服务层接口
Go 网关内部与外部 AI 服务Deepgram STT、GPT-4o、OpenAI TTS的调用契约。前后端联调时,后端需实现这些接口。
Go 网关内部与外部 AI 服务(STT、LLM、TTS的调用契约。默认配置为 Deepgram STT、GPT-4o LLM、OpenAI TTS,但通过 OpenAI 兼容接口可灵活切换到其他服务商(如 MiMo ASR、通义千问等)。前后端联调时,后端需实现这些接口。
### STT 服务接口
@@ -370,7 +370,7 @@ user: [图片 + 用户语音文本]
```
- 超时10 秒,超时返回 `LLM_TIMEOUT` 错误
- 模型选择:默认 `gpt-4o`由 Model Router 按需切换
- 模型选择:默认 `gpt-4o`可通过配置切换到其他 OpenAI 兼容模型
### TTS 服务接口
@@ -644,7 +644,7 @@ backend/config.dev.yaml # 开发环境go run 时使用)
backend/config.prod.yaml # 生产环境
```
Viper 加载顺序:先读 `config.yaml`,再根据 `APP_ENV` 环境变量尝试读 `config.{env}.yaml` 覆盖,最后所有环境变量自动覆盖对应字段。
Viper 加载顺序:先读 `config.yaml`,再根据 `APP_ENV` 环境变量尝试读 `config.{env}.yaml` 覆盖,最后所有环境变量自动覆盖对应字段。此外,代码还通过 `godotenv` 加载 `.env` 文件(优先级最低,仅用于本地开发环境)。
### Go 配置结构体
@@ -686,6 +686,7 @@ type AIConfig struct {
type STTConfig struct {
Provider string `mapstructure:"provider"` // "deepgram"
APIKey string `mapstructure:"api_key"`
Model string `mapstructure:"model"` // 默认 "nova-2"
Endpoint string `mapstructure:"endpoint"` // 默认 "wss://api.deepgram.com/v1/listen"
}
@@ -700,6 +701,7 @@ type LLMConfig struct {
type TTSConfig struct {
Provider string `mapstructure:"provider"` // "openai"
APIKey string `mapstructure:"api_key"`
Model string `mapstructure:"model"` // 默认 "tts-1"
Voice string `mapstructure:"voice"` // 默认 "alloy"
Speed float64 `mapstructure:"speed"` // 默认 1.0
Endpoint string `mapstructure:"endpoint"` // 默认 "https://api.openai.com/v1"
@@ -738,6 +740,7 @@ redis:
ai:
stt:
provider: deepgram
model: nova-2
endpoint: "wss://api.deepgram.com/v1/listen"
llm:
provider: openai
@@ -746,6 +749,7 @@ ai:
timeout: 10
tts:
provider: openai
model: tts-1
voice: alloy
speed: 1.0
endpoint: "https://api.openai.com/v1"
@@ -791,8 +795,9 @@ func Load() (*Config, error) {
// 1. 读默认配置文件
v.SetConfigName("config")
v.SetConfigType("yaml")
v.AddConfigPath("./config") // go run 时
v.AddConfigPath(".") // 二进制运行时
v.AddConfigPath(".")
v.AddConfigPath("./config")
v.AddConfigPath("./backend")
if err := v.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return nil, fmt.Errorf("read config: %w", err)
@@ -800,7 +805,7 @@ func Load() (*Config, error) {
}
// 2. 按环境覆盖
env := os.Getenv("CAMTALK_APP_ENV")
env := os.Getenv("APP_ENV")
if env == "" {
env = "dev"
}
@@ -1030,7 +1035,7 @@ func NewApp(cfg *Config) *App {
## 十、连接管理
**心跳机制**:客户端每 30 秒发送 `ping`,服务端回复 `pong`。超过 60 秒无 `ping`,服务端判定连接断开并清理会话资源。
**心跳机制**:客户端每 30 秒发送应用层 `{type: "ping"}` 消息,服务端回复 `{type: "pong"}` 并刷新心跳计时器。超过 60 秒无 `ping`,服务端判定连接断开并清理会话资源。
**重连策略**(指数退避 + 抖动):
@@ -1049,18 +1054,10 @@ function reconnect(attempt: number) {
**生产环境**Nginx 将 `/`(前端)、`/api/*`REST`/ws`WebSocket统一反代到同一域名详见 `02-系统架构.md` 部署架构章节。
**开发环境**Vite 内置代理,前端 :5173 的 `/api``/ws` 请求代理到后端 :8080
**开发环境**前端 WebSocket 地址硬编码为 `ws://localhost:8080/ws`,直连后端,不经过 Vite 代理。REST API 同理直连 `http://localhost:8080`
```typescript
// frontend/vite.config.ts
server: {
proxy: {
"/api": "http://localhost:8080",
"/ws": { target: "ws://localhost:8080", ws: true },
},
},
```
> 当前开发模式为前后端直连,未使用 Vite proxy。如需使用 Vite 代理解决跨域,需在 `vite.config.ts` 中添加 `server.proxy` 配置,并将前端 WebSocket 地址改为相对路径。
**Go 后端 WebSocket CheckOrigin**:生产环境 Nginx 同源,`CheckOrigin` 可保持默认(拒绝跨域)。开发环境由 Vite proxy 转发,不存在跨域。因此后端无需配置 CORS 中间件,`CheckOrigin` 保持 gorilla/websocket 默认值即可
**Go 后端 WebSocket CheckOrigin**:生产环境 Nginx 同源,`CheckOrigin` 可保持默认(拒绝跨域)。开发环境前端直连后端,需确保 `CheckOrigin` 允许跨域或使用 Vite proxy 转发
> 如果未来需要支持第三方客户端直连(如移动端),再按需添加 CORS 中间件和 `CheckOrigin` 白名单。