1. 页面概览

1.1 是什么

「部署总览」页(对应前端源码 action/web/src/views/ApolloPage.vue)是 LightApollo 交付运维工作台登录后的默认落地页,也是该产品的首页与侧边栏第一项。页面展示的名称「Apollo 部署总览」与其职责完全对应——它管理的是交付运维的核心概念期望状态(Desired State),即"某应用在某版本下应该由哪些组件、按什么依赖顺序、带什么部署策略被部署出来"的一套完整声明。与字面联想不同,本页不是布满统计卡片的仪表盘,而是一张期望状态的运维控制台:列出全部期望状态、创建新期望状态、激活草稿、编辑草稿、回滚、删除/弃用,以及点击行展开查看组件清单与依赖 DAG。真正把"期望状态"落成"实际部署"并做漂移检测的动作在「部署与漂移」「Spoke Agent」等相邻页面完成;本页管的是"想让系统变成什么样子"的声明侧,而不碰"系统现在是什么样子"的执行侧。

要正确理解本页,需要先抓住"期望状态"在 LightApollo 里的地位:它相当于声明式部署的"配置清单",一条期望状态记录的组成是 ① 基础信息(name/app/version/channel,渠道选填)② 组件清单(每个组件的名称、类型 kind、版本、depends_on 依赖、就绪探针 probes)③ 部署策略(policy 块,含国密签名要求 require_guomi_signature 与漂移忽略路径 ignore_paths)。创建时前端把这些表单输入拼装成一段 YAML 声明文本提交给后端,后端解析校验、按 per-app 单调递增分配 bundle_version、落库为一条 status=draft 的记录;之后在本页把它"激活"为 status=active,Spoke Agent / 部署编排才会把这份声明当作拉取与部署的依据。

页面同时承载期望状态的全生命周期管理:从创建(生成 draft)→ 激活(draft → active)→ 弃用(active → deprecated)到回滚(以某条历史期望状态为模板生成新的回滚草稿,原行标记 rollbacked)。删除按钮则按状态给出两种语义:draft 直接物理删除、active 仅标记弃用(留审计痕迹)。页面覆盖的正是这一整条从"声明"到"可部署状态"再到"退役/回退"的链路,一句话职责:让操作者能对"期望状态"这个交付核心对象做创建、激活、编辑、回滚、删除与查看,并直观看到组件间依赖关系

页面的输入→输出闭环大致是:操作者以表单或 YAML 两种方式描述期望状态(输入)→ 后端持久化并编号(bundle_version/状态机)→ 页面列表呈现全部期望状态并支持展开查看详情、随状态变化提供对应操作(输出)→ 激活后的 active 期望状态成为下游部署编排与 Spoke 拉取的消费源。因此,本页既是"查看全球当前应该长什么样"的台账,也是"声明新版本、让旧版本退役"的操作台。

1.2 核心价值/能力表

能力说明对应页面操作
期望状态台账列出全部期望状态的 ID/名称/应用/版本/bundle_version/状态/更新时间进入页面自动加载列表
创建期望状态以组件动态行 + 依赖 DAG + 探针 + 策略/渠道的表单组装 YAML 声明并提交点「创建期望状态」→ 填表 → 点「创建期望状态」提交
DAG 依赖可视化详情展开区以"层 → 层"文本拓扑呈现组件依赖顺序,创建结果弹窗同步展示点击行展开详情;创建成功后看结果弹窗
草稿编辑以原始 YAML 编辑器直接改写 draft 声明并保存draft 行点「编辑草稿」→ 改 YAML → 「保存草稿」
激活把 draft 提升为 active,使其可被部署编排消费draft 行点「激活」
回滚以同 app 的历史期望状态为模板生成新回滚草稿,原行标记 rollbacked任意行点「回滚」→ 选目标 → 「确认回滚」
删除/弃用draft 物理删除、active 标记弃用(deprecated),均留痕行点「删除」,二次确认
就绪探针配置每组件可配 http/tcp/process/file 型就绪探针与就绪地址组件行的「探针类型」「就绪地址/路径」

1.3 一句话总结

部署总览页是 LightApollo 的期望状态全生命周期控制台,覆盖从创建草稿、编辑声明、激活发布、查看依赖 DAG,到回滚与弃用/删除的完整链路,是交付运维流程中"声明期望状态"这一环的前端唯一入口。

2. 访问入口

2.1 路由与菜单

路由 path/apollo(父路由 /apollo 下 path 为空的默认子路由)
路由 nameApollo
meta.titleApollo 部署总览(父路由 meta.title Apollo,均 requiresAuth: true)
布局ApolloLayout(挂产品侧边栏与顶栏的布局组件)
侧边栏位置LightApollo 侧边栏第一项「部署总览」
前端源码action/web/src/views/ApolloPage.vue
API 客户端action/web/src/api/apolloClient.js
路由注册action/web/src/router/index.js 约 404-414 行(/apollo 的 children 首项)

