import type { IncomingMessage, ServerResponse } from 'node:http'; import type { HostApiContext } from '../context'; import { sendJson } from '../route-utils'; import { WORKS_SQUARE_CONFIG } from '../works-config'; import { getValidWorksSquareAccessToken } from '../../services/works-square-session'; import { proxyAwareFetch } from '../../utils/proxy-fetch'; const LOCAL_ROOT = '/api/works/image-prompt-museum'; const UPSTREAM_ROOT = '/api/image-prompt-museum'; const LIST_QUERY_KEYS = ['q', 'use_case', 'style', 'subject', 'language', 'model', 'cursor', 'limit'] as const; const MAX_QUERY_VALUE_LENGTH = 256; type PromptMuseumRouteDependencies = { fetchImpl?: typeof fetch; getAccessToken?: typeof getValidWorksSquareAccessToken; apiBaseUrl?: string; }; function isMuseumPath(pathname: string): boolean { return pathname === LOCAL_ROOT || /^\/api\/works\/image-prompt-museum\/[^/]+$/.test(pathname); } function upstreamPath(pathname: string): string | null { if (pathname === LOCAL_ROOT) return UPSTREAM_ROOT; const entryId = pathname.slice(`${LOCAL_ROOT}/`.length); if (!entryId) return null; return `${UPSTREAM_ROOT}/${encodeURIComponent(decodeURIComponent(entryId))}`; } function copyAllowedQuery(source: URL): string { const query = new URLSearchParams(); for (const key of LIST_QUERY_KEYS) { const value = source.searchParams.get(key)?.trim(); if (!value || value.length > MAX_QUERY_VALUE_LENGTH) continue; query.set(key, value); } const encoded = query.toString(); return encoded ? `?${encoded}` : ''; } async function readPayload(response: Response): Promise { try { return await response.json(); } catch { return null; } } function errorMessage(payload: unknown, fallback: string): string { if (typeof payload === 'object' && payload !== null && !Array.isArray(payload)) { const value = (payload as Record).error; if (typeof value === 'string' && value.trim()) return value; } return fallback; } export function createImagePromptMuseumRouteHandler( dependencies: PromptMuseumRouteDependencies = {}, ) { const fetchImpl = dependencies.fetchImpl ?? proxyAwareFetch; const getAccessToken = dependencies.getAccessToken ?? getValidWorksSquareAccessToken; const apiBaseUrl = (dependencies.apiBaseUrl ?? WORKS_SQUARE_CONFIG.apiBaseUrl).replace(/\/+$/, ''); return async function handleImagePromptMuseumRoutes( req: IncomingMessage, res: ServerResponse, url: URL, _ctx: HostApiContext, ): Promise { if (!isMuseumPath(url.pathname)) return false; if (req.method !== 'GET') { sendJson(res, 405, { success: false, code: 'PROMPT_MUSEUM_METHOD_NOT_ALLOWED', error: '提示词博物馆只支持读取', }); return true; } const path = upstreamPath(url.pathname); if (!path) { sendJson(res, 404, { success: false, code: 'PROMPT_MUSEUM_NOT_FOUND', error: '提示词条目不存在', }); return true; } try { const token = await getAccessToken({ fetchImpl }); if (!token) { sendJson(res, 401, { success: false, status: 401, code: 'PROMPT_MUSEUM_AUTH_REQUIRED', error: '请先登录后再获取灵感', }); return true; } const request = (accessToken: string) => fetchImpl( `${apiBaseUrl}${path}${url.pathname === LOCAL_ROOT ? copyAllowedQuery(url) : ''}`, { method: 'GET', headers: { Accept: 'application/json', Authorization: `Bearer ${accessToken}`, }, redirect: 'manual', }, ); let response = await request(token); if (response.status === 401) { await response.body?.cancel().catch(() => undefined); const refreshed = await getAccessToken({ fetchImpl, forceRefresh: true }); if (refreshed) response = await request(refreshed); } const payload = await readPayload(response); if (!response.ok) { sendJson(res, response.status, { success: false, status: response.status, code: typeof payload === 'object' && payload !== null && !Array.isArray(payload) && typeof (payload as Record).code === 'string' ? (payload as Record).code : response.status === 404 ? 'PROMPT_MUSEUM_NOT_FOUND' : 'PROMPT_MUSEUM_UNAVAILABLE', error: errorMessage(payload, '提示词博物馆暂时不可用'), }); return true; } if (payload === null) { sendJson(res, 502, { success: false, status: 502, code: 'PROMPT_MUSEUM_INVALID_RESPONSE', error: '提示词博物馆返回了无效数据', }); return true; } sendJson(res, response.status, payload); return true; } catch (error) { sendJson(res, 502, { success: false, status: 502, code: 'PROMPT_MUSEUM_UNAVAILABLE', error: error instanceof Error ? error.message : '提示词博物馆暂时不可用', }); return true; } }; } export const handleImagePromptMuseumRoutes = createImagePromptMuseumRouteHandler();