From 54c6b4a74e9bda2c21bb28aa962461ab2a0fef87 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 18 May 2026 18:49:12 +0800 Subject: [PATCH] fix: prevent agent workspace loading deadlock --- .../[id]/CanvasEditorLegacyPageClient.tsx | 170 ++++++++++++++++-- app/layout.tsx | 43 +++-- .../agents/AgentGenerationWorkspaceDarkV2.tsx | 2 +- 3 files changed, 186 insertions(+), 29 deletions(-) diff --git a/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx b/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx index b26146327..7484395dc 100644 --- a/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx +++ b/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx @@ -60,6 +60,7 @@ import dynamic from "next/dynamic"; import { useParams, useRouter, useSearchParams } from "next/navigation"; import { type ChangeEvent, + type ComponentType, startTransition, useCallback, useDeferredValue, @@ -71,6 +72,7 @@ import { import { toast } from "sonner"; import useSWR from "swr"; // 深色主题组件 V2 (新版布局) +import type { AgentGenerationWorkspaceDarkV2Props } from "@/components/agents/AgentGenerationWorkspaceDarkV2"; import { type AgentWorkflowInputs } from "@/components/agents/AgentWorkflowPanelNew"; import { CanvasToolbarIcon } from "@/components/canvas/CanvasToolbarIcon"; import { Badge } from "@/components/ui/badge"; @@ -320,21 +322,153 @@ const AgentCanvasTopNavDark = dynamic( } ); -const AgentGenerationWorkspaceDarkV2 = dynamic( - () => - import("@/components/agents/AgentGenerationWorkspaceDarkV2").then( - (module) => module.AgentGenerationWorkspaceDarkV2 - ), - { - loading: () => ( - - ), - ssr: false, +const AGENT_WORKSPACE_LOAD_TIMEOUT_MS = 15_000; + +let cachedAgentGenerationWorkspace: + | ComponentType + | null = null; +let agentGenerationWorkspaceLoadPromise: + | Promise> + | null = null; + +function loadAgentGenerationWorkspaceDarkV2() { + if (cachedAgentGenerationWorkspace) { + return Promise.resolve(cachedAgentGenerationWorkspace); } -); + + if (!agentGenerationWorkspaceLoadPromise) { + agentGenerationWorkspaceLoadPromise = import( + "@/components/agents/AgentGenerationWorkspaceDarkV2" + ) + .then((module) => { + cachedAgentGenerationWorkspace = module.AgentGenerationWorkspaceDarkV2; + return cachedAgentGenerationWorkspace; + }) + .catch((error) => { + agentGenerationWorkspaceLoadPromise = null; + throw error; + }); + } + + return agentGenerationWorkspaceLoadPromise; +} + +function resetAgentGenerationWorkspaceLoader() { + cachedAgentGenerationWorkspace = null; + agentGenerationWorkspaceLoadPromise = null; +} + +function AgentGenerationWorkspaceDarkV2( + props: AgentGenerationWorkspaceDarkV2Props +) { + const [WorkspaceComponent, setWorkspaceComponent] = useState< + ComponentType | null + >(() => cachedAgentGenerationWorkspace); + const [loadError, setLoadError] = useState(null); + const [showRecovery, setShowRecovery] = useState(false); + const [retryToken, setRetryToken] = useState(0); + + useEffect(() => { + if (WorkspaceComponent) return; + + let cancelled = false; + setLoadError(null); + setShowRecovery(false); + + const timeoutId = window.setTimeout(() => { + if (!cancelled) { + setShowRecovery(true); + } + }, AGENT_WORKSPACE_LOAD_TIMEOUT_MS); + + loadAgentGenerationWorkspaceDarkV2() + .then((component) => { + if (cancelled) return; + setWorkspaceComponent(() => component); + setLoadError(null); + setShowRecovery(false); + }) + .catch((error) => { + if (cancelled) return; + const normalizedError = + error instanceof Error ? error : new Error(String(error)); + setLoadError(normalizedError); + setShowRecovery(true); + }) + .finally(() => { + window.clearTimeout(timeoutId); + }); + + return () => { + cancelled = true; + window.clearTimeout(timeoutId); + }; + }, [WorkspaceComponent, retryToken]); + + if (WorkspaceComponent) { + return ; + } + + return ( +
+ + {showRecovery ? ( +
+

+ 工作台资源还没有完成加载,可能是浏览器缓存了旧版本资源或当前网络较慢。可以先重试加载,仍不恢复时刷新页面即可继续当前项目。 +

+ {loadError?.message ? ( +

+ {loadError.message} +

+ ) : null} +
+ + + {props.onOpenCanvasEditor ? ( + + ) : null} +
+
+ ) : null} +
+ ); +} const AgentProjectListSheet = dynamic( () => @@ -1909,6 +2043,14 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) { [siteSettingsData?.canvasToolbarConfig] ); const isAgentMode = isAgentWorkspaceEnabled && requestedAgentMode; + useEffect(() => { + if (!isAgentMode) return; + + void loadAgentGenerationWorkspaceDarkV2().catch((error) => { + console.warn("[Agent Workspace] preload failed:", error); + }); + }, [isAgentMode]); + const hasLoadedCurrentAgentProject = !requestedAgentMode || !agentProjectId || diff --git a/app/layout.tsx b/app/layout.tsx index 64b3b13ac..0c0ed7c4c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -91,32 +91,47 @@ const CHUNK_ERROR_RECOVERY_SCRIPT = `\ } } - var hasReloaded = safeGetSessionStorage('chunk-reload-attempted'); + var RECOVERY_KEY = 'chunk-reload-attempted-v2'; + var lastReloadAt = Number(safeGetSessionStorage(RECOVERY_KEY) || '0') || 0; function shouldAutoRecover(message) { if (!message) return false; + var normalized = String(message).toLowerCase(); return ( - message.includes('Failed to load chunk') || - message.includes('Loading chunk') || - message.includes('/_next/static/chunks/') || - message.includes('Failed to find Server Action') || - message.includes('Server Components render') || - message.includes('Application error: a client-side exception') + normalized.indexOf('chunkloaderror') !== -1 || + normalized.indexOf('failed to load chunk') !== -1 || + normalized.indexOf('loading chunk') !== -1 || + normalized.indexOf('failed to fetch dynamically imported module') !== -1 || + normalized.indexOf('error loading dynamically imported module') !== -1 || + normalized.indexOf('importing a module script failed') !== -1 || + normalized.indexOf('module script load failed') !== -1 || + normalized.indexOf('/_next/static/chunks/') !== -1 || + normalized.indexOf('failed to find server action') !== -1 || + normalized.indexOf('server components render') !== -1 || + normalized.indexOf('application error: a client-side exception') !== -1 ); } function triggerAutoRecover(reason) { - if (hasReloaded) return; + var now = Date.now(); + if (lastReloadAt && now - lastReloadAt < 30000) return; + lastReloadAt = now; console.warn('[RuntimeRecover] 检测到前端运行时异常,自动刷新页面...', reason || ''); - safeSetSessionStorage('chunk-reload-attempted', 'true'); + safeSetSessionStorage(RECOVERY_KEY, String(now)); setTimeout(function() { window.location.reload(); }, 800); } window.addEventListener('error', function(event) { - if (shouldAutoRecover(event && event.message)) { - triggerAutoRecover(event && event.message); + var message = ''; + if (event && event.message) message += event.message; + if (event && event.filename) message += ' ' + event.filename; + if (event && event.error && event.error.message) { + message += ' ' + event.error.message; + } + if (shouldAutoRecover(message)) { + triggerAutoRecover(message); } }, true); @@ -130,11 +145,11 @@ const CHUNK_ERROR_RECOVERY_SCRIPT = `\ } }); - // 页面加载成功后清除标记 + // 页面稳定一段时间后清除标记,避免旧资源异常无限刷新。 window.addEventListener('load', function() { setTimeout(function() { - safeRemoveSessionStorage('chunk-reload-attempted'); - }, 2000); + safeRemoveSessionStorage(RECOVERY_KEY); + }, 120000); }); })();`; diff --git a/components/agents/AgentGenerationWorkspaceDarkV2.tsx b/components/agents/AgentGenerationWorkspaceDarkV2.tsx index 494fa2c4d..07eb42989 100644 --- a/components/agents/AgentGenerationWorkspaceDarkV2.tsx +++ b/components/agents/AgentGenerationWorkspaceDarkV2.tsx @@ -124,7 +124,7 @@ import { normalizeAgentGenerationProjectPersistedState, } from "@/lib/agents/generation-project-state"; -interface AgentGenerationWorkspaceDarkV2Props { +export interface AgentGenerationWorkspaceDarkV2Props { chatId: string; projectId: string; selectedAgentPresetId?: string | null;