1. 页面概览

1.1 是什么

「制品与渠道」页(对应前端源码 action/web/src/views/BundlePage.vue,页面标题「Apollo 制品与渠道」)是 LightApollo 的可验签分发中枢。它把发布链路上最关键的三种对象收在同一个页面里:bundle 制品(用户在浏览器选若干文件,经 multipart 上传由后端打包签名、登记成带 digest 与签名者指纹的制品记录)、发布渠道(release channel)(把 bundle 绑到「发布给谁」的语义——渠道指向一条期望状态后,Spoke Agent 的 Pull 目标选择就能以 stable 渠道为准来拉取)、部署策略(deployment policy)(用一段 rules JSON 描述「部署该怎么跑」——是否自动回滚、并发上限、就绪超时、退避参数)。页面上半部是构建 bundle 表单与 bundle 制品列表,下半部是左右并排的渠道卡与策略卡,各自独立做创建、编辑、删除与(渠道独有)指向期望状态。

需要先分清本页与相邻页的对象关系:期望状态(desired state)在部署侧创建与激活(YAML 声明,见 deployments 页);bundle 是本页构建的「带签名、可验签的分发最小单元」,一个期望状态在激活时通常要关联某个 bundle 的 digest(desired_states.digest,G7 连接键);渠道在本页把「发布目标」绑定到某条期望状态上(current_desired_state_id),并把 promotion/approval 两个策略字段作为 JSON 存起来;策略在本页维护部署行为规则,实际生效在部署编排 Advance 阶段按策略名解析。而「制品管理」页(/apollo/artifacts)的制品仓库走另一套轻量元数据登记,与 bundle 通过 artifacts.bundle_id / storage_path="bundle://{digest}" 关联——本页是打包本体所在,制品页只是登记引用。

典型使用链路(演示视角的完整闭环):① 首次构建时后端若发现签名者白名单里还没有 demo-signer 的真实公钥,会自动生成一套 SM2 演示密钥并把它 upsert 进白名单(幂等,指纹 = SM3(公钥)),因此「开箱即可签名、开箱即可验签」;② 在「构建 bundle」表单填应用(如 order-service)、版本(如 1.0.0),点「+ 文件行」追加若干文件行并选定本地文件,文件行的「路径」输入框决定该文件在 bundle 内的路径(留空回退用文件名),点「构建 bundle」提交 multipart;③ 后端打包(逐文件 SM3 校验和进清单)→ 计算整体 digest(SM3 规范化 manifest + 按路径排序的文件字节)→ 用演示 SM2 私钥对 digest 签名 → 落盘到 ./temp/apollo_bundles/<digest> 并登记 bundles 表,页面提示 bundle 构建成功 id=… digest=…;④ 在 bundle 制品列表点「验签」,后端从落盘目录读回制品逐项校验(逐文件 SM3 / digest / 白名单信任锚 / SM2 签名),结果弹窗展示 files_ok / digest_ok / signature_ok / trusted,行内出现「可信/不可信」徽标;⑤ 点「下载 zip」拿到 manifest.json + digest.sig + files/… 归档(文件按 bundle-{id}-{app}-{version}.zip 命名),供真实 Spoke 侧下载后自行 LoadBundleFromDir 校验应用;⑥ 回到页面下半部,为某渠道在「指向期望状态」下拉里选一条 active 期望状态,或新建 stable/dev 渠道并把 promotion/approval 策略 JSON 填进去;⑦ 新建/编辑 default 部署策略控制 auto_rollback 等行为——到此「制品签名 → 渠道指向 → 策略放行」的发布前准备即告完成,之后即可在部署页发起部署,让 Agent 按本页固化的声明去拉取应用。

1.2 核心价值/能力表

能力说明对应页面操作
一键构建签名制品多文件行 + 文件键(bundle 内路径),multipart 上传由后端 SM3/SM2 打包签名,返回含 id/digest 的记录「构建 bundle」按钮
开箱自洽信任锚首次构建惰性生成 demo-signer 的 SM2 密钥并 upsert 白名单,构建即签名、验签即可信首次「构建 bundle」
逐项验签诊断逐文件 SM3 → digest → 白名单 → SM2 四步校验,结果 JSON 弹窗展示行内「验签」按钮
zip 下载分发任意 bundle 可下载归档(manifest.json + digest.sig + files/...)行内「下载 zip」
渠道管理渠道 CRUD + 软删 + 「指向期望状态」绑定发布目标「发布渠道」卡
渠道-期望状态绑定渠道下拉把发布目标指向一条 active 期望状态,Agent Pull 目标选择参考 stable 渠道「指向期望状态」下拉
部署策略即代码policies 表存 rules JSON(auto_rollback 等),空规则自动落默认值「部署策略」卡
并行加载互斥四份数据 Promise.all 并行拉取,任一失败静默降级空数组;busy 互斥防连点页面 onMounted / 全部写操作

1.3 一句话总结

制品与渠道页把「bundle 打包签名、逐项验签、zip 下载、渠道指向期望状态、部署策略维护」五件事收敛成 Apollo 发布分发的前置中枢——bundle 决定「发什么」、渠道决定「发给谁」、策略决定「怎么发」

2. 访问入口

2.1 路由与菜单

