1. 页面概览

1.1 是什么

「部署与漂移」页(对应前端源码 action/web/src/views/DeploymentPage.vue,页面标题「Apollo 部署与漂移」)是 LightApollo 控制面的部署执行与状态一致性演示台。它把「把期望状态下发到 Agent」到「检测并收敛实际状态漂移」的整条闭环搬到了页面上:你可以在本页为某个已激活的期望状态在指定 Agent 上发起一次部署;在部署列表中观察部署逐批推进;由于页面是纯 Pull 演示(部署实际由 Spoke Agent 周期性拉取执行),本页以「推进」弹窗来模拟 Agent 上报组件状态,从而驱动后端的就绪门控与状态机向前走;当实际状态与期望声明出现偏差时,本页还提供漂移检测与收敛能力,把漂移作为结构化 diff 落库并展示,必要时触发自动调和。

在整个 Apollo 产品的流程中,本页处于「期望状态 → 部署执行 → 运行收敛」的中段:期望状态的创建、激活与审批在「期望状态」管理页(部署总览等相邻页面)完成,渠道/策略(含 drift_policy)在「制品与渠道」与 GitOps 页面配置,Spoke Agent 在「Spoke Agent」页登记并拉取;本页则是唯一能主动「发起一次部署」并把 Agent 组件状态喂回后端的控制点,漂移检测的输入(期望声明 vs 实际状态 JSON)也在本页手工构造。输入是用户选择的期望状态 + Agent 名称 + 部署策略;输出是部署记录(六种状态)与漂移事件(带路径级 diff),收敛后由 Agent 重新拉取使实际状态回到期望。

典型使用链路(演示视角的完整闭环):① 先在相邻页面激活一条期望状态并确认组件声明完整(depends_on 依赖成 DAG);② 在本页「发起部署」选该期望状态 + Agent 名(如 app-001)+ 策略,观察列表出现 deploying 记录且首批组件已放行;③ 点击该行进详情,逐批用「推进」把当前批次组件上报 ready,观察 next_batch 放行下一批,直到全部组件 ready、部署变 synced;④ 制造并检测漂移:在「漂移检测」填入与期望不一致的 actual_state JSON 点「检测漂移」,事件列表出现带路径级 diff 的事件、关联部署变 drifting;⑤ 若该期望状态关联的同步目标 drift_policy=auto_fix,后端会自动把部署置回 pending 触发重调和,否则在事件上点「收敛(reconcile)」并让 Spoke Agent 重新拉取,最终状态回到 synced

1.2 核心价值/能力表

能力说明对应页面操作
发起部署选择 active 期望状态 + Agent + 策略,创建一条部署记录并放行首批组件「发起部署」表单
部署列表集中查看部署的 ID、期望状态、版本、Agent、策略、状态与开始时间页中部部署列表卡片
组件详情展开单条部署查看每个组件的类型/状态/就绪详情/错误点击部署行展开
模拟推进以 ready/failed/deploying 三种上报驱动就绪门控,放行下一批或触发回滚「推进」按钮 + 弹窗
手动 Sync对 failed/drifting/degraded 部署解除退避并打 sync 指令戳,强制 Agent 下一轮重调和「手动 Sync」按钮
漂移检测手工给 actual_state JSON,与期望声明做字段级 diff,落库并标记关联部署「漂移检测(drift check)」表单
漂移收敛指示 Agent 重新拉取应用,直到 diff 清空、部署恢复 synced漂移事件「收敛(reconcile)」
审计留痕发起/推进/失败/同步/漂移检测/收敛均写审计日志全部写操作

1.3 一句话总结

本页把「期望状态下发 → Agent 逐批就绪 → 漂移检测与收敛」的部署一致性闭环演示出来,并给出「发起部署、推进上报、手动 Sync、检测与收敛漂移」四类操作入口。

2. 访问入口

2.1 路由与菜单

路由 path/apollo/deployments
路由 nameApolloDeployments
meta.titleApollo 部署与漂移
侧边栏入口ApolloLayout 侧边栏「部署与漂移」,位于「部署总览」之后、「环境管理」之前
前端源码action/web/src/views/DeploymentPage.vue
API 客户端action/web/src/api/apolloClient.js
路由注册action/web/src/router/index.js/apollo 子路由 children 下,component: DeploymentPage,挂 ApolloLayout

相邻页(侧边栏顺序):部署总览 overview.html(前)、环境管理 environments.html(后)、Spoke Agent agents.html(Agent 拉取/上报的另一面)。

2.2 认证与权限

2.3 端口与 API 前缀

3. 界面布局

