Files
makelore/src/lib/works-image.ts
2026-07-29 17:22:35 +08:00

120 lines
3.6 KiB
TypeScript

import { hostApiFetch } from '@/lib/host-api';
export type WorksImageAspectRatio = '1:1' | '4:3' | '16:9' | '9:16';
export type WorksImageQuality = 'standard' | 'high';
export type WorksImageJobStatus =
| 'queued'
| 'running'
| 'succeeded'
| 'failed'
| 'expired'
| 'cancelled'
| string;
export type WorksImageJob = {
id: string;
status: WorksImageJobStatus;
prompt?: string;
outputUrls?: string[];
error?: string | { message?: string } | null;
createdAt?: string;
updatedAt?: string;
};
export type WorksImageGenerationInput = {
accessToken: string;
prompt: string;
aspectRatio: WorksImageAspectRatio;
quality: WorksImageQuality;
};
type WorksImageResponse<TField extends string, TValue> = {
success: boolean;
status?: number;
error?: string;
} & {
[key in TField]?: TValue;
};
export class WorksImageApiError extends Error {
readonly statusCode?: number;
constructor(message: string, statusCode?: number) {
super(message);
this.name = 'WorksImageApiError';
this.statusCode = statusCode;
}
}
function assertSuccess<TField extends string, TValue>(
response: WorksImageResponse<TField, TValue>,
field: TField,
fallback: string,
): TValue {
if (!response.success || response[field] === undefined) {
throw new WorksImageApiError(response.error || fallback, response.status);
}
return response[field] as TValue;
}
function authHeaders(accessToken: string): Record<string, string> {
return {
'X-NianCode-Access-Token': accessToken,
};
}
function mapQuality(quality: WorksImageQuality): string {
return quality === 'high' ? 'high' : 'medium';
}
function normalizeJob(payload: unknown, fallbackPrompt?: string): WorksImageJob {
const record = payload && typeof payload === 'object'
? payload as Record<string, unknown>
: {};
const taskId = record.task_id ?? record.taskId ?? record.id;
const resultUrls = record.result_urls ?? record.resultUrls ?? record.outputUrls;
return {
id: typeof taskId === 'string' && taskId ? taskId : 'image-task',
status: typeof record.status === 'string' ? record.status : 'queued',
prompt: typeof record.prompt === 'string' ? record.prompt : fallbackPrompt,
outputUrls: Array.isArray(resultUrls)
? resultUrls.filter((url): url is string => typeof url === 'string' && Boolean(url.trim()))
: [],
error: typeof record.error === 'string' || (record.error && typeof record.error === 'object')
? record.error as WorksImageJob['error']
: null,
createdAt: typeof record.created_at === 'string' ? record.created_at : undefined,
updatedAt: typeof record.updated_at === 'string' ? record.updated_at : undefined,
};
}
export async function fetchWorksImageTask(accessToken: string, taskId: string): Promise<WorksImageJob> {
const response = await hostApiFetch<WorksImageResponse<'job', unknown>>(
`/api/works/ai-gateway/images/tasks/${encodeURIComponent(taskId)}`,
{
headers: authHeaders(accessToken),
},
);
return normalizeJob(assertSuccess(response, 'job', 'Failed to load image generation task'));
}
export async function submitWorksImageGeneration(input: WorksImageGenerationInput): Promise<WorksImageJob> {
const response = await hostApiFetch<WorksImageResponse<'job', unknown>>(
'/api/works/ai-gateway/images/generations',
{
method: 'POST',
headers: authHeaders(input.accessToken),
body: JSON.stringify({
prompt: input.prompt,
image_urls: [],
size: input.aspectRatio,
quality: mapQuality(input.quality),
resolution: '2K',
}),
},
);
return normalizeJob(assertSuccess(response, 'job', 'Failed to submit image generation'), input.prompt);
}