Files
zclaw_openfang/docs/knowledge-base/frontend-integration.md
iven 07079293f4 feat(hands): restructure Hands UI with Chinese localization
Major changes:
- Add HandList.tsx component for left sidebar
- Add HandTaskPanel.tsx for middle content area
- Restructure Sidebar tabs: 分身/HANDS/Workflow
- Remove Hands tab from RightPanel
- Localize all UI text to Chinese
- Archive legacy OpenClaw documentation
- Add Hands integration lessons document
- Update feature checklist with new components

UI improvements:
- Left sidebar now shows Hands list with status icons
- Middle area shows selected Hand's tasks and results
- Consistent styling with Tailwind CSS
- Chinese status labels and buttons

Documentation:
- Create docs/archive/openclaw-legacy/ for old docs
- Add docs/knowledge-base/hands-integration-lessons.md
- Update docs/knowledge-base/feature-checklist.md
- Update docs/knowledge-base/README.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 23:16:32 +08:00

10 KiB

前端集成模式

记录 ZCLAW Desktop 前端与 OpenFang 后端的集成模式和最佳实践。


1. 架构概览

┌─────────────────────────────────────────────────────────┐
│                     React UI                            │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐    │
│  │ ChatArea│  │ Sidebar │  │Settings │  │ Panels  │    │
│  └────┬────┘  └────┬────┘  └────┬────┘  └────┬────┘    │
│       │            │            │            │          │
│       └────────────┴────────────┴────────────┘          │
│                          │                              │
│                    Zustand Store                        │
│           ┌──────────────┼──────────────┐              │
│           │              │              │              │
│      chatStore    gatewayStore    settingsStore        │
│           │              │              │              │
└───────────┼──────────────┼──────────────┼──────────────┘
            │              │              │
            └──────────────┼──────────────┘
                           │
                  GatewayClient
                  ┌─────────┴─────────┐
                  │                   │
            WebSocket (流式)    REST API (非流式)
                  │                   │
                  └─────────┬─────────┘
                            │
                      OpenFang Backend
                      (port 50051)

2. 状态管理

2.1 Store 分层

Store 职责 关键状态
chatStore 聊天消息、对话、流式状态 messages, conversations, isStreaming
gatewayStore Gateway 连接、Agent、Hands connectionState, clones, hands
settingsStore 用户设置、主题 backendType, theme

2.2 chatStore 核心模式

// chatStore.ts
interface ChatState {
  // 状态
  messages: Message[];
  conversations: Conversation[];
  isStreaming: boolean;

  // 操作
  addMessage: (message: Message) => void;
  updateMessage: (id: string, updates: Partial<Message>) => void;
  sendMessage: (content: string) => Promise<void>;
}

// sendMessage 实现
sendMessage: async (content: string) => {
  // 1. 添加用户消息
  addMessage({ id: `user_${Date.now()}`, role: 'user', content });

  // 2. 创建助手消息占位符
  const assistantId = `assistant_${Date.now()}`;
  addMessage({ id: assistantId, role: 'assistant', content: '', streaming: true });
  set({ isStreaming: true });

  try {
    // 3. 优先使用流式 API
    if (client.getState() === 'connected') {
      await client.chatStream(content, {
        onDelta: (delta) => {
          // 累积更新内容
          updateMessage(assistantId, {
            content: /* 当前内容 + delta */
          });
        },
        onComplete: () => {
          updateMessage(assistantId, { streaming: false });
          set({ isStreaming: false });
        },
        onError: (error) => {
          updateMessage(assistantId, { content: `⚠️ ${error}`, error });
        },
      });
    } else {
      // 4. Fallback 到 REST API
      const result = await client.chat(content);
      updateMessage(assistantId, { content: result.response, streaming: false });
    }
  } catch (err) {
    // 5. 错误处理
    updateMessage(assistantId, { content: `⚠️ ${err.message}`, error: err.message });
  }
}

2.3 gatewayStore 核心模式

// gatewayStore.ts
interface GatewayState {
  connectionState: 'disconnected' | 'connecting' | 'connected';
  clones: AgentProfile[];
  hands: Hand[];

  connect: () => Promise<void>;
  loadClones: () => Promise<void>;
  loadHands: () => Promise<void>;
}

// 连接流程
connect: async () => {
  const client = getGatewayClient();
  set({ connectionState: 'connecting' });

  try {
    await client.connect();
    set({ connectionState: 'connected' });

    // 自动加载数据
    await get().loadClones();
    await get().loadHands();
  } catch (err) {
    set({ connectionState: 'disconnected' });
    throw err;
  }
}

3. GatewayClient 模式

3.1 单例模式

// gateway-client.ts
let instance: GatewayClient | null = null;

export function getGatewayClient(): GatewayClient {
  if (!instance) {
    instance = new GatewayClient();
  }
  return instance;
}

3.2 流式聊天实现

