""" 入群欢迎插件 当新成员加入群聊时,发送欢迎卡片 支持两种方式: 1. type=11098 群成员新增事件(如果 API 支持) 2. type=11058 系统消息解析(通用方式) """ import tomllib from pathlib import Path from loguru import logger from utils.plugin_base import PluginBase from utils.decorators import ( on_text_message, on_system_message, on_chatroom_member_add, on_chatroom_info_change ) class GroupWelcome(PluginBase): """入群欢迎插件""" # 插件元数据 description = "新成员入群时发送欢迎卡片" author = "ShiHao" version = "1.0.0" def __init__(self): super().__init__() self.config = None # 群成员缓存:{room_wxid: set(member_wxids)} self.member_cache = {} # 已欢迎成员缓存,防止重复欢迎:{(room_wxid, wxid_or_nickname): timestamp} self.welcomed_cache = {} # 机器人自己的 wxid self.bot_wxid = None async def async_init(self): """插件异步初始化""" # 读取配置 config_path = Path(__file__).parent / "config.toml" with open(config_path, "rb") as f: self.config = tomllib.load(f) logger.info("入群欢迎插件已加载") def _is_already_welcomed(self, room_wxid: str, member_key: str, alias_keys: list | None = None) -> bool: """检查成员是否已经被欢迎过(30秒内去重)""" import time cache_key = (room_wxid, member_key) now = time.time() # 清理过期缓存(超过30秒的) expired_keys = [k for k, t in self.welcomed_cache.items() if now - t > 30] for k in expired_keys: del self.welcomed_cache[k] if cache_key in self.welcomed_cache: return True if alias_keys: for alias_key in alias_keys: if alias_key and (room_wxid, alias_key) in self.welcomed_cache: return True self.welcomed_cache[cache_key] = now if alias_keys: for alias_key in alias_keys: if alias_key: self.welcomed_cache[(room_wxid, alias_key)] = now return False def _normalize_nickname(self, nickname: str) -> str: """规范化昵称,提升去重和匹配稳定性""" if not nickname: return "" return nickname.strip().strip('"').strip("“").strip("”") @on_chatroom_member_add(priority=50) async def on_chatroom_member_add(self, bot, message: dict): """处理群成员新增事件(type=11098)""" logger.info(f"[GroupWelcome] 收到群成员新增事件,原始消息: {message}") # 检查是否启用 if not self.config["behavior"]["enabled"]: logger.warning("[GroupWelcome] 插件未启用,跳过处理") return room_wxid = message.get("RoomWxid", "") member_list = message.get("MemberList", []) logger.info(f"[GroupWelcome] 解析结果 - 群ID: {room_wxid}, 成员列表: {member_list}") # 检查群聊过滤 if not self._should_welcome(room_wxid): logger.info(f"[GroupWelcome] 群 {room_wxid} 不在欢迎列表中,跳过") return logger.success(f"[GroupWelcome] 群 {room_wxid} 有新成员加入: {len(member_list)} 人") await self._process_new_members(bot, room_wxid, member_list) @on_system_message(priority=50) async def on_system_message(self, bot, message: dict): """处理系统消息(type=11058),解析入群通知""" # 检查是否启用 if not self.config["behavior"]["enabled"]: return raw_msg = message.get("Content", "") room_wxid = message.get("FromWxid", "") # 只处理群聊系统消息 if not room_wxid.endswith("@chatroom"): return logger.info(f"[GroupWelcome] 收到系统消息: room={room_wxid}, msg={raw_msg}") # 解析入群消息 # 格式1: "你邀请xxxx加入了群聊" # 格式2: "xxxx邀请xxxx加入了群聊" # 格式3: "xxxxx通过扫描你分享的二维码加入群聊" # 格式4: "xxxxx通过扫描xxxxxx分享的二维码加入群聊" import re # 匹配入群消息 patterns = [ r"(.+?)通过扫描.+?二维码加入群聊", # 扫码入群 r"(?:你|.+?)邀请(.+?)加入了群聊", # 邀请入群 ] for pattern in patterns: match = re.search(pattern, raw_msg) if match: nickname = match.group(1).strip() logger.success(f"[GroupWelcome] 从系统消息解析到新成员: {nickname}") # 检查群聊过滤 if not self._should_welcome(room_wxid): logger.info(f"[GroupWelcome] 群 {room_wxid} 不在欢迎列表中,跳过") return # 构造成员列表(只有昵称,没有 wxid) member_list = [{"wxid": "", "nickname": nickname}] await self._process_new_members(bot, room_wxid, member_list) break @on_chatroom_info_change(priority=50) async def on_chatroom_info_change(self, bot, message: dict): """处理群信息变化事件(type=11100),用于扫码加群等场景""" # 检查是否启用 if not self.config["behavior"]["enabled"]: return room_wxid = message.get("RoomWxid", "") total_member = message.get("TotalMember", 0) member_list = message.get("MemberList", []) logger.info(f"[GroupWelcome] 收到群信息变化事件: room={room_wxid}, total_member={total_member}, has_member_list={len(member_list) > 0}") # 检查群聊过滤 if not self._should_welcome(room_wxid): return # 注意:member_list 可能是全部成员列表,不能直接处理 # 必须通过缓存对比找出真正的新成员 # 获取当前群成员列表 try: current_members = await bot.get_chatroom_members(room_wxid) if not current_members: logger.warning(f"[GroupWelcome] 无法获取群成员列表: {room_wxid}") return current_wxids = {m.get("wxid") for m in current_members if m.get("wxid")} # 如果缓存中没有这个群,初始化缓存(首次) if room_wxid not in self.member_cache: logger.info(f"[GroupWelcome] 首次记录群成员: {room_wxid}, 成员数: {len(current_wxids)}") self.member_cache[room_wxid] = current_wxids return # 对比找出新成员 cached_wxids = self.member_cache[room_wxid] new_wxids = current_wxids - cached_wxids if new_wxids: logger.success(f"[GroupWelcome] 检测到新成员: {len(new_wxids)} 人") # 构造新成员列表 new_members = [m for m in current_members if m.get("wxid") in new_wxids] # 更新缓存 self.member_cache[room_wxid] = current_wxids # 处理新成员 await self._process_new_members(bot, room_wxid, new_members) else: # 成员数量没有增加,可能是其他信息变化 logger.debug(f"[GroupWelcome] 群信息变化但无新成员: {room_wxid}") # 更新缓存(可能有成员退出) self.member_cache[room_wxid] = current_wxids except Exception as e: logger.error(f"[GroupWelcome] 处理群信息变化失败: {e}") import traceback logger.error(f"详细错误: {traceback.format_exc()}") async def _process_new_members(self, bot, room_wxid: str, member_list: list): """处理新成员列表""" # 获取机器人自己的 wxid(用于过滤) if not self.bot_wxid: try: login_info = await bot.get_login_info() if login_info: self.bot_wxid = login_info.get("wxid", "") except Exception as e: logger.warning(f"[GroupWelcome] 获取登录信息失败: {e}") # 为每个新成员发送欢迎卡片 for member in member_list: wxid = member.get("wxid", "") nickname_raw = member.get("nickname", "新成员") nickname = self._normalize_nickname(nickname_raw) or "新成员" # 过滤机器人自己 if wxid and self.bot_wxid and wxid == self.bot_wxid: logger.info(f"[GroupWelcome] 跳过机器人自己: {wxid}") continue # 去重检查(使用 wxid 或 nickname 作为 key) member_key = wxid if wxid else nickname alias_keys = [nickname] if wxid else None if self._is_already_welcomed(room_wxid, member_key, alias_keys): logger.info(f"[GroupWelcome] 成员已欢迎过,跳过: {member_key}") continue try: await self._welcome_single_member(bot, room_wxid, wxid, nickname) except Exception as e: logger.error(f"处理新成员 {nickname} 欢迎失败: {e}") async def _welcome_single_member(self, bot, room_wxid: str, wxid: str, nickname: str): """欢迎单个新成员""" # 如果有 wxid,尝试获取详细信息(包括头像) if wxid: # 使用接口获取最新成员信息(新成员可能尚未同步进数据库) user_info = await bot.get_group_member_contact(room_wxid, wxid) if user_info: # 提取头像 URL(处理可能是字典或字符串的情况) big_head = user_info.get("bigHeadImgUrl", "") if isinstance(big_head, dict): big_head_img_url = big_head.get("string", "") else: big_head_img_url = big_head if isinstance(big_head, str) else "" # 提取昵称 nick_name = user_info.get("nickName", {}) if isinstance(nick_name, dict): actual_nickname = nick_name.get("string", nickname) else: actual_nickname = nick_name if isinstance(nick_name, str) else nickname logger.info(f"获取到新成员信息: {actual_nickname} ({wxid}), 头像: {big_head_img_url}") # 发送欢迎卡片 await self._send_welcome_card(bot, room_wxid, actual_nickname, big_head_img_url) else: logger.warning(f"无法获取新成员 {wxid} 的详细信息,使用默认配置") await self._send_welcome_card(bot, room_wxid, nickname, "") else: # 没有 wxid(从系统消息解析),尝试通过昵称匹配 await self._welcome_by_nickname(bot, room_wxid, nickname) async def _welcome_by_nickname(self, bot, room_wxid: str, nickname: str): """通过昵称匹配欢迎新成员""" import asyncio logger.info(f"[GroupWelcome] 尝试通过昵称匹配获取用户信息: {nickname}") # 获取群成员列表(走接口,确保最新) members = await bot.get_chatroom_members(room_wxid) # 如果没找到,等待1秒后重试 if not members or not any(nickname.strip('"') == m.get("nickname", "") for m in members): await asyncio.sleep(1) members = await bot.get_chatroom_members(room_wxid) if not members: logger.warning(f"[GroupWelcome] 无法获取群成员列表") await self._send_welcome_card(bot, room_wxid, nickname, "") return # 通过昵称匹配 matched_member = None for member in members: member_nickname = member.get("nickname", "") member_display_name = member.get("display_name", "") if nickname.strip('"') in (member_nickname, member_display_name): matched_member = member break if not matched_member: logger.warning(f"[GroupWelcome] 未找到匹配的成员: {nickname}") await self._send_welcome_card(bot, room_wxid, nickname, "") return member_wxid = matched_member.get("wxid", "") member_nickname = matched_member.get("nickname", nickname) logger.success(f"[GroupWelcome] 匹配成功: {member_nickname} ({member_wxid})") # 获取头像 try: user_info = await bot.get_group_member_contact(room_wxid, member_wxid) if user_info: big_head = user_info.get("bigHeadImgUrl", "") if isinstance(big_head, dict): member_avatar = big_head.get("string", "") else: member_avatar = big_head if isinstance(big_head, str) else "" await self._send_welcome_card(bot, room_wxid, member_nickname, member_avatar) else: await self._send_welcome_card(bot, room_wxid, member_nickname, "") except Exception as e: logger.error(f"[GroupWelcome] 获取成员详细信息失败: {e}") await self._send_welcome_card(bot, room_wxid, member_nickname, "") async def _send_welcome_card( self, bot, room_wxid: str, nickname: str, image_url: str ): """发送欢迎卡片""" welcome_config = self.config["welcome"] # 替换变量 title = welcome_config["title"].replace("{nickname}", nickname) desc = welcome_config["desc"].replace("{nickname}", nickname) url = welcome_config["url"] # 使用新成员的头像作为卡片图片 card_image_url = image_url if image_url else "" try: await bot.send_link_card( to_wxid=room_wxid, title=title, desc=desc, url=url, image_url=card_image_url, ) logger.success(f"已向 {nickname} 发送欢迎卡片") except Exception as e: logger.error(f"发送欢迎卡片失败: {e}") def _should_welcome(self, room_wxid: str) -> bool: """判断是否应该发送欢迎""" enabled_groups = self.config["behavior"]["enabled_groups"] disabled_groups = self.config["behavior"]["disabled_groups"] # 如果在禁用列表中,不欢迎 if room_wxid in disabled_groups: return False # 如果启用列表为空,对所有群生效 if not enabled_groups: return True # 否则只对启用列表中的群生效 return room_wxid in enabled_groups @on_text_message(priority=90) async def handle_test_command(self, bot, message: dict): """处理测试命令""" content = message.get("Content", "").strip() from_wxid = message.get("FromWxid", "") is_group = message.get("IsGroup", False) # 只处理群聊消息 if not is_group: return # 检查是否是测试入群命令 if content.startswith("/测试入群"): parts = content.split() if len(parts) < 2: await bot.send_text(from_wxid, "用法: /测试入群 wxid") return False test_wxid = parts[1].strip() logger.info(f"收到测试入群命令: wxid={test_wxid}") try: # 模拟群成员新增事件 test_message = { "RoomWxid": from_wxid, "MemberList": [{"wxid": test_wxid, "nickname": "测试用户"}], } await self.on_chatroom_member_add(bot, test_message) await bot.send_text(from_wxid, f"已触发入群欢迎测试: {test_wxid}") except Exception as e: logger.error(f"测试入群欢迎失败: {e}") await bot.send_text(from_wxid, f"测试失败: {e}") return False # 阻止后续处理