import { logError, logWarn } from './logger.js' const RESPONSE_TIME_ZONE = 'Asia/Shanghai' export type HttpErrorLike = { statusCode?: number errorCode?: string cause?: unknown context?: unknown [key: string]: unknown } type CreateHttpErrorOptions = { statusCode?: number errorCode?: string cause?: unknown context?: unknown } export function buildSuccessPayload(data, msg = 'ok') { return { code: 0, msg, time: Math.floor(Date.now() / 1000), data: normalizeResponseDateTime(data), } } export function buildErrorPayload(error, fallbackMessage) { const classification = classifyRouteError(error) return { code: 1, msg: error instanceof Error ? error.message : fallbackMessage, errorCode: classification.errorCode, time: Math.floor(Date.now() / 1000), data: null, } } export function sendRouteError(res, error, fallbackMessage, scope) { const classification = classifyRouteError(error) const logger = classification.statusCode >= 500 ? logError : logWarn logger(scope, fallbackMessage, { statusCode: classification.statusCode, errorCode: classification.errorCode, error, }) res.status(classification.statusCode).json(buildErrorPayload(error, fallbackMessage)) } export function buildNotFoundPayload(req) { return { code: 1, msg: `未实现接口: ${req.method} ${req.originalUrl}`, time: Math.floor(Date.now() / 1000), data: null, } } export function createHttpError(message: string, options: CreateHttpErrorOptions = {}): Error & HttpErrorLike { const { statusCode = 500, errorCode = '', cause, context } = options const error = new Error(message) as Error & HttpErrorLike error.statusCode = statusCode error.errorCode = errorCode || defaultErrorCodeForStatus(statusCode) if (typeof cause !== 'undefined') { error.cause = cause } if (typeof context !== 'undefined') { error.context = context } return error } function classifyRouteError(error) { if (error && typeof error === 'object') { const currentError = error as HttpErrorLike const statusCode = Number(currentError.statusCode) const errorCode = String(currentError.errorCode || '').trim() if (Number.isInteger(statusCode) && statusCode >= 400 && statusCode < 600) { return { statusCode, errorCode: errorCode || defaultErrorCodeForStatus(statusCode), } } } const message = error instanceof Error ? error.message : String(error || '') if (message.includes('缺少') || message.includes('无效') || message.includes('验签失败')) { return { statusCode: 400, errorCode: 'invalid_request' } } if (message.includes('不存在')) { return { statusCode: 404, errorCode: 'not_found' } } if (message.includes('已关闭')) { return { statusCode: 410, errorCode: 'gone' } } if (message.includes('不能') || message.includes('冲突')) { return { statusCode: 409, errorCode: 'conflict' } } if ( message.includes('OCR') || message.includes('uv sync') || message.includes('spawn uv ENOENT') || message.includes("Executable doesn't exist") || message.includes('please run the following command to download new browsers') ) { return { statusCode: 503, errorCode: 'dependency_unavailable' } } return { statusCode: 500, errorCode: 'internal_error' } } function defaultErrorCodeForStatus(statusCode) { if (statusCode === 400) { return 'invalid_request' } if (statusCode === 401) { return 'unauthorized' } if (statusCode === 403) { return 'forbidden' } if (statusCode === 404) { return 'not_found' } if (statusCode === 409) { return 'conflict' } if (statusCode === 410) { return 'gone' } if (statusCode === 503) { return 'dependency_unavailable' } return 'internal_error' } function normalizeResponseDateTime(value) { if (value instanceof Date) { return formatResponseDateTime(value.toISOString()) } if (Array.isArray(value)) { return value.map((item) => normalizeResponseDateTime(item)) } if (value && typeof value === 'object') { return Object.fromEntries( Object.entries(value).map(([key, currentValue]) => [key, normalizeResponseDateTime(currentValue)]), ) } if (isIsoDateTimeString(value)) { return formatResponseDateTime(value) } return value } function isIsoDateTimeString(value) { return ( typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/.test(value.trim()) ) } function formatResponseDateTime(value) { const date = new Date(value) if (Number.isNaN(date.getTime())) { return String(value || '').replace('T', ' ').replace('Z', '') } const parts = new Intl.DateTimeFormat('zh-CN', { timeZone: RESPONSE_TIME_ZONE, hour12: false, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', }).formatToParts(date) const tokens = Object.fromEntries( parts .filter((item) => item.type !== 'literal') .map((item) => [item.type, item.value]), ) return `${tokens.year}-${tokens.month}-${tokens.day} ${tokens.hour}:${tokens.minute}:${tokens.second}` }