┌───────────────────────────────────────────────────────────┐
│ Apollo 部署与漂移                            [发起部署/收起表单] │
│ [操作结果提示条(v-if alert.message,可「关闭」)]              │
├───────────────────────────────────────────────────────────┤
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 发起部署(v-if showCreate,默认收起)                      │ │
│ │ 期望状态(须active)* | Agent 名称 * | 部署策略            │ │
│ │ [发起部署] [取消]                                        │ │
│ └────────────────────────────────────────────────────────┘ │
├───────────────────────────────────────────────────────────┤
│ ┌ 部署列表(loading/空态/表格 三态卡片)─────────────────────┐ │
│ │ ID 期望状态 版本 Agent 策略 状态 开始时间 操作             │ │
│ │ (行点击展开;展开行内:组件推进状态表 + 推进进度/就绪门控)  │ │
│ │ 操作列按状态:推进 / 收敛漂移 / 手动 Sync                  │ │
│ └────────────────────────────────────────────────────────┘ │
├───────────────────────────────────────────────────────────┤
│ ┌ 推进部署 #N(弹窗 modal)────────────────────────────────┐ │
│ │ 选择组件 | 上报状态 | 就绪快照(readiness JSON,选填)      │ │
│ │ [推进(POST advance)] [取消] · [推进结果 code block]      │ │
│ └────────────────────────────────────────────────────────┘ │
├───────────────────────────────────────────────────────────┤
│ ┌ 漂移检测(drift check)卡片──────────────────────────────┐ │
│ │ 期望状态 * | Agent 名称 * | 实际状态(actual_state JSON) │ │
│ │ [检测漂移]                                              │ │
│ └────────────────────────────────────────────────────────┘ │
│ ┌ 漂移事件卡片 ────────────────────────────────────────────┐ │
│ │ 事件 #id · 期望状态 #id · Agent · has_drift · 时间       │ │
│ │ [收敛(reconcile)]                                    │ │
│ │ diff JSON(code block)                                  │ │
│ └────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────┘
板块职责
页头 + 提示条标题、发起部署表单开关按钮、全局操作结果提示(成功/失败/信息)
发起部署卡片收集期望状态/Agent/策略,创建部署(默认收起,点「发起部署」展开)
部署列表卡片六种状态展示、点击行展开组件级详情、按状态提供推进/收敛漂移/手动 Sync
推进弹窗模拟 Agent 上报单个组件状态(ready/failed/deploying + 可选 readiness),驱动就绪门控
漂移检测卡片手工提供实际状态 JSON,触发字段级 diff 检测
漂移事件卡片展示历史漂移事件(含 diff),对单条事件执行收敛

4. 交互元素

4.1 页头与操作结果提示条

控件位置含义必填/默认/可用条件操作效果触发后端调用边界与细节
按钮「发起部署」/「收起表单」页头右侧切换发起部署表单显隐标签随状态切换:收起时显示「发起部署」,展开时显示「收起表单」点击反转 showCreate,表单在卡片中展开/收起纯前端显隐,不影响数据
提示条「关闭」顶部提示条右侧关闭当前操作结果提示有 alert.message 才显示alert.message='' 清空提示提示为单槽,新操作覆盖旧提示

提示条形态:<div class="alert" :class="alert.type">,类型有 alert-info(默认)、alert-successalert-error。发起成功/未检测到漂移等成功场景用 success;失败、检测到漂移、收敛中这类场景用 error。每条关键路径文案在下文逐条列出(按钮文字、提示文案与源码逐字一致)。

4.2 发起部署表单(卡片「发起部署」)

打开方式:点页头「发起部署」按钮。表单字段表:

