@agentunion/fastaun-browser 0.2.19 → 0.2.20

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 (81) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/_packed_docs/CHANGELOG.md +26 -0
  3. package/_packed_docs/agent.md/SCHEMA.md +173 -0
  4. package/_packed_docs/agent.md/examples/codeagent-claudecode.md +61 -0
  5. package/_packed_docs/agent.md/examples/human-developer.md +60 -0
  6. package/_packed_docs/agent.md/examples/openclaw-lobster.md +52 -0
  7. package/_packed_docs/agent.md/examples/signed-openclaw-lobster.md +43 -0
  8. package/_packed_docs/protocol/00-/346/200/273/350/247/210/344/270/216/345/210/206/345/261/202.md +205 -0
  9. 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 +197 -0
  10. 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 +549 -0
  11. package/_packed_docs/protocol/02-/350/257/201/344/271/246/344/270/216/344/277/241/344/273/273/344/275/223/347/263/273.md +810 -0
  12. package/_packed_docs/protocol/03-Gateway-/350/277/236/346/216/245/346/250/241/345/274/217.md +262 -0
  13. package/_packed_docs/protocol/04-Peer-/345/255/220/345/215/217/350/256/256.md +180 -0
  14. package/_packed_docs/protocol/05-Relay-/345/255/220/345/215/217/350/256/256.md +164 -0
  15. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1135 -0
  16. 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 +234 -0
  17. package/_packed_docs/protocol/08-AUN-E2EE-Group.md +900 -0
  18. package/_packed_docs/protocol/08-AUN-E2EE.md +413 -0
  19. package/_packed_docs/protocol/09-/345/256/211/345/205/250/350/200/203/350/231/221.md +316 -0
  20. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +804 -0
  21. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +271 -0
  22. package/_packed_docs/protocol/12-Stream-/345/255/220/345/215/217/350/256/256.md +329 -0
  23. package/_packed_docs/protocol/13-Agent/350/241/214/344/270/272/350/247/204/350/214/203.md +141 -0
  24. 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 -0
  25. package/_packed_docs/protocol/README.md +71 -0
  26. package/_packed_docs/protocol/agent.md/SCHEMA.md +118 -0
  27. package/_packed_docs/protocol/agent.md/examples/codeagent-claudecode.md +61 -0
  28. package/_packed_docs/protocol/agent.md/examples/human-developer.md +60 -0
  29. package/_packed_docs/protocol/agent.md/examples/openclaw-lobster.md +52 -0
  30. package/_packed_docs/protocol/aun-docs-guide.md +49 -0
  31. package/_packed_docs/protocol/index.md +114 -0
  32. package/_packed_docs/protocol//350/215/211/346/241/210-agent.md/347/255/276/345/220/215/345/215/217/350/256/256.md +205 -0
  33. 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 +249 -0
  34. package/_packed_docs/protocol//351/231/204/345/275/225A-/346/234/257/350/257/255/350/241/250.md +337 -0
  35. 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 +80 -0
  36. package/_packed_docs/protocol//351/231/204/345/275/225C-/347/247/201/351/222/245/347/256/241/347/220/206/344/270/216/350/272/253/344/273/275/346/201/242/345/244/215.md +704 -0
  37. package/_packed_docs/protocol//351/231/204/345/275/225D-Root_CA_/346/262/273/347/220/206/346/234/272/345/210/266.md +620 -0
  38. package/_packed_docs/protocol//351/231/204/345/275/225E-Root_CA_/345/207/206/345/205/245/346/265/201/347/250/213.md +605 -0
  39. package/_packed_docs/protocol//351/231/204/345/275/225F-Issuer_CA_/347/224/263/350/257/267/346/265/201/347/250/213.md +548 -0
  40. package/_packed_docs/protocol//351/231/204/345/275/225G-AID_/345/255/244/345/204/277/351/242/204/351/230/262/344/270/216/346/225/221/346/217/264/346/234/272/345/210/266.md +513 -0
  41. package/_packed_docs/protocol//351/231/204/345/275/225H-Identity/346/234/215/345/212/241/345/256/236/347/216/260/346/214/207/345/215/227.md +619 -0
  42. package/_packed_docs/protocol//351/231/204/345/275/225I-/350/267/250/345/237/237/346/266/210/346/201/257/350/267/257/347/224/261/345/256/236/347/216/260/346/214/207/345/215/227.md +492 -0
  43. 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 +402 -0
  44. package/_packed_docs/protocol//351/231/204/345/275/225K-Agent_Web/345/217/221/347/216/260/345/215/217/350/256/256.md +130 -0
  45. package/_packed_docs/protocol//351/231/204/345/275/225L-E2EE/345/256/236/347/216/260/346/214/207/345/215/227.md +267 -0
  46. 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 +367 -0
  47. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +223 -0
  48. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +354 -0
  49. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +172 -0
  50. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +373 -0
  51. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +611 -0
  52. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +1152 -0
  53. package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +150 -0
  54. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +89 -0
  55. package/_packed_docs/sdk/09-custody-api-manual.md +445 -0
  56. package/_packed_docs/sdk/09-group-rpc-manual.md +1895 -0
  57. package/_packed_docs/sdk/09-message-rpc-manual.md +597 -0
  58. package/_packed_docs/sdk/09-meta-rpc-manual.md +142 -0
  59. package/_packed_docs/sdk/09-payload-reference.md +702 -0
  60. package/_packed_docs/sdk/09-storage-rpc-manual.md +408 -0
  61. package/_packed_docs/sdk/09-stream-rpc-manual.md +275 -0
  62. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +72 -0
  63. package/_packed_docs/sdk/INDEX.md +131 -0
  64. package/_packed_docs/sdk/README.md +307 -0
  65. package/dist/auth.d.ts +2 -1
  66. package/dist/auth.d.ts.map +1 -1
  67. package/dist/auth.js +13 -11
  68. package/dist/auth.js.map +1 -1
  69. package/dist/client.d.ts +38 -8
  70. package/dist/client.d.ts.map +1 -1
  71. package/dist/client.js +179 -97
  72. package/dist/client.js.map +1 -1
  73. package/dist/namespaces/auth.d.ts +1 -0
  74. package/dist/namespaces/auth.d.ts.map +1 -1
  75. package/dist/namespaces/auth.js +20 -6
  76. package/dist/namespaces/auth.js.map +1 -1
  77. package/dist/transport.d.ts +9 -1
  78. package/dist/transport.d.ts.map +1 -1
  79. package/dist/transport.js +24 -0
  80. package/dist/transport.js.map +1 -1
  81. package/package.json +40 -37