由于它是父路由的空 path 子路由,/apollo/apollo/ 都落到本页;同时它是登录(/apollo/login)成功后 router.replace('/apollo') 的目标页,所以登录后的默认落地页就是本页——这也是「部署总览」被放到侧边栏第一项的原因。相邻页面「部署与漂移」(/apollo/deployments)「环境管理」(/apollo/environments)等同在 /apollo children 下,从侧边栏可互相跳转。

2.2 认证与权限

2.3 端口与 API 前缀

2.4 与相邻页面的分工

同一布局下 LightApollo 有多个"部署"相关的页面,分工差异容易混淆,先点明再进入正文:

3. 界面布局

┌──────────────────────────────────────────────────────────┐
│ page-header:Apollo 部署总览(h2)   [创建期望状态/收起表单]  │
├──────────────────────────────────────────────────────────┤
│ [操作结果提示条 .alert](v-if="alert.message") [关闭]      │
├──────────────────────────────────────────────────────────┤
│ [创建期望状态 card](v-if="showCreateForm",默认收起)       │
│  基础字段:名称(name)* 应用(app)* 版本(version)*        │
│            发布渠道(channel)                            │
│  组件(components)子区:[+ 组件行]                        │
│    每行:组件名 | kind下拉 | 版本 | 依赖(逗号分隔) |        │
│          探针类型下拉 | 就绪地址/路径 | [删除]              │
│  部署策略(policy)子区:☐要求国密签名                      │
│    漂移忽略路径(ignore_paths,逗号分隔)输入框              │
│  [创建期望状态(提交)] [取消]                               │
├──────────────────────────────────────────────────────────┤
│ [期望状态列表 card](loading → 加载中...;空 → 提示文案)    │
│  ID | 名称 | 应用 | 版本 | bundle_version | 状态 | 更新时间 | 操作│
│  (行可点击展开:详情面板 + 组件清单表 + DAG 拓扑 + 策略块)    │
│  操作列(@click.stop):draft→[激活][编辑草稿];全行→[回滚][删除]│
├──────────────────────────────────────────────────────────┤
│ 弹窗层:①创建结果弹窗 ②回滚期望状态弹窗 ③编辑草稿弹窗          │
└──────────────────────────────────────────────────────────┘

各板块职责:

板块职责
页头「Apollo 部署总览」标题 + 「创建期望状态/收起表单」切换按钮
操作结果提示条承载一切操作的结果消息(alert-info/alert-success/alert-error),带「关闭」链接
创建期望状态表单名称/应用/版本/渠道基础字段 + 组件动态行 + 部署策略,默认收起、按钮展开
期望状态列表全部期望状态的台账表格,行点击展开/收起详情,操作列按状态给出可用按钮
创建结果弹窗创建成功后展示 ID/版本/bundle_version/状态/DAG 拓扑
回滚弹窗选择同 app 的历史期望状态作为回滚目标
编辑草稿弹窗直接编辑 draft 的原始 YAML 声明并保存

页面自绘样式要点:状态徽标以颜色区分——draft 黄底(#fefcbf/#975a16)、active 绿底(#e6fffa/#2f855a)、deprecated 灰底(#edf2f7/#718096)、rollbacked 红底(#fed7d7/#9b2c2c);展开行背景高亮(.row-selected)便于识别当前展开的是哪一行;创建表单用 grid 三列布局排布基础字段、虚线子区收纳组件行与策略,整页卡片堆叠、无分页组件,列表一次性渲染全部记录。

4. 交互元素

本章按"能点/能填的控件 + 真实后果"逐项拆解:4.1-4.4 是创建表单(显隐切换、基础字段、组件动态行、部署策略),4.5 是创建提交与结果弹窗,4.6 是期望状态列表区与行展开详情,4.7-4.10 是四种行操作(激活/编辑草稿/回滚/删除)及各自弹窗/确认框,4.11 汇总按钮可用条件,4.12 汇总次要展示控件。凡出现按钮名、提示文案均与源码逐字一致。

4.1 「创建期望状态」/「收起表单」切换按钮(页头)

属性
控件名页头主按钮,文案随状态二选一:创建期望状态收起表单
位置page-header 右侧
触发toggleCreateForm():翻转 showCreateForm 布尔值
操作效果点「创建期望状态」→ 创建表单 card 展开显示;再点变「收起表单」→ 收起表单 card。仅切换显隐,不重置也不提交表单
触发后端调用
边界与细节收起再展开后已填内容保留(不 reset);「取消」按钮才真正重置表单并收起

4.2 创建表单基础字段(名称/应用/版本/渠道)

字段位置含义必填/默认/可用条件操作效果触发后端调用边界与细节
名称(name)基础字段第一列期望状态全局唯一名称必填(label 带 * + HTML required),placeholder「如 order-service-v1」进入 buildYaml 的 name:创建/保存草稿时随 YAML 提交后端按 name 全局唯一(含 draft)校验,重名返回 409 DESIRED_STATE_CONFLICT
应用(app)基础字段第二列所属应用,回滚/版本号按它聚合必填(*),placeholder「如 order-service」进入 app: 行;成为同 app 候选的聚合键同上编辑草稿提示要求 name 保持当前一致、app/version 可改但会同步列值
版本(version)基础字段第三列应用版本号必填(*),placeholder「如 1.0.0」进入 version: 行,组件 version 外层不带 v 前缀同上仅展示与记录用;bundle_version 才是后端分配的单调部署序号
发布渠道(channel)基础字段第四列关联发布渠道(如需审批的渠道)选填;空则无渠道非空时写 channel:同上渠道 require_approval 时创建后 approval_status=pending,本页列表不展示该字段(见 4.12)

4.3 组件动态行区(组件 components)

创建表单中部是虚线子区「组件(components)」,右侧有「+ 组件行」按钮(type=button,避免误触发表单提交)。点击追加一行空组件行;每行右侧有「删除」按钮(btn-danger,type=button)移除该行。初始化时 emptyCreateForm() 已预置一行空组件行。表单提交时 buildYaml()跳过组件名为空的行if (!c.name) continue),所以空行不报错、也不进声明。

