From fe316ced81c04cba21f5f71822d1385f16c04aaa Mon Sep 17 00:00:00 2001 From: liuwei Date: Thu, 16 Apr 2026 10:17:04 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96robot=5Fmenu=E4=BA=A4?= =?UTF-8?q?=E4=BA=92=EF=BC=9A=E6=94=AF=E6=8C=81@=E9=97=AE=E7=AD=94?= =?UTF-8?q?=E5=BC=8F=E5=8A=9F=E8=83=BD=E6=B8=85=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 变更项: 1. 新增@语义入口:群内@机器人并包含“功能清单、怎么用、帮助”等关键词时自动回复菜单。 2. 菜单输出改为用户友好格式:按功能展示名称、触发方式和一句话说明,提升可读性与可操作性。 3. 保留原指令入口(菜单/功能菜单),并统一走新的直观菜单文案。 4. 增加对Feature描述的解析逻辑,自动提取方括号中的触发命令作为使用方法。 --- plugins/robot_menu/main.py | 136 ++++++++++++++++++++++++++++++------- 1 file changed, 112 insertions(+), 24 deletions(-) diff --git a/plugins/robot_menu/main.py b/plugins/robot_menu/main.py index 5c6753a..ddac3b4 100644 --- a/plugins/robot_menu/main.py +++ b/plugins/robot_menu/main.py @@ -20,6 +20,16 @@ class RobotMenuPlugin(MessagePluginInterface): # 功能权限常量 FEATURE_KEY = "ROBOT_MENU" FEATURE_DESCRIPTION = "📋 功能菜单 [菜单 - 显示功能菜单 | 菜单 状态 - 显示功能状态]" + AT_QUERY_KEYWORDS = ( + "功能清单", + "功能菜单", + "功能列表", + "菜单", + "怎么用", + "如何用", + "帮助", + "help", + ) @property def name(self) -> str: @@ -92,9 +102,56 @@ class RobotMenuPlugin(MessagePluginInterface): return False content = str(message.get("content", "")).strip() - command = content.split(" ")[0] + command = content.split(" ")[0] if content else "" + roomid = str(message.get("roomid", "") or "") + is_at = bool(message.get("is_at", False)) - return command in self._commands + # 指令入口 + if command in self._commands: + return True + + # @语义入口:只在群聊 + @机器人 + 明确菜单意图时触发 + if roomid and is_at and self._is_at_menu_query(content): + return True + + return False + + @staticmethod + def _normalize_text(content: str) -> str: + text = str(content or "").lower().strip() + text = re.sub(r"\s+", "", text) + return text + + def _is_at_menu_query(self, content: str) -> bool: + text = self._normalize_text(content) + if not text: + return False + return any(self._normalize_text(keyword) in text for keyword in self.AT_QUERY_KEYWORDS) + + @staticmethod + def _extract_usage_from_description(description: str) -> str: + """ + 从 Feature.description 中提取 [ ... ] 内的触发方式 + 例如: '📋 功能菜单 [菜单 - 显示功能菜单]' -> '菜单' + """ + desc = str(description or "") + match = re.search(r"\[(.*?)\]", desc) + if not match: + return "请发送“菜单”查看" + inner = match.group(1).strip() + # 取第一段触发方式,避免太长 + first = inner.split("|")[0].strip() + if "-" in first: + first = first.split("-", 1)[0].strip() + return first or "请发送“菜单”查看" + + @staticmethod + def _extract_brief_from_description(description: str) -> str: + desc = str(description or "") + # 去掉开头 emoji 和 [..] 指令块,只保留一句用途 + desc = re.sub(r"^[^\u4e00-\u9fa5A-Za-z0-9]+", "", desc) + desc = re.sub(r"\[.*?\]", "", desc).strip() + return desc or "暂无说明" def display_menu_status(self, group_id: str) -> str: """显示所有功能列表及其在指定群组中的当前状态,带emoji""" @@ -106,43 +163,64 @@ class RobotMenuPlugin(MessagePluginInterface): menu.append(f"{status_emoji} {status_str}-{feature.value}-{feature.description}") return "\n".join(menu) - def get_enabled_features(self, group_id: str) -> str: - """获取某个群已启用的功能列表及其描述,并返回格式化的字符串 - 只返回描述中包含指令(方括号[])的功能 - - Args: - group_id: 群ID - - Returns: - str: 格式化的已启用功能列表字符串 - """ - enabled_features = [] - + def _get_enabled_feature_items(self, group_id: str) -> List[Dict[str, str]]: + """获取本群已启用且可对用户展示的功能条目""" + items: List[Dict[str, str]] = [] # 检查群是否在列表中 if group_id not in GroupBotManager.local_cache["group_list"]: - return "该群未启用机器人功能" + return items # 遍历所有功能,检查哪些已启用且包含指令 for feature in Feature: status = GroupBotManager.get_group_permission(group_id, feature) # 只添加已启用且描述中包含方括号的功能 if status == PermissionStatus.ENABLED and "[" in feature.description and "]" in feature.description: - enabled_features.append({ + items.append({ "id": feature.value, "name": feature.name, "description": feature.description }) + return items - # 如果没有启用任何带指令的功能 + def get_enabled_features(self, group_id: str) -> str: + """兼容旧接口:返回简化文本菜单""" + enabled_features = self._get_enabled_feature_items(group_id) + if group_id not in GroupBotManager.local_cache["group_list"]: + return "该群未启用机器人功能" if not enabled_features: return "该群未启用任何带指令的功能" - - # 构建格式化的字符串 - result = f"不支持@交互,请通过指令触发\n 群功能菜单:\n" + result = "群功能菜单:\n" for feature in enabled_features: - result += f"{feature['id']}.{feature['description']}\n" + usage = self._extract_usage_from_description(feature["description"]) + brief = self._extract_brief_from_description(feature["description"]) + result += f"{feature['id']}. {feature['name']} | 触发:{usage} | {brief}\n" + return result.strip() - return result + def build_user_friendly_menu(self, group_id: str) -> str: + """构建给普通群成员看的直观功能菜单""" + if group_id not in GroupBotManager.local_cache["group_list"]: + return "当前群未开通机器人功能,请联系管理员开启。" + + enabled_features = self._get_enabled_feature_items(group_id) + if not enabled_features: + return "当前群暂无可用功能。" + + lines = [ + "本群已开通功能:", + "直接复制“触发”里的命令发送即可。", + "", + ] + for idx, feature in enumerate(enabled_features, start=1): + usage = self._extract_usage_from_description(feature["description"]) + brief = self._extract_brief_from_description(feature["description"]) + lines.append(f"{idx}. {feature['name']}") + lines.append(f"触发:{usage}") + lines.append(f"说明:{brief}") + lines.append("") + + lines.append("快捷入口:") + lines.append("发送“菜单”可再次查看;发送“菜单 状态”可看全部开关状态。") + return "\n".join(lines).strip() def get_group_list(self) -> str: """返回所有启用了群机器人的群组清单""" @@ -171,6 +249,7 @@ class RobotMenuPlugin(MessagePluginInterface): self.LOG.debug(f"插件执行: {self.name}:{content}") sender = message.get("sender") roomid = message.get("roomid", "") + is_at = bool(message.get("is_at", False)) command = content.split(" ")[0] gbm: GroupBotManager = message.get("gbm") bot: WechatAPIClient = message.get("bot") @@ -187,15 +266,24 @@ class RobotMenuPlugin(MessagePluginInterface): return False, "没有权限" target = roomid if roomid else sender + is_at_menu_query = roomid and is_at and self._is_at_menu_query(content) + + # @语义菜单入口:用户@机器人直接问“功能清单/怎么用” + if is_at_menu_query and command not in self._commands: + menu_text = self.build_user_friendly_menu(roomid) + client_msg_id, create_time, new_msg_id = await bot.send_text_message(target, menu_text, sender) + revoke.add_message_to_revoke(target, client_msg_id, create_time, new_msg_id, 120) + return True, "at菜单问答" # 检查命令格式 parts = content.split() if len(parts) == 1: # 显示功能菜单 - menu_text = self.get_enabled_features(roomid if roomid else sender) + group_for_menu = roomid if roomid else sender + menu_text = self.build_user_friendly_menu(group_for_menu) client_msg_id, create_time, new_msg_id = await bot.send_text_message(target, menu_text, sender) - revoke.add_message_to_revoke(target, client_msg_id, create_time, new_msg_id, 90) + revoke.add_message_to_revoke(target, client_msg_id, create_time, new_msg_id, 120) return True, "显示功能菜单" # 提取命令名