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>
17 KiB
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 已驗證三層模型可行。採用三層的理由:
- 崩潰隔離:UI 殼(Wails)崩潰不應殺掉 server,server 崩潰不應殺掉 Python inference loop
- Python 必須獨立 interpreter:KneronPLUS SDK 是 C extension wheel,無法塞進 Go binary
- 重啟成本低:Go server 重啟只需 ~1 秒,不影響 Wails app 存活
- 沿用既有程式碼:
installer/app.go(894 行)與server/main.go(305 行)已驗證此模型 - 可獨立 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.pyspawn,改由內建假資料 provider 回應/api/devices/scaninference.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:
- 前端呼叫
POST /api/system/modebody{"mode":"mock"|"real"}(見api-endpoints.md) - Go server
device.Manager.SetMode():- real → mock:kill 既有 Python sidecar + 清空 device registry + 載入 mock devices
- mock → real:spawn Python sidecar + 呼叫 scan → 回填 device registry
- 所有進行中的 inference session 強制終止,前端收 WebSocket broadcast 後提示「模式已切換,請重新啟動推論」
- 這個切換僅影響 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):
// 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 啟動時執行一次遷移檢查:
- 掃描舊路徑:
~/.visiona-local/、~/Library/Application Support/visionA-local/、%APPDATA%\visionA-local\、~/.local/share/visionA-local/ - 若存在且新路徑(
visiona-local全小寫)不存在 →os.Rename整個目錄 - 在新路徑寫一個
.migrated-frombreadcrumb 檔記錄舊路徑與時間戳 - 若新路徑已存在 → 不動舊的,log 一行 warning 提示使用者手動清理
- 遷移失敗不擋啟動,只記 log
實作位置:visiona-local/data_migration.go(新寫,M2)。