mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-06 13:05:09 +00:00
Add plugin WebUI extension page and API routing support
Introduces a plugin router registry for registering plugin-specific API routes, static resources, and extension pages. Updates the plugin manager and context to expose the router, and implements backend and frontend support for serving and displaying plugin extension pages in the WebUI. Also adds a demo extension page and static resource to the builtin plugin.
This commit is contained in:
@@ -8,6 +8,7 @@ import { IOB11NetworkAdapter } from '@/napcat-onebot/network/adapter';
|
||||
import { PluginConfig } from '@/napcat-onebot/config/config';
|
||||
import { NapCatConfig } from './config';
|
||||
import { PluginLoader } from './loader';
|
||||
import { PluginRouterRegistryImpl } from './router-registry';
|
||||
import {
|
||||
PluginEntry,
|
||||
PluginLogger,
|
||||
@@ -24,6 +25,9 @@ export class OB11PluginManager extends IOB11NetworkAdapter<PluginConfig> impleme
|
||||
/** 插件注册表: ID -> 插件条目 */
|
||||
private plugins: Map<string, PluginEntry> = new Map();
|
||||
|
||||
/** 插件路由注册表: pluginId -> PluginRouterRegistry */
|
||||
private pluginRouters: Map<string, PluginRouterRegistryImpl> = new Map();
|
||||
|
||||
declare config: PluginConfig;
|
||||
public NapCatConfig = NapCatConfig;
|
||||
|
||||
@@ -183,6 +187,13 @@ export class OB11PluginManager extends IOB11NetworkAdapter<PluginConfig> impleme
|
||||
error: (...args: any[]) => coreLogger.logError(pluginPrefix, ...args),
|
||||
};
|
||||
|
||||
// 创建或获取插件路由注册器
|
||||
let router = this.pluginRouters.get(entry.id);
|
||||
if (!router) {
|
||||
router = new PluginRouterRegistryImpl(entry.id, entry.pluginPath);
|
||||
this.pluginRouters.set(entry.id, router);
|
||||
}
|
||||
|
||||
return {
|
||||
core: this.core,
|
||||
oneBot: this.obContext,
|
||||
@@ -195,6 +206,7 @@ export class OB11PluginManager extends IOB11NetworkAdapter<PluginConfig> impleme
|
||||
adapterName: this.name,
|
||||
pluginManager: this,
|
||||
logger: pluginLogger,
|
||||
router,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -391,6 +403,20 @@ export class OB11PluginManager extends IOB11NetworkAdapter<PluginConfig> impleme
|
||||
return path.join(this.getPluginDataPath(pluginId), 'config.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件路由注册器
|
||||
*/
|
||||
public getPluginRouter (pluginId: string): PluginRouterRegistryImpl | undefined {
|
||||
return this.pluginRouters.get(pluginId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有插件路由注册器
|
||||
*/
|
||||
public getAllPluginRouters (): Map<string, PluginRouterRegistryImpl> {
|
||||
return this.pluginRouters;
|
||||
}
|
||||
|
||||
// ==================== 事件处理 ====================
|
||||
|
||||
async onEvent<T extends OB11EmitEventContent> (event: T): Promise<void> {
|
||||
|
||||
221
packages/napcat-onebot/network/plugin/router-registry.ts
Normal file
221
packages/napcat-onebot/network/plugin/router-registry.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import { Router, static as expressStatic, Request, Response, NextFunction } from 'express';
|
||||
import path from 'path';
|
||||
import {
|
||||
PluginRouterRegistry,
|
||||
PluginRequestHandler,
|
||||
PluginApiRouteDefinition,
|
||||
PluginPageDefinition,
|
||||
PluginHttpRequest,
|
||||
PluginHttpResponse,
|
||||
HttpMethod,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* 包装 Express Request 为 PluginHttpRequest
|
||||
*/
|
||||
function wrapRequest (req: Request): PluginHttpRequest {
|
||||
return {
|
||||
path: req.path,
|
||||
method: req.method,
|
||||
query: req.query as Record<string, string | string[] | undefined>,
|
||||
body: req.body,
|
||||
headers: req.headers as Record<string, string | string[] | undefined>,
|
||||
params: req.params,
|
||||
raw: req,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 包装 Express Response 为 PluginHttpResponse
|
||||
*/
|
||||
function wrapResponse (res: Response): PluginHttpResponse {
|
||||
const wrapped: PluginHttpResponse = {
|
||||
status (code: number) {
|
||||
res.status(code);
|
||||
return wrapped;
|
||||
},
|
||||
json (data: unknown) {
|
||||
res.json(data);
|
||||
},
|
||||
send (data: string | Buffer) {
|
||||
res.send(data);
|
||||
},
|
||||
setHeader (name: string, value: string) {
|
||||
res.setHeader(name, value);
|
||||
return wrapped;
|
||||
},
|
||||
sendFile (filePath: string) {
|
||||
res.sendFile(filePath);
|
||||
},
|
||||
redirect (url: string) {
|
||||
res.redirect(url);
|
||||
},
|
||||
raw: res,
|
||||
};
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件路由注册器实现
|
||||
* 为每个插件创建独立的路由注册器,收集路由定义
|
||||
*/
|
||||
export class PluginRouterRegistryImpl implements PluginRouterRegistry {
|
||||
private apiRoutes: PluginApiRouteDefinition[] = [];
|
||||
private pageDefinitions: PluginPageDefinition[] = [];
|
||||
private staticRoutes: Array<{ urlPath: string; localPath: string; }> = [];
|
||||
|
||||
constructor (
|
||||
private readonly pluginId: string,
|
||||
private readonly pluginPath: string
|
||||
) { }
|
||||
|
||||
// ==================== API 路由注册 ====================
|
||||
|
||||
api (method: HttpMethod, routePath: string, handler: PluginRequestHandler): void {
|
||||
this.apiRoutes.push({ method, path: routePath, handler });
|
||||
}
|
||||
|
||||
get (routePath: string, handler: PluginRequestHandler): void {
|
||||
this.api('get', routePath, handler);
|
||||
}
|
||||
|
||||
post (routePath: string, handler: PluginRequestHandler): void {
|
||||
this.api('post', routePath, handler);
|
||||
}
|
||||
|
||||
put (routePath: string, handler: PluginRequestHandler): void {
|
||||
this.api('put', routePath, handler);
|
||||
}
|
||||
|
||||
delete (routePath: string, handler: PluginRequestHandler): void {
|
||||
this.api('delete', routePath, handler);
|
||||
}
|
||||
|
||||
// ==================== 页面注册 ====================
|
||||
|
||||
page (pageDef: PluginPageDefinition): void {
|
||||
this.pageDefinitions.push(pageDef);
|
||||
}
|
||||
|
||||
pages (pageDefs: PluginPageDefinition[]): void {
|
||||
this.pageDefinitions.push(...pageDefs);
|
||||
}
|
||||
|
||||
// ==================== 静态资源 ====================
|
||||
|
||||
static (urlPath: string, localPath: string): void {
|
||||
// 如果是相对路径,则相对于插件目录
|
||||
const absolutePath = path.isAbsolute(localPath)
|
||||
? localPath
|
||||
: path.join(this.pluginPath, localPath);
|
||||
this.staticRoutes.push({ urlPath, localPath: absolutePath });
|
||||
}
|
||||
|
||||
// ==================== 构建路由 ====================
|
||||
|
||||
/**
|
||||
* 构建 Express Router(用于 API 路由)
|
||||
*/
|
||||
buildApiRouter (): Router {
|
||||
const router = Router();
|
||||
|
||||
// 注册静态文件路由
|
||||
for (const { urlPath, localPath } of this.staticRoutes) {
|
||||
router.use(urlPath, expressStatic(localPath));
|
||||
}
|
||||
|
||||
// 注册 API 路由
|
||||
for (const route of this.apiRoutes) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 包装处理器,添加错误处理和请求/响应包装
|
||||
*/
|
||||
private wrapHandler (handler: PluginRequestHandler): (req: Request, res: Response, next: NextFunction) => void {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const wrappedReq = wrapRequest(req);
|
||||
const wrappedRes = wrapResponse(res);
|
||||
await handler(wrappedReq, wrappedRes, next);
|
||||
} catch (error: any) {
|
||||
console.error(`[Plugin: ${this.pluginId}] Route error:`, error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
code: -1,
|
||||
message: `Plugin error: ${error.message || 'Unknown error'}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== 查询方法 ====================
|
||||
|
||||
/**
|
||||
* 检查是否有注册的 API 路由
|
||||
*/
|
||||
hasApiRoutes (): boolean {
|
||||
return this.apiRoutes.length > 0 || this.staticRoutes.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有注册的页面
|
||||
*/
|
||||
hasPages (): boolean {
|
||||
return this.pageDefinitions.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有注册的页面定义
|
||||
*/
|
||||
getPages (): PluginPageDefinition[] {
|
||||
return [...this.pageDefinitions];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件 ID
|
||||
*/
|
||||
getPluginId (): string {
|
||||
return this.pluginId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件路径
|
||||
*/
|
||||
getPluginPath (): string {
|
||||
return this.pluginPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空路由(用于插件卸载)
|
||||
*/
|
||||
clear (): void {
|
||||
this.apiRoutes = [];
|
||||
this.pageDefinitions = [];
|
||||
this.staticRoutes = [];
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { NapCatCore } from 'napcat-core';
|
||||
import { NapCatOneBot11Adapter, OB11Message } from '@/napcat-onebot/index';
|
||||
import { ActionMap } from '@/napcat-onebot/action';
|
||||
import { OB11EmitEventContent } from '@/napcat-onebot/network/index';
|
||||
import { NetworkAdapterConfig } from '@/napcat-onebot/config/config';
|
||||
|
||||
// ==================== 插件包信息 ====================
|
||||
|
||||
@@ -49,11 +50,130 @@ export interface INapCatConfigStatic {
|
||||
/** NapCatConfig 类型(包含静态方法) */
|
||||
export type NapCatConfigClass = INapCatConfigStatic;
|
||||
|
||||
// ==================== 插件路由相关类型(包装层,不直接依赖 express) ====================
|
||||
|
||||
/** HTTP 请求对象(包装类型) */
|
||||
export interface PluginHttpRequest {
|
||||
/** 请求路径 */
|
||||
path: string;
|
||||
/** 请求方法 */
|
||||
method: string;
|
||||
/** 查询参数 */
|
||||
query: Record<string, string | string[] | undefined>;
|
||||
/** 请求体 */
|
||||
body: unknown;
|
||||
/** 请求头 */
|
||||
headers: Record<string, string | string[] | undefined>;
|
||||
/** 路由参数 */
|
||||
params: Record<string, string>;
|
||||
/** 原始请求对象(用于高级用法) */
|
||||
raw: unknown;
|
||||
}
|
||||
|
||||
/** HTTP 响应对象(包装类型) */
|
||||
export interface PluginHttpResponse {
|
||||
/** 设置状态码 */
|
||||
status (code: number): PluginHttpResponse;
|
||||
/** 发送 JSON 响应 */
|
||||
json (data: unknown): void;
|
||||
/** 发送文本响应 */
|
||||
send (data: string | Buffer): void;
|
||||
/** 设置响应头 */
|
||||
setHeader (name: string, value: string): PluginHttpResponse;
|
||||
/** 发送文件 */
|
||||
sendFile (filePath: string): void;
|
||||
/** 重定向 */
|
||||
redirect (url: string): void;
|
||||
/** 原始响应对象(用于高级用法) */
|
||||
raw: unknown;
|
||||
}
|
||||
|
||||
/** 下一步函数类型 */
|
||||
export type PluginNextFunction = (err?: unknown) => void;
|
||||
|
||||
/** 插件请求处理器类型 */
|
||||
export type PluginRequestHandler = (
|
||||
req: PluginHttpRequest,
|
||||
res: PluginHttpResponse,
|
||||
next: PluginNextFunction
|
||||
) => void | Promise<void>;
|
||||
|
||||
/** HTTP 方法类型 */
|
||||
export type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'all';
|
||||
|
||||
/** 插件 API 路由定义 */
|
||||
export interface PluginApiRouteDefinition {
|
||||
/** HTTP 方法 */
|
||||
method: HttpMethod;
|
||||
/** 路由路径(相对于插件路由前缀) */
|
||||
path: string;
|
||||
/** 请求处理器 */
|
||||
handler: PluginRequestHandler;
|
||||
}
|
||||
|
||||
/** 插件页面定义 */
|
||||
export interface PluginPageDefinition {
|
||||
/** 页面路径(用于路由,如 'settings') */
|
||||
path: string;
|
||||
/** 页面标题(显示在 Tab 上) */
|
||||
title: string;
|
||||
/** 页面图标(可选,支持 emoji 或图标名) */
|
||||
icon?: string;
|
||||
/** 页面 HTML 文件路径(相对于插件目录) */
|
||||
htmlFile: string;
|
||||
/** 页面描述 */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/** 插件路由注册器 */
|
||||
export interface PluginRouterRegistry {
|
||||
// ==================== API 路由注册 ====================
|
||||
|
||||
/**
|
||||
* 注册单个 API 路由
|
||||
* @param method HTTP 方法
|
||||
* @param path 路由路径
|
||||
* @param handler 请求处理器
|
||||
*/
|
||||
api (method: HttpMethod, path: string, handler: PluginRequestHandler): void;
|
||||
/** 注册 GET API */
|
||||
get (path: string, handler: PluginRequestHandler): void;
|
||||
/** 注册 POST API */
|
||||
post (path: string, handler: PluginRequestHandler): void;
|
||||
/** 注册 PUT API */
|
||||
put (path: string, handler: PluginRequestHandler): void;
|
||||
/** 注册 DELETE API */
|
||||
delete (path: string, handler: PluginRequestHandler): void;
|
||||
|
||||
// ==================== 页面注册 ====================
|
||||
|
||||
/**
|
||||
* 注册插件页面
|
||||
* @param page 页面定义
|
||||
*/
|
||||
page (page: PluginPageDefinition): void;
|
||||
|
||||
/**
|
||||
* 注册多个插件页面
|
||||
* @param pages 页面定义数组
|
||||
*/
|
||||
pages (pages: PluginPageDefinition[]): void;
|
||||
|
||||
// ==================== 静态资源 ====================
|
||||
|
||||
/**
|
||||
* 提供静态文件服务
|
||||
* @param urlPath URL 路径
|
||||
* @param localPath 本地文件夹路径(相对于插件目录或绝对路径)
|
||||
*/
|
||||
static (urlPath: string, localPath: string): void;
|
||||
}
|
||||
|
||||
// ==================== 插件管理器接口 ====================
|
||||
|
||||
/** 插件管理器公共接口 */
|
||||
export interface IPluginManager {
|
||||
readonly config: unknown;
|
||||
readonly config: NetworkAdapterConfig;
|
||||
getPluginPath (): string;
|
||||
getPluginConfig (): PluginStatusConfig;
|
||||
getAllPlugins (): PluginEntry[];
|
||||
@@ -124,11 +244,16 @@ export interface NapCatPluginContext {
|
||||
pluginManager: IPluginManager;
|
||||
/** 插件日志器 - 自动添加插件名称前缀 */
|
||||
logger: PluginLogger;
|
||||
/**
|
||||
* WebUI 路由注册器
|
||||
* 用于注册插件的 HTTP API 路由,路由将挂载到 /api/Plugin/ext/{pluginId}/
|
||||
*/
|
||||
router: PluginRouterRegistry;
|
||||
}
|
||||
|
||||
// ==================== 插件模块接口 ====================
|
||||
|
||||
export interface PluginModule<T extends OB11EmitEventContent = OB11EmitEventContent> {
|
||||
export interface PluginModule<T extends OB11EmitEventContent = OB11EmitEventContent, C = unknown> {
|
||||
plugin_init: (ctx: NapCatPluginContext) => void | Promise<void>;
|
||||
plugin_onmessage?: (
|
||||
ctx: NapCatPluginContext,
|
||||
@@ -143,8 +268,8 @@ export interface PluginModule<T extends OB11EmitEventContent = OB11EmitEventCont
|
||||
) => void | Promise<void>;
|
||||
plugin_config_schema?: PluginConfigSchema;
|
||||
plugin_config_ui?: PluginConfigSchema;
|
||||
plugin_get_config?: (ctx: NapCatPluginContext) => unknown | Promise<unknown>;
|
||||
plugin_set_config?: (ctx: NapCatPluginContext, config: unknown) => void | Promise<void>;
|
||||
plugin_get_config?: (ctx: NapCatPluginContext) => C | Promise<C>;
|
||||
plugin_set_config?: (ctx: NapCatPluginContext, config: C) => void | Promise<void>;
|
||||
/**
|
||||
* 配置界面控制器 - 当配置界面打开时调用
|
||||
* 返回清理函数,在界面关闭时调用
|
||||
|
||||
Reference in New Issue
Block a user