依 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>
635 lines
19 KiB
Markdown
635 lines
19 KiB
Markdown
# v2/deletions.md — 刪檔 / 刪程式碼清單
|
||
|
||
> 所屬:TDD v2 §2.5
|
||
> 版本:v2.1(2026-04-14 吸收 PM Minor 5 grep 精準化 + Architect Q1 互審結論)
|
||
> 決策依據:R5-5a(Mock 模式完全砍除)、R5-7 前置(yt-dlp 全套砍除)
|
||
> 對應 milestone:M8-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 內的 binary(Go 1.19+ Windows 不再搜 cwd)。
|
||
```
|
||
|
||
改為:
|
||
|
||
```go
|
||
// 把 VISIONA_BUNDLE_BIN_DIR 加到 PATH,讓 exec.Command("ffmpeg") / exec.Command("ffprobe")
|
||
// 能透過 LookPath 找到 bundle 內的 binary(Go 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。
|