Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a6a0e6e849 | |||
| 00383ad079 | |||
| 0318346b85 | |||
| cb0b35a689 | |||
| c4f615ce7f | |||
| a72e08403f |
@@ -1,6 +1,6 @@
|
||||
# 三国团队工具链与开发流程设计
|
||||
|
||||
> **状态**: v3.1 — P3 端到端验证通过 + 调研结论写入 + Review API 枚举值修正
|
||||
> **状态**: v3.3 — #19 上下文四层改造合并 + CI 修复 + A13 修订
|
||||
> **作者**: 庞统(副军师)🐦
|
||||
> **评审**: 司马懿(仲达)🗡️
|
||||
> **日期**: 2026-06-06
|
||||
@@ -2765,3 +2765,378 @@ Gitea v1.23.4 自带完整的 CI 管理界面:
|
||||
| §16.8 #10 | Gitea v1.23.4 review payload 调研结论(姜维 2026-06-08):Gitea v1.23.4 review payload 只有 `type` + `content`,没有 `state`/`body`/`user`,这不是 org vs repo 差异而是 Gitea 设计。v1.24.0 格式不变。双格式兼容是防御性编码,保持现状 |
|
||||
| §16.8 #11 | Spawner compact 检测窗口修复:窗口 300s→900s,尾部读取 50KB→1MB。实测长对话中 compact 记录被推出窗口导致漏检 |
|
||||
| §16.8 #12 | inform 类型 Mail crash 误标 done bug 修复:`_mail_auto_complete` 增加 outcome 感知,inform 用白名单(completed/claimed/no_reply)控制 done 标记。spawner crash cooldown 300s→60s |
|
||||
|
||||
---
|
||||
|
||||
### 一、问题诊断
|
||||
|
||||
#### 1.1 E2E 真实场景测试暴露的三个断层
|
||||
|
||||
主公在 moziplus-v2 仓库创建了 Issue #32(添加 /api/stats 端点),指派张飞。链条在第一步就断了。
|
||||
|
||||
| 断层 | 现象 | 根因 |
|
||||
|------|------|------|
|
||||
| **Agent 不知道该做什么** | 张飞收到 Issue 指派 Mail,回复"已阅"就结束了 | Mail 模板(issue_assigned.md)5 行信息,无流程引导;spawn prompt 说"已阅即可" |
|
||||
| **Agent 去错了仓库** | 张飞去读了 sanguo_moziplus_v2 平台代码,而不是空的实验仓库 moziplus-v2 | Mail 模板没有仓库 clone URL,张飞凭习惯去了开发目录 |
|
||||
| **Agent 在 Control UI 提问** | 张飞遇到问题直接在 Control UI 问主公,没有去 Gitea Issue 评论 | 没有任何地方引导"有疑问去 Gitea Issue 评论" |
|
||||
| **Agent 不知道怎么协作** | 张飞判断任务需要澄清,但不知道该怎么请求澄清 | 没有"做不了→在 Issue 评论 / Mail 庞统"的回退路径 |
|
||||
| **跨 Agent @mention 无法通知** | 张飞在 Issue 评论 @赵云,赵云收不到通知 | issue_comment handler 只处理 [CI] 评论,@mention 被忽略 |
|
||||
|
||||
#### 1.2 根因:工具链在四层架构中的断层
|
||||
|
||||
| 层 | 应该有 | 实际有 | Gap |
|
||||
|---|---|---|---|
|
||||
| **L0 铁律** | — | — | 无需改动 |
|
||||
| **L1 角色** | 工具链协作行为规范(所有 Agent 共享) | 无 | AGENTS.md 没有工具链相关内容 |
|
||||
| **L2 引擎注入** | 事件上下文(仓库 clone URL、Gitea API、Issue/PR 详情) | Mail 模板只有 5 行摘要 | 缺仓库信息和流程引导 |
|
||||
| **L3 被动参考** | 技术细节(分支命名、commit 规范、PR 创建方式) | git-workflow 等 Skill 已存在但没人触发 | Agent 不知道该加载哪个 Skill |
|
||||
|
||||
---
|
||||
|
||||
### 二、改造方案:四层归属
|
||||
|
||||
#### 2.1 分层原则
|
||||
|
||||
| 层 | 放什么 | 不放什么 | 理由 |
|
||||
|---|---|---|---|
|
||||
| **L0** | 不放 | — | 工具链不是安全底线 |
|
||||
| **L1** | 协作行为规范:收到什么通知该做什么、遇到问题怎么办 | 技术细节(分支命名、commit 格式) | 行为规范是团队常识,每个 Agent 都要知道 |
|
||||
| **L2** | 事件上下文:仓库 clone URL、Gitea API URL、Issue/PR 链接、动态信息 | 固定的协作流程 | 动态信息每次不同,由 Mail 模板 + spawn 时注入 |
|
||||
| **L3** | 技术细节:git-workflow、code-review 等 Skill 全文 | — | 按需加载,Agent 知道"我要提 PR"后自己读 |
|
||||
|
||||
#### 2.2 各层具体内容
|
||||
|
||||
##### L1:AGENTS.md 加工具链协作行为段(所有 Agent 统一)
|
||||
|
||||
```markdown
|
||||
## 工具链协作(Gitea)
|
||||
|
||||
收到 Gitea 事件通知(Issue 指派、Review 请求、CI 失败等)时,按以下流程操作:
|
||||
|
||||
### 基本流程
|
||||
- **Issue 指派** → clone 仓库 → 开分支 → 编码 → 提 PR(参考 git-workflow Skill)
|
||||
- **Review 请求** → 读 PR diff(Gitea API)→ 提交 Review(参考 code-review Skill)
|
||||
- **Review 通过** → 等 merge
|
||||
- **Review 驳回** → 看 review body → 修代码 → 重新 push
|
||||
- **CI 失败** → 看错误摘要 → 修代码 → push(自动重触发 CI)
|
||||
- **部署失败** → 查 deploy 日志 → 修复
|
||||
|
||||
### 协作规则
|
||||
- **有疑问?** 在 Gitea Issue 下评论,不要在 Control UI 或 Mail 里问
|
||||
- **需要别人帮忙?** 在 Issue 评论中 @mention 对应 Agent(如 @zhaoyun-data)
|
||||
- **做不了?** 回复 Mail 说明原因和建议的接手人
|
||||
- **获取完整上下文** → 用 Gitea API 拉取 Issue 详情和评论,不要只看 Mail 里的快照
|
||||
|
||||
### Gitea API 速查
|
||||
> 其中 `{owner}/{repo}` 替换为实际仓库,如 `sanguo/sanguo_moziplus_v2`
|
||||
- Issue 详情: GET /api/v1/repos/{owner}/{repo}/issues/{number}
|
||||
- Issue 评论: GET /api/v1/repos/{owner}/{repo}/issues/{number}/comments
|
||||
- PR diff: GET /api/v1/repos/{owner}/{repo}/pulls/{number}.diff
|
||||
- 提交 Review: POST /api/v1/repos/{owner}/{repo}/pulls/{number}/reviews
|
||||
```
|
||||
|
||||
**改动范围**:6 个 Agent 的 AGENTS.md 各加一段(内容统一)。
|
||||
|
||||
##### L2:Mail 模板精简 + 事件上下文注入
|
||||
|
||||
**原则**:模板只放摘要 + 链接 + 仓库信息,不写固定步骤(步骤在 L1)。
|
||||
|
||||
**issue_assigned.md** 改为:
|
||||
|
||||
```markdown
|
||||
Issue 指派
|
||||
|
||||
Issue: {issue_url}
|
||||
标题: {issue_title}
|
||||
标签: {labels}
|
||||
|
||||
📋 获取完整上下文(先读再动手):
|
||||
- Issue 详情: GET {gitea_api}/repos/{repo}/issues/{issue_number}
|
||||
- Issue 评论: GET {gitea_api}/repos/{repo}/issues/{issue_number}/comments
|
||||
|
||||
仓库: {repo_clone_url}
|
||||
建议分支: feat/issue-{issue_number}-{brief}
|
||||
```
|
||||
|
||||
**review_request.md** 改为:
|
||||
|
||||
```markdown
|
||||
PR Review 请求
|
||||
|
||||
PR: {pr_url}
|
||||
标题: {pr_title}
|
||||
作者: {pr_author}
|
||||
分支: {branch}
|
||||
风险级别: {risk_level}
|
||||
|
||||
📋 获取完整上下文:
|
||||
- PR diff: GET {gitea_api}/repos/{repo}/pulls/{pr_number}.diff
|
||||
- PR 文件列表: GET {gitea_api}/repos/{repo}/pulls/{pr_number}/files
|
||||
```
|
||||
|
||||
**review_result.md** 改为:
|
||||
|
||||
```markdown
|
||||
Review {result}
|
||||
|
||||
PR: {pr_url}
|
||||
标题: {pr_title}
|
||||
审查者: {reviewer}
|
||||
|
||||
{review_body}
|
||||
```
|
||||
|
||||
**ci_failure.md** 改为:
|
||||
|
||||
```markdown
|
||||
CI 失败
|
||||
|
||||
Issue: {issue_url}
|
||||
分支: {branch}
|
||||
|
||||
错误摘要:
|
||||
{error_summary}
|
||||
|
||||
📋 CI 日志: {gitea_url}/{repo}/actions
|
||||
修复后 push 会自动重触发 CI。
|
||||
```
|
||||
|
||||
**deploy_failure.md** 改为:
|
||||
|
||||
```markdown
|
||||
部署失败
|
||||
|
||||
仓库: {repo}
|
||||
Commit: {commit_sha}
|
||||
|
||||
📋 排查步骤:
|
||||
- CI 日志: {gitea_url}/{repo}/actions
|
||||
- 服务器: pm2 logs {service_name}
|
||||
```
|
||||
|
||||
**L2 代码改动**(`toolchain_routes.py`):
|
||||
|
||||
1. 从 Webhook payload 的 `repository` 对象提取 `clone_url` 和 `html_url`
|
||||
2. `render_template()` 传入新变量:`gitea_api`、`gitea_url`、`repo_clone_url`
|
||||
3. 所有模板变量统一补齐
|
||||
|
||||
##### L3:Skill 按需加载(不改 Skill 本身)
|
||||
|
||||
git-workflow、code-review 等 Skill 保持不变。
|
||||
|
||||
L1 的协作行为段里会引用 Skill 名称("参考 git-workflow Skill"),Agent 收到 Mail 后根据 L1 的引导自主加载对应 Skill。
|
||||
|
||||
**不改 Skill 路由机制**——靠 L1 的文案触发 Agent 的 Skill 路由器匹配。
|
||||
|
||||
---
|
||||
|
||||
### 三、新增功能:issue_comment @mention 通知
|
||||
|
||||
#### 3.1 设计
|
||||
|
||||
当前 `_handle_issue_comment` 只处理 `[CI]` 前缀评论。扩展为:
|
||||
|
||||
```
|
||||
issue_comment 事件
|
||||
├── 含 [CI] / CI 失败 → 原有 CI 失败通知逻辑
|
||||
└── 含 @username → 解析 @mention → Mail 通知被 @的 Agent
|
||||
```
|
||||
|
||||
#### 3.2 实现
|
||||
|
||||
**`toolchain_routes.py` 新增 `_handle_issue_comment_mention()`**:
|
||||
|
||||
```python
|
||||
AGENT_IDS = {
|
||||
"zhangfei-dev", "guanyu-dev", "zhaoyun-data",
|
||||
"jiangwei-infra", "simayi-challenger", "pangtong-fujunshi",
|
||||
}
|
||||
|
||||
# 前缀映射:@张飞 → zhangfei-dev
|
||||
# 中文名映射:Agent 在 Gitea Issue 评论中可能用中文名 @mention
|
||||
# 英文短名映射:Agent 可能用不带 -dev/-infra 后缀的短名
|
||||
AGENT_ALIAS = {
|
||||
"张飞": "zhangfei-dev",
|
||||
"关羽": "guanyu-dev",
|
||||
"赵云": "zhaoyun-data",
|
||||
"姜维": "jiangwei-infra",
|
||||
"司马懿": "simayi-challenger",
|
||||
"庞统": "pangtong-fujunshi",
|
||||
"pangtong": "pangtong-fujunshi",
|
||||
"simayi": "simayi-challenger",
|
||||
"zhangfei": "zhangfei-dev",
|
||||
"guanyu": "guanyu-dev",
|
||||
"zhaoyun": "zhaoyun-data",
|
||||
"jiangwei": "jiangwei-infra",
|
||||
}
|
||||
|
||||
def extract_mentions(body: str, sender: str) -> list[str]:
|
||||
"""从评论 body 中提取 @mention 的 Agent ID"""
|
||||
candidates = re.findall(r"@([a-zA-Z\u4e00-\u9fa5][a-zA-Z0-9\u4e00-\u9fff-]*)", body)
|
||||
result = set()
|
||||
for c in candidates:
|
||||
# 精确匹配
|
||||
if c in AGENT_IDS:
|
||||
result.add(c)
|
||||
# 前缀/别名匹配
|
||||
elif c in AGENT_ALIAS:
|
||||
result.add(AGENT_ALIAS[c])
|
||||
else:
|
||||
# 前缀模糊匹配:@zhangfei → zhangfei-dev
|
||||
for aid in AGENT_IDS:
|
||||
if aid.startswith(c):
|
||||
result.add(aid)
|
||||
break
|
||||
# 过滤掉评论者自己
|
||||
result.discard(sender)
|
||||
return list(result)
|
||||
```
|
||||
|
||||
**新增 mention 通知模板** `templates/toolchain/mention.md`:
|
||||
|
||||
```markdown
|
||||
你在 Issue 中被 @mention
|
||||
|
||||
Issue: {issue_url}
|
||||
评论者: {commenter}
|
||||
评论内容:
|
||||
{comment_body}
|
||||
|
||||
📋 获取完整上下文:
|
||||
- Issue 详情: GET {gitea_api}/repos/{repo}/issues/{issue_number}
|
||||
- Issue 评论: GET {gitea_api}/repos/{repo}/issues/{issue_number}/comments
|
||||
```
|
||||
|
||||
**改动 `_handle_issue_comment`**:
|
||||
|
||||
```python
|
||||
async def _handle_issue_comment(payload):
|
||||
comment = payload.get("comment", {})
|
||||
body = comment.get("body", "")
|
||||
sender = comment.get("user", {}).get("login", "")
|
||||
repo = _repo_fullname(payload)
|
||||
issue = payload.get("issue", {})
|
||||
|
||||
# 原有 CI 失败逻辑(不变)
|
||||
if "[CI]" in body or "CI 失败" in body:
|
||||
# ... 原有逻辑 ...
|
||||
|
||||
# 新增:@mention 通知
|
||||
mentions = extract_mentions(body, sender)
|
||||
if mentions:
|
||||
issue_number = issue.get("number", 0)
|
||||
issue_title = issue.get("title", "")
|
||||
text = render_template("mention", {
|
||||
"repo": repo,
|
||||
"issue_number": str(issue_number),
|
||||
"issue_url": issue.get("html_url", ""),
|
||||
"commenter": sender,
|
||||
"comment_body": body[:500],
|
||||
"gitea_api": "http://192.168.2.154:3000/api/v1",
|
||||
})
|
||||
title = f"@mention: {issue_title} ({repo}#{issue_number})"
|
||||
for agent_id in mentions:
|
||||
_send_mail(agent_id, title, text)
|
||||
```
|
||||
|
||||
#### 3.3 去重
|
||||
|
||||
- 同一条评论 @多人:每人一封 Mail(不同 to,内容相同)
|
||||
- 同一事件 org webhook + repo webhook 双触发:现有 delivery UUID 去重机制覆盖
|
||||
- 同一人被 @多次:`extract_mentions` 返回 set,自动去重
|
||||
|
||||
---
|
||||
|
||||
### 四、Mail Spawn Prompt 改造
|
||||
|
||||
#### 4.1 问题
|
||||
|
||||
当前工具链 Mail 走 Mail 通道,spawn prompt 是:
|
||||
|
||||
```
|
||||
你收到一封飞鸽传书(纯通知)。
|
||||
发件者: system
|
||||
主题: Issue 指派: xxx
|
||||
内容: [工具链模板]
|
||||
已阅即可。
|
||||
```
|
||||
|
||||
"已阅即可"直接让 Agent 不做事。
|
||||
|
||||
#### 4.2 方案
|
||||
|
||||
**不改 MAIL_INFORM_TEMPLATE / MAIL_REQUEST_TEMPLATE 本身**(那是 Mail 系统通用的)。
|
||||
|
||||
改为:**工具链 Mail 使用 `type=request`(而不是默认的 inform)**。
|
||||
|
||||
在 `_send_mail()` 中,工具链事件创建的 Mail 默认 `performative=request`,这样 Agent 收到时走 `MAIL_REQUEST_TEMPLATE`,知道需要处理。
|
||||
|
||||
具体改动在 `_send_mail()` 函数或其调用处:工具链路由调用 `_send_mail` 时传入 `performative="request"`。
|
||||
|
||||
**⚠️ 验证要点**:改为 request 后,Agent spawn prompt 变为 "请处理以下请求",需确认:
|
||||
1. Agent 不再把工具链 Mail 当纯通知忽略
|
||||
2. Agent 能正确处理「已阅型」工具链事件(如 CI 失败通知——不需要回复,但需要知道)
|
||||
3. 对已关闭 PR/Issue 的延迟通知,Agent 不会尝试去处理
|
||||
|
||||
验证方法:部署后发一条 Issue 指派 Mail,观察 Agent 行为是否符合预期。
|
||||
|
||||
---
|
||||
|
||||
### 五、完整改动清单
|
||||
|
||||
| # | 改什么 | 改动内容 | 层 | 风险 |
|
||||
|---|--------|---------|---|------|
|
||||
| 1 | 6 个 Agent 的 `AGENTS.md` | 加"工具链协作"段(内容统一) | L1 | 低(纯追加) |
|
||||
| 2 | `templates/toolchain/issue_assigned.md` | 精简 + 加仓库上下文 + Gitea API 引导 | L2 | 低 |
|
||||
| 3 | `templates/toolchain/review_request.md` | 精简 + 加 Gitea API 引导 | L2 | 低 |
|
||||
| 4 | `templates/toolchain/review_result.md` | 精简 | L2 | 低 |
|
||||
| 5 | `templates/toolchain/ci_failure.md` | 精简 + 加 CI 日志链接 | L2 | 低 |
|
||||
| 6 | `templates/toolchain/deploy_failure.md` | 精简 + 加排查步骤 | L2 | 低 |
|
||||
| 7 | **新建** `templates/toolchain/mention.md` | @mention 通知模板 | L2 | 低 |
|
||||
| 8 | `src/api/toolchain_routes.py` | 提取 clone_url/html_url 传入模板;issue_comment 增加 @mention 解析;工具链 Mail 改为 request 类型 | L2 | 中 |
|
||||
| 9 | 不改 | git-workflow 等 Skill 保持不变 | L3 | — |
|
||||
| 10 | 不改 | daemon 核心逻辑、BootstrapBuilder、Skill 路由 | — | — |
|
||||
|
||||
---
|
||||
|
||||
### 六、验证方案
|
||||
|
||||
#### 6.1 单元验证
|
||||
|
||||
| 验证点 | 方法 |
|
||||
|--------|------|
|
||||
| `extract_mentions()` 提取 `@zhangfei-dev` | unit test |
|
||||
| `extract_mentions()` 别名匹配 `@张飞` → zhangfei-dev | unit test |
|
||||
| `extract_mentions()` 前缀匹配 `@zhangfei` → zhangfei-dev | unit test |
|
||||
| `extract_mentions()` 过滤自己 | unit test |
|
||||
| 模板渲染新变量不报错 | unit test |
|
||||
|
||||
#### 6.2 真实场景 E2E 验证
|
||||
|
||||
重复 Issue #32 的场景:
|
||||
1. 创建 Issue 指派张飞
|
||||
2. **验证**:张飞收到的 Mail 含 clone URL + Gitea API 引导
|
||||
3. **验证**:张飞 spawn 后知道该做什么(L1 AGENTS.md 有流程引导)
|
||||
4. **验证**:张飞有疑问时去 Gitea Issue 评论(而不是 Control UI)
|
||||
5. 在 Issue 评论 @赵云
|
||||
6. **验证**:赵云收到 @mention Mail
|
||||
|
||||
---
|
||||
|
||||
### 七、不做的事(标记为后续)
|
||||
|
||||
| 标记 | 描述 | 原因 |
|
||||
|------|------|------|
|
||||
| 后续-1 | Agent 离开工具链讨论后,是否有意识回到工具链 | 需要更多真实场景观察 |
|
||||
| 后续-2 | 工具链使用标准在所有 Agent 间的一致性验证 | L1 统一段落是第一步,需要 E2E 验证 |
|
||||
| 后续-3 | Mail 通道接入 BootstrapBuilder L2 注入 | 改动大,当前方案(L1 统一段落 + 模板引导)够用 |
|
||||
| 后续-4 | Skill 路由器自动触发(引擎注入) | 改动 daemon 核心,当前靠 L1 文案触发 |
|
||||
|
||||
---
|
||||
|
||||
### 八、变更记录
|
||||
|
||||
| 日期 | 版本 | 变更 |
|
||||
|------|------|------|
|
||||
| 2026-06-09 | v1.0 | 初版:E2E 真实场景暴露问题 → 四层改造方案 + @mention 通知 + Mail type 改造 |
|
||||
|
||||
|
||||
@@ -1,382 +0,0 @@
|
||||
# #19 工具链事件中枢 — 上下文四层改造方案
|
||||
|
||||
> 版本: v1.0
|
||||
> 日期: 2026-06-09
|
||||
> 作者: 庞统(副军师)
|
||||
> 状态: 待主公确认
|
||||
> 前置: #13 工具链与开发流程 §16, #05 上下文四层架构
|
||||
> 来源: E2E 真实场景测试暴露的三个断层
|
||||
|
||||
---
|
||||
|
||||
## 一、问题诊断
|
||||
|
||||
### 1.1 E2E 真实场景测试暴露的三个断层
|
||||
|
||||
主公在 moziplus-v2 仓库创建了 Issue #32(添加 /api/stats 端点),指派张飞。链条在第一步就断了。
|
||||
|
||||
| 断层 | 现象 | 根因 |
|
||||
|------|------|------|
|
||||
| **Agent 不知道该做什么** | 张飞收到 Issue 指派 Mail,回复"已阅"就结束了 | Mail 模板(issue_assigned.md)5 行信息,无流程引导;spawn prompt 说"已阅即可" |
|
||||
| **Agent 去错了仓库** | 张飞去读了 sanguo_moziplus_v2 平台代码,而不是空的实验仓库 moziplus-v2 | Mail 模板没有仓库 clone URL,张飞凭习惯去了开发目录 |
|
||||
| **Agent 在 Control UI 提问** | 张飞遇到问题直接在 Control UI 问主公,没有去 Gitea Issue 评论 | 没有任何地方引导"有疑问去 Gitea Issue 评论" |
|
||||
| **Agent 不知道怎么协作** | 张飞判断任务需要澄清,但不知道该怎么请求澄清 | 没有"做不了→在 Issue 评论 / Mail 庞统"的回退路径 |
|
||||
| **跨 Agent @mention 无法通知** | 张飞在 Issue 评论 @赵云,赵云收不到通知 | issue_comment handler 只处理 [CI] 评论,@mention 被忽略 |
|
||||
|
||||
### 1.2 根因:工具链在四层架构中的断层
|
||||
|
||||
| 层 | 应该有 | 实际有 | Gap |
|
||||
|---|---|---|---|
|
||||
| **L0 铁律** | — | — | 无需改动 |
|
||||
| **L1 角色** | 工具链协作行为规范(所有 Agent 共享) | 无 | AGENTS.md 没有工具链相关内容 |
|
||||
| **L2 引擎注入** | 事件上下文(仓库 clone URL、Gitea API、Issue/PR 详情) | Mail 模板只有 5 行摘要 | 缺仓库信息和流程引导 |
|
||||
| **L3 被动参考** | 技术细节(分支命名、commit 规范、PR 创建方式) | git-workflow 等 Skill 已存在但没人触发 | Agent 不知道该加载哪个 Skill |
|
||||
|
||||
---
|
||||
|
||||
## 二、改造方案:四层归属
|
||||
|
||||
### 2.1 分层原则
|
||||
|
||||
| 层 | 放什么 | 不放什么 | 理由 |
|
||||
|---|---|---|---|
|
||||
| **L0** | 不放 | — | 工具链不是安全底线 |
|
||||
| **L1** | 协作行为规范:收到什么通知该做什么、遇到问题怎么办 | 技术细节(分支命名、commit 格式) | 行为规范是团队常识,每个 Agent 都要知道 |
|
||||
| **L2** | 事件上下文:仓库 clone URL、Gitea API URL、Issue/PR 链接、动态信息 | 固定的协作流程 | 动态信息每次不同,由 Mail 模板 + spawn 时注入 |
|
||||
| **L3** | 技术细节:git-workflow、code-review 等 Skill 全文 | — | 按需加载,Agent 知道"我要提 PR"后自己读 |
|
||||
|
||||
### 2.2 各层具体内容
|
||||
|
||||
#### L1:AGENTS.md 加工具链协作行为段(所有 Agent 统一)
|
||||
|
||||
```markdown
|
||||
## 工具链协作(Gitea)
|
||||
|
||||
收到 Gitea 事件通知(Issue 指派、Review 请求、CI 失败等)时,按以下流程操作:
|
||||
|
||||
### 基本流程
|
||||
- **Issue 指派** → clone 仓库 → 开分支 → 编码 → 提 PR(参考 git-workflow Skill)
|
||||
- **Review 请求** → 读 PR diff(Gitea API)→ 提交 Review(参考 code-review Skill)
|
||||
- **Review 通过** → 等 merge
|
||||
- **Review 驳回** → 看 review body → 修代码 → 重新 push
|
||||
- **CI 失败** → 看错误摘要 → 修代码 → push(自动重触发 CI)
|
||||
- **部署失败** → 查 deploy 日志 → 修复
|
||||
|
||||
### 协作规则
|
||||
- **有疑问?** 在 Gitea Issue 下评论,不要在 Control UI 或 Mail 里问
|
||||
- **需要别人帮忙?** 在 Issue 评论中 @mention 对应 Agent(如 @zhaoyun-data)
|
||||
- **做不了?** 回复 Mail 说明原因和建议的接手人
|
||||
- **获取完整上下文** → 用 Gitea API 拉取 Issue 详情和评论,不要只看 Mail 里的快照
|
||||
|
||||
### Gitea API 速查
|
||||
> 其中 `{owner}/{repo}` 替换为实际仓库,如 `sanguo/sanguo_moziplus_v2`
|
||||
- Issue 详情: GET /api/v1/repos/{owner}/{repo}/issues/{number}
|
||||
- Issue 评论: GET /api/v1/repos/{owner}/{repo}/issues/{number}/comments
|
||||
- PR diff: GET /api/v1/repos/{owner}/{repo}/pulls/{number}.diff
|
||||
- 提交 Review: POST /api/v1/repos/{owner}/{repo}/pulls/{number}/reviews
|
||||
```
|
||||
|
||||
**改动范围**:6 个 Agent 的 AGENTS.md 各加一段(内容统一)。
|
||||
|
||||
#### L2:Mail 模板精简 + 事件上下文注入
|
||||
|
||||
**原则**:模板只放摘要 + 链接 + 仓库信息,不写固定步骤(步骤在 L1)。
|
||||
|
||||
**issue_assigned.md** 改为:
|
||||
|
||||
```markdown
|
||||
Issue 指派
|
||||
|
||||
Issue: {issue_url}
|
||||
标题: {issue_title}
|
||||
标签: {labels}
|
||||
|
||||
📋 获取完整上下文(先读再动手):
|
||||
- Issue 详情: GET {gitea_api}/repos/{repo}/issues/{issue_number}
|
||||
- Issue 评论: GET {gitea_api}/repos/{repo}/issues/{issue_number}/comments
|
||||
|
||||
仓库: {repo_clone_url}
|
||||
建议分支: feat/issue-{issue_number}-{brief}
|
||||
```
|
||||
|
||||
**review_request.md** 改为:
|
||||
|
||||
```markdown
|
||||
PR Review 请求
|
||||
|
||||
PR: {pr_url}
|
||||
标题: {pr_title}
|
||||
作者: {pr_author}
|
||||
分支: {branch}
|
||||
风险级别: {risk_level}
|
||||
|
||||
📋 获取完整上下文:
|
||||
- PR diff: GET {gitea_api}/repos/{repo}/pulls/{pr_number}.diff
|
||||
- PR 文件列表: GET {gitea_api}/repos/{repo}/pulls/{pr_number}/files
|
||||
```
|
||||
|
||||
**review_result.md** 改为:
|
||||
|
||||
```markdown
|
||||
Review {result}
|
||||
|
||||
PR: {pr_url}
|
||||
标题: {pr_title}
|
||||
审查者: {reviewer}
|
||||
|
||||
{review_body}
|
||||
```
|
||||
|
||||
**ci_failure.md** 改为:
|
||||
|
||||
```markdown
|
||||
CI 失败
|
||||
|
||||
Issue: {issue_url}
|
||||
分支: {branch}
|
||||
|
||||
错误摘要:
|
||||
{error_summary}
|
||||
|
||||
📋 CI 日志: {gitea_url}/{repo}/actions
|
||||
修复后 push 会自动重触发 CI。
|
||||
```
|
||||
|
||||
**deploy_failure.md** 改为:
|
||||
|
||||
```markdown
|
||||
部署失败
|
||||
|
||||
仓库: {repo}
|
||||
Commit: {commit_sha}
|
||||
|
||||
📋 排查步骤:
|
||||
- CI 日志: {gitea_url}/{repo}/actions
|
||||
- 服务器: pm2 logs {service_name}
|
||||
```
|
||||
|
||||
**L2 代码改动**(`toolchain_routes.py`):
|
||||
|
||||
1. 从 Webhook payload 的 `repository` 对象提取 `clone_url` 和 `html_url`
|
||||
2. `render_template()` 传入新变量:`gitea_api`、`gitea_url`、`repo_clone_url`
|
||||
3. 所有模板变量统一补齐
|
||||
|
||||
#### L3:Skill 按需加载(不改 Skill 本身)
|
||||
|
||||
git-workflow、code-review 等 Skill 保持不变。
|
||||
|
||||
L1 的协作行为段里会引用 Skill 名称("参考 git-workflow Skill"),Agent 收到 Mail 后根据 L1 的引导自主加载对应 Skill。
|
||||
|
||||
**不改 Skill 路由机制**——靠 L1 的文案触发 Agent 的 Skill 路由器匹配。
|
||||
|
||||
---
|
||||
|
||||
## 三、新增功能:issue_comment @mention 通知
|
||||
|
||||
### 3.1 设计
|
||||
|
||||
当前 `_handle_issue_comment` 只处理 `[CI]` 前缀评论。扩展为:
|
||||
|
||||
```
|
||||
issue_comment 事件
|
||||
├── 含 [CI] / CI 失败 → 原有 CI 失败通知逻辑
|
||||
└── 含 @username → 解析 @mention → Mail 通知被 @的 Agent
|
||||
```
|
||||
|
||||
### 3.2 实现
|
||||
|
||||
**`toolchain_routes.py` 新增 `_handle_issue_comment_mention()`**:
|
||||
|
||||
```python
|
||||
AGENT_IDS = {
|
||||
"zhangfei-dev", "guanyu-dev", "zhaoyun-data",
|
||||
"jiangwei-infra", "simayi-challenger", "pangtong-fujunshi",
|
||||
}
|
||||
|
||||
# 前缀映射:@张飞 → zhangfei-dev
|
||||
# 中文名映射:Agent 在 Gitea Issue 评论中可能用中文名 @mention
|
||||
# 英文短名映射:Agent 可能用不带 -dev/-infra 后缀的短名
|
||||
AGENT_ALIAS = {
|
||||
"张飞": "zhangfei-dev",
|
||||
"关羽": "guanyu-dev",
|
||||
"赵云": "zhaoyun-data",
|
||||
"姜维": "jiangwei-infra",
|
||||
"司马懿": "simayi-challenger",
|
||||
"庞统": "pangtong-fujunshi",
|
||||
"pangtong": "pangtong-fujunshi",
|
||||
"simayi": "simayi-challenger",
|
||||
"zhangfei": "zhangfei-dev",
|
||||
"guanyu": "guanyu-dev",
|
||||
"zhaoyun": "zhaoyun-data",
|
||||
"jiangwei": "jiangwei-infra",
|
||||
}
|
||||
|
||||
def extract_mentions(body: str, sender: str) -> list[str]:
|
||||
"""从评论 body 中提取 @mention 的 Agent ID"""
|
||||
candidates = re.findall(r"@([a-zA-Z\u4e00-\u9fa5][a-zA-Z0-9\u4e00-\u9fff-]*)", body)
|
||||
result = set()
|
||||
for c in candidates:
|
||||
# 精确匹配
|
||||
if c in AGENT_IDS:
|
||||
result.add(c)
|
||||
# 前缀/别名匹配
|
||||
elif c in AGENT_ALIAS:
|
||||
result.add(AGENT_ALIAS[c])
|
||||
else:
|
||||
# 前缀模糊匹配:@zhangfei → zhangfei-dev
|
||||
for aid in AGENT_IDS:
|
||||
if aid.startswith(c):
|
||||
result.add(aid)
|
||||
break
|
||||
# 过滤掉评论者自己
|
||||
result.discard(sender)
|
||||
return list(result)
|
||||
```
|
||||
|
||||
**新增 mention 通知模板** `templates/toolchain/mention.md`:
|
||||
|
||||
```markdown
|
||||
你在 Issue 中被 @mention
|
||||
|
||||
Issue: {issue_url}
|
||||
评论者: {commenter}
|
||||
评论内容:
|
||||
{comment_body}
|
||||
|
||||
📋 获取完整上下文:
|
||||
- Issue 详情: GET {gitea_api}/repos/{repo}/issues/{issue_number}
|
||||
- Issue 评论: GET {gitea_api}/repos/{repo}/issues/{issue_number}/comments
|
||||
```
|
||||
|
||||
**改动 `_handle_issue_comment`**:
|
||||
|
||||
```python
|
||||
async def _handle_issue_comment(payload):
|
||||
comment = payload.get("comment", {})
|
||||
body = comment.get("body", "")
|
||||
sender = comment.get("user", {}).get("login", "")
|
||||
repo = _repo_fullname(payload)
|
||||
issue = payload.get("issue", {})
|
||||
|
||||
# 原有 CI 失败逻辑(不变)
|
||||
if "[CI]" in body or "CI 失败" in body:
|
||||
# ... 原有逻辑 ...
|
||||
|
||||
# 新增:@mention 通知
|
||||
mentions = extract_mentions(body, sender)
|
||||
if mentions:
|
||||
issue_number = issue.get("number", 0)
|
||||
issue_title = issue.get("title", "")
|
||||
text = render_template("mention", {
|
||||
"repo": repo,
|
||||
"issue_number": str(issue_number),
|
||||
"issue_url": issue.get("html_url", ""),
|
||||
"commenter": sender,
|
||||
"comment_body": body[:500],
|
||||
"gitea_api": "http://192.168.2.154:3000/api/v1",
|
||||
})
|
||||
title = f"@mention: {issue_title} ({repo}#{issue_number})"
|
||||
for agent_id in mentions:
|
||||
_send_mail(agent_id, title, text)
|
||||
```
|
||||
|
||||
### 3.3 去重
|
||||
|
||||
- 同一条评论 @多人:每人一封 Mail(不同 to,内容相同)
|
||||
- 同一事件 org webhook + repo webhook 双触发:现有 delivery UUID 去重机制覆盖
|
||||
- 同一人被 @多次:`extract_mentions` 返回 set,自动去重
|
||||
|
||||
---
|
||||
|
||||
## 四、Mail Spawn Prompt 改造
|
||||
|
||||
### 4.1 问题
|
||||
|
||||
当前工具链 Mail 走 Mail 通道,spawn prompt 是:
|
||||
|
||||
```
|
||||
你收到一封飞鸽传书(纯通知)。
|
||||
发件者: system
|
||||
主题: Issue 指派: xxx
|
||||
内容: [工具链模板]
|
||||
已阅即可。
|
||||
```
|
||||
|
||||
"已阅即可"直接让 Agent 不做事。
|
||||
|
||||
### 4.2 方案
|
||||
|
||||
**不改 MAIL_INFORM_TEMPLATE / MAIL_REQUEST_TEMPLATE 本身**(那是 Mail 系统通用的)。
|
||||
|
||||
改为:**工具链 Mail 使用 `type=request`(而不是默认的 inform)**。
|
||||
|
||||
在 `_send_mail()` 中,工具链事件创建的 Mail 默认 `performative=request`,这样 Agent 收到时走 `MAIL_REQUEST_TEMPLATE`,知道需要处理。
|
||||
|
||||
具体改动在 `_send_mail()` 函数或其调用处:工具链路由调用 `_send_mail` 时传入 `performative="request"`。
|
||||
|
||||
**⚠️ 验证要点**:改为 request 后,Agent spawn prompt 变为 "请处理以下请求",需确认:
|
||||
1. Agent 不再把工具链 Mail 当纯通知忽略
|
||||
2. Agent 能正确处理「已阅型」工具链事件(如 CI 失败通知——不需要回复,但需要知道)
|
||||
3. 对已关闭 PR/Issue 的延迟通知,Agent 不会尝试去处理
|
||||
|
||||
验证方法:部署后发一条 Issue 指派 Mail,观察 Agent 行为是否符合预期。
|
||||
|
||||
---
|
||||
|
||||
## 五、完整改动清单
|
||||
|
||||
| # | 改什么 | 改动内容 | 层 | 风险 |
|
||||
|---|--------|---------|---|------|
|
||||
| 1 | 6 个 Agent 的 `AGENTS.md` | 加"工具链协作"段(内容统一) | L1 | 低(纯追加) |
|
||||
| 2 | `templates/toolchain/issue_assigned.md` | 精简 + 加仓库上下文 + Gitea API 引导 | L2 | 低 |
|
||||
| 3 | `templates/toolchain/review_request.md` | 精简 + 加 Gitea API 引导 | L2 | 低 |
|
||||
| 4 | `templates/toolchain/review_result.md` | 精简 | L2 | 低 |
|
||||
| 5 | `templates/toolchain/ci_failure.md` | 精简 + 加 CI 日志链接 | L2 | 低 |
|
||||
| 6 | `templates/toolchain/deploy_failure.md` | 精简 + 加排查步骤 | L2 | 低 |
|
||||
| 7 | **新建** `templates/toolchain/mention.md` | @mention 通知模板 | L2 | 低 |
|
||||
| 8 | `src/api/toolchain_routes.py` | 提取 clone_url/html_url 传入模板;issue_comment 增加 @mention 解析;工具链 Mail 改为 request 类型 | L2 | 中 |
|
||||
| 9 | 不改 | git-workflow 等 Skill 保持不变 | L3 | — |
|
||||
| 10 | 不改 | daemon 核心逻辑、BootstrapBuilder、Skill 路由 | — | — |
|
||||
|
||||
---
|
||||
|
||||
## 六、验证方案
|
||||
|
||||
### 6.1 单元验证
|
||||
|
||||
| 验证点 | 方法 |
|
||||
|--------|------|
|
||||
| `extract_mentions()` 提取 `@zhangfei-dev` | unit test |
|
||||
| `extract_mentions()` 别名匹配 `@张飞` → zhangfei-dev | unit test |
|
||||
| `extract_mentions()` 前缀匹配 `@zhangfei` → zhangfei-dev | unit test |
|
||||
| `extract_mentions()` 过滤自己 | unit test |
|
||||
| 模板渲染新变量不报错 | unit test |
|
||||
|
||||
### 6.2 真实场景 E2E 验证
|
||||
|
||||
重复 Issue #32 的场景:
|
||||
1. 创建 Issue 指派张飞
|
||||
2. **验证**:张飞收到的 Mail 含 clone URL + Gitea API 引导
|
||||
3. **验证**:张飞 spawn 后知道该做什么(L1 AGENTS.md 有流程引导)
|
||||
4. **验证**:张飞有疑问时去 Gitea Issue 评论(而不是 Control UI)
|
||||
5. 在 Issue 评论 @赵云
|
||||
6. **验证**:赵云收到 @mention Mail
|
||||
|
||||
---
|
||||
|
||||
## 七、不做的事(标记为后续)
|
||||
|
||||
| 标记 | 描述 | 原因 |
|
||||
|------|------|------|
|
||||
| 后续-1 | Agent 离开工具链讨论后,是否有意识回到工具链 | 需要更多真实场景观察 |
|
||||
| 后续-2 | 工具链使用标准在所有 Agent 间的一致性验证 | L1 统一段落是第一步,需要 E2E 验证 |
|
||||
| 后续-3 | Mail 通道接入 BootstrapBuilder L2 注入 | 改动大,当前方案(L1 统一段落 + 模板引导)够用 |
|
||||
| 后续-4 | Skill 路由器自动触发(引擎注入) | 改动 daemon 核心,当前靠 L1 文案触发 |
|
||||
|
||||
---
|
||||
|
||||
## 八、变更记录
|
||||
|
||||
| 日期 | 版本 | 变更 |
|
||||
|------|------|------|
|
||||
| 2026-06-09 | v1.0 | 初版:E2E 真实场景暴露问题 → 四层改造方案 + @mention 通知 + Mail type 改造 |
|
||||
@@ -0,0 +1,649 @@
|
||||
---
|
||||
title: "TaskTypeRegistry + Handler 架构重构"
|
||||
created: 2026-06-10
|
||||
version: v2.0
|
||||
---
|
||||
|
||||
# §1 现状分析
|
||||
|
||||
moziplus v2 的任务调度系统当前通过 `if/else` 硬编码区分两种 task type:普通任务(task)和邮件(mail)。分支逻辑散落在 dispatcher、spawner、ticker 三个核心模块中,新增 task type 需要同时改动三处。
|
||||
|
||||
### dispatcher.py 中的分支
|
||||
|
||||
- **`is_mail` 判断**:`project_id == "_mail"` 作为唯一判据
|
||||
- **mail 分支**:跳过 guardrail 检查;有专属的 `on_checks_passed` 回调(`_mail_auto_working`)和 `on_complete` 回调(`_mail_auto_complete`,含幻觉门控 + 失败通知)
|
||||
- **非 mail 分支**:走正常 guardrail 检查 → `on_checks_passed` → `on_complete` 全链路
|
||||
|
||||
### spawner.py 中的分支
|
||||
|
||||
- **`_build_prompt`**:`_mail` 走 `_build_mail_prompt()`,其余走 BootstrapBuilder L0-L3 全链
|
||||
- **`_build_api_section`**:`_mail` 的 `success_status = "done"`,其余 `success_status = "review"`
|
||||
- **retry 逻辑**:mail 用 `MAIL_RETRY_PROMPT`,task 用标准 `RETRY_PROMPT`
|
||||
- **`classify_outcome` 完成处理**:mail 走幻觉门控(hallucination gate)
|
||||
|
||||
### ticker.py 中的分支
|
||||
|
||||
- **虚拟项目扫描**:`_general` 和 `_mail` 两个虚拟项目各自硬编码扫描逻辑
|
||||
- **mail 专属逻辑**:`_mail_check_reply` 判断邮件回复是否闭环
|
||||
|
||||
**问题总结**:新增第三种 task type(toolchain)如果继续硬编码,需要在三个文件中各加一套 if/else,且未来每种新类型都重复这个模式,维护成本线性增长。
|
||||
|
||||
---
|
||||
|
||||
# §2 三种 Task Type 行为差异表
|
||||
|
||||
| 维度 | task(普通任务) | mail | toolchain |
|
||||
|------|---------|------|-----------|
|
||||
| 存储 | 项目 DB(`projects/{pid}/blackboard.db`) | `_mail` DB(`_mail/blackboard.db`) | `_toolchain` DB |
|
||||
| 状态流转 | pending→claimed→working→review→done | 跳过 claimed,auto-working→auto-done | auto-working→auto-done |
|
||||
| prompt 构建 | BootstrapBuilder L0-L3 | MAIL_INFORM / MAIL_REQUEST 精简模板 | TOOLCHAIN 模板 + 事件上下文 |
|
||||
| guardrail | 正常检查 | 跳过 | 跳过 |
|
||||
| 完成标准 | 产出物 + review | 回复邮件 / inform done | Agent 处理完毕自动 done(无需回复)。闭环由 classify_outcome 判定:Agent 输出含实质行动(代码修复/分析结论)→ done;输出为空/幻觉 → failed。CI/Deploy 类事件最终闭环在 Gitea 侧(Agent push 修复后 CI 自动重跑),mozi 不跟踪 Gitea 侧最终结果。 |
|
||||
| on_complete | classify_outcome → 状态机 | 幻觉门控 + 失败通知 | auto-done + 可选 escalate |
|
||||
| 路由 | Router 四条快速路径 + 广播认领 | 直接路由到收件人 | 直接路由到事件相关 agent |
|
||||
| retry | 标准 `RETRY_PROMPT` | `MAIL_RETRY_PROMPT` | 标准(或专用) |
|
||||
| 前端展示 | 任务看板 Tab | 飞鸽传书 Tab | 待定 |
|
||||
|
||||
---
|
||||
|
||||
# §3 Handler 接口定义
|
||||
|
||||
定义 Python Protocol,所有 task type handler 必须满足此接口:
|
||||
|
||||
```python
|
||||
from typing import Protocol, Optional, Dict, Any
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class TaskTypeHandler(Protocol):
|
||||
"""所有 task type handler 的统一接口。"""
|
||||
|
||||
# 属性通过 __init__ 参数设置,Protocol 不强制 property
|
||||
task_type: str # 类型标识:'task' | 'mail' | 'toolchain'
|
||||
virtual_project: Optional[str] # 虚拟项目 ID,如 '_mail'、'_toolchain'。普通任务为 None
|
||||
|
||||
def build_prompt(
|
||||
self,
|
||||
task_id: str,
|
||||
title: str,
|
||||
description: str,
|
||||
must_haves: str,
|
||||
project_id: str,
|
||||
agent_id: str,
|
||||
task: Optional[Dict] = None,
|
||||
spawn_type: str = "executor",
|
||||
spawner: Any = None,
|
||||
) -> str:
|
||||
"""构建 Agent prompt。"""
|
||||
...
|
||||
|
||||
def build_api_section(
|
||||
self, project_id: str, task_id: str, agent_id: str
|
||||
) -> str:
|
||||
"""构建 API 操作指令(success_status 等)。"""
|
||||
...
|
||||
|
||||
def skip_guardrail(self, project_id: str) -> bool:
|
||||
"""是否跳过 guardrail 检查。"""
|
||||
...
|
||||
|
||||
def pre_spawn(
|
||||
self, task_id: str, db_path: Path, dispatcher: Any
|
||||
) -> Optional[callable]:
|
||||
"""spawn 前回调,返回 on_checks_passed 回调或 None。"""
|
||||
...
|
||||
|
||||
def post_complete(
|
||||
self,
|
||||
task_id: str,
|
||||
agent_id: str,
|
||||
outcome: str,
|
||||
db_path: Path,
|
||||
must_haves: str,
|
||||
dispatcher: Any,
|
||||
) -> None:
|
||||
"""spawn 完成后回调。"""
|
||||
...
|
||||
|
||||
def build_retry_prompt(
|
||||
self,
|
||||
task_id: str,
|
||||
agent_id: str,
|
||||
retry_count: int,
|
||||
max_retries: int,
|
||||
retry_field: str,
|
||||
task_info: Dict,
|
||||
spawner: Any,
|
||||
) -> str:
|
||||
"""构建重试 prompt。"""
|
||||
...
|
||||
|
||||
def check_completion(self, task_id: str, db_path: Path) -> bool:
|
||||
"""检查任务是否已完成(如 mail 的回复检查)。"""
|
||||
...
|
||||
|
||||
def get_sections(self) -> list['PromptSection']:
|
||||
"""返回此 handler 的 prompt section 列表。
|
||||
|
||||
返回有序的 PromptSection 列表,由 PromptComposer 统一拼装。
|
||||
build_prompt 可使用此列表自动拼装,也可 override 做自定义(向后兼容)。
|
||||
"""
|
||||
...
|
||||
```
|
||||
|
||||
**设计原则**:
|
||||
|
||||
- 每个方法在现有代码中都有明确的对应实现点,不存在"悬空"抽象
|
||||
- `pre_spawn` 返回 Optional[callable],普通任务返回 None 即可,不需要回调
|
||||
- `spawner` 和 `dispatcher` 参数用 `Any` 类型,避免循环导入;handler 只调用已知方法
|
||||
|
||||
**兼容期过渡策略**:
|
||||
|
||||
引擎中 handler 查询优先,无 handler 时 fallback 到现有逻辑。具体流程:
|
||||
1. `TaskTypeRegistry.get_by_project(project_id)` 查到 handler → 走 handler 路径
|
||||
2. 未查到 → 走现有 if/else 分支(兼容期)
|
||||
3. 三种 handler 全部稳定后,Step 5 删除旧分支,统一走 handler
|
||||
4. 兼容期内新旧路径互斥——同一个 project_id 只会走其中一条,不存在重复执行
|
||||
|
||||
---
|
||||
|
||||
# §4 TaskTypeRegistry 注册表
|
||||
|
||||
```python
|
||||
class TaskTypeRegistry:
|
||||
"""Task type handler 注册表。启动时一次性加载,运行时只读。"""
|
||||
|
||||
_handlers: Dict[str, TaskTypeHandler] = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls, handler: TaskTypeHandler) -> None:
|
||||
"""注册一个 handler。启动时调用一次。"""
|
||||
if handler.task_type in cls._handlers:
|
||||
raise ValueError(
|
||||
f"Task type '{handler.task_type}' already registered"
|
||||
)
|
||||
cls._handlers[handler.task_type] = handler
|
||||
|
||||
@classmethod
|
||||
def get_by_project(cls, project_id: str) -> Optional[TaskTypeHandler]:
|
||||
"""通过 project_id 查找 handler(匹配 virtual_project)。"""
|
||||
for h in cls._handlers.values():
|
||||
if h.virtual_project == project_id:
|
||||
return h
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get(cls, task_type: str) -> Optional[TaskTypeHandler]:
|
||||
"""通过 task_type 标识查找 handler。"""
|
||||
return cls._handlers.get(task_type)
|
||||
|
||||
@classmethod
|
||||
def virtual_projects(cls) -> list[str]:
|
||||
"""返回所有已注册的虚拟项目 ID(ticker 自动发现用)。"""
|
||||
return [
|
||||
h.virtual_project
|
||||
for h in cls._handlers.values()
|
||||
if h.virtual_project is not None
|
||||
]
|
||||
```
|
||||
|
||||
**使用方式**(daemon 启动时):
|
||||
|
||||
```python
|
||||
from task_handler import TaskHandler
|
||||
from mail_handler import MailHandler
|
||||
from toolchain_handler import ToolchainHandler
|
||||
|
||||
TaskTypeRegistry.register(TaskHandler())
|
||||
TaskTypeRegistry.register(MailHandler())
|
||||
TaskTypeRegistry.register(ToolchainHandler())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# §5 三个 Handler 的实现边界
|
||||
|
||||
## TaskHandler(普通任务)
|
||||
|
||||
将现有 default(非 mail)分支封装为 handler,**不替代 BootstrapBuilder**。
|
||||
|
||||
| 方法 | 实现 |
|
||||
|------|------|
|
||||
| `task_type` | `"task"` |
|
||||
| `virtual_project` | `None` |
|
||||
| `build_prompt` | 调用 `BootstrapBuilder`(透传参数) |
|
||||
| `build_api_section` | 现有 default 逻辑,`success_status = "review"` |
|
||||
| `skip_guardrail` | `False` |
|
||||
| `pre_spawn` | `None`(不需要回调) |
|
||||
| `post_complete` | `classify_outcome` → 状态机 |
|
||||
| `check_completion` | `False`(由状态机管理) |
|
||||
| `build_retry_prompt` | 标准 `RETRY_PROMPT` |
|
||||
|
||||
**改动量**:~60 行,从 spawner/dispatcher 的 default 分支包一层。
|
||||
|
||||
## MailHandler
|
||||
|
||||
将分散在 dispatcher / spawner / ticker 三处的 mail 逻辑集中到一个 handler。
|
||||
|
||||
| 方法 | 实现 |
|
||||
|------|------|
|
||||
| `task_type` | `"mail"` |
|
||||
| `virtual_project` | `"_mail"` |
|
||||
| `build_prompt` | 复用 `_build_mail_prompt` / `MAIL_INFORM_TEMPLATE` / `MAIL_REQUEST_TEMPLATE` |
|
||||
| `build_api_section` | `success_status = "done"` |
|
||||
| `skip_guardrail` | `True` |
|
||||
| `pre_spawn` | auto_working 回调(从 dispatcher `_mail_on_checks_passed` 搬入) |
|
||||
| `post_complete` | 幻觉门控 + auto_done + 失败通知(从 dispatcher `_mail_auto_complete` 搬入) |
|
||||
| `check_completion` | `_mail_check_reply`(从 ticker 搬入) |
|
||||
| `build_retry_prompt` | `MAIL_RETRY_PROMPT` |
|
||||
|
||||
**改动量**:~150 行,三处逻辑集中。
|
||||
|
||||
## ToolchainHandler
|
||||
|
||||
全新 handler,处理 Gitea Webhook 事件通知。
|
||||
|
||||
| 方法 | 实现 |
|
||||
|------|------|
|
||||
| `task_type` | `"toolchain"` |
|
||||
| `virtual_project` | `"_toolchain"` |
|
||||
| `build_prompt` | 新建 `TOOLCHAIN_TEMPLATE` + 事件上下文注入 |
|
||||
| `build_api_section` | `success_status = "done"` |
|
||||
| `skip_guardrail` | `True` |
|
||||
| `pre_spawn` | auto_working 回调 |
|
||||
| `post_complete` | auto-done。escalate 触发条件:Agent 输出包含"需要人工介入"标记(如 block/无法修复/权限不足),或连续 retry 失败达到 max_retries(默认 2 次)。escalate 方式:通过 Mail 通知项目负责人(pangtong-fujunshi) |
|
||||
| `check_completion` | `False`(由 post_complete 中 classify_outcome 判定,不需要额外的完成检查) |
|
||||
| `build_retry_prompt` | 标准 `RETRY_PROMPT`(或后续定制) |
|
||||
|
||||
**改动量**:~100 行,全新代码。
|
||||
|
||||
---
|
||||
|
||||
# §6 引擎改动点(一次性)
|
||||
|
||||
引擎代码改动是受控的、一次性的。改完后新增 task type 不再触碰引擎。
|
||||
|
||||
## dispatcher.py(~20 行改动)
|
||||
|
||||
```python
|
||||
# dispatch() 中,替换现有的 is_mail 分支
|
||||
handler = TaskTypeRegistry.get_by_project(project_id)
|
||||
|
||||
if handler:
|
||||
if handler.skip_guardrail(project_id):
|
||||
# 跳过 guardrail,直接 spawn
|
||||
...
|
||||
on_passed = handler.pre_spawn(task_id, db_path, self)
|
||||
# spawn 后由 on_passed 回调驱动后续
|
||||
...
|
||||
handler.post_complete(task_id, agent_id, outcome, db_path, must_haves, self)
|
||||
else:
|
||||
# 兼容期:保留现有 default 逻辑
|
||||
...
|
||||
```
|
||||
|
||||
## spawner.py(~15 行改动)
|
||||
|
||||
```python
|
||||
# _build_prompt() 中
|
||||
handler = TaskTypeRegistry.get_by_project(project_id)
|
||||
if handler:
|
||||
return handler.build_prompt(
|
||||
task_id, title, description, must_haves,
|
||||
project_id, agent_id, task, spawn_type, self
|
||||
)
|
||||
# else: 现有 BootstrapBuilder 逻辑(兼容期)
|
||||
|
||||
# _build_api_section() 中
|
||||
handler = TaskTypeRegistry.get_by_project(project_id)
|
||||
if handler:
|
||||
return handler.build_api_section(project_id, task_id, agent_id)
|
||||
|
||||
# retry 逻辑中
|
||||
handler = TaskTypeRegistry.get_by_project(project_id)
|
||||
if handler:
|
||||
return handler.build_retry_prompt(
|
||||
task_id, agent_id, retry_count, max_retries,
|
||||
retry_field, task_info, self
|
||||
)
|
||||
```
|
||||
|
||||
**classify_outcome 完成处理**:
|
||||
- spawner.py 第 1102 行 `is_mail` 分支在 `classify_outcome` 后触发幻觉门控
|
||||
- 迁移到 MailHandler.post_complete 后,此处的 `is_mail` 判断改为查注册表:
|
||||
```python
|
||||
handler = TaskTypeRegistry.get_by_project(project_id)
|
||||
if handler:
|
||||
handler.post_complete(task_id, agent_id, outcome, db_path, must_haves, self)
|
||||
else:
|
||||
# 兼容期:现有 classify_outcome 逻辑
|
||||
...
|
||||
```
|
||||
|
||||
## ticker.py(~10 行改动)
|
||||
|
||||
```python
|
||||
# 虚拟项目扫描:从注册表自动发现
|
||||
for vp in TaskTypeRegistry.virtual_projects():
|
||||
handler = TaskTypeRegistry.get_by_project(vp)
|
||||
if handler and handler.check_completion:
|
||||
# 调用 handler.check_completion 检查
|
||||
...
|
||||
|
||||
# 保留 _general 硬编码作为 fallback(非 task type 机制)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# §7 实施顺序
|
||||
|
||||
按风险从低到高排列,每步完成后跑 `pytest -m "not e2e"` 全量回归测试。
|
||||
|
||||
### Step 1:注册表 + PromptComposer 基础设施
|
||||
|
||||
- 新建 `src/daemon/task_type_registry.py`:`TaskTypeHandler` Protocol + `TaskTypeRegistry`
|
||||
- 新建 `src/daemon/prompt_composer.py`:`PromptSection` Protocol + `PromptContext` + `PromptComposer`
|
||||
- 编写单元测试验证:注册/查询、section 排序/去重/条件过滤
|
||||
- **风险**:极低,纯新增文件,不改动现有代码
|
||||
|
||||
### Step 2:TaskHandler
|
||||
|
||||
- 新建 `src/daemon/task_handler.py`
|
||||
- 实现 5 个 section(TaskContext / PriorOutputs / RoleSkill / TaskApi / TaskConstraints)
|
||||
- `build_prompt` 内部用 PromptComposer 拼装
|
||||
- 注册到 TaskTypeRegistry
|
||||
- 运行全量回归测试,验证普通任务路径不变
|
||||
- **风险**:低,现有 BootstrapBuilder 逻辑包一层
|
||||
|
||||
### Step 3:ToolchainHandler
|
||||
|
||||
- 新建 `src/daemon/toolchain_handler.py`
|
||||
- 实现 3 个 section(ToolchainContext / ToolchainApi / ToolchainConstraints)
|
||||
- 注册到 TaskTypeRegistry
|
||||
- **风险**:低,全新代码
|
||||
|
||||
### Step 4:MailHandler
|
||||
|
||||
- 新建 `src/daemon/mail_handler.py`
|
||||
- 实现 3 个 section(MailContext / MailApi / MailConstraints)
|
||||
- 从 dispatcher / spawner / ticker 三处迁移 mail 逻辑
|
||||
- 注册到 TaskTypeRegistry
|
||||
- 重点回归测试 mail 路径:发送、回复、重试、幻觉门控
|
||||
- **风险**:中
|
||||
|
||||
### Step 5:引擎清理
|
||||
|
||||
- 删除 dispatcher / spawner / ticker 中的旧 if/else 分支
|
||||
- 删除或保留 BootstrapBuilder(作为 TaskContextSection 的内部实现)
|
||||
- 全量回归测试 + 手工验证
|
||||
- **风险**:中,删除代码需确保无遗漏
|
||||
|
||||
---
|
||||
|
||||
# §8 未来新增 Task Type 的步骤
|
||||
|
||||
以新增 `cron`(定时任务)为例:
|
||||
|
||||
1. **新建 handler 文件**:`src/daemon/cron_handler.py`
|
||||
2. **实现 `TaskTypeHandler` 接口**:填入每个方法的具体逻辑
|
||||
3. **注册**:在 daemon 启动时 `TaskTypeRegistry.register(CronHandler())`
|
||||
4. **完事**。dispatcher / spawner / ticker 不改一行。
|
||||
|
||||
可选:
|
||||
- 如需专用 API,在 `api/` 下新增路由
|
||||
- 如需前端展示,在对应 Tab 里加渲染模板
|
||||
|
||||
---
|
||||
|
||||
# §9 前后端接口联动方案
|
||||
|
||||
### 后端 API 设计原则
|
||||
|
||||
- 每个 task type 可以有自己专用的 API(如 `/api/mail`、`/api/toolchain`)
|
||||
- 也可以用统一 API 查询:`GET /api/tasks?type=mail|toolchain|task`
|
||||
- 新增 task type 时,API 层自由选择:复用统一接口或新增专用接口
|
||||
- 统一数据协议:`{id, type, status, title, from, to, metadata}`
|
||||
|
||||
### 前端展示原则
|
||||
|
||||
- 前端 Tab 和后端 task type 解耦——一个 Tab 可以展示多种 type,一种 type 也可以出现在多个 Tab
|
||||
- 新增 task type 不需要新增前端组件,只需在对应 Tab 里加渲染模板
|
||||
- 前端通过 `type` 字段区分渲染逻辑
|
||||
|
||||
### 本次范围
|
||||
|
||||
**前端不动**,后端架构先行。ToolchainHandler 的前端展示方案在后续迭代中确定。
|
||||
|
||||
---
|
||||
|
||||
# §10 风险评估
|
||||
|
||||
| 风险 | 概率 | 影响 | 缓解措施 |
|
||||
|------|------|------|----------|
|
||||
| MailHandler 迁移遗漏逻辑 | 中 | 高 | 逐步迁移,每步对比迁移前后全量测试;保留旧代码注释期 |
|
||||
| 注册表查询性能 | 低 | 低 | dict 查询 O(1),handler 数量个位数,忽略不计 |
|
||||
| ticker 自动发现虚拟项目引入 bug | 低 | 中 | 保留 `_general` 硬编码作为 fallback;`virtual_projects()` 仅返回显式注册的虚拟项目 |
|
||||
| 并发安全 | 低 | 高 | 注册表启动时一次性加载,运行时只读,无线程安全问题 |
|
||||
| Handler 接口设计不足 | 低 | 中 | Protocol 可后续扩展方法,默认实现提供合理 fallback |
|
||||
| 引擎清理删除过早 | 中 | 中 | Step 5 放在最后,确认所有 handler 稳定后再删旧代码 |
|
||||
|
||||
---
|
||||
|
||||
# §11 Prompt 构建现状分析
|
||||
|
||||
当前系统的 prompt 构建有四条独立路径,散落在三个文件中:
|
||||
|
||||
| 路径 | 位置 | 结构 | 服务对象 |
|
||||
|------|------|------|----------|
|
||||
| BootstrapBuilder 4 段 | `bootstrap.py` | 任务上下文 → 前序产出 → 角色规范(Skill全文) → 硬约束 | 普通任务 |
|
||||
| Mail inform 模板 | `spawner.py` MAIL_INFORM_TEMPLATE | from/to/title/text 纯文本 | mail 通知 |
|
||||
| Mail request 模板 | `spawner.py` MAIL_REQUEST_TEMPLATE | from/to/title/text + API 指令 | mail 请求 |
|
||||
| Toolchain 模板 | `toolchain_templates.py` + `templates/toolchain/*.md` | 5 个 md 文件占位符渲染 | 工具链事件 |
|
||||
|
||||
此外 `_build_api_section` 在 spawner 中为所有路径拼 API 操作指令,但 success_status 不同(mail="done",其他="review")。
|
||||
|
||||
**问题**:
|
||||
|
||||
1. **四条路径完全独立**,共性逻辑(API 指令、约束注入)重复实现
|
||||
2. **新增 task type 需要理解所有路径**才能加入
|
||||
3. **没有统一的 token 预算管理**
|
||||
4. **段的顺序硬编码**在各自的拼装逻辑中
|
||||
|
||||
---
|
||||
|
||||
# §12 PromptSection 模式
|
||||
|
||||
基于知识库优秀实践(Hermes 10层有序注入、Microsoft 三层中间件、我们自己的四层加载架构),引入 PromptSection 模式。
|
||||
|
||||
## 核心思想
|
||||
|
||||
把 prompt 拆成有序的 section 列表,handler 声明自己需要哪些 section,PromptComposer 统一拼装。
|
||||
|
||||
## PromptSection 协议
|
||||
|
||||
```python
|
||||
class PromptSection(Protocol):
|
||||
"""一个 prompt 段"""
|
||||
name: str # 段名(去重用,同名覆盖)
|
||||
priority: int # 排序优先级(小数字=靠前)
|
||||
|
||||
def render(self, context: PromptContext) -> str:
|
||||
"""渲染此段的文本内容
|
||||
|
||||
Args:
|
||||
context: 包含 task_id, title, description, must_haves,
|
||||
project_id, agent_id, task, role 等信息的上下文对象
|
||||
Returns:
|
||||
此段的文本内容(可以为空字符串表示不注入)
|
||||
"""
|
||||
...
|
||||
|
||||
def should_include(self, context: PromptContext) -> bool:
|
||||
"""是否注入此段(默认 True,条件段可覆盖)
|
||||
|
||||
例如:前序产出段只在有 depends_on 时注入
|
||||
"""
|
||||
return True
|
||||
```
|
||||
|
||||
## PromptContext 数据对象
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class PromptContext:
|
||||
"""Prompt 渲染的统一上下文"""
|
||||
task_id: str
|
||||
title: str
|
||||
description: str
|
||||
must_haves: str
|
||||
project_id: str
|
||||
agent_id: str
|
||||
task: Optional[Dict] = None
|
||||
role: str = "executor"
|
||||
spawn_type: str = "executor"
|
||||
# mail 专用
|
||||
from_agent: str = ""
|
||||
mail_type: str = "" # inform / request
|
||||
# toolchain 专用
|
||||
event_type: str = "" # ci_failure / review_request / ...
|
||||
event_data: Dict = field(default_factory=dict)
|
||||
# 前序产出
|
||||
depends_on_outputs: Optional[List] = None
|
||||
```
|
||||
|
||||
## PromptComposer 拼装器
|
||||
|
||||
```python
|
||||
class PromptComposer:
|
||||
"""有序拼装 prompt sections"""
|
||||
|
||||
def __init__(self):
|
||||
self._sections: List[PromptSection] = []
|
||||
|
||||
def add(self, section: PromptSection) -> None:
|
||||
"""添加一个 section(同名覆盖)"""
|
||||
self._sections = [s for s in self._sections if s.name != section.name]
|
||||
self._sections.append(section)
|
||||
|
||||
def add_many(self, sections: List[PromptSection]) -> None:
|
||||
"""批量添加"""
|
||||
for s in sections:
|
||||
self.add(s)
|
||||
|
||||
def compose(self, context: PromptContext) -> str:
|
||||
"""拼装最终 prompt
|
||||
|
||||
1. 过滤 should_include=False 的段
|
||||
2. 按 priority 排序
|
||||
3. 逐段 render
|
||||
4. 用分隔符连接
|
||||
5. Token 预算警告(不截断)
|
||||
"""
|
||||
active = [s for s in self._sections if s.should_include(context)]
|
||||
active.sort(key=lambda s: s.priority)
|
||||
parts = [s.render(context) for s in active]
|
||||
parts = [p for p in parts if p.strip()] # 过滤空段
|
||||
return "\n\n---\n\n".join(parts)
|
||||
```
|
||||
|
||||
## Section 优先级约定
|
||||
|
||||
| 优先级范围 | 用途 | 示例 |
|
||||
|------------|------|------|
|
||||
| 10-19 | 任务上下文 | 任务标题/描述、Mail 内容、Toolchain 事件 |
|
||||
| 20-29 | 前序信息 | depends_on 产出、handoff comment |
|
||||
| 30-39 | 角色规范 | Skill 全文注入、工具链行为指引 |
|
||||
| 40-49 | API 操作指令 | 状态回写、curl 示例 |
|
||||
| 50-59 | 硬约束 | 安全红线、禁止行为 |
|
||||
| 60-69 | 扩展段 | 保留给未来使用 |
|
||||
|
||||
---
|
||||
|
||||
# §13 三个 Handler 的 Section 注册
|
||||
|
||||
每个 handler 通过 `get_sections()` 声明自己需要的 section 列表。
|
||||
|
||||
## TaskHandler sections
|
||||
|
||||
```python
|
||||
def get_sections(self) -> list[PromptSection]:
|
||||
return [
|
||||
TaskContextSection(priority=10), # BootstrapBuilder 段 1
|
||||
PriorOutputsSection(priority=20), # BootstrapBuilder 段 2(有 depends_on 时)
|
||||
RoleSkillSection(priority=30), # BootstrapBuilder 段 3(Skill 全文)
|
||||
TaskApiSection(priority=40), # API 操作指令,success_status="review"
|
||||
TaskConstraintsSection(priority=50), # 硬约束
|
||||
]
|
||||
```
|
||||
|
||||
| Section | 来源 | 共性/个性 |
|
||||
|---------|------|------------|
|
||||
| TaskContextSection | BootstrapBuilder 段 1 | 个性:title/desc/must_haves 格式 |
|
||||
| PriorOutputsSection | BootstrapBuilder 段 2 | 个性:只有 task 有 depends_on |
|
||||
| RoleSkillSection | BootstrapBuilder 段 3 | 个性:只有 task 读 Skill 全文 |
|
||||
| TaskApiSection | spawner `_build_api_section` | **共性基础 + 个性参数**(success_status) |
|
||||
| TaskConstraintsSection | BootstrapBuilder 段 4 | 个性:每种 task 约束不同 |
|
||||
|
||||
## MailHandler sections
|
||||
|
||||
```python
|
||||
def get_sections(self) -> list[PromptSection]:
|
||||
return [
|
||||
MailContextSection(priority=10), # from/to/title/text,区分 inform/request
|
||||
MailApiSection(priority=40), # API 操作指令,success_status="done"
|
||||
MailConstraintsSection(priority=50), # 硬约束(禁止状态转换命令等)
|
||||
]
|
||||
```
|
||||
|
||||
| Section | 来源 | 共性/个性 |
|
||||
|---------|------|------------|
|
||||
| MailContextSection | MAIL_INFORM_TEMPLATE / MAIL_REQUEST_TEMPLATE | 个性:邮件格式 |
|
||||
| MailApiSection | spawner `_build_api_section` 变体 | **共性基础 + 个性参数**(success_status="done",含 Mail API 指令) |
|
||||
| MailConstraintsSection | 模板中的 ⚠️ 约束 | 个性 |
|
||||
|
||||
## ToolchainHandler sections
|
||||
|
||||
```python
|
||||
def get_sections(self) -> list[PromptSection]:
|
||||
return [
|
||||
ToolchainContextSection(priority=10), # 事件类型 + 事件详情
|
||||
ToolchainApiSection(priority=40), # API 操作指令,success_status="done"
|
||||
ToolchainConstraintsSection(priority=50), # 硬约束
|
||||
]
|
||||
```
|
||||
|
||||
| Section | 来源 | 共性/个性 |
|
||||
|---------|------|------------|
|
||||
| ToolchainContextSection | toolchain_templates.py + md 文件 | 个性:事件格式 |
|
||||
| ToolchainApiSection | spawner `_build_api_section` 变体 | **共性基础 + 个性参数** |
|
||||
| ToolchainConstraintsSection | 新增 | 个性 |
|
||||
|
||||
## Section 复用分析
|
||||
|
||||
| Section | task | mail | toolchain | 是否可复用 |
|
||||
|---------|------|------|-----------|-------------|
|
||||
| *ContextSection | ✅ | ✅ | ✅ | ❌ 格式完全不同,各自实现 |
|
||||
| *ApiSection | ✅ | ✅ | ✅ | ⚠️ 基础框架可复用(BaseApiSection),success_status 参数化 |
|
||||
| *ConstraintsSection | ✅ | ✅ | ✅ | ❌ 约束内容不同,各自实现 |
|
||||
| PriorOutputsSection | ✅ | ❌ | ❌ | 仅 task |
|
||||
| RoleSkillSection | ✅ | ❌ | ❌ | 仅 task |
|
||||
|
||||
**结论**:ApiSection 可以抽一个 BaseApiSection(curl 模板 + success_status 参数),其余 section 各自实现。
|
||||
|
||||
---
|
||||
|
||||
## 附录:文件结构预览
|
||||
|
||||
```
|
||||
src/daemon/
|
||||
├── task_type_registry.py # §3 + §4:Protocol + Registry
|
||||
├── prompt_composer.py # §12 PromptSection + PromptContext + PromptComposer
|
||||
├── task_handler.py # §13 TaskHandler + 5 sections
|
||||
├── mail_handler.py # §13 MailHandler + 3 sections
|
||||
├── toolchain_handler.py # §13 ToolchainHandler + 3 sections
|
||||
├── dispatcher.py # §6 改动
|
||||
├── spawner.py # §6 改动
|
||||
├── ticker.py # §6 改动
|
||||
├── bootstrap.py # 保留,TaskContextSection 内部调用
|
||||
└── toolchain_templates.py # 保留,ToolchainContextSection 内部调用
|
||||
```
|
||||
@@ -459,11 +459,33 @@ async def _handle_issue_comment(payload: Dict[str, Any]) -> None:
|
||||
# 提取错误摘要(取 comment body 前 500 字符)
|
||||
error_summary = body[:500] if body else "(无错误信息)"
|
||||
|
||||
# 尝试从 commit sha 查询 CI run URL
|
||||
ci_url = ""
|
||||
sha_match = re.search(r"commit:\s*`?([0-9a-f]{40})`?", body)
|
||||
if sha_match:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
resp = await client.get(
|
||||
f"{_GITEA_BASE}/commits/{sha_match.group(1)}/status",
|
||||
headers={"Authorization": f"token {_GITEA_TOKEN}"},
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
statuses = resp.json().get("statuses", [])
|
||||
for s in statuses:
|
||||
if s.get("status") == "failure" or s.get("state") == "failure":
|
||||
target = s.get("target_url", "")
|
||||
if target:
|
||||
ci_url = "http://192.168.2.154:3000" + target if target.startswith("/") else target
|
||||
break
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to query CI run URL: %s", exc)
|
||||
|
||||
text = render_template("ci_failure", {
|
||||
"repo": repo,
|
||||
"pr_number": str(issue_number),
|
||||
"branch": branch,
|
||||
"error_summary": error_summary,
|
||||
"ci_url": ci_url,
|
||||
})
|
||||
|
||||
title = f"CI 失败: {repo}#{issue_number}"
|
||||
|
||||
@@ -7,3 +7,5 @@ PR: http://192.168.2.154:3000/{repo}/pulls/{pr_number}
|
||||
{error_summary}
|
||||
|
||||
请检查 CI 日志并修复。修复后 push 会自动触发 CI。
|
||||
|
||||
CI 日志链接: {ci_url}
|
||||
|
||||
Reference in New Issue
Block a user