@agentunion/fastaun-browser 0.5.1 → 0.5.3

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +570 -504
  2. package/_packed_docs/CHANGELOG-validators.md +134 -131
  3. package/_packed_docs/CHANGELOG.md +570 -504
  4. package/_packed_docs/INDEX.md +65 -52
  5. package/_packed_docs/KITE_DOCS_GUIDE.md +23 -18
  6. 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 +169 -116
  7. package/_packed_docs/aun-perf-audit-critical-bugs.md +315 -0
  8. package/_packed_docs/cli/AUN-CLI/350/256/276/350/256/241/346/226/207/346/241/243.md +263 -261
  9. package/_packed_docs/cli/CLI/346/211/213/345/206/214.md +328 -328
  10. package/_packed_docs/protocol/00-/346/200/273/350/247/210/344/270/216/345/210/206/345/261/202.md +2 -2
  11. package/_packed_docs/protocol/00A-/350/256/276/350/256/241/345/216/237/345/210/231-/344/270/272Agent/350/200/214/347/224/237.md +1 -1
  12. 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 +39 -16
  13. package/_packed_docs/protocol/03-Gateway-/350/277/236/346/216/245/346/250/241/345/274/217.md +8 -5
  14. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +18 -19
  15. package/_packed_docs/protocol/07-/351/224/231/350/257/257/347/240/201/344/270/216/347/212/266/346/200/201/346/234/272.md +1 -1
  16. package/_packed_docs/protocol/08-AUN-E2EE-Group.md +293 -294
  17. package/_packed_docs/protocol/08-AUN-E2EE.md +12 -10
  18. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +183 -60
  19. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +4 -4
  20. 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 +1 -1
  21. package/_packed_docs/protocol/16-/347/263/273/347/273/237/347/233/256/345/275/225/344/277/235/346/212/244/346/226/271/346/241/210.md +177 -177
  22. package/_packed_docs/protocol/README.md +7 -6
  23. package/_packed_docs/protocol/aun-docs-guide.md +5 -4
  24. package/_packed_docs/protocol/index.md +9 -8
  25. package/_packed_docs/protocol//350/215/211/346/241/210-/346/213/222/347/273/235/344/277/241/345/217/267/345/215/217/350/256/256.md +1 -1
  26. package/_packed_docs/protocol//351/231/204/345/275/225A-/346/234/257/350/257/255/350/241/250.md +13 -13
  27. package/_packed_docs/protocol//351/231/204/345/275/225L-E2EE/345/256/236/347/216/260/346/214/207/345/215/227.md +9 -9
  28. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +15 -13
  29. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +20 -1
  30. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +26 -18
  31. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +33 -34
  32. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +274 -4
  33. package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +41 -13
  34. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +28 -12
  35. package/_packed_docs/sdk/09-group-rpc-manual.md +237 -142
  36. package/_packed_docs/sdk/09-message-rpc-manual.md +50 -28
  37. package/_packed_docs/sdk/09-payload-reference.md +3 -3
  38. package/_packed_docs/sdk/09-storage-rpc-manual.md +1 -1
  39. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +12 -9
  40. 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 +3 -2
  41. package/_packed_docs/sdk/INDEX.md +29 -28
  42. package/_packed_docs/sdk/Notify/351/200/232/347/237/245/346/226/271/346/241/210.md +6 -2
  43. package/dist/agent-md.d.ts.map +1 -1
  44. package/dist/agent-md.js +18 -9
  45. package/dist/agent-md.js.map +1 -1
  46. package/dist/bundle.js +1084 -272
  47. package/dist/client/delivery.d.ts +4 -0
  48. package/dist/client/delivery.d.ts.map +1 -1
  49. package/dist/client/delivery.js +174 -7
  50. package/dist/client/delivery.js.map +1 -1
  51. package/dist/client/v2-e2ee.d.ts.map +1 -1
  52. package/dist/client/v2-e2ee.js +79 -8
  53. package/dist/client/v2-e2ee.js.map +1 -1
  54. package/dist/client.d.ts +27 -1
  55. package/dist/client.d.ts.map +1 -1
  56. package/dist/client.js +118 -12
  57. package/dist/client.js.map +1 -1
  58. package/dist/facades.d.ts +6 -0
  59. package/dist/facades.d.ts.map +1 -1
  60. package/dist/facades.js +213 -33
  61. package/dist/facades.js.map +1 -1
  62. package/dist/group-index.d.ts +105 -0
  63. package/dist/group-index.d.ts.map +1 -0
  64. package/dist/group-index.js +252 -0
  65. package/dist/group-index.js.map +1 -0
  66. package/dist/index.d.ts +1 -0
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +1 -0
  69. package/dist/index.js.map +1 -1
  70. package/dist/keystore/index.d.ts +15 -0
  71. package/dist/keystore/index.d.ts.map +1 -1
  72. package/dist/keystore/indexeddb-shared.d.ts +7 -1
  73. package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
  74. package/dist/keystore/indexeddb-shared.js +63 -2
  75. package/dist/keystore/indexeddb-shared.js.map +1 -1
  76. package/dist/keystore/indexeddb-token-store.d.ts +3 -1
  77. package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
  78. package/dist/keystore/indexeddb-token-store.js +22 -1
  79. package/dist/keystore/indexeddb-token-store.js.map +1 -1
  80. package/dist/tools/cross-sdk-agent.js +3 -1
  81. package/dist/tools/cross-sdk-agent.js.map +1 -1
  82. package/dist/transport.d.ts +2 -1
  83. package/dist/transport.d.ts.map +1 -1
  84. package/dist/transport.js +17 -31
  85. package/dist/transport.js.map +1 -1
  86. package/dist/version.d.ts +1 -1
  87. package/dist/version.js +1 -1
  88. package/package.json +1 -1
@@ -165,6 +165,25 @@ client.on("state_change", lambda e: print(e["state"]))
165
165
  client.on("message.received", lambda e: print(e["payload"]))
