// 克隆 / 电商历史的本地持久化模块。 // 从 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; 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 interface EcommerceHistoryRecord { id: string; title: string; createdAt: number; output: CloneOutputKey; platform: string; market: string; language: string; ratio: string; requirement: string; productImages: CloneImageItem[]; results: CloneResult[]; setResultImages: string[]; setCounts: Record; detailModules: string[]; modelScenes: string[]; referenceImages: CloneImageItem[]; replicateLevel: CloneReplicateLevelKey; } export const cloneLatestSettingStorageKey = "omniai.clone-ai.latest-setting"; export const ecommerceHistoryStorageKey = "omniai.ecommerce.history.records"; export const defaultCloneSetCounts: Record = { 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; 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; 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; 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; 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 normalizeEcommerceHistoryRecord(record: EcommerceHistoryRecord): EcommerceHistoryRecord { return { ...record, 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", }; } 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)), ); }