feat: 第五轮开发 - 14项未来路线图功能完整实现

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个后台调度器
This commit is contained in:
2026-05-19 12:01:09 +08:00
parent 78e3f450c2
commit bcf4d4e621
69 changed files with 14599 additions and 150 deletions
@@ -0,0 +1,179 @@
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)
}