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';