166
166
  ```
167
167
 
168
- RPC 方法参数见 `09-message-rpc-manual.md`、`09-group-rpc-manual.md`、`09-storage-rpc-manual.md` 等专项手册。
168
+ RPC 方法参数见 `09-message-rpc-manual.md`、`09-group-rpc-manual.md`、`09-storage-rpc-manual.md` 等专项手册。
169
+
170
+ ---
171
+
172
+ ## Group Index 观察模型
173
+
174
+ `group.index` 是群公告、群规则、入群要求和附件稳定引用的签名索引。它由 owner/admin 侧 SDK 生成并签名,Group 服务只校验、CAS 保存和在响应 `_meta.group_indexes` 中注入版本提示。
175
+
176
+ 关键语义:
177
+
178
+ - `_meta.group_indexes` 只包含 `etag`、`last_modified`、`schema`,不包含 index 正文。
179
+ - SDK 观察到远端 `etag` 变化后只记录观察到的远端版本;etag 不一致只表示本地与远端不同步,不表示远端一定更新,也不表示本地一定应被覆盖。
180
+ - `getAnnouncement` / `getRules` / `getJoinRequirements` 只返回 SDK 本地缓存;本地没有对应值时才读取相应 settings 初始化缓存,不会因为 etag 不一致自动 pull 远端。
181
+ - 应用层调用 `checkGroupIndex` 判断是否不同步;如选择远端为准,再调用 `getGroupIndex` 从服务端摘取当前 index 并同步本地缓存。
182
+ - owner/admin 修改 indexed settings 时调用 `updateGroupIndex`;SDK 会先读取当前 index,再基于 `expected_index_etag` CAS push 新签名版本。
183
+ - Gateway/Message 不生成、不校验、不更新 `group.index`,最多转发或合并 `_meta`。
184
+ - `getGroupIndex` pull 验签通过后必须持久化本地视图:Python / TypeScript(Node) / Go 写入 `{aun_path}/AIDs/{local_aid}/groups/{group_aid}/index.jsonl` 和同目录 `group-index-cache.json`;浏览器 JavaScript 写入 IndexedDB `group_index_cache`,字段包含 `index_jsonl`、`local_etag`、`remote_meta`、`settings`、`entry_etags`。
185
+ - 本地持久化目录按 `local_aid + group_aid` 隔离,不是 `{aun_path}/AIDs/{group_aid}/`;`index.jsonl` 保存签名 `group.index.body` 原文,SDK cache envelope 不再使用旧单文件 `group-index.json`。
186
+
187
+ 这个模型与 `agent.md` 的版本观察机制对齐:先观察变化,再由应用层决定以本地还是远端为准,显式 pull、merge 或 push。
169
188
 
170
189
 
@@ -105,12 +105,15 @@ print(auth["access_token"], auth["gateway"])
105
105
  ### 连接
106
106
 
107
107
  ```python
