Compare commits

...

14 Commits

Author SHA1 Message Date
pangtong-fujunshi b953f6da02 Merge pull request 'fix: S1-S4 建议项修复(终验)' (#24) from docs/s-fixes into main
Deploy / ci (push) Failing after 7s
Deploy / deploy (push) Has been skipped
Deploy / notify-deploy-failure (push) Successful in 1s
2026-06-10 16:40:41 +08:00
cfdaily cc974bf258 fix: S1-S4 建议项修复 — 类型标注精确化+BaseTaskHandler标注后续PR+token预算说明
CI / lint (pull_request) Failing after 8s
CI / test (pull_request) Has been skipped
CI / notify-on-failure (pull_request) Successful in 2s
2026-06-10 16:38:00 +08:00
pangtong-fujunshi 42a28585b8 Merge pull request 'docs: Task 系统架构重构设计 v3.0 — 五层架构+BaseTaskHandler+执行流程+决策记录' (#23) from docs/task-type-architecture into main
Deploy / ci (push) Failing after 6s
Deploy / deploy (push) Has been skipped
Deploy / notify-deploy-failure (push) Successful in 0s
2026-06-10 15:42:10 +08:00
cfdaily 0e4d12898d fix: M1-M4 修复 Protocol 签名与设计文档对齐 + §14 去重
CI / lint (pull_request) Failing after 7s
CI / test (pull_request) Has been skipped
CI / notify-on-failure (pull_request) Successful in 3s
2026-06-10 15:41:54 +08:00
cfdaily 86504faf1f docs: 20-task-type-architecture.md v3.0 - §14-§18 五层架构+BaseTaskHandler+执行流程+决策记录 2026-06-10 15:41:54 +08:00
cfdaily 2eba38a5a0 feat: Step 1 — TaskTypeRegistry + PromptComposer 基础设施
- task_type_registry.py: TaskTypeHandler Protocol (10方法+2属性) + TaskTypeRegistry 注册表
- prompt_composer.py: PromptSection Protocol + PromptContext dataclass + PromptComposer 拼装器
- 零依赖,纯新增文件,不影响现有功能
2026-06-10 15:41:54 +08:00
pangtong-fujunshi b0b9a72445 Merge pull request 'docs: Task 系统架构重构设计文档 v2.1(纯文档)' (#22) from docs/task-type-architecture into main
Deploy / ci (push) Successful in 9s
Deploy / deploy (push) Successful in 13s
Deploy / notify-deploy-failure (push) Successful in 1s
2026-06-10 12:42:25 +08:00
cfdaily 1c6b66dc63 docs: 20-task-type-architecture.md v2.1 - 修复 review M1-M3 必修项
CI / lint (pull_request) Successful in 7s
CI / test (pull_request) Successful in 9s
CI / notify-on-failure (pull_request) Successful in 3s
2026-06-10 12:41:43 +08:00
cfdaily 3fa6040b93 docs: 20-task-type-architecture.md v2.0 - 新增 §11-§13 PromptSection 模式 2026-06-10 12:41:43 +08:00
cfdaily 1485719b0e docs: add 20-task-type-architecture.md - TaskTypeRegistry + Handler 架构重构设计 2026-06-10 12:41:43 +08:00
cfdaily b00d43c8ac docs(#13): merge #19 context layers into #13, delete standalone #19
§19 上下文四层改造方案(原独立文档 #19)合并到 #13 工具链设计文档末尾。
v3.1 → v3.3。两个专题本就是一个整体,分开维护增加认知负担。
2026-06-10 12:41:43 +08:00
jiangwei-infra 25e7d46328 Merge pull request 'fix(frontend): resumed_from null→undefined 类型兼容' (#21) from fix/frontend-null-vs-undefined into main
Deploy / ci (push) Successful in 10s
Deploy / deploy (push) Successful in 11s
Deploy / notify-deploy-failure (push) Successful in 1s
2026-06-10 08:12:08 +08:00
cfdaily c1381c0c93 fix(frontend): resumed_from null→undefined 类型兼容
CI / lint (pull_request) Successful in 9s
CI / test (pull_request) Successful in 8s
CI / notify-on-failure (pull_request) Successful in 0s
TypeScript: resumed_from 是 string|null,StatusButtons 期望 string|undefined。
用 ?? undefined 转换。
2026-06-10 08:10:58 +08:00
jiangwei-infra 9a62a45a12 Merge pull request 'fix(test): e2e test collection crash - 跳过 import 安装目录' (#20) from fix/e2e-collection-crash into main
Deploy / ci (push) Successful in 9s
Deploy / deploy (push) Failing after 8s
Deploy / notify-deploy-failure (push) Successful in 0s
2026-06-10 07:53:58 +08:00
6 changed files with 1617 additions and 384 deletions
+376 -1
View File
@@ -1,6 +1,6 @@
# 三国团队工具链与开发流程设计
> **状态**: v3.1P3 端到端验证通过 + 调研结论写入 + 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.md5 行信息,无流程引导;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 各层具体内容
##### L1AGENTS.md 加工具链协作行为段(所有 Agent 统一)
```markdown
## 工具链协作(Gitea
收到 Gitea 事件通知(Issue 指派、Review 请求、CI 失败等)时,按以下流程操作:
### 基本流程
- **Issue 指派** → clone 仓库 → 开分支 → 编码 → 提 PR(参考 git-workflow Skill
- **Review 请求** → 读 PR diffGitea 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. 所有模板变量统一补齐
##### L3Skill 按需加载(不改 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 改造 |
-382
View File
@@ -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.md5 行信息,无流程引导;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 各层具体内容
#### L1AGENTS.md 加工具链协作行为段(所有 Agent 统一)
```markdown
## 工具链协作(Gitea
收到 Gitea 事件通知(Issue 指派、Review 请求、CI 失败等)时,按以下流程操作:
### 基本流程
- **Issue 指派** → clone 仓库 → 开分支 → 编码 → 提 PR(参考 git-workflow Skill
- **Review 请求** → 读 PR diffGitea 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. 所有模板变量统一补齐
#### L3Skill 按需加载(不改 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 改造 |
File diff suppressed because it is too large Load Diff
+127
View File
@@ -0,0 +1,127 @@
"""
prompt_composer.py — PromptSection Protocol + PromptContext + PromptComposer
拼装器:有序管理 prompt 段落,按优先级排序后合并为最终 prompt。
"""
import logging
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Protocol, runtime_checkable
logger = logging.getLogger("moziplus-v2.prompt_composer")
# ---------------------------------------------------------------------------
# Section 优先级范围约定
# ---------------------------------------------------------------------------
PRIORITY_CONTEXT = 10 # 任务上下文
PRIORITY_PRIOR = 20 # 前序信息
PRIORITY_ROLE = 30 # 角色规范
PRIORITY_API = 40 # API 操作指令
PRIORITY_CONSTRAINTS = 50 # 硬约束
PRIORITY_EXTENSION = 60 # 扩展段
# ---------------------------------------------------------------------------
# PromptSection Protocol
# ---------------------------------------------------------------------------
@runtime_checkable
class PromptSection(Protocol):
"""一个 prompt 段"""
name: str # 段名(去重用,同名覆盖)
priority: int # 排序优先级(小数字=靠前)
def render(self, context: "PromptContext") -> str:
"""渲染此段的文本内容。返回空字符串表示不注入。"""
...
def should_include(self, context: "PromptContext") -> bool:
"""是否注入此段(默认 True,条件段可覆盖)。"""
...
# ---------------------------------------------------------------------------
# PromptContext 数据对象
# ---------------------------------------------------------------------------
@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 拼装器
# ---------------------------------------------------------------------------
class PromptComposer:
"""有序拼装 prompt sections"""
SEPARATOR = "\n\n---\n\n"
TOKEN_BUDGET_WARN = 800 # token 预算警告阈值
CHARS_PER_TOKEN = 3.5 # 估算比率
def __init__(self) -> None:
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. 用分隔符连接
6. 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()]
result = self.SEPARATOR.join(parts)
# Token 估算
tokens = max(1, int(len(result) / self.CHARS_PER_TOKEN))
logger.debug(
"Composed prompt from %d sections, %d tokens",
len(parts), tokens,
)
if tokens > self.TOKEN_BUDGET_WARN:
logger.warning(
"Prompt exceeds %d token budget: %d tokens (task_id=%s)",
self.TOKEN_BUDGET_WARN, tokens, context.task_id,
)
return result
+102
View File
@@ -0,0 +1,102 @@
"""
task_type_registry.py — Task type handler Protocol + Registry.
启动时一次性加载 handler,运行时只读。
零依赖:不导入项目内其他模块。
"""
from __future__ import annotations
import logging
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Protocol, runtime_checkable
if TYPE_CHECKING:
from src.daemon.prompt_composer import PromptContext
logger = logging.getLogger("moziplus-v2.registry")
# ---------------------------------------------------------------------------
# Protocol
# ---------------------------------------------------------------------------
@runtime_checkable
class TaskTypeHandler(Protocol):
"""所有 task type handler 的统一接口。"""
# 属性(通过 __init__ 设置)
task_type: str # 类型标识:'task' | 'mail' | 'toolchain'
virtual_project: Optional[str] # 虚拟项目 ID,如 '_mail'、'_toolchain'。普通任务为 None
def build_prompt(self, context: "PromptContext") -> str:
"""构建 Agent prompt(通过 PromptComposer 拼 section)。"""
...
def pre_spawn(self, task_id: str, db_path: Path) -> bool:
"""spawn 前业务准备。默认 Truemail/toolchain override 为 auto_working。"""
...
def post_complete(
self,
task_id: str,
agent_id: str,
outcome: str,
db_path: Path,
) -> None:
"""spawn 完成后的业务处理。统一流程:crash→verify→mark→notify。"""
...
def check_completion(self, task_id: str, db_path: Path) -> bool:
"""ticker 级别的完成检查。"""
...
def get_sections(self) -> list:
"""返回此 handler 的 prompt section 列表。"""
...
# ---------------------------------------------------------------------------
# Registry
# ---------------------------------------------------------------------------
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
vp = getattr(handler, "virtual_project", None)
logger.info("Registered task type handler: %s (virtual_project=%s)", handler.task_type, vp)
@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
]
@classmethod
def clear(cls) -> None:
"""清空注册表(仅测试用)。"""
cls._handlers = {}
+1 -1
View File
@@ -426,7 +426,7 @@ export default function TaskModal() {
{/* 状态操作 */}
<div style={{ marginBottom: 16 }}>
<SectionLabel icon="🔄" title="状态操作" />
<StatusButtons status={task.status} taskId={task.id} resumedFrom={task.resumed_from} />
<StatusButtons status={task.status} taskId={task.id} resumedFrom={task.resumed_from ?? undefined} />
</div>
{/* v2.7: 子 Task 进度 + 列表 */}