Compare commits

...

7 Commits

Author SHA1 Message Date
手瓜一十雪
a5769b6a62 Expose plugin pages at /plugin/:id/page/:path
Add a public route to serve plugin extension pages without auth and update related pieces accordingly. Backend: register GET /plugin/:pluginId/page/:pagePath to locate the plugin router, validate page and HTML file existence, and send the file (returns appropriate 4xx/5xx errors). Frontend: switch iframe and new-window URLs to the new unauthenticated route (remove webui_token usage). Builtin plugin: clarify page registration comment and add a log line for the extension page URL. Minor formatting whitespace tweaks in plugin manager type annotations.
2026-02-02 15:40:18 +08:00
手瓜一十雪
d9297c1e10 Bump napcat-types & add plugin static/memory tests
Upgrade napcat-types to v0.0.15 and update the built-in plugin UI to test both filesystem and in-memory static resources. dashboard.html: clarify which plugin endpoints require auth, add buttons and a testMemoryResource() function that fetches an in-memory JSON resource, and add staticBase/memBase variables for non-auth static routes. napcat-webui-backend: return after 404 for missing memory files to stop further handling. (Lockfile updated accordingly.)
2026-02-02 15:35:26 +08:00
手瓜一十雪
94f07ab98b Support memory static files and plugin APIs
Introduce in-memory static file support and inter-plugin exports. Add MemoryStaticFile/MemoryFileGenerator types and expose staticOnMem in PluginRouterRegistry; router registry now tracks memory routes and exposes getters. Add getPluginExports to plugin manager adapters to allow plugins to call each other's exported modules. WebUI backend gains routes to serve /plugin/:pluginId/mem/* (memory files) and /plugin/:pluginId/files/* (plugin filesystem static) without auth. Update builtin plugin to demonstrate staticOnMem and inter-plugin call, and add frontend UI to open extension pages in a new window. Note: API router no longer mounts static filesystem routes — those are handled by webui-backend.
2026-02-02 15:01:26 +08:00
手瓜一十雪
01a6594707 Add image-size alias to napcat-schema vite config
Add a new Vite path alias '@/napcat-image-size' in packages/napcat-schema/vite.config.ts pointing to ../napcat-image-size. This enables cleaner imports of the napcat-image-size package from within napcat-schema source files.
2026-02-02 14:15:27 +08:00
手瓜一十雪
82a7154b92 fix: #1574 & Clear CJS cache and reload plugins on install
Add support for clearing CommonJS module cache when unloading plugins and reload plugins on install. PluginLoader now uses createRequire to access require.cache and exposes clearCache(pluginPath) to remove cached modules under the plugin directory; plugin manager calls this when unloading. Web UI backend install handlers now reload an existing plugin (with progress updates) instead of skipping registration, ensuring updated code/metadata take effect.
2026-02-02 14:14:06 +08:00
手瓜一十雪
9b385ac9c9 Ignore env file and db-shm in .gitignore
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Add packages/napcat-develop/config/.env to .gitignore to prevent committing environment configuration, and restore the guild1.db-shm entry (fix EOF/newline).
2026-02-02 13:22:29 +08:00
手瓜一十雪
e3d4cee416 Rename .env to .env.example
Rename the tracked dotenv file to .env.example (identical content) to avoid committing environment secrets and provide a template/example for project configuration.
2026-02-02 13:21:34 +08:00
16 changed files with 352 additions and 34 deletions

3
.gitignore vendored
View File

@@ -16,4 +16,5 @@ checkVersion.sh
bun.lockb
tests/run/
guild1.db-wal
guild1.db-shm
guild1.db-shm
packages/napcat-develop/config/.env

View File

