Files
MoShiro/docs/api-guidelines.md
T
2026-10-04 21:46:39 +08:00

142 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](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 放在 `Monitor.Backend.Api/Contracts/`,并注册到 Backend 的 JSON 源生成上下文。**不要直接暴露 `Monitor.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](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)。