Build in Public · LF-13
实时能力目录如何减少文档漂移:把声明、运行与交付分开核对
从实际遇到的平台标识、参数声明和 MCP 发现差异出发,设计目录快照、契约差异与示例检查,明确缓存、缺失字段和发布门,避免把目录一致当成服务全面可用。
写 API 文档时,最危险的错误往往看起来很合理。把地点评论平台写成 google_maps,比 google_maps_reviews 更像人会记住的名字;给每个评论接口都加上 since,也很符合“做增量采集”的直觉。但合理不代表实际支持,复制这类示例的读者会在第一步就卡住。
在 EveryInfra 的这轮内容核对中,我们把文章引用的能力与公开目录逐项对照,同时只读检查实现。这个过程再次说明:实时目录很有用,但它不是万能的正确性证明。文档维护需要同时回答“服务声明什么”“这次请求发生了什么”和“业务拿到了什么”,不能只保留一个绿色状态。
从三个具体差异看目录的价值
第一个是平台身份。2026-09-04 20:46(北京时间),查询 google_maps 的公开目录得到 HTTP 400 unknown_capability,提示中包含 google_maps_reviews。目录能指出错误名称,却仍需要编辑确认目标确实是地点评论,不能自动采用任何相近候选。
第二个是 Amazon 评论参数。同日的公开条目只列出 url 与 domain,没有声明 default_limit、max_limit 的具体数值。本地实现中的部分参数说明更丰富,但不能因此写成已部署能力。正文需要取明确可确认的范围;没有声明上限,只能记录未知,不能写成无限量。
第三个是 MCP 发现。20:43 的探针能读取六个工具,初始化完成通知却带有不应存在的响应体。工具名称和数量对上了,仍不足以证明协议完全符合要求,更不足以证明每个客户端已完成认证与实际调用。这个差异需要自己的验收项,不能藏在“目录正常”后面。
三者分别涉及标识、声明范围和协议行为。它们都可能影响接入,但处理方式不同:修正错误名称、收窄文档承诺、保留协议复核条件。把所有差异统称为“文档过期”会丢掉该由谁解决、怎样验证的信息。
将结构、运行和交付拆成三层
- 结构证据:当前目录写明 action、必填参数、允许值及可返回字段,用于构造请求与解析策略。
- 运行证据:一次具体 GET、HTTP 请求或协议发现确实完成,记录输入、响应类型、状态和观察时间。
- 交付证据:一个获准业务目标得到符合约定的内容,并核对空结果、部分完成、失败及计费;需要单独的业务验收。
目录里有 owner_response,不能证明每条评论都有商家回复。请求得到 200,不能证明所有分页都已完成。一次业务样本成功,也不能证明每个目录项都可用。文章应在论断旁保留对应证据层级,不让一个较弱的检查支撑更强的承诺。
复用机器可读契约,而不是复制多份说明
同一能力可能出现在 HTTP 文档、能力目录、字段说明、MCP schema 和文章示例中。维护时可以让标识、输入与字段定义来自一份可复用契约,再让各出口承担不同职责:目录帮助发现,字段页解释语义,示例说明读者任务,测试对照运行行为。
EveryInfra 当前代码中的能力契约渲染与参数校验可以支持这类复用;但共享函数存在,不证明所有线上页面和部署实例已经同步。人工文字也不会因为读取了同一目录,就自动具备正确的用例、授权边界和失败说明。
OpenAPI 描述 HTTP 接口及其结构,是这种维护方式的一部分。实际业务中还有 action 语义、字段缺失、样本范围与用途限制,仍需明确写出。自动生成可以减少抄错字段,却不能替编辑判断一句话是否扩大了能力。
结构比较还要区分 properties、required 与 null。JSON Schema 官方指南明确说明:列在 properties 里的字段默认不是必填。只比较字段名集合,可能漏掉“可省略变成必填”这样的接入变化;反过来,允许字段缺失也不代表允许显式 null。
让每篇教程只查询自己需要的契约
接入时先确定平台与对象,再缩小到 action。下面只读公开目录,不获取评论或使用 Key。jq 会把未返回的所选属性显示为 null,因此还要区分“原响应缺字段”和“响应明确写了空值”;两者都不能擅自解释成零。
curl -fsS --max-time 30 \
'https://api.everyinfra.com/api/v1/social/catalog?platform=amazon' \
| jq -e '.capabilities[] | select(.action == "reviews") | {
platform, action, required_params, optional_params, param_meanings,
mode, returns_list, default_limit, max_limit, response_fields
}'给这份观察附上来源 URL、获取时间和实际响应摘要。请求失败或解析失败时,不要生成一份空目录覆盖旧快照;保留旧内容与失败状态,让维护者知道是“没取到新的声明”,而不是“服务删除了全部能力”。
针对自己的应用,还应登记用了哪些参数和返回字段。某个不相关 action 新增字段,不一定需要暂停当前任务;正在使用的必填参数或输出形状改变,则需要有明确负责人处理。差异检测要落到实际消费者,而不是只汇报总条数变化。
比较语义差异,不比较整个 JSON 文本
对象键顺序、列表展示顺序和说明文字调整,可能让文件看起来变化很大,却没有改变调用方式。相反,returns_list 从 true 变为 false 只改一个值,就足以让解析器失效。先选择需要检查的字段,再分类处理差异。
function contractDiff(before, after) {
if (before.platform !== after.platform || before.action !== after.action) {
throw new Error("compare the same capability");
}
const normalize = row => {
const names = key => {
const values = row[key];
if (!Array.isArray(values)
|| values.some(v => typeof v !== "string" || !v.trim())
|| new Set(values).size !== values.length) {
throw new Error("explicit unique field list required");
}
return [...values].sort();
};
if (typeof row.mode !== "string" || !row.mode
|| typeof row.returns_list !== "boolean") {
throw new Error("explicit mode and result shape required");
}
return {
required_params: names("required_params"),
response_fields: names("response_fields"),
mode: row.mode, returns_list: row.returns_list
};
};
const left = normalize(before), right = normalize(after);
return Object.keys(left).filter(key =>
JSON.stringify(left[key]) !== JSON.stringify(right[key]));
}
const baseline = {
platform: "synthetic", action: "comments",
required_params: ["url"], response_fields: ["id", "text"],
mode: "sync", returns_list: true
};
console.log(contractDiff(baseline, {
...baseline, response_fields: ["text", "id"]
}));
console.log(contractDiff(baseline, {
...baseline, required_params: ["url", "region"]
}));这两个合成结果分别是无差异,以及 required_params 有变化。它只比较四项显式结构,没有实现完整的兼容性判断;可选参数、枚举、字段类型、数量限制、可用性和计费声明仍需各自检查。缺失结构会报错,不会被悄悄当作空列表。
需要重点审查的变化包括:新增必填输入、删除已用字段、收窄枚举或数量范围,以及执行模式改变。即使是增加返回字段,也要检查下游是否错误地拒绝未知字段。检测器只提示变化,由维护者结合真实消费者确认影响,不自动生成“兼容”结论。
缓存保存的是观察,不是永久保证
缓存键至少区分服务地址、平台与 action;记录获取时间与使用的协议或契约版本。多个环境共用同一缓存键,可能把测试声明带入生产。可用性、参数和文章来源的复核节奏也可能不同,不能用一个统一时长掩盖全部风险。
例如,编辑安排一天后复查资料,只是团队的工作安排,不是服务端承诺目录一天内不变。遇到明确未知参数、返回形状变化或部署更新时,应触发针对性刷新;不必等固定时钟到了才处理。过期内容可以供人查看,但高影响操作不能继续依赖未经确认的旧声明。
旧快照还要保留自己的时间。新请求失败时把旧日期改成今天,会制造“已经验证”的假象。保存历史观察与最新一次检查状态,才能解释某篇教程依据的到底是哪一次服务状态。
若服务确实提供相应响应头,可以参考 RFC 9111 的条件请求与再验证机制,判断缓存能否复用。本文没有验证目录支持 ETag 或 304;在缺少该契约时,不能自行把本地摘要当成服务器验证器,或把复查周期称为服务器缓存承诺。
把正文示例也视为一个客户端
示例检查应从实际文章中提取代码,不另写一个看似相同的测试请求。免费目录命令可以直接核对;业务 POST 在没有获准样本时,先检查语法、请求组装和离线响应分支,明确尚未验证服务结果。不要让测试副本正确、读者复制的版本却把 limit 放错层级。
链接也要按用途检查。站内能力页帮助继续接入,官方文档支持对应产品与协议的原始定义;某个平台的官方链接不代表它为 EveryInfra 的全部用途背书。未发布文章不提前用一个会 404 的详情链接承接读者。
发现漂移后的处理顺序
- 读取失败:保留故障状态,不覆盖成新契约;必要时暂停依赖该声明的操作。
- 契约变化:列出受影响参数、字段与消费者,决定更新示例、适配代码或继续阻断。
- 本地与公开状态不同:分别记录,不能把本地修改当成部署完成。
- 公开声明与交付冲突:保留最小获准样本,将问题交给服务维护者;文章先收窄相关承诺。
- 各项相符:只报告本次检查范围内没有发现差异,不自动发布文章或宣布所有集成成功。
从业务最常用的一个 action 开始,建立目录观察、示例检查和交付验收三份能互相关联的记录。这样,文档更新不再只是把旧截图换成新截图,而是能说明哪条事实改变了、谁受影响,以及什么证据足以继续使用。