@agentunion/fastaun-browser 0.4.3 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +190 -178
- package/_packed_docs/AUN_SDK_0.4.0_/350/256/276/350/256/241/345/257/271/346/257/224/345/210/206/346/236/220.md +194 -194
- package/_packed_docs/AUN_SDK_/351/207/215/346/236/204/345/256/236/346/226/275/350/256/241/345/210/222.md +596 -596
- package/_packed_docs/AUN_SDK_/351/207/215/346/236/204/350/256/276/350/256/241/346/226/271/346/241/210_v3.md +1698 -1697
- package/_packed_docs/CHANGELOG.md +190 -178
- package/_packed_docs/INDEX.md +17 -17
- package/_packed_docs/KITE_DOCS_GUIDE.md +11 -11
- package/_packed_docs/agent.md/SCHEMA.md +49 -49
- package/_packed_docs/agent.md/examples/signed-openclaw-lobster.md +22 -22
- package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +327 -327
- package/_packed_docs/cli/AUN-CLI/350/256/276/350/256/241/346/226/207/346/241/243.md +686 -686
- package/_packed_docs/design/2026-05-22-aun-rpc-trace-enhancement.md +542 -542
- package/_packed_docs/design/E2EE_V2/347/256/200/345/214/226/344/270/2721DH/345/212/240Per-AID_Wrap/346/226/271/346/241/210.md +124 -124
- package/_packed_docs/design//350/267/250/350/257/255/350/250/200/345/256/271/345/231/250E2E/346/265/213/350/257/225/346/226/271/346/241/210.md +665 -665
- package/_packed_docs/protocol/01-/350/272/253/344/273/275/344/270/216/345/207/255/350/257/201/345/215/217/350/256/256-auth.md +2 -2
- package/_packed_docs/protocol/14-/344/272/244/344/272/222/346/234/272/345/210/266-/345/223/215/345/272/224/346/250/241/345/274/217/344/270/216/350/207/252/344/270/273/346/250/241/345/274/217.md +170 -170
- package/_packed_docs/protocol/15-/347/246/273/347/272/277/346/216/250/351/200/201/351/200/232/347/237/245/345/215/217/350/256/256.md +419 -419
- package/_packed_docs/protocol/README.md +1 -1
- package/_packed_docs/protocol/aun-docs-guide.md +1 -1
- package/_packed_docs/protocol//351/231/204/345/275/225A-/346/234/257/350/257/255/350/241/250.md +15 -15
- package/_packed_docs/protocol//351/231/204/345/275/225B-/346/211/251/345/261/225/346/200/247/346/214/207/345/215/227.md +4 -4
- package/_packed_docs/protocol//351/231/204/345/275/225J-/345/256/242/346/210/267/347/253/257/346/216/245/345/205/245/347/244/272/344/276/213.md +98 -98
- package/_packed_docs/protocol//351/231/204/345/275/225M-JWT/350/256/244/350/257/201/345/256/236/347/216/260/346/214/207/345/215/227.md +46 -46
- package/_packed_docs/protocol//351/231/204/345/275/225N-/345/210/206/345/270/203/345/274/217Trace/345/215/217/350/256/256.md +257 -257
- package/_packed_docs/python-sdk-v2-only-changelog.md +189 -189
- package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +1 -1
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +1 -0
- package/_packed_docs/sdk/09-payload-reference.md +13 -13
- package/_packed_docs/sdk/E2EE_V2/346/266/210/346/201/257/351/200/232/344/277/241/346/227/266/345/272/217/345/233/276.md +171 -171
- package/dist/aid.d.ts +2 -1
- package/dist/aid.d.ts.map +1 -1
- package/dist/aid.js +7 -6
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +4 -0
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +237 -149
- package/dist/client.d.ts +7 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +238 -153
- package/dist/client.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -1,328 +1,328 @@
|
|
|
1
|
-
# 远程 agent.md 缓存与 ETag 透传方案
|
|
2
|
-
|
|
3
|
-
状态:方案草案
|
|
4
|
-
|
|
5
|
-
## 目标
|
|
6
|
-
|
|
7
|
-
让 SDK 在给对端发送消息、收到对端消息时,都能观察到对端云端 `agent.md` 的最新 ETag,并据此维护本机远程 `agent.md` 缓存状态。
|
|
8
|
-
|
|
9
|
-
核心目标:
|
|
10
|
-
|
|
11
|
-
- 每个远程 AID 在 SDK 本地内存中维护一条 `agent.md` 记录,包含 `remote_etag`、`local_etag`、`content`、`last_modified` 等字段。
|
|
12
|
-
- 同一条远程缓存记录持久化到本地 SQLite 表,SDK 启动或内存 miss 时按需加载。
|
|
13
|
-
- `message.send` 的 RPC 响应携带接收方 `agent.md` ETag,让发送方更新 `to` 的云端版本。
|
|
14
|
-
- 接收端收到消息信封时携带发送方 `agent.md` ETag,让接收方更新 `from` 的云端版本。
|
|
15
|
-
- ETag 只作为版本提示,不替代 `agent.md` 内容下载和验签。
|
|
16
|
-
|
|
17
|
-
## 可行性结论
|
|
18
|
-
|
|
19
|
-
方案可行。服务端已有 `agent.md` HEAD/ETag 能力,Gateway 当前也已经在 RPC response `_meta.agent_md_etag` 中注入“请求者自己”的服务端 ETag。需要扩展两条路径:
|
|
20
|
-
|
|
21
|
-
1. `message.send` / V2 P2P send:把 `from` 的 `agent.md` ETag 注入消息信封,随消息到达接收端。
|
|
22
|
-
2. `message.send` RPC response:把 `to` 的 `agent.md` ETag 注入响应 `_meta`,返回发送端。
|
|
23
|
-
|
|
24
|
-
注意:服务端注入的 ETag 只能代表云端版本。SDK 本地必须区分“观察到的远端云端 ETag”和“当前本地内容对应的 ETag”。字段命名固定为 `remote_etag` 和 `local_etag`,其中 `remote_etag` 表示远端云端版本,`local_etag` 表示本地 `content` 对应版本。不能在只有远端 ETag、没有内容的情况下直接覆盖 `local_etag`,否则后续 `If-None-Match` 命中 304 时本地可能没有可用内容。
|
|
25
|
-
|
|
26
|
-
## 字段建议
|
|
27
|
-
|
|
28
|
-
消息信封新增字段:
|
|
29
|
-
|
|
30
|
-
```json
|
|
31
|
-
{
|
|
32
|
-
"agent_md": {
|
|
33
|
-
"sender": {
|
|
34
|
-
"aid": "alice.agentid.pub",
|
|
35
|
-
"etag": "\"sha256...\""
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
`message.send` RPC response 的 `_meta` 新增字段:
|
|
42
|
-
|
|
43
|
-
```json
|
|
44
|
-
{
|
|
45
|
-
"_meta": {
|
|
46
|
-
"agent_md_etag": "\"sender-self-etag\"",
|
|
47
|
-
"agent_md_etags": {
|
|
48
|
-
"to": {
|
|
49
|
-
"aid": "bob.agentid.pub",
|
|
50
|
-
"etag": "\"sha256...\""
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
兼容说明:
|
|
58
|
-
|
|
59
|
-
- `_meta.agent_md_etag` 保持现有语义,仍表示请求者自己在服务端的 `agent.md` ETag。
|
|
60
|
-
- `_meta.agent_md_etags.to` 表示本次消息接收方的 `agent.md` ETag。
|
|
61
|
-
- `envelope.agent_md.sender` 表示本条消息发送方的 `agent.md` ETag。
|
|
62
|
-
- 字段缺失、ETag 为空、HEAD 失败均不影响消息收发。
|
|
63
|
-
|
|
64
|
-
## SDK 缓存模型
|
|
65
|
-
|
|
66
|
-
每个远程 AID 在内存和 SQLite 表中都维护一条缓存记录。内存记录与 SQLite 表记录字段语义一致,SQLite 用于 SDK 重启或内存 miss 时按需加载。
|
|
67
|
-
|
|
68
|
-
| 字段 | 含义 |
|
|
69
|
-
| --- | --- |
|
|
70
|
-
| `aid` | 远程 AID |
|
|
71
|
-
| `content` | 本地缓存的完整 `agent.md` 内容,可为空 |
|
|
72
|
-
| `local_etag` | 当前 `content` 对应的 ETag,只能由 GET 200/304 确认 |
|
|
73
|
-
| `remote_etag` | 从消息信封或 RPC `_meta` 观察到的远端云端 ETag |
|
|
74
|
-
| `last_modified` | GET 响应的 `Last-Modified` |
|
|
75
|
-
| `fetched_at` | 最近一次成功确认内容的本机时间 |
|
|
76
|
-
| `observed_at` | 最近一次观察到远端 ETag 的本机时间 |
|
|
77
|
-
| `stale` | `remote_etag` 与 `local_etag` 不一致,或有远端 ETag 但无内容 |
|
|
78
|
-
| `verify_status` | 最近一次 `verify_agent_md` 结果:`verified` / `unsigned` / `invalid` |
|
|
79
|
-
|
|
80
|
-
状态规则:
|
|
81
|
-
|
|
82
|
-
- 收到远端 ETag 时,只更新 `remote_etag` 和 `observed_at`。
|
|
83
|
-
- 只有下载到内容并完成验签后,才能更新 `content`、`local_etag`、`last_modified`、`fetched_at`。
|
|
84
|
-
- `remote_etag == local_etag` 时,`stale=false`。
|
|
85
|
-
- `remote_etag != local_etag` 或 `content` 为空时,`stale=true`。
|
|
86
|
-
- `verify_status=invalid` 时内容可以缓存但应用层应能看到无效状态;是否拒绝展示由上层策略决定。
|
|
87
|
-
|
|
88
|
-
## 时序图
|
|
89
|
-
|
|
90
|
-
### 发送消息时,发送端获得 to 的 agent.md ETag
|
|
91
|
-
|
|
92
|
-
```mermaid
|
|
93
|
-
sequenceDiagram
|
|
94
|
-
participant A as Sender SDK
|
|
95
|
-
participant GW as Gateway
|
|
96
|
-
participant MSG as Message Service
|
|
97
|
-
participant NS as NameService
|
|
98
|
-
participant DB as Message DB
|
|
99
|
-
|
|
100
|
-
A->>GW: RPC message.send(to=B, payload=v2 envelope)
|
|
101
|
-
GW->>MSG: 转发 send,附带 _auth.aid=A
|
|
102
|
-
MSG->>NS: HEAD https://A/agent.md<br/>取 sender ETag(缓存命中则不请求)
|
|
103
|
-
NS-->>MSG: ETag(A)
|
|
104
|
-
MSG->>MSG: 注入 envelope.agent_md.sender={aid:A, etag}
|
|
105
|
-
MSG->>DB: 持久化 envelope / wraps
|
|
106
|
-
MSG-->>GW: send result
|
|
107
|
-
GW->>NS: HEAD https://B/agent.md<br/>取 to ETag(缓存命中则不请求)
|
|
108
|
-
NS-->>GW: ETag(B)
|
|
109
|
-
GW-->>A: RPC response + _meta.agent_md_etags.to
|
|
110
|
-
A->>A: observeRemoteAgentMdEtag(B, etag)<br/>更新内存 + SQLite remote_etag
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
### 接收消息时,接收端获得 from 的 agent.md ETag
|
|
114
|
-
|
|
115
|
-
```mermaid
|
|
116
|
-
sequenceDiagram
|
|
117
|
-
participant MSG as Message Service
|
|
118
|
-
participant B as Receiver SDK
|
|
119
|
-
participant Cache as SDK AgentMdCache
|
|
120
|
-
participant NS as NameService
|
|
121
|
-
|
|
122
|
-
MSG-->>B: peer.v2.message_received(seq, from=A)
|
|
123
|
-
B->>MSG: message.v2.pull(after_seq)
|
|
124
|
-
MSG-->>B: messages[].envelope_json<br/>包含 agent_md.sender={aid:A, etag}
|
|
125
|
-
B->>Cache: observeRemoteAgentMdEtag(A, etag)
|
|
126
|
-
|
|
127
|
-
alt 本地 local_etag == remote_etag
|
|
128
|
-
Cache-->>B: 只刷新 observed_at,不下载
|
|
129
|
-
else 本地无内容或 ETag 不一致
|
|
130
|
-
Cache->>Cache: 标记 stale,按需或后台拉取
|
|
131
|
-
B->>NS: GET https://A/agent.md<br/>If-None-Match=local_etag
|
|
132
|
-
NS-->>B: 200 content + ETag 或 304
|
|
133
|
-
B->>B: verify_agent_md(content, aid=A)
|
|
134
|
-
B->>Cache: 写内存 + SQLite
|
|
135
|
-
end
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
### SDK 本地缓存按需加载
|
|
139
|
-
|
|
140
|
-
```mermaid
|
|
141
|
-
sequenceDiagram
|
|
142
|
-
participant App
|
|
143
|
-
participant SDK
|
|
144
|
-
participant Mem as Memory Cache
|
|
145
|
-
participant SQL as SQLite
|
|
146
|
-
participant NS as NameService
|
|
147
|
-
|
|
148
|
-
App->>SDK: fetchAgentMd(A) / getRemoteAgentMd(A)
|
|
149
|
-
SDK->>Mem: 查 A
|
|
150
|
-
alt 内存有可用 content
|
|
151
|
-
Mem-->>SDK: content, local_etag
|
|
152
|
-
else 内存缺失
|
|
153
|
-
SDK->>SQL: load remote_agent_md_cache where aid=A
|
|
154
|
-
SQL-->>SDK: content/remote_etag/local_etag/last_modified
|
|
155
|
-
SDK->>Mem: 回填内存
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
alt 内容缺失或 remote_etag != local_etag
|
|
159
|
-
SDK->>NS: GET /agent.md<br/>If-None-Match=local_etag
|
|
160
|
-
NS-->>SDK: 200/304/404/error
|
|
161
|
-
SDK->>SDK: 200 时验签;304 时复用旧 content
|
|
162
|
-
SDK->>Mem: upsert
|
|
163
|
-
SDK->>SQL: upsert
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
SDK-->>App: agent.md content + signature + sync 状态
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
### agent.md 上传后的服务端缓存失效
|
|
170
|
-
|
|
171
|
-
```mermaid
|
|
172
|
-
sequenceDiagram
|
|
173
|
-
participant A as A SDK
|
|
174
|
-
participant NS as NameService
|
|
175
|
-
participant GW as Gateway
|
|
176
|
-
participant MSG as Message Service
|
|
177
|
-
|
|
178
|
-
A->>NS: PUT /agent.md
|
|
179
|
-
NS->>NS: 保存 content,生成新 ETag
|
|
180
|
-
NS-->>A: upload result + ETag
|
|
181
|
-
NS-->>GW: event nameservice.agent_md_updated(aid=A)
|
|
182
|
-
GW->>GW: invalidate agent_md_etag_cache[A]
|
|
183
|
-
NS-->>MSG: 可选同事件
|
|
184
|
-
MSG->>MSG: invalidate message-side agent_md_etag_cache[A]
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
## 服务端流程细化
|
|
188
|
-
|
|
189
|
-
### Gateway
|
|
190
|
-
|
|
191
|
-
现有行为:
|
|
192
|
-
|
|
193
|
-
- `deliver_response_to_client` 会在 RPC response `_meta.agent_md_etag` 中注入请求者自己的 `agent.md` ETag。
|
|
194
|
-
- ETag 获取采用本地 TTL 缓存,miss 时异步 HEAD 预热,不阻塞响应热路径。
|
|
195
|
-
- `nameservice.agent_md_updated` 事件会失效对应 AID 的 Gateway ETag 缓存。
|
|
196
|
-
|
|
197
|
-
新增行为:
|
|
198
|
-
|
|
199
|
-
- 对 `message.send` 和未来真实启用的 `message.v2.send`,根据请求参数提取 `to`。
|
|
200
|
-
- 在响应 `_meta.agent_md_etags.to` 中注入 `to` 的 ETag。
|
|
201
|
-
- 注入逻辑应使用同一套 ETag 缓存和 HEAD fetcher。
|
|
202
|
-
- 如果缓存 miss,第一轮响应可以不带 `to` ETag;后台预热后下一次消息或 RPC 再带上。
|
|
203
|
-
- 如果产品希望“发送后立即拿到 to ETag”,可对 message send 做同步 HEAD,但应设置短超时并保证失败不影响发送。
|
|
204
|
-
|
|
205
|
-
### Message Service
|
|
206
|
-
|
|
207
|
-
现有 V2 路径:
|
|
208
|
-
|
|
209
|
-
- SDK 加密 P2P 消息当前实际调用 `message.send`。
|
|
210
|
-
- 服务端通过 payload `type=e2ee.p2p_encrypted` 且 `version=v2` 进入 `_rpc_send_v2_p2p`。
|
|
211
|
-
- `_rpc_send_v2_p2p` 持久化 `protected_headers`、`context`,并在 `message.v2.pull` 时重建 `envelope_json`。
|
|
212
|
-
|
|
213
|
-
新增行为:
|
|
214
|
-
|
|
215
|
-
- 在 `_rpc_send_v2_p2p` 写入共享体前,为 `from_aid` 查询 `agent.md` ETag。
|
|
216
|
-
- 将结果注入 envelope 顶层 `agent_md.sender`。
|
|
217
|
-
- `agent_md` 应随 envelope 持久化并在 `_rebuild_v2_envelope_json` 中恢复。
|
|
218
|
-
- 在线 push 事件可以只带 seq,不强制带完整 ETag;接收端通过 pull 取得完整信封即可。
|
|
219
|
-
- V1 明文/旧 `message.send` 如需同样能力,可在传统 message envelope 中透传同等 `agent_md.sender` 字段。
|
|
220
|
-
|
|
221
|
-
### NameService
|
|
222
|
-
|
|
223
|
-
现有能力足够支撑:
|
|
224
|
-
|
|
225
|
-
- `GET /agent.md` 与 `HEAD /agent.md` 返回 `ETag` 和 `Last-Modified`。
|
|
226
|
-
- `PUT /agent.md` 上传后生成新 ETag。
|
|
227
|
-
- 上传后发布 `nameservice.agent_md_updated` 事件,Gateway 已订阅并失效缓存。
|
|
228
|
-
|
|
229
|
-
建议补齐:
|
|
230
|
-
|
|
231
|
-
- 如果 Message Service 也维护自己的 ETag 缓存,应订阅同一事件或复用 Gateway 的注入结果。
|
|
232
|
-
- HEAD 失败、404、超时返回空 ETag,不影响消息主链路。
|
|
233
|
-
|
|
234
|
-
## SDK 流程细化
|
|
235
|
-
|
|
236
|
-
### 观察远端 ETag
|
|
237
|
-
|
|
238
|
-
SDK 增加统一入口:
|
|
239
|
-
|
|
240
|
-
```text
|
|
241
|
-
observe_remote_agent_md_etag(aid, etag, source)
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
触发来源:
|
|
245
|
-
|
|
246
|
-
- RPC response `_meta.agent_md_etags.to`:发送消息后观察 `to`。
|
|
247
|
-
- 消息信封 `agent_md.sender`:收到消息后观察 `from`。
|
|
248
|
-
- 现有 `_meta.agent_md_etag`:仍用于当前客户端自己的云端 ETag。
|
|
249
|
-
|
|
250
|
-
处理规则:
|
|
251
|
-
|
|
252
|
-
- aid 或 etag 为空时忽略。
|
|
253
|
-
- etag 与当前 `remote_etag` 相同:只刷新 `observed_at`。
|
|
254
|
-
- etag 变化:更新 `remote_etag`、`observed_at`,并根据 `local_etag` 设置 `stale`。
|
|
255
|
-
- 变更需要同时写入内存和 SQLite。
|
|
256
|
-
|
|
257
|
-
### 按需下载
|
|
258
|
-
|
|
259
|
-
当应用调用 `fetchAgentMd(aid)` 或 SDK 需要展示远程 agent 信息时:
|
|
260
|
-
|
|
261
|
-
- 先查内存,miss 时查 SQLite 表。
|
|
262
|
-
- 如果 `content` 存在且 `local_etag == remote_etag`,可直接返回缓存。
|
|
263
|
-
- 如果 `content` 缺失或 stale,则发起 GET。
|
|
264
|
-
- GET 时优先带 `If-None-Match=local_etag`,其次带 `If-Modified-Since=last_modified`。
|
|
265
|
-
- 200:验签,更新内容和 `local_etag`。
|
|
266
|
-
- 304:只有本地已有 content 时才能复用;如果本地没有 content 却收到 304,应清空条件头重试一次 GET。
|
|
267
|
-
- 404:标记远端未发布 `agent.md`,不要删除已有内容,除非产品要求严格同步。
|
|
268
|
-
- 网络错误:保留旧内容,记录 `fetch_error` 或更新失败时间。
|
|
269
|
-
|
|
270
|
-
### SQLite 持久化
|
|
271
|
-
|
|
272
|
-
新增结构化表 `remote_agent_md_cache`,每个远程 AID 一行:
|
|
273
|
-
|
|
274
|
-
| 列 | 类型 |
|
|
275
|
-
| --- | --- |
|
|
276
|
-
| `aid` | TEXT PRIMARY KEY |
|
|
277
|
-
| `content` | TEXT NOT NULL DEFAULT '' |
|
|
278
|
-
| `local_etag` | TEXT NOT NULL DEFAULT '' |
|
|
279
|
-
| `remote_etag` | TEXT NOT NULL DEFAULT '' |
|
|
280
|
-
| `last_modified` | TEXT NOT NULL DEFAULT '' |
|
|
281
|
-
| `fetched_at` | INTEGER NOT NULL DEFAULT 0 |
|
|
282
|
-
| `observed_at` | INTEGER NOT NULL DEFAULT 0 |
|
|
283
|
-
| `verify_status` | TEXT NOT NULL DEFAULT '' |
|
|
284
|
-
| `verify_error` | TEXT NOT NULL DEFAULT '' |
|
|
285
|
-
| `updated_at` | INTEGER NOT NULL DEFAULT 0 |
|
|
286
|
-
|
|
287
|
-
建议 SQL:
|
|
288
|
-
|
|
289
|
-
```sql
|
|
290
|
-
CREATE TABLE IF NOT EXISTS remote_agent_md_cache (
|
|
291
|
-
aid TEXT PRIMARY KEY,
|
|
292
|
-
content TEXT NOT NULL DEFAULT '',
|
|
293
|
-
local_etag TEXT NOT NULL DEFAULT '',
|
|
294
|
-
remote_etag TEXT NOT NULL DEFAULT '',
|
|
295
|
-
last_modified TEXT NOT NULL DEFAULT '',
|
|
296
|
-
fetched_at INTEGER NOT NULL DEFAULT 0,
|
|
297
|
-
observed_at INTEGER NOT NULL DEFAULT 0,
|
|
298
|
-
verify_status TEXT NOT NULL DEFAULT '',
|
|
299
|
-
verify_error TEXT NOT NULL DEFAULT '',
|
|
300
|
-
updated_at INTEGER NOT NULL DEFAULT 0
|
|
301
|
-
);
|
|
302
|
-
|
|
303
|
-
CREATE INDEX IF NOT EXISTS idx_remote_agent_md_cache_observed_at
|
|
304
|
-
ON remote_agent_md_cache(observed_at);
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
五个 SDK 应保持字段语义一致。浏览器 JS 可落 IndexedDB;Node TS、Python、Go、C++ 落各自现有本地存储。
|
|
308
|
-
|
|
309
|
-
## 异常与竞态处理
|
|
310
|
-
|
|
311
|
-
- 多个消息同时观察同一 AID 的新 ETag:按 ETag 值幂等 upsert。
|
|
312
|
-
- 多个协程同时触发同一 AID 下载:需要 per-AID in-flight 去重。
|
|
313
|
-
- 观察到 ETag A 后开始下载,期间又观察到 ETag B:下载完成时只更新 `local_etag=A`,随后仍保持 stale,下一轮继续拉 B。
|
|
314
|
-
- 304 但本地 content 缺失:不能返回空内容,必须无条件 GET 重试。
|
|
315
|
-
- 信封里的 ETag 不参与 AAD,不作为安全声明;安全性仍依赖 `agent.md` 签名和证书校验。
|
|
316
|
-
- HEAD/GET 超时不影响 message send 和 message pull。
|
|
317
|
-
- 跨域场景中,目标域 Message Service 注入 sender ETag 时可能需要跨域 HEAD;失败时允许缺字段。
|
|
318
|
-
|
|
319
|
-
## 测试要点
|
|
320
|
-
|
|
321
|
-
- 发送方收到 `message.send` 响应后,能把 `to` 的 ETag 写入本地缓存 `remote_etag`。
|
|
322
|
-
- 接收方 `message.v2.pull` 后,能从 `envelope.agent_md.sender` 写入 `from` 的 `remote_etag`。
|
|
323
|
-
- ETag 变化但内容未下载时,缓存状态为 stale。
|
|
324
|
-
- 本地 SQLite 有缓存、内存为空时,SDK 能按需加载。
|
|
325
|
-
- 304 且本地有内容时复用内容;304 但本地无内容时强制重拉。
|
|
326
|
-
- `agent.md` 上传后,Gateway 缓存失效,后续消息能看到新 ETag。
|
|
327
|
-
- HEAD/GET 404、超时、网络错误不影响消息收发主链路。
|
|
1
|
+
# 远程 agent.md 缓存与 ETag 透传方案
|
|
2
|
+
|
|
3
|
+
状态:方案草案
|
|
4
|
+
|
|
5
|
+
## 目标
|
|
6
|
+
|
|
7
|
+
让 SDK 在给对端发送消息、收到对端消息时,都能观察到对端云端 `agent.md` 的最新 ETag,并据此维护本机远程 `agent.md` 缓存状态。
|
|
8
|
+
|
|
9
|
+
核心目标:
|
|
10
|
+
|
|
11
|
+
- 每个远程 AID 在 SDK 本地内存中维护一条 `agent.md` 记录,包含 `remote_etag`、`local_etag`、`content`、`last_modified` 等字段。
|
|
12
|
+
- 同一条远程缓存记录持久化到本地 SQLite 表,SDK 启动或内存 miss 时按需加载。
|
|
13
|
+
- `message.send` 的 RPC 响应携带接收方 `agent.md` ETag,让发送方更新 `to` 的云端版本。
|
|
14
|
+
- 接收端收到消息信封时携带发送方 `agent.md` ETag,让接收方更新 `from` 的云端版本。
|
|
15
|
+
- ETag 只作为版本提示,不替代 `agent.md` 内容下载和验签。
|
|
16
|
+
|
|
17
|
+
## 可行性结论
|
|
18
|
+
|
|
19
|
+
方案可行。服务端已有 `agent.md` HEAD/ETag 能力,Gateway 当前也已经在 RPC response `_meta.agent_md_etag` 中注入“请求者自己”的服务端 ETag。需要扩展两条路径:
|
|
20
|
+
|
|
21
|
+
1. `message.send` / V2 P2P send:把 `from` 的 `agent.md` ETag 注入消息信封,随消息到达接收端。
|
|
22
|
+
2. `message.send` RPC response:把 `to` 的 `agent.md` ETag 注入响应 `_meta`,返回发送端。
|
|
23
|
+
|
|
24
|
+
注意:服务端注入的 ETag 只能代表云端版本。SDK 本地必须区分“观察到的远端云端 ETag”和“当前本地内容对应的 ETag”。字段命名固定为 `remote_etag` 和 `local_etag`,其中 `remote_etag` 表示远端云端版本,`local_etag` 表示本地 `content` 对应版本。不能在只有远端 ETag、没有内容的情况下直接覆盖 `local_etag`,否则后续 `If-None-Match` 命中 304 时本地可能没有可用内容。
|
|
25
|
+
|
|
26
|
+
## 字段建议
|
|
27
|
+
|
|
28
|
+
消息信封新增字段:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"agent_md": {
|
|
33
|
+
"sender": {
|
|
34
|
+
"aid": "alice.agentid.pub",
|
|
35
|
+
"etag": "\"sha256...\""
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`message.send` RPC response 的 `_meta` 新增字段:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"_meta": {
|
|
46
|
+
"agent_md_etag": "\"sender-self-etag\"",
|
|
47
|
+
"agent_md_etags": {
|
|
48
|
+
"to": {
|
|
49
|
+
"aid": "bob.agentid.pub",
|
|
50
|
+
"etag": "\"sha256...\""
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
兼容说明:
|
|
58
|
+
|
|
59
|
+
- `_meta.agent_md_etag` 保持现有语义,仍表示请求者自己在服务端的 `agent.md` ETag。
|
|
60
|
+
- `_meta.agent_md_etags.to` 表示本次消息接收方的 `agent.md` ETag。
|
|
61
|
+
- `envelope.agent_md.sender` 表示本条消息发送方的 `agent.md` ETag。
|
|
62
|
+
- 字段缺失、ETag 为空、HEAD 失败均不影响消息收发。
|
|
63
|
+
|
|
64
|
+
## SDK 缓存模型
|
|
65
|
+
|
|
66
|
+
每个远程 AID 在内存和 SQLite 表中都维护一条缓存记录。内存记录与 SQLite 表记录字段语义一致,SQLite 用于 SDK 重启或内存 miss 时按需加载。
|
|
67
|
+
|
|
68
|
+
| 字段 | 含义 |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| `aid` | 远程 AID |
|
|
71
|
+
| `content` | 本地缓存的完整 `agent.md` 内容,可为空 |
|
|
72
|
+
| `local_etag` | 当前 `content` 对应的 ETag,只能由 GET 200/304 确认 |
|
|
73
|
+
| `remote_etag` | 从消息信封或 RPC `_meta` 观察到的远端云端 ETag |
|
|
74
|
+
| `last_modified` | GET 响应的 `Last-Modified` |
|
|
75
|
+
| `fetched_at` | 最近一次成功确认内容的本机时间 |
|
|
76
|
+
| `observed_at` | 最近一次观察到远端 ETag 的本机时间 |
|
|
77
|
+
| `stale` | `remote_etag` 与 `local_etag` 不一致,或有远端 ETag 但无内容 |
|
|
78
|
+
| `verify_status` | 最近一次 `verify_agent_md` 结果:`verified` / `unsigned` / `invalid` |
|
|
79
|
+
|
|
80
|
+
状态规则:
|
|
81
|
+
|
|
82
|
+
- 收到远端 ETag 时,只更新 `remote_etag` 和 `observed_at`。
|
|
83
|
+
- 只有下载到内容并完成验签后,才能更新 `content`、`local_etag`、`last_modified`、`fetched_at`。
|
|
84
|
+
- `remote_etag == local_etag` 时,`stale=false`。
|
|
85
|
+
- `remote_etag != local_etag` 或 `content` 为空时,`stale=true`。
|
|
86
|
+
- `verify_status=invalid` 时内容可以缓存但应用层应能看到无效状态;是否拒绝展示由上层策略决定。
|
|
87
|
+
|
|
88
|
+
## 时序图
|
|
89
|
+
|
|
90
|
+
### 发送消息时,发送端获得 to 的 agent.md ETag
|
|
91
|
+
|
|
92
|
+
```mermaid
|
|
93
|
+
sequenceDiagram
|
|
94
|
+
participant A as Sender SDK
|
|
95
|
+
participant GW as Gateway
|
|
96
|
+
participant MSG as Message Service
|
|
97
|
+
participant NS as NameService
|
|
98
|
+
participant DB as Message DB
|
|
99
|
+
|
|
100
|
+
A->>GW: RPC message.send(to=B, payload=v2 envelope)
|
|
101
|
+
GW->>MSG: 转发 send,附带 _auth.aid=A
|
|
102
|
+
MSG->>NS: HEAD https://A/agent.md<br/>取 sender ETag(缓存命中则不请求)
|
|
103
|
+
NS-->>MSG: ETag(A)
|
|
104
|
+
MSG->>MSG: 注入 envelope.agent_md.sender={aid:A, etag}
|
|
105
|
+
MSG->>DB: 持久化 envelope / wraps
|
|
106
|
+
MSG-->>GW: send result
|
|
107
|
+
GW->>NS: HEAD https://B/agent.md<br/>取 to ETag(缓存命中则不请求)
|
|
108
|
+
NS-->>GW: ETag(B)
|
|
109
|
+
GW-->>A: RPC response + _meta.agent_md_etags.to
|
|
110
|
+
A->>A: observeRemoteAgentMdEtag(B, etag)<br/>更新内存 + SQLite remote_etag
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 接收消息时,接收端获得 from 的 agent.md ETag
|
|
114
|
+
|
|
115
|
+
```mermaid
|
|
116
|
+
sequenceDiagram
|
|
117
|
+
participant MSG as Message Service
|
|
118
|
+
participant B as Receiver SDK
|
|
119
|
+
participant Cache as SDK AgentMdCache
|
|
120
|
+
participant NS as NameService
|
|
121
|
+
|
|
122
|
+
MSG-->>B: peer.v2.message_received(seq, from=A)
|
|
123
|
+
B->>MSG: message.v2.pull(after_seq)
|
|
124
|
+
MSG-->>B: messages[].envelope_json<br/>包含 agent_md.sender={aid:A, etag}
|
|
125
|
+
B->>Cache: observeRemoteAgentMdEtag(A, etag)
|
|
126
|
+
|
|
127
|
+
alt 本地 local_etag == remote_etag
|
|
128
|
+
Cache-->>B: 只刷新 observed_at,不下载
|
|
129
|
+
else 本地无内容或 ETag 不一致
|
|
130
|
+
Cache->>Cache: 标记 stale,按需或后台拉取
|
|
131
|
+
B->>NS: GET https://A/agent.md<br/>If-None-Match=local_etag
|
|
132
|
+
NS-->>B: 200 content + ETag 或 304
|
|
133
|
+
B->>B: verify_agent_md(content, aid=A)
|
|
134
|
+
B->>Cache: 写内存 + SQLite
|
|
135
|
+
end
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### SDK 本地缓存按需加载
|
|
139
|
+
|
|
140
|
+
```mermaid
|
|
141
|
+
sequenceDiagram
|
|
142
|
+
participant App
|
|
143
|
+
participant SDK
|
|
144
|
+
participant Mem as Memory Cache
|
|
145
|
+
participant SQL as SQLite
|
|
146
|
+
participant NS as NameService
|
|
147
|
+
|
|
148
|
+
App->>SDK: fetchAgentMd(A) / getRemoteAgentMd(A)
|
|
149
|
+
SDK->>Mem: 查 A
|
|
150
|
+
alt 内存有可用 content
|
|
151
|
+
Mem-->>SDK: content, local_etag
|
|
152
|
+
else 内存缺失
|
|
153
|
+
SDK->>SQL: load remote_agent_md_cache where aid=A
|
|
154
|
+
SQL-->>SDK: content/remote_etag/local_etag/last_modified
|
|
155
|
+
SDK->>Mem: 回填内存
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
alt 内容缺失或 remote_etag != local_etag
|
|
159
|
+
SDK->>NS: GET /agent.md<br/>If-None-Match=local_etag
|
|
160
|
+
NS-->>SDK: 200/304/404/error
|
|
161
|
+
SDK->>SDK: 200 时验签;304 时复用旧 content
|
|
162
|
+
SDK->>Mem: upsert
|
|
163
|
+
SDK->>SQL: upsert
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
SDK-->>App: agent.md content + signature + sync 状态
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### agent.md 上传后的服务端缓存失效
|
|
170
|
+
|
|
171
|
+
```mermaid
|
|
172
|
+
sequenceDiagram
|
|
173
|
+
participant A as A SDK
|
|
174
|
+
participant NS as NameService
|
|
175
|
+
participant GW as Gateway
|
|
176
|
+
participant MSG as Message Service
|
|
177
|
+
|
|
178
|
+
A->>NS: PUT /agent.md
|
|
179
|
+
NS->>NS: 保存 content,生成新 ETag
|
|
180
|
+
NS-->>A: upload result + ETag
|
|
181
|
+
NS-->>GW: event nameservice.agent_md_updated(aid=A)
|
|
182
|
+
GW->>GW: invalidate agent_md_etag_cache[A]
|
|
183
|
+
NS-->>MSG: 可选同事件
|
|
184
|
+
MSG->>MSG: invalidate message-side agent_md_etag_cache[A]
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## 服务端流程细化
|
|
188
|
+
|
|
189
|
+
### Gateway
|
|
190
|
+
|
|
191
|
+
现有行为:
|
|
192
|
+
|
|
193
|
+
- `deliver_response_to_client` 会在 RPC response `_meta.agent_md_etag` 中注入请求者自己的 `agent.md` ETag。
|
|
194
|
+
- ETag 获取采用本地 TTL 缓存,miss 时异步 HEAD 预热,不阻塞响应热路径。
|
|
195
|
+
- `nameservice.agent_md_updated` 事件会失效对应 AID 的 Gateway ETag 缓存。
|
|
196
|
+
|
|
197
|
+
新增行为:
|
|
198
|
+
|
|
199
|
+
- 对 `message.send` 和未来真实启用的 `message.v2.send`,根据请求参数提取 `to`。
|
|
200
|
+
- 在响应 `_meta.agent_md_etags.to` 中注入 `to` 的 ETag。
|
|
201
|
+
- 注入逻辑应使用同一套 ETag 缓存和 HEAD fetcher。
|
|
202
|
+
- 如果缓存 miss,第一轮响应可以不带 `to` ETag;后台预热后下一次消息或 RPC 再带上。
|
|
203
|
+
- 如果产品希望“发送后立即拿到 to ETag”,可对 message send 做同步 HEAD,但应设置短超时并保证失败不影响发送。
|
|
204
|
+
|
|
205
|
+
### Message Service
|
|
206
|
+
|
|
207
|
+
现有 V2 路径:
|
|
208
|
+
|
|
209
|
+
- SDK 加密 P2P 消息当前实际调用 `message.send`。
|
|
210
|
+
- 服务端通过 payload `type=e2ee.p2p_encrypted` 且 `version=v2` 进入 `_rpc_send_v2_p2p`。
|
|
211
|
+
- `_rpc_send_v2_p2p` 持久化 `protected_headers`、`context`,并在 `message.v2.pull` 时重建 `envelope_json`。
|
|
212
|
+
|
|
213
|
+
新增行为:
|
|
214
|
+
|
|
215
|
+
- 在 `_rpc_send_v2_p2p` 写入共享体前,为 `from_aid` 查询 `agent.md` ETag。
|
|
216
|
+
- 将结果注入 envelope 顶层 `agent_md.sender`。
|
|
217
|
+
- `agent_md` 应随 envelope 持久化并在 `_rebuild_v2_envelope_json` 中恢复。
|
|
218
|
+
- 在线 push 事件可以只带 seq,不强制带完整 ETag;接收端通过 pull 取得完整信封即可。
|
|
219
|
+
- V1 明文/旧 `message.send` 如需同样能力,可在传统 message envelope 中透传同等 `agent_md.sender` 字段。
|
|
220
|
+
|
|
221
|
+
### NameService
|
|
222
|
+
|
|
223
|
+
现有能力足够支撑:
|
|
224
|
+
|
|
225
|
+
- `GET /agent.md` 与 `HEAD /agent.md` 返回 `ETag` 和 `Last-Modified`。
|
|
226
|
+
- `PUT /agent.md` 上传后生成新 ETag。
|
|
227
|
+
- 上传后发布 `nameservice.agent_md_updated` 事件,Gateway 已订阅并失效缓存。
|
|
228
|
+
|
|
229
|
+
建议补齐:
|
|
230
|
+
|
|
231
|
+
- 如果 Message Service 也维护自己的 ETag 缓存,应订阅同一事件或复用 Gateway 的注入结果。
|
|
232
|
+
- HEAD 失败、404、超时返回空 ETag,不影响消息主链路。
|
|
233
|
+
|
|
234
|
+
## SDK 流程细化
|
|
235
|
+
|
|
236
|
+
### 观察远端 ETag
|
|
237
|
+
|
|
238
|
+
SDK 增加统一入口:
|
|
239
|
+
|
|
240
|
+
```text
|
|
241
|
+
observe_remote_agent_md_etag(aid, etag, source)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
触发来源:
|
|
245
|
+
|
|
246
|
+
- RPC response `_meta.agent_md_etags.to`:发送消息后观察 `to`。
|
|
247
|
+
- 消息信封 `agent_md.sender`:收到消息后观察 `from`。
|
|
248
|
+
- 现有 `_meta.agent_md_etag`:仍用于当前客户端自己的云端 ETag。
|
|
249
|
+
|
|
250
|
+
处理规则:
|
|
251
|
+
|
|
252
|
+
- aid 或 etag 为空时忽略。
|
|
253
|
+
- etag 与当前 `remote_etag` 相同:只刷新 `observed_at`。
|
|
254
|
+
- etag 变化:更新 `remote_etag`、`observed_at`,并根据 `local_etag` 设置 `stale`。
|
|
255
|
+
- 变更需要同时写入内存和 SQLite。
|
|
256
|
+
|
|
257
|
+
### 按需下载
|
|
258
|
+
|
|
259
|
+
当应用调用 `fetchAgentMd(aid)` 或 SDK 需要展示远程 agent 信息时:
|
|
260
|
+
|
|
261
|
+
- 先查内存,miss 时查 SQLite 表。
|
|
262
|
+
- 如果 `content` 存在且 `local_etag == remote_etag`,可直接返回缓存。
|
|
263
|
+
- 如果 `content` 缺失或 stale,则发起 GET。
|
|
264
|
+
- GET 时优先带 `If-None-Match=local_etag`,其次带 `If-Modified-Since=last_modified`。
|
|
265
|
+
- 200:验签,更新内容和 `local_etag`。
|
|
266
|
+
- 304:只有本地已有 content 时才能复用;如果本地没有 content 却收到 304,应清空条件头重试一次 GET。
|
|
267
|
+
- 404:标记远端未发布 `agent.md`,不要删除已有内容,除非产品要求严格同步。
|
|
268
|
+
- 网络错误:保留旧内容,记录 `fetch_error` 或更新失败时间。
|
|
269
|
+
|
|
270
|
+
### SQLite 持久化
|
|
271
|
+
|
|
272
|
+
新增结构化表 `remote_agent_md_cache`,每个远程 AID 一行:
|
|
273
|
+
|
|
274
|
+
| 列 | 类型 |
|
|
275
|
+
| --- | --- |
|
|
276
|
+
| `aid` | TEXT PRIMARY KEY |
|
|
277
|
+
| `content` | TEXT NOT NULL DEFAULT '' |
|
|
278
|
+
| `local_etag` | TEXT NOT NULL DEFAULT '' |
|
|
279
|
+
| `remote_etag` | TEXT NOT NULL DEFAULT '' |
|
|
280
|
+
| `last_modified` | TEXT NOT NULL DEFAULT '' |
|
|
281
|
+
| `fetched_at` | INTEGER NOT NULL DEFAULT 0 |
|
|
282
|
+
| `observed_at` | INTEGER NOT NULL DEFAULT 0 |
|
|
283
|
+
| `verify_status` | TEXT NOT NULL DEFAULT '' |
|
|
284
|
+
| `verify_error` | TEXT NOT NULL DEFAULT '' |
|
|
285
|
+
| `updated_at` | INTEGER NOT NULL DEFAULT 0 |
|
|
286
|
+
|
|
287
|
+
建议 SQL:
|
|
288
|
+
|
|
289
|
+
```sql
|
|
290
|
+
CREATE TABLE IF NOT EXISTS remote_agent_md_cache (
|
|
291
|
+
aid TEXT PRIMARY KEY,
|
|
292
|
+
content TEXT NOT NULL DEFAULT '',
|
|
293
|
+
local_etag TEXT NOT NULL DEFAULT '',
|
|
294
|
+
remote_etag TEXT NOT NULL DEFAULT '',
|
|
295
|
+
last_modified TEXT NOT NULL DEFAULT '',
|
|
296
|
+
fetched_at INTEGER NOT NULL DEFAULT 0,
|
|
297
|
+
observed_at INTEGER NOT NULL DEFAULT 0,
|
|
298
|
+
verify_status TEXT NOT NULL DEFAULT '',
|
|
299
|
+
verify_error TEXT NOT NULL DEFAULT '',
|
|
300
|
+
updated_at INTEGER NOT NULL DEFAULT 0
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
CREATE INDEX IF NOT EXISTS idx_remote_agent_md_cache_observed_at
|
|
304
|
+
ON remote_agent_md_cache(observed_at);
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
五个 SDK 应保持字段语义一致。浏览器 JS 可落 IndexedDB;Node TS、Python、Go、C++ 落各自现有本地存储。
|
|
308
|
+
|
|
309
|
+
## 异常与竞态处理
|
|
310
|
+
|
|
311
|
+
- 多个消息同时观察同一 AID 的新 ETag:按 ETag 值幂等 upsert。
|
|
312
|
+
- 多个协程同时触发同一 AID 下载:需要 per-AID in-flight 去重。
|
|
313
|
+
- 观察到 ETag A 后开始下载,期间又观察到 ETag B:下载完成时只更新 `local_etag=A`,随后仍保持 stale,下一轮继续拉 B。
|
|
314
|
+
- 304 但本地 content 缺失:不能返回空内容,必须无条件 GET 重试。
|
|
315
|
+
- 信封里的 ETag 不参与 AAD,不作为安全声明;安全性仍依赖 `agent.md` 签名和证书校验。
|
|
316
|
+
- HEAD/GET 超时不影响 message send 和 message pull。
|
|
317
|
+
- 跨域场景中,目标域 Message Service 注入 sender ETag 时可能需要跨域 HEAD;失败时允许缺字段。
|
|
318
|
+
|
|
319
|
+
## 测试要点
|
|
320
|
+
|
|
321
|
+
- 发送方收到 `message.send` 响应后,能把 `to` 的 ETag 写入本地缓存 `remote_etag`。
|
|
322
|
+
- 接收方 `message.v2.pull` 后,能从 `envelope.agent_md.sender` 写入 `from` 的 `remote_etag`。
|
|
323
|
+
- ETag 变化但内容未下载时,缓存状态为 stale。
|
|
324
|
+
- 本地 SQLite 有缓存、内存为空时,SDK 能按需加载。
|
|
325
|
+
- 304 且本地有内容时复用内容;304 但本地无内容时强制重拉。
|
|
326
|
+
- `agent.md` 上传后,Gateway 缓存失效,后续消息能看到新 ETag。
|
|
327
|
+
- HEAD/GET 404、超时、网络错误不影响消息收发主链路。
|
|
328
328
|
- Python / TS / JS / Go / C++ 五个 SDK 对 `remote_etag`、`local_etag`、`content`、`stale` 语义一致。
|