feat: 完善图像工作区与创作工具体验
This commit is contained in:
158
electron/api/routes/image-prompt-museum.ts
Normal file
158
electron/api/routes/image-prompt-museum.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
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<unknown> {
|
||||
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<string, unknown>).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<boolean> {
|
||||
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<string, unknown>).code === 'string'
|
||||
? (payload as Record<string, unknown>).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();
|
||||
Reference in New Issue
Block a user