对齐实施方案 (2026-07-07)#
背景: 对照开源项目
qinshihu/itops-agent-platform的诊断-修复闭环, 补齐三块能力。 状态: 已实现 (分支feat/alignment-20260707, C→A→B 三 commit, 全套 356 passed)。
- C 定时巡检
5e2818e; A 分级门禁6ea3207; B 飞书一键审批110b28f。- 三块新能力默认开关全关 (INSPECTION_ENABLED / FEISHU_WS_ENABLED=false; 门禁工具缺失自动跳过), 合并后零行为变化。 基线: 治理闭环批次已在本地 main (
05a2f15)。
三块 (相互独立, 可任意顺序单独落地):
- A. 闭环验证门禁分级 —— 2 段验证 → 分级门禁 (补 service_health / metric_recovery)
- B. 手机 IM 一键审批 —— 飞书通知型 → 飞书长连接卡片内一键 (无需公网)
- C. 定时巡检 —— 纯被动触发 → 主动周期巡检入队
A. 闭环验证门禁分级#
现状锚点#
app/remediation/regression.py::verify_regression—— 现在只有 2 个 check:resource_state+alerts_resolved, 无重试。app/remediation/closed_loop.py::_run_regression—— committed 后调用, 只注入fetch_active_alerts。- 事务引擎
app/remediation/engine.py的verify已覆盖”资源态到目标态 + stabilization 稳定窗口”。
差距 (对照 verificationGates 五级)#
| 门禁级 | 现状 | 动作 |
|---|---|---|
| command_success | ✅ engine.apply | 不动 |
| service_health | 🟡 只看资源态 | 新增探针: 端口/HTTP/进程存活, 带重试 |
| metric_recovery | 🟡 只看告警 firing | 新增探针: 查 Prom 指标值 vs 阈值, 带重试 |
| baseline_comparison | ❌ | 不做 (性价比低) |
| impact_assessment | ❌ | 可选 (二期) |
设计 (注入式, 不改事务引擎)#
把 verify_regression 内部改成门禁列表驱动, 每级一个描述符:
@dataclass
class Gate:
name: str
required: bool = True
max_retries: int = 0
retry_interval_sec: float = 0.0
check: Callable[[], Awaitable[dict]] # -> {"ok": bool, "detail": str, ...}
async def verify_gates(gates: list[Gate], *, settle_sec: float, sleep=asyncio.sleep) -> RegressionResult:
# 逐级跑; required 且重试用尽仍失败 → 整体 fail 并终止后续级;
# 非 required 失败 → 记录跳过不判失败 (沿用现有 inconclusive 语义)python现有两个 check 平移成两个 Gate (resource_state/alerts_resolved), 对外 RegressionResult 结构不变, closed_loop 调用方零改动即可回落旧行为。
新增探针 (注入回调, 复用现有只读工具)#
probe_service_health(resource_uri)—— 复用 MCPnetwork_ping/host_*/container_*读工具; container 资源探端口, service 资源探systemctl is-active+ 端口。fetch_metric(promql, threshold, op)—— 复用 Prometheus 查询工具 (Loki/Prom 已接), 判value op threshold。
在 closed_loop.py::_build_alerts_fetcher 旁加 _build_health_prober / _build_metric_fetcher, 从 tools_by_name 懒加载, 工具缺失则该 Gate 降级为 inconclusive (不阻断)。
配置 (沿用命名风格)#
REMEDIATION_GATE_SERVICE_HEALTH_ENABLED=true
REMEDIATION_GATE_SERVICE_HEALTH_RETRIES=3
REMEDIATION_GATE_SERVICE_HEALTH_INTERVAL_SEC=10
REMEDIATION_GATE_METRIC_RECOVERY_ENABLED=true
REMEDIATION_GATE_METRIC_RECOVERY_RETRIES=2
REMEDIATION_GATE_METRIC_RECOVERY_INTERVAL_SEC=30plaintext默认全开但工具缺失自动降级; 关掉即回退现状 2 段。
文件#
- 改
app/remediation/regression.py(加 Gate/verify_gates, 旧函数保留为薄封装) - 改
app/remediation/closed_loop.py(加两个 prober builder + 组装 gates) - 改
app/config.py/.env.example(6 个配置项) - 新
tests/test_regression_gates.py(离线 fake 探针: 通过/重试后通过/重试用尽失败/工具缺失降级)
工作量 / 风险#
中偏小。风险低 —— 纯增强, 默认可降级, RegressionResult 契约不变, 旧路径可关开关回退。
B. 手机 IM 一键审批 (飞书长连接)#
现状锚点#
app/runtime/approval_notifier.py—— 裸 httpx 发通知卡片 (一个”去审批中心处理”跳转按钮), 无回调。app/api/v1/approvals.py:79 decide()—— 审批决策入口:approval_repository.decide()→claim_resume()→resume_diagnosis_graph()。app/runtime/approvals.py::ApprovalRepository—— decide / claim_resume / find_inflight 等已齐。app/main.py:46 lifespan—— checkpointer 在此挂载/关闭 (长连接常驻挂同处)。
关键洞察: 无公网也能收卡片回调#
原设计放弃”卡片内一键”的理由是本机无公网收不到回调。飞书长连接 (WebSocket) 模式让本地应用主动连飞书, 卡片按钮点击经长连接推回, 无需公网 IP / 内网穿透。这是原报告未考虑的路径。
设计#
- 卡片升级:
approval_notifier.send_card的 action 从单个跳转按钮 → 两个交互按钮”✅ 同意 / ❌ 拒绝”, 每个带value={"action":"approve|deny","req_id":...}; 保留”去审批中心”跳转按钮作为兜底。 - 长连接常驻: 新增
app/runtime/feishu_ws.py, 用lark-oapi的lark.ws.Client+EventDispatcherHandler注册 card action callback handler; 在main.pylifespan 里create_task起后台常驻, 关闭时取消。凭据复用app/core/connectors/feishu.py的 app_id/app_secret。 - 回调 handler (核心, 三道关):
- 身份校验: 回调带
open_id/user_id, 校验在审批人白名单 (FEISHU_APPROVER_OPEN_IDS) 内, 否则拒绝并回卡片提示。 - 幂等: 复用
ApprovalRepository.decide的WHERE status='pending'原子性 —— 重复点击/已决策直接短路。 - 落决策: 调与 decide() 端点共享的内部函数 (把
approvals.py的 decide 逻辑抽成app/runtime/approval_decide.py::apply_decision(req_id, decision, decided_by), 端点和 WS handler 都调它, 避免逻辑二写)。
- 身份校验: 回调带
- 卡片回执更新: 决策后用飞书
PATCH message把卡片更新为”已由 xxx 同意/拒绝”, 防重复点击 + 留痕。
与现有超时降级的衔接#
app/runtime/approval_timeout.py 的 escalate-then-deny 不变: 手机点了 → decide 写 pending→终态 → wait_once 立即返回非 timeout; 没点 → 照常催办→二次超时 deny。零冲突。
依赖 / 配置#
requirements.txt: + lark-oapi
FEISHU_WS_ENABLED=false # 默认关, 开了才起长连接 (灰度)
FEISHU_APPROVER_OPEN_IDS= # 逗号分隔审批人 open_id 白名单; 空=不接受卡片决策(只通知)plaintextFEISHU_WS_ENABLED=false 时行为 = 现状 (只通知+跳转), 完全不引入风险。
文件#
- 新
app/runtime/feishu_ws.py(长连接客户端 + card action handler) - 新
app/runtime/approval_decide.py(抽出的共享apply_decision) - 改
app/api/v1/approvals.py::decide(改调apply_decision, 行为不变) - 改
app/runtime/approval_notifier.py(卡片加同意/拒绝按钮 + 回执更新) - 改
app/main.py(lifespan 挂长连接) - 改
app/config.py/.env.example/requirements.txt - 新
tests/test_feishu_ws_handler.py(离线: 伪造 card action 事件 → 身份校验/幂等/非授权拒绝, mock 掉真实 WS 与飞书 API)
工作量 / 风险#
中。风险点: ① lark-oapi 新依赖 (锁版本); ② 长连接断线重连 (SDK 自带, 需验证); ③ 身份白名单必须先配, 否则任何人点按钮都能批 —— 默认关 + 白名单空=不接受决策 双保险。回退: FEISHU_WS_ENABLED=false。
C. 定时巡检#
现状锚点#
- 纯被动:
POST /diagnose/submit/ Alertmanager webhook 触发。 app/queue/redis_streams.py:128 enqueue_task—— 复用其入队。- 无 scheduler。
设计#
新增轻量周期调度器 (不引 APScheduler, 复用 asyncio + Redis 防多 worker 重复触发):
app/runtime/inspection_scheduler.py—— lifespan 起后台 loop; 每个巡检项{name, interval_sec, query/target, mode}; 到点用 Redis SETNX + TTL 抢占 (多 worker 只有一个真触发), 然后enqueue_task入队一条诊断任务 (低优先级)。- 巡检项配置来源: 先用
.env/ 配置文件静态定义 (如”每 5min 巡检核心容器健康”); 二期可做成 DB 表 + API 管理。 - 巡检结果走现有诊断链路 → 落库 → 有问题才走处置审批 (复用现有闭环, 不新造)。
配置#
INSPECTION_ENABLED=false # 默认关
INSPECTION_INTERVAL_SEC=300
INSPECTION_TARGETS=container:core-*,host:cpu # 简单声明式, 二期转 DBplaintext文件#
- 新
app/runtime/inspection_scheduler.py - 改
app/main.py(lifespan 起调度 loop, 仅当INSPECTION_ENABLED) - 改
app/config.py/.env.example - 新
tests/test_inspection_scheduler.py(离线: fake clock + fake enqueue, 验证 SETNX 去重/到点触发/关开关不触发)
工作量 / 风险#
小。风险低 —— 默认关, 复用现有队列与诊断链路, 不改核心路径。多 worker 重复触发由 Redis SETNX 兜底。
建议实施顺序与依赖#
| 顺序 | 块 | 理由 | 依赖 |
|---|---|---|---|
| 1 | C 定时巡检 | 最小、风险最低、体感最明显, 且不碰核心 | 无 |
| 2 | A 门禁分级 | 中小, 纯增强, 提升闭环可信度 | 无 (但需确认可用的 Prom/health 读工具名) |
| 3 | B 手机一键审批 | 最有”闭环感”但引新依赖, 放最后稳一点 | 需先配飞书 app 凭据 + 审批人 open_id |
三块互不依赖, 也可只挑其一。每块都遵循本项目惯例: 新能力默认开关关/可降级、单测全离线、不动 .env 只加 .env.example 占位。
统一验收口径 (三块共用)#
- 每块独立分支 + 独立 commit, 离线 pytest 全绿再合。
- 涉及
.env的开关默认值 = 关闭 / 现状行为, 保证合并后零行为变化, 需显式开启才生效。 - B、C 的后台常驻任务必须在 lifespan 关闭钩子里可靠取消 (对齐 checkpointer 的挂载/关闭对称)。