repair-agent c219ec2fcf
Some checks failed
Build and Deploy Backend / build-and-deploy (push) Failing after 4m28s
feat(hw-ws-service): 将 Go WebSocket 服务纳入 CI/CD 并通过 Traefik 统一入口
## 变更内容

### k8s/ingress.yaml
- 新增 /xiaozhi/v1/ 路径规则,将 WebSocket 流量路由到 hw-ws-svc:8888
- Traefik 最长前缀优先,/xiaozhi/v1/ 不影响 / 下的 Django 路由

### hw_service_go/k8s/service.yaml
- Service 类型由 LoadBalancer 改为 ClusterIP
- 移除阿里云 SLB 注解(通过 Traefik Ingress 统一暴露,不再需要独立公网 IP)

### hw_service_go/k8s/deployment.yaml
- 镜像地址改为 ${CI_REGISTRY_IMAGE}/hw-ws-service:latest 占位符
- CI/CD 部署时统一通过 sed 替换为华为云 SWR 实际地址

### hw_service_go/internal/server/server.go
- 新增 GET /xiaozhi/v1/healthz 接口,返回 {"status":"ok","active_connections":N}
- 用于部署后验证服务存活及当前连接数

### .gitea/workflows/deploy.yaml
- 新增 Build and Push HW WebSocket Service 步骤,构建并推送 hw_service_go 镜像
- 部署步骤新增 kubectl apply hw_service_go/k8s/deployment.yaml 和 service.yaml
- 新增 kubectl rollout restart deployment/hw-ws-service

### run.sh
- 本地同时启动 Django + hw_service_go 的开发脚本

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 17:16:26 +08:00

252 lines
12 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.

