@agentunion/fastaun-browser 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 (54) 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.d.ts.map +1 -1
  12. package/dist/agent-md.js +23 -1
  13. package/dist/agent-md.js.map +1 -1
  14. package/dist/auth.d.ts.map +1 -1
  15. package/dist/auth.js +16 -2
  16. package/dist/auth.js.map +1 -1
  17. package/dist/bundle.js +1374 -267
  18. package/dist/client/delivery.d.ts +34 -11
  19. package/dist/client/delivery.d.ts.map +1 -1
  20. package/dist/client/delivery.js +390 -100
  21. package/dist/client/delivery.js.map +1 -1
  22. package/dist/client/group-state.js +6 -6
  23. package/dist/client/group-state.js.map +1 -1
  24. package/dist/client/lifecycle.d.ts.map +1 -1
  25. package/dist/client/lifecycle.js +34 -38
  26. package/dist/client/lifecycle.js.map +1 -1
  27. package/dist/client/rpc-pipeline.d.ts +4 -0
  28. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  29. package/dist/client/rpc-pipeline.js +70 -4
  30. package/dist/client/rpc-pipeline.js.map +1 -1
  31. package/dist/client/v2-e2ee.d.ts +4 -1
  32. package/dist/client/v2-e2ee.d.ts.map +1 -1
  33. package/dist/client/v2-e2ee.js +169 -38
  34. package/dist/client/v2-e2ee.js.map +1 -1
  35. package/dist/client.d.ts +1 -1
  36. package/dist/client.d.ts.map +1 -1
  37. package/dist/client.js +83 -33
  38. package/dist/client.js.map +1 -1
  39. package/dist/events.d.ts +31 -2
  40. package/dist/events.d.ts.map +1 -1
  41. package/dist/events.js +167 -7
  42. package/dist/events.js.map +1 -1
  43. package/dist/register-flow.d.ts.map +1 -1
  44. package/dist/register-flow.js +16 -2
  45. package/dist/register-flow.js.map +1 -1
  46. package/dist/transport.d.ts +10 -1
  47. package/dist/transport.d.ts.map +1 -1
  48. package/dist/transport.js +427 -29
  49. package/dist/transport.js.map +1 -1
  50. package/dist/version.d.ts +1 -1
  51. package/dist/version.d.ts.map +1 -1
  52. package/dist/version.js +1 -1
  53. package/dist/version.js.map +1 -1
  54. package/package.json +1 -1
@@ -847,11 +847,17 @@ export class V2E2EECoordinator {
847
847
  if (!fromAid || client._v2SenderIKFetching.has(fetchKey))
848
848
  return;
849
849
  client._v2SenderIKFetching.add(fetchKey);
850
- client._safeAsync(this.resolveSenderIKPending(fromAid, senderDeviceId, groupId, fetchKey));
850
+ const releasePullWork = client._rpcPipeline?.reservePullWork?.() ?? (() => { });
851
+ const generation = client._delivery.captureInlineGeneration?.();
852
+ client._safeAsync(this.resolveSenderIKPending(fromAid, senderDeviceId, groupId, fetchKey, generation).finally(releasePullWork));
851
853
  }
