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 认证与权限

2.3 端口与 API 前缀

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_idcpumem_mbcpu 为浮点、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_mba.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"
}

字段含义(SpokeAgentserver/models.go):id 主键;agent_name 唯一索引;statusonlinereconcile_statesynced/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.DesiredStateDeclarationapi_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_updatetrue(候选即代表有目标);reconcile_required 见 5.4 调和轮语义。

补充说明:components 内组件的 kind 取值 binary|config|llm_route|eval_set|ontology_yamldepends_on 声明组件间依赖(编排按此排布批次 DAG);policy.rollout.strategy/max_surge/max_concurrent 是部署滚动参数。声明内 digest 与顶层 bundle_digest 一致(顶层来自 desired_states 行,声明内 digest 由 handler 用 ds.Digest 回填);signer_idbundle_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_STATEagent/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 最近部署被置 pendingDesiredStateID 等于本次拉取目标、且 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_statesadvanceStatusOf 映射为编排组件状态(running→readyerror/degraded→failedstopped/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/degradeddegraded;部署 driftingdrifting;部署 pending/deploying 任一组件 stopped/restarting/unknownpending(收敛中/重试窗口);任一组件 degraded/errordegraded;组件全 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. 权限与安全

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(蓝预留)