# 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)。