所属产品:LightApollo · 信息截止:2026-09-14

1. 页面概览

1.1 是什么

「环境管理」是 LightApollo 的部署目标环境管理页(对应前端源码 action/web/src/views/apollo/EnvironmentsPage.vue,对齐 TAD-05 多环境管理)。LightApollo 采用 GitOps 模型:环境是部署、同步与漂移检测的作用对象。期望状态(desired state)在审批通过后要落到某个环境,实际状态则由部署在该环境上的 Spoke Agent 周期性上报;两者之间的差异就是漂移,需要调和收敛。本页就是这些「环境」的注册、状态与接入 Agent 的统一管理面。

环境在概念上表示一套可部署的计算资源,如 Kubernetes 集群、虚拟机集群、裸金属服务器或边缘设备,通常按 dev / staging / prod 划分。环境之间相互独立,同一份期望状态可以发布到多个环境,同一个 Agent 只归属一个环境。页面把环境的完整生命周期管理起来:新建时登记基本信息(名称、显示名、类型、云厂商、地域与可选配置 JSON),创建后环境状态按状态机从 registering 起步流转;测试连接会为环境写入一条 health_check 探测任务;接入 Agent 则需要为环境签发一次性 Bootstrap Token,Spoke Agent 凭该 Token 引导登记,此后周期心跳让环境保持 online。操作列还提供编辑、删除与 Agent 生命周期管理(重启 / 升级 / 吊销)。

页面的输入是操作者对环境与 Agent 的管理意图,输出则是对 env_agentsenvironmentsagent_tasks 等表的写库结果(环境列表、summary 计数、Agent 列表与一次性 Token 即为其可视化呈现)。需要特别指出:环境不是本产品唯一的「目标」——「部署与漂移」「Spoke Agent」等页同样围绕期望状态与 Agent 展开;环境页重点在于环境本身及其Agent 登记与纳管,Spoke Agent 页的「模拟上报」也会对环境的在线状态产生联动(上报即心跳)。「Git 仓库」页的同步目标(sync target)用 target_environment_id 指向本页登记的环境,因此新建环境后即可在 Git 仓库页把它选作同步落点。

这里需要把两张 Agent 表区分清楚,否则很容易把本页与「Spoke Agent」页看混。环境页的「已注册 Agent 列表」读的是 env_agents(受管 Agent:由本页签发 Bootstrap Token 引导登记,生命周期由本页的重启/升级/吊销按钮管理);而「Spoke Agent」页在线列表读的是服务端 spoke_agents 快照表(真正上报过实际状态的节点)。两者的关联链条是:一个真实部署的 Spoke Agent 先凭本页签发的一次性 Token 完成引导登记、写入 env_agents 且置 active,随后开始周期性 pull 期望状态、report 实际状态,服务端据上报维护 spoke_agents 并把心跳回写到 env_agents 与其所属环境。因此同一逻辑节点往往先后出现在两个页面的列表里:先在环境页登记为受管 Agent,再在 Spoke 页作为在线节点出现。反过来,若某个旧链路 Agent 不经过本页登记(env_agents 无记录),它仍可能在 Spoke 页上线,但环境页的列表不会包含它——两套数据的口径并不强制一致。

给第一次接触本页的操作者一条最短动线,能少走弯路:第一步在本页「新建环境」登记目标(填名称 + 选类型即可,配置先空着);第二步打开该行的「Agents」弹窗「注册 Agent」拿到一次性 Bootstrap Token,立即复制保存;第三步把 Token 交给真实 Spoke Agent(或在 Spoke Agent 页用同一 Agent 名做模拟上报),Agent 登记成功后本页该环境心跳变为 online;最后到「Git 仓库」页新建同步目标,把 target_environment_id 指向这个环境,即可开始 GitOps 配置下发。中途任何一步缺位(例如没建环境就想在 Git 仓库页选目标环境,或没发 Token 就想让 Agent 登记),都会在对应页面暴露失败原因。

1.2 核心价值表

能力说明对应页面操作
环境列表表格展示全部环境:ID/名称/显示名/类型/状态/地域/云厂商/Agent/最后心跳,状态徽标区分进入页面自动加载
summary 统计五张卡片分别统计环境总数与 online / degraded / registering / offline 四态数量进入页面自动加载,新建/删除后刷新
新建环境登记 name/display_name/type/provider/region/config JSON,类型与云厂商用下拉避免手填错值页头「新建环境」弹窗
测试连接向环境写入 health_check 探测任务(优先挂到首个 active Agent),检测可达性操作列「测试连接」
Agent 注册为环境签发一次性明文 Bootstrap Token(envt_ 前缀),登记 Agent 仅存 Token 哈希Agents 弹窗「注册 Agent」
一次性 Token 展示与复制深色代码块醒目展示仅本次可见的 Token,一键复制到剪贴板弹窗「复制 Token」
Agent 生命周期对已登记 Agent 执行重启 / 升级 / 吊销(吊销即不可逆,Token 立即失效)Agent 列表操作列
编辑 / 删除环境编辑显示名/地域/云厂商(类型与配置不可改);删除为级联物理删除操作列「编辑」/「删除」

1.3 一句话总结