路由 path/apollo/bundles
路由 nameApolloBundles
meta.titleApollo 制品与渠道
父路由/apollo(组件 ApolloLayoutmeta.title: 'Apollo'
侧边栏入口ApolloLayout 侧边栏「制品与渠道」,位于「配置管理」之后、「Spoke Agent」之前
前端源码action/web/src/views/BundlePage.vue
API 客户端action/web/src/api/apolloClient.js
路由注册action/web/src/router/index.js/apollo 子路由 children 下(约 439-444 行)

访问方式:登录 Apollo 后从左侧菜单「制品与渠道」进入,或直接访问 /apollo/bundles。相邻页(侧边栏顺序):配置管理 configs.html(前)、Spoke Agent agents.html(后);概念对照:制品管理 artifacts.html(制品仓库 / bundle 概念区分)、部署与漂移 deployments.html(期望状态创建激活、bundle digest 关联、部署推进)。

2.2 认证与权限

2.3 端口与 API 前缀

3. 界面布局

+------------------------------------------------------------------+
| Apollo 制品与渠道(h2 页头)                                       |
| [操作结果提示条 v-if alert.message:{{alert.message}}  [关闭]]     |
+------------------------------------------------------------------+
| [构建 bundle 卡]                                                  |
|   应用(app)* | 版本(version)* | bundle_version * | 签名者(signer)|
|   制品文件(文件键为 bundle 内路径)            [+ 文件行]          |
|     [路径输入][file 选择][已选文件名][删除] ×N                    |
|   [构建 bundle / 构建中...](busy 禁用)                          |
+------------------------------------------------------------------+
| [bundle 制品卡]                                                   |
|   ID|名称|应用|版本|bundle_version|digest|签名者指纹|大小|验签|操作 |
|     (digest/指纹:shortDigest 悬停全文;名称列 b.name||b.app)  |
|     验签列:[验签] [可信/不可信徽标]  操作列:[下载 zip]            |
+------------------------------------------------------------------+
| [发布渠道(channels)卡]  |  [部署策略(policies)卡]  ← 左右并排   |
|   创建表单(名称/描述/promo/approval)+[创建渠道]                  |
|   ID|名称|指向期望状态|操作       ID|名称|规则|操作                |
|   指向期望状态=[select: 未指向(0)│#id name...]                     |
|   操作=[编辑][删除]            操作=[编辑][删除]                   |
+------------------------------------------------------------------+
| [编辑渠道弹窗] [编辑策略弹窗] [验签结果弹窗]  @click.self 可关      |
+------------------------------------------------------------------+

各板块职责:

板块职责
页头 + 提示条标题「Apollo 制品与渠道」、全局操作结果提示(info/success/error,可「关闭」)
构建 bundle 卡收集 app/version/bundle_version/signer 与文件行(路径+本地文件),multipart 构建并刷新列表
bundle 制品卡全量 bundle 记录表格(id 降序),含验签入口、可信徽标、zip 下载
发布渠道卡渠道创建表单 + 渠道表格(指向期望状态下拉、编辑、删除)
部署策略卡策略创建表单 + 策略表格(rules JSON 截断展示、编辑、删除)
三个弹窗编辑渠道(name 不可改)/ 编辑策略(name 不可改)/ 验签结果 JSON

页面无独立「加载中」骨架屏:数据未到时表格区为空,写操作进行中由按钮 busy 文案体现(构建中.../保存中...);四份数据任一接口失败被 .catch(() => ({data:[]})) 静默降级,不弹错误(见 7.10)。

4. 交互元素

4.1 页面标题与操作结果提示条

控件位置含义必填/默认/可用条件操作效果触发后端调用边界与细节
页面标题页头 h2页面身份标识常驻文案 Apollo 制品与渠道与源码 h2 逐字一致
操作结果提示条页头下方展示最近一次操作结果alert.message 非空才显示;alert.type 决定样式(alert-info/alert-success/alert-error成功用 success、失败用 error 展示消息单槽覆盖:新操作覆盖旧提示,无历史列表
提示条「关闭」提示条右侧(float:right清空当前提示提示条可见即可用alert.message = '' 立即消失纯本地状态;showAlert(msg, type) 的 type 缺省 alert-info

页面用一组 ref 维护状态:四个列表 bundles/channels/policies/desiredStates、全局互斥位 busy、提示 alert、验签结果缓存 verifyMap(键 = bundle id)与三个弹窗 verifyModal/channelEditModal/policyEditModal所有写操作共用 busy:任一进行中,构建按钮、验签、下载、渠道/策略的创建与编辑保存按钮全部 :disabled="busy",防止并发写把列表状态弄乱;只有「删除」用 window.confirm 前置确认。

4.2 「构建 bundle」表单

控件位置含义必填/默认/可用条件操作效果触发后端调用边界与细节
应用(app)*表单第一行左一bundle 所属应用名必填(required),placeholder 如 order-service提交时 trim 校验非空multipart 字段 app缺省会先被前端拦:app/version 必填且至少需要一个文件
版本(version)*第一行左二bundle 版本串必填(required),placeholder 如 1.0.0同上multipart 字段 version与 app 共同构成 bundle 命名 {app}-{version}-b{version}
bundle_version *第一行左三构建序号必填,type="number" min="1" 默认 1,v-model.number参与 BundleID 与期望状态 per-app 版本比对multipart 字段 bundle_version(字符串化)清空或非法由后端 parseInt 兜底默认 1
签名者(signer)第一行左四指定签名者名称非必填,默认占位 demo-signeremptyBuildForm 初始值)非空才 append 字段multipart 字段 signer留空提交时后端 signerID == "" 也会兜底成 demo-signer;白名单找不到该名称时仍用 demo 私钥签名并把 manifest signer_id 记成所填名称(验签时以指纹匹配白名单)
「+ 文件行」「制品文件(文件键为 bundle 内路径)」子标题右侧追加一条文件行常驻buildForm.files.push({ path:'', file:null })行数无上限;每条新行路径为空、文件未选
路径输入每行左侧该文件在 bundle 内的路径(即 multipart 键)非必填,placeholder bundle 内路径,如 config/app.yaml提交时 trim,留空回退为本地文件名f.path.trim() || f.file.name作为 FormData 的文件键重复键会互相覆盖(后者胜,见 7.7);路径会被 sanitizeRelPath 二次清洗防止路径穿越
文件选择每行中 input[type=file]选择本地文件必选:file 为 null 的行在提交时被过滤@change 写入 f.file = e.target.files[0]经 FormData 上传不选文件的行即使填了路径也不会上传;只有路径/只有文件键二者等价(空路径回退文件名)
已选文件名每行右侧展示已选文件选中后才出现展示 f.file.name只读提示,与 multipart 文件名无关(文件键优先用「路径」输入)
「删除」每行末尾(btn-danger)移除当前文件行常驻buildForm.files.splice(i, 1)行内删除不影响其他行;至少保留一行由前端保证不了(可删空,提交时校验兜底)
「构建 bundle」表单底部(btn-primary, submit)提交构建busy=false 才可点;构建中文案 构建中...校验通过发 multipart;成功提示 bundle 构建成功 id={id} digest={前16位}(签名者 {已签名/-})、重置表单为空行、loadAll() 刷新;失败提示 构建失败:{msg}POST /bundles前端缺 app/version/文件时提示 app/version 必填且至少需要一个文件 并不发请求;后端另有校验(app/version 不能为空未上传任何 bundle 文件)兜底

构建表单字段 → 后端:FormData 组装顺序为 appversionbundle_versionString(bundle_version || 1))、可选 signer,随后每个文件以「其 bundle 内路径」为键逐条 fd.append(path, file)。后端 handleCreateBundle 先读 app/version(trim 空 → 400 app/version 不能为空),parseInt(bundle_version, 1) 兜底,再 parseFormFiles 解析全部上传文件——所以「文件键即 bundle 内路径」是该接口的第一语义:multipart 字段名不是固定文件名,而是发布时制品在 bundle 里的相对路径

4.3 bundle 制品列表与「验签」

bundle 制品卡标题 bundle 制品bundles.length===0 时居中灰字空态 暂无 bundle,请构建一个。;有数据显示表格,列头依次 ID / 名称 / 应用 / 版本 / bundle_version / digest / 签名者指纹 / 大小 / 验签 / 操作。列表由后端按 id 降序返回(最新构建在最上)。

控件/列位置含义必填/默认/可用条件操作效果触发后端调用边界与细节
ID首列记录主键-纯展示每次构建即使内容一致也会插新行(digest 冲突行 upsert,见 5.4)
名称列第二列bundle 展示名取值 b.name || b.app纯展示b.name = 后端存的 BundleID{app}-{version}-b{bundle_version});历史行若无 name 回退 app
digestdigest 列(cell-code)内容哈希shortDigest(b.digest) 显示前 16 位加 ,悬停 title 展示完整值;空显示 -纯展示digest = SM3(规范化 manifest + 文件字节按路径排序拼接),G7 唯一连接键,也是落盘目录名
签名者指纹指纹列(cell-code)签名者公钥指纹同 shortDigest 规则纯展示指纹 = SM3(公钥),是白名单匹配键;demo-signer 首次构建后为真实密钥指纹,seed 里的占位指纹会被自动升级
大小大小列制品字节数formatSize(b.size_bytes)纯展示格式化:<1024 → n B;<1MB → x.x KB(1 位小数);否则 x.xx MB(2 位小数);空 → -
「验签」验签列(btn-sm btn-outline)触发完整验签链busy=false见下方验签行为POST /bundles/:id/verify需要 PermBundleWrite(只读角色会 403,见 7.4)
可信徽标验签按钮右侧展示最近一次验签结论verifyMap[b.id] 存在时渲染verifyResultOk(...) 四项全为真 → 绿底 可信(verify-ok);否则红底 不可信(verify-bad)已修复(提交 412e9c48):徽标改为四项合取 trusted && files_ok && digest_ok && signature_okBundlePage.vue:355-357),不再单看 trusted(见 8 章 D2);验签失败/未验签该行无徽标
「下载 zip」操作列(btn-sm btn-primary)下载归档busy=false见 4.4GET /bundles/:id/download(blob)无二次确认;bundle 被清理后下载会 404

