mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-03-01 08:10:25 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5769b6a62 | ||
|
|
d9297c1e10 | ||
|
|
94f07ab98b | ||
|
|
01a6594707 | ||
|
|
82a7154b92 | ||
|
|
9b385ac9c9 | ||
|
|
e3d4cee416 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -16,4 +16,5 @@ checkVersion.sh
|
|||||||
bun.lockb
|
bun.lockb
|
||||||
tests/run/
|
tests/run/
|
||||||
guild1.db-wal
|
guild1.db-wal
|
||||||
guild1.db-shm
|
guild1.db-shm
|
||||||
|
packages/napcat-develop/config/.env
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export { PluginModule } from './plugin/types';
|
|||||||
export { PluginStatusConfig } from './plugin/types';
|
export { PluginStatusConfig } from './plugin/types';
|
||||||
export { PluginRouterRegistry, PluginRequestHandler, PluginApiRouteDefinition, PluginPageDefinition, HttpMethod } from './plugin/types';
|
export { PluginRouterRegistry, PluginRequestHandler, PluginApiRouteDefinition, PluginPageDefinition, HttpMethod } from './plugin/types';
|
||||||
export { PluginHttpRequest, PluginHttpResponse, PluginNextFunction } from './plugin/types';
|
export { PluginHttpRequest, PluginHttpResponse, PluginNextFunction } from './plugin/types';
|
||||||
|
export { MemoryStaticFile, MemoryFileGenerator } from './plugin/types';
|
||||||
export { PluginRouterRegistryImpl } from './plugin/router-registry';
|
export { PluginRouterRegistryImpl } from './plugin/router-registry';
|
||||||
export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> implements IPluginManager {
|
export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> implements IPluginManager {
|
||||||
private readonly pluginPath: string;
|
private readonly pluginPath: string;
|
||||||
@@ -179,6 +180,9 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> i
|
|||||||
this.pluginRouters.delete(entry.id);
|
this.pluginRouters.delete(entry.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理模块缓存
|
||||||
|
this.loader.clearCache(entry.pluginPath);
|
||||||
|
|
||||||
// 重置状态
|
// 重置状态
|
||||||
entry.loaded = false;
|
entry.loaded = false;
|
||||||
entry.runtime = {
|
entry.runtime = {
|
||||||
@@ -211,6 +215,15 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> i
|
|||||||
// 保存到路由注册表
|
// 保存到路由注册表
|
||||||
this.pluginRouters.set(entry.id, routerRegistry);
|
this.pluginRouters.set(entry.id, routerRegistry);
|
||||||
|
|
||||||
|
// 创建获取其他插件导出的方法
|
||||||
|
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 {
|
return {
|
||||||
core: this.core,
|
core: this.core,
|
||||||
oneBot: this.obContext,
|
oneBot: this.obContext,
|
||||||
@@ -224,6 +237,7 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> i
|
|||||||
pluginManager: this,
|
pluginManager: this,
|
||||||
logger: pluginLogger,
|
logger: pluginLogger,
|
||||||
router: routerRegistry,
|
router: routerRegistry,
|
||||||
|
getPluginExports,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { createRequire } from 'module';
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
import { LogWrapper } from 'napcat-core/helper/log';
|
import { LogWrapper } from 'napcat-core/helper/log';
|
||||||
import {
|
import {
|
||||||
PluginPackageJson,
|
PluginPackageJson,
|
||||||
@@ -295,4 +297,24 @@ export class PluginLoader {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 清除插件文件的 require 缓存
|
||||||
|
* 用于确保卸载插件时清理 CJS 模块缓存
|
||||||
|
*/
|
||||||
|
clearCache (pluginPath: string): void {
|
||||||
|
try {
|
||||||
|
// 规范化路径以确保匹配正确
|
||||||
|
const normalizedPluginPath = path.resolve(pluginPath);
|
||||||
|
|
||||||
|
// 遍历缓存并删除属于该插件目录的模块
|
||||||
|
Object.keys(require.cache).forEach((id) => {
|
||||||
|
if (id.startsWith(normalizedPluginPath)) {
|
||||||
|
delete require.cache[id];
|
||||||
|
this.logger.logDebug(`[PluginLoader] Cleared cache for: ${id}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.logError('[PluginLoader] Error clearing module cache:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,6 +194,15 @@ export class OB11PluginManager extends IOB11NetworkAdapter<PluginConfig> impleme
|
|||||||
this.pluginRouters.set(entry.id, router);
|
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 {
|
return {
|
||||||
core: this.core,
|
core: this.core,
|
||||||
oneBot: this.obContext,
|
oneBot: this.obContext,
|
||||||
@@ -207,6 +216,7 @@ export class OB11PluginManager extends IOB11NetworkAdapter<PluginConfig> impleme
|
|||||||
pluginManager: this,
|
pluginManager: this,
|
||||||
logger: pluginLogger,
|
logger: pluginLogger,
|
||||||
router,
|
router,
|
||||||
|
getPluginExports,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 path from 'path';
|
||||||
import {
|
import {
|
||||||
PluginRouterRegistry,
|
PluginRouterRegistry,
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
PluginHttpRequest,
|
PluginHttpRequest,
|
||||||
PluginHttpResponse,
|
PluginHttpResponse,
|
||||||
HttpMethod,
|
HttpMethod,
|
||||||
|
MemoryStaticFile,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -59,10 +60,17 @@ function wrapResponse (res: Response): PluginHttpResponse {
|
|||||||
* 插件路由注册器实现
|
* 插件路由注册器实现
|
||||||
* 为每个插件创建独立的路由注册器,收集路由定义
|
* 为每个插件创建独立的路由注册器,收集路由定义
|
||||||
*/
|
*/
|
||||||
|
/** 内存静态路由定义 */
|
||||||
|
interface MemoryStaticRoute {
|
||||||
|
urlPath: string;
|
||||||
|
files: MemoryStaticFile[];
|
||||||
|
}
|
||||||
|
|
||||||
export class PluginRouterRegistryImpl implements PluginRouterRegistry {
|
export class PluginRouterRegistryImpl implements PluginRouterRegistry {
|
||||||
private apiRoutes: PluginApiRouteDefinition[] = [];
|
private apiRoutes: PluginApiRouteDefinition[] = [];
|
||||||
private pageDefinitions: PluginPageDefinition[] = [];
|
private pageDefinitions: PluginPageDefinition[] = [];
|
||||||
private staticRoutes: Array<{ urlPath: string; localPath: string; }> = [];
|
private staticRoutes: Array<{ urlPath: string; localPath: string; }> = [];
|
||||||
|
private memoryStaticRoutes: MemoryStaticRoute[] = [];
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private readonly pluginId: string,
|
private readonly pluginId: string,
|
||||||
@@ -111,19 +119,19 @@ export class PluginRouterRegistryImpl implements PluginRouterRegistry {
|
|||||||
this.staticRoutes.push({ urlPath, localPath: absolutePath });
|
this.staticRoutes.push({ urlPath, localPath: absolutePath });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
staticOnMem (urlPath: string, files: MemoryStaticFile[]): void {
|
||||||
|
this.memoryStaticRoutes.push({ urlPath, files });
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 构建路由 ====================
|
// ==================== 构建路由 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建 Express Router(用于 API 路由)
|
* 构建 Express Router(用于 API 路由)
|
||||||
|
* 注意:静态资源路由不在此处挂载,由 webui-backend 直接在不需要鉴权的路径下处理
|
||||||
*/
|
*/
|
||||||
buildApiRouter (): Router {
|
buildApiRouter (): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
// 注册静态文件路由
|
|
||||||
for (const { urlPath, localPath } of this.staticRoutes) {
|
|
||||||
router.use(urlPath, expressStatic(localPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注册 API 路由
|
// 注册 API 路由
|
||||||
for (const route of this.apiRoutes) {
|
for (const route of this.apiRoutes) {
|
||||||
const handler = this.wrapHandler(route.handler);
|
const handler = this.wrapHandler(route.handler);
|
||||||
@@ -179,7 +187,14 @@ export class PluginRouterRegistryImpl implements PluginRouterRegistry {
|
|||||||
* 检查是否有注册的 API 路由
|
* 检查是否有注册的 API 路由
|
||||||
*/
|
*/
|
||||||
hasApiRoutes (): boolean {
|
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;
|
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.apiRoutes = [];
|
||||||
this.pageDefinitions = [];
|
this.pageDefinitions = [];
|
||||||
this.staticRoutes = [];
|
this.staticRoutes = [];
|
||||||
|
this.memoryStaticRoutes = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,6 +125,19 @@ export interface PluginPageDefinition {
|
|||||||
description?: string;
|
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 {
|
export interface PluginRouterRegistry {
|
||||||
// ==================== API 路由注册 ====================
|
// ==================== API 路由注册 ====================
|
||||||
@@ -167,6 +180,13 @@ export interface PluginRouterRegistry {
|
|||||||
* @param localPath 本地文件夹路径(相对于插件目录或绝对路径)
|
* @param localPath 本地文件夹路径(相对于插件目录或绝对路径)
|
||||||
*/
|
*/
|
||||||
static (urlPath: string, localPath: string): void;
|
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 路由注册器
|
* WebUI 路由注册器
|
||||||
* 用于注册插件的 HTTP API 路由,路由将挂载到 /api/Plugin/ext/{pluginId}/
|
* 用于注册插件的 HTTP API 路由,路由将挂载到 /api/Plugin/ext/{pluginId}/
|
||||||
|
* 静态资源将挂载到 /plugin/{pluginId}/files/{urlPath}/
|
||||||
*/
|
*/
|
||||||
router: PluginRouterRegistry;
|
router: PluginRouterRegistry;
|
||||||
|
/**
|
||||||
|
* 获取其他插件的导出模块
|
||||||
|
* @param pluginId 目标插件 ID
|
||||||
|
* @returns 插件导出的模块,如果插件未加载则返回 undefined
|
||||||
|
*/
|
||||||
|
getPluginExports: <T = PluginModule>(pluginId: string) => T | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 插件模块接口 ====================
|
// ==================== 插件模块接口 ====================
|
||||||
|
|||||||
@@ -65,10 +65,32 @@ const plugin_init: PluginModule['plugin_init'] = async (ctx) => {
|
|||||||
|
|
||||||
// ==================== 注册 WebUI 路由示例 ====================
|
// ==================== 注册 WebUI 路由示例 ====================
|
||||||
|
|
||||||
// 注册静态资源目录(webui 目录下的文件可通过 /api/Plugin/ext/{pluginId}/static/ 访问)
|
// 注册静态资源目录
|
||||||
|
// 静态资源可通过 /plugin/{pluginId}/files/static/ 访问(无需鉴权)
|
||||||
ctx.router.static('/static', 'webui');
|
ctx.router.static('/static', 'webui');
|
||||||
|
|
||||||
// 注册 API 路由
|
// 注册内存生成的静态资源(无需鉴权)
|
||||||
|
// 可通过 /plugin/{pluginId}/mem/dynamic/info.json 访问
|
||||||
|
ctx.router.staticOnMem('/dynamic', [
|
||||||
|
{
|
||||||
|
path: '/info.json',
|
||||||
|
contentType: 'application/json',
|
||||||
|
// 使用生成器函数动态生成内容
|
||||||
|
content: () => JSON.stringify({
|
||||||
|
pluginName: ctx.pluginName,
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
uptime: Date.now() - startTime,
|
||||||
|
config: currentConfig
|
||||||
|
}, null, 2)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/readme.txt',
|
||||||
|
contentType: 'text/plain',
|
||||||
|
content: `NapCat Builtin Plugin\n=====================\nThis is a demonstration of the staticOnMem feature.\nPlugin: ${ctx.pluginName}\nPath: ${ctx.pluginPath}`
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 注册 API 路由(需要鉴权,挂载到 /api/Plugin/ext/{pluginId}/)
|
||||||
ctx.router.get('/status', (_req, res) => {
|
ctx.router.get('/status', (_req, res) => {
|
||||||
const uptime = Date.now() - startTime;
|
const uptime = Date.now() - startTime;
|
||||||
res.json({
|
res.json({
|
||||||
@@ -107,7 +129,38 @@ const plugin_init: PluginModule['plugin_init'] = async (ctx) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 注册扩展页面
|
// ==================== 插件互调用示例 ====================
|
||||||
|
// 演示如何调用其他插件的导出方法
|
||||||
|
ctx.router.get('/call-plugin/:pluginId', (req, res) => {
|
||||||
|
const { pluginId } = req.params;
|
||||||
|
|
||||||
|
// 使用 getPluginExports 获取其他插件的导出模块
|
||||||
|
const targetPlugin = ctx.getPluginExports<PluginModule>(pluginId);
|
||||||
|
|
||||||
|
if (!targetPlugin) {
|
||||||
|
res.status(404).json({
|
||||||
|
code: -1,
|
||||||
|
message: `Plugin '${pluginId}' not found or not loaded`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回目标插件的信息
|
||||||
|
res.json({
|
||||||
|
code: 0,
|
||||||
|
data: {
|
||||||
|
pluginId,
|
||||||
|
hasInit: typeof targetPlugin.plugin_init === 'function',
|
||||||
|
hasOnMessage: typeof targetPlugin.plugin_onmessage === 'function',
|
||||||
|
hasOnEvent: typeof targetPlugin.plugin_onevent === 'function',
|
||||||
|
hasCleanup: typeof targetPlugin.plugin_cleanup === 'function',
|
||||||
|
hasConfigSchema: Array.isArray(targetPlugin.plugin_config_schema),
|
||||||
|
hasConfigUI: Array.isArray(targetPlugin.plugin_config_ui),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册扩展页面(无需鉴权,可通过 /plugin/{pluginId}/page/dashboard 访问)
|
||||||
ctx.router.page({
|
ctx.router.page({
|
||||||
path: 'dashboard',
|
path: 'dashboard',
|
||||||
title: '插件仪表盘',
|
title: '插件仪表盘',
|
||||||
@@ -116,7 +169,11 @@ const plugin_init: PluginModule['plugin_init'] = async (ctx) => {
|
|||||||
description: '查看内置插件的运行状态和配置'
|
description: '查看内置插件的运行状态和配置'
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info('WebUI 路由已注册: /api/Plugin/ext/' + ctx.pluginName);
|
logger.info('WebUI 路由已注册:');
|
||||||
|
logger.info(' - API 路由: /api/Plugin/ext/' + ctx.pluginName + '/');
|
||||||
|
logger.info(' - 扩展页面: /plugin/' + ctx.pluginName + '/page/dashboard');
|
||||||
|
logger.info(' - 静态资源: /plugin/' + ctx.pluginName + '/files/static/');
|
||||||
|
logger.info(' - 内存资源: /plugin/' + ctx.pluginName + '/mem/dynamic/');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const plugin_get_config: PluginModule['plugin_get_config'] = async () => {
|
export const plugin_get_config: PluginModule['plugin_get_config'] = async () => {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"description": "NapCat 内置插件",
|
"description": "NapCat 内置插件",
|
||||||
"author": "NapNeko",
|
"author": "NapNeko",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"napcat-types": "0.0.14"
|
"napcat-types": "0.0.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.0.1"
|
"@types/node": "^22.0.1"
|
||||||
|
|||||||
@@ -259,11 +259,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="static-content">
|
<div id="static-content">
|
||||||
<p style="color: var(--text-secondary); font-size: 13px; margin-bottom: 12px;">
|
<p style="color: var(--text-secondary); font-size: 13px; margin-bottom: 12px;">
|
||||||
测试插件静态资源服务是否正常工作
|
测试插件静态资源服务是否正常工作(不需要鉴权)
|
||||||
</p>
|
</p>
|
||||||
<div class="actions" style="margin-top: 0;">
|
<div class="actions" style="margin-top: 0;">
|
||||||
<button class="btn btn-primary" onclick="testStaticResource()">
|
<button class="btn btn-primary" onclick="testStaticResource()">
|
||||||
获取 test.txt
|
获取 test.txt(文件系统)
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" onclick="testMemoryResource()">
|
||||||
|
获取 info.json(内存生成)
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="static-result" style="margin-top: 12px;"></div>
|
<div id="static-result" style="margin-top: 12px;"></div>
|
||||||
@@ -280,8 +283,12 @@
|
|||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const webuiToken = urlParams.get('webui_token') || '';
|
const webuiToken = urlParams.get('webui_token') || '';
|
||||||
|
|
||||||
// 插件自行管理 API 调用
|
// 插件 API 基础路径(需要鉴权)
|
||||||
const apiBase = '/api/Plugin/ext/napcat-plugin-builtin';
|
const apiBase = '/api/Plugin/ext/napcat-plugin-builtin';
|
||||||
|
// 插件静态资源基础路径(不需要鉴权)
|
||||||
|
const staticBase = '/plugin/napcat-plugin-builtin/files';
|
||||||
|
// 插件内存资源基础路径(不需要鉴权)
|
||||||
|
const memBase = '/plugin/napcat-plugin-builtin/mem';
|
||||||
|
|
||||||
// 封装 fetch,自动携带认证
|
// 封装 fetch,自动携带认证
|
||||||
async function authFetch (url, options = {}) {
|
async function authFetch (url, options = {}) {
|
||||||
@@ -392,12 +399,13 @@
|
|||||||
resultDiv.innerHTML = '<div class="loading">加载中</div>';
|
resultDiv.innerHTML = '<div class="loading">加载中</div>';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await authFetch(`${apiBase}/static/test.txt`);
|
// 静态资源不需要鉴权,直接请求
|
||||||
|
const response = await fetch(`${staticBase}/static/test.txt`);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
resultDiv.innerHTML = `
|
resultDiv.innerHTML = `
|
||||||
<div style="background: var(--bg-item); border: 1px solid var(--border-color); border-radius: 8px; padding: 12px;">
|
<div style="background: var(--bg-item); border: 1px solid var(--border-color); border-radius: 8px; padding: 12px;">
|
||||||
<div style="font-size: 11px; color: var(--success-color); margin-bottom: 8px;">静态资源访问成功</div>
|
<div style="font-size: 11px; color: var(--success-color); margin-bottom: 8px;">文件系统静态资源访问成功</div>
|
||||||
<pre style="font-family: Monaco, Consolas, monospace; font-size: 12px; color: var(--text-primary); white-space: pre-wrap; margin: 0;">${text}</pre>
|
<pre style="font-family: Monaco, Consolas, monospace; font-size: 12px; color: var(--text-primary); white-space: pre-wrap; margin: 0;">${text}</pre>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -408,6 +416,30 @@
|
|||||||
resultDiv.innerHTML = `<div class="error">请求失败: ${error.message}</div>`;
|
resultDiv.innerHTML = `<div class="error">请求失败: ${error.message}</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 测试内存资源
|
||||||
|
async function testMemoryResource () {
|
||||||
|
const resultDiv = document.getElementById('static-result');
|
||||||
|
resultDiv.innerHTML = '<div class="loading">加载中</div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 内存资源不需要鉴权,直接请求
|
||||||
|
const response = await fetch(`${memBase}/dynamic/info.json`);
|
||||||
|
if (response.ok) {
|
||||||
|
const json = await response.json();
|
||||||
|
resultDiv.innerHTML = `
|
||||||
|
<div style="background: var(--bg-item); border: 1px solid var(--border-color); border-radius: 8px; padding: 12px;">
|
||||||
|
<div style="font-size: 11px; color: var(--success-color); margin-bottom: 8px;">内存生成资源访问成功</div>
|
||||||
|
<pre style="font-family: Monaco, Consolas, monospace; font-size: 12px; color: var(--text-primary); white-space: pre-wrap; margin: 0;">${JSON.stringify(json, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
resultDiv.innerHTML = `<div class="error">请求失败: ${response.status} ${response.statusText}</div>`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
resultDiv.innerHTML = `<div class="error">请求失败: ${error.message}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export default defineConfig({
|
|||||||
'@/napcat-schema': resolve(__dirname, './src'),
|
'@/napcat-schema': resolve(__dirname, './src'),
|
||||||
'@/napcat-core': resolve(__dirname, '../napcat-core'),
|
'@/napcat-core': resolve(__dirname, '../napcat-core'),
|
||||||
'@/napcat-webui-backend': resolve(__dirname, '../napcat-webui-backend'),
|
'@/napcat-webui-backend': resolve(__dirname, '../napcat-webui-backend'),
|
||||||
|
'@/napcat-image-size': resolve(__dirname, '../napcat-image-size'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "napcat-types",
|
"name": "napcat-types",
|
||||||
"version": "0.0.14",
|
"version": "0.0.15",
|
||||||
"private": false,
|
"private": false,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "./napcat-types/index.d.ts",
|
"types": "./napcat-types/index.d.ts",
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import compression from 'compression';
|
|||||||
import { napCatVersion } from 'napcat-common/src/version';
|
import { napCatVersion } from 'napcat-common/src/version';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { dirname, resolve } from 'node:path';
|
import { dirname, resolve } from 'node:path';
|
||||||
|
import { NapCatOneBot11Adapter } from '@/napcat-onebot/index';
|
||||||
|
import { OB11PluginMangerAdapter } from '@/napcat-onebot/network/plugin-manger';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
@@ -294,6 +296,108 @@ export async function InitWebUi (logger: ILogWrapper, pathWrapper: NapCatPathWra
|
|||||||
app.use('/webui', express.static(pathWrapper.staticPath, {
|
app.use('/webui', express.static(pathWrapper.staticPath, {
|
||||||
maxAge: '1d',
|
maxAge: '1d',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// 插件内存静态资源路由(不需要鉴权)
|
||||||
|
// 路径格式: /plugin/:pluginId/mem/:urlPath/*
|
||||||
|
app.use('/plugin/:pluginId/mem', async (req, res) => {
|
||||||
|
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);
|
||||||
|
const memoryRoutes = routerRegistry?.getMemoryStaticRoutes() || [];
|
||||||
|
|
||||||
|
for (const { urlPath, files } of memoryRoutes) {
|
||||||
|
const prefix = urlPath.startsWith('/') ? urlPath : '/' + urlPath;
|
||||||
|
if (req.path.startsWith(prefix)) {
|
||||||
|
const filePath = '/' + (req.path.substring(prefix.length).replace(/^\//, '') || '');
|
||||||
|
const memFile = files.find(f => ('/' + f.path.replace(/^\//, '')) === filePath);
|
||||||
|
if (memFile) {
|
||||||
|
try {
|
||||||
|
const content = typeof memFile.content === 'function' ? await memFile.content() : memFile.content;
|
||||||
|
res.setHeader('Content-Type', memFile.contentType || 'application/octet-stream');
|
||||||
|
return res.send(content);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[Plugin: ${pluginId}] Error serving memory file:`, err);
|
||||||
|
return res.status(500).json({ code: -1, message: 'Error serving memory file' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res.status(404).json({ code: -1, message: 'Memory file not found' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// 插件页面路由(不需要鉴权)
|
||||||
|
// 路径格式: /plugin/:pluginId/page/:pagePath
|
||||||
|
app.get('/plugin/:pluginId/page/:pagePath', (req, res) => {
|
||||||
|
const { pluginId, pagePath } = 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.hasPages()) {
|
||||||
|
return res.status(404).json({ code: -1, message: `Plugin '${pluginId}' has no registered pages` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const pages = routerRegistry.getPages();
|
||||||
|
const page = pages.find(p => p.path === '/' + pagePath || p.path === pagePath);
|
||||||
|
if (!page) {
|
||||||
|
return res.status(404).json({ code: -1, message: `Page '${pagePath}' not found in plugin '${pluginId}'` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginPath = routerRegistry.getPluginPath();
|
||||||
|
if (!pluginPath) {
|
||||||
|
return res.status(500).json({ code: -1, message: 'Plugin path not available' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlFilePath = join(pluginPath, page.htmlFile);
|
||||||
|
if (!existsSync(htmlFilePath)) {
|
||||||
|
return res.status(404).json({ code: -1, message: `HTML file not found: ${page.htmlFile}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.sendFile(htmlFilePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 插件文件系统静态资源路由(不需要鉴权)
|
||||||
|
// 路径格式: /plugin/:pluginId/files/*
|
||||||
|
app.use('/plugin/:pluginId/files', (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);
|
||||||
|
const staticRoutes = routerRegistry?.getStaticRoutes() || [];
|
||||||
|
|
||||||
|
for (const { urlPath, localPath } of staticRoutes) {
|
||||||
|
const prefix = urlPath.startsWith('/') ? urlPath : '/' + urlPath;
|
||||||
|
if (req.path.startsWith(prefix) || req.path === prefix.slice(0, -1)) {
|
||||||
|
const staticMiddleware = express.static(localPath, { maxAge: '1d' });
|
||||||
|
const originalUrl = req.url;
|
||||||
|
req.url = '/' + (req.path.substring(prefix.length).replace(/^\//, '') || '');
|
||||||
|
return staticMiddleware(req, res, (err) => {
|
||||||
|
req.url = originalUrl;
|
||||||
|
err ? next(err) : next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.status(404).json({ code: -1, message: 'Static resource not found' });
|
||||||
|
});
|
||||||
|
|
||||||
// 初始化WebSocket服务器
|
// 初始化WebSocket服务器
|
||||||
const sslCerts = await checkCertificates(logger);
|
const sslCerts = await checkCertificates(logger);
|
||||||
const isHttps = !!sslCerts;
|
const isHttps = !!sslCerts;
|
||||||
|
|||||||
@@ -254,11 +254,13 @@ export const InstallPluginFromStoreHandler: RequestHandler = async (req, res) =>
|
|||||||
// 删除临时文件
|
// 删除临时文件
|
||||||
fs.unlinkSync(tempZipPath);
|
fs.unlinkSync(tempZipPath);
|
||||||
|
|
||||||
// 如果 pluginManager 存在,立即注册插件
|
// 如果 pluginManager 存在,立即注册或重载插件
|
||||||
const pluginManager = getPluginManager();
|
const pluginManager = getPluginManager();
|
||||||
if (pluginManager) {
|
if (pluginManager) {
|
||||||
// 检查是否已注册,避免重复注册
|
// 如果插件已存在,则重载以刷新版本信息;否则注册新插件
|
||||||
if (!pluginManager.getPluginInfo(id)) {
|
if (pluginManager.getPluginInfo(id)) {
|
||||||
|
await pluginManager.reloadPlugin(id);
|
||||||
|
} else {
|
||||||
await pluginManager.loadPluginById(id);
|
await pluginManager.loadPluginById(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,11 +338,14 @@ export const InstallPluginFromStoreSSEHandler: RequestHandler = async (req, res)
|
|||||||
sendProgress('解压完成,正在清理...', 90);
|
sendProgress('解压完成,正在清理...', 90);
|
||||||
fs.unlinkSync(tempZipPath);
|
fs.unlinkSync(tempZipPath);
|
||||||
|
|
||||||
// 如果 pluginManager 存在,立即注册插件
|
// 如果 pluginManager 存在,立即注册或重载插件
|
||||||
const pluginManager = getPluginManager();
|
const pluginManager = getPluginManager();
|
||||||
if (pluginManager) {
|
if (pluginManager) {
|
||||||
// 检查是否已注册,避免重复注册
|
// 如果插件已存在,则重载以刷新版本信息;否则注册新插件
|
||||||
if (!pluginManager.getPluginInfo(id)) {
|
if (pluginManager.getPluginInfo(id)) {
|
||||||
|
sendProgress('正在刷新插件信息...', 95);
|
||||||
|
await pluginManager.reloadPlugin(id);
|
||||||
|
} else {
|
||||||
sendProgress('正在注册插件...', 95);
|
sendProgress('正在注册插件...', 95);
|
||||||
await pluginManager.loadPluginById(id);
|
await pluginManager.loadPluginById(id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,13 +63,12 @@ export default function ExtensionPage () {
|
|||||||
}, [extensionPages]);
|
}, [extensionPages]);
|
||||||
|
|
||||||
// 获取当前选中页面的 iframe URL
|
// 获取当前选中页面的 iframe URL
|
||||||
|
// 新路由格式不需要鉴权: /plugin/:pluginId/page/:pagePath
|
||||||
const currentPageUrl = useMemo(() => {
|
const currentPageUrl = useMemo(() => {
|
||||||
if (!selectedTab) return '';
|
if (!selectedTab) return '';
|
||||||
const [pluginId, ...pathParts] = selectedTab.split(':');
|
const [pluginId, ...pathParts] = selectedTab.split(':');
|
||||||
const path = pathParts.join(':').replace(/^\//, '');
|
const path = pathParts.join(':').replace(/^\//, '');
|
||||||
// 获取认证 token
|
return `/plugin/${pluginId}/page/${path}`;
|
||||||
const token = localStorage.getItem('token') || '';
|
|
||||||
return `/api/Plugin/page/${pluginId}/${path}?webui_token=${token}`;
|
|
||||||
}, [selectedTab]);
|
}, [selectedTab]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -86,6 +85,13 @@ export default function ExtensionPage () {
|
|||||||
setIframeLoading(false);
|
setIframeLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 在新窗口打开页面(新路由不需要鉴权)
|
||||||
|
const openInNewWindow = (pluginId: string, path: string) => {
|
||||||
|
const cleanPath = path.replace(/^\//, '');
|
||||||
|
const url = `/plugin/${pluginId}/page/${cleanPath}`;
|
||||||
|
window.open(url, '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<title>扩展页面 - NapCat WebUI</title>
|
<title>扩展页面 - NapCat WebUI</title>
|
||||||
@@ -125,7 +131,16 @@ export default function ExtensionPage () {
|
|||||||
title={
|
title={
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
{tab.icon && <span>{tab.icon}</span>}
|
{tab.icon && <span>{tab.icon}</span>}
|
||||||
<span>{tab.title}</span>
|
<span
|
||||||
|
className='cursor-pointer hover:underline'
|
||||||
|
title='点击在新窗口打开'
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
openInNewWindow(tab.pluginId, tab.path);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.title}
|
||||||
|
</span>
|
||||||
<span className='text-xs text-default-400'>({tab.pluginName})</span>
|
<span className='text-xs text-default-400'>({tab.pluginName})</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -232,8 +232,8 @@ importers:
|
|||||||
packages/napcat-plugin-builtin:
|
packages/napcat-plugin-builtin:
|
||||||
dependencies:
|
dependencies:
|
||||||
napcat-types:
|
napcat-types:
|
||||||
specifier: 0.0.14
|
specifier: 0.0.15
|
||||||
version: 0.0.14
|
version: 0.0.15
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.0.1
|
specifier: ^22.0.1
|
||||||
@@ -5448,8 +5448,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.14:
|
napcat-types@0.0.15:
|
||||||
resolution: {integrity: sha512-q5ke+vzzXeZkYPsr9jmj94NxgH63/xv5yS/lPEU++A3x2mOM8SYJqdFEMbHG1QIFciyH1u3qnnNiJ0mBxOBFbA==}
|
resolution: {integrity: sha512-uOkaQPO3SVgkO/Rt0cQ+02wCI9C9jzdYVViHByHrr9sA+2ZjT1HV5nVSgNNQXUaZ9q405LUu45xQ4lysNyLpBA==}
|
||||||
|
|
||||||
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==}
|
||||||
@@ -12774,7 +12774,7 @@ snapshots:
|
|||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
napcat-types@0.0.14:
|
napcat-types@0.0.15:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sinclair/typebox': 0.34.41
|
'@sinclair/typebox': 0.34.41
|
||||||
'@types/node': 22.19.1
|
'@types/node': 22.19.1
|
||||||
|
|||||||
Reference in New Issue
Block a user