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
This commit is contained in:
Archer
2024-08-15 13:12:39 +08:00
committed by GitHub
parent fdeb1590d7
commit 86c27e85ef
16 changed files with 499 additions and 479 deletions

View File

@@ -20,4 +20,10 @@ weight: 816
## V4.8.10 更新说明 ## V4.8.10 更新说明
1. 新增 - 模板市场 1. 新增 - 模板市场
2. 2. 新增 - 工作流节点拖动自动对齐吸附
3. 新增 - 用户选择节点Debug 模式暂未支持)
4. 商业版新增 - 飞书机器人接入
5. 商业版新增 - 公众号接入接入
6. 修复 - Prompt 模式调用工具stream=false 模式下,会携带 0: 开头标记。
7. 修复 - 对话日志鉴权问题:仅为 APP 管理员的用户,无法查看对话日志详情。
8. 修复 - 选择 Milvus 部署时,无法导出知识库。

View File

@@ -411,6 +411,7 @@ const parseAnswer = (
str = str.trim(); str = str.trim();
// 首先使用正则表达式提取TOOL_ID和TOOL_ARGUMENTS // 首先使用正则表达式提取TOOL_ID和TOOL_ARGUMENTS
const prefixReg = /^1(:|)/; const prefixReg = /^1(:|)/;
const answerPrefixReg = /^0(:|)/;
if (prefixReg.test(str)) { if (prefixReg.test(str)) {
const toolString = sliceJsonStr(str); const toolString = sliceJsonStr(str);
@@ -432,7 +433,7 @@ const parseAnswer = (
} }
} else { } else {
return { return {
answer: str answer: str.replace(answerPrefixReg, '')
}; };
} }
}; };

View File

