@agentunion/fastaun 0.4.2 → 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 (63) hide show
  1. package/CHANGELOG.md +198 -173
  2. package/_packed_docs/0.4.0_/345/267/256/345/274/202/346/240/270/345/256/236/345/206/263/347/255/226/350/256/260/345/275/225.md +302 -0
  3. 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 -0
  4. 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
  5. 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
  6. package/_packed_docs/CHANGELOG.md +198 -173
  7. package/_packed_docs/INDEX.md +17 -17
  8. package/_packed_docs/KITE_DOCS_GUIDE.md +11 -11
  9. package/_packed_docs/agent.md/SCHEMA.md +49 -49
  10. package/_packed_docs/agent.md/examples/signed-openclaw-lobster.md +22 -22
  11. 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
  12. package/_packed_docs/cli/AUN-CLI/350/256/276/350/256/241/346/226/207/346/241/243.md +686 -686
  13. package/_packed_docs/design/2026-05-22-aun-rpc-trace-enhancement.md +542 -542
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. package/_packed_docs/protocol/README.md +1 -1
  20. package/_packed_docs/protocol/aun-docs-guide.md +1 -1
  21. package/_packed_docs/protocol//351/231/204/345/275/225A-/346/234/257/350/257/255/350/241/250.md +15 -15
  22. 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
  23. 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
  24. 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
  25. 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
  26. package/_packed_docs/python-sdk-v2-only-changelog.md +189 -189
  27. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +7 -3
  28. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +1 -1
  29. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +3 -1
  30. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +1 -1
  31. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +63 -15
  32. package/_packed_docs/sdk/09-payload-reference.md +13 -13
  33. 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
  34. package/_packed_docs/sdk/README.md +5 -5
  35. package/dist/aid-store.js +1 -5
  36. package/dist/aid-store.js.map +1 -1
  37. package/dist/aid.d.ts +2 -1
  38. package/dist/aid.js +7 -6
  39. package/dist/aid.js.map +1 -1
  40. package/dist/auth.js +4 -0
  41. package/dist/auth.js.map +1 -1
  42. package/dist/client.d.ts +13 -16
  43. package/dist/client.js +242 -91
  44. package/dist/client.js.map +1 -1
  45. package/dist/config.d.ts +3 -0
  46. package/dist/config.js +17 -2
  47. package/dist/config.js.map +1 -1
  48. package/dist/index.d.ts +1 -1
  49. package/dist/index.js.map +1 -1
  50. package/dist/keystore/aid-db.js +103 -90
  51. package/dist/keystore/aid-db.js.map +1 -1
  52. package/dist/keystore/file.d.ts +0 -2
  53. package/dist/keystore/file.js +6 -44
  54. package/dist/keystore/file.js.map +1 -1
  55. package/dist/tools/cross-sdk-agent.js +0 -9
  56. package/dist/tools/cross-sdk-agent.js.map +1 -1
  57. package/dist/transport.d.ts +1 -0
  58. package/dist/transport.js +7 -1
  59. package/dist/transport.js.map +1 -1
  60. package/dist/v2/session/keystore.js +2 -2
  61. package/dist/version.d.ts +1 -1
  62. package/dist/version.js +1 -1
  63. package/package.json +1 -1
package/dist/client.js CHANGED
@@ -16,7 +16,7 @@ import * as http from 'node:http';
16
16
  import * as https from 'node:https';
17
17
  import * as path from 'node:path';
18
18
  import { URL } from 'node:url';
19
- import { configFromMap, getDeviceId, normalizeInstanceId } from './config.js';
19
+ import { configFromMap, getDeviceId, normalizeInstanceId, normalizeSlotId, slotIsolationKey } from './config.js';
20
20
  import { CryptoProvider } from './crypto.js';
21
21
  import { GatewayDiscovery } from './discovery.js';
22
22
  import { DnsResilientNet } from './net.js';
@@ -37,6 +37,14 @@ import { AID } from './aid.js';
37
37
  function isPromiseLike(value) {
38
38
  return Boolean(value && typeof value.then === 'function');
39
39
  }
