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

19 KiB
Raw Permalink Blame History

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-3ffmpeg 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 + newVideoSourceisURL 參數 消失 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 handleVideoSeekvideoIsURL 分支 消失 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 UImode toggle + input + help 消失 source-selector.tsx -183..-253
18 i18n pasteUrl / urlPlaceholder / urlHelpText / cannotOpenVideoUrl 消失 types.ts / zh-TW.ts / en.ts 三檔同步
19 i18n camera.uploadFilemode 切換用) 消失 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 locateBundleBinDiryt-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 消失 Makefiledev-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..L48R5-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// 無硬體時應回傳空 listR5-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

PASSCompiled successfully in 4.2sTypeScript 通過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 只有 ffmpegpython3
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.ymlvendor-ytdlp-* 兩處 合理,否則 CI build 會叫不存在的 target
visiona-local/app.go:locateBundleBinDirfileExists("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 改:locateBundleBinDirfileExists("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 改:.PHONYdev-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 改L18zero-dep bullet 去 yt-dlp、L74支援 URL / yt-dlp 行去掉、L128 / L131vendor-sync / payload-macos 描述去 yt-dlp、L182授權表格 yt-dlp row 刪)
  • M8-2 改L24零學習成本 bullet 去 Mock、L73Mock 模式 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 #3deps checker 加 ffprobe

優點

  • M8-1 Agent:砍得非常乾淨,連 orphan i18n keyuploadFile)和 bundle bin dir 的 yt-dlp sentinel 都主動處理
  • M8-2 AgentTestManager_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 推論核心」的決策。砍得漂亮。