class GatewayClient {
  private streamCallbacks = new Map<string, StreamCallbacks>();

  async chatStream(
    message: string,
    callbacks: StreamCallbacks,
    options?: { sessionKey?: string; agentId?: string }
  ): Promise<{ runId: string }> {
    const runId = generateRunId();
    const agentId = options?.agentId || this.defaultAgentId;

    // 存储回调
    this.streamCallbacks.set(runId, callbacks);

    // 连接 WebSocket
    const ws = this.connectOpenFangStream(agentId, runId, options?.sessionKey, message);

    return { runId };
  }

  private handleOpenFangStreamEvent(runId: string, event: unknown) {
    const callbacks = this.streamCallbacks.get(runId);
    if (!callbacks) return;

    const e = event as OpenFangEvent;

    switch (e.type) {
      case 'text_delta':
        callbacks.onDelta(e.content || '');
        break;
      case 'response':
        callbacks.onComplete();
        this.streamCallbacks.delete(runId);
        break;
      case 'error':
        callbacks.onError(e.content || 'Unknown error');
        this.streamCallbacks.delete(runId);
        break;
    }
  }
}

3.3 回调类型定义

interface StreamCallbacks {
  onDelta: (delta: string) => void;
  onTool?: (tool: string, input: string, output: string) => void;
  onHand?: (name: string, status: string, result?: unknown) => void;
  onComplete: () => void;
  onError: (error: string) => void;
}

4. 组件模式

4.1 使用 Store

// ChatArea.tsx
function ChatArea() {
  // 使用 selector 优化性能
  const messages = useChatStore((state) => state.messages);
  const isStreaming = useChatStore((state) => state.isStreaming);
  const sendMessage = useChatStore((state) => state.sendMessage);

  // ...
}

4.2 流式消息渲染

function MessageBubble({ message }: { message: Message }) {
  return (
    <div className={cn(
      "message-bubble",
      message.streaming && "animate-pulse",
      message.error && "text-red-500"
    )}>
      {message.content}
      {message.streaming && (
        <span className="cursor-blink"></span>
      )}
    </div>
  );
}

4.3 错误边界

class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError(error: Error) {
    return { hasError: true };
  }

  componentDidCatch(error: Error, info: React.ErrorInfo) {
    console.error('Component error:', error, info);
  }

  render() {
    if (this.state.hasError) {
      return <ErrorFallback onRetry={() => this.setState({ hasError: false })} />;
    }
    return this.props.children;
  }
}

5. 代理配置

5.1 Vite 开发代理

// vite.config.ts
export default defineConfig({
  server: {
    port: 1420,
    proxy: {
      '/api': {
        target: 'http://127.0.0.1:50051',
        changeOrigin: true,
        secure: false,
        ws: true,  // WebSocket 支持
      },
    },
  },
});

5.2 动态后端切换

// 根据后端类型切换代理目标
const BACKEND_PORTS = {
  openclaw: 18789,
  openfang: 50051,
};

const backendType = localStorage.getItem('zclaw-backend') || 'openfang';
const targetPort = BACKEND_PORTS[backendType];

6. 持久化

6.1 Zustand Persist

export const useChatStore = create<ChatState>()(
  persist(
    (set, get) => ({
      // ... state and actions
    }),
    {
      name: 'zclaw-chat-storage',
      partialize: (state) => ({
        conversations: state.conversations,
        currentModel: state.currentModel,
      }),
      onRehydrateStorage: () => (state) => {
        // 重建 Date 对象
        if (state?.conversations) {
          for (const conv of state.conversations) {
            conv.createdAt = new Date(conv.createdAt);
            conv.updatedAt = new Date(conv.updatedAt);
          }
        }
      },
    }
  )
);

7. 最佳实践

7.1 不要直接调用 WebSocket

// ❌ 错误 - 在组件中直接创建 WebSocket
function ChatArea() {
  const ws = new WebSocket(url);  // 不要这样做
}

// ✅ 正确 - 通过 GatewayClient
function ChatArea() {
  const sendMessage = useChatStore((state) => state.sendMessage);
  // sendMessage 内部使用 GatewayClient
}

7.2 处理连接状态

// 显示连接状态给用户
function ConnectionStatus() {
  const state = useGatewayStore((state) => state.connectionState);

  return (
    <div className={cn(
      "status-indicator",
      state === 'connected' && "bg-green-500",
      state === 'connecting' && "bg-yellow-500",
      state === 'disconnected' && "bg-red-500"
    )}>
      {state}
    </div>
  );
}

7.3 优雅降级

// 流式失败时降级到 REST
try {
  await client.chatStream(message, callbacks);
} catch (streamError) {
  console.warn('Stream failed, falling back to REST:', streamError);
  const result = await client.chat(message);
  // 处理 REST 响应
}

更新历史

日期 变更
2026-03-14 初始版本