Skip to content

DPE 投递协议规范 v1(dpe-push/1)

外部数据源系统向 Robot Memory 投递 DPE 文档的通信协议规范正文。

!!! note "状态" 起草中(v1)· 2026-09-18 · 归属 TFRS-379

Hash 算法契约见 [hash-contract-v1.md](hash-contract-v1.md)。协议总览与设计基线见 [index.md](index.md)。

1. 协议全景

协议分为三个阶段。Phase 0 可缓存、可跳过;Phase 1 是纯只读操作;Phase 2 是唯一的写入点。

Phase 0  能力发现   GET  /.well-known/dpe-push          无需认证,可缓存
Phase 1  协商       POST /v1/memory/dpe:negotiate         manifest → gap(只读)
Phase 2  投递       POST /v1/memory/dpe:commit            manifest + contents → result

设计决策

决策 选择 理由
协商粒度 批量(一次请求携带完整 manifest) 文档动辄数百 element。若效仿 Docker Registry 逐 blob HEAD,将产生数百次往返。此处采用 Git fetch 式批量协商(一次 have / want),而非 Docker 式逐对象探测
接口形态 AIP-136 自定义方法后缀 :negotiate / :commit Google AIP-136 是 RPC-over-HTTP 的标准形态,语义自明,且不与未来可能新增的子资源路径冲突
版本表达 media type:application/vnd.tfrs.dpe.v1+json OCI / Docker 既有实践;使内容协商与协议版本协商合一,避免 /v2/ 式路径版本污染资源层级

快速路径

若客户端确信服务端已持有目标内容(例如刚完成一次成功投递且本地未再变更),可跳过 Phase 0 与 Phase 1,直接进入 Phase 2。此时若内容不足,服务端返回 DPE_INSUFFICIENT_CONTENT(见 §7 错误模型),客户端据此补传即可——幂等键保证该重试安全。

2. Phase 0 — 能力发现

GET /.well-known/dpe-push

无需认证。与项目既有的 GET /.well-known/oauth-protected-resource(RFC 9728)同规格。该文档全局一致、不含任何 robot 专属信息,泄露面为零。

{
  "protocol_versions": ["1"],
  "accepted_hash_strategies": [
    {
      "uri": "hash-strategy://default?algo_version=v1",
      "status": "stable",
      "deprecated_at": null
    }
  ],
  "default_hash_strategy_uri": "hash-strategy://default?algo_version=v1",
  "max_payload_bytes": 52428800,
  "content_encodings": ["gzip", "identity"],
  "delta_supported": true,
  "endpoints": {
    "negotiate": "/v1/memory/dpe:negotiate",
    "commit": "/v1/memory/dpe:commit"
  }
}

accepted_hash_strategies 是服务端算法版本闸门:客户端从中选择一个双方都支持的 hash_strategy_uri。

!!! warning "能力文档不是信任来源" 能力文档可被缓存,客户端可能在服务端收紧 allowlist 之后仍持有旧副本。服务端必须在写入路径上重新校验一次 hash_strategy_uri 是否仍在 allowlist 内,不匹配则拒绝,不得依赖 Phase 0 的结果。

配置项:dpe_push.accepted_hash_strategies(Robot 配置)。默认仅放行 v1。运维可用它单独关停某个算法版本而无需改代码。

3. Phase 1 — 协商

POST /v1/memory/dpe:negotiate
Content-Type: application/vnd.tfrs.dpe.v1+json
Content-Encoding: gzip
Authorization: Bearer <RS256 JWT>
X-TFRS-Robot-Id: <rid>

X-TFRS-Robot-Id 可选,仅用于防误路由校验(不匹配时返回 RobotIdMismatchError 语义的错误),不是路由键——见 index.md 的「设计基线」章节。

请求体(manifest)

{
  "hash_strategy_uri": "hash-strategy://default?algo_version=v1",
  "file_uri": "dpe://acme-crm/handbook",
  "file_type": "markdown",
  "doc_metadata": {},
  "doc_hash": "a1b2c3…",
  "base_doc_hash": "0f9e8d…",
  "pages": [
    {
      "number": 0,
      "title": "第一章",
      "page_hash": "c3d4e5…",
      "elements": [
        { "content_hash": "e5f6a7…" },
        { "content_hash": "b8c9d0…" }
      ]
    }
  ]
}

manifest 的三条规格决定

决定 理由
必须含 page.number 内核 LCS 按 page.number 配对新旧页(unified_memory.py 的 old_by_num / new_by_num),doc_hash 亦按 number 排序取 page_hash 序列。缺此字段则无法重建页身份
不含 seq_in_page HashStrategy.attach() 在装配阶段按元素数组顺序 enumerate 赋 0..N-1。数组顺序即阅读顺序——不设冗余字段,即从结构上排除「顺序与 seq 不一致」这类歧义
page_hash / doc_hash 是提示,不是断言 见 §5 权威模型

