jim800121chen 8cd5751ce3 feat(local-tool): M8 重構 — Wails 控制台 + 瀏覽器 Web UI(R5 決策)
依 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>
2026-04-15 17:57:54 +08:00

163 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# TDD v2 — visionA-localRound-2 refactor 後)
> 作者Architect Agent
> 版本:**v2.1**
> 日期2026-04-14v2.0 → v2.1 吸收 PM 審閱 + R5-D + R5-E
> 狀態Draftv2.1 由 Architect 根據 PM 互審 + R5-D/E 新決策產出,待三方再次 cross-review + 使用者確認)
> 取代:`TDD.md` v1.02026-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 成功都開**(砍 flagR5-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 sArchitect 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 處理** | 允許 fallback3721 → 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-1010 個)| **+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 差異速覽
| 面向 | v12026-04-11 | v22026-04-14 | 觸發決策 |
|------|-----------------|-----------------|---------|
| **Wails 視窗載入內容** | Splash → `window.location.replace` 跳到 Next.js 主 UIM7-B| Splash 路徑完全移除Wails 永遠停在「控制台 UI」server 狀態 / log panel / 啟停控制 / Open in Browser | R5-1 A+B+G |
| **使用者業務 UI 承載者** | Wails WebViewM7-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 授權** | GPLevermeet / BtbN / johnvansickle`VISIONA_ALLOW_GPL_FFMPEG=1` release blocker | **LGPL 方案 B混合**Win/Linux 換 BtbN LGPLmacOS 自 build decoder-only ~20 MBbinary 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 pollboot-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-TW` JSON詳見 `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.pyKneronPLUS SDKsidecar
└─spawn─▶ ffmpeg / ffprobeon-demand 解碼)
Wails 殼 ←── IPC / Wails bindings ── Wails 視窗內的控制台 UIvanilla HTML/JS/CSS
Go server ←── HTTP / WebSocket over loopback ── 瀏覽器 tabNext.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` §A1v1→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` goroutinebufio 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 intervalPage 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`](./v2/control-panel.md) | Wails 控制台 UI + Go App bindings + LogBuffer + log pump + 狀態機 + PreferencesR5-D2/D3+ OS 通知觸發點 | R5-1, R5-5, R5-D1/D2/D3, R5-EM8-4, M8-4b, M8-5 |
| 2.2 | [`v2/ffmpeg-lgpl.md`](./v2/ffmpeg-lgpl.md) | 三平台 LGPL ffmpeg vendor 策略Makefile patch + macOS build script + 授權檔管理) | R5-6, R5-6a, R5-6b, R5-6cM8-3 |
| 2.3 | [`v2/server-lifecycle.md`](./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-3M8-4, M8-9 |
| 2.4 | [`v2/cors-security.md`](./v2/cors-security.md) | CORS whitelist middleware、WS origin check、資料驗證邊界 | 三方共識 #5M8-8 |
| 2.5 | [`v2/deletions.md`](./v2/deletions.md) | yt-dlp / Mock 模式全清單(檔案 / 函式 / 行號 / i18n key / vendor / installerv2.1 修正 `videoIsURL` / `NewVideoSourceFromURL` 明確刪除 | R5-5a, R5-7, PM Minor 5M8-1, M8-2 |
| 2.6 | [`v2/web-ui-offline-overlay.md`](./v2/web-ui-offline-overlay.md) | 瀏覽器 tab 的 `<ServerOfflineOverlay>` 實作polling + **WebSocket shutdown-imminent** 雙管道、重試、SSR 相容 | R5-2 三方共識 #14, PM Minor 4M8-7 |
| 2.7 | [`v2/milestone-plan.md`](./v2/milestone-plan.md) | M8-1 ~ M8-10 + **M8-4b 階段化啟動**;總工時 ~12 人天 | 整體M8-* |
| 2.8 | [`v2/code-reuse-v2.md`](./v2/code-reuse-v2.md) | 逐模組沿用 / 改寫 / 新寫比例表 | 整體 |
| 2.9 | [`v2/startup-pipeline.md`](./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~E6M8-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 hostmacOS 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` eventpayload 為陣列);(2) server 推論 frame 狀態禁止用 logger.Info改用 debug levelline-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 視窗通常也會關瀏覽器 tabrace 不構成實際問題。若使用者真的沒關瀏覽器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 APItab visible → 5 s intervaltab hidden → 停 pollingtab 再次 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 blockerF-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 | 使用者 | 待確認 |