依 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>
17 KiB
TDD v2 — visionA-local(Round-2 refactor 後)
作者:Architect Agent 版本:v2.1 日期:2026-04-14(v2.0 → v2.1 吸收 PM 審閱 + R5-D + R5-E) 狀態:Draft(v2.1 由 Architect 根據 PM 互審 + R5-D/E 新決策產出,待三方再次 cross-review + 使用者確認) 取代:
TDD.mdv1.0(2026-04-11,四輪修訂 + Plan B 補件) 決策來源:progress.md§「R5 第五輪使用者決策」+ 「R5-D 補充決策」+「R5-E 階段化啟動新指標」(2026-04-14)
0. 版本資訊與變更摘要
0.0 v2.0 → v2.1 差異速覽(2026-04-14 本次更新)
| # | 變更面向 | v2.0 | v2.1 | 觸發 |
|---|---|---|---|---|
| 1 | AC-1.3 啟動時間 | 10 秒硬指標 | 60 秒 + 6 階段進度顯示(20 s soft timeout / 60 s hard timeout) | R5-E1~E6 |
| 2 | 自動開瀏覽器行為 | 首次啟動開一次(autoOpenedThisSession flag 控制 per-session-once) |
每次 Start/Restart 成功都開(砍 flag;R5-D3) | R5-D3 |
| 3 | AutoOpenBrowser 預設值 | 三平台統一 true | macOS/Windows true、Linux false(分平台 default) | R5-D2 |
| 4 | Server 崩潰通知 | 僅控制台 Error banner | Error banner + OS 原生通知並存(notify.go 三平台實作) |
R5-D1 |
| 5 | Shutdown grace period | 5 s → 10 s(Architect Q4 提議) | 7 s + 1 s 顯示「停止中…」modal(PM Q4 定案) | PM Q4 |
| 6 | Shutdown race condition | 靠 polling 3 次失敗(0.5-15 s 後顯示 overlay) | WebSocket server:shutdown-imminent 廣播(秒內觸發 overlay) |
PM Minor 4 |
| 7 | Restart port 處理 | 允許 fallback(3721 → 3722 …) | 強制保留舊 port,被佔用進 Error state | Architect F-2 |
| 8 | Preferences 持久化 | 提及 atomic write 但未定位置 | <dataDir>/preferences.json + write-rename(完整 spec) |
PM §11-1 |
| 9 | videoIsURL field 處置 |
「視情況保留或刪除」 | 明確刪除(grep 證實是 dead code) | PM Minor 5 |
| 10 | idle RAM 450 MB 目標 | 未澄清範圍 | 澄清不含瀏覽器 tab(只計 Wails + Go server + Python) | PM §11-3 |
| 11 | 日常啟動時間估算 | 無 | 新增估算 ~3.8 s,符合 AC-2.1 ≤ 5 s | PM Minor 2 |
| 12 | boot-id 生成方式 | 建議 google/uuid | 用 crypto/rand 16 bytes → hex(不引依賴) |
Architect Q3 |
| 13 | navigator.language fallback | 基本 startsWith('zh') | 強化:處理 C/POSIX/空字串 + hardcoded 英文 fallback | Architect Q5 |
| 14 | milestone 數量 | M8-1 ~ M8-10(10 個) | +M8-4b 階段化啟動(11 個) | R5-E |
| 15 | 總工時估算 | ~10 人天 | ~12 人天(R5-E +1 天、R5-D1 +0.3 天、PM Q4 +0.2 天、Minor 4 +0.3 天 + 其他 0.2 天) | 以上 |
| 16 | 新增檔案 | — | v2/startup-pipeline.md(R5-E 實作細節) |
R5-E |
未變更(v2.0 其餘設計保持不動):Wails 視窗 = 控制台 UI、瀏覽器 tab 載業務、yt-dlp/Mock 砍除、ffmpeg LGPL 方案 B、CORS 白名單、boot-id 機制、LogBuffer 2000 行 ring buffer、state machine 5 個狀態。
0.1 v1 → v2 差異速覽
| 面向 | v1(2026-04-11) | v2(2026-04-14) | 觸發決策 |
|---|---|---|---|
| Wails 視窗載入內容 | Splash → window.location.replace 跳到 Next.js 主 UI(M7-B) |
Splash 路徑完全移除,Wails 永遠停在「控制台 UI」:server 狀態 / log panel / 啟停控制 / Open in Browser | R5-1 A+B+G |
| 使用者業務 UI 承載者 | Wails WebView(M7-B 是純 HTTP,但仍在 WebView 內) | 瀏覽器 tab(Chrome / Safari / Edge)http://127.0.0.1:<port>/ |
R5-1 |
| yt-dlp 全套 | 保留(M6 vendor 35 MB + handler + 前端 URL tab) | 完全砍除(vendor + handler + frontend UI + i18n + bootstrap + installer payload) | R5-7 前置;M8-1 |
| Mock 模式 | 保留(--mock flag + VISIONA_MOCK env + UI hint + 兩份 i18n) |
完全砍除(Go mock driver / mock camera / env var / flag / UI 元件 / i18n keys) | R5-5a |
| ffmpeg 授權 | GPL(evermeet / BtbN / johnvansickle),VISIONA_ALLOW_GPL_FFMPEG=1 release blocker |
LGPL 方案 B(混合):Win/Linux 換 BtbN LGPL;macOS 自 build decoder-only ~20 MB,binary commit 到 vendor/ffmpeg/macos/ |
R5-6 / R5-6a / R5-6b |
| ffprobe | 未打包 | 三平台都一起包(ffmpeg + ffprobe) | R5-6c |
| Tray | 第三輪 Q-A 砍掉 | 維持砍,不復議 | R5-3 |
| 關閉視窗行為 | Q7=B 傳統式(關 = 結束 app) | 維持 Q7=B,但新增:關閉前先 StopServer() 優雅結束;瀏覽器 tab 偵測 server 離線後顯示全域 Offline Overlay |
R5-2 |
| Server 控制 bindings | 只有 GetServerStatus / GetServerURL / OpenBrowser(隱式 start) |
補齊 StartServer / StopServer / RestartServer / GetRecentLogs / ClearLogs / GetSystemInfo 及完整 state machine |
R5-1 |
| watchServer 失敗行為 | 3 次失敗 → reportFatal + os.Exit(1)(app 一起死) |
3 次失敗 → 切 ServerStateError,Wails app 保留讓使用者手動 Restart 或查 log |
三方共識 #10 |
| 自動開瀏覽器 | — | 首次 server 就緒自動開一次;Settings 的 openBrowserOnStart 可關 |
R5-4 |
| CORS 政策 | 寬鬆(Access-Control-Allow-Origin: <任意 Origin>) |
嚴格 whitelist http://127.0.0.1:* + http://localhost:*;其他 Origin → 不回 ACAO header / OPTIONS 405 |
三方共識 #5 |
| 綁定 interface | --host 127.0.0.1 |
維持不動(不做 LAN) | R5-1 |
| 上傳影片副檔名 | .mp4 / .avi / .mov |
.mp4 / .avi / .mov / .mpeg / .mpg(瀏覽器能吃 + Kneron pipeline 吃得到的交集) |
三方共識 #11 |
| Boot-ID 機制 | 無 | 新增 GET /api/system/boot-id,server 啟動時產生 UUID;瀏覽器每 5 s poll,boot-id 變更 → force reload |
三方共識 #14 |
| 控制台 UI 技術選型 | — | vanilla HTML/JS/CSS,從現有 visiona-local/frontend/ splash 改寫 |
三方共識 #7 |
| 沿用率 | — | 85-95%(詳見 v2/code-reuse-v2.md) |
三方共識 #1 |
| 總工時 | — | ~10 人天,拆成 10 個 milestone(詳見 v2/milestone-plan.md) |
三方共識 #1 |
0.2 v1 未變的決策(v2 繼續沿用)
- 三層程序模型(Wails 殼 + Go server 子行程 + Python sidecar)— D1
- Python runtime 雙策略(bundled / system / auto)— D2
- 完全放棄程式碼簽章(macOS ad-hoc、Windows 無 Authenticode、Linux 無簽章)— D3
- x86_64 only,三平台都不做 ARM — D4
- 預置模型全部打包(~73 MB),不做 auto-update、不收 telemetry — D5
- 資料目錄位置、single-instance lock、舊資料目錄遷移、IPC raise 機制全部保留
- 首次安裝 ≤ 5 分鐘、首次推論 ≤ 30 s / 回訪 15 s 等 NFR 目標不變
- Ubuntu 與 Windows 打包流程(AppImage / Inno Setup)不變
- 中英雙語(前端)機制不變,控制台 UI 使用同一份
en-US/zh-TWJSON(詳見v2/control-panel.md)
1. 新架構總覽
1.1 三層進程模型
Wails 殼(桌面控制台 UI)
└─spawn─▶ Go server 子行程(Gin HTTP + WebSocket on 127.0.0.1:random_port)
├─spawn─▶ python3 kneron_bridge.py(KneronPLUS SDK,sidecar)
└─spawn─▶ ffmpeg / ffprobe(on-demand 解碼)
Wails 殼 ←── IPC / Wails bindings ── Wails 視窗內的控制台 UI(vanilla HTML/JS/CSS)
Go server ←── HTTP / WebSocket over loopback ── 瀏覽器 tab(Next.js Web UI,業務操作全在這裡)
關鍵差別於 v1:Wails 視窗不載入業務 UI,它是獨立的控制台(status / log / start-stop / open-in-browser / preferences)。業務 UI 在瀏覽器 tab 跑 http://127.0.0.1:<port>/ 的 Next.js SPA。
完整 ASCII 架構圖詳見:v2/control-panel.md §3(控制台 UI wireframe)、architect-analysis-round2-refactor.md §A1(v1→v2 資料流對照)。
1.2 ServerController State Machine
五個狀態:Stopped / Starting / Running / Stopping / Error。轉換由 ServerController.txMu + mu 雙 mutex 保護,不可跳過中間狀態。
- Start:
Stopped|Error → Starting → Running(失敗走 Error) - Stop:
Running → Stopping → Stopped - Restart:
Running → Stopping → Stopped → Starting → Running - watchServer 3 次失敗:
Running → Error(v1 是os.Exit,v2 改為 Error state 讓使用者手動復原)
完整細節見 v2/server-lifecycle.md §5。
1.3 資料流摘要
| 情境 | 簡述 |
|---|---|
| 冷啟動 + R5-4 自動開瀏覽器 | Wails OnStartup → 常規 seed / lock / IPC → ServerController.Start() → spawn server + logPump × 2 → 健康檢查 → state = Running → 若 openBrowserOnStart 且本 session 首次 → OpenInBrowser("") |
| Log 推送到控制台 | server stdout/stderr → cmd.StdoutPipe/StderrPipe → logPump goroutine(bufio scanner + 10 ms micro-batch)→ 同時 (1) 寫 logs/server.{stdout,stderr}.log + (2) append 到 LogBuffer(ring 2000 行)+ (3) EventsEmit("log:append", []LogLine) → Wails JS 訂閱 EventsOn('log:append', ...) → log panel 增量 render |
| Restart 期間瀏覽器 tab 自動重連 | 使用者按 Restart → Stop → Start → server 新 boot-id → 瀏覽器 polling /api/system/boot-id(5 s interval,Page Visibility API)偵測到 id 變 → window.location.reload() |
| 關 Wails 視窗 (R5-2) | OnBeforeClose return false → OnShutdown → watchCancel → ServerController.Stop()(SIGTERM → 10 s → SIGKILL)→ releaseLock → Wails quit。瀏覽器 tab 的 polling 連續 3 次失敗(15 s)→ <ServerOfflineOverlay> 顯示「Server 已離線」 |
詳細時序與 Go 實作見 v2/server-lifecycle.md §2-9;瀏覽器端實作見 v2/web-ui-offline-overlay.md。
2. 子檔案地圖
| # | 子檔 | 目的 | 對應 R5 決策 / M8 milestone |
|---|---|---|---|
| 2.1 | v2/control-panel.md |
Wails 控制台 UI + Go App bindings + LogBuffer + log pump + 狀態機 + Preferences(R5-D2/D3)+ OS 通知觸發點 | R5-1, R5-5, R5-D1/D2/D3, R5-E;M8-4, M8-4b, M8-5 |
| 2.2 | v2/ffmpeg-lgpl.md |
三平台 LGPL ffmpeg vendor 策略(Makefile patch + macOS build script + 授權檔管理) | R5-6, R5-6a, R5-6b, R5-6c;M8-3 |
| 2.3 | v2/server-lifecycle.md |
Server 生命週期細節:state machine、port 分配(F-2 強制保留)、pipe 捕捉、7+1 秒 graceful shutdown、boot-id、OS 通知(§10)、Preferences 持久化(§11) | R5-2, R5-4, R5-D1, PM Q4, F-2, PM §11-1/11-3;M8-4, M8-9 |
| 2.4 | v2/cors-security.md |
CORS whitelist middleware、WS origin check、資料驗證邊界 | 三方共識 #5;M8-8 |
| 2.5 | v2/deletions.md |
yt-dlp / Mock 模式全清單(檔案 / 函式 / 行號 / i18n key / vendor / installer);v2.1 修正 videoIsURL / NewVideoSourceFromURL 明確刪除 |
R5-5a, R5-7, PM Minor 5;M8-1, M8-2 |
| 2.6 | v2/web-ui-offline-overlay.md |
瀏覽器 tab 的 <ServerOfflineOverlay> 實作:polling + WebSocket shutdown-imminent 雙管道、重試、SSR 相容 |
R5-2 三方共識 #14, PM Minor 4;M8-7 |
| 2.7 | v2/milestone-plan.md |
M8-1 ~ M8-10 + M8-4b 階段化啟動;總工時 ~12 人天 | 整體;M8-* |
| 2.8 | v2/code-reuse-v2.md |
逐模組沿用 / 改寫 / 新寫比例表 | 整體 |
| 2.9 | v2/startup-pipeline.md |
v2.1 新增:R5-E 6 階段啟動管線實作(event schema、StartupPipeline struct、watcher goroutine、60 s hard timeout / 20 s soft timeout、前端 startup-panel.js) | R5-E1~E6;M8-4b |
3. 風險清單(v2 更新)
繼承 v1 risks-and-mitigations.md 全部風險,以下為 v2 新增或升級的條目:
| # | 風險 | 等級 | 新增/升級 | 緩解 |
|---|---|---|---|---|
| R-v2-1 | M7-B M1 驗收漏看 Wails 視窗 的教訓 — v2 控制台是全新 UI,重複踩同樣坑的風險 | 🟠 中 | 新增 | M8-10 驗收 checklist 強制三個檢查:(1) 雙擊 .app / .exe / .AppImage 打開後 Wails 視窗顯示的是控制台 UI(不是 splash / wizard / 白畫面);(2) 點 Open in Browser 後瀏覽器確實載入 Next.js;(3) 點 Stop 後瀏覽器 tab 能看到 Offline Overlay。每個平台都要做 |
| R-v2-2 | macOS 自 build ffmpeg 的可重現性 — LGPL 合規稽核時必須能證明 vendor/ffmpeg/macos/ffmpeg 是我們在特定 configure flags 下從特定 ffmpeg commit build 出來的 |
🔴 高 | 新增 | vendor/ffmpeg/macos/BUILD.md 必須記錄:ffmpeg release tag(如 n7.1)、source tarball sha256、完整 ./configure line、build host(macOS version + Xcode CLT version)、build date、binary sha256。未來升級 ffmpeg 時要重跑並更新 BUILD.md,不能「手改一下再傳」。同步把 configure flags 寫進 Makefile 的 vendor-ffmpeg-macos-build target(而非埋在 BUILD.md 內)讓其可 reproduce |
| R-v2-3 | Wails EventsEmit 在高頻 stdout 下丟事件或延遲 — server boot 時 Gin / logger 一次可能噴 200+ 行;推論 frame log 若誤進 stdout 會產生秒級 30-100 行 | 🟠 中 | 新增(v1 F-4 升級) | (1) logPump 加 micro-batch:緩存 10 ms window 內的行,一次 emit 一個 log:append event(payload 為陣列);(2) server 推論 frame 狀態禁止用 logger.Info,改用 debug level(line-rate 測試會檢查此條);(3) 若 LogBuffer 滿 > 80% 時 logPump 降為只寫檔不 emit event(控制台看到「…(skipped N events)…」提示,使用者可 Clear Logs)。實作細節見 v2/control-panel.md §4 |
| R-v2-4 | Wails 關閉視窗 → StopServer 過程中瀏覽器 tab 的 race condition — 使用者按 × → Wails OnBeforeClose → ServerController.Stop() → SIGTERM → wait → SIGKILL → Wails 退出,整段 ~0.5-5s 內瀏覽器 tab 的 polling 可能看到 ECONNREFUSED 但 Overlay 還沒觸發(需連續 3 次失敗,15 s 才顯示) | 🟡 低 | 新增 | 實務上:使用者關了 Wails 視窗通常也會關瀏覽器 tab,race 不構成實際問題。若使用者真的沒關瀏覽器,15 s 後 Overlay 會出現,使用者看到「Server 已離線」訊息即可理解。不做額外優化(例:Wails 關閉前主動告知瀏覽器 — 需要 Wails → Browser 的 push channel,成本太高) |
| R-v2-5 | macOS 自 build 的 ffmpeg 需要 codesign — Gatekeeper 會擋未簽章的執行檔 | 🟠 中 | 新增 | (1) 在 macOS build script 最後做 codesign --force --sign - ...(ad-hoc sign);(2) wails-macos target 的 codesign --force --deep --sign - visiona-local.app 已覆蓋 Resources/bin 下的執行檔,沿用即可;(3) 驗收時用 spctl --assess --verbose vendor/ffmpeg/macos/ffmpeg 確認不會被 Gatekeeper 拒絕 |
| R-v2-6 | boot-id polling 對瀏覽器 tab 休眠的影響 — Chrome 會把背景 tab 的 setInterval 降頻到 1 次/分鐘,可能讓使用者切回 tab 時 60 s 才偵測到 server 重啟 | 🟡 低 | 新增 | 用 Page Visibility API:tab visible → 5 s interval;tab hidden → 停 polling;tab 再次 visible → 立即 probe 一次再恢復 5 s interval。實作細節見 v2/web-ui-offline-overlay.md §3 |
| R-v2-7 | 砍 Mock 後空白 UI 體驗 — 使用者第一次打開沒插硬體時,Devices 頁會是空的 | 🟡 低 | 新增 | (1) Devices 頁顯示友善 empty state:「未偵測到 Kneron 裝置。請連接 KL520/KL720 後按『掃描』。」附安裝 driver 按鈕(Windows);(2) 這是 R5-5a 明示接受的結果,PRD v2 也會記錄為預期行為 |
v1 已列、v2 解除的風險:
- ffmpeg GPL release blocker(F-5, R9) — R5-6 LGPL 方案 B 解除
- F-1 Wails tray 在 Linux GNOME 可能壞掉 — R5-3 維持砍 tray,風險消失
- F-3 使用者找不回 app — R5-2 維持關閉 = 結束,風險消失
4. 審閱紀錄
| 日期 | 審閱者 | 結論 |
|---|---|---|
| 2026-04-14 | Architect Agent | v2.0 Draft 產出 |
| 2026-04-14 | PM Agent | v2.0 互審完成(見 reviews/pm-review-of-tdd-v2.md)— Major × 4 / Minor × 5 |
| 2026-04-14 | Architect Agent | v2.1 產出:吸收 PM Major/Minor 修正 + R5-D 三條補充決策 + R5-E 階段化啟動 + Architect 自補清單(F-2 port 保留 / B-1 OS 通知 / Q1/Q3/Q5/Q7 自決)。新增 v2/startup-pipeline.md;總工時 ~10 → ~12 人天 |
| 2026-04-14 | PM Agent | v2.1 待再次審閱(確認 Major × 4 都已落地 + R5-D/E 理解一致) |
| 2026-04-14 | Design Agent | v2.1 待審(重點:R5-E5 啟動階段文案、7+1 秒 stopping modal 文案、startup-panel.js 視覺對齊 Design Spec v2.1) |
| 2026-04-14 | 使用者 | 待確認 |