/** * TTS 请求日志完整性验证脚本 * * 目的: * 1. 直接发 N 次 TTS API 请求(MiniMax + 阿里云) * 2. 验证每次请求都写入了 AiCallLog 数据库表 * 3. 对比日志文件中的请求次数与数据库记录 * 4. 检测是否有异常的大量请求或日志遗漏 * * 用法:npx ts-node scripts/test-tts-logging.ts */ import axios from 'axios'; import { prisma } from '../src/models'; import { withAiLog } from '../src/services/ai-call-logger'; import * as fs from 'fs'; import * as path from 'path'; // ============ 配置 ============ const TEST_COUNT = 10; // 每个供应商发多少条请求 const LOG_DIR = path.join(process.cwd(), '..', '..'); const LOG_FILE = path.join(process.cwd(), '..', 'tts-debug.log'); // 从 models.json 读取 API Key const modelsConfig = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'src', 'config', 'models.json'), 'utf-8')); const minimaxVendor = modelsConfig.vendors?.minimax; const bailianVendor = modelsConfig.vendors?.bailian; const MINIMAX_API_KEY = minimaxVendor?.apiKey || ''; const BAILIAN_API_KEY = bailianVendor?.apiKey || ''; // ============ 主流程 ============ async function main() { console.log('╔══════════════════════════════════════════╗'); console.log('║ TTS 请求日志完整性验证脚本 ║'); console.log('╚══════════════════════════════════════════╝\n'); // ---- Step 1: 记录测试前的数据库基线 ---- const beforeCount = await prisma.aiCallLog.count(); console.log(`[基线] 测试前 AiCallLog 总记录数: ${beforeCount}`); // ---- Step 2: 发 MiniMax TTS 请求(createTask)---- console.log(`\n[测试] 发送 ${TEST_COUNT} 次 MiniMax createTask 请求...`); const miniMaxResults: string[] = []; for (let i = 0; i < TEST_COUNT; i++) { try { await withAiLog( () => axios.post('https://api.minimaxi.com/v1/t2a_async_v2', { model: 'speech-2.8-hd', text: `测试文本_${i + 1},用于验证TTS请求日志完整性。`.repeat(5), voice_setting: { voice_id: 'audiobook_female_1', speed: 1, vol: 1, pitch: 1 }, audio_setting: { audio_sample_rate: 32000, bitrate: 128000, format: 'mp3', channel: 1 }, }, { headers: { 'Authorization': `Bearer ${MINIMAX_API_KEY}`, 'Content-Type': 'application/json' }, timeout: 10000, }), { callType: 'tts_test_create', provider: 'minimax', model: 'speech-2.8-hd', textLen: 200 } ); miniMaxResults.push(`#${i + 1}: 成功`); } catch (err: any) { const msg = err?.message?.substring(0, 60) || '未知错误'; miniMaxResults.push(`#${i + 1}: 失败(${msg})`); } } // ---- Step 3: 发阿里云 TTS 请求(synthesize)---- console.log(`\n[测试] 发送 ${TEST_COUNT} 次阿里云 synthesize 请求...`); const aliyunResults: string[] = []; for (let i = 0; i < TEST_COUNT; i++) { try { await withAiLog( () => axios.post('https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation', { model: 'qwen3-tts-instruct-flash', input: { text: `测试文本_${i + 1}`.repeat(10), voice: 'Cherry', language_type: 'Chinese' }, }, { headers: { 'Authorization': `Bearer ${BAILIAN_API_KEY}`, 'Content-Type': 'application/json' }, timeout: 10000, }), { callType: 'tts_test_synthesize', provider: 'bailian', model: 'qwen3-tts-instruct-flash', textLen: 100 } ); aliyunResults.push(`#${i + 1}: 成功`); } catch (err: any) { const msg = err?.message?.substring(0, 60) || '未知错误'; aliyunResults.push(`#${i + 1}: 失败(${msg})`); } } // ---- Step 4: 等异步写入落库 ---- console.log('\n⏳ 等待异步日志写入...'); await new Promise(r => setTimeout(r, 3000)); // ---- Step 5: 数据库验证 ---- const afterCount = await prisma.aiCallLog.count(); const newRecords = afterCount - beforeCount; const miniMaxDbCount = await prisma.aiCallLog.count({ where: { callType: 'tts_test_create' } }); const aliyunDbCount = await prisma.aiCallLog.count({ where: { callType: 'tts_test_synthesize' } }); const totalExpected = TEST_COUNT * 2; // MiniMax + 阿里云 const totalActual = miniMaxDbCount + aliyunDbCount; console.log('\n╔══════════════════════════════════════════╗'); console.log('║ 📊 测试结果报告 ║'); console.log('╚══════════════════════════════════════════╝'); console.log(`\n 发送请求总数: ${totalExpected} (MiniMax ${TEST_COUNT} + 阿里云 ${TEST_COUNT})`); console.log(` DB 新增记录: ${newRecords}`); console.log(` MiniMax 入库: ${miniMaxDbCount} / ${TEST_COUNT}`); console.log(` 阿里云 入库: ${aliyunDbCount} / ${TEST_COUNT}`); console.log(` 实际入库总数: ${totalActual} / ${totalExpected}`); console.log(` 遗漏率: ${totalExpected - totalActual > 0 ? ((totalExpected - totalActual) / totalExpected * 100).toFixed(1) + '%' : '0%'}`); // ---- 详细信息 ---- const recentLogs = await prisma.aiCallLog.findMany({ where: { callType: { in: ['tts_test_create', 'tts_test_synthesize'] } }, orderBy: { createdAt: 'desc' }, take: 5, select: { id: true, callType: true, provider: true, success: true, errorMsg: true, duration: true, createdAt: true } }); console.log(`\n 最近 5 条测试日志:`); for (const log of recentLogs) { const status = log.success ? '✅' : '❌'; const err = log.errorMsg ? ` (${log.errorMsg.substring(0, 40)})` : ''; console.log(` ${status} #${log.id} ${log.callType} | ${log.provider} | ${log.duration}ms${err}`); } // ---- Step 6: 检查是否有异常大量的历史 TTS 请求 ---- const last24h = new Date(Date.now() - 24 * 60 * 60 * 1000); const last24hTtsCalls = await prisma.aiCallLog.count({ where: { callType: { startsWith: 'tts' }, createdAt: { gte: last24h }, } }); const ttsBreakdown = await prisma.aiCallLog.groupBy({ by: ['callType', 'provider'], where: { callType: { startsWith: 'tts' }, createdAt: { gte: last24h }, }, _count: { id: true }, }); ttsBreakdown.sort((a, b) => b._count.id - a._count.id); console.log(`\n\n╔══════════════════════════════════════════╗`); console.log(`║ 📊 过去 24h TTS 请求统计 ║`); console.log(`╚══════════════════════════════════════════╝`); console.log(`\n 24h TTS 总请求数: ${last24hTtsCalls}`); console.log(`\n 按类型/供应商拆分:`); for (const item of ttsBreakdown.slice(0, 15)) { console.log(` ${item.callType.padEnd(25)} | ${(item.provider || '-').padEnd(15)} | ${String(item._count.id).padStart(6)} 次`); } // ---- Step 7: 对比日志文件 ---- let logFileCount = 0; try { if (fs.existsSync(LOG_FILE)) { const logContent = fs.readFileSync(LOG_FILE, 'utf-8'); logFileCount = (logContent.match(/\[TTS\]/g) || []).length; console.log(`\n tts-debug.log 文件中的 [TTS] 标记数: ${logFileCount}`); } else { console.log(`\n ⚠️ tts-debug.log 文件不存在`); } } catch (e) { console.log(`\n ⚠️ 无法读取 tts-debug.log: ${(e as Error).message}`); } // ---- 结论 ---- console.log(`\n\n╔══════════════════════════════════════════╗`); console.log(`║ 🎯 结论 ║`); console.log(`╚══════════════════════════════════════════╝`); if (totalActual === totalExpected) { console.log(` ✅ 日志记录完整:${totalActual}/${totalExpected} 全部入库,0%遗漏`); } else if (totalActual >= totalExpected * 0.9) { console.log(` ⚠️ 轻微遗漏:${totalExpected - totalActual} 条未入库(${((1 - totalActual/totalExpected)*100).toFixed(1)}%)`); } else { console.log(` ❌ 严重遗漏:${totalExpected - totalActual} 条未入库(${((1 - totalActual/totalExpected)*100).toFixed(1)}%)`); } if (last24hTtsCalls > 1000) { console.log(` ⚠️ 过去24h有 ${last24hTtsCalls} 次TTS请求,次数可能异常偏高,请检查死循环`); } else { console.log(` ✅ 过去24h TTS请求 ${last24hTtsCalls} 次,在正常范围内`); } // 清理 await prisma.$disconnect(); } main().catch(err => { console.error('脚本异常:', err); prisma.$disconnect().then(() => process.exit(1)); });