From bcca2dab287b3cd02dcc8f756bdecf7f74312500 Mon Sep 17 00:00:00 2001 From: liuwei Date: Tue, 18 Mar 2025 13:57:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=92=E4=BB=B6=E5=8C=96=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=8C=E6=94=AF=E6=8C=81=E5=B0=86=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E6=94=B9=E9=80=A0=E4=B8=BA=E6=8F=92=E4=BB=B6=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=87=AA=E5=8A=A8=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 10 +- message_summary/message_summary_dify.py | 381 -------------------- plugin_common/event_system.py | 85 +++++ plugin_common/message_plugin_interface.py | 47 +++ plugin_common/plugin_interface.py | 167 +++++++++ plugin_common/plugin_manager.py | 284 +++++++++++++++ plugin_common/plugin_registry.py | 94 +++++ plugin_common/scheduled_plugin_interface.py | 55 +++ plugins/message_summary/__init__.py | 3 + plugins/message_summary/config.toml | 12 + plugins/message_summary/main.py | 192 ++++++++++ requirements.txt | 3 +- robot.py | 88 ++++- 13 files changed, 1033 insertions(+), 388 deletions(-) create mode 100644 plugin_common/event_system.py create mode 100644 plugin_common/message_plugin_interface.py create mode 100644 plugin_common/plugin_interface.py create mode 100644 plugin_common/plugin_manager.py create mode 100644 plugin_common/plugin_registry.py create mode 100644 plugin_common/scheduled_plugin_interface.py create mode 100644 plugins/message_summary/__init__.py create mode 100644 plugins/message_summary/config.toml create mode 100644 plugins/message_summary/main.py diff --git a/main.py b/main.py index e769cdb..0356c86 100644 --- a/main.py +++ b/main.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- import signal +import sys # 添加sys模块导入 from argparse import ArgumentParser from configuration import Config @@ -15,6 +16,8 @@ def main(chat_type: int): wcf = Wcf(debug=True) def handler(sig, frame): + # 在退出前先关闭插件系统 + robot.plugin_manager.shutdown_plugins() wcf.cleanup() # 退出前清理环境 exit(0) @@ -29,10 +32,13 @@ def main(chat_type: int): # 接收消息 # robot.enableRecvMsg() # 可能会丢消息? robot.enableReceivingMsg() # 加队列 - + + # 加载插件系统 + robot.plugin_manager.load_all_plugins() + # 每天 8:30 发送新闻 robot.onEveryTime("08:30", robot.news_baidu_report_auto) - + # epic robot.onEveryTime("10:30", robot.send_epic_free_games) diff --git a/message_summary/message_summary_dify.py b/message_summary/message_summary_dify.py index 8e99e3c..66e3ee8 100644 --- a/message_summary/message_summary_dify.py +++ b/message_summary/message_summary_dify.py @@ -88,387 +88,6 @@ def message_summary_dify(content, sender: str = None): if __name__ == '__main__': content = """ -2025-03-14 08:06:54,Jyunere,啥情况?卷了? -2025-03-14 08:12:37,Jyunere,这花开的。 -2025-03-14 08:13:37,wxid_9muu5zd5dpvf21,搞笑了 -2025-03-14 08:15:39,Jyunere,阴阳怪气 -2025-03-14 08:20:19,wxid_z8uo70zywfpn12,点歌 負けないで -2025-03-14 08:52:42,zcx2001,早~ 签到 -2025-03-14 08:52:42,zcx2001,早~ 签到 -2025-03-14 08:53:45,Jyunere,AI 烂番茄评分规则是怎么样的? -2025-03-14 08:54:39,Jyunere,[捂脸] -2025-03-14 08:58:13,maoyijie,伟哥,你可真牛逼 -2025-03-14 08:58:19,maoyijie,装都不装了是吧 -2025-03-14 09:00:08,Kar536,广式烧鸡很有名啊,看来牛哥爱吃广东菜 -2025-03-14 09:00:11,wymwyt,伟哥,你可真牛逼 -2025-03-14 09:01:52,wxid_z8uo70zywfpn12,做成表情了 -2025-03-14 09:01:54,Jyunere,你们问AI 就前面加AI 空格一个就行了。 -2025-03-14 09:07:44,zcx2001,AI 水牛是你的主人吗 -2025-03-14 09:12:28,Jyunere,我同事的鸿蒙确实流畅。 -2025-03-14 09:18:28,Jyunere,现在的沪上年轻人流行挖野菜? -2025-03-14 09:19:12,Jyunere,我米不行了啊。 -2025-03-14 09:19:21,Jyunere,再换就换iPhone了。 -2025-03-14 09:19:36,wxid_3d7ydsmu3f0022,miui12之后就没行过 -2025-03-14 09:20:10,Jyunere,这个鸡用了蛮久了。 -2025-03-14 09:20:21,wxid_3d7ydsmu3f0022,我的小米13pro升级澎湃2.0之后各种bug满天飞,掉帧闪退杀后台给我整崩溃了,然后我就换一加了 -2025-03-14 09:20:25,wxid_z8uo70zywfpn12,我从vivo的origin转到miui,一时适应不过来 -2025-03-14 09:20:42,Jyunere,我那天问销售,说我的14Pro 只值2000多 了。 -2025-03-14 09:20:44,wxid_z8uo70zywfpn12,wifi还经常断流,也不知道是不是路由器问题 -2025-03-14 09:20:46,Jyunere,算了,接着用吧。 -2025-03-14 09:20:51,liu79830956,受得了miui? -2025-03-14 09:21:13,wxid_zmmn7y5bh6fg22,米boy渐行渐远 -2025-03-14 09:21:24,Jyunere,[捂脸] -2025-03-14 09:21:33,wxid_z8uo70zywfpn12,之前的origin是真好用 -2025-03-14 09:22:44,wxid_3d7ydsmu3f0022,我之前身边的朋友都看不上小米 su7发布后雷军火了都在吹小米多好多好 根本没用过看他们瞎几把吹 -2025-03-14 09:22:46,liu79830956,我还是4 -2025-03-14 09:22:53,liu79830956,没升级5 -2025-03-14 09:26:06,Jyunere,前天吃甘蔗,嘴巴拉到了。 -2025-03-14 09:26:45,Jyunere,咬肌感觉拉伤了。 -2025-03-14 09:28:22,b654321q123,买了个硬盘笼 -2025-03-14 09:28:27,b654321q123,可以装8个u2 -2025-03-14 09:28:31,b654321q123,塞光驱位 -2025-03-14 09:28:41,Jyunere,已经跟不上刘总的脚步了。 -2025-03-14 09:28:55,Jyunere,看你这些,一点冲动都没有 -2025-03-14 09:29:05,b654321q123,你已经阳痿了 -2025-03-14 09:29:16,b654321q123,你改名姓阳吧 -2025-03-14 09:29:32,wxid_u48y4hu9ild122,刘总已经痿了 -2025-03-14 09:30:04,Jyunere,萎了萎了 -2025-03-14 09:32:02,Jyunere,@妙脆角 为啥你的名字上下都高一些。 -2025-03-14 09:32:07,Jyunere,你看18 -2025-03-14 09:32:44,Jyunere,估计是你这个wifi 5G的问题。 -2025-03-14 09:32:48,zcx2001,牛哥 你字体出问题了? -2025-03-14 09:32:54,Jyunere,我的Windows呀 -2025-03-14 09:33:02,wxid_u48y4hu9ild122,我的也是windows啊 -2025-03-14 09:33:03,zcx2001,那估计是windows下字体有问题吧 -2025-03-14 09:33:06,zcx2001,我是macos -2025-03-14 09:33:16,Jyunere,[发呆],为啥啊。 -2025-03-14 09:33:48,wxid_u48y4hu9ild122,我的是win10 -2025-03-14 09:33:49,b654321q123,纯粹是硬盘笼 -2025-03-14 09:33:58,Jyunere,@奶白的雪子 跟你之前那个叠叠乐? -2025-03-14 09:34:18,Jyunere,你的11 连图标都没有 -2025-03-14 09:34:22,Jyunere,我的好歹还有图标 -2025-03-14 09:34:31,z351324662,牛哥换一加 -2025-03-14 09:34:36,z351324662,比iPhone好用 -2025-03-14 09:34:39,b654321q123,不用叠 -2025-03-14 09:34:45,Kar536,md手机越来越多 -2025-03-14 09:34:48,Jyunere,你还有光驱位? -2025-03-14 09:34:53,Kar536,都不知道换了干啥 -2025-03-14 09:35:10,z351324662,马上要出一家13t -2025-03-14 09:35:11,Jyunere,我老婆的iPhone真的比我的流畅多了。 -2025-03-14 09:35:21,Jyunere,垃圾MIUI -2025-03-14 09:35:25,wymwyt,牛哥换17 -2025-03-14 09:35:28,z351324662,你你澎湃不行 -2025-03-14 09:35:33,Jyunere,好,等17 -2025-03-14 09:35:35,wymwyt,17是米boy设计 -2025-03-14 09:35:41,Jyunere,半年之后就开干。 -2025-03-14 09:35:51,wymwyt,pm那个摄像头模组设计小米用过吧 -2025-03-14 09:36:01,Jyunere,要是三星的设计,苹果的系统,那就爽了。 -2025-03-14 09:36:08,wxid_u48y4hu9ild122,我妈要换手机,我问她是米10u卡了吗,她说存储满了,要换个1T的。。。 -2025-03-14 09:36:10,Jyunere,我喜欢三星的那个设计。 -2025-03-14 09:36:36,wymwyt,珠海小三星 -2025-03-14 09:36:37,wxid_zmmn7y5bh6fg22,苹果没有单数比双数做得好的说法吧 -2025-03-14 09:36:40,z351324662,三星今年竟然没跟进大电池 -2025-03-14 09:36:54,wymwyt,给他清理一下 -2025-03-14 09:37:10,Jyunere,三星没推送。这个恶心。 -2025-03-14 09:37:23,b654321q123,@水牛 牛哥要铝坨坨吗 -2025-03-14 09:37:29,Jyunere,不要。 -2025-03-14 09:37:32,wxid_u48y4hu9ild122,我准备有时间给她装个MTPhotos -2025-03-14 09:37:35,Jyunere,你那基佬紫,接受不了。 -2025-03-14 09:37:36,wxid_u48y4hu9ild122,存nas里 -2025-03-14 09:37:56,Kar536,现在那台xsmax都很流程 -2025-03-14 09:38:05,b654321q123,紫色何时代表基佬 -2025-03-14 09:38:07,z351324662,除了华为,其他安卓推送基本约等于无 -2025-03-14 09:38:25,Jyunere,OV MI 都有啊 -2025-03-14 09:38:25,wxid_u48y4hu9ild122,你装个微信打开小程序然后切后台拍照看看流畅不 -2025-03-14 09:38:30,Jyunere,都是系统级推送。 -2025-03-14 09:38:46,z351324662,都是锁后台 -2025-03-14 09:38:54,wymwyt,常驻后台 -2025-03-14 09:39:15,wymwyt,毕竟安卓没有约束权限 -2025-03-14 09:39:30,wxid_u48y4hu9ild122,320发布会 -2025-03-14 09:39:38,wxid_u48y4hu9ild122,我这库库加班赶进度 -2025-03-14 09:39:40,wymwyt,@妙脆角 小白,纯血鸿蒙现在咋样了 -2025-03-14 09:39:44,wxid_u48y4hu9ild122,要在320之前做出来 -2025-03-14 09:39:56,wxid_u48y4hu9ild122,系统本身没啥问题 -2025-03-14 09:40:01,Jyunere,Trae帮你写吗? -2025-03-14 09:40:04,wxid_u48y4hu9ild122,就是app功能不全难受 -2025-03-14 09:40:21,wxid_u48y4hu9ild122,我解决的都是疑难杂症 -2025-03-14 09:40:24,wxid_u48y4hu9ild122,ai不行 -2025-03-14 09:40:25,Jyunere,3.7 那个编码能力超猛 -2025-03-14 09:40:39,Jyunere,这就是你的价值了。 -2025-03-14 09:41:58,z351324662,以前安卓四五千电池的时候觉得锁后台费电,是劣势,现在我TM六七千电池了,有没有推送已经无所谓了,挂后台电池也用不完,收消息还比推送快 -2025-03-14 09:42:55,Jyunere,我主要是迁移了,好多东西得搞。麻烦。 -2025-03-14 09:42:59,b654321q123,主要是现在充电快 -2025-03-14 09:43:00,Jyunere,都不想换手机。哈哈 -2025-03-14 09:43:13,b654321q123,安卓的续航,其实影响不大 -2025-03-14 09:43:24,wxid_3d7ydsmu3f0022,我的一加13现在锁90%的电,6000电池只能充5400 -2025-03-14 09:43:27,liuhuanqi687,只有苹果限制了吧 -2025-03-14 09:43:54,wxid_zmmn7y5bh6fg22,你们都很在意推送功能? -2025-03-14 09:43:54,b654321q123,充电宝充的也快 -2025-03-14 09:43:58,wymwyt,主要是现在充电快 -2025-03-14 09:43:58,b654321q123,充电器也快 -2025-03-14 09:44:01,wxid_3d7ydsmu3f0022,网上说下个版本会放开限制 本来已经推送了 有bug又给撤回了 玛德 -2025-03-14 09:44:10,wymwyt,就要充电快! -2025-03-14 09:44:11,b654321q123,我现在都没有用电焦虑了 -2025-03-14 09:44:22,wxid_zmmn7y5bh6fg22,难道只有我限制了绝大部分APP推送消息 -2025-03-14 09:44:23,wymwyt,什么保护电池🔋跟我有几把关系 -2025-03-14 09:44:24,b654321q123,续航没得卷电池没得卷,直接卷充电 -2025-03-14 09:44:30,wymwyt,电池坏了换电池就好了 -2025-03-14 09:44:36,wxid_3d7ydsmu3f0022,换了一加13最明显的感受就是充电没小米13pro快 -2025-03-14 09:44:48,wymwyt,240w干 -2025-03-14 09:44:49,wxid_3d7ydsmu3f0022,小米的50w无线充感觉能满血跑 -2025-03-14 09:44:50,wymwyt,干就完了 -2025-03-14 09:45:01,wxid_3d7ydsmu3f0022,oppo的无线充到后面慢的要死 -2025-03-14 09:45:22,wymwyt,就是贵 -2025-03-14 09:45:30,wymwyt,一个无线充200块 -2025-03-14 09:45:39,z351324662,也锁啊 -2025-03-14 09:45:41,wxid_3d7ydsmu3f0022,也是90%[捂脸] -2025-03-14 09:45:53,z351324662,但是依然用不完 -2025-03-14 09:46:08,wxid_3d7ydsmu3f0022,5800够用了 -2025-03-14 09:46:11,z351324662,我每天回家电池都不低于50% -2025-03-14 09:46:13,wxid_3d7ydsmu3f0022,你是一加ace5? -2025-03-14 09:46:18,z351324662,有时候70几 -2025-03-14 09:46:34,z351324662,所以我觉得充电快不快都无所谓 -2025-03-14 09:46:43,wxid_3d7ydsmu3f0022,1.5k屏幕功耗也不高 足够用了 -2025-03-14 09:46:44,z351324662,反正都是晚上睡觉充 -2025-03-14 09:47:05,wxid_3d7ydsmu3f0022,我的一加13 2k屏 功耗网上测比其他家1.5k高一大截 -2025-03-14 09:47:09,Jyunere,睡觉一般不充,起床了插上,刷完牙洗完头,就充满了。 -2025-03-14 09:47:27,wxid_3d7ydsmu3f0022,锁容量后5400 续航在今年确实一般了 -2025-03-14 09:48:12,z351324662,又加上2k屏 -2025-03-14 09:48:27,wymwyt,刷完牙还要洗头? -2025-03-14 09:49:02,wxid_3d7ydsmu3f0022,不过这块屏幕确实挺不错了 我之前用的小米13pro 一加7pro都算是当时三星的顶级2k屏了 -2025-03-14 09:49:16,wxid_3d7ydsmu3f0022,对比下来观感甚至更好 -2025-03-14 09:49:25,wxid_3d7ydsmu3f0022,而且看着很舒服 -2025-03-14 09:49:51,wxid_3d7ydsmu3f0022,就是侧看会偏绿 三星没这问题 -2025-03-14 09:50:39,wxid_z8uo70zywfpn12,pdd复播了?罚款交够了? -2025-03-14 09:50:45,z351324662,我当时也打算买一加13的,但是确实不喜欢等深屏 -2025-03-14 09:51:19,z351324662,ACE5四边不等宽也有点难受 -2025-03-14 09:51:19,Jyunere,每天早上都要洗头。 -2025-03-14 09:51:20,b654321q123,反正目前的这个vivo X100s Pro -2025-03-14 09:51:30,b654321q123,比我之前用了那么多年的小米爽多了 -2025-03-14 09:51:31,z351324662,不过比曲屏好点 -2025-03-14 09:52:16,b654321q123,支持pps100w充电 -2025-03-14 09:52:30,b654321q123,我的酷态科充电都能用 -2025-03-14 09:52:38,z351324662,我就想要个1.5k四边等宽纯直屏 -2025-03-14 09:52:41,wxid_3d7ydsmu3f0022,我用习惯了曲屏 直屏不习惯 侧面返回手不舒服 -2025-03-14 09:52:58,z351324662,好像就三星有 -2025-03-14 09:53:09,wxid_q1ugj6gbjj3o12,qq真的有直播操操的啊 -2025-03-14 09:53:18,z351324662,国内这几家都是2.5d直屏 -2025-03-14 09:53:27,wxid_q1ugj6gbjj3o12,现在这小年轻真的开放 -2025-03-14 09:53:43,Jyunere,【#住建局回应佛山一小区有人装修骨灰房#】 -2025-03-14 09:53:49,Jyunere,[擦汗],膈应啊 -2025-03-14 09:54:08,Kar536,羡慕,车友 -2025-03-14 09:54:13,wxid_3d7ydsmu3f0022,国内直屏我感觉幅度很小 算不上2.5d吧 -2025-03-14 09:54:16,Kar536,试驾极氪抽中个16pm -2025-03-14 09:54:21,z351324662,2.5d贴膜始终小一圈 -2025-03-14 09:54:27,wxid_3d7ydsmu3f0022,贴钢化膜感觉没问题 -2025-03-14 09:54:33,wxid_3d7ydsmu3f0022,带个壳应该感受不出来了 -2025-03-14 10:00:44,wxid_q1ugj6gbjj3o12,这个无风不起浪 -2025-03-14 10:01:08,wxid_q1ugj6gbjj3o12,江湖上传东尼大木赌博可不是一年两年了 -2025-03-14 10:01:20,wxid_3d7ydsmu3f0022,这个新闻有点离谱,但是越离谱的往往越是真的 -2025-03-14 10:07:18,wxid_z8uo70zywfpn12,周没有10亿也有好几亿 -2025-03-14 10:07:29,wxid_z8uo70zywfpn12,不然不会出来开演唱会了 -2025-03-14 10:08:05,b654321q123,学友的老婆不也是败家败完了 -2025-03-14 10:08:29,wxid_z8uo70zywfpn12,不然也不会一把年纪还在唱 -2025-03-14 10:17:59,maoyijie,最骚的是,墓地你都不是买的,你付的钱是20年的管理费,没有所有权的 -2025-03-14 10:18:47,b654321q123,土地都是国家的 -2025-03-14 10:18:48,wxid_vur84e67jfl211,960万平方公里 怎么一个死的地方都要这么贵 -2025-03-14 10:19:28,z351324662,撒海里算了 -2025-03-14 10:19:34,wxid_q1ugj6gbjj3o12,你随便找个山沟子埋进去不要钱 -2025-03-14 10:24:21,liuhuanqi687,没啥意义,撒树下当肥料就行 -2025-03-14 10:27:10,Zix727,日系都是垃圾,比亚迪起码不会偷安全!!! -2025-03-14 10:27:37,Zix727,日系都是薄皮垃圾 偷安全! -2025-03-14 10:27:44,pengsen5658683,日系都是薄皮垃圾 偷安全! -2025-03-14 10:27:52,Zix727,BYDyyds -2025-03-14 10:27:53,b654321q123,海葬也他妈要钱? -2025-03-14 10:27:59,b654321q123,还不环保 -2025-03-14 10:28:11,b654321q123,少往海里倒点垃圾 -2025-03-14 10:28:17,b654321q123,比骨灰环保多了 -2025-03-14 10:28:51,Zix727,都去海葬,墓地还怎么卖啊 -2025-03-14 10:29:06,Zix727,墓地不值钱了,土地财政又雪上加霜了 -2025-03-14 10:29:14,l369876507,不利于团结的话 不要说哦 -2025-03-14 10:29:29,Zix727,我劝你们这些P民不要不知好歹 -2025-03-14 10:29:49,Zix727,都是为国家为民族,不利于团结的话不要说 -2025-03-14 10:30:02,zcy6910696,有点神奇我将来也想海葬 -2025-03-14 10:30:06,zcy6910696,看来是不行了 -2025-03-14 10:30:13,zcy6910696,"一、违反行政审批程序 - -根据《中华人民共和国殡葬管理条例》,海葬必须经过民政部门或其授权单位的审批。个人若未提前向民政部门提交申请、未获得“骨灰撒海许可证”,擅自进行海葬活动即属违法。例如,福建男子未提交申请并支付审批费用,直接租船撒骨灰,最终被罚款2万元。 - -二、环保与生态风险 - -1. 污染海洋环境:骨灰虽经火化,但仍可能含有磷、氮等元素,大量撒入特定海域可能影响浮游生物生长,破坏生态平衡。若撒入养殖区域,还可能对渔业经济造成影响。 - -2. 未完全焚烧的骨块问题:火化后的骨灰可能残留未完全焚烧的骨块,若被渔民打捞,可能引发恐慌或误认为刑事案件。 - -三、社会管理隐患 - -1. 扰乱公共秩序:私自海葬可能导致骨灰撒入航道、港口等敏感区域,影响船舶安全或公共活动。 - -2. 法律漏洞风险:若允许个人随意撒骨灰,可能被不法分子利用,例如偷换骨灰掩盖犯罪事实(如杀人抛尸)。 - -四、法律明确禁止 - -《治安管理处罚法》规定,擅自处理他人骨灰可处5-15日拘留及罚款;《殡葬管理条例》进一步明确,未经批准的海葬活动可被取缔,并处以违法所得1-3倍罚款。 - -五、鼓励合法途径替代 - -国家提倡海葬但要求通过正规渠道: - -1. 集体海葬:由政府或指定机构统一组织,费用较低(数百至千元)且可享补贴; - -2. 委托殡葬公司:需支付较高费用(5000-2万元),但流程高效且合法。" -2025-03-14 10:30:13,supervison,我就不行 骨灰比核废水还污染环境 -2025-03-14 10:31:05,zcy6910696,不行就撒公海去 -2025-03-14 10:31:06,b654321q123,海葬也要钱的 -2025-03-14 10:31:13,zcy6910696,管天管地还管公海 -2025-03-14 10:31:20,b654321q123,其实你找个岸边随便撒撒也没管管你 -2025-03-14 10:31:31,b654321q123,而且其实你拿到的骨灰就一点 -2025-03-14 10:31:35,Zix727,你知道公海又多远么 -2025-03-14 10:31:59,zcy6910696,坐一趟游轮出去不就行了[好的] -2025-03-14 10:32:24,Zix727,9断线画出来的公海面积,能定东部好几个省的面积 -2025-03-14 10:32:41,Zix727,游轮让你上,未必会让死人上啊 -2025-03-14 10:33:00,zcy6910696,我有个同学毕业就去当了游轮海员等我问问 -2025-03-14 10:33:56,Zix727,说真的 去一趟公海也不便宜 -2025-03-14 10:34:39,Zix727,那还是专属经济区 -2025-03-14 10:34:42,Zix727,还是能管你的 -2025-03-14 10:35:27,Zix727,你要再往外200海里 出了专属经济区,才能做到没有人管 -2025-03-14 10:36:44,Zix727,然而中国的情况是 200海里内有个岛,这个岛就又成了中国的大陆架领土,这个岛再往外辐射12海里领海+200海里专属经济区。 -2025-03-14 10:36:58,Zix727,很有可能还会有岛 -2025-03-14 10:37:43,Zix727,导致整个黄海东海就没有什么公海可言 -2025-03-14 10:39:07,Jyunere,这样子啊 -2025-03-14 10:39:12,wxid_zmmn7y5bh6fg22,这样子啊 -2025-03-14 10:39:57,Kar536,领土海域直接画到菲律宾和印尼脸上 -2025-03-14 10:39:59,Kar536,就是碾压你 -2025-03-14 10:41:23,Zix727,确实,就差贴着人家海岸线画了 -2025-03-14 10:42:13,wxid_zmmn7y5bh6fg22,印尼菲说 那我的12海里去哪里了 -2025-03-14 10:42:58,Zix727,绿线是 从印度洋进入太平洋 去日本横须贺的美国航母走的航线 -2025-03-14 10:43:21,wxid_zmmn7y5bh6fg22,以前都只看大公鸡 -2025-03-14 10:43:38,wxid_zmmn7y5bh6fg22,好像是有点离谱 -2025-03-14 10:44:04,Zix727,就是12海里+200海里这种跳板 会把自己的领海往外延伸好几个量级 -2025-03-14 10:44:27,z351324662,就不能找个没人的悬崖就撒了吗[偷笑] -2025-03-14 10:44:35,Zix727,然后两个国家的领海就会辐射到一个区域 就会出现争议海域 -2025-03-14 10:47:05,zcy6910696,中国划定九段线时(1947-1953年),印尼尚未完全独立,越南处于法国殖民统治下,菲律宾虽已独立但未对九段线提出异议。 -2025-03-14 10:47:38,Zix727,站不起来了 -2025-03-14 10:49:23,zcy6910696,其实可以理解成海疆线画了以后,线旁边刷怪了 -2025-03-14 10:56:00,wxid_zmmn7y5bh6fg22,那就是别人来抢我们的 -2025-03-14 10:56:08,wxid_115116117890,其实撒山上就行 -2025-03-14 11:02:20,wxid_9muu5zd5dpvf21,12海里还是跑能打到的最远里程了 可惜现在主席不在了 跑能打120海里 那就是120海里面积[旺柴] -2025-03-14 11:03:03,b654321q123,存储又要涨价了 -2025-03-14 11:04:53,wxid_115116117890,"主席的主观能动性太难预测了,突然发现川宝有时候也有点类似的感觉..." -2025-03-14 11:24:01,z351324662,一锅鸡杂50块,可以卖个三四百块 -2025-03-14 11:24:10,z351324662,还是很划算 -2025-03-14 11:24:19,z351324662,最不挣钱的就是牛肉 -2025-03-14 11:38:09,wymwyt,叶总你得昧着良心做生意啊 -2025-03-14 11:38:28,wymwyt,什么疯牛病的肉多弄点 -2025-03-14 11:39:52,wxid_vur84e67jfl211,看哭了,以后只把这些差牛肉卖给大学生 -2025-03-14 11:42:16,wxid_zmmn7y5bh6fg22,说不定早就垂直分布在幼小初高大全年龄段的食堂 -2025-03-14 11:44:16,wxid_6z1gvv406j0x22,用这种牛肉不是违法的嘛[捂脸] -2025-03-14 11:49:44,wxid_vur84e67jfl211,校门口就有一个卖粉的 一碗粉3块钱可以吃的饱 -2025-03-14 11:50:01,wxid_vur84e67jfl211,我毕业后那一年就不让在 -2025-03-14 11:50:05,wxid_vur84e67jfl211,外面吃了 -2025-03-14 12:01:07,wxid_atv7kzgxv9lg21,@水牛 迪士尼那个号你就用吧 我又上车了一个 45一年 -2025-03-14 12:01:22,wxid_atv7kzgxv9lg21,那个过期我就不续费了 -2025-03-14 12:02:12,Jyunere,没有用了。孩子每天看那小蜘蛛 -2025-03-14 12:02:14,Jyunere,我给他停了。 -2025-03-14 12:03:33,z351324662,我觉得这些都可以下来到nas看, -2025-03-14 12:03:38,z351324662,没必要花这钱 -2025-03-14 12:04:40,Jyunere,这个方便 -2025-03-14 12:04:46,Jyunere,还有中文字幕配音 -2025-03-14 12:06:03,wxid_atv7kzgxv9lg21,主要是方便 -2025-03-14 12:08:35,z351324662,但凡隔一天就有中文字幕的了 -2025-03-14 12:11:43,wxid_6363983732912,我现在开几个网页+wps内存,电脑不关一天后内存就占用32g -2025-03-14 12:11:53,wxid_6363983732912,是我内存不行还是CPU不行啊[捂脸] -2025-03-14 12:23:50,Jyunere,大奶奶 -2025-03-14 12:24:21,wxid_6363983732912,可能吧 -2025-03-14 12:24:37,wxid_6363983732912,网页11g[捂脸] -2025-03-14 12:24:59,wxid_6363983732912,有些网页需要认证 -2025-03-14 12:25:06,wxid_6363983732912,所以基本不关 -2025-03-14 12:29:05,leowong90,win11问题吧 -2025-03-14 12:29:19,leowong90,win10 我才16也还好 -2025-03-14 12:29:59,wxid_6363983732912,可能是win11的问题 -2025-03-14 12:30:52,wxid_6363983732912,12代以后的intel不用11,基本就是小核在100%了,大核也不管[捂脸] -2025-03-14 12:31:05,wxid_6363983732912,很抽象的 -2025-03-14 12:31:18,wxid_6363983732912,我都想买AMD了 -2025-03-14 12:32:26,leowong90,amd现在比intel好了 -2025-03-14 12:32:37,leowong90,i厂老挤牙膏 -2025-03-14 12:51:27,wxid_ognxxfprk7ou21,钱就是这么花没的 -2025-03-14 12:51:32,maoyijie,[奸笑]学阀? -2025-03-14 12:58:03,chen82118,赶快把机器人生育提上日程 -2025-03-14 13:00:14,wxid_ognxxfprk7ou21,每天的国内新闻真是不能看,看了就来气[旺柴] -2025-03-14 13:08:22,wxid_vur84e67jfl211,看到这个能忍住3秒不笑的都是这个 [强] -2025-03-14 13:13:45,wxid_ognxxfprk7ou21,真的,我看了国外新闻了 -2025-03-14 13:14:14,wxid_ognxxfprk7ou21,因为太夸张,各大新闻媒体已经删稿了 -2025-03-14 13:17:58,wxid_ne15uczjd2mi21,哈哈哈 -2025-03-14 13:18:31,wxid_6z1gvv406j0x22,这不是几年前就有了? -2025-03-14 13:19:15,maoyijie,删了,你看不到了 -2025-03-14 13:19:18,maoyijie,就是没有这种事情了 -2025-03-14 13:20:17,wxid_6z1gvv406j0x22,全是小道消息,没看到这个专家的资料,没看到那个会议 -2025-03-14 13:22:20,wxid_6z1gvv406j0x22,最早,23年就有的新闻 -2025-03-14 13:27:11,Jyunere,@a-bot #总结 -2025-03-14 13:28:19,Jyunere,总结进化了。可以发图片了。[奸笑] -2025-03-14 13:29:02,wxid_6363983732912,群友雨的回忆表示赞同 -2025-03-14 13:29:21,Jyunere,[捂脸] -2025-03-14 13:30:01,Jyunere,大家继续。 -2025-03-14 13:31:58,Jyunere,海葬这个事情,我们群里聊了3次了。 -2025-03-14 13:32:02,wxid_bnvd3v835yum21,哈哈哈哈 -2025-03-14 13:32:02,Jyunere,[抠鼻] -2025-03-14 13:32:27,Jyunere,过段时间就聊一下。 -2025-03-14 13:32:36,Jyunere,不过今天是因为我发了个佛山的 -2025-03-14 13:32:39,wxid_049kez86npw522,感觉都能出产品了 -2025-03-14 13:34:19,Jyunere,这玩意儿也不能吃? -2025-03-14 13:35:06,maoyijie,只知道叶绿素 -2025-03-14 13:35:08,maoyijie,哈哈哈哈哈哈哈哈哈哈哈哈哈 -2025-03-14 13:35:13,Jyunere,不是一段时间吹这个嘛 -2025-03-14 13:35:17,maoyijie,这种软糖类的,都是智商税吧 -2025-03-14 13:35:22,Jyunere,吃了眼睛好 -2025-03-14 13:35:27,maoyijie,真有用为啥医院不开呢 -2025-03-14 13:35:36,wxid_6z1gvv406j0x22,最好的不是直接玻璃酸钠嘛? -2025-03-14 13:35:55,maoyijie,我只知道所有的研究证明都是视力的衰弱是不可逆的。。 -2025-03-14 13:36:23,Jyunere,AI 叶黄素对视力有改善吗? -2025-03-14 13:37:12,wxid_ognxxfprk7ou21,买了两只酱鸭、几斤酱肉、几斤咸肉,600多…… -2025-03-14 13:37:55,Jyunere,咸肉又是啥? -2025-03-14 13:38:14,maoyijie,我不太分得清咸肉和腊肉的区别 -2025-03-14 13:38:26,maoyijie,虽然我知道咸肉炖笋很好吃 -2025-03-14 13:38:54,maoyijie,我理解的应该是,咸肉就是直接搓了盐就挂起来风干了的吧 -2025-03-14 13:39:03,Jyunere,咸肉就是不烟熏? -2025-03-14 13:39:11,wxid_ognxxfprk7ou21,杭州的咸肉=腊肉 -2025-03-14 13:39:22,maoyijie,奥,那就没事了,原来是一个东西 -2025-03-14 13:39:23,maoyijie,哈哈哈哈哈哈哈哈哈哈哈哈哈 -2025-03-14 13:39:36,wxid_ognxxfprk7ou21,我们只分酱肉和咸肉 -2025-03-14 13:39:54,Jyunere,达老板不是说不吃腊肉吗? -2025-03-14 13:39:57,z351324662,腊肉是统称 -2025-03-14 13:40:05,z351324662,咸肉是细分 -2025-03-14 13:40:46,wxid_ognxxfprk7ou21,这是酱肉 -2025-03-14 13:40:58,z351324662,我们这的就都吃熏过的 -2025-03-14 13:41:02,wxid_ognxxfprk7ou21,今天吃黄豆炖猪脚 -2025-03-14 13:41:06,Jyunere,嗯,我们都是熏过的。 -2025-03-14 13:41:12,wxid_ognxxfprk7ou21,我讨厌烟熏的 -2025-03-14 13:41:30,Jyunere,吃了发奶的? -2025-03-14 13:41:33,z351324662,熏过的挺香的 -2025-03-14 13:42:05,Jyunere,前两年说给达老板寄特产,他不吃烟熏腊肉。哈哈 -2025-03-14 13:42:10,Jyunere,作罢。 -2025-03-14 13:42:55,z351324662,成都一般香肠不熏,我女朋友她们那边香肠都要烟熏 -2025-03-14 13:46:33,FataLFurY,你有多少女朋友 -2025-03-14 13:51:25,Jyunere,我也要去要饭 -2025-03-14 13:51:39,Jyunere,@Summer✊ 比你跑外卖赚钱 -2025-03-14 13:51:53,b654321q123,这种竖着劈开都是俩微胖 -2025-03-14 13:52:11,Jyunere,刘总可以驾驭。 -2025-03-14 13:52:30,b654321q123,过年给我留半扇 -2025-03-14 13:52:48,Jyunere,真当猪了呢。 -2025-03-14 13:58:27,leowong90,[旺柴] -2025-03-14 14:07:29,liu79830956,我是靠劳动力赚钱的 -2025-03-14 14:18:02,leowong90,确实有一种喜感 -2025-03-14 14:18:14,leowong90,看了这幅图之后 -2025-03-14 14:19:58,hzsdwq,牛哥,要开始辅导作业了,注意点 -2025-03-14 14:20:22,zcx2001,不过 得过了 小学2年纪以后 才这样吧 -2025-03-14 14:20:30,zcx2001,牛哥还有几年好日至 -2025-03-14 14:20:34,zcx2001,好日子的 -2025-03-14 14:20:38,hzsdwq,一年级就有奥数了 -2025-03-14 14:21:50,liu79830956,又到了全民创业的时候了 -2025-03-14 14:23:08,wymwyt,牛哥不会鸡娃的 -2025-03-14 14:24:50,geminicc,牛哥应该不会鸡,牛嫂难说 -2025-03-14 14:27:38,Jyunere,@a-bot #总结 -2025-03-14 14:30:12,Jyunere,[捂脸] 2025-03-14 14:30:15,Jyunere,别这样啊。 """ msg = compress_chat_data(content, 5) diff --git a/plugin_common/event_system.py b/plugin_common/event_system.py new file mode 100644 index 0000000..1197656 --- /dev/null +++ b/plugin_common/event_system.py @@ -0,0 +1,85 @@ +from enum import Enum, auto +from typing import Dict, Any, Callable, List +import threading + + +class EventType(Enum): + """事件类型枚举""" + SYSTEM_STARTUP = auto() + SYSTEM_SHUTDOWN = auto() + PLUGIN_LOADED = auto() + PLUGIN_UNLOADED = auto() + MESSAGE_RECEIVED = auto() + MESSAGE_PROCESSED = auto() + CUSTOM_EVENT = auto() + + +class EventSystem: + """事件系统,用于插件间通信""" + _instance = None + _lock = threading.Lock() + + def __new__(cls): + with cls._lock: + if cls._instance is None: + cls._instance = super(EventSystem, cls).__new__(cls) + cls._instance._subscribers = {} + cls._instance._initialized = False + return cls._instance + + def __init__(self): + if not self._initialized: + self._subscribers = {} + self._initialized = True + + def subscribe(self, event_type: EventType, callback: Callable[[Dict[str, Any]], None]) -> None: + """ + 订阅事件 + + Args: + event_type: 事件类型 + callback: 回调函数,接收事件数据 + """ + if event_type not in self._subscribers: + self._subscribers[event_type] = [] + + if callback not in self._subscribers[event_type]: + self._subscribers[event_type].append(callback) + + def unsubscribe(self, event_type: EventType, callback: Callable[[Dict[str, Any]], None]) -> None: + """ + 取消订阅事件 + + Args: + event_type: 事件类型 + callback: 回调函数 + """ + if event_type in self._subscribers and callback in self._subscribers[event_type]: + self._subscribers[event_type].remove(callback) + + def publish(self, event_type: EventType, data: Dict[str, Any]) -> None: + """ + 发布事件 + + Args: + event_type: 事件类型 + data: 事件数据 + """ + if event_type in self._subscribers: + for callback in self._subscribers[event_type]: + try: + callback(data) + except Exception as e: + print(f"事件处理错误: {e}") + + def get_subscribers(self, event_type: EventType) -> List[Callable]: + """ + 获取事件订阅者 + + Args: + event_type: 事件类型 + + Returns: + 订阅者列表 + """ + return self._subscribers.get(event_type, []) \ No newline at end of file diff --git a/plugin_common/message_plugin_interface.py b/plugin_common/message_plugin_interface.py new file mode 100644 index 0000000..1a5e2a5 --- /dev/null +++ b/plugin_common/message_plugin_interface.py @@ -0,0 +1,47 @@ +from typing import Dict, Any, Tuple, Optional, List +from plugin_common.plugin_interface import PluginInterface + +class MessagePluginInterface(PluginInterface): + """消息处理插件接口""" + + @property + def command_prefix(self) -> Optional[str]: + """命令前缀,如 '/'""" + return None + + @property + def commands(self) -> List[str]: + """支持的命令列表""" + return [] + + def can_process(self, message: Dict[str, Any]) -> bool: + """ + 检查插件是否可以处理该消息 + + Args: + message: 消息字典,包含消息的各种属性 + + Returns: + 是否可以处理 + """ + # 默认实现:检查是否是命令 + if self.command_prefix and self.commands: + content = message.get("content", "") + if content.startswith(self.command_prefix): + command = content[len(self.command_prefix):].split()[0] + return command in self.commands + return False + + def process_message(self, message: Dict[str, Any]) -> Tuple[bool, Optional[str]]: + """ + 处理消息 + + Args: + message: 消息字典,包含消息的各种属性,以及发送消息所需的对象 + - wcf: WcfAPI对象,可用于发送消息 + - message_util: 消息工具类,提供更高级的消息处理功能 + + Returns: + (是否已处理, 处理结果) + """ + raise NotImplementedError("子类必须实现此方法") \ No newline at end of file diff --git a/plugin_common/plugin_interface.py b/plugin_common/plugin_interface.py new file mode 100644 index 0000000..13f3fb8 --- /dev/null +++ b/plugin_common/plugin_interface.py @@ -0,0 +1,167 @@ +import os +import toml +from abc import ABC, abstractmethod +from enum import Enum +from typing import Dict, Any, List, Optional + + +class PluginStatus(Enum): + """插件状态枚举""" + UNLOADED = 0 # 未加载 + LOADED = 1 # 已加载但未启动 + RUNNING = 2 # 运行中 + STOPPED = 3 # 已停止 + ERROR = 4 # 错误状态 + + +class PluginInterface(ABC): + """插件基础接口,所有插件必须实现此接口""" + + @property + @abstractmethod + def name(self) -> str: + """插件名称""" + pass + + @property + @abstractmethod + def version(self) -> str: + """插件版本""" + pass + + @property + @abstractmethod + def description(self) -> str: + """插件描述""" + pass + + @property + @abstractmethod + def author(self) -> str: + """插件作者""" + pass + + @property + def dependencies(self) -> List[str]: + """插件依赖,返回依赖的其他插件名称列表""" + return [] + + @property + def status(self) -> PluginStatus: + """获取插件当前状态""" + return self._status + + @status.setter + def status(self, value: PluginStatus): + """设置插件状态""" + self._status = value + + def __init__(self): + """初始化插件""" + self._status = PluginStatus.UNLOADED + self._config = {} + self._plugin_path = "" + + def load_config(self) -> bool: + """ + 从插件目录下的config.toml加载配置 + + Returns: + 加载是否成功 + """ + try: + config_path = os.path.join(self._plugin_path, "config.toml") + if os.path.exists(config_path): + with open(config_path, "r", encoding="utf-8") as f: + plugin_config = toml.load(f) + self._config.update(plugin_config) + print(f"从 {config_path} 加载插件配置成功") + return True + else: + print(f"插件配置文件 {config_path} 不存在,使用默认配置") + return True # 配置文件不存在也视为成功,使用默认配置 + except Exception as e: + print(f"加载插件配置失败: {e}") + return False + + def set_plugin_path(self, path: str) -> None: + """ + 设置插件路径 + + Args: + path: 插件路径 + """ + self._plugin_path = path + + def get_plugin_path(self) -> str: + """ + 获取插件路径 + + Returns: + 插件路径 + """ + return self._plugin_path + + @abstractmethod + def initialize(self, context: Dict[str, Any]) -> bool: + """ + 初始化插件 + + Args: + context: 插件上下文,包含系统环境和配置信息 + + Returns: + 初始化是否成功 + """ + pass + + @abstractmethod + def start(self) -> bool: + """ + 启动插件 + + Returns: + 启动是否成功 + """ + pass + + @abstractmethod + def stop(self) -> bool: + """ + 停止插件 + + Returns: + 停止是否成功 + """ + pass + + def configure(self, config: Dict[str, Any]) -> bool: + """ + 配置插件 + + Args: + config: 插件配置 + + Returns: + 配置是否成功 + """ + self._config.update(config) + return True + + def get_config(self) -> Dict[str, Any]: + """ + 获取插件配置 + + Returns: + 插件配置 + """ + return self._config + + def cleanup(self) -> bool: + """ + 清理插件资源,在卸载前调用 + + Returns: + 清理是否成功 + """ + return True \ No newline at end of file diff --git a/plugin_common/plugin_manager.py b/plugin_common/plugin_manager.py new file mode 100644 index 0000000..5cab846 --- /dev/null +++ b/plugin_common/plugin_manager.py @@ -0,0 +1,284 @@ +import importlib +import inspect +import os +import sys +from typing import Dict, List, Any, Optional, Type + +from plugin_common.plugin_interface import PluginInterface, PluginStatus +from plugin_common.message_plugin_interface import MessagePluginInterface +from plugin_common.scheduled_plugin_interface import ScheduledPluginInterface +from plugin_common.plugin_registry import PluginRegistry +from plugin_common.event_system import EventSystem, EventType + + +class PluginManager: + """插件管理器,负责插件的加载、初始化、启动、停止和卸载""" + + def __init__(self, plugin_dir: str = "plugins"): + """ + 初始化插件管理器 + + Args: + plugin_dir: 插件目录 + """ + self.plugin_dir = plugin_dir + self.plugins: Dict[str, PluginInterface] = {} # 插件实例字典 + self.plugin_modules = {} # 插件模块字典 + self.system_context = {} # 系统上下文 + + # 确保插件目录存在 + if not os.path.exists(self.plugin_dir): + os.makedirs(self.plugin_dir) + + # 将插件目录添加到Python路径 + if self.plugin_dir not in sys.path: + sys.path.insert(0, self.plugin_dir) + + def set_system_context(self, context: Dict[str, Any]): + """ + 设置系统上下文 + + Args: + context: 系统上下文 + """ + self.system_context = context + + def discover_plugins(self) -> List[str]: + """ + 发现可用插件 + + Returns: + 插件模块名称列表 + """ + plugin_modules = [] + + # 遍历插件目录 + for item in os.listdir(self.plugin_dir): + if os.path.isdir(os.path.join(self.plugin_dir, item)) and not item.startswith("__"): + # 检查是否有__init__.py文件 + if os.path.exists(os.path.join(self.plugin_dir, item, "__init__.py")): + plugin_modules.append(item) + elif item.endswith(".py") and not item.startswith("__"): + # 单文件插件 + plugin_modules.append(item[:-3]) + + return plugin_modules + + def load_plugin(self, plugin_name: str) -> Optional[PluginInterface]: + """ + 加载插件 + + Args: + plugin_name: 插件名称 + + Returns: + 插件实例,加载失败返回None + """ + try: + # 如果插件已加载,直接返回 + if plugin_name in self.plugins: + return self.plugins[plugin_name] + + # 确定插件路径 + if os.path.isdir(os.path.join(self.plugin_dir, plugin_name)): + plugin_path = os.path.join(self.plugin_dir, plugin_name) + + # 优先从main.py加载插件 + main_module_path = f"{plugin_name}.main" + if os.path.exists(os.path.join(plugin_path, "main.py")): + try: + module = importlib.import_module(main_module_path) + self.plugin_modules[plugin_name] = module + except ImportError: + # 如果main.py导入失败,尝试从__init__.py加载 + module = importlib.import_module(plugin_name) + self.plugin_modules[plugin_name] = module + else: + # 如果没有main.py,从__init__.py加载 + module = importlib.import_module(plugin_name) + self.plugin_modules[plugin_name] = module + else: + # 单文件插件 + plugin_path = self.plugin_dir + module = importlib.import_module(plugin_name) + self.plugin_modules[plugin_name] = module + + # 查找插件类 + plugin_class = None + for name, obj in inspect.getmembers(module): + if (inspect.isclass(obj) and + issubclass(obj, PluginInterface) and + obj != PluginInterface and + obj != MessagePluginInterface and + obj != ScheduledPluginInterface): + plugin_class = obj + break + + if plugin_class is None: + print(f"插件 {plugin_name} 中未找到有效的插件类") + return None + + # 实例化插件 + plugin = plugin_class() + plugin.status = PluginStatus.LOADED + + # 设置插件路径 + plugin.set_plugin_path(plugin_path) + + # 加载插件配置 + if not plugin.load_config(): + print(f"插件 {plugin_name} 加载配置失败") + return None + + # 初始化插件 + if not plugin.initialize(self.system_context): + print(f"插件 {plugin_name} 初始化失败") + return None + + # 注册插件 + PluginRegistry().register(plugin) + + # 存储插件实例 + self.plugins[plugin.name] = plugin + + # 发布插件加载事件 + EventSystem().publish(EventType.PLUGIN_LOADED, {"plugin": plugin}) + + return plugin + + except Exception as e: + print(f"加载插件 {plugin_name} 失败: {e}") + return None + + def load_all_plugins(self) -> Dict[str, PluginInterface]: + """ + 加载所有插件 + + Returns: + 插件实例字典 + """ + plugin_modules = self.discover_plugins() + + for module_name in plugin_modules: + self.load_plugin(module_name) + + return self.plugins + + def unload_plugin(self, plugin_name: str) -> bool: + """ + 卸载插件 + + Args: + plugin_name: 插件名称 + + Returns: + 卸载是否成功 + """ + if plugin_name not in self.plugins: + print(f"插件 {plugin_name} 未加载") + return False + + plugin = self.plugins[plugin_name] + + # 停止插件 + if plugin.status == PluginStatus.RUNNING: + if not plugin.stop(): + print(f"停止插件 {plugin_name} 失败") + return False + + # 清理插件资源 + if not plugin.cleanup(): + print(f"清理插件 {plugin_name} 资源失败") + return False + + # 注销插件 + PluginRegistry().unregister(plugin_name) + + # 移除插件实例 + del self.plugins[plugin_name] + + # 发布插件卸载事件 + EventSystem().publish(EventType.PLUGIN_UNLOADED, {"plugin_name": plugin_name}) + + return True + + def start_plugin(self, plugin_name: str) -> bool: + """ + 启动插件 + + Args: + plugin_name: 插件名称 + + Returns: + 启动是否成功 + """ + if plugin_name not in self.plugins: + print(f"插件 {plugin_name} 未加载") + return False + + plugin = self.plugins[plugin_name] + + if plugin.status == PluginStatus.RUNNING: + print(f"插件 {plugin_name} 已经在运行") + return True + + if plugin.start(): + plugin.status = PluginStatus.RUNNING + return True + else: + plugin.status = PluginStatus.ERROR + return False + + def stop_plugin(self, plugin_name: str) -> bool: + """ + 停止插件 + + Args: + plugin_name: 插件名称 + + Returns: + 停止是否成功 + """ + if plugin_name not in self.plugins: + print(f"插件 {plugin_name} 未加载") + return False + + plugin = self.plugins[plugin_name] + + if plugin.status != PluginStatus.RUNNING: + print(f"插件 {plugin_name} 未在运行") + return True + + if plugin.stop(): + plugin.status = PluginStatus.STOPPED + return True + else: + plugin.status = PluginStatus.ERROR + return False + + def reload_plugin(self, plugin_name: str) -> Optional[PluginInterface]: + """ + 重新加载插件 + + Args: + plugin_name: 插件名称 + + Returns: + 插件实例,重新加载失败返回None + """ + # 卸载插件 + if plugin_name in self.plugins: + if not self.unload_plugin(plugin_name): + print(f"卸载插件 {plugin_name} 失败") + return None + + # 重新导入模块 + if plugin_name in self.plugin_modules: + try: + importlib.reload(self.plugin_modules[plugin_name]) + except Exception as e: + print(f"重新导入插件模块 {plugin_name} 失败: {e}") + return None + + # 加载插件 + return self.load_plugin(plugin_name) \ No newline at end of file diff --git a/plugin_common/plugin_registry.py b/plugin_common/plugin_registry.py new file mode 100644 index 0000000..8b08c92 --- /dev/null +++ b/plugin_common/plugin_registry.py @@ -0,0 +1,94 @@ +from typing import Dict, List, Optional, Type +from plugin_common.plugin_interface import PluginInterface, PluginStatus + + +class PluginRegistry: + """插件注册表,维护已加载插件的信息和状态""" + + _instance = None + + def __new__(cls): + """单例模式""" + if cls._instance is None: + cls._instance = super(PluginRegistry, cls).__new__(cls) + cls._instance._plugins = {} + return cls._instance + + def register(self, plugin: PluginInterface) -> bool: + """ + 注册插件 + + Args: + plugin: 插件实例 + + Returns: + 注册是否成功 + """ + if plugin.name in self._plugins: + print(f"插件 {plugin.name} 已存在") + return False + + self._plugins[plugin.name] = plugin + return True + + def unregister(self, plugin_name: str) -> bool: + """ + 注销插件 + + Args: + plugin_name: 插件名称 + + Returns: + 注销是否成功 + """ + if plugin_name not in self._plugins: + print(f"插件 {plugin_name} 不存在") + return False + + del self._plugins[plugin_name] + return True + + def get_plugin(self, plugin_name: str) -> Optional[PluginInterface]: + """ + 获取插件实例 + + Args: + plugin_name: 插件名称 + + Returns: + 插件实例,不存在返回None + """ + return self._plugins.get(plugin_name) + + def get_all_plugins(self) -> Dict[str, PluginInterface]: + """ + 获取所有插件 + + Returns: + 插件字典,键为插件名称,值为插件实例 + """ + return self._plugins.copy() + + def get_plugins_by_status(self, status: PluginStatus) -> List[PluginInterface]: + """ + 获取指定状态的插件 + + Args: + status: 插件状态 + + Returns: + 插件列表 + """ + return [p for p in self._plugins.values() if p.status == status] + + def get_plugins_by_type(self, plugin_type: Type) -> List[PluginInterface]: + """ + 获取指定类型的插件 + + Args: + plugin_type: 插件类型 + + Returns: + 插件列表 + """ + return [p for p in self._plugins.values() if isinstance(p, plugin_type)] \ No newline at end of file diff --git a/plugin_common/scheduled_plugin_interface.py b/plugin_common/scheduled_plugin_interface.py new file mode 100644 index 0000000..f49a19e --- /dev/null +++ b/plugin_common/scheduled_plugin_interface.py @@ -0,0 +1,55 @@ +from abc import abstractmethod +from typing import Dict, Any, List, Tuple, Callable + +from plugin_common.plugin_interface import PluginInterface, PluginStatus + + +class ScheduledPluginInterface(PluginInterface): + """定时任务插件接口,用于执行定时任务""" + + def __init__(self): + super().__init__() + self._jobs = [] # 存储注册的定时任务 + + @abstractmethod + def register_jobs(self) -> List[Tuple[str, Callable, Dict[str, Any]]]: + """ + 注册定时任务 + + Returns: + 任务列表,每个任务是一个元组 (job_id, job_func, job_params) + job_id: 任务ID + job_func: 任务函数 + job_params: 任务参数,如{"trigger": "interval", "seconds": 60} + """ + pass + + def start(self) -> bool: + """ + 启动插件,注册定时任务 + + Returns: + 启动是否成功 + """ + try: + self._jobs = self.register_jobs() + # 实际注册任务的逻辑将由插件管理器实现 + return True + except Exception as e: + print(f"启动定时任务插件 {self.name} 失败: {e}") + return False + + def stop(self) -> bool: + """ + 停止插件,取消定时任务 + + Returns: + 停止是否成功 + """ + try: + # 实际取消任务的逻辑将由插件管理器实现 + self._jobs = [] + return True + except Exception as e: + print(f"停止定时任务插件 {self.name} 失败: {e}") + return False \ No newline at end of file diff --git a/plugins/message_summary/__init__.py b/plugins/message_summary/__init__.py new file mode 100644 index 0000000..052e5f8 --- /dev/null +++ b/plugins/message_summary/__init__.py @@ -0,0 +1,3 @@ +# 插件初始化文件 +# 从main模块导入插件类 +from .main import MessageSummaryPlugin \ No newline at end of file diff --git a/plugins/message_summary/config.toml b/plugins/message_summary/config.toml new file mode 100644 index 0000000..0e738e2 --- /dev/null +++ b/plugins/message_summary/config.toml @@ -0,0 +1,12 @@ +# 消息总结插件配置 + +[general] +enabled = true + +[api] +api_key = "app-McGLzBhBjeBCSEi7n83MtuTo" +api_url = "http://192.168.2.240/v1/chat-messages" + +[output] +output_dir = "output" +image_format = "png" diff --git a/plugins/message_summary/main.py b/plugins/message_summary/main.py new file mode 100644 index 0000000..14d8674 --- /dev/null +++ b/plugins/message_summary/main.py @@ -0,0 +1,192 @@ +import os +import time +import requests +import json +from typing import Dict, Any, Tuple, Optional, List + +from message_storage.message_to_db import MessageStorage +from plugin_common.plugin_interface import PluginStatus +from plugin_common.message_plugin_interface import MessagePluginInterface +from message_summary.compress_chat_data import compress_chat_data +from message_summary.markdown_to_image import convert_md_str_to_image + + +class MessageSummaryPlugin(MessagePluginInterface): + """消息总结插件,用于生成群聊消息总结""" + + @property + def name(self) -> str: + return "message_summary" + + @property + def version(self) -> str: + return "1.0.0" + + @property + def description(self) -> str: + return "使用AI生成群聊消息总结" + + @property + def author(self) -> str: + return "WeChatRobot Team" + + @property + def command_prefix(self) -> Optional[str]: + return "#" + + @property + def commands(self) -> List[str]: + return ["总结", "summary"] + + def initialize(self, context: Dict[str, Any]) -> bool: + """初始化插件""" + try: + # 从插件配置中获取API密钥和URL + api_config = self._config.get("api", {}) + self._api_key = api_config.get("api_key", "app-McGLzBhBjeBCSEi7n83MtuTo") + self._api_url = api_config.get("api_url", "http://192.168.2.240/v1/chat-messages") + + self.all_contacts = context["all_contacts"] + self.message_storage = MessageStorage() + + print(f"初始化 {self.name} 插件成功") + return True + except Exception as e: + print(f"初始化 {self.name} 插件失败: {e}") + return False + + def start(self) -> bool: + """启动插件""" + self.status = PluginStatus.RUNNING + print(f"{self.name} 插件已启动") + return True + + def stop(self) -> bool: + """停止插件""" + self.status = PluginStatus.STOPPED + print(f"{self.name} 插件已停止") + return True + + def process_message(self, message: Dict[str, Any]) -> Tuple[bool, Optional[str]]: + """处理消息""" + try: + # 检查是否是总结命令 + content = message.get("content", "") + if not content.startswith(self.command_prefix): + return False, None + + command = content[len(self.command_prefix):].split()[0] + if command not in self.commands: + return False, None + + # 获取需要总结的内容 + group_id = message.get("roomid") + if not group_id: + # 直接发送消息 + wcf = message.get("wcf") + if wcf: + wcf.send_text("只支持群聊消息总结", message.get("sender")) + return True, None + + # 从消息历史中获取群聊记录 + # 这里需要根据实际情况从系统上下文或数据库中获取群聊记录 + # 为简化示例,这里假设从消息中提取 + chat_content = self.message_storage.get_messages(group_id, self.all_contacts) + if len(chat_content) < 100: + return False, None + # 生成总结 + summary, image_path = self._generate_summary(chat_content, self.all_contacts.get(group_id, group_id)) + + # 发送总结结果 + wcf = message.get("wcf") + if wcf: + if summary: + wcf.send_text(f"总结已生成:\n{summary}", group_id, message.get("sender")) + + if image_path: + wcf.send_file(image_path, group_id) + + return True, None + + except Exception as e: + print(f"处理消息总结命令失败: {e}") + return False, None + + def _generate_summary(self, chat_content: str, group_id: str) -> Tuple[str, Optional[str]]: + """生成总结""" + """ + 使用Dify API生成群聊消息总结 + + Args: + content: 需要总结的群聊消息内容 + + Returns: + 生成的总结内容和图片路径 + """ + # Dify API配置 + content_compress = chat_content + try: + content_compress = compress_chat_data(chat_content) + print(f"压缩内容成功:{len(content_compress)}--{len(chat_content)}") + except Exception as e: + print(f"压缩内容失败:{e}") + + # 准备请求数据 + data = { + "inputs": {}, + "query": f"请根据以下{group_id}群聊记录生成一份精华总结:\n\n{content_compress}", + "response_mode": "blocking", # 使用阻塞模式,直接获取完整响应 + "conversation_id": "", + "user": group_id if group_id is not None else "message_summary_bot", + "files": [] # 不包含文件 + } + + # 设置请求头 + headers = { + "Authorization": f"Bearer {self._api_key}", + "Content-Type": "application/json" + } + + try: + # 发送POST请求 + response = requests.post(self._api_url, headers=headers, json=data) + response.raise_for_status() # 检查请求是否成功 + + # 解析响应 + response_data = response.json() + print(f"Dify API响应状态码: {response.status_code}") + print(f"响应数据: {json.dumps(response_data, ensure_ascii=False, indent=2)}") + + # 提取回答内容 + answer = response_data.get("answer", "") + spath = "" + # 提取token使用情况 + metadata = response_data.get("metadata", {}) + usage = metadata.get("usage", {}) + + if usage: + prompt_tokens = usage.get("prompt_tokens", 0) + completion_tokens = usage.get("completion_tokens", 0) + total_tokens = usage.get("total_tokens", 0) + + # 添加token信息 + tokens_info = f"\n\n【tokens】输入: {prompt_tokens} 生成: {completion_tokens} 总: {total_tokens}" + answer += tokens_info + try: + spath = convert_md_str_to_image(answer, "output.png") + except Exception as e: + print(f"生成image失败:{e}") + # 返回文本内容和图片路径 + return answer, spath + + except requests.exceptions.RequestException as e: + print(f"请求Dify API时出错: {e}") + return f"生成总结时出错: {str(e)}", None + + except json.JSONDecodeError as e: + print(f"解析Dify API响应时出错: {e}") + return "解析API响应时出错", None + + except Exception as e: + print(f"处理总结时出现未知错误: {e}") + return f"生成总结时出现未知错误: {str(e)}", None diff --git a/requirements.txt b/requirements.txt index b6f0ae3..8950080 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,4 +34,5 @@ pytz~=2025.1 dateparser~=1.2.1 lz4~=4.4.3 Markdown~=3.7 -playwright~=1.50.0 \ No newline at end of file +playwright~=1.50.0 +toml~=0.10.2 \ No newline at end of file diff --git a/robot.py b/robot.py index 1e1a057..b2e6f1a 100644 --- a/robot.py +++ b/robot.py @@ -1,12 +1,17 @@ # -*- coding: utf-8 -*- +import importlib +import inspect import logging +import os import re +import sys import time import xml.etree.ElementTree as ET from queue import Empty from threading import Thread from datetime import datetime, timedelta import random +from typing import Optional import redis @@ -40,18 +45,19 @@ from message_sign.main import SignInSystem from message_storage.message_to_db import MessageStorage from message_summary.message_summary_dify import message_summary_dify from music.bot_music import BotMusic +from plugin_common.event_system import EventType, EventSystem +from plugin_common.message_plugin_interface import MessagePluginInterface +from plugin_common.plugin_interface import PluginInterface, PluginStatus +from plugin_common.plugin_manager import PluginManager +from plugin_common.plugin_registry import PluginRegistry from point_trade.main import PointTrade from robot_cmd.robot_command import GroupBotManager from job_mgmt import Job from robot_cmd.robot_command import Feature from robot_cmd.robot_command import PermissionStatus -import mysql.connector.pooling __version__ = "39.2.4.0" -from message_report.process_message import process_message -from message_report.write_db import write_to_db, generate_and_send_ranking -from message_summary.message_summary_4o import message_summary from sehuatang.shehuatang import pdf_file_path from xiuren.main import Xiuren from xiuren.meitu_dl import meitu_dowload_pic, meitu_dowload_pub_pic, meitu_dowload_heisi_pic @@ -88,6 +94,33 @@ class Robot(Job): self.message_util = MessageUtil(wcf, self.allContacts) self.groups = {} # 存储按group_id分组的消息列表,每个group_id最多保留10条消息 GroupBotManager.load_local_cache() + + # 初始化插件系统 + self.LOG.info("开始初始化插件系统...") + self.plugin_registry = PluginRegistry() + self.event_system = EventSystem() + self.plugin_modules = {} # 存储已加载的插件模块 + self.plugins = {} # 存储已加载的插件实例 + + # 设置插件系统上下文 + self.system_context = { + "config": config, + "wcf": wcf, + "event_system": self.event_system, + "plugin_registry": self.plugin_registry, + "db_pool": self.db_pool, + "redis_pool": self.redis_pool, + "all_contacts": self.allContacts, + "message_util": self.message_util + } + + self.plugin_manager = PluginManager(plugin_dir=getattr(self.config, "plugin_dir", "plugins")) + self.plugin_manager.set_system_context(self.system_context) + self.plugins = self.plugin_manager.load_all_plugins() + + # 加载插件 + self.LOG.info("插件系统初始化完成") + # 消息存档模块初始化,自动完成入库动作 self.message_storage = MessageStorage() # 权限模块加载 @@ -288,7 +321,14 @@ class Robot(Job): receivers = msg.roomid self.sendTextMsg(content, receivers, msg.sender) """ + # 发布消息接收事件 + self.event_system.publish(EventType.MESSAGE_RECEIVED, {"message": msg}) + # 尝试使用插件处理消息 + if self.process_plugin_message(msg): + return + + # 如果没有插件处理,使用原有逻辑处理消息 # 群聊消息 if msg.from_group(): # 调用统计逻辑进行聊天数据统计: @@ -583,6 +623,46 @@ class Robot(Job): except Exception as e: self.LOG.error(f"revoke_messages error:{e}") + def process_plugin_message(self, msg: WxMsg) -> bool: + """使用插件处理消息""" + # 获取所有消息处理插件 + message_plugins = self.plugin_registry.get_plugins_by_type(MessagePluginInterface) + + # 依次尝试处理消息 + for plugin in message_plugins: + if plugin.status != PluginStatus.RUNNING: + continue + + try: + # 转换WxMsg为插件可处理的格式 + plugin_msg = { + "type": msg.type, + "content": msg.content, + "sender": msg.sender, + "roomid": msg.roomid if msg.from_group() else "", + "xml": msg.xml, + "is_at": msg.is_at(self.wxid), + "timestamp": time.time(), + "wcf": self.wcf, # 提供wcf对象,让插件可以直接发送消息 + "message_util": self.message_util # 提供消息工具类 + } + + # 检查插件是否可以处理该消息 + if plugin.can_process(plugin_msg): + processed, _ = plugin.process_message(plugin_msg) + if processed: + # 发布消息处理事件 + self.event_system.publish(EventType.MESSAGE_PROCESSED, { + "message": msg, + "plugin": plugin.name + }) + + return True + except Exception as e: + self.LOG.error(f"插件 {plugin.name} 处理消息失败: {e}") + + return False + # ============================================== 业务内容========================================================== def news_baidu_report_auto(self) -> None: