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, recordUsageForJob, updateGenerationJob } from "@/lib/server/data-store"; import { chargeGenerationJob, quoteGenerationCharge } from "@/lib/server/billing-service"; 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 { BillingJobCharge, EnabledImageCapability, GenerationJob, UsageContext, 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; quality?: string; retryOf?: string; idempotencyKey?: string; idempotencyFingerprint?: string; priority?: number; maxAttempts?: number; webhookUrl?: string; usageContext?: UsageContext; }; type PreparedImageGeneration = { ownerId: string; normalizedUrls: string[]; engine: ImageCreationEngine; providerPayload: Record; missingBailianKey: boolean; provider: "mock" | "bailian" | "evolink" | "volcengine-visual"; reqKey: string; requestPayload: Record; }; export async function quoteImageGeneration(input: SubmitImageJobInput, origin: string): Promise { const prepared = prepareImageGeneration(input, origin); if (prepared.missingBailianKey) return undefined; return quoteGenerationCharge({ provider: prepared.provider, capability: input.capability, reqKey: prepared.reqKey, requestPayload: prepared.requestPayload, usageContext: input.usageContext, externalClientId: input.externalClientId, allowUnboundOrganization: true }); } export async function submitImageJob(input: SubmitImageJobInput, origin: string): Promise { const prepared = prepareImageGeneration(input, origin); const { ownerId, normalizedUrls, providerPayload, missingBailianKey, provider, reqKey, requestPayload } = prepared; const billing = missingBailianKey ? undefined : await quoteGenerationCharge({ provider, capability: input.capability, reqKey, requestPayload, usageContext: input.usageContext, externalClientId: input.externalClientId }); let job = await createGenerationJob({ ownerId, externalClientId: input.externalClientId, capability: input.capability, provider, reqKey, status: missingBailianKey ? "failed" : "queued", prompt: input.prompt, inputAssetIds: input.inputAssetIds || [], inputUrls: normalizedUrls, outputAssetIds: [], requestPayload, 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, usageContext: input.usageContext, billing }); if (!billing) return job; try { return await chargeGenerationJob(job); } catch (error) { await updateGenerationJob(job.id, { status: "failed", billing: { ...billing, status: "not_charged" }, error: { message: error instanceof Error ? error.message : String(error), retryable: false } }).catch(() => undefined); throw error; } } function prepareImageGeneration(input: SubmitImageJobInput, origin: string): PreparedImageGeneration { 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; const provider = mock ? "mock" : engine === "bailian" ? "bailian" : engine === "evolink" ? "evolink" : "volcengine-visual"; const requestPayload = { engine, input, providerPayload }; return { ownerId, normalizedUrls, engine, providerPayload, missingBailianKey, provider, reqKey, requestPayload }; } export async function advanceImageJob(jobId: string, origin: string): Promise { let 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.billing?.status === "pending") job = await chargeGenerationJob(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 === "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, 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); } 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 }); } 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 recordUsageForJob(job); } return updateGenerationJob(job.id, { status: "succeeded", outputAssetIds: assets.map((asset) => asset.id), responsePayload: response as unknown as Record }); } async function syncBailianImageJob(job: GenerationJob, origin: string): Promise { 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 recordUsageForJob(job); return updateGenerationJob(job.id, { status: "succeeded", outputAssetIds: assets.map((asset) => asset.id), responsePayload: response }); } 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 recordUsageForJob(job); 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, ownerId?: string, usageContext?: UsageContext ): Promise { 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, usageContext: usageContext || job.usageContext || input.usageContext, 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 }); 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 } }); return updateGenerationJob(job.id, { status: "succeeded", outputAssetIds: [asset.id], responsePayload: { mock: true, data: { status: "done", image_urls: [asset.url] } } }); } function sourceForCapability(_capability: string): "generated" { 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 : {}; }