# 24 domainCheck Node Agent 协议与 Release Hub 设计 如果当前已经进入“把方案真正落地到上线前收口”的阶段,建议配套阅读: - `docs/25_domainCheck_海外单脑控制面上线收口总表.md` - `docs/schemas/ops_driver_contract.md` - `docs/schemas/ops_stack_diagnosis_contract.md` ## 一、Node Agent 为什么是核心 终局架构里,大陆节点不应该再是“需要人工登录的机器”,而应该是: > 受控执行节点 每个节点通过 `domaincheck-node-agent` 接入海外控制面,承担: - 注册 - 心跳 - 拉取任务 - 执行任务 - 回传结果 - 回传日志 - 回传诊断包 这样后台按钮和 Codex 都不用再直接碰节点 shell。 补一个很关键的落地约束: - OpsCenter 顶部的“总检入口”现在应正式优先消费 `GET /api/v1/ops/go-live-summary` - `GET /api/v1/ops/stack-diagnosis` - 保留为更细的可解释总检层 - 用于继续下钻 `issues / operator_decision / next_actions / recommended_commands` - CLI 总检脚本也应优先消费同一个 contract - 海外 Codex 驾驶员后续默认也先看这一份总检结果 也就是说,Node Agent、Release Hub、页面按钮、Codex 驾驶员,不再各自拼一套“当前现场判断”,而是统一先看: > go-live summary 然后再根据 `stack-diagnosis / overview / release launchpad` 继续下钻。 这里还要再固定一个新口径,避免页面和运维动作继续混用“可联调”和“可发版”: - `go_live_status` - 回答“当前是否已经进入统一收口、联调、复核车道” - 允许出现 `attention` - `publish_status / publish_ready` - 回答“当前是否已经满足正式发版门禁” - 必须明确区分 - `blocked` - `attention` - `ready` 这里再补一条已经收口成代码规则、后续不要再放松的判断: - 如果目标节点的 Node Agent `delivery_queue` 仍然存在 `dead_letter` - 这不是普通 warning - 对 `publish_status` 必须按 `blocked` 处理 - 对 `default_rollout_gate` 也必须按阻断处理 - 如果只是 `retrying` - 可以继续保留为 `warning / attention` - 但不应该再被当成“完全干净的发布现场” 原因很简单: - `dead_letter` 说明控制面到节点的执行回执链路已经出现语义失败 - 这时候页面如果还显示“可发布”,就会制造最危险的假阳性 - 所以 `go_live_status` 可以是 `attention` - 但 `publish_status` 必须更严格,直接阻断正式发版 这里还要补一条已经落成代码的事件语义,后续不要再退回“任务成功就算一切成功”的旧口径: - 如果 Node Agent 在执行 `jobs/{job_id}/start` 时,开始回执投递失败 - 允许节点继续本地执行 - 不允许因为这一次投递失败就直接中断真实任务 - 但控制面必须把这类现场显性化为单独问题,而不是静默吞掉 - activity stream 中对应 `ops_job` 应升级为 `attention` - 保留原始 `job_status` - 并暴露 - `start_delivery_state` - `start_delivery_error` - `source_focus_ref` - `ops overview / go-live summary` - 也必须把它当成正式关注项 - 不能只在活动流里可见、但首页总检仍显示“可直接上线” - 当前口径应是 - `go_live_status = attention` - `publish_status = attention` - 默认下一步指向 `focus_latest_job_events` - `stack-diagnosis` - 也必须把这类现场提升为可操作 issue - 默认建议动作应该回到 `focus_latest_job_events` 这里再固定一个动作层约束,避免后面 UI / CLI / Codex 又各自猜入口: - 如果活动焦点已经明确落到 - `source_focus_ref.kind = ops_job_event` - 或者问题本身就是“开始回执异常” - 那么默认焦点动作不应继续使用泛化的 `focus_activity_item` - 而应直接升级为 - `focus_latest_job_events` 原因是: - `focus_activity_item` 适合“先进入这条活动对应的详情” - `focus_latest_job_events` 适合“问题已经明确在 job event 级别” - 对开始回执异常这类问题,继续停在 activity 级别只会多一跳,降低定位效率 原因也很直接: - 这类问题不代表节点没执行 - 但代表控制面和节点现场之间已经出现“开始态失联” - 如果不显性化,页面会以为“任务没问题,只是还没刷新” - 实际上这是自动化链路一致性已经开始打折的早期信号 也就是说: - “总检入口”不只告诉你系统是不是在健康收口 - 还必须同时告诉你“现在能不能正式发布” 后续页面、CLI、Codex、按钮动作,都应该优先消费这两个层级,而不是再各自推导一套“看起来差不多能上”的临时判断。 这里再补一个已经落地的收口原则: - `GET /api/v1/ops/stack-diagnosis` 现在不只返回传统的 - `issues` - `recommended_actions` - `next_step` - 还会正式返回 - `diagnosis.operator_decision` - `diagnosis.next_actions` - `diagnosis.recommended_commands` 这三层的定位要固定下来: - `operator_decision` 先回答“当前主处理车道是什么” 例如:`observability / node_handover / release / ops_jobs / steady` - `next_actions` 直接给出主动作和复核动作的下一跳命令 - `recommended_commands` 把节点现场日志、接管、Release Hub、driver-resolve 等常用 drill-down 统一收口成可复用命令集 这样页面顶部“当前主决策”卡、CLI 的 `check_ops_center_stack.sh`、海外 Codex 驾驶员,就都不该再各自重新推导“当前应该先看日志还是先接管还是先看发布门禁”,而是优先共享后端总检 contract。 这里再补一条已经收口成正式操作约束、后续不要再退回去的口径: - 如果总检已经识别出 - `runtime_build_schema_stale` - `runtime_route_surface_incomplete` - `runtime_build_info_unavailable` - 页面、CLI、Codex 驾驶员的第一反应不应该再是“给我一条 `systemctl restart domaincheck-api`” - 而应该统一提升为标准恢复入口 - `runtime-refresh-recover` - `go-live-recover` 这两个入口的语义要固定: - `runtime-refresh-recover` - 回答“当前运行中的 API 是否还是旧 schema / 旧路由面” - 给出固定恢复链 - 再输出下一跳复检命令 - `go-live-recover` - 把 `runtime-refresh-recover` - `stack-diagnosis` - `stack-next` - `go-live-check` - `doctor` 这条联调收口链直接串起来 也就是说: - 原子 systemd 重启仍然只是底层执行事实 - 对页面按钮、CLI、Codex 来说,正式入口必须是组合恢复动作 - 这样后续总检摘要、主决策卡、推荐命令集,才不会再次退化成“看见 schema 漂移就手工重启” 再往前一层,现在还应该固定: - `GET /api/v1/ops/driver-feed` - 返回 `go_live_summary` - `GET /api/v1/ops/driver-feed` - 继续返回 `automation_coverage` - `GET /api/v1/ops/codex-brief` - 返回 `go_live_summary` - `GET /api/v1/ops/codex-brief` - 继续返回 `automation_coverage` 也就是说: - 页面顶部先看 `go-live-summary` - Codex 驾驶员先看 `codex-brief.go_live_summary` - CLI 总检先看 `go-live-check` - 真正需要解释“为什么阻断 / 下一步为什么是这个动作”时,再回退到 `stack-diagnosis` 同时还要固定一条“自动化覆盖率”口径,避免后续页面、CLI、Codex 又各自推导一套“现在已经自动化到什么程度”: - `driver_feed.automation_coverage` - `codex_brief.automation_coverage` 至少需要稳定包含: - `automation_level_counts` - `recommendation_counts` - `executor_kind_counts` - `safe_auto_total` - `guarded_auto_total` - `mixed_total` - `ui_only_total` - `blocked_total` - `preview_only_total` - `backend_handled_total` - `execution_ready_total` - `human_dependency_total` - `launch_status` - `launch_ready` 其中语义固定为: - `preview_only_total` 统计那些已经后端化、但本质仍属于“只读预览 / 聚焦 / 审阅”的动作 - `backend_handled_total` 统计已经脱离人工页面点击、可以直接走后端 contract 的动作 - `execution_ready_total` 统计 `auto_execute + confirm_then_execute` - `human_dependency_total` 统计 `resolve_first + open_ui + blocked` - `launch_status` 作为“自动化层面的收口判断” 取值固定为 `ready / attention / blocked` - `launch_ready` 只回答“从自动化执行角度看,当前是否已经接近无人值守可上线” 这样才是完整的: - 一份稳定收口摘要 - 一份可解释总检 - 一组可执行下钻命令 --- ## 二、当前已经落下的第一版协议骨架 当前后端已经补出这些接口骨架: - `POST /api/v1/ops/agent/tokens` - `POST /api/v1/ops/agent/bootstrap-plan` - `POST /api/v1/ops/agent/register` - `POST /api/v1/ops/agent/heartbeat` - `POST /api/v1/ops/agent/pull` - `POST /api/v1/ops/agent/jobs/{job_id}/start` - `POST /api/v1/ops/agent/jobs/{job_id}/complete` - `POST /api/v1/ops/agent/jobs/{job_id}/events` 这几类接口已经足够表达“节点是怎么接控制面的”。 当前仓库里也已经补出第一版 agent 运行骨架: - `domain-api/app/node_agent.py` - `domain-api/deploy/systemd/domain-node-agent.service` - `domain-api/deploy/multi-region/build_node_agent_bootstrap_plan.sh` - `domain-api/deploy/multi-region/templates/domaincheck-node-agent.env.example` 也就是说,这已经不是纯文档方案,而是开始具备正式执行器入口了。 另外现在控制面已经不只是“签发一枚 token”,而是可以直接生成完整接入方案: - 后端通过 `POST /api/v1/ops/agent/bootstrap-plan` - 一次返回: - token - env 内容 - 安装命令块 - 启动检查命令 这一步很关键,因为它把“接管新节点”从零散知识点,收敛成了控制面里的标准对象。 控制面页面现在也已经补上了第一版“可视化接管态”: - `GET /api/v1/ops/nodes` 不再只返回“已纳管节点”,也会把“集群可见但未纳管”的节点一起带回来 - `GET /api/v1/ops/nodes/{node_code}/handover` 可以直接拿到单节点接管详情、阻断项、下一步建议和 bootstrap 默认参数 - `POST /api/v1/ops/nodes/{node_code}/handover/bootstrap-plan` 可以按节点当前上下文直接生成新的 bootstrap 方案,而不是让页面或脚本自己再拼 `node_region / node_role` - `GET /api/v1/ops/nodes/{node_code}/onboarding` 现在除了返回 `onboarding_stage / acceptance / recommended_actions`,还应返回统一的 `recovery_decision` - `POST /api/v1/ops/nodes/{node_code}/onboarding/recovery/preview` 应作为 `node-recover` 的正式 preview 入口 - `POST /api/v1/ops/nodes/{node_code}/onboarding/recovery/execute` 应作为 `node-recover confirm` 的正式 execute 入口 - 节点会被明确区分为: - `已接管` - `仅运行态在线` - `待接入` - `心跳过期` - `Token 过期 / 已禁用` 这意味着海外控制面第一次真正具备了“先看见,再接管”的统一入口,而不是只有接入完成后才能看见节点。 这里补一个非常关键的收口原则: - `recovery_decision` 必须由后端统一生成 - 前端、CLI、Codex 驾驶员不再各自重新推导“下一步该 bootstrap 还是该 acceptance” - 它至少要固定返回: - `action` - `label` - `summary` - `command_hint` - `window` - `stage_code` - `acceptance_status_code` 当前口径应固定为: - 节点仍处于 - `pending_bootstrap` - `runtime_only` - `stale` - `agent_pending` - 以及其他接入中状态 时,优先收口为 `bootstrap_run` - 节点真正进入 - `acceptance_ready` - 或 Agent 已在线 时,才切到 `run_acceptance` 这样 `node-recover`、OpsCenter“自动收口建议”、海外 Codex 驾驶员三边看到的恢复结论才会完全一致。 这里还要补一条很关键的执行约束,避免页面、CLI、Codex 后面又各自补出“双发一次”的隐藏 bug: - `node-recover` - preview 时优先命中 `onboarding/recovery/preview` - confirm 时优先命中 `onboarding/recovery/execute` - 如果控制面已经成功命中正式 `recovery/execute` - 就表示后端已经完成“选择动作 + 发起执行” - CLI / 页面 / Codex 不允许再额外补跑一次 - `node-bootstrap-run` - 或 `node-acceptance-run` - 只有在 recovery endpoint 不存在、退回兼容视图时 - CLI 才可以根据 `recovery_decision / stage_code / acceptance_status_code` 再本地降级决定下一跳 也就是说,正式协议下: - 动作选择权在后端 recovery endpoint - CLI 和页面只是展示恢复结论与执行回执 - 兼容模式下才允许本地推断 这样才能避免后续接管链路出现: - preview 看的是一套 - execute 走的是另一套 - CLI 还再补跑一次底层动作 一旦出现这种双发结构,节点接管、验收、发布回执都会失真,这在上线期是不能接受的。 接管通过后,控制面也不应该只是停在“状态已通过”这一步,而是要立刻进入正式运维动作: - 单节点执行标准巡检 - 批量对“真正参与检测节点”发起巡检 - 批量对“在线未参与节点”发起巡检 - 节点定向进入 Release / Rollout 其中“标准巡检”已经统一成固定序列: - `health.snapshot` - `logs.collect` - `diagnostics.collect` 后续无论是 Codex 驾驶员还是后台按钮,都应该优先复用这条标准序列,而不是再回到临时 shell 排障。 在控制面页面上,这些状态不应该只是文字摘要,还应该继续收口成“驾驶建议卡”: - 如果存在有效执行节点但尚未接管,优先进入接管动作 - 如果存在参与检测节点但现场日志还不可见,优先开启关键日志回传 - 如果存在真正参与检测节点,优先执行标准巡检 - 如果存在在线未参与节点,优先排查接单/待命问题 - 当接管和巡检链路稳定后,再推进 Release / Rollout 这样 Node Agent 协议、Ops Job 模型和页面按钮才会是同一个体系,而不是三套彼此分裂的运维逻辑。 这里的驾驶动作命名也要尽量收敛成通用语义,而不是绑定页面分组: - `run_standard_inspection` - `open_worker_logs` - `open_diagnostics` 这样同一套动作才能在: - 驾驶建议卡 - 后台按钮 - Codex 自动驾驶 - 后续自动化编排 之间稳定复用。 再进一步,为了避免前端按钮、Codex 驾驶员、临时脚本各自维护一套“多步动作串联逻辑”,控制面里还需要再上一层: - `ops playbook` 它的职责不是直接执行 shell,而是把一个高层运维意图展开为一组标准 `ops jobs`。 例如: - `inspection.standard` - `health.snapshot` - `logs.collect(worker)` - `diagnostics.collect` - `scene.logs.key` - `logs.collect(worker, 120)` - `scene.logs.full` - `logs.collect(worker, 300)` - `diagnostics.collect(300)` 这样: - 后端推荐的是 playbook - 前端按钮点的是 playbook - Codex 驾驶员调用的也是 playbook - 真正落库和回执的仍然是 `ops jobs` 整个模型会更稳定,也更适合后面持续扩机器。 并且这层已经开始真正进入页面主流程: - OpsCenter 已能列出 playbook - OpsCenter 已能预览 playbook 展开结果 - “标准巡检”已优先直接调用 `inspection.standard` - “接管后一键验收”已优先直接调用 `onboarding.acceptance` 也就是说,控制面 UI、Codex 驾驶员、后端编排这三层终于开始共用同一套多步动作模型。 同样,OpsCenter 不应该只会发任务,还要能直接承接执行现场: - 把 `participating_nodes` 和 `non_participating_nodes` 一起并入运维视图 - 把 `log_sync.enabled / mode / source_nodes / last_line` 一起并入运维视图 - 并允许控制面直接切换远端日志回传模式 这样 Codex 驾驶员和人工按钮都会在同一页看到: - 谁正在跑 - 谁在线但没跑 - 日志有没有回来 - 要不要临时切到全量回传 并且这层不能只停留在页面按钮,还要提供固定 drill-down 命令: - `bash domain-api/deploy/multi-region/drive_ops_center.sh scene-node-log [limit] [key|full]` - `bash domain-api/deploy/multi-region/check_node_scene_log.sh [limit] [key|full]` 这样海外控制面、CLI 和 Codex 驾驶员在看到“参与检测但缺样本”时,不需要再人工猜下一步看哪台机器,而是直接按节点下钻现场日志。 这里再补一个已经落地的恢复流原则: - 当 `stack-diagnosis` / `ops overview` 判断为 - `remote_log_sync_disabled` - 或“已有参与节点但现场日志仍不可见” - CLI、页面按钮、Codex 驾驶员都不应该再只给“去看日志”这种观察型建议 - 而应该优先给出: > `log-sync-recover` 也就是: - 先执行 `enable_log_sync_key` 或 `enable_log_sync_full` - 再自动复核 `execution_scene.log_sync` - 再自动给出下一跳 drill-down 当前海外单入口已经有这一条组合命令: ```bash bash domain-api/deploy/multi-region/drive_ops_center.sh log-sync-recover bash domain-api/deploy/multi-region/drive_ops_center.sh log-sync-recover http://127.0.0.1:8100 full confirm cli/ops ``` 它不是“额外的便捷脚本”,而是终局驾驶 contract 的一个关键落地点: - 页面按钮可直接复用 - Codex 驾驶员可直接复用 - `stack-diagnosis / doctor / stack-next` 也应该优先推荐它 这样“恢复可见性”就不再是人工脑补的一串步骤,而是正式的一跳动作。 Node Agent 接管链路也应采用同样原则: - 当 `stack-diagnosis` 判断主车道已经进入 `node_handover` - 或发现 `managed_nodes_agent_pending` - 不应该只给出零散的 - `node-handover` - `node-bootstrap-plan` - `doctor` - 而应该优先给出一条组合恢复流: > `agent-gap-recover` 它的目标不是“替代真正的节点落地”,而是把海外控制面需要做的这几步先标准化: 1. 识别首个接管缺口节点 2. 输出该节点当前 handover 状态 3. 生成该节点 bootstrap plan 4. 给出后续检查命令 当前海外单入口已经支持: ```bash bash domain-api/deploy/multi-region/drive_ops_center.sh agent-gap-check bash domain-api/deploy/multi-region/drive_ops_center.sh agent-gap-recover bash domain-api/deploy/multi-region/drive_ops_center.sh agent-gap-export ``` 而且这条链路也已经具备版本兼容能力: - 如果当前控制面 API 还没有节点级 `/handover` - 退回 `stack-diagnosis` - 如果当前控制面 API 还没有节点级 `/handover/bootstrap-plan` - 退回通用 `/ops/agent/bootstrap-plan` 这样“节点接管”就不再只是页面里的一个弹窗动作,而是正式进入海外单入口、CLI 总检、Codex 驾驶员都可复用的统一恢复模型。 其中: - `agent-gap-recover` - 负责把首个缺口节点的状态、bootstrap plan、下一跳建议串成一条恢复流 - `agent-gap-export` - 负责把首个缺口节点的接管材料直接导出成目录 - 默认应至少包含: - `stack-first-gap-handover` - `agent-gap-check` - `node-onboarding` - `node-bootstrap-plan` - `node-bootstrap-preview` - `node-acceptance-plan` - `manifest.json` 这样海外控制面就不只是“告诉你下一步做什么”,而是可以直接产出一份可执行、可交接、可归档的节点接管包。 另外,`node-bootstrap-plan` 的 condensed summary 还应该主动区分两类情况: - 仓库代码本身还不支持最新 bootstrap 字段 - 仓库代码已经支持,但运行中的 API 还没重启到最新版本 后一类在联调里非常常见,所以 summary 必须直接给出: - `repo_capability.supports_install_command_block` - `repo_capability.runtime_may_need_restart` - `recommended_commands.restart_api_if_runtime_stale` 这样一眼就能判断,这次缺口到底是“代码没写到位”,还是“代码已经在仓库里,但线上进程还是旧的”。 这个判断还必须继续上提到总检 contract: - 一旦 `stack-diagnosis` 识别出 `runtime_build_schema_stale` - 默认 `next_step.action_code` - 必须优先收敛为 `api-restart` - `diagnosis.operator_decision.lane` - 必须优先收敛为 `runtime_recovery` 因为这类问题的本质不是“节点还没接管”,而是“控制面 API 还是旧 schema / 旧路由面”。 如果这里不先做 API 重启,而是继续把人带去执行 `bootstrap_run / fix_managed_nodes` - 只会让真正问题继续被掩盖 - 页面、CLI、Codex 也会一起被带偏 另外,bootstrap 计划和安装脚本都必须兼容多种部署目录布局: - `${ROOT_DIR}/domainCheck/...` - `${ROOT_DIR}/domain-api/...` - `${ROOT_DIR}/deploy/...` 原因很现实: - 开发仓库里脚本位于 `domain-api/deploy/...` - 实际上线包可能已经把 API 内容平铺到 `domainCheck/...` 如果 bootstrap 计划只写死其中一种路径,就会在“控制面判定缺口正确,但落地脚本直接找不到 install script”这个位置翻车。 同样,这个 drill-down 也应该进入 `doctor-export`: - 当 `stack-diagnosis` 已经给出 `node_scene_log` 焦点 - 导出包自动追加 `15_scene_node_log_.txt` - `manifest.json.summary.scene_log_reports` 也同步记录这些节点级现场日志附件 并且这里的焦点来源现在也应该统一成后端总检 contract: - `diagnosis.issues[*].focus_ref` - `diagnosis.next_step.focus_ref` - `diagnosis.operator_decision.next_focus` 只要其中任一层已经明确给出 `node_scene_log`,诊断包就应自动把该节点现场样本带上,而不是再由导出脚本自行重新推导“哪台机器值得看”。 这样海外 Codex、后台自动驾驶和人工交接看到的不是一句“建议去看某台节点”,而是一份已经带着关键现场样本的诊断包。 再往前一步,驾驶建议本身也不应只存在于前端页面,而应成为 ops overview contract 的一部分: - 后端返回 `driver_recommendations` - 每条建议附带 `action_code` - 前端只做动作映射,不再重新计算优先级 继续往下,`driver_recommendations` 也不能只给“同一组 node_codes + 同一份 payload”。 因为真实场景里经常会出现: - 主动作是 `创建 Rollout` - 次动作是 `打开点状灰度模板` - 两个动作对应的目标节点、release_id、预填参数并不相同 所以 contract 还要支持: - `primary_node_codes` - `secondary_node_codes` - `primary_action_payload` - `secondary_action_payload` 这样海外控制面、Codex 驾驶员、CLI 推荐执行器才能真正做到“同一张建议卡,主次动作各走各的参数”,而不是被迫回退成前端手写分支。 这样未来换成 Codex 自动驾驶或脚本编排时,也能直接复用同一套推荐逻辑。 并且这套 contract 不该只停在“给建议”,而要继续补到“统一执行入口”: - `POST /api/v1/ops/driver-actions/preview` - `POST /api/v1/ops/driver-actions/execute` - 能后端化的动作先统一走这里 - 仍需人工选择、弹窗补参数的动作,再回退到前端交互 其中 `preview` 这层尤其关键,因为它负责把: - execution chain - target api / method - 主次动作 request payload - runbook resolve 结果 统一冻结成后端 contract。 这样页面、CLI 和海外 Codex 驾驶员都不需要自己重新拼 request preview,也不会再出现“页面自己猜 sequence -> action 映射”的口径漂移。 现在这条链路继续往下已经开始接入: - `GET /api/v1/ops/playbooks` - `GET /api/v1/ops/playbook-runs` - `GET /api/v1/ops/playbook-runs/{run_code}` - `GET /api/v1/ops/playbook-runs/{run_code}/events` - `GET /api/v1/ops/activity-stream` - `POST /api/v1/ops/playbook-runs/{run_code}/rerun` - `POST /api/v1/ops/playbook-runs/{run_code}/cancel` - `POST /api/v1/ops/playbooks/preview` - `POST /api/v1/ops/playbooks/execute` 也就是说,驾驶动作的后端执行入口不再只是“直接发一个动作模板”,而是可以把多步标准动作整体展开。 这样整条链路才是: - 后端推荐 - 后端标准动作执行 - 前端只做展示和少量交互兜底 再进一步,控制面还要具备“巡检结果收口视图”: - 一个节点最近的 `health.snapshot` - 一个节点最近的 `logs.collect` - 一个节点最近的 `diagnostics.collect` 要能在页面里按节点重新聚合,而不是让操作者自己翻几十条 `ops job` 去拼结果。 这一步很关键,因为它决定了: - Codex 驾驶员读取的是“节点收口状态” - 而不是一堆离散任务记录 现在这层也不该继续停留在前端临时聚合,而应该进入正式后端协议: - `GET /api/v1/ops/overview` 返回 `inspection` - `GET /api/v1/ops/inspection-overview` 返回独立巡检收口视图 - 页面只负责展示,收口排序、异常优先级、Worker 日志口径都以后端为准 并且这层 contract 不能只停留在“返回一句总结”,还要正式结构化: - `problem_kind` - `problem_label` - `problem_level` - `recommended_action_code` - `ui_intent` 这样页面筛选、Codex 自动驾驶、后续策略编排才不需要再从自然语言里反推“这是失败、执行中还是巡检缺口”。 同理,动作模板 contract 也不能继续停留在“只有 text / number / select”: - `boolean` - `textarea` - `text_list` 这三类字段是运维动作真正高频的表达方式: - `boolean` 用来表达是否自动审批、是否自动回滚、是否切 current - `textarea` 用来承载 artifact URL、长文本备注、临时参数块 - `text_list` 用来承载服务列表、健康检查 URL 列表、节点列表 否则发布、回滚、批量诊断这些动作永远只能靠前端写死表单,无法真正回收到统一模板层。 尤其 `deploy.release` 这类动作,应该正式进入模板目录,而不是只存在于 rollout 内部: - `deploy.release.control` - `deploy.release.worker` - `deploy.release.custom` 这样单节点灰度验证、点状修复、正式 rollout 之前的预演,才能统一落到同一个 `ops job` 体系里。 模板动作再往前还要补一层正式能力: - `action template preview` - `batch policy preview` 也就是在真正创建 `ops jobs` 之前,控制面先基于: - 目标节点集合 - 当前 execution mode - 动作 payload - 集群参与态 / 忙碌态 / control / effective worker 身份 给出统一预检结论: - `blocked` - `approval_required` - `warnings` - `recommendations` 这样发布、停 Worker、重启 API 这些高风险动作,就不会变成“点了按钮才知道会出事”,而是先看到: - 哪些节点被阻断 - 哪些节点建议审批 - 哪些只是需要关注 这层能力既服务于页面按钮,也服务于后续的海外 Codex 驾驶员,因为两边都应该调用同一份 policy preview contract,而不是各自写一套风险判断。 在 playbook 这一层,还要继续补一条非常关键的 contract: - 一次 playbook 执行 = 一次 `playbook run` - 每个 run 都要有自己的 `run_code` - 每个子任务都写入统一的 playbook metadata - 控制面能够把这些子任务重新聚合成: - run 总状态 - step 状态 - 目标节点集合 - 最近可追事件的任务入口 这一步的意义是把“多步动作”从前端临时串联,正式升级成可审计、可追溯、可回放的运维对象。 继续补齐 run 级动作之后,控制面就可以直接: - 对 playbook run 做状态筛选 - 按分组收窄到 onboarding / diagnostics / scene - 按 run_code、创建来源、目标节点做关键词检索 - 直接查看某轮 run 的 focus summary - 直接看到 problem steps / active steps 也就是说,控制面针对 playbook run 的 contract 已经不再只是“能聚合”,而是进一步升级成“能定位焦点、能快速追问题”。 再往前一步,playbook run 还必须具备“整轮事件视角”: - 不是只查某一条 job 的事件 - 而是能把同一 `run_code` 下面所有 job 的事件重新聚合 - 并补齐: - `step_key` - `step_title` - `job_code` - `job_status` - `target_node_code` 这样海外控制面的详情抽屉才能真正像“远端驾驶舱”,因为它看到的是整轮执行过程,而不是零散子任务的碎片。 现在这层 contract 还要进一步冻结成“前端不再自行翻译”的正式字段: - `playbook run` - `status_label` - `summary` - `summary_text` - `steps_total` - `steps_success` - `steps_running` - `steps_problem` - `steps_terminal` - `focus_ref` - `playbook run.steps[]` - `status_label` - `summary` - `summary_text` - `target_node_codes` - `focus_ref` - `playbook run events` - `level_label` - `job_status_label` - `summary` - `summary_text` - `event_key` - `occurred_at` - `focus_ref` - `source_focus_ref` - `playbook run events.summary` - `returned_total` - `event_type_counts` - `job_status_counts` - `filters.limit` 也就是说,从这一步开始: - 前端不再自己把 `running` 翻译成“收口中” - 前端不再自己拼“当前焦点摘要” - 前端也不再把“播放焦点”和“远端执行焦点”混成一个对象 这里还要再补一条非常重要的消费规则: - `playbook run events.focus_ref` - 始终用于定位到本轮 playbook run 的正式落点 - `playbook run events.source_focus_ref` - 始终保留远端 Agent / ops job 原始焦点 - 允许携带: - `job_id` - `job_code` - `event_key` - `step_key` - 其他执行侧专有定位字段 这样控制面详情抽屉、Codex 驾驶员和后续 CLI 就不会再遇到一个老问题: - 页面需要跳到“这一轮 playbook 的哪个步骤” - 但排障又需要知道“远端 Agent 具体卡在了哪条事件” 这两种焦点必须并存,而不能互相覆盖。 - 前端不再自己推断“应该跳到哪一轮 / 哪一步 / 哪条事件” 而是统一消费控制面已经产出的结构化别名字段。 同样,驾驶建议层也开始和 playbook run 正式打通: - recommendation 不再只推荐“去看哪个节点” - 而是可以直接推荐“先处理哪轮异常编排” - 并携带: - `run_code` - `focus_step_key` - `focus_step_title` 这样 recommendation -> playbook run detail -> playbook run events 才形成一条真正闭环的驾驶链路。 继续往前收一层,recommendation 也不该只看 playbook run,而要继续消费统一 activity stream: - 如果最近存在失败 / 阻断 / 已取消的 standalone `ops job` - 或存在 `awaiting_approval`、`halted`、`completed_with_issues` 的 rollout - recommendation 应直接把这条 activity 提升成驾驶建议 - 并通过统一 `ui_intent` 把前端直接送到: - `job_events` - `rollout_jobs` 这样 recommendation 才真正具备“先聚焦异常活动,再决定是否补动作”的现场驾驶能力。 再继续往下一层,控制面还需要统一的“最近活动入口”: - 不只是 playbook run - 也包括 standalone ops jobs - 以及 rollout 推进记录 所以活动流 contract 也应该成为正式后端协议: - `GET /api/v1/ops/activity-stream` - 每条 activity 带: - `kind` - `activity_key` - `status` - `status_label` - `occurred_at` - `summary` - `summary_text` - `target_node_codes` - `ui_intent` - `focus_ref` 其中 `ui_intent` 不是让前端自己猜跳哪,而是由后端直接声明: - `playbook_run_detail` - `job_events` - `rollout_jobs` 这样后端不仅能告诉前端“这里有一条异常活动”,还能直接告诉它“应该落到哪一个处理界面”。 这样海外控制面、后台按钮和 Codex 驾驶员才能共享一套“最近活动 -> 正确落点”的跳转 contract。 并且这层不要再停在“页面展示友好字段”,而要正式成为统一驾驶对象: - `status_label` - 直接给页面、CLI、Codex 看的人类可读状态 - `summary_text` - 直接给驾驶建议和摘要面板消费的短摘要 - `target_node_codes` - 不让调用方再从自然语言里猜目标节点 - `focus_ref` - 让页面、CLI、Codex 都能用同一份定位对象跳到: - `playbook run` - `ops job` - `rollout` - `execution scene` 这样后续不管是海外 Codex 驾驶员、CLI 还是后台按钮,都能把 activity 当成正式 contract,而不是“页面 table 的一行数据”。 同样,`activity-stream` 里的 `ops_job` 项也要冻结一条优先级规则: - 如果 job 本身已经带 `result.summary_text` - activity 的 `summary_text` 优先使用它 - 如果 job result 里已经带 `focus_ref` - activity 的 `focus_ref` 优先吸收它 - 只有在没有明确执行结果摘要时 - 才回退到“目标节点 / 发起人 / 执行方式”这类模板化描述 原因很简单: - 模板摘要适合“任务刚创建” - 结果摘要才适合“任务已经执行过,且远端已经给出结论” 否则页面上会长期出现这种低信噪比口径: - `目标节点 xxx / 发起 web-ui / 方式 远端 Agent` 但真正更有价值的应该是: - `远端 Agent 已回传 120 行 Worker 日志,可直接继续分析` 这一点对海外 Codex 驾驶员尤其关键,因为它读取 activity-feed 时,优先需要的是“执行结论”,而不是“模板元信息”。 这条规则继续往下也要落到 Codex brief: - `codex-brief.entries` - 仍然是可执行主线 - `codex-brief.activity_focus` - 则是可观察焦点 - `codex-brief.scene_log_observation` - 则是节点级现场日志观察摘要 - 如果当前没有动作主线,但已有观察焦点 - `codex-brief.focus` 允许回退到首条 `activity_focus` - 但推荐行为只能是 `open_ui` - 不允许伪装成 `auto_execute` - 如果当前没有动作主线、也没有合适的 activity focus,但现场日志已经识别出明确目标节点 - `codex-brief.focus` 允许继续回退到 `scene_log_observation.focus_ref` - 这样海外 Codex 会直接落到 `node_scene_log` 这样海外 Codex 在“现场正在变化,但暂时没有标准动作要打”的时候,仍然能稳定给出: - 先盯哪一条 - 回到哪个正式落点 - 为什么现在是观察优先而不是执行优先 接下来 driver-feed / runbook 这一层也必须正式冻结: - `driver_feed.entries[*]` - `focus_ref` - `primary_focus_ref` - `secondary_focus_ref` - `runbook_sequences[*]` - `focus_ref` - `primary_focus_ref` - `secondary_focus_ref` 也就是说: - driver-feed 不再只返回“动作码 + 按钮文案” - runbook 也不再只返回“主次动作” - 而是必须同时告诉页面、CLI、Codex: - 这条主线整体应该聚焦哪里 - 主动作对应哪个正式落点 - 次动作对应哪个正式落点 这样后端才能真正消灭“前端根据 action_code 猜页面落点”的分叉逻辑。 这一层现在还要继续补一条非常实用的分流规则: - `driver-feed.entries` - 仍然表示“驾驶动作主线” - `driver-feed.activity_focus` - 单独表示“观察与跟踪焦点” - 不再把观察项硬塞成一条可执行建议 - `driver-feed.scene_log_observation` - 单独表示“当前远端日志回传是否已经形成可下钻的节点级现场日志” 也就是说以后海外控制面和 Codex 同时会拿到两类对象: - 一类是可以继续 preview / resolve / execute 的动作主线 - 一类是当前值得优先盯住的现场对象 - 例如: - 正在跑的 `ops job` - 未收口的 `playbook run` - 当前 `execution scene` - 远端 `log sync` 这样不会再出现一个老问题: - 页面把“值得先看”误渲染成“可以直接执行” - Codex 又把“先看现场”误读成“可以自动发动作” 同样的冻结规则还要继续往下压到驾驶执行链: - `driver-actions/preview` - `focus_ref` - `primary_focus_ref` - `secondary_focus_ref` - `primary.focus_ref` - `secondary.focus_ref` - `driver-actions/resolve` - `preview_request.focus_ref` - `preview_request.primary_focus_ref` - `preview_request.secondary_focus_ref` - `gate.focus_ref` - 顶层 `focus_ref` - `codex-actions/resolve` - `selected_entry.focus_ref` - `preview_request.focus_ref` - `gate.focus_ref` - 顶层 `focus_ref` 只有这样页面、CLI、海外 Codex 驾驶员在 preview / resolve / execute 三段链路里,才不会重新猜“当前应该回看哪个 release、哪个 rollout、哪个 execution scene”。这也是后续 NodeAgent、ReleaseHub、OpsCenter 可以共享同一份驾驶协议的关键。 进一步说,`activity-stream` 不只是最近事件列表,还应该是正式可筛选的观察面: - `kind` - `status` - `query` 这些过滤条件必须后端支持,而不是只在前端本地过滤。这样未来: - 海外 Codex 驾驶员 - CLI 检查脚本 - 后台页面 三者都可以复用同一套活动流收口逻辑。 继续升级之后,`driver action` 的协议也不该停在“一个 action_code”: - 应返回: - `handled` - `mode` - `ui_intent` - 其中 `ui_intent` 用来表达: - 应打开哪个详情页 - 应聚焦哪一轮 playbook run - 应进入哪个发布入口 - 应打开哪个节点接管入口 这一步的价值很大,因为它意味着: - recommendation 的解释权回到后端 - 前端变成“执行/跳转承载层” - 后续如果换成海外 Codex 驾驶员,仍然可以直接复用同一份 intent contract - 对某一轮编排做统一重跑 - 对某一轮编排做统一取消 - 不需要再由前端把同一批子任务逐条循环处理 这正是“海外控制面统一驾驶,大陆节点被托管执行”的关键基础。 同理,execution scene contract 也不该只是“参与节点数组 + 待命节点数组”,而要继续收成更明确的运行口径: - `dispatch_active_nodes` - `recent_only_nodes` - `standby_nodes` - `load_syncing_nodes` 以及日志回传本身的正式状态: - `disabled` - `waiting_sample` - `partial_coverage` - `healthy` - `full_capture` 并且要直接返回: - 覆盖了多少参与节点 - 哪些参与节点还没回传样本 - 当前推荐动作文案 这样页面、Codex 驾驶员和后续自动化脚本都能共享同一套“现场观察 -> 判断缺口 -> 执行动作”的协议,而不是各自再做一次前端猜测。 这样未来新增: - Codex 自动巡检 - 后台按钮巡检 - 节点接管后的首轮验收 - 发布后的批次验收 都能直接复用同一个 inspection contract。 在收口视图之上,还应该继续有“异常节点优先队列”: - 先处理失败 / 阻断 / 取消 - 再处理仍在执行中的节点 - 再处理尚未形成完整巡检记录的节点 另外标准巡检里的 `logs.collect` 应明确限定为 Worker 日志收口,不应把 Node Agent 的辅助排障日志混成同一类结果。 当前第一版 agent 执行器已经覆盖: - `service.start` - `service.stop` - `service.restart` - `service.status` - `health.snapshot` - `logs.collect` - `diagnostics.collect` - `deploy.release` 后续继续增强时,只需要在这条主链路上加能力,不需要重换架构。 --- ## 三、Agent 标准执行流程 ### 1. 签发 token 控制面给节点签发一枚 agent token。 ### 2. 节点注册 节点启动后调用: - `register` 上报: - `node_code` - `region` - `role` - `agent_version` - `capabilities` - `labels` - `hostname` - `ip` ### 3. 节点心跳 节点周期调用: - `heartbeat` 控制面更新: - `last_seen_at` - agent 元数据 - 节点在线状态依据 ### 4. 节点拉任务 节点调用: - `pull` 控制面返回分配给该节点的 `ops job`。 ### 5. 节点开工 节点拿到 job 后先调用: - `jobs/{id}/start` 这一步不是可有可无,而是为了把“任务已派发”和“任务已真实开始执行”区分开。 ### 6. 节点完工 执行结束后调用: - `jobs/{id}/complete` 上报: - `success / failed / partially_succeeded` - `stdout` - `stderr` - `result json` ### 7. 节点事件流 执行过程中可持续调用: - `jobs/{id}/events` 把中间事件推回控制面。 --- ## 四、为什么要有 Release Hub 因为正式环境不应该继续依赖: - 节点自己 `git pull` - 工作区状态不确定 - root/www 用户混跑 正式版必须把“版本”变成平台中的一级对象。 也就是: > 先有 Release,再有 Deploy --- ## 五、当前已经落下的 Release Hub 骨架 当前后端已经补出: - `POST /api/v1/ops/releases` - `GET /api/v1/ops/releases` - `GET /api/v1/ops/releases/latest` - `GET /api/v1/ops/releases/{id}` - `POST /api/v1/ops/releases/{id}/activate` - `POST /api/v1/ops/releases/{id}/rollouts` - `GET /api/v1/ops/rollouts/{id}` - `GET /api/v1/ops/rollouts/{id}/jobs` - `POST /api/v1/ops/rollouts/{id}/advance` 并落了两类核心对象: - `ops_releases` - `ops_release_rollouts` 这意味着后面发布系统可以正式围绕: - 版本号 - 渠道 - commit sha - artifact url - checksum - 激活版本 来运作。 但是 Release Hub 还不能只回答“有没有版本”,还要回答: > 现在这版到底能不能发 因此控制面里还需要一个正式门禁对象: - `release_hub.default_rollout_gate` 它至少要返回: - 默认 Release - 默认 Rollout 目标节点 - 门禁状态: - `missing_release` - `release_not_ready` - `artifact_missing` - `no_targets` - `blocked` - `attention` - `ready` - 阻断原因 - 告警原因 - 推荐动作 - `remote_agent_ready_nodes / nodes_total` - `inspection_healthy_nodes / nodes_total` 这样首页驾驶建议、Release Hub 顶部摘要、Codex 自动驾驶、真正发 Rollout 前的 preview,才能统一读同一份门禁判断。 --- ## 六、现在这版 rollout 已经不只是“建记录” 当前 rollout 已经补上三类关键能力: - 分批推进 - rollout 与 ops job 关联 - 执行结果自动反推 rollout 状态 也就是说,控制面不再是一次性给所有节点平铺发任务,而是可以表达: - 第一批先发 `1` 台 canary - 健康后再发后续批次 - 某批失败则暂停 - 人工确认后继续下一批 当前 rollout 状态会根据 job 结果自动进入: - `planned` - `running` - `awaiting_approval` - `ready_for_next_batch` - `halted` - `completed` - `completed_with_issues` 这已经非常接近正式发布系统的骨架,而不是临时脚本。 --- ## 七、终局版发布链路 推荐的正式发布链路: 1. 海外控制面构建 release 2. 写入 `ops_releases` 3. 标记某个 channel 的 active release 4. 后台点击“部署 release” 5. 生成 `ops job` 6. node-agent 拉取任务 7. 下载 artifact 8. 校验 checksum 9. 切换版本 10. 重启服务 11. 健康检查 12. 成功或回滚 当前后端也已经补出: - Release 对象 - Release 激活 - Rollout 对象 - `release -> rollout -> deploy jobs` 分批生成 - rollout 推进接口 - 节点执行结果回写后自动刷新 rollout 总结 这意味着后续真正接发布包仓库时,只需要把 artifact 构建和分发接入,不需要再重新设计控制面对象模型。 --- ## 八、Node Agent 侧的发布保险丝 当前 `deploy.release` 执行链路已经补上: - 下载 artifact - checksum 校验 - 解压到 `releases/` - 切换 `current` 软链 - 重启指定服务 - 执行健康检查 - 健康失败时回滚到旧版本 健康检查目前支持: - `systemctl is-active` - HTTP URL 探测 并且 agent 会把关键事件回推控制面,例如: - `deploy_download_started` - `deploy_checksum_verified` - `deploy_current_switched` - `deploy_health_failed` - `deploy_rollback_completed` 这套事件流很重要,因为后面后台和 Codex 都是靠这条链路看懂“节点到底做到了哪一步”。 所以事件 contract 也要继续冻结,而不能只返回一行原始 message: - 每条 job event 至少应带: - `event_key` - `event_type` - `level` - `level_label` - `summary` - `occurred_at` - `ui_intent` - 任务事件接口本身还要有 `summary`: - `returned_total` - `level_counts` - `event_type_counts` - `node_counts` - `latest_at` 这样海外控制面打开“任务事件流”时,先看到的是结构化执行回放,而不是先读一大串原始日志文本。 --- ## 九、为什么这套设计后续最省心 因为它把三件事情分清楚了: - `release` 是版本对象 - `ops job` 是动作对象 - `node agent` 是执行对象 后面不管节点从 `3` 台增加到 `30` 台,还是 Codex 接管更多动作,这三层都不用推翻。 --- ## 十、接管不是终点,要有“验收后直接进入正式运维”的桥 实际落地里最容易断掉的一环,不是 token,也不是 heartbeat,而是: > 节点明明已经接进来了,但接下来该做什么,操作员还得自己切页面、自己填 node_code、自己判断先巡检还是先发布。 这会把接管成功后的效率重新打回人工模式。 所以控制面里要把 Node Agent 接管链路拆成三段: ### 1. 接入观察 用于回答: - token 是否有效 - node agent 是否已经 register - heartbeat 是否稳定 - 当前是 `未纳管 / 待接入 / 已接管 / 心跳过期` ### 2. 标准验收 接管完成后,不应该直接默认“节点已经可用”,而是先跑一轮固定验收动作: 1. `health.snapshot` 2. `service.status(domaincheck-node-agent)` 3. `logs.collect(domaincheck-node-agent)` 4. `service.status(domaincheck-worker)` 5. `logs.collect(domaincheck-worker)` 这轮验收的意义不是“多做几步”,而是把最常见的接管隐患一次性暴露出来: - 服务虽然启动了,但 register 没成功 - heartbeat 有,但 pull / complete 异常 - worker 在线,但 Redis / DB / proxy 配置不对 - systemd 看似 active,但日志里已经反复报错 ### 3. 正式运维入口 如果标准验收通过,控制面不能只显示一个“已通过”标签,而要直接给出两类后续入口: - `进入巡检模式` 默认锁定当前节点,直接预填 `diagnostics.collect` - `进入发布模式` 默认锁定当前节点,直接预填目标 release 和单节点 rollout 这样节点接管就形成了完整闭环: > 看见节点 -> 纳管 -> 签发接入方案 -> 接入观察 -> 标准验收 -> 巡检 / 发布 而不是“接进来之后又回到人工记忆和手填”。 当前前端已经按这个思路落了一版: - 接管弹窗里增加“接管验收结论” - 可一键跑标准验收 - 验收通过后,直接出现: - `进入巡检模式` - `进入发布模式` - 节点会自动锁定为当前接管节点 - 发布会默认带上当前选中的 release,没有 release 时直接提示先创建 并且 OpsCenter 现在不再只是“有按钮”,而是开始把 contract 直接展示到关键弹窗里: - `生成 Node Agent 接入方案` - 明确显示当前使用 `ops_agent_protocol` - 展示主入口、schema 文档、发现入口、执行链路 - `创建 Release` - 明确显示当前使用 `release_hub_contract` - 展示 release 主入口、launchpad、schema 文档、发布链路 - `创建 Rollout` - 明确显示当前使用 `release_hub_contract` - 展示 rollout 主入口、launchpad、execution_mode、执行链路 - `驾驶建议 / 驾驶主线 / 自动驾驶判断` - 可直接打开“驾驶动作契约预览” - 展示 `ops_driver_contract`、目标 API、执行链路、request preview - `overview / driver-feed` 已下沉到后端 `driver-actions/resolve` / `driver-actions/execute-resolved` - `driver-actions/preview` 保留为基础契约预览与兜底入口 - `codex-focus-preview` / `codex-focus-run` 进一步下沉到后端 `codex-actions/resolve` / `codex-actions/execute` - 把“这次点击到底会调用什么”从黑盒按钮变成可审阅对象 命令行侧也开始走同一条 contract 预览链: - `bash domain-api/deploy/multi-region/drive_ops_center.sh contracts` - `bash domain-api/deploy/multi-region/drive_ops_center.sh driver-feed` - `bash domain-api/deploy/multi-region/drive_ops_center.sh driver-focus-preview` - `bash domain-api/deploy/multi-region/drive_ops_center.sh driver-focus-run` - `bash domain-api/deploy/multi-region/drive_ops_center.sh driver-preview ...` - `bash domain-api/deploy/multi-region/drive_ops_center.sh driver-resolve ...` - `bash domain-api/deploy/multi-region/drive_ops_center.sh driver-run ...` - `bash domain-api/deploy/multi-region/drive_ops_center.sh runbook-preview ...` - `bash domain-api/deploy/multi-region/drive_ops_center.sh runbook-run ...` - `bash domain-api/deploy/multi-region/drive_ops_center.sh codex-focus-preview` - `bash domain-api/deploy/multi-region/drive_ops_center.sh codex-focus-run` 这样海外主机上即使不打开页面,也能先看到统一 contract,再决定要不要执行。 这里还要再收一条非常重要的约束: - `codex-focus-preview / codex-focus-run` - 不能只认 `codex-brief.entries` - 当当前没有动作主线、但存在 `codex-brief.activity_focus` 时 - 应允许选中观察焦点 - 并统一收口为 `focus_activity_item` - 其 gate 必须落到 `open_ui` - 不能伪装成 `auto_execute` 并且这条命令链现在已经补上了真正面向“单脑驾驶”的默认入口: - `driver-feed` - 看当前驾驶主线 - `driver-focus-preview` - 默认消费 `driver-feed.top_recommendation` - 没传 `entry_key` 时,不再要求人工先抄 `action_code` - 如果显式传入的是 `activity_focus.key` - 应自动收口为 `focus_activity_item` - 只做“进入工作区”的观察焦点,不升级成自动执行 - `driver-focus-run` - 先按同一条 focus 生成 `resolve / execute-resolved` 请求 - 自动兼容: - 普通 `driver_action` - `runbook_sequence` - `secondary` - `confirm` 这样后续海外 Codex 驾驶员拿到的就不是“很多离散脚本”,而是一条稳定的动作主线: 1. 看 `driver-feed` 2. 选 `top_recommendation` 3. 先 `preview` 4. 再按门禁 `run` 并且 `codex-focus-run` 已经开始真正落地“驾驶门禁”: - `safe_auto` - 直接执行 - `guarded_auto` - 默认不执行,只输出审阅结果 - 只有显式 `confirm` 才放行 - `resolve_first / open_ui / blocked` - 一律不执行,只给出原因和目标入口 现在这层门禁已经不再由 CLI 本地解释,而是由后端统一返回: - `selected_entry` - `preview` - `gate` - `execution_result` 这样后续海外 Codex 驾驶员就不是“会调接口的脚本”,而是严格受同一份后端 contract 和门禁规则约束。 这样后续不管是人工点击、CLI 脚本调用,还是海外 Codex 驾驶员接管,都不会再出现“页面这样理解、后端那样理解、节点执行又是第三套”的口径漂移。 Release Hub 自己也要走同一套“标准动作对象”思路,而不是继续靠页面里临时拼表单。 当前已经开始往这条线上收: - `发节点任务` 不再只是一个通用入口,而是正式区分: - `deploy.release.control` - `deploy.release.worker` - `deploy.release.custom` - 同时命令行侧的 `check_release_hub.sh` 也会直接输出: - release 模板摘要 - 控制面批量预检 - Worker 批量预检 - 混合节点预检 这样页面按钮、海外 Codex 驾驶员、CLI 自检脚本三者看到的就是同一套发布口径,而不是三套不同的发布逻辑。 这一步非常关键,因为它把: - Node Agent 接管 - Ops Job - Release Hub 第一次真正接成了一条连续工作流。 --- ## 十一、正式协议收口原则 为了让后续开发不再反复改口径,这套体系必须先明确 6 条协议原则: ### 1. 前后端与 Codex 共用同一份 contract 同一个动作、同一个节点、同一轮 rollout,必须由后端返回统一结构: - 页面直接展示 - Codex 直接判断 - CLI 直接检查 不能让三边各自“再推导一次”。 ### 2. 所有远控动作都要先变成对象 最终要统一成: - `release` - `rollout` - `ops job` - `playbook run` - `activity` 而不是: - 页面点按钮直接跑 shell - Codex 临时拼命令 - 节点本地脚本自己决定做什么 ### 3. Node Agent 只执行已声明动作 Agent 不应该接受任意 shell 文本,而应该只接受标准动作: - `service.start` - `service.stop` - `service.restart` - `service.status` - `health.snapshot` - `logs.collect` - `diagnostics.collect` - `deploy.release` 这样控制面才能做审计、门禁、批量预检和回滚。 ### 4. 所有对象都必须能审计 至少都要带: - `created_at` - `updated_at` - `requested_by` - `target_node_code / target_nodes` - `status` - `result` 后面不管是人、后台还是 Codex 都要能回放过程。 ### 5. 协议必须支持“逐步增强” 当前已经落下的是第一版骨架,所以协议要区分: - `当前已实现字段` - `后续建议补充字段` 这样可以先稳定主链路,再逐步增强,而不是一开始就推翻。 ### 6. API 返回必须统一包裹 当前所有接口都已经走: ```json { "code": 0, "message": "ok", "data": {} } ``` 所以后面所有新增接口也应继续遵守: - `code=0` 表示成功 - `code=1` 表示业务失败 - 具体错误语义继续放进 `message` 和后续建议补的 `detail_code` --- ## 十二、Node Agent HTTP 协议 ### 1. 公共约束 #### 请求头 Agent 相关接口统一使用: - `Content-Type: application/json` - `X-Domaincheck-Agent-Token: ` 其中: - `tokens` - `bootstrap-plan` 是控制面主动签发,不要求 agent token。 而以下接口要求带 token: - `register` - `heartbeat` - `pull` - `jobs/{id}/start` - `jobs/{id}/complete` - `jobs/{id}/events` #### 公共节点载荷 当前代码里的 `_base_payload()` 已经收敛出最小节点信息: - `node_code` - `region` - `role` - `title` - `hostname` - `ip` - `agent_version` - `capabilities` - `labels` - `metadata.service_names` - `metadata.delivery_queue` 这意味着后续页面、Codex 和 agent 日志里看到的节点身份,应该都围绕这组字段统一。 其中 `metadata.delivery_queue` 当前已经是正式 contract,而不是预留字段。它至少会带: - `state` - `label` - `reason` - `pending_count` - `dead_letter_count` - `last_flush_at` - `oldest_pending_at` - `oldest_dead_letter_at` 也就是说,heartbeat 不只是在汇报“Agent 还在线”,也在汇报: > 当前本地是否有待补发回执、是否已经出现死信、最近一次补发冲刷是否成功 --- ### 2. 签发 Agent Token `POST /api/v1/ops/agent/tokens` #### 请求体 ```json { "node_code": "mainland-worker-02", "issued_by": "web-ui", "expires_in_hours": 72, "metadata": { "source": "ops-center" } } ``` #### 当前返回字段 - `token` - `token_preview` - `record_id` - `node_code` - `expires_at` - `created_at` #### 设计要求 这一步只做“签发”,不做安装、不做纳管、不做启动。 也就是说: - token 是凭证对象 - bootstrap plan 才是接入方案对象 --- ### 3. 生成 Bootstrap Plan `POST /api/v1/ops/agent/bootstrap-plan` #### 请求体 ```json { "node_code": "mainland-worker-02", "node_region": "mainland", "node_role": "worker", "issued_by": "web-ui", "expires_in_hours": 72, "control_plane_base_url": "https://ops.example.com", "root_dir": "/opt/domaincheck", "metadata": { "source": "ops-center" } } ``` #### 当前返回字段 当前后端 `build_node_agent_bootstrap_plan()` 已返回: - token 相关字段 - `control_plane_base_url` - `bootstrap_plan.node_code` - `bootstrap_plan.node_region` - `bootstrap_plan.node_role` - `bootstrap_plan.root_dir` - `bootstrap_plan.env_file` - `bootstrap_plan.service_name` - `bootstrap_plan.service_file` - `bootstrap_plan.install_script_path` - `bootstrap_plan.env_content` - `bootstrap_plan.command_lines` - `bootstrap_plan.command_block` - `bootstrap_plan.health_checks` - `bootstrap_plan.health_check_block` - `bootstrap_plan.bootstrap_script_name` - `bootstrap_plan.bootstrap_script_path` - `bootstrap_plan.bootstrap_script_content` - `bootstrap_plan.bootstrap_run_script_block` #### 正式定位 `bootstrap_plan` 不只是“给一段命令”,它是: > 控制面签发给目标节点的一份标准接管方案 后续: - 页面按钮 - Codex 驾驶员 - CLI 辅助脚本 都应该复用这一个对象,不再自己拼接 env 或 shell。 --- ### 4. Agent Register `POST /api/v1/ops/agent/register` #### 请求体 ```json { "node_code": "mainland-worker-02", "region": "mainland", "role": "worker", "title": "mainland-worker-02", "hostname": "host-a", "ip": "10.0.0.12", "agent_version": "0.1.0", "capabilities": [ "service.start", "service.stop", "service.restart", "service.status", "health.snapshot", "logs.collect", "diagnostics.collect", "deploy.release" ], "labels": {}, "metadata": { "service_names": { "api": "domaincheck-api", "worker": "domaincheck-worker", "sync_agent": "domaincheck-sync-agent", "node_agent": "domaincheck-node-agent" } } } ``` #### 当前返回字段 - `node_code` - `expires_at` - `capabilities` #### 后端当前行为 当前 `agent_register()` 会: - 校验 token 与 `node_code` - 调 `_upsert_agent_runtime()` - 更新 `ops_managed_nodes` 所以 register 的实际语义是: > 让节点正式进入“已接管候选 / 已纳管”目录 --- ### 5. Agent Heartbeat `POST /api/v1/ops/agent/heartbeat` #### 请求体 当前与 register 共用同一份最小节点载荷。 #### 当前返回字段 - `node_code` - `server_time` - `expires_at` #### 协议语义 heartbeat 不是简单“在线 ping”,而是: - 刷新 last seen - 刷新节点身份与 service_names - 维持控制面里 Node Agent 在线态 - 同步回执队列现场快照 #### 当前已落地的队列快照字段 当前 heartbeat 已经会通过 `metadata.delivery_queue` 上报: - `state` - `healthy` - `retrying` - `dead_letter` - `pending_count` - `dead_letter_count` - `last_flush_at` - `oldest_pending_at` - `oldest_pending_request_id` - `oldest_pending_kind` - `oldest_dead_letter_at` - `oldest_dead_letter_request_id` - `oldest_dead_letter_kind` #### 后续建议补充字段 后续建议 heartbeat 再扩: - `active_jobs` - `disk_free_mb` - `load_average` - `python_version` 其中 `local_queue_depth` 这一类信息,当前已经由 `delivery_queue` 快照承接,不再是空白能力。 --- ### 6. Agent Pull `POST /api/v1/ops/agent/pull?limit=1` #### 请求体 ```json { "node_code": "mainland-worker-02" } ``` #### 当前后端行为 当前 `agent_pull_jobs()` 只派发: - `target_node_code = 当前节点` - `status = queued` - `execution_mode = remote-agent` 并在派发时自动改成: - `ops_jobs.status = dispatching` - `ops_job_steps.status = dispatching` 然后写入事件: - `agent_dispatched` #### 当前返回字段 ```json { "jobs": [ { "id": 123, "job_code": "ops-xxx", "action": "health.snapshot", "target_node_code": "mainland-worker-02", "status": "dispatching", "execution_mode": "remote-agent", "payload": {}, "metadata": {}, "policy": {}, "steps": [] } ], "count": 1 } ``` #### 正式约束 后续就算扩到批量拉取,也应继续遵守: - agent 只能拿到属于自己的 job - agent 不负责选择 job - 调度权始终在控制面 --- ### 7. 标记任务开始 `POST /api/v1/ops/agent/jobs/{job_id}/start` #### 请求体 ```json { "node_code": "mainland-worker-02" } ``` #### 当前后端行为 当前 `agent_mark_job_started()` 会: - 把 `ops_jobs.status` 改成 `running` - 把同 job 的 steps 改成 `running` - 写入 `agent_started` #### 设计要求 这一步必须保留,不能省略。 因为: - `dispatching` 表示控制面已派发 - `running` 表示节点已真实开始执行 这两者对排障非常重要。 --- ### 8. 标记任务完成 `POST /api/v1/ops/agent/jobs/{job_id}/complete` #### 请求体 ```json { "node_code": "mainland-worker-02", "client_request_id": "complete-ops-123-1", "status": "success", "stdout": "", "stderr": "", "result": { "summary": "ok" }, "error_message": "" } ``` #### 当前允许状态 - `success` - `failed` - `partially_succeeded` #### 当前后端行为 `agent_complete_job()` 会: - 校验 job 属于该节点 - 基于 `client_request_id` 做完成回执幂等 - 更新 `ops_jobs.status` - 写入 `result_json` - 回写 `error_message` - 更新全部 `ops_job_steps` - 写入 `agent_completed` - 如果 job 属于 rollout,自动刷新 rollout 汇总 #### 正式约束 节点只负责回写: - 结构化结果 - stdout/stderr - 失败原因 不负责自己修改 rollout 策略、release 状态或下个批次。 这些都必须仍由控制面决定。 #### 当前已落地的幂等键 当前 `complete` 已支持: - `client_request_id` 控制面会把它落到: - `ops_jobs.last_agent_complete_request_id` 因此同一个完成回执即使因为网络抖动被重复补发,也不会再次制造副作用。 --- ### 9. 上报中间事件 `POST /api/v1/ops/agent/jobs/{job_id}/events` #### 请求体 ```json { "node_code": "mainland-worker-02", "client_event_id": "event-ops-123-checksum-1", "step_id": 0, "event_type": "deploy_checksum_verified", "level": "info", "message": "checksum ok", "payload": { "checksum": "sha256:..." } } ``` #### 当前后端行为 `agent_append_job_event()` 会: - 校验 token 和 node_code - 基于 `client_event_id` 做事件幂等 - 追加结构化事件 - 返回 `event_id` #### 正式要求 中间事件不能只传纯文本,至少还要带: - `event_type` - `level` - `message` - `payload` 因为后面: - 页面活动流 - rollout 详情 - playbook run events - Codex 驾驶员 都依赖结构化事件,而不是只靠日志文本猜。 #### 当前已落地的事件去重约束 当前事件表已经有: - `ops_job_events.client_event_id` - `uq_ops_job_events_job_client_event` 所以同一个事件即使被 Agent 重放,也应继续表现为: - 控制面接收成功 - 不重复制造第二条事件 --- ## 十三、Node Agent 状态机 ### 1. Token 状态机 - `issued` - `disabled` - `expired` 当前代码里 `ops_node_tokens` 已具备: - `is_enabled` - `expires_at` - `last_used_at` 所以 token 生命周期应该正式解释成: - `issued + enabled + 未过期`:可用 - `issued + disabled`:不可用 - `issued + 已过期`:不可用 --- ### 2. 节点接管状态机 页面与控制面应该统一理解为: - `未纳管` - `待接入` - `已接管` - `仅运行态在线` - `心跳过期` - `token 过期 / 已禁用` 建议正式映射为: - `discovered` - `pending_bootstrap` - `agent_online` - `runtime_only` - `stale` - `token_invalid` 前端显示名可以中文化,但后端内部最好固定英文状态码。 --- ### 3. Job 状态机 当前主链路已经形成: - `queued` - `dispatching` - `running` - `success` - `failed` - `partially_succeeded` 正式语义应固定为: - `queued`:已创建,尚未派发 - `dispatching`:控制面已派发给节点,但节点尚未确认开工 - `running`:节点已开始执行 - `success`:完成且成功 - `failed`:完成且失败 - `partially_succeeded`:部分成功,需要人工关注 这套状态机后面不能再随意改名,否则活动流、巡检收口、rollout 汇总都会被打散。 --- ## 十四、`ops job` 的 Agent 执行载荷标准 这部分后续建议直接以 [ops_job_contract.md](/www/wwwroot/getDomain/docs/schemas/ops_job_contract.md:1) 为任务对象母本,再由 [ops_agent_protocol.md](/www/wwwroot/getDomain/docs/schemas/ops_agent_protocol.md:1) 继续冻结 Agent 视角下的拉取、开工、完工与事件回执协议。 ### 1. Agent 可见 job 最小字段 Agent 从 `pull` 接口拿到 job 时,最少应该依赖这些字段: - `id` - `job_code` - `action` - `target_node_code` - `execution_mode` - `payload` - `metadata` - `policy` 其他字段如: - `requested_by` - `risk_level` - `approval_status` - `rollout_id` 虽然不是最小执行必需,但建议一起保留,便于未来 agent 诊断与本地审计。 ### 2. 结构化动作必须优先 当前 agent 已按动作名分发: - `supports_structured_action(action)` - `execute_structured_action(...)` - `execute_release_action(...)` 这条设计必须坚持: - 先结构化动作 - 再结构化 payload - 最后才在执行器内部落成本地命令 不能反过来回到“控制面下发整段 shell”。 ### 3. `deploy.release` 的 payload 基线 当前 release 执行器已经支持的正式意图至少包括: - artifact 下载 - checksum 校验 - 解压到 `releases/` - 切换 `current` - 重启服务 - 健康检查 - 失败回滚 因此 `deploy.release` 的 payload 应长期固定围绕: - `release_version` - `artifact_url` - `checksum` - `restart_services` - `health_check_urls` - `health_check_timeout_seconds` - `health_check_retries` - `health_check_interval_seconds` - `rollback_on_failure` - `switch_current` 这套字段已经足够支撑正式 release 部署。 ### 4. `logs.collect` 的口径 标准巡检里的 `logs.collect` 应优先解释为: > 收集目标服务日志,而不是执行任意日志命令 正式建议字段: - `service_name` - `lines` - `include_agent_logs` - `since_seconds` 并且默认仍要优先收口: - `domaincheck-worker` - `domaincheck-node-agent` 而不是允许页面任意填系统命令。 --- ## 十五、Release Hub 正式契约 ### 1. Release 对象 当前 `ops_releases` 已具备: - `release_version` - `channel` - `commit_sha` - `status` - `artifact_url` - `checksum` - `notes` - `metadata` - `created_by` - `created_at` - `activated_at` 现在正式 contract 还需要进一步固定这些别名字段: - `status_label` - `summary` - `summary_text` - `focus_ref` 正式上应把 Release 理解为: > 一份不可变版本记录 也就是说: - release 创建后,不应该允许直接把它重新指向另一份 artifact - 同一 `release_version` 只对应一份发布物 ### 2. Rollout 对象 当前 `ops_release_rollouts` 已具备: - `release_id` - `rollout_code` - `target_selector` - `target_nodes` - `policy` - `status` - `batch_cursor` - `batches_total` - `jobs_total` - `jobs_created` - `result_summary` 而 rollout 这一层也不该再只返回“原始推进计数”,还应稳定返回: - `status_label` - `target_node_codes` - `summary` - `summary_text` - `focus_ref` 所以 rollout 的正式定义应为: > 某个 release 在某批目标节点上的一次分批推进计划 ### 3. `default_rollout_gate` 这层是 Release Hub 最关键的后端 contract,必须作为正式对象保留。 至少应返回: - `status` - `status_label` - `status_type` - `summary` - `summary_text` - `release` - `execution_mode` - `execution_mode_label` - `target_node_codes` - `default_target_node_codes` - `blocking_reasons` - `warning_reasons` - `recommendations` - `operational_readiness.summary` - `operational_readiness.rows` - `focus_ref` #### 推荐状态码 - `missing_release` - `release_not_ready` - `artifact_missing` - `no_targets` - `blocked` - `attention` - `ready` 这组状态码后面必须同时服务: - 首页驾驶建议 - Release 页面门禁卡 - Rollout 预检 - Codex 自动驾驶 ### 4. `release_launchpad` 除了默认门禁外,还应保留更偏“驾驶舱”的发布聚合对象: - `latest_package` - `latest_release` - `worker_rollout_preview` - `control_rollout_preview` - `launchpad_status` 其中 `launchpad_status` 至少应回答: - 当前应该先补什么 - 推荐动作码是什么 - 推荐执行方式是什么 - 是先看 Worker 预案还是先看 Control 预案 - 当前是否还存在接入缺口 - 首个缺口节点是谁 - 这个缺口是 - `bootstrap_pending` - 还是 `acceptance_ready` 并且这层也应该直接给出: - `summary_text` - `focus_ref` - `recommended_target_node_code` - `recommended_recovery_label` - `recommended_recovery_summary` - `onboarding_bootstrap_pending_nodes` - `onboarding_acceptance_ready_nodes` 其中 `focus_ref` 不该只是“回到 release hub”这么模糊,而应该允许继续携带: - `section` - `mode` 这样页面、CLI、Codex 都不需要自己再推断“该跳回 release hub 的哪个区域”。 这里再固定一个已经收口成代码字段的判断口径: - 如果 `launchpad_status.recommended_action_code` 已经落在 - `bootstrap_run` - `run_acceptance` - 那么页面、CLI、Codex 不应再只显示笼统的“待补执行面” - 而应直接把以下字段作为主提示: - `recommended_target_node_code` - `recommended_recovery_label` - `recommended_recovery_summary` - `onboarding_bootstrap_pending_nodes` - `onboarding_acceptance_ready_nodes` 原因是: - 真正的阻断并不总是“发布包有问题” - 很多时候是“发布包已经好了,但节点还没接管完” - 如果 launchpad 只返回一个大而化之的 `attention` 前端和 Codex 还得再各自遍历 gap rows 去猜 - 这会重新制造多套判断逻辑 所以现在要把 launchpad 的缺口口径固定成: - `launchpad_status` 直接给驾驶层结论 - `gap_rows` 继续给详情层做下钻 而不是反过来让驾驶层去解析详情层。 当前仓库里这套骨架已经存在,所以后续不要再新造第四套发布摘要模型。 --- ## 十六、Rollout 状态机 当前 rollout 状态已经接近正式版,建议固定为: - `planned` - `running` - `awaiting_approval` - `ready_for_next_batch` - `halted` - `completed` - `completed_with_issues` ### 正式语义 - `planned`:已创建,尚未开始发批次 - `running`:当前批次已发出,正在等待 job 结果 - `awaiting_approval`:下一步推进需要人工确认 - `ready_for_next_batch`:当前批次通过,可进入后续批次 - `halted`:由于失败、阻断或人工暂停而停止 - `completed`:全部批次完成且整体健康 - `completed_with_issues`:全部批次结束,但存在失败或告警 后续不建议再引入新的近义状态名,否则页面和 Codex 会反复适配。 --- ## 十七、错误码、幂等与重试规则 ### 1. `detail_code` 已经进入 Agent / Ops 主链路 现在统一响应体已经有: - `code` - `message` - `data` 而且 `detail_code` 已经开始进入顶层或 `data` 中,成为控制面和 Agent 都能消费的结构化错误语义。 这层现在的意义不再是“未来建议”,而是: > 不再靠中文 message 猜失败原因,而是让页面、Codex 和 Node Agent 共用同一份错误码 当前主链路里的错误返回,已经会继续补: - `detail_code` 例如: - `agent_token_invalid` - `agent_token_expired` - `agent_token_node_mismatch` - `ops_job_not_owned_by_agent` - `ops_job_invalid_status_transition` - `release_checksum_mismatch` - `release_health_check_failed` 这样页面和 Codex 就不用再从中文 message 里反推错误类型。 ### 2. 幂等 contract 当前已经正式落地两条关键幂等线: - `complete` - 使用 `client_request_id` - 回写到 `ops_jobs.last_agent_complete_request_id` - `events` - 使用 `client_event_id` - 通过 `uq_ops_job_events_job_client_event` 去重 这意味着 Agent 在遇到控制面瞬断、网络抖动或重试补发时,已经具备: - 回执不重入 - 事件不重复落库 后续仍建议继续保持幂等的动作有: 以下动作应该尽量幂等: - `register` - `heartbeat` - `bootstrap-plan` - `activate release` - `rollout preview` 也就是重复请求最多刷新状态,不应制造重复副作用。 ### 3. Agent 重试与本地补发规则 建议正式约束,并且当前主链路已经落下一版: - `register` 失败:指数退避重试 - `heartbeat` 失败:短退避重试,不立即退出 - `pull` 失败:进入轮询退避 - `start/complete/events` 失败:本地保留待补发队列 其中现在已经正式落地的是: - `complete` - `events` 这两类回执在失败时会进入 Agent 本地待补发队列,而不是直接丢失。 ### 4. 当前已经落地的本地待补发 / 死信队列 当前 `domain-api/app/node_agent.py` 已经具备: - 本地 `pending` 队列 - 本地 `dead-letter` 队列 - 定时 `_flush_delivery_queue()` - 对永久性 `detail_code` 的死信分流 也就是说,Agent 当前不是“请求失败就算了”,而是已经形成: 1. 优先即时发送 2. 失败则本地排队 3. 下个 tick 自动重放 4. 如果命中永久性错误,则进入死信 这层能力当前主要覆盖: - `jobs/{id}/complete` - `jobs/{id}/events` ### 5. 控制面现在如何看见队列问题 这条链路现在也已经不只存在于 Agent 本地。 当前控制面会把 heartbeat 里的 `metadata.delivery_queue` 收口成: - 节点行字段 - `delivery_queue_state` - `delivery_queue_label` - `delivery_queue_reason` - `delivery_queue_pending_count` - `delivery_queue_dead_letter_count` - 节点汇总字段 - `queue_retrying_nodes` - `queue_dead_letter_nodes` - `queue_pending_records` - `queue_dead_letter_records` 并且驾驶建议已经开始优先提升这类问题: - 如果存在死信节点,优先建议先处理 Agent 死信 - 如果存在待重试积压,优先建议先观察回执积压 所以这已经不是“隐藏在节点本地目录里的运维细节”,而是正式进入海外控制面的驾驶 contract。 --- ## 十八、终局验收标准 当这套体系真正算“上线可用”,至少要满足以下 10 条: ### 1. 新增节点不再需要人工拼命令 控制面能够直接生成: - token - env - bootstrap script - health check block ### 2. 海外控制面能先看见节点,再决定是否接管 不是只有接入成功后才看见机器。 ### 3. 接管完成后可直接跑标准验收 而不是再去手工 systemctl / journalctl。 ### 4. 标准巡检统一走 `ops job` / `playbook run` 而不是页面每个按钮都各做一套逻辑。 ### 5. 真正参与检测节点和在线未参与节点口径统一 页面、后端、Codex 必须读同一份 execution scene。 ### 6. 远端日志回传状态正式结构化 至少统一成: - `disabled` - `waiting_sample` - `partial_coverage` - `healthy` - `full_capture` ### 7. 发布必须先有 Release,再有 Rollout 不能回退到节点直接 `git pull`。 ### 8. Rollout 必须能分批、暂停、继续、回滚 而不是一次性把所有节点推平。 ### 9. 页面按钮和 Codex 都只创建对象,不直接执行 shell 最终: - 页面创建 `ops job` - Codex 创建 `ops job` / `playbook run` - agent 消费 `ops job` ### 10. 海外主机必须成为唯一运维大脑 大陆机器只负责: - 执行 - 回传 - 承载业务 不再承担“人工登录后做决定”的角色。 --- ## 十九、下一步最值得继续补的 6 件事 基于当前仓库状态,接下来最值得继续补的,不再是“把主链路补出来”,而是把已经成型的体系收成真正可长期运维的平台。 ### 1. 给死信队列补正式操作面 当前已经有: - 本地待补发 - 本地死信 - 控制面可见的队列快照 下一步最该补的是: - 死信详情查看 - 死信重放 - 死信丢弃 - 死信按 `detail_code` 聚类 否则“看见死信”之后,操作者还是得回到节点本地处理。 ### 2. 把 action template preview / batch policy preview 正式后端化 在真正创建 `ops jobs` 之前,要让控制面先统一回答: - 哪些节点被阻断 - 哪些节点需要审批 - 哪些节点只是 warning - 当前批次策略是否合理 这样发布、重启、停 Worker 这类高风险动作,才能在创建对象前先收口风险。 ### 3. 把接管验收、标准巡检、现场观察彻底收进 playbook run 现在 playbook run 骨架已经有了,而且其中一部分已经开始从“打开弹窗”进入“直接执行正式动作”。下一步要继续把: - `onboarding.acceptance` - `inspection.standard` - `scene.logs.key` - `scene.logs.full` 都变成真正稳定的编排对象,而不是“部分走编排、部分还停留在前端直触发”。 当前已经落地的直接驾驶动作包括: - `bootstrap_run` - `run_acceptance` - `run_standard_inspection` - `open_diagnostics` - `run_scene_logs_key` - `run_scene_logs_full` 这意味着纳管收口、标准巡检和现场观察都已经开始直接进入正式执行链,而不是先把操作者送去弹窗二次选择。 发布审阅链也应该遵守同样原则: - `review_smart_rollout_preview` - `review_control_rollout` - `fix_rollout_blockers` 这几类动作不应只返回“打开 launchpad 审阅页”的 UI intent,而要同时直接返回结构化 `release_launchpad_review` 结果,让自动驾驶、CLI 和后端任务能直接消费 launchpad 预案。 并且这里的自动化等级也要彻底锁死: - 这三类动作属于 `safe_auto` - 它们的职责是“审阅”和“解释” - 不是“创建发布对象” 所以默认返回应是: - 结构化 `release_launchpad_review` - 可选 `ui_intent = release_launchpad_review` - 但不直接创建 rollout / publish 记录 同理,`focus_release_hub` 也不应继续只是一个纯导航意图: - 页面侧仍可消费 `ui_intent = focus_release_hub` - 但后端同时还应返回 `release_hub_preview` 至少包含: - `summary` - `launchpad` 这样海外 Codex、CLI、driver-feed 调用方即使不打开页面,也能继续拿这份摘要决定后续是: - 继续审阅 launchpad - 进入发布创建 - 还是先回补门禁缺口 `release_package` 也应遵守同样的“先给后端摘要,再保留 UI 入口”原则: - 页面仍可通过 `ui_intent = open_release_dialog` 进入打包区 - 但动作返回里还应包含 `release_package_preview` 至少包括: - `available` - `package_name` - `release_version_suggestion` - `reason` 这样 Node Agent、Release Hub、海外 Codex 驾驶员三边的判断都能先基于同一份“发布包现场状态”,而不是必须打开页面后才能知道有没有包可发。 同理: - `open_release_dialog` 也不应只是纯导航 应同时返回 `release_dialog_preview` - `open_rollout_dialog` 也不应只是纯导航 应同时返回 `rollout_dialog_preview` 这样就算暂时仍保留页面入口,自动驾驶层也已经可以先读: - Release 当前摘要 - Launchpad 当前摘要 - 发布包是否存在 而不是必须依赖前端抽屉作为唯一信息入口。 继续往下一层,模板类动作和发布部署模板动作也应该统一走“预览优先”: - `open_playbook_dialog` 应返回 `playbook_preview` - `open_action_template_dialog` 应返回 `template_preview` - `open_release_deploy_worker / open_release_deploy_control / open_release_deploy_custom` 应返回 `release_deploy_preview` 其中 `release_deploy_preview` 至少应包含: - `template` - `summary` - `launchpad` - `target_node_codes` - `execution_mode` - `release_id` 这样自动驾驶层拿到的就不是“去某个弹窗”,而是一份已经够继续判断的执行预案。 再往前,焦点类动作也不应继续只是“跳到某个详情页”: - `focus_playbook_run` 应返回 `playbook_run_focus` - `open_playbook_run_latest_events` 应返回 `playbook_run_events_preview` - `focus_latest_job_events` 应返回 `job_events_preview` - `focus_activity_item` 应返回 `activity_focus_preview` 这样 Codex、CLI、OpsCenter 三边在进入具体页面之前,就已经能先读到: - 当前编排主状态 - 最近事件流 - 当前 job 回执 - 当前 activity 焦点对象 后续“跳到页面”就只是补充查看,而不是唯一信息入口。 这里还要把动作分级口径正式固定下来: - 只要动作已经满足: - 后端可直接执行 - 不修改现场状态 - 主要返回 `preview / focus / summary / events` - 那么它就不应继续归在 `ui_only` - 而应统一归到 `safe_auto` 当前已经应按这个口径归类的动作包括: - `handover_first_gap` - `view_first_gap` - `open_playbook_dialog` - `open_action_template_dialog` - `focus_playbook_run` - `focus_activity_item` - `focus_latest_job_events` - `open_playbook_run_latest_events` - `open_release_dialog` - `open_rollout_dialog` - `open_release_deploy_control` - `open_release_deploy_worker` - `open_release_deploy_custom` - `focus_release_hub` - `release_package` 也就是说,页面仍然可以消费这些动作的 `ui_intent`,但 Codex / CLI / 自动驾驶网关已经可以把它们当成: - “可直接执行的只读观察动作” 而不是: - “必须先打开某个页面才能继续” 而真正进入发布创建层的动作,例如: - `publish_latest_worker` - `create_smart_release_rollout_worker` - `create_smart_release_rollout_control` - `create_release_rollout_worker` - `create_release_rollout_control` 则必须继续保持: - `guarded_auto` - `confirm_then_execute` - 正式写入发布与审计链 ### 4. 把 Release Artifact 管道做成正式不可变发布链 现在 Release / Rollout 对象已经成型,下一步最关键的是补齐: - 构建产物打包 - checksum / manifest - 上传与留存 - channel 激活 - 回滚版本选择 也就是把“版本对象存在”继续推进到“版本供应链完整”。 ### 5. 把 SSH Rescue Executor 收成首发接管与紧急救援闭环 终局不是回到 SSH 主运维,但 SSH 仍应有正式位置: - 首次 bootstrap - Node Agent 尚未接入时的应急接管 - Agent 故障时的救援执行 这一层应继续做成正式 executor,而不是保留为人工口令。 ### 6. 把 Node Agent / ReleaseHub / OpsCenter contract 固化为 schema / OpenAPI 现在最怕的已经不是“没有能力”,而是: > 页面、后端、Codex、CLI 在继续迭代时发生字段漂移 所以后续上线前,建议正式抽出: - `schemas/ops_agent_protocol.md` - `schemas/release_hub_contract.md` - `schemas/ops_driver_contract.md` - `schemas/ops_playbook_contract.md` 并且不只要有 schema 文件,还要让控制面直接暴露统一索引: - `GET /api/v1/ops/contracts` - `GET /api/v1/ops/contracts/{contract_key}` 前者负责列出整张 contract map,后者负责按 key 钻取单份协议和相关 contract。 控制面前端与海外 CLI 也要统一走这两层入口: - 页面默认只拉 registry,避免首屏过重 - 当用户点击某个 contract 时,再按 key 拉 detail - detail 返回里必须带 `related_contract_keys` / `related_contracts` - 这样排查一条链路时,可以从 `ops_stack_diagnosis_contract` 直接跳到 `ops_driver_contract`、`release_hub_contract`、`ops_agent_protocol` 或者直接写入后端 schema / OpenAPI 注释,让 contract 成为真正受版本控制的对象。 --- ## 二十、海外 Codex 驾驶员的正式闭环 如果要把“海外主机单脑控制面”真正做成长期稳定体系,海外 Codex 驾驶员不应该被设计成: - 远端 shell 执行器 - 看日志后临时拼命令的脚本 - 依赖人工不断复制上下文的助手 它应该是严格跑在统一 contract 上的“驾驶层”。 ### 1. 默认入口只看 3 类信号 海外 Codex 驾驶员默认只消费: 1. `driver-feed` 2. `codex-brief` 3. `activity-stream` 分别回答: - 现在最该处理什么 - 这一步能不能自动执行 - 最近执行后现场发生了什么 也就是说,驾驶员不应该先去翻零散日志,再猜下一步,而是先看控制面已经结构化好的主线。 ### 2. 默认执行顺序必须固定 标准顺序应固定成: 1. `driver-feed` - 取 `top_recommendation` 2. `driver-focus-preview` - 看 contract / gate / request preview 3. `driver-focus-run` - 只有后端 gate 允许时才执行 4. `activity-stream` - 回看这一步是否真正落地 5. 必要时钻取: - `playbook run` - `ops job` - `release / rollout` 这样驾驶员并不是“直接开车”,而是: - 先看路标 - 再看交通规则 - 然后再执行 ### 3. 驾驶员永远不直接决定 shell 正式规则必须收死: - 驾驶员不直接 SSH 到节点上拼 shell - 驾驶员不自己判断某台机器该不该 `git pull` - 驾驶员不自己决定 systemd 命令如何组合 - 如果控制面已经把首个缺口节点收敛成 `bootstrap_run / run_acceptance` - 驾驶员也不能退回成泛化的 `fix_managed_nodes` - 而应优先围绕同一个节点继续做预览、执行和回执追踪 驾驶员只做三件事: 1. 选择动作对象 2. 请求后端解析门禁 3. 创建正式执行对象 最终执行必须落到: - `ops job` - `playbook run` - `release / rollout` - `node agent task` 这样才具备: - 审计 - 回放 - 回滚 - 风险分级 ### 4. 驾驶员必须把“看见问题”和“处理问题”拆开 终局方案里最容易犯的错,就是把: - 看见节点离线 - 打开日志 - 重启服务 - 改配置 - 再观察 全部写成一段混杂的脚本。 正确方式应该拆成两层: - 观察层 - `driver-feed` - `activity-stream` - `inspection` - `release launchpad` - 执行层 - `driver-actions/execute-resolved` - `ops job` - `playbook run` - `rollout` 这样后面无论是: - 人工值班 - 海外 Codex 自动驾驶 - 后台一键按钮 都能共用同一套观察口径与执行口径。 补充一条正式约束: - `driver-feed` - 顶层 payload 需要带 `contract_navigation` - 每条 `entries` - 每条 `activity_focus` 都要带 `contract_navigation` - `activity-stream` - 顶层 payload 需要带 `contract_navigation` - 每条 `items` 都要带 `contract_navigation` 这样海外单脑控制面里的卡片、时间线、CLI drilldown、Codex 驾驶员才能共享同一条“协议跳转链”,而不是 UI 有一套、CLI 有一套、排障时再临时翻文档。 ### 5. 驾驶员需要一套明确的升级路径 真正上线时,建议把海外 Codex 驾驶员分成 4 档能力,而不是一步到位全自动: #### L1 观察驾驶 - 只看 `driver-feed / codex-brief / activity-stream` - 只输出建议,不执行 #### L2 门禁驾驶 - 可以跑 `driver-focus-preview` - 可以对 `safe_auto` 动作执行 `driver-focus-run` - 对 `guarded_auto` 只提示确认 这里的典型分界现在应明确到动作名,避免后续接入新驾驶员时再自己猜: - `safe_auto` - `review_smart_rollout_preview` - `review_control_rollout` - `fix_rollout_blockers` - `run_standard_inspection` - `run_scene_logs_key` - `run_scene_logs_full` - `guarded_auto` - `publish_latest_worker` - `create_smart_release_rollout_worker` - `create_smart_release_rollout_control` - `create_release_rollout_worker` - `create_release_rollout_control` #### L3 编排驾驶 - 可以创建: - `playbook run` - `ops job` - `release / rollout` - 但仍不直连 shell #### L4 救援驾驶 - 仅在 Node Agent 失效或接管初期 - 允许走受控 `SSH Rescue Executor` - 仍然必须把动作写回控制面审计链 这样自动化能力能逐档放开,而不是一开始就把所有权力交给一段脚本。 ### 6. 海外主机最终应该成为唯一“决策写入口” 终局不是“海外主机能看到更多日志”,而是: > 所有决策类动作,都只能从海外控制面写入。 这意味着: - 大陆节点不再自己 `git pull` 决定版本 - 大陆节点不再靠人工 SSH 决定是否重启 - 大陆节点只负责执行、回传、承载业务 而海外主机负责: - 产物选择 - 风险判断 - 动作签发 - 回执汇总 - 运行态总览 这才是真正的“单脑控制面”。 ### 7. 对你这个项目,最推荐的默认日常路径 如果按最省心、后续最少返工的标准,建议以后日常操作尽量收成下面这条路径: 1. 海外主机打开 OpsCenter 2. 看 `driver-feed.top_recommendation` 3. 先 `driver-focus-preview` 4. 再 `driver-focus-run` 5. 如果动作升级,进入: - `inspection` - `playbook run` - `release hub` 6. 如果执行异常,看: - `activity-stream` - 死信队列 - 回执补发状态 7. 只有在 Agent 不可用时,才进入 SSH Rescue 这样后续再新增 1 台大陆机器,不会再变成“重新写一套部署说明”,而只是: - 新节点 bootstrap - Node Agent 注册 - 控制面验收 - 进入正式运维 架构不会再被机器数量拖垮。 --- ## 二十一、死信队列必须从“可见”升级到“可操作” 当前这套体系已经能看见: - 哪台节点存在 `dead_letter` - 有多少条死信 - 最早死信时间是什么 这说明“现场可见性”已经有了。 但如果没有正式操作面,控制面还是会卡在最后一步: - 看到死信 - 给出建议 - 最后仍然要人工 SSH 去处理 这就违背了“海外单脑控制面”的目标。 所以这层必须继续升级成正式对象: - 单节点队列总览 - 队列记录列表 - 死信详情 - 单条重放 - 批量重放 - 丢弃 - 主动冲刷 其中第一阶段已落地: - `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` 当前这组入口明确采用: - `record_visibility=head_only` 也就是控制面先稳定看到: - 节点级队列状态 - `pending / dead_letter` 头部记录 而不是假装已经具备远端全量死信列表与直接操作能力。 也就是说现在已经进入“可操作的第一阶段”: - 控制面能看节点级队列状态 - 控制面能看 `pending / dead_letter` 头部记录 - 控制面能通过正式 `ops job` 触发冲刷 / 重放 / 丢弃 - 但还没有把远端全量记录目录完整投影成控制面对象 并且这第一阶段现在已经不是“后端接口预留”,而是已经形成了三层统一操作面: - OpsCenter 托管节点表可直接打开“回执队列治理”抽屉 - driver recommendation 遇到 `dead_letter / retrying` 时,会优先落到标准动作模板 - 海外单入口 CLI 已支持直接查看 / 冲刷 / 重放 / 丢弃 也就是说当前这层已经具备明确的人机协作分工: - 页面负责看现场、做单条处理、做危险动作确认 - driver / Codex 负责把“当前最该处理什么”压成建议 - CLI 负责在海外主机上做统一批处理和交接执行 具体到当前仓库,已经形成下面这些稳定入口: ### 1. 页面入口 - OpsCenter 托管节点表: - `队列` - `立即冲刷` - `重放死信` - `单条重放` - `单条丢弃` - 丢弃动作强制要求填写原因 - 当前页面仍明确遵守: - `record_visibility=head_only` 也就是页面不会假装自己能枚举远端完整死信目录,而是只围绕“当前可见头部记录”和“节点级摘要”操作。 ### 2. 驾驶建议入口 当前 `driver_recommendations` 已经升级成: - `dead_letter` - 主动作:直接执行 `replay_delivery_queue` - 后端落点:创建 `delivery.queue.replay` 的标准 `ops job batch` - 次动作:查看 Worker 日志 - `retrying` - 主动作:直接执行 `flush_delivery_queue` - 后端落点:创建 `delivery.queue.flush` 的标准 `ops job batch` - 次动作:查看 Worker 日志 这一步很关键,因为它意味着控制面不再只是说“这里有死信,你自己想办法”,也不再只是把人送去模板弹窗,而是已经能直接走正式 `ops job` 链路下发队列治理动作。 ### 3. 海外单入口 CLI 当前 `drive_ops_center.sh` 也已经补上了这组命令: - `bash domain-api/deploy/multi-region/drive_ops_center.sh queue-status ` - `bash domain-api/deploy/multi-region/drive_ops_center.sh queue-records [state]` - `bash domain-api/deploy/multi-region/drive_ops_center.sh queue-flush [limit] [requested_by]` - `bash domain-api/deploy/multi-region/drive_ops_center.sh queue-replay [limit] [requested_by]` - `bash domain-api/deploy/multi-region/drive_ops_center.sh queue-replay-record [requested_by]` - `bash domain-api/deploy/multi-region/drive_ops_center.sh queue-discard-record [requested_by]` - `bash domain-api/deploy/multi-region/drive_ops_center.sh activity-stream [key=value ...]` - `bash domain-api/deploy/multi-region/drive_ops_center.sh activity-preview [activity_key] [requested_by]` - `bash domain-api/deploy/multi-region/drive_ops_center.sh driver-feed` - `bash domain-api/deploy/multi-region/drive_ops_center.sh driver-focus-preview` - `bash domain-api/deploy/multi-region/drive_ops_center.sh driver-focus-run` - `bash domain-api/deploy/multi-region/drive_ops_center.sh codex-brief` - `bash domain-api/deploy/multi-region/drive_ops_center.sh codex-focus-preview` - `bash domain-api/deploy/multi-region/drive_ops_center.sh codex-focus-run` 建议把这组命令视为“海外集中治理 Node Agent 回执队列”的正式入口,而不是临时调试命令。 并且现在这组命令的输出口径也要冻结下来: - `activity-stream / driver-feed / codex-brief` - condensed summary 必须打印顶层 `contract_navigation` - `activity-preview / driver-focus-preview / codex-focus-preview / stack-next` - condensed summary 必须打印 `preview.contract_key / contract_version / contract_primary_endpoint / preview_endpoint / schema_doc_path` - 所有 `selected_entry` - 必须尽可能保留 `contract_navigation` 这样海外 CLI、页面按钮、未来海外 Codex 驾驶员看到的就不是三套不同“解释文本”,而是一套可追踪到协议、接口和 schema 文档的正式输出。 同理,海外单脑控制面导出的交接包也不能只停留在: - `stack summary` - `ops plane` - `participation` 还应该把 contract-aware 驾驶面一起固化进去: - `contracts registry` - `driver-feed` - `activity-stream` - `codex-brief` - `stack-next preview` - `driver-focus preview` - `activity preview` - `codex-focus preview` 并且 preview 类报告允许“当前没有焦点但仍导出文件”,文件尾部只记录 `command_exit_code=1`,这样交接人拿到包以后不会误解成“导出中断”,而能明确判断是“现场当时没有可预览入口”。 进一步讲,这个交接包还应该再固定一层机器摘要: - `manifest.json` 它不是替代原始报告,而是给海外 Codex / 后台自动驾驶 / 未来 OpsCenter UI 二次消费的摘要面。 正式口径应该固定成: - `meta.json` - 负责说明这包是谁、何时、针对哪个 profile / 哪组 API 生成的 - `manifest.json` - 负责说明这包里每份报告是否存在、exit code 是多少、哪些 preview 当前可用、现场暴露了哪些 contract key - 如果某个 required 报告已经解析出 `http_status` 且不是 `2xx`,也必须记入 `required_failures` - `*.txt` - 保留完整原始现场输出,供人类继续钻取 并且 `doctor-export` 命令自己的 stdout 也应该回传一份缩略版 `summary + decision`,这样海外 Codex 不必先打开文件树,也能先做第一轮分流判断。 进一步往终局落地时,CLI 侧还应该固定一个更适合机器先消费的统一入口: - `doctor-decision` 正式定位应该是: - `doctor-export` - 负责生成完整交接包 - `doctor-decision` - 负责读取 `manifest.json` - 如果没有现成 `manifest.json`,就临时生成一份 export - 然后只回传 `summary + decision` - 并且允许用 `-` 明确关闭 overseas 拓扑探测,避免因为默认海外地址不可达把第一轮分流卡死 这里的 `summary` 不能只做 surface 成败汇总,还要继续固定带出 launchpad 接管口径: - `launchpad_recommended_target_node_code` - `launchpad_recommended_recovery_label` - `launchpad_recommended_recovery_summary` - `launchpad_onboarding_bootstrap_pending_nodes` - `launchpad_onboarding_acceptance_ready_nodes` 同时,`decision.evidence` 也要镜像这组字段,确保海外 Codex / 自动驾驶器 / 人工值班 在只消费 `doctor-decision` 的前提下,就能直接判断: - 当前最该先接哪台节点 - 当前是“还缺 bootstrap”还是“已经 ready,等待 acceptance” - 是否需要继续下钻 `release-launchpad / driver-feed / codex-brief` 这样海外单脑控制面的“第一跳”就稳定了: - 海外 Codex 先跑 `doctor-decision` - 后台自动驾驶按钮先跑 `doctor-decision` - 人工值班也先看 `doctor-decision` 并且从当前实现口径上,`doctor-decision` 不应再只消费旧的 `next_step_action_code`,而要优先消费: - `diagnosis.operator_decision` - `diagnosis.next_actions` - `diagnosis.recommended_commands` 这意味着如果总检已经明确判定“先看现场日志 / 先打通节点接管 / 先看 Release Hub”,那么 `doctor-export` 产出的 `manifest.json` 与 `doctor-decision` 最终给出的 `recommended_commands` 也必须同步落到同一条主决策,不允许再次退化成泛化建议。 只有当 `decision.status` 显示需要继续钻取时,才去打开整包 `*.txt` 和 `manifest.json` 深挖。 而且 `doctor-export / doctor-decision` 这一层本身也应该固定成“步骤级超时继续导出”: - 某个 surface 卡住,不允许整包诊断悬死 - required report 失败,要进入 `required_failures` - required report 失败明细,要进入 `required_failure_details` - optional preview 不可用,要进入 `optional_unavailable` - optional preview 不可用明细,要进入 `optional_unavailable_details` - 海外单脑永远优先拿到一份可判断的 `decision`,而不是拿到一个还在转圈的进程 并且这些明细不能只停留在“文件名失败”: - `reports[].failure.reason_code` - `reports[].failure.label` - `reports[].failure.detail` 至少要能稳定区分: - `command_timeout` - `command_failed` - `http_unreachable` - `http_error` - `result_unparsed` 再往前一步,控制面 API 自身也应该暴露正式“运行指纹”: - `/health` - `/api/v1/runtime/status` - `/api/v1/runtime/build-info` 这些接口至少应该稳定给出: - `build.source` - `build.package_name` - `build.generated_at` - `build.commit_sha` - `build.commit_ref` - `build.manifest_path` - `build.route_surface.surface_complete` - `build.route_surface.missing_paths` 这样以后看到 `ops/contracts` 返回 404 时,就不用再靠猜: - 如果 `build` 缺失,说明运行实例还没切到新版本 - 如果 `build` 存在但 `route_surface` 缺路由,说明当前实例加载的仍然不是完整协议面 - 只有 `build` 与 `route_surface` 都完整,才值得继续往 preview / execute 深挖 ### 4. 当前阶段的明确边界 当前仍然要刻意坚持下面这几点,避免以后又滑回“假装可控”: - 不直接 SSH 进节点手改 `pending / dead-letter` 文件 - 不允许页面、CLI、Codex 各自定义不同的 replay / discard 逻辑 - 所有操作都必须落成正式 `ops job` - 单条 replay / discard 当前只允许针对控制面可见头部记录 这样未来从 `head_only` 升级到“远端全量记录正式投影”时,扩的是可见范围,不是重写整个治理模型 推荐以后直接以 [ops_agent_protocol.md](/www/wwwroot/getDomain/docs/schemas/ops_agent_protocol.md:133) 为母本,把 `delivery_queue` 从 heartbeat 快照扩到控制面操作面。 正式要求应该固定成: - Agent 只负责补发与执行 - 控制面负责查看、聚类、重放、丢弃 - 页面、CLI、Codex 不允许各自发明不同的死信处理逻辑 --- ## 二十二、Playbook Run 必须成为正式一等对象 当前仓库里 `playbook` 已经不只是“预留想法”,而是已经有: - catalog - preview - execute - run detail - run events - rerun - cancel 这说明骨架已经具备,只差把 contract 冻结清楚。 后续最关键的不是再补更多“脚本型能力”,而是把这几类正式收口: - `onboarding.bootstrap` - `onboarding.acceptance` - `inspection.standard` - `scene.logs.key` - `scene.logs.full` - `scene.diagnostics` 也就是说: - 接管验收 - 标准巡检 - 现场观察 都应该优先变成 `playbook run`,而不是继续散落在: - 页面按钮 - Driver Action 临时逻辑 - 手工命令 推荐以后直接以 [ops_playbook_contract.md](/www/wwwroot/getDomain/docs/schemas/ops_playbook_contract.md:1) 为正式母本。 这样后续: - 页面点“标准巡检” - CLI 执行“现场观察” - 海外 Codex 驾驶员决定“先验收还是先抓日志” 最终看到的都是同一类对象: - 先 `preview` - 再 `execute` - 最后围绕 `playbook run / playbook events` 观察 这样控制面才算真正从“按钮集合”进化成“正式编排平台”。 --- ## 二十三、单脑控制面最终要固定成 4 张操作面 Node Agent、Release Hub、Driver、Playbook 这些听起来很多,但终局页面不应该把操作者淹没在概念里。 最终应该稳定成 4 张操作面。 其中“现场面 + 队列面”建议以后直接以 [ops_observability_contract.md](/www/wwwroot/getDomain/docs/schemas/ops_observability_contract.md:1) 为正式母本,避免后面又把这层能力重新拆散到页面局部状态里。 ### 1. 接管面 负责回答: - 哪些节点已经纳管 - 哪些节点只是集群可见但未纳管 - 哪些节点 token 过期 - 哪些节点 agent 心跳异常 - 哪些节点已经通过接管验收 这一面主要消费: - `ops_agent_protocol` - `managed nodes` - `bootstrap-plan` - `onboarding.acceptance` ### 2. 现场面 负责回答: - 哪些节点正在执行 - 哪些节点在线但未参与 - 哪些节点近窗有吞吐 - 日志回传当前是否关闭 / 关键 / 全量 - 当前应该优先巡检哪台节点 这一面主要消费: - `overview` - `inspection` - `activity-stream` - `driver_recommendations` - `playbook runs` ### 3. 队列面 负责回答: - 哪台节点的回执队列有积压 - 哪台节点有死信 - 头部记录是什么 - 当前应该冲刷、重放,还是丢弃 这一面主要消费: - `delivery_queue` - `delivery_queue.records` - `flush / replay / discard` 这里要持续坚持: - 当前阶段允许 `head_only` - 但不允许退回人工 SSH 改文件 ### 4. 发布面 负责回答: - 当前默认 release 是谁 - 默认 gate 是 `blocked / attention / ready` - 哪些节点可以灰度 - 哪一批 rollout 正在执行 - 哪一轮 rollout 需要审批 / 暂停 / 回滚 这一面主要消费: - `release_hub_contract` - `launchpad` - `default_rollout_gate` - `rollouts` 这样页面心智模型才会稳定: - 接管面:先让节点进体系 - 现场面:看现场和巡检 - 队列面:处理回执不一致 - 发布面:推进版本与回滚 这 4 张面板不是 4 套系统,而是: > 同一套控制面 contract 的 4 个观察窗口 --- ## 二十四、新节点从“拿到机器”到“可以发版”必须走标准路径 后续不应该再写“某台机器特殊处理说明”,而要把新节点接入路径固定为下面 8 步。 ### 1. 签发接入凭证 控制面生成: - token - bootstrap plan - env 内容 - systemd 文件 - 启动检查命令 这一步之后,页面、CLI、Codex 都只能复用同一份 bootstrap 对象。 ### 2. 节点安装 Node Agent 节点完成: - 安装 agent - 写入 env - 启动 systemd 此时节点还只是: - `agent installed` 还不算已经正式接管成功。 ### 3. 节点注册并进入托管目录 节点完成: - `register` - `heartbeat` 控制面能看见: - 节点身份 - capabilities - service_names - delivery queue 摘要 这时节点进入: - `managed candidate` ### 4. 立即跑接管验收 控制面不应等待人工另开命令,而应立刻执行: - `onboarding.acceptance` 必要时自动追加: - `inspection.standard` 这样新节点接入不会停留在“能连上就算完成”,而会直接走到: - 基础服务是否正常 - 日志是否可收口 - 诊断是否可采 - worker / api 拓扑是否正确 ### 5. 如果有问题,先走现场面而不是手工 SSH 验收失败后,优先动作应该是: - 查看 `activity-stream` - 打开 `playbook run detail` - 看 `inspection` - 看 `delivery queue` 只有 Node Agent 已明显不可用,才进入 SSH Rescue。 ### 6. 验收通过后进入发布门禁 节点通过验收后,不应马上让它进入发布目标集合,而是先进入: - release gate readiness 也就是明确判断: - agent 在线否 - inspection 健康否 - delivery queue 正常否 - 是否允许 remote-agent rollout ### 7. 先进入 Worker / Control 正确角色集合 接入后必须明确: - 它是 `worker` - 还是 `control` - 是否 `effective_worker` - 是否允许参与 rollout 不能让“纳管成功”和“自动进入发版目标”之间没有门槛。 ### 8. 最后才进入 Release / Rollout 到这一步才允许: - worker canary - batch rollout - advance / pause / rollback 这条标准路径的真正意义是: > 新节点接入不再是一次性脚本,而是正式运维生命周期的开始 --- ## 二十五、后续实现时绝不再回退的 10 条边界 为了避免系统越来越大以后又退回人工联调,下面 10 条边界建议直接视为终局铁律。 ### 1. 不再把 `git pull` 当正式发布方式 正式发布只认: - release artifact - checksum - rollout ### 2. 不再让节点自己决定版本 节点只能执行: - 下载 - 校验 - 切换 - 回滚 不能自己挑版本。 ### 3. 不再让页面、CLI、Codex 各自拼动作语义 所有动作必须收口到: - action template - playbook - rollout - delivery queue action ### 4. 不再把 SSH 当日常主链路 SSH 只保留: - bootstrap - rescue ### 5. 不再让回执队列治理脱离 `ops job` 无论: - flush - replay - discard 都必须形成正式留痕。 ### 5.1 不再让发布兜底路径退回“打开面板” `release_progression` 现在也应该遵守同一原则: - 有明确 Launchpad 推荐时,优先走 `publish_latest_worker` - 已经存在默认 Release、但 Launchpad 还没形成明确推荐时,兜底也要直接走 `create_release_rollout_worker` 也就是说,不能因为“还没形成完整 launchpad 推荐”就退回到“打开 Worker 点状灰度面板”这种 UI 导航式动作。 ### 6. 不再让“在线”直接等于“参与检测” 必须明确区分: - 在线 - 参与执行 - 在线待命 - 近窗有吞吐 - 负载同步中 ### 7. 不再让页面自己猜跳转落点 后端必须给: - `ui_intent` - `run_code` - `activity_key` - `focus_step_key` ### 8. 不再让日志体系只靠全量大文本 必须分层: - event - playbook / job log - runtime log - diagnostics bundle ### 9. 不再把 root/www 混合工作区当正常运维模型 正式版只允许: - release build side - release deploy side 角色分清,而不是在目标节点长期维护一份脏 git 工作区。 ### 10. 不再让“设计口径”和“实现口径”分裂 所以现在开始,后续新增能力都建议优先先补到: - `ops_agent_protocol.md` - `ops_driver_contract.md` - `ops_playbook_contract.md` - `release_hub_contract.md` 再去写页面和执行器。 这样你这套系统才会越来越像: > 正式控制平台 而不是: > 一直迭代的一组强力调试脚本 ## 二十六、海外单脑控制面的总检入口也要正式化 上面整套设计如果只停留在: - 有很多 endpoint - 有很多检查脚本 - 有很多 contract 文档 那后面现场一复杂,还是会退回“先想想这次该跑哪个脚本”。 所以海外单脑控制面还需要一个固定的总检入口: - `check_ops_center_stack.sh` 它的职责不是替代: - `check_ops_contracts.sh` - `check_ops_plane.sh` - `check_node_agent.sh` - `check_release_hub.sh` 而是把这四层先按固定顺序收口成一次总览: 1. 先看 API 与 contract registry 2. 再看 `link_snapshot / overview` 当前驾驶主线 3. 再看托管节点、Node Agent、回执队列 4. 最后看 ReleaseHub 与最近 playbook / activity 这样后续海外主机第一次接手现场时,默认起手式就固定为: ```bash cd /opt/domaincheck/domain-api bash domain-api/deploy/multi-region/check_ops_center_stack.sh http://127.0.0.1:8100 # 如果只想看最终收口,不展开全量原始 JSON bash domain-api/deploy/multi-region/check_ops_center_stack.sh http://127.0.0.1:8100 summary ``` 它本质上回答的是 5 个问题: - 当前控制面跑的是哪一版 contract - 当前最该处理的 driver recommendation 是什么 - 当前有哪些节点已经被正式接管、哪些还没进入稳定执行面 - 当前回执队列有没有死信或待补发积压 - 当前 ReleaseHub 是 ready / attention / blocked 哪一种 而在正式实现上,这份总检结果不应该只停留在“把几段 JSON 拼起来”,而是要直接形成一层可被人和程序共用的诊断输出: - `diagnosis.surface_status` 说明总检表面层是不是完整联通 - `diagnosis.automation_status` 说明海外单脑自动化执行面是不是已经真正可接管 - `diagnosis.stack_status` 作为总检最终结论 - `diagnosis.issues` 作为问题清单 - `diagnosis.next_step` 作为默认下一步动作 - `diagnosis.quick_commands` 作为现场立刻可复制执行的命令集合 这样一来,总检结果就不只是“读给人看”的说明书,而是后续: - 海外 Codex 驾驶 - 后台按钮自动诊断 - 交接包导出 - 标准作业入口编排 都可以直接消费的正式 contract。 对应的正式入口也应该固定为: - `GET /api/v1/ops/stack-diagnosis` CLI 总检脚本只是它的终端包装层,而不应该继续长期承担“总检规则真正定义者”的角色。 进一步说,CLI 默认动作入口也应该固定下来: - `bash domain-api/deploy/multi-region/drive_ops_center.sh stack-next` 它不再要求海外操作人先从总检结果里人工抄: - `action_code` - `focus_ref` - `recommended command` 而是直接消费: - `diagnosis.next_step` 然后统一进入: - `driver-actions/resolve` - `driver-actions/execute-resolved` 这一步很重要,因为它把“总检”和“下一步执行”正式接成一条链: - 页面顶部总检入口 - 海外 CLI 单入口 - 后续海外 Codex 驾驶员 都不再自己猜“下一步该做什么”,而是统一信任一份正式 contract。 如果现场执行 `stack-next` 时拿到的是: - `404 Not Found` 那也不要把它理解成“设计不成立”,而应理解为: - 运行中的控制面 API 版本还没升级到包含 `/api/v1/ops/stack-diagnosis` - 或代码已同步,但 `domaincheck-api` 尚未重启 也就是说,后续现场排障里,`404` 属于: - 部署版本未对齐问题 而不是: - 总检 contract 本身设计错误 这一步很关键,因为它把“海外主机的第一反应”也变成正式 contract 的一部分,而不是继续依赖人工记忆。 也就是说,未来不管是: - 人工运维 - 海外 Codex 驾驶员 - 后台按钮触发自动诊断 都应该优先先看这份总检结果,再决定下一步钻取哪一层。 只有这样,这套体系才真正具备: > 单入口观察,分层下钻,协议不漂移 ## 二十七、终局实现顺序不能再乱 为了避免后面继续一边联调、一边改口径,这套体系后续实现顺序必须固定。 建议严格按下面 6 段推进: ### 1. 先冻结 contract,再扩页面和执行器 优先冻结的母本应固定为: - [ops_stack_diagnosis_contract.md](/www/wwwroot/getDomain/docs/schemas/ops_stack_diagnosis_contract.md:1) - [ops_driver_contract.md](/www/wwwroot/getDomain/docs/schemas/ops_driver_contract.md:1) - [ops_job_contract.md](/www/wwwroot/getDomain/docs/schemas/ops_job_contract.md:1) - [ops_agent_protocol.md](/www/wwwroot/getDomain/docs/schemas/ops_agent_protocol.md:1) - [release_hub_contract.md](/www/wwwroot/getDomain/docs/schemas/release_hub_contract.md:1) - [ops_observability_contract.md](/www/wwwroot/getDomain/docs/schemas/ops_observability_contract.md:1) 只有这些 contract 先稳住,页面按钮、CLI、Codex、Node Agent 才能围绕同一套结构化对象工作。 ### 2. 再固定控制面的统一执行链 控制面日常动作主链必须固定为: 1. `stack-diagnosis` 2. `driver-actions/resolve` 3. `driver-actions/execute-resolved` 4. `ops job` 5. `Node Agent / rollout executor` 6. `event / complete / activity` 也就是说: - 页面不直接执行壳命令 - CLI 不直接拼最终执行接口 - Codex 不直接猜动作 target api 所有高层动作都先下沉为统一 resolve / execute-resolved contract。 ### 3. 再把 Node Agent 执行面补齐 Node Agent 侧必须先补齐: - `register` - `heartbeat` - `pull` - `jobs/{job_id}/start` - `jobs/{job_id}/complete` - `jobs/{job_id}/events` - 本地待补发 / 死信队列 直到这条链稳定之前,不应把“节点能 SSH 上去”误判成“执行面已经正式接管”。 ### 4. 再把 Release Hub 收成正式发布面 Release Hub 后续必须坚持: - 包只认 artifact - 发布只认 rollout gate - 批次推进只认正式 rollout / ops job - 节点不允许直接 `git pull` 这样后续: - worker canary - controller rollout - 失败回滚 - 灰度暂停 才会继续沿用同一个正式模型,而不是变回“发布时再单独写一套命令”。 ### 5. 最后才是 SSH Rescue SSH 不应再作为日常主链路,只保留给下面几类场景: - Node Agent 尚未接管 - Node Agent 失联 - 运行环境损坏到 agent 无法自救 - release 切换半途损坏,需要紧急修复目录或 systemd 而且即使走 SSH,也要继续满足: - 创建正式 `ops job` - 留下执行人、节点、动作来源 - 回写控制面审计链 ### 6. 页面和 Codex 只能做“消费者”,不能做第二大脑 终局架构里: - 页面负责展示和触发 - CLI 负责终端入口 - Codex 负责智能判断和编排 但三者都不能再自己定义: - 新状态机 - 新动作对象 - 新门禁口径 - 新下一步推断逻辑 否则“海外单脑控制面”就又会退化成三套脑子。 ## 二十八、上线前必须通过的收口验收 如果以后要说“这套系统已经进入正式长期可维护状态”,至少应通过下面 10 条验收。 ### 1. 总检入口必须真实可用 下面命令必须能在海外控制面直接打通: ```bash cd /opt/domaincheck/domain-api bash domain-api/deploy/multi-region/check_ops_center_stack.sh http://127.0.0.1:8100 summary ``` 而且结果里必须能给出: - `diagnosis.stack_status` - `diagnosis.issues` - `diagnosis.next_step` ### 2. 默认下一步必须可以直接消费 下面命令必须成立: ```bash bash domain-api/deploy/multi-region/drive_ops_center.sh stack-next http://127.0.0.1:8100 ``` 也就是说,现场不应再需要人工从总检结果里抄: - `action_code` - `focus_ref` - `command` ### 3. 所有标准动作都必须落为正式对象 后续任何标准动作都必须能落成: - `driver action` - `ops job` - 或 `playbook run` 不允许再出现“后台按钮一按,后端直接去某台机器执行一个临时命令,但控制面里没有正式记录”。 ### 4. Node Agent 接管和 runtime 心跳必须明确区分 控制面必须能够直接区分: - `已接管` - `仅运行态在线` - `待接入` - `心跳过期` - `失联` 绝不能再用“API 能看到节点在线”代替“Node Agent 已正式接管”。 ### 5. 回执队列必须能治理,不只是能看见 必须能在页面或海外单入口完成: - `queue-status` - `queue-records` - `queue-flush` - `queue-replay` - `queue-discard-record` 否则后续只要出现一次跨地域回执积压,就又会退回到手工删文件。 ### 6. 远端日志回传必须可开关 多机联调时,海外控制面必须能够: - 默认关闭,避免长期噪声 - 调试时打开,拿到远端关键或全量日志 - 调试结束后关闭 否则后续每新增一台大陆机器,都会重新回到“复制日志给 Codex 看”的低效模式。 ### 7. Release Hub 必须可执行 canary 到 rollout 至少要能稳定完成: - 选默认版本 - 看 rollout gate - 发 worker canary - 看结果 - 继续批次 - 异常暂停 / 回滚 ### 8. 节点发布必须只认 artifact,不认 git pull 一旦进入正式执行链,节点侧必须坚持: - 不 `git pull` - 不直接以运行目录为发布源 - 不允许 controller / worker 各自拉不同本地临时代码 ### 9. 救援必须是正式例外,不是常态 如果某类动作每次都需要: - SSH 上机 - 手工重启 - 手工改 env - 手工粘日志 那说明它还没有进入正式执行面,不能算收口完成。 ### 10. 页面、CLI、Codex 三端口径必须一致 同一条动作在三端看到的: - 状态 - 下一步 - 风险说明 - focus_ref - target_api 必须一致。 只有这样,后面你才不会再遇到: - 页面说能做 - CLI 说不能做 - Codex 又判断成第三种情况 ## 二十九、后续绝不能回退的 8 种反模式 后续无论现场多急,都不建议再回到下面这些模式。 ### 1. 新增页面按钮,直接在后端执行 shell 这是最容易把平台重新打回“脚本集合”的退路。 正确做法应始终是: - 先 resolve - 再 execute-resolved - 最终落成 `ops job` ### 2. 页面、CLI、Codex 各自拼 request payload 一旦三端自己拼: - `action_code` - `target_api` - `focus_ref` - `execution chain` 后面就一定会再次口径漂移。 ### 3. 用 runtime 心跳冒充正式接管 “节点在线”不等于: - Node Agent 已接管 - 可以标准发布 - 可以标准巡检 - 可以标准回执 这两者必须永远分开。 ### 4. 把发布流程重新做成节点本地更新 正式发布链绝不应回退到: - `git pull` - 覆盖运行目录 - 临时复制文件 正确做法只能是: - 构建 artifact - 生成 rollout - 由执行面标准接收和切换 ### 5. 把 SSH 当成默认执行器 SSH 只能是: - bootstrap - rescue - 临时过渡 而不能成为: - 日常发布 - 日常巡检 - 日常日志拉取 - 日常服务控制 的默认方式。 ### 6. 故障时重新回到“复制日志给 Codex” 终局目标是: - 海外控制面统一拿日志 - Codex 直接消费控制面 contract 而不是每次故障又退回: - 现场机器跑命令 - 人工复制粘贴 - 再让 Codex 二次解释 ### 7. 新增机器时再写一份特殊部署说明 以后新增大陆 controller / worker,不应再写: - “这台机器例外” - “这台机器单独执行某个手工命令” - “这台机器按另一套说明” 而应继续只走: - bootstrap plan - Node Agent 接管 - onboarding acceptance - 标准巡检 / rollout ### 8. 因为赶时间,就跳过正式审计链 只要动作会改变远端节点现场,就必须有: - 发起人 - 动作来源 - 目标节点 - 执行结果 - 回执事件 否则后续节点一多,你就再也无法回答: - 是谁改的 - 为什么改 - 哪次改坏了 - 哪台节点没跟上 这 8 条如果长期守住,后面这套系统才不会越做越散,而会越来越像真正的: > 海外单脑控制平台 ## 三十、这一轮继续冻结的 4 组关键字段 这轮设计继续往前收口时,有 4 组字段必须正式冻结,否则后面最容易再次出现“页面一套、后端一套、Codex 一套”的漂移。 ### 1. Execution Scene 的参与态字段必须固定 现在“节点在线”和“节点真正参与检测”已经被证明不能混为一谈。 所以后续 `execution_scene` 里每个节点都应该优先稳定返回: - `participation_state` - `participation_label` - `participation_reason` 推荐值直接固定成: - `dispatch_active` - `recent_only` - `standby` - `load_syncing` 这样页面“参与检测节点”、CLI 摘要、海外 Codex 驾驶员才能直接区分: - 在线但未参与 - 正在领任务 / 正在执行 - 近窗刚参与但当前已收口 - 控制面和现场仍在同步 这层后续建议直接以 [ops_observability_contract.md](/www/wwwroot/getDomain/docs/schemas/ops_observability_contract.md:1) 为观察面母本。 ### 2. 远端日志回传必须从“有无样本”升级成“覆盖到哪些节点” 远端检测日志回传后续不能再只回答: - 开了没 - 有样本没 这还不够支撑多机现场。 正式 contract 还要继续稳定返回: - `source_node_summaries` - `covered_participating_node_summaries` - `missing_participating_node_summaries` 也就是说,控制面要能直接回答: - 哪些参与节点已经有样本 - 哪些参与节点还没有样本 - 每台已覆盖节点当前拿到了多少关键行 / 全量行 - 最近一条样本来自谁 这样后面你不需要再自己翻日志去判断“到底是哪台机器没回传”,页面和 Codex 都能直接聚焦缺口节点。 ### 3. Node Agent 拉到的任务包裹必须固定 Node Agent 后续不能只拿到: - `action` - `payload` 然后自己去猜这是不是 rollout、是不是巡检、是不是日志采样。 正式 Agent Job Envelope 应继续带上: - `job_type` - `step_key` - `step_title` - `policy` - `release_context` - `focus_ref` 这样同一个 Agent 才能稳定承接: - 标准巡检 - Worker 日志 - 诊断收集 - Release 部署 - Rollout 回滚 这层后续建议直接以 [ops_agent_protocol.md](/www/wwwroot/getDomain/docs/schemas/ops_agent_protocol.md:1) 为执行面母本。 ### 4. Release Hub 不能只显示版本,还必须冻结 manifest / readiness / preview Release Hub 如果只停在: - latest release - rollout list 那本质上还是“看版本页面”,不是正式发布控制面。 接下来必须固定的 3 个发布对象是: - `artifact manifest` - `operational_readiness.rows` - `rollout preview` 它们分别回答: - 这个包到底会改哪些服务、怎么验、怎么回滚 - 哪台节点为什么能发 / 不能发 - 真正发 rollout 之前预计会分几批、风险在哪 这层后续建议直接以 [release_hub_contract.md](/www/wwwroot/getDomain/docs/schemas/release_hub_contract.md:1) 为发布面母本。 这 4 组字段一旦收死,后面再往下做页面、后端、CLI、Codex 自动驾驶时,就不会再反复经历: - 先做一个能跑的版本 - 现场一复杂就发现字段不够 - 再回头改 contract - 然后前后端一起返工 后续真正高效的节奏应该变成: 1. 先定 contract 2. 再做后端聚合 3. 再做页面展示 4. 最后接 Codex 自动驾驶 只有这样,Node Agent、Release Hub、OpsCenter 才会越来越像一个统一系统,而不是三块并排长大的模块。