依 autoflow-agent workspace v2 設計把 PRD / 設計 / 架構 / 交付類 共享文件從個人層 .autoflow/(ignored)搬到 docs/autoflow/(進 git), 讓團隊可共享產品與架構文件,個人層只留 progress / review / testing 等 per-branch 筆記。 - 02-prd/ 21 個檔(PRD、features、market-analysis 等) - 03-design/ 18 個檔(design-spec、wireframes、flows 等) - 04-architecture/ 31 個檔(TDD、design-doc、ADR×14、API 規格等) - 07-delivery/ 3 個檔(project-summary、phase-0.6-handover、stage-deployment-setup) 合計 73 檔。原檔已從 .autoflow/ 移除(migration 工具執行 git mv, 但因 .autoflow/ 在 .gitignore 中、git 將此操作視為新增、無 rename history)。
977 lines
52 KiB
Markdown
977 lines
52 KiB
Markdown
# visionA Agent — Technical Design Document(局部 TDD)
|
||
|
||
## Metadata
|
||
|
||
| 項目 | 內容 |
|
||
|------|------|
|
||
| 文件角色 | **局部 TDD** — 只規範 visionA Agent 這一塊,不重寫 visionA-backend / visionA-frontend 的 TDD |
|
||
| 作者 | Architect Agent |
|
||
| 最後更新 | 2026-04-22(v0.2 — 對齊使用者裁決 C1/C2) |
|
||
| 狀態 | Approved — frontend 改 Next.js(沿用 local-tool / visionA-frontend stack)|
|
||
| 上位文件 | `.autoflow/04-architecture/design-doc.md`、`.autoflow/04-architecture/TDD.md`(既有)、`.autoflow/03-design/visiona-agent-spec.md` |
|
||
| 下位文件 | `adr/adr-007-visiona-agent-architecture.md`、`adr/adr-008-tunnel-client-reuse.md`、`adr/adr-009-token-storage.md` |
|
||
| 讀者 | 要實作 visionA Agent 的 Backend Agent + Frontend Agent |
|
||
|
||
---
|
||
|
||
## 1. 產品定位回顧
|
||
|
||
visionA Agent 是 visionA 雲端版的 **local agent / tunnel bridge**,部署在使用者桌機。
|
||
|
||
### 1.1 它**是**什麼
|
||
|
||
- 一個完整的 **local-tool server**(沿用 KneronPLUS / camera / inference / device / model / Python runtime / ffmpeg 全部邏輯)
|
||
- 加上 **tunnel client**(reverse tunnel 到雲端 remote-proxy)
|
||
- 加上 **3 頁極簡配置 UI**(狀態 / 配對 / 設定)
|
||
- Wails v2 桌面應用(macOS DMG / Windows EXE / Linux AppImage)
|
||
|
||
### 1.2 它**不是**什麼
|
||
|
||
- 不是「純 tunnel bridge」(它**必須**跑完整 server 邏輯,因為要操作 Kneron 裝置)
|
||
- 不是 local-tool 的修改版(local-tool 不動;visionA Agent 是 **fork 後獨立演進**)
|
||
- 不是 headless CLI(本次雛形是 Wails 桌面應用)
|
||
- 不保留 local-tool 原本的裝置 / 模型 / 推論操作 UI(這些改由雲端 Web UI 負責)
|
||
|
||
### 1.3 與 local-tool 的職責差異(視覺化)
|
||
|
||
```
|
||
┌─────────────────────────────┐ ┌─────────────────────────────┐
|
||
│ local-tool │ │ visionA Agent │
|
||
│ │ │ │
|
||
│ [前端 Next.js 完整 UI] │ │ [前端 Next.js 3 頁配置 UI] │
|
||
│ 裝置頁 / 模型頁 / 推論頁 │ │ 狀態 / 配對 / 設定 │
|
||
│ 工作區 / 儀表板 │ │ (output: 'export' static) │
|
||
│ ↓ HTTP │ │ ↓ Wails runtime │
|
||
│ [Gin server :3721] │ │ [Wails app.go] │
|
||
│ 所有業務 handler │ │ Pair / Disconnect /... │
|
||
│ │ │ ↓ │
|
||
│ │ │ [Tunnel client] │
|
||
│ │ │ WSS to remote-proxy │
|
||
│ │ │ ↓ 轉發進來的 HTTP │
|
||
│ │ │ [Gin server 127.0.0.1:RND] │
|
||
│ │ │ 同 local-tool 的 handler │
|
||
│ ↓ │ │ ↓ │
|
||
│ [Kneron / Camera / Python] │ │ [Kneron / Camera / Python] │
|
||
│ └─ 完全一樣 ────────────────────────────────────────┘ │
|
||
└─────────────────────────────┘ └─────────────────────────────┘
|
||
```
|
||
|
||
**結論:server 邏輯 100% 一樣,差別在於「前端 UI 的職責」與「多了一個 tunnel client」**。
|
||
|
||
---
|
||
|
||
## 2. 整體架構
|
||
|
||
### 2.1 元件圖:雲端資料流中的 visionA Agent
|
||
|
||
```
|
||
┌──────────┐ HTTPS ┌─────────────┐ internal HTTP ┌──────────────┐
|
||
│ Browser │ ───────────────►│ api-server │────────────────►│ remote-proxy │
|
||
│ (cloud │ │ (stateless) │◄────────────────│ (stateful, │
|
||
│ web UI) │ └─────────────┘ │ yamux hub) │
|
||
└──────────┘ └──────┬───────┘
|
||
│
|
||
WSS + yamux (出站長連線)
|
||
│
|
||
▼
|
||
┌────────────────────────────────────────┐
|
||
│ visionA Agent(使用者桌機) │
|
||
│ │
|
||
│ ┌─────────────┐ ┌──────────────┐ │
|
||
│ │ Tunnel │ │ 前端 SPA │ │
|
||
│ │ Client │ │ 3 頁配置 UI │ │
|
||
│ │ (yamux) │ │ (Wails WebView) │
|
||
│ └──────┬──────┘ └──────┬───────┘ │
|
||
│ │ 每個 stream │ Wails bind│
|
||
│ ▼ ▼ │
|
||
│ ┌──────────────────────────────────┐ │
|
||
│ │ Internal HTTP Server │ │
|
||
│ │ 127.0.0.1:<random port> │ │
|
||
│ │ (Gin, 不對外;沿用 local-tool API)│ │
|
||
│ └─────────┬────────────────────────┘ │
|
||
│ │ │
|
||
│ ┌─────────▼────────────────────────┐ │
|
||
│ │ Reused server packages │ │
|
||
│ │ device / model / inference / │ │
|
||
│ │ camera / flash / deps │ │
|
||
│ └─────────┬────────────────────────┘ │
|
||
│ ▼ │
|
||
│ ┌────────────────────────────────┐ │
|
||
│ │ Python runtime + KneronPLUS │ │
|
||
│ │ (bundled) + ffmpeg │ │
|
||
│ └───────────┬────────────────────┘ │
|
||
└──────────────┼─────────────────────────┘
|
||
▼
|
||
Kneron USB 裝置
|
||
```
|
||
|
||
### 2.2 與 local-tool 執行時期比較
|
||
|
||
| 元件 | local-tool | visionA Agent |
|
||
|------|-----------|---------------|
|
||
| 前端 Next.js 打包產物 | 多頁完整 UI(`output: 'export'` static export) | **取代為** 3 頁極簡 UI(**同樣 Next.js + `output: 'export'`**) |
|
||
| Wails app shell | 有(`visiona-local/`) | 有(`visiona-agent/`),行為改造 |
|
||
| Gin HTTP server | 綁 `0.0.0.0:3721`(對外)| 綁 `127.0.0.1:<random>`(不對外) |
|
||
| Router | 所有 `/api/*`, `/ws/*` | **完全相同**(沿用 router.go) |
|
||
| `internal/device`, `internal/model`, `internal/inference`, `internal/camera`, `internal/flash`, `internal/deps`, `internal/driver` | ✅ | **整包複製,不改** |
|
||
| Python runtime / wheels / ffmpeg | bundled | **整包複製,不改** |
|
||
| Tunnel client | 無 | **新增**(sync 自 POC,見 ADR-008) |
|
||
| 配對 / 設定 UI | 無 | **新增**(3 頁,見 Design spec) |
|
||
| 開機自啟管理 | 無 | **新增**(3 平台實作) |
|
||
| 產品行銷 UI(Onboarding / ServerDashboard / LogViewer)| 有 | **移除或替換** |
|
||
|
||
---
|
||
|
||
## 3. 專案結構
|
||
|
||
**路徑:** `/Users/jimchen/visionA/local-agent/`
|
||
|
||
```
|
||
local-agent/ # 🆕 全新專案(fork from local-tool @ 2026-04-22)
|
||
│
|
||
├── wails.json # 改造:name / productName / bundle ID
|
||
├── Makefile # 改造:build targets 調整(見 §6)
|
||
├── go.mod # 改造:module visiona-agent(非 visiona-local)
|
||
├── go.sum
|
||
├── README.md # 🆕 新寫
|
||
├── .env.example # 🆕 VISIONA_AGENT_* env
|
||
│
|
||
├── cmd/ # 🆕 新增(local-tool 沒 cmd/,它 main.go 在根)
|
||
│ └── visiona-agent/
|
||
│ ├── main.go # 🆕 Wails 入口(取代 local-tool/visiona-local/main.go)
|
||
│ └── app.go # 🆕 Wails bindings + 生命週期
|
||
│
|
||
├── server/ # ✅ 整包複製自 local-tool/server/,不改
|
||
│ ├── main.go # ⚠️ 不用(改由 cmd/visiona-agent 內嵌啟動)
|
||
│ ├── go.mod
|
||
│ ├── internal/
|
||
│ │ ├── api/ # ✅ 整包複製
|
||
│ │ │ ├── router.go
|
||
│ │ │ ├── middleware.go
|
||
│ │ │ ├── handlers/ (system, model, device, camera...)
|
||
│ │ │ └── ws/ (device_events, inference, flash, system_ws, server_logs)
|
||
│ │ ├── camera/ # ✅ 整包複製
|
||
│ │ ├── config/ # ✅ 整包複製
|
||
│ │ ├── deps/ # ✅ 整包複製
|
||
│ │ ├── device/ # ✅ 整包複製
|
||
│ │ ├── driver/ # ✅ 整包複製
|
||
│ │ ├── flash/ # ✅ 整包複製
|
||
│ │ ├── inference/ # ✅ 整包複製
|
||
│ │ └── model/ # ✅ 整包複製
|
||
│ ├── pkg/
|
||
│ │ ├── logger/ # ✅ 整包複製
|
||
│ │ └── wsconn/ # ✅ 整包複製(tunnel client 會用到)
|
||
│ └── web/ # ⚠️ 不用;Wails 自己 embed 前端
|
||
│
|
||
├── internal/ # 🆕 Agent 專屬邏輯(非 server 邏輯)
|
||
│ ├── tunnel/ # 🆕 Tunnel client(複製自 POC,見 ADR-008)
|
||
│ │ ├── client.go # 從 edge-ai-platform/server/internal/tunnel/client.go
|
||
│ │ ├── backoff.go # 退避演算法(10s 心跳 / 30s 判定,對齊 TDD M-5)
|
||
│ │ └── client_test.go
|
||
│ ├── pairing/ # 🆕 配對邏輯
|
||
│ │ ├── exchanger.go # 呼叫雲端 exchange endpoint 換 Session Token
|
||
│ │ ├── validator.go # `^vAc_[0-9a-f]{32}$` 格式驗證
|
||
│ │ └── exchanger_test.go
|
||
│ ├── tokenstore/ # 🆕 Pairing / Session Token 持久化(見 ADR-009)
|
||
│ │ ├── store.go # TokenStore interface
|
||
│ │ ├── keychain_darwin.go # macOS Keychain
|
||
│ │ ├── keychain_windows.go # Windows Credential Manager
|
||
│ │ ├── keychain_linux.go # Secret Service (libsecret) or fallback
|
||
│ │ ├── encrypted_file.go # 雛形 fallback:AES-GCM + passphrase from OS
|
||
│ │ └── store_test.go
|
||
│ ├── agentconfig/ # 🆕 Agent 層級 config(不同於 server/internal/config)
|
||
│ │ ├── config.go # Relay URL / autostart / log level / reconnect strategy
|
||
│ │ ├── persist.go # 讀寫 YAML(位置見 §4.3)
|
||
│ │ └── config_test.go
|
||
│ ├── autostart/ # 🆕 開機自啟管理(3 平台)
|
||
│ │ ├── autostart.go # AutoStart interface
|
||
│ │ ├── autostart_darwin.go # LaunchAgent plist
|
||
│ │ ├── autostart_windows.go # Registry Run key
|
||
│ │ ├── autostart_linux.go # ~/.config/autostart/*.desktop
|
||
│ │ └── autostart_test.go
|
||
│ ├── connstate/ # 🆕 連線狀態機 + 事件推送
|
||
│ │ ├── state.go # online / offline / reconnecting / notPaired / error
|
||
│ │ ├── broadcaster.go # Wails EventsEmit("connection:status")
|
||
│ │ └── log_buffer.go # 最近 10 筆連線事件(給 RecentLog)
|
||
│ ├── logexport/ # 🆕 匯出 log(打包最近 7 天)
|
||
│ │ ├── exporter.go
|
||
│ │ └── exporter_test.go
|
||
│ └── httpserver/ # 🆕 wrap local-tool server 的啟動 / 停止
|
||
│ ├── controller.go # sync.Once + random port + 127.0.0.1 綁定
|
||
│ └── controller_test.go
|
||
│
|
||
├── frontend/ # 🆕 Next.js 16 + React 19 + TS5 + Tailwind 4 + Radix UI + Zustand 5
|
||
│ │ # ⭐ 完全對齊 local-tool / visionA-frontend stack(C1 裁決)
|
||
│ ├── package.json # 同 visionA-frontend 的 dependencies(複製後刪掉用不到的)
|
||
│ ├── next.config.mjs # `output: 'export'` + `images.unoptimized: true`
|
||
│ ├── tsconfig.json # ✅ 從 visionA-frontend 複製
|
||
│ ├── tailwind.config.ts # ✅ 從 visionA-frontend 複製(Design Tokens 一致)
|
||
│ ├── postcss.config.mjs # ✅ 從 visionA-frontend 複製
|
||
│ ├── components.json # ✅ 從 visionA-frontend 複製(shadcn 產生器設定,雛形不會再跑 CLI 但保留以利未來新增元件)
|
||
│ └── src/
|
||
│ ├── app/ # Next.js App Router
|
||
│ │ ├── layout.tsx # Root layout:Header + ThemeProvider + Toaster
|
||
│ │ ├── page.tsx # 3 tabs state machine(單頁切換,不走多 route)
|
||
│ │ ├── globals.css # ✅ 從 visionA-frontend 複製,不改一行
|
||
│ │ └── not-found.tsx # Wails 環境理論上不會走到,保留以避免 export 報錯
|
||
│ ├── components/
|
||
│ │ ├── layout/
|
||
│ │ │ ├── Header.tsx # h-14 + 3 tabs + ConnectionBadge
|
||
│ │ │ └── TabBar.tsx
|
||
│ │ ├── ui/ # ✅ 從 visionA-frontend `src/components/ui/` 複製(18 個 Radix UI primitives 全部)
|
||
│ │ │ ├── button.tsx # 雛形實際只用約 10 個,但全複製以利未來擴充
|
||
│ │ │ ├── card.tsx # (複製成本 = 0,有用就好)
|
||
│ │ │ ├── dialog.tsx
|
||
│ │ │ ├── input.tsx
|
||
│ │ │ ├── label.tsx
|
||
│ │ │ ├── tabs.tsx
|
||
│ │ │ ├── checkbox.tsx
|
||
│ │ │ ├── radio-group.tsx
|
||
│ │ │ ├── select.tsx
|
||
│ │ │ ├── alert-dialog.tsx
|
||
│ │ │ ├── alert.tsx
|
||
│ │ │ ├── sonner.tsx
|
||
│ │ │ ├── tooltip.tsx
|
||
│ │ │ ├── badge.tsx
|
||
│ │ │ ├── separator.tsx
|
||
│ │ │ ├── switch.tsx
|
||
│ │ │ ├── popover.tsx
|
||
│ │ │ └── dropdown-menu.tsx
|
||
│ │ ├── theme-provider.tsx # ✅ 從 visionA-frontend 複製(next-themes wrapper)
|
||
│ │ ├── status/
|
||
│ │ │ ├── StatusHero.tsx
|
||
│ │ │ ├── InfoCard.tsx
|
||
│ │ │ └── RecentLog.tsx
|
||
│ │ ├── pair/
|
||
│ │ │ └── PairForm.tsx
|
||
│ │ └── settings/
|
||
│ │ ├── SettingsConnection.tsx
|
||
│ │ ├── SettingsBehavior.tsx
|
||
│ │ ├── SettingsLog.tsx
|
||
│ │ ├── SettingsAbout.tsx
|
||
│ │ └── SettingsDanger.tsx
|
||
│ ├── views/ # 3 個 tab 對應的 view 元件(不是 Next.js page,因為走單頁 tabs)
|
||
│ │ ├── StatusView.tsx
|
||
│ │ ├── PairView.tsx
|
||
│ │ └── SettingsView.tsx
|
||
│ ├── hooks/
|
||
│ │ ├── use-connection-status.ts # Wails EventsOn + 初始拉取
|
||
│ │ ├── use-recent-log.ts
|
||
│ │ └── use-theme-sync.ts # ✅ 從 visionA-frontend 複製
|
||
│ ├── lib/
|
||
│ │ ├── bindings.ts # re-export Wails 自動產生的 TS binding(位於 wailsjs/go/main/App.d.ts)
|
||
│ │ ├── wails-runtime.ts # wrap @wailsapp/runtime EventsOn / EventsEmit;SSR-safe(雖然 export 不會 SSR,加 `typeof window !== 'undefined'` guard)
|
||
│ │ ├── i18n/
|
||
│ │ │ ├── index.ts # ✅ 結構從 visionA-frontend 複製(i18n loader / OS locale 偵測)
|
||
│ │ │ ├── zh-TW.ts # 見 Design spec §10,~135 keys(文案重寫)
|
||
│ │ │ └── en.ts # 同上
|
||
│ │ ├── utils.ts # ✅ 從 visionA-frontend 複製(cn / clsx helper)
|
||
│ │ └── format.ts # 遮蔽 session token / 時間格式
|
||
│ └── stores/
|
||
│ ├── connection-store.ts # zustand 5, 連線狀態
|
||
│ └── settings-store.ts # zustand 5, UI 層設定
|
||
│
|
||
├── visiona-agent/ # 🆕 Wails build dir(類似 local-tool 的 visiona-local/)
|
||
│ ├── wails.json # 見 §6.2
|
||
│ ├── payload/ # 執行時需要的 Python / wheels / ffmpeg
|
||
│ └── icon/ # 三平台 icon
|
||
│
|
||
├── vendor/ # Python runtime / wheels / ffmpeg(同 local-tool 結構)
|
||
│ ├── python/
|
||
│ ├── wheels/
|
||
│ └── ffmpeg/
|
||
│
|
||
├── scripts/ # 三平台 bootstrap / installer 腳本
|
||
│ ├── bootstrap-macos.sh
|
||
│ ├── bootstrap-windows.ps1
|
||
│ └── bootstrap-linux.sh
|
||
│
|
||
└── build/ # Installer 打包輸入 + 中間產物
|
||
├── macos/ (dmgbuild / create-dmg 設定)
|
||
├── windows/ (Inno Setup .iss)
|
||
└── linux/ (AppImage recipe)
|
||
```
|
||
|
||
### 3.1 標記說明
|
||
|
||
- **✅ 整包複製**:從 local-tool(或 visionA-frontend)原樣拿,**不改一行**
|
||
- **🆕 新增**:visionA Agent 專屬
|
||
- **⚠️ 不用 / 取代**:原本的檔案不會被載入(例如 `server/main.go`、`server/web/embed.go`)
|
||
|
||
### 3.2 Frontend 框架決策(C1:沿用 Next.js)
|
||
|
||
**結論:visionA Agent 前端用 Next.js 16 + `output: 'export'` 產出 static HTML/JS/CSS,由 Wails `go:embed` 嵌入。**
|
||
|
||
這個決策對齊使用者的 4 大核心原則第 4 條「**雲端 web UI 先抄 local-tool**」與整個 visionA 產品線的 stack 一致性:
|
||
|
||
| 專案 | Frontend 框架 | 部署形態 |
|
||
|------|---------------|---------|
|
||
| **local-tool** | Next.js 16 + `output: 'export'` | Wails 嵌入 |
|
||
| **visionA-frontend** | Next.js 16 | Vercel / 雲端 |
|
||
| **visionA Agent**(本專案)| **Next.js 16 + `output: 'export'`** | **Wails 嵌入** |
|
||
|
||
**為什麼放棄 Vite + SPA(修正前一版方案):**
|
||
|
||
- ❌ 違反原則 4「先抄 local-tool」— local-tool 已經在 Wails 用 Next.js static export 跑得好好的,再引入 Vite 是發明問題
|
||
- ❌ 兩套 build pipeline / config 形態(Vite vs Next.js)增加團隊維護心智成本
|
||
- ❌ visionA-frontend 的元件 / utils / Tailwind config 已經是 Next.js 形態,搬到 Vite 反而要改 import / `'use client'` directive 處理
|
||
|
||
**沿用 Next.js 的好處:**
|
||
|
||
- ✅ **元件直接複製**:`src/components/ui/*` 的 18 個 Radix UI primitives、`theme-provider.tsx`、`hooks/use-theme-sync.ts` 從 visionA-frontend 一行不改搬過來
|
||
- ✅ **Design Tokens 100% 一致**:`globals.css` + `tailwind.config.ts` 直接複製
|
||
- ✅ **i18n 結構沿用**:loader + OS locale 偵測邏輯複製,只改文案
|
||
- ✅ **Wails 友善已驗證**:local-tool 已經跑了一年多,`output: 'export'` + `images.unoptimized: true` + 單頁 tabs(不走 Next.js routing)的模式穩定
|
||
|
||
**Next.js 在 Wails 環境的注意事項:**
|
||
|
||
| 議題 | 處理方式 |
|
||
|------|---------|
|
||
| SSR / SSG | 不用;`output: 'export'` 純 client side |
|
||
| Image Optimization | `images.unoptimized: true`(local-tool 同樣設定) |
|
||
| Routing | **不走 Next.js routing**,用 useState 切 3 個 view(Wails 環境 location.href 行為奇怪,避免之)|
|
||
| `'use client'` | 所有元件預設加(沒有 server component 概念)|
|
||
| `next/font` | 可用;字型會被 export 到 `out/_next/static/media/` |
|
||
| API Routes | **不用**(Wails 不會跑 Next.js server)|
|
||
| Hydration | 不會發生(無 SSR)|
|
||
|
||
**Design spec §13.2 修正備註**:原本寫「不引入 Next.js(Agent 是純 SPA)」是 v0.1 版的方案,使用者裁決後已對齊為 Next.js。Design Agent 不需要動文件(spec 描述的是 UI 結構與 Design Tokens,與框架選型無關),只需要把 §13.2 的這句話視為 obsolete。
|
||
|
||
---
|
||
|
||
## 4. Tunnel 整合策略
|
||
|
||
### 4.1 Tunnel Client 從哪來
|
||
|
||
採用 **「程式碼複製」** 策略(見 ADR-008 完整分析):
|
||
|
||
- 從 `/Users/jimchen/visionA/visionA-backend/internal/tunnel/client.go` **複製**到 `local-agent/internal/tunnel/client.go`
|
||
- **不用** git submodule(跨 repo 管理複雜、CI 綁定多)
|
||
- **不用** go module replace(兩個專案 module 不同,replace 語義勉強)
|
||
- **不用** 共用 library(還沒有「共用 library repo」,POC 也是每個專案各自複製)
|
||
|
||
對應 `progress.md` 原則 1「local-tool 不要動,可以複製出來」的精神。
|
||
|
||
### 4.2 啟動時序
|
||
|
||
```
|
||
Wails app.go: OnStartup(ctx)
|
||
│
|
||
├─ 1. 載入 Agent config (relay URL, reconnect strategy, log level)
|
||
│
|
||
├─ 2. 啟動 Internal HTTP Server (Gin)
|
||
│ - bind 127.0.0.1:0 (OS 分配 random port)
|
||
│ - 掛上 local-tool server/internal/api/router.go 的所有 handler
|
||
│ - 保留 port number 給後續 tunnel 用
|
||
│
|
||
├─ 3. 從 TokenStore 讀取已保存的 Session / Pairing Token
|
||
│ ├─ 有 Session Token → state = connecting → 進入步驟 4
|
||
│ ├─ 有 Pairing Token (上次配對未完成) → state = connecting → 進入步驟 4
|
||
│ └─ 無 → state = notPaired → 停在配對頁
|
||
│
|
||
├─ 4. 啟動 Tunnel Client
|
||
│ - 以 Session Token (或 fallback Pairing Token) 連 relayURL
|
||
│ - Accept 到的 stream → 轉發到 step 2 的 127.0.0.1:<randomPort>
|
||
│ - 指數退避重連(1s → 2s → 4s → ... 上限 30s)
|
||
│ - yamux KeepAlive 10s / 30s 判定掉線(對齊 §4 心跳規範)
|
||
│
|
||
└─ 5. 訂閱 tunnel 狀態變化 → 透過 Wails EventsEmit("connection:status") 推前端
|
||
```
|
||
|
||
**Shutdown 時序**:`OnBeforeClose` → `tunnelClient.Stop()`(grace 5s)→ `httpServer.Shutdown(ctx)` → process exit。
|
||
|
||
### 4.3 配對流程(雛形 + Phase 1)
|
||
|
||
#### 雛形流程(當前要實作的)
|
||
|
||
```
|
||
使用者在 Wails UI 貼上 Pairing Token (vAc_...)
|
||
│
|
||
▼
|
||
agent 呼叫雲端: POST {relayHttpURL}/api/pairing/exchange
|
||
Body: { pairing_token: "vAc_..." }
|
||
│
|
||
▼
|
||
雲端 api-server:
|
||
- StaticPairingStore.Validate(token) → 雛形 env 比對
|
||
- 生成 Session Token (vAs_ + 64 hex)
|
||
- 回傳 { session_token: "vAs_..." }
|
||
│
|
||
▼
|
||
agent:
|
||
- TokenStore.SaveSession(session_token)
|
||
- TokenStore.DeletePairing() (雛形為了簡化可略)
|
||
- 啟動 tunnel client with session_token
|
||
│
|
||
▼
|
||
tunnel client 連 WS /tunnel/connect?token=vAs_...
|
||
雲端 remote-proxy 接受 → tunnel 建立
|
||
│
|
||
▼
|
||
agent 發事件 connection:status = "online"
|
||
前端 Toast "配對成功" + 0.5s 後切狀態頁
|
||
```
|
||
|
||
#### 雛形 vs 正式版 API 行為
|
||
|
||
| 事項 | 雛形 | 正式版 |
|
||
|------|------|--------|
|
||
| `/api/pairing/exchange` 端點 | **需要新增**(雛形 backend 還沒有)| 存在 |
|
||
| Pairing Token 來源 | 使用者從環境變數 / 雲端 web UI 取 `vAc_...`(雛形 web 已有 `/devices/pair`)| 同左 |
|
||
| Session Token 驗證 | 比對 config env | DB 查詢(兩階段完整) |
|
||
| Token TTL | 無 | Pairing 15min / Session 90 days |
|
||
|
||
**雛形 backend 新增端點(同步通知 Backend Agent):**
|
||
|
||
```
|
||
POST /api/pairing/exchange
|
||
Request body: { pairing_token: "vAc_[0-9a-f]{32}" }
|
||
Response body: { session_token: "vAs_[0-9a-f]{64}", expires_at: "2026-..." }
|
||
OR 401 { code: "token_invalid" | "token_expired" | "token_used" | "token_revoked" }
|
||
```
|
||
|
||
雛形 handler 實作:
|
||
|
||
```go
|
||
// visionA-backend/internal/api/handlers/pairing.go(現有檔案新增 method)
|
||
func (h *PairingHandler) Exchange(c *gin.Context) {
|
||
var req struct{ PairingToken string `json:"pairing_token" binding:"required"` }
|
||
if err := c.ShouldBindJSON(&req); err != nil { ... }
|
||
// 雛形 StaticPairingStore:比對 env 值
|
||
if req.PairingToken != os.Getenv("VISIONA_PAIRING_TOKEN") {
|
||
c.JSON(401, gin.H{"code": "token_invalid"})
|
||
return
|
||
}
|
||
// 雛形沒 DB,直接用同一個 pairing token 當 session token 用
|
||
// 或者回一個固定的 vAs_ prefixed token
|
||
c.JSON(200, gin.H{
|
||
"session_token": "vAs_" + strings.Repeat("0", 60) + strings.Repeat("f", 4),
|
||
"expires_at": time.Now().Add(90 * 24 * time.Hour).Format(time.RFC3339),
|
||
})
|
||
}
|
||
```
|
||
|
||
> **備註**:這個端點屬於 visionA-backend 側的**小型變更**(S 級),等 Agent 要實作時再正式新增。TDD 只記錄契約。
|
||
|
||
### 4.4 重連策略
|
||
|
||
- **指數退避**:1s → 2s → 4s → 8s → 16s → 30s(上限)
|
||
- **Max 5 次**後(設定頁可切「手動」)停止自動重試;使用者在狀態頁可點「立即重試」
|
||
- **Session Token 失效**(remote-proxy 回 401)→ `TokenStore.DeleteSession()` → state = notPaired → 前端切配對頁 + Toast「Session 已失效,請重新配對」
|
||
- **連線成功後**重置退避計數
|
||
|
||
### 4.5 心跳 / 掉線判定(對齊 TDD M-5)
|
||
|
||
- yamux `KeepAliveInterval = 10s`
|
||
- 連續 3 次無 pong(30s)→ 判定掉線 → state = reconnecting
|
||
- 重連成功 → state = online
|
||
- 雛形與 Phase 1 一致。
|
||
|
||
---
|
||
|
||
## 5. 內部 HTTP Server 設計
|
||
|
||
### 5.1 綁定
|
||
|
||
- **框架**:Gin(沿用 local-tool/server/internal/api)
|
||
- **位址**:`127.0.0.1:0`(OS 分配 random port;不對外、不需要 firewall rule)
|
||
- **認證**:**無**(綁 127.0.0.1 + tunnel 已是信任邊界;加 auth 反而複雜且無意義)
|
||
- **Router**:直接 import `server/internal/api.NewRouter()`(local-tool 原本的組裝函式)
|
||
|
||
### 5.2 與 local-tool 的關係
|
||
|
||
- **不共用程式碼**(原則 3「server 邏輯一樣」 = **複製**,不 = **共享**)
|
||
- 兩個專案各自持有一份 `server/internal/`,2026-04-22 fork 後獨立演進
|
||
- 未來需要 cherry-pick 時,走 **手動** git 流程(對 diff 人工檢視 → apply)
|
||
|
||
### 5.3 Handler 差異
|
||
|
||
雛形階段 handler **完全一致**;Phase 1 可能出現 agent 專屬差異的地方:
|
||
|
||
| 可能差異點 | 說明 |
|
||
|-----------|------|
|
||
| system info endpoint | 回傳的內容 agent 版可能多幾個欄位(relay URL、session status) |
|
||
| `/api/system/shutdown-notify` | Agent 版可能直接退 app;local-tool 是退 server |
|
||
|
||
這些差異雛形**不處理**,等需求浮現再 fork。
|
||
|
||
### 5.4 與 tunnel 的配對
|
||
|
||
```
|
||
tunnel.Client (inbound yamux stream)
|
||
│
|
||
▼
|
||
handleStream: 讀 HTTP request → 解析目標位址
|
||
│
|
||
▼
|
||
將 req.URL.Host 改成 "127.0.0.1:<internalPort>"
|
||
│
|
||
▼
|
||
http.DefaultTransport.RoundTrip(req) ← 打 Internal HTTP Server
|
||
│
|
||
▼
|
||
resp.Write(stream) ← 回寫 yamux stream
|
||
```
|
||
|
||
與 POC / local-tool 行為完全一致,只是 `c.localAddr` 從 hardcoded 改成「啟動時 httpserver.Controller 告訴 tunnel client 的 port」。
|
||
|
||
---
|
||
|
||
## 6. 三個 UI 頁面對接
|
||
|
||
### 6.1 Wails Bindings(Go → TS)
|
||
|
||
`cmd/visiona-agent/app.go` 暴露給前端的方法:
|
||
|
||
```go
|
||
// App 是 Wails 綁定的主結構,前端透過 window.go.main.App.<Method>() 呼叫
|
||
type App struct {
|
||
ctx context.Context
|
||
config *agentconfig.Config
|
||
tokenStore tokenstore.Store
|
||
tunnel *tunnel.Client
|
||
httpSrv *httpserver.Controller
|
||
autostart autostart.Manager
|
||
connState *connstate.Broadcaster
|
||
logExport *logexport.Exporter
|
||
}
|
||
|
||
// Public methods(Wails 自動產生 TS binding)
|
||
func (a *App) GetStatus(ctx context.Context) (*StatusResponse, error) // 初始載入用
|
||
func (a *App) Pair(ctx context.Context, pairingToken string) error // 配對頁送出
|
||
func (a *App) Disconnect(ctx context.Context) error // 狀態頁「斷開」
|
||
func (a *App) Reconnect(ctx context.Context) error // 狀態頁「重新連線」
|
||
func (a *App) RepairFlow(ctx context.Context) error // 狀態頁「重新配對」(清 token)
|
||
func (a *App) GetSettings(ctx context.Context) (*SettingsResponse, error)
|
||
func (a *App) UpdateSettings(ctx context.Context, patch SettingsPatch) error
|
||
func (a *App) TestRelay(ctx context.Context, url string) (*TestResult, error)
|
||
func (a *App) GetAutoStart(ctx context.Context) (bool, error)
|
||
func (a *App) SetAutoStart(ctx context.Context, enabled bool) error
|
||
func (a *App) GetRecentLog(ctx context.Context) ([]LogEntry, error)
|
||
func (a *App) ExportLog(ctx context.Context) (path string, err error) // 觸發 save dialog
|
||
func (a *App) OpenLogFolder(ctx context.Context) error
|
||
func (a *App) CheckForUpdates(ctx context.Context) (*UpdateResult, error) // 雛形 stub 回 up-to-date
|
||
func (a *App) ResetAll(ctx context.Context) error // 危險區域
|
||
func (a *App) GetVersion(ctx context.Context) string // 顯示於「關於」
|
||
```
|
||
|
||
### 6.2 Wails Events(Go → 前端事件)
|
||
|
||
| Event Name | Payload | 何時 emit |
|
||
|-----------|---------|-----------|
|
||
| `connection:status` | `{ state, error?, attemptNo?, relayUrl, account?, connectedSince?, sessionTokenPreview? }` | tunnel 狀態變化 |
|
||
| `connection:log` | `{ ts, icon, text }` | 新的連線事件(啟動/重連/錯誤) |
|
||
| `settings:updated` | `{}` | 設定有變,前端可重新拉 |
|
||
| `pairing:result` | `{ success: bool, error?: string }` | Pair() 結束 |
|
||
|
||
前端用 `EventsOn(name, handler)` 訂閱(Wails runtime 內建)。
|
||
|
||
### 6.3 三頁面的對接重點
|
||
|
||
| 頁面 | 主要 bindings / events | Design spec 對應章節 |
|
||
|------|----------------------|---------------------|
|
||
| **狀態頁** | `GetStatus` 初始拉取 + 訂閱 `connection:status` / `connection:log`;按鈕 → `Disconnect` / `Reconnect` / `RepairFlow` | Design spec §4 |
|
||
| **配對頁** | `Pair(token)` → 結束觸發 `pairing:result`;成功 0.5s 後前端自動切狀態 tab | Design spec §5 |
|
||
| **設定頁** | `GetSettings` / `UpdateSettings`;`TestRelay`;`GetAutoStart` / `SetAutoStart`;`ExportLog`;`OpenLogFolder`;`CheckForUpdates` | Design spec §6 |
|
||
|
||
### 6.4 狀態機 (connstate)
|
||
|
||
```go
|
||
type State string
|
||
const (
|
||
StateNotPaired State = "notPaired"
|
||
StateConnecting State = "connecting" // = 配對中 / 首次連線中
|
||
StateOnline State = "online"
|
||
StateReconnecting State = "reconnecting"
|
||
StateOffline State = "offline" // 手動斷開 或 Max 重試後
|
||
StateError State = "error"
|
||
)
|
||
|
||
type Snapshot struct {
|
||
State State `json:"state"`
|
||
Error string `json:"error,omitempty"`
|
||
AttemptNo int `json:"attemptNo,omitempty"`
|
||
RelayURL string `json:"relayUrl"`
|
||
Account string `json:"account,omitempty"` // 雛形填 demo-user@innovedus.com
|
||
ConnectedSince *time.Time `json:"connectedSince,omitempty"`
|
||
SessionTokenPreview string `json:"sessionTokenPreview,omitempty"` // "vAs_a1b2c3d4 ··· e7f8"
|
||
}
|
||
```
|
||
|
||
Broadcaster 使用 `sync.Mutex` 保護 `current *Snapshot`,每次狀態變更同時:
|
||
1. 更新 snapshot
|
||
2. `runtime.EventsEmit(ctx, "connection:status", snapshot)` 推前端
|
||
3. `logBuffer.Append(...)` 記連線事件(最多 100 筆,前端只取 10 筆)
|
||
|
||
---
|
||
|
||
## 7. Build / Package 策略
|
||
|
||
### 7.1 Makefile 改造要點
|
||
|
||
**沿用 local-tool 結構**(`vendor-sync` → `build-frontend` → `build-server` → `payload-*` → `wails-*` → `dmg/exe/appimage`),但:
|
||
|
||
| 項目 | local-tool | visionA Agent |
|
||
|------|-----------|---------------|
|
||
| `VERSION` / app name | `visiona-local` / `visionA Local` | `visiona-agent` / `visionA Agent` |
|
||
| `wails.json` 位置 | `visiona-local/` | `visiona-agent/` |
|
||
| `build-frontend` 命令 | `next build`(產 `out/`)| **相同**:`next build` 產 `frontend/out/` |
|
||
| `payload` 內容 | Python / wheels / ffmpeg / server binary | **相同** |
|
||
| `dmg / exe / appimage` 目標檔名 | `visiona-local-*` | `visiona-agent-*` |
|
||
| Bundle ID (macOS) | `com.innovedus.visiona-local` | **`com.innovedus.visiona-agent`** |
|
||
| NSIS App name (Windows) | `visionA Local` | **`visionA Agent`** |
|
||
| `.desktop` StartupWMClass (Linux) | `visiona-local` | **`visiona-agent`** |
|
||
|
||
**保留 `vendor-sync`(Python / wheels / ffmpeg)**:原則 3「server 邏輯一樣」暗示 Kneron 推論功能必須能跑,所以三者全部要 bundle。這是最大的 installer 體積來源(macOS ~200MB、Windows ~250MB、Linux ~220MB),但無法省。
|
||
|
||
### 7.2 wails.json(差異)
|
||
|
||
```json
|
||
{
|
||
"$schema": "https://wails.io/schemas/config.v2.json",
|
||
"name": "visiona-agent",
|
||
"outputfilename": "visiona-agent",
|
||
"frontend:install": "npm --prefix ./frontend ci",
|
||
"frontend:build": "npm --prefix ./frontend run build",
|
||
"frontend:dev:watcher": "npm --prefix ./frontend run dev",
|
||
"frontend:dev:serverUrl": "http://localhost:3000",
|
||
"assetdir": "./frontend/out",
|
||
"author": {
|
||
"name": "Innovedus",
|
||
"email": "support@innovedus.com"
|
||
},
|
||
"info": {
|
||
"companyName": "Innovedus",
|
||
"productName": "visionA Agent",
|
||
"productVersion": "0.1.0",
|
||
"copyright": "Copyright 2026 Innovedus",
|
||
"comments": "Local agent for visionA cloud — connects your Kneron devices to the cloud"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 7.3 三平台打包
|
||
|
||
| 平台 | 工具 | 同 local-tool | 差異點 |
|
||
|------|------|--------------|--------|
|
||
| macOS | `create-dmg`(local-tool 現用)| ✅ | 換 icon、DMG 背景圖、volume name、bundle ID |
|
||
| Windows | Inno Setup(`.iss`)| ✅ | 換 AppId、AppName、DefaultDirName |
|
||
| Linux | appimagetool | ✅ | 換 `.desktop` Name / Icon / StartupWMClass |
|
||
|
||
### 7.4 簽章策略(同 local-tool 現況)
|
||
|
||
| 平台 | 雛形 | Phase 1 |
|
||
|------|------|---------|
|
||
| macOS | **Ad-hoc codesign**(`codesign -s -`)+ 使用者首次執行 Gatekeeper 彈窗 | Apple Developer ID + notarize |
|
||
| Windows | **不簽**(SmartScreen 首次警告)| Code signing cert(EV / OV) |
|
||
| Linux | **不簽**(AppImage 本來就不簽)| — |
|
||
|
||
雛形就**不處理簽章成本**,跟 local-tool 一致。
|
||
|
||
### 7.5 版本與更新
|
||
|
||
- `info.productVersion`(`wails.json`)+ `VERSION` Makefile 變數雙寫(建置腳本自動同步)
|
||
- 「檢查更新」按鈕 Phase 0:stub 永遠回 `{ status: "up-to-date" }`(即使 disable 按鈕也行,見 Design spec §11.3)
|
||
- Phase 1:參考 POC `internal/update/`(Gitea release API)
|
||
|
||
---
|
||
|
||
## 8. 與 visionA-backend 的整合驗證
|
||
|
||
### 8.1 端對端測試路徑
|
||
|
||
```
|
||
[Browser]
|
||
↓ HTTPS
|
||
[visionA-frontend]
|
||
↓ /api/devices
|
||
[api-server :3001]
|
||
↓ internal HTTP POST /internal/forward/http?token=vAs_...
|
||
[remote-proxy :3801]
|
||
↓ yamux.Open(session[token]).Write(HTTP req bytes)
|
||
[WS wss://.../tunnel/connect?token=vAs_...] ← tunnel 內部 yamux stream
|
||
↓
|
||
[visionA Agent 的 tunnel.Client.handleStream]
|
||
↓ http.DefaultTransport.RoundTrip(req with Host=127.0.0.1:<port>)
|
||
[visionA Agent 的 Internal HTTP Server (Gin)]
|
||
↓ /api/devices handler
|
||
[server/internal/device/manager] 操作 Kneron USB
|
||
↑ response 順著原路回
|
||
```
|
||
|
||
### 8.2 雛形整合測試腳本
|
||
|
||
在專案建立後,測試步驟:
|
||
|
||
1. 啟動 visionA-backend:`cd visionA-backend && make dev`(api-server + remote-proxy)
|
||
2. 設 env:`export VISIONA_PAIRING_TOKEN="vAc_$(openssl rand -hex 16)"`
|
||
3. 啟動 visionA Agent:`cd local-agent && make dev` (啟動 Wails app)
|
||
4. 在 Agent UI 配對頁貼 `$VISIONA_PAIRING_TOKEN`(**需要新增 `/api/pairing/exchange` 端點**)
|
||
5. Agent 取得 Session Token → 自動開 tunnel
|
||
6. 啟動 visionA-frontend:`cd visionA-frontend && npm run dev`
|
||
7. 登入(demo-user),到 `/devices` → 看到 Agent 上報的 Kneron 裝置
|
||
8. 點 `Connect` → 能透過 tunnel 操作 KL520 / KL720
|
||
|
||
### 8.3 這個整合 fork 了什麼責任
|
||
|
||
| 責任 | 誰做 |
|
||
|------|------|
|
||
| `/api/pairing/exchange` 端點實作 | **visionA-backend(小型變更 S 級)** |
|
||
| Tunnel client(出站 WSS + yamux)| visionA Agent |
|
||
| Tunnel server(接入 WSS + session hub)| visionA-backend `internal/relay` |
|
||
| Session Token 存取 | visionA Agent 本地 + visionA-backend DB(雛形:無 DB, env 比對) |
|
||
|
||
---
|
||
|
||
## 9. Token 儲存策略(摘要;詳見 ADR-009)
|
||
|
||
| 平台 | 主要儲存 | Fallback |
|
||
|------|---------|---------|
|
||
| macOS | Keychain(`github.com/keybase/go-keychain` 或 `github.com/99designs/keyring`) | AES-GCM encrypted file |
|
||
| Windows | Credential Manager | AES-GCM encrypted file |
|
||
| Linux | Secret Service / libsecret | AES-GCM encrypted file |
|
||
|
||
**雛形策略**:如果 keyring 三平台整合太複雜(`CGO` 麻煩),**直接先用 encrypted file**,TODO 標註 Phase 1 切 keychain。passphrase 從 OS machine ID 衍生(不讓使用者輸入)。
|
||
|
||
**存放位置**:
|
||
|
||
| 平台 | 路徑 |
|
||
|------|------|
|
||
| macOS | `~/Library/Application Support/visionA Agent/tokens.enc` |
|
||
| Windows | `%APPDATA%\visionA Agent\tokens.enc` |
|
||
| Linux | `~/.config/visionA-agent/tokens.enc` |
|
||
|
||
**存什麼**:
|
||
|
||
```json
|
||
{
|
||
"session_token": "vAs_...",
|
||
"pairing_token": "vAc_...", // 只在還沒換到 session 時才有
|
||
"account_email": "demo-user@innovedus.com",
|
||
"last_paired_at": "2026-04-22T14:30:00Z"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 10. Agent Config 檔(`agentconfig/`)
|
||
|
||
**存放位置**(跟 token 同目錄):`${dataDir}/config.yaml`
|
||
|
||
```yaml
|
||
# visionA Agent config — 使用者可讀,但主要由 UI 改
|
||
version: 1
|
||
relay:
|
||
url: wss://relay.visionA.cloud
|
||
reconnect: auto # auto | manual
|
||
behavior:
|
||
autostart: false
|
||
log:
|
||
level: info # debug | info | warn | error
|
||
```
|
||
|
||
Agent 啟動時讀;UI 設定頁 `UpdateSettings` 寫回;每次變更立即 persist。
|
||
|
||
---
|
||
|
||
## 11. 跨平台實作要點
|
||
|
||
### 11.1 Autostart
|
||
|
||
```go
|
||
// internal/autostart/autostart.go
|
||
type Manager interface {
|
||
IsEnabled() (bool, error)
|
||
Enable() error
|
||
Disable() error
|
||
}
|
||
|
||
// 各平台實作:
|
||
// darwin: ~/Library/LaunchAgents/com.innovedus.visiona-agent.plist
|
||
// windows: HKCU\Software\Microsoft\Windows\CurrentVersion\Run (value="visionA Agent")
|
||
// linux: ~/.config/autostart/visiona-agent.desktop
|
||
```
|
||
|
||
三平台都是 **純檔案 / Registry 操作**,不需要 root / UAC。
|
||
|
||
### 11.2 Log 路徑
|
||
|
||
| 平台 | 路徑 |
|
||
|------|------|
|
||
| macOS | `~/Library/Application Support/visionA Agent/logs/` |
|
||
| Windows | `%APPDATA%\visionA Agent\logs\` |
|
||
| Linux | `~/.config/visionA-agent/logs/`(XDG) |
|
||
|
||
Log 檔名:`visiona-agent-YYYYMMDD.log`(每天 rotate)。
|
||
|
||
### 11.3 「開啟資料夾」/「開啟 URL」
|
||
|
||
用 Wails runtime 內建:
|
||
|
||
```go
|
||
wailsRuntime.BrowserOpenURL(ctx, "https://visionA.cloud/devices/pair")
|
||
// 開檔案總管:darwin 用 `open`, windows 用 `explorer`, linux 用 `xdg-open`
|
||
```
|
||
|
||
### 11.4 Save File Dialog(匯出 Log)
|
||
|
||
```go
|
||
path, err := wailsRuntime.SaveFileDialog(ctx, wailsRuntime.SaveDialogOptions{
|
||
Title: "匯出 visionA Agent Log",
|
||
DefaultFilename: fmt.Sprintf("visiona-agent-log-%s.zip", time.Now().Format("20060102-150405")),
|
||
Filters: []wailsRuntime.FileFilter{{DisplayName: "Zip", Pattern: "*.zip"}},
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
## 12. 測試策略(雛形)
|
||
|
||
| 類型 | 範圍 | 工具 |
|
||
|------|------|------|
|
||
| Unit | `internal/tunnel` / `internal/tokenstore` / `internal/connstate` / `internal/autostart` | `go test` |
|
||
| Integration | App 起 → token 存 → tunnel 假 server 接受 → 狀態 online | 自寫 fake remote-proxy + `httptest` |
|
||
| E2E | 跑完整雲端 + agent,從 web UI 操作裝置 | 手動(整合測試 §8.2) |
|
||
| Frontend | Radix UI 元件 + 狀態/配對/設定頁互動 | Vitest + RTL(沿用 visionA-frontend 的測試慣例 + Next.js Jest preset 的等效 Vitest 設定) |
|
||
|
||
**雛形 MVP 覆蓋目標**:
|
||
- tunnel reconnect / backoff:至少 happy path + 3 次失敗 retry path
|
||
- pairing exchange 正確 / 失敗(4 種 error code)
|
||
- tokenstore encrypted file 讀寫
|
||
- connstate 狀態轉移(5 狀態 × 常見事件)
|
||
|
||
---
|
||
|
||
## 13. TODO(Phase 1 或之後再做)
|
||
|
||
### 功能層
|
||
- [ ] System Tray / Menu Bar 圖示(跨 Wails / Systray library,待 UX 覆盤)
|
||
- [ ] 自動更新機制(參考 POC `internal/update/`,對接 Gitea / GitHub release)
|
||
- [ ] Metrics export(`expvar` / Prometheus ?)
|
||
- [ ] 匯入 / 匯出 config 檔
|
||
- [ ] 國際化更多語言(只繁中 + English 就先)
|
||
|
||
### 安全 / 儲存
|
||
- [ ] Keychain / Credential Manager / Secret Service 真正接上(雛形用 encrypted file)
|
||
- [ ] Session Token 過期 30 天前主動刷新(Phase 1 + backend 支援)
|
||
- [ ] Pairing Token 首次使用後明確從本地清除(雛形為了簡化沒清)
|
||
|
||
### 建置 / 交付
|
||
- [ ] macOS notarize + hardened runtime
|
||
- [ ] Windows code signing
|
||
- [ ] Linux .deb / .rpm(目前只 AppImage)
|
||
- [ ] 自動化 release pipeline(GitHub Actions matrix)
|
||
- [ ] Silent install / MSI / .pkg 變體(企業佈署)
|
||
|
||
### 觀測
|
||
- [ ] 本地 metrics dashboard(tunnel uptime, reconnect count)
|
||
- [ ] Crash reporter(Sentry / Rollbar)
|
||
- [ ] 匿名 usage telemetry(opt-in)
|
||
|
||
### 與雲端互動
|
||
- [ ] visionA-backend 新增 `/api/pairing/exchange`(雛形 backend S 級小改)
|
||
- [ ] Agent 提供自己的身分 header(Agent-Version / Agent-OS / Agent-Serial)
|
||
- [ ] 雲端 web 顯示「哪些裝置來自哪個 agent」
|
||
|
||
### 與 local-tool 關係
|
||
- [ ] cherry-pick 策略文件(怎麼從 local-tool 搬修復到 agent)
|
||
- [ ] Agent 與 local-tool 同一台電腦共存的 port / config 衝突檢查
|
||
|
||
---
|
||
|
||
## 14. ADR 一覽(新增)
|
||
|
||
- **ADR-007** visionA Agent 架構(為何 fork local-tool 而不是改造 local-tool)
|
||
- **ADR-008** Tunnel client 複用策略(程式碼複製 vs submodule vs library)
|
||
- **ADR-009** Token 儲存策略(OS Keychain vs Encrypted file)
|
||
|
||
詳見 `.autoflow/04-architecture/adr/adr-007-*.md` ~ `adr-009-*.md`。
|
||
|
||
---
|
||
|
||
## 15. 開發任務拆分(給 Backend + Frontend Agent)
|
||
|
||
### 15.1 Agent-Backend(Go 部分)
|
||
|
||
| # | 任務 | 大小 | 依賴 |
|
||
|---|------|------|------|
|
||
| **AB1** | 專案初始化:建 `local-agent/` 目錄 + `go.mod` + Makefile 骨架(複製 local-tool Makefile,改 app name / bundle id)。**子任務**:確認 `visionA-backend/internal/tunnel/` 已不存在(C2 裁決:實際上已於 2026-04-21 刪除,README 也已記錄;本子任務只需 `ls visionA-backend/internal/ \| grep tunnel` 驗證無結果即可。如未來有人意外加回,立即刪除)| M | — |
|
||
| **AB2** | 複製 `server/` 整包(local-tool → local-agent)驗證 `go build ./server/...` 能過 | S | AB1 |
|
||
| **AB3** | 新建 `internal/httpserver/controller.go`:啟動/停止 Gin server 綁 127.0.0.1:0 | S | AB2 |
|
||
| **AB4** | 複製 tunnel client(**從 POC `edge-ai-platform/server/internal/tunnel/`** → `local-agent/internal/tunnel/`,因為 visionA-backend 那份要刪)+ 參數化 localAddr | M | AB3 |
|
||
| **AB5** | 新建 `internal/agentconfig/` config 讀寫(YAML) | S | AB1 |
|
||
| **AB6** | 新建 `internal/tokenstore/`:TokenStore interface + encrypted file 實作 | M | AB1 |
|
||
| **AB7** | 新建 `internal/pairing/exchanger.go`:呼叫雲端 `/api/pairing/exchange` | M | AB6 |
|
||
| **AB8** | 新建 `internal/connstate/`:狀態機 + broadcaster(Wails events) | M | AB4 |
|
||
| **AB9** | 新建 `internal/autostart/` 三平台實作 | M | AB1 |
|
||
| **AB10** | 新建 `internal/logexport/`:壓縮最近 7 天 log 成 zip | S | AB2 |
|
||
| **AB11** | `cmd/visiona-agent/main.go` + `app.go`:Wails 啟動 + 所有 bindings(§6.1) | L | AB3-AB10 |
|
||
| **AB12** | 整合測試:fake remote-proxy + agent 完整配對 + 連線 | L | AB11 |
|
||
| **AB13** | **visionA-backend 端 `/api/pairing/exchange`**(S 級小改,另開任務) | S | — |
|
||
|
||
> ⚠️ AB1 子任務「確認 `visionA-backend/internal/tunnel/` 已刪除」說明:經查證 2026-04-22,該目錄已不存在(`visionA-backend/README.md` §Known Issues 已記錄 2026-04-21 刪除事實),visionA-backend 也沒有任何程式碼 import 過此 package。當初它是 B3 階段為了「預留給未來 local-agent 用」而從 POC 複製進來的,但實際上 local-agent 直接從 POC 複製即可,不需要繞道 visionA-backend。保留這個未使用的 package 會誤導未來的維護者以為 visionA-backend 有 tunnel client 角色(實際上 visionA-backend 只有 `internal/relay/` = tunnel **server** 端,與 tunnel **client** 不同職責)。AB1 子任務僅需驗證 + 必要時防止意外重新加入。詳見 ADR-008 v2 補充段落。
|
||
|
||
### 15.2 Agent-Frontend(前端 3 頁 + 全域)
|
||
|
||
> **C1 裁決後重新估算**:因 frontend 改用 Next.js + 直接複製 visionA-frontend 大量資產(18 個 Radix UI 元件、Design Tokens、theme provider、i18n 結構),任務從原本的 11 個壓縮為 **7 個**。
|
||
|
||
| # | 任務 | 大小 | 依賴 | 備註 |
|
||
|---|------|------|------|------|
|
||
| **AF1** | 專案初始化 + 共用資產複製:建 `frontend/`(Next.js 16 + `output: 'export'` + React 19 + TS5 + Tailwind 4 + Radix UI + Zustand 5);同步從 visionA-frontend 複製 `tsconfig.json` / `tailwind.config.ts` / `postcss.config.mjs` / `app/globals.css` / `components/ui/*`(18 個)/ `components/theme-provider.tsx` / `hooks/use-theme-sync.ts` / `lib/utils.ts` / i18n 結構(loader + locale 偵測,文案重寫成 Agent 用)+ Wails build pipeline 設定(`assetdir: ./frontend/out`)| L | — | 把原 AF1+AF2+AF3+AF9+AF10 合併(這些都是「複製不改」性質的工作,一個任務做完即可) |
|
||
| **AF2** | Layout:Root layout + Header + 3-tab bar + ConnectionBadge + Toaster | M | AF1 | 原 AF4 |
|
||
| **AF3** | Wails bindings TS 型別 & 共用 hooks:`use-connection-status`、`use-recent-log`、`use-settings`、SSR-safe wails-runtime wrapper | M | AB11 | 原 AF5 |
|
||
| **AF4** | 狀態 view:StatusHero + InfoCard + RecentLog + 按鈕互動(Disconnect / Reconnect / RepairFlow) | L | AF2, AF3 | 原 AF6 |
|
||
| **AF5** | 配對 view:PairForm(格式驗證 + 配對送出 + Toast + 0.5s 自動切狀態 tab) | M | AF2, AF3 | 原 AF7 |
|
||
| **AF6** | 設定 view:連線 / 行為 / Log / 關於 / 危險區域 5 區塊 | L | AF2, AF3 | 原 AF8 |
|
||
| **AF7** | E2E:從假 Agent(mock Wails bindings)起 → 走完配對 + 狀態 + 設定全流程 | M | AF4-AF6 | 原 AF11 |
|
||
|
||
**被合併 / 移除的任務說明:**
|
||
|
||
| 原任務 | 處理 | 原因 |
|
||
|--------|------|------|
|
||
| 原 AF2「複製 globals.css + shadcn」 | 併入新 AF1 | 純複製,與專案初始化合併效率高 |
|
||
| 原 AF3「i18n 骨架」 | 併入新 AF1 | i18n loader 結構從 visionA-frontend 直接複製,只剩文案重寫,不值得獨立 |
|
||
| 原 AF9「Dark Mode 跟隨 OS」 | 併入新 AF1 | `theme-provider.tsx` + `use-theme-sync.ts` 直接複製,零工作量 |
|
||
| 原 AF10「Wails build pipeline」 | 併入新 AF1 | 跟 wails.json `frontend:install/build/dev:*` 設定一起做 |
|
||
|
||
### 15.3 並行性建議
|
||
|
||
- AB1 / AF1 可**同時**開始
|
||
- AB2-AB10 / AF2 **可並行**
|
||
- AB11 需要 AB3-AB10 先完成;AF3-AF7 需要 AB11 的 binding 確定(AF3 可以先做 mock binding 平行)
|
||
- AB12 / AF7 是最後的收尾整合
|
||
|
||
### 15.4 時間估算
|
||
|
||
以 1 人 = 1 個任務 / 天(熟手)估計(L 任務算 1.5 天,M 算 1 天,S 算 0.5 天):
|
||
|
||
- Backend 全部:約 **13 人日**(AB1-AB13;含刪除 visionA-backend tunnel 子任務,工作量極小不另計)
|
||
- Frontend 全部:約 **8 人日**(AF1-AF7;比原估 11 人日省 3 人日,因大量 visionA-frontend 資產可直接複製)
|
||
- 重疊後實際 wall-clock:**約 2 週**(1 人做 backend / 1 人做 frontend 並行;比原估 2-3 週縮短)
|
||
|
||
---
|
||
|
||
## 16. 使用者裁決紀錄(2026-04-22)
|
||
|
||
### 已決事項(v0.2)
|
||
|
||
| # | 問題 | 裁決 |
|
||
|---|------|------|
|
||
| **C1** | Frontend 框架 | **沿用 Next.js 16 + `output: 'export'`**(對齊原則 4「先抄 local-tool」與 visionA-frontend stack 一致性)|
|
||
| **C2** | Tunnel client 整合 | local-agent 從 **POC** 直接複製一份 + **刪除 `visionA-backend/internal/tunnel/`**(從未被任何 visionA-backend 程式碼 import,B3 預留是誤導;visionA-backend 只需要 `internal/relay/` = tunnel server 端)|
|
||
| **U-1** | `cmd/visiona-agent/` 子目錄 | **是**(更乾淨,Go 慣例)|
|
||
| **U-2** | Token 雛形儲存 | **encrypted file**(machineID 衍生 passphrase),Phase 1 換 OS keychain |
|
||
| **U-3** | `/api/pairing/exchange` 回傳 | **crypto/rand 產 `vAs_` + 64 hex 後 in-memory map 存**(對應正式行為)|
|
||
| **D1** | Wails 視窗大小 | 720×560,記住上次 |
|
||
| **D2** | 配對成功轉場 | 0.5 秒後自動切狀態頁 + toast |
|
||
| **D3** | 「檢查更新」按鈕 | Phase 0 disable + 顯示「Phase 1 才支援」 |
|
||
|
||
---
|
||
|
||
## 版本記錄
|
||
|
||
| 日期 | 版本 | 變更 |
|
||
|------|------|------|
|
||
| 2026-04-22 | 0.1 | Architect Agent 初稿(局部 TDD,單檔)|
|
||
| 2026-04-22 | **0.2** | 對齊使用者裁決 C1 / C2:frontend 改 Next.js + `output: 'export'`(取代原 Vite + SPA 方案);ADR-008 補刪除 visionA-backend tunnel package;§15 frontend 任務從 11 個壓縮為 7 個(複製 visionA-frontend 資產省 3 人日);§3 frontend 結構改 Next.js App Router;§7 wails.json `frontend:*` 改用 npm 啟動 next 命令、`assetdir` 改 `./frontend/out`;AB1 加刪除子任務;AB4 來源從 visionA-backend 改為 POC |
|