跳到主要内容

01 — Checkout Session 生命周期(主线)

本章讲透 ACP 的中央循环。先建立直觉,再看状态机、数据模型,最后是最容易踩坑的 idempotency 语义。源头:rfcs/rfc.agentic_checkout.md(定义 Agentic Checkout Specification, ACS)。

1.1 它要解决的小问题

agent 要替用户下单,需要一个有状态的、可来回修改的对象来承载「这一单现在长什么样」:买了啥、寄到哪、选了哪种配送、一共多少钱、能不能付了。这个对象就是 checkout session

关键设计立场写在 RFC 开头:商家是所有订单/支付/税/合规的 system of record,会话只是商家暴露给 agent 的一个标准化视图(rfcs/rfc.agentic_checkout.md:9-13)。

1.2 思路/直觉:authoritative cart

ACP 不让 agent 自己算价。每一次对会话的操作,商家都回完整的、最新的、权威的购物车状态(§1 目标第 2 条,rfcs/rfc.agentic_checkout.md:19-20)。

为什么这么设计?因为价格/税/库存/配送费只有商家算得准,而且会随地址、数量、时间变。让 agent 缓存计算 = 给自己挖一个「显示价和真实价对不上」的坑。所以契约是:agent 把响应当唯一真相,自己不做算术。

1.3 状态机(五个端点 + 五个状态)

会话有五个核心端点,对应一条状态流转线。

怎么读这张图: 方框是状态,箭头上是触发它的端点调用;authentication_required 是 3DS 触发的旁路(见 04 章)。

POST /checkout_sessions


┌──────────────────────┐ 缺地址/缺信息
│ not_ready_for_payment │◀──────────────┐
└──────────┬───────────┘ │
补齐信息 │ │ POST /checkout_sessions/{id}
▼ │ (update:改 items/地址/配送)
┌──────────────────────┐────────────────┘
│ ready_for_payment │
└──────────┬───────────┘
│ POST .../complete

┌───────────────┐ 需 3DS 且没带 result
│ (校验支付) │───────────────────────▶ authentication_required
└───────┬───────┘ (agent 做认证后,带 authentication_result 重试 complete)

┌────────────┐ POST .../cancel(未完成时)
│ completed │ ───────────────────────▶ canceled
└────────────┘

状态枚举:not_ready_for_payment | ready_for_payment | completed | canceled | in_progress(rfcs/rfc.agentic_checkout.md:131、校验规则 :335)。3DS 引入的 authentication_required 状态见 :159-164

五个端点速查:

端点方法成功码干什么
/checkout_sessionsPOST201从 items + 可选买家/地址建会话
/checkout_sessions/{id}POST200改 items / 地址 / 选配送
/checkout_sessions/{id}GET200取最新权威状态(404 不存在)
/checkout_sessions/{id}/completePOST200提交支付,必须建单
/checkout_sessions/{id}/cancelPOST200取消(已 completed/canceled 则 405)

端点定义见 rfcs/rfc.agentic_checkout.md:95-168。注意一个不对称设计:update 用 POST 而不是 PATCH/PUT(:144),好让它和 create 共享同一套幂等重放语义(见 §1.6)。

1.4 数据模型(权威购物车的骨架)

一份会话响应的主要部件(完整抽取见 rfcs/rfc.agentic_checkout.md:172-228):

字段是什么
status上面五态之一
currencyISO 4217,小写如 usd
line_items[]每行:base_amount/discount/subtotal/tax/total(全整数分)
totals[]车级别合计,每项一个 type(subtotal/tax/fulfillment/total…)+ 整数 amount
fulfillment_options[]可选配送方式(shipping/digital/pickup/local_delivery),各带 totals 与可选送达窗口
selected_fulfillment_options[]已选配送(option_id ↔ item_ids 映射)
capabilities.payment.handlers[]支持的支付 handler(见 02 章)
messages[]info / warning / error 三类(见 §1.7)
links[]政策链接(条款/隐私/退货)
order仅 complete 后出现:id/checkout_session_id/permalink_url

一个易混点——total 行的语义。 totals[] 是个数组,里头至少应有一个 type:"total"(校验规则 :336),其余是分项明细。整车响应实例见 rfcs/rfc.agentic_checkout.md:367-473

fulfillment_details 的演进(踩坑预警)。 v2 把扁平的 fulfillment_address 改成了嵌套的 fulfillment_details(含 name/phone_number/email/address),并把单数 fulfillment_option_id 换成数组 selected_fulfillment_options[](变更记录 rfcs/rfc.agentic_checkout.md:793-797)。看老示例时别被旧字段名带偏。

1.5 complete:真正发生支付的那一步

