@agentunion/fastaun 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -444,6 +444,8 @@ export class AUNClient {
444
444
  _pushedSeqs = new Map();
445
445
  /** 已解密但因 seq 空洞暂缓发布的应用层消息(按 namespace -> seq) */
446
446
  _pendingOrderedMsgs = new Map();
447
+ /** P2P pull 进行中到达的纯通知 push 上界;pull gate 释放后需要补拉一次。 */
448
+ _pendingP2pPullUpper = new Map();
447
449
  /** 缺 sender IK 时暂存原始 V2 消息,后台补齐 IK 后重试解密。 */
448
450
  _v2SenderIKPending = new Map();
449
451
  /** sender IK 后台补齐任务去重。 */
@@ -1491,11 +1493,12 @@ export class AUNClient {
1491
1493
  if (method === 'message.pull' || method === 'message.v2.pull') {
1492
1494
  await this._ensureV2SessionReady('message.pull');
1493
1495
  const skipAutoAck = p._skip_auto_ack === true || p.skip_auto_ack === true;
1496
+ const force = p.force === true;
1494
1497
  const afterSeq = Number(p.after_seq ?? 0) || 0;
1495
1498
  const limit = Number(p.limit ?? 50) || 50;
1496
1499
  const messages = skipAutoAck
1497
- ? await runWithRpcPriority(() => this.pullV2(afterSeq, limit, { skipAutoAck: true, gateLocked: true }))
1498
- : await runWithRpcPriority(() => this.pullV2(afterSeq, limit, { gateLocked: true }));
1500
+ ? await runWithRpcPriority(() => this.pullV2(afterSeq, limit, { skipAutoAck: true, gateLocked: true, force }))
1501
+ : await runWithRpcPriority(() => this.pullV2(afterSeq, limit, { gateLocked: true, force }));
1499
1502
  return { messages };
1500
1503
  }
1501
1504
  if (method === 'message.ack' || method === 'message.v2.ack') {
@@ -1760,6 +1763,7 @@ export class AUNClient {
1760
1763
  this._clientLog.debug(`P2P push filtered by instance: message_id=${String(msg.message_id ?? '')}, seq=${String(msg.seq ?? '')}, target_device=${String(msg.device_id ?? '')}, target_slot=${String(msg.slot_id ?? '')}, local_device=${this._deviceId}, local_slot=${this._slotId}`);
1761
1764
  return;
1762
1765
  }
1766
+ const encryptedPush = this._isEncryptedPushMessage(msg);
1763
1767
  // P2P 空洞检测
1764
1768
  const seq = msg.seq;
1765
1769
  if (seq !== undefined && seq !== null && this._aid) {
@@ -1768,7 +1772,9 @@ export class AUNClient {
1768
1772
  if (seq > 0)
1769
1773
  this._seqTracker.updateMaxSeen(ns, seq);
1770
1774
  const contigBefore = this._seqTracker.getContiguousSeq(ns);
1771
- const published = await this._publishOrderedMessage('message.received', ns, seq, msg);
1775
+ const published = encryptedPush
1776
+ ? await this._publishEncryptedPushMessage('message.received', 'message.undecryptable', ns, seq, msg, false)
1777
+ : await this._publishOrderedMessage('message.received', ns, seq, msg);
1772
1778
  const contigAfter = this._seqTracker.getContiguousSeq(ns);
1773
1779
  const needPull = Number(seq) > contigAfter && !published;
1774
1780
  if (needPull) {
@@ -1788,8 +1794,14 @@ export class AUNClient {
1788
1794
  // 即时持久化 cursor,异常断连后不回退
1789
1795
  if (contigAfter !== contigBefore)
1790
1796
  this._saveSeqTrackerState();
1797
+ if (encryptedPush)
1798
+ return;
1791
1799
  }
1792
1800
  else {
1801
+ if (encryptedPush) {
1802
+ await this._publishEncryptedPushMessage('message.received', 'message.undecryptable', '', seq ?? 0, msg, false);
1803
+ return;
1804
+ }
1793
1805
  // V2-only:普通 _raw.message.received 只承载明文;V2 密文由 peer.v2.message_received 通知触发 pull。
1794
1806
  await this._publishAppEvent('message.received', msg, 'push');
1795
1807
  }
@@ -1848,13 +1860,16 @@ export class AUNClient {
1848
1860
  });
1849
1861
  return;
1850
1862
  }
1863
+ const encryptedPush = this._isEncryptedPushMessage(msg);
1851
1864
  if (groupId && seq !== undefined && seq !== null) {
1852
1865
  const ns = `group:${groupId}`;
1853
1866
  // Push 只先更新 maxSeenSeq;contiguous_seq 是已交付游标,必须等应用层发布返回后再推进。
1854
1867
  if (seq > 0)
1855
1868
  this._seqTracker.updateMaxSeen(ns, seq);
1856
1869
  const contigBefore = this._seqTracker.getContiguousSeq(ns);
1857
- const published = await this._publishOrderedMessage('group.message_created', ns, seq, msg);
1870
+ const published = encryptedPush
1871
+ ? await this._publishEncryptedPushMessage('group.message_created', 'group.message_undecryptable', ns, seq, msg, true)
1872
+ : await this._publishOrderedMessage('group.message_created', ns, seq, msg);
1858
1873
  const contigAfter = this._seqTracker.getContiguousSeq(ns);
1859
1874
  const needPull = Number(seq) > contigAfter && !published;
1860
1875
  if (needPull) {
@@ -1872,8 +1887,14 @@ export class AUNClient {
1872
1887
  }
1873
1888
  if (contigAfter !== contigBefore)
1874
1889
  this._saveSeqTrackerState();
1890
+ if (encryptedPush)
1891
+ return;
1875
1892
  }
1876
1893
  else {
1894
+ if (encryptedPush) {
1895
+ await this._publishEncryptedPushMessage('group.message_created', 'group.message_undecryptable', '', seq ?? 0, msg, true);
1896
+ return;
1897
+ }
1877
1898
  // V2-only:普通 group.message_created 只承载明文;V2 密文由 group.v2.message_created 通知触发 pull。
1878
1899
  await this._publishAppEvent('group.message_created', msg, 'group-push');
1879
1900
  }
@@ -1992,6 +2013,38 @@ export class AUNClient {
1992
2013
  this._pushedSeqs.set(ns, new Set(keep));
1993
2014
  }
1994
2015
  }
2016
+ _recordPendingP2pPull(ns, seq) {
2017
+ if (!ns || seq <= 0)
2018
+ return;
2019
+ const previous = this._pendingP2pPullUpper.get(ns) ?? 0;
2020
+ if (seq > previous) {
2021
+ this._pendingP2pPullUpper.set(ns, seq);
2022
+ }
2023
+ this._clientLog.debug(`P2P pending pull upper recorded: ns=${ns}, seq=${seq}, previous=${previous}, contiguous=${this._seqTracker.getContiguousSeq(ns)}`);
2024
+ }
2025
+ _schedulePendingP2pPullIfNeeded(ns, reason) {
2026
+ if (!ns)
2027
+ return false;
2028
+ const upperSeq = this._pendingP2pPullUpper.get(ns) ?? 0;
2029
+ if (upperSeq <= 0) {
2030
+ this._pendingP2pPullUpper.delete(ns);
2031
+ return false;
2032
+ }
2033
+ const contig = this._seqTracker.getContiguousSeq(ns);
2034
+ if (upperSeq <= contig) {
2035
+ this._pendingP2pPullUpper.delete(ns);
2036
+ this._clientLog.debug(`P2P pending pull upper already covered: ns=${ns}, upper_seq=${upperSeq}, contiguous=${contig}, reason=${reason}`);
2037
+ return false;
2038
+ }
2039
+ if (this._state !== 'connected' || this._closing) {
2040
+ this._clientLog.debug(`P2P pending pull postponed: ns=${ns}, upper_seq=${upperSeq}, contiguous=${contig}, state=${this._state}, closing=${this._closing}, reason=${reason}`);
2041
+ return false;
2042
+ }
2043
+ this._pendingP2pPullUpper.delete(ns);
2044
+ this._clientLog.info(`P2P pending push follow-up pull scheduled: ns=${ns}, upper_seq=${upperSeq}, contiguous=${contig}, reason=${reason}`);
2045
+ void this._fillP2pGap();
2046
+ return true;
2047
+ }
1995
2048
  _markPublishedSeq(ns, seq) {
1996
2049
  let pushed = this._pushedSeqs.get(ns);
1997
2050
  if (!pushed) {
@@ -2242,6 +2295,9 @@ export class AUNClient {
2242
2295
  return;
2243
2296
  gate.inflight = false;
2244
2297
  gate.startedAt = 0;
2298
+ if (key.startsWith('p2p:')) {
2299
+ this._schedulePendingP2pPullIfNeeded(key, 'pull-gate-release');
2300
+ }
2245
2301
  }
2246
2302
  _pullGateKeyForCall(method, params) {
2247
2303
  if (method === 'message.pull' || method === 'message.v2.pull') {
@@ -2423,12 +2479,16 @@ export class AUNClient {
2423
2479
  this._releasePullGate(key, token);
2424
2480
  }
2425
2481
  }
2426
- async _tryRunBackgroundPull(key, operation, followupOnMessages = false) {
2427
- if (key && this._isPullResponseProcessing(key))
2482
+ async _tryRunBackgroundPull(key, operation, followupOnMessages = false, onBusy) {
2483
+ if (key && this._isPullResponseProcessing(key)) {
2484
+ onBusy?.();
2428
2485
  return false;
2486
+ }
2429
2487
  const token = this._tryAcquirePullGate(key);
2430
- if (token === null)
2488
+ if (token === null) {
2489
+ onBusy?.();
2431
2490
  return false;
2491
+ }
2432
2492
  let count = 0;
2433
2493
  try {
2434
2494
  count = await this._withBackgroundRpc(operation);
@@ -3420,6 +3480,7 @@ export class AUNClient {
3420
3480
  this._gapFillDone.clear();
3421
3481
  this._pushedSeqs.clear();
3422
3482
  this._pendingOrderedMsgs.clear();
3483
+ this._pendingP2pPullUpper.clear();
3423
3484
  this._v2SenderIKPending.clear();
3424
3485
  this._v2SenderIKFetching.clear();
3425
3486
  this._groupSynced.clear();
@@ -3432,6 +3493,7 @@ export class AUNClient {
3432
3493
  this._gapFillDone.clear();
3433
3494
  this._pushedSeqs.clear();
3434
3495
  this._pendingOrderedMsgs.clear();
3496
+ this._pendingP2pPullUpper.clear();
3435
3497
  this._v2SenderIKPending.clear();
3436
3498
  this._v2SenderIKFetching.clear();
3437
3499
  this._groupSynced.clear();
@@ -3706,6 +3768,27 @@ export class AUNClient {
3706
3768
  identity = null;
3707
3769
  }
3708
3770
  }
3771
+ if (!identity?.private_key_pem) {
3772
+ // fallback:缓存的 identity 可能被 instanceState 污染,重新从 keystore 加载
3773
+ try {
3774
+ identity = this._keystore.loadIdentity(this._aid);
3775
+ if (identity?.private_key_pem) {
3776
+ this._identity = identity;
3777
+ this._clientLog.warn('V2 session init: identity cache was stale, reloaded from keystore');
3778
+ // 重新持久化 instance_state,清理脏数据
3779
+ const persistIdentity = this._auth._persistIdentity;
3780
+ if (typeof persistIdentity === 'function') {
3781
+ try {
3782
+ persistIdentity.call(this._auth, identity);
3783
+ }
3784
+ catch { /* best-effort */ }
3785
+ }
3786
+ }
3787
+ }
3788
+ catch {
3789
+ identity = null;
3790
+ }
3791
+ }
3709
3792
  if (!identity?.private_key_pem) {
3710
3793
  this._clientLog.warn('V2 session init skipped: no AID private key');
3711
3794
  return;
@@ -4169,7 +4252,7 @@ export class AUNClient {
4169
4252
  }
4170
4253
  const decrypted = [];
4171
4254
  let totalRawCount = 0;
4172
- let nextAfterSeq = afterSeq || (ns ? this._seqTracker.getContiguousSeq(ns) : 0);
4255
+ let nextAfterSeq = opts?.force ? afterSeq : (afterSeq || (ns ? this._seqTracker.getContiguousSeq(ns) : 0));
4173
4256
  let pageCount = 0;
4174
4257
  const maxPages = 100;
4175
4258
  while (pageCount < maxPages) {
@@ -4178,6 +4261,7 @@ export class AUNClient {
4178
4261
  const result = await this._callRawV2Rpc('message.v2.pull', {
4179
4262
  after_seq: nextAfterSeq,
4180
4263
  limit,
4264
+ ...(opts?.force ? { force: true } : {}),
4181
4265
  });
4182
4266
  const messages = (Array.isArray(result?.messages) ? result.messages : []);
4183
4267
  totalRawCount += messages.length;
@@ -4844,6 +4928,12 @@ export class AUNClient {
4844
4928
  encrypted: true,
4845
4929
  e2ee,
4846
4930
  };
4931
+ const explicitDirection = String(msg.direction ?? '').trim();
4932
+ result.direction = explicitDirection || (fromAid && fromAid === this._aid ? 'outbound_sync' : 'inbound');
4933
+ if (msg.device_id !== undefined)
4934
+ result.device_id = msg.device_id;
4935
+ if (msg.slot_id !== undefined)
4936
+ result.slot_id = msg.slot_id;
4847
4937
  this._attachV2EnvelopeMetadata(result, e2ee);
4848
4938
  this._logMessageDebug('decrypt-ok', 'v2.decrypt', groupIdForKeys ? 'group.message_created' : 'message.received', result);
4849
4939
  return result;
@@ -4910,6 +5000,146 @@ export class AUNClient {
4910
5000
  }
4911
5001
  return null;
4912
5002
  }
5003
+ _truthyBool(value) {
5004
+ if (value === true || value === 1)
5005
+ return true;
5006
+ if (typeof value === 'string') {
5007
+ const normalized = value.trim().toLowerCase();
5008
+ return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';
5009
+ }
5010
+ return false;
5011
+ }
5012
+ _encryptedPushEnvelope(msg) {
5013
+ const payload = msg.payload;
5014
+ if (this._isEncryptedEnvelopePayload(payload))
5015
+ return payload;
5016
+ if (typeof msg.envelope_json === 'string' && msg.envelope_json.trim()) {
5017
+ try {
5018
+ const parsed = JSON.parse(msg.envelope_json);
5019
+ if (this._isEncryptedEnvelopePayload(parsed))
5020
+ return parsed;
5021
+ }
5022
+ catch {
5023
+ return null;
5024
+ }
5025
+ }
5026
+ return null;
5027
+ }
5028
+ _isEncryptedPushMessage(msg) {
5029
+ if (this._truthyBool(msg.encrypted))
5030
+ return true;
5031
+ return this._encryptedPushEnvelope(msg) !== null;
5032
+ }
5033
+ _isEncryptedEnvelopePayload(payload) {
5034
+ if (!isJsonObject(payload))
5035
+ return false;
5036
+ const envelope = payload;
5037
+ const payloadType = String(envelope.type ?? '').trim();
5038
+ if (payloadType.startsWith('e2ee.'))
5039
+ return true;
5040
+ if (!String(envelope.ciphertext ?? '').trim())
5041
+ return false;
5042
+ return envelope.nonce !== undefined
5043
+ || envelope.tag !== undefined
5044
+ || envelope.recipient !== undefined
5045
+ || envelope.recipients !== undefined
5046
+ || envelope.wrapped_key !== undefined
5047
+ || envelope.recipients_digest !== undefined;
5048
+ }
5049
+ _isV2EncryptedEnvelopePayload(envelope) {
5050
+ if (!envelope)
5051
+ return false;
5052
+ const payloadType = String(envelope.type ?? '').trim();
5053
+ if (payloadType === 'e2ee.p2p_encrypted' || payloadType === 'e2ee.group_encrypted')
5054
+ return true;
5055
+ return String(envelope.version ?? '').trim().toLowerCase() === 'v2' && payloadType.startsWith('e2ee.');
5056
+ }
5057
+ _safeUndecryptablePushEvent(msg, group) {
5058
+ const event = {
5059
+ message_id: msg.message_id,
5060
+ from: msg.from,
5061
+ seq: msg.seq,
5062
+ timestamp: (msg.timestamp ?? msg.t_server),
5063
+ device_id: msg.device_id,
5064
+ slot_id: msg.slot_id,
5065
+ _decrypt_error: 'encrypted push payload is not decryptable on raw push path',
5066
+ _decrypt_stage: 'push_envelope',
5067
+ };
5068
+ if (group) {
5069
+ event.group_id = msg.group_id;
5070
+ }
5071
+ else {
5072
+ event.to = msg.to;
5073
+ }
5074
+ const envelope = this._encryptedPushEnvelope(msg);
5075
+ if (envelope) {
5076
+ event._envelope_type = String(envelope.type ?? '');
5077
+ event._suite = String(envelope.suite ?? '');
5078
+ if (this._isV2EncryptedEnvelopePayload(envelope)) {
5079
+ this._attachV2EnvelopeMetadata(event, this._v2E2eeMeta(envelope));
5080
+ }
5081
+ }
5082
+ return event;
5083
+ }
5084
+ async _decryptEncryptedPushPayload(msg, group) {
5085
+ const envelope = this._encryptedPushEnvelope(msg);
5086
+ if (!this._isV2EncryptedEnvelopePayload(envelope))
5087
+ return null;
5088
+ const aad = isJsonObject(envelope.aad) ? envelope.aad : {};
5089
+ const fromAid = String(msg.from_aid ?? msg.from ?? msg.sender_aid ?? aad.from ?? '').trim();
5090
+ const plaintext = await this._decryptV2EnvelopeForThought({ envelope, fromAid });
5091
+ if (!plaintext)
5092
+ return null;
5093
+ const e2ee = this._v2E2eeMeta(envelope);
5094
+ const result = {
5095
+ message_id: String(msg.message_id ?? ''),
5096
+ from: fromAid,
5097
+ seq: msg.seq,
5098
+ timestamp: (msg.t_server ?? msg.timestamp),
5099
+ payload: plaintext,
5100
+ encrypted: true,
5101
+ e2ee,
5102
+ };
5103
+ result.direction = fromAid && fromAid === this._aid ? 'outbound_sync' : 'inbound';
5104
+ if (msg.t_server !== undefined)
5105
+ result.t_server = msg.t_server;
5106
+ if (msg.device_id !== undefined)
5107
+ result.device_id = msg.device_id;
5108
+ if (msg.slot_id !== undefined)
5109
+ result.slot_id = msg.slot_id;
5110
+ if (group) {
5111
+ result.group_id = (msg.group_id ?? aad.group_id ?? envelope.group_id);
5112
+ }
5113
+ else {
5114
+ result.to = (msg.to ?? this._aid ?? '');
5115
+ }
5116
+ this._attachV2EnvelopeMetadata(result, e2ee);
5117
+ this._logMessageDebug('decrypt-ok', 'push.encrypted', group ? 'group.message_created' : 'message.received', result);
5118
+ return result;
5119
+ }
5120
+ async _publishEncryptedPushAsUndecryptable(event, ns, seq, msg, group) {
5121
+ const safeEvent = this._safeUndecryptablePushEvent(msg, group);
5122
+ this._logMessageDebug('decrypt-fail', 'push.encrypted', event, safeEvent);
5123
+ if (ns) {
5124
+ return await this._publishOrderedMessage(event, ns, seq, safeEvent);
5125
+ }
5126
+ const published = this._publishAppEvent(event, safeEvent, 'push');
5127
+ if (isPromiseLike(published))
5128
+ await published;
5129
+ return true;
5130
+ }
5131
+ async _publishEncryptedPushMessage(normalEvent, undecryptableEvent, ns, seq, msg, group) {
5132
+ const decrypted = await this._decryptEncryptedPushPayload(msg, group);
5133
+ if (decrypted) {
5134
+ if (ns)
5135
+ return await this._publishOrderedMessage(normalEvent, ns, seq, decrypted);
5136
+ const published = this._publishAppEvent(normalEvent, decrypted, 'push');
5137
+ if (isPromiseLike(published))
5138
+ await published;
5139
+ return true;
5140
+ }
5141
+ return await this._publishEncryptedPushAsUndecryptable(undecryptableEvent, ns, seq, msg, group);
5142
+ }
4913
5143
  _metadataWithoutAuth(value) {
4914
5144
  const candidate = value;
4915
5145
  if (!isJsonObject(candidate))
@@ -5694,8 +5924,10 @@ export class AUNClient {
5694
5924
  void this._tryRunBackgroundPull(ns, async () => {
5695
5925
  const operationBefore = this._seqTracker.getContiguousSeq(ns);
5696
5926
  const dedupKey = `p2p_pull:${ns}`;
5697
- if (this._gapFillDone.has(dedupKey))
5927
+ if (this._gapFillDone.has(dedupKey)) {
5928
+ this._recordPendingP2pPull(ns, pushSeq);
5698
5929
  return 0;
5930
+ }
5699
5931
  this._gapFillDone.set(dedupKey, Date.now());
5700
5932
  try {
5701
5933
  const pulled = await this.pullV2(0, 50, { gateLocked: true });
@@ -5708,7 +5940,7 @@ export class AUNClient {
5708
5940
  finally {
5709
5941
  this._gapFillDone.delete(dedupKey);
5710
5942
  }
5711
- }, true).catch((exc) => {
5943
+ }, true, () => this._recordPendingP2pPull(ns, pushSeq)).catch((exc) => {
5712
5944
  const newContig = this._seqTracker.getContiguousSeq(ns);
5713
5945
  this._clientLog.warn(`V2 push auto-pull failed: contiguous_seq=${contigBefore}->${newContig} err=${formatCaughtError(exc)}`);
5714
5946
  });