- CLAUDE.md: 紅區治理章節 - Skills 01/03: 版本更新 - ADR/Architecture: 標準化 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
6.7 KiB
6.7 KiB
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 Pattern,Code 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])
}
參考
- Zustand
- TanStack Query
- ADR-002: Nothing.tech 設計系統 (動畫需求)
- approvals-contract.yaml - API 契約定義