ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

opencode httpapi-codegen 深度解析:从 HttpApi 契约到 Promise 与 Effect 双客户端的代码生成机制

opencode httpapi-codegen 深度解析:从 HttpApi 契约到 Promise 与 Effect 双客户端的代码生成机制 opencode httpapi-codegen 深度解析从 HttpApi 契约到 Promise 与 Effect 双客户端的代码生成机制【免费下载链接】opencodeThe open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/openc/opencodeopencode-ai/httpapi-codegen是 opencode 仓库中的一个构建期源码生成包它以HttpApi与 Effect Schema 契约为唯一输入在编译期反射出共享 Contract再分别产出零 Effect 依赖的 Promise 客户端与Effect 原生的富客户端两套 TypeScript 源码。读完本篇你能理解该包compile → emit → write三阶段流水线的每一层设计决策输入展平、成功值解包、流式语义、不可移植 Schema 的拒绝策略以及生成产物如何通过.httpapi-codegen.json清单与 CI 再生成校验来保持仓库内始终一致。包的定位私有、测试即规格、与 Core 解耦README 对该包有三条明确的定位约束README私有包package.json中声明private: true在 API 仍被探索期间不对外发布测试即可执行规格executable specification包的测试用例就是生成器行为的规范定义任何行为变更都必须先体现为测试断言与 OpenCode Core 完全独立生成器本身不依赖 opencode 的任何业务代码所有测试均使用合成的HttpApifixture见 test/fixture.ts从而保证这个通用包可以被独立演进。从 test/fixture.ts 可以看到这个合成契约的覆盖面session组包含无参GET /session/health、带可选 query 的GET /session、带路径参数 { data: A }成功信封 404 声明错误的GET /session/:sessionID、以及返回NoContent的POST .../interruptevent组是 202 状态的 SSE 流端点system组则是topLevel: true的顶层组。这四个维度正好覆盖了生成器需要处理的主要形态。生成流水线compile / emit / write 三阶段README 中Staged API描述了一条纯函数流水线与一个遗留的公共操作并存。对照 src/index.ts 的公开导出各阶段职责如下compile(Api)反射权威 HttpApi 为共享 Contractcompile是唯一接收HttpApi的入口通过HttpApi.reflect回调遍历每个组与端点把传输层细节归一化为中间表示export function compileId extends string, Groups extends HttpApiGroup.Any( api: HttpApi.HttpApiId, Groups, options?: { readonly groupNames?: ReadonlyRecordstring, string readonly endpointNames?: ReadonlyRecordstring, string readonly omitEndpoints?: ReadonlySetstring }, ): Contract它产出的Contract包含按消费者组名分组的端点集合每个端点记录了归一化后的 params/query/headers/payload Schema、输入字段列表含来源通道与可选性、成功形态value/void/stream、声明错误列表以及effectPortable标志。compile是纯函数不触碰文件系统——这正是 README 强调Compiler tests inspect virtual files directly的前提。compile的options是该包唯一允许的命名干预点对应 README 最后一条规则groupNames把宿主侧组名映射为消费者可见组名内部仍保留sourceIdentifier作为传输标识endpointNames显式指定公开端点名未显式指定时端点操作 ID 会被投影为其最终点分片段源码中clientEndpointName取最后一个.之后的部分如session.get→get。生成器不做其他隐式的产品级命名映射。emitPromise / emitEffect / emitEffectImported每个 emitter 独立产出README 规定每个 emitter 拥有自己的公开类型投影共享 Contract而非某个生成的类型包才是共同来源。源码中三个 emit 函数都只消费ContractemitEffect(contract)产出可移植的富 Effect 客户端——每个HttpApiGroup一个自包含模块内嵌重建的运行时 Schema加根client.ts与index.ts若任一端点标记为不可移植effectPortable: false直接抛GenerationError强制走导入路径emitPromise(contract)产出四个文件——types.ts结构化线协议类型、client-error.ts、client.ts直接fetch实现、index.ts支持可选的outputTypes参数把个别输出类型替换为权威导入如 opencode client 包用OpenCodeEventEncoded覆盖 SSE 事件类型emitEffectImported(contract, options)不重建 Schema而是从指定模块导入权威HttpApi三种模式整api、整group、或按endpoints投影端点常量适配器集中在根client.ts中。write带清单与安全检查的落盘write(output, directory)是唯一 Effect 阶段需要FileSystem服务。它执行四步见 src/index.ts#L710-L765校验输出路径扁平、唯一大小写不敏感去重、非绝对路径、非./..、不含/或\且不得占用保留清单名.httpapi-codegen.json读取旧清单只删除上一次由生成器拥有、本次不再产出的文件concurrency: 8对目标已存在的符号链接拒绝写入用 Prettierparser: typescript, semi: false, printWidth: 120格式化后写入最后落一份排序后的清单。README 中Commit generated source for review; CI regenerates and fails when the worktree changes在这条流水线上有了落点因为compile/emit是纯函数CI 重新执行后逐字节比对即可判定漂移。输入模型四个通道展平为一个领域对象README 的第一组 Settled rules 定义了输入契约源码与测试共同验证了它们展平flattenpath、query、header、payload 字段被合并进同一个输入对象。测试flattens transport input channels into one domain input断言POST /session/:sessionIDparamssessionID queryresume headerstraceID payloadprompt编译出的字段顺序为sessionID → resume → traceID → prompt且生成的请求装配代码形如params: { sessionID: input[sessionID] }重名即拒绝跨通道出现同名字段时抛GenerationError(Input field collision: id)测试rejects colliding input names across transport channels三种入参形态Operation.inputMode零字段 → 方法无参数none全部字段可选 → 可选对象optional生成input?: XxxInput装配时用input?[field]访问任一字段的必选 → 必选对象required非结构化index signature输入与无字符串字段名都会被拒绝保证输入对象永远是一个可静态投影的 struct。字段访问统一使用括号记法input[x-example-token]因此 header 名等非标识符字符不会破坏生成的 TypeScript。成功值与错误映射的判定规则这是该包语义密度最高的一组规则每条都能在 test/generate.test.ts 找到对应断言规则实现行为测试锚点解包精确的{ data: A }成功信封isDataEnvelope判定单字段data结构体Effect 端追加Effect.map((value) value.data)Promise 端.then((value) value.data)unwraps an exact data success envelope无内容成功映射为voidHttpApiSchema.NoContent含Created201 等非默认空响应状态→Operation.success: voidPromise 端empty: true时消费响应体并返回undefinedmaps no-content success to void、preserves non-default empty response statuses保留其他单成功值单 Schema 成功原样返回returns a non-envelope success unchanged拒绝多成功契约端点声明多个成功 Schema 时抛Multiple success schemasrejects multiple success shapes多 payload 拒绝一个端点只能有一个 payload Schemarejects multiple payload alternatives流式成功暴露为Stream而非EffectStreamEffect 端通过Stream.unwrap展开Promise 端为惰性AsyncIterablemodels an SSE success as a direct stream错误侧的统一约定是传输失败、意外状态码、响应解码失败都归一到同一个生成的ClientError而声明过的 tagged 错误如 fixture 中 404 的Missing按其原类型 reject/失败。Promise 端ClientError的四个 reason 字面量——Transport、UnexpectedStatus、UnsupportedContentType、MalformedResponse——直接内嵌在emitPromise写出的client-error.ts模板中Effect 端则是Schema.TaggedErrorClass携带Schema.Defect()类型的cause。每个操作的errors列表是声明错误标识符 ∪ ClientError测试maps transport and decode failures to one stable client error明确断言最终错误集合不包含HttpClientError与SchemaError。Promise 客户端零 Effect、结构化线协议类型、直接 fetchemitPromise产出的client.ts是一个自包含的fetch客户端其运行时结构在生成模板中完整可见renderPromiseClientexport function make(options: ClientOptions) { /* baseUrl 可选自定义 fetch */ } interface RequestDescriptor { readonly method: string readonly path: string readonly query?: Recordstring, unknown readonly headers?: Recordstring, unknown readonly body?: unknown readonly successStatus: number readonly declaredStatuses: ReadonlyArraynumber readonly empty: boolean }值得注意的实现细节路径拼接promisePath把:name段替换为${encodeURIComponent(input.name)}测试验证sessionID: a/b生成 URLhttps://example.com/session/a%2Fb路径通配符*被直接拒绝query 序列化appendQuery支持数组重复追加与嵌套对象key[child]语法声明状态短路响应状态若命中declaredStatuses来自httpApiStatus注解缺省 500直接解析 JSON 并按声明错误类型 reject类型守卫isXxx一并导出测试rejects with declared tagged errors and exports a type guard验证了isMissing(error) trueSSE 惰性迭代sseA返回AsyncIterable首次迭代才发起请求测试断言构造时requests 0内部手写事件解析\r\n归一化、data:行拼接、JSON 解析失败抛MalformedResponse缓冲超过 1 MiB 判为畸形响应运行时不会自动重连——这与 README 中Neither runtime reconnects automatically逐字对应无运行时结构校验Promise 类型是结构化、面向线协议的Schema.toEncoded投影后展开为纯 TS 类型品牌类型被擦除、非递归引用被内联展开、Schema.Json替换为本地JsonValue递归定义——测试erases brands from Promise wire types与inlines non-recursive references固定了这些行为。这正是语法解析而非运行时校验的成本调用方拿到的是零依赖的轻量客户端但结构正确性由 Effect 客户端一侧保证。Promise 端还有明确的支持边界assertPromiseEndpointpayload 与成功体必须为 JSON 编码asText/asUint8Array均拒绝、SSE 必须是sseMode: data且 error 为Never、声明错误必须携带字面量判别键_tag或name。任何越界组合都会以GenerationError失败而不是生成一个语义不确定的客户端。Effect 客户端运行时 Schema 与可移植性守门emitEffect走的是相反方向生成自包含的 Effect 客户端。每个组模块如 test/generated/session.ts内嵌重建的HttpApiGroup与全部运行时 Schema通过HttpApiClient.Client.Group拿到原始客户端后再适配为领域方法根client.ts只做组装配见 test/generated/client.ts 中adaptClient与make(options?: { baseUrl })。可精确生成是该侧的硬约束README 的 Reject schemas whose wire/domain transformation cannot be generated exactly 在源码中落地为多层守门normalizeTransport用解码后的 Schema 重建端点再比对编码链sameEncoding逐层递归比较 encoding/checks/context任何藏在标准 HttpApi 编解码器之下的自定义decodeTo转换如测试中yes/no → boolean的 query 布尔都会得到effectPortable: false进而被emitEffect拒绝为Effect schema requires authoritative import——此时应改用emitEffectImported从权威模块导入原始 APImetadataPortable/assertPortable检查注解与 checks 的可移植性generation注解只接受Schema.*或从effect命名空间导入的运行时引用函数型注解如custom: () local与无元数据的 filter 直接判为Unportable schema被篡改的编码链测试用反射replaceEncoding构造toCodecJson(Number)的变体即使转换本身规范也会被sameEncoding比对拒绝。这些拒绝不是防御性告警而是该包的核心设计生成器宁可失败也不产出与权威契约语义不一致的代码。边界通用生成器与产品决策的切分README 的 Boundary 一节划定了包的能力边界值得逐条对照只生成客户端不生成 embedded-only 能力。opencode 的网络形态与内嵌形态共用同一个生成的 Effect 客户端分别挂在网络HttpClient与内存HttpClient传输上内嵌宿主在该客户端之上结构性地扩展同进程能力而不是让 codegen 掺和业务特性全端点生成、无过滤策略通用包生成传入HttpApi的每一个端点生成哪些端点是产品决策。opencode 在调用生成器前就组装好精确的远端 API——packages/client/script/build.ts正是通过groupNames、endpointNames、omitEndpoints三个选项见 client/src/contract.ts 导入完成这一组装再执行compile遗留 API 保留公共的generate(Api, { directory })写富 Effect 输出、仍是需要FileSystem的 Effect与新三阶段 API 并存。命名投影规则也与边界一致传输标识符sourceIdentifier、端点原始名在内部全程保留compile只做消费面组名映射与端点名末段投影杜绝隐式重命名。生成产物的仓库级治理清单、安全路径与 CI 再生成README 中两条治理型规则——提交生成源码、清单跟踪文件——在仓库中有完整实现证据清单机制write维护.httpapi-codegen.json字符串数组排序后写入再生成时仅删除清单中记录、但本次未产出的文件write.test.ts 用FileSystem.makeNoop精确断言了只删除old.ts、保留session.ts的行为以及清单路径本身被保留为私有名路径安全输出路径扁平唯一、拒绝../穿越、拒绝占用清单名、拒绝覆盖既有符号链接大小写不敏感重名同样拒绝测试给出Duplicate output path: CLIENT.tsCI 漂移检测generate.test.ts中的keeps the strict generated-consumer fixture current用例对 test/generated/ 目录逐文件执行再生成 → Prettier 格式化 → 逐字节相等比对这正是 README 所述CI regenerates and fails when the worktree changes在该包内的执行方式。另外README 提到可移植 Effect 输出允许跨组重复 Schema 依赖跨组 Schema 分区延后到实测输出或打包成本要求出现时再处理——这是一条明确的工程决策记录当前实现中每个组模块自包含renderGroup独立收集 slots 并渲染 Schema不追求跨模块去重。在 opencode 中的真实用法client 包的双路生成packages/client是该生成器的真实消费者script/build.ts 完整展示了 README 流水线在仓库内的落地形态import { compile, emitEffectImported, emitPromise, write } from opencode-ai/httpapi-codegen import { ClientApi, endpointNames, groupNames, omitEndpoints } from ../src/contract const contract compile(ClientApi, { groupNames, endpointNames, omitEndpoints }) await Effect.runPromise( Effect.all( [ write( emitPromise(contract, { outputTypes: { events.subscribe: { name: OpenCodeEventEncoded, import: import type { OpenCodeEventEncoded } from opencode-ai/protocol/groups/event, }, }, }), fileURLToPath(new URL(../src/generated, import.meta.url)), ), write( emitEffectImported(contract, { module: ../contract, api: ClientApi }), fileURLToPath(new URL(../src/generated-effect, import.meta.url)), ), ], { concurrency: 2, discard: true }, ).pipe(Effect.provide(NodeFileSystem.layer)), )这一段印证了 README 的全部关键断言同一contract驱动两个 emitter 独立产出Promise 输出落在 client/src/generated/其中 SSE 事件类型通过outputTypes指向 protocol 包的权威编码类型Effect 输出走emitEffectImported从同仓contract.ts导入权威ClientApi落在 client/src/generated-effect/。生成的 Effect 客户端首行注释// Generated by opencode-ai/httpapi-codegen. Do not edit.也是产物可追溯性的直接证据。小结opencode-ai/httpapi-codegen的价值不在于能生成代码而在于一组可验证的生成纪律compile纯函数化让测试只比对虚拟文件write独立为 Effect 让落盘行为可用FileSystem.makeNoop精确断言不可精确生成的 Schema 一律拒绝而非降级生成产物提交入库、由清单与再生成比对维持一致性。对希望在自己的 Effect 项目中为HttpApi同时提供轻量 Promise SDK 与富 Effect SDK 的读者而言src/index.ts 与 test/generate.test.ts 构成了一份以测试为规格的可参考实现。【免费下载链接】opencodeThe open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/openc/opencode创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表