|
| 1 | +# ClawNet 团队协作请求:P2P 事件订阅代理(Event Subscription Proxy) |
| 2 | + |
| 3 | +| 字段 | 值 | |
| 4 | +| --- | --- | |
| 5 | +| 优先级 | **P2 — 用户体验优化(非阻塞性)** | |
| 6 | +| 提出方 | TelagentNode 团队 | |
| 7 | +| 提出日期 | 2026-03-09 | |
| 8 | +| 影响范围 | DID 远程访问时的实时消息推送 | |
| 9 | +| 当前临时方案 | Webapp 轮询 HTTP API(活跃 3s / 空闲 15s) | |
| 10 | +| 前置依赖 | DID 远程接入(API Proxy)已实现 | |
| 11 | + |
| 12 | +--- |
| 13 | + |
| 14 | +## 1. 问题背景 |
| 15 | + |
| 16 | +### 1.1 当前架构 |
| 17 | + |
| 18 | +TelagentNode 已实现基于 DID 的远程节点接入(API Proxy),Webapp 可通过网关节点访问 NAT 内网的目标节点 REST API: |
| 19 | + |
| 20 | +``` |
| 21 | +Webapp ──HTTP──► Gateway Node ──P2P──► Target Node (NAT 内) |
| 22 | + │ |
| 23 | + ClawNet WS 订阅 |
| 24 | + topic: telagent/* |
| 25 | +``` |
| 26 | + |
| 27 | +目标节点通过 **WebSocket 订阅 ClawNet** 获取实时 P2P 消息(信封、回执、群组同步等),响应延迟 <100ms。 |
| 28 | + |
| 29 | +但 **Webapp 与节点之间** 没有实时通道,全部通过 HTTP 轮询获取更新: |
| 30 | + |
| 31 | +| 轮询内容 | 间隔 | 端点 | |
| 32 | +|---------|------|------| |
| 33 | +| 当前会话新消息 | 3 秒 | `GET /api/v1/messages/pull?conversation_id=X` | |
| 34 | +| 全局新消息 + 会话列表 | 15 秒 | `GET /api/v1/messages/pull` + `GET /api/v1/conversations` | |
| 35 | +| 撤回消息 | 15 秒 | `GET /api/v1/messages/retracted` | |
| 36 | + |
| 37 | +### 1.2 本地连接 vs DID 远程连接 |
| 38 | + |
| 39 | +**本地连接**(Webapp 直连本机节点): |
| 40 | + |
| 41 | +- 轮询延迟可接受(3s 内可见新消息,localhost 无网络延迟) |
| 42 | +- 后续可在节点本地加 SSE/WebSocket 端点直接推送(纯 TelagentNode 改造,不需要 ClawNet) |
| 43 | + |
| 44 | +**DID 远程连接**(Webapp 通过网关中继访问远端节点): |
| 45 | + |
| 46 | +- 每次轮询经过完整 P2P round-trip(Webapp → Gateway → P2P → Target → P2P → Gateway → Webapp) |
| 47 | +- 单次 round-trip 延迟 200-800ms,3s 轮询间隔意味着大量浪费的空查询 |
| 48 | +- 15s 全局轮询延迟导致新消息通知严重滞后 |
| 49 | + |
| 50 | +### 1.3 理想架构 |
| 51 | + |
| 52 | +``` |
| 53 | + ┌─ SSE/WS ──── Webapp |
| 54 | + │ |
| 55 | +Gateway Node ◄═══ P2P 事件订阅 ═══► Target Node |
| 56 | + │ │ |
| 57 | + │ ClawNet WS 订阅 |
| 58 | + │ topic: telagent/* |
| 59 | + │ |
| 60 | + └── 收到 Target 的 |
| 61 | + 新消息事件后 |
| 62 | + 立即推送给 Webapp |
| 63 | +``` |
| 64 | + |
| 65 | +当目标节点收到新的 P2P 消息(信封、回执等)时,网关节点能立即得到通知,并推送给已连接的 Webapp 客户端。 |
| 66 | + |
| 67 | +--- |
| 68 | + |
| 69 | +## 2. 现有方案评估 |
| 70 | + |
| 71 | +### 2.1 方案 A:纯应用层 — 事件转发(不需要 ClawNet 改动) |
| 72 | + |
| 73 | +TelagentNode 可在应用层完全自建事件转发,不依赖 ClawNet 新能力: |
| 74 | + |
| 75 | +``` |
| 76 | +1. Webapp 通过 Gateway 向 Target 发送"订阅注册"请求 |
| 77 | + POST /relay/{targetDid}/api/v1/events/subscribe |
| 78 | + Body: { gatewayDid, sessionId, topics: ["envelope", "receipt"] } |
| 79 | +
|
| 80 | +2. Target Node 维护一张 subscribers 表: |
| 81 | + Map<gatewayDid, { sessionIds, subscribedTopics, lastPingMs }> |
| 82 | +
|
| 83 | +3. 当 Target 处理完新消息后,向所有注册的 Gateway 推送轻量事件: |
| 84 | + P2P Topic: telagent/event-push |
| 85 | + Payload: { sessionId, event: "new-envelope", conversationId, envelopeId, atMs } |
| 86 | +
|
| 87 | +4. Gateway 收到 event-push → 查找对应的 SSE/WS 连接 → 转发给 Webapp |
| 88 | +
|
| 89 | +5. Webapp 收到通知 → 立即 fetch 具体数据(增量拉取,非全量轮询) |
| 90 | +``` |
| 91 | + |
| 92 | +**此方案的问题:** |
| 93 | + |
| 94 | +| 问题 | 影响 | |
| 95 | +|------|------| |
| 96 | +| Target 必须追踪所有 Gateway 订阅状态 | 增加复杂度、状态管理负担 | |
| 97 | +| 订阅注册/注销/超时清理都需要自建 | 大量 edge case(Target 重启、Gateway 断连) | |
| 98 | +| Target 每条消息都要额外向 N 个 Gateway 发送通知 | 放大 P2P 消息量,O(N) | |
| 99 | +| 无法利用 ClawNet 原生的消息投递保障 | 通知可能丢失,需自建重试 | |
| 100 | +| Target 是唯一知道"有新消息到达"的节点 | 瓶颈在 Target 的事件分发 | |
| 101 | + |
| 102 | +**结论:可行但笨重。** 本质上是在 ClawNet P2P 之上重新构建了一套发布-订阅系统。 |
| 103 | + |
| 104 | +### 2.2 方案 B:ClawNet 层 — 事件订阅代理(需要 ClawNet 支持)⭐️ 推荐 |
| 105 | + |
| 106 | +ClawNet 在协议层支持"授权代理订阅":一个 DID(Gateway)可由另一个 DID(Target)授权,接收发送给 Target 的特定 topic 消息副本。 |
| 107 | + |
| 108 | +**核心优势:Target 节点无需做任何额外工作。** ClawNet 在消息投递时自动向已授权的代理也投递一份副本。 |
| 109 | + |
| 110 | +--- |
| 111 | + |
| 112 | +## 3. 期望 ClawNet 提供的能力 |
| 113 | + |
| 114 | +### 3.1 授权 API — 目标节点发出授权 |
| 115 | + |
| 116 | +```typescript |
| 117 | +// Target Node 授权 Gateway 代理订阅自己的指定 topic |
| 118 | +const delegation = await client.messaging.createSubscriptionDelegation({ |
| 119 | + delegateDid: 'did:claw:zGateway...', // 被授权方 DID |
| 120 | + topics: ['telagent/envelope', 'telagent/receipt', 'telagent/group-sync'], |
| 121 | + expiresInSec: 3600, // 授权有效期(秒) |
| 122 | + // 可选:只转发 metadata,不转发完整 payload |
| 123 | + metadataOnly: true, |
| 124 | +}); |
| 125 | +// 返回: { delegationId: string, expiresAtMs: number } |
| 126 | + |
| 127 | +// Target Node 撤销授权 |
| 128 | +await client.messaging.revokeSubscriptionDelegation({ |
| 129 | + delegationId: 'dlg_xxx', |
| 130 | +}); |
| 131 | + |
| 132 | +// Target Node 查看当前授权列表 |
| 133 | +const delegations = await client.messaging.listSubscriptionDelegations(); |
| 134 | +// 返回: [{ delegationId, delegateDid, topics, expiresAtMs, createdAtMs }] |
| 135 | +``` |
| 136 | + |
| 137 | +### 3.2 代理订阅 API — 网关节点使用授权 |
| 138 | + |
| 139 | +```typescript |
| 140 | +// Gateway Node 使用授权订阅 Target 的消息 |
| 141 | +const unsub = await client.messaging.subscribeDelegated({ |
| 142 | + delegationId: 'dlg_xxx', |
| 143 | + onMessage: (msg: DelegatedMessage) => { |
| 144 | + // msg.originalTargetDid — 消息原始目标 DID |
| 145 | + // msg.sourceDid — 消息来源 DID |
| 146 | + // msg.topic — 原始 topic |
| 147 | + // msg.payload — 完整 payload(如果 metadataOnly=false) |
| 148 | + // 或 msg.metadata — 仅元数据(如果 metadataOnly=true) |
| 149 | + // msg.seq — 序列号(支持 sinceSeq 重连) |
| 150 | + } |
| 151 | +}); |
| 152 | +``` |
| 153 | + |
| 154 | +### 3.3 REST API 方式(如果 SDK 暂不支持) |
| 155 | + |
| 156 | +``` |
| 157 | +# 创建授权(Target Node 调用) |
| 158 | +POST /api/v1/messaging/subscription-delegations |
| 159 | +{ |
| 160 | + "delegateDid": "did:claw:zGateway...", |
| 161 | + "topics": ["telagent/envelope", "telagent/receipt"], |
| 162 | + "expiresInSec": 3600, |
| 163 | + "metadataOnly": true |
| 164 | +} |
| 165 | +→ { "delegationId": "dlg_abc123", "expiresAtMs": 1741564800000 } |
| 166 | +
|
| 167 | +# 撤销授权(Target Node 调用) |
| 168 | +DELETE /api/v1/messaging/subscription-delegations/{delegationId} |
| 169 | +
|
| 170 | +# 代理订阅(Gateway Node 调用) |
| 171 | +WS /api/v1/messaging/subscribe-delegated?delegationId=dlg_abc123&sinceSeq=0 |
| 172 | +→ 帧格式同现有 messaging/subscribe,增加 originalTargetDid 字段 |
| 173 | +``` |
| 174 | + |
| 175 | +### 3.4 metadataOnly 模式(推荐默认开启) |
| 176 | + |
| 177 | +为了减少数据传输和保护隐私,推荐支持"仅转发元数据"模式: |
| 178 | + |
| 179 | +```typescript |
| 180 | +// metadataOnly: true 时,Gateway 收到的消息: |
| 181 | +{ |
| 182 | + "type": "delegated-message", |
| 183 | + "originalTargetDid": "did:claw:zTarget...", |
| 184 | + "sourceDid": "did:claw:zPeerC...", |
| 185 | + "topic": "telagent/envelope", |
| 186 | + "metadata": { |
| 187 | + "messageId": "msg_xxx", |
| 188 | + "seq": 456, |
| 189 | + "payloadSizeBytes": 2048, |
| 190 | + "receivedAtMs": 1741564800000 |
| 191 | + } |
| 192 | + // 注意:不包含 payload 内容 |
| 193 | +} |
| 194 | +``` |
| 195 | + |
| 196 | +**Gateway 不需要消息全文**,只需要知道"Target 收到了新消息"这一事件即可。Gateway 收到通知后推送给 Webapp,Webapp 再通过 API Proxy 拉取实际消息内容。 |
| 197 | + |
| 198 | +这样设计的好处: |
| 199 | +- **隐私安全**:Gateway 看不到消息 payload,只知道"有新消息到达" |
| 200 | +- **带宽节省**:元数据 ~100 bytes vs 完整信封 ~2-5 KB |
| 201 | +- **授权粒度**:Target 可以随时撤销,不影响消息本身的安全性 |
| 202 | + |
| 203 | +--- |
| 204 | + |
| 205 | +## 4. TelagentNode 侧的使用场景 |
| 206 | + |
| 207 | +### 4.1 完整事件流(有 ClawNet 支持时) |
| 208 | + |
| 209 | +``` |
| 210 | + Webapp Gateway Node ClawNet Target Node |
| 211 | + │ │ │ │ |
| 212 | + ├── SSE Connect ───────►│ │ │ |
| 213 | + │ /relay/{did}/events │ │ │ |
| 214 | + │ │ │ │ |
| 215 | + │ ├── API Proxy ──────────────────────────────►│ |
| 216 | + │ │ POST /events/subscribe │ |
| 217 | + │ │ { gatewayDid } │ |
| 218 | + │ │ │ │ |
| 219 | + │ │ │◄── createDelegation ┤ |
| 220 | + │ │ │ { delegateDid, │ |
| 221 | + │ │ │ topics, │ |
| 222 | + │ │ │ metadataOnly } │ |
| 223 | + │ │ │ │ |
| 224 | + │ │◄── delegationId ──────────────────────────── │ |
| 225 | + │ │ │ │ |
| 226 | + │ ├── subscribeDelegated ──► │ |
| 227 | + │ │ (WS to ClawNet) │ │ |
| 228 | + │ │ │ │ |
| 229 | + │ │ ╔══════════════════╗ │ |
| 230 | + │ │ ║ Peer C sends ║ │ |
| 231 | + │ │ ║ message to ║──────────────────►│ |
| 232 | + │ │ ║ Target ║ │ |
| 233 | + │ │ ╚══════════════════╝ │ |
| 234 | + │ │ │ │ |
| 235 | + │ │◄── delegated-message ──┤ │ |
| 236 | + │ │ { topic, metadata } │ │ |
| 237 | + │ │ │ │ |
| 238 | + │◄── SSE event ─────────┤ │ │ |
| 239 | + │ { type: "new-envelope", │ │ |
| 240 | + │ conversationId } │ │ │ |
| 241 | + │ │ │ │ |
| 242 | + ├── fetch (API Proxy) ──►──────────────────────────────────────────►│ |
| 243 | + │ GET /messages/pull │ │ │ |
| 244 | + │◄── actual messages ───┤◄─────────────────────────────────────────── │ |
| 245 | +``` |
| 246 | + |
| 247 | +### 4.2 降级方案(无 ClawNet 支持时) |
| 248 | + |
| 249 | +如果 ClawNet 暂不实现,TelagentNode 将自行实现方案 A(应用层事件转发),使用新的 P2P topic: |
| 250 | + |
| 251 | +- `telagent/event-subscribe` — Gateway → Target:注册订阅 |
| 252 | +- `telagent/event-unsubscribe` — Gateway → Target:取消订阅 |
| 253 | +- `telagent/event-push` — Target → Gateway:事件推送 |
| 254 | +- `telagent/event-heartbeat` — 双向心跳保活 |
| 255 | + |
| 256 | +我们优先希望减少自建复杂度,所以如果 ClawNet 能在协议层支持,效果会好很多。 |
| 257 | + |
| 258 | +--- |
| 259 | + |
| 260 | +## 5. 对 ClawNet P2P 层的具体要求 |
| 261 | + |
| 262 | +| 能力 | 说明 | 优先级 | |
| 263 | +|-----|------|--------| |
| 264 | +| 创建订阅授权 | Target DID 授权 Gateway DID 代理订阅指定 topics | **必须** | |
| 265 | +| 撤销授权 | Target DID 可随时撤销 | **必须** | |
| 266 | +| 授权有效期 | 支持 TTL 自动过期 | **必须** | |
| 267 | +| 代理 WebSocket 订阅 | Gateway 通过 WS 接收 Target 的消息副本 | **必须** | |
| 268 | +| sinceSeq 重连恢复 | 代理订阅断线后可从断点续接 | **必须** | |
| 269 | +| metadataOnly 模式 | 只转发消息元数据不含 payload | **强烈推荐** | |
| 270 | +| 多 Gateway 支持 | 一个 Target 可授权多个 Gateway | 推荐 | |
| 271 | +| 授权列表查询 | Target 查看/管理所有授权 | 推荐 | |
| 272 | +| 授权配额限制 | 防滥用:每个 DID 最多 N 个活跃授权 | 推荐 | |
| 273 | + |
| 274 | +--- |
| 275 | + |
| 276 | +## 6. 安全考量 |
| 277 | + |
| 278 | +### 6.1 授权模型 |
| 279 | + |
| 280 | +- **单向授权**:只有 Target 能创建授权,Gateway 无法自行订阅 |
| 281 | +- **Scope 限制**:授权只能指定具体 topic,不能用 `*` 通配 |
| 282 | +- **TTL 强制**:授权必须有有效期,到期自动清除 |
| 283 | +- **撤销即失效**:Target 撤销后 ClawNet 立即停止向 Gateway 投递 |
| 284 | + |
| 285 | +### 6.2 隐私保护 |
| 286 | + |
| 287 | +- `metadataOnly=true` 时 Gateway 无法获取消息内容 |
| 288 | +- Gateway 只知道"Target 收到了来自某 DID 的某 topic 消息" |
| 289 | +- Webapp 获取实际内容仍需通过 API Proxy 调用目标节点(经目标节点鉴权) |
| 290 | + |
| 291 | +### 6.3 防滥用 |
| 292 | + |
| 293 | +- 每个 DID 的活跃授权数量应有上限(建议 10) |
| 294 | +- 代理订阅产生的消息投递应计入 Target 的配额 |
| 295 | +- 恶意 Gateway 无法扩大授权范围 |
| 296 | + |
| 297 | +--- |
| 298 | + |
| 299 | +## 7. 不需要 ClawNet 改动的部分 |
| 300 | + |
| 301 | +以下工作由 TelagentNode 团队自行完成,无需 ClawNet 支持: |
| 302 | + |
| 303 | +| 工作项 | 说明 | |
| 304 | +|--------|------| |
| 305 | +| 节点本地 SSE 端点 | `GET /api/v1/events`(SSE),推送新消息/回执/撤回事件给本地 Webapp | |
| 306 | +| Gateway SSE 转发 | `GET /relay/{targetDid}/api/v1/events`,Gateway → Webapp 的 SSE 桥接 | |
| 307 | +| Webapp SSE 客户端 | 替换 `usePollMessages` 轮询为 EventSource 自动重连 | |
| 308 | +| 事件类型定义 | `new-envelope`, `receipt`, `retraction`, `conversation-update` 等 | |
| 309 | + |
| 310 | +--- |
| 311 | + |
| 312 | +## 8. 时间线建议 |
| 313 | + |
| 314 | +| 阶段 | 内容 | 依赖 | |
| 315 | +|------|------|------| |
| 316 | +| **v0.2.x(近期)** | TelagentNode 实现本地 SSE 端点(无 ClawNet 依赖) | 无 | |
| 317 | +| **v0.3.0** | 如有 ClawNet 支持 → 实现代理订阅方案(方案 B) | ClawNet 订阅代理 API | |
| 318 | +| **v0.3.0 降级** | 如无 ClawNet 支持 → 自建应用层事件转发(方案 A) | 无 | |
| 319 | + |
| 320 | +--- |
| 321 | + |
| 322 | +## 9. 参考 |
| 323 | + |
| 324 | +- DID 远程接入实现文档:`docs/implementation/did-remote-access-implementation.md` |
| 325 | +- P2P 消息 RFC:`docs/design/p2p-messaging-rfc.md` |
| 326 | +- 当前 Webapp 轮询逻辑:`packages/webapp/src/hooks/use-poll-messages.ts` |
| 327 | +- API Proxy Service:`packages/node/src/services/api-proxy-service.ts` |
| 328 | +- ClawNet Transport Service:`packages/node/src/services/clawnet-transport-service.ts` |
0 commit comments