Plugins.ts 25 KB

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