# Reviewer 審查 M8-1 + M8-2 合併結果(2026-04-15) ## 摘要 - **總結論**:✅ **通過**(零 Major / 零 Critical;只有若干 Minor 提醒) - **M8-1(砍 yt-dlp)**:✅ 完整砍除,8 項 deletions.md 條目全達成 - **M8-2(砍 Mock)**:✅ 完整砍除,11 項 deletions.md 條目全達成 - **是否阻擋 M8-4**:❌ 不阻擋。M8-4 可以開動(mockMode field 已清、videoIsURL 已清、StartFromURL 已清,M8-4 做 server lifecycle 時無衝突) - **額外觀察**:working tree 同時包含 **M8-3(ffmpeg LGPL 切換)** 的改動(Makefile / README / installer / scripts / build.yml),該部分依使用者指示**不在本次 review 範圍** --- ## A. 砍除完整性 ### A1. M8-1 yt-dlp 砍除清單 | # | 項目 | 預期 | 實測 | 證據 | |---|------|------|------|------| | 1 | `ResolveWithYTDLP` function | 消失 | ✅ | `video_source.go` diff -91..-140 整段 delete | | 2 | `friendlyYTDLPError` function | 消失 | ✅ | 同上 | | 3 | `NewVideoSourceFromURL` / `...WithSeek` | 消失 | ✅ | `video_source.go` diff -79..-86 | | 4 | `VideoSource.isURL` field + `newVideoSource` 的 `isURL` 參數 | 消失 | ✅ | `video_source.go` L63/L68 簽名已簡化 | | 5 | `ytdlpHosts` map | 消失 | ✅ | `camera_handler.go` -341..-373 | | 6 | `urlKind` enum / `urlYTDLP` / `urlDirect` / `urlBad` | 消失 | ✅ | 同上 | | 7 | `classifyVideoURL()` function | 消失 | ✅ | 同上 | | 8 | `StartFromURL` handler | 消失 | ✅ | -408..-497 整段 delete | | 9 | `CameraHandler.videoIsURL` field | 消失 | ✅ | `camera_handler.go` L33/L303/L536 三處清除 | | 10 | `handleVideoSeek` 的 `videoIsURL` 分支 | 消失 | ✅ | L566 簡化為直接呼叫 `NewVideoSourceWithSeek` | | 11 | `/media/url` route | 消失 | ✅ | `router.go:83` delete | | 12 | `deps/checker.go` yt-dlp check | 消失 | ✅ | `checker.go` -30..-32 | | 13 | `main.go` 註解改 ffmpeg/ffprobe | ✅ | L88 | 註解已改 | | 14 | frontend `startFromUrl` function | 消失 | ✅ | `camera-store.ts` -167..-196 | | 15 | frontend `videoMode` / `videoUrl` state | 消失 | ✅ | `source-selector.tsx` -51..-52 | | 16 | frontend `handleUrlSubmit` function | 消失 | ✅ | `source-selector.tsx` -94..-96 | | 17 | frontend URL tab UI(mode toggle + input + help) | 消失 | ✅ | `source-selector.tsx` -183..-253 | | 18 | i18n `pasteUrl` / `urlPlaceholder` / `urlHelpText` / `cannotOpenVideoUrl` | 消失 | ✅ | `types.ts` / `zh-TW.ts` / `en.ts` 三檔同步 | | 19 | i18n `camera.uploadFile`(mode 切換用) | 消失 | ✅ | M8-1 自補:三檔同步刪(本來 deletions.md 沒列,但砍完 mode toggle 就 orphan) | | 20 | Makefile `vendor-ytdlp` / `vendor-ytdlp-windows` / `vendor-ytdlp-linux` | 消失 | ✅ | `Makefile` 全檔無 `yt-dlp` / `ytdlp` / `YTDLP` 字樣 | | 21 | installer/windows visiona-local.iss yt-dlp.exe 行 | 消失 | ✅ | L76 已 delete | | 22 | installer/linux build-appimage.sh yt-dlp loop | 消失 | ✅ | L61 `for tool in ffmpeg ffprobe` | | 23 | scripts/bootstrap-linux.sh `vendor-ytdlp-linux` | 消失 | ✅ | L57 | | 24 | scripts/bootstrap-windows.ps1 `vendor-ytdlp-windows` | 消失 | ✅ | L200 | | 25 | visiona-local/app.go `locateBundleBinDir` 的 `yt-dlp` 字串 | 改為 `ffprobe` | ✅ | `app.go:871` fileExists("ffprobe") | | 26 | visiona-local/app.go `startServer` 註解「ffmpeg / yt-dlp」 | 改為 `ffmpeg / ffprobe` | ✅ | L480 | | 27 | README.md yt-dlp 三處敘述 | 消失 | ✅ | L18, L74, L128, L131, L179 五處清除(M8-1 自補) | | 28 | .github/workflows/build.yml `vendor-ytdlp-*` | 消失 | ✅ | L135/L233 | **A1 結論**:M8-1 砍除 **100% 達成**,且主動自補了 4 個 deletions.md 未明列的位置(README 五處、workflow、app.go bundle dir、i18n uploadFile orphan key)。 ### A2. M8-2 Mock 砍除清單 | # | 項目 | 預期 | 實測 | 證據 | |---|------|------|------|------| | 1 | `server/internal/driver/mock/mock_driver.go` | 整檔刪 | ✅ | `git status: deleted` | | 2 | `server/internal/driver/mock/` 目錄 | 刪 | ✅ | `find server/internal/driver/mock` 無 | | 3 | `server/internal/camera/mock_camera.go` | 整檔刪 | ✅ | `git status: deleted` | | 4 | `VISIONA_MOCK` env 偵測 | 消失 | ✅ | `grep -r VISIONA_MOCK` 0 match | | 5 | `config.MockMode` / `MockCamera` / `MockDeviceCount` 欄位 | 消失 | ✅ | `config.go` L19..L26 簡化 | | 6 | `--mock` / `--mock-camera` / `--mock-devices` flag | 消失 | ✅ | `config.go` L36..L38 已刪 | | 7 | `device.Manager.mockMode` field | 消失 | ✅ | `device/manager.go` L17 | | 8 | `NewManager(registry, mockMode, mockCount, scriptPath)` 簽名簡化 | `NewManager(registry, scriptPath)` | ✅ | `device/manager.go` L22 | | 9 | `device/manager.go` 所有 `if m.mockMode {}` 分支 | 消失 | ✅ | `Start()` / `Rescan()` 已簡化 | | 10 | `mockdriver` import | 消失 | ✅ | `device/manager.go` import 區已清 | | 11 | `camera.Manager.mockMode` / `mockCamera` field | 消失 | ✅ | `camera/manager.go` L16..L19 | | 12 | `camera.NewManager(mockMode)` → `NewManager()` | 簡化 | ✅ | `camera/manager.go` L23 | | 13 | `camera.Manager.ListCameras/Open/Close/ReadFrame` mock 分支 | 消失 | ✅ | `camera/manager.go` L29-82 | | 14 | `main.go` `Mock mode:` log | 消失 | ✅ | `main.go:86` 已改為 `Dev mode: %v, Python mode: %s` | | 15 | `main.go` `NewManager(registry, cfg.MockMode, ...)` 呼叫 | 簡化 | ✅ | `main.go:141` | | 16 | `main.go` `camera.NewManager(cfg.MockCamera)` 呼叫 | 簡化 | ✅ | `main.go:146` | | 17 | `visiona-local/app.go` `mockMode` field | 消失 | ✅ | `app.go:82` 已清除 | | 18 | `visiona-local/app.go` `NewApp()` 讀 `VISIONA_MOCK` env | 消失 | ✅ | `app.go:117` 替換為 R5-5a 註解 | | 19 | `visiona-local/app.go` `startServer` 所有 `a.mockMode` 分支 | 消失 | ✅ | L425 / L435 / L455-465 / L488 皆清除 | | 20 | frontend `settings/page.tsx` runtimeMode Select + Separator | 消失 | ✅ | `page.tsx` -140..-158 | | 21 | frontend i18n `runtimeMode` / `...Mock` / `...Real` / `...Hint` | 消失 | ✅ | `types.ts` / `zh-TW.ts` / `en.ts` L270..L277 同步 | | 22 | frontend i18n `noDevices` 文字去 Mock | 改為 R5-v2-7 empty state | ✅ | 兩語言都改為「請連接 KL520 / KL720 後按掃描」 | | 23 | Makefile `dev-mock` target | 消失 | ✅ | `Makefile` 無 `dev-mock` | | 24 | README Mock 段落 | 消失 | ✅ | L73 「Mock 模式」bullet 整行刪、L24 零學習成本 bullet 改為「插上 Kneron 裝置 30 秒」 | | 25 | `manager_test.go` `TestNewManager_MockMode` | 消失 | ✅ | 已刪 | | 26 | `manager_test.go` `TestManager_ListDevices_Empty` 新增 | 新增 | ✅ | L39..L48,R5-5a 紀念註解合法 | | 27 | `api_e2e_test.go` 整檔刪 | 刪 | ✅ | `git status: deleted` | | 28 | `camera-controls.tsx` `'mock-cam-0'` fallback | 改為 `disabled={cameras.length === 0}` | ✅ | L22..L28 已改 | **A2 結論**:M8-2 砍除 **100% 達成**,且主動自補了 `api_e2e_test.go` 整檔刪、`manager_test.go` 新增空 list 測試、`camera-controls.tsx` fallback 修正。 --- ## B. 誤刪檢查(保留功能 red line) | 保留項目 | 狀態 | 證據 | |---------|------|------| | `POST /api/camera/start` | ✅ 保留 | router gin-debug 有 `/api/camera/start` | | `POST /api/camera/stop` | ✅ 保留 | 同上 | | `POST /api/media/upload/image` | ✅ 保留 | 同上 | | `POST /api/media/upload/video`(本機檔案,非 URL) | ✅ 保留 | 同上 | | `POST /api/media/upload/batch-images` | ✅ 保留 | 同上 | | `GET /api/media/batch-images/:index` | ✅ 保留 | 同上 | | `POST /api/media/seek`(本機影片 seek) | ✅ 保留 | 同上 | | `GET /api/camera/stream` | ✅ 保留 | 同上 | | `GET /api/devices`(無硬體回空 list,非 mock) | ✅ 保留 | smoke test:`{"devices":[]}` | | 模型管理 `/api/models*` | ✅ 保留 | router gin-debug 完整 | | WebSocket `/ws/devices/:id/inference` | ✅ 保留 | router gin-debug | | `VideoSource` + `NewVideoSource` + `NewVideoSourceWithSeek` | ✅ 保留 | `video_source.go` L70/L75 | | `ProbeVideoInfo` | ✅ 保留 | M8-1 未觸碰 | | `handleVideoSeek` 本地影片分支 | ✅ 保留 | `camera_handler.go:566` | | `stopActivePipeline` | ✅ 保留 | L532 | | `ffmpeg` 本身 | ✅ 保留 | deps checker + PATH 注入都還在 | **B 結論**:**零誤刪**。所有 R5 要求保留的功能完整存在。 --- ## C. 殘留 grep 結果 命令(排除 `.autoflow/` / `.git/` / `vendor/` / `node_modules/`): ### C1. yt-dlp 組 ``` grep -rn 'yt-dlp\|ytdlp\|YTDLP\|ResolveWithYTDLP\|StartFromURL\|ytdlpHosts\|urlYTDLP\|videoIsURL\|startFromUrl\|pasteUrl\|urlPlaceholder\|urlHelpText' ``` **結果**:**0 match** ✅ ### C2. Mock 組(嚴格:排除合法測試框架用詞) ``` grep -rn 'VISIONA_MOCK\|MockMode\|MockCamera\|NewMockDriver\|NewMockCamera\|mockdriver\|mock_camera\|runtimeModeMock\|runtimeModeReal\|mock-cam-0' ``` **結果**:**0 match** ✅ ### C3. 寬鬆 Mock 組(含一般 `mock` 字樣) `server/internal/` 下只剩: - `server/internal/device/manager_test.go:43` — `// 無硬體時應回傳空 list(R5-5a:沒插硬體就空白,不給 Mock 資料)` ← **合法紀念註解** `frontend/src/tests/` 下的 `vi.mock*` / `mockResolvedValue` / `clearAllMocks` — **合法 Vitest API** `visiona-local/` / `server/main.go` / `server/internal/camera/` / `server/internal/driver/` 下 — **0 match** **C 結論**:grep 完全 clean。 --- ## D. Build / test 結果 ### D1. `cd server && go build ./...` ✅ **PASS**(無輸出) ### D2. `cd server && go vet ./...` ✅ **PASS**(無輸出) ### D3. `cd server && go test ./...` ✅ **PASS** ``` ok visiona-local/server/internal/device (cached) ok visiona-local/server/internal/model (cached) ``` 其餘 package 顯示 `[no test files]`,符合預期(本次沒有新增 unit test)。 ### D4. `cd visiona-local && go build .` ✅ **PASS**(無輸出) ### D5. `cd frontend && pnpm build` ✅ **PASS** — `Compiled successfully in 4.2s`,TypeScript 通過,12 頁 SSG 生成完成。 **D 結論**:五項 build / test 全綠。 --- ## E. Smoke test 結果 啟動 `go run . --port 3722`,等 3 秒: | 測試 | 結果 | 備註 | |------|------|------| | Server 啟動成功 | ✅ | 看到 `Server listening on 127.0.0.1:3722` | | log 無 `Mock mode:` | ✅ | log 只剩 `Dev mode: false, Python mode: auto` | | log 無 `yt-dlp:` | ✅ | deps check 只有 `ffmpeg` 和 `python3` | | `GET /api/devices` | ✅ 200 | `{"data":{"devices":[]},"success":true}` — 空 list,非 mock 資料 | | `POST /api/media/url` | ✅ 404 | Route 已砍,gin 回 404(符合預期) | | router gin-debug 印出 | ✅ | 完全沒有 `StartFromURL`,只有 `UploadVideo / UploadImage / UploadBatchImages / SeekVideo / ListCameras / StartPipeline / StopPipeline / StreamMJPEG` | | Kneron 偵測 | ✅ | `No Kneron devices detected` — 走真實硬體 path(機器上沒插就空 list) | **E 結論**:Runtime 行為完全符合 R5-5a / R5-7 預期。 --- ## F. Edge case 自補合理性 ### F1. M8-1 自補 | 自補項 | 合理性 | |--------|-------| | README 清 yt-dlp 五處 + 改「零學習成本」文字 | ✅ 合理,deletions.md 未列但必然要改 | | `.github/workflows/build.yml` 清 `vendor-ytdlp-*` 兩處 | ✅ 合理,否則 CI build 會叫不存在的 target | | `visiona-local/app.go:locateBundleBinDir` 的 `fileExists("yt-dlp")` → `fileExists("ffprobe")` | ✅ 合理。ffprobe 會隨 M8-3 進 bundle bin dir,用 ffprobe 當 sentinel 比 ffmpeg 更精準(避免和 system ffmpeg 混淆) | | 刪 `camera.uploadFile` i18n key | ✅ 合理,URL tab 砍後這個 key 變 dead key | ### F2. M8-2 自補 | 自補項 | 合理性 | |--------|-------| | `api_e2e_test.go` 整檔刪 | ✅ 合理。該 test 依賴 mock driver + 舊 `NewRouter` 簽名,修起來成本高於價值。deletions.md §2.7 本來就留給執行者決定 | | `camera-controls.tsx` 的 `'mock-cam-0'` fallback 改 `disabled={cameras.length === 0}` | ✅ 合理。之前是「沒 camera 還是硬塞 mock-cam-0」,現在改為 button disabled + guard `if (firstCam)`,邏輯更嚴謹 | | `manager_test.go` 新增 `TestManager_ListDevices_Empty` | ✅ 合理。補足了刪除 `TestNewManager_MockMode` 後的測試缺口,明確驗證 R5-5a 的空 list 行為 | | `visiona-local/app.go` 的註解改寫(「M7:預設真實硬體模式」→「R5-5a:沒插硬體就顯示空白狀態」;「M1 預設 mock 模式...」→「R5-5a 之後:python 失敗直接擋啟動」) | ✅ 合理,註解與新行為一致 | **F 結論**:兩個 Agent 的自補都是必要且最小化的,沒有過度也沒有不足。 --- ## G. 兩個 milestone 交集檔案的正確性 ### G1. `visiona-local/app.go` - M8-1 改:`locateBundleBinDir` 的 `fileExists("yt-dlp")` → `fileExists("ffprobe")`;`startServer` 註解「ffmpeg / yt-dlp」→「ffmpeg / ffprobe」 - M8-2 改:`App.mockMode` field 砍、`NewApp()` 讀 `VISIONA_MOCK` 砍、`startServer` 所有 `a.mockMode` 分支砍、註解改寫 兩者互不干擾,merge 正確。`args` 陣列現在無條件帶 `--python-mode` 和(若 `pyBin != ""`)`--python`,符合 M8-2 deletions.md §3.1 的改法。`ensurePythonRuntime` 錯誤不再被 mockMode 遮蔽,直接 return error,符合 R5-5a 精神。 ✅ **正確** ### G2. `server/main.go` - M8-1 改:L88 PATH 注入註解「yt-dlp / ffmpeg」→「ffmpeg / ffprobe」 - M8-2 改:L86 `logger.Info("Mock mode: ...")` → `Dev mode: ..., Python mode: ...`;L141 `device.NewManager` 簽名簡化;L146 `camera.NewManager` 簽名簡化 兩者無衝突,merge 正確。 ✅ **正確** ### G3. `Makefile` - M8-1 改:`vendor-ytdlp` / `vendor-ytdlp-windows` / `vendor-ytdlp-linux` 三個 target 整段刪、`.PHONY` 移除、`vendor-sync` / `payload-*` 依賴移除、help 文字更新、`YTDLP_URL_*` 變數刪、`payload-*` 內 `cp .../yt-dlp ...` 行刪 - M8-2 改:`.PHONY` 的 `dev-mock` 移除、底部 `dev-mock:` target 整段刪 - **M8-3(不審)**:`vendor-ffmpeg` 系列從 GPL 改為 LGPL v3 + 新增 `vendor-ffmpeg-macos-build` target + ffmpeg URL 從 evermeet / johnvansickle / BtbN-gpl 改為 BtbN-lgpl M8-1 + M8-2 在 Makefile 內的改動乾淨、無殘留。M8-3 內容雖然混入但不與 M8-1 / M8-2 衝突(操作的是不同的 line / variable / target)。 ✅ **M8-1 + M8-2 在 Makefile 的部分正確** ### G4. `README.md` - M8-1 改:L18(zero-dep bullet 去 yt-dlp)、L74(支援 URL / yt-dlp 行去掉)、L128 / L131(vendor-sync / payload-macos 描述去 yt-dlp)、L182(授權表格 yt-dlp row 刪) - M8-2 改:L24(零學習成本 bullet 去 Mock)、L73(Mock 模式 bullet 整行刪) - **M8-3(不審)**:L163 GPL TODO 整行刪、L180 ffmpeg GPL row 改 LGPL M8-1 + M8-2 在 README 的部分正確且完整。 ✅ **正確** ### G5. scripts/installer 類(bootstrap-linux / bootstrap-windows / visiona-local.iss / build-appimage.sh / build.yml) - M8-1 改:清除 `vendor-ytdlp-*` / `yt-dlp*.exe` / tool loop 中的 yt-dlp - **M8-3(不審)**:LGPL 相關的 LICENSE.txt / COPYING.LGPLv3 複製、GPL warning 去除、ffprobe 複製 M8-1 的 yt-dlp 清除動作完整。 ✅ **正確** --- ## H. 問題清單 ### Major(阻擋 M8-4) **無。** ### Minor(可先往下走,後續 M8-6 / 另開處理) | # | 檔案 | 問題描述 | 建議處理 | |---|------|---------|---------| | 1 | `frontend/src/components/camera/source-selector.tsx:184,189` | `accept=".mp4,.avi,.mov"` 和 `t('camera.mp4AviMov')` 仍為三種副檔名 | **不是 M8-1 的 bug**。deletions.md §5.1 的「新增 mpeg/mpg」和 milestone-plan M8-6 任務 2+3 明確把副檔名擴充列為 M8-6 的工作。留給 M8-6 處理即可 | | 2 | `server/internal/api/handlers/camera_handler.go:~251`(未檢視) | 後端影片 extension whitelist 仍為 `.mp4 / .avi / .mov` | 同上,milestone-plan M8-6 任務 4 明確列入 | | 3 | `server/internal/deps/checker.go` | `CheckAll()` 仍只檢查 `ffmpeg`,沒加 `ffprobe` | 不是 M8-1 範圍。M8-3 切 LGPL ffmpeg 後 ffprobe 變必備,屆時應在 deps checker 加一項 | | 4 | working tree 混入 M8-3 內容 | Makefile / README / installer / scripts / build.yml / `.gitignore` 都有 M8-3 的改動 | 這是「working tree 目前同時有三個 milestone」的狀態,不是 bug。Orchestrator 決定要不要分 commit 即可 | | 5 | `server/internal/api/api_e2e_test.go` 整檔刪 | 本來有 E2E HTTP 層級的煙霧測試,刪除後該層級失去測試 | 不 block M8-4,但 M8-10 前應考慮補回一份不依賴 mock 的 e2e(可改為:無硬體環境下測 `/api/devices` 回空 list + `/api/models` 回內建模型等純 read-only 路徑) | ### Suggestion(可做可不做) | # | 檔案 | 建議 | |---|------|------| | 1 | `visiona-local/app.go:671` 的 docblock 「R5-5a 之後:python 失敗直接擋啟動(沒有模擬回退)」 | 文字 OK,可考慮補一句「若要無 Kneron 硬體環境 debug,請直接透過 server binary `go run` 而不透過 Wails 殼」方便開發者 | | 2 | `server/internal/device/manager_test.go:43` 的 R5-5a 紀念註解 | 可愛的紀錄,保留 | --- ## I. 通過 / 不通過結論 ### ✅ **通過** **理由**: 1. M8-1 + M8-2 的 28 + 28 = 56 項砍除清單 **100% 達成** 2. 零誤刪,所有 R5 指定保留功能完整存在 3. 殘留 grep 完全 clean 4. 五項 build / test 全綠 5. Smoke test 驗證 runtime 行為正確(空 device list、/media/url 404、log 無 mock / yt-dlp) 6. 兩個 Agent 的自補決定全部合理 7. 三個 merge 交集檔案(app.go / main.go / Makefile / README)的 M8-1 和 M8-2 改動相容無衝突 ### 下一步建議給 Orchestrator 1. **M8-1 + M8-2 可以 commit**(建議兩個獨立 commit,或一個合併 commit 標示「M8-1 + M8-2 砍除」) 2. **M8-3 的 working tree 改動需另外處理** — 建議分開 commit / 分開 review 3. **M8-4 可以開動** — 無技術阻擋 4. **M8-6 日後執行時** 記得處理 Minor #1 和 #2(副檔名擴充 + 後端 whitelist + i18n 改名) 5. **M8-3 執行完** 順便處理 Minor #3(deps checker 加 ffprobe) --- ## 優點 - **M8-1 Agent**:砍得非常乾淨,連 orphan i18n key(`uploadFile`)和 bundle bin dir 的 yt-dlp sentinel 都主動處理 - **M8-2 Agent**:`TestManager_ListDevices_Empty` 新增測試體現 R5-5a 精神;`camera-controls.tsx` 的 fallback 改為 `disabled={cameras.length === 0}` 是比原本塞 `'mock-cam-0'` 更嚴謹的寫法 - **兩個 milestone 的 merge**:沒有互相踩腳。`visiona-local/app.go` 尤其漂亮,`mockMode` 砍掉後 `args` 陣列變得非常直觀 - **grep 殘留 0 match**:連 `videoIsURL` 這種容易忘記的 dead flag 都有徹底清除 砍完這兩個 milestone 後,整個 codebase 瘦了 **~655 行** 程式碼(29 修改 + 3 刪除),直接對應 R5-7「砍掉非核心功能、focus 推論核心」的決策。砍得漂亮。