@agentunion/fastaun-browser 0.5.11 → 0.5.13

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 (47) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/_packed_docs/CHANGELOG.md +46 -0
  3. package/_packed_docs/INDEX.md +3 -3
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +1 -1
  5. package/_packed_docs/agent.md/SCHEMA.md +84 -84
  6. package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +8 -6
  7. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +29 -15
  8. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +21 -3
  9. package/_packed_docs/sdk/09-group-rpc-manual.md +29 -2
  10. package/dist/agent-md-schema.d.ts.map +1 -1
  11. package/dist/agent-md-schema.js +8 -4
  12. package/dist/agent-md-schema.js.map +1 -1
  13. package/dist/bundle.js +565 -298
  14. package/dist/client/delivery.d.ts +1 -0
  15. package/dist/client/delivery.d.ts.map +1 -1
  16. package/dist/client/delivery.js +161 -242
  17. package/dist/client/delivery.js.map +1 -1
  18. package/dist/client/group-state.d.ts.map +1 -1
  19. package/dist/client/group-state.js +1 -0
  20. package/dist/client/group-state.js.map +1 -1
  21. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  22. package/dist/client/rpc-pipeline.js +24 -11
  23. package/dist/client/rpc-pipeline.js.map +1 -1
  24. package/dist/client/v2-e2ee.d.ts +0 -1
  25. package/dist/client/v2-e2ee.d.ts.map +1 -1
  26. package/dist/client/v2-e2ee.js +65 -44
  27. package/dist/client/v2-e2ee.js.map +1 -1
  28. package/dist/client.d.ts +14 -0
  29. package/dist/client.d.ts.map +1 -1
  30. package/dist/client.js +341 -18
  31. package/dist/client.js.map +1 -1
  32. package/dist/facades.d.ts +7 -0
  33. package/dist/facades.d.ts.map +1 -1
  34. package/dist/facades.js +44 -0
  35. package/dist/facades.js.map +1 -1
  36. package/dist/seq-tracker.d.ts +2 -2
  37. package/dist/seq-tracker.d.ts.map +1 -1
  38. package/dist/seq-tracker.js +11 -7
  39. package/dist/seq-tracker.js.map +1 -1
  40. package/dist/tools/cross-sdk-agent.js +50 -12
  41. package/dist/tools/cross-sdk-agent.js.map +1 -1
  42. package/dist/transport.d.ts.map +1 -1
  43. package/dist/transport.js +3 -0
  44. package/dist/transport.js.map +1 -1
  45. package/dist/version.d.ts +1 -1
  46. package/dist/version.js +1 -1
  47. package/package.json +1 -1
@@ -58,6 +58,10 @@ function positiveSafeSequenceHint(value) {
58
58
  const parsed = Number(value);
59
59
  return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 0;
60
60
  }
61
+ function hasNonEmptyInlineMessage(value) {
62
+ return isJsonObject(value)
63
+ && Object.keys(value).length > 0;
64
+ }
61
65
  function nonNegativeSafeSequenceHint(value) {
62
66
  if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
63
67
  return null;
@@ -981,14 +985,22 @@ export class MessageDeliveryEngine {
981
985
  publishedCount += 1;
982
986
  }
983
987
  }
988
+ else if (this.isSelfSentGroupMessage(message)) {
989
+ this.markPublishedSeq(ns, seq);
990
+ }
984
991
  else if (await this.publishPulledMessage('group.message_created', ns, seq, message, false)) {
985
992
  this.ensurePullOperationCurrent();
986
993
  publishedCount += 1;
987
994
  }
988
995
  }
989
996
  this.ensurePullOperationCurrent();
990
- if (messages.length > 0)
997
+ const contiguousSeq = Number.isSafeInteger(result.contiguous_seq)
998
+ ? result.contiguous_seq
999
+ : undefined;
1000
+ if (contiguousSeq === undefined)
991
1001
  client._seqTracker.onPullResult(ns, messages, afterSeq);
1002
+ else
1003
+ client._seqTracker.onPullResult(ns, messages, afterSeq, contiguousSeq);
992
1004
  const commitTarget = Math.max(client._seqTracker.getContiguousSeq(ns), retentionFloor, visibilityFloor, deferredServerCursor);
