跳到主要内容

UCP — 发现与能力协商

本章讲 UCP 最核心的一招:两个素未谋面的系统,如何在一次请求/响应里自动算出“我们俩到底能一起做哪些事”。看懂这一章,就看懂了 UCP 为什么能“免对接接入”。

1. 它要解决的小问题

平台和商家彼此不认识,各自支持的功能、各自支持的版本都不一样。要做生意,得先就“这次交互用哪些功能、哪个版本”达成一致——而且不能靠事先人工对接。这就是“发现 + 协商”。

2. 思路/直觉:profile 是单一事实源

UCP 的做法是:每一方都发布一份机器可读的 profile 文档,里面声明自己支持的 services / capabilities / payment_handlers,外加用于身份验证的 signing_keys

  • 商家把 profile 挂在固定地址 /.well-known/ucp
  • 平台不挂固定地址,而是每次请求用 HTTP 头 UCP-Agent: profile="…" 报上自己 profile 的 URL。

这套设计带来 permissionless onboarding(免许可接入):任何拥有可发现 profile 的平台,都能跟任何商家交互,无需事先注册。来源:docs/documentation/core-concepts.md:214-230

一个文档同时干两件事——声明能力 + 发布公钥——所以发现和身份认证一次解决。来源:docs/documentation/core-concepts.md:252-260

3. 主算法:能力取交集(server-selects)

协商是 server-selects(服务端选择) 架构:由商家(服务端)决定本次生效能力集。算法分三步,第三步要反复跑到稳定。

商家能力集 B 平台能力集 P
┌───────────────┐ ┌───────────────┐
│ checkout │ │ checkout │
│ fulfillment │ │ cart │
│ discount │ │ discount │
│ order │ │ order │
└───────┬───────┘ └───────┬───────┘
│ │
└──────────────┬───────────────────┘

① 按 name 取交集 → {checkout, discount, order}

② 每个能力选“双方公共版本里最高的那个日期”
没有公共版本 → 整个能力踢出

③ 剪孤儿扩展:extends 的父能力一个都不在集合里 → 剪掉
反复剪到不再变化(处理链式扩展)

生效集 = 双方都支持、版本兼容、扩展依赖满足的最小集

(注意 fulfillment 在第①步就因平台没声明而出局;若它是某扩展的唯一父能力,那个扩展会在第③步被剪。)

逐步拆解,对照规范散文:

  1. 按 name 取交集。 只有双方都声明的能力名才是候选。
  2. 选版本。 对每个候选,求双方版本数组的公共版本集;非空就选最高(日期最新)的;为空就排除该能力。
  3. 剪孤儿扩展。 扩展(带 extends 的能力)若其父能力一个都不在交集里,就移除;多父扩展只要至少一个父在就保留。
  4. 重复剪枝直到不再有能力被移除(处理传递性扩展链)。

来源:docs/specification/overview.md:679-703。同样的描述在 core-concepts 里有更白话的版本:docs/documentation/core-concepts.md:233-249

原理演示(示意,非源码)

下面用 Python 把“取交集 + 选版本 + 剪枝”演一遍,帮你建立直觉。重点看第三步的循环剪枝。

# 示意,非源码:演示 UCP 能力交集算法的骨架
def negotiate(business: dict, platform: dict) -> dict:
active = {}
# ① 按 name 取交集;② 每个能力选双方公共版本里最高的
for name, b_entries in business.items():
if name not in platform:
continue # 平台没声明 → 跳过
b_versions = {e["version"] for e in b_entries}
p_versions = {e["version"] for e in platform[name]}
common = b_versions & p_versions
if not common:
continue # 无公共版本 → 排除该能力
active[name] = {"version": max(common), # 日期字符串可直接比大小
"extends": b_entries[0].get("extends")}
# ③ 剪孤儿扩展,反复跑到稳定(处理链式扩展)
changed = True
while changed:
changed = False
for name, entry in list(active.items()):
parents = entry.get("extends")
if not parents:
continue # 根能力不剪
parents = parents if isinstance(parents, list) else [parents]
if not any(p in active for p in parents): # 多父:一个都没命中才剪
del active[name]
changed = True
return active

