local-tool/: visionA-local desktop app
- M1: Wails shell + Go server + Next.js UI + Mock mode (macOS dmg ready)
- M2: i18n (zh-TW/en) + Settings 4-tab refactor
- M3: Embedded Python 3.12 runtime (python-build-standalone) + KneronPLUS wheels
- M4: Windows Inno Setup script (build on Windows runner)
- M5: Linux AppImage script + udev rule (build on Linux runner)
- M6: ffmpeg (GPL, pending legal review) + yt-dlp bundled
- Lifecycle: watchServer health check, fatal native dialog,
Wails IPC raise endpoint, stale process cleanup
.autoflow/: full PRD / Design Spec / Architecture / Testing docs
(4 rounds tri-party discussion + cross review)
.github/workflows/: macOS / Windows / Linux build CI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
380 lines
17 KiB
Markdown
380 lines
17 KiB
Markdown
# Architecture Overview — visionA-local
|
||
|
||
> 架構總覽、系統分層、程序模型、資料流、目錄結構
|
||
|
||
---
|
||
|
||
## 1. 系統分層
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ Layer 1: 使用者互動層 │
|
||
│ - Wails WebView(嵌入 Next.js 前端) │
|
||
│ - 原生 file picker / notification │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ Layer 2: 應用控制層(visiona-local Wails app binary) │
|
||
│ - 生命週期管理(啟動 / 停止 / single-instance) │
|
||
│ - Server 子行程管理(spawn / log / kill) │
|
||
│ - 首次安裝精靈(解壓 payload、建 venv、Python runtime) │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ Layer 3: 業務服務層(visiona-local-server Go binary) │
|
||
│ - HTTP REST API(Gin) │
|
||
│ - WebSocket Hub │
|
||
│ - Device Manager / Camera Manager / Inference Svc │
|
||
│ - Model Repository │
|
||
│ - Dependency checker │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ Layer 4: 硬體橋接層 │
|
||
│ - Python sidecar(kneron_bridge.py) │
|
||
│ - KneronPLUS SDK(pyusb + libusb + .dylib/.so/.dll) │
|
||
│ - ffmpeg(camera pipeline / video transcode) │
|
||
│ - yt-dlp(media/url) │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
## 2. 程序模型
|
||
|
||
三個獨立程序,透過 **stdin/stdout JSON-RPC** 與 **localhost HTTP** 通訊:
|
||
|
||
```
|
||
Process 1: visiona-local (Wails app binary, parent;display name 仍為 visionA-local)
|
||
│
|
||
├─ spawns ──→ Process 2: visiona-local-server (Go binary)
|
||
│ │
|
||
│ ├─ spawns ──→ Process 3: python3 kneron_bridge.py
|
||
│ │ (stdin/stdout JSON-RPC)
|
||
│ │
|
||
│ └─ spawns (on demand) ──→ ffmpeg / yt-dlp
|
||
│
|
||
└─ WebView loads ──→ http://127.0.0.1:3721/
|
||
```
|
||
|
||
### 2.1 程序間通訊
|
||
|
||
| 連結 | 協定 | 用途 |
|
||
|------|------|------|
|
||
| Wails app ↔ Go server | HTTP(localhost:3721)| 啟動後透過 `/api/system/health` 確認 server 活著;WebView 透過 `file://` → `http://127.0.0.1:3721/` 載入 Next.js |
|
||
| Wails app ↔ Go server(控制) | OS signal / exec | 結束時送 SIGTERM;重啟時 `os/exec` spawn 新程序 |
|
||
| Go server ↔ Python sidecar | stdin/stdout JSON-RPC | 沿用 `edge-ai-platform` 現有機制 |
|
||
| Go server ↔ ffmpeg | stdin/stdout pipe | MJPEG 串流 |
|
||
| Go server ↔ yt-dlp | subprocess + stdout | URL 下載 |
|
||
| Wails WebView ↔ Go server | HTTP + WebSocket | 前端 fetch / WS 訂閱 |
|
||
|
||
### 2.2 為何三層而不是 single binary
|
||
|
||
原 `edge-ai-platform` 已驗證三層模型可行。採用三層的理由:
|
||
|
||
1. **崩潰隔離**:UI 殼(Wails)崩潰不應殺掉 server,server 崩潰不應殺掉 Python inference loop
|
||
2. **Python 必須獨立 interpreter**:KneronPLUS SDK 是 C extension wheel,無法塞進 Go binary
|
||
3. **重啟成本低**:Go server 重啟只需 ~1 秒,不影響 Wails app 存活
|
||
4. **沿用既有程式碼**:`installer/app.go`(894 行)與 `server/main.go`(305 行)已驗證此模型
|
||
5. **可獨立 debug**:開發時可以 `go run main.go` 跑 server,Wails app 不用重編
|
||
|
||
### 2.3 視窗關閉行為(使用者決策 Q7 = B)
|
||
|
||
**關閉主視窗 = 結束整個程式**(Wails app → 主動 SIGTERM Go server → Python sidecar 跟著被 reaped)。
|
||
|
||
這與 Docker Desktop 的「關視窗只是收進 tray」行為**不同**——我們刻意選擇傳統桌面 app 模型。此外使用者決策 Q-A=A3 已砍掉 tray,沒有 tray icon 可以收。
|
||
|
||
## 3. 資料流(三個代表性情境)
|
||
|
||
### 3.1 首次啟動
|
||
|
||
```
|
||
使用者雙擊 .app
|
||
↓
|
||
Wails app 啟動 → 檢查資料目錄下的 .installed 標記
|
||
(macOS: ~/Library/Application Support/visiona-local/;Windows: %APPDATA%\visiona-local\;Linux: ~/.local/share/visiona-local/)
|
||
↓
|
||
(未安裝)→ 進入安裝精靈
|
||
├─ 解壓 embed FS → <資料目錄>/{bin, scripts, data}
|
||
├─ 偵測 Python runtime(策略 A → B fallback,見 dependency-bundling.md)
|
||
├─ 建 venv → pip install --no-index --find-links wheels/
|
||
├─ 安裝 KneronPLUS wheel
|
||
├─ (Windows)安裝 WinUSB driver → UAC 提示
|
||
├─ (Linux)寫入 /etc/udev/rules.d/99-kneron.rules → pkexec 提權
|
||
├─ 寫 .installed 標記
|
||
└─ 進入主流程
|
||
↓
|
||
主流程:spawn Go server → 等 /api/system/health 200 → WebView loads localhost:3721
|
||
↓
|
||
Dashboard 顯示
|
||
```
|
||
|
||
### 3.2 連上 USB 裝置
|
||
|
||
```
|
||
使用者插入 Kneron USB
|
||
↓
|
||
前端按「掃描」→ POST /api/devices/scan
|
||
↓
|
||
Go server → spawn python3 kneron_bridge.py scan
|
||
↓
|
||
Python → kp.scan_devices() → 回傳裝置列表
|
||
↓
|
||
Go server → WebSocket push /ws/devices/events → 前端更新卡片
|
||
↓
|
||
使用者按「connect」→ POST /api/devices/:id/connect
|
||
↓
|
||
Go server → 維持一個持續的 Python sidecar 程序連線
|
||
```
|
||
|
||
### 3.3 關閉應用
|
||
|
||
```
|
||
使用者按視窗關閉 / ⌘Q / Cmd+Q
|
||
↓
|
||
Wails app → 攔截 close event → 呼叫 cleanupAndExit()
|
||
↓
|
||
├─ 送 SIGTERM 給 Go server PID
|
||
├─ 等 3 秒 graceful shutdown
|
||
│ ├─ Go server → shutdownFn() → inferenceSvc.StopAll() / httpServer.Shutdown()
|
||
│ └─ Go server → 送 SIGTERM 給 python sidecar PID
|
||
├─ 3 秒 timeout → SIGKILL
|
||
└─ Wails app exit
|
||
```
|
||
|
||
## 4. 目錄結構(建議)
|
||
|
||
### 4.1 開發時(source tree)
|
||
|
||
```
|
||
/Users/jimchen/visionA/local-tool/
|
||
├── .autoflow/ ← Autoflow 文件(PRD、設計、架構...)
|
||
├── .claude/
|
||
├── Makefile
|
||
├── README.md
|
||
├── go.work ← 統一 visiona-local + server 的 go modules
|
||
├── visiona-local/ ← Wails app(前身 installer/)
|
||
│ ├── main.go
|
||
│ ├── app.go
|
||
│ ├── embed.go
|
||
│ ├── platform_darwin.go
|
||
│ ├── platform_linux.go
|
||
│ ├── platform_windows.go
|
||
│ ├── wails.json
|
||
│ ├── frontend/ ← Wails 內部 minimal UI(非業務前端)
|
||
│ ├── payload/ ← build 時 stage 的資料
|
||
│ │ ├── bin/
|
||
│ │ │ └── visiona-local-server
|
||
│ │ ├── data/
|
||
│ │ │ ├── models.json
|
||
│ │ │ └── nef/
|
||
│ │ ├── scripts/
|
||
│ │ │ ├── kneron_bridge.py
|
||
│ │ │ ├── requirements.txt
|
||
│ │ │ ├── wheels/ ← python-build-standalone + Kneron wheels
|
||
│ │ │ └── ffmpeg
|
||
│ │ └── python/ ← 內嵌的 python-build-standalone runtime
|
||
│ │ ├── bin/python3
|
||
│ │ └── lib/...
|
||
│ ├── drivers/ ← Windows WinUSB driver 檔
|
||
│ │ └── amd64/
|
||
│ └── build/ ← wails build 產物
|
||
├── server/ ← Go 業務後端
|
||
│ ├── main.go
|
||
│ ├── go.mod
|
||
│ ├── internal/
|
||
│ │ ├── api/
|
||
│ │ │ ├── handlers/
|
||
│ │ │ ├── ws/
|
||
│ │ │ ├── middleware.go
|
||
│ │ │ └── router.go
|
||
│ │ ├── camera/
|
||
│ │ ├── config/
|
||
│ │ │ └── config.go
|
||
│ │ ├── deps/
|
||
│ │ ├── device/
|
||
│ │ ├── driver/
|
||
│ │ ├── inference/
|
||
│ │ └── model/
|
||
│ ├── pkg/
|
||
│ │ └── logger/
|
||
│ ├── data/
|
||
│ │ ├── models.json
|
||
│ │ └── nef/
|
||
│ ├── scripts/
|
||
│ │ ├── kneron_bridge.py
|
||
│ │ └── requirements.txt
|
||
│ └── web/ ← go:embed 前端產物
|
||
│ └── out/
|
||
├── frontend/ ← Next.js 業務前端
|
||
│ ├── src/app/
|
||
│ │ ├── page.tsx ← Dashboard
|
||
│ │ ├── devices/
|
||
│ │ ├── models/
|
||
│ │ ├── workspace/
|
||
│ │ └── settings/
|
||
│ ├── package.json
|
||
│ └── next.config.ts
|
||
└── dist/ ← 最終發行物
|
||
├── visiona-local-v1.0.0-macos-x64.dmg
|
||
├── visiona-local-v1.0.0-windows-x64.exe
|
||
└── visiona-local-v1.0.0-linux-x64.AppImage
|
||
```
|
||
|
||
### 4.2 安裝後(使用者機器)
|
||
|
||
**macOS**:
|
||
```
|
||
/Applications/visiona-local.app/ ← Wails binary + 內嵌 payload(檔名全小寫,對齊 Bundle ID;Info.plist 的 display name 仍為 visionA-local)
|
||
~/Library/Application Support/visiona-local/
|
||
├── bin/
|
||
│ └── visiona-local-server ← 解壓出的 Go binary
|
||
├── python/ ← 解壓出的 python-build-standalone
|
||
├── venv/ ← 建立的 venv(pip install 進去)
|
||
├── data/
|
||
│ ├── models.json
|
||
│ ├── nef/
|
||
│ └── custom-models/ ← 使用者上傳的 .nef
|
||
├── scripts/
|
||
├── logs/
|
||
│ ├── visiona-local.log
|
||
│ └── server.log
|
||
└── .installed ← 版本標記
|
||
```
|
||
|
||
**Windows**:
|
||
```
|
||
C:\Program Files\visiona-local\ ← Wails binary + payload(Inno Setup 安裝,檔名全小寫)
|
||
%APPDATA%\visiona-local\
|
||
├── bin\visiona-local-server.exe
|
||
├── python\
|
||
├── venv\
|
||
├── data\
|
||
├── scripts\
|
||
├── logs\
|
||
└── .installed
|
||
```
|
||
|
||
**Ubuntu**(AppImage):
|
||
```
|
||
~/Applications/visiona-local-v1.0.0-x86_64.AppImage ← 使用者自己放的 AppImage(檔名全小寫)
|
||
~/.local/share/visiona-local/
|
||
├── bin/
|
||
├── python/
|
||
├── venv/
|
||
├── data/
|
||
├── scripts/
|
||
├── logs/
|
||
└── .installed
|
||
```
|
||
|
||
## 5. 核心模組責任表
|
||
|
||
| 模組 | 位置 | 責任 |
|
||
|------|------|------|
|
||
| **Wails app** | `visiona-local/` | 生命週期、安裝精靈、WebView |
|
||
| **HTTP API** | `server/internal/api/` | REST + WebSocket 路由與 handlers |
|
||
| **Device Manager** | `server/internal/device/` | USB 裝置列舉 / 連線 / 狀態 |
|
||
| **Camera Manager** | `server/internal/camera/` | webcam 列舉 / MJPEG 串流 / 影片上傳 |
|
||
| **Model Repository** | `server/internal/model/` | 預置模型 + 使用者自訂 .nef |
|
||
| **Inference Service** | `server/internal/inference/` | 推論 pipeline(對接 Python sidecar) |
|
||
| **Python Bridge** | `server/scripts/kneron_bridge.py` | 封裝 KneronPLUS SDK |
|
||
| **Logger** | `server/pkg/logger/` | 統一 log + WebSocket broadcaster |
|
||
| **Config** | `server/internal/config/` | CLI flags / env vars |
|
||
| **Deps Checker** | `server/internal/deps/` | 啟動時檢查 python / ffmpeg 可用性 |
|
||
|
||
## 6. 非功能性需求對應
|
||
|
||
| NFR | 對應機制 |
|
||
|-----|---------|
|
||
| 完全離線 | 依賴全部內嵌(Python runtime、wheels、ffmpeg、yt-dlp、模型)。詳見 `dependency-bundling.md` |
|
||
| 啟動 < 5 秒 | Go server 冷啟約 1s;Wails WebView 首次載入 Next.js ~2s;總計 3-4s |
|
||
| idle CPU < 5% | **Mock 模式完全不 spawn Python sidecar**(重要:不是「閒置時不跑」,而是「整個 Mock session 期間都不會有 python 程序」,詳見第 7 節);`inferenceSvc` 只有在 active session 才啟動 worker |
|
||
| 單機多裝置 | `device.Registry` 支援多個並存(原專案已有) |
|
||
| 跨平台一致 UI | Next.js 前端跨平台一致;三平台皆使用同一份 WebView 內容 |
|
||
| 安裝檔 < 500MB | 實測預估 ~300-350MB(見 packaging.md 第 4 節) |
|
||
|
||
---
|
||
|
||
## 7. Mock 模式的程序模型(重要)
|
||
|
||
**Mock 模式(`--mock` 或 Settings 切換)下,Go server 完全不 spawn Python sidecar。** 這是為了守住 Mock idle RAM 上限(≤ 600MB,第四輪決策 R4-4)並避免無謂的 KneronPLUS 載入失敗噪音。
|
||
|
||
實作要點:
|
||
- `device.Manager` 在 `mockMode=true` 時跳過 `kneron_bridge.py` spawn,改由內建假資料 provider 回應 `/api/devices/scan`
|
||
- `inference.Service` 在 Mock session 啟動時檢查 `mockMode` 旗標,走 in-process 假推論 pipeline(隨機 bounding box、固定分類結果),不經過 Python
|
||
- Camera pipeline(webcam + ffmpeg)仍啟動,因為 ffmpeg 不依賴 Kneron
|
||
- Mock ↔ Real 切換時(見第 8 節)才會在必要時 spawn/kill Python sidecar
|
||
|
||
## 8. Mock ↔ Real 模式切換
|
||
|
||
使用者可在 Settings > 一般 切換「執行模式」,**不需要重啟整個 app**,只需要切 inference backend:
|
||
|
||
1. 前端呼叫 `POST /api/system/mode` body `{"mode":"mock"|"real"}`(見 `api-endpoints.md`)
|
||
2. Go server `device.Manager.SetMode()`:
|
||
- real → mock:kill 既有 Python sidecar + 清空 device registry + 載入 mock devices
|
||
- mock → real:spawn Python sidecar + 呼叫 scan → 回填 device registry
|
||
3. 所有進行中的 inference session 強制終止,前端收 WebSocket broadcast 後提示「模式已切換,請重新啟動推論」
|
||
4. 這個切換僅影響 Go server 的 inference backend,Wails app / server HTTP listener / WebView 都不重啟
|
||
|
||
## 9. Wails 檔案拖放代理層
|
||
|
||
Wails v2 的 `OnFileDrop` 回傳**絕對路徑字串陣列**(不是 HTML5 File 物件),與現有 `frontend/src/lib/api/models.ts` 走 `multipart/form-data` 上傳不相容。新增一條「Wails 代理上傳」路徑:
|
||
|
||
```
|
||
使用者拖 .nef 到 Models 頁
|
||
↓
|
||
Wails WebView 捕獲 drop event → runtime.OnFileDrop callback
|
||
↓
|
||
Wails app(Go)取得絕對路徑陣列 → 透過 Wails Bind API emit event 給前端
|
||
↓
|
||
前端收到路徑 → 呼叫 window.runtime.UploadFileByPath(path)
|
||
↓
|
||
Wails app(Go)讀取檔案 → 包成 multipart → POST http://127.0.0.1:{port}/api/models/upload
|
||
↓
|
||
Go server 接收(走原本的 upload handler,無需修改)
|
||
```
|
||
|
||
實作位置:
|
||
- `visiona-local/file_drop.go`(新寫)
|
||
- 前端 `frontend/src/lib/wails/file-drop.ts`(新寫)
|
||
- `wails.json` 需設 `"dragAndDrop": {"enableFileDrop": true}`
|
||
|
||
**這條代理層同時適用於 Models 上傳、Workspace 的影片 / 圖片拖入、批次圖片匯入**。
|
||
|
||
M1 不做(只做點選上傳),**M2 納入**(前端清理完成後)。
|
||
|
||
## 10. OS 通知實作
|
||
|
||
對應第四輪決策 R4-8:
|
||
|
||
| 情境 | 機制 | 實作 |
|
||
|------|------|------|
|
||
| 裝置連接 / 斷線 | **App 內 toast**(前端處理) | shadcn `toaster` + WebSocket `/ws/devices/events` 觸發 |
|
||
| Server 崩潰 | **原生 OS 通知**(Wails app 處理) | shell out 方式,無需引入第三方 Go 套件 |
|
||
|
||
**Server 崩潰原生通知實作(shell out):**
|
||
|
||
```go
|
||
// visiona-local/native_notify.go
|
||
func ShowNativeNotification(title, body string) {
|
||
switch runtime.GOOS {
|
||
case "darwin":
|
||
script := fmt.Sprintf(`display notification %q with title %q`, body, title)
|
||
exec.Command("osascript", "-e", script).Run()
|
||
case "linux":
|
||
exec.Command("notify-send", title, body).Run()
|
||
case "windows":
|
||
// 用 PowerShell + BurntToast 或直接 New-BurntToastNotification
|
||
ps := fmt.Sprintf(`New-BurntToastNotification -Text "%s","%s"`, title, body)
|
||
exec.Command("powershell", "-NoProfile", "-Command", ps).Run()
|
||
}
|
||
}
|
||
```
|
||
|
||
**降級策略**:若 shell out 失敗(例如 Linux 無 `notify-send`、Windows 無 BurntToast 模組),退回純前端 toast + logging,不炸 app。
|
||
|
||
## 11. 舊資料目錄遷移
|
||
|
||
為了兼容早期開發階段可能殘留的舊命名(例如隱藏資料夾 `~/.visiona-local/` 或舊大寫 `~/Library/Application Support/visionA-local/`),Wails app 啟動時執行一次遷移檢查:
|
||
|
||
1. 掃描舊路徑:`~/.visiona-local/`、`~/Library/Application Support/visionA-local/`、`%APPDATA%\visionA-local\`、`~/.local/share/visionA-local/`
|
||
2. 若存在且新路徑(`visiona-local` 全小寫)不存在 → `os.Rename` 整個目錄
|
||
3. 在新路徑寫一個 `.migrated-from` breadcrumb 檔記錄舊路徑與時間戳
|
||
4. 若新路徑已存在 → 不動舊的,log 一行 warning 提示使用者手動清理
|
||
5. 遷移失敗不擋啟動,只記 log
|
||
|
||
實作位置:`visiona-local/data_migration.go`(新寫,M2)。
|