依 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>
514 lines
28 KiB
Markdown
514 lines
28 KiB
Markdown
# v2/milestone-plan.md — M8 開發 milestone 拆分
|
||
|
||
> 所屬:TDD v2 §2.7
|
||
> 版本:v2.1(2026-04-14 吸收 PM 審閱 + R5-D + R5-E)
|
||
> 目的:給 Orchestrator 調度工程師 Agent 用。每個 milestone = 一個 Reviewer 審查單位。
|
||
> 總工時估算:**~12.0 人天**(v2.0 是 10 人天;R5-E 階段化啟動 +1 天 / PM Q4 7+1 秒 shutdown modal +0.2 天 / R5-D1 OS 通知 +0.3 天 / Minor 4 WebSocket 廣播 +0.3 天 / M8-10 驗收項目擴充 +0.2 天,合計 +2 人天。與 §3 合計 12.0 一致)
|
||
|
||
---
|
||
|
||
## 1. M8 全景
|
||
|
||
v2.1 的開發總共拆成 11 個 milestone,編號 M8-1 ~ M8-10 + M8-4b。依賴關係:
|
||
|
||
```
|
||
M8-1 (砍 yt-dlp) ──────┐
|
||
├──▶ M8-4 (server lifecycle) ──▶ M8-4b (階段化啟動) ──▶ M8-5 (Wails UI 改寫) ──▶ M8-9 (boot-id + 重連)
|
||
M8-2 (砍 Mock) ────────┘ ▲
|
||
│
|
||
M8-3 (ffmpeg LGPL vendor) ─────────────────────────────────────────────────────────────────────────────────┼──▶ M8-10 (end-to-end)
|
||
│
|
||
M8-6 (source-selector 調整) ────────────────────────────────────────────────────────────────────────────────┤
|
||
│
|
||
M8-7 (Offline Overlay) ─────────────────────────────────────────────────────────────────────────────────────┤
|
||
│
|
||
M8-8 (CORS) ────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
- M8-1 / M8-2 / M8-3 / M8-8 可以同時開工(互不依賴)
|
||
- M8-4 依賴 M8-1(因為 app.go 內的 mockMode field 會在 M8-2 砍掉;但 M8-4 主要碰 server_control.go / log_buffer.go 不衝突,實務可平行)
|
||
- **M8-4b 依賴 M8-4**(需要 ServerController 已存在才能插入 pipeline hook)
|
||
- M8-5 必須等 M8-4b 完成(bindings + event schema 定義完)
|
||
- M8-6 可以獨立進行
|
||
- M8-7 依賴 M8-4(需要 boot-id server 端 + WebSocket hub,會在 M8-4 一併做)
|
||
- M8-9 依賴 M8-4 + M8-4b + M8-7
|
||
- M8-10 必須全部完成後
|
||
|
||
---
|
||
|
||
## 2. Milestone 明細
|
||
|
||
### M8-1:砍 yt-dlp 全套
|
||
|
||
**預估**:0.5 人天
|
||
|
||
**負責 Agent**:Backend Agent + Frontend Agent(同時進行,因為跨 Go + TS)
|
||
|
||
**依賴**:無
|
||
|
||
**任務**:
|
||
1. 依 `v2/deletions.md` §1 刪後端 Go(`video_source.go` / `camera_handler.go` / `router.go` / `deps/checker.go` / `main.go` 註解)
|
||
2. 依 `v2/deletions.md` §5 刪前端 TS(`source-selector.tsx` 的 URL tab / `camera-store.ts` 的 `startFromUrl` / i18n 4 個 key)
|
||
3. 依 `v2/deletions.md` §4.1 + §4.2 + §4.3 + §4.4 + §4.5 刪打包流程(Makefile / installer / bootstrap)
|
||
4. 刪除 `/vendor/yt-dlp/` 目錄(若存在)
|
||
|
||
**驗收條件**:
|
||
- `go build ./...` PASS
|
||
- `go test ./server/...` PASS
|
||
- `pnpm --dir frontend build` PASS
|
||
- `git grep -i 'yt-dlp\|ytdlp\|ResolveWithYTDLP\|StartFromURL\|classifyVideoURL\|pasteUrl\|urlPlaceholder'` 無業務程式碼 match(僅 `.autoflow/` 內的歷史文件可接受)
|
||
- `make payload-macos` 可成功(不再 `cp yt-dlp`)
|
||
|
||
**Reviewer 檢查重點**:
|
||
- `camera_handler.go` 砍掉後 import 清理乾淨
|
||
- i18n types.ts / zh-TW.ts / en.ts 三檔同步刪
|
||
- Makefile 三個平台的 `vendor-ytdlp*` target 都清
|
||
|
||
---
|
||
|
||
### M8-2:砍 Mock 模式全套
|
||
|
||
**預估**:0.5 人天
|
||
|
||
**負責 Agent**:Backend Agent + Frontend Agent
|
||
|
||
**依賴**:無(理論上可與 M8-1 平行)
|
||
|
||
**任務**:
|
||
1. 依 `v2/deletions.md` §2 + §3 刪 Go(兩個檔整檔刪、`device/manager.go` / `camera/manager.go` / `config.go` / `main.go` / `app.go` 所有 `mockMode` 條件砍掉)
|
||
2. 依 `v2/deletions.md` §6 刪前端 Mock 切換 UI + 4 個 i18n key
|
||
3. 更新 `noDevices` 文字(empty state 友善化)
|
||
|
||
**驗收條件**:
|
||
- Go + 前端 build PASS
|
||
- `git grep -i 'VISIONA_MOCK\|MockMode\|mockMode\|MockCamera\|MockDriver\|NewMockDriver\|runtimeModeMock'` 無業務程式碼 match
|
||
- 實機啟動 server(沒插 Kneron 硬體)→ Devices 頁顯示友善 empty state「未偵測到 Kneron 裝置」
|
||
|
||
**Reviewer 檢查重點**:
|
||
- `NewManager` 簽名改動後所有呼叫點都更新
|
||
- 舊 `--mock` / `--mock-camera` / `--mock-devices` CLI flag 真的拿掉(server `--help` 不再列)
|
||
- 任何剩下的 test 檔案不再依賴 mockMode
|
||
|
||
---
|
||
|
||
### M8-3:ffmpeg LGPL vendor 切換
|
||
|
||
**預估**:1.5 人天
|
||
|
||
**負責 Agent**:DevOps Agent(主)+ Backend Agent(支援 macOS build 環境)
|
||
|
||
**依賴**:無
|
||
|
||
**任務**:
|
||
1. **Windows / Linux**(0.5 人天):依 `v2/ffmpeg-lgpl.md` §3 + §4 修改 `Makefile` 的 URL 與 vendor target,實測 `make vendor-ffmpeg-windows` / `vendor-ffmpeg-linux` 可成功
|
||
2. **macOS build script**(1 人天):
|
||
- 依 `v2/ffmpeg-lgpl.md` §2.3 新增 `vendor-ffmpeg-macos-build` Makefile target
|
||
- 第一次跑:下載 ffmpeg n7.1 source、算 sha256 填進 Makefile、跑 configure + make、驗證 binary size < 25 MB、驗證 `ffmpeg -version` 不含 `--enable-gpl` / libx264 / libx265
|
||
- 把 ffmpeg + ffprobe + COPYING.LGPLv3 + BUILD.md 進 git commit 到 `vendor/ffmpeg/macos/`
|
||
- 修改 `.gitignore` 讓 `vendor/ffmpeg/macos/` 成為 git tracked
|
||
- 修改 `vendor-ffmpeg` target 改為「驗證 binary 存在且是 LGPL」
|
||
3. **Payload 階段同步**(§7):三個平台的 `payload-*` target 改為 copy `ffmpeg + ffprobe + COPYING.LGPLv3`,不再 copy yt-dlp(M8-1 會處理刪)
|
||
4. **Installer 同步**(§8):
|
||
- `installer/windows/visiona-local.iss` 新增 ffprobe 和 LGPL license 行
|
||
- `installer/linux/build-appimage.sh` 迭代 tool 改為 `ffmpeg ffprobe`
|
||
|
||
**驗收條件**:
|
||
- `make vendor-ffmpeg` PASS(macOS),binary size OK(見 `v2/ffmpeg-lgpl.md` §10 驗收表)
|
||
- `vendor/ffmpeg/macos/ffmpeg -version | grep -- --enable-gpl` 無輸出
|
||
- `spctl --assess --verbose vendor/ffmpeg/macos/ffmpeg` → accepted
|
||
- `make vendor-ffmpeg-windows` + `vendor-ffmpeg-linux` 可成功,檔案解出後 version 無 `--enable-gpl`
|
||
- `make payload-macos` 成功,`payload/darwin/bin/` 內有 `ffmpeg / ffprobe / ffmpeg-COPYING.LGPLv3`
|
||
- 實測用 `vendor/ffmpeg/macos/ffmpeg` 解 5 種格式(mp4/avi/mov/mpeg/mpg)正常
|
||
|
||
**Reviewer 檢查重點**:
|
||
- `vendor/ffmpeg/macos/BUILD.md` 內容正確、可重現
|
||
- 開發者用單一 `make vendor-ffmpeg-macos-build` 指令真的能從零 build 出來(不依賴本機 cached state)
|
||
- `.gitignore` 的 `!` rule 順序正確
|
||
- Makefile 的 configure flags 與 `v2/ffmpeg-lgpl.md` §2.3 完全一致
|
||
|
||
---
|
||
|
||
### M8-4:Server lifecycle + log buffer + bindings + OS notify + Preferences
|
||
|
||
**預估**:2 人天(v2.0 是 1.5 天;+0.3 天 R5-D1 OS 通知、+0.2 天 PM Q4 7+1 秒 shutdown modal)
|
||
|
||
**負責 Agent**:Backend Agent(Go / Wails)
|
||
|
||
**依賴**:M8-1(避免 mockMode 欄位被砍後衝突)。實務上可與 M8-2 平行。
|
||
|
||
**任務**:
|
||
|
||
1. **新增檔案**(`v2/control-panel.md` §7 + §4):
|
||
- `visiona-local/log_buffer.go`(~120 行 ring buffer)
|
||
- `visiona-local/server_control.go`(~280 行 ServerController + state machine + startServerV2 + logPump + **7+1 秒 shutdown modal timer**,見 `server-lifecycle.md` §8.2)
|
||
- `visiona-local/preferences.go`(~80 行)— **R5-D2**:必須實作 `DefaultPreferences()` 依 `runtime.GOOS` 分平台預設(macOS/Windows `AutoOpenBrowser=true`、Linux `false`)+ atomic write-rename(`server-lifecycle.md` §11.3)
|
||
- `visiona-local/notify.go`(~100 行)— **R5-D1**:三平台 OS 通知,macOS `osascript display notification` / Linux `notify-send` / Windows PowerShell BurntToast + `msg *` fallback,詳見 `server-lifecycle.md` §10
|
||
2. **修改 `visiona-local/app.go`**:
|
||
- 新增 `ctrl` / `logBuf` / `prefs` 欄位(**不要** `autoOpenedThisSession` — R5-D3 已砍掉 per-session-once 概念)
|
||
- 砍 `GetBootstrapStatus` / `setBootstrapStatus` / `bootstrapStatus` 欄位
|
||
- 改 `watchServer()` 失敗後行為(不 os.Exit,改設 Error state + **goroutine 呼叫 `sendCrashNotification`** — R5-D1)
|
||
- 新增 12 個 Wails bindings(`v2/control-panel.md` §4.2)
|
||
3. **修改 `visiona-local/main.go`**:
|
||
- 加 `OnBeforeClose` handler
|
||
- `shutdownGracePeriod` 5 s → **7 s**(PM Q4 定案,不是 10 s)
|
||
4. **修改 `server/main.go` + `system_handler.go` + `router.go`**:
|
||
- 新增 boot-id 生成(使用 `crypto/rand` 16 bytes → hex,Architect Q3 決定,不引 google/uuid)
|
||
- 新增 `GET /api/system/boot-id` endpoint
|
||
- **Minor 4**:server 新增 WebSocket hub 廣播 `server:shutdown-imminent` 的能力(新增 `/ws/system` endpoint 或擴充現有 `/ws/server-logs`);Wails 的 `ctrl.Stop()` 在開始 SIGTERM 前透過 HTTP 或 server 內部 API 觸發此廣播
|
||
- **對齊 shutdown timeout**:`server/main.go:166` 的 `shutdownFn` timeout 10 s → **6 s**(Wails 7 s 減 1 s,確保 server 先完成 cleanup)
|
||
|
||
**驗收條件**:
|
||
- `cd visiona-local && go build .` PASS
|
||
- `cd server && go build ./...` PASS
|
||
- 12 個 bindings 在 `visiona-local/frontend/wailsjs/go/main/App.d.ts` 正確生成
|
||
- 手動測試:新 bindings 可從 Wails dev 模式的 devtools console 呼叫
|
||
- 手動測試:Start → server 啟動 → 看 log 噴進 LogBuffer → 用 `GetRecentLogs(20)` 取回
|
||
- 手動測試:Stop → 1 秒內顯示「停止中…」modal(若 server 未秒 exit)+ 7 秒內 SIGKILL(若卡死)
|
||
- 手動測試:Restart → state: Running → Stopping → Stopped → Starting → Running
|
||
- 手動測試:殺 server process → watchServer 3 次失敗後 state → Error + **收到 OS 原生通知**(三平台都要)
|
||
- **R5-D2 驗收**:全新使用者第一次在 macOS/Windows 開 app → `preferences.json` 不存在 → `DefaultPreferences()` 回傳 `AutoOpenBrowser=true`;Linux 上回傳 `false`
|
||
- `curl http://127.0.0.1:<port>/api/system/boot-id` 回傳 JSON 含 bootId(hex 32 字元)
|
||
- WebSocket `server:shutdown-imminent` 廣播:用 websocat 連 `/ws/system`,按 Stop 後秒收到廣播
|
||
|
||
**Reviewer 檢查重點**:
|
||
- LogBuffer thread-safety(mutex 正確)
|
||
- ServerController 的 `txMu` / `mu` 互斥
|
||
- logPump 在 server 死掉 / pipe EOF / 高頻 stdout 下都不 leak goroutine
|
||
- `parseLogLevel` 不會 panic on 空字串或非 ASCII
|
||
- boot-id 生成使用 `crypto/rand` + `encoding/hex`(不引 google/uuid)
|
||
- Preferences JSON atomic write(tmp + rename,見 §11.3)
|
||
- `DefaultPreferences()` 依 `runtime.GOOS` 正確切換(Linux 分支有測試)
|
||
- `sendCrashNotification` 三平台檔案(darwin/linux/windows build tag)齊全且不阻塞
|
||
- shutdown modal 的 1 秒 timer 與 7 秒 grace timer 正確(goroutine 泄漏檢查)
|
||
- server 端 WebSocket hub 廣播邏輯與 Wails 端呼叫的契約(傳遞 `reason` 欄位)
|
||
|
||
---
|
||
|
||
### M8-4b:階段化啟動管線(R5-E)
|
||
|
||
**預估**:1 人天(v2.0 未拆出,v2.1 新增)
|
||
|
||
**負責 Agent**:Backend Agent(Go / Wails)+ Frontend Agent(vanilla JS startup panel)
|
||
|
||
**依賴**:M8-4(需要 ServerController + bindings)
|
||
|
||
**任務**:依 `v2/startup-pipeline.md`(若該檔不存在則落在 `control-panel.md` §3.1 + `server-lifecycle.md` §2.1)
|
||
|
||
1. **新增 `visiona-local/startup_pipeline.go`**(~180 行):
|
||
- `StartupPipeline` struct(含 currentStage / stageStartedAt / startedAt / softTimeout / hardTimeout)
|
||
- `NewStartupPipeline(ctx)` / `Start(stage int)` / `Complete(stage int)` / `Fail(stage int, err)` / `Ready()` 方法
|
||
- `watcher(ctx)` goroutine:每秒檢查 soft timeout (20 s) + hard timeout (60 s),emit `startup:stage-timeout` / `startup:error` event
|
||
- 4 個 event schema:`startup:progress` / `startup:stage-timeout` / `startup:error` / `startup:ready`(見 `v2/startup-pipeline.md` §1)
|
||
2. **修改 `visiona-local/app.go`** 的 `startup(ctx)`:
|
||
- 在 `OnStartup` 最前面初始化 pipeline 並 Start(1)
|
||
- 每個既有階段的對應處呼叫 `pipeline.Complete(N)` + `pipeline.Start(N+1)`
|
||
- 6 個階段對應:1=Wails init、2=Python runtime、3=server binary spawn + health check、4=first `/api/devices` 查詢、5=OpenInBrowser call、6=WebSocket 首個 client connect
|
||
- 最後 `pipeline.Ready()` → emit `startup:ready`
|
||
3. **修改 server 端**:
|
||
- `pkg/ws`(或對應的 WebSocket hub)新增 `OnClientConnected` callback,讓 Wails 能收到「第一個 client 連上」的通知(透過 WebSocket 或 HTTP poll)
|
||
- Wails 的 `startup_pipeline.go` 訂閱此通知完成階段 6
|
||
4. **失敗處理**:任一階段失敗 → `pipeline.Fail(stage, err)` → `ctrl.setState(Error, ...)` + `sendCrashNotification` + emit `startup:error`
|
||
5. **非阻塞**:所有 `EventsEmit` 呼叫都走 buffered channel 或 fire-and-forget goroutine,不阻塞啟動流程
|
||
6. **前端(Wails 控制台)**:
|
||
- `visiona-local/frontend/components/startup-panel.js`(~100 行)
|
||
- 訂閱 4 個 event 更新 DOM
|
||
- 收到 `startup:ready` 淡出(300 ms ease)
|
||
- i18n key 從 `i18n/zh-TW.json` / `en-US.json` 讀(文案由 Design Spec v2.1 敲定)
|
||
|
||
**驗收條件**:
|
||
- `cd visiona-local && go build .` PASS
|
||
- 正常冷啟動(樂觀情境):4 s 內看到「啟動進度面板」6 階段逐一 completed,最後淡出顯示主控台
|
||
- 慢啟動情境:mock 階段 2 jam 25 秒 → 看到「階段 2 正在重試 …」副文字
|
||
- 失敗情境:mock 階段 3 fail → 看到進度面板變紅、切 Error state、收到 OS 通知、發 `startup:error` event
|
||
- 總時 > 60 秒情境:mock 每個階段都等 12 秒 → 60 秒後進 Error state(watcher 總時檢查有效)
|
||
- 日常啟動(非首次):~3 s 內就緒,符合 PM AC-2.1
|
||
|
||
**Reviewer 檢查重點**:
|
||
- watcher goroutine 在 pipeline.Ready() / Fail() 後正確停止,不 leak
|
||
- `EventsEmit` 非阻塞(若 Wails IPC 慢也不拖啟動)
|
||
- 6 階段的 labelKey 與 design-spec v2.1 一致
|
||
- 失敗後 Error state 透過 ctrl.setState 觸發,不繞過 state machine
|
||
- startup-panel.js 的淡出動畫不會卡住主控台 UI
|
||
|
||
---
|
||
|
||
### M8-5:Wails 控制台 vanilla UI 改寫
|
||
|
||
**預估**:2 人天
|
||
|
||
**負責 Agent**:Frontend Agent(vanilla JS)
|
||
|
||
**依賴**:M8-4(需要 bindings + events)
|
||
|
||
**任務**:
|
||
1. **改寫 `visiona-local/frontend/index.html`** 成控制台 layout(`v2/control-panel.md` §3)
|
||
2. **改寫 `visiona-local/frontend/app.js`** 成控制台主程式(§5)
|
||
3. **改寫 `visiona-local/frontend/style.css`** 新增 status card / log panel / action bar / preferences 樣式,並加 light/dark mode CSS variables
|
||
4. **新增 components**:
|
||
- `components/status-card.js`
|
||
- `components/log-panel.js`(含 virtual scroll lite + batch render)
|
||
- `components/action-bar.js`(含 disable 矩陣)
|
||
- `components/preferences.js`
|
||
5. **新增 i18n**:
|
||
- `i18n/en-US.json` / `zh-TW.json`
|
||
- `i18n/loader.js`
|
||
6. **新增 icons**:`icons/*.svg` × 6
|
||
|
||
**驗收條件**:
|
||
- Wails dev 模式下開 app:
|
||
- ✅ 看到控制台 UI(status / log / actions / preferences),不是 splash / blank / Next.js
|
||
- ✅ Start 按鈕能啟動 server,狀態卡片變 Running 綠色 badge
|
||
- ✅ Log panel 即時顯示 server stdout(看到 Gin log)
|
||
- ✅ Stop 按鈕能停 server,badge 變灰
|
||
- ✅ Restart 按鈕能重啟(狀態過程正確)
|
||
- ✅ Open in Browser 開瀏覽器到正確 URL
|
||
- ✅ Reveal Logs 開啟 `<dataDir>/logs/` 資料夾
|
||
- ✅ Clear Logs 清畫面但不動磁碟檔
|
||
- ✅ Preferences 切 `openBrowserOnStart` 持久化到 `preferences.json`
|
||
- ✅ Dark mode 跟隨系統(macOS 切換 Dark Mode 後控制台自動變色)
|
||
- ✅ Log panel 按 Pause 後自動捲動停止,解除後恢復
|
||
- `make wails-macos` 能 build 出 .app,打開後看到控制台 UI(M7-B 教訓)
|
||
|
||
**Reviewer 檢查重點**:
|
||
- vanilla JS 沒引入任何 npm 依賴(`package.json` 不存在於 visiona-local/frontend/)
|
||
- Log panel 在高頻(1000 行/秒)下不會 freeze UI(batch render 有效)
|
||
- i18n loader SSR-safe(不適用,但 Wails 沒 SSR 概念)
|
||
- icons/ 下的 SVG 符合 inline 使用的簡潔規則(viewBox + single path)
|
||
- Action bar 在每個 state 下 button enable/disable 正確(disable 矩陣全部驗證)
|
||
|
||
---
|
||
|
||
### M8-6:Web UI source-selector + 副檔名擴充
|
||
|
||
**預估**:0.5 人天
|
||
|
||
**負責 Agent**:Frontend Agent
|
||
|
||
**依賴**:無(與其他 milestone 平行)
|
||
|
||
**任務**:
|
||
1. `frontend/src/components/camera/source-selector.tsx` 依 `v2/deletions.md` §5.1 移除 URL tab + mode toggle
|
||
2. 把 `accept=".mp4,.avi,.mov"` 改為 `accept=".mp4,.avi,.mov,.mpeg,.mpg"`
|
||
3. i18n 新增 `videoFormats` key 或改既有 `mp4AviMov`
|
||
4. **後端同步**:`server/internal/api/handlers/camera_handler.go:251` 把 extension whitelist 從 `.mp4 / .avi / .mov` 擴充為 5 個:
|
||
```diff
|
||
-if ext != ".mp4" && ext != ".avi" && ext != ".mov" {
|
||
- c.JSON(400, gin.H{"success": false, "error": gin.H{"code": "BAD_REQUEST", "message": "only MP4/AVI/MOV files are supported"}})
|
||
+if ext != ".mp4" && ext != ".avi" && ext != ".mov" && ext != ".mpeg" && ext != ".mpg" {
|
||
+ c.JSON(400, gin.H{"success": false, "error": gin.H{"code": "BAD_REQUEST", "message": "only MP4/AVI/MOV/MPEG/MPG files are supported"}})
|
||
}
|
||
```
|
||
|
||
**驗收條件**:
|
||
- 前端 build PASS
|
||
- 手動測試:5 種副檔名都能上傳且正常開始推論
|
||
- UI 不再有「Paste URL」按鈕
|
||
|
||
**Reviewer 檢查重點**:
|
||
- handler 裡的 extension 比對是 `strings.ToLower` 後再比,大小寫不敏感
|
||
- 錯誤訊息中英文同步
|
||
|
||
---
|
||
|
||
### M8-7:Web UI Server Offline Overlay
|
||
|
||
**預估**:1 人天
|
||
|
||
**負責 Agent**:Frontend Agent
|
||
|
||
**依賴**:M8-4(需要 server 端 boot-id endpoint)
|
||
|
||
**任務**:依 `v2/web-ui-offline-overlay.md` §4 的 7 個檔案清單:
|
||
1. 新增 `frontend/src/stores/system-store.ts`
|
||
2. 新增 `frontend/src/hooks/use-boot-id-watcher.ts`
|
||
3. 新增 `frontend/src/components/server-offline-overlay.tsx`
|
||
4. 新增 `frontend/src/components/boot-id-watcher-mount.tsx`
|
||
5. 修改 `frontend/src/app/layout.tsx`(掛 overlay + watcher)
|
||
6. 修改 `frontend/src/lib/i18n/{types,zh-TW,en}.ts`(新增 serverOffline 區塊)
|
||
|
||
**驗收條件**:
|
||
- `pnpm --dir frontend build` PASS(含 SSR 相容性驗證)
|
||
- 手動測試:
|
||
- 正常啟動沒 overlay
|
||
- 關 Wails 視窗後 15 s 內瀏覽器 tab 顯示 overlay
|
||
- 重新開 Wails app → Web UI 的「重試」按鈕有效(overlay 消失)
|
||
- 控制台按 Restart → boot-id 變 → 自動 `window.location.reload()`
|
||
- 切到別的 tab 5 分鐘再切回 → polling 立即 probe 不用等
|
||
|
||
**Reviewer 檢查重點**:
|
||
- Zustand store 的 failures state 遞增正確
|
||
- `use-boot-id-watcher.ts` 的 cleanup(useEffect return)正確 cancel
|
||
- `AbortSignal.timeout(3000)` 在舊瀏覽器相容性(若需支援 Chrome < 103 則 fallback)
|
||
- Overlay 元件 a11y(`role="alertdialog"` + `aria-labelledby`)
|
||
- i18n 新增 key 三個檔同步(types / zh-TW / en)
|
||
|
||
---
|
||
|
||
### M8-8:CORS + origin check middleware
|
||
|
||
**預估**:0.5 人天
|
||
|
||
**負責 Agent**:Backend Agent
|
||
|
||
**依賴**:無
|
||
|
||
**任務**:依 `v2/cors-security.md` §4 + §5:
|
||
1. 覆寫 `server/internal/api/middleware.go`(白名單版)
|
||
2. 新增 `server/internal/api/ws/origin.go`
|
||
3. 修改所有 WS handler 的 upgrader CheckOrigin
|
||
4. 新增 `server/internal/api/middleware_test.go` 單元測試(`TestIsAllowedOrigin`)
|
||
5. 在 `router.go` 掛 `requireSameOriginOrNoOrigin` middleware 到 `/api/*`
|
||
|
||
**驗收條件**:
|
||
- `go test ./server/internal/api/... -run TestIsAllowedOrigin` PASS
|
||
- `v2/cors-security.md` §9 所有 curl 驗收條件通過
|
||
- WS websocat 測試:白名單 origin 可連、非白名單擋
|
||
- 既有 Next.js Web UI 的 fetch 仍正常運作(same-origin + white list)
|
||
|
||
**Reviewer 檢查重點**:
|
||
- `isAllowedOrigin` 對 edge case 處理正確(空字串 / null / https / 不合規 URL)
|
||
- 不 accidentally 把 `X-Relay-Token` header 保留(relay 早已砍)
|
||
- WS upgrade 在非白名單 origin 下回 403,不是靜默失敗
|
||
- Middleware 套用順序(middleware.go 的 CORSMiddleware 要在 requireSameOriginOrNoOrigin 之前)
|
||
|
||
---
|
||
|
||
### M8-9:Boot-ID 端到端整合 + Restart 重連
|
||
|
||
**預估**:1 人天
|
||
|
||
**負責 Agent**:Backend Agent + Frontend Agent
|
||
|
||
**依賴**:M8-4 + M8-7
|
||
|
||
**任務**:
|
||
1. **整合驗證**:M8-4 的 server 端 boot-id API + M8-7 的前端 polling 串起來
|
||
2. **Restart 情境驗證**:
|
||
- 開 app → Open in Browser
|
||
- 在 Wails 控制台按 Restart
|
||
- 瀏覽器 tab 在 3-5 s 內自動 reload
|
||
- Reload 後 server 是新的 port 也能正常連(因為 Next.js 是 static export,URL path 不變)
|
||
3. **自動開瀏覽器**(R5-4 + R5-D2 + R5-D3):
|
||
- **R5-D2(分平台預設)**:macOS/Windows 預設 `AutoOpenBrowser=true`;Linux 預設 `false`
|
||
- **R5-D3(每次都開)**:只要 `AutoOpenBrowser=true`,每次 Start/Restart 成功都呼叫 `OpenInBrowser("")` — **取消**原 v2.0 的 per-session-once 設計
|
||
- 使用者在 Preferences 切換 → 立即持久化到 `preferences.json`(atomic write)
|
||
4. **Restart 期間 port 行為**(F-2 強制保留):
|
||
- Restart 不允許 port fallback(`startWithPort(oldPort, forceMatch=true)`)
|
||
- 舊 port 被佔用 → 進 Error state + 發 OS 通知
|
||
- 正常 Restart → 新 server 用原 port → 瀏覽器 tab 偵測 boot-id 變 → reload → URL 原 port 仍有效 → 自動連上
|
||
- **不會**發生 v2.0 所述「port 變動後瀏覽器連不上」的情境
|
||
|
||
**驗收條件**:
|
||
- 關機情境(Wails 關 → server 停 → 瀏覽器 tab):**WebSocket shutdown-imminent 秒內顯示 overlay**(不是 15 s 了,Minor 4)
|
||
- Restart 情境(Wails 按 Restart):3-5 s 內瀏覽器自動 reload,**URL 原 port 仍有效**(F-2),UI 正常
|
||
- 首次開 app(macOS/Windows):瀏覽器自動跳出新 tab
|
||
- 首次開 app(Linux):瀏覽器**不**自動開(R5-D2 預設 false)
|
||
- Preferences 關閉後再開 app:瀏覽器不自動開
|
||
- **多次按 Restart**:若 `AutoOpenBrowser=true`,每次 Restart **都會呼叫 `OpenInBrowser`**(R5-D3)。OS 瀏覽器通常聚焦既有 tab 而不是開新 tab,使用者可接受
|
||
- R5-E 啟動進度面板:Starting 期間顯示 6 階段進度,完成後淡出
|
||
|
||
**Reviewer 檢查重點**:
|
||
- `app.go` 中**不存在** `autoOpenedThisSession` 欄位(v2.0 已砍)
|
||
- Restart 呼叫 `OpenInBrowser` 的行為有在 M8-9 手動測試紀錄中驗證
|
||
- `DefaultPreferences()` 三平台分支在單元測試或手動測試中涵蓋
|
||
- Restart 的 port 強制保留邏輯正確(錯的 mock 情境下會進 Error state 而非 fallback)
|
||
- boot-id 在 server 重啟後真的變(不會意外 memoize 舊值)
|
||
- polling 的 `consecutiveFailures` 在 Restart 期間不會錯誤累積到 3(Restart 約 3 s,只有 0-1 次失敗)
|
||
|
||
---
|
||
|
||
### M8-10:端到端 build + smoke test
|
||
|
||
**預估**:1 人天
|
||
|
||
**負責 Agent**:DevOps Agent + Testing Agent(QA)
|
||
|
||
**依賴**:M8-1 到 M8-9 全部完成
|
||
|
||
**任務**:
|
||
1. **macOS**:`make clean-all && make dmg` → `dist/visiona-local.dmg`
|
||
- 安裝到全新 mac(或 VM)
|
||
- 8 核心驗收(v2.1 擴充):
|
||
- ✅ 打開 app 先看到**啟動進度面板**(6 階段,R5-E),完成後淡出顯示**控制台 UI**(不是 splash / wizard / 白畫面)
|
||
- ✅ 點 Open in Browser 後瀏覽器載入 Next.js Web UI
|
||
- ✅ Web UI 沒有 URL tab(source-selector 清乾淨)
|
||
- ✅ Web UI 沒有 Mock 模式切換(Settings > Hardware 乾淨)
|
||
- ✅ 點 Stop 後瀏覽器**秒內看到 Offline Overlay**(WebSocket 廣播,不是 15 s 才顯示)
|
||
- ✅ **手動殺 server process → 收到 macOS 原生通知 + 控制台 Error banner**(R5-D1)
|
||
- ✅ **R5-D2:首次開 app 時 `preferences.json` 不存在,macOS 預設自動開瀏覽器**(Linux 相對要反過來驗證)
|
||
- ✅ 按 Restart 兩次,每次都會觸發 `OpenInBrowser`(R5-D3,砍掉 per-session-once)
|
||
2. **Windows**:在 Windows 機器上 `make clean-all && make exe`
|
||
- 同樣 5 核心驗收
|
||
- **注意**:R5-7 同意 M7 Windows 先不管,但這次 M8-10 要順帶驗證(做完再驗)
|
||
3. **Linux**:在 Ubuntu 上 `make clean-all && make appimage`
|
||
- 同樣 5 核心驗收
|
||
4. **LGPL 稽核**:三個平台的 installer 安裝後,`<install>/bin/` 下都有 `ffmpeg + ffprobe + ffmpeg-COPYING.LGPLv3`
|
||
5. **Installer size 對照**:
|
||
- macOS .dmg:預期 ~80-100 MB(v1 是 220 MB,砍 yt-dlp 35 MB + 砍 GPL ffmpeg 77 MB 換成 LGPL ~20 MB = 約 128 MB 降到 ~100 MB)
|
||
- Windows .exe:預期 ~280-320 MB(v1 是 ~380 MB)
|
||
- Linux .AppImage:預期 ~240-280 MB(v1 是 ~317 MB)
|
||
6. **回歸測試**:
|
||
- Kneron KL520 / KL720 實機跑推論(若有硬體)
|
||
- 5 種副檔名上傳影片
|
||
- 批次影像上傳
|
||
- Camera(webcam)串流
|
||
|
||
**驗收條件**:
|
||
- 三平台 installer 全部能裝、能啟動、能進入控制台、能 Open Browser、能跑推論
|
||
- LGPL 合規稽核:`grep -r 'enable-gpl' vendor/ffmpeg/` 無輸出
|
||
- 5 核心驗收 checklist 全綠
|
||
- Installer size 在預期範圍內
|
||
|
||
**Reviewer 檢查重點**:
|
||
- 驗收 checklist 每一項都有實測截圖 / log
|
||
- 回歸測試涵蓋 PRD v2 所列的核心 user story
|
||
|
||
---
|
||
|
||
## 3. 風險與緩衝
|
||
|
||
- **M8-3 macOS 自 build ffmpeg 可能踩坑**(configure flag 組合、pkg-config 環境、codesign):預估 1.5 人天但可能實際花 2 人天。buffer 放在 M8-10 的 1 人天裡
|
||
- **M8-4b R5-E 6 階段 event emit 的非阻塞保證**:Wails v2 IPC 若慢,可能把啟動流程本身拖慢。需在 M8-4b 後做 mock 測試(每個階段插 dummy 延遲)確認 watcher goroutine 與 event emit 不互相阻塞
|
||
- **M8-4 OS 通知三平台實測**(R5-D1):macOS 首次呼叫會彈出通知授權對話框;Windows 沒裝 BurntToast 會 fallback `msg *`(某些版本可能 block);Linux 需要 `libnotify-bin` 已安裝。這三者都需要實機驗證,預估 0.3 天
|
||
- **M8-5 控制台 UI 的 dark mode** 可能需要反覆調整:若實測後使用者不喜歡預設配色,留給 Design Agent 一輪調整(不在本次範圍內)
|
||
- **M8-4 高頻 log 壓測**(R-v2-3):若 Wails v2 EventsEmit 真的 flaky,要切 micro-batch 升級成 throttling + coalescing,可能再多 0.5 人天
|
||
|
||
### 總工時重估
|
||
|
||
| Milestone | v2.0 | v2.1 | 差異原因 |
|
||
|-----------|------|------|---------|
|
||
| M8-1 | 0.5 | 0.5 | — |
|
||
| M8-2 | 0.5 | 0.5 | — |
|
||
| M8-3 | 1.5 | 1.5 | — |
|
||
| M8-4 | 1.5 | **2.0** | +0.3 R5-D1 OS 通知、+0.2 Q4 7+1 秒 shutdown modal |
|
||
| **M8-4b** | — | **1.0** | **新增,R5-E 階段化啟動** |
|
||
| M8-5 | 2.0 | 2.0 | — |
|
||
| M8-6 | 0.5 | 0.5 | — |
|
||
| M8-7 | 1.0 | **1.3** | +0.3 Minor 4 WebSocket shutdown-imminent 訂閱 |
|
||
| M8-8 | 0.5 | 0.5 | — |
|
||
| M8-9 | 1.0 | 1.0 | — |
|
||
| M8-10 | 1.0 | 1.2 | +0.2 核心驗收項目從 5 個擴為 8 個 |
|
||
| **合計** | **10.0** | **12.0** | **+2 人天** |
|
||
|
||
加上 buffer(M8-3 可能踩坑 + M8-4 高頻 log 壓測可能重構)~1 人天 → **建議 Orchestrator 對使用者回報 ~13 人天**。
|
||
|
||
---
|
||
|
||
## 4. 交棒給 Orchestrator 的清單
|
||
|
||
| Milestone | 誰 | 觸發條件 |
|
||
|-----------|-----|---------|
|
||
| M8-1 | Backend + Frontend(並行)| 使用者確認 TDD v2.1 |
|
||
| M8-2 | Backend + Frontend(並行)| 同 M8-1 |
|
||
| M8-3 | DevOps + Backend(macOS host 可用)| 同上,不依賴 M8-1/2 |
|
||
| M8-4 | Backend | M8-1 + M8-2 砍完 |
|
||
| **M8-4b** | **Backend + Frontend(vanilla JS)** | **M8-4 完成(需要 ServerController + WebSocket hub)** |
|
||
| M8-5 | Frontend | M8-4b bindings + event schema 可用 |
|
||
| M8-6 | Frontend | 可與 M8-1/2/5 平行 |
|
||
| M8-7 | Frontend | M8-4 boot-id endpoint + WebSocket `/ws/system` 就緒 |
|
||
| M8-8 | Backend | 可獨立進行 |
|
||
| M8-9 | Backend + Frontend | M8-4 + M8-4b + M8-7 完成 |
|
||
| M8-10 | DevOps + Testing | 全部完成 |
|
||
|
||
**提醒**:每個 milestone 完成後,Orchestrator 啟動 Reviewer 審查,審查通過才進下一個 milestone(per 根目錄 CLAUDE.md 「強制 Review 規則」)。
|