import fs from 'fs/promises'; import path from 'path'; import config from '../config/config.js'; import logger from '../utils/logger.js'; export const DEBUG_DUMP_FILE = path.join(process.cwd(), 'data', 'debug-dump.log'); export function isDebugDumpEnabled() { return config.debugDumpRequestResponse === true; } let dumpDirEnsured = false; async function ensureDumpDir() { if (dumpDirEnsured) return; await fs.mkdir(path.dirname(DEBUG_DUMP_FILE), { recursive: true }); dumpDirEnsured = true; } function getTimestamp() { const now = new Date(); const pad2 = (n) => String(n).padStart(2, '0'); const pad3 = (n) => String(n).padStart(3, '0'); return `${now.getFullYear()}-${pad2(now.getMonth() + 1)}-${pad2(now.getDate())} ` + `${pad2(now.getHours())}:${pad2(now.getMinutes())}:${pad2(now.getSeconds())}.${pad3(now.getMilliseconds())}`; } export function createDumpId(prefix = 'dump') { const rand = Math.random().toString(16).slice(2, 10); return `${prefix}-${rand}`; } async function appendDumpLog(content) { await ensureDumpDir(); await fs.appendFile(DEBUG_DUMP_FILE, content, 'utf8'); } export function createStreamCollector() { return { chunks: [] }; } export function collectStreamChunk(collector, chunk) { if (collector) collector.chunks.push(chunk); } export async function dumpFinalRequest(dumpId, requestBody) { if (!isDebugDumpEnabled()) return; try { const json = JSON.stringify(requestBody, null, 2); const header = `\n${'='.repeat(80)}\n[${getTimestamp()}] REQUEST ${dumpId}\n${'='.repeat(80)}\n`; await appendDumpLog(header + json + '\n'); logger.warn(`[DEBUG_DUMP ${dumpId}] 已写入请求体到: ${DEBUG_DUMP_FILE}`); } catch (e) { logger.error(`[DEBUG_DUMP ${dumpId}] 写入请求体失败:`, e?.message || e); } } export async function dumpStreamResponse(dumpId, collector) { if (!isDebugDumpEnabled() || !collector) return; try { const header = `\n${'-'.repeat(80)}\n[${getTimestamp()}] RESPONSE ${dumpId} (STREAM)\n${'-'.repeat(80)}\n`; const rawContent = collector.chunks.join(''); const jsonObjects = []; const lines = rawContent.split('\n'); for (const line of lines) { const trimmed = line.trim(); if (trimmed.startsWith('data:')) { const dataStr = trimmed.slice(5).trim(); if (dataStr && dataStr !== '[DONE]') { try { const parsed = JSON.parse(dataStr); jsonObjects.push(parsed); } catch { jsonObjects.push({ raw: dataStr }); } } } } const jsonOutput = JSON.stringify(jsonObjects, null, 2); const footer = `\n[${getTimestamp()}] END ${dumpId}\n`; await appendDumpLog(header + jsonOutput + footer); logger.warn(`[DEBUG_DUMP ${dumpId}] 响应记录完成 (${jsonObjects.length} 条数据)`); } catch (e) { logger.error(`[DEBUG_DUMP ${dumpId}] 写入流式响应失败:`, e?.message || e); } } export async function dumpFinalRawResponse(dumpId, rawText) { if (!isDebugDumpEnabled()) return; try { const header = `\n${'-'.repeat(80)}\n[${getTimestamp()}] RESPONSE ${dumpId} (NO-STREAM)\n${'-'.repeat(80)}\n`; const footer = `\n[${getTimestamp()}] END ${dumpId}\n`; await appendDumpLog(header + (rawText ?? '') + footer); logger.warn(`[DEBUG_DUMP ${dumpId}] 响应记录完成`); } catch (e) { logger.error(`[DEBUG_DUMP ${dumpId}] 写入响应失败:`, e?.message || e); } }