From a0c42a5f6ee43cad5fbe1d3932510922f98d3ce9 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 17 Aug 2025 16:51:44 +0800 Subject: [PATCH] feat: plugin operations --- pyproject.toml | 2 +- web/package.json | 1 + .../plugins/plugin-installed/PluginCardVO.ts | 11 +- .../PluginInstalledComponent.tsx | 140 +++++++- .../plugin-card/PluginCardComponent.tsx | 310 +++++++++++------- .../plugin-form/PluginForm.tsx | 131 -------- web/src/app/infra/entities/plugin/index.ts | 12 +- web/src/i18n/locales/zh-Hans.ts | 5 +- 8 files changed, 343 insertions(+), 269 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c371f1c9..d960936e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ dependencies = [ "pre-commit>=4.2.0", "uv>=0.7.11", "mypy>=1.16.0", - "langbot-plugin==0.1.1a1", + "langbot-plugin==0.1.1a2", ] keywords = [ "bot", diff --git a/web/package.json b/web/package.json index 36b25dae..952a6fe6 100644 --- a/web/package.json +++ b/web/package.json @@ -19,6 +19,7 @@ "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@hookform/resolvers": "^5.0.1", "@radix-ui/react-checkbox": "^1.3.1", "@radix-ui/react-dialog": "^1.1.14", diff --git a/web/src/app/home/plugins/plugin-installed/PluginCardVO.ts b/web/src/app/home/plugins/plugin-installed/PluginCardVO.ts index 11ce2154..e230bec4 100644 --- a/web/src/app/home/plugins/plugin-installed/PluginCardVO.ts +++ b/web/src/app/home/plugins/plugin-installed/PluginCardVO.ts @@ -1,3 +1,5 @@ +import { PluginComponent } from '@/app/infra/entities/plugin'; + export interface IPluginCardVO { author: string; name: string; @@ -8,8 +10,7 @@ export interface IPluginCardVO { install_source: string; install_info: Record; // eslint-disable-line @typescript-eslint/no-explicit-any status: string; - tools: object[]; - event_handlers: object; + components: PluginComponent[]; debug: boolean; } @@ -24,18 +25,16 @@ export class PluginCardVO implements IPluginCardVO { install_source: string; install_info: Record; // eslint-disable-line @typescript-eslint/no-explicit-any status: string; - tools: object[]; - event_handlers: object; + components: PluginComponent[]; constructor(prop: IPluginCardVO) { this.author = prop.author; this.description = prop.description; this.enabled = prop.enabled; - this.event_handlers = prop.event_handlers; + this.components = prop.components; this.name = prop.name; this.priority = prop.priority; this.status = prop.status; - this.tools = prop.tools; this.version = prop.version; this.debug = prop.debug; this.install_source = prop.install_source; diff --git a/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx b/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx index 732e15ab..81428e36 100644 --- a/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx +++ b/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx @@ -11,14 +11,24 @@ import { DialogContent, DialogHeader, DialogTitle, + DialogDescription, + DialogFooter, } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; import { useTranslation } from 'react-i18next'; import { extractI18nObject } from '@/i18n/I18nProvider'; +import { toast } from 'sonner'; export interface PluginInstalledComponentRef { refreshPluginList: () => void; } +enum PluginRemoveStatus { + WAIT_INPUT = 'WAIT_INPUT', + REMOVING = 'REMOVING', + ERROR = 'ERROR', +} + // eslint-disable-next-line react/display-name const PluginInstalledComponent = forwardRef( (props, ref) => { @@ -28,6 +38,15 @@ const PluginInstalledComponent = forwardRef( const [selectedPlugin, setSelectedPlugin] = useState( null, ); + const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false); + const [pluginRemoveStatus, setPluginRemoveStatus] = + useState(PluginRemoveStatus.WAIT_INPUT); + const [pluginRemoveError, setPluginRemoveError] = useState( + null, + ); + const [pluginToDelete, setPluginToDelete] = useState( + null, + ); useEffect(() => { initData(); @@ -55,8 +74,7 @@ const PluginInstalledComponent = forwardRef( name: plugin.manifest.manifest.metadata.name, version: plugin.manifest.manifest.metadata.version ?? '', status: plugin.status, - tools: [], - event_handlers: {}, + components: plugin.components, priority: plugin.priority, install_source: plugin.install_source, install_info: plugin.install_info, @@ -75,8 +93,125 @@ const PluginInstalledComponent = forwardRef( setModalOpen(true); } + function handlePluginDelete(plugin: PluginCardVO) { + setPluginToDelete(plugin); + setShowDeleteConfirmModal(true); + setPluginRemoveStatus(PluginRemoveStatus.WAIT_INPUT); + } + + function deletePlugin() { + setPluginRemoveStatus(PluginRemoveStatus.REMOVING); + httpClient + .removePlugin(pluginToDelete!.author, pluginToDelete!.name) + .then((res) => { + const taskId = res.task_id; + + let alreadySuccess = false; + + const interval = setInterval(() => { + httpClient.getAsyncTask(taskId).then((res) => { + if (res.runtime.done) { + clearInterval(interval); + if (res.runtime.exception) { + setPluginRemoveError(res.runtime.exception); + setPluginRemoveStatus(PluginRemoveStatus.ERROR); + } else { + // success + if (!alreadySuccess) { + toast.success('插件删除成功'); + alreadySuccess = true; + } + setPluginRemoveStatus(PluginRemoveStatus.WAIT_INPUT); + setShowDeleteConfirmModal(false); + } + } + }); + }, 1000); + }) + .catch((error) => { + setPluginRemoveError(error.message); + setPluginRemoveStatus(PluginRemoveStatus.ERROR); + }); + } + return ( <> + { + if (!open) { + setShowDeleteConfirmModal(false); + setPluginRemoveStatus(PluginRemoveStatus.WAIT_INPUT); + setPluginToDelete(null); + } + }} + > + + + {t('plugins.deleteConfirm')} + + + {pluginRemoveStatus === PluginRemoveStatus.WAIT_INPUT && ( +
+ {t('plugins.confirmDeletePlugin', { + author: pluginToDelete?.author ?? '', + name: pluginToDelete?.name ?? '', + })} +
+ )} + {pluginRemoveStatus === PluginRemoveStatus.REMOVING && ( +
{t('plugins.deleting')}
+ )} + {pluginRemoveStatus === PluginRemoveStatus.ERROR && ( +
+ {t('plugins.deleteError')} +
{pluginRemoveError}
+
+ )} +
+ + {pluginRemoveStatus === PluginRemoveStatus.WAIT_INPUT && ( + + )} + {pluginRemoveStatus === PluginRemoveStatus.WAIT_INPUT && ( + + )} + {pluginRemoveStatus === PluginRemoveStatus.REMOVING && ( + + )} + {pluginRemoveStatus === PluginRemoveStatus.ERROR && ( + + )} + +
+
+ {pluginList.length === 0 ? (
( handlePluginClick(vo)} + onDeleteClick={() => handlePluginDelete(vo)} />
); diff --git a/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx b/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx index 0aae5f39..cb6fb7a7 100644 --- a/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx +++ b/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx @@ -5,15 +5,85 @@ import { Badge } from '@/components/ui/badge'; import { Switch } from '@/components/ui/switch'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; -import { BugIcon, ExternalLink } from 'lucide-react'; +import { TFunction } from 'i18next'; +import { + AudioWaveform, + Wrench, + Hash, + BugIcon, + ExternalLink, + Ellipsis, + Trash, +} from 'lucide-react'; import { getCloudServiceClientSync } from '@/app/infra/http'; +import { PluginComponent } from '@/app/infra/entities/plugin'; +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; + +enum PluginRemoveStatus { + WAIT_INPUT = 'WAIT_INPUT', + REMOVING = 'REMOVING', + ERROR = 'ERROR', +} + +function getComponentList(components: PluginComponent[], t: TFunction) { + const componentKindCount: Record = {}; + + for (const component of components) { + const kind = component.manifest.manifest.kind; + if (componentKindCount[kind]) { + componentKindCount[kind]++; + } else { + componentKindCount[kind] = 1; + } + } + + const kindIconMap: Record = { + Tool: , + EventListener: , + Command: , + }; + + const componentKindList = Object.keys(componentKindCount); + + return ( + <> +
{t('plugins.componentsList')}
+ {componentKindList.length > 0 && ( + <> + {componentKindList.map((kind) => { + return ( +
+ {kindIconMap[kind]} {componentKindCount[kind]} +
+ ); + })} + + )} + + {componentKindList.length === 0 &&
{t('plugins.noComponents')}
} + + ); +} export default function PluginCardComponent({ cardVO, onCardClick, + onDeleteClick, }: { cardVO: PluginCardVO; onCardClick: () => void; + onDeleteClick: (cardVO: PluginCardVO) => void; }) { const { t } = useTranslation(); const [enabled, setEnabled] = useState(cardVO.enabled); @@ -34,143 +104,137 @@ export default function PluginCardComponent({ setSwitchEnable(true); }); } + return ( -
-
- - - + <> +
+
+ + + -
-
+
-
- {cardVO.author} /{' '} -
-
-
{cardVO.name}
- - v{cardVO.version} - - {cardVO.debug && ( - - - {t('plugins.debugging')} +
+
+ {cardVO.author} /{' '} +
+
+
{cardVO.name}
+ + v{cardVO.version} - )} - {!cardVO.debug && ( - <> - {cardVO.install_source === 'github' && ( - { - e.stopPropagation(); - window.open(cardVO.install_info.github_url, '_blank'); - }} - > - {t('plugins.fromGithub')} - - - )} - {cardVO.install_source === 'local' && ( - - {t('plugins.fromLocal')} - - )} - {cardVO.install_source === 'marketplace' && ( - { - e.stopPropagation(); - window.open( - getCloudServiceClientSync().getPluginMarketplaceURL( - cardVO.author, - cardVO.name, - ), - '_blank', - ); - }} - > - {t('plugins.fromMarketplace')} - - - )} - - )} + {cardVO.debug && ( + + + {t('plugins.debugging')} + + )} + {!cardVO.debug && ( + <> + {cardVO.install_source === 'github' && ( + { + e.stopPropagation(); + window.open( + cardVO.install_info.github_url, + '_blank', + ); + }} + > + {t('plugins.fromGithub')} + + + )} + {cardVO.install_source === 'local' && ( + + {t('plugins.fromLocal')} + + )} + {cardVO.install_source === 'marketplace' && ( + { + e.stopPropagation(); + window.open( + getCloudServiceClientSync().getPluginMarketplaceURL( + cardVO.author, + cardVO.name, + ), + '_blank', + ); + }} + > + {t('plugins.fromMarketplace')} + + + )} + + )} +
+
+ +
+ {cardVO.description}
-
- {cardVO.description} +
+ {getComponentList(cardVO.components, t)}
-
-
- - - -
- {t('plugins.eventCount', { - count: Object.keys(cardVO.event_handlers).length, - })} -
+
+
+ handleEnable(e)} + disabled={!switchEnable} + />
-
- - - -
- {t('plugins.toolCount', { count: cardVO.tools.length })} -
+
+ + + + + + { + onDeleteClick(cardVO); + e.stopPropagation(); + }} + > + + {t('plugins.delete')} + + +
- -
-
- handleEnable(e)} - disabled={!switchEnable} - /> -
- -
- {/* */} -
-
-
+ ); } diff --git a/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx b/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx index 081253b6..909e87c2 100644 --- a/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx +++ b/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx @@ -16,12 +16,6 @@ import { toast } from 'sonner'; import { extractI18nObject } from '@/i18n/I18nProvider'; import { useTranslation } from 'react-i18next'; -enum PluginRemoveStatus { - WAIT_INPUT = 'WAIT_INPUT', - REMOVING = 'REMOVING', - ERROR = 'ERROR', -} - export default function PluginForm({ pluginAuthor, pluginName, @@ -38,13 +32,6 @@ export default function PluginForm({ const [pluginConfig, setPluginConfig] = useState(); const [isSaving, setIsLoading] = useState(false); - const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false); - const [pluginRemoveStatus, setPluginRemoveStatus] = - useState(PluginRemoveStatus.WAIT_INPUT); - const [pluginRemoveError, setPluginRemoveError] = useState( - null, - ); - useEffect(() => { // 获取插件信息 httpClient.getPlugin(pluginAuthor, pluginName).then((res) => { @@ -76,113 +63,8 @@ export default function PluginForm({ return
{t('plugins.loading')}
; } - function deletePlugin() { - setPluginRemoveStatus(PluginRemoveStatus.REMOVING); - httpClient - .removePlugin(pluginAuthor, pluginName) - .then((res) => { - const taskId = res.task_id; - - let alreadySuccess = false; - - const interval = setInterval(() => { - httpClient.getAsyncTask(taskId).then((res) => { - if (res.runtime.done) { - clearInterval(interval); - if (res.runtime.exception) { - setPluginRemoveError(res.runtime.exception); - setPluginRemoveStatus(PluginRemoveStatus.ERROR); - } else { - // success - if (!alreadySuccess) { - toast.success('插件删除成功'); - alreadySuccess = true; - } - setPluginRemoveStatus(PluginRemoveStatus.WAIT_INPUT); - setShowDeleteConfirmModal(false); - onFormSubmit(); - } - } - }); - }, 1000); - }) - .catch((error) => { - setPluginRemoveError(error.message); - setPluginRemoveStatus(PluginRemoveStatus.ERROR); - }); - } - return (
- - - - {t('plugins.deleteConfirm')} - - - {pluginRemoveStatus === PluginRemoveStatus.WAIT_INPUT && ( -
- {t('plugins.confirmDeletePlugin', { - author: pluginAuthor, - name: pluginName, - })} -
- )} - {pluginRemoveStatus === PluginRemoveStatus.REMOVING && ( -
{t('plugins.deleting')}
- )} - {pluginRemoveStatus === PluginRemoveStatus.ERROR && ( -
- {t('plugins.deleteError')} -
{pluginRemoveError}
-
- )} -
- - {pluginRemoveStatus === PluginRemoveStatus.WAIT_INPUT && ( - - )} - {pluginRemoveStatus === PluginRemoveStatus.WAIT_INPUT && ( - - )} - {pluginRemoveStatus === PluginRemoveStatus.REMOVING && ( - - )} - {pluginRemoveStatus === PluginRemoveStatus.ERROR && ( - - )} - -
-
-
{extractI18nObject(pluginInfo.manifest.manifest.metadata.label)} @@ -220,19 +102,6 @@ export default function PluginForm({
- -