- 开发工具
- Agent Traces
- AI 应用
【免费下载链接】cli
📜 Entire CLI hooks into your Git workflow to capture AI agent sessions as you work. Sessions are indexed alongside commits, creating a searchable record of how code was written in your repo.
Entire CLI 的internal/coreapi包是一个面向控制平面(Core API)的纯客户端,其代码由 ogen,系统梳理上游 OpenAPI 文档中尚未修复的五类缺陷(错误响应折叠、只读枚举过严、必填字段前向兼容、可空对象类型、交互式登录安全方案),逐一说明其触发症状、上游修复方向、仓库内的工作区实现(spec/normalize.go)以及对应的测试锁定,帮助读者理解一个生成式 API 客户端如何在"上游规范有缺陷"的约束下保持健壮、可前向/后向兼容,并学会维护这类上游缺陷清单。
适用前提:本文所讲内容均以当前仓库实际代码与文档为准。核心 API 端点示例(
https://us.console.entire.io/api/v1/openapi.json)出自仓库源码注释,仅用于说明刷新规范的操作方式。
背景:为什么会有 UPSTREAM.md 这份清单
Entire CLI 的 Core API 客户端完全是控制平面的客户端(identity、orgs、projects、repos、mirrors、service accounts 等资源的管理面),它通过 internal/coreapi/gen.go 中的go:generate指令完成生成:
//go:generate go run spec/normalize.go //go:generate go run github.com/ogen-go/ogen/cmd/ogen@v1.20.3 --target . --package coreapi --config ogen.yml --clean spec/core.gen.json //go:generate gofmt -s -w .生成链路是:先curl拉取上游原始规范spec/core.openapi.json,由 spec/normalize.go(带//go:build ignore构建标签的独立命令)改写成规范化产物spec/core.gen.json,ogen 再基于它生成oas_*.go系列文件。上游原始文件永不被修改,刷新规范就是一次干净的curl覆盖(见 spec/normalize.go 的包注释)。
问题在于:上游控制平面的 OpenAPI 文档及其暴露面存在若干缺陷(bug / gaps),直接按原样生成会让客户端不可用或非常难用。UPSTREAM.md 正是这份"运行清单"(running checklist):每个条目都应该在上游源头(控制平面服务的规范生成 / 路由设计)修复;上游修复后,删除对应工作区代码(以文件为索引)并删掉该条目。
normalize.go 的包注释明确指向这份清单:
The running checklist of upstream fixes lives in internal/coreapi/UPSTREAM.md.
两者形成闭环:清单记录"为什么",normalize.go 给出"怎么做",client_test.go 等测试则把行为锁死,防止回归。
缺陷 1:每个操作逐一枚举错误状态,缺少统一的default响应
症状
上游规范中,每个操作都声明了真实的成功码(create 用 201、read 用 200、delete 用 204,这很好),但随后把每个错误状态单独罗列(400、401、403、422、500……)而没有default响应。ogen 会据此为每个操作生成一个"逐操作求和类型"(sum-type result),强制每个调用点都做一次类型 switch,而不是得到更符合人体工学的(*T, error)。
上游修复方向
在上游统一发出单一的default错误响应——因为每个错误响应本来就引用同一个ErrorModel(RFC 7807 problem+json 错误模型),折叠为default是无损的。这样 ogen 会直接生成"convenient errors":(*T, error)签名,任何非 2xx 都作为带类型的*ErrorModelStatusCode返回,从规范层面一步到位。
仓库内工作区实现
spec/normalize.go 中的foldErrorResponses函数(实现位置)遍历所有 path 下的 HTTP 操作,把显式 4xx/5xx 折叠成一个default,同时原样保留真实成功码:
folded := map[string]any{"default": defaultErrorResponse()} for status, resp := range responses { if len(status) > 0 && status[0] == '2' { folded[status] = resp } } operation["responses"] = folded关键细节:保留字面成功码(而非 "2XX" 范围)意味着 ogen 直接返回成功类型,不需要*…StatusCode包装再解包;default错误则复用errorModelRef = "#/components/schemas/ErrorModel"(定义)。生成的core.gen.json中每个操作都能看到"200"与"default"并存的结构。
这是五个转换中唯一一个有意的工程易用性选择,而非纯 bug 工作区(normalize.go 注释原文:a deliberate ergonomics choice rather than a pure bug workaround);一旦上游出现共享default,它就可以退役。
配套的运行时收益在 client.go 的APIError帮助函数:用errors.As提取*ErrorModelStatusCode,按Detail→Title→ 状态码的优先级渲染出干净的用户可读错误,避免直接暴露 ogen 的解码包装串。
缺陷 2:只读展示用的枚举在未知值上硬失败
症状
CLI 只用于展示的读模型字符串字段(如Repo.state、Repo.visibility、Repo.objectFormat)在上游被声明为enum。ogen 会把每个枚举字段变成具名类型并附带严格的Validate(),而响应解码器会无条件调用这个校验。于是,一旦服务端新增枚举值(例如仓库新增生命周期状态),整个repo list/repo view请求就会解码失败——尽管客户端根本不会针对该值做分支判断。
同类形状的字段还包括:Membership.role/status与Invitation.role/status——entire org grant list展示成员、entire org invite list展示邀请,生成的ListOrgInvitations客户端要解码它们。新增的角色或生命周期状态必须能"展示"而非"让整个列表失败"。
上游修复方向
把"客户端只展示、未来可能新增取值"的字段建模为纯字符串(去掉enum),或让 ogen 把它们当作开放枚举(open enums)。而客户端会发送的枚举(如请求体SetRepoVisibilityInputBody)应保持严格。
仓库内工作区实现
loosenReadModelEnums(实现)按白名单readModelEnumFields(完整清单)删除这些响应读模型字段上的enum约束,使 ogen 生成纯字符串、不带Validate(),未知值直接透传展示。白名单覆盖:
| Schema | 被放宽字段 |
|---|---|
Repo/CreatedRepo | objectFormat、provider、state、visibility |
Membership/OrgMemberListItem | role、status |
Invitation | role、status |
DetachPrecondition | precondition |
DetachAccessEntry | source、subjectType |
DetachRepoResult | status |
RepoDetachState | status |
RepoIndexEntry | permission、provider |
RepoReference/RepoResolution/RepoIDResolution | provider |
只有响应读模型被放宽;请求体枚举保持严格——CreateOrgInvitationInputBody.role仍带 enum,出站请求仍会拒绝非法值。
Repo.provider是典型的"分支判断"案例:repo_protection.go 的reportNoProtectionRules分别测试"github"与"entire"两个已知值,其他任何值落入第三种渲染(protectionUnknownNote)——一个缺失或无法识别 provider 的仓库绝不能误入任一已知值分支。这正是 normalize.go 注释强调的取舍:未知值会静默落入已知值分支的字段,不适合放进该白名单。
测试锁定(均在 internal/coreapi/client_test.go):
TestListProjectRepos_UnknownEnumValuesPassThrough(L260):注入state:"archiving"、visibility:"internal"、objectFormat:"sha512"等当前规范枚举不允许的值,验证repo list不失败且原样透传。TestListOrgInvitations_UnknownEnumValuesPassThrough(L430):role:"auditor"、status:"bounced"透传。TestListOrgMembers_UnknownEnumValuesPassThrough(L539):role:"auditor"、status:"suspended"透传。TestDetachRepo_UnknownEnumValuesPassThrough(L484)与TestGetRepoDetach_UnknownStatusPassesThrough(L506):detach 场景的preconditionslug、source/subjectType、status服务端文档明确为"不断增长的集合",必须透传。
当上游逐步放宽对应字段时,逐一删除白名单条目即可。
缺陷 2b:新增读模型字段以required发布
症状
上游给Org、Project、Repo增加capabilities,给RepoIndexEntry增加org/provider,给ListReposOutputBody增加candidatesIncomplete,且都是required。ogen 的解码器在字段缺失时直接让整个响应失败——于是早于该字段发布的核心、或混版本滚动部署(mixed-version roll),会让所有 list、get 以及 repo 路由调用全部失败,即使客户端根本不依赖这些字段。
创建响应(CreatedOrg、CreatedProject、CreatedRepo)复刻了对应读模型的形状,因此同样复发此问题。
上游修复方向
读模型字段应以可选方式新增,直到所有部署都能发送它们,再收紧为必填。
仓库内工作区实现
loosenReadModelRequired(实现)按白名单readModelOptionalFields(完整清单)把这些字段从required列表移除:
| Schema | 被放宽字段 |
|---|---|
Org/CreatedOrg | capabilities |
Project/CreatedProject | capabilities |
Repo/CreatedRepo | capabilities、provider |
RepoIndexEntry | org、provider |
ListReposOutputBody | candidatesIncomplete |
白名单的入选标准是"不依赖其存在"(does not depend on),而非"从不读取":字段可能通过Opt访问器读取并带安全默认值,缺失只劣化一个分支而不至于让整个响应失败。例如repo protection list就是通过Repo.provider的Opt访问器读取,服务端缺省时回落到通用提示;当 CLI 需要字段必须存在才能保证正确性时,就应把该字段移出此表。
值得注意,RepoIndexEntry获得了专门的测试TestListRepos_UnsentRequiredReadFieldsDecode(client_test.go),原因是ListRepos是最宽泛的消费者:合并后的仓库索引正是resolveRepoCellTarget(见 cell_target.go)做 cell 路由的数据源,search、repo mirror和 dispatch wizard 也都经过它。该处解码失败不是"单个命令报错",而是cell 路由与搜索同时失明——爆炸半径远大于其他命令(normalize.go 与测试注释均如此强调)。同一测试还验证了permission:"triage"(未知枚举值)透传,一次请求同时锁定 2 与 2b 两个转换。
TestListOrgsAndProjects_UnsentCapabilitiesDecode(client_test.go)则覆盖另外两个新增capabilities的 Schema:响应不带capabilities也必须能解码。
此外,RepoIndexEntry新增的provider与permission枚举同时出现在readModelEnumFields中(CLI 要么展示它们、要么针对已知值测试、其余一律视为未知),保证新旧转换协同生效。
缺陷 2c:可空对象被类型化为"仅有对象"
症状
ResourcePerson.directGrant是对ResourcePersonDirectGrant的$ref,但服务端对没有直接授权的人发送"directGrant": null——而 GitHub 同步的协作者全部属于这种情况。ogen 的解码器会因此拒绝整个GET /repos/{repoId}/people(以及/projects/{projectId}/people)响应。
上游修复方向
把该字段声明为可空(anyOf: [$ref, {type: null}]),或者干脆缺省发送而不发 null。
仓库内工作区实现
allowReadModelNulls(实现)按白名单readModelNullableFields(定义,当前仅ResourcePerson.directGrant一项)把裸$ref重写为anyOf组合,ogen 随之生成Nil…包装类型:
props[field] = map[string]any{"anyOf": []any{ map[string]any{"$ref": ref}, map[string]any{"type": "null"}, }}测试TestListRepoPeople_NullDirectGrantDecodes(client_test.go)注入"directGrant":null的真实生产形状(GitHub 协作者),断言列表成功解码且DirectGrant.IsNull()为真。normalize.go 的注释补充了业务背景:repo mirror detach需要读取该字段来命名展示的账户。
缺陷 3:每个操作都声明交互式登录安全方案
症状
上游规范在每个操作上列出四种安全备选方案(oauth2、oidc、bearerAuth、sessionAuth)。其中oauth2和oidc描述的是浏览器或设备如何获取 token——而已经持有 bearer 的客户端根本不会驱动它们。更致命的是:ogen没有openIdConnect的生成器,遇到oidc会直接中止,导致规范根本无法按原样消费。
上游原始规范中的定义(core.openapi.json)确实是这种形状:oauth2声明 authorizationCode flow(含 PKCE-S256 与 RFC 8628 设备流说明)、oidc为openIdConnectUrl: "/.well-known/openid-configuration"、sessionAuth为entire_sessioncookie(apiKey in cookie)。
上游修复方向
oauth2/oidc保留在components.securitySchemes中用于文档,但每个操作的实际 security 要求只列bearerAuth与sessionAuth——因为请求真正携带的就是这两个。
仓库内工作区实现
dropInteractiveSecurity(实现)配合interactiveSecuritySchemes({"oauth2": true, "oidc": true})做三层清理:
- 从
components.securitySchemes删除这两个方案; - 删除文档级
security列表中的对应条目; - 遍历每个 path/method 操作,从操作的
security列表删除只含交互式方案的备选项。
生成的SecuritySource因此只保留客户端真正实现的BearerAuth与SessionAuth方法——对比规范化前后(core.gen.json 只剩bearerAuth与sessionAuth两个 scheme)。
防御性设计:该转换拒绝清空一个有内容的 security 列表(filterSecurityRequirements的报错分支,normalize.go)。原因:在 OpenAPI 中,空的 operation 级security意味着"无需认证",把仅含 oauth2 的操作过滤成空,会静默生成一个不再向其发送 bearer 的客户端。当前规范中没有任何操作是这样的形状,但如果上游未来新增 device-code 或 authorize 端点就会触发——这是唯一一种"不会自我宣告"的失败模式(其同类会自我宣告:混搭oauth2与bearerAuth的多键 requirement 会整体幸存,留下对已被删除方案的悬空引用,使 ogen 在生成期直接中止)。
客户端侧的运行时配合在 client.go 的providerSource/staticBearer:SessionAuth一律返回ogenerrors.ErrSkipClientSecurity,让 ogen 中间件仅凭 bearer 满足 "bearerAuth OR sessionAuth" 要求,不添加多余的Cookie: entire_session=头(若返回空的SessionAuth,生成的securitySessionAuth会无条件req.AddCookie,反而发出空 cookie)。TestBearerOnlySource_NoCookieOnTheWire(client_test.go)在 HTTP 线上层验证了"仅 bearer、无 cookie"契约。
已在上游解决的条目(清单维护示范)
UPSTREAM.md 底部以 HTML 注释记录了已解决并移除的两项,是"修复后如何清理"的范例:
- 可空数组(
"type": ["array","null"]):entiredb 现在发送非空数组("type": "array",缺省即[]),对应的collapseTypeUnions转换已删除。 - 按镜像查找 vs mirrorId 删除:entiredb ENT-741 用单一"按坐标删除"路由(
DELETE /mirrors?provider&owner&repo&clusterHost)取代了原来的"先查后删"两步调用;mirror remove现在直接调用它,并呈现新的 404/403/503 契约。
这两条注释的存在也说明:清单只保留"当前仍未修复"的条目,每修好一个就删一个——这正是 UPSTREAM.md 作为运行清单的维护纪律。
如何刷新规范与验证转换
手动刷新并重新生成客户端的完整流程(出自 spec/normalize.go 与 gen.go 注释):
# 1. 拉取上游原始规范(仅覆盖 core.openapi.json,绝不触碰其他文件) curl -fsSL https://us.console.entire.io/api/v1/openapi.json \ | jq . > internal/coreapi/spec/core.openapi.json # 2. 运行规范化转换 + ogen 生成 + gofmt 简化(保持 CI 干净、幂等) go generate ./internal/coreapi/...normalize.go 结尾会打印每个转换的处理数量:
normalize: folded error responses on N operation(s), loosened N read-model enum field(s), made N read-model field(s) optional, made N read-model field(s) nullable, dropped N interactive security scheme(s) → spec/core.gen.jsonogen 生成配置见 ogen.yml:关闭了服务端一半(handlers、router、request decoders、unimplemented stubs 等,因为 CLI 纯做客户端)以及 OpenTelemetry 接线(CLI 有自己的遥测路径,不追踪这些调用)。
总结:清单、转换、测试三位一体
UPSTREAM.md 的价值在于把"生成式客户端对上游缺陷的容忍"变成可审计、可清理的工程资产:
- **清单(UPSTREAM.md)**记录每个缺陷的症状、上游修复方向与工作区文件索引,是"上游修复后删哪段代码"的依据;
- **转换(spec/normalize.go)**用五个白名单驱动的转换在生成前改写规范,全部以
//go:build ignore的独立命令形态存在,刷新即重跑; - **测试(client_test.go)**用注入"规范不允许的值 / 缺失的必填字段 / null 对象"的方式,把前向兼容(未知值透传)与后向兼容(缺字段可解码)契约锁死,防止未来改动悄然破坏。
维护者的工作流因此非常清晰:发现上游新缺陷 → 在 normalize.go 增加转换与白名单 → 在 client_test.go 增加锁定测试 → 在 UPSTREAM.md 登记条目;上游修复 → 删除工作区代码 → 删除清单条目。这也正是本仓库 internal/coreapi/UPSTREAM.md 被设计为"running checklist"的原因。
- 开发工具
- Agent Traces
- AI 应用
【免费下载链接】cli
📜 Entire CLI hooks into your Git workflow to capture AI agent sessions as you work. Sessions are indexed alongside commits, creating a searchable record of how code was written in your repo.
相关推荐
EMQX 聚焦式 API 规范切片(/api-spec)与 api_keys CLI 实战指南
EMQX 聚焦式 API 规范切片(/api spec)与 api_keys CLI 实战指南 本篇技术指南聚焦 EMQX 新引入的 /api spec 端点体
后端物联网消息队列通信agent-core-java 依赖治理规范:Maven 依赖版本、作用域、禁用清单与 CVE 升级实战指南
agent core java 依赖治理规范:Maven 依赖版本、作用域、禁用清单与 CVE 升级实战指南 本文是 agent core java 仓库的依赖
人工智能AI AgentAgent 框架工具调用Agent 工作流RAGMCP Clients强化学习gRPC API 向后兼容性治理:解析 doc/core/pending_api_cleanups.md 待清理清单
gRPC API 向后兼容性治理:解析 doc/core/pending_api_cleanups.md 待清理清单 这是一篇面向 gRPC 维护者与深度用户的
后端RPC框架微服务通信
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考