From e7f28cd36e237e7ad97510bb2b10288946092fff Mon Sep 17 00:00:00 2001 From: cfdaily Date: Fri, 12 Jun 2026 18:56:49 +0800 Subject: [PATCH] fix(mention): address PR #45 review feedback (M1-M3, S1-S3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M1: Remove '帮忙' from help_keywords to fix keyword priority bug M3: Add unit tests for mention_utils (§25.7) S1: Merge two 'if action == opened' blocks in _handle_issues S2: Extract _send_review_mentions helper to deduplicate @mention code S3: Remove redundant 'import re' inside conditional block --- src/api/mention_utils.py | 4 +- src/api/toolchain_routes.py | 87 ++++++++++----------- tests/unit/test_mention_utils.py | 129 +++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+), 48 deletions(-) create mode 100644 tests/unit/test_mention_utils.py diff --git a/src/api/mention_utils.py b/src/api/mention_utils.py index cc9c4d2..5922ff0 100644 --- a/src/api/mention_utils.py +++ b/src/api/mention_utils.py @@ -110,8 +110,8 @@ def infer_intent(body: str) -> str: if any(kw in body for kw in assign_keywords): return "assign" - # 求助关键词 - help_keywords = ["怎么", "如何", "?", "?", "什么", "哪个", "能否", "帮忙"] + # 求助关键词(注意:"帮忙"已由 assign_keywords 的"帮忙做"覆盖,"请帮忙"由 collab_keywords 覆盖) + help_keywords = ["怎么", "如何", "?", "?", "什么", "哪个", "能否"] if any(kw in body for kw in help_keywords): return "help" diff --git a/src/api/toolchain_routes.py b/src/api/toolchain_routes.py index c503502..1881558 100644 --- a/src/api/toolchain_routes.py +++ b/src/api/toolchain_routes.py @@ -400,6 +400,32 @@ async def _handle_pr_opened(payload: Dict[str, Any]) -> None: ) +async def _send_review_mentions( + review_body: str, + reviewer: str, + pr_author: str, + pr: dict, + repo: str, + pr_number: int, +) -> None: + """提取并发送 Review body 中的 @mention 通知(COMMENTED / 非 COMMENTED 通用)。""" + mentions = extract_mentions(review_body, reviewer) + if mentions: + auto_targets = [pr_author] + await _send_mention_mails( + mentions=mentions, + auto_targets=auto_targets, + source_type="Review", + mention_type="Review @mention", + source_url=pr.get("html_url", ""), + commenter=reviewer, + content=review_body, + repo=repo, + issue_number=pr_number, + is_pr=True, + ) + + async def _handle_pull_request_review(payload: Dict[str, Any]) -> None: """处理 pull_request_review 事件:非 COMMENTED → 通知 PR 作者。 @@ -463,22 +489,7 @@ async def _handle_pull_request_review(payload: Dict[str, Any]) -> None: _send_mail(pr_author, title, text) # S5: Review body @mention 通知(COMMENTED 路径) - mentions = extract_mentions(review_body, reviewer) - if mentions: - # 自动流转已通知 PR 作者(review_comment) - auto_targets = [pr_author] - await _send_mention_mails( - mentions=mentions, - auto_targets=auto_targets, - source_type="Review", - mention_type="Review @mention", - source_url=pr.get("html_url", ""), - commenter=reviewer, - content=review_body, - repo=repo, - issue_number=pr_number, - is_pr=True, - ) + await _send_review_mentions(review_body, reviewer, pr_author, pr, repo, pr_number) return @@ -500,22 +511,7 @@ async def _handle_pull_request_review(payload: Dict[str, Any]) -> None: _send_mail(pr_author, title, text) # S5: Review body @mention 通知(非 COMMENTED 路径) - mentions = extract_mentions(review_body, reviewer) - if mentions: - # 自动流转已通知 PR 作者(review_result) - auto_targets = [pr_author] - await _send_mention_mails( - mentions=mentions, - auto_targets=auto_targets, - source_type="Review", - mention_type="Review @mention", - source_url=pr.get("html_url", ""), - commenter=reviewer, - content=review_body, - repo=repo, - issue_number=pr_number, - is_pr=True, - ) + await _send_review_mentions(review_body, reviewer, pr_author, pr, repo, pr_number) async def _fetch_latest_reviewer(repo: str, pr_number: int) -> str: @@ -700,7 +696,6 @@ async def _handle_pr_closed(payload: Dict[str, Any]) -> None: self_restart = False if pm2_name and os.environ.get("PM2_HOME") and "pm2 restart" in cmd: # 检查命令是否包含当前进程名 - import re if re.search(rf'pm2\s+restart\s+{re.escape(pm2_name)}', cmd): self_restart = True @@ -792,22 +787,22 @@ async def _handle_issues(payload: Dict[str, Any]) -> None: title = f"Issue 指派: {issue_title} ({repo}#{issue_number})" _send_mail(assignee, title, text) - elif action == "opened" and "部署失败" in issue_title: - # 从 Issue body 提取 commit hash(Gitea deploy workflow 格式) - sha_match = re.search(r'[0-9a-f]{40}', issue.get("body", "")) - commit_sha = sha_match.group(0) if sha_match else "(未知)" + elif action == "opened": + if "部署失败" in issue_title: + # 从 Issue body 提取 commit hash(Gitea deploy workflow 格式) + sha_match = re.search(r'[0-9a-f]{40}', issue.get("body", "")) + commit_sha = sha_match.group(0) if sha_match else "(未知)" - text = render_template("deploy_failure", { - "repo": repo, - "commit_sha": commit_sha or "(未知)", - }) + text = render_template("deploy_failure", { + "repo": repo, + "commit_sha": commit_sha or "(未知)", + }) - title = f"部署失败: {repo}" - for agent_id in ("jiangwei-infra", "pangtong-fujunshi"): - _send_mail(agent_id, title, text) + title = f"部署失败: {repo}" + for agent_id in ("jiangwei-infra", "pangtong-fujunshi"): + _send_mail(agent_id, title, text) - # S1: Issue body @mention(opened 时检查) - if action == "opened": + # Issue body @mention(opened 时检查) issue_body = issue.get("body", "") or "" sender = payload.get("sender", {}).get("login", "") mentions = extract_mentions(issue_body, sender) diff --git a/tests/unit/test_mention_utils.py b/tests/unit/test_mention_utils.py new file mode 100644 index 0000000..0a32cc7 --- /dev/null +++ b/tests/unit/test_mention_utils.py @@ -0,0 +1,129 @@ +"""mention_utils 单元测试 — §25.7 覆盖。""" + +import pytest + +from src.api.mention_utils import ( + extract_mentions, + should_suppress_mention, + infer_intent, +) + + +# --------------------------------------------------------------------------- +# extract_mentions +# --------------------------------------------------------------------------- + +class TestExtractMentions: + """测试 @mention 提取逻辑。""" + + def test_exact_match(self): + """@zhangfei-dev 精确匹配。""" + assert extract_mentions("@zhangfei-dev 请看一下", "someone") == ["zhangfei-dev"] + + def test_chinese_alias(self): + """@张飞 中文别名匹配。""" + assert extract_mentions("@张飞 帮忙看看", "someone") == ["zhangfei-dev"] + + def test_english_short_name(self): + """@zhangfei 英文短名匹配。""" + assert extract_mentions("@zhangfei 快来", "someone") == ["zhangfei-dev"] + + def test_prefix_unique(self): + """@zhangf 前缀唯一匹配。""" + assert extract_mentions("@zhangf 来一下", "someone") == ["zhangfei-dev"] + + def test_prefix_ambiguous_no_match(self): + """@z 前缀模糊,多个候选,不匹配。""" + assert extract_mentions("@z 看看", "someone") == [] + + def test_dedup_same_person(self): + """@张飞 @zhangfei-dev 同时出现去重。""" + result = extract_mentions("@张飞 @zhangfei-dev 来一下", "someone") + assert result == ["zhangfei-dev"] + + def test_exclude_self(self): + """@zhangfei-dev 排除自己(sender=zhangfei-dev)。""" + assert extract_mentions("@zhangfei-dev 自己说", "zhangfei-dev") == [] + + def test_unknown_person(self): + """@unknown 不匹配任何 Agent。""" + assert extract_mentions("@unknown 你好", "someone") == [] + + def test_multiple_mentions(self): + """多个 @mention 返回多个 Agent。""" + result = set(extract_mentions("@张飞 @关羽 来讨论", "someone")) + assert result == {"zhangfei-dev", "guanyu-dev"} + + def test_mention_with_hyphen_in_middle(self): + """@mention 后面紧跟标点也能识别。""" + result = extract_mentions("@赵云,请看下", "someone") + assert result == ["zhaoyun-data"] + + +# --------------------------------------------------------------------------- +# should_suppress_mention +# --------------------------------------------------------------------------- + +class TestShouldSuppressMention: + """测试 @mention 通知抑制逻辑。""" + + def test_suppress_when_in_list(self): + """被提及者在自动通知列表中 → 抑制。""" + assert should_suppress_mention("zhangfei-dev", ["zhangfei-dev", "guanyu-dev"]) is True + + def test_not_suppress_when_not_in_list(self): + """被提及者不在自动通知列表中 → 不抑制。""" + assert should_suppress_mention("zhangfei-dev", ["guanyu-dev"]) is False + + def test_suppress_empty_list(self): + """自动通知列表为空 → 不抑制。""" + assert should_suppress_mention("zhangfei-dev", []) is False + + +# --------------------------------------------------------------------------- +# infer_intent +# --------------------------------------------------------------------------- + +class TestInferIntent: + """测试意图推断逻辑。 + + 优先级:assign → collaborate → help → notify(默认) + """ + + def test_help_question_mark(self): + """疑问句 → help。""" + assert infer_intent("@赵云 数据格式是什么?") == "help" + + def test_notify_plain_mention(self): + """纯通知(无关键词) → notify。""" + assert infer_intent("@关羽 这个 PR 涉及风控变更") == "notify" + + def test_collaborate_please_help(self): + """'请帮忙' → collaborate(NOT help!)。""" + assert infer_intent("@庞统 请帮忙澄清需求") == "collaborate" + + def test_assign_keywords(self): + """'交给你' → assign。""" + assert infer_intent("@张飞 前端部分交给你") == "assign" + + def test_help_how_to(self): + """'如何' → help。""" + assert infer_intent("@姜维 如何部署这个服务") == "help" + + def test_collaborate_please_review(self): + """'请review' → collaborate。""" + assert infer_intent("@司马懿 请review 这个方案") == "collaborate" + + def test_notify_default(self): + """无任何关键词 → notify。""" + assert infer_intent("@赵云 已更新数据") == "notify" + + def test_assign_takes_priority_over_help(self): + """assign 关键词优先于 help 关键词。""" + # "交给" in body → assign, even though "?" also present + assert infer_intent("@张飞 这个模块交给你,有问题?") == "assign" + + def test_collaborate_takes_priority_over_help(self): + """collaborate 关键词优先于 help 关键词。""" + # "请帮忙" in body → collaborate, even though "?" absent + assert infer_intent("@赵云 请帮忙看看数据") == "collaborate"