!!! warning "客户端必须维持稳定的 page 编号" 由于页按 number 配对,在文档头部插入一页会使后续所有 number 位移,导致配对全部失配——LCS 将退化为「全部重建」。

增量收益依赖编号稳定。客户端应为每页分配**稳定且持久**的编号(例如以页内容指纹派生,或维护本地编号映射),而非每次投递重新从 0 顺序编号。

响应

{
  "status": "gap",
  "missing_content_hashes": ["e5f6a7…"],
  "server_doc_exists": true,
  "server_doc_hash": "0f9e8d…",
  "base_doc_hash_matched": true
}

status 取值:

值 含义 客户端动作
unchanged server_doc_hash == manifest.doc_hash 无需 Phase 2,投递完成
gap 存在缺失内容 携带 contents 进入 Phase 2
new 服务端无此文档(首次投递) 补传全部内容进入 Phase 2

缺口的作用域

!!! danger "安全硬约束:缺口池必须按 file_uri 限定" missing = manifest 的 hash 集合 − **本文档旧树**的 hash 集合

**绝不可**改用全库 hash 池。若服务端跨文档解析 hash,客户端即可通过构造 manifest 引用**它无权访问的其它文档**的内容,并借由「服务端补齐」把该内容写入自己的文档,随后正常读取——这是一条**越权内容外泄路径**。

实现上不需要内核新增原语。内核 get_doc_by_file_uri() 已能拉取旧树全树,在内存中做集合差即可。不要直连内核表编写 SQL 查询 hash——DPE schema 由内核拥有,绕过其 API 会形成脆弱的隐式耦合。

4. Phase 2 — 投递

POST /v1/memory/dpe:commit
Content-Type: application/vnd.tfrs.dpe.v1+json
Content-Encoding: gzip
Authorization: Bearer <RS256 JWT>
Idempotency-Key: <client-uuid>
X-TFRS-Robot-Id: <rid>
{
  "manifest": { "…同 §3…" },
  "contents": [
    {
      "content_hash": "e5f6a7…",
      "element": { "…完整 element payload(含 category / text / ele_metadata)…" }
    }
  ]
}

contents[].content_hash 有两个用途:① 与 manifest 中的槽位匹配;② 作为早失败断言(见 §5 权威模型)。

响应

{
  "status": "applied",
  "doc_hash": "f7a8b9…",
  "hash_strategy_uri": "hash-strategy://default?algo_version=v1",
  "counts": {
    "elements_added": 3,
    "elements_removed": 1,
    "elements_synced": 0,
    "elements_unchanged": 996,
    "llm_calls_saved": 1992
  }
}

status 取值 created / unchanged / updated / rebuilt / applied,counts 直接映射内核 UpsertResult。

!!! tip "返回的 doc_hash 是服务端权威值" 客户端应存储该值作为下次投递的 base_doc_hash。协议因此不需要另建版本号系统——内容寻址 hash 本身就是现成的乐观并发令牌。

5. 权威模型:服务端重算

这是本协议最重要的设计决策。

数据 权威方 客户端提供的值用于
element 内容 客户端 直接写入
content_hash 服务端(attach() 重算) ① 匹配 manifest 槽位 ② 早失败断言
page_hash / doc_hash 服务端 仅 Phase 1 协商,永不入库

服务端收到 Phase 2 请求后的处理:

  1. 构造 Document,传入由 hash_strategy_uri 解析出的 strategy 实例。内核 Document 的构造校验器会据此自动调用 HashStrategy.attach(),重算三层 hash 与 seq_in_page。
  2. 调用内核 upsert_doc() —— 唯一的 reconcile 入口。

由此得到的性质

!!! tip "协议是 fail-safe 的" 客户端 hash 算错、算偏、甚至伪造,都不会污染数据库。最坏情况是协商未命中、多传输了一些内容,服务端最终仍会算出正确答案。

这是「服务端重算」相对于「信任客户端 hash + 校验后拒绝」的关键优势:后者需要定义失败恢复路径,前者不需要。

唯一的 fail-loud 点

contents[].content_hash 与服务端重算结果不符 → 立即拒绝(DPE_CONTENT_HASH_MISMATCH)。

理由:这是 SDK 实现存在缺陷的信号,必须在边界上拦截,绝不能落库。一旦错误的 hash 进入数据库,Merkle 不变量即告失守,此后所有同步都会出错。

第 0 条校验规则

!!! danger "必须显式拒绝客户端提交 hash 字段" 接口必须拒绝客户端将 doc_hash / page_hash / content_hash / seq_in_page 作为文档字段提交。hash_strategy_uri 只允许出现在 manifest 顶层作为协商参数。

