import type { Request, Response } from "express"; 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: unknown, msg: string = "ok") { return { code: 0, msg, time: Math.floor(Date.now() / 1000), data: normalizeResponseDateTime(data), }; } export function buildErrorPayload(error: unknown, fallbackMessage: string) { 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: Response, error: unknown, fallbackMessage: string, scope: string ): void { 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: Request) { 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: unknown): { statusCode: number; errorCode: string; } { 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: number): string { 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: unknown): unknown { 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 as string); } return value; } function isIsoDateTimeString(value: unknown): boolean { 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: string): string { 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}`; } type RouteHandlerOptions = { successMessage?: string; errorMessage?: string; scope?: string; }; type RouteFileHandlerOptions = { errorMessage?: string; scope?: string; }; export function createRouteHandler( action: (req: Request, res: Response) => unknown | Promise, { successMessage = "ok", errorMessage, scope }: RouteHandlerOptions = {} ) { return async (req: Request, res: Response): Promise => { try { const data = await action(req, res); res.json(buildSuccessPayload(data, successMessage)); } catch (error) { sendRouteError(res, error, errorMessage, scope); } }; } export function createRouteFileHandler( action: (req: Request, res: Response) => string | Promise, { errorMessage, scope }: RouteFileHandlerOptions = {} ) { return async (req: Request, res: Response): Promise => { try { const filePath = await action(req, res); res.sendFile(filePath); } catch (error) { sendRouteError(res, error, errorMessage, scope); } }; }