# hw_service_go 本地硬件通讯测试计划
> 目标:用浏览器模拟 ESP32 硬件,验证 `hw_service_go` WebSocket 服务能否正常接收指令、获取故事、推送 Opus 音频。
---
## 一、协议对比分析
### 1.1 小智xiaozhi-servervs 我们的服务
| 维度 | xiaozhi-server | hw_service_go本服务 |
|------|---------------|------------------------|
| **WebSocket URL** | `ws://host:port/xiaozhi/v1/?device-id=&client-id=` | 完全相同 |
| **连接参数** | `device-id`MAC`client-id`UUID| 完全相同 |
| **握手消息** | 需要发送 `hello` JSON | **不需要**,连上即用 |
| **触发指令** | `listen`(语音输入) | **只需发 `{"type":"story"}`** |
| **音频方向** | 双向(硬件上传语音 + 服务下发 TTS| **单向下行**(服务→硬件,推 Opus |
| **Opus 编解码** | 需要编码(麦克风)+ 解码(播放)| **只需解码**(浏览器只播放) |
| **认证** | token 参数 | **无需认证**(仅 device-id 校验) |
| **消息复杂度** | hello/listen/stt/llm/tts/mcp | **只有 tts 系列** |
### 1.2 我们服务的完整消息流
```
浏览器(模拟硬件) hw_service_go Django
│ │ │
│── WS 连接 ──────────────────────────→│ │
│ ?device-id=AA:BB:CC:DD:EE:FF │ │
│ &client-id=test-001 │ │
│ │ │
│── {"type":"story"} ────────────────→ │ │
│ │── GET /api/v1/devices/ │
│ │ stories/?mac_address │
│ │ =AA:BB:CC:DD:EE:FF → │
│ │ │
│← {"type":"tts","state":"start"} ───── │ │
│ │← {title, audio_url} ── │
│ │ │
│ │── 下载 MP3 ──────────→ CDN
│ │← MP3 二进制流 ─────── │
│ │ │
│ │ ffmpeg 转码 PCM→Opus │
│ │ │
│← {"type":"tts","state":"sentence_start","text":"故事标题"} ─── │
│ │ │
│← [Opus帧1 二进制] ─────────────────── │ 60ms/帧前3帧预缓冲 │
│← [Opus帧2 二进制] ─────────────────── │ │
│← [Opus帧3 二进制] ─────────────────── │ │
│← [Opus帧N 二进制] ─────────────────── │ 按时序流控发送 │
│ │ │
│← {"type":"tts","state":"stop"} ─────── │ │
│ │ │
```
### 1.3 Opus 音频参数(与小智完全一致)
| 参数 | 值 |
|------|----|
| 采样率 | 16000 Hz |
| 声道 | 1单声道|
| 帧时长 | 60ms |
| 每帧采样数 | 960 |
| 编码器 | libopusWASM |
---
## 二、前置条件检查
在开始测试之前,需要满足以下条件:
### 2.1 服务运行状态
- [ ] Django 后端运行在 `http://localhost:8000`
- [ ] `hw_service_go` 运行在 `ws://localhost:8888`
- [ ] 健康检查通过:`curl http://localhost:8888/healthz` 返回 200
### 2.2 Django 数据准备(关键!)
测试必须使用一个在 Django 数据库中**真实存在**的设备 MAC 地址。
Django API 查询逻辑(`GET /api/v1/devices/stories/?mac_address=<MAC>`
- 根据 MAC 查找设备 → 找到设备绑定的用户 → 查找该用户的故事
- 任何一步缺失,服务返回 `null`,硬件不会播放任何内容
**需要在 Django Admin 或 API 中准备:**
1. 注册一个设备,记下其 MAC 地址(格式:`AA:BB:CC:DD:EE:FF`
2. 该设备需已绑定用户owner
3. 该用户名下有至少一个故事(有 `audio_url` 字段)
> **快速验证**`curl "http://localhost:8000/api/v1/devices/stories/?mac_address=你的MAC"` 应返回 `{"code":0,"data":{"title":"...","audio_url":"..."}}`
---
## 三、测试程序设计
### 3.1 技术选型
| 方案 | 优点 | 缺点 |
|------|------|------|
| **纯 HTML+JS推荐** | 零依赖,直接浏览器打开,与小智方案一致 | - |
| Python 脚本 | 简单但无法播放音频 | 无法验证音频播放端到端 |
| Go 命令行 | 需额外音频库 | 环境搭建复杂 |
**选择方案:纯 HTML+JS 单文件**,复用小智项目的 `libopus.js`WASM做解码。
### 3.2 文件结构
```
hw_service_go/test/
├── PLAN.md ← 本文件
├── test.html ← 测试主页面(待实现)
└── libopus.js ← 复制自小智项目Opus WASM 解码库)
```
`libopus.js` 来源:
```
/Users/maidong/Desktop/zyc/jikashe/xiaozhi-server/main/xiaozhi-server/test/libopus.js
```
### 3.3 测试页面功能
```
┌─────────────────────────────────────────────────────┐
│ hw_service_go 硬件通讯测试 │
├─────────────────────────────────────────────────────┤
│ 服务地址: [ws://localhost:8888/xiaozhi/v1/ ] │
│ device-id: [AA:BB:CC:DD:EE:FF ] │
│ client-id: [test-browser-001 ] [随机生成] │
├─────────────────────────────────────────────────────┤
│ [连接] [断开] 状态: ● 未连接 │
├─────────────────────────────────────────────────────┤
│ [▶ 触发故事播放] [⏹ 停止] │
├─────────────────────────────────────────────────────┤
│ 消息日志 [清空] │
│ ┌───────────────────────────────────────────────┐ │
│ │ [10:23:01] → 已连接 │ │
│ │ [10:23:02] → 发送: {"type":"story"} │ │
│ │ [10:23:02] ← 收到: {"type":"tts","state":"start"} │
│ │ [10:23:03] ← 收到: {"type":"tts","state":"sentence_start","text":"..."} │
│ │ [10:23:03] ← 收到: [Binary] Opus帧 #1 (38 bytes) │
│ │ [10:23:03] 🔊 开始播放... │ │
│ │ [10:23:15] ← 收到: {"type":"tts","state":"stop"} │
│ │ [10:23:15] 🔊 播放完毕 │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ 统计: 已收到 85 个Opus帧 | 约 5.1s 音频 │
└─────────────────────────────────────────────────────┘
```
### 3.4 核心实现逻辑
#### 连接流程
```javascript
const ws = new WebSocket(
`ws://localhost:8888/xiaozhi/v1/?device-id=${deviceId}&client-id=${clientId}`
);
ws.binaryType = 'arraybuffer';
```
#### 触发故事
```javascript
ws.send(JSON.stringify({ type: 'story' }));
```
#### 接收消息处理
```javascript
ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
// 二进制Opus 音频帧
const opusFrame = new Uint8Array(event.data);
const pcm = opusDecoder.decode(opusFrame); // Int16Array
schedulePlay(pcm); // 排队播放
} else {
// 文本:控制消息
const msg = JSON.parse(event.data);
handleTtsControl(msg); // 处理 start/sentence_start/stop
}
};
```
#### Opus 解码 + 播放(与小智方案完全一致)
- 使用 `libopus.js`WASM初始化解码器16000Hz单声道
- 解码:`Int16Array``Float32Array`
- 使用 `AudioContext` + `AudioBufferSourceNode` 按时序排队播放
- 使用 `BlockingQueue` 缓冲帧,避免播放卡顿
---
## 四、测试用例
### Case 1基础连接测试
- 输入正确的 `device-id``client-id`
- 期望WebSocket 连接建立成功,状态变为"已连接"
### Case 2故事触发测试
- 发送 `{"type":"story"}`
- 期望:
1. 收到 `{"type":"tts","state":"start"}`
2. 收到 `{"type":"tts","state":"sentence_start","text":"<故事标题>"}`
3. 陆续收到多个二进制 Opus 帧
4. 最终收到 `{"type":"tts","state":"stop"}`
### Case 3音频播放验证
- 期望:浏览器实际播放出故事音频,声音正常无杂音、无卡顿
### Case 4设备不存在测试
- 使用未注册的 MAC 地址
- 期望:发送故事指令后立即收到 `{"type":"tts","state":"stop"}`(服务侧找不到故事,直接结束)
### Case 5重复触发测试
- 播放过程中再次点击"触发故事"
- 期望旧播放被打断新故事从头开始hw_service_go 的 `StartPlayback` 会 close 旧 abortCh
### Case 6断线重连测试
- 连接后断开,再重新连接
- 期望:可以正常重新发起故事请求
---
## 五、实现步骤
1. **复制 libopus.js**
```bash
cp /Users/maidong/Desktop/zyc/jikashe/xiaozhi-server/main/xiaozhi-server/test/libopus.js \
/Users/maidong/Desktop/zyc/qy_gitlab/rtc_backend/hw_service_go/test/
```
2. **编写 test.html**(单文件,嵌入所有 JS
- 参考小智 `StreamingContext.js` 和 `BlockingQueue.js` 的逻辑
- 去掉录音/编码部分(我们只需解码)
- 保留 Opus 解码 + AudioContext 播放部分
- 添加连接配置 UI 和消息日志面板
3. **浏览器打开测试**
```
直接用浏览器打开 test.htmlfile:// 协议即可)
```
> 注意macOS Safari 对 WebSocket + file:// 可能有限制,建议用 Chrome
4. **按测试用例逐项验证**
---
## 六、已知限制与注意事项
| 问题 | 说明 |
|------|------|
| **device-id 必须真实存在** | MAC 地址若未在 Django 数据库注册,服务会静默返回无故事 |
| **ffmpeg 必须安装** | `hw_service_go` 的转码依赖系统 `ffmpeg`,需提前安装 |
| **audio_url 必须可访问** | 故事的 MP3 链接需要能从本机下载(阿里云 OSS 等) |
| **浏览器 AudioContext 限制** | 需要用户交互(点击)后才能创建 AudioContext不能自动播放 |
| **WASM 加载** | libopus.js 较大844KB首次加载需要等待约 1-2 秒 |