数据截至 (上游 commit 9ed9049a14d9)
UCP — 结算状态机与错误处理
本章讲一次结算(checkout)从创建到下单的全过程:它的状态怎么流转、平台怎么读懂商家想要什么、金额怎么显示、以及自主 agent 场景下的安全锁定。
1. 它要解决的小问题
结算是个多步过程:缺地址、缺支付方式、要 3DS 验证、要买家点确认……平台(尤其 agent)需要一套确定性的协议来知道“现在卡在哪、下一步该我做什么”。UCP 用一个由商家驱动的状态机回答这个问题——商家设状态,平台读 messages 决定动作。来源:docs/specification/shopping/checkout/index.md:340-345。
2. 状态机
+------------+ continue_url +---------------------+
| incomplete |◀────────────────────────▶| requires_escalation |
+-----+------+ | (交回买家:经 |
│ 信息齐了 | continue_url) |
▼ +----------+----------+
+-------------------+ │
| ready_for_complete| 平台可调 Complete │ continue_url
+--------+----------+ │
│ Complete Checkout │
▼ │
+----------------------+ │
| complete_in_progress | │
+----------+-----------+ │
└───────────────┬──────────────────────────────┘
▼
+-----------+ +----------+
| completed | | canceled | (任意态可进:会话失效/过期)
+-----------+ +----------+
这些状态值是闭枚举(天生封闭的状态机,所以这里用 enum 是对的)——见 source/schemas/shopping/checkout.json:65-77。各状态含义见 docs/specification/shopping/checkout/index.md:379-407。状态机原图:docs/specification/shopping/checkout/index.md:346-378。
关键规则:商家返回 requires_escalation 时 MUST 提供 continue_url,且 MUST 至少带一条 requires_buyer_input 或 requires_buyer_review 的消息。docs/specification/shopping/checkout/index.md:1022-1026、:947-953。
3. 核心机制:把响应当“优先级错误栈”处理
这是平台开发者最该掌握的一招。每条 messages[] 带一个 severity,它规定了平台该怎么做:
| severity | 含义 | 平台动作 |
|---|---|---|
recoverable | 平台改输入就能解 | 改资源 + 重试(Update Checkout) |
requires_buyer_input | 商家要的输入 API 给不了(checkout 仍未完成) | 经 continue_url 交回买家 |
requires_buyer_review | checkout 已完成,但政策/法规要买家授权 | 经 continue_url 交回买家 |
unrecoverable | 没有可操作的资源 | 用新资源/输入重试,或交回买家 |
来源:docs/specification/shopping/checkout/index.md:498-513。注意 requires_buyer_input 与 requires_buyer_review 都导致交回买家,但代表不同的 checkout 状态(未完成 vs 已完成待审)——这个区分很精细。
处理算法(示意,非源码)
规范给的处理逻辑(docs/specification/shopping/checkout/index.md:542-613)翻成代码长这样。重点看优先级顺序:
# 示意,非源码:平台处理 checkout 响应里的 messages
errors = [m for m in response["messages"] if m["type"] == "error"]
if any(e["severity"] == "unrecoverable" for e in errors):
retry_with_new_resource_or_handoff(); return # 最高优先:没救了
recoverable = [e for e in errors if e["severity"] == "recoverable"]
if recoverable:
for e in recoverable:
if "path" in e: # path 定位到具体出错字段
fix_field_at(e["path"]) # 如重排 $.buyer.phone_number
else:
generic_fix(e["code"])
update_checkout(); return # 先把能改的改了再说
# 剩下的才是“必须交回买家”
handoff_via(response["continue_url"])
重点看:先穷尽 recoverable,最后才 handoff——平台应尽量自己解决,别动不动把用户踢去网页。
path 字段是 RFC 9535 JSONPath,相对响应根,把错误精确锚到某字段/某行项(如 $.line_items[0].quantity),便于平台高亮表单字段。docs/specification/shopping/checkout/index.md:635-674。
4. Totals 渲染契约:商家权威,平台只渲染
金额展示是个有“政治性”的设计决定:因为多辖区税务、强制费用披露等都是商家的法定义务,所以 totals[] 由商家完全掌控其内容与顺序,平台 MUST NOT 解释、过滤、重排、聚合或自行套展示逻辑。docs/specification/shopping/checkout/index.md:1289-1325。
不变量(值得记):
- 每条带
type+amount;金额是带符号整数,负数=减项(折扣),正数=加项——符号即方向。 subtotal恰好一条、total恰好一条;其余类型可重复(如多辖区税行)。- 子行
lines[]的金额之和 MUST 等于父项金额。
来源:docs/specification/shopping/checkout/index.md:1326-1362、:1341-1362。
平台可以核验但不能替换:算出的和对不上 total 时,平台 MUST NOT 改渲染输出,但 MUST NOT 自主完成这笔结算——应拒绝或经 continue_url 请买家复核。docs/specification/shopping/checkout/index.md:1326-1340。
5. 披露式警告:合规内容的强渲染契约
messages 里的 warning 可带 presentation 控制渲染契约。disclosure(如过敏原、Prop 65、能效标签)比默认的 notice 严格得多:
notice(默认) | disclosure | |
|---|---|---|
| 显示内容 | MUST | MUST |
靠近 path 所指组件 | MAY | MUST |
| 可关闭 | MAY | MUST NOT |
渲染 image_url | MAY | MUST |
| 无法满足契约时 | — | MUST 经 continue_url 升级 |
来源:docs/specification/shopping/checkout/index.md:808-869。这把“法定披露必须显示且不可隐藏”的语义直接写进了协议层。
6. AP2:自主 agent 的会话锁定
当 dev.ucp.shopping.ap2_mandate 扩展协商命中,会话进入 Security Locked:
- 商家 MUST 在所有 checkout 响应里嵌入对条款(价格、line items)的密码学签名(
ap2.merchant_authorization)。 - 平台 MUST 在
complete时提供签名的 mandate(ap2.checkout_mandate),证明用户授权了这个具体的 checkout 状态。 - 任何一方都不得退回到无保护的普通流程。
来源:docs/specification/ap2-mandates.md:26-38、107-117。
所有 AP2 字段统一收纳在一个 ap2 对象下,好处是:商家签名计算时只需一条规则——排除 ap2,未来加 AP2 字段自动适配。docs/specification/ap2-mandates.md:42-53。
7. 五个操作 + 幂等
Checkout 能力定义五个逻辑操作(docs/specification/shopping/checkout/index.md:1035-1046):
| 操作 | 干什么 | 注意 |
|---|---|---|
| Create Checkout | 创建会话 | 用户表达购买意图时调 |
| Get Checkout | 取当前状态 | TTL 由商家用 expires_at 决定 |
| Update Checkout | 整体替换资源 | 平台 REQUIRED 发送完整 checkout 资源(非 patch) |
| Complete Checkout | 下单 | 响应回带 order 字段 |
| Cancel Checkout | 取消 | 非 completed/canceled 的会话应可取消 |
Update 是全量替换而非增量——这点容易踩坑。状态改变型操作配幂等键防重放/防重复扣款(详见 05 章)。
8. 代码地图
| 主题 | 文件 | 符号 / 锚点 |
|---|---|---|
| 状态机 + 状态值 | docs/specification/shopping/checkout/index.md | “Checkout Status Lifecycle” §340-407 |
| 状态枚举(真值) | source/schemas/shopping/checkout.json | properties.status.enum |
| severity 错误栈 | docs/specification/shopping/checkout/index.md | “Error Handling”/“Error Processing Algorithm” §488-613 |
| Totals 渲染契约 | docs/specification/shopping/checkout/index.md | “Total”/“Rendering Contract” §1285-1425 |
| 披露式警告 | docs/specification/shopping/checkout/index.md | “Warning Presentation” §808-941 |
| AP2 锁定 | docs/specification/ap2-mandates.md | “Overview”/“Activation and Session Locking” |
| checkout 必填/各操作字段 | source/schemas/shopping/checkout.json | required、各属性 ucp_request(见 06 章) |