852
- async resolveSenderIKPending(fromAid, senderDeviceId, groupId, fetchKey) {
854
+ async resolveSenderIKPending(fromAid, senderDeviceId, groupId, fetchKey, generation) {
853
855
  const client = this.client;
856
+ const generationCurrent = () => generation === undefined
857
+ || client._delivery.isInlineGenerationCurrent?.(generation) !== false;
854
858
  try {
859
+ if (!generationCurrent())
860
+ return;
855
861
  const session = client._v2Session;
856
862
  if (session && fromAid) {
857
863
  try {
@@ -887,18 +893,26 @@ export class V2E2EECoordinator {
887
893
  await this.getV2SenderPubDer(fromAid, senderDeviceId);
888
894
  }
889
895
  }
896
+ if (!generationCurrent())
897
+ return;
890
898
  const pendingItems = [...client._v2SenderIKPending.entries()].filter(([, entry]) => entry.fromAid === fromAid && entry.senderDeviceId === senderDeviceId && entry.groupId === groupId);
891
899
  for (const [key, entry] of pendingItems) {
900
+ if (!generationCurrent())
901
+ return;
892
902
  let plaintext = null;
893
903
  const retryStatus = {};
894
904
  try {
895
- plaintext = await client._decryptV2Message(entry.msg, false, true, true, true, retryStatus);
905
+ plaintext = await client._decryptV2Message(entry.msg, false, true, true, true, retryStatus, false, 'pending_retry');
896
906
  }
897
907
  catch (exc) {
908
+ if (!generationCurrent())
909
+ return;
898
910
  client._clientLog.warn(`V2 sender IK pending retry raised: key=${key} err=${String(formatE2EEError(exc))}`);
899
911
  client._v2SenderIKPending.delete(key);
900
912
  continue;
901
913
  }
914
+ if (!generationCurrent())
915
+ return;
902
916
  if (plaintext === null) {
903
917
  if (retryStatus.deferred) {
904
918
  client._clientLog.warn(`V2 pending retry still missing key material: key=${key}`);
@@ -918,10 +932,10 @@ export class V2E2EECoordinator {
918
932
  if (entry.groupId) {
919
933
  plaintext = normalizeGroupMentionMode(plaintext, entry.msg, entry.msg.envelope_json);
920
934
  plaintext.group_id = entry.groupId;
921
- await client._publishPulledMessage('group.message_created', `group:${entry.groupId}`, seq, plaintext);
935
+ await client._publishPulledMessage('group.message_created', `group:${entry.groupId}`, seq, plaintext, false, 'pending_retry');
922
936
  }
923
937
  else {
924
- await client._publishPulledMessage('message.received', `p2p:${client._aid ?? ''}`, seq, plaintext);
938
+ await client._publishPulledMessage('message.received', `p2p:${client._aid ?? ''}`, seq, plaintext, false, 'pending_retry');
925
939
  }
926
940
  client._clientLog.debug(`V2 sender IK pending retry delivered: key=${key}`);
927
941
  }
@@ -951,6 +965,7 @@ export class V2E2EECoordinator {
951
965
  return client.call('message.send', {
952
966
  to: toAid,
953
967
  payload: envelope,
968
+ message_id: opts?.messageId,
954
969
  encrypt: false,
955
970
  _skip_send_result_envelope: true,
956
971
  });
@@ -999,10 +1014,10 @@ export class V2E2EECoordinator {
999
1014
  if (String(msg.version ?? '') === 'v1') {
1000
1015
  const legacy = isJsonObject(msg.legacy_v1)
1001
1016
  ? msg.legacy_v1 : {};
1002
- const payload = legacy.payload;
1017
+ const payload = legacy.payload !== undefined ? legacy.payload : msg.payload;
1003
1018
  const payloadType = isJsonObject(payload)
1004
1019
  ? String(payload.type ?? '').trim() : '';
1005
- if (payload !== undefined && payload !== null && !['e2ee.encrypted', 'e2ee.group_encrypted'].includes(payloadType)) {
1020
+ if (payload !== undefined && payload !== null && !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
1006
1021
  plaintext = {
1007
1022
  message_id: String(msg.message_id ?? ''), from: String(msg.from_aid ?? ''),
1008
1023
  to: String(legacy.to ?? client._aid ?? ''), seq, type: String(msg.type ?? ''),
@@ -1010,6 +1025,11 @@ export class V2E2EECoordinator {
1010
1025
  };
1011
1026
  attachGatewayProximity(plaintext, msg);
1012
1027
  }
1028
+ else if (isJsonObject(payload)
1029
+ && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1030
+ msg = { ...msg, envelope_json: JSON.stringify(payload) };
1031
+ plaintext = await client._decryptV2Message(msg, false, !history, true, true, undefined, false, source);
1032
+ }
1013
1033
  }
1014
1034
  else {
1015
1035
  plaintext = source === 'inline_push'
@@ -1025,7 +1045,7 @@ export class V2E2EECoordinator {
1025
1045
  if (publish) {
1026
1046
  const event = client._delivery.p2pAppEventForMessage(plaintext);
1027
1047
  if (orderedPublish) {
1028
- await client._delivery.publishOrderedMessage(event.event, client._aid ? `p2p:${client._aid}` : '', seq, event.payload);
1048
+ await client._delivery.publishOrderedMessage(event.event, client._aid ? `p2p:${client._aid}` : '', seq, event.payload, source);
1029
1049
  }
1030
1050
  else {
1031
1051
  await client._publishAppEvent(event.event, event.payload, source);
@@ -1039,26 +1059,48 @@ export class V2E2EECoordinator {
1039
1059
  let plaintext = null;
1040
1060
  if (String(msg.version ?? '') === 'v1') {
1041
1061
  const payload = msg.payload;
1062
+ const recall = typeof client._delivery?.recallEventFromGroupMessage === 'function'
1063
+ ? client._delivery.recallEventFromGroupMessage(msg)
1064
+ : null;
1065
+ if (recall) {
1066
+ const recallPayload = {
1067
+ ...recall,
1068
+ group_id: groupId,
1069
+ group_aid: groupAid,
1070
+ seq,
1071
+ };
1072
+ if (publish) {
1073
+ if (orderedPublish) {
1074
+ await client._delivery.publishOrderedMessage('group.message_recalled', `group:${groupId}`, seq, recallPayload, source);
1075
+ }
1076
+ else {
1077
+ await client._publishAppEvent('group.message_recalled', recallPayload, source);
1078
+ }
1079
+ }
1080
+ return recallPayload;
1081
+ }
1042
1082
  if (payload === undefined || payload === null) {
1043
1083
  if (history)
1044
1084
  return this.historyDecryptFailure(msg, 'legacy payload is missing');
1045
1085
  client._clientLog.warn(`Group Tail 缺少 payload 的 V1 行已跳过,连续性证明仍保留: group=${groupId}, seq=${seq}`);
1046
1086
  return null;
1047
1087
  }
1088
+ const payloadType = isJsonObject(payload)
1089
+ ? String(payload.type ?? '').trim() : '';
1048
1090
  if (isJsonObject(payload)
1049
- && ['e2ee.encrypted', 'e2ee.group_encrypted'].includes(String(payload.type ?? ''))) {
1050
- if (history)
1051
- return this.historyDecryptFailure(msg, 'unsupported legacy encrypted envelope');
1052
- client._clientLog.warn(`Group Tail 不支持的旧加密消息已跳过,连续性证明仍保留: group=${groupId}, seq=${seq}`);
1053
- return null;
1091
+ && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1092
+ msg = { ...msg, envelope_json: JSON.stringify(payload) };
1093
+ plaintext = await client._decryptV2Message(msg, false, !history, true, true, undefined, false, source);
1094
+ }
1095
+ else {
1096
+ plaintext = {
1097
+ message_id: String(msg.message_id ?? ''), from: String(msg.from_aid ?? ''),
1098
+ group_id: groupId, group_aid: groupAid, seq, type: String(msg.type ?? ''),
1099
+ timestamp: msg.t_server, payload: payload, encrypted: false,
1100
+ };
1101
+ plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
1102
+ attachGatewayProximity(plaintext, msg);
1054
1103
  }
1055
- plaintext = {
1056
- message_id: String(msg.message_id ?? ''), from: String(msg.from_aid ?? ''),
1057
- group_id: groupId, group_aid: groupAid, seq, type: String(msg.type ?? ''),
1058
- timestamp: msg.t_server, payload: payload, encrypted: false,
1059
- };
1060
- plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
1061
- attachGatewayProximity(plaintext, msg);
1062
1104
  }
1063
1105
  else {
1064
1106
  plaintext = source === 'inline_push'
@@ -1083,7 +1125,7 @@ export class V2E2EECoordinator {
1083
1125
  }
1084
1126
  if (publish) {
1085
1127
  if (orderedPublish) {
1086
- await client._delivery.publishOrderedMessage('group.message_created', `group:${groupId}`, seq, plaintext);
1128
+ await client._delivery.publishOrderedMessage('group.message_created', `group:${groupId}`, seq, plaintext, source);
1087
1129
  }
1088
1130
  else {
1089
1131
  await client._publishAppEvent('group.message_created', plaintext, source);
@@ -1114,6 +1156,8 @@ export class V2E2EECoordinator {
1114
1156
  const afterSeq = strictWindowSeq(params.after_seq ?? 0, 'after_seq');
1115
1157
  const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
1116
1158
  const ns = client._aid ? `p2p:${client._aid}` : '';
1159
+ if (ns)
1160
+ client._delivery.onPullStarted?.(ns);
1117
1161
  const result = await client._callRawV2Rpc('message.v2.pull', { window_mode: 'tail', after_seq: afterSeq, limit, _rpc_foreground: true });
1118
1162
  const floor = Number(result.retention_floor_seq ?? 0);
1119
1163
  const previous = ns ? client._seqTracker.getSyncState(ns) : null;
@@ -1135,6 +1179,9 @@ export class V2E2EECoordinator {
1135
1179
  const ack = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
1136
1180
  if (previous && ack > previous.ack)
1137
1181
  await this.ackV2(ack);
1182
+ if (ns)
1183
+ 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);
1184
+ client._delivery.onPullWorkSettled?.();
1138
1185
  return { ...result, messages: decoded, raw_count: page.messages.length };
1139
1186
  }
1140
1187
  async historyV2Internal(params) {
@@ -1150,16 +1197,33 @@ export class V2E2EECoordinator {
1150
1197
  }
1151
1198
  return { ...result, messages: decoded };
1152
1199
  }
1200
+ async runPullWithLifecycle(ns, operation) {
1201
+ if (ns)
1202
+ this.client._delivery.onPullStarted?.(ns);
1203
+ try {
1204
+ return await operation();
1205
+ }
1206
+ catch (error) {
1207
+ this.client._delivery.onPullAborted?.();
1208
+ throw error;
1209
+ }
1210
+ }
1153
1211
  async pullV2(afterSeq = 0, limit = 50, opts) {
1212
+ const ns = this.client._aid ? `p2p:${this.client._aid}` : '';
1213
+ return await this.runPullWithLifecycle(ns, () => this.pullV2Impl(afterSeq, limit, opts));
1214
+ }
1215
+ async pullV2Impl(afterSeq = 0, limit = 50, opts) {
1154
1216
  const client = this.client;
1155
1217
  await client._ensureV2SessionReady('message.pull');
1156
1218
  const ns = client._aid ? `p2p:${client._aid}` : '';
1219
+ if (ns)
1220
+ client._delivery.onPullStarted?.(ns);
1157
1221
  if (opts?.windowMode === 'tail') {
1158
1222
  if (!opts.gateLocked) {
1159
1223
  const key = pullGateKeyForClient(client, 'message.v2.pull', {
1160
1224
  window_mode: 'tail', after_seq: afterSeq, limit,
1161
1225
  }, `${ns}|tail|after=${afterSeq}|limit=${limit}`);
1162
- return await client._runPullSerialized(key, () => this.pullV2(afterSeq, limit, {
1226
+ return await client._runPullSerialized(key, () => this.pullV2Impl(afterSeq, limit, {
1163
1227
  ...(opts ?? {}),
1164
1228
  gateLocked: true,
1165
1229
  }), false);
@@ -1171,7 +1235,7 @@ export class V2E2EECoordinator {
1171
1235
  const key = pullGateKeyForClient(client, 'message.v2.pull', {
1172
1236
  after_seq: afterSeq, force: opts?.force === true, limit,
1173
1237
  }, `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
1174
- return await client._runPullSerialized(key, () => this.pullV2(afterSeq, limit, {
1238
+ return await client._runPullSerialized(key, () => this.pullV2Impl(afterSeq, limit, {
1175
1239
  ...(opts ?? {}),
1176
1240
  gateLocked: true,
1177
1241
  }), true);
@@ -1235,6 +1299,7 @@ export class V2E2EECoordinator {
1235
1299
  const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
1236
1300
  const deferredKeyFetches = new Map();
1237
1301
  let blockedSeq = 0;
1302
+ const pageDecryptedBefore = decrypted.length;
1238
1303
  for (const msg of messages) {
1239
1304
  const seq = Number(msg.seq ?? 0);
1240
1305
  if (!Number.isFinite(seq) || seq <= 0)
@@ -1242,12 +1307,12 @@ export class V2E2EECoordinator {
1242
1307
  const version = String(msg.version ?? 'v2');
1243
1308
  if (version === 'v1') {
1244
1309
  const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
1245
- const legacyPayload = legacy.payload;
1310
+ const legacyPayload = legacy.payload !== undefined ? legacy.payload : msg.payload;
1246
1311
  const payloadType = isJsonObject(legacyPayload)
1247
1312
  ? String(legacyPayload.type ?? '').trim()
1248
1313
  : '';
1249
1314
  if (legacyPayload !== undefined && legacyPayload !== null
1250
- && payloadType !== 'e2ee.encrypted' && payloadType !== 'e2ee.group_encrypted') {
1315
+ && !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
1251
1316
  const v1Msg = {
1252
1317
  message_id: String(msg.message_id ?? ''),
1253
1318
  from: String(msg.from_aid ?? ''),
@@ -1263,9 +1328,28 @@ export class V2E2EECoordinator {
1263
1328
  if (ns)
1264
1329
  await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
1265
1330
  else
1266
- await client._publishAppEvent(appEvent.event, appEvent.payload);
1331
+ await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull');
1267
1332
  decrypted.push(v1Msg);
1268
1333
  }
1334
+ else if (isJsonObject(legacyPayload)
1335
+ && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1336
+ const deferStatus = {};
1337
+ const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(legacyPayload) }, true, true, true, true, deferStatus, true, 'pull');
1338
+ if (deferStatus.deferred) {
1339
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1340
+ if (deferStatus.fromAid) {
1341
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1342
+ deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
1343
+ }
1344
+ }
1345
+ if (plaintext) {
1346
+ if (ns)
1347
+ await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1348
+ else
1349
+ await client._publishAppEvent('message.received', plaintext, 'pull');
1350
+ decrypted.push(plaintext);
1351
+ }
1352
+ }
1269
1353
  else {
1270
1354
  client._clientLog.debug(`message.v2.pull skipping V1 envelope seq=${seq} payload_type=${payloadType || '<none>'} (V1 E2EE removed)`);
1271
1355
  }
@@ -1280,7 +1364,7 @@ export class V2E2EECoordinator {
1280
1364
  client._v2Session.trackOldSPKMaxSeq(msgSpkId, seq);
1281
1365
  }
1282
1366
  const deferStatus = {};
1283
- const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true);
1367
+ const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
1284
1368
  if (deferStatus.deferred) {
1285
1369
  blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1286
1370
  }
@@ -1299,7 +1383,7 @@ export class V2E2EECoordinator {
1299
1383
  decrypted.push(plaintext);
1300
1384
  }
1301
1385
  else {
1302
- await client._publishAppEvent('message.received', plaintext);
1386
+ await client._publishAppEvent('message.received', plaintext, 'pull');
1303
1387
  decrypted.push(plaintext);
1304
1388
  }
1305
1389
  }
@@ -1360,7 +1444,7 @@ export class V2E2EECoordinator {
1360
1444
  if (ackNeeded) {
1361
1445
  this.recordForwardAck(ns, ackSeq);
1362
1446
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
1363
- const canContinuePage = messages.length > 0 && nextAfter > nextAfterSeq;
1447
+ const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq;
1364
1448
  if (canContinuePage) {
1365
1449
  pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
1366
1450
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
@@ -1371,6 +1455,8 @@ export class V2E2EECoordinator {
1371
1455
  }
1372
1456
  }
1373
1457
  }
1458
+ if (ns)
1459
+ 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);
1374
1460
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
1375
1461
  const rawCount = messages.length;
1376
1462
  if (blockedSeq > 0)
@@ -1406,6 +1492,7 @@ export class V2E2EECoordinator {
1406
1492
  pendingAckSeq = 0;
1407
1493
  }
1408
1494
  }
1495
+ client._delivery.onPullWorkSettled?.();
1409
1496
  return decrypted;
1410
1497
  }
1411
1498
  async confirmForwardP2PAck(upToSeq, inlineGeneration) {
@@ -1496,6 +1583,7 @@ export class V2E2EECoordinator {
1496
1583
  return client.call('group.v2.send', withExplicitGroupAid({
1497
1584
  group_id: gid,
1498
1585
  envelope,
1586
+ message_id: opts?.messageId,
1499
1587
  }, groupAid));
1500
1588
  };
1501
1589
  try {
@@ -1558,6 +1646,14 @@ export class V2E2EECoordinator {
1558
1646
  const afterSeq = strictWindowSeq(params.after_seq ?? 0, 'after_seq');
1559
1647
  const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
1560
1648
  const ns = `group:${groupId}`;
1649
+ const cursorParams = isJsonObject(params._group_cursor_params)
1650
+ ? params._group_cursor_params : params;
1651
+ const requestDeviceId = String(cursorParams.device_id ?? '').trim();
1652
+ const requestSlotId = String(cursorParams.slot_id ?? '').trim();
1653
+ const ownsCursor = (!requestDeviceId || requestDeviceId === String(client._deviceId ?? ''))
1654
+ && (!requestSlotId || requestSlotId === String(client._slotId ?? ''));
1655
+ if (ownsCursor)
1656
+ client._delivery.onPullStarted?.(ns);
1561
1657
  const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
1562
1658
  group_id: groupId, window_mode: 'tail', after_seq: afterSeq, limit, _rpc_foreground: true,
1563
1659
  }, groupAid));
@@ -1583,6 +1679,9 @@ export class V2E2EECoordinator {
1583
1679
  const ack = client._seqTracker.getContiguousSeq(ns);
1584
1680
  if (ack > previous.ack)
1585
1681
  await this.ackGroupV2(groupId, ack, groupAid);
1682
+ if (ownsCursor)
1683
+ 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);
1684
+ client._delivery.onPullWorkSettled?.();
1586
1685
  return { ...result, messages: decoded, raw_count: page.messages.length };
1587
1686
  }
1588
1687
  async historyGroupV2Internal(params) {
@@ -1615,6 +1714,13 @@ export class V2E2EECoordinator {
1615
1714
  });
1616
1715
  }
1617
1716
  async pullGroupV2(groupId, afterSeq = 0, limit = 50, opts) {
1717
+ const lifecycleNs = `group:${String(groupId ?? '').trim()}`;
1718
+ if (opts?.ownsCursor === false) {
1719
+ return await this.pullGroupV2Impl(groupId, afterSeq, limit, opts);
1720
+ }
1721
+ return await this.runPullWithLifecycle(lifecycleNs, () => this.pullGroupV2Impl(groupId, afterSeq, limit, opts));
1722
+ }
1723
+ async pullGroupV2Impl(groupId, afterSeq = 0, limit = 50, opts) {
1618
1724
  const client = this.client;
1619
1725
  await client._ensureV2SessionReady('group.pull');
1620
1726
  const gid = String(groupId ?? '').trim();
@@ -1628,12 +1734,13 @@ export class V2E2EECoordinator {
1628
1734
  group_id: gid, window_mode: 'tail', after_seq: afterSeq, limit,
1629
1735
  _group_cursor_params: opts?.cursorParams,
1630
1736
  }, `${ns}|tail|after=${afterSeq}|limit=${limit}`);
1631
- return await client._runPullSerialized(key, () => this.pullGroupV2(gid, afterSeq, limit, {
1737
+ return await client._runPullSerialized(key, () => this.pullGroupV2Impl(gid, afterSeq, limit, {
1632
1738
  ...(opts ?? {}),
1633
1739
  gateLocked: true,
1634
1740
  }), false);
1635
1741
  }
1636
1742
  const result = await this.pullGroupV2TailInternal({
1743
+ ...(opts?.cursorParams ?? {}),
1637
1744
  group_id: String(opts.wireGroupId ?? gid), group_aid: groupAid || undefined,
1638
1745
  window_mode: 'tail', after_seq: afterSeq, limit,
1639
1746
  });
@@ -1644,7 +1751,7 @@ export class V2E2EECoordinator {
1644
1751
  group_id: gid, after_seq: afterSeq, force: opts?.force === true, limit,
1645
1752
  _group_cursor_params: opts?.cursorParams,
1646
1753
  }, `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
1647
- return await client._runPullSerialized(key, () => this.pullGroupV2(gid, afterSeq, limit, {
1754
+ return await client._runPullSerialized(key, () => this.pullGroupV2Impl(gid, afterSeq, limit, {
1648
1755
  ...(opts ?? {}),
1649
1756
  gateLocked: true,
1650
1757
  }), true);
@@ -1653,6 +1760,8 @@ export class V2E2EECoordinator {
1653
1760
  const wireGroupId = String(opts?.wireGroupId ?? groupId ?? '').trim() || gid;
1654
1761
  const cursorParams = opts?.cursorParams ?? {};
1655
1762
  const ownsCursor = opts?.ownsCursor !== false;
1763
+ if (ownsCursor)
1764
+ client._delivery.onPullStarted?.(ns);
1656
1765
  let pullGateKey = ownsCursor
1657
1766
  ? pullGateKeyForClient(client, 'group.v2.pull', {
1658
1767
  group_id: gid, after_seq: afterSeq, force: opts?.force === true, limit,
@@ -1721,28 +1830,31 @@ export class V2E2EECoordinator {
1721
1830
  const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
1722
1831
  const deferredKeyFetches = new Map();
1723
1832
  let blockedSeq = 0;
1833
+ const pageDecryptedBefore = decrypted.length;
1724
1834
  for (const msg of messages) {
1725
1835
  const seq = Number(msg.seq ?? 0);
1726
1836
  if (!Number.isFinite(seq) || seq <= 0)
1727
1837
  continue;
1728
1838
  const version = String(msg.version ?? 'v2');
1729
1839
  if (version === 'v1') {
1730
- const payload = msg.payload;
1840
+ const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
1841
+ const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
1731
1842
  const payloadObj = isJsonObject(payload) ? payload : null;
1732
1843
  // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
1733
- if (client._delivery.recallEventFromGroupMessage(msg)) {
1844
+ if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
1845
+ && client._delivery.recallEventFromGroupMessage(msg)) {
1734
1846
  await client._delivery.publishGroupRecallTombstone(gid, seq, {
1735
1847
  ...msg,
1736
1848
  group_id: gid,
1737
1849
  group_aid: eventGroupAid,
1738
- });
1850
+ }, 'pull');
1739
1851
  client._markPublishedSeq(ns, seq);
1740
1852
  client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
1741
1853
  continue;
1742
1854
  }
1743
1855
  if (payloadObj) {
1744
1856
  const payloadType = String(payloadObj.type ?? '').trim();
1745
- if (payloadType !== 'e2ee.encrypted' && payloadType !== 'e2ee.group_encrypted') {
1857
+ if (payloadType !== 'e2ee.encrypted' && payloadType !== 'e2ee.group_encrypted' && payloadType !== 'e2ee.p2p_encrypted') {
1746
1858
  let v1Msg = {
1747
1859
  message_id: String(msg.message_id ?? ''),
1748
1860
  from: String(msg.from_aid ?? ''),
@@ -1779,6 +1891,23 @@ export class V2E2EECoordinator {
1779
1891
  decrypted.push(v1Msg);
1780
1892
  continue;
1781
1893
  }
1894
+ if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(String(payloadObj.type ?? '').trim())) {
1895
+ const deferStatus = {};
1896
+ const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
1897
+ if (deferStatus.deferred) {
1898
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1899
+ if (deferStatus.fromAid) {
1900
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1901
+ deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
1902
+ }
1903
+ }
1904
+ if (plaintext) {
1905
+ plaintext.group_id = gid;
1906
+ plaintext.group_aid = eventGroupAid;
1907
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
1908
+ decrypted.push(plaintext);
1909
+ }
1910
+ }
1782
1911
  client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
1783
1912
  continue;
1784
1913
  }
@@ -1787,7 +1916,7 @@ export class V2E2EECoordinator {
1787
1916
  continue;
1788
1917
  }
1789
1918
  const deferStatus = {};
1790
- let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true);
1919
+ let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
1791
1920
  if (deferStatus.deferred) {
1792
1921
  blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1793
1922
  }
@@ -1851,6 +1980,7 @@ export class V2E2EECoordinator {
1851
1980
  for (const fetch of deferredKeyFetches.values()) {
1852
1981
  this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1853
1982
  }
1983
+ 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);
1854
1984
  if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
1855
1985
  this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
1856
1986
  }
@@ -1868,7 +1998,7 @@ export class V2E2EECoordinator {
1868
1998
  if (ackNeeded) {
1869
1999
  this.recordForwardAck(ns, ackSeq);
1870
2000
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
1871
- const canContinuePage = messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
2001
+ const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
1872
2002
  if (canContinuePage) {
1873
2003
  pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
1874
2004
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
@@ -1916,6 +2046,7 @@ export class V2E2EECoordinator {
1916
2046
  pendingAckSeq = 0;
1917
2047
  }
1918
2048
  }
2049
+ client._delivery.onPullWorkSettled?.();
1919
2050
  return decrypted;
1920
2051
  }
1921
2052
  async confirmForwardGroupAck(groupId, upToSeq, groupAid) {
@@ -2470,7 +2601,7 @@ export class V2E2EECoordinator {
2470
2601
  async decryptV2PushMessage(data) {
2471
2602
  if (!isJsonObject(data))
2472
2603
  return null;
2473
- return await this.client._decryptV2Message(data);
2604
+ return await this.client._decryptV2Message(data, false, false, false, false, undefined, false, 'inline_push');
2474
2605
  }
2475
2606
  async buildV2P2PEnvelope(opts) {
2476
2607
  const client = this.client;