@@ -7,19 +7,15 @@ import { pipeline } from 'stream/promises';
77import { createWriteStream } from 'fs' ;
88import compressing from 'compressing' ;
99import { findAvailableDownloadUrl , GITHUB_RAW_MIRRORS } from 'napcat-common/src/mirror' ;
10+ import { webUiPathWrapper } from '@/napcat-webui-backend/index' ;
1011
1112// 插件商店源配置
1213const PLUGIN_STORE_SOURCES = [
1314 'https://raw.githubusercontent.com/NapNeko/napcat-plugin-index/main/plugins.v4.json' ,
1415] ;
1516
16- // 插件目录
17- const PLUGINS_DIR = path . join ( process . cwd ( ) , 'plugins' ) ;
18-
19- // 确保插件目录存在
20- if ( ! fs . existsSync ( PLUGINS_DIR ) ) {
21- fs . mkdirSync ( PLUGINS_DIR , { recursive : true } ) ;
22- }
17+ // 插件目录 - 使用 pathWrapper
18+ const getPluginsDir = ( ) => webUiPathWrapper . pluginPath ;
2319
2420// 插件列表缓存
2521let pluginListCache : PluginStoreList | null = null ;
@@ -80,7 +76,7 @@ async function fetchPluginList (): Promise<PluginStoreList> {
8076 * 下载文件,使用镜像系统
8177 * 自动识别 GitHub Release URL 并使用镜像加速
8278 */
83- async function downloadFile ( url : string , destPath : string ) : Promise < void > {
79+ async function downloadFile ( url : string , destPath : string , customMirror ?: string ) : Promise < void > {
8480 try {
8581 let downloadUrl : string ;
8682
@@ -91,25 +87,36 @@ async function downloadFile (url: string, destPath: string): Promise<void> {
9187 if ( githubReleasePattern . test ( url ) ) {
9288 // 使用镜像系统查找可用的下载 URL(支持 GitHub Release 镜像)
9389 console . log ( `Detected GitHub Release URL: ${ url } ` ) ;
90+ console . log ( `Custom mirror: ${ customMirror || 'auto' } ` ) ;
91+
9492 downloadUrl = await findAvailableDownloadUrl ( url , {
95- validateContent : true ,
96- minFileSize : 1024 , // 最小 1KB
97- timeout : 60000 , // 60秒超时
98- useFastMirrors : true , // 使用快速镜像列表
93+ validateContent : false , // 不验证内容,只检查状态码和 Content-Type
94+ timeout : 5000 , // 每个镜像测试5秒超时
95+ useFastMirrors : false , // 不使用快速镜像列表(避免测速阻塞)
96+ customMirror : customMirror || undefined , // 使用用户选择的镜像
9997 } ) ;
98+
99+ console . log ( `Selected download URL: ${ downloadUrl } ` ) ;
100100 } else {
101101 // 其他URL直接下载
102102 console . log ( `Direct download URL: ${ url } ` ) ;
103103 downloadUrl = url ;
104104 }
105105
106- console . log ( `Downloading from: ${ downloadUrl } ` ) ;
106+ console . log ( `Starting download from: ${ downloadUrl } ` ) ;
107+
108+ // 确保目标目录存在
109+ const destDir = path . dirname ( destPath ) ;
110+ if ( ! fs . existsSync ( destDir ) ) {
111+ fs . mkdirSync ( destDir , { recursive : true } ) ;
112+ console . log ( `Created directory: ${ destDir } ` ) ;
113+ }
107114
108115 const response = await fetch ( downloadUrl , {
109116 headers : {
110117 'User-Agent' : 'NapCat-WebUI' ,
111118 } ,
112- signal : AbortSignal . timeout ( 60000 ) ,
119+ signal : AbortSignal . timeout ( 120000 ) , // 实际下载120秒超时
113120 } ) ;
114121
115122 if ( ! response . ok ) {
@@ -138,20 +145,38 @@ async function downloadFile (url: string, destPath: string): Promise<void> {
138145 * 解压插件到指定目录
139146 */
140147async function extractPlugin ( zipPath : string , pluginId : string ) : Promise < void > {
148+ const PLUGINS_DIR = getPluginsDir ( ) ;
141149 const pluginDir = path . join ( PLUGINS_DIR , pluginId ) ;
142150
151+ console . log ( `[extractPlugin] PLUGINS_DIR: ${ PLUGINS_DIR } ` ) ;
152+ console . log ( `[extractPlugin] pluginId: ${ pluginId } ` ) ;
153+ console . log ( `[extractPlugin] Target directory: ${ pluginDir } ` ) ;
154+ console . log ( `[extractPlugin] Zip file: ${ zipPath } ` ) ;
155+
156+ // 确保插件根目录存在
157+ if ( ! fs . existsSync ( PLUGINS_DIR ) ) {
158+ fs . mkdirSync ( PLUGINS_DIR , { recursive : true } ) ;
159+ console . log ( `[extractPlugin] Created plugins root directory: ${ PLUGINS_DIR } ` ) ;
160+ }
161+
143162 // 如果目录已存在,先删除
144163 if ( fs . existsSync ( pluginDir ) ) {
164+ console . log ( `[extractPlugin] Directory exists, removing: ${ pluginDir } ` ) ;
145165 fs . rmSync ( pluginDir , { recursive : true , force : true } ) ;
146166 }
147167
148168 // 创建插件目录
149169 fs . mkdirSync ( pluginDir , { recursive : true } ) ;
170+ console . log ( `[extractPlugin] Created directory: ${ pluginDir } ` ) ;
150171
151172 // 解压
152173 await compressing . zip . uncompress ( zipPath , pluginDir ) ;
153174
154- //console.log(`Plugin extracted to: ${pluginDir}`);
175+ console . log ( `[extractPlugin] Plugin extracted to: ${ pluginDir } ` ) ;
176+
177+ // 列出解压后的文件
178+ const files = fs . readdirSync ( pluginDir ) ;
179+ console . log ( `[extractPlugin] Extracted files:` , files ) ;
155180}
156181
157182/**
@@ -186,11 +211,11 @@ export const GetPluginStoreDetailHandler: RequestHandler = async (req, res) => {
186211} ;
187212
188213/**
189- * 安装插件(从商店)
214+ * 安装插件(从商店)- 普通 POST 接口
190215 */
191216export const InstallPluginFromStoreHandler : RequestHandler = async ( req , res ) => {
192217 try {
193- const { id } = req . body ;
218+ const { id, mirror } = req . body ;
194219
195220 if ( ! id ) {
196221 return sendError ( res , 'Plugin ID is required' ) ;
@@ -205,10 +230,11 @@ export const InstallPluginFromStoreHandler: RequestHandler = async (req, res) =>
205230 }
206231
207232 // 下载插件
233+ const PLUGINS_DIR = getPluginsDir ( ) ;
208234 const tempZipPath = path . join ( PLUGINS_DIR , `${ id } .temp.zip` ) ;
209235
210236 try {
211- await downloadFile ( plugin . downloadUrl , tempZipPath ) ;
237+ await downloadFile ( plugin . downloadUrl , tempZipPath , mirror ) ;
212238
213239 // 解压插件
214240 await extractPlugin ( tempZipPath , id ) ;
@@ -232,3 +258,83 @@ export const InstallPluginFromStoreHandler: RequestHandler = async (req, res) =>
232258 return sendError ( res , 'Failed to install plugin: ' + e . message ) ;
233259 }
234260} ;
261+
262+ /**
263+ * 安装插件(从商店)- SSE 版本,实时推送进度
264+ */
265+ export const InstallPluginFromStoreSSEHandler : RequestHandler = async ( req , res ) => {
266+ const { id, mirror } = req . query ;
267+
268+ if ( ! id || typeof id !== 'string' ) {
269+ res . status ( 400 ) . json ( { error : 'Plugin ID is required' } ) ;
270+ return ;
271+ }
272+
273+ // 设置 SSE 响应头
274+ res . setHeader ( 'Content-Type' , 'text/event-stream' ) ;
275+ res . setHeader ( 'Cache-Control' , 'no-cache' ) ;
276+ res . setHeader ( 'Connection' , 'keep-alive' ) ;
277+ res . flushHeaders ( ) ;
278+
279+ const sendProgress = ( message : string , progress ?: number ) => {
280+ res . write ( `data: ${ JSON . stringify ( { message, progress } ) } \n\n` ) ;
281+ } ;
282+
283+ try {
284+ sendProgress ( '正在获取插件信息...' , 10 ) ;
285+
286+ // 获取插件信息
287+ const data = await fetchPluginList ( ) ;
288+ const plugin = data . plugins . find ( p => p . id === id ) ;
289+
290+ if ( ! plugin ) {
291+ sendProgress ( '错误: 插件不存在' , 0 ) ;
292+ res . write ( `data: ${ JSON . stringify ( { error : 'Plugin not found in store' } ) } \n\n` ) ;
293+ res . end ( ) ;
294+ return ;
295+ }
296+
297+ sendProgress ( `找到插件: ${ plugin . name } v${ plugin . version } ` , 20 ) ;
298+ sendProgress ( `下载地址: ${ plugin . downloadUrl } ` , 25 ) ;
299+
300+ if ( mirror && typeof mirror === 'string' ) {
301+ sendProgress ( `使用镜像: ${ mirror } ` , 28 ) ;
302+ }
303+
304+ // 下载插件
305+ const PLUGINS_DIR = getPluginsDir ( ) ;
306+ const tempZipPath = path . join ( PLUGINS_DIR , `${ id } .temp.zip` ) ;
307+
308+ try {
309+ sendProgress ( '正在下载插件...' , 30 ) ;
310+ await downloadFile ( plugin . downloadUrl , tempZipPath , mirror as string | undefined ) ;
311+
312+ sendProgress ( '下载完成,正在解压...' , 70 ) ;
313+ await extractPlugin ( tempZipPath , id ) ;
314+
315+ sendProgress ( '解压完成,正在清理...' , 90 ) ;
316+ fs . unlinkSync ( tempZipPath ) ;
317+
318+ sendProgress ( '安装成功!' , 100 ) ;
319+ res . write ( `data: ${ JSON . stringify ( {
320+ success : true ,
321+ message : 'Plugin installed successfully' ,
322+ plugin : plugin ,
323+ installPath : path . join ( PLUGINS_DIR , id ) ,
324+ } ) } \n\n`) ;
325+ res . end ( ) ;
326+ } catch ( downloadError : any ) {
327+ // 清理临时文件
328+ if ( fs . existsSync ( tempZipPath ) ) {
329+ fs . unlinkSync ( tempZipPath ) ;
330+ }
331+ sendProgress ( `错误: ${ downloadError . message } ` , 0 ) ;
332+ res . write ( `data: ${ JSON . stringify ( { error : downloadError . message } ) } \n\n` ) ;
333+ res . end ( ) ;
334+ }
335+ } catch ( e : any ) {
336+ sendProgress ( `错误: ${ e . message } ` , 0 ) ;
337+ res . write ( `data: ${ JSON . stringify ( { error : e . message } ) } \n\n` ) ;
338+ res . end ( ) ;
339+ }
340+ } ;
0 commit comments