From e8927288938128c7b36584303fad33d45c1d11ed Mon Sep 17 00:00:00 2001 From: DigHuang <114602213+DigHuang@users.noreply.github.com> Date: Thu, 2 Apr 2026 20:07:08 +0800 Subject: [PATCH 1/2] refactor(chat): optimize sandbox status logic and decouple UI/Status hooks --- packages/global/core/chat/type.ts | 2 +- .../app/detail/Edit/ChatAgent/ChatTest.tsx | 12 +- .../app/detail/Edit/SimpleApp/ChatTest.tsx | 12 +- .../app/detail/Logs/DetailLogsModal.tsx | 12 +- .../WorkflowComponents/Flow/ChatTest.tsx | 12 +- .../chat/SandboxEditor/hook.tsx | 125 +++++++++++++----- .../app/src/pageComponents/chat/ToolMenu.tsx | 22 +-- 7 files changed, 132 insertions(+), 65 deletions(-) diff --git a/packages/global/core/chat/type.ts b/packages/global/core/chat/type.ts index 64dec5cf8238..6388c1489445 100644 --- a/packages/global/core/chat/type.ts +++ b/packages/global/core/chat/type.ts @@ -169,7 +169,7 @@ export type AIChatItemType = { citeCollectionIds?: string[]; /** - * @deprecated 不再存储在 chatItemSchema 里,分别存储到 chatItemResponseSchema + * 不再存储在 chatItemSchema 里,分别存储到 chatItemResponseSchema */ [DispatchNodeResponseKeyEnum.nodeResponse]?: ChatHistoryItemResType[]; }; diff --git a/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/ChatTest.tsx b/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/ChatTest.tsx index 88abb461fe43..77bcfeda6251 100644 --- a/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/ChatTest.tsx +++ b/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/ChatTest.tsx @@ -24,7 +24,7 @@ import type { HelperBotRefType } from '@/components/core/chat/HelperBot/context' import { HelperBotTypeEnum } from '@fastgpt/global/core/chat/helperBot/type'; import { loadGeneratedTools } from './utils'; import { systemSubInfo } from '@fastgpt/global/core/workflow/node/agent/constants'; -import { useSandboxEditor } from '@/pageComponents/chat/SandboxEditor/hook'; +import { useSandboxEditor, useSandboxStatus } from '@/pageComponents/chat/SandboxEditor/hook'; type Props = { appForm: AppFormEditFormType; @@ -50,8 +50,12 @@ const ChatTest = ({ appForm, setAppForm, setRenderEdit, form2WorkflowFn }: Props edges: appDetail.edges || [] }); - // Sandbox state - const { SandboxEditorModal, SandboxEntryIcon } = useSandboxEditor({ + // Sandbox: Status Hook 负责网络同步,UI Hook 负责弹窗渲染 + const { SandboxEntryIcon } = useSandboxStatus({ + appId: appDetail._id, + chatId + }); + const { SandboxEditorModal, onOpenSandboxModal } = useSandboxEditor({ appId: appDetail._id, chatId }); @@ -126,7 +130,7 @@ const ChatTest = ({ appForm, setAppForm, setRenderEdit, form2WorkflowFn }: Props )} - + { edges: appDetail.edges || [] }); - // Sandbox state - const { SandboxEditorModal, SandboxEntryIcon, setSandboxExists } = useSandboxEditor({ + // Sandbox: Status Hook 负责网络同步,UI Hook 负责弹窗渲染 + const { SandboxEntryIcon, setSandboxExists } = useSandboxStatus({ + appId: appDetail._id, + chatId + }); + const { SandboxEditorModal, onOpenSandboxModal } = useSandboxEditor({ appId: appDetail._id, chatId }); @@ -81,7 +85,7 @@ const ChatTest = ({ appForm, setRenderEdit, form2WorkflowFn }: Props) => { {!isVariableVisible && } - + import('@/components/core/chat/ChatContainer/PluginRunBox')); @@ -87,11 +87,9 @@ const DetailLogsModal = ({ const chatModels = chat?.app?.chatModels; const isPlugin = chat?.app.type === AppTypeEnum.workflowTool; - // Sandbox state - const { SandboxEditorModal, SandboxEntryIcon } = useSandboxEditor({ - appId, - chatId - }); + // Sandbox: Status Hook 负责网络同步,UI Hook 负责弹窗渲染 + const { SandboxEntryIcon } = useSandboxStatus({ appId, chatId }); + const { SandboxEditorModal, onOpenSandboxModal } = useSandboxEditor({ appId, chatId }); return ( <> @@ -176,7 +174,7 @@ const DetailLogsModal = ({ )} - + const isVariableVisible = useContextSelector(ChatItemContext, (v) => v.isVariableVisible); const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords); - // Sandbox state - const { SandboxEditorModal, SandboxEntryIcon } = useSandboxEditor({ + // Sandbox: Status Hook 负责网络同步,UI Hook 负责弹窗渲染 + const { SandboxEntryIcon } = useSandboxStatus({ + appId: appDetail._id, + chatId + }); + const { SandboxEditorModal, onOpenSandboxModal } = useSandboxEditor({ appId: appDetail._id, chatId }); @@ -143,7 +147,7 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose, chatId }: Props) => {!isVariableVisible && } - + void; }) => { - const { t } = useTranslation(); - // Sandbox state const [sandboxModalOpen, setSandboxModalOpen] = useState(false); - const [sandboxExists, setSandboxExists] = useState(false); - - // 检查沙盒是否存在 - const checkSandboxStatus = useCallback(async () => { - try { - const result = await checkSandboxExist({ appId, chatId, outLinkAuthData }); - setSandboxExists(result.exists); - } catch (error) { - console.error('Failed to check sandbox status:', error); - } - }, [appId, chatId, outLinkAuthData]); - - // 组件挂载时检查 - useInterval(checkSandboxStatus, 10000, { - immediate: true - }); const onOpenSandboxModal = useCallback(() => { setSandboxModalOpen(true); @@ -44,11 +37,10 @@ export const useSandboxEditor = ({ const onCloseSandboxModal = useCallback(() => { setSandboxModalOpen(false); - // 关闭后重新检查状态 - checkSandboxStatus(); - }, [checkSandboxStatus]); + onCloseCallback?.(); + }, [onCloseCallback]); - const Dom = useCallback(() => { + const SandboxEditorModalDom = useCallback(() => { return sandboxModalOpen ? ( { + const { t } = useTranslation(); + const [prevChatId, setPrevChatId] = useState(undefined); + const [apiSandboxExists, setApiSandboxExists] = useState(false); + + if (prevChatId !== chatId) { + setPrevChatId(chatId); + setApiSandboxExists(false); + } + + const chatRecords = useContextSelector(ChatRecordContext, (v) => { + return v.chatRecords; + }); + const isChatRecordsLoaded = useContextSelector(ChatRecordContext, (v) => v.isChatRecordsLoaded); + + const hasSandboxInHistory = useMemo(() => { + if (!isChatRecordsLoaded) return false; + return chatRecords.some((record) => { + const enriched = addStatisticalDataToHistoryItem(record); + return enriched.useAgentSandbox === true; + }); + }, [chatRecords, isChatRecordsLoaded]); + + useEffect(() => { + if (!chatId) return; + let cancelled = false; + checkSandboxExist({ appId, chatId, outLinkAuthData }) + .then((result) => { + if (!cancelled) setApiSandboxExists(result.exists); + }) + .catch((error) => { + console.error('Failed to check sandbox status:', error); + }); + return () => { + cancelled = true; + }; + }, [appId, chatId, outLinkAuthData]); + + const sandboxExists = hasSandboxInHistory || apiSandboxExists; + const SandboxEntryIcon = useCallback( - (props: Omit) => { - // 只有沙盒存在时才显示图标 + ({ + onOpen, + ...props + }: Omit & { onOpen: () => void }) => { if (!sandboxExists) return null; return ( @@ -70,23 +129,19 @@ export const useSandboxEditor = ({ variant={'whiteBase'} size={'smSquare'} icon={} - onClick={onOpenSandboxModal} + onClick={onOpen} {...props} aria-label="Sandbox Entry" /> ); }, - [sandboxExists, t, onOpenSandboxModal] + [sandboxExists, t] ); return { sandboxExists, - setSandboxExists, - checkSandboxStatus, - SandboxEntryIcon, - SandboxEditorModal: Dom, - onOpenSandboxModal, - onCloseSandboxModal + setSandboxExists: setApiSandboxExists, + SandboxEntryIcon }; }; diff --git a/projects/app/src/pageComponents/chat/ToolMenu.tsx b/projects/app/src/pageComponents/chat/ToolMenu.tsx index f5885aad68b8..81ed97f9bb11 100644 --- a/projects/app/src/pageComponents/chat/ToolMenu.tsx +++ b/projects/app/src/pageComponents/chat/ToolMenu.tsx @@ -9,6 +9,7 @@ import { useContextSelector } from 'use-context-selector'; import { ChatContext } from '@/web/core/chat/context/chatContext'; import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; import { useSandboxEditor } from './SandboxEditor/hook'; +import { useSandboxStatus } from './SandboxEditor/hook'; import { useChatStore } from '@/web/core/chat/context/useChatStore'; import { useSystem } from '@fastgpt/web/hooks/useSystem'; @@ -25,16 +26,17 @@ const ToolMenu = ({ const onChangeChatId = useContextSelector(ChatContext, (v) => v.onChangeChatId); const chatData = useContextSelector(ChatItemContext, (v) => v.chatBoxData); - const { chatId, appId, setChatId, outLinkAuthData } = useChatStore(); + const { chatId, outLinkAuthData } = useChatStore(); - // Sandbox state - const { - SandboxEditorModal, - SandboxEntryIcon, - setSandboxExists, - sandboxExists, - onOpenSandboxModal - } = useSandboxEditor({ + // Status Hook: 顶层单例,负责网络同步与入口图标显示 + const { sandboxExists, setSandboxExists, SandboxEntryIcon } = useSandboxStatus({ + appId: chatData.appId, + chatId, + outLinkAuthData + }); + + // UI Hook: 负责弹窗渲染 + const { SandboxEditorModal, onOpenSandboxModal } = useSandboxEditor({ appId: chatData.appId, chatId, outLinkAuthData @@ -42,7 +44,7 @@ const ToolMenu = ({ return ( <> - {isPc && } + {isPc && } From 4c1ac36d6760a591a4596846e007f0fda9a24b35 Mon Sep 17 00:00:00 2001 From: DigHuang <114602213+DigHuang@users.noreply.github.com> Date: Fri, 3 Apr 2026 10:47:25 +0800 Subject: [PATCH 2/2] fix: useRef, rename onClose to afterClose --- .../chat/SandboxEditor/hook.tsx | 25 ++++++++----------- .../app/src/pageComponents/chat/ToolMenu.tsx | 3 +-- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/projects/app/src/pageComponents/chat/SandboxEditor/hook.tsx b/projects/app/src/pageComponents/chat/SandboxEditor/hook.tsx index 69950fd62a69..b8471679d008 100644 --- a/projects/app/src/pageComponents/chat/SandboxEditor/hook.tsx +++ b/projects/app/src/pageComponents/chat/SandboxEditor/hook.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import SandboxEditorModal from '@/pageComponents/chat/SandboxEditor/modal'; import type { IconButtonProps } from '@chakra-ui/react'; import { IconButton } from '@chakra-ui/react'; @@ -15,19 +15,17 @@ import { addStatisticalDataToHistoryItem } from '@/global/core/chat/utils'; * useSandboxEditor —— UI Hook * * 职责:仅负责渲染 SandboxEditorModal 弹窗及其开关逻辑。 - * 性能:移除所有 fetch 逻辑与定时器。 - * 组件:供 ResponseTags 等细粒度组件调用,不产生额外网络开销。 */ export const useSandboxEditor = ({ appId, chatId, outLinkAuthData, - onClose: onCloseCallback + afterClose }: { appId: string; chatId: string; outLinkAuthData?: OutLinkChatAuthProps; - onClose?: () => void; + afterClose?: () => void; }) => { const [sandboxModalOpen, setSandboxModalOpen] = useState(false); @@ -37,8 +35,8 @@ export const useSandboxEditor = ({ const onCloseSandboxModal = useCallback(() => { setSandboxModalOpen(false); - onCloseCallback?.(); - }, [onCloseCallback]); + afterClose?.(); + }, [afterClose]); const SandboxEditorModalDom = useCallback(() => { return sandboxModalOpen ? ( @@ -64,9 +62,8 @@ export const useSandboxEditor = ({ * 职责:负责 checkSandboxExist 的网络同步及 SandboxEntryIcon 的显示控制。 * 同步模式: * 1. 历史记录(ChatRecordContext):useMemo 派生,无副作用。 - * 2. chatId 切换:渲染期 `prevChatId !== chatId` 重置 API 状态(React 推荐的 during-render 更新模式)。 - * 3. 网络请求:单一 useEffect,仅在 chatId 变化时触发 1 次。 - * 组件:供 Header / ToolMenu 等单一顶层入口组件调用。 + * 2. chatId 切换:渲染周期利用 useRef 确认 ID 变化并同步重置状态,防止 UI 闪烁。 + * 3. 网络请求:单一 useEffect,在参数变化时触发 1 次。 */ export const useSandboxStatus = ({ appId, @@ -78,11 +75,11 @@ export const useSandboxStatus = ({ outLinkAuthData?: OutLinkChatAuthProps; }) => { const { t } = useTranslation(); - const [prevChatId, setPrevChatId] = useState(undefined); const [apiSandboxExists, setApiSandboxExists] = useState(false); + const lastChatIdRef = useRef(chatId); - if (prevChatId !== chatId) { - setPrevChatId(chatId); + if (lastChatIdRef.current !== chatId) { + lastChatIdRef.current = chatId; setApiSandboxExists(false); } @@ -112,7 +109,7 @@ export const useSandboxStatus = ({ return () => { cancelled = true; }; - }, [appId, chatId, outLinkAuthData]); + }, [appId, chatId]); const sandboxExists = hasSandboxInHistory || apiSandboxExists; diff --git a/projects/app/src/pageComponents/chat/ToolMenu.tsx b/projects/app/src/pageComponents/chat/ToolMenu.tsx index 81ed97f9bb11..81ab9184891b 100644 --- a/projects/app/src/pageComponents/chat/ToolMenu.tsx +++ b/projects/app/src/pageComponents/chat/ToolMenu.tsx @@ -8,8 +8,7 @@ import MyMenu from '@fastgpt/web/components/common/MyMenu'; import { useContextSelector } from 'use-context-selector'; import { ChatContext } from '@/web/core/chat/context/chatContext'; import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; -import { useSandboxEditor } from './SandboxEditor/hook'; -import { useSandboxStatus } from './SandboxEditor/hook'; +import { useSandboxEditor, useSandboxStatus } from './SandboxEditor/hook'; import { useChatStore } from '@/web/core/chat/context/useChatStore'; import { useSystem } from '@fastgpt/web/hooks/useSystem';