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

52 KiB
Raw Permalink Blame History

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.mdadr/adr-008-tunnel-client-reuse.mdadr/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 clientreverse 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 打包產物 多頁完整 UIoutput: '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.goserver/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.tsxhooks/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: truelocal-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 時序OnBeforeClosetunnelClient.Stop()grace 5shttpServer.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 實作:

// 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 回 401TokenStore.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:0OS 分配 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 暴露給前端的方法:

// 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 / UpdateSettingsTestRelayGetAutoStart / SetAutoStartExportLogOpenLogFolderCheckForUpdates Design spec §6

6.4 狀態機 (connstate)

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-syncbuild-frontendbuild-serverpayload-*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 buildfrontend/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-syncPython / wheels / ffmpeg:原則 3「server 邏輯一樣」暗示 Kneron 推論功能必須能跑,所以三者全部要 bundle。這是最大的 installer 體積來源macOS ~200MB、Windows ~250MB、Linux ~220MB但無法省。

7.2 wails.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-dmglocal-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 codesigncodesign -s -+ 使用者首次執行 Gatekeeper 彈窗 Apple Developer ID + notarize
Windows 不簽SmartScreen 首次警告) Code signing certEV / OV
Linux 不簽AppImage 本來就不簽)

雛形就不處理簽章成本,跟 local-tool 一致。

7.5 版本與更新

  • info.productVersionwails.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-backendcd visionA-backend && make devapi-server + remote-proxy
  2. 設 envexport VISIONA_PAIRING_TOKEN="vAc_$(openssl rand -hex 16)"
  3. 啟動 visionA Agentcd local-agent && make dev (啟動 Wails app
  4. 在 Agent UI 配對頁貼 $VISIONA_PAIRING_TOKEN需要新增 /api/pairing/exchange 端點
  5. Agent 取得 Session Token → 自動開 tunnel
  6. 啟動 visionA-frontendcd 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 Keychaingithub.com/keybase/go-keychaingithub.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 fileTODO 標註 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

存什麼

{
  "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

# 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

// 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 內建:

wailsRuntime.BrowserOpenURL(ctx, "https://visionA.cloud/devices/pair")
// 開檔案總管darwin 用 `open`, windows 用 `explorer`, linux 用 `xdg-open`

11.4 Save File Dialog匯出 Log

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 exportexpvar / 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.goWails 啟動 + 所有 bindings§6.1 L AB3-AB10
AB12 整合測試fake remote-proxy + agent 完整配對 + 連線 L AB11
AB13 visionA-backend 端 /api/pairing/exchangeS 級小改,另開任務) 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 型別 & 共用 hooksuse-connection-statususe-recent-loguse-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 filemachineID 衍生 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/outAB1 加刪除子任務AB4 來源從 visionA-backend 改為 POC