39 KiB
§25 — Gitea @mention 工具链端到端集成
状态: 草案 v2(修订版),待评审 作者: 庞统(副军师)🐦 日期: 2026-06-12 框架: 基于 §13 工具链四层改造 + §23 PR 全生命周期 + §20 Task Type Architecture 前置: §16 v3.2 已有
extract_mentions()+AGENT_ALIAS初步设计(仅覆盖 issue_comment),本篇补全为端到端完整方案
§25.0 背景与动机
问题
三国团队在 Gitea 上协作时,Agent 经常需要跨角色沟通:
- 张飞在 Issue 评论
@赵云 这个数据接口怎么用?——赵云收不到 - 庞统在 PR body 写
@simayi-challenger 请重点审查风控模块——司马懿没有额外感知 - 司马懿 review 驳回后评论
@zhangfei-dev 请注意边界检查——张飞虽然会收到 review_result Mail,但不知道 specifically @了他
§13.3.2 已有 extract_mentions() 初步设计,但只覆盖了 issue_comment 场景,且没有和工具链自动流转做去重/互补分析。本设计做完整端到端覆盖。
目标
- 全场景覆盖:@mention 在 Gitea 所有可写场景(Issue body、Issue comment、PR body、PR comment、Review body)都能触发通知
- 和自动流转互补不冗余:已有自动 Mail(如 Review 请求→司马懿)不因 @mention 重复通知
- 幂等安全:同一事件不会发两封相同 Mail
- 闭环可操作:Agent 收到 mention Mail 后,有明确的"做什么、怎么做"指引
- 复用现有架构:不改
_EVENT_HANDLERS分发结构,只扩展各 handler 内部逻辑
关键假设(显式记录)
| # | 假设 | 风险 | 缓解 |
|---|---|---|---|
| A1 | Gitea login = Agent ID(如 zhangfei-dev) |
如果某人 Gitea 用户名与 Agent ID 不同,extract_mentions 中 sender 排除会失效 |
当前团队 Gitea 用户名均按 Agent ID 注册,可在 AGENT_ALIAS 中维护映射 |
| A2 | 前缀模糊匹配唯一时才生效,匹配到多个候选则不匹配(报 warn 日志) | @zh 可能同时前缀匹配 zhangfei-dev 和 zhuge(如果未来加入) |
extract_mentions 改为统计匹配数,>1 则跳过并 log warning |
| A3 | Gitea 对 PR 上的普通评论同时发出 issue_comment 和 pull_request_comment 两个事件 |
双事件触发会导致重复 Mail | 只注册 issue_comment handler,不注册 pull_request_comment(详见 §25.5) |
§25.1 视角 A:@mention 场景遍历
Gitea 中 @mention 可能出现的 5 种场景
注:原 S6
commit_comment经姜维查阅 Gitea 源码modules/webhook/type.go确认 Gitea 不存在commit_commentwebhook 事件类型。故删除 S6,保留 5 个场景。
| # | 场景 | Gitea Webhook 事件 | 触发时机 | @mention 意图 | 现有自动流转 | 关系 |
|---|---|---|---|---|---|---|
| S1 | Issue body 中的 @mention | issues (action=opened) |
创建 Issue 时 | 拉人关注、协作请求、分配子任务 | Issue assigned → 通知被指派人 | 互补:assigned 通知指派人,@mention 通知额外关注者 |
| S2 | Issue comment 中的 @mention | issue_comment (action=created) |
在 Issue 下评论时 | 讨论、求助、通知特定人 | CI 失败评论 → 通知 PR 作者 | 互补:CI 逻辑和 @mention 逻辑互不干扰,同一条评论可同时触发 |
| S3 | PR body 中的 @mention | pull_request (action=opened) |
创建 PR 时 | 指定特定 reviewer、拉人关注 PR | PR opened → 通知 simayi-challenger review | 互补:默认 reviewer 通知照发,@mention 通知额外的人 |
| S4 | PR 上的评论(非 review)中的 @mention | issue_comment (on PR, is_pull=true) |
在 PR 页面评论时 | 讨论、提问、拉人参与 | §23 标记为低优先级(E4),未实现 | 增强:@mention 通知覆盖了 E4 的需求 |
| S5 | Review body 中的 @mention | pull_request_review (action=submitted) |
提交 Review 时 | 在 review 中 @PR 作者以外的人 | Review 结果 → 通知 PR 作者 | 互补:作者照收 review_result/review_comment,@mention 通知额外关注者 |
场景意图总结(含闭环行为)
@mention 在工具链中有 4 种核心意图,每种意图对应不同的闭环行为:
| 意图 | 典型场景 | Agent 行为 | 闭环方式 | 闭环操作 |
|---|---|---|---|---|
| 求助 | @zhaoyun-data 数据格式是什么? |
被助者获取上下文并回答 | 在 Gitea 评论回复 | 1. 获取 Issue/PR 详情 2. 回答问题 3. 在 Gitea 评论回复 |
| 通知关注 | @guanyu-dev 这个 PR 涉及风控变更 |
被通知者查看并知晓 | 查看 + 知晓(不强制回复) | 1. 获取 Issue/PR 详情 2. 阅读 3. 如有意见再评论 |
| 协作请求 | @pangtong-fujunshi 请帮忙澄清需求 |
被请求者评估并回复 | 评估后回复(评论或 Mail) | 1. 获取 Issue/PR 详情 2. 评估请求 3. 评论回复或发 Mail |
| 分配子任务 | @zhangfei-dev 这个 Issue 的前端交给你 |
被分配者认领并执行 | 认领 + 执行 + 更新状态 | 1. 获取 Issue 详情 2. 评估可行性 3. 认领(assign self) 4. 执行任务 5. 更新 Issue 状态 |
§25.2 视角 B:工具链端到端流程中的 @mention 位置
完整生命周期 + @mention 嵌入点
┌─────────────────────────────────────────────────────────────────────────┐
│ 需求阶段 │
│ ┌──────────┐ issues(opened) ┌──────────┐ │
│ │ 创建 Issue │ ──────────────────→│ 指派通知 │ ← 自动:assigned→Mail │
│ │ + @mention │ │ + @mention │ ← 新增:@额外人→Mail │
│ └──────────┘ └──────────┘ │
│ │ │ │
│ │ issue_comment │ │
│ ↓ ↓ │
│ ┌──────────┐ issue_comment ┌──────────┐ │
│ │ 需求讨论 │ ──────────────────→│ @mention │ ← 新增:讨论中@人→Mail │
│ │ + @mention │ │ 通知 │ │
│ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────────────────────────────────┤
│ 编码阶段 │
│ (本地开发,无 Gitea @mention 场景) │
├─────────────────────────────────────────────────────────────────────────┤
│ PR 阶段 │
│ ┌──────────┐ pull_request ┌──────────────┐ │
│ │ 创建 PR │ (opened) ──────────→│ Review 请求 │ ← 自动→simayi │
│ │ + @mention │ │ + @mention通知│ ← 新增:@额外人→Mail │
│ └──────────┘ └──────────────┘ │
│ │ │ │
│ │ PR 上评论 (issue_comment on PR) │
│ ↓ ↓ │
│ ┌──────────┐ issue_comment ┌──────────┐ │
│ │ PR 讨论 │ ────────────────────→│ @mention │ ← 新增 │
│ │ + @mention │ │ 通知 │ │
│ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────────────────────────────────┤
│ Review 阶段 │
│ ┌──────────┐ pull_request_ ┌──────────────┐ │
│ │ 提交 Review │ review(submitted) ─→│ Review 结果 │ ← 自动→PR作者 │
│ │ + @mention │ │ + @mention通知│ ← 新增:@额外人→Mail │
│ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────────────────────────────────┤
│ 修复阶段(Review 驳回后) │
│ PR 作者修改 → push → synchronize → 通知 reviewer 重新 review(§23) │
│ 讨论中 @mention → 同 S2/S4 │
├─────────────────────────────────────────────────────────────────────────┤
│ 合并 + 部署 │
│ PR merged → 通知 PR 作者 + 自动部署(§23) │
│ 此阶段无额外 @mention 场景 │
├─────────────────────────────────────────────────────────────────────────┤
│ CI/部署验证 │
│ CI 失败 → 自动通知 PR 作者 │
│ 评论中 @mention → 同 S2 │
│ 部署失败 → 自动通知 jiangwei + pangtong │
│ 无额外 @mention 场景 │
└─────────────────────────────────────────────────────────────────────────┘
流程完整性检查
| 检查项 | 结果 |
|---|---|
| 每个环节都有通知机制(自动或 @mention) | ✅ |
| @mention 不会导致流程断链 | ✅:@mention 是附加通知,不替代自动流转 |
| @mention 不会导致双重通知同一人 | ✅ 去重机制保障(见 §25.4) |
| 每种意图都有闭环指引 | ✅ 意图表含闭环方式(见 §25.1) |
§25.3 架构设计
25.3.1 核心组件:mention_utils.py
抽取 @mention 解析逻辑为独立模块,供所有 handler 复用。
# src/api/mention_utils.py
"""@mention 解析工具模块。供所有 toolchain handler 复用。"""
import re
import logging
from typing import List, Set
from src.config.agents import AGENT_IDS
logger = logging.getLogger(__name__)
# Gitea API 基地址常量(避免硬编码)
GITEA_API_BASE = "http://192.168.2.154:3000/api/v1"
GITEA_WEB_BASE = "http://192.168.2.154:3000"
# Agent 别名映射
# 规则:
# 1. 中文名(如"张飞")→ 完整 Agent ID
# 2. 英文短名(如"zhangfei")→ 完整 Agent ID
# 3. 前缀模糊匹配需唯一匹配(见 extract_mentions 假设 A2)
AGENT_ALIAS: dict[str, str] = {
# 中文名
"张飞": "zhangfei-dev",
"关羽": "guanyu-dev",
"赵云": "zhaoyun-data",
"姜维": "jiangwei-infra",
"司马懿": "simayi-challenger",
"庞统": "pangtong-fujunshi",
# 字+号(常见写法)
"翼德": "zhangfei-dev",
"云长": "guanyu-dev",
"子龙": "zhaoyun-data",
"伯约": "jiangwei-infra",
"仲达": "simayi-challenger",
"士元": "pangtong-fujunshi",
# 英文短名
"zhangfei": "zhangfei-dev",
"guanyu": "guanyu-dev",
"zhaoyun": "zhaoyun-data",
"jiangwei": "jiangwei-infra",
"simayi": "simayi-challenger",
"pangtong": "pangtong-fujunshi",
}
# 正则:匹配 @后面跟着的合法 Agent 名(英文字母/中文/数字/连字符)
_MENTION_PATTERN = re.compile(r"@([a-zA-Z\u4e00-\u9fa5][a-zA-Z0-9\u4e00-\u9fff-]*)")
def extract_mentions(body: str, sender: str) -> list[str]:
"""从文本中提取 @mention 的 Agent ID 列表。
Args:
body: 评论文本
sender: 评论者 Gitea 用户名(用于排除自己 @自己)
Returns:
去重后的 Agent ID 列表
匹配优先级:精确 > 别名 > 前缀模糊(需唯一匹配,多候选则跳过)
"""
candidates = _MENTION_PATTERN.findall(body)
result: Set[str] = set()
for c in candidates:
# 1. 精确匹配(@zhangfei-dev)
if c in AGENT_IDS:
result.add(c)
# 2. 别名匹配(@张飞、@zhangfei)
elif c in AGENT_ALIAS:
result.add(AGENT_ALIAS[c])
else:
# 3. 前缀模糊匹配(@zhangf → zhangfei-dev)
# 假设 A2:多个候选时不匹配,只 log warning
matches = [aid for aid in AGENT_IDS if aid.startswith(c)]
if len(matches) == 1:
result.add(matches[0])
elif len(matches) > 1:
logger.warning(
"Prefix '%s' matched %d agents (%s), skipping ambiguous mention",
c, len(matches), matches)
# 排除自己 @自己(假设 A1:Gitea login = Agent ID)
result.discard(sender)
return list(result)
def should_suppress_mention(
mentioned_agent: str,
auto_notify_targets: List[str],
) -> bool:
"""判断 @mention 通知是否应被抑制(因为自动流转已通知同一人)。
Args:
mentioned_agent: 被 @的 Agent ID
auto_notify_targets: 本次事件自动流转已通知的目标列表
Returns:
True 表示应抑制(不发 @mention Mail)
"""
return mentioned_agent in auto_notify_targets
25.3.2 通用 Mail 模板:mention.md
模板设计对齐 review_request.md 的风格:提供完整上下文 + 明确的流程指引 + API endpoint。
{mention_type}通知
来源: {source_type} {source_url}
评论者: {commenter}
意图: {intent_hint}
内容:
{content_snippet}
📋 获取完整上下文:
1. 查看{source_type}详情: GET {gitea_api}/repos/{repo}/{source_detail_api_path}
2. 查看评论列表: GET {gitea_api}/repos/{repo}/{source_comments_api_path}
📌 响应指引:
{response_guidance}
完成后按指引操作。
变量说明:
| 变量 | 说明 | 示例 |
|---|---|---|
mention_type |
场景标签 | @mention / Issue @mention / PR @mention / Review @mention |
source_type |
来源类型 | Issue / PR / Review |
source_url |
来源 URL(浏览器可访问) | http://192.168.2.154:3000/sanguo/repo/issues/32 |
commenter |
@mention 发起人 | zhangfei-dev |
intent_hint |
意图提示 | 求助 / 通知关注 / 协作请求 / 分配子任务 |
content_snippet |
内容摘要(前 500 字符) | @赵云 数据接口格式是什么?... |
gitea_api |
Gitea API 基地址(常量) | http://192.168.2.154:3000/api/v1 |
repo |
仓库全名 | sanguo/sanguo_moziplus_v2 |
source_detail_api_path |
详情 API 路径 | issues/32 或 pulls/15 |
source_comments_api_path |
评论 API 路径 | issues/32/comments 或 pulls/15/reviews |
response_guidance |
按意图生成的响应指引 | 见下方"响应指引生成规则" |
响应指引生成规则
根据意图类型,_send_mention_mails 函数会注入不同的 response_guidance 文本:
求助(help):
这是一条求助,请到 Gitea 评论回复:
1. 获取评论上下文(上方 API)
2. 组织回答
3. 在 Gitea 评论回复: POST {gitea_api}/repos/{repo}/issues/{number}/comments
Body: {"body": "你的回答内容"}
通知关注(notify):
这是一条通知,请查看并知晓。如有意见,可到 Gitea 评论:
- 查看{source_type}详情(上方 API)
- 如有意见,评论回复: POST {gitea_api}/repos/{repo}/issues/{number}/comments
协作请求(collaborate):
这是一条协作请求,请评估后回复(评论或 Mail):
1. 获取{source_type}详情(上方 API)
2. 评估可行性
3a. 评论回复: POST {gitea_api}/repos/{repo}/issues/{number}/comments
Body: {"body": "你的回复"}
3b. 或通过 Mail 回复评论者: {commenter}
分配子任务(assign):
这是一条任务分配,请认领并执行:
1. 获取 Issue 详情(上方 API)
2. 评估可行性
3. 认领 Issue: POST {gitea_api}/repos/{repo}/issues/{number}/assignees
Body: {"assignees": ["{your_agent_id}"]}
4. 执行任务
5. 完成后更新 Issue 状态: PATCH {gitea_api}/repos/{repo}/issues/{number}
Body: {"state": "closed"}
注:所有 API 调用需要 Gitea PAT,token scope 需包含
issue或repository。
意图推断规则
意图推断基于简单的关键词启发式:
def infer_intent(body: str, context: str = "") -> str:
"""从 @mention 内容推断意图。
Returns:
"help" | "notify" | "collaborate" | "assign"
"""
# 分配子任务关键词
assign_keywords = ["交给", "分配", "负责", "认领", "做一下", "帮忙做", "implement"]
if any(kw in body for kw in assign_keywords):
return "assign"
# 求助关键词("帮忙"已由 assign_keywords 的"帮忙做"覆盖,"请帮忙"由 collab_keywords 覆盖)
help_keywords = ["怎么", "如何", "?", "?", "什么", "哪个", "能否"]
if any(kw in body for kw in help_keywords):
return "help"
# 协作请求关键词
collab_keywords = ["请帮忙", "请协助", "请澄清", "请review", "请审查", "评估"]
if any(kw in body for kw in collab_keywords):
return "collaborate"
# 默认为通知关注
return "notify"
25.3.3 各场景 Handler 改动
S1: Issue body @mention — 改 _handle_issues
改动点:_handle_issues 处理 opened action 时,在发送 assigned Mail 后,额外检查 Issue body 中的 @mention。
设计决策:仅处理
opened,不处理edited(编辑 Issue 较少见,且可能产生重复通知——首次 opened 已发过)。见 §25.8 "不做的事"。
# _handle_issues 改动(伪代码)
async def _handle_issues(payload):
action = payload.get("action", "")
if action == "opened":
# ... 现有逻辑:assigned → 发指派 Mail ...
# 新增:Issue body @mention 通知
await _process_issue_body_mentions(payload)
elif action == "assigned":
# 现有逻辑不变
...
_process_issue_body_mentions 逻辑:
- 从
issue.body提取 mentions - 排除 assignee(已被 assigned Mail 通知过)
- 排除 sender(Issue 创建者)
- 推断意图、生成响应指引
- 发送 mention Mail
自动流转互补分析:
- assigned Mail → 通知 assignee
- @mention Mail → 通知额外关注者
- 去重:assignee 不再收到 mention Mail(
should_suppress_mention)
S2+S4: Issue/PR comment @mention — 重构 _handle_issue_comment
⚠️ 关键改动:控制流重构(M2)
现状问题:现有 _handle_issue_comment 的控制流是:
# toolchain_routes.py 现有代码
async def _handle_issue_comment(payload):
...
if "[CI]" not in body and "CI 失败" not in body:
return # ← 非 CI 评论直接 return,@mention 逻辑到不了
# ... CI 失败处理 ...
非 CI 评论在第一行就被 return 了,无法在 CI 逻辑之后追加 @mention 检测。
重构方案:改为两条独立路径(if/elif + 并行 @mention)
async def _handle_issue_comment(payload):
comment = payload.get("comment", {})
body = comment.get("body", "")
sender = comment.get("user", {}).get("login", "")
issue = payload.get("issue", {})
action = payload.get("action", "")
if action != "created":
return
# === 路径 1:CI 失败通知(原有逻辑) ===
if "[CI]" in body or "CI 失败" in body:
if issue.get("state") != "closed":
# ... CI 失败通知逻辑(不变) ...
pass
# CI 处理完不 return,继续检查 @mention
# === 路径 2:@mention 通知(新增) ===
# 注意:@mention 检测与 CI 检测是独立的,同一条评论可同时触发两者
mentions = extract_mentions(body, sender)
if mentions:
# 判断是 PR 还是 Issue(Gitea 中 PR 本质是特殊的 Issue)
is_pr = "pull_request" in issue
source_type = "PR" if is_pr else "Issue"
mention_type = "PR @mention" if is_pr else "Issue @mention"
issue_number = issue.get("number", 0)
# 自动流转已通知的人(CI 失败通知的 PR 作者)
auto_targets = []
if ("[CI]" in body or "CI 失败" in body) and issue.get("state") != "closed":
auto_targets.append(issue.get("user", {}).get("login", ""))
await _send_mention_mails(
mentions=mentions,
auto_targets=auto_targets,
source_type=source_type,
mention_type=mention_type,
source_url=issue.get("html_url", ""),
commenter=sender,
content=body,
repo=_repo_fullname(payload),
issue_number=issue_number,
is_pr=is_pr,
)
重构要点:
- 删除
if "[CI]" not in body ... return的早期退出 - CI 检测改为正向
if(满足条件才进入 CI 处理),不再用 guard clause 拦截所有非 CI 评论 - @mention 检测在 CI 逻辑之后,两条路径独立,不互斥
- 同一条评论可同时触发 CI 通知 + @mention 通知(如 CI 失败评论里同时 @了人)
S2 和 S4 共享同一个 handler:Gitea 中 PR 上的普通评论也是 issue_comment 事件。通过 issue 中是否包含 pull_request 字段判断是 PR 还是 Issue。
S3: PR body @mention — 改 _handle_pr_opened
改动点:PR opened 时检查 PR body 中的 @mention。
async def _handle_pr_opened(payload):
# ... 现有逻辑:review_request Mail → simayi-challenger ...
# 新增:PR body @mention 通知
pr = payload.get("pull_request", {})
body = pr.get("body", "")
sender = pr.get("user", {}).get("login", "")
mentions = extract_mentions(body, sender)
if mentions:
# 自动流转已通知 simayi-challenger(review_request)
auto_targets = ["simayi-challenger"]
await _send_mention_mails(
mentions=mentions,
auto_targets=auto_targets,
source_type="PR",
mention_type="PR @mention",
source_url=pr.get("html_url", ""),
commenter=sender,
content=body,
repo=_repo_fullname(payload),
issue_number=pr.get("number", 0),
is_pr=True,
)
自动流转互补分析:
- review_request Mail → simayi-challenger(自动)
- @mention Mail → 额外的人(如
@guanyu-dev 请审查风控) - 去重:simayi 如果在 PR body 被 @,不会收到两封 Mail
S5: Review body @mention — 改 _handle_pull_request_review
改动点:Review 提交时检查 review body 中的 @mention。
async def _handle_pull_request_review(payload):
review = payload.get("review", {})
pr = payload.get("pull_request", {})
review_body = review.get("body", "") or review.get("content", "")
reviewer = review.get("user", {}).get("login", "") or payload.get("sender", {}).get("login", "")
pr_author = pr.get("user", {}).get("login", "")
# ... 现有逻辑:review_result / review_comment → 通知 PR 作者 ...
# 新增:Review body @mention 通知
mentions = extract_mentions(review_body, reviewer)
if mentions:
# 自动流转已通知 PR 作者(review_result 或 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_fullname(payload),
issue_number=pr.get("number", 0),
is_pr=True,
)
自动流转互补分析:
- review_result/review_comment Mail → PR 作者(自动)
- @mention Mail → review 中额外 @的人
- 去重:PR 作者如果在 review body 被 @,不会收到两封 Mail
25.3.4 通用发送函数:_send_mention_mails
async def _send_mention_mails(
mentions: list[str],
auto_targets: list[str],
source_type: str,
mention_type: str,
source_url: str,
commenter: str,
content: str,
repo: str,
issue_number: int,
is_pr: bool,
) -> None:
"""通用 @mention Mail 发送函数。
自动抑制已在 auto_targets 中的 Agent,避免双重通知。
根据内容推断意图,生成不同的响应指引。
"""
# 确定 API 路径(S2:使用常量避免硬编码)
if is_pr:
detail_api = f"pulls/{issue_number}"
comments_api = f"issues/{issue_number}/comments"
else:
detail_api = f"issues/{issue_number}"
comments_api = f"issues/{issue_number}/comments"
for agent_id in mentions:
if should_suppress_mention(agent_id, auto_targets):
logger.info(
"Mention suppressed for %s (already notified by auto flow)",
agent_id)
continue
# 推断意图(S4:注释说明编号提取意图)
# 从 api_path 提取编号用于标题,如 "issues/32" → "#32"
number_str = f"#{issue_number}" if issue_number else ""
intent = infer_intent(content)
intent_hint = {"help": "求助", "notify": "通知关注",
"collaborate": "协作请求", "assign": "分配子任务"}[intent]
# 生成响应指引
guidance = _build_response_guidance(
intent=intent,
gitea_api=GITEA_API_BASE,
repo=repo,
issue_number=issue_number,
commenter=commenter,
)
text = render_template("mention", {
"mention_type": mention_type,
"source_type": source_type,
"source_url": source_url,
"commenter": commenter,
"intent_hint": intent_hint,
"content_snippet": content[:500],
"gitea_api": GITEA_API_BASE,
"repo": repo,
"source_detail_api_path": detail_api,
"source_comments_api_path": comments_api,
"response_guidance": guidance,
})
title = f"@mention ({intent_hint}): {source_type} {number_str} ({repo})"
_send_mail(agent_id, title, text)
def _build_response_guidance(
intent: str,
gitea_api: str,
repo: str,
issue_number: int,
commenter: str,
) -> str:
"""根据意图类型生成响应指引文本。"""
if intent == "help":
return (
f"这是一条求助,请到 Gitea 评论回复:\n"
f"1. 获取评论上下文(上方 API)\n"
f"2. 组织回答\n"
f"3. 在 Gitea 评论回复: POST {gitea_api}/repos/{repo}/issues/{issue_number}/comments\n"
f" Body: {{\"body\": \"你的回答内容\"}}"
)
elif intent == "notify":
return (
f"这是一条通知,请查看并知晓。如有意见,可到 Gitea 评论:\n"
f"- 查看 Issue/PR 详情(上方 API)\n"
f"- 如有意见,评论回复: POST {gitea_api}/repos/{repo}/issues/{issue_number}/comments"
)
elif intent == "collaborate":
return (
f"这是一条协作请求,请评估后回复(评论或 Mail):\n"
f"1. 获取详情(上方 API)\n"
f"2. 评估可行性\n"
f"3a. 评论回复: POST {gitea_api}/repos/{repo}/issues/{issue_number}/comments\n"
f" Body: {{\"body\": \"你的回复\"}}\n"
f"3b. 或通过 Mail 回复评论者: {commenter}"
)
elif intent == "assign":
return (
f"这是一条任务分配,请认领并执行:\n"
f"1. 获取 Issue 详情(上方 API)\n"
f"2. 评估可行性\n"
f"3. 认领 Issue: POST {gitea_api}/repos/{repo}/issues/{issue_number}/assignees\n"
f" Body: {{\"assignees\": [\"{{your_agent_id}}\"]}}\n"
f"4. 执行任务\n"
f"5. 完成后更新 Issue 状态: PATCH {gitea_api}/repos/{repo}/issues/{issue_number}\n"
f" Body: {{\"state\": \"closed\"}}"
)
return "请查看详情(上方 API)并按需回复。"
§25.4 幂等与防重复机制
五层去重保障
| 层 | 机制 | 覆盖场景 |
|---|---|---|
| L1: Webhook 投递去重 | _is_duplicate() — delivery UUID + 内容哈希 |
Gitea 重试、org+repo webhook 双投递 |
| L2: extract_mentions 内去重 | 返回 set,同一人 @多次只出现一次 |
@张飞 @zhangfei-dev 请看看 |
| L3: 自动流转抑制 | should_suppress_mention() — 自动已通知的人不再发 mention Mail |
simayi 同时是默认 reviewer + 被 @ |
| L4: Handler 内排重 | 各 handler 按事件粒度调用,同一事件只处理一次 | PR opened 不会触发两次 _handle_pr_opened |
| L5: 双事件去重(M3) | 只注册 issue_comment handler,不注册 pull_request_comment |
避免 PR 评论同时触发 issue_comment + pull_request_comment 导致重复 Mail |
L5 双事件去重详细说明(M3)
问题:Gitea 对 PR 上的普通评论会同时发出两个 webhook 事件:
issue_comment(is_pull=true)pull_request_comment
两个事件有不同的 delivery UUID,L1 去重拦不住。
解决方案:
_EVENT_HANDLERS中只注册issue_comment,不注册pull_request_comment- 因为 Gitea 对 PR 普通评论也发
issue_comment(可通过is_pull或 issue 中的pull_request字段判断) - 这样 PR 上的普通评论只会走一条路径,避免双重触发
# _EVENT_HANDLERS 中不出现这一行:
# "pull_request_comment": _handle_pull_request_comment, # ← 不要注册
不可去重的场景(正确行为)
| 场景 | 行为 | 原因 |
|---|---|---|
| 同一人在不同 PR 分别被 @ | 各发一封 | 不同上下文 |
| 同一人在同一 Issue 的不同评论被 @ | 各发一封 | 新评论有新信息 |
| PR 作者收到 review_result + 被 @ | 发两封(不同内容) | review_result 是结构化通知,mention 是额外关注 |
L3 抑制的详细规则
| 自动流转通知 | @mention 抑制条件 | 理由 |
|---|---|---|
| Issue assigned → assignee | Issue body @assignee → 抑制 | 同一事件,同一人 |
| PR opened → simayi (review_request) | PR body @simayi-challenger → 抑制 | 同一事件,同一人 |
| Review result → PR 作者 | Review body @PR作者 → 抑制 | 同一事件,同一人 |
| Review comment → PR 作者 | Review body @PR作者 → 抑制 | 同一事件,同一人 |
| CI 失败 → PR 作者 | Issue comment @PR作者(同一条评论)→ 抑制 | 同一事件,同一人 |
| PR merged → PR 作者 | 不涉及 @mention | merged 事件无 body |
| Deploy failure → jiangwei+pangtong | 不涉及 @mention | 由 Issue opened 触发 |
关键设计决策:抑制是按事件粒度的,不是全局的。即只有同一 Webhook 事件产生的自动通知才抑制 @mention。如果 PR 作者先收到 review_result(事件 A),之后在另一条评论中被 @(事件 B),两封 Mail 都会发——这是正确的,因为它们是不同事件。
§25.5 _EVENT_HANDLERS 变更
结论:不需要修改 _EVENT_HANDLERS 映射表。
@mention 处理是各现有 handler 的内部逻辑扩展:
| Handler | 变更 | 新事件类型? |
|---|---|---|
_handle_issues |
opened → 增加 body @mention 检测 | 否 |
_handle_issue_comment |
created → 控制流重构 + 增加 comment body @mention 检测 | 否 |
_handle_pr_opened(在 _handle_pull_request 内) |
opened → 增加 PR body @mention 检测 | 否 |
_handle_pull_request_review |
submitted → 增加 review body @mention 检测 | 否 |
_handle_pr_synchronize |
无变更(synchronize 事件无 body) | 否 |
_handle_pr_closed |
无变更(merged 事件无 body) | 否 |
注意:不注册 pull_request_comment handler,避免 S2/S4 双事件触发(详见 §25.4 L5)。
§25.6 改动范围
| # | 文件 | 改动内容 | 风险 |
|---|---|---|---|
| 1 | 新建 src/api/mention_utils.py |
extract_mentions() + should_suppress_mention() + infer_intent() + AGENT_ALIAS + GITEA_API_BASE 常量 |
低(新文件,独立模块) |
| 2 | 新建 templates/toolchain/mention.md |
通用 @mention 通知模板(含响应指引) | 低(新文件) |
| 3 | src/api/toolchain_routes.py |
各 handler 增加 @mention 检测 + _handle_issue_comment 控制流重构 + _send_mention_mails() + _build_response_guidance() |
中(修改现有 handler 控制流) |
| 4 | src/daemon/toolchain_templates.py |
_TEMPLATE_MAP 新增 "mention" 映射 |
低 |
| 5 | 不改 | _EVENT_HANDLERS 映射(不注册 pull_request_comment) |
— |
| 6 | 不改 | 幂等检查机制 | — |
| 7 | 不改 | 现有模板文件 | — |
| 8 | 不改 | toolchain_handler.py(daemon 侧) |
— |
§25.7 验证方案
7.1 单元测试
| 测试用例 | 验证点 |
|---|---|
extract_mentions("@zhangfei-dev") |
精确匹配 |
extract_mentions("@张飞") |
中文名别名 |
extract_mentions("@zhangfei") |
英文短名 |
extract_mentions("@翼德") |
字号别名 |
extract_mentions("@zhangf") |
前缀模糊匹配(唯一) |
extract_mentions("@zh") |
前缀匹配多个候选 → 不匹配,不抛异常 |
extract_mentions("@zhangfei-dev @张飞") |
去重(同一人只出现一次) |
extract_mentions("sender=zhangfei-dev, body=@zhangfei-dev") |
排除自己 |
extract_mentions("@unknown-person") |
非 Agent 忽略 |
should_suppress_mention("simayi-challenger", ["simayi-challenger"]) |
抑制生效 |
should_suppress_mention("zhangfei-dev", ["simayi-challenger"]) |
不抑制 |
infer_intent("数据格式是什么?") |
识别为"求助" |
infer_intent("这个 PR 涉及风控变更") |
识别为"通知关注" |
infer_intent("请帮忙澄清需求") |
识别为"协作请求" |
infer_intent("前端部分交给你") |
识别为"分配子任务" |
7.2 E2E 验证场景
| # | 场景 | 操作 | 预期 |
|---|---|---|---|
| E2E-1 | Issue body @mention | 创建 Issue body 含 @赵云 请提供数据字典,指派张飞 |
张飞收到 assigned Mail;赵云收到 @mention Mail(含"求助"响应指引) |
| E2E-2 | Issue comment @mention | 在 Issue 下评论 @pangtong-fujunshi 需求不明确 |
庞统收到 @mention Mail(含"协作请求"响应指引) |
| E2E-3 | PR body @mention | 创建 PR body 含 @guanyu-dev 风控审查 |
关羽收到 @mention Mail;司马懿照收 review_request |
| E2E-4 | PR body @simayi 去重 | 创建 PR body 含 @simayi-challenger 请审查 |
司马懿只收 1 封 Mail(review_request) |
| E2E-5 | Review body @mention | Review 驳回 body 含 @pangtong-fujunshi 设计有问题 |
庞统收到 @mention Mail;PR 作者收到 review_result(不重复) |
| E2E-6 | Review body @PR作者 去重 | Review body 含 @zhangfei-dev 请修改 |
张飞只收 1 封 Mail(review_result) |
| E2E-7 | CI 失败 + @mention 同评论 | CI 失败评论同时含 @pangtong-fujunshi |
PR 作者收到 CI 失败 Mail;庞统收到 @mention Mail |
| E2E-8 | PR 上评论 @mention | 在 PR 页面评论 @zhaoyun-data 数据源是什么? |
赵云收到 @mention Mail(只触发一次,非双重) |
| E2E-9 | 非 CI 普通评论 @mention | 在 PR 下评论 @zhangfei-dev 请注意边界检查(非 CI 评论) |
张飞收到 @mention Mail(验证重构后非 CI 评论不再被 return 丢弃) |
| E2E-10 | 前缀模糊多候选 | 评论 @zh(假设 zh 开头有多个 Agent) |
不匹配,不发 Mail,日志有 warning |
§25.8 不做的事
| 标记 | 描述 | 原因 |
|---|---|---|
| 后续-1 | Gitea 不支持 commit_comment webhook 事件(姜维已确认源码),搁置 |
|
| 后续-2 | Issue edited 时 body @mention |
编辑 Issue 较少见,且可能产生重复通知(首次 opened 已发过),暂不处理 |
| 后续-3 | @mention 触发自动操作(如自动 assign、自动 add reviewer) | 当前只做通知,不做自动化操作。自动化操作需要更复杂的权限和去重逻辑 |
| 后续-4 | @mention 权限控制(谁能 @谁) | 团队规模小,暂不需要 |
| 后续-5 | @mention 在 Mail 中的双向回复 | 被通知者按模板中的响应指引操作(去 Gitea 评论或发 Mail),不支持 Mail 直接回复 |
§25.9 变更记录
| 日期 | 版本 | 变更 |
|---|---|---|
| 2026-06-12 | v1.0 | 初版:6 场景遍历 + 端到端流程 + 幂等去重 + 和自动流转互补分析 |
| 2026-06-12 | v2.0 | 修订版(Review 反馈整合):M1 闭环链路 + M2 控制流重构 + M3 双事件去重 + M4 删除 commit_comment + M5 编号改为 §25 + S1 多候选不匹配 + S2 常量化 + S3 删除 edited 分支 + S4 注释说明 + S5 假设文档化 |