所属产品: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_agents、environments、agent_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 |
| 路由 name | ApolloEnvironments |
| meta.title | Apollo 环境管理 |
| 侧边栏位置 | 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 认证与权限
- 路由
meta: { title: 'Apollo 环境管理', requiresAuth: true }:未登录访问/apollo/environments会被前端路由守卫拦截并跳转/apollo/login;守卫对/apollo前缀仅认apollo_token(localStorage),2026-09-12 修订后不再回退读取aip_token。 - 后端按权限点鉴权,环境域权限命名
env:read/env:write/env:execute(常量PermEnvRead/PermEnvWrite/PermEnvExecute):
| 权限 | 覆盖操作 |
|---|---|
| PermEnvRead | 环境列表、summary、详情、Agent 列表、Agent 详情、resources/applications 占位接口 |
| PermEnvWrite | 新建环境、编辑、删除 |
| PermEnvExecute | 测试连接、注册 Agent、吊销/重启/升级 Agent |
- 401 无有效 Token:apolloClient 响应拦截器清除本地 token 并跳转登录页(已在登录页则只报错);403 无权限:拦截器统一
alert('无权限执行该操作')。 - 落到实际操作上的差异是:拥有
env:read但缺env:write的用户能正常看到列表与 Agent,但点「新建环境」「编辑」「删除」会被后端 403 拒绝、操作不写库;而缺env:execute的用户点「测试连接」「注册 Agent」及 Agent 的吊销/重启/升级同样被拒。换言之权限不足不会隐藏按钮,而是点下去才弹「无权限执行该操作」。
2.3 端口与 API 前缀
- Apollo 后端端口 18082;Vite 将
/apollo-api前缀代理到 18082。 - 客户端
apolloClient.js:baseURL: '/apollo-api/v1'、timeout 30000 ms、请求拦截器附 Bearer token、响应拦截器解包{code:0,data}信封(见 5.1)。 - 完整请求示例:
GET /apollo-api/v1/projects/1/environments。
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」为空显示 -;「最后心跳」为空或非法时间显示 -(formatTime 把 T 换成空格、去掉 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/revoke | 有 confirm 二次确认:「确定吊销 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),保存请求体也不含 name(EnvironmentsPage.vue:597),界面与后端语义一致,不再出现「可改却不生效、无提示」的误导。此外环境类型(type)与 config 在编辑弹窗不提供,符合后端仅按出现字段更新的指针语义。
表单校验与更新语义再补两点:名称仍受 required 与前端非空校验约束(空则提示「环境名称(name)必填」并中止提交)。保存请求体不含 type 与 config,后端是「请求里出现哪个字段才更新哪个」的指针式合并,未出现的字段不会被改动也不会被清空;换句话说用本弹窗既不能纠正当初填错的环境类型,也无法清掉旧 config——这两处想改只能删除后重建(注意 4.7 的级联影响)。显示名/地域/云厂商留空时提交 '',会把原值覆盖为空(等同清空),这是编辑与新建在空串语义上一致的体现。
4.7 删除环境
| 控件 | 含义 | 触发后端调用 | 边界与细节 |
|---|---|---|---|
| 按钮「删除」(危险) | 物理删除环境 | DELETE /environments/:id | 有 confirm 二次确认:「确定删除环境 "{名称}"(#{id})吗?该操作不可恢复。」;成功后提示「环境已删除」并刷新列表与 summary |
后端删除为事务内级联清理:删除该环境下全部 Agent、其全部 Agent 任务,以及 agent_id=0 的环境级探测任务(payload.environment_id 匹配),最后物理删除环境。删除是不可恢复操作,会连带清除接入该环境的全部 Agent 记录与任务历史,被删环境曾经注册过的一次性 Token 也随 Agent 记录一并作废。
删除的影响范围要放在产品全貌里评估:本页删除只清理「环境域」自身的数据,不会级联清理其它域对环境的引用——例如「Git 仓库」页的同步目标通过 target_environment_id 指向环境,删除环境后这些同步目标仍会残留(后端删除逻辑只处理 envmgr 域,跨域无外键约束,此结论由代码逻辑推断、未实测)。因此建议删除前先到相关页面解除引用(暂停/删除指向该环境的同步目标),再执行删除,避免留下悬空配置。正在进行的部署若引用该环境同样可能受影响,操作前请确认环境无在用工作负载。删除成功后环境 name 随即释放可被复用,系统不提供软删除或回收站。
4.8 操作结果提示条
页面所有操作结果统一走顶部提示条:alert 以 alert-success(绿)/ alert-error(红)/ alert-info(蓝)区分,右侧「关闭」按钮可收起,新提示覆盖旧提示。错误信息兜底链为 err.response.data.message → err.response.data.error → err.message → '未知错误'。值得一提:提示条的内容是用户判断操作是否成功的唯一即时反馈,除提示外的行内状态变化都依赖自动刷新(见下)。
4.9 数据刷新约定(哪些操作会自动刷新什么)
- 页面挂载时并行拉取一次环境列表(
fetchEnvironments)与 summary(fetchSummary),此后没有自动轮询或定时刷新;后端心跳监控在持续改变环境状态,因此长时间停留的页面看到的状态会滞后,需要手动刷新浏览器获取最新视图。 - 新建环境成功:刷新环境列表 + summary;删除环境成功:刷新环境列表 + summary。
- 编辑环境成功:只刷新环境列表(编辑不改状态与计数,summary 无需更新)。
- 注册 Agent 成功、以及重启/升级/吊销成功后:只刷新当前弹窗内的 Agent 列表;关闭弹窗再次打开会重新拉取,环境列表的「最后心跳」不会因 Agent 操作立即变化。
- 任何刷新失败只在顶部提示条给出错误,不打断当前视图(旧数据继续显示)。
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 初始即 registering;agent_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: pending;bootstrap_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}
}
}
后端 EnvironmentSummary 含 total / 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. 权限与安全
- 认证分层:页面路由
requiresAuth;列表/summary/Agent 列表走 protected 组 +PermEnvRead;新建/编辑/删除走PermEnvWrite;测试连接与 Agent 注册/吊销/重启/升级走PermEnvExecute。 - 后端不一致提示:
POST /environments/:id/agents在 server.go 实际要求PermEnvExecute,而access/permissions.go的 ProtectedRoutes 清单登记为PermEnvWrite——鉴权以 server.go 实际执行为准,文档清单待同步(后端内部差异,非本页缺陷)。 - Bootstrap Token 安全设计:明文仅创建当次返回、只存 SHA-256 哈希、比对用恒定时间算法;本页 Token 块仅在内存(
agentModal.token),关闭弹窗即不可再取。吊销即 Token 失效,report 写入侧 401 拒绝形成安全闭环。 - 删除防护:删除环境/吊销 Agent 均有
confirm二次确认;页面无批量操作,写操作单一明确,失败不影响其它功能。 - 页面本身不承载用户/角色管理:能操作哪些环境动作取决于当前登录用户在「用户管理」「角色管理」页被授予的角色与权限点;环境域暂无环境级 RLS/密级,隔离粒度是「项目」(当前恒为项目 1)加权限点两层。
- config 属于环境敏感信息但页面只读后不再回显:新建后列表不展示 config 内容,编辑也不提供查看入口;若需核对 config,走后端接口或数据库。
7. 常见问题与排错
- 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 章)。 - 新建环境提示「config 不是合法 JSON:...」:原因是配置文本框内容不是合法 JSON。处理:按 placeholder 的
{"namespace_allowlist":["prod"]}格式修正引号与括号,或清空该字段(config 非必填)。 - 新建环境提示创建失败且消息含
ENVIRONMENT_CONFLICT:原因是项目内已有同名环境(name唯一)。处理:换一个环境名(参考prod-k8s-hangzhou命名风格)。 - 点「测试连接」提示「连接测试:连接测试请求已受理(探测任务 #N,状态 pending,待 Agent 上报结果)」:已修复(提交 412e9c48):后端该接口返回
{environment_id, task}(投递 health_check 任务),前端现以「响应含task」判定探测已下发并展示任务号/状态(EnvironmentsPage.vue:476-486),不再恒提示「连接失败」。任务真实执行结果在 Spoke 侧,本页不展示;需查结果请到 Agent 端或数据库看agent_tasks。 - 注册 Agent 提示成功但没看到 Token 块:原因是后端响应缺
token / bootstrap_token / bootstrapToken全部字段(仅提示「Agent 注册成功(后端未返回明文 Token)」),或 Token 只在前一次注册后展示、本次被覆盖。处理:核对后端 agent 模块是否开启明文 Token 生成;若确已生成,Token 仅本次可见,关闭弹窗即丢失,需重新注册获取。 - Agent 重启/升级后状态没变化:原因是重启/升级只是投递
restart/upgrade任务,Spoke Agent 拉取并执行后才可能变化;且agent_tasks的执行结果不显示在本页。处理:到 Spoke Agent 端或数据库查agent_tasks记录确认任务执行状态。 - 编辑环境弹窗里名称不可改:名称不可改属 TAD-05 设计约束(后端更新请求体不含
name)。已修复(提交 412e9c48):前端编辑弹窗名称输入框已置灰禁用并标注「(name,不可修改)」(EnvironmentsPage.vue:159-160),不再出现「可改却不生效」的误导。若需改名,可先删除再以新名称重建(注意删除的级联影响)。 - 顶部提示「加载环境列表失败:...」:原因是请求被拦(401 已跳登录/403 无权限)或后端 18082 未启动/网络异常。处理:按消息内容区分——含认证/权限字样先重新登录或找管理员授
env:read;含网络字样确认后端进程与 Vite 代理。 - 新建/保存按钮长时间停在「创建中.../保存中...」:原因是全局 busy 未释放(请求挂起到 30s 超时,或 401 被拦截器静默跳转导致 finally 未执行完毕)。处理:先看是否已被带到登录页;等待超时后按钮应自动恢复,仍不恢复则刷新页面(未提交的操作不会生效)。
- 注册同名 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 样式,文案显示 unknown | STATUS_META/AGENT_STATUS_META 兜底 |
| 无自动刷新/轮询 | 页面仅在挂载与写操作后拉取数据,后端心跳持续改变状态,停留页面会看到滞后视图 | onMounted 只调一次 fetch |
| 列表无分页/搜索/排序 | 环境全部一次性加载,数量大时页面与交互压力随行数上升 | fetchEnvironments 全量拉取 |
| 删除环境不级联跨域引用 | 指向该环境的 GitOps 同步目标等不会被级联清理(代码逻辑推断,未实测) | envmgr Delete 事务仅处理环境域 |
| Agent 无独立「下线」语义 | 状态只有 pending/active/disconnected/revoked,停止 Agent 靠吊销或等心跳超时转 disconnected | env_agents 状态模型 |
| 「最后心跳」不随页面操作即时变化 | 心跳由后端监控维护,页面写操作不直接改 last_heartbeat_at | 前后端职责划分 |