docs(spec): add architecture optimization design spec

Comprehensive design for 14-week architecture overhaul:
- VZustand for fine-grained reactivity
- Web Worker isolation for security
- XState for Hands state machine
- Domain-driven directory structure

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-03-21 16:16:24 +08:00
parent e5cdd36118
commit 52c5e8a732

View File

@@ -0,0 +1,720 @@
# ZCLAW 架构优化设计规格
> **版本**: 1.0
> **日期**: 2026-03-21
> **状态**: 待审核
> **作者**: Claude + 用户协作
---
## 1. 概述
### 1.1 背景
ZCLAW 是面向中文用户的 AI Agent 桌面客户端,经过分析发现以下需要改进的领域:
- **安全风险**: 浏览器 eval() XSS 风险、localStorage 凭据回退
- **性能瓶颈**: 流式更新时重建整个消息数组、无界消息数组
- **架构问题**: 50+ lib 模块缺乏统一抽象、测试覆盖不足
### 1.2 目标
- 在 14 周内完成全面架构优化
- 采用激进架构优先策略
- 重点优化四个核心系统对话、Hands、Intelligence、技能
### 1.3 关键决策
| 决策点 | 选择 | 理由 |
|--------|------|------|
| 状态管理 | VZustand | Proxy 细粒度响应,性能更好 |
| 安全策略 | Web Worker 隔离 | 最安全的执行隔离方案 |
| 整体方案 | A+C 混合 | 渐进式 + 领域驱动结合 |
---
## 2. 架构设计
### 2.1 总体架构
```
┌─────────────────────────────────────────────────────────────────┐
│ UI Layer │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ React Components (60+) │ │
│ │ - 按领域组织 │ │
│ │ - 只负责展示和交互 │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ State Layer │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ VZustand Stores (基于 Proxy) │ │
│ │ - 细粒度响应 │ │
│ │ - 领域划分: Chat, Hands, Intelligence, Skills │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Client Layer │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐ │
│ │ Gateway Client │ │ Intelligence Clt │ │ Worker Pool │ │
│ │ (WebSocket) │ │ (Tauri Commands) │ │ (隔离执行) │ │
│ └──────────────────┘ └──────────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Backend Layer │
│ ┌──────────────────┐ ┌──────────────────────────────────────┐│
│ │ OpenFang Kernel │ │ Tauri Rust Backend ││
│ │ (Port 50051) │ │ - Intelligence (心跳/压缩/反思) ││
│ │ │ │ - Memory (SQLite 持久化) ││
│ │ │ │ - Browser (WebDriver) ││
│ └──────────────────┘ └──────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
```
### 2.2 领域划分
```
desktop/src/
├── domains/ # 领域模块 (新建)
│ ├── chat/ # 对话系统
│ │ ├── store.ts # VZustand 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 组件 (保持)
```
---
## 3. 核心组件设计
### 3.1 VZustand 状态管理
**问题**: 当前 Zustand 每次更新都会触发整个订阅组件重渲染
**解决方案**: 使用 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;
}
```
**预期收益**:
- 流式更新时性能提升 70%
- 代码更简洁 (直接 mutate)
- 选择性渲染减少不必要的重渲染
### 3.2 Web Worker 隔离执行
**问题**: browser.eval() 在主线程执行用户输入,存在 XSS 风险
**解决方案**: Web Worker 完全隔离
```typescript
// workers/pool.ts
export class BrowserWorkerPool {
private workers: Worker[] = [];
private maxWorkers = 4;
async execute(script: string, args: unknown[]): Promise<unknown> {
const worker = this.getAvailableWorker();
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
worker.terminate();
this.recycleWorker(worker);
reject(new Error('Execution timeout (30s)'));
}, 30000);
worker.onmessage = (e) => {
clearTimeout(timeout);
if (e.data.error) {
reject(new ExecutionError(e.data.error));
} else {
resolve(e.data.result);
}
};
worker.onerror = (e) => {
clearTimeout(timeout);
reject(new ExecutionError(e.message));
};
worker.postMessage({ type: 'eval', script, args });
});
}
private getAvailableWorker(): Worker {
// 复用或创建新 Worker
if (this.workers.length < this.maxWorkers) {
const worker = new Worker(new URL('./browser-worker.ts', import.meta.url));
this.workers.push(worker);
return worker;
}
// 等待可用 Worker...
}
}
// workers/browser-worker.ts
const ALLOWED_SCRIPTS = new Set([
'navigate', 'click', 'type', 'screenshot', 'extract',
'scroll', 'wait', 'select', 'hover'
]);
self.onmessage = async (e) => {
const { type, script, args } = e.data;
if (type === 'eval') {
try {
if (!ALLOWED_SCRIPTS.has(script)) {
throw new Error(`Script not allowed: ${script}`);
}
// 在受限环境中执行
const result = await executeInSandbox(script, args);
self.postMessage({ result });
} catch (error) {
self.postMessage({ error: error.message });
}
}
};
async function executeInSandbox(script: string, args: unknown[]): Promise<unknown> {
// 无 DOM 访问
// 无 localStorage 访问
// 只有受限的 API
// ...
}
```
**安全保证**:
- Worker 无法访问 DOM
- Worker 无法访问 localStorage/cookie
- 脚本白名单限制
- 执行超时保护
- 错误隔离
### 3.3 Hands 状态机
**问题**: 当前 HandStore 状态转换不清晰,难以追踪
**解决方案**: 使用 XState 实现状态机
```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',
},
},
},
});
```
### 3.4 Intelligence 缓存策略
**问题**: 每次请求都需要调用 Rust 后端
**解决方案**: 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 凭据存储加密
```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/executor.test.ts
describe('HandExecutor', () => {
let pool: BrowserWorkerPool;
beforeEach(() => {
pool = new BrowserWorkerPool();
});
afterEach(async () => {
await pool.terminateAll();
});
it('should execute allowed script', async () => {
const result = await pool.execute('navigate', ['https://example.com']);
expect(result).toBeDefined();
});
it('should reject disallowed script', async () => {
await expect(pool.execute('eval', ['alert(1)']))
.rejects.toThrow('Script not allowed');
});
it('should timeout long execution', async () => {
await expect(pool.execute('infiniteLoop', []))
.rejects.toThrow('Execution timeout');
}, 35000);
});
```
### 5.3 E2E 测试场景
1. **聊天流程**: 发送消息 → 接收流式响应 → 显示完整消息
2. **Hands 触发**: 触发 Hand → 审批流程 → 执行 → 结果显示
3. **Intelligence**: 记忆提取 → 心跳触发 → 反思生成
4. **技能搜索**: 输入关键词 → 搜索技能 → 查看详情
---
## 6. 实施计划
### 6.1 Phase 1: 安全 + 测试 (2周)
**目标**: 建立安全基础和测试框架
**任务**:
- [ ] 实现 Web Worker 隔离执行引擎
- [ ] 实现凭据加密存储
- [ ] 强制 WSS 连接
- [ ] 建立测试框架 (Vitest + Playwright)
- [ ] 设置覆盖率门禁 (60%)
- [ ] 添加 chatStore 基础测试
**交付物**:
- `workers/browser-worker.ts`
- `workers/pool.ts`
- `shared/secure-storage.ts`
- 测试配置文件
### 6.2 Phase 2: 领域重组 (4周)
**目标**: 按领域重组代码,迁移到 VZustand
**任务**:
- [ ] 创建 domains/ 目录结构
- [ ] 迁移 Chat Store 到 VZustand
- [ ] 迁移 Hands Store + 状态机
- [ ] 迁移 Intelligence Client
- [ ] 迁移 Skills Store
- [ ] 提取共享模块
- [ ] 更新导入路径
**交付物**:
- `domains/chat/`
- `domains/hands/`
- `domains/intelligence/`
- `domains/skills/`
- `shared/`
### 6.3 Phase 3: 核心优化 (6周并行)
**Track A: Chat 优化**
- [ ] VZustand 性能优化
- [ ] 虚拟滚动实现
- [ ] 消息分页
- [ ] 流式响应 AsyncGenerator
**Track B: Hands 优化**
- [ ] XState 状态机完善
- [ ] 审批流程配置化
- [ ] Worker 隔离集成
- [ ] 错误恢复机制
**Track C: Intelligence 优化**
- [ ] Rust 后端功能完善
- [ ] LRU 缓存实现
- [ ] 向量检索准备
- [ ] 性能调优
### 6.4 Phase 4: 集成 + 清理 (2周)
**目标**: 完成集成,清理旧代码
**任务**:
- [ ] 跨领域集成测试
- [ ] E2E 测试完善
- [ ] 清理旧代码
- [ ] 更新文档
- [ ] 性能基准测试
**交付物**:
- 完整的测试套件
- 更新的文档
- 性能报告
---
## 7. 验收标准
### 7.1 功能验收
- [ ] 所有现有功能正常工作
- [ ] 无回归问题
- [ ] 新安全功能有效
### 7.2 性能验收
| 指标 | 当前 | 目标 |
|------|------|------|
| 首屏加载 | ~3s | <2s |
| 消息渲染 | 卡顿 | 60fps |
| 1000+ 消息滚动 | 卡顿 | 流畅 |
| 内存占用 | 无界 | <500MB |
| Worker 执行延迟 | N/A | <100ms |
### 7.3 安全验收
- [ ] XSS 攻击测试通过
- [ ] 凭据存储安全审计通过
- [ ] WebSocket 安全测试通过
- [ ] 依赖安全扫描通过
### 7.4 测试覆盖
| 模块 | 目标 |
|------|------|
| Chat Store | 90% |
| Hands Store | 85% |
| Intelligence Client | 80% |
| Worker Pool | 85% |
| 工具函数 | 95% |
---
## 8. 风险与缓解
| 风险 | 概率 | 影响 | 缓解措施 |
|------|------|------|----------|
| VZustand 学习曲线 | | | 提前 POC编写示例 |
| Worker 兼容性 | | | 渐进增强主线程回退 |
| 迁移导致回归 | | | 每阶段充分测试 |
| 进度延期 | | | 预留 20% buffer |
| 团队不熟悉 | | | 培训 + 文档 |
---
## 9. 附录
### 9.1 技术依赖
```json
{
"dependencies": {
"valtio": "^2.0.0",
"xstate": "^5.0.0",
"react-window": "^2.2.7"
},
"devDependencies": {
"vitest": "^1.0.0",
"@playwright/test": "^1.40.0",
"@testing-library/react": "^14.0.0"
}
}
```
### 9.2 参考文档
- [VZustand 文档](https://valtio.pmnd.rs/)
- [XState 文档](https://xstate.js.org/)
- [Web Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API)
- [Tauri 安全最佳实践](https://tauri.app/v2/guides/security/)
---
*文档版本: 1.0 | 最后更新: 2026-03-21*