Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a2505b137 | |||
| c2e710bf67 | |||
| d2e449bca8 | |||
| 91116520d2 | |||
| 6536d99e7f | |||
| cbb58c3092 | |||
| a402bb2a2d | |||
| ac8444bb41 | |||
| d39f2ba1df | |||
| 548e39d3b8 | |||
| 1a536e84c6 | |||
| d25eea4e82 | |||
| 38c676c627 | |||
| 0c1acbd073 | |||
| b921b094ca | |||
| 7bb7637d24 | |||
| 02883750f5 | |||
| 60925c3395 | |||
| 7335cafa90 | |||
| 1362cc5d64 | |||
| 6df7563070 | |||
| da137ba193 | |||
| ac4e28a57c | |||
| 41d60cca2c | |||
| bae3244e24 | |||
| e238eaec31 | |||
| b0bca0df5c | |||
| 6d1d906551 | |||
| 37b8115485 | |||
| 7c11c6b9aa |
@@ -0,0 +1,27 @@
|
||||
name: 文档任务
|
||||
about: 编写或更新文档
|
||||
title: "[moz] docs: "
|
||||
labels:
|
||||
- type/docs
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 文档目标
|
||||
description: 要写什么文档,解决什么问题
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: path
|
||||
attributes:
|
||||
label: 文档路径
|
||||
description: 目标文件路径(如 docs/design/xxx.md)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: outline
|
||||
attributes:
|
||||
label: 大纲
|
||||
description: 文档结构和要点
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,33 @@
|
||||
name: 实现任务
|
||||
about: 按设计文档实现功能
|
||||
title: "[moz] impl: "
|
||||
labels:
|
||||
- type/feat
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 实现目标
|
||||
description: 清晰描述要实现什么
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: design_doc
|
||||
attributes:
|
||||
label: 设计文档
|
||||
description: 关联的设计文档路径(如 docs/design/xxx.md)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: scope
|
||||
attributes:
|
||||
label: 实现范围
|
||||
description: 涉及哪些文件/模块
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: acceptance
|
||||
attributes:
|
||||
label: 验收标准
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,27 @@
|
||||
name: 重构任务
|
||||
about: 重构现有代码
|
||||
title: "[moz] refactor: "
|
||||
labels:
|
||||
- type/refactor
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 重构目标
|
||||
description: 为什么重构,期望改善什么
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: scope
|
||||
attributes:
|
||||
label: 影响范围
|
||||
description: 涉及哪些文件/模块,是否有 breaking change
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: tests
|
||||
attributes:
|
||||
label: 测试保障
|
||||
description: 重构前哪些测试必须通过
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,171 @@
|
||||
# toolchain-templates.yaml
|
||||
# §21 §4 steps 模板化 — 按 action_type + business_type 查找
|
||||
# 占位符 {issue_number}/{brief}/{title}/{pr_number} 等运行时渲染
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# issue_assigned(按 Issue label 分 6 路 + infrastructure)
|
||||
# ---------------------------------------------------------------------------
|
||||
issue_assigned:
|
||||
feat:
|
||||
steps:
|
||||
- "理解需求(Issue body)→ 如有不明确在 Issue comment 追问"
|
||||
- "git checkout main && git pull origin main"
|
||||
- "git checkout -b feat/{issue_number}-{brief}"
|
||||
- "编码实现 + 写 UT"
|
||||
- "文档同步:如果本次改动涉及设计变更或接口变更,在同一分支更新 docs/design/ 对应文档。如无需更新,在 action report 中说明「文档无需更新」"
|
||||
- "git add -A && git commit -m '[moz] feat: {title}' && git push origin feat/{issue_number}-{brief}"
|
||||
- "CI 通过后创建 PR(body 含 Closes #{issue_number})"
|
||||
- "等 Review"
|
||||
output_template: |
|
||||
[Action Report]
|
||||
**分支**:feat/{issue_number}-{brief}
|
||||
**PR**:#{pr_number}
|
||||
**改动文件**:{files}
|
||||
**CI**:{ci_status}
|
||||
**文档同步**:{doc_status}
|
||||
|
||||
impl:
|
||||
steps:
|
||||
- "读设计文档(Issue body 中的路径)→ 理解实现范围"
|
||||
- "git checkout main && git pull origin main"
|
||||
- "git checkout -b impl/{issue_number}-{brief}"
|
||||
- "按设计编码实现 + 写 UT"
|
||||
- "文档同步:如果本次改动涉及设计变更或接口变更,在同一分支更新 docs/design/ 对应文档。如无需更新,在 action report 中说明「文档无需更新」"
|
||||
- "git add -A && git commit -m '[moz] impl: {title}' && git push origin impl/{issue_number}-{brief}"
|
||||
- "CI 通过后创建 PR(body 含 Closes #{issue_number} + 设计文档路径)"
|
||||
- "等 Review"
|
||||
output_template: |
|
||||
[Action Report]
|
||||
**设计文档**:{design_doc}
|
||||
**分支**:impl/{issue_number}-{brief}
|
||||
**PR**:#{pr_number}
|
||||
**改动文件**:{files}
|
||||
**CI**:{ci_status}
|
||||
**文档同步**:{doc_status}
|
||||
|
||||
bug:
|
||||
steps:
|
||||
- "读 Bug 描述 + 复现步骤(Issue body)"
|
||||
- "定位根因(读代码/日志,不要猜测)"
|
||||
- "git checkout main && git pull origin main"
|
||||
- "git checkout -b fix/{issue_number}-{brief}"
|
||||
- "修复 + 写回归测试"
|
||||
- "文档同步:如修复涉及设计/接口变更,同步更新 docs/design/ 对应文档"
|
||||
- "git add -A && git commit -m '[moz] fix: {title}' && git push origin fix/{issue_number}-{brief}"
|
||||
- "CI 通过后创建 PR(body 含 Closes #{issue_number} + 根因和修复方式)"
|
||||
- "等 Review"
|
||||
output_template: |
|
||||
[Action Report]
|
||||
**根因**:{root_cause}
|
||||
**修复方式**:{fix_approach}
|
||||
**分支**:fix/{issue_number}-{brief}
|
||||
**PR**:#{pr_number}
|
||||
**CI**:{ci_status}
|
||||
**文档同步**:{doc_status}
|
||||
|
||||
docs:
|
||||
steps:
|
||||
- "读文档目标(Issue body)"
|
||||
- "git checkout main && git pull origin main"
|
||||
- "git checkout -b docs/{issue_number}-{brief}"
|
||||
- "编写文档到 docs/ 对应目录"
|
||||
- "git add -A && git commit -m '[moz] docs: {title}' && git push origin docs/{issue_number}-{brief}"
|
||||
- "创建 PR(body 含 Closes #{issue_number})"
|
||||
- "等 Review"
|
||||
output_template: |
|
||||
[Action Report]
|
||||
**文档路径**:{doc_path}
|
||||
**分支**:docs/{issue_number}-{brief}
|
||||
**PR**:#{pr_number}
|
||||
|
||||
refactor:
|
||||
steps:
|
||||
- "读重构目标 + 影响范围(Issue body)"
|
||||
- "git checkout main && git pull origin main"
|
||||
- "git checkout -b refactor/{issue_number}-{brief}"
|
||||
- "重构 + 确保现有测试不 break(python3 -m pytest tests/unit/ -q)"
|
||||
- "文档同步:如重构涉及架构变更,同步更新 docs/design/ 对应文档"
|
||||
- "git add -A && git commit -m '[moz] refactor: {title}' && git push origin refactor/{issue_number}-{brief}"
|
||||
- "CI 通过后创建 PR(body 含 Closes #{issue_number} + 重构内容和影响范围)"
|
||||
- "等 Review"
|
||||
output_template: |
|
||||
[Action Report]
|
||||
**重构范围**:{scope}
|
||||
**测试结果**:{test_result} passed
|
||||
**分支**:refactor/{issue_number}-{brief}
|
||||
**PR**:#{pr_number}
|
||||
**CI**:{ci_status}
|
||||
**文档同步**:{doc_status}
|
||||
|
||||
test:
|
||||
steps:
|
||||
- "读测试目标(Issue body)"
|
||||
- "git checkout main && git pull origin main"
|
||||
- "git checkout -b test/{issue_number}-{brief}"
|
||||
- "编写测试脚本到 tests/ 对应目录"
|
||||
- "运行测试验证(python3 -m pytest {test_file} -v)"
|
||||
- "git add -A && git commit -m '[moz] test: {title}' && git push origin test/{issue_number}-{brief}"
|
||||
- "创建 PR(body 含 Closes #{issue_number})"
|
||||
- "等 Review"
|
||||
output_template: |
|
||||
[Action Report]
|
||||
**测试文件**:{test_file}
|
||||
**测试结果**:{test_result}
|
||||
**分支**:test/{issue_number}-{brief}
|
||||
**PR**:#{pr_number}
|
||||
|
||||
infrastructure:
|
||||
steps:
|
||||
- "根据 Issue body 中的错误来源和日志片段排查问题"
|
||||
- "修复基础设施问题(CI runner/网络/Gitea/磁盘等)"
|
||||
- "修复后在 Issue 上 comment 说明修复方式和结果"
|
||||
- "提交 action report(POST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/comments,comment_type=action_report)"
|
||||
output_template: |
|
||||
[Action Report]
|
||||
**问题**:{problem}
|
||||
**根因**:{root_cause}
|
||||
**修复方式**:{fix}
|
||||
**验证**:{verification}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ci_failure
|
||||
# ---------------------------------------------------------------------------
|
||||
ci_failure:
|
||||
steps:
|
||||
- "查看完整 CI 日志(PR 页面或 Gitea Actions 页面)"
|
||||
- "根据 CI 日志判断失败原因类型:\n a. 代码问题(lint/test 失败)→ 修复失败的测试 → push 到原分支 → CI 自动重跑\n b. 基础设施问题(runner 环境/Python/venv/Gitea/网络故障)→ 在该仓库创建 Issue 指派 jiangwei-infra(label 必须包含 type/infrastructure)"
|
||||
- "文档同步:如修复涉及设计/接口变更,同步更新 docs/design/ 对应文档"
|
||||
- "提交 action report(POST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/comments,comment_type=action_report)— 报告中说明判断的原因类型和执行的操作,以及文档是否需要更新"
|
||||
output_template: |
|
||||
[Action Report]
|
||||
**原因类型**:{cause_type}
|
||||
**操作**:{action}
|
||||
**CI 重跑**:{ci_status}
|
||||
**文档同步**:{doc_status}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# review_result — APPROVED
|
||||
# ---------------------------------------------------------------------------
|
||||
review_result_approved:
|
||||
steps:
|
||||
- "合并 PR(Gitea API: POST /repos/{repo}/pulls/{pr_number}/merge)"
|
||||
- "提交 action report(POST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/comments,comment_type=action_report)"
|
||||
output_template: |
|
||||
[Action Report]
|
||||
**PR #{pr_number}**:merged
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# review_result — REQUEST_CHANGES
|
||||
# ---------------------------------------------------------------------------
|
||||
review_result_request_changes:
|
||||
steps:
|
||||
- "按审查意见逐条修改代码"
|
||||
- "文档同步:如审查涉及设计/接口变更,同步更新 docs/design/ 对应文档"
|
||||
- "push 到原分支 → CI 自动跑"
|
||||
- "CI 通过后等重新 Review"
|
||||
- "提交 action report(POST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/comments,comment_type=action_report)— 报告中必须说明文档是否需要更新及处理结果"
|
||||
output_template: |
|
||||
[Action Report]
|
||||
**修改内容**:{changes}
|
||||
**CI**:{ci_status}
|
||||
**文档同步**:{doc_status}
|
||||
@@ -2,7 +2,7 @@
|
||||
title: "Issue-Centric Orchestration — Gitea Issue 替代黑板 DB 协作面"
|
||||
created: 2026-06-19
|
||||
version: v2.1 draft
|
||||
status: draft
|
||||
status: superseded by §21 (toolchain 部分)
|
||||
changelog: v2.1 修正 M1(dispatcher 直接 SQL 声明)+ M2(Phase 格式)+ S1/S2(TaskAdapter 残留清理)
|
||||
v2.0 纳入姜维+司马懿 Review 反馈 + 庞统 Repository 模式修正
|
||||
v1.1 纳入姜维 Review 反馈
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
---
|
||||
title: "Unified Toolchain Design — 统一工具链工作流设计"
|
||||
created: 2026-06-20
|
||||
version: v1.0 draft
|
||||
version: v1.2 draft
|
||||
status: draft
|
||||
changelog: v1.0 初版
|
||||
changelog: v1.2 补充 §15b Comment @assignee 行为约束
|
||||
v1.1 补充 §11b Issue opened 无 assignee 处理 + 修正 §13.1 触发路径
|
||||
v1.0 初版
|
||||
---
|
||||
|
||||
# Unified Toolchain Design
|
||||
@@ -569,6 +571,125 @@ issue_closed 走 auto-pass(和 review_merged 一样),纯通知不需要 ag
|
||||
|
||||
---
|
||||
|
||||
## §11b. Issue opened 无 assignee 处理
|
||||
|
||||
### 11b.1 问题
|
||||
|
||||
`_handle_issues` 的 `action == "opened"` 分支只处理"部署失败"关键词和 @mention。无 assignee 的普通 Issue(如庞统创建的 parent Issue)被静默忽略,无法触发 discussion 流程。
|
||||
|
||||
### 11b.2 设计
|
||||
|
||||
`_handle_issues` 的 `opened` 分支新增无 assignee 处理路径:
|
||||
|
||||
```
|
||||
Gitea Issue 创建(无 assignee, 有 type/* label)
|
||||
→ webhook: issues/opened
|
||||
→ _handle_issues:
|
||||
action == "opened"
|
||||
→ 非"部署失败"
|
||||
→ 无 assignee + 有 type/* label
|
||||
→ 创建 toolchain task(assignee=None, action_type=issue_discussion)
|
||||
→ ticker 扫到 pending task
|
||||
→ router.route: assignee=None → 不走快速路径 4 → delegate 庞统
|
||||
→ 庞统收到 discussion prompt(spawner._build_discussion_prompt)
|
||||
→ 庞统在 Issue 上 comment 发起讨论,引导其他 agent 参与
|
||||
```
|
||||
|
||||
### 11b.3 `_send_toolchain_task` 改动
|
||||
|
||||
`to_agent` 参数允许 `None`:
|
||||
|
||||
```python
|
||||
def _send_toolchain_task(
|
||||
to_agent: str | None, # None = 无指派,待路由
|
||||
...
|
||||
) -> str:
|
||||
# None 不校验 AGENT_IDS(非 None 时仍然校验)
|
||||
if to_agent is not None and to_agent not in AGENT_IDS:
|
||||
logger.warning("Unknown agent: %s, skipping toolchain task", to_agent)
|
||||
return ""
|
||||
|
||||
task = Task(
|
||||
...
|
||||
assignee=to_agent, # None → 待路由
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
### 11b.4 `_handle_issues` opened 分支改动
|
||||
|
||||
```python
|
||||
elif action == "opened":
|
||||
if "部署失败" in issue_title:
|
||||
# 部署失败处理(不变)
|
||||
...
|
||||
elif not (issue.get("assignees") or issue.get("assignee")):
|
||||
# §11b: 无 assignee 的普通 Issue
|
||||
labels_list = [lbl.get("name", "")
|
||||
for lbl in (issue.get("labels") or [])]
|
||||
has_type_label = any(lbl.lower().startswith("type/") for lbl in labels_list)
|
||||
if not has_type_label:
|
||||
return # 无 type label 不处理(避免噪音)
|
||||
|
||||
title = f"Issue 讨论: {issue_title} ({repo}#{issue_number})"
|
||||
_send_toolchain_task(
|
||||
to_agent=None, # 无指派 → router delegate 庞统
|
||||
title=title,
|
||||
description=f"## Issue 需要讨论\n\n{issue.get('body', '(无描述)')}",
|
||||
event_type="issue_discussion",
|
||||
action_type="issue_discussion",
|
||||
steps=[], # discussion 不需要结构化步骤
|
||||
context_data={
|
||||
"issue_number": issue_number,
|
||||
"repo": repo,
|
||||
"issue_title": issue_title,
|
||||
"issue_body": issue.get("body", ""),
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
# @mention 检查(不变)
|
||||
...
|
||||
```
|
||||
|
||||
### 11b.5 ToolchainHandler verify 改动
|
||||
|
||||
`issue_discussion` 走 auto-pass(纯触发,不需要 agent 执行步骤):
|
||||
|
||||
```python
|
||||
if meta.get("action_type") == "issue_discussion":
|
||||
return VerifyResult(True, "discussion_passthrough",
|
||||
"issue_discussion auto-pass")
|
||||
```
|
||||
|
||||
### 11b.6 为什么用 delegate 而非 broadcast?
|
||||
|
||||
现有 ticker 有两种调度路径:
|
||||
- **deterministic**:有 assignee → 直接 spawn 给该 agent
|
||||
- **broadcast**:无 assignee → 所有空闲 agent 收到 claim prompt
|
||||
|
||||
无 assignee 的 Issue 如果走 broadcast,每个 agent 收到的是 claim prompt(认领 task),不是 discussion prompt(讨论 Issue)。而 §13.2 设计的 discussion prompt 才是正确行为——agent 应该讨论 Issue 内容,自主决定是否参与,而不是竞争认领一个 task。
|
||||
|
||||
**delegate 庞统**是更合理的路径:
|
||||
- 庞统收到 task → 读取 Issue body → 按副军师职责发起讨论
|
||||
- 庞统在 Issue 上 comment 引导其他 agent 参与
|
||||
- 其他 agent 看到 @mention 后自主创建 sub Issue
|
||||
|
||||
如果未来需要多 agent 并行讨论,可以在 ticker 中新增 discussion broadcast 路径(spawn_type=discussion)。但 MVP 阶段 delegate 庞统已足够。
|
||||
|
||||
### 11b.7 涉及改动
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `src/api/toolchain_routes.py` `_send_toolchain_task` | `to_agent` 允许 None |
|
||||
| `src/api/toolchain_routes.py` `_handle_issues` | opened 分支加无 assignee 路径 |
|
||||
| `src/daemon/toolchain_handler.py` `verify_completion` | issue_discussion auto-pass |
|
||||
| `src/daemon/toolchain_handler.py` `_ACTION_HINTS` | 新增 issue_discussion |
|
||||
| `src/daemon/toolchain_handler.py` `EVENT_LABELS_ZH` | 新增 issue_discussion |
|
||||
| `tests/` | 新增 issue_discussion 测试 |
|
||||
|
||||
---
|
||||
|
||||
## §12. AI Native 能力完整性(v2 补充)
|
||||
|
||||
> 本节确保 Gitea 替代黑板后,PRD-v3.0 的 AI native 能力不降级。
|
||||
@@ -606,12 +727,17 @@ issue_closed 走 auto-pass(和 review_merged 一样),纯通知不需要 ag
|
||||
庞统创建 parent Issue(无 assignee)后,触发 discussion:
|
||||
|
||||
```
|
||||
庞统创建 parent Issue → webhook: issues/assigned(或 ticker 发现 pending 无 assignee)
|
||||
→ daemon 检测:无 assignee = 广播讨论
|
||||
→ ticker 广播 spawn 所有 agent(spawn_type=discussion)
|
||||
→ 每个 agent 收到 DISCUSSION_PROMPT_TEMPLATE
|
||||
庞统创建 parent Issue(无 assignee)
|
||||
→ Gitea webhook: issues/opened(注意:不是 assigned,无 assignee 的 Issue 只触发 opened)
|
||||
→ _handle_issues opened 分支检测:无 assignee + 有 type/* label
|
||||
→ 创建 toolchain task(assignee=None, action_type=issue_discussion)
|
||||
→ ticker 扫到 pending task → router delegate 庞统
|
||||
→ 庞统收到 discussion prompt → 在 Issue 上 comment 发起讨论
|
||||
→ 其他 agent 看到讨论 → 自主创建 sub Issue 认领
|
||||
```
|
||||
|
||||
**关键修正(v1.1)**:原设计写 "webhook: issues/assigned",实际 Gitea 对无 assignee 的 Issue 只发 `issues/opened` 事件。`_handle_issues` 的 `opened` 分支需要新增无 assignee 处理路径(见 §11b)。
|
||||
|
||||
### 13.2 Discussion Prompt 设计(v3 重构)
|
||||
|
||||
**设计参考**:Edict(角色驱动主动发言)+ APM(自包含 Task Prompt)+ PAV 循环(输入/输出/验证)。
|
||||
@@ -954,6 +1080,59 @@ agent 写 Issue/PR comment → webhook: issue_comment/created → daemon 解析
|
||||
|
||||
---
|
||||
|
||||
## §15b. Comment @assignee 行为约束
|
||||
|
||||
### 15b.1 问题
|
||||
|
||||
`_handle_issue_comment` 只有两条通知路径(CI 关键词 + @mention)。当 agent 在 Issue/PR 上写了 comment 但没有 @ 任何人时,Issue 的 assignee/创建者收不到通知。
|
||||
|
||||
例:姜维在 Issue #114 上写了排查结论,没有 @ 庞统。庞统不知道排查已完成。
|
||||
|
||||
### 15b.2 设计决策:prompt 约束 vs 代码路径
|
||||
|
||||
| 方案 | 优点 | 缺点 |
|
||||
|------|------|------|
|
||||
| 代码路径 3(自动通知 assignee) | 不依赖 agent 行为 | 噪音——每条 comment 都通知,review 来回几轮就炸 |
|
||||
| prompt 约束(agent @assignee) | agent 自主判断,语义精确 | 依赖 agent 遵守 |
|
||||
|
||||
**选择 prompt 约束**。理由:
|
||||
1. 与 §21 设计哲学一致(agent 自主决策,不过度自动化)
|
||||
2. 噪音问题是真实的——不是每条 comment 都需要通知 assignee
|
||||
3. agent 可以根据场景判断:纯确认不 @,有结论/需关注才 @
|
||||
4. 兜底机制:round review + 庞统人工检查
|
||||
|
||||
### 15b.3 约束内容
|
||||
|
||||
在 ToolchainConstraintsSection §5「所有协作通过 Gitea 完成」中新增一条:
|
||||
|
||||
```
|
||||
### 5. 所有协作通过 Gitea 完成
|
||||
...(现有内容不变)
|
||||
- ⚠️ 在 Issue/PR 上写 comment 时,如果内容需要 Issue 的 assignee 或创建者知晓,必须在 comment 中 @对方。纯确认性回复(如"收到")不需要 @。
|
||||
```
|
||||
|
||||
同时在 `_ACTION_HINTS` 中 review_comment 的 hint 补充提示:
|
||||
```
|
||||
"review_comment": "你收到一个 Review 评论,这是一个需要你查看并响应的事件。回复时 @评论者。",
|
||||
```
|
||||
|
||||
### 15b.4 不做的事
|
||||
|
||||
- 不在 `_handle_issue_comment` 中加代码路径 3(自动通知 assignee)
|
||||
- 不做 subscribe/unsubscribe 机制
|
||||
- 不通知 Issue 创建者(只通知 assignee,assignee 是责任人)
|
||||
|
||||
### 15b.5 涉及改动
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `src/daemon/toolchain_handler.py` ToolchainConstraintsSection §5 | 加 @assignee 约束 |
|
||||
| `src/daemon/toolchain_handler.py` `_ACTION_HINTS` | review_comment hint 补充 @提示 |
|
||||
|
||||
约 +5 行,1 文件。
|
||||
|
||||
---
|
||||
|
||||
## §16. Round Review 迁移
|
||||
|
||||
### 16.1 当前实现
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
---
|
||||
title: "End-to-End Flow — 端到端任务流程设计"
|
||||
created: 2026-06-22
|
||||
version: v1.1
|
||||
status: draft
|
||||
changelog: v1.1 补充轻量路径设计(§22.6)、数据流澄清(§22.7)、修正设计原则3、统一Phase编号、Phase 1标注
|
||||
v1.0 初版
|
||||
---
|
||||
|
||||
# End-to-End Flow — 端到端任务流程设计
|
||||
|
||||
> 本文档描述一个任务从发起到结束的**完整系统行为链路**。
|
||||
> §21 各章节按功能点分章,本文档把它们串成一条端到端流程。
|
||||
> 每个 Phase 标注:触发源、daemon 函数、prompt 来源、agent 行为、留痕位置。
|
||||
|
||||
---
|
||||
|
||||
## §22.1 流程总览
|
||||
|
||||
```
|
||||
Phase 0: 庞统创建 parent Issue(无 assignee, 有 type/* label)
|
||||
↓ webhook: issues/opened
|
||||
Phase 1: Discussion 广播(所有空闲 agent)
|
||||
↓ agent 在 Gitea Issue comment 讨论 → 创建 sub Issue(assign 自己)
|
||||
Phase 2: sub Issue assigned → executor 分派
|
||||
↓ webhook: issues/assigned
|
||||
Phase 3: 编码 + PR + CI
|
||||
↓ webhook: pull_request/opened → CI 自动触发
|
||||
↓ CI 通过 → 等 Review / CI 失败 → agent 修复 → 重跑
|
||||
Phase 4: Review(司马懿)
|
||||
↓ APPROVED → 通知 agent 合并 / REQUEST_CHANGES → agent 修改 → 回 Phase 3
|
||||
Phase 5: Merge + sub Issue 自动关闭
|
||||
↓ webhook: pull_request/closed(merged)
|
||||
Phase 6: Round Review(庞统三问)
|
||||
↓ GOAL_ACHIEVED → 关闭 parent / 需要新轮 → 创建新 sub → 回 Phase 2
|
||||
Phase 7: parent Issue 关闭
|
||||
↓ webhook: issues/closed
|
||||
```
|
||||
|
||||
**核心设计原则**:
|
||||
|
||||
1. **协作面是 Gitea**(Issue/PR comment),不是黑板 DB
|
||||
2. **每个 Phase 有明确的前置条件和产出**,前一步未完成则后续不触发
|
||||
3. **webhook 和 ticker 协同**——webhook 负责事件感知和 task 创建,ticker 负责广播调度(Phase 1)和聚合检测(Phase 6)
|
||||
4. **agent 在 Gitea 留痕**(Issue comment、PR、Review),黑板 DB 只存 daemon 内部状态
|
||||
|
||||
---
|
||||
|
||||
## §22.2 阶段详解
|
||||
|
||||
### Phase 0: parent Issue 创建
|
||||
|
||||
| 维度 | 内容 |
|
||||
|------|------|
|
||||
| **触发** | 庞统(或用户)在 Gitea 创建 parent Issue |
|
||||
| **条件** | 无 assignee + 有 `type/*` label |
|
||||
| **daemon 函数** | `_handle_issues`(`toolchain_routes.py`),opened 分支 |
|
||||
| **daemon 行为** | 检测无 assignee + 有 type/* label → 创建 toolchain task(`assignee=None`, `action_type=issue_discussion`)写入 `_toolchain` DB |
|
||||
| **agent 行为** | 无(此阶段不 spawn agent) |
|
||||
| **产出** | `_toolchain` DB 中一条 pending task |
|
||||
|
||||
**webhook 流转**:
|
||||
|
||||
```
|
||||
Gitea: Issue created (no assignee, label type/feat)
|
||||
→ webhook: issues/opened
|
||||
→ daemon: _handle_issues → action="opened"
|
||||
→ 非部署失败 + 无 assignee + 有 type/* label
|
||||
→ _send_toolchain_task(to_agent=None, action_type="issue_discussion")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 1: Discussion 广播
|
||||
|
||||
| 维度 | 内容 |
|
||||
|------|------|
|
||||
| **触发** | ticker 30s 扫到 Phase 0 创建的 pending task |
|
||||
| **daemon 函数** | `ticker._dispatch_pending` → `dispatcher.decide` → `_broadcast_claim`。⚠️ 当前 broadcast 使用 claim prompt(黑板 API)而非 discussion prompt,P0 修复后改为 discussion broadcast |
|
||||
| **daemon 行为** | assignee=None → router 返回 mode=delegate → ticker 归入 broadcast_tasks → 广播给所有空闲 agent |
|
||||
| **prompt 来源(设计期望)** | `discussion prompt`(§13.2):你是谁 + 你必须做什么(4 维度)+ Gitea API + Boids 行为准则 |
|
||||
| **agent 行为(设计期望)** | 每个 agent 在 **Gitea Issue** comment(角色名开头,4 维度回应);需要参与的 agent 创建 sub Issue(Gitea API,assign 自己);在 parent Issue comment 注册 sub |
|
||||
| **产出** | parent Issue 上有所有 agent 的讨论 comment;Gitea 上有若干 sub Issue |
|
||||
|
||||
**discussion prompt 核心(§13.2)**:
|
||||
|
||||
```
|
||||
你被 spawn 来参与 Gitea Issue 讨论。
|
||||
|
||||
## 讨论主题
|
||||
{parent Issue body 全文}
|
||||
|
||||
## 你是谁
|
||||
你是 {agent_id}({display_name}),角色是 {role},能力是 {capabilities}
|
||||
|
||||
## 你必须做什么(每个 agent 必须 comment)
|
||||
1.【定位】这个需求和你有什么关系?
|
||||
2.【建议】你对实现方案有什么建议?
|
||||
3.【认领】如果你需要参与,创建 sub Issue 并在 parent comment 注册:
|
||||
POST /repos/{repo}/issues
|
||||
title: "[moz][sub][parent #{N}] 任务名"
|
||||
body: "Parent: #{N}\nDepends: #M\n## 任务\n..."
|
||||
assignees: ["{你的 agent_id}"]
|
||||
4.【风险】如果发现风险或不合理假设,直接提出
|
||||
|
||||
## Comment 格式
|
||||
[角色名] 你的观点
|
||||
|
||||
## API(Gitea)
|
||||
- 读 Issue: GET /repos/{repo}/issues/{N}
|
||||
- 写 comment: POST /repos/{repo}/issues/{N}/comments
|
||||
- 创建 sub Issue: POST /repos/{repo}/issues
|
||||
```
|
||||
|
||||
**Phase 1 的结束条件**(⚠️ 设计目标,检测逻辑待实现):
|
||||
|
||||
- 所有被广播的 agent 都已 comment(或 NO_REPLY)——daemon 需维护"已回复 agent"集合,当前无此机制
|
||||
- 至少有一个 agent 创建了 sub Issue
|
||||
- 如果没有任何 agent 创建 sub Issue → ticker 升级庞统(3 轮无 taker 机制)
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: sub Issue assigned → executor 分派
|
||||
|
||||
| 维度 | 内容 |
|
||||
|------|------|
|
||||
| **触发** | agent 在 Phase 1 创建 sub Issue 时 assign 自己 → Gitea 发 `issues/assigned` webhook |
|
||||
| **daemon 函数** | `_handle_issues`(`toolchain_routes.py`),assigned 分支 |
|
||||
| **daemon 行为** | 解析 type/* label → 确定 business_type → 从 `toolchain-templates.yaml` 获取 steps → 创建 toolchain task(`action_type=issue_assigned`)→ ticker dispatch |
|
||||
| **prompt 来源** | `ToolchainHandler.build_prompt`(通过 PromptComposer 拼装 sections) |
|
||||
| **agent 行为** | 建分支 `{type}/{sub_issue_number}-{brief}` → 编码 → 写测试 → 创建 PR(body 含 `Closes #N` + `Parent #M`) |
|
||||
| **产出** | Gitea 上有 PR + 分支有代码 |
|
||||
|
||||
**steps 来源(`config/toolchain-templates.yaml`)**:
|
||||
|
||||
```yaml
|
||||
issue_assigned:
|
||||
feat:
|
||||
steps:
|
||||
- "理解需求(Issue body)"
|
||||
- "git checkout -b feat/{issue_number}-{brief}"
|
||||
- "编码 + 写 UT"
|
||||
- "文档同步检查"
|
||||
- "git push + 创建 PR(body 含 Closes #{issue_number})"
|
||||
- "等 Review"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: 编码 + PR + CI
|
||||
|
||||
| 维度 | 内容 |
|
||||
|------|------|
|
||||
| **触发** | agent 创建 PR → webhook `pull_request/opened` → CI 自动触发 |
|
||||
| **daemon 函数(PR opened)** | `_handle_pull_request` → 创建 `review_request` task → 通知司马懿 |
|
||||
| **daemon 函数(CI)** | CI 结果通过 Gitea comment 反馈 → `_handle_issue_comment` CI 路径 |
|
||||
| **CI 通过** | 等待 Review(Phase 5) |
|
||||
| **CI 失败** | 创建 `ci_failure` task → 通知 agent 修复 |
|
||||
| **agent prompt(CI 失败时)** | `ToolchainHandler.build_prompt`,action_type=ci_failure,steps 含 CI 日志查看 + 根因判断 + 修复 |
|
||||
| **产出** | CI 通过的 PR |
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Review(司马懿)
|
||||
|
||||
| 维度 | 内容 |
|
||||
|------|------|
|
||||
| **触发** | `review_request` task 被 ticker dispatch 给司马懿 |
|
||||
| **daemon 函数** | ticker spawn 司马懿 → 司马懿通过 Review API 提交 verdict → webhook `pull_request_review` → `_handle_pull_request_review` |
|
||||
| **prompt 来源** | `ToolchainHandler.build_prompt`,action_type=review_request |
|
||||
| **Review APPROVED** | 创建 `review_result_approved` task → 通知 agent 合并 |
|
||||
| **Review REQUEST_CHANGES** | 创建 `review_result_request_changes` task → 通知 agent 修改 → agent push 同分支 → CI 重跑 → 回 Phase 3 |
|
||||
| **产出** | Gitea Review 记录 |
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: Merge + sub Issue 关闭
|
||||
|
||||
| 维度 | 内容 |
|
||||
|------|------|
|
||||
| **触发** | agent merge PR |
|
||||
| **daemon 函数** | Gitea 自动关闭 sub Issue(PR body 含 `Closes #N`)→ webhook `pull_request/closed(merged)` → `_handle_pull_request` closed 分支 |
|
||||
| **daemon 行为** | 创建 `review_merged` task → 通知 agent(纯通知);ToolchainHandler verify: auto-pass |
|
||||
| **产出** | sub Issue closed + 分支自动删除 |
|
||||
|
||||
---
|
||||
|
||||
### Phase 6: Round Review(庞统三问)
|
||||
|
||||
| 维度 | 内容 |
|
||||
|------|------|
|
||||
| **触发** | daemon 检测 parent Issue 下所有 sub Issue 终态(done/closed) |
|
||||
| **daemon 函数** | `ticker._check_round_complete` 扫描 parent/sub 映射 → 所有 sub 终态 → spawn 庞统 |
|
||||
| **prompt 来源** | `ticker._build_review_prompt`(三问框架) |
|
||||
| **agent 行为** | 庞统通过 Gitea API 读 parent Issue body + 所有 sub Issue comments/outputs → 三问评估 |
|
||||
| **GOAL_ACHIEVED** | 庞统关闭 parent Issue → Phase 7 |
|
||||
| **需要新轮** | 庞统创建新 sub Issues → 回 Phase 2 |
|
||||
| **产出** | parent Issue closed 或新一轮 sub Issues |
|
||||
|
||||
**三问框架**:
|
||||
|
||||
```
|
||||
1. Goal 还清晰吗?(是否有 goal drift)
|
||||
2. 成果物覆盖 goal 了吗?(逐条检查验收标准 + docs/design 同步确认)
|
||||
3. 下一轮需要做什么?(创建新 sub / 标记完成 / 调整方向)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 7: parent Issue 关闭
|
||||
|
||||
| 维度 | 内容 |
|
||||
|------|------|
|
||||
| **触发** | 庞统关闭 parent Issue |
|
||||
| **daemon 函数** | webhook `issues/closed` → `_handle_issues` closed 分支 |
|
||||
| **daemon 行为** | 创建 `issue_closed` task → 通知(纯通知,auto-pass) |
|
||||
| **产出** | parent Issue closed,全流程结束 |
|
||||
|
||||
---
|
||||
|
||||
## §22.3 Prompt 模板对照表
|
||||
|
||||
| Phase | prompt 用途 | 设计指定的模板来源 | 当前实际来源 | 一致? |
|
||||
|-------|-----------|----------------|------------|-------|
|
||||
| 1 Discussion | 广播讨论 | discussion prompt(§13.2,Gitea API) | claim prompt(`_build_claim_prompt`,黑板 API) | ❌ |
|
||||
| 2 Executor | 编码执行 | `ToolchainHandler.build_prompt` + YAML steps | 同设计 | ✅ |
|
||||
| 3 CI 失败 | 修复 CI | `ToolchainHandler.build_prompt` ci_failure | 同设计 | ✅ |
|
||||
| 4 Review | 审查 PR | `ToolchainHandler.build_prompt` review_request | 同设计 | ✅ |
|
||||
| 5 Merge | 合并通知 | `ToolchainHandler.build_prompt` review_merged | 同设计 | ✅ |
|
||||
| 6 Round Review | 庞统三问 | `ticker._build_review_prompt` | 同设计 | ✅ |
|
||||
| 7 Issue closed | 关闭通知 | `ToolchainHandler.build_prompt` issue_closed | 同设计 | ✅ |
|
||||
|
||||
**唯一偏差在 Phase 1**:discussion 设计了完整的 prompt(§13.2),但 ticker 从未调用它。
|
||||
|
||||
---
|
||||
|
||||
## §22.4 当前实现差距
|
||||
|
||||
| Phase | 实现状态 | 差距描述 |
|
||||
|-------|---------|---------|
|
||||
| 0 parent Issue 创建 | ✅ 已实现 | PR #113 `_handle_issues` opened 分支,无 assignee + type/* label → toolchain task |
|
||||
| 1 Discussion 广播 | ❌ **未实现** | ticker broadcast 只有 `_build_claim_prompt`(黑板 API,认领模式),没有 discussion broadcast(Gitea API,讨论模式)。`_build_discussion_prompt` 存在于 spawner 中但 ticker 从未调用。agent 收到的是"认领并执行"而不是"讨论后创建 sub" |
|
||||
| 2 sub Issue → executor | ⚠️ 部分 | assigned 路径 + YAML steps 已实现(PR #107),但因为 Phase 1 断裂,agent 不会创建 sub Issue,走不到此阶段 |
|
||||
| 3 PR + CI | ✅ 已实现 | toolchain handler 正常处理 PR opened + CI 失败 |
|
||||
| 4 Review | ✅ 已实现 | Review 请求 + Review 结果通知正常 |
|
||||
| 5 Merge + sub 关闭 | ⚠️ 部分 | merge 通知正常。但 agent 创建的 PR body 不含 `Closes #N`(因为走的是 claim prompt 不是 executor prompt,prompt 中没有 Closes #N 约束) |
|
||||
| 6 Round Review | ❌ 未适配 | `_check_round_complete` 扫黑板 `parent_task` 字段,不扫 Gitea parent/sub Issue 映射。没有 sub Issue 就检测不到 |
|
||||
| 7 parent Issue 关闭 | ✅ 已实现 | PR #113 issue_closed auto-pass |
|
||||
|
||||
---
|
||||
|
||||
## §22.5 差距优先级排序
|
||||
|
||||
| 优先级 | 差距 | 影响范围 | 修复建议 |
|
||||
|--------|------|---------|---------|
|
||||
| **P0** | Phase 1 discussion broadcast | **核心断裂**。整个 Gitea Issue 流程退化成黑板 claim 模式 | ticker `_broadcast_claim` 中判断 `action_type=issue_discussion` → 调用 discussion prompt(Gitea API 版本)而非 claim prompt |
|
||||
| **P1** | Phase 5 PR body Closes #N | sub Issue 不自动关闭 | Phase 2 executor prompt 中已有 Closes #N 约束(YAML steps),Phase 1 修好后自然解决 |
|
||||
| **P2** | Phase 6 Round Review Gitea 适配 | 无法触发庞统三问 | `_check_round_complete` 改为扫 Gitea parent/sub Issue 映射,详见 §22.7 数据流设计 |
|
||||
|
||||
**关键结论**:P0 是唯一阻断点。修好 Phase 1 discussion broadcast 后,Phase 2-5 自然走通(已实现),Phase 6-7 后续跟进。
|
||||
|
||||
---
|
||||
|
||||
## §22.6 轻量路径设计(Direct Assignment)
|
||||
|
||||
> 不是所有任务都需要 Discussion 广播。需求明确、单一执行者的任务应跳过 Phase 0/1 直接进入 Phase 2。
|
||||
|
||||
### 路径决策矩阵
|
||||
|
||||
| 条件 | 路径 | 流程 |
|
||||
|------|------|------|
|
||||
| Issue 无 assignee + type/* label | **Discussion 路径**(默认) | Phase 0 → 1 → 2 → … |
|
||||
| Issue 有 assignee | **Direct 路径** | → Phase 2(跳过 Phase 0/1) |
|
||||
| Issue 有 `flow/direct` label | **强制 Direct** | → Phase 2(即使无 assignee,daemon 自动 assign) |
|
||||
| Issue 有 `flow/discuss` label | **强制 Discussion** | Phase 0 → 1 → 2 → …(即使有 assignee) |
|
||||
|
||||
### 判断逻辑
|
||||
|
||||
```
|
||||
parent Issue 创建
|
||||
├─ 有 flow/discuss label?→ Discussion 路径(Phase 0 → 1)
|
||||
├─ 有 flow/direct label?→ Direct 路径(daemon 自动 assign → Phase 2)
|
||||
├─ 有 assignee?→ Direct 路径(→ Phase 2)
|
||||
└─ 无 assignee(默认)→ Discussion 路径(Phase 0 → 1)
|
||||
```
|
||||
|
||||
优先级:`flow/*` label > assignee > 默认行为。
|
||||
|
||||
### 适用场景
|
||||
|
||||
| 路径 | 适用场景 | 示例 |
|
||||
|------|---------|------|
|
||||
| Discussion | 需求不明确、需要多角色讨论、跨模块协作 | 新功能设计、架构变更、涉及 3+ agent 的任务 |
|
||||
| Direct | 需求明确、单一执行者、小范围改动 | Bug 修复、文档更新、配置调整、单人任务 |
|
||||
|
||||
### Discussion 路径中的降级
|
||||
|
||||
Discussion 进行中,如果所有 agent 都认为任务足够简单只涉及一个角色,任何 agent 可以在 comment 中建议:
|
||||
|
||||
```
|
||||
@pangtong-fujunshi 建议直接指派 @agent-id,理由:...
|
||||
```
|
||||
|
||||
庞统判断后创建 sub Issue 直接 assign → 该 agent 跳过讨论直接进入 Phase 2。
|
||||
|
||||
### Direct 路径跳过的 Phase
|
||||
|
||||
| Phase | 是否跳过 | 原因 |
|
||||
|-------|---------|------|
|
||||
| Phase 0(parent Issue 讨论 task) | ✅ 跳过 | 不创建 discussion task |
|
||||
| Phase 1(Discussion 广播) | ✅ 跳过 | 不广播,不打扰其他 agent |
|
||||
| Phase 2(assigned → executor) | ❌ 直接进入 | 起点 |
|
||||
|
||||
### 对现有代码的影响
|
||||
|
||||
当前 `_handle_issues` 已有两个分支:
|
||||
|
||||
- `opened`(无 assignee + type/* label)→ discussion task ← Discussion 路径 ✅
|
||||
- `assigned`(有 assignee)→ executor task ← Direct 路径 ✅
|
||||
|
||||
**轻量路径的后端已实现**。需要补充的是:
|
||||
|
||||
1. `flow/direct` 和 `flow/discuss` label 的识别逻辑(`toolchain_routes.py` `_handle_issues` opened 分支)
|
||||
2. Discussion prompt 中告知 agent 可以建议 direct assignment(降级机制)
|
||||
|
||||
---
|
||||
|
||||
## §22.7 数据流设计澄清
|
||||
|
||||
> 本文档涉及两个数据源(黑板 DB 和 Gitea Issue),parent/sub 映射的数据流必须明确。
|
||||
|
||||
### 当前状态 vs 设计目标
|
||||
|
||||
| 数据 | 当前存储 | §20/§21 设计目标 | 差距 |
|
||||
|------|---------|-----------------|------|
|
||||
| 协作面(title/body/comment) | 黑板 DB tasks/comments 表 | Gitea Issue/PR | ❌ Phase 1 断裂导致 agent 用黑板 API |
|
||||
| parent/sub 映射 | `tasks.parent_task`(黑板字段) | `task_state.parent_issue`(新表) | ❌ task_state 表不存在 |
|
||||
| 执行状态 | 黑板 DB tasks.status | task_state.status | ❌ task_state 表不存在 |
|
||||
| 成果物 | 黑板 DB outputs 表 | git commit + PR | ⚠️ 需 prompt 引导 |
|
||||
|
||||
### parent/sub Issue 映射数据流(设计)
|
||||
|
||||
```
|
||||
Agent 在 Phase 1 创建 sub Issue:
|
||||
POST /repos/{repo}/issues
|
||||
title: "[moz][sub][parent #{N}] 任务名"
|
||||
assignees: ["{agent_id}"]
|
||||
↓
|
||||
Gitea webhook: issues/opened (with assignee)
|
||||
↓
|
||||
daemon: _handle_issues → assigned 分支
|
||||
→ 解析标题 [parent #{N}] → 提取 parent_issue_number
|
||||
→ 写入 task_state (issue_number, parent_issue=N, status='pending')
|
||||
↓
|
||||
ticker: _check_round_complete
|
||||
→ SELECT DISTINCT parent_issue FROM task_state WHERE parent_issue IS NOT NULL
|
||||
→ 对每个 parent_issue: SELECT status FROM task_state WHERE parent_issue=?
|
||||
→ 全部终态 → spawn 庞统 review
|
||||
```
|
||||
|
||||
### 前置条件
|
||||
|
||||
1. **task_state 表创建**:§20 设计了 DDL 但未实现。需要先建表:
|
||||
|
||||
```sql
|
||||
CREATE TABLE task_state (
|
||||
issue_number INTEGER PRIMARY KEY,
|
||||
repo TEXT,
|
||||
parent_issue INTEGER,
|
||||
status TEXT DEFAULT 'pending',
|
||||
action_type TEXT,
|
||||
retry_count INTEGER DEFAULT 0,
|
||||
dispatch_count INTEGER DEFAULT 0,
|
||||
round_count INTEGER DEFAULT 0,
|
||||
created_at TEXT,
|
||||
updated_at TEXT
|
||||
);
|
||||
CREATE INDEX idx_task_state_parent ON task_state(parent_issue);
|
||||
```
|
||||
|
||||
2. **parent_issue 解析**:daemon 在 `_handle_issues` 中解析标题 `[parent #{N}]` 模式,写入 `task_state.parent_issue`(§21 §14b L920 已有此设计)。
|
||||
|
||||
3. **_check_round_complete 改造**:从扫 `tasks.parent_task`(黑板)改为扫 `task_state.parent_issue`(toolchain DB)(§21 §16.2 L1148 已有此设计)。
|
||||
|
||||
### 混合期处理
|
||||
|
||||
task_state 表创建前,`_check_round_complete` 仍扫 `tasks.parent_task`。两种数据可以共存:
|
||||
|
||||
- **黑板路径**(Phase 1 断裂时):agent 通过黑板 API 认领 → `tasks.parent_task` 有值
|
||||
- **Gitea 路径**(Phase 1 修复后):agent 创建 sub Issue → `task_state.parent_issue` 有值
|
||||
|
||||
`_check_round_complete` 需要同时扫两个源,直到黑板路径完全废弃。
|
||||
|
||||
### 各 Phase 数据读写汇总
|
||||
|
||||
| Phase | daemon 写 | daemon 读 | agent 读 | agent 写 |
|
||||
|-------|----------|----------|----------|----------|
|
||||
| 0 | toolchain DB: task(pending, action_type=issue_discussion) | Gitea webhook payload | — | — |
|
||||
| 1 | toolchain DB: broadcast log | toolchain DB: pending tasks | Gitea Issue body | Gitea Issue comment + sub Issue |
|
||||
| 2 | toolchain DB: task(pending, action_type=issue_assigned) | Gitea webhook payload, toolchain-templates.yaml | Gitea Issue body | git branch + PR |
|
||||
| 3 | toolchain DB: task(review_request/ci_failure) | Gitea webhook (PR opened, CI status) | Gitea PR + CI logs | git push (修复) |
|
||||
| 4 | toolchain DB: task(review_result_*) | Gitea webhook (pull_request_review) | Gitea PR diff + files | Gitea Review API |
|
||||
| 5 | toolchain DB: task(review_merged) | Gitea webhook (PR closed/merged) | — | — |
|
||||
| 6 | task_state: round_count++ | task_state: parent_issue + sub status | Gitea parent Issue body + sub Issue comments | Gitea Issue comment(三问结论)+ 关闭/创建 Issue |
|
||||
| 7 | toolchain DB: task(issue_closed) | Gitea webhook (issues/closed) | — | — |
|
||||
@@ -0,0 +1 @@
|
||||
# algorithms package
|
||||
@@ -0,0 +1,16 @@
|
||||
"""find_max_bubble — 冒泡排序法找最大值"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def find_max_bubble(nums: list[int | float]) -> int | float | None:
|
||||
"""冒泡排序后取最后一个元素,空列表返回 None。"""
|
||||
if not nums:
|
||||
return None
|
||||
arr = list(nums) # 不修改原列表
|
||||
n = len(arr)
|
||||
for i in range(n - 1):
|
||||
for j in range(n - 1 - i):
|
||||
if arr[j] > arr[j + 1]:
|
||||
arr[j], arr[j + 1] = arr[j + 1], arr[j]
|
||||
return arr[-1]
|
||||
@@ -0,0 +1,18 @@
|
||||
"""find_max_linear — 线性扫描法找最大值"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def find_max_linear(nums: list[int | float]) -> int | float | None:
|
||||
"""返回列表中的最大值,空列表返回 None。"""
|
||||
if not nums:
|
||||
return None
|
||||
result = nums[0]
|
||||
for num in nums[1:]:
|
||||
if num > result:
|
||||
result = num
|
||||
return result
|
||||
|
||||
|
||||
# 兼容别名
|
||||
find_max = find_max_linear
|
||||
@@ -0,0 +1,41 @@
|
||||
"""find_max_quickselect — 快速选择 partition 思路找最大值"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
|
||||
|
||||
def find_max_quickselect(nums: list[int | float]) -> int | float | None:
|
||||
"""使用快速选择的 partition 思路直接找最大值,空列表返回 None。"""
|
||||
if not nums:
|
||||
return None
|
||||
arr = list(nums) # 不修改原列表
|
||||
return _quickselect_max(arr, 0, len(arr) - 1)
|
||||
|
||||
|
||||
def _quickselect_max(arr: list[int | float], lo: int, hi: int) -> int | float:
|
||||
"""在 arr[lo..hi] 中找最大值。
|
||||
|
||||
Lomuto partition: < pivot 左,>= pivot 右。
|
||||
pivot 落在 store 位置,最大值在 [store, hi]。
|
||||
"""
|
||||
if lo == hi:
|
||||
return arr[lo]
|
||||
|
||||
pivot_idx = random.randint(lo, hi)
|
||||
arr[pivot_idx], arr[hi] = arr[hi], arr[pivot_idx]
|
||||
|
||||
pivot = arr[hi]
|
||||
store = lo
|
||||
for i in range(lo, hi):
|
||||
if arr[i] < pivot:
|
||||
arr[store], arr[i] = arr[i], arr[store]
|
||||
store += 1
|
||||
arr[store], arr[hi] = arr[hi], arr[store]
|
||||
|
||||
# store 是 pivot 最终位置
|
||||
# 如果 store == hi,pivot 是当前区间最大值
|
||||
if store == hi:
|
||||
return arr[store]
|
||||
# 否则在 store+1..hi 中继续找(右侧都 >= pivot)
|
||||
return _quickselect_max(arr, store + 1, hi)
|
||||
+104
-12
@@ -219,7 +219,7 @@ def _toolchain_db_path() -> Path:
|
||||
|
||||
|
||||
def _send_toolchain_task(
|
||||
to_agent: str,
|
||||
to_agent: str | None,
|
||||
title: str,
|
||||
description: str,
|
||||
event_type: str,
|
||||
@@ -231,7 +231,7 @@ def _send_toolchain_task(
|
||||
"""创建 Toolchain Task 并写入 _toolchain DB。
|
||||
|
||||
Args:
|
||||
to_agent: 收件人 Agent ID
|
||||
to_agent: 收件人 Agent ID,None 表示无指派(待路由/delegate)
|
||||
title: 任务标题
|
||||
description: 任务描述(模板渲染后的事件信息)
|
||||
event_type: 事件类型(review_result / ci_failure / ...)
|
||||
@@ -243,7 +243,7 @@ def _send_toolchain_task(
|
||||
Returns:
|
||||
创建的 Task ID
|
||||
"""
|
||||
if to_agent not in AGENT_IDS:
|
||||
if to_agent is not None and to_agent not in AGENT_IDS:
|
||||
logger.warning("Unknown agent: %s, skipping toolchain task", to_agent)
|
||||
return ""
|
||||
|
||||
@@ -1025,14 +1025,38 @@ async def _handle_issues(payload: Dict[str, Any]) -> None:
|
||||
},
|
||||
)
|
||||
else:
|
||||
title = f"Issue 指派: {issue_title} ({repo}#{issue_number})"
|
||||
_send_toolchain_task(
|
||||
to_agent=assignee,
|
||||
title=title,
|
||||
description=text,
|
||||
event_type="issue_assigned",
|
||||
action_type="issue_assigned",
|
||||
steps=[
|
||||
# §21 §5 按 type/* label 解析 business_type
|
||||
business_type = "feat" # default
|
||||
for lbl in labels_list:
|
||||
lbl_lower = lbl.lower()
|
||||
if "bug" in lbl_lower:
|
||||
business_type = "bug"
|
||||
elif "impl" in lbl_lower:
|
||||
business_type = "impl"
|
||||
elif "feat" in lbl_lower:
|
||||
business_type = "feat"
|
||||
elif "docs" in lbl_lower or "documentation" in lbl_lower:
|
||||
business_type = "docs"
|
||||
elif "refactor" in lbl_lower:
|
||||
business_type = "refactor"
|
||||
elif "test" in lbl_lower:
|
||||
business_type = "test"
|
||||
|
||||
# §21 §4 从 YAML 模板获取 steps(fallback 到硬编码)
|
||||
from src.daemon.toolchain_templates import get_steps
|
||||
yaml_steps = get_steps("issue_assigned", business_type)
|
||||
if yaml_steps:
|
||||
# 渲染占位符
|
||||
rendered_steps = []
|
||||
for s in yaml_steps:
|
||||
s = s.replace("{issue_number}", str(issue_number))
|
||||
s = s.replace("{brief}", brief)
|
||||
s = s.replace("{title}", issue_title[:30])
|
||||
rendered_steps.append(s)
|
||||
steps_to_use = rendered_steps
|
||||
else:
|
||||
# fallback: 硬编码 steps(向后兼容)
|
||||
steps_to_use = [
|
||||
f"在开发目录执行 git 操作:\n a. git checkout main && git pull origin main\n b. git checkout -b fix/{issue_number}-{brief}",
|
||||
"编码 + 写 UT",
|
||||
"文档同步:如果本次改动涉及设计变更或接口变更,在同一分支更新 docs/design/ 对应文档。如无需更新,在 action report 中说明「文档无需更新」",
|
||||
@@ -1040,7 +1064,16 @@ async def _handle_issues(payload: Dict[str, Any]) -> None:
|
||||
f"CI 通过后创建 PR(Gitea API: POST /repos/{repo}/pulls,head: fix/{issue_number}-{brief}, base: main)— PR body 必须含 Closes #{issue_number}",
|
||||
"等 Review",
|
||||
"提交 action report(POST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/comments,comment_type=action_report)— 报告中必须说明文档是否需要更新及处理结果",
|
||||
],
|
||||
]
|
||||
|
||||
title = f"Issue 指派: {issue_title} ({repo}#{issue_number})"
|
||||
_send_toolchain_task(
|
||||
to_agent=assignee,
|
||||
title=title,
|
||||
description=text,
|
||||
event_type="issue_assigned",
|
||||
action_type="issue_assigned",
|
||||
steps=steps_to_use,
|
||||
context_data={
|
||||
"issue_number": issue_number,
|
||||
"repo": repo,
|
||||
@@ -1048,10 +1081,69 @@ async def _handle_issues(payload: Dict[str, Any]) -> None:
|
||||
"labels": labels,
|
||||
"issue_body": issue_body or "(无描述)",
|
||||
"brief": brief,
|
||||
"business_type": business_type,
|
||||
},
|
||||
)
|
||||
|
||||
elif action == "closed":
|
||||
# §21 §11 Issue closed 纯通知(auto-pass)
|
||||
assignee_login = ""
|
||||
issue_assignees = issue.get("assignees") or []
|
||||
if issue_assignees:
|
||||
assignee_login = issue_assignees[-1].get("login", "")
|
||||
if not assignee_login or assignee_login not in AGENT_IDS:
|
||||
logger.debug("Issue closed but no valid assignee, skipping")
|
||||
return
|
||||
title = f"Issue 已关闭: {issue_title} ({repo}#{issue_number})"
|
||||
_send_toolchain_task(
|
||||
to_agent=assignee_login,
|
||||
title=title,
|
||||
description=f"## Issue 已关闭\n\n**{repo}#{issue_number}**: {issue_title}\n\nIssue 已被关闭。",
|
||||
event_type="issue_closed",
|
||||
action_type="issue_closed",
|
||||
steps=[], # 纯通知,无步骤
|
||||
context_data={"repo": repo, "issue_number": issue_number},
|
||||
)
|
||||
|
||||
elif action == "opened":
|
||||
# §11b: 无 assignee 的普通 Issue → discussion task
|
||||
assignees = issue.get("assignees") or []
|
||||
single_assignee = issue.get("assignee")
|
||||
if single_assignee and isinstance(single_assignee, dict) and single_assignee not in assignees:
|
||||
assignees = list(assignees) + [single_assignee]
|
||||
|
||||
if not assignees and not ("部署失败" in issue_title):
|
||||
# 无 assignee + 非部署失败 → 检查是否有 type/* label
|
||||
labels_list_opened = [
|
||||
lbl.get("name", "") for lbl in (issue.get("labels") or [])
|
||||
]
|
||||
has_type_label = any(
|
||||
lbl.lower().startswith("type/") for lbl in labels_list_opened
|
||||
)
|
||||
if has_type_label:
|
||||
issue_body = issue.get("body", "") or "(无描述)"
|
||||
title_discussion = f"Issue 讨论: {issue_title} ({repo}#{issue_number})"
|
||||
_send_toolchain_task(
|
||||
to_agent=None, # 无指派 → router delegate 庞统
|
||||
title=title_discussion,
|
||||
description=f"## Issue 需要讨论\n\n"
|
||||
f"**{repo}#{issue_number}**: {issue_title}\n\n"
|
||||
f"{issue_body}",
|
||||
event_type="issue_discussion",
|
||||
action_type="issue_discussion",
|
||||
steps=[], # discussion 不需要结构化步骤
|
||||
context_data={
|
||||
"issue_number": issue_number,
|
||||
"repo": repo,
|
||||
"issue_title": issue_title,
|
||||
"issue_body": issue_body,
|
||||
},
|
||||
)
|
||||
logger.info(
|
||||
"Issue #%s: no assignee + type label → discussion task",
|
||||
issue_number)
|
||||
# discussion task 创建后继续检查 @mention(不 return)
|
||||
|
||||
if "部署失败" in issue_title:
|
||||
# 从 Issue body 提取 commit hash(Gitea deploy workflow 格式)
|
||||
sha_match = re.search(r'[0-9a-f]{40}', issue.get("body", ""))
|
||||
|
||||
+32
-6
@@ -103,9 +103,9 @@ SPAWN_PROMPT_TEMPLATE = """{identity_section}
|
||||
"""
|
||||
|
||||
|
||||
DISCUSSION_PROMPT_TEMPLATE = """你被 spawn 来参与黑板讨论。这是一个 v2.9 四相循环的讨论环节。
|
||||
DISCUSSION_PROMPT_TEMPLATE = """你被 spawn 来参与讨论。这是一个四相循环的讨论环节。
|
||||
|
||||
## 你的任务
|
||||
## 讨论主题
|
||||
|
||||
{goal_snapshot}
|
||||
|
||||
@@ -113,14 +113,38 @@ DISCUSSION_PROMPT_TEMPLATE = """你被 spawn 来参与黑板讨论。这是一
|
||||
|
||||
{constraints}
|
||||
|
||||
## 你是谁
|
||||
|
||||
{agent_identity}
|
||||
|
||||
## 你必须做什么
|
||||
|
||||
读完需求后,在黑板 comment 回应(必须,不是可选):
|
||||
|
||||
1.【定位】这个需求和你有什么关系?你的专业能力能贡献什么?
|
||||
2.【建议】你对实现方案有什么建议?(技术选型、数据来源、实现路径)
|
||||
3.【认领】如果你需要参与,创建 sub task 并在 parent task comment 注册:
|
||||
- 创建 sub task: POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks
|
||||
body: {{"title": "...", "description": "...", "task_type": "...", "parent_task": "{task_id}", "must_haves": "{{\"capability\": \"...\"}}"}}
|
||||
- 创建后在 parent task comment: "[你的角色名] 我创建了 sub: 任务名,我负责 简述"
|
||||
4.【风险】如果你发现风险、不合理的假设、或遗漏的环节,直接提出
|
||||
|
||||
⚠️ 每个 agent 必须 comment。即使你认为和自己无关,也要说明原因——这证明你读过并思考过了。
|
||||
|
||||
## Comment 格式
|
||||
|
||||
你的 comment 必须以角色名开头,让其他人知道你是谁:
|
||||
[角色名] 你的观点
|
||||
例:[张飞] 我来负责策略编码,用 vnpy CtaTemplate 实现。
|
||||
例:[关羽] 这个策略需要风控,连亏 3 天应暂停。
|
||||
例:[赵云] 数据已就绪,2024-08 缺失已补齐。
|
||||
|
||||
## 黑板 API
|
||||
|
||||
你可以随时:
|
||||
- 读黑板:GET http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_id}?expand=all(含 comments、outputs)
|
||||
- 读黑板:GET http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_id}?expand=all
|
||||
- 写 comment:POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_id}/comments
|
||||
body: {{"author": "{agent_id}", "body": "内容(@agent-id 自动路由)"}}
|
||||
body: {{"author": "{agent_id}", "body": "内容"}}
|
||||
- 创建 sub task:POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks
|
||||
body: {{"title": "...", "description": "...", "task_type": "...", "parent_task": "{task_id}", "must_haves": "{{\"capability\": \"...\"}}"}}
|
||||
- 认领任务:POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{{sub_task_id}}/claim
|
||||
|
||||
## 行为准则
|
||||
@@ -387,12 +411,14 @@ curl -X POST http://{self.api_host}:{self.api_port}/api/projects/{project_id}/ta
|
||||
goal_snapshot = description or title
|
||||
constraints = must_haves or "(无特殊约束)"
|
||||
|
||||
agent_identity = self._inject_agent_identity(agent_id)
|
||||
return DISCUSSION_PROMPT_TEMPLATE.format(
|
||||
goal_snapshot=goal_snapshot,
|
||||
constraints=constraints,
|
||||
project_id=project_id,
|
||||
task_id=task_id,
|
||||
agent_id=agent_id,
|
||||
agent_identity=agent_identity,
|
||||
api_host=self.api_host,
|
||||
api_port=self.api_port,
|
||||
)
|
||||
|
||||
@@ -31,9 +31,11 @@ _ACTION_HINTS: Dict[str, str] = {
|
||||
"review_result": "你收到一个 Review 结果通知,这是一个需要你执行动作的事件(不是纯通知)。",
|
||||
"review_request": "你收到一个 Review 请求,这是一个需要你审查并提交 Review 的事件。",
|
||||
"review_updated": "你收到一个 PR 更新通知,这是一个需要你重新审查修改部分的事件。",
|
||||
"review_comment": "你收到一个 Review 评论,这是一个需要你查看并响应的事件。",
|
||||
"review_comment": "你收到一个 Review 评论,这是一个需要你查看并响应的事件。回复时 @评论者。",
|
||||
"ci_failure": "你收到一个 CI 失败通知,这是一个需要你修复失败测试的事件。",
|
||||
"issue_assigned": "你收到一个 Issue 指派,这是一个需要你编码实现的事件。",
|
||||
"issue_closed": "你收到一个 Issue 关闭通知。这是一条纯通知,阅读即可。",
|
||||
"issue_discussion": "你收到一个需要讨论的 Issue。请阅读 Issue 内容,发起讨论并引导其他 agent 参与。",
|
||||
"deploy_failure": "你收到一个部署失败通知,这是一个需要你排查并修复的事件。",
|
||||
"mention": "你收到一个 @mention 通知,这是一个需要你按指引响应的事件。",
|
||||
"review_merged": "你收到一个 PR 合并通知。这是一条纯通知,阅读即可。",
|
||||
@@ -247,6 +249,7 @@ class ToolchainConstraintsSection:
|
||||
'- 如果遇到问题需要其他角色支持,在关联的 PR/Issue 上创建 comment @对方',
|
||||
'- 不要使用 Mail API(飞鸽传书)发送消息',
|
||||
'- 你的所有操作都在 toolchain 流程内,通过 Gitea 留痕',
|
||||
'- ⚠️ 在 Issue/PR 上写 comment 时,如果内容需要 Issue 的 assignee 或创建者知晓,必须在 comment 中 @对方。纯确认性回复(如"收到")不需要 @。',
|
||||
"",
|
||||
"### 6. 文档同步(涉及代码改动时)",
|
||||
'- 改了实现 → 检查 docs/design/ 对应设计文档是否需要更新',
|
||||
@@ -264,7 +267,7 @@ class ToolchainConstraintsSection:
|
||||
'| “步骤太多了,选几个做就行” | ❌ 错!必须逐条执行,不可跳过 |',
|
||||
'| “这个步骤不适用于当前情况” | ❌ 如果确实不适用,在 action report 中说明原因,但其他步骤必须执行 |',
|
||||
'| “CI/部署失败不是我代码的问题,我什么也不用做” | ❌ 错!即使是基础设施问题,你也必须创建 Issue 指派 jiangwei-infra(body 含错误来源链接 + 日志 + 判断依据),并在 action report 中说明。不能只报告“不是我的问题”就完事 |',
|
||||
'| "文档以后再说" | ❌ 错!文档同步和代码改动在同一 PR 中完成,action report 中必须说明文档处理情况 |',
|
||||
'| “文档以后再说” | ❌ 错!文档同步和代码改动在同一 PR 中完成,action report 中必须说明文档处理情况 |',
|
||||
"",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
@@ -331,6 +334,16 @@ class ToolchainHandler(BaseTaskHandler):
|
||||
return VerifyResult(True, "merged_passthrough",
|
||||
"review_merged auto-pass")
|
||||
|
||||
# 特殊处理:issue_closed 始终通过(纯通知, §21 §11)
|
||||
if meta.get("action_type") == "issue_closed":
|
||||
return VerifyResult(True, "issue_closed_passthrough",
|
||||
"issue_closed auto-pass")
|
||||
|
||||
# 特殊处理:issue_discussion 始终通过(触发 delegate 庞统 §21 §11b)
|
||||
if meta.get("action_type") == "issue_discussion":
|
||||
return VerifyResult(True, "discussion_passthrough",
|
||||
"issue_discussion auto-pass")
|
||||
|
||||
# 1. 优先检查 action_report comment
|
||||
report_row = conn.execute(
|
||||
"SELECT id FROM comments WHERE task_id=? "
|
||||
|
||||
@@ -8,7 +8,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -87,3 +87,65 @@ def render_template(name: str, variables: Dict[str, str]) -> str:
|
||||
def clear_cache() -> None:
|
||||
"""清空模板缓存(用于测试或热更新)"""
|
||||
_template_cache.clear()
|
||||
global _steps_cache
|
||||
_steps_cache = None # 重置为 None,强制下次 reload
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# §21 §4.2 YAML steps 模板加载
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
STEPS_YAML_PATH = Path(__file__).parent.parent.parent / "config" / "toolchain-templates.yaml"
|
||||
|
||||
_steps_cache: Optional[dict] = None
|
||||
|
||||
|
||||
def _load_steps_yaml() -> dict:
|
||||
"""加载并缓存 toolchain-templates.yaml。"""
|
||||
global _steps_cache
|
||||
if _steps_cache is not None:
|
||||
return _steps_cache
|
||||
try:
|
||||
import yaml
|
||||
with open(STEPS_YAML_PATH, encoding="utf-8") as f:
|
||||
_steps_cache = yaml.safe_load(f) or {}
|
||||
logger.debug("Loaded steps YAML: %d action_types", len(_steps_cache))
|
||||
except FileNotFoundError:
|
||||
logger.warning("Steps YAML not found: %s", STEPS_YAML_PATH)
|
||||
_steps_cache = {}
|
||||
except Exception as e:
|
||||
logger.error("Failed to load steps YAML: %s", e)
|
||||
_steps_cache = {}
|
||||
return _steps_cache
|
||||
|
||||
|
||||
def get_steps(action_type: str, business_type: str = "") -> List[str]:
|
||||
"""从 YAML 模板配置获取 steps。
|
||||
|
||||
Args:
|
||||
action_type: 动作类型(issue_assigned / ci_failure / ...)
|
||||
business_type: 业务子类型(feature/impl/bug/docs/refactor/test/infrastructure)
|
||||
|
||||
Returns:
|
||||
steps 列表,找不到返回空列表
|
||||
"""
|
||||
templates = _load_steps_yaml()
|
||||
section = templates.get(action_type, {})
|
||||
if isinstance(section, dict) and business_type:
|
||||
subsection = section.get(business_type, {})
|
||||
return subsection.get("steps", [])
|
||||
if isinstance(section, dict):
|
||||
return section.get("steps", [])
|
||||
return []
|
||||
|
||||
|
||||
def get_output_template(action_type: str, business_type: str = "") -> str:
|
||||
"""从 YAML 模板配置获取 output_template。"""
|
||||
templates = _load_steps_yaml()
|
||||
section = templates.get(action_type, {})
|
||||
if isinstance(section, dict) and business_type:
|
||||
subsection = section.get(business_type, {})
|
||||
return subsection.get("output_template", "")
|
||||
if isinstance(section, dict):
|
||||
return section.get("output_template", "")
|
||||
return ""
|
||||
|
||||
@@ -6,8 +6,14 @@
|
||||
需要 RUN_INTEGRATION=1 + 生产 daemon 运行。
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
if not os.environ.get("RUN_INTEGRATION"):
|
||||
pytest.skip("E2E tests require RUN_INTEGRATION=1", allow_module_level=True)
|
||||
|
||||
import json
|
||||
import re
|
||||
import sqlite3
|
||||
import sys
|
||||
@@ -17,7 +23,6 @@ from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
import pytest
|
||||
import requests as http_requests
|
||||
|
||||
# 指向部署目录
|
||||
|
||||
@@ -4,15 +4,20 @@
|
||||
需要 RUN_INTEGRATION=1 + 生产 daemon 运行。
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
if not os.environ.get("RUN_INTEGRATION"):
|
||||
pytest.skip("E2E tests require RUN_INTEGRATION=1", allow_module_level=True)
|
||||
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import pytest
|
||||
import requests as http_requests
|
||||
|
||||
# 指向部署目录
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import pytest
|
||||
import os
|
||||
|
||||
skip_no_integration = pytest.mark.skipif(
|
||||
not __import__("os").environ.get("RUN_INTEGRATION"),
|
||||
reason="Set RUN_INTEGRATION=1 to run E2E tests against real daemon",
|
||||
)
|
||||
if not os.environ.get("RUN_INTEGRATION"):
|
||||
pytest.skip("E2E tests require RUN_INTEGRATION=1", allow_module_level=True)
|
||||
|
||||
pytestmark = [pytest.mark.e2e, skip_no_integration]
|
||||
pytestmark = pytest.mark.e2e
|
||||
|
||||
"""v2.7 端到端测试 — 全链路真实环境
|
||||
|
||||
@@ -14,7 +13,6 @@ pytestmark = [pytest.mark.e2e, skip_no_integration]
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
@@ -22,7 +20,6 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import pytest
|
||||
import os
|
||||
|
||||
if not os.environ.get("RUN_INTEGRATION"):
|
||||
pytest.skip("E2E tests require RUN_INTEGRATION=1", allow_module_level=True)
|
||||
|
||||
pytestmark = pytest.mark.e2e
|
||||
|
||||
skip_no_integration = pytest.mark.skipif(
|
||||
not __import__("os").environ.get("RUN_INTEGRATION"),
|
||||
reason="Set RUN_INTEGRATION=1 to run E2E tests against real daemon",
|
||||
)
|
||||
|
||||
"""v3.1 端到端测试 — 新增场景覆盖
|
||||
|
||||
覆盖 v3.1 新增功能:
|
||||
@@ -22,7 +21,6 @@ skip_no_integration = pytest.mark.skipif(
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import time
|
||||
@@ -31,7 +29,6 @@ from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import pytest
|
||||
import requests as http_requests
|
||||
|
||||
# ── 路径设置 ──
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import os
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
pytestmark = [pytest.mark.e2e, pytest.mark.skipif(
|
||||
not os.environ.get("RUN_INTEGRATION"),
|
||||
reason="Set RUN_INTEGRATION=1 to run E2E tests",
|
||||
)]
|
||||
if not os.environ.get("RUN_INTEGRATION"):
|
||||
pytest.skip("E2E tests require RUN_INTEGRATION=1", allow_module_level=True)
|
||||
|
||||
pytestmark = [pytest.mark.e2e]
|
||||
|
||||
"""#01 四相循环 单元测试
|
||||
|
||||
@@ -23,13 +24,10 @@ pytestmark = [pytest.mark.e2e, pytest.mark.skipif(
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
# ── 路径设置 ──
|
||||
DEPLOY_DIR = Path.home() / ".sanguo_projects" / "sanguo_moziplus_v2"
|
||||
SRC_DIR = DEPLOY_DIR / "src"
|
||||
|
||||
@@ -6,14 +6,19 @@
|
||||
覆盖:项目管理 → Task CRUD → SubTask → Stage 进度 → 父状态聚合 → 依赖链 → 超时 → Mail
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
if not os.environ.get("RUN_INTEGRATION"):
|
||||
pytest.skip("Integration tests require RUN_INTEGRATION=1", allow_module_level=True)
|
||||
|
||||
import json
|
||||
import sys
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""find_max 单元测试"""
|
||||
|
||||
import pytest
|
||||
|
||||
from src.algorithms.find_max_linear import find_max
|
||||
|
||||
|
||||
class TestFindMax:
|
||||
def test_normal_list(self):
|
||||
assert find_max([3, 1, 4, 1, 5, 9, 2, 6]) == 9
|
||||
|
||||
def test_empty_list(self):
|
||||
assert find_max([]) is None
|
||||
|
||||
def test_single_element(self):
|
||||
assert find_max([42]) == 42
|
||||
|
||||
def test_negative_numbers(self):
|
||||
assert find_max([-5, -1, -10, -3]) == -1
|
||||
|
||||
def test_floats(self):
|
||||
assert find_max([1.5, 2.7, 0.3, 3.14]) == 3.14
|
||||
|
||||
def test_mixed_int_float(self):
|
||||
assert find_max([1, 2.5, 3, 0.1]) == 3
|
||||
|
||||
def test_duplicate_max(self):
|
||||
assert find_max([7, 7, 7]) == 7
|
||||
@@ -0,0 +1,63 @@
|
||||
"""三种排序算法找最大值 — 单元测试"""
|
||||
|
||||
import pytest
|
||||
|
||||
from src.algorithms.find_max_linear import find_max_linear
|
||||
from src.algorithms.find_max_bubble import find_max_bubble
|
||||
from src.algorithms.find_max_quickselect import find_max_quickselect
|
||||
|
||||
ALL_ALGOS = [find_max_linear, find_max_bubble, find_max_quickselect]
|
||||
|
||||
|
||||
# ── 通用测试(每种算法都跑) ──
|
||||
|
||||
@pytest.mark.parametrize("algo", ALL_ALGOS, ids=["linear", "bubble", "quickselect"])
|
||||
class TestAllAlgorithms:
|
||||
def test_normal_list(self, algo):
|
||||
assert algo([3, 1, 4, 1, 5, 9, 2, 6]) == 9
|
||||
|
||||
def test_empty_list(self, algo):
|
||||
assert algo([]) is None
|
||||
|
||||
def test_single_element(self, algo):
|
||||
assert algo([42]) == 42
|
||||
|
||||
def test_negative_numbers(self, algo):
|
||||
assert algo([-5, -1, -10, -3]) == -1
|
||||
|
||||
def test_floats(self, algo):
|
||||
assert algo([1.5, 2.7, 0.3, 3.14]) == 3.14
|
||||
|
||||
def test_mixed_int_float(self, algo):
|
||||
assert algo([1, 2.5, 3, 0.1]) == 3
|
||||
|
||||
def test_duplicate_max(self, algo):
|
||||
assert algo([7, 7, 7]) == 7
|
||||
|
||||
|
||||
# ── 一致性测试(同输入三法结果相同) ──
|
||||
|
||||
class TestConsistency:
|
||||
@pytest.mark.parametrize("nums", [
|
||||
[3, 1, 4, 1, 5, 9, 2, 6],
|
||||
[-5, -1, -10, -3],
|
||||
[1.5, 2.7, 0.3, 3.14],
|
||||
[42],
|
||||
[7, 7, 7],
|
||||
[0, -0.0, 0.0],
|
||||
[100, 200, 50, 150, 75],
|
||||
])
|
||||
def test_three_algorithms_same_result(self, nums):
|
||||
results = [algo(nums) for algo in ALL_ALGOS]
|
||||
assert all(r == results[0] for r in results), f"Inconsistent: {results}"
|
||||
|
||||
def test_empty_consistency(self):
|
||||
results = [algo([]) for algo in ALL_ALGOS]
|
||||
assert all(r is None for r in results)
|
||||
|
||||
def test_does_not_mutate_input(self):
|
||||
original = [3, 1, 4, 1, 5, 9, 2, 6]
|
||||
for algo in ALL_ALGOS:
|
||||
nums = list(original)
|
||||
algo(nums)
|
||||
assert nums == original, f"{algo.__name__} mutated input"
|
||||
Reference in New Issue
Block a user