visionA/local-tool/.autoflow/04-architecture/architecture-overview.md
jim800121chen c54f16fca0 Initial commit: visionA monorepo with local-tool subproject
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>
2026-04-11 22:10:38 +08:00

17 KiB
Raw Blame History

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 APIGin                                │
│   - WebSocket Hub                                       │
│   - Device Manager / Camera Manager / Inference Svc    │
│   - Model Repository                                    │
│   - Dependency checker                                  │
├─────────────────────────────────────────────────────────┤
│ Layer 4: 硬體橋接層                                      │
│   - Python sidecarkneron_bridge.py                  │
│   - KneronPLUS SDKpyusb + libusb + .dylib/.so/.dll  │
│   - ffmpegcamera pipeline / video transcode          │
│   - yt-dlpmedia/url                                 │
└─────────────────────────────────────────────────────────┘

2. 程序模型

三個獨立程序,透過 stdin/stdout JSON-RPClocalhost HTTP 通訊:

Process 1: visiona-local (Wails app binary, parentdisplay 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 HTTPlocalhost: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崩潰不應殺掉 serverserver 崩潰不應殺掉 Python inference loop
  2. Python 必須獨立 interpreterKneronPLUS SDK 是 C extension wheel無法塞進 Go binary
  3. 重啟成本低Go server 重啟只需 ~1 秒,不影響 Wails app 存活
  4. 沿用既有程式碼installer/app.go894 行)與 server/main.go305 行)已驗證此模型
  5. 可獨立 debug:開發時可以 go run main.go 跑 serverWails 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 IDInfo.plist 的 display name 仍為 visionA-local
~/Library/Application Support/visiona-local/
├── bin/
│   └── visiona-local-server                ← 解壓出的 Go binary
├── python/                                 ← 解壓出的 python-build-standalone
├── venv/                                   ← 建立的 venvpip install 進去)
├── data/
│   ├── models.json
│   ├── nef/
│   └── custom-models/                      ← 使用者上傳的 .nef
├── scripts/
├── logs/
│   ├── visiona-local.log
│   └── server.log
└── .installed                              ← 版本標記

Windows

C:\Program Files\visiona-local\             ← Wails binary + payloadInno Setup 安裝,檔名全小寫)
%APPDATA%\visiona-local\
├── bin\visiona-local-server.exe
├── python\
├── venv\
├── data\
├── scripts\
├── logs\
└── .installed

UbuntuAppImage

~/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 冷啟約 1sWails 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.ManagermockMode=true 時跳過 kneron_bridge.py spawn改由內建假資料 provider 回應 /api/devices/scan
  • inference.Service 在 Mock session 啟動時檢查 mockMode 旗標,走 in-process 假推論 pipeline隨機 bounding box、固定分類結果不經過 Python
  • Camera pipelinewebcam + 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 → mockkill 既有 Python sidecar + 清空 device registry + 載入 mock devices
    • mock → realspawn Python sidecar + 呼叫 scan → 回填 device registry
  3. 所有進行中的 inference session 強制終止,前端收 WebSocket broadcast 後提示「模式已切換,請重新啟動推論」
  4. 這個切換僅影響 Go server 的 inference backendWails app / server HTTP listener / WebView 都不重啟

9. Wails 檔案拖放代理層

Wails v2 的 OnFileDrop 回傳絕對路徑字串陣列(不是 HTML5 File 物件),與現有 frontend/src/lib/api/models.tsmultipart/form-data 上傳不相容。新增一條「Wails 代理上傳」路徑:

使用者拖 .nef 到 Models 頁
  ↓
Wails WebView 捕獲 drop event → runtime.OnFileDrop callback
  ↓
Wails appGo取得絕對路徑陣列 → 透過 Wails Bind API emit event 給前端
  ↓
前端收到路徑 → 呼叫 window.runtime.UploadFileByPath(path)
  ↓
Wails appGo讀取檔案 → 包成 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 啟動時執行一次遷移檢查:

  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