关于 spree-typescript-sdk
spree-typescript-sdk 是面向 Spree 6 店面、管理和卖家 API 的 TypeScript SDK 集合,供构建店面和集成的开发者使用。它提供统一的请求接口,并包含重试、幂等性支持及可选的 Zod 模式。
主要功能
- 01覆盖店面、管理和卖家 API
- 02提供资源 CRUD 与领域操作
- 03支持令牌认证及请求级选项
- 04内置重试、错误规范化和幂等性
- 05提供生成类型及可选 Zod 模式
适用场景
- 01构建 Spree 店面与结账流程
- 02开发管理后台或卖家界面
- 03在集成中调用管理 API
- 04验证 API 响应并处理 Webhook
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ spree/ agent-skills --skill spree-typescript-sdk安装后在 Claude Code 中输入/spree-typescript-sdk确认已加载。
详细介绍
Spree TypeScript SDK
| 包 | 接口 | 身份验证 | 版本 |
|---|---|---|---|
@spree/sdk | Store API(/api/v3/store) | Publishable key + 可选的客户 JWT / 访客令牌 | 2.x(当前为 2.0 beta) |
@spree/admin-sdk | Admin API(/api/v3/admin) | Secret key 或员工 JWT | 1.x beta |
@spree/seller-sdk | Seller API(/api/v3/seller) | Seller JWT + seller ID(createSellerClient、setSeller) | 1.x beta。单例使用与 Admin SDK 相同的 get/update 方法(seller.me.get()、seller.me.update({ selected_locale })、seller.profile.update(…))。参见 spree-marketplace |
@spree/sdk-core | 共享 HTTP、重试、错误 | — | 内部使用,从不发布。请勿导入 |
这三个 SDK 都采用相同的结构,并共享 SpreeError、重试、幂等性和 request 逃生通道。它们面向 Spree 6。Spree 6 店面需要 @spree/sdk 2.x。在 6.0 正式发布之前,这些 SDK 和 dashboard 包会以预发布版本形式发布到 npm 的 beta dist-tag(pnpm add @spree/sdk@beta);而 @spree/cli 和 create-spree-app 为稳定版(3.1.0 / 2.1.0,与 gems 6.0.0.beta4 同期发布)。
@spree/sdk 还提供 @spree/sdk/types(生成的类型)、@spree/sdk/zod(Zod 模式)和 @spree/sdk/webhooks(签名验证,仅限 Node)。Admin 和 Seller SDK 也提供对应的 /types,并从 1.0.0-beta.3 之后的版本起提供 /zod 子路径(@spree/admin-sdk/zod、@spree/seller-sdk/zod)。如果子路径无法解析,说明已安装的版本早于该版本。模式命名为 <Type>Schema,并由 API 序列化器生成。zod(>= 3)是可选的 peer dependency,因此请自行安装,以便在运行时验证响应:
import { OrderSchema } from '@spree/admin-sdk/zod'
const order = OrderSchema.parse(await adminClient.orders.get('or_xxx')) // throws on a shape mismatch
@spree/sdk — Store API
import { createClient } from '@spree/sdk'
const client = createClient({
baseUrl: 'https://shop.example.com',
publishableKey: 'pk_xxx',
// optional defaults, sent as X-Spree-* headers
country: 'DE', locale: 'de', currency: 'EUR', channel: 'online',
})
const { data: products, meta } = await client.products.list({ name_cont: 'shirt', limit: 24, expand: ['default_variant'] })
const product = await client.products.get('classic-tee') // id or slug
设置器会更改后续请求的默认值:client.setLocale('fr')、setCurrency、setCountry、setChannel。每个客户端实例都有自己的默认值,因此服务端渲染客户端和浏览器客户端必须分别设置。
方法命名
list(params?, options?)、get(idOrSlug, params?, options?)、create(body, options?)、update(id, body, options?)、delete(id, options?)。方法始终为 get(绝不会是 show)和 delete(绝不会是 destroy)。嵌套资源的第一个参数为父级 ID:client.carts.items.create(cartId, body, options)。
Store SDK 仅公开 Store API 允许使用的内容。如果某个方法无法通过类型检查,则表示该接口不提供此方法。
| 资源 | 提供的内容 |
|---|---|
products、categories、collections(+ collections.products.list)、sellers | 商品目录读取(用于筛选项的 products.filters) |
markets、countries、currencies、locales、channel、policies、deliveryMethods | 上下文和参考数据 |
carts(+ items、discountCodes、giftCards、fulfillments、payments、paymentSessions、storeCredits)、carts.associate、carts.complete | 购物车和结账 |
orders.get(+ returns、claims) | 已完成的订单 |
auth、customers.create、passwordResets、customer.*(profile、addresses、creditCards、giftCards、storeCredits、digitalLinks、orders、paymentSetupSessions) | 账户 |
wishlists(+ items) | 心愿单 |
newsletterSubscribers(create、verify、requestUnsubscribe、delete) | 双重确认订阅的新闻简报 |
account.companies、companies(+ addresses、members、invitations、orders)、companyInvitations(lookup、accept) | B2B 自助服务(参见 spree-b2b)。companies.members.create(id, { customer_email }) 始终返回一个 CompanyInvitation |
尚无封装方法的端点(例如 /customers/me/data_requests、购物车的 tax_identifier、po_document)可通过 client.request 访问(见下文)。
身份验证、访客令牌和单次请求选项
// Customer login: tokens aren't stored on the client; pass them per request
const { token, refresh_token, user } = await client.auth.login({ email, password })
const { data: orders } = await client.customer.orders.list({}, { token })
const fresh = await client.auth.refresh({ refresh_token }) // rotates both tokens
await client.auth.logout({ refresh_token })
// Third-party IdP (strategy registered on the server)
await client.auth.login({ provider: 'external_idp', token: idpJwt })
RequestOptions:token(Bearer JWT)、spreeToken(访客购物车 / 订单令牌,通过 X-Spree-Token 发送)、idempotencyKey、headers。
购物车 → 订单
let cart = await client.carts.create()
const opts = { spreeToken: cart.token } // or { token } for a signed-in customer
cart = await client.carts.items.create(cart.id, { variant_id: 'variant_xxx', quantity: 1 }, opts)
cart = await client.carts.update(cart.id, { email, shipping_address, use_shipping: true }, opts)
// cart.requirements → [{ step, field, code, message }] (what still blocks completion)
const result = await client.carts.complete(cart.id, opts) // Order (or_…, order.cart_id === cart.id) | OrderGroup (ogrp_…)
- 完成后,购物车将不再出现在购物车端点中。
client.carts.get(cartId)会抛出 404SpreeError(cart_not_found)。在确认页面上,或在基于重定向的付款后,读取client.orders.get(cartId, {}, opts)。订单端点接受购物车 ID,并使用相同的访客令牌解析对应订单。 - 根据
cart.requirements驱动结账 UI,而不是使用状态机(参见spree-checkout)。 Order提供number、cart_id、coupon_code、payment_status、fulfillment_status、总额和display_*。Store 序列化器没有state和status。carts.complete的类型为Promise<Order | OrderGroup>:多卖家市场的购物车完成后会变为订单组(ogrp_…,包含orders[])。使用从@spree/sdk导出的isOrderGroup类型守卫缩窄类型——const orders = isOrderGroup(result) ? result.orders : [result]。非市场商店始终返回Order,但 TypeScript 仍要求你缩窄类型。(@spree/admin-sdk有自己的isOrderGroup,用于管理端完成操作。)
金额类型为 string | null
金额是十进制字符串("29.99"),并带有对应的 display_* 字段。在对访客隐藏价格的渠道中,金额为 null,生成的类型也声明为 string | null。请处理 null,不要盲目使用 parseFloat。
列表参数(Ransack)
await client.products.list({ page: 2, limit: 50, name_cont: 'shirt', sort: '-created_at', expand: ['media'], fields: ['name', 'slug'] })
除 page/limit/sort/expand/fields 之外的键都会变为 q[...] 谓词(name_cont → q[name_cont];数组会添加 [])。不要将谓词嵌套在 filter 键下。
错误
import { SpreeError } from '@spree/sdk'
try { await client.carts.items.create(cartId, { variant_id, quantity: 1 }, opts) }
catch (err) {
if (err instanceof SpreeError) {
err.status // 422
err.code // 'insufficient_stock' | 'validation_error' | ...
err.details // { quantity: ['...'] } (attribute keys; 'base' = form-level)
}
throw err
}
重试与幂等性
默认重试 2 次,采用指数退避和抖动;遇到 429/500/502/503/504 和网络错误时重试,并遵循 Retry-After。启用重试时,变更请求会自动添加 Idempotency-Key,因此重试 POST 不会重复执行操作。使用 retry: { maxRetries, baseDelay, maxDelay, retryOnStatus, retryOnNetworkError } 进行配置,或使用 retry: false 关闭重试。传入 fetch,以支持 Next.js 缓存、追踪或测试。
@spree/admin-sdk — 管理 API
import { createAdminClient } from '@spree/admin-sdk'
// Integration (server only)
const admin = createAdminClient({ baseUrl, secretKey: process.env.SPREE_SECRET_KEY! })
const { data: orders } = await admin.orders.list({ status_eq: 'placed', sort: '-completed_at' })
const order = await admin.orders.get('or_xxx', { expand: ['items', 'payments', 'fulfillments'] })
await admin.orders.cancel('or_xxx', { cancel_reason_id: 'ocr_xxx', cancel_note: 'Customer request' })
await admin.orders.refunds.create('or_xxx', { payment_id: 'py_xxx', amount: '25.00', refund_reason_id: 'rr_xxx' }) // refund_reason_id, not reason_id
- 密钥:受作用域限制。缺少作用域时会抛出 403,并包含
details.required_scope。请创建权限范围尽可能窄的密钥(参见spree-api-v3/references/scopes.md)。 - 员工 JWT(自定义后台 UI):
admin.auth.login({ email, password })(或{ provider, ... })会返回{ token, user },刷新令牌会设为 HttpOnly Cookie。然后调用admin.setToken(token)。admin.auth.refresh()不接受令牌参数(由 Cookie 提供令牌)。管理 JWT 在 5 分钟后过期,因此请设置admin.onUnauthorized(async () => { const { token } = await admin.auth.refresh(); admin.setToken(token); return true })。客户端默认使用credentials: 'include'。员工的访问权限由其角色权限键控制(403details.required_permission);参见spree-auth-permissions。 admin.setStore(storeId)会在多店铺主机上发送X-Spree-Store-Id。- 支持完整的 CRUD 和领域操作:
orders.complete/cancel/approve、products.clone/bulkStatusUpdate/bulkAddToCategories/...、prices.bulkUpsert、stockLevels.bulkUpsert、roles、permissions.list()、exports、imports、webhookEndpoints、apiKeys等(参见packages/admin-sdk/src/admin-client.ts)。 - 邀请列表不包含
acceptance_url;需要时可通过admin.invitations.acceptanceLink(id)(或admin.sellers.invitations.acceptanceLink(sellerId, id))获取链接——此操作需要写入权限。admin.invitations.create需要role_id。 - 当密钥或员工缺少相应读取权限时,订单/付款/客户/礼品卡/店铺余额的
?expand=会被静默忽略——在断定 API 未返回数据之前,请先检查作用域。 - 库存使用
admin.stockLevels(sl_…),而非stockItems。地址使用country_code/state_code,而非country_iso。 - 简单商品或各个变体的价格需使用
prices: [{ currency: 'USD', amount: '29.99' }]格式。 - 单例使用
get:admin.me.get()(包含permission_keys)、admin.store.get()。 - 导出:调用
admin.exports.create({ type, search_params }),通过轮询exports.get(id)等待任务完成,然后使用带有Authorization标头的 fetch 将数据下载到 Blob。没有.download()辅助方法。
Webhook 接收器
// app/api/webhooks/spree/route.ts (Node runtime)
import { verifyWebhookSignature, type WebhookEvent } from '@spree/sdk/webhooks'
import type { Order } from '@spree/sdk'
export async function POST(request: Request) {
const rawBody = await request.text() // raw bytes, before JSON.parse
const ok = verifyWebhookSignature(
rawBody,
request.headers.get('x-spree-webhook-signature') ?? '',
request.headers.get('x-spree-webhook-timestamp') ?? '',
process.env.SPREE_WEBHOOK_SECRET!,
300, // replay tolerance (seconds)
)
if (!ok) return new Response('Unauthorized', { status: 401 })
const event = JSON.parse(rawBody) as WebhookEvent<Order>
if (event.name === 'order.placed') await syncToERP(event.data)
return Response.json({ ok: true })
}
订阅 order.placed。在 6.0 中仍会发送 order.completed,作为已弃用的别名(其元数据包含 deprecated_alias_of),并将在 6.1 中移除。验证器使用 node:crypto;在 Edge 运行时中,请使用 Web Crypto 对 `${timestamp}.${rawBody}` 实现 HMAC-SHA256(参见 spree-events-webhooks)。失败的投递会采用退避策略重试(最多 5 次,event.id 相同),因此处理器应根据 event.id 实现幂等。
为自定义端点扩展 SDK
不要派生 SDK,也不要绕过 SDK 直接使用原始 fetch:否则会失去身份验证标头、语言/货币/渠道默认值、重试、幂等性和错误规范化功能。
1. 单次调用:client.request
路径相对于对应的 API 根路径(/api/v3/store 或 /api/v3/admin)。request 不会应用 transformListParams,因此请自行封装 Ransack 谓词。
import type { PaginatedResponse } from '@spree/sdk'
type Brand = { id: string; name: string; slug: string }
const { data } = await client.request<PaginatedResponse<Brand>>('GET', '/brands', {
params: { 'q[name_cont]': 'acme', page: 1, limit: 25 },
})
const created = await admin.request<Brand>('POST', '/brands', { body: { name: 'Acme' } })
2. 封装资源类
import type { RequestFn, PaginatedResponse } from '@spree/sdk'
export class BrandsClient {
constructor(private readonly request: RequestFn) {}
list(params: { page?: number; limit?: number; name_cont?: string } = {}) {
const { page, limit, ...predicates } = params
const q = Object.fromEntries(Object.entries(predicates).map(([k, v]) => [`q[${k}]`, v]))
return this.request<PaginatedResponse<Brand>>('GET', '/brands', { params: { page, limit, ...q } })
}
get(id: string) { return this.request<Brand>('GET', `/brands/${id}`) }
}
@spree/admin-sdk 不会导出 RequestFn,因此请使用 AdminClient['request'] 为管理端封装器指定类型。
3. 将其附加到客户端
import { createClient, type Client, type ClientConfig } from '@spree/sdk'
export interface ExtendedClient extends Client { brands: BrandsClient }
export function createExtendedClient(config: ClientConfig): ExtendedClient {
const client = createClient(config) as ExtendedClient
client.brands = new BrandsClient(client.request)
return client
}
要为内置资源添加方法,请使用 Object.assign(base, { products: { ...base.products, republish(id: string) { ... } } })。不要展开客户端本身({ ...base }),因为资源位于原型上,展开后会被丢弃。
自定义资源的类型
由 spree:api_resource 生成的序列化器通过 Typelizer 指定类型(在 API 引擎中运行 bundle exec rake typelizer:generate)。在独立应用中,可将 Typelizer 的输出指向自己的文件夹,或手写与序列化器匹配的接口。序列化器是唯一可信来源。
使用 MSW 进行测试
SDK 只会调用 fetch,因此 MSW 处理器能看到每个请求:
server.use(
http.get('https://test.spree.local/api/v3/store/products', () =>
HttpResponse.json({ data: [{ id: 'prod_test1', name: 'Test' }], meta: { page: 1, limit: 25, count: 1, pages: 1 } })),
)
const client = createClient({ baseUrl: 'https://test.spree.local', publishableKey: 'pk_test' })
常见问题
- 结账后,
carts.get返回 404。 这是预期行为。请使用orders.get(cartId)。 - 总价显示为 "NaN"。 该渠道隐藏了价格,因此这些字段为
null。 - 管理端请求每 5 分钟返回一次 401。 你尚未注册
onUnauthorized,或者刷新 Cookie 被拦截。跨源设置需要 HTTPS,并且需要将源添加到 Settings → Allowed Origins。 - 使用密钥时返回 403。 查看
err.details.required_scope,并创建具有该权限范围的密钥。现有密钥的权限范围无法编辑。 - 密钥出现在浏览器包中。 绝不要这样做。
@spree/admin-sdk的secretKey只能用于服务器端,仪表板使用 JWT 流程。 - 监听
order.completed。 请改用order.placed。
延伸阅读
- Store SDK 源码:
packages/sdk/src/store-client.ts。Admin SDK:packages/admin-sdk/src/admin-client.ts。Seller SDK:packages/seller-sdk/src/seller-client.ts - 文档:
node_modules/@spree/docs/dist/developer/sdk/(Store 快速入门、admin/quickstart.md、admin/authentication.md) - OpenAPI:
node_modules/@spree/docs/dist/api-reference/store.yaml、admin.yaml、seller.yaml - 相关技能:
spree-api-v3、spree-storefront、spree-checkout、spree-events-webhooks、spree-auth-permissions、spree-marketplace。