每行组件的控件明细:

控件含义可选/默认值进入声明的方式
组件名输入框(placeholder「组件名」)组件唯一名必填;空行被跳过 - name: {name}
kind 下拉组件类型默认 binary;可选项 binary / config / llm_route / eval_set / ontology_yaml kind: {kind || 'binary'}
版本输入框(placeholder「版本」)组件版本选填非空时 version: "{version}"(加引号)
依赖输入框(placeholder「依赖(逗号分隔)」)depends_on 组件名列表选填;splitList 按中英文逗号/空白切分非空时 depends_on: [a, b]
探针类型下拉(placeholder「探针类型」)就绪探针形态默认空(不产生探针);可选 http / tcp / process / file与就绪地址组合生成 probes:
就绪地址/路径输入框(placeholder「就绪地址/路径」)http/tcp/file 的 url/path选填生成 readiness: {type: X, url: "Y", ...}
「删除」按钮移除本行任意行可用直接 splice 移除,不请求后端

探针生成的精确语义:仅当 c.probe_type 非空且(c.probe_url 非空或类型为 process)时,才写探针块:

probes:
  readiness: {type: process, url: "", interval_s: 1, failure_threshold: 3}

interval_s: 1failure_threshold: 3 为前端写死的固定值,页面无控件可改;process 型探针允许就绪地址留空(此时 url 为空串),其余形态必须有就绪地址否则整段探针不生成。注意声明中的探针是 probes.readiness(就绪探针,判部署门控),详情面板的「就绪探针」列对旧式 healthcheck 数组做了兼容展示——见 4.12。

依赖关系的后端约束(编辑草稿直接改 YAML 时同样生效):depends_on 引用的组件必须存在、不得自环、不得成环,否则创建/保存被拒(400,错误码见 5.4)。

4.4 部署策略区(policy)

创建表单底部是虚线子区「部署策略(policy)」,两个控件:

