From 59bc3636e1b1e1b8e21704d002abae88a8f83766 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 27 Jan 2026 21:47:59 +0800 Subject: [PATCH] feat: rename scene fusion to detail replace, add auto-retry up to 10 times --- .../settings/admin-settings-client.tsx | 24 ++-- components/canvas/nodes/ImageNode.tsx | 6 +- components/image-annotator.tsx | 6 +- components/scene-fusion-dialog.tsx | 50 +++++--- components/scene-fusion-view.tsx | 118 ++++++++++++++---- 5 files changed, 146 insertions(+), 58 deletions(-) diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index f5b2c00d4..b7aa2b458 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -408,7 +408,7 @@ export function AdminSettingsClient() { timeout: 120, }); - // 场景裂变配置状态 + // 详情替换配置状态 const [sceneFusionConfig, setSceneFusionConfig] = useState({ enabled: true, defaultPrompt: "将图1的产品替换到图2的场景中,保持图1产品的外观、颜色和细节不变,自然融合到图2的环境中。", @@ -499,7 +499,7 @@ export function AdminSettingsClient() { } }, [angleEditConfigData]); - // 加载场景裂变配置 + // 加载详情替换配置 const { data: sceneFusionConfigData } = useSWR("/api/admin/settings?key=scene_fusion_config", fetcher); useEffect(() => { if (sceneFusionConfigData?.setting?.value) { @@ -961,7 +961,7 @@ export function AdminSettingsClient() { } }; - // --- 场景裂变配置逻辑 --- + // --- 详情替换配置逻辑 --- const handleSaveSceneFusionConfig = async () => { try { if (!sceneFusionConfig.defaultPrompt.trim()) { @@ -984,9 +984,9 @@ export function AdminSettingsClient() { }); if (!response.ok) throw new Error("保存失败"); - toast.success("场景裂变配置保存成功"); + toast.success("详情替换配置保存成功"); } catch (error) { - toast.error("保存场景裂变配置失败"); + toast.error("保存详情替换配置失败"); } }; @@ -3894,24 +3894,24 @@ export function AdminSettingsClient() { - {/* 场景裂变配置 */} + {/* 详情替换配置 */} - 场景裂变配置 + 详情替换配置 - 配置场景裂变功能的默认提示词和参数。场景裂变可将产品图批量融合到多个场景中。 + 配置详情替换功能的默认提示词和参数。详情替换可将产品图批量融合到多个场景中。 {/* 功能开关 */}
- +
- 开启后用户可在图片编辑器中使用场景裂变功能 + 开启后用户可在图片编辑器中使用详情替换功能
- 该提示词将作为场景裂变的基础指令,用户可以在此基础上补充关键词 + 该提示词将作为详情替换的基础指令,用户可以在此基础上添加修改要求
@@ -3997,7 +3997,7 @@ export function AdminSettingsClient() {
- 选择用于场景裂变的生图模型,支持阿里生图、Gemini、既梦等图片模型 + 选择用于详情替换的生图模型,支持阿里生图、Gemini、既梦等图片模型
diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index ebfb02fd1..3abcbaebd 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -109,7 +109,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { 'extend': '图片扩展', 'resize': '尺寸调整', 'angle': '角度编辑', - 'scene-fusion': '场景裂变', + 'scene-fusion': '详情替换', }; return names[action] || action; }; @@ -428,7 +428,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { handleAction('scene-fusion')}> - 场景裂变 + 详情替换 @@ -465,7 +465,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { /> )} - {/* 场景裂变对话框 */} + {/* 详情替换对话框 */} { setShowSceneFusion(!showSceneFusion); - // 进入场景裂变时,退出其他模式 + // 进入详情替换时,退出其他模式 if (!showSceneFusion) { setIsResizeMode(false); setShowAngleEditDialog(false); } }} disabled={showAngleEditDialog || isRemovingBg || isEnhancing || isExtending || isResizeMode} - title="场景裂变" + title="详情替换" className={showSceneFusion ? "bg-primary text-white" : "text-zinc-300 hover:text-white hover:bg-zinc-800"} > - 场景裂变 + 详情替换 {!showSceneFusion && ( <> diff --git a/components/scene-fusion-dialog.tsx b/components/scene-fusion-dialog.tsx index f6add6c76..e774bbc12 100644 --- a/components/scene-fusion-dialog.tsx +++ b/components/scene-fusion-dialog.tsx @@ -36,7 +36,8 @@ const fetcher = (url: string) => fetch(url).then((res) => res.json()); export interface SceneItem { id: string; image: string; // 场景图 URL 或 Base64 - customPrompt: string; // 该场景的补充关键词 + customPrompt: string; // 该场景的修改要求 + retryCount?: number; // 重试次数 status: 'pending' | 'processing' | 'completed' | 'failed'; result?: string; // 生成结果缩略图 URL originalUrl?: string; // 生成结果原图 URL(用于下载) @@ -49,7 +50,10 @@ interface SceneFusionDialogProps { } export function SceneFusionDialog({ open, onOpenChange }: SceneFusionDialogProps) { - // 获取场景裂变配置(使用公开 API 端点) + // 最大自动重试次数 + const MAX_AUTO_RETRY = 10; + + // 获取详情替换配置(使用公开 API 端点) const { data: configData } = useSWR( open ? "/api/scene-fusion/config" : null, fetcher @@ -140,7 +144,7 @@ export function SceneFusionDialog({ open, onOpenChange }: SceneFusionDialogProps // 开始裂变 const startFusion = async () => { if (config?.enabled === false) { - toast.error("场景裂变功能已被管理员禁用"); + toast.error("详情替换功能已被管理员禁用"); return; } if (!productImage) { @@ -226,16 +230,19 @@ export function SceneFusionDialog({ open, onOpenChange }: SceneFusionDialogProps } }; - // 重新生成单个场景 - const regenerateScene = async (id: string) => { + // 重新生成单个场景(带自动重试) + const regenerateScene = async (id: string, retryCount: number = 0) => { if (!productImage) return; const scene = scenes.find(s => s.id === id); if (!scene) return; - setScenes(prev => prev.map(s => - s.id === id ? { ...s, status: 'processing', error: undefined } : s - )); + // 只在首次调用时显示处理状态 + if (retryCount === 0) { + setScenes(prev => prev.map(s => + s.id === id ? { ...s, status: 'processing', error: undefined, retryCount: 0 } : s + )); + } try { const response = await fetch('/api/scene-fusion', { @@ -300,9 +307,10 @@ export function SceneFusionDialog({ open, onOpenChange }: SceneFusionDialogProps status: 'completed', result: result.result, originalUrl: result.originalUrl, + retryCount: 0, } : s )); - toast.success('重新生成成功'); + toast.success('生成成功'); success = true; } else { throw new Error(result.error || '生成失败'); @@ -325,11 +333,25 @@ export function SceneFusionDialog({ open, onOpenChange }: SceneFusionDialogProps throw new Error('未收到有效的生成结果'); } } catch (error) { - console.error('[SceneFusion] Regenerate error:', error); + console.error(`[SceneFusion] Regenerate error (retry ${retryCount}/${MAX_AUTO_RETRY}):`, error); + + // 检查是否需要自动重试 + if (retryCount < MAX_AUTO_RETRY) { + console.log(`[SceneFusion] Auto-retrying scene ${id} (${retryCount + 1}/${MAX_AUTO_RETRY})...`); + // 更新重试计数(用户无感) + setScenes(prev => prev.map(s => + s.id === id ? { ...s, retryCount: retryCount + 1 } : s + )); + // 延迟2秒后自动重试 + await new Promise(resolve => setTimeout(resolve, 2000)); + return regenerateScene(id, retryCount + 1); + } + + // 达到最大重试次数,显示失败 setScenes(prev => prev.map(s => - s.id === id ? { ...s, status: 'failed', error: error instanceof Error ? error.message : '处理失败' } : s + s.id === id ? { ...s, status: 'failed', error: `重试${MAX_AUTO_RETRY}次后失败: ${error instanceof Error ? error.message : '处理失败'}` } : s )); - toast.error(error instanceof Error ? error.message : '重新生成失败'); + toast.error(`重试${MAX_AUTO_RETRY}次后仍然失败`); } }; @@ -388,7 +410,7 @@ export function SceneFusionDialog({ open, onOpenChange }: SceneFusionDialogProps - 场景裂变 + 详情替换 上传1张产品图和多张场景图,AI 自动将产品融合到所有场景中。 @@ -505,7 +527,7 @@ export function SceneFusionDialog({ open, onOpenChange }: SceneFusionDialogProps )} updateScenePrompt(scene.id, e.target.value)} className="h-8 text-xs" diff --git a/components/scene-fusion-view.tsx b/components/scene-fusion-view.tsx index 691d80611..d73f19acb 100644 --- a/components/scene-fusion-view.tsx +++ b/components/scene-fusion-view.tsx @@ -27,7 +27,8 @@ const fetcher = (url: string) => fetch(url).then((res) => res.json()); export interface SceneItem { id: string; image: string; // 场景图 URL 或 Base64 - customPrompt: string; // 该场景的补充关键词 + customPrompt: string; // 该场景的修改要求 + retryCount?: number; // 重试次数 targetName: string; // 背景中需要被替换的物体名称 status: 'pending' | 'processing' | 'completed' | 'failed'; result?: string; // 生成结果缩略图 URL @@ -40,7 +41,10 @@ interface SceneFusionViewProps { } export function SceneFusionView({ onBack }: SceneFusionViewProps) { - // 获取场景裂变配置(使用公开 API 端点) + // 最大自动重试次数 + const MAX_AUTO_RETRY = 10; + + // 获取详情替换配置(使用公开 API 端点) const { data: configData } = useSWR( "/api/scene-fusion/config", fetcher @@ -159,7 +163,7 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) { // 开始裂变 const startFusion = async () => { if (config?.enabled === false) { - toast.error("场景裂变功能已被管理员禁用"); + toast.error("详情替换功能已被管理员禁用"); return; } if (!productImage) { @@ -270,14 +274,29 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) { status: 'completed', result: result.result, originalUrl: result.originalUrl, + retryCount: 0, // 重置重试计数 }; } else { - failedCount++; - return { - ...s, - status: 'failed', - error: result.error || '生成失败' - }; + // 检查是否需要自动重试 + const currentRetryCount = s.retryCount || 0; + if (currentRetryCount < MAX_AUTO_RETRY) { + console.log(`[SceneFusion] Scene ${result.id} failed, will auto-retry (${currentRetryCount + 1}/${MAX_AUTO_RETRY})`); + // 标记为需要重试,稍后会自动重试 + return { + ...s, + status: 'pending', // 改为 pending 以便重新处理 + error: undefined, + retryCount: currentRetryCount + 1, + }; + } else { + failedCount++; + console.log(`[SceneFusion] Scene ${result.id} failed after ${MAX_AUTO_RETRY} retries`); + return { + ...s, + status: 'failed', + error: `重试${MAX_AUTO_RETRY}次后仍然失败: ${result.error || '生成失败'}` + }; + } } } return s; @@ -286,9 +305,9 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) { const { summary } = message; console.log('[SceneFusion Client] All done:', summary); - if (summary.failed > 0) { - toast.warning(`批量处理完成:${summary.success} 成功,${summary.failed} 失败`); - } else { + // 检查是否有需要自动重试的场景 + // 注意:这里不直接调用,而是在 finally 中检查 + if (summary.failed === 0) { toast.success(`批量处理完成:${summary.success} 张图片已生成`); } } else if (message.type === 'error') { @@ -308,25 +327,57 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) { console.error('[SceneFusion] Error:', error); toast.error(error instanceof Error ? error.message : '处理失败'); - // 将 processing 状态改回 failed - setScenes(prev => prev.map(s => - s.status === 'processing' ? { ...s, status: 'failed', error: '处理失败' } : s - )); + // 将 processing 状态改回 pending(以便自动重试)或 failed + setScenes(prev => prev.map(s => { + if (s.status === 'processing') { + const currentRetryCount = s.retryCount || 0; + if (currentRetryCount < MAX_AUTO_RETRY) { + return { ...s, status: 'pending', retryCount: currentRetryCount + 1 }; + } + return { ...s, status: 'failed', error: '处理失败' }; + } + return s; + })); } finally { setIsProcessing(false); + + // 检查是否有需要自动重试的场景(状态为 pending 且 retryCount > 0) + // 使用 setTimeout 延迟执行,避免立即重试 + setTimeout(() => { + setScenes(currentScenes => { + const pendingRetries = currentScenes.filter(s => s.status === 'pending' && (s.retryCount || 0) > 0); + if (pendingRetries.length > 0) { + console.log(`[SceneFusion] Auto-retrying ${pendingRetries.length} failed scenes...`); + // 触发重新处理 - 由于 startFusion 会处理 pending 状态的场景 + // 我们需要在下一个事件循环中调用 + setTimeout(() => startFusion(), 100); + } else { + // 所有场景都处理完成,检查最终结果 + const failed = currentScenes.filter(s => s.status === 'failed').length; + const completed = currentScenes.filter(s => s.status === 'completed').length; + if (failed > 0 && completed > 0) { + toast.warning(`批量处理完成:${completed} 成功,${failed} 失败`); + } + } + return currentScenes; + }); + }, 500); } }; - // 重新生成单个场景 - const regenerateScene = async (id: string) => { + // 重新生成单个场景(带自动重试) + const regenerateScene = async (id: string, retryCount: number = 0) => { if (!productImage) return; const scene = scenes.find(s => s.id === id); if (!scene) return; - setScenes(prev => prev.map(s => - s.id === id ? { ...s, status: 'processing', error: undefined } : s - )); + // 只在首次调用时显示处理状态 + if (retryCount === 0) { + setScenes(prev => prev.map(s => + s.id === id ? { ...s, status: 'processing', error: undefined, retryCount: 0 } : s + )); + } try { const response = await fetch('/api/scene-fusion', { @@ -395,9 +446,10 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) { status: 'completed', result: result.result, originalUrl: result.originalUrl, + retryCount: 0, } : s )); - toast.success('重新生成成功'); + toast.success('生成成功'); success = true; } else { throw new Error(result.error || '生成失败'); @@ -420,9 +472,23 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) { throw new Error('未收到有效的生成结果'); } } catch (error) { - console.error('[SceneFusion] Regenerate error:', error); + console.error(`[SceneFusion] Regenerate error (retry ${retryCount}/${MAX_AUTO_RETRY}):`, error); + + // 检查是否需要自动重试 + if (retryCount < MAX_AUTO_RETRY) { + console.log(`[SceneFusion] Auto-retrying scene ${id} (${retryCount + 1}/${MAX_AUTO_RETRY})...`); + // 更新重试计数(用户无感) + setScenes(prev => prev.map(s => + s.id === id ? { ...s, retryCount: retryCount + 1 } : s + )); + // 延迟2秒后自动重试 + await new Promise(resolve => setTimeout(resolve, 2000)); + return regenerateScene(id, retryCount + 1); + } + + // 达到最大重试次数,显示失败 setScenes(prev => prev.map(s => - s.id === id ? { ...s, status: 'failed', error: error instanceof Error ? error.message : '处理失败' } : s + s.id === id ? { ...s, status: 'failed', error: `重试${MAX_AUTO_RETRY}次后失败: ${error instanceof Error ? error.message : '处理失败'}` } : s )); toast.error(error instanceof Error ? error.message : '重新生成失败'); } @@ -488,7 +554,7 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) {
- 场景裂变 + 详情替换
@@ -678,7 +744,7 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) { disabled={isProcessing} /> updateScenePrompt(scene.id, e.target.value)} className="h-7 text-xs"