refactor: 认证系统重构 + DevTools CLI 重写 + 文档全面更新

- auth: Login 简化为管理员始终通过 .env 验证,GetProfile 修正 admin DB 查询
- devtools: .sh/.bat 同步重写为完整 CLI (start/stop/status/logs/build/db:*)
- docs: 新增 devtools.md,重写 Deploy.md (三种方式+Windows说明),更新 README/gateway-api
- voice-service: DashScope 实时流式 STT 支持
- gateway: Phase 6 多模型配置 + 多端客户端管理 + WebSocket 增强

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 14:55:47 +08:00
parent 83e94d9e97
commit 7eb5e984c2
18 changed files with 2405 additions and 677 deletions
@@ -4,11 +4,12 @@ import (
"bufio"
"bytes"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/yourname/cyrene-ai/pkg/logger"
"io"
"mime/multipart"
"net/http"
"strings"
"time"
@@ -19,6 +20,7 @@ import (
"github.com/yourname/cyrene-ai/gateway/internal/config"
"github.com/yourname/cyrene-ai/gateway/internal/store"
"github.com/yourname/cyrene-ai/gateway/internal/ws"
"github.com/yourname/cyrene-ai/pkg/logger"
)
// ChatHandler 聊天处理器
@@ -422,6 +424,8 @@ func (h *ChatHandler) streamResponse(client *ws.Client, mode string, reqBody []b
Type: "stream_end",
MessageID: msgID,
SessionID: client.SessionID,
Content: fullText,
Text: fullText,
Timestamp: time.Now().UnixMilli(),
})
@@ -458,14 +462,145 @@ func (h *ChatHandler) streamResponse(client *ws.Client, mode string, reqBody []b
// handleVoiceInput 处理语音输入
func (h *ChatHandler) handleVoiceInput(client *ws.Client, msg ws.ClientMessage) {
// MVP阶段:返回提示
response := ws.ServerMessage{
Type: "error",
MessageID: "msg_" + generateID(),
Error: "语音处理功能将在后续版本中启用",
Timestamp: time.Now().UnixMilli(),
audioB64 := msg.AudioData
if audioB64 == "" {
client.SendMessage(ws.ServerMessage{
Type: "error",
MessageID: "msg_" + generateID(),
Error: "语音数据为空",
Timestamp: time.Now().UnixMilli(),
})
return
}
client.SendMessage(response)
format := msg.Mode
if format == "" {
format = "webm"
}
// 在 goroutine 中处理转录,避免阻塞 ReadPump
go func() {
text, err := h.transcribeAudio(audioB64, format)
if err != nil {
logger.Printf("[voice] 转录失败: %v", err)
client.SendMessage(ws.ServerMessage{
Type: "voice_transcript",
MessageID: "msg_" + generateID(),
Error: fmt.Sprintf("语音识别失败: %v", err),
Timestamp: time.Now().UnixMilli(),
})
return
}
if text == "" {
client.SendMessage(ws.ServerMessage{
Type: "voice_transcript",
MessageID: "msg_" + generateID(),
Text: "",
Timestamp: time.Now().UnixMilli(),
})
return
}
// 发送转录结果给前端
client.SendMessage(ws.ServerMessage{
Type: "voice_transcript",
MessageID: "msg_" + generateID(),
Text: text,
Timestamp: time.Now().UnixMilli(),
})
// 将转录文本作为聊天消息处理
chatMsg := ws.ClientMessage{
Type: "message",
Content: text,
Mode: msg.Mode,
}
h.handleChatMessage(client, chatMsg)
}()
}
// transcribeAudio 将 base64 编码的音频发送到 voice-service 进行转录。
func (h *ChatHandler) transcribeAudio(audioB64 string, format string) (string, error) {
audioData, err := decodeBase64(audioB64)
if err != nil {
return "", fmt.Errorf("解码音频数据失败: %w", err)
}
// 构建 multipart form
var buf bytes.Buffer
mw := multipart.NewWriter(&buf)
ext := ".webm"
switch format {
case "wav", "wave":
ext = ".wav"
case "mp3", "mpeg":
ext = ".mp3"
case "ogg", "opus":
ext = ".ogg"
case "pcm":
ext = ".pcm"
}
fw, err := mw.CreateFormFile("audio", "recording"+ext)
if err != nil {
return "", fmt.Errorf("创建表单字段失败: %w", err)
}
if _, err := fw.Write(audioData); err != nil {
return "", fmt.Errorf("写入音频数据失败: %w", err)
}
mw.Close()
voiceURL := h.cfg.VoiceServiceURL
if voiceURL == "" {
voiceURL = "http://localhost:8093"
}
httpReq, err := http.NewRequest("POST", voiceURL+"/api/v1/transcribe", &buf)
if err != nil {
return "", fmt.Errorf("创建请求失败: %w", err)
}
httpReq.Header.Set("Content-Type", mw.FormDataContentType())
httpClient := &http.Client{Timeout: 60 * time.Second}
resp, err := httpClient.Do(httpReq)
if err != nil {
return "", fmt.Errorf("voice-service 调用失败: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("读取响应失败: %w", err)
}
var result struct {
Success bool `json:"success"`
Text string `json:"text"`
Error string `json:"error"`
}
if err := json.Unmarshal(respBody, &result); err != nil {
return "", fmt.Errorf("解析响应失败: %w", err)
}
if !result.Success {
if result.Error != "" {
return "", fmt.Errorf("%s", result.Error)
}
return "", fmt.Errorf("转录返回空结果")
}
return result.Text, nil
}
// decodeBase64 解码 base64 字符串(支持 Data URL 前缀)。
func decodeBase64(s string) ([]byte, error) {
// 移除 data:xxx;base64, 前缀
if idx := strings.Index(s, ","); idx != -1 {
s = s[idx+1:]
}
return base64.StdEncoding.DecodeString(s)
}
// handleHistoryRequest 处理历史消息请求