fix: improve agent workflow settings loading
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-05-18 15:32:05 +08:00
parent 03cdefca34
commit 040a1f514f
4 changed files with 192 additions and 104 deletions

View File

@ -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<AgentWorkflowSettingsPayload>(
"/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);
}

View File

@ -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<string>([
const SCOPED_SITE_SETTINGS_SERVER_CACHE_TTL_MS = 30_000;
const scopedSiteSettingsServerCache = new Map<
PublicSiteSettingsScope,
string,
{ data: Record<string, unknown>; expiresAt: number }
>();
function createScopedSiteSettingsResponse(
scope: PublicSiteSettingsScope,
data: Record<string, unknown>,
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<string, unknown> | 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,

View File

@ -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<RealtimeCanvasModeIconConfig>;
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(() => {

View File

@ -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") => (
<div className="space-y-3">
@ -127,6 +133,8 @@ export function AgentWorkflowBrandModelPicker({
{isLoading ? (
renderPlaceholderGrid("正在加载品牌模特图库,请稍候...", "loading")
) : error ? (
renderPlaceholderGrid("品牌模特图库暂时加载失败,不影响继续生成;稍后可刷新重试。")
) : !normalizedBrand ? (
renderPlaceholderGrid("先回到第一步选择品牌,第二步才会按品牌展示可选模特图。")
) : images.length > 0 ? (