bcf4d4e621
W1-W14 全部完成: - W1: 消息搜索 (ILIKE全文检索 + SearchModal) - W2: 对话导出 (JSON/Markdown/TXT三格式) - W3: 记忆时间线 DevTools 可视化 - W4: 通知推送系统 (WebSocket + Browser Notification API) - W5: 定时提醒 (30s轮询 + 重复提醒 + WebSocket推送) - W6: 每日简报 (08:00自动生成: 天气+新闻+提醒+AI摘要) - W7: IoT场景自动化 (规则引擎 10s轮询 + 条件评估 + 场景执行) - W8: 语音输入 (浏览器 Speech Recognition API) - W9: STT服务 (voice-service + whisper.cpp) - W10: TTS服务 (浏览器 Speech Synthesis + edge-tts三档回退) - W11: 文件管理 (上传/下载/缩略图/纯Go bilinear缩放) - W12: 知识库RAG (PostgreSQL tsvector + 文档分块 + 检索) - W13: 多模态 (图片上传+分析: Vision API + 本地Go分析回退) - W14: PWA (Service Worker + 离线页 + install prompt) 总计: 6个Go微服务 + 10+前端组件 + 10+ PostgreSQL表 + 4个后台调度器
180 lines
5.0 KiB
Go
180 lines
5.0 KiB
Go
package handler
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"io"
|
||
"log"
|
||
"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 {
|
||
log.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 {
|
||
log.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 {
|
||
log.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 {
|
||
log.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 {
|
||
log.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)
|
||
}
|