40
+ function isAIDObject(value) {
41
+ const candidate = value;
42
+ return Boolean(candidate
43
+ && typeof candidate === 'object'
44
+ && typeof candidate.aid === 'string'
45
+ && typeof candidate.aunPath === 'string'
46
+ && typeof candidate.isPrivateKeyValid === 'function');
47
+ }
40
48
  /**
41
49
  * 递归排序键的 JSON 序列化(Canonical JSON for AUN)
42
50
  * 等价于 Python json.dumps(sort_keys=True, separators=(",",":"), ensure_ascii=False)
@@ -128,6 +136,20 @@ const DEFAULT_SESSION_OPTIONS = {
128
136
  http: 30.0,
129
137
  },
130
138
  };
139
+ const PUBLIC_CONNECTION_OPTION_KEYS = new Set([
140
+ 'auto_reconnect',
141
+ 'connect_timeout',
142
+ 'retry_initial_delay',
143
+ 'retry_max_delay',
144
+ 'retry_max_attempts',
145
+ 'heartbeat_interval',
146
+ 'call_timeout',
147
+ 'connection_kind',
148
+ 'short_ttl_ms',
149
+ 'delivery_mode',
150
+ 'extra_info',
151
+ 'background_sync',
152
+ ]);
131
153
  const PROTECTED_HEADERS_METHODS = new Set([
132
154
  'message.send',
133
155
  'group.send',
@@ -410,22 +432,6 @@ async function fetchWithTimeout(input, init, timeoutMs = AGENT_MD_HTTP_TIMEOUT_M
410
432
  clearTimeout(timer);
411
433
  }
412
434
  }
413
- function assertClientOptions(value, label) {
414
- if (value == null)
415
- return;
416
- if (typeof value !== 'object' || Array.isArray(value) || value instanceof AID) {
417
- throw new ValidationError(`${label} must be an options object`);
418
- }
419
- }
420
- function clientOptionsConfig(options) {
421
- const raw = { ...(options ?? {}) };
422
- if (Object.prototype.hasOwnProperty.call(raw, 'aid')) {
423
- throw new ValidationError('AUNClient options must not include aid; pass an AID object as the first argument');
424
- }
425
- delete raw.debug;
426
- delete raw.protected_headers;
427
- return raw;
428
- }
429
435
  export class AUNClient {
430
436
  /** 原始配置 */
431
437
  config;
