AgentEvalTool/.scratch/v111-architecture-scan.md
sinohqb a03067720b
All checks were successful
CI / test (push) Successful in 4m18s
test(intelligent-eval): add openclaw_client + webhook tests (#5)
T5 OpenClawClient subprocess args: docker exec cmd + token-last placement,
   cron add/rm/list params, JSON parse success + failure paths.
T6 webhook: marked sent on 2xx, failure does not raise; xfail guards expose
   missing retry and missing dedupe (.scratch/v111-architecture-scan.md §6.2).
2026-08-13 02:56:32 +08:00

153 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# v1.1.1 智能评估模块架构扫描
> 生成2026-08-13 用途v1.1.1(收尾 + 测试加固 + 架构深化)的输入文档,驱动 Gitea tickets 拆解
> 范围:`backend/agenteval/intelligent_eval/` 为核心 + 外部边界web routers、openclaw_client、前端、Worker Skill
> 依据grilling 共识(测试先行为锁定 → 分阶段深化 → 统一发布 v1.1.1
---
## 1. 模块现状
`intelligent_eval/` 共 13 文件 / 2883 行v1.1.0 新增功能模块,生产单进程 FastAPI + SQLite
| 文件 | 行数 | 职责 |
|---|---|---|
| repository.py | 459 | 3 个 Repository 类Eval/Session/Message+ CAS 条件写 |
| lifecycle.py | 354 | 智能评估生命周期create/plan/approve/reject/cancel/report + session 会话) |
| cron_pool.py | 310 | Cron 池管理initialize/scale/auto_scale/sync/stuck 检测) |
| decision.py | 276 | 自主决策(时段/欠账/严重度 → execute/wait/analyze |
| task_queue.py | 247 | 任务队列(扫描入队/优先级/next/assign/complete |
| alerts.py | 216 | 告警规则与历史 |
| fault_tolerance.py | 204 | 故障恢复stuck 处理/对账/双端重启恢复) |
| read_model.py | 196 | 读模型投影list/detail/report |
| openclaw_client.py | 177 | OpenClaw 容器 CLI 客户端docker exec |
| report.py | 130 | 报告渲染 |
| metrics.py | 117 | 指标聚合 |
| config_snapshot.py | 109 | 配置快照 |
| models.py | 87 | 领域模型 |
**依赖关系要点**`lifecycle`→repository`read_model`→repository`cron_pool`→openclaw_client + **task_queue267 行运行时 import complete_task**`fault_tolerance`→cron_pool+task_queue+openclaw_client`decision`/`task_queue` 直接 `session.exec` 查库。
**外部边界**
- **Web**`web/routers/intelligent_evals.py`(前缀 `/api/intelligent-evals`+ `web/routers/openclaw_cron_pool.py`(前缀 `/api/openclaw`
- **OpenClaw 客户端**docker exec `openclaw` CLI 管理容器内 cron**硬编码** `gateway_token="agenteval-openclaw-token-2026"`
- **Worker Skill**OpenClaw 容器内 cron worker 经 docker 网络直连后端 `tasks/next|assign|complete` + `heartbeat`**不经前端**
- **前端**`IntelligentEvals.tsx`(读模型 `useIntelligentEvalRead`,统一轮询 + stale 过滤);`CronPoolMonitor.tsx`**自带 setInterval 5s**,无统一 hook
---
## 2. Seam 泄漏清单(深化候选)
| # | 位置 | 泄漏 | 影响 | 深化方向 |
|---|---|---|---|---|
| **S1** | `task_queue.py``_is_slot_due`/`_calculate_session_deficit`/`_calculate_priority`vs `decision.py``_parse_time_slot`/`_get_current_slot`/`_count_sessions_in_slot`/`_has_high_severity_issues` | **时段/欠账/严重度领域知识重复**"8-10h" 解析两份、时段内 session 统计两份、优先级/欠账各自实现 | 改口径需改两处,易漂移 | 收敛为 shared 领域模块,两模块消费同一实现 |
| **S2** | `web/routers/intelligent_evals.py` 后段tasks/next 内联拼装、assign/complete、decision-logs POST/GET 直接 ORM at L332/360+ `openclaw_cron_pool.py`scale 决策混入、heartbeat ORM at L86/87 | **HTTP 层直接拼装 ORM/领域逻辑**;前段干净 HTTP→lifecycle后段落差 | decision-logsWorker 审计面)回归风险高 | 收进 service/repositoryrouter 只做参数校验+委托 |
| **S3** | `decision.py`、`task_queue.py` 直接 `session.exec``IntelligentEvalSessionDB` | **纯决策/队列逻辑与 SQLite 直接耦合**,绕过 repository/read_model | 可测试性受限(需真实 DB逻辑难单测 | 注入读模型/查询接口,逻辑层纯化 |
| **S4** | `cron_pool.py:267` `handle_stuck_cron``from ... import complete_task` | **stuck→任务结算横跨两模块**(运行时 import | 卡死恢复的结算顺序分散 | 把 stuck→requeue/complete 结算收进 task_queue/统一作业模块 |
| **S5** | `openclaw_client.py` 硬编码 `gateway_token`/`container_name` + subprocess | **外部系统边界配置硬编码**token 明文进进程列表;无独立测试(全 mock | 换 token/环境需改代码;边界无守护 | token/容器走 settings 注入subprocess 参数/解析加契约测试 |
| **S6** | `pages/CronPoolMonitor.tsx` 自带 setInterval | **前端轮询不一致**Cron 池不走统一 usePolling无 stale 过滤;无测试 | 与智能评估读模型行为漂移;卸载/不可见不清理 | 抽统一轮询 hook + 页面测试 |
| **S7** | Worker Skill 回路tasks/next/assign/complete + heartbeat | **关键链路无端到端守护**heartbeat→busy→kill→requeue 故障链路缺失) | 深化动代码时最怕破坏的回环 | 补 e2e 测试 + router 下沉后契约守卫 |
> 非候选:`repository.py` 单文件多类——v2 方法论明确"不因文件长度拆分 Repository文件移动不算 deepening",不处理。
---
## 3. 测试覆盖矩阵
### 现有覆盖智能评估相关10 单元 + 9 集成 + 1 前端)
| 主题 | 单元 | 集成 | 前端 | 评价 |
|---|---|---|---|---|
| 生命周期状态机 | ✅ model 测试 | ✅ evals_api | — | 完善 |
| 决策逻辑 | ✅ decision 测试 | — | — | 分支覆盖好 |
| 任务队列 | ✅ task_queue 测试 | ✅ task_queue_api | — | happy path 完整 |
| Cron 池管理 | ✅ cron_pool 测试 | ✅ cron_pool_api | — | 扩缩容路径完整 |
| 故障恢复 | ✅ fault_tolerance | ✅ fault_tolerance_e2e | — | 双端恢复均有 |
| 告警 | ✅ alerts 测试 | ✅ metrics_alerts_api | — | 规则覆盖好 |
| 快照 | ✅ config_snapshot | ✅ config_snapshot_api | — | 完善 |
| 读模型 | ✅ read_model 测试 | — | ✅ intelligentEval.test | 有 stale 过滤 |
| Worker Skill | — | ✅ worker_skill_api | ❌ | CRUD 有,回路无 e2e |
| **Cron 池监控前端** | — | — | ❌ **0 测试** | 5s 轮询无守卫 |
## 6. 实施期间发现的新问题v1.1.1 P0 期间)
> 这里记录在按本扫描实施时**新暴露**的真实问题(不在原 seam/T 列表里),是测试防护网的首要价值。
### 6.1 `assign_task` 缺原子 CAS**真 bug**T1 暴露)
**症状**`tests/integration/test_worker_task_resilience.py::test_concurrent_assign_via_two_sessions` 失败。
两个独立 session两个 worker 进程)并发调用 `assign_task` 同一 pending task 时,**两个都返回 True**——任务被双认领。
**根因**`backend/agenteval/intelligent_eval/task_queue.py` 的 `assign_task` 是非原子的 *读-检查-写*
```python
task = session.get(...); if task.status != "pending": return False
task.status = "assigned"; session.commit()
```
SQLite`check_same_thread=False` + StaticPool下两个 session 各自读到 `pending`、各自检查通过、各自 commit无行级锁/CAS。
**影响**Worker Skill 端点 `POST /tasks/{id}/assign` 可被双认领 → 两个 cron 同时执行同一任务 → 重复执行、可能双写 report/decision log。当前顺序 assign单 session通过只是因为 session 内 serializable。
**修复方向**(待开独立 issue不在 #3 范围):
- `assign_task` 改为 `UPDATE ... WHERE id=? AND status='pending'`,按 affected_rows 判定(真正的 CAS
- 同样审计 `complete_task` 的并发(虽然 converge 测试通过,但非原子写)。
- `get_next_task``SELECT ... LIMIT 1` 也非原子,应改 `UPDATE ... WHERE status='pending' ... RETURNING` 抢占式。
### 6.2 Webhook 缺重试 + 缺去重(**两个真缺口**T6 暴露)
**症状**`test_webhook_retries_on_failure` 与 `test_webhook_dedupes_repeat_triggers` xfail。
`backend/agenteval/intelligent_eval/alerts.py::AlertManager._send_webhook`
```python
try:
httpx.post(self.webhook_url, json=payload, timeout=5.0); ...
except Exception as e:
_logger.error(f"Failed to send webhook: {e}") # 吞掉,不重试
```
- **无重试**:首次失败仅 log`webhook_sent` 保持 `False`,无后续尝试。
- **无去重**`check_alerts` 每次触发都会调 `_send_webhook(alert)`;同一 alert 在恢复前可被重复发送。
**影响**:瞬时网络抖动丢告警;告警风暴期间 webhook 被反复打。
**修复方向**(待开独立 issue不在 #5 范围):
- 重试:失败后排队 N 次重试(指数退避),或记入 outbox 表后台重发。
- 去重:`alert.webhook_sent=True` 后跳过;或加 `webhook_in_flight` 锁。
**当前状态**:两个 xfail 守卫在 `test_openclaw_client_and_webhook.py`CI 不阻塞;修复后移除 xfail 即转绿。
### 薄弱/缺失点(加固清单 T
### 薄弱/缺失点(加固清单 T
| # | 缺口 | 位置 |
|---|---|---|
| T1 | Worker Skill 三端点**并发/竞争**(双 worker 拉 next 不得同 taskcomplete 与 stuck 同时发生不丢任务) | task_queue / worker 回路 |
| T2 | **CronPoolMonitor 页面/hook 测试**unmount 清理、不可见暂停轮询) | 前端 |
| T3 | **router 内嵌 ORM 契约测试**decision-logs、heartbeat 一旦下沉的守卫) | 集成 |
| T4 | **auto-scale 振荡**(连续调度不抖动翻转) | cron_pool |
| T5 | **OpenClawClient 单元测试**subprocess 参数、token 位置、JSON 解析失败路径) | openclaw_client |
| T6 | **webhook 失败重试/去重**(已发送但失败、重发不重复建 alert | alerts |
| T7 | **告警→扩缩容端到端联动**check_alerts → auto_scale 贯穿链路) | 集成 |
| T8 | **decision-logs 不可变性/去重**(同 eval+type+context 不重复context 修改追加非覆盖) | worker_skill |
| T9 | **stuck 检测临界值边界**last_active_at 临界) | cron_pool |
| T10 | **e2e 故障链路**heartbeat→busy→kill→requeue 完整闭环) | 集成 e2e |
---
## 4. 深化 Phase 框架v2 风格,内容待 tickets 细化)
| Phase | 内容 | 对应 seam |
|---|---|---|
| **P0 测试加固(防护网)** | 覆盖矩阵清零 + T1T10 补齐;不改生产代码 | — |
| **P1 收敛领域知识** | 时段/欠账/严重度 → shared 模块stuck 结算收敛 | S1, S4 |
| **P2 解耦 DB** | decision/task_queue 走读模型/repository逻辑纯化 | S3 |
| **P3 下沉 Router 逻辑** | decision-logs/heartbeat/tasks 收进 service | S2 |
| **P4 边界收尾 + ADR** | openclaw_client 配置化、前端轮询统一、删旧、记录 ADR | S5, S6, S7 |
> 顺序依赖P0 先行(锁行为)→ P1收敛→ P2解耦为 P3 提供 repository 能力)→ P3router 下沉)→ P4边界+ADR。每阶段 CI 全绿、可独立合入。
---
## 5. 收尾发布v1.1.1
- 测试/深化全部合入 main、CI 全绿后统一发布:`bump 1.1.0 → 1.1.1` → 完整回归(后端全套 + 前端 + tsc + 生产构建 + ruff + 迁移往返)→ 生产部署演练(副本验证迁移/恢复/读取)→ **CD 自动部署**到生产1.0.0→1.1.1)→ 健康检查 + 关键路径 smoke
- 附带收尾CI 镜像 push 到私有 registry持久化