The panel now lets the deployer decide which release keys it trusts. The official CI key is built in and always trusted; an own P-256 public key can be added (off by default, the private key is rejected) so manifests signed with it are accepted too. Removing it drops the manifests only it signed. Changes are audited and need update.confirm. Admin UI: System > Trust > "Trusted release keys" with a collapsible guide on generating a key pair and signing a manifest with openssl. Registration now has one path, POST /api/release/offer (no token, signature verified). The admin POST /releases route, the release.publish capability and the ci-release preset are removed.
17 KiB
Backend API 规范(v1)
本文列出 Backend 的全部对外接口。通用规则见 api-guidelines.md;OpenAPI 3.1 文档在运行时由 /openapi/v1.json 提供,构建时同时生成快照 backend/openapi/v1.json。两者冲突时,以 OpenAPI 为准。
1. 分层
IPv4 / IPv6
PATCH /api/admin/agents/{id}新增可选ipv6_host:IPv6 地址或域名;空字符串关闭 IPv6,缺省或 null 不修改。原public_host用于 IPv4。GET /api/public/overview新增可选ip_version=ipv4|ipv6,缺省 IPv4。SSE/api/public/live的同名参数选择 overview 主题的地址族;series 主题仍按线路 id 定位。- IPv4 线路 id 保持
agent~probe,IPv6 为agent~probe~ipv6。series、distribution、heatmap、compare 和管理端测试端点共用这个标识。测试记录与评分叠加独立。 - 线路、时序、测试和连接 DTO 新增
ip_version;管理端 Agent DTO 新增ipv6_host、ipv6_supported;线路详情新增ipv6_available(是否配置了 IPv6 地址)。公开 DTO 不包含任一种 IP 地址。 - API v1 采用新增字段和可选参数,现有 IPv4 请求及历史链接继续有效。前端类型从 OpenAPI 重新生成。
/api/public/* 只读。站点设为公开时匿名可访问,设为私有时需要登录或只读令牌;
public_visible = false 的 Agent 对匿名访客隐藏
/api/admin/* 管理。需要 Cookie 会话(含 CSRF 校验)或带有对应**能力**的 API 令牌;每个端点声明自己需要的一项能力(OpenAPI 里的 `x-moshiro-capability`),完整目录见 `GET /api/admin/capabilities`,设计见 [access.md](access.md)
/api/agent/v1/* 节点专用:注册(使用加入令牌)和 RPC(使用签名认证)
/healthz /metrics /openapi/v1.json /install /install.ps1
资源总览:
/api/public
├─ overview 首屏快照:所有线路的在线状态、最新指标、评分
├─ live SSE 实时推送
├─ lines/{line_id} 线路详情(线路 = Agent 与 Probe 的一对,id 形如 agt_…~prb_…)
├─ lines/{line_id}/series 历史曲线(指标与评分)
├─ nodes about 节点的展示信息(系统、国家、运营商,开启后带版本和 tag);后端的版本信息
├─ compare 多条线路对比
├─ tests/{test_id} 测试结果
└─ catalog/{kinds|plans|metrics|models} 目录(由代码注册表生成)
/api/admin
├─ auth/{login|logout|me}
├─ agents agents/{id} agents/{id}/{approve|disable|enable|takeover}
├─ join-tokens
├─ probes probes/{id} probes/{id}/revoke Probe(用 probe 角色的加入令牌注册、吊销、删除)
├─ links links/{probe_id}/{agent_id} 线路开关:哪些(Probe,Agent)连成线路(自动连接 + 手动选择)
├─ tasks tasks/{id} 线路模板、针对单个 Agent 的覆盖、目标任务
├─ lines/{line_id}/tests 在某条线路(某个 Probe)上发起按需测试;tests/{id}/cancel
├─ raw/chunks raw/chunks/{id} 原始数据检索与下载
├─ build-info 后端和每个节点的版本、tag、提交、构建时间、更新状态
├─ releases nodes/{id}/update CI 签名的发布清单(登记、列表);请求节点更新
├─ events analysis/recompute api-tokens settings users
2. Public API
展示数据的查询规则(时间段、步长、分层、指标分组、列式响应、实时跟随、缓存)以 query.md 为准。本节只列出端点。
2.1 首屏排行
GET /api/public/overview?range=5m&preset=udp,tcp,scores&sort=score.mc&order=desc
- 返回每条线路在该时间段内的精确汇总,以及在线状态、会话信息(源地址变化次数)、最近一次测试的摘要;
range默认5m,也可以写from/to;- 汇总字段的格式见 query.md 第 6 节。
2.2 实时推送(SSE)
GET /api/public/live?topics=nodes,overview:15m:udp,series:agt_…:2s:udp,tcp,test:tst_…(text/event-stream)
| 事件 | 订阅主题 | 频率 | 内容 |
|---|---|---|---|
node |
nodes |
上下线时 | {kind, id, online, at} |
overview |
overview:{range}:{preset} |
约每 10 s | 每条线路的最新汇总 |
series |
series:{line_id}:{step}:{preset}(step ≤ 1m) |
约每 10 s | 新增的桶(列式)和最新的汇总 |
host |
hosts |
Agent 每次上报(约 2 s) | {agent_id, online, host}:Agent 主机资源占用的最新快照(连接时先推一份当前的) |
test |
test:{test_id} |
状态变化时 | {test_id, status, phase, phases} |
- 每 15 秒发送一行注释作为心跳;断线后浏览器会自动重连,服务端先推送当前快照,再推送增量,不补发历史。
- 单个连接最多订阅 32 个主题;每个客户端最多 4 个 SSE 连接。
2.3 时序、对比、分布
| 端点 | 说明 |
|---|---|
GET /api/public/lines/{line_id}/series?range=15m&preset=udp,tcp |
单条线路的时序(列式),附带整个时间段的精确汇总、缺口和事件 |
GET /api/public/compare?lines=a~p1,b~p1,c~p2&metric=loss.down&range=1h |
多条线路的时间轴对齐对比 |
GET /api/public/lines/{line_id}/distribution?metric=rtt&range=15m |
该时间段内的直方图和 CDF |
GET /api/public/lines/{line_id}/heatmap?metric=rtt&range=6h |
时间 × 延迟桶的热力图 |
参数、响应示例和限额见 query.md。
线路详情 GET /api/public/lines/{line_id} 带 data_since:这条线路最早的数据是什么时候(没有数据是 null),前端据此只提供「有数据」的时间范围。展示端的响应里没有 Probe 的版本和来源地址(那些只在管理端)。
2.3.1 Agent 的主机资源占用
| 端点 | 内容 |
|---|---|
GET /api/public/hosts |
每个对外可见的 Agent 的最新快照:{agent_id, name, online, stale, last_seen_at, host};超过 15 s 没有更新 stale 为真(离线,或者 Agent 关了上报) |
GET /api/public/agents/{agent_id}/host?range=1h |
历史曲线(列式,按步长取平均;range 从 1 小时到 30 天):cpu、mem、disk(百分比)、rx_bps、tx_bps |
host 只有汇总的数字:CPU 占用、内存与根分区的已用和总量、网卡累计收发字节与瞬时网速、系统运行时长。没有 CPU 型号、主机名、网卡名,也没有任何 IP——Agent 不采集这些,公开接口当然也不会返回;有测试断言响应里不出现这类字段。实时更新走 SSE 的 hosts 主题,历史每个 Agent 每分钟存一个样本(retention.host_days,默认 7 天)。
2.3.2 节点的展示信息
GET /api/public/nodes:看得见的 Agent 和没被吊销的 Probe 的展示信息,{id, kind, os, os_name, arch, country, isp_key, isp_name}——系统(linux / windows / macos)、「Debian 12」这样的系统名、架构(x64 / arm64…)、国家代码(CN)、运营商类别(mobile / telecom / unicom / broadnet / other)和名字。没有 IP 地址、城市、ASN、经纬度,有测试保证:公开接口的整个响应里不出现 IP 形状的字符串,也没有 ip、city、asn 这些字段。管理员在设置里打开 site.show_build 之后,每项还有 version 和 tag(节点自己上报的程序版本和 tag);关着时这两个字段是 null。
GET /api/public/about:{show_build, backend}。show_build 关着时 backend 是 null;打开后 backend 是 {version, tag, built_at}(后端自己的版本、tag、构建时间,不含提交号)。
- 系统和架构是节点自报的:Agent 和 Probe 都在各自连面板时的
fleet.hello里自报。只有粗粒度的名字,没有主机名、内核版本、CPU 型号。 - 国家和运营商由 Backend 用节点的来源地址向 IP 归属服务查一次(
MoShiro__GeoLookupUrl,缺省https://ip.007995.xyz,留空就不查):Probe 用 Agent 看到的来源 IP,Agent 用它的公开地址。Probe 和 Agent 不访问任何外部服务。 只查公网地址(回环、内网、CGNAT、链路本地一律不发出去);同一个地址一周内只查一次,失败 10 分钟后再试;查到的只留国家代码和运营商,地址只存在 Backend 内部的node_meta.geo_host,不对外。
2.4 测试结果与目录
GET /api/public/tests/{test_id}:包括计划、参数、状态、阶段,metrics与scores,series(例如码率–丢包曲线),以及置信说明caveats。GET /api/public/catalog/kinds:每个 Kind 的积木组合与ParamSpec(默认值和上下限)。GET /api/public/catalog/plans、/metrics(id、单位、标签、说明、better、阈值、来源 Kind、版本)、/presets(指标分组)、/models(评分维度与所需指标)。
2.5 跨域访问
默认前端和 Backend 同源(Cookie 会话)。前端在别的来源时(比如本机跑前端、连线上的 Backend),Backend 设 MoShiro__CorsOrigins(逗号分隔的来源,或 *;默认空 = 不允许)。只对 /api/* 生效,响应带 Access-Control-Allow-Origin,预检请求(OPTIONS)返回 204,允许的头是 Authorization, Content-Type, X-CSRF-Token。不带凭据,Cookie 不会跨域,所以跨域的调用只能用 Authorization: Bearer <API 令牌>:展示端用只读令牌(站点私有时需要),管理端用 admin 范围的令牌(/api/admin/auth/me 对令牌同样可用)。SSE 在跨域时前端用 fetch 读流,因为 EventSource 带不了请求头。
3. Admin API
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/admin/auth/login |
{username, password} → 设置 HttpOnly; Secure; SameSite=Strict 的会话 Cookie,并返回 csrf_token |
| POST | /api/admin/auth/logout |
|
| GET | /api/admin/auth/me |
|
| GET | /api/admin/agents |
全部 Agent(包括 pending/disabled)、版本、本地硬上限、心跳计数、任务同步状态 |
| PATCH | /api/admin/agents/{id} |
name、labels、public_host、ipv6_host、public_visible;public_host_auto / ipv6_host_auto 为 true 时撤销手填、回到自动识别。手填就是手动,Agent 上报的地址不再覆盖;DTO 的 public_host_auto / ipv6_host_auto 表示当前是不是自动 |
| POST | /api/admin/agents/{id}/approve /disable /enable /takeover |
见 enrollment.md |
| DELETE | /api/admin/agents/{id} |
删除;历史数据按保留策略自然过期 |
| GET/POST | /api/admin/join-tokens |
创建时返回令牌明文和安装命令(仅此一次);role 是 agent(默认)或 probe,决定用它注册出来的是哪种节点 |
| DELETE | /api/admin/join-tokens/{id} |
吊销 |
| GET | /api/admin/probes |
列出所有 Probe(含它现在连着哪些 Agent:版本、远端 IP、起始时间);Probe 由 role=probe 的加入令牌注册,密钥由它自己生成 |
| GET/PATCH/DELETE | /api/admin/probes/{id} |
详情、改名、删除(历史数据按保留策略自然过期) |
| POST | /api/admin/probes/{id}/revoke |
吊销:公钥从所有 Agent 的名单里移除,Agent 立即断开它;记录和历史保留 |
| POST | /api/admin/agents/{id}/probes/{probe_id}/disconnect |
让这个 Agent 断开这个 Probe 的会话(Probe 会重连,除非已吊销);没有会话时返回 409 probe_offline |
| GET/POST | /api/admin/tasks |
列表和创建:line 模板(不带 agent_id)、单个 Agent 的覆盖(带 agent_id + base_task_id)、target 任务 |
| GET/PATCH/DELETE | /api/admin/tasks/{id} |
参数按 Kind 的 ParamSpec 校验,并检查带宽预算 |
| POST | /api/admin/lines/{line_id}/tests |
{plan, params?} → 202 + Location: /api/public/tests/{id};Probe 离线时返回 409 probe_offline,节点繁忙时返回 409 test_busy |
| POST | /api/admin/tests/{id}/cancel |
|
| GET | /api/admin/raw/chunks?agent_id=&kind=&from=&to=&test_id= |
原始块元数据 |
| GET | /api/admin/raw/chunks/{chunk_id} |
下载原始块(application/vnd.moshiro.rawchunk);过期返回 410 raw_expired |
| GET | /api/admin/events?agent_id=&kind=&from=&to= |
事件 |
| GET | /api/admin/analysis/recompute |
指标重算进度 |
| GET/POST/DELETE | /api/admin/api-tokens |
创建时勾选能力(capabilities,只能是创建者自己拥有能力的子集,写蕴含读)和可选的有效期(expires_in_days);明文只显示一次 |
| POST | /api/admin/auth/passkey/options、/api/admin/auth/passkey/login |
匿名(限流)。passkey 登录:先取挑战,再提交断言;响应同密码登录 |
| GET/POST/DELETE | /api/admin/auth/passkeys、/auth/passkeys/options、/auth/passkeys/{id} |
已登录用户管理自己的 passkey:列出、取注册挑战、注册、删除(API key 不能用) |
| GET | /api/admin/capabilities |
能力目录:分组、哪些改变信任、哪些是预留、依赖关系和预设(只读、运维、接入、CI 发布、全部) |
| GET/PATCH | /api/admin/settings |
站点公开或私有、是否对访客显示构建信息(site.show_build)、保留期、节点定时重启(fleet.restart_enabled、fleet.restart_hour、fleet.restart_minute、fleet.restart_utc_offset_minutes,默认开、04:30、UTC+8;改完立即通知在线节点)等 |
| GET | /api/admin/build-info |
后端和每个 Agent、Probe 的版本、tag、提交、构建时间、系统,以及节点宿主机更新器报告的更新状态(管理端「系统 → 版本」) |
| GET | /api/admin/releases |
每个组件最新的一份签名发布清单 |
| GET/PUT/DELETE | /api/admin/release-key |
可信资产来源:GET 看官方发布密钥和(如果有)自有发布公钥的指纹;PUT {public_key} 另外信任自己的发布公钥(P-256 的 PEM,拒绝私钥);DELETE 停用它,只有它签过的已登记清单会被丢掉。官方密钥始终信任。PUT/DELETE 需要 update.confirm,写审计 |
| POST | /api/release/offer |
CI 登记发布清单的入口,不需要令牌:{manifest, signature} 的签名必须能用 Backend 内嵌的官方发布公钥或已开启的自有发布公钥验过(ECDSA P-256 / SHA-256,DER),否则 403;seq 必须比已登记的新,否则 409。登记不依赖面板里的任何凭证,清库、重装、还没有 passkey 时照常工作 |
| GET | /api/admin/links |
每一对(Probe,Agent)的线路开关:linked(最终结果)、override(手动设置,null 跟随自动连接)、connected,以及 auto_link |
| PUT | /api/admin/links/{probe_id}/{agent_id} |
{enabled: true|false|null}:手动强制连或不连,null 取消手动设置;改完立即重新下发 |
| POST | /api/admin/nodes/{id}/update |
请求一个在线的 Agent 或 Probe 更新到它收到的清单(节点不在线返回 409) |
| GET/POST/PATCH | /api/admin/users |
首次启动时通过命令行或环境变量创建初始管理员 |
任务的请求体示例:
{ "scope": "line", "kind": "tcp_echo", "schedule": "continuous",
"params": { "rate_hz": 50, "tcp_info_ms": 20, "port": 7444 } }
4. Agent API
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/agent/v1/enroll |
由 MapFleetEnrollment 提供(使用加入令牌认证),见 ../backend/fleet/README.md 的「注册」一节 |
| GET | /api/agent/v1/rpc |
由 MapFleetLink 提供,升级为 WebSocket;MoShiro 的方法见 protocol.md |
5. 运维端点
| 路径 | 认证 | 说明 |
|---|---|---|
GET /healthz |
无 | SQLite 与原始数据目录都可写 |
GET /metrics |
admin 令牌 | Prometheus 格式:在线数、块入库速率、分析队列长度、重算进度、磁盘占用、SSE 连接数 |
GET /openapi/v1.json |
无 | |
GET /install?code=…(也接受 /install.sh)、GET /install.ps1?code=… |
无(凭一次性代码) | 一行安装脚本:Agent 和 Probe 共用同一份,角色、面板地址和镜像由代码决定,用 Docker 或 Podman 起容器;代码不能用就是 404 |
6. 错误码(节选,完整列表见共享的 ErrorCode)
| HTTP | code |
|---|---|
| 400 | validation_failed、budget_exceeded |
| 401 | unauthorized、csrf_failed |
| 403 | forbidden(凭证缺少端点需要的能力)、site_private |
| 404 | agent_not_found、task_not_found、test_not_found、chunk_not_found、unknown_kind、unknown_plan |
| 409 | name_conflict、probe_offline、test_busy、agent_not_active |
| 410 | raw_expired、join_token_expired |
| 422 | range_too_large、invalid_step、unknown_metric、unknown_preset、too_many_metrics |
| 429 | too_many_requests、too_many_streams |
| 503 | storage_unavailable |