Files
NianAIGC/lib/server/generation-service.ts
2026-07-14 12:59:07 +08:00

438 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
buildJimengPayload,
buildJimengQueryPayload,
getEnabledImageCapability,
isRetryableVisualCode
} from "@/lib/jimeng/capabilities";
import {
buildEvolinkImagePayload,
extractEvolinkResultUrls,
getEffectiveImageEngine,
getEvolinkImageSettings,
getEvolinkTaskId,
mapEvolinkStatus,
queryEvolinkTask,
shouldMockEvolinkApi,
submitEvolinkImageTask,
type ImageCreationEngine,
type EvolinkTaskResponse
} from "@/lib/evolink/image-client";
import {
createGenerationJob,
getGenerationJob,
recordUsageEvent,
updateGenerationJob
} from "@/lib/server/data-store";
import { createMockImageBuffer } from "@/lib/server/mock-image";
import { importRemoteImageAsAsset, saveGeneratedAsset } from "@/lib/server/storage";
import { DEFAULT_OWNER_ID, toAbsoluteUrl } from "@/lib/server/runtime";
import type { EnabledImageCapability, GenerationJob, VisualTaskQueryResponse } from "@/lib/types";
import { queryVisualTask, shouldMockVisualApi, submitVisualTask } from "@/lib/volcengine/visual-client";
import {
bailianResultUrls,
bailianStatus,
bailianTaskId,
buildBailianImagePayload,
getBailianConfig,
queryBailianTask,
shouldMockBailian,
submitBailianTask
} from "@/lib/bailian/client";
export type SubmitImageJobInput = {
ownerId?: string;
externalClientId?: string;
capability: EnabledImageCapability;
engine?: ImageCreationEngine;
prompt?: string;
imageUrls?: string[];
inputAssetIds?: string[];
scale?: number;
width?: number;
height?: number;
min_ratio?: number;
max_ratio?: number;
force_single?: boolean;
resolution?: "4k" | "8k";
quality?: string;
seed?: number;
retryOf?: string;
idempotencyKey?: string;
idempotencyFingerprint?: string;
priority?: number;
maxAttempts?: number;
webhookUrl?: string;
};
export async function submitImageJob(input: SubmitImageJobInput, origin: string): Promise<GenerationJob> {
const ownerId = input.ownerId || DEFAULT_OWNER_ID;
const capability = getEnabledImageCapability(input.capability);
const normalizedUrls = (input.imageUrls || []).map((url) => toAbsoluteUrl(url, origin));
const engine = getEffectiveImageEngine(input.capability, input.engine);
const providerPayload = engine === "bailian"
? buildBailianImagePayload(input.capability, { ...input, imageUrls: normalizedUrls })
: engine === "evolink"
? buildEvolinkImagePayload(input.capability, { ...input, imageUrls: normalizedUrls })
: buildJimengPayload(input.capability, capability.reqKey, { ...input, imageUrls: normalizedUrls });
const bailianConfig = getBailianConfig();
const mock = engine === "bailian" ? shouldMockBailian() : engine === "evolink" ? shouldMockEvolinkApi() : shouldMockVisualApi();
const missingBailianKey = engine === "bailian" && !mock && !bailianConfig.apiKey;
const reqKey = engine === "bailian" ? bailianConfig.imageModel : engine === "evolink" ? getEvolinkImageSettings().model : capability.reqKey;
let job = await createGenerationJob({
ownerId,
externalClientId: input.externalClientId,
capability: input.capability,
provider: mock ? "mock" : engine === "bailian" ? "bailian" : engine === "evolink" ? "evolink" : "volcengine-visual",
reqKey,
status: missingBailianKey ? "failed" : "queued",
prompt: input.prompt,
inputAssetIds: input.inputAssetIds || [],
inputUrls: normalizedUrls,
outputAssetIds: [],
requestPayload: {
engine,
input,
providerPayload
},
error: missingBailianKey ? { message: "缺少 BAILIAN_API_KEY请先在设置页配置阿里云百炼 API Key。", retryable: true } : undefined,
retryOf: input.retryOf,
idempotencyKey: input.idempotencyKey,
idempotencyFingerprint: input.idempotencyFingerprint,
priority: input.priority,
maxAttempts: input.maxAttempts,
webhookUrl: input.webhookUrl
});
return job;
}
export async function advanceImageJob(jobId: string, origin: string): Promise<GenerationJob> {
const job = await getGenerationJob(jobId);
if (!job) throw new Error(`Generation job not found: ${jobId}`);
if (["succeeded", "failed", "expired", "cancelled"].includes(job.status)) return job;
if (job.provider === "mock") return completeMockJob(job, origin);
if (!job.providerTaskId) return dispatchImageJob(job);
return syncImageJob(job.id, origin);
}
async function dispatchImageJob(job: GenerationJob): Promise<GenerationJob> {
const providerPayload = asRecord(job.requestPayload.providerPayload);
try {
if (job.provider === "bailian") {
const response = await submitBailianTask("image", providerPayload);
const taskId = bailianTaskId(response);
if (!taskId) throw new Error(response.message || "百炼图片任务响应缺少 task_id。");
return updateGenerationJob(job.id, { status: "queued", providerTaskId: taskId, responsePayload: response });
}
if (job.provider === "evolink") {
const response = await submitEvolinkImageTask(providerPayload);
const taskId = getEvolinkTaskId(response);
if (!taskId) {
job = await updateGenerationJob(job.id, {
status: "failed",
responsePayload: response as unknown as Record<string, unknown>,
error: {
code: response.error?.code,
message: response.error?.message || response.message || "EvoLink task submission failed.",
retryable: false
}
});
return job;
}
return updateGenerationJob(job.id, {
status: "queued",
providerTaskId: taskId,
responsePayload: response as unknown as Record<string, unknown>
});
}
const response = await submitVisualTask(providerPayload);
if (response.code !== 10000 || !response.data?.task_id) {
job = await updateGenerationJob(job.id, {
status: "failed",
responsePayload: response as unknown as Record<string, unknown>,
error: {
code: response.code,
message: response.message || "Volcengine Visual task submission failed.",
retryable: isRetryableVisualCode(response.code)
}
});
return job;
}
return updateGenerationJob(job.id, {
status: "queued",
providerTaskId: response.data.task_id,
responsePayload: response as unknown as Record<string, unknown>
});
} catch (error) {
return updateGenerationJob(job.id, {
status: "failed",
error: {
message: error instanceof Error ? error.message : String(error),
retryable: false
}
});
}
}
export async function syncImageJob(jobId: string, origin: string): Promise<GenerationJob> {
const job = await getGenerationJob(jobId);
if (!job) throw new Error(`Generation job not found: ${jobId}`);
if (["succeeded", "failed", "expired", "cancelled"].includes(job.status)) return job;
if (job.provider === "mock") return completeMockJob(job, origin);
if (!job.providerTaskId) return job;
if (job.provider === "evolink") {
return syncEvolinkImageJob(job, origin);
}
if (job.provider === "bailian") return syncBailianImageJob(job, origin);
const queryPayload = buildJimengQueryPayload(job.reqKey, job.providerTaskId);
let response: VisualTaskQueryResponse;
try {
response = await queryVisualTask(queryPayload);
} catch (error) {
return updateGenerationJob(job.id, {
status: "failed",
error: {
message: error instanceof Error ? error.message : String(error),
retryable: false
}
});
}
if (response.data?.status === "in_queue") {
return updateGenerationJob(job.id, { status: "queued", responsePayload: response as unknown as Record<string, unknown> });
}
if (response.data?.status === "generating") {
return updateGenerationJob(job.id, { status: "running", responsePayload: response as unknown as Record<string, unknown> });
}
if (response.data?.status === "expired" || response.data?.status === "not_found") {
return updateGenerationJob(job.id, {
status: "expired",
responsePayload: response as unknown as Record<string, unknown>,
error: {
code: response.code,
message: response.message || "Volcengine Visual task expired or was not found.",
retryable: true
}
});
}
if (response.code !== 10000) {
return updateGenerationJob(job.id, {
status: "failed",
responsePayload: response as unknown as Record<string, unknown>,
error: {
code: response.code,
message: response.message || "Volcengine Visual task failed.",
retryable: isRetryableVisualCode(response.code)
}
});
}
const urls = response.data?.image_urls || [];
const assets = [];
for (let index = 0; index < urls.length; index += 1) {
assets.push(await importRemoteImageAsAsset({
ownerId: job.ownerId,
url: urls[index],
origin,
source: sourceForCapability(job.capability),
capability: job.capability,
jobId: job.id,
index,
tags: assetTagsForJob(job)
}));
}
if (assets.length) {
await recordUsageEvent({
ownerId: job.ownerId,
jobId: job.id,
capability: job.capability,
quantity: assets.length,
estimatedUnit: "image"
});
}
return updateGenerationJob(job.id, {
status: "succeeded",
outputAssetIds: assets.map((asset) => asset.id),
responsePayload: response as unknown as Record<string, unknown>
});
}
async function syncBailianImageJob(job: GenerationJob, origin: string): Promise<GenerationJob> {
const response = await queryBailianTask(job.providerTaskId!);
const status = bailianStatus(response);
if (status !== "succeeded") {
return updateGenerationJob(job.id, {
status,
responsePayload: response,
error: status === "failed" ? { message: response.message || "百炼图片任务失败。", retryable: false } : undefined
});
}
const urls = bailianResultUrls(response, "image");
if (!urls.length) return updateGenerationJob(job.id, { status: "failed", responsePayload: response, error: { message: "百炼图片任务未返回结果地址。", retryable: true } });
const assets = [];
for (let index = 0; index < urls.length; index += 1) {
assets.push(await importRemoteImageAsAsset({ ownerId: job.ownerId, url: urls[index], origin, source: sourceForCapability(job.capability), capability: job.capability, jobId: job.id, index, tags: assetTagsForJob(job) }));
}
await recordUsageEvent({ ownerId: job.ownerId, jobId: job.id, capability: job.capability, quantity: assets.length, estimatedUnit: "image" });
return updateGenerationJob(job.id, { status: "succeeded", outputAssetIds: assets.map((asset) => asset.id), responsePayload: response });
}
async function syncEvolinkImageJob(job: GenerationJob, origin: string): Promise<GenerationJob> {
if (!job.providerTaskId) return job;
let response: EvolinkTaskResponse;
try {
response = await queryEvolinkTask(job.providerTaskId);
} catch (error) {
return updateGenerationJob(job.id, {
status: "failed",
error: {
message: error instanceof Error ? error.message : String(error),
retryable: false
}
});
}
const status = mapEvolinkStatus(response);
if (status === "queued" || status === "running") {
return updateGenerationJob(job.id, {
status,
responsePayload: response as unknown as Record<string, unknown>
});
}
if (status === "expired" || status === "cancelled") {
return updateGenerationJob(job.id, {
status,
responsePayload: response as unknown as Record<string, unknown>,
error: {
code: response.error?.code,
message: response.error?.message || response.message || "EvoLink image task was not completed.",
retryable: status === "expired"
}
});
}
if (status === "failed") {
return updateGenerationJob(job.id, {
status: "failed",
responsePayload: response as unknown as Record<string, unknown>,
error: {
code: response.error?.code,
message: response.error?.message || response.message || "EvoLink image task failed.",
retryable: false
}
});
}
const urls = extractEvolinkResultUrls(response);
if (!urls.length) {
return updateGenerationJob(job.id, {
status: "failed",
responsePayload: response as unknown as Record<string, unknown>,
error: {
message: "EvoLink image task completed without result URLs.",
retryable: true
}
});
}
const assets = [];
for (let index = 0; index < urls.length; index += 1) {
assets.push(await importRemoteImageAsAsset({
ownerId: job.ownerId,
url: urls[index],
origin,
source: sourceForCapability(job.capability),
capability: job.capability,
jobId: job.id,
index,
tags: assetTagsForJob(job)
}));
}
await recordUsageEvent({
ownerId: job.ownerId,
jobId: job.id,
capability: job.capability,
quantity: assets.length,
estimatedUnit: "image"
});
return updateGenerationJob(job.id, {
status: "succeeded",
outputAssetIds: assets.map((asset) => asset.id),
responsePayload: response as unknown as Record<string, unknown>
});
}
export async function retryImageJob(jobId: string, origin: string, ownerId?: string): Promise<GenerationJob> {
const job = await getGenerationJob(jobId);
if (!job) throw new Error(`Generation job not found: ${jobId}`);
if (ownerId && job.ownerId !== ownerId) throw new Error(`Generation job not found: ${jobId}`);
const input = (job.requestPayload.input || {}) as SubmitImageJobInput;
return submitImageJob({
...input,
ownerId: ownerId || job.ownerId,
capability: job.capability as EnabledImageCapability,
retryOf: job.id
}, origin);
}
async function completeMockJob(job: GenerationJob, origin: string): Promise<GenerationJob> {
if (job.status === "succeeded" && job.outputAssetIds.length > 0) return job;
const requestInput = (job.requestPayload.input || {}) as SubmitImageJobInput;
const buffer = createMockImageBuffer({
title: "智念AIGC生成结果",
prompt: job.prompt || requestInput.prompt,
capability: job.capability,
resolution: requestInput.resolution
});
const asset = await saveGeneratedAsset({
ownerId: job.ownerId,
bytes: buffer,
fileName: `${job.capability.replace(/\W+/g, "-")}-${job.id}.svg`,
contentType: "image/svg+xml",
origin,
source: sourceForCapability(job.capability),
capability: job.capability,
jobId: job.id,
tags: assetTagsForJob(job),
metadata: {
mock: true
}
});
await recordUsageEvent({
ownerId: job.ownerId,
jobId: job.id,
capability: job.capability,
quantity: 1,
estimatedUnit: "image"
});
return updateGenerationJob(job.id, {
status: "succeeded",
outputAssetIds: [asset.id],
responsePayload: {
mock: true,
data: {
status: "done",
image_urls: [asset.url]
}
}
});
}
function sourceForCapability(capability: string) {
if (capability === "image.inpaint") return "edited";
if (capability === "image.upscale") return "upscaled";
return "generated";
}
function assetTagsForJob(job: GenerationJob): string[] {
return job.externalClientId ? [job.capability, `api-client:${job.externalClientId}`] : [job.capability];
}
function asRecord(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
? value as Record<string, unknown>
: {};
}