@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +190 -178
  2. 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
  3. 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
  4. 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
  5. package/_packed_docs/CHANGELOG.md +190 -178
  6. package/_packed_docs/INDEX.md +17 -17
  7. package/_packed_docs/KITE_DOCS_GUIDE.md +11 -11
  8. package/_packed_docs/agent.md/SCHEMA.md +49 -49
  9. package/_packed_docs/agent.md/examples/signed-openclaw-lobster.md +22 -22
  10. 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
  11. package/_packed_docs/cli/AUN-CLI/350/256/276/350/256/241/346/226/207/346/241/243.md +686 -686
  12. package/_packed_docs/design/2026-05-22-aun-rpc-trace-enhancement.md +542 -542
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. package/_packed_docs/protocol/README.md +1 -1
  19. package/_packed_docs/protocol/aun-docs-guide.md +1 -1
  20. package/_packed_docs/protocol//351/231/204/345/275/225A-/346/234/257/350/257/255/350/241/250.md +15 -15
  21. 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
  22. 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
  23. 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
  24. 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
  25. package/_packed_docs/python-sdk-v2-only-changelog.md +189 -189
  26. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
  27. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +1 -1
  28. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +1 -0
  29. package/_packed_docs/sdk/09-payload-reference.md +13 -13
  30. 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
  31. package/dist/aid.d.ts +2 -1
  32. package/dist/aid.d.ts.map +1 -1
  33. package/dist/aid.js +7 -6
  34. package/dist/aid.js.map +1 -1
  35. package/dist/auth.d.ts.map +1 -1
  36. package/dist/auth.js +4 -0
  37. package/dist/auth.js.map +1 -1
  38. package/dist/bundle.js +237 -149
  39. package/dist/client.d.ts +7 -1
  40. package/dist/client.d.ts.map +1 -1
  41. package/dist/client.js +238 -153
  42. package/dist/client.js.map +1 -1
  43. package/dist/version.d.ts +1 -1
  44. package/dist/version.js +1 -1
  45. package/package.json +1 -1
package/dist/client.js CHANGED
@@ -52,6 +52,14 @@ function getV2DeviceId(dev) {
52
52
  }
53
53
  return { present: false, value: '' };
54
54
  }
55
+ function isAIDObject(value) {
56
+ const candidate = value;
57
+ return Boolean(candidate
58
+ && typeof candidate === 'object'
59
+ && typeof candidate.aid === 'string'
60
+ && typeof candidate.aunPath === 'string'
61
+ && typeof candidate.isPrivateKeyValid === 'function');
62
+ }
55
63
  function sortObjectKeys(obj) {
56
64
  if (obj === null || obj === undefined || typeof obj !== 'object')
57
65
  return obj;
@@ -169,6 +177,20 @@ const DEFAULT_SESSION_OPTIONS = {
169
177
  http: 30.0,
170
178
  },
171
179
  };