@@ -30,7 +30,7 @@ export type InitChatResponse = {
chatId?: string; chatId?: string;
appId: string; appId: string;
userAvatar?: string; userAvatar?: string;
title: string; title?: string;
variables: Record<string, any>; variables: Record<string, any>;
history: ChatItemType[]; history: ChatItemType[];
app: { app: {

View File

@@ -1,6 +1,6 @@
import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { InitChatResponse } from './api'; import { InitChatResponse } from './api';
import { i18nT } from '@fastgpt/web/i18n/utils';
export const defaultChatData: InitChatResponse = { export const defaultChatData: InitChatResponse = {
chatId: '', chatId: '',
appId: '', appId: '',
@@ -12,7 +12,7 @@ export const defaultChatData: InitChatResponse = {
type: AppTypeEnum.simple, type: AppTypeEnum.simple,
pluginInputs: [] pluginInputs: []
}, },
title: i18nT('chat:new_chat'), title: '',
variables: {}, variables: {},
history: [] history: []
}; };

View File

@@ -1,11 +1,15 @@
import { authChatCrud } from '@/service/support/permission/auth/chat'; 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 { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type'; import { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat'; import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
export type getResDataQuery = OutLinkChatAuthProps & { export type getResDataQuery = OutLinkChatAuthProps & {
chatId?: string; chatId?: string;
@@ -25,12 +29,24 @@ async function handler(
if (!appId || !chatId || !dataId) { if (!appId || !chatId || !dataId) {
return {}; return {};
} }
await authChatCrud({
req, // 1. Un login api: share chat, team chat
authToken: true, // 2. Login api: account chat, chat log
...req.query, try {
per: ReadPermissionVal await authChatCrud({
}); req,
authToken: true,
...req.query,
per: ReadPermissionVal
});
} catch (error) {
await authApp({
req,
authToken: true,
appId,
per: ManagePermissionVal
});
}
const chatData = await MongoChatItem.findOne({ const chatData = await MongoChatItem.findOne({
appId, appId,

View File

@@ -14,7 +14,7 @@ import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { transformPreviewHistories } from '@/global/core/chat/utils'; import { transformPreviewHistories } from '@/global/core/chat/utils';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { i18nT } from '@fastgpt/web/i18n/utils';
async function handler( async function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse res: NextApiResponse
@@ -62,7 +62,7 @@ async function handler(
return { return {
chatId, chatId,
appId, appId,
title: chat?.title || i18nT('chat:new_chat'), title: chat?.title,
userAvatar: undefined, userAvatar: undefined,
variables: chat?.variables || {}, variables: chat?.variables || {},
history: app.type === AppTypeEnum.plugin ? histories : transformPreviewHistories(histories), history: app.type === AppTypeEnum.plugin ? histories : transformPreviewHistories(histories),

View File

@@ -18,90 +18,84 @@ import { getAppLatestVersion } from '@fastgpt/service/core/app/controller';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { transformPreviewHistories } from '@/global/core/chat/utils'; 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();
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 // auth link permission
const { shareChat, uid, appId } = await authOutLink({ shareId, outLinkUid }); const { shareChat, uid, appId } = await authOutLink({ shareId, outLinkUid });
// auth app permission // auth app permission
const [tmb, chat, app] = await Promise.all([ const [tmb, chat, app] = await Promise.all([
MongoTeamMember.findById(shareChat.tmbId, '_id userId').populate('userId', 'avatar').lean(), MongoTeamMember.findById(shareChat.tmbId, '_id userId').populate('userId', 'avatar').lean(),
MongoChat.findOne({ appId, chatId, shareId }).lean(), MongoChat.findOne({ appId, chatId, shareId }).lean(),
MongoApp.findById(appId).lean() MongoApp.findById(appId).lean()
]); ]);
if (!app) { if (!app) {
throw new Error(AppErrEnum.unExist); throw new Error(AppErrEnum.unExist);
} }
// auth chat permission // auth chat permission
if (chat && chat.outLinkUid !== uid) { if (chat && chat.outLinkUid !== uid) {
throw new Error(ChatErrEnum.unAuthChat); throw new Error(ChatErrEnum.unAuthChat);
} }
const [{ histories }, { nodes }] = await Promise.all([ const [{ histories }, { nodes }] = await Promise.all([
getChatItems({ getChatItems({
appId: app._id, appId: app._id,
chatId, chatId,
limit: 30, limit: 30,
field: `dataId obj value userGoodFeedback userBadFeedback ${ field: `dataId obj value userGoodFeedback userBadFeedback ${
shareChat.responseDetail || app.type === AppTypeEnum.plugin shareChat.responseDetail || app.type === AppTypeEnum.plugin
? `adminFeedback ${DispatchNodeResponseKeyEnum.nodeResponse}` ? `adminFeedback ${DispatchNodeResponseKeyEnum.nodeResponse}`
: '' : ''
} ` } `
}), }),
getAppLatestVersion(app._id, app) getAppLatestVersion(app._id, app)
]); ]);
// pick share response field // pick share response field
app.type !== AppTypeEnum.plugin && app.type !== AppTypeEnum.plugin &&
histories.forEach((item) => { histories.forEach((item) => {
if (item.obj === ChatRoleEnum.AI) { if (item.obj === ChatRoleEnum.AI) {
item.responseData = filterPublicNodeResponseData({ flowResponses: item.responseData }); item.responseData = filterPublicNodeResponseData({ flowResponses: item.responseData });
}
});
jsonRes<InitChatResponse>(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 ?? []
}
} }
}); });
} catch (err) {
jsonRes(res, { jsonRes<InitChatResponse>(res, {
code: 500, data: {
error: err 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 = { export const config = {
api: { api: {
responseLimit: '10mb' responseLimit: '10mb'

View File

@@ -18,92 +18,85 @@ import { getAppLatestVersion } from '@fastgpt/service/core/app/controller';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { transformPreviewHistories } from '@/global/core/chat/utils'; 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) { async function handler(req: NextApiRequest, res: NextApiResponse) {
try { let { teamId, appId, chatId, teamToken } = req.query as InitTeamChatProps;
await connectToDatabase();
let { teamId, appId, chatId, teamToken } = req.query as InitTeamChatProps; if (!teamId || !appId || !teamToken) {
throw new Error('teamId, appId, teamToken are required');
}
if (!teamId || !appId || !teamToken) { const { uid } = await authTeamSpaceToken({
throw new Error('teamId, appId, teamToken are required'); teamId,
} teamToken
});
const { uid } = await authTeamSpaceToken({ const [team, chat, app] = await Promise.all([
teamId, MongoTeam.findById(teamId, 'name avatar').lean(),
teamToken MongoChat.findOne({ teamId, appId, chatId }).lean(),
}); MongoApp.findById(appId).lean()
]);
const [team, chat, app] = await Promise.all([ if (!app) {
MongoTeam.findById(teamId, 'name avatar').lean(), throw new Error(AppErrEnum.unExist);
MongoChat.findOne({ teamId, appId, chatId }).lean(), }
MongoApp.findById(appId).lean()
]);
if (!app) { // auth chat permission
throw new Error(AppErrEnum.unExist); if (chat && chat.outLinkUid !== uid) {
} throw new Error(ChatErrEnum.unAuthChat);
}
// auth chat permission // get app and history
if (chat && chat.outLinkUid !== uid) { const [{ histories }, { nodes }] = await Promise.all([
throw new Error(ChatErrEnum.unAuthChat); getChatItems({
} appId,
chatId,
limit: 30,
field: `dataId obj value userGoodFeedback userBadFeedback adminFeedback ${DispatchNodeResponseKeyEnum.nodeResponse}`
}),
getAppLatestVersion(app._id, app)
]);
// get app and history // pick share response field
const [{ histories }, { nodes }] = await Promise.all([ app.type !== AppTypeEnum.plugin &&
getChatItems({ histories.forEach((item) => {
appId, if (item.obj === ChatRoleEnum.AI) {
chatId, item.responseData = filterPublicNodeResponseData({ flowResponses: item.responseData });
limit: 30,
field: `dataId obj value userGoodFeedback userBadFeedback adminFeedback ${DispatchNodeResponseKeyEnum.nodeResponse}`
}),
getAppLatestVersion(app._id, app)
]);
// pick share response field
app.type !== AppTypeEnum.plugin &&
histories.forEach((item) => {
if (item.obj === ChatRoleEnum.AI) {
item.responseData = filterPublicNodeResponseData({ flowResponses: item.responseData });
}
});
jsonRes<InitChatResponse>(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 ?? []
}
} }
}); });
} catch (err) {
jsonRes(res, { jsonRes<InitChatResponse>(res, {
code: 500, data: {
error: err 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 = { export const config = {
api: { api: {
responseLimit: '10mb' responseLimit: '10mb'

View File

@@ -18,7 +18,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
datasetId: string; datasetId: string;
}; };
if (!datasetId || !global.pgClient) { if (!datasetId) {
return Promise.reject(CommonErrEnum.missingParams); return Promise.reject(CommonErrEnum.missingParams);
} }

View File

@@ -89,7 +89,7 @@ function HelperLinesRenderer({ horizontal, vertical }: HelperLinesProps) {
drawCross(node.right * transform[2] + transform[0], y, 5 * zoom); drawCross(node.right * transform[2] + transform[0], y, 5 * zoom);
}); });
} }
}, [width, height, transform, horizontal, vertical]); }, [width, height, transform, horizontal, vertical, zoom]);
return <canvas ref={canvasRef} style={canvasStyle} />; return <canvas ref={canvasRef} style={canvasStyle} />;
} }

View File

@@ -3,14 +3,6 @@ import { WorkflowContext } from '../../context';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; 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<XYPosition>;
};
export const useWorkflowUtils = () => { export const useWorkflowUtils = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -40,235 +32,8 @@ export const useWorkflowUtils = () => {
[nodeList] [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<GetHelperLinesResult>(
(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 { return {
computedNewNodeName, computedNewNodeName
getHelperLines
}; };
}; };

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useState } from 'react';
import { import {
Connection, Connection,
NodeChange, NodeChange,
@@ -7,7 +7,9 @@ import {
EdgeChange, EdgeChange,
Edge, Edge,
applyNodeChanges, applyNodeChanges,
Node Node,
NodePositionChange,
XYPosition
} from 'reactflow'; } from 'reactflow';
import { EDGE_TYPE } from '@fastgpt/global/core/workflow/node/constant'; import { EDGE_TYPE } from '@fastgpt/global/core/workflow/node/constant';
import 'reactflow/dist/style.css'; import 'reactflow/dist/style.css';
@@ -17,7 +19,242 @@ import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useKeyboard } from './useKeyboard'; import { useKeyboard } from './useKeyboard';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context'; 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<XYPosition>;
};
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<GetHelperLinesResult>(
(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 = () => { export const useWorkflow = () => {
const { toast } = useToast(); const { toast } = useToast();
@@ -35,65 +272,81 @@ export const useWorkflow = () => {
onNodesChange, onNodesChange,
setEdges, setEdges,
onEdgesChange, onEdgesChange,
setHoverEdgeId, setHoverEdgeId
setHelperLineHorizontal,
setHelperLineVertical
} = useContextSelector(WorkflowContext, (v) => v); } = useContextSelector(WorkflowContext, (v) => v);
const { getHelperLines } = useWorkflowUtils(); /* helper line */
const [helperLineHorizontal, setHelperLineHorizontal] = useState<THelperLine>();
const [helperLineVertical, setHelperLineVertical] = useState<THelperLine>();
const customApplyNodeChanges = useCallback((changes: NodeChange[], nodes: Node[]): Node[] => { const customApplyNodeChanges = (changes: NodeChange[], nodes: Node[]): Node[] => {
setHelperLineHorizontal(undefined); const positionChange =
setHelperLineVertical(undefined); changes[0].type === 'position' && changes[0].dragging ? changes[0] : undefined;
if ( if (changes.length === 1 && positionChange?.position) {
changes.length === 1 && // 只判断3000px 内的 nodes并按从近到远的顺序排序
changes[0].type === 'position' && const filterNodes = nodes
changes[0].dragging && .filter((node) => {
changes[0].position if (!positionChange.position) return false;
) {
const helperLines = getHelperLines(changes[0], nodes);
changes[0].position.x = helperLines.snapPosition.x ?? changes[0].position.x; return (
changes[0].position.y = helperLines.snapPosition.y ?? changes[0].position.y; 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);
const helperLines = computeHelperLines(positionChange, filterNodes);
positionChange.position.x = helperLines.snapPosition.x ?? positionChange.position.x;
positionChange.position.y = helperLines.snapPosition.y ?? positionChange.position.y;
setHelperLineHorizontal(helperLines.horizontal); setHelperLineHorizontal(helperLines.horizontal);
setHelperLineVertical(helperLines.vertical); setHelperLineVertical(helperLines.vertical);
} else {
setHelperLineHorizontal(undefined);
setHelperLineVertical(undefined);
} }
return applyNodeChanges(changes, nodes); return applyNodeChanges(changes, nodes);
}, []); };
/* node */ /* node */
const handleNodesChange = useCallback( const handleNodesChange = (changes: NodeChange[]) => {
(changes: NodeChange[]) => { setNodes((nodes) => customApplyNodeChanges(changes, nodes));
setNodes((nodes) => customApplyNodeChanges(changes, nodes));
for (const change of changes) { for (const change of changes) {
if (change.type === 'remove') { if (change.type === 'remove') {
const node = nodes.find((n) => n.id === change.id); const node = nodes.find((n) => n.id === change.id);
if (node && node.data.forbidDelete) { if (node && node.data.forbidDelete) {
return toast({ return toast({
status: 'warning', status: 'warning',
title: t('common:core.workflow.Can not delete node') title: t('common:core.workflow.Can not delete node')
}); });
} else { } else {
return onOpenConfirmDeleteNode(() => { return onOpenConfirmDeleteNode(() => {
onNodesChange(changes); onNodesChange(changes);
setEdges((state) => setEdges((state) =>
state.filter((edge) => edge.source !== change.id && edge.target !== change.id) state.filter((edge) => edge.source !== change.id && edge.target !== change.id)
); );
})(); })();
}
} else if (change.type === 'select' && change.selected === false && isDowningCtrl) {
change.selected = true;
} }
} else if (change.type === 'select' && change.selected === false && isDowningCtrl) {
change.selected = true;
} }
}
onNodesChange(changes); onNodesChange(changes);
}, };
[isDowningCtrl, nodes, onNodesChange, onOpenConfirmDeleteNode, setEdges, t, toast]
);
const handleEdgeChange = useCallback( const handleEdgeChange = useCallback(
(changes: EdgeChange[]) => { (changes: EdgeChange[]) => {
onEdgesChange(changes.filter((change) => change.type !== 'remove')); onEdgesChange(changes.filter((change) => change.type !== 'remove'));
@@ -163,7 +416,11 @@ export const useWorkflow = () => {
onConnect, onConnect,
customOnConnect, customOnConnect,
onEdgeMouseEnter, onEdgeMouseEnter,
onEdgeMouseLeave onEdgeMouseLeave,
helperLineHorizontal,
setHelperLineHorizontal,
helperLineVertical,
setHelperLineVertical
}; };
}; };

View File

@@ -64,8 +64,7 @@ const edgeTypes = {
}; };
const Workflow = () => { const Workflow = () => {
const { nodes, edges, reactFlowWrapper, helperLineHorizontal, helperLineVertical } = const { nodes, edges, reactFlowWrapper } = useContextSelector(WorkflowContext, (v) => v);
useContextSelector(WorkflowContext, (v) => v);
const { const {
ConfirmDeleteModal, ConfirmDeleteModal,
@@ -75,7 +74,9 @@ const Workflow = () => {
onConnectEnd, onConnectEnd,
customOnConnect, customOnConnect,
onEdgeMouseEnter, onEdgeMouseEnter,
onEdgeMouseLeave onEdgeMouseLeave,
helperLineHorizontal,
helperLineVertical
} = useWorkflow(); } = useWorkflow();
const { const {
@@ -85,7 +86,7 @@ const Workflow = () => {
} = useDisclosure(); } = useDisclosure();
return ( return (
<ReactFlowProvider> <>
<Box <Box
flex={'1 0 0'} flex={'1 0 0'}
h={0} h={0}
@@ -143,11 +144,19 @@ const Workflow = () => {
</Box> </Box>
<ConfirmDeleteModal /> <ConfirmDeleteModal />
</>
);
};
const Render = () => {
return (
<ReactFlowProvider>
<Workflow />
</ReactFlowProvider> </ReactFlowProvider>
); );
}; };
export default React.memo(Workflow); export default React.memo(Render);
const FlowController = React.memo(function FlowController() { const FlowController = React.memo(function FlowController() {
const { fitView } = useReactFlow(); const { fitView } = useReactFlow();

View File

@@ -48,7 +48,6 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { formatTime2HM, formatTime2YMDHMW } from '@fastgpt/global/common/string/time'; import { formatTime2HM, formatTime2YMDHMW } from '@fastgpt/global/common/string/time';
import type { InitProps } from '@/pages/app/detail/components/PublishHistoriesSlider'; import type { InitProps } from '@/pages/app/detail/components/PublishHistoriesSlider';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { THelperLine } from '@fastgpt/global/core/workflow/type';
type OnChange<ChangesType> = (changes: ChangesType[]) => void; type OnChange<ChangesType> = (changes: ChangesType[]) => void;
@@ -136,12 +135,6 @@ type WorkflowContextType = {
historiesDefaultData?: InitProps; historiesDefaultData?: InitProps;
setHistoriesDefaultData: React.Dispatch<React.SetStateAction<undefined | InitProps>>; setHistoriesDefaultData: React.Dispatch<React.SetStateAction<undefined | InitProps>>;
// helper line
helperLineHorizontal?: THelperLine;
setHelperLineHorizontal: React.Dispatch<React.SetStateAction<THelperLine | undefined>>;
helperLineVertical?: THelperLine;
setHelperLineVertical: React.Dispatch<React.SetStateAction<THelperLine | undefined>>;
// chat test // chat test
setWorkflowTestData: React.Dispatch< setWorkflowTestData: React.Dispatch<
React.SetStateAction< React.SetStateAction<
@@ -267,14 +260,6 @@ export const WorkflowContext = createContext<WorkflowContextType>({
setHistoriesDefaultData: function (value: React.SetStateAction<InitProps | undefined>): void { setHistoriesDefaultData: function (value: React.SetStateAction<InitProps | undefined>): void {
throw new Error('Function not implemented.'); throw new Error('Function not implemented.');
}, },
helperLineHorizontal: undefined,
setHelperLineHorizontal: function (value: React.SetStateAction<THelperLine | undefined>): void {
throw new Error('Function not implemented.');
},
helperLineVertical: undefined,
setHelperLineVertical: function (value: React.SetStateAction<THelperLine | undefined>): void {
throw new Error('Function not implemented.');
},
getNodeDynamicInputs: function (nodeId: string): FlowNodeInputItemType[] { getNodeDynamicInputs: function (nodeId: string): FlowNodeInputItemType[] {
throw new Error('Function not implemented.'); throw new Error('Function not implemented.');
} }
@@ -742,11 +727,6 @@ const WorkflowContextProvider = ({
/* Version histories */ /* Version histories */
const [historiesDefaultData, setHistoriesDefaultData] = useState<InitProps>(); const [historiesDefaultData, setHistoriesDefaultData] = useState<InitProps>();
/* helper line */
const [helperLineHorizontal, setHelperLineHorizontal] = useState<THelperLine | undefined>(
undefined
);
const [helperLineVertical, setHelperLineVertical] = useState<THelperLine | undefined>(undefined);
/* event bus */ /* event bus */
useEffect(() => { useEffect(() => {
eventBus.on(EventNameEnum.requestWorkflowStore, () => { eventBus.on(EventNameEnum.requestWorkflowStore, () => {
@@ -816,12 +796,6 @@ const WorkflowContextProvider = ({
historiesDefaultData, historiesDefaultData,
setHistoriesDefaultData, setHistoriesDefaultData,
// helper line
helperLineHorizontal,
setHelperLineHorizontal,
helperLineVertical,
setHelperLineVertical,
// chat test // chat test
setWorkflowTestData setWorkflowTestData
}; };

View File

@@ -36,6 +36,7 @@ const ChatHeader = ({
apps?: AppListItemType[]; apps?: AppListItemType[];
onRouteToAppDetail?: () => void; onRouteToAppDetail?: () => void;
}) => { }) => {
const { t } = useTranslation();
const isPlugin = chatData.app.type === AppTypeEnum.plugin; const isPlugin = chatData.app.type === AppTypeEnum.plugin;
const { isPc } = useSystem(); const { isPc } = useSystem();
@@ -50,7 +51,11 @@ const ChatHeader = ({
> >
{isPc ? ( {isPc ? (
<> <>
<PcHeader title={chatData.title} chatModels={chatData.app.chatModels} history={history} /> <PcHeader
title={chatData.title || t('chat:new_chat')}
chatModels={chatData.app.chatModels}
history={history}
/>
<Box flex={1} /> <Box flex={1} />
</> </>
) : ( ) : (

View File

@@ -70,7 +70,7 @@ export async function authChatCrud({
if (!chat) return { id: outLinkUid }; if (!chat) return { id: outLinkUid };
// auth req // auth req
const { teamId, tmbId, permission } = await authUserPer({ const { teamId, tmbId, permission } = await authUserPer({
...props, ...props,
per: ReadPermissionVal per: ReadPermissionVal
@@ -81,7 +81,7 @@ export async function authChatCrud({
if (permission.isOwner) return { uid: outLinkUid }; if (permission.isOwner) return { uid: outLinkUid };
if (String(tmbId) === String(chat.tmbId)) 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 }; if (per === WritePermissionVal && permission.hasManagePer) return { uid: outLinkUid };
return Promise.reject(ChatErrEnum.unAuthChat); return Promise.reject(ChatErrEnum.unAuthChat);