mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-03-03 09:10:25 +00:00
Add plugin install SSE API and mirror selection UI
Introduces a new SSE-based plugin installation API for real-time progress updates and adds frontend support for selecting download mirrors, especially for GitHub-based plugins. Refactors backend plugin directory handling, improves logging, and updates the frontend to use the new API with user-selectable mirrors and progress feedback.
This commit is contained in:
@@ -7,19 +7,15 @@ import { pipeline } from 'stream/promises';
|
||||
import { createWriteStream } from 'fs';
|
||||
import compressing from 'compressing';
|
||||
import { findAvailableDownloadUrl, GITHUB_RAW_MIRRORS } from 'napcat-common/src/mirror';
|
||||
import { webUiPathWrapper } from '@/napcat-webui-backend/index';
|
||||
|
||||
// 插件商店源配置
|
||||
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 });
|
||||
}
|
||||
// 插件目录 - 使用 pathWrapper
|
||||
const getPluginsDir = () => webUiPathWrapper.pluginPath;
|
||||
|
||||
// 插件列表缓存
|
||||
let pluginListCache: PluginStoreList | null = null;
|
||||
@@ -80,7 +76,7 @@ async function fetchPluginList (): Promise<PluginStoreList> {
|
||||
* 下载文件,使用镜像系统
|
||||
* 自动识别 GitHub Release URL 并使用镜像加速
|
||||
*/
|
||||
async function downloadFile (url: string, destPath: string): Promise<void> {
|
||||
async function downloadFile (url: string, destPath: string, customMirror?: string): Promise<void> {
|
||||
try {
|
||||
let downloadUrl: string;
|
||||
|
||||
@@ -91,25 +87,36 @@ async function downloadFile (url: string, destPath: string): Promise<void> {
|
||||
if (githubReleasePattern.test(url)) {
|
||||
// 使用镜像系统查找可用的下载 URL(支持 GitHub Release 镜像)
|
||||
console.log(`Detected GitHub Release URL: ${url}`);
|
||||
console.log(`Custom mirror: ${customMirror || 'auto'}`);
|
||||
|
||||
downloadUrl = await findAvailableDownloadUrl(url, {
|
||||
validateContent: true,
|
||||
minFileSize: 1024, // 最小 1KB
|
||||
timeout: 60000, // 60秒超时
|
||||
useFastMirrors: true, // 使用快速镜像列表
|
||||
validateContent: false, // 不验证内容,只检查状态码和 Content-Type
|
||||
timeout: 5000, // 每个镜像测试5秒超时
|
||||
useFastMirrors: false, // 不使用快速镜像列表(避免测速阻塞)
|
||||
customMirror: customMirror || undefined, // 使用用户选择的镜像
|
||||
});
|
||||
|
||||
console.log(`Selected download URL: ${downloadUrl}`);
|
||||
} else {
|
||||
// 其他URL直接下载
|
||||
console.log(`Direct download URL: ${url}`);
|
||||
downloadUrl = url;
|
||||
}
|
||||
|
||||
console.log(`Downloading from: ${downloadUrl}`);
|
||||
console.log(`Starting download from: ${downloadUrl}`);
|
||||
|
||||
// 确保目标目录存在
|
||||
const destDir = path.dirname(destPath);
|
||||
if (!fs.existsSync(destDir)) {
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
console.log(`Created directory: ${destDir}`);
|
||||
}
|
||||
|
||||
const response = await fetch(downloadUrl, {
|
||||
headers: {
|
||||
'User-Agent': 'NapCat-WebUI',
|
||||
},
|
||||
signal: AbortSignal.timeout(60000),
|
||||
signal: AbortSignal.timeout(120000), // 实际下载120秒超时
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -138,20 +145,38 @@ async function downloadFile (url: string, destPath: string): Promise<void> {
|
||||
* 解压插件到指定目录
|
||||
*/
|
||||
async function extractPlugin (zipPath: string, pluginId: string): Promise<void> {
|
||||
const PLUGINS_DIR = getPluginsDir();
|
||||
const pluginDir = path.join(PLUGINS_DIR, pluginId);
|
||||
|
||||
console.log(`[extractPlugin] PLUGINS_DIR: ${PLUGINS_DIR}`);
|
||||
console.log(`[extractPlugin] pluginId: ${pluginId}`);
|
||||
console.log(`[extractPlugin] Target directory: ${pluginDir}`);
|
||||
console.log(`[extractPlugin] Zip file: ${zipPath}`);
|
||||
|
||||
// 确保插件根目录存在
|
||||
if (!fs.existsSync(PLUGINS_DIR)) {
|
||||
fs.mkdirSync(PLUGINS_DIR, { recursive: true });
|
||||
console.log(`[extractPlugin] Created plugins root directory: ${PLUGINS_DIR}`);
|
||||
}
|
||||
|
||||
// 如果目录已存在,先删除
|
||||
if (fs.existsSync(pluginDir)) {
|
||||
console.log(`[extractPlugin] Directory exists, removing: ${pluginDir}`);
|
||||
fs.rmSync(pluginDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// 创建插件目录
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
console.log(`[extractPlugin] Created directory: ${pluginDir}`);
|
||||
|
||||
// 解压
|
||||
await compressing.zip.uncompress(zipPath, pluginDir);
|
||||
|
||||
//console.log(`Plugin extracted to: ${pluginDir}`);
|
||||
console.log(`[extractPlugin] Plugin extracted to: ${pluginDir}`);
|
||||
|
||||
// 列出解压后的文件
|
||||
const files = fs.readdirSync(pluginDir);
|
||||
console.log(`[extractPlugin] Extracted files:`, files);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,11 +211,11 @@ export const GetPluginStoreDetailHandler: RequestHandler = async (req, res) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 安装插件(从商店)
|
||||
* 安装插件(从商店)- 普通 POST 接口
|
||||
*/
|
||||
export const InstallPluginFromStoreHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.body;
|
||||
const { id, mirror } = req.body;
|
||||
|
||||
if (!id) {
|
||||
return sendError(res, 'Plugin ID is required');
|
||||
@@ -205,10 +230,11 @@ export const InstallPluginFromStoreHandler: RequestHandler = async (req, res) =>
|
||||
}
|
||||
|
||||
// 下载插件
|
||||
const PLUGINS_DIR = getPluginsDir();
|
||||
const tempZipPath = path.join(PLUGINS_DIR, `${id}.temp.zip`);
|
||||
|
||||
try {
|
||||
await downloadFile(plugin.downloadUrl, tempZipPath);
|
||||
await downloadFile(plugin.downloadUrl, tempZipPath, mirror);
|
||||
|
||||
// 解压插件
|
||||
await extractPlugin(tempZipPath, id);
|
||||
@@ -232,3 +258,83 @@ export const InstallPluginFromStoreHandler: RequestHandler = async (req, res) =>
|
||||
return sendError(res, 'Failed to install plugin: ' + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 安装插件(从商店)- SSE 版本,实时推送进度
|
||||
*/
|
||||
export const InstallPluginFromStoreSSEHandler: RequestHandler = async (req, res) => {
|
||||
const { id, mirror } = req.query;
|
||||
|
||||
if (!id || typeof id !== 'string') {
|
||||
res.status(400).json({ error: 'Plugin ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置 SSE 响应头
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.flushHeaders();
|
||||
|
||||
const sendProgress = (message: string, progress?: number) => {
|
||||
res.write(`data: ${JSON.stringify({ message, progress })}\n\n`);
|
||||
};
|
||||
|
||||
try {
|
||||
sendProgress('正在获取插件信息...', 10);
|
||||
|
||||
// 获取插件信息
|
||||
const data = await fetchPluginList();
|
||||
const plugin = data.plugins.find(p => p.id === id);
|
||||
|
||||
if (!plugin) {
|
||||
sendProgress('错误: 插件不存在', 0);
|
||||
res.write(`data: ${JSON.stringify({ error: 'Plugin not found in store' })}\n\n`);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
sendProgress(`找到插件: ${plugin.name} v${plugin.version}`, 20);
|
||||
sendProgress(`下载地址: ${plugin.downloadUrl}`, 25);
|
||||
|
||||
if (mirror && typeof mirror === 'string') {
|
||||
sendProgress(`使用镜像: ${mirror}`, 28);
|
||||
}
|
||||
|
||||
// 下载插件
|
||||
const PLUGINS_DIR = getPluginsDir();
|
||||
const tempZipPath = path.join(PLUGINS_DIR, `${id}.temp.zip`);
|
||||
|
||||
try {
|
||||
sendProgress('正在下载插件...', 30);
|
||||
await downloadFile(plugin.downloadUrl, tempZipPath, mirror as string | undefined);
|
||||
|
||||
sendProgress('下载完成,正在解压...', 70);
|
||||
await extractPlugin(tempZipPath, id);
|
||||
|
||||
sendProgress('解压完成,正在清理...', 90);
|
||||
fs.unlinkSync(tempZipPath);
|
||||
|
||||
sendProgress('安装成功!', 100);
|
||||
res.write(`data: ${JSON.stringify({
|
||||
success: true,
|
||||
message: 'Plugin installed successfully',
|
||||
plugin: plugin,
|
||||
installPath: path.join(PLUGINS_DIR, id),
|
||||
})}\n\n`);
|
||||
res.end();
|
||||
} catch (downloadError: any) {
|
||||
// 清理临时文件
|
||||
if (fs.existsSync(tempZipPath)) {
|
||||
fs.unlinkSync(tempZipPath);
|
||||
}
|
||||
sendProgress(`错误: ${downloadError.message}`, 0);
|
||||
res.write(`data: ${JSON.stringify({ error: downloadError.message })}\n\n`);
|
||||
res.end();
|
||||
}
|
||||
} catch (e: any) {
|
||||
sendProgress(`错误: ${e.message}`, 0);
|
||||
res.write(`data: ${JSON.stringify({ error: e.message })}\n\n`);
|
||||
res.end();
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user