# domainCheck Ops Observability Contract ## 1. 目标 这份文档用于冻结海外单脑控制面里“看现场”这一层的正式 contract。 它不负责: - 发布决策 - Driver 推荐 - Playbook 编排 它负责回答 3 类问题: 1. 哪些节点现在真正处于执行现场 2. 这些节点最近巡检收口结果是什么 3. Node Agent 回执队列现在是否健康 也就是说,这份 contract 的职责是: > 让页面、CLI、Codex 看到同一份现场事实,而不是各自从日志和状态字串里猜 --- ## 2. 入口接口 建议把下面这些接口视为同一观察面的一组正式入口: - `GET /api/v1/ops/overview` - `GET /api/v1/ops/inspection-overview` - `GET /api/v1/ops/activity-stream` - `GET /api/v1/ops/nodes/{node_code}/handover` - `GET /api/v1/ops/nodes/{node_code}/delivery-queue` - `GET /api/v1/ops/nodes/{node_code}/delivery-queue/records` - `POST /api/v1/ops/nodes/{node_code}/delivery-queue/flush` - `POST /api/v1/ops/nodes/{node_code}/delivery-queue/replay` - `POST /api/v1/ops/nodes/{node_code}/delivery-queue/records/{record_id}/replay` - `POST /api/v1/ops/nodes/{node_code}/delivery-queue/records/{record_id}/discard` 配套关系: - `overview` - 首页收口 - `inspection-overview` - 节点巡检收口 - `activity-stream` - 最近异常活动 - `handover` - 单节点接管阻断与下一步建议 - `delivery-queue` - Node Agent 回执健康 正式要求: - 页面不能自己再定义第 5 套“观察口径” - CLI 不应重新从数据库拼现场 - Codex 不应通过自然语言日志推断核心状态 --- ## 3. Execution Scene Contract 来源: - `GET /api/v1/ops/overview` - `overview.execution_scene` 最小字段: ```json { "summary": "当前存在 2 台有效执行节点,其中 1 台正在领任务,1 台在线待命。", "dispatch_active_nodes": [ { "node_code": "mainland-worker-01", "region": "mainland", "role": "worker", "status": "busy", "participation_state": "dispatch_active", "participation_label": "正在执行", "participation_reason": "当前存在已领任务和执行中工作负载", "current_load": 9, "items_claimed": 9, "items_running": 5, "items_completed_recent": 14 } ], "recent_only_nodes": [], "standby_nodes": [ { "node_code": "mainland-controller-01", "region": "mainland", "role": "control", "status": "online", "participation_state": "standby", "participation_label": "在线待命", "participation_reason": "节点在线、有效,但当前没有领任务、执行中任务或近窗吞吐", "current_load": 0 } ], "load_syncing_nodes": [], "counts": { "dispatch_active": 1, "recent_only": 0, "standby": 1, "load_syncing": 0 } } ``` 分组语义必须固定: - `dispatch_active_nodes` - 当前正在领任务、执行任务、或仍有实时工作负载 - `recent_only_nodes` - 当前不再运行,但近窗刚有吞吐 - `standby_nodes` - 在线、有效,但当前不参与 - `load_syncing_nodes` - 节点与控制面状态尚未完全同步 正式约束: - “在线”不等于“参与检测” - “有效执行节点”不等于“正在跑” - 页面展示、CLI 摘要、Codex 判断必须沿用这 4 类分组 节点参与态附加字段也应固定: - `participation_state` - 机器判断主键,推荐值: - `dispatch_active` - `recent_only` - `standby` - `load_syncing` - `participation_label` - 直接给页面、CLI、Codex 展示的人类可读标签 - `participation_reason` - 用一句稳定摘要解释为什么节点落在当前分组 正式要求: - 页面必须直接区分: - 在线但未参与 - 正在执行或领任务 - 近窗刚参与但当前已收口 - 新消费方不允许再从: - `status` - `current_load` - `items_running` 手工推断“这个节点到底算不算正在参与” --- ## 4. Log Sync Runtime Contract 来源: - `overview.execution_scene.log_sync` - 或 `overview.log_sync` 最小字段: ```json { "status": "partial_coverage", "status_label": "部分覆盖", "mode": "key", "mode_label": "关键回传", "enabled": true, "source_nodes": [ "mainland-worker-01" ], "source_node_summaries": [ { "node_code": "mainland-worker-01", "line_count": 42, "key_line_count": 42, "full_line_count": 0, "last_at": "2026-04-18 06:40:00", "last_line": "域名检测完成: demo.com" } ], "covered_participating_node_summaries": [ { "node_code": "mainland-worker-01", "region": "mainland", "role": "worker", "participation_state": "dispatch_active", "participation_label": "正在执行", "participation_reason": "当前存在已领任务和执行中工作负载", "line_count": 42, "key_line_count": 42, "full_line_count": 0, "last_at": "2026-04-18 06:40:00", "last_line": "域名检测完成: demo.com" } ], "missing_participating_node_summaries": [ { "node_code": "mainland-worker-02", "region": "mainland", "role": "worker", "participation_state": "dispatch_active", "participation_label": "正在执行", "participation_reason": "当前存在已领任务和执行中工作负载", "missing_reason": "no_sample" } ], "covered_participating_nodes": 1, "participating_nodes_total": 2, "missing_sample_node_codes": [ "mainland-worker-02" ], "last_line_at": "2026-04-18 06:40:00", "summary": "当前已覆盖 1/2 台参与节点,建议继续观察未回传样本节点。" } ``` 推荐状态: - `disabled` - `waiting_sample` - `partial_coverage` - `healthy` - `full_capture` 这层是“现场可见性”最关键的辅助状态。 正式要求: - 如果存在参与节点但日志回传还没开,优先推荐开关键回传 - 如果日志回传已开但没有样本,优先推荐抓 Worker 日志 - 页面和 Codex 都不能再自己定义另一套日志覆盖结论 节点级日志覆盖字段也应固定: - `source_nodes` - 为兼容旧页面保留的节点编码列表 - `source_node_summaries` - 当前已有日志样本的节点级统计对象 - `covered_participating_node_summaries` - 已参与且已拿到样本的节点对象 - `missing_participating_node_summaries` - 已参与但尚未拿到样本的节点对象 `source_node_summaries[]` 至少应返回: - `node_code` - `line_count` - `key_line_count` - `full_line_count` - `last_at` - `last_line` `missing_participating_node_summaries[]` 至少应返回: - `node_code` - `region` - `role` - `participation_state` - `participation_label` - `participation_reason` - `missing_reason` 推荐 `missing_reason`: - `no_sample` - `delayed_flush` - `filtered_out` 兼容要求: - 旧页面仍可继续消费: - `source_nodes` - `missing_sample_node_codes` - 新页面、CLI、Codex 应优先消费: - `source_node_summaries` - `covered_participating_node_summaries` - `missing_participating_node_summaries` 补充节点级现场入口: - `GET /api/v1/ops/nodes/{node_code}/scene-log` 最小字段: ```json { "node_code": "mainland-worker-01", "status": "healthy", "status_label": "关键覆盖", "mode": "key", "mode_label": "关键回传", "log_sync_enabled": true, "summary": "节点当前正在参与检测,已保留 42 条现场日志样本。", "node": { "region": "mainland", "role": "worker", "status": "busy", "current_load": 6, "last_heartbeat_at": "2026-04-18 06:40:00" }, "participation": { "detect_participating": true, "participation_state": "claimed", "participation_label": "已领待跑", "participation_bucket": "dispatch_active", "participation_reason": "已领取任务,等待线程继续执行。" }, "source_summary": { "line_count": 42, "key_line_count": 42, "full_line_count": 0, "last_at": "2026-04-18 06:40:00", "last_line": "[2026-04-18 06:40:00] [mainland-worker-01] 域名检测完成: demo.com" }, "records_total": 42, "records_visible": 80, "records": [ { "created_at": "2026-04-18 06:38:00", "node_code": "mainland-worker-01", "message": "开始检测域名: demo.com", "mode": "key", "line": "[2026-04-18 06:38:00] [mainland-worker-01] 开始检测域名: demo.com" } ] } ``` 节点级入口的正式语义: - 这是“看某一台节点当前现场”的正式入口,不允许页面再自己从聚合样本中二次猜测 - `status` 至少覆盖: - `healthy` - `full_capture` - `historical_sample` - `waiting_sample` - `missing_sample` - `disabled` - `standby` - `unknown` - 当节点当前在线但未参与时,必须明确返回 `standby` - 当节点正在参与但还没有样本时,必须明确返回 `waiting_sample` - 当节点已有历史样本但当前不在参与面中时,必须明确返回 `historical_sample` --- ## 5. Inspection Overview Contract 来源: - `GET /api/v1/ops/inspection-overview` - `overview.inspection` 最小字段: ```json { "status": "attention", "status_label": "待处理", "summary": "存在 1 台节点最近缺少 Worker 日志收口。", "counts": { "healthy_nodes": 1, "attention_nodes": 1, "problem_nodes": 0 }, "rows": [ { "node_code": "mainland-worker-01", "problem_kind": "missing_worker_logs", "problem_label": "缺少 Worker 日志收口", "problem_level": "warning", "recommended_action_code": "open_worker_logs", "ui_intent": { "kind": "node_inspection_detail", "node_code": "mainland-worker-01" }, "latest_health_snapshot": {}, "latest_worker_logs": {}, "latest_diagnostics": {} } ] } ``` 设计要求: - 巡检不是几条离散 job 记录 - 必须按节点重新聚合 - 必须直接给出: - `problem_kind` - `problem_label` - `problem_level` - `recommended_action_code` - `ui_intent` 这样页面、CLI、Codex 才能真正消费“节点巡检结论”,而不是再从几十条 job 记录里回推问题。 --- ## 6. Inspection Row Contract `rows[]` 中每个节点建议最少包含: - `node_code` - `region` - `role` - `effective_worker` - `problem_kind` - `problem_label` - `problem_level` - `recommended_action_code` - `ui_intent` - `latest_health_snapshot` - `latest_worker_logs` - `latest_diagnostics` 其中 3 类最近结果建议统一结构: ```json { "status": "success", "status_label": "成功", "occurred_at": "2026-04-18 06:35:00", "job_code": "ops-20260418-001", "summary": "最近一次 Worker 日志收集成功,共采样 120 行。" } ``` 正式边界: - `logs.collect` 默认指 Worker 日志收口 - Node Agent 辅助排障日志不应混成同一种巡检结果 --- ## 7. Delivery Queue Summary Contract 来源: - `GET /api/v1/ops/nodes/{node_code}/delivery-queue` 最小字段: ```json { "node": { "node_code": "mainland-worker-01", "title": "mainland-worker-01" }, "summary": { "state": "dead_letter", "label": "死信", "reason": "当前有 2 条记录进入死信。", "pending_count": 3, "dead_letter_count": 2, "oldest_pending_at": "2026-04-18 06:20:00", "oldest_dead_letter_at": "2026-04-18 06:18:00" }, "record_visibility": "head_only", "capabilities": { "can_flush": true, "can_replay": true, "can_discard": true } } ``` 推荐状态: - `healthy` - `retrying` - `dead_letter` - `stalled` 正式约束: - 控制面要明确告诉操作者当前是 `head_only` 还是 `full_projection` - 不允许页面假装能列出远端完整目录 - 不允许因为能看见死信就绕过 `ops job` 直接改文件 --- ## 8. Delivery Queue Record Contract 来源: - `GET /api/v1/ops/nodes/{node_code}/delivery-queue/records` 当前阶段建议最小字段: ```json { "records": [ { "record_id": 101, "state": "dead_letter", "state_label": "死信", "request_id": "req-20260418-001", "kind": "job_complete", "created_at": "2026-04-18 06:18:00", "updated_at": "2026-04-18 06:19:00", "summary": "某次任务完成回执多次补发失败。", "visibility": "head_only" } ], "summary": { "returned_records": 1, "record_visibility": "head_only" } } ``` 当前阶段要求: - 单条 `replay / discard` 只针对控制面可见记录 - 当前 `head_only` 不等于未来不能扩展 - 以后若进入 `full_projection`,扩的是可见范围,不是重写操作模型 --- ## 9. Delivery Queue Action Contract 来源: - `flush` - `replay` - `record replay` - `record discard` 正式要求: - 所有动作都必须落成正式 `ops job` - 返回体要能告诉调用方: - 是否已受理 - 生成了什么 job - 建议跳转到哪里观察 最小返回体建议: ```json { "accepted": true, "job_code": "ops-20260418-queue-001", "ui_intent": { "kind": "job_events", "job_code": "ops-20260418-queue-001" } } ``` 这里的边界必须比“临时脚本”更严格: - `discard` 必须带原因 - `replay` 应支持限流 / limit - `flush` 应支持显式 `requested_by` --- ## 10. Activity Stream Contract 来源: - `GET /api/v1/ops/activity-stream` 最小字段建议: ```json { "items": [ { "kind": "ops_job", "activity_key": "ops-job:101", "title": "logs.collect", "subtitle": "ops-20260418064500-a1b2c3", "summary": "目标节点 mainland-worker-01 / 发起 web-ui / 方式 Node Agent", "summary_text": "目标节点 mainland-worker-01 / 发起 web-ui / 方式 Node Agent", "status": "running", "status_label": "执行中", "execution_mode": "remote-agent", "execution_mode_label": "Node Agent", "target_node_codes": [ "mainland-worker-01" ], "occurred_at": "2026-04-18 06:45:08", "ui_intent_kind": "job_events", "focus_ref": { "kind": "ops_job", "job_id": 101, "job_code": "ops-20260418064500-a1b2c3", "action": "logs.collect", "target_node_code": "mainland-worker-01" } } ], "summary": { "status": "running", "status_label": "执行中", "summary_text": "最近活动里存在正在推进的现场、任务、编排或回传。", "filtered_total": 6, "unfiltered_total": 18, "latest_at": "2026-04-18 06:45:08" } } ``` 正式要求: - `activity-stream` 不是页面内部 table 数据 - 它必须是: - 页面 - CLI - Codex 驾驶员 - 自动驾驶编排器 共同消费的“最近活动摘要” 字段语义需要固定: - `summary_text` - 是直接消费的活动短摘要 - `status_label` - 是稳定的人类可读状态标签 - `target_node_codes` - 是活动关联的节点口径,不允许调用方再从自然语言里猜 - `ui_intent_kind` - 是当前活动默认落点类型 - `focus_ref` - 是跨页面 / CLI / Codex 的统一定位对象 正式边界: - 活动流应保留老字段以兼容现有页面 - 新消费方优先用: - `summary_text` - `status_label` - `focus_ref` - 不允许新客户端再从 `title + subtitle + summary` 手工解析定位参数 ## 11. 与 Driver / Playbook / Release 的边界关系 这份 contract 只负责观察面和现场治理,不负责替代其他 contract。 边界应明确为: - `ops_driver_contract` - 决定“现在优先做什么” - `ops_playbook_contract` - 决定“标准多步动作怎么编排” - `release_hub_contract` - 决定“版本怎么发、怎么停、怎么回滚” - `ops_observability_contract` - 决定“现场事实是什么、当前缺口在哪里” 这样后续系统再扩展时,不会又回到“所有东西都写在 overview 一坨大 json 里”的状态。 --- ## 12. 终局验收标准 如果这份 contract 真正落地,海外单脑控制面应该做到: ### 1. 不登录大陆机器也能回答现场问题 例如: - 谁在跑 - 谁在线但没跑 - 谁近窗刚跑过 - 谁日志没回来 - 谁队列死信了 ### 2. 不复制大日志也能先做判断 先看: - execution scene - inspection overview - activity stream - delivery queue 再决定是否需要全量日志或 diagnostics bundle。 ### 3. 页面、CLI、Codex 看到的是同一现场 不能出现: - 页面说节点正在执行 - CLI 说节点待命 - Codex 说节点离线 ### 4. 所有高风险动作都有正式观察落点 也就是: - replay 后能看 job - discard 后能看审计 - inspection 后能看节点收口 - rollout 后能看 launchpad / gate / rollout jobs 这样这套系统才能真正从“调试台”升级成“驾驶舱”。