1. 页面概览
1.1 是什么
「Spoke Agent」页(对应前端源码 action/web/src/views/AgentPage.vue,页面标题「Apollo Spoke Agent」)是 LightApollo 的 Agent 在线状态与 Pull 链路演示台。Spoke Agent 是部署在实际节点上的轻量代理,周期性「Pull 期望状态」并「上报实际状态快照」,本页把这一闭环的两端都搬到浏览器里:上半部的「模拟 Agent」控制台让你输入任意 Agent 名称即模拟一次 Pull(拉取期望状态声明 + bundle digest)或一次上报(幂等登记/更新 spoke_agents 表并触发编排推进),下半部的「Agent 在线列表」集中呈现所有已登记 Agent 的状态、心跳、调和状态与实际状态快照。页面不启动任何真实进程,只做协议层面的语义模拟,真实 Agent 的接入走同一套公开端点自行实现 Poller 即可。
「纯 Pull」是理解本页的关键:真实 Spoke Agent 的执行循环是「周期 Poll(GET /agent/pull)→ 比对 digest / has_update 决定是否重新应用 → 应用后上报(POST /agent/report)实际状态」。本页模拟的正是这个循环的「拉」与「报」两拍——「模拟 Pull」回答"后端会让我拉什么",「模拟上报」回答"后端如何登记我现在的实际状态";中间"重新应用期望声明"在真实节点上由本地 reconciler 与进程管理器完成,页面不做也不模拟。因此后端会把上报的 component_states 当作可能的编排推进信号:对编排器来说,每次上报都可能是某轮部署中组件陆续就绪的真实事件,据此放行下一批(详见 4.4 与 5.4)。
在整个 Apollo 产品的流程中,本页位于「期望状态 / 渠道 → Agent 拉取 → 实际状态回连」链路的关键位置上:期望状态的创建、激活与审批在相邻页面完成,部署的发起与漂移收敛在「部署与漂移」页完成,而本页是唯一能从 Agent 视角看到「后端到底会让我拉取什么、上报之后后端如何回应」的观察窗口——Pull 的目标选择、审批拦截、调和轮触发(reconcile_required)都在 GET /agent/pull 一次调用里体现;上报则把 component_states 转换为编排推进输入,与「部署与漂移」页的「推进」走同一套 Advance 语义。输入是用户填写的 Agent 名称与可选的实际状态 JSON;输出是一条期望状态声明 + bundle digest(Pull)或一个 {agent_name, accepted:true} 确认(上报),以及下方列表中的 Agent 行。
典型使用链路(演示视角的完整闭环):① 先在相邻页创建并激活一条期望状态(确认 approval_status 已过审,关联 bundle 后 Digest 有值);② 本页输入 Agent 名称(如 app-001)点「模拟 Pull」,结果区块展示拉取到的期望状态声明 JSON 与 bundle_digest 摘要,顶部提示 has_update=true;③ 点「模拟上报」,列表立即出现该 Agent(状态 online、调和状态按上报内容聚合);④ 若此时该 Agent 在「部署与漂移」页正有进行中部署,上报的 component_states 会转成编排推进输入、驱动部署批次放行;⑤ 观察列表中该 Agent 的最近心跳(last_seen)、调和状态、资源与「查看快照」里的实际状态 JSON,反复 Pull/上报可看到状态按上报内容如实变化。若想进一步观察「调和轮」,可在该 Agent 处于漂移(drifting)或部署被 auto_fix/手动 sync 置 pending 时再点一次「模拟 Pull」——后端会返回 reconcile_required=true 并把部署原子转回 deploying,期望 Agent 无条件重应用一次;页面不直接显示该标志(见 4.2 与 8 章),需结合「部署与漂移」页的部署状态变化判断。
1.2 核心价值/能力表
| 能力 | 说明 | 对应页面操作 |
|---|---|---|
| 模拟 Pull | 一键拉取 Agent 应执行的期望状态声明 + bundle digest,观察 has_update / reconcile_required 标志 | 「模拟 Pull」按钮 |
| 模拟上报 | 幂等 upsert spoke_agents(实际状态快照 + 资源 + 心跳),有进行中部署时触发编排推进 | 「模拟上报」按钮 |
| Agent 在线列表 | 集中查看各 Agent 的状态、调和状态、最近心跳、当前期望版本、内存/CPU 资源 | 页下部「Agent 在线列表」卡片 |
| 快照下钻 | 行内「查看快照」弹窗展示该 Agent 上报的实际状态 JSON(空显示 {}) | 实际状态列「查看快照」 |
| 公开端点 | Pull/Report 无需登录即可调用,支持 ?token=/Bearer 出向鉴权,供真实 Agent 直接对接 | 直接 HTTP 调用 |
| 出向鉴权复现 | 已登记 token_hash 的环境,Pull/Report 携带错误/缺失 token 可复现 401,验证"登记必验"语义 | 对公开端点带 ?token= 调用 |
| 环境心跳联动 | 上报同步 Touch env_agents/environments 心跳,与「环境管理」页 Agent 在线状态互通 | 「模拟上报」 |
1.3 一句话总结
本页把「Agent 拉取期望状态、上报实际状态」的闭环用「模拟 Agent」控制台 + 在线列表呈现出来,是观察 Spoke 协议行为与 Agent 在线状态的演示窗口。
2. 访问入口
2.1 路由与菜单
| 项 | 值 |
|---|---|
| 路由 path | /apollo/agents |
| 路由 name | ApolloAgents |
| meta.title | Apollo Spoke Agent |
| 侧边栏入口 | ApolloLayout 侧边栏「Spoke Agent」,位于「制品与渠道」之后、分组「平台管理」之前 |
| 前端源码 | action/web/src/views/AgentPage.vue |
| API 客户端 | action/web/src/api/apolloClient.js |
| 路由注册 | action/web/src/router/index.js,/apollo 子路由 children 下,component: AgentPage,挂 ApolloLayout |
相邻页(侧边栏顺序):制品与渠道 bundles.md(前)、用户管理 admin-users.md(后,平台管理组第一项)、部署与漂移 deployments.md(部署推进的组件状态可在本页模拟上报)、环境管理 environments.md(Agent 在 env_agents 登记 token_hash / 吊销 / 心跳)。
2.2 认证与权限
- 路由
requiresAuth: true:未登录访问被前端路由守卫拦到/apollo/login(登录态为独立apollo_token,向后兼容回退读取旧aip_token)。 - 列表接口受保护:
GET /agents在 protected 组,需登录且具备PermAgentRead(agent:read)权限点;403 时 apolloClient 统一alert('无权限执行该操作')。 - Pull/Report 为公开端点:
GET /agent/pull、POST /agent/report注册在 api 组、不经access.Authn中间件,未登录也能调用;但支持可选出向鉴权(?token=或Authorization: Bearer)——Agent 未登记 env_agents 时放行(兼容旧 demo Agent),登记过token_hash的环境则必须携带正确 token,否则 401 拒绝(report 还会记录「疑似伪造上报」告警日志)。 - 前端 401(apollo_token 失效)由响应拦截器清除 token 并跳转
/apollo/login。 - 公开端点在页面内的实际路径:页面经 apolloClient 调用时 URL 为
/apollo-api/v1/agent/pull,Vite rewrite 后后端收到/api/v1/agent/pull;因为请求拦截器总会附带登录 token,浏览器里的 Pull/Report 实际总是带 Bearer 的——这不影响公开访问,token 仅在后端按 Agent 登记情况做可选校验。脚本直调则直接访问 18082 的/api/v1/agent/...路径即可(无需 apollo_token)。
2.3 端口与 API 前缀
- Apollo 后端端口 18082。Vite 开发代理把
/apollo-api前缀 rewrite 为/api转发(后端路由注册在/api/v1),客户端 baseURL/apollo-api/v1。 - 统一响应 envelope
{code,message,data,request_id}:拦截器在code===0时把response.data解包为业务数据,页面const { data } = await apiClient.get(...)直接拿业务数据。 - 注意:
/agents走 envelope,而公开端点 Pull/Report 的成功响应不套 envelope(裸对象);404(NO_ACTIVE_DESIRED_STATE)也是裸{code:"NO_ACTIVE_DESIRED_STATE", error:...}(code 为字符串,拦截器判非 envelope 原样放行),页面从error.response.data.error取 message 展示。
3. 界面布局
┌──────────────────────────────────────────────────────────────┐
│ Apollo Spoke Agent │
│ [操作结果提示条(v-if alert.message,含「关闭」)] │
├──────────────────────────────────────────────────────────────┤
│ ┌ 模拟 Agent(纯 Pull 演示)卡片 ─────────────────────────────┐ │
│ │ [Agent 名称输入,placeholder「Agent 名称,如 app-001」] │ │
│ │ [模拟 Pull] [模拟上报](空名称时两者禁用) │ │
│ │ 上报实际状态快照(actual_snapshot JSON)[textarea] │ │
│ │ 组件状态(component_states JSON 数组,选填)[textarea] │ │
│ │ ┌ Pull 结果区块(v-if pullResult)───────────────────────┐ │ │
│ │ │ 应用/版本/期望状态名/bundle_digest 元信息行 │ │
│ │ │ 期望状态声明 JSON(code-block) │ │
│ │ └───────────────────────────────────────────────────────┘ │ │
│ │ ┌ 上报结果区块(v-if reportResult)──────────────────────┐ │ │
│ │ │ JSON(code-block) │ │
│ │ └───────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────────┤
│ ┌ Agent 在线列表(GET /agents)卡片 ──────────────────────────┐ │
│ │ 空态:暂无 Agent,请在上方输入名称并点击"模拟上报"登记。 │ │
│ │ 表格:ID | Agent 名称 | 状态 | 调和状态 | 最近心跳 | │ │
│ │ 当前期望版本 | 资源 | 实际状态(查看快照) │ │
│ └────────────────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────────┤
│ ┌ 实际状态快照弹窗(modal,@click.self 可关)────────────────┐ │
│ │ {{agent}} 实际状态快照 [关闭] │ │
│ │ 快照 JSON(code-block,空显示 {}) │ │
│ └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
各板块职责:
| 板块 | 职责 |
|---|---|
| 页头 + 提示条 | 标题「Apollo Spoke Agent」、全局操作结果提示(info/success/error 三型,可「关闭」) |
| 模拟 Agent 控制台卡片 | 收集 Agent 名称与两个可选 JSON,触发模拟 Pull / 模拟上报,就地展示结果 |
| Pull 结果区块 | 展示期望状态声明元信息(应用/版本/状态名)+ bundle_digest 摘要 + 完整声明 JSON |
| 上报结果区块 | 展示后端返回的 {agent_name, accepted} JSON |
| Agent 在线列表卡片 | 全量 spoke_agents 表格(空态/表格两态),行内「查看快照」 |
| 实际状态快照弹窗 | 行内下钻展示该 Agent 的 actual_state JSON |
4. 交互元素
4.1 页头、操作结果提示条
| 控件 | 位置 | 含义 | 必填/默认/可用条件 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|---|---|---|
| 页面标题 | 页头 h2 | 页面身份标识 | 常驻 | Apollo Spoke Agent | 无 | 与源码 h2 逐字一致 |
| 操作结果提示条 | 页头下方 | 展示最近一次操作结果 | 有 alert.message 才显示;类型默认 alert-info | 成功用 success、失败用 error 显示消息 | 无 | 单槽提示:新操作结果覆盖旧提示,无历史 |
| 提示条「关闭」 | 提示条右侧 | 清空当前提示 | 提示条可见时可用 | alert.message='' 立即消失 | 无 | 纯本地状态,不触发请求 |
页面用一组 ref 维护状态:agentName(名称输入)、busy(请求互斥位,模拟 Pull 与模拟上报共用——任一进行中两者同时禁用)、pullResult/reportResult(两个结果区块的数据,互斥显示)、reportForm.actual_snapshot / reportForm.component_states(两个 textarea 的原始文本)、alert(提示条,单槽覆盖)、snapshotModal(快照弹窗)。注意 busy 只约束「模拟 Pull/模拟上报」两个按钮,行内「查看快照」不受影响。
页面所有关键路径的提示文案在 4.2~4.7 中逐字列出。
4.2 「模拟 Pull」按钮
| 控件 | 位置 | 含义 | 必填/默认/可用条件 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|---|---|---|
| 「模拟 Pull」 | 模拟 Agent 控制台(btn-primary) | 模拟一次 Spoke Pull,拉取应执行的期望状态 | agentName 非空且 busy=false 才可用(:disabled="busy || !agentName");进行中按钮保持禁用 | 成功后结果区块填充期望状态 JSON、顶部提示 Agent "{name}" 拉取到期望状态:{app}(has_update={has_update})(success);失败则提示 Pull 失败:{msg}(若无 active 期望状态,后端返回 404 NO_ACTIVE_DESIRED_STATE)(error)并把结果区块清空 | GET /agent/pull?agent={encodeURIComponent(agentName)} | 发起前会清空上一次 reportResult;不刷新下方 Agent 列表(Pull 本身不写库)。has_update 后端恒返回 true(见 5.4),提示仅为透传 |
后端行为速览(详见 5.4):无「active 且已过审」期望状态 → HTTP 404,body {"code":"NO_ACTIVE_DESIRED_STATE","error":"no active desired state for agent"}(页面据此提示);有候选 → HTTP 200 裸对象 {debug_consume_err, desired_state, bundle_digest, has_update:true, reconcile_required}。agent 查询参数 trim 后为空 → 400(agent 查询参数不能为空)。
调和轮的可见性边界:当该 Agent 最近部署处于待调和状态(pending 由 auto_fix 自动调和或手动 sync 产生)且期望状态未变时,Pull 会返回 reconcile_required=true,并把部署原子转移 pending→deploying(一次性语义,WHERE status=pending 保证不会每轮重复)。但页面结果区块只渲染 desired_state 声明 JSON,不在界面单独展示 reconcile_required 与 debug_consume_err——要观察完整响应可用 curl 直调 /api/v1/agent/pull?agent=app-001,或在「部署与漂移」页看该部署状态是否从 drifting/pending 收敛回 synced。
另外 has_update 目前后端恒返回 true:只要选到了拉取目标就视为有更新,真正决定"要不要重应用"的是声明 digest 是否变化与调和轮标志;对纯演示页面来说,提示条里透传该值即可。
一次典型 Pull 会话的可预期输出:在已激活期望状态 web-app-v2(stable 渠道指向它、审批已过审、含 web/db 两个组件)的前提下,对 app-001 执行「模拟 Pull」,结果区块应看到 期望状态名:web-app-v2、应用:demo-web、版本:1.2.0 与 16 位截断的 bundle_digest,声明 JSON 含完整 components;若该 Agent 最近部署正被 auto_fix/手动 sync 置为待调和,响应同时含 reconcile_required=true(页面不单独展示,见上)。声明或渠道配置有变时重新 Pull 即可看到最新声明,无需刷新整页。
4.3 「模拟上报」按钮
| 控件 | 位置 | 含义 | 必填/默认/可用条件 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|---|---|---|
| 「模拟上报」 | 模拟 Agent 控制台(btn-outline) | 模拟一次 Spoke 上报,幂等登记/更新该 Agent | agentName 非空且 busy=false;两个 textarea 均留空也允许(只登记心跳) | 成功后结果区块展示 {agent_name, accepted:true}、提示 上报成功:agent="{agent_name}" accepted={accepted}(success)并自动刷新 Agent 列表;失败提示 上报失败:{msg}(error) | POST /agent/report(body 见 4.4) | 发起前清空上一次 pullResult。JSON 解析异常发生在 JSON.parse,SyntaxError 会被 catch 兜到「上报失败」提示。上报不删除任何历史数据(幂等 upsert) |
上报会触发三件事(详见 5.4):① 幂等 upsert spoke_agents;② 该 Agent 有进行中部署时把 component_states 转成编排推进(Advance);③ 更新 env_agents/environments 心跳(Agent 未登记时后端 NOT_FOUND 属正常,不报错)。
上报即心跳:只要「模拟上报」通过出向校验,spoke_agents 行的 last_seen 就刷新为当前 UTC 时间,env_agents/environments 心跳也同步 Touch(未登记环境时后端 NOT_FOUND,视为旧链路正常)。因此「最近心跳」列完全由上报驱动;若只想"让某 Agent 显得在线",把两个 JSON 都留空、只带 agent_name 上报即可,列表也会随之刷新。
4.4 上报字段输入区(两个 textarea)
「模拟上报」下方是两行可选输入,控制台渲染成 form-group,均有 form-label 标签:
| 字段/控件 | 位置 | 含义 | 必填/默认/可用条件 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|---|---|---|
| 上报实际状态快照(actual_snapshot JSON) | 按钮行下方 textarea rows=4 | 该 Agent 当前实际状态的对象 | 选填;留空则不携带该字段(后端 ActualSnapshot 为 nil,落库快照为空) | 非空且在「模拟上报」提交前被 JSON.parse 为对象随 body 发送 | 经 report body 字段 actual_snapshot 传递 | placeholder 示例 {"web":{"status":"running","digest":"abc123"},"db":{"status":"running"}};必须为 JSON 对象(数组语法会解析成功但语义是快照结构异常,后端只做透传存储,不做 schema 校验) |
| 组件状态(component_states JSON 数组,选填) | actual_snapshot 下方 textarea rows=3 | 组件级运行状态,参与调和状态聚合与编排推进 | 选填;留空则不携带 | 非空且被 JSON.parse 为数组随 body 发送;驱动 reconcile_state 聚合与部署批次推进 | 经 report body 字段 component_states 传递 | placeholder 示例 [{"name":"web","status":"running","readiness":{"ready":true}}]。status 枚举 running|stopped|restarting|degraded|error|unknown,经 advanceStatusOf 映射后参与编排(见 5.4) |
提交 body(源码 handleReport 组装,reportForm.actual_snapshot / reportForm.component_states 均 trim 后非空才 parse 进 payload):
{
"agent_name": "app-001",
"actual_snapshot": { "web": { "status": "running", "digest": "abc123" }, "db": { "status": "running" } },
"component_states": [ { "name": "web", "status": "running", "readiness": { "ready": true } } ]
}
component_states 数组元素结构(agent.ComponentReport,源码 JSON 标签):name(组件名)、status(进程状态枚举,见上)、readiness(选填对象,透传就绪信息)、error(选填,透传错误描述)。后端还接受顶层可选 deployment_id、cpu、mem_mb(cpu 为浮点、mem_mb 为整数 MB),页面当前不提供这三个字段的输入(cpu/mem 恒为缺省,列表中资源列显示 -)。
上报如何驱动部署状态机(与「部署与漂移」页「推进」是同一条 Advance 链路):后端把上报的每个组件状态映射为编排语义——running→ready(组件就绪、可放行下一批)、error/degraded→failed(批次失败触发失败策略/回滚)、stopped/restarting/unknown→deploying(仍在收敛)。演示例子:在「部署与漂移」页为 app-001 发起部署后,回本页第一次上报 [{"name":"web","status":"running"}],web 被判 ready、next_batch 放行下一批;若把 db 组件上报为 {"status":"degraded"},聚合出的调和状态立即变 degraded 且推进失败。这是用上报字段直接影响部署状态机的最直观路径。
readiness 与 error 字段的去向:上报组件里的 readiness(就绪对象)与 error(错误描述)会原样随 ComponentReport 进入编排推进上下文(hub.ComponentReport),供批次就绪判定与失败原因展示使用;而 reconcile_state 聚合本身只看 status 枚举,不看 readiness/error 内容。也就是说可以把 readiness 写成 {"ready":true,"port":8080} 这类形态验证透传,但它不影响本页显示——本页只渲染 actual_state 整体 JSON,「查看快照」弹窗里能看到的是上报快照而非编排组件详情。
4.5 Pull 结果区块
「模拟 Pull」成功(pullResult 非空)后在控制台内渲染 .result-block:
| 元素 | 内容与规则 |
|---|---|
| 区块标题 | Pull 结果(期望状态声明 + bundle digest) |
元信息行(.pull-meta) | 逐字格式:应用:{{desired_state.app || '-'}} · 版本:{{desired_state.version || '-'}} · 期望状态名:{{desired_state.name || '-'}} · bundle_digest:{{shortDigest(...)}};其中 bundle_digest 用等宽 <code class="cell-code"> 展示,shortDigest 超过 16 字符截前 16 位加 …,空则 - |
| 声明 JSON | JSON.stringify(pullResult.desired_state, null, 2) 深色 code block 全文展示 |
该区块为纯展示,不触发任何后端调用;点下一次「模拟 Pull」/「模拟上报」会整体替换(发起动作前分别置空 reportResult/pullResult)。
4.6 上报结果区块
「模拟上报」成功(reportResult 非空)后同样渲染 .result-block:标题 上报结果,下方 JSON.stringify(reportResult, null, 2) 深色 code block 展示后端返回(envelope 解包后即 { "agent_name": "app-001", "accepted": true })。纯展示、可被下次 Pull/上报替换。
4.7 Agent 在线列表卡片
卡片标题固定 Agent 在线列表(GET /agents)。agents.length===0 显示空态文案 暂无 Agent,请在上方输入名称并点击"模拟上报"登记。;有数据渲染表格,列头依次 ID / Agent 名称 / 状态 / 调和状态 / 最近心跳 / 当前期望版本 / 资源 / 实际状态。列表在页面挂载(onMounted)与每次「模拟上报」成功后拉取,无手动刷新按钮、无自动轮询。
列表按 spoke_agents 主键升序返回全量行;同一 agent_name 重复上报只覆盖六列(status/reconcile_state/actual_state/mem_mb/cpu/last_seen),不会新增行。行的 actual_state 正是「模拟上报」填入的快照对象——它是「查看快照」弹窗的内容来源,也是「部署与漂移」页漂移检测(drift check)比对「实际 vs 期望」的材料;做漂移演示时建议把快照写成与期望声明组件结构一致的形态(组件键 + status/version 等字段),diff 才能落在组件/字段级。
| 控件/列 | 位置 | 含义 | 必填/默认/可用条件 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|---|---|---|
| ID / Agent 名称 | 前两列 | 行主键与名称(加粗) | - | 纯展示 | 无 | 名称在 spoke_agents 唯一,重复上报不会产生新行 |
| 状态徽标 | 状态列 | Agent 在线状态 | class agent-{status} | online 绿 / offline 灰,未知灰 | 无 | 后端 report 恒定写 status:"online",当前无任何代码把 spoke_agents 翻 offline(env_agents 的 offline/吊销状态不写回本表)——看到灰色徽标需等待后端新增离线翻转逻辑,见 8 章 |
| 调和状态徽标 | 调和状态列 | 组件/部署状态聚合 | 取 a.reconcile_state || 'synced',class rs-{reconcile_state||synced} | synced 绿、pending 蓝、drifting 橙、degraded 红、reconciling 蓝(未知灰) | 无 | 页面预置 5 色,但当前后端 reconcileStateOf 只产出 synced/pending/drifting/degraded 四种(reconciling 仅预留),见 5.4 |
| 最近心跳 | 心跳列 | 上次上报时间 | formatTime(a.last_seen) | 后端 RFC3339 时间字符串去掉 T、截前 19 位(如 2026-09-07 05:00:00);空则 - | 无 | 展示的是 UTC 串不转本地时区;只读列 |
| 当前期望版本 | 版本列 | Agent 期望版本 | 取 a.version || a.actual_state?.version || '-' | 纯展示 | 无 | report 端点当前不写 version 字段,故通常回落 actual_state.version(需上报快照里带 version 键),否则显示 - |
| 资源 | 资源列 | 内存/CPU | 取 a.mem_mb、a.cpu | 显示 {mem_mb} MB / {cpu.toFixed(2)} core;任一字段缺失/为 0 显示 - | 无 | 页面不提交 mem/cpu,故资源列通常为 - / - |
| 「查看快照」 | 实际状态列(btn-sm btn-outline) | 打开快照弹窗 | 始终可点(busy 不约束) | 见 4.8 | 无(数据已在列表中) | 空快照显示 {} |
4.8 实际状态快照弹窗
| 控件 | 位置 | 含义 | 必填/默认/可用条件 | 操作效果 | 触发后端调用 | 边界与细节 |
|---|---|---|---|---|---|---|
| 「查看快照」 | 列表实际状态列 | 打开快照弹窗 | 始终可点 | showActualState(a) 置弹窗可见,content = a.actual_state || {} | 无 | 复用列表中已带的行数据,不额外请求 |
| 弹窗标题 | 弹窗头 | 标识该 Agent 快照 | - | {{agent_name}} 实际状态快照 | 无 | 与源码 modal-header 文案一致 |
| 快照正文 | 弹窗 body | 展示快照 JSON | - | JSON.stringify(snapshotModal.content, null, 2) 深色 code block | 无 | 空则输出 {} |
| 「关闭」/ 遮罩 | 弹窗 | 关闭弹窗 | 始终可用 | snapshotModal.visible=false | 无 | 点遮罩 @click.self 同样可关 |
快照数据取自列表行本身(showActualState(a) 直接把 a.actual_state 放入弹窗),因此弹窗不会发新请求;若上报未携带快照,actual_state 缺省,弹窗显示 {}。由于上报成功后列表会立即刷新,弹窗里看到的快照始终是最近一次上报的值——旧快照在每次新上报时被覆盖,弹窗内看到的与实际列表行一致。
5. 后端关联
5.1 API 客户端
action/web/src/api/apolloClient.js:axios 实例 baseURL /apollo-api/v1、timeout 30000ms、请求拦截器对所有请求附加 Authorization: Bearer <token>(token 取 apollo_token,无则回退 aip_token)。响应拦截器:HTTP 2xx 且 code===0 时把 response.data 解包为业务数据(页面 const { data } = ... 直接得到业务对象/数组);HTTP 2xx 但 code!==0 拒绝;4xx/5xx 提取 body.message || body.error 并把 message 别名写入 error.response.data.error 后 reject——401 清 token 并跳 /apollo/login(登录页自身 401 不跳,避免死循环),403 alert('无权限执行该操作')。非 envelope 响应(无数字 code,如公开 Pull/Report 的裸响应与 404)原样放行。页面各 catch 统一以 err.response?.data?.error || err.message 展示后端错误。
5.2 端点表
| 方法 | 路径 | 权限/认证 | 请求体/Query | 页面触发点 |
|---|---|---|---|---|
| GET | /agents | protected,PermAgentRead(agent:read) | - | 加载 Agent 在线列表 |
| GET | /agent/pull | 公开(可选出向 token) | Query agent(必填)+ 可选 ?token= | 「模拟 Pull」 |
| POST | /agent/report | 公开(可选出向 token,登记必验) | body AgentStatusReport(见 4.4) | 「模拟上报」 |
注册位置:action/products/apollo/server/server.go——pull/report 在公开 api 组(705-706 行)、/agents 在 protected 组(789 行)。真实 Spoke Agent 或脚本直接访问 /api/v1/agent/pull?agent=xx 与 /api/v1/agent/report 即可(经网关/vite 时为 /apollo-api/v1/... 前缀)。
5.3 响应结构示例
GET /agents(envelope 解包后为数组,单项即 SpokeAgent 行):
{
"id": 5,
"agent_name": "app-001",
"status": "online",
"reconcile_state": "synced",
"actual_state": { "web": { "status": "running", "digest": "abc123" } },
"mem_mb": 0,
"cpu": 0,
"last_seen": "2026-09-07T05:00:00.123Z",
"created_at": "2026-09-07T04:59:00+08:00",
"updated_at": "2026-09-07T05:00:00.123Z"
}
字段含义(SpokeAgent,server/models.go):id 主键;agent_name 唯一索引;status 恒 online;reconcile_state 为 synced/pending/drifting/degraded 聚合值;actual_state 上报的实际状态快照 JSON(omitempty,空行不出现);mem_mb/cpu 资源(omitempty,页面不提交故常缺省);last_seen 最近上报时间;environment/hostname/version 为可选字段(omitempty,report 端点当前不写,行内一般不出现)。
GET /agent/pull(200,裸对象非 envelope):
{
"debug_consume_err": "",
"desired_state": {
"api_version": "v1",
"name": "web-app-v2",
"app": "demo-web",
"version": "1.2.0",
"bundle_version": 3,
"digest": "sha256:7f2a...",
"signer_id": "sg_001",
"target": "app-001",
"channel": "stable",
"components": [
{ "name": "web", "kind": "binary", "version": "1.2.0", "digest": "a1b2...", "depends_on": [] },
{ "name": "db", "kind": "config", "version": "v3", "digest": "c3d4..." }
],
"policy": { "rollout": { "strategy": "rolling", "max_surge": 1 } }
},
"bundle_digest": "7f2a...",
"has_update": true,
"reconcile_required": false
}
字段说明:debug_consume_err 调和轮消费的调试字段(正常为空串;pending→deploying 转移失败时带错误或 NOT_CONSUMED);desired_state 为回填了 name/digest/signer_id/bundle_version 的期望状态声明(hub.DesiredStateDeclaration:api_version/name/app/version/bundle_version/digest/signer_id/signature/target/channel/components[]/policy{},组件含 name/kind/version/digest/source/depends_on 等);bundle_digest 关联 bundle 内容 digest(= desired_states.digest,作为 G7 连接键);has_update 恒 true(候选即代表有目标);reconcile_required 见 5.4 调和轮语义。
补充说明:components 内组件的 kind 取值 binary|config|llm_route|eval_set|ontology_yaml,depends_on 声明组件间依赖(编排按此排布批次 DAG);policy.rollout.strategy/max_surge/max_concurrent 是部署滚动参数。声明内 digest 与顶层 bundle_digest 一致(顶层来自 desired_states 行,声明内 digest 由 handler 用 ds.Digest 回填);signer_id、bundle_version 同样由后端从 desired_states 行回填(与声明 YAML/激活两处口径统一),因此页面上看到的声明 JSON 总是"落库增强后"的形态。
GET /agent/pull(404,裸对象非 envelope):
{ "code": "NO_ACTIVE_DESIRED_STATE", "error": "no active desired state for agent" }
POST /agent/report(200,envelope 解包后):
{ "agent_name": "app-001", "accepted": true }
POST /agent/report(401,登记过 token_hash 但 token 缺失/错误):HTTP 401,body {code:"AUTHZ_ERROR", error:"agent token 缺失"/"agent token 校验失败"};吊销 Agent 上报被拒则 error 为 agent 已吊销,拒绝上报。前端经 error.response.data.error 取 message 后提示 上报失败:...。
Envelope 与裸响应的差异落在哪:/agents 与 report 走统一 ok()/fail() envelope;pull 的成功与 404 是 handler 直接 c.JSON 的裸对象。由于拦截器对「无数字 code」的响应原样放行、对 code===0 的 envelope 解包 response.data,三条路径经 axios 后最终都落在页面 const { data } = await ... 的 data 上——这正是 Pull/Report 虽形态不同却能共用同一取值方式的原因,也是 curl 直调时看响应记得区分 envelope 与否的要点。
错误码速查:pull 无候选走裸 404 NO_ACTIVE_DESIRED_STATE;agent/agent_name 空为 400 参数错误(agent 查询参数不能为空 / agent_name 不能为空);Pull/Report 的 401 用业务码 AUTHZ_ERROR(非 envelope 数字码);/agents 失败按 envelope 规则(40101 认证 / 40301 权限)。
5.4 关键机制
Pull 目标选择优先级(pullTargetForAgent)。依次尝试三路候选,任一路返回「active 且已过审」即命中:① Agent 最近部署记录对应的期望状态(GetLatestForAgent 后按 DesiredStateID 取);② stable 渠道的当前期望状态(chRepo.GetByName("stable") 后取 CurrentDesiredStateID,只认 stable 不认其它渠道);③ 第一个 active 期望状态(ListActive 顺序遍历兜底 demo 场景)。approvedForPull 判定 status=active && IsApprovedForDeploy()——即 审批拦截:approval_status 非空且非 approved(pending/rejected)的期望状态一律不进候选,即使它 status 是 active 或 Agent 最近部署指向它,也回退下一候选,保证 require_approval 通道的未过审期望状态不会被 Spoke 应用。全部不命中返回 404。
调和轮(reconcile_required)与一次性消费。若 Agent 最近部署被置 pending 且 DesiredStateID 等于本次拉取目标、且 AutoFixCount>0 || SyncRequestedAt!=nil(即该 pending 来自 auto_fix 自动调和或手动 sync,而非新建部署的瞬时 pending),Pull 返回 reconcile_required=true 并在同请求内执行 ConsumeSyncRequest:单条原子 UPDATE pending→deploying + 清除 sync 指令戳,WHERE status=pending 保证一次性语义——即使期望状态 digest 未变也强制 Agent 下轮重新应用使实际收敛,同时避免每轮 poll 重复重调和。消费失败仅 debug_consume_err 带值并 logger.Warn,不阻断返回。
上报三件事(handleAgentReport)。① 幂等 upsert spoke_agents:按 agent_name 冲突更新 status/reconcile_state/actual_state/mem_mb/cpu/last_seen 六列(clauseUpsertAgent),status 恒写 online、last_seen 写当前 UTC;② 编排推进:若该 Agent 有进行中部署(pending/deploying),把 component_states 经 advanceStatusOf 映射为编排组件状态(running→ready、error/degraded→failed、stopped/restarting/unknown→deploying)后调 orch.Advance,与「部署与漂移」页的「推进」同一套 Advance 门控——失败的部署(非 pending/deploying)或超时置 failed 后回连上报不推进(幂等);③ 心跳:envHeartbeat.Touch(agent, clientIP) 更新 env_agents/environments 心跳,Agent 未登记返回 NOT_FOUND 属旧链路正常(仅 logger.Warn 不阻断)。
编排推进的幂等与失败路径:buildAdvanceFromReport 只在最近部署为 pending/deploying 时构造 Advance,若部署已失败/超时(非进行中)返回 nil——回连上报不会推进已终态部署。Advance 调用本身失败仅 logger.Warn,上报仍返回 200 accepted,偶发编排失败不阻塞 Agent 心跳登记;纠错应在「部署与漂移」页对部署重发 sync 或重新发起,而不是依赖再次上报。
调和状态聚合(reconcileStateOf)。聚合最近部署状态 + 上报组件状态,取值集合复用既有枚举不再新增:部署 failed/degraded → degraded;部署 drifting → drifting;部署 pending/deploying 或任一组件 stopped/restarting/unknown → pending(收敛中/重试窗口);任一组件 degraded/error → degraded;组件全 running 且无失败/漂移/进行中部署 → synced。deployStatus 为空(无部署记录)时仅按组件聚合。拉取失败重试场景由部署侧收敛:Agent 长时间拉取失败 → 部署超时置 failed(F7)→ 回连上报聚合为 degraded。
调和状态聚合示例速查:上报组件 [web running, db degraded] 且无部署 → degraded;上报 [web stopped] 且部署 deploying → pending;部署 failed 且无组件 → degraded;部署 synced 且组件全 running → synced;部署 drifting → drifting(不看组件)。该枚举由 reconcile_state.go 固定(复用部署状态枚举、不新增取值),若未来扩展取值需同步页面徽标色映射。
出向鉴权(可选 token,登记必验)。Pull/Report 均接受 ?token= 或 Authorization: Bearer(后者仅在 query 无 token 时读):token 为空、或 Agent 未登记 env_agents(无 token_hash)→ 跳过(兼容旧 demo Agent/旧链路);登记过且 token_hash 非空 → envmgr.VerifyAgentToken 恒定时间比对 SHA256(token)==token_hash,不匹配返回 401。Pull 为引导式(token 空即跳过,且吊销 Agent 的 pull 仍返回期望状态——只读不敏感);Report 为写入端点登记必验:已吊销 Agent 直接 401 agent 已吊销,拒绝上报(吊销即上报立即失效,防止失陷 Agent 继续污染 spoke_agents/干扰编排收敛),缺失或错误 token 401 且记录 Spoke report 鉴权失败(疑似伪造上报) 告警,不写库、不推进、不心跳。
上报快照是漂移检测的输入:spoke_agents.actual_state 落的是上报快照 JSON 的透传存储(hub.ToJSON),不校验结构、不比对期望声明——比对发生在「部署与漂移」页的漂移检测,对期望声明与上报实际状态做字段级 diff。若期望状态关联的同步目标 drift_policy=auto_fix,后端检出漂移会置部署 pending 等待重调和,下一次 Pull 即命中调和轮(见上文)。这条链路把「本页上报快照」与「漂移页检测事件」串成完整演示闭环。
6. 权限与安全
- 认证分层:列表接口 protected(
PermAgentRead),Pull/Report 公开但支持可选出向鉴权;未登记 Agent 放行是向后兼容设计,生产部署应在 env_agents 登记 token_hash 并让真实 Agent 带 token 拉取/上报。 - 吊销即熔断:env_agents 中被吊销的 Agent 上报被 401 硬拒(即使 token 哈希正确),Pull 只读仍放行——写入端从严、读取端从宽的边界清晰。
- 写操作防护:页面唯一写操作是模拟上报(幂等 upsert,无删除/改状态入口);上报失败不影响其它功能;反复上报只覆盖快照与心跳,不产生重复行。
- 错误透出:Pull/Report 均为公开端点,错误响应不套 envelope、不携带敏感信息;列表 403 由拦截器统一 alert。
- 审计覆盖范围:公开 Pull/Report 不写审计日志(拉取读取与 Agent 心跳上报属高频流量,不计入控制面审计);登录态用户在相邻页做的登记/吊销/心跳配置等控制面操作才有审计记录——别在审计页找不到本页的模拟上报而疑惑。
7. 常见问题与排错
以下问题均由 AgentPage.vue 的 catch 分支提示、后端 handler/状态机代码与既有测试观察归纳,均可按步骤复现。
1. 点「模拟 Pull」提示 Pull 失败:... NO_ACTIVE_DESIRED_STATE:原因是没有任何「active 且已过审」的期望状态可拉取——未创建、未激活、或处于 pending/rejected 审批态(approval_status 未过审),渠道/兜底候选也都不命中。处理:先在期望状态管理页创建并激活一条期望状态、确认其审批已通过后重试;若确认有 active 状态仍 404,检查是否在 require_approval 通道下处于 pending/rejected。
2. 上报成功提示 accepted=true,但下方列表没有新 Agent:原因通常是列表刷新失败或上报被 401 拒绝(该名称已登记过 token_hash 但请求未带正确 token,后端不写库)。处理:查看提示是否含鉴权类错误;成功后页面会自动 fetchAgents(),仍缺失时到后端查 spoke_agents 表确认是否真的写入。
3. 调和状态徽标显示 degraded/drifting:原因是聚合到了受损/漂移状态——组件级 degraded/error、部署失败或漂移未收敛、或部署超时置 failed 后回连上报。处理:点「查看快照」比对实际状态与期望声明的差异;到「部署与漂移」页对该 Agent 发起 sync 或等待 auto_fix 调和收敛,收敛后下次上报会聚合回 synced。
4. 上报提示 JSON 解析错误(非 5xx 文案):原因是 actual_snapshot 或 component_states 文本框内容不是合法 JSON,JSON.parse 抛 SyntaxError 被兜底成「上报失败」。处理:按 placeholder 格式填写(对象 / 对象数组),核对引号与括号;留空可跳过(不携带该字段)。
5. Agent 名称只填了空格也能点按钮,提交报参数为空:原因是前端仅用 !agentName 判空(" " 为 truthy 不禁用),后端 trim 后 400。处理:名称前后不要留空格;这是前端判空未 trim 的边界(见 8 章),修复前按 agent 查询参数不能为空 / agent_name 不能为空 提示自查。
6. 列表「状态」列恒为 online、Agent 掉线不消失:原因是后端 report 恒定写 status:"online",当前无任何逻辑把 spoke_agents 翻 offline(env_agents 的断开/吊销状态不写回本表),且本页列表无手动刷新与轮询。处理:以「最近心跳」列是否陈旧判断 Agent 是否掉线;需要即时离线状态时到「环境管理」页看 env_agents 状态。
7. 上报后「当前期望版本」显示 -:原因是该列取 version || actual_state.version || '-',而 report 端点不写 version 字段——只能靠上报快照里带 version 键,由 actual_state.version 兜底读取。处理:属展示字段边界(见 8 章),不影响 Pull/上报功能;要看期望以「模拟 Pull」结果为准。
8. 想观察「漂移后自动调和」怎么配合:先在「部署与漂移」页/期望状态侧把该 Agent 置于 drifted(或依赖 drift_policy=auto_fix 自动置 pending),回到本页对同名 Agent 点「模拟 Pull」——命中调和轮时响应含 reconcile_required=true 并把部署原子转回 deploying(页面不显示该字段,见 8 章)。处理:以「部署与漂移」页该部署状态在 Pull 前后从 drifting/pending 收敛回 synced 判断调和已发生;若期望声明本身有更新(digest 变),Pull 的 desired_state 会直接换成新声明。
9. 脚本/curl 直调公开端点怎么带 token:Pull 例 GET http://127.0.0.1:18082/api/v1/agent/pull?agent=app-001&token=xxx,或用 Authorization: Bearer;Report 同理。未登记 env_agents 的名称免 token(demo/旧链路兼容);已登记 token_hash 的名称缺 token 或 token 错 → 401 AUTHZ_ERROR,Report 侧不写库、不推进、不心跳。
10. 上报时没填组件状态,调和状态会是什么:组件数组缺省时 reconcileStateOf 只按最近部署聚合——无部署或部署已 synced → synced;部署进行中 → pending;部署失败/漂移 → degraded/drifting。因此"上报空组件"适合做纯心跳登记,不会把已收敛的 Agent 误标为受损。
8. 已知缺陷与边界
| 缺陷/边界 | 说明 |
|---|---|
| 在线列表无刷新/轮询 | fetchAgents 只在 onMounted 与上报成功后触发;Agent 后续状态变化不会自动反映,也无「刷新」按钮——用户需再次「模拟上报」才能重拉列表。web/src 可修:加手动刷新按钮或定时轮询 |
| reconcile_required 页面不展示 | Pull 响应的 reconcile_required / debug_consume_err 未渲染进结果区块(只显示 desired_state JSON),用户无法在页面上直接看到调和轮标志,只能从「部署与漂移」页部署状态变化间接判断。web/src 可修:Pull 结果区块补 has_update / reconcile_required 元信息行 |
| 组件上报缺 UI 占位 | 后端 AgentStatusReport 支持顶层 deployment_id/cpu/mem_mb,页面未暴露输入,资源列恒 - / -(与「版本/资源列恒空」同源) |
| 「状态」列恒 online | 后端 report 恒写 status:"online",无代码把 spoke_agents 翻 offline;offline 徽标样式与 env 侧离线判定存在但不对应本表。列表「状态」语义弱于「最近心跳」列 |
| 版本/资源列恒空 | report 端点不写 version、页面不提交 mem_mb/cpu;「当前期望版本」靠 actual_state.version 兜底、资源列恒 - / -,为展示字段缺口(后端 AgentStatusReport 有 cpu/mem 字段但页面未暴露输入) |
| 前端判空未 trim | !agentName 不拦截纯空格名称,靠后端 400 兜底(提示「agent 查询参数不能为空 / agent_name 不能为空」) |
| JSON 校验缺 schema | actual_snapshot 必须手写合法对象、component_states 合法数组;前端只 JSON.parse 不做结构校验,后端只透传存储;填错结构(如快照传数组)不报错但语义异常 |
| 纯 Pull 演示 | 页面仅模拟请求语义,不真正启动 Agent 进程、不执行部署;真实接入需按 pull/report 协议自实现 Poller(参考 agent/ 包) |
| 时间展示为 UTC | 最近心跳把 RFC3339 UTC 串截断展示,不转浏览器本地时区,跨时区用户看到的「心跳时间」偏差为本地时差 |
| reconcile 五色/四值 | 页面预置 reconciling 蓝色徽标与 CSS,但后端 reconcileStateOf 只产出 synced/pending/drifting/degraded,reconciling 为预留值不会出现 |
| 公开端点依赖安全配置 | Pull/Report 无 token 也可用(未登记环境放行);页面模拟上报是写入公开端点,对外暴露时需确认 env_agents 已登记 token_hash 并启用出向鉴权 |
附录 A:速查——页面文案与关键常量清单
| 场景 | 源码取值/文案(逐字) |
|---|---|
| 页面标题 | Apollo Spoke Agent |
| 控制台卡片标题 | 模拟 Agent(纯 Pull 演示) |
| 列表卡片标题 | Agent 在线列表(GET /agents) |
| Agent 名称输入 placeholder | Agent 名称,如 app-001 |
| 按钮 | 模拟 Pull / 模拟上报 / 查看快照 / 关闭 |
| 快照标签 | 上报实际状态快照(actual_snapshot JSON) / 组件状态(component_states JSON 数组,选填) |
| 快照 placeholder | {"web":{"status":"running","digest":"abc123"},"db":{"status":"running"}} |
| 组件 placeholder | [{"name":"web","status":"running","readiness":{"ready":true}}] |
| Pull 结果标题 | Pull 结果(期望状态声明 + bundle digest) |
| Pull 元信息前缀 | 应用: / 版本: / 期望状态名: / bundle_digest: |
| 上报结果标题 | 上报结果 |
| 列表空态 | 暂无 Agent,请在上方输入名称并点击"模拟上报"登记。 |
| Pull 成功提示 | Agent "{name}" 拉取到期望状态:{app}(has_update={has_update}) |
| Pull 失败提示 | Pull 失败:{msg}(若无 active 期望状态,后端返回 404 NO_ACTIVE_DESIRED_STATE) |
| Pull 参数空(后端) | agent 查询参数不能为空 |
| 上报参数空(后端) | agent_name 不能为空 |
| 上报成功提示 | 上报成功:agent="{agent_name}" accepted={accepted} |
| 上报失败提示 | 上报失败:{msg} |
| 列表加载失败提示 | 加载 Agent 列表失败:{msg} |
| 快照弹窗标题 | {{agent}} 实际状态快照 |
| 组件状态枚举 | running | stopped | restarting | degraded | error | unknown |
| 调和状态取值 | synced(绿)pending(蓝)drifting(橙)degraded(红)reconciling(蓝预留) |