验签行为handleVerify):点「验签」→ busy=truePOST /bundles/:id/verify(无请求体)。成功时把解包后的结果对象存入 verifyMap[b.id] 并打开验签弹窗:标题 验签结果(bundle #{id}),正文 <pre class="code-block">{{ JSON.stringify(result, null, 2) }}</pre> 深色代码块完整展示 {files_ok, digest_ok, signature_ok, trusted, signer_name} 逐项结果。失败时不红条、而是同样打开弹窗verifyMap[b.id] = { trusted:false, error: ... } 且弹窗内容为 { error: 错误信息 },行内徽标因此显示红底 不可信——即「验签失败也展示」(如 403 VERIFY_SIGNER_NOT_TRUSTED),这不是页面 bug 而是设计取舍:让用户看得见「为什么不可信」。失败后点开弹窗只能看到错误串,看不到四步明细(后端错误分支不返回部分成功项,见 5.4)。

4.4 「下载 zip」

控件位置含义必填/默认/可用条件操作效果触发后端调用边界与细节
「下载 zip」行操作列把 bundle 落盘目录打包下载busy=false;记录存在成功后浏览器触发下载,提示 bundle #{id} 已下载(success);失败提示 下载失败:{msg}GET /bundles/:id/downloadresponseType:'blob'文件名被前端硬编码覆盖bundle-{id}-{app}-{version}.zip(不采用后端 Content-Disposition 的 BundleID 名)

下载实现:拿 blob → URL.createObjectURL → 构造隐藏 <a>download → 点击 → 移除节点 → revokeObjectURL。后端把落盘目录(manifest.json + digest.sig + files/<path>…)整树 walk 进 zip,Content-Disposition: attachment; filename="{BundleID}.zip";前端 override 后实际保存名总带记录 id,避免同名 bundle 覆盖本地文件。注意:拦截器对 blob 原样放行,因此下载成功与否完全看 HTTP 状态——若记录文件已被后端清理策略删除,后端打包阶段报 打包 bundle 失败: ...(500)。

4.5 渠道创建表单与渠道列表(含指向期望状态)

「发布渠道(channels)」卡顶是创建表单(两行四个输入 + 按钮),下方 channel-table 渠道表格,列头 ID / 名称 / 指向期望状态 / 操作。列表由后端 id 升序返回(软删行自动过滤)。

创建渠道表单字段表

字段位置类型必填校验规则默认保存逻辑
渠道名(name)首行左一文本框必填channelForm.name 为空 → 渠道名必填 拦截无,placeholder 渠道名,如 stablepayload.name 原样提交
描述(description)首行左二文本框选填''非空才进 payload(源码 description || ''
promotion_policy次行左一文本框选填非空且 trim 后非空 → JSON.parse,非法抛错被 catch无,placeholder promotion_policy JSON,如 {"batch_percent":10}parse 后的对象进 payload
approval_policy次行左二文本框选填同上无,placeholder approval_policy JSON,如 {"require_approval":false}parse 后的对象进 payload
「创建渠道」表单下(btn-sm btn-primary)按钮busy=false后端:空名 → CHANNEL_INVALID 400;重名 → CHANNEL_CONFLICT 409-成功提示 渠道已创建 id={id}(success),清空表单并 loadAll();失败提示 创建渠道失败:{msg}

渠道行内控件

控件含义必填/默认/可用条件操作效果触发后端调用边界与细节
ID / 名称主键与加粗名称纯展示-名称创建后不可改(编辑弹窗里 disabled)
「指向期望状态」下拉渠道当前发布目标:value="ch.current_desired_state_id || 0";选项含固定 未指向(值 0) 与各 active 期望状态 #{{ ds.id }} {{ ds.name }}切换即调 handleSetChannelDsPOST /channels/:id/set-desired-state(body {desired_state_id}已修复(提交 412e9c48):选 0(未指向)经 confirm 确认后提交 desired_state_id=0 解除指向、取消则回显当前指向(BundlePage.vue:548-572;后端 hub/channel.go:158-165 支持 0 清空指向);下拉选项仅列 active 期望状态,当前指向已非 active 时保留展示并标注状态(desiredStateOptions:360-374
「编辑」打开编辑渠道弹窗busy=falseopenEditChannel(ch) 预填弹窗(描述与两个策略 JSON.stringify)无(仅本地)保存见 4.6
「删除」删除渠道busy=falseconfirm('确定删除渠道 "{name}" 吗?') 确认后删除DELETE /channels/:id软删(deleted_at),成功提示 渠道已删除 并刷新;失败 删除渠道失败:{msg};确认框取消则什么都不发生

指向期望状态的结果语义:切换下拉 → busy=true → POST。选 未指向(0) 时先 confirm 解除指向;成功提示 渠道 "{name}" 已指向期望状态 #{desired_state_id}(选 0 时提示 已解除指向(未指向任何期望状态))(success)并刷新;失败提示 指向失败:{msg}(选 0 时 解除指向失败:{msg})且也刷新一次(让下拉回到后端实际值)。后端中 desired_state_id=0 直接清空指向(幂等),非 0 要求目标期望状态存在且 status=active(审批流未过审的也不行,见 IsApprovedForDeploy),否则 400 期望状态 N 未激活(当前状态 X),不可作为通道期望状态hub/channel.go:153-181)。渠道指向变化会直接影响 Agent 的 Pull 目标选择(见 5.4 渠道机制)。

4.6 「编辑渠道」弹窗

channelEditModal.visible 为 true 时以 modal-overlay 覆盖层出现,点遮罩(@click.self)或「关闭」可关。标题 编辑渠道 "{{current.name}}"

字段位置含义可用条件操作效果触发后端调用边界与细节
渠道名(name,不可修改)首行只读展示disabled 输入框(:value="current?.name"不可输入改名只能删除重建;payload 仍带原名(后端忽略 name)
描述(description)次行渠道描述可编辑,placeholder 渠道描述非空才进 payload-清空描述后保存 = 提交 description:'',后端会更新为空串
promotion_policy(JSON)第三行渐进发布策略 JSON可编辑,placeholder 如 {"batch_percent":10}trim 非空才 parse-留空则 payload 不含该字段
approval_policy(JSON)第四行审批策略 JSON可编辑,placeholder 如 {"require_approval":false}同上-同上
「保存渠道」弹窗底左提交busy=false;进行中文案 保存中...成功提示 渠道 "{name}" 已更新(success)、关闭弹窗、loadAll();失败 保存渠道失败:{msg} 且弹窗不关PUT /channels/:idpayload {name, description, promotion_policy?, approval_policy?};后端对策略字段做非空才更新的局部更新
「取消」弹窗底右(btn-outline)关闭弹窗常驻channelEditModal.visible = false放弃未保存改动,无确认

注意与创建的差异:后端 UpdateChannel 只更新描述与两个策略(name 参数在 handler 里根本没传),策略字段传空对象/空串时是幂等跳过而非清空——因此想用编辑弹窗把某个已填的策略清掉是做不到的(源码只对 trim 非空才 parse、后端也只对非空更新),这是「无法清空策略字段」边界(见 8 章)。

4.7 策略创建表单与策略列表

「部署策略(policies)」卡顶创建表单(名称 + rules + 按钮),下方表格列头 ID / 名称 / 规则 / 操作,列表 id 升序。

控件含义必填/默认/可用条件操作效果触发后端调用边界与细节
策略名placeholder 策略名,如 default必填空 → 策略名必填 拦截payload.name名称唯一、不可修改
规则(rules)placeholder rules JSON,如 {"auto_rollback":true}选填trim 非空才 JSON.parse空则不传 rules 字段,后端落默认规则非法 JSON 抛 SyntaxError → 创建策略失败:...
「创建策略」btn-sm btn-primarybusy=false成功 策略已创建 id={id}(success)、清空、刷新POST /policies后端:空名 策略名称不能为空 400;重复名冲突 409;rules 非法 策略规则非法 JSON: ... 400
规则列展示表格中 <code class="cell-code rules-cell">展示 JSON.stringify(p.rules || {})截断省略纯展示悬停无 title 全文,超宽省略;rules 为空(旧行)显示 {} 但实际解析用默认值
「编辑」btn-sm btn-outlinebusy=false打开编辑策略弹窗(见 4.8)--
「删除」btn-sm btn-dangerbusy=falseconfirm('确定删除策略 "{name}" 吗?')DELETE /policies/:id成功 策略已删除;失败 删除策略失败:{msg}

默认规则说明:新建策略不填 rules(或后端发现 rules 为空)时,后端把 DefaultPolicyRules() 序列化落库——即 {"auto_rollback":true,"max_concurrent":2,"readiness_timeout_sec":60,"reconcile_backoff":{"initial_s":5,"multiplier":2,"max_s":300}}。因此本页创建的每条策略保存后都会带一套完整 JSON,而非空对象。

4.8 「编辑策略」弹窗与「验签结果」弹窗

编辑策略弹窗policyEditModal):标题 编辑策略 "{{current.name}}";字段表:

字段位置含义可用条件操作效果触发后端调用边界与细节
策略名(name,不可修改)首行只读展示disabled不可输入-改名需删除重建
rules(JSON)次行策略规则textarea rows="5" spellcheck=false,编辑时以 JSON.stringify(p.rules, null, 2) 格式化填充trim 非空才 parsePUT /policies/:id空 rules 提交 → 后端重置为默认规则(UpdatePolicy 空 rules 会 DefaultPolicyRules().ToJSON()
「保存策略」底左提交busy=false,中文案 保存中...成功 策略 "{name}" 已更新、关弹窗、刷新;失败 保存策略失败:{msg} 不关弹窗-修改在 textarea 里手写,无 schema 校验
「取消」底右关闭常驻放弃未保存改动-无确认

验签结果弹窗verifyModal):标题 验签结果(bundle #{id}),正文 <pre class="code-block">{{ JSON.stringify(result, null, 2) }}</pre>result 为验签成功时的 VerifyResult{files_ok,digest_ok,signature_ok,trusted,signer_name})或失败时的 {error: "..."}。深色等宽代码块 white-space: pre-wrap 会折行,超长 digest/错误串也能完整看到。「关闭」或点遮罩关闭,无二次操作入口。

5. 后端关联

5.1 API 客户端

action/web/src/api/apolloClient.js:axios 实例 baseURL /apollo-api/v1、timeout 30000ms、默认 Content-Type: application/json(构建 bundle 时显式覆盖为 multipart/form-data)。请求拦截器对所有请求附加 Authorization: Bearer <token>(仅取 apollo_token,不回退 aip_token)。响应拦截器:HTTP 2xx 且 code===0 → 把 response.data 解包为业务数据;HTTP 2xx 但 code 非 0 → reject;4xx/5xx → 从 body.message 提取并 reject(401 清 apollo_token 并跳 /apollo/login(登录页自身 401 不跳防死循环),403 alert('无权限执行该操作')),同时把服务端 message 别名写入 error.response.data.errorerror.message——页面 catch 统一用 err.response?.data?.error || err.message 取文案。blob/arraybuffer 响应(zip 下载)原样放行不解包。

5.2 端点表

方法路径权限请求体/Query页面触发点
GET/bundlesPermBundleRead-加载 bundle 制品列表(id 降序)
POST/bundlesPermBundleWritemultipart:app/version/bundle_version/signer + 以路径为键的文件「构建 bundle」
GET/bundles/:id/downloadPermBundleRead-(响应 zip blob)「下载 zip」
POST/bundles/:id/verifyPermBundleWriteQuery 可选 environment_id(页面不传)「验签」
POST/bundles/cleanup/runPermBundleWrite-无 UI 入口(后端/脚本触发,见 5.4)
GET/channelsPermChannelRead-加载渠道列表(id 升序)
POST/channelsPermChannelWriteJSON {name, description?, promotion_policy?, approval_policy?}「创建渠道」
PUT/channels/:idPermChannelWriteJSON 同上(name 忽略,仅更新非空 description/策略)「保存渠道」
DELETE/channels/:idPermChannelWrite-「删除」(软删)
POST/channels/:id/set-desired-statePermChannelWriteJSON {desired_state_id}「指向期望状态」下拉
GET/policiesPermPolicyRead-加载策略列表(id 升序)
POST/policiesPermPolicyWriteJSON {name, rules?}(空 rules 落默认)「创建策略」
PUT/policies/:idPermPolicyWriteJSON {rules?}(空 rules 重置默认)「保存策略」
DELETE/policies/:idPermPolicyWrite-「删除」
GET/desired-statesPermDesiredStateReadQuery 可选 app(页面不带,全量)「指向期望状态」下拉选项

所有端点注册于 action/products/apollo/server/server.go protected 组(约 746-765 行)。返回形状:创建类 201 {id}(bundles 除外——返回完整记录)、修改类 {id}、删除类 {id, deleted:true}、指向类 {id, desired_state_id}

5.3 响应结构示例

GET /bundles(envelope 解包后为数组,单项 BundleRecord

[
  {
    "id": 7,
    "name": "demo-app-1.0.0-b2",
    "app": "demo-app",
    "version": "1.0.0",
    "bundle_version": 2,
    "digest": "8f3a1c2e5b9d0476aa11cc22dd33ee44ff55aa66bb77cc88dd99ee00ff11aa22",
    "signer_id": "demo-signer",
    "signer_fingerprint": "c9d8...",
    "size_bytes": 4096,
    "created_at": "2026-09-07T03:12:00+08:00"
  }
]

字段含义(server/models.go BundleRecord):id 主键;nameBundleID{app}-{version}-b{bundle_version});digest bundle 内容哈希(size:128;uniqueIndex,G7 唯一连接键);signer_fingerprint = SM3(公钥) 指纹(白名单匹配键,展示列悬停全文);size_bytes 各文件字节和;dir 字段 json:"-" 不外发(落盘路径只在服务端用,下载/验签按 id 反查)。

POST /bundles(201,envelope 解包后即 BundleRecord):响应就是刚登记的完整记录(同列表单项),页面 toast 用其中的 rec.id/rec.digest/rec.signer_fingerprint。成功时 digest 冲突走 upsert:digest 唯一索引 OnConflict 只更新 size_bytes/dir 两列,id 保持首次登记的旧值。

POST /bundles/:id/verify(200)

{
  "files_ok": true,
  "digest_ok": true,
  "signature_ok": true,
  "trusted": true,
  "signer_name": "demo-signer"
}

字段含义(hub/bundle.go VerifyResult):files_ok 逐文件重算 SM3 校验和与大小对照 manifest(含文件数与路径覆盖检查);digest_ok 用 manifest+文件重算 digest 对照 manifest.Digesttrusted 白名单信任锚命中(指纹存在且 enabled=true——注意它不隐含其它三项通过;signer_name 命中的白名单签名者名(omitempty)。400/403 错误分支:制品加载失败 400 BUNDLE_INVALID;指纹不在白名单(或 enabled=false)→ HTTP 403,envelope 保留领域码与原始 message(见下,页面 catch 把 message 展示在弹窗):

{
  "code": "VERIFY_SIGNER_NOT_TRUSTED",
  "message": "签名者指纹 \"c9d8...\" 不在可信白名单(enabled=true)中,拒绝应用",
  "request_id": "req_1725600000000abcd"
}

GET /channels(envelope 解包后为数组,单项 ReleaseChannel

[
  {
    "id": 1,
    "name": "stable",
    "description": "演示发布通道(指向 demo-app)",
    "current_desired_state_id": 3,
    "promotion_policy": { "batch_percent": 10 },
    "approval_policy": { "require_approval": false },
    "created_at": "2026-09-07T02:00:00+08:00",
    "updated_at": "2026-09-07T02:00:00+08:00"
  }
]

current_desired_state_id 为 0 或缺失即「未指向」(页面 select 映射成 未指向);两个策略字段 omitempty,未填的渠道行不会出现该键。set-desired-state 响应 {id: 1, desired_state_id: 3}(页面 toast 取 data.desired_state_id)。

GET /policies(envelope 解包后为数组,单项 DeploymentPolicy

[
  {
    "id": 1,
    "name": "default",
    "rules": {
      "auto_rollback": true,
      "max_concurrent": 2,
      "readiness_timeout_sec": 60,
      "reconcile_backoff": { "initial_s": 5, "multiplier": 2, "max_s": 300 }
    },
    "created_at": "2026-09-07T02:00:00+08:00",
    "updated_at": "2026-09-07T02:00:00+08:00"
  }
]

GET /desired-states(下拉选项数据源):页面只消费每项的 idname(渲染 #{{ ds.id }} {{ ds.name }}),但后端返回整行 DesiredState(含 status/approval_status/digest/version 等,见下方折叠),页面未做任何筛选/状态展示。

GET /desired-states 单项示例(DesiredState 全字段)
{
  "id": 3,
  "name": "demo-app",
  "description": "",
  "app": "demo-app",
  "version": "1.0.0",
  "bundle_version": 1,
  "digest": "8f3a1c2e5b9d0476...",
  "signer_id": "demo-signer",
  "declaration": { "app": "demo-app", "version": "1.0.0", "name": "demo-app", "channel": "stable", "components": [ ... ] },
  "status": "active",
  "approval_status": "",
  "created_by": "system",
  "created_at": "2026-09-07T02:00:00+08:00",
  "updated_at": "2026-09-07T02:00:00+08:00"
}

5.4 关键机制

① bundle 打包与签名(构建即签名,契约④)POST /bundles 的完整链路:handleCreateBundle 校验后 parseFormFiles 把 multipart 解成 []BundleSourceFile{Path,Data}(Path = 文件键,空键回退 fh.Filename);bundleStore.build 里若请求 signer 为空自动兜底 demo-signer,随后 ensureSigner() 惰性取演示签名者——首次构建crypto.GenerateSM2KeyPair() 生成 SM2 密钥对(私钥仅存进程内存),计算指纹 SignerFingerprint = SM3(公钥字节) 并幂等 upsert trusted_signersname 冲突更新 public_key_fingerprint/public_key/enabled/description,行名固定 demo-signer、描述「演示签名者(服务器首次构建 bundle 时自动注册,G1 信任锚)」),因此 seed 里那只占位指纹 demo-fingerprint-placeholder 会在第一次构建后被真实指纹顶替,白名单自洽。随后 hub.BuildBundle 四步:① 枚举文件生成清单 Files[](path/size/SM3 校验和);② 由文件路径推导组件清单 Components[]classifyComponentKind:路径含 ontologyontology_yamlllm_route/llm-route/llmroutellm_route、含 evaleval_set、含 config 或以 .yaml/.yml 结尾→config、否则→binary;组件名取文件基名去扩展名);③ digest = SM3(manifest 规范化 JSON(digest 置空防自引用)+ 全部文件字节按 Path 字典序拼接)(G7 不可变连接键);④ 用 SM2 私钥对 digest 签名,manifest 记 signer_fingerprintsigned_at。之后制品落盘:目录 = ./temp/apollo_bundles/<digest>bundleDirBase,相对进程工作目录),内部结构为 manifest.jsonjson.MarshalIndent 缩进)+ digest.sig(签名 hex 字符串)+ files/<sanitized路径>(每文件先 sanitizeRelPath 防穿越再 MkdirAll 写盘);记录登记 bundles 表(name=BundleIDdigest 唯一索引、size_bytes = 清单各文件字节和)。

② multipart 上传的边界与静默截断parseFormFilesr.ParseMultipartForm(32 << 20)(整请求内存/磁盘缓冲上限 32MB,超限后端报解析错误),再对每个上传文件 io.ReadAll(io.LimitReader(f, 8<<20))——单文件超过 8MB 会被静默截断到 8MB:字节被砍掉,但后端不比对原长度、不报错,只会构建出一个「manifest 里 size 与你上传的不一致」的制品。前端无法从响应发现截断(页面 formatSize 展示的是截断后清单里的大小)。已加前端前置拦截(提交 412e9c48):选文件时即按 MAX_FILE_SIZE(8MB) 预校验并提示(BundlePage.vue:398-413),从入口避免上传超限文件(见 8 章)。另外 multipart 以「文件键」为字段名,若两个文件行填了相同路径,后 append 的会覆盖先 append 的(同名键最后一次生效),前端行内不提示重复(见 7.7)。

③ 验签链与「可信」语义(G1)POST /bundles/:id/verify(可选 ?environment_id=,页面不传,走全局白名单)→ store.verifyLoadBundleFromDir 从落盘目录读回制品 → hub.VerifyBundle/VerifyBundleScoped 按序执行:1) verifyFiles 逐文件重算 SM3 与大小对照 manifest(文件数、路径覆盖都查)→ files_ok;2) 重算整体 digest 对照 manifest.Digestdigest_ok;3) 用 manifest.SignerFingerprint 在白名单查(环境级 scope 时环境级优先回退全局,需 enabled=true)——命中即 trusted=true 并记 signer_name,未命中/禁用直接返回 HTTP 403(领域码 VERIFY_SIGNER_NOT_TRUSTED、message 含指纹);4) 有签名且 digest 非空才用白名单公钥做 SM2 验签 → signature_ok(无签名时 signature_ok=false 但仍 200 返回)。需要点破的是:后端 trusted 字段只代表「签名者进了白名单」,不等于四步全过——若落盘文件被人为改过,步骤 1/2 会置 files_ok=false/digest_ok=false 但响应仍 200、trusted=true(签名是对 manifest 里存的 digest 验的,digest 字段没变就验得过)。已修复(提交 412e9c48):页面徽标改为四项合取判定(verifyResultOk = trusted && files_ok && digest_ok && signature_okBundlePage.vue:355-357),并且验签弹窗把四项分开展示「通过/失败」徽标(BundlePage.vue:284-304),因此这类"内容被篡改但签名者仍可信"的制品不再误显示绿徽标。对本页演示路径(构建即验签)四步恒全过、白名单恒命中,结论与直觉一致;异常只在制品被外部改盘/白名单变动时出现。

