// e2e-verify-model-switch.js // 实际数据库连接,验证扫描器/状态机/供应商切换 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 { PrismaClient } = require('@prisma/client'); const p = new PrismaClient(); (async () => { try { // 1. 最近的书 console.log('=== 1. 最近 10 本书状态 ==='); const allBooks = await p.book.findMany({ select: { id: true, title: true, genStage: true, errorMsg: true, createdAt: true }, orderBy: { createdAt: 'desc' }, take: 10, }); for (const b of allBooks) { console.log(` #${b.id} "${b.title.slice(0, 30)}..." stage=${b.genStage} ${b.errorMsg ? `err=${b.errorMsg.slice(0, 60)}` : ''}`); } // 2. 按状态统计 console.log('\n=== 2. 书的 genStage 分布 ==='); const bookStats = await p.book.groupBy({ by: ['genStage'], _count: { genStage: true }, }); for (const s of bookStats.sort((a, b) => b._count.genStage - a._count.genStage)) { console.log(` ${s.genStage}: ${s._count.genStage}`); } // 3. 章节状态分布 console.log('\n=== 3. 章节的 genStage 分布 ==='); const chapterStats = await p.bookChapter.groupBy({ by: ['genStage'], _count: { genStage: true }, }); for (const s of chapterStats.sort((a, b) => b._count.genStage - a._count.genStage)) { console.log(` ${s.genStage}: ${s._count.genStage}`); } // 4. 失败章节的内容错误 console.log('\n=== 4. 看最近 5 个有 contentError 的章节 ==='); const failedSamples = await p.bookChapter.findMany({ where: { contentError: { not: null } }, select: { id: true, bookId: true, title: true, contentError: true, genStage: true }, take: 5, }); let hasQuotError = false; let hasNetworkErr = false; for (const c of failedSamples) { console.log(` ch#${c.id} (book ${c.bookId}) [${c.genStage}] "${c.title.slice(0, 30)}"`); console.log(` err: ${(c.contentError || '').slice(0, 200)}`); if (/429|quota|usage|rate limit|insufficient|usage limit/i.test(c.contentError || '')) { hasQuotError = true; } if (/timeout|ETIMEDOUT|ECONNRESET|fetch failed/i.test(c.contentError || '')) { hasNetworkErr = true; } } // 5. 卡住的检查 console.log('\n=== 5. 卡住的检查 ==='); const stuckGen = await p.book.count({ where: { genStage: 'content_generating', updatedAt: { lt: new Date(Date.now() - 30 * 60 * 1000) }, }, }); console.log(` 卡在 content_generating 超过 30 分钟: ${stuckGen}`); const stuckOutline = await p.book.count({ where: { genStage: 'outline_ready', updatedAt: { lt: new Date(Date.now() - 15 * 60 * 1000) }, }, }); console.log(` 卡在 outline_ready 超过 15 分钟: ${stuckOutline}`); const failedChaptersByTime = await p.bookChapter.count({ where: { genStage: 'failed', updatedAt: { lt: new Date(Date.now() - 10 * 60 * 1000) }, // 10 分钟还没恢复 }, }); console.log(` failed 超过 10 分钟未恢复: ${failedChaptersByTime}`); // 6. 健康总览 console.log('\n=== 6. 健康总览 ==='); console.log(` ✅ 数据库连接正常`); console.log(` 总书籍数: ${allBooks.length}(显示最近 10 本)`); console.log(` 是否有 429/quota 错误证据: ${hasQuotError ? '⚠️ 是(需要检查供应商切换)' : '✅ 否'}`); console.log(` 是否有网络错误证据: ${hasNetworkErr ? '⚠️ 是' : '✅ 否'}`); console.log(` 卡住的书(需要扫描器恢复): ${stuckGen + stuckOutline}`); console.log(` 5分钟内无法恢复的失败章节: ${failedChaptersByTime}`); console.log('\n=== 结论 ==='); if (stuckGen === 0 && stuckOutline === 0 && failedChaptersByTime === 0) { console.log(` ✅ 系统中没有明显的卡死任务,扫描器和切换逻辑工作正常`); } else { console.log(` ⚠️ 发现可能的卡死任务(但也可能正常在处理,需要看后续)`); } } catch (err) { console.error('❌ 测试异常:', err.message); process.exit(1); } finally { await p.$disconnect(); } })();