Files
omniai-ds-code-package/src/features/ecommerce/utils/clonePersistence.ts
T
stringadmin a2ccf290e5
CI / verify (pull_request) Waiting to run
Fix ecommerce generation history sync
2026-06-18 10:16:40 +08:00

320 lines
11 KiB
TypeScript

// 克隆 / 电商历史的本地持久化模块。
// 从 EcommercePage.tsx 抽出,逻辑零改动。
// 把 localStorage 读写 + 字段校验 + 默认值收口在此,页面只调用 read/write。
//
// 领域类型(CloneImageItem / CloneResult / CloneSavedSetting / EcommerceHistoryRecord
// 及其依赖的 type alias)也定义在此并 export,因为它们本质上是"持久化数据契约";
// EcommercePage 从这里 re-import,避免循环依赖(类型 import 编译期擦除)。
import type { CloneOutputKey } from "./platformRules";
export type CloneSetCountKey = "selling" | "white" | "scene";
export type CloneModelPanelTab = "scene" | "model";
export type CloneVideoQualityKey = "standard" | "high" | "ultra";
export type CloneReplicateLevelKey = "style" | "high";
export type CloneReferenceMode = "upload" | "link";
export interface CloneImageItem {
id: string;
src: string;
name: string;
file?: File;
width?: number;
height?: number;
format?: string;
mimeType?: string;
ossKey?: string;
}
export interface CloneResult {
id: string;
src: string;
label: string;
type?: "image" | "video";
}
export interface CloneSavedSetting {
id: string;
name: string;
savedAt: string;
output: CloneOutputKey;
platform: string;
market: string;
language: string;
ratio: string;
setCounts: Record<CloneSetCountKey, number>;
detailModules: string[];
modelPanelTab: CloneModelPanelTab;
modelScenes: string[];
modelCustomScene: string;
modelGender: string;
modelAge: string;
modelEthnicity: string;
modelBody: string;
modelAppearance: string;
videoQuality: CloneVideoQualityKey;
videoDurationSeconds: number;
videoSmart: boolean;
referenceMode?: CloneReferenceMode;
replicateLevel?: CloneReplicateLevelKey;
requirement: string;
}
export type EcommerceHistoryStatus = "generating" | "done" | "failed";
export interface EcommerceHistoryTurn {
id: string;
createdAt: number;
status: EcommerceHistoryStatus;
errorMessage?: string;
output: CloneOutputKey;
modeLabel?: string;
settingLabel?: string;
generationKind?: "singleImage" | "imageEdit" | "imageSet" | "video";
platform: string;
market: string;
language: string;
ratio: string;
requirement: string;
productImages: CloneImageItem[];
results: CloneResult[];
setResultImages: string[];
setCounts: Record<CloneSetCountKey, number>;
detailModules: string[];
modelScenes: string[];
referenceImages: CloneImageItem[];
replicateLevel: CloneReplicateLevelKey;
}
export interface EcommerceHistoryRecord {
id: string;
title: string;
createdAt: number;
status?: EcommerceHistoryStatus;
errorMessage?: string;
output: CloneOutputKey;
modeLabel?: string;
settingLabel?: string;
generationKind?: "singleImage" | "imageEdit" | "imageSet" | "video";
platform: string;
market: string;
language: string;
ratio: string;
requirement: string;
productImages: CloneImageItem[];
results: CloneResult[];
setResultImages: string[];
setCounts: Record<CloneSetCountKey, number>;
detailModules: string[];
modelScenes: string[];
referenceImages: CloneImageItem[];
replicateLevel: CloneReplicateLevelKey;
turns?: EcommerceHistoryTurn[];
}
export const cloneLatestSettingStorageKey = "omniai.clone-ai.latest-setting";
export const ecommerceHistoryStorageKey = "omniai.ecommerce.history.records";
export const defaultCloneSetCounts: Record<CloneSetCountKey, number> = {
selling: 3,
white: 1,
scene: 3,
};
export const defaultCloneDetailModuleIds = ["hero", "selling", "usage", "angle", "scene", "detail"];
export function isCloneImageItem(item: unknown): item is CloneImageItem {
const candidate = item as Partial<CloneImageItem>;
return typeof candidate.id === "string" && typeof candidate.src === "string" && typeof candidate.name === "string";
}
export function isCloneResult(item: unknown): item is CloneResult {
const candidate = item as Partial<CloneResult>;
return typeof candidate.id === "string" && typeof candidate.src === "string" && typeof candidate.label === "string";
}
export function isEcommerceHistoryRecord(item: unknown): item is EcommerceHistoryRecord {
const candidate = item as Partial<EcommerceHistoryRecord>;
return (
typeof candidate.id === "string" &&
typeof candidate.title === "string" &&
typeof candidate.createdAt === "number" &&
typeof candidate.output === "string" &&
typeof candidate.platform === "string" &&
typeof candidate.market === "string" &&
typeof candidate.language === "string" &&
typeof candidate.ratio === "string" &&
typeof candidate.requirement === "string" &&
Array.isArray(candidate.productImages) &&
candidate.productImages.every(isCloneImageItem) &&
Array.isArray(candidate.results) &&
candidate.results.every(isCloneResult)
);
}
export function isCloneSavedSetting(item: unknown): item is CloneSavedSetting {
const candidate = item as Partial<CloneSavedSetting>;
return (
typeof candidate.id === "string" &&
typeof candidate.name === "string" &&
typeof candidate.savedAt === "string" &&
typeof candidate.output === "string" &&
typeof candidate.platform === "string" &&
typeof candidate.market === "string" &&
typeof candidate.language === "string" &&
typeof candidate.ratio === "string" &&
typeof candidate.videoDurationSeconds === "number"
);
}
export function removeFilePayloadFromImages(images: CloneImageItem[]): CloneImageItem[] {
return images.map(({ id, src, name, width, height, format, mimeType, ossKey }) => ({
id,
src,
name,
width,
height,
format,
mimeType,
ossKey,
}));
}
export function getTurnResults(turn: EcommerceHistoryTurn): CloneResult[] {
if (turn.results?.length) return turn.results.filter((item) => item.src);
if (turn.output !== "set") return [];
return (turn.setResultImages ?? [])
.filter(Boolean)
.map((src, index) => ({ id: `${turn.id}-set-${index}`, src, label: `套图 ${index + 1}` }));
}
export function buildHistoryTurnFromRecord(record: EcommerceHistoryRecord): EcommerceHistoryTurn {
return {
id: `${record.id}-turn-initial`,
createdAt: record.createdAt,
status: record.status ?? "done",
errorMessage: record.status === "failed" ? record.errorMessage : undefined,
output: record.output,
modeLabel: record.modeLabel,
settingLabel: record.settingLabel,
generationKind: record.generationKind,
platform: record.platform,
market: record.market,
language: record.language,
ratio: record.ratio,
requirement: record.requirement,
productImages: record.productImages ?? [],
results: record.results ?? [],
setResultImages: record.setResultImages ?? [],
setCounts: record.setCounts ?? defaultCloneSetCounts,
detailModules: record.detailModules ?? defaultCloneDetailModuleIds,
modelScenes: record.modelScenes ?? [],
referenceImages: record.referenceImages ?? [],
replicateLevel: record.replicateLevel ?? "high",
};
}
export function normalizeEcommerceHistoryTurn(turn: EcommerceHistoryTurn, fallback: EcommerceHistoryRecord, index: number): EcommerceHistoryTurn {
const status = turn.status ?? fallback.status ?? "done";
return {
id: typeof turn.id === "string" && turn.id ? turn.id : `${fallback.id}-turn-${index + 1}`,
createdAt: typeof turn.createdAt === "number" ? turn.createdAt : fallback.createdAt,
status,
errorMessage: status === "failed" ? turn.errorMessage ?? fallback.errorMessage : undefined,
output: turn.output ?? fallback.output,
modeLabel: turn.modeLabel ?? fallback.modeLabel,
settingLabel: turn.settingLabel ?? fallback.settingLabel,
generationKind: turn.generationKind ?? fallback.generationKind,
platform: turn.platform ?? fallback.platform,
market: turn.market ?? fallback.market,
language: turn.language ?? fallback.language,
ratio: turn.ratio ?? fallback.ratio,
requirement: turn.requirement ?? fallback.requirement,
productImages: removeFilePayloadFromImages(Array.isArray(turn.productImages) ? turn.productImages : fallback.productImages),
results: Array.isArray(turn.results) ? turn.results.filter(isCloneResult) : [],
setResultImages: Array.isArray(turn.setResultImages) ? turn.setResultImages.filter(Boolean) : [],
setCounts: turn.setCounts ?? fallback.setCounts ?? defaultCloneSetCounts,
detailModules: turn.detailModules ?? fallback.detailModules ?? defaultCloneDetailModuleIds,
modelScenes: turn.modelScenes ?? fallback.modelScenes ?? [],
referenceImages: removeFilePayloadFromImages(Array.isArray(turn.referenceImages) ? turn.referenceImages : fallback.referenceImages ?? []),
replicateLevel: turn.replicateLevel ?? fallback.replicateLevel ?? "high",
};
}
export function normalizeEcommerceHistoryRecord(record: EcommerceHistoryRecord): EcommerceHistoryRecord {
const status = record.status ?? "done";
const baseRecord = {
...record,
status,
errorMessage: status === "failed" ? record.errorMessage : undefined,
modeLabel: record.modeLabel,
settingLabel: record.settingLabel,
generationKind: record.generationKind,
productImages: removeFilePayloadFromImages(record.productImages),
referenceImages: removeFilePayloadFromImages(record.referenceImages ?? []),
results: record.results ?? [],
setResultImages: record.setResultImages ?? [],
setCounts: record.setCounts ?? defaultCloneSetCounts,
detailModules: record.detailModules ?? defaultCloneDetailModuleIds,
modelScenes: record.modelScenes ?? [],
replicateLevel: record.replicateLevel ?? "high",
};
const rawTurns = Array.isArray(record.turns) && record.turns.length
? record.turns
: [buildHistoryTurnFromRecord(baseRecord)];
const turns = rawTurns.map((turn, index) => normalizeEcommerceHistoryTurn(turn, baseRecord, index));
return {
...baseRecord,
turns,
};
}
export function readCloneLatestSetting(): CloneSavedSetting | null {
if (typeof window === "undefined") return null;
try {
const rawValue = window.localStorage.getItem(cloneLatestSettingStorageKey);
if (rawValue) {
const parsedValue: unknown = JSON.parse(rawValue);
if (isCloneSavedSetting(parsedValue)) return parsedValue;
}
} catch {
return null;
}
return null;
}
export function writeCloneLatestSetting(setting: CloneSavedSetting): void {
if (typeof window === "undefined") return;
window.localStorage.setItem(cloneLatestSettingStorageKey, JSON.stringify(setting));
}
export function clearCloneLatestSetting(): void {
if (typeof window === "undefined") return;
window.localStorage.removeItem(cloneLatestSettingStorageKey);
}
export function readEcommerceHistoryRecords(): EcommerceHistoryRecord[] {
if (typeof window === "undefined") return [];
try {
const rawValue = window.localStorage.getItem(ecommerceHistoryStorageKey);
if (!rawValue) return [];
const parsedValue: unknown = JSON.parse(rawValue);
if (!Array.isArray(parsedValue)) return [];
return parsedValue
.filter(isEcommerceHistoryRecord)
.map(normalizeEcommerceHistoryRecord)
.sort((a, b) => b.createdAt - a.createdAt)
.slice(0, 30);
} catch {
return [];
}
}
export function writeEcommerceHistoryRecords(records: EcommerceHistoryRecord[]): void {
if (typeof window === "undefined") return;
window.localStorage.setItem(
ecommerceHistoryStorageKey,
JSON.stringify(records.map(normalizeEcommerceHistoryRecord).slice(0, 30)),
);
}