④ bundle 生命周期清理(本页无 UI,后端保留策略驱动)POST /bundles/cleanup/run 会先 ensureDefaultBundlePolicy:若不存在 (project_id=0, scope=bundle) 的清理策略则创建 KeepCount=10artifact_cleanup_policies 表);再读策略(enabled=false 则跳过)按每 app 分组 CreatedAt DESC 保留最近 N 个,叠加 keep_days/keep_tags 过滤,标记过期 bundle 后删除记录与本地目录。引用保护desired_states.digest(G7 连接键)与 artifacts.bundle_id 引用中的 bundle 强制保留;删除目录前 dirWithinBundleBase 校验目标在 ./temp/apollo_bundles 内(纵深防御,防 DB 的 Dir 被篡改指向任意目录),trusted_signers 白名单不随 bundle 删除。该端点显式写审计 BUNDLE_CLEANUP_RUN(scanned/deleted/freed_bytes)。演示中若发现 bundle「自己不见了」,多半是有人调过清理或按保留策略过期,本页没有恢复入口。

⑤ 渠道机制:软删、唯一名、指向须 active、策略是存储而非执行release_channels 用 GORM DeletedAt 软删,列表自动过滤;name 唯一索引,重名创建返回 409(CHANNEL_CONFLICT),空名 400(CHANNEL_INVALID)。SetChannelDesiredStatedesired_state_id=0直接清空当前指向并落库(幂等),非 0 时校验期望状态存在且 status=activeRequiresApproval/审批态见下)(hub/channel.go:153-181)——页面选「未指向」经确认即解除指向,前后端两处均已打通。promotion_policy/approval_policy 在此仅作 JSON 存储:审批 gate(approval_status 驱动 Spoke Pull 拦截)在期望状态审批端点(G15)执行,渐进发布的 rollout 百分比在编排侧消费,本页不触发执行,只负责把它填进渠道。Seed 里自动创建渠道 stable(描述「演示发布通道(指向 demo-app)」)并指向 demo 期望状态,若其指向已被清/弃用则 seed 会重置指向 demo——即 demo 环境里 stable 渠道永远是「有指向」的,演示 Pull 目标选择因此恒能命中第二优先级。

