This commit is contained in:
@@ -1,10 +1,37 @@
|
||||
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"]),
|
||||
@@ -68,3 +95,237 @@ export function saveUnifiedEcommerceGenerationRecord(input: EcommerceUnifiedReco
|
||||
export async function deleteEcommerceGenerationRecord(clientRecordId: string): Promise<void> {
|
||||
await deleteGenerationRecordByClientId(clientRecordId);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user