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