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

269 lines
6.7 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.
# 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<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 切換 |
### 禁止事項
```typescript
// ❌ 禁止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 整合範例
```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 PatternCode 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<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 狀態機
```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<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超時則重連 |
```typescript
// 企業級 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](https://zustand-demo.pmnd.rs/)
- [TanStack Query](https://tanstack.com/query)
- ADR-002: Nothing.tech 設計系統 (動畫需求)
- [approvals-contract.yaml](../api/approvals-contract.yaml) - API 契約定義