数据截至 (上游 commit 9ed9049a14d9)
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:218-234。
一个文档同时干两件事——声明能力 + 发布公钥——所以发现和身份认证一次解决。来源:
docs/documentation/core-concepts.md:256-267。
3. 主算法:能力取交集(server-selects)
协商是 server-selects(服务端选择) 架构:由商家(服务端)决定本次生效能力集。算法分三步,第三步要反复跑到稳定。
商家能力集 B 平台能力集 P
┌───────────────┐ ┌───────────────┐
│ checkout │ │ checkout │
│ fulfillment │ │ cart │
│ discount │ │ discount │
│ order │ │ order │
└───────┬───────┘ └───────┬───────┘
│ │
└──────────────┬───────────────────┘
▼
① 按 name 取交集 → {checkout, discount, order}
▼
② 每个能力选“双方公共版本里最高的那个日期”
没有公共版本 → 整个能力踢出
▼
③ 剪孤儿扩展:extends 的父能力一个都不在集合里 → 剪掉
反复剪到不再变化(处理链式扩展)
▼
生效集 = 双方都支持、版本兼容、扩展依赖满足的最小集
(注意 fulfillment 在第①步就因平台没声明而出局;若它是某扩展的唯一父能力,那个扩展会在第③步被剪。)
逐步拆解,对照规范散文:
- 按 name 取交集。 只有双方都声明的能力名才是候选。
- 选版本。 对每个候选,求双方版本数组的公共版本集;非空就选最高(日期最新)的;为空就排除该能力。
- 剪孤儿扩展。 扩展(带
extends的能力)若其父能力一个都不在交集里,就移除;多父扩展只要至少一个父在就保留。 - 重复剪枝直到不再有能力被移除(处理传递性扩展链)。
来源:docs/specification/overview/index.md:1793-1817。同样的描述在 core-concepts 里有更白话的版本:docs/documentation/core-concepts.md:237-253。
原理演示(示意,非源码)
下面用 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-24,能力注册表“按 name 作键”的结构定义于 source/schemas/ucp.json:131-139。版本格式 ^\d{4}-\d{2}-\d{2}$ 定义于 source/schemas/ucp.json:7-12。
4. 响应里回带生效能力(协议的“收据”)
协商结果不是一次性的——商家每个响应都在 ucp.capabilities 里回带本次生效的能力,让平台始终知道当前功能集。来源:docs/specification/overview/index.md:1788-1791、docs/specification/overview/index.md:2072-2102。
而且回带要按响应类型裁剪:checkout 响应只带 checkout 及挂在它上面的扩展,不带 cart/order。
| 响应类型 | 带哪些 | 不带哪些 |
|---|---|---|
| Checkout | checkout、discount、fulfillment | cart、order |
| Cart | cart、discount | checkout、fulfillment、order |
| Order | order | checkout、cart、discount |
来源:docs/specification/overview/index.md:2125-2132。
5. 版本两层:协议版本 vs 能力版本
UCP 把版本兼容拆成两层,互不干扰:
- 协议版本(
ucp.version):管核心机制——发现、协商流程、传输绑定、签名要求。 - 能力版本:每个能力独立版本化,管该功能自身的语义。
协议版本用 supported_versions 映射表向后兼容:商家在当前 profile 里列出自己还支持的旧版本 → 各自的 profile URI;平台按自己的协议版本去 match。来源:docs/specification/overview/index.md:3461-3474、source/schemas/ucp.json:211-220。
平台协议版本 == 商家 version ? → 直接用 /.well-known/ucp
平台协议版本 ∈ supported_versions 的键 ? → fetch 对应 URI 的 leaf profile
都不是 → version_unsupported 错误(不创建任何资源)
版本特定 profile 是叶子文档:它只描述一个版本,且 MUST NOT 再带 supported_versions。来源:docs/specification/overview/index.md:3511-3513。
6. 协商失败怎么办(区分两类错误)
规范明确把失败分成“传输错误”和“业务结果”两类——这是个值得学的设计:
- 发现失败(抓不到/解析不了平台 profile)→ 传输错误(HTTP 4xx,如
profile_unreachable424、version_unsupported422),因为输入根本没拿到。 - 协商失败(profile 有效但能力交集为空)→ 是业务结果:handler 跑过了,结果写在 UCP 响应里(HTTP 200 +
ucp.status: "error"+capabilities_incompatible)。
两者都可附 continue_url 做优雅降级——把买家导去普通网页完成任务。来源:docs/specification/overview/index.md:1819-1837、错误码表 docs/specification/overview/index.md:1840-1849。
7. 巧妙之处 / 坑
- 巧:单文档双用途。 profile 同时承载能力 + 公钥,发现与认证合一,省掉一套独立的密钥管理流程。
docs/documentation/core-concepts.md:256-267。 - 巧:日期版本号可直接比大小。 “选最高版本”就是字符串
max(),无需解析。source/schemas/ucp.json:7-12。 - 坑:剪枝必须迭代。 单趟剪枝处理不了“扩展 A 依赖扩展 B、B 又被剪”的链式情况,规范要求重复到稳定。
docs/specification/overview/index.md:1813-1814。 - 坑:schema URL 必须与命名权威同源。 否则可被伪造能力。平台 MUST 校验这层绑定(见 02 章);注意新规范里
spec文档 URL 不再受绑定、只要求 https。docs/specification/overview/index.md:831-853。
8. 代码地图
| 主题 | 文件 | 符号 / 锚点 |
|---|---|---|
| 交集算法(权威散文) | docs/specification/overview/index.md | “Intersection Algorithm” §1793-1817 |
extends 字段(单/多父) | source/schemas/capability.json | $defs.base 内 extends |
| 能力注册表结构 | source/schemas/ucp.json | $defs.base.properties.capabilities |
| 版本格式 / supported_versions | source/schemas/ucp.json | $defs.version、$defs.business_schema |
| 响应能力裁剪规则 | docs/specification/overview/index.md | “Response Capability Selection” §2104-2132 |
| 协商错误码 | docs/specification/overview/index.md | “Error Codes” §1838-1881 |