mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-13 00:10:27 +00:00
Add plugin WebUI extension page and API routing support
Introduces a plugin router registry for registering plugin-specific API routes, static resources, and extension pages. Updates the plugin manager and context to expose the router, and implements backend and frontend support for serving and displaying plugin extension pages in the WebUI. Also adds a demo extension page and static resource to the builtin plugin.
This commit is contained in:
@@ -62,7 +62,7 @@ export const GetPluginListHandler: RequestHandler = async (_req, res) => {
|
||||
const pluginManager = getPluginManager();
|
||||
if (!pluginManager) {
|
||||
// 返回成功但带特殊标记
|
||||
return sendSuccess(res, { plugins: [], pluginManagerNotFound: true });
|
||||
return sendSuccess(res, { plugins: [], pluginManagerNotFound: true, extensionPages: [] });
|
||||
}
|
||||
|
||||
const loadedPlugins = pluginManager.getAllPlugins();
|
||||
@@ -74,8 +74,19 @@ export const GetPluginListHandler: RequestHandler = async (_req, res) => {
|
||||
author: string;
|
||||
status: string;
|
||||
hasConfig: boolean;
|
||||
hasPages: boolean;
|
||||
}> = new Array();
|
||||
|
||||
// 收集所有插件的扩展页面
|
||||
const extensionPages: Array<{
|
||||
pluginId: string;
|
||||
pluginName: string;
|
||||
path: string;
|
||||
title: string;
|
||||
icon?: string;
|
||||
description?: string;
|
||||
}> = [];
|
||||
|
||||
// 1. 整理已加载的插件
|
||||
for (const p of loadedPlugins) {
|
||||
// 根据插件状态确定 status
|
||||
@@ -88,6 +99,10 @@ export const GetPluginListHandler: RequestHandler = async (_req, res) => {
|
||||
status = 'stopped'; // 启用但未加载(可能加载失败)
|
||||
}
|
||||
|
||||
// 检查插件是否有注册页面
|
||||
const pluginRouter = pluginManager.getPluginRouter(p.id);
|
||||
const hasPages = pluginRouter?.hasPages() ?? false;
|
||||
|
||||
AllPlugins.push({
|
||||
name: p.packageJson?.plugin || p.name || '', // 优先显示 package.json 的 plugin 字段
|
||||
id: p.id, // 包名,用于 API 操作
|
||||
@@ -95,12 +110,28 @@ export const GetPluginListHandler: RequestHandler = async (_req, res) => {
|
||||
description: p.packageJson?.description || '',
|
||||
author: p.packageJson?.author || '',
|
||||
status,
|
||||
hasConfig: !!(p.runtime.module?.plugin_config_schema || p.runtime.module?.plugin_config_ui)
|
||||
hasConfig: !!(p.runtime.module?.plugin_config_schema || p.runtime.module?.plugin_config_ui),
|
||||
hasPages
|
||||
});
|
||||
|
||||
// 收集插件的扩展页面
|
||||
if (hasPages && pluginRouter) {
|
||||
const pages = pluginRouter.getPages();
|
||||
for (const page of pages) {
|
||||
extensionPages.push({
|
||||
pluginId: p.id,
|
||||
pluginName: p.packageJson?.plugin || p.name || p.id,
|
||||
path: page.path,
|
||||
title: page.title,
|
||||
icon: page.icon,
|
||||
description: page.description
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return sendSuccess(res, { plugins: AllPlugins, pluginManagerNotFound: false });
|
||||
return sendSuccess(res, { plugins: AllPlugins, pluginManagerNotFound: false, extensionPages });
|
||||
};
|
||||
|
||||
export const SetPluginStatusHandler: RequestHandler = async (req, res) => {
|
||||
|
||||
@@ -16,10 +16,7 @@ export async function auth (req: Request, res: Response, next: NextFunction) {
|
||||
req.url === '/auth/passkey/verify-authentication') {
|
||||
return next();
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 判断是否有Authorization头
|
||||
let hash: string | undefined;
|
||||
if (req.headers?.authorization) {
|
||||
// 切割参数以获取token
|
||||
const authorization = req.headers.authorization.split(' ');
|
||||
@@ -28,8 +25,14 @@ export async function auth (req: Request, res: Response, next: NextFunction) {
|
||||
return sendError(res, 'Unauthorized');
|
||||
}
|
||||
// 获取token
|
||||
const hash = authorization[1];
|
||||
if (!hash) return sendError(res, 'Unauthorized');
|
||||
hash = authorization[1];
|
||||
} else if (req.query['webui_token'] && typeof req.query['webui_token'] === 'string') {
|
||||
// 支持通过query参数传递token
|
||||
hash = req.query['webui_token'];
|
||||
}
|
||||
// 判断是否有Authorization头
|
||||
if (hash) {
|
||||
//if (!hash) return sendError(res, 'Unauthorized');
|
||||
// 解析token
|
||||
let Credential: WebUiCredentialJson;
|
||||
try {
|
||||
|
||||
@@ -20,6 +20,9 @@ import {
|
||||
InstallPluginFromStoreHandler,
|
||||
InstallPluginFromStoreSSEHandler
|
||||
} from '@/napcat-webui-backend/src/api/PluginStore';
|
||||
import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data';
|
||||
import { NapCatOneBot11Adapter } from '@/napcat-onebot/index';
|
||||
import { OB11PluginMangerAdapter } from '@/napcat-onebot/network/plugin-manger';
|
||||
|
||||
// 配置 multer 用于文件上传
|
||||
const uploadDir = path.join(os.tmpdir(), 'napcat-plugin-uploads');
|
||||
@@ -72,4 +75,92 @@ router.get('/Store/Detail/:id', GetPluginStoreDetailHandler);
|
||||
router.post('/Store/Install', InstallPluginFromStoreHandler);
|
||||
router.get('/Store/Install/SSE', InstallPluginFromStoreSSEHandler);
|
||||
|
||||
// 插件扩展路由 - 动态挂载插件注册的 API 路由
|
||||
router.use('/ext/:pluginId', (req, res, next): void => {
|
||||
const { pluginId } = req.params;
|
||||
|
||||
if (!pluginId) {
|
||||
res.status(400).json({ code: -1, message: 'Plugin ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取插件管理器
|
||||
const ob11 = WebUiDataRuntime.getOneBotContext() as NapCatOneBot11Adapter;
|
||||
if (!ob11) {
|
||||
res.status(503).json({ code: -1, message: 'OneBot context not available' });
|
||||
return;
|
||||
}
|
||||
|
||||
const pluginManager = ob11.networkManager.findSomeAdapter('plugin_manager') as OB11PluginMangerAdapter;
|
||||
if (!pluginManager) {
|
||||
res.status(503).json({ code: -1, message: 'Plugin manager not available' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取插件路由
|
||||
const routerRegistry = pluginManager.getPluginRouter(pluginId);
|
||||
if (!routerRegistry || !routerRegistry.hasApiRoutes()) {
|
||||
res.status(404).json({ code: -1, message: `Plugin '${pluginId}' has no registered API routes` });
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建并执行插件路由
|
||||
const pluginRouter = routerRegistry.buildApiRouter();
|
||||
pluginRouter(req, res, next);
|
||||
});
|
||||
|
||||
// 插件页面路由 - 服务插件注册的 HTML 页面
|
||||
router.get('/page/:pluginId/:pagePath', (req, res): void => {
|
||||
const { pluginId, pagePath } = req.params;
|
||||
|
||||
if (!pluginId) {
|
||||
res.status(400).json({ code: -1, message: 'Plugin ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取插件管理器
|
||||
const ob11 = WebUiDataRuntime.getOneBotContext() as NapCatOneBot11Adapter;
|
||||
if (!ob11) {
|
||||
res.status(503).json({ code: -1, message: 'OneBot context not available' });
|
||||
return;
|
||||
}
|
||||
|
||||
const pluginManager = ob11.networkManager.findSomeAdapter('plugin_manager') as OB11PluginMangerAdapter;
|
||||
if (!pluginManager) {
|
||||
res.status(503).json({ code: -1, message: 'Plugin manager not available' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取插件路由
|
||||
const routerRegistry = pluginManager.getPluginRouter(pluginId);
|
||||
if (!routerRegistry || !routerRegistry.hasPages()) {
|
||||
res.status(404).json({ code: -1, message: `Plugin '${pluginId}' has no registered pages` });
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找匹配的页面
|
||||
const pages = routerRegistry.getPages();
|
||||
const page = pages.find(p => p.path === '/' + pagePath || p.path === pagePath);
|
||||
if (!page) {
|
||||
res.status(404).json({ code: -1, message: `Page '${pagePath}' not found in plugin '${pluginId}'` });
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取插件路径
|
||||
const pluginPath = routerRegistry.getPluginPath();
|
||||
if (!pluginPath) {
|
||||
res.status(500).json({ code: -1, message: 'Plugin path not available' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建 HTML 文件路径并发送
|
||||
const htmlFilePath = path.join(pluginPath, page.htmlFile);
|
||||
if (!fs.existsSync(htmlFilePath)) {
|
||||
res.status(404).json({ code: -1, message: `HTML file not found: ${page.htmlFile}` });
|
||||
return;
|
||||
}
|
||||
|
||||
res.sendFile(htmlFilePath);
|
||||
});
|
||||
|
||||
export { router as PluginRouter };
|
||||
|
||||
Reference in New Issue
Block a user