From 040a1f514fc50b12b66ad7a7082b6754891f6375 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 18 May 2026 15:32:05 +0800 Subject: [PATCH] fix: improve agent workflow settings loading --- .../agent-workflow-settings-client.tsx | 51 ++++-- app/api/site-settings/route.ts | 158 ++++++++++++------ .../[id]/CanvasEditorLegacyPageClient.tsx | 67 ++++---- .../agents/AgentWorkflowBrandModelPicker.tsx | 20 ++- 4 files changed, 192 insertions(+), 104 deletions(-) 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 31988c1bc..b81874c6d 100644 --- a/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx +++ b/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx @@ -184,6 +184,9 @@ const WORKFLOW_STEP_DRAG_DATA_TYPE = "application/x-agent-workflow-step"; const WORKFLOW_SETTINGS_DRAFT_STORAGE_KEY = "agent-workflow-settings-draft:v1"; const WORKFLOW_SETTINGS_STALE_DRAFT_STORAGE_KEY = "agent-workflow-settings-stale-draft:v1"; const WORKFLOW_SETTINGS_DRAFT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; +const AGENT_WORKFLOW_SETTINGS_ENDPOINT = + "/api/site-settings?scope=agent-workflow-admin"; +const AGENT_WORKFLOW_SAVE_TIMEOUT_MS = 30_000; const DELETE_WORKFLOW_STEP_PASSWORD = "123456"; const BUSINESS_MODE_DEFAULT_MODEL_EMPTY_VALUE = "__agent_workflow_mode_default_model_empty__"; @@ -282,6 +285,24 @@ function scheduleWorkflowIdleTask(callback: () => void, timeoutMs = 2000) { return () => globalThis.clearTimeout(timer); } +async function fetchAgentWorkflowSettingsWithTimeout( + input: RequestInfo | URL, + init: RequestInit = {}, + timeoutMs = AGENT_WORKFLOW_SAVE_TIMEOUT_MS +) { + const controller = new AbortController(); + const timeoutId = globalThis.setTimeout(() => controller.abort(), timeoutMs); + + try { + return await fetch(input, { + ...init, + signal: controller.signal, + }); + } finally { + globalThis.clearTimeout(timeoutId); + } +} + function BufferedInput({ commitDelayMs = 450, onBlur, @@ -1881,7 +1902,7 @@ export function AgentWorkflowSettingsClient({ backLabel = "返回设置", }: AgentWorkflowSettingsClientProps = {}) { const { data, isLoading, mutate } = useSWR( - "/api/site-settings", + AGENT_WORKFLOW_SETTINGS_ENDPOINT, fetcher, { refreshInterval: 60_000 } ); @@ -3817,9 +3838,12 @@ export function AgentWorkflowSettingsClient({ const handleSave = async () => { try { setIsSaving(true); - const latestSettingsResponse = await fetch("/api/site-settings", { - cache: "no-store", - }); + const latestSettingsResponse = await fetchAgentWorkflowSettingsWithTimeout( + AGENT_WORKFLOW_SETTINGS_ENDPOINT, + { + cache: "no-store", + } + ); if (!latestSettingsResponse.ok) { throw new Error("保存前校验服务器配置失败,请刷新页面后重试"); @@ -3871,11 +3895,14 @@ export function AgentWorkflowSettingsClient({ changedPayload.agentWorkflowSettingsBaseSignatures[field] = baseSignatures[field]; } - const response = await fetch("/api/site-settings", { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(changedPayload), - }); + const response = await fetchAgentWorkflowSettingsWithTimeout( + "/api/site-settings", + { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(changedPayload), + } + ); if (!response.ok) { let message = "保存失败"; @@ -3939,7 +3966,11 @@ export function AgentWorkflowSettingsClient({ .join("、")}` ); } catch (error) { - toast.error(error instanceof Error ? error.message : "保存失败"); + if (error instanceof DOMException && error.name === "AbortError") { + toast.error("保存请求超时,请检查网络或稍后重试"); + } else { + toast.error(error instanceof Error ? error.message : "保存失败"); + } } finally { setIsSaving(false); } diff --git a/app/api/site-settings/route.ts b/app/api/site-settings/route.ts index 0764c3671..dcc0367ad 100644 --- a/app/api/site-settings/route.ts +++ b/app/api/site-settings/route.ts @@ -38,7 +38,6 @@ import { normalizeAgentWorkflowStepOrdering, normalizeAgentWorkflowUiConfig, } from "@/lib/agents/workflow-ui-config"; -import { optimizeAgentWorkflowUiConfigPreviewImages } from "@/lib/agents/workflow-preview-images"; import { AGENT_WORKFLOW_BRAND_MODEL_CONFIG_KEY, defaultAgentWorkflowBrandModelConfig, @@ -505,16 +504,17 @@ const PUBLIC_SITE_SETTINGS_SCOPES = new Set([ const SCOPED_SITE_SETTINGS_SERVER_CACHE_TTL_MS = 30_000; const scopedSiteSettingsServerCache = new Map< - PublicSiteSettingsScope, + string, { data: Record; expiresAt: number } >(); function createScopedSiteSettingsResponse( scope: PublicSiteSettingsScope, data: Record, - cacheStatus: "HIT" | "MISS" = "MISS" + cacheStatus: "HIT" | "MISS" = "MISS", + cacheKey: string = scope ) { - scopedSiteSettingsServerCache.set(scope, { + scopedSiteSettingsServerCache.set(cacheKey, { data, expiresAt: Date.now() + SCOPED_SITE_SETTINGS_SERVER_CACHE_TTL_MS, }); @@ -561,7 +561,13 @@ function createBrandingSettings(settings: Record | undefined) { }; } -async function getScopedPublicSiteSettings(scope: PublicSiteSettingsScope) { +async function getScopedPublicSiteSettings( + scope: PublicSiteSettingsScope, + options: { + brandName?: string; + cacheKey?: string; + } = {} +) { if (scope === "auth") { const { settings, valueByKey } = await readScopedSiteSettings([ "login_notification", @@ -580,11 +586,30 @@ async function getScopedPublicSiteSettings(scope: PublicSiteSettingsScope) { const { valueByKey } = await readScopedSiteSettings([ AGENT_WORKFLOW_BRAND_MODEL_CONFIG_KEY, ]); - return createScopedSiteSettingsResponse(scope, { - agentWorkflowBrandModelConfig: normalizeAgentWorkflowBrandModelConfig( - valueByKey.get(AGENT_WORKFLOW_BRAND_MODEL_CONFIG_KEY) - ), - }); + const brandModelConfig = normalizeAgentWorkflowBrandModelConfig( + valueByKey.get(AGENT_WORKFLOW_BRAND_MODEL_CONFIG_KEY) + ); + const normalizedBrandName = String(options.brandName || "") + .trim() + .toLowerCase(); + const scopedBrandModelConfig = normalizedBrandName + ? { + ...brandModelConfig, + entries: brandModelConfig.entries.filter( + (entry) => + entry.brandName.trim().toLowerCase() === normalizedBrandName + ), + } + : brandModelConfig; + + return createScopedSiteSettingsResponse( + scope, + { + agentWorkflowBrandModelConfig: scopedBrandModelConfig, + }, + "MISS", + options.cacheKey + ); } if (scope === "agent-presets") { @@ -603,24 +628,19 @@ async function getScopedPublicSiteSettings(scope: PublicSiteSettingsScope) { } if (scope === "agent-canvas") { - const [{ settings, valueByKey }, agentPsdTemplates] = await Promise.all([ - readScopedSiteSettings([ - REALTIME_CANVAS_CONFIG_KEY, - REALTIME_CANVAS_MODE_ICON_CONFIG_KEY, - CANVAS_TOOLBAR_CONFIG_SETTING_KEY, - LAYER_SPLIT_CONFIG_KEY, - ANGLE_EDIT_CONFIG_KEY, - CANVAS_AGENT_MODE_ENABLED_KEY, - VIDEO_CANVAS_ENABLED_KEY, - VIDEO_CANVAS_SIDEBAR_ENABLED_KEY, - ENHANCE_PRO_KREA_SETTING_KEY, - STYLE_PRO_CONFIG_KEY, - AGENT_WORKFLOW_UI_CONFIG_KEY, - AGENT_WORKFLOW_BRAND_MODEL_CONFIG_KEY, - AGENT_WORKFLOW_SHOWCASE_CONFIG_KEY, - AGENT_PSD_TEMPLATE_CATEGORY_TAG_CONFIG_KEY, - ]), - readAgentPsdTemplates().catch(() => []), + const { settings, valueByKey } = await readScopedSiteSettings([ + REALTIME_CANVAS_CONFIG_KEY, + REALTIME_CANVAS_MODE_ICON_CONFIG_KEY, + CANVAS_TOOLBAR_CONFIG_SETTING_KEY, + LAYER_SPLIT_CONFIG_KEY, + ANGLE_EDIT_CONFIG_KEY, + CANVAS_AGENT_MODE_ENABLED_KEY, + VIDEO_CANVAS_ENABLED_KEY, + VIDEO_CANVAS_SIDEBAR_ENABLED_KEY, + ENHANCE_PRO_KREA_SETTING_KEY, + STYLE_PRO_CONFIG_KEY, + AGENT_WORKFLOW_UI_CONFIG_KEY, + AGENT_WORKFLOW_SHOWCASE_CONFIG_KEY, ]); const canvasFeatureAvailability = await readCanvasFeatureAvailability( valueByKey @@ -629,11 +649,6 @@ async function getScopedPublicSiteSettings(scope: PublicSiteSettingsScope) { const videoCanvasSidebarEnabled = normalizeVideoCanvasSidebarEnabled( valueByKey.get(VIDEO_CANVAS_SIDEBAR_ENABLED_KEY) ); - const agentPsdTemplateCategoryTagConfig = - normalizeAgentPsdTemplateCategoryTagConfig( - valueByKey.get(AGENT_PSD_TEMPLATE_CATEGORY_TAG_CONFIG_KEY) - ); - return createScopedSiteSettingsResponse(scope, { ...createBrandingSettings(typedSettings), agentEnabled: typedSettings?.agentEnabled ?? true, @@ -667,16 +682,9 @@ async function getScopedPublicSiteSettings(scope: PublicSiteSettingsScope) { agentWorkflowUiConfig: normalizeAgentWorkflowUiConfig( valueByKey.get(AGENT_WORKFLOW_UI_CONFIG_KEY) ), - agentWorkflowBrandModelConfig: normalizeAgentWorkflowBrandModelConfig( - valueByKey.get(AGENT_WORKFLOW_BRAND_MODEL_CONFIG_KEY) - ), agentWorkflowShowcaseConfig: normalizeAgentWorkflowShowcaseConfig( valueByKey.get(AGENT_WORKFLOW_SHOWCASE_CONFIG_KEY) ), - agentPsdTemplateCategoryTagIndex: buildAgentPsdTemplateCategoryTagIndex({ - templates: agentPsdTemplates, - config: agentPsdTemplateCategoryTagConfig, - }), }); } @@ -842,23 +850,71 @@ async function getScopedPublicSiteSettings(scope: PublicSiteSettingsScope) { }); } +async function getAgentWorkflowAdminSettingsResponse() { + const { settings, valueByKey } = await readScopedSiteSettings([ + ...AGENT_WORKFLOW_SETTINGS_CONFLICT_KEYS, + ]); + const workflowSettings = + createAgentWorkflowSettingsStateFromValues(valueByKey); + const response = NextResponse.json({ + ...workflowSettings, + agentWorkflowDefaultModelId: + String( + (settings as { agentWorkflowDefaultModelId?: string | null } | undefined) + ?.agentWorkflowDefaultModelId || "" + ).trim() || null, + }); + + response.headers.set("Cache-Control", "no-store"); + response.headers.set("X-Site-Settings-Scope", "agent-workflow-admin"); + return response; +} + // GET /api/site-settings - 获取网站设置 (公开) export async function GET(request: NextRequest) { try { const scope = request.nextUrl.searchParams.get("scope") || ""; if (PUBLIC_SITE_SETTINGS_SCOPES.has(scope)) { const scopedScope = scope as PublicSiteSettingsScope; - const cached = scopedSiteSettingsServerCache.get(scopedScope); + const brandName = + scopedScope === "agent-brand-model" + ? request.nextUrl.searchParams.get("brand") || "" + : ""; + const cacheKey = brandName.trim() + ? `${scopedScope}:${brandName.trim().toLowerCase()}` + : scopedScope; + const cached = scopedSiteSettingsServerCache.get(cacheKey); if (cached && cached.expiresAt > Date.now()) { - return createScopedSiteSettingsResponse(scopedScope, cached.data, "HIT"); + return createScopedSiteSettingsResponse( + scopedScope, + cached.data, + "HIT", + cacheKey + ); } - return getScopedPublicSiteSettings(scopedScope); + return getScopedPublicSiteSettings(scopedScope, { + brandName, + cacheKey, + }); } const session = await auth().catch(() => null); const isAdmin = isSiteAdminType(session?.user?.type); const canManageAgentWorkflow = isAgentWorkflowAdminType(session?.user?.type); + + if (scope === "agent-workflow-admin") { + if (!session?.user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + if (!canManageAgentWorkflow) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + return getAgentWorkflowAdminSettingsResponse(); + } + const [ settings, notificationSetting, @@ -2316,14 +2372,9 @@ export async function PUT(request: NextRequest) { if (agentWorkflowUiConfig !== undefined) { const normalized = normalizeAgentWorkflowUiConfig(agentWorkflowUiConfig); - const { config: optimized, summary } = - await optimizeAgentWorkflowUiConfigPreviewImages(normalized); - if (summary.optimized > 0 || summary.failed > 0) { - console.log("[Site Settings API] Agent workflow preview optimization:", summary); - } await persistSystemSettingWithHistory( AGENT_WORKFLOW_UI_CONFIG_KEY, - optimized, + normalized, "agent-workflow" ); } @@ -2574,6 +2625,7 @@ export async function PUT(request: NextRequest) { } await persistHomeAnimationConfig(); + scopedSiteSettingsServerCache.clear(); return NextResponse.json({ ...updated, @@ -2973,14 +3025,9 @@ export async function PUT(request: NextRequest) { if (agentWorkflowUiConfig !== undefined) { const normalized = normalizeAgentWorkflowUiConfig(agentWorkflowUiConfig); - const { config: optimized, summary } = - await optimizeAgentWorkflowUiConfigPreviewImages(normalized); - if (summary.optimized > 0 || summary.failed > 0) { - console.log("[Site Settings API] Agent workflow preview optimization:", summary); - } await persistSystemSettingWithHistory( AGENT_WORKFLOW_UI_CONFIG_KEY, - optimized, + normalized, "agent-workflow" ); } @@ -3227,6 +3274,7 @@ export async function PUT(request: NextRequest) { }, }); } + scopedSiteSettingsServerCache.clear(); return NextResponse.json({ ...created, diff --git a/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx b/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx index 889e88a73..01d5e736c 100644 --- a/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx +++ b/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx @@ -133,7 +133,6 @@ import { normalizeAgentModeWorkspaceState, } from "@/lib/agents/mode-workspace-state"; import { normalizeAgentPresetModeConfig } from "@/lib/agents/preset-mode-config"; -import type { AgentPsdTemplateCategoryTagIndex } from "@/lib/agents/psd-template-category-tag-config"; import { normalizeAgentWorkflowBrandModelConfig } from "@/lib/agents/workflow-brand-model-config"; import { compactVideoCanvasMarkers, @@ -171,7 +170,6 @@ import { getEnabledAgentWorkflowStepTree, getAgentWorkflowBusinessModeSettings, getAgentWorkflowPageFeatureSettings, - normalizeAgentWorkflowUiConfig, sanitizeAgentWorkflowSelections, } from "@/lib/agents/workflow-ui-config"; import { cn, fetcher } from "@/lib/utils"; @@ -222,6 +220,7 @@ import type { AgentWorkflowSelectedBrandModel, AgentWorkflowShowcaseConfig, AgentWorkflowStepKey, + AgentWorkflowUiConfig, } from "@/types/agent"; import type { CanvasEdge, @@ -234,11 +233,20 @@ import type { } from "@/types/canvas"; const CANVAS_SITE_SETTINGS_REFRESH_INTERVAL_MS = 15000; +const CANVAS_SITE_SETTINGS_TIMEOUT_MS = 10000; const fetchCanvasSiteSettings = async (url: string) => { const separator = url.includes("?") ? "&" : "?"; + const controller = new AbortController(); + const timeoutId = setTimeout(() => { + controller.abort(); + }, CANVAS_SITE_SETTINGS_TIMEOUT_MS); + const response = await fetch(`${url}${separator}t=${Date.now()}`, { cache: "no-store", + signal: controller.signal, + }).finally(() => { + clearTimeout(timeoutId); }); if (!response.ok) { @@ -1869,37 +1877,18 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) { realtimeCanvasModeIconConfig?: Partial; canvasToolbarConfig?: CanvasToolbarConfig; agentWorkflowShowcaseConfig?: AgentWorkflowShowcaseConfig; - agentWorkflowBrandModelConfig?: unknown; - agentPsdTemplateCategoryTagIndex?: AgentPsdTemplateCategoryTagIndex; - agentWorkflowUiConfig?: { - pageFeatureSettings?: { - presetStepEnabled?: boolean; - materialStepEnabled?: boolean; - psdTemplateEnabled?: boolean; - batchProcessingEnabled?: boolean; - }; - steps?: Array<{ - key: AgentWorkflowStepKey; - title: string; - description?: string | null; - enabled?: boolean; - sortOrder?: number; - options?: Array<{ - key: string; - buttonName: string; - description?: string | null; - previewImageUrl?: string | null; - enabled?: boolean; - sortOrder?: number; - promptFragment?: string | null; - }>; - }>; - }; + agentWorkflowUiConfig?: AgentWorkflowUiConfig; }>(canvasSiteSettingsKey, fetchCanvasSiteSettings, { onSuccess: () => { setHasResolvedCanvasSiteSettings(true); }, - refreshInterval: CANVAS_SITE_SETTINGS_REFRESH_INTERVAL_MS, + onError: () => { + setHasResolvedCanvasSiteSettings(true); + }, + refreshInterval: requestedAgentMode + ? 0 + : CANVAS_SITE_SETTINGS_REFRESH_INTERVAL_MS, + errorRetryCount: 1, revalidateOnFocus: true, revalidateOnReconnect: true, }); @@ -1955,8 +1944,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) { !agentProjectId || loadedAgentProjectId === agentProjectId; const resolvedWorkflowUiConfig = useMemo( - () => - normalizeAgentWorkflowUiConfig(siteSettingsData?.agentWorkflowUiConfig), + () => siteSettingsData?.agentWorkflowUiConfig || defaultAgentWorkflowUiConfig, [siteSettingsData?.agentWorkflowUiConfig] ); const workflowPageFeatureSettings = useMemo( @@ -1989,6 +1977,19 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) { (isAgentWorkspaceEnabled || shouldUseStyleModeButtonForStylePro); const shouldShowStandaloneStyleProToolbarButton = isStyleProToolbarEnabled && !shouldUseStyleModeButtonForStylePro; + const normalizedBrandModelName = workflowInputs.brand.trim(); + const brandModelSettingsKey = + requestedAgentMode && normalizedBrandModelName + ? `/api/site-settings?scope=agent-brand-model&brand=${encodeURIComponent( + normalizedBrandModelName + )}` + : null; + const { data: brandModelSettingsData } = useSWR<{ + agentWorkflowBrandModelConfig?: unknown; + }>(brandModelSettingsKey, fetchCanvasSiteSettings, { + errorRetryCount: 1, + revalidateOnFocus: false, + }); const sanitizedWorkflowSelections = useMemo( () => sanitizeAgentWorkflowSelections({ @@ -2003,9 +2004,9 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) { const resolvedBrandModelConfig = useMemo( () => normalizeAgentWorkflowBrandModelConfig( - siteSettingsData?.agentWorkflowBrandModelConfig + brandModelSettingsData?.agentWorkflowBrandModelConfig ), - [siteSettingsData?.agentWorkflowBrandModelConfig] + [brandModelSettingsData?.agentWorkflowBrandModelConfig] ); useEffect(() => { diff --git a/components/agents/AgentWorkflowBrandModelPicker.tsx b/components/agents/AgentWorkflowBrandModelPicker.tsx index 4ab268e73..b41466178 100644 --- a/components/agents/AgentWorkflowBrandModelPicker.tsx +++ b/components/agents/AgentWorkflowBrandModelPicker.tsx @@ -24,12 +24,18 @@ export function AgentWorkflowBrandModelPicker({ onSelectBrandModel, theme = "light", }: AgentWorkflowBrandModelPickerProps) { - const { data } = useSWR<{ agentWorkflowBrandModelConfig?: AgentWorkflowBrandModelConfig }>( - "/api/site-settings?scope=agent-brand-model", - fetcher - ); - const normalizedBrand = brand.trim(); + const brandModelSettingsKey = normalizedBrand + ? `/api/site-settings?scope=agent-brand-model&brand=${encodeURIComponent( + normalizedBrand + )}` + : null; + const { data, error } = useSWR<{ + agentWorkflowBrandModelConfig?: AgentWorkflowBrandModelConfig; + }>(brandModelSettingsKey, fetcher, { + errorRetryCount: 1, + revalidateOnFocus: false, + }); const config = useMemo( () => normalizeAgentWorkflowBrandModelConfig(data?.agentWorkflowBrandModelConfig), [data?.agentWorkflowBrandModelConfig] @@ -43,7 +49,7 @@ export function AgentWorkflowBrandModelPicker({ ); const images = matchedEntry?.images || []; const isDark = theme === "dark"; - const isLoading = !data; + const isLoading = Boolean(normalizedBrand) && !data && !error; const renderPlaceholderGrid = (message: string, iconMode: "loading" | "empty" = "empty") => (
@@ -127,6 +133,8 @@ export function AgentWorkflowBrandModelPicker({ {isLoading ? ( renderPlaceholderGrid("正在加载品牌模特图库,请稍候...", "loading") + ) : error ? ( + renderPlaceholderGrid("品牌模特图库暂时加载失败,不影响继续生成;稍后可刷新重试。") ) : !normalizedBrand ? ( renderPlaceholderGrid("先回到第一步选择品牌,第二步才会按品牌展示可选模特图。") ) : images.length > 0 ? (