4.8.10 fix (#2572)

* fix: circle workflow response modal

* perf: workflow runtime check
This commit is contained in:
Archer
2024-08-29 18:00:56 +08:00
committed by GitHub
parent 322ca757af
commit 813eaacfd0
11 changed files with 143 additions and 97 deletions

View File

@@ -151,6 +151,7 @@ export type ChatHistoryItemType = HistoryItemType & {
/* ------- response data ------------ */
export type ChatHistoryItemResType = DispatchNodeResponseType & {
nodeId: string;
id: string;
moduleType: FlowNodeTypeEnum;
moduleName: string;
};

View File

@@ -159,6 +159,9 @@ export type DispatchNodeResponseType = {
// user select
userSelectResult?: string;
// update var
updateVarResult?: any[];
};
export type DispatchNodeResultType<T> = {

View File

@@ -117,39 +117,6 @@ export const filterWorkflowEdges = (edges: RuntimeEdgeItemType[]) => {
);
};
/*
区分普通连线和递归连线
递归连线:可以通过往上查询 nodes最终追溯到自身
*/
export const splitEdges2WorkflowEdges = ({
edges,
allEdges,
currentNode
}: {
edges: RuntimeEdgeItemType[];
allEdges: RuntimeEdgeItemType[];
currentNode: RuntimeNodeItemType;
}) => {
const commonEdges: RuntimeEdgeItemType[] = [];
const recursiveEdges: RuntimeEdgeItemType[] = [];
edges.forEach((edge) => {
const checkIsCurrentNode = (edge: RuntimeEdgeItemType): boolean => {
const sourceEdge = allEdges.find((item) => item.target === edge.source);
if (!sourceEdge) return false;
if (sourceEdge.source === currentNode.nodeId) return true;
return checkIsCurrentNode(sourceEdge);
};
if (checkIsCurrentNode(edge)) {
recursiveEdges.push(edge);
} else {
commonEdges.push(edge);
}
});
return { commonEdges, recursiveEdges };
};
/*
1. 输入线分类:普通线和递归线(可以追溯到自身)
2. 起始线全部非 waiting 执行,或递归线全部非 waiting 执行
@@ -161,31 +128,72 @@ export const checkNodeRunStatus = ({
node: RuntimeNodeItemType;
runtimeEdges: RuntimeEdgeItemType[];
}) => {
const workflowEdges = filterWorkflowEdges(runtimeEdges).filter(
/*
区分普通连线和递归连线
递归连线:可以通过往上查询 nodes最终追溯到自身
*/
const splitEdges2WorkflowEdges = ({
sourceEdges,
allEdges,
currentNode
}: {
sourceEdges: RuntimeEdgeItemType[];
allEdges: RuntimeEdgeItemType[];
currentNode: RuntimeNodeItemType;
}) => {
const commonEdges: RuntimeEdgeItemType[] = [];
const recursiveEdges: RuntimeEdgeItemType[] = [];
const checkIsCircular = (edge: RuntimeEdgeItemType, visited: Set<string>): boolean => {
if (edge.source === currentNode.nodeId) {
return true; // 检测到环,并且环中包含当前节点
}
if (visited.has(edge.source)) {
return false; // 检测到环,但不包含当前节点(子节点成环)
}
visited.add(edge.source);
const nextEdges = allEdges.filter((item) => item.target === edge.source);
return nextEdges.some((nextEdge) => checkIsCircular(nextEdge, new Set(visited)));
};
sourceEdges.forEach((edge) => {
if (checkIsCircular(edge, new Set([currentNode.nodeId]))) {
recursiveEdges.push(edge);
} else {
commonEdges.push(edge);
}
});
return { commonEdges, recursiveEdges };
};
const runtimeNodeSourceEdge = filterWorkflowEdges(runtimeEdges).filter(
(item) => item.target === node.nodeId
);
// Entry
if (workflowEdges.length === 0) {
if (runtimeNodeSourceEdge.length === 0) {
return 'run';
}
// Classify edges
const { commonEdges, recursiveEdges } = splitEdges2WorkflowEdges({
edges: workflowEdges,
sourceEdges: runtimeNodeSourceEdge,
allEdges: runtimeEdges,
currentNode: node
});
// check skip
if (commonEdges.every((item) => item.status === 'skipped')) {
// check skip(其中一组边,全 skip
if (commonEdges.length > 0 && commonEdges.every((item) => item.status === 'skipped')) {
return 'skip';
}
if (recursiveEdges.length > 0 && recursiveEdges.every((item) => item.status === 'skipped')) {
return 'skip';
}
// check active
if (commonEdges.every((item) => item.status !== 'waiting')) {
// check active(有一类边,不全是 wait 即可运行)
if (commonEdges.length > 0 && commonEdges.every((item) => item.status !== 'waiting')) {
return 'run';
}
if (recursiveEdges.length > 0 && recursiveEdges.every((item) => item.status !== 'waiting')) {

View File

@@ -19,7 +19,7 @@ import {
FlowNodeInputTypeEnum,
FlowNodeTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import { replaceVariable } from '@fastgpt/global/common/string/tools';
import { getNanoid, replaceVariable } from '@fastgpt/global/common/string/tools';
import { getSystemTime } from '@fastgpt/global/common/time/timezone';
import { replaceEditorVariable } from '@fastgpt/global/core/workflow/utils';
@@ -434,6 +434,7 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
const formatResponseData: ChatHistoryItemResType = (() => {
if (!dispatchRes[DispatchNodeResponseKeyEnum.nodeResponse]) return undefined;
return {
id: getNanoid(),
nodeId: node.nodeId,
moduleName: node.name,
moduleType: node.flowNodeType,

View File

@@ -19,12 +19,12 @@ export const dispatchUpdateVariable = async (props: Props): Promise<Response> =>
const { params, variables, runtimeNodes, workflowStreamResponse, node } = props;
const { updateList } = params;
updateList.forEach((item) => {
const result = updateList.map((item) => {
const varNodeId = item.variable?.[0];
const varKey = item.variable?.[1];
if (!varNodeId || !varKey) {
return;
return null;
}
const value = (() => {
@@ -48,10 +48,11 @@ export const dispatchUpdateVariable = async (props: Props): Promise<Response> =>
}
})();
// Global variable
if (varNodeId === VARIABLE_NODE_ID) {
// update global variable
variables[varKey] = value;
} else {
// Other nodes
runtimeNodes
.find((node) => node.nodeId === varNodeId)
?.outputs?.find((output) => {
@@ -61,6 +62,8 @@ export const dispatchUpdateVariable = async (props: Props): Promise<Response> =>
}
});
}
return value;
});
workflowStreamResponse?.({
@@ -70,7 +73,7 @@ export const dispatchUpdateVariable = async (props: Props): Promise<Response> =>
return {
[DispatchNodeResponseKeyEnum.nodeResponse]: {
totalPoints: 0
updateVarResult: result
}
};
};

View File

@@ -565,6 +565,7 @@
"plugin output": "Plugin output value",
"search using reRank": "Result rearrangement",
"text output": "text output",
"update_var_result": "Variable update results (display multiple variable update results in order)",
"user_select_result": "User select result"
},
"retry": "Regenerate",

View File

@@ -10,6 +10,8 @@
"auto_renew_q": "订阅套餐会自动续费么?",
"change_package_a": "当前套餐价格大于新套餐时,无法立即切换,将会在当前套餐过期后以“续费”形式进行切换。\n当前套餐价格小于新套餐时系统会自动计算当前套餐剩余余额您可支付差价进行套餐切换。",
"change_package_q": "能否切换订阅套餐?",
"check_subscription_a": "账号-个人信息-套餐详情-使用情况。您可以查看所拥有套餐的生效和到期时间。当付费套餐到期后将自动切换免费版。",
"check_subscription_q": "在哪里查看已订阅的套餐?",
"dataset_compute_a": "1条知识库存储等于1条知识库索引。一条知识库数据可以包含1条或多条知识库索引。增强训练中1条数据会生成5条索引。",
"dataset_compute_q": "知识库存储怎么计算?",
"dataset_index_a": "不会。但知识库索引超出时,无法插入和更新知识库内容。",
@@ -18,19 +20,15 @@
"free_user_clean_q": "免费版数据会清除么?",
"package_overlay_a": "可以的。每次购买的资源包都是独立的在其有效期内将会叠加使用。AI积分会优先扣除最先过期的资源包。",
"package_overlay_q": "额外资源包可以叠加么?",
"switch_package_q": "是否切换订阅套餐?",
"switch_package_a": "套餐使用规则为优先使用更高级的套餐,因此,购买的新套餐若比当前套餐更高级,则新套餐立即生效:否则将继续使用当前套餐。",
"check_subscription_q": "在哪里查看已订阅套餐?",
"check_subscription_a": "账号-个人信息-套餐详情-使用情况。您可以查看所拥有套餐的生效和到期时间。当付费套餐到期后将自动切换免费版。"
"switch_package_q": "是否切换订阅套餐?"
},
"Folder": "文件夹",
"Login": "登录",
"is_using": "正在使用",
"Move": "移动",
"Name": "名称",
"Rename": "重命名",
"Resume": "恢复",
"free": "免费",
"Running": "运行中",
"UnKnow": "未知",
"Warning": "提示",
@@ -119,7 +117,6 @@
"Cancel": "取消",
"Choose": "选择",
"Close": "关闭",
"base_config": "基础配置",
"Config": "配置",
"Confirm": "确认",
"Confirm Create": "确认创建",
@@ -224,6 +221,7 @@
"Select Avatar": "点击选择头像",
"Select Failed": "选择头像异常"
},
"base_config": "基础配置",
"choosable": "可选",
"confirm": {
"Common Tip": "操作确认"
@@ -567,6 +565,7 @@
"plugin output": "插件输出值",
"search using reRank": "结果重排",
"text output": "文本输出",
"update_var_result": "变量更新结果(按顺序展示多个变量更新结果)",
"user_select_result": "用户选择结果"
},
"retry": "重新生成",
@@ -639,7 +638,8 @@
"success": "开始同步"
}
},
"training": {}
"training": {
}
},
"data": {
"Auxiliary Data": "辅助数据",
@@ -775,7 +775,6 @@
"test result tip": "根据知识库内容与测试文本的相似度进行排序,你可以根据测试结果调整对应的文本。\n注意测试记录中的数据可能已经被修改过点击某条测试数据后将展示最新的数据。"
},
"training": {
"tag": "排队情况",
"Agent queue": "QA 训练排队",
"Auto mode": "增强处理(实验)",
"Auto mode Tip": "通过子索引以及调用模型生成相关问题与摘要,来增加数据块的语义丰富度,更利于检索。需要消耗更多的存储空间和增加 AI 调用次数。",
@@ -785,7 +784,8 @@
"QA mode": "问答拆分",
"Vector queue": "索引排队",
"Waiting": "预计 5 分钟",
"Website Sync": "Web 站点同步"
"Website Sync": "Web 站点同步",
"tag": "排队情况"
},
"website": {
"Base Url": "根地址",
@@ -1078,6 +1078,7 @@
},
"extraction_results": "提取结果",
"field_name": "字段名",
"free": "免费",
"get_QR_failed": "获取二维码失败",
"get_app_failed": "获取应用失败",
"get_laf_failed": "获取Laf函数列表失败",
@@ -1097,6 +1098,7 @@
},
"invalid_variable": "无效变量",
"is_open": "是否开启",
"is_using": "正在使用",
"item_description": "字段描述",
"item_name": "字段名",
"key_repetition": "key 重复",
@@ -1125,14 +1127,14 @@
"notice": "请勿关闭页面",
"old_package_price": "旧套餐余额",
"other": "其他金额,请取整数",
"to_recharge": "余额不足,去充值",
"wechat": "请微信扫码支付: {{price}}元\n请勿关闭页面",
"yuan": "{{amount}}元",
"package_tip": {
"buy": "您购买的套餐等级低于当前套餐,该套餐将在当前套餐过期后生效。您可在账号—个人信息—套餐详情里,查看套餐使用情况。",
"renewal": "您正在续费套餐。您可在账号—个人信息—套餐详情里,查看套餐使用情况。",
"upgrade": "您购买的套餐等级高于当前套餐,该套餐将即刻生效,当前套餐将延后生效。您可在账号—个人信息—套餐详情里,查看套餐使用情况。"
}
},
"to_recharge": "余额不足,去充值",
"wechat": "请微信扫码支付: {{price}}元\n请勿关闭页面",
"yuan": "{{amount}}元"
},
"permission": {
"Collaborator": "协作者",
@@ -1217,8 +1219,8 @@
"standard": {
"AI Bonus Points": "AI 积分",
"Expired Time": "结束时间",
"due_date": "到期时间",
"Start Time": "开始时间",
"due_date": "到期时间",
"storage": "存储量",
"type": "类型"
},
@@ -1334,11 +1336,6 @@
"noBill": "无账单记录~",
"no_invoice": "暂无开票记录",
"subscription": {
"status": {
"expired": "已过期",
"active": "生效中",
"inactive": "待使用"
},
"AI points": "AI 积分",
"AI points click to read tip": "每次调用 AI 模型时,都会消耗一定的 AI 积分(类似于 token。点击可查看详细计算规则。",
"AI points usage": "AI 积分使用量",
@@ -1384,13 +1381,18 @@
"standardSubLevel": {
"custom": "自定义版",
"enterprise": "企业版",
"enterprise_desc": "适合中小企业在生产环境构建知识库应用",
"experience": "体验版",
"experience_desc": "可解锁 FastGPT 完整功能",
"free": "免费版",
"free desc": "每月均可免费使用基础功能,连续 30 天未登录系统,将会自动清除知识库",
"team": "团队版",
"experience_desc": "可解锁 FastGPT 完整功能",
"team_desc": "适合小团队构建知识库应用并提供对外服务",
"enterprise_desc": "适合中小企业在生产环境构建知识库应用"
"team_desc": "适合小团队构建知识库应用并提供对外服务"
},
"status": {
"active": "生效中",
"expired": "已过期",
"inactive": "待使用"
},
"token_compute": "点击查看在线 Tokens 计算器",
"type": {

View File

@@ -24,7 +24,8 @@ type sideTabItemType = {
moduleName: string;
runningTime?: number;
moduleType: string;
nodeId: string;
// nodeId:string; // abandon
id: string;
children: sideTabItemType[];
};
@@ -149,18 +150,28 @@ export const ResponseBox = React.memo(function ResponseBox({
}) {
const { t } = useTranslation();
const { isPc } = useSystem();
const flattedResponse = useMemo(() => flattenArray(response), [response]);
const flattedResponse = useMemo(
() =>
flattenArray(response).map((item) => ({
...item,
id: item.id ?? item.nodeId
})),
[response]
);
const [currentNodeId, setCurrentNodeId] = useState(
flattedResponse[0]?.nodeId ? flattedResponse[0].nodeId : ''
flattedResponse[0]?.id ?? flattedResponse[0]?.nodeId ?? ''
);
const activeModule = useMemo(
() => flattedResponse.find((item) => item.nodeId === currentNodeId) as ChatHistoryItemResType,
() => flattedResponse.find((item) => item.id === currentNodeId) as ChatHistoryItemResType,
[currentNodeId, flattedResponse]
);
const sideResponse: sideTabItemType[] = useMemo(() => {
const sliderResponseList: sideTabItemType[] = useMemo(() => {
return pretreatmentResponse(response);
}, [response]);
const {
isOpen: isOpenMobileModal,
onOpen: onOpenMobileModal,
@@ -174,7 +185,7 @@ export const ResponseBox = React.memo(function ResponseBox({
<Box flex={'2 0 0'} borderRight={'sm'} p={3}>
<Box overflow={'auto'} height={'100%'}>
<WholeResponseSideTab
response={sideResponse}
response={sliderResponseList}
value={currentNodeId}
onChange={setCurrentNodeId}
/>
@@ -192,7 +203,7 @@ export const ResponseBox = React.memo(function ResponseBox({
<Box h={'100%'} overflow={'auto'}>
{!isOpenMobileModal && (
<WholeResponseSideTab
response={sideResponse}
response={sliderResponseList}
value={currentNodeId}
onChange={(item: string) => {
setCurrentNodeId(item);
@@ -442,11 +453,11 @@ export const WholeResponseContent = ({
/>
{/* code */}
<>
<Row label={t('workflow:response.Custom inputs')} value={activeModule?.customInputs} />
<Row
label={t('workflow:response.Custom outputs')}
value={activeModule?.customOutputs}
/>
<Row label={t('workflow:response.Custom inputs')} value={activeModule?.customInputs} />
<Row label={t('workflow:response.Code log')} value={activeModule?.codeLog} />
</>
@@ -491,6 +502,12 @@ export const WholeResponseContent = ({
label={t('common:core.chat.response.user_select_result')}
value={activeModule?.userSelectResult}
/>
{/* update var */}
<Row
label={t('common:core.chat.response.update_var_result')}
value={activeModule?.updateVarResult}
/>
</Box>
)}
</>
@@ -512,7 +529,7 @@ const WholeResponseSideTab = ({
<>
{response.map((item) => (
<Box
key={item.nodeId}
key={item.id}
bg={isMobile ? 'myGray.100' : ''}
m={isMobile ? 3 : 0}
borderRadius={'md'}
@@ -532,7 +549,7 @@ const AccordionSideTabItem = ({
index
}: {
sideBarItem: sideTabItemType;
onChange: (nodeId: string) => void;
onChange: (id: string) => void;
value: string;
index: number;
}) => {
@@ -565,7 +582,7 @@ const AccordionSideTabItem = ({
{sideBarItem.children.map((item) => (
<SideTabItem
value={value}
key={item.nodeId}
key={item.id}
sideBarItem={item}
onChange={onChange}
index={index + 1}
@@ -585,7 +602,7 @@ const NormalSideTabItem = ({
children
}: {
sideBarItem: sideTabItemType;
onChange: (nodeId: string) => void;
onChange: (id: string) => void;
value: string;
index: number;
children?: React.ReactNode;
@@ -596,9 +613,9 @@ const NormalSideTabItem = ({
<Flex
alignItems={'center'}
onClick={() => {
onChange(sideBarItem.nodeId);
onChange(sideBarItem.id);
}}
background={value === sideBarItem.nodeId ? 'myGray.100' : ''}
background={value === sideBarItem.id ? 'myGray.100' : ''}
_hover={{ background: 'myGray.100' }}
p={2}
width={'100%'}
@@ -647,7 +664,7 @@ const SideTabItem = ({
index
}: {
sideBarItem: sideTabItemType;
onChange: (nodeId: string) => void;
onChange: (id: string) => void;
value: string;
index: number;
}) => {
@@ -668,6 +685,7 @@ const SideTabItem = ({
);
};
/* Format response data to slider data */
function pretreatmentResponse(res: ChatHistoryItemResType[]): sideTabItemType[] {
return res.map((item) => {
let children: sideTabItemType[] = [];
@@ -681,12 +699,13 @@ function pretreatmentResponse(res: ChatHistoryItemResType[]): sideTabItemType[]
moduleName: item.moduleName,
runningTime: item.runningTime,
moduleType: item.moduleType,
nodeId: item.nodeId,
id: item.id ?? item.nodeId,
children
};
});
}
/* Flat response */
function flattenArray(arr: ChatHistoryItemResType[]) {
const result: ChatHistoryItemResType[] = [];

View File

@@ -56,9 +56,9 @@ const FlowController = React.memo(function FlowController() {
<>
<MiniMap
style={{
height: 98,
width: 184,
marginBottom: 72,
height: 92,
width: 150,
marginBottom: 62,
borderRadius: '10px',
boxShadow: '0px 0px 1px rgba(19, 51, 107, 0.10), 0px 4px 10px rgba(19, 51, 107, 0.10)'
}}
@@ -68,7 +68,7 @@ const FlowController = React.memo(function FlowController() {
position={'bottom-right'}
style={{
display: 'flex',
marginBottom: 24,
marginBottom: 16,
padding: '5px 8px',
background: 'white',
borderRadius: '6px',

View File

@@ -375,15 +375,21 @@ const ConditionSelect = ({
return [];
}, [valueType]);
const filterQuiredConditionList = useMemo(() => {
if (required) {
return conditionList.filter(
(item) =>
item.value !== VariableConditionEnum.isEmpty &&
item.value !== VariableConditionEnum.isNotEmpty
);
}
return conditionList;
}, [conditionList, required]);
const list = (() => {
if (required) {
return conditionList.filter(
(item) =>
item.value !== VariableConditionEnum.isEmpty &&
item.value !== VariableConditionEnum.isNotEmpty
);
}
return conditionList;
})();
return list.map((item) => ({
...item,
label: t(item.label)
}));
}, [conditionList, required, t]);
return (
<MySelect

View File

@@ -87,7 +87,9 @@ export const getEditorVariables = ({
appDetail: AppDetailType;
t: TFunction;
}) => {
const currentNode = nodeList.find((node) => node.nodeId === nodeId)!;
const currentNode = nodeList.find((node) => node.nodeId === nodeId);
if (!currentNode) return [];
const nodeVariables = currentNode.inputs
.filter((input) => input.canEdit)
.map((item) => ({