Spec audit · 2026-05-17

Omada Open API 规范化分析报告

覆盖 1713 条路径 / 2269 个操作 / 3116 个 schema。对标 OpenAPI 3、REST/RFC 9110、Google AIP、Stripe & GitHub 接口规范。

source · specs/omada_api.json
title · Omada Open API v0.1
openapi · 3.0.1

01端点全景

两套并行的资源树:单租户控制器 /openapi/v1/{omadacId}/...(1609)与 MSP 多租户 /openapi/v1/msp/{mspId}/...(104)。版本 v1 主导,v2/v3 零星补丁。

1,713
unique paths
最深 16 段,众数 8–9
2,269
operations
0 重复 operationId
3,116
schemas
含 10+ 名字带空格的 schema
79
deprecated ops
零继任者声明

HTTP 方法分布

2269 operations
GET98343.3%
POST62027.3%
PATCH27312.0%
DELETE2038.9%
PUT1908.4%

Top 10 业务标签(按 operation 数)

Wired Network100
Ap75
Device75
VPN66
Switch64
Wired (Template)62
Profiles60
VoIP56
Stack55
Profiles (Tmpl)50

路径深度分布(段数)

5 段22
6 段72
7 段214
8 段453
9 段443
10 段229
11 段116
12 段69
13 段63
14 段29
15–16 段3

API 版本分布(按路径)

/openapi/v11639
/openapi/v266
/openapi/v38

lan-networks 三版本共存;clients 双版本。 47 个 operationId 还把版本号塞在名字里(modifyBtIbeaconConfigV2)。

02关键违规 · P0

与 OpenAPI / HTTP 规范直接冲突。导致 SDK 必须手写补丁、第三方代码生成器/Postman/Stoplight 无法正确导入。

