From 86c27e85efbd8134d903f3c50d0eb53987d2289a Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Thu, 15 Aug 2024 13:12:39 +0800 Subject: [PATCH] 4.8.10 perf (#2378) * perf: helpline code * fix: prompt call stream=false response prefix * fix: app chat log auth * perf: new chat i18n * fix: milvus dataset cannot export data * perf: doc intro --- .../zh-cn/docs/development/upgrading/4810.md | 8 +- .../dispatch/agent/runTool/promptCall.ts | 3 +- projects/app/src/global/core/chat/api.d.ts | 2 +- .../app/src/global/core/chat/constants.ts | 4 +- .../app/src/pages/api/core/chat/getResData.ts | 30 +- projects/app/src/pages/api/core/chat/init.ts | 4 +- .../src/pages/api/core/chat/outLink/init.ts | 140 ++++--- .../app/src/pages/api/core/chat/team/init.ts | 141 ++++--- .../src/pages/api/core/dataset/exportAll.ts | 2 +- .../Flow/components/HelperLines.tsx | 2 +- .../Flow/hooks/useUtils.tsx | 237 +----------- .../Flow/hooks/useWorkflow.tsx | 351 +++++++++++++++--- .../WorkflowComponents/Flow/index.tsx | 19 +- .../components/WorkflowComponents/context.tsx | 26 -- .../src/pages/chat/components/ChatHeader.tsx | 7 +- .../service/support/permission/auth/chat.ts | 4 +- 16 files changed, 500 insertions(+), 480 deletions(-) diff --git a/docSite/content/zh-cn/docs/development/upgrading/4810.md b/docSite/content/zh-cn/docs/development/upgrading/4810.md index 5b59a889c6c0..f676b5924908 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/4810.md +++ b/docSite/content/zh-cn/docs/development/upgrading/4810.md @@ -20,4 +20,10 @@ weight: 816 ## V4.8.10 更新说明 1. 新增 - 模板市场 -2. +2. 新增 - 工作流节点拖动自动对齐吸附 +3. 新增 - 用户选择节点(Debug 模式暂未支持) +4. 商业版新增 - 飞书机器人接入 +5. 商业版新增 - 公众号接入接入 +6. 修复 - Prompt 模式调用工具,stream=false 模式下,会携带 0: 开头标记。 +7. 修复 - 对话日志鉴权问题:仅为 APP 管理员的用户,无法查看对话日志详情。 +8. 修复 - 选择 Milvus 部署时,无法导出知识库。 diff --git a/packages/service/core/workflow/dispatch/agent/runTool/promptCall.ts b/packages/service/core/workflow/dispatch/agent/runTool/promptCall.ts index 8e6d11fb5783..eba6571f4797 100644 --- a/packages/service/core/workflow/dispatch/agent/runTool/promptCall.ts +++ b/packages/service/core/workflow/dispatch/agent/runTool/promptCall.ts @@ -411,6 +411,7 @@ const parseAnswer = ( str = str.trim(); // 首先,使用正则表达式提取TOOL_ID和TOOL_ARGUMENTS const prefixReg = /^1(:|:)/; + const answerPrefixReg = /^0(:|:)/; if (prefixReg.test(str)) { const toolString = sliceJsonStr(str); @@ -432,7 +433,7 @@ const parseAnswer = ( } } else { return { - answer: str + answer: str.replace(answerPrefixReg, '') }; } }; diff --git a/projects/app/src/global/core/chat/api.d.ts b/projects/app/src/global/core/chat/api.d.ts index e835cae1f890..df10e550111c 100644 --- a/projects/app/src/global/core/chat/api.d.ts +++ b/projects/app/src/global/core/chat/api.d.ts @@ -30,7 +30,7 @@ export type InitChatResponse = { chatId?: string; appId: string; userAvatar?: string; - title: string; + title?: string; variables: Record; history: ChatItemType[]; app: { diff --git a/projects/app/src/global/core/chat/constants.ts b/projects/app/src/global/core/chat/constants.ts index c9f4ba6b88c0..d98efc0e0f90 100644 --- a/projects/app/src/global/core/chat/constants.ts +++ b/projects/app/src/global/core/chat/constants.ts @@ -1,6 +1,6 @@ import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { InitChatResponse } from './api'; -import { i18nT } from '@fastgpt/web/i18n/utils'; + export const defaultChatData: InitChatResponse = { chatId: '', appId: '', @@ -12,7 +12,7 @@ export const defaultChatData: InitChatResponse = { type: AppTypeEnum.simple, pluginInputs: [] }, - title: i18nT('chat:new_chat'), + title: '', variables: {}, history: [] }; diff --git a/projects/app/src/pages/api/core/chat/getResData.ts b/projects/app/src/pages/api/core/chat/getResData.ts index fc38a5bca394..fdcea14b0b2f 100644 --- a/projects/app/src/pages/api/core/chat/getResData.ts +++ b/projects/app/src/pages/api/core/chat/getResData.ts @@ -1,11 +1,15 @@ import { authChatCrud } from '@/service/support/permission/auth/chat'; -import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; +import { + ManagePermissionVal, + ReadPermissionVal +} from '@fastgpt/global/support/permission/constant'; import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; import { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type'; import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat'; +import { authApp } from '@fastgpt/service/support/permission/app/auth'; export type getResDataQuery = OutLinkChatAuthProps & { chatId?: string; @@ -25,12 +29,24 @@ async function handler( if (!appId || !chatId || !dataId) { return {}; } - await authChatCrud({ - req, - authToken: true, - ...req.query, - per: ReadPermissionVal - }); + + // 1. Un login api: share chat, team chat + // 2. Login api: account chat, chat log + try { + await authChatCrud({ + req, + authToken: true, + ...req.query, + per: ReadPermissionVal + }); + } catch (error) { + await authApp({ + req, + authToken: true, + appId, + per: ManagePermissionVal + }); + } const chatData = await MongoChatItem.findOne({ appId, diff --git a/projects/app/src/pages/api/core/chat/init.ts b/projects/app/src/pages/api/core/chat/init.ts index 624a8cb9252a..ca990df555e5 100644 --- a/projects/app/src/pages/api/core/chat/init.ts +++ b/projects/app/src/pages/api/core/chat/init.ts @@ -14,7 +14,7 @@ import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { transformPreviewHistories } from '@/global/core/chat/utils'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; -import { i18nT } from '@fastgpt/web/i18n/utils'; + async function handler( req: NextApiRequest, res: NextApiResponse @@ -62,7 +62,7 @@ async function handler( return { chatId, appId, - title: chat?.title || i18nT('chat:new_chat'), + title: chat?.title, userAvatar: undefined, variables: chat?.variables || {}, history: app.type === AppTypeEnum.plugin ? histories : transformPreviewHistories(histories), diff --git a/projects/app/src/pages/api/core/chat/outLink/init.ts b/projects/app/src/pages/api/core/chat/outLink/init.ts index 71c451286bf0..74795b35ca38 100644 --- a/projects/app/src/pages/api/core/chat/outLink/init.ts +++ b/projects/app/src/pages/api/core/chat/outLink/init.ts @@ -18,90 +18,84 @@ import { getAppLatestVersion } from '@fastgpt/service/core/app/controller'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { transformPreviewHistories } from '@/global/core/chat/utils'; -import { i18nT } from '@fastgpt/web/i18n/utils'; -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - await connectToDatabase(); +import { NextAPI } from '@/service/middleware/entry'; - let { chatId, shareId, outLinkUid } = req.query as InitOutLinkChatProps; +async function handler(req: NextApiRequest, res: NextApiResponse) { + let { chatId, shareId, outLinkUid } = req.query as InitOutLinkChatProps; - // auth link permission - const { shareChat, uid, appId } = await authOutLink({ shareId, outLinkUid }); + // auth link permission + const { shareChat, uid, appId } = await authOutLink({ shareId, outLinkUid }); - // auth app permission - const [tmb, chat, app] = await Promise.all([ - MongoTeamMember.findById(shareChat.tmbId, '_id userId').populate('userId', 'avatar').lean(), - MongoChat.findOne({ appId, chatId, shareId }).lean(), - MongoApp.findById(appId).lean() - ]); + // auth app permission + const [tmb, chat, app] = await Promise.all([ + MongoTeamMember.findById(shareChat.tmbId, '_id userId').populate('userId', 'avatar').lean(), + MongoChat.findOne({ appId, chatId, shareId }).lean(), + MongoApp.findById(appId).lean() + ]); - if (!app) { - throw new Error(AppErrEnum.unExist); - } - - // auth chat permission - if (chat && chat.outLinkUid !== uid) { - throw new Error(ChatErrEnum.unAuthChat); - } + if (!app) { + throw new Error(AppErrEnum.unExist); + } - const [{ histories }, { nodes }] = await Promise.all([ - getChatItems({ - appId: app._id, - chatId, - limit: 30, - field: `dataId obj value userGoodFeedback userBadFeedback ${ - shareChat.responseDetail || app.type === AppTypeEnum.plugin - ? `adminFeedback ${DispatchNodeResponseKeyEnum.nodeResponse}` - : '' - } ` - }), - getAppLatestVersion(app._id, app) - ]); + // auth chat permission + if (chat && chat.outLinkUid !== uid) { + throw new Error(ChatErrEnum.unAuthChat); + } - // pick share response field - app.type !== AppTypeEnum.plugin && - histories.forEach((item) => { - if (item.obj === ChatRoleEnum.AI) { - item.responseData = filterPublicNodeResponseData({ flowResponses: item.responseData }); - } - }); + const [{ histories }, { nodes }] = await Promise.all([ + getChatItems({ + appId: app._id, + chatId, + limit: 30, + field: `dataId obj value userGoodFeedback userBadFeedback ${ + shareChat.responseDetail || app.type === AppTypeEnum.plugin + ? `adminFeedback ${DispatchNodeResponseKeyEnum.nodeResponse}` + : '' + } ` + }), + getAppLatestVersion(app._id, app) + ]); - jsonRes(res, { - data: { - chatId, - appId: app._id, - title: chat?.title || i18nT('chat:new_chat'), - //@ts-ignore - userAvatar: tmb?.userId?.avatar, - variables: chat?.variables || {}, - history: app.type === AppTypeEnum.plugin ? histories : transformPreviewHistories(histories), - app: { - chatConfig: getAppChatConfig({ - chatConfig: app.chatConfig, - systemConfigNode: getGuideModule(nodes), - storeVariables: chat?.variableList, - storeWelcomeText: chat?.welcomeText, - isPublicFetch: false - }), - chatModels: getChatModelNameListByModules(nodes), - name: app.name, - avatar: app.avatar, - intro: app.intro, - type: app.type, - pluginInputs: - app?.modules?.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput) - ?.inputs ?? [] - } + // pick share response field + app.type !== AppTypeEnum.plugin && + histories.forEach((item) => { + if (item.obj === ChatRoleEnum.AI) { + item.responseData = filterPublicNodeResponseData({ flowResponses: item.responseData }); } }); - } catch (err) { - jsonRes(res, { - code: 500, - error: err - }); - } + + jsonRes(res, { + data: { + chatId, + appId: app._id, + title: chat?.title, + //@ts-ignore + userAvatar: tmb?.userId?.avatar, + variables: chat?.variables || {}, + history: app.type === AppTypeEnum.plugin ? histories : transformPreviewHistories(histories), + app: { + chatConfig: getAppChatConfig({ + chatConfig: app.chatConfig, + systemConfigNode: getGuideModule(nodes), + storeVariables: chat?.variableList, + storeWelcomeText: chat?.welcomeText, + isPublicFetch: false + }), + chatModels: getChatModelNameListByModules(nodes), + name: app.name, + avatar: app.avatar, + intro: app.intro, + type: app.type, + pluginInputs: + app?.modules?.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput) + ?.inputs ?? [] + } + } + }); } +export default NextAPI(handler); + export const config = { api: { responseLimit: '10mb' diff --git a/projects/app/src/pages/api/core/chat/team/init.ts b/projects/app/src/pages/api/core/chat/team/init.ts index 44f5cef4c792..1b9cb95e0a54 100644 --- a/projects/app/src/pages/api/core/chat/team/init.ts +++ b/projects/app/src/pages/api/core/chat/team/init.ts @@ -18,92 +18,85 @@ import { getAppLatestVersion } from '@fastgpt/service/core/app/controller'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { transformPreviewHistories } from '@/global/core/chat/utils'; -import { i18nT } from '@fastgpt/web/i18n/utils'; +import { NextAPI } from '@/service/middleware/entry'; -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - await connectToDatabase(); +async function handler(req: NextApiRequest, res: NextApiResponse) { + let { teamId, appId, chatId, teamToken } = req.query as InitTeamChatProps; - let { teamId, appId, chatId, teamToken } = req.query as InitTeamChatProps; - - if (!teamId || !appId || !teamToken) { - throw new Error('teamId, appId, teamToken are required'); - } - - const { uid } = await authTeamSpaceToken({ - teamId, - teamToken - }); + if (!teamId || !appId || !teamToken) { + throw new Error('teamId, appId, teamToken are required'); + } - const [team, chat, app] = await Promise.all([ - MongoTeam.findById(teamId, 'name avatar').lean(), - MongoChat.findOne({ teamId, appId, chatId }).lean(), - MongoApp.findById(appId).lean() - ]); + const { uid } = await authTeamSpaceToken({ + teamId, + teamToken + }); - if (!app) { - throw new Error(AppErrEnum.unExist); - } + const [team, chat, app] = await Promise.all([ + MongoTeam.findById(teamId, 'name avatar').lean(), + MongoChat.findOne({ teamId, appId, chatId }).lean(), + MongoApp.findById(appId).lean() + ]); - // auth chat permission - if (chat && chat.outLinkUid !== uid) { - throw new Error(ChatErrEnum.unAuthChat); - } + if (!app) { + throw new Error(AppErrEnum.unExist); + } - // get app and history - const [{ histories }, { nodes }] = await Promise.all([ - getChatItems({ - appId, - chatId, - limit: 30, - field: `dataId obj value userGoodFeedback userBadFeedback adminFeedback ${DispatchNodeResponseKeyEnum.nodeResponse}` - }), - getAppLatestVersion(app._id, app) - ]); + // auth chat permission + if (chat && chat.outLinkUid !== uid) { + throw new Error(ChatErrEnum.unAuthChat); + } - // pick share response field - app.type !== AppTypeEnum.plugin && - histories.forEach((item) => { - if (item.obj === ChatRoleEnum.AI) { - item.responseData = filterPublicNodeResponseData({ flowResponses: item.responseData }); - } - }); + // get app and history + const [{ histories }, { nodes }] = await Promise.all([ + getChatItems({ + appId, + chatId, + limit: 30, + field: `dataId obj value userGoodFeedback userBadFeedback adminFeedback ${DispatchNodeResponseKeyEnum.nodeResponse}` + }), + getAppLatestVersion(app._id, app) + ]); - jsonRes(res, { - data: { - chatId, - appId, - title: chat?.title || i18nT('chat:new_chat'), - userAvatar: team?.avatar, - variables: chat?.variables || {}, - history: app.type === AppTypeEnum.plugin ? histories : transformPreviewHistories(histories), - app: { - chatConfig: getAppChatConfig({ - chatConfig: app.chatConfig, - systemConfigNode: getGuideModule(nodes), - storeVariables: chat?.variableList, - storeWelcomeText: chat?.welcomeText, - isPublicFetch: false - }), - chatModels: getChatModelNameListByModules(nodes), - name: app.name, - avatar: app.avatar, - intro: app.intro, - type: app.type, - pluginInputs: - app?.modules?.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput) - ?.inputs ?? [] - } + // pick share response field + app.type !== AppTypeEnum.plugin && + histories.forEach((item) => { + if (item.obj === ChatRoleEnum.AI) { + item.responseData = filterPublicNodeResponseData({ flowResponses: item.responseData }); } }); - } catch (err) { - jsonRes(res, { - code: 500, - error: err - }); - } + + jsonRes(res, { + data: { + chatId, + appId, + title: chat?.title, + userAvatar: team?.avatar, + variables: chat?.variables || {}, + history: app.type === AppTypeEnum.plugin ? histories : transformPreviewHistories(histories), + app: { + chatConfig: getAppChatConfig({ + chatConfig: app.chatConfig, + systemConfigNode: getGuideModule(nodes), + storeVariables: chat?.variableList, + storeWelcomeText: chat?.welcomeText, + isPublicFetch: false + }), + chatModels: getChatModelNameListByModules(nodes), + name: app.name, + avatar: app.avatar, + intro: app.intro, + type: app.type, + pluginInputs: + app?.modules?.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput) + ?.inputs ?? [] + } + } + }); } +export default NextAPI(handler); + export const config = { api: { responseLimit: '10mb' diff --git a/projects/app/src/pages/api/core/dataset/exportAll.ts b/projects/app/src/pages/api/core/dataset/exportAll.ts index 0c52ea6d7ffe..26e5de5edd18 100644 --- a/projects/app/src/pages/api/core/dataset/exportAll.ts +++ b/projects/app/src/pages/api/core/dataset/exportAll.ts @@ -18,7 +18,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { datasetId: string; }; - if (!datasetId || !global.pgClient) { + if (!datasetId) { return Promise.reject(CommonErrEnum.missingParams); } diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/HelperLines.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/HelperLines.tsx index bd1bce3d3ded..a007e8eeec36 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/HelperLines.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/HelperLines.tsx @@ -89,7 +89,7 @@ function HelperLinesRenderer({ horizontal, vertical }: HelperLinesProps) { drawCross(node.right * transform[2] + transform[0], y, 5 * zoom); }); } - }, [width, height, transform, horizontal, vertical]); + }, [width, height, transform, horizontal, vertical, zoom]); return ; } diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useUtils.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useUtils.tsx index dd843eb2dab2..f0d973c6f10c 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useUtils.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useUtils.tsx @@ -3,14 +3,6 @@ import { WorkflowContext } from '../../context'; import { useTranslation } from 'next-i18next'; import { useCallback } from 'react'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; -import { Node, NodePositionChange, XYPosition } from 'reactflow'; -import { THelperLine } from '@fastgpt/global/core/workflow/type'; - -type GetHelperLinesResult = { - horizontal?: THelperLine; - vertical?: THelperLine; - snapPosition: Partial; -}; export const useWorkflowUtils = () => { const { t } = useTranslation(); @@ -40,235 +32,8 @@ export const useWorkflowUtils = () => { [nodeList] ); - const getHelperLines = ( - change: NodePositionChange, - nodes: Node[], - distance = 8 - ): GetHelperLinesResult => { - const nodeA = nodes.find((node) => node.id === change.id); - - if (!nodeA || !change.position) { - return { - horizontal: undefined, - vertical: undefined, - snapPosition: { x: undefined, y: undefined } - }; - } - - const nodeABounds = { - left: change.position.x, - right: change.position.x + (nodeA.width ?? 0), - top: change.position.y, - bottom: change.position.y + (nodeA.height ?? 0), - width: nodeA.width ?? 0, - height: nodeA.height ?? 0, - centerX: change.position.x + (nodeA.width ?? 0) / 2, - centerY: change.position.y + (nodeA.height ?? 0) / 2 - }; - - let horizontalDistance = distance; - let verticalDistance = distance; - - return nodes - .filter((node) => node.id !== nodeA.id) - .reduce( - (result, nodeB) => { - if (!result.vertical) { - result.vertical = { - position: nodeABounds.centerX, - nodes: [] - }; - } - - if (!result.horizontal) { - result.horizontal = { - position: nodeABounds.centerY, - nodes: [] - }; - } - - const nodeBBounds = { - left: nodeB.position.x, - right: nodeB.position.x + (nodeB.width ?? 0), - top: nodeB.position.y, - bottom: nodeB.position.y + (nodeB.height ?? 0), - width: nodeB.width ?? 0, - height: nodeB.height ?? 0, - centerX: nodeB.position.x + (nodeB.width ?? 0) / 2, - centerY: nodeB.position.y + (nodeB.height ?? 0) / 2 - }; - - const distanceLeftLeft = Math.abs(nodeABounds.left - nodeBBounds.left); - const distanceRightRight = Math.abs(nodeABounds.right - nodeBBounds.right); - const distanceLeftRight = Math.abs(nodeABounds.left - nodeBBounds.right); - const distanceRightLeft = Math.abs(nodeABounds.right - nodeBBounds.left); - const distanceTopTop = Math.abs(nodeABounds.top - nodeBBounds.top); - const distanceBottomTop = Math.abs(nodeABounds.bottom - nodeBBounds.top); - const distanceBottomBottom = Math.abs(nodeABounds.bottom - nodeBBounds.bottom); - const distanceTopBottom = Math.abs(nodeABounds.top - nodeBBounds.bottom); - const distanceCenterXCenterX = Math.abs(nodeABounds.centerX - nodeBBounds.centerX); - const distanceCenterYCenterY = Math.abs(nodeABounds.centerY - nodeBBounds.centerY); - - // |‾‾‾‾‾‾‾‾‾‾‾| - // | A | - // |___________| - // | - // | - // |‾‾‾‾‾‾‾‾‾‾‾| - // | B | - // |___________| - if (distanceLeftLeft < verticalDistance) { - result.snapPosition.x = nodeBBounds.left; - result.vertical.position = nodeBBounds.left; - result.vertical.nodes = [nodeABounds, nodeBBounds]; - verticalDistance = distanceLeftLeft; - } else if (distanceLeftLeft === verticalDistance) { - result.vertical.nodes.push(nodeBBounds); - } - - // |‾‾‾‾‾‾‾‾‾‾‾| - // | A | - // |___________| - // | - // | - // |‾‾‾‾‾‾‾‾‾‾‾| - // | B | - // |___________| - if (distanceRightRight < verticalDistance) { - result.snapPosition.x = nodeBBounds.right - nodeABounds.width; - result.vertical.position = nodeBBounds.right; - result.vertical.nodes = [nodeABounds, nodeBBounds]; - verticalDistance = distanceRightRight; - } else if (distanceRightRight === verticalDistance) { - result.vertical.nodes.push(nodeBBounds); - } - - // |‾‾‾‾‾‾‾‾‾‾‾| - // | A | - // |___________| - // | - // | - // |‾‾‾‾‾‾‾‾‾‾‾| - // | B | - // |___________| - if (distanceLeftRight < verticalDistance) { - result.snapPosition.x = nodeBBounds.right; - result.vertical.position = nodeBBounds.right; - result.vertical.nodes = [nodeABounds, nodeBBounds]; - verticalDistance = distanceLeftRight; - } else if (distanceLeftRight === verticalDistance) { - result.vertical.nodes.push(nodeBBounds); - } - - // |‾‾‾‾‾‾‾‾‾‾‾| - // | A | - // |___________| - // | - // | - // |‾‾‾‾‾‾‾‾‾‾‾| - // | B | - // |___________| - if (distanceRightLeft < verticalDistance) { - result.snapPosition.x = nodeBBounds.left - nodeABounds.width; - result.vertical.position = nodeBBounds.left; - result.vertical.nodes = [nodeABounds, nodeBBounds]; - verticalDistance = distanceRightLeft; - } else if (distanceRightLeft === verticalDistance) { - result.vertical.nodes.push(nodeBBounds); - } - - // |‾‾‾‾‾‾‾‾‾‾‾|‾‾‾‾‾|‾‾‾‾‾‾‾‾‾‾‾| - // | A | | B | - // |___________| |___________| - if (distanceTopTop < horizontalDistance) { - result.snapPosition.y = nodeBBounds.top; - result.horizontal.position = nodeBBounds.top; - result.horizontal.nodes = [nodeABounds, nodeBBounds]; - horizontalDistance = distanceTopTop; - } else if (distanceTopTop === horizontalDistance) { - result.horizontal.nodes.push(nodeBBounds); - } - - // |‾‾‾‾‾‾‾‾‾‾‾| - // | A | - // |___________|_________________ - // | | - // | B | - // |___________| - if (distanceBottomTop < horizontalDistance) { - result.snapPosition.y = nodeBBounds.top - nodeABounds.height; - result.horizontal.position = nodeBBounds.top; - result.horizontal.nodes = [nodeABounds, nodeBBounds]; - horizontalDistance = distanceBottomTop; - } else if (distanceBottomTop === horizontalDistance) { - result.horizontal.nodes.push(nodeBBounds); - } - - // |‾‾‾‾‾‾‾‾‾‾‾| |‾‾‾‾‾‾‾‾‾‾‾| - // | A | | B | - // |___________|_____|___________| - if (distanceBottomBottom < horizontalDistance) { - result.snapPosition.y = nodeBBounds.bottom - nodeABounds.height; - result.horizontal.position = nodeBBounds.bottom; - result.horizontal.nodes = [nodeABounds, nodeBBounds]; - horizontalDistance = distanceBottomBottom; - } else if (distanceBottomBottom === horizontalDistance) { - result.horizontal.nodes.push(nodeBBounds); - } - - // |‾‾‾‾‾‾‾‾‾‾‾| - // | B | - // | | - // |‾‾‾‾‾‾‾‾‾‾‾|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ - // | A | - // |___________| - if (distanceTopBottom < horizontalDistance) { - result.snapPosition.y = nodeBBounds.bottom; - result.horizontal.position = nodeBBounds.bottom; - result.horizontal.nodes = [nodeABounds, nodeBBounds]; - horizontalDistance = distanceTopBottom; - } else if (distanceTopBottom === horizontalDistance) { - result.horizontal.nodes.push(nodeBBounds); - } - - // |‾‾‾‾‾‾‾‾‾‾‾| - // | A | - // |___________| - // | - // | - // |‾‾‾‾‾‾‾‾‾‾‾| - // | B | - // |___________| - if (distanceCenterXCenterX < verticalDistance) { - result.snapPosition.x = nodeBBounds.centerX - nodeABounds.width / 2; - result.vertical.position = nodeBBounds.centerX; - result.vertical.nodes = [nodeABounds, nodeBBounds]; - verticalDistance = distanceCenterXCenterX; - } else if (distanceCenterXCenterX === verticalDistance) { - result.vertical.nodes.push(nodeBBounds); - } - - // |‾‾‾‾‾‾‾‾‾‾‾| |‾‾‾‾‾‾‾‾‾‾‾| - // | A |----| B | - // |___________| |___________| - if (distanceCenterYCenterY < horizontalDistance) { - result.snapPosition.y = nodeBBounds.centerY - nodeABounds.height / 2; - result.horizontal.position = nodeBBounds.centerY; - result.horizontal.nodes = [nodeABounds, nodeBBounds]; - horizontalDistance = distanceCenterYCenterY; - } else if (distanceCenterYCenterY === horizontalDistance) { - result.horizontal.nodes.push(nodeBBounds); - } - - return result; - }, - { snapPosition: { x: undefined, y: undefined } } as GetHelperLinesResult - ); - }; - return { - computedNewNodeName, - getHelperLines + computedNewNodeName }; }; diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useWorkflow.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useWorkflow.tsx index f57c56eae72d..b8fbb4a7014d 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useWorkflow.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useWorkflow.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useState } from 'react'; import { Connection, NodeChange, @@ -7,7 +7,9 @@ import { EdgeChange, Edge, applyNodeChanges, - Node + Node, + NodePositionChange, + XYPosition } from 'reactflow'; import { EDGE_TYPE } from '@fastgpt/global/core/workflow/node/constant'; import 'reactflow/dist/style.css'; @@ -17,7 +19,242 @@ import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import { useKeyboard } from './useKeyboard'; import { useContextSelector } from 'use-context-selector'; import { WorkflowContext } from '../../context'; -import { useWorkflowUtils } from './useUtils'; +import { THelperLine } from '@fastgpt/global/core/workflow/type'; + +/* + Compute helper lines for snapping nodes to each other + Refer: https://reactflow.dev/examples/interaction/helper-lines + */ +type GetHelperLinesResult = { + horizontal?: THelperLine; + vertical?: THelperLine; + snapPosition: Partial; +}; +const computeHelperLines = ( + change: NodePositionChange, + nodes: Node[], + distance = 8 // distance to snap +): GetHelperLinesResult => { + const nodeA = nodes.find((node) => node.id === change.id); + + if (!nodeA || !change.position) { + return { + horizontal: undefined, + vertical: undefined, + snapPosition: { x: undefined, y: undefined } + }; + } + + const nodeABounds = { + left: change.position.x, + right: change.position.x + (nodeA.width ?? 0), + top: change.position.y, + bottom: change.position.y + (nodeA.height ?? 0), + width: nodeA.width ?? 0, + height: nodeA.height ?? 0, + centerX: change.position.x + (nodeA.width ?? 0) / 2, + centerY: change.position.y + (nodeA.height ?? 0) / 2 + }; + + let horizontalDistance = distance; + let verticalDistance = distance; + + return nodes + .filter((node) => node.id !== nodeA.id) + .reduce( + (result, nodeB) => { + if (!result.vertical) { + result.vertical = { + position: nodeABounds.centerX, + nodes: [] + }; + } + + if (!result.horizontal) { + result.horizontal = { + position: nodeABounds.centerY, + nodes: [] + }; + } + + const nodeBBounds = { + left: nodeB.position.x, + right: nodeB.position.x + (nodeB.width ?? 0), + top: nodeB.position.y, + bottom: nodeB.position.y + (nodeB.height ?? 0), + width: nodeB.width ?? 0, + height: nodeB.height ?? 0, + centerX: nodeB.position.x + (nodeB.width ?? 0) / 2, + centerY: nodeB.position.y + (nodeB.height ?? 0) / 2 + }; + + const distanceLeftLeft = Math.abs(nodeABounds.left - nodeBBounds.left); + const distanceRightRight = Math.abs(nodeABounds.right - nodeBBounds.right); + const distanceLeftRight = Math.abs(nodeABounds.left - nodeBBounds.right); + const distanceRightLeft = Math.abs(nodeABounds.right - nodeBBounds.left); + const distanceTopTop = Math.abs(nodeABounds.top - nodeBBounds.top); + const distanceBottomTop = Math.abs(nodeABounds.bottom - nodeBBounds.top); + const distanceBottomBottom = Math.abs(nodeABounds.bottom - nodeBBounds.bottom); + const distanceTopBottom = Math.abs(nodeABounds.top - nodeBBounds.bottom); + const distanceCenterXCenterX = Math.abs(nodeABounds.centerX - nodeBBounds.centerX); + const distanceCenterYCenterY = Math.abs(nodeABounds.centerY - nodeBBounds.centerY); + + // |‾‾‾‾‾‾‾‾‾‾‾| + // | A | + // |___________| + // | + // | + // |‾‾‾‾‾‾‾‾‾‾‾| + // | B | + // |___________| + if (distanceLeftLeft < verticalDistance) { + result.snapPosition.x = nodeBBounds.left; + result.vertical.position = nodeBBounds.left; + result.vertical.nodes = [nodeABounds, nodeBBounds]; + verticalDistance = distanceLeftLeft; + } else if (distanceLeftLeft === verticalDistance) { + result.vertical.nodes.push(nodeBBounds); + } + + // |‾‾‾‾‾‾‾‾‾‾‾| + // | A | + // |___________| + // | + // | + // |‾‾‾‾‾‾‾‾‾‾‾| + // | B | + // |___________| + if (distanceRightRight < verticalDistance) { + result.snapPosition.x = nodeBBounds.right - nodeABounds.width; + result.vertical.position = nodeBBounds.right; + result.vertical.nodes = [nodeABounds, nodeBBounds]; + verticalDistance = distanceRightRight; + } else if (distanceRightRight === verticalDistance) { + result.vertical.nodes.push(nodeBBounds); + } + + // |‾‾‾‾‾‾‾‾‾‾‾| + // | A | + // |___________| + // | + // | + // |‾‾‾‾‾‾‾‾‾‾‾| + // | B | + // |___________| + if (distanceLeftRight < verticalDistance) { + result.snapPosition.x = nodeBBounds.right; + result.vertical.position = nodeBBounds.right; + result.vertical.nodes = [nodeABounds, nodeBBounds]; + verticalDistance = distanceLeftRight; + } else if (distanceLeftRight === verticalDistance) { + result.vertical.nodes.push(nodeBBounds); + } + + // |‾‾‾‾‾‾‾‾‾‾‾| + // | A | + // |___________| + // | + // | + // |‾‾‾‾‾‾‾‾‾‾‾| + // | B | + // |___________| + if (distanceRightLeft < verticalDistance) { + result.snapPosition.x = nodeBBounds.left - nodeABounds.width; + result.vertical.position = nodeBBounds.left; + result.vertical.nodes = [nodeABounds, nodeBBounds]; + verticalDistance = distanceRightLeft; + } else if (distanceRightLeft === verticalDistance) { + result.vertical.nodes.push(nodeBBounds); + } + + // |‾‾‾‾‾‾‾‾‾‾‾|‾‾‾‾‾|‾‾‾‾‾‾‾‾‾‾‾| + // | A | | B | + // |___________| |___________| + if (distanceTopTop < horizontalDistance) { + result.snapPosition.y = nodeBBounds.top; + result.horizontal.position = nodeBBounds.top; + result.horizontal.nodes = [nodeABounds, nodeBBounds]; + horizontalDistance = distanceTopTop; + } else if (distanceTopTop === horizontalDistance) { + result.horizontal.nodes.push(nodeBBounds); + } + + // |‾‾‾‾‾‾‾‾‾‾‾| + // | A | + // |___________|_________________ + // | | + // | B | + // |___________| + if (distanceBottomTop < horizontalDistance) { + result.snapPosition.y = nodeBBounds.top - nodeABounds.height; + result.horizontal.position = nodeBBounds.top; + result.horizontal.nodes = [nodeABounds, nodeBBounds]; + horizontalDistance = distanceBottomTop; + } else if (distanceBottomTop === horizontalDistance) { + result.horizontal.nodes.push(nodeBBounds); + } + + // |‾‾‾‾‾‾‾‾‾‾‾| |‾‾‾‾‾‾‾‾‾‾‾| + // | A | | B | + // |___________|_____|___________| + if (distanceBottomBottom < horizontalDistance) { + result.snapPosition.y = nodeBBounds.bottom - nodeABounds.height; + result.horizontal.position = nodeBBounds.bottom; + result.horizontal.nodes = [nodeABounds, nodeBBounds]; + horizontalDistance = distanceBottomBottom; + } else if (distanceBottomBottom === horizontalDistance) { + result.horizontal.nodes.push(nodeBBounds); + } + + // |‾‾‾‾‾‾‾‾‾‾‾| + // | B | + // | | + // |‾‾‾‾‾‾‾‾‾‾‾|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ + // | A | + // |___________| + if (distanceTopBottom < horizontalDistance) { + result.snapPosition.y = nodeBBounds.bottom; + result.horizontal.position = nodeBBounds.bottom; + result.horizontal.nodes = [nodeABounds, nodeBBounds]; + horizontalDistance = distanceTopBottom; + } else if (distanceTopBottom === horizontalDistance) { + result.horizontal.nodes.push(nodeBBounds); + } + + // |‾‾‾‾‾‾‾‾‾‾‾| + // | A | + // |___________| + // | + // | + // |‾‾‾‾‾‾‾‾‾‾‾| + // | B | + // |___________| + if (distanceCenterXCenterX < verticalDistance) { + result.snapPosition.x = nodeBBounds.centerX - nodeABounds.width / 2; + result.vertical.position = nodeBBounds.centerX; + result.vertical.nodes = [nodeABounds, nodeBBounds]; + verticalDistance = distanceCenterXCenterX; + } else if (distanceCenterXCenterX === verticalDistance) { + result.vertical.nodes.push(nodeBBounds); + } + + // |‾‾‾‾‾‾‾‾‾‾‾| |‾‾‾‾‾‾‾‾‾‾‾| + // | A |----| B | + // |___________| |___________| + if (distanceCenterYCenterY < horizontalDistance) { + result.snapPosition.y = nodeBBounds.centerY - nodeABounds.height / 2; + result.horizontal.position = nodeBBounds.centerY; + result.horizontal.nodes = [nodeABounds, nodeBBounds]; + horizontalDistance = distanceCenterYCenterY; + } else if (distanceCenterYCenterY === horizontalDistance) { + result.horizontal.nodes.push(nodeBBounds); + } + + return result; + }, + { snapPosition: { x: undefined, y: undefined } } as GetHelperLinesResult + ); +}; export const useWorkflow = () => { const { toast } = useToast(); @@ -35,65 +272,81 @@ export const useWorkflow = () => { onNodesChange, setEdges, onEdgesChange, - setHoverEdgeId, - setHelperLineHorizontal, - setHelperLineVertical + setHoverEdgeId } = useContextSelector(WorkflowContext, (v) => v); - const { getHelperLines } = useWorkflowUtils(); + /* helper line */ + const [helperLineHorizontal, setHelperLineHorizontal] = useState(); + const [helperLineVertical, setHelperLineVertical] = useState(); + + const customApplyNodeChanges = (changes: NodeChange[], nodes: Node[]): Node[] => { + const positionChange = + changes[0].type === 'position' && changes[0].dragging ? changes[0] : undefined; + + if (changes.length === 1 && positionChange?.position) { + // 只判断,3000px 内的 nodes,并按从近到远的顺序排序 + const filterNodes = nodes + .filter((node) => { + if (!positionChange.position) return false; - const customApplyNodeChanges = useCallback((changes: NodeChange[], nodes: Node[]): Node[] => { - setHelperLineHorizontal(undefined); - setHelperLineVertical(undefined); + return ( + Math.abs(node.position.x - positionChange.position.x) <= 3000 && + Math.abs(node.position.y - positionChange.position.y) <= 3000 + ); + }) + .sort((a, b) => { + if (!positionChange.position) return 0; + return ( + Math.abs(a.position.x - positionChange.position.x) + + Math.abs(a.position.y - positionChange.position.y) - + Math.abs(b.position.x - positionChange.position.x) - + Math.abs(b.position.y - positionChange.position.y) + ); + }) + .slice(0, 15); - if ( - changes.length === 1 && - changes[0].type === 'position' && - changes[0].dragging && - changes[0].position - ) { - const helperLines = getHelperLines(changes[0], nodes); + const helperLines = computeHelperLines(positionChange, filterNodes); - changes[0].position.x = helperLines.snapPosition.x ?? changes[0].position.x; - changes[0].position.y = helperLines.snapPosition.y ?? changes[0].position.y; + positionChange.position.x = helperLines.snapPosition.x ?? positionChange.position.x; + positionChange.position.y = helperLines.snapPosition.y ?? positionChange.position.y; setHelperLineHorizontal(helperLines.horizontal); setHelperLineVertical(helperLines.vertical); + } else { + setHelperLineHorizontal(undefined); + setHelperLineVertical(undefined); } return applyNodeChanges(changes, nodes); - }, []); + }; /* node */ - const handleNodesChange = useCallback( - (changes: NodeChange[]) => { - setNodes((nodes) => customApplyNodeChanges(changes, nodes)); - - for (const change of changes) { - if (change.type === 'remove') { - const node = nodes.find((n) => n.id === change.id); - if (node && node.data.forbidDelete) { - return toast({ - status: 'warning', - title: t('common:core.workflow.Can not delete node') - }); - } else { - return onOpenConfirmDeleteNode(() => { - onNodesChange(changes); - setEdges((state) => - state.filter((edge) => edge.source !== change.id && edge.target !== change.id) - ); - })(); - } - } else if (change.type === 'select' && change.selected === false && isDowningCtrl) { - change.selected = true; + const handleNodesChange = (changes: NodeChange[]) => { + setNodes((nodes) => customApplyNodeChanges(changes, nodes)); + + for (const change of changes) { + if (change.type === 'remove') { + const node = nodes.find((n) => n.id === change.id); + if (node && node.data.forbidDelete) { + return toast({ + status: 'warning', + title: t('common:core.workflow.Can not delete node') + }); + } else { + return onOpenConfirmDeleteNode(() => { + onNodesChange(changes); + setEdges((state) => + state.filter((edge) => edge.source !== change.id && edge.target !== change.id) + ); + })(); } + } else if (change.type === 'select' && change.selected === false && isDowningCtrl) { + change.selected = true; } + } - onNodesChange(changes); - }, - [isDowningCtrl, nodes, onNodesChange, onOpenConfirmDeleteNode, setEdges, t, toast] - ); + onNodesChange(changes); + }; const handleEdgeChange = useCallback( (changes: EdgeChange[]) => { onEdgesChange(changes.filter((change) => change.type !== 'remove')); @@ -163,7 +416,11 @@ export const useWorkflow = () => { onConnect, customOnConnect, onEdgeMouseEnter, - onEdgeMouseLeave + onEdgeMouseLeave, + helperLineHorizontal, + setHelperLineHorizontal, + helperLineVertical, + setHelperLineVertical }; }; diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/index.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/index.tsx index b386f48891c7..5bdc5cf80106 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/index.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/index.tsx @@ -64,8 +64,7 @@ const edgeTypes = { }; const Workflow = () => { - const { nodes, edges, reactFlowWrapper, helperLineHorizontal, helperLineVertical } = - useContextSelector(WorkflowContext, (v) => v); + const { nodes, edges, reactFlowWrapper } = useContextSelector(WorkflowContext, (v) => v); const { ConfirmDeleteModal, @@ -75,7 +74,9 @@ const Workflow = () => { onConnectEnd, customOnConnect, onEdgeMouseEnter, - onEdgeMouseLeave + onEdgeMouseLeave, + helperLineHorizontal, + helperLineVertical } = useWorkflow(); const { @@ -85,7 +86,7 @@ const Workflow = () => { } = useDisclosure(); return ( - + <> { + + ); +}; + +const Render = () => { + return ( + + ); }; -export default React.memo(Workflow); +export default React.memo(Render); const FlowController = React.memo(function FlowController() { const { fitView } = useReactFlow(); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx index a9dea3061675..fa31d0ce5876 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx @@ -48,7 +48,6 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { formatTime2HM, formatTime2YMDHMW } from '@fastgpt/global/common/string/time'; import type { InitProps } from '@/pages/app/detail/components/PublishHistoriesSlider'; import { cloneDeep } from 'lodash'; -import { THelperLine } from '@fastgpt/global/core/workflow/type'; type OnChange = (changes: ChangesType[]) => void; @@ -136,12 +135,6 @@ type WorkflowContextType = { historiesDefaultData?: InitProps; setHistoriesDefaultData: React.Dispatch>; - // helper line - helperLineHorizontal?: THelperLine; - setHelperLineHorizontal: React.Dispatch>; - helperLineVertical?: THelperLine; - setHelperLineVertical: React.Dispatch>; - // chat test setWorkflowTestData: React.Dispatch< React.SetStateAction< @@ -267,14 +260,6 @@ export const WorkflowContext = createContext({ setHistoriesDefaultData: function (value: React.SetStateAction): void { throw new Error('Function not implemented.'); }, - helperLineHorizontal: undefined, - setHelperLineHorizontal: function (value: React.SetStateAction): void { - throw new Error('Function not implemented.'); - }, - helperLineVertical: undefined, - setHelperLineVertical: function (value: React.SetStateAction): void { - throw new Error('Function not implemented.'); - }, getNodeDynamicInputs: function (nodeId: string): FlowNodeInputItemType[] { throw new Error('Function not implemented.'); } @@ -742,11 +727,6 @@ const WorkflowContextProvider = ({ /* Version histories */ const [historiesDefaultData, setHistoriesDefaultData] = useState(); - /* helper line */ - const [helperLineHorizontal, setHelperLineHorizontal] = useState( - undefined - ); - const [helperLineVertical, setHelperLineVertical] = useState(undefined); /* event bus */ useEffect(() => { eventBus.on(EventNameEnum.requestWorkflowStore, () => { @@ -816,12 +796,6 @@ const WorkflowContextProvider = ({ historiesDefaultData, setHistoriesDefaultData, - // helper line - helperLineHorizontal, - setHelperLineHorizontal, - helperLineVertical, - setHelperLineVertical, - // chat test setWorkflowTestData }; diff --git a/projects/app/src/pages/chat/components/ChatHeader.tsx b/projects/app/src/pages/chat/components/ChatHeader.tsx index 6e3f18fc0edf..c053c3cf7ff7 100644 --- a/projects/app/src/pages/chat/components/ChatHeader.tsx +++ b/projects/app/src/pages/chat/components/ChatHeader.tsx @@ -36,6 +36,7 @@ const ChatHeader = ({ apps?: AppListItemType[]; onRouteToAppDetail?: () => void; }) => { + const { t } = useTranslation(); const isPlugin = chatData.app.type === AppTypeEnum.plugin; const { isPc } = useSystem(); @@ -50,7 +51,11 @@ const ChatHeader = ({ > {isPc ? ( <> - + ) : ( diff --git a/projects/app/src/service/support/permission/auth/chat.ts b/projects/app/src/service/support/permission/auth/chat.ts index d72426335a20..7c1a03ba0d81 100644 --- a/projects/app/src/service/support/permission/auth/chat.ts +++ b/projects/app/src/service/support/permission/auth/chat.ts @@ -70,7 +70,7 @@ export async function authChatCrud({ if (!chat) return { id: outLinkUid }; - // auth req + // auth req const { teamId, tmbId, permission } = await authUserPer({ ...props, per: ReadPermissionVal @@ -81,7 +81,7 @@ export async function authChatCrud({ if (permission.isOwner) return { uid: outLinkUid }; if (String(tmbId) === String(chat.tmbId)) return { uid: outLinkUid }; - // admin + // Admin can manage all chat if (per === WritePermissionVal && permission.hasManagePer) return { uid: outLinkUid }; return Promise.reject(ChatErrEnum.unAuthChat);