diff --git a/desktop/src/components/HandTaskPanel.tsx b/desktop/src/components/HandTaskPanel.tsx index ff370bc..8f9b06f 100644 --- a/desktop/src/components/HandTaskPanel.tsx +++ b/desktop/src/components/HandTaskPanel.tsx @@ -57,19 +57,19 @@ export function HandTaskPanel({ handId, onBack }: HandTaskPanelProps) { // Load task history when hand is selected useEffect(() => { if (selectedHand) { - loadHandRuns(selectedHand.name, { limit: 50 }); + loadHandRuns(selectedHand.id, { limit: 50 }); } }, [selectedHand, loadHandRuns]); // Get runs for this hand from store - const tasks: HandRun[] = selectedHand ? (handRuns[selectedHand.name] || []) : []; + const tasks: HandRun[] = selectedHand ? (handRuns[selectedHand.id] || []) : []; // Refresh task history const handleRefresh = useCallback(async () => { if (!selectedHand) return; setIsRefreshing(true); try { - await loadHandRuns(selectedHand.name, { limit: 50 }); + await loadHandRuns(selectedHand.id, { limit: 50 }); } finally { setIsRefreshing(false); } @@ -80,11 +80,11 @@ export function HandTaskPanel({ handId, onBack }: HandTaskPanelProps) { if (!selectedHand) return; setIsActivating(true); try { - await triggerHand(selectedHand.name); + await triggerHand(selectedHand.id); // Refresh hands list and task history await Promise.all([ loadHands(), - loadHandRuns(selectedHand.name, { limit: 50 }), + loadHandRuns(selectedHand.id, { limit: 50 }), ]); } catch { // Error is handled in store diff --git a/desktop/src/components/HandsPanel.tsx b/desktop/src/components/HandsPanel.tsx index da917a1..bfc1ee7 100644 --- a/desktop/src/components/HandsPanel.tsx +++ b/desktop/src/components/HandsPanel.tsx @@ -367,7 +367,7 @@ export function HandsPanel() { const handleDetails = useCallback(async (hand: Hand) => { // Load full details before showing modal const { getHandDetails } = useGatewayStore.getState(); - const details = await getHandDetails(hand.name); + const details = await getHandDetails(hand.id); setSelectedHand(details || hand); setShowModal(true); }, []); @@ -375,7 +375,7 @@ export function HandsPanel() { const handleActivate = useCallback(async (hand: Hand) => { setActivatingHandId(hand.id); try { - await triggerHand(hand.name); + await triggerHand(hand.id); // Refresh hands after activation await loadHands(); } catch { diff --git a/desktop/src/components/WorkflowEditor.tsx b/desktop/src/components/WorkflowEditor.tsx index 4a8fff2..3b3ab3e 100644 --- a/desktop/src/components/WorkflowEditor.tsx +++ b/desktop/src/components/WorkflowEditor.tsx @@ -84,7 +84,7 @@ function StepEditor({ step, hands, index, onUpdate, onRemove, onMoveUp, onMoveDo > {hands.map(hand => ( - ))} diff --git a/plans/virtual-conjuring-stardust.md b/plans/virtual-conjuring-stardust.md new file mode 100644 index 0000000..f2939a1 --- /dev/null +++ b/plans/virtual-conjuring-stardust.md @@ -0,0 +1,209 @@ +# ZCLAW Gateway 连接稳定性与 API 修复计划 + +## Context + +**问题背景**: 用户报告"Gateway 连接不稳定",通过 Chrome DevTools 诊断发现: +- WebSocket 连接和聊天功能**实际正常工作** +- 真正的问题是 **6 个 API 端点返回 404**,大量错误日志被误认为是连接问题 + +**诊断结果**: +| 项目 | 状态 | +|------|------| +| WebSocket 连接 | ✅ 正常 | +| 消息发送/流式响应 | ✅ 正常 | +| 核心 API (agents, hands, workflows) | ✅ 正常 | +| 6 个 API 端点 | ❌ 404 错误 | + +**目标**: +1. P0: 修复 6 个 404 API 端点(通过前端 fallback 降级) +2. P1: 增强连接稳定性(心跳机制 + 改进重连策略) + +--- + +## Phase 1: P0 - API Fallback 降级处理 + +### 1.1 创建 API Fallback 模块 + +**新建文件**: `desktop/src/lib/api-fallbacks.ts` + +```typescript +// 提供 6 个 404 API 的降级数据 +export interface QuickConfigFallback { ... } +export interface WorkspaceInfoFallback { ... } +export interface UsageStatsFallback { ... } +export interface PluginStatusFallback { ... } +export interface ScheduledTaskFallback { ... } + +export function getQuickConfigFallback(): QuickConfigFallback { ... } +export function getWorkspaceInfoFallback(): WorkspaceInfoFallback { ... } +export function getUsageStatsFallback(sessions: Session[]): UsageStatsFallback { ... } +export function getPluginStatusFallback(skills: SkillInfo[]): PluginStatusFallback { ... } +export function getScheduledTasksFallback(triggers: Trigger[]): ScheduledTaskFallback[] { ... } +``` + +### 1.2 更新 gateway-client.ts + +为每个 404 API 添加结构化 fallback: + +```typescript +async getQuickConfig(): Promise { + try { + return await this.restGet('/api/config/quick'); + } catch (error) { + if ((error as any).status === 404) { + return { quickConfig: getQuickConfigFallback() }; + } + throw error; + } +} +``` + +### 1.3 更新 gatewayStore.ts + +在数据加载时使用 fallback: + +```typescript +loadUsageStats: async () => { + try { + const stats = await get().client.getUsageStats(); + set({ usageStats: stats }); + } catch { + const fallback = getUsageStatsFallback(get().sessions); + set({ usageStats: fallback }); + } +}, +``` + +--- + +## Phase 2: P1 - 心跳机制 + +### 2.1 添加心跳字段 + +**修改文件**: `desktop/src/lib/gateway-client.ts` + +```typescript +// 新增私有字段 +private heartbeatInterval: number | null = null; +private heartbeatTimeout: number | null = null; +private missedHeartbeats: number = 0; +private static readonly HEARTBEAT_INTERVAL = 30000; // 30秒 +private static readonly HEARTBEAT_TIMEOUT = 10000; // 10秒 +private static readonly MAX_MISSED_HEARTBEATS = 3; +``` + +### 2.2 心跳方法 + +```typescript +private startHeartbeat(): void { ... } +private stopHeartbeat(): void { ... } +private sendHeartbeat(): void { ... } +private handlePong(): void { ... } +``` + +### 2.3 集成到连接流程 + +- `connect()` 成功后调用 `startHeartbeat()` +- `cleanup()` 时调用 `stopHeartbeat()` +- `handleFrame()` 处理 `pong` 响应 + +--- + +## Phase 3: P1 - 改进重连策略 + +### 3.1 新增配置选项 + +```typescript +interface GatewayClientOptions { + maxReconnectAttempts?: number; // -1=无限, 0=禁用, 默认10 + reconnectBackoff?: 'linear' | 'exponential' | 'fixed'; +} +``` + +### 3.2 更新 scheduleReconnect + +- 支持无限重连模式 (`maxReconnectAttempts: -1`) +- 添加重连事件通知 (`reconnecting`, `reconnect_failed`) +- 改进退避算法 + +### 3.3 流式 WebSocket 重连 + +为 `openfangWs` 添加重连逻辑: + +```typescript +private streamState = { agentId: null, sessionId: null, lastMessage: null }; +private scheduleStreamReconnect(runId: string): void { ... } +``` + +--- + +## Phase 4: UI 集成 + +### 4.1 创建 ConnectionStatus 组件 + +**新建文件**: `desktop/src/components/ConnectionStatus.tsx` + +```typescript +export function ConnectionStatus() { + const { connectionState } = useGatewayStore(); + + const statusConfig = { + disconnected: { color: 'red', label: '已断开', icon: WifiOff }, + connecting: { color: 'yellow', label: '连接中...', icon: Loader2 }, + connected: { color: 'green', label: '已连接', icon: Wifi }, + reconnecting: { color: 'orange', label: '重连中...', icon: RefreshCw }, + }; + // ... +} +``` + +### 4.2 集成到 ChatArea + +在聊天区域顶部显示连接状态指示器。 + +--- + +## 关键文件 + +| 文件 | 修改类型 | 说明 | +|------|----------|------| +| `desktop/src/lib/api-fallbacks.ts` | 新建 | API 降级数据 | +| `desktop/src/lib/gateway-client.ts` | 修改 | 心跳 + 重连 + fallback | +| `desktop/src/store/gatewayStore.ts` | 修改 | 使用 fallback | +| `desktop/src/components/ConnectionStatus.tsx` | 新建 | 连接状态 UI | +| `desktop/src/components/ChatArea.tsx` | 修改 | 集成状态指示器 | +| `tests/desktop/gatewayStore.test.ts` | 修改 | 测试覆盖 | + +--- + +## Verification + +### 测试清单 + +**P0 - API Fallback**: +- [ ] 启动应用,检查控制台无 404 错误 +- [ ] 用量统计面板显示数据(即使 API 404) +- [ ] 设置页面显示配置(即使 API 404) + +**P1 - 心跳**: +- [ ] 连接后空闲 5 分钟,连接保持 +- [ ] 检查 WebSocket 流量有 ping/pong 帧 + +**P1 - 重连**: +- [ ] 关闭 OpenFang,显示"重连中" +- [ ] 重启 OpenFang,自动重连成功 +- [ ] 聊天中断开后自动恢复 + +### 运行测试 + +```bash +pnpm vitest run tests/desktop/gatewayStore.test.ts +``` + +--- + +## Implementation Sequence + +1. **Day 1**: Phase 1 - API Fallbacks +2. **Day 2**: Phase 2 - Heartbeat + Phase 3 - Reconnection +3. **Day 3**: Phase 4 - UI Integration + Testing