mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-12 16:56:02 +00:00
feat: youhua frontend
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { PluginCardVO } from '@/app/home/plugins/components/plugin-installed/PluginCardVO';
|
||||
import PluginCardComponent from '@/app/home/plugins/components/plugin-installed/plugin-card/PluginCardComponent';
|
||||
import { ExtensionCardVO, ExtensionType } from './ExtensionCardVO';
|
||||
import ExtensionCardComponent from './ExtensionCardComponent';
|
||||
import styles from '@/app/home/plugins/plugins.module.css';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { getCloudServiceClientSync } from '@/app/infra/http';
|
||||
@@ -22,42 +22,59 @@ import { toast } from 'sonner';
|
||||
import { useAsyncTask, AsyncTaskStatus } from '@/hooks/useAsyncTask';
|
||||
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
|
||||
import { Puzzle } from 'lucide-react';
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from '@/components/ui/toggle-group';
|
||||
import { Wrench, AudioWaveform, Book } from 'lucide-react';
|
||||
import { MCPSessionStatus } from '@/app/infra/entities/api';
|
||||
|
||||
export interface PluginInstalledComponentRef {
|
||||
refreshPluginList: () => void;
|
||||
}
|
||||
|
||||
enum PluginOperationType {
|
||||
enum ExtensionOperationType {
|
||||
DELETE = 'DELETE',
|
||||
UPDATE = 'UPDATE',
|
||||
}
|
||||
|
||||
type FilterType = 'all' | ExtensionType;
|
||||
|
||||
const FilterOptions = [
|
||||
{ value: 'all' as FilterType, labelKey: 'market.filters.allFormats', icon: null },
|
||||
{ value: 'plugin' as FilterType, labelKey: 'market.typePlugin', icon: Wrench },
|
||||
{ value: 'mcp' as FilterType, labelKey: 'market.typeMCP', icon: AudioWaveform },
|
||||
{ value: 'skill' as FilterType, labelKey: 'market.typeSkill', icon: Book },
|
||||
];
|
||||
|
||||
const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
(props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { refreshPlugins } = useSidebarData();
|
||||
const [pluginList, setPluginList] = useState<PluginCardVO[]>([]);
|
||||
const { refreshPlugins, refreshMCPServers, refreshSkills } = useSidebarData();
|
||||
const [extensionList, setExtensionList] = useState<ExtensionCardVO[]>([]);
|
||||
const [filterType, setFilterType] = useState<FilterType>('all');
|
||||
const [showOperationModal, setShowOperationModal] = useState(false);
|
||||
const [operationType, setOperationType] = useState<PluginOperationType>(
|
||||
PluginOperationType.DELETE,
|
||||
const [operationType, setOperationType] = useState<ExtensionOperationType>(
|
||||
ExtensionOperationType.DELETE,
|
||||
);
|
||||
const [targetPlugin, setTargetPlugin] = useState<PluginCardVO | null>(null);
|
||||
const [targetExtension, setTargetExtension] = useState<ExtensionCardVO | null>(null);
|
||||
const [deleteData, setDeleteData] = useState<boolean>(false);
|
||||
|
||||
const asyncTask = useAsyncTask({
|
||||
onSuccess: () => {
|
||||
const successMessage =
|
||||
operationType === PluginOperationType.DELETE
|
||||
operationType === ExtensionOperationType.DELETE
|
||||
? t('plugins.deleteSuccess')
|
||||
: t('plugins.updateSuccess');
|
||||
toast.success(successMessage);
|
||||
setShowOperationModal(false);
|
||||
getPluginList();
|
||||
getExtensionList();
|
||||
refreshPlugins();
|
||||
refreshMCPServers();
|
||||
refreshSkills();
|
||||
},
|
||||
onError: () => {
|
||||
// Error is already handled in the hook state
|
||||
},
|
||||
});
|
||||
|
||||
@@ -66,131 +83,171 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
}, []);
|
||||
|
||||
function initData() {
|
||||
getPluginList();
|
||||
getExtensionList();
|
||||
}
|
||||
|
||||
async function getPluginList() {
|
||||
async function getExtensionList() {
|
||||
try {
|
||||
// 获取已安装插件列表
|
||||
const installedPluginsResp = await httpClient.getPlugins();
|
||||
const installedPlugins = installedPluginsResp.plugins;
|
||||
|
||||
// 获取市场插件列表
|
||||
const client = getCloudServiceClientSync();
|
||||
const marketplaceResp = await client.getMarketplacePlugins(1, 100);
|
||||
const marketplacePlugins = marketplaceResp.plugins;
|
||||
|
||||
// 创建市场插件映射,便于快速查找
|
||||
const marketplacePluginMap = new Map();
|
||||
marketplacePlugins.forEach((plugin) => {
|
||||
const [installedPluginsResp, marketplaceResp, mcpResp, skillsResp] = await Promise.all([
|
||||
httpClient.getPlugins().catch(() => ({ plugins: [] })),
|
||||
client.getMarketplacePlugins(1, 100).catch(() => ({ plugins: [] })),
|
||||
httpClient.getMCPServers().catch(() => ({ servers: [] })),
|
||||
httpClient.getSkills().catch(() => ({ skills: [] })),
|
||||
]);
|
||||
|
||||
const marketplacePluginMap = new Map<string, any>();
|
||||
marketplaceResp.plugins.forEach((plugin: any) => {
|
||||
const key = `${plugin.author}/${plugin.name}`;
|
||||
marketplacePluginMap.set(key, plugin);
|
||||
});
|
||||
|
||||
// 转换并比较版本号
|
||||
const pluginCards = installedPlugins.map((plugin) => {
|
||||
const marketplaceKey = `${plugin.manifest.manifest.metadata.author}/${plugin.manifest.manifest.metadata.name}`;
|
||||
const marketplacePlugin = marketplacePluginMap.get(marketplaceKey);
|
||||
const cardVO = new PluginCardVO({
|
||||
author: plugin.manifest.manifest.metadata.author ?? '',
|
||||
label: extractI18nObject(plugin.manifest.manifest.metadata.label),
|
||||
description: extractI18nObject(
|
||||
plugin.manifest.manifest.metadata.description ?? {
|
||||
en_US: '',
|
||||
zh_Hans: '',
|
||||
},
|
||||
),
|
||||
debug: plugin.debug,
|
||||
enabled: plugin.enabled,
|
||||
name: plugin.manifest.manifest.metadata.name,
|
||||
version: plugin.manifest.manifest.metadata.version ?? '',
|
||||
status: plugin.status,
|
||||
components: plugin.components,
|
||||
priority: plugin.priority,
|
||||
install_source: plugin.install_source,
|
||||
install_info: plugin.install_info,
|
||||
type: marketplacePlugin?.type,
|
||||
});
|
||||
const extensions: ExtensionCardVO[] = [];
|
||||
|
||||
// 检查是否来自市场且有更新
|
||||
if (cardVO.install_source === 'marketplace' && marketplacePlugin) {
|
||||
for (const plugin of installedPluginsResp.plugins) {
|
||||
const meta = plugin.manifest.manifest.metadata;
|
||||
const author = meta.author ?? '';
|
||||
const name = meta.name;
|
||||
const marketplaceKey = `${author}/${name}`;
|
||||
const marketplacePlugin = marketplacePluginMap.get(marketplaceKey);
|
||||
|
||||
let hasUpdate = false;
|
||||
if (plugin.install_source === 'marketplace' && marketplacePlugin) {
|
||||
if (marketplacePlugin.latest_version) {
|
||||
cardVO.hasUpdate = isNewerVersion(
|
||||
hasUpdate = isNewerVersion(
|
||||
marketplacePlugin.latest_version,
|
||||
cardVO.version,
|
||||
meta.version ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return cardVO;
|
||||
});
|
||||
extensions.push(new ExtensionCardVO({
|
||||
id: marketplaceKey,
|
||||
author,
|
||||
label: extractI18nObject(meta.label) || name,
|
||||
name,
|
||||
description: extractI18nObject(meta.description ?? { en_US: '', zh_Hans: '' }),
|
||||
version: meta.version ?? '',
|
||||
enabled: plugin.enabled,
|
||||
type: marketplacePlugin?.type || 'plugin',
|
||||
iconURL: httpClient.getPluginIconURL(author, name),
|
||||
install_source: plugin.install_source,
|
||||
install_info: plugin.install_info,
|
||||
status: plugin.status,
|
||||
debug: plugin.debug,
|
||||
hasUpdate,
|
||||
}));
|
||||
}
|
||||
|
||||
setPluginList(pluginCards);
|
||||
for (const server of mcpResp.servers) {
|
||||
extensions.push(new ExtensionCardVO({
|
||||
id: `mcp-${server.name}`,
|
||||
author: '',
|
||||
label: server.name.replace(/__/g, '/'),
|
||||
name: server.name,
|
||||
description: '',
|
||||
version: '',
|
||||
enabled: server.enable,
|
||||
type: 'mcp',
|
||||
iconURL: httpClient.getPluginIconURL('mcp', server.name),
|
||||
status: server.runtime_info?.status,
|
||||
runtimeStatus: server.runtime_info?.status,
|
||||
tools: server.runtime_info?.tool_count || 0,
|
||||
mode: server.mode,
|
||||
}));
|
||||
}
|
||||
|
||||
for (const skill of skillsResp.skills) {
|
||||
extensions.push(new ExtensionCardVO({
|
||||
id: `skill-${skill.name}`,
|
||||
author: '',
|
||||
label: skill.display_name || skill.name,
|
||||
name: skill.name,
|
||||
description: skill.description || '',
|
||||
version: '',
|
||||
enabled: true,
|
||||
type: 'skill',
|
||||
iconURL: httpClient.getPluginIconURL('skill', skill.name),
|
||||
}));
|
||||
}
|
||||
|
||||
setExtensionList(extensions);
|
||||
} catch (error) {
|
||||
console.error('获取插件列表失败:', error);
|
||||
// 失败时仍显示已安装插件,不影响用户体验
|
||||
const installedPluginsResp = await httpClient.getPlugins();
|
||||
setPluginList(
|
||||
installedPluginsResp.plugins.map((plugin) => {
|
||||
return new PluginCardVO({
|
||||
author: plugin.manifest.manifest.metadata.author ?? '',
|
||||
label: extractI18nObject(plugin.manifest.manifest.metadata.label),
|
||||
description: extractI18nObject(
|
||||
plugin.manifest.manifest.metadata.description ?? {
|
||||
en_US: '',
|
||||
zh_Hans: '',
|
||||
},
|
||||
),
|
||||
debug: plugin.debug,
|
||||
enabled: plugin.enabled,
|
||||
name: plugin.manifest.manifest.metadata.name,
|
||||
version: plugin.manifest.manifest.metadata.version ?? '',
|
||||
status: plugin.status,
|
||||
components: plugin.components,
|
||||
priority: plugin.priority,
|
||||
install_source: plugin.install_source,
|
||||
install_info: plugin.install_info,
|
||||
});
|
||||
}),
|
||||
);
|
||||
console.error('Failed to fetch extension list:', error);
|
||||
setExtensionList([]);
|
||||
}
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
refreshPluginList: getPluginList,
|
||||
refreshPluginList: getExtensionList,
|
||||
}));
|
||||
|
||||
function handlePluginClick(plugin: PluginCardVO) {
|
||||
const pluginId = `${plugin.author}/${plugin.name}`;
|
||||
navigate(`/home/plugins?id=${encodeURIComponent(pluginId)}`);
|
||||
function handleExtensionClick(extension: ExtensionCardVO) {
|
||||
if (extension.type === 'mcp') {
|
||||
navigate(`/home/mcp`);
|
||||
} else if (extension.type === 'skill') {
|
||||
navigate(`/home/skills`);
|
||||
} else {
|
||||
const extensionId = `${extension.author}/${extension.name}`;
|
||||
navigate(`/home/plugins?id=${encodeURIComponent(extensionId)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function handlePluginDelete(plugin: PluginCardVO) {
|
||||
setTargetPlugin(plugin);
|
||||
setOperationType(PluginOperationType.DELETE);
|
||||
function handleExtensionDelete(extension: ExtensionCardVO) {
|
||||
setTargetExtension(extension);
|
||||
setOperationType(ExtensionOperationType.DELETE);
|
||||
setShowOperationModal(true);
|
||||
setDeleteData(false);
|
||||
asyncTask.reset();
|
||||
}
|
||||
|
||||
function handlePluginUpdate(plugin: PluginCardVO) {
|
||||
setTargetPlugin(plugin);
|
||||
setOperationType(PluginOperationType.UPDATE);
|
||||
function handleExtensionUpdate(extension: ExtensionCardVO) {
|
||||
setTargetExtension(extension);
|
||||
setOperationType(ExtensionOperationType.UPDATE);
|
||||
setShowOperationModal(true);
|
||||
asyncTask.reset();
|
||||
}
|
||||
|
||||
function executeOperation() {
|
||||
if (!targetPlugin) return;
|
||||
if (!targetExtension) return;
|
||||
|
||||
if (targetExtension.type === 'mcp') {
|
||||
httpClient.deleteMCPServer(targetExtension.name)
|
||||
.then(() => {
|
||||
toast.success(t('mcp.deleteSuccess'));
|
||||
setShowOperationModal(false);
|
||||
getExtensionList();
|
||||
refreshMCPServers();
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(t('mcp.deleteError') + error.message);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetExtension.type === 'skill') {
|
||||
httpClient.deleteSkill(targetExtension.name)
|
||||
.then(() => {
|
||||
toast.success(t('skills.deleteSuccess'));
|
||||
setShowOperationModal(false);
|
||||
getExtensionList();
|
||||
refreshSkills();
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(t('skills.deleteError') + error.message);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const apiCall =
|
||||
operationType === PluginOperationType.DELETE
|
||||
operationType === ExtensionOperationType.DELETE
|
||||
? httpClient.removePlugin(
|
||||
targetPlugin.author,
|
||||
targetPlugin.name,
|
||||
targetExtension.author,
|
||||
targetExtension.name,
|
||||
deleteData,
|
||||
)
|
||||
: httpClient.upgradePlugin(targetPlugin.author, targetPlugin.name);
|
||||
: httpClient.upgradePlugin(targetExtension.author, targetExtension.name);
|
||||
|
||||
apiCall
|
||||
.then((res) => {
|
||||
@@ -198,13 +255,32 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorMessage =
|
||||
operationType === PluginOperationType.DELETE
|
||||
operationType === ExtensionOperationType.DELETE
|
||||
? t('plugins.deleteError') + error.message
|
||||
: t('plugins.updateError') + error.message;
|
||||
toast.error(errorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
const filteredExtensions = extensionList.filter((ext) => {
|
||||
if (filterType === 'all') return true;
|
||||
return ext.type === filterType;
|
||||
});
|
||||
|
||||
const getDeleteConfirmMessage = () => {
|
||||
if (!targetExtension) return '';
|
||||
if (targetExtension.type === 'mcp') {
|
||||
return t('mcp.confirmDeleteServer');
|
||||
}
|
||||
if (targetExtension.type === 'skill') {
|
||||
return t('skills.deleteConfirmation');
|
||||
}
|
||||
return t('plugins.confirmDeletePlugin', {
|
||||
author: targetExtension.author,
|
||||
name: targetExtension.name,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
@@ -212,7 +288,7 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setShowOperationModal(false);
|
||||
setTargetPlugin(null);
|
||||
setTargetExtension(null);
|
||||
asyncTask.reset();
|
||||
}
|
||||
}}
|
||||
@@ -220,7 +296,7 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{operationType === PluginOperationType.DELETE
|
||||
{operationType === ExtensionOperationType.DELETE
|
||||
? t('plugins.deleteConfirm')
|
||||
: t('plugins.updateConfirm')}
|
||||
</DialogTitle>
|
||||
@@ -228,18 +304,8 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
<DialogDescription>
|
||||
{asyncTask.status === AsyncTaskStatus.WAIT_INPUT && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
{operationType === PluginOperationType.DELETE
|
||||
? t('plugins.confirmDeletePlugin', {
|
||||
author: targetPlugin?.author ?? '',
|
||||
name: targetPlugin?.name ?? '',
|
||||
})
|
||||
: t('plugins.confirmUpdatePlugin', {
|
||||
author: targetPlugin?.author ?? '',
|
||||
name: targetPlugin?.name ?? '',
|
||||
})}
|
||||
</div>
|
||||
{operationType === PluginOperationType.DELETE && (
|
||||
<div>{getDeleteConfirmMessage()}</div>
|
||||
{operationType === ExtensionOperationType.DELETE && targetExtension?.type === 'plugin' && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="delete-data"
|
||||
@@ -260,14 +326,14 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
)}
|
||||
{asyncTask.status === AsyncTaskStatus.RUNNING && (
|
||||
<div>
|
||||
{operationType === PluginOperationType.DELETE
|
||||
{operationType === ExtensionOperationType.DELETE
|
||||
? t('plugins.deleting')
|
||||
: t('plugins.updating')}
|
||||
</div>
|
||||
)}
|
||||
{asyncTask.status === AsyncTaskStatus.ERROR && (
|
||||
<div>
|
||||
{operationType === PluginOperationType.DELETE
|
||||
{operationType === ExtensionOperationType.DELETE
|
||||
? t('plugins.deleteError')
|
||||
: t('plugins.updateError')}
|
||||
<div className="text-red-500">{asyncTask.error}</div>
|
||||
@@ -280,7 +346,7 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setShowOperationModal(false);
|
||||
setTargetPlugin(null);
|
||||
setTargetExtension(null);
|
||||
asyncTask.reset();
|
||||
}}
|
||||
>
|
||||
@@ -290,7 +356,7 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
{asyncTask.status === AsyncTaskStatus.WAIT_INPUT && (
|
||||
<Button
|
||||
variant={
|
||||
operationType === PluginOperationType.DELETE
|
||||
operationType === ExtensionOperationType.DELETE
|
||||
? 'destructive'
|
||||
: 'default'
|
||||
}
|
||||
@@ -298,7 +364,7 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
executeOperation();
|
||||
}}
|
||||
>
|
||||
{operationType === PluginOperationType.DELETE
|
||||
{operationType === ExtensionOperationType.DELETE
|
||||
? t('plugins.confirmDelete')
|
||||
: t('plugins.confirmUpdate')}
|
||||
</Button>
|
||||
@@ -306,13 +372,13 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
{asyncTask.status === AsyncTaskStatus.RUNNING && (
|
||||
<Button
|
||||
variant={
|
||||
operationType === PluginOperationType.DELETE
|
||||
operationType === ExtensionOperationType.DELETE
|
||||
? 'destructive'
|
||||
: 'default'
|
||||
}
|
||||
disabled
|
||||
>
|
||||
{operationType === PluginOperationType.DELETE
|
||||
{operationType === ExtensionOperationType.DELETE
|
||||
? t('plugins.deleting')
|
||||
: t('plugins.updating')}
|
||||
</Button>
|
||||
@@ -332,21 +398,44 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{pluginList.length === 0 ? (
|
||||
<div className="px-[0.8rem] pb-4">
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={filterType}
|
||||
onValueChange={(value) => {
|
||||
if (value) setFilterType(value as FilterType);
|
||||
}}
|
||||
className="justify-start"
|
||||
>
|
||||
{FilterOptions.map((option) => (
|
||||
<ToggleGroupItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
variant="outline"
|
||||
className="px-4 py-2"
|
||||
>
|
||||
{option.icon && <option.icon className="w-4 h-4 mr-2" />}
|
||||
{t(option.labelKey)}
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
|
||||
{filteredExtensions.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center text-gray-500 min-h-[60vh] w-full gap-2">
|
||||
<Puzzle className="h-[3rem] w-[3rem]" />
|
||||
<div className="text-lg mb-2">{t('plugins.noPluginInstalled')}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`${styles.pluginListContainer}`}>
|
||||
{pluginList.map((vo, index) => {
|
||||
{filteredExtensions.map((vo, index) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
<PluginCardComponent
|
||||
<div key={vo.id || index}>
|
||||
<ExtensionCardComponent
|
||||
cardVO={vo}
|
||||
onCardClick={() => handlePluginClick(vo)}
|
||||
onDeleteClick={() => handlePluginDelete(vo)}
|
||||
onUpgradeClick={() => handlePluginUpdate(vo)}
|
||||
onCardClick={() => handleExtensionClick(vo)}
|
||||
onDeleteClick={() => handleExtensionDelete(vo)}
|
||||
onUpgradeClick={vo.type === 'plugin' ? () => handleExtensionUpdate(vo) : undefined}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -358,4 +447,4 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
},
|
||||
);
|
||||
|
||||
export default PluginInstalledComponent;
|
||||
export default PluginInstalledComponent;
|
||||
Reference in New Issue
Block a user