Files
zclaw_openfang/docs/superpowers/specs/2026-03-21-architecture-optimization-design.md
iven 52c5e8a732 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>
2026-03-21 16:16:24 +08:00

20 KiB
Raw Blame History

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 实现细粒度响应

// 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 完全隔离

// 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 实现状态机

// 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

// 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 凭据存储加密

// 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 安全

// 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 单元测试

// 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 集成测试

// 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 技术依赖

{
  "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 参考文档


文档版本: 1.0 | 最后更新: 2026-03-21