fault-tolerance.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. /**
  2. * 智能容错层 - 为书籍生成提供稳定性保障
  3. *
  4. * 核心功能:
  5. * 1. AI调用重试机制(3次重试 + 指数退避)
  6. * 2. 节点级超时控制(超时自动跳过或降级)
  7. * 3. 进度监控和长时间无响应告警
  8. * 4. 失败自动恢复(智能重试策略)
  9. */
  10. import { bookStore } from './book-generator.store';
  11. import { callLLMWithMessages, ChatMessage } from '../../services/llm';
  12. // ============ 配置 ============
  13. export const FAULT_TOLERANCE_CONFIG = {
  14. // AI调用重试
  15. aiRetry: {
  16. maxRetries: 3, // 最大重试次数
  17. initialDelayMs: 2000, // 初始延迟 2秒
  18. maxDelayMs: 30000, // 最大延迟 30秒
  19. backoffMultiplier: 2, // 指数退避倍数
  20. },
  21. // 节点超时
  22. nodeTimeout: {
  23. deep_plan: 3 * 60 * 1000, // 深度规划:3分钟
  24. generate_outline: 5 * 60 * 1000, // 大纲生成:5分钟
  25. rich_outline: 8 * 60 * 1000, // 富信息大纲:8分钟
  26. generate_sections: 10 * 60 * 1000, // 节生成:10分钟
  27. generate_subsections: 15 * 60 * 1000, // 小节生成:15分钟
  28. write_chapters: 30 * 60 * 1000, // 内容生成:30分钟
  29. parallel_content: 45 * 60 * 1000, // 并行内容生成:45分钟(8路并发)
  30. continuity_edit: 10 * 60 * 1000, // 连贯性编辑:10分钟
  31. quality_check: 10 * 60 * 1000, // 质量校验:10分钟
  32. rewrite: 15 * 60 * 1000, // 改写优化:15分钟
  33. write_foreword: 5 * 60 * 1000, // 前言:5分钟
  34. write_afterword: 5 * 60 * 1000, // 后记:5分钟
  35. },
  36. // 进度监控
  37. progressMonitor: {
  38. maxIdleTimeMs: 10 * 60 * 1000, // 最大空闲时间 10分钟
  39. checkIntervalMs: 60 * 1000, // 检查间隔 1分钟
  40. },
  41. // 自动恢复
  42. autoRecovery: {
  43. maxRecoveryAttempts: 2, // 最大恢复尝试次数
  44. recoveryDelayMs: 5 * 60 * 1000, // 恢复延迟 5分钟
  45. },
  46. };
  47. // ============ 类型定义 ============
  48. export interface FaultToleranceContext {
  49. bookId: string;
  50. nodeId: string;
  51. attempt: number;
  52. maxAttempts: number;
  53. }
  54. // ============ AI调用重试包装器 ============
  55. /**
  56. * 带重试机制的AI调用
  57. * 失败后自动重试,使用指数退避策略
  58. */
  59. export async function callLLMWithRetry(
  60. messages: ChatMessage[],
  61. modelId: string | undefined,
  62. context: FaultToleranceContext,
  63. maxTokens?: number
  64. ): Promise<string> {
  65. const { bookId, nodeId, attempt, maxAttempts } = context;
  66. const config = FAULT_TOLERANCE_CONFIG.aiRetry;
  67. let lastError: Error | null = null;
  68. for (let i = 0; i <= maxAttempts; i++) {
  69. try {
  70. // 第一次不显示重试日志
  71. if (i > 0) {
  72. const delay = Math.min(
  73. config.initialDelayMs * Math.pow(config.backoffMultiplier, i - 1),
  74. config.maxDelayMs
  75. );
  76. console.log(`[容错] AI调用重试 ${i}/${maxAttempts},等待 ${delay/1000}秒后重试...`);
  77. await sleep(delay);
  78. // 通知用户正在重试
  79. await notifyUser(bookId, {
  80. type: 'ai_retry',
  81. nodeId,
  82. attempt: i,
  83. maxAttempts,
  84. message: `AI调用失败,正在重试 (${i}/${maxAttempts})...`,
  85. });
  86. }
  87. return await callLLMWithMessages(messages, modelId, maxTokens);
  88. } catch (error: any) {
  89. lastError = error;
  90. console.error(`[容错] AI调用失败 (attempt ${i + 1}/${maxAttempts + 1}):`, error.message);
  91. // 记录失败到数据库
  92. await logAIFailure(bookId, nodeId, error.message, i + 1);
  93. }
  94. }
  95. // 所有重试都失败了
  96. const finalError = new Error(`AI调用失败,已重试${maxAttempts}次: ${lastError?.message}`);
  97. console.error(`[容错] ❌ AI调用最终失败:`, finalError.message);
  98. // 通知用户AI调用失败
  99. await notifyUser(bookId, {
  100. type: 'ai_failed',
  101. nodeId,
  102. message: `AI调用失败,已重试${maxAttempts}次。系统将尝试自动恢复。`,
  103. error: lastError?.message,
  104. });
  105. throw finalError;
  106. }
  107. // ============ 节点超时控制 ============
  108. /**
  109. * 带超时控制的节点执行
  110. * 超时后根据策略处理(跳过/降级/标记失败)
  111. */
  112. export async function executeNodeWithTimeout<T>(
  113. bookId: string,
  114. nodeId: string,
  115. nodeFn: () => Promise<T>,
  116. timeoutMs?: number
  117. ): Promise<T> {
  118. const timeout = timeoutMs || FAULT_TOLERANCE_CONFIG.nodeTimeout[nodeId] || 10 * 60 * 1000;
  119. console.log(`[容错] 执行节点 ${nodeId},超时时间: ${timeout/1000}秒`);
  120. // 通知用户节点开始执行
  121. await notifyUser(bookId, {
  122. type: 'node_start',
  123. nodeId,
  124. message: `正在执行: ${getNodeDisplayName(nodeId)}`,
  125. });
  126. return new Promise<T>((resolve, reject) => {
  127. const timeoutId = setTimeout(async () => {
  128. console.error(`[容错] ⚠️ 节点 ${nodeId} 执行超时 (${timeout/1000}秒)`);
  129. // 通知用户超时
  130. await notifyUser(bookId, {
  131. type: 'node_timeout',
  132. nodeId,
  133. message: `节点执行超时 (${timeout/1000}秒),系统将尝试恢复...`,
  134. });
  135. reject(new Error(`节点 ${nodeId} 执行超时`));
  136. }, timeout);
  137. nodeFn()
  138. .then((result) => {
  139. clearTimeout(timeoutId);
  140. // 通知用户节点完成
  141. notifyUser(bookId, {
  142. type: 'node_complete',
  143. nodeId,
  144. message: `节点完成: ${getNodeDisplayName(nodeId)}`,
  145. }).catch(err => console.error('[容错] 通知用户失败:', err));
  146. resolve(result);
  147. })
  148. .catch((error) => {
  149. clearTimeout(timeoutId);
  150. reject(error);
  151. });
  152. });
  153. }
  154. // ============ 进度监控 ============
  155. /**
  156. * 启动进度监控器
  157. * 检测长时间无响应的任务
  158. */
  159. export function startProgressMonitor(bookId: string) {
  160. const config = FAULT_TOLERANCE_CONFIG.progressMonitor;
  161. let lastProgressTime = Date.now();
  162. let lastProgress = 0;
  163. let idleWarnings = 0;
  164. const monitorInterval = setInterval(async () => {
  165. try {
  166. // 从数据库获取最新进度
  167. const book = await bookStore.getById(bookId);
  168. if (!book) {
  169. clearInterval(monitorInterval);
  170. return;
  171. }
  172. const currentProgress = book.progress || 0;
  173. // 如果进度有更新,重置计数器
  174. if (currentProgress > lastProgress) {
  175. lastProgress = currentProgress;
  176. lastProgressTime = Date.now();
  177. idleWarnings = 0;
  178. return;
  179. }
  180. // 检查是否超时
  181. const idleTime = Date.now() - lastProgressTime;
  182. if (idleTime > config.maxIdleTimeMs) {
  183. idleWarnings++;
  184. console.warn(`[容错] ⚠️ 任务长时间无响应: bookId=${bookId}, 空闲时间=${idleTime/1000}秒`);
  185. // 第一次警告
  186. if (idleWarnings === 1) {
  187. await notifyUser(bookId, {
  188. type: 'progress_warning',
  189. message: '生成进度长时间未更新,系统正在监控中...',
  190. idleTime: Math.round(idleTime / 1000 / 60), // 分钟
  191. });
  192. }
  193. // 第二次警告,建议用户手动干预
  194. if (idleWarnings === 2) {
  195. await notifyUser(bookId, {
  196. type: 'progress_critical',
  197. message: '生成任务可能已卡住,建议刷新页面或重新生成',
  198. idleTime: Math.round(idleTime / 1000 / 60),
  199. });
  200. }
  201. // 第三次,尝试自动恢复
  202. if (idleWarnings >= 3) {
  203. console.error(`[容错] ❌ 任务长时间无响应,尝试自动恢复...`);
  204. clearInterval(monitorInterval);
  205. await notifyUser(bookId, {
  206. type: 'auto_recovery',
  207. message: '系统检测到任务异常,正在尝试自动恢复...',
  208. });
  209. await attemptAutoRecovery(bookId);
  210. }
  211. }
  212. } catch (error) {
  213. console.error('[容错] 进度监控失败:', error);
  214. }
  215. }, config.checkIntervalMs);
  216. // 返回停止函数
  217. return () => {
  218. clearInterval(monitorInterval);
  219. console.log(`[容错] 进度监控已停止: bookId=${bookId}`);
  220. };
  221. }
  222. // ============ 自动恢复机制 ============
  223. /**
  224. * 尝试自动恢复失败的任务
  225. */
  226. export async function attemptAutoRecovery(bookId: string) {
  227. const config = FAULT_TOLERANCE_CONFIG.autoRecovery;
  228. try {
  229. // 获取书籍当前状态
  230. const book: any = await bookStore.getById(bookId);
  231. if (!book) return;
  232. // 检查是否已经超过最大恢复次数(从errorMsg中判断)
  233. const errorCount = (book.errorMsg || '').match(/自动恢复/g)?.length || 0;
  234. if (errorCount >= config.maxRecoveryAttempts) {
  235. console.error(`[容错] ❌ 已达到最大恢复次数 (${config.maxRecoveryAttempts}),放弃恢复`);
  236. await notifyUser(bookId, {
  237. type: 'recovery_failed',
  238. message: `自动恢复失败(已尝试${config.maxRecoveryAttempts}次),请手动重新生成`,
  239. });
  240. // 标记为最终失败
  241. await bookStore.update(bookId, {
  242. genStage: 'failed',
  243. errorMsg: `生成失败,已尝试自动恢复${config.maxRecoveryAttempts}次`,
  244. });
  245. return;
  246. }
  247. console.log(`[容错] 🔄 尝试自动恢复 (第${errorCount + 1}次)...`);
  248. // 等待一段时间后重试
  249. await sleep(config.recoveryDelayMs);
  250. // 直接重新生成(跳过队列,数据库驱动更可靠)
  251. const { langGraphGenerator } = await import('./index.js');
  252. langGraphGenerator.generate(
  253. bookId,
  254. book.description || book.title,
  255. book.bookScale || '1000'
  256. ).then(() => {
  257. console.log(`[容错] ✅ 自动恢复成功: bookId=${bookId}`);
  258. }).catch(err => {
  259. console.error(`[容错] ❌ 自动恢复失败: bookId=${bookId}`, err);
  260. });
  261. await notifyUser(bookId, {
  262. type: 'recovery_success',
  263. message: '系统已自动恢复生成任务,请耐心等待...',
  264. });
  265. } catch (error) {
  266. console.error('[容错] 自动恢复失败:', error);
  267. await notifyUser(bookId, {
  268. type: 'recovery_error',
  269. message: '自动恢复失败,请手动重新生成',
  270. error: error instanceof Error ? error.message : '未知错误',
  271. });
  272. }
  273. }
  274. // ============ 用户通知 ============
  275. export interface UserNotification {
  276. type: 'ai_retry' | 'ai_failed' | 'node_start' | 'node_complete' | 'node_timeout' |
  277. 'progress_warning' | 'progress_critical' | 'auto_recovery' |
  278. 'recovery_success' | 'recovery_failed' | 'recovery_error';
  279. nodeId?: string;
  280. attempt?: number;
  281. maxAttempts?: number;
  282. message: string;
  283. error?: string;
  284. idleTime?: number;
  285. jobId?: string;
  286. }
  287. /**
  288. * 记录用户通知(客户端通过轮询获取状态)
  289. */
  290. async function notifyUser(bookId: string, notification: UserNotification) {
  291. console.log(`[容错] 📢 通知: ${notification.type} - ${notification.message} (bookId: ${bookId})`);
  292. }
  293. // ============ 失败日志 ============
  294. /**
  295. * 记录AI调用失败到数据库
  296. */
  297. async function logAIFailure(bookId: string, nodeId: string, errorMessage: string, attempt: number) {
  298. try {
  299. // 目前先记录到errorMsg字段
  300. const book: any = await bookStore.getById(bookId);
  301. const existingErrors = book.errorMsg ? `${book.errorMsg}\n` : '';
  302. await bookStore.update(bookId, {
  303. errorMsg: `${existingErrors}[${new Date().toISOString()}] ${nodeId} 失败 (尝试${attempt}次): ${errorMessage}`,
  304. } as any);
  305. } catch (error) {
  306. console.error('[容错] 记录失败日志失败:', error);
  307. }
  308. }
  309. // ============ 辅助函数 ============
  310. function sleep(ms: number): Promise<void> {
  311. return new Promise(resolve => setTimeout(resolve, ms));
  312. }
  313. function getNodeDisplayName(nodeId: string): string {
  314. const names: Record<string, string> = {
  315. deep_plan: '深度规划',
  316. generate_outline: '生成章大纲',
  317. rich_outline: '生成富信息大纲',
  318. generate_sections: '生成节大纲',
  319. generate_subsections: '生成小节大纲',
  320. write_chapters: '生成章节内容',
  321. parallel_content: '并行生成章节内容',
  322. continuity_edit: '连贯性编辑',
  323. quality_check: '质量校验',
  324. rewrite: '改写优化',
  325. write_foreword: '生成前言',
  326. write_afterword: '生成后记',
  327. };
  328. return names[nodeId] || nodeId;
  329. }