Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
重构所有代码和文档中的项目名称,将OpenFang统一更新为ZCLAW。包括: - 配置文件中的项目名称 - 代码注释和文档引用 - 环境变量和路径 - 类型定义和接口名称 - 测试用例和模拟数据 同时优化部分代码结构,移除未使用的模块,并更新相关依赖项。
402 lines
10 KiB
Markdown
402 lines
10 KiB
Markdown
# 前端集成模式
|
|
|
|
> 记录 ZCLAW Desktop 前端与 ZCLAW 后端的集成模式和最佳实践。
|
|
|
|
---
|
|
|
|
## 1. 架构概览
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ React UI │
|
|
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
|
│ │ ChatArea│ │ Sidebar │ │Settings │ │ Panels │ │
|
|
│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │
|
|
│ │ │ │ │ │
|
|
│ └────────────┴────────────┴────────────┘ │
|
|
│ │ │
|
|
│ Zustand Store │
|
|
│ ┌──────────────┼──────────────┐ │
|
|
│ │ │ │ │
|
|
│ chatStore gatewayStore settingsStore │
|
|
│ │ │ │ │
|
|
└───────────┼──────────────┼──────────────┼──────────────┘
|
|
│ │ │
|
|
└──────────────┼──────────────┘
|
|
│
|
|
GatewayClient
|
|
┌─────────┴─────────┐
|
|
│ │
|
|
WebSocket (流式) REST API (非流式)
|
|
│ │
|
|
└─────────┬─────────┘
|
|
│
|
|
ZCLAW 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.connectZCLAWStream(agentId, runId, options?.sessionKey, message);
|
|
|
|
return { runId };
|
|
}
|
|
|
|
private handleZCLAWStreamEvent(runId: string, event: unknown) {
|
|
const callbacks = this.streamCallbacks.get(runId);
|
|
if (!callbacks) return;
|
|
|
|
const e = event as ZCLAWEvent;
|
|
|
|
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 = {
|
|
zclaw: 18789,
|
|
zclaw: 50051,
|
|
};
|
|
|
|
const backendType = localStorage.getItem('zclaw-backend') || 'zclaw';
|
|
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 | 初始版本 |
|