
VidBee 的 oRPC Contract-First 开发实践契约先行让下载 API 与前端类型同源【免费下载链接】VidBeeDownload video and audio from YouTube , TikTok , Twitter , Instagram , Facebook , Twitch , Bilibili , and 1000 sites—or import local media. Create searchable transcripts on your computer, then summarize, translate, or ask questions with your preferred AI provider.项目地址: https://gitcode.com/GitHub_Trending/vi/VidBee本文以 .agents/skills/orpc-contract-first/SKILL.md 为骨架结合 VidBee 仓库中真实落地的 oRPC 契约实现下载引擎downloaderContract、订阅subscriptionContract及其 API/Web 全链路系统讲解 contract-first 模式下如何定义契约、注册路由、生成类型化客户端与 hooks并给出可直接复用的工程化清单。读完你将掌握一套「一份契约前后端类型同源、跨主机复用」的 RPC 层组织方法。一、什么是 oRPC Contract-FirstoRPC 是一套以契约Contract为核心的 RPC 框架先用独立的契约对象描述「路径、方法、输入、输出」再分别由服务端implement实现、客户端createORPCClient消费。契约先行Contract-First意味着类型系统是 API 的唯一事实来源single source of truth两端都不再手写重复的类型定义。在 VidBee 中这一模式承担了关键职责Web 端通过它驱动下载队列、播放列表、订阅源管理服务端apps/api通过同一份契约暴露 HTTP RPC 端点。SKILL 文档.agents/skills/orpc-contract-first/SKILL.md明确要求在「新建 API 契约、新增 service 端点、用 TanStack Query 集成类型化契约、迁移遗留 service 调用」四类场景下统一使用该模式。二、契约层目录结构按领域划分、杜绝 barrel 文件SKILL 给出的理想结构是按领域拆分的契约目录web/contract/ ├── base.ts # Base contract (inputStructure: detailed) ├── router.ts # Router composition type exports ├── marketplace.ts # Marketplace contracts └── console/ # Console contracts by domain ├── system.ts └── billing.tsVidBee 落地时结合 monorepo 做了两处关键演进契约下沉到共享包而非放在前端源码里实现「一契约多主机消费」。下载域契约位于 packages/downloader-core/src/contract.ts订阅域契约位于 packages/subscriptions-core/src/contract.ts。后者的文件头注释明确写着desktop、api、cli 必须直接消费该契约任何主机自定义路由或派生 schema 都视为契约 bug——这正是 contract-first 的纪律性体现。严格无 barrel。所有导入都指向具体文件例如import { downloaderContract } from vidbee/downloader-core、import type { subscriptionContract } from vidbee/subscriptions-core/contract见 apps/web/src/lib/orpc-client.ts。三、三步工作流定义契约 → 注册路由 → 生成 hooks3.1 创建契约oc.input(...).output(...)描述端点在领域文件SKILL 中为web/contract/console/{domain}.ts中用oc组装输入输出。VidBee 的下载契约是完整范本packages/downloader-core/src/contract.tsimport { oc } from orpc/contract export const downloaderContract { status: oc.output(StatusOutputSchema), videoInfo: oc.input(VideoInfoInputSchema).output(VideoInfoOutputSchema), playlist: { info: oc.input(PlaylistInfoInputSchema).output(PlaylistInfoOutputSchema), download: oc.input(PlaylistDownloadInputSchema).output(PlaylistDownloadOutputSchema) }, downloads: { create: oc.input(CreateDownloadInputSchema).output(CreateDownloadOutputSchema), list: oc.output(ListDownloadsOutputSchema), cancel: oc.input(CancelDownloadInputSchema).output(CancelDownloadOutputSchema), pause: oc.input(PauseDownloadInputSchema).output(PauseDownloadOutputSchema), resume: oc.input(ResumeDownloadInputSchema).output(ResumeDownloadOutputSchema), retry: oc.input(RetryDownloadInputSchema).output(RetryDownloadOutputSchema) }, settings: { get: oc.output(GetWebSettingsOutputSchema), set: oc.input(SetWebSettingsInputSchema).output(GetWebSettingsOutputSchema) } // status / videoInfo / playlist / downloads / history / files / settings / engines ... }要点无参端点如status、downloads.list、settings.get只声明output有参端点通过oc.input(schema).output(schema)声明schema 全部来自 Zodpackages/downloader-core/src/schemas.ts例如DownloadTypeSchema z.enum([video, audio])、DownloadTaskSchema里用z.url()、z.number().optional()描述完整下载任务结构SKILL 中提到的typeT()辅助函数是可选做法VidBee 直接以 Zod schema 承担类型角色实际效果相同一份 schema 同时产出运行时校验与静态类型。3.2 注册进 Router按 API 前缀嵌套SKILL 要求在web/contract/router.ts中直接导入领域文件无 barrel并按 API 前缀分组嵌套如/billing/*→billing: {}。VidBee 的订阅契约packages/subscriptions-core/src/contract.ts展示了完整的分组与合并写法export const subscriptionContract { list: oc.output(SubscriptionListOutputSchema), get: oc.input(SubscriptionIdInputSchema).output(SubscriptionWithItemsSchema), resolve: oc.input(ResolveInputSchema).output(ResolvedFeedSchema), add: oc.input(SubscriptionCreateInputSchema).output(SubscriptionWithItemsSchema), update: oc .input(SubscriptionIdInputSchema.merge(SubscriptionUpdateInputSchema)) .output(SubscriptionWithItemsSchema), remove: oc.input(SubscriptionIdInputSchema).output(VoidOutputSchema), refresh: oc.input(RefreshInputSchema).output(SubscriptionWithItemsSchema), itemsList: oc.input(ItemsListInputSchema).output(SubscriptionItemsListOutputSchema), itemsQueue: oc.input(ItemsQueueInputSchema).output(ItemsQueueOutputSchema) }注意update用 Zod 的.merge()组合「id 可更新字段」这是对「同一路由需要多 schema 输入」的优雅解法。嵌套分组playlist: { info, download }、downloads: { create, list, ... }则直接映射 REST 风格的资源前缀让契约结构一眼可读。3.3 创建 hooks类型化 queryKey 与 API 调用SKILL 规定 hooks 中queryKey 用consoleQuery.{group}.{contract}.queryKey()API 调用用consoleClient.{group}.{contract}()。VidBee 的客户端实例apps/web/src/lib/orpc-client.ts是这一层的基石import { createORPCClient } from orpc/client; import { RPCLink } from orpc/client/fetch; import type { ContractRouterClient } from orpc/contract; import type { downloaderContract } from vidbee/downloader-core; import type { subscriptionContract } from vidbee/subscriptions-core/contract; const rpcUrl ${apiUrl}/rpc; export const orpcClient: ContractRouterClienttypeof downloaderContract createORPCClient(new RPCLink({ url: rpcUrl })); export const subscriptionsClient: ContractRouterClienttypeof subscriptionContract createORPCClient(new RPCLink({ url: ${rpcUrl}/subscriptions }));用ContractRouterClienttypeof downloaderContract显式标注类型客户端方法与契约 1:1 对应写错方法名、传错参数会在编译期直接报错两个契约各自挂载不同的 URL 前缀/rpc与/rpc/subscriptions与下文服务端路由一一对应apiUrl支持VITE_API_URL环境变量覆盖SSR 场景回退到VIDBEE_API_URL_INTERNAL || http://api:3100浏览器端则使用window.location.origin。真实的 hook 用法见 apps/web/src/hooks/use-web-settings.tsconst result await orpcClient.settings.get(); // 读取远端设置 void orpcClient.settings.set({ settings }); // 防抖 200ms 后回写调用失败时捕获异常并保留本地 localStorage 兜底WEB_SETTINGS_STORAGE_KEY同时监听storage事件做跨标签页同步——这是契约类型安全之上、业务健壮性层面的补充。四、服务端落地implement 路由器挂载 错误语义4.1implement(contract)强制形状对齐服务端用implement消费同一份契约。apps/api的下载路由器apps/api/src/lib/rpc-router.ts开头即const os implement(downloaderContract) export const rpcRouter os.router({ status: os.status.handler(() { ... }), videoInfo: os.videoInfo.handler(async ({ input }) { ... }), downloads: { create: os.downloads.create.handler(async ({ input }) { ... }), // ... }, // ... })implement的约束力在于契约里没有的路由写不进去契约里有的路由漏实现会直接类型报错。handler 接收类型化的{ input }返回值必须匹配对应 output schema彻底消灭「前后端字段名漂移」。订阅路由器apps/api/src/lib/subscriptions-router.ts同样是implement(subscriptionContract)os.router({...})每个 handler 1:1 转发到单例SubscriptionsApi并在边界上统一翻译错误。4.2 错误语义ORPCError 领域常量契约先行同样覆盖错误面。两个路由器都用ORPCError表达结构化错误业务错误new ORPCError(NOT_FOUND, { message: Subscription not found. })订阅不存在冲突语义重复订阅源时抛出ORPCError(CONFLICT, { message: SUBSCRIPTION_DUPLICATE_FEED_ERROR })其中SUBSCRIPTION_DUPLICATE_FEED_ERROR是定义在vidbee/subscriptions-core的共享常量服务端与前端或 CLI据此统一判断「重复订阅」兜底语义new ORPCError(INTERNAL_SERVER_ERROR, { message: toErrorMessage(error, fallback...) })并谨慎保留原始 message。安全相关的边界也不含糊rpc-router.ts中删除文件会先校验目标路径必须位于受管下载目录内否则抛ORPCError(FORBIDDEN, ...)。4.3 挂载到 FastifyRPC 处理器与 OpenAPI 文档自动生成apps/api/src/server.ts 将契约与传输层解耦const rpcHandler new RPCHandler(rpcRouter) const subscriptionsRpcHandler new RPCHandler(subscriptionsRouter) fastify.all(/rpc/*, async (request, reply) { await rpcHandler.handle(request, reply, { prefix: /rpc }) }) fastify.all(/rpc/subscriptions/*, async (request, reply) { await subscriptionsRpcHandler.handle(request, reply, { prefix: /rpc/subscriptions }) })值得注意的两点工程细节路由最具体匹配优先。注释明确说明/rpc/subscriptions/*必须在泛化的/rpc/*之前注册Fastify 才会按最具体规则命中订阅处理器契约即文档。同一份rpcRouter喂给OpenAPIHandler配合ZodToJsonSchemaConverter自动生成 Swagger 文档docsPath: /docs与 OpenAPI 规范specPath: /openapi.json文档永远与实现同步无需手工维护。五、关键规则速查SKILL 五条铁律 × 仓库印证SKILL 规则含义VidBee 源码印证输入结构恒为{ params, query?, body? }统一输入形状避免散装参数Zod 输入 schema 集中定义于 packages/downloader-core/src/schemas.ts路径参数用{paramName}与params对象对齐契约里显式声明路径变量客户端经RPCLink把input序列化到/rpc端点apps/web/src/lib/orpc-client.tsRouter 按 API 前缀分组嵌套/billing/*→billing: {}playlist.info/download、downloads.create/list、files.*分组packages/downloader-core/src/contract.ts/rpc/subscriptions独立契约独立处理器禁止 barrel 文件直接导入具体文件保持引用可追溯import { downloaderContract } from vidbee/downloader-core、import type { subscriptionContract } from vidbee/subscriptions-core/contract类型从/types/导入用typeT()辅助契约类型集中管理、可推导VidBee 以 Zod schemaz.object/z.enum/z.merge直接产出类型效果等价六、类型导出与跨端复用SKILL 给出的类型导出范式export type ConsoleInputs InferContractRouterInputstypeof consoleRouterContractVidBee 中的等价物是ContractRouterClient客户端用ContractRouterClienttypeof downloaderContract把整个契约「翻译」成可调用对象类型见 apps/web/src/lib/orpc-client.ts而订阅契约同时导出export type SubscriptionContract typeof subscriptionContractpackages/subscriptions-core/src/contract.ts供各主机引用同一类型源。这种「契约定义在共享包 → 各端按需导出类型」的做法正是 monorepo 下 contract-first 的核心收益desktopIPC 自动化、apiHTTP RPC、cli本地命令三个主机共同消费同一契约订阅契约文件头注释强调的「字节级一致的 schema diff 门禁」保证了任何主机私自扩展都会在代码评审与类型检查阶段被拦截。七、完整链路复盘以 settings 为例把 SKILL 的三步工作流映射到真实请求上一条设置读写链路是这样的契约settings: { get: oc.output(...), set: oc.input(SetWebSettingsInputSchema).output(GetWebSettingsOutputSchema) }packages/downloader-core/src/contract.ts实现os.settings.get.handler读取webSettingsStoreos.settings.set.handler保存后联动副作用——自动转写开关setApiAutoTranscribe、转写并发数applyApiTranscriptionConcurrency、yt-dlp 下载镜像与语言applyEngineDownloadSettingsapps/api/src/lib/rpc-router.ts传输RPCHandler挂载于fastify.all(/rpc/*)CORS 仅放行GET/POST/OPTIONSapps/api/src/server.ts客户端orpcClient.settings.get() / .set({ settings })类型由ContractRouterClient保障apps/web/src/lib/orpc-client.tsHookuseWebSettings首屏拉取远端设置、本地防抖回写、失败降级到 localStorageapps/web/src/hooks/use-web-settings.ts。从契约到 UI 状态类型在每一层都被复用而不是重写——这就是 contract-first 在生产项目中的实际价值。八、实践清单与注意事项契约只放在共享包下载域在packages/downloader-core订阅域在packages/subscriptions-core前端/服务端/CLI 一律从包入口导入禁止在任一主机内派生 schema输入输出都用 Zod运行时校验防脏数据入队 静态类型编译期纠错一次解决复杂输入用.merge()组合分组命名对齐 API 前缀playlist、downloads、history、files、settings、engines一目了然也便于未来按前缀迁移路由错误统一走 ORPCError用NOT_FOUND / CONFLICT / FORBIDDEN / INTERNAL_SERVER_ERROR表达语义业务常量如SUBSCRIPTION_DUPLICATE_FEED_ERROR放共享包供各端判断文档零维护接入OpenAPIHandler后/docs与/openapi.json由契约自动生成路由注册顺序敏感子前缀/rpc/subscriptions/*先于泛化路由/rpc/*注册依赖 Fastify 的最具体匹配规则迁移存量代码新增端点走契约流程存量遗留调用逐条迁移并复用已有 queryKey/客户端方法每次迁移都以类型检查通过为完成标准。【免费下载链接】VidBeeDownload video and audio from YouTube , TikTok , Twitter , Instagram , Facebook , Twitch , Bilibili , and 1000 sites—or import local media. Create searchable transcripts on your computer, then summarize, translate, or ask questions with your preferred AI provider.项目地址: https://gitcode.com/GitHub_Trending/vi/VidBee创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考