├── .gitignore ├── __init__.py ├── config.json.template ├── README.md └── groupcast.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | config.json -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from .groupcast import * 2 | -------------------------------------------------------------------------------- /config.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "bot交流共享群组": { 3 | "group_name_keywords": [ 4 | "bot交流", 5 | "大模型" 6 | ], 7 | "enable": true 8 | }, 9 | "测试共享群组": { 10 | "group_name_keywords": [ 11 | "测试", 12 | "测试群" 13 | ], 14 | "enable": true 15 | }, 16 | "sync_interval": 3, 17 | "ignore_at_bot_msg": true 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GroupCast 2 | Dify on WeChat项目插件,支持将群聊消息在不同的共享群组内部进行广播转发。 3 | 4 | 支持配置多个共享群组,每个共享群组可以设置不同的群名关键词,消息只在同一共享群组内的群聊之间转发。 5 | 6 | >注意,需要在**机器人的微信账号**上把广播微信群**保存到通讯录** 7 | 8 | 9 | ## 功能特点 10 | 11 | - 支持配置多个独立的共享群组 12 | - 群聊消息只在同一共享群组内转发 13 | - 可配置消息转发间隔时间 14 | - 支持忽略@机器人的消息 15 | - 消息转发带有发送者和来源群信息 16 | - 使用消息队列确保消息有序转发 17 | 18 | ## 配置说明 19 | 20 | config.json 配置示例: 21 | ```bash 22 | { 23 | "bot交流共享群组": { # 共享群组名称 24 | "group_name_keywords": [ # 群名关键词列表,群名包含任一关键词则匹配 25 | "bot交流", 26 | "大模型" 27 | ], 28 | "enable": true # 是否启用该共享群组 29 | }, 30 | "测试共享群组": { 31 | "group_name_keywords": [ 32 | "测试", 33 | "测试群" 34 | ], 35 | "enable": true 36 | }, 37 | "sync_interval": 3, # 消息转发间隔时间(秒) 38 | "ignore_at_bot_msg": true # 是否忽略@机器人的消息 39 | } 40 | ``` 41 | 42 | ## 工作原理 43 | 44 | 1. 插件启动时会扫描所有群聊,根据配置的关键词将群聊分配到对应的共享群组中 45 | 2. 当收到群聊消息时,插件会: 46 | - 判断消息来源群所属的共享群组 47 | - 将消息转发到该共享群组内的其他群聊 48 | - 转发的消息格式为:`[发送者@来源群]: 消息内容` 49 | 3. 使用消息队列和独立的发送线程: 50 | - 确保消息按照接收顺序依次转发 51 | - 控制消息发送间隔 52 | - 避免消息发送失败影响其他消息 53 | 54 | ## 注意事项 55 | 56 | 1. 目前插件仅支持文本消息的转发 57 | 2. 仅支持gewechat channel,需要配置相关参数才能使用 58 | 3. 建议合理设置消息发送间隔,避免消息发送过于频繁 59 | 4. 当消息队列满时,新的消息将被丢弃 60 | -------------------------------------------------------------------------------- /groupcast.py: -------------------------------------------------------------------------------- 1 | # encoding:utf-8 2 | import time 3 | import queue 4 | import threading 5 | from common.log import logger 6 | from bridge.context import ContextType 7 | import plugins 8 | from plugins import * 9 | from config import conf 10 | from lib.gewechat.client import GewechatClient 11 | 12 | @plugins.register( 13 | name="GroupCast", 14 | desire_priority=100, 15 | hidden=False, 16 | enabled=False, 17 | desc="将群聊消息广播到其他群聊", 18 | version="0.1.0", 19 | author="hanfangyuan", 20 | ) 21 | class GroupCast(Plugin): 22 | # 定义队列最大容量 23 | MAX_QUEUE_SIZE = 100 24 | 25 | def __init__(self): 26 | super().__init__() 27 | # 初始化成员变量 28 | self.running = False 29 | self.sender_thread = None 30 | self.msg_queue = None 31 | self.broadcast_groups = {} # 改为字典,key为共享组名称,value为该组内的群列表 32 | self.client = None 33 | self.app_id = None 34 | self.sync_interval = 3 # 默认同步间隔 35 | self.ignore_at_bot_msg = True # 默认忽略@机器人的消息 36 | 37 | try: 38 | # 初始化消息队列 39 | self.msg_queue = queue.Queue(maxsize=self.MAX_QUEUE_SIZE) 40 | 41 | # 加载配置文件 42 | self.config = super().load_config() 43 | if not self.config: 44 | raise Exception("GroupCast 插件配置文件不存在") 45 | 46 | # 获取配置参数 47 | self.sync_interval = self.config.get("sync_interval", 3) 48 | self.ignore_at_bot_msg = self.config.get("ignore_at_bot_msg", True) 49 | 50 | # 检查是否是 gewechat 渠道 51 | if conf().get("channel_type") != "gewechat": 52 | raise Exception("GroupCast 插件仅支持 gewechat 渠道") 53 | 54 | # 检查必要的配置 55 | base_url = conf().get("gewechat_base_url") 56 | token = conf().get("gewechat_token") 57 | app_id = conf().get("gewechat_app_id") 58 | 59 | if not all([base_url, token, app_id]): 60 | raise Exception("GroupCast 插件需要配置 gewechat_base_url, gewechat_token 和 gewechat_app_id") 61 | 62 | # 初始化 gewechat client 63 | self.client = GewechatClient(base_url, token) 64 | self.app_id = app_id 65 | 66 | # 获取通讯录列表 67 | contacts = self.client.fetch_contacts_list(self.app_id) 68 | logger.debug(f"[GroupCast] 获取通讯录列表: {contacts}") 69 | 70 | if contacts and contacts.get("data"): 71 | chatrooms = contacts["data"].get("chatrooms", []) 72 | if chatrooms: 73 | # 获取群聊详细信息 74 | group_details = self.client.get_detail_info(self.app_id, chatrooms) 75 | logger.debug(f"[GroupCast] 获取群聊详细信息: {group_details}") 76 | 77 | # 处理每个共享群组的配置 78 | for group_name, group_config in self.config.items(): 79 | if isinstance(group_config, dict) and group_config.get("enable", False): 80 | keywords = group_config.get("group_name_keywords", []) 81 | if keywords: 82 | self.broadcast_groups[group_name] = [] 83 | # 查找匹配关键字的群 84 | if group_details and group_details.get("data"): 85 | for group in group_details["data"]: 86 | group_nickname = group.get("nickName", "") 87 | if any(keyword in group_nickname for keyword in keywords): 88 | self.broadcast_groups[group_name].append({ 89 | "name": group_nickname, 90 | "wxid": group.get("userName") 91 | }) 92 | 93 | logger.info(f"[GroupCast] 找到的共享群组: {self.broadcast_groups}") 94 | 95 | # 启动发送线程 96 | self.running = True 97 | self.sender_thread = threading.Thread(target=self._message_sender, name="groupcast_sender") 98 | self.sender_thread.daemon = True 99 | self.sender_thread.start() 100 | 101 | self.handlers[Event.ON_RECEIVE_MESSAGE] = self.on_handle_receive 102 | 103 | except Exception as e: 104 | self.cleanup() 105 | logger.error(f"[GroupCast] 初始化异常:{e}") 106 | raise e 107 | 108 | def _message_sender(self): 109 | """消息发送线程""" 110 | while self.running: 111 | try: 112 | # 从队列获取消息,设置1秒超时防止阻塞 113 | try: 114 | msg_data = self.msg_queue.get(timeout=1) 115 | except queue.Empty: 116 | continue 117 | 118 | success = False 119 | # 发送消息 120 | try: 121 | self.client.post_text(self.app_id, msg_data['group_id'], msg_data['content']) 122 | logger.debug(f"[GroupCast] 消息已转发到群 {msg_data['group_name']}") 123 | success = True 124 | except Exception as e: 125 | logger.error(f"[GroupCast] 转发消息到群 {msg_data['group_name']} 失败: {e}") 126 | finally: 127 | # 标记任务完成,无论成功失败 128 | self.msg_queue.task_done() 129 | 130 | # 只在发送成功时等待 131 | if success: 132 | time.sleep(self.sync_interval) 133 | 134 | except Exception as e: 135 | logger.error(f"[GroupCast] 消息发送线程异常: {e}") 136 | 137 | def on_handle_receive(self, e_context: EventContext): 138 | context = e_context['context'] 139 | logger.debug(f"[GroupCast] 收到群聊消息: {context}") 140 | 141 | try: 142 | # 检查是否是群聊文本消息 143 | if not context.kwargs.get('isgroup') or context.type != ContextType.TEXT: 144 | return 145 | 146 | # 获取 GeWeChatMessage 对象 147 | msg = context.kwargs.get('msg') 148 | if not msg: 149 | logger.error("[GroupCast] 无法获取消息对象") 150 | return 151 | 152 | # 如果配置了忽略@机器人的消息,则检查是否@机器人 153 | if self.ignore_at_bot_msg and msg.is_at: 154 | return 155 | 156 | # 获取消息来源群ID 157 | group_id = msg.from_user_id 158 | 159 | # 查找该群所属的共享组 160 | target_share_group = None 161 | target_groups = [] 162 | for share_group_name, groups in self.broadcast_groups.items(): 163 | if any(group['wxid'] == group_id for group in groups): 164 | target_share_group = share_group_name 165 | target_groups = groups 166 | break 167 | 168 | if not target_share_group: 169 | return 170 | 171 | # 获取群名称和发送者昵称 172 | group_name = msg.other_user_nickname 173 | sender_name = msg.actual_user_nickname 174 | 175 | # 构造转发消息 176 | content = f"[{sender_name}@{group_name}]: {context.content}" 177 | 178 | # 将消息加入队列,转发到同一共享组的其他群 179 | for group in target_groups: 180 | if group['wxid'] != group_id: # 不转发到源群 181 | try: 182 | self.msg_queue.put_nowait({ 183 | 'group_id': group['wxid'], 184 | 'group_name': group['name'], 185 | 'content': content 186 | }) 187 | except queue.Full: 188 | logger.warning(f"[GroupCast] 消息队列已满,丢弃转发到群 {group['name']} 的消息") 189 | 190 | except Exception as e: 191 | logger.error(f"[GroupCast] 处理消息异常: {e}") 192 | 193 | def get_help_text(self, **kwargs): 194 | help_text = "群消息广播插件。将配置的群聊消息广播到其他群聊。\n" 195 | return help_text 196 | 197 | def cleanup(self): 198 | """清理资源的方法""" 199 | if hasattr(self, 'running'): 200 | self.running = False 201 | 202 | # 等待队列中的消息处理完成 203 | if hasattr(self, 'msg_queue') and self.msg_queue: 204 | try: 205 | self.msg_queue.join() 206 | except: 207 | pass 208 | 209 | # 等待线程结束 210 | if hasattr(self, 'sender_thread') and self.sender_thread and self.sender_thread.is_alive(): 211 | self.sender_thread.join(timeout=5) 212 | 213 | def __del__(self): 214 | """析构函数调用清理方法""" 215 | self.cleanup() 216 | --------------------------------------------------------------------------------