# 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: │ │ │ │ (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:`(不對外) | | 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: │ - 指數退避重連(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:" │ ▼ 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.() 呼叫 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:) [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 |