package/dist/client.js CHANGED
@@ -87,7 +87,7 @@ const DEFAULT_SESSION_OPTIONS = {
87
87
  },
88
88
  timeouts: {
89
89
  connect: 5.0,
90
- call: 10.0,
90
+ call: 35.0,
91
91
  http: 30.0,
92
92
  },
93
93
  };
@@ -99,6 +99,20 @@ const GROUP_ROTATION_RETRY_MAX_DELAY_MS = 300_000;
99
99
  const PENDING_DECRYPT_LIMIT = 100;
100
100
  const PUSHED_SEQS_LIMIT = 50_000;
101
101
  const PENDING_ORDERED_LIMIT = 50_000;
102
+ // 心跳间隔下/上限(秒)。0 = 关闭心跳;负值视为 0;其余值 clamp 到 [10, 600]。
103
+ // 服务端通过 hello.heartbeat_interval 与 meta.ping pong 中的同名字段下发。
104
+ const HEARTBEAT_MIN_INTERVAL_SECONDS = 10;
105
+ const HEARTBEAT_MAX_INTERVAL_SECONDS = 600;
106
+ function clampHeartbeatInterval(value) {
107
+ const n = Number(value);
108
+ if (!Number.isFinite(n) || n <= 0)
109
+ return 0;
110
+ if (n < HEARTBEAT_MIN_INTERVAL_SECONDS)
111
+ return HEARTBEAT_MIN_INTERVAL_SECONDS;
112
+ if (n > HEARTBEAT_MAX_INTERVAL_SECONDS)
113
+ return HEARTBEAT_MAX_INTERVAL_SECONDS;
114
+ return n;
115
+ }
102
116
  // P1-23: 非幂等方法使用更长超时(35s),避免 SDK 10s 超时 < gateway 30s 处理时间
