docs: reorganize into design, security, api, ops and dev; drop stale designs; refresh the diagrams [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful

- Group docs by category with an index (docs/README.md); fix every link,
  code comment and the protocol conformance test that read docs/protocol.md.
- access.md and security.md rewritten to what is implemented: removed the
  unadopted member-proof chain, roles and recovery key that do not exist,
  the old implementation order and the budgets; gaps now carry a status.
- enrollment, deployment, api: the install flow is the static, hash-checked
  script with environment variables; no default admin.
- README and architecture: new topology and update-flow diagrams (operator
  passkey, CI signing, registry feed, auth and trust modules).
This commit is contained in:
justme committed 2026-10-08 19:37:56 +08:00
1 parent 636a784043
commit e014a19554
45 files changed
+592 -966

No files matched your search

+141
View File
@@ -0,0 +1,141 @@
# API 设计约束
本文是所有 Backend REST 端点都必须遵守的**规则**;具体端点见 [api.md](api.md)。规则参考了 [Microsoft Azure REST API Guidelines](https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md) 和 RFC 9457,并根据本项目做了取舍。内部协议(Probe↔Agent、Agent↔Backend)的兼容规则见 [protocol.md](../design/protocol.md) 第 4 节。
## 1. 契约生成链路
```text
Minimal API 端点(TypedResults 返回类型 + 显式元数据)
│ dotnet build(Microsoft.Extensions.ApiDescription.Server)
▼
backend/openapi/v1.json(OpenAPI 3.1,提交到仓库)
│ CI:git diff --exit-code,未提交的变更会导致失败
▼
web:openapi-typescript 生成类型,调用方的类型错误会直接暴露
```
- 端点**必须**声明精确的返回类型,例如 `Results<Ok<AgentDto>, NotFound<ProblemDetails>>`,不能返回 `IResult` 或匿名对象,否则 OpenAPI 文档会不准确。
- 每个端点都必须设置 `WithName("agents_list")`(即 operationId,snake_case、`资源_动作`)、`WithSummary`、`WithTags`。
- 请求和响应 DTO 放在 `MoShiro.Backend.Api/Contracts/`,并注册到 Backend 的 JSON 源生成上下文。**不要直接暴露 `MoShiro.Protocol` 中的线协议类型**,因为 REST 与线协议的演进节奏不同。
- **结构强类型,指标弱类型**:资源结构(Agent、任务、测试等)必须是强类型 DTO;而**指标值和评分**统一使用字典 `metrics: { "<metric_id>": number }`、`scores: { "<model_id>": number }`,键的含义、单位、版本由 `/api/public/catalog/*` 给出。这样新增指标或模型不需要改 OpenAPI,也不需要改前端类型。
## 2. 版本与兼容性
- 当前版本为 v1,路径前缀为 `/api/public`、`/api/admin`、`/api/agent/v1`。出现破坏性变更时,新增 `/api/v2/public`、`/api/v2/admin` 等路径。v1 内只允许**非破坏性变更**:
| 非破坏性变更(v1 内允许) | 破坏性变更(需要新建 v2) |
| --- | --- |
| 新增端点 | 删除或重命名端点、字段 |
| 响应中新增字段 | 修改字段类型、单位或语义 |
| 新增可选的请求字段或查询参数 | 可选字段改为必填 |
| 枚举新增值 | 删除枚举值 |
| 放宽校验 | 收紧校验、修改默认值 |
| 新增错误码 | 修改已有错误码对应的 HTTP 状态 |
- 客户端必须忽略未知字段,也必须能处理未知的枚举值。
- 需要 v2 时,v1 和 v2 至少并存一个发布周期;OpenAPI 文档按版本分文件(`v1.json`、`v2.json`)。
## 3. URL 与方法
- **按调用方分层**:`/api/public`(只读展示)、`/api/admin`(管理)、`/api/agent`(节点)。同一个资源在不同层中可以有不同的 DTO,例如 public 层不暴露 `pubkey`、`local_limits`。
- 路径全部小写,资源名用复数名词,多个单词用 `-` 连接:`/api/admin/join-tokens`。
- 嵌套最多一层;子资源一旦有全局唯一 ID,就提供顶层路径(`/probes/{probe_id}`)。
- 查询参数和 JSON 字段使用 snake_case。
| 操作 | 方法 | 成功状态 | 说明 |
| --- | --- | --- | --- |
| 列表 | GET 集合 | 200 | 分页 |
| 读取 | GET 单个 | 200 | |
| 创建 | POST 集合 | 201 + `Location` | 响应体返回完整资源 |
| 部分更新 | PATCH | 200 | `application/merge-patch+json`(RFC 7396) |
| 删除 | DELETE | 204 | 资源不存在时同样返回 204(幂等) |
| 非 CRUD 动作 | POST `/{resource}/{id}/{verb}` | 200 或 204 | 例如 `revoke`、`rotate-token` |
- GET 请求不能产生副作用。PUT 不使用:配置类资源都通过 PATCH 修改。
## 4. 数据表示
| 类别 | 规则 | 示例 |
| --- | --- | --- |
| 时间点 | RFC 3339 UTC,以 `Z` 结尾,精确到毫秒。**唯一例外**:列式时序响应中的时间轴数组 `t` 使用 Unix 毫秒(见 [query.md](../design/query.md)) | `2026-09-27T10:00:00.000Z` |
| 时长 | 整数 + 单位后缀 | `interval_ms: 1000` |
| 延迟 | 带小数的毫秒值(指标字典中的单位见目录) | `rtt_p95_ms: 58.93` |
| 百分比 | 0–100 的数字 + `_pct` | `loss_pct: 0.5` |
| 计数 | 整数,不使用浮点数 | `attempts: 600` |
| ID | 带类型前缀的 ULID 字符串 | `prb_01J9Z...` |
| 枚举 | snake_case 字符串 | `"udp_echo"` |
| 缺失值 | 字段始终存在;未知值写 `null`,不省略字段 | `"last_seen_at": null` |
| 布尔值 | 使用 `is_`/`has_` 前缀,或者用意义明确的形容词 | `online`、`enabled` |
## 5. 错误
所有 4xx/5xx 响应都使用 `application/problem+json`(RFC 9457),并附加扩展字段:
```json
{
"type": "about:blank",
"title": "Validation failed",
"status": 400,
"code": "validation_failed",
"detail": "interval_ms must be >= 200",
"trace_id": "00-4bf92f...-01",
"errors": { "interval_ms": ["must be >= 200"] }
}
```
- `code` 取自共享的 `ErrorCode` 枚举,是客户端做判断的**唯一**依据;`title`/`detail` 只供人阅读,内容可能变化。
- `errors` 只在 400 `validation_failed` 中出现,键名是 JSON 字段路径。
- 5xx 响应不能泄露异常信息或调用栈,只提供 `trace_id`,用于在日志中定位问题。
| 状态码 | 使用场景 |
| --- | --- |
| 400 | 请求格式错误或校验失败 |
| 401 | 未认证 |
| 404 | 资源不存在 |
| 403 | 已认证但作用域不足 |
| 409 | 名称冲突、状态冲突(例如 `probe_offline`) |
| 410 | 资源曾经存在但已过保留期(例如 `raw_expired`) |
| 412 | `If-Match` 不匹配 |
| 413 | 请求体超过上限 |
| 422 | 语义上无法满足(例如 `range_too_large`) |
| 429 | 超过限流,需要带 `Retry-After` |
| 503 | 存储不可用,或正在关闭 |
## 6. 集合与查询
- 分页由服务端驱动:`?limit=`(默认 50,最大 500)和 `?cursor=`(不透明字符串);响应格式为 `{ "items": [...], "next_cursor": string | null }`。排序必须稳定(按 ID 或时间 + ID)。
- 时间范围参数统一使用 `from`/`to`,区间为左闭右开 `[from, to)`。缺省时 `to = now`,`from = to − 1h`。
- 所有查询都有上限(点数、时间跨度、结果条数);超出时返回 422 并在 `detail` 中给出可行的替代方案,不能悄悄截断。
## 7. 并发控制
- 配置类资源(Agent、Probe、任务)在响应中返回 `ETag`,PATCH/DELETE 支持 `If-Match`,不匹配时返回 412。没有带 `If-Match` 的请求按“最后写入者获胜”处理。
## 8. 安全
- 只提供 HTTPS。认证方式有两种:
- 浏览器使用 Cookie 会话:`HttpOnly; Secure; SameSite=Strict`;所有非 GET 请求都必须带 `X-CSRF-Token` 请求头;
- 脚本使用 `Authorization: Bearer <api_token>`,作用域为 `read` 或 `admin`。
- 凭据不能放在 URL 或查询参数中;SSE 依靠 Cookie 或同源令牌认证。
- 登录接口需要限流(例如每个 IP 每分钟 10 次),失败时统一返回 401,不区分“用户不存在”和“密码错误”。
- public 层根据站点设置和 `public_visible` 过滤数据:被隐藏的资源对匿名访客一律返回 404,而不是 403。
- 令牌明文只在创建或轮换的响应中出现一次;这类响应带 `Cache-Control: no-store`。
- 请求体上限为 64 KiB;查询接口有超时(默认 10 秒)。
## 9. 实时推送(SSE)
- 实时数据使用 `text/event-stream`。`event` 名称就是数据类型(`line`、`node`、`point`、`test`);`data` 是单行 JSON,遵守第 4 节的数据表示规则。
- 每 15 秒发送一行注释作为心跳。重连后服务端先推送当前快照,再推送增量,不补发历史。
- 订阅的主题数和连接数都有上限,超出时返回 429 `too_many_streams`。
- 推送的内容必须能由对应的 REST 查询重建:SSE 只负责“更快地获得变化”,不承载独有的数据。
## 10. 新增端点检查清单
1. 符合第 3 节的路径与方法规则,并有 operationId、summary 和 tag;
2. 返回类型精确(`TypedResults`),错误使用 problem+json,错误码来自 `ErrorCode`;
3. DTO 符合第 4 节的数据表示规则,并注册到 JSON 源生成上下文;
4. 列表支持分页,查询有上限;
5. 选择正确的层(public / admin / agent),public 层只暴露可以公开的字段;
6. 重新生成并提交 `openapi/v1.json`,在 PR 中说明是否属于破坏性变更;
7. 测试覆盖:成功路径、校验失败、404、鉴权失败、隐藏资源;
8. 同步更新 [api.md](api.md)。
+193
View File
@@ -0,0 +1,193 @@
# Backend API 规范(v1)
本文列出 Backend 的全部对外接口。通用规则见 [api-guidelines.md](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 重新生成。
```text
/api/public/* 只读。站点设为公开时匿名可访问,设为私有时需要登录或只读令牌;
public_visible = false 的 Agent 对匿名访客隐藏
/api/admin/* 管理。需要 Cookie 会话(含 CSRF 校验)或带有对应**能力**的 API 令牌;每个端点声明自己需要的一项能力(OpenAPI 里的 `x-moshiro-capability`),完整目录见 `GET /api/admin/capabilities`,设计见 [access.md](../security/access.md)
/api/agent/v1/* 节点专用:注册(使用加入令牌)和 RPC(使用签名认证)
/healthz /metrics /openapi/v1.json /install /install.ps1
```
资源总览:
```text
/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](../design/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](../design/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](../design/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](../security/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` | 首次启动时通过命令行或环境变量创建初始管理员 |
任务的请求体示例:
```json
{ "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](../../backend/fleet/README.md) 的「注册」一节 |
| GET | `/api/agent/v1/rpc` | 由 `MapFleetLink` 提供,升级为 WebSocket;MoShiro 的方法见 [protocol.md](../design/protocol.md) |
## 5. 运维端点
| 路径 | 认证 | 说明 |
| --- | --- | --- |
| `GET /healthz` | 无 | SQLite 与原始数据目录都可写 |
| `GET /metrics` | admin 令牌 | Prometheus 格式:在线数、块入库速率、分析队列长度、重算进度、磁盘占用、SSE 连接数 |
| `GET /openapi/v1.json` | 无 | |
| `GET /install`(也接受 `/install.sh`)、`GET /install.ps1` | 无 | 静态安装脚本:Agent 和 Probe 共用同一份,原样返回、不替换任何值,所以哈希在一个版本内固定;角色、面板地址、代码和属主走环境变量 |
| `GET /install/manifest?code=…`、`GET /install/manifest.sig?code=…` | 无(凭一次性代码) | 该角色已签名的发布清单(原样)和签名,安装脚本据此验签、按 digest 拉镜像;代码不能用就是 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` |