130 lines
4.2 KiB
TypeScript
130 lines
4.2 KiB
TypeScript
import { AppError } from '@/lib/error-model';
|
|
import { hostApiFetch } from '@/lib/host-api';
|
|
import {
|
|
IMAGE_PROMPT_MUSEUM_API_PATH,
|
|
type PromptMuseumEntry,
|
|
type PromptMuseumListQuery,
|
|
type PromptMuseumPage,
|
|
} from '../../shared/image-prompt-museum';
|
|
|
|
type PromptMuseumEnvelope<T> = {
|
|
success?: boolean;
|
|
status?: number;
|
|
code?: string;
|
|
error?: string;
|
|
data?: T;
|
|
};
|
|
|
|
type PromptMuseumMedia = {
|
|
dataBase64: string;
|
|
mimeType: string;
|
|
};
|
|
|
|
const PROMPT_MUSEUM_MEDIA_URL_PATTERN = /^\/api\/image-prompt-museum\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/media\/(?:thumbnail|[0-9]+)$/;
|
|
const PROMPT_MUSEUM_MEDIA_MIME_TYPES = new Set([
|
|
'image/avif',
|
|
'image/gif',
|
|
'image/jpeg',
|
|
'image/png',
|
|
'image/webp',
|
|
]);
|
|
const MAX_MEDIA_BASE64_LENGTH = Math.ceil((10 * 1024 * 1024) / 3) * 4;
|
|
|
|
export class PromptMuseumApiError extends Error {
|
|
readonly status: number;
|
|
readonly code: string;
|
|
|
|
constructor(status: number, code: string, message: string) {
|
|
super(message);
|
|
this.name = 'PromptMuseumApiError';
|
|
this.status = status;
|
|
this.code = code;
|
|
}
|
|
}
|
|
|
|
function errorStatus(error: unknown): number {
|
|
if (error instanceof AppError && typeof error.details?.status === 'number') {
|
|
return error.details.status;
|
|
}
|
|
return 502;
|
|
}
|
|
|
|
function errorCode(error: unknown, status: number): string {
|
|
if (error instanceof AppError && typeof error.details?.backendCode === 'string') {
|
|
return error.details.backendCode;
|
|
}
|
|
if (status === 401) return 'PROMPT_MUSEUM_AUTH_REQUIRED';
|
|
if (status === 404) return 'PROMPT_MUSEUM_NOT_FOUND';
|
|
return 'PROMPT_MUSEUM_REQUEST_FAILED';
|
|
}
|
|
|
|
function createQuery(query: PromptMuseumListQuery): string {
|
|
const params = new URLSearchParams();
|
|
const values: Array<[string, string | undefined]> = [
|
|
['q', query.q],
|
|
['use_case', query.useCase],
|
|
['style', query.style],
|
|
['subject', query.subject],
|
|
['language', query.language],
|
|
['model', query.model],
|
|
['cursor', query.cursor],
|
|
['limit', query.limit === undefined ? undefined : String(query.limit)],
|
|
];
|
|
for (const [key, value] of values) {
|
|
if (value?.trim()) params.set(key, value.trim());
|
|
}
|
|
const encoded = params.toString();
|
|
return encoded ? `?${encoded}` : '';
|
|
}
|
|
|
|
async function requestData<T>(path: string): Promise<T> {
|
|
let response: PromptMuseumEnvelope<T>;
|
|
try {
|
|
response = await hostApiFetch<PromptMuseumEnvelope<T>>(path);
|
|
} catch (error) {
|
|
const status = errorStatus(error);
|
|
throw new PromptMuseumApiError(
|
|
status,
|
|
errorCode(error, status),
|
|
error instanceof Error ? error.message : '提示词博物馆请求失败',
|
|
);
|
|
}
|
|
|
|
if (!response.success || response.data === undefined) {
|
|
throw new PromptMuseumApiError(
|
|
response.status ?? 502,
|
|
response.code ?? 'PROMPT_MUSEUM_REQUEST_FAILED',
|
|
response.error ?? '提示词博物馆请求失败',
|
|
);
|
|
}
|
|
return response.data;
|
|
}
|
|
|
|
export async function fetchPromptMuseumPage(query: PromptMuseumListQuery = {}): Promise<PromptMuseumPage> {
|
|
return await requestData(`${IMAGE_PROMPT_MUSEUM_API_PATH}${createQuery(query)}`);
|
|
}
|
|
|
|
export async function fetchPromptMuseumEntry(entryId: string): Promise<PromptMuseumEntry> {
|
|
return await requestData(`${IMAGE_PROMPT_MUSEUM_API_PATH}/${encodeURIComponent(entryId)}`);
|
|
}
|
|
|
|
export async function fetchPromptMuseumMedia(mediaUrl: string): Promise<string> {
|
|
if (!PROMPT_MUSEUM_MEDIA_URL_PATTERN.test(mediaUrl)) {
|
|
throw new PromptMuseumApiError(400, 'PROMPT_MUSEUM_INVALID_MEDIA_URL', '提示词图片地址无效');
|
|
}
|
|
const localPath = mediaUrl.replace('/api/image-prompt-museum/', `${IMAGE_PROMPT_MUSEUM_API_PATH}/`);
|
|
const payload = await hostApiFetch<PromptMuseumMedia>(localPath);
|
|
const mimeType = typeof payload?.mimeType === 'string' ? payload.mimeType.trim().toLowerCase() : '';
|
|
const dataBase64 = typeof payload?.dataBase64 === 'string' ? payload.dataBase64 : '';
|
|
if (
|
|
!PROMPT_MUSEUM_MEDIA_MIME_TYPES.has(mimeType)
|
|
|| !dataBase64
|
|
|| dataBase64.length > MAX_MEDIA_BASE64_LENGTH
|
|
|| !/^[A-Za-z0-9+/]*={0,2}$/.test(dataBase64)
|
|
|| dataBase64.length % 4 === 1
|
|
) {
|
|
throw new PromptMuseumApiError(502, 'PROMPT_MUSEUM_INVALID_MEDIA_RESPONSE', '提示词图片返回了无效数据');
|
|
}
|
|
return `data:${mimeType};base64,${dataBase64}`;
|
|
}
|