⑥ 部署策略机制:空规则落默认、按名解析兜底policiesrules 为 JSON;CreatePolicy/UpdatePolicy 遇到空 rules 都会 DefaultPolicyRules().ToJSON()auto_rollback:truemax_concurrent:2readiness_timeout_sec:60reconcile_backoff{initial_s:5,multiplier:2,max_s:300}),非法 JSON 400(策略规则非法 JSON: ...);重名创建冲突(名称唯一索引)。部署编排按策略名解析:ResolveRulesByPolicyName 找到则 ResolvePolicyRules(字段级默认值兜底),找不到或解析失败直接读 DefaultPolicyRules()——策略被删不会让部署崩溃,只是退回默认行为。Seed 创建 default 策略供部署引用。

⑦ 前端并行加载与互斥loadAllPromise.all 并行拉 GET /bundles/channels/policies/desired-states 四份,每份 .catch(() => ({data:[]}))——任一接口失败静默降级为空数组,不提示(例如无 PermDesiredStateRead 权限时下拉会空、但其余三卡正常)。页面 onMounted 与每个写操作成功/失败(部分)回调都会 loadAll()busy 全局互斥保证「构建/验签/下载/渠道写/策略写」不会并发交叠,避免提示条互相覆盖与列表竞态。因为没有独立的列表刷新按钮,若四份数据之一在别处被改,需要在本页做一次写操作或刷新页面才能看到新值。

