/** 开发环境终端日志着色(ANSI),生产结构化日志不走此处 */ const ANSI = { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', gray: '\x1b[90m', } as const export function isDevLogColorEnabled(stream?: { isTTY?: boolean }): boolean { if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '') return false const force = process.env.FORCE_COLOR if (force === '1' || force === 'true') return true if (force === '0' || force === 'false') return false return stream?.isTTY ?? process.stdout?.isTTY ?? false } function paint(text: string, ...styles: string[]): string { if (!isDevLogColorEnabled()) return text return `${styles.join('')}${text}${ANSI.reset}` } export function colorizeHttpStatus(status: number): string { if (status >= 500) return paint(String(status), ANSI.bold, ANSI.red) if (status >= 400) return paint(String(status), ANSI.bold, ANSI.yellow) if (status >= 300) return paint(String(status), ANSI.cyan) return paint(String(status), ANSI.bold, ANSI.green) } export interface DevSqlLogParts { tag?: string durationMs: number rowCount: number runnableSql: string } export function formatDevSqlLog(parts: DevSqlLogParts): string { const label = parts.tag ? `[SQL] ${parts.tag}` : '[SQL]' const header = [ paint(label, ANSI.bold, ANSI.cyan), paint(`${parts.durationMs}ms`, ANSI.dim, ANSI.gray), paint('rows=', ANSI.dim, ANSI.gray) + paint(String(parts.rowCount), ANSI.blue), ].join(' ') const sql = paint(parts.runnableSql, ANSI.gray) return `${header}\n${sql}` } export interface DevApiLogParts { method: string path: string status: number durationMs: number } export function formatDevApiLog(parts: DevApiLogParts): string { return [ paint('[API]', ANSI.bold, ANSI.magenta), paint(parts.method, ANSI.bold, ANSI.yellow), paint(parts.path, ANSI.cyan), '->', colorizeHttpStatus(parts.status), paint(`(${parts.durationMs}ms)`, ANSI.dim, ANSI.gray), ].join(' ') } export function formatDevBanner(message: string): string { return paint(message, ANSI.bold, ANSI.green) } export type DevLogLevel = 'info' | 'warn' | 'error' export function formatDevAppLog(level: DevLogLevel, message: string, data?: unknown): string { const tag = level === 'error' ? paint('[ERROR]', ANSI.bold, ANSI.red) : level === 'warn' ? paint('[WARN]', ANSI.bold, ANSI.yellow) : paint('[INFO]', ANSI.bold, ANSI.blue) const body = paint(message, ANSI.reset) if (data === undefined || data === '') return `${tag} ${body}` const extra = typeof data === 'string' ? data : JSON.stringify(data, null, 2) return `${tag} ${body}\n${paint(extra, ANSI.dim, ANSI.gray)}` }