P0-1
所有 2269 操作只声明 200,零非-2xx 状态码
证据:响应键集合为 {"200"}(命中 2269),错误一律以 HTTP 200 + body errorCode != 0 表达。
业界标准 · REST/RFC 9110 期望 201 Created、204 No Content、4xx 客户端错、5xx 服务端错。Stripe、GitHub、Google API 全部按状态码分支。
P0-2
响应 Content-Type 是 */*
全 2269 操作的 200 响应都只声明 */* 一个媒体类型。
业界标准 · OpenAPI 推荐 application/json*/* 让 SDK 生成器无法挑 schema,客户端必须凭直觉信任服务器实际返回的是 JSON。
P0-3
components.securitySchemes 为空,所有 op 无 security
证据:jq .components.securitySchemes{};逐 op .securitynull
业界标准 · 即使是私有 OAuth2,按 OpenAPI 也需写出 flowsscopes,否则 SDK 生成器、Postman 都不知道如何鉴权。
本仓库 SDK 因此必须手写 OAuthTokenStorebuildClient
P0-4
统一信封 {errorCode, msg} 取代 HTTP 语义
主响应 OperationResponseWithoutResult{ errorCode: int32, msg: string }。业务错通过 errorCode != 0 表达,HTTP 永远 200。
业界标准 · Google AIP-193 / RFC 7807 application/problem+json 是当代默认。GitHub、Stripe 在 4xx 上返回结构化 error。信封风格只有早期阿里云/腾讯云 API 采用,且都在向标准状态码迁移。
P0-5
servers[0].urlhttp://
http://use1-omada-northbound.tplinkcloud.com — 唯一一条 server,且仅 us-east-1 区域。
业界标准 · OAuth client credentials 必须 TLS。本仓库 SDK 已在 assertSecureUrl 强制 https — spec 本身仍是错的。

03风格漂移 / 客户端体验 · P1

不破坏功能但增加学习成本与样板代码量,长期累积形成"两套以上习惯共存"。

P1-6
70 个语义为读的端点使用 POST
例:getGridAllClients → POST /openapi/v2/.../clientsgetTopologyClientsByDevicesgetDevice5MinStatistic。GET ops 命名为 list/query 等的 59 个一致性问题。
业界标准 · 过滤条件太大时,Elastic 用 _search、Google AIP-136 用 :search 自定义动词命名以保留语义信号。
P1-7
/cmd/ 子树承担动作型写操作
35 条路径:/sites/{siteId}/cmd/devices/batch-adopt/cmd/aps/move/cmd/mlag/{mlagId}/reboot
业界标准 · Google AIP-136 推荐 :customVerbdevices:batchAdopt)。/cmd/ 与 REST /{collection}/{id} 形成两套并行习惯,客户端要记两套路由表。
P1-8
批量动词语义混乱
同时存在 batchbatch-adoptmulti-addbatchModifyApVlanConfigbatchSetPortStatusForGivenPorts_1(54 个 batch 前缀 + 2 个 multi-add 路径)。_1 后缀强烈暗示历史命名冲突未清理。
业界标准 · 统一为 batchXxx:batchCreate
P1-9
路径段大小写风格漂移
同一份 spec 内:customer-roles/audit-logs(kebab) vs allowApp/allTabs/categoryAppInfo(camel) vs 5Min/ipsec_failovers(数字开头 / snake_case)。
业界标准 · OpenAPI 风格指南 + Google AIP-122/154 都要求统一 kebab-case。
P1-10
schema 名称里含空格
"AP Info""AP MAC list"" BatchUnbindSites"(首字符空格)、"OperationResponseListActivity Records Of A Client's Single Connections"
业界标准 · OpenAPI Generator / openapi-typescript / NSwag 会把空格转义成 _%20 或直接报错。质量事故级问题。
P1-11
过滤/排序使用 dot-notation query 参数
sorts.name=ascfilters.wireless=truefilters.timeStart=…filters.radioId=0 共 80+ 个。
业界标准 · 与 OpenAPI 3 style: deepObject 不兼容。Stripe 用 expand[]、GitHub 用扁平 sort=/order=、Google 用 filter= AIP-160 表达式。
P1-12
分页 page+pageSize 双 required
282 个分页 GET 全部要求 pagepageSize 同时传,page 1-based。
业界标准 · GitHub page 默认 1;Stripe limit 默认 10;Google AIP-158 用 page_token+page_size。强制双必填只增加样板。
P1-13
同资源 v1/v2/v3 共存且无导航
/openapi/v1|v2/{omadacId}/sites/{siteId}/clients 双版本,lan-networks 三版本。
业界标准 · 缺 Deprecation/Sunset header(RFC 8594)、缺 x-replaced-by 扩展。79 个 deprecated 操作没有指向继任者的链接。
P1-14
operationId 同时使用 camelCase 与 V2 后缀
47 个 op 同时背了 URL 里的 /v2/ 和名字里的 V2,如 modifyBtIbeaconConfigV2 → /openapi/v1/.../config/{id}(URL 是 v1,名字是 V2)。
另有 112 个 operationId 违反 camelCase,含 _1PascalCase5Min
业界标准 · operationId 仅代表"行为",版本由 URL 唯一表达。
P1-15
DELETE 携带 body 37 次
批量删除把 ids[] 放进 DELETE body。
业界标准 · RFC 9110 §9.3.5:DELETE 的 body 行为未定义。CDN、代理、早期 fetch() 会丢弃。Stripe/GitHub 用 POST :batchDelete 或多次 DELETE。

04可观测性 / 治理缺失 · P2

P2-16
Idempotency-Key 头参数
写操作(620 POST + 273 PATCH)无幂等键,重试会重复扣减;本仓库 two-phase commit 在外层补救。
P2-17
响应无 rate-limit headers
任何 op 都没声明 X-RateLimit-*Retry-After。本仓库 parseRetryAfter 修复过"过去时间 → thundering herd" — 反映 spec 本就不规定。
P2-18
完全没有 examples / x- 业务扩展
顶层只有 x-order(排序用),无 examples、x-stabilityx-tier — SDK 智能补全无素材。
P2-19
路径最深 16 段,MSP 与单租户全量重复
/openapi/v1/msp/{mspId}/customers/{customerId}/sites/{siteId}/olts/{deviceMac}/l2-feature/eth-port/port/unit1/list。MSP 把租户/客户/站点都打进 URL,单租户 mode 又重复一遍。
P2-20
三层资源前缀完全平行
/openapi/v1/{omadacId}/... 几乎全在 /openapi/v1/msp/{mspId}/customers/{customerId}/sites/{siteId}/... 下重复一遍。

05与业界基准并排对照

同一维度上,Omada 当前状态 vs Stripe / GitHub / Google AIP 的做法。

维度 Omada Stripe GitHub Google AIP 差距
HTTP 状态码 永远 200 标准 2xx/4xx/5xx 标准 标准 + google.rpc.Status 最大
错误格式 {errorCode, msg} RFC 7807-like + type message + documentation_url google.rpc.Status
资源命名 复数 + camelCase 混 kebab 混 snake kebab,复数 kebab,复数 snake_case,复数
集合 vs 自定义动作 /cmd/... + 路径动词 + POST-as-GET 三种共存 POST /resource/:action POST /:action :customVerb
分页 offset,双 required cursor (starting_after) offset 或 cursor page_token + page_size
过滤 filters.x= dot-notation created[gte]= flat query filter= AIP-160 EBNF
鉴权声明 spec 空 OAuth2/Bearer 完整 OAuth2/PAT 完整 OAuth2 完整 直接缺失
弃用治理 79 处 deprecated: true,零继任者 Sunset + changelog X-GitHub-Media-Type + nbf resource_reference
幂等 Idempotency-Key 部分支持 ETag / If-Match
媒体类型 */* application/json application/vnd.github+json application/json
TLS spec http:// https-only https-only https-only

