feat: add workflow page feature toggles
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-19 15:36:29 +08:00
parent 9422c5de73
commit 076e4f55e6
6 changed files with 512 additions and 232 deletions

View File

@ -33,6 +33,7 @@ import {
import {
AGENT_WORKFLOW_PROMPT_FRAGMENT_MAX_LENGTH,
defaultAgentWorkflowConstraintSettings,
defaultAgentWorkflowPageFeatureSettings,
AGENT_WORKFLOW_MAX_OPTION_DEPTH,
AGENT_WORKFLOW_MAX_TREE_DEPTH,
countAgentWorkflowOptions,
@ -46,6 +47,7 @@ import {
getEnabledAgentWorkflowOptionTree,
getAgentWorkflowCategoryLabels,
getAgentWorkflowConstraintSettings,
getAgentWorkflowPageFeatureSettings,
normalizeAgentWorkflowOptionOrdering,
flattenAgentWorkflowSteps,
normalizeAgentWorkflowStepOrdering,
@ -1511,6 +1513,10 @@ export function AgentWorkflowSettingsClient({
() => getAgentWorkflowConstraintSettings(config),
[config]
);
const workflowPageFeatureSettings = useMemo(
() => getAgentWorkflowPageFeatureSettings(config),
[config]
);
const firstCategoryLabel = categoryLabels.firstCategory;
const secondCategoryLabel = categoryLabels.secondCategory;
const thirdCategoryLabel = categoryLabels.thirdCategory;
@ -4642,6 +4648,91 @@ export function AgentWorkflowSettingsClient({
variants={containerVariants}
className="grid gap-6 lg:grid-cols-2"
>
<motion.div variants={itemVariants} className="lg:col-span-2">
<Card>
<CardHeader className="pb-4">
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-violet-500/10">
<Settings2 className="h-4 w-4 text-violet-600" />
</div>
<div>
<CardTitle className="text-base">前台流程功能开关</CardTitle>
<CardDescription>
控制风格化页面是否展示选择生图模式、选择 SPU / SKU 和套 PSD 模板入口
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="grid gap-3 md:grid-cols-3">
<div className="flex items-center justify-between rounded-lg border bg-muted/20 px-4 py-3">
<div className="pr-4">
<div className="text-sm font-medium">开启第一步:选择生图模式</div>
<div className="text-xs text-muted-foreground">
关闭后前台不再要求选择生图模式,直接进入后续配置并选择模型生图。
</div>
</div>
<Switch
checked={workflowPageFeatureSettings.presetStepEnabled}
onCheckedChange={(checked) =>
setConfig((prev) => ({
...prev,
pageFeatureSettings: {
...(prev.pageFeatureSettings ||
defaultAgentWorkflowPageFeatureSettings),
presetStepEnabled: checked,
},
}))
}
/>
</div>
<div className="flex items-center justify-between rounded-lg border bg-muted/20 px-4 py-3">
<div className="pr-4">
<div className="text-sm font-medium">开启第二步:选择 SPU / SKU</div>
<div className="text-xs text-muted-foreground">
关闭后前台跳过商品素材搜索,不再强制补齐 SPU / SKU 输入图槽位。
</div>
</div>
<Switch
checked={workflowPageFeatureSettings.materialStepEnabled}
onCheckedChange={(checked) =>
setConfig((prev) => ({
...prev,
pageFeatureSettings: {
...(prev.pageFeatureSettings ||
defaultAgentWorkflowPageFeatureSettings),
materialStepEnabled: checked,
},
}))
}
/>
</div>
<div className="flex items-center justify-between rounded-lg border bg-muted/20 px-4 py-3">
<div className="pr-4">
<div className="text-sm font-medium">开启套 PSD 模板</div>
<div className="text-xs text-muted-foreground">
关闭后前台隐藏所有“套PSD”按钮和 PSD 模板编辑弹窗入口。
</div>
</div>
<Switch
checked={workflowPageFeatureSettings.psdTemplateEnabled}
onCheckedChange={(checked) =>
setConfig((prev) => ({
...prev,
pageFeatureSettings: {
...(prev.pageFeatureSettings ||
defaultAgentWorkflowPageFeatureSettings),
psdTemplateEnabled: checked,
},
}))
}
/>
</div>
</CardContent>
</Card>
</motion.div>
<motion.div variants={itemVariants} className="lg:col-span-2">
<Card>
<CardHeader className="pb-4">

View File

@ -1060,6 +1060,13 @@ const updateSchema = z.object({
crossLevelMutualExclusionEnabled: z.boolean().optional(),
})
.optional(),
pageFeatureSettings: z
.object({
presetStepEnabled: z.boolean().optional(),
materialStepEnabled: z.boolean().optional(),
psdTemplateEnabled: z.boolean().optional(),
})
.optional(),
steps: z
.array(agentWorkflowStepSchema)
.max(20),

View File

@ -177,6 +177,7 @@ import {
defaultAgentWorkflowUiConfig,
flattenVisibleAgentWorkflowSteps,
getEnabledAgentWorkflowStepTree,
getAgentWorkflowPageFeatureSettings,
normalizeAgentWorkflowUiConfig,
sanitizeAgentWorkflowSelections,
} from "@/lib/agents/workflow-ui-config";
@ -1552,6 +1553,11 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
agentWorkflowBrandModelConfig?: unknown;
agentPsdTemplateCategoryTagIndex?: AgentPsdTemplateCategoryTagIndex;
agentWorkflowUiConfig?: {
pageFeatureSettings?: {
presetStepEnabled?: boolean;
materialStepEnabled?: boolean;
psdTemplateEnabled?: boolean;
};
steps?: Array<{
key: AgentWorkflowStepKey;
title: string;
@ -1600,6 +1606,14 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
normalizeAgentWorkflowUiConfig(siteSettingsData?.agentWorkflowUiConfig),
[siteSettingsData?.agentWorkflowUiConfig]
);
const workflowPageFeatureSettings = useMemo(
() => getAgentWorkflowPageFeatureSettings(resolvedWorkflowUiConfig),
[resolvedWorkflowUiConfig]
);
const isWorkflowPresetStepEnabled =
workflowPageFeatureSettings.presetStepEnabled !== false;
const isWorkflowMaterialStepEnabled =
workflowPageFeatureSettings.materialStepEnabled !== false;
const sanitizedWorkflowSelections = useMemo(
() =>
sanitizeAgentWorkflowSelections({
@ -8152,11 +8166,14 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
selections: workflowSelectionsRef.current,
});
}, [resolvedWorkflowUiConfig]);
const effectiveSelectedAgentPresetId = isWorkflowPresetStepEnabled
? selectedAgentPresetId
: null;
const { data: selectedAgentPresetResponse } = useSWR<{
success?: boolean;
data?: AgentPreset;
}>(
selectedAgentPresetId ? `/api/agents/${selectedAgentPresetId}` : null,
effectiveSelectedAgentPresetId ? `/api/agents/${effectiveSelectedAgentPresetId}` : null,
fetcher
);
const selectedAgentPresetData = useMemo(
@ -8519,35 +8536,39 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/>
}
brandStorePanel={
<AgentWorkflowBrandStorePanel
config={resolvedWorkflowUiConfig}
inputs={workflowInputs}
onInputsChange={handleWorkflowInputsChange}
psdTemplateCategoryTagIndex={
siteSettingsData?.agentPsdTemplateCategoryTagIndex
}
theme="dark"
/>
isWorkflowPresetStepEnabled ? (
<AgentWorkflowBrandStorePanel
config={resolvedWorkflowUiConfig}
inputs={workflowInputs}
onInputsChange={handleWorkflowInputsChange}
psdTemplateCategoryTagIndex={
siteSettingsData?.agentPsdTemplateCategoryTagIndex
}
theme="dark"
/>
) : null
}
chatId={agentModeChatId}
deletedResultCollections={agentProjectDeletedResultCollections}
getLatestWorkflowContext={getLatestWorkflowContext}
materialSelectionPanel={
<AgentWorkflowMaterialSelectionPanel
appearanceRecognitionEnabled={appearanceRecognitionEnabled}
disabled={!selectedAgentPresetId}
inputs={workflowInputs}
materials={workflowMaterials}
modelImageRequirement={materialModelImageRequirement}
onAppearanceRecognitionEnabledChange={
handleAppearanceRecognitionEnabledChange
}
onInputsChange={handleWorkflowInputsChange}
onMaterialsChange={handleWorkflowMaterialsChange}
productSelectionLimit={materialProductSelectionLimit}
theme="dark"
totalUploadRequirement={materialTotalUploadRequirement}
/>
isWorkflowMaterialStepEnabled ? (
<AgentWorkflowMaterialSelectionPanel
appearanceRecognitionEnabled={appearanceRecognitionEnabled}
disabled={isWorkflowPresetStepEnabled && !selectedAgentPresetId}
inputs={workflowInputs}
materials={workflowMaterials}
modelImageRequirement={materialModelImageRequirement}
onAppearanceRecognitionEnabledChange={
handleAppearanceRecognitionEnabledChange
}
onInputsChange={handleWorkflowInputsChange}
onMaterialsChange={handleWorkflowMaterialsChange}
productSelectionLimit={materialProductSelectionLimit}
theme="dark"
totalUploadRequirement={materialTotalUploadRequirement}
/>
) : null
}
onAgentModeStateChange={setAgentModeState}
onClearPreset={() => setSelectedAgentPresetId(null)}
@ -8560,17 +8581,19 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
onResultPreviewUrlsChange={handleAgentResultPreviewUrlsChange}
onWorkspaceStateChange={setAgentWorkspaceState}
presetPanel={
<AgentPresetPanelDarkV2
embedded
embeddedEntityLabel="生图模式"
onSelectPreset={setSelectedAgentPresetId}
selectedPresetId={selectedAgentPresetId}
/>
isWorkflowPresetStepEnabled ? (
<AgentPresetPanelDarkV2
embedded
embeddedEntityLabel="生图模式"
onSelectPreset={setSelectedAgentPresetId}
selectedPresetId={selectedAgentPresetId}
/>
) : null
}
projectId={agentProjectId}
resultCollections={agentProjectResultCollections}
selectedAgentPresetId={
isAgentWorkspaceEnabled ? selectedAgentPresetId : null
isAgentWorkspaceEnabled ? effectiveSelectedAgentPresetId : null
}
selectedBrandModel={selectedBrandModel}
showcaseConfig={siteSettingsData?.agentWorkflowShowcaseConfig}
@ -8580,6 +8603,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
syncGeneratedResultsToCanvas={false}
topNavRightSlot={agentProjectNavSlot}
workflowContext={workflowContext}
workflowFeatureSettings={workflowPageFeatureSettings}
// 嵌入的风格化选择面板
workflowPromptOptimizationEnabled={appearanceRecognitionEnabled}
workflowStylePanel={

View File

@ -82,6 +82,9 @@ import {
import {
normalizeAgentPresetModeConfig,
} from "@/lib/agents/preset-mode-config";
import {
normalizeAgentWorkflowPageFeatureSettings,
} from "@/lib/agents/workflow-ui-config";
import { buildAgentSceneImageRolePrompt } from "@/lib/agents/scene-image-role-prompt";
import type { AgentPsdTemplateMode } from "@/lib/agents/psd-template-config";
import type { Attachment, ChatMessage } from "@/lib/types";
@ -100,6 +103,7 @@ import type {
AgentWorkflowShowcaseConfig,
AgentWorkflowComposerContext,
AgentWorkflowMaterialQueryType,
AgentWorkflowPageFeatureSettings,
AgentWorkflowSelectedBrandModel,
} from "@/types/agent";
import {
@ -116,6 +120,7 @@ interface AgentGenerationWorkspaceDarkV2Props {
selectedAgentPresetId?: string | null;
selectedBrandModel?: AgentWorkflowSelectedBrandModel | null;
workflowContext?: AgentWorkflowComposerContext | null;
workflowFeatureSettings?: AgentWorkflowPageFeatureSettings | null;
workflowPromptOptimizationEnabled?: boolean;
getLatestWorkflowContext?: () => AgentWorkflowComposerContext | null;
onBack?: () => void;
@ -955,6 +960,7 @@ export function AgentGenerationWorkspaceDarkV2({
selectedAgentPresetId,
selectedBrandModel = null,
workflowContext = null,
workflowFeatureSettings = null,
workflowPromptOptimizationEnabled = true,
getLatestWorkflowContext,
onBack,
@ -1018,6 +1024,16 @@ export function AgentGenerationWorkspaceDarkV2({
}).workspaceState || createEmptyAgentGenerationProjectWorkspaceState(),
[workspaceState]
);
const resolvedWorkflowFeatureSettings = useMemo(
() => normalizeAgentWorkflowPageFeatureSettings(workflowFeatureSettings),
[workflowFeatureSettings]
);
const isPresetStepEnabled = resolvedWorkflowFeatureSettings.presetStepEnabled !== false;
const isMaterialStepEnabled = resolvedWorkflowFeatureSettings.materialStepEnabled !== false;
const isPsdTemplateEnabled = resolvedWorkflowFeatureSettings.psdTemplateEnabled !== false;
const requiresPresetSelection = isPresetStepEnabled;
const isDirectModelGenerationMode = !requiresPresetSelection;
const hasMaterialStep = isMaterialStepEnabled && Boolean(materialSelectionPanel);
const [input, setInput] = useState(normalizedIncomingWorkspaceState.input);
const [attachments, setAttachments] = useState<Array<PickedAttachment | null>>(
@ -1088,6 +1104,10 @@ export function AgentGenerationWorkspaceDarkV2({
mode?: AgentPsdTemplateMode | null;
resultModeType?: AgentGenerationResultModeType | null;
}) => {
if (!isPsdTemplateEnabled) {
toast.info("套 PSD 模板功能已关闭");
return;
}
setPsdTemplateDialogSourceUrl(
isValidChatHttpUrl(params?.sourceImageUrl)
? String(params?.sourceImageUrl).trim()
@ -1103,7 +1123,7 @@ export function AgentGenerationWorkspaceDarkV2({
);
setIsPsdTemplateDialogOpen(true);
},
[activeResultModeType]
[activeResultModeType, isPsdTemplateEnabled]
);
const normalizedDeletedResultCollections = useMemo(
() => ({
@ -1144,11 +1164,17 @@ export function AgentGenerationWorkspaceDarkV2({
saveStoredAgentPendingGenerations(allItems);
}, [chatId, pendingGeneration]);
useEffect(() => {
if (!isPsdTemplateEnabled && isPsdTemplateDialogOpen) {
closePsdTemplateDialog();
}
}, [closePsdTemplateDialog, isPsdTemplateDialogOpen, isPsdTemplateEnabled]);
const { data: historyData } = useSWR<{ messages: ChatMessage[] }>(
rawChatId ? `/api/messages?chatId=${rawChatId}` : null,
fetcher,
{
refreshInterval: selectedAgentPresetId ? 3000 : 0,
refreshInterval: selectedAgentPresetId || isDirectModelGenerationMode ? 3000 : 0,
revalidateOnFocus: true,
}
);
@ -1228,7 +1254,9 @@ export function AgentGenerationWorkspaceDarkV2({
: true
: false;
const modelSlotIndex = modelSlotEnabled ? productSlotCount : -1;
const hasAgentUploadSlots = Boolean(selectedAgentPreset && selectedAgentPreset.type !== "text");
const hasAgentUploadSlots = Boolean(
hasMaterialStep && selectedAgentPreset && selectedAgentPreset.type !== "text"
);
const agentUploadSlots = useMemo(
() => {
if (!hasAgentUploadSlots) return [];
@ -1330,6 +1358,31 @@ export function AgentGenerationWorkspaceDarkV2({
}, [referenceImages]);
const sceneSecondaryReferenceCount = sceneSecondaryReferenceAttachments.length;
const sceneMessageAttachments = useMemo(() => {
if (isDirectModelGenerationMode && hasMaterialStep) {
const seenKeys = new Set<string>();
return referenceImages
.map((image) =>
toWorkflowReferenceAttachment({
url: image.url,
name: image.name,
label: image.label,
mimeType: image.mimeType,
assetId: image.assetId || null,
})
)
.filter((attachment): attachment is PickedAttachment => {
if (!attachment) return false;
const key = getAttachmentIdentityKey(attachment);
if (key && seenKeys.has(key)) {
return false;
}
if (key) {
seenKeys.add(key);
}
return true;
});
}
if (!isSceneGenerationPreset) {
return attachments
.map((attachment) => sanitizeChatAttachment(attachment))
@ -1384,7 +1437,10 @@ export function AgentGenerationWorkspaceDarkV2({
return ordered;
}, [
attachments,
hasMaterialStep,
isDirectModelGenerationMode,
isSceneGenerationPreset,
referenceImages,
scenePrimaryReferenceCount,
sceneSecondaryReferenceAttachments,
selectedModelAttachment,
@ -1577,8 +1633,33 @@ export function AgentGenerationWorkspaceDarkV2({
);
const [activeToolStep, setActiveToolStep] = useState<ToolStepKey>("preset");
const hasBrandModelStep = Boolean(brandModelPanel) && modelSlotEnabled;
const hasMaterialStep = Boolean(materialSelectionPanel);
const hasWorkflowStyleStep = Boolean(workflowStylePanel) || hasBrandModelStep;
const visibleWorkflowSectionKeys = useMemo(
() => [
...(isPresetStepEnabled ? ["mode"] : []),
...(hasMaterialStep ? ["materials"] : []),
...(hasWorkflowStyleStep ? ["style"] : []),
"generate",
],
[hasMaterialStep, hasWorkflowStyleStep, isPresetStepEnabled]
);
const getWorkflowSectionBadge = useCallback(
(key: "mode" | "materials" | "style" | "generate") => {
const index = visibleWorkflowSectionKeys.indexOf(key);
return String(index >= 0 ? index + 1 : visibleWorkflowSectionKeys.length + 1).padStart(
2,
"0"
);
},
[visibleWorkflowSectionKeys]
);
const firstWorkflowSectionAnchor = isPresetStepEnabled
? "agent-anchor-mode"
: hasMaterialStep
? "agent-anchor-materials"
: hasWorkflowStyleStep
? "agent-anchor-style"
: "agent-anchor-generate";
const { messages, setMessages, sendMessage, status, stop } = useChat<ChatMessage>({
id: chatId,
@ -1591,11 +1672,15 @@ export function AgentGenerationWorkspaceDarkV2({
prepareSendMessagesRequest(request) {
const transportState = transportStateRef.current;
const resolvedAgentPresetId =
transportState.selectedAgentPresetId || selectedAgentPresetId || undefined;
requiresPresetSelection
? transportState.selectedAgentPresetId || selectedAgentPresetId || undefined
: undefined;
const resolvedSelectedModelId =
transportState.selectedModelId !== "auto"
? transportState.selectedModelId
: selectedAgentPreset?.defaultModelId || transportState.selectedModelId;
: selectedAgentPreset?.defaultModelId ||
modelsData?.defaultModelId ||
transportState.selectedModelId;
const replacementModePayload =
transportState.selectedAgentPresetType === "image" &&
(transportState.isDetailReplacementPreset || transportState.isSkuReplacementPreset)
@ -1652,12 +1737,14 @@ export function AgentGenerationWorkspaceDarkV2({
MAX_CHAT_HIDDEN_PROMPT_LENGTH
) || undefined,
workflowPromptOptimizationEnabled,
agentPresetId: isValidChatUuid(resolvedAgentPresetId)
agentPresetId: requiresPresetSelection && isValidChatUuid(resolvedAgentPresetId)
? resolvedAgentPresetId
: undefined,
selectedChatModel: String(resolvedSelectedModelId || "auto").trim() || "auto",
composerMode:
selectedAgentPreset?.type === "image" ? ("image" as const) : ("agent" as const),
isDirectModelGenerationMode || selectedAgentPreset?.type === "image"
? ("image" as const)
: ("agent" as const),
selectedVisibilityType: "private",
imageParams: transportState.imageParams,
contextPayload: {
@ -2268,25 +2355,26 @@ export function AgentGenerationWorkspaceDarkV2({
const mergedSettingsStepDescription = hasMaterialStep
? "搜索素材、补充要求、确认槽位并开始生成"
: "补充要求并开始生成";
const styleStepBadge = hasMaterialStep ? "03" : "02";
const generateStepBadge =
hasMaterialStep && hasWorkflowStyleStep
? "04"
: hasMaterialStep || hasWorkflowStyleStep
? "03"
: "02";
const modeStepBadge = getWorkflowSectionBadge("mode");
const materialStepBadge = getWorkflowSectionBadge("materials");
const styleStepBadge = getWorkflowSectionBadge("style");
const generateStepBadge = getWorkflowSectionBadge("generate");
const toolSteps = useMemo(
() => [
{
key: "preset" as const,
label: "生图模式",
description: "先选择一个模式,再继续后续配置",
icon: Wand2,
disabled: false,
completed: Boolean(selectedAgentPresetId),
optional: false,
},
...(isPresetStepEnabled
? [
{
key: "preset" as const,
label: "生图模式",
description: "先选择一个模式,再继续后续配置",
icon: Wand2,
disabled: false,
completed: Boolean(selectedAgentPresetId),
optional: false,
},
]
: []),
...(hasBrandModelStep
? [
{
@ -2294,7 +2382,7 @@ export function AgentGenerationWorkspaceDarkV2({
label: "品牌模特",
description: "根据品牌选择模特图",
icon: UserRound,
disabled: !selectedAgentPresetId,
disabled: requiresPresetSelection && !selectedAgentPresetId,
completed: hasBrandModelSelection,
optional: true,
},
@ -2305,9 +2393,9 @@ export function AgentGenerationWorkspaceDarkV2({
label: mergedSettingsStepLabel,
description: mergedSettingsStepDescription,
icon: Settings2,
disabled: !selectedAgentPresetId,
disabled: requiresPresetSelection && !selectedAgentPresetId,
completed:
Boolean(selectedAgentPresetId) &&
(!requiresPresetSelection || Boolean(selectedAgentPresetId)) &&
(Boolean(input.trim()) || hasMaterialSelections || hasSatisfiedSlotRequirement),
optional: false,
},
@ -2317,10 +2405,12 @@ export function AgentGenerationWorkspaceDarkV2({
hasBrandModelStep,
hasMaterialSelections,
hasSatisfiedSlotRequirement,
isPresetStepEnabled,
mergedSettingsStepDescription,
mergedSettingsStepLabel,
hasMaterialStep,
input,
requiresPresetSelection,
selectedAgentPresetId,
]
);
@ -2492,7 +2582,7 @@ export function AgentGenerationWorkspaceDarkV2({
}, [hasAgentUploadSlots, selectedAgentPresetId]);
useEffect(() => {
if (!selectedAgentPresetId) {
if (requiresPresetSelection && !selectedAgentPresetId) {
setActiveToolStep("preset");
return;
}
@ -2511,14 +2601,15 @@ export function AgentGenerationWorkspaceDarkV2({
});
}, [
hasBrandModelStep,
requiresPresetSelection,
selectedAgentPresetId,
]);
useEffect(() => {
if (!toolSteps.some((step) => step.key === activeToolStep)) {
setActiveToolStep(selectedAgentPresetId ? "settings" : "preset");
setActiveToolStep(toolSteps[0]?.key || "settings");
}
}, [activeToolStep, selectedAgentPresetId, toolSteps]);
}, [activeToolStep, toolSteps]);
useEffect(() => {
if (!syncGeneratedResultsToCanvas) {
@ -2705,7 +2796,7 @@ export function AgentGenerationWorkspaceDarkV2({
void handleRunAiDetailGeneration();
return;
}
if (!selectedAgentPresetId) {
if (requiresPresetSelection && !selectedAgentPresetId) {
toast.error("请先选择生图模式");
return;
}
@ -2725,7 +2816,7 @@ export function AgentGenerationWorkspaceDarkV2({
const resolvedModelId =
selectedModelId !== "auto"
? selectedModelId
: selectedAgentPreset?.defaultModelId || selectedModelId;
: selectedAgentPreset?.defaultModelId || modelsData?.defaultModelId || selectedModelId;
if (!resolvedModelId) {
toast.error("当前风格化未配置模型");
return;
@ -2846,7 +2937,9 @@ export function AgentGenerationWorkspaceDarkV2({
isDetailAiGenerationMethod,
isDetailReplacementPreset,
isSkuReplacementPreset,
modelsData?.defaultModelId,
requiredProductUploadCount,
requiresPresetSelection,
selectedAgentPreset,
selectedAgentPresetId,
selectableModels,
@ -3022,12 +3115,12 @@ export function AgentGenerationWorkspaceDarkV2({
const nextStep = toolSteps.find((step) => step.key === nextKey);
if (!nextStep) return;
if (nextStep.disabled) {
toast.error("请先选择生图模式");
toast.error(requiresPresetSelection ? "请先选择生图模式" : "当前步骤暂不可用");
return;
}
setActiveToolStep(nextKey);
},
[toolSteps]
[requiresPresetSelection, toolSteps]
);
const scrollToSection = useCallback((sectionId: string) => {
if (
@ -4323,9 +4416,9 @@ export function AgentGenerationWorkspaceDarkV2({
>
<AgentWorkflowShowcaseBanner
config={showcaseConfig}
actionLabel="开始选择模式"
actionLabel={isPresetStepEnabled ? "开始选择模式" : "开始配置"}
secondaryActionLabel={allGeneratedImages.length > 0 ? "查看结果预览" : undefined}
onActionClick={() => scrollToSection("agent-anchor-mode")}
onActionClick={() => scrollToSection(firstWorkflowSectionAnchor)}
onSecondaryActionClick={() => scrollToSection("agent-anchor-results")}
/>
</motion.section>
@ -4338,85 +4431,87 @@ export function AgentGenerationWorkspaceDarkV2({
className="rounded-[28px] border border-white/12 bg-white/[0.04] p-5 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]"
>
<div className="space-y-6">
<section id="agent-anchor-mode" className="scroll-mt-24 rounded-[24px] border border-white/10 bg-black/20 p-4">
<div className="mb-4 flex flex-col gap-4 border-b border-white/10 pb-4 lg:flex-row lg:items-start lg:justify-between">
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="rounded-full bg-emerald-500/18 px-2.5 py-1 text-[11px] font-semibold text-emerald-200">
01
</span>
<span className="text-sm font-semibold text-white">选择生图模式</span>
</div>
<p className="text-sm text-white/45">
先确定本次使用的模式,并完成品牌和店铺选择,后续的商品图、品牌模特、参考图和结果展示都会随之联动。
</p>
{isDetailReplacementPreset || isSkuReplacementPreset ? (
<p className="text-xs text-amber-300/80">
{isDetailReplacementPreset
? isDetailAiGenerationMethod
? "当前模板已切换到 AI详情生成,系统会直接复用智能规划详情链路生成整套详情图。"
: "当前模板属于参考图替换模式,结果区会按参考图顺序逐屏展示。"
: "当前模板属于 SKU 替换模式,结果区会按参考图顺序一对一展示。"}
{isPresetStepEnabled ? (
<section id="agent-anchor-mode" className="scroll-mt-24 rounded-[24px] border border-white/10 bg-black/20 p-4">
<div className="mb-4 flex flex-col gap-4 border-b border-white/10 pb-4 lg:flex-row lg:items-start lg:justify-between">
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="rounded-full bg-emerald-500/18 px-2.5 py-1 text-[11px] font-semibold text-emerald-200">
{modeStepBadge}
</span>
<span className="text-sm font-semibold text-white">选择生图模式</span>
</div>
<p className="text-sm text-white/45">
先确定本次使用的模式,并完成品牌和店铺选择,后续的商品图、品牌模特、参考图和结果展示都会随之联动。
</p>
) : null}
</div>
<div className="flex flex-wrap items-center gap-2">
{selectedAgentPreset ? (
<div className="flex items-center gap-3 rounded-[20px] border border-white/10 bg-black/20 px-3 py-2">
{selectedAgentPreset.avatarUrl ? (
<img
src={selectedAgentPreset.avatarUrl}
alt={selectedAgentPreset.name}
className="h-10 w-10 rounded-xl object-cover"
/>
) : (
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-500/18">
<Wand2 className="h-5 w-5 text-emerald-300" />
</div>
)}
<div>
<div className="text-sm font-medium text-white">{selectedAgentPreset.name}</div>
<div className="text-xs text-white/40">
{selectedAgentPreset.type === "image" ? "生图模式" : "视频模式"}
{isDetailReplacementPreset || isSkuReplacementPreset ? (
<p className="text-xs text-amber-300/80">
{isDetailReplacementPreset
? isDetailAiGenerationMethod
? "当前模板已切换到 AI详情生成,系统会直接复用智能规划详情链路生成整套详情图。"
: "当前模板属于参考图替换模式,结果区会按参考图顺序逐屏展示。"
: "当前模板属于 SKU 替换模式,结果区会按参考图顺序一对一展示。"}
</p>
) : null}
</div>
<div className="flex flex-wrap items-center gap-2">
{selectedAgentPreset ? (
<div className="flex items-center gap-3 rounded-[20px] border border-white/10 bg-black/20 px-3 py-2">
{selectedAgentPreset.avatarUrl ? (
<img
src={selectedAgentPreset.avatarUrl}
alt={selectedAgentPreset.name}
className="h-10 w-10 rounded-xl object-cover"
/>
) : (
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-500/18">
<Wand2 className="h-5 w-5 text-emerald-300" />
</div>
)}
<div>
<div className="text-sm font-medium text-white">{selectedAgentPreset.name}</div>
<div className="text-xs text-white/40">
{selectedAgentPreset.type === "image" ? "生图模式" : "视频模式"}
</div>
</div>
</div>
</div>
) : null}
{selectedAgentPreset && onClearPreset ? (
<button
type="button"
onClick={onClearPreset}
className="rounded-full border border-white/10 bg-white/[0.03] px-3 py-2 text-xs text-emerald-300 transition-colors hover:border-emerald-300/35 hover:text-emerald-200"
>
更换生图模式
</button>
) : null}
) : null}
{selectedAgentPreset && onClearPreset ? (
<button
type="button"
onClick={onClearPreset}
className="rounded-full border border-white/10 bg-white/[0.03] px-3 py-2 text-xs text-emerald-300 transition-colors hover:border-emerald-300/35 hover:text-emerald-200"
>
更换生图模式
</button>
) : null}
</div>
</div>
</div>
{presetPanel ? (
<div className="agent-dark-scrollbar max-h-[760px] overflow-y-auto pr-1">
{presetPanel}
</div>
) : (
<div className="flex min-h-[220px] items-center justify-center rounded-[24px] border border-dashed border-white/12 bg-black/20 text-sm text-white/45">
暂无可用的生图模式配置。
</div>
)}
{presetPanel ? (
<div className="agent-dark-scrollbar max-h-[760px] overflow-y-auto pr-1">
{presetPanel}
</div>
) : (
<div className="flex min-h-[220px] items-center justify-center rounded-[24px] border border-dashed border-white/12 bg-black/20 text-sm text-white/45">
暂无可用的生图模式配置。
</div>
)}
{brandStorePanel ? (
<div className="mt-4 border-t border-white/10 pt-4">
{brandStorePanel}
</div>
) : null}
</section>
{brandStorePanel ? (
<div className="mt-4 border-t border-white/10 pt-4">
{brandStorePanel}
</div>
) : null}
</section>
) : null}
{hasMaterialStep ? (
<section id="agent-anchor-materials" className="scroll-mt-24 rounded-[24px] border border-white/10 bg-black/20 p-4">
<div className="mb-4 space-y-1 border-b border-white/10 pb-4">
<div className="flex items-center gap-2">
<span className="rounded-full bg-emerald-500/18 px-2.5 py-1 text-[11px] font-semibold text-emerald-200">
02
{materialStepBadge}
</span>
<span className="text-sm font-semibold text-white">选择 SPU / SKU</span>
</div>
@ -4607,7 +4702,7 @@ export function AgentGenerationWorkspaceDarkV2({
onClick={handleSend}
disabled={
isAnyGenerationRunning ||
!selectedAgentPresetId ||
(requiresPresetSelection && !selectedAgentPresetId) ||
(isDetailAiGenerationMethod
? detailGenerationValidationIssues.length > 0
: hasAgentUploadSlots && !hasSatisfiedSlotRequirement)
@ -5067,18 +5162,20 @@ export function AgentGenerationWorkspaceDarkV2({
{detailGenerationExpectedCount}
</div>
) : null}
<button
type="button"
onClick={() =>
openPsdTemplateDialog({
mode: getPsdTemplateModeByResultMode(activeResultModeType),
})
}
className="inline-flex items-center gap-1.5 rounded-full border border-white/10 bg-white/[0.03] px-3 py-1 text-xs text-white/72 transition-colors hover:border-white/20 hover:bg-white/[0.08] hover:text-white"
>
<ImagePlus className="h-3.5 w-3.5" />
<span>套PSD</span>
</button>
{isPsdTemplateEnabled ? (
<button
type="button"
onClick={() =>
openPsdTemplateDialog({
mode: getPsdTemplateModeByResultMode(activeResultModeType),
})
}
className="inline-flex items-center gap-1.5 rounded-full border border-white/10 bg-white/[0.03] px-3 py-1 text-xs text-white/72 transition-colors hover:border-white/20 hover:bg-white/[0.08] hover:text-white"
>
<ImagePlus className="h-3.5 w-3.5" />
<span>套PSD</span>
</button>
) : null}
</div>
</div>
@ -5913,28 +6010,30 @@ export function AgentGenerationWorkspaceDarkV2({
>
<Maximize2 className="h-4 w-4" />
</button>
<button
type="button"
onClick={(event) => {
event.stopPropagation();
setSelectedResultUrl(card.url!);
openPsdTemplateDialog({
sourceImageUrl: card.url!,
mode: getPsdTemplateModeByResultMode(activeResultModeType),
});
}}
className="absolute left-14 top-3 flex items-center gap-1 rounded-full border border-white/10 bg-black/60 px-2.5 py-1 text-[11px] text-white transition-colors hover:border-white/20"
aria-label="套用 PSD 模板"
>
<ImagePlus className="h-3.5 w-3.5" />
<span>套PSD</span>
</button>
{isPsdTemplateEnabled ? (
<button
type="button"
onClick={(event) => {
event.stopPropagation();
setSelectedResultUrl(card.url!);
openPsdTemplateDialog({
sourceImageUrl: card.url!,
mode: getPsdTemplateModeByResultMode(activeResultModeType),
});
}}
className="absolute left-14 top-3 flex items-center gap-1 rounded-full border border-white/10 bg-black/60 px-2.5 py-1 text-[11px] text-white transition-colors hover:border-white/20"
aria-label="套用 PSD 模板"
>
<ImagePlus className="h-3.5 w-3.5" />
<span>套PSD</span>
</button>
) : null}
<div className="pointer-events-none absolute inset-x-3 bottom-3 rounded-2xl border border-white/10 bg-black/55 px-3 py-2 text-[11px] text-white/78 backdrop-blur">
双击查看大图
</div>
</>
) : null}
{!isSceneResultCard ? (
{!isSceneResultCard && isPsdTemplateEnabled ? (
<button
type="button"
onClick={(event) => {
@ -6031,29 +6130,35 @@ export function AgentGenerationWorkspaceDarkV2({
<Wand2 className="h-8 w-8 text-white/20" />
</div>
<p className="text-base font-medium text-white/60">
{selectedAgentPresetId ? `${getResultModeLabel(activeResultModeType)}结果暂为空` : "请选择生图模式"}
{!requiresPresetSelection || selectedAgentPresetId
? `${getResultModeLabel(activeResultModeType)}结果暂为空`
: "请选择生图模式"}
</p>
<p className="mt-2 text-sm text-white/30">
{selectedAgentPresetId
? "切换到对应生图模式并开始生成后,结果会保留在当前项目的对应分类中。"
{!requiresPresetSelection || selectedAgentPresetId
? "选择模型并开始生成后,结果会保留在当前项目的对应分类中。"
: "请先从顶部流程线选择一个生图模式"}
</p>
<Button
type="button"
variant="outline"
onClick={() =>
openPsdTemplateDialog({
mode: getPsdTemplateModeByResultMode(activeResultModeType),
})
}
className="mt-5 rounded-full border-white/10 bg-white/[0.03] px-5 text-white hover:bg-white/[0.08]"
>
<ImagePlus className="mr-2 h-4 w-4" />
套PSD
</Button>
<p className="mt-2 text-xs text-white/35">
可直接进入模板页,并通过手动上传图片开始套版。
</p>
{isPsdTemplateEnabled ? (
<>
<Button
type="button"
variant="outline"
onClick={() =>
openPsdTemplateDialog({
mode: getPsdTemplateModeByResultMode(activeResultModeType),
})
}
className="mt-5 rounded-full border-white/10 bg-white/[0.03] px-5 text-white hover:bg-white/[0.08]"
>
<ImagePlus className="mr-2 h-4 w-4" />
套PSD
</Button>
<p className="mt-2 text-xs text-white/35">
可直接进入模板页,并通过手动上传图片开始套版。
</p>
</>
) : null}
</motion.div>
)}
</AnimatePresence>
@ -6080,61 +6185,63 @@ export function AgentGenerationWorkspaceDarkV2({
/>
) : null}
<ClientErrorBoundary
resetKeys={[
isPsdTemplateDialogOpen ? "open" : "closed",
psdTemplateDialogSourceUrl || "",
workflowContext?.brand || "",
workflowContext?.fifthCategory || "",
psdTemplateDialogMode,
]}
fallback={
<Dialog
{isPsdTemplateEnabled ? (
<ClientErrorBoundary
resetKeys={[
isPsdTemplateDialogOpen ? "open" : "closed",
psdTemplateDialogSourceUrl || "",
workflowContext?.brand || "",
workflowContext?.fifthCategory || "",
psdTemplateDialogMode,
]}
fallback={
<Dialog
open={isPsdTemplateDialogOpen}
onOpenChange={(open) => {
if (!open) {
closePsdTemplateDialog();
}
}}
>
<DialogContent
overlayClassName="bg-black/85 backdrop-blur-sm"
className="max-w-lg border border-white/10 bg-[#09090b] text-white"
>
<DialogHeader>
<DialogTitle>PSD 模板暂时无法打开</DialogTitle>
<DialogDescription>
已拦截本次客户端异常,避免整个风格化工作台被一起打断。
</DialogDescription>
</DialogHeader>
<div className="space-y-3 text-sm leading-6 text-white/72">
<p>请关闭弹窗后重试一次。</p>
<p>如果仍然出现,优先检查当前品牌下的 PSD 模板配置、图层数据或字体资源是否完整。</p>
</div>
</DialogContent>
</Dialog>
}
>
<AgentPsdTemplateEditorDialog
open={isPsdTemplateDialogOpen}
onOpenChange={(open) => {
if (!open) {
closePsdTemplateDialog();
}
}}
>
<DialogContent
overlayClassName="bg-black/85 backdrop-blur-sm"
className="max-w-lg border border-white/10 bg-[#09090b] text-white"
>
<DialogHeader>
<DialogTitle>PSD 模板暂时无法打开</DialogTitle>
<DialogDescription>
已拦截本次客户端异常,避免整个风格化工作台被一起打断。
</DialogDescription>
</DialogHeader>
<div className="space-y-3 text-sm leading-6 text-white/72">
<p>请关闭弹窗后重试一次。</p>
<p>如果仍然出现,优先检查当前品牌下的 PSD 模板配置、图层数据或字体资源是否完整。</p>
</div>
</DialogContent>
</Dialog>
}
>
<AgentPsdTemplateEditorDialog
open={isPsdTemplateDialogOpen}
onOpenChange={(open) => {
if (!open) {
closePsdTemplateDialog();
sourceImageUrl={psdTemplateDialogSourceUrl}
brandName={workflowContext?.brand || null}
selectedCategoryNames={[workflowContext?.fifthCategory || ""].filter(Boolean)}
sizeInfoText={workflowContext?.summaryText || null}
templateMode={psdTemplateDialogMode}
onDerivedImageCreated={
onResultPreviewUrlsChange ? handlePsdDerivedImageCreated : undefined
}
}}
sourceImageUrl={psdTemplateDialogSourceUrl}
brandName={workflowContext?.brand || null}
selectedCategoryNames={[workflowContext?.fifthCategory || ""].filter(Boolean)}
sizeInfoText={workflowContext?.summaryText || null}
templateMode={psdTemplateDialogMode}
onDerivedImageCreated={
onResultPreviewUrlsChange ? handlePsdDerivedImageCreated : undefined
}
onRenderedResultCreated={
onResultPreviewUrlsChange ? handlePsdRenderedResultCreated : undefined
}
/>
</ClientErrorBoundary>
onRenderedResultCreated={
onResultPreviewUrlsChange ? handlePsdRenderedResultCreated : undefined
}
/>
</ClientErrorBoundary>
) : null}
{/* Fullscreen Preview */}
<AnimatePresence>

View File

@ -4,6 +4,7 @@ import type {
AgentWorkflowConstraintSettings,
AgentWorkflowFourthCategoryEntry,
AgentWorkflowOptionUiConfig,
AgentWorkflowPageFeatureSettings,
AgentWorkflowStoreEntry,
AgentWorkflowThirdCategoryEntry,
AgentWorkflowStepKey,
@ -25,6 +26,11 @@ export const defaultAgentWorkflowCategoryLabels: AgentWorkflowCategoryLabels = {
export const defaultAgentWorkflowConstraintSettings: AgentWorkflowConstraintSettings = {
crossLevelMutualExclusionEnabled: false,
};
export const defaultAgentWorkflowPageFeatureSettings: AgentWorkflowPageFeatureSettings = {
presetStepEnabled: true,
materialStepEnabled: true,
psdTemplateEnabled: true,
};
export interface AgentWorkflowBrandCategoryOption {
value: string;
@ -169,6 +175,7 @@ export const defaultAgentWorkflowUiConfig: AgentWorkflowUiConfig = {
brandStoreEntries: [],
},
constraintSettings: defaultAgentWorkflowConstraintSettings,
pageFeatureSettings: defaultAgentWorkflowPageFeatureSettings,
steps: defaultSteps.map((step, index) =>
cloneStep({
...step,
@ -230,6 +237,30 @@ export function normalizeAgentWorkflowConstraintSettings(
};
}
export function normalizeAgentWorkflowPageFeatureSettings(
value: unknown
): AgentWorkflowPageFeatureSettings {
const record =
value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
return {
presetStepEnabled: normalizeBoolean(
record.presetStepEnabled ?? record.modeStepEnabled ?? record.enablePresetStep,
defaultAgentWorkflowPageFeatureSettings.presetStepEnabled
),
materialStepEnabled: normalizeBoolean(
record.materialStepEnabled ?? record.skuStepEnabled ?? record.enableMaterialStep,
defaultAgentWorkflowPageFeatureSettings.materialStepEnabled
),
psdTemplateEnabled: normalizeBoolean(
record.psdTemplateEnabled ?? record.enablePsdTemplate,
defaultAgentWorkflowPageFeatureSettings.psdTemplateEnabled
),
};
}
export function flattenAgentWorkflowSteps(
steps: AgentWorkflowStepUiConfig[],
depth = 1,
@ -1036,6 +1067,13 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
crossLevelMutualExclusionEnabled: raw.crossLevelMutualExclusionEnabled,
}
),
pageFeatureSettings: normalizeAgentWorkflowPageFeatureSettings(
raw.pageFeatureSettings ?? {
presetStepEnabled: raw.presetStepEnabled ?? raw.modeStepEnabled,
materialStepEnabled: raw.materialStepEnabled ?? raw.skuStepEnabled,
psdTemplateEnabled: raw.psdTemplateEnabled,
}
),
steps: sanitizedSteps,
};
}
@ -1056,6 +1094,12 @@ export function getAgentWorkflowConstraintSettings(
return normalizeAgentWorkflowConstraintSettings(config?.constraintSettings);
}
export function getAgentWorkflowPageFeatureSettings(
config?: AgentWorkflowUiConfig | null
) {
return normalizeAgentWorkflowPageFeatureSettings(config?.pageFeatureSettings);
}
export function getAgentWorkflowCategoryOptionsByBrand(params: {
config?: AgentWorkflowUiConfig | null;
brand?: string | null;

View File

@ -105,6 +105,12 @@ export interface AgentWorkflowConstraintSettings {
crossLevelMutualExclusionEnabled: boolean;
}
export interface AgentWorkflowPageFeatureSettings {
presetStepEnabled: boolean;
materialStepEnabled: boolean;
psdTemplateEnabled: boolean;
}
export interface AgentWorkflowUiConfig {
inputOptions?: {
brands: string[];
@ -117,6 +123,7 @@ export interface AgentWorkflowUiConfig {
brandStoreEntries?: AgentWorkflowBrandStoreEntry[];
};
constraintSettings?: AgentWorkflowConstraintSettings;
pageFeatureSettings?: AgentWorkflowPageFeatureSettings;
steps: AgentWorkflowStepUiConfig[];
}