103
117
  const NON_IDEMPOTENT_TIMEOUT = 35;
104
118
  const NON_IDEMPOTENT_METHODS = new Set([
@@ -321,6 +335,7 @@ export class AUNClient {
321
335
  _gatewayUrl = null;
322
336
  _deviceId;
323
337
  _slotId;
338
+ _connectedAt = 0;
324
339
  _connectDeliveryMode;
325
340
  _defaultConnectDeliveryMode;
326
341
  _closing = false;
@@ -354,6 +369,17 @@ export class AUNClient {
354
369
  _groupEpochCleanupTimer = null;
355
370
  _groupEpochRotateTimer = null;
356
371
  _cacheCleanupTimer = null;
372
+ /**
373
+ * 本地 agent.md 内容对应的 etag(quoted sha256 hex,与服务端 _agent_md_etag 一致)。
374
+ *
375
+ * 浏览器无法直接读本地文件,因此 API 不接收 path,而是接收 markdown 字符串:
376
+ * 应用层(如 `<input type=file>`)读出文本后调用 setLocalAgentMdContent() 设置。
377
+ * 用于跟服务端 RPC 注入的 _meta.agent_md_etag 比对,触发"本地未发布到服务端"或
378
+ * "服务端版本更新"的 UI 提示。
379
+ */
380
+ _localAgentMdEtag = '';
381
+ /** gateway 在 RPC envelope._meta.agent_md_etag 注入的服务端 etag;纯观察,无下游依赖。 */
382
+ _remoteAgentMdEtag = '';
357
383
  /** 消息序列号跟踪器(群消息 + P2P 空洞检测) */
358
384
  _seqTracker = new SeqTracker();
359
385
  _seqTrackerContext = null;
@@ -372,8 +398,6 @@ export class AUNClient {
372
398
  _groupEpochRotationRetryTimers = new Map();
373
399
  /** Lazy group sync:首次发送群消息前自动拉取历史 */
374
400
  _groupSynced = new Set();
375
- /** Lazy P2P sync:首次发送 P2P 消息前自动拉取历史 */
376
- _p2pSynced = false;
377
401
  /** gap fill 来源标记:true 表示当前正在补洞(pull 触发),false 表示非补洞 */
378
402
  _gapFillActive = false;
379
403
  // 重连相关
@@ -437,6 +461,7 @@ export class AUNClient {
437
461
  timeout: DEFAULT_SESSION_OPTIONS.timeouts.call,
438
462
  onDisconnect: (error, closeCode) => this._handleTransportDisconnect(error, closeCode),
439
463
  });
464
+ this._transport.setMetaObserver((meta) => this._observeRpcMeta(meta));
440
465
  this._e2ee = new E2EEManager({
441
466
  identityFn: () => this._identity ?? {},
442
467
  deviceIdFn: () => this._deviceId,
@@ -507,6 +532,58 @@ export class AUNClient {
507
532
  get aid() {
508
533
  return this._aid;
509
534
  }
535
+ /**
536
+ * 设置本地 agent.md 内容并一次性计算 etag(quoted sha256,与服务端一致)。
537
+ *
538
+ * 浏览器环境无法直接读本地文件,应用应通过 `<input type=file>` 等方式读出 markdown
539
+ * 文本后调用本方法。content 为空字符串时清除本地 etag。失败不抛异常(debug 日志记录),
540
+ * 应用可读 getLocalAgentMdEtag() 返回值或本方法返回值判断。
541
+ *
542
+ * 返回当前 etag(quoted hex 或空串)。
543
+ */
544
+ async setLocalAgentMdContent(content) {
545
+ const text = String(content ?? '');
546
+ if (text.length === 0) {
547
+ this._localAgentMdEtag = '';
548
+ return '';
549
+ }
550
+ try {
551
+ const data = new TextEncoder().encode(text);
552
+ const buf = await crypto.subtle.digest('SHA-256', data);
553
+ const bytes = new Uint8Array(buf);
554
+ let hex = '';
555
+ for (let i = 0; i < bytes.length; i++) {
556
+ hex += bytes[i].toString(16).padStart(2, '0');
557
+ }
558
+ this._localAgentMdEtag = `"${hex}"`;
559
+ }
560
+ catch (exc) {
561
+ this._clientLog.warn(`setLocalAgentMdContent sha256 failed: ${String(exc)}`);
562
+ this._localAgentMdEtag = '';
563
+ }
564
+ return this._localAgentMdEtag;
565
+ }
566
+ /** 返回 setLocalAgentMdContent 计算的 etag;未设置或计算失败时返回空串。 */
567
+ getLocalAgentMdEtag() {
568
+ return this._localAgentMdEtag;
569
+ }
570
+ /**
571
+ * 返回 gateway 在最近一次 RPC envelope._meta 注入的服务端 agent.md etag。
572
+ *
573
+ * 未收到过则为空串;不阻塞调用,纯内存读。
574
+ */
575
+ getRemoteAgentMdEtag() {
576
+ return this._remoteAgentMdEtag;
577
+ }
578
+ /** transport 的 meta observer:吸收 gateway 注入的 _meta 字段。失败不影响业务。 */
579
+ _observeRpcMeta(meta) {
580
+ if (!isJsonObject(meta))
581
+ return;
582
+ const etag = String(meta.agent_md_etag ?? '').trim();
583
+ if (etag) {
584
+ this._remoteAgentMdEtag = etag;
585
+ }
586
+ }
510
587
  get state() {
511
588
  return this._state;
512
589
  }
@@ -721,6 +798,7 @@ export class AUNClient {
721
798
  return this._sendEncrypted(p);
722
799
  }
723
800
  // encrypt=false:明文走通用 RPC 路径;protected_headers/headers 是信封元数据,加密与否都保留
801
+ this._maybeAppendEchoTraceSend(p);
724
802
  }
725
803
  // 自动加密:group.send 默认加密(encrypt 默认 true)
726
804
  if (method === 'group.send') {
@@ -729,6 +807,7 @@ export class AUNClient {
729
807
  if (encrypt) {
730
808
  return this._sendGroupEncrypted(p);
731
809
  }
810
+ this._maybeAppendEchoTraceSend(p);
732
811
  }
733
812
  if (method === 'group.thought.put') {
734
813
  const encrypt = p.encrypt !== undefined ? p.encrypt : true;
@@ -965,8 +1044,6 @@ export class AUNClient {
965
1044
  }
966
1045
  // P2P 空洞检测
967
1046
  const seq = msg.seq;
968
- // 推送路径收到 P2P 消息 → 标记已同步,后续发送无需再 lazySyncP2p
969
- this._p2pSynced = true;
970
1047
  if (seq !== undefined && seq !== null && this._aid) {
971
1048
  const ns = `p2p:${this._aid}`;
972
1049
  const needPull = this._seqTracker.onMessageSeq(ns, seq);
@@ -1396,8 +1473,61 @@ export class AUNClient {
1396
1473
  return this._attachCurrentInstanceContext(payload);
1397
1474
  }
1398
1475
  async _publishAppEvent(event, payload) {
1476
+ if ((event === 'message.received' || event === 'group.message_created') && isJsonObject(payload)) {
1477
+ this._maybeAppendEchoTraceReceive(payload);
1478
+ }
1479
+ // 注入本地/远端 agent.md etag,让应用层判断版本一致性;失败不影响业务。
1480
+ if (isJsonObject(payload)) {
1481
+ try {
1482
+ const localEtag = this._localAgentMdEtag || '';
1483
+ const remoteEtag = this._remoteAgentMdEtag || '';
1484
+ if ((localEtag || remoteEtag) && payload._agent_md === undefined) {
1485
+ payload._agent_md = {
1486
+ local_etag: localEtag,
1487
+ remote_etag: remoteEtag,
1488
+ };
1489
+ }
1490
+ }
1491
+ catch (exc) {
1492
+ this._clientLog.debug(`agent_md etag inject skipped: ${String(exc)}`);
1493
+ }
1494
+ }
1399
1495
  await this._dispatcher.publish(event, this._normalizePublishedMessagePayload(event, payload));
1400
1496
  }
1497
+ _echoTimestamp() {
1498
+ const now = new Date();
1499
+ const hh = String(now.getHours()).padStart(2, '0');
1500
+ const mm = String(now.getMinutes()).padStart(2, '0');
1501
+ const ss = String(now.getSeconds()).padStart(2, '0');
1502
+ const ms = String(now.getMilliseconds()).padStart(3, '0');
1503
+ return `${hh}:${mm}:${ss}.${ms}`;
1504
+ }
1505
+ _isEchoPayload(payload) {
1506
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
1507
+ return false;
1508
+ const text = payload.text;
1509
+ if (typeof text !== 'string' || text.length > 4096)
1510
+ return false;
1511
+ return text.split('\n', 1)[0].toLowerCase().includes('echo');
1512
+ }
1513
+ _maybeAppendEchoTraceSend(params) {
1514
+ const payload = params.payload;
1515
+ if (!this._isEchoPayload(payload))
1516
+ return;
1517
+ const uptime = this._connectedAt ? Math.floor((Date.now() - this._connectedAt) / 1000) : 0;
1518
+ const trace = `${this._echoTimestamp()} [AUN-SDK.send] aid=${this._aid ?? '-'} conn_uptime=${uptime}s`;
1519
+ params.payload = { ...payload, text: payload.text + '\n' + trace };
1520
+ }
1521
+ _maybeAppendEchoTraceReceive(msg) {
1522
+ if (msg.encrypted)
1523
+ return;
1524
+ const payload = msg.payload;
1525
+ if (!this._isEchoPayload(payload))
1526
+ return;
1527
+ const uptime = this._connectedAt ? Math.floor((Date.now() - this._connectedAt) / 1000) : 0;
1528
+ const trace = `${this._echoTimestamp()} [AUN-SDK.receive] aid=${this._aid ?? '-'} conn_uptime=${uptime}s`;
1529
+ msg.payload = { ...payload, text: payload.text + '\n' + trace };
1530
+ }
1401
1531
  _messageTargetsCurrentInstance(message) {
1402
1532
  if (!isJsonObject(message))
1403
1533
  return true;
@@ -1911,10 +2041,8 @@ export class AUNClient {
1911
2041
  }
1912
2042
  const persistRequired = Boolean(params.persist_required || params.durable);
1913
2043
  const protectedHeaders = this._protectedHeadersFromParams(params);
1914
- // Lazy P2P sync:首次发送前自动拉取历史,避免重连后 seq 空洞
1915
- if (!this._p2pSynced) {
1916
- await this._lazySyncP2p();
1917
- }
2044
+ // 惰性 P2P 同步由 connect/reconnect 完成后的 _fillP2pGap 异步触发,
2045
+ // 不再在 send 路径上 await(与 C++ FillP2PGap 行为对齐,避免阻塞用户发送)。
1918
2046
  // 内部发送逻辑,refreshPeerMaterial=true 时强制清缓存重新拉取对端材料
1919
2047
  const sendAttempt = async (refreshPeerMaterial = false) => {
1920
2048
  const recipientPrekeys = refreshPeerMaterial
@@ -1923,22 +2051,13 @@ export class AUNClient {
1923
2051
  const selfSyncCopies = await this._buildSelfSyncCopies({
1924
2052
  logicalToAid: toAid, payload, messageId, timestamp, protectedHeaders,
1925
2053
  });
1926
- // 多设备过滤:只保留有有效 device_id 的可路由 prekey
1927
- // 占位符 PREKEY_FALLBACK_DEVICE_ID 表示服务端未分配真实设备 ID,不可用于多设备路由
2054
+ // 统一 multi-device 路径:必须有 routable prekey
1928
2055
  const routablePrekeys = recipientPrekeys.filter(pk => {
1929
2056
  const did = String(pk.device_id ?? '').trim();
1930
2057
  return did && did !== PREKEY_FALLBACK_DEVICE_ID;
1931
2058
  });
1932
- // 只要有 routable prekey 就走 multi_device 路径(即使只有 1 个 recipient device + 0 self copies)。
1933
- // 这确保服务端为每个已注册设备存储副本,离线设备重连后能 pull 到。
1934
- // single 路径仅在完全没有 routable prekey 时使用(legacy 兼容)。
1935
- const canUseMultiDevice = routablePrekeys.length > 0;
1936
- if (!canUseMultiDevice) {
1937
- return await this._sendEncryptedSingle({
1938
- toAid, payload, messageId, timestamp,
1939
- prekey: routablePrekeys[0] ?? recipientPrekeys[0],
1940
- persistRequired, protectedHeaders,
1941
- });
2059
+ if (routablePrekeys.length === 0) {
2060
+ throw new Error(`no registered device prekeys for ${toAid}, cannot send encrypted message`);
1942
2061
  }
1943
2062
  const recipientCopies = await this._buildRecipientDeviceCopies({
1944
2063
  toAid, payload, messageId, timestamp,
@@ -1981,63 +2100,6 @@ export class AUNClient {
1981
2100
  throw err;
1982
2101
  }
1983
2102
  }
1984
- /**
1985
- * 首次发送 P2P 消息前懒拉取历史消息,同步 seqTracker 避免空洞。
1986
- * 只在本连接周期内执行一次。
1987
- */
1988
- async _lazySyncP2p() {
1989
- this._p2pSynced = true;
1990
- if (!this._aid)
1991
- return;
1992
- const ns = `p2p:${this._aid}`;
1993
- const afterSeq = this._seqTracker.getContiguousSeq(ns);
1994
- try {
1995
- const result = await this._transport.call('message.pull', {
1996
- after_seq: afterSeq,
1997
- limit: 200,
1998
- });
1999
- if (isJsonObject(result)) {
2000
- const messages = result.messages;
2001
- if (Array.isArray(messages) && messages.length > 0) {
2002
- this._seqTracker.onPullResult(ns, messages.filter(isJsonObject));
2003
- this._saveSeqTrackerState();
2004
- }
2005
- }
2006
- }
2007
- catch (exc) {
2008
- this._clientLog.warn(`lazySyncP2p failed:${String(exc)}`);
2009
- }
2010
- }
2011
- async _sendEncryptedSingle(opts) {
2012
- let prekey = opts.prekey;
2013
- if (prekey === undefined) {
2014
- prekey = await this._fetchPeerPrekey(opts.toAid);
2015
- }
2016
- const peerCertFingerprint = typeof prekey?.cert_fingerprint === 'string' ? prekey.cert_fingerprint : undefined;
2017
- const peerCertPem = await this._fetchPeerCert(opts.toAid, peerCertFingerprint);
2018
- const [envelope, encryptResult] = await this._encryptCopyPayload({
2019
- logicalToAid: opts.toAid,
2020
- payload: opts.payload,
2021
- peerCertPem,
2022
- prekey,
2023
- messageId: opts.messageId,
2024
- timestamp: opts.timestamp,
2025
- protectedHeaders: opts.protectedHeaders,
2026
- });
2027
- await this._ensureEncryptResult(opts.toAid, encryptResult);
2028
- const sendParams = {
2029
- to: opts.toAid,
2030
- payload: envelope,
2031
- type: 'e2ee.encrypted',
2032
- encrypted: true,
2033
- message_id: opts.messageId,
2034
- timestamp: opts.timestamp,
2035
- };
2036
- if (opts.persistRequired) {
2037
- sendParams.persist_required = true;
2038
- }
2039
- return this._transport.call('message.send', sendParams);
2040
- }
2041
2103
  async _buildRecipientDeviceCopies(opts) {
2042
2104
  const recipientCopies = [];
2043
2105
  const certCache = new Map();
@@ -4823,10 +4885,13 @@ export class AUNClient {
4823
4885
  this._sessionParams.access_token = String(auth.token ?? params.access_token ?? '');
4824
4886
  }
4825
4887
  }
4888
+ if (isJsonObject(auth.hello) && 'heartbeat_interval' in auth.hello) {
4889
+ this._applyServerHeartbeatInterval(auth.hello.heartbeat_interval, 'auth');
4890
+ }
4826
4891
  }
4827
4892
  }
4828
4893
  else {
4829
- await this._auth.initializeWithToken(this._transport, challenge, String(params.access_token), {
4894
+ const hello = await this._auth.initializeWithToken(this._transport, challenge, String(params.access_token), {
4830
4895
  deviceId: this._deviceId,
4831
4896
  slotId: this._slotId,
4832
4897
  deliveryMode: this._connectDeliveryMode,
@@ -4835,6 +4900,9 @@ export class AUNClient {
4835
4900
  extraInfo: params.extra_info,
4836
4901
  });
4837
4902
  await this._syncIdentityAfterConnect(String(params.access_token));
4903
+ if (isJsonObject(hello) && 'heartbeat_interval' in hello) {
4904
+ this._applyServerHeartbeatInterval(hello.heartbeat_interval, 'auth');
4905
+ }
4838
4906
  }
4839
4907
  }
4840
4908
  catch (err) {
@@ -4847,11 +4915,11 @@ export class AUNClient {
4847
4915
  throw err;
4848
4916
  }
4849
4917
  this._state = 'connected';
4918
+ this._connectedAt = Date.now();
4850
4919
  await this._dispatcher.publish('connection.state', {
4851
4920
  state: this._state,
4852
4921
  gateway: gatewayUrl,
4853
4922
  });
4854
- // auth 阶段 aid 可能被 identity 覆盖;若 context 发生变化,重新 refresh + restore。
4855
4923
  if (this._seqTrackerContext !== this._currentSeqTrackerContext()) {
4856
4924
  this._refreshSeqTrackerContext();
4857
4925
  await this._restoreSeqTrackerState();
@@ -4976,12 +5044,8 @@ export class AUNClient {
4976
5044
  }
4977
5045
  _buildSessionOptions(params) {
4978
5046
  const connectionKind = String(params.connection_kind ?? 'long');
4979
- // 短连接默认禁用 auto_reconnect:短连接生命周期短,自动重连无意义
4980
- const defaultAutoReconnect = connectionKind === 'short'
4981
- ? false
4982
- : DEFAULT_SESSION_OPTIONS.auto_reconnect;
4983
5047
  const options = {
4984
- auto_reconnect: defaultAutoReconnect,
5048
+ auto_reconnect: DEFAULT_SESSION_OPTIONS.auto_reconnect,
4985
5049
  heartbeat_interval: DEFAULT_SESSION_OPTIONS.heartbeat_interval,
4986
5050
  token_refresh_before: DEFAULT_SESSION_OPTIONS.token_refresh_before,
4987
5051
  retry: { ...DEFAULT_SESSION_OPTIONS.retry },
@@ -5008,12 +5072,12 @@ export class AUNClient {
5008
5072
  }
5009
5073
  // ── 内部:后台任务 ────────────────────────────────
5010
5074
  _startBackgroundTasks() {
5011
- // 短连接生命周期短,禁用心跳与 token 刷新(不接收推送、不需要长期会话维护)
5012
- if (this._sessionOptions?.connection_kind === 'short') {
5013
- return;
5075
+ // 短连接不启动 heartbeat 与 token 刷新(生命周期短,不需要长期会话维护);
5076
+ // auto_reconnect 仍允许,由 _sessionOptions.auto_reconnect 决定
5077
+ if (this._sessionOptions?.connection_kind !== 'short') {
5078
+ this._startHeartbeat();
5079
+ this._startTokenRefresh();
5014
5080
  }
5015
- this._startHeartbeat();
5016
- this._startTokenRefresh();
5017
5081
  this._startPrekeyRefresh();
5018
5082
  this._startGroupEpochTasks();
5019
5083
  }
@@ -5051,10 +5115,9 @@ export class AUNClient {
5051
5115
  _startHeartbeat() {
5052
5116
  if (this._heartbeatTimer !== null)
5053
5117
  return;
5054
- const rawIntervalSeconds = Number(this._sessionOptions.heartbeat_interval ?? DEFAULT_SESSION_OPTIONS.heartbeat_interval);
5055
- if (!Number.isFinite(rawIntervalSeconds) || rawIntervalSeconds <= 0)
5118
+ const interval = clampHeartbeatInterval(this._sessionOptions.heartbeat_interval ?? DEFAULT_SESSION_OPTIONS.heartbeat_interval);
5119
+ if (interval <= 0)
5056
5120
  return;
5057
- const interval = Math.max(rawIntervalSeconds, 30) * 1000;
5058
5121
  // M25: 把连续失败阈值从 3 次收窄到 2 次。既能容忍一次网络抖动/GC 暂停,
5059
5122
  // 又把半开连接的检测延迟从 3 个心跳周期降到 2 个,避免 RPC 长时间挂起。
5060
5123
  // 真正的 socket 死亡由 ws.on('close') 立即触发 _handleTransportDisconnect,
@@ -5065,8 +5128,12 @@ export class AUNClient {
5065
5128
  if (this._state !== 'connected' || this._closing)
5066
5129
  return;
5067
5130
  try {
5068
- await this._transport.call('meta.ping', {});
5131
+ const pong = await this._transport.call('meta.ping', {});
5069
5132
  consecutiveFailures = 0;
5133
+ // 服务端可在 pong 中下发新的 heartbeat_interval(秒,0=关闭)
5134
+ if (isJsonObject(pong) && 'heartbeat_interval' in pong) {
5135
+ this._applyServerHeartbeatInterval(pong.heartbeat_interval, 'pong');
5136
+ }
5070
5137
  }
5071
5138
  catch (exc) {
5072
5139
  consecutiveFailures++;
@@ -5077,7 +5144,24 @@ export class AUNClient {
5077
5144
  this._handleTransportDisconnect(exc instanceof Error ? exc : new Error(String(exc)));
5078
5145
  }
5079
5146
  }
5080
- }, interval);
5147
+ }, interval * 1000);
5148
+ }
5149
+ /** 服务端通过 hello/pong 下发 heartbeat_interval;clamp 后写入 session_options 并按需重启心跳。 */
5150
+ _applyServerHeartbeatInterval(raw, source) {
5151
+ const newInterval = clampHeartbeatInterval(raw);
5152
+ const oldInterval = clampHeartbeatInterval(this._sessionOptions.heartbeat_interval);
5153
+ if (newInterval === oldInterval)
5154
+ return;
5155
+ this._sessionOptions.heartbeat_interval = newInterval;
5156
+ this._clientLog.debug(`heartbeat_interval updated by ${source}: ${oldInterval} -> ${newInterval}`);
5157
+ // 重启定时器以应用新间隔(关闭/启动通过定时器有/无区分)
5158
+ if (this._heartbeatTimer !== null) {
5159
+ clearInterval(this._heartbeatTimer);
5160
+ this._heartbeatTimer = null;
5161
+ }
5162
+ if (newInterval > 0 && this._state === 'connected' && !this._closing) {
5163
+ this._startHeartbeat();
5164
+ }
5081
5165
  }
5082
5166
  /** Token 刷新定时器 */
5083
5167
  _startTokenRefresh() {
@@ -5716,7 +5800,6 @@ export class AUNClient {
5716
5800
  this._pendingOrderedMsgs.clear();
5717
5801
  this._pendingDecryptMsgs.clear();
5718
5802
  this._groupSynced.clear();
5719
- this._p2pSynced = false;
5720
5803
  }
5721
5804
  _refreshSeqTrackerContext() {
5722
5805
  const nextContext = this._currentSeqTrackerContext();
@@ -5728,7 +5811,6 @@ export class AUNClient {
5728
5811
  this._pendingOrderedMsgs.clear();
5729
5812
  this._pendingDecryptMsgs.clear();
5730
5813
  this._groupSynced.clear();
5731
- this._p2pSynced = false;
5732
5814
  this._seqTrackerContext = nextContext;
5733
5815
  }
5734
5816
  /** 将 SeqTracker 状态保存到 keystore */