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>
This commit is contained in:
401
docs/knowledge-base/frontend-integration.md
Normal file
401
docs/knowledge-base/frontend-integration.md
Normal file
@@ -0,0 +1,401 @@
|
||||
# 前端集成模式
|
||||
|
||||
> 记录 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 核心模式
|
||||
|
||||
```typescript
|
||||
// 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 核心模式
|
||||
|
||||
```typescript
|
||||
// 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 单例模式
|
||||
|
||||
```typescript
|
||||
// gateway-client.ts
|
||||
let instance: GatewayClient | null = null;
|
||||
|
||||
export function getGatewayClient(): GatewayClient {
|
||||
if (!instance) {
|
||||
instance = new GatewayClient();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 流式聊天实现
|
||||
|
||||
```typescript
|
||||
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 回调类型定义
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
// 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 流式消息渲染
|
||||
|
||||
```typescript
|
||||
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 错误边界
|
||||
|
||||
```typescript
|
||||
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 开发代理
|
||||
|
||||
```typescript
|
||||
// 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 动态后端切换
|
||||
|
||||
```typescript
|
||||
// 根据后端类型切换代理目标
|
||||
const BACKEND_PORTS = {
|
||||
openclaw: 18789,
|
||||
openfang: 50051,
|
||||
};
|
||||
|
||||
const backendType = localStorage.getItem('zclaw-backend') || 'openfang';
|
||||
const targetPort = BACKEND_PORTS[backendType];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 持久化
|
||||
|
||||
### 6.1 Zustand Persist
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
// ❌ 错误 - 在组件中直接创建 WebSocket
|
||||
function ChatArea() {
|
||||
const ws = new WebSocket(url); // 不要这样做
|
||||
}
|
||||
|
||||
// ✅ 正确 - 通过 GatewayClient
|
||||
function ChatArea() {
|
||||
const sendMessage = useChatStore((state) => state.sendMessage);
|
||||
// sendMessage 内部使用 GatewayClient
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 处理连接状态
|
||||
|
||||
```typescript
|
||||
// 显示连接状态给用户
|
||||
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 优雅降级
|
||||
|
||||
```typescript
|
||||
// 流式失败时降级到 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 | 初始版本 |
|
||||
Reference in New Issue
Block a user