Files
AskaEth 71f0a1abdb feat: Go模块路径迁移 + Docker生产部署适配 + ethend Docker兼容
- 所有Go模块路径从 github.com/yourname/cyrene-ai 迁移到 git.yeij.top/AskaEth/Cyrene
- 5个Go Dockerfile添加 GOPROXY=https://goproxy.cn,direct 解决国内构建问题
- ai-core go.mod 添加 pkg/plugins replace 指令
- Caddyfile 简化为 http:// 通配 + handle 保留 /api 前缀
- ethend Dockerfile 适配 (npm install + 仅 COPY package.json)
- ethend 新增 RUNNING_IN_DOCKER 环境变量,健康检查改用Docker服务名
- ethend 数据库状态检查支持Docker hostname (postgres/redis/qdrant/minio)
- process-manager 新增 CONTAINER_SVC_MAP + Docker模式自动检测
- 统一 docker-compose.dev.db.yml 卷名 (pg_data/redis_data/qdrant_data/minio_data)
- docker-compose.yml ethend服务挂载docker.sock + 端口变量化
- 清理 .env 统一后的残留文件与提示信息

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 13:43:22 +08:00

159 lines
4.1 KiB
Go

package handler
import (
"encoding/json"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
"git.yeij.top/AskaEth/Cyrene/pkg/logger"
"git.yeij.top/AskaEth/Cyrene/voice-service/internal/service"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 4096,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
// StreamingSTTHandler 处理实时语音识别 WebSocket 连接。
// 客户端通过 WebSocket 流式发送音频二进制帧,服务端通过一条持久的
// DashScope WebSocket 连接转发音频并持续返回识别结果。
type StreamingSTTHandler struct {
svc *service.STTService
}
// NewStreamingSTTHandler 创建流式 STT 处理器。
func NewStreamingSTTHandler(svc *service.STTService) *StreamingSTTHandler {
return &StreamingSTTHandler{svc: svc}
}
// HandleStreamingSTT 处理 WebSocket 升级和实时 STT 会话。
// GET /api/v1/stt/stream
func (h *StreamingSTTHandler) HandleStreamingSTT(w http.ResponseWriter, r *http.Request) {
if !h.svc.IsAvailable() {
writeJSON(w, http.StatusServiceUnavailable, map[string]interface{}{
"error": "STT 引擎不可用",
})
return
}
language := r.URL.Query().Get("language")
if language == "" {
language = "zh"
}
format := r.URL.Query().Get("format")
if format == "" {
format = "pcm"
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
logger.Printf("[stream-stt] WebSocket 升级失败: %v", err)
return
}
defer conn.Close()
logger.Printf("[stream-stt] 客户端已连接, format=%s, language=%s", format, language)
// 创建持久的 DashScope 流式会话
session, err := h.svc.StartStreaming(format, language)
if err != nil {
logger.Printf("[stream-stt] 创建 DashScope 会话失败: %v", err)
conn.WriteJSON(map[string]interface{}{
"type": "error",
"error": "启动语音识别失败: " + err.Error(),
})
return
}
defer session.Close()
var mu sync.Mutex
conn.SetReadDeadline(time.Now().Add(300 * time.Second)) // 5 分钟超时
// goroutine: 读取 DashScope 结果并推送到客户端
resultDone := make(chan struct{})
go func() {
defer close(resultDone)
for result := range session.Results() {
mu.Lock()
if result.Error != "" {
logger.Printf("[stream-stt] DashScope 错误: %s", result.Error)
conn.WriteJSON(map[string]interface{}{
"type": "error",
"error": result.Error,
})
} else if result.Text != "" {
conn.WriteJSON(map[string]interface{}{
"type": "result",
"text": result.Text,
"isFinal": result.IsFinal,
})
}
mu.Unlock()
}
}()
// 主循环: 读取客户端音频帧
for {
msgType, data, err := conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
logger.Printf("[stream-stt] 读取错误: %v", err)
}
break
}
// 文本控制消息
if msgType == websocket.TextMessage {
var ctrl map[string]interface{}
if json.Unmarshal(data, &ctrl) == nil {
action, _ := ctrl["action"].(string)
if action == "stop" {
logger.Printf("[stream-stt] 客户端请求停止")
mu.Lock()
conn.WriteJSON(map[string]interface{}{
"type": "done",
"action": "stop",
})
mu.Unlock()
break
}
// 支持动态切换语言
if lang, ok := ctrl["language"].(string); ok && lang != "" {
language = lang
logger.Printf("[stream-stt] 切换语言: %s", lang)
}
}
continue
}
// 二进制音频帧: 发送到 DashScope
if msgType == websocket.BinaryMessage && len(data) > 0 {
if err := session.SendAudio(data); err != nil {
logger.Printf("[stream-stt] 发送音频帧失败: %v", err)
mu.Lock()
conn.WriteJSON(map[string]interface{}{
"type": "error",
"error": "发送音频失败: " + err.Error(),
})
mu.Unlock()
break
}
}
}
// 等待结果推送完成
<-resultDone
logger.Printf("[stream-stt] 会话结束")
}
// RegisterStreamingRoutes 注册流式 STT 路由。
func (h *StreamingSTTHandler) RegisterStreamingRoutes(mux *http.ServeMux) {
mux.HandleFunc("/api/v1/stt/stream", h.HandleStreamingSTT)
}