6. 权限与安全

7. 常见问题与排错

以下问题均从 BundlePage.vue 提示分支、后端 handler/服务层错误码与既有测试归纳,可按步骤复现。

问题一:点「构建 bundle」提示 app/version 必填或缺少文件

现象:点「构建 bundle」无反应或提示 app/version 必填且至少需要一个文件

原因:app 或 version 为空,或所有文件行都没选中本地文件(file 为 null 的行在 handleBuild 里被 filter((f) => f.file) 过滤掉)。

处理:填全 app/version,至少给一行点选真实文件;路径可留空(后端回退文件名),但不能只填路径不选文件

问题二:构建成功但提示里签名者是「-」,或验签报 VERIFY_SIGNER_NOT_TRUSTED

现象:构建成功提示文案的签名者取记录 signer_fingerprint 是否存在(已签名/-);若为 - 通常指该记录没带指纹。

原因:验签 403 的最常见诱因是白名单里没有该指纹的 enabled 签名者——例如 seed 重建过、或有人在签名者白名单页删/禁用了 demo-signer

处理:到「签名者白名单」页确认 demo-signer 存在且 enabled;用默认 signer 重建一个 bundle 即可(重建会触发 ensureSigner 重新注册)。注意验签 403 时点「验签」会弹窗展示错误而非绿徽标,这是预期的失败展示路径。

