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

86 lines
2.1 KiB
Go

// Package connection 管理单个 ESP32 硬件 WebSocket 连接的状态。
package connection
import (
"encoding/json"
"fmt"
"sync"
"github.com/gorilla/websocket"
)
// Connection 保存单个硬件连接的状态,所有方法并发安全。
type Connection struct {
WS *websocket.Conn
DeviceID string // MAC 地址,来自 URL 参数 device-id
ClientID string // 来自 URL 参数 client-id
mu sync.Mutex
isPlaying bool
abortCh chan struct{} // close(abortCh) 通知流控 goroutine 中止播放
writeMu sync.Mutex // gorilla/websocket 写操作不并发安全,需独立锁
}
// New 创建新连接对象。
func New(ws *websocket.Conn, deviceID, clientID string) *Connection {
return &Connection{
WS: ws,
DeviceID: deviceID,
ClientID: clientID,
}
}
// StartPlayback 开始新一轮播放,返回 abortCh 供流控 goroutine 监听。
// 若已在播放,先中止上一轮再开始新的。
func (c *Connection) StartPlayback() <-chan struct{} {
c.mu.Lock()
defer c.mu.Unlock()
// 中止上一轮播放(若有)
if c.isPlaying && c.abortCh != nil {
close(c.abortCh)
}
c.abortCh = make(chan struct{})
c.isPlaying = true
return c.abortCh
}
// StopPlayback 结束播放状态。
func (c *Connection) StopPlayback() {
c.mu.Lock()
defer c.mu.Unlock()
c.isPlaying = false
}
// IsPlaying 返回当前是否正在播放。
func (c *Connection) IsPlaying() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.isPlaying
}
// SendJSON 序列化 v 并以文本帧发送给设备,并发安全。
func (c *Connection) SendJSON(v any) error {
data, err := json.Marshal(v)
if err != nil {
return fmt.Errorf("connection: marshal json: %w", err)
}
c.writeMu.Lock()
defer c.writeMu.Unlock()
return c.WS.WriteMessage(websocket.TextMessage, data)
}
// SendBinary 以二进制帧发送 Opus 数据,并发安全。
func (c *Connection) SendBinary(data []byte) error {
c.writeMu.Lock()
defer c.writeMu.Unlock()
return c.WS.WriteMessage(websocket.BinaryMessage, data)
}
// Close 关闭底层 WebSocket 连接。
func (c *Connection) Close() {
c.WS.Close()
}