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

311 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# visionA Agent — 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 Router`output: "export"` static | 16.1.x |
| 語言 | TypeScriptstrict | 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-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.md`local-agent 根目錄)
## 兩種開發模式
### 模式 A — 獨立前端開發(`pnpm dev`
純前端迭代(調色、調文案、元件樣式、互動邏輯)時使用,**不需要** Wails binding。
```bash
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的真實互動時使用。
```bash
# 在 local-agent/ 根目錄
wails dev
# → Wails 會自動啟動 `pnpm dev` 並注入 Wails runtime前端呼叫 window.go.main.App.* 走真實 binding
```
此模式下:
- `isWailsRuntime()` 會回 `true``agent-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
```bash
pnpm build
```
產出到 `out/`Next.js `output: "export"` 的靜態匯出結果。Wails build 時透過 `wails.json``frontend:install` + `frontend:build` 呼叫此指令,然後透過 `assetdir: "./frontend/out"``//go:embed``out/` 整包嵌入最終可執行檔。
```bash
# 完整桌面應用 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 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` | `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
1. 兩個字典(`zh-Hant.ts` + `en.ts`**同步新增** — 否則 `i18n.test.ts` 會失敗
2. 保持兩邊區塊順序一致(便於 diff 與 review
3. 變數內插用 `{name}` 風格(例:`Attempt {n} of 5`),呼叫端以 `String.prototype.replace` 替換
4. 錯誤訊息要「說清楚發生什麼 + 使用者可以做什麼」
## 測試
```bash
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 組合 + 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 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 會改用 Wails `BrowserOpenURL` 以觸發 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 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 應改為從 `agentconfig``wailsjs` bindings 取得。
3. **不要**為了「看起來好看」新增動畫 — spec §1.3 明列「不做動畫秀」。
4. **不要**引入 routing — 三 tab 切換就是切換Wails 環境下 URL 行為奇怪,避免之。
---
本 README 適用於任何接手 `local-agent/frontend/` 的工程師。有疑問請先讀 spec 與 TDD不清楚的決策請去查 ADR不確定的文案請去查 i18n 字典。