Support memory static files and plugin APIs

Introduce in-memory static file support and inter-plugin exports. Add MemoryStaticFile/MemoryFileGenerator types and expose staticOnMem in PluginRouterRegistry; router registry now tracks memory routes and exposes getters. Add getPluginExports to plugin manager adapters to allow plugins to call each other's exported modules. WebUI backend gains routes to serve /plugin/:pluginId/mem/* (memory files) and /plugin/:pluginId/files/* (plugin filesystem static) without auth. Update builtin plugin to demonstrate staticOnMem and inter-plugin call, and add frontend UI to open extension pages in a new window. Note: API router no longer mounts static filesystem routes — those are handled by webui-backend.
This commit is contained in:
手瓜一十雪
2026-02-02 15:01:26 +08:00
parent 3a841ed0f1
commit 610e19e34c
7 changed files with 230 additions and 11 deletions

View File

@@ -194,6 +194,15 @@ export class OB11PluginManager extends IOB11NetworkAdapter<PluginConfig> impleme
this.pluginRouters.set(entry.id, router);
}
// 创建获取其他插件导出的方法
const getPluginExports = <T = any>(pluginId: string): T | undefined => {
const targetEntry = this.plugins.get(pluginId);
if (!targetEntry || !targetEntry.loaded || targetEntry.runtime.status !== 'loaded') {
return undefined;
}
return targetEntry.runtime.module as T;
};
return {
core: this.core,
oneBot: this.obContext,
@@ -207,6 +216,7 @@ export class OB11PluginManager extends IOB11NetworkAdapter<PluginConfig> impleme
pluginManager: this,
logger: pluginLogger,
router,
getPluginExports,
};
}

View File

@@ -1,4 +1,4 @@
import { Router, static as expressStatic, Request, Response, NextFunction } from 'express';
import { Router, Request, Response, NextFunction } from 'express';
import path from 'path';
import {
PluginRouterRegistry,
@@ -8,6 +8,7 @@ import {
PluginHttpRequest,
PluginHttpResponse,
HttpMethod,
MemoryStaticFile,
} from './types';
/**
@@ -59,10 +60,17 @@ function wrapResponse (res: Response): PluginHttpResponse {
* 插件路由注册器实现
* 为每个插件创建独立的路由注册器,收集路由定义
*/
/** 内存静态路由定义 */
interface MemoryStaticRoute {
urlPath: string;
files: MemoryStaticFile[];
}
export class PluginRouterRegistryImpl implements PluginRouterRegistry {
private apiRoutes: PluginApiRouteDefinition[] = [];
private pageDefinitions: PluginPageDefinition[] = [];
private staticRoutes: Array<{ urlPath: string; localPath: string; }> = [];
private memoryStaticRoutes: MemoryStaticRoute[] = [];
constructor (
private readonly pluginId: string,
@@ -111,19 +119,19 @@ export class PluginRouterRegistryImpl implements PluginRouterRegistry {
this.staticRoutes.push({ urlPath, localPath: absolutePath });
}
staticOnMem (urlPath: string, files: MemoryStaticFile[]): void {
this.memoryStaticRoutes.push({ urlPath, files });
}
// ==================== 构建路由 ====================
/**
* 构建 Express Router用于 API 路由)
* 注意:静态资源路由不在此处挂载,由 webui-backend 直接在不需要鉴权的路径下处理
*/
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);
@@ -179,7 +187,14 @@ export class PluginRouterRegistryImpl implements PluginRouterRegistry {
* 检查是否有注册的 API 路由
*/
hasApiRoutes (): boolean {
return this.apiRoutes.length > 0 || this.staticRoutes.length > 0;
return this.apiRoutes.length > 0;
}
/**
* 检查是否有注册的静态资源路由
*/
hasStaticRoutes (): boolean {
return this.staticRoutes.length > 0 || this.memoryStaticRoutes.length > 0;
}
/**
@@ -210,6 +225,20 @@ export class PluginRouterRegistryImpl implements PluginRouterRegistry {
return this.pluginPath;
}
/**
* 获取所有注册的静态路由
*/
getStaticRoutes (): Array<{ urlPath: string; localPath: string; }> {
return [...this.staticRoutes];
}
/**
* 获取所有注册的内存静态路由
*/
getMemoryStaticRoutes (): MemoryStaticRoute[] {
return [...this.memoryStaticRoutes];
}
/**
* 清空路由(用于插件卸载)
*/
@@ -217,5 +246,6 @@ export class PluginRouterRegistryImpl implements PluginRouterRegistry {
this.apiRoutes = [];
this.pageDefinitions = [];
this.staticRoutes = [];
this.memoryStaticRoutes = [];
}
}

View File

@@ -125,6 +125,19 @@ export interface PluginPageDefinition {
description?: string;
}
/** 内存文件生成器 - 用于动态生成静态文件内容 */
export type MemoryFileGenerator = () => string | Buffer | Promise<string | Buffer>;
/** 内存静态文件定义 */
export interface MemoryStaticFile {
/** 文件路径(相对于 urlPath */
path: string;
/** 文件内容或生成器 */
content: string | Buffer | MemoryFileGenerator;
/** 可选的 MIME 类型 */
contentType?: string;
}
/** 插件路由注册器 */
export interface PluginRouterRegistry {
// ==================== API 路由注册 ====================
@@ -167,6 +180,13 @@ export interface PluginRouterRegistry {
* @param localPath 本地文件夹路径(相对于插件目录或绝对路径)
*/
static (urlPath: string, localPath: string): void;
/**
* 提供内存生成的静态文件服务
* @param urlPath URL 路径
* @param files 内存文件列表
*/
staticOnMem (urlPath: string, files: MemoryStaticFile[]): void;
}
// ==================== 插件管理器接口 ====================
@@ -247,8 +267,15 @@ export interface NapCatPluginContext {
/**
* WebUI 路由注册器
* 用于注册插件的 HTTP API 路由,路由将挂载到 /api/Plugin/ext/{pluginId}/
* 静态资源将挂载到 /plugin/{pluginId}/files/{urlPath}/
*/
router: PluginRouterRegistry;
/**
* 获取其他插件的导出模块
* @param pluginId 目标插件 ID
* @returns 插件导出的模块,如果插件未加载则返回 undefined
*/
getPluginExports: <T = PluginModule>(pluginId: string) => T | undefined;
}
// ==================== 插件模块接口 ====================