import { randomUUID } from 'node:crypto'; import { ApiResponse } from './ApiResponse'; import { BizException } from './BizException'; import { ErrorCodes } from './ErrorCodes'; export type ServiceParams = Record; type RequestLike = { headers?: { get(name: string): string | null; }; nextUrl?: { searchParams: URLSearchParams; }; json?: () => Promise; text?: () => Promise; }; export function readQueryParams(req: unknown): ServiceParams { const searchParams = (req as RequestLike).nextUrl?.searchParams; if (!searchParams) { return {}; } const params: ServiceParams = {}; searchParams.forEach((value, key) => { params[key] = value; }); return params; } export async function readBodyParams(req: unknown): Promise { const request = req as RequestLike; const contentType = request.headers?.get('content-type') ?? ''; const queryParams = readQueryParams(req); if (contentType.includes('application/json') && request.json) { const body = await request.json(); return { ...queryParams, ...normalizeParams(body) }; } if (request.text) { const text = await request.text(); if (!text) { return queryParams; } if (contentType.includes('application/x-www-form-urlencoded')) { return { ...queryParams, ...Object.fromEntries(new URLSearchParams(text)) }; } try { return { ...queryParams, ...normalizeParams(JSON.parse(text)) }; } catch { return queryParams; } } return queryParams; } export async function runService(operation: () => Promise): Promise { try { const result = await operation(); if (result instanceof Response) { return result; } if (isLegacyBackendResponse(result)) { return Response.json(result); } return ApiResponse.success(result); } catch (error) { if (error instanceof BizException) { const status = error.code === ErrorCodes.CONFIG_MISSING ? 500 : error.code === ErrorCodes.UNAUTHORIZED ? 401 : error.code === ErrorCodes.FORBIDDEN ? 403 : 200; return ApiResponse.error(error.code, error.message, null, status); } // 未预期的异常:细节只进服务端日志,回给调用方的只有一个事件号。 // 实测泄漏面:声明文件损坏时,`[PageRegistryStore] 声明文件损坏(非法 JSON): // /home/…/data/soma-runtime/pages/x.json` 会被原样回给任何匿名调用方—— // 绝对路径、目录结构、部署形态一并奉送。已预期的业务错误(BizException / // 各域的 AiApiError)文案是写给调用方看的,不受这条影响。 const eventId = randomUUID().slice(0, 8); console.error( `[soma-ai][接口] 事件 ${eventId} 未处理异常:`, error instanceof Error ? (error.stack ?? error.message) : String(error), ); return ApiResponse.error( ErrorCodes.INTERNAL_ERROR, `服务器内部错误(事件 ${eventId},详情见服务端日志)`, ); } } function isLegacyBackendResponse(value: unknown): value is Record { if (!value || typeof value !== 'object' || Array.isArray(value)) { return false; } const source = value as Record; return (typeof source.ret === 'number' || typeof source.code === 'number') && ('data' in source || 'msg' in source || 'message' in source); } function normalizeParams(body: unknown): ServiceParams { if (!body || typeof body !== 'object' || Array.isArray(body)) { return {}; } return body as ServiceParams; }