71f0a1abdb
- 所有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>
180 lines
5.1 KiB
Go
180 lines
5.1 KiB
Go
package handler
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"io"
|
||
"git.yeij.top/AskaEth/Cyrene/pkg/logger"
|
||
"net/http"
|
||
"strings"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// VoiceHandler 语音处理器 — 代理到 voice-service
|
||
type VoiceHandler struct {
|
||
voiceServiceURL string
|
||
client *http.Client
|
||
}
|
||
|
||
// NewVoiceHandler 创建语音处理器
|
||
func NewVoiceHandler(voiceServiceURL string) *VoiceHandler {
|
||
return &VoiceHandler{
|
||
voiceServiceURL: voiceServiceURL,
|
||
client: &http.Client{},
|
||
}
|
||
}
|
||
|
||
// Transcribe POST /api/v1/voice/transcribe
|
||
// 代理 multipart/form-data 请求到 voice-service
|
||
func (h *VoiceHandler) Transcribe(c *gin.Context) {
|
||
// 限制上传大小 (10MB)
|
||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 10<<20)
|
||
|
||
// 读取原始请求体
|
||
bodyBytes, err := io.ReadAll(c.Request.Body)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "读取请求体失败或文件过大,最大支持 10MB"})
|
||
return
|
||
}
|
||
|
||
// 构建代理请求
|
||
url := strings.TrimRight(h.voiceServiceURL, "/") + "/api/v1/transcribe"
|
||
|
||
proxyReq, err := http.NewRequest("POST", url, bytes.NewReader(bodyBytes))
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "构建代理请求失败"})
|
||
return
|
||
}
|
||
proxyReq.Header.Set("Content-Type", c.GetHeader("Content-Type"))
|
||
|
||
resp, err := h.client.Do(proxyReq)
|
||
if err != nil {
|
||
logger.Printf("[voice] Voice-Service 不可达 (Transcribe): %v", err)
|
||
c.JSON(http.StatusBadGateway, gin.H{
|
||
"error": "Voice-Service 不可达: " + err.Error(),
|
||
"errorType": "voice_service_unreachable",
|
||
"hint": "Voice-Service 服务未启动或不可达,请先在「服务管理」面板中启动 Voice-Service",
|
||
})
|
||
return
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
respBody, _ := io.ReadAll(resp.Body)
|
||
|
||
// 透传状态码和响应
|
||
c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), respBody)
|
||
}
|
||
|
||
// TTSSynthesize POST /api/v1/voice/tts
|
||
// 代理 JSON 请求到 voice-service TTS 合成
|
||
func (h *VoiceHandler) TTSSynthesize(c *gin.Context) {
|
||
// 读取 JSON 请求体
|
||
bodyBytes, err := io.ReadAll(c.Request.Body)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "读取请求体失败"})
|
||
return
|
||
}
|
||
|
||
// 构建代理请求
|
||
url := strings.TrimRight(h.voiceServiceURL, "/") + "/api/v1/tts/synthesize"
|
||
|
||
proxyReq, err := http.NewRequest("POST", url, bytes.NewReader(bodyBytes))
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "构建代理请求失败"})
|
||
return
|
||
}
|
||
proxyReq.Header.Set("Content-Type", "application/json")
|
||
|
||
resp, err := h.client.Do(proxyReq)
|
||
if err != nil {
|
||
logger.Printf("[voice] Voice-Service 不可达 (TTS): %v", err)
|
||
c.JSON(http.StatusBadGateway, gin.H{
|
||
"error": "Voice-Service 不可达: " + err.Error(),
|
||
"errorType": "voice_service_unreachable",
|
||
"hint": "Voice-Service 服务未启动或不可达",
|
||
})
|
||
return
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
respBody, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取 Voice-Service 响应失败"})
|
||
return
|
||
}
|
||
|
||
// 透传状态码、Content-Type 和响应体
|
||
c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), respBody)
|
||
}
|
||
|
||
// TTSVoices GET /api/v1/voice/tts/voices
|
||
// 代理请求到 voice-service 获取可用语音列表
|
||
func (h *VoiceHandler) TTSVoices(c *gin.Context) {
|
||
url := strings.TrimRight(h.voiceServiceURL, "/") + "/api/v1/tts/voices"
|
||
|
||
resp, err := h.client.Get(url)
|
||
if err != nil {
|
||
logger.Printf("[voice] Voice-Service 不可达 (Voices): %v", err)
|
||
c.JSON(http.StatusBadGateway, gin.H{
|
||
"error": "Voice-Service 不可达: " + err.Error(),
|
||
"errorType": "voice_service_unreachable",
|
||
})
|
||
return
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
respBody, _ := io.ReadAll(resp.Body)
|
||
|
||
// 解析并透传
|
||
var data interface{}
|
||
json.Unmarshal(respBody, &data)
|
||
c.JSON(resp.StatusCode, data)
|
||
}
|
||
|
||
// TTSStatus GET /api/v1/voice/tts/status
|
||
// 代理请求到 voice-service 获取 TTS 状态
|
||
func (h *VoiceHandler) TTSStatus(c *gin.Context) {
|
||
url := strings.TrimRight(h.voiceServiceURL, "/") + "/api/v1/tts/status"
|
||
|
||
resp, err := h.client.Get(url)
|
||
if err != nil {
|
||
logger.Printf("[voice] Voice-Service 不可达 (TTS Status): %v", err)
|
||
c.JSON(http.StatusBadGateway, gin.H{
|
||
"error": "Voice-Service 不可达: " + err.Error(),
|
||
"errorType": "voice_service_unreachable",
|
||
})
|
||
return
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
respBody, _ := io.ReadAll(resp.Body)
|
||
|
||
var data interface{}
|
||
json.Unmarshal(respBody, &data)
|
||
c.JSON(resp.StatusCode, data)
|
||
}
|
||
|
||
// VoiceStatus GET /api/v1/voice/status
|
||
// 代理请求到 voice-service 获取完整状态(STT + TTS)
|
||
func (h *VoiceHandler) VoiceStatus(c *gin.Context) {
|
||
url := strings.TrimRight(h.voiceServiceURL, "/") + "/api/v1/status"
|
||
|
||
resp, err := h.client.Get(url)
|
||
if err != nil {
|
||
logger.Printf("[voice] Voice-Service 不可达 (Status): %v", err)
|
||
c.JSON(http.StatusBadGateway, gin.H{
|
||
"error": "Voice-Service 不可达: " + err.Error(),
|
||
"errorType": "voice_service_unreachable",
|
||
})
|
||
return
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
respBody, _ := io.ReadAll(resp.Body)
|
||
|
||
var data interface{}
|
||
json.Unmarshal(respBody, &data)
|
||
c.JSON(resp.StatusCode, data)
|
||
}
|