diff --git a/.gitignore b/.gitignore index adb43235fe2f..f1aa0474d63d 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ pro/admin/worker/ /pro/llm_benchmark/content_benchmark/.env.*.local /pro/llm_benchmark/content_benchmark/eval-runs/ /pro/llm_benchmark/content_benchmark/.cache/ +.gstack/ diff --git a/document/content/self-host/upgrading/4-15/4151.en.mdx b/document/content/self-host/upgrading/4-15/4151.en.mdx index 3338051b69cc..ace51f527d7c 100644 --- a/document/content/self-host/upgrading/4-15/4151.en.mdx +++ b/document/content/self-host/upgrading/4-15/4151.en.mdx @@ -46,10 +46,12 @@ The script only fills missing `appName` values. It does not overwrite existing v 1. Added global API Key tag management and an `appName` display snapshot for historical app-level API Keys, making older API keys compatible and easier to find when they were previously associated with apps. 2. Pre-extract the skill name and description when publishing a skill to help with generation. 3. Added the `WECOM_LOGIN_AUTO_REDIRECT` environment variable to control whether WeCom terminals automatically redirect to login. It is disabled by default. +4. The Plugin Marketplace now supports official/community source filters, and the system tool list supports status and tag filters. ## ⚙️ Improvements 1. Removed system field parameters when AgentV2 calls nested workflows. +2. System tools now support uninstall and reinstall. Uninstalling changes the tool status to Uninstalled and requires entering the tool name for confirmation. Uninstalled tools show only basic information and can be reinstalled. ## 🐛 Fixes @@ -58,6 +60,7 @@ The script only fills missing `appName` values. It does not overwrite existing v 3. Fixed an issue where workflow tools did not initialize variables from the tool app's global variable configuration when running a sub-workflow, causing runtime variables such as default variables and system variables to be read incorrectly. 4. Fixed an issue where updates to global variables or outputs from nodes outside the container through **Variable Update** inside loop nodes and parallel execution nodes were not synchronized back to the main workflow by round or task completion. Successful rounds or tasks now write back their changes, while failed rounds or tasks do not commit their changes. 5. The component did not refresh immediately when retrying all Knowledge Base collections. +6. Fixed repeated shallow route updates in the embedded FastGPT Marketplace when filters did not change, which could keep the top progress bar loading. ## 🛠️ Code Improvements diff --git a/document/content/self-host/upgrading/4-15/4151.mdx b/document/content/self-host/upgrading/4-15/4151.mdx index 9ebe17856e20..5536f9c85ae3 100644 --- a/document/content/self-host/upgrading/4-15/4151.mdx +++ b/document/content/self-host/upgrading/4-15/4151.mdx @@ -48,10 +48,12 @@ curl -X POST "{{host}}/api/admin/initv4151" \ 1. 增加全局 API Key 标签管理,并为历史应用级 API Key 增加 `appName` 展示快照,便于兼容旧版 API 密钥并查找以前应用关联的密钥。 2. 发布技能时,预提取技能名称和描述,便于辅助生成。 3. 新增 `WECOM_LOGIN_AUTO_REDIRECT` 环境变量,可控制企微终端是否自动跳转登录,默认关闭。 +4. 插件市场支持官方/社区来源筛选,系统工具列表的状态列和标签列支持筛选。 ## ⚙️ 优化 1. AgentV2 调用嵌套工作流时候,去除系统字段参数。 +2. 系统工具支持卸载和重新安装。卸载会将工具状态改为“已卸载”,需要输入工具名称确认;已卸载工具只展示基础信息,并可重新安装恢复。 ## 🐛 修复 @@ -60,6 +62,7 @@ curl -X POST "{{host}}/api/admin/initv4151" \ 3. 修复工作流工具运行子工作流时未按工具应用的全局变量配置初始化变量,导致默认变量、系统变量等运行态变量读取异常的问题。 4. 修复循环节点和并行执行节点中通过【变量更新】修改全局变量或容器外节点输出时,主流程未按轮次/任务结束同步更新的问题。成功轮次或成功任务会回写本轮变更,失败轮次或失败任务不提交本轮变更。 5. 重试全部知识库集合时,未立即刷新组件。 +6. 修复 FastGPT 内嵌插件市场在筛选条件没有变化时重复更新浅路由,导致顶部进度条持续 loading 的问题。 ## 🛠️ 代码优化 diff --git a/packages/global/openapi/core/plugin/marketplace/api.ts b/packages/global/openapi/core/plugin/marketplace/api.ts index bb182788db57..584c7410f78c 100644 --- a/packages/global/openapi/core/plugin/marketplace/api.ts +++ b/packages/global/openapi/core/plugin/marketplace/api.ts @@ -4,7 +4,13 @@ import { PluginToolTagSchema } from '../../../../core/plugin/type'; import type { ToolListItemType } from '../../../../sdk/fastgpt-plugin'; export const MarketplaceOfficialSource = 'official'; +export const MarketplaceCommunitySource = 'community'; export const MarketplacePkgSourceSchema = z.string().trim().min(1); +export const MarketplaceSourceFilterSchema = z.enum([ + MarketplaceOfficialSource, + MarketplaceCommunitySource +]); +export type MarketplaceSourceFilterType = z.infer; const formatToolDetailSchema = z.object({}); const formatToolSimpleSchema = z.object({}); @@ -15,6 +21,7 @@ export type MarketplaceToolListItemType = ToolListItemType & { toolId: string; downloadCount: number; downloadUrl?: string; + source?: string; }; export const MarketplaceToolDetailItemSchema = formatToolDetailSchema.extend({ @@ -27,7 +34,8 @@ export const MarketplaceToolDetailSchema = z.object({ // List export const GetMarketplaceToolsBodySchema = PaginationSchema.extend({ searchKey: z.string().optional(), - tags: z.array(z.string()).nullish() + tags: z.array(z.string()).nullish(), + source: MarketplaceSourceFilterSchema.optional() }); export type GetMarketplaceToolsBodyType = z.infer; @@ -98,9 +106,7 @@ export const DeleteMarketplacePkgResponseSchema = z.object({ description: '插件来源' }) }); -export type DeleteMarketplacePkgResponseType = z.infer< - typeof DeleteMarketplacePkgResponseSchema ->; +export type DeleteMarketplacePkgResponseType = z.infer; // Tags export const GetMarketplaceToolTagsResponseSchema = z.array(PluginToolTagSchema); diff --git a/packages/service/core/app/tool/systemTool/systemTool.repo.ts b/packages/service/core/app/tool/systemTool/systemTool.repo.ts index 7ba8f9a9d491..817e47925592 100644 --- a/packages/service/core/app/tool/systemTool/systemTool.repo.ts +++ b/packages/service/core/app/tool/systemTool/systemTool.repo.ts @@ -202,6 +202,34 @@ const getVisiblePluginStatus = ({ return status ?? PluginStatusEnum.Normal; }; +const assertSystemToolRunnable = ({ + tool, + source +}: { + tool?: SystemPluginToolCollectionType | null; + source?: string; +}) => { + if (isDebugToolSource(source)) return; + if (tool?.status === PluginStatusEnum.Offline) { + return Promise.reject(PluginErrEnum.unExist); + } +}; + +const getParentSystemToolConfig = async ({ + pluginId, + idSource, + parentPluginId +}: { + pluginId: string; + idSource?: string; + parentPluginId: string; +}) => { + if (!pluginId.includes('/')) return; + if (idSource === AppToolSourceEnum.systemTool || idSource === AppToolSourceEnum.commercial) { + return getSystemToolConfig(`${idSource}-${parentPluginId}`); + } +}; + /** * SystemTool Repo * 系统工具仓储层 @@ -737,6 +765,13 @@ export class SystemToolRepo { const [parentPluginId] = rawPluginId.split('/'); const dbTool = await getSystemToolConfig(pluginId); + await assertSystemToolRunnable({ tool: dbTool, source: pluginSource }); + const parentDbTool = await getParentSystemToolConfig({ + pluginId, + idSource, + parentPluginId + }); + await assertSystemToolRunnable({ tool: parentDbTool, source: pluginSource }); if (!dbTool?.customConfig?.associatedPluginId) { const tool = await pluginClient.getTool({ @@ -778,6 +813,7 @@ export class SystemToolRepo { } const tool = await this.getSystemToolRecord(pluginId); + await assertSystemToolRunnable({ tool }); if (!tool || !tool.customConfig?.associatedPluginId) { return Promise.reject('Plugin is not associated with a app'); diff --git a/packages/service/test/core/app/tool/systemTool/systemTool.repo.test.ts b/packages/service/test/core/app/tool/systemTool/systemTool.repo.test.ts index 3290ce51a338..0ddb3254fcd2 100644 --- a/packages/service/test/core/app/tool/systemTool/systemTool.repo.test.ts +++ b/packages/service/test/core/app/tool/systemTool/systemTool.repo.test.ts @@ -6,6 +6,7 @@ import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { PluginStatusEnum, type PluginStatusType } from '@fastgpt/global/core/plugin/type'; +import { PluginErrEnum } from '@fastgpt/global/common/error/code/plugin'; const mocks = vi.hoisted(() => ({ listTools: vi.fn(), @@ -457,6 +458,28 @@ describe('SystemToolRepo.getSystemToolDetail', () => { }); describe('SystemToolRepo.getSystemToolWorkflowRuntime', () => { + it('rejects uninstalled workflow tools before loading app version', async () => { + mocks.findSystemTool.mockResolvedValue({ + pluginId: 'commercial-workflow-tool', + status: PluginStatusEnum.Offline, + currentCost: 2, + customConfig: { + name: 'Workflow Tool', + avatar: 'workflow.svg', + associatedPluginId: 'app-id' + } + }); + + await expect( + SystemToolRepo.getInstance().getSystemToolWorkflowRuntime({ + pluginId: 'commercial-workflow-tool', + version: 'version-id' + }) + ).rejects.toBe(PluginErrEnum.unExist); + + expect(mocks.getAppVersionById).not.toHaveBeenCalled(); + }); + it('returns workflow app chatConfig for runtime variable initialization', async () => { mocks.findSystemTool.mockResolvedValue({ pluginId: 'commercial-workflow-tool', @@ -909,6 +932,45 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { }); describe('SystemToolRepo.getSystemToolRuntime', () => { + it('rejects uninstalled system tools before calling plugin runtime', async () => { + mocks.findSystemTool.mockResolvedValue({ + pluginId: 'systemTool-weather', + status: PluginStatusEnum.Offline, + currentCost: 1, + systemKeyCost: 2, + customConfig: {} + }); + + await expect( + SystemToolRepo.getInstance().getSystemToolRuntime({ + pluginId: 'systemTool-weather', + source: 'system' + }) + ).rejects.toBe(PluginErrEnum.unExist); + + expect(mocks.getTool).not.toHaveBeenCalled(); + }); + + it('rejects toolset children when parent system tool is uninstalled', async () => { + mocks.findSystemTool.mockResolvedValueOnce(null).mockResolvedValueOnce({ + pluginId: 'systemTool-toolset', + status: PluginStatusEnum.Offline, + currentCost: 1, + systemKeyCost: 2, + customConfig: {} + }); + mocks.findSystemTools.mockResolvedValueOnce([]); + + await expect( + SystemToolRepo.getInstance().getSystemToolRuntime({ + pluginId: 'systemTool-toolset/child', + source: 'system' + }) + ).rejects.toBe(PluginErrEnum.unExist); + + expect(mocks.getTool).not.toHaveBeenCalled(); + }); + it('does not return configured system secrets for debug source', async () => { mocks.findSystemTool.mockResolvedValue({ pluginId: 'systemTool-weather', diff --git a/packages/web/components/core/plugin/tool/TagFilterBox.tsx b/packages/web/components/core/plugin/tool/TagFilterBox.tsx index a9ec35e2302e..923fd0e2cf01 100644 --- a/packages/web/components/core/plugin/tool/TagFilterBox.tsx +++ b/packages/web/components/core/plugin/tool/TagFilterBox.tsx @@ -1,21 +1,38 @@ -import { Box, Flex } from '@chakra-ui/react'; +import { Box, Flex, Menu, MenuButton, MenuItem, MenuList, Portal } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; import type { SystemPluginToolTagType } from '@fastgpt/global/core/plugin/type'; import React, { useMemo } from 'react'; +import MyIcon from '../../../common/Icon'; + +export type MarketplaceSourceFilterValue = 'official' | 'community'; const ToolTagFilterBox = ({ tags, selectedTagIds, onTagSelect, - size = 'base' + selectedSource, + onSourceSelect, + size = 'base', + variant = 'default' }: { tags: SystemPluginToolTagType[]; selectedTagIds: string[]; onTagSelect: (tagIds: string[]) => void; + selectedSource?: MarketplaceSourceFilterValue; + onSourceSelect?: (source?: MarketplaceSourceFilterValue) => void; size?: 'base' | 'sm'; + variant?: 'default' | 'marketplace'; }) => { const { t, i18n } = useTranslation(); + const isMarketplaceVariant = variant === 'marketplace'; + const sourceOptions = [ + { label: t('common:All'), value: undefined }, + { label: t('app:toolkit_official'), value: 'official' }, + { label: t('app:toolkit_community'), value: 'community' } + ] as const; + const selectedSourceLabel = + sourceOptions.find((option) => option.value === selectedSource)?.label || t('common:All'); const toggleTag = (tagId: string) => { if (selectedTagIds.includes(tagId)) { @@ -27,11 +44,18 @@ const ToolTagFilterBox = ({ const tagBaseStyles = useMemo(() => { const sizeStyles = { - base: { - px: 3, - py: 1.5, - fontSize: 'sm' - }, + base: isMarketplaceVariant + ? { + px: '13px', + h: '35px', + fontSize: '14px', + lineHeight: '21px' + } + : { + px: 3, + py: 1.5, + fontSize: 'sm' + }, sm: { px: 2, py: 1, @@ -42,14 +66,20 @@ const ToolTagFilterBox = ({ return { ...sizeStyles[size], fontWeight: 'medium', - color: 'myGray.700', + color: isMarketplaceVariant ? '#383F50' : 'myGray.700', border: '1px solid', - borderColor: 'myGray.200', + borderColor: isMarketplaceVariant ? '#E8EBF0' : 'myGray.200', whiteSpace: 'nowrap', flexShrink: 0, - cursor: 'pointer' + cursor: 'pointer', + ...(isMarketplaceVariant + ? { + alignItems: 'center', + justifyContent: 'center' + } + : {}) }; - }, [size]); + }, [isMarketplaceVariant, size]); return ( + {isMarketplaceVariant ? ( + + + + {selectedSourceLabel} + + + + + + + + {sourceOptions.map((option) => { + const isSelected = option.value === selectedSource; + + return ( + onSourceSelect?.(option.value)} + > + {option.label} + {isSelected && } + + ); + })} + + + + ) : ( + onTagSelect([])} + > + {t('common:All')} + + )} onTagSelect([])} - > - {t('common:All')} - - + mx={2} + h={'20px'} + w={'1px'} + bg={isMarketplaceVariant ? '#E8EBF0' : 'myGray.200'} + flexShrink={0} + /> {tags.map((tag) => { @@ -100,8 +213,15 @@ const ToolTagFilterBox = ({ { + if (isMarketplaceVariant) { + return isSelected ? 'myGray.50 !important' : 'white'; + } + return isSelected ? 'myGray.150 !important' : 'transparent'; + })()} + _hover={isMarketplaceVariant ? { bg: 'myGray.50' } : undefined} onClick={() => toggleTag(tag.tagId)} > {t(parseI18nString(tag.tagName, i18n.language))} diff --git a/packages/web/components/core/plugin/tool/ToolCard.tsx b/packages/web/components/core/plugin/tool/ToolCard.tsx index 2e053de3938a..853dd18f73d6 100644 --- a/packages/web/components/core/plugin/tool/ToolCard.tsx +++ b/packages/web/components/core/plugin/tool/ToolCard.tsx @@ -8,6 +8,8 @@ import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; import { PluginStatusEnum, type PluginStatusType } from '@fastgpt/global/core/plugin/type'; import DebugToolTag from './DebugToolTag'; +const marketplaceOfficialSource = 'official'; + export type ToolCardItemType = { id: string; name: string; @@ -43,7 +45,8 @@ const ToolCard = ({ onDelete, onUpdate, onClickCard, - showActionButton = true + showActionButton = true, + variant = 'default' }: { item: ToolCardItemType; systemTitle?: string; @@ -55,10 +58,16 @@ const ToolCard = ({ onUpdate?: () => Promise; onClickCard?: () => void; showActionButton?: boolean; + variant?: 'default' | 'marketplace'; }) => { const { t, i18n } = useTranslation(); const tagsContainerRef = useRef(null); const [visibleTagsCount, setVisibleTagsCount] = useState(item.tags?.length || 0); + const isMarketplaceVariant = variant === 'marketplace'; + const showOfficialBadge = + isMarketplaceVariant && (!item.source || item.source === marketplaceOfficialSource); + const showMarketplaceUninstallButton = + isMarketplaceVariant && mode === 'admin' && item.installed && !showActionButton; useEffect(() => { const calculate = () => { @@ -109,6 +118,8 @@ const ToolCard = ({ }; if (mode === 'admin') { + if (isMarketplaceVariant) return null; + return item.installed ? { label: t('app:toolkit_installed'), @@ -130,14 +141,25 @@ const ToolCard = ({ } : null; } - }, [item.installed, item.status]); + }, [isMarketplaceVariant, item.installed, item.status, mode, t]); return ( )} - - - + + + {parseI18nString(item.name, i18n.language)} + {showOfficialBadge && ( + + {t('app:toolkit_official')} + + )} {item.isDebug && } {statusLabel && ( {parseI18nString(item.description || '', i18n.language) || t('app:templateMarket.no_intro')} - + {item.tags?.slice(0, visibleTagsCount).map((tag) => { return ( @@ -264,14 +327,15 @@ const ToolCard = ({ })} {item.tags && item.tags.length > visibleTagsCount && ( +{item.tags.length - visibleTagsCount} @@ -279,11 +343,18 @@ const ToolCard = ({ )} - + {`by ${item.author || systemTitle || 'FastGPT'}`} {/*TODO: when statistics is ready*/} {/* @@ -296,7 +367,21 @@ const ToolCard = ({ */} - {showActionButton && mode === 'marketplace' ? ( + {showMarketplaceUninstallButton ? ( + + ) : showActionButton && mode === 'marketplace' ? ( - )} - {hasUpdateButton && ( - - )} - - ); - })()} + {showInstallButton && ( + + )} + {hasUpdateButton && ( + + )} + {showUninstallButton && ( + + )} )} diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index 32efd6f36ca0..fa13aa42c3cd 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -83,6 +83,7 @@ "confirm_delete_chats": "Are you sure you want to delete {{n}} chat records?\nThis cannot be undone.", "confirm_delete_folder_tip": "When you delete this folder, all applications and corresponding chat records under it will be deleted.", "confirm_delete_tool": "Confirm to delete this tool?", + "confirm_uninstall_tool": "After uninstalling this plugin, apps that depend on it may not run properly. Please proceed carefully.", "copilot_config_message": "Current Node Configuration Information: \n Code Type: {{codeType}} \n Current Code: \\\\`\\\\`\\\\`{{codeType}} \n{{code}} \\\\`\\\\`\\\\` \n Input Parameters: {{inputs}} \n Output Parameters: {{outputs}}", "copilot_confirm_message": "The original configuration has been received to understand the current code structure and input and output parameters. \nPlease explain your optimization requirements.", "copy_one_app": "Create Duplicate", @@ -115,6 +116,9 @@ "custom_plugin_config_title": "{{name}} Configuration", "custom_plugin_create": "Create Plugin", "custom_plugin_delete_success": "Delete successful", + "custom_plugin_install_success": "Install successful", + "custom_plugin_reinstall_success": "Reinstall successful", + "custom_plugin_uninstall_success": "Uninstall successful", "custom_plugin_has_token_fee_label": "Charge Token Fee", "custom_plugin_intro_label": "Introduction", "custom_plugin_intro_placeholder": "Add an introduction for this application", @@ -383,7 +387,7 @@ "tool_input_param_tip": "This tool requires configuration of relevant information for normal operation.", "tool_not_active": "This tool has not been activated yet", "tool_not_desc": "The tool lacks a description ~", - "tool_offset_tips": "This tool is no longer available and will interrupt application operation. Please replace it immediately.", + "tool_offset_tips": "This tool has been uninstalled and will interrupt application operation. Please replace it immediately.", "tool_param_config": "Parameter configuration", "tool_params_description_tips": "The description of parameter functions, if used as tool invocation parameters, affects the model tool invocation effect.", "tool_run_free": "This tool runs without points consumption", @@ -396,8 +400,10 @@ "toolkit_activation_required": "Activation Required", "toolkit_add_resource": "Add resources", "toolkit_basic_config": "Basic Configuration", + "toolkit_basic_info": "Basic Info", "toolkit_batch_update": "Batch Update", "toolkit_call_points_label": "Call Points", + "toolkit_community": "Community", "toolkit_config_system_key": "Configure System Key", "toolkit_contribute_resource": "Contribute Resource", "toolkit_debug_connection_link": "Debug Link", @@ -425,6 +431,7 @@ "toolkit_name": "Name", "toolkit_no_call_points": "This tool does not require call points", "toolkit_no_plugins": "No plugins", + "toolkit_official": "Official", "toolkit_open_marketplace": "Open Marketplace", "toolkit_outputs": "Output Parameters", "toolkit_params_description": "Parameters", @@ -435,12 +442,13 @@ "toolkit_plugin_version": "Plugin Version", "toolkit_promote_tags": "Promote Tags", "toolkit_promote_tags_tip": "Users with the following tags will see the \"Promoted\" badge", + "toolkit_reinstall": "Reinstall", "toolkit_search_placeholder": "Search tool names", "toolkit_select_app": "Select an existing app", "toolkit_select_user_tags": "Select user tags", "toolkit_status": "Status", "toolkit_status_normal": "Normal", - "toolkit_status_offline": "Offline", + "toolkit_status_offline": "Uninstalled", "toolkit_status_soon_offline": "Soon Offline", "toolkit_system_key": "System Key", "toolkit_system_key_configured": "Configured", @@ -462,7 +470,8 @@ "toolkit_tool_name": "Tool Name", "toolkit_tutorial_link": "Open tutorial link", "toolkit_uninstall": "Uninstall", - "toolkit_uninstalled": "Uninstalled", + "toolkit_uninstalled_reinstall_tip": "This plugin has been uninstalled. Please reinstall it.", + "toolkit_uninstalled": "Not Installed", "toolkit_updatable": "Updates Available", "toolkit_updatable_plugins": "Updatable Plugins", "toolkit_user_guide": "User Guide", diff --git a/packages/web/i18n/zh-CN/app.json b/packages/web/i18n/zh-CN/app.json index f2ad0de26078..9b76c5047e9d 100644 --- a/packages/web/i18n/zh-CN/app.json +++ b/packages/web/i18n/zh-CN/app.json @@ -83,6 +83,7 @@ "confirm_delete_chats": "确认删除 {{n}} 组对话记录?记录将会永久删除!", "confirm_delete_folder_tip": "删除该文件夹时,将会删除它下面所有应用及对应的聊天记录。", "confirm_delete_tool": "确认删除该工具?", + "confirm_uninstall_tool": "插件卸载后,依赖此插件的应用可能无法正常运行,请谨慎操作。", "copilot_config_message": "`当前节点配置信息: \n代码类型:{{codeType}} \n当前代码: \\`\\`\\`{{codeType}} \n{{code}} \\`\\`\\` \n输入参数: {{inputs}} \n输出参数: {{outputs}}`", "copilot_confirm_message": "已接收到原始配置,了解当前代码结构和输入输出参数。请说明您的优化需求。", "copy_one_app": "创建副本", @@ -115,6 +116,9 @@ "custom_plugin_config_title": "{{name}}配置", "custom_plugin_create": "新建插件", "custom_plugin_delete_success": "删除成功", + "custom_plugin_install_success": "安装成功", + "custom_plugin_reinstall_success": "重新安装成功", + "custom_plugin_uninstall_success": "卸载成功", "custom_plugin_has_token_fee_label": "是否收取 Token 费用", "custom_plugin_intro_label": "介绍", "custom_plugin_intro_placeholder": "为这个应用添加一个介绍", @@ -383,7 +387,7 @@ "tool_input_param_tip": "该工具正常运行需要配置相关信息", "tool_not_active": "该工具尚未激活", "tool_not_desc": "工具缺少描述~", - "tool_offset_tips": "该工具已无法使用,将中断应用运行,请立即替换", + "tool_offset_tips": "该工具已卸载,将中断应用运行,请立即替换", "tool_param_config": "参数配置", "tool_params_description_tips": "参数功能的描述,若作为工具调用参数,影响模型工具调用效果", "tool_run_free": "该工具运行无积分消耗", @@ -396,8 +400,10 @@ "toolkit_activation_required": "需要激活", "toolkit_add_resource": "添加插件", "toolkit_basic_config": "基础配置", + "toolkit_basic_info": "基础信息", "toolkit_batch_update": "批量更新", "toolkit_call_points_label": "调用积分", + "toolkit_community": "社区", "toolkit_config_system_key": "是否配置系统密钥", "toolkit_contribute_resource": "贡献插件", "toolkit_debug_connection_link": "调试链接", @@ -425,6 +431,7 @@ "toolkit_name": "名称", "toolkit_no_call_points": "该工具无需调用积分", "toolkit_no_plugins": "暂无插件", + "toolkit_official": "官方", "toolkit_open_marketplace": "打开插件市场", "toolkit_outputs": "输出参数", "toolkit_params_description": "参数说明", @@ -435,12 +442,13 @@ "toolkit_plugin_version": "插件版本", "toolkit_promote_tags": "推荐标签", "toolkit_promote_tags_tip": "拥有以下标签的用户会看到\"推荐\"标识", + "toolkit_reinstall": "重新安装", "toolkit_search_placeholder": "搜索工具名称", "toolkit_select_app": "选择现有应用", "toolkit_select_user_tags": "选择用户标签", "toolkit_status": "状态", "toolkit_status_normal": "正常", - "toolkit_status_offline": "已下线", + "toolkit_status_offline": "已卸载", "toolkit_status_soon_offline": "即将下线", "toolkit_system_key": "系统密钥", "toolkit_system_key_configured": "已配置", @@ -462,6 +470,7 @@ "toolkit_tool_name": "工具名", "toolkit_tutorial_link": "查看教程链接", "toolkit_uninstall": "卸载", + "toolkit_uninstalled_reinstall_tip": "该插件已卸载,请重新安装", "toolkit_uninstalled": "未安装", "toolkit_updatable": "可更新", "toolkit_updatable_plugins": "可更新的插件", diff --git a/packages/web/i18n/zh-Hant/app.json b/packages/web/i18n/zh-Hant/app.json index 324f99348f49..ccf5cc360810 100644 --- a/packages/web/i18n/zh-Hant/app.json +++ b/packages/web/i18n/zh-Hant/app.json @@ -81,6 +81,7 @@ "confirm_delete_chats": "確認刪除 {{n}} 組對話記錄?\n記錄將會永久刪除!", "confirm_delete_folder_tip": "刪除該文件夾時,將會刪除它下面所有應用及對應的聊天記錄。", "confirm_delete_tool": "確認刪除該工具?", + "confirm_uninstall_tool": "插件卸載後,依賴此插件的應用可能無法正常運行,請謹慎操作。", "copilot_config_message": "當前節點配置信息: \n代碼類型:{{codeType}} \n當前代碼: \\\\`\\\\`\\\\`{{codeType}} \n{{code}} \\\\`\\\\`\\\\` \n輸入參數: {{inputs}} \n輸出參數: {{outputs}}", "copilot_confirm_message": "已接收到原始配置,了解當前代碼結構和輸入輸出參數。\n請說明您的優化需求。", "copy_one_app": "建立副本", @@ -112,6 +113,9 @@ "custom_plugin_config_title": "{{name}}配置", "custom_plugin_create": "新建外掛", "custom_plugin_delete_success": "刪除成功", + "custom_plugin_install_success": "安裝成功", + "custom_plugin_reinstall_success": "重新安裝成功", + "custom_plugin_uninstall_success": "卸載成功", "custom_plugin_has_token_fee_label": "是否收取 Token 費用", "custom_plugin_intro_label": "介紹", "custom_plugin_intro_placeholder": "為這個應用程式新增一個介紹", @@ -373,7 +377,7 @@ "tool_input_param_tip": "該工具正常運行需要配置相關信息", "tool_not_active": "該工具尚未激活", "tool_not_desc": "工具缺少描述~", - "tool_offset_tips": "該工具已無法使用,將中斷應用運行,請立即替換", + "tool_offset_tips": "該工具已卸載,將中斷應用運行,請立即替換", "tool_param_config": "參數配置", "tool_params_description_tips": "參數功能的描述,若作為工具調用參數,影響模型工具調用效果", "tool_run_free": "該工具運行無積分消耗", @@ -386,8 +390,10 @@ "toolkit_activation_required": "需要激活", "toolkit_add_resource": "添加資源", "toolkit_basic_config": "基礎配置", + "toolkit_basic_info": "基礎資訊", "toolkit_batch_update": "批次更新", "toolkit_call_points_label": "調用積分", + "toolkit_community": "社區", "toolkit_config_system_key": "是否配置系統密鑰", "toolkit_contribute_resource": "貢獻資源", "toolkit_debug_connection_link": "調試連結", @@ -415,6 +421,7 @@ "toolkit_name": "名稱", "toolkit_no_call_points": "該工具無需調用積分", "toolkit_no_plugins": "暫無插件", + "toolkit_official": "官方", "toolkit_open_marketplace": "打開資源市場", "toolkit_outputs": "輸出參數", "toolkit_params_description": "參數說明", @@ -425,12 +432,13 @@ "toolkit_plugin_version": "插件版本", "toolkit_promote_tags": "推薦標籤", "toolkit_promote_tags_tip": "擁有以下標籤的使用者會看到「推薦」標識", + "toolkit_reinstall": "重新安裝", "toolkit_search_placeholder": "搜索工具名稱", "toolkit_select_app": "選擇現有應用", "toolkit_select_user_tags": "選擇使用者標籤", "toolkit_status": "狀態", "toolkit_status_normal": "正常", - "toolkit_status_offline": "下線", + "toolkit_status_offline": "已卸載", "toolkit_status_soon_offline": "即將下線", "toolkit_system_key": "系統密鑰", "toolkit_system_key_configured": "已配置", @@ -451,6 +459,7 @@ "toolkit_tool_name": "工具名", "toolkit_tutorial_link": "查看教學連結", "toolkit_uninstall": "卸載", + "toolkit_uninstalled_reinstall_tip": "該插件已卸載,請重新安裝", "toolkit_uninstalled": "未安裝", "toolkit_updatable": "可更新", "toolkit_updatable_plugins": "可更新的外掛程式", diff --git a/packages/web/styles/theme.ts b/packages/web/styles/theme.ts index 7256359f35bd..7c6be15928dd 100644 --- a/packages/web/styles/theme.ts +++ b/packages/web/styles/theme.ts @@ -247,6 +247,29 @@ const Button = defineStyleConfig({ color: 'red.600' } }, + dangerOutline: { + color: 'red.600', + border: '1px solid', + borderColor: 'red.500', + bg: 'white', + transition: 'background 0.1s', + boxShadow: '0px 0px 1px 0px rgba(19, 51, 107, 0.08), 0px 1px 2px 0px rgba(19, 51, 107, 0.05)', + _hover: { + color: 'red.600', + borderColor: 'red.600', + bg: 'red.50' + }, + _active: { + color: 'red.600', + borderColor: 'red.600', + bg: 'red.50' + }, + _disabled: { + color: 'red.300 !important', + borderColor: 'red.200 !important', + bg: 'white !important' + } + }, grayBase: { bg: 'myGray.150', color: 'myGray.900', 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 83d1c2646356..3471e2058951 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 @@ -85,6 +85,23 @@ type Props = FlowNodeItemType & { colorSchema?: keyof typeof NodeGradients; }; +const getCurrentSystemToolTemplate = async (node?: FlowNodeItemType) => { + if (!node?.pluginId || node.pluginData?.error || isDebugToolSource(node.source)) return; + + try { + const { source } = splitCombineToolId(node.pluginId); + if (source !== AppToolSourceEnum.systemTool && source !== AppToolSourceEnum.commercial) return; + + return getClientToolPreviewNode({ + appId: node.pluginId, + versionId: node.version ?? '', + source: node.source + }); + } catch { + return; + } +}; + const NodeCard = (props: Props) => { const { t } = useTranslation(); const { @@ -227,28 +244,6 @@ const NodeCard = (props: Props) => { const isAppNode = node && AppNodeFlowNodeTypeMap[node?.flowNodeType]; const isLoopNode = isNestedParentNodeType(node?.flowNodeType ?? ''); - const showVersion = useMemo(() => { - const source = node?.pluginId ? splitCombineToolId(node.pluginId).source : undefined; - if (isDebugToolSource(node?.source)) return false; - // 1. MCP/HTTP single tools use the latest toolset content and do not expose version selection. - if (source === AppToolSourceEnum.mcp || source === AppToolSourceEnum.http) return false; - - // 2. MCP/HTTP tool sets do not have version - if ( - isAppNode && - (node.toolConfig?.mcpToolSet || - node.toolConfig?.mcpTool || - node?.toolConfig?.httpToolSet || - node?.toolConfig?.httpTool) - ) - return false; - // 3. Team app/System commercial plugin - if (isAppNode && node?.pluginId && !node?.pluginData?.error) return true; - // 4. System tool - if (isAppNode && node?.toolConfig?.systemTool) return true; - - return false; - }, [isAppNode, node]); const { data: nodeTemplate } = useRequest( async () => { @@ -257,7 +252,21 @@ const NodeCard = (props: Props) => { } if (isAppNode) { - return { ...node, ...node.pluginData }; + const currentSystemToolTemplate = await getCurrentSystemToolTemplate(node); + + return { + ...node, + ...node.pluginData, + ...(currentSystemToolTemplate + ? { + status: currentSystemToolTemplate.status, + courseUrl: currentSystemToolTemplate.courseUrl, + readmeUrl: currentSystemToolTemplate.readmeUrl, + userGuide: currentSystemToolTemplate.userGuide, + diagram: currentSystemToolTemplate.diagram + } + : {}) + }; } else { const template = moduleTemplatesFlat.find( (item) => item.flowNodeType === node?.flowNodeType @@ -290,10 +299,45 @@ const NodeCard = (props: Props) => { } ]); }, - manual: false + manual: false, + errorToast: '', + refreshDeps: [ + isAppNode, + node?.pluginData?.error, + node?.pluginData?.status, + node?.pluginId, + node?.source, + node?.version + ] } ); + const toolStatus = nodeTemplate?.status ?? node?.pluginData?.status; + const showVersion = useMemo(() => { + if (toolStatus === PluginStatusEnum.Offline) return false; + + const source = node?.pluginId ? splitCombineToolId(node.pluginId).source : undefined; + if (isDebugToolSource(node?.source)) return false; + // 1. MCP/HTTP single tools use the latest toolset content and do not expose version selection. + if (source === AppToolSourceEnum.mcp || source === AppToolSourceEnum.http) return false; + + // 2. MCP/HTTP tool sets do not have version + if ( + isAppNode && + (node.toolConfig?.mcpToolSet || + node.toolConfig?.mcpTool || + node?.toolConfig?.httpToolSet || + node?.toolConfig?.httpTool) + ) + return false; + // 3. Team app/System commercial plugin + if (isAppNode && node?.pluginId && !node?.pluginData?.error) return true; + // 4. System tool + if (isAppNode && node?.toolConfig?.systemTool) return true; + + return false; + }, [isAppNode, node, toolStatus]); + /* Node header - 重构后的版本,依赖项大幅减少 */ const error = useMemo(() => formatToolError(node?.pluginData?.error), [node?.pluginData?.error]); const showHeader = node?.flowNodeType !== FlowNodeTypeEnum.comment; diff --git a/projects/app/src/pageComponents/config/tool/SystemToolConfigModal.tsx b/projects/app/src/pageComponents/config/tool/SystemToolConfigModal.tsx index a64a46ad272a..2f3485834aca 100644 --- a/projects/app/src/pageComponents/config/tool/SystemToolConfigModal.tsx +++ b/projects/app/src/pageComponents/config/tool/SystemToolConfigModal.tsx @@ -53,6 +53,7 @@ import type { UpdateSystemToolBodyType } from '@fastgpt/global/openapi/core/plug import CopyBox from '@fastgpt/web/components/common/String/CopyBox'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { jsonSchema2SecretInput } from '@fastgpt/global/core/app/jsonschema'; +import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; const COST_LIMITS = { max: 1000, min: 0, step: 0.1 }; const FORM_LABEL_WIDTH = '160px'; @@ -181,6 +182,9 @@ const SystemToolConfigModal = ({ const [runtimeConfig, setRuntimeConfig] = useState({}); const [selectedVersion, setSelectedVersion] = useState(); const [tabIndex, setTabIndex] = useState(0); + const { openConfirm: openUninstallConfirm, ConfirmModal: UninstallConfirmModal } = useConfirm({ + type: 'delete' + }); useEffect(() => { setSelectedVersion(undefined); @@ -245,6 +249,7 @@ const SystemToolConfigModal = ({ 'promoteTags', 'hideTags' ]); + const isToolOffline = status === PluginStatusEnum.Offline; // 从 tool 读取只读数据 const inputList = useMemo( @@ -345,8 +350,7 @@ const SystemToolConfigModal = ({ { label: t('app:toolkit_status_soon_offline'), value: PluginStatusEnum.SoonOffline - }, - { label: t('app:toolkit_status_offline'), value: PluginStatusEnum.Offline } + } ], [t] ); @@ -383,6 +387,57 @@ const SystemToolConfigModal = ({ } ); + const { runAsync: onUninstall, loading: uninstalling } = useRequest( + () => + putAdminUpdateSystemTool({ + id: toolId, + status: PluginStatusEnum.Offline, + children: tool?.children?.map((child) => ({ + id: child.id, + systemKeyCost: child.systemKeyCost + })) + }), + { + successToast: t('app:custom_plugin_uninstall_success'), + onSuccess() { + onSuccess(); + onClose(); + } + } + ); + + const { runAsync: onReinstall, loading: reinstalling } = useRequest( + () => + putAdminUpdateSystemTool({ + id: toolId, + status: PluginStatusEnum.Normal, + children: tool?.children?.map((child) => ({ + id: child.id, + systemKeyCost: child.systemKeyCost + })) + }), + { + successToast: t('app:custom_plugin_install_success'), + onSuccess() { + onSuccess(); + onClose(); + } + } + ); + + const openToolUninstallConfirm = () => { + const toolName = parseI18nString(tool?.name || '', i18n.language) || toolId; + + openUninstallConfirm({ + title: t('app:toolkit_uninstall'), + customContent: t('app:confirm_uninstall_tool'), + confirmText: t('app:toolkit_uninstall'), + confirmButtonVariant: 'dangerOutline', + inputConfirmText: toolName, + onConfirm: onUninstall + })(); + }; + // Secret input render const renderInputField = (item: InputConfigType) => { const fieldValue = secretsVal?.[item.key]; @@ -403,7 +458,7 @@ const SystemToolConfigModal = ({ if (item.inputType === 'switch') { return ( - + ); } @@ -417,6 +472,7 @@ const SystemToolConfigModal = ({ list={item.list ?? []} value={typeof fieldValue === 'string' ? fieldValue : undefined} placeholder={item.label} + isDisabled={isToolOffline} onChange={(value) => { setValue(`secretsVal.${item.key}`, value, { shouldDirty: true, @@ -434,6 +490,7 @@ const SystemToolConfigModal = ({ bg={'white'} h={9} borderColor={'myGray.200'} + isDisabled={isToolOffline} {...register(`secretsVal.${item.key}`, { required: item.required })} @@ -451,6 +508,7 @@ const SystemToolConfigModal = ({ register={register} name="systemKeyCost" defaultValue={0} + isDisabled={isToolOffline} {...COST_LIMITS} /> @@ -477,6 +535,7 @@ const SystemToolConfigModal = ({ fontSize={'xs'} min={field.min} step={1} + isDisabled={isToolOffline} value={ runtimeConfig[field.key] === undefined || runtimeConfig[field.key] === null ? '' @@ -517,7 +576,11 @@ const SystemToolConfigModal = ({ width={'100%'} h={9} value={status} + valueLabel={ + status === PluginStatusEnum.Offline ? t('app:toolkit_status_offline') : undefined + } list={pluginStatusSelectList} + isDisabled={isToolOffline} onChange={(e) => setValue('status', e)} /> @@ -541,6 +604,7 @@ const SystemToolConfigModal = ({ h={9} borderRadius={'sm'} bg={'white'} + isDisabled={isToolOffline} /> @@ -549,6 +613,7 @@ const SystemToolConfigModal = ({ { const val = e.target.checked; if (val) { @@ -578,6 +643,7 @@ const SystemToolConfigModal = ({ h={9} borderRadius={'sm'} bg={'white'} + isDisabled={isToolOffline} /> @@ -591,6 +657,7 @@ const SystemToolConfigModal = ({ h={9} borderRadius={'sm'} bg={'white'} + isDisabled={isToolOffline} /> @@ -633,6 +700,7 @@ const SystemToolConfigModal = ({ register={register} defaultValue={0} name={`children.${index}.systemKeyCost`} + isDisabled={isToolOffline} {...COST_LIMITS} /> setSelectedVersion(version)} /> @@ -678,13 +747,66 @@ const SystemToolConfigModal = ({ ); + const offlineVersionInfoSection = ( + + + {t('app:toolkit_id')}: + + + {tool?.id || toolId} + + + + + + } + > + + + width={'100%'} + h={9} + value={selectedVersion || tool?.version} + list={versionSelectList} + isLoading={loadingVersions} + isDisabled + onChange={(version) => setSelectedVersion(version)} + /> + + + + + {tool?.name || '-'} + + + + + + {tool?.intro || '-'} + + + + ); + return ( - - - - {t('app:toolkit_plugin_config')} - - + - {t('admin_plugin:toolkit_runtime_config')} - - - - - - - {basicConfigSection} - {versionInfoSection} - - - - {runtimeConfigSection || ( - - {t('admin_plugin:toolkit_no_runtime_config')} - - )} - - - + + {t('app:toolkit_plugin_config')} + + + {t('admin_plugin:toolkit_runtime_config')} + + + + + + + {basicConfigSection} + {versionInfoSection} + + + + {runtimeConfigSection || ( + + {t('admin_plugin:toolkit_no_runtime_config')} + + )} + + + + )} - - {showRuntimeConfig && tabIndex === 1 && ( - - {t('common:Reset')} - - } - /> - )} - - - - - + {isToolOffline ? ( + <> + + {t('app:toolkit_uninstalled_reinstall_tip')} + + + + + + + ) : ( + <> + + + {showRuntimeConfig && tabIndex === 1 && ( + + {t('common:Reset')} + + } + /> + )} + + + + + + + )} + ); }; diff --git a/projects/app/src/pageComponents/config/tool/WorkflowToolConfigModal.tsx b/projects/app/src/pageComponents/config/tool/WorkflowToolConfigModal.tsx index 41c5bcebd67a..9f0a7fd40ed7 100644 --- a/projects/app/src/pageComponents/config/tool/WorkflowToolConfigModal.tsx +++ b/projects/app/src/pageComponents/config/tool/WorkflowToolConfigModal.tsx @@ -20,7 +20,6 @@ import { useToast } from '@fastgpt/web/hooks/useToast'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import { getPluginToolTags } from '@/web/core/plugin/toolTag/api'; import { useRequest } from '@fastgpt/web/hooks/useRequest'; -import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm'; import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; import { PluginStatusEnum, type PluginStatusType } from '@fastgpt/global/core/plugin/type'; @@ -34,13 +33,15 @@ import type { UpdateWorkflowToolBodyType } from '@fastgpt/global/openapi/core/plugin/admin/tool/api'; import { - delAdminSystemTool, getAdminAllSystemAppTool, getAdminSystemToolDetail, postAdminCreateAppTypeTool, putAdminUpdateWorkflowTool } from '@/web/core/plugin/admin/tool/api'; import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; +import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; +import CopyBox from '@fastgpt/web/components/common/String/CopyBox'; +import MyIcon from '@fastgpt/web/components/common/Icon'; export const defaultForm: UpdateWorkflowToolBodyType = { id: '', @@ -76,9 +77,15 @@ const WorkflowToolConfigModal = ({ const name = watch('name'); const avatar = watch('avatar'); const associatedPluginId = watch('associatedPluginId'); + const intro = watch('intro'); const currentCost = watch('currentCost'); const status = watch('status'); const hasTokenFee = watch('hasTokenFee'); + const isToolOffline = status === PluginStatusEnum.Offline; + const [toolVersion, setToolVersion] = useState(''); + const { openConfirm: openUninstallConfirm, ConfirmModal: UninstallConfirmModal } = useConfirm({ + type: 'delete' + }); React.useEffect(() => { setValue('tags', selectedTags); @@ -88,6 +95,7 @@ const WorkflowToolConfigModal = ({ async () => { if (toolId) { const res = await getAdminSystemToolDetail({ toolId }); + setToolVersion(res.version); const form: UpdateWorkflowToolBodyType = { id: res.id, status: res.status, @@ -141,6 +149,16 @@ const WorkflowToolConfigModal = ({ })) || [], [i18n.language, tags] ); + const pluginStatusSelectList = useMemo( + () => [ + { label: t('app:toolkit_status_normal'), value: PluginStatusEnum.Normal }, + { + label: t('app:toolkit_status_soon_offline'), + value: PluginStatusEnum.SoonOffline + } + ], + [t] + ); const currentApp = useMemo(() => { return apps.find((item) => item._id === associatedPluginId); @@ -210,301 +228,435 @@ const WorkflowToolConfigModal = ({ } ); - const { runAsync: onDelete, loading: isDeleting } = useRequest(delAdminSystemTool, { - onSuccess() { - toast({ - title: t('app:custom_plugin_delete_success'), - status: 'success' - }); - onSuccess(); - onClose(); + const { runAsync: onUninstall, loading: isUninstalling } = useRequest( + () => + putAdminUpdateWorkflowTool({ + id: toolId, + status: PluginStatusEnum.Offline + }), + { + successToast: t('app:custom_plugin_uninstall_success'), + onSuccess() { + onSuccess(); + onClose(); + } } - }); + ); + + const { runAsync: onReinstall, loading: isReinstalling } = useRequest( + () => + putAdminUpdateWorkflowTool({ + id: toolId, + status: PluginStatusEnum.Normal + }), + { + successToast: t('app:custom_plugin_install_success'), + onSuccess() { + onSuccess(); + onClose(); + } + } + ); + + const openToolUninstallConfirm = () => { + openUninstallConfirm({ + title: t('app:toolkit_uninstall'), + customContent: t('app:confirm_uninstall_tool'), + confirmText: t('app:toolkit_uninstall'), + confirmButtonVariant: 'dangerOutline', + inputConfirmText: name || toolId, + onConfirm: onUninstall + })(); + }; + + const offlineVersionInfoSection = ( + + + + {t('app:toolkit_version_info')} + + + + {t('app:toolkit_id')}: + + + {toolId} + + + + + + + + + + {t('app:toolkit_plugin_version')} + + + + width={'100%'} + h={9} + value={toolVersion || '-'} + list={[ + { + label: toolVersion || '-', + value: toolVersion || '-' + } + ]} + isDisabled + onChange={setToolVersion} + /> + + + + + {t('app:toolkit_plugin_name')} + + + {name || '-'} + + + + + {t('app:toolkit_plugin_intro')} + + + {intro || '-'} + + + + + ); return ( - - - - {t('app:custom_plugin_name_label')} - - - - - - - - - - {t('app:custom_plugin_intro_label')} - -