diff --git a/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx b/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx index c773295e2..28dc60e8c 100644 --- a/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx +++ b/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx @@ -2717,11 +2717,23 @@ export function AgentWorkflowSettingsClient({ throw new Error(message); } - toast.success("风格化工作流配置已保存"); + const updatedSettings = await mutate(); + const savedWorkflowConfig = normalizeAgentWorkflowUiConfig( + updatedSettings?.agentWorkflowUiConfig + ); + const expectedStepCount = countAgentWorkflowSteps(config.steps || []); + const savedStepCount = countAgentWorkflowSteps(savedWorkflowConfig.steps || []); + if (savedStepCount !== expectedStepCount) { + toast.error( + `服务端回读到的模块数量不一致(本地 ${expectedStepCount} / 服务端 ${savedStepCount}),已保留本地自动草稿,请先不要刷新页面。` + ); + return; + } + removeAgentWorkflowSettingsDraft(); setAutoDraftUpdatedAt(null); setAutoDraftStatus("idle"); - await mutate(); + toast.success("风格化工作流配置已保存"); } catch (error) { toast.error(error instanceof Error ? error.message : "保存失败"); } finally { diff --git a/app/api/site-settings/route.ts b/app/api/site-settings/route.ts index 831fae190..b5f9f5d3f 100644 --- a/app/api/site-settings/route.ts +++ b/app/api/site-settings/route.ts @@ -701,6 +701,32 @@ export async function GET() { const AGENT_WORKFLOW_INPUT_OPTIONS_MAX_ITEMS = 5000; +const agentWorkflowOptionSchema: z.ZodTypeAny = z.lazy(() => + z.object({ + key: z.string().trim().min(1).max(64), + buttonName: z.string().trim().min(1).max(64), + description: z.string().trim().max(300).nullable().optional(), + previewImageUrl: z.string().trim().max(1000).nullable().optional(), + enabled: z.boolean().optional(), + sortOrder: z.number().min(0).max(999).optional(), + promptFragment: z.string().trim().max(1000).nullable().optional(), + blockedOptionKeys: z.array(z.string().trim().min(1).max(64)).max(100).optional(), + children: z.array(agentWorkflowOptionSchema).max(100).optional(), + }) +); + +const agentWorkflowStepSchema: z.ZodTypeAny = z.lazy(() => + z.object({ + key: z.string().trim().min(1).max(64), + title: z.string().trim().min(1).max(64), + description: z.string().trim().max(300).nullable().optional(), + enabled: z.boolean().optional(), + sortOrder: z.number().min(0).max(999).optional(), + options: z.array(agentWorkflowOptionSchema).max(100), + children: z.array(agentWorkflowStepSchema).max(100).optional(), + }) +); + const updateSchema = z.object({ siteName: z.string().min(1).max(128).optional(), siteLogo: z.string().nullable().optional(), @@ -892,35 +918,7 @@ const updateSchema = z.object({ }) .optional(), steps: z - .array( - z.object({ - key: z.string().trim().min(1).max(64), - title: z.string().trim().min(1).max(64), - description: z.string().trim().max(300).nullable().optional(), - enabled: z.boolean().optional(), - sortOrder: z.number().min(0).max(999).optional(), - options: z - .array( - z.lazy(() => - z.object({ - key: z.string().trim().min(1).max(64), - buttonName: z.string().trim().min(1).max(64), - description: z.string().trim().max(300).nullable().optional(), - previewImageUrl: z.string().trim().max(1000).nullable().optional(), - enabled: z.boolean().optional(), - sortOrder: z.number().min(0).max(999).optional(), - promptFragment: z.string().trim().max(1000).nullable().optional(), - blockedOptionKeys: z - .array(z.string().trim().min(1).max(64)) - .max(100) - .optional(), - children: z.array(z.any()).max(100).optional(), - }) - ) - ) - .max(100), - }) - ) + .array(agentWorkflowStepSchema) .max(20), }) .optional(),