@agentunion/fastaun-browser 0.5.10 → 0.5.12

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 (44) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/_packed_docs/CHANGELOG.md +62 -0
  3. package/_packed_docs/INDEX.md +3 -3
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +1 -1
  5. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +21 -3
  6. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +59 -0
  7. package/_packed_docs/sdk/09-group-rpc-manual.md +29 -2
  8. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +3 -2
  9. package/_packed_docs/sdk/INDEX.md +2 -1
  10. package/dist/bundle.js +1145 -381
  11. package/dist/client/delivery.d.ts +12 -4
  12. package/dist/client/delivery.d.ts.map +1 -1
  13. package/dist/client/delivery.js +341 -273
  14. package/dist/client/delivery.js.map +1 -1
  15. package/dist/client/group-state.d.ts.map +1 -1
  16. package/dist/client/group-state.js +1 -0
  17. package/dist/client/group-state.js.map +1 -1
  18. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  19. package/dist/client/rpc-pipeline.js +38 -13
  20. package/dist/client/rpc-pipeline.js.map +1 -1
  21. package/dist/client/v2-e2ee.d.ts +3 -1
  22. package/dist/client/v2-e2ee.d.ts.map +1 -1
  23. package/dist/client/v2-e2ee.js +211 -75
  24. package/dist/client/v2-e2ee.js.map +1 -1
  25. package/dist/client.d.ts +15 -0
  26. package/dist/client.d.ts.map +1 -1
  27. package/dist/client.js +402 -20
  28. package/dist/client.js.map +1 -1
  29. package/dist/facades.d.ts +7 -0
  30. package/dist/facades.d.ts.map +1 -1
  31. package/dist/facades.js +44 -0
  32. package/dist/facades.js.map +1 -1
  33. package/dist/seq-tracker.d.ts +2 -2
  34. package/dist/seq-tracker.d.ts.map +1 -1
  35. package/dist/seq-tracker.js +11 -7
  36. package/dist/seq-tracker.js.map +1 -1
  37. package/dist/tools/cross-sdk-agent.js +50 -12
  38. package/dist/tools/cross-sdk-agent.js.map +1 -1
  39. package/dist/transport.d.ts.map +1 -1
  40. package/dist/transport.js +162 -15
  41. package/dist/transport.js.map +1 -1
  42. package/dist/version.d.ts +1 -1
  43. package/dist/version.js +1 -1
  44. package/package.json +1 -1
@@ -67,11 +67,32 @@ function deduplicateForwardPageMessages(value, afterSeq) {
67
67
  }
68
68
  return [...bySeq.entries()].sort(([a], [b]) => a - b).map(([, row]) => row);
69
69
  }
