visionA/local-tool/.autoflow/04-architecture/architect-analysis-round2-refactor.md
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

799 lines
50 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

# Architect 第一輪分析 — visionA-local 方向變更2026-04-14
> 作者Architect Agent
> 狀態Round-2 refactor 的第一輪筆記(非正式 Design Doc / TDD
> 目的:針對使用者提出的方向重大變更做技術可行性評估 + 方案選項 + 風險標記
> 前置閱讀:`design-doc.md` 索引 + `architecture-overview.md` + `tray-and-lifecycle.md` + `dependency-bundling.md` + `risks-and-mitigations.md` + `progress.md` + `visiona-local/app.go` + `server/main.go`
---
## 摘要3 行)
1. **好消息:技術上非常可行,而且大部分搬遷成本已經被 M7-B 提前付掉**。前端現在已經是純 HTTP從 Wails WebView 搬到 Browser tab 只差「不把同一個網址 redirect 進 Wails」這一步。
2. **砍量顯著**yt-dlp 整包可丟vendor 87MB + resolver 程式碼 + frontend URL tab安裝檔從 220MB 降到 ~135MBWails 內嵌的控制台 UI 改寫成 3 頁獨立 shelllog / server 控制 / 開瀏覽器)。
3. **但 lifecycle 決策必須復議**Q-A砍 tray與 Q7關閉視窗=結束 app都站不住腳了——新方向的核心是「Wails 視窗只是控制台server 是主角」,控制台關掉不能殺 server否則瀏覽器 tab 瞬間斷線。這點若不先跟使用者談清楚,後面做出來會有嚴重體驗落差。
---
## A. 技術影響範圍
### A1. 新架構圖ASCII
```
┌─────────────────────────────────────────────────────────────────────┐
│ visionA-local.app (Wails Control Console — 桌面殼) │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 控制台 UIHTML/CSS/JSgo:embed 進 Wails binary │ │
│ │ ┌──────────────┐ ┌───────────────┐ ┌─────────────────┐ │ │
│ │ │ Server 狀態 │ │ Log panel │ │ 動作列 │ │ │
│ │ │ ● Running │ │ [ring buffer] │ │ [Start] [Stop] │ │ │
│ │ │ Port: 3721 │ │ scrollable │ │ [Restart] │ │ │
│ │ │ PID: 42131 │ │ filter/search │ │ [Open Browser] │ │ │
│ │ │ Python: sys │ │ clear / save │ │ [Reveal Logs] │ │ │
│ │ └──────────────┘ └───────────────┘ └─────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ ↑ Wails runtime (IPC) │
│ │ - Bindings: StartServer/StopServer/... │
│ │ - Events: server:log, server:status, │
│ │ server:dead, server:recovered │
│ ┌─────────────────┴───────────────────────────────────────────┐ │
│ │ App (Go, app.go) │ │
│ │ - Lifecycle / single-instance / data-dir migration │ │
│ │ - Server process manager (spawn/kill/watch) │ │
│ │ - Log pump: stdout/stderr → ring buffer → EventsEmit │ │
│ │ - Python runtime 雙策略、driver installer │ │
│ └──────────────────────┬──────────────────────────────────────┘ │
└───────────────────────────│──────────────────────────────────────────┘
│ exec.Command(...) pipe stdout/stderr
┌─────────────────────────────────────────────────────────────────────┐
│ visiona-local-server (Gin on 127.0.0.1:random_port) │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ REST API (/api/*) + WebSocket (/ws/*) │ │
│ │ - /api/devices/* /api/models/* /api/inference/* │ │
│ │ - /ws/devices/events /ws/inference/frames /ws/server-logs │ │
│ │ Next.js UI (go:embed) —— 使用者業務介面 │ │
│ │ - Dashboard / Devices / Models / Workspace / Settings │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ ▲ │
│ │ spawn │ HTTP/WS │
│ ▼ │ │
│ ┌──────────────────────────────┐ │ │
│ │ python3 kneron_bridge.py │ │ │
│ │ (KneronPLUS SDK) │ │ │
│ └──────────────────────────────┘ │ │
│ │ │
│ ┌──────────────────────────────┐ │ │
│ │ ffmpeg (on demand) │ │ │
│ │ (webcam / 上傳影片解碼) │ │ │
│ └──────────────────────────────┘ │ │
└──────────────────────────────────────┼──────────────────────────────┘
│ HTTP/WS over loopback
┌────────────────────────────────────────┐
│ Browser tab (Chrome / Safari / Edge) │
│ http://127.0.0.1:<random_port>/ │
│ │
│ Next.js SPA (fetch + WebSocket) │
│ - 所有業務操作在這裡 │
└────────────────────────────────────────┘
```
**資料流關鍵差異vs. 現況)**
| 項目 | 現況M7-B| 新方向 |
|------|-------------|--------|
| Wails WebView 載入 | splash → `window.location.replace(http://127.0.0.1:port/)` 跳 Next.js 主 UI | splash 邏輯移除Wails 永遠停在「控制台 UI」不跳轉 |
| 使用者業務操作 | Wails WebView 內(但已經是純 HTTP| 另開 Browser tabWails 不碰業務 UI |
| Server stdout/stderr | 寫進 `logs/server.stdout.log` + `server.stderr.log`file only| **同時**管到 ring buffer → Wails events 給控制台 log panel |
| 關閉 Wails 視窗 | Q7=B關 = 殺 server + 殺 app | **必須改**:關 = 隱藏到 tray 或 minimize真正結束需透過「Quit」按鈕 |
| yt-dlp pipeline | vendor 87MB + `ResolveWithYTDLP` + `/api/media/url` + 前端 URL tab | **整條砍** |
| 瀏覽器端情境 | 使用者若自己連 `http://127.0.0.1:port/` 也可以用(未文件化)| **一級公民**Wails 的「Open in Browser」會主動導引 |
**Lifecycle 狀態機**
```
┌──────────────┐
│ app launched │
└──────┬───────┘
┌────────────────────────┐
│ acquire single-instance│
└──────┬─────────────────┘
┌──────────────────┐
│ seed user-data │
└──────┬───────────┘
┌─────────────────────┐
│ ensure python │
└────┬────────────────┘
┌──────────────────────┐ ┌───────────────┐
│ STATE: Starting │──OK──▶│ STATE: Running │◀──┐
│ spawn server │ └──┬─────────────┘ │
│ wait /health │ │ │
└──┬───────────────────┘ │ user: Stop │
│ error ▼ │
▼ ┌───────────────┐ │
┌──────────────────┐ │ STATE: Stopping│ │
│ STATE: Error │ │ SIGTERM+wait │ │
│ show error in │ └──┬─────────────┘ │
│ console, wait │ ▼ │
│ for user action │ ┌───────────────┐ │
└──┬───────────────┘ │ STATE: Stopped │ │
│ └──┬─────────────┘ │
│ user: Retry │ user: Start │
▼ └───────────────────────▶│
(back to Starting) │
user: Restart ─────────▶ Stop then Start ──────────────┘
watchServer() 失敗 3 次 ──▶ STATE: Error非 reportFatal + os.Exit
```
跟現在最大的不同:**watchServer 觸發 3 次失敗時,不能再直接 `reportFatal + os.Exit(1)`**——那只是 server 死掉,使用者的控制台還有用,應該切到 Error state 等使用者手動 Restart。
---
### A2. Wails 視窗載入什麼(方案比較)
| 方案 | 描述 | 成本 | 風險 | 推薦? |
|------|------|------|------|-------|
| **A1繼續 go:embed 極簡 HTML/JS/CSS**(沿用現有 splash 路徑)| 直接改寫 `visiona-local/frontend/` 的 splash 成真正的控制台 UI保留 ES module + Wails bindings + events | ⭐ 最低(~200 行 JS、一張 index.html、一份 style.css| 控制台要長成什麼樣要自己刻,少 shadcn 之類 lib。但這是 3 個 widget 的頁面,手刻 1-2 天完成 | **✅ 推薦** |
| **A2Go server 內的 `/control` 路由**(和主 UI 同 server瀏覽器看不到| 把控制台 UI 也塞進 `server/web/out/`,用 Gin 中介層鎖「只有 `X-Wails-Control: 1` header 才能看」 | ⭐⭐⭐ 高(要兩個 SPA 共用 Next.js project + middleware 阻擋瀏覽器存取)| **根本性問題**server 還沒起來時Wails 視窗要載什麼Chicken-and-egg。而且 server 死掉時控制台也跟著死,等於沒控制台可用 | ❌ 否決 |
| **A3獨立的 Vite/Next.js mini project**(專為 Wails 控制台)| 新開一個 `visiona-local/console-ui/` 有自己的 build pipeline | ⭐⭐ 中(新增 node build 依賴、Tailwind/shadcn 可復用、更好看)| 對一個只有 3 個 widget 的頁面工具鏈太重。build 時間變長(~30s| ⚠️ 不推薦(除非要做很多控制台 UI |
| **A4純原生 Wails UI不 embed 任何網頁)**| 用 Wails 的 native 模式(但 Wails v2 不支援真原生)| — | Wails v2 一律要有 embedded WebView這方案根本不存在 | ❌ 技術上不可行 |
**推薦 A1**。理由:
- 控制台複雜度低status text + log scrollview + 4 個按鈕),手刻 vanilla 最直接
- 已經有現成 splash 範本app.js + style.css + index.html改寫比從零快
- 不引入 Next.js / Vite build 依賴Wails binary 體積不受影響
- Log panel 不需要 shadcn用原生 `<pre>` + CSS 就夠
- 未來若真的要加很多功能再重構成 A3
**必要改動**
```
visiona-local/frontend/
├── index.html ← 改寫成控制台 layout
├── app.js ← 改寫:訂閱 server:log/status events呼叫 Start/Stop/Restart bindings
├── style.css ← 新增 log panel 樣式
├── components/ ← 視需要拆出來log-panel.js / action-bar.js
└── wailsjs/ ← 自動生成,不動
```
---
### A3. Log 顯示機制
**核心技術決策**:伺服器 stdout/stderr 同時寫檔 + 寫 ring buffer + 推送 Wails event。
#### 現況app.go 481-516 行)
目前 `startServer()` 是把 server 的 stdout/stderr 直接 assign 給 `os.File`
```go
cmd.Stdout = stdoutLog // os.File, 寫到 logs/server.stdout.log
cmd.Stderr = stderrLog
```
**限制**
- 沒有任何地方能「讀」這些 log 回來
- Wails 前端拿不到 server log
- 使用者看 log 必須自己去 `~/Library/Application Support/visiona-local/logs/`
#### 新方案:多重 writer + 環形緩衝
```go
type LogBuffer struct {
mu sync.Mutex
lines []LogLine // ring buffercap=2000
head int
size int
subs map[uint64]chan LogLine
nextSub uint64
}
type LogLine struct {
Ts time.Time `json:"ts"`
Stream string `json:"stream"` // "stdout" / "stderr"
Line string `json:"line"`
Level string `json:"level,omitempty"` // 解析過的 levelinfo/warn/error
}
// 每條新 log
// 1. 寫入 logs/server.{stdout,stderr}.log持久化
// 2. append 進 ring buffer供初次載入 + "過去 2000 行"
// 3. broadcast 給訂閱者Wails events
func (b *LogBuffer) Write(stream, line string) { ... }
```
`startServer()` 改成:
```go
stdoutPipe, _ := cmd.StdoutPipe()
stderrPipe, _ := cmd.StderrPipe()
go a.logPump(stdoutPipe, "stdout", stdoutLog)
go a.logPump(stderrPipe, "stderr", stderrLog)
```
`logPump` 同時寫檔 + 寫 ring buffer + EventsEmit
```go
func (a *App) logPump(pipe io.ReadCloser, stream string, fileWriter io.Writer) {
scanner := bufio.NewScanner(pipe)
scanner.Buffer(make([]byte, 64*1024), 1024*1024) // 避免長 log line 爆
for scanner.Scan() {
line := scanner.Text()
fmt.Fprintln(fileWriter, line)
a.logBuf.Append(stream, line)
wailsRuntime.EventsEmit(a.ctx, "server:log", map[string]any{
"stream": stream,
"line": line,
"ts": time.Now().UnixMilli(),
})
}
}
```
#### 前端訂閱app.js in Wails frontend
```javascript
import { EventsOn } from '../wailsjs/runtime/runtime.js';
import { GetRecentLogs } from './wailsjs/go/main/App.js';
// 1. 初次載入:抓 ring buffer 裡已累積的行
const initial = await GetRecentLogs(2000);
logPanel.append(initial);
// 2. 持續訂閱
EventsOn('server:log', (line) => logPanel.append([line]));
```
#### 關鍵參數與風險
| 項目 | 值 | 理由 |
|------|----|------|
| Ring buffer 大小 | 2000 行 | ~200KB 記憶體,足以涵蓋「開啟控制台時看到最近 10 分鐘」 |
| 單行最大長度 | 1MB | 防止超長 log堆疊追蹤爆 scanner |
| Events 批次 | 每行 emit 一次 | 初版先簡單,若壓力大再改 micro-batch10ms window |
| 持久化 | 仍寫 `logs/server.{stdout,stderr}.log` | 不改現況;控制台 log panel 只是即時視圖 |
| Log 檔 rotation | 暫不做M1 就沒做)| 已在 `tray-and-lifecycle.md` 風險清單。建議跟這次重構一起做:按大小 rotate10MB × 5 份)|
**高頻 log 壓力評估**
- 推論一次 frame 若列一行 log → 30 fps × 即時 stream = 30 events/sec
- Wails IPC 在 macOS 上實測可輕鬆吞 >1000 events/sec
- 不是瓶頸。但**推論的 frame 狀態不應該用 log 傳**——server 應該用 WebSocket `/ws/inference/frames`
- 控制台只看 app 級別 log不看 per-frame log
**⚠️ 待確認**:目前 `server/pkg/logger` 是否有把 `per-frame` 塞進 stdout需要翻一下。若有必須先降級成 debug-only。
---
### A4. Server 生命週期控制
#### 目前狀態(`app.go`
- `startServer()`spawn → health check → 寫 ipc-port → 啟動 watchServer
- `stopServer()`cancel watch goroutine → `proc.stop()`SIGTERM → 5s → SIGKILL
- **沒有 Restart**。要重啟只能重開整個 Wails app
- `watchServer()` 3 次失敗 → `reportFatal``os.Exit(1)` → 連 Wails app 一起死
#### 新方案:補齊 Start/Stop/Restart彼此互斥
新增 bindings
```go
// Wails bindings
func (a *App) StartServer() error // 若 Runningno-op
func (a *App) StopServer() error // 若 Stoppedno-op
func (a *App) RestartServer() error // stop then start
func (a *App) GetServerState() ServerState // Running | Stopping | Starting | Stopped | Error
```
實作約束:
1. **互斥**:用 `sync.Mutex` 保護 state 欄位,任何時候只能有一個操作進行中
2. **Restart 期間 port 可重用**`pickPort(preferredPort=<old_port>)`,但若被別的程序搶了就用新 portipc-port 檔必須更新
3. **Python runtime 不重跑**`a.pythonBin` / `a.pythonModeR` 已 resolvedStop 後留著Start 時直接重用(省 5-10s
4. **Log buffer 不清空**Stop 時只 reset `server:status` eventlog panel 保留既往 log加個分隔線
#### Watch 行為變更(**重大修改**
現在:
```go
if failures >= 3 {
a.reportFatal("server died", ...) // os.Exit(1) — 連 Wails app 一起死
return
}
```
新版:
```go
if failures >= 3 {
a.setServerState(ServerStateError, "health check failed 3 times")
wailsRuntime.EventsEmit(a.ctx, "server:dead", ...)
// 不 os.Exit讓控制台 UI 顯示錯誤狀態,使用者可手動 Restart / 查看 log
return
}
```
這個是 **Q7 決策復議** 的關鍵理由之一(見 C2
- 現況的邏輯假設「沒有 server 就沒有 app」但新方向的 Wails app 就是要提供「server 死掉後給人看 log + 決定下一步」的地方
- 保留 reportFatal 只留給「完全無法啟動」的致命錯誤(例如 data-dir 建不起來、lock 取不到)
#### Restart 期間的瀏覽器 tab
這是體驗層的核心問題。Restart 過程(最快 ~3s含 SIGTERM grace + 新 server spawn + health check
```
t=0.0s 使用者按 Restart
t=0.0s SIGTERM → 舊 server
t=0.5s 舊 server 優雅結束
t=0.5s spawn 新 server可能是同 port
t=2.0s health check 通過
t=2.0s 寫新 ipc-port若 port 變更)
```
瀏覽器 tab 在 t=0~2s 期間的請求會 connection refused / ECONNREFUSED。方案
| 方案 | 描述 | 推薦? |
|------|------|-------|
| **R1前端 retry + toast** | Next.js 全域 fetch interceptor 偵測 ECONNREFUSED重試 5 次 × 500ms同時顯示 toast「伺服器重啟中」 | ✅ 推薦,且這不是大工 |
| **R2Wails 代理 proxy** | 讓 Wails app 自己起一個固定 port 做 reverse proxybackend 切換時透明處理 | ❌ 成本太高,且 Wails 關了瀏覽器更慘 |
| **R3不處理**| 使用者看到 ERR_CONNECTION_REFUSED 錯誤頁,自己按 reload | ❌ 體驗爛 |
R1 可搭配「port 若變更時 Wails 控制台 emit event瀏覽器透過 WS 接收後 location.reload()」,但要注意瀏覽器 tab **不可能**透過 Wails bindings 收事件——它就是個 HTTP client。解法server 啟動後暴露 `/api/system/boot-id` endpoint前端 poll 比對,不一致就 reload。
---
### A5. Open in Browser 機制
#### 現況(`platform_*.go`
`openBrowser(url string)` 已經實作,`app.go:329``OpenBrowser` binding。實際定義在 `platform_darwin.go` / `platform_linux.go` / `platform_windows.go`
- macOS`exec.Command("open", url).Start()`
- Linux`exec.Command("xdg-open", url).Start()`
- Windows`rundll32 url.dll,FileProtocolHandler url``cmd /c start url`
**可直接沿用,不用重寫**
#### 要加的東西
1. **URL 組合**`http://127.0.0.1:<actual_port>/`,從 `a.server.port`
2. **控制台按鈕**:呼叫 `OpenBrowser(url)` binding
3. **自動開啟(選配)**:使用者決定要不要在 server 就緒後自動開瀏覽器
- 預設「就緒後自動開一次」(符合 Ollama / Docker Desktop 行為)
- Settings 可關閉
- 避免每次 Restart 都多開一個 tab要記錄「這個 session 已自動開過」)
4. **多次點擊** :瀏覽器會自動 reuse 同一個 tab若 URL 相同),我們不用處理
#### Wails runtime 有沒有 BrowserOpenURL
Wails v2 runtime 有 `BrowserOpenURL(ctx, url)` 可用,但它底層也是 shell out。我們現有的 `openBrowser` 已經封裝得跟 Wails 一樣,不必換。
---
### A6. 依賴瘦身
#### 可以砍的
| 項目 | 大小 | 來源 | 原因 |
|------|------|------|------|
| `vendor/yt-dlp/` | **87MB** | M6-2 | 新方向完全不用 URL 推論 |
| `server/internal/camera/video_source.go``ResolveWithYTDLP` | <1KB | | yt-dlp 呼叫點 |
| `server/internal/api/handlers/camera_handler.go` `ytdlpHosts` / `classifyVideoURL` / `StartFromURL` | ~100 | | URL classify + resolve |
| `server/internal/api/router.go` `api.POST("/media/url", ...)` | 1 | | endpoint |
| `server/internal/deps/checker.go` `check("yt-dlp", ...)` | ~3 | | deps 檢查 |
| `server/main.go` yt-dlp PATH 注入註解 | | | 註解 |
| `frontend/src/components/camera/source-selector.tsx` `videoMode === 'url'` 分支 | ~50 | | UI |
| `frontend/src/stores/camera-store.ts` `startFromUrl` | ~25 | | store action |
| `frontend/src/lib/i18n/{zh-TW,en,types}.ts` `camera.pasteUrl` / `urlPlaceholder` / `urlHelpText` key | ~10 keys × 2 lang | | i18n |
| payload-*.sh 中的 `vendor-ytdlp` 相關 stage 步驟 | | M6-3 | 打包 |
| `Makefile` `vendor-ytdlp` target | | M6-2 | build |
| `server/internal/deps` `VISIONA_BUNDLE_BIN_DIR` yt-dlp 的偵測 | ~10 | M6-4 | env 注入 |
**總砍量**
- Binary size**~87MB**dmg 220MB 降到 ~135MB
- Source~200 Go + ~100 TS/TSX + 10 幾個 i18n keys
#### 必須保留的
**ffmpeg 必須留**新需求:「上傳影片 avi/mpeg/mp4」。這些格式的解碼靠 ffmpegKneron pipeline 吃的是 ffmpeg 產出的 MJPEG frame stream現狀 `UploadVideo` handler 已支援 `.mp4 / .avi / .mov`再加個 `.mpeg / .mpg` 即可
**但 ffmpeg GPL release blocker 還在**狀況比之前更尷尬
- 之前 URL 推論是賣點之一GPL 對使用情境比較 justify
- 現在 ffmpeg 的唯一用途是解碼一個使用者自己選的本地檔」,使用情境非常窄
- **機會**使用情境變窄後LGPL build feature set 更容易覆蓋不需要 x264 encoder只需要 decoder
- **建議**這次 refactor 是重新談 ffmpeg GPL 的好時機可以考慮切到 LGPL build 或甚至改用 Go 純粹解碼例如 `github.com/zergon321/reisen` libav-via-Go但還是需要 ffmpeg lib...
**Camerawebcampipeline**也需要 ffmpegmacOS AVFoundation deviceWindows dshowLinux v4l2)。這條保留
#### 決策點
- **yt-dlp** 無爭議
- **ffmpeg** 保留 LGPL 切換評估
---
### A7. 程式碼變更清單(砍哪些 / 改哪些)
#### 砍(刪整段 / 整檔)
| 檔案 | 改動 | 行數 |
|------|------|------|
| `vendor/yt-dlp/` 整個目錄 | | 87MB |
| `Makefile` `vendor-ytdlp` target + `payload-*` yt-dlp stage | | ~20 |
| `server/internal/camera/video_source.go` | `ResolveWithYTDLP` + `friendlyYTDLPError` | L91-L160 ~70 |
| `server/internal/api/handlers/camera_handler.go` | `ytdlpHosts` map + `urlKind` enum + `classifyVideoURL` + `StartFromURL` + `stopActivePipeline` 裡的 URL cleanup若有| L341-L500 ~160 |
| `server/internal/api/router.go` | `api.POST("/media/url", ...)` | L83 1 |
| `server/internal/deps/checker.go` | yt-dlp check entry | L30-L32 3 |
| `server/main.go` | yt-dlp PATH 相關註解 binDir 已放其他東西則 binDir 注入保留| ~5 |
| `frontend/src/components/camera/source-selector.tsx` | `videoMode` `'url'` 分支`pasteUrl` 按鈕URL inputhelper text | L51, L190-L260 ~70 |
| `frontend/src/stores/camera-store.ts` | `startFromUrl` action + 相關 state | L32, L167-L195 ~30 |
| `frontend/src/lib/i18n/zh-TW.ts` / `en.ts` / `types.ts` | `camera.pasteUrl` / `urlPlaceholder` / `urlHelpText` / 相關 errors | ~12 × 3 |
#### 砍(新方向要求)
| 檔案 | 改動 |
|------|------|
| `frontend/src/stores/model-store.ts` UX 決策| 確認上傳模型對應的只是使用者自訂 .nef 路徑使用者說模型除了預設的幾種只能用上傳的」= 保留現況預置 + 上傳不砍 |
#### 改
| 檔案 | 改動 | 預估行數 |
|------|------|---------|
| `visiona-local/frontend/index.html` | 改寫成控制台 layoutstatus / log panel / actions| ~80 |
| `visiona-local/frontend/app.js` | 改寫訂閱 server:log events呼叫 Start/Stop/RestartOpenBrowser**移除** 自動跳轉邏輯 | ~150 |
| `visiona-local/frontend/style.css` | log panel + buttons + status badge 樣式 | ~150 |
| `visiona-local/app.go` | 新增 LogBuffer + logPump + StartServer/StopServer/RestartServer bindings + GetRecentLogs + ServerStatewatchServer 不再 os.Exit | ~300 行新增 / 20 行刪改 |
| `visiona-local/main.go` | EventsEmit 支援已有若需要 tray 則補 options.App SystemTray | ~10 |
| `server/internal/api/handlers/camera_handler.go` `UploadVideo` | 擴充支援的副檔名`.mp4, .avi, .mov, .mpeg, .mpg`(「瀏覽器能吃的集合與 Kneron pipeline 吃得到的集合的交集| L251 1 行改 |
| `frontend/src/components/camera/source-selector.tsx` | `accept=".mp4,.avi,.mov,.mpeg,.mpg"` | 1 |
#### 新寫
| 檔案 | 用途 |
|------|------|
| `visiona-local/log_buffer.go` | Ring buffer + pubsub |
| `visiona-local/server_control.go` | StartServer / StopServer / RestartServer + state machine |
| `visiona-local/frontend/components/log-panel.js` | Log 顯示元件可選可與 app.js 合併|
---
### A8. 控制台 UI 技術選型(詳細)
A2 推薦 A1vanilla JS/HTML/CSS這裡補細節
**UI 結構建議**
```
┌─────────────────────────────────────────────────────────────────┐
│ visionA-local │
├─────────────────────────────────────────────────────────────────┤
│ Server Status │
│ ● Running • http://127.0.0.1:3721 • PID 42131 │
│ Python: system (/usr/bin/python3.12) │
├─────────────────────────────────────────────────────────────────┤
│ [▶ Start] [■ Stop] [⟲ Restart] [🌐 Open in Browser] │
│ [📁 Reveal Logs] │
├─────────────────────────────────────────────────────────────────┤
│ Server Log [Pause] [Clear] [Save] │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ 14:23:01 [INFO] visiona-local-server starting on :3721 ││
│ │ 14:23:01 [INFO] python bridge path: /usr/bin/python3.12 ││
│ │ 14:23:02 [INFO] loaded 8 preset models ││
│ │ 14:23:02 [INFO] HTTP server listening on 127.0.0.1:3721 ││
│ │ 14:23:15 [INFO] GET /api/system/info 200 2.1ms ││
│ │ ... ││
│ └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
```
**元件清單vanilla**
- Status card一個 `<div>` + CSSdata `GetServerStatus()` 輪詢或更好`EventsOn('server:status', ...)`
- Action bar5 button對應 5 bindings
- Log panel`<pre class="log">` with auto-scroll + virtual scrolling不需要第三方~100 JS 手刻
- Pause button停止 auto-scroll方便使用者檢查過去的 log
- Filtertext inputclient-side substring filterM2 再加
- Save ring buffer dump txt
**Dark mode**跟隨系統M1 已有 `prefers-color-scheme: dark`)。
**i18n**中英雙語 `navigator.language` 判斷 + code 兩份文字不用引 i18next)。或直接用現有 `server/web/out` i18n 資料檔不推薦兩邊耦合)。
**預估工作量**1.5-2 人天
---
### A9. 資料安全 / 綁定 interface
#### 現況
`server/main.go` 的預設 `--host` 是什麼 `config.go` 看應是可 flag 控制`visiona-local/app.go:468` 明寫
```go
args := []string{
"--host", "127.0.0.1",
...
}
```
**目前是強制 127.0.0.1**瀏覽器只能從同一台機器連
#### 新方向的安全情境
新方向把可在瀏覽器使用變成 first-class會不會有人希望能從區網另一台電腦連例如PM Mac 上跑 visionA-local工程師想從筆電瀏覽器連進來看推論結果
**方案比較**
| 方案 | 描述 | 風險 | 推薦 |
|------|------|------|------|
| **N1維持 127.0.0.1,不提供選項** | 只能本機連 | 無新風險 | 預設 |
| **N2Settings 新增「允許區網存取」toggle** | `0.0.0.0` + firewall 提示 | 必須加 auth token否則區網任何人都能控制裝置 / 上傳模型macOS/Windows 會跳 firewall 警告 | 有需求再做 |
| **N3LAN mode with bearer token** | N2 + 自動產生 token + Open in Browser 時帶 token query param + Cookie | 成本全套 auth middleware + session破壞 fetch auth的簡潔現況 | M1-M7 沒這需求 |
**建議 N1**若使用者有需求M8+ 再加 N2/N3
#### 127.0.0.1 的隱性安全假設
- 本機其他 process 可以連上 visiona-local-server例如惡意的 Electron app)。這是現況就有的風險
- 沒有 CORS 限制預設 gin 允許 localhost 跨來源)。在瀏覽器情境下 **要確認**使用者如果在 Chrome 開了其他網站那些網站能不能 `fetch('http://127.0.0.1:3721/api/models/upload')`
- 答案預設 **可以送請求** CORS 預檢會擋 server 沒回 `Access-Control-Allow-Origin`讀不到回應
- ** POST 是會被送出去的**CSRF risk
- **⚠ 建議**這次 refactor 時把 CORS 政策明確定義只允許 `Origin: http://127.0.0.1:*` `Origin: http://localhost:*`不允許其他 origin
- 這個議題在Wails WebView 模式下沒什麼威脅因為 WebView origin `wails://`沒人能偽造但移到瀏覽器後要當真
---
## B. 方案選項彙總表
| 議題 | 選項 | 推薦 | 工時 | 備註 |
|------|------|------|------|------|
| Wails 視窗載入 | A1 vanilla / A2 server route / A3 mini Vite / A4 native | **A1** | 1.5-2 | |
| Log 顯示 | stdoutfile only / + ring buffer + events / + WebSocket 分支 | **ring buffer + events** | 0.5 | |
| Server 控制 | 現有only start/ Stop/Restart | **加 Stop/Restart + state machine** | 1 | |
| Open in Browser | 沿用 `openBrowser` / Wails `BrowserOpenURL` | **沿用** | <1h | |
| yt-dlp | 保留 / | **砍** | 0.5 | |
| ffmpeg | 保留 GPL / LGPL / Go 解碼 | **保留,但安排 LGPL 評估** | | 跟使用者討論 |
| 影片格式 | `.mp4,.avi,.mov` / + `.mpeg,.mpg` / 所有 ffmpeg 能吃 | **`.mp4,.avi,.mov,.mpeg,.mpg`** | <1h | 對齊瀏覽器能吃 |
| 綁定 interface | 127.0.0.1 / 0.0.0.0 toggle / LAN + token | **127.0.0.1(維持)** | | |
| CORS 政策 | 不設寬鬆/ 只允許 localhost | **只允許 127.0.0.1/localhost** | 0.5 | 瀏覽器模式下必做 |
| Lifecycle Q7 復議 | 維持關閉=結束 / 改為關閉=隱藏 / tray | **復議,見 C2** | | 需要使用者決策 |
| watchServer 失敗行為 | os.Exit / Error state 等使用者 | **Error state** | 0.5 | |
---
## C. 和既有決策的衝突
### C1. 砍 tray 決策Q-A=A3是否復議
#### 原本的理由(第三輪決策)
> Q-A Tray 角色衝突Q7 選關閉=結束後 tray 價值變低)→ A3 砍掉 tray省跨平台圖資產與 Wails tray 踩坑。
原本邏輯是:「Q7 選關閉=結束程式,所以 tray 的核心價值關視窗後還能找回 app消失乾脆砍」。
#### 新方向下的衝突
新方向的本質是**server 是主角Wails app 只是控制台**。這產生幾個矛盾
1. **使用者主要時間在瀏覽器** Wails 控制台關了使用者還想停 server 或看 log就沒有入口
2. **使用者不關 Wails 控制台**因為不想斷 server桌面就一直有個不太用的視窗體驗像強迫你開著 Docker Desktop 的主視窗
3. **瀏覽器 tab 關了之後**使用者怎麼再開如果 Wails 控制台也關了就得重開 app
**Tray 在新方向下的價值反而提高了**
#### 方案比較
| 方案 | 描述 | 優點 | 缺點 | 推薦 |
|------|------|------|------|------|
| **T1維持砍 trayWails 視窗必須開著** | 現況 | 不用跨平台 tray 資產不用處理 Wails systray 踩坑 | 使用者被迫開著一個沒人看的視窗體驗倒退 | |
| **T2復活 traymacOS menu bar / Win systray / Linux indicator** | tray icon Show / Hide Console / Open in Browser / Start / Stop / Quit 選單 Wails 視窗 = minimize to tray | 主場使用者可關視窗但保留 server菜單直接提供核心動作 | Wails v2 tray 支援有限macOS OKWindows OKLinux DE要準備 icon 資產~10 KB可能會有個別 Linux DE 不支援KDE/GNOME StatusNotifierItem | |
| **T3不做 tray改「關視窗 = 隱藏視窗」**app 仍在前景| Wails 不支援 hide to dock onlymacOS 本來就有 `runtime.WindowHide` Dock 圖示仍在 | 不用 tray 資產 | Linux 下完全找不到那個視窗沒有 DockWindows 下工作列會殘留空白按鈕 | 只做 macOS 可接受跨平台不佳 |
| **T4Wails app 當 daemonOpen in Browser 是唯一入口** | 無視窗只有 tray | 最極致的 server-as-main | Wails v2 不支援完全無主視窗模式實作困難 | |
**推薦 T2復活 tray**但要給使用者決策
- 使用者是否接受跨平台 tray 資產icon × 3 平台 + light/dark
- 是否接受 Linux tray 可能在某些 DE work 的風險fallback視窗仍可關/最小化
**tray 菜單建議**
```
visionA-local
━━━━━━━━━━━━━━━━━
● Server: Running (3721)
─────────
Show Console ⌘0
Open in Browser ⌘B
─────────
▶ Start Server
■ Stop Server
⟲ Restart Server
─────────
Reveal Logs
About
Quit ⌘Q
```
#### 估工時
- Wails tray 綁定參考 [wails tray examples](https://wails.io/docs/reference/runtime/systray)~0.5
- Icon 資產 edge-ai-platform tray-*.png 可復用或新做~0.5
- 菜單行為 + 跨平台測試~0.5
- **合計 1.5 **
---
### C2. Q7 關閉視窗=結束 app 決策是否復議
#### 原本的理由(第二輪決策)
> Q7 視窗關閉行為 → **B** 傳統式(關閉 = 結束程式)
原本是因為 tray 砍了Q-A又要簡化體驗所以關視窗 = 結束 app最直觀
#### 新方向下的衝突
**非常嚴重**若維持 Q7=B
```
使用者打開 Wails 控制台 → 按 Open in Browser → 瀏覽器 tab 跑得好好
使用者覺得控制台占地方 → 關掉 Wails 視窗 → Wails shutdown → server 被 SIGTERM → 瀏覽器 tab 所有請求 ECONNREFUSED → 使用者傻眼
```
這不只是體驗問題是功能失效
#### 方案
必須改三個選項
| 方案 | 關閉視窗行為 | 真正結束 app 的方式 | 推薦 |
|------|------------|---------------------|------|
| **Q7-B1關視窗 = minimize to tray**配合 T2| hide windowtray 還在 | tray > Quit / 視窗內 Quit 按鈕 / ⌘Q | **✅ 配套 T2 最順** |
| **Q7-B2關視窗 = hide無 tray**(配合 T3| hide windowDock/taskbar 圖示留著 | macOS右鍵 Dock > Quit其他重開後 Quit | ⚠️ Linux 困難 |
| **Q7-B3關視窗 = confirm dialog 問「也要結束 server 嗎?」** | 使用者選擇 | — | ❌ 太囉嗦 |
**推薦 Q7-B1**(綁 T2 復活 tray
#### 額外細節
- **macOS 慣例本來就是**「紅色關閉鈕 = hide」+「⌘Q = quit」。Q7 原本選 B 是反慣例。現在回歸 macOS 慣例反而更好
- **Windows 慣例**× = quit。如果這邊改成 hide要明確顯示 tray tooltip「visionA-local 仍在執行」,避免使用者以為沒關
- **Linux**:看 DE通常 × = quit但實務上 GNOME 有些 app 有 tray fallback
---
## D. 遷移策略與沿用率
### 沿用率評估
| 模組 | 現況 | 新方向需要 | 沿用率 | 備註 |
|------|------|-----------|-------|------|
| `server/` 整個 Go 後端 | M1-M7 完成 | 幾乎全部保留 | **~95%** | 砍 yt-dlp 相關 + 加 UploadVideo 格式CORS 政策調整;預置模型系統不動 |
| `server/web/out/` Next.js embedded SPA | 完整業務 UI | 保留,幾乎不變 | **~90%** | 砍 URL tabupload 支援 extension 擴充;可能加 server restart 時的 reconnect 邏輯 |
| `visiona-local/frontend/` splash | 78 行 splash | 重寫成控制台80+150+150=~400 行)| **結構可沿用,程式砍掉寫新的** | 保留 ES module 載入、wailsjs 串接模式 |
| `visiona-local/app.go` startup/shutdown/lifecycle/IPC | 1584 行 | 新增 300 行 server control + log buffer改 ~30 行 watchServer 行為,其餘不動 | **~85%** | 核心 startup 順序、Python 雙策略、single-instance、driver install、data migration 全保留 |
| `visiona-local/payload/` | 完整 | 砍 yt-dlp stage其餘不變 | **~90%** | |
| 安裝器Inno Setup / dmgbuild / AppImage| M4/M5/M7-A | 不變 | **100%** | |
| Build pipeline / Makefile | 完整 | 砍 vendor-ytdlp target其餘不變 | **~95%** | |
**總體**:這次 refactor **不是丟掉重做**,是**擴充 + 砍功能**。M1-M7 的打包功夫 **100% 保留價值**,唯一被動搖的是:
1. Wails 視窗長什麼樣splash → 控制台)
2. server 生死邏輯connected to app lifetime → independent lifecycle
3. 關視窗行為quit → hide
### 工時預估
| 任務 | 工時 | 前置條件 |
|------|------|---------|
| A. 砍 yt-dlp 全套vendor + server + frontend + i18n + Makefile + payload| 0.5-1 天 | 使用者確認砍 |
| B. log buffer + log pump + GetRecentLogs binding | 0.5 天 | — |
| C. StartServer/StopServer/RestartServer bindings + state machine | 1 天 | B 完成 |
| D. Wails 控制台 UI 改寫vanilla| 1.5-2 天 | B, C 完成 |
| E. watchServer 改為 Error state不 os.Exit| 0.5 天 | C 完成 |
| F. Tray復活 Q-A| 1.5 天 | 使用者確認復活 tray |
| G. Q7 復議為 hide-to-tray | 0.5 天 | 使用者確認 + F 完成 |
| H. CORS 限制為 localhost | 0.5 天 | — |
| I. 瀏覽器 tab restart 重連邏輯(前端 retry + boot-id 比對)| 1 天 | C 完成 |
| J. UploadVideo 擴充副檔名 | 0.5h | — |
| K. ffmpeg LGPL 切換評估與可能的重 build pipeline | ? | 需另估 |
| L. 端到端重 build + smoke testmacOS + Windows | 1 天 | 全部完成 |
| M. PRD / Design spec / TDD 補文件M 級 refactor| 1 天 | 使用者確認範圍 |
**總計****~10 人天**K 不估)。比「從零做 M1」便宜得多。
### 風險的遷移成本
| 風險 | 影響 |
|------|------|
| **Wails v2 tray 在 Linux 某些 DE 壞掉** | T2 可能在 Ubuntu+GNOME 沒 icon需要 fallback 邏輯(指向「請使用主視窗」)|
| **Wails events 在 Windows 下壓力測試未知** | 若 server 產生大量 logWails IPC 可能掉 event。需要 log pump 加 rate limiting |
| **瀏覽器 tab 的 security context 問題** | 現有前端程式碼假設 origin 固定,移到瀏覽器後要確認 localStorage / sessionStorage 行為 |
| **ffmpeg LGPL 重新評估需時間** | 若使用者要趁這次切build pipeline 要重構,會延長 2-3 天 |
---
## E. 待使用者決策的問題(技術選型類)
| # | 問題 | 選項 | Architect 推薦 |
|---|------|------|---------------|
| **E-1** | Wails 控制台技術選型 | A1 vanilla / A2 同 server 路由 / A3 Vite mini / A4 native | **A1 vanilla** |
| **E-2** | 是否復活 trayQ-A 復議)| 維持砍T1/ 復活T2/ 只做 hideT3| **T2 復活** |
| **E-3** | 視窗關閉行為Q7 復議)| 維持關=quitB/ 改為 hide-to-trayB1/ 確認對話框B3| **B1 hide-to-tray綁 E-2** |
| **E-4** | yt-dlp 處理 | 砍 / 保留 | **砍**(無爭議)|
| **E-5** | ffmpeg 授權處理 | 維持 GPL 繼續開發、發佈前決定 / 現在就切 LGPL / 改純 Go 解碼 | **維持 GPL但把 LGPL 評估列為 M8 必做**(因為使用情境變窄,切起來成本變低)|
| **E-6** | 支援的上傳影片副檔名 | 現有 mp4/avi/mov / + mpeg/mpg / 所有 ffmpeg 能吃的 | **mp4/avi/mov/mpeg/mpg**(使用者明示「瀏覽器能吃的」)|
| **E-7** | 綁定 interface | 只 127.0.0.1 / 加區網 toggle | **只 127.0.0.1**M1-M7 延續)|
| **E-8** | CORS 政策 | 現況(可能寬鬆)/ 明確限制 127.0.0.1 + localhost | **限制**(瀏覽器模式下必做)|
| **E-9** | Restart 時瀏覽器 tab 重連 | 不處理 / 前端 retry + boot-id 比對 / Wails reverse proxy | **前端 retry + boot-id** |
| **E-10** | watchServer 失敗後行為 | 維持 os.Exit / 改為 Error state | **改為 Error state**(無爭議)|
| **E-11** | 自動開瀏覽器 | server 就緒後自動開一次 / 永不自動 / 使用者設定 | **預設自動開Settings 可關**(對齊 Ollama/Docker Desktop|
| **E-12** | Log panel 大小 | 500/1000/2000/5000 行 | **2000 行**~200KB平衡記憶體與可用性|
---
## F. 風險觀察
| # | 風險 | 等級 | 緩解 |
|---|------|------|------|
| F-1 | Wails tray 在 Linux GNOME 下可能完全看不到 iconStatusNotifierItem 協議在某些版本沒支援)| 🔴 高 | 加 fallback視窗仍可 minimize 到 dock提供「一律顯示視窗」的 config |
| F-2 | 瀏覽器 tab 與 Wails 控制台同時存在,兩邊 state 會分歧(例如 Wails 控制台顯示「Running」瀏覽器 fetch 失敗)| 🟠 中 | 控制台的 status 來自 Wails 本地狀態(準確),瀏覽器的失敗要優雅處理(全域 error boundary|
| F-3 | 使用者習慣以「× 關閉」結束程式,改成 hide 後回頭找 app 困難 | 🟠 中 | 第一次 hide 時彈 toast 告訴使用者「visionA-local 正在背景執行,可從 tray/menu bar 找到」|
| F-4 | Wails EventsEmit 在極高頻率下(例如每秒 >100 events可能丟事件或延遲 | 🟡 低 | log pump 加 rate limiting + batch 合併;推論 frame 不走 log |
| F-5 | ffmpeg GPL 發佈前 review 仍未解決 | 🔴 高 | 新方向下使用情境變窄,反而是切 LGPL 的好時機;建議 M8 專門處理 |
| F-6 | 瀏覽器的 CSRF / CORS 攻擊面(任何網站都能嘗試 fetch 127.0.0.1:3721| 🟠 中 | CORS 限制 + 必要時加 state-changing 操作的 CSRF tokenPOST /api/models/upload 等)|
| F-7 | 使用者同時開多個 visionA-local 視窗single-instance 被喚起目前「raise existing window」會失效因為 Wails 視窗可能被 hide 到 tray| 🟡 低 | `/ipc/raise` 除了 WindowShow 還要呼叫 WindowUnminimise + 從 tray 彈出 |
| F-8 | Restart 期間若使用者 close 視窗state machine 亂掉 | 🟡 低 | state 轉換要用 mutex 保護;關視窗也透過 command queue |
| F-9 | Wails app 關閉時若 server 還活著,下次啟動 port 可能佔用 | 🟡 低 | 已有 isOurStaleServer 偵測;新方向若 server 要獨立生命週期要重新評估「Wails 關閉是否真的能殺 server」或「要不要保留 daemon」 |
| F-10 | 瀏覽器快取問題:使用者更新 visionA-local 後,瀏覽器 tab 仍用舊版前端 | 🟠 中 | server 啟動時在每個 response 加 `ETag``Cache-Control: no-store`;或升級時改用新 boot-id前端偵測到強制 reload |
| F-11 | macOS Gatekeeper原本 Wails app 開瀏覽器 = shell out `open`,正常。但若加 trayicon 檔可能觸發 code signing 問題 | 🟡 低 | icon 打在 binary 內,不走 shell out |
| F-12 | 使用者可能期待 Wails app 關掉 = server 也停(避免 USB 裝置被占用)| 🟠 中 | Quit 動作tray > Quit 或 ⌘Q要明確停 serverhide 不停;需要清楚的 UX 差異 |
---
## G. 給 PM 和 Design 的議題
Architect 不作決定,只標記需要 PM / Design 處理的事)
### 給 PM
- **G-P1任務等級** — 這次 refactor 涉及 UX 模式變更Wails 窗 = 控制台、server 獨立生命週期)+ 新 user story「從瀏覽器使用 visionA-local」+ 決策復議Q-A、Q7。**我的判斷是 L 級**(新 user story + 原決策復議),需要 PM 更新 PRD 的「功能願景」「使用情境」「非功能需求」段落,並重新跑 RICE
- **G-P2定位重寫** — PRD 開頭的定位(「單機桌面應用,像 Docker Desktop」需要改為「本機 AI inference 服務,提供 server + 網頁介面,像 Ollamaollama serve + 網頁 / CLI client」。這不只是包裝是產品定位的實質變更
- **G-P3使用者旅程重寫** — 從「打開 app → 在視窗內操作」變成「打開 app → 控制台確認 server 在跑 → 在瀏覽器操作」
- **G-P4成功指標** — 現有 AC「首次啟動 < 5 」「關視窗 = 結束」)有一半要改
- **G-P5yt-dlp 砍功能的 user-facing 影響** 有沒有 PM 認為URL 推論是需要保留的功能若無YouTube / Vimeo / RTSP 這些 URL 都不支援了需不需要在 release note 明說
- **G-P6ffmpeg LGPL 評估**要不要排 M8 milestone
- **G-P7:「預設模型只能用預設的幾種其他只能上傳」** 現況已如此預置 + 使用者上傳是確認還是有新要求要不要 PM 確認
- **G-P8法律** 新方向下使用情境更窄GPL 評估可能更好談TOS/Privacy 要看是否有新的資料流動實際上沒有
### 給 Design
- **G-D1Wails 控制台 UI 設計** 雖然我推薦 vanilla JS 實作 layout視覺語言用字深色淺色規範還是需要 Design 出稿
- **G-D2tray 菜單 i18n + icon 設計** E-2 確認復活tray icon 需要新做至少 light/dark × 3 平台 × 2 狀態 = 12 icon
- **G-D3Wails 視窗關閉時的第一次提示 toast** visionA-local 正在背景執行可從 menu bar 找到要怎麼寫什麼時機
- **G-D4Settings 新增自動開瀏覽器選項**的文案與位置
- **G-D5Restart 期間瀏覽器 tab skeleton state / loading overlay 設計**
- **G-D6Workspace URL tab 砍掉後的 UI 調整** source-selector.tsx 目前上傳檔案 / URL mode砍掉 URL 後要不要簡化成單 tab還是加入 camera device 選擇
---
## H. 下一步建議
1. **給使用者看 E 段的 12 個決策點** 等使用者裁決
2. **決策後更新 progress.md 的「未解決問題」** 清掉已經解決的ffmpeg GPL 在新方向下的 postureWails 主視窗用途新增新出現的tray 復活Q7 復議
3. **若使用者確認走這條路**
- PM更新 PRDL 級文件更新
- Design出控制台 + tray 設計稿
- Architect把這份筆記升級為正式的 Design Doc 更新 + TDD 補丁
- 一路走完三方交叉審閱後再進 M8 開發
---
**簽名**Architect Agent2026-04-14