Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/global/openapi/core/plugin/marketplace/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const MarketplaceToolListItemSchema = formatToolSimpleSchema.extend({
});
export type MarketplaceToolListItemType = ToolSimpleType & {
downloadUrl: string;
downloadCount: number;
Comment on lines 14 to +15
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type inconsistency: The schema still includes downloadUrl: string in the type definition (line 14), but this property is being replaced with downloadCount: number according to the changes in the API response. The type definition should be updated to reflect this change: remove downloadUrl and ensure downloadCount is the only download-related property, or both should be present if both are needed.

Copilot uses AI. Check for mistakes.
};

export const MarketplaceToolDetailItemSchema = formatToolDetailSchema.extend({
Expand Down Expand Up @@ -57,7 +58,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
Expand Down
2 changes: 1 addition & 1 deletion packages/global/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/web/components/common/Icon/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
219 changes: 163 additions & 56 deletions packages/web/components/core/plugin/tool/ToolCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<void>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不会都有 install 把?

onDelete?: () => Promise<void>;
onUpdate?: () => Promise<void>;
Comment on lines +46 to +48
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The onInstall prop type should return void | Promise<void> for consistency with onDelete and onUpdate, or all three should consistently return Promise<void>. Currently, only onInstall returns Promise<void> while the others have optional ? modifiers, which creates inconsistency in the API design.

Copilot uses AI. Check for mistakes.
onClickCard?: () => void;
}) => {
const { t, i18n } = useTranslation();
Expand Down Expand Up @@ -76,7 +84,7 @@ const ToolCard = ({
};
}, [item.tags]);

const statusMap = useMemo(() => {
const statusLabel = useMemo(() => {
if (mode === 'marketplace') return null;

const pluginStatusMap: Record<number, { label: string; color: string; icon?: string } | null> =
Expand All @@ -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]);

Expand All @@ -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' && (
<Flex
alignItems="center"
position={'absolute'}
top={4}
right={4}
px={2}
py={0.5}
bg={'rgb(255, 247, 237)'}
color={'rgba(234,88,12,1)'}
fontSize={'12px'}
fontWeight={'medium'}
borderRadius={'0.5rem'}
borderColor={'rgba(255,237,213,1)'}
borderWidth={'1px'}
zIndex={1}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="12"
height="12"
viewBox="0 0 24 24"
fill="currentColor"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"></path>
</svg>
{t('app:app.modules.has new version')}
</Flex>
)}

<HStack>
<Avatar src={item.icon} borderRadius={'sm'} w={'1.5rem'} />
<Box color={'myGray.900'} fontWeight={'medium'}>
{parseI18nString(item.name, i18n.language)}
</Box>
{statusMap && (
<Flex fontSize={'12px'} fontWeight={'medium'} color={statusMap.color} gap={1}>
{statusMap.icon && <MyIcon name={statusMap.icon as any} w={4} />}
{statusMap.label}
{statusLabel && (
<Flex fontSize={'12px'} fontWeight={'medium'} color={statusLabel.color} gap={1}>
{statusLabel.icon && <MyIcon name={statusLabel.icon as any} w={4} />}
{statusLabel.label}
</Flex>
)}
</HStack>
Expand Down Expand Up @@ -194,36 +260,77 @@ const ToolCard = ({
</Flex>

<Flex w={'full'} fontSize={'mini'} alignItems={'end'} justifyContent={'space-between'}>
<Box color={'myGray.500'} mt={3}>{`by ${item.author || systemTitle || 'FastGPT'}`}</Box>
{mode === 'marketplace' ? (
<Button
className="install-button"
size={'sm'}
variant={'primary'}
onClick={(e) => {
e.stopPropagation();
onClickButton(false);
}}
isLoading={isLoading}
{...(!isLoading ? { display: 'none' } : {})}
>
{t('common:Download')}
</Button>
) : (
<Button
className="install-button"
{...(!isLoading ? { display: 'none' } : {})}
size={'sm'}
variant={item.installed ? 'primaryOutline' : 'primary'}
onClick={(e) => {
e.stopPropagation();
onClickButton(!item.installed);
}}
isLoading={isLoading}
>
{item.installed ? t('app:toolkit_uninstall') : t('app:toolkit_install')}
</Button>
)}
<Box
className="author-info"
color={'myGray.500'}
mt={3}
>{`by ${item.author || systemTitle || 'FastGPT'}`}</Box>
{/*TODO: when statistics is ready*/}
{/*<Flex flexDirection={'row'} gap={1} className="download-count" color={'myGray.500'} mt={3}>
<MyIcon name="common/downloadLine" />
{!item.downloadCount
? 0
: item.downloadCount < 1000
? `${item.downloadCount}`
: `${(item.downloadCount / 1000).toFixed(1)}k`}
</Flex>*/}

<Flex gap={2} alignItems={'center'} ml={'auto'}>
{mode === 'marketplace' ? (
<Button
className="install-button"
size={'sm'}
variant={'primary'}
onClick={(e) => {
e.stopPropagation();
onInstall();
}}
isLoading={isInstallingOrDeleting}
{...(!isInstallingOrDeleting ? { display: 'none' } : {})}
>
{t('common:Download')}
</Button>
) : (
<Button
className="install-button"
size={'sm'}
variant={item.installed ? 'primaryOutline' : 'primary'}
onClick={async (e) => {
e.stopPropagation();
if (item.installed) {
// delete
if (onDelete) {
return onDelete();
}
} else {
return onInstall();
}
}}
isLoading={isInstallingOrDeleting}
{...(!isInstallingOrDeleting ? { display: 'none' } : {})}
disabled={isUpdating}
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The disabled attribute should use isDisabled to follow Chakra UI conventions consistently (the same prop is used correctly on line 340).

Copilot uses AI. Check for mistakes.
>
{item.installed ? t('app:toolkit_uninstall') : t('app:toolkit_install')}
</Button>
)}

{/* Update button for admin mode when update is available */}
{item.update && mode === 'admin' && (
<Button
className="update-button"
size={'sm'}
variant={'primary'}
onClick={async (e) => {
e.stopPropagation();
return onUpdate?.();
Comment on lines +318 to +325
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The onUpdate prop is optional but the onUpdate?.() call on line 325 could be invoked when hovering over a card that doesn't have the update functionality. Consider adding a guard check: {item.update && mode === 'admin' && onUpdate && ...} to ensure the button is only rendered when all conditions are met.

Copilot uses AI. Check for mistakes.
}}
isLoading={isUpdating}
display={'none'}
>
{t('app:custom_plugin_update')}
</Button>
)}
</Flex>
</Flex>
</MyBox>
);
Expand Down
Loading
Loading