Files
sanguo_moziplus_v2/docs/design/13-toolchain-and-dev-workflow.md
T

3290 lines
136 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 三国团队工具链与开发流程设计
> **状态**: v3.3 — #19 上下文四层改造合并 + CI 修复 + A13 修订
> **作者**: 庞统(副军师)🐦
> **评审**: 司马懿(仲达)🗡️
> **日期**: 2026-06-06
> **v0.4 变更**: 主公 6 条反馈(需求review前置、E2E用户触发、测试分工、审查默认司马懿、项目维度组织、分支保护说明)
> **v0.5 变更**: E2E 改为自动跑(CI隔离环境不污染生产)、三环境架构、schema变更检查确认、分支保护确认开启
> **v0.6 变更**: Agent spawn 走生产 openclaw(不隔离)、程序逻辑走 CI 隔离环境、路径硬编码修复清单
> **定位**: 普适工具链和开发流程,适用于三国团队所有项目,与 moziplus 2.0 解耦
---
## §1. 设计原则
| # | 原则 | 说明 |
|---|------|------|
| 1 | **声明式 + 代码执行分离** | 规则声明在配置中,执行由框架代码保证,不依赖 Agent 自觉(来源: moziplus-quality-governance |
| 2 | **能自动就自动** | CI/CD/部署能自动触发就不手动,人只做决策和审查 |
| 3 | **Skill 固化流程** | 所有流程通过 Skill(提示词)固化,不靠口头约定 |
| 4 | **分级治理** | 按风险级别选择流程严格度,不搞一刀切(来源: quality-gate-patterns |
| 5 | **验证才算完** | Agent 说"完成"不算,CI 通过 + 产出物存在才算(来源: 幻觉门控) |
| 6 | **每个项目自带部署** | 部署脚本归项目管,工具链负责触发和验证,不替项目做部署决策 |
| 7 | **需求/问题必须经过 Review** | 所有需求和问题提交前必须经过 Review,不直接进入开发 |
| 8 | **项目维度组织** | 所有内容(Issue、CI、部署、Skill 配置)以项目为单位组织,支持多项目并行 |
---
## §2. 基础设施层
### 2.1 代码托管:Gitea(唯一)
| 项 | 配置 |
|----|------|
| 地址 | `http://192.168.2.154:3000` |
| 版本 | v1.26.22026-06-11 从 v1.23.4 升级) |
| 认证 | HTTP + tokenadmin 账号(姜维持有) |
| 权限 | 姜维持有 admin 权限(启用 Actions、分支保护、org webhook 等) |
| 数据库 | SQLite3 |
| 部署方式 | Docker(NAS 群晖),数据卷 `/volume2/@docker/volumes/gitea-data/_data` |
### 2.2 CI/CDGitea Actions
| 项 | 配置 |
|----|------|
| Runner | Mac mini 裸机,gitea-runner v1.0.8(通过 PM2 管理 `sanguo-act-runner` |
| 配置文件 | `.gitea/workflows/*.yml`,每个项目自管 |
| 语法 | 兼容 GitHub Actionsv1.26.2 已验证支持 concurrency groups |
| 触发 | push / PR / tag / workflow_dispatch |
| v1.26 新增 | concurrency groups、re-run failed jobs、可配置 GITEA_TOKEN 权限 |
| 仍不支持 | `failure()``continue-on-error``timeout-minutes` |
### 2.4 Gitea 基础设施 Setup 记录(2026-06-11 姜维)
> 以下为 Gitea 从 v1.23.4 升级到 v1.26.2 的完整操作记录,作为未来参考。
#### 2.4.1 升级 v1.23.4 → v1.26.2
**升级原因**v1.23.4 不支持 concurrency groups,导致双倍触发问题无根因解法。
**升级步骤**
1. 备份:`docker exec sanguo_gitea gitea dump -c /data/gitea/conf/app.ini -f /data/gitea/gitea-backup-pre-v126.zip`765MB
2. 拉取镜像:Mac 上 skopeo 下载 → python docker SDK 远程 load(群晖 Docker Hub 太慢)
3. 停止旧容器 + rename 保留回滚
4. 启动新容器(数据库自动迁移 Migration[312]→[326],含 concurrency #323
5. 验证:API + Web UI + 仓库数据 + 用户数据
**踩坑:群晖内核 3.10 + git 2.52 不兼容**
- 根因:git 2.52 使用 `getrandom(2)` syscall,群晖内核 3.10.108 不支持(3.17 才加入)
- 症状:`git push``unable to create temporary file: Function not implemented`
- 修复:entrypoint 脚本在容器启动时自动从本地缓存降级 git 到 2.45.4
- 持久化:`/data/entrypoint-wrapper.sh` + `/data/git-2.45.4-r0.apk` 在数据卷里,容器重建不丢失
- 群晖内核**无法通过 DSM 升级**,内核版本跟硬件型号绑定
**完整重建命令**
```bash
docker -H tcp://192.168.2.154:2375 run -d \
--name sanguo_gitea \
--restart=always \
-p 3000:3000 \
-p 2221:22 \
-v /volume2/@docker/volumes/gitea-data/_data:/data \
-e GITEA__database__DB_TYPE=sqlite3 \
-e GITEA__database__PATH=/data/gitea/gitea.db \
-e GITEA__server__ROOT_URL=http://192.168.2.154:3000/ \
--entrypoint /bin/sh \
gitea/gitea:1.26.2 \
-c '/data/entrypoint-wrapper.sh'
```
#### 2.4.2 act_runner 升级 v0.2.11 → v1.0.8
**升级原因**v0.2.11 的 multi-step job 执行有 bugSetup Python 和 Lint step 被跳过。
**升级步骤**
1. 下载 `gitea-runner-1.0.8-darwin-arm64`(从 gitea.com releases
2. `codesign --force --sign -` 重签(macOS Gatekeeper 会 SIGKILL 未签名的二进制)
3. 替换 `/Users/chufeng/bin/act_runner`
4. PM2 restart `sanguo-act-runner`
**注意**act_runner 通过 **PM2** 管理(`sanguo-act-runner`),不是 launchd。launchd plist 仅为备份。
**PM2 常用命令**
```bash
pm2 restart sanguo-act-runner # 重启
pm2 logs sanguo-act-runner # 查看日志
pm2 show sanguo-act-runner # 详情
```
#### 2.4.3 CI Workflow 配置
**三个 workflow 文件**
| 文件 | 触发 | concurrency | 说明 |
|------|------|-------------|------|
| `ci.yml` | `pull_request` | `group: ci-${{ gitea.ref }}, cancel-in-progress: true` | 同一 PR 新 push 自动取消旧 run |
| `deploy.yml` | `push to main` | `group: deploy-${{ gitea.ref }}, cancel-in-progress: false` | 部署排队不取消 |
| `e2e.yml` | `workflow_dispatch` | `group: e2e-${{ gitea.ref }}, cancel-in-progress: true` | 手动触发 |
**Branch Protectionmain 分支)**
- 禁止直接 push
- status check`CI / lint (pull_request)` 必须通过
- 至少 1 人 Review
**⚠️ 踩坑**v1.26 上报的 commit status context 格式变了:
- 旧格式:`lint`
- 新格式:`CI / lint (pull_request)`
- branch protection 必须用新格式匹配,否则 merge 报 "Not all required status checks successful"
#### 2.4.4 Org Webhook 配置
- **对象**Gitea 组织 `sanguo` webhook id=28
- **URL**`http://192.168.2.153:8083/webhook/gitea`
- **事件**16 个(push/issues/PR/PR review 等)
**⚠️ 踩坑**Gitea v1.26 的 PATCH hooks API,只传 `{"active": true}` 会把 events 重置为 `["push"]`。**必须每次 PATCH 都带上完整的 events 列表。**
**临时措施(已恢复)**2026-06-10 曾临时关闭 webhookCI 错误大爆炸期间),2026-06-11 已恢复。
#### 2.4.5 凭据管理
| 凭据 | 用途 | 持有者 |
|------|------|--------|
| Gitea admin:cf7561523 | 仓库管理、branch protection、org webhook | 姜维 |
| Gitea PAT (jiangwei-infra) | API 操作、git clone/push | 姜维 |
| Gitea PAT (cfdaily) | CI workflow 中的 git 操作 | CI secrets |
#### 2.4.6 备份与回滚
| 项目 | 路径 | 说明 |
|------|------|------|
| Gitea 数据库备份 | `/data/gitea/gitea-backup-pre-v126.zip` | 升级前 dump |
| 旧容器 | 已清理 | 升级验证通过后 `docker rm` |
| 变更记录 | `~/.openclaw/workspace-jiangwei/changes/gitea-emergency-2026-06-10.md` | 完整操作日志 |
| 环境 | 位置 | 说明 |
|------|------|------|
| Mac mini 本机 | `~/.sanguo_projects/<project>/` | 主力开发和运行环境 |
| NAS Docker | `192.168.2.154` | 部分服务(Gitea、回测等) |
#### 2.4.7 Gitea 迁移验证记录(2026-06-11 司马懿)
> 验证 Gitea 从 gitee 迁移完成后的状态。所有验证在 2026-06-11 完成。
**仓库迁移状态**
| 项目 | Gitea 仓库 | 开发目录 | 远程地址 | gitee 残留 |
|------|-----------|---------|---------|----------|
| sanguo_moziplus_v2 | `sanguo/sanguo_moziplus_v2` | `~/.openclaw/sanguo_projects/sanguo_moziplus_v2/` | `http://192.168.2.154:3000/sanguo/sanguo_moziplus_v2.git` | ✅ 无 |
| sanguo_quant_live | `sanguo/sanguo_quant_live` | `~/.openclaw/sanguo_projects/sanguo_quant_live/` | `http://192.168.2.154:3000/sanguo/sanguo_quant_live.git` | ✅ 无 |
| sanguo_vnpy | `sanguo/sanguo_vnpy` | `~/.openclaw/sanguo_projects/sanguo_vnpy/` | `http://192.168.2.154:3000/sanguo/sanguo_vnpy.git` | ✅ 无 |
**验证方法**:在 3 个开发目录分别执行 `git remote -v`,确认 origin 指向 gitea 且无 gitee remote。
**CI 管道验证**
| 验证项 | 结果 | 备注 |
|--------|------|------|
| PR #33 Lint 修复 CI 通过 | ✅ | flake8 全通过 |
| CD pipeline (deploy.yml) 合并 | ✅ | 含 CI + deploy + notify-deploy-failure 三个 job |
| Branch protection 生效 | ✅ | main 分支需 CI 通过 + 1 人 Review 才能合并 |
| Gitea squash merge 兼容 | ✅ | `merge_commit_sha` 在 squash merge 下仍等于 gitea.sha |
**工具链事件中枢验证**
| Webhook → Mail 流 | 验证结果 |
|-------------------|--------|
| PR opened → Review 请求 Mail | ✅ 司马懿收到 PR #30-#35 的 Review 请求 |
| PR review → 结果 Mail | ✅ 张飞/庞统收到 Review 结果通知 |
| Issue assigned → 指派 Mail | ✅ E2E 验证通过) |
| CI 失败评论 → 通知 Mail | ✅ (E2E 验证通过) |
| PR synchronize → reviewer 重审 Mail | ✅ 新增(§23 |
| Review COMMENTED → PR 作者通知 | ✅ 新增(§23) |
**Agent Gitea 凭据**(各 Agent 自行持有 PAT):
| Agent | Gitea 用户名 | PAT 用途 |
|-------|-------------|--------:|
| simayi-challenger | simayi-challenger | PR Review 提交 |
| pangtong-fujunshi | pangtong-fujunshi | PR 创建/合并、代码 push |
| jiangwei-infra | jiangwei-infra | 基础设施配置(admin 级操作) |
---
## §3. 分支策略
### 3.1 采用 Trunk-Based Development(简化版)
选 Trunk-Based 而非 Git Flow 的理由:
- **团队小**(<10 人),不需要复杂的 release 分支
- **AI Agent 参与编码**Agent 在短生命周期 feature 分支上工作,完成后快速合并
- **CI 是安全网**main 分支始终可部署,feature 分支通过 CI 才能合并
前提条件:**Agent 必须知道当前所在分支**。git-workflow Skill 需注入 `git branch --show-current` 到 Agent 上下文(仲达 Q1 补充)。
### 3.2 分支规则
| 分支类型 | 命名 | 生命周期 | 合并方式 | 适用场景 |
|---------|------|---------|---------|---------|
| `main` | `main` | 永久 | — | 始终可部署 |
| feature | `feat/<issue-N>-<简述>` | <3天 | PR → CI → Review → merge | 新功能 |
| bugfix | `fix/<issue-N>-<简述>` | <1天 | PR → CI → merge | Bug 修复 |
| hotfix | `hotfix/<简述>` | <2小时 | 直接 push main + CI + 自动回滚 | 紧急修复 |
### 3.3 分支保护规则
- `main` 分支:
- 不允许直接 pushhotfix 除外)
- PR 必须通过 CI(至少 unit + integration tests
- PR 必须至少 1 人 Review(人或 Agent
- **hotfix 直接 push main 后必须触发 CI**(至少 lint + unit),CI 失败则自动 revertM1 修订)
- feature/bugfix 分支:
- 无保护,自由 push
- 合并前必须 CI 通过
### 3.4 Commit 规范
```
<type>(<scope>): <subject>
<body>
```
| type | 说明 |
|------|------|
| `feat` | 新功能 |
| `fix` | Bug 修复 |
| `refactor` | 重构(不改行为) |
| `test` | 测试相关 |
| `docs` | 文档 |
| `chore` | 构建/工具/配置 |
---
## §4. 问题管理
### 4.1 问题来源
| 来源 | 入口 | 处理方式 |
|------|------|---------|
| 主公口头 | 庞统记录,**经 Review 后** | 创建 Gitea Issue |
| 司马懿 Review | Review 意见 | 创建 Gitea Issue,关联 PR |
| 自动化测试失败 | CI 报告 | 自动评论到已有 Issue,或手动创建 |
| 用户测试反馈 | 人工报告 | 创建 Gitea Issue |
| Agent 运行发现 | Agent 日志,**经 Review 后** | 创建 Gitea Issue |
### 4.2 Issue 标签体系
| 标签 | 颜色 | 说明 |
|------|------|------|
| `bug` | 红 | 功能异常 |
| `feature` | 蓝 | 新功能需求 |
| `improvement` | 绿 | 改进优化 |
| `security` | 橙 | 安全相关 |
| `risk:high/standard/low` | 分级色 | 风险级别(见 §6.1 判定规则) |
| `priority:high/medium/low` | 黄/灰 | 优先级 |
| `blocked` | 紫 | 阻塞中 |
### 4.3 需求/问题 Review 前置
所有需求和问题在进入开发之前必须经过 Review:
| 类型 | Review 者 | 目的 |
|------|-----------|------|
| 新需求 | 庞统(方案合理性)+ 司马懿(技术可行性) | 确认需求清晰、可行、值得做 |
| Bug/问题 | 司马懿(确认根因)或庞统(确认优先级) | 确认是真 bug、不是误报,根因明确 |
| Agent 发现的问题 | 庞统(确认有效性) | 过滤误报 |
未经过 Review 的 Issue 不进入开发流程。
### 4.4 Issue 状态流转
```
Open → In Progress → Review → Closed
↑ ↓
└──── Reopened ←───────────────────┘
```
---
## §5. CI/CD 管道设计
### 5.1 通用 CI 流程(所有项目共享骨架)
每个项目在 `.gitea/workflows/ci.yml` 自定义具体步骤,但遵循统一骨架。
> **注**Gitea Actions v1.26.2 不支持 `paths` 过滤触发条件。通过路径判断放在 job 级别的 `if` 条件中,使用确定支持的语法。(M4 修订)
```yaml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
# Job 1: 后端测试
backend:
# 只在有 Python 代码变更时跑
runs-on: [self-hosted, mac-mini]
steps:
- uses: actions/checkout@v4
- name: Setup Python
run: |
python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
- name: Lint
run: .venv/bin/ruff check src/
- name: Unit Tests
run: .venv/bin/pytest tests/unit/ -m "not e2e" --tb=short
- name: Integration Tests
run: .venv/bin/pytest tests/integration/ --tb=short
- name: Coverage Report
run: .venv/bin/pytest --cov=src --cov-report=term-missing
# Job 2: 前端构建
frontend:
# 只在有前端目录时跑
runs-on: [self-hosted, mac-mini]
steps:
- uses: actions/checkout@v4
- name: Install & Build
run: |
cd src/frontend
npm ci
npm run build
- name: Type Check
run: cd src/frontend && npx tsc --noEmit
```
**实际使用时**:每个项目根据实际情况删减 job。没有前端的删 frontend job,没有 Python 的删 backend job。
### 5.2 CI 门控级别
| 级别 | 触发条件 | 跑什么 | 阻断行为 |
|------|---------|--------|---------|
| **快速** | feature 分支 push | lint + unit tests | **软阻断**:失败标红,创建 PR 时如果最近一次 CI 失败则不允许进入 Review(S2 修订) |
| **标准** | PR 到 main | lint + unit + integration + coverage | 阻断 merge |
| **完整** | tag / main push | 标准 + buildE2E 仅 tag 触发) | 阻断部署 |
| **手动** | 手动触发 | 完整 + 特定检查 | 按需 |
### 5.3 覆盖率渐进策略(仲达 Q3 补充)
当前多数项目从未跑过覆盖率,直接设阈值不现实。分三阶段:
| 阶段 | 时间 | 覆盖率策略 | 说明 |
|------|------|-----------|------|
| **P1** | 启用后 2 周 | 只报告不阻断 | 先拿基线数据 |
| **P2** | 启用后 1 月 | 40% 阈值,低于警告不阻断 | 逐步提升 |
| **P3** | 启用后 2 月+ | 60% 阈值,低于阻断 | 正式生效 |
### 5.4 CI 结果反馈
- ✅ 通过 → PR 可合并
- ❌ 失败 → PR 阻断,自动评论失败信息
- 覆盖率低于阈值 → P1 只报告,P2 警告,P3 阻断
---
## §6. 代码审查流程
### 6.1 风险级别判定规则(M2 修订)
风险级别不由改动者自评,按以下规则自动判定 + 非改动者确认:
**自动判定规则**(在 CI 或 PR 模板中检查):
| 规则 | 级别 | 触发条件 |
|------|------|---------|
| 涉及核心模块 | **高** | 改动文件匹配 `**/spawner*``**/ticker*``**/dispatcher*``**/router*``**/guardrails*``**/strategy*``**/risk*` |
| 涉及资金/安全 | **高** | 改动包含交易逻辑、风控规则、密钥管理、资金操作 |
| 数据库 schema 变更 | **高** | 改动匹配 `**/migrations/**``**/schema*``**/models.py` 中的表定义 |
| 常规功能开发 | **标准** | 不匹配高风险规则的 src/ 下改动 |
| 文档/测试/配置 | **低** | 改动仅在 `docs/``tests/``*.md``*.yaml`(非 guardrails |
**确认机制**
1. PR 创建时自动标注风险级别(基于 git diff 文件路径)
2. 审查者(或庞统)确认或升级——**只能升级不能降级**
3. 改动者可以申请升级(但不能申请降级)
### 6.2 审查方式(按风险分级)
| 风险级别 | 审查方式 | 审查者 |
|---------|---------|--------|
| **高** | 固定 Review(必须) | 司马懿(专职) |
| **标准** | 固定 Review(必须) | 司马懿(默认审查者) |
| **低** | 简化走查 | 司马懿(快速确认)+ CI 通过 |
**例外**:司马懿自己的改动 → 庞统交叉审查。
> 所有级别的代码审查默认由司马懿执行。只有司马懿作为改动者时,才由庞统进行交叉审查。不再按改动领域分配审查者——简化流程,减少协调成本。
### 6.3 审查者分配规则
| 改动者 | 审查者 |
|--------|--------|
| 张飞 / 姜维 / 关羽 / 赵云 / 庞统 | 司马懿(默认) |
| 司马懿 | 庞统(交叉审查) |
### 6.4 审查清单(Skill 固化)
审查清单通过 Skill 提示词固化,不依赖审查者自觉。**基础清单 + 项目类型变体**:
**基础清单(所有项目通用)**
```
1. 正确性
- [ ] 代码逻辑正确,无死循环/空指针/边界遗漏
- [ ] 异常处理完整(网络超时、文件不存在、参数非法)
- [ ] 改动范围和 Issue/需求描述一致(不多不少)
2. 一致性
- [ ] 命名规范统一(变量/函数/文件)
- [ ] 和已有代码风格一致
3. 安全性
- [ ] 无硬编码密钥/密码/Token
- [ ] 用户输入有校验
- [ ] 文件操作有路径校验
4. 可维护性
- [ ] 复杂逻辑有注释(为什么,不是做什么)
- [ ] 无重复代码(DRY)
- [ ] 关键参数可配置,无魔法数字
5. 测试覆盖
- [ ] 新功能有对应测试
- [ ] Bug 修复有回归测试
- [ ] CI 通过
结论:通过 / 不通过(附具体行号和修改方向)
```
**量化策略项目变体**(追加项):
```
6. 量化策略专项
- [ ] 止损逻辑完整(触发条件、执行路径、异常兜底)
- [ ] 仓位计算正确(滑点、手续费已计入)
- [ ] 回测参数和实盘参数分离
```
### 6.5 审查时效
| 改动大小 | 期望审查时间 |
|---------|-------------|
| < 100 行 | 2 小时内 |
| 100-500 行 | 半天内 |
| > 500 行 | 1 天内 |
---
## §7. 部署流程
### 7.1 部署触发
| 触发方式 | 适用场景 | 流程 |
|---------|---------|------|
| **自动**(tag 触发) | 正式发布 | 打 tag → CI 完整跑(含 E2E)→ 自动执行 deploy 脚本 → 健康检查 |
| **自动**main push | 日常迭代 | push main → CI 标准跑 → 自动执行 deploy 脚本 → 健康检查 |
| **手动** | 紧急/调试 | 手动执行项目 deploy 脚本 |
### 7.2 项目部署脚本规范
每个项目必须提供 `scripts/deploy.sh`,接口统一:
```bash
#!/usr/bin/env bash
# deploy.sh — 项目部署脚本
#
# 必须支持的参数:
# --source=DIR 源码目录(CI 自动传入:源码 checkout 路径)
# --target=DIR 安装目标目录(CI 自动传入:安装路径)
# --skip-build 跳过构建步骤
# --health-check 部署后执行健康检查
# --version 输出当前部署版本(tag + commit hash + 部署时间)
# --rollback 回滚到上一个已记录的部署版本
#
# 必须维护的文件:
# data/deploy-history.jsonl — 最近 10 个部署版本记录
# 每行: {"tag":"v1.2.3","commit":"abc123","timestamp":"2026-06-06T12:00:00+08:00","status":"success"}
#
# 必须返回:
# 0 = 成功
# 非0 = 失败(stderr 输出原因)
```
### 7.3 自动部署 Workflow
```yaml
# .gitea/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
tags: ['v*']
jobs:
deploy:
needs: [ci] # 依赖 CI 通过
runs-on: [self-hosted, mac-mini]
steps:
- uses: actions/checkout@v4
- name: Record current version
run: bash scripts/deploy.sh --version
- name: Deploy
run: bash scripts/deploy.sh --source="$GITHUB_WORKSPACE" --health-check
- name: Rollback on failure
if: failure()
run: bash scripts/deploy.sh --rollback
```
### 7.4 健康检查
每个项目的 deploy 脚本必须包含健康检查:
- HTTP 服务:`curl -sf http://localhost:<port>/api/health`
- 数据服务:数据连通性检查
- 定时任务:PM2 状态检查
### 7.5 回滚策略(M3 修订)
**版本记录**
- 每次 deploy 成功后,记录 tag + commit + 时间戳到 `data/deploy-history.jsonl`(保留最近 10 条)
- `--version` 读取最后一行输出当前版本
- `--rollback` 读取倒数第二行,checkout 对应 commit,重新 deploy
**回滚流程**
1. `deploy.sh --rollback` 读取上一个成功版本
2. `git checkout <previous-commit>`
3. 重新执行 deploy(不含 rollback
4. 健康检查
**已知限制**
- revert 可能产生合并冲突 → 部署失败时人工介入
- 数据库变更回滚需人工介入 → schema 变更必须向前兼容(只加字段不删/不改),违反此规范由 CI 检查拦截(或人工 Review 拦截)
---
## §8. 验证流程集成
### 8.1 验证来源和层级
| 验证来源 | 层级 | 触发时机 | 自动化程度 |
|---------|------|---------|-----------|
| **Lint** | 机械 | 每次 push | 100% 自动 |
| **单元测试** | 机械 | 每次 push | 100% 自动 |
| **集成测试** | 语义 | PR 到 main | 100% 自动 |
| **覆盖率** | 语义 | PR 到 main | 渐进自动(见 §5.3 |
| **Code Review** | 共识 | PR 到 main | 人工(Skill 辅助) |
| **E2E 测试** | 共识 | PR 到 main 时自动跑(CI 隔离环境)+ 手动触发 | 自动+手动 |
| **用户测试** | 共识 | 部署后 | 手动 |
### 8.2 三阶段门控(来源: quality-gate-patterns
```
机械门控 (CI 快速) → 语义门控 (CI 标准 + Review) → 共识门控 (E2E + 用户验证)
零成本 中等成本 高成本
语法/lint/编译 API 契约/业务逻辑 端到端/用户体验
```
- 快速门控是**开发期辅助反馈**,不替代标准门控
- **PR 合入必须经过标准门控(机械 + 语义)**,这个顺序不可跳过(S3 修订)
- 机械失败 → 立即阻断,不看 Review
- 语义失败 → 可以 Review 但不能合并
- E2E 在 CI 隔离环境中自动跑,不污染生产环境(见 §8.5)。也可手动触发。
### 8.3 测试失败 → Issue 关联
- CI 失败时,如果已有 Issue → 自动评论失败信息
- CI 失败时,如果无 Issue → 不自动创建(避免 Issue 爆炸),由改动者手动创建
### 8.4 机械门控前提条件(仲达 Q6 补充)
P1 必须先配好以下工具,否则机械门控跑不起来:
| 工具 | 用途 | 安装 |
|------|------|------|
| `ruff` | Python lint + format | `pip install ruff` |
| `pytest-cov` | 覆盖率 | `pip install pytest-cov` |
| `act-runner` | CI 执行器 | 下载 Go 二进制 |
### 8.5 CI 隔离测试环境(E2E 安全运行)
E2E 自动跑的核心前提:**不污染生产环境**。通过 CI 隔离环境实现:
```
CI 临时测试环境(每次 CI 自动创建)
├─ 独立 venv(每次 CI 新建)
├─ 临时 SQLitetmpdir,跑完销毁)
├─ 临时端口(8084,不和生产 8083 冲突)
├─ 启动 FastAPI 服务 → 跑 E2E → 关闭
└─ 跑完整个 tmpdir 删除,不留痕迹
生产环境(~/.sanguo_projects/)完全不受影响
```
**效果**E2E 可以在每次 PR 到 main 时自动跑,无需担心污染生产数据或影响生产服务。
#### 隔离边界:程序逻辑 vs Agent 交互
| 层面 | 隔离方式 | 说明 |
|------|---------|------|
| **程序逻辑**FastAPI + Daemon + SQLite | ✅ 完全隔离 | 独立 venv + 临时数据库 + 临时端口 | — |
| **Agent spawn**openclaw agent | ❌ 走生产 openclaw | openclaw 是全局单例,无法隔离 | 测试 case 用 UUID 前缀标识 + Agent 回写地址指向 8084 临时端口 |
Agent spawn 走生产 openclaw 的决策理由:
1. openclaw 是全局单例(`~/.openclaw/` 只有一份),Agent 配置和 workspace 全局共享
2. 测试 case 会标识测试上下文,Agent 记忆混淆风险低
3. 完全隔离需要在 Docker 中跑独立 openclaw,维护成本不值得
4. Agent 的 API 回写地址由 spawn message 传入,指向 CI 临时服务的端口 8084
#### 路径硬编码修复清单
当前代码有 6 处硬编码绝对路径,需改为环境变量可配置(P1 必须完成):
| 位置 | 硬编码路径 | 改法 | 现状 | 行数 |
|------|-----------|------|------|------|
| `utils.py` | 数据根目录 | `BLACKBOARD_ROOT` 环境变量 | ✅ 已支持 | 1 |
| `bootstrap.py:42` | Skill 加载路径 | `MOZI_SKILL_PATH` 环境变量 | ✅ 已支持 | 1 |
| `registry.py:264` | 项目扫描目录 `~/.openclaw/sanguo_projects` | 加 `SANGUO_PROJECTS_DIR` | ❌ 硬编码 | 1 |
| `spawner.py:1177,1261` | `~/.openclaw/agents/<id>/sessions.json` | 加 `OPENCLAW_HOME` | ❌ 硬编码 | 2 |
| `blackboard_routes.py:161` | `~/.openclaw/openclaw.json` | 从 `OPENCLAW_HOME` 读 | ❌ 硬编码 | 1 |
共计 6 处(2 已支持、4 待修)。
#### 环境总结
| 环境 | 位置 | 用途 | 生命周期 |
|------|------|------|---------|
| 开发 | `~/.openclaw/sanguo_projects/` | 改代码、本地调试 | 常驻 |
| CI 测试 | CI runner 临时目录 | lint/unit/integration/E2E | CI 跑完即销毁 |
| 生产 | `~/.sanguo_projects/` | 正式运行服务 | 常驻 |
**schema 变更检查**(可选):CI 中加一步对比数据库 schema 变更,检测破坏性操作(删字段/改类型)。检测到则提 Issue 人工确认。可通过配置开关强制绕过。
---
## §9. 完整开发流程(端到端)
### 9.0 全局工作流串联图
以下是把所有关键环节串起来的完整链路。三个场景(新功能/Bug修复/Hotfix)共享大部分节点,差异在分支。
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 问题/需求入口 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 主公口头 │ │ CI 失败 │ │ 司马懿 │ │ 运行时 │ │
│ │ /需求文档 │ │ 自动报告 │ │ Review │ │ Agent发现 │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ └──────────────┴──────────────┴──────────────┘ │
│ ↓ │
│ ┌─────────────────────┐ │
│ │ 需求/问题 Review │ ← §4.3 │
│ │ │ │
│ │ 需求: 庞统(方案) │ │
│ │ + 司马懿(可行) │ │
│ │ Bug: 司马懿(确认根因)│ │
│ │ Agent发现: 庞统(过滤)│ │
│ └──────────┬──────────┘ │
│ ↓ 通过 │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 创建 Gitea Issue #N │ │
│ │ 标签: bug/feature/improvement + risk:high/standard/low │ │
│ │ 关联: PR / 复现步骤 / 根因分析 │ │
│ └──────────────────────────┬────────────────────────────────┘ │
│ ↓ │
│ ┌──── 场景分流 ────┐ │
│ ↓ ↓ │
│ ┌─── Hotfix ───┐ ┌── 标准流程 ──┐ │
│ │ │ │ │ │
│ │ 直接 main │ │ 分支开发 │ │
│ │ │ │ │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ ↓ ↓ │
└─────────────────────────────────────────────────────────────────────────┘
=== 标准流程(新功能 / Bug修复)===
┌─────────────────────────────────────────────┐
│ Step 1: 分支创建 │
│ git checkout -b feat/issue-N-xxx │
│ 或 git checkout -b fix/issue-N-xxx │
│ 角色Agent: 开发者(张飞/姜维/关羽/赵云) │
│ Skill: git-workflow │
│ 输入: Issue #N │
│ 输出: feature/bugfix 分支 │
└──────────────────┬──────────────────────────┘
┌─────────────────────────────────────────────┐
│ Step 2: 编码 + 自测 │
│ 编码 → 本地 pytest → commit │
│ 角色Agent: 开发者 │
│ Skill: 按任务类型(无专门编码skill) │
│ 输入: 分支 + Issue 需求 │
│ 输出: 代码改动 + UT │
│ 约束: Bug修复必须加回归测试 │
└──────────────────┬──────────────────────────┘
┌─────────────────────────────────────────────┐
│ Step 3: push → CI 快速门控(每次push) │
│ Lint + Unit Tests │
│ 工具: Gitea Actions │
│ 输入: feature 分支 push │
│ 输出: ✅通过 / ❌失败标红 │
│ 阻断: 软阻断(失败不阻止开发,但阻止PR进入Review)│
└──────────────────┬──────────────────────────┘
┌─────────────────────────────────────────────┐
│ Step 4: 创建 PR │
│ git push origin → Gitea 创建 PR │
│ 自动: 风险级别标注(按改动文件路径) │
│ 工具: Gitea PR │
│ 输入: feature 分支 + CI 快速门控结果 │
│ 输出: PR + 风险标签 │
└──────────────────┬──────────────────────────┘
┌─────────────────────────────────────────────┐
│ Step 5: CI 标准门控(PR到main时) │
│ Lint + Unit + Integration + Coverage │
│ 环境: CI 隔离临时环境 │
│ 工具: Gitea Actions + pytest-cov + ruff │
│ 输入: PR diff │
│ 输出: ✅通过 / ❌阻断merge │
│ 阻断: 硬阻断(CI不过不许merge) │
└──────────────────┬──────────────────────────┘
┌─────────────────────────────────────────────┐
│ Step 6: 代码审查 │
│ 审查者确认风险级别(只升不降) │
│ 默认审查者: 司马懿(所有级别) │
│ 例外: 司马懿的改动 → 庞统交叉审查 │
│ Skill: code-review │
│ 输入: PR + CI 结果 + 风险级别 │
│ 输出: 通过 / 不通过(附行号+修改方向) │
│ 不通过 → 回到 Step 2 │
└──────────────────┬──────────────────────────┘
↓ 通过
┌─────────────────────────────────────────────┐
│ Step 7: Merge → CI 完整门控 │
│ merge到main → 自动触发 │
│ 标准 CI + deploy workflow │
│ 触发: Gitea branch protectionCI+Review通过才许merge)│
│ 输入: main 分支 │
│ 输出: merge commit + CI 结果 │
└──────────────────┬──────────────────────────┘
┌─────────────────────────────────────────────┐
│ Step 8: 自动部署 │
│ deploy.sh --source=... --health-check │
│ 记录版本到 deploy-history.jsonl │
│ 角色Agent: 姜维(deploy.sh由项目维护) │
│ Skill: ci-cd-ops │
│ 输入: main 分支代码 │
│ 输出: 部署结果 + 版本记录 │
│ 失败: 自动回滚(deploy.sh --rollback
└──────────────────┬──────────────────────────┘
┌─────────────────────────────────────────────┐
│ Step 9: E2E 验证(CI 隔离环境自动跑) │
│ 临时venv + 临时SQLite + 临时端口8084 │
│ Agent spawn 走生产 openclaw(不隔离) │
│ 角色: 司马懿写E2E用例,CI自动跑 │
│ Skill: testing-workflow │
│ 输入: 部署后的代码 │
│ 输出: E2E 结果 │
│ 阻断: 初期不阻断,成熟后可阻断 │
└──────────────────┬──────────────────────────┘
┌─────────────────────────────────────────────┐
│ Step 10: Issue 关闭 │
│ 部署成功 + 验证通过 → 关闭 Issue #N │
│ 发现新问题 → 新开 Issue → 回到入口 Review │
└─────────────────────────────────────────────┘
=== Hotfix 分支流程 ===
┌─────────────────────────────────────────────┐
│ Step H1: 确认 hotfix │
│ 庞统或主公确认(线上故障、核心功能受影响) │
│ Skill: hotfix-workflow │
└──────────────────┬──────────────────────────┘
┌─────────────────────────────────────────────┐
│ Step H2: 直接 push main │
│ 修改 → commit → push main │
│ CI 必须触发(lint + unit),不可跳过 │
│ CI 失败 → 自动 revert → 人工介入 │
└──────────────────┬──────────────────────────┘
┌─────────────────────────────────────────────┐
│ Step H3: 自动部署(同标准 Step 8) │
│ E2E 验证(同标准 Step 9) │
└──────────────────┬──────────────────────────┘
┌─────────────────────────────────────────────┐
│ Step H4: 24h 内复盘(必须) │
│ 补 Gitea Issue(根因 + hotfix 内容) │
│ 司马懿 Review(事后审查代码质量) │
│ 复盘记录(为什么需要 hotfix、如何避免) │
│ 超时 → 下次 hotfix 权限暂停 │
└─────────────────────────────────────────────┘
```
#### 衔接机制:每一步怎么触发下一步
上图只画了"谁做什么",这里补清楚**步骤之间靠什么机制衔接**。
| 衔接点 | 触发方式 | 通知方式 | 技术实现 |
|--------|---------|---------|----------|
| 问题入口 → 问题Review | 庞统收到问题后主动发起 | Mail 通知司马懿(需求类)/ 司马懿自己提交(bug类) | Mail API POST `/api/mail` |
| 问题Review → Issue创建 | Review结论达成 | 庞统或司马懿创建 | Gitea API `POST /repos/{owner}/{repo}/issues` |
| Issue创建 → 分支创建 | 庞统指派任务给开发者 | Mail 通知被指派者,含 Issue #N + 分支名 | Mail API + Agent 读取 Issue |
| 分支创建 → 编码 | 开发者看到 Mail 后开始 | 无额外通知 | Agent 启动时 Skill 注入分支信息 |
| 编码 → CI 快速门控 | **git push 自动触发** | CI 结果显示在 commit 状态(Gitea Status Check 内置) | Gitea Actions `on: push` |
| CI 快速门控 → PR创建 | 开发者主动(认为代码写完) | 无自动通知 | 开发者在 Gitea Web UI 或 git push + 浏览器创建 PR |
| PR创建 → CI 标准门控 | **PR 创建/更新自动触发** | CI 结果自动评论到 PR | Gitea Actions `on: pull_request` |
| 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 自动触发** | 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 |
| 部署失败 → 自动回滚 | **deploy.sh 失败自动执行** | 无需通知(自动化) | Gitea Actions `if: failure()` step 调 `deploy.sh --rollback` |
#### 关键衔接机制详解
**1. Gitea Actions 自动触发(核心衔接)**
整个 CI/CD 链路的自动化靠 Gitea Actions 的触发机制:
```
git push feature分支
→ Gitea Actions on: push 匹配
→ 跑 lint + unit(快速门控)
创建 PR
→ Gitea Actions on: pull_request 匹配
→ 跑 lint + unit + integration + coverage(标准门控)
→ CI 结果自动出现在 PR 页面
Merge 到 main
→ Gitea Actions on: push: branches: [main] 匹配
→ 跑 deploy workflow(含 deploy.sh + health check + E2E
→ 全自动,无需人干预
```
触发链:push → CI → PR → CI → Review → merge → deploy+E2E,中间三个自动环节靠 Gitea Actions 的 `on` 触发器串联。
**2. Mail 通知(Agent 间协调)**
CI/CD 之外的衔接靠 Mail。Agent 不会自己去看 Gitea PR 评论,所以所有需要 Agent 行动的情况都通过 Mail 推送:
- 需求Review完成 → Mail 通知开发者开始
- Bug确认 → Mail 通知改动者(含根因)
- Hotfix确认 → Mail 通知执行者
- CI 失败 → CI workflow 写 PR 评论 → daemon Webhook 模块转发 Mail 通知改动者 Agent
- 审查不通过 → Mail 通知改动者(附 Review 摘要)
- 部署失败回滚 → Mail 通知庞统/姜维(人工介入)
**3. Gitea branch protection(硬门控)**
确保流程不可跳过:
- `main` 分支设置:CI 必须通过 + 至少 1 人 Review Approve
- 效果:没有 CI 绿勾 + Review Approvemerge 按钮灰色不可点
- 配置位置:Gitea → 仓库 Settings → Branches → Branch Protection
**4. Agent 间通知机制总结**
| 场景 | 通知方式 | 为什么不用另一种 |
|------|---------|----------------|
| 人→Agent任务指派 | Mail | Gitea Issue 不会主动通知 Agent,需要 Mail 推送 |
| CI 结果 | Gitea Status CheckPR页面)+ daemon Webhook 转发 MailAgent通知) | Status Check 给人看,Mail 给 Agent 推送 |
| Review 请求 | daemon Webhook 转发 Mail | Gitea Webhook 触发 → daemon 翻译成 Mail 给司马懿 |
| Review 意见(不通过/通过) | daemon Webhook 转发 Mail | Gitea Webhook 触发 → daemon 翻译成 Mail 给改动者 |
| CI 失败 | CI workflow 写 PR评论 → daemon Webhook 转发 Mail | CI 写评论是给所有人看,Webhook → Mail 给 Agent 推送 |
| 跨Agent协作请求 | Mail | Gitea 不支持跨 Agent 私信 |
### 9.0.1 通知模板(工具链通用版)
以下为 §9.0 衔接机制中用到的通用通知模板。§15.5 中还定义了更详细的流程强约束 Mail 模板(含完整流程步骤和 Gitea API 调用指令)。
所有自动化通知统一格式,确保 Agent 收到后能直接行动。
**模板 1:任务指派(庞统 → 开发者)**
```
From: pangtong-fujunshi
To: {被指派者}
Title: 任务指派: Issue #{N} - {Issue 标题}
Text:
{Issue 标题}
Issue: http://192.168.2.154:3000/{repo}/issues/{N}
分支: feat/issue-{N}-{简述}
风险级别: {high/standard/low}
要点:
- {需求要点1}
- {需求要点2}
验收标准:
- {验收标准1}
- {验收标准2}
```
**模板 2CI 失败通知(CI workflow → 改动者)**
```
From: daemon-webhook
To: {PR 作者}
Title: CI 失败: PR #{N} - {PR 标题}
Text:
PR #{N} CI 未通过。
PR: http://192.168.2.154:3000/{repo}/pulls/{N}
失败测试:
- {test_path}::{test_name} FAILED
- {test_path}::{test_name} FAILED
失败 step: {step_name}
错误摘要: {error_output_tail_20_lines}
请修复后重新 push。
```
**模板 3:Review 不通过通知(审查者 → 改动者)**
```
From: {审查者}
To: {改动者}
Title: Review 不通过: PR #{N} - {PR 标题}
Text:
PR #{N} 审查未通过。
PR: http://192.168.2.154:3000/{repo}/pulls/{N}
风险级别: {confirmed_level}
审查意见:
1. [{文件}:{行号}] {问题描述} → {修改方向}
2. [{文件}:{行号}] {问题描述} → {修改方向}
必须全部修改后重新 push。
```
**模板 4:部署失败回滚通知(CI workflow → 庞统/姜维)**
```
From: daemon-webhook
To: pangtong-fujunshi, jiangwei-infra
Title: 部署失败已回滚: {repo} {commit_hash}
Text:
{repo} 部署失败,已自动回滚到上一版本。
触发 commit: {commit_hash}
部署脚本: scripts/deploy.sh
失败原因: {deploy_stderr_tail}
当前版本: {previous_version}
请人工介入排查。
```
**模板 5:Bug 确认通知(司马懿 → 改动者)**
```
From: simayi-challenger
To: {改动者}
Title: Bug 确认: Issue #{N} - {Issue 标题}
Text:
根因: {根因描述}
复现步骤: {steps}
影响范围: {scope}
风险级别: {level}
Issue: http://192.168.2.154:3000/{repo}/issues/{N}
分支: fix/issue-{N}-{简述}
请修复并添加回归测试。
```
### 9.1 场景 A:新功能开发
```
1. 问题入口
└─ 主公口头 / 需求文档
└─ 庞统梳理需求方案 → 司马懿确认技术可行性
└─ Review 通过后 → 庞统创建 Gitea Issue #N
2. 分支创建
└─ git checkout -b feat/issue-N-xxx
└─ Agent 或人工编码(git-workflow Skill 注入分支信息到 Agent 上下文)
3. 开发过程中
└─ 每个 commit → push → CI 快速门控(lint + unit
└─ CI 失败 → 标红但不阻断开发 → 修好再 push
4. 开发完成
└─ git push origin feat/issue-N-xxx
└─ 创建 PR → 自动标注风险级别(基于改动文件路径)
└─ CI 标准门控(lint + unit + integration + coverage
5. 审查
└─ CI 通过后进入审查
└─ 审查者确认风险级别(只升不降)
└─ 司马懿审查(所有级别默认)
└─ 如果改动者是司马懿 → 庞统交叉审查
└─ 审查不通过 → 修改 → push → CI → 再审
└─ 审查通过 → merge
6. 部署
└─ merge 到 main → 自动触发 CI + deploy workflow
└─ deploy.sh 执行 → 记录版本 → 健康检查
└─ 部署成功 → Issue #N 关闭
└─ 部署失败 → 自动回滚 → 人工介入
7. 验证
└─ CI 隔离环境中自动跑 E2E(不污染生产)
└─ 用户测试(手动)
└─ 发现问题 → 新开 Issue(经 Review 后再开发)
```
### 9.2 场景 BBug 修复
```
1. 问题入口
└─ CI 失败 / 司马懿 Review / 运行时发现
└─ 司马懿确认根因(是真 bug 不是误报)
└─ 创建 Gitea Issue #N,标注 bug + 风险级别
2. 根因定位(bugfix-workflow Skill
└─ 调查根因(不猜,看日志/复现)
└─ 在 Issue 中记录根因
3. 修复
└─ git checkout -b fix/issue-N-xxx
└─ 修改代码 + 添加回归测试(必须)
└─ push → PR → CI → Review
4. 合并+部署
└─ merge → auto deploy → 健康检查
└─ Issue #N 关闭
5. 验证
└─ 确认原 bug 不再复现
```
### 9.3 场景 C:紧急修复(hotfix)(M1/S5 修订)
```
1. 判断是否需要 hotfix
└─ 标准:线上故障影响核心功能,等 PR 流程代价过高
└─ 庞统或主公确认
2. 直接在 main 上修
└─ git commit → push main
└─ CI 必须触发(lint + unit),不可跳过
└─ auto deploy → 健康检查
└─ CI 失败 → 自动 revert → 人工介入
3. 事后复盘(hotfix-workflow Skill
└─ 24 小时内必须完成:
├─ 补 Gitea Issue(记录根因 + hotfix 内容)
├─ 司马懿 Review(事后审查代码质量)
└─ 复盘记录(为什么需要 hotfix、如何避免)
└─ 超时未复盘 → 下次 hotfix 权限暂停
4. E2E 验证
└─ CI 隔离环境中自动跑 E2E 确认 hotfix 有效
```
---
## §10. Skill 清单(流程固化)(S4 修订)
以下流程通过 Skill 固化到对应 Agent
| Skill 名 | 固化什么 | 适用 Agent |
|----------|---------|-----------|
| `git-workflow` | 分支创建/命名/合并规范、commit 规范、PR 创建流程、**当前分支注入上下文** | 所有编码 Agent |
| `code-review` | 风险级别判定规则、审查清单(基础+项目变体)、审查标准、结论格式 | 司马懿(默认审查者)、庞统(交叉审查) |
| `ci-cd-ops` | CI 配置规范、deploy 脚本规范、健康检查标准、版本记录规范 | 姜维(平台)、所有项目维护者 |
| `bugfix-workflow` | 根因定位流程、回归测试要求、Issue 关联 | 所有编码 Agent |
| `hotfix-workflow` | hotfix 判断标准、直接 push main 流程、CI 必跑、24h 复盘要求 | 庞统(协调)、张飞、姜维 |
| `testing-workflow` | 测试分工规范、什么时候跑什么测试、测试数据隔离 | 所有 Agent |
| `release-workflow` | tag 规范、changelog、发布流程、数据库 schema 变更向前兼容规范 | 庞统(协调) |
### 测试分工规范(testing-workflow Skill 内)
| 测试类型 | 谁写 | 谁跑 |
|---------|------|------|
| **单元测试(UT** | 开发者自己(张飞/姜维/关羽/赵云等) | CI 自动 + 开发者本地 |
| **功能测试** | 司马懿(专职) | CI 自动 |
| **集成测试** | 司马懿(专职) | CI 自动 |
| **E2E 测试** | 司马懿(专职) | CI 自动(隔离环境)+ 手动触发 |
| **测试脚本/工具** | 司马懿 + 赵云(数据相关) | 按需 |
> 所有人都能写代码和写 UT。但功能测试、集成测试、E2E 由司马懿专职编写和维护——确保测试视角独立于开发视角。
---
## §11. 实施路线
| 阶段 | 内容 | 前置条件 | 耗时 |
|------|------|---------|------|
| **P0: 基础启用** | Gitea admin 开启 Actions + 部署 act-runnerMac mini 裸机) | 主公操作 NAS | 30 分钟 |
| **P1: CI 骨架** | 模板 CI workflow + pytest-cov + ruff + 覆盖率基线测量 | P0 | 1 天 |
| **P2: 流程 Skill** | 7 个 Skill 编写(见 §10 | 无 | 2-3 天 |
| **P3: 自动部署** | 部署 workflow + 项目 deploy.sh 规范化 + 版本记录 | P1 | 1-2 天 |
| **P4: 渐进收紧** | 覆盖率阈值 P1→P2→P3、E2E 集成 | P1-P3 | 按需 |
**P1 必须先就位的工具**(否则机械门控跑不起来):
- `ruff`lint
- `pytest-cov`(覆盖率)
- `act-runner`CI 执行器)
---
## §12. 评审记录
### v0.1 → v0.2 修订清单
| 编号 | 类型 | 问题 | 修订内容 |
|------|------|------|---------|
| M1 | 必须修 | hotfix 安全网不足 | §3.3 明确 hotfix push main 后 CI 必跑,失败自动 revert;§9.3 统一流程 |
| M2 | 必须修 | 风险级别谁来定 | §6.1 新增自动判定规则(按文件路径)+ 非改动者确认机制(只升不降) |
| M3 | 必须修 | 回滚策略过于简单 | §7.2 新增 `--version` 参数和 deploy-history.jsonl;§7.5 重写回滚策略 |
| M4 | 必须修 | CI 语法问题 | §5.1 删除 `hasPython()`/`hasFrontend()` 伪函数,改为每个项目按需删减 job |
| S1 | 建议改 | 轮查配对不合理 | §6.3 改为按改动领域分配,不再固定轮转 |
| S2 | 建议改 | 快速门控"不阻断"是噪音 | §5.2 改为"软阻断"PR 进入 Review 前检查最近 CI 状态 |
| S3 | 建议改 | 三阶段门控说明不清 | §8.2 补充:快速门控是开发辅助,PR 合入必须标准门控 |
| S4 | 建议改 | Skill 遗漏 | §10 增加 testing-workflow、hotfix-workflow、release-workflow(含 schema 变更) |
| S5 | 建议改 | hotfix 复盘无时限 | §9.3 加 24 小时复盘约束,超时暂停 hotfix 权限 |
| Q1 | 评审回答 | Trunk-Based 够用吗 | §3.1 补充前提:Agent 必须知道当前分支,git-workflow Skill 注入 |
| Q3 | 评审回答 | 覆盖率基线缺失 | §5.3 新增渐进策略(P1 只报告 → P2 40% → P3 60% |
| Q6 | 评审回答 | 机械门控前提未就绪 | §8.4 新增工具清单,§11 标注 P1 必须先就位 |
### v0.3 → v0.4 修订清单(主公审阅反馈)
| 编号 | 反馈 | 修订内容 |
|------|------|---------|
| F1 | 需求/问题必须经 Review | §1 新增原则 7;§4.3 新增需求 Review 前置规则;§9.1 场景 A 加入 Review 步骤 |
| F2 | E2E 仅用户触发 | §8.1 E2E 改为“仅用户手动触发”;§8.2 门控说明移除 E2E 自动跑;§9.1/9.3 验证步骤更新 |
| F3 | 编码/测试人人能做,功能/集成/E2E 司马懿专职 | §10 新增测试分工规范表 |
| F4 | 审查默认司马懿 | §6.2 简化为司马懿统一审查,仅司马懿自身改动由庞统交叉审查 |
| F5 | 以项目为单位组织 | §1 新增原则 8;Issue 标签体系含项目维度 |
| F6 | 姜维持有 Gitea admin | §2.1 权限更新 |
| F7 | 分支保护含义说明 | §3.3 补充分支保护规则说明 |
| F8 | schema 变更能自动化就自动化 | 保留在 release-workflowCI check + Issue 兜底 |
### v0.4 → v0.5 修订清单(主公二次审阅)
| 编号 | 反馈 | 修订内容 |
|------|------|----------|
| G1 | E2E 不自动跑是因为怕污染生产,不是不需要 | §8.5 新增 CI 隔离测试环境,E2E 改为自动跑(临时 venv + 临时 SQLite + 临时端口) |
| G2 | 三套环境是否有必要 | §8.5 环境总结表:开发/CI临时/生产,不需要常驻测试环境 |
| G3 | schema 变更检查可以,可选+开关 | §8.5 补充 schema 检查说明:可选 + 配置开关强制绕过 |
| G4 | 分支保护确认开启 | §14 更新为已确认 |
| G5 | E2E 触发方式 | §8.1 更新:CI 自动 + 手动触发 |
### v0.5 → v0.6 修订清单(主公三次审阅)
| 编号 | 反馈 | 修订内容 |
|------|------|----------|
| H1 | Agent spawn 走生产 openclaw,不隔离 | §8.5 新增隔离边界表:程序逻辑隔离 vs Agent 走生产 |
| H2 | 路径硬编码问题 | §8.5 新增路径硬编码修复清单(6 处,2 处已支持、4 处待修) |
### v0.6 → v1.0 定稿(司马懿终审通过)
- 隔离边界表增加"缓解措施"列(仲达建议)
- 路径硬编码清单增加"行数"列(仲达建议)
- 状态标记为 v1.0 定稿
### v1.0 → v1.1(主公要求:补全局工作流串联图)
- §9.0 新增全局工作流串联图:从问题入口到部署上线的完整链路
- 每个节点标注:谁做、用什么工具、什么Skill、输入输出
- 标准流程 10 步 + Hotfix 分支 4 步
- 新增工具链衔接总览表
- 新增衔接机制详解:每一步怎么触发下一步、靠什么技术、怎么通知
- §9.0.1 新增 5 个通知模板(任务指派/CI失败/Review不通过/部署回滚/Bug确认)
- 修正:Agent 不看 Gitea PR 评论,所有需 Agent 行动的场景都走 Mail 推送
### v1.2 → v2.0(串联架构重构)
- §15 新增完整串联架构:Gitea 自动化 + Daemon Webhook 模块 + Mail 执行层
- 核心变更:ci-notifier 取消,Webhook 转发集成到 daemon
- 新增:超时检测(三级处置)+ 失败处置(走 Issue 流程)
- 新增:Mail 模板(流程强约束固化)
- 新增:Skill vs Mail 边界明确
- 新增:Agent ID ↔ Git 用户名映射
- §9.0 衔接机制表更新:所有 ci-notifier 引用替换为 daemon Webhook 模块
- §9.0.1 通知模板 From 字段更新:ci-notifier → daemon-webhook
### v2.0 司马懿终审修订
| 编号 | 类型 | 问题 | 修订内容 |
|------|------|------|----------|
| M1 | 必须修 | 超时阈值统一 4h 不合理 | §15.4 改为从 Mail metadata 读取个性化 deadline,三级处置按超时程度而非绝对时间 |
| S1 | 建议 | Webhook 代码缺少映射步骤 | §15.3 代码示例加 to_agent_id() 映射函数,所有 send_mail 调用都经映射 |
| S2 | 建议 | Webhook 缺签名验证 | §15.3 代码示例加 X-Gitea-Signature HMAC 验证 |
| S3 | 建议 | 缺 Bug 确认模板 | §15.5 新增模板 6(Bug 确认通知) |
- §14 待讨论更新:已确认事项标记,新增 Webhook 模块待讨论项
### v2.0 实施记录(2026-06-06
| # | 实施项 | 状态 | 备注 |
|---|--------|------|------|
| 1 | Gitea Actions 能力调研 | ✅ | docs/research/gitea-actions-research.md |
| 2 | Mail API 回复能力调研 | ✅ | docs/research/mail-reply-research.md |
| 3 | 路径硬编码修复 | ✅ | 4处改为环境变量,411 tests passed |
| 4 | CI workflow 编写 | ✅ | ci.yml + deploy.yml + e2e.yml |
| 5 | Skill 文件编写 | ✅ | 7个共通技能 |
| 6 | Gitea 仓库创建 | ✅ | sanguo/moziplus-v2(姜维) |
| 7 | act-runner 部署 | ✅ | arm64 裸机 + LaunchAgent(姜维) |
| 8 | 分支保护配置 | ✅ | main: CI通过+1人Review+禁止force push(姜维) |
| 9 | Webhook 配置 | ✅ | 已配未启用(等 daemon 集成)(姜维) |
| 10 | CI Secret 配置 | ✅ | CI_TOKEN(注意:Gitea 保留 GITEA_* 前缀) |
| 11 | Workflow 变量前缀验证 | ✅ | gitea.* 正确,无需修改 |
| 12 | Mail 模板验证 | ✅ | 5项能力全验证通过 |
| 13 | PR #1 创建 | ✅ | feat/initial-setup → main |
| 14 | 开发→安装目录同步 | ✅ | diff 确认一致 |
| - | Webhook daemon 集成 | ⏳ | 需改 daemon 代码,暂不做 |
| - | Mail 模板自动发送(Webhook 触发) | ⏳ | 依赖 Webhook daemon 集成 |
| - | act-runner LaunchAgent 网络就绪优化 | ⏳ | 偶尔 gRPC 连接失败 |
| - | Gitea 升级到 v1.24+ | 💡 | 建议,非必须 |
---
## §13. 项目维度组织
### 13.1 多项目并行
所有内容以项目为单位组织:
| 维度 | 按项目组织 |
|------|------------|
| **代码仓库** | Gitea 上每个项目一个 repo |
| **Issue** | 每个项目的 Issue 独立管理 |
| **CI/CD** | 每个项目自管 `.gitea/workflows/` |
| **部署脚本** | 每个项目自管 `scripts/deploy.sh` |
| **测试** | 每个项目自管 `tests/` 目录 |
| **Skill 配置** | 通用 Skill 共享,项目特定 Skill 放项目仓库 |
特殊:`_general` 项目用于跨项目的通用任务(如工具链基础设施维护)。
---
## §14. 待讨论
1. **Gitea admin 权限**:姜维持有 admin 权限,可负责启用 Actions、配置分支保护等
2. **分支保护**:已确认开启——PR 必须 CI 通过 + 至少 1 人 Review 才能合并到 main
3. **数据库 schema 变更检查**:CI 自动检测破坏性变更(可选,有开关可强制绕过),检测不到的提 Issue 人工干预 — **已确认**
4. **环境架构**:三环境(开发/CI临时/生产),CI 临时环境支持 E2E 自动跑不污染生产 — **已确认**
5. **Webhook 转发模块集成到 daemon**:已确认方向,具体实现待设计(见 §15)
---
## §15. 串联架构
> §15 是 v2.0 的核心新增章节,定义工具链三层串联架构:Gitea 自动化 + Daemon Webhook 模块 + Mail 执行层。
> 此架构取消了之前的 ci-notifier 虚拟角色概念,所有事件流转通过三层清晰分工完成。
### §15.1 串联架构总览
三层架构图:
```
Layer 1: Gitea 自动化(流程引擎)
- Actions: push → CI, PR → CI, merge → deploy
- Status Check: CI 结果自动显示在 PR
- Branch Protection: CI通过+Review通过才许merge
- CI workflow 内可调 Gitea API 写 PR 评论
Layer 2: Daemon 流程编排(转发+监控)
- Webhook 接收模块:监听 /webhook 端点
- 简单事件:Webhook → 解析 → 直接发 Mail
- 复杂事件:Webhook → 解析 → spawn 庞统作为任务节点
- 超时检测:在 ticker 中扫描未回复 Mail
- 失败处置:轻度重发提醒,中度 spawn 庞统,严重创建 Issue
Layer 3: Mail 执行层(Agent 接口)
- Mail = 流程指令 + 回复确认 + 时限
- Agent 收到 Mail 后行动,完成后回复确认
- Mail 线程 = Issue 生命周期可追溯
- 流程强约束固化在 Mail 模板中
```
核心原则:
1. **Gitea 是流程引擎**——所有状态流转靠 Gitea 自己(Actions + Status Check + Branch Protection
2. **Webhook → Mail 转发集成在 daemon 里**——状态共享,可直接触发下一步
3. **Mail 是 Agent 接口**——Agent 不看 Gitea UI,所有需 Agent 行动的事件通过 Mail 推送
4. **简单事件 daemon 直接处理,复杂事件(多 Agent 协作)spawn 庞统作为协调节点**
5. **没有 ci-notifier**——不需要虚拟角色,Gitea 是流程引擎,daemon 是翻译器+监控器
### §15.2 Gitea Webhook 能力清单
| 事件类型 | 触发时机 | Payload 关键字段 | 工具链用途 |
|---------|---------|----------------|-----------|
| `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 | Mail 通知 PR 作者合并完成(PR #38 恢复) |
| `issue_comment` | PR/Issue 评论 | 评论者, 内容 | CI workflow 写的失败评论 → 转发 Mail |
| `issues` (opened+assigned) | Issue 创建/指派 | Issue ID, 标题, 被指派人 | → Mail 通知开发者 |
| `release` | Release 创建 | tag, 名称 | 触发完整 CI+部署 |
> **说明**Webhook 是单向 POSTGitea 发完不管。需要在 daemon 里实现 `/webhook` 端点接收。
### §15.3 Daemon Webhook 模块设计
在 daemon 中新增一个 Webhook 接收模块(FastAPI 路由):
```python
# src/api/webhook_routes.py
@router.post("/webhook/gitea")
async def handle_gitea_webhook(event: dict, x_gitea_event: str = Header(...), x_gitea_signature: str = Header(None)):
"""接收 Gitea Webhook,翻译成 Mail 或 spawn 庞统"""
# 签名验证(防止伪造)
if x_gitea_signature and WEBHOOK_SECRET:
expected = hmac.new(WEBHOOK_SECRET, json.dumps(event).encode(), sha256).hexdigest()
if not hmac.compare_digest(expected, x_gitea_signature):
raise HTTPException(403, "Invalid webhook signature")
# Git 用户名 → Agent ID 映射
def to_agent_id(git_username: str) -> str:
return GIT_TO_AGENT.get(git_username, git_username)
if x_gitea_event == "pull_request":
action = event["action"]
if action == "opened":
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"]:
# PR #38: 通知 PR 作者合并完成
await _handle_pr_closed(event)
elif x_gitea_event == "pull_request_review":
state = event["review"]["state"]
if state == "APPROVED":
pr_author = to_agent_id(event["pull_request"]["user"]["login"])
await send_mail(to=pr_author,
title=f"Review 通过: PR #{event['number']},请 merge", ...)
elif state == "REQUEST_CHANGES":
pr_author = to_agent_id(event["pull_request"]["user"]["login"])
await send_mail(to=pr_author,
title=f"Review 不通过: PR #{event['number']}", ...)
elif x_gitea_event == "issue_comment":
# CI 自动写的失败评论 → 转发给相关 Agent
if "CI 失败" in event["comment"]["body"]:
commenter = to_agent_id(event["sender"]["login"])
await send_mail(to=commenter,
title=f"CI 失败: PR #{event['number']}", ...)
elif x_gitea_event == "issues":
if event["action"] == "assigned":
assignee = to_agent_id(event["assignee"]["login"])
await send_mail(to=assignee,
title=f"任务指派: Issue #{event['number']}", ...)
```
关键设计:
- 模块部署在 daemon 的 FastAPI 里(已有服务,只加路由)
- 和 Mail API 共享 SQLite(直接调内部函数,不经过 HTTP)
- 简单事件(CI失败通知、Review通知、任务指派)直接发 Mail
- 复杂事件(部署失败、架构级 Review 问题)spawn 庞统
### §15.4 回复驱动与超时处置
> daemon ticker 中新增两个定时扫描任务,**回复驱动是主路径,超时检测是兜底**。
**主路径:回复驱动(Agent 回复 Mail 后,daemon 根据结果触发下一步)**
```python
# daemon ticker 每小时跑一次
async def process_flow():
# 路径1:扫描新回复,驱动下一步(主路径)
await check_replies()
# 路径2:扫描超时未回复,兜底处置
await check_stale_flows()
async def check_replies():
"""扫描 Mail 回复,根据结果触发下一步"""
# 查所有有新回复的 Mail(回复在上一轮扫描之后)
replied_mails = queries.find_recently_replied(since=last_scan)
for mail in replied_mails:
reply = get_reply(mail.id)
if is_success_reply(reply):
# 成功 → 记录闭环,不触发动作
# 原因:Gitea 自动化(Actions + Webhook)会自动推动下一步
# 例:张飞回复"已创建PR" → Gitea Webhook 自动通知司马懿 Review
# 例:张飞回复"已merge" → Gitea Actions 自动触发 deploy
log.info(f"Mail {mail.id} 已成功闭环: {reply.text[:100]}")
elif is_failure_reply(reply):
# 失败 → 创建 Issue 走正式流程
await create_issue(
title=f"执行失败: {mail.title}",
labels=["bug"],
body=f"原指令: {mail.text}\n\n失败原因: {reply.text}"
)
# 通知庞统
await send_mail(to="pangtong-fujunshi",
title=f"执行失败已创建 Issue: {mail.title}", ...)
elif is_help_reply(reply):
# 需协助 → spawn 庞统协调
await spawn_agent("pangtong-fujunshi",
f"Agent {reply.from} 执行 '{mail.title}' 遇到困难,请求协助。详情: {reply.text}")
```
**兜底路径:超时检测(Agent 没回复时的处置)**
```python
async def check_stale_flows():
"""扫描超时未回复的 Mail,按个性化 deadline 判断"""
pending_mails = queries.find_pending_requests()
for mail in pending_mails:
deadline_hours = mail.meta.get("deadline_hours", 8) # 默认 8 小时
hours_elapsed = (now() - mail.created_at).total_seconds() / 3600
if hours_elapsed < deadline_hours:
continue # 未超时,跳过
hours_overdue = hours_elapsed - deadline_hours
if hours_overdue < 4:
# 轻度:重发 Mail 提醒
await send_mail(to=mail.to, title=f"提醒: {mail.title}",
text=f"此任务已超时 {hours_overdue:.0f} 小时,请尽快处理。")
elif hours_overdue < 16:
# 中度:spawn 庞统介入
await spawn_agent("pangtong-fujunshi",
f"流程卡住: Mail {mail.id} '{mail.title}' 超时 {hours_overdue:.0f} 小时未回复。请介入协调。")
else:
# 严重:创建 Issue + 通知庞统
await create_issue(title=f"流程严重超时: {mail.title}", labels=["blocked", "priority:high"])
await send_mail(to="pangtong-fujunshi",
title=f"严重超时: {mail.title},已创建 Issue", ...)
```
**失败处置原则**
- 所有失败 → Issue 走正式流程,不私下通知 Agent 悄悄解决
- CI 失败 → CI workflow 写 PR 评论 → Webhook → Mail 通知 → Agent 修 → 走标准 PR 流程
- 部署失败回滚 → CI workflow 创建 Issuepriority:high → 走标准 Bug 修复流程
- 超时未响应 → 三级处置(提醒→庞统介入→Issue+通知主公)
**断点续传**
- 每个环节的状态都在 Gitea 上(PR 状态、CI 结果、Review 状态)
- Mail 线程提供完整的可追溯记录
- 回复驱动确保正常流程自动推进,超时检测确保异常被捕获
- 流程卡住时,庞统可以根据 Mail 线程 + Gitea 状态快速定位断点并继续
### §15.5 Mail 模板(流程强约束固化)
> **注意**:Mail 模板里固化的是**流程强约束**——这个工作链特有的一步步怎么做。
> 共通技能(git 操作、审查清单等)放在 Skill 里(见 §10)。
**模板 1:任务指派(庞统/daemon → 开发者)**
```
Title: 任务指派: Issue #{N} - {标题}
Text:
Issue: {url}
分支: {branch_name}
风险级别: {level}
验收标准: {criteria}
流程:
1. 创建分支 {branch_name}
2. 编码 + 写 UT
3. push → 等 CICI 会自动跑)
4. CI 通过后创建 PRGitea API: POST /repos/{owner}/{repo}/pulls
5. 等 Review(司马懿会自动收到通知)
6. Review 通过后 merge PR
如果 CI 失败:修复后重新 push,回到步骤 3
如果 Review 不通过:按意见修改后重新 push,回到步骤 3
完成后回复此 Mail 确认。
时限:{deadline}
```
**模板 2Review 请求(Webhook → 司马懿)**
```
Title: Review 请求: PR #{N}
Text:
PR: {url}
标题: {title}
风险级别: {level}
改动文件: {file_list}
流程:
1. 读取 PR diffGitea API: GET /repos/{owner}/{repo}/pulls/{N}.diff
2. 按审查清单审查(参考 code-review Skill
3. 提交 ReviewGitea API: POST /repos/{owner}/{repo}/pulls/{N}/reviews
- APPROVE 或 REQUEST_CHANGES
4. 提交后张飞会自动收到通知
完成后回复此 Mail 确认。
时限:{deadline}
```
**模板 3Review 结果通知(Webhook → 张飞)**
```
Title: Review {result}: PR #{N}
Text:
PR: {url}
结果: {APPROVED / REQUEST_CHANGES}
审查意见: {review_body}
{如果通过}:
流程:merge PRGitea API: POST /repos/{owner}/{repo}/pulls/{N}/merge
{如果不通过}:
流程:
1. 按审查意见修改代码
2. push → CI 自动跑
3. CI 通过后等重新 Review
完成后回复此 Mail 确认。
```
**模板 4CI 失败通知(Webhook from issue_comment → 张飞)**
```
Title: CI 失败: PR #{N}
Text:
PR: {url}
失败测试: {failed_tests}
错误摘要: {error_summary}
流程:
1. 查看完整 CI 日志(PR 页面)
2. 修复失败的测试
3. push → CI 自动重跑
完成后回复此 Mail 确认。
```
**模板 5:部署失败通知(→ 庞统,复杂事件 spawn)**
```
Title: 部署失败已回滚: {repo}
Text:
仓库: {repo}
触发 commit: {sha}
失败原因: {error}
已自动回滚到: {previous_version}
已自动创建 Issue #{M}。
请协调排查。
```
**模板 6:Bug 确认通知(司马懿 → 改动者)**
```
Title: Bug 确认: Issue #{N} - {标题}
Text:
Issue: {url}
根因: {根因描述}
复现步骤: {steps}
影响范围: {scope}
风险级别: {level}
分支: fix/issue-{N}-{简述}
流程:
1. 创建分支 fix/issue-{N}-{简述}
2. 修复 bug + 写回归测试
3. push → CI 自动跑
4. CI 通过后创建 PR
5. 等 Review
完成后回复此 Mail 确认。
时限:{deadline}
```
### §15.6 Skill vs Mail 边界
| 维度 | Mail 模板 | Skill |
|------|----------|-------|
| 作用域 | 工具链流程特有 | 全局,所有项目通用 |
| 强制度 | 强约束,Agent 必须按流程走 | 按需加载,提升质量 |
| 内容 | 流程步骤+Gitea API 调用指令 | 共通技能(审查清单、git规范等) |
| 示例 | "CI失败→修→push→等CI→等Review" | "怎么写好 UT"、"审查清单" |
| 没有 Skill 能否运转 | ✅ 能(流程不会断) | 只是质量没那么高 |
**原则:没有 Skill,流程还能运转。有了 Skill,流程运转得更好。**
Skill 只放共通的、所有项目都能用的技能:
- `git-workflow`:分支命名、commit 规范(所有项目通用)
- `code-review`:审查清单(所有项目通用)
- `testing-workflow`:测试规范(所有项目通用)
- `bugfix-workflow`:根因定位方法(所有项目通用)
工具链特有的流程("CI 失败后怎么做"、"Review 通过后怎么做")全部固化在 Mail 模板里。
### §15.7 Agent ID = Git 用户名(已确认一致)
姜维确认(2026-06-07):Gitea 注册用户名就是完整 Agent ID,短名用户已删除。
| Agent ID= Git 用户名) |
|-------------------------|
| pangtong-fujunshi |
| simayi-challenger |
| zhangfei-dev |
| guanyu-dev |
| zhaoyun-data |
| jiangwei-infra |
不需要映射表,`to_agent_id()` 直用。
---
## §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 类型 | 中枢产出就是 Mailfrom=system, type=inform),模板填充 description,投递复用 spawner。区别只在内容(模板化)和元数据(结构化),不需要新的数据结构 |
| D2 | 提示词独立模板文件 | 模板和代码分离,改文案不改代码,可审查可扩展 |
| D3 | 中枢是 daemon 内模块 | 要访问 Blackboard 创建 Task,共享进程。用独立路由模块(`toolchain_routes.py`)保持职责清晰 |
| D4 | 同步处理 | 事件处理很轻(解析+模板填充+创建Task),远在 Gitea 5秒超时内。复杂事件未来用 asyncio.create_task |
| D5 | Agent ID = Gitea 用户名,直用 | 姜维确认:Gitea 注册用户名就是完整 Agent IDzhangfei-dev / simayi-challenger 等),短名已删除。`to_agent_id()` 直用,不需要映射表 |
| 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 负责)
- 不负责 Agent 回复 Mail 后的流程驱动(由 daemon ticker 的回复驱动机制负责,见 §15.4)
### 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 创建 Issue → 触发 issues 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号、分支、失败步骤、错误摘要 |
| ~~部署成功~~ | ~~不需要通知~~ | ~~流程自动闭环~~ | ~~—~~ | ~~Gitea Issue 自动关闭~~ |
| `issues` created (标题匹配 🔴 部署失败) | Deploy workflow → Webhook | 姜维 + 庞统 | `deploy_failure.md` | 仓库、commit、失败原因 |
### 2.3 事件过滤规则
不是所有事件都需要处理:
| 规则 | 说明 |
|------|------|
| 只处理白名单内的事件类型 | 未知的忽略 + 日志 |
| issue_comment 需判断来源 | 只处理 CI workflow 写的评论(按特定前缀匹配:`❌ **CI 失败**` 或统一后的 `[CI]` 前缀) |
| PR 作者/审查者必须是已知 Agent | 未知的忽略 + 日志 |
| 幂等:同一事件不重复创建 Mail | 双重去重:① delivery UUID`{event}-{delivery}`)标准幂等;② review 事件 payload 内容去重(`{event}:{pr_num}:{sender}:{sha256(body_or_content)[:16]}`),防御同一 review 被不同来源重复提交(2026-06-09 新增) |
---
### §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_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 diffGitea API: GET /repos/{repo}/pulls/{pr_number}.diff
2. 按审查清单审查(参考 code-review Skill
3. 提交 ReviewGitea API: POST /repos/{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
from pathlib import PurePath
# 高风险文件路径模式(使用 pathlib.PurePath.match,支持 ** 递归)
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 PurePath(f).match(pattern):
return "high"
return "standard"
```
---
### §16.4. 技术设计
### 4.1 模块结构
```
src/config/
└── agents.py # Agent ID 统一注册表
src/api/
├── toolchain_routes.py # 事件中枢路由(~250行)
├── 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_failure.md
```
### 4.2 toolchain_routes.py 接口设计
```python
# src/api/toolchain_routes.py
import asyncio
from fastapi import APIRouter, Request, Response
from src.config.agents import AGENT_IDS
from src.daemon.toolchain_templates import TemplateEngine
router = APIRouter()
engine = TemplateEngine()
GITEA_WEBHOOK_SECRET = os.environ.get("GITEA_WEBHOOK_SECRET", "")
_idempotency_lock = asyncio.Lock()
@router.post("/webhook/gitea")
async def 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. 签名验证(HMAC-SHA256Gitea 1.23.4 已确认)
if GITEA_WEBHOOK_SECRET:
expected = hmac.new(GITEA_WEBHOOK_SECRET.encode(), body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, (x_gitea_signature or "")):
return Response(status_code=403, content="signature verification failed")
event = json.loads(body)
# 2. 幂等检查(asyncio.Lock 防并发竞态)
if x_gitea_event and x_gitea_delivery:
async with _idempotency_lock:
if _is_duplicate(x_gitea_event, x_gitea_delivery):
return Response(status_code=200, content="duplicate")
# 3. 路由到对应处理器
handler = HANDLERS.get(x_gitea_event)
if not handler:
return Response(status_code=200, content="ignored")
# 4. 处理事件 → 创建 Mail
try:
result = await handler(engine, event)
return Response(status_code=200)
except Exception as e:
logger.exception("Failed to handle %s event", x_gitea_event)
return Response(status_code=500, content=str(e))
```
### 4.3 事件处理器
每个事件类型一个处理函数,职责:解析 payload → 选模板 → 填充 → 创建 Mail Task。
```python
async def handle_issues(engine: TemplateEngine, event: dict) -> str:
"""issues 事件分发:assigned → 通知被指派人;opened + 标题匹配 → 部署失败通知"""
action = event.get("action")
if action == "assigned":
return await _handle_issue_assigned(engine, event)
if action == "opened":
title = event.get("issue", {}).get("title", "")
if "部署失败" in title:
return await _handle_deploy_failure(engine, event)
return None # 忽略其他 action
async def handle_pull_request(engine: TemplateEngine, event: dict) -> str:
"""pull_request 事件分发:只处理 opened,忽略 synchronized/closed/reopened"""
if event.get("action") != "opened":
return None
return await _handle_pr_opened(engine, event)
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"],
"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"],
}
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"]
# 匹配 CI workflow 写的失败评论(当前格式: "❌ **CI 失败**",未来统一为 "[CI]"
if "[CI]" in comment_body or "CI 失败" in comment_body:
return await handle_ci_comment(engine, event)
# 部署失败当前走创建 Issue(不走 PR comment),见 §16.8 #6
return None
return None # 非 CI/部署评论不处理
# HANDLERS 注册表
HANDLERS = {
"pull_request": handle_pull_request, # 只处理 opened
"pull_request_review": handle_review_submitted,
"issues": handle_issues, # 处理 assigned + opened(部署失败)
"issue_comment": handle_issue_comment, # 只处理 CI 失败评论
}
```
### 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 Taskfrom=system, type=inform
复用 mail_notify.py 的模式:直接通过 Blackboard 创建 Task。
"""
# 从 mail_routes.py 提取共用逻辑,或直接复用 mail_notify 的方式
...
def is_duplicate(event_key: str) -> bool:
"""幂等检查:同一事件不重复创建 Mail
存储在 SQLite events 表,daemon 重启后仍有效。
保留最近 7 天记录,ticker 定期清理过期记录。
"""
...
def calc_risk_level(files: list[str]) -> str:
"""按文件路径规则判定风险级别"""
...
def get_changed_files(pr: dict) -> list[str]:
"""从 PR payload 提取改动文件列表
payload 只有 changed_files 数量,需要额外调用 Gitea API:
GET /repos/{owner}/{repo}/pulls/{number}/files
"""
...
```
### 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 |
| PR 文件获取失败(3次重试后) | 降级为 risk_level=standard + 错误信息写入 Mail 正文 | 200handler 内部处理) |
| 模板填充失败 | 日志 error | 500(触发 Gitea 重试) |
| Mail 创建失败 | 日志 error | 500(触发 Gitea 重试) |
**原则**
- daemon 自身能处理的错误(格式、过滤、幂等)→ 返回 200,不让 Gitea 无意义重试
- daemon 处理不了的错误(数据库、内部异常)→ 返回 500,让 Gitea 重试
---
### §16.6. CI/Deploy Workflow 配合
### 6.1 CI 失败通知
CI workflow 已有 `notify-on-failure` jobci.yml),当前格式:
```
❌ **CI 失败**
请检查 CI 日志并修复。
触发 commit: `abc1234`
```
**需要修改**:将 comment 前缀统一为 `[CI]`,方便中枢匹配:
```yaml
# .gitea/workflows/ci.yml — notify-on-failure job
-d "{\"body\": \"❌ **CI 失败**\\n\\n请检查 CI 日志并修复。\"}"
+d "{\"body\": \"[CI] 失败\\n\\n分支: ${{ gitea.ref_name }}\\n触发 commit: \\`${{ gitea.sha }}\\`\\n请检查日志并修复。\"}"
```
### 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 | ✅ 已配置 | 组织级 webhook secret = `22760993dff898a190731da43aa8d964`daemon `GITEA_WEBHOOK_SECRET` 同步 |
| 3 | CI workflow PR comment | ✅ 已有 | ci.yml 已有 notify-on-failure job 写 PR comment,格式为 `❌ **CI 失败**...`,需改为 `[CI]` 前缀 |
| 4 | `from=system` 走内部函数 | ✅ 已确定 | 走内部函数(和 mail_notify.py 一致),不走 HTTP API |
| 5 | PR changed_files | ✅ 已实现 | `_fetch_pr_files()` 调用 Gitea API3 次重试 + 失败信息写入 Mail正文 |
| 6 | Deploy workflow 通知方式 | ✅ 已确认 | 当前创建 Issue(非 PR comment)。部署通知走 `issues` Webhookissue created)而非 `issue_comment` |
| 7 | 签名算法 | ✅ 已确认 | Gitea 使用 HMAC-SHA256,代码注释已补 |
| 8 | Webhook 作用范围 | ✅ 组织级 | Gitea 组织级 webhookHook ID=28),覆盖 sanguo 下所有仓库,新增仓库自动覆盖 |
| 9 | ALLOWED_HOST_LIST | ✅ 已修复 | Gitea 容器配置 `192.168.2.153, 127.0.0.1, localhost, 172.17.0.0/16, 192.168.2.0/24` |
| 10 | Gitea review payload 格式 | ✅ 姜维调研确认(2026-06-08 | Gitea v1.23.4 review payload 只有 `type` + `content`,没有 `state`/`body`/`user`,这不是 org vs repo 差异而是 Gitea 设计。v1.24.0 格式不变。双格式兼容是防御性编码,保持现状 |
| 11 | Spawner compact 检测窗口 | ✅ 已修复 | 窗口 300s→900s,尾部读取 50KB→1MB。实测长对话中 compact 记录被推出窗口导致漏检 |
| 12 | inform 类型 Mail crash 误标 done | ✅ 已修复 | `_mail_auto_complete` 增加 outcome 感知,inform 用白名单(completed/claimed/no_reply)控制 done 标记。spawner crash cooldown 300s→60s |
---
### §16.9. 实施计划
| 步骤 | 内容 | 依赖 |
|------|------|------|
| 1 | `toolchain_routes.py` 骨架 + Gitea Webhook 接收 + 签名验证 | 无 |
| 2 | `toolchain_templates.py` 模板引擎 | 无 |
| 3 | 5 个模板文件 | 步骤 2 |
| 4 | 4 个事件处理器(PR/Review/Issue/Comment | 步骤 1+3 |
| 5 | 幂等检查(SQLite events 表,保留 7 天) | 步骤 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` | ~1705个文件) | 新增 |
| `src/api/mail_routes.py` | ~20 | 修改(提取共用函数) |
| `.gitea/workflows/ci.yml` | ~15 | 修改(加 comment step |
| `.gitea/workflows/deploy.yml` | ~15 | 修改(加 comment step |
| **总计** | **~480** | |
---
### §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 的落地细节 |
### v2.1 → v2.2 变更(事件中枢完善 + E2E 验证通过)
| 编号 | 变更内容 |
|------|---------|
| §16.8 | #2 更新:组织级 webhook 已配置(Hook ID=28+ secret 生效;#5 更新:PR 文件获取已实现 3 次重试;新增 #8(组织级覆盖)和 #9ALLOWED_HOST_LIST 修复) |
| §16.4 | 技术设计更新:幂等检查加 `asyncio.Lock` 防并发竞态(T-02);Agent ID 提取到 `config/agents.py` 统一管理(T-04);`_fetch_pr_files` 返回 `Tuple[List[str], str]`,3 次重试 + 失败信息写入 Mail(T-05 |
| §16.5 | 错误处理更新:PR 文件获取失败不再静默,3 次重试后错误信息写入 Mail 正文 |
| 全局 | 广播风暴 bug 修复:`mail_notify.py` 校验 Agent ID,非有效 Agent(如 system)路由给庞统 |
| E2E | S22 模拟测试 13/13 通过;S23 真实 Gitea Webhook 8/8 通过;组织级 webhook 跨仓库投递 + 签名验证通过 |
### v2.2 → v3.0 变更(合并 §14 工具链 Skill + 自动部署)
| 编号 | 变更内容 |
|------|----------|
| §17 | 新增:原 `14-toolchain-skill-and-deploy.md` v1.1 全文合并为 §17「工具链落地设计」 |
| §17.1 | 目标:L1 TOOLS.md + L2 Skill 升级 + deploy.sh + 端到端验证 |
| §17.2 | 知识体系四层定位(L1 操作手段 + L2 流程规范) |
| §17.3 | L1 TOOLS.md:按 6 个角色定制 Gitea API 操作模板 |
| §17.4 | L2 Skill 升级:7 个现有 Skill 对齐事件中枢 + CI |
| §17.5 | deploy.sh + deploy.yml 统一接口(rsync + pm2 + health check + rollback |
| §17.6 | 端到端验证:S1-S6 六个场景 |
| §17.7 | 覆盖率渐进策略(P1 只报告 → P2 40% → P3 60% |
| §17.8 | 前端展示:Gitea 自带 CI 管理界面 |
| §17.9 | 实施路线(P1→P4 |
| §17.10 | 前置条件 Checklist6 项) |
| §17.11 | 评审记录(M1/M2 修正 + S1-S4 采纳) |
---
## §17. 工具链落地设计(原 §14 合并)
> **来源**: 原 `14-toolchain-skill-and-deploy.md` v1.1(仲达评审 M1/M2 修正 + S1-S4 采纳)
> **合并日期**: 2026-06-08
> **定位**: 工具链从"设计完成"到"正式投入使用"的落地设计
---
### §17.1. 目标
把工具链从设计态推进到可使用态:
1. **L1 TOOLS.md**:每个 Agent 的 TOOLS.md 加入 Gitea API 操作模板,收到 Mail 后开箱即用
2. **L2 Skill 升级**:7 个现有 Skill 对齐事件中枢 + CI 实际运行
3. **deploy.sh + deploy.yml**:补完自动部署的实际脚本
4. **端到端验证**:用 sanguo/moziplus-v2 实验项目验证全链路
---
### §17.2. 知识体系四层定位
> 来源: `architecture-v3.0.md` §10 BootstrapBuilder
| 层 | 定位 | 载体 | Token | 工具链职责 |
|----|------|------|-------|-----------|
| **L0** | 铁律层 | AGENTS.md / MEMORY.md | ~500 | 不涉及 |
| **L1** | 角色层 | SOUL.md + **TOOLS.md** | ~2000 | **Gitea API 操作模板**Agent 自带,开箱即用) |
| **L2** | 引擎注入 | BootstrapBuilder 注入 Skill 全文 | ~1500 | **7 个 Skill v2**(流程规范,Daemon 确定性注入) |
| **L3** | 被动参考 | Skill descriptionAgent 按需加载 | 按需 | 复用 L2 Skill,不新建 |
**核心原则**
- L1 给操作手段("怎么做"):curl 命令模板、参数说明
- L2 给流程规范("什么时候做什么"):审查清单、分支规范、测试策略
- L3 是 L2 的被动触发版本,不需要单独维护
---
### §17.3. L1TOOLS.md Gitea 操作模板
#### §17.3.1 设计原则
1. **开箱即用**:收到 Mail 后直接复制粘贴 curl 命令即可执行,不需要查文档
2. **按角色定制**:不是每个 Agent 都需要全套 API,只给该角色需要的
3. **统一格式**:所有模板使用相同的环境变量约定
#### §17.3.2 公共变量约定
每个 Agent 的 TOOLS.md 头部加入:
```markdown
## Gitea 工具链
- **地址**: http://192.168.2.154:3000
- **组织**: sanguo
- **认证**: `Authorization: token $GITEA_TOKEN`(各 Agent 使用自己的 token
- **CI 管理界面**: http://192.168.2.154:3000/sanguo/{repo}/actions
```
#### §17.3.3 按角色模板
##### A. 开发者(张飞/关羽/赵云)— PR 创建 + Merge
```markdown
### 创建 PR
```bash
curl -X POST "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/pulls" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"head": "{branch}",
"base": "main",
"title": "{标题}"
}'
# 返回 .number 即 PR 号
```
### 查看 PR diff
```bash
curl "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/pulls/{pr_number}.diff" \
-H "Authorization: token $GITEA_TOKEN"
```
### Merge PRReview 通过后)
```bash
curl -X POST "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/pulls/{pr_number}/merge" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"Do": "merge", "merge_title_field": "Merge PR #{pr_number}"}'
```
### 查看 CI 状态
```bash
curl "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/commits/{sha}/status" \
-H "Authorization: token $GITEA_TOKEN"
# .state = "success" | "pending" | "failure" | "error"
```
```
##### B. 审查者(司马懿)— Review 操作
```markdown
### 读取 PR diff
```bash
curl "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/pulls/{pr_number}.diff" \
-H "Authorization: token $GITEA_TOKEN"
```
### 查看 PR 改动文件列表
```bash
curl "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/pulls/{pr_number}/files" \
-H "Authorization: token $GITEA_TOKEN"
# 每个文件有 .filename, .additions, .deletions, .changes
```
### 提交 Review
```bash
curl -X POST "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/pulls/{pr_number}/reviews" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"body": "{审查意见}",
"event": "APPROVED"
}'
# event 可选: APPROVED | REQUEST_CHANGES | COMMENT
# ⚠️ 注意:是 APPROVED(过去式),不是 APPROVE
```
### 风险级别判定(自动 + 确认)
规则见 code-review Skill。改动者不能降级,只能维持或升级。
```
##### C. 协调者(庞统)— 全套管理
```markdown
### 创建 Issue + 指派
```bash
curl -X POST "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/issues" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "{标题}",
"body": "{描述}",
"assignees": ["{agent_id}"],
"labels": [1, 2]
}'
# labels 需用数字 ID,先 GET /repos/{owner}/{repo}/labels 查询
```
### 查询仓库 Labels
```bash
curl "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/labels" \
-H "Authorization: token $GITEA_TOKEN"
```
### 查询 PR 列表
```bash
curl "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/pulls?state=open" \
-H "Authorization: token $GITEA_TOKEN"
```
### 创建 Release
```bash
curl -X POST "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/releases" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tag_name": "v{version}",
"name": "v{version}",
"body": "{changelog}",
"target_commitish": "main"
}'
```
### 关闭 Issue
```bash
curl -X PATCH "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/issues/{issue_number}" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"state": "closed"}'
```
```
##### D. 平台运维(姜维)— deploy + runner 管理
在开发者模板基础上追加:
```markdown
### 触发手动部署(需要 push 到 main 或手动 re-run
```bash
# Re-run 最近一次 workflow
curl -X POST "http://192.168.2.154:3000/api/v1/repos/sanguo/{repo}/actions/runs/{run_id}/rerun" \
-H "Authorization: token $GITEA_TOKEN"
```
### 查看 deploy-history
```bash
cat ~/.sanguo_projects/{project}/data/deploy-history.jsonl
```
### deploy.sh 规范
见 §17.5 deploy.sh 设计。所有项目必须遵循统一接口。
```
#### §17.3.4 实施清单
| Agent | TOOLS.md 新增 | 预计行数 |
|-------|--------------|---------|
| zhangfei-dev | 开发者模板(PR/Merge/CI状态) | ~30 行 |
| guanyu-dev | 开发者模板 | ~30 行 |
| zhaoyun-data | 开发者模板 | ~30 行 |
| simayi-challenger | 审查者模板(diff/review/风险判定) | ~40 行 |
| pangtong-fujunshi | 协调者模板(全套管理) | ~50 行 |
| jiangwei-infra | 开发者 + 平台运维模板 | ~45 行 |
---
### §17.4. L2Skill 升级到 v2
#### §17.4.1 升级原则
1. **不重写已有内容**,在现有 Skill 基础上追加/修改
2. **对齐事件中枢**:每个 Skill 说明在事件中枢链路中的位置
3. **对齐 CI 实际运行**ci.yml/deploy.yml 已有实际配置,Skill 要反映真实情况
4. **对齐 Gitea v1.23.4 限制**:不支持 failure()、concurrency、permissions 等
#### §17.4.2 升级内容
##### git-workflow(小改动)
| 改动 | 说明 |
|------|------|
| 追加"事件中枢集成"节 | PR 创建 → Gitea Webhook → 中枢通知司马懿 → Review → 中枢通知作者 → Merge → deploy.yml 自动触发 |
| 追加"CI 自动触发"说明 | push 非 main 分支自动触发 ci.ymlpush main 自动触发 deploy.yml |
| 追加"分支感知"强化 | Agent spawn 后必须 `git branch --show-current`,确认分支正确 |
##### code-review(中等改动)
| 改动 | 说明 |
|------|------|
| 追加"事件中枢触发"节 | 收到 Mail → 读 PR diff → 审查 → 提交 Review → Webhook 自动通知作者 |
| 更新风险判定规则 | 对齐 §6.1 实际规则(按文件路径自动判定 + 只升不降) |
| 追加 Gitea Review API 操作 | curl 模板(和 L1 TOOLS.md 一致,这里放流程说明) |
| 追加审查结论格式 | APPROVE / REQUEST_CHANGES 的标准格式 |
##### testing-workflow(小改动)
| 改动 | 说明 |
|------|------|
| 追加"CI 集成"节 | UT 在 CI 自动跑(ci.yml test job)、coverage 在 deploy.yml ci job 跑 |
| 追加"E2E 触发方式" | 通过 e2e.yml 手动触发或 `RUN_INTEGRATION=1 pytest` 本地跑 |
| 追加"测试数据隔离" | CI 使用临时 venv + 临时 SQLite + 临时端口 |
| 追加"广播风暴禁止" | **禁止在 daemon 运行时跑含创建项目/Task/Mail 的测试**,否则会触发 Agent spawn 导致广播风暴。E2E 测试必须在 CI 隔离环境或 daemon 停止后跑 |
##### bugfix-workflow(小改动)
| 改动 | 说明 |
|------|------|
| 追加"事件中枢链路"节 | Bug Issue 创建/指派 → 中枢发 Mail → 修复 → PR → CI → Review → merge |
| 追加"CI 验证"步骤 | 修复后必须等 CI 通过再创建 PR |
##### hotfix-workflow(小改动)
| 改动 | 说明 |
|------|------|
| 追加"CI 自动跑"说明 | hotfix push main → deploy.yml 自动跑 CI + 部署 |
| 追加"失败自动创建 Issue" | deploy.yml notify-on-failure 已实现 |
| 更新 24h 复盘流程 | 复盘结论写到 Issue 评论中 |
##### ci-cd-ops(重写)
| 改动 | 说明 |
|------|------|
| 重写为 v2 | 对齐实际 ci.yml/deploy.yml 结构 |
| 新增 Gitea v1.23.4 限制清单 | 不支持 failure()/concurrency/permissions 等,workaround 方案 |
| 新增覆盖率渐进策略 | P1 只报告 → P2 40% 阈值 → P3 60% 阈值 |
| 新增 deploy.sh 规范 | 统一接口:--version / --source / --target / --health-check / --rollback |
| 新增 CI secret 配置 | CI_TOKEN 作为 repository secret 配置 |
##### release-workflow(中等改动)
| 改动 | 说明 |
|------|------|
| 追加"自动部署触发" | tag 创建 → deploy.yml 自动触发 |
| 追加 deploy-history.jsonl 规范 | 每次 deploy 记录 tag + commit + 时间戳 |
| 更新 schema 变更规范 | 向前兼容 Checklist(加列不加删、默认值、迁移脚本) |
#### §17.4.3 不新建 Skill
现有 7 个 Skill 覆盖所有工具链流程。不需要为事件中枢、CI 操作新建 Skill——这些流程固化在 Mail 模板(§16.4.3)中,Skill 只提供共识功能。
#### §17.4.4 实施清单
| Skill | 改动级别 | 预计改动行数 |
|-------|---------|------------|
| git-workflow | 小 | +15 行 |
| code-review | 中 | +40 行 |
| testing-workflow | 小 | +25 行(含广播风暴禁止约束) |
| bugfix-workflow | 小 | +15 行 |
| hotfix-workflow | 小 | +15 行 |
| ci-cd-ops | 重写 | ~120 行(原 114 行) |
| release-workflow | 中 | +30 行 |
---
### §17.5. deploy.sh + deploy.yml 补完
#### §17.5.1 deploy.sh 统一接口
每个项目的 `scripts/deploy.sh` 必须遵循以下接口:
```bash
#!/bin/bash
# scripts/deploy.sh — 项目部署脚本
# 用法:
# bash scripts/deploy.sh --version # 显示当前版本
# bash scripts/deploy.sh --source=DIR --target=DIR --health-check # 部署
# bash scripts/deploy.sh --rollback # 回滚到上一版本
set -euo pipefail
SOURCE_DIR=""
TARGET_DIR=""
HEALTH_CHECK=false
ACTION="deploy"
for arg in "$@"; do
case $arg in
--version) ACTION="version" ;;
--source=*) SOURCE_DIR="${arg#*=}" ;;
--target=*) TARGET_DIR="${arg#*=}" ;;
--health-check) HEALTH_CHECK=true ;;
--rollback) ACTION="rollback" ;;
esac
done
PROJECT_NAME="{project_name}"
DEPLOY_HISTORY="${TARGET_DIR}/data/deploy-history.jsonl"
version() {
echo "${PROJECT_NAME} deploy version: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
deploy() {
echo "=== Deploying ${PROJECT_NAME} ==="
echo "Source: ${SOURCE_DIR}"
echo "Target: ${TARGET_DIR}"
# 1. 同步文件(排除不需要部署的)
# ⚠️ --delete 会删除目标中源没有的文件,必须排除 data/(生产数据)
rsync -av --delete \
--exclude='.git' \
--exclude='__pycache__' \
--exclude='.venv' \
--exclude='data' \
--exclude='tests' \
--exclude='docs' \
--exclude='.gitea' \
--exclude='node_modules' \
"${SOURCE_DIR}/" "${TARGET_DIR}/"
# 2. 安装依赖
if [ -f "${TARGET_DIR}/pyproject.toml" ]; then
cd "${TARGET_DIR}"
python3 -m venv .venv
.venv/bin/pip install --quiet -e ".[dev]" 2>/dev/null || \
.venv/bin/pip install --quiet -e . 2>/dev/null || true
fi
# 3. 重启服务
if command -v pm2 &>/dev/null; then
pm2 restart ${PROJECT_NAME} 2>/dev/null || true
fi
# 4. 健康检查
if [ "$HEALTH_CHECK" = true ]; then
sleep 3
curl -sf http://localhost:8083/api/health && echo " ✓" || {
echo " ✗ Health check failed!"
exit 1
}
fi
# 5. 记录版本(rollback 时可通过 DEPLOY_OVERRIDE_COMMIT 覆盖)
local commit_hash
if [ -n "${DEPLOY_OVERRIDE_COMMIT:-}" ]; then
commit_hash="${DEPLOY_OVERRIDE_COMMIT}"
else
commit_hash=$(cd "${SOURCE_DIR}" && git rev-parse --short HEAD 2>/dev/null || echo "unknown")
fi
local timestamp
timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
echo "{\"timestamp\": \"${timestamp}\", \"commit\": \"${commit_hash}\", \"source\": \"${SOURCE_DIR}\"}" >> "${DEPLOY_HISTORY}"
# 保留最近 10 条
tail -10 "${DEPLOY_HISTORY}" > "${DEPLOY_HISTORY}.tmp" && mv "${DEPLOY_HISTORY}.tmp" "${DEPLOY_HISTORY}"
echo "=== Deploy complete: ${commit_hash} at ${timestamp} ==="
}
rollback() {
if [ ! -f "${DEPLOY_HISTORY}" ]; then
echo "No deploy history, cannot rollback"
exit 1
fi
# 读取倒数第二行的 commit(不依赖 SOURCE_DIR 的 git 状态)
local prev_line
prev_line=$(tail -2 "${DEPLOY_HISTORY}" | head -1)
local prev_commit
local prev_source
prev_commit=$(echo "${prev_line}" | python3 -c "import sys,json; print(json.load(sys.stdin)['commit'])" 2>/dev/null)
prev_source=$(echo "${prev_line}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('source',''))" 2>/dev/null)
if [ -z "${prev_commit}" ] || [ "${prev_commit}" = "unknown" ]; then
echo "Cannot determine previous version"
exit 1
fi
echo "=== Rolling back to ${prev_commit} ==="
# checkout 到指定 commit,部署,然后回到 main
cd "${SOURCE_DIR}"
local current_branch
current_branch=$(git branch --show-current)
git checkout "${prev_commit}"
# 部署时明确传 commit hash,不依赖 HEAD
DEPLOY_OVERRIDE_COMMIT="${prev_commit}" deploy
git checkout "${current_branch:-main}"
}
case $ACTION in
version) version ;;
deploy) deploy ;;
rollback) rollback ;;
esac
```
#### §17.5.2 deploy.yml 更新
替换当前 placeholder
```yaml
deploy:
runs-on: macos-arm64
needs: ci
steps:
- uses: actions/checkout@v4
- name: Deploy
env:
CI_TOKEN: ${{ secrets.CI_TOKEN }}
run: |
bash scripts/deploy.sh --source="$GITHUB_WORKSPACE" \
--target="$HOME/.sanguo_projects/sanguo_moziplus_v2" \
--health-check
- name: Rollback on failure
if: always()
env:
CI_TOKEN: ${{ secrets.CI_TOKEN }}
run: |
STATUS=$(curl -sf \
-H "Authorization: token $CI_TOKEN" \
"${{ gitea.api_url }}/repos/${{ gitea.repository }}/commits/${{ gitea.sha }}/status" \
| python3 -c "import sys,json; print(json.load(sys.stdin).get('state',''))" 2>/dev/null || echo "")
if [ "$STATUS" != "success" ]; then
echo "Deploy failed, rolling back..."
bash scripts/deploy.sh --rollback || echo "Rollback failed, manual intervention needed"
fi
```
#### §17.5.3 实施清单
| # | 内容 | 文件 | 说明 |
|---|------|------|------|
| D1 | 创建 deploy.sh | moziplus-v2 `scripts/deploy.sh` | 从模板创建,项目名填 sanguo_moziplus_v2 |
| D2 | 更新 deploy.yml | moziplus-v2 `.gitea/workflows/deploy.yml` | 替换 placeholder |
| D3 | 同步到 Gitea | push 到 moziplus-v2 | 触发 CI 验证 |
---
### §17.6. 端到端验证:sanguo/moziplus-v2 实验项目
#### §17.6.1 为什么用实验项目
- moziplus-v2 主项目已有正式数据(黑板项目、Mail、Task),不适合做破坏性验证
- sanguo/moziplus-v2 当前是空项目(只有 README.md + .gitea/workflows),可以随意实验
#### §17.6.2 验证场景
| 场景 | 操作 | 预期结果 |
|------|------|---------|
| **S1: CI 触发** | push 分支到实验项目 | ci.yml 自动跑 lint + test |
| **S2: PR Review 流程** | 创建 PR → 中枢 Mail → 司马懿 Review → 中枢通知作者 | 全链路 Mail 通知 |
| **S3: CI 失败通知** | push 含 lint 错误的代码 | ci.yml 失败 → 写 PR 评论 → 中枢发 Mail |
| **S4: 部署流程** | merge PR → push main | deploy.yml 自动跑 + 部署 + 健康检查 |
| **S5: Issue 指派** | 创建 Issue 并指派 | 中枢发 Mail 给被指派人 |
| **S6: 幂等验证** | 重复触发同一 delivery_id Webhook | 第二次返回 200 duplicate,不创建重复 Mail |
#### §17.6.4 P3 端到端验证结果(2026-06-08
| 场景 | 结果 | 说明 |
|------|------|------|
| **S1: CI 触发** | ✅ | push test/p3-e2e-s1 → ci.yml 触发。首次因 act-runner 离线未触发,姜维修复后积压任务自动执行 |
| **S2: PR Review 流程** | ✅ | PR opened → 中枢发 Review 请求 Mail 给 simayi-challenger ✅。Review APPROVED → 中枢发 Mail 给 PR 作者 ✅(Gitea 1.23.4 支持此 webhook |
| **S3: CI 失败通知** | ✅ | [CI] 评论 → 中枢发 Mail 给 PR作者 ✅ |
| **S4: 部署流程** | ⏭️ 跳过 | 依赖 CI 先跑通,未单独验证 |
| **S5: Issue 指派** | ✅ | Issue 指派 zhangfei-dev → 收到 Mail ✅ |
| **S6: 幂等验证** | ✅ | 同 delivery_id 第二次返回 `duplicate`,不创建重复 Mail ✅ |
##### 调研发现
**发现 1Gitea Review API event 枚举值**
Gitea ReviewStateType 枚举值应为 `APPROVED`(不是 `APPROVE`)。使用错误的枚举值会创建 PENDING review,且 PENDING 不触发 webhook 通知。
- 正确:`{"event": "APPROVED"}`
- 错误:`{"event": "APPROVE"}` → 创建 PENDING reviewwebhook 不触发
已修正:TOOLS.md 模板 + code-review Skill 中的 event 值。
**发现 2Gitea 1.23.4 支持 PullRequestReview webhook**
之前误判为"不支持",实际原因同发现 1——错误的枚举值创建了 PENDING reviewPENDING 不在 webhook switch 分支中导致报 "Unsupported review webhook type"。使用正确的 APPROVED event 后 webhook 正常触发。
daemon webhook handler 已正确覆盖:
- `pull_request_review` 事件 → `_handle_pull_request_review()`
- 只处理 APPROVED / REQUEST_CHANGES,跳过 COMMENTED 和 PENDING
- 通知对象:PR 作者
**发现 3act-runner 进程管理**
act-runner 未纳入进程管理,崩溃后不会自恢复。姜维已修复:
- 纳入 PM2 托管(进程名:sanguo-act-runnerid=44
- 启动命令:`pm2 start ~/bin/act_runner --name sanguo-act-runner --cwd ~/.config/act-runner/ -- daemon --config ~/.config/act-runner/config.yaml`
- 崩溃自动重启 ✅
- 开机自启:依赖 pm2 startup(需 sudo 配置 launchd
#### §17.6.3 实验项目 CI 配置
从 moziplus-v2 的 ci.yml 精简,实验项目只需要基本验证:
```yaml
name: CI
on:
push:
branches: ['**', '!main']
pull_request:
types: [opened, synchronize]
jobs:
lint:
runs-on: macos-arm64
steps:
- uses: actions/checkout@v4
- run: echo "lint placeholder"
test:
runs-on: macos-arm64
needs: lint
steps:
- uses: actions/checkout@v4
- run: echo "test placeholder"
```
> **注意**:主项目 ci.yml/deploy.yml 的 runs-on 已从 `ubuntu-latest` 改为 `macos-arm64`,对齐 act-runner 实际 label。deploy.yml 的 placeholder 也已替换为实际 deploy.sh 命令。
---
### §17.7. 覆盖率渐进策略
#### §17.7.1 P1 阶段(启用后 2 周)— 只报告不阻断
**零改动**。当前 deploy.yml 的 ci job 已包含 `--cov=src --cov-report=term-missing`push main 时自动跑)。
注意:ci.yml(非 main 分支)没有 coverage 配置——快速门控不需要覆盖率,这是有意为之。
收集基线数据后决定 P2 阈值。
#### §17.7.2 后续阶段(仅设计,暂不实施)
| 阶段 | 时间 | 策略 | 触发方式 |
|------|------|------|---------|
| P2 | 启用后 1 月 | 40% 阈值,低于警告不阻断 | ci.yml 加 coverage threshold check |
| P3 | 启用后 2 月+ | 60% 阈值,低于阻断 | ci.yml 加 `exit 1` |
---
### §17.8. 前端展示
#### §17.8.1 Gitea 自带管理界面
Gitea v1.23.4 自带完整的 CI 管理界面:
| 功能 | URL |
|------|-----|
| CI Runs 列表 | `http://192.168.2.154:3000/sanguo/{repo}/actions` |
| 单次 Run 日志 | `http://192.168.2.154:3000/sanguo/{repo}/actions/runs/{id}` |
| PR CI Status | PR 页面自动显示 CI 状态徽章 |
| Webhook 管理 | 仓库 Settings → Webhooks |
**不需要自己做 CI 前端。**
#### §17.8.2 moziplus v2 前端展示
工具链事件的 Mail 通知已在 moziplus v2 前端展示(Mail 列表页)。
如果未来要加,唯一值得做的是:在 moziplus 前端加一个「工具链状态」面板,聚合展示各仓库最近 CI 状态。这是 P4,不阻塞使用。
---
### §17.9. 实施路线
| 优先级 | 内容 | 耗时 | 前置 |
|--------|------|------|------|
| **P1** | L1 TOOLS.md6 个 Agent | 2h | 无 |
| **P1** | L2 Skill 升级(7 个) | 1d | 无 |
| **P2** | deploy.sh + deploy.yml | 2h | P1 Skill 升级(ci-cd-ops 定义了 deploy.sh 规范) |
| **P3** | sanguo/moziplus-v2 端到端验证 | 2h | P1 + P2 |
| **P4** | 前端工具链状态面板 | 按需 | 不阻塞 |
---
### §17.10. 前置条件 Checklist
工具链投入使用前必须确认:
| # | 条件 | 状态 | 谁确认 |
|---|------|------|--------|
| 1 | act-runner 已注册且 label = `macos-arm64` | ✅ PM2 托管(sanguo-act-runner, id=44),崩溃自动重启 | 姜维确认 |
| 2 | Gitea repository secrets 已配置(CI_TOKEN | ✅ 姜维确认(sanguo/moziplus-v2 已配 CI_TOKEN | 姜维 |
| 3 | Gitea 组织级 Webhook 已启用(Hook ID=28 | ✅ 已确认 | 已确认 |
| 4 | 各 Agent 的 GITEA_TOKEN 环境变量 | ✅ 已写入各 Agent TOOLS.md,姜维确认 token 记录存在 | 庞统+姜维 |
| 5 | main 分支保护规则(Review 才能 merge | ✅ 姜维已配置(moziplus-v2 + sanguo_moziplus_v2,需1个approve | 姜维 |
| 6 | 禁止在 daemon 运行时跑全量 E2E | ✅ 已警告司马懿 | 已确认 |
> 第 5 点很关键——如果 main 分支没有保护规则,开发者可以直接 push main 跳过 Review。
> act-runner 已于 2026-06-08 纳入 PM2 托管(姜维),崩溃自动重启 + pm2 save 已保存。开机自启依赖 pm2 startup(需 sudo 配置 launchd),P4 完成。
---
### §17.11. 评审记录(原 §14 评审)
#### v1.0 → v1.1 修订清单(仲达评审)
| 编号 | 类型 | 问题 | 修订内容 |
|------|------|------|----------|
| M1 | 必须修 | rsync --delete 会删 data/ | §17.5.1 --exclude 加 `data` + `node_modules` |
| M2 | 必须修 | runs-on 与实际环境不一致 | 实际 ci.yml/deploy.yml 已用 `macos-arm64`,CI 已跑通,属仲达误判。文档 §17.6.3 已明确说明 |
| S1 | 建议 | rollback commit 获取有竞态 | §17.5.1 rollback 改用 DEPLOY_OVERRIDE_COMMIT 显式传递,不依赖 git HEAD |
| S2 | 建议 | pip install 缺 lock file | 采纳但 P3,当前单一部署环境风险低 |
| S3 | 建议 | 缺前置条件 checklist | 新增 §17.10 前置条件 Checklist |
| S4 | 建议 | 验证场景补 S6 幂等 | §17.6.2 新增 S6 幂等验证场景 |
| — | 评审 | 庞统模板缺关闭 Issue curl | §17.3.3 C 节补关闭 Issue 模板 |
| — | 评审 | testing-workflow 加广播风暴禁止 | §17.4.2 testing-workflow 补充约束 |
| — | 评审 | ci-cd-ops 行数估算措辞 | 修正:重写后预计 ~120 行(原 114 行) |
| — | 评审 | 覆盖率 P1 前提需确认 | §17.7.1 明确指出 deploy.yml 已有 coverageci.yml 有意不加 |
---
### v3.0 → v3.1 变更(P3 端到端验证 + 调研结论)
| 编号 | 变更内容 |
|------|----------|
| §17.3.3 | Review API event 枚举值修正:APPROVE → APPROVED |
| §17.6.3 | 注意更新:runs-on 已改为 macos-arm64placeholder 已替换 |
| §17.6.4 | 新增 P3 端到端验证结果(S1-S6 逐项) |
| §17.6.4 | 新增调研发现:Review API 枚举值、PullRequestReview webhook 支持、act-runner PM2 托管 |
| §17.10 | #1 状态更新:act-runner 已纳入 PM2 托管 |
### v3.1 → v3.2 变更(工具链修复 + Mail 投递 bug 修复)
| 编号 | 变更内容 |
|------|----------|
| §16.4 | Review handler 双格式兼容:HANDLERS 注册表同时注册 `pull_request_review` / `pull_request_approved` 等多种事件名;`_handle_pull_request_review` 兼容 repo webhookreview.state/body/user)和 org webhookreview.type/content/sender)两种 payload 格式 |
| §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 改造 |