From 63b7fc71ac03ca5fcd3f6b60111e12a7e814f4e0 Mon Sep 17 00:00:00 2001 From: liuwei Date: Mon, 27 Apr 2026 15:05:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E7=B4=A7=E6=88=90=E5=91=98=E9=94=90?= =?UTF-8?q?=E8=AF=84=E7=9A=84=E6=8C=87=E4=BB=A4=E8=A7=A6=E5=8F=91=E6=96=B9?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 去掉@机器人触发,只保留句首文本命令入口 - 禁用名字模糊匹配,仅支持锐评一下@某人和锐评一下我 - 同步更新插件说明文案并清理废弃匹配代码 --- plugins/member_roast/config.toml | 6 +- plugins/member_roast/main.py | 119 +++++++------------------------ 2 files changed, 26 insertions(+), 99 deletions(-) diff --git a/plugins/member_roast/config.toml b/plugins/member_roast/config.toml index 3b73cc5..039ae98 100644 --- a/plugins/member_roast/config.toml +++ b/plugins/member_roast/config.toml @@ -3,9 +3,8 @@ enable = true command = ["锐评一下", "锐评", "吐槽一下", "锐评我", "吐槽我"] command_format = """ 锐评插件指令: -@机器人 锐评一下 @某人 -@机器人 锐评一下我 -@机器人 锐评一下 张三 +锐评一下 @某人 +锐评一下我 """ [llm] @@ -33,7 +32,6 @@ sample_days = 30 message_limit = 200 min_message_count = 8 context_stale_hours = 24 -name_match_min_chars = 2 # 历史画像窗口: # 1. 用户要求明确使用“历史两个月画像 + 当前本人画像”; # 2. 这里统一按 60 天窗口汇总成员历史摘要与群历史总结; diff --git a/plugins/member_roast/main.py b/plugins/member_roast/main.py index e5f145c..dca9ae9 100644 --- a/plugins/member_roast/main.py +++ b/plugins/member_roast/main.py @@ -325,9 +325,8 @@ class MemberRoastPlugin(MessagePluginInterface): """成员锐评插件。 用户场景: - 1. @机器人 锐评一下 @某人 - 2. @机器人 锐评一下我 - 3. @机器人 锐评一下 张三 + 1. 锐评一下 @某人 + 2. 锐评一下我 玩法目标: 1. 利用现有成员画像和群画像,让模型“骂得像认识这个人”; @@ -336,7 +335,7 @@ class MemberRoastPlugin(MessagePluginInterface): """ FEATURE_KEY = "MEMBER_ROAST" - FEATURE_DESCRIPTION = "🗡️ 成员锐评 [@机器人 锐评一下 @某人]" + FEATURE_DESCRIPTION = "🗡️ 成员锐评 [锐评一下 @某人]" RECENT_MESSAGE_STOPWORDS = { "这个", "那个", "就是", "然后", "但是", "还是", "我们", "你们", "他们", "自己", "一下", "已经", "没有", "一个", "可以", "什么", "怎么", "今天", "昨天", "现在", "时候", "知道", @@ -398,7 +397,6 @@ class MemberRoastPlugin(MessagePluginInterface): self.max_output_chars = 320 self.min_output_chars = 140 self.sharpness_level = "high" - self.name_match_min_chars = 2 # 这里给一个默认值兜底: # 1. `_build_user_prompt` 会直接使用该窗口天数; # 2. 如果插件刚构造、但还没完整初始化就被调用,至少不会因为属性不存在直接报错; @@ -415,7 +413,7 @@ class MemberRoastPlugin(MessagePluginInterface): self.command_format = str( cfg.get( "command_format", - "锐评插件指令:\n@机器人 锐评一下 @某人\n@机器人 锐评一下我", + "锐评插件指令:\n锐评一下 @某人\n锐评一下我", ) ) @@ -425,7 +423,6 @@ class MemberRoastPlugin(MessagePluginInterface): self.sharpness_level = str(style_cfg.get("sharpness_level", "high") or "high").strip().lower() profile_cfg = self._config.get("profile", {}) or {} - self.name_match_min_chars = max(int(profile_cfg.get("name_match_min_chars", 2) or 2), 1) # 历史窗口需要同步到插件实例本身: # 1. prompt 组装阶段会直接引用它; # 2. 之前只有 service 上有这个值,运行时存在属性缺失风险; @@ -456,10 +453,10 @@ class MemberRoastPlugin(MessagePluginInterface): def can_process(self, message: Dict[str, Any]) -> bool: """判断消息是否可能触发锐评。 - 这里不只看首词: - 1. 用户实际会说“@机器人 锐评一下 @张三”; - 2. 这时首词往往是 @机器人,而不是“锐评一下”; - 3. 因此需要做关键字级别的宽匹配。 + 触发策略这里刻意收紧成“文本命令开头”: + 1. 不再支持“@机器人 锐评一下 ...”,避免和群聊类 AI 插件抢消息; + 2. 也不再做“句中出现锐评”这类宽匹配,避免普通聊天误触; + 3. 只有以“锐评/吐槽”开头的文本指令,才进入后续处理。 """ if not self.enable: return False @@ -472,7 +469,7 @@ class MemberRoastPlugin(MessagePluginInterface): if not content: return False - return bool(re.search(r"(锐评(?:一下)?|吐槽一下|锐评我|吐槽我)", content)) + return self._is_text_command_trigger(content) @plugin_stats_decorator(plugin_name="成员锐评") @plugin_points_cost(10, "成员锐评消耗积分", FEATURE_KEY) @@ -496,8 +493,10 @@ class MemberRoastPlugin(MessagePluginInterface): if gbm and roomid and gbm.get_group_permission(roomid, self.feature) == PermissionStatus.DISABLED: return False, "没有权限" - # 群里更推荐 @机器人 触发,避免普通聊天里出现“锐评”二字时误打到插件。 - if not bool(message.get("is_at", False)) and not content.startswith(("锐评", "吐槽")): + # 这里只接纯文本指令入口: + # 1. 不再依赖 @机器人; + # 2. 非命令句首一律放过,尽量不给别的插件和正常聊天制造干扰。 + if not self._is_text_command_trigger(content): return False, None target_ok, target_payload = self._resolve_target_member(message, content) @@ -556,82 +555,19 @@ class MemberRoastPlugin(MessagePluginInterface): if any(keyword in normalized for keyword in ("锐评一下我", "锐评我", "吐槽我", "吐槽一下我")): return True, {"target_wxid": sender, "target_name": self._get_member_display_name(roomid, sender)} - candidate_name = self._extract_name_candidate(content) - if not candidate_name: - return False, {"error": "请 @一个人,或者直接说“锐评一下我”。"} - - matched = self._match_member_by_name(roomid, candidate_name) - if not matched: - return False, {"error": f"没在群里找到“{candidate_name}”这个人。"} - if len(matched) > 1: - names = "、".join([item["target_name"] for item in matched[:3]]) - return False, {"error": f"“{candidate_name}”对应了多个人:{names},建议直接 @TA。"} - return True, matched[0] - - def _extract_name_candidate(self, content: str) -> str: - """从文本里提取未 @ 时的目标名称候选。""" - text = self._normalize_text(content) - # 先去掉常见的 @机器人文案残留,尽量只保留命令后面真正的目标名。 - text = re.sub(r"^@\S+\s*", "", text) - text = re.sub(r"(锐评(?:一下)?|吐槽一下|吐槽我|锐评我)", " ", text, count=1) - text = re.sub(r"[@#::,,。.!!??\[\]\(\)]", " ", text) - text = re.sub(r"\s+", " ", text).strip() - if len(text) < self.name_match_min_chars: - return "" - return text - - def _match_member_by_name(self, roomid: str, candidate_name: str) -> List[Dict[str, str]]: - """按群昵称/微信昵称模糊匹配成员。 - - 匹配优先级: - 1. 完全匹配; - 2. 去空格后完全匹配; - 3. 包含匹配。 - """ - if not self.contacts_db: - return [] - - candidate = self._normalize_name(candidate_name) - if not candidate: - return [] - - members = self.contacts_db.get_chatroom_member_list(roomid) or [] - exact_matches: List[Dict[str, str]] = [] - contains_matches: List[Dict[str, str]] = [] - for member in members: - wxid = str(member.get("wxid") or "").strip() - if not wxid: - continue - display_name = str(member.get("display_name") or "").strip() - nick_name = str(member.get("nick_name") or "").strip() - for raw_name in [display_name, nick_name]: - normalized = self._normalize_name(raw_name) - if not normalized: - continue - payload = {"target_wxid": wxid, "target_name": display_name or nick_name or wxid} - if normalized == candidate: - exact_matches.append(payload) - break - if candidate in normalized: - contains_matches.append(payload) - break - - if exact_matches: - return self._dedup_target_list(exact_matches) - return self._dedup_target_list(contains_matches) + # 这里按你的要求明确收口: + # 1. 不再支持“锐评一下 张三”这种纯名字匹配; + # 2. 必须 @ 目标成员,或者直接说“锐评一下我”; + # 3. 这样可以避免重名误判,也能让指令边界更清晰。 + return False, {"error": "请使用“锐评一下 @某人”或“锐评一下我”,不支持直接写名字。"} @staticmethod - def _dedup_target_list(items: List[Dict[str, str]]) -> List[Dict[str, str]]: - """按 wxid 去重。""" - result: List[Dict[str, str]] = [] - seen = set() - for item in items: - wxid = str(item.get("target_wxid") or "").strip() - if not wxid or wxid in seen: - continue - seen.add(wxid) - result.append(item) - return result + def _is_text_command_trigger(content: str) -> bool: + """判断是否是插件允许的文本命令开头。""" + normalized = str(content or "").strip() + if not normalized: + return False + return bool(re.match(r"^(锐评(?:一下)?|吐槽(?:一下)?)(?:我|\s|$)", normalized)) def _generate_roast_text(self, payload: Dict[str, Any], requester_name: str = "") -> str: """调用大模型生成锐评文案。""" @@ -981,10 +917,3 @@ class MemberRoastPlugin(MessagePluginInterface): text = str(value or "").replace("\u2005", " ").replace("\xa0", " ") text = re.sub(r"\s+", " ", text) return text.strip() - - @staticmethod - def _normalize_name(value: str) -> str: - """规范化昵称用于模糊匹配。""" - text = str(value or "").strip().lower() - text = re.sub(r"\s+", "", text) - return text