# ADR-004: 前端狀態管理統一採用 Zustand > **狀態**: Accepted > **日期**: 2026-03-19 > **更新日期**: 2026-03-20 (Gate 0 驗證完成) > **決策者**: CTO + CPO --- ## Gate 0 里程碑驗證 (2026-03-20) **Tracer Bullet 測試通過!** 以下實作已驗證: | 元件 | Store | 狀態 | |------|-------|------| | Dashboard SSE | `dashboard.store.ts` | ✅ 即時同步 | | Approval Multi-Sig | `approval.store.ts` | ✅ 狀態機運作正常 | | HITL 簽核流程 | 整合 API `/approvals/{id}/approve` | ✅ TOCTOU 防護驗證 | --- ## 背景 AWOOOI 的前端 (Agent Hub) 需要處理高度頻繁的狀態更新,包括: - OpenClaw 的 SSE 思考串流 (`/agent/thinking`) - 即時狀態燈 (Data Pincer 呼吸動畫) - 待授權卡片的佇列管理 (`/approvals`) - Plugin 健康狀態即時更新 我們需要一個輕量、無需過度樣板代碼 (Boilerplate),且能與 React 18 完美協作的狀態管理庫。 ## 決策 **全面採用 Zustand 作為全域狀態管理工具** ```typescript // stores/agent.store.ts import { create } from 'zustand' import { subscribeWithSelector } from 'zustand/middleware' interface AgentState { status: 'idle' | 'thinking' | 'executing' | 'waiting_approval' thinkingStream: string[] pendingApprovals: Approval[] // Actions setStatus: (status: AgentState['status']) => void appendThinking: (chunk: string) => void addApproval: (approval: Approval) => void } export const useAgentStore = create()( subscribeWithSelector((set) => ({ status: 'idle', thinkingStream: [], pendingApprovals: [], setStatus: (status) => set({ status }), appendThinking: (chunk) => set((s) => ({ thinkingStream: [...s.thinkingStream, chunk] })), addApproval: (approval) => set((s) => ({ pendingApprovals: [...s.pendingApprovals, approval] })), })) ) ``` ### 狀態分層策略 | 層級 | 工具 | 用途 | |------|------|------| | **全域 UI 狀態** | Zustand | Agent 狀態、Sidebar 開關、Theme | | **伺服器資料快取** | TanStack Query | API 回應快取、自動重新驗證 | | **表單狀態** | React Hook Form | 表單驗證、欄位狀態 | | **元件局部狀態** | useState | 簡單 UI 切換 | ### 禁止事項 ```typescript // ❌ 禁止:Redux import { createStore } from 'redux' // ❌ 禁止:Context API 做複雜狀態管理 const GlobalContext = createContext(...) // ❌ 禁止:單一巨大 Store const useGodStore = create(() => ({ agent: ..., plugins: ..., pipelines: ..., // 太多! })) // ✅ 正確:Slice Pattern 分拆 const useAgentStore = create(...) const usePluginStore = create(...) const usePipelineStore = create(...) ``` ## 理由 ### 1. 效能優勢 | 特性 | Redux | Zustand | |------|-------|---------| | Bundle Size | ~7KB | ~1KB | | Boilerplate | 高 | 極低 | | Re-render 控制 | 需 memo/selector | 內建 selector | | SSE/WebSocket | 需 middleware | 原生支援 | ### 2. SSE 整合範例 ```typescript // hooks/useAgentThinking.ts export function useAgentThinking() { const appendThinking = useAgentStore((s) => s.appendThinking) useEffect(() => { const eventSource = new EventSource('/v1/agent/thinking') eventSource.onmessage = (event) => { appendThinking(event.data) // 直接更新 Zustand } return () => eventSource.close() }, [appendThinking]) } ``` ### 3. TanStack Query 協作 ```typescript // hooks/useApprovals.ts export function useApprovals() { return useQuery({ queryKey: ['approvals', 'pending'], queryFn: () => api.listApprovals({ status: 'pending' }), refetchInterval: 5000, // 每 5 秒輪詢 }) } ``` ## 後果 ### 優點 - **極度輕量** 不增加 bundle 負擔 - **高頻更新** 完美處理 SSE/WebSocket 串流 - **簡單 API** 降低學習曲線 - **TypeScript 友善** 完整型別推導 ### 缺點 - **生態較小** 相比 Redux 社群資源較少 - **DevTools** 功能不如 Redux DevTools 強大 ### 風險 | 風險 | 緩解措施 | |------|---------| | Store 肥大化 | 強制執行 Slice Pattern,Code Review 把關 | | 狀態同步錯誤 | 搭配 TanStack Query 管理伺服器狀態 | --- ## Gate 0 實作細節 ### 1. Dashboard SSE Store ```typescript // stores/dashboard.store.ts interface DashboardState { hosts: HostStatus[] connectionStatus: 'connecting' | 'connected' | 'disconnected' | 'error' lastUpdate: Date | null // SSE 控制 connect: (apiUrl: string) => void disconnect: () => void } export const useDashboardStore = create((set, get) => ({ hosts: [], connectionStatus: 'disconnected', lastUpdate: null, connect: (apiUrl) => { const eventSource = new EventSource(`${apiUrl}/api/v1/dashboard/stream`) eventSource.onmessage = (event) => { const data = JSON.parse(event.data) set({ hosts: data.hosts, lastUpdate: new Date() }) } eventSource.onerror = () => set({ connectionStatus: 'error' }) eventSource.onopen = () => set({ connectionStatus: 'connected' }) }, disconnect: () => { // AbortController cleanup } })) ``` ### 2. Approval Multi-Sig 狀態機 ```typescript // stores/approval.store.ts interface ApprovalState { pendingApprovals: Approval[] selectedApproval: Approval | null signingStatus: 'idle' | 'signing' | 'success' | 'error' // Actions signApproval: (id: string, userId: string, role: string) => Promise refreshApprovals: () => Promise } // 狀態機轉換圖 // pending → (簽核) → pending (需更多簽章) // pending → (簽核) → approved (達到閾值) // pending → (拒絕) → rejected // pending → (TOCTOU) → voided (資源狀態改變) ``` ### 3. SSE + Zustand 整合模式 **企業級 SSE 最佳實踐:** | 特性 | 實作 | |------|------| | **Buffer** | 累積 5 秒內的更新,批次 setState | | **AbortController** | 元件 unmount 時正確關閉連線 | | **Reconnection** | 指數退避重連 (1s → 2s → 4s → max 30s) | | **Heartbeat** | 每 30 秒 ping,超時則重連 | ```typescript // 企業級 SSE Hook 範例 function useSSE(url: string) { const abortControllerRef = useRef() const bufferRef = useRef([]) useEffect(() => { abortControllerRef.current = new AbortController() const flushBuffer = setInterval(() => { if (bufferRef.current.length > 0) { useDashboardStore.setState({ hosts: bufferRef.current }) bufferRef.current = [] } }, 5000) return () => { abortControllerRef.current?.abort() clearInterval(flushBuffer) } }, [url]) } ``` --- ## 參考 - [Zustand](https://zustand-demo.pmnd.rs/) - [TanStack Query](https://tanstack.com/query) - ADR-002: Nothing.tech 設計系統 (動畫需求) - [approvals-contract.yaml](../api/approvals-contract.yaml) - API 契約定義