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 为空的默认子路由) |
| 路由 name | Apollo |
| meta.title | Apollo 部署总览(父路由 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 认证与权限
- 页面级认证:meta 带
requiresAuth: true,守卫遍历to.matched见标记即要求登录态。无令牌访问/apollo会被重定向到 Apollo 独立登录页/apollo/login(ApolloLogin)。 - 令牌读取规则(针对
/apollo前缀路由):守卫仅认localStorage['apollo_token'],无 token 直接跳/apollo/login(ApolloLogin),不再回退读aip_token(Apollo 用户库与 AIP 已分离,跨产品令牌回退会放大越权面);其余产品路由仍校验aip_token。 - 接口权限点:本页全部请求落在期望状态资源。读类接口(列表/详情/声明)要求
PermDesiredStateRead;写类接口(创建/激活/回滚/删除/保存草稿)要求PermDesiredStateWrite,由后端access.RequirePerm中间件在 protected 路由组上强制。 - 错误呈现:请求 401(令牌失效)时 apolloClient 拦截器清 token 并
window.location.href='/apollo/login'硬跳转;403(权限不足)时浏览器弹出alert('无权限执行该操作'),列表区则按 catch 分支把服务端 message 拼进顶部提示条。
2.3 端口与 API 前缀
- Apollo 后端端口:18082;业务路由统一挂在
/api/v1下。 - 前端请求:baseURL
/apollo-api/v1(apolloClient.js),页面实际调用的形如/apollo-api/v1/desired-states;Vite 把/apollo-api代理到http://127.0.0.1:18082并做前缀重写/apollo-api → /api,后端收到/api/v1/desired-states。 - 生产形态:静态页由网关按
/apollo-api前缀分流到 18082,重写规则与 Vite 代理须一致,否则 404。 - 本页创建/编辑草稿采用 YAML 请求体:POST/PUT 声明接口
Content-Type: application/yaml(非默认的 JSON),走同一客户端、拦截器规则不变。
2.4 与相邻页面的分工
同一布局下 LightApollo 有多个"部署"相关的页面,分工差异容易混淆,先点明再进入正文:
- 部署总览(本页,/apollo):期望状态声明侧——"想要的状态"的增删改查与状态流转,纯控制面,不触发真实部署。
- 部署与漂移(/apollo/deployments):执行侧——把 active 期望状态派发成 deployment 记录、推进 step、做漂移检测与 sync/auto-fix 调和。它消费的是本页激活出来的 active 期望状态。
- Spoke Agent(/apollo/agents):模拟真实 Agent 拉取 active 期望状态声明并上报实际快照,验证"声明 → 执行 → 上报"链路。
- 一条期望状态只有在本页被激活为 active 后,才会成为下游部署/拉取的有效目标;在本页看到的"状态"徽标也因此是判断一条记录能否被消费的第一信号。
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: 1、failure_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=submit | btn-outline type=button |
| 可用条件 | :disabled="submitting"(提交中置灰,文案转「提交中...」) | 恒可用 |
| 操作效果 | @submit.prevent 触发 handleCreate() | resetCreateForm():表单回默认、收起表单 |
| 触发后端调用 | POST /desired-states(YAML body) | 无 |
handleCreate() 的校验与结果分支(按源码顺序):
- 前端校验(不触发网络请求):
name/app/version任一为空 → 顶部提示条 alert-error 显示name/app/version 必填;过滤空名后组件数为 0 → 提示至少需要一个组件。 - 通过后
buildYaml()把表单拼成 YAML 声明文本(见 5.3 示例),submitting=true置灰按钮。 apiClient.post('/desired-states', yaml, {headers:{'Content-Type':'application/yaml'}});后端 201 返回{id}(envelope 解包后页面拿data.id)。- 成功分支:前端用当前表单组件重算 Kahn 分层、拼接 DAG 拓扑文本 → 打开「创建成功」结果弹窗(见下)→ 顶部提示条
期望状态创建成功 id={id}(alert-success)→resetCreateForm()清表单并收起 → 刷新列表。 - 失败分支:提示条 alert-error 显示
创建失败:{err.response?.data?.error || err.message}(如重名 409 的服务端 message「同名期望状态 … 已存在(status=…)」)。 finally复位 submitting。
「创建成功」结果弹窗(modal-overlay 点击空白处 @click.self 可关闭,右上角「关闭」link-btn 同效)逐行展示:
| 行 | 内容 | 说明 |
|---|---|---|
| 期望状态 ID | createResult.id(后端返回) | 即新行主键 |
| 版本 | createResult.version | 页面填写的 version |
| bundle_version | 创建后用 GET /desired-states/{id} 回读的真实值(如 7) | 已修复(提交 412e9c48):创建接口仅返回 {id},前端随即补拉单条记录回填真实 bundle_version(ApolloPage.vue:503-517);回读失败时才回落占位文案 由后端分配(per-app 单调递增) |
| 状态 | 徽标 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 格式化展示)。详情按 id 缓存于 detailCache(ApolloPage.vue:284),命中缓存不再重复请求(:384-387),并在激活/删除/保存草稿等状态变更处主动失效(:531/551/591/614);展开行仍互斥(expandedId 单值),一次只能看一行详情。
4.7 「激活」操作(仅 draft 行)
操作列第一个按钮,仅 ds.status === 'draft' 渲染(btn-success 绿)。点击 handleActivate(ds):
- 无二次确认,直接
POST /desired-states/{id}/activate(busy=true置灰全页操作按钮)。 - 成功:顶部提示
期望状态 #id 已激活(alert-success),刷新列表;若当前展开的正是该行则自动收起详情再展开刷新。 - 失败:提示
激活失败:{error}。常见失败:非 draft(后端 409「仅 draft 状态可激活」);审批未过审(渠道 require_approval 且 approval_status=pending/rejected,后端 409「期望状态待审批(approval_status=…),须先通过审批后方可激活」);激活前部署门禁 deny(403 AUTHZ_ERROR,保持 draft)。
激活是"草稿 → 可部署"的关键一步,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 「删除」操作(仅 draft/active 行)
操作列第四按钮「删除」(btn-danger),已修复(提交 412e9c48):仅 ds.status === 'draft' || ds.status === 'active' 的行渲染(ApolloPage.vue:218),deprecated/rollbacked 行不再显示删除入口。点击 handleDelete(ds):先按状态弹出不同文案的浏览器 confirm——
- draft:
确定删除期望状态"{name}"吗?(draft 将硬删除) - active:
确定弃用期望状态"{name}"吗?(active 将标记为 deprecated)
确认后 DELETE /desired-states/{id}:draft 走物理删除(后端直删行,响应 {id, deleted:true});active 走后端 Deprecate(置 deprecated,响应 {id, status:"deprecated"});页面成功提示分两态——draft 期望状态 #{id} 已删除,非 draft 期望状态 #{id} 已弃用({status})(alert-success),随后刷新列表。取消 confirm 则不发请求。已修复(提交 412e9c48):删除按钮现仅对 draft/active 行渲染(ApolloPage.vue:218,v-if="ds.status === 'draft' || ds.status === 'active'"),deprecated/rollbacked 行不再出现删除入口,因此不会触达后端「仅 active 状态可弃用」的 409 拒绝。
4.11 操作按钮可用条件速查
| 按钮 | 出现条件 | 飞行期 | 依赖的二次确认/校验 |
|---|---|---|---|
| 激活 | 仅 draft | busy 置灰 | 无 confirm |
| 编辑草稿 | 仅 draft | busy/loading 置灰 | 无 |
| 回滚 | 恒显示 | busy 置灰 | confirm + 必选目标 |
| 删除 | 仅 draft/active 行 | busy 置灰 | confirm(文案随 draft/非 draft 变化) |
busy 是全局布尔:任一写操作(激活/回滚/删除/保存草稿)进行中都置 true,此时其它行按钮同步禁用,避免并发写;submitting 只控创建提交按钮。读操作(展开详情、加载声明)不禁按钮。
4.12 次要展示类控件合并
- 详情「组件清单」子表:组件/类型/版本/依赖/就绪探针五列;类型空显示
binary、版本空-、依赖空-、就绪探针经probeSummary:优先取probes.readiness({type} {url|path}),兼容旧式healthcheck[0]({type} {url|command}),都没有显示-。 - DAG 拓扑文本:
dagTopology对组件按computeLevels(Kahn 分层,level = 依赖的 max(level)+1)算层,同层内按名称排序、层间用→连接,如db, redis → backend → web;无组件/无依赖时显示(无依赖)。 - 状态徽标:draft(黄)/active(绿)/deprecated(灰)/rollbacked(红)四态,颜色见 3 章;已修复(提交 412e9c48):approval_status=pending 的行在状态列追加红色「待审批」徽标(
ApolloPage.vue:211,classapproval-pending),已过审/无审批要求的行不显示;创建结果弹窗仍只写死 draft 徽标。 - 操作结果提示条:所有成功/失败文案统一在此呈现(alert-success 绿/alert-error 红),右上「关闭」link-btn 置空 message;下次操作自动覆盖旧消息。
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('无权限执行该操作')。本页特例有两个:
- 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。 - 声明接口的版本号响应:创建成功返回的 envelope 内只有
{id},不含 bundle_version/status——因此页面创建成功后会补拉GET /desired-states/{id}回填真实 bundle_version(ApolloPage.vue:503-517,回读失败才回落占位文案);「状态」行仍恒写死 draft 徽标(新行本就恒为 draft)。
5.2 端点表
| 方法 | 路径(后端 /api/v1) | 请求体/Query | 页面触发点 | 鉴权 |
|---|---|---|---|---|
| GET | /desired-states | Query 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 | - | 编辑草稿加载原始 YAML | PermDesiredStateRead |
后端另有同资源接口未被本页直接使用: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_version | per-app 单调递增的部署序号(该 app 内 max+1 分配) |
| digest | 内容连接键,可由 bundle 打包环节后填;空时详情显示 - |
| status | draft / 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。已修复(提交 412e9c48):本页列表在 approval_status=pending 时会显示红色「待审批」徽标(ApolloPage.vue:211),不再只看到 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. 权限与安全
- 认证分层与权限点:读类接口挂
PermDesiredStateRead、写类挂PermDesiredStateWrite,全部经access.RequirePerm中间件;无有效 token 401、有 token 无权限 403。页面按钮不按权限点显隐——即便用户没有写权限,按钮仍在,点下去才由后端 403 兜底并弹「无权限执行该操作」,属于"前端不预判权限、后端强制"的风格。 - 资源归属:desired-states 是全局资源,不在项目级成员归属守卫(projectScopeAccessGuard,针对
/projects/:id)范围内,仍按权限点鉴权,不因项目归属额外限制。 - 写操作留痕:删除 draft 硬删、删除 active 只弃用(deprecated)保留审计;回滚不覆盖原行而是新行传播 + 原行 rollbacked 留痕;一切写操作(含激活/保存草稿)落 httpAudit 审计(记 user_id/username/IP/UA/method/path/status/action/result/duration_ms 等,body 脱敏)。
- 激活门禁:激活前强制评估部署安全门禁(require_signature/critical_vuln/no_plaintext_secret 等全局策略按 projectID=0 求值),deny 保持 draft、评估结果落审计;这是"绕过审批直接翻 active"的防线。
- 身份回填:创建/回滚以
currentUserID(c)(token 解析出的用户名)记 created_by,缺省 system;审计与行数据可追溯操作者。
7. 常见问题与排错
本章按"现象 → 原因 → 处理"三句结构,均可溯源到源码/后端行为;标注"未实测"的条目由代码逻辑推断。
- 创建提示「name/app/version 必填」。 现象:点提交直接弹红色提示。原因:三个基础字段任一为空触发前端校验,未发请求。处理:补全名称/应用/版本(channel 可空)后重试。
- 创建提示「至少需要一个组件」。 现象:字段齐了仍被拦。原因:所有组件行的组件名为空(空行在提交时被过滤),过滤后组件数为 0。处理:至少为一行填组件名;多余空行可不删,不影响提交。
- 创建/保存提示含"同名期望状态 … 已存在"。 现象:服务端 message 提到
已存在(status=…)。原因:name 全局唯一(含 draft),与历史任意状态的 name 撞名即 409DESIRED_STATE_CONFLICT。处理:改一个未用过的 name;需换版本的场景建议沿用 app+新 name(如 order-service-v2)创建。 - 点「激活」提示"仅 draft 状态可激活"或"期望状态待审批"。 现象:激活失败、状态未变。原因:前者行已不是 draft(可能已被并发操作改状态);后者行所属渠道 require_approval 且 approval_status=pending/rejected,未过审不得激活(本页不显示审批字段,易误判)。处理:刷新确认当前状态;pending 行先走审批能力页过审,approved 后再激活。
- 激活失败提示 403/AUTHZ 且含策略关键词。 现象:激活被拒且列表状态仍 draft。原因:激活前部署安全门禁评估 deny(如声明要求国密签名但缺 digest/signature,或命中危险策略)。处理:回「编辑草稿」补齐声明中签名/digest 相关字段或调整策略,重新保存后再激活;详情可结合后端 policy_evaluation_results 审计确认命中规则。
- 编辑草稿打开后文本域为空或提示「加载声明失败」。 现象:弹窗空 YAML 或直接关闭。原因:
GET /declaration返回空/失败,或该行实际非 draft(服务端已拒绝)。处理:确认该行状态为 draft;重新打开弹窗;用 curl 复现GET /apollo-api/v1/desired-states/{id}/declaration观察原始返回。 - 保存草稿提示"组件 name … 重复"或"depends_on 引用了不存在的组件"。 现象:改完 YAML 保存被拒。原因:直接编辑原始 YAML 绕过了表单的温和校验,后端对声明做严格 Validate(组件名唯一、依赖存在、无自环/成环)。处理:按错误 message 修正 YAML(去重组件名、修正 depends_on 引用)后重存。
- 删除 active 后该行仍在列表。 现象:确认删除后行没消失、状态徽标变 deprecated。原因:active 的删除语义是弃用(非物理删除),行保留作历史审计。处理:属预期行为;想彻底移除只有 draft 行走硬删。
- (已修复)deprecated/rollbacked 行为何没有删除按钮。 现象:对 deprecated/rollbacked 行看不到「删除」按钮。原因:提交 412e9c48 已把删除按钮限定为仅 draft/active 行渲染(
ApolloPage.vue:218)——弃用语义只对 active 有意义,deprecated/rollbacked 行本就无需再删,故不再显示入口(此前会点击后被后端 409 拒绝)。处理:无需操作,属预期收口。 - 回滚后想立刻部署但找不到新行。 现象:提示回滚成功(新 id)但列表里没看到可用记录。原因:回滚生成的是 status=draft 的新草稿(需刷新列表查看,名形如
xxx-rollback-N),draft 不被下游部署消费。处理:刷新列表找到该草稿行,点「激活」后再发起部署;列表较长时可留意原行已变 rollbacked 红徽标。 - 页面整体无权限/白屏。 现象:登录后访问 /apollo 弹「无权限执行该操作」或列表加载失败。原因:携带的
apollo_token对应账号缺PermDesiredStateRead/Write。处理:换用具备期望状态读写权限的 Apollo 账号登录(/apollo/login),或由管理员在角色管理授权;确认令牌类型为 apollo_token。
8. 已知缺陷与边界
本章如实列出页面边界与可改进点;其中「bundle_version 占位文案」「删除按钮显隐过宽」「列表不感知审批字段」「详情无缓存」四项已在提交 412e9c48 修复(见各行 file:line),余下为设计边界或后端契约缺口。
| 缺陷/边界 | 说明 |
|---|---|
| bundle_version 占位文案 | 已修复(提交 412e9c48):创建后补拉 GET /desired-states/{id} 回填真实 bundle_version(ApolloPage.vue:503-517),仅回读失败时回落文案「由后端分配(per-app 单调递增)」 |
| 状态写死 draft | 创建结果弹窗「状态」行恒为 draft 徽标(ApolloPage.vue:108);新行本就恒为 draft、后端 201 也只返回 {id},属展示简化而非缺陷 |
| 无分页 | 列表接口不带分页,期望状态量大时一次性全量加载渲染(含 deprecated/rollbacked 历史行) |
| 展开行互斥(缓存已修复) | 展开行之间仍互斥(expandedId 单值),一次只能看一行详情(设计边界);详情缓存已修复(提交 412e9c48):详情按 id 缓存于 detailCache(ApolloPage.vue:284/384-387),状态变更处主动失效(:531/551/591/614),不再每次展开都重新 GET /:id |
| 列表感知审批字段 | 已修复(提交 412e9c48):approval_status=pending 的行在状态列显示红色「待审批」徽标(ApolloPage.vue:211);列表虽不展示 approved/rejected 细态,pending 已在操作前可见(不再滞后到激活 409) |
| 删除按钮显隐 | 已修复(提交 412e9c48):删除按钮现仅对 draft/active 行渲染(ApolloPage.vue:218),deprecated/rollbacked 行不再出现入口,不会触达后端「仅 active 可弃用」的 409 |
| 激活即触门禁 | 勾选国密签名等策略但声明要素不全时激活被门禁 deny,页面提示偏后端化,缺少前端预校验引导 |
| DAG 与详情依赖客户端重算 | 拓扑层序文本由前端 Kahn 算法按详情 declaration 计算展示,后端不返回拓扑;展示口径与后端校验逻辑独立维护 |
| 创建/编辑双入口差异 | 创建走表单(温和校验),编辑草稿走原始 YAML(后端严格校验),同一目标两种校验强度,文案不一致 |
| 固定探针参数 | 前端写死 interval_s: 1, failure_threshold: 3,无控件可调;process 型探针允许空就绪地址,其余形态缺地址时整段探针不生成 |
| 版本列前缀 | 列表版本列显示 v{version} 前缀由前端拼接,仅展示层语义 |
| 并发写保护 | busy 全局置灰可防同页并发,但多标签页/多操作者无冲突提示,靠后端唯一约束与状态机 409 兜底 |
| 渠道配置依赖侧边栏 | 创建仅填渠道名,不校验渠道存在性与审批要求(后端 Create 对不存在的渠道返回错误);require_approval 渠道的审批操作不在本页 |