jim800121chen 3f0175f1a9 feat(local-agent): Phase 0.5 visionA Agent — Wails 桌面 + tunnel client + 配對 UI
從 local-tool 複製出獨立的「visionA Agent」桌面應用(A3 純橋樑:
tunnel client + 配對 UI + 設定,不開 HTTP port、不做本機裝置/推論 UI)。
Bundle ID 與 local-tool 不同(com.innovedus.visiona-agent vs visiona-local),
雙 app 可共存。fork 後不主動 sync,需要時手動 cherry-pick。

Backend / Wails Go(AB1-AB13):
- internal/tunnel:6 狀態機(Idle/Connecting/Connected/Reconnecting/Failed/Stopped)
  + Pair/Unpair/Reconnect/Disconnect binding + ClientHooks event
- internal/auth:encrypted file token store(AES-GCM + scrypt + machineID
  fallback salt + 13 tests)
- internal/config:YAML validation + atomic write + 11 tests
- internal/log:ring buffer + ExportLog 升級 zip
- visionA-backend /api/pairing/exchange:SessionTokenStore + 17 new tests
- 三平台 build 驗證(macOS DMG 160 MB / Windows EXE / Linux AppImage)
- end-to-end 5 milestone 全綠(pairing → tunnel → forward → reuse 防護
  → tunnel drop failover)

Frontend / Next.js(AF1-AF7,沿用 visionA-frontend 基礎):
- AppShell + Header + TabNav(StatusView / PairView / SettingsView 三 tab)
- ConnectionStatusBadge 5 種狀態
- TokenInput regex 驗證 + 7 種錯誤 + 0.5s auto-switch 到狀態頁
- 設定頁 4 區塊(含重新配對 AlertDialog)
- agent-api.ts 封裝 Wails bindings(mock/real 雙實作)+ 90 tests

Phase 0.7 review-driven fix(Round 2):
- A1 Session fixation 防護(RotateSessionID)
- A3 mock pairing 預設改 false(必須明確 opt-in)+ startup log
- A4 Pair 失敗後 state 清理矩陣(exchange/Save/Start fail 各自終態)
- A5 Pair/Unpair/Reconnect lifecycleMu + 50 goroutine race test
- F1 重新配對次按鈕 / F2 PairView Esc cancel / F3 Wails BrowserOpenURL
  / F4 Settings draft 持久 + 未儲存 badge

驗證:agent backend go test -race -count=3 ./... 4 packages 全綠 /
agent frontend pnpm test 119 tests 全綠

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:22:01 +08:00
..

visionA Agent — Frontend

visionA Agent 是 visionA 雲端的 local agent / tunnel bridge — 一個桌面應用,把本機的 Kneron 邊緣裝置安全地連上 visionA 雲端。本目錄是它的前端實作純靜態匯出Next.js + React由 Wails Go runtime 透過 go:embed 嵌入可執行檔三平台macOS / Windows / Linux共用同一份程式碼。

定位:「開了就忘掉它」的工具。使用者配對一次後Agent 只要維持連線、不出問題就好。UI 極簡,沒有裝置面板、推論視窗、行銷素材 — 只有連線狀態、配對、設定三頁。

技術堆疊

層級 技術 版本
框架 Next.jsApp Routeroutput: "export" static 16.1.x
語言 TypeScriptstrict 5.x
UI React 19.2.x
樣式 Tailwind CSS 4.x
基礎元件 Radix UIradix-ui 單一套件shadcn/ui 風格) 1.4.x
圖示 lucide-react 0.575.x
主題 next-themesLight / Dark / System 0.4.x
Toast sonner 2.x
動畫 tw-animate-css 1.4.x
狀態管理 zustand依賴保留雛形階段實際未用 5.x
測試 Vitest + @testing-library/react + jsdom Vitest 4 / RTL 16

與其他前端專案的關係

