diff --git a/packages/global/core/chat/type.d.ts b/packages/global/core/chat/type.d.ts index 10f5951e216e..67b1dadc9ec1 100644 --- a/packages/global/core/chat/type.d.ts +++ b/packages/global/core/chat/type.d.ts @@ -151,6 +151,7 @@ export type ChatHistoryItemType = HistoryItemType & { /* ------- response data ------------ */ export type ChatHistoryItemResType = DispatchNodeResponseType & { nodeId: string; + id: string; moduleType: FlowNodeTypeEnum; moduleName: string; }; diff --git a/packages/global/core/workflow/runtime/type.d.ts b/packages/global/core/workflow/runtime/type.d.ts index 3329f67eb5d6..a7f0ee97732c 100644 --- a/packages/global/core/workflow/runtime/type.d.ts +++ b/packages/global/core/workflow/runtime/type.d.ts @@ -159,6 +159,9 @@ export type DispatchNodeResponseType = { // user select userSelectResult?: string; + + // update var + updateVarResult?: any[]; }; export type DispatchNodeResultType = { diff --git a/packages/global/core/workflow/runtime/utils.ts b/packages/global/core/workflow/runtime/utils.ts index 534ad7fcdb21..c8e2b66e4e8f 100644 --- a/packages/global/core/workflow/runtime/utils.ts +++ b/packages/global/core/workflow/runtime/utils.ts @@ -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): 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')) { diff --git a/packages/service/core/workflow/dispatch/index.ts b/packages/service/core/workflow/dispatch/index.ts index 564e2d94c035..6c123699a0d5 100644 --- a/packages/service/core/workflow/dispatch/index.ts +++ b/packages/service/core/workflow/dispatch/index.ts @@ -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 { if (!dispatchRes[DispatchNodeResponseKeyEnum.nodeResponse]) return undefined; return { + id: getNanoid(), nodeId: node.nodeId, moduleName: node.name, moduleType: node.flowNodeType, diff --git a/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts b/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts index bee728281efe..bcb028a33f9b 100644 --- a/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts +++ b/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts @@ -19,12 +19,12 @@ export const dispatchUpdateVariable = async (props: Props): Promise => 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 => } })(); + // 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 => } }); } + + return value; }); workflowStreamResponse?.({ @@ -70,7 +73,7 @@ export const dispatchUpdateVariable = async (props: Props): Promise => return { [DispatchNodeResponseKeyEnum.nodeResponse]: { - totalPoints: 0 + updateVarResult: result } }; }; diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index 4c9889e643d1..174181836a99 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -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", diff --git a/packages/web/i18n/zh/common.json b/packages/web/i18n/zh/common.json index aeb8ecdc29ad..5708d0869ffe 100644 --- a/packages/web/i18n/zh/common.json +++ b/packages/web/i18n/zh/common.json @@ -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": { diff --git a/projects/app/src/components/core/chat/components/WholeResponseModal.tsx b/projects/app/src/components/core/chat/components/WholeResponseModal.tsx index 57a249c922e6..db9744105d79 100644 --- a/projects/app/src/components/core/chat/components/WholeResponseModal.tsx +++ b/projects/app/src/components/core/chat/components/WholeResponseModal.tsx @@ -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({ @@ -192,7 +203,7 @@ export const ResponseBox = React.memo(function ResponseBox({ {!isOpenMobileModal && ( { setCurrentNodeId(item); @@ -442,11 +453,11 @@ export const WholeResponseContent = ({ /> {/* code */} <> + - @@ -491,6 +502,12 @@ export const WholeResponseContent = ({ label={t('common:core.chat.response.user_select_result')} value={activeModule?.userSelectResult} /> + + {/* update var */} + )} @@ -512,7 +529,7 @@ const WholeResponseSideTab = ({ <> {response.map((item) => ( void; + onChange: (id: string) => void; value: string; index: number; }) => { @@ -565,7 +582,7 @@ const AccordionSideTabItem = ({ {sideBarItem.children.map((item) => ( void; + onChange: (id: string) => void; value: string; index: number; children?: React.ReactNode; @@ -596,9 +613,9 @@ const NormalSideTabItem = ({ { - 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[] = []; diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/FlowController.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/FlowController.tsx index aab7ebcb88b6..7485c33de0cc 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/FlowController.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/FlowController.tsx @@ -56,9 +56,9 @@ const FlowController = React.memo(function FlowController() { <> { - 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 ( { - 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) => ({