mcpField
S
spree-typescript-sdk

spree-typescript-sdk

github.com/spree开发10222 次安装4 周前更新
代码仓库

关于 spree-typescript-sdk

spree-typescript-sdk 是面向 Spree 6 店面、管理和卖家 API 的 TypeScript SDK 集合,供构建店面和集成的开发者使用。它提供统一的请求接口,并包含重试、幂等性支持及可选的 Zod 模式。

主要功能

  1. 01覆盖店面、管理和卖家 API
  2. 02提供资源 CRUD 与领域操作
  3. 03支持令牌认证及请求级选项
  4. 04内置重试、错误规范化和幂等性
  5. 05提供生成类型及可选 Zod 模式

适用场景

  1. 01构建 Spree 店面与结账流程
  2. 02开发管理后台或卖家界面
  3. 03在集成中调用管理 API
  4. 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/sdkStore API(/api/v3/store)Publishable key + 可选的客户 JWT / 访客令牌2.x(当前为 2.0 beta)
@spree/admin-sdkAdmin API(/api/v3/admin)Secret key 或员工 JWT1.x beta
@spree/seller-sdkSeller 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) 会抛出 404 SpreeError(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'。员工的访问权限由其角色权限键控制(403 details.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。

评论

支持 Markdown