70
- function shouldContinueForwardPage({ rawCount, nextAfterSeq, nextAfter, limit, hasMoreField, hasMore, serverHead, pendingAckSeq, }) {
70
+ function contiguousPullSeq(afterSeq, seqs, proof) {
71
+ // Forward 原始页中的缺号视为服务端永久空号;页完成后按页内最大
72
+ // seq 推进 A,避免单页空洞阻塞后续 pull/ACK。
73
+ let contiguous = seqs.length > 0 ? Math.max(afterSeq, ...seqs) : afterSeq;
74
+ const serverProof = safeNonNegativeSeq(proof);
75
+ return serverProof !== null ? Math.max(contiguous, serverProof) : contiguous;
76
+ }
77
+ function deduplicatePulledResults(messages) {
78
+ const seen = new Set();
79
+ return messages.filter((message) => {
80
+ if (!isJsonObject(message))
81
+ return true;
82
+ const seq = safeNonNegativeSeq(message.seq);
83
+ if (seq === null || seq <= 0)
84
+ return true;
85
+ if (seen.has(seq))
86
+ return false;
87
+ seen.add(seq);
88
+ return true;
89
+ });
90
+ }
91
+ function shouldContinueForwardPage({ rawCount, nextAfterSeq, nextAfter, limit, hasMoreField, hasMore, serverHead, pendingAckSeq, observedUpper, }) {
71
92
  // 无新 seq 或游标未前进时必须停止,避免错误元数据造成循环。
72
93
  if (rawCount <= 0 || nextAfter <= nextAfterSeq)
73
94
  return false;
74
- if (hasMore || serverHead > nextAfter || pendingAckSeq > 0)
95
+ if (hasMore || serverHead > nextAfter || pendingAckSeq > 0 || observedUpper > nextAfter)
75
96
  return true;
76
97
  // 仅旧服务缺失 has_more 时,以满页保留兼容续拉。
77
98
  return !hasMoreField && limit > 0 && rawCount >= limit;
@@ -889,7 +910,7 @@ export class V2E2EECoordinator {
889
910
  client._clientLog.warn(`V2 sender IK pending group bootstrap failed group=${groupId}: ${String(formatE2EEError(exc))}`);
890
911
  }
891
912
  }
892
- if (!session.getPeerIK(fromAid, senderDeviceId)) {
913
+ if (typeof session.getPeerIK === 'function' && !session.getPeerIK(fromAid, senderDeviceId)) {
893
914
  await this.getV2SenderPubDer(fromAid, senderDeviceId);
894
915
  }
895
916
  }
@@ -902,7 +923,7 @@ export class V2E2EECoordinator {
902
923
  let plaintext = null;
903
924
  const retryStatus = {};
904
925
  try {
905
- plaintext = await client._decryptV2Message(entry.msg, false, true, true, true, retryStatus);
926
+ plaintext = await client._decryptV2Message(entry.msg, false, true, true, true, retryStatus, false, 'pending_retry');
906
927
  }
907
928
  catch (exc) {
908
929
  if (!generationCurrent())
@@ -930,12 +951,14 @@ export class V2E2EECoordinator {
930
951
  client._v2SenderIKPending.delete(key);
931
952
  const seq = Number(entry.msg.seq ?? 0);
932
953
  if (entry.groupId) {
954
+ if (client._delivery.isSelfSentGroupMessage?.(entry.msg))
955
+ continue;
933
956
  plaintext = normalizeGroupMentionMode(plaintext, entry.msg, entry.msg.envelope_json);
934
957
  plaintext.group_id = entry.groupId;
935
- await client._publishPulledMessage('group.message_created', `group:${entry.groupId}`, seq, plaintext);
958
+ await client._publishPulledMessage('group.message_created', `group:${entry.groupId}`, seq, plaintext, false, 'pending_retry');
936
959
  }
937
960
  else {
938
- await client._publishPulledMessage('message.received', `p2p:${client._aid ?? ''}`, seq, plaintext);
961
+ await client._publishPulledMessage('message.received', `p2p:${client._aid ?? ''}`, seq, plaintext, false, 'pending_retry');
939
962
  }
940
963
  client._clientLog.debug(`V2 sender IK pending retry delivered: key=${key}`);
941
964
  }
@@ -1014,10 +1037,10 @@ export class V2E2EECoordinator {
1014
1037
  if (String(msg.version ?? '') === 'v1') {
1015
1038
  const legacy = isJsonObject(msg.legacy_v1)
1016
1039
  ? msg.legacy_v1 : {};
1017
- const payload = legacy.payload;
1040
+ const payload = legacy.payload !== undefined ? legacy.payload : msg.payload;
1018
1041
  const payloadType = isJsonObject(payload)
1019
1042
  ? String(payload.type ?? '').trim() : '';
1020
- if (payload !== undefined && payload !== null && !['e2ee.encrypted', 'e2ee.group_encrypted'].includes(payloadType)) {
1043
+ if (payload !== undefined && payload !== null && !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
1021
1044
  plaintext = {
1022
1045
  message_id: String(msg.message_id ?? ''), from: String(msg.from_aid ?? ''),
1023
1046
  to: String(legacy.to ?? client._aid ?? ''), seq, type: String(msg.type ?? ''),
@@ -1025,6 +1048,11 @@ export class V2E2EECoordinator {
1025
1048
  };
1026
1049
  attachGatewayProximity(plaintext, msg);
1027
1050
  }
1051
+ else if (isJsonObject(payload)
1052
+ && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1053
+ msg = { ...msg, envelope_json: JSON.stringify(payload) };
1054
+ plaintext = await client._decryptV2Message(msg, false, !history, true, true, undefined, false, source);
1055
+ }
1028
1056
  }
1029
1057
  else {
1030
1058
  plaintext = source === 'inline_push'
@@ -1054,26 +1082,48 @@ export class V2E2EECoordinator {
1054
1082
  let plaintext = null;
1055
1083
  if (String(msg.version ?? '') === 'v1') {
1056
1084
  const payload = msg.payload;
1085
+ const recall = typeof client._delivery?.recallEventFromGroupMessage === 'function'
1086
+ ? client._delivery.recallEventFromGroupMessage(msg)
1087
+ : null;
1088
+ if (recall) {
1089
+ const recallPayload = {
1090
+ ...recall,
1091
+ group_id: groupId,
1092
+ group_aid: groupAid,
1093
+ seq,
1094
+ };
1095
+ if (publish) {
1096
+ if (orderedPublish) {
1097
+ await client._delivery.publishOrderedMessage('group.message_recalled', `group:${groupId}`, seq, recallPayload, source);
1098
+ }
1099
+ else {
1100
+ await client._publishAppEvent('group.message_recalled', recallPayload, source);
1101
+ }
1102
+ }
1103
+ return recallPayload;
1104
+ }
1057
1105
  if (payload === undefined || payload === null) {
1058
1106
  if (history)
1059
1107
  return this.historyDecryptFailure(msg, 'legacy payload is missing');
1060
1108
  client._clientLog.warn(`Group Tail 缺少 payload 的 V1 行已跳过,连续性证明仍保留: group=${groupId}, seq=${seq}`);
1061
1109
  return null;
1062
1110
  }
1111
+ const payloadType = isJsonObject(payload)
1112
+ ? String(payload.type ?? '').trim() : '';
1063
1113
  if (isJsonObject(payload)
1064
- && ['e2ee.encrypted', 'e2ee.group_encrypted'].includes(String(payload.type ?? ''))) {
1065
- if (history)
1066
- return this.historyDecryptFailure(msg, 'unsupported legacy encrypted envelope');
1067
- client._clientLog.warn(`Group Tail 不支持的旧加密消息已跳过,连续性证明仍保留: group=${groupId}, seq=${seq}`);
1068
- return null;
1114
+ && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1115
+ msg = { ...msg, envelope_json: JSON.stringify(payload) };
1116
+ plaintext = await client._decryptV2Message(msg, false, !history, true, true, undefined, false, source);
1117
+ }
1118
+ else {
1119
+ plaintext = {
1120
+ message_id: String(msg.message_id ?? ''), from: String(msg.from_aid ?? ''),
1121
+ group_id: groupId, group_aid: groupAid, seq, type: String(msg.type ?? ''),
1122
+ timestamp: msg.t_server, payload: payload, encrypted: false,
1123
+ };
1124
+ plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
1125
+ attachGatewayProximity(plaintext, msg);
1069
1126
  }
1070
- plaintext = {
1071
- message_id: String(msg.message_id ?? ''), from: String(msg.from_aid ?? ''),
1072
- group_id: groupId, group_aid: groupAid, seq, type: String(msg.type ?? ''),
1073
- timestamp: msg.t_server, payload: payload, encrypted: false,
1074
- };
1075
- plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
1076
- attachGatewayProximity(plaintext, msg);
1077
1127
  }
1078
1128
  else {
1079
1129
  plaintext = source === 'inline_push'
@@ -1096,6 +1146,9 @@ export class V2E2EECoordinator {
1096
1146
  client._clientLog.warn(`Group Tail 单条消息解密失败,连续性证明仍保留: group=${groupId}, seq=${seq}`);
1097
1147
  return null;
1098
1148
  }
1149
+ if (publish && !history && client._delivery.isSelfSentGroupMessage?.(msg)) {
1150
+ return plaintext;
1151
+ }
1099
1152
  if (publish) {
1100
1153
  if (orderedPublish) {
1101
1154
  await client._delivery.publishOrderedMessage('group.message_created', `group:${groupId}`, seq, plaintext, source);
@@ -1112,14 +1165,6 @@ export class V2E2EECoordinator {
1112
1165
  async decodeInlineGroupMessage(msg, groupId, groupAid) {
1113
1166
  return await this.decodeGroupWindowMessage(msg, groupId, groupAid, false, false, 'inline_push');
1114
1167
  }
1115
- async applyTailFloor(ns, floor) {
1116
- const client = this.client;
1117
- if (!ns || floor <= 0 || client._seqTracker.getContiguousSeq(ns) >= floor)
1118
- return;
1119
- client._seqTracker.forceContiguousSeq(ns, floor);
1120
- client._persistSeq(ns);
1121
- await client._saveSeqTrackerState?.();
1122
- }
1123
1168
  async pullV2TailInternal(params, deferPublish = false) {
1124
1169
  const client = this.client;
1125
1170
  if (params.window_mode !== 'tail')
@@ -1129,10 +1174,9 @@ export class V2E2EECoordinator {
1129
1174
  const afterSeq = strictWindowSeq(params.after_seq ?? 0, 'after_seq');
1130
1175
  const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
1131
1176
  const ns = client._aid ? `p2p:${client._aid}` : '';
1177
+ if (ns)
1178
+ client._delivery.onPullStarted?.(ns);
1132
1179
  const result = await client._callRawV2Rpc('message.v2.pull', { window_mode: 'tail', after_seq: afterSeq, limit, _rpc_foreground: true });
1133
- const floor = Number(result.retention_floor_seq ?? 0);
1134
- const previous = ns ? client._seqTracker.getSyncState(ns) : null;
1135
- await this.applyTailFloor(ns, Number.isSafeInteger(floor) ? floor : 0);
1136
1180
  const page = validateTailPage(result, afterSeq, limit);
1137
1181
  if (ns) {
1138
1182
  client._seqTracker.commitTailWindow(ns, {
@@ -1148,8 +1192,9 @@ export class V2E2EECoordinator {
1148
1192
  decoded.push(plaintext);
1149
1193
  }
1150
1194
  const ack = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
1151
- if (previous && ack > previous.ack)
1152
- await this.ackV2(ack);
1195
+ if (ns)
1196
+ client._delivery.onPullPage?.(ns, decoded.length, result.has_more === true, page.head, typeof result.remaining === 'number' && Number.isSafeInteger(result.remaining) && result.remaining >= 0 ? result.remaining : null, page.messages.length, ack, page.head);
1197
+ client._delivery.onPullWorkSettled?.();
1153
1198
  return { ...result, messages: decoded, raw_count: page.messages.length };
1154
1199
  }
1155
1200
  async historyV2Internal(params) {
@@ -1165,16 +1210,33 @@ export class V2E2EECoordinator {
1165
1210
  }
1166
1211
  return { ...result, messages: decoded };
1167
1212
  }
1213
+ async runPullWithLifecycle(ns, operation) {
1214
+ if (ns)
1215
+ this.client._delivery.onPullStarted?.(ns);
1216
+ try {
1217
+ return await operation();
1218
+ }
1219
+ catch (error) {
1220
+ this.client._delivery.onPullAborted?.();
1221
+ throw error;
1222
+ }
1223
+ }
1168
1224
  async pullV2(afterSeq = 0, limit = 50, opts) {
1225
+ const ns = this.client._aid ? `p2p:${this.client._aid}` : '';
1226
+ return await this.runPullWithLifecycle(ns, () => this.pullV2Impl(afterSeq, limit, opts));
1227
+ }
1228
+ async pullV2Impl(afterSeq = 0, limit = 50, opts) {
1169
1229
  const client = this.client;
1170
1230
  await client._ensureV2SessionReady('message.pull');
1171
1231
  const ns = client._aid ? `p2p:${client._aid}` : '';
1232
+ if (ns)
1233
+ client._delivery.onPullStarted?.(ns);
1172
1234
  if (opts?.windowMode === 'tail') {
1173
1235
  if (!opts.gateLocked) {
1174
1236
  const key = pullGateKeyForClient(client, 'message.v2.pull', {
1175
1237
  window_mode: 'tail', after_seq: afterSeq, limit,
1176
1238
  }, `${ns}|tail|after=${afterSeq}|limit=${limit}`);
1177
- return await client._runPullSerialized(key, () => this.pullV2(afterSeq, limit, {
1239
+ return await client._runPullSerialized(key, () => this.pullV2Impl(afterSeq, limit, {
1178
1240
  ...(opts ?? {}),
1179
1241
  gateLocked: true,
1180
1242
  }), false);
@@ -1186,7 +1248,7 @@ export class V2E2EECoordinator {
1186
1248
  const key = pullGateKeyForClient(client, 'message.v2.pull', {
1187
1249
  after_seq: afterSeq, force: opts?.force === true, limit,
1188
1250
  }, `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
1189
- return await client._runPullSerialized(key, () => this.pullV2(afterSeq, limit, {
1251
+ return await client._runPullSerialized(key, () => this.pullV2Impl(afterSeq, limit, {
1190
1252
  ...(opts ?? {}),
1191
1253
  gateLocked: true,
1192
1254
  }), true);
@@ -1248,8 +1310,10 @@ export class V2E2EECoordinator {
1248
1310
  ? client._seqTracker.snapshotNamespace(ns)
1249
1311
  : null;
1250
1312
  const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
1313
+ const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
1251
1314
  const deferredKeyFetches = new Map();
1252
1315
  let blockedSeq = 0;
1316
+ const pageDecryptedBefore = decrypted.length;
1253
1317
  for (const msg of messages) {
1254
1318
  const seq = Number(msg.seq ?? 0);
1255
1319
  if (!Number.isFinite(seq) || seq <= 0)
@@ -1257,12 +1321,12 @@ export class V2E2EECoordinator {
1257
1321
  const version = String(msg.version ?? 'v2');
1258
1322
  if (version === 'v1') {
1259
1323
  const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
1260
- const legacyPayload = legacy.payload;
1324
+ const legacyPayload = legacy.payload !== undefined ? legacy.payload : msg.payload;
1261
1325
  const payloadType = isJsonObject(legacyPayload)
1262
1326
  ? String(legacyPayload.type ?? '').trim()
1263
1327
  : '';
1264
1328
  if (legacyPayload !== undefined && legacyPayload !== null
1265
- && payloadType !== 'e2ee.encrypted' && payloadType !== 'e2ee.group_encrypted') {
1329
+ && !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
1266
1330
  const v1Msg = {
1267
1331
  message_id: String(msg.message_id ?? ''),
1268
1332
  from: String(msg.from_aid ?? ''),
@@ -1281,6 +1345,25 @@ export class V2E2EECoordinator {
1281
1345
  await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull');
1282
1346
  decrypted.push(v1Msg);
1283
1347
  }
1348
+ else if (isJsonObject(legacyPayload)
1349
+ && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1350
+ const deferStatus = {};
1351
+ const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(legacyPayload) }, true, true, true, true, deferStatus, true, 'pull');
1352
+ if (deferStatus.deferred) {
1353
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1354
+ if (deferStatus.fromAid) {
1355
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1356
+ deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
1357
+ }
1358
+ }
1359
+ if (plaintext) {
1360
+ if (ns)
1361
+ await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1362
+ else
1363
+ await client._publishAppEvent('message.received', plaintext, 'pull');
1364
+ decrypted.push(plaintext);
1365
+ }
1366
+ }
1284
1367
  else {
1285
1368
  client._clientLog.debug(`message.v2.pull skipping V1 envelope seq=${seq} payload_type=${payloadType || '<none>'} (V1 E2EE removed)`);
1286
1369
  }
@@ -1295,7 +1378,7 @@ export class V2E2EECoordinator {
1295
1378
  client._v2Session.trackOldSPKMaxSeq(msgSpkId, seq);
1296
1379
  }
1297
1380
  const deferStatus = {};
1298
- const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true);
1381
+ const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
1299
1382
  if (deferStatus.deferred) {
1300
1383
  blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1301
1384
  }
@@ -1323,7 +1406,7 @@ export class V2E2EECoordinator {
1323
1406
  const serverAckSeq = parsedServerAckSeq ?? 0;
1324
1407
  const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
1325
1408
  if (ns) {
1326
- const commitTarget = blockedSeq > 0 ? pageContigBefore : Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
1409
+ const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, serverAckSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
1327
1410
  let ackSeq = pageContigBefore;
1328
1411
  let contigAdvanced = false;
1329
1412
  try {
@@ -1367,15 +1450,14 @@ export class V2E2EECoordinator {
1367
1450
  const knownServerAckSeq = hasServerAckSeq
1368
1451
  ? serverAckSeq
1369
1452
  : (pageCount === 1 ? deferredServerCursor : 0);
1370
- const ackNeeded = blockedSeq <= 0
1371
- && ackSeq > 0
1453
+ const ackNeeded = ackSeq > 0
1372
1454
  && ackSeq > lastAutoAckSeq
1373
1455
  && ((hasServerAckSeq && ackSeq > serverAckSeq)
1374
1456
  || (contigAdvanced && ackSeq > knownServerAckSeq));
1375
1457
  if (ackNeeded) {
1376
1458
  this.recordForwardAck(ns, ackSeq);
1377
- const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
1378
- const canContinuePage = messages.length > 0 && nextAfter > nextAfterSeq;
1459
+ const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
1460
+ const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq;
1379
1461
  if (canContinuePage) {
1380
1462
  pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
1381
1463
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
@@ -1386,7 +1468,9 @@ export class V2E2EECoordinator {
1386
1468
  }
1387
1469
  }
1388
1470
  }
1389
- const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
1471
+ if (ns)
1472
+ client._delivery.onPullPage?.(ns, decrypted.length - pageDecryptedBefore, hasMore, pageMaxSeq, typeof result.remaining === 'number' && Number.isSafeInteger(result.remaining) && result.remaining >= 0 ? result.remaining : null, messages.length, client._seqTracker.getContiguousSeq(ns), pageMaxSeq);
1473
+ const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
1390
1474
  const rawCount = messages.length;
1391
1475
  if (blockedSeq > 0)
1392
1476
  break;
@@ -1399,6 +1483,8 @@ export class V2E2EECoordinator {
1399
1483
  hasMore,
1400
1484
  serverHead,
1401
1485
  pendingAckSeq,
1486
+ observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
1487
+ ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
1402
1488
  });
1403
1489
  if (!shouldContinue)
1404
1490
  break;
@@ -1421,7 +1507,8 @@ export class V2E2EECoordinator {
1421
1507
  pendingAckSeq = 0;
1422
1508
  }
1423
1509
  }
1424
- return decrypted;
1510
+ client._delivery.onPullWorkSettled?.();
1511
+ return deduplicatePulledResults(decrypted);
1425
1512
  }
1426
1513
  async confirmForwardP2PAck(upToSeq, inlineGeneration) {
1427
1514
  const ns = this.client._aid ? `p2p:${this.client._aid}` : '';
@@ -1519,10 +1606,7 @@ export class V2E2EECoordinator {
1519
1606
  if (isJsonObject(result)) {
1520
1607
  const seq = Number(result.seq ?? 0);
1521
1608
  if (seq > 0) {
1522
- const ns = `group:${gid}`;
1523
- client._seqTracker.onMessageSeq(ns, seq);
1524
- client._markPublishedSeq(ns, seq);
1525
- await client._saveSeqTrackerState();
1609
+ client._markPublishedSeq(`group:${gid}`, seq);
1526
1610
  }
1527
1611
  }
1528
1612
  return client._delivery.attachSendResultEnvelope('group.send', withExplicitGroupAid({
@@ -1543,10 +1627,7 @@ export class V2E2EECoordinator {
1543
1627
  if (isJsonObject(result)) {
1544
1628
  const seq = Number(result.seq ?? 0);
1545
1629
  if (seq > 0) {
1546
- const ns = `group:${gid}`;
1547
- client._seqTracker.onMessageSeq(ns, seq);
1548
- client._markPublishedSeq(ns, seq);
1549
- await client._saveSeqTrackerState();
1630
+ client._markPublishedSeq(`group:${gid}`, seq);
1550
1631
  }
1551
1632
  }
1552
1633
  return client._delivery.attachSendResultEnvelope('group.send', withExplicitGroupAid({
@@ -1574,15 +1655,20 @@ export class V2E2EECoordinator {
1574
1655
  const afterSeq = strictWindowSeq(params.after_seq ?? 0, 'after_seq');
1575
1656
  const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
1576
1657
  const ns = `group:${groupId}`;
1658
+ const cursorParams = isJsonObject(params._group_cursor_params)
1659
+ ? params._group_cursor_params : params;
1660
+ const requestDeviceId = String(cursorParams.device_id ?? '').trim();
1661
+ const requestSlotId = String(cursorParams.slot_id ?? '').trim();
1662
+ const ownsCursor = (!requestDeviceId || requestDeviceId === String(client._deviceId ?? ''))
1663
+ && (!requestSlotId || requestSlotId === String(client._slotId ?? ''));
1664
+ if (ownsCursor)
1665
+ client._delivery.onPullStarted?.(ns);
1577
1666
  const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
1578
1667
  group_id: groupId, window_mode: 'tail', after_seq: afterSeq, limit, _rpc_foreground: true,
1579
1668
  }, groupAid));
1580
1669
  const resultAid = String(result.group_aid ?? result.groupAid ?? '').trim();
1581
1670
  if (resultAid)
1582
1671
  groupAid = resultAid;
1583
- const floor = Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0);
1584
- const previous = client._seqTracker.getSyncState(ns);
1585
- await this.applyTailFloor(ns, Number.isSafeInteger(floor) ? floor : 0);
1586
1672
  const page = validateTailPage(result, afterSeq, limit);
1587
1673
  client._seqTracker.commitTailWindow(ns, {
1588
1674
  windowStartSeq: page.windowStart, headSeq: page.head, coveredThroughSeq: page.head, pageLimit: limit,
@@ -1597,8 +1683,9 @@ export class V2E2EECoordinator {
1597
1683
  decoded.push(plaintext);
1598
1684
  }
1599
1685
  const ack = client._seqTracker.getContiguousSeq(ns);
1600
- if (ack > previous.ack)
1601
- await this.ackGroupV2(groupId, ack, groupAid);
1686
+ if (ownsCursor)
1687
+ client._delivery.onPullPage?.(ns, decoded.length, result.has_more === true, page.head, typeof result.remaining === 'number' && Number.isSafeInteger(result.remaining) && result.remaining >= 0 ? result.remaining : null, page.messages.length, ack, page.head);
1688
+ client._delivery.onPullWorkSettled?.();
1602
1689
  return { ...result, messages: decoded, raw_count: page.messages.length };
1603
1690
  }
1604
1691
  async historyGroupV2Internal(params) {
@@ -1631,6 +1718,13 @@ export class V2E2EECoordinator {
1631
1718
  });
1632
1719
  }
1633
1720
  async pullGroupV2(groupId, afterSeq = 0, limit = 50, opts) {
1721
+ const lifecycleNs = `group:${String(groupId ?? '').trim()}`;
1722
+ if (opts?.ownsCursor === false) {
1723
+ return await this.pullGroupV2Impl(groupId, afterSeq, limit, opts);
1724
+ }
1725
+ return await this.runPullWithLifecycle(lifecycleNs, () => this.pullGroupV2Impl(groupId, afterSeq, limit, opts));
1726
+ }
1727
+ async pullGroupV2Impl(groupId, afterSeq = 0, limit = 50, opts) {
1634
1728
  const client = this.client;
1635
1729
  await client._ensureV2SessionReady('group.pull');
1636
1730
  const gid = String(groupId ?? '').trim();
@@ -1644,12 +1738,13 @@ export class V2E2EECoordinator {
1644
1738
  group_id: gid, window_mode: 'tail', after_seq: afterSeq, limit,
1645
1739
  _group_cursor_params: opts?.cursorParams,
1646
1740
  }, `${ns}|tail|after=${afterSeq}|limit=${limit}`);
1647
- return await client._runPullSerialized(key, () => this.pullGroupV2(gid, afterSeq, limit, {
1741
+ return await client._runPullSerialized(key, () => this.pullGroupV2Impl(gid, afterSeq, limit, {
1648
1742
  ...(opts ?? {}),
1649
1743
  gateLocked: true,
1650
1744
  }), false);
1651
1745
  }
1652
1746
  const result = await this.pullGroupV2TailInternal({
1747
+ ...(opts?.cursorParams ?? {}),
1653
1748
  group_id: String(opts.wireGroupId ?? gid), group_aid: groupAid || undefined,
1654
1749
  window_mode: 'tail', after_seq: afterSeq, limit,
1655
1750
  });
@@ -1660,7 +1755,7 @@ export class V2E2EECoordinator {
1660
1755
  group_id: gid, after_seq: afterSeq, force: opts?.force === true, limit,
1661
1756
  _group_cursor_params: opts?.cursorParams,
1662
1757
  }, `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
1663
- return await client._runPullSerialized(key, () => this.pullGroupV2(gid, afterSeq, limit, {
1758
+ return await client._runPullSerialized(key, () => this.pullGroupV2Impl(gid, afterSeq, limit, {
1664
1759
  ...(opts ?? {}),
1665
1760
  gateLocked: true,
1666
1761
  }), true);
@@ -1669,6 +1764,8 @@ export class V2E2EECoordinator {
1669
1764
  const wireGroupId = String(opts?.wireGroupId ?? groupId ?? '').trim() || gid;
1670
1765
  const cursorParams = opts?.cursorParams ?? {};
1671
1766
  const ownsCursor = opts?.ownsCursor !== false;
1767
+ if (ownsCursor)
1768
+ client._delivery.onPullStarted?.(ns);
1672
1769
  let pullGateKey = ownsCursor
1673
1770
  ? pullGateKeyForClient(client, 'group.v2.pull', {
1674
1771
  group_id: gid, after_seq: afterSeq, force: opts?.force === true, limit,
@@ -1735,30 +1832,34 @@ export class V2E2EECoordinator {
1735
1832
  ? client._seqTracker.snapshotNamespace(ns)
1736
1833
  : null;
1737
1834
  const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
1835
+ const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
1738
1836
  const deferredKeyFetches = new Map();
1739
1837
  let blockedSeq = 0;
1838
+ const pageDecryptedBefore = decrypted.length;
1740
1839
  for (const msg of messages) {
1741
1840
  const seq = Number(msg.seq ?? 0);
1742
1841
  if (!Number.isFinite(seq) || seq <= 0)
1743
1842
  continue;
1744
1843
  const version = String(msg.version ?? 'v2');
1745
1844
  if (version === 'v1') {
1746
- const payload = msg.payload;
1845
+ const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
1846
+ const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
1747
1847
  const payloadObj = isJsonObject(payload) ? payload : null;
1748
1848
  // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
1749
- if (client._delivery.recallEventFromGroupMessage(msg)) {
1849
+ if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
1850
+ && client._delivery.recallEventFromGroupMessage(msg)) {
1750
1851
  await client._delivery.publishGroupRecallTombstone(gid, seq, {
1751
1852
  ...msg,
1752
1853
  group_id: gid,
1753
1854
  group_aid: eventGroupAid,
1754
- });
1855
+ }, 'pull');
1755
1856
  client._markPublishedSeq(ns, seq);
1756
1857
  client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
1757
1858
  continue;
1758
1859
  }
1759
1860
  if (payloadObj) {
1760
1861
  const payloadType = String(payloadObj.type ?? '').trim();
1761
- if (payloadType !== 'e2ee.encrypted' && payloadType !== 'e2ee.group_encrypted') {
1862
+ if (payloadType !== 'e2ee.encrypted' && payloadType !== 'e2ee.group_encrypted' && payloadType !== 'e2ee.p2p_encrypted') {
1762
1863
  let v1Msg = {
1763
1864
  message_id: String(msg.message_id ?? ''),
1764
1865
  from: String(msg.from_aid ?? ''),
@@ -1772,7 +1873,12 @@ export class V2E2EECoordinator {
1772
1873
  };
1773
1874
  v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
1774
1875
  attachGatewayProximity(v1Msg, msg);
1775
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
1876
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
1877
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
1878
+ }
1879
+ else {
1880
+ client._markPublishedSeq(ns, seq);
1881
+ }
1776
1882
  decrypted.push(v1Msg);
1777
1883
  continue;
1778
1884
  }
@@ -1795,6 +1901,28 @@ export class V2E2EECoordinator {
1795
1901
  decrypted.push(v1Msg);
1796
1902
  continue;
1797
1903
  }
1904
+ if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(String(payloadObj.type ?? '').trim())) {
1905
+ const deferStatus = {};
1906
+ const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
1907
+ if (deferStatus.deferred) {
1908
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1909
+ if (deferStatus.fromAid) {
1910
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1911
+ deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
1912
+ }
1913
+ }
1914
+ if (plaintext) {
1915
+ plaintext.group_id = gid;
1916
+ plaintext.group_aid = eventGroupAid;
1917
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
1918
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
1919
+ }
1920
+ else {
1921
+ client._markPublishedSeq(ns, seq);
1922
+ }
1923
+ decrypted.push(plaintext);
1924
+ }
1925
+ }
1798
1926
  client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
1799
1927
  continue;
1800
1928
  }
@@ -1803,7 +1931,7 @@ export class V2E2EECoordinator {
1803
1931
  continue;
1804
1932
  }
1805
1933
  const deferStatus = {};
1806
- let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true);
1934
+ let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
1807
1935
  if (deferStatus.deferred) {
1808
1936
  blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1809
1937
  }
@@ -1820,7 +1948,12 @@ export class V2E2EECoordinator {
1820
1948
  plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
1821
1949
  plaintext.group_id = gid;
1822
1950
  plaintext.group_aid = eventGroupAid;
1823
- await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
1951
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
1952
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
1953
+ }
1954
+ else {
1955
+ client._markPublishedSeq(ns, seq);
1956
+ }
1824
1957
  decrypted.push(plaintext);
1825
1958
  }
1826
1959
  const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
@@ -1829,7 +1962,7 @@ export class V2E2EECoordinator {
1829
1962
  const retentionFloor = Math.max(Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0), Number(cursor?.retention_floor_seq ?? 0));
1830
1963
  const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
1831
1964
  const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
1832
- const commitTarget = blockedSeq > 0 ? pageContigBefore : Math.max(pageContigBefore, Number.isFinite(effectiveFloor) ? effectiveFloor : 0, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
1965
+ const commitTarget = Math.max(pageContigBefore, Number.isFinite(effectiveFloor) ? effectiveFloor : 0, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, cursorCurrentSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
1833
1966
  let ackSeq = pageContigBefore;
1834
1967
  let contigAdvanced = false;
1835
1968
  if (ownsCursor) {
@@ -1867,6 +2000,7 @@ export class V2E2EECoordinator {
1867
2000
  for (const fetch of deferredKeyFetches.values()) {
1868
2001
  this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1869
2002
  }
2003
+ client._delivery.onPullPage?.(ns, decrypted.length - pageDecryptedBefore, hasMore, pageMaxSeq, typeof result.remaining === 'number' && Number.isSafeInteger(result.remaining) && result.remaining >= 0 ? result.remaining : null, messages.length, client._seqTracker.getContiguousSeq(ns), pageMaxSeq);
1870
2004
  if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
1871
2005
  this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
1872
2006
  }
@@ -1875,16 +2009,15 @@ export class V2E2EECoordinator {
1875
2009
  const knownServerCursorSeq = hasServerCursor
1876
2010
  ? cursorCurrentSeq
1877
2011
  : (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
1878
- const ackNeeded = blockedSeq <= 0
1879
- && ackSeq > 0
2012
+ const ackNeeded = ackSeq > 0
1880
2013
  && ackSeq > lastAutoAckSeq
1881
2014
  && ownsCursor
1882
2015
  && ((hasServerCursor && ackSeq > cursorCurrentSeq)
1883
2016
  || (contigAdvanced && ackSeq > knownServerCursorSeq));
1884
2017
  if (ackNeeded) {
1885
2018
  this.recordForwardAck(ns, ackSeq);
1886
- const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
1887
- const canContinuePage = messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
2019
+ const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
2020
+ const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
1888
2021
  if (canContinuePage) {
1889
2022
  pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
1890
2023
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
@@ -1894,7 +2027,7 @@ export class V2E2EECoordinator {
1894
2027
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1895
2028
  }
1896
2029
  }
1897
- const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
2030
+ const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
1898
2031
  if (!ownsCursor)
1899
2032
  break;
1900
2033
  const rawCount = messages.length;
@@ -1909,6 +2042,8 @@ export class V2E2EECoordinator {
1909
2042
  hasMore,
1910
2043
  serverHead,
1911
2044
  pendingAckSeq,
2045
+ observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
2046
+ ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
1912
2047
  });
1913
2048
  if (!shouldContinue)
1914
2049
  break;
@@ -1932,7 +2067,8 @@ export class V2E2EECoordinator {
1932
2067
  pendingAckSeq = 0;
1933
2068
  }
1934
2069
  }
1935
- return decrypted;
2070
+ client._delivery.onPullWorkSettled?.();
2071
+ return deduplicatePulledResults(decrypted);
1936
2072
  }
1937
2073
  async confirmForwardGroupAck(groupId, upToSeq, groupAid) {
1938
2074
  const ns = `group:${String(groupId ?? '').trim()}`;
@@ -2486,7 +2622,7 @@ export class V2E2EECoordinator {
2486
2622
  async decryptV2PushMessage(data) {
2487
2623
  if (!isJsonObject(data))
2488
2624
  return null;
2489
- return await this.client._decryptV2Message(data);
2625
+ return await this.client._decryptV2Message(data, false, false, false, false, undefined, false, 'inline_push');
2490
2626
  }
2491
2627
  async buildV2P2PEnvelope(opts) {
2492
2628
  const client = this.client;