feat(ecommerce): replace mock image generators with real gpt-image-2 API calls

All four image tools (套图, 详情图, 模特图, 爆款图复刻) previously used
setTimeout + static sample images. Now they:

1. Upload product images to OSS via uploadAssetBinary
2. Build contextual prompts including platform/ratio/language/market + user text
3. Call aiGenerationClient.createImageTask with model=gpt-image-2
4. Poll task status via waitForTask (SSE + fallback polling)
5. Display the generated result URL in the preview grid

Key changes:
- Add ServerRequestError + waitForTask imports
- Add imageAbortRef for task cancellation
- Add uploadCloneImages() (generic version of uploadProductImages)
- Add buildEcommerceImagePrompt() with output-type-specific instructions
- Add generateEcommerceImage() orchestrating upload → prompt → API → result
- Replace all 5 mock handlers (handleGenerate, handleSetGenerate,
  handleDetailGenerate, handleTryOnGenerate, handleGenerateModel) with
  real async API calls
- Handle 402 (Payment Required) with user-friendly error message

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 21:19:52 +08:00
parent 3f19829126
commit b07ff439f3
+150 -22
View File
@@ -20,6 +20,8 @@ const ecommerceSlide5 = `${OSS_MUBAN}/slide-5.png`;
import ImageMentionMenu, { getImageMentionQuery, insertImageMentionValue, type MentionImageOption } from "./ImageMentionMenu";
import EcommerceVideoWorkspace from "./EcommerceVideoWorkspace";
import { aiGenerationClient } from "../../api/aiGenerationClient";
import { ServerRequestError } from "../../api/serverConnection";
import { waitForTask } from "../../api/taskSubscription";
import {
analyzeProductImages,
buildProductSummary,
@@ -819,6 +821,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const [ratio, setRatio] = useState(getPlatformDefaultRatio(platformOptions[0]));
const [status, setStatus] = useState<ProductCloneStatus>("idle");
const [results, setResults] = useState<CloneResult[]>([]);
const imageAbortRef = useRef({ current: false });
const [garmentImages, setGarmentImages] = useState<CloneImageItem[]>([]);
const [modelSource, setModelSource] = useState<TryOnModelSource>("ai");
const [modelGender, setModelGender] = useState(tryOnModelOptions.gender[0]);
@@ -1339,6 +1342,116 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
return urls;
};
const uploadCloneImages = async (images: CloneImageItem[]): Promise<string[]> => {
const SUPPORTED_IMAGE_TYPES = new Set(["image/jpeg", "image/png", "image/webp", "image/gif"]);
const urls: string[] = [];
for (const item of images) {
try {
const resp = await fetch(item.src);
const rawBlob = await resp.blob();
const mimeType = SUPPORTED_IMAGE_TYPES.has(rawBlob.type) ? rawBlob.type : "image/png";
const blob = rawBlob.type === mimeType ? rawBlob : new Blob([rawBlob], { type: mimeType });
const { url } = await aiGenerationClient.uploadAssetBinary(blob, { name: item.name, mimeType, scope: "ecommerce-product" });
urls.push(url);
} catch {
// skip images that fail to upload
}
}
return urls;
};
const IMAGE_MODEL = "gpt-image-2";
const buildEcommerceImagePrompt = (outputKey: CloneOutputKey, userText: string, pPlatform: string, pRatio: string, pLanguage: string, pMarket: string): string => {
const parts: string[] = [];
if (outputKey === "set") {
parts.push("Generate a complete set of e-commerce product images for online marketplace listing.");
parts.push(`Include: main product image with clean white background, lifestyle/scene image showing product in real use, model-wearing image if applicable, detail/close-up image highlighting texture and quality, selling-point infographic image.`);
parts.push(`Platform: ${pPlatform}. Aspect ratio: ${pRatio}. Language/copy: ${pLanguage}. Market: ${pMarket}.`);
parts.push("All images must comply with platform image guidelines — proper margins, no watermark, no prohibited content.");
} else if (outputKey === "detail") {
parts.push("Generate a series of A+ detail page images for an e-commerce product listing.");
parts.push(`Create images for each section: hero/first-screen visual, core selling points, usage scenes, multi-angle views, product detail close-ups.`);
parts.push(`Platform: ${pPlatform}. Aspect ratio: ${pRatio}. Language/copy: ${pLanguage}. Market: ${pMarket}.`);
parts.push("Design should follow platform A+ page best practices — clear hierarchy, professional typography, high visual impact.");
} else if (outputKey === "model") {
parts.push("Generate model/try-on lifestyle images for an e-commerce product listing.");
parts.push(`Show the product being used or worn by a model in attractive lifestyle settings.`);
parts.push(`Platform: ${pPlatform}. Aspect ratio: ${pRatio}. Language/copy: ${pLanguage}. Market: ${pMarket}.`);
parts.push("Model should appear natural and appealing. Background should complement the product. Image must meet platform standards.");
} else if (outputKey === "hot") {
parts.push("Generate a high-conversion e-commerce product image that closely replicates the style and composition of the reference image while adapting it to the target platform.");
parts.push(`Replicate the visual style, color palette, and layout feel of the source product image, then adapt it for ${pPlatform} marketplace standards.`);
parts.push(`Platform: ${pPlatform}. Aspect ratio: ${pRatio}. Language/copy: ${pLanguage}. Market: ${pMarket}.`);
parts.push("The result must look professional and optimized for high click-through rate and conversion on the specified platform.");
}
if (userText.trim()) {
parts.push(`Additional user requirements: ${userText.trim()}`);
}
return parts.join(" ");
};
const generateEcommerceImage = async (
outputKey: CloneOutputKey,
images: CloneImageItem[],
userText: string,
pPlatform: string,
pRatio: string,
pLanguage: string,
pMarket: string,
setStatusFn: (status: "generating" | "done" | "idle") => void,
setResultFn: (results: CloneResult[]) => void,
): Promise<void> => {
setStatusFn("generating");
try {
const referenceUrls = await uploadCloneImages(images);
if (!referenceUrls.length) {
setStatusFn("idle");
return;
}
const prompt = buildEcommerceImagePrompt(outputKey, userText, pPlatform, pRatio, pLanguage, pMarket);
const cardLabels = productSetPreviewCards.map((c) => c.label);
const stamp = Date.now();
const { taskId } = await aiGenerationClient.createImageTask({
model: IMAGE_MODEL,
prompt,
ratio: pRatio,
quality: pRatio.includes("720") ? "720P" : "1080P",
gridMode: outputKey === "set" ? "grid" : "single",
referenceUrls,
});
const resultUrl = await waitForTask(taskId, {
abortRef: imageAbortRef.current,
onProgress: () => {},
});
if (resultUrl) {
setResultFn(
cardLabels.map((label, i) => ({
id: `ecommerce-${stamp}-${i}`,
src: resultUrl,
label,
})),
);
setStatusFn("done");
} else {
setStatusFn("idle");
}
} catch (err) {
if (err instanceof ServerRequestError && err.status === 402) {
setResultFn([{
id: `ecommerce-error-402`,
src: "",
label: "余额不足,请充值后继续",
}]);
}
setStatusFn("idle");
}
};
const adVideoUploadedUrlsRef = useRef<string[]>([]);
const handleAdVideoPlan = async () => {
@@ -1540,32 +1653,37 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const handleGenerate = () => {
if (!canGenerate) return;
setStatus("generating");
window.setTimeout(() => {
const stamp = Date.now();
setResults(
sampleResults.map((src, index) => ({
id: `clone-result-${stamp}-${index}`,
src,
label: index === 0 ? "高度复刻" : index === 1 ? "参考风格" : "平台适配",
})),
);
setStatus("done");
}, 900);
imageAbortRef.current = { current: false };
void generateEcommerceImage(
cloneOutput, productImages, requirement,
platform, ratio, language, market,
(s) => setStatus(s as ProductCloneStatus), setResults,
);
};
const handleGenerateModel = () => {
imageAbortRef.current = { current: false };
setTryOnStatus("modeling");
window.setTimeout(() => setTryOnStatus("ready"), 700);
void generateEcommerceImage(
"model", garmentImages, requirement,
platform, ratio, language, market,
(s) => {
if (s === "done") setTryOnStatus("ready");
else setTryOnStatus(s as TryOnStatus);
},
() => { setTryOnStatus("ready"); },
);
};
const handleTryOnGenerate = () => {
if (!canGenerateTryOn) return;
setTryOnStatus("generating");
window.setTimeout(() => {
setTryOnResultImages([tryOnAssets.tryA, tryOnAssets.tryB, tryOnAssets.hatResultA]);
setTryOnStatus("done");
}, 900);
imageAbortRef.current = { current: false };
void generateEcommerceImage(
"model", garmentImages, requirement,
platform, ratio, language, market,
(s) => setTryOnStatus(s as TryOnStatus),
(res) => setTryOnResultImages(res.map((r) => r.src).filter(Boolean)),
);
};
const toggleScene = (scene: string) => {
@@ -1582,8 +1700,13 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const handleSetGenerate = () => {
if (!canGenerateSet) return;
setProductSetStatus("generating");
window.setTimeout(() => setProductSetStatus("done"), 900);
imageAbortRef.current = { current: false };
void generateEcommerceImage(
"set", setImages, productSetRequirement,
productSetPlatform, productSetRatio, productSetLanguage, productSetMarket,
(s) => setProductSetStatus(s as ProductSetStatus),
(res) => { setProductSetStatus("done"); },
);
};
const openProductSetPreview = (card: { src: string; label: string }) => {
@@ -1598,8 +1721,13 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const handleDetailGenerate = () => {
if (!canGenerateDetail) return;
setDetailStatus("generating");
window.setTimeout(() => setDetailStatus("done"), 900);
imageAbortRef.current = { current: false };
void generateEcommerceImage(
"detail", detailProductImages, detailRequirement,
detailPlatform, getPlatformDefaultRatio(detailPlatform), detailLanguage, detailMarket,
(s) => setDetailStatus(s as DetailStatus),
(res) => { setDetailStatus("done"); },
);
};
const resetTask = () => {