字段类型必填校验/默认保存逻辑
期望状态(须 active)*下拉 v-model.number=startForm.desired_state_id仅列 activeDesiredStatesdesiredStates.filter(ds => ds.status==='active'));首项为禁用「请选择」(value null);选项文案 #{{id}} {{name}}(v{{version}});HTML required随请求体 desired_state_id 提交(number 保证为数字)
Agent 名称 *文本输入placeholder「如 app-001」;HTML required随请求体 agent_name 提交
部署策略下拉首项 value '' 文案「默认(default)」,其余为 policies 列表(value=策略名);空则回退 'default'随请求体 policy_name 提交(空串时前端补 'default'

提交流程(提交按钮文字「发起部署」,busy 时变为「发起中...」并禁用;旁边「取消」按钮纯关闭表单不清数据):

控件位置含义必填/默认/可用条件操作效果触发后端调用边界与细节
提交按钮「发起部署」表单底部提交发起部署busy 时禁用并显示「发起中...」;期望状态/Agent 为空时 HTML required 拦提交,JS 层再校验兜底成功提示 部署已发起 id=${data.id}(success),收起表单、清空三个字段并刷新部署列表;失败提示 发起部署失败:<错误详情>(error)POST /deployments/start,body {desired_state_id, agent_name, policy_name}发起前校验源码文案为「期望状态与 Agent 名称必填」;后端校验不通过返回 400/404/409(详见 7.1/7.2)

发起部署成功意味着:后端创建一条 pending→deploying 的部署记录 + 每组件一条 pending 的组件状态,并立即放行首批(无依赖层);记录 started_at;监控侧按组件声明探针登记健康检查(旁路,失败不阻断)。

4.3 部署列表卡片

加载三态:loading 时显示「加载中...」;deployments.length===0 时显示「暂无部署记录,请点击"发起部署"。」;有数据时渲染表格。列头依次为 ID / 期望状态 / 版本 / Agent / 策略 / 状态 / 开始时间 / 操作,单元格渲染:#{{dep.desired_state_id}}{{dep.bundle_version}}{{dep.agent_name}}{{dep.policy_name || 'default'}}、状态徽标、formatTime(dep.started_at || dep.created_at)(时间去掉 T 截取前 19 位,空则 -)。

状态徽标六种(class status-<status>):pending(灰)、deploying(蓝)、synced(绿)、drifting(橙)、degraded(紫)、failed(红)。语义:pending 等待 Agent 拉取或调和;deploying 进行中(已放行批次组件未全部就绪);synced 已收敛终态;drifting 漂移检测标记;degraded 降级(健康劣化/拉取失败重试聚合);failed 失败终态。

控件位置含义必填/默认/可用条件操作效果触发后端调用边界与细节
部署行列表 tbody单条部署始终可点行点击切换展开(同 id 再点折叠);展开异步并发拉详情与组件展开:GET /deployments/:id + GET /deployments/:id/components加载失败提示「加载部署详情失败:...」,不阻断列表
「推进」行操作列打开推进弹窗status==='pending'|'deploying' 显示打开推进弹窗(见 4.5)无(弹窗提交才调后端)若该行未展开,会先自动展开加载组件
「收敛漂移」行操作列对 drifting 部署收敛status==='drifting' 显示点击抛 ReferenceError,无任何效果(前端缺陷,见 8 章)该按钮绑定的 openReconcile 函数在 <script setup> 中未定义
「手动 Sync」行操作列解除退避并强制重调和status==='failed'|'drifting'|'degraded' 显示;busy 时禁用见 4.6POST /deployments/:id/sync高影响写操作,有原生 confirm 二次确认

busy 是页面级提交锁:任一写操作进行中,提交类按钮全部置灰(发起部署、推进、手动 Sync、检测漂移、收敛),避免并发提交制造混乱状态;操作完成(含失败)后统一复位。

4.4 部署详情展开区

点击部署行展开一个跨列 <tr>(class detail-row),内容包括:

控件位置含义必填/默认/可用条件操作效果触发后端调用边界与细节
组件状态表展开区组件级推进视图展开后异步加载展示各组件类型/状态/readiness/errorGET /deployments/:id/components组件状态按部署唯一索引 (deployment_id,name) upsert,幂等
行(再次点击)列表收起详情已展开的同 id 行折叠并清空 detailDeployment/componentStates「推进」成功后会再次调用 toggleDetail(id)同 id 会直接折叠——每推进一轮详情会自动收起,需重新点击行查看进度

4.5 「推进」按钮与推进弹窗(Agent 上报模拟)

行内「推进」(openAdvance)打开弹窗,标题 推进部署 #{{deployment.id}}(右上「关闭」可关)。弹窗字段与控件:

控件位置含义必填/默认/可用条件操作效果触发后端调用边界与细节
下拉「选择组件」弹窗表单本次上报哪个组件必填;选项为该部署的 componentStates(按 name);首项禁用「请选择组件」记录 advanceForm.name行未展开时组件异步加载,弹窗刚打开时选项可能为空,等详情加载完成再选
下拉「上报状态」弹窗表单该组件的模拟上报状态默认 ready;三选项:ready(就绪,放行下一批)failed(失败,触发自动回滚)deploying(部署中)记录 advanceForm.status语义由后端编排消化
文本域「就绪快照(readiness JSON,选填)」弹窗表单附带 readiness JSON选填;placeholder {"ready":true};内容非空时前端 JSON.parse解析成功后并入该组件 readiness非法 JSON 抛异常进 catch,提示 推进失败:Unexpected token ...,请求不发出
按钮「推进(POST advance)」弹窗底部提交组件上报advanceForm.name 为空或 busy 时禁用;busy 文案「推进中...」成功把返回结果 JSON 渲染到弹窗下方「推进结果」code block,并提示 推进完成:状态 ${status}(${ready_count}/${total_count});若有 next_batch 追加 ,放行下一批:${names.join(', ')}status==='failed' 时提示为 error 类型否则 success;随后刷新部署列表并(重新)展开该部署详情POST /deployments/:id/advance,body {deployment_id, agent_name, components:[{name, status, readiness?}]}失败提示「推进失败:...」;选择组件为空时提示「请选择组件」
按钮「取消」弹窗底部关闭弹窗始终可用关闭弹窗(不清结果)点遮罩(@click.self)也可关闭

前提提示:若当前部署行尚未展开,点「推进」会先异步加载组件,弹窗打开瞬间「选择组件」下拉可能是空的,需等详情加载完再选组件;已展开的行则可直接用上次加载的组件列表。

「推进」是把「就绪门控 + 超时 + 失败自动回滚」驱动起来的关键动作:上报 ready 的组件若声明了 readiness 探针,监控侧会执行探针,未通过则组件保持 deploying 并写错误(readiness 探针未通过(声明探针已由 monitoring 执行并落库));批次内全部 ready 才放行下一批(响应 next_batch);组件 deploying 超过策略就绪超时(默认 60s)被判 readiness 就绪超时(60s) 而置 failed;任一组件 failed → 见 5.4 失败与自动回滚。

4.6 「手动 Sync」按钮

列表行对 failed/drifting/degraded 状态显示「手动 Sync」。点击先弹原生 confirm:对部署 #{{id}}(Agent {{agent_name}},状态 {{status}})执行手动 Sync?\n将清除退避/失败状态并指示 Agent 强制重调和一轮。 取消则中止;确认后调 POST /deployments/:id/sync(body 无内容)。

控件位置含义必填/默认/可用条件操作效果触发后端调用边界与细节
「手动 Sync」行操作列解除退避/失败并强制重调和仅 failed/drifting/degraded;busy 禁用成功提示按响应分两种:带 noteSync 成功(${note});否则 Sync 成功:${from_status||原状态} → ${status},已打 sync 指令戳;随后刷新部署列表POST /deployments/:id/sync失败提示「手动 Sync 失败:...」;synced 终态被后端拒绝(409,文案「部署已收敛(synced),无需手动 sync」),但按钮不向 synced 显示

4.7 漂移检测表单(drift check)

卡片标题 漂移检测(drift check)。表单字段:

字段类型必填校验/默认保存逻辑
期望状态 *下拉 v-model.number=driftCheck.desired_state_id注意与发起部署不同:此下拉列全部 desiredStates(不做 active 过滤),选项文案 #{{id}} {{name}}({{status}});首项禁用「请选择」;requireddesired_state_id 提交
Agent 名称 *文本输入placeholder「如 drift-agent」;requiredagent_name 提交
实际状态(actual_state JSON)文本域 rows=4placeholder {"app":"demo-app","version":"9.9.9"};非空时前端 JSON.parse 为对象有值则随 actual_state 提交(对象),留空不传(后端按空对象判定)

提交按钮「检测漂移」(busy 时「检测中...」并禁用)。流程:空校验拦截(源码文案「期望状态与 Agent 名称必填」)→ POST /drift/check(body 含 desired_state_id/agent_name,actual_state 按解析结果附加)。结果按 has_drift 分两路提示:

非法 JSON 在前端 parse 即抛异常,走 catch 提示「漂移检测失败:Unexpected token ...」且不会发出请求。检测成功但后端报错时提示「漂移检测失败:<后端 message>」。

后端副作用:actual_state(缺省空对象)与期望声明(declaration JSON,含 policy.ignore_paths 忽略规则)做字段级 diff;有 diff 则写一条 drift_events(无 diff 不落库)并把该期望状态+Agent 关联的最近部署标记 drifting;随后按 drift_policy 评估 auto_fix(详见 5.4)。留空提交时因期望声明通常非空而实际为空对象,会产出大量 add 型 diff——基本必报漂移,这是演示语义而非缺陷,收敛要用漂移事件区的「收敛(reconcile)」配合 Agent 重拉。

4.8 漂移事件列表与「收敛(reconcile)」

卡片标题 漂移事件;无事件时显示「暂无漂移事件。」。每条 drift-item

控件位置含义必填/默认/可用条件操作效果触发后端调用边界与细节
「收敛(reconcile)」漂移事件条目对该事件收敛busy 时禁用弹 confirm:对漂移事件 #{{id}}(期望状态 #{{ds_id}},Agent {{agent_name||'-'}})执行收敛?;确认后请求POST /drift/:id/reconcile,body 为空对象 {}结果按 has_drift 提示:有漂移 收敛中(reconciling=true,已指示 Agent 重新拉取应用,剩余 ${diff.length} 项漂移)(error 型);无漂移 收敛完成(部署已恢复 synced)(success 型);随后刷新列表与事件

后端收敛语义:收敛以空 actual_state 重新执行一次漂移判定——只要期望声明非空就必然仍有 diff,于是返回 reconciling=true(若有 gitops 关联且 drift_policy 允许,还可能再次触发 auto_fix 把部署置 pending),真正的收敛靠 Spoke Agent 下一轮拉取重应用实现;只有「收敛」请求能携带精确 actual_state 时才会判定已收敛并把关联部署置回 synced(当前页面固定传空体,页面上的「收敛完成」提示需 Agent 已实际收敛后才能出现)。收敛对象取该事件记录的 desired_state_id + agent_name 对:即使收敛的是较早的事件,也会针对这一对当前的实际/期望状态重新判定,与事件新旧无关。失败提示「收敛失败:...」;事件 id 不存在后端 404。

5. 后端关联

5.1 API 客户端

action/web/src/api/apolloClient.js:axios 实例 baseURL /apollo-api/v1、timeout 30000ms、请求拦截器对所有请求附加 Authorization: Bearer <token>(token 取 apollo_token,无则回退 aip_token)。响应拦截器:HTTP 2xx 且 code===0 时把 response.data 替换为业务数据本身(页面 const { data } = ... 直接得到业务对象/数组);HTTP 2xx 但 code!==0 拒绝;4xx/5xx 时提取 body.message || body.error 并把 message 别名写入 error.response.data.error 后 reject——401 清 token 并跳 /apollo/login(登录页自身 401 不跳,避免死循环),403 alert('无权限执行该操作');blob/arraybuffer 下载与无数字 code 的裸响应原样放行。页面各 catch 统一用 err.response?.data?.error || err.message 展示后端错误。

5.2 端点表

方法路径权限点请求体/Query页面触发点
POST/deployments/startdeployment:execute{desired_state_id, agent_name, policy_name}发起部署
GET/deploymentsdeployment:read?agent=(可选过滤)加载部署列表
GET/deployments/:iddeployment:read-展开详情
GET/deployments/:id/componentsdeployment:read-展开详情组件表
POST/deployments/:id/advancedeployment:execute{deployment_id, agent_name, components:[{name,status,readiness?}]}(AgentReport)推进弹窗提交
POST/deployments/:id/syncdeployment:execute手动 Sync
GET/desired-statesdesired_state:read-两个下拉的数据源(发起表单过滤 active)
GET/policiespolicy:read-策略下拉数据源
GET/drift/eventsdrift:read?desired_state_id=&agent_name=漂移事件列表
POST/drift/checkdrift:reconcile{desired_state_id, agent_name, actual_state?}检测漂移
POST/drift/:id/reconciledrift:reconcile{}(可选 actual_state收敛(reconcile)

5.3 响应结构示例

统一 envelope 成功形态(拦截器已解包,业务代码只见 data):

POST /deployments/start 响应(原始 envelope 与解包后)
{ "code": 0, "message": "ok", "data": { "id": 23 }, "request_id": "req_b63f..." }

解包后业务代码 data.id === 23,页面提示 部署已发起 id=23。字段说明:id 新部署记录主键。创建类接口返回 HTTP 201。

部署记录(GET /deployments 列表项与 GET /deployments/:id 本体,字段即 hub.DeploymentRecord):

{
  "id": 23,
  "desired_state_id": 7,
  "bundle_version": 3,
  "agent_name": "app-001",
  "policy_name": "default",
  "status": "deploying",
  "error": "",
  "started_at": "2026-09-07T10:20:30+08:00",
  "finished_at": null,
  "auto_fix_count": 0,
  "last_auto_fix_at": null,
  "sync_requested_at": null,
  "created_at": "2026-09-07T10:20:30+08:00",
  "updated_at": "2026-09-07T10:20:31+08:00"
}

字段含义:id 主键;desired_state_id 所部署期望状态;bundle_version 期望状态当时的 bundle 版本;agent_name 目标 Agent;policy_name 使用的策略名(可空,空串时列表显示 default);status 六态之一 pending/deploying/synced/failed/drifting/degradederror 失败原因(空串无);started_at 发起时间(列表「开始时间」优先取它);finished_at 终态完成时间(synced/failed 置位);auto_fix_count/last_auto_fix_at 漂移自动调和轮数与最近触发时间(防循环判据,页面不直接展示但参与 sync/auto_fix 判定);sync_requested_at 手动 sync 指令戳(非空表示待消费「立即重调和」指令,pull 消费后清空)。

组件状态(GET /deployments/:id/components 数组项,字段即 hub.ComponentState):

{
  "id": 301,
  "deployment_id": 23,
  "name": "web",
  "kind": "binary",
  "status": "ready",
  "readiness": { "ready": true },
  "digest": "sha256:abc123",
  "started_at": "2026-09-07T10:20:32+08:00",
  "error": "",
  "created_at": "2026-09-07T10:20:30+08:00",
  "updated_at": "2026-09-07T10:20:40+08:00"
}

字段含义:name 组件名;kind 组件类型(默认 binary);status 组件状态 pending/deploying/ready/failed(对应徽标 comp-*);readiness 就绪详情 JSON(就绪快照/探针结果,页面 JSON.stringify 展示);digest 组件摘要;started_at 组件进入 deploying 的时间(就绪超时判据);error 组件错误(探针未通过/就绪超时/上报 failed 会写入)。

推进结果(POST /deployments/:id/advance,即 hub.AdvanceResult):

上报 web 为 ready、放行下一批时的响应示例
{
  "deployment_id": 23,
  "status": "deploying",
  "next_batch": ["db"],
  "ready_count": 1,
  "total_count": 3,
  "auto_rollback": null
}

字段含义:status 推进后部署状态(synced 表示已全部收敛终态;failed 表示本次推进触发失败);next_batch 本次放行的下一批组件名数组(空数组时提示串不追加「放行下一批」);ready_count/total_count 当前 ready 组件数/总组件数(页面提示 推进完成:状态 X(1/3));auto_rollback 非空(回滚草稿期望状态 id)表示本次失败已触发自动回滚。全部批次完成时 status=synced

手动 Sync(POST /deployments/:id/sync)三种响应分支:

成功清退避分支 / 幂等分支 / 终态拒绝分支
{ "id": 23, "from_status": "drifting", "status": "pending", "sync_requested_at": "2026-09-07T11:00:00+08:00" }

成功分支:from_status 原状态(drifting/failed/degraded 之一);status 恒为 pending(清退避置回待拉取);sync_requested_at 打上的指令戳——Spoke 下轮拉取即使 digest 未变也强制重调和。页面提示 Sync 成功:drifting → pending,已打 sync 指令戳

{ "id": 23, "from_status": "deploying", "status": "deploying", "sync_requested_at": null, "note": "部署已在进行中,无需重调和" }

幂等分支(部署已在 deploying):无退避可解除,不落新指令戳,sync_requested_at 返回 null,页面提示 Sync 成功(部署已在进行中,无需重调和)

{ "code": "DEPLOYMENT_SYNC_INVALID", "error": "部署已收敛(synced),无需手动 sync" }

终态拒绝分支:HTTP 409;synced 已收敛不允许再 sync(列表行不向 synced 显示按钮,此分支多为列表未刷新的陈旧态触发)。

漂移检测/收敛结果(POST /drift/checkPOST /drift/:id/reconcile,即 hub.DriftResult):

检测到漂移(含 auto_fix)示例
{
  "has_drift": true,
  "diff": [
    { "op": "replace", "path": "/version", "expected": "9.9.9", "actual": "8.8.8" },
    { "op": "add", "path": "/components/1", "expected": { "name": "db", "kind": "binary", "version": "2.0" } }
  ],
  "reconciling": false,
  "auto_fix_triggered": true,
  "auto_fix_reason": "drift_policy=auto_fix 允许自动调和:部署已置 pending,Agent 下轮 poll 将重新应用期望状态"
}

字段含义:has_drift 忽略规则过滤后是否存在 diff;diff JSON Patch(RFC 6902)路径级 diff 数组,op ∈ add(期望有实际无,附 expected)/remove(实际有期望无)/replace(值不同,附 expected+actual),path 形如 /a/b/0/c(数组用下标),支持 */** 通配 ignore;reconciling check 端点恒 false,reconcile 且有漂移时为 true;auto_fix_triggered 本次是否触发自动调和;auto_fix_reason 触发/未触发原因文案。未检测到漂移时 {"has_drift":false,"reconciling":false,"diff":[]}(无 diff 不写事件、不动部署状态,页面提示「未检测到漂移」)。

漂移事件(GET /drift/events 数组项,即 hub.DriftEvent):

{
  "id": 5,
  "desired_state_id": 7,
  "agent_name": "drift-agent",
  "diff": [
    { "op": "replace", "path": "/version", "expected": "9.9.9", "actual": "8.8.8" }
  ],
  "has_drift": true,
  "created_at": "2026-09-07T10:30:00+08:00"
}

字段含义:id 事件主键(「收敛(reconcile)」按钮路径参数);desired_state_id/agent_name 检测对象;diff 结构化 diff JSON(页面 code block 全文展示);has_drift 恒为 true(无漂移不落库);created_at 检测时间。列表按 id 降序、未分页。

5.4 关键机制

部署锁与发起校验(StartDeployment)。发起部署时后端依次校验:agent 名非空(空 → 400 DEPLOYMENT_INVALID「agent 标识不能为空」);期望状态存在(无 → 404);期望状态必须已激活status !== active → 400,文案含当前状态);期望状态必须声明了组件(无组件 → 400「期望状态无组件,无可部署内容」);组件依赖须构成合法 DAG(computeLevels Kahn 拓扑分层失败则拒绝)。随后在事务内做同一 Agent 部署锁:若该 Agent 已存在 status ∈ {pending, deploying} 的进行中部署则 409 DEPLOYMENT_CONFLICT(文案含进行中部署 id 与状态),保证同一 Agent 串行执行。事务内创建 DeploymentRecord(初始 pending、started_at=now)→ 立即置 deploying → 为每个组件建一条 ComponentState(pending)。事务提交后调用 gateAndRelease 放行首批(无依赖层)组件(置 deploying),并对监控侧回调登记带 deployment_id 的探针健康检查(G18 打点,旁路失败不阻断)。页面上「发起部署」成功即完成了上述整串动作。

DAG 分层与就绪门控(Advance)。期望状态组件靠 depends_on 构成 DAG,computeLevels 按依赖关系分出层级;就绪门控的规则是批次内组件全部 ready 才放行下一批(页面 legend 文案「就绪门控:仅当依赖全部 ready 才放行下一批」)。每次 advance 上报对应当前批次内一个组件:上报 ready → 组件置 ready;但若该组件声明了 readiness 探针且监控侧执行未通过,则组件保持 deploying 并写错误(readiness 探针未通过(声明探针已由 monitoring 执行并落库)/readiness 探针执行失败: ...)——就绪门控不放行,等待策略超时兜底。上报 deploying 仅刷新就绪快照(readiness)。上报对未知组件名直接忽略;组件状态按 (deployment_id,name) 唯一索引幂等 upsert(重复推进不产生重复记录)。当某层全部 ready 后 gateAndRelease 计算并放行下一层(响应 next_batch 返回放行的组件名);全部层完成 → 部署置 synced 终态(finished_at 记录),审计 DEPLOYMENT_SYNC

就绪超时与失败自动回滚。组件进入 deploying 会记 started_at;advance 请求内会做超时检查——超过策略 readiness_timeout_sec(默认策略 60s)的 deploying 组件被置 failed 并写错误 readiness 就绪超时(60s)。另有一个后台超时收敛任务(server/deployment_timeout.go)周期性对滞留 deploying 的部署做兜底超时判定(探测 Agent 探活失败时本轮保守跳过)。任一组件的失败上报/超时都会把部署引向失败处理:部署置 failed + 记录 error、审计 DEPLOYMENT_FAILED(含 auto_rollback 标志);若策略 rules.auto_rollback 为 true(默认)则调用期望状态服务的 Rollback 生成一条回滚期望状态草稿rollback_of 指向上一稳定版、bundle 版本号 +1,AdvanceResult.auto_rollback 返回草稿 id),让 Agent 按旧版本收敛回来。对已终态(synced/failed)的部署再提交 advance 不会重复推进,直接回当前结果(幂等)。

手动 Sync 与指令戳消费(F2 即时收敛载体)handleDeploymentSync 先读部署:synced → 409 拒绝(DEPLOYMENT_SYNC_INVALID);否则调 RequestSync——状态门控只对 failed/drifting/degraded/pending 生效,把它们置回 pending、清除 error/finished_at(置 NULL)、打上 sync_requested_at 指令戳;对 deploying(已在收敛中)不落新指令、返回幂等 200(note「部署已在进行中,无需重调和」)。指令戳的消费在 Agent pull 端:Spoke 下轮拉取时若该 Agent 最近部署是 pending 且打了 sync_requested_at(或 auto_fix_count>0),pull 响应携带 reconcile_required=true,并原子执行 ConsumeSyncRequestWHERE status=pending 单条 UPDATE,pending→deploying 并清指令戳)保证一次性语义——即使期望状态 digest 未变也强制 Agent 重新应用一轮,使实际状态收敛回期望。

漂移检测:结构化 diff 与落库(G3)ComputeDrift 递归对比期望声明(declaration JSON)与实际状态(map/数组任意嵌套):期望有而实际无 → add(附 expected;期望值 null 视为"应删除"不产 add);实际有而期望无 → remove;均存在但值不同 → replace;数值类型统一归一为 float64 比较避免 int/float 误判。路径为 JSON Pointer 风格(/a/b/0/c),并支持期望声明 policy.ignore_paths 忽略规则(精确路径、单段 *、深通配 **,命中路径及其整棵子树不产 diff)。CheckAndRecord 只在 hasDrift=true 时写一条 drift_events(无 diff 不落库)并把该期望状态+Agent 关联的最近部署标记 drifting(若存在且非 drifting),审计按结果写 DRIFT_DETECTED(diff 计数)/DRIFT_CLEAN

漂移自动调和(auto_fix,阶段 E 批4b)与防循环。漂移被检测到后,server 层注入的 apolloDriftAutoFix(通过最近 50 条 sync_history 的 OperationSummary 反查该期望状态关联的 gitops 同步目标)评估 drift_policy:仅 auto_fix 允许自动调和(alert 等策略仅记录)。通过后 hub 层再做两道防循环闸:单条部署 auto_fix_count 已达上限(默认 MaxRounds=3)不触发;距 last_auto_fix_at 小于防抖窗口(默认 5 分钟)不触发。放行则 RecordAutoFix 原子自增(WHERE auto_fix_count < max 条件更新防并发超限),把部署置 pending——Agent 下轮 pull 走与手动 sync 相同的 reconcile_required 消费路径重新应用。未触发原因会写进 auto_fix_reason 并随 DRIFT_AUTO_FIX 审计留痕。

收敛(Reconcile)语义。收敛本质是用(可选)actual_state 再跑一次 CheckAndRecord:仍有漂移 → 返回 reconciling=true(指示 Spoke 重拉应用,页面提示「收敛中」);无漂移 → 把关联部署置回 synced、清漂移(页面提示「收敛完成」)。本页收敛请求固定传空体 {},对非空期望声明等价于"当前无任何实际状态",必然再报 diff——页面上的「收敛完成」只有当 Agent 真正把实际状态拉回期望之后、再以一致实际状态收敛时才会出现,实际收敛执行者是 Spoke Agent 而非本页。

6. 权限与安全

7. 常见问题与排错

  1. 发起部署提示「期望状态与 Agent 名称必填」:原因是两个必填项之一为空(未选期望状态/Agent 名为空)。处理:期望状态下拉只列 active 项,先在部署总览/期望状态管理页把目标期望状态激活,再回本页重新选择。
  2. 发起部署失败,提示 409 DEPLOYMENT_CONFLICT(「已有进行中部署...同一 agent 串行执行」):原因是对同一 Agent 发起了第二次部署,部署锁拦截(pending/deploying 视为进行中)。处理:等该 Agent 部署推进到终态(synced/failed),或先对旧部署执行手动 Sync/推进完成后再发起。
  3. 部署一直停留在 deploying,推进不前进:原因有二——① 就绪门控:本批组件未全部 ready(有组件上报 deploying 或探针未通过);② 组件就绪超时(默认 60s)被判 failed 触发了回滚。处理:展开部署行看组件状态/错误列定位未就绪组件,点「推进」把它上报为 ready 放行下一批;若看到 readiness 就绪超时/readiness 探针未通过 错误,检查探针声明与组件真实就绪情况。
  4. 点「推进」后提示 推进失败:Unexpected token ...:原因是「就绪快照(readiness JSON,选填)」填了非法 JSON,前端 JSON.parse 抛异常且请求未发出。处理:按 placeholder {"ready":true} 修正引号/括号,或留空跳过;确认提示里没有「已推进」字样(本轮请求未提交,不会产生半途状态)。
  5. 漂移检测报 漂移检测失败:Unexpected token ...:原因是「实际状态(actual_state JSON)」不是合法 JSON,parse 抛异常、请求未发出。处理:按 placeholder {"app":"demo-app","version":"9.9.9"} 修正;留空则提交空对象判定。
  6. 漂移检测总提示「检测到漂移(N 项 diff)」:原因是 actual_state 留空提交 → 后端按空对象与(通常非空的)期望声明比对,必产大量 add 型 diff。处理:按期望声明的实际内容填写接近真实的 actual_state 再检;drifting 的收敛请走漂移事件区「收敛(reconcile)」。
  7. 点「收敛(reconcile)」后提示 收敛中(...剩余 N 项漂移) 而非收敛完成:原因是本页收敛固定传空 actual_state,重新判定必然仍有 diff,reconciling=true 只是指示 Spoke 重新拉取。处理:这不是失败——等 Agent 下一轮拉取并应用期望状态后再查看漂移事件;若漂移持续存在且收敛反复触发,检查 auto_fix 轮数上限(3 轮)与防抖窗口(5 分钟),以及 Agent 上报链路。
  8. 部署列表里 drifting 行点「收敛漂移」没有反应(控制台 ReferenceError):原因是该按钮绑定的 openReconcile 函数在 <script setup> 中未定义(前端缺陷,见 8 章)。处理:改用下方漂移事件列表里对应事件的「收敛(reconcile)」按钮执行收敛;该问题需由前端修复。
  9. 手动 Sync 提示失败且文案含「部署已收敛(synced),无需手动 sync」:原因是列表数据陈旧——该部署实际已 synced,仍显示旧按钮态。处理:刷新页面/重进列表后确认状态;synced 部署无需也不允许 sync。
  10. 打开推进弹窗后「选择组件」为空、无法推进:原因是在行未展开时直接点「推进」,组件列表是异步加载的,弹窗先于详情加载完成打开。处理:等几秒让组件加载完成再选,或先点该行展开详情等组件表渲染后,再点「推进」重开弹窗;若始终为空,检查该部署的 GET /deployments/:id/components 是否返回空(理论上发起部署必含组件,为空属异常,可看后端日志)。

8. 已知缺陷与边界

缺陷/边界说明
drifting 行「收敛漂移」按钮失效DeploymentPage.vue 行内按钮 @click="openReconcile(dep)" 引用了未定义函数(script setup 只有 openAdvance/handleReconcile),点击抛 ReferenceError、不发起任何请求——收敛请走漂移事件列表的「收敛(reconcile)」按钮;需前端修复
漂移事件加载失败被静默吞掉fetchDriftEvents 的 catch 只把 driftEvents 置空数组,无提示条——漂移事件加载异常对用户不可见,易误判「无漂移事件」
推进成功自动收起详情advance 成功后调用 toggleDetail(id) 对已展开的同一行是折叠语义,详情每次推进后自动收起,多轮推进需每轮重新点击行展开查看进度(易用性)
纯 Pull 演示页面通过「推进」弹窗模拟 Agent 上报,真实收敛依赖 Spoke Agent 拉取;页面不轮询、不自动刷新,状态变化需手动操作触发
收敛/漂移检测传空状态必报漂移漂移检测留空 actual_state 与收敛固定空体,对非空期望声明必然再检出差量;是演示语义,需配合 Agent 重拉收敛
全量无分页部署列表、期望状态、策略、漂移事件均一次性全量加载(drift events 后端 limit=0),数据量大时页面临压力
漂移下拉不过滤状态「漂移检测」期望状态下拉列全部状态(含 draft/deprecated/rollbacked),未像发起部署那样只列 active,可能误选已弃用状态
策略加载失败静默fetchPolicies 失败仅注释「不阻断」,无任何提示,策略下拉会只剩「默认(default)」
原生 confirm手动 Sync、收敛均用浏览器原生 confirm,同步阻塞且样式不统一;误确认后无撤销通道