diff --git a/docs/design/13-toolchain-and-dev-workflow.md b/docs/design/13-toolchain-and-dev-workflow.md index c36b972..8bddc2b 100644 --- a/docs/design/13-toolchain-and-dev-workflow.md +++ b/docs/design/13-toolchain-and-dev-workflow.md @@ -1474,3 +1474,545 @@ CI workflow 和 Webhook 中的用户标识是 Git 用户名,Mail API 需要 Ag | zhaoyun | zhaoyun-data | daemon Webhook 模块中维护此映射表。 + + +--- + +## §16. 事件中枢详细设计 + +> §16 是对 §15 串联架构的落地设计补充,定义事件中枢的具体实现方案。 +> 版本: v1.0-draft | 日期: 2026-06-07 | 状态: 待评审 + +## §16.0. 讨论纪要 + +本节记录设计过程中的关键讨论,供未来回溯。 + +### 两条线的定位(主公决策) + +| 线 | 方向 | 机制 | 定位 | +|----|------|------|------| +| **出线** | Agent → Gitea | Agent 直接操作 Gitea,完成后发 Mail 通知下一个 Agent | Gitea = 黑板,Mail = 触发器 | +| **入线** | 外部 → Agent | 外部事件(CI/部署/Webhook)→ 事件中枢 → Mail → Agent | 事件中枢 = 入口 | + +**原则 1**:Agent 之间的协作不走事件中枢。中枢只处理"外部 → Agent"。 +**原则 2**:所有工具链留痕在 Gitea,Mail 是辅助推送。即使 Mail 失败,Agent 可主动查 Gitea 恢复上下文。 +**原则 3**:工具链反馈 Agent 的通知统一走事件中枢,避免散乱。 + +### 关键决策记录 + +| # | 决策 | 理由 | +|---|------|------| +| D1 | 不做第三种 task 类型 | 中枢产出就是 Mail(from=system, type=inform),模板填充 description,投递复用 spawner。区别只在内容(模板化)和元数据(结构化),不需要新的数据结构 | +| D2 | 提示词独立模板文件 | 模板和代码分离,改文案不改代码,可审查可扩展 | +| D3 | 中枢是 daemon 内模块 | 要访问 Blackboard 创建 Task,共享进程。用独立路由模块(`toolchain_routes.py`)保持职责清晰 | +| D4 | 同步处理 | 事件处理很轻(解析+模板填充+创建Task),远在 Gitea 5秒超时内。复杂事件未来用 asyncio.create_task | +| D5 | Agent ID = Gitea 用户名 | 设计目标一致,代码里留 `to_agent_id()` 函数兜底,实际直用。待姜维确认各 Agent 在 Gitea 上的注册用户名 | +| D6 | CI/部署通知也走事件中枢 | 统一入口,不走 workflow 直接调 Mail API。CI workflow 写 PR comment → Gitea 触发 issue_comment Webhook → 中枢处理 | + +--- + +## §16.1. 定位与边界 + +### 1.1 是什么 + +事件中枢是 daemon 内的一个路由模块,负责将**外部工具链事件**翻译成 Mail 通知推送给 Agent。 + +### 1.2 不是什么 + +- 不是消息队列 +- 不是 Agent 间通信通道(Agent 间协作走 Mail,不经过中枢) +- 不是编排引擎(任务编排由 dispatcher + spawner 负责) + +### 1.3 两条线 + +``` +出线(Agent → 工具链): + Agent 直接操作 Gitea(创建 Issue/PR/Review 等) + → 完成后发 Mail 通知下一个 Agent + → 下一个 Agent 去读 Gitea 继续干活 + +入线(工具链 → Agent): + 外部事件(CI 结果/部署结果/Webhook 事件) + → 事件中枢 + → 模板化 Mail + → Agent 收到通知,去 Gitea 查看详情并行动 +``` + +### 1.4 数据流 + +``` +外部事件源 事件中枢 Agent +───────── ────── ────── + +Gitea Webhook ─────┐ + │ +CI workflow ───────┤ toolchain_routes.py Mail Task + (PR comment) ────┤ ┌─────────────────┐ (from: system) + ├──→│ 1. 接收事件 │ │ +Deploy workflow ──┤ │ 2. 验签/过滤 │ ↓ + (PR comment) ────┤ │ 3. 幂等检查 │ dispatcher + │ │ 4. 选模板+填充 │ │ +daemon 内部 ───────┘ │ 5. 创建 Mail │ ↓ + (预留接口) └─────────────────┘ spawner 投递 +``` + +--- + +## §16.2. 事件分类与处理 + +### 2.1 事件来源 + +| 来源 | 触发方式 | 事件类型 | +|------|---------|---------| +| Gitea Webhook | Gitea 主动 POST | PR opened, Review submitted, Issue assigned, issue_comment | +| CI workflow | CI 写 PR comment → 触发 issue_comment Webhook | CI 失败 | +| Deploy workflow | Deploy 写 PR comment → 触发 issue_comment Webhook | 部署成功/失败 | +| daemon 内部 | 内部函数调用 | 预留,当前不走中枢 | + +### 2.2 事件处理矩阵 + +| 事件 | 来源 | 通知谁 | 模板 | 关键信息 | +|------|------|--------|------|---------| +| `pull_request` opened | Gitea Webhook | 司马懿(Review) | `review_request.md` | PR号、标题、作者、分支、文件列表、风险级别 | +| `pull_request_review` submitted | Gitea Webhook | PR 作者 | `review_result.md` | PR号、审查结论、评论、审查者 | +| `issues` assigned | Gitea Webhook | 被指派人 | `issue_assigned.md` | Issue号、标题、标签、描述、分支名建议 | +| `issue_comment` (CI失败) | CI workflow → Webhook | PR 作者 | `ci_failure.md` | PR号、分支、失败步骤、错误摘要 | +| `issue_comment` (部署成功) | Deploy workflow → Webhook | 庞统 | `deploy_success.md` | 仓库、版本、commit | +| `issue_comment` (部署失败) | Deploy workflow → Webhook | 庞统 + 姜维 | `deploy_failure.md` | 仓库、失败原因、已回滚版本 | + +### 2.3 事件过滤规则 + +不是所有事件都需要处理: + +| 规则 | 说明 | +|------|------| +| 只处理白名单内的事件类型 | 未知的忽略 + 日志 | +| issue_comment 需判断来源 | 只处理 CI/deploy workflow 写的评论(按特定前缀匹配,如 `[CI]`、`[Deploy]`) | +| PR 作者/审查者必须是已知 Agent | 未知的忽略 + 日志 | +| 幂等:同一事件不重复创建 Mail | 按 `{event_type}-{payload_id}` 去重 | + +--- + +## §16.3. 模板系统 + +### 3.1 模板文件组织 + +``` +templates/toolchain/ +├── review_request.md # PR opened → 司马懿 Review +├── review_result.md # Review submitted → PR 作者 +├── issue_assigned.md # Issue assigned → 开发者 +├── ci_failure.md # CI 失败 → PR 作者 +├── deploy_success.md # 部署成功 → 庞统 +└── deploy_failure.md # 部署失败 → 庞统+姜维 +``` + +### 3.2 模板格式 + +模板使用 `{variable}` 占位符,中枢填充后生成 Mail description。 + +**示例:`review_request.md`** + +```markdown +PR Review 请求 + +PR: http://192.168.2.154:3000/{repo}/pulls/{pr_number} +标题: {pr_title} +作者: {pr_author} +分支: {branch} +风险级别: {risk_level} +改动文件: +{file_list} + +流程: +1. 读取 PR diff(Gitea API: GET /repos/{repo_owner}/{repo}/pulls/{pr_number}.diff) +2. 按审查清单审查(参考 code-review Skill) +3. 提交 Review(Gitea API: POST /repos/{repo_owner}/{repo}/pulls/{pr_number}/reviews) +4. 提交后改动者会自动收到通知 + +完成后回复此 Mail 确认。 +``` + +### 3.3 模板变量提取 + +| 变量 | 来源 | 提取方式 | +|------|------|---------| +| `pr_number` | Webhook payload | `event["pull_request"]["number"]` | +| `pr_title` | Webhook payload | `event["pull_request"]["title"]` | +| `pr_author` | Webhook payload | `to_agent_id(event["pull_request"]["user"]["login"])` | +| `branch` | Webhook payload | `event["pull_request"]["head"]["ref"]` | +| `file_list` | Webhook payload | `event["pull_request"]["changed_files"]` 或需要额外 API 调用 | +| `risk_level` | daemon 计算 | 按文件路径规则匹配(见 §3.4) | +| `repo` | Webhook payload | 从 `event["repository"]["full_name"]` 提取 | + +### 3.4 风险级别自动判定(简化版) + +按改动文件路径匹配规则: + +```python +HIGH_PATTERNS = ["**/spawner*", "**/ticker*", "**/dispatcher*", + "**/router*", "**/guardrails*", "**/strategy*", "**/risk*"] + +def calc_risk_level(changed_files: list[str]) -> str: + for f in changed_files: + for pattern in HIGH_PATTERNS: + if fnmatch(f, pattern): + return "high" + return "standard" +``` + +--- + +## §16.4. 技术设计 + +### 4.1 模块结构 + +``` +src/api/ +├── toolchain_routes.py # 事件中枢路由(~150行) +├── mail_routes.py # 现有 Mail API +└── ... + +src/daemon/ +├── toolchain_templates.py # 模板加载+填充(~80行) +├── mail_notify.py # 现有 Mail 失败通知 +└── ... + +templates/toolchain/ +├── review_request.md +├── review_result.md +├── issue_assigned.md +├── ci_failure.md +├── deploy_success.md +└── deploy_failure.md +``` + +### 4.2 toolchain_routes.py 接口设计 + +```python +# src/api/toolchain_routes.py + +from fastapi import APIRouter, Request, Header, HTTPException +from src.daemon.toolchain_templates import TemplateEngine + +router = APIRouter() +engine = TemplateEngine() + +GITEA_WEBHOOK_SECRET = os.environ.get("GITEA_WEBHOOK_SECRET", "") + +@router.post("/webhook/gitea") +async def handle_gitea_webhook( + request: Request, + x_gitea_event: str = Header(...), + x_gitea_signature: str = Header(None), + x_gitea_delivery: str = Header(None), +): + """接收 Gitea Webhook,翻译成 Mail""" + + body = await request.body() + + # 1. 签名验证(可选) + if GITEA_WEBHOOK_SECRET: + expected = hmac.new(GITEA_WEBHOOK_SECRET.encode(), body, sha256).hexdigest() + if not hmac.compare_digest(expected, (x_gitea_signature or "")): + raise HTTPException(403, "Invalid signature") + + event = json.loads(body) + + # 2. 幂等检查 + event_key = f"{x_gitea_event}-{x_gitea_delivery}" + if is_duplicate(event_key): + return {"status": "duplicate"} + + # 3. 路由到对应处理器 + handler = HANDLERS.get(x_gitea_event) + if not handler: + logger.info("Ignoring unhandled event: %s", x_gitea_event) + return {"status": "ignored"} + + # 4. 处理事件 → 创建 Mail + try: + result = await handler(engine, event) + return {"status": "ok", "mail_id": result} + except Exception as e: + logger.exception("Failed to handle %s event", x_gitea_event) + raise HTTPException(500, str(e)) +``` + +### 4.3 事件处理器 + +每个事件类型一个处理函数,职责:解析 payload → 选模板 → 填充 → 创建 Mail Task。 + +```python +async def handle_pr_opened(engine: TemplateEngine, event: dict) -> str: + """PR opened → Review 请求给司马懿""" + pr = event["pull_request"] + + # 提取变量 + variables = { + "pr_number": pr["number"], + "pr_title": pr["title"], + "pr_author": to_agent_id(pr["user"]["login"]), + "branch": pr["head"]["ref"], + "repo": event["repository"]["full_name"], + "repo_owner": event["repository"]["owner"]["login"], + "risk_level": calc_risk_level(get_changed_files(pr)), + "file_list": format_file_list(get_changed_files(pr)), + } + + # 填充模板 + text = engine.render("review_request.md", variables) + + # 创建 Mail Task + meta = { + "source": "toolchain", + "webhook_event": "pull_request_opened", + "pr_number": pr["number"], + "repo": variables["repo"], + } + + return create_mail_task( + to="simayi-challenger", + title=f"Review 请求: PR #{pr['number']} {pr['title']}", + text=text, + meta=meta, + ) + + +async def handle_review_submitted(engine: TemplateEngine, event: dict) -> str: + """Review submitted → 结果通知 PR 作者""" + review = event["review"] + pr = event["pull_request"] + pr_author = to_agent_id(pr["user"]["login"]) + state = review["state"] # APPROVED / REQUEST_CHANGES / COMMENTED + + if state == "COMMENTED": + return None # 普通评论不通知 + + variables = { + "pr_number": pr["number"], + "pr_title": pr["title"], + "result": "通过" if state == "APPROVED" else "不通过", + "reviewer": to_agent_id(review["user"]["login"]), + "review_body": review["body"] or "无", + "repo": event["repository"]["full_name"], + "repo_owner": event["repository"]["owner"]["login"], + } + + template = "review_result.md" + text = engine.render(template, variables) + + return create_mail_task( + to=pr_author, + title=f"Review {variables['result']}: PR #{pr['number']}", + text=text, + meta={"source": "toolchain", "webhook_event": "review_submitted", "pr_number": pr["number"]}, + ) + + +async def handle_issue_comment(engine: TemplateEngine, event: dict) -> str: + """issue_comment → 判断来源,路由到 CI/部署通知""" + comment_body = event["comment"]["body"] + + if comment_body.startswith("[CI]"): + return await handle_ci_comment(engine, event) + elif comment_body.startswith("[Deploy]"): + return await handle_deploy_comment(engine, event) + + return None # 非 CI/部署评论不处理 + + +# HANDLERS 注册表 +HANDLERS = { + "pull_request": handle_pr_event, # 根据 action 分发 + "pull_request_review": handle_review_submitted, + "issues": handle_issue_event, # 根据 action 分发 + "issue_comment": handle_issue_comment, +} +``` + +### 4.4 共用函数 + +```python +def to_agent_id(gitea_username: str) -> str: + """Gitea 用户名 → Agent ID。当前设计目标一致,直用。""" + return gitea_username + +def create_mail_task(to: str, title: str, text: str, meta: dict) -> str: + """创建 Mail Task(from=system, type=inform) + 复用 mail_notify.py 的模式:直接通过 Blackboard 创建 Task。 + """ + # 从 mail_routes.py 提取共用逻辑,或直接复用 mail_notify 的方式 + ... + +def is_duplicate(event_key: str) -> bool: + """幂等检查:同一事件不重复创建 Mail""" + ... + +def calc_risk_level(files: list[str]) -> str: + """按文件路径规则判定风险级别""" + ... + +def get_changed_files(pr: dict) -> list[str]: + """从 PR payload 提取改动文件列表""" + ... +``` + +### 4.5 模板引擎 + +```python +# src/daemon/toolchain_templates.py + +from pathlib import Path +import string + +TEMPLATE_DIR = Path(__file__).parent.parent.parent / "templates" / "toolchain" + +class TemplateEngine: + def __init__(self, template_dir: Path = TEMPLATE_DIR): + self.template_dir = template_dir + self._cache = {} + + def render(self, template_name: str, variables: dict) -> str: + """加载模板文件并填充变量""" + if template_name not in self._cache: + path = self.template_dir / template_name + self._cache[template_name] = path.read_text() + + template = self._cache[template_name] + return template.format_map(defaultdict(str, variables)) +``` + +--- + +## §16.5. 错误处理 + +| 场景 | 处理 | HTTP 返回 | +|------|------|----------| +| 签名验证失败 | 日志 warning | 403 | +| payload 解析失败 | 日志 error | 200(不触发 Gitea 重试) | +| 未知事件类型 | 忽略 + 日志 info | 200 | +| 幂等检测到重复 | 忽略 + 日志 info | 200 | +| 未知 Agent(不在映射表) | 忽略 + 日志 warning | 200 | +| 模板填充失败 | 日志 error | 500(触发 Gitea 重试) | +| Mail 创建失败 | 日志 error | 500(触发 Gitea 重试) | + +**原则**: +- daemon 自身能处理的错误(格式、过滤、幂等)→ 返回 200,不让 Gitea 无意义重试 +- daemon 处理不了的错误(数据库、内部异常)→ 返回 500,让 Gitea 重试 + +--- + +## §16.6. CI/Deploy Workflow 配合 + +### 6.1 CI 失败通知 + +CI workflow 失败时写 PR comment,触发 issue_comment Webhook: + +```yaml +# .gitea/workflows/ci.yml +- name: Report failure + if: failure() + run: | + curl -s -X POST \ + "http://192.168.2.154:3000/api/v1/repos/{owner}/{repo}/issues/{pr_number}/comments" \ + -H "Authorization: token ${{ secrets.CI_TOKEN }}" \ + -H "Content-Type: application/json" \ + -d "{ + \"body\": \"[CI] CI 失败\\n\\n分支: ${{ gitea.ref_name }}\\n失败步骤: ${{ job.status }}\\n错误摘要: $(tail -20 $GITHUB_STEP_SUMMARY)\" + }" +``` + +### 6.2 Deploy 结果通知 + +同理,deploy workflow 成功/失败时写 comment: + +```yaml +# .gitea/workflows/deploy.yml +- name: Report success + if: success() + run: | + curl -s -X POST \ + "http://192.168.2.154:3000/api/v1/repos/{owner}/{repo}/issues/{pr_number}/comments" \ + -H "Authorization: token ${{ secrets.CI_TOKEN }}" \ + -d "{\"body\": \"[Deploy:成功] 版本: $(bash scripts/deploy.sh --version)\\n请确认后关闭 Issue.\"}" + +- name: Report failure + if: failure() + run: | + curl -s -X POST \ + "http://192.168.2.154:3000/api/v1/repos/{owner}/{repo}/issues/{pr_number}/comments" \ + -H "Authorization: token ${{ secrets.CI_TOKEN }}" \ + -d "{\"body\": \"[Deploy:失败] 已回滚到上一版本。原因: ...\"}" +``` + +--- + +## §16.7. 配置 + +| 环境变量 | 用途 | 默认值 | +|---------|------|--------| +| `GITEA_WEBHOOK_SECRET` | Webhook HMAC 签名密钥(可选) | 空(跳过验签) | +| `BLACKBOARD_ROOT` | Blackboard 数据根目录(已有) | `~/.sanguo_projects/sanguo_moziplus_v2/data` | +| `TOOLCHAIN_TEMPLATES_DIR` | 模板文件目录(可选) | `templates/toolchain/` | + +--- + +## §16.8. 待确认项 + +| # | 项 | 负责人 | 说明 | +|---|------|--------|------| +| 1 | 各 Agent 在 Gitea 上的注册用户名是否和 Agent ID 一致 | 姜维 | 决定是否需要映射表 | +| 2 | Gitea Webhook 是否已配置 secret | 姜维 | 当前已配未启用 | +| 3 | CI workflow 是否已有写 PR comment 的 step | — | 当前 CI workflow 可能没有 | +| 4 | `from=system` 走 HTTP API 还是只走内部函数 | — | mail_routes.py 当前只在内部函数支持 system | +| 5 | PR changed_files 是否包含在 Webhook payload 中 | — | Gitea v1.23.4 可能需要额外 API 调用 | + +--- + +## §16.9. 实施计划 + +| 步骤 | 内容 | 依赖 | +|------|------|------| +| 1 | `toolchain_routes.py` 骨架 + Gitea Webhook 接收 + 签名验证 | 无 | +| 2 | `toolchain_templates.py` 模板引擎 | 无 | +| 3 | 6 个模板文件 | 步骤 2 | +| 4 | 4 个事件处理器(PR/Review/Issue/Comment) | 步骤 1+3 | +| 5 | 幂等检查(内存缓存或 SQLite 记录) | 步骤 1 | +| 6 | `create_mail_task` 共用函数(从 mail_routes.py 提取) | 无 | +| 7 | CI/Deploy workflow 加 comment step | 步骤 1+4 | +| 8 | Gitea Webhook 启用 + 测试 | 姜维 | +| 9 | 端到端测试 | 步骤 1-8 | + +### 9.1 改动量估算 + +| 文件 | 行数 | 类型 | +|------|------|------| +| `src/api/toolchain_routes.py` | ~200 | 新增 | +| `src/daemon/toolchain_templates.py` | ~60 | 新增 | +| `templates/toolchain/*.md` | ~200(6个文件) | 新增 | +| `src/api/mail_routes.py` | ~20 | 修改(提取共用函数) | +| `.gitea/workflows/ci.yml` | ~15 | 修改(加 comment step) | +| `.gitea/workflows/deploy.yml` | ~15 | 修改(加 comment step) | +| **总计** | **~510** | | + +--- + +## §16.10. 评审检查清单 + +- [ ] §0 讨论纪要是否准确反映了决策过程 +- [ ] §1 两条线的边界是否清晰(出线不走中枢,入线统一走中枢) +- [ ] §2 事件矩阵是否有遗漏 +- [ ] §3 模板内容是否完整,变量提取是否正确 +- [ ] §4 接口设计是否合理,共用函数提取是否恰当 +- [ ] §5 错误处理策略是否完备 +- [ ] §6 CI/Deploy workflow 配合方案是否可行 +- [ ] §8 待确认项是否完整 +- [ ] §9 实施步骤是否合理 + + +### v2.0 → v2.1 变更(事件中枢落地设计) + +| 编号 | 变更内容 | +|------|---------| +| §16 | 新增事件中枢详细设计(§16.0-§16.10),基于 §15 串联架构 v2.0 的落地细节 |