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}/.
This commit is contained in:
手瓜一十雪 2026-02-02 19:13:01 +08:00
parent 74781fda0a
commit 78ac36f670
7 changed files with 148 additions and 15 deletions

View File

@ -68,6 +68,7 @@ interface MemoryStaticRoute {
export class PluginRouterRegistryImpl implements PluginRouterRegistry { export class PluginRouterRegistryImpl implements PluginRouterRegistry {
private apiRoutes: PluginApiRouteDefinition[] = []; private apiRoutes: PluginApiRouteDefinition[] = [];
private apiNoAuthRoutes: PluginApiRouteDefinition[] = [];
private pageDefinitions: PluginPageDefinition[] = []; private pageDefinitions: PluginPageDefinition[] = [];
private staticRoutes: Array<{ urlPath: string; localPath: string; }> = []; private staticRoutes: Array<{ urlPath: string; localPath: string; }> = [];
private memoryStaticRoutes: MemoryStaticRoute[] = []; private memoryStaticRoutes: MemoryStaticRoute[] = [];
@ -99,6 +100,28 @@ export class PluginRouterRegistryImpl implements PluginRouterRegistry {
this.api('delete', routePath, handler); 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 { page (pageDef: PluginPageDefinition): void {
@ -184,12 +207,52 @@ export class PluginRouterRegistryImpl implements PluginRouterRegistry {
// ==================== 查询方法 ==================== // ==================== 查询方法 ====================
/** /**
* API * API
*/ */
hasApiRoutes (): boolean { hasApiRoutes (): boolean {
return this.apiRoutes.length > 0; 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 { clear (): void {
this.apiRoutes = []; this.apiRoutes = [];
this.apiNoAuthRoutes = [];
this.pageDefinitions = []; this.pageDefinitions = [];
this.staticRoutes = []; this.staticRoutes = [];
this.memoryStaticRoutes = []; this.memoryStaticRoutes = [];

View File

@ -140,24 +140,42 @@ export interface MemoryStaticFile {
/** 插件路由注册器 */ /** 插件路由注册器 */
export interface PluginRouterRegistry { export interface PluginRouterRegistry {
// ==================== API 路由注册 ==================== // ==================== API 路由注册(需要认证) ====================
/** /**
* API * API /api/Plugin/ext/{pluginId}/
* @param method HTTP * @param method HTTP
* @param path * @param path
* @param handler * @param handler
*/ */
api (method: HttpMethod, path: string, handler: PluginRequestHandler): void; api (method: HttpMethod, path: string, handler: PluginRequestHandler): void;
/** 注册 GET API */ /** 注册 GET API(需要认证) */
get (path: string, handler: PluginRequestHandler): void; get (path: string, handler: PluginRequestHandler): void;
/** 注册 POST API */ /** 注册 POST API(需要认证) */
post (path: string, handler: PluginRequestHandler): void; post (path: string, handler: PluginRequestHandler): void;
/** 注册 PUT API */ /** 注册 PUT API(需要认证) */
put (path: string, handler: PluginRequestHandler): void; put (path: string, handler: PluginRequestHandler): void;
/** 注册 DELETE API */ /** 注册 DELETE API(需要认证) */
delete (path: string, handler: PluginRequestHandler): void; 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;
// ==================== 页面注册 ==================== // ==================== 页面注册 ====================
/** /**

View File

@ -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) => { 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('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 + '/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

@ -7,7 +7,7 @@
"description": "NapCat 内置插件", "description": "NapCat 内置插件",
"author": "NapNeko", "author": "NapNeko",
"dependencies": { "dependencies": {
"napcat-types": "0.0.15" "napcat-types": "0.0.16"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.0.1" "@types/node": "^22.0.1"

View File

@ -1,6 +1,6 @@
{ {
"name": "napcat-types", "name": "napcat-types",
"version": "0.0.15", "version": "0.0.16",
"private": false, "private": false,
"type": "module", "type": "module",
"types": "./napcat-types/index.d.ts", "types": "./napcat-types/index.d.ts",

View File

@ -332,6 +332,28 @@ 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' });
}); });
// 插件无认证 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 // 路径格式: /plugin/:pluginId/page/:pagePath
app.get('/plugin/:pluginId/page/:pagePath', (req, res) => { app.get('/plugin/:pluginId/page/:pagePath', (req, res) => {

View File

@ -232,8 +232,8 @@ importers:
packages/napcat-plugin-builtin: packages/napcat-plugin-builtin:
dependencies: dependencies:
napcat-types: napcat-types:
specifier: 0.0.15 specifier: 0.0.16
version: 0.0.15 version: 0.0.16
devDependencies: devDependencies:
'@types/node': '@types/node':
specifier: ^22.0.1 specifier: ^22.0.1
@ -5457,8 +5457,8 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true hasBin: true
napcat-types@0.0.15: napcat-types@0.0.16:
resolution: {integrity: sha512-uOkaQPO3SVgkO/Rt0cQ+02wCI9C9jzdYVViHByHrr9sA+2ZjT1HV5nVSgNNQXUaZ9q405LUu45xQ4lysNyLpBA==} resolution: {integrity: sha512-y3qhpdd16ATsMp4Jf88XwisFBVKqY+XSfvGX1YqMEasVFTNXeKr1MZrIzhHMkllW1QJZXAI8iNGVJO1gkHEtLQ==}
napcat.protobuf@1.1.4: napcat.protobuf@1.1.4:
resolution: {integrity: sha512-z7XtLSBJ/PxmYb0VD/w+eYr/X3LyGz+SZ2QejFTOczwt6zWNxy2yV1mTMTvJoc3BWkI3ESVFRxkuT6+pj1tb1Q==} resolution: {integrity: sha512-z7XtLSBJ/PxmYb0VD/w+eYr/X3LyGz+SZ2QejFTOczwt6zWNxy2yV1mTMTvJoc3BWkI3ESVFRxkuT6+pj1tb1Q==}
@ -12783,7 +12783,7 @@ snapshots:
nanoid@3.3.11: {} nanoid@3.3.11: {}
napcat-types@0.0.15: napcat-types@0.0.16:
dependencies: dependencies:
'@sinclair/typebox': 0.34.41 '@sinclair/typebox': 0.34.41
'@types/node': 22.19.1 '@types/node': 22.19.1