@agentunion/fastaun 0.5.9 → 0.5.11

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 (43) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/_packed_docs/CHANGELOG.md +64 -0
  3. package/_packed_docs/INDEX.md +3 -3
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +1 -1
  5. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +6 -5
  6. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +102 -8
  7. package/_packed_docs/sdk/09-group-rpc-manual.md +18 -3
  8. package/_packed_docs/sdk/09-message-rpc-manual.md +34 -10
  9. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +3 -2
  10. package/_packed_docs/sdk/INDEX.md +2 -1
  11. package/dist/agent-md.js +5 -1
  12. package/dist/agent-md.js.map +1 -1
  13. package/dist/auth.d.ts +2 -1
  14. package/dist/auth.js +30 -44
  15. package/dist/auth.js.map +1 -1
  16. package/dist/client/delivery.d.ts +39 -9
  17. package/dist/client/delivery.js +407 -73
  18. package/dist/client/delivery.js.map +1 -1
  19. package/dist/client/group-state.js +6 -6
  20. package/dist/client/group-state.js.map +1 -1
  21. package/dist/client/lifecycle.js +5 -14
  22. package/dist/client/lifecycle.js.map +1 -1
  23. package/dist/client/rpc-pipeline.d.ts +4 -0
  24. package/dist/client/rpc-pipeline.js +51 -5
  25. package/dist/client/rpc-pipeline.js.map +1 -1
  26. package/dist/client/v2-e2ee.d.ts +3 -0
  27. package/dist/client/v2-e2ee.js +236 -59
  28. package/dist/client/v2-e2ee.js.map +1 -1
  29. package/dist/client.d.ts +1 -0
  30. package/dist/client.js +84 -30
  31. package/dist/client.js.map +1 -1
  32. package/dist/events.d.ts +23 -8
  33. package/dist/events.js +120 -26
  34. package/dist/events.js.map +1 -1
  35. package/dist/register-flow.js +15 -111
  36. package/dist/register-flow.js.map +1 -1
  37. package/dist/transport.d.ts +36 -3
  38. package/dist/transport.js +855 -33
  39. package/dist/transport.js.map +1 -1
  40. package/dist/version.d.ts +1 -1
  41. package/dist/version.js +1 -1
  42. package/dist/version.js.map +1 -1
  43. package/package.json +1 -1
@@ -298,6 +298,10 @@ export class V2E2EECoordinator {
298
298
  this.pendingForwardCursors.clear();
299
299
  this.pendingForwardAcks.clear();
300
300
  }
