120 lines
3.6 KiB
TypeScript
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);
|
|
}
|