diff --git a/packages/napcat-webui-backend/src/api/Plugin.ts b/packages/napcat-webui-backend/src/api/Plugin.ts index 74014641..b52909bb 100644 --- a/packages/napcat-webui-backend/src/api/Plugin.ts +++ b/packages/napcat-webui-backend/src/api/Plugin.ts @@ -16,7 +16,8 @@ const getPluginManager = (): OB11PluginMangerAdapter | null => { export const GetPluginListHandler: RequestHandler = async (_req, res) => { const pluginManager = getPluginManager(); if (!pluginManager) { - return sendError(res, 'Plugin Manager not found'); + // 返回成功但带特殊标记 + return sendSuccess(res, { plugins: [], pluginManagerNotFound: true }); } // 辅助函数:根据文件名/路径生成唯一ID(作为配置键) @@ -113,7 +114,7 @@ export const GetPluginListHandler: RequestHandler = async (_req, res) => { } } - return sendSuccess(res, allPlugins); + return sendSuccess(res, { plugins: allPlugins, pluginManagerNotFound: false }); }; export const ReloadPluginHandler: RequestHandler = async (req, res) => { @@ -124,7 +125,7 @@ export const ReloadPluginHandler: RequestHandler = async (req, res) => { const pluginManager = getPluginManager(); if (!pluginManager) { - return sendError(res, 'Plugin Manager not found'); + return sendError(res, '插件管理器未加载,请检查 plugins 目录是否存在'); } const success = await pluginManager.reloadPlugin(name); diff --git a/packages/napcat-webui-backend/src/api/PluginStore.ts b/packages/napcat-webui-backend/src/api/PluginStore.ts new file mode 100644 index 00000000..ce299c40 --- /dev/null +++ b/packages/napcat-webui-backend/src/api/PluginStore.ts @@ -0,0 +1,177 @@ +import { RequestHandler } from 'express'; +import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response'; +import { PluginStoreList } from '@/napcat-webui-backend/src/types/PluginStore'; + +// Mock数据 - 模拟远程插件列表 +const mockPluginStoreData: PluginStoreList = { + version: '1.0.0', + updateTime: new Date().toISOString(), + plugins: [ + { + id: 'napcat-plugin-example', + name: '示例插件', + version: '1.0.0', + description: '这是一个示例插件,展示如何开发NapCat插件', + author: 'NapCat Team', + homepage: 'https://github.com/NapNeko/NapCatQQ', + repository: 'https://github.com/NapNeko/NapCatQQ', + downloadUrl: 'https://example.com/plugins/napcat-plugin-example-1.0.0.zip', + tags: ['示例', '教程'], + screenshots: ['https://picsum.photos/800/600?random=1'], + minNapCatVersion: '1.0.0', + downloads: 1234, + rating: 4.5, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-20T00:00:00Z', + }, + { + id: 'napcat-plugin-auto-reply', + name: '自动回复插件', + version: '2.1.0', + description: '支持关键词匹配的自动回复功能,可配置多种回复规则', + author: 'Community', + homepage: 'https://github.com/example/auto-reply', + repository: 'https://github.com/example/auto-reply', + downloadUrl: 'https://example.com/plugins/napcat-plugin-auto-reply-2.1.0.zip', + tags: ['自动回复', '消息处理'], + minNapCatVersion: '1.0.0', + downloads: 5678, + rating: 4.8, + createdAt: '2024-01-05T00:00:00Z', + updatedAt: '2024-01-22T00:00:00Z', + }, + { + id: 'napcat-plugin-welcome', + name: '入群欢迎插件', + version: '1.2.3', + description: '新成员入群时自动发送欢迎消息,支持自定义欢迎语', + author: 'Developer', + homepage: 'https://github.com/example/welcome', + repository: 'https://github.com/example/welcome', + downloadUrl: 'https://example.com/plugins/napcat-plugin-welcome-1.2.3.zip', + tags: ['欢迎', '群管理'], + minNapCatVersion: '1.0.0', + downloads: 3456, + rating: 4.3, + createdAt: '2024-01-10T00:00:00Z', + updatedAt: '2024-01-18T00:00:00Z', + }, + { + id: 'napcat-plugin-music', + name: '音乐点歌插件', + version: '3.0.1', + description: '支持网易云、QQ音乐等平台的点歌功能', + author: 'Music Lover', + homepage: 'https://github.com/example/music', + repository: 'https://github.com/example/music', + downloadUrl: 'https://example.com/plugins/napcat-plugin-music-3.0.1.zip', + tags: ['音乐', '娱乐'], + screenshots: ['https://picsum.photos/800/600?random=4', 'https://picsum.photos/800/600?random=5'], + minNapCatVersion: '1.1.0', + downloads: 8901, + rating: 4.9, + createdAt: '2023-12-01T00:00:00Z', + updatedAt: '2024-01-23T00:00:00Z', + }, + { + id: 'napcat-plugin-admin', + name: '群管理插件', + version: '2.5.0', + description: '提供踢人、禁言、设置管理员等群管理功能', + author: 'Admin Tools', + homepage: 'https://github.com/example/admin', + repository: 'https://github.com/example/admin', + downloadUrl: 'https://example.com/plugins/napcat-plugin-admin-2.5.0.zip', + tags: ['管理', '群管理', '工具'], + minNapCatVersion: '1.0.0', + downloads: 6789, + rating: 4.6, + createdAt: '2023-12-15T00:00:00Z', + updatedAt: '2024-01-21T00:00:00Z', + }, + { + id: 'napcat-plugin-image-search', + name: '以图搜图插件', + version: '1.5.2', + description: '支持多个搜图引擎,快速找到图片来源', + author: 'Image Hunter', + homepage: 'https://github.com/example/image-search', + repository: 'https://github.com/example/image-search', + downloadUrl: 'https://example.com/plugins/napcat-plugin-image-search-1.5.2.zip', + tags: ['图片', '搜索', '工具'], + minNapCatVersion: '1.0.0', + downloads: 4567, + rating: 4.4, + createdAt: '2024-01-08T00:00:00Z', + updatedAt: '2024-01-19T00:00:00Z', + }, + ], +}; + +/** + * 获取插件商店列表 + * 未来可以从远程URL读取 + */ +export const GetPluginStoreListHandler: RequestHandler = async (_req, res) => { + try { + // TODO: 未来从远程URL读取 + // const remoteUrl = 'https://napcat.example.com/plugin-list.json'; + // const response = await fetch(remoteUrl); + // const data = await response.json(); + + // 目前返回Mock数据 + return sendSuccess(res, mockPluginStoreData); + } catch (e: any) { + return sendError(res, 'Failed to fetch plugin store list: ' + e.message); + } +}; + +/** + * 获取单个插件详情 + */ +export const GetPluginStoreDetailHandler: RequestHandler = async (req, res) => { + try { + const { id } = req.params; + const plugin = mockPluginStoreData.plugins.find(p => p.id === id); + + if (!plugin) { + return sendError(res, 'Plugin not found'); + } + + return sendSuccess(res, plugin); + } catch (e: any) { + return sendError(res, 'Failed to fetch plugin detail: ' + e.message); + } +}; + +/** + * 安装插件(从商店) + * TODO: 实现实际的下载和安装逻辑 + */ +export const InstallPluginFromStoreHandler: RequestHandler = async (req, res) => { + try { + const { id } = req.body; + + if (!id) { + return sendError(res, 'Plugin ID is required'); + } + + const plugin = mockPluginStoreData.plugins.find(p => p.id === id); + + if (!plugin) { + return sendError(res, 'Plugin not found in store'); + } + + // TODO: 实现实际的下载和安装逻辑 + // 1. 下载插件文件 + // 2. 解压到插件目录 + // 3. 加载插件 + + return sendSuccess(res, { + message: 'Plugin installation started', + plugin: plugin + }); + } catch (e: any) { + return sendError(res, 'Failed to install plugin: ' + e.message); + } +}; diff --git a/packages/napcat-webui-backend/src/router/Plugin.ts b/packages/napcat-webui-backend/src/router/Plugin.ts index 9e87d681..f5c19f5e 100644 --- a/packages/napcat-webui-backend/src/router/Plugin.ts +++ b/packages/napcat-webui-backend/src/router/Plugin.ts @@ -1,5 +1,6 @@ import { Router } from 'express'; import { GetPluginListHandler, ReloadPluginHandler, SetPluginStatusHandler, UninstallPluginHandler } from '@/napcat-webui-backend/src/api/Plugin'; +import { GetPluginStoreListHandler, GetPluginStoreDetailHandler, InstallPluginFromStoreHandler } from '@/napcat-webui-backend/src/api/PluginStore'; const router: Router = Router(); @@ -8,4 +9,9 @@ router.post('/Reload', ReloadPluginHandler); router.post('/SetStatus', SetPluginStatusHandler); router.post('/Uninstall', UninstallPluginHandler); +// 插件商店相关路由 +router.get('/Store/List', GetPluginStoreListHandler); +router.get('/Store/Detail/:id', GetPluginStoreDetailHandler); +router.post('/Store/Install', InstallPluginFromStoreHandler); + export { router as PluginRouter }; diff --git a/packages/napcat-webui-backend/src/types/PluginStore.ts b/packages/napcat-webui-backend/src/types/PluginStore.ts new file mode 100644 index 00000000..f54f4b2a --- /dev/null +++ b/packages/napcat-webui-backend/src/types/PluginStore.ts @@ -0,0 +1,27 @@ +// 插件商店相关类型定义 + +export interface PluginStoreItem { + id: string; // 插件唯一标识 + name: string; // 插件名称 + version: string; // 最新版本 + description: string; // 插件描述 + author: string; // 作者 + homepage?: string; // 主页链接 + repository?: string; // 仓库地址 + downloadUrl: string; // 下载地址 + tags?: string[]; // 标签 + icon?: string; // 图标URL + screenshots?: string[]; // 截图 + minNapCatVersion?: string; // 最低NapCat版本要求 + dependencies?: Record; // 依赖 + downloads?: number; // 下载次数 + rating?: number; // 评分 + createdAt?: string; // 创建时间 + updatedAt?: string; // 更新时间 +} + +export interface PluginStoreList { + version: string; // 索引版本 + updateTime: string; // 更新时间 + plugins: PluginStoreItem[]; // 插件列表 +} diff --git a/packages/napcat-webui-frontend/src/App.tsx b/packages/napcat-webui-frontend/src/App.tsx index 796ed195..3c502e44 100644 --- a/packages/napcat-webui-frontend/src/App.tsx +++ b/packages/napcat-webui-frontend/src/App.tsx @@ -26,6 +26,7 @@ const LogsPage = lazy(() => import('@/pages/dashboard/logs')); const NetworkPage = lazy(() => import('@/pages/dashboard/network')); const TerminalPage = lazy(() => import('@/pages/dashboard/terminal')); const PluginPage = lazy(() => import('@/pages/dashboard/plugin')); +const PluginStorePage = lazy(() => import('@/pages/dashboard/plugin_store')); function App () { return ( @@ -78,6 +79,7 @@ function AppRoutes () { } /> } /> } /> + } /> } /> } /> diff --git a/packages/napcat-webui-frontend/src/components/display_card/plugin_store_card.tsx b/packages/napcat-webui-frontend/src/components/display_card/plugin_store_card.tsx new file mode 100644 index 00000000..f2fa577b --- /dev/null +++ b/packages/napcat-webui-frontend/src/components/display_card/plugin_store_card.tsx @@ -0,0 +1,117 @@ +import { Button } from '@heroui/button'; +import { Chip } from '@heroui/chip'; +import { useState } from 'react'; +import { IoMdStar, IoMdDownload } from 'react-icons/io'; + +import DisplayCardContainer from './container'; +import { PluginStoreItem } from '@/types/plugin-store'; + +export interface PluginStoreCardProps { + data: PluginStoreItem; + onInstall: () => Promise; +} + +const PluginStoreCard: React.FC = ({ + data, + onInstall, +}) => { + const { name, version, author, description, tags, rating, icon } = data; + const [processing, setProcessing] = useState(false); + + const handleInstall = () => { + setProcessing(true); + onInstall().finally(() => setProcessing(false)); + }; + + return ( + + v{version} + + } + enableSwitch={ + icon ? ( +
+ {name} +
+ ) : undefined + } + action={ + + } + > +
+
+ + 作者 + +
+ {author || '未知'} +
+
+
+ + 版本 + +
+ v{version} +
+
+
+ + 描述 + +
+ {description || '暂无描述'} +
+
+ {rating && ( +
+ + 评分 + +
+ + {rating.toFixed(1)} +
+
+ )} + {tags && tags.length > 0 && ( +
+ + 标签 + +
+ {tags.slice(0, 2).join(' · ')} +
+
+ )} +
+
+ ); +}; + +export default PluginStoreCard; diff --git a/packages/napcat-webui-frontend/src/config/site.tsx b/packages/napcat-webui-frontend/src/config/site.tsx index 05a06e8e..3332a92c 100644 --- a/packages/napcat-webui-frontend/src/config/site.tsx +++ b/packages/napcat-webui-frontend/src/config/site.tsx @@ -9,6 +9,7 @@ import { LuTerminal, LuZap, LuPackage, + LuStore, } from 'react-icons/lu'; export type SiteConfig = typeof siteConfig; @@ -65,6 +66,11 @@ export const siteConfig = { icon: , href: '/plugins', }, + { + label: '插件商店', + icon: , + href: '/plugin_store', + }, { label: '系统终端', icon: , diff --git a/packages/napcat-webui-frontend/src/controllers/plugin_manager.ts b/packages/napcat-webui-frontend/src/controllers/plugin_manager.ts index e27e1829..61dec6ef 100644 --- a/packages/napcat-webui-frontend/src/controllers/plugin_manager.ts +++ b/packages/napcat-webui-frontend/src/controllers/plugin_manager.ts @@ -1,4 +1,5 @@ import { serverRequest } from '@/utils/request'; +import { PluginStoreList, PluginStoreItem } from '@/types/plugin-store'; export interface PluginItem { name: string; @@ -9,6 +10,11 @@ export interface PluginItem { filename?: string; } +export interface PluginListResponse { + plugins: PluginItem[]; + pluginManagerNotFound: boolean; +} + export interface ServerResponse { code: number; message: string; @@ -17,7 +23,7 @@ export interface ServerResponse { export default class PluginManager { public static async getPluginList () { - const { data } = await serverRequest.get>('/Plugin/List'); + const { data } = await serverRequest.get>('/Plugin/List'); return data.data; } @@ -32,4 +38,19 @@ export default class PluginManager { public static async uninstallPlugin (name: string, filename?: string) { await serverRequest.post>('/Plugin/Uninstall', { name, filename }); } + + // 插件商店相关方法 + public static async getPluginStoreList () { + const { data } = await serverRequest.get>('/Plugin/Store/List'); + return data.data; + } + + public static async getPluginStoreDetail (id: string) { + const { data } = await serverRequest.get>(`/Plugin/Store/Detail/${id}`); + return data.data; + } + + public static async installPluginFromStore (id: string) { + await serverRequest.post>('/Plugin/Store/Install', { id }); + } } diff --git a/packages/napcat-webui-frontend/src/pages/dashboard/plugin.tsx b/packages/napcat-webui-frontend/src/pages/dashboard/plugin.tsx index e5dada94..82b829a9 100644 --- a/packages/napcat-webui-frontend/src/pages/dashboard/plugin.tsx +++ b/packages/napcat-webui-frontend/src/pages/dashboard/plugin.tsx @@ -11,13 +11,20 @@ import useDialog from '@/hooks/use-dialog'; export default function PluginPage () { const [plugins, setPlugins] = useState([]); const [loading, setLoading] = useState(false); + const [pluginManagerNotFound, setPluginManagerNotFound] = useState(false); const dialog = useDialog(); const loadPlugins = async () => { setLoading(true); + setPluginManagerNotFound(false); try { - const data = await PluginManager.getPluginList(); - setPlugins(data); + const result = await PluginManager.getPluginList(); + if (result.pluginManagerNotFound) { + setPluginManagerNotFound(true); + setPlugins([]); + } else { + setPlugins(result.plugins); + } } catch (e: any) { toast.error(e.message); } finally { @@ -94,7 +101,17 @@ export default function PluginPage () { - {plugins.length === 0 ? ( + {pluginManagerNotFound ? ( +
+
📦
+

+ 无插件加载 +

+

+ 插件管理器未加载,请检查 plugins 目录是否存在 +

+
+ ) : plugins.length === 0 ? (
暂时没有安装插件
) : (
diff --git a/packages/napcat-webui-frontend/src/pages/dashboard/plugin_store.tsx b/packages/napcat-webui-frontend/src/pages/dashboard/plugin_store.tsx new file mode 100644 index 00000000..927ab9ff --- /dev/null +++ b/packages/napcat-webui-frontend/src/pages/dashboard/plugin_store.tsx @@ -0,0 +1,161 @@ +import { Button } from '@heroui/button'; +import { Input } from '@heroui/input'; +import { Tab, Tabs } from '@heroui/tabs'; +import { useEffect, useMemo, useState } from 'react'; +import toast from 'react-hot-toast'; +import { IoMdRefresh, IoMdSearch } from 'react-icons/io'; +import clsx from 'clsx'; + +import PageLoading from '@/components/page_loading'; +import PluginStoreCard from '@/components/display_card/plugin_store_card'; +import PluginManager from '@/controllers/plugin_manager'; +import { PluginStoreItem } from '@/types/plugin-store'; + +interface EmptySectionProps { + isEmpty: boolean; +} + +const EmptySection: React.FC = ({ isEmpty }) => { + return ( +
+ 暂时没有可用的插件 +
+ ); +}; + +export default function PluginStorePage () { + const [plugins, setPlugins] = useState([]); + const [loading, setLoading] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const [activeTab, setActiveTab] = useState('all'); + + const loadPlugins = async () => { + setLoading(true); + try { + const data = await PluginManager.getPluginStoreList(); + setPlugins(data.plugins); + } catch (e: any) { + toast.error(e.message); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadPlugins(); + }, []); + + // 按标签分类和搜索 + const categorizedPlugins = useMemo(() => { + let filtered = plugins; + + // 搜索过滤 + if (searchQuery) { + filtered = filtered.filter( + (p) => + p.name.toLowerCase().includes(searchQuery.toLowerCase()) || + p.description.toLowerCase().includes(searchQuery.toLowerCase()) || + p.author.toLowerCase().includes(searchQuery.toLowerCase()) || + p.tags?.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase())) + ); + } + + // 按下载量排序 + filtered.sort((a, b) => (b.downloads || 0) - (a.downloads || 0)); + + // 定义主要分类 + const categories: Record = { + all: filtered, + popular: filtered.filter(p => (p.downloads || 0) > 5000), + tools: filtered.filter(p => p.tags?.some(t => ['工具', '管理', '群管理'].includes(t))), + entertainment: filtered.filter(p => p.tags?.some(t => ['娱乐', '音乐', '游戏'].includes(t))), + message: filtered.filter(p => p.tags?.some(t => ['消息处理', '自动回复', '欢迎'].includes(t))), + }; + + return categories; + }, [plugins, searchQuery]); + + const tabs = useMemo(() => { + return [ + { key: 'all', title: '全部', count: categorizedPlugins.all?.length || 0 }, + { key: 'popular', title: '热门推荐', count: categorizedPlugins.popular?.length || 0 }, + { key: 'tools', title: '工具管理', count: categorizedPlugins.tools?.length || 0 }, + { key: 'entertainment', title: '娱乐功能', count: categorizedPlugins.entertainment?.length || 0 }, + { key: 'message', title: '消息处理', count: categorizedPlugins.message?.length || 0 }, + ]; + }, [categorizedPlugins]); + + const handleInstall = async () => { + toast('该功能尚未完工,敬请期待', { + icon: '🚧', + duration: 3000, + }); + }; + + return ( + <> + 插件商店 - NapCat WebUI +
+ + + {/* 头部 */} +
+

插件商店

+ +
+ + {/* 搜索框 */} +
+ } + value={searchQuery} + onValueChange={setSearchQuery} + className="max-w-md" + /> +
+ + {/* 标签页 */} + setActiveTab(String(key))} + classNames={{ + tabList: 'bg-white/40 dark:bg-black/20 backdrop-blur-md', + cursor: 'bg-white/80 dark:bg-white/10 backdrop-blur-md shadow-sm', + }} + > + {tabs.map((tab) => ( + + +
+ {categorizedPlugins[tab.key]?.map((plugin) => ( + + ))} +
+
+ ))} +
+
+ + ); +} diff --git a/packages/napcat-webui-frontend/src/types/plugin-store.ts b/packages/napcat-webui-frontend/src/types/plugin-store.ts new file mode 100644 index 00000000..f54f4b2a --- /dev/null +++ b/packages/napcat-webui-frontend/src/types/plugin-store.ts @@ -0,0 +1,27 @@ +// 插件商店相关类型定义 + +export interface PluginStoreItem { + id: string; // 插件唯一标识 + name: string; // 插件名称 + version: string; // 最新版本 + description: string; // 插件描述 + author: string; // 作者 + homepage?: string; // 主页链接 + repository?: string; // 仓库地址 + downloadUrl: string; // 下载地址 + tags?: string[]; // 标签 + icon?: string; // 图标URL + screenshots?: string[]; // 截图 + minNapCatVersion?: string; // 最低NapCat版本要求 + dependencies?: Record; // 依赖 + downloads?: number; // 下载次数 + rating?: number; // 评分 + createdAt?: string; // 创建时间 + updatedAt?: string; // 更新时间 +} + +export interface PluginStoreList { + version: string; // 索引版本 + updateTime: string; // 更新时间 + plugins: PluginStoreItem[]; // 插件列表 +}