import { buildJimengPayload, buildJimengQueryPayload, getEnabledImageCapability, isRetryableVisualCode } from "@/lib/jimeng/capabilities"; import { buildEvolinkImagePayload, extractEvolinkResultUrls, getEffectiveImageEngine, getEvolinkImageSettings, getEvolinkTaskId, mapEvolinkStatus, queryEvolinkTask, shouldMockEvolinkApi, submitEvolinkImageTask, 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"; export type SubmitImageJobInput = { ownerId?: string; externalClientId?: string; capability: EnabledImageCapability; 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 { 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); const providerPayload = engine === "evolink" ? buildEvolinkImagePayload(input.capability, { ...input, imageUrls: normalizedUrls }) : buildJimengPayload(input.capability, capability.reqKey, { ...input, imageUrls: normalizedUrls }); const mock = engine === "evolink" ? shouldMockEvolinkApi() : shouldMockVisualApi(); const reqKey = engine === "evolink" ? getEvolinkImageSettings().model : capability.reqKey; let job = await createGenerationJob({ ownerId, externalClientId: input.externalClientId, capability: input.capability, provider: mock ? "mock" : engine === "evolink" ? "evolink" : "volcengine-visual", reqKey, status: "queued", prompt: input.prompt, inputAssetIds: input.inputAssetIds || [], inputUrls: normalizedUrls, outputAssetIds: [], requestPayload: { engine, input, providerPayload }, 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 { 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 { const providerPayload = asRecord(job.requestPayload.providerPayload); try { 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, 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 }); } 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, 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 }); } 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 { 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); } 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 }); } if (response.data?.status === "generating") { return updateGenerationJob(job.id, { status: "running", responsePayload: response as unknown as Record }); } if (response.data?.status === "expired" || response.data?.status === "not_found") { return updateGenerationJob(job.id, { status: "expired", responsePayload: response as unknown as Record, 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, 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 }); } async function syncEvolinkImageJob(job: GenerationJob, origin: string): Promise { 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 }); } if (status === "expired" || status === "cancelled") { return updateGenerationJob(job.id, { status, responsePayload: response as unknown as Record, 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, 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, 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 }); } export async function retryImageJob(jobId: string, origin: string): Promise { const job = await getGenerationJob(jobId); if (!job) throw new Error(`Generation job not found: ${jobId}`); const input = (job.requestPayload.input || {}) as SubmitImageJobInput; return submitImageJob({ ...input, capability: job.capability as EnabledImageCapability, retryOf: job.id }, origin); } async function completeMockJob(job: GenerationJob, origin: string): Promise { 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 { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : {}; }