内核 `Document` 的构造校验器存在**第二条路径**:若 data 自带 `hash_strategy_uri` + `doc_hash`,将跳过 `attach()` 直接通过校验(DB 读回路径)。外部数据不可信,必须钉死走 attach 路径,否则客户端可自证 hash 混入数据库。

6. 认证与授权

复用项目既有的 RS256 JWT + scope 体系。建议新增 scope dpe:push。

!!! warning "不要复用 config:write" 复用会让外部集成方获得改写机器人配置(LLM 密钥、工具、链)的权限——这是提权。外部集成方只应能投递内容。

新增 scope 的成本需计入:scope 词表 SoT 在 TFRSManager(M0/TFRM-164),需跨项目协调,并需 bump CONTRACT_VERSION、更新 CLAUDE.md 的 Contract Registry 表。

路由挂载需在 main.py 挂 enforce_scope,并在 security/scope_contract.py 的 _RULES 中登记(默认拒绝 CI 会拦截未分类路由)。

7. 错误模型

协议错误寄生在项目既有的 {code, message, data} 信封内(TFRS-481 契约,唯一构造点为 error_envelope.py::error_response)——不引入 RFC 9457 第二套错误体,那会与前端 BFF 的解析逻辑冲突。

code 承载协议错误码,data 承载机器可读细节。

HTTP code 可恢复 data 内容
400 DPE_MANIFEST_INVALID 否 violations[]
400 DPE_CONTENT_HASH_MISMATCH 否 expected / actual / element_index
400 DPE_HASH_STRATEGY_UNSUPPORTED 否 accepted[](服务端 allowlist)
409 DPE_INSUFFICIENT_CONTENT 是 missing_content_hashes[]
413 DPE_PAYLOAD_TOO_LARGE 否 max_payload_bytes
415 DPE_UNSUPPORTED_PROTOCOL_VERSION 否 supported[]
429 DPE_RATE_LIMITED 是 附 Retry-After
503 DPE_ROBOT_UNAVAILABLE 是 附 Retry-After

!!! warning "两条硬约束" 1. 绝不使用 422 —— 项目的 422 被 RequestValidationError 占用且有意保留 pydantic 原生形状(TFRS-385)。协议错误一律用 400。 2. 绝不动 RequestValidationError 处理器 —— 两个错误契约不要「顺手统一」。

8. 幂等与重试

机制 做法
幂等键 Idempotency-Key: <client-uuid> 头。服务端按 (scope, rid, key) 去重,并回放首次响应
并发语义 last-writer-wins,整文档覆盖。base_doc_hash 仅诊断;不匹配时在响应中回 base_doc_hash_matched: false 告警,不报错
重试 negotiate 天然幂等(只读);commit 依赖幂等键。429 / 503 按 Retry-After 退避

!!! note "并发语义为何是「无合并」" 服务端以 manifest 的结构 + 补传内容整体重建文档,与全量推送语义完全一致。协议不引入任何新的并发语义——这是它能保持简单、且相对全量推送「零行为差异」的保证。

9. 版本治理与废弃

场景 做法
新增可选字段 同协议版本内直接添加。接收方 must-ignore 未知字段
破坏性变更 新 media type application/vnd.tfrs.dpe.v2+json;旧版至少保留 2 个发布周期
算法版本废弃 能力文档标 deprecated_at;协商时服务端回 Deprecation / Sunset 响应头(RFC 8594)
已存文档使用旧算法 不需要迁移 —— 内核 hash_strategy_uri 不匹配时自动走 _full_rebuild。协议只需在响应中如实回传 hash_strategy_uri,客户端自行发现版本变化

!!! warning "must-ignore 与 must-understand 必须逐字段明确" 一刀切 must-ignore 对数据完整性协议是危险的(静默丢字段);一刀切 must-understand 则断掉前向兼容。

规则:**结构承载字段 must-understand(未知即拒绝),`x-` 前缀扩展位 must-ignore。**

10. 明确不支持

以下项在 v1 中明确不支持,写进规范以避免第三方猜测:

项 原因
image-source 策略 该策略的 doc_hash = source_file_hash,需要源文件字节;而 DPE-only 投递没有源字节。这是唯一可能需要向 TFROB 提能力需求的地方——若业务必须支持,需内核允许客户端自带 source_file_hash
image_path 图片通道 现有链路强制图片相对路径以 {robot_id}/ 开头(隔离规则)。新端点不复用该检查,隔离规则需另行设计。v1 直接禁止 image_path,要求使用 image_base64
file_uri 命名空间冲突 file_uri 在 PG 中 UNIQUE。外部系统 URI 必须走保留 scheme(推荐 dpe://{tenant}/{path})与内部 feishu / cos 源隔离,否则可能撞键
部分提交 / 断点续传 内核 upsert_doc 是整文档事务,无法部分提交。靠幂等键 + gzip 缓解;续传列为 v2 候选

11. 相关文档