- CLAUDE.md: 紅區治理章節 - Skills 01/03: 版本更新 - ADR/Architecture: 標準化 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
269 lines
6.7 KiB
Markdown
269 lines
6.7 KiB
Markdown
# 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 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<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 契約定義
|