From a7ae0eca7a0e647c1fc209b15ee0f2b6771cf340 Mon Sep 17 00:00:00 2001 From: iven Date: Sun, 15 Mar 2026 20:20:10 +0800 Subject: [PATCH] feat(ui): improve CloneManager with error handling - Add loading state during clone creation - Add error display for failed operations - Add retry/refresh functionality - Improve gateway-client error handling Co-Authored-By: Claude Opus 4.6 --- desktop/src/components/CloneManager.tsx | 125 +++++++++++++++--------- desktop/src/lib/gateway-client.ts | 21 +++- 2 files changed, 100 insertions(+), 46 deletions(-) diff --git a/desktop/src/components/CloneManager.tsx b/desktop/src/components/CloneManager.tsx index e5dfa8d..53a30b3 100644 --- a/desktop/src/components/CloneManager.tsx +++ b/desktop/src/components/CloneManager.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { useGatewayStore } from '../store/gatewayStore'; import { toChatAgent, useChatStore } from '../store/chatStore'; -import { Bot, Plus, X, Globe, Cat, Search, BarChart2 } from 'lucide-react'; +import { Bot, Plus, X, Globe, Cat, Search, BarChart2, AlertCircle, RefreshCw } from 'lucide-react'; interface CloneFormData { name: string; @@ -42,10 +42,12 @@ function createFormFromDraft(quickConfig: { } export function CloneManager() { - const { clones, loadClones, createClone, deleteClone, connectionState, quickConfig, saveQuickConfig } = useGatewayStore(); + const { clones, loadClones, createClone, deleteClone, connectionState, quickConfig, saveQuickConfig, error: storeError } = useGatewayStore(); const { agents, currentAgent, setCurrentAgent } = useChatStore(); const [showForm, setShowForm] = useState(false); const [form, setForm] = useState(createFormFromDraft({})); + const [isCreating, setIsCreating] = useState(false); + const [createError, setCreateError] = useState(null); const connected = connectionState === 'connected'; @@ -63,47 +65,64 @@ export function CloneManager() { const handleCreate = async () => { if (!form.name.trim()) return; - const scenarios = form.scenarios - ? form.scenarios.split(',').map((s) => s.trim()).filter(Boolean) - : undefined; - await saveQuickConfig({ - agentName: form.name, - agentRole: form.role || undefined, - agentNickname: form.nickname || undefined, - scenarios, - workspaceDir: form.workspaceDir || undefined, - userName: form.userName || undefined, - userRole: form.userRole || undefined, - restrictFiles: form.restrictFiles, - privacyOptIn: form.privacyOptIn, - }); - const clone = await createClone({ - name: form.name, - role: form.role || undefined, - nickname: form.nickname || undefined, - scenarios, - workspaceDir: form.workspaceDir || undefined, - userName: form.userName || undefined, - userRole: form.userRole || undefined, - restrictFiles: form.restrictFiles, - privacyOptIn: form.privacyOptIn, - }); - if (clone) { - setCurrentAgent(toChatAgent(clone)); + setCreateError(null); + setIsCreating(true); + + try { + const scenarios = form.scenarios + ? form.scenarios.split(',').map((s) => s.trim()).filter(Boolean) + : undefined; + + await saveQuickConfig({ + agentName: form.name, + agentRole: form.role || undefined, + agentNickname: form.nickname || undefined, + scenarios, + workspaceDir: form.workspaceDir || undefined, + userName: form.userName || undefined, + userRole: form.userRole || undefined, + restrictFiles: form.restrictFiles, + privacyOptIn: form.privacyOptIn, + }); + + const clone = await createClone({ + name: form.name, + role: form.role || undefined, + nickname: form.nickname || undefined, + scenarios, + workspaceDir: form.workspaceDir || undefined, + userName: form.userName || undefined, + userRole: form.userRole || undefined, + restrictFiles: form.restrictFiles, + privacyOptIn: form.privacyOptIn, + }); + + if (clone) { + setCurrentAgent(toChatAgent(clone)); + setForm(createFormFromDraft({ + ...quickConfig, + agentName: form.name, + agentRole: form.role, + agentNickname: form.nickname, + scenarios, + workspaceDir: form.workspaceDir, + userName: form.userName, + userRole: form.userRole, + restrictFiles: form.restrictFiles, + privacyOptIn: form.privacyOptIn, + })); + setShowForm(false); + } else { + // Show error from store or generic message + const errorMsg = storeError || '创建分身失败。请检查 Gateway 连接状态和后端日志。'; + setCreateError(errorMsg); + } + } catch (err: unknown) { + const errorMsg = err instanceof Error ? err.message : String(err); + setCreateError(`创建失败: ${errorMsg}`); + } finally { + setIsCreating(false); } - setForm(createFormFromDraft({ - ...quickConfig, - agentName: form.name, - agentRole: form.role, - agentNickname: form.nickname, - scenarios, - workspaceDir: form.workspaceDir, - userName: form.userName, - userRole: form.userRole, - restrictFiles: form.restrictFiles, - privacyOptIn: form.privacyOptIn, - })); - setShowForm(false); }; const handleDelete = async (id: string) => { @@ -222,12 +241,28 @@ export function CloneManager() { onChange={e => setForm({ ...form, privacyOptIn: e.target.checked })} /> + {createError && ( +
+ + {createError} + +
+ )} )} diff --git a/desktop/src/lib/gateway-client.ts b/desktop/src/lib/gateway-client.ts index 2fd7cbc..1ad24fa 100644 --- a/desktop/src/lib/gateway-client.ts +++ b/desktop/src/lib/gateway-client.ts @@ -439,8 +439,10 @@ export class GatewayClient { // Check if OpenFang API is healthy const health = await this.restGet<{ status: string; version?: string }>('/api/health'); if (health.status === 'ok') { + this.reconnectAttempts = 0; this.setState('connected'); this.log('info', `Connected to OpenFang via REST API${health.version ? ` (v${health.version})` : ''}`); + this.emitEvent('connected', { version: health.version }); } else { throw new Error('Health check failed'); } @@ -1588,15 +1590,32 @@ export class GatewayClient { this.setState('disconnected'); } + private static readonly MAX_RECONNECT_ATTEMPTS = 10; + private scheduleReconnect() { + if (this.reconnectAttempts >= GatewayClient.MAX_RECONNECT_ATTEMPTS) { + this.log('error', `Max reconnect attempts (${GatewayClient.MAX_RECONNECT_ATTEMPTS}) reached. Please reconnect manually.`); + this.setState('disconnected'); + this.emitEvent('reconnect_failed', { + attempts: this.reconnectAttempts, + maxAttempts: GatewayClient.MAX_RECONNECT_ATTEMPTS + }); + return; + } + this.reconnectAttempts++; this.setState('reconnecting'); const delay = Math.min(this.reconnectInterval * Math.pow(1.5, this.reconnectAttempts - 1), 30000); + this.log('info', `Scheduling reconnect attempt ${this.reconnectAttempts} in ${delay}ms`); + this.reconnectTimer = window.setTimeout(async () => { try { await this.connect(); - } catch { /* close handler will trigger another reconnect */ } + } catch { + /* close handler will trigger another reconnect */ + this.log('warn', `Reconnect attempt ${this.reconnectAttempts} failed`); + } }, delay); }