108
- await client.connect({
109
- "slot_id": "main",
110
- "connection_kind": "long",
111
- "auto_reconnect": True,
112
- "heartbeat_interval": 30.0,
113
- "token_refresh_before": 60.0,
108
+ await client.connect({
109
+ "slot_id": "main",
110
+ "connection_kind": "long",
111
+ "delivery_mode": {"mode": "fanout"},
112
+ "background_sync": True,
113
+ "extra_info": {"app": "demo"},
114
+ "auto_reconnect": True,
115
+ "heartbeat_interval": 30.0,
116
+ "token_refresh_before": 60.0,
114
117
  "retry": {
115
118
  "initial_delay": 1.0,
116
119
  "max_delay": 64.0,
@@ -126,12 +129,14 @@ await client.connect({
126
129
  | 选项 | 说明 |
127
130
  |------|------|
128
131
  | `slot_id` | 同一设备内的实例槽位;允许 `/`、`:`、空格作为共享隔离键分隔符 |
129
- | `connection_kind` | `"long"` 或 `"short"` |
130
- | `short_ttl_ms` | 短连接服务端兜底超时 |
131
- | `delivery_mode` | 连接级投递语义 |
132
- | `auto_reconnect` | 断线后是否自动重连 |
133
- | `heartbeat_interval` | 心跳间隔,秒 |
134
- | `token_refresh_before` | token 过期前刷新提前量,秒 |
132
+ | `connection_kind` | `"long"` 或 `"short"` |
133
+ | `short_ttl_ms` | 短连接服务端兜底超时 |
134
+ | `delivery_mode` | 连接级投递语义 |
135
+ | `extra_info` | 应用层自定义连接信息;下划线开头的键不会透传给 Gateway |
136
+ | `background_sync` | 连接成功后是否执行 SDK 后台补洞 / 未读同步,默认开启 |
137
+ | `auto_reconnect` | 断线后是否自动重连 |
138
+ | `heartbeat_interval` | 心跳间隔,秒 |
139
+ | `token_refresh_before` | token 过期前刷新提前量,秒 |
135
140
  | `retry.initial_delay` / `retry.max_delay` | 退避重连参数 |
136
141
  | `timeouts.connect/call/http` | 连接、RPC、HTTP 超时 |
137
142
 
@@ -188,10 +193,11 @@ print(client.can_connect, client.can_send, client.is_online)
188
193
 
189
194
  | 事件 | 说明 |
190
195
  |------|------|
191
- | `state_change` | 状态变化,payload 中的 `state` 是九态公开值 |
192
- | `connection.error` | 连接、认证、重连错误 |
193
- | `token.refreshed` | token 自动刷新完成 |
194
- | `message.received` | 收到消息 |
196
+ | `state_change` | 状态变化,payload 中的 `state` 是九态公开值 |
197
+ | `connection.error` | 连接、认证、重连错误 |
198
+ | `token.refreshed` | token 自动刷新完成 |
199
+ | `token.refresh_exhausted` | refresh_token 缺失、过期或刷新链耗尽,SDK 已清理本地 token,下一次重连会重新走完整登录 |
200
+ | `message.received` | 收到消息 |
195
201
  | `group.changed` | 群组事件 |
196
202
  | `message.undecryptable` / `group.message_undecryptable` | E2EE 解密失败 |
197
203
 
@@ -238,8 +244,10 @@ state = await store.check_agent_md("bob.agentid.pub", ttl_days=1)
238
244
  | `AUNClient` | 连接、事件和 RPC 调用;不再暴露 agent.md 上传入口 |
239
245
 
240
246
  本地落盘位置由 SDK 管理。Python / TypeScript / Go 写入 `{aun_path}/AIDs/{aid}/agent.md` 和同目录 `agentmd.json`;浏览器 JavaScript 写入 IndexedDB 的等价 logical key,存储不可用时退化为内存缓存。agent.md 不写入 SQLite,也不再使用旧 `{aun_path}/AgentMDs` 目录。
241
-
242
- ---
247
+
248
+ 连接后的 RPC 响应、事件推送和消息信封会被 SDK 自动观察:Gateway `_meta.agent_md_etags` 中的 `requester`、`peer`、`group` 以及兼容别名 `receiver`、`target`、`to`、`sender`、`from` 会更新对应 AID 的 `remote_etag` / `last_modified`;V2 信封中的 `agent_md.sender` 和 `agent_md.group` 也会写入同一份本地记录。`group` 表示群自身 `group_aid` / `group_id` 的 agent.md,缺少 `aid` 时 SDK 会从信封顶层或 AAD 的 `group_aid` / `group_id` 兜底。
249
+
250
+ ---
243
251
 
244
252
  ## RPC 调用
245
253
 
@@ -41,14 +41,14 @@ await client.call("message.send", {
41
41
  })
42
42
  ```
43
43
 
44
- SDK 优先使用 prekey_ecdh_v2,并默认要求前向保密:
45
-
46
- 1. **prekey_ecdh_v2** 对方有预上传的 prekey,四路 ECDH(ephemeral×prekey + ephemeral×identity + sender×prekey + sender×identity),前向安全,附带发送方签名
47
- 2. **long_term_key** 对方无 prekey,双路 ECDH(ephemeral×recipient_identity + sender×recipient_identity)+ HKDF 派生密钥(降级模式),附带发送方签名
48
-
49
- > Python SDK 默认 `require_forward_secrecy=true`,当加密结果不满足前向保密(无论是因为无 prekey 还是 prekey 加密失败降级到 long_term_key)时拒绝发送并抛出错误。需显式配置 `require_forward_secrecy=false` 才允许降级。
50
-
51
- 每条消息独立生成临时 ECDH 密钥对,实现一消息一密钥。
44
+ 当前 SDK 使用 E2EE V2 多设备 wrap 主路径:
45
+
46
+ 1. 发送前通过 `message.v2.bootstrap` `group.v2.bootstrap` 获取接收方当前活跃设备、设备 prekey、self-sync 设备和 audit recipients。
47
+ 2. SDK 为每条消息生成独立 `master_key`、消息 nonce 和发送方临时 session key,只加密一次正文。
48
+ 3. SDK 为每个接收设备生成一条 recipient wrap。设备有可用 SPK 时使用 `3DH` wrap;缺少 SPK 的兼容场景才使用 `1DH` wrap。
49
+ 4. E2EE 信封包含 `sender_signature`、AAD、`recipients_digest` / Merkle proof,接收端必须验签、验 AAD、验 recipient proof 后再解密。
50
+
51
+ 每条消息独立密钥;V2 当前主路径不再使用旧 `prekey_ecdh_v2` / `long_term_key` 信封作为默认发送格式。旧术语只用于历史兼容文档或迁移排查。
52
52
 
53
53
  ## ProtectedHeaders 与可验证上下文
54
54
 
@@ -65,9 +65,11 @@ SDK 优先使用 prekey_ecdh_v2,并默认要求前向保密:
65
65
 
66
66
  `payload_type` 不需要应用层传入。SDK 会读取加密前 `payload.type`,自动写入 `protected_headers.payload_type`,接收端解密后会校验它与明文 `payload.type` 一致。
67
67
 
68
- `protected_headers` / `headers` 是 send/thought 参数的顶层字段,不放入单独的 `envelope` 入参对象,也不属于业务 `payload`。裸 WebSocket 客户端若自行发送已加密信封,需要把 protected headers 放在自构造的 E2EE 信封内并自行完成 `_auth`,服务端不会替裸 RPC 调用生成或校验明文侧的 protected headers。
69
-
70
- 示例:
68
+ `protected_headers` / `headers` 是 send/thought 参数的顶层字段,不放入单独的 `envelope` 入参对象,也不属于业务 `payload`。裸 WebSocket 客户端若自行发送已加密信封,需要把 protected headers 放在自构造的 E2EE 信封内并自行完成 `_auth`,服务端不会替裸 RPC 调用生成或校验明文侧的 protected headers。
69
+
70
+ `agent_md` 是独立于 E2EE 的版本提示元数据,不属于 `protected_headers`,也不参与 AAD 或业务鉴权。当前 Message Service V2 P2P 信封可携带 `agent_md.sender`;SDK 也识别 `agent_md.group`。Gateway 在 RPC response / event push 的 `_meta.agent_md_etags` 中注入 `requester`、`peer`、`group`,四端 SDK 会自动写入对应 AID 的 `remote_etag` / `last_modified`,后续仍以下载后的 agent.md 签名验证作为可信依据。
71
+
72
+ 示例:
71
73
 
72
74
  ```python
73
75
  from aun_core import ProtectedHeaders
@@ -103,7 +105,8 @@ await client.call("group.send", {
103
105
 
104
106
  ```json
105
107
  {
106
- "type": "e2ee.encrypted",
108
+ "type": "e2ee.p2p_encrypted",
109
+ "version": "v2",
107
110
  "ciphertext": "...",
108
111
  "protected_headers": {
109
112
  "device_id": "dev-123",
@@ -127,8 +130,8 @@ await client.call("group.send", {
127
130
 
128
131
  计算规则:
129
132
 
130
- 1. 解密流程会派生出本条消息的 `message_key`。
131
- 2. `metadata_key = HMAC-SHA256(message_key, "aun-envelope-metadata-key-v1")`。
133
+ 1. 解密流程会得到本条消息的 `master_key`。
134
+ 2. `metadata_key = HMAC-SHA256(master_key, "aun-envelope-metadata-key-v1")`。
132
135
  3. 对字典去掉 `_auth` 后做 canonical JSON:UTF-8、key 排序、紧凑分隔符。
133
136
  4. `tag = HMAC-SHA256(metadata_key, domain + "\0" + canonical_json(body))`。
134
137
  5. `domain` 对 `protected_headers` 为 `aun-protected-headers-v1`,对 `context` 为 `aun-protected-context-v1`。
@@ -294,24 +297,20 @@ for msg in result["messages"]:
294
297
 
295
298
  ---
296
299
 
297
- ## Prekey 管理
298
-
299
- 连接时 SDK 自动上传 prekey,并定时轮换(默认每小时)。一般无需手动管理。
300
-
301
- 手动上传 prekey:
302
-
303
- ```python
304
- # 通过 AUNClient 上传(生成 + RPC)
305
- await client._upload_prekey()
306
- ```
307
-
308
- 底层 API(E2EEManager 只生成材料,不做 RPC):
309
-
310
- ```python
311
- prekey_material = client.e2ee.generate_prekey()
312
- # 返回 {"prekey_id": "...", "public_key": "...", "signature": "...", "created_at": ...}
313
- # 需要自行上传:await transport.call("message.e2ee.put_prekey", prekey_material)
314
- ```
300
+ ## V2 设备公钥与 SPK 管理
301
+
302
+ 连接成功后,SDK 会初始化本设备 V2 session,生成或加载 IK / SPK,并通过 `message.v2.put_peer_pk` 幂等注册当前 P2P 设备 SPK。群组路径会按群生成独立 group SPK,并通过 `group.v2.put_group_pk` 注册。应用层一般无需手动管理这些密钥。
303
+
304
+ 当前主路径的要点:
305
+
306
+ - P2P 设备 SPK 的 `key_source` 为 `peer_device_prekey`,由 AID 私钥签名背书。
307
+ - 群内独立 group SPK `key_source` 为 `group_device_prekey`,按规范化后的 `group_aid` 隔离。
308
+ - SDK 发送前通过 `message.v2.bootstrap` / `group.v2.bootstrap` 获取目标设备集合和当前 SPK。
309
+ - 旧 SPK 会在本地保留一段安全窗口,用于解密引用旧 SPK 的历史消息;满足已消费和保留窗口条件后才销毁。
310
+
311
+ WebSocket 客户端如果绕过 SDK,需要自行完成同等的 SPK 生成、AID 私钥签名、注册和 bootstrap 逻辑。旧 `message.e2ee.put_prekey/get_prekey` 只用于 legacy `prekey_ecdh_v2` 信封兼容,不是当前 SDK 的默认路径。
312
+
313
+ SPK 签名里的 `spk_timestamp` 使用 Unix 秒;消息、群事件和服务端 `timestamp` / `created_at` 等主路径时间字段仍使用 Unix 毫秒。
315
314
 
316
315
  ---
317
316
 
@@ -361,7 +360,7 @@ client = AUNClient(aid)
361
360
  | P2P 消息默认要求发送方签名 | 无 `sender_signature` 的消息被拒绝 |
362
361
  | 群组消息默认要求发送方签名 | `require_signature=True`,无签名或无发送方证书的消息被拒绝 |
363
362
  | 群组 E2EE 为固定启用能力 | `group_e2ee=true`,不可关闭 |
364
- | 默认要求前向保密 | `require_forward_secrecy=true`,无 prekey 时拒绝 long_term_key 降级 |
365
- | 客户端操作签名 | `group.send`/`group.kick`/`group.add_member`/`group.leave` 等操作自动附加 `client_signature`,服务端强制验签 |
363
+ | 默认要求前向保密 | V2 优先使用设备 SPK 的 `3DH` wrap;缺少 SPK `1DH` 路径仅作为兼容降级 |
364
+ | 客户端操作签名 | SDK 会为关键操作附加 `client_signature`;Gateway 对 `send/pull/ack` 等常规 RPC 优先使用连接级身份认证,只有身份声明与连接不一致、敏感操作、能力身份或主动携签场景才执行 ECDSA 验签 |
366
365
 
367
366
 
@@ -118,6 +118,8 @@ Python、TS/Node 与 Go 的 `AIDStore` 本地方法返回 Result 包装;浏览
118
118
 
119
119
  agent.md 本地记录不写入 SQLite。Python / TypeScript / Go 使用 `{aun_path}/AIDs/{aid}/agent.md` 与 `agentmd.json`;浏览器 JavaScript 使用 IndexedDB 等价 key,存储不可用时退化为内存缓存。
120
120
 
121
+ `remote_etag` / `last_modified` 除了来自 `check_agent_md()` 的 HEAD,也会由连接后的内部观察器更新:SDK 会读取 RPC response / event push `_meta.agent_md_etags` 的 `requester`、`peer`、`group` 及兼容别名,并读取 V2 envelope 的 `agent_md.sender` / `agent_md.group`。`group` 记录使用群自身 `group_aid` / `group_id` 作为 AID key。
122
+
121
123
  > **v0.4.2 变更**:`discoveryPort` 配置项已移除,Gateway 地址完全由 SDK 根据 AID issuer 自动发现,无需手动指定端口。
122
124
 
123
125
  ---
@@ -260,7 +262,7 @@ await client.call("meta.trust_roots", {})
260
262
  ```python
261
263
  await client.notify("notification/client.activity", {"state": "idle"})
262
264
  await client.notify("event/app.typing", {"thread_id": "t1"}, to="bob.agentid.pub", ttl_ms=5000)
263
- await client.notify("event/app.presence", {"state": "active"}, group_id="group.agentid.pub/123")
265
+ await client.notify("event/app.presence", {"state": "active"}, group_id="g-abc123.agentid.pub")
264
266
  ```
265
267
 
266
268
  路由选项:
@@ -268,7 +270,7 @@ await client.notify("event/app.presence", {"state": "active"}, group_id="group.a
268
270
  | 选项 | 说明 |
269
271
  |------|------|
270
272
  | `to` / `To` | 目标 AID;可同域或跨域 |
271
- | `group_id` / `groupId` / `GroupID` | 目标群;与 `to` 互斥 |
273
+ | `group_id` / `groupId` / `GroupID` | 目标群;兼容参数名,值使用目标态 `group_aid`,与 `to` 互斥 |
272
274
  | `device_id` / `deviceId` / `DeviceID` | 限定目标 AID 的在线设备;必须配合 `to` |
273
275
  | `slot_id` / `slotId` / `SlotID` | 限定目标设备的在线 slot;必须配合 `device_id` |
274
276
  | `ttl_ms` / `ttlMs` / `TTLMS` | `0..60000`,只控制在线投递过期,不表示离线缓存 |
@@ -310,6 +312,7 @@ headers = client.get_protected_headers()
310
312
  - agent.md 上传、下载和检查入口都在 `AIDStore`;`AUNClient` 不再暴露上传入口。
311
313
  - 上传要求目标 AID 已在本地加载且私钥有效;SDK 会对正文签名,并通过 `AuthFlow` 获取或复用该 AID 的 access_token。
312
314
  - SDK 发起 GET 时只发送 `Accept: text/markdown`,不主动发送 `If-None-Match` / `If-Modified-Since`。如果服务端异常返回 304,本地有内容则复用;无内容时再发一次无条件 GET。
315
+ - SDK 会自动从 Gateway `_meta.agent_md_etags` 和信封 `agent_md` 观察远端版本;`requester`、`peer`、`group` 是标准角色键,`receiver`、`target`、`to`、`sender`、`from` 是兼容别名。
313
316
  - `Accept: text/markdown` 与 agent.md 的 YAML frontmatter + Markdown 格式兼容;agent.md 仍是 Markdown 媒体类型上的结构化约定。
314
317
 
315
318
  ---
@@ -344,13 +347,278 @@ headers = client.get_protected_headers()
344
347
  - `getInfo()` — 查询群组信息(扁平化格式,提升常用字段到顶层),**推荐外部使用**
345
348
  - `info()` — 查询群组详细信息(带权限控制,非成员只能看公开群,成员能看 seq/epoch 等运行时状态)
346
349
 
347
- **群设置便利方法**:`GroupFacade` 提供向后兼容的便利方法,基于 `group.set_settings` / `group.get_settings` 实现:
350
+ **群设置便利方法**:`GroupFacade` 提供向后兼容的便利方法:
348
351
 
349
352
  - `getAnnouncement()` / `updateAnnouncement()` — 群公告
350
353
  - `getRules()` / `updateRules()` — 群规则
351
354
  - `getJoinRequirements()` / `updateJoinRequirements()` — 入群要求
352
355
 
353
- 便利方法返回旧格式(嵌套对象 `{announcement: {content, attachments}}`),屏蔽 `settings` 数组的繁琐。新代码建议直接使用 `group.set_settings` / `group.get_settings` 以获得更灵活的批量操作能力(一次调用可设置多个键)。
356
+ 读取方法优先返回 SDK 本地缓存;本地没有对应值时才读取相应 settings 做初始化。便利读取从服务端拿到 canonical `group_aid` 后,会同时以 canonical `group_aid` 和本次入参 `group_id` 写入 settings cache,避免 legacy/base `group_id` 下一次读取直接 cache miss。即使 `checkGroupIndex` 观察到远端 etag 与本地 etag 不一致,`getAnnouncement()` / `getRules()` / `getJoinRequirements()` 也不会自动拉取远端版本覆盖本地缓存。`updateAnnouncement()` / `updateRules()` / `updateJoinRequirements()` 属于 indexed 写入,内部会调用 `updateGroupIndex` 生成签名 `group.index` 并带 `expected_index_etag` CAS 提交。
357
+
358
+ **Group Index 高级同步方法**:`group.index` 是 SDK 内部签名 manifest,用于记录群公告、群规则、入群要求及附件稳定引用的版本。SDK 观察 `_meta.group_indexes` 后只记录远端 etag;etag 不一致只表示本地与观察到的远端版本不同,可能是远端更新,也可能是本地有未提交修改。应用层需要显式选择 pull 远端或 push 本地。
359
+
360
+ | 语义 | Python | TS/JS | Go | 说明 |
361
+ |------|--------|-------|----|------|
362
+ | 检查 index 是否不同步 | `client.group.check_group_index({...})` | `client.group.checkGroupIndex({...})` | `client.Group().CheckGroupIndex(ctx, params)` | 本地判断,不发网络请求;返回 `local_found/remote_found/local_etag/remote_etag/in_sync/needs_update/last_modified/status/cached` |
363
+ | 显式 pull 远端 index | `client.group.get_group_index({...})` | `client.group.getGroupIndex({...})` | `client.Group().GetGroupIndex(ctx, params)` | 调用 `group.get_settings(keys=["group.index"])` 摘取 manifest,并按 entry etag 只拉取变化的 db settings 写入本地缓存 |
364
+ | 显式 push 本地 indexed settings + index | `client.group.update_group_index({...})` | `client.group.updateGroupIndex({...})` | `client.Group().UpdateGroupIndex(ctx, params)` | 先读取当前 index 得到 `expected_index_etag`,在远端基线上合并本地变更,生成签名 `group.index` 后 CAS push |
365
+
366
+ `getGroupIndex` pull 验签成功后会持久化本地视图。Python / TypeScript(Node) / Go 使用 `{aun_path}/AIDs/{local_aid}/groups/{group_aid}/index.jsonl` 保存签名 `group.index.body` 原文,并用同目录 `group-index-cache.json` 保存 `local_etag`、`remote_meta`、`settings`、`entry_etags` 等 cache envelope。浏览器 JavaScript 使用 IndexedDB `group_index_cache` store 的等价记录,按 `local_aid + group_aid` 隔离。普通便利读取可额外写入本次请求 `group_id` 的 settings cache alias;签名正文和 `getGroupIndex` 视图仍以 canonical `group_aid` 为准。不要使用 `{aun_path}/AIDs/{group_aid}/`,也不要使用旧单文件 `group-index.json`。
367
+
368
+ #### checkGroupIndex — 检查 index 同步状态
369
+
370
+ **本地判断**,不发网络请求。基于 SDK 观察到的 `_meta.group_indexes` 远端 etag 与本地缓存 etag 对比,返回同步状态。
371
+
372
+ **参数**:
373
+
374
+ | 参数 | 类型 | 必填 | 说明 |
375
+ |------|------|:----:|------|
376
+ | `group_id` | string | ✅ | 群组标识(支持 `group_aid` 格式) |
377
+
378
+ **返回值**:
379
+
380
+ ```python
381
+ {
382
+ "group_id": "g-team.agentid.pub",
383
+ "group_aid": "g-team.agentid.pub",
384
+ "local_found": true, # 本地是否有缓存 etag
385
+ "remote_found": true, # 是否观察到远端 _meta.group_indexes
386
+ "local_etag": "\"sha256:...\"", # 本地缓存 etag(带引号)
387
+ "remote_etag": "\"sha256:...\"", # 远端 etag(带引号)
388
+ "in_sync": false, # local_etag == remote_etag
389
+ "needs_update": true, # remote_found && !in_sync(建议 pull)
390
+ "last_modified": 1780000000000, # 远端 last_modified(若有)
391
+ "schema": "aun.group.index.v1", # 远端 schema(若有)
392
+ "status": "stale" # "fresh" / "stale" / "unknown"
393
+ }
394
+ ```
395
+
396
+ **使用场景**:
397
+
398
+ - 群列表展示同步状态图标(如"本地有未同步修改"或"远端有更新")
399
+ - 判断是否需要调用 `getGroupIndex` pull 远端
400
+
401
+ **示例**:
402
+
403
+ ```python
404
+ # Python
405
+ status = await client.group.check_group_index(group_id="g-team.agentid.pub")
406
+ if status["needs_update"]:
407
+ print("远端有更新,建议 pull")
408
+ ```
409
+
410
+ ```typescript
411
+ // TypeScript/JavaScript
412
+ const status = await client.group.checkGroupIndex({ group_id: 'g-team.agentid.pub' });
413
+ if (status.needs_update) {
414
+ console.log('远端有更新,建议 pull');
415
+ }
416
+ ```
417
+
418
+ ```go
419
+ // Go
420
+ status, err := client.Group().CheckGroupIndex(ctx, map[string]any{
421
+ "group_id": "g-team.agentid.pub",
422
+ })
423
+ if status["needs_update"].(bool) {
424
+ fmt.Println("远端有更新,建议 pull")
425
+ }
426
+ ```
427
+
428
+ ---
429
+
430
+ #### getGroupIndex — 拉取远端 index 并更新本地缓存
431
+
432
+ 调用 `group.get_settings(keys=["group.index"])` 摘取远端签名 manifest,验签后按 entry etag 只拉取变化的 indexed settings,更新本地缓存。
433
+
434
+ **参数**:
435
+
436
+ | 参数 | 类型 | 必填 | 说明 |
437
+ |------|------|:----:|------|
438
+ | `group_id` | string | ✅ | 群组标识(支持 `group_aid` 格式) |
439
+
440
+ **返回值**:
441
+
442
+ ```python
443
+ {
444
+ "group_id": "g-team.agentid.pub",
445
+ "group_aid": "g-team.agentid.pub",
446
+ "group_index": { # 完整的 group.index 值
447
+ "body": "...", # 签名 JSONL 原文
448
+ "meta": {...}, # 解析出的 meta 行
449
+ "entries": [...] # 解析出的 entries 行数组
450
+ },
451
+ "meta": { # 从 meta 行提取的关键字段
452
+ "etag": "\"sha256:...\"",
453
+ "last_modified": 1780000000000,
454
+ "schema": "aun.group.index.v1"
455
+ },
456
+ "entries": [...], # 同 group_index.entries
457
+ "settings": { # 水合后的 indexed settings 值
458
+ "rules.content": "...",
459
+ "announcement.content": "...",
460
+ ...
461
+ }
462
+ }
463
+ ```
464
+
465
+ **行为**:
466
+
467
+ 1. 调用 `group.get_settings(keys=["group.index"])` 获取远端 manifest
468
+ 2. 解析 JSONL,验证 `signed_by` / `body_hash` / `etag` / 签名(当前仅支持 **ECDSA-P256-SHA256**)
469
+ 3. 按 entry etag 对比本地缓存,只拉取变化的 settings(如 `rules.content`、`announcement.content` 等)
470
+ 4. 持久化 `index.jsonl` 和 `group-index-cache.json`(或 IndexedDB)
471
+ 5. 调用 `client.mark_group_index_fresh(group_aid, etag)` 标记本地与远端同步
472
+
473
+ **错误处理**:
474
+
475
+ - **签名验证失败**:抛异常,不更新本地缓存
476
+ - **不支持的 `sig_alg`**:当前四语言 SDK 仅支持 `ECDSA-P256-SHA256`,其他算法(Ed25519/RSA)会被拒绝
477
+ - **网络错误**:透传底层 RPC 错误
478
+
479
+ **使用场景**:
480
+
481
+ - 群成员首次进群后拉取群公告、群规则
482
+ - `checkGroupIndex` 发现远端有更新时主动 pull
483
+ - 冲突解决:放弃本地修改,以远端为准
484
+
485
+ **示例**:
486
+
487
+ ```python
488
+ # Python
489
+ result = await client.group.get_group_index(group_id="g-team.agentid.pub")
490
+ print(f"拉取成功,etag: {result['meta']['etag']}")
491
+ print(f"群公告: {result['settings'].get('announcement.content')}")
492
+ ```
493
+
494
+ ```typescript
495
+ // TypeScript/JavaScript
496
+ const result = await client.group.getGroupIndex({ group_id: 'g-team.agentid.pub' });
497
+ console.log(`拉取成功,etag: ${result.meta.etag}`);
498
+ console.log(`群公告: ${result.settings['announcement.content']}`);
499
+ ```
500
+
501
+ ```go
502
+ // Go
503
+ result, err := client.Group().GetGroupIndex(ctx, map[string]any{
504
+ "group_id": "g-team.agentid.pub",
505
+ })
506
+ if err != nil {
507
+ log.Fatal(err)
508
+ }
509
+ fmt.Printf("拉取成功,etag: %s\n", result["meta"].(map[string]any)["etag"])
510
+ ```
511
+
512
+ ---
513
+
514
+ #### updateGroupIndex — 推送本地 indexed settings 修改
515
+
516
+ 在远端基线上合并本地 indexed settings 修改,生成签名 `group.index` 后通过 CAS(Compare-And-Swap)机制提交。支持自动重试 etag 冲突。
517
+
518
+ **参数**:
519
+
520
+ | 参数 | 类型 | 必填 | 说明 |
521
+ |------|------|:----:|------|
522
+ | `group_id` | string | ✅ | 群组标识(支持 `group_aid` 格式) |
523
+ | `settings` | object | ✅ | 要更新的 indexed settings(key-value 对象) |
524
+ | `signer` | AID | ❌ | 签名者身份(默认 `client.current_aid`) |
525
+ | `last_modified` | int | ❌ | 时间戳毫秒(默认 `Date.now()` / `time.time()*1000`) |
526
+ | `max_attempts` | int | ❌ | CAS 冲突最大重试次数(默认 2) |
527
+
528
+ **支持的 indexed settings keys**:
529
+
530
+ - `rules.content` / `rules.attachments` — 群规则及附件
531
+ - `announcement.content` / `announcement.attachments` — 群公告及附件
532
+ - `join.mode` / `join.question` / `join.auto_approve_patterns` / `join.max_pending` — 入群要求
533
+
534
+ **返回值**:
535
+
536
+ ```python
537
+ {
538
+ "group_id": "g-team.agentid.pub",
539
+ "group_aid": "g-team.agentid.pub",
540
+ "updated_keys": ["announcement.content", "group.index"],
541
+ "_meta": {
542
+ "group_indexes": {
543
+ "g-team.agentid.pub": {
544
+ "etag": "\"sha256:...\"", # 推送成功后的新 etag
545
+ "last_modified": 1780000000000,
546
+ "schema": "aun.group.index.v1"
547
+ }
548
+ }
549
+ }
550
+ }
551
+ ```
552
+
553
+ **行为**:
554
+
555
+ 1. 调用 `group.get_settings(keys=["group.index"])` 获取当前远端 etag(作为 CAS 基线)
556
+ 2. 解析远端 `group.index` 的 entries,保留不在 `settings` 中的条目
557
+ 3. 为 `settings` 中每个 key 计算新的 entry(包含 `etag: "sha256:<value的sha256>"`)
558
+ 4. 合并远端保留条目和新 entries,生成新的 canonical JSONL
559
+ 5. 用 `signer` 签名生成完整的 `group.index`(包含 meta 行的 `signature` 字段)
560
+ 6. 调用 `group.set_settings(settings={...修改的key..., "group.index": {...}}, expected_index_etag=<远端etag>)`
561
+ 7. **CAS 冲突自动重试**:若返回 "etag conflict" 错误,回到步骤 1 重新拉取基线(最多 `max_attempts` 次)
562
+ 8. 推送成功后调用 `client.mark_group_index_fresh()` 和 `client.cache_group_index_settings()` 更新本地缓存
563
+
564
+ **错误处理**:
565
+
566
+ - **CAS 冲突重试耗尽**:抛出最后一次的 "etag conflict" 异常
567
+ - **非 CAS 错误**:立即抛出(如权限不足、签名失败)
568
+ - **`signer` 与 RPC `actor` 不一致**:服务端会拒绝(`signed_by` 必须等于 `actor_aid`)
569
+
570
+ **使用场景**:
571
+
572
+ - 群主/管理员修改群公告、群规则后推送
573
+ - 冲突解决:本地修改优先,覆盖远端(若冲突次数超限需人工介入)
574
+
575
+ **示例**:
576
+
577
+ ```python
578
+ # Python - 更新群公告
579
+ result = await client.group.update_group_index(
580
+ group_id="g-team.agentid.pub",
581
+ settings={
582
+ "announcement.content": "新公告内容",
583
+ "announcement.attachments": []
584
+ }
585
+ )
586
+ print(f"推送成功,新 etag: {result['_meta']['group_indexes']['g-team.agentid.pub']['etag']}")
587
+ ```
588
+
589
+ ```typescript
590
+ // TypeScript/JavaScript - 更新群规则
591
+ const result = await client.group.updateGroupIndex({
592
+ group_id: 'g-team.agentid.pub',
593
+ settings: {
594
+ 'rules.content': '1. 禁止广告\n2. 尊重他人',
595
+ 'rules.attachments': []
596
+ }
597
+ });
598
+ console.log(`推送成功,新 etag: ${result._meta.group_indexes['g-team.agentid.pub'].etag}`);
599
+ ```
600
+
601
+ ```go
602
+ // Go - 更新入群要求
603
+ result, err := client.Group().UpdateGroupIndex(ctx, map[string]any{
604
+ "group_id": "g-team.agentid.pub",
605
+ "settings": map[string]any{
606
+ "join.mode": "approval",
607
+ "join.question": "你是如何知道本群的?",
608
+ },
609
+ })
610
+ if err != nil {
611
+ log.Fatal(err)
612
+ }
613
+ fmt.Printf("推送成功\n")
614
+ ```
615
+
616
+ **注意事项**:
617
+
618
+ 1. **权限要求**:写入 indexed settings 需要 admin 及以上权限
619
+ 2. **签名算法限制**:当前版本仅支持 ECDSA-P256-SHA256,使用其他算法的 AID 无法签名
620
+ 3. **CAS 冲突策略**:默认重试 2 次,高并发场景建议增加 `max_attempts`
621
+ 4. **`signer` 必须是当前连接身份**:服务端强制校验 `signed_by == actor_aid`,传入其他 AID 会被拒绝
354
622
 
355
623
  ---
356
624
 
@@ -418,11 +686,13 @@ sub.unsubscribe()
418
686
  | `state_change` | 状态变化,`state` 为九态公开值 |
419
687
  | `connection.error` | 连接或重连错误 |
420
688
  | `token.refreshed` | token 刷新完成 |
689
+ | `token.refresh_exhausted` | refresh_token 缺失、过期或刷新链耗尽,SDK 已清理本地 token 并等待重新登录 |
421
690
  | `message.received` | 收到 P2P 消息 |
422
691
  | `message.ack` | 消息 ack |
423
692
  | `message.undecryptable` | P2P E2EE 解密失败 |
424
693
  | `group.changed` | 群组事件 |
425
694
  | `group.message_undecryptable` | 群 E2EE 解密失败 |
695
+ | `storage.object_changed` | Storage 对象变更事件透传 |
426
696
 
427
697
  ---
428
698
 
@@ -60,11 +60,13 @@ except AUNError as e:
60
60
  | -32008 | 资源不存在 | `NotFoundError` |
61
61
  | -32009 | 版本冲突 | `VersionConflictError` |
62
62
  | -32010 / -32011 / -32013 | 会话错误 | `SessionError` |
63
- | -32051 | 客户端签名验证失败 | `ClientSignatureError`(继承自 `ValidationError`) |
64
- | -32029 | 请求限流(目标/联邦维度) | `RateLimitError` |
65
- | -32429 | 请求限流(Gateway 入口背压,排队超时) | `RateLimitError` |
66
- | -32600 / -32601 / -32602 | JSON-RPC 参数错误 | `ValidationError` |
67
- | -32040 ~ -32044 | E2EE 群组错误 | `E2EEError` 子类 |
63
+ | -32051 | 客户端签名验证失败 | `ClientSignatureError`(继承自 `ValidationError`) |
64
+ | -32029 | 请求限流(目标/联邦维度) | `RateLimitError` |
65
+ | -32429 | 请求限流(Gateway 入口背压,排队超时) | `RateLimitError` |
66
+ | -32600 / -32601 / -32602 | JSON-RPC 参数错误 | `ValidationError` |
67
+ | -32602 且消息包含 `group.index etag conflict` | group.index CAS 冲突 | 应重新 `getGroupIndex`、合并、签名后重试 |
68
+ | -32602 且消息包含 `group.index` 签名/schema/hash/etag 错误 | group.index 校验失败 | 检查 signer、canonical JSONL、`body_hash`、`etag` 和 `signature` |
69
+ | -32040 ~ -32044 | E2EE 群组错误 | `E2EEError` 子类 |
68
70
  | 4090 | 身份冲突 | `IdentityConflictError` |
69
71
  | -32050 | 证书已吊销 | `CertificateRevokedError` |
70
72
  | -33001 | 群组不存在 | `GroupNotFoundError` |
@@ -143,12 +145,38 @@ await client.call("message.send", params) # 非 ready 状态会抛 ConnectionEr
143
145
  发送前检查:
144
146
 
145
147
  ```python
146
- if not client.can_send:
147
- await client.connect({"auto_reconnect": True})
148
- ```
149
-
150
- ### E2EE 解密失败
151
-
152
- P2P 或群消息解密失败通常由 prekey 不匹配、AAD 篡改、密文损坏或群 epoch 不一致引起。SDK 会发布 `message.undecryptable` / `group.message_undecryptable` 事件,应用可记录并继续处理其他消息。
153
-
148
+ if not client.can_send:
149
+ await client.connect({"auto_reconnect": True})
150
+ ```
151
+
152
+ ### refresh_token 失效
153
+
154
+ 当自动刷新命中 `missing refresh_token`、`invalid_or_expired_refresh_token`、刷新链耗尽或服务端返回 `relogin_required=true` 时,SDK 会清理本地 `access_token` / `refresh_token` / `kite_token`,发布 `token.refresh_exhausted` 事件,并在后续重连时重新走完整登录链路。应用层可监听该事件提示用户重新授权;不要继续复用旧 token。
155
+
156
+ ### E2EE 解密失败
157
+
158
+ P2P 或群消息解密失败通常由 prekey 不匹配、AAD 篡改、密文损坏或群 epoch 不一致引起。SDK 会发布 `message.undecryptable` / `group.message_undecryptable` 事件,应用可记录并继续处理其他消息。
159
+
160
+ ### group.index CAS 冲突
161
+
162
+ 多个 owner/admin 并发修改公告、规则或入群要求时,服务端通过 `expected_index_etag` 做 CAS。冲突时错误消息包含 `group.index etag conflict`。
163
+
164
+ 处理方式:
165
+
166
+ ```python
167
+ try:
168
+ await client.group.update_group_index({
169
+ "group_id": group_aid,
170
+ "settings": {"announcement.content": "new text"},
171
+ })
172
+ except Exception as exc:
173
+ if "etag conflict" in str(exc):
174
+ latest = await client.group.get_group_index({"group_id": group_aid})
175
+ # 在 latest["entries"] 上合并本地修改,然后重新 update_group_index
176
+ raise
177
+ raise
178
+ ```
179
+
180
+ 通常优先使用 SDK 的 `updateGroupIndex` facade;它会按 `max_attempts` 自动重读当前 index 并重试。超过重试次数仍冲突时,把冲突交给应用层做合并策略。
181
+
154
182