180
+ const PUBLIC_CONNECTION_OPTION_KEYS = new Set([
181
+ 'auto_reconnect',
182
+ 'connect_timeout',
183
+ 'retry_initial_delay',
184
+ 'retry_max_delay',
185
+ 'retry_max_attempts',
186
+ 'heartbeat_interval',
187
+ 'call_timeout',
188
+ 'connection_kind',
189
+ 'short_ttl_ms',
190
+ 'delivery_mode',
191
+ 'extra_info',
192
+ 'background_sync',
193
+ ]);
172
194
  const PROTECTED_HEADERS_METHODS = new Set([
173
195
  'message.send',
174
196
  'group.send',
@@ -656,6 +678,7 @@ export class AUNClient {
656
678
  // V2 E2EE 状态
657
679
  _v2Session;
658
680
  _v2KeyStore;
681
+ _v2SessionInitInFlight = null;
659
682
  _v2BootstrapCache = new Map();
660
683
  _v2SenderIKPending = new Map();
661
684
  _v2SenderIKFetching = new Set();
@@ -732,10 +755,10 @@ export class AUNClient {
732
755
  _logDiscovery;
733
756
  _logEvents;
734
757
  constructor(aid) {
735
- const inputAid = (aid !== null && aid !== undefined && typeof aid.aunPath === 'string' && typeof aid.isPrivateKeyValid === 'function') ? aid : null;
736
- if (typeof aid === 'string') {
737
- throw new ValidationError('AUNClient aid must be an AID object, not a string');
758
+ if (aid !== null && aid !== undefined && !isAIDObject(aid)) {
759
+ throw new ValidationError('AUNClient only accepts an AID object or no argument');
738
760
  }
761
+ const inputAid = aid ?? null;
739
762
  const rawConfig = {};
740
763
  if (inputAid)
741
764
  rawConfig.aun_path = inputAid.aunPath;
@@ -761,7 +784,7 @@ export class AUNClient {
761
784
  this._clientLog.info(`AUNClient initialized: debug=${_debug} aunPath=${this.configModel.aunPath} aid=${initAid ?? '-'}`);
762
785
  this._dispatcher = new EventDispatcher();
763
786
  this._discovery = new GatewayDiscovery();
764
- this._keystore = new IndexedDBKeyStore({ encryptionSeed: this.configModel.seedPassword ?? undefined });
787
+ this._keystore = new IndexedDBKeyStore({});
765
788
  this._slotId = inputAid?.slotId || 'default';
766
789
  this._connectDeliveryMode = normalizeDeliveryModeConfig({ mode: 'fanout' });
767
790
  this._defaultConnectDeliveryMode = { ...this._connectDeliveryMode };
@@ -790,7 +813,7 @@ export class AUNClient {
790
813
  this._currentAid = inputAid;
791
814
  this._identity = {
792
815
  aid: inputAid.aid,
793
- private_key_pem: inputAid._privateKeyPem ?? '',
816
+ private_key_pem: inputAid.privateKeyPem,
794
817
  public_key_der_b64: inputAid.publicKey,
795
818
  cert: inputAid.certPem,
796
819
  };
@@ -1564,6 +1587,63 @@ export class AUNClient {
1564
1587
  get lastError() { return this._lastError; }
1565
1588
  /** 最近一次错误码(对齐 Python last_error_code) */
1566
1589
  get lastErrorCode() { return this._lastErrorCode; }
1590
+ _applyAidRuntimeContext(aid) {
1591
+ const nextConfig = createConfig({
1592
+ aunPath: aid.aunPath,
1593
+ rootCaPem: aid.rootCaPath,
1594
+ verifySsl: aid.verifySsl,
1595
+ });
1596
+ Object.assign(this.configModel, nextConfig);
1597
+ this.config.aun_path = nextConfig.aunPath;
1598
+ this.config.root_ca_path = nextConfig.rootCaPem;
1599
+ this.config.seed_password = nextConfig.seedPassword;
1600
+ this._agentMdPath = this._agentMdDefaultRoot();
1601
+ this._agentMdCache.clear();
1602
+ this._agentMdFetchInflight.clear();
1603
+ this._peerCache.clear();
1604
+ this._certCache.clear();
1605
+ this._gatewayUrl = null;
1606
+ this._deviceId = aid.deviceId || getDeviceId();
1607
+ this._slotId = aid.slotId || 'default';
1608
+ this._logger = new AUNLogger({ debug: aid.debug, aunPath: nextConfig.aunPath });
1609
+ this._logger.bindDeviceId(this._deviceId);
1610
+ this._clientLog = this._logger.for('aun_core.client');
1611
+ this._logAuth = this._logger.for('aun_core.auth');
1612
+ this._logTransport = this._logger.for('aun_core.transport');
1613
+ this._logKeystore = this._logger.for('aun_core.keystore');
1614
+ this._logDiscovery = this._logger.for('aun_core.discovery');
1615
+ this._logEvents = this._logger.for('aun_core.events');
1616
+ this._discovery = new GatewayDiscovery();
1617
+ this._keystore = new IndexedDBKeyStore({});
1618
+ this._auth = new AuthFlow({
1619
+ keystore: this._keystore,
1620
+ crypto: new CryptoProvider(),
1621
+ aid: aid.aid,
1622
+ deviceId: this._deviceId,
1623
+ slotId: this._slotId,
1624
+ rootCaPem: nextConfig.rootCaPem,
1625
+ verifySsl: nextConfig.verifySsl,
1626
+ });
1627
+ this._transport = new RPCTransport({
1628
+ eventDispatcher: this._dispatcher,
1629
+ timeout: DEFAULT_SESSION_OPTIONS.timeouts.call,
1630
+ onDisconnect: (error, closeCode) => this._handleTransportDisconnect(error, closeCode),
1631
+ });
1632
+ this._transport.setMetaObserver((meta) => {
1633
+ void this._observeRpcMeta(meta).catch((exc) => {
1634
+ this._clientLog.debug(`agent.md meta observer skipped: ${String(exc)}`);
1635
+ });
1636
+ });
1637
+ this._auth.setLogger(this._logAuth);
1638
+ this._transport.setLogger(this._logTransport);
1639
+ this._dispatcher.setLogger(this._logEvents);
1640
+ if (typeof this._discovery.setLogger === 'function') {
1641
+ this._discovery.setLogger(this._logDiscovery);
1642
+ }
1643
+ if (typeof this._keystore.setLogger === 'function') {
1644
+ this._keystore.setLogger(this._logKeystore);
1645
+ }
1646
+ }
1567
1647
  loadIdentity(aid) {
1568
1648
  if (!aid?.isPrivateKeyValid())
1569
1649
  throw new StateError('loadIdentity requires an AID with a valid private key');
@@ -1571,16 +1651,15 @@ export class AUNClient {
1571
1651
  if (publicState !== ConnectionState.NO_IDENTITY && publicState !== ConnectionState.CLOSED) {
1572
1652
  throw new StateError(`loadIdentity not allowed in state ${publicState}`);
1573
1653
  }
1654
+ this._applyAidRuntimeContext(aid);
1574
1655
  this._currentAid = aid;
1575
1656
  this._aid = aid.aid;
1576
1657
  this._identity = {
1577
1658
  aid: aid.aid,
1578
- private_key_pem: aid._privateKeyPem ?? '',
1659
+ private_key_pem: aid.privateKeyPem,
1579
1660
  public_key_der_b64: aid.publicKey,
1580
1661
  cert: aid.certPem,
1581
1662
  };
1582
- this._auth._aid = aid.aid;
1583
- this._slotId = aid.slotId || 'default';
1584
1663
  this._state = 'disconnected';
1585
1664
  this._closing = false;
1586
1665
  }
@@ -1632,9 +1711,6 @@ export class AUNClient {
1632
1711
  get gatewayUrl() {
1633
1712
  return this._gatewayUrl;
1634
1713
  }
1635
- set gatewayUrl(url) {
1636
- this._gatewayUrl = url;
1637
- }
1638
1714
  get discovery() {
1639
1715
  return this._discovery;
1640
1716
  }
@@ -1676,10 +1752,11 @@ export class AUNClient {
1676
1752
  /** 连接到 Gateway;身份来自构造函数或 loadIdentity(aid),认证由 SDK 内部自动完成。 */
1677
1753
  async connect(opts) {
1678
1754
  const tStart = Date.now();
1679
- if (opts !== undefined && typeof opts === 'object') {
1755
+ if (opts !== undefined && opts !== null && typeof opts === 'object') {
1680
1756
  const raw = opts;
1681
- if ('access_token' in raw || 'aid' in raw || 'token' in raw) {
1682
- throw new ValidationError('connect options must not include access_token/aid; these are managed internally');
1757
+ const invalid = Object.keys(raw).filter((key) => !PUBLIC_CONNECTION_OPTION_KEYS.has(key)).sort();
1758
+ if (invalid.length > 0) {
1759
+ throw new ValidationError(`connect options contain unsupported field(s): ${invalid.join(', ')}`);
1683
1760
  }
1684
1761
  }
1685
1762
  const target = this._currentAid?.aid ?? this._aid ?? '';
@@ -1855,6 +1932,14 @@ export class AUNClient {
1855
1932
  }
1856
1933
  this._validateOutboundCall(method, p);
1857
1934
  this._injectMessageCursorContext(method, p);
1935
+ if (method.startsWith('group.')
1936
+ && !('_group_cursor_params' in p)
1937
+ && !Boolean(p._pull_gate_locked)) {
1938
+ const explicitCursorParams = this._groupCursorParams(p);
1939
+ if (Object.keys(explicitCursorParams).length > 0) {
1940
+ p._group_cursor_params = explicitCursorParams;
1941
+ }
1942
+ }
1858
1943
  // group.* 方法的 group_id 归一化为 canonical 格式(兼容老/污染数据)
1859
1944
  if (method.startsWith('group.') && p.group_id !== undefined && p.group_id !== null) {
1860
1945
  const rawGroupId = String(p.group_id);
@@ -1876,9 +1961,7 @@ export class AUNClient {
1876
1961
  const encrypt = p.encrypt !== undefined ? p.encrypt : true;
1877
1962
  delete p.encrypt;
1878
1963
  if (encrypt) {
1879
- if (!this._v2Session) {
1880
- throw new StateError('V2 session not initialized; encrypted message.send requires V2 (V1 E2EE removed)');
1881
- }
1964
+ await this._ensureV2SessionReady('message.send', 'V2 session not initialized; encrypted message.send requires V2 (V1 E2EE removed)');
1882
1965
  this._clientLog.debug('call route: message.send → V2 encrypted send');
1883
1966
  return await this._sendV2(String(p.to ?? ''), p.payload ?? {}, {
1884
1967
  messageId: String(p.message_id ?? '') || undefined,
@@ -1895,9 +1978,7 @@ export class AUNClient {
1895
1978
  const encrypt = p.encrypt !== undefined ? p.encrypt : true;
1896
1979
  delete p.encrypt;
1897
1980
  if (encrypt) {
1898
- if (!this._v2Session) {
1899
- throw new StateError('V2 session not initialized; encrypted group.send requires V2 (V1 E2EE removed)');
1900
- }
1981
+ await this._ensureV2SessionReady('group.send', 'V2 session not initialized; encrypted group.send requires V2 (V1 E2EE removed)');
1901
1982
  this._clientLog.debug('call route: group.send → V2 encrypted send');
1902
1983
  return await this._sendGroupV2(String(p.group_id ?? ''), p.payload ?? {}, {
1903
1984
  messageId: String(p.message_id ?? '') || undefined,
@@ -1912,9 +1993,7 @@ export class AUNClient {
1912
1993
  const encrypt = p.encrypt !== undefined ? p.encrypt : true;
1913
1994
  delete p.encrypt;
1914
1995
  if (encrypt) {
1915
- if (!this._v2Session) {
1916
- throw new StateError('V2 session not initialized; encrypted group.thought.put requires V2 (V1 E2EE removed)');
1917
- }
1996
+ await this._ensureV2SessionReady('group.thought.put', 'V2 session not initialized; encrypted group.thought.put requires V2 (V1 E2EE removed)');
1918
1997
  this._clientLog.debug('call route: group.thought.put → V2 encrypted put');
1919
1998
  return this._putGroupThoughtEncryptedV2(p);
1920
1999
  }
@@ -1923,9 +2002,7 @@ export class AUNClient {
1923
2002
  const encrypt = p.encrypt !== undefined ? p.encrypt : true;
1924
2003
  delete p.encrypt;
1925
2004
  if (encrypt) {
1926
- if (!this._v2Session) {
1927
- throw new StateError('V2 session not initialized; encrypted message.thought.put requires V2 (V1 E2EE removed)');
1928
- }
2005
+ await this._ensureV2SessionReady('message.thought.put', 'V2 session not initialized; encrypted message.thought.put requires V2 (V1 E2EE removed)');
1929
2006
  this._clientLog.debug('call route: message.thought.put → V2 encrypted put');
1930
2007
  return this._putMessageThoughtEncryptedV2(p);
1931
2008
  }
@@ -1944,26 +2021,45 @@ export class AUNClient {
1944
2021
  * 拆分出来以便 pull gate 包裹整个操作。
1945
2022
  */
1946
2023
  async _callImplInner(method, p) {
1947
- // message.pull:V2 就绪时走 V2 pull
1948
- if (method === 'message.pull' && this._v2Session) {
2024
+ // message.pull:V2-only,按需初始化后走 V2 pull
2025
+ if (method === 'message.pull') {
2026
+ await this._ensureV2SessionReady('message.pull');
1949
2027
  this._clientLog.debug('call route: message.pull → V2 pull');
1950
2028
  const messages = await this._pullV2(Number(p.after_seq ?? 0) || 0, Number(p.limit ?? 50) || 50, { force: p.force === true });
1951
2029
  return { messages };
1952
2030
  }
1953
- // message.ack:V2 就绪时走 V2 ack
1954
- if (method === 'message.ack' && this._v2Session) {
2031
+ // message.ack:V2-only,按需初始化后走 V2 ack
2032
+ if (method === 'message.ack') {
2033
+ await this._ensureV2SessionReady('message.ack');
1955
2034
  this._clientLog.debug('call route: message.ack → V2 ack');
1956
2035
  return await this._ackV2(Number(p.seq ?? p.up_to_seq ?? 0) || undefined);
1957
2036
  }
1958
- // group.pull:V2 就绪时走 V2 pull
1959
- if (method === 'group.pull' && this._v2Session && p.group_id) {
2037
+ // group.pull:V2-only,按需初始化后走 V2 pull
2038
+ if (method === 'group.pull' && p.group_id) {
2039
+ await this._ensureV2SessionReady('group.pull');
1960
2040
  this._clientLog.debug('call route: group.pull → V2 pull');
1961
- const messages = await this._pullGroupV2(String(p.group_id), Number(p.after_seq ?? p.after_message_seq ?? 0) || 0, Number(p.limit ?? 50) || 50);
2041
+ const hasExplicitAfterSeq = 'after_seq' in p || 'after_message_seq' in p;
2042
+ const cursorParams = this._explicitGroupCursorParams(p);
2043
+ const ownsCursor = Object.keys(cursorParams).length === 0 || this._groupCursorTargetsCurrentInstance(cursorParams);
2044
+ const pullOpts = {};
2045
+ if (hasExplicitAfterSeq)
2046
+ pullOpts.explicitAfterSeq = true;
2047
+ if (Object.keys(cursorParams).length > 0)
2048
+ pullOpts.cursorParams = cursorParams;
2049
+ if (!ownsCursor)
2050
+ pullOpts.ownsCursor = false;
2051
+ const messages = await this._pullGroupV2(String(p.group_id), Number(p.after_seq ?? p.after_message_seq ?? 0) || 0, Number(p.limit ?? 50) || 50, Object.keys(pullOpts).length > 0 ? pullOpts : undefined);
1962
2052
  return { messages };
1963
2053
  }
1964
- // group.ack_messages:V2 就绪时走 V2 ack
1965
- if (method === 'group.ack_messages' && this._v2Session && p.group_id) {
2054
+ // group.ack_messages:V2-only,按需初始化后走 V2 ack
2055
+ if (method === 'group.ack_messages' && p.group_id) {
2056
+ await this._ensureV2SessionReady('group.ack_messages');
1966
2057
  this._clientLog.debug('call route: group.ack_messages → V2 ack');
2058
+ const cursorParams = this._explicitGroupCursorParams(p);
2059
+ const ownsCursor = Object.keys(cursorParams).length === 0 || this._groupCursorTargetsCurrentInstance(cursorParams);
2060
+ if (!ownsCursor) {
2061
+ return await this._rawGroupAckMessages(p);
2062
+ }
1967
2063
  return await this._ackGroupV2(String(p.group_id), Number(p.seq ?? p.msg_seq ?? p.up_to_seq ?? 0) || undefined);
1968
2064
  }
1969
2065
  // 关键操作自动附加客户端签名
@@ -2070,6 +2166,7 @@ export class AUNClient {
2070
2166
  delete p._pull_gate_locked;
2071
2167
  delete p._skip_auto_ack;
2072
2168
  delete p.skip_auto_ack;
2169
+ delete p._group_cursor_params;
2073
2170
  if (method.startsWith('group.') && p.group_id !== undefined && p.group_id !== null) {
2074
2171
  p.group_id = normalizeGroupId(String(p.group_id)) || String(p.group_id);
2075
2172
  }
@@ -2433,6 +2530,9 @@ export class AUNClient {
2433
2530
  // 状态保护:非 connected 或正在关闭时跳过(与 Python 对齐)
2434
2531
  if (this._state !== 'connected' || this._closing)
2435
2532
  return;
2533
+ groupId = normalizeGroupId(groupId) || String(groupId ?? '').trim();
2534
+ if (!groupId)
2535
+ return;
2436
2536
  const ns = `group:${groupId}`;
2437
2537
  const afterSeq = this._seqTracker.getContiguousSeq(ns);
2438
2538
  // per-namespace 去重:同一 group namespace 只允许 1 个 in-flight pull
@@ -2441,45 +2541,11 @@ export class AUNClient {
2441
2541
  return;
2442
2542
  this._gapFillDone.add(dedupKey);
2443
2543
  this._gapFillActive = true;
2544
+ let filled = 0;
2444
2545
  try {
2445
- const result = await this.call('group.pull', {
2446
- group_id: groupId,
2447
- after_message_seq: afterSeq,
2448
- device_id: this._deviceId,
2449
- limit: 50,
2450
- });
2451
- if (isJsonObject(result)) {
2452
- const messages = result.messages;
2453
- if (Array.isArray(messages)) {
2454
- // ⚠️ 不再重复调用 onPullResult:call('group.pull') 拦截器已在内部调用过一次
2455
- const pushed = this._pushedSeqs.get(ns);
2456
- for (const msg of messages) {
2457
- if (isJsonObject(msg)) {
2458
- const s = msg.seq;
2459
- if (pushed && s !== undefined && s !== null && pushed.has(s))
2460
- continue;
2461
- if (s !== undefined && s !== null) {
2462
- await this._publishPulledMessage('group.message_created', ns, s, msg);
2463
- }
2464
- else {
2465
- await this._publishAppEvent('group.message_created', msg);
2466
- }
2467
- }
2468
- }
2469
- this._prunePushedSeqs(ns);
2470
- // publish 完成后 auto-ack
2471
- const contig = this._seqTracker.getContiguousSeq(ns);
2472
- if (contig > 0) {
2473
- const gid = groupId;
2474
- this._transport.call('group.ack_messages', {
2475
- group_id: gid,
2476
- msg_seq: contig,
2477
- device_id: this._deviceId,
2478
- slot_id: this._slotId,
2479
- }).catch((e) => { this._clientLog.warn(`group gap-fill auto-ack failed: group=${gid}`, e); });
2480
- }
2481
- }
2482
- }
2546
+ const messages = await this._pullGroupV2(groupId, afterSeq, 50);
2547
+ filled = messages.length;
2548
+ this._prunePushedSeqs(ns);
2483
2549
  }
2484
2550
  catch (exc) {
2485
2551
  this._clientLog.warn(`group message gap-fill failed:${String(exc)}`);
@@ -2488,6 +2554,9 @@ export class AUNClient {
2488
2554
  // S1: 成功 / 失败路径都必须清理飞行标记
2489
2555
  this._gapFillDone.delete(dedupKey);
2490
2556
  this._gapFillActive = false;
2557
+ if (filled > 0 && this._seqTracker.getContiguousSeq(ns) > afterSeq) {
2558
+ this._safeAsync(this._fillGroupGap(groupId));
2559
+ }
2491
2560
  }
2492
2561
  }
2493
2562
  /** 后台补齐群事件空洞 */
@@ -2604,42 +2673,11 @@ export class AUNClient {
2604
2673
  return;
2605
2674
  this._gapFillDone.add(dedupKey);
2606
2675
  this._gapFillActive = true;
2676
+ let filled = 0;
2607
2677
  try {
2608
- const result = await this.call('message.pull', {
2609
- after_seq: afterSeq,
2610
- limit: 50,
2611
- });
2612
- if (isJsonObject(result)) {
2613
- const messages = result.messages;
2614
- if (Array.isArray(messages)) {
2615
- // ⚠️ 不再重复调用 onPullResult:call('message.pull') 拦截器已在内部调用过一次
2616
- // 与 _fillGroupGap 路径对齐,避免双重 tracker 推进。
2617
- const pushed = this._pushedSeqs.get(ns);
2618
- for (const msg of messages) {
2619
- if (isJsonObject(msg)) {
2620
- const s = msg.seq;
2621
- if (pushed && s !== undefined && s !== null && pushed.has(s))
2622
- continue;
2623
- if (s !== undefined && s !== null) {
2624
- await this._publishPulledMessage('message.received', ns, s, msg);
2625
- }
2626
- else {
2627
- await this._publishAppEvent('message.received', msg);
2628
- }
2629
- }
2630
- }
2631
- this._prunePushedSeqs(ns);
2632
- // publish 完成后 auto-ack
2633
- const contig = this._seqTracker.getContiguousSeq(ns);
2634
- if (contig > 0) {
2635
- this._transport.call('message.ack', {
2636
- seq: contig,
2637
- device_id: this._deviceId,
2638
- slot_id: this._slotId,
2639
- }).catch((e) => { this._clientLog.warn(`P2P gap-fill auto-ack failed:${String(e)}`); });
2640
- }
2641
- }
2642
- }
2678
+ const messages = await this._pullV2(afterSeq, 50);
2679
+ filled = messages.length;
2680
+ this._prunePushedSeqs(ns);
2643
2681
  }
2644
2682
  catch (exc) {
2645
2683
  this._clientLog.warn(`P2P message gap-fill failed:${String(exc)}`);
@@ -2648,6 +2686,9 @@ export class AUNClient {
2648
2686
  // S1: 成功 / 失败路径都必须清理飞行标记
2649
2687
  this._gapFillDone.delete(dedupKey);
2650
2688
  this._gapFillActive = false;
2689
+ if (filled > 0 && this._seqTracker.getContiguousSeq(ns) > afterSeq) {
2690
+ this._safeAsync(this._fillP2pGap());
2691
+ }
2651
2692
  }
2652
2693
  }
2653
2694
  /** 只按硬上限裁剪 published guard,不能按 contiguousSeq 清理。 */
@@ -3582,11 +3623,11 @@ export class AUNClient {
3582
3623
  * 使用 SubtleCrypto 异步签名。
3583
3624
  */
3584
3625
  async _signClientOperation(method, params) {
3585
- const identity = this._identity;
3586
- if (!identity || !identity.private_key_pem)
3626
+ const currentAid = this._currentAid;
3627
+ if (!currentAid?.privateKeyPem)
3587
3628
  return;
3588
3629
  try {
3589
- const aid = (identity.aid ?? '');
3630
+ const aid = currentAid.aid;
3590
3631
  const ts = String(Math.floor(Date.now() / 1000));
3591
3632
  // 计算 params hash:覆盖所有非 _ 前缀且非 client_signature 的业务字段
3592
3633
  const paramsForHash = {};
@@ -3603,14 +3644,14 @@ export class AUNClient {
3603
3644
  .join('');
3604
3645
  const signData = new TextEncoder().encode(`${method}|${aid}|${ts}|${paramsHash}`);
3605
3646
  // 导入私钥并签名
3606
- const pkcs8 = pemToArrayBuffer(identity.private_key_pem);
3647
+ const pkcs8 = pemToArrayBuffer(currentAid.privateKeyPem);
3607
3648
  const cryptoKey = await crypto.subtle.importKey('pkcs8', pkcs8, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']);
3608
3649
  const sigP1363 = await crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, cryptoKey, signData);
3609
3650
  // P1363 → DER 格式(与 Python 兼容)
3610
3651
  const sigDer = p1363ToDer(new Uint8Array(sigP1363));
3611
3652
  // 证书指纹
3612
3653
  let certFingerprint = '';
3613
- const certPem = (identity.cert ?? '');
3654
+ const certPem = currentAid.certPem;
3614
3655
  if (certPem) {
3615
3656
  const certDer = pemToArrayBuffer(certPem);
3616
3657
  const fpBuf = await crypto.subtle.digest('SHA-256', certDer);
@@ -3706,15 +3747,25 @@ export class AUNClient {
3706
3747
  await this._restoreSeqTrackerState();
3707
3748
  }
3708
3749
  this._startBackgroundTasks();
3709
- // V2 E2EE: 初始化 session 并注册设备 SPK(与 Python `_init_v2_session` 对齐)
3710
- try {
3711
- await this._initV2Session();
3750
+ const connectionKind = String(params.connection_kind ?? 'long');
3751
+ const isShortConnection = connectionKind === 'short';
3752
+ if (!isShortConnection) {
3753
+ // V2 E2EE: 长连接上线时初始化 session 并注册设备 SPK(与 Python `_init_v2_session` 对齐)
3754
+ try {
3755
+ await this._initV2Session();
3756
+ }
3757
+ catch (exc) {
3758
+ this._clientLog.warn(`V2 session init failed (non-fatal): ${String(exc)}`);
3759
+ }
3712
3760
  }
3713
- catch (exc) {
3714
- this._clientLog.warn(`V2 session init failed (non-fatal): ${String(exc)}`);
3761
+ else {
3762
+ this._clientLog.debug('V2 session init deferred for short connection');
3715
3763
  }
3716
3764
  // connect/reconnect 成功后自动触发一次 P2P message.pull,补齐离线期间积压
3717
- if (this._sessionOptions?.background_sync !== false) {
3765
+ const hasExplicitBackgroundSync = Object.prototype.hasOwnProperty.call(params, 'background_sync');
3766
+ const backgroundSyncEnabled = this._sessionOptions?.background_sync !== false
3767
+ && (!isShortConnection || hasExplicitBackgroundSync);
3768
+ if (backgroundSyncEnabled) {
3718
3769
  this._safeAsync(this._fillP2pGap());
3719
3770
  }
3720
3771
  this._clientLog.debug(`_connectOnce exit: elapsed=${Date.now() - tStart}ms aid=${this._aid ?? '-'}`);
@@ -4660,6 +4711,20 @@ export class AUNClient {
4660
4711
  this._clientLog.warn(`${label} push repaired contiguous_seq: ns=${ns} payload=${hasPayload} push_seq=${pushSeq} contiguous=${contig}->${repaired}`);
4661
4712
  return repaired;
4662
4713
  }
4714
+ async _ensureV2SessionReady(method, errorMessage) {
4715
+ if (!this._v2Session) {
4716
+ if (!this._v2SessionInitInFlight) {
4717
+ this._v2SessionInitInFlight = this._initV2Session()
4718
+ .finally(() => {
4719
+ this._v2SessionInitInFlight = null;
4720
+ });
4721
+ }
4722
+ await this._v2SessionInitInFlight;
4723
+ }
4724
+ if (!this._v2Session) {
4725
+ throw new StateError(errorMessage ?? `V2 session not initialized; encrypted ${method} requires E2EE V2`);
4726
+ }
4727
+ }
4663
4728
  // ── V2 E2EE API(async,与 Python `client.py` `_init_v2_session` / `send_v2` / `pull_v2` / `ack_v2` 对齐) ──
4664
4729
  /**
4665
4730
  * 初始化 V2 session:从 AID PEM 私钥提取 raw scalar + DER 公钥,
@@ -4670,35 +4735,16 @@ export class AUNClient {
4670
4735
  async _initV2Session() {
4671
4736
  if (!this._aid)
4672
4737
  return;
4673
- let identity = this._identity;
4674
- if (!identity?.private_key_pem) {
4675
- // fallback:缓存的 identity 可能被 instance_state 污染,重新从 keystore 加载
4676
- try {
4677
- const reloaded = await this._keystore.loadIdentity(this._aid);
4678
- if (reloaded?.private_key_pem) {
4679
- this._identity = reloaded;
4680
- identity = reloaded;
4681
- this._clientLog.warn('V2 session init: identity cache was stale, reloaded from keystore');
4682
- // 自愈:重新持久化,清理 instance_state 中的脏数据
4683
- try {
4684
- const persistIdentity = this._auth._persistIdentity;
4685
- if (typeof persistIdentity === 'function') {
4686
- await persistIdentity.call(this._auth, reloaded);
4687
- }
4688
- }
4689
- catch { /* best-effort */ }
4690
- }
4691
- }
4692
- catch { /* ignore */ }
4693
- }
4694
- if (!identity?.private_key_pem) {
4738
+ // 私钥由 AIDStore 管理,直接从 _currentAid 读取明文私钥
4739
+ const currentAid = this._currentAid;
4740
+ if (!currentAid?.privateKeyPem) {
4695
4741
  this._clientLog.warn('V2 session init skipped: no AID private key');
4696
4742
  return;
4697
4743
  }
4698
4744
  if (this._v2Session)
4699
4745
  return;
4700
4746
  // 1. PEM → DER PKCS8(去掉 header/footer 后 base64 解码)
4701
- const pem = String(identity.private_key_pem).trim();
4747
+ const pem = currentAid.privateKeyPem.trim();
4702
4748
  const pemBody = pem
4703
4749
  .replace(/-----BEGIN [^-]+-----/g, '')
4704
4750
  .replace(/-----END [^-]+-----/g, '')
@@ -5096,6 +5142,7 @@ export class AUNClient {
5096
5142
  decrypted.push(plaintext);
5097
5143
  }
5098
5144
  }
5145
+ const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
5099
5146
  const serverAckSeq = Number(result.server_ack_seq ?? 0);
5100
5147
  if (ns && Number.isFinite(serverAckSeq) && serverAckSeq > 0) {
5101
5148
  const contig = this._seqTracker.getContiguousSeq(ns);
@@ -5111,7 +5158,10 @@ export class AUNClient {
5111
5158
  await this._drainOrderedMessages(ns);
5112
5159
  this._saveSeqTrackerState();
5113
5160
  }
5114
- if (messages.length > 0 && contigAdvanced && ackSeq > 0) {
5161
+ const ackNeeded = messages.length > 0
5162
+ && ackSeq > 0
5163
+ && (contigAdvanced || (hasServerAckSeq && ackSeq > serverAckSeq));
5164
+ if (ackNeeded) {
5115
5165
  this._safeAsync(this._ackV2(ackSeq).then(() => undefined));
5116
5166
  }
5117
5167
  }
@@ -5143,7 +5193,7 @@ export class AUNClient {
5143
5193
  seq = maxSeen;
5144
5194
  }
5145
5195
  }
5146
- const raw = await this.call('message.v2.ack', { up_to_seq: seq });
5196
+ const raw = await this._callRawV2Rpc('message.v2.ack', { up_to_seq: seq });
5147
5197
  const result = isJsonObject(raw)
5148
5198
  ? { ...raw }
5149
5199
  : { result: raw };
@@ -5435,7 +5485,7 @@ export class AUNClient {
5435
5485
  * @param afterSeq 从此 seq 之后开始拉取(0/省略 = 从当前 contiguous 开始)
5436
5486
  * @param limit 最多拉取条数
5437
5487
  */
5438
- async _pullGroupV2(groupId, afterSeq = 0, limit = 50) {
5488
+ async _pullGroupV2(groupId, afterSeq = 0, limit = 50, opts) {
5439
5489
  if (!this._v2Session) {
5440
5490
  throw new StateError('V2 session not initialized (not connected?)');
5441
5491
  }
@@ -5444,15 +5494,18 @@ export class AUNClient {
5444
5494
  throw new ValidationError('group.pull requires group_id');
5445
5495
  const ns = `group:${gid}`;
5446
5496
  const decrypted = [];
5447
- let nextAfterSeq = afterSeq || this._seqTracker.getContiguousSeq(ns);
5497
+ const cursorParams = opts?.cursorParams ?? {};
5498
+ const ownsCursor = opts?.ownsCursor !== false;
5499
+ let nextAfterSeq = opts?.explicitAfterSeq ? afterSeq : (afterSeq || this._seqTracker.getContiguousSeq(ns));
5448
5500
  let pageCount = 0;
5449
5501
  const maxPages = 100;
5450
5502
  while (pageCount < maxPages) {
5451
5503
  pageCount += 1;
5452
- const result = await this.call('group.v2.pull', {
5504
+ const result = await this._callRawV2Rpc('group.v2.pull', {
5453
5505
  group_id: gid,
5454
5506
  after_seq: nextAfterSeq,
5455
5507
  limit,
5508
+ ...cursorParams,
5456
5509
  });
5457
5510
  const messages = (Array.isArray(result?.messages) ? result.messages : []);
5458
5511
  const seqs = messages
@@ -5519,6 +5572,7 @@ export class AUNClient {
5519
5572
  decrypted.push(plaintext);
5520
5573
  }
5521
5574
  const cursor = isJsonObject(result.cursor) ? result.cursor : null;
5575
+ const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
5522
5576
  const serverAckSeq = Number(cursor?.current_seq ?? 0);
5523
5577
  if (Number.isFinite(serverAckSeq) && serverAckSeq > 0) {
5524
5578
  const contig = this._seqTracker.getContiguousSeq(ns);
@@ -5533,10 +5587,16 @@ export class AUNClient {
5533
5587
  await this._drainOrderedMessages(ns);
5534
5588
  this._saveSeqTrackerState();
5535
5589
  }
5536
- if (messages.length > 0 && contigAdvanced && ackSeq > 0) {
5590
+ const ackNeeded = messages.length > 0
5591
+ && ackSeq > 0
5592
+ && ownsCursor
5593
+ && (contigAdvanced || (hasServerCursor && ackSeq > serverAckSeq));
5594
+ if (ackNeeded) {
5537
5595
  this._safeAsync(this._ackGroupV2(gid, ackSeq).then(() => undefined));
5538
5596
  }
5539
5597
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
5598
+ if (!ownsCursor)
5599
+ break;
5540
5600
  if (messages.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
5541
5601
  break;
5542
5602
  nextAfterSeq = nextAfter;
@@ -5546,6 +5606,31 @@ export class AUNClient {
5546
5606
  }
5547
5607
  return decrypted;
5548
5608
  }
5609
+ _groupCursorParams(params) {
5610
+ const cursorParams = {};
5611
+ for (const key of ['device_id', 'slot_id', 'device_name', 'device_type']) {
5612
+ const value = params[key];
5613
+ if (value !== undefined && value !== null)
5614
+ cursorParams[key] = value;
5615
+ }
5616
+ return cursorParams;
5617
+ }
5618
+ _explicitGroupCursorParams(params) {
5619
+ const value = params._group_cursor_params;
5620
+ if (!isJsonObject(value))
5621
+ return {};
5622
+ return { ...value };
5623
+ }
5624
+ _groupCursorTargetsCurrentInstance(params) {
5625
+ const deviceId = String(params.device_id ?? '').trim();
5626
+ const slotId = String(params.slot_id ?? '').trim();
5627
+ return (!deviceId || deviceId === (this._deviceId ?? ''))
5628
+ && (!slotId || slotId === (this._slotId ?? ''));
5629
+ }
5630
+ async _rawGroupAckMessages(params) {
5631
+ const p = { ...params };
5632
+ return await this._callRawV2Rpc('group.ack_messages', p);
5633
+ }
5549
5634
  /**
5550
5635
  * 确认 V2 群消息已消费。
5551
5636
  *
@@ -5566,7 +5651,7 @@ export class AUNClient {
5566
5651
  this._clientLog.warn(`ackGroupV2 clamp: group=${gid} up_to_seq=${seq} > max_seen=${maxSeen}, clamp`);
5567
5652
  seq = maxSeen;
5568
5653
  }
5569
- return this.call('group.v2.ack', { group_id: gid, up_to_seq: seq });
5654
+ return this._callRawV2Rpc('group.v2.ack', { group_id: gid, up_to_seq: seq });
5570
5655
  }
5571
5656
  // ── V2 thought(per-device wrap,服务端透传,不持久化)──────────
5572
5657
  /**
@@ -6387,8 +6472,8 @@ export class AUNClient {
6387
6472
  return;
6388
6473
  }
6389
6474
  let signature = '';
6390
- const identity = this._identity;
6391
- if (identity?.private_key_pem) {
6475
+ const currentAid = this._currentAid;
6476
+ if (currentAid?.privateKeyPem) {
6392
6477
  try {
6393
6478
  const signPayloadObj = {
6394
6479
  group_id: groupId,
@@ -6398,7 +6483,7 @@ export class AUNClient {
6398
6483
  };
6399
6484
  const signPayload = stableStringify(signPayloadObj);
6400
6485
  const signPayloadBytes = new TextEncoder().encode(signPayload);
6401
- const privKey = await importPrivateKeyEcdsa(identity.private_key_pem);
6486
+ const privKey = await importPrivateKeyEcdsa(currentAid.privateKeyPem);
6402
6487
  const sigBytes = await ecdsaSignDer(privKey, signPayloadBytes);
6403
6488
  signature = uint8ToBase64(sigBytes);
6404
6489
  }
@@ -6594,7 +6679,7 @@ export class AUNClient {
6594
6679
  if (newContig > 0 && newContig !== contigBefore) {
6595
6680
  const maxSeen = this._seqTracker.getMaxSeenSeq(ns);
6596
6681
  const ackSeq = maxSeen > 0 ? Math.min(newContig, maxSeen) : newContig;
6597
- this.call('message.v2.ack', { up_to_seq: ackSeq })
6682
+ this._callRawV2Rpc('message.v2.ack', { up_to_seq: ackSeq })
6598
6683
  .catch(e => this._clientLog.debug(`V2 P2P push-ack failed: ${e}`));
6599
6684
  }
6600
6685
  this._clientLog.debug(`_onV2PushNotification: push 带 payload 解密成功, contiguous_seq=${contigBefore}->${newContig} push_seq=${pushSeq}`);