字段含义默认值进入声明的方式
勾选「要求国密签名」require_guomi_signature 开关默认 false(不勾选)policy: require_guomi_signature: false/true
漂移忽略路径(ignore_paths,逗号分隔)漂移检测忽略的路径列表默认填 /components/*/runtime/*非空时逐条 - {路径}(逗号/空白切分多条)

注意「取消」后重新展开表单,两个策略控件回到默认值(require_guomi_signature=false、ignore_paths=/components/*/runtime/*);「收起表单」再展开则保留当前值。勾选国密签名后,激活时后端部署门禁会评估签名要求(见 5.4),若声明缺 digest/signature 且策略要求签名,激活可能被门禁 deny。

4.5 创建提交与创建结果弹窗

表单底部两个按钮:

属性「创建期望状态」(提交)「取消」
类型btn-primary type=submitbtn-outline type=button
可用条件:disabled="submitting"(提交中置灰,文案转「提交中...」)恒可用
操作效果@submit.prevent 触发 handleCreate()resetCreateForm():表单回默认、收起表单
触发后端调用POST /desired-states(YAML body)

handleCreate() 的校验与结果分支(按源码顺序):

  1. 前端校验(不触发网络请求):name/app/version 任一为空 → 顶部提示条 alert-error 显示 name/app/version 必填;过滤空名后组件数为 0 → 提示 至少需要一个组件
  2. 通过后 buildYaml() 把表单拼成 YAML 声明文本(见 5.3 示例),submitting=true 置灰按钮。
  3. apiClient.post('/desired-states', yaml, {headers:{'Content-Type':'application/yaml'}});后端 201 返回 {id}(envelope 解包后页面拿 data.id)。
  4. 成功分支:前端用当前表单组件重算 Kahn 分层、拼接 DAG 拓扑文本 → 打开「创建成功」结果弹窗(见下)→ 顶部提示条 期望状态创建成功 id={id}(alert-success)→ resetCreateForm() 清表单并收起 → 刷新列表。
  5. 失败分支:提示条 alert-error 显示 创建失败:{err.response?.data?.error || err.message}(如重名 409 的服务端 message「同名期望状态 … 已存在(status=…)」)。
  6. finally 复位 submitting。

「创建成功」结果弹窗(modal-overlay 点击空白处 @click.self 可关闭,右上角「关闭」link-btn 同效)逐行展示:

内容说明
期望状态 IDcreateResult.id(后端返回)即新行主键
版本createResult.version页面填写的 version
bundle_version由后端分配(per-app 单调递增)占位文案非真实值(见 8 章缺陷)
状态徽标 draft新行恒为草稿
DAG 拓扑code 单元格展示层序文本,无依赖时 (无依赖)由前端 computeLevels 计算,非后端返回
hint新期望状态为 draft,请在列表中点击"激活"后再发起部署。引导下一步

弹窗关闭后表单已被重置,若想再建需重新点「创建期望状态」展开。

4.6 期望状态列表区

页面下半部为列表 card,三态渲染:加载中显示「加载中...」;空数据显示「暂无期望状态,请点击"创建期望状态"添加。」;有数据则渲染表格。表头依次:ID / 名称 / 应用 / 版本 / bundle_version / 状态 / 更新时间 / 操作。行数据来源 GET /desired-states(首次进入 onMounted 即拉取)。单元格格式细节:版本列显示为 v{version}(带 v 前缀);状态列是彩色徽标 status-{status};更新时间经 formatTime 显示(String(t).replace('T',' ').slice(0,19),如 2026-09-07 10:23:45,空值显示 -);列表接口不带分页、一次性加载全部(含 deprecated/rollbacked 历史行)。

行点击展开详情:整行可点击(cursor:pointer),点击该行触发 toggleDetail(id)——再次点击同一行则收起(expandedId 置 null);展开的行背景高亮 .row-selected。展开时 GET /desired-states/:id 拉取该行详情 JSON,面板包含:detail-meta 行(name=… · app=… · digest=… · 创建人 … · 创建时间 …,digest 缺失显示 -、创建人缺失显示 system)→「组件清单」子表(表头 组件/类型/版本/依赖/就绪探针)→「依赖关系(DAG)」区块(层序文本 + 图例「箭头方向表示依赖:前端组件依赖后端组件,先部署后端。」)→「策略块」区块(若有 policy,JSON 格式化展示)。详情每次展开都重新请求、无前端缓存。

4.7 「激活」操作(仅 draft 行)

操作列第一个按钮,仅 ds.status === 'draft' 渲染(btn-success 绿)。点击 handleActivate(ds)

激活是"草稿 → 可部署"的关键一步,active 之后的期望状态才会被部署编排与 Spoke 拉取消费;激活不要求重新审批(approval 已在 Create 或 Approve 侧处理)。

4.8 「编辑草稿」操作与编辑草稿弹窗(仅 draft 行)

操作列第二个按钮,仅 draft 渲染(btn-outline)。点击 openEditDraft(ds):打开「编辑草稿」弹窗并先异步加载原始 YAML——GET /desired-states/{id}/declaration(响应为原始 YAML 文本而非 envelope),弹窗按钮短暂显示「加载中...」;加载失败则提示 加载声明失败:{error} 并直接关闭弹窗。

弹窗要素:

元素说明
标题编辑草稿 #{id}({name})
提示直接编辑期望状态 YAML 声明(app/version/components 必填,name 需保持与当前一致)。仅 draft 状态可修改。
声明(YAML)编辑框textarea rows=18、spellcheck=false、等宽字体
「保存草稿」按钮文案三态:加载中→加载中...;busy→保存中...;空闲→保存草稿disabled = busy || loading
「取消」关闭弹窗,不保存

「保存草稿」点击 → handleSaveDraft():YAML 为空或全空白时提示 声明内容不能为空(alert-error)且不发请求;否则 PUT /desired-states/{id}Content-Type: application/yaml)。成功提示 草稿 #{id} 已保存(alert-success)、关闭弹窗、刷新列表;失败提示 保存草稿失败:{error}仅 draft 可改:后端对非 draft 行 PUT 返回 409「仅 draft 状态可修改声明(当前状态 …)」;页面只在 draft 行放按钮,不会触达该错误,但改 YAML 若违反必填/DAG 规则会收到 400(组件 name 重复、depends_on 引用不存在组件、自环/成环等)。

4.9 「回滚」操作与回滚弹窗(任意状态行)

操作列第三按钮「回滚」(btn-outline),所有状态行恒显示。点击 openRollback(ds) 打开「回滚期望状态 #{id}」弹窗:

元素说明
选择器标签选择回滚目标(同 app 的历史期望状态)
下拉首项为 disabled 的占位「请选择回滚目标」;候选来自 rollbackCandidates = 列表内 ds.app === current.app && ds.id !== current.id 的全部行(不限状态,draft/active/deprecated/rollbacked 都可当目标),选项文案 #{id} {name}(v{version} · {status})
「确认回滚」disabled = !targetId || busy;busy 时文案「处理中...」
「取消」关闭弹窗

点「确认回滚」→ handleRollback():未选目标提示 请选择回滚目标(按钮已禁用,属兜底);随后浏览器 confirm('确定执行回滚吗?将生成新的期望状态草稿并标记原行为 rollbacked。'),点确定才发 POST /desired-states/{id}/rollback,body {target_desired_state_id: id}。成功提示 回滚成功,新期望状态 id={data.id}(status=draft,请激活后部署)(alert-success)、关闭弹窗、刷新列表;失败提示 回滚失败:{error}

回滚的真实后果(后端语义,务必知晓):不是把当前行改回去,而是以目标期望状态的声明为模板生成一条全新记录——name={target.name}-rollback-{newBV}(newBV 为该 app 当前最大 bundle_version+1)、status=draft、rollback_of=目标 id、app/version/digest/signer_id 复制目标行;同时当前行被标记 rollbacked(保留审计留痕、不再可部署)。因此回滚后列表会出现两条新变化:原行变红底 rollbacked、新增一条名为 xxx-rollback-N 的草稿。跨 app 回滚在后端被拒(400「回滚目标 … 与当前期望状态 … 不属于同一应用」),页面候选已限同 app 不会触达。

4.10 「删除」操作(任意状态行)

操作列第四按钮「删除」(btn-danger),所有状态行恒显示。点击 handleDelete(ds):先按状态弹出不同文案的浏览器 confirm——

确认后 DELETE /desired-states/{id}:draft 走物理删除(后端直删行,响应 {id, deleted:true});active 走后端 Deprecate(置 deprecated,响应 {id, status:"deprecated"});页面成功提示分两态——draft 期望状态 #{id} 已删除,非 draft 期望状态 #{id} 已弃用({status})(alert-success),随后刷新列表。取消 confirm 则不发请求。注意对 deprecated/rollbacked 行点删除,后端 Deprecate 仅接受 active(否则 409),页面提示文案此时写"弃用"但后端会拒绝——即对已 deprecated/rollbacked 行删除大概率得到 操作失败:仅 active 状态可弃用(当前状态 …),属页面未对这两种状态隐藏删除按钮的边界(见 8 章)。

4.11 操作按钮可用条件速查

按钮出现条件飞行期依赖的二次确认/校验
激活仅 draftbusy 置灰无 confirm
编辑草稿仅 draftbusy/loading 置灰
回滚恒显示busy 置灰confirm + 必选目标
删除恒显示busy 置灰confirm(文案随 draft/非 draft 变化)

busy 是全局布尔:任一写操作(激活/回滚/删除/保存草稿)进行中都置 true,此时其它行按钮同步禁用,避免并发写;submitting 只控创建提交按钮。读操作(展开详情、加载声明)不禁按钮。

4.12 次要展示类控件合并

5. 后端关联

5.1 API 客户端

apolloClient.js(action/web/src/api/apolloClient.js)行为同登录页所述:axios 实例 baseURL /apollo-api/v1、timeout 30000ms;请求拦截器附加 Authorization: Bearer {getApolloToken()};响应拦截器把 {code:0,data:…} envelope 解包(code===0 时页面 const { data } 直接拿业务数据),HTTP 401 清 token 并跳 /apollo/login(已在登录页豁免)、403 alert('无权限执行该操作')。本页特例有两个:

  1. YAML 请求/响应走非 envelope 形态:创建与保存草稿的 POST/PUT 请求体是 YAML 文本(Content-Type: application/yaml),后端对声明接口的响应则分两种——普通 CRUD 仍返回 envelope JSON(创建 201 {id}、更新/激活/删除/回滚各自的 JSON);GET /desired-states/:id/declaration 返回 application/x-yaml 的纯文本,body 无数字 code 字段,拦截器判定"非 envelope"原样放行,页面直接拿文本填进 textarea。
  2. 声明接口的版本号响应:创建成功返回的 envelope 内只有 {id},不含 bundle_version/status——所以页面结果弹窗的 bundle_version 用占位文案、状态恒写死 draft,真实值要靠刷新后的列表读取。

5.2 端点表

方法路径(后端 /api/v1)请求体/Query页面触发点鉴权
GET/desired-statesQuery app(选填,按应用过滤)进入页面/每次操作后刷新列表PermDesiredStateRead
POST/desired-states期望状态 YAML(Content-Type: application/yaml创建提交PermDesiredStateWrite
GET/desired-states/:id-行点击展开详情PermDesiredStateRead
PUT/desired-states/:id期望状态 YAML保存草稿PermDesiredStateWrite
DELETE/desired-states/:id-删除/弃用PermDesiredStateWrite
POST/desired-states/:id/activate-激活PermDesiredStateWrite
POST/desired-states/:id/rollback{"target_desired_state_id": N}确认回滚PermDesiredStateWrite
GET/desired-states/:id/declaration-编辑草稿加载原始 YAMLPermDesiredStateRead

后端另有同资源接口未被本页直接使用:POST /desired-states/:id/deprecate(显式弃用,本页用 DELETE 语义触发)、POST /desired-states/:id/approve / reject(渠道审批流,走审批能力页而非本页)。本页页面代码触发点与后端路由在 server.go 逐字对应,未使用任何未注册路径。

5.3 响应结构示例

列表 GET /desired-states(envelope 解包后为数组)

[
  {
    "id": 12,
    "name": "order-service-rollback-7",
    "app": "order-service",
    "version": "1.2.0",
    "bundle_version": 7,
    "digest": "sha256:abc…",
    "signer_id": "",
    "declaration": { "app": "order-service", "version": "1.2.0", "components": [ … ] },
    "status": "draft",
    "approval_status": "pending",
    "rollback_of": 6,
    "created_by": "admin",
    "created_at": "2026-09-07T10:20:31+08:00",
    "updated_at": "2026-09-07T10:20:31+08:00"
  }
]
字段含义
id期望状态主键
name全局唯一名称;回滚行形态为 {目标名}-rollback-{bundle_version}
app / version应用与版本;版本在列表中以 v{version} 展示
bundle_versionper-app 单调递增的部署序号(该 app 内 max+1 分配)
digest内容连接键,可由 bundle 打包环节后填;空时详情显示 -
statusdraft / active / deprecated / rollbacked 四态之一
approval_status选填(omitempty);渠道要求审批时为 pending,审批后 approved/rejected,无审批要求则字段不出现
rollback_of选填;回滚行的来源目标 id
created_by / created_at / updated_at登记人(缺省 system)与时间

创建成功 POST /desired-states(HTTP 201)

{ "id": 12 }

激活成功 POST /desired-states/:id/activate(HTTP 200)

{ "id": 12, "status": "active" }

回滚成功 POST /desired-states/:id/rollback(HTTP 201)

{ "id": 13, "kind": "rollback" }

声明 GET /desired-states/:id/declaration(HTTP 200,application/x-yaml 纯文本)——前端文本域里编辑的就是这种形态(登记字段 name/digest/signer_id/bundle_version 由后端回填进 YAML):

app: order-service
version: 1.3.0
name: order-service-v3
components:
  - name: db
    kind: binary
    version: "15.2"
  - name: backend
    kind: binary
    version: "2.1.0"
    depends_on: [db]
    probes:
      readiness: {type: http, url: "/healthz", interval_s: 1, failure_threshold: 3}
  - name: web
    kind: binary
    version: "3.0.1"
    depends_on: [backend]
policy:
  require_guomi_signature: false
  ignore_paths:
    - /components/*/runtime/*

错误(HTTP 4xx/5xx,统一 envelope)——重名创建示例:

{
  "code": 40901,
  "message": "同名期望状态 \"order-service-v3\" 已存在(status=active)",
  "data": null,
  "request_id": "req_4b2e9f…"
}

字段说明:code 为 TAD-12 统一业务码,期望状态资源的错误码细分见 5.4;页面统一取 err.response?.data?.error || err.message 展示(error 为客户端拦截器回填的服务端 message 别名字段)。

5.4 关键机制

状态机与生命周期。期望状态四态流转(hub/desired_state.go 语义):draft 是创建的初始态;draft → active 仅经激活(或渠道审批通过 Approve 事务内同时置 approved + active);active → deprecated 经弃用(显式 deprecate 或本页 DELETE);active/draft 所在行被回滚时 → rollbacked(仅作留痕,不再可部署)。激活仅接受 draft(非 draft 409「仅 draft 状态可激活」);弃用仅接受 active(非 active 409「仅 active 状态可弃用」);改声明仅接受 draft(非 draft 409「仅 draft 状态可修改声明」)。被弃用/回滚的行保留在列表(本页列表不筛状态),形成完整历史。

bundle_version 单调递增与防回滚。每次创建或回滚新行时,后端事务内对该 app 取当前最大 bundle_version 再 +1 分配(GetMaxBundleVersion),保证同一应用内部署序号单调、可比较新旧;回滚行的序号必然大于历史所有行,因此"回滚到旧声明"在版本语义上仍是"更新到更新的 bundle",避免部署编排误判"版本倒退"而不推进。

审批 gate(G15)。创建时若声明的 channel 指向 require_approval 渠道,新行 approval_status 直接置 pending,登记即进入审批流;approval_status=pending/rejected 的行不被 Spoke 拉取、不被激活IsApprovedForDeploy 判定,激活时 409「期望状态待审批 …」);审批通过由 Approve 事务同时置 approved + active,被拒保持 draft。本页列表与操作不感知 approval_status,只看到 draft 徽标——对 pending 行点激活会得到上述 409 错误,这是跨"审批流页面 → 本页"协同时需要留意的信息缺口。

激活的部署左移门禁。激活(draft→active)前,后端对该期望状态执行强制安全门禁评估(与 GitOps 同步激活共用 apolloSecurityGate),每次评估自动落 policy_evaluation_results 审计;决策 deny 时保持 draft 并返回 AUTHZ_ERROR(HTTP 403,前端显示「激活失败:…」+ 浏览器可能弹无权限 alert)。因此勾选了"要求国密签名"(或命中 require_signature/critical_vuln/no_plaintext_secret 等策略)的声明,若缺 digest/signature 等要素,即使按普通流程激活也可能被拦在 active 之外。

回滚的新行传播语义。回滚生成的新行:声明复制自目标(含 components/probes/rollout/policy/channel/ignore_paths),name={target.name}-rollback-{newBV},app/digest/signer_id/version 复制目标,rollback_of 记目标 id,status=draft、created_by=操作者。新行名同样受全局唯一约束(预检转 409 而非落裸错误)。原行标 rollbacked。由于新行是 draft,回滚后必须再到本页对其「激活」才能真正让目标声明重新生效——页面成功提示已点明这一后续动作。

声明的解析/编组与必填校验。声明结构 DesiredStateDeclaration(app/version/name/channel/components/policy/api_version 等)经 yaml 解析后做 Validate:app/version/components 非空、组件 name 非空且不重复、depends_on 引用的组件必须存在、无自环、无成环(Kahn 拓扑检测,命中返回 400 DESIRED_STATE_DAG_CYCLE 等)。错误码映射:DESIRED_STATE_CONFLICT=40901(同名冲突)、DESIRED_STATE_INVALID=40001(解析/必填/DAG 引用错误)、DESIRED_STATE_DAG_CYCLE=40001(成环/自环)、INVALID_STATE_TRANSITION=40901(非法状态流转)。声明内 digest/signer_id/bundle_version 以服务侧登记字段回填为准;更新声明是全量替换语义——编辑草稿保存后,app/version/digest/signer_id 列同步用新声明覆盖,消费方(Spoke 拉取/门禁/审批)读列值。

列表排序与加载GET /desired-states 默认 List(ctx,0,0) 全量返回(按仓库排序,通常 id 序或时间序);带 ?app= 时走 ListByApp(bundle_version 降序)。页面未用 app 过滤,恒全量加载。

6. 权限与安全

7. 常见问题与排错

本章按"现象 → 原因 → 处理"三句结构,均可溯源到源码/后端行为;标注"未实测"的条目由代码逻辑推断。

  1. 创建提示「name/app/version 必填」。 现象:点提交直接弹红色提示。原因:三个基础字段任一为空触发前端校验,未发请求。处理:补全名称/应用/版本(channel 可空)后重试。
  2. 创建提示「至少需要一个组件」。 现象:字段齐了仍被拦。原因:所有组件行的组件名为空(空行在提交时被过滤),过滤后组件数为 0。处理:至少为一行填组件名;多余空行可不删,不影响提交。
  3. 创建/保存提示含"同名期望状态 … 已存在"。 现象:服务端 message 提到 已存在(status=…)。原因:name 全局唯一(含 draft),与历史任意状态的 name 撞名即 409 DESIRED_STATE_CONFLICT。处理:改一个未用过的 name;需换版本的场景建议沿用 app+新 name(如 order-service-v2)创建。
  4. 点「激活」提示"仅 draft 状态可激活"或"期望状态待审批"。 现象:激活失败、状态未变。原因:前者行已不是 draft(可能已被并发操作改状态);后者行所属渠道 require_approval 且 approval_status=pending/rejected,未过审不得激活(本页不显示审批字段,易误判)。处理:刷新确认当前状态;pending 行先走审批能力页过审,approved 后再激活。
  5. 激活失败提示 403/AUTHZ 且含策略关键词。 现象:激活被拒且列表状态仍 draft。原因:激活前部署安全门禁评估 deny(如声明要求国密签名但缺 digest/signature,或命中危险策略)。处理:回「编辑草稿」补齐声明中签名/digest 相关字段或调整策略,重新保存后再激活;详情可结合后端 policy_evaluation_results 审计确认命中规则。
  6. 编辑草稿打开后文本域为空或提示「加载声明失败」。 现象:弹窗空 YAML 或直接关闭。原因:GET /declaration 返回空/失败,或该行实际非 draft(服务端已拒绝)。处理:确认该行状态为 draft;重新打开弹窗;用 curl 复现 GET /apollo-api/v1/desired-states/{id}/declaration 观察原始返回。
  7. 保存草稿提示"组件 name … 重复"或"depends_on 引用了不存在的组件"。 现象:改完 YAML 保存被拒。原因:直接编辑原始 YAML 绕过了表单的温和校验,后端对声明做严格 Validate(组件名唯一、依赖存在、无自环/成环)。处理:按错误 message 修正 YAML(去重组件名、修正 depends_on 引用)后重存。
  8. 删除 active 后该行仍在列表。 现象:确认删除后行没消失、状态徽标变 deprecated。原因:active 的删除语义是弃用(非物理删除),行保留作历史审计。处理:属预期行为;想彻底移除只有 draft 行走硬删。
  9. 对 deprecated/rollbacked 行点删除报"仅 active 状态可弃用"。 现象:删除操作失败,提示该错误。原因:页面对所有非 draft 行统一显示「删除」并走弃用语义,但后端弃用仅接受 active;对已是 deprecated/rollbacked 的行删除即 409。处理:属页面按钮显隐边界(见 8 章);该状态行本就无需再删,忽略或留作历史。
  10. 回滚后想立刻部署但找不到新行。 现象:提示回滚成功(新 id)但列表里没看到可用记录。原因:回滚生成的是 status=draft 的新草稿(需刷新列表查看,名形如 xxx-rollback-N),draft 不被下游部署消费。处理:刷新列表找到该草稿行,点「激活」后再发起部署;列表较长时可留意原行已变 rollbacked 红徽标。
  11. 页面整体无权限/白屏。 现象:登录后访问 /apollo 弹「无权限执行该操作」或列表加载失败。原因:携带的令牌对应账号缺 PermDesiredStateRead/Write(或兜底的 aip_token 用户在本库无同名用户)。处理:换用具备期望状态读写权限的 Apollo 账号登录(/apollo/login),或由管理员在角色管理授权;确认令牌类型为 apollo_token。

8. 已知缺陷与边界

本章如实列出页面边界与可改进点;其中「创建结果弹窗 bundle_version 占位文案」「删除按钮对 deprecated/rollbacked 行显隐过宽」「列表不感知审批字段」「详情展开互斥且无缓存」四项属 web/src 源码层可修,已记入临时缺陷报告供主流程收口。

缺陷/边界说明
bundle_version 占位文案创建结果弹窗的 bundle_version 行恒显示「由后端分配(per-app 单调递增)」文案(源码 id > 0 ? … 三元),不显示真实值;真实值需刷新列表读取
状态写死 draft创建结果弹窗「状态」行恒为 draft 徽标,由后端 201 只返回 {id} 所致
无分页列表接口不带分页,期望状态量大时一次性全量加载渲染(含 deprecated/rollbacked 历史行)
详情无缓存每次展开行都重新 GET /:id;展开行之间互斥(expandedId 单值),一次只能看一行详情
列表不感知审批字段列表不展示 approval_status,pending 行点激活才会收到 409 错误,信息滞后到操作时才暴露
删除按钮显隐过宽对 deprecated/rollbacked 行仍显示「删除」(语义实为弃用),点击被后端 409 拒绝,缺按钮禁用或文案区分
激活即触门禁勾选国密签名等策略但声明要素不全时激活被门禁 deny,页面提示偏后端化,缺少前端预校验引导
DAG 与详情依赖客户端重算拓扑层序文本由前端 Kahn 算法按详情 declaration 计算展示,后端不返回拓扑;展示口径与后端校验逻辑独立维护
创建/编辑双入口差异创建走表单(温和校验),编辑草稿走原始 YAML(后端严格校验),同一目标两种校验强度,文案不一致
固定探针参数前端写死 interval_s: 1, failure_threshold: 3,无控件可调;process 型探针允许空就绪地址,其余形态缺地址时整段探针不生成
版本列前缀列表版本列显示 v{version} 前缀由前端拼接,仅展示层语义
并发写保护busy 全局置灰可防同页并发,但多标签页/多操作者无冲突提示,靠后端唯一约束与状态机 409 兜底
渠道配置依赖侧边栏创建仅填渠道名,不校验渠道存在性与审批要求(后端 Create 对不存在的渠道返回错误);require_approval 渠道的审批操作不在本页