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:
手瓜一十雪
2026-01-30 12:48:24 +08:00
parent bb69ae6c62
commit 012c283dee
18 changed files with 1245 additions and 23 deletions

View File

@@ -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) => {

View File

@@ -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 {

View File

@@ -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 };