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

380 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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.

# 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-RPC****localhost 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 必須獨立 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` 跑 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
```
**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 冷啟約 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.Manager` `mockMode=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.ts``multipart/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**
```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