From 78ac36f6701031baaad4db59ff9aa859021ab17e 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 19:13:01 +0800 Subject: [PATCH] Add plugin no-auth API routes and WebUI handling Introduce support for plugin API routes that do not require WebUI authentication. Updates include: - napcat-onebot: Add apiNoAuth route storage and helpers (apiNoAuth, getNoAuth, postNoAuth, putNoAuth, deleteNoAuth), hasApiNoAuthRoutes, buildApiNoAuthRouter, and clear handling in PluginRouterRegistryImpl. - napcat-onebot types: Extend PluginRouterRegistry interface with no-auth API methods and document that authenticated APIs remain separate. - napcat-webui-backend: Mount a new unauthenticated plugin route handler at /plugin/:pluginId/api that looks up the plugin router and dispatches requests to the plugin's no-auth router, returning appropriate errors when context or routes are missing. - napcat-plugin-builtin: Add example no-auth endpoints (public/info and health) and update logger messages to reflect both auth and no-auth API paths. - Bump napcat-types version to 0.0.16 and update napcat-plugin-builtin dependency accordingly. These changes enable plugins to expose public endpoints (e.g. health checks or public metadata) under /plugin/{pluginId}/api/ while keeping existing authenticated APIs under /api/Plugin/ext/{pluginId}/. --- .../network/plugin/router-registry.ts | 66 ++++++++++++++++++- .../napcat-onebot/network/plugin/types.ts | 30 +++++++-- packages/napcat-plugin-builtin/index.ts | 31 ++++++++- packages/napcat-plugin-builtin/package.json | 2 +- packages/napcat-types/package.public.json | 2 +- packages/napcat-webui-backend/index.ts | 22 +++++++ pnpm-lock.yaml | 10 +-- 7 files changed, 148 insertions(+), 15 deletions(-) diff --git a/packages/napcat-onebot/network/plugin/router-registry.ts b/packages/napcat-onebot/network/plugin/router-registry.ts index 2bf4b1af..69395c39 100644 --- a/packages/napcat-onebot/network/plugin/router-registry.ts +++ b/packages/napcat-onebot/network/plugin/router-registry.ts @@ -68,6 +68,7 @@ interface MemoryStaticRoute { export class PluginRouterRegistryImpl implements PluginRouterRegistry { private apiRoutes: PluginApiRouteDefinition[] = []; + private apiNoAuthRoutes: PluginApiRouteDefinition[] = []; private pageDefinitions: PluginPageDefinition[] = []; private staticRoutes: Array<{ urlPath: string; localPath: string; }> = []; private memoryStaticRoutes: MemoryStaticRoute[] = []; @@ -99,6 +100,28 @@ export class PluginRouterRegistryImpl implements PluginRouterRegistry { this.api('delete', routePath, handler); } + // ==================== 无认证 API 路由注册 ==================== + + apiNoAuth (method: HttpMethod, routePath: string, handler: PluginRequestHandler): void { + this.apiNoAuthRoutes.push({ method, path: routePath, handler }); + } + + getNoAuth (routePath: string, handler: PluginRequestHandler): void { + this.apiNoAuth('get', routePath, handler); + } + + postNoAuth (routePath: string, handler: PluginRequestHandler): void { + this.apiNoAuth('post', routePath, handler); + } + + putNoAuth (routePath: string, handler: PluginRequestHandler): void { + this.apiNoAuth('put', routePath, handler); + } + + deleteNoAuth (routePath: string, handler: PluginRequestHandler): void { + this.apiNoAuth('delete', routePath, handler); + } + // ==================== 页面注册 ==================== page (pageDef: PluginPageDefinition): void { @@ -184,12 +207,52 @@ export class PluginRouterRegistryImpl implements PluginRouterRegistry { // ==================== 查询方法 ==================== /** - * 检查是否有注册的 API 路由 + * 检查是否有注册的 API 路由(需要认证) */ hasApiRoutes (): boolean { return this.apiRoutes.length > 0; } + /** + * 检查是否有注册的无认证 API 路由 + */ + hasApiNoAuthRoutes (): boolean { + return this.apiNoAuthRoutes.length > 0; + } + + /** + * 构建无认证 Express Router(用于 /plugin/{pluginId}/api/ 路径) + */ + buildApiNoAuthRouter (): Router { + const router = Router(); + + for (const route of this.apiNoAuthRoutes) { + const handler = this.wrapHandler(route.handler); + switch (route.method) { + case 'get': + router.get(route.path, handler); + break; + case 'post': + router.post(route.path, handler); + break; + case 'put': + router.put(route.path, handler); + break; + case 'delete': + router.delete(route.path, handler); + break; + case 'patch': + router.patch(route.path, handler); + break; + case 'all': + router.all(route.path, handler); + break; + } + } + + return router; + } + /** * 检查是否有注册的静态资源路由 */ @@ -244,6 +307,7 @@ export class PluginRouterRegistryImpl implements PluginRouterRegistry { */ clear (): void { this.apiRoutes = []; + this.apiNoAuthRoutes = []; this.pageDefinitions = []; this.staticRoutes = []; this.memoryStaticRoutes = []; diff --git a/packages/napcat-onebot/network/plugin/types.ts b/packages/napcat-onebot/network/plugin/types.ts index c0290ff0..1c0718ae 100644 --- a/packages/napcat-onebot/network/plugin/types.ts +++ b/packages/napcat-onebot/network/plugin/types.ts @@ -140,24 +140,42 @@ export interface MemoryStaticFile { /** 插件路由注册器 */ export interface PluginRouterRegistry { - // ==================== API 路由注册 ==================== + // ==================== API 路由注册(需要认证) ==================== /** - * 注册单个 API 路由 + * 注册单个 API 路由(需要认证,挂载到 /api/Plugin/ext/{pluginId}/) * @param method HTTP 方法 * @param path 路由路径 * @param handler 请求处理器 */ api (method: HttpMethod, path: string, handler: PluginRequestHandler): void; - /** 注册 GET API */ + /** 注册 GET API(需要认证) */ get (path: string, handler: PluginRequestHandler): void; - /** 注册 POST API */ + /** 注册 POST API(需要认证) */ post (path: string, handler: PluginRequestHandler): void; - /** 注册 PUT API */ + /** 注册 PUT API(需要认证) */ put (path: string, handler: PluginRequestHandler): void; - /** 注册 DELETE API */ + /** 注册 DELETE API(需要认证) */ delete (path: string, handler: PluginRequestHandler): void; + // ==================== 无认证 API 路由注册 ==================== + + /** + * 注册单个无认证 API 路由(挂载到 /plugin/{pluginId}/api/) + * @param method HTTP 方法 + * @param path 路由路径 + * @param handler 请求处理器 + */ + apiNoAuth (method: HttpMethod, path: string, handler: PluginRequestHandler): void; + /** 注册 GET API(无认证) */ + getNoAuth (path: string, handler: PluginRequestHandler): void; + /** 注册 POST API(无认证) */ + postNoAuth (path: string, handler: PluginRequestHandler): void; + /** 注册 PUT API(无认证) */ + putNoAuth (path: string, handler: PluginRequestHandler): void; + /** 注册 DELETE API(无认证) */ + deleteNoAuth (path: string, handler: PluginRequestHandler): void; + // ==================== 页面注册 ==================== /** diff --git a/packages/napcat-plugin-builtin/index.ts b/packages/napcat-plugin-builtin/index.ts index 701fd1c9..b94315a5 100644 --- a/packages/napcat-plugin-builtin/index.ts +++ b/packages/napcat-plugin-builtin/index.ts @@ -129,6 +129,34 @@ const plugin_init: PluginModule['plugin_init'] = async (ctx) => { } }); + // ==================== 无认证 API 路由示例 ==================== + // 路由挂载到 /plugin/{pluginId}/api/,无需 WebUI 登录即可访问 + + // 获取插件公开信息(无需鉴权) + ctx.router.getNoAuth('/public/info', (_req, res) => { + const uptime = Date.now() - startTime; + res.json({ + code: 0, + data: { + pluginName: ctx.pluginName, + uptime, + uptimeFormatted: formatUptime(uptime), + platform: process.platform + } + }); + }); + + // 健康检查接口(无需鉴权) + ctx.router.getNoAuth('/health', (_req, res) => { + res.json({ + code: 0, + data: { + status: 'ok', + timestamp: new Date().toISOString() + } + }); + }); + // ==================== 插件互调用示例 ==================== // 演示如何调用其他插件的导出方法 ctx.router.get('/call-plugin/:pluginId', (req, res) => { @@ -178,7 +206,8 @@ const plugin_init: PluginModule['plugin_init'] = async (ctx) => { }); logger.info('WebUI 路由已注册:'); - logger.info(' - API 路由: /api/Plugin/ext/' + ctx.pluginName + '/'); + logger.info(' - API 路由(需认证): /api/Plugin/ext/' + ctx.pluginName + '/'); + logger.info(' - API 路由(无认证): /plugin/' + ctx.pluginName + '/api/'); 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-plugin-builtin/package.json b/packages/napcat-plugin-builtin/package.json index 6dbb2d8b..68b2d850 100644 --- a/packages/napcat-plugin-builtin/package.json +++ b/packages/napcat-plugin-builtin/package.json @@ -7,7 +7,7 @@ "description": "NapCat 内置插件", "author": "NapNeko", "dependencies": { - "napcat-types": "0.0.15" + "napcat-types": "0.0.16" }, "devDependencies": { "@types/node": "^22.0.1" diff --git a/packages/napcat-types/package.public.json b/packages/napcat-types/package.public.json index 5201bb6f..ed7ed95f 100644 --- a/packages/napcat-types/package.public.json +++ b/packages/napcat-types/package.public.json @@ -1,6 +1,6 @@ { "name": "napcat-types", - "version": "0.0.15", + "version": "0.0.16", "private": false, "type": "module", "types": "./napcat-types/index.d.ts", diff --git a/packages/napcat-webui-backend/index.ts b/packages/napcat-webui-backend/index.ts index b714366d..87481d6b 100644 --- a/packages/napcat-webui-backend/index.ts +++ b/packages/napcat-webui-backend/index.ts @@ -332,6 +332,28 @@ export async function InitWebUi (logger: ILogWrapper, pathWrapper: NapCatPathWra return res.status(404).json({ code: -1, message: 'Memory file not found' }); }); + // 插件无认证 API 路由(不需要鉴权) + // 路径格式: /plugin/:pluginId/api/* + app.use('/plugin/:pluginId/api', (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); + if (!routerRegistry || !routerRegistry.hasApiNoAuthRoutes()) { + return res.status(404).json({ code: -1, message: `Plugin '${pluginId}' has no registered no-auth API routes` }); + } + + // 构建并执行插件无认证 API 路由 + const pluginRouter = routerRegistry.buildApiNoAuthRouter(); + return pluginRouter(req, res, next); + }); + // 插件页面路由(不需要鉴权) // 路径格式: /plugin/:pluginId/page/:pagePath app.get('/plugin/:pluginId/page/:pagePath', (req, res) => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1bcf636f..bfcbd7eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -232,8 +232,8 @@ importers: packages/napcat-plugin-builtin: dependencies: napcat-types: - specifier: 0.0.15 - version: 0.0.15 + specifier: 0.0.16 + version: 0.0.16 devDependencies: '@types/node': specifier: ^22.0.1 @@ -5457,8 +5457,8 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - napcat-types@0.0.15: - resolution: {integrity: sha512-uOkaQPO3SVgkO/Rt0cQ+02wCI9C9jzdYVViHByHrr9sA+2ZjT1HV5nVSgNNQXUaZ9q405LUu45xQ4lysNyLpBA==} + napcat-types@0.0.16: + resolution: {integrity: sha512-y3qhpdd16ATsMp4Jf88XwisFBVKqY+XSfvGX1YqMEasVFTNXeKr1MZrIzhHMkllW1QJZXAI8iNGVJO1gkHEtLQ==} napcat.protobuf@1.1.4: resolution: {integrity: sha512-z7XtLSBJ/PxmYb0VD/w+eYr/X3LyGz+SZ2QejFTOczwt6zWNxy2yV1mTMTvJoc3BWkI3ESVFRxkuT6+pj1tb1Q==} @@ -12783,7 +12783,7 @@ snapshots: nanoid@3.3.11: {} - napcat-types@0.0.15: + napcat-types@0.0.16: dependencies: '@sinclair/typebox': 0.34.41 '@types/node': 22.19.1