收紧成员锐评的指令触发方式

- 去掉@机器人触发,只保留句首文本命令入口

- 禁用名字模糊匹配,仅支持锐评一下@某人和锐评一下我

- 同步更新插件说明文案并清理废弃匹配代码
This commit is contained in:
liuwei
2026-04-27 15:05:59 +08:00
parent 80a81b0d62
commit 63b7fc71ac
2 changed files with 26 additions and 99 deletions

View File

@@ -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 天窗口汇总成员历史摘要与群历史总结;

View File

@@ -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