NapCatQQ source snapshot
Build NapCat Artifacts / Type Check & Test (push) Waiting to run
Release Tag / validate-version (push) Waiting to run

This commit is contained in:
TanTan committed 2026-11-13 09:01:47 +08:00
commit f954252a34
1111 files changed
+139867

No files matched your search

@@ -0,0 +1,39 @@
import { PluginConfigItem, PluginConfigSchema } from './types';
/**
* NapCat 插件配置构建器
* 提供便捷的配置项创建方法
*/
export class NapCatConfig {
static text (key: string, label: string, defaultValue?: string, description?: string, reactive?: boolean): PluginConfigItem {
return { key, type: 'string', label, default: defaultValue, description, reactive };
}
static number (key: string, label: string, defaultValue?: number, description?: string, reactive?: boolean): PluginConfigItem {
return { key, type: 'number', label, default: defaultValue, description, reactive };
}
static boolean (key: string, label: string, defaultValue?: boolean, description?: string, reactive?: boolean): PluginConfigItem {
return { key, type: 'boolean', label, default: defaultValue, description, reactive };
}
static select (key: string, label: string, options: { label: string; value: string | number; }[], defaultValue?: string | number, description?: string, reactive?: boolean): PluginConfigItem {
return { key, type: 'select', label, options, default: defaultValue, description, reactive };
}
static multiSelect (key: string, label: string, options: { label: string; value: string | number; }[], defaultValue?: (string | number)[], description?: string, reactive?: boolean): PluginConfigItem {
return { key, type: 'multi-select', label, options, default: defaultValue, description, reactive };
}
static html (content: string): PluginConfigItem {
return { key: `_html_${Math.random().toString(36).slice(2)}`, type: 'html', label: '', default: content };
}
static plainText (content: string): PluginConfigItem {
return { key: `_text_${Math.random().toString(36).slice(2)}`, type: 'text', label: '', default: content };
}
static combine (...items: PluginConfigItem[]): PluginConfigSchema {
return items;
}
}
@@ -0,0 +1,23 @@
// 导出类型
export type {
PluginPackageJson,
PluginConfigItem,
PluginConfigSchema,
INapCatConfigStatic,
NapCatConfigClass,
IPluginManager,
PluginConfigUIController,
PluginLogger,
NapCatPluginContext,
PluginModule,
PluginRuntimeStatus,
PluginRuntime,
PluginEntry,
PluginStatusConfig,
} from './types';
// 导出配置构建器
export { NapCatConfig } from './config';
// 导出加载器
export { PluginLoader } from './loader';
@@ -0,0 +1,429 @@
import fs from 'fs';
import path from 'path';
import { createRequire } from 'module';
import { LogWrapper } from 'napcat-core/helper/log';
import {
PluginPackageJson,
PluginModule,
PluginEntry,
PluginStatusConfig,
} from './types';
const require = createRequire(import.meta.url);
/** 允许加载的官方插件 ID 白名单。不在表内的插件一律拒绝加载。 */
const OFFICIAL_PLUGIN_IDS = new Set<string>([
'napcat-plugin-builtin',
'napcat-plugin-cleaner',
'napcat-plugin-ssqq',
'napcat-plugin-qce',
]);
/**
* 敏感词表:插件源码命中(黑灰产 / 支付发卡相关)即拒绝加载。
* 仅作启发式威慑,混淆/编码可绕过,并非唯一防线。
*/
const SENSITIVE_KEYWORDS: string[] = [
'发卡', '卡密', '卡网', '自动发卡', '寄售', '代刷', '刷单', '跑分',
'四件套', '洗钱', '黑产', '灰产', '博彩', '赌博', '菠菜', '涉黄',
'色情', '收款码', '代付', '担保交易', '第三方支付', 'USDT',
];
/** 纳入敏感词检测的源码扩展名 */
const SCAN_EXTS = new Set(['.js', '.mjs', '.cjs', '.ts']);
/** 跳过敏感词扫描的目录名 */
const SCAN_SKIP_DIRS = new Set(['data', 'node_modules', '.git']);
/** 单文件扫描大小上限,避免超大文件拖慢启动 */
const SCAN_MAX_FILE_BYTES = 5 * 1024 * 1024;
/**
* 插件加载器
* 负责扫描、加载和导入插件模块
*/
export class PluginLoader {
constructor (
private readonly pluginPath: string,
private readonly configPath: string,
private readonly logger: LogWrapper
) { }
/**
* 加载插件状态配置
*/
loadPluginStatusConfig (): PluginStatusConfig {
if (fs.existsSync(this.configPath)) {
try {
return JSON.parse(fs.readFileSync(this.configPath, 'utf-8'));
} catch (e) {
this.logger.logWarn('[PluginLoader] Error parsing plugins.json', e);
}
}
return {};
}
/**
* 保存插件状态配置
*/
savePluginStatusConfig (config: PluginStatusConfig): void {
try {
fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), 'utf-8');
} catch (e) {
this.logger.logError('[PluginLoader] Error saving plugins.json', e);
}
}
/**
* 扫描插件目录,收集所有有效插件条目(异步版本,验证模块有效性)
* 只有包含有效 plugin_init 函数的插件才会被收集
*/
async scanPlugins (): Promise<PluginEntry[]> {
const entries: PluginEntry[] = [];
// 确保插件目录存在
if (!fs.existsSync(this.pluginPath)) {
this.logger.logWarn(`[PluginLoader] Plugin directory does not exist: ${this.pluginPath}`);
fs.mkdirSync(this.pluginPath, { recursive: true });
return entries;
}
const items = fs.readdirSync(this.pluginPath, { withFileTypes: true });
const statusConfig = this.loadPluginStatusConfig();
for (const item of items) {
if (!item.isDirectory()) {
continue;
}
const entry = this.scanDirectoryPlugin(item.name, statusConfig);
if (!entry) {
continue;
}
// 如果没有入口文件,跳过
if (!entry.entryPath) {
this.logger.logWarn(`[PluginLoader] Skipping ${item.name}: no entry file found`);
continue;
}
// 如果插件被禁用,跳过模块验证,直接添加到列表
if (!entry.enable) {
entries.push(entry);
continue;
}
// 验证模块有效性(仅对启用的插件)
const validation = await this.validatePluginEntry(entry.entryPath);
if (!validation.valid) {
this.logger.logWarn(`[PluginLoader] Skipping ${item.name}: ${validation.error}`);
continue;
}
entries.push(entry);
}
return entries;
}
/**
* 扫描单个目录插件
*/
private scanDirectoryPlugin (dirname: string, statusConfig: PluginStatusConfig): PluginEntry | null {
const pluginDir = path.join(this.pluginPath, dirname);
try {
// 尝试读取 package.json
let packageJson: PluginPackageJson | undefined;
const packageJsonPath = path.join(pluginDir, 'package.json');
if (fs.existsSync(packageJsonPath)) {
try {
const packageContent = fs.readFileSync(packageJsonPath, 'utf-8');
packageJson = JSON.parse(packageContent);
} catch (error) {
this.logger.logWarn(`[PluginLoader] Invalid package.json in ${dirname}:`, error);
}
}
// 获取插件 ID(包名或目录名)
const pluginId = packageJson?.name || dirname;
// 官方白名单 + 敏感词校验:被拒绝的插件视为不可见,
// 不入列表、不加载、更不会被动态导入执行。
// 该检查置于此处统一拦截 scanPlugins 与 rescanPlugin(安装/重载)两条路径。
const rejectReason = this.getRejectReason(pluginId, pluginDir);
if (rejectReason) {
this.logger.logWarn(`[PluginLoader] Rejected ${pluginId} (${dirname}): ${rejectReason}`);
return null;
}
// 确定入口文件
const entryFile = this.findEntryFile(pluginDir, packageJson);
const entryPath = entryFile ? path.join(pluginDir, entryFile) : undefined;
// 获取启用状态(默认禁用,内置插件除外)
const enable = statusConfig[pluginId] ?? (pluginId === 'napcat-plugin-builtin');
// 创建插件条目
const entry: PluginEntry = {
id: pluginId,
fileId: dirname,
name: packageJson?.name,
version: packageJson?.version,
description: packageJson?.description,
author: packageJson?.author,
pluginPath: pluginDir,
entryPath,
packageJson,
enable,
loaded: false,
runtime: {
status: 'unloaded',
},
};
// 如果没有入口文件,标记为错误
if (!entryPath) {
entry.runtime = {
status: 'error',
error: `No valid entry file found for plugin directory: ${dirname}`,
};
}
return entry;
} catch (error: any) {
// 创建错误条目
return {
id: dirname, // 使用目录名作为 ID
fileId: dirname,
pluginPath: path.join(this.pluginPath, dirname),
enable: statusConfig[dirname] ?? (dirname === 'napcat-plugin-builtin'),
loaded: false,
runtime: {
status: 'error',
error: error.message || 'Unknown error during scan',
},
};
}
}
/**
* 查找插件目录的入口文件
*/
private findEntryFile (pluginDir: string, packageJson?: PluginPackageJson): string | null {
const possibleEntries = [
packageJson?.main,
'index.mjs',
'index.js',
'main.mjs',
'main.js',
].filter(Boolean) as string[];
for (const entry of possibleEntries) {
const root = path.resolve(pluginDir);
const entryPath = path.resolve(pluginDir, entry);
// 拒绝 package.json main 指向插件目录之外(防路径穿越)
if (entryPath !== root && !entryPath.startsWith(root + path.sep)) {
continue;
}
if (fs.existsSync(entryPath) && fs.statSync(entryPath).isFile()) {
return entry;
}
}
return null;
}
/** 是否为白名单内的官方插件 */
isOfficialPlugin (pluginId: string): boolean {
return OFFICIAL_PLUGIN_IDS.has(pluginId);
}
/**
* 判断插件是否应被拒绝(非官方 / 命中敏感词)。
* @returns 拒绝原因;通过校验返回 null
*/
private getRejectReason (pluginId: string, pluginDir: string): string | null {
const hit = this.scanSensitiveWords(pluginDir);
if (this.isOfficialPlugin(pluginId)) {
return null;
}
if (hit) {
return `sensitive keyword "${hit}"`;
}
return 'not in official plugin whitelist';
}
/**
* 递归扫描插件目录内源码是否命中敏感词(黑灰产 / 支付发卡)。
* @returns 命中的敏感词;未命中返回 null
*/
scanSensitiveWords (pluginDir: string): string | null {
const visit = (dir: string): string | null => {
let items: fs.Dirent[];
try {
items = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return null;
}
for (const item of items) {
if (item.isDirectory()) {
if (SCAN_SKIP_DIRS.has(item.name)) {
continue;
}
const hit = visit(path.join(dir, item.name));
if (hit) {
return hit;
}
continue;
}
if (!item.isFile()) {
continue;
}
if (!SCAN_EXTS.has(path.extname(item.name).toLowerCase())) {
continue;
}
const filePath = path.join(dir, item.name);
try {
if (fs.statSync(filePath).size > SCAN_MAX_FILE_BYTES) {
continue;
}
const content = fs.readFileSync(filePath, 'utf-8');
for (const word of SENSITIVE_KEYWORDS) {
if (content.includes(word)) {
return word;
}
}
} catch {
// 读取失败忽略该文件
}
}
return null;
};
return visit(pluginDir);
}
/**
* 动态导入模块
*/
async importModule (filePath: string): Promise<any> {
const fileUrl = `file://${filePath.replace(/\\/g, '/')}`;
const fileUrlWithQuery = `${fileUrl}?t=${Date.now()}`;
return await import(fileUrlWithQuery);
}
/**
* 加载插件模块
*/
async loadPluginModule (entry: PluginEntry): Promise<PluginModule | null> {
if (!entry.entryPath) {
entry.runtime = {
status: 'error',
error: 'No entry path specified',
};
return null;
}
try {
const module = await this.importModule(entry.entryPath);
if (!this.isValidPluginModule(module)) {
entry.runtime = {
status: 'error',
error: 'Invalid plugin module: missing plugin_init function',
};
return null;
}
return module;
} catch (error: any) {
entry.runtime = {
status: 'error',
error: error.message || 'Failed to import module',
};
return null;
}
}
/**
* 检查模块是否为有效的插件模块
*/
isValidPluginModule (module: any): module is PluginModule {
return module && typeof module.plugin_init === 'function';
}
/**
* 验证插件入口文件是否包含有效的 plugin_init 函数
* 用于扫描阶段快速验证
*/
async validatePluginEntry (entryPath: string): Promise<{ valid: boolean; error?: string; }> {
try {
const module = await this.importModule(entryPath);
if (this.isValidPluginModule(module)) {
return { valid: true };
}
return { valid: false, error: 'Missing plugin_init function' };
} catch (error: any) {
return { valid: false, error: error.message || 'Failed to import module' };
}
}
/**
* 重新扫描单个插件
*/
rescanPlugin (dirname: string): PluginEntry | null {
const statusConfig = this.loadPluginStatusConfig();
return this.scanDirectoryPlugin(dirname, statusConfig);
}
/**
* 通过 ID 查找插件目录名
*/
findPluginDirById (pluginId: string): string | null {
if (!fs.existsSync(this.pluginPath)) {
return null;
}
const items = fs.readdirSync(this.pluginPath, { withFileTypes: true });
for (const item of items) {
if (!item.isDirectory()) continue;
const packageJsonPath = path.join(this.pluginPath, item.name, 'package.json');
if (fs.existsSync(packageJsonPath)) {
try {
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
if (pkg.name === pluginId) {
return item.name;
}
} catch (_e) { }
}
// 如果目录名就是 ID
if (item.name === pluginId) {
return item.name;
}
}
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);
}
}
}
@@ -0,0 +1,315 @@
import { Router, Request, Response, NextFunction } from 'express';
import path from 'path';
import {
PluginRouterRegistry,
PluginRequestHandler,
PluginApiRouteDefinition,
PluginPageDefinition,
PluginHttpRequest,
PluginHttpResponse,
HttpMethod,
MemoryStaticFile,
} 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;
}
/**
* 插件路由注册器实现
* 为每个插件创建独立的路由注册器,收集路由定义
*/
/** 内存静态路由定义 */
interface MemoryStaticRoute {
urlPath: string;
files: MemoryStaticFile[];
}
export class PluginRouterRegistryImpl implements PluginRouterRegistry {
private apiRoutes: PluginApiRouteDefinition[] = [];
private apiNoAuthRoutes: PluginApiRouteDefinition[] = [];
private pageDefinitions: PluginPageDefinition[] = [];
private staticRoutes: Array<{ urlPath: string; localPath: string; }> = [];
private memoryStaticRoutes: MemoryStaticRoute[] = [];
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);
}
// ==================== 无认证 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 {
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 });
}
staticOnMem (urlPath: string, files: MemoryStaticFile[]): void {
this.memoryStaticRoutes.push({ urlPath, files });
}
// ==================== 构建路由 ====================
/**
* 构建 Express Router(用于 API 路由)
* 注意:静态资源路由不在此处挂载,由 webui-backend 直接在不需要鉴权的路径下处理
*/
buildApiRouter (): Router {
const router = Router();
// 注册 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;
}
/**
* 检查是否有注册的无认证 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;
}
/**
* 检查是否有注册的静态资源路由
*/
hasStaticRoutes (): boolean {
return this.staticRoutes.length > 0 || this.memoryStaticRoutes.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;
}
/**
* 获取所有注册的静态路由
*/
getStaticRoutes (): Array<{ urlPath: string; localPath: string; }> {
return [...this.staticRoutes];
}
/**
* 获取所有注册的内存静态路由
*/
getMemoryStaticRoutes (): MemoryStaticRoute[] {
return [...this.memoryStaticRoutes];
}
/**
* 清空路由(用于插件卸载)
*/
clear (): void {
this.apiRoutes = [];
this.apiNoAuthRoutes = [];
this.pageDefinitions = [];
this.staticRoutes = [];
this.memoryStaticRoutes = [];
}
}
@@ -0,0 +1,395 @@
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';
// ==================== 插件包信息 ====================
export interface PluginPackageJson {
name?: string;
plugin?: string;
version?: string;
main?: string;
description?: string;
author?: string;
homepage?: string;
repository?: string | { type: string; url: string; };
icon?: string; // 插件图标文件路径(相对于插件目录),如 "icon.png"
}
// ==================== 插件配置 Schema ====================
export interface PluginConfigItem {
key: string;
type: 'string' | 'number' | 'boolean' | 'select' | 'multi-select' | 'html' | 'text';
label: string;
description?: string;
default?: unknown;
options?: { label: string; value: string | number; }[];
placeholder?: string;
/** 标记此字段为响应式:值变化时触发 schema 刷新 */
reactive?: boolean;
/** 是否隐藏此字段 */
hidden?: boolean;
}
export type PluginConfigSchema = PluginConfigItem[];
// ==================== NapCatConfig 静态接口 ====================
/** NapCatConfig 类的静态方法接口(用于 typeof NapCatConfig) */
export interface INapCatConfigStatic {
text (key: string, label: string, defaultValue?: string, description?: string, reactive?: boolean): PluginConfigItem;
number (key: string, label: string, defaultValue?: number, description?: string, reactive?: boolean): PluginConfigItem;
boolean (key: string, label: string, defaultValue?: boolean, description?: string, reactive?: boolean): PluginConfigItem;
select (key: string, label: string, options: { label: string; value: string | number; }[], defaultValue?: string | number, description?: string, reactive?: boolean): PluginConfigItem;
multiSelect (key: string, label: string, options: { label: string; value: string | number; }[], defaultValue?: (string | number)[], description?: string, reactive?: boolean): PluginConfigItem;
html (content: string): PluginConfigItem;
plainText (content: string): PluginConfigItem;
combine (...items: PluginConfigItem[]): PluginConfigSchema;
}
/** 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 type MemoryFileGenerator = () => string | Buffer | Promise<string | Buffer>;
/** 内存静态文件定义 */
export interface MemoryStaticFile {
/** 文件路径(相对于 urlPath) */
path: string;
/** 文件内容或生成器 */
content: string | Buffer | MemoryFileGenerator;
/** 可选的 MIME 类型 */
contentType?: string;
}
/** 插件路由注册器 */
export interface PluginRouterRegistry {
// ==================== API 路由注册(需要认证) ====================
/**
* 注册单个 API 路由(需要认证,挂载到 /api/Plugin/ext/{pluginId}/)
* @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;
// ==================== 无认证 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;
// ==================== 页面注册 ====================
/**
* 注册插件页面
* @param page 页面定义
*/
page (page: PluginPageDefinition): void;
/**
* 注册多个插件页面
* @param pages 页面定义数组
*/
pages (pages: PluginPageDefinition[]): void;
// ==================== 静态资源 ====================
/**
* 提供静态文件服务
* @param urlPath URL 路径
* @param localPath 本地文件夹路径(相对于插件目录或绝对路径)
*/
static (urlPath: string, localPath: string): void;
/**
* 提供内存生成的静态文件服务
* @param urlPath URL 路径
* @param files 内存文件列表
*/
staticOnMem (urlPath: string, files: MemoryStaticFile[]): void;
}
// ==================== 插件管理器接口 ====================
/** 插件管理器公共接口 */
export interface IPluginManager {
readonly config: NetworkAdapterConfig;
getPluginPath (): string;
getPluginConfig (): PluginStatusConfig;
getAllPlugins (): PluginEntry[];
getLoadedPlugins (): PluginEntry[];
getPluginInfo (pluginId: string): PluginEntry | undefined;
setPluginStatus (pluginId: string, enable: boolean): Promise<void>;
loadPluginById (pluginId: string): Promise<boolean>;
unregisterPlugin (pluginId: string): Promise<void>;
uninstallPlugin (pluginId: string, cleanData?: boolean): Promise<void>;
reloadPlugin (pluginId: string): Promise<boolean>;
loadDirectoryPlugin (dirname: string): Promise<void>;
getPluginDataPath (pluginId: string): string;
getPluginConfigPath (pluginId: string): string;
}
// ==================== 插件配置 UI 控制器 ====================
/** 插件配置 UI 控制器 - 用于动态控制配置界面 */
export interface PluginConfigUIController {
/** 更新整个 schema */
updateSchema: (schema: PluginConfigSchema) => void;
/** 更新单个字段 */
updateField: (key: string, field: Partial<PluginConfigItem>) => void;
/** 移除字段 */
removeField: (key: string) => void;
/** 添加字段 */
addField: (field: PluginConfigItem, afterKey?: string) => void;
/** 显示字段 */
showField: (key: string) => void;
/** 隐藏字段 */
hideField: (key: string) => void;
/** 获取当前配置值 */
getCurrentConfig: () => Record<string, unknown>;
}
// ==================== 插件日志接口 ====================
/**
* 插件日志接口 - 简化的日志 API
*/
export interface PluginLogger {
/** 普通日志 */
log (...args: unknown[]): void;
/** 调试日志 */
debug (...args: unknown[]): void;
/** 信息日志 */
info (...args: unknown[]): void;
/** 警告日志 */
warn (...args: unknown[]): void;
/** 错误日志 */
error (...args: unknown[]): void;
}
// ==================== 插件上下文 ====================
export interface NapCatPluginContext {
core: NapCatCore;
oneBot: NapCatOneBot11Adapter;
actions: ActionMap;
pluginName: string;
pluginPath: string;
configPath: string;
dataPath: string;
/** NapCatConfig 配置构建器 */
NapCatConfig: NapCatConfigClass;
adapterName: string;
/** 插件管理器实例 */
pluginManager: IPluginManager;
/** 插件日志器 - 自动添加插件名称前缀 */
logger: PluginLogger;
/**
* 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;
}
// ==================== 插件模块接口 ====================
export interface PluginModule<T extends OB11EmitEventContent = OB11EmitEventContent, C = unknown> {
plugin_init: (ctx: NapCatPluginContext) => void | Promise<void>;
plugin_onmessage?: (
ctx: NapCatPluginContext,
event: OB11Message,
) => void | Promise<void>;
plugin_onevent?: (
ctx: NapCatPluginContext,
event: T,
) => void | Promise<void>;
plugin_cleanup?: (
ctx: NapCatPluginContext
) => void | Promise<void>;
plugin_config_schema?: PluginConfigSchema;
plugin_config_ui?: PluginConfigSchema;
plugin_get_config?: (ctx: NapCatPluginContext) => C | Promise<C>;
plugin_set_config?: (ctx: NapCatPluginContext, config: C) => void | Promise<void>;
/**
* 配置界面控制器 - 当配置界面打开时调用
* 返回清理函数,在界面关闭时调用
*/
plugin_config_controller?: (
ctx: NapCatPluginContext,
ui: PluginConfigUIController,
initialConfig: Record<string, unknown>
) => void | (() => void) | Promise<void | (() => void)>;
/**
* 响应式字段变化回调 - 当标记为 reactive 的字段值变化时调用
*/
plugin_on_config_change?: (
ctx: NapCatPluginContext,
ui: PluginConfigUIController,
key: string,
value: unknown,
currentConfig: Record<string, unknown>
) => void | Promise<void>;
}
// ==================== 插件运行时状态 ====================
export type PluginRuntimeStatus = 'loaded' | 'error' | 'unloaded';
export interface PluginRuntime {
/** 运行时状态 */
status: PluginRuntimeStatus;
/** 错误信息(当 status 为 'error' 时) */
error?: string;
/** 插件模块(当 status 为 'loaded' 时) */
module?: PluginModule;
/** 插件上下文(当 status 为 'loaded' 时) */
context?: NapCatPluginContext;
}
// ==================== 插件条目(统一管理所有插件) ====================
export interface PluginEntry {
// ===== 基础信息 =====
/** 插件 ID(包名或目录名) */
id: string;
/** 文件系统目录名 */
fileId: string;
/** 显示名称 */
name?: string;
/** 版本号 */
version?: string;
/** 描述 */
description?: string;
/** 作者 */
author?: string;
/** 插件目录路径 */
pluginPath: string;
/** 入口文件路径 */
entryPath?: string;
/** package.json 内容 */
packageJson?: PluginPackageJson;
// ===== 状态 =====
/** 是否启用(用户配置) */
enable: boolean;
/** 运行时是否已加载 */
loaded: boolean;
// ===== 运行时 =====
/** 运行时信息 */
runtime: PluginRuntime;
}
// ==================== 插件状态配置(持久化) ====================
export interface PluginStatusConfig {
[key: string]: boolean; // key: pluginId, value: enabled
}