Files
omniai-ds-code-package/src/features/ecommerce/ecommerceGenerationPersistence.ts
T

332 lines
14 KiB
TypeScript
Raw Normal View History

import {
buildGenerationOssScope,
deleteGenerationRecordByClientId,
2026-06-18 10:16:40 +08:00
listGenerationRecords,
saveGenerationRecord,
2026-06-18 10:16:40 +08:00
type GenerationRecord,
type GenerationRecordAsset,
type SaveGenerationRecordInput,
} from "../../api/generationRecordClient";
2026-06-18 10:16:40 +08:00
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<string, unknown>;
result?: Record<string, unknown>;
metadata?: Record<string, unknown>;
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<void> {
await deleteGenerationRecordByClientId(clientRecordId);
}
2026-06-18 10:16:40 +08:00
const ecommerceHistoryStatuses = new Set<EcommerceHistoryStatus>(["generating", "done", "failed"]);
const cloneOutputs = new Set<CloneOutputKey>(["set", "detail", "model", "video", "hot"]);
const generationKinds = new Set<EcommerceHistoryTurn["generationKind"]>(["singleImage", "imageEdit", "imageSet", "video"]);
const replicateLevels = new Set<CloneReplicateLevelKey>(["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<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
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<CloneSetCountKey, number> {
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<EcommerceHistoryTurn, "id" | "createdAt">, 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<EcommerceHistoryTurn, "id" | "createdAt"> = {
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<EcommerceHistoryRecord[]> {
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);
}