Puter API 贡献指南:从 Controller 与 Driver 到 puter.js 的七步落地法
本文为 Puter 官方 API 贡献文档(doc/contributing-apis.md)的扩展解读,面向希望在 Puter 中新增或维护一个「公共 API」的贡献者与 AI Agent。读完本篇,你将掌握:如何判断 API 该放进核心还是扩展层、Controller 与 Driver 两种入口的选型与注册方式、各类鉴权与计费门禁(requireAuth / requireSubscription / requireReputation / requireCredits 等)的真实语义,以及覆盖后端、SDK、类型、文档、测试与安全检查的完整交付清单。
Puter 的「公共 API」指一切可被外部应用调用的表面:HTTP 端点、/drivers/* RPC 方法,以及包裹它们的 puter.js 方法。由于 puter.js 以无版本钉扎的方式从固定线上地址长期服务(文档原文表述为从 https://js.puter.com/v2/ 提供服务且不做版本固定),每一个既有应用都会在你部署的瞬间拿到你的改动——这决定了整篇指南的第一原则。
规则零:不要破坏调用方
puter.js 和它底下的后端端点都具备同一性质:任何可观察行为(参数处理、响应字段、错误码、排序顺序)都可能已经被某个现网应用依赖。因此:
- 所有改动默认必须向后兼容,除非维护者事先明确同意了破坏性变更;
- 响应字段一经发布即为永久承诺——只返回调用方需要的内容,不多不少;
- 错误码使用稳定的
snake_case命名。
配套文档:doc/architecture.md(后端分层架构)、doc/pagination.md(列表 API 的分页约定)、src/puter-js/tests/api/README.md(SDK 测试环境说明)。
核心还是扩展:API 的归属决策
第一个决策是 API 放在哪里。官方判据非常清晰:如果核心功能从不调用它,优先做成扩展(extension)而不是接入核心。
扩展位于 extensions/ 目录,与核心分层栈平行,通过 extension.import(...) 惰性访问核心实例,并通过 registerDriver / registerController 或轻量路由助手注册自己的表面:
const services = extension.import('service');
const stores = extension.import('store');
extension.registerDriver('myFeature', MyFeatureDriver); // 一等 driver
extension.post('/my-feature/frobnicate', opts, handler); // 或者普通路由
扩展内部同样遵循 driver/controller → service → store 的分层结构;除非它真的只需要两三个路由处理器——那时轻量的 extension.get/post/... 助手就够了。这些路由助手接受与核心 Controller 完全相同的 RouteOptions,所以 subdomain、requireAuth、adminOnly、body parser 等都能照常工作(参见 doc/architecture.md 的 Extensions 一节)。
依赖方向测试:把扩展移除后 Puter 必须仍然能正常工作。一旦核心开始需要调用你的 API,它就该进核心。extensions/whoami.ts 是文档点名的反面教材——一个对所有已认证客户端「承重」的「扩展」,架构文档把它列为决策时要记住的警示案例;而 extensions/thumbnails.ts、extensions/serverInfo.ts、extensions/devWatcher.ts 则是「非关键、可整体移除」的正面范例。
新增 API 的七步法
官方流程要求七步全部走完,PR 才算完整。
第 1 步:先设计表面
写代码之前,先画出方法签名、选项、返回形态与错误分支,并找到两三个最相似的既有 API,对齐它们的约定:
- 新的参数与字段名一律
camelCase(既有的snake_case名称保持原位不动); - 任何返回列表的 API 遵循 doc/pagination.md 的约定:入参
limit/cursor,出参{ items, cursor, total }信封。该文档还规定:cursor是不透明 base64 JSON、仅由后端在 src/backend/util/pagination.ts 中产生与消费;请求未带任何分页参数时,端点必须永远返回旧式的裸数组全量结果,保证老客户端不坏; - 优先使用 options 对象而非不断增长的位置参数,但在兄弟 API 已有惯例时,保留「单参数快捷调用」形态。
第 2 步:后端实现
按分层栈(doc/architecture.md)实现:边缘层是 controller 或 driver,业务逻辑在 service,持久化在 store。边缘层负责解析与校验输入、应用各类门禁;service 假定调用方已获授权。
Controller 还是 Driver?
两者都是被支持的一等做法,选型依据是控制粒度:
| 维度 | Controller | Driver |
|---|---|---|
| 适用场景 | 需要精细控制 URL 形状、HTTP 动词、逐路由门禁、响应与流式格式 | API 是一组实现命名接口(puter-kvstore、puter-chat-completion…)的 RPC 方法,通用的单一调用信封、可互换实现、逐方法策略机制已够用 |
| 基类 / 注册 | 继承 PuterController(src/backend/controllers/types.ts),核心注册于 src/backend/controllers/index.ts |
继承 PuterDriver(src/backend/drivers/types.ts),核心注册于 src/backend/drivers/index.ts |
| 声明方式 | @Controller(prefix) 类装饰器 + @Get/@Post/… 方法装饰器(src/backend/core/http/decorators.ts),或直接覆写 registerRoutes(router) 命令式注册 |
@Driver(interfaceName, opts) 装饰器(src/backend/drivers/decorators.ts),或在类上命令式声明 driverInterface / driverName / isDefault |
| 扩展接入 | extension.registerController(...) 或普通路由助手 |
extension.registerDriver(...) |
从源码看,@Controller 装饰器(src/backend/core/http/decorators.ts)把路径前缀存到原型元数据上,并为未自定义 registerRoutes 的类安装一个默认遍历器,把方法装饰器收集到的路由逐条喂给 PuterRouter——所以「纯装饰器、无方法体」的 Controller 可以完全不加命令式代码。@Driver 装饰器(src/backend/drivers/decorators.ts)则在类初始化阶段就把 rateLimit、concurrent、requireSubscription、requireReputation 等策略块做急切校验(eager validation),使配置错误在模块加载时就暴露,而不是等第一个请求打到路由才报错。同一接口允许多个实现并存(例如多个 puter-chat-completion 实现),default 标记缺省实现。
中间件与门禁
- Controller 路由(扩展路由同形)接受 RouteOptions:鉴权门禁(
requireAuth、requireUserActor、noUserSession、adminOnly、allowedAppIds及 access-token 控制)、subdomain路由、逐路由rateLimit、body parser 以及任意附加middleware。这些鉴权字段语义微妙且默认拒绝,选择前先读每个字段的 JSDoc。 - Driver 方法的策略来自
@Driver选项:逐方法rateLimit(limit / window / backend)、concurrent在途并发上限(可bySubscription按订阅档缩放)、requireSubscription、requireReputation、noUserSession,由/drivers/call表面统一执行。 - 只允许付费账户到达的表面声明
requireSubscription——Controller 上是路由选项,Driver 上是逐方法@Driver({ requireSubscription })块(/drivers/call是共享单路由,驱动的要求无法写在路由选项里)。取值语义:true接受任何非免费计划(扩展注册的计划也算,核心无需点名);字符串数组如['business', 'pro']只接受所列计划;false等价于不声明;空数组是启动错误而非静默失效——因为它读起来像「仅订阅者」却实际放行所有人。默认关闭,判定来自计量服务缓存的逐 actor 订阅,成本只是一次 map 查找。它与requireCredits的区别:前者问的是账户处于哪个计划,后者问的是预算还剩多少。没有任何付费计划的部署(自托管)可用meteringEnforcement.subscriptions: false整体关闭。 - 只允许信任度足够账户到达的表面声明
requireReputation(同样是路由选项或@Driver块)。取值只命名一个层级(tier);该层级对应多少分由配置项reputationGate.tiers决定,因此门槛可以随部署调参而不必改表面代码。当前配置未定义的层级惰性失效(人人放行)——一个从不给账户计分的安装不应拿它从未计算的分数挡流量;reputationGate.enabled: false可一次性停掉所有已声明的门禁。该机制是选入式、隐含requireAuth,拒绝时返回裸 403reputation_required,不透露分数与层级。文档还指出:目前代码树中尚无任何地方真正声明它——机制先于启用落地。 - 账户必须已验证特定因子——
requireVerified(邮箱,且仅在strict_email_verification_required下)、requirePhoneVerified、requireCardVerified(见 src/backend/core/http/types.ts)。这些是选入式的,且要求因子确实已完成验证,区别于默认开启的那道门禁——后者只挡掉「仍挂着滥用工具申请触发的待验证状态」的账户。 - 替调用方消耗计量资源的端点——搬运文件内容、发起对象存储请求等任何账户会被计费的动作——应声明
requireCredits: true,在 handler 运行前就用 402 挡掉预算耗尽的账户。只描述或删除资源的端点则刻意不声明:预算耗尽的账户仍要能看到自己有什么、能清理、能进账单页。Driver 没有路由选项可写这项,因此自行调用assertActorHasCredits(src/backend/services/metering/enforcement.ts)——src/backend/drivers/kv/KVStoreDriver.ts 就是每个方法统一执行一次的范例。
第 3 步:puter.js
- 在 src/puter-js/src/modules/ 对应模块中添加方法,对齐同模块兄弟方法的调用约定:返回 promise;局部惯用模式是「位置参数快捷形式 + options 形式」双轨。
- 廉价前置条件在客户端就地校验并抛出
{ message, code }对象;后端错误原样透传,不要吞掉或重新包装。
第 4 步:类型
- 在实现处用 JSDoc 描述方法:
@param/@returns写实现,每种被接受的调用形态一个@overload块,新形态用@typedef {Object}+@property。JSDoc 是唯一事实源——声明文件由它生成,无需手工同步。 - 多个文件需要的形态放进模块的
types.js(如 src/puter-js/src/modules/kv/types.js);跨模块共享的放 src/puter-js/src/lib/types.js;只有一个消费者的形态可以留在原地。 - 运行
npm run check:puterjs:types——它生成声明并在不启用skipLibCheck的情况下对发布面做类型检查。永远不要手编src/puter-js/types/下的任何文件:那是 gitignore 的构建产物,随 SDK 构建生成、打进 npm 包,下次构建即被覆盖。 - 若消费者应能直接 import 新类型,在 src/puter-js/index.d.ts 中命名它。该文件是包内唯一手写的声明文件:它决定什么是公开的,不转导出其他任何东西。
第 5 步:文档
在 src/docs/src/<Area>/<method>.md 添加方法页——frontmatter(title、description、platforms)、语法、参数、返回值,以及至少一个可运行示例——并更新所属区域总览页(<Area>.md)。直接复制既有页面的结构即可,例如 src/docs/src/KV/add.md 这样的叶子页面与其所在区域。
第 6 步:测试
- 后端:与源码同置的 Vitest 测试;优先使用内存测试服务器(src/backend/testUtil.ts 中的
setupPuterTestEnv)而不是打 mock。 - SDK:把用例加进 src/puter-js/tests/api/suites/
<area>.suite.ts(新套件需在suites/index.ts注册)。一个套件同时跑在 node、浏览器与 workerd 三个平台(npm run test:puterjs)——永远不要写按平台分叉的测试;且 runner 执行的是构建产物,需先npm run build:workerLib重新构建。仓库现存的套件覆盖 ai、apps、auth、components、events、fs、hosting、kv、net、os、perms、sharing、system、util、workers 等区域。 - 桌面渲染 UI(
puter.ui.*):按 src/puter-js/TESTING.md 为每个功能补 Playwright spec。
第 7 步:安全检查
开 PR 前逐行扫 diff:错误与日志不泄漏内部实现、响应不过度宽泛、鉴权门禁齐备。凡涉及鉴权、权限或数据导出的改动,在 PR 描述中显式标注。
维护既有 API
变更默认是增量的:
- 新参数必须可选,且默认值精确复现旧行为;
- 永不重命名、改用途或移除既有参数、响应字段、错误码;不改类型、顺序保证、以及「字段何时出现」的规则;
- 可能让既有调用方意外的新行为,放到选入式开关(opt-in flag)后面;
- 文档、类型、测试与行为变更同一个 PR 提交——签名变了文档没变就是 bug,文档是用户据以编码的契约;
- bug 修复必须附带一个「修复前会失败」的回归测试。对「改变可观察行为」的修复保持警惕:可能有人依赖这个 bug,拿不准就问维护者。
破坏性变更稀少且刻意,顺序固定:维护者明确签核 → 文档化的迁移路径 → 上线计划(通常是先加新表面,再废弃旧表面,旧表面很久以后才移除,甚至永不移除)。重构的副产物绝不允许构成破坏。
废弃:旧表面继续可用。在类型声明中标 @deprecated,在其文档页注明替代项,并在示例中停止使用它。移除是另一个独立的、需维护者批准的决定。
完成定义(Definition of Done)
对照官方清单逐项核验,全部勾掉才算完成:
- [ ] 向后兼容(或破坏已被明确批准)
- [ ] 归属正确:核心从不调用它则进扩展;无论放哪都遵守分层结构
- [ ] puter.js 方法符合兄弟方法约定;错误是
{ message, code }且 code 稳定 - [ ] 类型已更新且与运行时行为一致
- [ ] 文档页与区域总览已更新,且含可运行示例
- [ ] 测试齐备:后端 + 三平台 SDK 套件(桌面渲染 UI 另有 e2e)
- [ ] 已对 diff 做完安全检查
- [ ] 已端到端实际跑过一遍
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00