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>
10 KiB
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 | 初始版本 |