Files
zclaw_openfang/docs/superpowers/specs/2026-03-21-architecture-optimization-design.md
iven 0d4fa96b82
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
refactor: 统一项目名称从OpenFang到ZCLAW
重构所有代码和文档中的项目名称,将OpenFang统一更新为ZCLAW。包括:
- 配置文件中的项目名称
- 代码注释和文档引用
- 环境变量和路径
- 类型定义和接口名称
- 测试用例和模拟数据

同时优化部分代码结构,移除未使用的模块,并更新相关依赖项。
2026-03-27 07:36:03 +08:00

719 lines
23 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.

# ZCLAW 架构优化设计规格
> **版本**: 1.1
> **日期**: 2026-03-21
> **状态**: 待审核
> **作者**: Claude + 用户协作
---
## 1. 概述
### 1.1 背景
ZCLAW 是面向中文用户的 AI Agent 桌面客户端,经过分析发现以下需要改进的领域:
- **安全风险**: localStorage 凭据回退明文存储、WebSocket 非 localhost 允许 ws://
- **性能瓶颈**: 流式更新时重建整个消息数组、无界消息数组
- **架构问题**: 50+ lib 模块缺乏统一抽象、测试覆盖不足
### 1.2 目标
- 在 14 周内完成全面架构优化 (含 2 周缓冲)
- 采用激进架构优先策略
- 重点优化四个核心系统对话、Hands、Intelligence、技能
### 1.3 术语表
| 术语 | 含义 | 备注 |
|------|------|------|
| **Valtio** | Proxy-based 状态管理库 | pmnd.rs/valtio将替代 Zustand |
| Zustand | 当前使用的状态管理库 | 将被 Valtio 替代 |
| XState | 状态机库 | 用于 Hands 状态管理 |
### 1.4 关键决策
| 决策点 | 选择 | 理由 |
|--------|------|------|
| 状态管理 | **Valtio** (替代 Zustand) | Proxy 细粒度响应,性能更好 |
| 安全策略 | 凭据加密存储 + WSS 强制 | 解决实际存在的安全风险 |
| 整体方案 | A+C 混合 | 渐进式 + 领域驱动结合 |
---
## 2. 架构设计
### 2.1 总体架构
```
┌─────────────────────────────────────────────────────────────────┐
│ UI Layer │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ React Components (60+) │ │
│ │ - 按领域组织 │ │
│ │ - 只负责展示和交互 │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ State Layer │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Valtio Stores (基于 Proxy) │ │
│ │ - 细粒度响应 │ │
│ │ - 领域划分: Chat, Hands, Intelligence, Skills │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Client Layer │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐ │
│ │ Gateway Client │ │ Intelligence Clt │ │ Worker Pool │ │
│ │ (WebSocket) │ │ (Tauri Commands) │ │ (隔离执行) │ │
│ └──────────────────┘ └──────────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Backend Layer │
│ ┌──────────────────┐ ┌──────────────────────────────────────┐│
│ │ ZCLAW Kernel │ │ Tauri Rust Backend ││
│ │ (Port 50051) │ │ - Intelligence (心跳/压缩/反思) ││
│ │ │ │ - Memory (SQLite 持久化) ││
│ │ │ │ - Browser (WebDriver) ││
│ └──────────────────┘ └──────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
```
### 2.2 领域划分
```
desktop/src/
├── domains/ # 领域模块 (新建)
│ ├── chat/ # 对话系统
│ │ ├── store.ts # Valtio store
│ │ ├── types.ts # 类型定义
│ │ ├── api.ts # API 调用
│ │ └── hooks.ts # React hooks
│ ├── hands/ # 自动化系统
│ │ ├── store.ts # 状态机 store
│ │ ├── machine.ts # XState 状态机
│ │ ├── executor.ts # 执行器
│ │ └── types.ts
│ ├── intelligence/ # 智能层
│ │ ├── client.ts # 统一客户端
│ │ ├── cache.ts # 缓存策略
│ │ └── types.ts
│ └── skills/ # 技能系统
│ ├── store.ts
│ ├── loader.ts # 技能加载器
│ └── types.ts
├── workers/ # Web Workers (新建)
│ ├── browser-worker.ts # 浏览器隔离执行
│ └── pool.ts # Worker 池管理
├── shared/ # 共享模块 (新建)
│ ├── error-handling.ts # 统一错误处理
│ ├── logging.ts # 统一日志
│ └── types.ts # 共享类型
└── components/ # UI 组件 (保持)
```
### 2.3 现有代码分析
| 模块 | 现有文件 | 行数 | 迁移策略 |
|------|----------|------|----------|
| Chat | `store/chatStore.ts` | 689 | 迁移到 Valtio保留 API |
| Hands | `store/handStore.ts` | 535 | 迁移到 XState 状态机 |
| Intelligence | `lib/intelligence-client.ts` | 956 | 保留,增强缓存层 |
| Browser | `lib/browser-client.ts` | 461 | 保留,无需 Worker |
| Secure Storage | `lib/secure-storage.ts` | 350 | 增强,添加加密回退 |
| Gateway | `lib/gateway-client.ts` | 1100 | 保留,强制 WSS |
### 2.4 迁移映射表
| 现有文件 | 新位置 | 操作 |
|----------|--------|------|
| `store/chatStore.ts` | `domains/chat/store.ts` | 迁移 + 重写 |
| `store/handStore.ts` | `domains/hands/store.ts` | 迁移 + 状态机 |
| `store/skillStore.ts` | `domains/skills/store.ts` | 迁移 |
| `lib/intelligence-client.ts` | `domains/intelligence/client.ts` | 迁移 + 增强 |
| `lib/error-handling.ts` | `shared/error-handling.ts` | 迁移 |
| `lib/secure-storage.ts` | `shared/secure-storage.ts` | 增强 |
| `store/agentStore.ts` | 保留 | 不变 |
| `store/connectionStore.ts` | 保留 | 不变 |
| `lib/gateway-client.ts` | 保留 | 不变 |
---
## 3. 核心组件设计
### 3.1 Valtio 状态管理 (替代 Zustand)
**问题**: 当前 Zustand 每次更新都会触发整个订阅组件重渲染
**解决方案**: 使用 Valtio 的 Proxy 实现细粒度响应
```typescript
// domains/chat/store.ts
import { proxy, useSnapshot } from 'valtio';
interface ChatState {
messages: Message[];
conversations: Conversation[];
currentConversationId: string | null;
isStreaming: boolean;
// Actions
addMessage: (message: Message) => void;
updateMessage: (id: string, update: Partial<Message>) => void;
setStreaming: (streaming: boolean) => void;
}
export const chatState = proxy<ChatState>({
messages: [],
conversations: [],
currentConversationId: null,
isStreaming: false,
addMessage: (message) => {
chatState.messages.push(message); // 直接 mutate
},
updateMessage: (id, update) => {
const msg = chatState.messages.find(m => m.id === id);
if (msg) Object.assign(msg, update); // 细粒度更新
},
setStreaming: (streaming) => {
chatState.isStreaming = streaming;
}
});
// Hook
export function useChatState() {
return useSnapshot(chatState); // 只在访问的字段变化时重渲染
}
// 组件使用
function MessageList() {
const { messages } = useChatState(); // 只订阅 messages
return messages.map(m => <Message key={m.id} message={m} />);
}
function StreamingIndicator() {
const { isStreaming } = useChatState(); // 只订阅 isStreaming
return isStreaming ? <Spinner /> : null;
}
```
**迁移策略**:
- 现有 `chatStore.ts` (689 行) 将被重写
- 保持相同的 API 接口,确保组件无需修改
- 逐步迁移,先迁移 Chat再迁移其他 Store
**预期收益**:
- 流式更新时性能提升 70%
- 代码更简洁 (直接 mutate)
- 选择性渲染减少不必要的重渲染
### 3.2 Hands 状态机 (XState)
**问题**: 当前 `handStore.ts` (535 行) 状态转换逻辑分散,难以追踪
**解决方案**: 使用 XState 实现状态机,与现有 Store 集成
```typescript
// domains/hands/machine.ts
import { createMachine, assign } from 'xstate';
export const handMachine = createMachine({
id: 'hand',
initial: 'idle',
states: {
idle: {
on: { TRIGGER: 'validating' },
},
validating: {
on: {
VALID: 'checking_approval',
INVALID: 'error',
},
},
checking_approval: {
on: {
NEEDS_APPROVAL: 'pending_approval',
AUTO_APPROVE: 'executing',
},
},
pending_approval: {
on: {
APPROVE: 'executing',
REJECT: 'cancelled',
TIMEOUT: 'error',
},
},
executing: {
on: {
SUCCESS: 'completed',
ERROR: 'error',
TIMEOUT: 'error',
},
},
completed: { on: { RESET: 'idle' } },
error: { on: { RETRY: 'validating', RESET: 'idle' } },
cancelled: { on: { RESET: 'idle' } },
},
});
// domains/hands/store.ts - 与现有 API 集成
import { createActorContext } from '@xstate/react';
export const HandContext = createActorContext(handMachine);
// 使用方式
function HandPanel() {
const state = HandContext.useSelector(s => s);
const send = HandContext.useActorRef();
return (
<div>
{state.matches('pending_approval') && <ApprovalDialog />}
{state.matches('executing') && <Spinner />}
</div>
);
}
```
**迁移策略**:
- 现有 `handStore.ts` 保持向后兼容
- 新建 `domains/hands/` 模块
- 逐步将状态逻辑迁移到 XState
- 保持现有组件 API 不变
### 3.3 Intelligence 缓存增强
**问题**: 现有 `intelligence-client.ts` (956 行) 已有 localStorage 回退缓存,但缺少 LRU 淘汰和 TTL
**解决方案**: 增强现有缓存层,添加 LRU + TTL
> **注意**: 这是增强现有实现,而非替换
```typescript
// domains/intelligence/cache.ts
interface CacheEntry<T> {
value: T;
expiresAt: number;
}
export class IntelligenceCache {
private cache = new Map<string, CacheEntry<unknown>>();
private maxSize = 100;
private defaultTTL = 5 * 60 * 1000; // 5 minutes
get<T>(key: string): T | null {
const entry = this.cache.get(key);
if (!entry) return null;
if (Date.now() > entry.expiresAt) {
this.cache.delete(key);
return null;
}
return entry.value as T;
}
set<T>(key: string, value: T, ttl = this.defaultTTL): void {
// LRU 淘汰
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, {
value,
expiresAt: Date.now() + ttl,
});
}
invalidate(pattern: string): void {
for (const key of this.cache.keys()) {
if (key.includes(pattern)) {
this.cache.delete(key);
}
}
}
}
```
---
## 4. 安全设计
### 4.1 凭据存储加密增强
> **现有实现**: `lib/secure-storage.ts` (350 行) 已使用 OS keyring + localStorage fallback
> **问题**: localStorage fallback 存储明文密钥,存在安全风险
> **策略**: 增强现有实现,为 localStorage fallback 添加加密
```typescript
// shared/secure-storage.ts
export class SecureCredentialStorage {
private async getEncryptionKey(): Promise<CryptoKey> {
// 从 OS keyring 获取或派生
const masterKey = await this.getMasterKey();
return deriveKey(masterKey, SALT, {
name: 'AES-GCM',
length: 256,
});
}
async store(key: string, value: string): Promise<void> {
const encKey = await this.getEncryptionKey();
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
encKey,
new TextEncoder().encode(value)
);
// 存储加密后的数据
const stored = {
iv: arrayToBase64(iv),
data: arrayToBase64(new Uint8Array(encrypted)),
};
if (isTauriEnv()) {
await invoke('secure_store', { key, value: JSON.stringify(stored) });
} else {
localStorage.setItem(`enc_${key}`, JSON.stringify(stored));
}
}
async retrieve(key: string): Promise<string | null> {
let stored: { iv: string; data: string };
if (isTauriEnv()) {
stored = await invoke('secure_retrieve', { key });
} else {
const raw = localStorage.getItem(`enc_${key}`);
if (!raw) return null;
stored = JSON.parse(raw);
}
const encKey = await this.getEncryptionKey();
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: base64ToArray(stored.iv) },
encKey,
base64ToArray(stored.data)
);
return new TextDecoder().decode(decrypted);
}
}
```
### 4.2 WebSocket 安全
```typescript
// shared/websocket-security.ts
export function createSecureWebSocket(url: string): WebSocket {
// 强制 WSS for non-localhost
if (!url.startsWith('wss://') && !isLocalhost(url)) {
throw new SecurityError(
'Non-localhost connections must use WSS protocol'
);
}
const ws = new WebSocket(url);
// 添加消息验证
ws.addEventListener('message', (event) => {
const message = validateMessage(event.data);
if (!message) {
console.error('Invalid message format');
ws.close(1008, 'Policy Violation');
}
});
return ws;
}
function isLocalhost(url: string): boolean {
const parsed = new URL(url);
return ['localhost', '127.0.0.1', '::1'].includes(parsed.hostname);
}
```
---
## 5. 测试策略
### 5.1 单元测试
```typescript
// domains/chat/store.test.ts
describe('ChatState', () => {
beforeEach(() => {
chatState.messages = [];
chatState.isStreaming = false;
});
it('should add message', () => {
const msg = createMockMessage();
chatState.addMessage(msg);
expect(chatState.messages).toHaveLength(1);
expect(chatState.messages[0]).toEqual(msg);
});
it('should update message content', () => {
chatState.messages = [createMockMessage({ id: '1', content: 'old' })];
chatState.updateMessage('1', { content: 'new' });
expect(chatState.messages[0].content).toBe('new');
});
it('should not trigger re-render for unrelated updates', () => {
const { result, rerender } = renderHook(() => useChatState());
act(() => {
chatState.isStreaming = true; // 不会触发 MessageList 重渲染
});
// 验证只有订阅 isStreaming 的组件重渲染
});
});
```
### 5.2 集成测试
```typescript
// domains/hands/machine.test.ts
describe('HandMachine', () => {
it('should transition from idle to validating on TRIGGER', () => {
const service = interpret(handMachine).start();
service.send('TRIGGER');
expect(service.state.value).toBe('validating');
});
it('should reach pending_approval for hands requiring approval', () => {
const service = interpret(handMachine).start();
service.send('TRIGGER');
service.send('VALID');
service.send('NEEDS_APPROVAL');
expect(service.state.value).toBe('pending_approval');
});
it('should cancel on REJECT', () => {
const service = interpret(handMachine).start();
// ... navigate to pending_approval
service.send('REJECT');
expect(service.state.value).toBe('cancelled');
});
});
```
### 5.3 E2E 测试场景
1. **聊天流程**: 发送消息 → 接收流式响应 → 显示完整消息
2. **Hands 触发**: 触发 Hand → 审批流程 → 执行 → 结果显示
3. **Intelligence**: 记忆提取 → 心跳触发 → 反思生成
4. **技能搜索**: 输入关键词 → 搜索技能 → 查看详情
---
## 6. 实施计划
> **总周期**: 14 周 (含 2 周缓冲)
### 6.1 Phase 1: 安全 + 测试 (2周)
**目标**: 建立安全基础和测试框架
#### TASK-001: 凭据加密存储增强
- **输入条件**: 现有 `secure-storage.ts` 可用
- **输出产物**: 增强的 `shared/secure-storage.ts`
- **验收标准**:
- localStorage fallback 使用 AES-GCM 加密
- 所有单元测试通过
- 无回归问题
- **预估工时**: 3 人日
- **依赖**: 无
#### TASK-002: 强制 WSS 连接
- **输入条件**: 现有 `gateway-client.ts` 可用
- **输出产物**: 更新的 `gateway-client.ts`
- **验收标准**:
- 非 localhost 连接必须使用 wss://
- 测试覆盖边界情况
- **预估工时**: 1 人日
- **依赖**: 无
#### TASK-003: 测试框架建立
- **输入条件**: 无
- **输出产物**: Vitest + Playwright 配置
- **验收标准**:
- 覆盖率门禁 60% 生效
- CI 集成完成
- **预估工时**: 2 人日
- **依赖**: 无
#### TASK-004: chatStore 基础测试
- **输入条件**: 测试框架就绪
- **输出产物**: `store/chatStore.test.ts`
- **验收标准**:
- 核心功能测试覆盖
- 覆盖率 > 80%
- **预估工时**: 2 人日
- **依赖**: TASK-003
### 6.2 Phase 2: 领域重组 (4周)
**目标**: 按领域重组代码,迁移到 Valtio
#### TASK-101: 创建 domains/ 目录结构
- **预估工时**: 1 人日
#### TASK-102: 迁移 Chat Store 到 Valtio
- **输入条件**: 现有 `chatStore.ts` (689 行)
- **输出产物**: `domains/chat/store.ts`
- **验收标准**:
- API 向后兼容
- 性能提升 > 50%
- 测试覆盖率 > 90%
- **预估工时**: 5 人日
#### TASK-103: 迁移 Hands Store + XState
- **输入条件**: 现有 `handStore.ts` (535 行)
- **输出产物**: `domains/hands/store.ts` + `machine.ts`
- **验收标准**:
- 状态机完整实现
- 审批流程正常
- 测试覆盖率 > 85%
- **预估工时**: 5 人日
#### TASK-104: 增强 Intelligence 缓存
- **输入条件**: 现有 `intelligence-client.ts` (956 行)
- **输出产物**: 增强的缓存层
- **预估工时**: 3 人日
#### TASK-105: 提取共享模块
- **预估工时**: 2 人日
### 6.3 Phase 3: 核心优化 (6周并行)
**Track A: Chat 优化 (2人)**
- [ ] Valtio 性能优化
- [ ] 虚拟滚动实现 (react-window)
- [ ] 消息分页 + 惰性加载
- [ ] 流式响应 AsyncGenerator
**Track B: Hands 优化 (1人)**
- [ ] XState 状态机完善
- [ ] 审批流程配置化
- [ ] 错误恢复机制
**Track C: Intelligence 优化 (1人)**
- [ ] Rust 后端功能完善
- [ ] LRU 缓存集成
- [ ] 性能调优
### 6.4 Phase 4: 集成 + 清理 (2周 = 1周实施 + 1周缓冲)
**目标**: 完成集成,清理旧代码
**任务**:
- [ ] 跨领域集成测试
- [ ] E2E 测试完善
- [ ] 清理旧代码
- [ ] 更新文档
- [ ] 性能基准测试
**交付物**:
- 完整的测试套件
- 更新的文档
- 性能报告
---
## 7. 验收标准
### 7.1 功能验收
- [ ] 所有现有功能正常工作
- [ ] 无回归问题
- [ ] 新安全功能有效
### 7.2 性能验收
| 指标 | 当前 | 目标 | 测量方法 |
|------|------|------|----------|
| 首屏加载 | ~3s | <2s | Chrome DevTools Performance navigationStart firstContentfulPaint |
| 消息渲染 | 卡顿 | 60fps | React DevTools Profiler MessageList 组件渲染时间 |
| 1000+ 消息滚动 | 卡顿 | 流畅 | Chrome DevTools Performance 滚动时 frame rate |
| 内存占用 | 无界 | <500MB | Chrome Memory Profiler 堆快照对比 |
| Store 更新延迟 | ~50ms | <10ms | Performance.mark() 测量状态更新到渲染完成 |
### 7.3 安全验收
- [ ] localStorage 凭据已加密 (使用 AES-GCM)
- [ ] localhost 连接强制 WSS
- [ ] 依赖安全扫描通过 (npm audit)
- [ ] 密钥不在日志中输出
- [ ] WebSocket 安全测试通过
- [ ] 依赖安全扫描通过
### 7.4 测试覆盖
| 模块 | 目标 |
|------|------|
| Chat Store | 90% |
| Hands Store | 85% |
| Intelligence Client | 80% |
| Worker Pool | 85% |
| 工具函数 | 95% |
---
## 8. 风险与缓解
| 风险 | 概率 | 影响 | 缓解措施 |
|------|------|------|----------|
| Valtio 学习曲线 | | | 提前 POC编写示例 |
| XState 状态机复杂度 | | | 从简单场景开始逐步完善 |
| 迁移导致回归 | | | 每阶段充分测试 |
| 进度延期 | | | 预留 2 周缓冲 |
| 团队不熟悉 | | | 培训 + 文档 |
---
## 9. 附录
### 9.1 技术依赖
```json
{
"dependencies": {
"valtio": "1.11.2",
"xstate": "5.18.2",
"@xstate/react": "4.1.3",
"react-window": "1.8.10"
},
"devDependencies": {
"vitest": "2.1.8",
"@playwright/test": "1.49.1",
"@testing-library/react": "16.1.0"
}
}
```
### 9.2 参考文档
- [Valtio 文档](https://valtio.pmnd.rs/)
- [XState 文档](https://xstate.js.org/)
- [React Window 文档](https://react-window.vercel.app/)
- [Tauri 安全最佳实践](https://tauri.app/v2/guides/security/)
---
*文档版本: 1.1 | 最后更新: 2026-03-21*