@agentunion/fastaun-browser 0.3.4 → 0.3.6

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.
@@ -1,454 +1,469 @@
1
- # AUN SDK Python - 连接与认证
2
-
3
- ---
4
-
5
- ## v0.3.4 行为变更
6
-
7
- ### 注册与认证分离
8
-
9
- v0.3.4 起,`authenticate()` **不再隐式注册**。如果本地无完整身份(缺 keypair 或 cert),`authenticate()` 直接抛 `StateError`。
10
-
11
- 正确流程:
12
-
13
- ```python
14
- await client.auth.register_aid({"aid": aid}) # 1. 注册(生成密钥对 + 获取证书)
15
- auth = await client.auth.authenticate({"aid": aid}) # 2. 认证(获取 token)
16
- await client.connect(auth, {}) # 3. 连接
17
- ```
18
-
19
- **半成品恢复**:如果本地有 keypair 但无 cert(上次注册中断),`register_aid()` 会自动向服务端查询并恢复证书,无需手动清理。
20
-
21
- ### 迁移说明
22
-
23
- | 旧 API(v0.3.3-) | API(v0.3.4+) |
24
- |---|---|
25
- | `create_aid()` | `register_aid()`(已移除 `create_aid`) |
26
- | `authenticate()` 自动注册 | 必须先显式调用 `register_aid()` |
27
-
28
- ---
29
-
30
- ## 注册 AID 和认证
31
-
32
- ```python
33
- from aun_core import AUNClient
34
-
35
- async def setup(aid: str):
36
- client = AUNClient() # 默认 aun_path: ~/.aun
37
-
38
- # 注册 AID
39
- try:
40
- result = await client.auth.register_aid({"aid": aid})
41
- # result: {"aid": ..., "cert_pem": ..., "gateway": ...}
42
- except Exception as e:
43
- print(f"注册 AID 失败: {e}")
44
- raise
45
-
46
- auth = await client.auth.authenticate({"aid": aid})
47
- # auth: {"aid": ..., "access_token": ..., "refresh_token": ...,
48
- # "expires_at": ..., "gateway": ...}
49
- return client, auth
50
- ```
51
-
52
- ---
53
-
54
- ## 连接到网关
55
-
56
- **必须先调用 `client.auth.authenticate()` 获取令牌和网关地址,再调用 `connect()`,此步骤不可跳过。** `authenticate()` 返回的 `gateway` 字段即为网关 WebSocket 地址。
57
-
58
- > 当前各语言 SDK 的稳定连接模式都是 Gateway。协议层虽然定义了 Peer / Relay,但 Python SDK 的 `connect(topology=...)` 目前会对 `peer` / `relay` 明确返回未实现。
59
-
60
- ### 基础连接
61
-
62
- ```python
63
- # 第一步:认证
64
- auth = await client.auth.authenticate({"aid": MY_AID})
65
- # auth["access_token"] — 访问令牌
66
- # auth["gateway"] — 网关 WebSocket 地址
67
-
68
- # 第二步:连接(auth 结果 + 连接选项)
69
- await client.connect(auth, {})
70
- ```
71
-
72
- ### 完整连接选项
73
-
74
- ```python
75
- # 以下为可选覆盖值;不传时默认 auto_reconnect=true、heartbeat_interval=30、
76
- # token_refresh_before=60、retry.initial_delay=1.0、retry.max_delay=64.0、
77
- # timeouts={connect:5, call:10, http:30}
78
- await client.connect(auth, {
79
- "auto_reconnect": True, # 断线自动重连
80
- "heartbeat_interval": 30.0, # 心跳间隔(秒)
81
- "token_refresh_before": 60.0, # 令牌刷新提前量(秒)
82
- "connection_kind": "long", # 可选,"long"(默认)或 "short"
83
- "short_ttl_ms": 30000, # 可选,仅 kind=short 时有效,服务端兜底超时
84
- "retry": {
85
- "initial_delay": 1.0, # 初始退避延迟
86
- "max_delay": 64.0, # 最大退避延迟
87
- },
88
- "timeouts": {
89
- "connect": 5.0, # WebSocket 连接超时
90
- "call": 10.0, # RPC 调用超时
91
- "http": 30.0, # HTTP 请求超时
92
- },
93
- })
94
- ```
95
-
96
- > 当前实现只读取 `retry.initial_delay` / `retry.max_delay`;未提供 `retry.max_attempts` 选项,上层如需停止自动重连,应主动关闭客户端。
97
-
98
- **长短连接共存**:同一 `(aid, device_id, slot_id)` 槽位下允许 1 条长连接 + 最多 10 条短连接。长连接承担服务端推送(消息、事件);短连接仅用于 RPC 请求-响应后立即断开(CLI 工具场景)。短连接默认禁用 `auto_reconnect`、心跳和 token 主动刷新。
99
-
100
- ### 长连接 / 短连接代码示例
101
-
102
- #### 长连接(守护进程:常驻收件箱)
103
-
104
- ```python
105
- from aun_core import AUNClient
106
-
107
- client = AUNClient({"aun_path": "/home/alice/.aun/alice"})
108
- # 首次:走完整 login + discovery;之后:复用 keystore 里的 cached token + gateway_url
109
- auth = await client.auth.authenticate({"aid": "alice.example.com"})
110
- await client.connect(auth, {
111
- "connection_kind": "long", # 默认值,可省略
112
- "slot_id": "main",
113
- "auto_reconnect": True,
114
- })
115
-
116
- # 监听消息推送
117
- client.on("message.received", lambda data: print(data["payload"]))
118
-
119
- # 常驻
120
- await asyncio.Event().wait()
121
- ```
122
-
123
- #### 短连接(CLI 工具:发完即退)
124
-
125
- ```python
126
- from aun_core import AUNClient
127
-
128
- # 关键:使用与长连接守护进程相同的 aun_path → 共享 keystore → 自动复用 token
129
- client = AUNClient({"aun_path": "/home/alice/.aun/alice"})
130
-
131
- # authenticate 自动从 keystore 读 cached access_token,跳过两阶段 login
132
- auth = await client.auth.authenticate({"aid": "alice.example.com"})
133
-
134
- # connect 时声明 short kind + 同 slot_id(与长连接共存于同一槽位)
135
- await client.connect(auth, {
136
- "connection_kind": "short",
137
- "slot_id": "main", # 与长连接同 slot
138
- "short_ttl_ms": 30000, # 服务端兜底超时(防 CLI 异常退出占名额)
139
- })
140
-
141
- # RPC,响应原路返回到这条短连接
142
- result = await client.call("message.send", {
143
- "to": "bob.example.com",
144
- "payload": {"type": "text", "text": "hello"},
145
- })
146
-
147
- # 短连接发完立即关闭(不影响长连接守护进程)
148
- await client.close()
149
- ```
150
-
151
- #### Token 复用机制
152
-
153
- 同一 `aun_path` 的多个 AUNClient 实例自动共享 keystore,包括:
154
-
155
- - `access_token`(JWT,1 小时有效期)
156
- - `refresh_token`(7 天有效期)
157
- - `access_token_expires_at`(精确到秒)
158
- - `gateway_url`(well-known discovery 结果)
159
-
160
- `authenticate()` 调用时优先读 keystore 中的有效 cached_token,命中则跳过两阶段 login(`auth.aid_login1` + `auth.aid_login2`),节省一次 well-known discovery + 两次 RPC 往返。
161
-
162
- 长连接守护进程的后台 `_token_refresh_task` 会在 token 过期前 30 分钟自动刷新并写回 keystore。CLI 短连接每次启动直接读到最新 token,无需关心刷新细节。
163
-
164
- token 过期或 refresh 失败时,SDK 自动 fallback:cached_token → refresh_token → 完整两阶段 login,应用层无感。
165
-
166
- #### 三种典型场景
167
-
168
- | 场景 | aun_path | connection_kind | slot_id |
169
- |---|---|---|---|
170
- | 守护进程常驻接收 | `/home/alice/.aun/alice` | `"long"` | `"main"` |
171
- | CLI 工具发消息 | `/home/alice/.aun/alice`(同上) | `"short"` | `"main"`(同上) |
172
- | 多实例独立运行 | `/home/alice/.aun/instance-N`(不同) | `"long"` | 自定义 |
173
-
174
- > 跨语言用法:TS / JS 用 `connectionKind` / `slotId`(camelCase),Go 用 `ConnectionKind` / `SlotID`(PascalCase),其余语义一致。
175
-
176
- ### 查看状态
177
-
178
- ```python
179
- print(client.state) # "connected"
180
- print(client.aid) # "alice.agentid.pub"
181
- ```
182
-
183
- ---
184
-
185
- ## 网关自动发现
186
-
187
- `register_aid()` / `authenticate()` 内部会自动发现 Gateway。
188
-
189
- - 生产配置(`verify_ssl=true`)下,优先尝试 `https://{aid}/.well-known/aun-gateway`
190
- - 若失败,则回退到 `https://gateway.{issuer}/.well-known/aun-gateway`
191
- - 开发配置(`verify_ssl=false`)下,为兼容未启用泛域名的环境,尝试顺序相反
192
-
193
- 发现到的 Gateway URL 会缓存在客户端内部,后续 `connect()` 默认复用。
194
-
195
- 发现成功后,SDK 会基于服务器返回的 Gateway WebSocket URL 动态构造健康检查地址:将末尾路径替换为 `/health`,并将 `wss://` / `ws://` 分别转换为 `https://` / `http://`。例如 `wss://gateway.example.com/aun` 会检查 `https://gateway.example.com/health`。健康检查使用 `GET /health`,结果可通过 `client.gateway_health`(Python)/ `client.gatewayHealth`(TS/JS)/ `client.GatewayHealth()`(Go)查询,也可主动调用 `check_gateway_health(url)` 触发检查。
196
-
197
- ### 跨域通信
198
-
199
- 当发送消息到不同 Issuer 的 AID 时(如 `alice.aid.pub` 发送给 `bob.example.com`),调用方式对用户保持一致:
200
-
201
- 1. 应用层仍通过当前 Gateway 会话调用 `message.send`
202
- 2. 需要对端证书/预密钥时,SDK 会根据目标 AID 的 issuer 派生或发现目标 Gateway 的 HTTP 端点
203
- 3. 真正的跨域消息路由由 Gateway / Federation 服务端链路完成,而不是由 SDK 为每个目标额外建立一个远端 WebSocket 会话
204
-
205
- **对用户完全透明**,无需额外配置。跨域消息和本域消息使用相同的 API:
206
-
207
- ```python
208
- # 本域消息
209
- await client.call("message.send", {"to": "bob.aid.pub", ...})
210
-
211
- # 跨域消息(自动路由)
212
- await client.call("message.send", {"to": "charlie.example.com", ...})
213
- ```
214
-
215
- > 跨域路由的详细实现机制见协议文档:[附录I-跨域消息路由实现指南](../src/aun_core/docs/protocol/附录I-跨域消息路由实现指南.md)
216
-
217
- ---
218
-
219
- ## Agent Web / agent.md
220
-
221
- Name Service 同时提供面向 Agent Web 的标准 HTTP 资源:
222
-
223
- - `PUT https://{aid}/agent.md`
224
- 需要 `Authorization: Bearer <access_token>`,用于上传或覆盖当前 AID 的公开 `agent.md`
225
- - `GET https://{aid}/agent.md`
226
- 匿名下载指定 AID 的 `agent.md`
227
- - `HEAD https://{aid}/agent.md`
228
- 匿名查询是否存在,并获取 `ETag`、`Last-Modified`、`Cache-Control`
229
-
230
- ### 推荐主 API(自 v0.x 起)
231
-
232
- SDK 在 `AUNClient` 上提供三个一站式主方法,分别封装"发布"、"下载"、"一致性检查"三条主线。文件统一存放在 `{aun_path}/AgentMDs/{aid}/agent.md`,由 SDK 管理;元数据持久化到 `agent_md_cache` 表 / `agentmd.json`。
233
-
234
- ```python
235
- # 发布自己的 agent.md(读 SDK 管理的本地文件 → 签名 → 上传 → 刷新内部 etag)
236
- await client.publish_agent_md()
237
-
238
- # 检查本地与云端一致性(不主动下载;max_unsynced_days=0 时强制 HEAD)
239
- state = await client.check_agent_md("bob.agentid.pub", max_unsynced_days=3)
240
-
241
- # 下载并自动验签(自动写到 SDK 管理的目录)
242
- if state["remote_found"] and not state["in_sync"]:
243
- info = await client.fetch_agent_md("bob.agentid.pub")
244
- print(info["signature"]["status"], info["in_sync"], info["saved_to"])
245
- ```
246
-
247
- 详细签名见 `06-API手册.md` 中的 `publish_agent_md` / `fetch_agent_md` / `check_agent_md` 章节。
248
-
249
- | SDK | publish | fetch | check |
250
- |------|---------|-------|-------|
251
- | Python | `client.publish_agent_md()` | `client.fetch_agent_md(aid?)` | `client.check_agent_md(aid?, max_unsynced_days=0)` |
252
- | TypeScript(Node) | `client.publishAgentMd()` | `client.fetchAgentMd(aid?)` | `client.checkAgentMd(aid?, maxUnsyncedDays=0)` |
253
- | Go | `client.PublishAgentMD(ctx)` | `client.FetchAgentMD(ctx, aid)` | `client.CheckAgentMD(ctx, aid, maxUnsyncedDays...)` |
254
- | C++ | `client.PublishAgentMd(out)` | `client.FetchAgentMd(aid, out)` | `client.CheckAgentMd(aid, max_days, out)` |
255
- | JavaScript(浏览器) | `client.publishAgentMd(content?)` | `client.fetchAgentMd(aid?)` | `client.checkAgentMd(aid?, maxUnsyncedDays=0)` |
256
-
257
- > **`check_agent_md` 不主动下载**:仅当远程存在且本地从未保存过该 aid 时,SDK 才异步触发后台 fetch;其他场景由应用层根据返回值决定是否调 `fetch_agent_md`。
258
-
259
- ### Deprecated(保留代码、未来版本将移除)
260
-
261
- | 旧方法 | 推荐替代 |
262
- |--------|----------|
263
- | `client.auth.sign_agent_md` | `client.publish_agent_md` 内部已包含 |
264
- | `client.auth.verify_agent_md` | `client.fetch_agent_md` 内部已包含 |
265
- | `client.auth.upload_agent_md` | `client.publish_agent_md` |
266
- | `client.auth.download_agent_md` | `client.fetch_agent_md` |
267
- | `client.auth.head_agent_md` | `client.check_agent_md`(带缓存窗口) |
268
-
269
- 底层方法仅推荐用于离线签名 / 纯文本验签等特殊场景。
270
-
271
- > v0.x 起删除了 `set_local_agent_md_path` / `get_local_agent_md_etag` / `get_remote_agent_md_etag` 三个 client 端 API;本地 etag 现在由 `publish_agent_md` / `fetch_agent_md(自身 aid)` 自动计算并缓存。事件 payload 仍会注入 `_agent_md.{local_etag, remote_etag}` 供应用层比对。
272
-
273
- ### Gateway 多 AID etag 推送(v0.x+)
274
-
275
- Gateway 在 RPC response 和事件通知的 `_meta` 中**最多同时注入两个 AID** 的 agent.md 元数据,每个条目都带 `aid` 字段:
276
-
277
- - **requester**:调用者 / 事件订阅方(自身)
278
- - **peer**:RPC 对端 / 事件源(仅当 `peer_aid != requester_aid` 时注入;否则该条目省略)
279
-
280
- ```json
281
- {
282
- "_meta": {
283
- "agent_md_etag": "\"requester-etag\"",
284
- "agent_md_etags": {
285
- "requester": {"aid": "alice.agentid.pub", "etag": "\"...\"", "last_modified": "..."},
286
- "peer": {"aid": "bob.agentid.pub", "etag": "\"...\"", "last_modified": "..."}
287
- }
288
- }
289
- }
290
- ```
291
-
292
- | 场景 | requester | peer |
293
- |------|-----------|------|
294
- | RPC response | 调用者 | RPC 涉及的 `to_aid`(不等于调用者时) |
295
- | 事件通知 | 订阅方 | 事件源(存在且不等于订阅方时) |
296
-
297
- `receiver` / `to` / `target` / `sender` / `from` 等键也会同时注入以兼容旧 SDK,但它们指向与 `requester` 或 `peer` 完全相同的对象(不是独立 AID)。**新 SDK 只需读 `requester` 和 `peer` 即可**。
298
-
299
- SDK 收到后通过 `_observe_agent_md_meta` 写入 `agent_md_cache` 持久化记录(按 AID 自动去重);若发现"远程有 etag 但本地从未保存该 aid",会异步触发后台 fetch(不阻塞 RPC 返回)。
300
-
301
- ### 错误返回
302
-
303
- - `PUT /agent.md` 可能返回 `401`(缺失或无效 token)、`403`(token 的 AID 与 Host 不匹配)、`400`(frontmatter 非法或 frontmatter.aid 与 Host 不匹配)、`413`(文档超过大小上限)
304
- - `GET/HEAD /agent.md` 在目标尚未发布时返回 `404`
305
- - 主 API 在上述场景抛对应异常(NotFoundError / AUNError 等)
306
-
307
- ---
308
-
309
- ## 调用 RPC 方法
310
-
311
- ### `client.call(method, params)` — 通用 RPC 接口
312
-
313
- 认证连接后,所有业务操作通过 `client.call()` 调用服务端 RPC 方法:
314
-
315
- ```python
316
- result = await client.call(method: str, params: dict | None = None) -> Any
317
- ```
318
-
319
- **示例:**
320
-
321
- ```python
322
- # 发送消息
323
- result = await client.call("message.send", {
324
- "to": "bob.agentid.pub",
325
- "payload": {"type": "text", "text": "Hello!"},
326
- })
327
- print(result) # {"message_id": "...", "seq": 123, "status": "sent"}
328
-
329
- # 拉取消息
330
- result = await client.call("message.pull", {"after_seq": 0, "limit": 20})
331
- print(result) # {"messages": [...], "count": 5, "latest_seq": 128}
332
-
333
- # 创建群组
334
- result = await client.call("group.create", {
335
- "name": "项目组",
336
- "members": ["bob.agentid.pub", "carol.agentid.pub"],
337
- })
338
- print(result) # {"group_id": "...", "created_at": ...}
339
- ```
340
-
341
- **错误处理:**
342
-
343
- ```python
344
- from aun_core import AUNError, NotFoundError, RateLimitError
345
-
346
- try:
347
- result = await client.call("message.send", {...})
348
- except NotFoundError as e:
349
- print(f"目标不存在: {e.code}")
350
- except RateLimitError as e:
351
- print(f"请求限流,{e.data['retry_after']}秒后重试")
352
- except AUNError as e:
353
- print(f"RPC 错误: code={e.code}, retryable={e.retryable}")
354
- ```
355
-
356
- **RPC 方法完整参数和响应格式见 RPC 手册:**
357
-
358
- | 领域 | 手册 | 涵盖方法 |
359
- |------|------|----------|
360
- | 消息 | [09-message-rpc-manual.md](09-message-rpc-manual.md) | message.send / pull / ack / recall / thought.put / thought.get |
361
- | 群组 | [09-group-rpc-manual.md](09-group-rpc-manual.md) | 群组生命周期、成员管理、群设置、群消息、群 thought |
362
- | 存储 | [09-storage-rpc-manual.md](09-storage-rpc-manual.md) | 文件上传下载、对象存储 |
363
- | 元信息 | [09-meta-rpc-manual.md](09-meta-rpc-manual.md) | meta.ping / status / trust_roots |
364
-
365
- ---
366
-
367
- ## 事件订阅
368
-
369
- ### `client.on(event, handler)` — 订阅事件
370
-
371
- **`on()` 应在 `connect()` 之前调用**,否则连接建立瞬间触发的事件(如 `connection.state`)会丢失。
372
-
373
- 服务端推送的事件通过 `client.on()` 订阅,支持同步和异步 handler:
374
-
375
- ```python
376
- subscription = client.on(event: str, handler: Callable) -> Subscription
377
- ```
378
-
379
- **同步 handler:**
380
-
381
- ```python
382
- def on_message(event):
383
- print(f"收到消息: {event['payload']}")
384
-
385
- sub = client.on("message.received", on_message)
386
- ```
387
-
388
- **异步 handler:**
389
-
390
- ```python
391
- async def on_message(event):
392
- await process_message(event)
393
- await client.call("message.ack", {"seq": event["seq"]})
394
-
395
- sub = client.on("message.received", on_message)
396
- ```
397
-
398
- **只触发一次(手动取消订阅):**
399
-
400
- ```python
401
- def on_state_change(e):
402
- print(f"状态变更: {e}")
403
- sub.unsubscribe() # 触发后立即取消
404
-
405
- sub = client.on("connection.state", on_state_change)
406
- ```
407
-
408
- **取消订阅:**
409
-
410
- ```python
411
- sub = client.on("message.received", handler)
412
- # ... 稍后
413
- sub.unsubscribe()
414
- ```
415
-
416
- **多个 handler:**
417
-
418
- ```python
419
- # 同一事件可注册多个 handler,按注册顺序依次调用
420
- client.on("message.received", log_message)
421
- client.on("message.received", update_ui)
422
- client.on("message.received", send_notification)
423
- ```
424
-
425
- **常用事件示例:**
426
-
427
- ```python
428
- # 连接状态变化
429
- client.on("connection.state", lambda e: print(f"状态: {e['state']}"))
430
-
431
- # 消息推送
432
- client.on("message.received", handle_new_message)
433
-
434
- # 群组变更
435
- client.on("group.changed", lambda e: print(f"群组 {e['group_id']} 已更新"))
436
-
437
- # 令牌刷新
438
- client.on("token.refreshed", lambda e: print(f"令牌已刷新: {e['aid']}"))
439
-
440
- # 连接错误
441
- client.on("connection.error", lambda e: print(f"连接错误: {e}"))
442
- ```
443
-
444
- 内置事件完整列表见 [06-API手册.md](06-API手册.md) 的「内置事件」节。
445
-
446
- ---
447
-
448
- ## 关闭连接
449
-
450
- ```python
451
- await client.close()
452
- ```
453
-
454
- 关闭后状态变为 `"closed"`,不可复用,需重新创建 `AUNClient`。
1
+ # AUN SDK Python - 连接与认证
2
+
3
+ ---
4
+
5
+ ## v0.3.4 行为变更
6
+
7
+ ### 注册与认证分离
8
+
9
+ v0.3.4 起,`authenticate()` **不再隐式注册**。如果本地无完整身份(缺 keypair 或 cert),`authenticate()` 直接抛 `StateError`。
10
+
11
+ 正确流程:
12
+
13
+ ```python
14
+ await client.auth.register_aid({"aid": aid}) # 1. 注册(生成密钥对 + 获取证书)
15
+ auth = await client.auth.authenticate({"aid": aid}) # 2. 认证(获取 token)
16
+ await client.connect(auth, {}) # 3. 连接
17
+ ```
18
+
19
+ **半成品恢复**:如果本地有 keypair 但无 cert(上次注册中断),`register_aid()` 会自动向服务端查询并恢复证书,无需手动清理。
20
+
21
+ ### 身份查询 API
22
+
23
+ v0.3.4 新增两个只读 API,用于检查本地身份状态(无网络请求、无副作用):
24
+
25
+ ```python
26
+ # 加载身份(不存在时抛 StateError)
27
+ identity = client.auth.load_identity({"aid": aid})
28
+
29
+ # 加载身份(不存在时返回 None)
30
+ identity = client.auth.load_identity_or_none({"aid": aid})
31
+
32
+ # 获取对端证书 PEM(本地缓存优先,未命中走 PKI)
33
+ cert_pem = await client.auth.fetch_peer_cert({"aid": peer_aid})
34
+ ```
35
+
36
+ ### 迁移说明
37
+
38
+ | API(v0.3.3-) | 新 API(v0.3.4+) |
39
+ |---|---|
40
+ | `create_aid()` | `register_aid()`(已移除 `create_aid`) |
41
+ | `authenticate()` 自动注册 | 必须先显式调用 `register_aid()` |
42
+
43
+ ---
44
+
45
+ ## 注册 AID 和认证
46
+
47
+ ```python
48
+ from aun_core import AUNClient
49
+
50
+ async def setup(aid: str):
51
+ client = AUNClient() # 默认 aun_path: ~/.aun
52
+
53
+ # 注册 AID
54
+ try:
55
+ result = await client.auth.register_aid({"aid": aid})
56
+ # result: {"aid": ..., "cert_pem": ..., "gateway": ...}
57
+ except Exception as e:
58
+ print(f"注册 AID 失败: {e}")
59
+ raise
60
+
61
+ auth = await client.auth.authenticate({"aid": aid})
62
+ # auth: {"aid": ..., "access_token": ..., "refresh_token": ...,
63
+ # "expires_at": ..., "gateway": ...}
64
+ return client, auth
65
+ ```
66
+
67
+ ---
68
+
69
+ ## 连接到网关
70
+
71
+ **必须先调用 `client.auth.authenticate()` 获取令牌和网关地址,再调用 `connect()`,此步骤不可跳过。** `authenticate()` 返回的 `gateway` 字段即为网关 WebSocket 地址。
72
+
73
+ > 当前各语言 SDK 的稳定连接模式都是 Gateway。协议层虽然定义了 Peer / Relay,但 Python SDK 的 `connect(topology=...)` 目前会对 `peer` / `relay` 明确返回未实现。
74
+
75
+ ### 基础连接
76
+
77
+ ```python
78
+ # 第一步:认证
79
+ auth = await client.auth.authenticate({"aid": MY_AID})
80
+ # auth["access_token"] 访问令牌
81
+ # auth["gateway"] — 网关 WebSocket 地址
82
+
83
+ # 第二步:连接(auth 结果 + 连接选项)
84
+ await client.connect(auth, {})
85
+ ```
86
+
87
+ ### 完整连接选项
88
+
89
+ ```python
90
+ # 以下为可选覆盖值;不传时默认 auto_reconnect=true、heartbeat_interval=30、
91
+ # token_refresh_before=60、retry.initial_delay=1.0、retry.max_delay=64.0、
92
+ # timeouts={connect:5, call:10, http:30}
93
+ await client.connect(auth, {
94
+ "auto_reconnect": True, # 断线自动重连
95
+ "heartbeat_interval": 30.0, # 心跳间隔(秒)
96
+ "token_refresh_before": 60.0, # 令牌刷新提前量(秒)
97
+ "connection_kind": "long", # 可选,"long"(默认)或 "short"
98
+ "short_ttl_ms": 30000, # 可选,仅 kind=short 时有效,服务端兜底超时
99
+ "retry": {
100
+ "initial_delay": 1.0, # 初始退避延迟
101
+ "max_delay": 64.0, # 最大退避延迟
102
+ },
103
+ "timeouts": {
104
+ "connect": 5.0, # WebSocket 连接超时
105
+ "call": 10.0, # RPC 调用超时
106
+ "http": 30.0, # HTTP 请求超时
107
+ },
108
+ })
109
+ ```
110
+
111
+ > 当前实现只读取 `retry.initial_delay` / `retry.max_delay`;未提供 `retry.max_attempts` 选项,上层如需停止自动重连,应主动关闭客户端。
112
+
113
+ **长短连接共存**:同一 `(aid, device_id, slot_id)` 槽位下允许 1 条长连接 + 最多 10 条短连接。长连接承担服务端推送(消息、事件);短连接仅用于 RPC 请求-响应后立即断开(CLI 工具场景)。短连接默认禁用 `auto_reconnect`、心跳和 token 主动刷新。
114
+
115
+ ### 长连接 / 短连接代码示例
116
+
117
+ #### 长连接(守护进程:常驻收件箱)
118
+
119
+ ```python
120
+ from aun_core import AUNClient
121
+
122
+ client = AUNClient({"aun_path": "/home/alice/.aun/alice"})
123
+ # 首次:走完整 login + discovery;之后:复用 keystore 里的 cached token + gateway_url
124
+ auth = await client.auth.authenticate({"aid": "alice.example.com"})
125
+ await client.connect(auth, {
126
+ "connection_kind": "long", # 默认值,可省略
127
+ "slot_id": "main",
128
+ "auto_reconnect": True,
129
+ })
130
+
131
+ # 监听消息推送
132
+ client.on("message.received", lambda data: print(data["payload"]))
133
+
134
+ # 常驻
135
+ await asyncio.Event().wait()
136
+ ```
137
+
138
+ #### 短连接(CLI 工具:发完即退)
139
+
140
+ ```python
141
+ from aun_core import AUNClient
142
+
143
+ # 关键:使用与长连接守护进程相同的 aun_path → 共享 keystore → 自动复用 token
144
+ client = AUNClient({"aun_path": "/home/alice/.aun/alice"})
145
+
146
+ # authenticate 自动从 keystore 读 cached access_token,跳过两阶段 login
147
+ auth = await client.auth.authenticate({"aid": "alice.example.com"})
148
+
149
+ # connect 时声明 short kind + 同 slot_id(与长连接共存于同一槽位)
150
+ await client.connect(auth, {
151
+ "connection_kind": "short",
152
+ "slot_id": "main", # 与长连接同 slot
153
+ "short_ttl_ms": 30000, # 服务端兜底超时(防 CLI 异常退出占名额)
154
+ })
155
+
156
+ # RPC,响应原路返回到这条短连接
157
+ result = await client.call("message.send", {
158
+ "to": "bob.example.com",
159
+ "payload": {"type": "text", "text": "hello"},
160
+ })
161
+
162
+ # 短连接发完立即关闭(不影响长连接守护进程)
163
+ await client.close()
164
+ ```
165
+
166
+ #### Token 复用机制
167
+
168
+ 同一 `aun_path` 的多个 AUNClient 实例自动共享 keystore,包括:
169
+
170
+ - `access_token`(JWT,1 小时有效期)
171
+ - `refresh_token`(7 天有效期)
172
+ - `access_token_expires_at`(精确到秒)
173
+ - `gateway_url`(well-known discovery 结果)
174
+
175
+ `authenticate()` 调用时优先读 keystore 中的有效 cached_token,命中则跳过两阶段 login(`auth.aid_login1` + `auth.aid_login2`),节省一次 well-known discovery + 两次 RPC 往返。
176
+
177
+ 长连接守护进程的后台 `_token_refresh_task` 会在 token 过期前 30 分钟自动刷新并写回 keystore。CLI 短连接每次启动直接读到最新 token,无需关心刷新细节。
178
+
179
+ token 过期或 refresh 失败时,SDK 自动 fallback:cached_token → refresh_token → 完整两阶段 login,应用层无感。
180
+
181
+ #### 三种典型场景
182
+
183
+ | 场景 | aun_path | connection_kind | slot_id |
184
+ |---|---|---|---|
185
+ | 守护进程常驻接收 | `/home/alice/.aun/alice` | `"long"` | `"main"` |
186
+ | CLI 工具发消息 | `/home/alice/.aun/alice`(同上) | `"short"` | `"main"`(同上) |
187
+ | 多实例独立运行 | `/home/alice/.aun/instance-N`(不同) | `"long"` | 自定义 |
188
+
189
+ > 跨语言用法:TS / JS 用 `connectionKind` / `slotId`(camelCase),Go 用 `ConnectionKind` / `SlotID`(PascalCase),其余语义一致。
190
+
191
+ ### 查看状态
192
+
193
+ ```python
194
+ print(client.state) # "connected"
195
+ print(client.aid) # "alice.agentid.pub"
196
+ ```
197
+
198
+ ---
199
+
200
+ ## 网关自动发现
201
+
202
+ `register_aid()` / `authenticate()` 内部会自动发现 Gateway
203
+
204
+ - 生产配置(`verify_ssl=true`)下,优先尝试 `https://{aid}/.well-known/aun-gateway`
205
+ - 若失败,则回退到 `https://gateway.{issuer}/.well-known/aun-gateway`
206
+ - 开发配置(`verify_ssl=false`)下,为兼容未启用泛域名的环境,尝试顺序相反
207
+
208
+ 发现到的 Gateway URL 会缓存在客户端内部,后续 `connect()` 默认复用。
209
+
210
+ 发现成功后,SDK 会基于服务器返回的 Gateway WebSocket URL 动态构造健康检查地址:将末尾路径替换为 `/health`,并将 `wss://` / `ws://` 分别转换为 `https://` / `http://`。例如 `wss://gateway.example.com/aun` 会检查 `https://gateway.example.com/health`。健康检查使用 `GET /health`,结果可通过 `client.gateway_health`(Python)/ `client.gatewayHealth`(TS/JS)/ `client.GatewayHealth()`(Go)查询,也可主动调用 `check_gateway_health(url)` 触发检查。
211
+
212
+ ### 跨域通信
213
+
214
+ 当发送消息到不同 Issuer 的 AID 时(如 `alice.aid.pub` 发送给 `bob.example.com`),调用方式对用户保持一致:
215
+
216
+ 1. 应用层仍通过当前 Gateway 会话调用 `message.send`
217
+ 2. 需要对端证书/预密钥时,SDK 会根据目标 AID 的 issuer 派生或发现目标 Gateway 的 HTTP 端点
218
+ 3. 真正的跨域消息路由由 Gateway / Federation 服务端链路完成,而不是由 SDK 为每个目标额外建立一个远端 WebSocket 会话
219
+
220
+ **对用户完全透明**,无需额外配置。跨域消息和本域消息使用相同的 API:
221
+
222
+ ```python
223
+ # 本域消息
224
+ await client.call("message.send", {"to": "bob.aid.pub", ...})
225
+
226
+ # 跨域消息(自动路由)
227
+ await client.call("message.send", {"to": "charlie.example.com", ...})
228
+ ```
229
+
230
+ > 跨域路由的详细实现机制见协议文档:[附录I-跨域消息路由实现指南](../src/aun_core/docs/protocol/附录I-跨域消息路由实现指南.md)
231
+
232
+ ---
233
+
234
+ ## Agent Web / agent.md
235
+
236
+ Name Service 同时提供面向 Agent Web 的标准 HTTP 资源:
237
+
238
+ - `PUT https://{aid}/agent.md`
239
+ 需要 `Authorization: Bearer <access_token>`,用于上传或覆盖当前 AID 的公开 `agent.md`
240
+ - `GET https://{aid}/agent.md`
241
+ 匿名下载指定 AID `agent.md`
242
+ - `HEAD https://{aid}/agent.md`
243
+ 匿名查询是否存在,并获取 `ETag`、`Last-Modified`、`Cache-Control`
244
+
245
+ ### 推荐主 API(自 v0.x 起)
246
+
247
+ SDK `AUNClient` 上提供三个一站式主方法,分别封装"发布"、"下载"、"一致性检查"三条主线。文件统一存放在 `{aun_path}/AgentMDs/{aid}/agent.md`,由 SDK 管理;元数据持久化到 `agent_md_cache` / `agentmd.json`。
248
+
249
+ ```python
250
+ # 发布自己的 agent.md(读 SDK 管理的本地文件 → 签名 → 上传 → 刷新内部 etag)
251
+ await client.publish_agent_md()
252
+
253
+ # 检查本地与云端一致性(不主动下载;max_unsynced_days=0 时强制 HEAD)
254
+ state = await client.check_agent_md("bob.agentid.pub", max_unsynced_days=3)
255
+
256
+ # 下载并自动验签(自动写到 SDK 管理的目录)
257
+ if state["remote_found"] and not state["in_sync"]:
258
+ info = await client.fetch_agent_md("bob.agentid.pub")
259
+ print(info["signature"]["status"], info["in_sync"], info["saved_to"])
260
+ ```
261
+
262
+ 详细签名见 `06-API手册.md` 中的 `publish_agent_md` / `fetch_agent_md` / `check_agent_md` 章节。
263
+
264
+ | SDK | publish | fetch | check |
265
+ |------|---------|-------|-------|
266
+ | Python | `client.publish_agent_md()` | `client.fetch_agent_md(aid?)` | `client.check_agent_md(aid?, max_unsynced_days=0)` |
267
+ | TypeScript(Node) | `client.publishAgentMd()` | `client.fetchAgentMd(aid?)` | `client.checkAgentMd(aid?, maxUnsyncedDays=0)` |
268
+ | Go | `client.PublishAgentMD(ctx)` | `client.FetchAgentMD(ctx, aid)` | `client.CheckAgentMD(ctx, aid, maxUnsyncedDays...)` |
269
+ | C++ | `client.PublishAgentMd(out)` | `client.FetchAgentMd(aid, out)` | `client.CheckAgentMd(aid, max_days, out)` |
270
+ | JavaScript(浏览器) | `client.publishAgentMd(content?)` | `client.fetchAgentMd(aid?)` | `client.checkAgentMd(aid?, maxUnsyncedDays=0)` |
271
+
272
+ > **`check_agent_md` 不主动下载**:仅当远程存在且本地从未保存过该 aid 时,SDK 才异步触发后台 fetch;其他场景由应用层根据返回值决定是否调 `fetch_agent_md`。
273
+
274
+ ### Deprecated(保留代码、未来版本将移除)
275
+
276
+ | 旧方法 | 推荐替代 |
277
+ |--------|----------|
278
+ | `client.auth.sign_agent_md` | `client.publish_agent_md` 内部已包含 |
279
+ | `client.auth.verify_agent_md` | `client.fetch_agent_md` 内部已包含 |
280
+ | `client.auth.upload_agent_md` | `client.publish_agent_md` |
281
+ | `client.auth.download_agent_md` | `client.fetch_agent_md` |
282
+ | `client.auth.head_agent_md` | `client.check_agent_md`(带缓存窗口) |
283
+
284
+ 底层方法仅推荐用于离线签名 / 纯文本验签等特殊场景。
285
+
286
+ > v0.x 起删除了 `set_local_agent_md_path` / `get_local_agent_md_etag` / `get_remote_agent_md_etag` 三个 client 端 API;本地 etag 现在由 `publish_agent_md` / `fetch_agent_md(自身 aid)` 自动计算并缓存。事件 payload 仍会注入 `_agent_md.{local_etag, remote_etag}` 供应用层比对。
287
+
288
+ ### Gateway 多 AID etag 推送(v0.x+)
289
+
290
+ Gateway 在 RPC response 和事件通知的 `_meta` 中**最多同时注入两个 AID** 的 agent.md 元数据,每个条目都带 `aid` 字段:
291
+
292
+ - **requester**:调用者 / 事件订阅方(自身)
293
+ - **peer**:RPC 对端 / 事件源(仅当 `peer_aid != requester_aid` 时注入;否则该条目省略)
294
+
295
+ ```json
296
+ {
297
+ "_meta": {
298
+ "agent_md_etag": "\"requester-etag\"",
299
+ "agent_md_etags": {
300
+ "requester": {"aid": "alice.agentid.pub", "etag": "\"...\"", "last_modified": "..."},
301
+ "peer": {"aid": "bob.agentid.pub", "etag": "\"...\"", "last_modified": "..."}
302
+ }
303
+ }
304
+ }
305
+ ```
306
+
307
+ | 场景 | requester | peer |
308
+ |------|-----------|------|
309
+ | RPC response | 调用者 | RPC 涉及的 `to_aid`(不等于调用者时) |
310
+ | 事件通知 | 订阅方 | 事件源(存在且不等于订阅方时) |
311
+
312
+ `receiver` / `to` / `target` / `sender` / `from` 等键也会同时注入以兼容旧 SDK,但它们指向与 `requester` 或 `peer` 完全相同的对象(不是独立 AID)。**新 SDK 只需读 `requester` 和 `peer` 即可**。
313
+
314
+ SDK 收到后通过 `_observe_agent_md_meta` 写入 `agent_md_cache` 持久化记录(按 AID 自动去重);若发现"远程有 etag 但本地从未保存该 aid",会异步触发后台 fetch(不阻塞 RPC 返回)。
315
+
316
+ ### 错误返回
317
+
318
+ - `PUT /agent.md` 可能返回 `401`(缺失或无效 token)、`403`(token 的 AID 与 Host 不匹配)、`400`(frontmatter 非法或 frontmatter.aid 与 Host 不匹配)、`413`(文档超过大小上限)
319
+ - `GET/HEAD /agent.md` 在目标尚未发布时返回 `404`
320
+ - 主 API 在上述场景抛对应异常(NotFoundError / AUNError 等)
321
+
322
+ ---
323
+
324
+ ## 调用 RPC 方法
325
+
326
+ ### `client.call(method, params)` — 通用 RPC 接口
327
+
328
+ 认证连接后,所有业务操作通过 `client.call()` 调用服务端 RPC 方法:
329
+
330
+ ```python
331
+ result = await client.call(method: str, params: dict | None = None) -> Any
332
+ ```
333
+
334
+ **示例:**
335
+
336
+ ```python
337
+ # 发送消息
338
+ result = await client.call("message.send", {
339
+ "to": "bob.agentid.pub",
340
+ "payload": {"type": "text", "text": "Hello!"},
341
+ })
342
+ print(result) # {"message_id": "...", "seq": 123, "status": "sent"}
343
+
344
+ # 拉取消息
345
+ result = await client.call("message.pull", {"after_seq": 0, "limit": 20})
346
+ print(result) # {"messages": [...], "count": 5, "latest_seq": 128}
347
+
348
+ # 创建群组
349
+ result = await client.call("group.create", {
350
+ "name": "项目组",
351
+ "members": ["bob.agentid.pub", "carol.agentid.pub"],
352
+ })
353
+ print(result) # {"group_id": "...", "created_at": ...}
354
+ ```
355
+
356
+ **错误处理:**
357
+
358
+ ```python
359
+ from aun_core import AUNError, NotFoundError, RateLimitError
360
+
361
+ try:
362
+ result = await client.call("message.send", {...})
363
+ except NotFoundError as e:
364
+ print(f"目标不存在: {e.code}")
365
+ except RateLimitError as e:
366
+ print(f"请求限流,{e.data['retry_after']}秒后重试")
367
+ except AUNError as e:
368
+ print(f"RPC 错误: code={e.code}, retryable={e.retryable}")
369
+ ```
370
+
371
+ **RPC 方法完整参数和响应格式见 RPC 手册:**
372
+
373
+ | 领域 | 手册 | 涵盖方法 |
374
+ |------|------|----------|
375
+ | 消息 | [09-message-rpc-manual.md](09-message-rpc-manual.md) | message.send / pull / ack / recall / thought.put / thought.get |
376
+ | 群组 | [09-group-rpc-manual.md](09-group-rpc-manual.md) | 群组生命周期、成员管理、群设置、群消息、群 thought |
377
+ | 存储 | [09-storage-rpc-manual.md](09-storage-rpc-manual.md) | 文件上传下载、对象存储 |
378
+ | 元信息 | [09-meta-rpc-manual.md](09-meta-rpc-manual.md) | meta.ping / status / trust_roots |
379
+
380
+ ---
381
+
382
+ ## 事件订阅
383
+
384
+ ### `client.on(event, handler)` — 订阅事件
385
+
386
+ **`on()` 应在 `connect()` 之前调用**,否则连接建立瞬间触发的事件(如 `connection.state`)会丢失。
387
+
388
+ 服务端推送的事件通过 `client.on()` 订阅,支持同步和异步 handler
389
+
390
+ ```python
391
+ subscription = client.on(event: str, handler: Callable) -> Subscription
392
+ ```
393
+
394
+ **同步 handler:**
395
+
396
+ ```python
397
+ def on_message(event):
398
+ print(f"收到消息: {event['payload']}")
399
+
400
+ sub = client.on("message.received", on_message)
401
+ ```
402
+
403
+ **异步 handler:**
404
+
405
+ ```python
406
+ async def on_message(event):
407
+ await process_message(event)
408
+ await client.call("message.ack", {"seq": event["seq"]})
409
+
410
+ sub = client.on("message.received", on_message)
411
+ ```
412
+
413
+ **只触发一次(手动取消订阅):**
414
+
415
+ ```python
416
+ def on_state_change(e):
417
+ print(f"状态变更: {e}")
418
+ sub.unsubscribe() # 触发后立即取消
419
+
420
+ sub = client.on("connection.state", on_state_change)
421
+ ```
422
+
423
+ **取消订阅:**
424
+
425
+ ```python
426
+ sub = client.on("message.received", handler)
427
+ # ... 稍后
428
+ sub.unsubscribe()
429
+ ```
430
+
431
+ **多个 handler:**
432
+
433
+ ```python
434
+ # 同一事件可注册多个 handler,按注册顺序依次调用
435
+ client.on("message.received", log_message)
436
+ client.on("message.received", update_ui)
437
+ client.on("message.received", send_notification)
438
+ ```
439
+
440
+ **常用事件示例:**
441
+
442
+ ```python
443
+ # 连接状态变化
444
+ client.on("connection.state", lambda e: print(f"状态: {e['state']}"))
445
+
446
+ # 消息推送
447
+ client.on("message.received", handle_new_message)
448
+
449
+ # 群组变更
450
+ client.on("group.changed", lambda e: print(f"群组 {e['group_id']} 已更新"))
451
+
452
+ # 令牌刷新
453
+ client.on("token.refreshed", lambda e: print(f"令牌已刷新: {e['aid']}"))
454
+
455
+ # 连接错误
456
+ client.on("connection.error", lambda e: print(f"连接错误: {e}"))
457
+ ```
458
+
459
+ 内置事件完整列表见 [06-API手册.md](06-API手册.md) 的「内置事件」节。
460
+
461
+ ---
462
+
463
+ ## 关闭连接
464
+
465
+ ```python
466
+ await client.close()
467
+ ```
468
+
469
+ 关闭后状态变为 `"closed"`,不可复用,需重新创建 `AUNClient`。