Some checks failed
Build and Deploy Backend / build-and-deploy (push) Failing after 4m28s
## 变更内容
### 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>
74 lines
2.1 KiB
Go
74 lines
2.1 KiB
Go
package handler
|
||
|
||
import (
|
||
"context"
|
||
"log"
|
||
"time"
|
||
|
||
"github.com/qy/hw-ws-service/internal/audio"
|
||
"github.com/qy/hw-ws-service/internal/connection"
|
||
"github.com/qy/hw-ws-service/internal/rtcclient"
|
||
)
|
||
|
||
// HandleStory 处理硬件发来的 {"type":"story"} 指令。
|
||
// 在独立 goroutine 中调用,不阻塞消息读取循环。
|
||
func HandleStory(conn *connection.Connection, client *rtcclient.Client) {
|
||
tag := "[story][" + conn.DeviceID + "]"
|
||
|
||
// 整个故事播放流程最长允许 10 分钟
|
||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||
defer cancel()
|
||
|
||
// 1. 通知硬件:TTS 开始
|
||
if err := conn.SendJSON(map[string]string{"type": "tts", "state": "start"}); err != nil {
|
||
log.Printf("%s send start failed: %v", tag, err)
|
||
return
|
||
}
|
||
|
||
// 确保异常退出时也发送 stop,避免硬件卡住
|
||
defer func() {
|
||
conn.StopPlayback()
|
||
conn.SendJSON(map[string]string{"type": "tts", "state": "stop"}) //nolint:errcheck
|
||
}()
|
||
|
||
// 2. 调用 RTC 后端获取故事
|
||
story, err := client.FetchStoryByMAC(ctx, conn.DeviceID)
|
||
if err != nil {
|
||
log.Printf("%s fetch story error: %v", tag, err)
|
||
return
|
||
}
|
||
if story == nil {
|
||
log.Printf("%s no story available", tag)
|
||
return
|
||
}
|
||
log.Printf("%s playing: %s", tag, story.Title)
|
||
|
||
// 3. 下载 MP3 并转码为 Opus 帧(CPU 密集,在当前 goroutine 中执行)
|
||
frames, err := audio.MP3URLToOpusFrames(ctx, story.AudioURL)
|
||
if err != nil {
|
||
log.Printf("%s audio convert error: %v", tag, err)
|
||
return
|
||
}
|
||
log.Printf("%s converted %d frames (~%.1fs)", tag, len(frames),
|
||
float64(len(frames)*audio.FrameDurationMs)/1000)
|
||
|
||
// 4. 通知硬件:句子开始(发送故事标题)
|
||
if err := conn.SendJSON(map[string]any{
|
||
"type": "tts",
|
||
"state": "sentence_start",
|
||
"text": story.Title,
|
||
}); err != nil {
|
||
log.Printf("%s send sentence_start failed: %v", tag, err)
|
||
return
|
||
}
|
||
|
||
// 5. 开始播放,获取打断 channel
|
||
abortCh := conn.StartPlayback()
|
||
|
||
// 6. 流控推送 Opus 帧
|
||
SendOpusStream(conn, frames, abortCh)
|
||
|
||
log.Printf("%s playback finished", tag)
|
||
// defer 会发送 stop 并调用 StopPlayback
|
||
}
|