diff --git a/packages/global/openapi/core/plugin/marketplace/api.ts b/packages/global/openapi/core/plugin/marketplace/api.ts
index 56e5f5dfe073..ebf5a1bccc86 100644
--- a/packages/global/openapi/core/plugin/marketplace/api.ts
+++ b/packages/global/openapi/core/plugin/marketplace/api.ts
@@ -7,19 +7,16 @@ const formatToolDetailSchema = z.object({});
const formatToolSimpleSchema = z.object({});
// Create intersection types for extended schemas
-export const MarketplaceToolListItemSchema = formatToolSimpleSchema.extend({
- downloadUrl: z.string()
-});
+export const MarketplaceToolListItemSchema = formatToolSimpleSchema;
export type MarketplaceToolListItemType = ToolSimpleType & {
- downloadUrl: string;
+ downloadCount: number;
};
export const MarketplaceToolDetailItemSchema = formatToolDetailSchema.extend({
readme: z.string().optional()
});
export const MarketplaceToolDetailSchema = z.object({
- tools: z.array(MarketplaceToolDetailItemSchema),
- downloadUrl: z.string()
+ tools: z.array(MarketplaceToolDetailItemSchema)
});
// List
@@ -57,7 +54,12 @@ export type GetSystemInstalledPluginsQueryType = z.infer<
typeof GetSystemInstalledPluginsQuerySchema
>;
export const GetSystemInstalledPluginsResponseSchema = z.object({
- ids: z.array(z.string())
+ list: z.array(
+ z.object({
+ id: z.string(),
+ version: z.string()
+ })
+ )
});
export type GetSystemInstalledPluginsResponseType = z.infer<
typeof GetSystemInstalledPluginsResponseSchema
diff --git a/packages/global/package.json b/packages/global/package.json
index e0e775a420cd..4ca63017718f 100644
--- a/packages/global/package.json
+++ b/packages/global/package.json
@@ -2,7 +2,7 @@
"name": "@fastgpt/global",
"version": "1.0.0",
"dependencies": {
- "@fastgpt-sdk/plugin": "0.2.15",
+ "@fastgpt-sdk/plugin": "0.2.16",
"@apidevtools/swagger-parser": "^10.1.0",
"@bany/curl-to-json": "^1.2.8",
"axios": "^1.12.1",
diff --git a/packages/web/components/common/Icon/constants.ts b/packages/web/components/common/Icon/constants.ts
index dbc136653c5a..cdc8095080c5 100644
--- a/packages/web/components/common/Icon/constants.ts
+++ b/packages/web/components/common/Icon/constants.ts
@@ -45,6 +45,7 @@ export const iconPaths = {
'common/disable': () => import('./icons/common/disable.svg'),
'common/downArrowFill': () => import('./icons/common/downArrowFill.svg'),
'common/download': () => import('./icons/common/download.svg'),
+ 'common/downloadLine': () => import('./icons/common/downloadLine.svg'),
'common/edit': () => import('./icons/common/edit.svg'),
'common/editor/resizer': () => import('./icons/common/editor/resizer.svg'),
'common/ellipsis': () => import('./icons/common/ellipsis.svg'),
diff --git a/packages/web/components/common/Icon/icons/common/downloadLine.svg b/packages/web/components/common/Icon/icons/common/downloadLine.svg
new file mode 100644
index 000000000000..f8400f11d9db
--- /dev/null
+++ b/packages/web/components/common/Icon/icons/common/downloadLine.svg
@@ -0,0 +1,3 @@
+
diff --git a/packages/web/components/core/plugin/tool/ToolCard.tsx b/packages/web/components/core/plugin/tool/ToolCard.tsx
index af5556fedcd4..753a927c2a16 100644
--- a/packages/web/components/core/plugin/tool/ToolCard.tsx
+++ b/packages/web/components/core/plugin/tool/ToolCard.tsx
@@ -7,12 +7,6 @@ import MyIcon from '../../../common/Icon';
import { parseI18nString } from '@fastgpt/global/common/i18n/utils';
import { PluginStatusEnum } from '@fastgpt/global/core/plugin/type';
-/*
- 3 种使用场景:
- 1. admin 视角插件市场:显示是否安装,无状态,显示安装/卸载
- 2. team 视角资源库:显示是否安装,状态文本,以及安装/卸载
- 3. 开放的插件市场:不显示任何状态,只显示下载按钮
-*/
export type ToolCardItemType = {
id: string;
name: string;
@@ -23,21 +17,35 @@ export type ToolCardItemType = {
downloadUrl?: string;
status?: number;
installed?: boolean;
+ update?: boolean;
+ downloadCount?: number;
};
+/**
+ 3 种使用场景:
+ 1. admin 视角插件市场:显示是否安装,是否更新,无状态,显示安装/卸载
+ 2. team 视角资源库:显示是否安装,不显示更新,状态文本,以及安装/卸载
+ 3. 开放的插件市场:不显示任何状态,只显示下载按钮
+*/
const ToolCard = ({
item,
systemTitle,
- isLoading,
+ isInstallingOrDeleting,
+ isUpdating,
mode,
- onClickButton,
+ onInstall,
+ onDelete,
+ onUpdate,
onClickCard
}: {
item: ToolCardItemType;
systemTitle?: string;
- isLoading?: boolean;
+ isInstallingOrDeleting?: boolean;
+ isUpdating?: boolean;
mode: 'admin' | 'team' | 'marketplace';
- onClickButton: (installed: boolean) => void;
+ onInstall: () => Promise;
+ onDelete?: () => Promise;
+ onUpdate?: () => Promise;
onClickCard?: () => void;
}) => {
const { t, i18n } = useTranslation();
@@ -76,7 +84,7 @@ const ToolCard = ({
};
}, [item.tags]);
- const statusMap = useMemo(() => {
+ const statusLabel = useMemo(() => {
if (mode === 'marketplace') return null;
const pluginStatusMap: Record =
@@ -91,23 +99,27 @@ const ToolCard = ({
}
};
- const installedStatusMap = item.installed
- ? {
- label: t('app:toolkit_installed'),
- color: 'myGray.500',
- icon: 'common/check'
- }
- : null;
-
if (mode === 'admin') {
- return installedStatusMap;
+ return item.installed
+ ? {
+ label: t('app:toolkit_installed'),
+ color: 'myGray.500',
+ icon: 'common/check'
+ }
+ : null;
}
if (mode === 'team') {
if (item.status && pluginStatusMap[item.status]) {
return pluginStatusMap[item.status];
}
- return installedStatusMap;
+ return item.installed
+ ? {
+ label: t('app:toolkit_installed'),
+ color: 'myGray.500',
+ icon: 'common/check'
+ }
+ : null;
}
}, [item.installed, item.status]);
@@ -122,23 +134,77 @@ const ToolCard = ({
display={'flex'}
flexDirection={'column'}
cursor={onClickCard ? 'pointer' : 'default'}
- onClick={onClickCard}
+ position={'relative'}
+ onClick={() => {
+ if (isInstallingOrDeleting || isUpdating) return;
+ onClickCard?.();
+ }}
_hover={{
boxShadow: '0 4px 4px 0 rgba(19, 51, 107, 0.05), 0 0 1px 0 rgba(19, 51, 107, 0.08);',
'& .install-button': {
display: 'flex'
+ },
+ '& .update-button': {
+ display: 'flex'
+ },
+ // Only hide author info when there are multiple buttons
+ ...(item.update && mode === 'admin'
+ ? {
+ '& .author-info': {
+ display: 'none'
+ }
+ }
+ : {}),
+ '& .download-count': {
+ display: 'none'
}
}}
>
+ {/* Update badge in top-right corner */}
+ {item.update && mode === 'admin' && (
+
+
+ {t('app:app.modules.has new version')}
+
+ )}
+
{parseI18nString(item.name, i18n.language)}
- {statusMap && (
-
- {statusMap.icon && }
- {statusMap.label}
+ {statusLabel && (
+
+ {statusLabel.icon && }
+ {statusLabel.label}
)}
@@ -194,36 +260,77 @@ const ToolCard = ({
- {`by ${item.author || systemTitle || 'FastGPT'}`}
- {mode === 'marketplace' ? (
-
- ) : (
-
- )}
+ {`by ${item.author || systemTitle || 'FastGPT'}`}
+ {/*TODO: when statistics is ready*/}
+ {/*
+
+ {!item.downloadCount
+ ? 0
+ : item.downloadCount < 1000
+ ? `${item.downloadCount}`
+ : `${(item.downloadCount / 1000).toFixed(1)}k`}
+ */}
+
+
+ {mode === 'marketplace' ? (
+
+ ) : (
+
+ )}
+
+ {/* Update button for admin mode when update is available */}
+ {item.update && mode === 'admin' && onUpdate && (
+
+ )}
+
);
diff --git a/packages/web/components/core/plugin/tool/ToolDetailDrawer.tsx b/packages/web/components/core/plugin/tool/ToolDetailDrawer.tsx
index fcb0dc6a453b..dbdd1797c773 100644
--- a/packages/web/components/core/plugin/tool/ToolDetailDrawer.tsx
+++ b/packages/web/components/core/plugin/tool/ToolDetailDrawer.tsx
@@ -163,6 +163,8 @@ const ToolDetailDrawer = ({
onClose,
selectedTool,
onToggleInstall,
+ onUpdate,
+ isUpdating,
systemTitle,
onFetchDetail,
isLoading,
@@ -172,6 +174,8 @@ const ToolDetailDrawer = ({
onClose: () => void;
selectedTool: ToolCardItemType;
onToggleInstall: (installed: boolean) => void;
+ onUpdate?: () => void;
+ isUpdating?: boolean;
systemTitle?: string;
onFetchDetail?: (toolId: string) => Promise;
isLoading?: boolean;
@@ -321,23 +325,39 @@ const ToolDetailDrawer = ({
{`by ${parentTool?.author || systemTitle || 'FastGPT'}`}
-
-
+
+ {/* Determine if we have two buttons */}
+ {(() => {
+ const hasUpdateButton = selectedTool.update && onUpdate && mode !== 'marketplace';
+ const buttonFlex = hasUpdateButton ? 1 : 1; // Both use flex=1, but when single button it fills the space
+
+ return (
+ <>
+
+ {hasUpdateButton && (
+
+ )}
+ >
+ );
+ })()}
{showPoint && (
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d5df2b40e6c0..0a8c3753b950 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -72,8 +72,8 @@ importers:
specifier: ^1.2.8
version: 1.2.8
'@fastgpt-sdk/plugin':
- specifier: 0.2.15
- version: 0.2.15(@types/node@20.14.0)
+ specifier: 0.2.16
+ version: 0.2.16(@types/node@20.14.0)
axios:
specifier: ^1.12.1
version: 1.12.1
@@ -753,6 +753,9 @@ importers:
i18next:
specifier: 23.16.8
version: 23.16.8
+ mongoose:
+ specifier: ^8.10.1
+ version: 8.12.1(socks@2.8.4)
next:
specifier: 14.2.33
version: 14.2.33(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1)
@@ -2332,8 +2335,8 @@ packages:
resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
- '@fastgpt-sdk/plugin@0.2.15':
- resolution: {integrity: sha512-z2JMoQ02gDHDM3feWemY10GyI9g0AH9g1IUAuqSfThkmPMhdLGsB4B6V6r2aSCUCb9FbdNqJblzriPYH9VtP9w==}
+ '@fastgpt-sdk/plugin@0.2.16':
+ resolution: {integrity: sha512-tEpwtSLqPJCvvEk+KpdlgCl2N7n3m5ew2C9/hFKt+ahBY+eaU4WVrblJ6idSIImZ9lqOjOW4Mq7U1lO1mq06mQ==}
'@fastify/accept-negotiator@1.1.0':
resolution: {integrity: sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==}
@@ -13061,7 +13064,7 @@ snapshots:
'@eslint/js@8.57.1': {}
- '@fastgpt-sdk/plugin@0.2.15(@types/node@20.14.0)':
+ '@fastgpt-sdk/plugin@0.2.16(@types/node@20.14.0)':
dependencies:
'@fortaine/fetch-event-source': 3.0.6
'@ts-rest/core': 3.52.1(@types/node@20.14.0)(zod@3.25.76)
@@ -17817,8 +17820,8 @@ snapshots:
'@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.8.2)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
- eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.31.0)(eslint@8.57.1)
- eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.9.0)(eslint@8.57.1)
+ eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1)
+ eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
eslint-plugin-react: 7.37.4(eslint@8.57.1)
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1)
@@ -17837,6 +17840,21 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1):
+ dependencies:
+ '@nolyfill/is-core-module': 1.0.39
+ debug: 4.4.0
+ eslint: 8.57.1
+ get-tsconfig: 4.10.0
+ is-bun-module: 1.3.0
+ oxc-resolver: 5.0.0
+ stable-hash: 0.0.5
+ tinyglobby: 0.2.12
+ optionalDependencies:
+ eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
+ transitivePeerDependencies:
+ - supports-color
+
eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0)(eslint@8.56.0):
dependencies:
'@nolyfill/is-core-module': 1.0.39
@@ -17878,6 +17896,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
+ dependencies:
+ debug: 3.2.7
+ optionalDependencies:
+ '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.8.2)
+ eslint: 8.57.1
+ eslint-import-resolver-node: 0.3.9
+ eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1)
+ transitivePeerDependencies:
+ - supports-color
+
eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@8.57.1):
dependencies:
debug: 3.2.7
@@ -17918,6 +17947,35 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
+ eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
+ dependencies:
+ '@rtsao/scc': 1.1.0
+ array-includes: 3.1.8
+ array.prototype.findlastindex: 1.2.6
+ array.prototype.flat: 1.3.3
+ array.prototype.flatmap: 1.3.3
+ debug: 3.2.7
+ doctrine: 2.1.0
+ eslint: 8.57.1
+ eslint-import-resolver-node: 0.3.9
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
+ hasown: 2.0.2
+ is-core-module: 2.16.1
+ is-glob: 4.0.3
+ minimatch: 3.1.2
+ object.fromentries: 2.0.8
+ object.groupby: 1.0.3
+ object.values: 1.2.1
+ semver: 6.3.1
+ string.prototype.trimend: 1.0.9
+ tsconfig-paths: 3.15.0
+ optionalDependencies:
+ '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.8.2)
+ transitivePeerDependencies:
+ - eslint-import-resolver-typescript
+ - eslint-import-resolver-webpack
+ - supports-color
+
eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.9.0)(eslint@8.57.1):
dependencies:
'@rtsao/scc': 1.1.0
diff --git a/projects/app/src/pages/api/core/plugin/admin/marketplace/installed.ts b/projects/app/src/pages/api/core/plugin/admin/marketplace/installed.ts
index 6006318a3f1f..a6faa1b6e331 100644
--- a/projects/app/src/pages/api/core/plugin/admin/marketplace/installed.ts
+++ b/projects/app/src/pages/api/core/plugin/admin/marketplace/installed.ts
@@ -26,7 +26,10 @@ async function handler(
const tools = await APIGetSystemToolList();
return {
- ids: tools.map((tool) => tool.id.replace(`${AppToolSourceEnum.systemTool}-`, ''))
+ list: tools.map((tool) => ({
+ id: tool.id.replace(`${AppToolSourceEnum.systemTool}-`, ''),
+ version: tool.version
+ }))
};
}
diff --git a/projects/app/src/pages/config/tool/marketplace.tsx b/projects/app/src/pages/config/tool/marketplace.tsx
index 3a3ca6fb02a1..7bbb9f5bb38c 100644
--- a/projects/app/src/pages/config/tool/marketplace.tsx
+++ b/projects/app/src/pages/config/tool/marketplace.tsx
@@ -9,7 +9,7 @@ import MyBox from '@fastgpt/web/components/common/MyBox';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useState, useMemo, useRef, useEffect, useCallback, useReducer } from 'react';
-import { useDebounce, useMount } from 'ahooks';
+import { useDebounce, useMount, useSet } from 'ahooks';
import ToolCard, { type ToolCardItemType } from '@fastgpt/web/components/core/plugin/tool/ToolCard';
import ToolTagFilterBox from '@fastgpt/web/components/core/plugin/tool/TagFilterBox';
import ToolDetailDrawer from '@fastgpt/web/components/core/plugin/tool/ToolDetailDrawer';
@@ -19,6 +19,7 @@ import { intallPluginWithUrl } from '@/web/core/plugin/admin/api';
import { deletePkgPlugin } from '@/web/core/plugin/admin/api';
import {
getMarketPlaceToolTags,
+ getMarketplaceDownloadURL,
getMarketplaceToolDetail,
getMarketplaceTools,
getSystemInstalledPlugins
@@ -60,28 +61,6 @@ const useSearchParams = () => {
return { searchText, tagIds, updateParams };
};
-type OperatingAction = { type: 'TRY_ADD'; toolId: string } | { type: 'REMOVE'; toolId: string };
-
-const operatingReducer = (state: Set, action: OperatingAction): Set => {
- if (action.type === 'TRY_ADD') {
- if (state.has(action.toolId)) {
- return state;
- }
- const newSet = new Set(state);
- newSet.add(action.toolId);
- return newSet;
- }
- if (action.type === 'REMOVE') {
- if (!state.has(action.toolId)) {
- return state;
- }
- const newSet = new Set(state);
- newSet.delete(action.toolId);
- return newSet;
- }
- return state;
-};
-
const ToolkitMarketplace = ({ marketplaceUrl }: { marketplaceUrl: string }) => {
const { t, i18n } = useTranslation();
const router = useRouter();
@@ -92,7 +71,8 @@ const ToolkitMarketplace = ({ marketplaceUrl }: { marketplaceUrl: string }) => {
const { searchText, tagIds, updateParams } = useSearchParams();
const [selectedTool, setSelectedTool] = useState(null);
- const [operatingToolIds, dispatchOperating] = useReducer(operatingReducer, new Set());
+ const [installingOrDeletingToolIds, installingOrDeletingToolIdsDispatch] = useSet();
+ const [updatingToolIds, updatingToolIdsDispatch] = useSet();
const operatingPromisesRef = useRef