依 R5 五輪決策把 visionA-local 從「Wails 內嵌 Next.js」重構為「Wails
本機伺服器控制台 + 瀏覽器 Web UI」模式(類比 Docker Desktop / Ollama)。
程式碼變動
- M8-1 砍 yt-dlp 全套(後端 resolver / URL handler / 前端 URL tab /
Makefile vendor / installer / bootstrap / CI workflow,-555 行)
- M8-2 砍 Mock 模式全套(driver/mock、mock_camera、Settings runtimeMode、
VISIONA_MOCK 環境變數,-528 行)
- M8-3 ffmpeg 從 GPL 切換到 LGPL 混合方案:Windows/Linux 用 BtbN 現成
LGPL binary,macOS 自 build minimal decoder-only 進 git
(vendor/ffmpeg/macos/ffmpeg 5.7MB + ffprobe 5.6MB,比 GPL 版省 85% 空間)
- M8-4 Wails Server Controller:state machine、log ring buffer 2000 行、
preferences.json atomic write、boot-id、Gin SkipPaths、shutdown 7+1 秒、
notify_*.go 三平台 OS 通知、watchServer 改 Error state 不 os.Exit
- M8-4b 啟動階段管線 R5-E:6 階段進度 event、20s soft / 60s hard timeout、
stage 5/6 skip 規則、sentinel file、RestartStartupSequence 5 步驟
- M8-5 Wails 控制台 vanilla HTML/JS/CSS(9 檔 ~2012 行)取代 M7-B splash:
state 視覺、log panel、startup progress panel、Stage 6 manual CTA
pulse、shutdown modal、Settings、Dark Mode、i18n 中英雙語
- M8-6 上傳影片副檔名擴充(mp4/avi/mov/mpeg/mpg)
- M8-7 Web UI Server Offline Overlay(role=alertdialog + focus trap +
wsEverConnected 容錯 + Page Visibility)
- M8-8 CORS middleware(127.0.0.1/localhost only + suffix attack 防護)+
ws/origin.go 獨立 WebSocket CheckOrigin 避 package cycle
- MAJ-4 server:shutdown-imminent WebSocket broadcast 機制
(/ws/system endpoint + notifyShutdownImminent helper)
- M8-9 Boot-ID + 瀏覽器 tab 自動重連(sessionStorage loop guard)
品質
- ~105+ 新 unit test + race detector (-count=2) 全綠
- 10 個 milestone 全部通過 Reviewer 審查
- 三方 v2 + v2.1 文件(PRD / Design Spec / TDD)+ 交叉互審紀錄
收錄在 .autoflow/
交付前待處理(M8-10)
- 重跑 make payload-macos 把舊 GPL 77MB binary 換成新 LGPL
- 三平台 end-to-end build 驗證
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
84 lines
2.2 KiB
Go
84 lines
2.2 KiB
Go
package ws
|
||
|
||
// system_ws_integration_test.go — MAJ-4 補丁:/ws/system 整合 smoke test
|
||
//
|
||
// 啟一個 httptest server 掛 SystemEventsHandler,真的用 gorilla WebSocket client
|
||
// 連進去,然後呼叫 hub.BroadcastToRoom("system", ...),驗證 client 收到訊息。
|
||
//
|
||
// 這個測試取代外部 websocat / wscat 的需求,讓 smoke test 在 CI 就能跑。
|
||
|
||
import (
|
||
"encoding/json"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/gorilla/websocket"
|
||
)
|
||
|
||
func TestSystemEventsHandler_ReceivesBroadcast(t *testing.T) {
|
||
gin.SetMode(gin.TestMode)
|
||
|
||
hub := NewHub()
|
||
go hub.Run()
|
||
|
||
r := gin.New()
|
||
r.GET("/ws/system", SystemEventsHandler(hub))
|
||
|
||
srv := httptest.NewServer(r)
|
||
defer srv.Close()
|
||
|
||
wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "/ws/system"
|
||
dialer := websocket.DefaultDialer
|
||
conn, _, err := dialer.Dial(wsURL, http.Header{})
|
||
if err != nil {
|
||
t.Fatalf("dial: %v", err)
|
||
}
|
||
defer conn.Close()
|
||
|
||
// 給 Hub.register channel 一點時間處理 client 加入 room
|
||
// (RegisterSync 會同步等完,但我們這邊是透過 HTTP upgrade 流程,
|
||
// client 先連上後才 RegisterSync — 需要等 handler 執行到那一行)
|
||
// 改用 poll:持續廣播直到收到或 timeout。
|
||
_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
|
||
|
||
// 等 hub 吸收 Register(最多 500 ms)
|
||
deadline := time.Now().Add(500 * time.Millisecond)
|
||
for time.Now().Before(deadline) {
|
||
hub.mu.RLock()
|
||
n := len(hub.rooms["system"])
|
||
hub.mu.RUnlock()
|
||
if n > 0 {
|
||
break
|
||
}
|
||
time.Sleep(10 * time.Millisecond)
|
||
}
|
||
|
||
// 廣播 shutdown-imminent
|
||
hub.BroadcastToRoom("system", map[string]interface{}{
|
||
"type": "server:shutdown-imminent",
|
||
"reason": "quit",
|
||
"ts": time.Now().UnixMilli(),
|
||
})
|
||
|
||
// Read 第一則訊息
|
||
_, data, err := conn.ReadMessage()
|
||
if err != nil {
|
||
t.Fatalf("read: %v", err)
|
||
}
|
||
|
||
var got map[string]interface{}
|
||
if err := json.Unmarshal(data, &got); err != nil {
|
||
t.Fatalf("json: %v; raw=%s", err, string(data))
|
||
}
|
||
if got["type"] != "server:shutdown-imminent" {
|
||
t.Errorf("wrong type: %+v", got)
|
||
}
|
||
if got["reason"] != "quit" {
|
||
t.Errorf("wrong reason: %+v", got)
|
||
}
|
||
}
|