From 3fed0b0db1f837d5197796bbb1750686ce3da461 Mon Sep 17 00:00:00 2001 From: snowy-yang <102860110+snowy-yang@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:19:04 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B9=B1=E7=82=B9=E6=8F=90=E7=A4=BA=20@?= =?UTF-8?q?=E7=82=B9=E5=87=BB=E8=80=85=EF=BC=9Amarkdown=20=E5=86=85?= =?UTF-8?q?=E8=81=94=E6=8F=90=E5=8F=8A=20<@openid>?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .zcodeignore | 47 +++++++++++++++++++++++++++++++++++++++ authbot/handlers.py | 28 +++++++++++++++++++---- tests/test_interaction.py | 13 ++++++----- 3 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 .zcodeignore diff --git a/.zcodeignore b/.zcodeignore new file mode 100644 index 0000000..f808cfa --- /dev/null +++ b/.zcodeignore @@ -0,0 +1,47 @@ +# 密钥与本地配置(绝不提交) +.env + +# Python 运行产物 +__pycache__/ +*.pyc +.pytest_cache/ + +# 虚拟环境 +.venv/ + +# 运行时状态(禁言/验证进度,含成员 openid) +data/ + +# ZCode 工作目录 +.zcode/ + +# ===== ↑ 以上同步自 .gitignore(「从 .gitignore 同步」只重写以上部分)===== +.git/ +.hg/ +.svn/ +node_modules/ +bower_components/ +jspm_packages/ +site-packages/ +venv/ +coverage/ +htmlcov/ +lcov-report/ +cmakefiles/ +cmake-build-*/ +bazel-*/ +pods/ +deriveddata/ +storybook-static/ +playwright-report/ +test-results/ +allure-results/ +allure-report/ +cdk.out/ +*.egg-info/ +*.dist-info/ +eggs/ +pip-wheel-metadata/ +wheels/ +# ----- ↑ 以上为 ZCode 默认排除规则(自定义规则请写在本行下方,不会被同步/恢复改动)----- +# 自定义规则写在下方(本行提示可删除) diff --git a/authbot/handlers.py b/authbot/handlers.py index b6da693..eb8d1a7 100644 --- a/authbot/handlers.py +++ b/authbot/handlers.py @@ -96,10 +96,26 @@ async def set_member_mute( return False -async def _reply_text(api: BotApi, group_openid: str, text: str, *, event_id: str | None = None, msg_id: str | None = None) -> None: - """被动回复文本;窗口过期等失败降级为日志,不影响主流程。""" +async def _reply_text( + api: BotApi, group_openid: str, text: str, *, + event_id: str | None = None, msg_id: str | None = None, + mention: str | None = None, +) -> None: + """被动回复文本;mention 传 openid 时改为 markdown 内联提及 @对方。 + + 窗口过期等失败降级为日志,不影响主流程。提及必须走 markdown + (纯文本 content 不解析 <@> 语法)。 + """ try: - await api.post_group_message(group_openid, content=text, event_id=event_id, msg_id=msg_id) + if mention: + await api.post_group_message( + group_openid, markdown=f"<@{mention}> {text}", + event_id=event_id, msg_id=msg_id, + ) + else: + await api.post_group_message( + group_openid, content=text, event_id=event_id, msg_id=msg_id, + ) except ApiError as e: logger.warning(f"回复消息失败 (group={group_openid}): {e}") @@ -194,6 +210,7 @@ async def _handle_foreign_click( f"这道题不是给你的~(再乱点 {remaining} 次将被禁言 " f"{cfg.foreign_click_mute_minutes} 分钟)", event_id=event_id, + mention=member_openid, ) return @@ -215,6 +232,7 @@ async def _handle_foreign_click( api, group_openid, f"多次乱点他人的验证按钮,已禁言 {cfg.foreign_click_mute_minutes} 分钟。", event_id=event_id, + mention=member_openid, ) else: await _reply_text( @@ -222,6 +240,7 @@ async def _handle_foreign_click( "多次乱点他人的验证按钮!禁言未能生效(群主/管理员无法被禁言)," "请管理员处理。", event_id=event_id, + mention=member_openid, ) @@ -350,7 +369,8 @@ def setup(bot: Bot, cfg: AuthConfig) -> VerifyState: else: # 旧题/已失效按钮,礼貌忽略不计处罚 await _reply_text(api, inter.group_openid, "这道题不是给你的~", - event_id=event.event_id) + event_id=event.event_id, + mention=inter.group_member_openid) return if not is_subject: await _reply_text(api, inter.group_openid, "这题已经被换掉啦,请回答最新一题", diff --git a/tests/test_interaction.py b/tests/test_interaction.py index a71c2a3..e86f49a 100644 --- a/tests/test_interaction.py +++ b/tests/test_interaction.py @@ -142,15 +142,17 @@ def test_foreign_clicker_muted_on_third_click(tmp_path, monkeypatch): # 每次点击都回应了互动 assert len(fake.interactions_responded) == 3 - # 前两次只有警告,第三次才禁言 5 分钟 - warns = [m["content"] for m in fake.group_messages[:2]] - assert all("不是给你的" in (c or "") for c in warns) + # 前两次只有警告(markdown 内联提及点击者),第三次才禁言 5 分钟 + warns = [(m["markdown"] or "") for m in fake.group_messages[:2]] + assert all("不是给你的" in md for md in warns) + assert all(f"<@MEMBER_B>" in md for md in warns) mutes = [j for p, j in fake.raw_posts if p.endswith("/restrict_chat_setting")] assert len(mutes) == 1 entry = mutes[0]["members"][0] assert entry["op"] == "add" assert entry["member_openid"] == "MEMBER_B" - assert "已禁言 5 分钟" in (fake.group_messages[-1]["content"] or "") + assert "已禁言 5 分钟" in (fake.group_messages[-1]["markdown"] or "") + assert "<@MEMBER_B>" in (fake.group_messages[-1]["markdown"] or "") # 被试者的记录不受影响 assert state.get(pending_key("GROUP1", "MEMBER_A")) is not None state.pop(pending_key("GROUP1", "MEMBER_A")) @@ -173,6 +175,7 @@ def test_stale_button_click_not_punished(tmp_path, monkeypatch): await asyncio.sleep(0.03) assert fake.raw_posts == [] # 三次都不触发禁言 - assert all("不是给你的" in (m["content"] or "") for m in fake.group_messages) + assert all("不是给你的" in (m["markdown"] or "") for m in fake.group_messages) + assert all("<@MEMBER_C>" in (m["markdown"] or "") for m in fake.group_messages) asyncio.run(scenario())