@@ -28,6 +28,7 @@ export { PluginModule } from './plugin/types';
export { PluginStatusConfig } from './plugin/types';
export { PluginRouterRegistry, PluginRequestHandler, PluginApiRouteDefinition, PluginPageDefinition, HttpMethod } from './plugin/types';
export { PluginHttpRequest, PluginHttpResponse, PluginNextFunction } from './plugin/types';
export { MemoryStaticFile, MemoryFileGenerator } from './plugin/types';
export { PluginRouterRegistryImpl } from './plugin/router-registry';
export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> implements IPluginManager {
private readonly pluginPath: string;
@@ -179,6 +180,9 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> i
this.pluginRouters.delete(entry.id);
}
// 清理模块缓存
this.loader.clearCache(entry.pluginPath);
// 重置状态
entry.loaded = false;
entry.runtime = {
@@ -211,6 +215,15 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> i
// 保存到路由注册表
this.pluginRouters.set(entry.id, routerRegistry);
// 创建获取其他插件导出的方法
const getPluginExports = <T = any> (pluginId: string): T | undefined => {
const targetEntry = this.plugins.get(pluginId);
if (!targetEntry || !targetEntry.loaded || targetEntry.runtime.status !== 'loaded') {
return undefined;
}
return targetEntry.runtime.module as T;
};
return {
core: this.core,
oneBot: this.obContext,
@@ -224,6 +237,7 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> i
pluginManager: this,
logger: pluginLogger,
router: routerRegistry,
getPluginExports,
};
}

View File

@@ -1,5 +1,7 @@
import fs from 'fs';
import path from 'path';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
import { LogWrapper } from 'napcat-core/helper/log';
import {
PluginPackageJson,
@@ -295,4 +297,24 @@ export class PluginLoader {
return null;
}
/**
* 清除插件文件的 require 缓存
* 用于确保卸载插件时清理 CJS 模块缓存
*/
clearCache (pluginPath: string): void {
try {
// 规范化路径以确保匹配正确
const normalizedPluginPath = path.resolve(pluginPath);
// 遍历缓存并删除属于该插件目录的模块
Object.keys(require.cache).forEach((id) => {
if (id.startsWith(normalizedPluginPath)) {
delete require.cache[id];
this.logger.logDebug(`[PluginLoader] Cleared cache for: ${id}`);
}
});
} catch (e) {
this.logger.logError('[PluginLoader] Error clearing module cache:', e);
}
}
}

View File

@@ -194,6 +194,15 @@ export class OB11PluginManager extends IOB11NetworkAdapter<PluginConfig> impleme
this.pluginRouters.set(entry.id, router);
}
// 创建获取其他插件导出的方法
const getPluginExports = <T = any> (pluginId: string): T | undefined => {
const targetEntry = this.plugins.get(pluginId);
if (!targetEntry || !targetEntry.loaded || targetEntry.runtime.status !== 'loaded') {
return undefined;
}
return targetEntry.runtime.module as T;
};
return {
core: this.core,
oneBot: this.obContext,
@@ -207,6 +216,7 @@ export class OB11PluginManager extends IOB11NetworkAdapter<PluginConfig> impleme
pluginManager: this,
logger: pluginLogger,
router,
getPluginExports,
};
}

View File

