從 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>
17 KiB
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.js(App Router,output: "export" static) |
16.1.x |
| 語言 | TypeScript(strict) | 5.x |
| UI | React | 19.2.x |
| 樣式 | Tailwind CSS | 4.x |
| 基礎元件 | Radix UI(radix-ui 單一套件,shadcn/ui 風格) |
1.4.x |
| 圖示 | lucide-react | 0.575.x |
| 主題 | next-themes(Light / 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.md(local-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 backend(tunnel client / pairing / connstate)的真實互動時使用。
# 在 local-agent/ 根目錄
wails dev
# → Wails 會自動啟動 `pnpm dev` 並注入 Wails runtime;前端呼叫 window.go.main.App.* 走真實 binding
此模式下:
isWailsRuntime()會回true,agent-api.ts改走真實 Wailswindow.go.main.App.*- 事件(
connection:status/connection:log/settings:updated/pairing:result)由 Go broadcaster 推送 - Token 會真的寫入 OS keychain(macOS Keychain / Windows Credential Manager / Linux Secret Service)
Build
pnpm build
產出到 out/(Next.js output: "export" 的靜態匯出結果)。Wails build 時透過 wails.json 的 frontend:install + frontend:build 呼叫此指令,然後透過 assetdir: "./frontend/out" 用 //go:embed 把 out/ 整包嵌入最終可執行檔。
# 完整桌面應用 build(在 local-agent/ 根目錄)
wails build
為什麼是 output: "export"?
| 方案 | 是否可行 | 理由 |
|---|---|---|
Next.js server(standalone) |
❌ | Wails 不跑 Node server |
| Next.js SSR / SSG | ❌ | 同上,且 Wails 桌面環境無 HTTP server |
Next.js output: "export" |
✅ | 純靜態,可 embed;local-tool 已長期驗證此模式穩定 |
| Vite + SPA | ❓ | 可行但放棄 — 會引入第二套 build pipeline,且 visionA-frontend 元件要改 import / 'use client' directive |
詳見 .autoflow/04-architecture/visiona-agent-tdd.md §3.2。
可用腳本
| 指令 | 說明 |
|---|---|
pnpm dev |
啟動開發伺服器(mock bindings,http://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 layout(LocaleProvider / ThemeProvider / TooltipProvider / Toaster)
│ └── page.tsx # AgentApp — 3 tabs(Radix Tabs)單頁切換入口
├── components/
│ ├── theme-provider.tsx
│ ├── layout/ # Agent 專屬 layout:AppShell / Header / TabNav / ConnectionStatusBadge
│ ├── agent/ # 狀態頁元件:StatusHero / InfoCard / RecentLog;配對頁元件:TokenInput
│ └── ui/ # 24 個 Radix UI primitives(shadcn 風格封裝)+ 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 dictionary(key 集合與順序與 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 |
StatusHero(80px 大狀態圓)+ 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 Tabs(controlled value/onValueChange),不走 Next.js routing。跨 view 的程式化切換透過 window CustomEvent agent:switch-tab 實現(見 status-view.tsx 與 page.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-sans(UI)/ font-mono(Token / Log / Relay URL) |
| 圓角 / 陰影 / 間距 | Tailwind 4 預設 + 少量 @theme 擴充 |
規則:不允許在元件裡寫死 #xxxxxx 或 rgb(...),一律透過 Tailwind utility class 引用 CSS 變數(例如 bg-status-online、text-destructive)。
i18n
採用自製輕量 i18n(不依賴 next-intl),以 React Context + localStorage 管理當前 locale。
- 預設 locale:
zh-Hant(繁中) - 支援:
zh-Hant/en - 儲存 key:
visionA.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:
- 兩個字典(
zh-Hant.ts+en.ts)同步新增 — 否則i18n.test.ts會失敗 - 保持兩邊區塊順序一致(便於 diff 與 review)
- 變數內插用
{name}風格(例:Attempt {n} of 5),呼叫端以String.prototype.replace替換 - 錯誤訊息要「說清楚發生什麼 + 使用者可以做什麼」
測試
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.tsx(Header + 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 組合 + iconaria-hidden
效能預算
本 Agent 不是 Web App,不需要滿足 Core Web Vitals。但仍有一些自律規則:
| 項目 | 目標 |
|---|---|
| Build 時間 | < 10s(Next.js 16 Turbopack 目前約 3-4s) |
out/ 總大小 |
< 5 MB(static export 後,Wails embed 成本敏感) |
| 首次渲染到互動 | < 200ms(Wails 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 motion:
motion-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 會改用 WailsBrowserOpenURL以觸發 OS 預設瀏覽器(而非 in-app WebView)。 - 不讀取使用者憑證 / 帳號 / API Key:Agent 的認證全靠 Pairing Token → Session Token 二段式換取,沒有使用者密碼。
路線圖 / 任務歷史
本 frontend 以 7 個任務逐步建置:
| Task | 範圍 | 狀態 |
|---|---|---|
| AF1 | 專案初始化(Next.js + Tailwind + Radix UI + i18n / ThemeProvider 基礎層) | ✅ |
| AF2 | Tab navigation Layout(AppShell / 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 儲存策略
給維護者的備忘
- 不要把 zustand store 加進來用於儲存狀態頁 / 配對頁的資料 — 連線狀態本質上是後端主導的狀態,前端以 Wails event 接收即可。若未來要做 UI-only 的複雜 state(例如多步表單),再引入 zustand。
- 不要在元件裡寫死 URL(雲端 pair 頁、docs、github)— 目前
pair-view.tsx/settings-view.tsx的 URL 還是 placeholder,Phase 1 應改為從agentconfig或wailsjsbindings 取得。 - 不要為了「看起來好看」新增動畫 — spec §1.3 明列「不做動畫秀」。
- 不要引入 routing — 三 tab 切換就是切換,Wails 環境下 URL 行為奇怪,避免之。
本 README 適用於任何接手 local-agent/frontend/ 的工程師。有疑問請先讀 spec 與 TDD;不清楚的決策請去查 ADR;不確定的文案請去查 i18n 字典。