06本仓库 SDK / MCP 层已打的"补丁"

阅读 packages/sdk/src/clientTODO.md 的修复记录,可以反推:很多 P0/P1 都是被工程外科手术地绕过去的。

P0-3 ← TLS
assertSecureUrl 强制 https — 因为 spec server 是 http://
P0-2 / P1-2
expiresIn ≤ 0Retry-After 过去时间挡掉 — 因为 spec 不约束这些边界。
P1-3 ← pagination
paginate() 硬上限 10,000 页 — 因为分页参数永远要求 page+pageSize,又没有 nextPageToken
P1-4 ← audit
Audit sink 必填 onError + flush — 因为没有标准化错误模型,业务 errorCode 都靠你审计。
A-3 ← deprecation
api-diff --fail-on-breaking — 因为上游 79 deprecated 没有继任者声明。
MCP 22 intent tools
直接放弃 2269 op 的 1:1 暴露,本身就是对 spec 不可用性的事实承认。

07规范化路线图(若反馈给上游 TP-Link)

破坏性 · 需新版本

P0 — 协议层正确性

  1. HTTP 状态码语义补回:错误用 4xx/5xx、删除用 204、创建用 201。errorCode/msg 转 RFC 7807 application/problem+json
  2. components.securitySchemes 写出 OAuth2 client-credentials + scopes,每 op 标注 security
  3. 响应 content*/* 改为 application/json
  4. servers[].url 改 https,补齐多区域。
向后兼容 · 可在 v3 收敛

P1 — 一致性 & 客户体验

  1. 路径风格统一 kebab-case,schema 名删空格(破坏 SDK,必须 v3)。
  2. /cmd/ 与 POST-as-GET 收敛到 :customVerb/{collection}/{id}/{action}
  3. 分页:补 pageToken/nextPageTokenpageSize 可省。
  4. 过滤:单一 filter= AIP-160 或扁平参数,废弃 filters.*
  5. DELETE 携 body 的端点 → POST :batchDelete
  6. 79 个 deprecated 添 x-replaced-by + Sunset
治理 / 运维

P2 — 可运营性

  1. 写操作支持 Idempotency-Key header;two-phase confirm-token 可借此 anchor。
  2. 响应一律带 X-RateLimit-{Limit,Remaining,Reset}
  3. 每 op 加 examples + x-stability: stable|beta|deprecated
  4. MSP 与单租户两套树通过 X-Omada-Tenant header 合并,路径深度 ≤ 8。

08给本仓库的内部行动建议

docs/upstream-feedback.md
把 P0/P1 落到一个反馈文档,跟 M6 staging dogfood 一起回传给 TP-Link,配合 m6-auth-research-questions.md
scripts/diff-api.ts
新增 lint 规则:检测「新增 POST 路径 opId 以 get 开头」「新增 DELETE 带 body」「响应只有 200」等可机器化规范回归。
MCP 22 intent tools
不要尝试 2269:1 暴露给 LLM;继续保持 intent-grouped + two-phase 写入路径。
生成器后处理
pnpm generate 后做一次重命名映射,避免生成的 TS 类型名包含 $ / _20 这种转义。