依 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)。
52 KiB
visionA Agent — Technical Design Document(局部 TDD)
Metadata
| 項目 | 內容 |
|---|---|
| 文件角色 | 局部 TDD — 只規範 visionA Agent 這一塊,不重寫 visionA-backend / visionA-frontend 的 TDD |
| 作者 | Architect Agent |
| 最後更新 | 2026-04-22(v0.2 — 對齊使用者裁決 C1/C2) |
| 狀態 | Approved — frontend 改 Next.js(沿用 local-tool / visionA-frontend stack) |
| 上位文件 | .autoflow/04-architecture/design-doc.md、.autoflow/04-architecture/TDD.md(既有)、.autoflow/03-design/visiona-agent-spec.md |
| 下位文件 | adr/adr-007-visiona-agent-architecture.md、adr/adr-008-tunnel-client-reuse.md、adr/adr-009-token-storage.md |
| 讀者 | 要實作 visionA Agent 的 Backend Agent + Frontend Agent |
1. 產品定位回顧
visionA Agent 是 visionA 雲端版的 local agent / tunnel bridge,部署在使用者桌機。
1.1 它是什麼
- 一個完整的 local-tool server(沿用 KneronPLUS / camera / inference / device / model / Python runtime / ffmpeg 全部邏輯)
- 加上 tunnel client(reverse tunnel 到雲端 remote-proxy)
- 加上 3 頁極簡配置 UI(狀態 / 配對 / 設定)
- Wails v2 桌面應用(macOS DMG / Windows EXE / Linux AppImage)
1.2 它不是什麼
- 不是「純 tunnel bridge」(它必須跑完整 server 邏輯,因為要操作 Kneron 裝置)
- 不是 local-tool 的修改版(local-tool 不動;visionA Agent 是 fork 後獨立演進)
- 不是 headless CLI(本次雛形是 Wails 桌面應用)
- 不保留 local-tool 原本的裝置 / 模型 / 推論操作 UI(這些改由雲端 Web UI 負責)
1.3 與 local-tool 的職責差異(視覺化)
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ local-tool │ │ visionA Agent │
│ │ │ │
│ [前端 Next.js 完整 UI] │ │ [前端 Next.js 3 頁配置 UI] │
│ 裝置頁 / 模型頁 / 推論頁 │ │ 狀態 / 配對 / 設定 │
│ 工作區 / 儀表板 │ │ (output: 'export' static) │
│ ↓ HTTP │ │ ↓ Wails runtime │
│ [Gin server :3721] │ │ [Wails app.go] │
│ 所有業務 handler │ │ Pair / Disconnect /... │
│ │ │ ↓ │
│ │ │ [Tunnel client] │
│ │ │ WSS to remote-proxy │
│ │ │ ↓ 轉發進來的 HTTP │
│ │ │ [Gin server 127.0.0.1:RND] │
│ │ │ 同 local-tool 的 handler │
│ ↓ │ │ ↓ │
│ [Kneron / Camera / Python] │ │ [Kneron / Camera / Python] │
│ └─ 完全一樣 ────────────────────────────────────────┘ │
└─────────────────────────────┘ └─────────────────────────────┘
結論:server 邏輯 100% 一樣,差別在於「前端 UI 的職責」與「多了一個 tunnel client」。
2. 整體架構
2.1 元件圖:雲端資料流中的 visionA Agent
┌──────────┐ HTTPS ┌─────────────┐ internal HTTP ┌──────────────┐
│ Browser │ ───────────────►│ api-server │────────────────►│ remote-proxy │
│ (cloud │ │ (stateless) │◄────────────────│ (stateful, │
│ web UI) │ └─────────────┘ │ yamux hub) │
└──────────┘ └──────┬───────┘
│
WSS + yamux (出站長連線)
│
▼
┌────────────────────────────────────────┐
│ visionA Agent(使用者桌機) │
│ │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ Tunnel │ │ 前端 SPA │ │
│ │ Client │ │ 3 頁配置 UI │ │
│ │ (yamux) │ │ (Wails WebView) │
│ └──────┬──────┘ └──────┬───────┘ │
│ │ 每個 stream │ Wails bind│
│ ▼ ▼ │
│ ┌──────────────────────────────────┐ │
│ │ Internal HTTP Server │ │
│ │ 127.0.0.1:<random port> │ │
│ │ (Gin, 不對外;沿用 local-tool API)│ │
│ └─────────┬────────────────────────┘ │
│ │ │
│ ┌─────────▼────────────────────────┐ │
│ │ Reused server packages │ │
│ │ device / model / inference / │ │
│ │ camera / flash / deps │ │
│ └─────────┬────────────────────────┘ │
│ ▼ │
│ ┌────────────────────────────────┐ │
│ │ Python runtime + KneronPLUS │ │
│ │ (bundled) + ffmpeg │ │
│ └───────────┬────────────────────┘ │
└──────────────┼─────────────────────────┘
▼
Kneron USB 裝置
2.2 與 local-tool 執行時期比較
| 元件 | local-tool | visionA Agent |
|---|---|---|
| 前端 Next.js 打包產物 | 多頁完整 UI(output: 'export' static export) |
取代為 3 頁極簡 UI(同樣 Next.js + output: 'export') |
| Wails app shell | 有(visiona-local/) |
有(visiona-agent/),行為改造 |
| Gin HTTP server | 綁 0.0.0.0:3721(對外) |
綁 127.0.0.1:<random>(不對外) |
| Router | 所有 /api/*, /ws/* |
完全相同(沿用 router.go) |
internal/device, internal/model, internal/inference, internal/camera, internal/flash, internal/deps, internal/driver |
✅ | 整包複製,不改 |
| Python runtime / wheels / ffmpeg | bundled | 整包複製,不改 |
| Tunnel client | 無 | 新增(sync 自 POC,見 ADR-008) |
| 配對 / 設定 UI | 無 | 新增(3 頁,見 Design spec) |
| 開機自啟管理 | 無 | 新增(3 平台實作) |
| 產品行銷 UI(Onboarding / ServerDashboard / LogViewer) | 有 | 移除或替換 |
3. 專案結構
路徑: /Users/jimchen/visionA/local-agent/
local-agent/ # 🆕 全新專案(fork from local-tool @ 2026-04-22)
│
├── wails.json # 改造:name / productName / bundle ID
├── Makefile # 改造:build targets 調整(見 §6)
├── go.mod # 改造:module visiona-agent(非 visiona-local)
├── go.sum
├── README.md # 🆕 新寫
├── .env.example # 🆕 VISIONA_AGENT_* env
│
├── cmd/ # 🆕 新增(local-tool 沒 cmd/,它 main.go 在根)
│ └── visiona-agent/
│ ├── main.go # 🆕 Wails 入口(取代 local-tool/visiona-local/main.go)
│ └── app.go # 🆕 Wails bindings + 生命週期
│
├── server/ # ✅ 整包複製自 local-tool/server/,不改
│ ├── main.go # ⚠️ 不用(改由 cmd/visiona-agent 內嵌啟動)
│ ├── go.mod
│ ├── internal/
│ │ ├── api/ # ✅ 整包複製
│ │ │ ├── router.go
│ │ │ ├── middleware.go
│ │ │ ├── handlers/ (system, model, device, camera...)
│ │ │ └── ws/ (device_events, inference, flash, system_ws, server_logs)
│ │ ├── camera/ # ✅ 整包複製
│ │ ├── config/ # ✅ 整包複製
│ │ ├── deps/ # ✅ 整包複製
│ │ ├── device/ # ✅ 整包複製
│ │ ├── driver/ # ✅ 整包複製
│ │ ├── flash/ # ✅ 整包複製
│ │ ├── inference/ # ✅ 整包複製
│ │ └── model/ # ✅ 整包複製
│ ├── pkg/
│ │ ├── logger/ # ✅ 整包複製
│ │ └── wsconn/ # ✅ 整包複製(tunnel client 會用到)
│ └── web/ # ⚠️ 不用;Wails 自己 embed 前端
│
├── internal/ # 🆕 Agent 專屬邏輯(非 server 邏輯)
│ ├── tunnel/ # 🆕 Tunnel client(複製自 POC,見 ADR-008)
│ │ ├── client.go # 從 edge-ai-platform/server/internal/tunnel/client.go
│ │ ├── backoff.go # 退避演算法(10s 心跳 / 30s 判定,對齊 TDD M-5)
│ │ └── client_test.go
│ ├── pairing/ # 🆕 配對邏輯
│ │ ├── exchanger.go # 呼叫雲端 exchange endpoint 換 Session Token
│ │ ├── validator.go # `^vAc_[0-9a-f]{32}$` 格式驗證
│ │ └── exchanger_test.go
│ ├── tokenstore/ # 🆕 Pairing / Session Token 持久化(見 ADR-009)
│ │ ├── store.go # TokenStore interface
│ │ ├── keychain_darwin.go # macOS Keychain
│ │ ├── keychain_windows.go # Windows Credential Manager
│ │ ├── keychain_linux.go # Secret Service (libsecret) or fallback
│ │ ├── encrypted_file.go # 雛形 fallback:AES-GCM + passphrase from OS
│ │ └── store_test.go
│ ├── agentconfig/ # 🆕 Agent 層級 config(不同於 server/internal/config)
│ │ ├── config.go # Relay URL / autostart / log level / reconnect strategy
│ │ ├── persist.go # 讀寫 YAML(位置見 §4.3)
│ │ └── config_test.go
│ ├── autostart/ # 🆕 開機自啟管理(3 平台)
│ │ ├── autostart.go # AutoStart interface
│ │ ├── autostart_darwin.go # LaunchAgent plist
│ │ ├── autostart_windows.go # Registry Run key
│ │ ├── autostart_linux.go # ~/.config/autostart/*.desktop
│ │ └── autostart_test.go
│ ├── connstate/ # 🆕 連線狀態機 + 事件推送
│ │ ├── state.go # online / offline / reconnecting / notPaired / error
│ │ ├── broadcaster.go # Wails EventsEmit("connection:status")
│ │ └── log_buffer.go # 最近 10 筆連線事件(給 RecentLog)
│ ├── logexport/ # 🆕 匯出 log(打包最近 7 天)
│ │ ├── exporter.go
│ │ └── exporter_test.go
│ └── httpserver/ # 🆕 wrap local-tool server 的啟動 / 停止
│ ├── controller.go # sync.Once + random port + 127.0.0.1 綁定
│ └── controller_test.go
│
├── frontend/ # 🆕 Next.js 16 + React 19 + TS5 + Tailwind 4 + Radix UI + Zustand 5
│ │ # ⭐ 完全對齊 local-tool / visionA-frontend stack(C1 裁決)
│ ├── package.json # 同 visionA-frontend 的 dependencies(複製後刪掉用不到的)
│ ├── next.config.mjs # `output: 'export'` + `images.unoptimized: true`
│ ├── tsconfig.json # ✅ 從 visionA-frontend 複製
│ ├── tailwind.config.ts # ✅ 從 visionA-frontend 複製(Design Tokens 一致)
│ ├── postcss.config.mjs # ✅ 從 visionA-frontend 複製
│ ├── components.json # ✅ 從 visionA-frontend 複製(shadcn 產生器設定,雛形不會再跑 CLI 但保留以利未來新增元件)
│ └── src/
│ ├── app/ # Next.js App Router
│ │ ├── layout.tsx # Root layout:Header + ThemeProvider + Toaster
│ │ ├── page.tsx # 3 tabs state machine(單頁切換,不走多 route)
│ │ ├── globals.css # ✅ 從 visionA-frontend 複製,不改一行
│ │ └── not-found.tsx # Wails 環境理論上不會走到,保留以避免 export 報錯
│ ├── components/
│ │ ├── layout/
│ │ │ ├── Header.tsx # h-14 + 3 tabs + ConnectionBadge
│ │ │ └── TabBar.tsx
│ │ ├── ui/ # ✅ 從 visionA-frontend `src/components/ui/` 複製(18 個 Radix UI primitives 全部)
│ │ │ ├── button.tsx # 雛形實際只用約 10 個,但全複製以利未來擴充
│ │ │ ├── card.tsx # (複製成本 = 0,有用就好)
│ │ │ ├── dialog.tsx
│ │ │ ├── input.tsx
│ │ │ ├── label.tsx
│ │ │ ├── tabs.tsx
│ │ │ ├── checkbox.tsx
│ │ │ ├── radio-group.tsx
│ │ │ ├── select.tsx
│ │ │ ├── alert-dialog.tsx
│ │ │ ├── alert.tsx
│ │ │ ├── sonner.tsx
│ │ │ ├── tooltip.tsx
│ │ │ ├── badge.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── switch.tsx
│ │ │ ├── popover.tsx
│ │ │ └── dropdown-menu.tsx
│ │ ├── theme-provider.tsx # ✅ 從 visionA-frontend 複製(next-themes wrapper)
│ │ ├── status/
│ │ │ ├── StatusHero.tsx
│ │ │ ├── InfoCard.tsx
│ │ │ └── RecentLog.tsx
│ │ ├── pair/
│ │ │ └── PairForm.tsx
│ │ └── settings/
│ │ ├── SettingsConnection.tsx
│ │ ├── SettingsBehavior.tsx
│ │ ├── SettingsLog.tsx
│ │ ├── SettingsAbout.tsx
│ │ └── SettingsDanger.tsx
│ ├── views/ # 3 個 tab 對應的 view 元件(不是 Next.js page,因為走單頁 tabs)
│ │ ├── StatusView.tsx
│ │ ├── PairView.tsx
│ │ └── SettingsView.tsx
│ ├── hooks/
│ │ ├── use-connection-status.ts # Wails EventsOn + 初始拉取
│ │ ├── use-recent-log.ts
│ │ └── use-theme-sync.ts # ✅ 從 visionA-frontend 複製
│ ├── lib/
│ │ ├── bindings.ts # re-export Wails 自動產生的 TS binding(位於 wailsjs/go/main/App.d.ts)
│ │ ├── wails-runtime.ts # wrap @wailsapp/runtime EventsOn / EventsEmit;SSR-safe(雖然 export 不會 SSR,加 `typeof window !== 'undefined'` guard)
│ │ ├── i18n/
│ │ │ ├── index.ts # ✅ 結構從 visionA-frontend 複製(i18n loader / OS locale 偵測)
│ │ │ ├── zh-TW.ts # 見 Design spec §10,~135 keys(文案重寫)
│ │ │ └── en.ts # 同上
│ │ ├── utils.ts # ✅ 從 visionA-frontend 複製(cn / clsx helper)
│ │ └── format.ts # 遮蔽 session token / 時間格式
│ └── stores/
│ ├── connection-store.ts # zustand 5, 連線狀態
│ └── settings-store.ts # zustand 5, UI 層設定
│
├── visiona-agent/ # 🆕 Wails build dir(類似 local-tool 的 visiona-local/)
│ ├── wails.json # 見 §6.2
│ ├── payload/ # 執行時需要的 Python / wheels / ffmpeg
│ └── icon/ # 三平台 icon
│
├── vendor/ # Python runtime / wheels / ffmpeg(同 local-tool 結構)
│ ├── python/
│ ├── wheels/
│ └── ffmpeg/
│
├── scripts/ # 三平台 bootstrap / installer 腳本
│ ├── bootstrap-macos.sh
│ ├── bootstrap-windows.ps1
│ └── bootstrap-linux.sh
│
└── build/ # Installer 打包輸入 + 中間產物
├── macos/ (dmgbuild / create-dmg 設定)
├── windows/ (Inno Setup .iss)
└── linux/ (AppImage recipe)
3.1 標記說明
- ✅ 整包複製:從 local-tool(或 visionA-frontend)原樣拿,不改一行
- 🆕 新增:visionA Agent 專屬
- ⚠️ 不用 / 取代:原本的檔案不會被載入(例如
server/main.go、server/web/embed.go)
3.2 Frontend 框架決策(C1:沿用 Next.js)
結論:visionA Agent 前端用 Next.js 16 + output: 'export' 產出 static HTML/JS/CSS,由 Wails go:embed 嵌入。
這個決策對齊使用者的 4 大核心原則第 4 條「雲端 web UI 先抄 local-tool」與整個 visionA 產品線的 stack 一致性:
| 專案 | Frontend 框架 | 部署形態 |
|---|---|---|
| local-tool | Next.js 16 + output: 'export' |
Wails 嵌入 |
| visionA-frontend | Next.js 16 | Vercel / 雲端 |
| visionA Agent(本專案) | Next.js 16 + output: 'export' |
Wails 嵌入 |
為什麼放棄 Vite + SPA(修正前一版方案):
- ❌ 違反原則 4「先抄 local-tool」— local-tool 已經在 Wails 用 Next.js static export 跑得好好的,再引入 Vite 是發明問題
- ❌ 兩套 build pipeline / config 形態(Vite vs Next.js)增加團隊維護心智成本
- ❌ visionA-frontend 的元件 / utils / Tailwind config 已經是 Next.js 形態,搬到 Vite 反而要改 import /
'use client'directive 處理
沿用 Next.js 的好處:
- ✅ 元件直接複製:
src/components/ui/*的 18 個 Radix UI primitives、theme-provider.tsx、hooks/use-theme-sync.ts從 visionA-frontend 一行不改搬過來 - ✅ Design Tokens 100% 一致:
globals.css+tailwind.config.ts直接複製 - ✅ i18n 結構沿用:loader + OS locale 偵測邏輯複製,只改文案
- ✅ Wails 友善已驗證:local-tool 已經跑了一年多,
output: 'export'+images.unoptimized: true+ 單頁 tabs(不走 Next.js routing)的模式穩定
Next.js 在 Wails 環境的注意事項:
| 議題 | 處理方式 |
|---|---|
| SSR / SSG | 不用;output: 'export' 純 client side |
| Image Optimization | images.unoptimized: true(local-tool 同樣設定) |
| Routing | 不走 Next.js routing,用 useState 切 3 個 view(Wails 環境 location.href 行為奇怪,避免之) |
'use client' |
所有元件預設加(沒有 server component 概念) |
next/font |
可用;字型會被 export 到 out/_next/static/media/ |
| API Routes | 不用(Wails 不會跑 Next.js server) |
| Hydration | 不會發生(無 SSR) |
Design spec §13.2 修正備註:原本寫「不引入 Next.js(Agent 是純 SPA)」是 v0.1 版的方案,使用者裁決後已對齊為 Next.js。Design Agent 不需要動文件(spec 描述的是 UI 結構與 Design Tokens,與框架選型無關),只需要把 §13.2 的這句話視為 obsolete。
4. Tunnel 整合策略
4.1 Tunnel Client 從哪來
採用 「程式碼複製」 策略(見 ADR-008 完整分析):
- 從
/Users/jimchen/visionA/visionA-backend/internal/tunnel/client.go複製到local-agent/internal/tunnel/client.go - 不用 git submodule(跨 repo 管理複雜、CI 綁定多)
- 不用 go module replace(兩個專案 module 不同,replace 語義勉強)
- 不用 共用 library(還沒有「共用 library repo」,POC 也是每個專案各自複製)
對應 progress.md 原則 1「local-tool 不要動,可以複製出來」的精神。
4.2 啟動時序
Wails app.go: OnStartup(ctx)
│
├─ 1. 載入 Agent config (relay URL, reconnect strategy, log level)
│
├─ 2. 啟動 Internal HTTP Server (Gin)
│ - bind 127.0.0.1:0 (OS 分配 random port)
│ - 掛上 local-tool server/internal/api/router.go 的所有 handler
│ - 保留 port number 給後續 tunnel 用
│
├─ 3. 從 TokenStore 讀取已保存的 Session / Pairing Token
│ ├─ 有 Session Token → state = connecting → 進入步驟 4
│ ├─ 有 Pairing Token (上次配對未完成) → state = connecting → 進入步驟 4
│ └─ 無 → state = notPaired → 停在配對頁
│
├─ 4. 啟動 Tunnel Client
│ - 以 Session Token (或 fallback Pairing Token) 連 relayURL
│ - Accept 到的 stream → 轉發到 step 2 的 127.0.0.1:<randomPort>
│ - 指數退避重連(1s → 2s → 4s → ... 上限 30s)
│ - yamux KeepAlive 10s / 30s 判定掉線(對齊 §4 心跳規範)
│
└─ 5. 訂閱 tunnel 狀態變化 → 透過 Wails EventsEmit("connection:status") 推前端
Shutdown 時序:OnBeforeClose → tunnelClient.Stop()(grace 5s)→ httpServer.Shutdown(ctx) → process exit。
4.3 配對流程(雛形 + Phase 1)
雛形流程(當前要實作的)
使用者在 Wails UI 貼上 Pairing Token (vAc_...)
│
▼
agent 呼叫雲端: POST {relayHttpURL}/api/pairing/exchange
Body: { pairing_token: "vAc_..." }
│
▼
雲端 api-server:
- StaticPairingStore.Validate(token) → 雛形 env 比對
- 生成 Session Token (vAs_ + 64 hex)
- 回傳 { session_token: "vAs_..." }
│
▼
agent:
- TokenStore.SaveSession(session_token)
- TokenStore.DeletePairing() (雛形為了簡化可略)
- 啟動 tunnel client with session_token
│
▼
tunnel client 連 WS /tunnel/connect?token=vAs_...
雲端 remote-proxy 接受 → tunnel 建立
│
▼
agent 發事件 connection:status = "online"
前端 Toast "配對成功" + 0.5s 後切狀態頁
雛形 vs 正式版 API 行為
| 事項 | 雛形 | 正式版 |
|---|---|---|
/api/pairing/exchange 端點 |
需要新增(雛形 backend 還沒有) | 存在 |
| Pairing Token 來源 | 使用者從環境變數 / 雲端 web UI 取 vAc_...(雛形 web 已有 /devices/pair) |
同左 |
| Session Token 驗證 | 比對 config env | DB 查詢(兩階段完整) |
| Token TTL | 無 | Pairing 15min / Session 90 days |
雛形 backend 新增端點(同步通知 Backend Agent):
POST /api/pairing/exchange
Request body: { pairing_token: "vAc_[0-9a-f]{32}" }
Response body: { session_token: "vAs_[0-9a-f]{64}", expires_at: "2026-..." }
OR 401 { code: "token_invalid" | "token_expired" | "token_used" | "token_revoked" }
雛形 handler 實作:
// visionA-backend/internal/api/handlers/pairing.go(現有檔案新增 method)
func (h *PairingHandler) Exchange(c *gin.Context) {
var req struct{ PairingToken string `json:"pairing_token" binding:"required"` }
if err := c.ShouldBindJSON(&req); err != nil { ... }
// 雛形 StaticPairingStore:比對 env 值
if req.PairingToken != os.Getenv("VISIONA_PAIRING_TOKEN") {
c.JSON(401, gin.H{"code": "token_invalid"})
return
}
// 雛形沒 DB,直接用同一個 pairing token 當 session token 用
// 或者回一個固定的 vAs_ prefixed token
c.JSON(200, gin.H{
"session_token": "vAs_" + strings.Repeat("0", 60) + strings.Repeat("f", 4),
"expires_at": time.Now().Add(90 * 24 * time.Hour).Format(time.RFC3339),
})
}
備註:這個端點屬於 visionA-backend 側的小型變更(S 級),等 Agent 要實作時再正式新增。TDD 只記錄契約。
4.4 重連策略
- 指數退避:1s → 2s → 4s → 8s → 16s → 30s(上限)
- Max 5 次後(設定頁可切「手動」)停止自動重試;使用者在狀態頁可點「立即重試」
- Session Token 失效(remote-proxy 回 401)→
TokenStore.DeleteSession()→ state = notPaired → 前端切配對頁 + Toast「Session 已失效,請重新配對」 - 連線成功後重置退避計數
4.5 心跳 / 掉線判定(對齊 TDD M-5)
- yamux
KeepAliveInterval = 10s - 連續 3 次無 pong(30s)→ 判定掉線 → state = reconnecting
- 重連成功 → state = online
- 雛形與 Phase 1 一致。
5. 內部 HTTP Server 設計
5.1 綁定
- 框架:Gin(沿用 local-tool/server/internal/api)
- 位址:
127.0.0.1:0(OS 分配 random port;不對外、不需要 firewall rule) - 認證:無(綁 127.0.0.1 + tunnel 已是信任邊界;加 auth 反而複雜且無意義)
- Router:直接 import
server/internal/api.NewRouter()(local-tool 原本的組裝函式)
5.2 與 local-tool 的關係
- 不共用程式碼(原則 3「server 邏輯一樣」 = 複製,不 = 共享)
- 兩個專案各自持有一份
server/internal/,2026-04-22 fork 後獨立演進 - 未來需要 cherry-pick 時,走 手動 git 流程(對 diff 人工檢視 → apply)
5.3 Handler 差異
雛形階段 handler 完全一致;Phase 1 可能出現 agent 專屬差異的地方:
| 可能差異點 | 說明 |
|---|---|
| system info endpoint | 回傳的內容 agent 版可能多幾個欄位(relay URL、session status) |
/api/system/shutdown-notify |
Agent 版可能直接退 app;local-tool 是退 server |
這些差異雛形不處理,等需求浮現再 fork。
5.4 與 tunnel 的配對
tunnel.Client (inbound yamux stream)
│
▼
handleStream: 讀 HTTP request → 解析目標位址
│
▼
將 req.URL.Host 改成 "127.0.0.1:<internalPort>"
│
▼
http.DefaultTransport.RoundTrip(req) ← 打 Internal HTTP Server
│
▼
resp.Write(stream) ← 回寫 yamux stream
與 POC / local-tool 行為完全一致,只是 c.localAddr 從 hardcoded 改成「啟動時 httpserver.Controller 告訴 tunnel client 的 port」。
6. 三個 UI 頁面對接
6.1 Wails Bindings(Go → TS)
cmd/visiona-agent/app.go 暴露給前端的方法:
// App 是 Wails 綁定的主結構,前端透過 window.go.main.App.<Method>() 呼叫
type App struct {
ctx context.Context
config *agentconfig.Config
tokenStore tokenstore.Store
tunnel *tunnel.Client
httpSrv *httpserver.Controller
autostart autostart.Manager
connState *connstate.Broadcaster
logExport *logexport.Exporter
}
// Public methods(Wails 自動產生 TS binding)
func (a *App) GetStatus(ctx context.Context) (*StatusResponse, error) // 初始載入用
func (a *App) Pair(ctx context.Context, pairingToken string) error // 配對頁送出
func (a *App) Disconnect(ctx context.Context) error // 狀態頁「斷開」
func (a *App) Reconnect(ctx context.Context) error // 狀態頁「重新連線」
func (a *App) RepairFlow(ctx context.Context) error // 狀態頁「重新配對」(清 token)
func (a *App) GetSettings(ctx context.Context) (*SettingsResponse, error)
func (a *App) UpdateSettings(ctx context.Context, patch SettingsPatch) error
func (a *App) TestRelay(ctx context.Context, url string) (*TestResult, error)
func (a *App) GetAutoStart(ctx context.Context) (bool, error)
func (a *App) SetAutoStart(ctx context.Context, enabled bool) error
func (a *App) GetRecentLog(ctx context.Context) ([]LogEntry, error)
func (a *App) ExportLog(ctx context.Context) (path string, err error) // 觸發 save dialog
func (a *App) OpenLogFolder(ctx context.Context) error
func (a *App) CheckForUpdates(ctx context.Context) (*UpdateResult, error) // 雛形 stub 回 up-to-date
func (a *App) ResetAll(ctx context.Context) error // 危險區域
func (a *App) GetVersion(ctx context.Context) string // 顯示於「關於」
6.2 Wails Events(Go → 前端事件)
| Event Name | Payload | 何時 emit |
|---|---|---|
connection:status |
{ state, error?, attemptNo?, relayUrl, account?, connectedSince?, sessionTokenPreview? } |
tunnel 狀態變化 |
connection:log |
{ ts, icon, text } |
新的連線事件(啟動/重連/錯誤) |
settings:updated |
{} |
設定有變,前端可重新拉 |
pairing:result |
{ success: bool, error?: string } |
Pair() 結束 |
前端用 EventsOn(name, handler) 訂閱(Wails runtime 內建)。
6.3 三頁面的對接重點
| 頁面 | 主要 bindings / events | Design spec 對應章節 |
|---|---|---|
| 狀態頁 | GetStatus 初始拉取 + 訂閱 connection:status / connection:log;按鈕 → Disconnect / Reconnect / RepairFlow |
Design spec §4 |
| 配對頁 | Pair(token) → 結束觸發 pairing:result;成功 0.5s 後前端自動切狀態 tab |
Design spec §5 |
| 設定頁 | GetSettings / UpdateSettings;TestRelay;GetAutoStart / SetAutoStart;ExportLog;OpenLogFolder;CheckForUpdates |
Design spec §6 |
6.4 狀態機 (connstate)
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,每次狀態變更同時:
- 更新 snapshot
runtime.EventsEmit(ctx, "connection:status", snapshot)推前端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(差異)
{
"$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)+VERSIONMakefile 變數雙寫(建置腳本自動同步)- 「檢查更新」按鈕 Phase 0:stub 永遠回
{ status: "up-to-date" }(即使 disable 按鈕也行,見 Design spec §11.3) - Phase 1:參考 POC
internal/update/(Gitea release API)
8. 與 visionA-backend 的整合驗證
8.1 端對端測試路徑
[Browser]
↓ HTTPS
[visionA-frontend]
↓ /api/devices
[api-server :3001]
↓ internal HTTP POST /internal/forward/http?token=vAs_...
[remote-proxy :3801]
↓ yamux.Open(session[token]).Write(HTTP req bytes)
[WS wss://.../tunnel/connect?token=vAs_...] ← tunnel 內部 yamux stream
↓
[visionA Agent 的 tunnel.Client.handleStream]
↓ http.DefaultTransport.RoundTrip(req with Host=127.0.0.1:<port>)
[visionA Agent 的 Internal HTTP Server (Gin)]
↓ /api/devices handler
[server/internal/device/manager] 操作 Kneron USB
↑ response 順著原路回
8.2 雛形整合測試腳本
在專案建立後,測試步驟:
- 啟動 visionA-backend:
cd visionA-backend && make dev(api-server + remote-proxy) - 設 env:
export VISIONA_PAIRING_TOKEN="vAc_$(openssl rand -hex 16)" - 啟動 visionA Agent:
cd local-agent && make dev(啟動 Wails app) - 在 Agent UI 配對頁貼
$VISIONA_PAIRING_TOKEN(需要新增/api/pairing/exchange端點) - Agent 取得 Session Token → 自動開 tunnel
- 啟動 visionA-frontend:
cd visionA-frontend && npm run dev - 登入(demo-user),到
/devices→ 看到 Agent 上報的 Kneron 裝置 - 點
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 |
存什麼:
{
"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. 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 |