From 0c736fbee2a9e76ceb83a46ecfc86b168ce66309 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Mon, 8 Jun 2026 21:59:26 +0800 Subject: [PATCH 01/39] auto-sync: 2026-06-08 21:59:26 --- docs/design/13-toolchain-and-dev-workflow.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/design/13-toolchain-and-dev-workflow.md b/docs/design/13-toolchain-and-dev-workflow.md index e64b2d0..dc874b2 100644 --- a/docs/design/13-toolchain-and-dev-workflow.md +++ b/docs/design/13-toolchain-and-dev-workflow.md @@ -2713,10 +2713,10 @@ Gitea v1.23.4 自带完整的 CI 管理界面: | # | 条件 | 状态 | 谁确认 | |---|------|------|--------| | 1 | act-runner 已注册且 label = `macos-arm64` | ✅ PM2 托管(sanguo-act-runner, id=44),崩溃自动重启 | 姜维确认 | -| 2 | Gitea repository secrets 已配置(CI_TOKEN) | ⚠️ 需确认 | 姜维 | +| 2 | Gitea repository secrets 已配置(CI_TOKEN) | ✅ 姜维确认(sanguo/moziplus-v2 已配 CI_TOKEN) | 姜维 | | 3 | Gitea 组织级 Webhook 已启用(Hook ID=28) | ✅ 已确认 | 已确认 | -| 4 | 各 Agent 的 GITEA_TOKEN 环境变量 | ⚠️ 待分配 | 庞统协调 | -| 5 | main 分支保护规则(Review 才能 merge) | ⚠️ 需确认 | 姜维 | +| 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。 -- 2.45.4 From 64755aaf8ec6bcbce27e32ebcf1c41f20da8f2b3 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Mon, 8 Jun 2026 22:04:07 +0800 Subject: [PATCH 02/39] auto-sync: 2026-06-08 22:04:07 --- src/main.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main.py b/src/main.py index 88c04f2..5754acc 100644 --- a/src/main.py +++ b/src/main.py @@ -268,6 +268,17 @@ app.include_router(sse_router) app.include_router(mail_router) app.include_router(toolchain_router) +# --------------------------------------------------------------------------- +# 健康检查端点 +# --------------------------------------------------------------------------- + + +@app.get("/api/healthz") +async def healthz(): + """轻量级健康检查,无需认证""" + return {"status": "ok"} + + # --------------------------------------------------------------------------- # 兼容端点 # --------------------------------------------------------------------------- -- 2.45.4 From dd5ee7bea3aaa6448cc22f46663814fd8d690f66 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Mon, 8 Jun 2026 22:11:11 +0800 Subject: [PATCH 03/39] auto-sync: 2026-06-08 22:11:11 --- src/api/toolchain_routes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/api/toolchain_routes.py b/src/api/toolchain_routes.py index f8e96da..930efb5 100644 --- a/src/api/toolchain_routes.py +++ b/src/api/toolchain_routes.py @@ -401,6 +401,9 @@ async def _handle_issue_comment(payload: Dict[str, Any]) -> None: _EVENT_HANDLERS: Dict[str, Any] = { "pull_request": _handle_pull_request, "pull_request_review": _handle_pull_request_review, + "pull_request_review_approved": _handle_pull_request_review, + "pull_request_review_rejected": _handle_pull_request_review, + "pull_request_review_comment": _handle_pull_request_review, "issues": _handle_issues, "issue_comment": _handle_issue_comment, } -- 2.45.4 From 6d6950807133961d28e1349d60cb91b93ef928f3 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Mon, 8 Jun 2026 22:26:47 +0800 Subject: [PATCH 04/39] auto-sync: 2026-06-08 22:26:47 --- src/api/toolchain_routes.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/api/toolchain_routes.py b/src/api/toolchain_routes.py index 930efb5..774af5b 100644 --- a/src/api/toolchain_routes.py +++ b/src/api/toolchain_routes.py @@ -372,6 +372,12 @@ async def _handle_issue_comment(payload: Dict[str, Any]) -> None: if not issue or not isinstance(issue, dict): logger.warning("issue_comment event missing issue field, skipping") return + + # 已关闭的 Issue/PR 不再发送 CI 失败通知 + if issue.get("state") == "closed": + logger.debug("Skipping CI failure notification for closed issue #%s", issue.get("number")) + return + repo = _repo_fullname(payload) issue_number = issue.get("number", 0) -- 2.45.4 From 24e47c69680240f92d716b55705a97f9a5780ca8 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Mon, 8 Jun 2026 22:58:35 +0800 Subject: [PATCH 05/39] auto-sync: 2026-06-08 22:58:35 --- src/api/toolchain_routes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/api/toolchain_routes.py b/src/api/toolchain_routes.py index 774af5b..bb5ad07 100644 --- a/src/api/toolchain_routes.py +++ b/src/api/toolchain_routes.py @@ -457,9 +457,10 @@ async def gitea_webhook( return Response(status_code=200, content="invalid payload") # 4. 查找 handler + logger.info("Webhook received: event=%s delivery=%s", x_gitea_event, x_gitea_delivery) handler = _EVENT_HANDLERS.get(x_gitea_event or "") if not handler: - logger.debug("Unhandled event type: %s", x_gitea_event) + logger.info("Unhandled event type: %s", x_gitea_event) return Response(status_code=200, content=f"unhandled event: {x_gitea_event}") # 5. 执行 handler -- 2.45.4 From fdda305c3d19dd0bac15c8024e7f5001c2ad2781 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Mon, 8 Jun 2026 23:19:23 +0800 Subject: [PATCH 06/39] auto-sync: 2026-06-08 23:19:23 --- src/api/toolchain_routes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/toolchain_routes.py b/src/api/toolchain_routes.py index bb5ad07..059f889 100644 --- a/src/api/toolchain_routes.py +++ b/src/api/toolchain_routes.py @@ -457,10 +457,10 @@ async def gitea_webhook( return Response(status_code=200, content="invalid payload") # 4. 查找 handler - logger.info("Webhook received: event=%s delivery=%s", x_gitea_event, x_gitea_delivery) + print(f"[WEBHOOK_DEBUG] event={x_gitea_event} delivery={x_gitea_delivery} payload_keys={list(payload.keys()) if isinstance(payload, dict) else 'not_dict'}", flush=True) handler = _EVENT_HANDLERS.get(x_gitea_event or "") if not handler: - logger.info("Unhandled event type: %s", x_gitea_event) + print(f"[WEBHOOK_DEBUG] UNHANDLED event={x_gitea_event}", flush=True) return Response(status_code=200, content=f"unhandled event: {x_gitea_event}") # 5. 执行 handler -- 2.45.4 From 137fa01b3d2ef21852d0372e8daa1d306b96c849 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Mon, 8 Jun 2026 23:20:47 +0800 Subject: [PATCH 07/39] auto-sync: 2026-06-08 23:20:47 --- src/api/toolchain_routes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/api/toolchain_routes.py b/src/api/toolchain_routes.py index 059f889..c463f40 100644 --- a/src/api/toolchain_routes.py +++ b/src/api/toolchain_routes.py @@ -410,6 +410,9 @@ _EVENT_HANDLERS: Dict[str, Any] = { "pull_request_review_approved": _handle_pull_request_review, "pull_request_review_rejected": _handle_pull_request_review, "pull_request_review_comment": _handle_pull_request_review, + # Gitea v1.23.4 实际发出的 review 子事件(无 _review_ 中间段) + "pull_request_approved": _handle_pull_request_review, + "pull_request_rejected": _handle_pull_request_review, "issues": _handle_issues, "issue_comment": _handle_issue_comment, } -- 2.45.4 From c3781284c1d973326e8e3119a873d001a6b7d9e8 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Mon, 8 Jun 2026 23:21:31 +0800 Subject: [PATCH 08/39] auto-sync: 2026-06-08 23:21:31 --- src/api/toolchain_routes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/api/toolchain_routes.py b/src/api/toolchain_routes.py index c463f40..864f3e2 100644 --- a/src/api/toolchain_routes.py +++ b/src/api/toolchain_routes.py @@ -468,6 +468,8 @@ async def gitea_webhook( # 5. 执行 handler try: + if x_gitea_event and 'approved' in x_gitea_event or 'rejected' in x_gitea_event: + print(f"[WEBHOOK_DEBUG] Calling review handler, review={payload.get('review',{})}", flush=True) await handler(payload) except Exception: logger.exception("Mail creation failed for %s event", x_gitea_event) -- 2.45.4 From 5d508944308329a36c7b905e05e98dec46dadcfd Mon Sep 17 00:00:00 2001 From: cfdaily Date: Mon, 8 Jun 2026 23:22:36 +0800 Subject: [PATCH 09/39] auto-sync: 2026-06-08 23:22:36 --- src/api/toolchain_routes.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/api/toolchain_routes.py b/src/api/toolchain_routes.py index 864f3e2..f54ac4d 100644 --- a/src/api/toolchain_routes.py +++ b/src/api/toolchain_routes.py @@ -258,7 +258,12 @@ async def _handle_pull_request(payload: Dict[str, Any]) -> None: async def _handle_pull_request_review(payload: Dict[str, Any]) -> None: - """处理 pull_request_review 事件:非 COMMENTED → 通知 PR 作者。""" + """处理 pull_request_review 事件:非 COMMENTED → 通知 PR 作者。 + + 支持两种 payload 格式: + - repo webhook: review.state = "APPROVED" / "REQUEST_CHANGES" + - org webhook (Gitea v1.23.4): review.type = "pull_request_review_approved" / "pull_request_review_rejected" + """ review = payload.get("review") if not review or not isinstance(review, dict): logger.warning("pull_request_review event missing review field, skipping") @@ -267,7 +272,18 @@ async def _handle_pull_request_review(payload: Dict[str, Any]) -> None: if not pr or not isinstance(pr, dict): logger.warning("pull_request_review event missing pull_request field, skipping") return + + # 兼容两种 payload 格式提取 state state = review.get("state", "") + if not state: + # org webhook 格式:review.type = "pull_request_review_approved" + review_type = review.get("type", "") + type_map = { + "pull_request_review_approved": "APPROVED", + "pull_request_review_rejected": "REQUEST_CHANGES", + "pull_request_review_comment": "COMMENTED", + } + state = type_map.get(review_type, "") # 只通知 APPROVED 和 REQUEST_CHANGES,跳过 COMMENTED 和其他状态 if state == "COMMENTED": @@ -277,8 +293,9 @@ async def _handle_pull_request_review(payload: Dict[str, Any]) -> None: pr_number = pr.get("number", 0) pr_title = pr.get("title", "") pr_author = pr.get("user", {}).get("login", "unknown") - reviewer = review.get("user", {}).get("login", "unknown") - review_body = review.get("body", "(无评论)") + # 兼容:org webhook 的 review 没有 user,从 sender 取 + reviewer = review.get("user", {}).get("login", "") or payload.get("sender", {}).get("login", "unknown") + review_body = review.get("body", "") or review.get("content", "(无评论)") result_map = {"APPROVED": "通过 ✓", "REQUEST_CHANGES": "驳回 ✗"} if state not in result_map: -- 2.45.4 From 91d53f27719332292f539436de3a00826cc33b28 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Mon, 8 Jun 2026 23:23:43 +0800 Subject: [PATCH 10/39] auto-sync: 2026-06-08 23:23:43 --- src/api/toolchain_routes.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/api/toolchain_routes.py b/src/api/toolchain_routes.py index f54ac4d..c0f3abe 100644 --- a/src/api/toolchain_routes.py +++ b/src/api/toolchain_routes.py @@ -477,16 +477,13 @@ async def gitea_webhook( return Response(status_code=200, content="invalid payload") # 4. 查找 handler - print(f"[WEBHOOK_DEBUG] event={x_gitea_event} delivery={x_gitea_delivery} payload_keys={list(payload.keys()) if isinstance(payload, dict) else 'not_dict'}", flush=True) handler = _EVENT_HANDLERS.get(x_gitea_event or "") if not handler: - print(f"[WEBHOOK_DEBUG] UNHANDLED event={x_gitea_event}", flush=True) + logger.debug("Unhandled event type: %s", x_gitea_event) return Response(status_code=200, content=f"unhandled event: {x_gitea_event}") # 5. 执行 handler try: - if x_gitea_event and 'approved' in x_gitea_event or 'rejected' in x_gitea_event: - print(f"[WEBHOOK_DEBUG] Calling review handler, review={payload.get('review',{})}", flush=True) await handler(payload) except Exception: logger.exception("Mail creation failed for %s event", x_gitea_event) -- 2.45.4 From b889082a0bc9b9c470f552850e5b088c7acddf9e Mon Sep 17 00:00:00 2001 From: cfdaily Date: Mon, 8 Jun 2026 23:37:25 +0800 Subject: [PATCH 11/39] auto-sync: 2026-06-08 23:37:25 --- docs/test-guide.md | 7 ++++--- pyproject.toml | 4 +++- tests/conftest.py | 8 ++++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/test-guide.md b/docs/test-guide.md index 5401e36..2dae11f 100644 --- a/docs/test-guide.md +++ b/docs/test-guide.md @@ -11,9 +11,10 @@ | 场景 | 命令 | 耗时 | 说明 | |------|------|------|------| | **改了某个模块** | `pytest tests/unit/test_spawner.py` | <5s | 只跑改动的模块对应的单元测试 | -| **改了 API 层** | `pytest tests/integration/` | ~1min | 跑全部集成测试 | -| **提交前快速验证** | `pytest -m "not e2e"` | ~2min | 不跑 E2E,验证不破坏现有功能 | -| **部署前全量验证** | `RUN_INTEGRATION=1 pytest` | ~60min | 含 E2E,真实 Agent | +| **改了 API 层** | `RUN_INTEGRATION=1 pytest tests/integration/` | ~1min | 跑全部集成测试 | +| **提交前快速验证** | `pytest` | ~2min | 默认排除 integration 和 e2e | +| **含集成测试** | `RUN_INTEGRATION=1 pytest` | ~5min | 包含 integration 测试 | +| **部署前全量验证** | `RUN_INTEGRATION=1 pytest` | ~60min | 含 e2e,真实 Agent | | **只跑 E2E 场景** | `RUN_INTEGRATION=1 pytest tests/e2e/test_e2e_scenarios.py` | ~30min | 串行,一个跑完再下一个 | | **只跑 E2E 压力** | `RUN_INTEGRATION=1 pytest tests/e2e/test_e2e_stress.py` | ~10min | 并发测试 | diff --git a/pyproject.toml b/pyproject.toml index 801e0a4..61c214d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,8 +8,10 @@ requires-python = ">=3.9" asyncio_mode = "auto" testpaths = ["tests"] markers = [ - "integration: real agent tests (requires RUN_INTEGRATION=1)", + "integration: integration tests (requires RUN_INTEGRATION=1)", + "e2e: end-to-end tests with real daemon + Agent (requires RUN_INTEGRATION=1)", ] +addopts = "-m 'not integration and not e2e'" [tool.pyright] venvPath = "." diff --git a/tests/conftest.py b/tests/conftest.py index a1e4aca..ba0bd54 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -55,6 +55,14 @@ def client_with_isolation(isolated_data_root): # ── E2E gate ── +def pytest_collection_modifyitems(config, items): + if not os.environ.get("RUN_INTEGRATION"): + skip = pytest.mark.skip(reason="needs RUN_INTEGRATION=1") + for item in items: + if "integration" in item.keywords or "e2e" in item.keywords: + item.add_marker(skip) + + skip_no_integration = pytest.mark.skipif( not os.environ.get("RUN_INTEGRATION"), reason="Set RUN_INTEGRATION=1 to run E2E tests against real daemon", -- 2.45.4 From 8c6782a7ad6849b77e696d5c107747b534c986c7 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Mon, 8 Jun 2026 23:37:35 +0800 Subject: [PATCH 12/39] auto-sync: 2026-06-08 23:37:35 (catch-all) --- docs/test-guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/test-guide.md b/docs/test-guide.md index 2dae11f..1ba1a5d 100644 --- a/docs/test-guide.md +++ b/docs/test-guide.md @@ -102,7 +102,7 @@ E2E(慢,真实 Agent) → 验证完整链路,需要 RUN_INTEGRATION=1 ## 关键规则 1. **只有 E2E 会 spawn 真实 Agent**,单元和集成不会 -2. **不带 `RUN_INTEGRATION=1` 跑 `pytest` 是安全的**,E2E 全部 skip +2. **直接跑 `pytest` 是安全的**,integration 和 e2e 全部被排除(需 `RUN_INTEGRATION=1` 才跑) 3. **E2E 场景测试串行**,一个完成再下一个,失败要分析根因再继续 4. **E2E 压力测试并行**,场景测试全通过后再跑 5. **测试数据用 `e2e-` 前缀**,atexit 兜底清理,手动清理见上方 -- 2.45.4 From ec2bd9b3ecb3730b333eb11d94bbbb48aebe2701 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Mon, 8 Jun 2026 23:38:34 +0800 Subject: [PATCH 13/39] auto-sync: 2026-06-08 23:38:34 (catch-all) --- pyproject.toml | 2 +- tests/conftest.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 61c214d..47b40c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ markers = [ "integration: integration tests (requires RUN_INTEGRATION=1)", "e2e: end-to-end tests with real daemon + Agent (requires RUN_INTEGRATION=1)", ] -addopts = "-m 'not integration and not e2e'" +addopts = "-m not integration and not e2e" [tool.pyright] venvPath = "." diff --git a/tests/conftest.py b/tests/conftest.py index ba0bd54..1eca4f9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,6 +21,10 @@ def pytest_configure(config): for name, desc in markers.items(): config.addinivalue_line("markers", f"{name}: {desc}") + # When RUN_INTEGRATION=1, remove default marker filter so integration/e2e tests run + if os.environ.get("RUN_INTEGRATION"): + config.known_args_namespace.markexpr = "" + @pytest.fixture def isolated_data_root(tmp_path): -- 2.45.4 From 89888a5e519fbd8bd7054a00d8df2ef01a4cefaf Mon Sep 17 00:00:00 2001 From: cfdaily Date: Mon, 8 Jun 2026 23:38:59 +0800 Subject: [PATCH 14/39] auto-sync: 2026-06-08 23:38:59 (catch-all) --- pyproject.toml | 2 +- tests/conftest.py | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 47b40c4..34e4063 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ markers = [ "integration: integration tests (requires RUN_INTEGRATION=1)", "e2e: end-to-end tests with real daemon + Agent (requires RUN_INTEGRATION=1)", ] -addopts = "-m not integration and not e2e" +# Default deselection of integration/e2e handled in conftest.py pytest_collection_modifyitems [tool.pyright] venvPath = "." diff --git a/tests/conftest.py b/tests/conftest.py index 1eca4f9..ba0bd54 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,10 +21,6 @@ def pytest_configure(config): for name, desc in markers.items(): config.addinivalue_line("markers", f"{name}: {desc}") - # When RUN_INTEGRATION=1, remove default marker filter so integration/e2e tests run - if os.environ.get("RUN_INTEGRATION"): - config.known_args_namespace.markexpr = "" - @pytest.fixture def isolated_data_root(tmp_path): -- 2.45.4 From e0af9f6bcee4b0006b9b9c9dc6d017e85d97ef33 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Mon, 8 Jun 2026 23:39:15 +0800 Subject: [PATCH 15/39] auto-sync: 2026-06-08 23:39:15 --- tests/conftest.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index ba0bd54..734a97b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -57,10 +57,17 @@ def client_with_isolation(isolated_data_root): def pytest_collection_modifyitems(config, items): if not os.environ.get("RUN_INTEGRATION"): - skip = pytest.mark.skip(reason="needs RUN_INTEGRATION=1") + skip_reason = "needs RUN_INTEGRATION=1" + remaining = [] + deselected = [] for item in items: if "integration" in item.keywords or "e2e" in item.keywords: - item.add_marker(skip) + deselected.append(item) + else: + remaining.append(item) + if deselected: + config.hook.pytest_deselected(items=deselected) + items[:] = remaining skip_no_integration = pytest.mark.skipif( -- 2.45.4 From f866e47390fdc5a7fa6314d23aa68b46b2fef944 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Tue, 9 Jun 2026 00:14:14 +0800 Subject: [PATCH 16/39] auto-sync: 2026-06-09 00:14:14 --- src/daemon/dispatcher.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/daemon/dispatcher.py b/src/daemon/dispatcher.py index 8d73093..fd6064d 100644 --- a/src/daemon/dispatcher.py +++ b/src/daemon/dispatcher.py @@ -218,7 +218,7 @@ class Dispatcher: def _mail_on_complete(aid, outcome): # 幻觉门控:检查是否有回复,自动标 done/failed try: - _dispatcher._mail_auto_complete(_task_id, aid, _mail_db, _must_haves) + _dispatcher._mail_auto_complete(_task_id, aid, _mail_db, _must_haves, outcome=outcome) except Exception as e: logger.error("Mail %s: on_complete error: %s", _task_id, e) on_complete = _mail_on_complete @@ -576,7 +576,7 @@ class Dispatcher: def _mail_oc_legacy(aid, outcome): try: - _disp._mail_auto_complete(_t_id, aid, _m_db, _m_mh) + _disp._mail_auto_complete(_t_id, aid, _m_db, _m_mh, outcome=outcome) except Exception as e: logger.error("Mail %s: legacy on_complete error: %s", _t_id, e) on_complete_legacy = _mail_oc_legacy @@ -661,7 +661,7 @@ class Dispatcher: logger.error("Mail %s: failed to revert to pending: %s", task_id, e) def _mail_auto_complete(self, task_id: str, agent_id: str, - db_path: Path, must_haves: str) -> None: + db_path: Path, must_haves: str, outcome=None) -> None: """Mail 任务:on_complete 后自动标 done/failed(含幻觉门控)""" try: # 解析 performative -- 2.45.4 From 1083c118db43e8518b1e25fa3527a71a28ac0083 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Tue, 9 Jun 2026 00:14:25 +0800 Subject: [PATCH 17/39] auto-sync: 2026-06-09 00:14:25 (catch-all) --- src/daemon/dispatcher.py | 8 ++++++++ src/daemon/spawner.py | 7 +++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/daemon/dispatcher.py b/src/daemon/dispatcher.py index fd6064d..4f9fa2b 100644 --- a/src/daemon/dispatcher.py +++ b/src/daemon/dispatcher.py @@ -712,6 +712,14 @@ class Dispatcher: logger.error("Mail %s: all 3 failed attempts failed, leaving for ticker", task_id) return + # inform 类型:只对成功 outcome 标 done,失败 outcome 留 working 等 ticker 重投 + # Task 路径不受此 bug 影响(走 _task_auto_complete 独立逻辑) + if performative == "inform": + INFORM_DONE_OUTCOMES = {"completed", "claimed", "no_reply"} + if outcome not in INFORM_DONE_OUTCOMES: + logger.info("Mail %s: inform outcome=%s, skip auto-done", task_id, outcome) + return + # 标 done(重试 3 次) for attempt in range(3): try: diff --git a/src/daemon/spawner.py b/src/daemon/spawner.py index 1d45bf2..7876435 100644 --- a/src/daemon/spawner.py +++ b/src/daemon/spawner.py @@ -848,10 +848,13 @@ curl -X POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_ # A8(gateway_unreachable), A11(lock_conflict), # A10(compact_failed), A12(agent_error) # v2.8.1 Fix-3a: crash 类 outcome 设 cooldown,给 agent session 恢复时间 - if outcome in ("crashed", "compact_failed", "process_crash", "session_stuck", + if outcome == "crashed" and self.counter: + self.counter.set_cooldown(agent_id, seconds=60) + logger.info("Crash cooldown set for %s: 60s (outcome=%s)", agent_id, outcome) + elif outcome in ("compact_failed", "process_crash", "session_stuck", "compact_hanging", "agent_error", "compact_interrupted") and self.counter: self.counter.set_cooldown(agent_id, seconds=300) # 5 分钟 - logger.info("Crash/error cooldown set for %s: 300s (outcome=%s)", agent_id, outcome) + logger.info("Error cooldown set for %s: 300s (outcome=%s)", agent_id, outcome) # F1: 不可恢复 outcome → 立刻标 failed + 写黑板 if outcome in ("auth_failed", "agent_error") and db_path and task_id: logger.error("Task %s: unrecoverable outcome=%s, marking failed immediately", task_id, outcome) -- 2.45.4 From f267ccc699d936362493ec63c1ac1d88f6628e85 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Tue, 9 Jun 2026 00:24:51 +0800 Subject: [PATCH 18/39] auto-sync: 2026-06-09 00:24:51 --- docs/design/13-toolchain-and-dev-workflow.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/design/13-toolchain-and-dev-workflow.md b/docs/design/13-toolchain-and-dev-workflow.md index dc874b2..22b7e9d 100644 --- a/docs/design/13-toolchain-and-dev-workflow.md +++ b/docs/design/13-toolchain-and-dev-workflow.md @@ -2007,6 +2007,9 @@ CI workflow 已有 `notify-on-failure` job(ci.yml),当前格式: | 7 | 签名算法 | ✅ 已确认 | Gitea 使用 HMAC-SHA256,代码注释已补 | | 8 | Webhook 作用范围 | ✅ 组织级 | Gitea 组织级 webhook(Hook 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 | --- @@ -2753,3 +2756,12 @@ Gitea v1.23.4 自带完整的 CI 管理界面: | §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 webhook(review.state/body/user)和 org webhook(review.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 | -- 2.45.4 From 1b3a8b56fe606459a047b8411a3cc5a8919987f4 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Tue, 9 Jun 2026 00:38:45 +0800 Subject: [PATCH 19/39] auto-sync: 2026-06-09 00:38:45 --- docs/design/18-toolchain-e2e-test.md | 48 ++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 docs/design/18-toolchain-e2e-test.md diff --git a/docs/design/18-toolchain-e2e-test.md b/docs/design/18-toolchain-e2e-test.md new file mode 100644 index 0000000..56c2fa8 --- /dev/null +++ b/docs/design/18-toolchain-e2e-test.md @@ -0,0 +1,48 @@ +# §18. 工具链端到端验证测试 + +> 日期:2026-06-09 +> 状态:执行中 +> 目标:用真实 Webhook 触发验证整条 Mail 通知链路 + +## 前置确认 + +- Gitea 用户名 ↔ Agent ID 映射:完全一致(admin, guanyu-dev, jiangwei-infra, pangtong-fujunshi, simayi-challenger, zhangfei-dev, zhaoyun-data) +- Gitea 组织级 Webhook(Hook ID=28):姜维确认最近 5 条投递全部 is_succeed=1 +- Daemon 在线:sanguo-moziplus-v2 运行中 +- 测试仓库:sanguo/moziplus-v2 + +## 命名规范 + +- Issue 标题:`[E2E-TEST] xxx` +- PR 标题:`[E2E-TEST] xxx` +- 分支名:`test/e2e-` + +## 验证步骤 + +| 步骤 | 操作 | 触发事件 | 预期 Mail 通知 | 验证点 | +|------|------|----------|---------------|--------| +| 1 | 创建 Issue `[E2E-TEST] Issue指派测试`,assignee=zhangfei-dev | issues (assigned) | zhangfei-dev 收到 "Issue 指派" Mail | Mail to/模板正确 | +| 2 | 开分支 `test/e2e-`,创建 PR `[E2E-TEST] Review请求测试` | pull_request (opened) | simayi-challenger 收到 "Review 请求" Mail | Mail to/风险级别/文件列表 | +| 3 | PR Review APPROVED | pull_request_review (approved) | PR 作者(pangtong-fujunshi) 收到 "Review 通过 ✓" Mail | result=通过 ✓ | +| 4 | PR Review REQUEST_CHANGES | pull_request_review (rejected) | PR 作者收到 "Review 驳回 ✗" Mail | result=驳回 ✗ | +| 5 | Issue 上发评论 `[CI] CI 失败 — 分支: test/e2e-xxx, 错误: build timeout` | issue_comment | Issue 作者收到 "CI 失败" Mail | 模板含分支/错误摘要 | +| 6 | 创建标题含"部署失败"的 Issue(无指派) | issues (opened) | jiangwei-infra + pangtong-fujunshi 各收到 "部署失败" Mail | 双收件人 | +| 7 | 关闭步骤 1 的 Issue,再发 CI 失败评论 | issue_comment (closed issue) | 不产生 Mail(负面测试) | handler 跳过 closed | +| 8 | 重发步骤 1 Webhook(相同 delivery ID) | 重复事件 | 不产生新 Mail(幂等测试) | 返回 duplicate | + +## 签名校验 + +暂不测试(需恢复 GITEA_WEBHOOK_SECRET)。 + +## Review 意见来源 + +- 姜维(基础设施确认 + 边界验证建议) +- 司马懿(遗漏点补充 + 命名规范 + 风险防范) + +--- + +## 执行记录 + +> 以下由自动化测试脚本填写 + +(待填写) -- 2.45.4 From 9f97f85ea2f86165de1504dc6cef4ddd20ee5f5e Mon Sep 17 00:00:00 2001 From: cfdaily Date: Tue, 9 Jun 2026 00:44:21 +0800 Subject: [PATCH 20/39] auto-sync: 2026-06-09 00:44:21 --- docs/design/18-toolchain-e2e-test.md | 64 +++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/docs/design/18-toolchain-e2e-test.md b/docs/design/18-toolchain-e2e-test.md index 56c2fa8..2d53f4e 100644 --- a/docs/design/18-toolchain-e2e-test.md +++ b/docs/design/18-toolchain-e2e-test.md @@ -43,6 +43,66 @@ ## 执行记录 -> 以下由自动化测试脚本填写 +> 2026-06-09 00:40~00:50 CST -(待填写) +### 步骤 1:Issue 指派 ✅ +- 操作:创建 Issue #22 `[E2E-TEST] Issue指派测试`,assignee=zhangfei-dev +- Mail:`mail-1780936736480`,from=system, to=zhangfei-dev, title=`Issue 指派: [E2E-TEST] Issue指派测试` +- 模板渲染正确(含 Issue 链接、标签、描述、建议分支名) + +### 步骤 2:PR Review 请求 ✅ +- 操作:创建分支 `test/e2e-1780936838`,创建 PR #23 +- Mail:`mail-1780936851715`,from=system, to=simayi-challenger +- 模板含 PR 链接、标题、作者(pangtong-fujunshi)、分支、风险级别(standard) +- 附带:CI 失败通知 `mail-1780936876572`(CI 自动触发,符合预期) + +### 步骤 3:Review APPROVED ✅ +- 操作:用 simayi-challenger token 提交 APPROVED review +- Mail:`mail-1780936968411`,from=system, to=pangtong-fujunshi, title=`Review 通过 ✓` +- 描述含审查者(simayi-challenger)、review body +- ⚠️ 收到 2 封重复 Mail(org webhook + repo webhook 双触发) + +### 步骤 4:Review REQUEST_CHANGES ✅ +- 操作:用 simayi-challenger token 提交 REQUEST_CHANGES review +- Mail:`mail-1780936972207`,from=system, to=pangtong-fujunshi, title=`Review 驳回 ✗` +- ⚠️ 同上,收到 2 封重复 Mail + +### 步骤 5:CI 失败评论 ✅ +- 操作:在 Issue #22 发评论 `[CI] CI 失败 — 分支: test/e2e-1780936838, 错误: build timeout` +- Mail:`mail-1780936994513`,from=system, to=pangtong-fujunshi, title=`CI 失败: sanguo/moziplus-v2#22` +- 模板含分支提取和错误摘要 + +### 步骤 6:部署失败 Issue ✅ +- 操作:创建 Issue #24 `[E2E-TEST] 部署失败: test deploy`(无指派) +- Mail:`mail-1780936999660` to=jiangwei-infra, `mail-1780936999684` to=pangtong-fujunshi +- 双收件人验证通过 ✅ + +### 步骤 7:已关闭 Issue 负面测试 ✅ +- 操作:关闭 Issue #22 后发 `[CI] CI 失败 — 应被过滤` +- 结果:未产生新 Mail ✅(只有步骤 5 的 1 封 CI Mail,步骤 7 的评论被正确过滤) + +### 步骤 8:幂等测试 ⏳ 待执行 +- 需要手动构造重复 Webhook 请求验证 + +--- + +## 汇总 + +| 步骤 | 状态 | 备注 | +|------|------|------| +| 1. Issue 指派 | ✅ 通过 | Mail to/模板正确 | +| 2. PR Review 请求 | ✅ 通过 | Mail to/风险级别/文件列表正确 | +| 3. Review APPROVED | ✅ 通过 | 但收到 2 封重复 Mail(双 webhook) | +| 4. Review REQUEST_CHANGES | ✅ 通过 | 同上,2 封重复 | +| 5. CI 失败评论 | ✅ 通过 | 分支提取正确 | +| 6. 部署失败 Issue | ✅ 通过 | 双收件人验证通过 | +| 7. 已关闭 Issue 过滤 | ✅ 通过 | 负面测试通过,无新 Mail | +| 8. 幂等测试 | ⏳ 待执行 | 需手动构造重复请求 | + +## 发现的问题 + +### P1:Review 事件双 Mail +- **现象**:每次 Review 事件产生 2 封完全相同的 Mail +- **原因**:Gitea 同时触发了 org webhook + repo webhook,两个 Webhook 独立投递 +- **影响**:用户收到重复通知 +- **建议修复**:幂等检查已按 event+delivery 去重,但 org 和 repo webhook 的 delivery ID 不同,所以去重不生效。需改为按事件内容去重(如 review.id) -- 2.45.4 From 91fd0f931899d179e9454895f24aa57e35b1ea6e Mon Sep 17 00:00:00 2001 From: cfdaily Date: Tue, 9 Jun 2026 00:45:24 +0800 Subject: [PATCH 21/39] auto-sync: 2026-06-09 00:45:24 --- docs/design/18-toolchain-e2e-test.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/design/18-toolchain-e2e-test.md b/docs/design/18-toolchain-e2e-test.md index 2d53f4e..e95c67a 100644 --- a/docs/design/18-toolchain-e2e-test.md +++ b/docs/design/18-toolchain-e2e-test.md @@ -81,8 +81,11 @@ - 操作:关闭 Issue #22 后发 `[CI] CI 失败 — 应被过滤` - 结果:未产生新 Mail ✅(只有步骤 5 的 1 封 CI Mail,步骤 7 的评论被正确过滤) -### 步骤 8:幂等测试 ⏳ 待执行 -- 需要手动构造重复 Webhook 请求验证 +### 步骤 8:幂等测试 ✅ +- 操作:构造带正确 HMAC-SHA256 签名的 Webhook,用同一 delivery ID `test-idempotency-002` 发两次 +- 第一次:返回 `ok`,产生 Mail ✅ +- 第二次:返回 `duplicate`,无新 Mail ✅ +- 额外验证:不带签名的请求返回 403 `signature verification failed`(签名校验正常工作) --- @@ -97,7 +100,7 @@ | 5. CI 失败评论 | ✅ 通过 | 分支提取正确 | | 6. 部署失败 Issue | ✅ 通过 | 双收件人验证通过 | | 7. 已关闭 Issue 过滤 | ✅ 通过 | 负面测试通过,无新 Mail | -| 8. 幂等测试 | ⏳ 待执行 | 需手动构造重复请求 | +| 8. 幂等测试 | ✅ 通过 | 第二次返回 duplicate,无新 Mail;签名校验正常拦截无签名请求 | ## 发现的问题 -- 2.45.4 From 6e1d86f44a50c01472afe5e311ede7e44a03f4e0 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Tue, 9 Jun 2026 00:45:50 +0800 Subject: [PATCH 22/39] auto-sync: 2026-06-09 00:45:50 --- docs/design/18-toolchain-e2e-test.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/design/18-toolchain-e2e-test.md b/docs/design/18-toolchain-e2e-test.md index e95c67a..b744ecf 100644 --- a/docs/design/18-toolchain-e2e-test.md +++ b/docs/design/18-toolchain-e2e-test.md @@ -1,7 +1,7 @@ # §18. 工具链端到端验证测试 > 日期:2026-06-09 -> 状态:执行中 +> 状态:已完成 ✅ > 目标:用真实 Webhook 触发验证整条 Mail 通知链路 ## 前置确认 @@ -32,7 +32,9 @@ ## 签名校验 -暂不测试(需恢复 GITEA_WEBHOOK_SECRET)。 +已测试(GITEA_WEBHOOK_SECRET 已配置且生效): +- ✅ 正确签名:请求正常处理 +- ✅ 无签名:返回 403 `signature verification failed` ## Review 意见来源 -- 2.45.4 From 2b00269d7db9ced31c1b7a809ffa15932439ce81 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Tue, 9 Jun 2026 07:46:02 +0800 Subject: [PATCH 23/39] auto-sync: 2026-06-09 07:46:02 --- src/api/toolchain_routes.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/api/toolchain_routes.py b/src/api/toolchain_routes.py index c0f3abe..504e884 100644 --- a/src/api/toolchain_routes.py +++ b/src/api/toolchain_routes.py @@ -46,17 +46,37 @@ _TTL_SECONDS = 7 * 24 * 3600 _idempotency_lock = asyncio.Lock() -def _is_duplicate(event: str, delivery: str) -> bool: - """检查 Webhook 是否重复投递,自动清理过期条目。""" +def _is_duplicate(event: str, delivery: str, payload: Optional[Dict[str, Any]] = None) -> bool: + """检查 Webhook 是否重复投递,自动清理过期条目。 + + 双重去重策略: + 1. delivery UUID 去重(标准幂等) + 2. payload 内容去重(应对 Gitea v1.23.4 的 webhookNotifier + actionsNotifier + 对同一 review 生成不同 UUID 的双投递问题) + """ now = time.time() # 清理过期条目 while _delivery_timestamps and (now - _delivery_timestamps[0][0]) > _TTL_SECONDS: _, key = _delivery_timestamps.pop(0) _delivery_cache.discard(key) + # 检查 delivery UUID 去重 key = f"{event}-{delivery}" if key in _delivery_cache: return True + + # 检查 payload 内容去重(review 事件:同一 PR + 同一用户 + 同一内容) + if payload and "review" in event: + pr_num = payload.get("pull_request", {}).get("number") + sender = payload.get("sender", {}).get("login") + content = payload.get("review", {}).get("content", "") + content_key = f"content:{event}:{pr_num}:{sender}:{content}" + if content_key in _delivery_cache: + logger.info("Content-based duplicate detected: %s PR#%s by %s", event, pr_num, sender) + return True + _delivery_cache.add(content_key) + _delivery_timestamps.append((now, content_key)) + _delivery_cache.add(key) _delivery_timestamps.append((now, key)) return False @@ -462,14 +482,7 @@ async def gitea_webhook( logger.warning("Webhook signature verification failed") return Response(status_code=403, content="signature verification failed") - # 2. 幂等检查 - if x_gitea_event and x_gitea_delivery: - async with _idempotency_lock: - if _is_duplicate(x_gitea_event, x_gitea_delivery): - logger.debug("Duplicate webhook: %s/%s", x_gitea_event, x_gitea_delivery) - return Response(status_code=200, content="duplicate") - - # 3. 解析 payload + # 3. 解析 payload(提前解析,用于幂等检查) try: payload = await request.json() except Exception: -- 2.45.4 From 1f61b81734f49ce189d03b78ec191fda1668829f Mon Sep 17 00:00:00 2001 From: cfdaily Date: Tue, 9 Jun 2026 07:46:23 +0800 Subject: [PATCH 24/39] auto-sync: 2026-06-09 07:46:23 --- src/api/toolchain_routes.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/api/toolchain_routes.py b/src/api/toolchain_routes.py index 504e884..cb443ee 100644 --- a/src/api/toolchain_routes.py +++ b/src/api/toolchain_routes.py @@ -489,6 +489,13 @@ async def gitea_webhook( logger.warning("Failed to parse webhook payload") return Response(status_code=200, content="invalid payload") + # 2. 幂等检查(需要在 payload 解析后,以支持内容去重) + if x_gitea_event and x_gitea_delivery: + async with _idempotency_lock: + if _is_duplicate(x_gitea_event, x_gitea_delivery, payload): + logger.debug("Duplicate webhook: %s/%s", x_gitea_event, x_gitea_delivery) + return Response(status_code=200, content="duplicate") + # 4. 查找 handler handler = _EVENT_HANDLERS.get(x_gitea_event or "") if not handler: -- 2.45.4 From 38372786a240b93c3933ce3c9896dde05d998a31 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Tue, 9 Jun 2026 08:06:43 +0800 Subject: [PATCH 25/39] auto-sync: 2026-06-09 08:06:43 --- src/api/toolchain_routes.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/api/toolchain_routes.py b/src/api/toolchain_routes.py index cb443ee..1c2795c 100644 --- a/src/api/toolchain_routes.py +++ b/src/api/toolchain_routes.py @@ -66,10 +66,14 @@ def _is_duplicate(event: str, delivery: str, payload: Optional[Dict[str, Any]] = return True # 检查 payload 内容去重(review 事件:同一 PR + 同一用户 + 同一内容) + # 注意:Gitea webhookNotifier 用 review.body,actionsNotifier 用 review.content + # 所以去重 key 需要同时取两个字段,确保两种格式生成相同 key if payload and "review" in event: pr_num = payload.get("pull_request", {}).get("number") sender = payload.get("sender", {}).get("login") - content = payload.get("review", {}).get("content", "") + review = payload.get("review", {}) + # 取 body 或 content,优先 body(webhookNotifier 格式) + content = review.get("body", "") or review.get("content", "") content_key = f"content:{event}:{pr_num}:{sender}:{content}" if content_key in _delivery_cache: logger.info("Content-based duplicate detected: %s PR#%s by %s", event, pr_num, sender) -- 2.45.4 From f961037fd2204b4a272845eb761c7dd17ec6138f Mon Sep 17 00:00:00 2001 From: cfdaily Date: Tue, 9 Jun 2026 08:30:45 +0800 Subject: [PATCH 26/39] auto-sync: 2026-06-09 08:30:45 --- src/api/toolchain_routes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/api/toolchain_routes.py b/src/api/toolchain_routes.py index 1c2795c..666708a 100644 --- a/src/api/toolchain_routes.py +++ b/src/api/toolchain_routes.py @@ -74,7 +74,8 @@ def _is_duplicate(event: str, delivery: str, payload: Optional[Dict[str, Any]] = review = payload.get("review", {}) # 取 body 或 content,优先 body(webhookNotifier 格式) content = review.get("body", "") or review.get("content", "") - content_key = f"content:{event}:{pr_num}:{sender}:{content}" + content_hash = hashlib.sha256(content.encode()).hexdigest()[:16] + content_key = f"content:{event}:{pr_num}:{sender}:{content_hash}" if content_key in _delivery_cache: logger.info("Content-based duplicate detected: %s PR#%s by %s", event, pr_num, sender) return True -- 2.45.4 From 28f4cc7debd3a66970a8f9977d841ac03e84ebbe Mon Sep 17 00:00:00 2001 From: cfdaily Date: Tue, 9 Jun 2026 08:46:58 +0800 Subject: [PATCH 27/39] auto-sync: 2026-06-09 08:46:58 --- docs/design/18-toolchain-e2e-test.md | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/design/18-toolchain-e2e-test.md b/docs/design/18-toolchain-e2e-test.md index b744ecf..533e2fe 100644 --- a/docs/design/18-toolchain-e2e-test.md +++ b/docs/design/18-toolchain-e2e-test.md @@ -97,8 +97,8 @@ |------|------|------| | 1. Issue 指派 | ✅ 通过 | Mail to/模板正确 | | 2. PR Review 请求 | ✅ 通过 | Mail to/风险级别/文件列表正确 | -| 3. Review APPROVED | ✅ 通过 | 但收到 2 封重复 Mail(双 webhook) | -| 4. Review REQUEST_CHANGES | ✅ 通过 | 同上,2 封重复 | +| 3. Review APPROVED | ✅ 通过 | E2E 测试中产生 2 封 Mail(根因已查明,非平台问题) | +| 4. Review REQUEST_CHANGES | ✅ 通过 | 同上 | | 5. CI 失败评论 | ✅ 通过 | 分支提取正确 | | 6. 部署失败 Issue | ✅ 通过 | 双收件人验证通过 | | 7. 已关闭 Issue 过滤 | ✅ 通过 | 负面测试通过,无新 Mail | @@ -106,8 +106,16 @@ ## 发现的问题 -### P1:Review 事件双 Mail -- **现象**:每次 Review 事件产生 2 封完全相同的 Mail -- **原因**:Gitea 同时触发了 org webhook + repo webhook,两个 Webhook 独立投递 -- **影响**:用户收到重复通知 -- **建议修复**:幂等检查已按 event+delivery 去重,但 org 和 repo webhook 的 delivery ID 不同,所以去重不生效。需改为按事件内容去重(如 review.id) +### Review 事件双 Mail(已修复) +- **现象**:E2E 测试步骤 3/4 中 Review 事件产生 2 封 Mail +- **根因**(姜维深入调查确认):E2E 测试中庞统手动用 simayi token 提交了 Review,同时 simayi agent 收到 Review 请求 Mail 后也自主提交了 Review。是两次独立的 API 调用,**不是 Gitea bug 或平台配置问题** + - 姜维控制实验:一次 review API 调用只产生 1 个 hook_task + - Gitea 路由日志确认两次 POST 间隔 7 秒,payload 有差异(review_comments、updated_at 不同) + - 之前的错误分析("Gitea webhookNotifier + actionsNotifier 双投递")已被推翻:actionsNotifier 走 handleWorkflows() 不创建 hook_task +- **修复**:payload 内容去重作为防御性编程保留(`_is_duplicate` 新增内容去重 key = event + pr_num + sender + sha256(body_or_content)),司马懿 APPROVED +- **验证**:PR #27 实测只产生 1 封 Mail ✅ + +### 根因分析教训 +- 姜维第一次分析给出了错误根因(Gitea 双 notifier),第二次深入调查后自我纠正 +- 庞统把姜维的第一次结论当事实汇报给主公,没有标注"这是姜维的调查结论,尚未独立验证" +- **改进**:SOUL.md 新增规则——推测 vs 事实显式标注、引用他人结论时标注来源、结论被推翻时及时更正 -- 2.45.4 From 4ed5c18f21928a74343bd1f30a8e49dbbe82ff04 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Tue, 9 Jun 2026 08:47:09 +0800 Subject: [PATCH 28/39] auto-sync: 2026-06-09 08:47:09 (catch-all) --- docs/design/13-toolchain-and-dev-workflow.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design/13-toolchain-and-dev-workflow.md b/docs/design/13-toolchain-and-dev-workflow.md index 22b7e9d..ca9d151 100644 --- a/docs/design/13-toolchain-and-dev-workflow.md +++ b/docs/design/13-toolchain-and-dev-workflow.md @@ -1590,7 +1590,7 @@ daemon 内部 ───────┘ │ 5. 创建 Mail │ | 只处理白名单内的事件类型 | 未知的忽略 + 日志 | | issue_comment 需判断来源 | 只处理 CI workflow 写的评论(按特定前缀匹配:`❌ **CI 失败**` 或统一后的 `[CI]` 前缀) | | PR 作者/审查者必须是已知 Agent | 未知的忽略 + 日志 | -| 幂等:同一事件不重复创建 Mail | 按 `{x_gitea_event}-{x_gitea_delivery}` 去重(delivery ID 来自 `X-Gitea-Delivery` header) | +| 幂等:同一事件不重复创建 Mail | 双重去重:① delivery UUID(`{event}-{delivery}`)标准幂等;② review 事件 payload 内容去重(`{event}:{pr_num}:{sender}:{sha256(body_or_content)[:16]}`),防御同一 review 被不同来源重复提交(2026-06-09 新增) | --- -- 2.45.4 From 3bc629de8c455a1c83dfcbac9f2ee29a869fb9a0 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Tue, 9 Jun 2026 11:13:34 +0800 Subject: [PATCH 29/39] auto-sync: 2026-06-09 11:13:34 --- src/api/toolchain_routes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/api/toolchain_routes.py b/src/api/toolchain_routes.py index 666708a..20e58e8 100644 --- a/src/api/toolchain_routes.py +++ b/src/api/toolchain_routes.py @@ -400,6 +400,8 @@ async def _handle_issues(payload: Dict[str, Any]) -> None: async def _handle_issue_comment(payload: Dict[str, Any]) -> None: """处理 issue_comment 事件:CI 失败关键词 → 通知 PR 作者。""" + # DEBUG: log full payload for investigation + logger.info("issue_comment FULL PAYLOAD: %s", json.dumps(payload, ensure_ascii=False)[:3000]) comment = payload.get("comment") if not comment or not isinstance(comment, dict): logger.warning("issue_comment event missing comment field, skipping") -- 2.45.4 From 1e1d2bc4a0395c754bdee2a5521e08d3946a40fd Mon Sep 17 00:00:00 2001 From: cfdaily Date: Tue, 9 Jun 2026 11:15:09 +0800 Subject: [PATCH 30/39] auto-sync: 2026-06-09 11:15:09 --- src/api/toolchain_routes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/api/toolchain_routes.py b/src/api/toolchain_routes.py index 20e58e8..7dca3e4 100644 --- a/src/api/toolchain_routes.py +++ b/src/api/toolchain_routes.py @@ -484,6 +484,9 @@ async def gitea_webhook( """ body = await request.body() + # 0. DEBUG: log all webhook events + logger.info("WEBHOOK DEBUG event=%s delivery=%s", x_gitea_event, x_gitea_delivery) + # 1. 签名验证 if not _verify_signature(body, x_gitea_signature): logger.warning("Webhook signature verification failed") -- 2.45.4 From d1491a31d2d3f3695149418441f4e58a22c42ddd Mon Sep 17 00:00:00 2001 From: cfdaily Date: Tue, 9 Jun 2026 11:16:05 +0800 Subject: [PATCH 31/39] auto-sync: 2026-06-09 11:16:05 --- src/api/toolchain_routes.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/api/toolchain_routes.py b/src/api/toolchain_routes.py index 7dca3e4..dd03218 100644 --- a/src/api/toolchain_routes.py +++ b/src/api/toolchain_routes.py @@ -400,8 +400,11 @@ async def _handle_issues(payload: Dict[str, Any]) -> None: async def _handle_issue_comment(payload: Dict[str, Any]) -> None: """处理 issue_comment 事件:CI 失败关键词 → 通知 PR 作者。""" - # DEBUG: log full payload for investigation - logger.info("issue_comment FULL PAYLOAD: %s", json.dumps(payload, ensure_ascii=False)[:3000]) + # DEBUG: dump full payload to file for investigation + _debug_payload_dir = Path(get_data_root()) / "_debug" + _debug_payload_dir.mkdir(parents=True, exist_ok=True) + (_debug_payload_dir / f"issue_comment_payload_{int(time.time())}.json").write_text( + json.dumps(payload, ensure_ascii=False, indent=2)) comment = payload.get("comment") if not comment or not isinstance(comment, dict): logger.warning("issue_comment event missing comment field, skipping") @@ -484,9 +487,6 @@ async def gitea_webhook( """ body = await request.body() - # 0. DEBUG: log all webhook events - logger.info("WEBHOOK DEBUG event=%s delivery=%s", x_gitea_event, x_gitea_delivery) - # 1. 签名验证 if not _verify_signature(body, x_gitea_signature): logger.warning("Webhook signature verification failed") -- 2.45.4 From feff387ff7fd9f3ed50defe5ebf1a699c3502947 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Tue, 9 Jun 2026 11:17:56 +0800 Subject: [PATCH 32/39] auto-sync: 2026-06-09 11:17:56 --- src/api/toolchain_routes.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/api/toolchain_routes.py b/src/api/toolchain_routes.py index dd03218..666708a 100644 --- a/src/api/toolchain_routes.py +++ b/src/api/toolchain_routes.py @@ -400,11 +400,6 @@ async def _handle_issues(payload: Dict[str, Any]) -> None: async def _handle_issue_comment(payload: Dict[str, Any]) -> None: """处理 issue_comment 事件:CI 失败关键词 → 通知 PR 作者。""" - # DEBUG: dump full payload to file for investigation - _debug_payload_dir = Path(get_data_root()) / "_debug" - _debug_payload_dir.mkdir(parents=True, exist_ok=True) - (_debug_payload_dir / f"issue_comment_payload_{int(time.time())}.json").write_text( - json.dumps(payload, ensure_ascii=False, indent=2)) comment = payload.get("comment") if not comment or not isinstance(comment, dict): logger.warning("issue_comment event missing comment field, skipping") -- 2.45.4 From 31a5836dd6553cbe1b3797dab183ee6547b018de Mon Sep 17 00:00:00 2001 From: cfdaily Date: Tue, 9 Jun 2026 11:57:58 +0800 Subject: [PATCH 33/39] auto-sync: 2026-06-09 11:57:58 --- docs/design/19-toolchain-context-layers.md | 372 +++++++++++++++++++++ 1 file changed, 372 insertions(+) create mode 100644 docs/design/19-toolchain-context-layers.md diff --git a/docs/design/19-toolchain-context-layers.md b/docs/design/19-toolchain-context-layers.md new file mode 100644 index 0000000..949a81d --- /dev/null +++ b/docs/design/19-toolchain-context-layers.md @@ -0,0 +1,372 @@ +# #19 工具链事件中枢 — 上下文四层改造方案 + +> 版本: v1.0 +> 日期: 2026-06-09 +> 作者: 庞统(副军师) +> 状态: 待主公确认 +> 前置: #13 工具链与开发流程 §16, #05 上下文四层架构 +> 来源: E2E 真实场景测试暴露的三个断层 + +--- + +## 一、问题诊断 + +### 1.1 E2E 真实场景测试暴露的三个断层 + +主公在 moziplus-v2 仓库创建了 Issue #32(添加 /api/stats 端点),指派张飞。链条在第一步就断了。 + +| 断层 | 现象 | 根因 | +|------|------|------| +| **Agent 不知道该做什么** | 张飞收到 Issue 指派 Mail,回复"已阅"就结束了 | Mail 模板(issue_assigned.md)5 行信息,无流程引导;spawn prompt 说"已阅即可" | +| **Agent 去错了仓库** | 张飞去读了 sanguo_moziplus_v2 平台代码,而不是空的实验仓库 moziplus-v2 | Mail 模板没有仓库 clone URL,张飞凭习惯去了开发目录 | +| **Agent 在 Control UI 提问** | 张飞遇到问题直接在 Control UI 问主公,没有去 Gitea Issue 评论 | 没有任何地方引导"有疑问去 Gitea Issue 评论" | +| **Agent 不知道怎么协作** | 张飞判断任务需要澄清,但不知道该怎么请求澄清 | 没有"做不了→在 Issue 评论 / Mail 庞统"的回退路径 | +| **跨 Agent @mention 无法通知** | 张飞在 Issue 评论 @赵云,赵云收不到通知 | issue_comment handler 只处理 [CI] 评论,@mention 被忽略 | + +### 1.2 根因:工具链在四层架构中的断层 + +| 层 | 应该有 | 实际有 | Gap | +|---|---|---|---| +| **L0 铁律** | — | — | 无需改动 | +| **L1 角色** | 工具链协作行为规范(所有 Agent 共享) | 无 | AGENTS.md 没有工具链相关内容 | +| **L2 引擎注入** | 事件上下文(仓库 clone URL、Gitea API、Issue/PR 详情) | Mail 模板只有 5 行摘要 | 缺仓库信息和流程引导 | +| **L3 被动参考** | 技术细节(分支命名、commit 规范、PR 创建方式) | git-workflow 等 Skill 已存在但没人触发 | Agent 不知道该加载哪个 Skill | + +--- + +## 二、改造方案:四层归属 + +### 2.1 分层原则 + +| 层 | 放什么 | 不放什么 | 理由 | +|---|---|---|---| +| **L0** | 不放 | — | 工具链不是安全底线 | +| **L1** | 协作行为规范:收到什么通知该做什么、遇到问题怎么办 | 技术细节(分支命名、commit 格式) | 行为规范是团队常识,每个 Agent 都要知道 | +| **L2** | 事件上下文:仓库 clone URL、Gitea API URL、Issue/PR 链接、动态信息 | 固定的协作流程 | 动态信息每次不同,由 Mail 模板 + spawn 时注入 | +| **L3** | 技术细节:git-workflow、code-review 等 Skill 全文 | — | 按需加载,Agent 知道"我要提 PR"后自己读 | + +### 2.2 各层具体内容 + +#### L1:AGENTS.md 加工具链协作行为段(所有 Agent 统一) + +```markdown +## 工具链协作(Gitea) + +收到 Gitea 事件通知(Issue 指派、Review 请求、CI 失败等)时,按以下流程操作: + +### 基本流程 +- **Issue 指派** → clone 仓库 → 开分支 → 编码 → 提 PR(参考 git-workflow Skill) +- **Review 请求** → 读 PR diff(Gitea API)→ 提交 Review(参考 code-review Skill) +- **Review 通过** → 等 merge +- **Review 驳回** → 看 review body → 修代码 → 重新 push +- **CI 失败** → 看错误摘要 → 修代码 → push(自动重触发 CI) +- **部署失败** → 查 deploy 日志 → 修复 + +### 协作规则 +- **有疑问?** 在 Gitea Issue 下评论,不要在 Control UI 或 Mail 里问 +- **需要别人帮忙?** 在 Issue 评论中 @mention 对应 Agent(如 @zhaoyun-data) +- **做不了?** 回复 Mail 说明原因和建议的接手人 +- **获取完整上下文** → 用 Gitea API 拉取 Issue 详情和评论,不要只看 Mail 里的快照 + +### Gitea API 速查 +- Issue 详情: GET /api/v1/repos/{owner}/{repo}/issues/{number} +- Issue 评论: GET /api/v1/repos/{owner}/{repo}/issues/{number}/comments +- PR diff: GET /api/v1/repos/{owner}/{repo}/pulls/{number}.diff +- 提交 Review: POST /api/v1/repos/{owner}/{repo}/pulls/{number}/reviews +``` + +**改动范围**:6 个 Agent 的 AGENTS.md 各加一段(内容统一)。 + +#### L2:Mail 模板精简 + 事件上下文注入 + +**原则**:模板只放摘要 + 链接 + 仓库信息,不写固定步骤(步骤在 L1)。 + +**issue_assigned.md** 改为: + +```markdown +Issue 指派 + +Issue: {issue_url} +标题: {issue_title} +标签: {labels} + +📋 获取完整上下文(先读再动手): +- Issue 详情: GET {gitea_api}/repos/{repo}/issues/{issue_number} +- Issue 评论: GET {gitea_api}/repos/{repo}/issues/{issue_number}/comments + +仓库: {repo_clone_url} +建议分支: feat/issue-{issue_number}-{brief} +``` + +**review_request.md** 改为: + +```markdown +PR Review 请求 + +PR: {pr_url} +标题: {pr_title} +作者: {pr_author} +分支: {branch} +风险级别: {risk_level} + +📋 获取完整上下文: +- PR diff: GET {gitea_api}/repos/{repo}/pulls/{pr_number}.diff +- PR 文件列表: GET {gitea_api}/repos/{repo}/pulls/{pr_number}/files +``` + +**review_result.md** 改为: + +```markdown +Review {result} + +PR: {pr_url} +标题: {pr_title} +审查者: {reviewer} + +{review_body} +``` + +**ci_failure.md** 改为: + +```markdown +CI 失败 + +Issue: {issue_url} +分支: {branch} + +错误摘要: +{error_summary} + +📋 CI 日志: {gitea_url}/{repo}/actions +修复后 push 会自动重触发 CI。 +``` + +**deploy_failure.md** 改为: + +```markdown +部署失败 + +仓库: {repo} +Commit: {commit_sha} + +📋 排查步骤: +- CI 日志: {gitea_url}/{repo}/actions +- 服务器: pm2 logs {service_name} +``` + +**L2 代码改动**(`toolchain_routes.py`): + +1. 从 Webhook payload 的 `repository` 对象提取 `clone_url` 和 `html_url` +2. `render_template()` 传入新变量:`gitea_api`、`gitea_url`、`repo_clone_url` +3. 所有模板变量统一补齐 + +#### L3:Skill 按需加载(不改 Skill 本身) + +git-workflow、code-review 等 Skill 保持不变。 + +L1 的协作行为段里会引用 Skill 名称("参考 git-workflow Skill"),Agent 收到 Mail 后根据 L1 的引导自主加载对应 Skill。 + +**不改 Skill 路由机制**——靠 L1 的文案触发 Agent 的 Skill 路由器匹配。 + +--- + +## 三、新增功能:issue_comment @mention 通知 + +### 3.1 设计 + +当前 `_handle_issue_comment` 只处理 `[CI]` 前缀评论。扩展为: + +``` +issue_comment 事件 + ├── 含 [CI] / CI 失败 → 原有 CI 失败通知逻辑 + └── 含 @username → 解析 @mention → Mail 通知被 @的 Agent +``` + +### 3.2 实现 + +**`toolchain_routes.py` 新增 `_handle_issue_comment_mention()`**: + +```python +AGENT_IDS = { + "zhangfei-dev", "guanyu-dev", "zhaoyun-data", + "jiangwei-infra", "simayi-challenger", "pangtong-fujunshi", +} + +# 前缀映射:@张飞 → zhangfei-dev +AGENT_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"`。 + +--- + +## 五、完整改动清单 + +| # | 改什么 | 改动内容 | 层 | 风险 | +|---|--------|---------|---|------| +| 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 改造 | -- 2.45.4 From 466a5263fabfb3a8e43a2119f0385849cab09874 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Tue, 9 Jun 2026 12:51:27 +0800 Subject: [PATCH 34/39] fix: lint - 114 errors fixed (unused imports, indentation, blank lines) --- src/api/blackboard_routes.py | 18 ++++++++------ src/api/mail_routes.py | 5 ++-- src/api/project_routes.py | 4 +-- src/api/toolchain_routes.py | 1 - src/blackboard/db.py | 1 - src/blackboard/models.py | 2 +- src/blackboard/operations.py | 2 -- src/cli/blackboard.py | 4 +-- src/daemon/bootstrap.py | 3 +-- src/daemon/counter.py | 2 +- src/daemon/dispatcher.py | 15 ++++++------ src/daemon/experience.py | 6 ++--- src/daemon/guardrails.py | 2 +- src/daemon/health.py | 7 +++--- src/daemon/inbox.py | 5 ++-- src/daemon/mail_notify.py | 2 +- src/daemon/review.py | 5 +--- src/daemon/skill_system.py | 3 +-- src/daemon/spawner.py | 26 ++++++++++---------- src/daemon/sse.py | 5 +--- src/daemon/ticker.py | 47 ++++++++++++++++++------------------ src/main.py | 23 +++++++++--------- src/utils.py | 1 - 23 files changed, 88 insertions(+), 101 deletions(-) diff --git a/src/api/blackboard_routes.py b/src/api/blackboard_routes.py index 8bf30d3..1f197b2 100644 --- a/src/api/blackboard_routes.py +++ b/src/api/blackboard_routes.py @@ -5,14 +5,14 @@ from __future__ import annotations import json import os from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any, Dict, Optional from fastapi import APIRouter, HTTPException, Query from src.blackboard.operations import Blackboard from src.blackboard.models import Task, Review from src.blackboard.queries import Queries -from src.blackboard.db import VALID_STATUSES, VALID_TRANSITIONS, COMMENT_TYPES, OUTPUT_TYPES +from src.blackboard.db import VALID_STATUSES, OUTPUT_TYPES from src.blackboard.registry import ProjectRegistry from src.utils import get_data_root @@ -240,7 +240,7 @@ async def update_status(project_id: str, task_id: str, body: Dict[str, Any]): }) if not bb.update_task_status(task_id, new_status, - agent=body.get("agent")): + agent=body.get("agent")): raise HTTPException(409, { "error": "transition_failed", "detail": f"Status update failed for {task_id}", @@ -265,6 +265,7 @@ async def update_status(project_id: str, task_id: str, body: Dict[str, Any]): # --- @mention 自动提取(#04) --- _KNOWN_AGENT_IDS: list = [] + def _init_agent_ids(): """从配置文件加载 Agent ID 列表""" global _KNOWN_AGENT_IDS @@ -279,6 +280,7 @@ def _init_agent_ids(): except Exception: _KNOWN_AGENT_IDS = [] + def _extract_mentions(text: str) -> list: """从文本中自动提取 @agent-id 格式的 mention""" import re @@ -317,8 +319,8 @@ async def add_comment(project_id: str, task_id: str, body: Dict[str, Any]): merged_mentions = list(set(explicit_mentions + auto_mentions)) cid = bb.add_comment(task_id, body["author"], comment_body, - comment_type=body.get("comment_type", "general"), - mentions=merged_mentions) + comment_type=body.get("comment_type", "general"), + mentions=merged_mentions) if merged_mentions: bb.record_mentions(cid, task_id, merged_mentions) # #10: SSE 通知前端黑板有新 comment @@ -424,8 +426,8 @@ async def get_decisions(project_id: str, task_id: str): async def add_decision(project_id: str, task_id: str, body: Dict[str, Any]): bb = _bb(project_id) did = bb.add_decision(task_id, body["decider"], body["decision"], - body["rationale"], - alternatives=body.get("alternatives")) + body["rationale"], + alternatives=body.get("alternatives")) return {"ok": True, "decision_id": did} @@ -435,7 +437,7 @@ async def add_decision(project_id: str, task_id: str, body: Dict[str, Any]): async def add_observation(project_id: str, task_id: str, body: Dict[str, Any]): bb = _bb(project_id) oid = bb.add_observation(task_id, body["observer"], body["body"], - severity=body.get("severity", "info")) + severity=body.get("severity", "info")) return {"ok": True, "observation_id": oid} diff --git a/src/api/mail_routes.py b/src/api/mail_routes.py index ef83690..4a6e6d9 100644 --- a/src/api/mail_routes.py +++ b/src/api/mail_routes.py @@ -9,7 +9,7 @@ from __future__ import annotations import json from datetime import datetime from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any, Dict, Optional from fastapi import APIRouter, HTTPException, Query @@ -36,6 +36,7 @@ def _get_valid_agents() -> set: # fallback:硬编码 return {"zhangfei-dev", "guanyu-dev", "zhaoyun-data", "jiangwei-infra", "pangtong-fujunshi", "simayi-challenger"} + router = APIRouter(prefix="/api/mail", tags=["mail"]) MAIL_PROJECT_ID = "_mail" @@ -222,7 +223,7 @@ async def send_mail(body: Dict[str, Any]): # A8: 只有原邮件的双方能回复(严格 1 对 1) if from_agent not in (orig_from, orig_to): - raise HTTPException(400, f"只有邮件的发送者或接收者可以回复") + raise HTTPException(400, "只有邮件的发送者或接收者可以回复") # A6/A7: 自动纠正 to → 原邮件发件者 to_agent = body.get("to", "").strip() diff --git a/src/api/project_routes.py b/src/api/project_routes.py index ff9d1f4..0e3d209 100644 --- a/src/api/project_routes.py +++ b/src/api/project_routes.py @@ -3,7 +3,7 @@ from __future__ import annotations from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any, Dict from fastapi import APIRouter, HTTPException, Query @@ -76,7 +76,7 @@ async def list_projects(): async def create_project(body: Dict[str, Any]): reg = _registry() try: - info = reg.create_project( + reg.create_project( body["id"], body["name"], agents=body.get("agents", []), description=body.get("description", ""), diff --git a/src/api/toolchain_routes.py b/src/api/toolchain_routes.py index 666708a..db3a596 100644 --- a/src/api/toolchain_routes.py +++ b/src/api/toolchain_routes.py @@ -166,7 +166,6 @@ def _calc_risk_level(changed_files: List[str]) -> str: # --------------------------------------------------------------------------- - MAIL_PROJECT_ID = "_mail" diff --git a/src/blackboard/db.py b/src/blackboard/db.py index f94c88c..821318e 100644 --- a/src/blackboard/db.py +++ b/src/blackboard/db.py @@ -4,7 +4,6 @@ from __future__ import annotations import sqlite3 from pathlib import Path -from typing import Optional def init_db(db_path: Path) -> None: diff --git a/src/blackboard/models.py b/src/blackboard/models.py index 617588a..b6a2dbc 100644 --- a/src/blackboard/models.py +++ b/src/blackboard/models.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Any, Dict, List, Optional +from typing import Any, List, Optional @dataclass diff --git a/src/blackboard/operations.py b/src/blackboard/operations.py index 2d75f3e..d27e32d 100644 --- a/src/blackboard/operations.py +++ b/src/blackboard/operations.py @@ -11,7 +11,6 @@ from typing import Any, Dict, List, Optional from .db import ( VALID_TRANSITIONS, - VALID_STATUSES, COMMENT_TYPES, EVENT_TYPES, OUTPUT_TYPES, @@ -693,7 +692,6 @@ class Blackboard: finally: conn.close() - # ── Checkpoint CRUD(M3) ── def create_checkpoint( diff --git a/src/cli/blackboard.py b/src/cli/blackboard.py index 853332a..368744a 100644 --- a/src/cli/blackboard.py +++ b/src/cli/blackboard.py @@ -10,7 +10,7 @@ from typing import List, Optional from src.blackboard.operations import Blackboard from src.utils import get_data_root -from src.blackboard.models import Task, Comment, Output, Decision, Observation, Review, Experience +from src.blackboard.models import Task, Review from src.blackboard.queries import Queries from src.blackboard.registry import ProjectRegistry @@ -262,7 +262,7 @@ def build_admin_parser() -> argparse.ArgumentParser: p_pc.add_argument("--description", default="") # project list - p_pl = sub.add_parser("project-list", help="List projects") + _ = sub.add_parser("project-list", help="List projects") # project archive p_pa = sub.add_parser("project-archive", help="Archive project") diff --git a/src/daemon/bootstrap.py b/src/daemon/bootstrap.py index e5d5aca..e142b51 100644 --- a/src/daemon/bootstrap.py +++ b/src/daemon/bootstrap.py @@ -11,8 +11,7 @@ A 类 Skill 由引擎确定性注入全文,不靠 Description 触发。 import logging import os -from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any, List logger = logging.getLogger("moziplus-v2.bootstrap") diff --git a/src/daemon/counter.py b/src/daemon/counter.py index b70c209..999655f 100644 --- a/src/daemon/counter.py +++ b/src/daemon/counter.py @@ -73,7 +73,7 @@ class ActiveAgentCounter: cd = seconds if seconds is not None else self._default_cooldown_seconds self._cooldown_until[agent_id] = time.time() + cd logger.info("Cooldown set for %s: %.0fs (until %.0f)", - agent_id, cd, self._cooldown_until[agent_id]) + agent_id, cd, self._cooldown_until[agent_id]) async def can_acquire(self, agent_id: str, session_id: str = "main") -> bool: """三层检查:cooldown → global → per agent → per session key""" diff --git a/src/daemon/dispatcher.py b/src/daemon/dispatcher.py index 4f9fa2b..2f0cdbb 100644 --- a/src/daemon/dispatcher.py +++ b/src/daemon/dispatcher.py @@ -12,9 +12,9 @@ Dispatcher 负责: from __future__ import annotations import json +import pathlib import logging import sqlite3 -from datetime import datetime from enum import Enum from pathlib import Path from typing import Any, Dict, List, Optional @@ -22,7 +22,7 @@ from typing import Any, Dict, List, Optional from src.blackboard.models import Task from src.blackboard.db import get_connection from src.daemon.spawner import AgentBusyError -from src.daemon.router import AgentRouter, RouteDecision +from src.daemon.router import AgentRouter logger = logging.getLogger("moziplus-v2.dispatcher") @@ -194,6 +194,7 @@ class Dispatcher: _task_id = task.id _mail_db = db_path _disp = self + def _mail_on_checks_passed(): nonlocal _mail_marked_working if not _disp._mail_auto_working(_task_id, _mail_db): @@ -203,8 +204,8 @@ class Dispatcher: # 构建 spawn message message = self._build_spawn_message(task, agent_id, project_config, - mode=decision.get("mode", ""), - spawn_type=action_type or "executor") + mode=decision.get("mode", ""), + spawn_type=action_type or "executor") # v2.7.2: on_complete 只含业务逻辑,不含 counter.release # counter.release 由 spawn_full_agent 内部的 wrapped_on_complete 保证 @@ -269,8 +270,8 @@ class Dispatcher: from src.blackboard.blackboard import Blackboard bb = Blackboard(_task_db) bb.add_comment(_task_id, "daemon", - f"@{task_row['assignee']} 审查结论: {verdict_str},请查看详情并决定接受或反驳", - comment_type="review") + f"@{task_row['assignee']} 审查结论: {verdict_str},请查看详情并决定接受或反驳", + comment_type="review") logger.info("Task %s: review verdict=%s, notified assignee=%s", _task_id, verdict_str, task_row["assignee"] if task_row else "?") # 不标 done,保持 review 状态 @@ -661,7 +662,7 @@ class Dispatcher: logger.error("Mail %s: failed to revert to pending: %s", task_id, e) def _mail_auto_complete(self, task_id: str, agent_id: str, - db_path: Path, must_haves: str, outcome=None) -> None: + db_path: Path, must_haves: str, outcome=None) -> None: """Mail 任务:on_complete 后自动标 done/failed(含幻觉门控)""" try: # 解析 performative diff --git a/src/daemon/experience.py b/src/daemon/experience.py index 663ef74..1745ded 100644 --- a/src/daemon/experience.py +++ b/src/daemon/experience.py @@ -14,7 +14,7 @@ import logging import re from datetime import datetime from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional logger = logging.getLogger("moziplus-v2.experience") @@ -68,7 +68,7 @@ class Experience: @classmethod def from_dict(cls, data: Dict[str, Any]) -> Experience: return cls(**{k: v for k, v in data.items() if k != "id"}, - experience_id=data.get("id")) + experience_id=data.get("id")) class ExperienceStore: @@ -284,7 +284,7 @@ class ExperienceDistiller: all_tags.append(task_type) results = self.store.search(tags=all_tags if all_tags else None, - query=query, limit=limit) + query=query, limit=limit) # 按置信度排序 results.sort(key=lambda e: e.confidence, reverse=True) diff --git a/src/daemon/guardrails.py b/src/daemon/guardrails.py index 8412b58..6de476d 100644 --- a/src/daemon/guardrails.py +++ b/src/daemon/guardrails.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging import re -from dataclasses import dataclass, field +from dataclasses import dataclass from pathlib import Path from typing import Any, Dict, List, Optional diff --git a/src/daemon/health.py b/src/daemon/health.py index 50ca567..6ce2c0d 100644 --- a/src/daemon/health.py +++ b/src/daemon/health.py @@ -9,9 +9,9 @@ from __future__ import annotations import json import logging from pathlib import Path -from typing import Any, Dict, Optional +from typing import Any, Dict -from src.blackboard.db import get_connection, init_db +from src.blackboard.db import get_connection from src.blackboard.queries import Queries logger = logging.getLogger("moziplus-v2.health") @@ -41,7 +41,6 @@ class HealthChecker: {"healthy": bool, "zombie": bool, "stale_ticks": int, "alert_written": bool, "resolved": bool} """ - db_key = str(db_path) result: Dict[str, Any] = { "healthy": True, "zombie": False, @@ -58,7 +57,7 @@ class HealthChecker: # 用 event count 变化判断是否有真实变更 conn = queries._conn() try: - total_events = conn.execute("SELECT COUNT(*) FROM events").fetchone()[0] + conn.execute("SELECT COUNT(*) FROM events").fetchone()[0] non_tick_events = conn.execute( "SELECT COUNT(*) FROM events WHERE event_type != 'daemon_tick' " "AND event_type != 'agent_zombie_detected'" diff --git a/src/daemon/inbox.py b/src/daemon/inbox.py index f76d9ca..eb25989 100644 --- a/src/daemon/inbox.py +++ b/src/daemon/inbox.py @@ -15,7 +15,6 @@ from __future__ import annotations import asyncio import json import logging -import os from pathlib import Path from typing import Any, Callable, Coroutine, Dict, List, Optional @@ -57,7 +56,7 @@ class InboxWatcher: self._running = True self._task = asyncio.create_task(self._loop()) logger.info("Inbox watcher started (path=%s, interval=%.1fs)", - self.inbox_path, self.watch_interval) + self.inbox_path, self.watch_interval) async def stop(self) -> None: """停止监听""" @@ -69,7 +68,7 @@ class InboxWatcher: except asyncio.CancelledError: pass logger.info("Inbox watcher stopped (processed=%d, errors=%d)", - self._total_processed, self._total_errors) + self._total_processed, self._total_errors) @property def is_running(self) -> bool: diff --git a/src/daemon/mail_notify.py b/src/daemon/mail_notify.py index 020415e..d1ee741 100644 --- a/src/daemon/mail_notify.py +++ b/src/daemon/mail_notify.py @@ -108,7 +108,7 @@ def notify_mail_failed(db_path: Path, original_mail_id: str, ) bb.create_task(notify_task) logger.info("Mail %s: sent failure notification to %s (original_sender=%s, reason=%s, notify_id=%s)", - original_mail_id, target_agent, from_agent, reason, notify_id) + original_mail_id, target_agent, from_agent, reason, notify_id) except Exception as e: logger.warning("notify_mail_failed: failed to send notification for mail %s: %s", original_mail_id, e) diff --git a/src/daemon/review.py b/src/daemon/review.py index 667923b..1bff5b1 100644 --- a/src/daemon/review.py +++ b/src/daemon/review.py @@ -8,15 +8,12 @@ from __future__ import annotations import json import logging -import re -from datetime import datetime from enum import Enum from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Tuple +from typing import Any, Callable, Dict, List, Optional from src.blackboard.models import Task from src.blackboard.operations import Blackboard -from src.blackboard.queries import Queries logger = logging.getLogger("moziplus-v2.review") diff --git a/src/daemon/skill_system.py b/src/daemon/skill_system.py index 7774763..a54afb4 100644 --- a/src/daemon/skill_system.py +++ b/src/daemon/skill_system.py @@ -10,12 +10,11 @@ from __future__ import annotations import json import logging -import re from dataclasses import dataclass, field from datetime import datetime from enum import Enum from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple logger = logging.getLogger("moziplus-v2.skill") diff --git a/src/daemon/spawner.py b/src/daemon/spawner.py index 7876435..e385210 100644 --- a/src/daemon/spawner.py +++ b/src/daemon/spawner.py @@ -7,6 +7,7 @@ Subagent: 占位(实际通过 OpenClaw Gateway API sessions_spawn,F17 完善) from __future__ import annotations import asyncio +import pathlib import json import logging import os @@ -15,7 +16,7 @@ from datetime import datetime from pathlib import Path from typing import Any, Dict, List, Optional -from src.blackboard.db import get_connection, init_db +from src.blackboard.db import get_connection logger = logging.getLogger("moziplus-v2.spawner") @@ -299,7 +300,7 @@ class AgentSpawner: project_id, agent_id) def _build_minimal_fallback(self, task_id, title, description, must_haves, - project_id, agent_id): + project_id, agent_id): """最小 fallback:只有任务上下文 + API 指令""" task_section = f"""## 任务 {title} @@ -311,7 +312,7 @@ class AgentSpawner: return task_section + "\n\n---\n\n" + api_section def _build_api_section(self, project_id: str, task_id: str, - agent_id: str) -> str: + agent_id: str) -> str: """构建 API 回写操作指令(BootstrapBuilder 模式下补充)""" # mail 任务直接 done,不走 review success_status = '"done"' if project_id == "_mail" else '"review"' @@ -337,8 +338,8 @@ curl -X POST http://{self.api_host}:{self.api_port}/api/projects/{project_id}/ta """ def _build_discussion_prompt(self, task_id: str, title: str, - description: str, must_haves: str, - project_id: str, agent_id: str) -> str: + description: str, must_haves: str, + project_id: str, agent_id: str) -> str: """构建讨论类 spawn prompt(§3.3 框架 + Boids)""" goal_snapshot = description or title constraints = must_haves or "(无特殊约束)" @@ -379,9 +380,8 @@ curl -X POST http://{self.api_host}:{self.api_port}/api/projects/{project_id}/ta return router.agent_profiles.get(agent_id) return None - def _build_mail_prompt(self, task_id: str, title: str, description: str, - must_haves: str, agent_id: str) -> str: + must_haves: str, agent_id: str) -> str: """构建 Mail 专用精简模板""" # 解析 must_haves 获取 from 和 performative from_agent = agent_id @@ -575,7 +575,7 @@ curl -X POST http://{self.api_host}:{self.api_port}/api/projects/{project_id}/ta stderr=asyncio.subprocess.PIPE, ) self._register_session(session_id, agent_id, task_id, proc.pid, - broadcast_task_ids=broadcast_task_ids) + broadcast_task_ids=broadcast_task_ids) logger.info("Spawned agent %s (session=%s, pid=%d)", agent_id, session_id, proc.pid) @@ -852,7 +852,7 @@ curl -X POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_ self.counter.set_cooldown(agent_id, seconds=60) logger.info("Crash cooldown set for %s: 60s (outcome=%s)", agent_id, outcome) elif outcome in ("compact_failed", "process_crash", "session_stuck", - "compact_hanging", "agent_error", "compact_interrupted") and self.counter: + "compact_hanging", "agent_error", "compact_interrupted") and self.counter: self.counter.set_cooldown(agent_id, seconds=300) # 5 分钟 logger.info("Error cooldown set for %s: 300s (outcome=%s)", agent_id, outcome) # F1: 不可恢复 outcome → 立刻标 failed + 写黑板 @@ -881,7 +881,7 @@ curl -X POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_ except Exception: pass - stderr_text = b"".join(stderr_chunks).decode("utf-8", errors="replace") + _ = b"".join(stderr_chunks).decode("utf-8", errors="replace") # 检查 session 状态 state = self._check_session_state(agent_id) @@ -1428,7 +1428,7 @@ curl -X POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_ return defaults def _update_retry_counts(self, db_path: Optional[Path], - task_id: Optional[str], counts: dict): + task_id: Optional[str], counts: dict): """将 retry counts 写回最新 task_attempt 的 metadata""" if not db_path or not task_id: return @@ -1488,8 +1488,8 @@ curl -X POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_ from src.blackboard.operations import Blackboard bb = Blackboard(db_path) cid = bb.add_comment(task_id, "daemon", - f"@pangtong-fujunshi 任务执行失败: {reason},请评估是否需要介入", - comment_type="system") + f"@pangtong-fujunshi 任务执行失败: {reason},请评估是否需要介入", + comment_type="system") bb.record_mentions(cid, task_id, ["pangtong-fujunshi"]) logger.info("Task %s: failure notified pangtong via comment+mention (reason=%s)", task_id, reason) except Exception as e: diff --git a/src/daemon/sse.py b/src/daemon/sse.py index d3f960b..e844bd7 100644 --- a/src/daemon/sse.py +++ b/src/daemon/sse.py @@ -9,14 +9,11 @@ from __future__ import annotations import asyncio import json import logging -import subprocess import uuid from datetime import datetime from enum import Enum -from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Set +from typing import Any, Dict, List, Optional -from src.blackboard.models import Event logger = logging.getLogger("moziplus-v2.sse") diff --git a/src/daemon/ticker.py b/src/daemon/ticker.py index 6a75264..251c0b8 100644 --- a/src/daemon/ticker.py +++ b/src/daemon/ticker.py @@ -21,7 +21,6 @@ from dataclasses import dataclass, field as dc_field from src.blackboard.operations import Blackboard from src.blackboard.db import get_connection -from src.blackboard.models import Task from src.daemon.spawner import AgentBusyError from src.blackboard.queries import Queries from src.blackboard.registry import ProjectRegistry @@ -35,6 +34,7 @@ class BroadcastRound: responded_agents: set = dc_field(default_factory=set) # 已返回反馈的 Agent(含 NO_REPLY) round_number: int = 0 # 当前第几轮(0=未开始,1=第1轮) + logger = logging.getLogger("moziplus-v2.ticker") @@ -391,7 +391,7 @@ class Ticker: MAX_ROUNDS = 5 # §4.5 防无限循环 async def _check_round_complete(self, db_path: Path, - project_id: str) -> List[str]: + project_id: str) -> List[str]: """检测 parent task 下所有 sub task 终态 → spawn 庞统 review 流程(§4.4): @@ -462,7 +462,7 @@ class Ticker: "Round %d review spawned for parent %s (subs: %s)", new_round, parent_id, summary ) - except Exception as e: + except Exception: logger.exception("Round check error for parent %s", parent_id) return reviewed @@ -531,9 +531,9 @@ Parent Task ID: {parent_task.id} """ async def _spawn_pangtong_review(self, parent_task, - review_prompt: str, - project_id: str, - new_round: int = 0) -> bool: + review_prompt: str, + project_id: str, + new_round: int = 0) -> bool: """Spawn 庞统进行 review 流程: @@ -543,7 +543,6 @@ Parent Task ID: {parent_task.id} """ try: agent_id = "pangtong-fujunshi" - session_id = f"review-{parent_task.id}-r{new_round}" # 构造 on_complete 回调:解析庞统结论,更新 parent 状态 async def _on_review_complete(aid: str, outcome: str): @@ -586,7 +585,7 @@ Parent Task ID: {parent_task.id} self._set_parent_reviewing(parent_task.id, project_id) return True return False - except Exception as e: + except Exception: logger.exception("Failed to spawn pangtong review for %s", parent_task.id) return False @@ -603,14 +602,14 @@ Parent Task ID: {parent_task.id} (parent_id,)) conn.commit() logger.info("Parent %s → reviewing (round review in progress)", - parent_id) + parent_id) finally: conn.close() except Exception: logger.exception("Failed to set parent %s to reviewing", parent_id) def _handle_review_conclusion(self, parent_id: str, project_id: str, - review_text: str, round_num: int): + review_text: str, round_num: int): """解析庞统 review 结论,更新 parent 状态 review_text 是庞统回复的文本(从 spawner session meta payloads 拼接)。 @@ -675,7 +674,7 @@ Parent Task ID: {parent_task.id} MENTION_MAX_RETRIES = 5 async def _process_mentions(self, db_path: Path, - project_id: str) -> List[str]: + project_id: str) -> List[str]: """扫描 pending mentions → spawn 被 @ 的 Agent 流程(§3.4): @@ -767,8 +766,8 @@ Parent Task ID: {parent_task.id} from src.blackboard.blackboard import Blackboard bb2 = Blackboard(rdb_path) bb2.add_comment(_t_id, "daemon", - f"@{t_row['assignee']} 审查结论: {verdict_str},请查看详情并决定接受或反驳", - comment_type="review") + f"@{t_row['assignee']} 审查结论: {verdict_str},请查看详情并决定接受或反驳", + comment_type="review") logger.info("Rebuttal: task %s still %s after rebuttal", _t_id, verdict_str) except Exception: logger.exception("Rebuttal on_complete failed for task %s", _t_id) @@ -805,7 +804,7 @@ Parent Task ID: {parent_task.id} # Agent 忙,不递增 retry_count,等下次 tick 自然重试 logger.info("Mention spawn skipped: %s busy, will retry next tick", agent_id) - except Exception as e: + except Exception: logger.exception("Mention processing error for agent %s", agent_id) for item in items: try: @@ -948,7 +947,7 @@ Parent Task ID: {parent_task.id} # ------------------------------------------------------------------ async def _dispatch_pending(self, db_path: Path, - project_id: str) -> List[str]: + project_id: str) -> List[str]: """扫描 pending 任务并调度 v3.0: 两条路径 @@ -1242,7 +1241,7 @@ Parent Task ID: {parent_task.id} return [aid for aid in all_agents if active.get(aid, 0) == 0] async def _dispatch_reviews(self, db_path: Path, - project_id: str) -> List[str]: + project_id: str) -> List[str]: """扫描 review 状态任务,检查是否有产出,调度审查 Agent""" # mail 任务不走 review 流程,直接跳过 if project_id == "_mail": @@ -1344,7 +1343,7 @@ Parent Task ID: {parent_task.id} ) reclaimed.append(task.id) logger.warning("Escalated %s: no taker after %d broadcasts", - task.id, retry_count) + task.id, retry_count) finally: conn.close() else: @@ -1423,7 +1422,7 @@ Parent Task ID: {parent_task.id} if ok: reclaimed.append(task.id) logger.info("Mail %s: ticker recheck found reply, marked done (%.1fm)", - task.id, elapsed) + task.id, elapsed) finally: conn.close() continue @@ -1440,7 +1439,7 @@ Parent Task ID: {parent_task.id} if ok: reclaimed.append(task.id) logger.warning("Task %s timed out (working %.1fm > %.1fm)", - task.id, elapsed, timeout_minutes) + task.id, elapsed, timeout_minutes) finally: conn.close() except (ValueError, TypeError): @@ -1501,7 +1500,7 @@ Parent Task ID: {parent_task.id} return True # 保守:查询失败假设有回复 def _check_recent_routing(self, db_path: Path, task_id: str, - action_type: str) -> bool: + action_type: str) -> bool: """检查最近 5 分钟内是否已 dispatch 过指定类型的路由(防重复)""" try: conn = get_connection(db_path) @@ -1579,11 +1578,11 @@ Parent Task ID: {parent_task.id} if recovery_report["total_recovered"] > 0: logger.info("Startup recovery: %d tasks recovered across %d projects", - recovery_report["total_recovered"], - len(recovery_report["projects"])) + recovery_report["total_recovered"], + len(recovery_report["projects"])) elif recovery_report["total_noop"] > 0: logger.info("Startup recovery: %d tasks kept as-is (no recovery needed)", - recovery_report["total_noop"]) + recovery_report["total_noop"]) else: logger.info("Startup recovery: no non-terminal tasks found, clean start") @@ -1629,7 +1628,7 @@ Parent Task ID: {parent_task.id} return recovered, noop_count def _determine_recovery_action(self, conn, task, status: str, - db_path: Path) -> Optional[str]: + db_path: Path) -> Optional[str]: """根据黑板线索决定恢复动作,返回 None 表示不需要干预""" task_id = task["id"] diff --git a/src/main.py b/src/main.py index 5754acc..fa48c66 100644 --- a/src/main.py +++ b/src/main.py @@ -191,7 +191,6 @@ async def lifespan(app: FastAPI): ) # ExperienceDistiller(经验自动蒸馏) - experience_config = config.get("experience", {}) experience_distiller = ExperienceDistiller( store=ExperienceStore(store_path=DATA_ROOT / "experiences.jsonl"), ) @@ -252,13 +251,13 @@ app.add_middleware( # API 路由注册 # --------------------------------------------------------------------------- -from src.api.blackboard_routes import router as blackboard_router -from src.api.checkpoint_routes import router as checkpoint_router -from src.api.daemon_routes import router as daemon_router -from src.api.project_routes import router as project_router -from src.api.sse_routes import router as sse_router -from src.api.mail_routes import router as mail_router -from src.api.toolchain_routes import router as toolchain_router +from src.api.blackboard_routes import router as blackboard_router # noqa: E402 +from src.api.checkpoint_routes import router as checkpoint_router # noqa: E402 +from src.api.daemon_routes import router as daemon_router # noqa: E402 +from src.api.project_routes import router as project_router # noqa: E402 +from src.api.sse_routes import router as sse_router # noqa: E402 +from src.api.mail_routes import router as mail_router # noqa: E402 +from src.api.toolchain_routes import router as toolchain_router # noqa: E402 app.include_router(blackboard_router) app.include_router(checkpoint_router) @@ -300,16 +299,16 @@ async def list_projects_compat(): DIST_DIR = Path(__file__).parent / "frontend" / "dist" if DIST_DIR.exists(): # v3.1: 缓存策略 - HTML 不缓存(确保新版本生效),JS/CSS 长缓存(Vite content hash 已处理) - import mimetypes _static_app = StaticFiles(directory=str(DIST_DIR), html=True) - + class CachedStaticFiles: """包装 StaticFiles,添加 Cache-Control 头""" def __init__(self, app): self._app = app - + async def __call__(self, scope, receive, send): original_send = send + async def patched_send(message): if message.get("type") == "http.response.start": headers = dict(message.get("headers", [])) @@ -321,5 +320,5 @@ if DIST_DIR.exists(): message["headers"] = list(headers.items()) await original_send(message) await self._app(scope, receive, patched_send) - + app.mount("/", CachedStaticFiles(_static_app), name="frontend") diff --git a/src/utils.py b/src/utils.py index 9c3dac7..cf0d20e 100644 --- a/src/utils.py +++ b/src/utils.py @@ -10,7 +10,6 @@ from __future__ import annotations import os from pathlib import Path -from typing import Optional def get_data_root() -> Path: -- 2.45.4 From 3b78aceeaff9ab6fcc262b3b7b433ba0531a8f19 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Tue, 9 Jun 2026 13:43:28 +0800 Subject: [PATCH 35/39] ci: trigger CI re-run with fixed ci.yml -- 2.45.4 From 51f8c68204c0d453aa746a97792a805c60e02d27 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Tue, 9 Jun 2026 14:17:32 +0800 Subject: [PATCH 36/39] fix(ci): use pyproject.toml instead of requirements.txt --- .gitea/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index d6673ce..4491ec3 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: - name: Setup Python run: | python3 -m venv /tmp/ci-venv-test - /tmp/ci-venv-test/bin/pip install --quiet -r requirements.txt + /tmp/ci-venv-test/bin/pip install --quiet -e . - name: Run tests (exclude E2E) run: | -- 2.45.4 From bdd18f4e3b4884cc3b001fc131cfae09111be67c Mon Sep 17 00:00:00 2001 From: cfdaily Date: Tue, 9 Jun 2026 14:26:17 +0800 Subject: [PATCH 37/39] ci: trigger CI with fixed test job -- 2.45.4 From da354061cccd5320d66f77604758b5e6fc1f7288 Mon Sep 17 00:00:00 2001 From: jiangwei-infra Date: Tue, 9 Jun 2026 14:37:36 +0800 Subject: [PATCH 38/39] chore: sync ci.yml from main (pytest instead of -e .) --- .gitea/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 4491ec3..51549e6 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: - name: Setup Python run: | python3 -m venv /tmp/ci-venv-test - /tmp/ci-venv-test/bin/pip install --quiet -e . + /tmp/ci-venv-test/bin/pip install --quiet pytest pytest-asyncio - name: Run tests (exclude E2E) run: | -- 2.45.4 From 8f4b4b67a84e18986d19c7f4949614ee4cadb0c8 Mon Sep 17 00:00:00 2001 From: jiangwei-infra Date: Tue, 9 Jun 2026 14:54:19 +0800 Subject: [PATCH 39/39] chore: sync ci.yml from main (add test deps) --- .gitea/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 51549e6..3bb1537 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: - name: Setup Python run: | python3 -m venv /tmp/ci-venv-test - /tmp/ci-venv-test/bin/pip install --quiet pytest pytest-asyncio + /tmp/ci-venv-test/bin/pip install --quiet fastapi pydantic pyyaml uvicorn requests pytest pytest-asyncio httpx - name: Run tests (exclude E2E) run: | -- 2.45.4