From a5769b6a62cc4e30cc40d80335debe35f0143cc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Mon, 2 Feb 2026 15:40:18 +0800 Subject: [PATCH] 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. --- .../napcat-onebot/network/plugin-manger.ts | 2 +- .../napcat-onebot/network/plugin/manager.ts | 2 +- packages/napcat-plugin-builtin/index.ts | 3 +- packages/napcat-webui-backend/index.ts | 36 +++++++++++++++++++ .../src/pages/dashboard/extension.tsx | 10 +++--- 5 files changed, 44 insertions(+), 9 deletions(-) diff --git a/packages/napcat-onebot/network/plugin-manger.ts b/packages/napcat-onebot/network/plugin-manger.ts index 69f5f198..467d0ec0 100644 --- a/packages/napcat-onebot/network/plugin-manger.ts +++ b/packages/napcat-onebot/network/plugin-manger.ts @@ -216,7 +216,7 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter i this.pluginRouters.set(entry.id, routerRegistry); // 创建获取其他插件导出的方法 - const getPluginExports = (pluginId: string): T | undefined => { + const getPluginExports = (pluginId: string): T | undefined => { const targetEntry = this.plugins.get(pluginId); if (!targetEntry || !targetEntry.loaded || targetEntry.runtime.status !== 'loaded') { return undefined; diff --git a/packages/napcat-onebot/network/plugin/manager.ts b/packages/napcat-onebot/network/plugin/manager.ts index 03fadbe3..57918e3d 100644 --- a/packages/napcat-onebot/network/plugin/manager.ts +++ b/packages/napcat-onebot/network/plugin/manager.ts @@ -195,7 +195,7 @@ export class OB11PluginManager extends IOB11NetworkAdapter impleme } // 创建获取其他插件导出的方法 - const getPluginExports = (pluginId: string): T | undefined => { + const getPluginExports = (pluginId: string): T | undefined => { const targetEntry = this.plugins.get(pluginId); if (!targetEntry || !targetEntry.loaded || targetEntry.runtime.status !== 'loaded') { return undefined; diff --git a/packages/napcat-plugin-builtin/index.ts b/packages/napcat-plugin-builtin/index.ts index b438245f..9c0c9b7c 100644 --- a/packages/napcat-plugin-builtin/index.ts +++ b/packages/napcat-plugin-builtin/index.ts @@ -160,7 +160,7 @@ const plugin_init: PluginModule['plugin_init'] = async (ctx) => { }); }); - // 注册扩展页面 + // 注册扩展页面(无需鉴权,可通过 /plugin/{pluginId}/page/dashboard 访问) ctx.router.page({ path: 'dashboard', title: '插件仪表盘', @@ -171,6 +171,7 @@ const plugin_init: PluginModule['plugin_init'] = async (ctx) => { 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/'); }; diff --git a/packages/napcat-webui-backend/index.ts b/packages/napcat-webui-backend/index.ts index e45f0dec..b714366d 100644 --- a/packages/napcat-webui-backend/index.ts +++ b/packages/napcat-webui-backend/index.ts @@ -332,6 +332,42 @@ export async function InitWebUi (logger: ILogWrapper, pathWrapper: NapCatPathWra 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) => { diff --git a/packages/napcat-webui-frontend/src/pages/dashboard/extension.tsx b/packages/napcat-webui-frontend/src/pages/dashboard/extension.tsx index 9cbdf414..e61f1de1 100644 --- a/packages/napcat-webui-frontend/src/pages/dashboard/extension.tsx +++ b/packages/napcat-webui-frontend/src/pages/dashboard/extension.tsx @@ -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,11 +85,10 @@ export default function ExtensionPage () { setIframeLoading(false); }; - // 在新窗口打开页面 + // 在新窗口打开页面(新路由不需要鉴权) const openInNewWindow = (pluginId: string, path: string) => { const cleanPath = path.replace(/^\//, ''); - const token = localStorage.getItem('token') || ''; - const url = `/api/Plugin/page/${pluginId}/${cleanPath}?webui_token=${token}`; + const url = `/plugin/${pluginId}/page/${cleanPath}`; window.open(url, '_blank'); };