问题三:「验签」按钮点了没有提示、列表整块空

现象:验签失败也会弹窗+出红徽标,不会无声;若 bundle 卡直接空,多半是列表数据没加载到。

原因GET /bundles 401/403 被静默降级成空数组(loadAll.catch(() => ({data:[]})) 不提示)。

处理:打开浏览器 Network 看 /apollo-api/v1/bundles 响应;401 会先跳登录页,403 会有 alert('无权限执行该操作'),确认账号具备 PermBundleRead

问题四:点「验签」弹 alert「无权限执行该操作」

现象:点「验签」被 403。

原因:验签端点后端注册为 PermBundleWritePOST /bundles/:id/verifyserver/server.go:767),而账号只有读权限。

处理:这是后端路由设计(验签被归为写类动作),用具备 bundle:write 权限的角色操作;若认为读角色也该能验签,需后端把路由改为 PermBundleRead(见 8 章 D5,前端无法改变)。

问题五:渠道「指向期望状态」报「期望状态 N 未激活(当前状态 draft)」

现象:选了状态后提示 指向失败:期望状态 3 未激活(当前状态 draft),不可作为通道期望状态

原因:下拉通常只列 active 状态,但当前指向若已非 active 仍会保留展示,或后端状态在打开下拉后被改变。

处理:到期望状态管理侧激活目标状态(需其审批 approval_status 为空或 approved)再回来重选;选「未指向」经确认即解除指向BundlePage.vue:548-572,后端 hub/channel.go:158-165 支持 0 清空)。

问题六:渠道/策略 JSON 保存时报英文 SyntaxError 类错误

现象:提示 ...:Unexpected token 'x', "..." is not valid JSON 一类英文报错。

原因:promotion_policy/approval_policy/rules 输入框里的内容不是合法 JSON,JSON.parse 抛 SyntaxError,被 catch 拼进失败提示(创建渠道失败:/保存渠道失败:/创建策略失败:/保存策略失败: + 原始解析器报错)。

处理已修复(提交 412e9c48):前端改用 tryParseJSON(text, label)BundlePage.vue:378-396),解析失败返回中文提示并按 position 定位换算成 第 N 行第 M 列附近语法错误(如 promotion_policy 不是合法 JSON:第 1 行第 5 列附近语法错误),不再透传英文 SyntaxError。按 placeholder 的完整对象格式填写(如 {"batch_percent":10}{"auto_rollback":true})。

问题七:同路径文件互相覆盖

现象:同一个 bundle 里放了两个同名路径文件,构建出的制品只有一个。

原因:multipart 以「文件键」为字段名,相同路径会互相覆盖(后 append 的生效),后端 digest 也是按清单路径唯一计算的。

处理:构建前检查文件行路径不重复;这不是报错场景,后端不提示。另外若你期望文件放进子目录,路径应写 config/app.yaml 这类相对路径(会落盘到 files/config/app.yaml 并出现在 zip 的对应子目录)。

问题八:下载失败或 zip 里缺文件

现象:提示「下载失败」,或下载下来的 zip 里文件不完整。

原因:下载失败通常是记录 id 对应的落盘目录已不在(bundle 被清理策略删除),后端打包抛 500 打包 bundle 失败: ...。缺文件则要怀疑上传时就超了 8MB 单文件上限被静默截断(见 5.4-②)——zip 里文件是截断后的内容,manifest size 也对不上你的原文件。

处理已修复(提交 412e9c48):前端选文件时即按 MAX_FILE_SIZE(8MB)预校验,超限文件会清空选择并提示「…超过后端单文件 8MB 上限,会被静默截断,请拆分后再上传」(BundlePage.vue:398-413),从入口拦住截断。若仍见文件缺失,确认构建成功提示里大小列是否与本地文件一致;被清理的 bundle 只能重新构建,本页无恢复入口。

问题九:编辑渠道想清空策略字段清不掉

