Files
awoooi/docs/adr/ADR-004-state-management.md
OG T 604e38cf07 docs: Phase 14 紅區治理 + Skills 01/03 更新
- CLAUDE.md: 紅區治理章節
- Skills 01/03: 版本更新
- ADR/Architecture: 標準化

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-26 09:55:47 +08:00

6.7 KiB
Raw Blame History

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 作為全域狀態管理工具

// 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<AgentState>()(
  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 切換

禁止事項

// ❌ 禁止Redux
import { createStore } from 'redux'

// ❌ 禁止Context API 做複雜狀態管理
const GlobalContext = createContext<ComplexState>(...)

// ❌ 禁止:單一巨大 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 整合範例

// 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 協作

// 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 PatternCode Review 把關
狀態同步錯誤 搭配 TanStack Query 管理伺服器狀態

Gate 0 實作細節

1. Dashboard SSE Store

// 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<DashboardState>((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 狀態機

// 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<void>
  refreshApprovals: () => Promise<void>
}

// 狀態機轉換圖
// 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超時則重連
// 企業級 SSE Hook 範例
function useSSE(url: string) {
  const abortControllerRef = useRef<AbortController>()
  const bufferRef = useRef<HostStatus[]>([])

  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])
}

參考