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>
101 lines
2.7 KiB
Go
101 lines
2.7 KiB
Go
// Package rtcclient 封装对 RTC 后端 Django REST API 的 HTTP 调用。
|
||
package rtcclient
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// StoryInfo 是 GET /api/v1/devices/stories/ 返回的故事信息。
|
||
type StoryInfo struct {
|
||
Title string `json:"title"`
|
||
AudioURL string `json:"audio_url"`
|
||
}
|
||
|
||
// Client 是 RTC 后端的 HTTP 客户端,复用连接池。
|
||
type Client struct {
|
||
baseURL string
|
||
httpClient *http.Client
|
||
}
|
||
|
||
// New 创建 Client,baseURL 形如 "http://rtc-backend-svc:8000"。
|
||
func New(baseURL string) *Client {
|
||
return &Client{
|
||
baseURL: strings.TrimRight(baseURL, "/"),
|
||
httpClient: &http.Client{
|
||
Timeout: 10 * time.Second,
|
||
Transport: &http.Transport{
|
||
MaxIdleConns: 50,
|
||
MaxIdleConnsPerHost: 10,
|
||
IdleConnTimeout: 90 * time.Second,
|
||
},
|
||
// 限制重定向次数,防止无限跳转
|
||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||
if len(via) >= 3 {
|
||
return errors.New("rtcclient: too many redirects")
|
||
}
|
||
return nil
|
||
},
|
||
},
|
||
}
|
||
}
|
||
|
||
// rtcResponse 是 RTC 后端的统一响应结构。
|
||
type rtcResponse struct {
|
||
Code int `json:"code"`
|
||
Message string `json:"message"`
|
||
Data json.RawMessage `json:"data"`
|
||
}
|
||
|
||
// FetchStoryByMAC 通过设备 MAC 地址获取随机故事。
|
||
// 返回 nil, nil 表示设备/用户/故事不存在(非错误,调用方直接跳过)。
|
||
func (c *Client) FetchStoryByMAC(ctx context.Context, mac string) (*StoryInfo, error) {
|
||
url := c.baseURL + "/api/v1/devices/stories/"
|
||
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("rtcclient: build request: %w", err)
|
||
}
|
||
|
||
q := req.URL.Query()
|
||
q.Set("mac_address", strings.ToUpper(mac))
|
||
req.URL.RawQuery = q.Encode()
|
||
|
||
resp, err := c.httpClient.Do(req)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("rtcclient: request failed: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
// 404 表示设备/用户/故事不存在,不是服务器错误
|
||
if resp.StatusCode == http.StatusNotFound {
|
||
return nil, nil
|
||
}
|
||
if resp.StatusCode != http.StatusOK {
|
||
return nil, fmt.Errorf("rtcclient: unexpected status %d", resp.StatusCode)
|
||
}
|
||
|
||
var result rtcResponse
|
||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||
return nil, fmt.Errorf("rtcclient: decode response: %w", err)
|
||
}
|
||
if result.Code != 0 {
|
||
return nil, nil // 业务错误(如暂无故事),返回 nil 让调用方处理
|
||
}
|
||
|
||
var story StoryInfo
|
||
if err := json.Unmarshal(result.Data, &story); err != nil {
|
||
return nil, fmt.Errorf("rtcclient: decode story: %w", err)
|
||
}
|
||
if story.Title == "" || story.AudioURL == "" {
|
||
return nil, nil
|
||
}
|
||
|
||
return &story, nil
|
||
}
|