From 24623f18d8a83dc40bfee14d483ffe8b9d98afbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Tue, 27 Jan 2026 22:03:47 +0800 Subject: [PATCH] Implement real plugin store backend and install logic Replaces mock plugin store data with live fetching from a remote source using mirrors and caching. Implements actual plugin download, extraction, and installation logic in the backend. Updates frontend to call the new install API and display installation results, and removes unused icon/rating display from plugin cards. --- .../src/api/PluginStore.ts | 305 +++++++++++------- .../display_card/plugin_store_card.tsx | 25 +- .../src/pages/dashboard/plugin_store.tsx | 15 +- 3 files changed, 197 insertions(+), 148 deletions(-) diff --git a/packages/napcat-webui-backend/src/api/PluginStore.ts b/packages/napcat-webui-backend/src/api/PluginStore.ts index ce299c40..77d5d051 100644 --- a/packages/napcat-webui-backend/src/api/PluginStore.ts +++ b/packages/napcat-webui-backend/src/api/PluginStore.ts @@ -1,126 +1,166 @@ import { RequestHandler } from 'express'; import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response'; import { PluginStoreList } from '@/napcat-webui-backend/src/types/PluginStore'; +import * as fs from 'fs'; +import * as path from 'path'; +import { pipeline } from 'stream/promises'; +import { createWriteStream } from 'fs'; +import compressing from 'compressing'; +import { findAvailableDownloadUrl, GITHUB_RAW_MIRRORS } from 'napcat-common/src/mirror'; -// 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', - }, - ], -}; +// 插件商店源配置 +const PLUGIN_STORE_SOURCES = [ + 'https://raw.githubusercontent.com/NapNeko/napcat-plugin-index/main/plugins.v4.json', +]; + +// 插件目录 +const PLUGINS_DIR = path.join(process.cwd(), 'plugins'); + +// 确保插件目录存在 +if (!fs.existsSync(PLUGINS_DIR)) { + fs.mkdirSync(PLUGINS_DIR, { recursive: true }); +} + +// 插件列表缓存 +let pluginListCache: PluginStoreList | null = null; +let cacheTimestamp: number = 0; +const CACHE_TTL = 10 * 60 * 1000; // 10分钟缓存 + +/** + * 从多个源获取插件列表,使用镜像系统 + * 带10分钟缓存 + */ +async function fetchPluginList (): Promise { + // 检查缓存 + const now = Date.now(); + if (pluginListCache && (now - cacheTimestamp) < CACHE_TTL) { + //console.log('Using cached plugin list'); + return pluginListCache; + } + + const errors: string[] = []; + + for (const source of PLUGIN_STORE_SOURCES) { + // 使用镜像系统的 raw 镜像列表 + for (const mirror of GITHUB_RAW_MIRRORS) { + try { + const url = mirror ? `${mirror}/${source.replace('https://raw.githubusercontent.com/', '')}` : source; + + const response = await fetch(url, { + headers: { + 'User-Agent': 'NapCat-WebUI', + }, + signal: AbortSignal.timeout(10000), // 10秒超时 + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + //console.log(`Successfully fetched plugin list from: ${url}`); + + // 更新缓存 + pluginListCache = data as PluginStoreList; + cacheTimestamp = now; + + return pluginListCache; + } catch (e: any) { + const errorMsg = `Failed to fetch from ${source} via mirror: ${e.message}`; + console.warn(errorMsg); + errors.push(errorMsg); + } + } + } + + throw new Error(`All plugin sources failed:\n${errors.join('\n')}`); +} + +/** + * 下载文件,使用镜像系统 + * 自动识别 GitHub Release URL 并使用镜像加速 + */ +async function downloadFile (url: string, destPath: string): Promise { + try { + let downloadUrl: string; + + // 判断是否是 GitHub Release URL + // 格式: https://github.com/{owner}/{repo}/releases/download/{tag}/{filename} + const githubReleasePattern = /^https:\/\/github\.com\/[^/]+\/[^/]+\/releases\/download\//; + + if (githubReleasePattern.test(url)) { + // 使用镜像系统查找可用的下载 URL(支持 GitHub Release 镜像) + console.log(`Detected GitHub Release URL: ${url}`); + downloadUrl = await findAvailableDownloadUrl(url, { + validateContent: true, + minFileSize: 1024, // 最小 1KB + timeout: 60000, // 60秒超时 + useFastMirrors: true, // 使用快速镜像列表 + }); + } else { + // 其他URL直接下载 + console.log(`Direct download URL: ${url}`); + downloadUrl = url; + } + + console.log(`Downloading from: ${downloadUrl}`); + + const response = await fetch(downloadUrl, { + headers: { + 'User-Agent': 'NapCat-WebUI', + }, + signal: AbortSignal.timeout(60000), + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + if (!response.body) { + throw new Error('Response body is null'); + } + + // 写入文件 + const fileStream = createWriteStream(destPath); + await pipeline(response.body as any, fileStream); + + console.log(`Successfully downloaded to: ${destPath}`); + } catch (e: any) { + // 删除可能的不完整文件 + if (fs.existsSync(destPath)) { + fs.unlinkSync(destPath); + } + throw new Error(`Download failed: ${e.message}`); + } +} + +/** + * 解压插件到指定目录 + */ +async function extractPlugin (zipPath: string, pluginId: string): Promise { + const pluginDir = path.join(PLUGINS_DIR, pluginId); + + // 如果目录已存在,先删除 + if (fs.existsSync(pluginDir)) { + fs.rmSync(pluginDir, { recursive: true, force: true }); + } + + // 创建插件目录 + fs.mkdirSync(pluginDir, { recursive: true }); + + // 解压 + await compressing.zip.uncompress(zipPath, pluginDir); + + //console.log(`Plugin extracted to: ${pluginDir}`); +} /** * 获取插件商店列表 - * 未来可以从远程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); + const data = await fetchPluginList(); + return sendSuccess(res, data); } catch (e: any) { return sendError(res, 'Failed to fetch plugin store list: ' + e.message); } @@ -132,7 +172,8 @@ export const GetPluginStoreListHandler: RequestHandler = async (_req, res) => { export const GetPluginStoreDetailHandler: RequestHandler = async (req, res) => { try { const { id } = req.params; - const plugin = mockPluginStoreData.plugins.find(p => p.id === id); + const data = await fetchPluginList(); + const plugin = data.plugins.find(p => p.id === id); if (!plugin) { return sendError(res, 'Plugin not found'); @@ -146,7 +187,6 @@ export const GetPluginStoreDetailHandler: RequestHandler = async (req, res) => { /** * 安装插件(从商店) - * TODO: 实现实际的下载和安装逻辑 */ export const InstallPluginFromStoreHandler: RequestHandler = async (req, res) => { try { @@ -156,21 +196,38 @@ export const InstallPluginFromStoreHandler: RequestHandler = async (req, res) => return sendError(res, 'Plugin ID is required'); } - const plugin = mockPluginStoreData.plugins.find(p => p.id === id); + // 获取插件信息 + const data = await fetchPluginList(); + const plugin = data.plugins.find(p => p.id === id); if (!plugin) { return sendError(res, 'Plugin not found in store'); } - // TODO: 实现实际的下载和安装逻辑 - // 1. 下载插件文件 - // 2. 解压到插件目录 - // 3. 加载插件 + // 下载插件 + const tempZipPath = path.join(PLUGINS_DIR, `${id}.temp.zip`); - return sendSuccess(res, { - message: 'Plugin installation started', - plugin: plugin - }); + try { + await downloadFile(plugin.downloadUrl, tempZipPath); + + // 解压插件 + await extractPlugin(tempZipPath, id); + + // 删除临时文件 + fs.unlinkSync(tempZipPath); + + return sendSuccess(res, { + message: 'Plugin installed successfully', + plugin: plugin, + installPath: path.join(PLUGINS_DIR, id), + }); + } catch (downloadError: any) { + // 清理临时文件 + if (fs.existsSync(tempZipPath)) { + fs.unlinkSync(tempZipPath); + } + throw downloadError; + } } catch (e: any) { return sendError(res, 'Failed to install plugin: ' + e.message); } 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 index f2fa577b..d76e0161 100644 --- 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 @@ -1,7 +1,7 @@ import { Button } from '@heroui/button'; import { Chip } from '@heroui/chip'; import { useState } from 'react'; -import { IoMdStar, IoMdDownload } from 'react-icons/io'; +import { IoMdDownload } from 'react-icons/io'; import DisplayCardContainer from './container'; import { PluginStoreItem } from '@/types/plugin-store'; @@ -15,7 +15,7 @@ const PluginStoreCard: React.FC = ({ data, onInstall, }) => { - const { name, version, author, description, tags, rating, icon } = data; + const { name, version, author, description, tags, id } = data; const [processing, setProcessing] = useState(false); const handleInstall = () => { @@ -37,17 +37,7 @@ const PluginStoreCard: React.FC = ({ v{version} } - enableSwitch={ - icon ? ( -
- {name} -
- ) : undefined - } + enableSwitch={undefined} action={