This commit is contained in:
2026-04-10 15:37:02 +08:00
parent 161cd32277
commit f19e482c8f
233 changed files with 43987 additions and 5127 deletions

199
packages/shared/src/http.ts Normal file
View File

@@ -0,0 +1,199 @@
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<string, unknown> | null;
};
export type ApiMeta = {
apiVersion: string;
requestId?: string;
routeVersion?: string;
operation?: string | null;
latencyMs?: number;
timestamp?: string;
};
export type ApiSuccessResponse<T> = {
ok: true;
data: T;
error: null;
meta: ApiMeta;
};
export type ApiErrorResponse = {
ok: false;
data: null;
error: ApiErrorPayload;
meta: ApiMeta;
};
export type ApiResponse<T> = ApiSuccessResponse<T> | ApiErrorResponse;
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function buildApiMeta(meta: Partial<ApiMeta> = {}): 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<T>(
data: T,
meta: Partial<ApiMeta> = {},
): ApiSuccessResponse<T> {
return {
ok: true,
data,
error: null,
meta: buildApiMeta(meta),
};
}
export function createApiError(
error: ApiErrorPayload,
meta: Partial<ApiMeta> = {},
): ApiErrorResponse {
return {
ok: false,
data: null,
error: {
code: error.code,
message: error.message,
details: error.details ?? null,
},
meta: buildApiMeta(meta),
};
}
export function isApiResponse<T>(value: unknown): value is ApiResponse<T> {
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<T>(value: ApiResponse<T> | T): T {
if (!isApiResponse<T>(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;
}