环境管理页统一登记与管理 dev/staging/prod 等部署环境,签发一次性 Bootstrap Token 完成 Spoke Agent 的引导接入,让每个环境随时能作为部署与 GitOps 同步的目标落点。

2. 访问入口

2.1 路由与菜单

路由 path/apollo/environments
路由 nameApolloEnvironments
meta.titleApollo 环境管理
侧边栏位置ApolloLayout 侧边栏「环境管理」(位于「部署与漂移」之后、「Git 仓库」之前)
前端源码action/web/src/views/apollo/EnvironmentsPage.vue
API 客户端action/web/src/api/apolloClient.js
路由注册action/web/src/router/index.js:import 于第 22 行,children 注册于第 421~426 行

2.2 认证与权限

权限覆盖操作
PermEnvRead环境列表、summary、详情、Agent 列表、Agent 详情、resources/applications 占位接口
PermEnvWrite新建环境、编辑、删除
PermEnvExecute测试连接、注册 Agent、吊销/重启/升级 Agent

2.3 端口与 API 前缀

3. 界面布局

┌─────────────────────────────────────────────────────────┐
│ Apollo 环境管理                            [ 新建环境 ]      │
│ [ 操作结果提示条(可关闭)]                                  │
│ [ 环境总数 ] [ 在线 online ] [ 降级 degraded ]               │
│ [ 注册中 registering ] [ 离线 offline ]                     │
│ [ 环境列表卡片 ]                                            │
│  ID 名称 显示名 类型 状态 地域 云厂商 Agent 最后心跳 操作       │
│  操作列:测试连接 / Agents / 编辑 / 删除                      │
│ [ 新建环境弹窗 ]  [ 编辑环境弹窗 ]                            │
│ [ Agent 管理弹窗(注册输入 + 一次性Token块 + 已注册Agent表)]    │
└─────────────────────────────────────────────────────────┘
板块职责
操作结果提示条展示成功/失败/信息提示,右侧「关闭」按钮可收起
summary 卡片行五张卡片统计各状态环境数,顶部色条区分状态(在线绿 / 降级橙 / 注册中蓝 / 离线灰)
环境列表卡片加载中显示「加载中...」;空数据显示「暂无环境,点击"新建环境"注册(项目 #1)。」;有数据渲染表格
弹窗区新建/编辑环境为表单弹窗;Agent 管理弹窗分「注册新 Agent」「一次性 Token」「已注册 Agent」三块

页面整体走白底卡片风格:顶部是页头(左标题、右主按钮),随后是提示条、summary 行、列表卡片,弹窗为居中的半透明遮罩(点击遮罩空白处也能关闭,等于是「取消」)。弹窗宽度分级:编辑环境用小号、新建环境用中号、Agent 管理用大号。Agent 弹窗里的一次性 Token 区块特意用黄底描边(token-block)包裹,Token 本身以等宽字体在深色代码块(token-pre)内高亮,视觉上强调「仅此一次」。加载中与空数据都有占位文案(「加载中...」「暂无环境,点击"新建环境"注册(项目 #1)。」),不会出现白屏。

4. 交互元素

在逐个控件展开之前,先说明两条贯穿全页的交互约束,便于后文理解。其一,全局互斥 busy:页面只有一个 busy 布尔位,任一异步操作进行中(创建/编辑/删除/测试连接/Agent 注册等)都会同时禁用列表操作列的四个按钮与所有弹窗的提交按钮,避免并发写库或重复点击;正常操作的响应都在秒级以内,若长时间停在「创建中.../保存中.../注册中...」不恢复,多半是请求被拦截器静默处理(如 401 跳转)或后端未就绪。其二,危险操作二次确认:删除环境、吊销 Agent 属于不可逆操作,前端一律先弹浏览器原生 confirm 确认框,点「取消」则不发起任何请求;其余操作(编辑、测试连接、注册、重启、升级)没有二次确认,点了立即执行。

4.1 summary 卡片区

控件位置含义默认/可用条件操作效果触发后端调用边界与细节
五张 summary 卡片列表上方环境总数与四态计数页面挂载即加载纯展示GET /projects/1/environments/summary加载失败静默降级为全零,不阻断主列表,无错误提示

卡片取值自 summary 对象 {total, online, degraded, registering, offline}。实现上 parseSummary() 兼容后端嵌套 {project_id, summary:{total, by_type, by_status}}、数组 [{status,count}]{items:[...]} 与扁平对象多种形态。已修复(提交 412e9c48)parseSummary 现递归进 data.summary 子对象并读取 by_status 计数(EnvironmentsPage.vue:363-386),卡片计数按后端真实形态正确归一化,不再退化为全零。新建环境成功后与删除环境成功后均会重新拉取 summary。

4.2 环境列表与状态徽标

控件位置含义边界与细节
环境行列表卡片每行一个环境「显示名/地域/Agent」为空显示 -;「最后心跳」为空或非法时间显示 -formatTimeT 换成空格、去掉 Z、截到秒)
状态徽标「状态」列环境状态online→绿、degraded→橙、registering→蓝、offline→灰;未知状态(含空值)回退 offline 样式,徽标文案为 e.status || 'unknown'
类型 / 云厂商文字「类型」「云厂商」列枚举值转中文类型:kubernetes→Kubernetes 集群、vm_cluster→虚拟机集群、bare_metal→裸金属服务器、edge→边缘设备;云厂商:aliyun→阿里云、tencent→腾讯云、aws→AWS、azure→Azure、on_premise→本地 IDC;未识别原样展示

解读「状态」列时要结合第 5.4 章的心跳机制:徽标不是操作者手动设置的结果,而是后端心跳监控按最近上报时间推导出来的。一个刚新建的环境停在 registering(等待首个 Agent 登记);首个 Agent 成功登记并开始心跳后变 online;Agent 失联约 90 秒环境转 degraded、约 300 秒转 offline;Agent 恢复后重新触摸心跳又回到 online。因此看到某个环境「莫名降级/离线」,优先怀疑它的 Agent 停了或网络断了,而不是去环境页改什么配置。列表默认一次性加载全部环境,不提供分页、搜索与排序,环境多时靠 summary 卡片粗看分布。

4.3 新建环境弹窗

弹窗标题「新建环境(项目 #1)」,页头按钮「新建环境」打开。表单提交走 POST /projects/1/environments,成功后提示「环境创建成功(id=N)」,关闭弹窗并刷新列表与 summary。

表单字段表

字段类型必填校验规则默认保存逻辑
名称(name)文本required,前端还校验非空(空则提示「环境名称(name)必填」)作为环境唯一名 trim 后提交;项目内重名后端返回 409 ENVIRONMENT_CONFLICT
显示名(display_name)文本空串按 '' 提交
环境类型(type)下拉选项来自 ENV_TYPES 四枚举kubernetes原样提交;后端校验非法类型返回 400 ENVIRONMENT_INVALID
云厂商(provider)下拉含「未选择」空项空则提交 ''
地域(region)文本无,placeholder 如 cn-hangzhou空则提交 ''
配置(config JSON,可选)textarea填了就必须是合法 JSON非空时 JSON.parse 预解析,解析失败提示「config 不是合法 JSON:{错误信息}」并中止提交;解析成功作为 config 对象提交

底部按钮:「取消」直接关闭弹窗不提交;「创建」提交,提交期间按钮文案变「创建中...」并禁用(全局 busy 同时禁用列表操作列)。注意「配置(config JSON)」的 textarea 位于新建弹窗最下方,placeholder 示例为 {"namespace_allowlist":["prod"]}——config 是环境特定的自由结构 JSON(如 K8s namespace 白名单),后端以 map[string]any 存储、不校验具体键名——namespace_allowlist 只是约定示例,写别的键同样能存。

各输入框的 placeholder 起着命名规范提示的作用:「名称」为「如 prod-k8s-hangzhou」(环境唯一名建议按「用途-形态-地域」组织)、「显示名」为「如 生产 K8s(杭州)」(展示用,可中文)、「地域」为「如 cn-hangzhou」(云厂商 region 编码)。云厂商下拉带一个「未选择」空项,类型下拉无空项且默认 kubernetes,配合前端 required 基本杜绝了把枚举值填错。除名称外所有字段均可留空,留空字段提交空字符串或省略 config——也就是说一个最小可用的环境只需要填 name 并选一个 type。

成功提示里的 id 取自响应 data?.id || data?.environment?.id,兼容列表直出与嵌套两种返回。后端在项目内对 name 判重,重名返回 409 与错误码 ENVIRONMENT_CONFLICT;非法 type 返回 400 ENVIRONMENT_INVALID。创建是「登记即生效」的:不要求该环境此刻真实可连,类型与云厂商只是元数据,真正代表「环境可用」的是后续 Agent 的心跳状态。

4.4 测试连接

控件含义触发后端调用边界与细节
按钮「测试连接」向环境写入一条 health_check 探测任务验证可达性POST /environments/:id/test-connection成功判定读取响应中 success===true / ok===true / connected===true;提示「环境 "{名称}" 连接测试:{message}(延迟 {latency_ms} ms)」,message 缺失时按判定回退「连接成功/连接失败」

实际后端响应为 {environment_id, task},不含 success/latency 等字段。已修复(提交 412e9c48)handleTestConnection 现以「HTTP 2xx 且响应含 task」判定探测已下发,提示改为 环境 "{名称}" 连接测试:{message}(探测任务 #{id},状态 {status},待 Agent 上报结果)EnvironmentsPage.vue:476-486),不再因缺 success 字段而恒提示「连接失败」。真实语义是:后端为环境创建一条 health_check 类型的 Agent 任务(agent_tasks,状态 pending),优先挂到环境内首个 active Agent;若环境还没有 active Agent,则落一条 agent_id=0 的环境级探测任务,等 Agent 上报/心跳后由 Agent 真正执行。所以本按钮本质是「投递探测任务」,任务的真实执行结果在 Spoke 侧,不在本页展示。

4.5 Agents 管理弹窗

行内「Agents」按钮打开弹窗,标题「Agent 管理:{环境名称}」,弹窗分三块。

4.5.1 注册新 Agent 块

控件含义操作效果触发后端调用边界与细节
Agent 名称输入框输入要登记的 Agent 名,placeholder「Agent 名称,如 spoke-hz-001」,支持回车提交空名点按钮提示「请输入 Agent 名称」-名称唯一(后端按环境判重,重名 409)
按钮「注册 Agent」签发一次性 Token 并登记 Agent成功后清空输入框、展示 Token、提示「Agent 注册成功,请立即复制并保存一次性 Token」;若后端没返回明文 Token 则提示「Agent 注册成功(后端未返回明文 Token)」POST /environments/:id/agents,body {name}提交期间按钮文案「注册中...」并禁用(连同下方 Agent 行操作一起禁用)

4.5.2 一次性 Bootstrap Token 块

控件含义边界与细节
Token 代码块黄底区块内深色 pre 展示 Token标题「一次性 Bootstrap Token(仅显示一次,请立即复制保存)」;Token 仅存内存(agentModal.token),关闭弹窗即丢失,后端也不会再返回明文
按钮「复制 Token」写入剪贴板navigator.clipboard,成功提示「Token 已复制到剪贴板」;失败(如非 HTTPS)提示「复制失败,请手动选中 Token 复制」

Token 明文以 envt_ 前缀 + 32 位十六进制随机串生成;后端只落 SHA-256 哈希(token_hash,JSON 序列化隐藏),明文仅创建响应当次返回。字段解析兼容 token / bootstrap_token / bootstrapToken 三种命名。Spoke Agent 引导接入时携带该 Token 走登记流程,此后环境心跳由监控自动维护。

4.5.3 已注册 Agent 列表

标题「已注册 Agent(N)」,列为 ID / 名称 / 类型 / 版本 / 状态 / 创建时间 / 操作。加载中与空态分别提示「加载中...」「暂无 Agent,请先注册。」。列取值:名称取 a.name || a.agent_name || a.id;类型取 a.agent_type || a.type || '-'(当前 env_agents 表无该字段,恒为 -);版本 a.version || '-';状态取 a.status || 'pending'

控件含义触发后端调用边界与细节
按钮「重启」下发 restart 指令POST /agents/:id/restart成功提示「Agent "{名称}" 重启成功({message})」并刷新列表
按钮「升级」下发 upgrade 指令(目标版本缺省 latest)POST /agents/:id/upgrade同上提示「... 升级成功」
按钮「吊销」(危险)吊销 Agent,Token 立即失效POST /agents/:id/revokeconfirm 二次确认:「确定吊销 Agent "{名称}" 吗?吊销后该 Agent 将无法连接。」;吊销不可逆,状态置 revoked

Agent 状态徽标active→绿、pending→灰、disconnected→橙、revoked→红,未知回退灰。状态列取 a.status || 'pending',也就是说后端记录若没有 status 也会按 pending 渲染,而不是按 unknown。

三个生命周期按钮的语义差异要分清:重启与升级是「投递指令」(服务端写一条 agent_tasks,返回 {id, restarting/upgrading: true, task_id},真正执行要等 Spoke Agent 拉取),所以成功提示只代表指令已下发、不代表 Agent 已重启完成;吊销则是「立即生效」的服务端状态变更(返回 {id, revoked: true, status},Token 即刻作废)。吊销按钮是红色危险样式且带 confirm,误点前可以取消;重启/升级是普通描边按钮,点了没有二次确认。三个操作成功后都会刷新一次 Agent 列表,行内状态徽标的变化(如 active→pending 等待重连、active→revoked)要等 Spoke 侧的动作落地后才会体现。

4.6 编辑环境弹窗

行内「编辑」打开小号弹窗「编辑环境 #{id}」,仅四个字段:名称(name)*、显示名(display_name)、云厂商(provider)、地域(region)。提交 PUT /environments/:id,body 为 {name, display_name, region, provider},成功提示「环境 #{id} 更新成功」并刷新列表;提交期间按钮「保存中...」。

重要边界:后端 updateEnvironmentRequest 并无 name 字段——名称不可改是 TAD-05 的设计(环境名在项目内唯一且被引用)。已修复(提交 412e9c48):前端编辑弹窗的名称输入框已置灰禁用、标签明示「名称(name,不可修改)」(EnvironmentsPage.vue:159-160),保存请求体也不含 nameEnvironmentsPage.vue:597),界面与后端语义一致,不再出现「可改却不生效、无提示」的误导。此外环境类型(type)与 config 在编辑弹窗不提供,符合后端仅按出现字段更新的指针语义。

表单校验与更新语义再补两点:名称仍受 required 与前端非空校验约束(空则提示「环境名称(name)必填」并中止提交)。保存请求体不含 type 与 config,后端是「请求里出现哪个字段才更新哪个」的指针式合并,未出现的字段不会被改动也不会被清空;换句话说用本弹窗既不能纠正当初填错的环境类型,也无法清掉旧 config——这两处想改只能删除后重建(注意 4.7 的级联影响)。显示名/地域/云厂商留空时提交 '',会把原值覆盖为空(等同清空),这是编辑与新建在空串语义上一致的体现。

4.7 删除环境

控件含义触发后端调用边界与细节
按钮「删除」(危险)物理删除环境DELETE /environments/:idconfirm 二次确认:「确定删除环境 "{名称}"(#{id})吗?该操作不可恢复。」;成功后提示「环境已删除」并刷新列表与 summary

后端删除为事务内级联清理:删除该环境下全部 Agent、其全部 Agent 任务,以及 agent_id=0 的环境级探测任务(payload.environment_id 匹配),最后物理删除环境。删除是不可恢复操作,会连带清除接入该环境的全部 Agent 记录与任务历史,被删环境曾经注册过的一次性 Token 也随 Agent 记录一并作废。

删除的影响范围要放在产品全貌里评估:本页删除只清理「环境域」自身的数据,不会级联清理其它域对环境的引用——例如「Git 仓库」页的同步目标通过 target_environment_id 指向环境,删除环境后这些同步目标仍会残留(后端删除逻辑只处理 envmgr 域,跨域无外键约束,此结论由代码逻辑推断、未实测)。因此建议删除前先到相关页面解除引用(暂停/删除指向该环境的同步目标),再执行删除,避免留下悬空配置。正在进行的部署若引用该环境同样可能受影响,操作前请确认环境无在用工作负载。删除成功后环境 name 随即释放可被复用,系统不提供软删除或回收站。

4.8 操作结果提示条

页面所有操作结果统一走顶部提示条:alertalert-success(绿)/ alert-error(红)/ alert-info(蓝)区分,右侧「关闭」按钮可收起,新提示覆盖旧提示。错误信息兜底链为 err.response.data.message → err.response.data.error → err.message → '未知错误'。值得一提:提示条的内容是用户判断操作是否成功的唯一即时反馈,除提示外的行内状态变化都依赖自动刷新(见下)。

4.9 数据刷新约定(哪些操作会自动刷新什么)

5. 后端关联

5.1 API 客户端

action/web/src/api/apolloClient.js:axios 实例 baseURL '/apollo-api/v1'timeout 30000。请求拦截器从 getApolloToken()apollo_token(仅此一处,不回退 aip_token)附加 Authorization: Bearer。响应拦截器解包信封:body.code === 0 时把 response.data 替换为 body.data(页面拿到的 data 即业务数据);HTTP 401 清除 token 并跳转 /apollo/login(已在登录页则不重复跳转);HTTP 403 统一 alert('无权限执行该操作');其余直接抛错交给页面 errMsg() 兜底。后端信封成功形态:{code:0, message:"ok", data:..., request_id:...}

5.2 端点表

页面真实触发到的端点(路由前缀 /api/v1,经 Vite /apollo-api 代理到 18082;括号内为 server.go 实际鉴权权限点):

方法路径请求体页面触发点
GET/projects/:pid/environments-加载环境列表(PermEnvRead)
POST/projects/:pid/environments{name, display_name?, type, provider?, region?, config?}新建环境(PermEnvWrite,201)
GET/projects/:pid/environments/summary-加载 summary(PermEnvRead)
PUT/environments/:id{name?, display_name?, provider?, region?}编辑环境(PermEnvWrite)
DELETE/environments/:id-删除环境(PermEnvWrite)
POST/environments/:id/test-connection-测试连接(PermEnvExecute)
POST/environments/:id/agents{name}(兼容 agent_name注册 Agent(server.go 要求 PermEnvExecute;access 权限点清单登记为 PermEnvWrite——后端内部不一致,见第 6 章)
GET/environments/:id/agents-加载 Agent 列表(PermEnvRead)
POST/agents/:id/revoke-吊销 Agent(PermEnvExecute)
POST/agents/:id/restart-重启 Agent(PermEnvExecute)
POST/agents/:id/upgrade{target_version?}(缺省 latest)升级 Agent(PermEnvExecute)
GET/environments/:id-环境详情(后端已提供,本页未直接用,PermEnvRead)
GET/environments/:id/resources-环境资源占位概览(后端提供,本页无 UI 入口,PermEnvRead)
GET/environments/:id/applications-环境应用占位列表(后端提供,本页无 UI 入口,PermEnvRead)
GET/agents/:id-单 Agent 详情(后端已提供,本页未直接用,PermEnvRead)

本页实际用到的就是前 11 行;后 4 行是后端已具备、但当前页面没有对应控件的端点,一并列出以免排查时以为接口不存在。它们的响应结构分别见 env_handlers.go 的 handleGetEnvironment / handleEnvironmentResources / handleEnvironmentApplications / handleGetAgent。

环境与 Agent 归属项目作用域:projectScopeAccessGuard 中间件会对 /projects/:pid/... 资源做成员归属校验(super admin 直通,非 super 须为 project_members 登记成员,否则 403 AUTHZ_ERROR)。页面固定使用 PROJECT_ID = 1(default 项目)。

5.3 响应结构示例

新建环境成功(201,信封解包后的 data)

{
  "id": 7,
  "project_id": 1,
  "name": "prod-k8s-hangzhou",
  "display_name": "生产 K8s(杭州)",
  "type": "kubernetes",
  "provider": "aliyun",
  "region": "cn-hangzhou",
  "agent_id": null,
  "status": "registering",
  "config": null,
  "last_heartbeat_at": null,
  "created_at": "2026-09-07T10:30:00Z",
  "updated_at": "2026-09-07T10:30:00Z"
}

字段说明:id/project_id 主键与归属项目;name 环境唯一名(不可改);status 初始即 registeringagent_id 仅在接入 Agent 后由后端维护;config 为环境特定 JSON(未提供为 null,序列化 omitempty);时间均为 UTC RFC3339。列表接口 GET /projects/1/environments 返回上述对象的数组(env_handlers 直接 ok(c, list),data 即数组,前端 toList 兼容 {items:[...]} 形态)。

注册 Agent 成功(201)

{
  "agent": {
    "id": 21,
    "name": "spoke-hz-001",
    "environment_id": 7,
    "status": "pending",
    "connected_at": null,
    "disconnected_at": null,
    "last_ip": "",
    "metadata": null,
    "version": "",
    "created_at": "2026-09-07T10:35:00Z",
    "updated_at": "2026-09-07T10:35:00Z"
  },
  "bootstrap_token": "envt_9f2c3a...(32 位十六进制,此处截断示意)",
  "note": "bootstrap_token 仅返回一次,请立即下发并妥善保存"
}

字段说明:agent 为登记的 Agent 记录,初始 status: pendingbootstrap_token 为一次性明文,仅本次响应可见,后端只存其 SHA-256 哈希;note 提示尽快下发。POST /agents/:id/revoke 返回 {id, revoked:true, status}restart 返回 {id, restarting:true, task_id}upgrade 返回 {id, upgrading:true, task_id, target_version}

环境 summary(注意嵌套结构)

{
  "project_id": 1,
  "summary": {
    "total": 4,
    "by_type": {"kubernetes": 2, "vm_cluster": 1, "edge": 1},
    "by_status": {"online": 2, "degraded": 1, "registering": 1}
  }
}

后端 EnvironmentSummarytotal / by_type / by_status 三个子结构,包在 summary 键内。已修复(提交 412e9c48):前端 parseSummary() 现递归进 data.summary 并按 by_status(在线/降级/注册中/离线)归一化计数(EnvironmentsPage.vue:363-386),与后端嵌套契约一致,summary 卡片不再恒为全零。测试连接响应为 {environment_id, task}task 即 health_check 的 AgentTask 对象,字段见下)。

测试连接成功响应示例(信封解包后的 data):

{
  "environment_id": 7,
  "task": {
    "id": 88,
    "agent_id": 0,
    "type": "health_check",
    "status": "pending",
    "payload": "{\"environment_id\":7}",
    "priority": 5,
    "created_at": "2026-09-07T11:00:00Z",
    "updated_at": "2026-09-07T11:00:00Z"
  }
}

该示例展示的是环境尚无 active Agent 时的形态:任务落到 agent_id=0 的环境级探测任务上,等 Spoke Agent 上报后才会被真正拉走执行;若环境已有 active Agent,agent_id 会指向该 Agent。priority=5 是 health_check 任务的基础优先级。响应里没有任何 success/connected/latency_ms 字面结论,属后端契约现状;已修复(提交 412e9c48):前端改为按「HTTP 2xx 且响应含 task」判定探测已下发并在提示中展示任务号与状态(EnvironmentsPage.vue:476-486),不再依赖不存在的成功标志位。

列表列与后端字段的对位关系:ID 列取 e.id(等宽字体);名称列取 e.name 加粗;显示名取 e.display_name || '-';类型列取 typeLabel(e.type) 的中文标签;状态列取 statusClass(e.status) 的徽标 + e.status || 'unknown' 文案;地域取 e.region || '-';云厂商取 providerLabel(e.provider);Agent 列取 e.agent_id || '-';最后心跳取 formatTime(e.last_heartbeat_at)。时间字段一律是 UTC RFC3339 字符串,前端把 T 换成空格、去掉 Z、截到秒后再展示,因此看到 2026-09-07 10:35:00 这样的本地化前格式是正常的。

5.4 关键机制

环境状态机。环境状态取值 registering / online / degraded / offline。合法转移(validEnvTransitions):registering → online/degraded/offline;online → degraded/offline;degraded → online/offline;offline → online/registering。新建环境状态固定为 registering;此后状态的推进不由本页直接写状态,而由心跳监控驱动(见下):Agent 持续上报使环境保持 online,心跳超时先降级后离线,Agent 重新登记又回到 online。手动改状态只能通过后端更新接口按转移表校验,非法转移返回 409 INVALID_STATE_TRANSITION——本页未暴露直接改状态的控件。

心跳阈值与监控节奏要记住,排错时直接用得上:envmgr 的 HeartbeatMonitor 以 30 秒为周期扫描(DefaultTickInterval),Agent 心跳默认 30 秒一次;last_heartbeat_at 距今超过 90 秒(DefaultDegradedAfter)环境被置为 degraded,超过 300 秒(DefaultOfflineAfter)置为 offline。也就是说,一个 Agent 停止上报后,环境徽标先等约两三个周期才转降级、再过约三分钟才转离线——不是即时变化,短时间内看不到状态翻转不要误判为功能异常。反过来,Agent 只要恢复一次上报/登记,环境立即被触摸回 online,无需等待周期。

心跳监控与状态推导。envmgr 的 HeartbeatMonitor 按固定周期扫描:Agent 心跳默认每 30s 一次(监控 tick 亦为 30s),last_heartbeat_at 距今超过 90s 判定为失联并把环境置为 degraded,超过 300s 置为 offline;Agent 恢复心跳时 touchEnvironment 刷新环境心跳并置回 online。因此「状态」列反映的是最近心跳新鲜度,与 Spoke Agent 页「模拟上报」联动——在 Agent 页上报即等同于给所属环境打心跳。列表「最后心跳」列即该环境的 last_heartbeat_at

Agent 引导登记闭环。本页「注册 Agent」只完成第一步:签发一次性 Bootstrap Token(envt_ + 32 位 hex,crypto/rand 生成)并把 Agent 登记为 pending。真实 Spoke Agent 携带 Token 调用公开登记接口后,服务层 RegisterAgent 以恒定时间比对 Token 哈希(crypto/subtle 防时序侧信道):未登记或哈希为空时放行(兼容旧 demo 链路);已吊销直接拒绝;不匹配返回 AGENT_TOKEN_INVALID。登记成功后 Agent 置 active、环境心跳被触摸置 online。吊销是安全闭环:revoke 后 Agent 状态置 revoked 并记录断开时间,此后即使携带正确 Token,pull 侧只读放行、report 侧写入一律 401 AUTHZ_ERROR 拒绝,防止已失陷 Agent 继续污染数据与推进编排。

Agent 任务模型。重启/升级/测试连接本质都是投递异步任务:agent_tasks 表记录 type ∈ {deploy, rollback, config_update, health_check, upgrade, restart},状态 pending → sent → running → success/failed/timeout。本页的测试连接写 health_check(无 active Agent 时 agent_id=0 环境级任务)、重启写 restart、升级写 upgrade(携带 target_version)。前端只提示投递成功并刷新列表,任务的真实执行在 Spoke Agent 拉取后发生,不反映在本页状态徽标上。

删除的级联语义DELETE /environments/:id 在事务内:先删该环境全部 Agent 任务 → 删全部 Agent → 删 agent_id=0 且 payload 指向该环境的探测任务 → 物理删环境。删除不可恢复、需二次确认;被删除环境的 name 随即释放,可被复用(无软删除/回收站)。

审计与操作留痕。上述写操作均在 server.go handler 中调用 recordAudit 记录审计事件(ENV_CREATE/ENV_UPDATE/ENV_DELETE/ENV_TEST_CONNECTION/AGENT_REGISTER/AGENT_REVOKE/AGENT_RESTART/AGENT_UPGRADE),可在「审计日志」页检索。

本页在发布闭环中的位置。把页面放进 LightApollo 的整体流程里读会更清楚:Git 仓库登记 → 同步目标把仓库内声明同步成期望状态(draft→active)→ 编排把 active 期望状态发布到目标环境 → 该环境的 Spoke Agent 拉取期望并应用、随后上报实际状态并检测漂移。环境页在这个链条里提供两样东西:其一,可供选择的目标环境——Git 仓库页同步目标的 target_environment_id、部署/漂移页的作用对象,最终都落到本页登记的环境 id 上;其二,Agent 的信任根——每个环境独立签发 Bootstrap Token,Agent 的登记归属、心跳、吊销全部挂在环境之下。这解释了为什么「删除环境」的影响面那么大:它同时拆掉了部署目标落点与一批 Agent 的信任关系。

区分「同步操作」与「投递操作」。本页可点的按钮中,只有删除与吊销是立即改变状态的同步操作;测试连接、重启、升级都只是把意图写成 agent_tasks 里的一行记录(分别对应 health_check / restart / upgrade),再由 Spoke Agent 的下一轮拉取真正执行。要判断这类动作到底成没成,正确姿势是查任务记录与 Agent/环境的后续状态,而不是只看本页的绿色提示条。心跳窗口(默认 90 秒降级、300 秒离线)只作用于后端对 last_heartbeat_at 的判定,页面开着不刷新看不到状态流转,属预期行为。

6. 权限与安全

7. 常见问题与排错

  1. summary 卡片全为 0,但环境列表正常(历史现象,已修复):原因旧版前端 parseSummary 只解析扁平/数组形态,读不到后端嵌套 {project_id, summary:{total,by_type,by_status}} 里的 summary 子对象。已修复(提交 412e9c48)parseSummary 现递归进 data.summary 并按 by_status 归一化(EnvironmentsPage.vue:363-386),卡片计数正常。若仍全零,请确认 summary 接口本身返回成功(fetchSummary 请求异常时仍会静默置零,见 8 章)。
  2. 新建环境提示「config 不是合法 JSON:...」:原因是配置文本框内容不是合法 JSON。处理:按 placeholder 的 {"namespace_allowlist":["prod"]} 格式修正引号与括号,或清空该字段(config 非必填)。
  3. 新建环境提示创建失败且消息含 ENVIRONMENT_CONFLICT:原因是项目内已有同名环境(name 唯一)。处理:换一个环境名(参考 prod-k8s-hangzhou 命名风格)。
  4. 点「测试连接」提示「连接测试:连接测试请求已受理(探测任务 #N,状态 pending,待 Agent 上报结果)」已修复(提交 412e9c48):后端该接口返回 {environment_id, task}(投递 health_check 任务),前端现以「响应含 task」判定探测已下发并展示任务号/状态(EnvironmentsPage.vue:476-486),不再恒提示「连接失败」。任务真实执行结果在 Spoke 侧,本页不展示;需查结果请到 Agent 端或数据库看 agent_tasks
  5. 注册 Agent 提示成功但没看到 Token 块:原因是后端响应缺 token / bootstrap_token / bootstrapToken 全部字段(仅提示「Agent 注册成功(后端未返回明文 Token)」),或 Token 只在前一次注册后展示、本次被覆盖。处理:核对后端 agent 模块是否开启明文 Token 生成;若确已生成,Token 仅本次可见,关闭弹窗即丢失,需重新注册获取。
  6. Agent 重启/升级后状态没变化:原因是重启/升级只是投递 restart/upgrade 任务,Spoke Agent 拉取并执行后才可能变化;且 agent_tasks 的执行结果不显示在本页。处理:到 Spoke Agent 端或数据库查 agent_tasks 记录确认任务执行状态。
  7. 编辑环境弹窗里名称不可改:名称不可改属 TAD-05 设计约束(后端更新请求体不含 name)。已修复(提交 412e9c48):前端编辑弹窗名称输入框已置灰禁用并标注「(name,不可修改)」(EnvironmentsPage.vue:159-160),不再出现「可改却不生效」的误导。若需改名,可先删除再以新名称重建(注意删除的级联影响)。
  8. 顶部提示「加载环境列表失败:...」:原因是请求被拦(401 已跳登录/403 无权限)或后端 18082 未启动/网络异常。处理:按消息内容区分——含认证/权限字样先重新登录或找管理员授 env:read;含网络字样确认后端进程与 Vite 代理。
  9. 新建/保存按钮长时间停在「创建中.../保存中...」:原因是全局 busy 未释放(请求挂起到 30s 超时,或 401 被拦截器静默跳转导致 finally 未执行完毕)。处理:先看是否已被带到登录页;等待超时后按钮应自动恢复,仍不恢复则刷新页面(未提交的操作不会生效)。
  10. 注册同名 Agent 报错:原因是 Agent 名称在同一环境下唯一(重复注册 409)。处理:换名或先吊销旧的再登记新的;吊销后同名可重新登记(名称未做软删占用)。

8. 已知缺陷与边界

缺陷/边界说明位置
a. summary 卡片恒为全零(已修复)已修复(提交 412e9c48):前端 parseSummary 现递归进 data.summary 并按 by_status 归一化计数(EnvironmentsPage.vue:363-386),与后端嵌套 {summary:{...}} 契约一致EnvironmentsPage.vue:363-386;后端 env_handlers handleEnvironmentSummary
b. 编辑弹窗名称可改但不生效(已修复)已修复(提交 412e9c48):编辑弹窗名称输入框置灰禁用 + 标签「(name,不可修改)」,保存请求体不含 name(EnvironmentsPage.vue:159-160/597),界面与后端语义一致;后端 updateEnvironmentRequest/名称不可改(TAD-05)EnvironmentsPage.vue:159-160/597;后端 updateEnvironmentRequest
c. 测试连接提示恒为连接失败(已修复)已修复(提交 412e9c48):前端改按「HTTP 2xx 且响应含 task」判定探测已下发,提示展示任务号与状态(EnvironmentsPage.vue:476-486),不再依赖不存在的 success/latency 标志位;后端仍只返回 {environment_id, task}EnvironmentsPage.vue:476-486;后端 handleTestEnvironmentConnection
环境类型与 config 不可编辑编辑弹窗仅显示名/地域/云厂商;新建后想改类型需删除重建前后端共同约束(TAD-05)
类型列/Agent 列多为 -env_agents 表无 agent_type 字段,Agent「类型」列恒 -;环境行「Agent」列读 agent_id,普通建连流程不维护该字段env_agents 数据模型(前端只读呈现)
resources/applications 无页面入口后端提供 /environments/:id/resources|applications 占位接口,本页无对应 UI 标签后端占位视图(CPU/内存指标待 TAD-08)
summary 加载失败静默summary 请求异常时静默置全零、无错误提示,主列表不受影响fetchSummary catch 分支
项目 id 写死为 1页面与数据均按 default 项目(PROJECT_ID=1),未从 URL/路由读取,多项目场景需改造EnvironmentsPage.vue 常量
状态未知回退灰环境/Agent 未知状态徽标回退 offline 样式,文案显示 unknownSTATUS_META/AGENT_STATUS_META 兜底
无自动刷新/轮询页面仅在挂载与写操作后拉取数据,后端心跳持续改变状态,停留页面会看到滞后视图onMounted 只调一次 fetch
列表无分页/搜索/排序环境全部一次性加载,数量大时页面与交互压力随行数上升fetchEnvironments 全量拉取
删除环境不级联跨域引用指向该环境的 GitOps 同步目标等不会被级联清理(代码逻辑推断,未实测)envmgr Delete 事务仅处理环境域
Agent 无独立「下线」语义状态只有 pending/active/disconnected/revoked,停止 Agent 靠吊销或等心跳超时转 disconnectedenv_agents 状态模型
「最后心跳」不随页面操作即时变化心跳由后端监控维护,页面写操作不直接改 last_heartbeat_at前后端职责划分