mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-03-01 16:20:25 +00:00
Improve plugin status handling and dirname lookup
Enhanced setPluginStatus to support enabling/disabling plugins by both package name and dirname, improving robustness when plugins are not loaded. Also removed redundant directory name matching logic from findDirnameById in the web UI backend. Register plugin after installation in PluginStore Adds logic to immediately register a plugin with the plugin manager after installation, both in the standard and SSE install handlers. This ensures newly installed plugins are available without requiring a restart or manual reload. Refactor plugin path handling in plugin manager Simplifies plugin directory and data path resolution by using pluginPath from the plugin context instead of fileId. Streamlines plugin uninstall and reload logic, removing redundant file system scans and improving code clarity. Refactor plugin API to use package id and improve UX Standardized plugin management APIs and frontend to use 'id' (package name) instead of ambiguous 'name' or 'filename'. Added support for a 'plugin' display field in package.json and improved plugin store UI to show install/update status. Refactored backend and frontend logic for enabling, disabling, uninstalling, and configuring plugins to use consistent identifiers, and enhanced type definitions and documentation for better maintainability.
This commit is contained in:
@@ -17,7 +17,7 @@ export default function PluginPage () {
|
||||
const dialog = useDialog();
|
||||
|
||||
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||
const [currentPluginName, setCurrentPluginName] = useState<string>('');
|
||||
const [currentPluginId, setCurrentPluginId] = useState<string>('');
|
||||
|
||||
const loadPlugins = async () => {
|
||||
setLoading(true);
|
||||
@@ -49,7 +49,7 @@ export default function PluginPage () {
|
||||
const actionText = isEnable ? '启用' : '禁用';
|
||||
const loadingToast = toast.loading(`${actionText}中...`);
|
||||
try {
|
||||
await PluginManager.setPluginStatus(plugin.name, isEnable, plugin.filename);
|
||||
await PluginManager.setPluginStatus(plugin.id, isEnable);
|
||||
toast.success(`${actionText}成功`, { id: loadingToast });
|
||||
loadPlugins();
|
||||
} catch (e: any) {
|
||||
@@ -85,7 +85,7 @@ export default function PluginPage () {
|
||||
|
||||
const loadingToast = toast.loading('卸载中...');
|
||||
try {
|
||||
await PluginManager.uninstallPlugin(plugin.name, plugin.filename, cleanData);
|
||||
await PluginManager.uninstallPlugin(plugin.id, cleanData);
|
||||
toast.success('卸载成功', { id: loadingToast });
|
||||
loadPlugins();
|
||||
resolve();
|
||||
@@ -102,7 +102,7 @@ export default function PluginPage () {
|
||||
};
|
||||
|
||||
const handleConfig = (plugin: PluginItem) => {
|
||||
setCurrentPluginName(plugin.name); // Use Loaded Name for config lookup
|
||||
setCurrentPluginId(plugin.id);
|
||||
onOpen();
|
||||
};
|
||||
|
||||
@@ -114,7 +114,7 @@ export default function PluginPage () {
|
||||
<PluginConfigModal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
pluginName={currentPluginName}
|
||||
pluginId={currentPluginId}
|
||||
/>
|
||||
|
||||
<div className='flex mb-6 items-center gap-4'>
|
||||
@@ -145,7 +145,7 @@ export default function PluginPage () {
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 justify-start items-stretch gap-x-2 gap-y-4'>
|
||||
{plugins.map(plugin => (
|
||||
<PluginDisplayCard
|
||||
key={plugin.name}
|
||||
key={plugin.id}
|
||||
data={plugin}
|
||||
onToggleStatus={() => handleToggle(plugin)}
|
||||
onUninstall={() => handleUninstall(plugin)}
|
||||
|
||||
@@ -10,31 +10,32 @@ import PluginManager, { PluginConfigSchemaItem } from '@/controllers/plugin_mana
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
onOpenChange: () => void;
|
||||
pluginName: string;
|
||||
/** 插件包名 (id) */
|
||||
pluginId: string;
|
||||
}
|
||||
|
||||
export default function PluginConfigModal ({ isOpen, onOpenChange, pluginName }: Props) {
|
||||
export default function PluginConfigModal ({ isOpen, onOpenChange, pluginId }: Props) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [schema, setSchema] = useState<PluginConfigSchemaItem[]>([]);
|
||||
const [config, setConfig] = useState<any>({});
|
||||
const [config, setConfig] = useState<Record<string, unknown>>({});
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && pluginName) {
|
||||
if (isOpen && pluginId) {
|
||||
loadConfig();
|
||||
}
|
||||
}, [isOpen, pluginName]);
|
||||
}, [isOpen, pluginId]);
|
||||
|
||||
const loadConfig = async () => {
|
||||
setLoading(true);
|
||||
setSchema([]);
|
||||
setConfig({});
|
||||
try {
|
||||
const data = await PluginManager.getPluginConfig(pluginName);
|
||||
const data = await PluginManager.getPluginConfig(pluginId);
|
||||
setSchema(data.schema || []);
|
||||
setConfig(data.config || {});
|
||||
} catch (e: any) {
|
||||
toast.error('Load config failed: ' + e.message);
|
||||
toast.error('加载配置失败: ' + e.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -43,7 +44,7 @@ export default function PluginConfigModal ({ isOpen, onOpenChange, pluginName }:
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
await PluginManager.setPluginConfig(pluginName, config);
|
||||
await PluginManager.setPluginConfig(pluginId, config);
|
||||
toast.success('Configuration saved');
|
||||
onOpenChange();
|
||||
} catch (e: any) {
|
||||
@@ -177,7 +178,7 @@ export default function PluginConfigModal ({ isOpen, onOpenChange, pluginName }:
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
Configuration: {pluginName}
|
||||
插件配置: {pluginId}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
{loading ? (
|
||||
|
||||
@@ -10,8 +10,8 @@ import { useRequest } from 'ahooks';
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
|
||||
import PageLoading from '@/components/page_loading';
|
||||
import PluginStoreCard from '@/components/display_card/plugin_store_card';
|
||||
import PluginManager from '@/controllers/plugin_manager';
|
||||
import PluginStoreCard, { InstallStatus } from '@/components/display_card/plugin_store_card';
|
||||
import PluginManager, { PluginItem } from '@/controllers/plugin_manager';
|
||||
import WebUIManager from '@/controllers/webui_manager';
|
||||
import { PluginStoreItem } from '@/types/plugin-store';
|
||||
import useDialog from '@/hooks/use-dialog';
|
||||
@@ -35,6 +35,7 @@ const EmptySection: React.FC<EmptySectionProps> = ({ isEmpty }) => {
|
||||
|
||||
export default function PluginStorePage () {
|
||||
const [plugins, setPlugins] = useState<PluginStoreItem[]>([]);
|
||||
const [installedPlugins, setInstalledPlugins] = useState<PluginItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [activeTab, setActiveTab] = useState<string>('all');
|
||||
@@ -57,6 +58,7 @@ export default function PluginStorePage () {
|
||||
// 检查插件管理器是否已加载
|
||||
const listResult = await PluginManager.getPluginList();
|
||||
setPluginManagerNotFound(listResult.pluginManagerNotFound);
|
||||
setInstalledPlugins(listResult.plugins || []);
|
||||
} catch (e: any) {
|
||||
toast.error(e.message);
|
||||
} finally {
|
||||
@@ -95,6 +97,23 @@ export default function PluginStorePage () {
|
||||
return categories;
|
||||
}, [plugins, searchQuery]);
|
||||
|
||||
// 获取插件的安装状态和已安装版本
|
||||
const getPluginInstallInfo = (plugin: PluginStoreItem): { status: InstallStatus; installedVersion?: string; } => {
|
||||
// 通过 id (包名) 或 name 匹配已安装的插件
|
||||
const installed = installedPlugins.find(p => p.id === plugin.id);
|
||||
|
||||
if (!installed) {
|
||||
return { status: 'not-installed' };
|
||||
}
|
||||
|
||||
// 使用不等于判断:版本不同就显示更新
|
||||
if (installed.version !== plugin.version) {
|
||||
return { status: 'update-available', installedVersion: installed.version };
|
||||
}
|
||||
|
||||
return { status: 'installed', installedVersion: installed.version };
|
||||
};
|
||||
|
||||
const tabs = useMemo(() => {
|
||||
return [
|
||||
{ key: 'all', title: '全部', count: categorizedPlugins.all?.length || 0 },
|
||||
@@ -293,13 +312,18 @@ export default function PluginStorePage () {
|
||||
>
|
||||
<EmptySection isEmpty={!categorizedPlugins[tab.key]?.length} />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 justify-start items-stretch gap-x-2 gap-y-4">
|
||||
{categorizedPlugins[tab.key]?.map((plugin) => (
|
||||
<PluginStoreCard
|
||||
key={plugin.id}
|
||||
data={plugin}
|
||||
onInstall={() => handleInstall(plugin)}
|
||||
/>
|
||||
))}
|
||||
{categorizedPlugins[tab.key]?.map((plugin) => {
|
||||
const installInfo = getPluginInstallInfo(plugin);
|
||||
return (
|
||||
<PluginStoreCard
|
||||
key={plugin.id}
|
||||
data={plugin}
|
||||
installStatus={installInfo.status}
|
||||
installedVersion={installInfo.installedVersion}
|
||||
onInstall={() => handleInstall(plugin)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Tab>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user