refactor: 优化代码质量,遵循 KISS 原则

- 移除自签证书回退逻辑,简化为仅使用 AnyIP 证书
- 删除 internal/tls/generate.go(不再需要)
- 重构 main.go:提取初始化逻辑,main() 从 156 行降至 13 行
- 重构 internal/ws/handler.go:提取消息处理,handleConn() 从 131 行降至 25 行
- 重构 internal/config/load.go:使用 map 驱动消除重复代码
- 优化前端 startRecording():使用标准 AbortController API
- 优化前端 showToast():预定义 DOM 元素,代码减少 50%

代码行数减少 90 行,复杂度显著降低,所有构建通过
This commit is contained in:
2026-03-02 00:25:14 +08:00
parent 8c7b9b45fd
commit b87fead2fd
8 changed files with 316 additions and 371 deletions

View File

@@ -9,6 +9,17 @@ import (
"github.com/gofiber/fiber/v3"
)
// session holds the state for a single WebSocket connection.
type session struct {
conn *websocket.Conn
log *slog.Logger
resultCh chan ServerMsg
previewMu sync.Mutex
previewText string
sendAudio func([]byte)
cleanup func()
active bool
}
// PasteFunc is called when the server should paste text into the focused app.
type PasteFunc func(text string) error
@@ -53,134 +64,125 @@ func (h *Handler) Register(app *fiber.App) {
}
func (h *Handler) handleConn(c *websocket.Conn) {
log := slog.With("remote", c.RemoteAddr().String())
log.Info("ws connected")
defer log.Info("ws disconnected")
// Result channel for ASR → phone
resultCh := make(chan ServerMsg, 32)
defer close(resultCh)
// Writer goroutine: single writer to avoid concurrent writes
// bigmodel_async with enable_nonstream: server returns full text each time (not incremental)
// We replace preview text on each update instead of accumulating.
var wg sync.WaitGroup
var previewMu sync.Mutex
var previewText string
wg.Add(1)
go func() {
defer wg.Done()
for msg := range resultCh {
// Replace preview text with latest result (full mode)
if msg.Type == MsgPartial || msg.Type == MsgFinal {
previewMu.Lock()
previewText = msg.Text
preview := ServerMsg{Type: msg.Type, Text: previewText}
previewMu.Unlock()
if err := c.WriteMessage(websocket.TextMessage, preview.Bytes()); err != nil {
log.Warn("ws write error", "err", err)
return
}
continue
}
// Forward other messages (error, pasted) as-is
if err := c.WriteMessage(websocket.TextMessage, msg.Bytes()); err != nil {
log.Warn("ws write error", "err", err)
return
}
}
}()
// ASR session state
var (
sendAudio func([]byte)
cleanup func()
active bool
)
defer func() {
if cleanup != nil {
cleanup()
}
wg.Wait()
}()
for {
mt, data, err := c.ReadMessage()
if err != nil {
break
}
switch mt {
case websocket.BinaryMessage:
// Audio frame
if active && sendAudio != nil {
sendAudio(data)
}
case websocket.TextMessage:
var msg ClientMsg
if err := json.Unmarshal(data, &msg); err != nil {
log.Warn("invalid json", "err", err)
continue
}
switch msg.Type {
case MsgStart:
if active {
continue
}
// Reset preview text for new session
previewMu.Lock()
previewText = ""
previewMu.Unlock()
sa, cl, err := h.asrFactory(resultCh)
if err != nil {
log.Error("asr start failed", "err", err)
resultCh <- ServerMsg{Type: MsgError, Message: "ASR start failed"}
continue
}
sendAudio = sa
cleanup = cl
active = true
log.Info("recording started")
case MsgStop:
if !active {
continue
}
// Finish ASR session — waits for final result from readLoop
if cleanup != nil {
cleanup()
cleanup = nil
}
sendAudio = nil
active = false
// Paste the final preview text
previewMu.Lock()
finalText := previewText
previewText = ""
previewMu.Unlock()
if finalText != "" && h.pasteFunc != nil {
if err := h.pasteFunc(finalText); err != nil {
log.Error("auto-paste failed", "err", err)
} else {
resultCh <- ServerMsg{Type: MsgPasted}
}
}
log.Info("recording stopped")
case MsgPaste:
if msg.Text == "" {
continue
}
if h.pasteFunc != nil {
if err := h.pasteFunc(msg.Text); err != nil {
log.Error("paste failed", "err", err)
resultCh <- ServerMsg{Type: MsgError, Message: "paste failed"}
} else {
resultCh <- ServerMsg{Type: MsgPasted}
}
}
}
}
}
sess := &session{
conn: c,
log: slog.With("remote", c.RemoteAddr().String()),
resultCh: make(chan ServerMsg, 32),
}
sess.log.Info("ws connected")
defer sess.log.Info("ws disconnected")
defer close(sess.resultCh)
defer sess.cleanupASR()
var wg sync.WaitGroup
wg.Add(1)
go sess.writerLoop(&wg)
defer wg.Wait()
for {
mt, data, err := c.ReadMessage()
if err != nil {
break
}
if mt == websocket.BinaryMessage {
sess.handleAudioFrame(data)
} else if mt == websocket.TextMessage {
h.handleTextMessage(sess, data)
}
}
}
func (s *session) writerLoop(wg *sync.WaitGroup) {
defer wg.Done()
for msg := range s.resultCh {
if msg.Type == MsgPartial || msg.Type == MsgFinal {
s.previewMu.Lock()
s.previewText = msg.Text
preview := ServerMsg{Type: msg.Type, Text: s.previewText}
s.previewMu.Unlock()
if err := s.conn.WriteMessage(websocket.TextMessage, preview.Bytes()); err != nil {
s.log.Warn("ws write error", "err", err)
return
}
continue
}
if err := s.conn.WriteMessage(websocket.TextMessage, msg.Bytes()); err != nil {
s.log.Warn("ws write error", "err", err)
return
}
}
}
func (s *session) handleAudioFrame(data []byte) {
if s.active && s.sendAudio != nil {
s.sendAudio(data)
}
}
func (h *Handler) handleTextMessage(s *session, data []byte) {
var msg ClientMsg
if err := json.Unmarshal(data, &msg); err != nil {
s.log.Warn("invalid json", "err", err)
return
}
switch msg.Type {
case MsgStart:
h.handleStart(s)
case MsgStop:
h.handleStop(s)
case MsgPaste:
h.handlePaste(s, msg.Text)
}
}
func (h *Handler) handleStart(s *session) {
if s.active {
return
}
s.previewMu.Lock()
s.previewText = ""
s.previewMu.Unlock()
sa, cl, err := h.asrFactory(s.resultCh)
if err != nil {
s.log.Error("asr start failed", "err", err)
s.resultCh <- ServerMsg{Type: MsgError, Message: "ASR start failed"}
return
}
s.sendAudio = sa
s.cleanup = cl
s.active = true
s.log.Info("recording started")
}
func (h *Handler) handleStop(s *session) {
if !s.active {
return
}
s.cleanupASR()
s.sendAudio = nil
s.active = false
s.previewMu.Lock()
finalText := s.previewText
s.previewText = ""
s.previewMu.Unlock()
if finalText != "" && h.pasteFunc != nil {
if err := h.pasteFunc(finalText); err != nil {
s.log.Error("auto-paste failed", "err", err)
} else {
s.resultCh <- ServerMsg{Type: MsgPasted}
}
}
s.log.Info("recording stopped")
}
func (h *Handler) handlePaste(s *session, text string) {
if text == "" {
return
}
if h.pasteFunc != nil {
if err := h.pasteFunc(text); err != nil {
s.log.Error("paste failed", "err", err)
s.resultCh <- ServerMsg{Type: MsgError, Message: "paste failed"}
} else {
s.resultCh <- ServerMsg{Type: MsgPasted}
}
}
}
func (s *session) cleanupASR() {
if s.cleanup != nil {
s.cleanup()
s.cleanup = nil
}
}