/** * API Fallbacks for ZCLAW Gateway * * Provides sensible default data when ZCLAW API endpoints return 404. * This allows the UI to function gracefully even when backend features * are not yet implemented. */ // === Types === export interface QuickConfigFallback { agentName: string; agentRole: string; userName: string; userRole: string; agentNickname?: string; scenarios?: string[]; workspaceDir?: string; gatewayUrl?: string; gatewayToken?: string; skillsExtraDirs?: string[]; mcpServices?: Array<{ id: string; name: string; enabled: boolean }>; theme: 'light' | 'dark'; autoStart?: boolean; showToolCalls: boolean; restrictFiles?: boolean; autoSaveContext?: boolean; fileWatching?: boolean; privacyOptIn?: boolean; } export interface WorkspaceInfoFallback { path: string; resolvedPath: string; exists: boolean; fileCount: number; totalSize: number; } export interface UsageStatsFallback { totalSessions: number; totalMessages: number; totalTokens: number; byModel: Record; } export interface PluginStatusFallback { id: string; name?: string; status: 'active' | 'inactive' | 'error' | 'loading'; version?: string; description?: string; } export interface ScheduledTaskFallback { id: string; name: string; schedule: string; status: 'active' | 'paused' | 'completed' | 'error'; lastRun?: string; nextRun?: string; description?: string; } export interface SecurityLayerFallback { name: string; enabled: boolean; description?: string; } export interface SecurityStatusFallback { _isFallback?: true; layers: SecurityLayerFallback[]; enabledCount: number; totalCount: number; securityLevel: 'critical' | 'high' | 'medium' | 'low'; } // Session type for usage calculation interface SessionForStats { id: string; messageCount?: number; metadata?: { tokens?: { input?: number; output?: number }; model?: string; }; } // Skill type for plugin fallback interface SkillForPlugins { id: string; name: string; source: 'builtin' | 'extra'; enabled?: boolean; description?: string; } // Trigger type for scheduled tasks interface TriggerForTasks { id: string; type: string; enabled: boolean; } // === Fallback Implementations === /** * Default quick config when /api/config/quick returns 404. * Uses sensible defaults for a new user experience. */ export function getQuickConfigFallback(): QuickConfigFallback & { _isFallback: true } { console.warn('[fallback] 使用降级数据: getQuickConfigFallback'); return { _isFallback: true as const, agentName: '默认助手', agentRole: 'AI 助手', userName: '用户', userRole: '用户', agentNickname: 'ZCLAW', scenarios: ['通用对话', '代码助手', '文档编写'], theme: 'dark', showToolCalls: true, autoSaveContext: true, fileWatching: true, privacyOptIn: false, }; } /** * Default workspace info when /api/workspace returns 404. * Returns a placeholder indicating workspace is not configured. */ export function getWorkspaceInfoFallback(): WorkspaceInfoFallback & { _isFallback: true } { console.warn('[fallback] 使用降级数据: getWorkspaceInfoFallback'); // Try to get a reasonable default path const defaultPath = typeof window !== 'undefined' ? `${navigator.userAgent.includes('Windows') ? 'C:\\Users' : '/home'}/workspace` : '/workspace'; return { _isFallback: true as const, path: defaultPath, resolvedPath: defaultPath, exists: false, fileCount: 0, totalSize: 0, }; } /** * Calculate usage stats from session data when /api/stats/usage returns 404. */ export function getUsageStatsFallback(sessions: SessionForStats[] = []): UsageStatsFallback & { _isFallback: true } { console.warn('[fallback] 使用降级数据: getUsageStatsFallback — 基于本地 session 数据计算'); const stats: UsageStatsFallback = { totalSessions: sessions.length, totalMessages: 0, totalTokens: 0, byModel: {}, }; for (const session of sessions) { stats.totalMessages += session.messageCount || 0; if (session.metadata?.tokens) { const input = session.metadata.tokens.input || 0; const output = session.metadata.tokens.output || 0; stats.totalTokens += input + output; if (session.metadata.model) { const model = session.metadata.model; if (!stats.byModel[model]) { stats.byModel[model] = { messages: 0, inputTokens: 0, outputTokens: 0 }; } stats.byModel[model].messages += session.messageCount || 0; stats.byModel[model].inputTokens += input; stats.byModel[model].outputTokens += output; } } } return { ...stats, _isFallback: true as const }; } /** * Convert skills to plugin status when /api/plugins/status returns 404. * ZCLAW uses Skills instead of traditional plugins. */ export function getPluginStatusFallback(skills: SkillForPlugins[] = []): Array { console.warn('[fallback] 使用降级数据: getPluginStatusFallback — 从 Skills 列表推断'); if (skills.length === 0) { // No skills loaded — return empty rather than fabricating fake builtins return []; } return skills.map((skill) => ({ id: skill.id, name: skill.name, status: skill.enabled !== false ? 'active' : 'inactive', description: skill.description, })); } /** * Convert triggers to scheduled tasks when /api/scheduler/tasks returns 404. */ export function getScheduledTasksFallback(triggers: TriggerForTasks[] = []): Array { console.warn('[fallback] 使用降级数据: getScheduledTasksFallback — 从 Triggers 列表推断'); return triggers .filter((t) => t.enabled) .map((trigger) => ({ id: trigger.id, name: `Trigger: ${trigger.type}`, schedule: 'event-based', status: 'active' as const, description: `Event trigger of type: ${trigger.type}`, })); } /** * Default security status when /api/security/status returns 404. * Returns honest minimal response — only includes layers that correspond * to real ZCLAW capabilities, no fabricated layers. */ export function getSecurityStatusFallback(): SecurityStatusFallback & { _isFallback: true } { console.warn('[fallback] 使用降级数据: getSecurityStatusFallback — 返回静态安全层状态'); const layers: SecurityLayerFallback[] = [ { name: 'device_auth', enabled: true, description: '设备认证' }, { name: 'rbac', enabled: true, description: '角色权限控制' }, { name: 'audit_log', enabled: true, description: '审计日志' }, { name: 'approval_gate', enabled: true, description: '操作审批门' }, { name: 'input_validation', enabled: true, description: '输入验证' }, { name: 'secret_storage', enabled: true, description: '密钥安全存储 (OS keyring)' }, ]; const enabledCount = layers.filter((l) => l.enabled).length; const securityLevel = calculateSecurityLevel(enabledCount, layers.length); return { _isFallback: true as const, layers, enabledCount, totalCount: layers.length, securityLevel, }; } /** * Calculate security level based on enabled layers ratio. */ function calculateSecurityLevel(enabledCount: number, totalCount: number): 'critical' | 'high' | 'medium' | 'low' { if (totalCount === 0) return 'low'; const ratio = enabledCount / totalCount; if (ratio >= 0.875) return 'critical'; // 14-16 layers if (ratio >= 0.625) return 'high'; // 10-13 layers if (ratio >= 0.375) return 'medium'; // 6-9 layers return 'low'; // 0-5 layers } // === Error Detection Helpers === /** * Check if an error is a 404 Not Found response. */ export function isNotFoundError(error: unknown): boolean { if (error instanceof Error) { const message = error.message.toLowerCase(); return message.includes('404') || message.includes('not found'); } if (typeof error === 'object' && error !== null) { const status = (error as { status?: number }).status; return status === 404; } return false; } /** * Check if an error is a network/connection error. */ export function isNetworkError(error: unknown): boolean { if (error instanceof Error) { const message = error.message.toLowerCase(); return ( message.includes('network') || message.includes('connection') || message.includes('timeout') || message.includes('abort') ); } return false; }