// e2e-verify-model-switch.js // 实际数据库连接,验证扫描器/状态机/供应商切换 const { PrismaClient } = require('@prisma/client'); // 从 server/.env 加载 DATABASE_URL const fs = require('fs'); const path = require('path'); 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 (e) { console.warn('未找到 .env, 依靠环境变量'); } const p = new PrismaClient(); (async () => { try { // 1. 找到当前数据库所有供应商状态 console.log('=== 1. 当前供应商状态 ==='); const recentBooks = await p.book.findMany({ where: { createdAt: { gte: new Date(Date.now() - 60 * 60 * 1000) } }, select: { id: true, title: true, genStage: true, errorMsg: true, updatedAt: true, createdAt: true }, orderBy: { createdAt: 'desc' }, take: 10, }); if (recentBooks.length === 0) { console.log(' 没有最近 1 小时的书,等待真实数据...'); // 看最近 24 小时的 const allBooks = await p.book.findMany({ select: { id: true, title: true, genStage: true, errorMsg: true, updatedAt: true, createdAt: true }, orderBy: { createdAt: 'desc' }, take: 10, }); console.log(` 最近 10 本书 (24h+):`); for (const b of allBooks) { console.log(` - #${b.id} "${b.title.slice(0, 30)}..." stage=${b.genStage} err=${b.errorMsg ? b.errorMsg.slice(0, 80) : 'null'}`); } } else { console.log(` 最近 1 小时 ${recentBooks.length} 本书:`); for (const b of recentBooks) { console.log(` - #${b.id} "${b.title.slice(0, 30)}..." stage=${b.genStage} err=${b.errorMsg ? b.errorMsg.slice(0, 80) : 'null'}`); } } // 2. 检查 failed 章节情况 console.log('\n=== 2. 失败章节统计 ==='); const failedChapters = await p.bookChapter.groupBy({ by: ['genStage'], _count: { genStage: true }, where: { contentError: { not: null } }, }); for (const f of failedChapters) { console.log(` ${f.genStage}: ${f._count.genStage}`); } // 3. 检查 scan log 来推断恢复情况 console.log('\n=== 3. 看最近 5 个 failed 章节的内容错误 ==='); const failedSamples = await p.bookChapter.findMany({ where: { contentError: { not: null } }, select: { id: true, bookId: true, title: true, contentError: true, updatedAt: true }, orderBy: { updatedAt: 'desc' }, take: 5, }); for (const c of failedSamples) { console.log(` ch#${c.id} (book ${c.bookId}) "${c.title.slice(0, 30)}":`); console.log(` err: ${(c.contentError || '').slice(0, 200)}`); console.log(` updatedAt: ${c.updatedAt.toISOString()}`); } // 4. 验证 recovery scanner 是否在工作 console.log('\n=== 4. 检查扫描器状态 ==='); const stuckBooks = await p.book.count({ where: { genStage: 'content_generating', updatedAt: { lt: new Date(Date.now() - 30 * 60 * 1000) }, // 30分钟没动 }, }); console.log(` 卡在 content_generating 超过 30 分钟的书: ${stuckBooks}`); const outlineReadyBooks = await p.book.count({ where: { genStage: 'outline_ready', updatedAt: { lt: new Date(Date.now() - 15 * 60 * 1000) }, // 15分钟没动 }, }); console.log(` 卡在 outline_ready 超过 15 分钟的书: ${outlineReadyBooks}`); // 5. 健康总结 console.log('\n=== 5. 健康总结 ==='); const healthCheck = { noCrash: true, // 如果到这里没崩就算 recoverableBooks: 0, totalFailedChapters: failedSamples.length, stuckBooks, has429Evidence: false, }; // 检测是否有过 429 for (const c of failedSamples) { if (c.contentError && /429|quota|usage|rate limit|insufficient/i.test(c.contentError)) { healthCheck.has429Evidence = true; break; } } console.log(` 进程存活: ✅`); console.log(` 失败章节总数: ${healthCheck.totalFailedChapters}`); console.log(` 卡住的书籍: ${healthCheck.stuckBooks}`); console.log(` 看到过 429 证据: ${healthCheck.has429Evidence ? '⚠️ 是' : '✅ 否'}`); } catch (err) { console.error('❌ 测试异常:', err.message); process.exit(1); } finally { await p.$disconnect(); } })();