Plugins.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. import botlogger from "./logger.js";
  2. import { promises as fsPromises } from 'fs';
  3. import { HtmlImg } from "./Puppeteer.js";
  4. import type { GroupMessage, EventHandleMap as MessageContext, PrivateFriendMessage, PrivateGroupMessage } from 'node-napcat-ts/dist/Interfaces.js';
  5. import * as cron from 'node-cron';
  6. import 'reflect-metadata';
  7. import {
  8. ParamType,
  9. commandList,
  10. Command,
  11. Plugin,
  12. } from './decorators.js';
  13. import * as fs from 'fs'
  14. import * as path from 'path'
  15. // 获取指令前缀
  16. import { Botconfig as config, PermissionConfig } from './config.js'
  17. import { ImageSegment, ReplySegment, TextSegment } from "node-napcat-ts/dist/Structs.js";
  18. import { fileURLToPath } from 'node:url';
  19. import { qqBot } from "../app.js";
  20. import { count } from "node:console";
  21. import { IsPermission } from "./Permission.js";
  22. import { download } from "./download.js";
  23. //WSSendParam
  24. const CMD_PREFIX = config?.cmd?.prefix ?? '#';
  25. // 导出装饰器
  26. // export { param, ParamType };
  27. // export const plugins = pluginsDecorator;
  28. export { config }; // 导出配置对象
  29. // 创建消息工厂函数
  30. function createTextMessage(text: string): TextSegment {
  31. return {
  32. type: "text",
  33. data: {
  34. text: text,
  35. }
  36. };
  37. }
  38. function createImageMessage(base64Data: string): ImageSegment {
  39. return {
  40. type: "image",
  41. data: {
  42. file: `base64://${base64Data}`,
  43. }
  44. };
  45. }
  46. function createReplyMessage(messageId: number | string): ReplySegment {
  47. return {
  48. type: "reply",
  49. data: {
  50. id: String(messageId)
  51. }
  52. };
  53. }
  54. // 修改插件查找函数
  55. function findPlugin(pluginId: string): Plugin | undefined {
  56. return commandList.find((p: Plugin) => p.id === pluginId);
  57. }
  58. // 修改命令查找函数
  59. function findCommand(plugin: Plugin, cmdName: string): Command | undefined {
  60. return plugin.commands.find((cmd: Command) => {
  61. // 从完整命令中提取命令名
  62. const cmdParts = cmd.cmd.split(/\s+/);
  63. const matchCmd = cmdParts[cmdParts.length - 1] === cmdName;
  64. // 检查别名
  65. const matchAlias = cmd.aliases?.some((alias: string) => {
  66. const aliasParts = alias.split(/\s+/);
  67. return aliasParts[aliasParts.length - 1] === cmdName;
  68. });
  69. return matchCmd || matchAlias;
  70. });
  71. }
  72. // 添加插件加载函数
  73. async function loadPlugins(): Promise<void> {
  74. try {
  75. const __dirname = path.dirname(fileURLToPath(import.meta.url));
  76. const pluginsDir = path.join(__dirname, '..', 'plugins');
  77. // 删除require缓存相关代码
  78. const files = await fsPromises.readdir(pluginsDir);
  79. for (const file of files) {
  80. if (file.endsWith('.ts') && file !== 'index.ts') {
  81. const filePath = path.join(pluginsDir, file);
  82. try {
  83. // 使用ESM动态导入并添加时间戳防止缓存
  84. const module = await import(`${filePath}?t=${Date.now()}`);
  85. const pluginClasses = Object.values(module).filter(
  86. value => typeof value === 'function' && value.prototype?.plugincfg
  87. );
  88. for (const PluginClass of pluginClasses) {
  89. const instance = new (PluginClass as any)();
  90. const pluginConfig = instance.constructor.prototype.plugincfg;
  91. if (pluginConfig) {
  92. const plugin: Plugin = {
  93. id: pluginConfig.id,
  94. name: pluginConfig.name,
  95. commands: [] as Command[],
  96. class: instance.constructor,
  97. version: pluginConfig.version,
  98. author: pluginConfig.author,
  99. describe: pluginConfig.describe
  100. };
  101. // 触发装饰器
  102. await initializePluginCommands(instance);
  103. // 初始化定时任务
  104. await initializeScheduledTasks(instance);
  105. }
  106. }
  107. } catch (error) {
  108. botlogger.error(`加载插件文件失败 ${file}:`, error);
  109. }
  110. }
  111. }
  112. } catch (error) {
  113. botlogger.error("加载插件目录失败:", error);
  114. }
  115. }
  116. // 初始化插件命令
  117. async function initializePluginCommands(instance: any): Promise<void> {
  118. const methods = Object.getOwnPropertyNames(instance.constructor.prototype)
  119. .filter(name => name !== 'constructor');
  120. for (const methodName of methods) {
  121. const method = instance.constructor.prototype[methodName];
  122. try {
  123. if (typeof method === 'function') {
  124. method.call(instance, '', '');
  125. }
  126. } catch (error) {
  127. if (error instanceof TypeError && error.message.includes('Cannot read properties of undefined')) {
  128. continue;
  129. }
  130. botlogger.error(`触发装饰器时出错: ${error instanceof Error ? error.message : '未知错误'}`);
  131. }
  132. }
  133. }
  134. // 初始化定时任务
  135. async function initializeScheduledTasks(instance: any): Promise<void> {
  136. const methods = Object.getOwnPropertyNames(instance.constructor.prototype)
  137. .filter(name => name !== 'constructor');
  138. for (const methodName of methods) {
  139. const method = instance.constructor.prototype[methodName];
  140. if (method.isScheduled) {
  141. try {
  142. // 创建定时任务
  143. cron.schedule(method.cron, async () => {
  144. try {
  145. await method.call(instance);
  146. } catch (error) {
  147. botlogger.error(`执行定时任务失败 [${methodName}]:`, error);
  148. }
  149. });
  150. botlogger.info(`注册定时任务 [${methodName}]: ${method.cron}`);
  151. } catch (error) {
  152. botlogger.error(`注册定时任务失败 [${methodName}]:`, error);
  153. }
  154. }
  155. }
  156. }
  157. // 修改 runplugins 函数
  158. export async function runplugins() {
  159. try {
  160. // 清理旧实例
  161. commandList.forEach(plugin => {
  162. plugin.commands.forEach(cmd => {
  163. // 检查 cmd.fn 是否存在,并且是否有 close 方法
  164. if (cmd.fn && typeof (cmd.fn as any).close === 'function') {
  165. (cmd.fn as any).close();
  166. }
  167. });
  168. });
  169. // 清空现有命令列表
  170. commandList.length = 0;
  171. // 注册插件
  172. botlogger.info("开始注册插件...");
  173. // 自动加载插件
  174. await loadPlugins();
  175. // 设置消息处理器
  176. qqBot.on('message', async (context) => {
  177. try {
  178. // 检查消息类型和内容
  179. if (context.message[0].type === "file") {
  180. const file = context.message[0].data;
  181. botlogger.info("收到文件消息:" + JSON.stringify(file));
  182. if (file.file.includes(".ts")) {
  183. let isAdmin = false
  184. // 使用 some 方法简化循环逻辑,只要数组中有一个元素满足条件就返回 true
  185. isAdmin = PermissionConfig.admins.some((admin: string) => admin === String(context.sender.user_id));
  186. if (!isAdmin) {
  187. context.quick_action([{
  188. type: 'text',
  189. data: { text: `无权限,无法加载插件` }
  190. }]);
  191. return;
  192. }
  193. const url = (file as any).url; // 文件URL
  194. await download(url, `../plugins/${file.file}`);
  195. botlogger.info("下载完成:" + JSON.stringify(file));
  196. context.quick_action([{
  197. type: 'text',
  198. data: { text: `插件下载完成,开始重载` }
  199. }]);
  200. }
  201. return;
  202. }
  203. if (context.message[0].type !== 'text') {
  204. return;
  205. }
  206. const msg = context.message[0].data.text || '';
  207. botlogger.info('收到消息:' + context.message[0].data.text);
  208. // 检查是否是命令
  209. if (!msg.startsWith(CMD_PREFIX)) {
  210. return;
  211. }
  212. // 解析命令
  213. const parts = msg.slice(CMD_PREFIX.length).trim().split(/\s+/);
  214. const pluginId = parts[0];
  215. const cmdName = parts[1];
  216. const args = parts.slice(2);
  217. botlogger.info('尝试匹配插件:', pluginId);
  218. // 显示可用插件
  219. botlogger.info('可用插件:');
  220. commandList.forEach(p => {
  221. botlogger.info(` [${p.id}]: ${p.name}`);
  222. });
  223. // 查找插件
  224. const plugin = findPlugin(pluginId);
  225. if (!plugin) {
  226. botlogger.info(`插件未找到: ${pluginId}`);
  227. return;
  228. }
  229. botlogger.info(`找到插件[${plugin.id}]: ${plugin.name}`);
  230. // 显示可用命令
  231. botlogger.info('可用命令:');
  232. plugin.commands.forEach(cmd => {
  233. botlogger.info(`${CMD_PREFIX}${plugin.id} ${cmd.cmd}`);
  234. });
  235. // 查找命令
  236. const command = findCommand(plugin, cmdName);
  237. if (!command) {
  238. botlogger.info(`命令未找到: ${cmdName}`);
  239. return;
  240. }
  241. botlogger.info(`找到命令: ${CMD_PREFIX}${plugin.id} ${command.cmd}`);
  242. //指令权限检查
  243. if (context.message_type === 'private') {
  244. if (!await IsPermission(context.user_id, plugin.id, command.cmd)) {
  245. botlogger.info(`[${context.user_id}]无权限执行命令: ${CMD_PREFIX}${plugin.id} ${command.cmd}`);
  246. context.quick_action([{
  247. type: 'text',
  248. data: { text: `你没有权限执行此命令` }
  249. }]);
  250. return;
  251. }
  252. }
  253. if (context.message_type === 'group') {
  254. if (!await IsPermission(context.group_id, plugin.id, command.cmd)) {
  255. botlogger.info(`[${context.group_id}]无权限执行命令: ${CMD_PREFIX}${plugin.id} ${command.cmd}`);
  256. context.quick_action([{
  257. type: 'text',
  258. data: { text: `你没有权限执行此命令` }
  259. }])
  260. return;
  261. }
  262. }
  263. // 执行命令
  264. await handleCommand(context, plugin, command, args);
  265. } catch (error) {
  266. botlogger.error("处理消息时出错:", error);
  267. await context.quick_action([{
  268. type: 'text',
  269. data: { text: `处理消息时出错: ${error instanceof Error ? error.message : '未知错误'}` }
  270. }]);
  271. }
  272. });
  273. botlogger.info("插件注册完成");
  274. botlogger.info("命令表:");
  275. for (const plugin of commandList) {
  276. botlogger.info(`[${plugin.id}]:`);
  277. for (const cmd of plugin.commands) {
  278. botlogger.info(` ${CMD_PREFIX}${plugin.id} ${cmd.cmd}`);
  279. }
  280. }
  281. } catch (error) {
  282. botlogger.error("注册插件时出错:", error);
  283. }
  284. }
  285. // 修改 handleCommand 函数
  286. async function handleCommand(context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage, plugin: Plugin, command: Command, args: string[]): Promise<void> {
  287. try {
  288. // 解析参数 - 传入完整消息文本
  289. if (!context.message[0].type || context.message[0].type !== 'text') {
  290. throw new Error('消息内容为空');
  291. }
  292. const message = context.message[0].data.text || '';
  293. const parsedArgs = await parseCommandParams(message, context);
  294. botlogger.info('命令参数解析完成:' + JSON.stringify({
  295. command: command.cmd,
  296. args: parsedArgs.slice(0, -1) // 不显示 context 对象
  297. }));
  298. // 执行命令
  299. const pluginInstance = new (command.class)();
  300. const result = await command.fn.apply(pluginInstance, parsedArgs);
  301. // 检查是否是群消息
  302. const isGroupMessage = context.message_type === 'group';
  303. const baseMessage = isGroupMessage && context.message_id
  304. ? [createReplyMessage(context.message_id)]
  305. : [];
  306. // 检查是否有模板配置
  307. if (result?.template?.enabled) {
  308. try {
  309. const templatePath = result.template.path;
  310. if (!templatePath || !fs.existsSync(templatePath)) {
  311. throw new Error(`Template not found: ${templatePath}`);
  312. }
  313. // 生成图片
  314. const htmlImg = new HtmlImg();
  315. try {
  316. const img = await htmlImg.render({
  317. template: templatePath,
  318. data: result,
  319. width: result.template.render?.width || 800,
  320. height: result.template.render?.height || 600,
  321. type: result.template.render?.type || 'png',
  322. quality: result.template.render?.quality || 100,
  323. fullPage: result.template.render?.fullPage || false,
  324. background: result.template.render?.background || true
  325. });
  326. // 发送图片
  327. const base64Data = Buffer.from(img).toString('base64');
  328. const imageMessage = createImageMessage(base64Data);
  329. const message = [...baseMessage, imageMessage];
  330. if (isGroupMessage && context.group_id) {
  331. await qqBot.send_group_msg({
  332. group_id: Number(context.group_id),
  333. message: message as any[]
  334. });
  335. } else {
  336. await qqBot.send_private_msg({
  337. user_id: Number(context.user_id),
  338. message: message as any[]
  339. });
  340. }
  341. // 如果配置了同时发送文字
  342. if (result.template.sendText) {
  343. const text = result?.toString?.() || String(result);
  344. const textMessage = createTextMessage(text);
  345. const textOnlyMessage = [...baseMessage, textMessage];
  346. if (isGroupMessage && context.group_id) {
  347. await qqBot.send_group_msg({
  348. group_id: Number(context.group_id),
  349. message: textOnlyMessage as any[]
  350. });
  351. } else {
  352. await qqBot.send_private_msg({
  353. user_id: Number(context.user_id),
  354. message: textOnlyMessage as any[]
  355. });
  356. }
  357. }
  358. } finally {
  359. await htmlImg.close();
  360. }
  361. } catch (error) {
  362. botlogger.error('图片生成失败:', error);
  363. // 如果图片生成失败,发送文本
  364. const text = result?.toString?.() || String(result);
  365. const textMessage = createTextMessage(text);
  366. const message = [...baseMessage, textMessage];
  367. if (isGroupMessage && context.group_id) {
  368. await qqBot.send_group_msg({
  369. group_id: Number(context.group_id),
  370. message: message as any[]
  371. });
  372. } else {
  373. await qqBot.send_private_msg({
  374. user_id: Number(context.user_id),
  375. message: message as any[]
  376. });
  377. }
  378. }
  379. } else if (result?.picture?.enabled) {
  380. const messages = [createImageMessage(result.picture.base64)];
  381. if (typeof result.picture.supplement == "string") {
  382. messages.push(createTextMessage(result.picture.supplement));
  383. }
  384. if (isGroupMessage && context.group_id) {
  385. await qqBot.send_group_msg({
  386. group_id: Number(context.group_id),
  387. message: messages
  388. });
  389. } else {
  390. await qqBot.send_private_msg({
  391. user_id: Number(context.user_id),
  392. message: messages
  393. });
  394. }
  395. } else {
  396. // 发送普通文本响应
  397. const message = [...baseMessage, createTextMessage(result)];
  398. if (isGroupMessage && context.group_id) {
  399. await qqBot.send_group_msg({
  400. group_id: Number(context.group_id),
  401. message: message
  402. });
  403. } else {
  404. await qqBot.send_private_msg({
  405. user_id: Number(context.user_id),
  406. message: message
  407. });
  408. }
  409. }
  410. } catch (error: unknown) {
  411. botlogger.error('执行命令出错:', error);
  412. const errorMessage = error instanceof Error ? error.message : '未知错误';
  413. await context.quick_action([{
  414. type: 'text',
  415. data: { text: `执行命令时出错: ${errorMessage}` }
  416. }]);
  417. }
  418. }
  419. // 导出加载插件函数
  420. export async function loadplugins() {
  421. await runplugins();
  422. }
  423. export const getCommands = () => commandList;
  424. // 修改 runcod 装饰器
  425. export function runcod(cmd: string | string[], desc: string): MethodDecorator {
  426. return function decorator(target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor): PropertyDescriptor {
  427. // 获取插件配置
  428. const pluginConfig = target.constructor.prototype.plugincfg;
  429. if (!pluginConfig) {
  430. botlogger.error(`未找到插件配置: ${target.constructor.name}`);
  431. return descriptor;
  432. }
  433. const pluginId = pluginConfig.id;
  434. const pluginName = pluginConfig.name;
  435. // 获或创建插件的命令列表
  436. let plugin = commandList.find((p: Plugin) => p.class === target.constructor);
  437. if (!plugin) {
  438. plugin = {
  439. id: pluginId,
  440. name: pluginName,
  441. commands: [],
  442. class: target.constructor
  443. };
  444. commandList.push(plugin);
  445. botlogger.info(`创建新插件[${pluginId}]: ${pluginName}`);
  446. }
  447. // 使用新的命令格式
  448. const cmdList = Array.isArray(cmd) ? cmd : [cmd];
  449. const [mainCmd, ...aliases] = cmdList;
  450. // 修改命令创建
  451. const command: Command = {
  452. cmd: mainCmd,
  453. desc,
  454. fn: descriptor.value,
  455. aliases,
  456. cmdPrefix: CMD_PREFIX,
  457. pluginId: pluginId,
  458. class: target.constructor,
  459. template: {
  460. enabled: false,
  461. sendText: true
  462. }
  463. };
  464. plugin.commands.push(command);
  465. botlogger.info(`注册命令[${pluginId}]: ${CMD_PREFIX}${pluginId} ${mainCmd}`);
  466. return descriptor;
  467. };
  468. }
  469. // 修改参数解析函数
  470. async function parseCommandParams(message: string, context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage): Promise<any[]> {
  471. const cmdArgs = message.split(/\s+/).filter(Boolean);
  472. // 移除命令前缀和命令名
  473. const cmdPrefix = '#';
  474. const parts = message.split(/\s+/);
  475. const paramArgs = parts.slice(2); // 跳过 #test param 这两个部分
  476. // 调试日志
  477. botlogger.info('DEBUG - 命令参数:', JSON.stringify({
  478. message,
  479. cmdArgs,
  480. paramArgs,
  481. parts
  482. }));
  483. const params: any[] = [];
  484. // 添加参数
  485. if (paramArgs.length > 0) {
  486. // 第一个参数作为字符串
  487. params.push(paramArgs[0]);
  488. // 第二个参数尝试转换为数字
  489. if (paramArgs.length > 1) {
  490. const num = Number(paramArgs[1]);
  491. if (!isNaN(num)) {
  492. params.push(num);
  493. }
  494. }
  495. }
  496. // 添加 context 参数
  497. params.push(context);
  498. // 调试日志
  499. botlogger.info('DEBUG - 最终参数:', JSON.stringify({
  500. params: params.slice(0, -1), // 不显示 context
  501. paramCount: params.length,
  502. paramArgs
  503. }));
  504. return params;
  505. }