export const API_VERSION = '2026-04-08'; export const API_RESPONSE_ENVELOPE_HEADER = 'x-genarrative-response-envelope'; export const API_RESPONSE_ENVELOPE_VERSION = 'v1'; export type ApiErrorCode = | 'BAD_REQUEST' | 'INVALID_REQUEST' | 'VALIDATION_ERROR' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'NOT_FOUND' | 'CONFLICT' | 'UPSTREAM_ERROR' | 'INTERNAL_SERVER_ERROR' | 'bad_request' | 'validation_error' | 'unauthorized' | 'forbidden' | 'not_found' | 'conflict' | 'upstream_error' | 'internal_error' | (string & {}); export type ApiErrorPayload = { code: ApiErrorCode; message: string; details?: Record | null; }; export type ApiMeta = { apiVersion: string; requestId?: string; routeVersion?: string; operation?: string | null; latencyMs?: number; timestamp?: string; }; export type ApiSuccessResponse = { ok: true; data: T; error: null; meta: ApiMeta; }; export type ApiErrorResponse = { ok: false; data: null; error: ApiErrorPayload; meta: ApiMeta; }; export type ApiResponse = ApiSuccessResponse | ApiErrorResponse; function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null; } function buildApiMeta(meta: Partial = {}): ApiMeta { return { apiVersion: meta.apiVersion ?? API_VERSION, requestId: typeof meta.requestId === 'string' && meta.requestId.trim() ? meta.requestId.trim() : undefined, routeVersion: typeof meta.routeVersion === 'string' && meta.routeVersion.trim() ? meta.routeVersion.trim() : undefined, operation: typeof meta.operation === 'string' && meta.operation.trim() ? meta.operation.trim() : meta.operation === null ? null : undefined, latencyMs: typeof meta.latencyMs === 'number' && Number.isFinite(meta.latencyMs) ? meta.latencyMs : undefined, timestamp: typeof meta.timestamp === 'string' && meta.timestamp.trim() ? meta.timestamp.trim() : undefined, }; } export function createApiSuccess( data: T, meta: Partial = {}, ): ApiSuccessResponse { return { ok: true, data, error: null, meta: buildApiMeta(meta), }; } export function createApiError( error: ApiErrorPayload, meta: Partial = {}, ): ApiErrorResponse { return { ok: false, data: null, error: { code: error.code, message: error.message, details: error.details ?? null, }, meta: buildApiMeta(meta), }; } export function isApiResponse(value: unknown): value is ApiResponse { if (!isRecord(value) || typeof value.ok !== 'boolean' || !('meta' in value)) { return false; } if (!isRecord(value.meta) || typeof value.meta.apiVersion !== 'string') { return false; } if (value.ok) { return 'data' in value && value.error === null; } return ( value.data === null && isRecord(value.error) && typeof value.error.code === 'string' && typeof value.error.message === 'string' ); } export function unwrapApiResponse(value: ApiResponse | T): T { if (!isApiResponse(value)) { return value as T; } if (value.ok) { return value.data; } throw new Error(value.error.message || '请求失败'); } export function parseApiErrorMessage(rawText: string, fallbackMessage: string) { if (!rawText.trim()) { return fallbackMessage; } try { const parsed = JSON.parse(rawText) as | ApiErrorResponse | { error?: { message?: string; code?: string; }; message?: string; code?: string; }; if ( typeof parsed.error?.message === 'string' && parsed.error.message.trim() ) { return parsed.error.message.trim(); } const topLevelMessage = 'message' in parsed && typeof parsed.message === 'string' ? parsed.message.trim() : ''; if (topLevelMessage) { return topLevelMessage; } const errorCode = typeof parsed.error?.code === 'string' && parsed.error.code.trim() ? parsed.error.code.trim() : 'code' in parsed && typeof parsed.code === 'string' && parsed.code.trim() ? parsed.code.trim() : ''; if (errorCode) { return `${fallbackMessage}(${errorCode})`; } } catch { // Ignore malformed json responses. } return rawText.trim() || fallbackMessage; }