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 = { 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(path: string): Promise { let response: PromptMuseumEnvelope; try { response = await hostApiFetch>(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 { return await requestData(`${IMAGE_PROMPT_MUSEUM_API_PATH}${createQuery(query)}`); } export async function fetchPromptMuseumEntry(entryId: string): Promise { return await requestData(`${IMAGE_PROMPT_MUSEUM_API_PATH}/${encodeURIComponent(entryId)}`); } export async function fetchPromptMuseumMedia(mediaUrl: string): Promise { 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(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}`; }