專案 定位 是否為本專案的來源
visionA-frontend/(雲端 Web 使用者的主要操作介面 — 裝置管理、模型管理、推論 。Design Tokens (globals.css)、Theme Provider、i18n 結構、utils.ts、Radix UI 基礎元件複製自此
local-tool/frontend/(本機 GUI 開發者跑推論用的桌面工具 不是。本專案不是 local-tool 的 fork — 我們只借鑒其 Wails + Next.js output:"export" 的打包模式UI 和邏輯完全獨立
local-agent/frontend/(本目錄) 連線橋樑 UI狀態 / 配對 / 設定) 獨立專案

核心設計決策:視覺上與 visionA-frontend 保持一致,讓同一位使用者在雲端 Web 與桌面 Agent 之間切換「能一眼認出是同家人」。

前置需求

  • Node.js ≥ 20
  • pnpm ≥ 9專案使用 pnpm請勿 npm install / yarn install,會產生第二份 lockfile
  • (整合模式才需要)Go ≥ 1.22 與 Wails CLI v2 — 詳見 ../README.mdlocal-agent 根目錄)

兩種開發模式

模式 A — 獨立前端開發(pnpm dev

純前端迭代(調色、調文案、元件樣式、互動邏輯)時使用,不需要 Wails binding。

pnpm install
pnpm dev
# → http://localhost:3000支援 HMR

此模式下:

  • src/hooks/use-* 的 Wails binding 呼叫會自動走 mock 實作(見 src/lib/agent-api.ts 的 runtime 偵測)
  • 連線狀態、配對結果、設定值皆為假資料;可切 notPaired / online / reconnecting / error 等變體驗證 UI
  • 可直接切換 Light / Dark、zh-Hant / en 驗證 Design Tokens 與 i18n

模式 B — 與 Wails 整合(wails dev

驗證與 Go backendtunnel client / pairing / connstate的真實互動時使用。

# 在 local-agent/ 根目錄
wails dev
# → Wails 會自動啟動 `pnpm dev` 並注入 Wails runtime前端呼叫 window.go.main.App.* 走真實 binding

此模式下:

  • isWailsRuntime() 會回 trueagent-api.ts 改走真實 Wails window.go.main.App.*
  • 事件(connection:status / connection:log / settings:updated / pairing:result)由 Go broadcaster 推送
  • Token 會真的寫入 OS keychainmacOS Keychain / Windows Credential Manager / Linux Secret Service

Build

pnpm build

產出到 out/Next.js output: "export" 的靜態匯出結果。Wails build 時透過 wails.jsonfrontend:install + frontend:build 呼叫此指令,然後透過 assetdir: "./frontend/out"//go:embedout/ 整包嵌入最終可執行檔。

# 完整桌面應用 build在 local-agent/ 根目錄)
wails build

為什麼是 output: "export"

方案 是否可行 理由
Next.js serverstandalone Wails 不跑 Node server
Next.js SSR / SSG 同上,且 Wails 桌面環境無 HTTP server
Next.js output: "export" 純靜態,可 embedlocal-tool 已長期驗證此模式穩定
Vite + SPA 可行但放棄 — 會引入第二套 build pipeline且 visionA-frontend 元件要改 import / 'use client' directive

詳見 .autoflow/04-architecture/visiona-agent-tdd.md §3.2。

可用腳本

指令 說明
pnpm dev 啟動開發伺服器mock bindingshttp://localhost:3000
pnpm build 產出靜態檔到 out/(供 Wails go:embed
pnpm lint ESLint 檢查(eslint-config-next + react-hooks
pnpm test 執行所有 Vitest 測試one-shot
pnpm test:watch Watch 模式

專案結構

frontend/
├── next.config.ts          # output: "export" + trailingSlash + images.unoptimized
├── package.json            # name: visiona-agent-frontend
├── tsconfig.json
├── postcss.config.mjs
├── eslint.config.mjs
├── vitest.config.ts
├── components.json         # shadcn/ui 產生器設定(保留以利未來新增元件)
├── public/
│   └── visiona-logo.png
└── src/
    ├── app/
    │   ├── globals.css     # Design Tokens + Tailwind 層100% 從 visionA-frontend 複製)
    │   ├── layout.tsx      # Root layoutLocaleProvider / ThemeProvider / TooltipProvider / Toaster
    │   └── page.tsx        # AgentApp — 3 tabsRadix Tabs單頁切換入口
    ├── components/
    │   ├── theme-provider.tsx
    │   ├── layout/         # Agent 專屬 layoutAppShell / Header / TabNav / ConnectionStatusBadge
    │   ├── agent/          # 狀態頁元件StatusHero / InfoCard / RecentLog配對頁元件TokenInput
    │   └── ui/             # 24 個 Radix UI primitivesshadcn 風格封裝)+ EmptyState / Spinner / Sonner
    ├── views/              # 3 個 tab 對應的 view 元件(不走 Next.js routing以 useState 切換)
    │   ├── status-view.tsx
    │   ├── pair-view.tsx
    │   └── settings-view.tsx
    ├── hooks/              # Wails bindings 的 React wrapper
    │   ├── use-connection-status.ts   # GetStatus() + connection:status event
    │   ├── use-recent-logs.ts         # GetRecentLog() + connection:log event
    │   ├── use-pair.ts                # Pair(token) + pairing:result event
    │   ├── use-settings.ts            # GetSettings() + UpdateSettings(patch) + settings:updated
    │   ├── use-test-connection.ts     # TestRelay(url)
    │   └── use-export-log.ts          # ExportLog()
    ├── lib/
    │   ├── utils.ts        # cn() — clsx + tailwind-merge
    │   ├── agent-api.ts    # Wails binding 抽象層(真實 binding + mock 雙實作自動切換)
    │   └── i18n/
    │       ├── context.tsx         # LocaleProvider / useLocale / useT
    │       ├── sync.tsx            # LocaleSync — mount 後從 localStorage 拉偏好
    │       ├── index.ts            # dictionaries 匯出入口
    │       ├── types.ts            # Locale / Dictionary / SUPPORTED_LOCALES
    │       └── dictionaries/
    │           ├── zh-Hant.ts      # 繁中字典(預設,約 93 keys
    │           └── en.ts           # English dictionarykey 集合與順序與 zh-Hant 完全一致)
    ├── types/              # 前端型別定義
    │   ├── agent.ts        # ConnectionState / ConnectionSnapshot / LogEntry / AgentSettings / PairError
    │   └── api.ts          # 與本地 server 互動的 envelope預留
    └── tests/
        └── setup.ts        # @testing-library/jest-dom 全域 matcher

三個頁面對應

Tab 路徑 主要元件 對應 spec 章節
狀態(status,預設) views/status-view.tsx StatusHero80px 大狀態圓)+ InfoCard(帳號/Relay/Session+ RecentLog(最近 10 筆事件) spec §4
配對(pair views/pair-view.tsx TokenInput(格式驗證 + 貼上 trim + Enter 送出)+ 錯誤 Alert + 底部安全提示 spec §5
設定(settings views/settings-view.tsx 5 區塊:連線 / 行為 / Log / 關於 / 危險區域 spec §6

Tab 切換走 Radix Tabscontrolled value/onValueChange),不走 Next.js routing。跨 view 的程式化切換透過 window CustomEvent agent:switch-tab 實現(見 status-view.tsxpage.tsx)。

Design Tokens

完全沿用 visionA-frontend:不動一行 globals.css

Token 類別 來源
色彩(--background / --primary / --muted-foreground 等) globals.css@layer base 定義 light / dark 值)
連線狀態色(--status-online / --status-offline / --status-reconnecting / --status-idle / --status-error 同上visionA-frontend F2 定義)
字型 font-sansUI/ font-monoToken / Log / Relay URL
圓角 / 陰影 / 間距 Tailwind 4 預設 + 少量 @theme 擴充

規則:不允許在元件裡寫死 #xxxxxxrgb(...),一律透過 Tailwind utility class 引用 CSS 變數(例如 bg-status-onlinetext-destructive)。

i18n

採用自製輕量 i18n不依賴 next-intl),以 React Context + localStorage 管理當前 locale。

  • 預設 localezh-Hant(繁中)
  • 支援:zh-Hant / en
  • 儲存 keyvisionA.locale
  • Fallback找不到 key 時回傳 key 本身production 靜默dev 印 warning
  • 測試保證:i18n.test.ts 驗證兩語系 key 集合完全一致,避免漏譯

字典採扁平 key 結構(例:nav.status),共分 8 個區塊:

區塊 說明
app.* 產品名稱、標語
common.* 通用按鈕 / 文案loading / cancel / save ...
nav.* Tab 導航標籤
connection.* 連線狀態online / offline / reconnecting / notPaired / error
header.* Header 工具列(切主題、切語言)
status.* 狀態頁hero / info / action / confirm / log / empty
pair.* 配對頁title / input / button / alert / error
settings.* 設定頁section / relayUrl / behavior / log / about / reset

新增 i18n key 時的 checklist

  1. 兩個字典(zh-Hant.ts + en.ts同步新增 — 否則 i18n.test.ts 會失敗
  2. 保持兩邊區塊順序一致(便於 diff 與 review
  3. 變數內插用 {name} 風格(例:Attempt {n} of 5),呼叫端以 String.prototype.replace 替換
  4. 錯誤訊息要「說清楚發生什麼 + 使用者可以做什麼」

測試

pnpm test           # one-shot 執行所有測試
pnpm test:watch     # watch 模式

測試策略

層級 工具 範例
單元(元件) Vitest + RTL button.test.tsx / token-input.test.tsx(行為 + 格式驗證 regex
單元hooks / utils Vitest i18n.test.ts(字典完整性 + key 集合一致性)
整合view Vitest + RTL app-shell.test.tsxHeader + Badge 組合)
E2E 由 Testing Agent 負責,不在本 repo

關鍵測試

  • i18n.test.ts — 字典完整性守門員;兩語系 key 集合差異時立即失敗
  • token-input.test.tsx — Pairing Token regex ^vAc_[a-f0-9]{32}$/i(大小寫不敏感)+ 貼上清理 + Enter 送出
  • status-hero.test.tsx — 5 種狀態變體online / offline / reconnecting / notPaired / error+ aria-label 組合 + icon aria-hidden

效能預算

本 Agent 不是 Web App不需要滿足 Core Web Vitals。但仍有一些自律規則

項目 目標
Build 時間 < 10sNext.js 16 Turbopack 目前約 3-4s
out/ 總大小 < 5 MBstatic export 後Wails embed 成本敏感)
首次渲染到互動 < 200msWails WebView 2 本機載入,無網路延遲)
冷啟動到可互動 < 1s含 Wails process init + WebView init

bundle 分析:pnpm build 時 Next.js 會印出每條 route 的大小。若單 route JS > 200KB需檢討是否有多餘依賴或未做 code splitting。

無障礙規範

嚴格遵守 WCAG 2.2 AA對齊 spec §9

  • 不只靠顏色:每個連線狀態同時有「圓點顏色 + icon + 文字標籤」
  • 語義化 HTML<header> / <main> / <nav> / <section> / <dl><dt><dd> / <ol>;避免 <div> 當按鈕
  • 鍵盤導航:所有操作 Tab 可達Enter 送出表單Esc 關閉 Dialog
  • ARIA
    • role="status" aria-live="polite" — 狀態變化主動朗讀StatusHero / ConnectionStatusBadge
    • Session Token 遮蔽版:aria-label="Session token, ending in e7f8"(不念完整字串)
    • Tab使用 Radix Tabs 原生 role="tab" / role="tabpanel" / aria-selected
  • 焦點可見:所有互動元件 focus-visible:ring-[3px] ring-ring
  • Reduce motionmotion-safe: prefix 讓 prefers-reduced-motion: reduce 使用者停用 pulse / spin

環境變數

變數 說明 預設值 必要
NODE_ENV development / production(由 Next.js 自動設定)

Agent 前端刻意不讀取環境變數 — 所有使用者偏好Relay URL、Log 等級、自啟動)由 Go backend 的 agentconfig 管理;前端只透過 Wails binding GetSettings() / UpdateSettings(patch) 讀寫。

安全

  • 不儲存 Pairing Token:使用者貼上的 token 經 Pair(token) 送到 Go backend前端 state 在成功 / 失敗後清空。長期 Session Token 由 Go 寫入 OS keychain前端永遠拿不到完整 token,只能看到遮蔽版 sessionTokenPreview(例如 vAs_a1b2c3d4 ··· e7f8)。
  • dangerouslySetInnerHTML:本前端 0 處使用,已 grep 驗證。
  • 外部 URL 開啟:使用 window.open(url, "_blank", "noopener,noreferrer")AF6 會改用 Wails BrowserOpenURL 以觸發 OS 預設瀏覽器(而非 in-app WebView
  • 不讀取使用者憑證 / 帳號 / API KeyAgent 的認證全靠 Pairing Token → Session Token 二段式換取,沒有使用者密碼。

路線圖 / 任務歷史

本 frontend 以 7 個任務逐步建置:

Task 範圍 狀態
AF1 專案初始化Next.js + Tailwind + Radix UI + i18n / ThemeProvider 基礎層)
AF2 Tab navigation LayoutAppShell / Header / TabNav / ConnectionStatusBadge
AF3 狀態頁StatusHero + InfoCard + RecentLog + EmptyState
AF4 配對頁TokenInput + PairView + 7 種錯誤訊息對照)
AF5 設定頁(連線 / 行為 / Log / 關於 / 危險區域 5 區塊)
AF6 Wails bindings 整合(agent-api.ts 抽象層 + 真實 binding 取代 hooks 的 mock (與 AF7 併發)
AF7 i18n 字典審查 + README 完整版 + 前端收尾

細節與原始 TDD 任務拆分詳見 .autoflow/04-architecture/visiona-agent-tdd.md §15.2。

參考文件

  • .autoflow/03-design/visiona-agent-spec.md — UI 設計規格3 頁面 + 全域 Header + 所有互動行為 + i18n key 清單)
  • .autoflow/04-architecture/visiona-agent-tdd.md — 技術設計文件
    • §2 整體架構
    • §3 專案結構§3.2 為何沿用 Next.js
    • §6 三個 UI 頁面對接Wails bindings + events 規格)
  • .autoflow/04-architecture/adr/adr-008-tunnel-code-copy.md — Tunnel client 複製策略
  • .autoflow/04-architecture/adr/adr-009-token-storage.md — Token 儲存策略

給維護者的備忘

  1. 不要把 zustand store 加進來用於儲存狀態頁 / 配對頁的資料 — 連線狀態本質上是後端主導的狀態,前端以 Wails event 接收即可。若未來要做 UI-only 的複雜 state例如多步表單再引入 zustand。
  2. 不要在元件裡寫死 URL雲端 pair 頁、docs、github— 目前 pair-view.tsx / settings-view.tsx 的 URL 還是 placeholderPhase 1 應改為從 agentconfigwailsjs bindings 取得。
  3. 不要為了「看起來好看」新增動畫 — spec §1.3 明列「不做動畫秀」。
  4. 不要引入 routing — 三 tab 切換就是切換Wails 環境下 URL 行為奇怪,避免之。

本 README 適用於任何接手 local-agent/frontend/ 的工程師。有疑問請先讀 spec 與 TDD不清楚的決策請去查 ADR不確定的文案請去查 i18n 字典。