From da3f4fad64b286c7d66d0b72ee1e0e98d168fbeb Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 21 Mar 2026 23:04:59 +0800 Subject: [PATCH] feat: support configurable agent reference sources --- .../agent-workflow/reference-search/route.ts | 22 ++- .../agents/AgentGenerationWorkspaceDarkV2.tsx | 148 +++++++++++------- components/agents/AgentPresetDialog.tsx | 90 ++++++++++- lib/agents/preset-mode-config.ts | 18 +++ types/agent.ts | 4 + 5 files changed, 217 insertions(+), 65 deletions(-) diff --git a/app/api/agent-workflow/reference-search/route.ts b/app/api/agent-workflow/reference-search/route.ts index c6d487823..96e280320 100644 --- a/app/api/agent-workflow/reference-search/route.ts +++ b/app/api/agent-workflow/reference-search/route.ts @@ -17,7 +17,7 @@ export const maxDuration = 180; const querySchema = z.object({ keyword: z.string().trim().min(1).max(64), queryType: z.enum(["sku", "spu"]).default("spu"), - target: z.enum(["detail", "sku"]), + target: z.enum(["main", "detail", "sku"]), }); type ExternalTaskDetailCandidate = { @@ -116,17 +116,23 @@ function normalizeCandidate( function getCandidateTargetUrls( candidate: AgentModeReferenceCandidate, - target: "detail" | "sku" + target: "main" | "detail" | "sku" ) { + if (target === "main") return candidate.mainImageUrls; return target === "detail" ? candidate.detailImageUrls : candidate.skuImageUrls; } function buildReferenceItems(params: { candidate: AgentModeReferenceCandidate; - target: "detail" | "sku"; + target: "main" | "detail" | "sku"; }): AgentDetailReplacementReferenceItem[] { const urls = getCandidateTargetUrls(params.candidate, params.target); - const targetLabel = params.target === "detail" ? "详情参考图" : "SKU参考图"; + const targetLabel = + params.target === "main" + ? "主图参考图" + : params.target === "detail" + ? "详情参考图" + : "SKU参考图"; const prefix = params.candidate.sceneName || params.candidate.taskNo || @@ -222,11 +228,15 @@ export async function GET(request: NextRequest) { success: false, error: { code: - parsed.target === "detail" + parsed.target === "main" + ? "MAIN_REFERENCE_NOT_FOUND" + : parsed.target === "detail" ? "DETAIL_REFERENCE_NOT_FOUND" : "SKU_REFERENCE_NOT_FOUND", message: - parsed.target === "detail" + parsed.target === "main" + ? "当前整套素材中没有可用的主图参考图" + : parsed.target === "detail" ? "当前整套素材中没有可用的详情参考图" : "当前整套素材中没有可用的 SKU 参考图", }, diff --git a/components/agents/AgentGenerationWorkspaceDarkV2.tsx b/components/agents/AgentGenerationWorkspaceDarkV2.tsx index 1acf39abd..6822919be 100644 --- a/components/agents/AgentGenerationWorkspaceDarkV2.tsx +++ b/components/agents/AgentGenerationWorkspaceDarkV2.tsx @@ -54,6 +54,7 @@ import type { AgentModeReferenceCandidate, AgentModeWorkspaceState, AgentPreset, + AgentTaskDetailReferenceSource, AgentWorkflowShowcaseConfig, AgentWorkflowComposerContext, AgentWorkflowMaterialQueryType, @@ -107,7 +108,7 @@ interface AgentModeReferenceSearchResponse { data?: { query: string; queryType: AgentWorkflowMaterialQueryType; - target: "detail" | "sku"; + target: AgentTaskDetailReferenceSource; matchedSpu?: string | null; matchedSku?: string | null; categoryName?: string | null; @@ -123,6 +124,10 @@ interface AgentModeReferenceSearchResponse { }; } +function getTaskDetailReferenceSourceLabel(source: AgentTaskDetailReferenceSource) { + return source === "main" ? "主图" : source === "sku" ? "SKU图" : "详情图"; +} + function areImageGenerationParamsEqual( left: ImageGenerationParams, right: ImageGenerationParams @@ -237,15 +242,20 @@ function getImageDimensions(url: string): Promise<{ function buildReferenceCardsFromCandidate(params: { candidate: AgentModeReferenceCandidate | null | undefined; - target: "detail" | "sku"; + target: AgentTaskDetailReferenceSource; productCount?: number; + assignProductIndex?: boolean; }) { const candidate = params.candidate; if (!candidate) return [] as AgentModeWorkspaceState["detailReplacement"]["references"]; const urls = - params.target === "detail" ? candidate.detailImageUrls || [] : candidate.skuImageUrls || []; - const targetLabel = params.target === "detail" ? "详情参考图" : "SKU参考图"; + params.target === "main" + ? candidate.mainImageUrls || [] + : params.target === "detail" + ? candidate.detailImageUrls || [] + : candidate.skuImageUrls || []; + const targetLabel = `${getTaskDetailReferenceSourceLabel(params.target)}参考图`; const prefix = candidate.sceneName || candidate.taskNo || candidate.productModel || "候选整套"; @@ -256,17 +266,19 @@ function buildReferenceCardsFromCandidate(params: { candidateId: candidate.id, sortOrder: index, assignedProductIndex: - params.target === "sku" && (params.productCount || 0) > 0 + params.assignProductIndex === true && (params.productCount || 0) > 0 ? Math.min(index, Math.max((params.productCount || 1) - 1, 0)) : null, })); } -function formatCandidateMeta(candidate: AgentModeReferenceCandidate, target: "detail" | "sku") { +function formatCandidateMeta(candidate: AgentModeReferenceCandidate, target: AgentTaskDetailReferenceSource) { const imageCount = - target === "detail" - ? candidate.detailImageUrls.length - : candidate.skuImageUrls.length; + target === "main" + ? candidate.mainImageUrls.length + : target === "detail" + ? candidate.detailImageUrls.length + : candidate.skuImageUrls.length; const uploadedAt = candidate.uploadedAt ? new Date(candidate.uploadedAt) : null; const timeText = uploadedAt && !Number.isNaN(uploadedAt.getTime()) @@ -289,10 +301,14 @@ function formatCandidateMeta(candidate: AgentModeReferenceCandidate, target: "de function getCandidatePreviewUrl( candidate: AgentModeReferenceCandidate, - target: "detail" | "sku" + target: AgentTaskDetailReferenceSource ) { const targetUrls = - target === "detail" ? candidate.detailImageUrls || [] : candidate.skuImageUrls || []; + target === "main" + ? candidate.mainImageUrls || [] + : target === "detail" + ? candidate.detailImageUrls || [] + : candidate.skuImageUrls || []; return ( targetUrls[0] || candidate.mainImageUrls[0] || @@ -301,6 +317,17 @@ function getCandidatePreviewUrl( ); } +function getCandidateTargetCount( + candidate: AgentModeReferenceCandidate, + target: AgentTaskDetailReferenceSource +) { + return target === "main" + ? candidate.mainImageUrls.length + : target === "detail" + ? candidate.detailImageUrls.length + : candidate.skuImageUrls.length; +} + type ReplacementCardStatus = "waiting" | "queued" | "generating" | "completed" | "missing"; function getReplacementCardStatus(params: { @@ -535,6 +562,10 @@ export function AgentGenerationWorkspaceDarkV2({ fallback: modelSlotEnabled ? 3 : productSlotCount, }) : 0; + const detailReferenceTarget = presetModeConfig.detail.referenceImageSource; + const skuReferenceTarget = presetModeConfig.sku.referenceImageSource; + const detailReferenceSourceLabel = getTaskDetailReferenceSourceLabel(detailReferenceTarget); + const skuReferenceSourceLabel = getTaskDetailReferenceSourceLabel(skuReferenceTarget); const shouldAllowModelOverride = !isSceneGenerationPreset || presetModeConfig.scene.allowModelOverride; const selectedProductAttachments = attachments.filter((item, index) => modelSlotIndex >= 0 ? index !== modelSlotIndex && Boolean(item) : Boolean(item) @@ -1274,11 +1305,11 @@ export function AgentGenerationWorkspaceDarkV2({ return; } if (isDetailReplacementPreset && detailReplacementReferenceCards.length === 0) { - toast.error("请先查询并选择详情参考图"); + toast.error(`请先查询并选择${detailReferenceSourceLabel}参考图`); return; } if (isSkuReplacementPreset && skuReplacementReferenceCards.length === 0) { - toast.error("请先查询并选择 SKU 参考图"); + toast.error(`请先查询并选择${skuReferenceSourceLabel}参考图`); return; } const uploadedAttachmentCount = attachments.filter(Boolean).length; @@ -1431,7 +1462,7 @@ export function AgentGenerationWorkspaceDarkV2({ const handleDetailReplacementSearch = useCallback(async () => { const normalized = detailReplacementQueryInput.trim().toUpperCase(); if (!normalized) { - toast.error("请输入 SPU 后再查询详情图"); + toast.error(`请输入 SPU 后再查询${detailReferenceSourceLabel}`); return; } @@ -1444,13 +1475,13 @@ export function AgentGenerationWorkspaceDarkV2({ const response = await fetch( `/api/agent-workflow/reference-search?keyword=${encodeURIComponent( normalized - )}&queryType=spu&target=detail`, + )}&queryType=spu&target=${detailReferenceTarget}`, { cache: "no-store" } ); const result = (await response.json().catch(() => ({}))) as AgentModeReferenceSearchResponse; if (!response.ok || !result?.success || !result?.data) { - throw new Error(result?.error?.message || "详情参考图查询失败"); + throw new Error(result?.error?.message || `${detailReferenceSourceLabel}参考图查询失败`); } updateDetailReplacementState({ @@ -1465,15 +1496,15 @@ export function AgentGenerationWorkspaceDarkV2({ isLoading: false, lastQuery: normalized, }); - toast.success(`已载入 ${result.data.references?.length || 0} 张详情参考图`); + toast.success(`已载入 ${result.data.references?.length || 0} 张${detailReferenceSourceLabel}参考图`); } catch (error) { setDetailReplacementSearchState({ isLoading: false, lastQuery: normalized, }); - toast.error(error instanceof Error ? error.message : "详情参考图查询失败"); + toast.error(error instanceof Error ? error.message : `${detailReferenceSourceLabel}参考图查询失败`); } - }, [detailReplacementQueryInput, updateDetailReplacementState]); + }, [detailReferenceSourceLabel, detailReferenceTarget, detailReplacementQueryInput, updateDetailReplacementState]); const handleSelectDetailReplacementCandidate = useCallback((candidateId: string) => { updateDetailReplacementState((current) => { const nextCandidate = @@ -1483,12 +1514,12 @@ export function AgentGenerationWorkspaceDarkV2({ selectedCandidateId: candidateId, references: buildReferenceCardsFromCandidate({ candidate: nextCandidate, - target: "detail", + target: detailReferenceTarget, productCount: selectedProductAttachments.length, }), }; }); - }, [selectedProductAttachments.length, updateDetailReplacementState]); + }, [detailReferenceTarget, selectedProductAttachments.length, updateDetailReplacementState]); const handleRemoveDetailReferenceCard = useCallback((cardId: string) => { updateDetailReplacementState((current) => ({ ...current, @@ -1506,7 +1537,7 @@ export function AgentGenerationWorkspaceDarkV2({ const handleSkuReplacementSearch = useCallback(async () => { const normalized = skuReplacementQueryInput.trim().toUpperCase(); if (!normalized) { - toast.error(`请输入${skuReplacementQueryType === "spu" ? "SPU" : "SKU"}后再查询 SKU 参考图`); + toast.error(`请输入${skuReplacementQueryType === "spu" ? "SPU" : "SKU"}后再查询${skuReferenceSourceLabel}参考图`); return; } @@ -1519,13 +1550,13 @@ export function AgentGenerationWorkspaceDarkV2({ const response = await fetch( `/api/agent-workflow/reference-search?keyword=${encodeURIComponent( normalized - )}&queryType=${skuReplacementQueryType}&target=sku`, + )}&queryType=${skuReplacementQueryType}&target=${skuReferenceTarget}`, { cache: "no-store" } ); const result = (await response.json().catch(() => ({}))) as AgentModeReferenceSearchResponse; if (!response.ok || !result?.success || !result?.data) { - throw new Error(result?.error?.message || "SKU 参考图查询失败"); + throw new Error(result?.error?.message || `${skuReferenceSourceLabel}参考图查询失败`); } updateSkuReplacementState({ @@ -1547,16 +1578,18 @@ export function AgentGenerationWorkspaceDarkV2({ isLoading: false, lastQuery: normalized, }); - toast.success(`已载入 ${result.data.references?.length || 0} 张 SKU 参考图`); + toast.success(`已载入 ${result.data.references?.length || 0} 张${skuReferenceSourceLabel}参考图`); } catch (error) { setSkuReplacementSearchState({ isLoading: false, lastQuery: normalized, }); - toast.error(error instanceof Error ? error.message : "SKU 参考图查询失败"); + toast.error(error instanceof Error ? error.message : `${skuReferenceSourceLabel}参考图查询失败`); } }, [ selectedProductAttachments.length, + skuReferenceSourceLabel, + skuReferenceTarget, skuReplacementQueryInput, skuReplacementQueryType, updateSkuReplacementState, @@ -1570,12 +1603,13 @@ export function AgentGenerationWorkspaceDarkV2({ selectedCandidateId: candidateId, references: buildReferenceCardsFromCandidate({ candidate: nextCandidate, - target: "sku", + target: skuReferenceTarget, productCount: selectedProductAttachments.length, + assignProductIndex: true, }), }; }); - }, [selectedProductAttachments.length, updateSkuReplacementState]); + }, [selectedProductAttachments.length, skuReferenceTarget, updateSkuReplacementState]); const handleAssignSkuReferenceProduct = useCallback((cardId: string, productIndex: number) => { updateSkuReplacementState((current) => ({ ...current, @@ -2191,9 +2225,9 @@ export function AgentGenerationWorkspaceDarkV2({
-
详情参考图
+
{detailReferenceSourceLabel}参考图
- 仅支持按 SPU 查询任务-详情整套候选,并切换某一套的详情参考图。 + 仅支持按 SPU 查询任务整套候选,并切换某一套的{detailReferenceSourceLabel}参考图。
@@ -2216,7 +2250,7 @@ export function AgentGenerationWorkspaceDarkV2({ queryType: "spu", }) } - placeholder="输入 SPU,查询任务-详情整套中的详情参考图" + placeholder={`输入 SPU,查询任务整套中的${detailReferenceSourceLabel}参考图`} className="min-h-[68px] resize-none rounded-[22px] border-white/10 bg-black/20 text-sm text-white placeholder:text-white/22 focus:border-emerald-400/30" /> {detailReplacementReferenceCards.length > 0 ? ( @@ -2253,7 +2287,7 @@ export function AgentGenerationWorkspaceDarkV2({ {detailReplacementSearchState.lastQuery || detailReplacementQueryInput.trim().toUpperCase()} - 已接入真实详情参考图接口 + {`已接入真实${detailReferenceSourceLabel}参考图接口`}
) : null} @@ -2271,7 +2305,7 @@ export function AgentGenerationWorkspaceDarkV2({
{detailReplacementCandidates.map((candidate) => { const isSelected = detailReplacementSelectedCandidateId === candidate.id; - const previewUrl = getCandidatePreviewUrl(candidate, "detail"); + const previewUrl = getCandidatePreviewUrl(candidate, detailReferenceTarget); return (
@@ -2500,7 +2534,7 @@ export function AgentGenerationWorkspaceDarkV2({
等待替换结果
- 详情参考图加载后,右侧会按相同顺序生成对应结果图,并逐屏支持文字编辑。 + {detailReferenceSourceLabel}参考图加载后,右侧会按相同顺序生成对应结果图,并逐屏支持文字编辑。
)} @@ -2514,7 +2548,7 @@ export function AgentGenerationWorkspaceDarkV2({
输入图
- 选中的白底图会和 SKU 参考图建立一对一替换关系 + 选中的白底图会和{skuReferenceSourceLabel}参考图建立一对一替换关系
@@ -2566,9 +2600,9 @@ export function AgentGenerationWorkspaceDarkV2({
-
SKU 参考图
+
{skuReferenceSourceLabel}参考图
- 支持按 SKU 或 SPU 查询任务-详情整套候选,并切换对应的 SKU 参考图。 + 支持按 SKU 或 SPU 查询任务整套候选,并切换对应的{skuReferenceSourceLabel}参考图。
@@ -2613,7 +2647,7 @@ export function AgentGenerationWorkspaceDarkV2({ query: e.target.value, }) } - placeholder={`输入${skuReplacementQueryType === "spu" ? "SPU" : "SKU"},查询任务-详情整套中的 SKU 参考图`} + placeholder={`输入${skuReplacementQueryType === "spu" ? "SPU" : "SKU"},查询任务整套中的${skuReferenceSourceLabel}参考图`} className="min-h-[68px] resize-none rounded-[22px] border-white/10 bg-black/20 text-sm text-white placeholder:text-white/22 focus:border-emerald-400/30" /> {skuReplacementReferenceCards.length > 0 ? ( @@ -2650,7 +2684,7 @@ export function AgentGenerationWorkspaceDarkV2({ {skuReplacementSearchState.lastQuery || skuReplacementQueryInput.trim().toUpperCase()} - 已接入真实 SKU 参考图接口 + {`已接入真实${skuReferenceSourceLabel}参考图接口`}
) : null} @@ -2668,7 +2702,7 @@ export function AgentGenerationWorkspaceDarkV2({
{skuReplacementCandidates.map((candidate) => { const isSelected = skuReplacementSelectedCandidateId === candidate.id; - const previewUrl = getCandidatePreviewUrl(candidate, "sku"); + const previewUrl = getCandidatePreviewUrl(candidate, skuReferenceTarget); return (
@@ -2930,7 +2964,7 @@ export function AgentGenerationWorkspaceDarkV2({
等待替换结果
- SKU 参考图加载后,右侧会按相同顺序生成一对一替换结果,并支持批量下载。 + {skuReferenceSourceLabel}参考图加载后,右侧会按相同顺序生成一对一替换结果,并支持批量下载。
)} diff --git a/components/agents/AgentPresetDialog.tsx b/components/agents/AgentPresetDialog.tsx index d861ef118..d493be02d 100644 --- a/components/agents/AgentPresetDialog.tsx +++ b/components/agents/AgentPresetDialog.tsx @@ -44,6 +44,7 @@ import type { AgentCreatePayload, AgentPresetModeType, AgentPresetModeConfig, + AgentTaskDetailReferenceSource, AgentPresetType, AgentPreset, AgentPresetSlot, @@ -102,6 +103,28 @@ const IMAGE_MODE_OPTIONS: Array<{ }, ]; +const TASK_DETAIL_REFERENCE_SOURCE_OPTIONS: Array<{ + value: AgentTaskDetailReferenceSource; + label: string; + description: string; +}> = [ + { + value: "main", + label: "主图", + description: "获取任务整套里的主图作为参考图来源。", + }, + { + value: "sku", + label: "SKU图", + description: "获取任务整套里的 SKU 图作为参考图来源。", + }, + { + value: "detail", + label: "详情图", + description: "获取任务整套里的详情图作为参考图来源。", + }, +]; + export function AgentPresetDialog({ open, onOpenChange, @@ -1209,6 +1232,69 @@ export function AgentPresetDialog({
+
+
+ 参考图来源 +
+ +
+ {TASK_DETAIL_REFERENCE_SOURCE_OPTIONS.find( + (option) => + option.value === + (isDetailReplacementModeBuilder + ? modeConfig.detail.referenceImageSource + : modeConfig.sku.referenceImageSource) + )?.description || "根据模式配置自动获取对应参考图"} +
+
+
模特图槽位
@@ -1284,8 +1370,8 @@ export function AgentPresetDialog({
{isDetailReplacementModeBuilder - ? "详情替换模式不需要上传模板参考图,第二步会通过 SPU 获取详情参考图,再把输入图逐张替换到详情参考图中。" - : "SKU替换模式会把用户选中的白底图与参考图建立一对一关系,后续在第二步专用工作台中执行替换。"} + ? `详情替换模式不需要上传模板参考图,第二步会通过 SPU 获取${TASK_DETAIL_REFERENCE_SOURCE_OPTIONS.find((option) => option.value === modeConfig.detail.referenceImageSource)?.label || "详情图"},再把输入图逐张替换到对应参考图中。` + : `SKU替换模式会把用户选中的白底图与${TASK_DETAIL_REFERENCE_SOURCE_OPTIONS.find((option) => option.value === modeConfig.sku.referenceImageSource)?.label || "SKU图"}建立一对一关系,后续在第二步专用工作台中执行替换。`}
)} diff --git a/lib/agents/preset-mode-config.ts b/lib/agents/preset-mode-config.ts index b39d8da29..63a7f7a80 100644 --- a/lib/agents/preset-mode-config.ts +++ b/lib/agents/preset-mode-config.ts @@ -1,6 +1,7 @@ import type { AgentPresetModeConfig, AgentPresetModeType, + AgentTaskDetailReferenceSource, } from "@/types/agent"; export const defaultAgentSceneGenerationModeConfig = { @@ -14,12 +15,14 @@ export const defaultAgentDetailReplacementModeConfig = { enableTextEditing: true, productImageSlotCount: 2, includeModelSlot: true, + referenceImageSource: "detail", } as const; export const defaultAgentSkuReplacementModeConfig = { enableTextEditing: true, productImageSlotCount: 2, includeModelSlot: true, + referenceImageSource: "sku", } as const; export const defaultAgentPresetModeConfig: AgentPresetModeConfig = { @@ -39,6 +42,13 @@ function normalizeInteger(value: unknown, fallback: number, min: number, max: nu return Math.min(max, Math.max(min, Math.round(normalized))); } +function normalizeReferenceSource( + value: unknown, + fallback: AgentTaskDetailReferenceSource +): AgentTaskDetailReferenceSource { + return value === "main" || value === "sku" || value === "detail" ? value : fallback; +} + export function normalizeAgentPresetModeConfig(value: unknown): AgentPresetModeConfig { const raw = value && typeof value === "object" && !Array.isArray(value) @@ -108,6 +118,10 @@ export function normalizeAgentPresetModeConfig(value: unknown): AgentPresetModeC detailRaw.includeModelSlot, defaultAgentDetailReplacementModeConfig.includeModelSlot ), + referenceImageSource: normalizeReferenceSource( + detailRaw.referenceImageSource, + defaultAgentDetailReplacementModeConfig.referenceImageSource + ), }, sku: { enableTextEditing: normalizeBoolean( @@ -124,6 +138,10 @@ export function normalizeAgentPresetModeConfig(value: unknown): AgentPresetModeC skuRaw.includeModelSlot, defaultAgentSkuReplacementModeConfig.includeModelSlot ), + referenceImageSource: normalizeReferenceSource( + skuRaw.referenceImageSource, + defaultAgentSkuReplacementModeConfig.referenceImageSource + ), }, }; } diff --git a/types/agent.ts b/types/agent.ts index 915f4c138..6cbb2710f 100644 --- a/types/agent.ts +++ b/types/agent.ts @@ -8,6 +8,8 @@ export type AgentPresetModeType = | "detail_replacement" | "sku_replacement"; +export type AgentTaskDetailReferenceSource = "main" | "sku" | "detail"; + export interface AgentSceneGenerationModeConfig { productImageSlotCount: number; includeModelSlot: boolean; @@ -19,12 +21,14 @@ export interface AgentDetailReplacementModeConfig { enableTextEditing: boolean; productImageSlotCount: number; includeModelSlot: boolean; + referenceImageSource: AgentTaskDetailReferenceSource; } export interface AgentSkuReplacementModeConfig { enableTextEditing: boolean; productImageSlotCount: number; includeModelSlot: boolean; + referenceImageSource: AgentTaskDetailReferenceSource; } export interface AgentPresetModeConfig {