重点看:版本是 YYYY-MM-DD 字符串,直接字典序比较就等于按日期比较——这是日期版本号的一个隐藏便利。

真实依据

本仓库没有协商算法的实现代码(它在外部 SDK / ucp-schema 里),但驱动它的数据结构在 schema 里是真的:extends 字段(单值或数组)定义于 source/schemas/capability.json:14-30,能力注册表“按 name 作键”的结构定义于 source/schemas/ucp.json:101-109。版本格式 ^\d{4}-\d{2}-\d{2}$ 定义于 source/schemas/ucp.json:7-12

4. 响应里回带生效能力(协议的“收据”)

协商结果不是一次性的——商家每个响应都在 ucp.capabilities 里回带本次生效的能力,让平台始终知道当前功能集。来源:docs/specification/overview.md:674-678docs/specification/overview.md:958-988

而且回带要按响应类型裁剪:checkout 响应只带 checkout 及挂在它上面的扩展,不带 cart/order。

响应类型带哪些不带哪些
Checkoutcheckout、discount、fulfillmentcart、order
Cartcart、discountcheckout、fulfillment、order
Orderordercheckout、cart、discount

来源:docs/specification/overview.md:1011-1018

5. 版本两层:协议版本 vs 能力版本

UCP 把版本兼容拆成两层,互不干扰:

  • 协议版本ucp.version):管核心机制——发现、协商流程、传输绑定、签名要求。
  • 能力版本:每个能力独立版本化,管该功能自身的语义。

协议版本用 supported_versions 映射表向后兼容:商家在当前 profile 里列出自己还支持的旧版本 → 各自的 profile URI;平台按自己的协议版本去 match。来源:docs/specification/overview.md:1907-1962source/schemas/ucp.json:181-190

平台协议版本 == 商家 version ? → 直接用 /.well-known/ucp
平台协议版本 ∈ supported_versions 的键 ? → fetch 对应 URI 的 leaf profile
都不是 → version_unsupported 错误(不创建任何资源)

版本特定 profile 是叶子文档:它只描述一个版本,且 MUST NOT 再带 supported_versions。来源:docs/specification/overview.md:1959-1962

6. 协商失败怎么办(区分两类错误)

规范明确把失败分成“传输错误”和“业务结果”两类——这是个值得学的设计:

  • 发现失败(抓不到/解析不了平台 profile)→ 传输错误(HTTP 4xx,如 profile_unreachable 424、version_unsupported 422),因为输入根本没拿到。
  • 协商失败(profile 有效但能力交集为空)→ 是业务结果:handler 跑过了,结果写在 UCP 响应里(HTTP 200 + ucp.status: "error" + capabilities_incompatible)。

两者都可附 continue_url 做优雅降级——把买家导去普通网页完成任务。来源:docs/specification/overview.md:706-779、错误码表 docs/specification/overview.md:727-733

7. 巧妙之处 / 坑

  • 巧:单文档双用途。 profile 同时承载能力 + 公钥,发现与认证合一,省掉一套独立的密钥管理流程。docs/documentation/core-concepts.md:252-260
  • 巧:日期版本号可直接比大小。 “选最高版本”就是字符串 max(),无需解析。source/schemas/ucp.json:7-12
  • 坑:剪枝必须迭代。 单趟剪枝处理不了“扩展 A 依赖扩展 B、B 又被剪”的链式情况,规范要求重复到稳定docs/specification/overview.md:699-701
  • 坑:spec URL 必须与命名权威同源。 否则可被伪造能力。平台 MUST 校验这层绑定(见 02 章)。docs/specification/overview.md:80-92

8. 代码地图

主题文件符号 / 锚点
交集算法(权威散文)docs/specification/overview.md“Intersection Algorithm” §679-703
extends 字段(单/多父)source/schemas/capability.json$defs.baseextends
能力注册表结构source/schemas/ucp.json$defs.base.properties.capabilities
版本格式 / supported_versionssource/schemas/ucp.json$defs.version$defs.business_schema
响应能力裁剪规则docs/specification/overview.md“Response Capability Selection” §990-1018
协商错误码docs/specification/overview.md“Error Codes” §723-745