87214b9441
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>
180 lines
5.1 KiB
Go
180 lines
5.1 KiB
Go
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)
|
||
}
|