现象:清空 promotion_policy 保存后再打开它还在。

原因:前端只对 trim 非空的 JSON 才 parse 进 payload,后端 UpdateChannel 也只在字段非空时更新——两个策略字段都没有「传空清除」通道。

处理:属后端契约边界(见 8 章「无法清空策略字段」),临时办法是删除渠道重建(不填策略);注意这与「解除渠道指向」不同,后者已支持(下拉选「未指向」)。

问题十:页面某一块列表为空但其它正常

现象:四个卡中某一个列表为空,其它正常。

原因loadAll 四接口任一失败都会静默降级为对应空数组,且页面没有手动刷新按钮。

处理:刷新整页重拉(onMounted 会再次 loadAll);仍空则看 Network 定位失败的接口与状态码——例如缺 PermDesiredStateRead 时「指向期望状态」下拉为空,但渠道卡仍正常(静默降级属已知边界,见 5.4-⑦)。

问题十一:改动渠道/策略后另一处没生效

现象:策略改了但部署表现不变,或渠道改名后 stable 链路不认。

原因:策略在部署发起/Advance 时按名称实时解析(ResolveRulesByPolicyName);渠道指向变化要在 Agent 下一次 Pull 时才体现。Pull 目标选择只认名为 stable 的渠道(见 5.4-⑤ 与 agents 页 5.4),改渠道名需要删除重建。

处理:策略改完保存立即生效,无需重启;需要新渠道进入发布链路时保持名称为 stable

8. 已知缺陷与边界

缺陷/边界说明
D1「未指向」选项不可用(已修复)已修复(提交 412e9c48):前端选「未指向」经 confirm 后提交 desired_state_id=0 解除指向、取消则回显当前指向(BundlePage.vue:548-572),后端 SetChannelDesiredState 对 0 直接清空指向并落库(hub/channel.go:158-165)——渠道现可在本页解除指向
D2 验签徽标只取 trusted(已修复)已修复(提交 412e9c48):徽标改为四项合取 trusted && files_ok && digest_ok && signature_okBundlePage.vue:355-357),验签弹窗分项展示四项「通过/失败」(:284-304);后端 trusted 语义(仅白名单命中)不变,但页面不再单看它
D3 期望状态下拉不过滤(已修复)已修复(提交 412e9c48):下拉仅列 status==='active' 期望状态(当前指向已非 active 时保留展示并标注状态,desiredStateOptionsBundlePage.vue:360-374);后端仍只允许指向 active(hub/channel.go:173-177
D4 JSON 输入无友好校验(已修复)已修复(提交 412e9c48)tryParseJSON(text, label) 解析失败返回中文 {label} 不是合法 JSON:第 N 行第 M 列附近语法错误BundlePage.vue:378-396),替代裸 JSON.parse 的英文 SyntaxError
D5 验签挂在写权限(现实边界)POST /bundles/:id/verify 后端注册为 PermBundleWriteserver/server.go:767),语义是只读诊断却要求写权限,最小权限的只读角色无法验签。属后端路由契约边界,需后端改 PermBundleRead 才能收口(前端无法改变)
无法清空策略字段(现实边界)创建/编辑渠道时两个策略 JSON 为空即不传字段,后端 UpdateChannel 只更新非空——不存在「显式清空 promotion_policy/approval_policy」的通道;编辑渠道描述与策略互相独立,保存渠道只发 description 或策略的局部更新(后端契约边界)
单文件 8MB 静默截断(前端已加预校验)后端 io.LimitReader(f, 8<<20) 静默截断超大文件且不比对原长度;multipart 整请求 32MB 上限超限则直接解析失败(后端边界)。前端已加前置拦截(提交 412e9c48):选文件时按 MAX_FILE_SIZE(8MB) 预校验,超限清空并提示(BundlePage.vue:398-413
无列表刷新/分页四列表均一次性全量(bundle 数大时表格会很长),页面无手动刷新按钮、无分页、无搜索/过滤;期望状态「按 app 过滤」能力未暴露
空态缺指引bundle/渠道/策略/期望状态各自空态文案不一(bundle 有 暂无 bundle,请构建一个。,渠道与策略空表体无文案),首次进入对「怎么建渠道、怎么建策略」无引导
并发约束全部写操作共用 busy 串行;删除渠道/策略虽有 confirm,但渠道被删除时若有部署/Agent 依赖其指向,指向随软删清空,无预警

附录 A:速查——页面文案与关键常量清单

场景源码取值/文案(逐字)
页面标题Apollo 制品与渠道
卡片标题构建 bundle / bundle 制品 / 发布渠道(channels) / 部署策略(policies)
构建表单标签应用(app)* / 版本(version)* / bundle_version * / 签名者(signer)
构建 placeholder如 order-service / 如 1.0.0 / 如 demo-signer
文件区标题制品文件(文件键为 bundle 内路径)
路径 placeholderbundle 内路径,如 config/app.yaml
构建按钮构建 bundle / 构建中... / + 文件行 / 删除
构建成功提示bundle 构建成功 id={id} digest={前16位}(签名者 {已签名|-})
构建失败提示构建失败:{msg}
前端校验文案app/version 必填且至少需要一个文件
空态暂无 bundle,请构建一个。
验签按钮/徽标验签 / 可信(verify-ok 绿)/ 不可信(verify-bad 红)
下载下载 zip / 成功 bundle #{id} 已下载 / 失败 下载失败:{msg}
验签弹窗标题验签结果(bundle #{id})
渠道 placeholder渠道名,如 stable / 描述 / promotion_policy JSON,如 {"batch_percent":10} / approval_policy JSON,如 {"require_approval":false}
渠道按钮/提示创建渠道 / 渠道已创建 id={id} / 渠道已删除 / 渠道名必填 / 请选择有效的期望状态
渠道指向提示渠道 "{name}" 已指向期望状态 #{desired_state_id} / 指向失败:{msg}
渠道编辑弹窗标题 编辑渠道 "{name}" / 标签 渠道名(name,不可修改) / 保存渠道 / 保存中... / 取消 / 关闭
策略 placeholder策略名,如 default / rules JSON,如 {"auto_rollback":true}
策略按钮/提示创建策略 / 策略已创建 id={id} / 策略已删除 / 策略名必填 / 编辑标题 编辑策略 "{name}" / 保存策略
默认策略规则{"auto_rollback":true,"max_concurrent":2,"readiness_timeout_sec":60,"reconcile_backoff":{"initial_s":5,"multiplier":2,"max_s":300}}
bundle 落盘目录./temp/apollo_bundles/<digest>bundleDirBase
BundleID 命名{app}-{version}-b{bundle_version}(manifest.schema_version=1.1