收紧成员锐评的指令触发方式
- 去掉@机器人触发,只保留句首文本命令入口 - 禁用名字模糊匹配,仅支持锐评一下@某人和锐评一下我 - 同步更新插件说明文案并清理废弃匹配代码
This commit is contained in:
@@ -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 天窗口汇总成员历史摘要与群历史总结;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user