993
1005
  if (commitTarget > client._seqTracker.getContiguousSeq(ns)) {
994
1006
  client._seqTracker.forceContiguousSeq(ns, commitTarget);
@@ -1536,11 +1548,7 @@ export class MessageDeliveryEngine {
1536
1548
  return true;
1537
1549
  }
1538
1550
  async onRawGroupMessageRecalled(data) {
1539
- // 在线 push 是实时通道,与 pull 兜底的双 tombstone 互补。push 携带的 seq 是通知
1540
- // tombstone 的 notice_seq;必须像普通群消息 push 一样推进 seqTracker + markPublished +
1541
- // auto-ack,否则该 seq 在本地 contiguous 序列留洞,后续 pull/reconnect 会重复拉到并
1542
- // 重复处理。publishGroupRecallTombstone 内部再按 (group_id, message_ids) 去重,
1543
- // 确保应用层只回调一次。
1551
+ // inline push 仅作为实时通知,不推进 seqTracker/cursor/ACK。
1544
1552
  const client = this.runtime.client;
1545
1553
  if (!isJsonObject(data))
1546
1554
  return;
@@ -1571,39 +1579,11 @@ export class MessageDeliveryEngine {
1571
1579
  return;
1572
1580
  }
1573
1581
  const ns = `group:${groupId}`;
1574
- if (seqNum > 0) {
1575
- client._seqTracker.updateMaxSeen(ns, seqNum);
1576
- if (client._seqTracker.getContiguousSeq(ns) === seqNum) {
1577
- // 已覆盖(pull 先到并推进过),仍走去重发布兜底,不重复推进 seq。
1578
- await this.publishGroupRecallTombstone(groupId, seq, wrapped);
1579
- return;
1580
- }
1581
- client._repairPushContiguousBound(ns, seqNum, true, '_raw.group.message_recalled');
1582
- }
1583
- // 该 notice_seq 已由 pull 路径处理过(已发布或挂起待发布)时,去重发布兜底后返回。
1584
- const pushed = client._pushedSeqs.get(ns);
1585
- const pending = client._pendingOrderedMsgs.get(ns);
1586
- if (pushed?.has(seqNum) || pending?.has(seqNum)) {
1587
- await this.publishGroupRecallTombstone(groupId, seq, wrapped);
1588
- return;
1589
- }
1590
- const contigBefore = client._seqTracker.getContiguousSeq(ns);
1591
- client._seqTracker.onMessageSeq(ns, seqNum);
1582
+ client._seqTracker.updateMaxSeen(ns, seqNum);
1583
+ if (seqNum > client._seqTracker.getContiguousSeq(ns)
1584
+ && client._sessionOptions?.background_sync !== false)
1585
+ client._safeAsync(this.fillGroupGap(groupId));
1592
1586
  await this.publishGroupRecallTombstone(groupId, seq, wrapped);
1593
- this.markPublishedSeq(ns, seqNum);
1594
- const contig = client._seqTracker.getContiguousSeq(ns);
1595
- if (contig > 0) {
1596
- const ackSeq = this.clampAckSeq('group.ack_messages', 'msg_seq', ns, contig);
1597
- client._transport.call('group.ack_messages', {
1598
- group_id: groupId,
1599
- msg_seq: ackSeq,
1600
- device_id: client._deviceId,
1601
- slot_id: client._slotId,
1602
- _rpc_background: true,
1603
- }).catch((e) => { client._clientLog.warn('group recall auto-ack failed: group=' + groupId, e); });
1604
- }
1605
- if (contig !== contigBefore)
1606
- this.persistSeq(ns);
1607
1587
  }
1608
1588
  async publishAppEvent(event, payload, source = 'direct', ns = '', batch) {
1609
1589
  const client = this.runtime.client;
@@ -1684,6 +1664,21 @@ export class MessageDeliveryEngine {
1684
1664
  }
1685
1665
  return true;
1686
1666
  }
1667
+ isSelfSentGroupMessage(message) {
1668
+ if (!isJsonObject(message))
1669
+ return false;
1670
+ const client = this.runtime.client;
1671
+ const selfAid = String(client._aid ?? '').trim().toLowerCase();
1672
+ const senderAid = String(message.sender_aid ?? message.from_aid ?? message.from ?? '').trim().toLowerCase();
1673
+ if (!selfAid || senderAid !== selfAid)
1674
+ return false;
1675
+ const senderDevice = Object.prototype.hasOwnProperty.call(message, 'sender_device_id')
1676
+ ? message.sender_device_id
1677
+ : message.from_device_id;
1678
+ if (typeof senderDevice !== 'string' || !senderDevice.trim() || !String(client._deviceId ?? '').trim())
1679
+ return false;
1680
+ return senderDevice.trim() === String(client._deviceId).trim();
1681
+ }
1687
1682
  strictTargetString(source, key) {
1688
1683
  if (!Object.prototype.hasOwnProperty.call(source, key)) {
1689
1684
  return { present: false, valid: false, value: '' };
@@ -1753,13 +1748,12 @@ export class MessageDeliveryEngine {
1753
1748
  const ns = isJsonObject(data) && client._aid && data.seq !== undefined
1754
1749
  ? `p2p:${client._aid}`
1755
1750
  : '';
1756
- const seq = isJsonObject(data) && typeof data.seq === 'number' ? data.seq : 0;
1757
- if (client._v2Session && ns && Number.isSafeInteger(seq) && seq > 0
1758
- && this.messageTargetsCurrentInstance(data)) {
1751
+ const seq = isJsonObject(data) ? positiveSafeSequenceHint(data.seq) : 0;
1752
+ if (client._v2Session && ns && seq > 0 && this.messageTargetsCurrentInstance(data)) {
1759
1753
  client._seqTracker.updateMaxSeen(ns, seq);
1760
1754
  }
1761
1755
  const hasInlineObject = isJsonObject(data)
1762
- && isJsonObject(data.inline_message);
1756
+ && hasNonEmptyInlineMessage(data.inline_message);
1763
1757
  const inlineGeneration = hasInlineObject ? this.captureInlineGeneration() : undefined;
1764
1758
  const operation = () => this.processAndPublishMessage(data, inlineGeneration);
1765
1759
  client._safeAsync(client._v2Session && ns && !hasInlineObject
@@ -1776,33 +1770,15 @@ export class MessageDeliveryEngine {
1776
1770
  msg.type = 'message.recalled';
1777
1771
  if (!this.messageTargetsCurrentInstance(msg))
1778
1772
  return;
1779
- const seq = msg.seq;
1780
- if (seq !== undefined && seq !== null && client._aid) {
1773
+ const seq = positiveSafeSequenceHint(msg.seq);
1774
+ if (client._aid && seq > 0) {
1781
1775
  const ns = `p2p:${client._aid}`;
1782
- if (seq > 0)
1783
- client._seqTracker.updateMaxSeen(ns, seq);
1784
- const contigBefore = client._seqTracker.getContiguousSeq(ns);
1785
- const seqNeedsPull = client._seqTracker.onMessageSeq(ns, seq);
1786
- const published = await this.publishOrderedMessage('message.recalled', ns, seq, msg);
1787
- const contigAfter = client._seqTracker.getContiguousSeq(ns);
1788
- if (seqNeedsPull && !published && client._sessionOptions?.background_sync !== false) {
1776
+ client._seqTracker.updateMaxSeen(ns, seq);
1777
+ if (seq > client._seqTracker.getContiguousSeq(ns)
1778
+ && client._sessionOptions?.background_sync !== false)
1789
1779
  client._safeAsync(this.fillP2pGap());
1790
- }
1791
- const contig = client._seqTracker.getContiguousSeq(ns);
1792
- if (contig > 0) {
1793
- const ackSeq = this.clampAckSeq('message.ack', 'seq', ns, contig);
1794
- client._transport.call('message.ack', {
1795
- seq: ackSeq,
1796
- device_id: client._deviceId,
1797
- slot_id: client._slotId,
1798
- _rpc_background: true,
1799
- }).catch((e) => { client._clientLog.warn(`P2P recall auto-ack failed:${String(e)}`); });
1800
- }
1801
- if (contigAfter !== contigBefore)
1802
- this.persistSeq(ns);
1803
- return;
1804
1780
  }
1805
- await this.publishMessageRecallTombstone(seq, msg);
1781
+ await this.publishMessageRecallTombstone(msg.seq, msg);
1806
1782
  }
1807
1783
  async processAndPublishMessage(data, inlineGeneration) {
1808
1784
  const client = this.runtime.client;
@@ -1819,47 +1795,36 @@ export class MessageDeliveryEngine {
1819
1795
  // 新服务端为兼容旧 SDK 仍可能发送 message.received;V2 会话下必须
1820
1796
  // 在旧 seq tracking / ordered publish 之前转入前台 Tail 协调器。
1821
1797
  const legacyPushSeq = positiveSafeSequenceHint(msg.seq);
1822
- const hasInlineField = Object.prototype.hasOwnProperty.call(msg, 'inline_message');
1798
+ const hasInlineField = hasNonEmptyInlineMessage(msg.inline_message);
1823
1799
  if (client._v2Session && client._aid && (legacyPushSeq > 0 || hasInlineField)) {
1824
1800
  await this.processV2P2PNotification(msg, 'v1', inlineGeneration);
1825
1801
  return;
1826
1802
  }
1803
+ if (client._aid && legacyPushSeq > 0) {
1804
+ const ns = `p2p:${client._aid}`;
1805
+ client._seqTracker.updateMaxSeen(ns, legacyPushSeq);
1806
+ if (client._sessionOptions?.background_sync !== false)
1807
+ client._safeAsync(this.fillP2pGap());
1808
+ return;
1809
+ }
1827
1810
  const seq = msg.seq;
1828
1811
  const encryptedPush = client._isEncryptedPushMessage(msg);
1829
- if (seq !== undefined && seq !== null && client._aid) {
1830
- const ns = `p2p:${client._aid}`;
1831
- if (seq > 0)
1832
- client._seqTracker.updateMaxSeen(ns, seq);
1833
- const contigBefore = client._seqTracker.getContiguousSeq(ns);
1834
- const seqNeedsPull = client._seqTracker.onMessageSeq(ns, seq);
1835
- const published = encryptedPush
1836
- ? await client._publishEncryptedPushMessage('message.received', 'message.undecryptable', ns, seq, msg, false)
1837
- : await this.publishOrderedMessage('message.received', ns, seq, msg);
1838
- const contigAfter = client._seqTracker.getContiguousSeq(ns);
1839
- const needPull = seqNeedsPull && !published;
1840
- if (needPull && client._sessionOptions?.background_sync !== false) {
1812
+ const ns = client._aid ? `p2p:${client._aid}` : '';
1813
+ const seqNum = positiveSafeSequenceHint(seq);
1814
+ if (ns && seqNum > 0) {
1815
+ client._seqTracker.updateMaxSeen(ns, seqNum);
1816
+ if (seqNum > client._seqTracker.getContiguousSeq(ns)
1817
+ && client._sessionOptions?.background_sync !== false)
1841
1818
  client._safeAsync(this.fillP2pGap());
1842
- }
1843
- const contig = client._seqTracker.getContiguousSeq(ns);
1844
- if (contig > 0) {
1845
- const ackSeq = this.clampAckSeq('message.ack', 'seq', ns, contig);
1846
- client._transport.call('message.ack', {
1847
- seq: ackSeq,
1848
- device_id: client._deviceId,
1849
- slot_id: client._slotId,
1850
- _rpc_background: true,
1851
- }).catch((e) => { client._clientLog.warn(`P2P auto-ack failed:${String(e)}`); });
1852
- }
1853
- if (contigAfter !== contigBefore)
1854
- this.persistSeq(ns);
1855
- if (encryptedPush)
1856
- return;
1819
+ }
1820
+ if (encryptedPush) {
1821
+ await client._publishEncryptedPushMessage('message.received', 'message.undecryptable', ns, seq ?? 0, msg, false);
1822
+ return;
1823
+ }
1824
+ if (ns && seq !== undefined && seq !== null) {
1825
+ await this.publishOrderedMessage('message.received', ns, seq, msg);
1857
1826
  }
1858
1827
  else {
1859
- if (encryptedPush) {
1860
- await client._publishEncryptedPushMessage('message.received', 'message.undecryptable', '', seq ?? 0, msg, false);
1861
- return;
1862
- }
1863
1828
  await client._publishAppEvent('message.received', msg, 'push');
1864
1829
  }
1865
1830
  }
@@ -1884,15 +1849,14 @@ export class MessageDeliveryEngine {
1884
1849
  const client = this.runtime.client;
1885
1850
  client._clientLog.debug(`_onRawGroupMessageCreated enter: group_id=${data?.group_id ?? '-'} from=${data?.from ?? '-'} seq=${data?.seq ?? '-'}`);
1886
1851
  const groupId = isJsonObject(data) && typeof data.group_id === 'string' ? data.group_id.trim() : '';
1887
- const seq = isJsonObject(data) && typeof data.seq === 'number' ? data.seq : 0;
1888
1852
  const kind = isJsonObject(data) ? String(data.kind ?? '').trim() : '';
1889
- if (client._v2Session && groupId && kind !== 'group.online_unread_hint'
1890
- && Number.isSafeInteger(seq) && seq > 0) {
1891
- client._seqTracker.updateMaxSeen(`group:${groupId}`, seq);
1892
- }
1893
1853
  const ns = groupId ? `group:${groupId}` : '';
1854
+ const seq = isJsonObject(data) ? positiveSafeSequenceHint(data.seq) : 0;
1855
+ if (client._v2Session && ns && kind !== 'group.online_unread_hint' && seq > 0) {
1856
+ client._seqTracker.updateMaxSeen(ns, seq);
1857
+ }
1894
1858
  const hasInlineObject = isJsonObject(data)
1895
- && isJsonObject(data.inline_message);
1859
+ && hasNonEmptyInlineMessage(data.inline_message);
1896
1860
  const inlineGeneration = hasInlineObject ? this.captureInlineGeneration() : undefined;
1897
1861
  const operation = () => this.processAndPublishGroupMessage(data, inlineGeneration);
1898
1862
  client._safeAsync(client._v2Session && ns && !hasInlineObject
@@ -1917,74 +1881,61 @@ export class MessageDeliveryEngine {
1917
1881
  // V2 会话中旧 group.message_created 也只是 Head 通知;必须先 Tail,
1918
1882
  // 不能让 payload 路径直接推进旧 ordered publish 状态。
1919
1883
  const legacyPushSeq = positiveSafeSequenceHint(seq);
1920
- const hasInlineField = Object.prototype.hasOwnProperty.call(msg, 'inline_message');
1884
+ const hasInlineField = hasNonEmptyInlineMessage(msg.inline_message);
1921
1885
  if (client._v2Session && groupId && (legacyPushSeq > 0 || hasInlineField)) {
1922
1886
  await this.processGroupV2MessageCreated(msg, 'v1', inlineGeneration);
1923
1887
  return;
1924
1888
  }
1889
+ if (groupId && legacyPushSeq > 0) {
1890
+ const ns = `group:${groupId}`;
1891
+ client._seqTracker.updateMaxSeen(ns, legacyPushSeq);
1892
+ if (client._sessionOptions?.background_sync !== false)
1893
+ client._safeAsync(this.fillGroupGap(groupId));
1894
+ return;
1895
+ }
1925
1896
  if (payload === undefined || payload === null
1926
1897
  || (typeof payload === 'object' && Object.keys(payload).length === 0)) {
1927
- await this.autoPullGroupMessages(msg);
1898
+ if (client._v2Session)
1899
+ await this.autoPullGroupMessages(msg);
1900
+ else if (groupId && seq !== undefined && seq !== null) {
1901
+ const ns = `group:${groupId}`;
1902
+ const seqNum = positiveSafeSequenceHint(seq);
1903
+ if (seqNum > 0) {
1904
+ client._seqTracker.updateMaxSeen(ns, seqNum);
1905
+ if (seqNum > client._seqTracker.getContiguousSeq(ns)
1906
+ && client._sessionOptions?.background_sync !== false)
1907
+ client._safeAsync(this.fillGroupGap(groupId));
1908
+ }
1909
+ await this.publishOrderedMessage('group.message_created', ns, seq, msg);
1910
+ }
1911
+ else
1912
+ await client._publishAppEvent('group.message_created', msg, 'push');
1928
1913
  return;
1929
1914
  }
1930
1915
  const encryptedPush = client._isEncryptedPushMessage(msg);
1931
- if (groupId && seq !== undefined && seq !== null) {
1932
- const ns = `group:${groupId}`;
1933
- if (seq > 0)
1934
- client._seqTracker.updateMaxSeen(ns, seq);
1935
- const contigBefore = client._seqTracker.getContiguousSeq(ns);
1936
- const seqNeedsPull = client._seqTracker.onMessageSeq(ns, seq);
1937
- // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled,仍占 seq。
1938
- if (!encryptedPush && this.recallEventFromGroupMessage(msg)) {
1939
- await this.publishGroupRecallTombstone(groupId, seq, msg);
1940
- this.markPublishedSeq(ns, Number(seq));
1941
- const contigAfter = client._seqTracker.getContiguousSeq(ns);
1942
- const contig = client._seqTracker.getContiguousSeq(ns);
1943
- if (contig > 0) {
1944
- const ackSeq = this.clampAckSeq('group.ack_messages', 'msg_seq', ns, contig);
1945
- client._transport.call('group.ack_messages', {
1946
- group_id: groupId,
1947
- msg_seq: ackSeq,
1948
- device_id: client._deviceId,
1949
- slot_id: client._slotId,
1950
- _rpc_background: true,
1951
- }).catch((e) => { client._clientLog.warn('group recall auto-ack failed: group=' + groupId, e); });
1952
- }
1953
- if (contigAfter !== contigBefore)
1954
- this.persistSeq(ns);
1955
- return;
1956
- }
1957
- const published = encryptedPush
1958
- ? await client._publishEncryptedPushMessage('group.message_created', 'group.message_undecryptable', ns, seq, msg, true)
1959
- : await this.publishOrderedMessage('group.message_created', ns, seq, msg);
1960
- const contigAfter = client._seqTracker.getContiguousSeq(ns);
1961
- const needPull = seqNeedsPull && !published;
1962
- if (needPull && client._sessionOptions?.background_sync !== false) {
1916
+ const ns = groupId ? `group:${groupId}` : '';
1917
+ const seqNum = positiveSafeSequenceHint(seq);
1918
+ if (ns && seqNum > 0) {
1919
+ client._seqTracker.updateMaxSeen(ns, seqNum);
1920
+ if (seqNum > client._seqTracker.getContiguousSeq(ns)
1921
+ && client._sessionOptions?.background_sync !== false)
1963
1922
  client._safeAsync(this.fillGroupGap(groupId));
1964
- }
1965
- const contig = client._seqTracker.getContiguousSeq(ns);
1966
- if (contig > 0) {
1967
- const ackSeq = this.clampAckSeq('group.ack_messages', 'msg_seq', ns, contig);
1968
- client._transport.call('group.ack_messages', {
1969
- group_id: groupId,
1970
- msg_seq: ackSeq,
1971
- device_id: client._deviceId,
1972
- slot_id: client._slotId,
1973
- _rpc_background: true,
1974
- }).catch((e) => { client._clientLog.warn('group message auto-ack failed: group=' + groupId, e); });
1975
- }
1976
- if (contigAfter !== contigBefore)
1977
- this.persistSeq(ns);
1978
- if (encryptedPush)
1979
- return;
1980
1923
  }
1981
- else {
1982
- if (encryptedPush) {
1983
- await client._publishEncryptedPushMessage('group.message_created', 'group.message_undecryptable', '', seq ?? 0, msg, true);
1984
- return;
1985
- }
1986
- await client._publishAppEvent('group.message_created', msg, 'push');
1924
+ if (this.isSelfSentGroupMessage(msg))
1925
+ return;
1926
+ if (!encryptedPush && this.recallEventFromGroupMessage(msg)) {
1927
+ await this.publishGroupRecallTombstone(groupId, seq, msg);
1928
+ return;
1929
+ }
1930
+ if (encryptedPush) {
1931
+ await client._publishEncryptedPushMessage('group.message_created', 'group.message_undecryptable', ns, seq ?? 0, msg, true);
1932
+ return;
1933
+ }
1934
+ if (ns && seq !== undefined && seq !== null) {
1935
+ await this.publishOrderedMessage('group.message_created', ns, seq, msg);
1987
1936
  }
1937
+ else
1938
+ await client._publishAppEvent('group.message_created', msg, 'push');
1988
1939
  }
1989
1940
  catch (exc) {
1990
1941
  client._clientLog.warn(`group push processing failed:${String(exc)}`);
@@ -2192,6 +2143,7 @@ export class MessageDeliveryEngine {
2192
2143
  client._gapFillDone.add(dedupKey);
2193
2144
  this.runtime.delivery.setGapFillActive(true);
2194
2145
  let continuationAfterSeq = 0;
2146
+ let pendingAckSeq = 0;
2195
2147
  let failed = false;
2196
2148
  try {
2197
2149
  let nextAfterSeq = afterSeq;
@@ -2214,9 +2166,11 @@ export class MessageDeliveryEngine {
2214
2166
  return;
2215
2167
  const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
2216
2168
  const eventObjects = events.filter((evt) => isJsonObject(evt));
2217
- if (eventObjects.length > 0) {
2169
+ const contiguousSeq = Number.isSafeInteger(result.contiguous_seq) ? result.contiguous_seq : undefined;
2170
+ if (contiguousSeq === undefined)
2218
2171
  client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq);
2219
- }
2172
+ else
2173
+ client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq, contiguousSeq);
2220
2174
  const cursor = isJsonObject(result.cursor) ? result.cursor : null;
2221
2175
  const retentionFloor = Math.max(0, Number(result.retention_floor_event_seq ?? 0), Number(cursor?.retention_floor_seq ?? 0));
2222
2176
  const serverAckFloor = Math.max(0, Number(cursor?.current_seq ?? 0));
@@ -2264,35 +2218,35 @@ export class MessageDeliveryEngine {
2264
2218
  this.persistSeq(ns);
2265
2219
  }
2266
2220
  if (eventObjects.length > 0 && ackContig > 0 && ackContig !== pageContigBefore) {
2267
- try {
2268
- const ackPromise = client._transport.call('group.ack_events', {
2269
- group_id: groupId,
2270
- event_seq: ackContig,
2271
- device_id: client._deviceId,
2272
- slot_id: client._slotId,
2273
- _rpc_background: true,
2274
- });
2275
- if (singlePage)
2276
- await ackPromise;
2277
- else
2278
- ackPromise.catch((e) => { client._clientLog.warn('group event auto-ack failed: group=' + groupId, e); });
2279
- }
2280
- catch (e) {
2281
- client._clientLog.warn('group event auto-ack failed: group=' + groupId, e);
2282
- }
2221
+ pendingAckSeq = Math.max(pendingAckSeq, ackContig);
2283
2222
  }
2284
2223
  this.onPullPage(ns, publishedEventCount, result.has_more === true, eventObjects.length > 0 ? Number(eventObjects[eventObjects.length - 1].event_seq ?? 0) : undefined, typeof result.remaining === 'number' && Number.isSafeInteger(result.remaining) && result.remaining >= 0 ? result.remaining : null, eventObjects.length, ackContig, eventObjects.length > 0 ? Number(eventObjects[eventObjects.length - 1].event_seq ?? 0) : undefined);
2285
2224
  const nextAfter = Math.max(eventSeqs.length > 0 ? Math.max(...eventSeqs) : nextAfterSeq, nextAfterSeq);
2286
- if (singlePage && result.has_more === true && nextAfter > nextAfterSeq) {
2225
+ const needsMore = result.has_more === true || client._seqTracker.getMaxSeenSeq(ns) > nextAfter;
2226
+ if (singlePage && needsMore && nextAfter > nextAfterSeq) {
2287
2227
  continuationAfterSeq = nextAfter;
2288
2228
  }
2289
- if (eventObjects.length === 0 || nextAfter <= nextAfterSeq || result.has_more !== true)
2229
+ if (eventObjects.length === 0 || nextAfter <= nextAfterSeq || !needsMore)
2290
2230
  break;
2291
2231
  nextAfterSeq = nextAfter;
2292
2232
  }
2293
2233
  if (pageCount >= maxPages) {
2294
2234
  client._clientLog.warn(`group event gap fill reached max_pages=${maxPages} group=${groupId} after_seq=${nextAfterSeq}`);
2295
2235
  }
2236
+ if (pendingAckSeq > 0) {
2237
+ try {
2238
+ await client._transport.call('group.ack_events', {
2239
+ group_id: groupId,
2240
+ event_seq: pendingAckSeq,
2241
+ device_id: client._deviceId,
2242
+ slot_id: client._slotId,
2243
+ _rpc_background: true,
2244
+ });
2245
+ }
2246
+ catch (e) {
2247
+ client._clientLog.warn('group event auto-ack failed: group=' + groupId, e);
2248
+ }
2249
+ }
2296
2250
  }
2297
2251
  catch (exc) {
2298
2252
  failed = true;
@@ -2331,43 +2285,15 @@ export class MessageDeliveryEngine {
2331
2285
  this.enqueueOnlineUnreadEventHint(data);
2332
2286
  return;
2333
2287
  }
2334
- if (this.isSelfJoinGroupChanged(data)) {
2335
- const contig = client._seqTracker.getContiguousSeq(ns);
2336
- const maxSeen = client._seqTracker.getMaxSeenSeq(ns);
2337
- if (contig === 0 && maxSeen === 0 && eventSeq > 1) {
2338
- client._clientLog.debug(`group.changed self-join baseline: group=${groupId}, event_seq=${eventSeq}, baseline=${eventSeq - 1}`);
2339
- client._seqTracker.forceContiguousSeq(ns, eventSeq - 1);
2340
- }
2341
- }
2342
2288
  const contigBefore = client._seqTracker.getContiguousSeq(ns);
2289
+ client._seqTracker.updateMaxSeen(ns, eventSeq);
2343
2290
  const publishedDuplicate = client._pushedSeqs.get(ns)?.has(eventSeq) === true;
2344
2291
  if (eventSeq <= contigBefore || publishedDuplicate) {
2345
2292
  client._clientLog.debug(`group.changed skipped duplicate/stale: group=${groupId}, event_seq=${eventSeq}, contiguous=${contigBefore}`);
2346
- if (eventSeq <= contigBefore) {
2347
- this.fireGroupEventAck(groupId, ns, eventSeq, 'group event covered push ack');
2348
- }
2349
- else if (contigBefore > 0) {
2350
- this.fireGroupEventAck(groupId, ns, contigBefore, 'group event covered push ack');
2351
- }
2352
2293
  return;
2353
2294
  }
2354
2295
  this.enqueueOrderedMessage(ns, 'group.changed', eventSeq, data);
2355
- needPull = client._seqTracker.onMessageSeq(ns, eventSeq);
2356
- const ackContig = client._seqTracker.getContiguousSeq(ns);
2357
- await this.drainOrderedMessages(ns);
2358
- if (ackContig > 0 && ackContig !== contigBefore) {
2359
- if (data.action !== 'dissolved')
2360
- this.persistSeq(ns);
2361
- client._transport.call('group.ack_events', {
2362
- group_id: groupId,
2363
- event_seq: ackContig,
2364
- device_id: client._deviceId,
2365
- slot_id: client._slotId,
2366
- _rpc_background: true,
2367
- }).catch((e) => {
2368
- client._clientLog.warn('group event push auto-ack failed: group=' + groupId, e);
2369
- });
2370
- }
2296
+ needPull = eventSeq > contigBefore;
2371
2297
  if (needPull && groupId && !data._from_gap_fill && client._sessionOptions?.background_sync !== false) {
2372
2298
  client._safeAsync(this.fillGroupEventGap(groupId));
2373
2299
  }
@@ -2986,20 +2912,19 @@ export class MessageDeliveryEngine {
2986
2912
  if (forceTail || order[0] === 'tail')
2987
2913
  result = await run(true, false, undefined, false, forceTail);
2988
2914
  const state = this.syncState(ns);
2989
- // Tail 的连续性证明只覆盖其返回的 head。Tail 进行期间到达的新
2990
- // Push 不能再次进入 realtime planner(那会再做一次 Tail);以已经
2991
- // 落盘的 ack 为起点,至多补一页普通 Forward 即可恢复该小窗口。
2992
- const tailMissedDuringPull = tailCompleted
2993
- && Number(client._seqTracker.getMaxSeenSeq(ns) || 0) > state.head;
2994
2915
  const maxSeen = Number(client._seqTracker.getMaxSeenSeq(ns) || 0);
2916
+ // Tail 期间到达的新 Push 只记录在 pending Pull 上界,不写入 SeqTracker。
2917
+ const pendingUpper = Number(this.pendingP2pPullUpper?.get(ns) ?? 0);
2918
+ const observedUpper = Math.max(maxSeen, pendingUpper);
2919
+ const tailMissedDuringPull = tailCompleted && observedUpper > state.head;
2995
2920
  const pendingThroughSeq = tailMissedDuringPull
2996
- ? maxSeen
2921
+ ? observedUpper
2997
2922
  : 0;
2998
- const tailForward = tailCompleted && (state.ack < state.tail - 1 || tailMissedDuringPull);
2923
+ const tailForward = tailCompleted && (state.ack < state.head || tailMissedDuringPull);
2999
2924
  const backgroundWindowForward = order[0] === 'forward' && !headForward;
3000
2925
  if (headForward || tailForward
3001
2926
  || (client._sessionOptions?.background_sync !== false && backgroundWindowForward)) {
3002
- const forwardTarget = Math.max(headForward ? Math.max(pushSeq, maxSeen) : 0, (tailForward || backgroundWindowForward) ? state.tail - 1 : 0, tailMissedDuringPull ? maxSeen : 0);
2927
+ const forwardTarget = Math.max(headForward ? Math.max(pushSeq, maxSeen) : 0, (tailForward || backgroundWindowForward) ? state.tail - 1 : 0, tailMissedDuringPull ? observedUpper : 0);
3003
2928
  const forwardMaxPages = this.forwardMaxPages(state.ack, forwardTarget, pageLimit);
3004
2929
  result = await run(false, !(headForward || tailForward), forwardMaxPages, headForward || tailMissedDuringPull);
3005
2930
  if (pendingThroughSeq > 0) {
@@ -3061,17 +2986,18 @@ export class MessageDeliveryEngine {
3061
2986
  const state = this.syncState(ns);
3062
2987
  // 与 P2P 相同:真实完成 Tail 后,至多用一页 Forward 收敛 Tail
3063
2988
  // 期间新到达的序号;绝不重新规划或再执行 Tail。
3064
- const tailMissedDuringPull = tailCompleted
3065
- && Number(client._seqTracker.getMaxSeenSeq(ns) || 0) > state.head;
3066
2989
  const maxSeen = Number(client._seqTracker.getMaxSeenSeq(ns) || 0);
2990
+ const pendingUpper = Number(this.pendingGroupPullUpper?.get(ns) ?? 0);
2991
+ const observedUpper = Math.max(maxSeen, pendingUpper);
2992
+ const tailMissedDuringPull = tailCompleted && observedUpper > state.head;
3067
2993
  const pendingThroughSeq = tailMissedDuringPull
3068
- ? maxSeen
2994
+ ? observedUpper
3069
2995
  : 0;
3070
- const tailForward = tailCompleted && (state.ack < state.tail - 1 || tailMissedDuringPull);
2996
+ const tailForward = tailCompleted && (state.ack < state.head || tailMissedDuringPull);
3071
2997
  const backgroundWindowForward = order[0] === 'forward' && !headForward;
3072
2998
  if (headForward || tailForward
3073
2999
  || (client._sessionOptions?.background_sync !== false && backgroundWindowForward)) {
3074
- const forwardTarget = Math.max(headForward ? Math.max(pushSeq, maxSeen) : 0, (tailForward || backgroundWindowForward) ? state.tail - 1 : 0, tailMissedDuringPull ? maxSeen : 0);
3000
+ const forwardTarget = Math.max(headForward ? Math.max(pushSeq, maxSeen) : 0, (tailForward || backgroundWindowForward) ? state.tail - 1 : 0, tailMissedDuringPull ? observedUpper : 0);
3075
3001
  const forwardMaxPages = this.forwardMaxPages(state.ack, forwardTarget, pageLimit);
3076
3002
  result = await run(false, !(headForward || tailForward), forwardMaxPages, headForward || tailMissedDuringPull);
3077
3003
  if (pendingThroughSeq > 0) {
@@ -3128,15 +3054,13 @@ export class MessageDeliveryEngine {
3128
3054
  const groupId = typeof row.group_id === 'string' ? row.group_id.trim() : '';
3129
3055
  if (!groupId)
3130
3056
  return;
3131
- const seq = typeof row.seq === 'number' ? row.seq : 0;
3132
3057
  const eventKind = String(row.kind ?? '').trim();
3133
3058
  const ns = this.resolvePendingGroupInlineAckNamespace(`group:${groupId}`);
3134
- if (client._v2Session && eventKind !== 'group.online_unread_hint'
3135
- && Number.isSafeInteger(seq) && seq > 0) {
3136
- // 必须在进入 namespace 队列前登记,才能让在途 Tail 看见后来 Push。
3059
+ const seq = positiveSafeSequenceHint(row.seq);
3060
+ if (client._v2Session && eventKind !== 'group.online_unread_hint' && seq > 0) {
3137
3061
  client._seqTracker.updateMaxSeen(ns, seq);
3138
3062
  }
3139
- const hasInlineObject = isJsonObject(row.inline_message)
3063
+ const hasInlineObject = hasNonEmptyInlineMessage(row.inline_message)
3140
3064
  && eventKind !== 'group.online_unread_hint';
3141
3065
  const inlineGeneration = hasInlineObject ? this.captureInlineGeneration() : undefined;
3142
3066
  const operation = () => this.processGroupV2MessageCreated(data, 'v2', inlineGeneration);
@@ -3153,7 +3077,6 @@ export class MessageDeliveryEngine {
3153
3077
  return;
3154
3078
  const groupId = typeof data.group_id === 'string' ? data.group_id.trim() : '';
3155
3079
  const seq = positiveSafeSequenceHint(data.seq);
3156
- const strictSeq = typeof data.seq === 'number' && Number.isSafeInteger(data.seq) && data.seq > 0;
3157
3080
  if (!groupId)
3158
3081
  return;
3159
3082
  const eventKind = String(data.kind ?? '').trim();
@@ -3165,12 +3088,13 @@ export class MessageDeliveryEngine {
3165
3088
  return;
3166
3089
  }
3167
3090
  const ns = this.resolvePendingGroupInlineAckNamespace(`group:${groupId}`);
3168
- const inlinePresent = Object.prototype.hasOwnProperty.call(data, 'inline_message');
3091
+ const inlinePresent = hasNonEmptyInlineMessage(data.inline_message);
3169
3092
  if (seq <= 0) {
3170
3093
  if (inlinePresent)
3171
3094
  await this.recoverInvalidGroupRealtimeHead(groupId, ns, data.seq);
3172
3095
  return;
3173
3096
  }
3097
+ client._seqTracker.updateMaxSeen(ns, seq);
3174
3098
  const inline = this.inlineMessage(data);
3175
3099
  const inlineMatches = inline !== null
3176
3100
  && this.inlineGroupBindingMatches(data, inline, groupId, seq, expectedInlineVersion);
@@ -3189,15 +3113,13 @@ export class MessageDeliveryEngine {
3189
3113
  const before = this.syncState(ns);
3190
3114
  // 无正文的旧服务端 Head 可把 JSON-safe 数字字符串作为恢复 hint;
3191
3115
  // inline 绑定仍要求原始 seq 是严格 number,避免类型归一化后消费正文。
3192
- if (strictSeq || !inlinePresent)
3193
- client._seqTracker.updateMaxSeen(ns, seq);
3194
3116
  if (seq <= before.ack && before.ack >= before.tail - 1) {
3195
3117
  const canonicalNs = this.resolvePendingGroupInlineAckNamespace(ns);
3196
3118
  const pending = this.pendingGroupInlineAcks?.get(canonicalNs);
3197
- if (pending && seq >= pending.seq) {
3119
+ if (inlinePresent && pending && seq >= pending.seq) {
3198
3120
  this.retryPendingGroupInlineAck(canonicalNs, pending.seq, 'covered inline Group ack', seq);
3199
3121
  }
3200
- else if (!inlinePresent || inlineMatches) {
3122
+ else if (inlineMatches) {
3201
3123
  this.fireGroupV2Ack(groupId, ns, seq, 'group v2 covered push ack', inlineMatches ? inlineGeneration : null);
3202
3124
  }
3203
3125
  return;
@@ -3233,11 +3155,10 @@ export class MessageDeliveryEngine {
3233
3155
  }
3234
3156
  const pushSeq = isJsonObject(data) && typeof data.seq === 'number' ? data.seq : 0;
3235
3157
  if (client._v2Session && Number.isSafeInteger(pushSeq) && pushSeq > 0) {
3236
- // 与 Group 相同,在排队前暴露更高 Head 给当前在途 Tail。
3237
3158
  client._seqTracker.updateMaxSeen(ns, pushSeq);
3238
3159
  }
3239
3160
  const hasInlineObject = isJsonObject(data)
3240
- && isJsonObject(data.inline_message);
3161
+ && hasNonEmptyInlineMessage(data.inline_message);
3241
3162
  const inlineGeneration = hasInlineObject ? this.captureInlineGeneration() : undefined;
3242
3163
  const operation = () => this.processV2P2PNotification(data, 'v2', inlineGeneration);
3243
3164
  if (hasInlineObject)
@@ -3252,12 +3173,12 @@ export class MessageDeliveryEngine {
3252
3173
  if (!client._v2Session)
3253
3174
  return;
3254
3175
  const pushSeq = isJsonObject(data) ? positiveSafeSequenceHint(data.seq) : 0;
3255
- const strictSeq = isJsonObject(data)
3256
- && typeof data.seq === 'number' && Number.isSafeInteger(data.seq) && data.seq > 0;
3257
3176
  const ns = client._aid ? `p2p:${client._aid}` : '';
3258
3177
  if (!ns)
3259
3178
  return;
3260
- const inlinePresent = isJsonObject(data) && Object.prototype.hasOwnProperty.call(data, 'inline_message');
3179
+ if (pushSeq > 0)
3180
+ client._seqTracker.updateMaxSeen(ns, pushSeq);
3181
+ const inlinePresent = isJsonObject(data) && hasNonEmptyInlineMessage(data.inline_message);
3261
3182
  if (pushSeq <= 0) {
3262
3183
  if (inlinePresent)
3263
3184
  await this.recoverInvalidP2PRealtimeHead(ns, isJsonObject(data) ? data.seq : undefined);
@@ -3277,11 +3198,9 @@ export class MessageDeliveryEngine {
3277
3198
  if (inlineMatches && !this.isInlineGenerationCurrent(inlineGeneration))
3278
3199
  return;
3279
3200
  const before = this.syncState(ns);
3280
- if (strictSeq || !inlinePresent)
3281
- client._seqTracker.updateMaxSeen(ns, pushSeq);
3282
3201
  if (pushSeq <= before.ack && before.ack >= before.tail - 1) {
3283
3202
  const pending = this.pendingP2PInlineAcks?.get(ns);
3284
- if (pending && pushSeq >= pending.seq) {
3203
+ if (inlinePresent && pending && pushSeq >= pending.seq) {
3285
3204
  this.retryPendingP2PInlineAck(ns, pending.seq, 'covered inline P2P ack', pushSeq);
3286
3205
  }
3287
3206
  else if (inlineMatches) {