Files
Cyrene/backend/gateway/internal/handler/voice_handler.go
T
AskaEth 87214b9441 feat: Phase 1+2 架构进化 — 连续思考链/主动消息决策/情感状态机/离线自主思考 (86文件)
Phase 1 (基础设施):
- ThinkChain 思考链连续性 + 差异化思考提示词 (persistent)
- AutonomousToolPolicy 工具安全策略 (safe/unsafe/conditional)
- MessageScheduler 自适应消息节奏 (Idle/Available/Busy)
- SessionEnrichmentStore 渐进式上下文丰富 (5层)
- ConversationBus 事件总线 + ResponseCache (dedup)
- pkg/logger 统一日志 + 所有 handler 替换 fmt.Printf
- NPE 守卫/链路优化/数据库表修复/Go workspace

Phase 2 (人格交互):
- EmotionState/EmotionTracker 情感状态机 (5种心情, 情绪衰减)
- ProactiveGuard 主动消息多维决策 (静默时段/紧急度/频率/校验)
- Gateway↔ai-core 在线状态感知链路 (presence notification)
- 离线思考频率控制 + 重连问候 + 离线消息排队

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 15:25:12 +08:00

180 lines
5.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"bytes"
"encoding/json"
"io"
"github.com/yourname/cyrene-ai/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)
}