import fs from 'node:fs/promises' import path from 'node:path' import process from 'node:process' import { PROJECT_ROOT, runtimeConfig } from '../config/runtime.js' const DATA_ROOT = resolveDataRoot() const LOG_DIR = path.join(DATA_ROOT, 'logs') const APP_LOG_FILE = path.join(LOG_DIR, 'app.log') const WEBHOOK_LOG_FILE = path.join(LOG_DIR, 'webhook.log') let writeQueue = Promise.resolve() export function createRequestId(prefix = 'req') { return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}` } export function logDebug(scope, message, detail) { return writeLog('debug', scope, message, detail) } export function logInfo(scope, message, detail) { return writeLog('info', scope, message, detail) } export function logWarn(scope, message, detail) { return writeLog('warn', scope, message, detail) } export function logError(scope, message, detail) { return writeLog('error', scope, message, detail) } export function logWebhook(scope, message, detail, { level = 'info' } = {}) { return writeLog(level, scope, message, detail, { channel: 'webhook' }) } function writeLog(level, scope, message, detail, { channel = 'app' } = {}) { const entry = { time: new Date().toISOString(), level, scope: String(scope || 'app').trim() || 'app', message: String(message || '').trim() || '-', pid: process.pid, detail: normalizeLogValue(detail), } writeConsole(entry) enqueueFileWrite(entry, channel === 'webhook' ? WEBHOOK_LOG_FILE : APP_LOG_FILE) return entry } function enqueueFileWrite(entry, filePath) { const line = JSON.stringify(entry) writeQueue = writeQueue .then(async () => { await fs.mkdir(LOG_DIR, { recursive: true }) await fs.appendFile(filePath, `${line}\n`, 'utf8') }) .catch((error) => { const reason = error instanceof Error ? error.message : String(error || '未知错误') console.error('[logger] failed to write log file:', reason) }) } function writeConsole(entry) { const prefix = `[${entry.time}] [${entry.level}] [${entry.scope}] ${entry.message}` const logger = resolveConsoleMethod(entry.level) if (typeof entry.detail === 'undefined') { logger(prefix) return } logger(prefix, entry.detail) } function resolveConsoleMethod(level) { if (level === 'error') { return console.error } if (level === 'warn') { return console.warn } return console.log } function resolveDataRoot() { const configured = String(runtimeConfig.data?.root || '').trim() return configured ? path.resolve(configured) : path.resolve(PROJECT_ROOT, 'data') } function normalizeLogValue(value) { if (typeof value === 'undefined') { return undefined } try { return JSON.parse( JSON.stringify(value, (_key, current) => { if (current instanceof Error) { return { name: current.name, message: current.message, stack: current.stack, statusCode: current.statusCode, errorCode: current.errorCode, } } if (typeof current === 'bigint') { return String(current) } return current }), ) } catch { return String(value) } }