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

516 lines
37 KiB
Markdown
Raw Permalink 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.

# PM 交叉審閱 TDD v22026-04-14
> 審閱者PM Agent
> 被審物:`/Users/jimchen/visionA/local-tool/.autoflow/04-architecture/TDD-v2.md` + `v2/` 全 8 份子檔
> 對照基準:`PRD-v2.md`484 行)+ R5 五輪決策 + R5-D 補充決策
> 審閱範圍需求面完整度、R5 / R5-D 落地、PM 5 懸念的技術回答、體驗紅線
---
## 摘要
- **總結論:需小改後通過**(不是大改、不阻擋整體方向)
- **問題數**Major 4 個 / Minor 5 個
- **是否阻擋進開發****部分阻擋**——Major #1 / #2R5-D1 / R5-D2 / R5-D3 未落地)必須在進 M8-4/M8-5 開發前補上;其餘 Major 可在 M8-10 前修正
- **整體品質**TDD v2 對 R5 主決策的落地完整度 **90%**;對 R5-D 補充決策的落地完整度 **0%**(這是 PM 的主要不滿);對 R5 主決策以外的技術設計state machine、LogBuffer、CORS、ffmpeg LGPL品質很高
---
## A. 需求完整性檢查(對 PRD v2
| PRD v2 章節 | 要求 | TDD v2 落地位置 | 檢查結果 |
|------------|------|---------------|---------|
| §4.1 保留功能 (8 項) | device scan / 模型管理 / 批次影像上傳 / camera / 上傳影片 / MJPEG / WebSocket / 系統 API / i18n / Dark Mode / Log 持久化 | `code-reuse-v2.md`(沿用 85-95%+ `deletions.md` §1-6 明確區分保留 vs 刪除 | ✅ 無誤刪。`deletions.md` §2.6/2.7 小心處理 manager_test / api_e2e_test |
| §4.2 砍除 9 項 | URL endpoint / url_handler / yt-dlp / 前端 URL tab / Mock 全部 / webview 載 Next.js / tray / first-run 模式選擇 / US-2 Mock | `deletions.md` §1-6 + `milestone-plan.md` M8-1/M8-2 逐檔逐行 | ✅ 全部對應;`deletions.md` §8 的驗收 grep 是漂亮的防呆 |
| §4.3 新增 6 項 (N1-N6) | Wails 控制台 / 自動開瀏覽器 / Offline Overlay / boot-id / CORS / watchServer Error state | `control-panel.md`N1 / N2+ `web-ui-offline-overlay.md`N3+ `server-lifecycle.md` §9N4+ `cors-security.md`N5+ `control-panel.md` §4.7N6 | ✅ 六項皆有專屬子檔 |
| §5 US-1 ~ US-7 AC | 每個 AC 應有技術對應 | 見 §C「US AC 追蹤表」 | ⚠️ 有 2 個 AC 沒對應(見 C|
| §6 非功能 | 安裝檔 ≤ 185/165/165 MB、idle RAM ≤ 450 MB、首次推論、瀏覽器相容 | `milestone-plan.md` M8-10 有驗收 installer sizeidle RAM 和首次推論時間**完全沒被估算** | ❌ **見 Major #3** |
| §7 第三方授權 | ffmpeg LGPL 方案 B三平台+ ffprobe + macOS commit + 授權檔 | `ffmpeg-lgpl.md` 完整落地 | ✅ 很完整,甚至連 `spctl` / `codesign` / BUILD.md reproducibility 都想到 |
| §8 新風險 N-R1/2/3/4 | 瀏覽器相容性 / macOS ffmpeg 維護 / 關窗誤解 / CI 測試分層 | TDD v2 §3 新增 R-v2-1 ~ R-v2-7 對應N-R1 吸收進 R-v2-6N-R2 吸收進 R-v2-2N-R3 部分吸收進 AC-7.6 要求N-R4 無明確對應 | ⚠️ **見 Minor #1**N-R4 要求在 TDD v2 討論階段提出分層方案TDD 只在 PM 懸念欄說「交 Testing」而沒給方案|
---
## B. R5 / R5-D 決策落地檢查
### B.1 R5 主決策12 條)
| 決策 | TDD v2 落地位置 | 落地狀況 |
|------|---------------|---------|
| R5-1動機 A+B+G純 127.0.0.1| `control-panel.md` §1-2、`server-lifecycle.md` §2.1、`cors-security.md` §3 | ✅ 完整 |
| R5-2視窗關閉 = 結束 server| `server-lifecycle.md` §2.3、§7 `OnBeforeClose` return false、§8 shutdown 順序 | ✅ 完整,且 main.go 明確寫出 OnBeforeClose hook預留未來改動空間 |
| R5-3維持砍 tray| `server-lifecycle.md` §2.3 註解「不 hide-to-tray」、`TDD-v2.md` §0.1 | ✅ 不存在任何 tray code |
| R5-4啟動自動開瀏覽器 + Settings 可關)| `control-panel.md` §4.6 ServerController.Start 末段、`server-lifecycle.md` §2.1 t=2.500 | ⚠️ **語意違反 R5-D3**(見 Major #1|
| R5-5控制台 scope| `control-panel.md` §1 明列「不做業務 UI」 | ✅ 完整 |
| R5-5aMock 完全砍)| `deletions.md` §2 + §3 + §6、`milestone-plan.md` M8-2 | ✅ 涵蓋 Go / Wails app / 前端 / i18n / CLI flag 所有層 |
| R5-6LGPL 方案 B| `ffmpeg-lgpl.md` §1 表格 | ✅ |
| R5-6amacOS decoder-only ~20 MB| `ffmpeg-lgpl.md` §2.3 configure flags + §2.2 feature set | ✅ 驗收條件 `< 25 MB` 合理 |
| R5-6bmacOS binary commit| `ffmpeg-lgpl.md` §5 layout + §6 `.gitignore` | ✅ 連 `!/vendor/ffmpeg/macos/**` 的 gitignore 順序陷阱都提到 |
| R5-6c一起包 ffprobe| `ffmpeg-lgpl.md` §2.2 末段 + §2.3 cp 指令 + §7 payload + §8 installer | ✅ 三平台全覆蓋 |
| R5-7M7 Windows 先不管)| `milestone-plan.md` M8-10 註解「R5-7 同意先不管,這次順帶驗證」 | ✅ 沒偷跑 M7 Windows但又不漏驗 |
| 共識 #14boot-id| `server-lifecycle.md` §9 + `web-ui-offline-overlay.md` | ✅ 端到端 |
### B.2 R5-D 補充決策3 條)— **這是最大問題**
| 決策 | 應落地位置 | 落地狀況 |
|------|----------|---------|
| R5-D1Server 崩潰時保留 OS 原生通知 | `control-panel.md` §4.7 watchServer Error state / `server-lifecycle.md` §6 watchServer 或 `control-panel.md` §4 ServerController.setState | ❌ **完全缺失**`grep -ri 'osascript|notify-send|powershell.*notification' v2/` 零匹配。TDD 只 emit `server:error` Wails event 給控制台,沒有發系統原生通知 |
| R5-D2Linux 預設 `openBrowserOnStart=OFF`mac/Win = ON | `preferences.go`(預設值載入邏輯)或 `control-panel.md` §4.3 Preferences struct 預設 | ❌ **完全缺失**。TDD 只定義 `Preferences.OpenBrowserOnStart bool`,沒有 `NewDefaultPreferences()``runtime.GOOS` 分平台初值的邏輯。若 `preferences.json` 不存在,使用者第一次啟動在 Linux 上會用 Go zero valuefalse剛好誤中但這是 **碰巧對**,不是設計對 |
| R5-D3每次 Start 成功都自動開瀏覽器(不只首次)| `control-panel.md` §4.6 ServerController.Start 末段 | ❌ **實作與決策相反**。現況是 `if c.app.prefs.OpenBrowserOnStart && !c.app.autoOpenedThisSession``autoOpenedThisSession` per-session-once 只會開一次Restart 後不再開。**這和 R5-D3 「每次啟動都自動開」語意相反**。milestone-plan.md M8-9 step 3 甚至把「防呆autoOpenedThisSession 確保 Restart 不會二次開瀏覽器」列為驗收條件,直接落地了**錯的語意** |
---
## C. US AC 追蹤表PRD §5 × TDD v2
| AC | 需求 | TDD 對應 | 狀況 |
|----|------|---------|------|
| AC-1.1/1.2 | 安裝 ≤ 3 分 / 上限 5 分 | v1 沿用TDD 未碰 | ✅ |
| **AC-1.3** | 首次啟動 Wails + server + 瀏覽器 ≤ 10 秒 | **未估算** | ❌ Major #3 |
| AC-1.4 | 無硬體時顯示 empty state | `deletions.md` §6.5 noDevices 文案、R-v2-7 | ✅ |
| AC-1.5 | Gatekeeper 引導 | 同 v1 沿用 | ✅ |
| AC-1.6 | 可關閉自動開瀏覽器 | Preferences.OpenBrowserOnStart | ✅(但受 R5-D2 / D3 牽連)|
| AC-2.1 | 日常啟動 ≤ 5 秒全就緒 | 未估算 | ⚠️ Minor #2 |
| AC-2.2 | 保留 i18n / dark mode 偏好 | 前端 localStorage 沿用 | ✅(但沒寫進 TDD |
| AC-2.3 | 關過自動開瀏覽器後只開控制台 | Preferences + autoOpenedThisSession | ✅ |
| AC-3.7 | 使用者關瀏覽器後不發 toast / 不發 OS 通知 | TDD 未明確處理,`control-panel.md` 沒有 toast 入口,默認符合 | ⚠️ 和 R5-D1 衝突。AC-3.7 說「不發 OS 通知」R5-D1 說「server 崩潰時保留 OS 通知」。**兩者 scope 不同**AC-3.7 是裝置事件R5-D1 是 server 崩潰TDD 都沒處理,但兩者需要分開實作 |
| AC-4.5 | 拖放 .nef 只在瀏覽器有效 | `control-panel.md` §1 明列「不做業務」隱含 | ✅ |
| AC-4.6 | 8 個預設 .nef + 無 Model Zoo | v1 沿用 | ✅ |
| AC-5.1-5.6 | 瀏覽器 Workspace / 後端 camera pipeline / 不走 getUserMedia | v1 沿用 | ✅ |
| AC-6.1-6.4 | 5 種副檔名上傳影片 / 不支援 URL | `milestone-plan.md` M8-6 + `deletions.md` §5.1 前端 accept + backend handler patch | ✅ diff 清楚 |
| **AC-7.1** | 偵測斷線顯示 Offline Overlay | `web-ui-offline-overlay.md` § 全部 | ✅ |
| **AC-7.2** | Overlay 文案 + Retry | `web-ui-offline-overlay.md` §4.6 i18n | ✅ |
| **AC-7.3** | 控制台狀態變 Error + log panel 顯示錯誤 | `control-panel.md` §4.7 + §3 狀態 badge 紅 | ✅ |
| **AC-7.4** | Restart 後 boot-id 變 → reload | `server-lifecycle.md` §2.2 + `web-ui-offline-overlay.md` §4.3 | ✅ |
| AC-7.5 | 關 Wails → server 結束 → 相同 Overlay | `server-lifecycle.md` §2.3 | ✅ |
| AC-7.6 | Close 按鈕 tooltip / confirm 警語 | **未落地**Design Agent 任務TDD 未強制 Wails main.go 加 confirm | ⚠️ Minor #3 |
---
## D. PM 5 懸念的技術回答檢查
| # | PRD §11 問題 | 歸屬 | TDD 有無回答 |
|---|-------------|------|------------|
| 1 | 自動開瀏覽器 Settings 與 Web UI Settings 資料是否同一份 | Architect | ✅ `control-panel.md` §4.1 明確說「`visiona-local/preferences.go``<dataDir>/preferences.json`」— 這是**控制台側**的偏好Web UI 側的 i18n / dark mode 仍用 browser localStoragev1 沿用)。兩邊**分離**PM 接受這個答案 |
| 2 | AC-1.3 10 秒預算是否可達 | Architect 估算 | ❌ **完全沒估算**。TDD 沒有任何段落討論「Wails 冷啟動 + Go server waitHealthy + 瀏覽器冷啟動」的時間預算分解。PRD 明確要求 Architect 驗證此預算 |
| 3 | idle RAM ≤ 450 MB 可達性 | Architect 實測 | ❌ **完全沒提**。TDD 沒有任何段落討論 idle RAM 估算v2 砍了 Mock state 和 yt-dlp 子程序後的 baseline |
| 4 | N-R4 CI / E2E 測試分層方案 | Testing Agent現階段不審 | — 略過 |
| 5 | 「由 visionA-local 跑」常駐徽章 | Design Agent現階段不審 | — 略過 |
**PM 5 懸念中的 2/3AC-1.3、idle RAM完全沒被回答。這兩題是 PRD §11 明列「交 Architect」的題目Architect 沒回等於把球踢回給 PM。**
---
## E. 臆測 / 誤刪 / 誤解清單
### E.1 臆測超出範圍
- **無**。TDD v2 沒有偷偷新增 LAN mode、tray fallback、daemon、auto-update、telemetry 等 R5 明確否決的功能。這點乾淨
### E.2 誤刪
- **無誤刪**。批次影像上傳R5 共識 10 / PRD §4.1)在 `deletions.md` 中未被列入刪除清單,`code-reuse-v2.md` 也把它放在沿用區。`camera_handler.go` 只砍 `StartFromURL`,沒碰 `UploadBatch` 相關 handler
- **但有一個 risky 邊界**`deletions.md` §1.1 說「若 `NewVideoSourceFromURL` **唯一呼叫者**是 StartFromURL → 一起砍」。實際 grep 顯示 `camera_handler.go:731` 還有 `NewVideoSourceFromURLWithSeek` 被本地上傳影片 seek 呼叫。**M8-1 執行者必須先把這個 grep 結果列出**,否則可能會誤砍本地影片 seek 功能。TDD `ffmpeg-lgpl.md` §11 已警示這點,但 `deletions.md` §1.1 的敘述「可從 RTSP stream 呼叫」不精準,實際是「從本地影片 seek 呼叫」
### E.3 誤解 R5 / R5-D
- **R5-4 理解錯誤****Major #1**TDD 把「首次啟動自動開瀏覽器」實作成 per-session-once`autoOpenedThisSession` flag但 R5-D3 明確修正語意為「每次 Start Server 成功都開」。這個誤解在 `milestone-plan.md` M8-9 和 `control-panel.md` §4.6 各出現一次,是**系統性誤解**
- **R5-D1 / R5-D2 整組遺漏****Major #1** 包含)
- **R5-2 理解正確**TDD 明確寫 `OnBeforeClose` return false、不 hide-to-tray、有 `shutdownGracePeriod` 5s → 10s 對齊 server 的討論。這點很到位
---
## F. 體驗紅線檢查
### F.1 北極星指標「5 分鐘內瀏覽器跑第一幀推論」可達嗎
PRD §1.4 把「瀏覽器啟動時間」也納入 5 分鐘預算。TDD 沒有做端到端預算分解,但從元件看:
- Wails 冷啟動:~0.5-1 sv1 經驗)
- Go server spawn + waitHealthy1-3 s依 Python sidecar 啟動)
- `OpenInBrowser("")`:系統 call ~0.1 s
- 瀏覽器冷啟動(第一次點開 Chrome/Safari/Edge**3-8 s**(最大不確定性)
- Next.js static export 載入:~0.5-1 s
→ 在已開瀏覽器的情境下 5-6 s 可達 AC-2.1 的「日常啟動 ≤ 5 秒」;在冷啟動情境下 6-13 s **很可能超過 AC-1.3 的 10 秒上限**
**PM 結論**AC-1.3 的 10 秒上限在「使用者尚未開過瀏覽器」的情境下可能不可達。建議 Architect 在 TDD v2 增補一段預算分解若確實超過PRD 要放寬到 15 秒。這是進開發前必須釐清的事項(見 Major #3)。
### F.2 race conditionWails 關閉視窗 → StopServer 過程中瀏覽器 tab
TDD v2 `R-v2-4` 已明確提到這個 race condition0.5-5s 內瀏覽器可能看到 ECONNREFUSED 但 Overlay 還沒觸發),並分析為「實務上使用者關 Wails 通常也會關瀏覽器 tabrace 不構成實際問題若真的沒關15 s 後 Overlay 會出現」。**PM 認可這個取捨**,但有補充意見:
- **建議**:在 Wails OnBeforeClose 觸發前,先透過 WebSocket broadcaster 發一個 `server:shutdown-imminent` 訊息給所有連著的瀏覽器 tab利用現有 `/ws/server-logs``/ws/devices/*` 通道),前端收到後立即顯示 Overlay不需要等 15 s polling。這個成本低server 端只 3-5 行、前端 store 新增一個 listener 即 10 行)
- 若 Architect 評估成本過高 → 接受現狀。這是 **Minor #4**,不阻擋進開發
### F.3 「關閉視窗 ≠ 結束 server」的心智負擔N-R3
PRD §8.2 N-R3 要求三處文案到位:
1. Wails Close 按鈕 tooltip / confirm dialogAC-7.6)— **TDD 未落地**main.go 只寫了 `OnBeforeClose: return false`,沒加 confirm 對話框邏輯;是否加是 Design Agent 的 scope但 TDD 應該**預留 hook**
2. Offline Overlay 文案引導 — `web-ui-offline-overlay.md` §4.6 ✅
3. First-Run 歡迎頁一句說明 — TDD 未提(但 v1 有v2 沿用,推測 OK
**Minor #3**TDD 應在 `server-lifecycle.md` §7 的 main.go 範例裡保留 `OnBeforeClose` 可讀 `Preferences.ConfirmOnClose` 的 hook而不是寫死 `return false`。這樣 Design Agent 後續加 confirm dialog 時不用再重寫 TDD
---
## G. PM 對 Architect Q4 的回答
Architect 在 `server-lifecycle.md` §10 問:「`shutdownGracePeriod` 5s → 10s 的副作用?使用者頻繁開關 app 可能感覺變慢。」
**PM 回答**(基於使用者體驗基準):
| 問題 | 答案 |
|------|-----|
| 總 grace period 建議值 | **7 秒**(而非 10 秒理由Nielsen Norman 的研究指出 1 秒內 = 即時反饋、10 秒 = 使用者注意力臨界點7 秒保留足夠 server 優雅結束餘量,又不會逼近「我是不是當機」的感受閾值 |
| 超過多久要顯示「停止中…」進度提示 | **1 秒**。Wails 收到關閉訊號後應立即(< 100 ms顯示一個不可關閉的 modal正在停止本機伺服器…」+ spinner避免使用者看到空白卡住」。這個 modal 7 秒後若 server 仍未退出換文字為伺服器未正常結束正在強制關閉…」,之後 SIGKILL |
| 如果 7 秒真的不夠server 端某些清理需要 > 7 秒)| 兩個選擇:(a) 降低 server 端清理時間理想b) 把 server 端的 `shutdownFn` 10 s timeout 改為 6 s與 Wails 7 s 留 1 s 緩衝 |
**建議**:採用 PM 的 7 秒 + modal 方案,這是比 Architect 提的「5-10 秒區間 emit event」更簡單不需要 event 機制)且更符合 UX 基準的做法。Architect 若有技術面反對意見,請在使用者確認前提出。
---
## H. 問題清單
### Major阻擋進開發必須在 M8-4 / M8-5 開發前修)
#### Major #1R5-D1 / R5-D2 / R5-D3 三條補充決策完全沒落地
**問題**R5-D 三條是使用者在 2026-04-14 Design v2 產出後的補充決策,時間上**晚於 TDD v2 draft**,但 TDD v2 必須被修正以落地這三條。現況:
- **R5-D1崩潰時保留 OS 原生通知)**TDD 只在 Wails 控制台發 `server:error` event沒叫 `osascript display notification` / `powershell New-BurntToastNotification` / `notify-send`
- **R5-D2Linux 預設自動開瀏覽器 OFF**`preferences.go` 沒有依 `runtime.GOOS` 分平台 default 的邏輯
- **R5-D3每次 Start 都開,不只首次)**`autoOpenedThisSession` flag 和 milestone-plan M8-9 驗收條件明確是 per-session-once**和決策相反**
**應加在**
| 決策 | 子檔 | 修正位置 |
|------|------|---------|
| R5-D1 | `control-panel.md` §4.7watchServer Error state+ 新增 §4.8「OS 原生通知 helper」 | 新增 `visiona-local/notify_darwin.go` / `notify_windows.go` / `notify_linux.go` 三檔,在 `ctrl.setState(ServerStateError, ...)` 時呼叫 |
| R5-D2 | `control-panel.md` §4.3 Preferences 型別 + `v2/server-lifecycle.md` §2.1 載入流程 | 新增 `NewDefaultPreferences() Preferences``runtime.GOOS == "linux"``OpenBrowserOnStart: false`,否則 `true``preferences.go` 的 load 若檔不存在,用這個預設 |
| R5-D3 | `control-panel.md` §4.6 `ServerController.Start` 末段 + `milestone-plan.md` M8-9 step 3/5 驗收條件 | **砍 `autoOpenedThisSession` flag**,改為:只要 `prefs.OpenBrowserOnStart == true` 就每次 Start 成功後 `OpenInBrowser("")`。M8-9 驗收改為「多次按 Restart每次都開新 tab或 focus 既有 tab瀏覽器行為決定」 |
**阻擋等級**Major必須在 M8-4/M8-5 開發前修 TDD 子檔。因為這三條會影響 `preferences.go` / `server_control.go` / 新增的 notify_*.go 的介面簽名,若先開發再改會浪費時間
---
#### Major #2R5-D3 誤解已被寫進 milestone-plan M8-9 驗收條件
**問題**`milestone-plan.md` 第 335 / 347 / 350 行把「Restart 不會二次開瀏覽器」列為**驗收成功**條件。這會在 Reviewer 審查 M8-9 時被當成「通過」,但其實這**違反 R5-D3**。
**修正**M8-9 step 3 改寫,驗收條件改為:
- 首次啟動 Server瀏覽器開 1 個 tab
- 手動按 Restart瀏覽器開第 2 個 tab或在瀏覽器自動重用既有 tab視瀏覽器行為
- 使用者在 Preferences 關掉 `openBrowserOnStart`:之後 Start/Restart 都不開瀏覽器
**阻擋等級**:與 Major #1 綁在一起修
---
#### Major #3PM 5 懸念中 AC-1.310 秒)與 idle RAM≤ 450 MB沒被 Architect 回答
**問題**PRD §11 明確把這兩題交給 Architect「TDD v2 驗證」,但 TDD 所有 8 個子檔都找不到任何段落在討論這兩個預算。
**應加在**
- **AC-1.3 10 秒可達性**:建議在 `TDD-v2.md` §1 或 `server-lifecycle.md` §2.1 後新增一節「冷啟動預算分解」:
```
Wails 冷啟動 ~0.5-1 s
migrateOldDataDirs ~0.01 s
startIPCServer ~0.05 s
seedUserDataDir ~0.02-2 s首次安裝時 copy 8 個 .nef約 73 MB
ensurePythonRuntime ~0.5-3 s首次啟動 bundled Python extract
exec.Command spawn ~0.01 s
waitHealthy (Python sidecar 起) ~1-3 s
OpenInBrowser system call ~0.1 s
瀏覽器冷啟動 ~3-8 s使用者尚未開瀏覽器
Next.js SSG 首次載入 ~0.5-1 s
────────────────────────
首次安裝冷啟動總計 ~5.5-18 s
回訪情境(瀏覽器已開)~2.5-10 s
```
結論:**AC-1.3 的 10 秒上限在首次安裝 + 冷啟動瀏覽器情境下不可達**,建議 PRD 放寬到 15 秒,或在 PRD AC-1.3 明確註記「若瀏覽器尚未啟動,可能延長到 15 s」
- **idle RAM ≤ 450 MB**:建議在 `TDD-v2.md` §3 風險清單新增一條,或在 `control-panel.md` 新增一節「資源估算」:
```
Wails 殼 ~80-120 MBWebView2 / WKWebView 基礎)
Go server 子程序 ~40-60 MBGin + 8 個預載入 .nef metadata
Python sidecar ~150-220 MBPython runtime + numpy + opencv + pyusb
logs/ LogBuffer 等 ~5 MB
────────────────────
總計 ~275-405 MB
```
結論:**idle RAM ≤ 450 MB 應可達**,上限 550 MB 安全。PM 可以接受此估算作為 TDD 回答
**阻擋等級**Major必須在使用者確認 TDD v2 前補上。若 10 秒不可達,要決定是放寬 AC-1.3 還是改架構(例如 Wails 內建 WebView 搭配預先隱藏視窗的 fallback
---
#### Major #4shutdownGracePeriod 5s → 10s 的使用者體感取捨未解決
**問題**TDD `server-lifecycle.md` §10-2 把這題列為「待確認」,但這是**體驗面**的問題不是技術面Architect 不該把球踢回使用者。
**PM 回答**:見 §G。採用 **7 秒 + 1 秒內顯示「停止中…」modal** 方案。
**應加在**`server-lifecycle.md` §8 把 `shutdownGracePeriod` 改為 7 s§5 ServerController.Stop 新增 `emit "server:stopping"` event前端新增「stopping modal」元件`control-panel.md` §5 action-bar.js 補M8-5 驗收條件加「關閉視窗後 1 秒內看到停止中 modal」
**阻擋等級**Major但範圍小。可併入 M8-4 / M8-5 開發
---
### Minor可先進開發後續修
#### Minor #1N-R4CI / E2E 測試分層)沒有方案
PRD §8.2 明確要求 Architect 在 TDD v2 討論階段提出分層方案。TDD 沒提。但這題現階段交 Testing Agent暫不阻擋。
**建議**:在 `milestone-plan.md` M8-10 新增一段「測試分層建議」,或在 TDD v2 §3 風險區新增 R-v2-8 占位,標註「待 Testing Agent 審 TDD 時補方案」
---
#### Minor #2AC-2.1(日常啟動 ≤ 5 秒)沒估算
和 Major #3 同源。若 Major #3 補上冷啟動預算分解,順便也覆蓋 AC-2.1
---
#### Minor #3AC-7.6Wails Close 按鈕 tooltip / confirm 警語)沒落地
TDD `server-lifecycle.md` §7 的 main.go 範例寫死 `OnBeforeClose: return false`。這等同「直接關,無警告」,和 PRD AC-7.6 要求的「明確寫『關閉此視窗會結束 Local Server』」衝突。
**建議**TDD 應預留 hook例如
```go
OnBeforeClose: func(ctx context.Context) (prevent bool) {
if app.prefs.ConfirmOnClose {
// TODO: Design Agent 決定 confirm dialog 文案
// 用 wailsRuntime.MessageDialog 跳對話框
}
return false
},
```
這樣 Design Agent 後續加 dialog 時不用再修 main.go。是否顯示 confirm 由 Design Agent 決定,但 hook 要先留
---
#### Minor #4Wails 關閉時可主動通知瀏覽器 tab 避免 15 s race condition
見 §F.2。建議在 Wails OnBeforeClose 之前透過 WebSocket 廣播 `server:shutdown-imminent` 訊息,前端立即顯示 Overlay。成本低server 端 ~3 行、前端 ~10 行),體驗提升明顯。
**阻擋等級**Minor可在 M8-10 smoke test 發現體驗不佳時補
---
#### Minor #5`deletions.md` §1.1 「RTSP stream 可能呼叫 NewVideoSourceFromURL」敘述不精準
實際 grep 顯示 `camera_handler.go:731` 用 `NewVideoSourceFromURLWithSeek` 處理**本地上傳影片的 seek**,不是 RTSP。若 M8-1 執行者照字面理解,會把「不是 URL 但共用函式」的本地影片 seek 功能誤砍。
**修正**`deletions.md` §1.1 文字改為「若唯一呼叫者是 `StartFromURL` → 整個砍;若仍有本地影片 seek 呼叫者camera_handler.go:731 `NewVideoSourceFromURLWithSeek`)→ **函式保留**,但 rename 去掉 `FromURL` 字眼避免誤解,例如改為 `NewVideoSourceWithSeek`,並把 `--disable-network` 這項 configure flag 保留(因為本地檔案讀取走 `protocol=file` 不需 network
---
## I. 通過 / 不通過 結論
### 結論:**條件通過**
| 條件 | 必須完成 |
|------|---------|
| Major #1 + Major #2 修完R5-D1/D2/D3 落地 + milestone-plan 驗收條件修正)| **進 M8-4 / M8-5 前必須** |
| Major #3 補上冷啟動預算分解 + idle RAM 估算 | **使用者確認 TDD v2 前必須** |
| Major #4 採用 7 秒 + modal 方案 | **進 M8-4 / M8-5 前必須**(介面簽名受影響)|
| Minor #1-#5 | 可先進開發M8-10 smoke test 前補齊即可 |
### 整體評價
**TDD v2 品質8.5/10**(扣分在 R5-D 整組遺漏 + PM 5 懸念 2 題沒回答)。
- ✅ **值得稱讚**
- `ffmpeg-lgpl.md` 的完整度configure flags、BUILD.md reproducibility、spctl 驗證、gitignore 陷阱)
- `deletions.md` 的「按表操作 + 驗收 grep」設計防呆到位
- R5-2 / R5-3 的處理乾淨俐落(不存在任何 tray 殘跡)
- state machine 五態的思考完整(`txMu` vs `mu` 雙 mutex 對抗 race
- 新風險 R-v2-1 ~ R-v2-7 對應具體且有緩解
- milestone-plan.md 的依賴圖清晰,支援平行開發
- ❌ **需要補強**
- R5-D 三條補充決策**整組遺漏**,這是 PM 的主要不滿。Architect 需要在使用者回答 R5-D 後,立即在 TDD v2 出一個 patch
- PM 5 懸念的 AC-1.3 與 idle RAM **完全沒回答**PRD 明確要求 Architect 在 TDD v2 驗證
- UX 取捨shutdownGracePeriod被當成技術問題丟回使用者
### 下一步建議
1. Orchestrator 啟動 Architect Agent做 **TDD v2 patch**,聚焦修 Major #1 ~ #4
2. 修完後 Orchestrator 再啟動 PM 做第二輪審閱(只審 patch 部分,~30 分鐘)
3. 同時 Orchestrator 啟動 Design Agent 做 Design 交叉審閱(本輪 PM 審閱發現 AC-7.6 是 Design 的未決項)
4. Design + PM 兩輪都通過後交使用者確認 → 進 M8-1 ~ M8-3互不依賴的三個砍除/ffmpeg milestone 可立即啟動,不受 Major #1/#2 影響,加速開發)
---
## 附錄:對照資料
- **PRD v2**`/Users/jimchen/visionA/local-tool/.autoflow/02-prd/PRD-v2.md`484 行)
- **TDD v2 索引**`/Users/jimchen/visionA/local-tool/.autoflow/04-architecture/TDD-v2.md`136 行)
- **TDD v2 子檔 8 份**`/Users/jimchen/visionA/local-tool/.autoflow/04-architecture/v2/`3738 行)
- **R5 / R5-D 決策**`/Users/jimchen/visionA/local-tool/.autoflow/progress.md` §「R5 第五輪」+ §「R5-Design 補充」
---
# PM 第二輪審閱 TDD v2.12026-04-14
> 審閱者PM Agent
> 被審物:`TDD-v2.md` v2.1 + 9 份 `v2/*.md` 子檔(新增 `startup-pipeline.md`
> 審閱範圍:只審 v2.0 → v2.1 補丁Major × 4 + Minor × 5 + R5-E 落地 + Architect 自補),不重審 v2.0 既有內容
## 摘要
- **總結論:通過(附 1 個 Minor 餘項)**
- 第一輪 **Major × 4 全部修好**Minor × 5 修好 4 個 + 1 個以另一等效方案處理
- **R5-E1~E6 六條全部落地**`v2/startup-pipeline.md` 新檔 518 行,程式碼級完整)
- **不阻擋進 M8 開發**。M8-1/M8-2/M8-3/M8-8 可立即啟動M8-4/M8-4b/M8-5/M8-9 在 Design v2.1 文案完成後即可啟動
---
## A. 第一輪 Major 修復檢查
| # | 問題 | 檢查位置 | 結果 |
|---|-----|---------|------|
| **Major 1 R5-D1 OS 通知** | `server-lifecycle.md` 新增 §10L690-791含 `notify.go` + macOS `osascript` / Linux `notify-send -u critical` / Windows PowerShell BurntToast + `msg *` fallback`control-panel.md` L482-486 在 Start 失敗時 `go sendCrashNotification(...)`§6 diffL405-408在 watchServer 3 次失敗時 fire-and-forget`startup-pipeline.md` L295-299 pipeline 失敗時一併發通知 | ✅ 完整。觸發點三處Start 失敗 / watchServer 3 次失敗 / startup pipeline 失敗)都有 non-blocking `go` 呼叫 |
| **Major 1 R5-D2 Linux 預設 OFF** | `server-lifecycle.md` §11.3L824-850`DefaultPreferences()` 依 `runtime.GOOS != "linux"` 分平台;`control-panel.md` L169 + L498-500 註解對應;`milestone-plan.md` M8-9 驗收 L404-405 分 macOS/Win vs Linux 兩條 | ✅ 完整 |
| **Major 1 R5-D3 每次都開** | `control-panel.md` `ServerController.Start` L491-503 直接檢查 `if c.app.prefs.AutoOpenBrowser` + 註解明確「取消 v2.0 的 autoOpenedThisSession flag」`server-lifecycle.md` L923 明示「已移除」;`milestone-plan.md` L393 / L411 / L439 反覆強調砍除 | ✅ 完整。但 `code-reuse-v2.md:92` 有殘留(見 §G Minor|
| **Major 2 M8-9 驗收條件** | `milestone-plan.md` M8-9 L407「多次按 Restart若 `AutoOpenBrowser=true`,每次 Restart 都會呼叫 `OpenInBrowser`」L411 Reviewer 檢查重點「不存在 `autoOpenedThisSession` 欄位」 | ✅ 完整。原「Restart 不會二次開」條件已全數砍除 |
| **Major 3 §11-1 Preferences 持久化** | `server-lifecycle.md` §11 全段L795-924§11.1 `<dataDir>/preferences.json` + §11.2 struct 定義 + §11.3 DefaultPreferences + §11.4 讀寫時機 + L879 atomic write-rename | ✅ 完整 spec |
| **Major 3 §11-2 冷啟動預算** | 被 R5-E 取代(索引 L35 明示「原 10 秒可達性已被 R5-E 60 s 上限 + 階段化進度取代」);`server-lifecycle.md` §2.1aL83-90補上日常啟動 ~3.8 s 估算 | ✅ 以 R5-E 取代是合理方案 |
| **Major 3 §11-3 450 MB 範圍** | `server-lifecycle.md` §11.6L925-943明確「450 MB 目標不含瀏覽器 tab 的記憶體消耗」+ 量測條件定義 + 悲觀情境 50 MB 超標時的 fallback | ✅ 完整 |
| **Major 4 shutdown 7+1** | `server-lifecycle.md` L125 PM Q4 定案「7 秒 + 1 秒 modal」L527 `shutdownGracePeriod = 7 秒`L550 `graceTimer := time.NewTimer(7 * time.Second)`§8L499-521含廣播 + 1 秒 modal + SIGKILL 完整順序§2.3 時序 t=1.000 `shutdown:modal-show` | ✅ 完整,採 PM 方案 |
**結論**4 個 Major 全部修好。
---
## B. 第一輪 Minor 修復檢查
| # | 問題 | 檢查 | 結果 |
|---|-----|------|------|
| Minor 1 N-R4 測試分層 | `control-panel.md` L826 標 `blocked-on-testing-agent` | ✅ 按約定處理 |
| Minor 2 AC-2.1 ≤5 s | `server-lifecycle.md` §2.1aL83-90日常啟動 ~3.8 s 估算表 + 結論「遠低於 60 s 上限」| ✅ |
| Minor 3 OnBeforeClose confirm hook | `server-lifecycle.md` §7L475-479寫死 `return false` + 註解「不跳確認對話框」,**未預留 `ConfirmOnClose` hook**;但 Architect 在 L490-493 解釋「Wails v2 default 就是直接關,與 R5-2 語意一致,不加 hook」 | ⚠️ 以等效方案處理。PM 第一輪建議是「預留 hook」方便未來改動Architect 選擇「保持乾淨不加 dead code」。兩者語意均不彈對話框符合 R5-2PM 接受此取捨,**非阻擋** |
| Minor 4 WebSocket shutdown-imminent | `server-lifecycle.md` §2.3 t=0.005 / §8 L511 / `web-ui-offline-overlay.md` §3.2aL78-95+ L158 新增 `use-shutdown-watcher.ts` + L361 訂閱處理server 新增 `/ws/system` endpointWails `ctrl.Stop()` SIGTERM 前先廣播 | ✅ 完整server + 前端 + Wails 三側齊全)|
| Minor 5 deletions.md §1.1 精準化 | `deletions.md` L38-94 新增 v2.1 Minor 5 段落:明列 `grep -rn 'NewVideoSourceFromURL'` 命令 + `camera_handler.go:731` 是 `handleVideoSeek` 的 `videoIsURL` dead code + 整個 function 砍 + `videoIsURL` field 砍 + 驗收 grep | ✅ 完整,甚至列出驗收 grep 指令 |
**結論**5 個 Minor 全部處理1 個以等效方案)。
---
## C. R5-E 落地檢查(重點)
| # | 預期 | 位置 | 結果 |
|---|-----|------|------|
| **R5-E1** 60 s hard timeout | `startup-pipeline.md` L156 `startupHardTimeout = 60 * time.Second` + L325-330 watcher 檢查 `sinceTotal > startupHardTimeout` → `Fail()` | ✅ |
| **R5-E2** 6 階段化 | L154 `startupTotalStages = 6` + L167-177 `StartupPipeline` struct + §3 6 階段表L118+ L422 前端 render 6 rows | ✅ |
| **R5-E3** 20 s soft timeout 顯示重試提示 | L155 `startupSoftTimeout = 20 * time.Second` + L332-346 watcher 檢查 `sinceStage > startupSoftTimeout` → emit `startup:stage-timeout` + `softTimeoutEmitted` flag 防重複 | ✅ |
| **R5-E4** 60 s Error state + 三按鈕 | L325-330 hard timeout 呼叫 `Fail()` + `emitError()` + L292-294 同步叫 `ctrl.setState(ServerStateError, ...)`;三按鈕(重試/檢視 log/回報)在 `startup-panel.js` `showStartupError()`L469+)和 Design Spec 對齊 | ✅ |
| **R5-E5** 階段文字交 Design | `startup-pipeline.md` §2L87-105只定義 i18n key`startup.stage.{1-6}.label`),實際文字留給 `i18n/zh-TW.json` / `en-US.json`Design 填L517 明示「R5-E5 Design Spec v2.1 尚未敲定i18n key 已預留」 | ✅ 零硬編碼文字,完全交 Design |
| **R5-E6** WebSocket 連上 = 就緒 | `startup-pipeline.md` L22 R5-E6 明文§3 L118 階段 6 定義「`OnClientConnected` 第一次觸發」L120-130 兩種實作方式long-poll endpoint / sentinel file留給 M8-4b 執行者選擇L515 `startup-pipeline.md` 末段留 A-Q1 待 M8-4b 決定 | ✅ 設計完整,實作方式待 M8-4b 決定PM 接受)|
**結論**R5-E 六條全部落地且程式碼級細節event schema、1-indexed 陣列、非阻塞 EventsEmit、softTimeoutEmitted 防重複、watcher 生命週期管理)皆嚴謹。
---
## D. TDD ↔ PRD v2.1 對齊
| PRD v2.1 AC | TDD v2.1 對應 | 結果 |
|------------|--------------|------|
| AC-1.3 60 s 硬上限 | `startup-pipeline.md` L156 + `TDD-v2.md` 索引 L18 | ✅ |
| AC-1.3a 6 階段進度 | `startup-pipeline.md` §3 + §6 前端 render | ✅ |
| AC-1.3b 20 s 重試提示 | `startup-pipeline.md` L332-346 | ✅ |
| AC-1.3c 60 s Error state + 三按鈕 | `startup-pipeline.md` L325-330 + `startup-panel.js` showStartupError | ✅ |
| AC-1.3d 階段文字 Design | `startup-pipeline.md` L517 | ✅ |
| AC-7.7 OS 通知並存 | `server-lifecycle.md` §10 三平台 + L791「兩者互補不取代」 | ✅ |
| AC-2.1 日常啟動 ≤5 s | `server-lifecycle.md` §2.1a 日常啟動 ~3.8 s | ✅ |
| AC-2.1a 每次 Start 都自動開 | `control-panel.md` L491-503 | ✅ |
---
## E. Architect 自補項檢查
- **F-2 Restart port 強制保留**`server-lifecycle.md` §3.2L179-224完整含 `pickPort(preferredPort, forceMatch=true)` + 使用者情境解釋 + port 被佔則進 Error state + 廣播 shutdown-imminent ✅
- **階段 6 WebSocket 實作細節**`startup-pipeline.md` L120-130 列出 long-poll endpoint / sentinel file 兩方案L515 明示「M8-4b 執行者決定並回報」。PM 接受此處延後決策,因為兩方案介面簽名相容,不影響上游 M8-4 開發 ✅
- **boot-id crypto/rand 16 bytes**Q3索引 L29 v2.1 變更條目記錄,不引額外依賴 ✅
- **navigator.language fallback 強化**Q5索引 L30 記錄 ✅
---
## F. 工時合理性
v2.0 10 人天 → v2.1 12 人天(+2 天),增量分解:
- R5-E 階段化啟動 +1 天M8-4b 新 milestone
- R5-D1 OS 通知 +0.3 天
- PM Q4 7+1 shutdown modal +0.2 天
- Minor 4 WebSocket 廣播 +0.3 天
- 其餘M8-10 驗收項目擴充)+0.2 天
合計 2.0 天,與 milestone-plan.md L491 總計相符。
**PM 接受 12 人天**;另 L493 建議「加 M8-3 / M8-4 buffer 1 天 → 對使用者回報 ~13 人天」也合理。
**輕微瑕疵**`milestone-plan.md` L6 文字摘要寫「~11.5 人天」L491 合計表寫「12.0」。摘要行未同步更新,屬文字不一致。非阻擋。
---
## G. 第二輪新發現問題
### Major
無。
### Minor
#### Minor 6`code-reuse-v2.md:92` 殘留 v2.0 字串
**問題**`v2/code-reuse-v2.md` L92 仍然寫
```
第 83 行:砍 `mockMode` 欄位;新增 `ctrl` / `logBuf` / `prefs` / `autoOpenedThisSession` 欄位
```
**但** `milestone-plan.md:147` / `server-lifecycle.md:923` / `control-panel.md:811` 全部明示「**不存在** `autoOpenedThisSession`」。這是 v2.0 → v2.1 轉版時 `code-reuse-v2.md` 沒同步更新。
**影響**M8-4 / M8-5 的工程師 Agent 若以此為唯一指引,可能加回 `autoOpenedThisSession` 欄位 → 違反 R5-D3。
**修正**`code-reuse-v2.md:92` 移除 `autoOpenedThisSession` 字樣。一行 diff。
**阻擋等級**Minor。不阻擋進開發因其他三處指引都正確但進 M8-5 前最好修掉避免混淆。
#### Minor 7`milestone-plan.md:6` 摘要 11.5 vs 合計 12.0
見 §F。非阻擋下次順手改。
---
## H. 第二輪通過 / 不通過 結論
### 結論:**通過**(附 Minor 6 / Minor 7 可隨開發過程修)
| 項目 | 第一輪 | 第二輪 |
|------|-------|-------|
| Major | 4 | **0** |
| Minor | 5 | **2**(新 Minor 6 / 7非阻擋|
| R5-E 落地 | — | **100%** |
| 是否阻擋進 M8 開發 | 部分阻擋 | **不阻擋** |
| 整體品質 | 8.5/10 | **9.3/10** |
### 稱讚
- R5-D1 OS 通知的三個觸發點Start 失敗 / watchServer 3 次失敗 / startup pipeline 失敗)全覆蓋 + 非阻塞 goroutine 設計完美
- `startup-pipeline.md` 是一份**程式碼級**的子檔518 行 Go + JS 全部寫出Reviewer 可以直接比對
- R5-E5 階段文字零硬編碼,完全以 i18n key 交給 Design等 Design Spec v2.1 敲定就能直接填 JSON
- §11.6 的 450 MB 範圍澄清 + 悲觀情境 fallback 策略很到位
- `deletions.md` Minor 5 修正後的驗收 grep 命令可直接複製執行,防呆
### 下一步
1. 使用者確認 TDD v2.1 + Design Spec v2.1 → 即可進 M8 開發
2. **M8-1 / M8-2 / M8-3 / M8-8 可立即並行啟動**互不依賴Major 修復未觸及此四者)
3. **M8-4 / M8-4b / M8-5 / M8-9** 依賴 `DefaultPreferences()` / `sendCrashNotification()` / `StartupPipeline` / Preferences 持久化,這四者 TDD v2.1 都已 spec 完整可開工
4. Minor 6 / Minor 7 建議 M8-5 啟動前或開發過程中順手修
5. 本輪 PM 審閱完成,不需第三輪
- **審閱耗時**~1 小時(含所有子檔閱讀 + 交叉對照)