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.
This commit is contained in:
手瓜一十雪 2026-02-02 15:40:18 +08:00
parent d9297c1e10
commit a5769b6a62
5 changed files with 44 additions and 9 deletions

View File

@ -160,7 +160,7 @@ const plugin_init: PluginModule['plugin_init'] = async (ctx) => {
}); });
}); });
// 注册扩展页面 // 注册扩展页面(无需鉴权,可通过 /plugin/{pluginId}/page/dashboard 访问)
ctx.router.page({ ctx.router.page({
path: 'dashboard', path: 'dashboard',
title: '插件仪表盘', title: '插件仪表盘',
@ -171,6 +171,7 @@ const plugin_init: PluginModule['plugin_init'] = async (ctx) => {
logger.info('WebUI 路由已注册:'); logger.info('WebUI 路由已注册:');
logger.info(' - API 路由: /api/Plugin/ext/' + ctx.pluginName + '/'); 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 + '/files/static/');
logger.info(' - 内存资源: /plugin/' + ctx.pluginName + '/mem/dynamic/'); logger.info(' - 内存资源: /plugin/' + ctx.pluginName + '/mem/dynamic/');
}; };

View File

@ -332,6 +332,42 @@ export async function InitWebUi (logger: ILogWrapper, pathWrapper: NapCatPathWra
return res.status(404).json({ code: -1, message: 'Memory file not found' }); 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/* // 路径格式: /plugin/:pluginId/files/*
app.use('/plugin/:pluginId/files', (req, res, next) => { app.use('/plugin/:pluginId/files', (req, res, next) => {

View File

@ -63,13 +63,12 @@ export default function ExtensionPage () {
}, [extensionPages]); }, [extensionPages]);
// 获取当前选中页面的 iframe URL // 获取当前选中页面的 iframe URL
// 新路由格式不需要鉴权: /plugin/:pluginId/page/:pagePath
const currentPageUrl = useMemo(() => { const currentPageUrl = useMemo(() => {
if (!selectedTab) return ''; if (!selectedTab) return '';
const [pluginId, ...pathParts] = selectedTab.split(':'); const [pluginId, ...pathParts] = selectedTab.split(':');
const path = pathParts.join(':').replace(/^\//, ''); const path = pathParts.join(':').replace(/^\//, '');
// 获取认证 token return `/plugin/${pluginId}/page/${path}`;
const token = localStorage.getItem('token') || '';
return `/api/Plugin/page/${pluginId}/${path}?webui_token=${token}`;
}, [selectedTab]); }, [selectedTab]);
useEffect(() => { useEffect(() => {
@ -86,11 +85,10 @@ export default function ExtensionPage () {
setIframeLoading(false); setIframeLoading(false);
}; };
// 在新窗口打开页面 // 在新窗口打开页面(新路由不需要鉴权)
const openInNewWindow = (pluginId: string, path: string) => { const openInNewWindow = (pluginId: string, path: string) => {
const cleanPath = path.replace(/^\//, ''); const cleanPath = path.replace(/^\//, '');
const token = localStorage.getItem('token') || ''; const url = `/plugin/${pluginId}/page/${cleanPath}`;
const url = `/api/Plugin/page/${pluginId}/${cleanPath}?webui_token=${token}`;
window.open(url, '_blank'); window.open(url, '_blank');
}; };