cyberbot-core
Version:
cyberbot, 基于napcat-ts, nodejs,轻量qq机器人框架。
1,254 lines (1,253 loc) • 70 kB
JavaScript
import { NCWebsocket, Structs } from "node-napcat-ts";
import { join } from "path";
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync, readdirSync } from "fs";
import { writeFile } from 'fs/promises';
import axios from "axios";
import { createJiti } from "jiti";
import { createHash } from 'crypto';
// @ts-ignore
import * as cron from "node-cron";
// 导入日志模块
import { logger } from "./logger.js";
export { Structs, NCWebsocket, axios as http };
// Config
// 使用单例模式存储配置,避免多次解析
let configCache = null;
export function getConfig() {
// 如果缓存存在,直接返回
if (configCache)
return configCache;
const configPath = join(process.cwd(), "config.json");
if (!existsSync(configPath)) {
throw new Error("配置文件未找到。请在项目根目录创建 config.json 文件。");
}
try {
// 读取并解析配置文件
const rawContent = readFileSync(configPath, "utf-8");
const parsed = JSON.parse(rawContent);
// 验证必要的配置项
if (!parsed.baseUrl) {
throw new Error("配置错误: 缺少 baseUrl 字段");
}
if (!parsed.accessToken) {
throw new Error("配置错误: 缺少 accessToken 字段");
}
// 缓存配置
configCache = parsed;
return configCache;
}
catch (error) {
if (error instanceof SyntaxError) {
throw new Error(`配置文件解析错误: ${error.message}`);
}
throw error;
}
}
// Index
const logo = `
.oooooo. .o8 oooooooooo. .
d8P' \`Y8b "888 \`888' \`Y8b .o8
888 oooo ooo 888oooo. .ooooo. oooo d8b 888 888 .ooooo. .o888oo
888 \`88. .8' d88' \`88b d88' \`88b \`888\"\"8P 888oooo888' d88' \`88b 888
888 \`88..8' 888 888 888ooo888 888 888 \`88b 888 888 888
\`88b ooo \`888' 888 888 888 .o 888 888 .88P 888 888 888 .
\`Y8bood8P' .8' \`Y8bod8P' \`Y8bod8P' d888b o888bood8P' \`Y8bod8P' "888"
.o..P'
\`Y8P'
CyberBot 一个基于 node-napcat-ts 的 QQ 机器人
参考: kivibot@viki && Abot@takayama
@auther: 星火
`;
// 初始化日志系统
export const log = logger;
export class Bot {
constructor() {
// 获取配置,如果失败则抛出错误
try {
this.config = getConfig();
}
catch (error) {
log.error(`[-]配置加载失败: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
// 创建websocket连接
this.bot = new NCWebsocket({
"baseUrl": this.config.baseUrl,
"accessToken": this.config.accessToken,
"reconnection": {
"enable": this.config.reconnection?.enable ?? true,
"attempts": this.config.reconnection?.attempts ?? 10,
"delay": this.config.reconnection?.delay ?? 5000
}
}, this.config.reconnection?.debug ?? false);
this.pluginManager = new PluginManager(this.bot, this.config);
this.plugins = null;
// 初始化错误处理器
ErrorHandler.initialize();
}
async start() {
this.bot.on("socket.open", (ctx) => {
log.info("[*]开始连接: " + this.config.baseUrl);
});
this.bot.on("socket.error", (ctx) => {
log.error("[-]websocket 连接错误: " + ctx.error_type);
});
this.bot.on("socket.close", (ctx) => {
log.error("[-]websocket 连接关闭: " + ctx.code);
});
this.bot.on("meta_event.lifecycle", (ctx) => {
if (ctx.sub_type == "connect") {
log.info(`[+]连接成功: ${this.config.baseUrl}`);
log.info(logo);
}
});
this.bot.on("meta_event.heartbeat", (ctx) => {
log.info(`[*]心跳包♥`);
});
this.bot.on("message", (ctx) => {
log.info("[*]receive message: " + ctx.raw_message);
});
this.bot.on("api.response.failure", (ctx) => {
log.error(`[-]ApiError, status: ${ctx.status}, message: ${ctx.message}`);
});
this.bot.on("api.preSend", (ctx) => {
log.info(`[*]${ctx.action}: ${JSON.stringify(ctx.params)}`);
});
this.plugins = await this.pluginManager.init();
await this.bot.connect();
// 在连接成功并加载插件后向主人发送上线通知
this.sendOnlineNotificationToMasters();
// 设置错误日志内存优化模式
// 如果配置中有debug标志,则不启用内存优化
const debugMode = this.config.reconnection?.debug ?? false;
ErrorHandler.setMemoryOptimizedMode(!debugMode);
}
/**
* 向所有主人发送机器人上线通知
*/
async sendOnlineNotificationToMasters() {
// 等待短暂时间确保连接稳定
await new Promise(resolve => setTimeout(resolve, 1000));
this.config.master.forEach(async (masterId) => {
try {
// 获取插件信息,确保plugins是Map类型
let pluginCount = 0;
let totalPlugins = 0;
if (this.pluginManager) {
const plugins = this.pluginManager.plugins;
pluginCount = Array.from(plugins.values()).filter(info => info.setup && info.setup.enable).length;
// 从plugins目录获取所有可用插件数量
totalPlugins = this.pluginManager.getPluginsFromDir().length;
}
await this.bot.send_msg({
user_id: masterId,
message: [
Structs.text(`[Bot🤖] 已成功上线!\n` +
`📅 ${new Date().toLocaleString()}\n` +
`🧩 插件状态: ${pluginCount}/${totalPlugins} 已启用\n` +
`💻 系统信息: ${process.platform} ${process.arch}\n` +
`🎉 机器人已准备就绪,随时为您服务!`)
]
});
log.info(`[+]已向主人 ${masterId} 发送上线通知`);
}
catch (error) {
log.error(`[-]向主人 ${masterId} 发送上线通知失败: ${error}`);
}
});
}
// 添加停止方法,在系统关闭时调用
async stop() {
// 在这里可以添加其他清理逻辑
log.info('[*]系统已停止,资源已清理');
}
}
// Plugin
export function definePlugin(plugin) {
return plugin;
}
/**
* 错误处理工具类
*/
class ErrorHandler {
/**
* 初始化错误处理器
*/
static initialize() {
if (this.isInitialized)
return;
try {
this.loadErrorLogs();
this.isInitialized = true;
// 启动时清理旧日志
this.cleanOldLogs();
// 设置进程退出时保存日志
process.on('exit', () => {
this.saveErrorLogsSynchronously();
});
// 设置每小时自动保存
setInterval(() => {
this.saveErrorLogs().catch(err => {
console.error('Failed to auto-save error logs:', err);
});
}, 60 * 60 * 1000);
log.info(`[*]错误处理系统已初始化,最大日志数量: ${this.MAX_ERROR_LOGS}`);
}
catch (error) {
console.error('Error initializing ErrorHandler:', error);
}
}
/**
* 启用内存优化模式,减少日志细节
* @param enable 是否启用
*/
static setMemoryOptimizedMode(enable) {
this.memoryOptimizedMode = enable;
log.info(`[*]错误日志内存优化模式已${enable ? '启用' : '禁用'}`);
}
/**
* 格式化错误对象为字符串
*/
static formatError(error) {
if (!error)
return 'Unknown error';
// 在内存优化模式下简化错误信息
if (this.memoryOptimizedMode) {
return error.message || String(error);
}
// 常规模式下,返回更详细的错误信息
try {
if (error instanceof Error) {
return `${error.name}: ${error.message}\n${error.stack || ''}`;
}
if (typeof error === 'string') {
return error;
}
return JSON.stringify(error, null, 2);
}
catch (e) {
return String(error);
}
}
/**
* 记录错误日志
* @param plugin 插件名称
* @param type 错误类型
* @param error 错误对象
*/
static logError(plugin, type, error) {
try {
if (!this.isInitialized) {
this.initialize();
}
// 格式化错误消息
const message = this.formatError(error);
// 在内存优化模式下限制错误消息长度
const limitedMessage = this.memoryOptimizedMode && message.length > 500
? message.substring(0, 500) + '...(truncated)'
: message;
// 添加到错误日志数组
this.errorLogs.unshift({
timestamp: Date.now(),
plugin,
type,
message: limitedMessage,
// 在内存优化模式下不保存代码片段
code: this.memoryOptimizedMode ? undefined : (error.code || undefined)
});
// 保持日志数量在限制范围内
if (this.errorLogs.length > this.MAX_ERROR_LOGS) {
this.errorLogs = this.errorLogs.slice(0, this.MAX_ERROR_LOGS);
}
// 延迟保存以减少I/O操作
this.pendingSave = true;
const now = Date.now();
if (now - this.lastSaveTime > this.SAVE_DELAY) {
this.saveErrorLogs().catch(e => console.error('Failed to save error logs:', e));
this.lastSaveTime = now;
this.pendingSave = false;
}
else if (!this.pendingSave) {
// 设置延迟保存
this.pendingSave = true;
setTimeout(() => {
if (this.pendingSave) {
this.saveErrorLogs().catch(e => console.error('Failed to save error logs:', e));
this.lastSaveTime = Date.now();
this.pendingSave = false;
}
}, this.SAVE_DELAY);
}
// 同时输出错误日志到控制台
log.error(`[${plugin}][${type}] ${message}`);
}
catch (e) {
console.error('Error in logError:', e);
}
}
/**
* 加载错误日志从文件
*/
static loadErrorLogs() {
try {
// 确保日志目录存在
const logDir = join(process.cwd(), "logs");
if (!existsSync(logDir)) {
mkdirSync(logDir, { recursive: true });
}
if (!existsSync(this.ERROR_LOGS_FILE)) {
this.errorLogs = [];
return;
}
const data = readFileSync(this.ERROR_LOGS_FILE, 'utf8');
this.errorLogs = JSON.parse(data);
// 验证并清理损坏的日志
this.errorLogs = this.errorLogs.filter(log => log && typeof log === 'object' &&
typeof log.timestamp === 'number' &&
typeof log.plugin === 'string' &&
typeof log.type === 'string');
log.info(`[*]已加载 ${this.errorLogs.length} 条错误日志记录`);
}
catch (error) {
console.error('Error loading error logs:', error);
this.errorLogs = [];
}
}
/**
* 异步保存错误日志到文件
*/
static async saveErrorLogs() {
try {
// 确保日志目录存在
const logDir = join(process.cwd(), "logs");
if (!existsSync(logDir)) {
mkdirSync(logDir, { recursive: true });
}
const data = JSON.stringify(this.errorLogs);
await writeFile(this.ERROR_LOGS_FILE, data, 'utf8');
}
catch (error) {
console.error('Error saving error logs:', error);
}
}
/**
* 同步保存错误日志到文件(进程退出时使用)
*/
static saveErrorLogsSynchronously() {
try {
// 确保日志目录存在
const logDir = join(process.cwd(), "logs");
if (!existsSync(logDir)) {
mkdirSync(logDir, { recursive: true });
}
const data = JSON.stringify(this.errorLogs);
writeFileSync(this.ERROR_LOGS_FILE, data, 'utf8');
}
catch (error) {
console.error('Error saving error logs synchronously:', error);
}
}
/**
* 获取特定插件的错误日志
*/
static getPluginErrors(pluginName) {
return this.errorLogs.filter(log => log.plugin === pluginName);
}
/**
* 清理旧的错误日志
* @param maxAge 最大保留时间(毫秒)
*/
static cleanOldLogs(maxAge = this.MAX_LOG_AGE_DAYS * 24 * 60 * 60 * 1000) {
try {
const now = Date.now();
const oldSize = this.errorLogs.length;
this.errorLogs = this.errorLogs.filter(log => (now - log.timestamp) < maxAge);
const removedCount = oldSize - this.errorLogs.length;
if (removedCount > 0) {
log.info(`[*]已清理 ${removedCount} 条过期错误日志`);
// 仅当有日志被删除时才保存
this.saveErrorLogs().catch(e => console.error('Failed to save logs after cleaning:', e));
}
}
catch (error) {
console.error('Error cleaning old logs:', error);
}
}
/**
* 立即清空所有错误日志
*/
static clearAllLogs() {
this.errorLogs = [];
this.saveErrorLogs().catch(e => console.error('Failed to save after clearing logs:', e));
log.info('[*]已清空所有错误日志');
}
}
ErrorHandler.MAX_ERROR_LOGS = 50; // 最大错误日志数量
ErrorHandler.ERROR_LOGS_FILE = join(process.cwd(), "logs", "error_logs.json");
ErrorHandler.MAX_LOG_AGE_DAYS = 3; // 默认保留3天的错误日志
ErrorHandler.errorLogs = [];
ErrorHandler.isInitialized = false;
// 提高性能的内存优化标志
ErrorHandler.memoryOptimizedMode = false;
// 最后保存时间,用于延迟写入
ErrorHandler.lastSaveTime = 0;
// 待保存标志
ErrorHandler.pendingSave = false;
// 保存延迟
ErrorHandler.SAVE_DELAY = 5000; // 5秒
export class PluginManager {
constructor(bot, config) {
this.pluginCtxProxies = new Map();
this.sharedMethodWrappers = new Map();
this.pluginErrorHandlers = new Map();
this.cronTaskPool = new Map();
this.plugins = new Map();
this.bot = bot;
this.tempListener = [];
this.tempCronJob = [];
// 初始化定时任务池
this.cronTaskPool = new Map();
this.jiti = createJiti(import.meta.url, { moduleCache: false });
this.ctx = {
config: config,
http: axios,
bot: this.bot,
bot_uin: config.bot,
cron: (cronTasks, func) => {
// 存储定时任务的数组
const cronJobInstances = [];
// 如果是数组格式,表示多个定时任务
if (Array.isArray(cronTasks)) {
for (const [cronExpression, callback] of cronTasks) {
if (!cron.validate(cronExpression)) {
log.error(`[-]无效的 cron 表达式: ${cronExpression}`);
cronJobInstances.push(null); // 占位,保持索引一致
this.tempCronJob.push(false);
continue;
}
// 1. 创建一个轻量级事件对象模板 - 避免在闭包中重复创建
const baseEventTemplate = {
message_type: 'group',
raw_message: '',
message_id: 0,
user_id: 0,
group_id: 0,
sender: { user_id: 0 }
};
// 2. 预先创建reply方法,避免每次调用都创建
const replyMethod = async (message, quote = false) => {
try {
let messageArray = Array.isArray(message) ? message : [message];
const processedMessages = messageArray.map(item => {
if (typeof item === 'string' || typeof item === 'number') {
return Structs.text(item.toString());
}
return item;
});
return await this.bot.send_msg({
user_id: 0, // 默认值,实际发送时不会用到
message: processedMessages
});
}
catch (error) {
log.error(`Failed to send cron message: ${error}`);
return { message_id: 0 };
}
};
// 3. 提取真正需要的ctx属性,而不是捕获整个ctx
// 创建一个最小化的上下文对象,包含常用属性
const minimalCtx = {
bot: this.bot,
config: {
master: [...this.ctx.config.master], // 复制数组,避免引用
bot: this.ctx.config.bot
},
// 添加任务可能需要的其他最小化属性
sendPrivateMessage: this.ctx.sendPrivateMessage,
sendGroupMessage: this.ctx.sendGroupMessage
};
// 4. 创建轻量级回调包装器
const wrappedCallback = () => {
try {
// 每次创建新的事件对象,避免状态共享问题
const eventObj = { ...baseEventTemplate };
// 添加reply方法
eventObj.reply = replyMethod;
// 使用类型断言处理类型兼容问题
return callback(minimalCtx, eventObj);
}
catch (error) {
// 捕获并记录错误,但不中断cron执行
log.error(`[-]Cron任务执行错误: ${error}`);
}
};
// 5. 创建定时任务实例,但初始状态为暂停
const job = cron.schedule(cronExpression, wrappedCallback, {
scheduled: false
});
// 存储到临时数组和结果数组
cronJobInstances.push(job);
this.tempCronJob.push(job);
}
// 返回创建的所有任务实例,便于后续管理
return cronJobInstances;
}
// 原有的字符串格式处理(单个定时任务)
if (!cron.validate(cronTasks)) {
log.error(`[-]无效的 cron 表达式: ${cronTasks}`);
this.tempCronJob.push(false);
return null;
}
// 同样使用最小化上下文创建单个任务
const job = cron.schedule(cronTasks, func, {
scheduled: false
});
this.tempCronJob.push(job);
cronJobInstances.push(job);
return job; // 返回单个任务实例
},
plugin: {
getPlugins: () => {
return this.getPlugins();
},
onPlugin: (pluginName) => {
return this.onPlugin(pluginName);
},
offPlugin: (pluginName) => {
return this.offPlugin(pluginName);
},
reloadPlugin: (pluginName) => {
return this.reloadPlugin(pluginName);
},
getPluginsFromDir: () => {
return this.getPluginsFromDir();
},
loadPlugin: (pluginName) => {
return this.loadPlugin(pluginName);
}
},
handle: (eventName, func) => {
const wrappedFunc = async (e) => {
try {
// 添加reply方法
const extendedEvent = this.ctx.utils.addReplyMethod(e);
// @ts-ignore: 忽略复杂联合类型的错误
return await func(extendedEvent);
}
catch (error) {
// 记录错误但不中断事件处理流程
log.error(`[-]处理${eventName}事件时出错: ${error}`);
// 避免错误影响整个系统
return null;
}
};
const obj = {
event: eventName,
fn: wrappedFunc
};
this.tempListener.push(obj);
},
isMaster: (e) => {
if (typeof e === 'number' && !isNaN(e)) {
return this.ctx.config.master.includes(e);
}
if (typeof e === 'object' && e.sender && typeof e.sender.user_id === 'number') {
return this.ctx.config.master.includes(e.sender.user_id);
}
return false;
},
isAdmin: (e) => {
if (typeof e === 'number' && !isNaN(e)) {
return this.ctx.config.master.includes(e) || this.ctx.config.admins.includes(e);
}
if (typeof e === 'object' && e.sender && typeof e.sender.user_id === 'number') {
const userId = e.sender.user_id;
return this.ctx.config.master.includes(userId) || this.ctx.config.admins.includes(userId);
}
return false;
},
hasRight: (user_id) => {
return this.ctx.isMaster(user_id) || this.ctx.isAdmin(user_id);
},
sendPrivateMessage: async (user_id, message) => {
try {
return await this.bot.send_private_msg({
user_id: user_id,
message: Array.isArray(message) ? message : [Structs.text(String(message))]
});
}
catch (error) {
log.error(`Failed to send message: ${error}`);
return { message_id: 0 };
}
},
sendGroupMessage: async (group_id, message) => {
try {
return await this.bot.send_group_msg({
group_id: group_id,
message: Array.isArray(message) ? message : [Structs.text(String(message))]
});
}
catch (error) {
log.error(`Failed to send message: ${error}`);
return { message_id: 0 };
}
},
delete_msg: async (message_id) => {
try {
await this.bot.delete_msg({ message_id });
}
catch (error) {
log.error(`Failed to delete message: ${error}`);
}
},
kick: async (group_id, user_id, reject_add_request) => {
try {
await this.bot.set_group_kick({
group_id: group_id,
user_id: user_id,
reject_add_request: reject_add_request
});
}
catch (error) {
log.error(`Failed to kick user ${user_id} from group ${group_id}: ${error}`);
}
},
ban: async (group_id, user_id, duration) => {
try {
await this.bot.set_group_ban({
group_id: group_id,
user_id: user_id,
duration: duration
});
}
catch (error) {
log.error(`Failed to ban user ${user_id} in group ${group_id}: ${error}`);
}
},
banAll: async (group_id, enable) => {
try {
await this.bot.set_group_whole_ban({
group_id: group_id,
enable: enable
});
}
catch (error) {
log.error(`Failed to set whole ban for group ${group_id} to ${enable}: ${error}`);
}
},
setGroupName: async (group_id, name) => {
try {
await this.bot.set_group_name({
group_id: group_id,
group_name: name
});
}
catch (error) {
log.error(`Failed to set group name for group ${group_id} to ${name}: ${error}`);
}
},
setAdmin: async (group_id, user_id, enable) => {
try {
await this.bot.set_group_admin({
group_id: group_id,
user_id: user_id,
enable: enable
});
}
catch (error) {
log.error(`Failed to set admin status for user ${user_id} in group ${group_id} to ${enable}: ${error}`);
}
},
setTitle: async (group_id, user_id, title) => {
try {
await this.bot.set_group_special_title({
group_id: group_id,
user_id: user_id,
special_title: title
});
}
catch (error) {
log.error(`Failed to set special title for user ${user_id} in group ${group_id} to ${title}: ${error}`);
}
},
aprroveGroup: async (flag) => {
try {
await this.bot.set_group_add_request({
flag: flag,
approve: true
});
}
catch (error) {
log.error(`Failed to approve group request: ${error}`);
}
},
rejectGroup: async (flag) => {
try {
await this.bot.set_group_add_request({
flag: flag,
approve: false
});
}
catch (error) {
log.error(`Failed to reject group request: ${error}`);
}
},
isGroupAdmin: async (group_id, user_id) => {
try {
const memberInfo = await this.bot.get_group_member_info({ group_id, user_id });
return memberInfo.role === 'admin' || memberInfo.role === 'owner';
}
catch (error) {
log.error(`Failed to check if user ${user_id} is an admin in group ${group_id}: ${error}`);
return false;
}
},
isGroupOwner: async (group_id, user_id) => {
try {
const memberInfo = await this.bot.get_group_member_info({ group_id, user_id });
return memberInfo.role === 'owner';
}
catch (error) {
log.error(`Failed to check if user ${user_id} is an owner in group ${group_id}: ${error}`);
return false;
}
},
md5: (text) => {
const hash = createHash('md5');
hash.update(text);
return hash.digest('hex');
},
randomInt: (min, max) => {
return Math.floor(Math.random() * (max - min + 1)) + min;
},
randomItem: (array) => {
return array[Math.floor(Math.random() * array.length)];
},
getGroupAvatarLink: (group_id, size) => {
return `https://p.qlogo.cn/gh/${group_id}/${group_id}/${size || 40}`;
},
getQQAvatarLink: (user_id, size) => {
return `https://q2.qlogo.cn/headimg_dl?dst_uin=${user_id}&spec=${size || 40}`;
},
getImageLink: (e) => {
try {
if (!Array.isArray(e.message))
return "";
const imageItem = e.message.find(item => item.type === "image");
return imageItem?.data?.url.trim() || "";
}
catch (error) {
log.error('提取图片链接时发生错误:', error);
return "";
}
},
getDirectLink: async (url) => {
try {
const rKey = await this.bot.nc_get_rkey();
if (!rKey) {
log.error('获取 rkey 失败,无法替换');
return "";
}
// 从URL中提取appid
const appidMatch = url.match(/appid=(\d+)/);
const appid = appidMatch ? appidMatch[1] : null;
// 根据appid选择rkey
let current_rkey;
if (appid === '1406') {
current_rkey = rKey[0]?.rkey;
}
else if (appid === '1407') {
current_rkey = rKey[1]?.rkey;
}
else {
log.error('未知的appid或无法从URL中提取appid');
return "";
}
// 使用正则表达式提取 &rkey= 之前的内容
const regex = /^(.*?)&rkey=/;
const baseUrl = url.match(regex)?.[1];
// 如果匹配到内容,拼接 rKey,否则返回空字符串
return baseUrl ? `${baseUrl}${current_rkey}` : "";
}
catch (error) {
log.error('获取直链失败:', error);
return "";
}
},
getReplyMessageId: (e) => {
try {
if (!Array.isArray(e.message))
return "";
const replyObj = e.message.find(item => item.type === "reply");
return replyObj?.data?.id.trim() || ""; // 转为 number 或 null
}
catch (error) {
log.error('提取消息ID时发生错误:', error);
return "";
}
},
getMessageAt: (e) => {
try {
if (!Array.isArray(e.message))
return [];
return e.message
.filter(item => item.type === "at") // 筛选所有 type 为 "at" 的项
.map(item => Number(item.data?.qq)) // 提取 qq 字段
.filter(qq => !isNaN(qq)); // 过滤掉 undefined
}
catch (error) {
log.error('提取消息ID时发生错误:', error);
return [];
}
},
getText: (e) => {
try {
if (!Array.isArray(e.message))
return "";
const textObj = e.message.find(item => item.type === "text");
return textObj?.data?.text.trim() || ""; // 返回 "%%" 或其他文本
}
catch (error) {
log.error('提取纯文本内容时发生错误:', error);
return "";
}
},
getQuotedText: async (e) => {
try {
const message_id = this.ctx.getReplyMessageId(e);
if (!message_id)
return ""; // 提前返回无效情况
const { raw_message } = await this.bot.get_msg({
message_id: Number(message_id)
});
return raw_message || ""; // 确保总是返回字符串
}
catch (error) {
logger.error('提取被引用的文本时发生错误:', error);
return "";
}
},
fakeMessage: async (target_id, message, isGroup) => {
try {
// 调用 send_group_forward_msg 函数
/**@ =message例子=
* message: [
* {
* type: 'node',
* data: {
* content: [
* Structs.text(message) // 消息内容,使用 Structs.text 生成文本消息
* ]
* }
* }
* ]
**/
// 动态构建参数对象
const params = isGroup
? { group_id: target_id, message: message } // 群聊消息
: { user_id: target_id, message: message }; // 私聊消息
// 调用转发消息函数
return await this.bot.send_forward_msg(params);
}
catch (error) {
log.error(`Failed to send fake message to target ${target_id}: ${error}`);
throw error;
}
},
/** 工具函数 */
utils: {
addReplyMethod: (e) => {
// 如果已经有reply方法,直接返回
if (e.reply)
return e;
// 提取消息类型和ID,避免闭包持有整个事件对象
const messageType = e.message_type || 'private';
const messageId = e.message_id;
const userId = e.user_id;
const groupId = e.group_id;
// 添加reply方法,尽量减少引用
e.reply = async (message, quote = false) => {
// 处理消息内容,统一转为数组格式
let messageArray = Array.isArray(message) ? message : [message];
// 转换文本和数字为消息段
const processedMessages = messageArray.map(item => {
if (typeof item === 'string' || typeof item === 'number') {
return Structs.text(item.toString());
}
return item;
});
// 添加回复消息段(如果需要引用)
if (quote && messageId) {
processedMessages.unshift(Structs.reply(messageId));
}
// 根据消息类型确定发送参数
const sendParams = (() => {
if (messageType === 'group' || groupId) {
return { group_id: groupId };
}
else {
return { user_id: userId };
}
})();
// 发送消息并返回结果
try {
const response = await this.bot.send_msg({
...sendParams,
message: processedMessages
});
return { message_id: response.message_id };
}
catch (error) {
log.error(`Failed to send message: ${error}`);
return { message_id: 0 };
}
};
// 添加kick方法,方便移除群成员
if (messageType === 'group' && groupId) {
e.kick = async (kickUserId, reject_add_request) => {
try {
await this.bot.set_group_kick({
group_id: groupId,
user_id: kickUserId,
reject_add_request
});
}
catch (error) {
log.error(`Failed to kick user ${kickUserId}: ${error}`);
}
};
}
return e;
}
}
};
}
// 创建插件上下文代理的方法
createPluginContextProxy(pluginName) {
// 如果已经存在此插件的代理,直接返回
if (this.pluginCtxProxies.has(pluginName)) {
return this.pluginCtxProxies.get(pluginName);
}
// 确保此插件有错误处理函数缓存
if (!this.pluginErrorHandlers.has(pluginName)) {
this.pluginErrorHandlers.set(pluginName, new Map());
}
const pluginErrorCache = this.pluginErrorHandlers.get(pluginName);
// 为特定插件创建的状态,可以单独维护
const pluginState = {
name: pluginName,
// 这里可以添加插件特定的状态
};
// 创建轻量级代理对象 - 直接代理原始ctx
const pluginCtxProxy = new Proxy(this.ctx, {
get: (target, prop, receiver) => {
const value = Reflect.get(target, prop, receiver);
// 处理函数类型的属性
if (typeof value === 'function') {
const propKey = String(prop);
// 1. 首先尝试获取插件特定的错误处理包装器
if (pluginErrorCache.has(propKey)) {
return pluginErrorCache.get(propKey);
}
// 2. 然后尝试从共享函数缓存获取
if (this.sharedMethodWrappers.has(propKey)) {
// 获取通用的函数包装
const sharedWrapper = this.sharedMethodWrappers.get(propKey);
// 创建插件特定的错误处理包装 - 复用通用逻辑但添加插件特定的错误处理
const errorHandler = (...args) => {
try {
return sharedWrapper.apply(target, args);
}
catch (error) {
// 添加插件特定的错误处理
ErrorHandler.logError(pluginName, `ctx_method_${propKey}`, error);
log.warn(`[!]插件${pluginName}调用${propKey}方法出错: ${ErrorHandler.formatError(error)}`);
throw error;
}
};
// 缓存此插件特定的错误处理包装
pluginErrorCache.set(propKey, errorHandler);
return errorHandler;
}
// 3. 如果缓存中不存在,创建并存储通用函数包装
const genericWrapper = function (...args) {
return value.apply(target, args);
};
// 存储到共享函数缓存
this.sharedMethodWrappers.set(propKey, genericWrapper);
// 创建并缓存插件特定的错误处理包装
const errorHandler = (...args) => {
try {
return genericWrapper.apply(target, args);
}
catch (error) {
// 添加插件特定的错误处理
ErrorHandler.logError(pluginName, `ctx_method_${propKey}`, error);
log.warn(`[!]插件${pluginName}调用${propKey}方法出错: ${ErrorHandler.formatError(error)}`);
throw error;
}
};
// 缓存此插件特定的错误处理包装
pluginErrorCache.set(propKey, errorHandler);
return errorHandler;
}
// 处理需要隔离的属性
if (prop === 'plugin') {
// 确保plugin工具方法在调用时能正确获取当前插件名
return {
...value,
// 重写可能需要特殊处理的方法
reloadPlugin: (name) => {
// 默认重载自己
if (!name || name === '') {
return this.reloadPlugin(pluginName);
}
return value.reloadPlugin(name);
}
};
}
return value;
}
});
// 保存到代理缓存中
this.pluginCtxProxies.set(pluginName, pluginCtxProxy);
return pluginCtxProxy;
}
async init() {
// 移除对initSharedContext的调用
// this.initSharedContext();
// 之前的方法是获取所有插件目录中的插件
//const pluginList = this.getPluginsFromDir();
// 修改为只获取配置文件中指定的系统和用户插件
const configSystemPlugins = this.ctx.config.plugins.system || [];
const configUserPlugins = this.ctx.config.plugins.user || [];
// 合并系统插件和用户插件
const pluginList = [...configSystemPlugins, ...configUserPlugins];
// 输出加载的插件
log.info(`[+]正在加载配置中的插件: ${pluginList.join(', ') || '无'}`);
let success = 0, fail = 0;
for (const p of pluginList) {
try {
const result = await this.loadPlugin(p);
if (result) {
success++;
}
else {
log.error(`[-]插件${p}加载失败`);
fail++;
}
}
catch (err) {
log.error(`[-]插件${p}导入失败: ${err}`);
fail++;
}
}
log.info(`[+]插件加载完毕, 一共导入${success + fail}个插件, 成功: ${success}, 失败: ${fail}`);
// 显示启用插件数量比例(相对于所有可用插件)
const enabledCount = Array.from(this.plugins.values()).filter(info => info.setup.enable).length;
const totalAvailablePlugins = this.getPluginsFromDir().length;
log.info(`[+]已启用插件: ${enabledCount}/${totalAvailablePlugins} (已加载/可用)`);
return this.plugins;
}
getPluginsFromDir() {
const pluginsPath = join(process.cwd(), "plugins");
const plugins = [];
// 读取所有文件和目录
if (existsSync(pluginsPath)) {
const allFiles = readdirSync(pluginsPath);
// 处理所有文件和目录
for (const item of allFiles) {
const fullPath = join(pluginsPath, item);
const stats = statSync(fullPath);
if (stats.isDirectory()) {
// 如果是目录,检查是否有index.ts或index.js
const hasTsIndex = existsSync(join(fullPath, "index.ts"));
const hasJsIndex = existsSync(join(fullPath, "index.js"));
if (hasTsIndex || hasJsIndex) {
plugins.push(item);
}
}
else if (stats.isFile()) {
// 如果是文件,检查是否是.ts或.js文件
if (item.endsWith('.ts') || item.endsWith('.js')) {
// 去掉文件扩展名
const pluginName = item.replace(/\.(ts|js)$/, '');
plugins.push(pluginName);
}
}
}
}
return plugins;
}
async loadPlugin(pluginName) {
try {
log.info(`[*]正在加载插件 ${pluginName}...`);
// 使用绝对路径替代相对路径
const pluginDir = join(process.cwd(), "plugins");
// 优先检查JS文件(编译后的文件)再检查TS文件
// 检查子目录中的插件文件
const subDirJsPath = join(pluginDir, pluginName, "index.js");
const subDirTsPath = join(pluginDir, pluginName, "index.ts");
// 检查直接的插件文件
const directJsPath = join(pluginDir, `${pluginName}.js`);
const directTsPath = join(pluginDir, `${pluginName}.ts`);
// 首先检查子目录js,然后子目录ts,然后直接js,最后直接ts
let pluginPath = '';
if (existsSync(subDirJsPath)) {
pluginPath = subDirJsPath;
}
else if (existsSync(subDirTsPath)) {
pluginPath = subDirTsPath;
}
else if (existsSync(directJsPath)) {
pluginPath = directJsPath;
}
else if (existsSync(directTsPath)) {
pluginPath = directTsPath;
}
else {
log.error(`[-]插件${pluginName}不存在`);
return `[-]插件${pluginName}不存在`;
}
// 尝试加载插件
try {
// 清除之前的模块缓存
this.cleanPluginModuleCache(pluginName);
// 动态导入插件
const plugin = await this.jiti(pluginPath);
// 检查插件结构
if (!plugin || !plugin.default || !plugin.default.name) {
log.error(`[-]插件${pluginName}格式错误,缺少必要字段`);
return `[-]插件${pluginName}格式错误,缺少必要字段`;
}
// 创建此插件的上下文代理
const pluginCtx = this.createPluginContextProxy(pluginName);
// 安全地执行插件初始化
try {
this.tempListener = [];
this.tempCronJob = [];
// 使用插件特定的上下文代理
await Promise.resolve(plugin.default.setup(pluginCtx));
// 设置插件信息
const pluginType = this.ctx.config.plugins.system.includes(pluginName) ? 'system' : 'user';
this.plugins.set(pluginName, {
version: plugin.default.version || "0.1.0",
description: plugin.default.description || "",
type: pluginType,
setup: {
enable: false,
listeners: [...this.tempListener], // 创建新数组,避免引用
cron: [...this.tempCronJob]
}
});
// 存储插件的定时任务到任务池中,便于后续管理
if (this.tempCronJob.length > 0) {
// 过滤掉无效的任务(null或false值)
const validJobs = this.tempCronJob.filter(job => job && typeof job === 'object');
if (validJobs.length > 0) {
this.cronTaskPool.set(pluginName, validJobs);
log.debug(`[*]已存储插件 ${pluginName} 的 ${validJobs.length} 个定时任务`);
}
}
// 检查是否需要自动启用
const enabledPlugins = pluginType === 'system' ?
this.ctx.config.plugins.system :
this.ctx.config.plugins.user;
if (enabledPlugins.includes(pluginName)) {
log.info(`[*]插件${pluginName}在配置中已启用,正在激活...`);
// 使用 onPlugin 方法来确保正确启用
const result = this.onPlugin(pluginName);
if (result.startsWith('[-]')) {
log.error(`[-]插件${pluginName}自动启用失败: ${result}`);
return false;
}
}
return true;
}
catch (error) {
log.error(`[-]插件${pluginName}初始化失败: ${error}`);
// 清理已注册的临时资源
this.tempListener = [];
this.tempCronJob = [];
return false;
}
}
catch (error) {
ErrorHandler.logError(pluginName, 'plugin_load', error);
log.error(`[-]加载插件${pluginName}失败: ${ErrorHandler.formatError(error)}`);
return `[-]加载插件${pluginName}失败: ${ErrorHandler.formatError(error)}`;
}
}
catch (error) {
ErrorHandler.logError(pluginName, 'plugin_load_outer', error);
log.error(`[-]加载插件${pluginName}外部错误: ${ErrorHandler.formatError(error)}`);
return `[-]加载插件${pluginName}外部错误: ${ErrorHandler.formatError(error)}`;
}
}
getPlugins() {
// 获取实际文件系统中的插件列表
const actualPlugins = this.getPluginsFromDir();
// 清理不存在的插件
for (const [pluginName] of this.plugins) {
if (!actualPlugins.includes(pluginName)) {
this.plugins.delete(pluginName);
// 从配置文件中移除该插件
this.saveConfig(pluginName, false);
}
}
return this.plugins;
}
/**
* 保存配置到文件
* @param pluginName 插件名称
* @param isEnabled 是否启用
* @private
*/
saveConfig(pluginName, isEnabled) {
try {
const configPath = join(process.cwd(), "config.json");
// 读取完整配置文件
const configContent = readFileSync(configPath, "utf-8");
// 使用显式类型注解
const fullConfig = JSON.parse(configContent);
// 确保plugins部分存在
if (!fullConfig.plugins) {
fullConfig.plugins = { system: [], user: [] };
}
// 判断是系统插件还是用户插件
const pluginInfo = this.plugins.get(pluginName);
const pluginType = pluginInfo?.type || 'user';
// 确保对应数组存在
if (!fullConfig.plugins[pluginType]) {
fullConfig.plugins[pluginType] = [];
}
const targetArray = fullConfig.plugins[pluginType];
// 添加或移除插件名
if (isEnabled && !targetArray.includes(pluginName)) {
targetArray.push(pluginName);
}
else if (!isEnabled) {
const index = targetArray.indexOf(pluginName);
if (index > -1) {
targetArray.splice(index, 1);
}
}
// 保存回文件,使用同步方法避免并发问题
writeFileSync(configPath, JSON.stringify(fullConfig, null, 2));
log.info(`[+]配置文件已更新: ${pluginName} ${isEnabled ? '已启用' : '已禁用'}`);
}
catch (error) { // 添加类型注解
log.error(`[-]保存配置文件失败: ${error}`);
// 通知出现错误,而不是默默失败
throw new Error(`保存配置文件失败: ${error.message || String(error)}`);
}
}
offPlugin(pluginName) {
const map = this.plugins.get(pluginName);
if (!this.plugins.has(pluginName)) {
return "[-]该插件不存在";
}
// 如果插件已经是禁用状态,则直接返回
if (map?.setup && map.setup.enable === false) {
log.debug(`[*]插件${pl