05 — 发现 / 商品流 / 订单(三个周边面)
主线(checkout)前面有「怎么知道这家店能用 ACP、怎么选商品」,后面有「下单后订单怎么追踪」。这一章把三个周边面讲清:Discovery、Product Feeds、Orders + Webhook,外加 Cart 一瞥。
5.1 Discovery:无鉴权的预检
问题: agent 拿到一个域名,它怎么知道「这家支不支持 ACP?API 在哪?支持哪个版本?」如果靠「直接 POST 一个 checkout 看它崩不崩」,既浪费请求又难判断(rfcs/rfc.discovery.md:13-21)。
答案: 一个 RFC 8615 风格的 well-known 文档 GET /.well-known/acp.json,无需鉴权、可缓存(rfcs/rfc.discovery.md:94-105)。它解决一个 bootstrapping 问题:只给域名,agent 一次请求就拿到 api_base_url、版本、支持的服务与传输(:73-77)。
文档主要字段(rfcs/rfc.discovery.md:111-163):
| 字段 | 内容 |
|---|---|
protocol.version / supported_versions | 当前版本 + 全部支持版本(按时序,最旧在前) |
api_base_url | REST API 基址,agent 拼资源路径 |
transports | rest / mcp(MCP 传输绑定见 SEP #135) |
capabilities.services | checkout / orders / delegate_payment / carts(+ feeds) |
capabilities.extensions | 支持的扩展(各带 spec/schema URL) |
capabilities.intervention_types | 3ds / biometric / address_verification |
Discovery vs 能力协商——别搞混。 这是全章最该记的对照(rfcs/rfc.discovery.md:269-278):
| 方面 | Discovery(.well-known) | 能力协商(POST /checkout_sessions) |
|---|---|---|
| 范围 | 商家级、稳定 | 会话级、随单变 |
| 鉴权 | 不需要 | 需要 Bearer |
| 内容 | 版本/服务/扩展/传输 | 支付方式/handler/介入交集 |
| 可缓存 | 可(小时~天) | 仅本会话 |
| 回答的问题 | 「这能用 ACP 吗?API 在哪?」 | 「这一单具体啥能用?」 |
所以 discovery 不替代内联能力协商——读了 well-known,该带的 capabilities 还得带(:278)。
一个刻意的安全非目标: 当 Seller Platform(如 Stripe)替很多商家托管同一个 well-known 时,文档不得接受或返回 merchant_id——因为它无鉴权,暴露商家身份会让任何人枚举出平台上有哪些商家(rfcs/rfc.discovery.md:52、:301-303)。
5.2 Product Feeds:方向反过来的推送
问题: agent 怎么知道「这家店卖什么、该把哪个确切的 item id 传进 checkout」?靠爬网页既脆又拿不到 variant id(rfcs/rfc.product_feeds.md:27-40)。
最反直觉的设计——调用方向是反的。 Product Feed API 由 agent 托管,商家/平台调 agent 来推送目录、回读 agent 当前已知的状态。agent 绝不反过来调商家的 feed 端点(rfcs/rfc.product_feeds.md:155-160、:509-512)。这是个「推送(push)」模型,不是「拉取(pull)」。
端点(都在 agent 侧)(rfcs/rfc.product_feeds.md:242-247):
| 操作 | 方法 | 端点 |
|---|---|---|
| 建 feed | POST | /feeds |
| 取 feed 元数据 | GET | /feeds/{id} |
| 取当前商品集 | GET | /feeds/{id}/products |
| 增量 upsert 商品 | PATCH | /feeds/{id}/products |
数据模型分两层:Product(分组)含一个或多个 Variant(可购买单元,通常就是传进 checkout 的那个 id)(rfcs/rfc.product_feeds.md:176-202)。两种发布模式:离线全量替换(metadata.json + products.jsonl)或 API 增量 upsert(按 Product.id,漏掉的不变)(:222-255)。
贯穿 feeds 的一条铁律:feed 不是权威。 feed 的价格/库存只是发现期信号;checkout 永远是权威,即使两者不一致(rfcs/rfc.product_feeds.md:204-220、:625-630)。agent 必须把 checkout 响应当真相,且不得把 feed 的价/货当作保证。这和 01 章的 authoritative cart 是同一套世界观。
5.3 Orders:在最小订单上「渐进富化」
checkout 完成时只回一个最小订单(id/checkout_session_id/permalink_url)。Orders RFC(rfcs/rfc.orders.md)在它上面加可选字段,回答下单后的问题:东西到哪了、退款了没。
设计哲学是渐进富化(progressive enrichment):所有新字段可选,商家系统支持到哪就加到哪(rfcs/rfc.orders.md:54-79)。
富化后的订单加了(:109-127):line_items[](带三段式数量)、fulfillments[](履约)、adjustments[](退款/退货/争议)、totals[]。
两个值得记的精华:
- 三段式数量模型(
rfcs/rfc.orders.md:81-96):ordered(下单时,不变)/current(当前有效,可因取消/退货下降)/fulfilled(已履约,递增)。行项状态可由它推导:current==0→removed、fulfilled==current→fulfilled、0<fulfilled<current→partial、否则 processing。状态不是独立存的,是算出来的。 total的语义锁死(rfcs/rfc.orders.md:299-308):type:"total"永远是 checkout 时的原始扣款额,不减退款;退款单独用amount_refunded表示。这样 agent 能无歧义地说「您这单 $670.99,已退 $325.16」,而不是把两个数搅在一起。RFC 明确:agent 不应自己拿 total 减 adjustment 去算净额。
用 fulfillments[] 而非 shipments[],是为了同时覆盖 shipping/pickup/digital 三种履约(rfcs/rfc.orders.md:97-105)。
5.4 Webhook:订单事件怎么推回 agent
下单后,商家通过 webhook 把订单生命周期事件推给 agent 的接收端(spec/2026-04-17/openapi/openapi.agentic_checkout_webhook.yaml)。两个要点:
- 全量推送:
data必须是完整 Order 对象,不是增量 delta;可选字段尽量带全(rfcs/rfc.orders.md:466-470)。webhook 的EventDataOrder直接$ref整个 Order schema(:472-477)。 - HMAC 签名防重放:每个 webhook 带
Merchant-Signature: t=<unix>,v1=<64hex>,签名体是timestamp + "." + raw_body,算法 HMAC-SHA256;缺失/格式错/时间窗超(推荐 300s 容差)/验签失败 → 回 401(webhook YAML 的description与Merchant-Signature参数)。
注意一个迁移点:旧的 refunds[] 字段已被移除,统一用 adjustments[](type: refund 等)表达退款/退货/争议(rfcs/rfc.orders.md:479-487)。
5.5 Cart 一瞥:checkout 之前的轻量篮子
Cart(rfcs/rfc.cart.md)是个可选服务,补「还没决定买、只是逛逛加篮子」这个常见场景。它刻意做得轻:无支付、无状态机、无能力协商(:52-60)。
Cart vs Checkout 的边界很清楚(rfcs/rfc.cart.md:71-81):cart 给估算总价(可缺税),checkout 给权威定价;cart 没有 complete,要购买就 GET /carts/{id} 拿内容去建 checkout。一个好用的设计:continue_url 支持购物车在 agent 与人之间「交接」——人在商家网店里建的篮子,agent 能接手结算(:65-67)。