diff --git a/docs/superpowers/specs/2026-03-21-architecture-optimization-design.md b/docs/superpowers/specs/2026-03-21-architecture-optimization-design.md new file mode 100644 index 0000000..d528b13 --- /dev/null +++ b/docs/superpowers/specs/2026-03-21-architecture-optimization-design.md @@ -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) => void; + setStreaming: (streaming: boolean) => void; +} + +export const chatState = proxy({ + 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 => ); +} + +function StreamingIndicator() { + const { isStreaming } = useChatState(); // 只订阅 isStreaming + return isStreaming ? : 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 { + 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 { + // 无 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 { + value: T; + expiresAt: number; +} + +export class IntelligenceCache { + private cache = new Map>(); + private maxSize = 100; + private defaultTTL = 5 * 60 * 1000; // 5 minutes + + get(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(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 { + // 从 OS keyring 获取或派生 + const masterKey = await this.getMasterKey(); + return deriveKey(masterKey, SALT, { + name: 'AES-GCM', + length: 256, + }); + } + + async store(key: string, value: string): Promise { + 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 { + 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*