diff --git a/packages/global/core/app/plugin/type.d.ts b/packages/global/core/app/plugin/type.d.ts index 807e15a4d22e..9fd752f5560d 100644 --- a/packages/global/core/app/plugin/type.d.ts +++ b/packages/global/core/app/plugin/type.d.ts @@ -33,6 +33,9 @@ export type SystemPluginTemplateItemType = WorkflowTemplateType & { versionList?: { value: string; description?: string; + enabled?: boolean; + selectedVersionId?: string; + storedVersions?: string[]; inputs: FlowNodeInputItemType[]; outputs: FlowNodeOutputItemType[]; diff --git a/packages/global/core/workflow/type/node.d.ts b/packages/global/core/workflow/type/node.d.ts index 69a83353cd48..857690ce31c9 100644 --- a/packages/global/core/workflow/type/node.d.ts +++ b/packages/global/core/workflow/type/node.d.ts @@ -44,6 +44,10 @@ export type NodeToolConfigType = { toolId: string; name: string; description: string; + enabled: boolean; + selectedVersionId: string; + storedVersions: Array; + type: 'deprecated' | 'invalid'; }[]; }; }; diff --git a/packages/service/core/app/plugin/controller.ts b/packages/service/core/app/plugin/controller.ts index 387985927fe6..b17f79740bd2 100644 --- a/packages/service/core/app/plugin/controller.ts +++ b/packages/service/core/app/plugin/controller.ts @@ -159,11 +159,13 @@ export const getSystemPluginByIdAndVersionId = async ( export async function getChildAppPreviewNode({ appId, versionId, - lang = 'en' + lang = 'en', + toolConfig }: { appId: string; versionId?: string; lang?: localeType; + toolConfig?: NodeToolConfigType; }): Promise { const { source, pluginId } = splitCombinePluginId(appId); @@ -305,13 +307,24 @@ export async function getChildAppPreviewNode({ ? { systemToolSet: { toolId: app.id, - toolList: children - .filter((item) => item.isActive !== false) - .map((item) => ({ - toolId: item.id, - name: parseI18nString(item.name, lang), - description: parseI18nString(item.intro, lang) - })) + toolList: + toolConfig?.systemToolSet?.toolList.filter( + (item) => item.type !== 'deprecated' + ) ?? + children + .filter((item) => item.isActive !== false) + .map((item) => { + const storedVersions = item.versionList?.map((v) => v.value) ?? []; + return { + toolId: item.id, + name: parseI18nString(item.name, lang), + description: parseI18nString(item.intro, lang), + enabled: item.isActive ?? true, + selectedVersionId: '', + storedVersions: storedVersions, + type: 'invalid' + }; + }) } } : { systemTool: { toolId: app.id } }) diff --git a/packages/service/core/app/utils.ts b/packages/service/core/app/utils.ts index 70ffaea5c198..7c61e7c0e40b 100644 --- a/packages/service/core/app/utils.ts +++ b/packages/service/core/app/utils.ts @@ -57,7 +57,8 @@ export async function rewriteAppWorkflowToDetail({ getChildAppPreviewNode({ appId: node.pluginId, versionId: node.version, - lang + lang, + toolConfig: node.toolConfig }), ...(source === PluginSourceEnum.personal ? [ diff --git a/packages/service/core/workflow/utils.ts b/packages/service/core/workflow/utils.ts index 11e70791ad67..ef85edae02ef 100644 --- a/packages/service/core/workflow/utils.ts +++ b/packages/service/core/workflow/utils.ts @@ -49,10 +49,11 @@ export async function getSystemToolRunTimeNodeFromSystemToolset({ const nodes = await Promise.all( children.map(async (child, index) => { const toolListItem = toolSetNode.toolConfig?.systemToolSet?.toolList.find( - (item) => item.toolId === child.id + (item) => item.toolId === child.id && item.type !== 'deprecated' ); - const tool = await getSystemPluginByIdAndVersionId(child.id); + const versionId = toolListItem?.selectedVersionId; + const tool = await getSystemPluginByIdAndVersionId(child.id, versionId); const inputs = tool.inputs ?? []; if (toolsetInputConfig?.value) { diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index 8e682bfcec9f..5f667d244e97 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -30,6 +30,7 @@ "ai_point_price": "Billing", "ai_settings": "AI Configuration", "all_apps": "All Applications", + "all_tools_keep_latest": "All tools are kept up to date", "app.Version name": "Version Name", "app.error.publish_unExist_app": "Release failed, please check whether the tool call is normal", "app.error.unExist_app": "Some components are missing, please delete them", @@ -69,15 +70,18 @@ "cron.every_month": "Run Monthly", "cron.every_week": "Run Weekly", "cron.interval": "Run at Intervals", + "current_by_global_setting_all_tools_keep_latest": "Currently controlled by global settings, all tools remain latest version", "dataset": "dataset", "dataset_search_tool_description": "Call the \"Semantic Search\" and \"Full-text Search\" capabilities to find reference content that may be related to the problem from the \"Knowledge Base\". \nPrioritize calling this tool to assist in answering user questions.", "day": "Day", "deleted": "App deleted", + "disabled": "Disabled", "document_quote": "Document Reference", "document_quote_tip": "Usually used to accept user-uploaded document content (requires document parsing), and can also be used to reference other string data.", "document_upload": "Document Upload", "edit_app": "Application details", "edit_info": "Edit", + "enable_tool": "Enable Tools", "execute_time": "Execution Time", "export_config_successful": "Configuration copied, some sensitive information automatically filtered. Please check for any remaining sensitive data.", "export_configs": "Export", @@ -86,6 +90,7 @@ "file_recover": "File will overwrite current content", "file_upload": "File Upload", "file_upload_tip": "Once enabled, documents/images can be uploaded. Documents are retained for 7 days, images for 15 days. Using this feature may incur additional costs. To ensure a good experience, please choose an AI model with a larger context length when using this feature.", + "global_control": "Global control", "go_to_chat": "Go to Conversation", "go_to_run": "Go to Execution", "image_upload": "Image Upload", @@ -100,6 +105,7 @@ "interval.4_hours": "Every 4 Hours", "interval.6_hours": "Every 6 Hours", "interval.per_hour": "Every Hour", + "invalid": "Have expired", "invalid_json_format": "JSON format error", "keep_the_latest": "Keep the latest", "llm_not_support_vision": "This model does not support image recognition", @@ -178,7 +184,11 @@ "month.unit": "Day", "move.hint": "After moving, the selected app/folder will inherit the permission settings for the new folder.", "move_app": "Move Application", + "new_tool": "There are new tools", + "new_version": "There is a new version", + "newest": "Newest", "no_mcp_tools_list": "No data yet, the MCP address needs to be parsed first", + "no_version_info": "No version information yet", "node_not_intro": "This node is not introduced", "not_json_file": "Please select a JSON file", "not_the_newest": "Not the latest", diff --git a/packages/web/i18n/zh-CN/app.json b/packages/web/i18n/zh-CN/app.json index cea7ed34d281..22a54a6694e7 100644 --- a/packages/web/i18n/zh-CN/app.json +++ b/packages/web/i18n/zh-CN/app.json @@ -30,6 +30,7 @@ "ai_point_price": "AI积分计费", "ai_settings": "AI 配置", "all_apps": "全部应用", + "all_tools_keep_latest": "所有工具保持最新版本", "app.Version name": "版本名称", "app.error.publish_unExist_app": "发布失败,请检查工具调用是否正常", "app.error.unExist_app": "部分组件缺失,请删除", @@ -69,15 +70,18 @@ "cron.every_month": "每月执行", "cron.every_week": "每周执行", "cron.interval": "间隔执行", + "current_by_global_setting_all_tools_keep_latest": "当前由全局设置控制,所有工具保持最新版本", "dataset": "知识库", "dataset_search_tool_description": "调用“语义检索”和“全文检索”能力,从“知识库”中查找可能与问题相关的参考内容。优先调用该工具来辅助回答用户的问题。", "day": "日", "deleted": "应用已删除", + "disabled": "已禁用", "document_quote": "文档引用", "document_quote_tip": "通常用于接受用户上传的文档内容(这需要文档解析),也可以用于引用其他字符串数据。", "document_upload": "文档上传", "edit_app": "应用详情", "edit_info": "编辑信息", + "enable_tool": "启用工具", "execute_time": "执行时间", "export_config_successful": "已复制配置,自动过滤部分敏感信息,请注意检查是否仍有敏感数据", "export_configs": "导出配置", @@ -86,6 +90,7 @@ "file_recover": "文件将覆盖当前内容", "file_upload": "文件上传", "file_upload_tip": "开启后,可以上传文档/图片。文档保留7天,图片保留15天。使用该功能可能产生较多额外费用。为保证使用体验,使用该功能时,请选择上下文长度较大的AI模型。", + "global_control": "全局控制", "go_to_chat": "去对话", "go_to_run": "去运行", "image_upload": "图片上传", @@ -100,6 +105,7 @@ "interval.4_hours": "每4小时", "interval.6_hours": "每6小时", "interval.per_hour": "每小时", + "invalid": "已失效", "invalid_json_format": "JSON 格式错误", "keep_the_latest": "保持最新版本", "llm_not_support_vision": "该模型不支持图片识别", @@ -187,7 +193,11 @@ "month.unit": "号", "move.hint": "移动后,所选应用/文件夹将继承新文件夹的权限设置。", "move_app": "移动应用", + "new_tool": "有新工具", + "new_version": "有新版本", + "newest": "最新", "no_mcp_tools_list": "暂无数据,需先解析 MCP 地址", + "no_version_info": "暂无版本信息", "node_not_intro": "这个节点没有介绍", "not_json_file": "请选择JSON文件", "not_the_newest": "非最新版", diff --git a/packages/web/i18n/zh-Hant/app.json b/packages/web/i18n/zh-Hant/app.json index 25b9a1c8b032..1e08048e521f 100644 --- a/packages/web/i18n/zh-Hant/app.json +++ b/packages/web/i18n/zh-Hant/app.json @@ -30,6 +30,7 @@ "ai_point_price": "AI 積分計費", "ai_settings": "AI 設定", "all_apps": "所有應用程式", + "all_tools_keep_latest": "所有工具保持最新版本", "app.Version name": "版本名稱", "app.error.publish_unExist_app": "發布失敗,請檢查工具呼叫是否正常", "app.error.unExist_app": "部分元件遺失,請刪除", @@ -68,15 +69,18 @@ "cron.every_month": "每月執行", "cron.every_week": "每週執行", "cron.interval": "間隔執行", + "current_by_global_setting_all_tools_keep_latest": "當前由全局設置控制,所有工具保持最新版本", "dataset": "知識庫", "dataset_search_tool_description": "呼叫「語意搜尋」和「全文搜尋」功能,從「知識庫」中尋找可能與問題相關的參考內容。優先呼叫這個工具來協助回答使用者的問題。", "day": "日", "deleted": "應用已刪除", + "disabled": "已禁用", "document_quote": "文件引用", "document_quote_tip": "通常用於接受使用者上傳的文件內容(這需要文件解析),也可以用於引用其他字串資料。", "document_upload": "文件上傳", "edit_app": "應用詳情", "edit_info": "編輯資訊", + "enable_tool": "啟用工具", "execute_time": "執行時間", "export_config_successful": "已複製設定,自動過濾部分敏感資訊,請注意檢查是否仍有敏感資料", "export_configs": "匯出設定", @@ -85,6 +89,7 @@ "file_recover": "檔案將會覆蓋目前內容", "file_upload": "檔案上傳", "file_upload_tip": "開啟後,可以上傳文件/圖片。文件保留 7 天,圖片保留 15 天。使用這個功能可能產生較多額外費用。為了確保使用體驗,使用這個功能時,請選擇上下文長度較大的 AI 模型。", + "global_control": "全局控制", "go_to_chat": "前往對話", "go_to_run": "前往執行", "image_upload": "圖片上傳", @@ -99,6 +104,7 @@ "interval.4_hours": "每 4 小時", "interval.6_hours": "每 6 小時", "interval.per_hour": "每小時", + "invalid": "已失效", "invalid_json_format": "JSON 格式錯誤", "keep_the_latest": "保持最新版本", "llm_not_support_vision": "這個模型不支援圖片辨識", @@ -177,7 +183,11 @@ "month.unit": "號", "move.hint": "移動後,所選應用/文件夾將繼承新文件夾的權限設置。", "move_app": "移動應用程式", + "new_tool": "有新工具", + "new_version": "有新版本", + "newest": "最新", "no_mcp_tools_list": "暫無數據,需先解析 MCP 地址", + "no_version_info": "暫無版本信息", "node_not_intro": "這個節點沒有介紹", "not_json_file": "請選擇 JSON 檔案", "not_the_newest": "非最新版", diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeToolSet.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeToolSet.tsx index c8b9a9d8199d..6928ca8eb117 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeToolSet.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeToolSet.tsx @@ -1,62 +1,746 @@ -import { type FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node'; -import React from 'react'; +import React, { useState, useMemo, useEffect, useCallback } from 'react'; +import { useTranslation } from 'next-i18next'; +import { useContextSelector } from 'use-context-selector'; +import { + Box, + Flex, + Popover, + PopoverTrigger, + PopoverContent, + PopoverBody, + Switch, + Text, + Checkbox +} from '@chakra-ui/react'; import { type NodeProps } from 'reactflow'; +import type { NodeTemplateListItemType } from '@fastgpt/global/core/workflow/type/node'; +import { type FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; +import { getToolVersionList, getSystemPlugTemplates } from '@/web/core/app/api/plugin'; +import { WorkflowContext } from '../../context'; import NodeCard from './render/NodeCard'; import IOTitle from '../components/IOTitle'; import Container from '../components/Container'; -import { useTranslation } from 'next-i18next'; -import { Box, Flex } from '@chakra-ui/react'; const NodeToolSet = ({ data, selected }: NodeProps) => { const { t } = useTranslation(); + const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); const { toolConfig } = data; - const toolList = toolConfig?.mcpToolSet?.toolList ?? toolConfig?.systemToolSet?.toolList ?? []; + // get tool list from toolConfig + const toolList = useMemo( + () => toolConfig?.systemToolSet?.toolList ?? [], + [toolConfig?.systemToolSet?.toolList] + ); + // plugin data + const [pluginData, setPluginData] = useState<{ + toolList: any[]; + toolVersions: Record; + }>({ toolList: [], toolVersions: {} }); + + // single tool state + const [toolState, setToolState] = useState({ + hoveredToolIndex: null as number | null, + openPopoverIndex: null as number | null, + allToolsKeepLatest: false + }); + + // all tools state + const [toolStates, setToolStates] = useState<{ + enabled: Record; + versions: Record; + newToolIds: Set; + updatedVersionToolIds: Set; + }>({ + enabled: {}, + versions: {}, + newToolIds: new Set(), + updatedVersionToolIds: new Set() + }); + + // initialize tool states + useEffect(() => { + const newStates = toolList.reduce( + (acc, tool) => { + acc.enabled[tool.toolId] = tool.enabled ?? true; + acc.versions[tool.toolId] = tool.selectedVersionId ?? ''; + return acc; + }, + { enabled: {} as Record, versions: {} as Record } + ); + + setToolStates((prev) => ({ + ...prev, + enabled: newStates.enabled, + versions: newStates.versions + })); + }, [toolList]); + + // update tool config in node data + const updateToolConfigInNodeData = useCallback( + ( + toolId: string, + updates: { + enabled?: boolean; + selectedVersionId?: string; + storedVersions?: string[]; + type?: 'deprecated' | 'invalid'; + name?: string; + }, + isNewTool = false + ) => { + if (!toolConfig?.systemToolSet) return; + + const updatedToolList = isNewTool + ? [ + ...toolConfig.systemToolSet.toolList, + { + toolId, + name: updates.name || '', + enabled: updates.enabled ?? true, + selectedVersionId: updates.selectedVersionId ?? '', + storedVersions: updates.storedVersions ?? [], + ...(updates.type && { type: updates.type }) + } + ] + : toolConfig.systemToolSet.toolList.map((tool) => + tool.toolId === toolId ? { ...tool, ...updates } : tool + ); + + onChangeNode({ + nodeId: data.nodeId, + type: 'attr', + key: 'toolConfig', + value: { + ...toolConfig, + systemToolSet: { + ...toolConfig.systemToolSet, + toolList: updatedToolList + } + } + }); + }, + [toolConfig, data.nodeId, onChangeNode] + ); + + const updateToolStoredVersions = useCallback( + (toolId: string, storedVersions: string[]) => { + updateToolConfigInNodeData(toolId, { storedVersions: storedVersions }); + }, + [updateToolConfigInNodeData] + ); + + const toolOperations = useMemo( + () => ({ + setVersion: (toolId: string, versionId: string) => { + setToolStates((prev) => ({ + ...prev, + versions: { ...prev.versions, [toolId]: versionId } + })); + updateToolConfigInNodeData(toolId, { selectedVersionId: versionId }); + + const versionList = pluginData.toolVersions[toolId]; + if (versionList?.length) { + const storedVersions = versionList.map((v: any) => v.value || v._id); + updateToolStoredVersions(toolId, storedVersions); + } + }, + + setEnabled: (toolId: string, enabled: boolean) => { + setToolStates((prev) => ({ + ...prev, + enabled: { ...prev.enabled, [toolId]: enabled } + })); + updateToolConfigInNodeData(toolId, { enabled }); + }, + + toggleAllKeepLatest: (checked: boolean) => { + setToolState((prev) => ({ ...prev, allToolsKeepLatest: checked })); + + if (checked) { + const newVersions = toolList.reduce( + (acc, tool) => { + acc[tool.toolId] = ''; + updateToolConfigInNodeData(tool.toolId, { selectedVersionId: '' }); + return acc; + }, + {} as Record + ); + + setToolStates((prev) => ({ ...prev, versions: newVersions })); + } + }, + + getVersionInfo: (toolId: string, tool?: any) => { + if (toolState.allToolsKeepLatest) return ''; + return tool?.selectedVersionId ?? toolStates.versions[toolId] ?? ''; + } + }), + [ + toolState.allToolsKeepLatest, + toolStates, + updateToolConfigInNodeData, + updateToolStoredVersions, + pluginData.toolVersions, + toolList + ] + ); + // fetch tool data + useEffect(() => { + const fetchToolData = async () => { + const toolSetId = toolConfig?.systemToolSet?.toolId; + if (!toolSetId) return; + + try { + const toolList = await getSystemPlugTemplates({ + searchKey: '', + parentId: toolSetId + }); + + const versionResults = await Promise.allSettled( + toolList.map(async (tool: NodeTemplateListItemType) => { + const versionResponse = await getToolVersionList({ + pluginId: tool.id, + pageSize: 100, + pageNum: 1 + }); + return { [tool.id]: versionResponse || [] }; + }) + ); + + const mergedVersions = versionResults.reduce( + (acc, result) => { + if (result.status === 'fulfilled') { + return { ...acc, ...result.value }; + } + return acc; + }, + {} as Record + ); + + setPluginData({ toolList, toolVersions: mergedVersions }); + } catch (error) { + console.error('Failed to fetch tool data:', error); + setPluginData({ toolList: [], toolVersions: {} }); + } + }; + + fetchToolData(); + }, [toolConfig?.systemToolSet?.toolId]); + + // check version updates + const checkVersionUpdates = useCallback( + (tool: any) => { + const versionList = pluginData.toolVersions[tool.toolId]; + + const versionArray = versionList.list; + + if (!versionArray.length) { + return { hasNewVersion: false }; + } + + const currentVersions = versionArray.map((v: any) => v.value || v._id); + const storedVersions = tool.storedVersions || []; + + const storedVersionsSet = new Set(storedVersions); + const hasNewVersion = currentVersions.some( + (version: string) => !storedVersionsSet.has(version) + ); + + return { hasNewVersion }; + }, + [pluginData.toolVersions] + ); + + // all tools including new tools information + const toolListWithStatus = useMemo(() => { + if (!pluginData.toolList.length) { + return toolList.map((tool) => ({ + ...tool, + isDeprecated: false, + isNew: false, + hasNewVersion: false + })); + } + + const pluginToolMap = new Map( + pluginData.toolList.map((tool: NodeTemplateListItemType) => [tool.id, tool]) + ); + + const existingToolsWithStatus = toolList.map((tool) => { + const isDeprecated = !pluginToolMap.has(tool.toolId); + const hasNewVersion = + toolStates.updatedVersionToolIds.has(tool.toolId) || + checkVersionUpdates(tool).hasNewVersion; + const isNew = toolStates.newToolIds.has(tool.toolId); + + return { + ...tool, + isDeprecated, + isNew, + hasNewVersion + }; + }); + + const newTools = pluginData.toolList + .filter( + (latestTool: NodeTemplateListItemType) => + !toolList.some((tool) => tool.toolId === latestTool.id) + ) + .map((newTool: NodeTemplateListItemType) => ({ + toolId: newTool.id, + name: newTool.name, + description: newTool.intro, + enabled: true, + selectedVersionId: '', + isDeprecated: false, + isNew: true, + hasNewVersion: false + })); + + // deprecated tools at the bottom + const allTools = [...existingToolsWithStatus, ...newTools]; + const activeTools = allTools.filter((tool) => !tool.isDeprecated); + const deprecatedTools = allTools.filter((tool) => tool.isDeprecated); + + return [...activeTools, ...deprecatedTools]; + }, [ + toolList, + pluginData, + checkVersionUpdates, + toolStates.newToolIds, + toolStates.updatedVersionToolIds + ]); + + useEffect(() => { + if (!pluginData.toolList.length || !toolConfig?.systemToolSet) return; + + const pluginToolMap = new Map( + pluginData.toolList.map((tool: NodeTemplateListItemType) => [tool.id, tool]) + ); + + const newToolIdsSet = new Set(); + const updatedVersionToolIdsSet = new Set(); + + // check and add new tools + pluginData.toolList.forEach((pluginTool: NodeTemplateListItemType) => { + const existingTool = toolConfig.systemToolSet?.toolList.find( + (tool) => tool.toolId === pluginTool.id + ); + + if (!existingTool) { + newToolIdsSet.add(pluginTool.id); + + const versionList = pluginData.toolVersions[pluginTool.id]; + + const latestVersions = versionList.list.map( + (v: { _id: string; versionName: string }) => v._id + ) as string[]; + + updateToolConfigInNodeData( + pluginTool.id, + { + name: pluginTool.name, + enabled: true, + selectedVersionId: '', + storedVersions: latestVersions, + type: 'invalid' as const + }, + true + ); + } else { + // check version updates + const versionList = pluginData.toolVersions[pluginTool.id]; + + const latestVersions = versionList.list.map( + (v: { _id: string; versionName: string }) => v._id + ) as string[]; + + const currentStoredVersions = existingTool.storedVersions || []; + const versionsChanged = + latestVersions.length !== currentStoredVersions.length || + latestVersions.some((v) => !currentStoredVersions.includes(v)) || + currentStoredVersions.some((v) => !latestVersions.includes(v)); + + if (versionsChanged) { + updatedVersionToolIdsSet.add(pluginTool.id); + updateToolConfigInNodeData(pluginTool.id, { storedVersions: latestVersions }); + } + } + }); + + toolList.forEach((tool) => { + const isDeprecated = !pluginToolMap.has(tool.toolId); + + if (isDeprecated && tool.type !== 'deprecated') { + updateToolConfigInNodeData(tool.toolId, { type: 'deprecated' }); + } + }); + + if (newToolIdsSet.size > 0 || updatedVersionToolIdsSet.size > 0) { + setToolStates((prev) => ({ + ...prev, + newToolIds: newToolIdsSet, + updatedVersionToolIds: updatedVersionToolIdsSet + })); + } + }, [pluginData.toolList, pluginData.toolVersions, toolConfig, updateToolConfigInNodeData]); + + // check tool set status + const toolSetStatus = useMemo(() => { + const hasData = pluginData.toolList.length > 0 && toolList.length > 0; + + if (!hasData) { + return { hasUpdates: false, hasDeprecated: false }; + } + + const hasUpdates = toolListWithStatus.some( + (tool) => tool.hasNewVersion || tool.isDeprecated || tool.isNew + ); + + const pluginToolMap = new Map( + pluginData.toolList.map((tool: NodeTemplateListItemType) => [tool.id, tool]) + ); + const hasDeprecated = toolList.some((tool) => !pluginToolMap.has(tool.toolId)); + + return { hasUpdates, hasDeprecated }; + }, [toolListWithStatus, toolList, pluginData.toolList]); + + // update tool set version status + useEffect(() => { + const { hasUpdates } = toolSetStatus; + const shouldUpdate = + (hasUpdates && data.isLatestVersion !== false) || + (!hasUpdates && data.isLatestVersion !== true); + + if (shouldUpdate) { + onChangeNode({ + nodeId: data.nodeId, + type: 'attr', + key: 'isLatestVersion', + value: !hasUpdates + }); + } + }, [toolSetStatus, data.isLatestVersion, data.nodeId, onChangeNode]); return ( - + - - {toolList?.map((tool, index) => ( - - - {index + 1 < 10 ? `0${index + 1}` : index + 1} - - - - {tool.name} + + {toolListWithStatus?.map((toolWithStatus, index) => { + const { + toolId, + isDeprecated, + isNew, + hasNewVersion: hasNewVersionAvailable + } = toolWithStatus; + + const isEnabled = toolWithStatus.enabled ?? toolStates.enabled[toolId] ?? true; + + return ( + setToolState((prev) => ({ ...prev, hoveredToolIndex: index }))} + onMouseLeave={() => setToolState((prev) => ({ ...prev, hoveredToolIndex: null }))} + position="relative" + > + + {(index + 1).toString().padStart(2, '0')} - - {tool.description || t('app:tools_no_description')} + + + + {toolWithStatus.name} + + + + + {toolWithStatus.description || t('app:tools_no_description')} + - - - - ))} + + + {/* version info expand button - show when hover or popover open, only show for system tool set */} + {(toolState.hoveredToolIndex === index || toolState.openPopoverIndex === index) && + toolConfig?.systemToolSet && ( + setToolState((prev) => ({ ...prev, openPopoverIndex: index }))} + onClose={() => setToolState((prev) => ({ ...prev, openPopoverIndex: null }))} + > + + + + + + + + + toolOperations.setEnabled(toolId, enabled) + } + onVersionChange={(versionId) => + toolOperations.setVersion(toolId, versionId) + } + allToolsKeepLatest={toolState.allToolsKeepLatest} + /> + + + + )} + + ); + })} + + toolOperations.toggleAllKeepLatest(e.target.checked)} + > + {t('app:all_tools_keep_latest')} + + ); }; +const ToolVersionInfo = ({ + toolId, + isEnabled, + selectedVersion: initialSelectedVersion, + onEnabledChange, + onVersionChange, + allToolsKeepLatest +}: { + toolId: string; + isEnabled: boolean; + selectedVersion: string; + onEnabledChange: (enabled: boolean) => void; + onVersionChange: (versionId: string) => void; + allToolsKeepLatest: boolean; +}) => { + const { t } = useTranslation(); + const [selectedVersion, setSelectedVersion] = useState(initialSelectedVersion); + + const { data: versionList } = useScrollPagination(getToolVersionList, { + pageSize: 20, + params: { pluginId: toolId }, + refreshDeps: [toolId], + manual: false + }); + + useEffect(() => { + setSelectedVersion(initialSelectedVersion); + }, [initialSelectedVersion]); + + useEffect(() => { + if (allToolsKeepLatest) { + setSelectedVersion(''); + } + }, [allToolsKeepLatest]); + + const handleVersionChange = useCallback( + (versionId: string) => { + setSelectedVersion(versionId); + onVersionChange(versionId); + }, + [onVersionChange] + ); + + return ( + + + + + {t('app:enable_tool')} + + onEnabledChange(e.target.checked)} + /> + + + {versionList?.length ? ( + + {allToolsKeepLatest && ( + + {t('app:current_by_global_setting_all_tools_keep_latest')} + + )} + + handleVersionChange('')} + label={t('app:keep_the_latest')} + badge={allToolsKeepLatest ? t('app:global_control') : undefined} + /> + + {versionList.map((item, index) => ( + handleVersionChange(item._id)} + label={item.versionName} + badge={index === 0 ? t('app:newest') : undefined} + /> + ))} + + + ) : ( + + {t('app:no_version_info')} + + )} + + + ); +}; + +const VersionOption = ({ + isSelected, + isDisabled, + onClick, + label, + badge +}: { + isSelected: boolean; + isDisabled: boolean; + onClick: () => void; + label: string; + badge?: string; +}) => ( + + + + {label} + + {badge && ( + + {badge} + + )} + + +); + +// show status tag +const StatusTag = ({ + isEnabled, + isDeprecated, + isNew, + hasNewVersionAvailable +}: { + isEnabled: boolean; + isDeprecated: boolean; + isNew: boolean; + hasNewVersionAvailable: boolean; +}) => { + const { t } = useTranslation(); + const tagConfigs = [ + { condition: !isEnabled, text: t('app:disabled'), color: 'myGray.600', bg: 'myGray.100' }, + { condition: isDeprecated, text: t('app:invalid'), color: 'red.600', bg: 'red.50' }, + { condition: isNew, text: t('app:new_tool'), color: 'green.600', bg: 'green.50' }, + { + condition: hasNewVersionAvailable, + text: t('app:new_version'), + color: 'blue.600', + bg: 'blue.50' + } + ]; + + const activeTag = tagConfigs.find((config) => config.condition); + + if (!activeTag) return null; + + return ( + + {activeTag.text} + + ); +}; + export default React.memo(NodeToolSet); diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx index 108cd5e59e75..c417d7a9e402 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx @@ -121,9 +121,11 @@ const NodeCard = (props: Props) => { const showVersion = useMemo(() => { // 1. MCP tool set do not have version if (isAppNode && (node.toolConfig?.mcpToolSet || node.toolConfig?.mcpTool)) return false; - // 2. Team app/System commercial plugin + // 2. System tool set do not have version + if (isAppNode && node.toolConfig?.systemToolSet) return false; + // 3. Team app/System commercial plugin if (isAppNode && node?.pluginId && !node?.pluginData?.error) return true; - // 3. System tool + // 4. System tool if (isAppNode && node?.toolConfig?.systemTool) return true; return false; @@ -747,18 +749,39 @@ const NodeVersion = React.memo(function NodeVersion({ node }: { node: FlowNodeIt label: t('app:keep_the_latest'), value: '' }, - ...versionList.map((item) => ({ - label: item.versionName, + ...versionList.map((item, index) => ({ + label: + index === 0 ? ( + + {item.versionName} + + {t('app:newest')} + + + ) : ( + item.versionName + ), value: item._id })) ], [node.isLatestVersion, node.version, t, versionList] ); const valueLabel = useMemo(() => { + // When auto-update is enabled (version is empty), show "keep the latest" text + const displayLabel = node?.version === '' ? t('app:keep_the_latest') : node?.versionLabel; + return ( - {node?.version === '' ? t('app:keep_the_latest') : node?.versionLabel} - {!node.isLatestVersion && ( + {displayLabel} + {node?.version !== '' && !node.isLatestVersion && ( {t('app:not_the_newest')} diff --git a/projects/app/src/web/core/workflow/utils.ts b/projects/app/src/web/core/workflow/utils.ts index 05ef99765655..f67432cf8d58 100644 --- a/projects/app/src/web/core/workflow/utils.ts +++ b/projects/app/src/web/core/workflow/utils.ts @@ -101,6 +101,7 @@ export const storeNode2FlowNode = ({ ...template, ...storeNode, avatar: template.avatar ?? storeNode.avatar, + toolConfig: template.toolConfig ?? storeNode.toolConfig, version: template.version || storeNode.version, catchError: storeNode.catchError ?? template.catchError, // template 中的输入必须都有 @@ -758,7 +759,25 @@ export const compareSnapshot = ( intro: node.data.intro, avatar: node.data.avatar, version: node.data.version, - isFolded: node.data.isFolded + isFolded: node.data.isFolded, + ...(node.data.toolConfig && { + toolConfig: { + ...(node.data.toolConfig.systemToolSet && { + systemToolSet: { + toolId: node.data.toolConfig.systemToolSet.toolId, + toolList: node.data.toolConfig.systemToolSet.toolList.map((tool: any) => ({ + toolId: tool.toolId, + name: tool.name, + description: tool.description, + enabled: tool.enabled ?? true, + selectedVersionId: tool.selectedVersionId ?? '', + storedVersions: tool.storedVersions ?? [], + type: tool.type ?? 'invalid' + })) + } + }) + } + }) } })); };