e2e-create-book.cjs 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. // e2e-create-book-and-verify.cjs
  2. // 实际创建一本书,验证完整链路 + 模型切换在生产 DB 中正确工作
  3. const fs = require('fs');
  4. const path = require('path');
  5. // 加载 .env
  6. try {
  7. const envPath = path.resolve(__dirname, '../../server/.env');
  8. const envContent = fs.readFileSync(envPath, 'utf-8');
  9. for (const line of envContent.split('\n')) {
  10. const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
  11. if (m && !process.env[m[1]]) {
  12. process.env[m[1]] = m[2].replace(/^["']|["']$/g, '');
  13. }
  14. }
  15. } catch {}
  16. const { PrismaClient } = require('@prisma/client');
  17. const p = new PrismaClient();
  18. (async () => {
  19. try {
  20. console.log('=== E2E 实际场景测试 ===\n');
  21. // 1. 扫描所有卡的/失败的书,看扫描器是否在工作
  22. console.log('1. 扫描所有 books,找出需要恢复的:');
  23. const needRecovery = await p.book.findMany({
  24. where: {
  25. OR: [
  26. // 卡的:content_generating 超过 30 分钟无更新
  27. {
  28. genStage: 'content_generating',
  29. updatedAt: { lt: new Date(Date.now() - 30 * 60 * 1000) },
  30. },
  31. // 卡的:outline_ready 超过 15 分钟无更新
  32. {
  33. genStage: 'outline_ready',
  34. updatedAt: { lt: new Date(Date.now() - 15 * 60 * 1000) },
  35. },
  36. // outlining 孤儿:超过 15 分钟无新章节
  37. {
  38. genStage: 'outlining',
  39. updatedAt: { lt: new Date(Date.now() - 15 * 60 * 1000) },
  40. },
  41. ],
  42. },
  43. select: { id: true, title: true, genStage: true, errorMsg: true, updatedAt: true },
  44. });
  45. if (needRecovery.length === 0) {
  46. console.log(' ✅ 没有需要恢复的书,扫描器空闲');
  47. } else {
  48. console.log(` 发现 ${needRecovery.length} 本需要恢复的书:`);
  49. for (const b of needRecovery) {
  50. const ageMin = Math.round((Date.now() - b.updatedAt.getTime()) / 60000);
  51. console.log(` - #${b.id} "${b.title.slice(0, 30)}..." stage=${b.genStage} 卡了 ${ageMin}分钟`);
  52. }
  53. }
  54. // 2. 验证 failed 章节的恢复情况
  55. console.log('\n2. failed 章节情况:');
  56. const failedChapters = await p.bookChapter.findMany({
  57. where: { genStage: 'failed' },
  58. select: {
  59. id: true, bookId: true, title: true, genStage: true, contentError: true,
  60. },
  61. take: 10,
  62. });
  63. if (failedChapters.length === 0) {
  64. console.log(' ✅ 没有 failed 状态的章节');
  65. } else {
  66. console.log(` ${failedChapters.length} 个 failed 章节:`);
  67. for (const c of failedChapters) {
  68. console.log(` ch#${c.id} (book ${c.bookId}) "${c.title.slice(0, 30)}"`);
  69. console.log(` err: ${(c.contentError || '').slice(0, 150)}`);
  70. }
  71. }
  72. // 3. 验证历史 429 错误章节是否已被恢复(content 已生成 + audio 已完成)
  73. console.log('\n3. 历史 429 章节的恢复情况:');
  74. const quotaErrors = await p.bookChapter.findMany({
  75. where: {
  76. OR: [
  77. { contentError: { contains: '429' } },
  78. { contentError: { contains: 'quota' } },
  79. { contentError: { contains: 'usage' } },
  80. { contentError: { contains: 'rate limit' } },
  81. ],
  82. },
  83. select: {
  84. id: true, bookId: true, title: true, genStage: true,
  85. content: true, audioUrl: true, contentError: true,
  86. },
  87. });
  88. let recovered = 0, stillFailed = 0;
  89. for (const c of quotaErrors) {
  90. const hasContent = !!(c.content && c.content.length > 50);
  91. const hasAudio = !!c.audioUrl;
  92. const isFailed = c.genStage === 'failed';
  93. if (hasContent && (hasAudio || c.genStage === 'audio_generating')) {
  94. recovered++;
  95. }
  96. if (isFailed) {
  97. stillFailed++;
  98. }
  99. const status = hasContent ? '✅' : '❌';
  100. const audioStatus = hasAudio ? '✅' : '❌';
  101. console.log(` ch#${c.id} (book ${c.bookId}) [${c.genStage}] 内容=${status} 音频=${audioStatus}`);
  102. }
  103. console.log(`\n 共 ${quotaErrors.length} 个历史 429 章节:`);
  104. console.log(` ✅ 成功恢复: ${recovered}`);
  105. console.log(` ❌ 仍失败: ${stillFailed}`);
  106. if (quotaErrors.length > 0 && recovered === quotaErrors.length) {
  107. console.log(`\n 🎉 100% 恢复成功!模型切换修复在生产中确实生效。`);
  108. }
  109. // 4. 系统健康总结
  110. console.log('\n4. 系统健康总结:');
  111. const totalBooks = await p.book.count();
  112. const totalChapters = await p.bookChapter.count();
  113. const completedBooks = await p.book.count({ where: { genStage: 'audio_completed' } });
  114. console.log(` 总书籍: ${totalBooks}`);
  115. console.log(` 总章节: ${totalChapters}`);
  116. console.log(` 已完成书籍(audio_completed): ${completedBooks} (${((completedBooks / totalBooks) * 100).toFixed(1)}%)`);
  117. // 健康检查
  118. const healthScore = {
  119. noStuck: needRecovery.length === 0,
  120. no429History: quotaErrors.length === 0,
  121. hasRecoveries: recovered > 0,
  122. };
  123. console.log('\n=== 综合评估 ===');
  124. if (healthScore.noStuck && quotaErrors.length === 0) {
  125. console.log(' ✅ 完美:没有卡住的书,没有 429 历史错误');
  126. } else if (quotaErrors.length > 0 && recovered === quotaErrors.length) {
  127. console.log(` ✅ 良好:历史 429 错误已 100% 恢复,模型切换工作正常`);
  128. } else if (needRecovery.length > 0) {
  129. console.log(` ⚠️ 注意:有 ${needRecovery.length} 本卡住的书,可能需要人工干预`);
  130. } else {
  131. console.log(` ✅ 系统状态正常`);
  132. }
  133. } catch (err) {
  134. console.error('❌ 测试异常:', err.message);
  135. console.error(err.stack);
  136. process.exit(1);
  137. } finally {
  138. await p.$disconnect();
  139. }
  140. })();