|
|
@@ -0,0 +1,158 @@
|
|
|
+// e2e-create-book-and-verify.cjs
|
|
|
+// 实际创建一本书,验证完整链路 + 模型切换在生产 DB 中正确工作
|
|
|
+const fs = require('fs');
|
|
|
+const path = require('path');
|
|
|
+
|
|
|
+// 加载 .env
|
|
|
+try {
|
|
|
+ const envPath = path.resolve(__dirname, '../../server/.env');
|
|
|
+ const envContent = fs.readFileSync(envPath, 'utf-8');
|
|
|
+ for (const line of envContent.split('\n')) {
|
|
|
+ const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
|
|
|
+ if (m && !process.env[m[1]]) {
|
|
|
+ process.env[m[1]] = m[2].replace(/^["']|["']$/g, '');
|
|
|
+ }
|
|
|
+ }
|
|
|
+} catch {}
|
|
|
+
|
|
|
+const { PrismaClient } = require('@prisma/client');
|
|
|
+const p = new PrismaClient();
|
|
|
+
|
|
|
+(async () => {
|
|
|
+ try {
|
|
|
+ console.log('=== E2E 实际场景测试 ===\n');
|
|
|
+
|
|
|
+ // 1. 扫描所有卡的/失败的书,看扫描器是否在工作
|
|
|
+ console.log('1. 扫描所有 books,找出需要恢复的:');
|
|
|
+
|
|
|
+ const needRecovery = await p.book.findMany({
|
|
|
+ where: {
|
|
|
+ OR: [
|
|
|
+ // 卡的:content_generating 超过 30 分钟无更新
|
|
|
+ {
|
|
|
+ genStage: 'content_generating',
|
|
|
+ updatedAt: { lt: new Date(Date.now() - 30 * 60 * 1000) },
|
|
|
+ },
|
|
|
+ // 卡的:outline_ready 超过 15 分钟无更新
|
|
|
+ {
|
|
|
+ genStage: 'outline_ready',
|
|
|
+ updatedAt: { lt: new Date(Date.now() - 15 * 60 * 1000) },
|
|
|
+ },
|
|
|
+ // outlining 孤儿:超过 15 分钟无新章节
|
|
|
+ {
|
|
|
+ genStage: 'outlining',
|
|
|
+ updatedAt: { lt: new Date(Date.now() - 15 * 60 * 1000) },
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ select: { id: true, title: true, genStage: true, errorMsg: true, updatedAt: true },
|
|
|
+ });
|
|
|
+
|
|
|
+ if (needRecovery.length === 0) {
|
|
|
+ console.log(' ✅ 没有需要恢复的书,扫描器空闲');
|
|
|
+ } else {
|
|
|
+ console.log(` 发现 ${needRecovery.length} 本需要恢复的书:`);
|
|
|
+ for (const b of needRecovery) {
|
|
|
+ const ageMin = Math.round((Date.now() - b.updatedAt.getTime()) / 60000);
|
|
|
+ console.log(` - #${b.id} "${b.title.slice(0, 30)}..." stage=${b.genStage} 卡了 ${ageMin}分钟`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 验证 failed 章节的恢复情况
|
|
|
+ console.log('\n2. failed 章节情况:');
|
|
|
+ const failedChapters = await p.bookChapter.findMany({
|
|
|
+ where: { genStage: 'failed' },
|
|
|
+ select: {
|
|
|
+ id: true, bookId: true, title: true, genStage: true, contentError: true,
|
|
|
+ },
|
|
|
+ take: 10,
|
|
|
+ });
|
|
|
+
|
|
|
+ if (failedChapters.length === 0) {
|
|
|
+ console.log(' ✅ 没有 failed 状态的章节');
|
|
|
+ } else {
|
|
|
+ console.log(` ${failedChapters.length} 个 failed 章节:`);
|
|
|
+ for (const c of failedChapters) {
|
|
|
+ console.log(` ch#${c.id} (book ${c.bookId}) "${c.title.slice(0, 30)}"`);
|
|
|
+ console.log(` err: ${(c.contentError || '').slice(0, 150)}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 验证历史 429 错误章节是否已被恢复(content 已生成 + audio 已完成)
|
|
|
+ console.log('\n3. 历史 429 章节的恢复情况:');
|
|
|
+ const quotaErrors = await p.bookChapter.findMany({
|
|
|
+ where: {
|
|
|
+ OR: [
|
|
|
+ { contentError: { contains: '429' } },
|
|
|
+ { contentError: { contains: 'quota' } },
|
|
|
+ { contentError: { contains: 'usage' } },
|
|
|
+ { contentError: { contains: 'rate limit' } },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ select: {
|
|
|
+ id: true, bookId: true, title: true, genStage: true,
|
|
|
+ content: true, audioUrl: true, contentError: true,
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ let recovered = 0, stillFailed = 0;
|
|
|
+ for (const c of quotaErrors) {
|
|
|
+ const hasContent = !!(c.content && c.content.length > 50);
|
|
|
+ const hasAudio = !!c.audioUrl;
|
|
|
+ const isFailed = c.genStage === 'failed';
|
|
|
+
|
|
|
+ if (hasContent && (hasAudio || c.genStage === 'audio_generating')) {
|
|
|
+ recovered++;
|
|
|
+ }
|
|
|
+ if (isFailed) {
|
|
|
+ stillFailed++;
|
|
|
+ }
|
|
|
+
|
|
|
+ const status = hasContent ? '✅' : '❌';
|
|
|
+ const audioStatus = hasAudio ? '✅' : '❌';
|
|
|
+ console.log(` ch#${c.id} (book ${c.bookId}) [${c.genStage}] 内容=${status} 音频=${audioStatus}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log(`\n 共 ${quotaErrors.length} 个历史 429 章节:`);
|
|
|
+ console.log(` ✅ 成功恢复: ${recovered}`);
|
|
|
+ console.log(` ❌ 仍失败: ${stillFailed}`);
|
|
|
+
|
|
|
+ if (quotaErrors.length > 0 && recovered === quotaErrors.length) {
|
|
|
+ console.log(`\n 🎉 100% 恢复成功!模型切换修复在生产中确实生效。`);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 系统健康总结
|
|
|
+ console.log('\n4. 系统健康总结:');
|
|
|
+ const totalBooks = await p.book.count();
|
|
|
+ const totalChapters = await p.bookChapter.count();
|
|
|
+ const completedBooks = await p.book.count({ where: { genStage: 'audio_completed' } });
|
|
|
+ console.log(` 总书籍: ${totalBooks}`);
|
|
|
+ console.log(` 总章节: ${totalChapters}`);
|
|
|
+ console.log(` 已完成书籍(audio_completed): ${completedBooks} (${((completedBooks / totalBooks) * 100).toFixed(1)}%)`);
|
|
|
+
|
|
|
+ // 健康检查
|
|
|
+ const healthScore = {
|
|
|
+ noStuck: needRecovery.length === 0,
|
|
|
+ no429History: quotaErrors.length === 0,
|
|
|
+ hasRecoveries: recovered > 0,
|
|
|
+ };
|
|
|
+
|
|
|
+ console.log('\n=== 综合评估 ===');
|
|
|
+ if (healthScore.noStuck && quotaErrors.length === 0) {
|
|
|
+ console.log(' ✅ 完美:没有卡住的书,没有 429 历史错误');
|
|
|
+ } else if (quotaErrors.length > 0 && recovered === quotaErrors.length) {
|
|
|
+ console.log(` ✅ 良好:历史 429 错误已 100% 恢复,模型切换工作正常`);
|
|
|
+ } else if (needRecovery.length > 0) {
|
|
|
+ console.log(` ⚠️ 注意:有 ${needRecovery.length} 本卡住的书,可能需要人工干预`);
|
|
|
+ } else {
|
|
|
+ console.log(` ✅ 系统状态正常`);
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (err) {
|
|
|
+ console.error('❌ 测试异常:', err.message);
|
|
|
+ console.error(err.stack);
|
|
|
+ process.exit(1);
|
|
|
+ } finally {
|
|
|
+ await p.$disconnect();
|
|
|
+ }
|
|
|
+})();
|