301
+ hasPendingSenderIKWork() {
302
+ return this.senderIKSyncFetches.size > 0
303
+ || Boolean(this.client._v2SenderIKFetching?.size);
304
+ }
301
305
  pendingForwardCursor(ns, localAck) {
302
306
  if (!ns)
303
307
  return 0;
@@ -648,6 +652,7 @@ export class V2E2EECoordinator {
648
652
  finally {
649
653
  if (this.senderIKSyncFetches.get(fetchKey) === task)
650
654
  this.senderIKSyncFetches.delete(fetchKey);
655
+ client._rpcPipeline?.requestPullDrainedCheck?.();
651
656
  }
652
657
  }
653
658
  scheduleSenderIKPending(args) {
@@ -679,6 +684,10 @@ export class V2E2EECoordinator {
679
684
  }
680
685
  async resolveSenderIKPending(fromAid, senderDeviceId, groupId, fetchKey) {
681
686
  const client = this.client;
687
+ const generation = Number(client._v2RuntimeGeneration ?? 0);
688
+ const pending = client._v2SenderIKPending;
689
+ const fetching = client._v2SenderIKFetching;
690
+ const generationIsCurrent = () => Number(client._v2RuntimeGeneration ?? 0) === generation;
682
691
  try {
683
692
  const session = client._v2Session;
684
693
  if (session && fromAid) {
@@ -718,18 +727,23 @@ export class V2E2EECoordinator {
718
727
  await this.getV2SenderPubDer(fromAid, senderDeviceId);
719
728
  }
720
729
  }
721
- const pendingItems = [...client._v2SenderIKPending.entries()].filter(([, entry]) => entry.fromAid === fromAid && entry.senderDeviceId === senderDeviceId && entry.groupId === groupId);
730
+ if (!generationIsCurrent())
731
+ return;
732
+ const pendingItems = [...pending.entries()].filter(([, entry]) => entry.fromAid === fromAid && entry.senderDeviceId === senderDeviceId && entry.groupId === groupId);
722
733
  for (const [key, entry] of pendingItems) {
723
734
  let plaintext = null;
724
735
  const retryStatus = {};
725
736
  try {
726
- plaintext = await client._decryptV2Message(entry.msg, false, true, true, true, retryStatus);
737
+ plaintext = await client._decryptV2Message(entry.msg, false, true, true, true, retryStatus, false, 'pending_retry');
727
738
  }
728
739
  catch (exc) {
729
740
  client._clientLog.warn(`V2 sender IK pending retry raised: key=${key} err=${formatE2EEError(exc)}`);
730
- client._v2SenderIKPending.delete(key);
741
+ if (generationIsCurrent())
742
+ pending.delete(key);
731
743
  continue;
732
744
  }
745
+ if (!generationIsCurrent())
746
+ return;
733
747
  if (plaintext === null) {
734
748
  if (retryStatus.deferred) {
735
749
  client._clientLog.warn(`V2 pending retry still missing key material: key=${key}`);
@@ -741,24 +755,33 @@ export class V2E2EECoordinator {
741
755
  const safeEvent = this.safeUndecryptablePushEvent(entry.msg, isGroup);
742
756
  safeEvent._decrypt_error = 'sender IK pending retry failed';
743
757
  safeEvent._decrypt_stage = 'sender_ik_pending_retry';
744
- await client._publishAppEvent(eventName, safeEvent, 'pending_retry');
758
+ try {
759
+ await client._publishAppEvent(eventName, safeEvent, 'pending_retry');
760
+ }
761
+ finally {
762
+ if (generationIsCurrent())
763
+ pending.delete(key);
764
+ }
745
765
  continue;
746
766
  }
747
- client._v2SenderIKPending.delete(key);
767
+ pending.delete(key);
748
768
  const seq = Number(entry.msg.seq ?? 0);
749
769
  if (entry.groupId) {
750
770
  plaintext = normalizeGroupMentionMode(plaintext, entry.msg, entry.msg.envelope_json);
751
771
  plaintext.group_id = entry.groupId;
752
- await client._publishPulledMessage('group.message_created', `group:${entry.groupId}`, seq, plaintext);
772
+ await client._publishPulledMessage('group.message_created', `group:${entry.groupId}`, seq, plaintext, false, 'pending_retry');
753
773
  }
754
774
  else {
755
- await client._publishPulledMessage('message.received', `p2p:${client._aid ?? ''}`, seq, plaintext);
775
+ await client._publishPulledMessage('message.received', `p2p:${client._aid ?? ''}`, seq, plaintext, false, 'pending_retry');
756
776
  }
757
777
  client._clientLog.debug(`V2 sender IK pending retry delivered: key=${key}`);
758
778
  }
759
779
  }
760
780
  finally {
761
- client._v2SenderIKFetching.delete(fetchKey);
781
+ if (generationIsCurrent() && client._v2SenderIKFetching === fetching) {
782
+ fetching.delete(fetchKey);
783
+ client._rpcPipeline?.requestPullDrainedCheck?.();
784
+ }
762
785
  }
763
786
  }
764
787
  async buildV2P2PEnvelope(opts) {
@@ -924,9 +947,10 @@ export class V2E2EECoordinator {
924
947
  throw new ValidationError("message.send requires 'to'");
925
948
  if (!isJsonObject(payload))
926
949
  throw new ValidationError('message.send payload must be a dict for V2 encryption');
950
+ const messageId = String(opts?.messageId ?? '').trim() || `m-${crypto.randomUUID().replace(/-/g, '')}`;
927
951
  client._logMessageDebug('send-plaintext', 'message.send.v2', 'message.send', {
928
952
  to: toAid,
929
- message_id: opts?.messageId ?? '',
953
+ message_id: messageId,
930
954
  payload,
931
955
  }, { payloadOverride: payload });
932
956
  const attempt = async (useCache) => {
@@ -934,7 +958,7 @@ export class V2E2EECoordinator {
934
958
  const envelope = await client._buildV2P2PEnvelope({
935
959
  to: toAid,
936
960
  payload,
937
- messageId: opts?.messageId,
961
+ messageId,
938
962
  timestamp: opts?.timestamp,
939
963
  protectedHeaders: opts?.protectedHeaders,
940
964
  context: opts?.context,
@@ -943,6 +967,7 @@ export class V2E2EECoordinator {
943
967
  const result = await client.call('message.send', {
944
968
  to: toAid,
945
969
  payload: envelope,
970
+ message_id: messageId,
946
971
  encrypt: false,
947
972
  _skip_send_result_envelope: true,
948
973
  });
@@ -954,7 +979,7 @@ export class V2E2EECoordinator {
954
979
  return client._delivery.attachSendResultEnvelope('message.send', {
955
980
  to: toAid,
956
981
  payload,
957
- message_id: opts?.messageId,
982
+ message_id: messageId,
958
983
  timestamp: opts?.timestamp,
959
984
  protected_headers: opts?.protectedHeaders,
960
985
  context: opts?.context,
@@ -969,7 +994,7 @@ export class V2E2EECoordinator {
969
994
  return client._delivery.attachSendResultEnvelope('message.send', {
970
995
  to: toAid,
971
996
  payload,
972
- message_id: opts?.messageId,
997
+ message_id: messageId,
973
998
  timestamp: opts?.timestamp,
974
999
  protected_headers: opts?.protectedHeaders,
975
1000
  context: opts?.context,
@@ -993,10 +1018,10 @@ export class V2E2EECoordinator {
993
1018
  if (String(msg.version ?? '') === 'v1') {
994
1019
  const legacy = isJsonObject(msg.legacy_v1)
995
1020
  ? msg.legacy_v1 : {};
996
- const payload = legacy.payload;
1021
+ const payload = legacy.payload !== undefined ? legacy.payload : msg.payload;
997
1022
  const payloadType = isJsonObject(payload)
998
1023
  ? String(payload.type ?? '').trim() : '';
999
- if (payload !== undefined && payload !== null && !['e2ee.encrypted', 'e2ee.group_encrypted'].includes(payloadType)) {
1024
+ if (payload !== undefined && payload !== null && !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
1000
1025
  plaintext = {
1001
1026
  message_id: String(msg.message_id ?? ''),
1002
1027
  from: String(msg.from_aid ?? ''),
@@ -1009,9 +1034,14 @@ export class V2E2EECoordinator {
1009
1034
  };
1010
1035
  attachGatewayProximity(plaintext, msg);
1011
1036
  }
1037
+ else if (isJsonObject(payload)
1038
+ && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1039
+ msg = { ...msg, envelope_json: JSON.stringify(payload) };
1040
+ plaintext = await client._decryptV2Message(msg, false, !history, true, true, undefined, false, source);
1041
+ }
1012
1042
  }
1013
1043
  else {
1014
- plaintext = await client._decryptV2Message(msg, false, !history);
1044
+ plaintext = await client._decryptV2Message(msg, false, !history, true, true, undefined, false, source);
1015
1045
  }
1016
1046
  if (!plaintext) {
1017
1047
  if (history)
@@ -1022,7 +1052,7 @@ export class V2E2EECoordinator {
1022
1052
  if (publish) {
1023
1053
  const event = client._delivery.p2pAppEventForMessage(plaintext);
1024
1054
  if (orderedPublish) {
1025
- await client._delivery.publishOrderedMessage(event.event, client._aid ? `p2p:${client._aid}` : '', seq, event.payload);
1055
+ await client._delivery.publishOrderedMessage(event.event, client._aid ? `p2p:${client._aid}` : '', seq, event.payload, source);
1026
1056
  }
1027
1057
  else {
1028
1058
  await client._publishAppEvent(event.event, event.payload, source);
@@ -1036,35 +1066,57 @@ export class V2E2EECoordinator {
1036
1066
  let plaintext = null;
1037
1067
  if (String(msg.version ?? '') === 'v1') {
1038
1068
  const payload = msg.payload;
1069
+ const recall = typeof client._delivery?.recallEventFromGroupMessage === 'function'
1070
+ ? client._delivery.recallEventFromGroupMessage(msg)
1071
+ : null;
1072
+ if (recall) {
1073
+ const recallPayload = {
1074
+ ...recall,
1075
+ group_id: groupId,
1076
+ group_aid: groupAid,
1077
+ seq,
1078
+ };
1079
+ if (publish) {
1080
+ if (orderedPublish) {
1081
+ await client._delivery.publishOrderedMessage('group.message_recalled', `group:${groupId}`, seq, recallPayload, source);
1082
+ }
1083
+ else {
1084
+ await client._publishAppEvent('group.message_recalled', recallPayload, source);
1085
+ }
1086
+ }
1087
+ return recallPayload;
1088
+ }
1039
1089
  if (payload === undefined || payload === null) {
1040
1090
  if (history)
1041
1091
  return this.historyDecryptFailure(msg, 'legacy payload is missing');
1042
1092
  client._clientLog.warn(`Group Tail 缺少 payload 的 V1 行已跳过,连续性证明仍保留: group=${groupId}, seq=${seq}`);
1043
1093
  return null;
1044
1094
  }
1095
+ const payloadType = isJsonObject(payload)
1096
+ ? String(payload.type ?? '').trim() : '';
1045
1097
  if (isJsonObject(payload)
1046
- && ['e2ee.encrypted', 'e2ee.group_encrypted'].includes(String(payload.type ?? ''))) {
1047
- if (history)
1048
- return this.historyDecryptFailure(msg, 'unsupported legacy encrypted envelope');
1049
- client._clientLog.warn(`Group Tail 不支持的旧加密消息已跳过,连续性证明仍保留: group=${groupId}, seq=${seq}`);
1050
- return null;
1098
+ && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1099
+ msg = { ...msg, envelope_json: JSON.stringify(payload) };
1100
+ plaintext = await client._decryptV2Message(msg, false, !history, true, true, undefined, false, source);
1101
+ }
1102
+ else {
1103
+ plaintext = {
1104
+ message_id: String(msg.message_id ?? ''),
1105
+ from: String(msg.from_aid ?? ''),
1106
+ group_id: groupId,
1107
+ group_aid: groupAid,
1108
+ seq,
1109
+ type: String(msg.type ?? ''),
1110
+ timestamp: msg.t_server,
1111
+ payload: payload,
1112
+ encrypted: false,
1113
+ };
1114
+ plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
1115
+ attachGatewayProximity(plaintext, msg);
1051
1116
  }
1052
- plaintext = {
1053
- message_id: String(msg.message_id ?? ''),
1054
- from: String(msg.from_aid ?? ''),
1055
- group_id: groupId,
1056
- group_aid: groupAid,
1057
- seq,
1058
- type: String(msg.type ?? ''),
1059
- timestamp: msg.t_server,
1060
- payload: payload,
1061
- encrypted: false,
1062
- };
1063
- plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
1064
- attachGatewayProximity(plaintext, msg);
1065
1117
  }
1066
1118
  else {
1067
- plaintext = await client._decryptV2Message(msg, false, !history);
1119
+ plaintext = await client._decryptV2Message(msg, false, !history, true, true, undefined, false, source);
1068
1120
  if (plaintext) {
1069
1121
  plaintext.group_id = groupId;
1070
1122
  plaintext.group_aid = groupAid;
@@ -1079,7 +1131,7 @@ export class V2E2EECoordinator {
1079
1131
  }
1080
1132
  if (publish) {
1081
1133
  if (orderedPublish) {
1082
- await client._delivery.publishOrderedMessage('group.message_created', `group:${groupId}`, seq, plaintext);
1134
+ await client._delivery.publishOrderedMessage('group.message_created', `group:${groupId}`, seq, plaintext, source);
1083
1135
  }
1084
1136
  else {
1085
1137
  await client._publishAppEvent('group.message_created', plaintext, source);
@@ -1104,6 +1156,8 @@ export class V2E2EECoordinator {
1104
1156
  const afterSeq = strictWindowSeq(params.after_seq ?? 0, 'after_seq');
1105
1157
  const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
1106
1158
  const ns = client._aid ? `p2p:${client._aid}` : '';
1159
+ if (ns)
1160
+ client._delivery.syncStarted(ns);
1107
1161
  const result = await client._callRawV2Rpc('message.v2.pull', { window_mode: 'tail', after_seq: afterSeq, limit, _rpc_foreground: true });
1108
1162
  const floor = Number(result.retention_floor_seq ?? 0);
1109
1163
  const previous = ns ? client._seqTracker.getSyncState(ns) : null;
@@ -1128,6 +1182,15 @@ export class V2E2EECoordinator {
1128
1182
  const ack = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
1129
1183
  if (previous && ack > previous.ack)
1130
1184
  await this.ackV2(ack);
1185
+ if (ns)
1186
+ client._delivery.syncProgress(ns, {
1187
+ rawCount: page.messages.length,
1188
+ pulledCount: decoded.length,
1189
+ remaining: result.remaining,
1190
+ hasMore: typeof result.has_more === 'boolean' ? result.has_more : undefined,
1191
+ currentSeq: ack,
1192
+ targetSeq: page.head,
1193
+ });
1131
1194
  return { ...result, messages: decoded, raw_count: page.messages.length };
1132
1195
  }
1133
1196
  async historyV2Internal(params) {
@@ -1144,21 +1207,23 @@ export class V2E2EECoordinator {
1144
1207
  }
1145
1208
  return { ...result, messages: decoded };
1146
1209
  }
1147
- async pullV2(afterSeq = 0, limit = 50, opts) {
1210
+ async pullV2Impl(afterSeq = 0, limit = 50, opts) {
1148
1211
  const client = this.client;
1149
1212
  await client._ensureV2SessionReady('message.pull');
1150
1213
  const ns = client._aid ? `p2p:${client._aid}` : '';
1214
+ if (ns)
1215
+ client._delivery.syncStarted(ns);
1151
1216
  if (opts?.windowMode === 'tail') {
1152
1217
  if (!opts.gateLocked) {
1153
1218
  const key = `${ns}|tail|after=${afterSeq}|limit=${limit}`;
1154
- return (await client._runPullSerialized(key, () => this.pullV2(afterSeq, limit, { ...opts, gateLocked: true }), false));
1219
+ return (await client._runPullSerialized(key, () => this.pullV2Impl(afterSeq, limit, { ...opts, gateLocked: true }), false));
1155
1220
  }
1156
1221
  const result = await this.pullV2TailInternal({ window_mode: 'tail', after_seq: afterSeq, limit });
1157
1222
  return (Array.isArray(result.messages) ? result.messages : []);
1158
1223
  }
1159
1224
  if (ns && !opts?.gateLocked) {
1160
1225
  const key = `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`;
1161
- return await client._runPullSerialized(key, async () => this.pullV2(afterSeq, limit, {
1226
+ return await client._runPullSerialized(key, async () => this.pullV2Impl(afterSeq, limit, {
1162
1227
  ...(opts ?? {}),
1163
1228
  gateLocked: true,
1164
1229
  scheduleFollowup: true,
@@ -1228,6 +1293,7 @@ export class V2E2EECoordinator {
1228
1293
  }
1229
1294
  const deferredKeyFetches = new Map();
1230
1295
  let blockedSeq = 0;
1296
+ const pageDecryptedBefore = decrypted.length;
1231
1297
  for (const msg of messages) {
1232
1298
  const seq = Number(msg.seq ?? 0);
1233
1299
  if (!Number.isFinite(seq) || seq <= 0)
@@ -1235,11 +1301,12 @@ export class V2E2EECoordinator {
1235
1301
  const version = String(msg.version ?? 'v2');
1236
1302
  if (version === 'v1') {
1237
1303
  const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
1238
- const legacyPayload = legacy.payload;
1304
+ const legacyPayload = legacy.payload !== undefined ? legacy.payload : msg.payload;
1239
1305
  const payloadType = isJsonObject(legacyPayload)
1240
1306
  ? String(legacyPayload.type ?? '').trim()
1241
1307
  : '';
1242
- if (legacyPayload !== undefined && legacyPayload !== null && payloadType !== 'e2ee.encrypted' && payloadType !== 'e2ee.group_encrypted') {
1308
+ if (legacyPayload !== undefined && legacyPayload !== null
1309
+ && !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
1243
1310
  const v1Msg = {
1244
1311
  message_id: String(msg.message_id ?? ''),
1245
1312
  from: String(msg.from_aid ?? ''),
@@ -1261,6 +1328,25 @@ export class V2E2EECoordinator {
1261
1328
  decrypted.push(v1Msg);
1262
1329
  client._clientLog.debug(`message.v2.pull plaintext V1 delivered: seq=${seq}, ns=${ns || '<none>'}`);
1263
1330
  }
1331
+ else if (isJsonObject(legacyPayload)
1332
+ && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1333
+ const deferStatus = {};
1334
+ const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(legacyPayload) }, true, true, true, true, deferStatus, true, 'pull');
1335
+ if (deferStatus.deferred) {
1336
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1337
+ if (deferStatus.fromAid) {
1338
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1339
+ deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
1340
+ }
1341
+ }
1342
+ if (plaintext) {
1343
+ if (ns)
1344
+ await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1345
+ else
1346
+ await client._publishAppEvent('message.received', plaintext, 'pull');
1347
+ decrypted.push(plaintext);
1348
+ }
1349
+ }
1264
1350
  else {
1265
1351
  client._clientLog.debug(`message.v2.pull skipping V1 envelope seq=${seq} payload_type=${payloadType || '<none>'} (V1 E2EE removed)`);
1266
1352
  }
@@ -1275,7 +1361,7 @@ export class V2E2EECoordinator {
1275
1361
  client._v2Session.trackOldSPKMaxSeq(spkId, seq);
1276
1362
  }
1277
1363
  const deferStatus = {};
1278
- const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true);
1364
+ const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
1279
1365
  if (deferStatus.deferred) {
1280
1366
  blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1281
1367
  }
@@ -1337,7 +1423,7 @@ export class V2E2EECoordinator {
1337
1423
  throw exc;
1338
1424
  }
1339
1425
  if (contigAdvanced)
1340
- await client._drainOrderedMessages(ns, undefined, false, false);
1426
+ await client._drainOrderedMessages(ns, undefined, false, false, 'pull');
1341
1427
  for (const fetch of deferredKeyFetches.values()) {
1342
1428
  this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1343
1429
  }
@@ -1374,8 +1460,19 @@ export class V2E2EECoordinator {
1374
1460
  this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1375
1461
  }
1376
1462
  }
1463
+ if (ns)
1464
+ client._delivery.syncProgress(ns, {
1465
+ rawCount: messages.length,
1466
+ pulledCount: decrypted.length - pageDecryptedBefore,
1467
+ remaining: result.remaining,
1468
+ hasMore: hasMoreField ? hasMore : undefined,
1469
+ currentSeq: client._seqTracker.getContiguousSeq(ns),
1470
+ targetSeq: serverHead,
1471
+ });
1377
1472
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
1378
1473
  const rawCount = messages.length;
1474
+ if (blockedSeq > 0)
1475
+ break;
1379
1476
  const shouldContinue = shouldContinueForwardPage({
1380
1477
  rawCount,
1381
1478
  nextAfterSeq,
@@ -1409,6 +1506,19 @@ export class V2E2EECoordinator {
1409
1506
  client._clientLog.debug(`message.v2.pull done: requested_after_seq=${afterSeq}, pages=${pageCount}, decrypted=${decrypted.length}, ns=${ns || '<none>'}`);
1410
1507
  return decrypted;
1411
1508
  }
1509
+ async pullV2(afterSeq = 0, limit = 50, opts) {
1510
+ const ns = this.client._aid ? `p2p:${this.client._aid}` : '';
1511
+ if (!ns)
1512
+ return await this.pullV2Impl(afterSeq, limit, opts);
1513
+ this.client._delivery.syncStarted(ns);
1514
+ try {
1515
+ return await this.pullV2Impl(afterSeq, limit, opts);
1516
+ }
1517
+ catch (error) {
1518
+ this.client._delivery.onPullAborted?.();
1519
+ throw error;
1520
+ }
1521
+ }
1412
1522
  async confirmForwardP2PAck(upToSeq) {
1413
1523
  const ns = this.client._aid ? `p2p:${this.client._aid}` : '';
1414
1524
  try {
@@ -1487,9 +1597,10 @@ export class V2E2EECoordinator {
1487
1597
  const groupAid = String(opts?.groupAid ?? '').trim();
1488
1598
  if (!isJsonObject(payload))
1489
1599
  throw new ValidationError('group.send payload must be a dict for V2 encryption');
1600
+ const messageId = String(opts?.messageId ?? '').trim() || `m-${crypto.randomUUID().replace(/-/g, '')}`;
1490
1601
  client._logMessageDebug('send-plaintext', 'group.send.v2', 'group.send', withExplicitGroupAid({
1491
1602
  group_id: gid,
1492
- message_id: opts?.messageId ?? '',
1603
+ message_id: messageId,
1493
1604
  payload,
1494
1605
  }, groupAid), { payloadOverride: payload });
1495
1606
  const attempt = async (useCache) => {
@@ -1497,7 +1608,7 @@ export class V2E2EECoordinator {
1497
1608
  const envelope = await client._buildV2GroupEnvelope({
1498
1609
  groupId: gid,
1499
1610
  payload,
1500
- messageId: opts?.messageId,
1611
+ messageId,
1501
1612
  timestamp: opts?.timestamp,
1502
1613
  protectedHeaders: opts?.protectedHeaders,
1503
1614
  context: opts?.context,
@@ -1529,7 +1640,7 @@ export class V2E2EECoordinator {
1529
1640
  return client._delivery.attachSendResultEnvelope('group.send', withExplicitGroupAid({
1530
1641
  group_id: gid,
1531
1642
  payload,
1532
- message_id: opts?.messageId,
1643
+ message_id: messageId,
1533
1644
  timestamp: opts?.timestamp,
1534
1645
  protected_headers: opts?.protectedHeaders,
1535
1646
  context: opts?.context,
@@ -1545,7 +1656,7 @@ export class V2E2EECoordinator {
1545
1656
  return client._delivery.attachSendResultEnvelope('group.send', withExplicitGroupAid({
1546
1657
  group_id: gid,
1547
1658
  payload,
1548
- message_id: opts?.messageId,
1659
+ message_id: messageId,
1549
1660
  timestamp: opts?.timestamp,
1550
1661
  protected_headers: opts?.protectedHeaders,
1551
1662
  context: opts?.context,
@@ -1713,6 +1824,14 @@ export class V2E2EECoordinator {
1713
1824
  const afterSeq = strictWindowSeq(params.after_seq ?? 0, 'after_seq');
1714
1825
  const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
1715
1826
  const ns = `group:${groupId}`;
1827
+ const cursorParams = isJsonObject(params._group_cursor_params)
1828
+ ? params._group_cursor_params : params;
1829
+ const requestDeviceId = String(cursorParams.device_id ?? '').trim();
1830
+ const requestSlotId = String(cursorParams.slot_id ?? '').trim();
1831
+ const ownsCursor = (!requestDeviceId || requestDeviceId === String(client._deviceId ?? ''))
1832
+ && (!requestSlotId || requestSlotId === String(client._slotId ?? ''));
1833
+ if (ownsCursor)
1834
+ client._delivery.syncStarted(ns);
1716
1835
  const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
1717
1836
  group_id: groupId,
1718
1837
  window_mode: 'tail',
@@ -1745,6 +1864,15 @@ export class V2E2EECoordinator {
1745
1864
  const ack = client._seqTracker.getContiguousSeq(ns);
1746
1865
  if (ack > previous.ack)
1747
1866
  await this.ackGroupV2(groupId, ack, groupAid);
1867
+ if (ownsCursor)
1868
+ client._delivery.syncProgress(ns, {
1869
+ rawCount: page.messages.length,
1870
+ pulledCount: decoded.length,
1871
+ remaining: result.remaining,
1872
+ hasMore: typeof result.has_more === 'boolean' ? result.has_more : undefined,
1873
+ currentSeq: ack,
1874
+ targetSeq: page.head,
1875
+ });
1748
1876
  return { ...result, messages: decoded, raw_count: page.messages.length };
1749
1877
  }
1750
1878
  async historyGroupV2Internal(params) {
@@ -1780,7 +1908,7 @@ export class V2E2EECoordinator {
1780
1908
  force: params.force === true,
1781
1909
  });
1782
1910
  }
1783
- async pullGroupV2(groupId, afterSeq = 0, limit = 50, opts) {
1911
+ async pullGroupV2Impl(groupId, afterSeq = 0, limit = 50, opts) {
1784
1912
  const client = this.client;
1785
1913
  await client._ensureV2SessionReady('group.pull');
1786
1914
  const gid = String(groupId ?? '').trim();
@@ -1788,15 +1916,18 @@ export class V2E2EECoordinator {
1788
1916
  throw new ValidationError('group.pull requires group_id');
1789
1917
  let groupAid = String(opts?.groupAid ?? '').trim();
1790
1918
  const ns = `group:${gid}`;
1919
+ if (opts?.ownsCursor !== false)
1920
+ client._delivery.syncStarted(ns);
1791
1921
  if (opts?.windowMode === 'tail') {
1792
1922
  if (!opts.gateLocked) {
1793
1923
  const key = `${ns}|tail|after=${afterSeq}|limit=${limit}`;
1794
- return (await client._runPullSerialized(key, () => this.pullGroupV2(gid, afterSeq, limit, {
1924
+ return (await client._runPullSerialized(key, () => this.pullGroupV2Impl(gid, afterSeq, limit, {
1795
1925
  ...opts,
1796
1926
  gateLocked: true,
1797
1927
  }), false));
1798
1928
  }
1799
1929
  const result = await this.pullGroupV2TailInternal({
1930
+ ...(opts?.cursorParams ?? {}),
1800
1931
  group_id: String(opts.wireGroupId ?? gid),
1801
1932
  group_aid: groupAid || undefined,
1802
1933
  window_mode: 'tail',
@@ -1807,7 +1938,7 @@ export class V2E2EECoordinator {
1807
1938
  }
1808
1939
  if (!opts?.gateLocked) {
1809
1940
  const key = `${ns}|forward|after=${afterSeq}|force=${opts?.force === true}|limit=${limit}`;
1810
- return await client._runPullSerialized(key, async () => this.pullGroupV2(gid, afterSeq, limit, {
1941
+ return await client._runPullSerialized(key, async () => this.pullGroupV2Impl(gid, afterSeq, limit, {
1811
1942
  ...(opts ?? {}),
1812
1943
  gateLocked: true,
1813
1944
  scheduleFollowup: true,
@@ -1890,28 +2021,31 @@ export class V2E2EECoordinator {
1890
2021
  }
1891
2022
  const deferredKeyFetches = new Map();
1892
2023
  let blockedSeq = 0;
2024
+ const pageDecryptedBefore = decrypted.length;
1893
2025
  for (const msg of messages) {
1894
2026
  const seq = Number(msg.seq ?? 0);
1895
2027
  if (!Number.isFinite(seq) || seq <= 0)
1896
2028
  continue;
1897
2029
  const version = String(msg.version ?? 'v2');
1898
2030
  if (version === 'v1') {
1899
- const payload = msg.payload;
2031
+ const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
2032
+ const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
1900
2033
  const payloadObj = isJsonObject(payload) ? payload : null;
2034
+ const payloadType = payloadObj ? String(payloadObj.type ?? '').trim() : '';
1901
2035
  // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
1902
- if (client._delivery.recallEventFromGroupMessage(msg)) {
2036
+ if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
2037
+ && client._delivery.recallEventFromGroupMessage(msg)) {
1903
2038
  await client._delivery.publishGroupRecallTombstone(gid, seq, {
1904
2039
  ...msg,
1905
2040
  group_id: gid,
1906
2041
  group_aid: eventGroupAid,
1907
- });
2042
+ }, 'pull');
1908
2043
  client._markPublishedSeq(ns, seq);
1909
2044
  client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
1910
2045
  continue;
1911
2046
  }
1912
2047
  if (payloadObj) {
1913
- const payloadType = String(payloadObj.type ?? '').trim();
1914
- if (payloadType !== 'e2ee.encrypted' && payloadType !== 'e2ee.group_encrypted') {
2048
+ if (!['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
1915
2049
  let v1Msg = {
1916
2050
  message_id: String(msg.message_id ?? ''),
1917
2051
  from: String(msg.from_aid ?? ''),
@@ -1950,7 +2084,26 @@ export class V2E2EECoordinator {
1950
2084
  client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
1951
2085
  continue;
1952
2086
  }
1953
- client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
2087
+ if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
2088
+ const deferStatus = {};
2089
+ const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
2090
+ if (deferStatus.deferred) {
2091
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2092
+ if (deferStatus.fromAid) {
2093
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2094
+ deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
2095
+ }
2096
+ }
2097
+ if (plaintext) {
2098
+ plaintext.group_id = gid;
2099
+ plaintext.group_aid = eventGroupAid;
2100
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2101
+ decrypted.push(plaintext);
2102
+ }
2103
+ }
2104
+ else {
2105
+ client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
2106
+ }
1954
2107
  continue;
1955
2108
  }
1956
2109
  if (version !== 'v2') {
@@ -1958,7 +2111,7 @@ export class V2E2EECoordinator {
1958
2111
  continue;
1959
2112
  }
1960
2113
  const deferStatus = {};
1961
- let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true);
2114
+ let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
1962
2115
  if (deferStatus.deferred) {
1963
2116
  blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1964
2117
  }
@@ -2020,7 +2173,7 @@ export class V2E2EECoordinator {
2020
2173
  throw exc;
2021
2174
  }
2022
2175
  if (contigAdvanced)
2023
- await client._drainOrderedMessages(ns, undefined, false, false);
2176
+ await client._drainOrderedMessages(ns, undefined, false, false, 'pull');
2024
2177
  }
2025
2178
  for (const fetch of deferredKeyFetches.values()) {
2026
2179
  this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
@@ -2053,10 +2206,20 @@ export class V2E2EECoordinator {
2053
2206
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
2054
2207
  }
2055
2208
  }
2209
+ client._delivery.syncProgress(ns, {
2210
+ rawCount: messages.length,
2211
+ pulledCount: decrypted.length - pageDecryptedBefore,
2212
+ remaining: result.remaining,
2213
+ hasMore: hasMoreField ? hasMore : undefined,
2214
+ currentSeq: client._seqTracker.getContiguousSeq(ns),
2215
+ targetSeq: serverHead,
2216
+ });
2056
2217
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
2057
2218
  if (!ownsCursor)
2058
2219
  break;
2059
2220
  const rawCount = messages.length;
2221
+ if (blockedSeq > 0)
2222
+ break;
2060
2223
  const shouldContinue = shouldContinueForwardPage({
2061
2224
  rawCount,
2062
2225
  nextAfterSeq,
@@ -2090,6 +2253,20 @@ export class V2E2EECoordinator {
2090
2253
  client._clientLog.debug(`group.v2.pull done: group=${gid}, requested_after_seq=${afterSeq}, pages=${pageCount}, decrypted=${decrypted.length}, ns=${ns}`);
2091
2254
  return decrypted;
2092
2255
  }
2256
+ async pullGroupV2(groupId, afterSeq = 0, limit = 50, opts) {
2257
+ if (opts?.ownsCursor === false) {
2258
+ return await this.pullGroupV2Impl(groupId, afterSeq, limit, opts);
2259
+ }
2260
+ const ns = `group:${String(groupId ?? '').trim()}`;
2261
+ this.client._delivery.syncStarted(ns);
2262
+ try {
2263
+ return await this.pullGroupV2Impl(groupId, afterSeq, limit, opts);
2264
+ }
2265
+ catch (error) {
2266
+ this.client._delivery.onPullAborted?.();
2267
+ throw error;
2268
+ }
2269
+ }
2093
2270
  async confirmForwardGroupAck(groupId, upToSeq, groupAid) {
2094
2271
  const ns = `group:${String(groupId ?? '').trim()}`;
2095
2272
  try {
@@ -2649,7 +2826,7 @@ export class V2E2EECoordinator {
2649
2826
  async decryptV2PushMessage(data) {
2650
2827
  if (!isJsonObject(data))
2651
2828
  return null;
2652
- return await this.client._decryptV2Message(data, false, false, false, false);
2829
+ return await this.client._decryptV2Message(data, false, false, false, false, undefined, false, 'inline_push');
2653
2830
  }
2654
2831
  handleGroupChangedSpk(data, groupId, action) {
2655
2832
  if (!this.client._v2Session || !groupId || !MEMBERSHIP_ACTIONS.has(action))