visionA/docs/autoflow/04-architecture/visiona-agent-tdd.md
jim800121chen fb7da5d180 chore(autoflow): migrate .autoflow/ 共享層文件至 docs/autoflow/
依 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)。
2026-05-04 16:55:55 +08:00

977 lines
52 KiB
Markdown
Raw 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.

# visionA Agent — Technical Design Document局部 TDD
## Metadata
| 項目 | 內容 |
|------|------|
| 文件角色 | **局部 TDD** — 只規範 visionA Agent 這一塊,不重寫 visionA-backend / visionA-frontend 的 TDD |
| 作者 | Architect Agent |
| 最後更新 | 2026-04-22v0.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 平台實作) |
| 產品行銷 UIOnboarding / 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 # 雛形 fallbackAES-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 stackC1 裁決)
│ ├── 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 layoutHeader + 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 / EventsEmitSSR-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 個 viewWails 環境 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.jsAgent 是純 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 次無 pong30s→ 判定掉線 → 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 版可能直接退 applocal-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 BindingsGo → 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 methodsWails 自動產生 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 EventsGo → 前端事件)
| 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 certEV / OV |
| Linux | **不簽**AppImage 本來就不簽)| — |
雛形就**不處理簽章成本**,跟 local-tool 一致。
### 7.5 版本與更新
- `info.productVersion``wails.json`+ `VERSION` Makefile 變數雙寫(建置腳本自動同步)
- 「檢查更新」按鈕 Phase 0stub 永遠回 `{ 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. TODOPhase 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 pipelineGitHub Actions matrix
- [ ] Silent install / MSI / .pkg 變體(企業佈署)
### 觀測
- [ ] 本地 metrics dashboardtunnel uptime, reconnect count
- [ ] Crash reporterSentry / Rollbar
- [ ] 匿名 usage telemetryopt-in
### 與雲端互動
- [ ] visionA-backend 新增 `/api/pairing/exchange`(雛形 backend S 級小改)
- [ ] Agent 提供自己的身分 headerAgent-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-BackendGo 部分)
| # | 任務 | 大小 | 依賴 |
|---|------|------|------|
| **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/`:狀態機 + broadcasterWails 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** | LayoutRoot 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** | 狀態 viewStatusHero + InfoCard + RecentLog + 按鈕互動Disconnect / Reconnect / RepairFlow | L | AF2, AF3 | 原 AF6 |
| **AF5** | 配對 viewPairForm格式驗證 + 配對送出 + Toast + 0.5s 自動切狀態 tab | M | AF2, AF3 | 原 AF7 |
| **AF6** | 設定 view連線 / 行為 / Log / 關於 / 危險區域 5 區塊 | L | AF2, AF3 | 原 AF8 |
| **AF7** | E2E從假 Agentmock 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 程式碼 importB3 預留是誤導visionA-backend 只需要 `internal/relay/` = tunnel server 端)|
| **U-1** | `cmd/visiona-agent/` 子目錄 | **是**更乾淨Go 慣例)|
| **U-2** | Token 雛形儲存 | **encrypted file**machineID 衍生 passphrasePhase 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 / C2frontend 改 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 |