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 <noreply@anthropic.com>
This commit is contained in:
iven
2026-03-15 20:20:10 +08:00
parent 01737a7ef5
commit a7ae0eca7a
2 changed files with 100 additions and 46 deletions

View File

@@ -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<CloneFormData>(createFormFromDraft({}));
const [isCreating, setIsCreating] = useState(false);
const [createError, setCreateError] = useState<string | null>(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 })}
/>
</label>
{createError && (
<div className="flex items-center gap-2 text-xs text-red-600 bg-red-50 px-2 py-1.5 rounded">
<AlertCircle className="w-3.5 h-3.5 flex-shrink-0" />
<span className="flex-1">{createError}</span>
<button onClick={() => setCreateError(null)} className="hover:text-red-800">
<X className="w-3 h-3" />
</button>
</div>
)}
<button
onClick={handleCreate}
disabled={!form.name.trim()}
className="w-full text-xs bg-gray-900 text-white rounded py-1.5 hover:bg-gray-800 disabled:opacity-50"
disabled={!form.name.trim() || isCreating}
className="w-full text-xs bg-gray-900 text-white rounded py-1.5 hover:bg-gray-800 disabled:opacity-50 flex items-center justify-center gap-2"
>
{isCreating ? (
<>
<RefreshCw className="w-3 h-3 animate-spin" />
...
</>
) : (
'完成配置'
)}
</button>
</div>
)}

View File

@@ -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);
}