mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-04 06:31:13 +00:00
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:
parent
74781fda0a
commit
78ac36f670
@ -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 = [];
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
// ==================== 页面注册 ====================
|
// ==================== 页面注册 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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/');
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user