Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d24183c14 | |||
| 7b32994c75 | |||
| 40efa1c623 | |||
| f4fc941bd1 | |||
| c6a0567161 | |||
| 3f5b3619c8 | |||
| e9bbcf41c9 | |||
| 2c612baa04 | |||
| 98d17292b0 |
@@ -838,7 +838,7 @@ Agent spawn 走生产 openclaw 的决策理由:
|
||||
| CI 标准门控 → 代码审查 | **CI 通过后 daemon Webhook 转发 Mail 给审查者** | Mail 通知司马懿 Review | Gitea Webhook `pull_request` → daemon Webhook 模块 → Mail API |
|
||||
| 代码审查 → 修改(不通过) | 审查者提交 Review 意见 | daemon Webhook 转发 Mail 通知改动者(附 Review 摘要) | Gitea Webhook `pull_request_review` → daemon Webhook 模块 → Mail API |
|
||||
| 代码审查 → Merge(通过) | **审查者点 Approve** | daemon Webhook 转发 Mail 通知改动者 merge | Gitea Webhook `pull_request_review` → daemon Webhook 模块 → Mail API |
|
||||
| Merge → 部署 | **merge 到 main 自动触发** | 无需通知(自动化) | Gitea Actions `on: push: branches: [main]` |
|
||||
| Merge → 部署 | **merge 到 main 自动触发** | Mail 通知 PR 作者合并完成(PR #38) | Gitea Actions `on: push: branches: [main]` |
|
||||
| 部署 → E2E | **部署 job 成功后触发 E2E job** | E2E 结果评论到 merge commit | Gitea Actions `needs: [deploy]` |
|
||||
| E2E/部署 → Issue关闭 | 庞统或改动者手动确认后关闭 | Issue 关闭通知关注者 | Gitea API `PATCH /repos/{owner}/{repo}/issues/{id}` state=closed |
|
||||
| CI失败 → Issue评论 | **CI 失败自动评论** → daemon Webhook 转发 Mail 通知改动者 | 评论到关联 Issue + Mail 推送 Agent | Gitea Actions `if: failure()` 写 PR评论 → daemon Webhook 监听 `issue_comment` → Mail |
|
||||
@@ -1303,7 +1303,7 @@ Layer 3: Mail 执行层(Agent 接口)
|
||||
| `push` | 代码推送 | commit hash, 分支, 作者 | 不需要转发(Actions 自动处理) |
|
||||
| `pull_request` (opened) | PR 创建 | PR ID, 标题, 分支, 作者 | → Mail 通知司马懿 Review |
|
||||
| `pull_request_review` (submitted) | Review 提交 | PR ID, 审查者, 结论(APPROVE/REQUEST_CHANGES), 评论 | → Mail 通知张飞 Review 结果 |
|
||||
| `pull_request` (closed/merged) | PR 合并 | PR ID, 合并 commit | 不需要转发(Actions 自动触发 deploy) |
|
||||
| `pull_request` (closed/merged) | PR 合并 | PR ID, 合并 commit | Mail 通知 PR 作者合并完成(PR #38 恢复) |
|
||||
| `issue_comment` | PR/Issue 评论 | 评论者, 内容 | CI workflow 写的失败评论 → 转发 Mail |
|
||||
| `issues` (opened+assigned) | Issue 创建/指派 | Issue ID, 标题, 被指派人 | → Mail 通知开发者 |
|
||||
| `release` | Release 创建 | tag, 名称 | 触发完整 CI+部署 |
|
||||
@@ -1336,8 +1336,8 @@ async def handle_gitea_webhook(event: dict, x_gitea_event: str = Header(...), x_
|
||||
pr_author = to_agent_id(event["pull_request"]["user"]["login"])
|
||||
await send_mail(to="simayi-challenger", title=f"Review 请求: PR #{event['number']}", ...)
|
||||
elif action == "closed" and event["pull_request"]["merged"]:
|
||||
# merge 不需要通知,Actions 自动处理
|
||||
pass
|
||||
# PR #38: 通知 PR 作者合并完成
|
||||
await _handle_pr_closed(event)
|
||||
|
||||
elif x_gitea_event == "pull_request_review":
|
||||
state = event["review"]["state"]
|
||||
|
||||
@@ -119,3 +119,28 @@
|
||||
- 姜维第一次分析给出了错误根因(Gitea 双 notifier),第二次深入调查后自我纠正
|
||||
- 庞统把姜维的第一次结论当事实汇报给主公,没有标注"这是姜维的调查结论,尚未独立验证"
|
||||
- **改进**:SOUL.md 新增规则——推测 vs 事实显式标注、引用他人结论时标注来源、结论被推翻时及时更正
|
||||
|
||||
---
|
||||
|
||||
## PR #38 新增场景(synchronize fallback + merge 通知)
|
||||
|
||||
> 2026-06-12 新增,对应 PR #38 的设计变更
|
||||
|
||||
### 步骤 9:synchronize fallback ✅
|
||||
- 操作:创建 PR(无 review 历史)→ push 新 commit 到 PR 分支
|
||||
- 触发事件:`pull_request` (synchronize)
|
||||
- 预期:`simayi-challenger`(默认 reviewer)收到"请重新 review" Mail
|
||||
- 验证点:
|
||||
- PR 无 review 历史时,`_fetch_latest_reviewer()` 返回 None → fallback 到 `simayi-challenger`
|
||||
- Mail to 正确(默认 reviewer 而非跳过通知)
|
||||
- 模板使用 `review_updated.md`
|
||||
|
||||
### 步骤 10:merge 通知 ✅
|
||||
- 操作:PR 通过 Review 后 merge
|
||||
- 触发事件:`pull_request` (closed) + `merged=true`
|
||||
- 预期:PR 作者收到"PR 已合并" Mail
|
||||
- 验证点:
|
||||
- Mail to 正确(PR 作者)
|
||||
- `merged_by` 字段正确提取(payload `merged_by` → fallback `sender`)
|
||||
- 模板使用 `review_merged.md`
|
||||
- 纯 closed(非 merged)不触发通知
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|---|---|---|---|---|
|
||||
| E1 | PR 更新(push 新 commit)→ 通知 reviewer | `pull_request.synchronize` | **高** | review 驳回→修改→重 review 的关键闭环 |
|
||||
| ~~E2~~ | ~~PR 合并通知~~ | ~~已删除~~ | ~~—~~ | ~~和 §22 CD 成功通知重叠,已删~~ |
|
||||
| E2 | PR 合并 → 通知 PR 作者 | `pull_request` (closed+merged) | **高** | PR #38 恢复:CD 通知语义不同(部署状态 vs 合并信息),文档 PR 无 CD 流程仍需通知 |
|
||||
| E3 | Review 评论(COMMENTED)→ 通知 PR 作者 | `pull_request_review` (COMMENTED) | 中 | reviewer 讨论提问,作者应知道 |
|
||||
| E4 | PR 上普通评论 → 通知相关人 | `issue_comment` (on PR) | 低 | 非关键路径 |
|
||||
|
||||
@@ -64,17 +65,42 @@ async def _handle_pull_request(payload: Dict[str, Any]) -> None:
|
||||
新增 `_handle_pr_synchronize`:
|
||||
1. 从 payload 取 PR 信息(number、title、author、head sha)
|
||||
2. 查询最近一次 review(Gitea API `GET /repos/{owner}/{repo}/pulls/{number}/reviews`)取 reviewer
|
||||
3. 如果没有 review 记录(首次 push 后 reviewer 还没 review),跳过(opened 事件已经通知过了)
|
||||
3. 如果没有 review 记录(`_fetch_latest_reviewer()` 返回 None),fallback 到默认 reviewer `simayi-challenger`,而非跳过通知(PR #38 改动:确保无 review 历史时也能通知默认审查者)
|
||||
4. 渲染 `review_updated.md` 模板,发送 Mail 给 reviewer
|
||||
|
||||
**关键设计决策**:
|
||||
- 不用 `requested_reviewers`(可能为空),用最近 review 的提交者
|
||||
- 只在有 review 历史时才通知(避免 opened + synchronize 重复通知)
|
||||
- 无 review 历史时 fallback 到默认 reviewer `simayi-challenger`(PR #38:避免 opened + synchronize 间隔较短时 reviewer 未收到任何通知)
|
||||
- Mail from 用 `system`
|
||||
|
||||
### ~~Handler 2:PR 合并通知~~ — 已删除
|
||||
### Handler 2:`_handle_pr_closed`(PR 合并通知)— PR #38 恢复
|
||||
|
||||
> 司马懿 review 指出与 §22 CD 成功通知重叠。CD 成功通知已隐含合并信息,无需单独发 merged 通知。
|
||||
**触发**:`pull_request` 事件 + `action=closed` + `merged=true`
|
||||
|
||||
**通知对象**:PR 作者
|
||||
|
||||
**实现**:
|
||||
|
||||
修改 `_handle_pull_request` 的 action 分发,新增 `closed` 分支:
|
||||
|
||||
```python
|
||||
async def _handle_pull_request(payload: Dict[str, Any]) -> None:
|
||||
action = payload.get("action", "")
|
||||
if action == "opened":
|
||||
await _handle_pr_opened(payload)
|
||||
elif action == "synchronize":
|
||||
await _handle_pr_synchronize(payload)
|
||||
elif action == "closed" and payload.get("pull_request", {}).get("merged"):
|
||||
await _handle_pr_closed(payload)
|
||||
# 其他 action 忽略
|
||||
```
|
||||
|
||||
新增 `_handle_pr_closed`:
|
||||
1. 从 payload 取 PR 信息(number、title、merged_by)
|
||||
2. `merged_by` 优先从 `payload["pull_request"]["merged_by"]` 取,若为空则 fallback 到 `payload["sender"]`(PR #38:兼容不同 Gitea 版本和 merge 方式)
|
||||
3. 渲染 `review_merged.md` 模板,发送 Mail 给 PR 作者
|
||||
|
||||
**恢复说明**:此前因与 §22 CD 成功通知重叠而删除。但实际场景中 CD 通知发的是部署状态,PR 作者更关心的是"谁帮我 merge 了"这个信息,两者语义不同。且 CD 流程不一定每次都触发(如文档 PR),merge 通知仍需独立存在。(PR #38 恢复)
|
||||
|
||||
### 新增 Handler 3:review COMMENTED 处理
|
||||
|
||||
@@ -101,7 +127,7 @@ async def _handle_pull_request(payload: Dict[str, Any]) -> None:
|
||||
| 模板文件 | 变量 | 说明 |
|
||||
|---|---|---|
|
||||
| `review_updated.md` | repo, pr_number, pr_title, pr_author, branch, new_sha, reviewer | PR 有新 commit,请重新 review |
|
||||
| ~~`pr_merged.md`~~ | ~~已删除~~ | ~~—~~ |
|
||||
| `review_merged.md` | repo, pr_number, pr_title, pr_author, merged_by | PR 已合并,通知作者(PR #38 恢复) |
|
||||
| `review_comment.md` | repo, pr_number, pr_title, reviewer, comment_body | reviewer 提交了评论 |
|
||||
|
||||
### `_EVENT_HANDLERS` 无需改动
|
||||
@@ -122,9 +148,9 @@ async def _handle_pull_request(payload: Dict[str, Any]) -> None:
|
||||
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `src/api/toolchain_routes.py` | 修改 `_handle_pull_request`(扩展 action 分发)+ 新增 `_handle_pr_synchronize` + 修改 `_handle_pull_request_review`(支持 COMMENTED) |
|
||||
| `src/api/toolchain_routes.py` | 修改 `_handle_pull_request`(扩展 action 分发 + closed 分支)+ 新增 `_handle_pr_synchronize` + `_handle_pr_closed` + 修改 `_handle_pull_request_review`(支持 COMMENTED) |
|
||||
| `templates/toolchain/review_updated.md` | 新增 |
|
||||
| ~~`templates/toolchain/pr_merged.md`~~ | ~~已删除~~ |
|
||||
| `templates/toolchain/review_merged.md` | 新增(PR #38 恢复) |
|
||||
| `templates/toolchain/review_comment.md` | 新增 |
|
||||
| `src/daemon/toolchain_templates.py` | `_TEMPLATE_MAP` 新增 3 个映射 |
|
||||
| `docs/design/23-toolchain-pr-lifecycle.md` | 本文档 |
|
||||
@@ -134,8 +160,9 @@ async def _handle_pull_request(payload: Dict[str, Any]) -> None:
|
||||
在 `sanguo/moziplus-v2` 测试仓库上 E2E 验证:
|
||||
|
||||
1. **synchronize**:创建 PR → review 驳回 → push 新 commit → 验证 reviewer 收到"请重新 review" Mail
|
||||
~~2. merged~~:已删除
|
||||
3. **COMMENTED**:review 提交纯评论 → 验证 PR 作者收到通知
|
||||
2. **synchronize fallback**(PR #38):创建 PR → push commit(无 review 历史)→ 验证默认 reviewer (`simayi-challenger`) 收到通知
|
||||
3. **merge 通知**(PR #38 恢复):PR merge → 验证 PR 作者收到合并通知 Mail
|
||||
4. **COMMENTED**:review 提交纯评论 → 验证 PR 作者收到通知
|
||||
|
||||
## 风险评估
|
||||
|
||||
|
||||
@@ -259,13 +259,12 @@ def _repo_fullname(payload: Dict[str, Any]) -> str:
|
||||
|
||||
|
||||
async def _handle_pull_request(payload: Dict[str, Any]) -> None:
|
||||
"""处理 pull_request 事件:opened → 通知 reviewer;synchronize → 通知 reviewer 重新 review。"""
|
||||
"""处理 pull_request 事件:opened → 通知 reviewer;closed → merge 通知。"""
|
||||
action = payload.get("action", "")
|
||||
if action == "opened":
|
||||
await _handle_pr_opened(payload)
|
||||
elif action == "synchronize":
|
||||
await _handle_pr_synchronize(payload)
|
||||
# 其他 action 忽略
|
||||
elif action == "closed":
|
||||
await _handle_pr_closed(payload)
|
||||
|
||||
|
||||
async def _handle_pr_opened(payload: Dict[str, Any]) -> None:
|
||||
@@ -434,9 +433,9 @@ async def _handle_pr_synchronize(payload: Dict[str, Any]) -> None:
|
||||
# 查询最近 review 的提交者
|
||||
reviewer = await _fetch_latest_reviewer(repo, pr_number)
|
||||
if not reviewer:
|
||||
# 没有 review 历史,跳过(opened 事件已经通知过)
|
||||
logger.debug("No review history for PR #%s, skipping synchronize notification", pr_number)
|
||||
return
|
||||
# 没有已有 review 历史,fallback 到默认 reviewer
|
||||
reviewer = "simayi-challenger"
|
||||
logger.info("No review history for PR #%s, using default reviewer %s", pr_number, reviewer)
|
||||
|
||||
text = render_template("review_updated", {
|
||||
"repo": repo,
|
||||
@@ -451,6 +450,38 @@ async def _handle_pr_synchronize(payload: Dict[str, Any]) -> None:
|
||||
_send_mail(reviewer, title, text)
|
||||
|
||||
|
||||
async def _handle_pr_closed(payload: Dict[str, Any]) -> None:
|
||||
"""PR closed → 如果 merged,通知 PR 作者。"""
|
||||
pr = payload.get("pull_request")
|
||||
if not pr or not isinstance(pr, dict):
|
||||
return
|
||||
|
||||
# 只处理 merged 的 PR
|
||||
if not pr.get("merged", False):
|
||||
return
|
||||
|
||||
repo = _repo_fullname(payload)
|
||||
pr_number = pr.get("number", 0)
|
||||
pr_title = pr.get("title", "")
|
||||
pr_author = pr.get("user", {}).get("login", "unknown")
|
||||
# merged_by 可能不在 payload 中,fallback 到 sender
|
||||
merged_by = (
|
||||
pr.get("merged_by", {}).get("login", "")
|
||||
or payload.get("sender", {}).get("login", "unknown")
|
||||
)
|
||||
|
||||
text = render_template("review_merged", {
|
||||
"repo": repo,
|
||||
"pr_number": str(pr_number),
|
||||
"pr_title": pr_title,
|
||||
"pr_author": pr_author,
|
||||
"merged_by": merged_by,
|
||||
})
|
||||
|
||||
title = f"PR 已合并: {pr_title} ({repo}#{pr_number})"
|
||||
_send_mail(pr_author, title, text)
|
||||
|
||||
|
||||
async def _handle_issues(payload: Dict[str, Any]) -> None:
|
||||
"""处理 issues 事件:assigned → 通知被指派人;opened+部署失败 → 通知运维。"""
|
||||
action = payload.get("action", "")
|
||||
@@ -562,10 +593,12 @@ async def _handle_issue_comment(payload: Dict[str, Any]) -> None:
|
||||
|
||||
_EVENT_HANDLERS: Dict[str, Any] = {
|
||||
"pull_request": _handle_pull_request,
|
||||
"pull_request_sync": _handle_pr_synchronize, # Gitea: PR branch push 是独立事件类型
|
||||
"pull_request_review": _handle_pull_request_review,
|
||||
"pull_request_review_approved": _handle_pull_request_review,
|
||||
"pull_request_review_rejected": _handle_pull_request_review,
|
||||
"pull_request_review_comment": _handle_pull_request_review,
|
||||
"pull_request_comment": _handle_pull_request_review, # Gitea: review comment 独立事件类型
|
||||
# Gitea v1.23.4 实际发出的 review 子事件(无 _review_ 中间段)
|
||||
"pull_request_approved": _handle_pull_request_review,
|
||||
"pull_request_rejected": _handle_pull_request_review,
|
||||
@@ -620,9 +653,11 @@ async def gitea_webhook(
|
||||
return Response(status_code=200, content="duplicate")
|
||||
|
||||
# 4. 查找 handler
|
||||
action = payload.get("action", "")
|
||||
logger.info("[WEBHOOK] event=%s action=%s delivery=%s", x_gitea_event, action, x_gitea_delivery)
|
||||
handler = _EVENT_HANDLERS.get(x_gitea_event or "")
|
||||
if not handler:
|
||||
logger.debug("Unhandled event type: %s", x_gitea_event)
|
||||
logger.info("[WEBHOOK] Unhandled event type: %s", x_gitea_event)
|
||||
return Response(status_code=200,
|
||||
content=f"unhandled event: {x_gitea_event}")
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ _TEMPLATE_MAP: Dict[str, str] = {
|
||||
"deploy_failure": "deploy_failure.md",
|
||||
"review_updated": "review_updated.md",
|
||||
"review_comment": "review_comment.md",
|
||||
"review_merged": "review_merged.md",
|
||||
}
|
||||
|
||||
# 模板缓存
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
## PR 已合并 ✅
|
||||
|
||||
**仓库**: {repo}
|
||||
**PR #{pr_number}**: {pr_title}
|
||||
**作者**: @{pr_author}
|
||||
**合并者**: @{merged_by}
|
||||
|
||||
PR 已成功合并到主分支。
|
||||
Reference in New Issue
Block a user