@@ -514,6 +520,7 @@ export class AUNClient {
514
520
  // ── V2 E2EE 状态 ──────────────────────────────────────────────
515
521
  _v2Session;
516
522
  _v2KeyStore;
523
+ _v2SessionInitInFlight = null;
517
524
  /** V2 bootstrap 缓存:aid/group:id → 设备列表 + 时间戳 */
518
525
  _v2BootstrapCache = new Map();
519
526
  _connectCapabilities = null;
@@ -543,12 +550,11 @@ export class AUNClient {
543
550
  _logger;
544
551
  _clientLog;
545
552
  constructor(aid) {
546
- if (typeof aid === 'string') {
547
- throw new ValidationError('AUNClient aid must be an AID object, not a string');
553
+ if (aid !== null && aid !== undefined && !isAIDObject(aid)) {
554
+ throw new ValidationError('AUNClient only accepts an AID object or no argument');
548
555
  }
549
- const inputAid = aid instanceof AID ? aid : null;
550
- const options = {};
551
- const rawConfig = clientOptionsConfig(options);
556
+ const inputAid = aid ?? null;
557
+ const rawConfig = {};
552
558
  if (inputAid) {
553
559
  rawConfig.aun_path = inputAid.aunPath;
554
560
  rawConfig.verify_ssl = inputAid.verifySsl;
@@ -557,7 +563,7 @@ export class AUNClient {
557
563
  rawConfig.debug = inputAid.debug;
558
564
  }
559
565
  this._configModel = configFromMap(rawConfig);
560
- const initAid = inputAid ? inputAid.aid : null;
566
+ const initAid = (inputAid && inputAid.isPrivateKeyValid()) ? inputAid.aid : null;
561
567
  this._agentMdPath = path.join(this._configModel.aunPath, 'AIDs');
562
568
  this.config = {
563
569
  aun_path: this._configModel.aunPath,
@@ -583,7 +589,6 @@ export class AUNClient {
583
589
  });
584
590
  this._discovery = new GatewayDiscovery({ verifySsl: this._configModel.verifySsl, logger: this._clientLog, net: dnsNet });
585
591
  const keystore = new FileKeyStore(this._configModel.aunPath, {
586
- encryptionSeed: this._configModel.seedPassword ?? undefined,
587
592
  logger: this._logger.for('aun_core.keystore'),
588
593
  secretStoreLogger: this._logger.for('aun_core.secret-store'),
589
594
  });
@@ -626,17 +631,17 @@ export class AUNClient {
626
631
  });
627
632
  this._transport.setMetaObserver((meta) => this._observeRpcMeta(meta));
628
633
  if (inputAid) {
629
- if (!inputAid.isPrivateKeyValid()) {
630
- throw new StateError('AUNClient requires an AID with a valid private key');
631
- }
632
- this._currentAid = inputAid;
633
- this._identity = {
634
- aid: inputAid.aid,
635
- private_key_pem: inputAid._privateKeyPem ?? '',
636
- public_key_der_b64: inputAid.publicKey,
637
- cert: inputAid.certPem,
638
- };
639
- this._state = 'standby';
634
+ // Python 对齐:私钥无效时只用 aunPath 配置,state 保持 no_identity
635
+ if (inputAid.isPrivateKeyValid()) {
636
+ this._currentAid = inputAid;
637
+ this._identity = {
638
+ aid: inputAid.aid,
639
+ private_key_pem: inputAid.privateKeyPem,
640
+ public_key_der_b64: inputAid.publicKey,
641
+ cert: inputAid.certPem,
642
+ };
643
+ this._state = 'standby';
644
+ }
640
645
  }
641
646
  // 内部订阅:推送消息自动解密后 re-publish 给用户
642
647
  this._dispatcher.subscribe('_raw.message.received', (data) => this._onRawMessageReceived(data));
@@ -714,6 +719,71 @@ export class AUNClient {
714
719
  get lastErrorCode() {
715
720
  return this._lastErrorCode;
716
721
  }
722
+ _applyAidRuntimeContext(aid) {
723
+ const rawConfig = {
724
+ aun_path: aid.aunPath,
725
+ verify_ssl: aid.verifySsl,
726
+ debug: aid.debug,
727
+ };
728
+ if (aid.rootCaPath)
729
+ rawConfig.root_ca_path = aid.rootCaPath;
730
+ const nextConfig = configFromMap(rawConfig);
731
+ try {
732
+ const close = this._keystore.close;
733
+ if (typeof close === 'function')
734
+ close.call(this._keystore);
735
+ }
736
+ catch {
737
+ // best-effort cleanup before switching keystore roots
738
+ }
739
+ this._configModel = nextConfig;
740
+ this.config.aun_path = nextConfig.aunPath;
741
+ this.config.root_ca_path = nextConfig.rootCaPath;
742
+ this.config.seed_password = nextConfig.seedPassword;
743
+ this._agentMdPath = path.join(nextConfig.aunPath, 'AIDs');
744
+ this._agentMdCache.clear();
745
+ this._agentMdFetchInflight.clear();
746
+ this._agentMdDownloadInflight.clear();
747
+ this._peerCache.clear();
748
+ this._certCache.clear();
749
+ this._gatewayUrl = null;
750
+ this._deviceId = aid.deviceId || getDeviceId(nextConfig.aunPath);
751
+ this._slotId = aid.slotId || 'default';
752
+ const debugFlag = nextConfig.debug;
753
+ this._logger = new AUNLogger({ debug: debugFlag, aunPath: nextConfig.aunPath });
754
+ this._logger.bindDeviceId(this._deviceId);
755
+ this._clientLog = this._logger.for('aun_core.client');
756
+ const dnsNet = new DnsResilientNet({
757
+ verifySsl: nextConfig.verifySsl,
758
+ logger: this._clientLog,
759
+ });
760
+ this._discovery = new GatewayDiscovery({ verifySsl: nextConfig.verifySsl, logger: this._clientLog, net: dnsNet });
761
+ const keystore = new FileKeyStore(nextConfig.aunPath, {
762
+ logger: this._logger.for('aun_core.keystore'),
763
+ secretStoreLogger: this._logger.for('aun_core.secret-store'),
764
+ });
765
+ this._keystore = keystore;
766
+ this._auth = new AuthFlow({
767
+ keystore,
768
+ crypto: new CryptoProvider(),
769
+ aid: aid.aid,
770
+ deviceId: this._deviceId,
771
+ slotId: this._slotId,
772
+ rootCaPath: nextConfig.rootCaPath ?? undefined,
773
+ verifySsl: nextConfig.verifySsl,
774
+ logger: this._logger.for('aun_core.auth'),
775
+ net: dnsNet,
776
+ });
777
+ this._transport = new RPCTransport({
778
+ eventDispatcher: this._dispatcher,
779
+ timeout: 10_000,
780
+ onDisconnect: (err, closeCode) => this._handleTransportDisconnect(err, closeCode),
781
+ verifySsl: nextConfig.verifySsl,
782
+ logger: this._logger.for('aun_core.transport'),
783
+ dnsNet,
784
+ });
785
+ this._transport.setMetaObserver((meta) => this._observeRpcMeta(meta));
786
+ }
717
787
  loadIdentity(aid) {
718
788
  if (!aid?.isPrivateKeyValid()) {
719
789
  throw new StateError('loadIdentity requires an AID with a valid private key');
@@ -722,15 +792,15 @@ export class AUNClient {
722
792
  if (publicState !== ConnectionState.NO_IDENTITY && publicState !== ConnectionState.CLOSED) {
723
793
  throw new StateError(`loadIdentity not allowed in state ${publicState}`);
724
794
  }
795
+ this._applyAidRuntimeContext(aid);
725
796
  this._currentAid = aid;
726
797
  this._aid = aid.aid;
727
798
  this._identity = {
728
799
  aid: aid.aid,
729
- private_key_pem: aid._privateKeyPem ?? '',
800
+ private_key_pem: aid.privateKeyPem,
730
801
  public_key_der_b64: aid.publicKey,
731
802
  cert: aid.certPem,
732
803
  };
733
- this._auth._aid = aid.aid;
734
804
  this._state = 'standby';
735
805
  this._closing = false;
736
806
  this._lastError = null;
@@ -1028,7 +1098,7 @@ export class AUNClient {
1028
1098
  */
1029
1099
  async publishAgentMd() {
1030
1100
  const target = this._agentMdOwnerAid();
1031
- if (!target) {
1101
+ if (!target || !this._currentAid) {
1032
1102
  throw new ValidationError('publishAgentMd requires local AID');
1033
1103
  }
1034
1104
  const content = this._readAgentMdContent(target);
@@ -1116,7 +1186,7 @@ export class AUNClient {
1116
1186
  this._agentMdCache.clear();
1117
1187
  return this._agentMdPath;
1118
1188
  }
1119
- /** 返回 setLocalAgentMdPath 计算的 etag;未设置或读取失败时返回空串。 */
1189
+ /** 返回本地 agent.md 文件的 etag;未设置或读取失败时返回空串。 */
1120
1190
  getLocalAgentMdEtag() {
1121
1191
  return this._localAgentMdEtag;
1122
1192
  }
@@ -1604,12 +1674,18 @@ export class AUNClient {
1604
1674
  /** 连接到 Gateway;身份来自构造函数或 loadIdentity(aid),认证由 SDK 内部自动完成。 */
1605
1675
  async connect(opts) {
1606
1676
  const tStart = Date.now();
1607
- if (opts !== undefined && typeof opts === 'object') {
1677
+ // 先校验非法参数(ValidationError),再检查身份(StateError)
1678
+ if (opts !== undefined && opts !== null && typeof opts === 'object') {
1608
1679
  const raw = opts;
1609
- if ('gateway' in raw || 'access_token' in raw || 'aid' in raw || 'token' in raw) {
1610
- throw new ValidationError('connect options must not include gateway/access_token/aid; these are managed internally');
1680
+ const invalid = Object.keys(raw).filter((key) => !PUBLIC_CONNECTION_OPTION_KEYS.has(key)).sort();
1681
+ if (invalid.length > 0) {
1682
+ throw new ValidationError(`connect options contain unsupported field(s): ${invalid.join(', ')}`);
1611
1683
  }
1612
1684
  }
1685
+ const target = this._currentAid?.aid ?? this._aid ?? '';
1686
+ if (!target || !this._currentAid?.isPrivateKeyValid()) {
1687
+ throw new StateError('connect requires a loaded AID with a valid private key');
1688
+ }
1613
1689
  const options = {};
1614
1690
  if (opts?.auto_reconnect !== undefined)
1615
1691
  options.auto_reconnect = opts.auto_reconnect;
@@ -1628,10 +1704,16 @@ export class AUNClient {
1628
1704
  max_attempts: opts.retry_max_attempts ?? 0,
1629
1705
  };
1630
1706
  }
1631
- const target = this._currentAid?.aid ?? this._aid ?? '';
1632
- if (!target || !this._currentAid?.isPrivateKeyValid()) {
1633
- throw new StateError('connect requires a loaded AID with a valid private key');
1634
- }
1707
+ if (opts?.connection_kind !== undefined)
1708
+ options.connection_kind = opts.connection_kind;
1709
+ if (opts?.short_ttl_ms !== undefined)
1710
+ options.short_ttl_ms = opts.short_ttl_ms;
1711
+ if (opts?.delivery_mode !== undefined)
1712
+ options.delivery_mode = opts.delivery_mode;
1713
+ if (opts?.extra_info !== undefined)
1714
+ options.extra_info = opts.extra_info;
1715
+ if (opts?.background_sync !== undefined)
1716
+ options.background_sync = opts.background_sync;
1635
1717
  const publicState = this.state;
1636
1718
  const allowed = new Set([
1637
1719
  ConnectionState.STANDBY,
@@ -1793,6 +1875,14 @@ export class AUNClient {
1793
1875
  }
1794
1876
  this._validateOutboundCall(method, p);
1795
1877
  this._injectMessageCursorContext(method, p);
1878
+ if (method.startsWith('group.')
1879
+ && !('_group_cursor_params' in p)
1880
+ && !Boolean(p._pull_gate_locked)) {
1881
+ const explicitCursorParams = this._groupCursorParams(p);
1882
+ if (Object.keys(explicitCursorParams).length > 0) {
1883
+ p._group_cursor_params = explicitCursorParams;
1884
+ }
1885
+ }
1796
1886
  // group.* 方法的 group_id 归一化为 canonical 格式(兼容老/污染数据)
1797
1887
  if (method.startsWith('group.') && p.group_id !== undefined && p.group_id !== null) {
1798
1888
  const rawGroupId = String(p.group_id);
@@ -1894,7 +1984,17 @@ export class AUNClient {
1894
1984
  throw new ValidationError('group.pull requires group_id');
1895
1985
  }
1896
1986
  await this._ensureV2SessionReady('group.pull');
1897
- const messages = await runWithRpcPriority(() => this._pullGroupV2(String(p.group_id), Number(p.after_seq ?? p.after_message_seq ?? 0) || 0, Number(p.limit ?? 50) || 50, { gateLocked: true }));
1987
+ const hasExplicitAfterSeq = 'after_seq' in p || 'after_message_seq' in p;
1988
+ const cursorParams = this._explicitGroupCursorParams(p);
1989
+ const ownsCursor = Object.keys(cursorParams).length === 0 || this._groupCursorTargetsCurrentInstance(cursorParams);
1990
+ const pullOpts = { gateLocked: true };
1991
+ if (hasExplicitAfterSeq)
1992
+ pullOpts.explicitAfterSeq = true;
1993
+ if (Object.keys(cursorParams).length > 0)
1994
+ pullOpts.cursorParams = cursorParams;
1995
+ if (!ownsCursor)
1996
+ pullOpts.ownsCursor = false;
1997
+ const messages = await runWithRpcPriority(() => this._pullGroupV2(String(p.group_id), Number(p.after_seq ?? p.after_message_seq ?? 0) || 0, Number(p.limit ?? 50) || 50, pullOpts));
1898
1998
  return { messages };
1899
1999
  }
1900
2000
  if (method === 'group.ack_messages' || method === 'group.v2.ack') {
@@ -1902,12 +2002,18 @@ export class AUNClient {
1902
2002
  throw new ValidationError('group.ack_messages requires group_id');
1903
2003
  }
1904
2004
  await this._ensureV2SessionReady('group.ack_messages');
2005
+ const cursorParams = this._explicitGroupCursorParams(p);
2006
+ const ownsCursor = Object.keys(cursorParams).length === 0 || this._groupCursorTargetsCurrentInstance(cursorParams);
2007
+ if (method === 'group.ack_messages' && !ownsCursor) {
2008
+ return await runWithRpcPriority(() => this._rawGroupAckMessages(p));
2009
+ }
1905
2010
  return await runWithRpcPriority(() => this._ackGroupV2(String(p.group_id), Number(p.seq ?? p.msg_seq ?? p.up_to_seq ?? 0) || undefined));
1906
2011
  }
1907
2012
  if (method === 'message.pull') {
1908
2013
  delete p._skip_auto_ack;
1909
2014
  delete p.skip_auto_ack;
1910
2015
  }
2016
+ delete p._group_cursor_params;
1911
2017
  // 关键操作自动附加客户端签名
1912
2018
  if (SIGNED_METHODS.has(method)) {
1913
2019
  if (this._shouldSkipClientSignature(method, p)) {
@@ -1978,6 +2084,7 @@ export class AUNClient {
1978
2084
  delete p._pull_gate_locked;
1979
2085
  delete p._skip_auto_ack;
1980
2086
  delete p.skip_auto_ack;
2087
+ delete p._group_cursor_params;
1981
2088
  if (method.startsWith('group.') && p.group_id !== undefined && p.group_id !== null) {
1982
2089
  p.group_id = normalizeGroupId(String(p.group_id)) || String(p.group_id);
1983
2090
  }
@@ -2026,13 +2133,12 @@ export class AUNClient {
2026
2133
  * 签名覆盖所有非 _ 前缀且非 client_signature 的业务字段。
2027
2134
  */
2028
2135
  _signClientOperation(method, params) {
2029
- const identity = this._identity;
2030
- if (!identity || !identity.private_key_pem)
2136
+ const currentAid = this._currentAid;
2137
+ if (!currentAid?.privateKeyPem)
2031
2138
  return;
2032
2139
  try {
2033
- const aid = String(identity.aid ?? '');
2140
+ const aid = currentAid.aid;
2034
2141
  const ts = String(Math.floor(Date.now() / 1000));
2035
- // 计算 params hash — 必须递归排序所有键(与 Python json.dumps(sort_keys=True, separators=(",",":")) 一致)
2036
2142
  const paramsForHash = {};
2037
2143
  for (const [k, v] of Object.entries(params)) {
2038
2144
  if (k !== 'client_signature' && !k.startsWith('_')) {
@@ -2042,11 +2148,11 @@ export class AUNClient {
2042
2148
  const paramsJson = stableStringify(paramsForHash);
2043
2149
  const paramsHash = crypto.createHash('sha256').update(paramsJson, 'utf-8').digest('hex');
2044
2150
  const signData = Buffer.from(`${method}|${aid}|${ts}|${paramsHash}`, 'utf-8');
2045
- const privateKey = crypto.createPrivateKey(String(identity.private_key_pem));
2151
+ const privateKey = crypto.createPrivateKey(currentAid.privateKeyPem);
2046
2152
  const signature = crypto.sign('SHA256', signData, privateKey);
2047
2153
  // 证书指纹
2048
2154
  let certFingerprint = '';
2049
- const certPem = String(identity.cert ?? '');
2155
+ const certPem = currentAid.certPem;
2050
2156
  if (certPem) {
2051
2157
  const certObj = new crypto.X509Certificate(certPem);
2052
2158
  certFingerprint = 'sha256:' + certObj.fingerprint256.replace(/:/g, '').toLowerCase();
@@ -2605,7 +2711,7 @@ export class AUNClient {
2605
2711
  }
2606
2712
  if ('slot_id' in message) {
2607
2713
  const targetSlotId = String(message.slot_id ?? '').trim();
2608
- if (targetSlotId !== this._slotId) {
2714
+ if (slotIsolationKey(targetSlotId) !== slotIsolationKey(this._slotId)) {
2609
2715
  return false;
2610
2716
  }
2611
2717
  }
@@ -2752,6 +2858,27 @@ export class AUNClient {
2752
2858
  }
2753
2859
  return Math.max(0, ...values.filter((value) => Number.isFinite(value)));
2754
2860
  }
2861
+ _groupCursorParams(params) {
2862
+ const cursorParams = {};
2863
+ for (const key of ['device_id', 'slot_id', 'device_name', 'device_type']) {
2864
+ const value = params[key];
2865
+ if (value !== undefined && value !== null)
2866
+ cursorParams[key] = value;
2867
+ }
2868
+ return cursorParams;
2869
+ }
2870
+ _explicitGroupCursorParams(params) {
2871
+ const value = params._group_cursor_params;
2872
+ if (!isJsonObject(value))
2873
+ return {};
2874
+ return { ...value };
2875
+ }
2876
+ _groupCursorTargetsCurrentInstance(params) {
2877
+ const deviceId = String(params.device_id ?? '').trim();
2878
+ const slotId = String(params.slot_id ?? '').trim();
2879
+ return (!deviceId || deviceId === (this._deviceId ?? ''))
2880
+ && (!slotId || slotId === (this._slotId ?? ''));
2881
+ }
2755
2882
  _schedulePullFollowup(method, params, result) {
2756
2883
  if (method === 'message.pull')
2757
2884
  method = 'message.v2.pull';
@@ -4031,18 +4158,30 @@ export class AUNClient {
4031
4158
  this._restoreSeqTrackerState();
4032
4159
  }
4033
4160
  this._startBackgroundTasks();
4034
- // V2 E2EE:初始化 session 并注册本设备 SPK。
4035
- try {
4036
- await this._initV2Session();
4161
+ const connectionKind = String(params.connection_kind ?? 'long');
4162
+ const isShortConnection = connectionKind === 'short';
4163
+ if (!isShortConnection) {
4164
+ // V2 E2EE:长连接上线时初始化 session 并注册本设备 SPK。
4165
+ try {
4166
+ await this._initV2Session();
4167
+ }
4168
+ catch (exc) {
4169
+ this._clientLog.warn(`V2 session init failed (non-fatal): ${formatCaughtError(exc)}`);
4170
+ }
4037
4171
  }
4038
- catch (exc) {
4039
- this._clientLog.warn(`V2 session init failed (non-fatal): ${formatCaughtError(exc)}`);
4172
+ else {
4173
+ this._clientLog.debug('V2 session init deferred for short connection');
4040
4174
  }
4041
4175
  // connect/reconnect 成功后自动触发一次 P2P message.v2.pull,补齐离线期间积压
4042
4176
  // 群消息按惰性触发,不在此处主动 pull
4043
- void this._fillP2pGap().catch((exc) => {
4044
- this._clientLog.warn(`schedule post-connect P2P gap fill failed: ${formatCaughtError(exc)}`);
4045
- });
4177
+ const hasExplicitBackgroundSync = Object.prototype.hasOwnProperty.call(params, 'background_sync');
4178
+ const backgroundSyncEnabled = this._sessionOptions.background_sync !== false
4179
+ && (!isShortConnection || hasExplicitBackgroundSync);
4180
+ if (backgroundSyncEnabled) {
4181
+ void this._fillP2pGap().catch((exc) => {
4182
+ this._clientLog.warn(`schedule post-connect P2P gap fill failed: ${formatCaughtError(exc)}`);
4183
+ });
4184
+ }
4046
4185
  this._clientLog.debug(`_connectOnce exit: elapsed=${Date.now() - tStart}ms gateway=${gatewayUrl}, aid=${this._aid ?? ''}`);
4047
4186
  }
4048
4187
  catch (err) {
@@ -4077,6 +4216,15 @@ export class AUNClient {
4077
4216
  }
4078
4217
  /** V2-only:所有加密入口都必须有 V2 session。 */
4079
4218
  async _ensureV2SessionReady(method, errorMessage) {
4219
+ if (!this._v2Session) {
4220
+ if (!this._v2SessionInitInFlight) {
4221
+ this._v2SessionInitInFlight = this._initV2Session()
4222
+ .finally(() => {
4223
+ this._v2SessionInitInFlight = null;
4224
+ });
4225
+ }
4226
+ await this._v2SessionInitInFlight;
4227
+ }
4080
4228
  if (!this._v2Session) {
4081
4229
  throw new StateError(errorMessage ?? `V2 session not initialized; encrypted ${method} requires E2EE V2`);
4082
4230
  }
@@ -4109,32 +4257,13 @@ export class AUNClient {
4109
4257
  identity = null;
4110
4258
  }
4111
4259
  }
4112
- if (!identity?.private_key_pem) {
4113
- // fallback:缓存的 identity 可能被 instanceState 污染,重新从 keystore 加载
4114
- try {
4115
- identity = this._keystore.loadIdentity(this._aid);
4116
- if (identity?.private_key_pem) {
4117
- this._identity = identity;
4118
- this._clientLog.warn('V2 session init: identity cache was stale, reloaded from keystore');
4119
- // 重新持久化 instance_state,清理脏数据
4120
- const persistIdentity = this._auth._persistIdentity;
4121
- if (typeof persistIdentity === 'function') {
4122
- try {
4123
- persistIdentity.call(this._auth, identity);
4124
- }
4125
- catch { /* best-effort */ }
4126
- }
4127
- }
4128
- }
4129
- catch {
4130
- identity = null;
4131
- }
4132
- }
4133
- if (!identity?.private_key_pem) {
4260
+ // 私钥由 AIDStore 管理,直接从 _currentAid 读取明文私钥
4261
+ const currentAid = this._currentAid;
4262
+ if (!currentAid?.privateKeyPem) {
4134
4263
  this._clientLog.warn('V2 session init skipped: no AID private key');
4135
4264
  return;
4136
4265
  }
4137
- const privateKey = crypto.createPrivateKey(String(identity.private_key_pem));
4266
+ const privateKey = crypto.createPrivateKey(currentAid.privateKeyPem);
4138
4267
  const jwk = privateKey.export({ format: 'jwk' });
4139
4268
  if (jwk.kty !== 'EC' || jwk.crv !== 'P-256' || !jwk.d) {
4140
4269
  throw new StateError('AID private key must be EC P-256');
@@ -4680,6 +4809,7 @@ export class AUNClient {
4680
4809
  decrypted.push(plaintext);
4681
4810
  this._logMessageDebug('decrypt-ok', 'message.v2.pull', 'message.received', plaintext);
4682
4811
  }
4812
+ const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
4683
4813
  const serverAckSeq = Number(result.server_ack_seq ?? 0);
4684
4814
  if (ns && Number.isFinite(serverAckSeq) && serverAckSeq > 0) {
4685
4815
  const contig = this._seqTracker.getContiguousSeq(ns);
@@ -4695,7 +4825,11 @@ export class AUNClient {
4695
4825
  await this._drainOrderedMessages(ns, undefined, true);
4696
4826
  this._saveSeqTrackerState();
4697
4827
  }
4698
- if (messages.length > 0 && contigAdvanced && ackSeq > 0 && !opts?.skipAutoAck) {
4828
+ const ackNeeded = messages.length > 0
4829
+ && ackSeq > 0
4830
+ && !opts?.skipAutoAck
4831
+ && (contigAdvanced || (hasServerAckSeq && ackSeq > serverAckSeq));
4832
+ if (ackNeeded) {
4699
4833
  this._clientLog.debug(`message.v2.pull scheduling auto-ack: ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
4700
4834
  this._safeAsync(this._ackV2(ackSeq).then(() => undefined));
4701
4835
  }
@@ -4958,7 +5092,9 @@ export class AUNClient {
4958
5092
  }
4959
5093
  const decrypted = [];
4960
5094
  let totalRawCount = 0;
4961
- let nextAfterSeq = afterSeq || this._seqTracker.getContiguousSeq(ns);
5095
+ const cursorParams = opts?.cursorParams ?? {};
5096
+ const ownsCursor = opts?.ownsCursor !== false;
5097
+ let nextAfterSeq = opts?.explicitAfterSeq ? afterSeq : (afterSeq || this._seqTracker.getContiguousSeq(ns));
4962
5098
  let pageCount = 0;
4963
5099
  const maxPages = 100;
4964
5100
  while (pageCount < maxPages) {
@@ -4968,6 +5104,7 @@ export class AUNClient {
4968
5104
  group_id: gid,
4969
5105
  after_seq: nextAfterSeq,
4970
5106
  limit,
5107
+ ...cursorParams,
4971
5108
  });
4972
5109
  const messages = (Array.isArray(result.messages) ? result.messages : []);
4973
5110
  totalRawCount += messages.length;
@@ -5046,7 +5183,9 @@ export class AUNClient {
5046
5183
  decrypted.push(plaintext);
5047
5184
  this._logMessageDebug('decrypt-ok', 'group.v2.pull', 'group.message_created', plaintext);
5048
5185
  }
5049
- const retentionFloor = this._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq');
5186
+ const cursorCurrentSeq = Number(cursor?.current_seq ?? 0);
5187
+ const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
5188
+ const retentionFloor = Math.max(this._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq'), Number.isFinite(cursorCurrentSeq) ? cursorCurrentSeq : 0);
5050
5189
  if (retentionFloor > 0) {
5051
5190
  const contig = this._seqTracker.getContiguousSeq(ns);
5052
5191
  if (contig < retentionFloor) {
@@ -5060,11 +5199,17 @@ export class AUNClient {
5060
5199
  await this._drainOrderedMessages(ns, undefined, true);
5061
5200
  this._saveSeqTrackerState();
5062
5201
  }
5063
- if (messages.length > 0 && contigAdvanced && ackSeq > 0) {
5202
+ const ackNeeded = messages.length > 0
5203
+ && ackSeq > 0
5204
+ && ownsCursor
5205
+ && (contigAdvanced || (hasServerCursor && ackSeq > cursorCurrentSeq));
5206
+ if (ackNeeded) {
5064
5207
  this._clientLog.debug(`group.v2.pull scheduling auto-ack: group=${gid}, ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
5065
5208
  this._safeAsync(this._ackGroupV2(gid, ackSeq).then(() => undefined));
5066
5209
  }
5067
5210
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
5211
+ if (!ownsCursor)
5212
+ break;
5068
5213
  if (messages.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
5069
5214
  break;
5070
5215
  nextAfterSeq = nextAfter;
@@ -5075,6 +5220,10 @@ export class AUNClient {
5075
5220
  this._clientLog.debug(`group.v2.pull done: group=${gid}, requested_after_seq=${afterSeq}, pages=${pageCount}, decrypted=${decrypted.length}, ns=${ns}`);
5076
5221
  return decrypted;
5077
5222
  }
5223
+ async _rawGroupAckMessages(params) {
5224
+ const p = { ...params };
5225
+ return await this._callRawV2Rpc('group.ack_messages', p);
5226
+ }
5078
5227
  /** V2 Group ack。 */
5079
5228
  async _ackGroupV2(groupId, upToSeq) {
5080
5229
  const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
@@ -6074,7 +6223,7 @@ export class AUNClient {
6074
6223
  return;
6075
6224
  }
6076
6225
  let signature = '';
6077
- const privateKeyPem = String(this._identity?.private_key_pem ?? '');
6226
+ const privateKeyPem = this._currentAid?.privateKeyPem ?? '';
6078
6227
  if (privateKeyPem) {
6079
6228
  try {
6080
6229
  const signPayload = stableStringify({
@@ -6512,7 +6661,7 @@ export class AUNClient {
6512
6661
  delete request.access_token;
6513
6662
  request.gateway = gateway;
6514
6663
  request.device_id = this._deviceId;
6515
- request.slot_id = normalizeInstanceId(request.slot_id ?? this._slotId, 'slot_id', { allowEmpty: true });
6664
+ request.slot_id = normalizeSlotId(request.slot_id ?? this._slotId);
6516
6665
  let deliveryModeRaw = request.delivery_mode;
6517
6666
  if (deliveryModeRaw == null) {
6518
6667
  deliveryModeRaw = { ...this._defaultConnectDeliveryMode };
@@ -6575,6 +6724,8 @@ export class AUNClient {
6575
6724
  if ('timeouts' in params && isJsonObject(params.timeouts)) {
6576
6725
  Object.assign(options.timeouts, params.timeouts);
6577
6726
  }
6727
+ if ('background_sync' in params)
6728
+ options.background_sync = Boolean(params.background_sync);
6578
6729
  return options;
6579
6730
  }
6580
6731
  // ── 内部:后台任务 ────────────────────────────────────────