@@ -1,4 +1,4 @@
import { Router, static as expressStatic, Request, Response, NextFunction } from 'express';
import { Router, Request, Response, NextFunction } from 'express';
import path from 'path';
import {
PluginRouterRegistry,
@@ -8,6 +8,7 @@ import {
PluginHttpRequest,
PluginHttpResponse,
HttpMethod,
MemoryStaticFile,
} from './types';
/**
@@ -59,10 +60,17 @@ function wrapResponse (res: Response): PluginHttpResponse {
* 插件路由注册器实现
* 为每个插件创建独立的路由注册器,收集路由定义
*/
/** 内存静态路由定义 */
interface MemoryStaticRoute {
urlPath: string;
files: MemoryStaticFile[];
}
export class PluginRouterRegistryImpl implements PluginRouterRegistry {
private apiRoutes: PluginApiRouteDefinition[] = [];
private pageDefinitions: PluginPageDefinition[] = [];
private staticRoutes: Array<{ urlPath: string; localPath: string; }> = [];
private memoryStaticRoutes: MemoryStaticRoute[] = [];
constructor (
private readonly pluginId: string,
@@ -111,19 +119,19 @@ export class PluginRouterRegistryImpl implements PluginRouterRegistry {
this.staticRoutes.push({ urlPath, localPath: absolutePath });
}
staticOnMem (urlPath: string, files: MemoryStaticFile[]): void {
this.memoryStaticRoutes.push({ urlPath, files });
}
// ==================== 构建路由 ====================
/**
* 构建 Express Router用于 API 路由)
* 注意:静态资源路由不在此处挂载,由 webui-backend 直接在不需要鉴权的路径下处理
*/
buildApiRouter (): Router {
const router = Router();
// 注册静态文件路由
for (const { urlPath, localPath } of this.staticRoutes) {
router.use(urlPath, expressStatic(localPath));
}
// 注册 API 路由
for (const route of this.apiRoutes) {
const handler = this.wrapHandler(route.handler);
@@ -179,7 +187,14 @@ export class PluginRouterRegistryImpl implements PluginRouterRegistry {
* 检查是否有注册的 API 路由
*/
hasApiRoutes (): boolean {
return this.apiRoutes.length > 0 || this.staticRoutes.length > 0;
return this.apiRoutes.length > 0;
}
/**
* 检查是否有注册的静态资源路由
*/
hasStaticRoutes (): boolean {
return this.staticRoutes.length > 0 || this.memoryStaticRoutes.length > 0;
}
/**
@@ -210,6 +225,20 @@ export class PluginRouterRegistryImpl implements PluginRouterRegistry {
return this.pluginPath;
}
/**
* 获取所有注册的静态路由
*/
getStaticRoutes (): Array<{ urlPath: string; localPath: string; }> {
return [...this.staticRoutes];
}
/**
* 获取所有注册的内存静态路由
*/
getMemoryStaticRoutes (): MemoryStaticRoute[] {
return [...this.memoryStaticRoutes];
}
/**
* 清空路由(用于插件卸载)
*/
@@ -217,5 +246,6 @@ export class PluginRouterRegistryImpl implements PluginRouterRegistry {
this.apiRoutes = [];
this.pageDefinitions = [];
this.staticRoutes = [];
this.memoryStaticRoutes = [];
}
}

View File

@@ -125,6 +125,19 @@ export interface PluginPageDefinition {
description?: string;
}
/** 内存文件生成器 - 用于动态生成静态文件内容 */
export type MemoryFileGenerator = () => string | Buffer | Promise<string | Buffer>;
/** 内存静态文件定义 */
export interface MemoryStaticFile {
/** 文件路径(相对于 urlPath */
path: string;
/** 文件内容或生成器 */
content: string | Buffer | MemoryFileGenerator;
/** 可选的 MIME 类型 */
contentType?: string;
}
/** 插件路由注册器 */
export interface PluginRouterRegistry {
// ==================== API 路由注册 ====================
@@ -167,6 +180,13 @@ export interface PluginRouterRegistry {
* @param localPath 本地文件夹路径(相对于插件目录或绝对路径)
*/
static (urlPath: string, localPath: string): void;
/**
* 提供内存生成的静态文件服务
* @param urlPath URL 路径
* @param files 内存文件列表
*/
staticOnMem (urlPath: string, files: MemoryStaticFile[]): void;
}
// ==================== 插件管理器接口 ====================
@@ -247,8 +267,15 @@ export interface NapCatPluginContext {
/**
* WebUI 路由注册器
* 用于注册插件的 HTTP API 路由,路由将挂载到 /api/Plugin/ext/{pluginId}/
* 静态资源将挂载到 /plugin/{pluginId}/files/{urlPath}/
*/
router: PluginRouterRegistry;
/**
* 获取其他插件的导出模块
* @param pluginId 目标插件 ID
* @returns 插件导出的模块,如果插件未加载则返回 undefined
*/
getPluginExports: <T = PluginModule>(pluginId: string) => T | undefined;
}
// ==================== 插件模块接口 ====================

View File

@@ -65,10 +65,32 @@ const plugin_init: PluginModule['plugin_init'] = async (ctx) => {
// ==================== 注册 WebUI 路由示例 ====================
// 注册静态资源目录webui 目录下的文件可通过 /api/Plugin/ext/{pluginId}/static/ 访问)
// 注册静态资源目录
// 静态资源可通过 /plugin/{pluginId}/files/static/ 访问(无需鉴权)
ctx.router.static('/static', 'webui');
// 注册 API 路由
// 注册内存生成的静态资源(无需鉴权)
// 可通过 /plugin/{pluginId}/mem/dynamic/info.json 访问
ctx.router.staticOnMem('/dynamic', [
{
path: '/info.json',
contentType: 'application/json',
// 使用生成器函数动态生成内容
content: () => JSON.stringify({
pluginName: ctx.pluginName,
generatedAt: new Date().toISOString(),
uptime: Date.now() - startTime,
config: currentConfig
}, null, 2)
},
{
path: '/readme.txt',
contentType: 'text/plain',
content: `NapCat Builtin Plugin\n=====================\nThis is a demonstration of the staticOnMem feature.\nPlugin: ${ctx.pluginName}\nPath: ${ctx.pluginPath}`
}
]);
// 注册 API 路由(需要鉴权,挂载到 /api/Plugin/ext/{pluginId}/
ctx.router.get('/status', (_req, res) => {
const uptime = Date.now() - startTime;
res.json({
@@ -107,7 +129,38 @@ const plugin_init: PluginModule['plugin_init'] = async (ctx) => {
}
});
// 注册扩展页面
// ==================== 插件互调用示例 ====================
// 演示如何调用其他插件的导出方法
ctx.router.get('/call-plugin/:pluginId', (req, res) => {
const { pluginId } = req.params;
// 使用 getPluginExports 获取其他插件的导出模块
const targetPlugin = ctx.getPluginExports<PluginModule>(pluginId);
if (!targetPlugin) {
res.status(404).json({
code: -1,
message: `Plugin '${pluginId}' not found or not loaded`
});
return;
}
// 返回目标插件的信息
res.json({
code: 0,
data: {
pluginId,
hasInit: typeof targetPlugin.plugin_init === 'function',
hasOnMessage: typeof targetPlugin.plugin_onmessage === 'function',
hasOnEvent: typeof targetPlugin.plugin_onevent === 'function',
hasCleanup: typeof targetPlugin.plugin_cleanup === 'function',
hasConfigSchema: Array.isArray(targetPlugin.plugin_config_schema),
hasConfigUI: Array.isArray(targetPlugin.plugin_config_ui),
}
});
});
// 注册扩展页面(无需鉴权,可通过 /plugin/{pluginId}/page/dashboard 访问)
ctx.router.page({
path: 'dashboard',
title: '插件仪表盘',
@@ -116,7 +169,11 @@ const plugin_init: PluginModule['plugin_init'] = async (ctx) => {
description: '查看内置插件的运行状态和配置'
});
logger.info('WebUI 路由已注册: /api/Plugin/ext/' + ctx.pluginName);
logger.info('WebUI 路由已注册:');
logger.info(' - API 路由: /api/Plugin/ext/' + ctx.pluginName + '/');
logger.info(' - 扩展页面: /plugin/' + ctx.pluginName + '/page/dashboard');
logger.info(' - 静态资源: /plugin/' + ctx.pluginName + '/files/static/');
logger.info(' - 内存资源: /plugin/' + ctx.pluginName + '/mem/dynamic/');
};
export const plugin_get_config: PluginModule['plugin_get_config'] = async () => {

View File

@@ -7,7 +7,7 @@
"description": "NapCat 内置插件",
"author": "NapNeko",
"dependencies": {
"napcat-types": "0.0.14"
"napcat-types": "0.0.15"
},
"devDependencies": {
"@types/node": "^22.0.1"

View File

@@ -259,11 +259,14 @@
</div>
<div id="static-content">
<p style="color: var(--text-secondary); font-size: 13px; margin-bottom: 12px;">
测试插件静态资源服务是否正常工作
测试插件静态资源服务是否正常工作(不需要鉴权)
</p>
<div class="actions" style="margin-top: 0;">
<button class="btn btn-primary" onclick="testStaticResource()">
获取 test.txt
获取 test.txt(文件系统)
</button>
<button class="btn btn-primary" onclick="testMemoryResource()">
获取 info.json内存生成
</button>
</div>
<div id="static-result" style="margin-top: 12px;"></div>
@@ -280,8 +283,12 @@
const urlParams = new URLSearchParams(window.location.search);
const webuiToken = urlParams.get('webui_token') || '';
// 插件自行管理 API 调用
// 插件 API 基础路径(需要鉴权)
const apiBase = '/api/Plugin/ext/napcat-plugin-builtin';
// 插件静态资源基础路径(不需要鉴权)
const staticBase = '/plugin/napcat-plugin-builtin/files';
// 插件内存资源基础路径(不需要鉴权)
const memBase = '/plugin/napcat-plugin-builtin/mem';
// 封装 fetch自动携带认证
async function authFetch (url, options = {}) {
@@ -392,12 +399,13 @@
resultDiv.innerHTML = '<div class="loading">加载中</div>';
try {
const response = await authFetch(`${apiBase}/static/test.txt`);
// 静态资源不需要鉴权,直接请求
const response = await fetch(`${staticBase}/static/test.txt`);
if (response.ok) {
const text = await response.text();
resultDiv.innerHTML = `
<div style="background: var(--bg-item); border: 1px solid var(--border-color); border-radius: 8px; padding: 12px;">
<div style="font-size: 11px; color: var(--success-color); margin-bottom: 8px;">静态资源访问成功</div>
<div style="font-size: 11px; color: var(--success-color); margin-bottom: 8px;">文件系统静态资源访问成功</div>
<pre style="font-family: Monaco, Consolas, monospace; font-size: 12px; color: var(--text-primary); white-space: pre-wrap; margin: 0;">${text}</pre>
</div>
`;
@@ -408,6 +416,30 @@
resultDiv.innerHTML = `<div class="error">请求失败: ${error.message}</div>`;
}
}
// 测试内存资源
async function testMemoryResource () {
const resultDiv = document.getElementById('static-result');
resultDiv.innerHTML = '<div class="loading">加载中</div>';
try {
// 内存资源不需要鉴权,直接请求
const response = await fetch(`${memBase}/dynamic/info.json`);
if (response.ok) {
const json = await response.json();
resultDiv.innerHTML = `
<div style="background: var(--bg-item); border: 1px solid var(--border-color); border-radius: 8px; padding: 12px;">
<div style="font-size: 11px; color: var(--success-color); margin-bottom: 8px;">内存生成资源访问成功</div>
<pre style="font-family: Monaco, Consolas, monospace; font-size: 12px; color: var(--text-primary); white-space: pre-wrap; margin: 0;">${JSON.stringify(json, null, 2)}</pre>
</div>
`;
} else {
resultDiv.innerHTML = `<div class="error">请求失败: ${response.status} ${response.statusText}</div>`;
}
} catch (error) {
resultDiv.innerHTML = `<div class="error">请求失败: ${error.message}</div>`;
}
}
</script>
</body>

View File

@@ -20,6 +20,7 @@ export default defineConfig({
'@/napcat-schema': resolve(__dirname, './src'),
'@/napcat-core': resolve(__dirname, '../napcat-core'),
'@/napcat-webui-backend': resolve(__dirname, '../napcat-webui-backend'),
'@/napcat-image-size': resolve(__dirname, '../napcat-image-size'),
},
},
plugins: [

View File

@@ -1,6 +1,6 @@
{
"name": "napcat-types",
"version": "0.0.14",
"version": "0.0.15",
"private": false,
"type": "module",
"types": "./napcat-types/index.d.ts",

View File

@@ -27,6 +27,8 @@ import compression from 'compression';
import { napCatVersion } from 'napcat-common/src/version';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
import { NapCatOneBot11Adapter } from '@/napcat-onebot/index';
import { OB11PluginMangerAdapter } from '@/napcat-onebot/network/plugin-manger';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -294,6 +296,108 @@ export async function InitWebUi (logger: ILogWrapper, pathWrapper: NapCatPathWra
app.use('/webui', express.static(pathWrapper.staticPath, {
maxAge: '1d',
}));
// 插件内存静态资源路由(不需要鉴权)
// 路径格式: /plugin/:pluginId/mem/:urlPath/*
app.use('/plugin/:pluginId/mem', async (req, res) => {
const { pluginId } = req.params;
if (!pluginId) return res.status(400).json({ code: -1, message: 'Plugin ID is required' });
const ob11 = WebUiDataRuntime.getOneBotContext() as NapCatOneBot11Adapter | null;
if (!ob11) return res.status(503).json({ code: -1, message: 'OneBot context not available' });
const pluginManager = ob11.networkManager.findSomeAdapter('plugin_manager') as OB11PluginMangerAdapter | undefined;
if (!pluginManager) return res.status(503).json({ code: -1, message: 'Plugin manager not available' });
const routerRegistry = pluginManager.getPluginRouter(pluginId);
const memoryRoutes = routerRegistry?.getMemoryStaticRoutes() || [];
for (const { urlPath, files } of memoryRoutes) {
const prefix = urlPath.startsWith('/') ? urlPath : '/' + urlPath;
if (req.path.startsWith(prefix)) {
const filePath = '/' + (req.path.substring(prefix.length).replace(/^\//, '') || '');
const memFile = files.find(f => ('/' + f.path.replace(/^\//, '')) === filePath);
if (memFile) {
try {
const content = typeof memFile.content === 'function' ? await memFile.content() : memFile.content;
res.setHeader('Content-Type', memFile.contentType || 'application/octet-stream');
return res.send(content);
} catch (err) {
console.error(`[Plugin: ${pluginId}] Error serving memory file:`, err);
return res.status(500).json({ code: -1, message: 'Error serving memory file' });
}
}
}
}
return res.status(404).json({ code: -1, message: 'Memory file not found' });
});
// 插件页面路由(不需要鉴权)
// 路径格式: /plugin/:pluginId/page/:pagePath
app.get('/plugin/:pluginId/page/:pagePath', (req, res) => {
const { pluginId, pagePath } = req.params;
if (!pluginId) return res.status(400).json({ code: -1, message: 'Plugin ID is required' });
const ob11 = WebUiDataRuntime.getOneBotContext() as NapCatOneBot11Adapter | null;
if (!ob11) return res.status(503).json({ code: -1, message: 'OneBot context not available' });
const pluginManager = ob11.networkManager.findSomeAdapter('plugin_manager') as OB11PluginMangerAdapter | undefined;
if (!pluginManager) return res.status(503).json({ code: -1, message: 'Plugin manager not available' });
const routerRegistry = pluginManager.getPluginRouter(pluginId);
if (!routerRegistry || !routerRegistry.hasPages()) {
return res.status(404).json({ code: -1, message: `Plugin '${pluginId}' has no registered pages` });
}
const pages = routerRegistry.getPages();
const page = pages.find(p => p.path === '/' + pagePath || p.path === pagePath);
if (!page) {
return res.status(404).json({ code: -1, message: `Page '${pagePath}' not found in plugin '${pluginId}'` });
}
const pluginPath = routerRegistry.getPluginPath();
if (!pluginPath) {
return res.status(500).json({ code: -1, message: 'Plugin path not available' });
}
const htmlFilePath = join(pluginPath, page.htmlFile);
if (!existsSync(htmlFilePath)) {
return res.status(404).json({ code: -1, message: `HTML file not found: ${page.htmlFile}` });
}
return res.sendFile(htmlFilePath);
});
// 插件文件系统静态资源路由(不需要鉴权)
// 路径格式: /plugin/:pluginId/files/*
app.use('/plugin/:pluginId/files', (req, res, next) => {
const { pluginId } = req.params;
if (!pluginId) return res.status(400).json({ code: -1, message: 'Plugin ID is required' });
const ob11 = WebUiDataRuntime.getOneBotContext() as NapCatOneBot11Adapter | null;
if (!ob11) return res.status(503).json({ code: -1, message: 'OneBot context not available' });
const pluginManager = ob11.networkManager.findSomeAdapter('plugin_manager') as OB11PluginMangerAdapter | undefined;
if (!pluginManager) return res.status(503).json({ code: -1, message: 'Plugin manager not available' });
const routerRegistry = pluginManager.getPluginRouter(pluginId);
const staticRoutes = routerRegistry?.getStaticRoutes() || [];
for (const { urlPath, localPath } of staticRoutes) {
const prefix = urlPath.startsWith('/') ? urlPath : '/' + urlPath;
if (req.path.startsWith(prefix) || req.path === prefix.slice(0, -1)) {
const staticMiddleware = express.static(localPath, { maxAge: '1d' });
const originalUrl = req.url;
req.url = '/' + (req.path.substring(prefix.length).replace(/^\//, '') || '');
return staticMiddleware(req, res, (err) => {
req.url = originalUrl;
err ? next(err) : next();
});
}
}
res.status(404).json({ code: -1, message: 'Static resource not found' });
});
// 初始化WebSocket服务器
const sslCerts = await checkCertificates(logger);
const isHttps = !!sslCerts;

View File

@@ -254,11 +254,13 @@ export const InstallPluginFromStoreHandler: RequestHandler = async (req, res) =>
// 删除临时文件
fs.unlinkSync(tempZipPath);
// 如果 pluginManager 存在,立即注册插件
// 如果 pluginManager 存在,立即注册或重载插件
const pluginManager = getPluginManager();
if (pluginManager) {
// 检查是否已注册,避免重复注册
if (!pluginManager.getPluginInfo(id)) {
// 如果插件已存在,则重载以刷新版本信息;否则注册新插件
if (pluginManager.getPluginInfo(id)) {
await pluginManager.reloadPlugin(id);
} else {
await pluginManager.loadPluginById(id);
}
}
@@ -336,11 +338,14 @@ export const InstallPluginFromStoreSSEHandler: RequestHandler = async (req, res)
sendProgress('解压完成,正在清理...', 90);
fs.unlinkSync(tempZipPath);
// 如果 pluginManager 存在,立即注册插件
// 如果 pluginManager 存在,立即注册或重载插件
const pluginManager = getPluginManager();
if (pluginManager) {
// 检查是否已注册,避免重复注册
if (!pluginManager.getPluginInfo(id)) {
// 如果插件已存在,则重载以刷新版本信息;否则注册新插件
if (pluginManager.getPluginInfo(id)) {
sendProgress('正在刷新插件信息...', 95);
await pluginManager.reloadPlugin(id);
} else {
sendProgress('正在注册插件...', 95);
await pluginManager.loadPluginById(id);
}

View File

@@ -63,13 +63,12 @@ export default function ExtensionPage () {
}, [extensionPages]);
// 获取当前选中页面的 iframe URL
// 新路由格式不需要鉴权: /plugin/:pluginId/page/:pagePath
const currentPageUrl = useMemo(() => {
if (!selectedTab) return '';
const [pluginId, ...pathParts] = selectedTab.split(':');
const path = pathParts.join(':').replace(/^\//, '');
// 获取认证 token
const token = localStorage.getItem('token') || '';
return `/api/Plugin/page/${pluginId}/${path}?webui_token=${token}`;
return `/plugin/${pluginId}/page/${path}`;
}, [selectedTab]);
useEffect(() => {
@@ -86,6 +85,13 @@ export default function ExtensionPage () {
setIframeLoading(false);
};
// 在新窗口打开页面(新路由不需要鉴权)
const openInNewWindow = (pluginId: string, path: string) => {
const cleanPath = path.replace(/^\//, '');
const url = `/plugin/${pluginId}/page/${cleanPath}`;
window.open(url, '_blank');
};
return (
<>
<title> - NapCat WebUI</title>
@@ -125,7 +131,16 @@ export default function ExtensionPage () {
title={
<div className='flex items-center gap-2'>
{tab.icon && <span>{tab.icon}</span>}
<span>{tab.title}</span>
<span
className='cursor-pointer hover:underline'
title='点击在新窗口打开'
onClick={(e) => {
e.stopPropagation();
openInNewWindow(tab.pluginId, tab.path);
}}
>
{tab.title}
</span>
<span className='text-xs text-default-400'>({tab.pluginName})</span>
</div>
}

10
pnpm-lock.yaml generated
View File

@@ -232,8 +232,8 @@ importers:
packages/napcat-plugin-builtin:
dependencies:
napcat-types:
specifier: 0.0.14
version: 0.0.14
specifier: 0.0.15
version: 0.0.15
devDependencies:
'@types/node':
specifier: ^22.0.1
@@ -5448,8 +5448,8 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
napcat-types@0.0.14:
resolution: {integrity: sha512-q5ke+vzzXeZkYPsr9jmj94NxgH63/xv5yS/lPEU++A3x2mOM8SYJqdFEMbHG1QIFciyH1u3qnnNiJ0mBxOBFbA==}
napcat-types@0.0.15:
resolution: {integrity: sha512-uOkaQPO3SVgkO/Rt0cQ+02wCI9C9jzdYVViHByHrr9sA+2ZjT1HV5nVSgNNQXUaZ9q405LUu45xQ4lysNyLpBA==}
napcat.protobuf@1.1.4:
resolution: {integrity: sha512-z7XtLSBJ/PxmYb0VD/w+eYr/X3LyGz+SZ2QejFTOczwt6zWNxy2yV1mTMTvJoc3BWkI3ESVFRxkuT6+pj1tb1Q==}
@@ -12774,7 +12774,7 @@ snapshots:
nanoid@3.3.11: {}
napcat-types@0.0.14:
napcat-types@0.0.15:
dependencies:
'@sinclair/typebox': 0.34.41
'@types/node': 22.19.1