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

635 lines
19 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.

# v2/deletions.md — 刪檔 / 刪程式碼清單
> 所屬TDD v2 §2.5
> 版本v2.12026-04-14 吸收 PM Minor 5 grep 精準化 + Architect Q1 互審結論)
> 決策依據R5-5aMock 模式完全砍除、R5-7 前置yt-dlp 全套砍除)
> 對應 milestoneM8-1砍 yt-dlp、M8-2砍 Mock
> 給工程師:這是按表操作的清單,依序砍完就能送 Reviewer
**操作規則**
- 每個「刪」動作後必須 `go build ./server/...``pnpm --dir frontend build` 確認還可 compile
- 每個「改」動作後同樣
- 全部砍完後 `git grep -i 'yt-dlp\|ytdlp\|YTDLP\|mock\|Mock\|MOCK\|VISIONA_MOCK'` 應該只剩註解(如「原本這裡有 Mock 模式,已在 v2 砍除」)或明確是別的 mock例如 gomock 測試框架)
---
## 1. 後端 Go — yt-dlp 全套砍除
### 1.1 `server/internal/camera/video_source.go`
**刪除區塊**:第 91-140 行
```go
// ResolveWithYTDLP uses yt-dlp to extract the direct video stream URL
// from platforms like YouTube, Vimeo, etc.
// Returns the resolved direct URL or an error.
func ResolveWithYTDLP(rawURL string) (string, error) {
... 整個 func ...
}
// friendlyYTDLPError 把 yt-dlp 的技術性錯誤訊息轉成使用者能理解的提示。
func friendlyYTDLPError(stderr string) string {
... 整個 func ...
}
```
同時檢查 import若原本因為 `ResolveWithYTDLP` 引入了 `strings` / `os/exec` / `fmt`,確認這些 import 在檔案其他地方還有用(視情況保留或移除)。
**`NewVideoSourceFromURL` / `NewVideoSourceFromURLWithSeek` 的砍除**v2.1 Minor 5 精準化):
v2.0 原本寫「可能仍然有其他路徑呼叫grep 再決定」— 這是模糊的。Architect Q1 互審時已實際 grep 確認結論:
```
grep -rn 'NewVideoSourceFromURL' /Users/jimchen/visionA/local-tool/server/
```
**實測結果**2026-04-14 Architect 互審):
- `server/internal/api/handlers/camera_handler.go:435``StartFromURL` 內部呼叫)— 即將砍
- `server/internal/api/handlers/camera_handler.go:731``handleVideoSeek``videoIsURL` guard 下的 seek 分支)— dead code連同 `videoIsURL` field 一起砍
**沒有其他呼叫者**。所以:
- 連同 `NewVideoSourceFromURL` 整個 function 砍
- 連同 `NewVideoSourceFromURLWithSeek`(若有)整個 function 砍
- 連同 `newVideoSource(..., isURL=true, ...)``isURL` 參數分支砍(簡化內部函式簽名)
- `camera_handler.go:731``handleVideoSeek``if h.videoIsURL { ... }` 整段 seek URL 分支砍dead code因為 `videoIsURL` 將不可能為 true
- `CameraHandler` struct 的 `videoIsURL bool` field 砍
**驗收**:砍完後以下 grep 應全部無輸出:
```bash
grep -rn 'NewVideoSourceFromURL\|videoIsURL' /Users/jimchen/visionA/local-tool/server/
```
### 1.2 `server/internal/api/handlers/camera_handler.go`
**刪除區塊**:第 341-497 行
```go
// ytdlpHosts lists hostnames where yt-dlp should be used to resolve the actual
var ytdlpHosts = map[string]bool{ ... }
type urlKind int
const (
urlDirect urlKind = iota
urlYTDLP
urlBad
)
// classifyVideoURL determines how to handle the given URL.
func classifyVideoURL(rawURL string) (urlKind, string) { ... }
// StartFromURL handles video/stream inference from a URL (HTTP, HTTPS, RTSP).
func (h *CameraHandler) StartFromURL(c *gin.Context) { ... }
```
→ 整個 `ytdlpHosts` / `urlKind` / `classifyVideoURL` / `StartFromURL` 函式全砍。
**`videoIsURL` field 的處置**v2.1 Minor 5 定案):
Architect Q1 互審時已 grep`videoIsURL` 只在 `camera_handler.go:731``handleVideoSeek` URL 分支被讀取,沒有 stopActivePipeline 的特殊 cleanup。因此
-`CameraHandler.videoIsURL bool` field
-`handleVideoSeek``if h.videoIsURL { ... }` 整個 URL seek 分支dead code — 砍 `StartFromURL` 後永遠不會是 true
-`startVideoInference``h.videoIsURL = true` 的行(若有)
- 結論:**不保留為 always-false flag**直接刪乾淨v2.0 原本留餘地「可能保留」v2.1 明確決定刪)
### 1.3 `server/internal/api/router.go`
**刪除**:第 83 行
```go
api.POST("/media/url", cameraHandler.StartFromURL)
```
### 1.4 `server/internal/deps/checker.go`
**刪除**:第 30-32 行
```go
check("yt-dlp", false,
"macOS: brew install yt-dlp | Windows: winget install yt-dlp",
"--version"),
```
**同時修改**:第 69-70 行的註解提及 yt-dlp 冷啟動 20 秒的內容,更新為:
```go
// 效能bundle 內的 binary 冷啟動可能較慢(尤其 PyInstaller
// bundle binary 已知良好,跳過 version 查詢以加速啟動。
```
### 1.5 `server/main.go`
**修改**:第 89-95 行的 PATH 注入註解。原文:
```go
// 把 VISIONA_BUNDLE_BIN_DIR 加到 PATH讓 exec.Command("yt-dlp") / exec.Command("ffmpeg")
// 能透過 LookPath 找到 bundle 內的 binaryGo 1.19+ Windows 不再搜 cwd
```
改為:
```go
// 把 VISIONA_BUNDLE_BIN_DIR 加到 PATH讓 exec.Command("ffmpeg") / exec.Command("ffprobe")
// 能透過 LookPath 找到 bundle 內的 binaryGo 1.19+ Windows 不再搜 cwd
```
**PATH 注入邏輯本身不動**ffmpeg 仍需要)。
---
## 2. 後端 Go — Mock 模式全套砍除R5-5a
### 2.1 整檔刪除
| 檔案 | 行數 | 說明 |
|------|------|------|
| `server/internal/driver/mock/mock_driver.go` | 183 | `MockDriver` struct 與所有 method |
| `server/internal/camera/mock_camera.go` | 95 | `MockCamera` struct 與所有 method |
**動作**
```
rm server/internal/driver/mock/mock_driver.go
rmdir server/internal/driver/mock # 該目錄若空就刪
rm server/internal/camera/mock_camera.go
```
### 2.2 修改 `server/internal/device/manager.go`
**現況**
```go
import (
...
mockdriver "visiona-local/server/internal/driver/mock"
...
)
type Manager struct {
...
mockMode bool
...
}
func NewManager(registry *DriverRegistry, mockMode bool, mockCount int, scriptPath string) *Manager {
...
if mockMode { ... mockdriver.NewMockDriver(...) ... }
}
func (m *Manager) Scan() error {
if m.mockMode { ... }
...
}
func (m *Manager) someMethod() {
if m.mockMode { ... }
...
}
```
**改法**
-`mockdriver` import
-`mockMode` / `mockCount` 欄位
- 修改 `NewManager` 簽名:`func NewManager(registry *DriverRegistry, scriptPath string) *Manager`
- 砍掉所有 `if m.mockMode { ... }` 分支,只留 real path
### 2.3 修改 `server/internal/camera/manager.go`
**現況**
```go
type Manager struct {
mockMode bool
mockCamera *MockCamera
...
}
func NewManager(mockMode bool) *Manager {
return &Manager{mockMode: mockMode}
}
func (m *Manager) Start(...) {
if m.mockMode {
m.mockCamera = NewMockCamera(width, height)
...
}
...
}
```
**改法**
-`mockMode` 欄位、`mockCamera` 欄位
- 改簽名:`func NewManager() *Manager`
- 砍所有 `if m.mockMode { ... }`
### 2.4 修改 `server/internal/config/config.go`
**現況**:第 21-23 行
```go
MockMode bool
MockCamera bool
MockDeviceCount int
```
第 39-41 行:
```go
flag.BoolVar(&cfg.MockMode, "mock", false, "Enable mock device driver")
flag.BoolVar(&cfg.MockCamera, "mock-camera", false, "Enable mock camera")
flag.IntVar(&cfg.MockDeviceCount, "mock-devices", 1, "Number of mock devices")
```
**改法**:整組刪(三個欄位 + 三個 flag
### 2.5 修改 `server/main.go`
**現況**
```go
// 第 86-87 行
logger.Info("Mock mode: %v, Mock camera: %v, Dev mode: %v, Python mode: %s",
cfg.MockMode, cfg.MockCamera, cfg.DevMode, cfg.PythonMode)
// 第 142 行
deviceMgr := device.NewManager(registry, cfg.MockMode, cfg.MockDeviceCount, bridgeScript)
// 第 147 行
cameraMgr := camera.NewManager(cfg.MockCamera)
```
**改法**
```go
logger.Info("Dev mode: %v, Python mode: %s", cfg.DevMode, cfg.PythonMode)
deviceMgr := device.NewManager(registry, bridgeScript)
cameraMgr := camera.NewManager()
```
### 2.6 修改 `server/internal/device/manager_test.go`
如果測試檔案裡有 mockMode 相關 case → 整段刪。若整個測試檔都是 mock-based → 整檔刪。
**動作**`cat server/internal/device/manager_test.go` 確認後決定。
### 2.7 修改 `server/internal/api/api_e2e_test.go`
如果 e2e test 開啟 mock mode 跑 → 改為 skip沒硬體時 skip或改寫成不依賴 mock。
**動作**`grep -n 'Mock\|mock' server/internal/api/api_e2e_test.go` 查看M8-2 執行者決定保留哪些測試。
---
## 3. Wails app.go — Mock 模式砍除
### 3.1 `visiona-local/app.go` 修改
**現況**
```go
// 第 83 行
type App struct {
...
mockMode bool
...
}
// 第 119-120 行
// M7預設真實硬體模式使用者決策 Q8
// 若要強制 mock 模式(無 Kneron 裝置環境下 debug設環境變數 VISIONA_MOCK=1
mock := os.Getenv("VISIONA_MOCK") == "1"
return &App{
pythonMode: mode,
mockMode: mock,
}
// 第 429 行(在 startServer 中)
if err != nil && !a.mockMode {
...
}
// 第 441 行
if !a.mockMode && pyBin != "" {
if err := a.ensureDriverInstalled(pyBin); err != nil {
...
}
}
// 第 472-479 行(組 args
if a.mockMode {
args = append(args, "--mock")
} else {
args = append(args, "--python-mode", string(pyMode))
if pyBin != "" {
args = append(args, "--python", pyBin)
}
}
// 第 502 行
if !a.mockMode && pyBin != "" {
env = append(env, "VISIONA_PYTHON="+pyBin)
...
}
```
**改法**
-`mockMode` struct field
-`NewApp()` 的 env 讀取 + 初始化
- 刪所有 `!a.mockMode` / `a.mockMode` 條件
- 組 args 的邏輯簡化:一定走 real path`--python-mode``--python` 一定帶
- `err != nil && !a.mockMode` → 直接 `err != nil`(失敗就 return 錯誤)
### 3.2 `visiona-local/frontend/app.js`
若有任何提及 mock 的內容M7-B 後應該沒有) → 刪
---
## 4. 打包流程砍除
### 4.1 `Makefile` — 砍 yt-dlp
**刪除 targets**
| 行數 | 內容 |
|------|------|
| ~22 | `vendor-sync vendor-python vendor-wheels vendor-ffmpeg vendor-ytdlp \` 中的 `vendor-ytdlp` |
| ~23 | `vendor-python-windows ... vendor-ytdlp-windows` 同上 |
| ~24 | `vendor-python-linux ... vendor-ytdlp-linux` 同上 |
| 39 | help 文字中 `/yt-dlp` |
| 70-71 | `YTDLP_URL_DARWIN := ...` |
| 73 | `vendor-sync` 依賴移除 `vendor-ytdlp` |
| 134-144 | `vendor-ytdlp:` target 整段 |
| 182 | `payload-macos` 依賴移除 `vendor-ytdlp` |
| 188-189 | `cp vendor/yt-dlp/darwin/yt-dlp payload/darwin/bin/` + `chmod +x payload/darwin/bin/yt-dlp` |
| 194 | help 文字 `+ yt-dlp` |
| 219 | `YTDLP_URL_WINDOWS := ...` |
| 288-296 | `vendor-ytdlp-windows:` target 整段 |
| 298 | `payload-windows` 依賴移除 `vendor-ytdlp-windows` |
| 299 | help 文字 `+ yt-dlp` |
| 307 | `cp vendor/yt-dlp/windows/yt-dlp.exe payload/windows/bin/` |
| 332 | `YTDLP_URL_LINUX := ...` |
| 381-390 | `vendor-ytdlp-linux:` target 整段 |
| 392 | `payload-linux` 依賴移除 `vendor-ytdlp-linux` |
| 393 | help 文字 `+ yt-dlp` |
| 401 | `cp vendor/yt-dlp/linux/yt-dlp ...` 整行 |
**同時**:刪 `/vendor/yt-dlp/` 整個目錄(若 Makefile 已經產出過)
```
rm -rf vendor/yt-dlp/
```
### 4.2 `installer/windows/visiona-local.iss`
**刪除**:第 74 行的區段標題 `; ── ffmpeg + yt-dlp ───` 改為 `; ── ffmpeg + ffprobe ───`。
第 76 行:
```
Source: "..\..\payload\windows\bin\yt-dlp.exe"; DestDir: "{app}\bin"; Flags: ignoreversion
```
**刪除此行**。同時新增 ffprobe 和 LGPL license 行(見 `v2/ffmpeg-lgpl.md` §8
### 4.3 `installer/linux/build-appimage.sh`
**修改**:第 61-62 行
```bash
# ffmpeg / yt-dlp
for tool in ffmpeg yt-dlp; do
```
改為:
```bash
# ffmpeg / ffprobe
for tool in ffmpeg ffprobe; do
```
### 4.4 `scripts/bootstrap-linux.sh`
**修改**:第 61 行
```bash
make vendor-python-linux vendor-wheels-linux vendor-ffmpeg-linux vendor-ytdlp-linux
```
改為:
```bash
make vendor-python-linux vendor-wheels-linux vendor-ffmpeg-linux
```
### 4.5 `scripts/bootstrap-windows.ps1`
**修改**
- 第 191 行註解:`保留 vendor/ 快取Python runtime / wheels / ffmpeg / yt-dlp以免重下 200MB` → `... 200MB` 改為 `... Python runtime / wheels / ffmpeg`
- 第 202 行註解:同上
- 第 207 行:`'make vendor-python-windows vendor-wheels-windows vendor-ffmpeg-windows vendor-ytdlp-windows'` 移除 `vendor-ytdlp-windows`
---
## 5. 前端 TypeScript / React — yt-dlp URL tab 砍除
### 5.1 `frontend/src/components/camera/source-selector.tsx`
**現況**260 行,使用 `videoMode` state + `pasteUrl` / `urlPlaceholder` / `urlHelpText` i18n、`handleUrlSubmit` / `startFromUrl` 呼叫。
**改動**
1. 刪 import`startFromUrl` 從 `useCameraStore` destructure 中移除(第 28 行附近)
2. 刪 state第 51 行 `const [videoMode, setVideoMode] = useState<'file' | 'url'>('file');` 整行刪
3. 刪 state同一段若有 `const [videoUrl, setVideoUrl] = useState('');` 也刪
4. 刪函式:第 94-96 行 `handleUrlSubmit` 整個函式刪
5. 刪 UI第 185-253 行的 video tab 內的 mode toggle + `videoMode === 'file' ? ... : ...` 整塊改為「只剩 file 選擇」:
```tsx
{activeTab === 'video' && (
<div className="flex items-center gap-3">
<Button
onClick={() => videoFileRef.current?.click()}
disabled={isUploading}
>
{isUploading ? t('common.uploading') : t('camera.selectVideo')}
</Button>
<span className="text-sm text-muted-foreground">
{t('camera.mp4AviMovMpeg')}
</span>
<input
ref={videoFileRef}
type="file"
accept=".mp4,.avi,.mov,.mpeg,.mpg" {/* R5-6 / 三方共識 #11新增 mpeg/mpg */}
className="hidden"
onChange={handleVideoSelect}
/>
</div>
)}
```
砍掉「上傳檔案 / 貼上連結」兩個 mode Button + URL input + help text + loading text。
### 5.2 `frontend/src/stores/camera-store.ts`
**刪除**
- 第 32 行 type 定義:`startFromUrl: (url: string, deviceId: string) => Promise<void>;`
- 第 167-196 行:`startFromUrl` 函式的整個實作30 行)
### 5.3 `frontend/src/lib/i18n/types.ts`
**刪除**
- 第 210-212 行:`pasteUrl`, `urlPlaceholder`, `urlHelpText` 三個 key 的 type 定義
- 第 422 行:`cannotOpenVideoUrl` key 的 type 定義
**新增**
- `mp4AviMovMpeg: string;`(取代 v1 的 `mp4AviMov` 或並存)— 決定改不改 key**建議改名成 `videoFormats`**(通用化),以便未來擴充時不用再改型別名。
### 5.4 `frontend/src/lib/i18n/zh-TW.ts`
**刪除/修改**
- 第 212 行 `pasteUrl: '貼上連結',` 刪
- 第 213 行 `urlPlaceholder: 'https://example.com/video.mp4',` 刪
- 第 214 行 `urlHelpText: '支援 YouTube、直接影片 URL.mp4 等)及 RTSP 串流。',` 刪
- 第 217 行 `mp4AviMov: 'MP4, AVI, MOV',` → **改為** `videoFormats: 'MP4 / AVI / MOV / MPEG / MPG',`
- 第 424 行 `cannotOpenVideoUrl: '無法開啟影片連結',` 刪(若 store 沒有呼叫處)
### 5.5 `frontend/src/lib/i18n/en.ts`
同 §5.4,英文版對應修改:
- 刪 `pasteUrl`, `urlPlaceholder`, `urlHelpText`, `cannotOpenVideoUrl`
- `mp4AviMov: 'MP4, AVI, MOV',` → `videoFormats: 'MP4 / AVI / MOV / MPEG / MPG',`
---
## 6. 前端 TypeScript / React — Mock 模式砍除
### 6.1 `frontend/src/app/settings/page.tsx`
**刪除區塊**:第 140-156 行
```tsx
<div className="space-y-2">
<Label>{t('settings.hardware.runtimeMode')}</Label>
{/* TODO: 連接 backend GET /api/system/config ... */}
<Select value="real" disabled>
<SelectTrigger className="w-[420px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="mock">{t('settings.hardware.runtimeModeMock')}</SelectItem>
<SelectItem value="real">{t('settings.hardware.runtimeModeReal')}</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
{t('settings.hardware.runtimeModeHint')}
</p>
</div>
<Separator />
```
→ 整段 `<div>` 與其後的 `<Separator />` 一起刪Separator 是用來分隔 runtimeMode 與 pythonMode 的)。
### 6.2 `frontend/src/lib/i18n/types.ts`
**刪除**:第 272-275 行
```typescript
runtimeMode: string;
runtimeModeMock: string;
runtimeModeReal: string;
runtimeModeHint: string;
```
### 6.3 `frontend/src/lib/i18n/zh-TW.ts`
**刪除**:第 274-277 行 4 個 key。
### 6.4 `frontend/src/lib/i18n/en.ts`
**刪除**:同 §6.3,第 274-277 行英文版。
### 6.5 `frontend/src/lib/i18n/zh-TW.ts` + `en.ts` 的 `noDevices` 文字
**現況**
```typescript
// zh-TW.ts:70
noDevices: '未偵測到裝置。請確認已啟用 Mock 模式或連接裝置。',
// en.ts:70
noDevices: 'No devices detected. Make sure mock mode is enabled or connect a device.',
```
**改為**(對應 R-v2-7 的 empty state 建議):
```typescript
// zh-TW
noDevices: '未偵測到 Kneron 裝置。請連接 KL520 / KL720 後按「掃描」。',
// en
noDevices: 'No Kneron devices detected. Please connect a KL520 / KL720 device and click "Scan".',
```
---
## 7. Wails 控制台(改寫而非刪)
`visiona-local/frontend/index.html` / `app.js` / `style.css` — **改寫**而非刪,詳見 `v2/control-panel.md` §7「檔案系統變化」。
**提醒**:改寫步驟屬於 M8-5不在本文件的 M8-1 / M8-2 範圍。
---
## 8. 驗收 grep
M8-1 + M8-2 全部完成後,執行以下 grep 應該全部 clean只剩註解或非相關的 match
```bash
# yt-dlp
grep -rn 'yt-dlp\|ytdlp\|YTDLP\|ResolveWithYTDLP\|friendlyYTDLPError\|ytdlpHosts\|urlYTDLP\|classifyVideoURL\|StartFromURL\|cannotOpenVideoUrl\|pasteUrl\|urlPlaceholder\|urlHelpText' \
/Users/jimchen/visionA/local-tool/server/ \
/Users/jimchen/visionA/local-tool/frontend/ \
/Users/jimchen/visionA/local-tool/visiona-local/ \
/Users/jimchen/visionA/local-tool/Makefile \
/Users/jimchen/visionA/local-tool/installer/ \
/Users/jimchen/visionA/local-tool/scripts/
# Mock
grep -rn 'MockMode\|mockMode\|MockCamera\|MockDriver\|NewMockDriver\|NewMockCamera\|VISIONA_MOCK\|runtimeModeMock' \
/Users/jimchen/visionA/local-tool/server/ \
/Users/jimchen/visionA/local-tool/frontend/ \
/Users/jimchen/visionA/local-tool/visiona-local/
```
**預期**
- `yt-dlp` grep**完全無輸出**(或只有 v1 文件 `.autoflow/` 內的歷史紀錄,忽略)
- `Mock` grep**完全無輸出**
若有殘留 → 補刀刪除或保留但註記原因。
---
## 9. 砍除後的 compile 確認
```bash
# Go server
cd /Users/jimchen/visionA/local-tool/server && go build ./...
cd /Users/jimchen/visionA/local-tool/server && go test ./...
# Wails app
cd /Users/jimchen/visionA/local-tool/visiona-local && go build .
# 前端
cd /Users/jimchen/visionA/local-tool/frontend && pnpm install && pnpm build
```
四個都要綠才算 M8-1 / M8-2 完成,可送 Reviewer。