import { buildGenerationOssScope, deleteGenerationRecordByClientId, listGenerationRecords, saveGenerationRecord, type GenerationRecord, type GenerationRecordAsset, type SaveGenerationRecordInput, } from "../../api/generationRecordClient"; import { defaultCloneDetailModuleIds, defaultCloneSetCounts, ecommerceHistoryStorageKey, normalizeEcommerceHistoryRecord, type CloneImageItem, type CloneReplicateLevelKey, type CloneResult, type CloneSetCountKey, type EcommerceHistoryRecord, type EcommerceHistoryStatus, type EcommerceHistoryTurn, } from "./utils/clonePersistence"; import { defaultCloneOutput, defaultEcommercePlatform, getPlatformDefaultLanguage, getPlatformDefaultRatio, marketOptions, type CloneOutputKey, normalizeLanguageForPlatform, normalizeMarket, normalizePlatform, normalizeRatioForPlatform, } from "./utils/platformRules"; export const ecommerceOssScopes = { productSource: buildGenerationOssScope(["ecommerce", "source", "product"]), cloneResult: (mode: string) => buildGenerationOssScope(["ecommerce", "result", mode]), videoSource: buildGenerationOssScope(["ecommerce", "short-video", "source"]), videoHistory: buildGenerationOssScope(["ecommerce", "short-video", "history"]), }; export interface EcommerceUnifiedRecordInput { clientRecordId: string; title: string; mode: string; prompt?: string; status?: SaveGenerationRecordInput["status"]; sourceImages?: Array<{ url: string; ossKey?: string | null; label?: string }>; results?: Array<{ url: string; label?: string; mediaType?: "image" | "video" | string; taskId?: string | null }>; taskIds?: string[]; config?: Record; result?: Record; metadata?: Record; createdAt?: string; } export function saveUnifiedEcommerceGenerationRecord(input: EcommerceUnifiedRecordInput): Promise<{ source: "server" | "local"; id: string }> { const assets: GenerationRecordAsset[] = [ ...(input.sourceImages || []).map((item): GenerationRecordAsset => ({ role: "source", mediaType: "image", url: item.url, ossKey: item.ossKey, label: item.label, scope: ecommerceOssScopes.productSource, })), ...(input.results || []).map((item): GenerationRecordAsset => ({ role: "result", mediaType: item.mediaType || "image", url: item.url, label: item.label, taskId: item.taskId, scope: item.mediaType === "video" ? ecommerceOssScopes.videoHistory : ecommerceOssScopes.cloneResult(input.mode), })), ].filter((asset) => Boolean(asset.url)); return saveGenerationRecord({ clientRecordId: input.clientRecordId, tool: "ecommerce", mode: input.mode, title: input.title, status: input.status || "completed", prompt: input.prompt, taskIds: input.taskIds, assets, config: input.config, result: input.result, metadata: input.metadata, createdAt: input.createdAt, updatedAt: new Date().toISOString(), }); } export async function deleteEcommerceGenerationRecord(clientRecordId: string): Promise { await deleteGenerationRecordByClientId(clientRecordId); } const ecommerceHistoryStatuses = new Set(["generating", "done", "failed"]); const cloneOutputs = new Set(["set", "detail", "model", "video", "hot"]); const generationKinds = new Set(["singleImage", "imageEdit", "imageSet", "video"]); const replicateLevels = new Set(["style", "high"]); function stringValue(value: unknown, fallback = ""): string { return typeof value === "string" && value.trim() ? value : fallback; } function numberValue(value: unknown, fallback: number): number { return typeof value === "number" && Number.isFinite(value) ? value : fallback; } function objectValue(value: unknown): Record { return value && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; } function stringArrayValue(value: unknown): string[] { return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string" && Boolean(item.trim())) : []; } function normalizeOutput(value: unknown): CloneOutputKey { if (value === "short-video") return "video"; return cloneOutputs.has(value as CloneOutputKey) ? value as CloneOutputKey : defaultCloneOutput; } function normalizeStatus(value: unknown): EcommerceHistoryStatus { if (value === "completed") return "done"; return ecommerceHistoryStatuses.has(value as EcommerceHistoryStatus) ? value as EcommerceHistoryStatus : "done"; } function normalizeGenerationKind(value: unknown, output: CloneOutputKey): EcommerceHistoryTurn["generationKind"] { if (generationKinds.has(value as EcommerceHistoryTurn["generationKind"])) return value as EcommerceHistoryTurn["generationKind"]; if (output === "video") return "video"; if (output === "set") return "imageSet"; return "singleImage"; } function normalizeReplicateLevel(value: unknown): CloneReplicateLevelKey { return replicateLevels.has(value as CloneReplicateLevelKey) ? value as CloneReplicateLevelKey : "high"; } function normalizeSetCounts(value: unknown): Record { const counts = objectValue(value); return { selling: numberValue(counts.selling, defaultCloneSetCounts.selling), white: numberValue(counts.white, defaultCloneSetCounts.white), scene: numberValue(counts.scene, defaultCloneSetCounts.scene), }; } function timestampValue(value: unknown, fallback: number): number { if (typeof value === "number" && Number.isFinite(value)) return value; if (typeof value === "string") { const parsed = new Date(value).getTime(); if (Number.isFinite(parsed)) return parsed; } return fallback; } function imageFromAsset(asset: GenerationRecordAsset, index: number): CloneImageItem { return { id: stringValue(asset.taskId, `server-source-${index + 1}`), src: asset.url, name: stringValue(asset.label, `source-${index + 1}`), ossKey: asset.ossKey || undefined, }; } function resultFromAsset(asset: GenerationRecordAsset, index: number): CloneResult { return { id: stringValue(asset.taskId, `server-result-${index + 1}`), src: asset.url, label: stringValue(asset.label, `result-${index + 1}`), type: asset.mediaType === "video" ? "video" : "image", }; } function normalizeHistoryImages(value: unknown, fallback: CloneImageItem[] = []): CloneImageItem[] { if (!Array.isArray(value)) return fallback; return value .map((item, index): CloneImageItem | null => { const record = objectValue(item); const src = stringValue(record.src); if (!src) return null; return { id: stringValue(record.id, `server-image-${index + 1}`), src, name: stringValue(record.name, `image-${index + 1}`), width: typeof record.width === "number" ? record.width : undefined, height: typeof record.height === "number" ? record.height : undefined, format: typeof record.format === "string" ? record.format : undefined, mimeType: typeof record.mimeType === "string" ? record.mimeType : undefined, ossKey: typeof record.ossKey === "string" ? record.ossKey : undefined, }; }) .filter((item): item is CloneImageItem => Boolean(item)); } function normalizeHistoryResults(value: unknown, fallback: CloneResult[] = []): CloneResult[] { if (!Array.isArray(value)) return fallback; return value .map((item, index): CloneResult | null => { const record = objectValue(item); const src = stringValue(record.src); if (!src) return null; return { id: stringValue(record.id, `server-result-${index + 1}`), src, label: stringValue(record.label, `result-${index + 1}`), type: record.type === "video" ? "video" : "image", }; }) .filter((item): item is CloneResult => Boolean(item)); } function buildTurnFromMetadata(value: unknown, fallback: Omit, fallbackCreatedAt: number, index: number): EcommerceHistoryTurn | null { const turn = objectValue(value); if (!Object.keys(turn).length) return null; const output = normalizeOutput(turn.output ?? fallback.output); const platform = normalizePlatform(stringValue(turn.platform, fallback.platform)); const market = normalizeMarket(stringValue(turn.market, fallback.market)); const language = normalizeLanguageForPlatform(platform, market, stringValue(turn.language, fallback.language)); const ratio = normalizeRatioForPlatform(platform, stringValue(turn.ratio, fallback.ratio), output === "hot" ? undefined : output); const results = normalizeHistoryResults(turn.results, fallback.results); const setResultImages = stringArrayValue(turn.setResultImages).length ? stringArrayValue(turn.setResultImages) : fallback.setResultImages; const status = normalizeStatus(turn.status ?? fallback.status); return { id: stringValue(turn.id, `server-turn-${index + 1}`), createdAt: timestampValue(turn.createdAt, fallbackCreatedAt), status, errorMessage: status === "failed" ? stringValue(turn.errorMessage, fallback.errorMessage) : undefined, output, modeLabel: typeof turn.modeLabel === "string" ? turn.modeLabel : fallback.modeLabel, settingLabel: typeof turn.settingLabel === "string" ? turn.settingLabel : fallback.settingLabel, generationKind: normalizeGenerationKind(turn.generationKind ?? fallback.generationKind, output), platform, market, language, ratio, requirement: stringValue(turn.requirement, fallback.requirement), productImages: normalizeHistoryImages(turn.productImages, fallback.productImages), results, setResultImages, setCounts: normalizeSetCounts(turn.setCounts ?? fallback.setCounts), detailModules: stringArrayValue(turn.detailModules).length ? stringArrayValue(turn.detailModules) : fallback.detailModules, modelScenes: stringArrayValue(turn.modelScenes).length ? stringArrayValue(turn.modelScenes) : fallback.modelScenes, referenceImages: normalizeHistoryImages(turn.referenceImages, fallback.referenceImages), replicateLevel: normalizeReplicateLevel(turn.replicateLevel ?? fallback.replicateLevel), }; } export function ecommerceHistoryRecordFromGenerationRecord(record: GenerationRecord): EcommerceHistoryRecord | null { if (record.tool !== "ecommerce") return null; const createdAt = timestampValue(record.createdAt, Date.now()); const output = normalizeOutput(record.mode); const config = objectValue(record.config); const metadata = objectValue(record.metadata); const sourceImages = record.assets.filter((asset) => asset.role === "source").map(imageFromAsset); const results = record.assets.filter((asset) => asset.role === "result").map(resultFromAsset); const hasHistoryMarker = metadata.localHistoryStorageKey === ecommerceHistoryStorageKey || typeof metadata.turnCount === "number"; if (!hasHistoryMarker && record.status !== "completed") return null; if (!hasHistoryMarker && !sourceImages.length && !results.length) return null; const platform = normalizePlatform(stringValue(config.platform, defaultEcommercePlatform)); const market = normalizeMarket(stringValue(config.market, marketOptions[0])); const language = normalizeLanguageForPlatform(platform, market, stringValue(config.language, getPlatformDefaultLanguage(platform, market))); const ratio = normalizeRatioForPlatform(platform, stringValue(config.ratio, getPlatformDefaultRatio(platform, output === "hot" ? undefined : output)), output === "hot" ? undefined : output); const setResultImages = results.filter((item) => item.type !== "video").map((item) => item.src); const status = normalizeStatus(record.status); const baseTurn: Omit = { status, errorMessage: status === "failed" ? "生成失败" : undefined, output, modeLabel: typeof metadata.modeLabel === "string" ? metadata.modeLabel : undefined, settingLabel: typeof metadata.settingLabel === "string" ? metadata.settingLabel : undefined, generationKind: normalizeGenerationKind(metadata.generationKind, output), platform, market, language, ratio, requirement: record.prompt ?? "", productImages: sourceImages, results, setResultImages: output === "set" ? setResultImages : [], setCounts: normalizeSetCounts(config.setCounts), detailModules: stringArrayValue(config.detailModules).length ? stringArrayValue(config.detailModules) : defaultCloneDetailModuleIds, modelScenes: stringArrayValue(config.modelScenes), referenceImages: normalizeHistoryImages(metadata.referenceImages), replicateLevel: normalizeReplicateLevel(config.replicateLevel), }; const turns = Array.isArray(metadata.turns) ? metadata.turns .map((turn, index) => buildTurnFromMetadata(turn, baseTurn, createdAt, index)) .filter((turn): turn is EcommerceHistoryTurn => Boolean(turn)) : []; const latestTurn = turns[turns.length - 1] ?? { id: `${record.clientRecordId}-turn-initial`, createdAt, ...baseTurn }; return normalizeEcommerceHistoryRecord({ id: record.clientRecordId, title: record.title || record.prompt || "生成记录", createdAt, status: latestTurn.status, errorMessage: latestTurn.errorMessage, output: latestTurn.output, modeLabel: latestTurn.modeLabel, settingLabel: latestTurn.settingLabel, generationKind: latestTurn.generationKind, platform: latestTurn.platform, market: latestTurn.market, language: latestTurn.language, ratio: latestTurn.ratio, requirement: latestTurn.requirement, productImages: latestTurn.productImages, results: latestTurn.results, setResultImages: latestTurn.setResultImages, setCounts: latestTurn.setCounts, detailModules: latestTurn.detailModules, modelScenes: latestTurn.modelScenes, referenceImages: latestTurn.referenceImages, replicateLevel: latestTurn.replicateLevel, turns: turns.length ? turns : [latestTurn], }); } export async function listEcommerceGenerationHistory(limit = 30): Promise { const payload = await listGenerationRecords({ tool: "ecommerce", limit }); return payload.items .map(ecommerceHistoryRecordFromGenerationRecord) .filter((record): record is EcommerceHistoryRecord => Boolean(record)) .sort((a, b) => b.createdAt - a.createdAt) .slice(0, limit); }