POST /complete 的请求体带 payment_data(handler_id + instrument + 可选账单地址)、可选 buyer、以及条件性的 authentication_result(rfcs/rfc.agentic_checkout.md:154-156)。响应必须status: completed 和一个 order

payment_data.instrument.credential 里通常是从 delegate_payment 拿到的 SPT——这条链路是 02 章的主题。请求实例见 rfcs/rfc.agentic_checkout.md:573-612,其中 credential 形如 { "type": "spt", "token": "spt_123" }

1.6 关键细节:idempotency 的精确语义

这是 ACS 里规则最密、最值得逐条读的一节(§6,rfcs/rfc.agentic_checkout.md:232-318),也是写实现最容易出错的地方。RFC 自称这是「ACP 幂等性的权威参考」(:234)。

直觉: 网络会抖,agent 会重试。如果「重试」导致重复扣款,就完蛋了。所以每个 POST 都带一个 Idempotency-Key,服务端用它去重。

核心规则,逐条:

  • 必带。 所有 POST(create/update/complete/cancel)都要带 Idempotency-Key,opaque 字符串,≤255 字符,推荐 UUID v4(:238-239)。漏带 → 400 idempotency_key_required(:241-249)。
  • 作用域。 key 按「认证身份 + 端点路径」隔离;同一个 key 在不同端点互不影响(:240)。
  • 等价判定只看 body。 header 不参与;判定是「请求体的语义 JSON 相等」(:253)。

这里有一个最容易踩的坑——null 与「键缺失」不等价:

变体算等价吗?
key 顺序不同
null vs 键不存在——null=「清空这个字段」,缺失=「别动这个字段」
数字尾零(1.0 vs 1)
数组元素顺序不同——数组是顺序敏感的

(对照表 rfcs/rfc.agentic_checkout.md:255-260。)所以 SDK 序列化器必须保留「null vs 缺失」的区别(实现指南 :318)。

重放行为。 同 key + 完全相同 body → 服务端重放原响应(同 HTTP 状态码),建议带 Idempotent-Replayed: true,且绝不重复执行副作用(扣款、占库存)(:266-269)。

冲突与在途。 同 key 不同 body → 422 idempotency_conflict(永久错误,别重试);同 key 原请求还在跑 → 409 idempotency_in_flight(可按 Retry-After 重试)。错误码表 :275-279

5xx 不缓存。 5xx 响应不得按 key 缓存——拿同 key 重试 5xx 必须当全新请求处理(:301-302)。这避免「一次抖动把失败永久钉死」。

保留期 ≥24h(:306)。

实现指南里还给了两条工程精华:幂等记录与业务操作应在同一个 ACID 事务里提交(防「鬼 key」/「丢 key」),以及对触发外部副作用(如 PSP 授权)的操作用恢复点(recovery-point)语义让重试能续跑而非重跑(:316-317)。

1.7 关键细节:消息与错误的两套通道

ACP 区分两种「出问题了」的表达:

  1. HTTP 级错误——扁平对象,直接当响应体返回。形状是 {type, code, message, param?},type ∈ invalid_request | processing_error | service_unavailable,param 是 RFC 9535 JSONPath(rfcs/rfc.agentic_checkout.md:78-89)。
  2. 会话内消息——messages[] 数组里的 info/warning/error,用于「会话本身没崩,但有话要说」(库存不足、需要登录、需要 3DS…)。错误码枚举见 :192

v2 给 message 加了一个很实用的 resolution 字段,告诉 agent「这条谁来解」(rfcs/rfc.agentic_checkout.md:194-199):

resolution含义
recoverableagent 能自行 API 重试修复(换参数再来)
requires_buyer_input必须问用户拿信息(API 拿不到)
requires_buyer_review必须用户在下单前确认(政策/法规/资格)

这让 agent 能程序化决定该自己重试、还是回头问用户,而不是把所有错误一视同仁。

markdown 注入防线。 当 message/disclosure 的 content_typemarkdown 时,内容必须符合 CommonMark 0.31.2 且禁止裸 HTML(<script>/<iframe>…),且三方共担责任:商家写、服务端校验拒绝裸 HTML、agent 渲染时关掉裸 HTML 输出(rfcs/rfc.agentic_checkout.md:203-214)。这是把「商家文案」当不可信输入处理的安全设计。

1.8 本章要带走的三句话

  • 主线 = 五端点状态机,每次响应都是权威购物车,agent 不自己算钱。
  • 所有 POST 必带幂等键;记牢「null ≠ 缺失」「数组顺序敏感」「5xx 不缓存」。
  • 出错分两层:HTTP 级扁平错误 vs 会话内 messages[];后者带 resolution 告诉你谁来解。