mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
feat: plugin operations
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<string, any>; // 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<string, any>; // 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;
|
||||
|
||||
@@ -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<PluginInstalledComponentRef>(
|
||||
(props, ref) => {
|
||||
@@ -28,6 +38,15 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
const [selectedPlugin, setSelectedPlugin] = useState<PluginCardVO | null>(
|
||||
null,
|
||||
);
|
||||
const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false);
|
||||
const [pluginRemoveStatus, setPluginRemoveStatus] =
|
||||
useState<PluginRemoveStatus>(PluginRemoveStatus.WAIT_INPUT);
|
||||
const [pluginRemoveError, setPluginRemoveError] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [pluginToDelete, setPluginToDelete] = useState<PluginCardVO | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
initData();
|
||||
@@ -55,8 +74,7 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
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<PluginInstalledComponentRef>(
|
||||
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 (
|
||||
<>
|
||||
<Dialog
|
||||
open={showDeleteConfirmModal}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setShowDeleteConfirmModal(false);
|
||||
setPluginRemoveStatus(PluginRemoveStatus.WAIT_INPUT);
|
||||
setPluginToDelete(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('plugins.deleteConfirm')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription>
|
||||
{pluginRemoveStatus === PluginRemoveStatus.WAIT_INPUT && (
|
||||
<div>
|
||||
{t('plugins.confirmDeletePlugin', {
|
||||
author: pluginToDelete?.author ?? '',
|
||||
name: pluginToDelete?.name ?? '',
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{pluginRemoveStatus === PluginRemoveStatus.REMOVING && (
|
||||
<div>{t('plugins.deleting')}</div>
|
||||
)}
|
||||
{pluginRemoveStatus === PluginRemoveStatus.ERROR && (
|
||||
<div>
|
||||
{t('plugins.deleteError')}
|
||||
<div className="text-red-500">{pluginRemoveError}</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogDescription>
|
||||
<DialogFooter>
|
||||
{pluginRemoveStatus === PluginRemoveStatus.WAIT_INPUT && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setShowDeleteConfirmModal(false);
|
||||
setPluginRemoveStatus(PluginRemoveStatus.WAIT_INPUT);
|
||||
setPluginToDelete(null);
|
||||
}}
|
||||
>
|
||||
{t('plugins.cancel')}
|
||||
</Button>
|
||||
)}
|
||||
{pluginRemoveStatus === PluginRemoveStatus.WAIT_INPUT && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
deletePlugin();
|
||||
}}
|
||||
>
|
||||
{t('plugins.confirmDelete')}
|
||||
</Button>
|
||||
)}
|
||||
{pluginRemoveStatus === PluginRemoveStatus.REMOVING && (
|
||||
<Button variant="destructive" disabled>
|
||||
{t('plugins.deleting')}
|
||||
</Button>
|
||||
)}
|
||||
{pluginRemoveStatus === PluginRemoveStatus.ERROR && (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
setShowDeleteConfirmModal(false);
|
||||
setPluginRemoveStatus(PluginRemoveStatus.WAIT_INPUT);
|
||||
}}
|
||||
>
|
||||
{t('plugins.close')}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{pluginList.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center text-gray-500 h-[calc(100vh-16rem)] w-full gap-2">
|
||||
<svg
|
||||
@@ -120,6 +255,7 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
<PluginCardComponent
|
||||
cardVO={vo}
|
||||
onCardClick={() => handlePluginClick(vo)}
|
||||
onDeleteClick={() => handlePluginDelete(vo)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<string, number> = {};
|
||||
|
||||
for (const component of components) {
|
||||
const kind = component.manifest.manifest.kind;
|
||||
if (componentKindCount[kind]) {
|
||||
componentKindCount[kind]++;
|
||||
} else {
|
||||
componentKindCount[kind] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
const kindIconMap: Record<string, React.ReactNode> = {
|
||||
Tool: <Wrench className="w-5 h-5" />,
|
||||
EventListener: <AudioWaveform className="w-5 h-5" />,
|
||||
Command: <Hash className="w-5 h-5" />,
|
||||
};
|
||||
|
||||
const componentKindList = Object.keys(componentKindCount);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>{t('plugins.componentsList')}</div>
|
||||
{componentKindList.length > 0 && (
|
||||
<>
|
||||
{componentKindList.map((kind) => {
|
||||
return (
|
||||
<div
|
||||
key={kind}
|
||||
className="flex flex-row items-center justify-start gap-[0.4rem]"
|
||||
>
|
||||
{kindIconMap[kind]} {componentKindCount[kind]}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{componentKindList.length === 0 && <div>{t('plugins.noComponents')}</div>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div
|
||||
className="w-[100%] h-[10rem] bg-white rounded-[10px] shadow-[0px_2px_2px_0_rgba(0,0,0,0.2)] p-[1.2rem] cursor-pointer"
|
||||
onClick={onCardClick}
|
||||
>
|
||||
<div className="w-full h-full flex flex-row items-start justify-start gap-[1.2rem]">
|
||||
<svg
|
||||
className="w-16 h-16 text-[#2288ee]"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M8 4C8 2.34315 9.34315 1 11 1C12.6569 1 14 2.34315 14 4C14 4.35064 13.9398 4.68722 13.8293 5H18C18.5523 5 19 5.44772 19 6V10.1707C19.3128 10.0602 19.6494 10 20 10C21.6569 10 23 11.3431 23 13C23 14.6569 21.6569 16 20 16C19.6494 16 19.3128 15.9398 19 15.8293V20C19 20.5523 18.5523 21 18 21H4C3.44772 21 3 20.5523 3 20V6C3 5.44772 3.44772 5 4 5H8.17071C8.06015 4.68722 8 4.35064 8 4Z"></path>
|
||||
</svg>
|
||||
<>
|
||||
<div
|
||||
className="w-[100%] h-[10rem] bg-white rounded-[10px] shadow-[0px_2px_2px_0_rgba(0,0,0,0.2)] p-[1.2rem] cursor-pointer"
|
||||
onClick={onCardClick}
|
||||
>
|
||||
<div className="w-full h-full flex flex-row items-start justify-start gap-[1.2rem]">
|
||||
<svg
|
||||
className="w-16 h-16 text-[#2288ee]"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M8 4C8 2.34315 9.34315 1 11 1C12.6569 1 14 2.34315 14 4C14 4.35064 13.9398 4.68722 13.8293 5H18C18.5523 5 19 5.44772 19 6V10.1707C19.3128 10.0602 19.6494 10 20 10C21.6569 10 23 11.3431 23 13C23 14.6569 21.6569 16 20 16C19.6494 16 19.3128 15.9398 19 15.8293V20C19 20.5523 18.5523 21 18 21H4C3.44772 21 3 20.5523 3 20V6C3 5.44772 3.44772 5 4 5H8.17071C8.06015 4.68722 8 4.35064 8 4Z"></path>
|
||||
</svg>
|
||||
|
||||
<div className="w-full h-full flex flex-col items-start justify-between gap-[0.6rem]">
|
||||
<div className="flex flex-col items-start justify-start">
|
||||
<div className="w-full h-full flex flex-col items-start justify-between gap-[0.6rem]">
|
||||
<div className="flex flex-col items-start justify-start">
|
||||
<div className="text-[0.7rem] text-[#666]">
|
||||
{cardVO.author} /{' '}
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-start gap-[0.4rem]">
|
||||
<div className="text-[1.2rem] text-black">{cardVO.name}</div>
|
||||
<Badge variant="outline" className="text-[0.7rem]">
|
||||
v{cardVO.version}
|
||||
</Badge>
|
||||
{cardVO.debug && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[0.7rem] border-orange-400 text-orange-400"
|
||||
>
|
||||
<BugIcon className="w-4 h-4" />
|
||||
{t('plugins.debugging')}
|
||||
<div className="flex flex-col items-start justify-start">
|
||||
<div className="text-[0.7rem] text-[#666]">
|
||||
{cardVO.author} /{' '}
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-start gap-[0.4rem]">
|
||||
<div className="text-[1.2rem] text-black">{cardVO.name}</div>
|
||||
<Badge variant="outline" className="text-[0.7rem]">
|
||||
v{cardVO.version}
|
||||
</Badge>
|
||||
)}
|
||||
{!cardVO.debug && (
|
||||
<>
|
||||
{cardVO.install_source === 'github' && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[0.7rem] border-blue-400 text-blue-400"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
window.open(cardVO.install_info.github_url, '_blank');
|
||||
}}
|
||||
>
|
||||
{t('plugins.fromGithub')}
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</Badge>
|
||||
)}
|
||||
{cardVO.install_source === 'local' && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[0.7rem] border-green-400 text-green-400"
|
||||
>
|
||||
{t('plugins.fromLocal')}
|
||||
</Badge>
|
||||
)}
|
||||
{cardVO.install_source === 'marketplace' && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[0.7rem] border-purple-400 text-purple-400"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
window.open(
|
||||
getCloudServiceClientSync().getPluginMarketplaceURL(
|
||||
cardVO.author,
|
||||
cardVO.name,
|
||||
),
|
||||
'_blank',
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t('plugins.fromMarketplace')}
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{cardVO.debug && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[0.7rem] border-orange-400 text-orange-400"
|
||||
>
|
||||
<BugIcon className="w-4 h-4" />
|
||||
{t('plugins.debugging')}
|
||||
</Badge>
|
||||
)}
|
||||
{!cardVO.debug && (
|
||||
<>
|
||||
{cardVO.install_source === 'github' && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[0.7rem] border-blue-400 text-blue-400"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
window.open(
|
||||
cardVO.install_info.github_url,
|
||||
'_blank',
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t('plugins.fromGithub')}
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</Badge>
|
||||
)}
|
||||
{cardVO.install_source === 'local' && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[0.7rem] border-green-400 text-green-400"
|
||||
>
|
||||
{t('plugins.fromLocal')}
|
||||
</Badge>
|
||||
)}
|
||||
{cardVO.install_source === 'marketplace' && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[0.7rem] border-purple-400 text-purple-400"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
window.open(
|
||||
getCloudServiceClientSync().getPluginMarketplaceURL(
|
||||
cardVO.author,
|
||||
cardVO.name,
|
||||
),
|
||||
'_blank',
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t('plugins.fromMarketplace')}
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-[0.8rem] text-[#666] line-clamp-2">
|
||||
{cardVO.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-[0.8rem] text-[#666] line-clamp-2">
|
||||
{cardVO.description}
|
||||
<div className="w-full flex flex-row items-start justify-start gap-[0.6rem]">
|
||||
{getComponentList(cardVO.components, t)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full flex flex-row items-start justify-start gap-[0.6rem]">
|
||||
<div className="flex h-full flex-row items-center justify-center gap-[0.4rem]">
|
||||
<svg
|
||||
className="w-[1.2rem] h-[1.2rem] text-black"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M24 12L18.3431 17.6569L16.9289 16.2426L21.1716 12L16.9289 7.75736L18.3431 6.34315L24 12ZM2.82843 12L7.07107 16.2426L5.65685 17.6569L0 12L5.65685 6.34315L7.07107 7.75736L2.82843 12ZM9.78845 21H7.66009L14.2116 3H16.3399L9.78845 21Z"></path>
|
||||
</svg>
|
||||
<div className="text-base text-black font-medium">
|
||||
{t('plugins.eventCount', {
|
||||
count: Object.keys(cardVO.event_handlers).length,
|
||||
})}
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-between h-full">
|
||||
<div className="flex items-center justify-center">
|
||||
<Switch
|
||||
className="cursor-pointer"
|
||||
checked={enabled}
|
||||
onClick={(e) => handleEnable(e)}
|
||||
disabled={!switchEnable}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full flex-row items-center justify-center gap-[0.4rem]">
|
||||
<svg
|
||||
className="w-[1.2rem] h-[1.2rem] text-black"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M5.32943 3.27158C6.56252 2.8332 7.9923 3.10749 8.97927 4.09446C10.1002 5.21537 10.3019 6.90741 9.5843 8.23385L20.293 18.9437L18.8788 20.3579L8.16982 9.64875C6.84325 10.3669 5.15069 10.1654 4.02952 9.04421C3.04227 8.05696 2.7681 6.62665 3.20701 5.39332L5.44373 7.63C6.02952 8.21578 6.97927 8.21578 7.56505 7.63C8.15084 7.04421 8.15084 6.09446 7.56505 5.50868L5.32943 3.27158ZM15.6968 5.15512L18.8788 3.38736L20.293 4.80157L18.5252 7.98355L16.7574 8.3371L14.6361 10.4584L13.2219 9.04421L15.3432 6.92289L15.6968 5.15512ZM8.97927 13.2868L10.3935 14.7011L5.09018 20.0044C4.69966 20.3949 4.06649 20.3949 3.67597 20.0044C3.31334 19.6417 3.28744 19.0699 3.59826 18.6774L3.67597 18.5902L8.97927 13.2868Z"></path>
|
||||
</svg>
|
||||
<div className="text-base text-black font-medium">
|
||||
{t('plugins.toolCount', { count: cardVO.tools.length })}
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<Ellipsis className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
className="flex flex-row items-center justify-start gap-[0.4rem] cursor-pointer"
|
||||
onClick={(e) => {
|
||||
onDeleteClick(cardVO);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Trash className="w-4 h-4" />
|
||||
<span>{t('plugins.delete')}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center justify-between h-full">
|
||||
<div className="flex items-center justify-center">
|
||||
<Switch
|
||||
className="cursor-pointer"
|
||||
checked={enabled}
|
||||
onClick={(e) => handleEnable(e)}
|
||||
disabled={!switchEnable}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center">
|
||||
{/* <Button variant="ghost">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6.17071 18C6.58254 16.8348 7.69378 16 9 16C10.3062 16 11.4175 16.8348 11.8293 18H22V20H11.8293C11.4175 21.1652 10.3062 22 9 22C7.69378 22 6.58254 21.1652 6.17071 20H2V18H6.17071ZM12.1707 11C12.5825 9.83481 13.6938 9 15 9C16.3062 9 17.4175 9.83481 17.8293 11H22V13H17.8293C17.4175 14.1652 16.3062 15 15 15C13.6938 15 12.5825 14.1652 12.1707 13H2V11H12.1707ZM6.17071 4C6.58254 2.83481 7.69378 2 9 2C10.3062 2 11.4175 2.83481 11.8293 4H22V6H11.8293C11.4175 7.16519 10.3062 8 9 8C7.69378 8 6.58254 7.16519 6.17071 6H2V4H6.17071ZM9 6C9.55228 6 10 5.55228 10 5C10 4.44772 9.55228 4 9 4C8.44772 4 8 4.44772 8 5C8 5.55228 8.44772 6 9 6ZM15 13C15.5523 13 16 12.5523 16 12C16 11.4477 15.5523 11 15 11C14.4477 11 14 11.4477 14 12C14 12.5523 14.4477 13 15 13ZM9 20C9.55228 20 10 19.5523 10 19C10 18.4477 9.55228 18 9 18C8.44772 18 8 18.4477 8 19C8 19.5523 8.44772 20 9 20Z"></path></svg>
|
||||
</Button> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<ApiRespPluginConfig>();
|
||||
const [isSaving, setIsLoading] = useState(false);
|
||||
|
||||
const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false);
|
||||
const [pluginRemoveStatus, setPluginRemoveStatus] =
|
||||
useState<PluginRemoveStatus>(PluginRemoveStatus.WAIT_INPUT);
|
||||
const [pluginRemoveError, setPluginRemoveError] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// 获取插件信息
|
||||
httpClient.getPlugin(pluginAuthor, pluginName).then((res) => {
|
||||
@@ -76,113 +63,8 @@ export default function PluginForm({
|
||||
return <div>{t('plugins.loading')}</div>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<Dialog
|
||||
open={showDeleteConfirmModal}
|
||||
onOpenChange={setShowDeleteConfirmModal}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('plugins.deleteConfirm')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription>
|
||||
{pluginRemoveStatus === PluginRemoveStatus.WAIT_INPUT && (
|
||||
<div>
|
||||
{t('plugins.confirmDeletePlugin', {
|
||||
author: pluginAuthor,
|
||||
name: pluginName,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{pluginRemoveStatus === PluginRemoveStatus.REMOVING && (
|
||||
<div>{t('plugins.deleting')}</div>
|
||||
)}
|
||||
{pluginRemoveStatus === PluginRemoveStatus.ERROR && (
|
||||
<div>
|
||||
{t('plugins.deleteError')}
|
||||
<div className="text-red-500">{pluginRemoveError}</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogDescription>
|
||||
<DialogFooter>
|
||||
{pluginRemoveStatus === PluginRemoveStatus.WAIT_INPUT && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setShowDeleteConfirmModal(false);
|
||||
setPluginRemoveStatus(PluginRemoveStatus.WAIT_INPUT);
|
||||
}}
|
||||
>
|
||||
{t('plugins.cancel')}
|
||||
</Button>
|
||||
)}
|
||||
{pluginRemoveStatus === PluginRemoveStatus.WAIT_INPUT && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
deletePlugin();
|
||||
}}
|
||||
>
|
||||
{t('plugins.confirmDelete')}
|
||||
</Button>
|
||||
)}
|
||||
{pluginRemoveStatus === PluginRemoveStatus.REMOVING && (
|
||||
<Button variant="destructive" disabled>
|
||||
{t('plugins.deleting')}
|
||||
</Button>
|
||||
)}
|
||||
{pluginRemoveStatus === PluginRemoveStatus.ERROR && (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
setShowDeleteConfirmModal(false);
|
||||
// setPluginRemoveStatus(PluginRemoveStatus.WAIT_INPUT);
|
||||
}}
|
||||
>
|
||||
{t('plugins.close')}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="text-lg font-medium">
|
||||
{extractI18nObject(pluginInfo.manifest.manifest.metadata.label)}
|
||||
@@ -220,19 +102,6 @@ export default function PluginForm({
|
||||
|
||||
<div className="sticky bottom-0 left-0 right-0 bg-background border-t p-4 mt-4">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
setShowDeleteConfirmModal(true);
|
||||
setPluginRemoveStatus(PluginRemoveStatus.WAIT_INPUT);
|
||||
}}
|
||||
disabled={pluginRemoveStatus === PluginRemoveStatus.REMOVING}
|
||||
>
|
||||
{pluginRemoveStatus === PluginRemoveStatus.REMOVING
|
||||
? t('plugins.deleting')
|
||||
: t('plugins.deletePlugin')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={() => handleSubmit(pluginConfig.config)}
|
||||
|
||||
@@ -11,11 +11,13 @@ export interface Plugin {
|
||||
enabled: boolean;
|
||||
install_source: string;
|
||||
install_info: Record<string, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
components: {
|
||||
component_config: object;
|
||||
manifest: {
|
||||
manifest: ComponentManifest;
|
||||
};
|
||||
components: PluginComponent[];
|
||||
}
|
||||
|
||||
export interface PluginComponent {
|
||||
component_config: object;
|
||||
manifest: {
|
||||
manifest: ComponentManifest;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -186,8 +186,11 @@ const zhHans = {
|
||||
selectFileToUpload: '选择要上传的插件文件',
|
||||
askConfirm: '确定要安装插件 "{{name}}" ({{version}}) 吗?',
|
||||
fromGithub: '来自 GitHub',
|
||||
fromLocal: '来自本地',
|
||||
fromLocal: '本地安装',
|
||||
fromMarketplace: '来自市场',
|
||||
componentsList: '组件: ',
|
||||
noComponents: '无组件',
|
||||
delete: '删除',
|
||||
},
|
||||
market: {
|
||||
searchPlaceholder: '搜索插件...',
|
||||
|
||||
Reference in New Issue
Block a user