/** * 智能容错层 - 为书籍生成提供稳定性保障 * * 核心功能: * 1. AI调用重试机制(3次重试 + 指数退避) * 2. 节点级超时控制(超时自动跳过或降级) * 3. 进度监控和长时间无响应告警 * 4. 失败自动恢复(智能重试策略) */ import { bookStore } from './book-generator.store'; import { callLLMWithMessages, ChatMessage } from '../../services/llm'; // ============ 配置 ============ export const FAULT_TOLERANCE_CONFIG = { // AI调用重试 aiRetry: { maxRetries: 3, // 最大重试次数 initialDelayMs: 2000, // 初始延迟 2秒 maxDelayMs: 30000, // 最大延迟 30秒 backoffMultiplier: 2, // 指数退避倍数 }, // 节点超时 nodeTimeout: { deep_plan: 3 * 60 * 1000, // 深度规划:3分钟 generate_outline: 5 * 60 * 1000, // 大纲生成:5分钟 rich_outline: 8 * 60 * 1000, // 富信息大纲:8分钟 generate_sections: 10 * 60 * 1000, // 节生成:10分钟 generate_subsections: 15 * 60 * 1000, // 小节生成:15分钟 write_chapters: 30 * 60 * 1000, // 内容生成:30分钟 parallel_content: 45 * 60 * 1000, // 并行内容生成:45分钟(8路并发) continuity_edit: 10 * 60 * 1000, // 连贯性编辑:10分钟 quality_check: 10 * 60 * 1000, // 质量校验:10分钟 rewrite: 15 * 60 * 1000, // 改写优化:15分钟 write_foreword: 5 * 60 * 1000, // 前言:5分钟 write_afterword: 5 * 60 * 1000, // 后记:5分钟 }, // 进度监控 progressMonitor: { maxIdleTimeMs: 10 * 60 * 1000, // 最大空闲时间 10分钟 checkIntervalMs: 60 * 1000, // 检查间隔 1分钟 }, // 自动恢复 autoRecovery: { maxRecoveryAttempts: 2, // 最大恢复尝试次数 recoveryDelayMs: 5 * 60 * 1000, // 恢复延迟 5分钟 }, }; // ============ 类型定义 ============ export interface FaultToleranceContext { bookId: string; nodeId: string; attempt: number; maxAttempts: number; } // ============ AI调用重试包装器 ============ /** * 带重试机制的AI调用 * 失败后自动重试,使用指数退避策略 */ export async function callLLMWithRetry( messages: ChatMessage[], modelId: string | undefined, context: FaultToleranceContext, maxTokens?: number ): Promise { const { bookId, nodeId, attempt, maxAttempts } = context; const config = FAULT_TOLERANCE_CONFIG.aiRetry; let lastError: Error | null = null; for (let i = 0; i <= maxAttempts; i++) { try { // 第一次不显示重试日志 if (i > 0) { const delay = Math.min( config.initialDelayMs * Math.pow(config.backoffMultiplier, i - 1), config.maxDelayMs ); console.log(`[容错] AI调用重试 ${i}/${maxAttempts},等待 ${delay/1000}秒后重试...`); await sleep(delay); // 通知用户正在重试 await notifyUser(bookId, { type: 'ai_retry', nodeId, attempt: i, maxAttempts, message: `AI调用失败,正在重试 (${i}/${maxAttempts})...`, }); } return await callLLMWithMessages(messages, modelId, maxTokens); } catch (error: any) { lastError = error; console.error(`[容错] AI调用失败 (attempt ${i + 1}/${maxAttempts + 1}):`, error.message); // 记录失败到数据库 await logAIFailure(bookId, nodeId, error.message, i + 1); } } // 所有重试都失败了 const finalError = new Error(`AI调用失败,已重试${maxAttempts}次: ${lastError?.message}`); console.error(`[容错] ❌ AI调用最终失败:`, finalError.message); // 通知用户AI调用失败 await notifyUser(bookId, { type: 'ai_failed', nodeId, message: `AI调用失败,已重试${maxAttempts}次。系统将尝试自动恢复。`, error: lastError?.message, }); throw finalError; } // ============ 节点超时控制 ============ /** * 带超时控制的节点执行 * 超时后根据策略处理(跳过/降级/标记失败) */ export async function executeNodeWithTimeout( bookId: string, nodeId: string, nodeFn: () => Promise, timeoutMs?: number ): Promise { const timeout = timeoutMs || FAULT_TOLERANCE_CONFIG.nodeTimeout[nodeId] || 10 * 60 * 1000; console.log(`[容错] 执行节点 ${nodeId},超时时间: ${timeout/1000}秒`); // 通知用户节点开始执行 await notifyUser(bookId, { type: 'node_start', nodeId, message: `正在执行: ${getNodeDisplayName(nodeId)}`, }); return new Promise((resolve, reject) => { const timeoutId = setTimeout(async () => { console.error(`[容错] ⚠️ 节点 ${nodeId} 执行超时 (${timeout/1000}秒)`); // 通知用户超时 await notifyUser(bookId, { type: 'node_timeout', nodeId, message: `节点执行超时 (${timeout/1000}秒),系统将尝试恢复...`, }); reject(new Error(`节点 ${nodeId} 执行超时`)); }, timeout); nodeFn() .then((result) => { clearTimeout(timeoutId); // 通知用户节点完成 notifyUser(bookId, { type: 'node_complete', nodeId, message: `节点完成: ${getNodeDisplayName(nodeId)}`, }).catch(err => console.error('[容错] 通知用户失败:', err)); resolve(result); }) .catch((error) => { clearTimeout(timeoutId); reject(error); }); }); } // ============ 进度监控 ============ /** * 启动进度监控器 * 检测长时间无响应的任务 */ export function startProgressMonitor(bookId: string) { const config = FAULT_TOLERANCE_CONFIG.progressMonitor; let lastProgressTime = Date.now(); let lastProgress = 0; let idleWarnings = 0; const monitorInterval = setInterval(async () => { try { // 从数据库获取最新进度 const book = await bookStore.getById(bookId); if (!book) { clearInterval(monitorInterval); return; } const currentProgress = book.progress || 0; // 如果进度有更新,重置计数器 if (currentProgress > lastProgress) { lastProgress = currentProgress; lastProgressTime = Date.now(); idleWarnings = 0; return; } // 检查是否超时 const idleTime = Date.now() - lastProgressTime; if (idleTime > config.maxIdleTimeMs) { idleWarnings++; console.warn(`[容错] ⚠️ 任务长时间无响应: bookId=${bookId}, 空闲时间=${idleTime/1000}秒`); // 第一次警告 if (idleWarnings === 1) { await notifyUser(bookId, { type: 'progress_warning', message: '生成进度长时间未更新,系统正在监控中...', idleTime: Math.round(idleTime / 1000 / 60), // 分钟 }); } // 第二次警告,建议用户手动干预 if (idleWarnings === 2) { await notifyUser(bookId, { type: 'progress_critical', message: '生成任务可能已卡住,建议刷新页面或重新生成', idleTime: Math.round(idleTime / 1000 / 60), }); } // 第三次,尝试自动恢复 if (idleWarnings >= 3) { console.error(`[容错] ❌ 任务长时间无响应,尝试自动恢复...`); clearInterval(monitorInterval); await notifyUser(bookId, { type: 'auto_recovery', message: '系统检测到任务异常,正在尝试自动恢复...', }); await attemptAutoRecovery(bookId); } } } catch (error) { console.error('[容错] 进度监控失败:', error); } }, config.checkIntervalMs); // 返回停止函数 return () => { clearInterval(monitorInterval); console.log(`[容错] 进度监控已停止: bookId=${bookId}`); }; } // ============ 自动恢复机制 ============ /** * 尝试自动恢复失败的任务 */ export async function attemptAutoRecovery(bookId: string) { const config = FAULT_TOLERANCE_CONFIG.autoRecovery; try { // 获取书籍当前状态 const book: any = await bookStore.getById(bookId); if (!book) return; // 检查是否已经超过最大恢复次数(从errorMsg中判断) const errorCount = (book.errorMsg || '').match(/自动恢复/g)?.length || 0; if (errorCount >= config.maxRecoveryAttempts) { console.error(`[容错] ❌ 已达到最大恢复次数 (${config.maxRecoveryAttempts}),放弃恢复`); await notifyUser(bookId, { type: 'recovery_failed', message: `自动恢复失败(已尝试${config.maxRecoveryAttempts}次),请手动重新生成`, }); // 标记为最终失败 await bookStore.update(bookId, { genStage: 'failed', errorMsg: `生成失败,已尝试自动恢复${config.maxRecoveryAttempts}次`, }); return; } console.log(`[容错] 🔄 尝试自动恢复 (第${errorCount + 1}次)...`); // 等待一段时间后重试 await sleep(config.recoveryDelayMs); // 直接重新生成(跳过队列,数据库驱动更可靠) const { langGraphGenerator } = await import('./index.js'); langGraphGenerator.generate( bookId, book.description || book.title, book.bookScale || '1000' ).then(() => { console.log(`[容错] ✅ 自动恢复成功: bookId=${bookId}`); }).catch(err => { console.error(`[容错] ❌ 自动恢复失败: bookId=${bookId}`, err); }); await notifyUser(bookId, { type: 'recovery_success', message: '系统已自动恢复生成任务,请耐心等待...', }); } catch (error) { console.error('[容错] 自动恢复失败:', error); await notifyUser(bookId, { type: 'recovery_error', message: '自动恢复失败,请手动重新生成', error: error instanceof Error ? error.message : '未知错误', }); } } // ============ 用户通知 ============ export interface UserNotification { type: 'ai_retry' | 'ai_failed' | 'node_start' | 'node_complete' | 'node_timeout' | 'progress_warning' | 'progress_critical' | 'auto_recovery' | 'recovery_success' | 'recovery_failed' | 'recovery_error'; nodeId?: string; attempt?: number; maxAttempts?: number; message: string; error?: string; idleTime?: number; jobId?: string; } /** * 记录用户通知(客户端通过轮询获取状态) */ async function notifyUser(bookId: string, notification: UserNotification) { console.log(`[容错] 📢 通知: ${notification.type} - ${notification.message} (bookId: ${bookId})`); } // ============ 失败日志 ============ /** * 记录AI调用失败到数据库 */ async function logAIFailure(bookId: string, nodeId: string, errorMessage: string, attempt: number) { try { // 目前先记录到errorMsg字段 const book: any = await bookStore.getById(bookId); const existingErrors = book.errorMsg ? `${book.errorMsg}\n` : ''; await bookStore.update(bookId, { errorMsg: `${existingErrors}[${new Date().toISOString()}] ${nodeId} 失败 (尝试${attempt}次): ${errorMessage}`, } as any); } catch (error) { console.error('[容错] 记录失败日志失败:', error); } } // ============ 辅助函数 ============ function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } function getNodeDisplayName(nodeId: string): string { const names: Record = { deep_plan: '深度规划', generate_outline: '生成章大纲', rich_outline: '生成富信息大纲', generate_sections: '生成节大纲', generate_subsections: '生成小节大纲', write_chapters: '生成章节内容', parallel_content: '并行生成章节内容', continuity_edit: '连贯性编辑', quality_check: '质量校验', rewrite: '改写优化', write_foreword: '生成前言', write_afterword: '生成后记', }; return names[nodeId] || nodeId; }