@agentunion/fastaun-browser 0.2.13 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.d.ts +4 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +16 -0
- package/dist/auth.js.map +1 -1
- package/dist/client.d.ts +19 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +503 -101
- package/dist/client.js.map +1 -1
- package/dist/seq-tracker.d.ts +3 -0
- package/dist/seq-tracker.d.ts.map +1 -1
- package/dist/seq-tracker.js +38 -2
- package/dist/seq-tracker.js.map +1 -1
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -59,6 +59,9 @@ const SIGNED_METHODS = new Set([
|
|
|
59
59
|
'group.transfer_owner', 'group.review_join_request',
|
|
60
60
|
'group.batch_review_join_request',
|
|
61
61
|
'group.request_join', 'group.use_invite_code',
|
|
62
|
+
'group.thought.put',
|
|
63
|
+
'message.thought.put',
|
|
64
|
+
'group.set_settings',
|
|
62
65
|
'group.resources.put', 'group.resources.update',
|
|
63
66
|
'group.resources.delete', 'group.resources.request_add',
|
|
64
67
|
'group.resources.direct_add', 'group.resources.approve_request',
|
|
@@ -84,6 +87,21 @@ const RECONNECT_MIN_BASE_DELAY_SECONDS = 1.0;
|
|
|
84
87
|
const RECONNECT_MAX_BASE_DELAY_SECONDS = 64.0;
|
|
85
88
|
const GROUP_ROTATION_LEASE_MS = 120_000;
|
|
86
89
|
const GROUP_ROTATION_RETRY_MAX_DELAY_MS = 300_000;
|
|
90
|
+
const PENDING_DECRYPT_LIMIT = 100;
|
|
91
|
+
const PUSHED_SEQS_LIMIT = 50_000;
|
|
92
|
+
const PENDING_ORDERED_LIMIT = 50_000;
|
|
93
|
+
// P1-23: 非幂等方法使用更长超时(35s),避免 SDK 10s 超时 < gateway 30s 处理时间
|
|
94
|
+
const NON_IDEMPOTENT_TIMEOUT = 35;
|
|
95
|
+
const NON_IDEMPOTENT_METHODS = new Set([
|
|
96
|
+
'message.send', 'group.send', 'group.create', 'group.invite',
|
|
97
|
+
'group.kick', 'group.remove_member', 'group.leave', 'group.dissolve',
|
|
98
|
+
'group.update_name', 'group.update_avatar', 'group.update_announcement',
|
|
99
|
+
'group.update_settings', 'group.rotate_epoch',
|
|
100
|
+
'storage.upload', 'storage.complete_upload', 'storage.delete',
|
|
101
|
+
'auth.create_aid', 'auth.renew_cert', 'auth.rekey',
|
|
102
|
+
'message.thought.put', 'group.thought.put',
|
|
103
|
+
'group.add_member',
|
|
104
|
+
]);
|
|
87
105
|
function clampReconnectDelaySeconds(value, fallback, upper = RECONNECT_MAX_BASE_DELAY_SECONDS) {
|
|
88
106
|
const parsed = Number(value);
|
|
89
107
|
const seconds = Number.isFinite(parsed) ? parsed : fallback;
|
|
@@ -269,8 +287,10 @@ export class AUNClient {
|
|
|
269
287
|
_seqTrackerContext = null;
|
|
270
288
|
/** 补洞去重:已完成/进行中的 key 集合,防止重复 pull 同一区间 */
|
|
271
289
|
_gapFillDone = new Set();
|
|
272
|
-
/**
|
|
290
|
+
/** 已发布到应用层的 seq 集合(按命名空间),补洞路径 publish 前检查以避免重复分发 */
|
|
273
291
|
_pushedSeqs = new Map();
|
|
292
|
+
/** 已解密但因 seq 空洞暂缓发布的应用层消息(按 namespace -> seq) */
|
|
293
|
+
_pendingOrderedMsgs = new Map();
|
|
274
294
|
_pendingDecryptMsgs = new Map();
|
|
275
295
|
_groupEpochRotationInflight = new Set();
|
|
276
296
|
_groupEpochRecoveryInflight = new Map();
|
|
@@ -343,7 +363,7 @@ export class AUNClient {
|
|
|
343
363
|
this._onRawGroupChanged(data);
|
|
344
364
|
});
|
|
345
365
|
// 其他事件直接透传
|
|
346
|
-
for (const evt of ['message.recalled', 'message.ack']) {
|
|
366
|
+
for (const evt of ['message.recalled', 'message.ack', 'storage.object_changed']) {
|
|
347
367
|
this._dispatcher.subscribe(`_raw.${evt}`, (data) => {
|
|
348
368
|
this._dispatcher.publish(evt, data);
|
|
349
369
|
});
|
|
@@ -522,11 +542,31 @@ export class AUNClient {
|
|
|
522
542
|
return this._sendGroupEncrypted(p);
|
|
523
543
|
}
|
|
524
544
|
}
|
|
545
|
+
if (method === 'group.thought.put') {
|
|
546
|
+
const encrypt = p.encrypt !== undefined ? p.encrypt : true;
|
|
547
|
+
delete p.encrypt;
|
|
548
|
+
if (!encrypt) {
|
|
549
|
+
throw new ValidationError('group.thought.put requires encrypt=true');
|
|
550
|
+
}
|
|
551
|
+
return this._putGroupThoughtEncrypted(p);
|
|
552
|
+
}
|
|
553
|
+
if (method === 'message.thought.put') {
|
|
554
|
+
const encrypt = p.encrypt !== undefined ? p.encrypt : true;
|
|
555
|
+
delete p.encrypt;
|
|
556
|
+
if (!encrypt) {
|
|
557
|
+
throw new ValidationError('message.thought.put requires encrypt=true');
|
|
558
|
+
}
|
|
559
|
+
return this._putMessageThoughtEncrypted(p);
|
|
560
|
+
}
|
|
525
561
|
// 关键操作自动附加客户端签名
|
|
526
562
|
if (SIGNED_METHODS.has(method)) {
|
|
527
563
|
await this._signClientOperation(method, p);
|
|
528
564
|
}
|
|
529
|
-
|
|
565
|
+
// P1-23: 非幂等方法使用更长超时
|
|
566
|
+
const callTimeout = NON_IDEMPOTENT_METHODS.has(method) ? NON_IDEMPOTENT_TIMEOUT : undefined;
|
|
567
|
+
const result = callTimeout
|
|
568
|
+
? await this._transport.call(method, p, callTimeout)
|
|
569
|
+
: await this._transport.call(method, p);
|
|
530
570
|
// 自动解密:message.pull 返回的消息
|
|
531
571
|
if (method === 'message.pull' && isJsonObject(result)) {
|
|
532
572
|
const r = result;
|
|
@@ -560,6 +600,7 @@ export class AUNClient {
|
|
|
560
600
|
this._transport.call('message.ack', {
|
|
561
601
|
seq: contig,
|
|
562
602
|
device_id: this._deviceId,
|
|
603
|
+
slot_id: this._slotId,
|
|
563
604
|
}).catch((e) => { console.warn('message.pull auto-ack 失败:', e); });
|
|
564
605
|
}
|
|
565
606
|
}
|
|
@@ -608,6 +649,12 @@ export class AUNClient {
|
|
|
608
649
|
}
|
|
609
650
|
}
|
|
610
651
|
}
|
|
652
|
+
if (method === 'group.thought.get' && isJsonObject(result)) {
|
|
653
|
+
return this._decryptGroupThoughts(result);
|
|
654
|
+
}
|
|
655
|
+
if (method === 'message.thought.get' && isJsonObject(result)) {
|
|
656
|
+
return this._decryptMessageThoughts(result);
|
|
657
|
+
}
|
|
611
658
|
// ── Group E2EE 自动编排 ────────────────────────────
|
|
612
659
|
// ── Group E2EE 自动编排(必备能力,始终启用)────────
|
|
613
660
|
{
|
|
@@ -639,7 +686,10 @@ export class AUNClient {
|
|
|
639
686
|
const groupId = this._extractGroupIdFromResult(result) || String(p.group_id ?? '');
|
|
640
687
|
if (groupId && this._membershipRotationChanged(method, result)) {
|
|
641
688
|
const expectedEpoch = this._membershipRotationExpectedEpoch(result);
|
|
642
|
-
|
|
689
|
+
// P0-12: await rotation 完成(带超时兜底),确保后续 group.send 使用新 epoch
|
|
690
|
+
const rotationPromise = this._maybeLeadRotateGroupEpoch(groupId, this._membershipRotationTriggerId(groupId, result), expectedEpoch);
|
|
691
|
+
const timeoutPromise = new Promise((resolve) => globalThis.setTimeout(resolve, 5000));
|
|
692
|
+
await Promise.race([rotationPromise, timeoutPromise]).catch((exc) => console.warn('membership RPC epoch rotation fallback failed:', exc));
|
|
643
693
|
}
|
|
644
694
|
}
|
|
645
695
|
return result;
|
|
@@ -677,10 +727,13 @@ export class AUNClient {
|
|
|
677
727
|
async _processAndPublishMessage(data) {
|
|
678
728
|
try {
|
|
679
729
|
if (!isJsonObject(data)) {
|
|
680
|
-
await this.
|
|
730
|
+
await this._publishAppEvent('message.received', data);
|
|
681
731
|
return;
|
|
682
732
|
}
|
|
683
733
|
const msg = { ...data };
|
|
734
|
+
if (!this._messageTargetsCurrentInstance(msg)) {
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
684
737
|
// 拦截 P2P 传输的群组密钥分发/请求/响应消息
|
|
685
738
|
if (await this._tryHandleGroupKeyMessage(msg)) {
|
|
686
739
|
return;
|
|
@@ -701,20 +754,20 @@ export class AUNClient {
|
|
|
701
754
|
this._transport.call('message.ack', {
|
|
702
755
|
seq: contig,
|
|
703
756
|
device_id: this._deviceId,
|
|
757
|
+
slot_id: this._slotId,
|
|
704
758
|
}).catch((e) => { console.warn('P2P auto-ack 失败:', e); });
|
|
705
759
|
}
|
|
706
760
|
// 即时持久化 cursor,异常断连后不回退
|
|
707
761
|
this._saveSeqTrackerState();
|
|
708
762
|
}
|
|
709
763
|
const decrypted = await this._decryptSingleMessage(msg);
|
|
710
|
-
// 记录已推送的 seq,补洞路径据此去重
|
|
711
764
|
if (seq !== undefined && seq !== null && this._aid) {
|
|
712
765
|
const ns = `p2p:${this._aid}`;
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
766
|
+
await this._publishOrderedMessage('message.received', ns, seq, decrypted);
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
await this._publishAppEvent('message.received', decrypted);
|
|
716
770
|
}
|
|
717
|
-
await this._dispatcher.publish('message.received', decrypted);
|
|
718
771
|
}
|
|
719
772
|
catch (exc) {
|
|
720
773
|
console.warn('消息解密失败:', exc);
|
|
@@ -730,7 +783,7 @@ export class AUNClient {
|
|
|
730
783
|
timestamp: (src.timestamp ?? null),
|
|
731
784
|
_decrypt_error: String(exc),
|
|
732
785
|
};
|
|
733
|
-
await this.
|
|
786
|
+
await this._publishAppEvent('message.undecryptable', safeEvent);
|
|
734
787
|
}
|
|
735
788
|
}
|
|
736
789
|
}
|
|
@@ -747,7 +800,7 @@ export class AUNClient {
|
|
|
747
800
|
async _processAndPublishGroupMessage(data) {
|
|
748
801
|
try {
|
|
749
802
|
if (!isJsonObject(data)) {
|
|
750
|
-
await this.
|
|
803
|
+
await this._publishAppEvent('group.message_created', data);
|
|
751
804
|
return;
|
|
752
805
|
}
|
|
753
806
|
const msg = { ...data };
|
|
@@ -783,19 +836,12 @@ export class AUNClient {
|
|
|
783
836
|
}
|
|
784
837
|
this._saveSeqTrackerState();
|
|
785
838
|
}
|
|
786
|
-
// 记录已推送的 seq,补洞路径据此去重
|
|
787
|
-
if (groupId && seq !== undefined && seq !== null) {
|
|
788
|
-
const nsKey = `group:${groupId}`;
|
|
789
|
-
if (!this._pushedSeqs.has(nsKey))
|
|
790
|
-
this._pushedSeqs.set(nsKey, new Set());
|
|
791
|
-
this._pushedSeqs.get(nsKey).add(seq);
|
|
792
|
-
}
|
|
793
839
|
// R3: 解密失败 → 不 publish 密文给应用层,入 pending 队列 + 发 undecryptable 事件
|
|
794
840
|
const payloadForCheck = isJsonObject(msg.payload) ? msg.payload : null;
|
|
795
841
|
if (payloadForCheck?.type === 'e2ee.group_encrypted' && !decrypted.e2ee) {
|
|
796
842
|
if (groupId)
|
|
797
843
|
this._enqueuePendingDecrypt(groupId, msg);
|
|
798
|
-
await this.
|
|
844
|
+
await this._publishAppEvent('group.message_undecryptable', {
|
|
799
845
|
message_id: msg.message_id ?? null,
|
|
800
846
|
group_id: groupId,
|
|
801
847
|
from: msg.from ?? null,
|
|
@@ -805,7 +851,13 @@ export class AUNClient {
|
|
|
805
851
|
});
|
|
806
852
|
return;
|
|
807
853
|
}
|
|
808
|
-
|
|
854
|
+
if (groupId && seq !== undefined && seq !== null) {
|
|
855
|
+
const nsKey = `group:${groupId}`;
|
|
856
|
+
await this._publishOrderedMessage('group.message_created', nsKey, seq, decrypted);
|
|
857
|
+
}
|
|
858
|
+
else {
|
|
859
|
+
await this._publishAppEvent('group.message_created', decrypted);
|
|
860
|
+
}
|
|
809
861
|
}
|
|
810
862
|
catch (exc) {
|
|
811
863
|
console.warn('群消息解密失败:', exc);
|
|
@@ -820,7 +872,7 @@ export class AUNClient {
|
|
|
820
872
|
timestamp: (src.timestamp ?? null),
|
|
821
873
|
_decrypt_error: String(exc),
|
|
822
874
|
};
|
|
823
|
-
await this.
|
|
875
|
+
await this._publishAppEvent('group.message_undecryptable', safeEvent);
|
|
824
876
|
}
|
|
825
877
|
}
|
|
826
878
|
}
|
|
@@ -828,7 +880,7 @@ export class AUNClient {
|
|
|
828
880
|
async _autoPullGroupMessages(notification) {
|
|
829
881
|
const groupId = (notification.group_id ?? '');
|
|
830
882
|
if (!groupId) {
|
|
831
|
-
await this.
|
|
883
|
+
await this._publishAppEvent('group.message_created', notification);
|
|
832
884
|
return;
|
|
833
885
|
}
|
|
834
886
|
const ns = `group:${groupId}`;
|
|
@@ -844,15 +896,19 @@ export class AUNClient {
|
|
|
844
896
|
const messages = result.messages;
|
|
845
897
|
if (Array.isArray(messages)) {
|
|
846
898
|
// ⚠️ 不再重复调用 onPullResult:call('group.pull') 拦截器已在内部调用过一次
|
|
847
|
-
// pushedSeqs 去重:跳过已通过推送路径分发的消息
|
|
848
899
|
const pushed = this._pushedSeqs.get(ns);
|
|
849
900
|
for (const msg of messages) {
|
|
850
901
|
if (isJsonObject(msg)) {
|
|
851
902
|
const s = msg.seq;
|
|
852
903
|
if (pushed && s !== undefined && s !== null && pushed.has(s)) {
|
|
853
|
-
continue; //
|
|
904
|
+
continue; // 已发布到应用层,跳过
|
|
905
|
+
}
|
|
906
|
+
if (s !== undefined && s !== null) {
|
|
907
|
+
await this._publishOrderedMessage('group.message_created', ns, s, msg);
|
|
908
|
+
}
|
|
909
|
+
else {
|
|
910
|
+
await this._publishAppEvent('group.message_created', msg);
|
|
854
911
|
}
|
|
855
|
-
await this._dispatcher.publish('group.message_created', msg);
|
|
856
912
|
}
|
|
857
913
|
}
|
|
858
914
|
this._prunePushedSeqs(ns);
|
|
@@ -864,7 +920,7 @@ export class AUNClient {
|
|
|
864
920
|
console.warn('自动 pull 群消息失败:', exc);
|
|
865
921
|
}
|
|
866
922
|
// pull 失败时仍透传原始通知
|
|
867
|
-
await this.
|
|
923
|
+
await this._publishAppEvent('group.message_created', notification);
|
|
868
924
|
}
|
|
869
925
|
/** 后台补齐群消息空洞 */
|
|
870
926
|
async _fillGroupGap(groupId) {
|
|
@@ -873,10 +929,6 @@ export class AUNClient {
|
|
|
873
929
|
return;
|
|
874
930
|
const ns = `group:${groupId}`;
|
|
875
931
|
const afterSeq = this._seqTracker.getContiguousSeq(ns);
|
|
876
|
-
// 冷启动(seq=0):服务端推送会带全量消息,SDK 不主动补洞避免重复拉取
|
|
877
|
-
if (afterSeq === 0) {
|
|
878
|
-
return;
|
|
879
|
-
}
|
|
880
932
|
// 去重:同一 (group:id:after_seq) 在飞行中只补一次
|
|
881
933
|
// S1: 使用 try/finally 保证无论成功失败都清理 dedupKey,避免旧实现只在异常路径清理
|
|
882
934
|
// 导致成功后相同 afterSeq 再次出现空洞时被永久抑制。
|
|
@@ -902,7 +954,12 @@ export class AUNClient {
|
|
|
902
954
|
const s = msg.seq;
|
|
903
955
|
if (pushed && s !== undefined && s !== null && pushed.has(s))
|
|
904
956
|
continue;
|
|
905
|
-
|
|
957
|
+
if (s !== undefined && s !== null) {
|
|
958
|
+
await this._publishOrderedMessage('group.message_created', ns, s, msg);
|
|
959
|
+
}
|
|
960
|
+
else {
|
|
961
|
+
await this._publishAppEvent('group.message_created', msg);
|
|
962
|
+
}
|
|
906
963
|
}
|
|
907
964
|
}
|
|
908
965
|
this._prunePushedSeqs(ns);
|
|
@@ -925,10 +982,6 @@ export class AUNClient {
|
|
|
925
982
|
return;
|
|
926
983
|
const ns = `group_event:${groupId}`;
|
|
927
984
|
const afterSeq = this._seqTracker.getContiguousSeq(ns);
|
|
928
|
-
// 冷启动(seq=0):服务端推送会带全量事件,SDK 不主动补洞避免重复拉取
|
|
929
|
-
if (afterSeq === 0) {
|
|
930
|
-
return;
|
|
931
|
-
}
|
|
932
985
|
// 去重:同一 (group_evt:id:after_seq) 在飞行中只补一次
|
|
933
986
|
// S1: 使用 try/finally 保证无论成功失败都清理 dedupKey
|
|
934
987
|
const dedupKey = `group_evt:${groupId}:${afterSeq}`;
|
|
@@ -947,14 +1000,24 @@ export class AUNClient {
|
|
|
947
1000
|
const events = result.events;
|
|
948
1001
|
if (Array.isArray(events)) {
|
|
949
1002
|
this._seqTracker.onPullResult(ns, events.filter(isJsonObject));
|
|
1003
|
+
const cursor = isJsonObject(result.cursor) ? result.cursor : null;
|
|
1004
|
+
const serverAck = cursor ? Number(cursor.current_seq ?? 0) : 0;
|
|
1005
|
+
if (serverAck > 0) {
|
|
1006
|
+
const contigBefore = this._seqTracker.getContiguousSeq(ns);
|
|
1007
|
+
if (contigBefore < serverAck) {
|
|
1008
|
+
console.info('[aun_core] group.pull_events retention-floor 推进: ns=' + ns + ' contiguous=' + contigBefore + ' -> cursor.current_seq=' + serverAck);
|
|
1009
|
+
this._seqTracker.forceContiguousSeq(ns, serverAck);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
950
1012
|
// 持久化 cursor + ack_events(与 Python 对齐)
|
|
951
1013
|
this._saveSeqTrackerState();
|
|
952
1014
|
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
953
|
-
if (contig > 0) {
|
|
1015
|
+
if (contig > 0 && (events.length > 0 || serverAck > 0)) {
|
|
954
1016
|
this._transport.call('group.ack_events', {
|
|
955
1017
|
group_id: groupId,
|
|
956
1018
|
event_seq: contig,
|
|
957
1019
|
device_id: this._deviceId,
|
|
1020
|
+
slot_id: this._slotId,
|
|
958
1021
|
}).catch((e) => { console.warn('群事件 auto-ack 失败: group=' + groupId, e); });
|
|
959
1022
|
}
|
|
960
1023
|
for (const evt of events) {
|
|
@@ -989,10 +1052,6 @@ export class AUNClient {
|
|
|
989
1052
|
return;
|
|
990
1053
|
const ns = `p2p:${this._aid}`;
|
|
991
1054
|
const afterSeq = this._seqTracker.getContiguousSeq(ns);
|
|
992
|
-
// 新设备(seq=0)没有历史 prekey,拉旧消息也解不了
|
|
993
|
-
if (afterSeq === 0) {
|
|
994
|
-
return;
|
|
995
|
-
}
|
|
996
1055
|
// 去重:同一 (type:after_seq) 在飞行中只补一次
|
|
997
1056
|
// S1: 使用 try/finally 保证无论成功失败都清理 dedupKey
|
|
998
1057
|
const dedupKey = `p2p:${afterSeq}`;
|
|
@@ -1016,7 +1075,12 @@ export class AUNClient {
|
|
|
1016
1075
|
const s = msg.seq;
|
|
1017
1076
|
if (pushed && s !== undefined && s !== null && pushed.has(s))
|
|
1018
1077
|
continue;
|
|
1019
|
-
|
|
1078
|
+
if (s !== undefined && s !== null) {
|
|
1079
|
+
await this._publishOrderedMessage('message.received', ns, s, msg);
|
|
1080
|
+
}
|
|
1081
|
+
else {
|
|
1082
|
+
await this._publishAppEvent('message.received', msg);
|
|
1083
|
+
}
|
|
1020
1084
|
}
|
|
1021
1085
|
}
|
|
1022
1086
|
this._prunePushedSeqs(ns);
|
|
@@ -1045,6 +1109,119 @@ export class AUNClient {
|
|
|
1045
1109
|
if (pushed.size === 0)
|
|
1046
1110
|
this._pushedSeqs.delete(ns);
|
|
1047
1111
|
}
|
|
1112
|
+
_markPublishedSeq(ns, seq) {
|
|
1113
|
+
let pushed = this._pushedSeqs.get(ns);
|
|
1114
|
+
if (!pushed) {
|
|
1115
|
+
pushed = new Set();
|
|
1116
|
+
this._pushedSeqs.set(ns, pushed);
|
|
1117
|
+
}
|
|
1118
|
+
pushed.add(seq);
|
|
1119
|
+
if (pushed.size > PUSHED_SEQS_LIMIT) {
|
|
1120
|
+
const keep = [...pushed].sort((a, b) => a - b).slice(-PUSHED_SEQS_LIMIT);
|
|
1121
|
+
this._pushedSeqs.set(ns, new Set(keep));
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
_enqueueOrderedMessage(ns, event, seq, payload) {
|
|
1125
|
+
let queue = this._pendingOrderedMsgs.get(ns);
|
|
1126
|
+
if (!queue) {
|
|
1127
|
+
queue = new Map();
|
|
1128
|
+
this._pendingOrderedMsgs.set(ns, queue);
|
|
1129
|
+
}
|
|
1130
|
+
queue.set(seq, { event, payload });
|
|
1131
|
+
if (queue.size > PENDING_ORDERED_LIMIT) {
|
|
1132
|
+
const drop = [...queue.keys()].sort((a, b) => a - b).slice(0, queue.size - PENDING_ORDERED_LIMIT);
|
|
1133
|
+
for (const oldSeq of drop)
|
|
1134
|
+
queue.delete(oldSeq);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
_isInstanceScopedMessageEvent(event) {
|
|
1138
|
+
return event === 'message.received'
|
|
1139
|
+
|| event === 'message.undecryptable'
|
|
1140
|
+
|| event === 'group.message_created'
|
|
1141
|
+
|| event === 'group.message_undecryptable';
|
|
1142
|
+
}
|
|
1143
|
+
_attachCurrentInstanceContext(payload) {
|
|
1144
|
+
if (!isJsonObject(payload))
|
|
1145
|
+
return payload;
|
|
1146
|
+
const result = { ...payload };
|
|
1147
|
+
if (this._deviceId && !String(result.device_id ?? '').trim()) {
|
|
1148
|
+
result.device_id = this._deviceId;
|
|
1149
|
+
}
|
|
1150
|
+
if (this._slotId && !String(result.slot_id ?? '').trim()) {
|
|
1151
|
+
result.slot_id = this._slotId;
|
|
1152
|
+
}
|
|
1153
|
+
return result;
|
|
1154
|
+
}
|
|
1155
|
+
_normalizePublishedMessagePayload(event, payload) {
|
|
1156
|
+
if (!this._isInstanceScopedMessageEvent(event))
|
|
1157
|
+
return payload;
|
|
1158
|
+
return this._attachCurrentInstanceContext(payload);
|
|
1159
|
+
}
|
|
1160
|
+
async _publishAppEvent(event, payload) {
|
|
1161
|
+
await this._dispatcher.publish(event, this._normalizePublishedMessagePayload(event, payload));
|
|
1162
|
+
}
|
|
1163
|
+
_messageTargetsCurrentInstance(message) {
|
|
1164
|
+
if (!isJsonObject(message))
|
|
1165
|
+
return true;
|
|
1166
|
+
const targetDeviceId = String(message.device_id ?? '').trim();
|
|
1167
|
+
if (targetDeviceId && this._deviceId && targetDeviceId !== this._deviceId) {
|
|
1168
|
+
return false;
|
|
1169
|
+
}
|
|
1170
|
+
const targetSlotId = String(message.slot_id ?? '').trim();
|
|
1171
|
+
if (targetSlotId && this._slotId && targetSlotId !== this._slotId) {
|
|
1172
|
+
return false;
|
|
1173
|
+
}
|
|
1174
|
+
return true;
|
|
1175
|
+
}
|
|
1176
|
+
async _drainOrderedMessages(ns, beforeSeq) {
|
|
1177
|
+
const queue = this._pendingOrderedMsgs.get(ns);
|
|
1178
|
+
if (!queue || queue.size === 0)
|
|
1179
|
+
return;
|
|
1180
|
+
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
1181
|
+
const ready = [...queue.keys()]
|
|
1182
|
+
.filter((seq) => seq <= contig && (beforeSeq === undefined || seq < beforeSeq))
|
|
1183
|
+
.sort((a, b) => a - b);
|
|
1184
|
+
for (const seq of ready) {
|
|
1185
|
+
const item = queue.get(seq);
|
|
1186
|
+
queue.delete(seq);
|
|
1187
|
+
if (!item || this._pushedSeqs.get(ns)?.has(seq))
|
|
1188
|
+
continue;
|
|
1189
|
+
await this._publishAppEvent(item.event, item.payload);
|
|
1190
|
+
this._markPublishedSeq(ns, seq);
|
|
1191
|
+
}
|
|
1192
|
+
if (queue.size === 0)
|
|
1193
|
+
this._pendingOrderedMsgs.delete(ns);
|
|
1194
|
+
}
|
|
1195
|
+
async _publishOrderedMessage(event, ns, seq, payload) {
|
|
1196
|
+
const seqNum = Number(seq);
|
|
1197
|
+
if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0) {
|
|
1198
|
+
await this._publishAppEvent(event, payload);
|
|
1199
|
+
return true;
|
|
1200
|
+
}
|
|
1201
|
+
if (this._pushedSeqs.get(ns)?.has(seqNum)) {
|
|
1202
|
+
const queue = this._pendingOrderedMsgs.get(ns);
|
|
1203
|
+
queue?.delete(seqNum);
|
|
1204
|
+
if (queue && queue.size === 0)
|
|
1205
|
+
this._pendingOrderedMsgs.delete(ns);
|
|
1206
|
+
return false;
|
|
1207
|
+
}
|
|
1208
|
+
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
1209
|
+
if (seqNum > contig) {
|
|
1210
|
+
this._enqueueOrderedMessage(ns, event, seqNum, payload);
|
|
1211
|
+
return false;
|
|
1212
|
+
}
|
|
1213
|
+
await this._drainOrderedMessages(ns, seqNum);
|
|
1214
|
+
if (this._pushedSeqs.get(ns)?.has(seqNum))
|
|
1215
|
+
return false;
|
|
1216
|
+
const queue = this._pendingOrderedMsgs.get(ns);
|
|
1217
|
+
queue?.delete(seqNum);
|
|
1218
|
+
if (queue && queue.size === 0)
|
|
1219
|
+
this._pendingOrderedMsgs.delete(ns);
|
|
1220
|
+
await this._publishAppEvent(event, payload);
|
|
1221
|
+
this._markPublishedSeq(ns, seqNum);
|
|
1222
|
+
await this._drainOrderedMessages(ns);
|
|
1223
|
+
return true;
|
|
1224
|
+
}
|
|
1048
1225
|
/**
|
|
1049
1226
|
* 上线/重连后一次性同步所有已加入群:
|
|
1050
1227
|
* 1. 有 epoch key 的群 → 补消息 + 补事件
|
|
@@ -1313,6 +1490,8 @@ export class AUNClient {
|
|
|
1313
1490
|
// 4. 清理推送 seq 去重缓存
|
|
1314
1491
|
this._pushedSeqs.delete(`group:${groupId}`);
|
|
1315
1492
|
this._pushedSeqs.delete(`group_event:${groupId}`);
|
|
1493
|
+
this._pendingOrderedMsgs.delete(`group:${groupId}`);
|
|
1494
|
+
this._pendingDecryptMsgs.delete(`group:${groupId}`);
|
|
1316
1495
|
console.info(`[aun_core] 已清理解散群组 ${groupId} 的本地状态`);
|
|
1317
1496
|
}
|
|
1318
1497
|
async _verifyEventSignature(_event, cs) {
|
|
@@ -1346,11 +1525,19 @@ export class AUNClient {
|
|
|
1346
1525
|
const ok = await ecdsaVerifyDer(pubKey, sigBytes, signData);
|
|
1347
1526
|
if (!ok) {
|
|
1348
1527
|
console.warn('[aun_core] 群事件验签失败 aid=%s method=%s', sigAid, method);
|
|
1528
|
+
// P1-16: 签名失败统一发布事件
|
|
1529
|
+
this._dispatcher.publish('signature.verification_failed', {
|
|
1530
|
+
aid: sigAid, method, error: 'ECDSA verification failed',
|
|
1531
|
+
});
|
|
1349
1532
|
}
|
|
1350
1533
|
return ok;
|
|
1351
1534
|
}
|
|
1352
1535
|
catch (exc) {
|
|
1353
1536
|
console.warn('[aun_core] 群事件验签异常:', exc);
|
|
1537
|
+
// P1-16: 签名失败统一发布事件
|
|
1538
|
+
this._dispatcher.publish('signature.verification_failed', {
|
|
1539
|
+
aid: sigAid, method, error: String(exc),
|
|
1540
|
+
});
|
|
1354
1541
|
return false;
|
|
1355
1542
|
}
|
|
1356
1543
|
}
|
|
@@ -1666,49 +1853,114 @@ export class AUNClient {
|
|
|
1666
1853
|
}
|
|
1667
1854
|
/** 自动加密并发送群组消息 */
|
|
1668
1855
|
async _sendGroupEncrypted(params) {
|
|
1669
|
-
|
|
1856
|
+
return this._callGroupEncryptedRpc('group.send', params, {
|
|
1857
|
+
idField: 'message_id',
|
|
1858
|
+
idPrefix: 'gm',
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
async _putGroupThoughtEncrypted(params) {
|
|
1862
|
+
return this._callGroupEncryptedRpc('group.thought.put', params, {
|
|
1863
|
+
idField: 'thought_id',
|
|
1864
|
+
idPrefix: 'gt',
|
|
1865
|
+
extraFields: ['reply_to'],
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
async _putMessageThoughtEncrypted(params) {
|
|
1869
|
+
const toAid = String(params.to ?? '').trim();
|
|
1870
|
+
this._validateMessageRecipient(toAid);
|
|
1670
1871
|
const payload = isJsonObject(params.payload) ? params.payload : null;
|
|
1671
|
-
if (!
|
|
1672
|
-
throw new ValidationError('
|
|
1872
|
+
if (!toAid) {
|
|
1873
|
+
throw new ValidationError('message.thought.put requires to');
|
|
1673
1874
|
}
|
|
1674
1875
|
if (payload === null) {
|
|
1675
|
-
throw new ValidationError('
|
|
1876
|
+
throw new ValidationError('message.thought.put payload must be an object when encrypt=true');
|
|
1676
1877
|
}
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
await this.
|
|
1682
|
-
await this.
|
|
1878
|
+
const thoughtId = String(params.thought_id ?? '') || `mt-${_uuidV4()}`;
|
|
1879
|
+
const timestamp = Number(params.timestamp ?? Date.now());
|
|
1880
|
+
const prekey = await this._fetchPeerPrekey(toAid);
|
|
1881
|
+
const peerCertFingerprint = typeof prekey?.cert_fingerprint === 'string' ? prekey.cert_fingerprint : undefined;
|
|
1882
|
+
const peerCertPem = await this._fetchPeerCert(toAid, peerCertFingerprint);
|
|
1883
|
+
const [envelope, encryptResult] = await this._encryptCopyPayload({
|
|
1884
|
+
logicalToAid: toAid,
|
|
1885
|
+
payload,
|
|
1886
|
+
peerCertPem,
|
|
1887
|
+
prekey,
|
|
1888
|
+
messageId: thoughtId,
|
|
1889
|
+
timestamp,
|
|
1890
|
+
});
|
|
1891
|
+
await this._ensureEncryptResult(toAid, encryptResult);
|
|
1892
|
+
const sendParams = {
|
|
1893
|
+
to: toAid,
|
|
1894
|
+
payload: envelope,
|
|
1895
|
+
type: 'e2ee.encrypted',
|
|
1896
|
+
encrypted: true,
|
|
1897
|
+
thought_id: thoughtId,
|
|
1898
|
+
timestamp,
|
|
1899
|
+
reply_to: params.reply_to,
|
|
1900
|
+
};
|
|
1901
|
+
await this._signClientOperation('message.thought.put', sendParams);
|
|
1902
|
+
return this._transport.call('message.thought.put', sendParams);
|
|
1903
|
+
}
|
|
1904
|
+
async _callGroupEncryptedRpc(method, params, options) {
|
|
1905
|
+
let prepared = await this._prepareGroupEncryptedRpcParams(method, params, options);
|
|
1683
1906
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1684
|
-
const epochResult = await this._committedGroupEpochState(groupId);
|
|
1685
|
-
const committedEpoch = Number(epochResult.committed_epoch ?? epochResult.epoch ?? 0);
|
|
1686
|
-
const envelope = committedEpoch > 0
|
|
1687
|
-
? await this._groupE2ee.encryptWithEpoch(groupId, await this._ensureCommittedGroupSecretForSend(groupId, committedEpoch, epochResult), payload)
|
|
1688
|
-
: await this._groupE2ee.encrypt(groupId, payload);
|
|
1689
|
-
const sendParams = {
|
|
1690
|
-
group_id: groupId,
|
|
1691
|
-
payload: envelope,
|
|
1692
|
-
type: 'e2ee.group_encrypted',
|
|
1693
|
-
encrypted: true,
|
|
1694
|
-
};
|
|
1695
|
-
if (this._deviceId && sendParams.device_id === undefined) {
|
|
1696
|
-
sendParams.device_id = this._deviceId;
|
|
1697
|
-
}
|
|
1698
|
-
await this._signClientOperation('group.send', sendParams);
|
|
1699
1907
|
try {
|
|
1700
|
-
return await this._transport.call(
|
|
1908
|
+
return await this._transport.call(method, prepared.sendParams);
|
|
1701
1909
|
}
|
|
1702
1910
|
catch (exc) {
|
|
1703
1911
|
if (attempt === 0 && this._isRecoverableGroupEpochError(exc)) {
|
|
1704
|
-
console.warn(`[aun_core] 群 ${groupId}
|
|
1705
|
-
await this.
|
|
1912
|
+
console.warn(`[aun_core] 群 ${prepared.groupId} 调用 ${method} 时 epoch 已过旧,恢复密钥后重加密重试一次: ${formatCaughtError(exc)}`);
|
|
1913
|
+
prepared = await this._prepareGroupEncryptedRpcParams(method, params, options, true);
|
|
1706
1914
|
continue;
|
|
1707
1915
|
}
|
|
1708
1916
|
throw exc;
|
|
1709
1917
|
}
|
|
1710
1918
|
}
|
|
1711
|
-
throw new StateError(
|
|
1919
|
+
throw new StateError(`${method} failed after epoch recovery retry: group=${prepared.groupId}`);
|
|
1920
|
+
}
|
|
1921
|
+
async _prepareGroupEncryptedRpcParams(method, params, options, strictEpochReady = false) {
|
|
1922
|
+
const groupId = String(params.group_id ?? '');
|
|
1923
|
+
const payload = isJsonObject(params.payload) ? params.payload : null;
|
|
1924
|
+
if (!groupId) {
|
|
1925
|
+
throw new ValidationError(`${method} requires group_id`);
|
|
1926
|
+
}
|
|
1927
|
+
if (payload === null) {
|
|
1928
|
+
throw new ValidationError(`${method} payload must be an object when encrypt=true`);
|
|
1929
|
+
}
|
|
1930
|
+
if (!this._groupSynced.has(groupId)) {
|
|
1931
|
+
await this._lazySyncGroup(groupId);
|
|
1932
|
+
}
|
|
1933
|
+
await this._ensureGroupEpochReady(groupId, strictEpochReady);
|
|
1934
|
+
await this._waitForGroupMembershipEpochFloor(groupId, 2000);
|
|
1935
|
+
const epochResult = await this._committedGroupEpochState(groupId);
|
|
1936
|
+
const committedEpoch = Number(epochResult.committed_epoch ?? epochResult.epoch ?? 0);
|
|
1937
|
+
const envelope = committedEpoch > 0
|
|
1938
|
+
? await this._groupE2ee.encryptWithEpoch(groupId, await this._ensureCommittedGroupSecretForSend(groupId, committedEpoch, epochResult), payload)
|
|
1939
|
+
: await this._groupE2ee.encrypt(groupId, payload);
|
|
1940
|
+
const operationId = String(params[options.idField] ?? '').trim()
|
|
1941
|
+
|| `${options.idPrefix}-${crypto.randomUUID()}`;
|
|
1942
|
+
const timestamp = Number(params.timestamp ?? Date.now());
|
|
1943
|
+
const sendParams = {
|
|
1944
|
+
group_id: groupId,
|
|
1945
|
+
payload: envelope,
|
|
1946
|
+
type: 'e2ee.group_encrypted',
|
|
1947
|
+
encrypted: true,
|
|
1948
|
+
[options.idField]: operationId,
|
|
1949
|
+
timestamp,
|
|
1950
|
+
};
|
|
1951
|
+
if (this._deviceId && sendParams.device_id === undefined) {
|
|
1952
|
+
sendParams.device_id = this._deviceId;
|
|
1953
|
+
}
|
|
1954
|
+
if (sendParams.slot_id === undefined) {
|
|
1955
|
+
sendParams.slot_id = this._slotId;
|
|
1956
|
+
}
|
|
1957
|
+
for (const field of options.extraFields ?? []) {
|
|
1958
|
+
if (params[field] !== undefined) {
|
|
1959
|
+
sendParams[field] = params[field];
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
await this._signClientOperation(method, sendParams);
|
|
1963
|
+
return { sendParams, groupId };
|
|
1712
1964
|
}
|
|
1713
1965
|
/**
|
|
1714
1966
|
* 首次发送群消息前懒拉取历史消息,同步 seqTracker 避免空洞。
|
|
@@ -2030,7 +2282,7 @@ export class AUNClient {
|
|
|
2030
2282
|
const ns = `group:${groupId}`;
|
|
2031
2283
|
const queue = this._pendingDecryptMsgs.get(ns) ?? [];
|
|
2032
2284
|
queue.push(msg);
|
|
2033
|
-
this._pendingDecryptMsgs.set(ns, queue.slice(-
|
|
2285
|
+
this._pendingDecryptMsgs.set(ns, queue.slice(-PENDING_DECRYPT_LIMIT));
|
|
2034
2286
|
}
|
|
2035
2287
|
async _retryPendingDecryptMsgs(groupId) {
|
|
2036
2288
|
const ns = `group:${groupId}`;
|
|
@@ -2047,14 +2299,20 @@ export class AUNClient {
|
|
|
2047
2299
|
stillPending.push(msg);
|
|
2048
2300
|
continue;
|
|
2049
2301
|
}
|
|
2050
|
-
|
|
2302
|
+
const seq = msg.seq;
|
|
2303
|
+
if (seq !== undefined && seq !== null) {
|
|
2304
|
+
await this._publishOrderedMessage('group.message_created', ns, seq, decrypted);
|
|
2305
|
+
}
|
|
2306
|
+
else {
|
|
2307
|
+
await this._publishAppEvent('group.message_created', decrypted);
|
|
2308
|
+
}
|
|
2051
2309
|
}
|
|
2052
2310
|
catch {
|
|
2053
2311
|
stillPending.push(msg);
|
|
2054
2312
|
}
|
|
2055
2313
|
}
|
|
2056
2314
|
const queuedDuringRetry = this._pendingDecryptMsgs.get(ns) ?? [];
|
|
2057
|
-
const mergedPending = [...stillPending, ...queuedDuringRetry].slice(-
|
|
2315
|
+
const mergedPending = [...stillPending, ...queuedDuringRetry].slice(-PENDING_DECRYPT_LIMIT);
|
|
2058
2316
|
if (mergedPending.length)
|
|
2059
2317
|
this._pendingDecryptMsgs.set(ns, mergedPending);
|
|
2060
2318
|
else
|
|
@@ -2144,7 +2402,7 @@ export class AUNClient {
|
|
|
2144
2402
|
async _decryptGroupMessage(message, opts) {
|
|
2145
2403
|
const payload = isJsonObject(message.payload) ? message.payload : null;
|
|
2146
2404
|
if (payload === null || payload.type !== 'e2ee.group_encrypted') {
|
|
2147
|
-
return message;
|
|
2405
|
+
return this._attachGroupDispatchModeToPayload(message);
|
|
2148
2406
|
}
|
|
2149
2407
|
// 确保发送方证书已缓存(签名验证需要)
|
|
2150
2408
|
const senderAid = String(message.from ?? message.sender_aid ?? '');
|
|
@@ -2158,7 +2416,7 @@ export class AUNClient {
|
|
|
2158
2416
|
// 先尝试直接解密
|
|
2159
2417
|
const result = await this._groupE2ee.decrypt(message, opts);
|
|
2160
2418
|
if (result !== null && isJsonObject(result.e2ee)) {
|
|
2161
|
-
return result;
|
|
2419
|
+
return this._attachGroupDispatchModeToPayload(result);
|
|
2162
2420
|
}
|
|
2163
2421
|
// replay guard 命中:decrypt 返回了原消息(非 null)但无 e2ee 字段
|
|
2164
2422
|
// 不是解密失败,不应触发 recover
|
|
@@ -2174,7 +2432,7 @@ export class AUNClient {
|
|
|
2174
2432
|
if (await this._recoverGroupEpochKey(groupId, epoch, sender, 5000)) {
|
|
2175
2433
|
const retry = await this._groupE2ee.decrypt(message, opts);
|
|
2176
2434
|
if (retry !== null && retry.e2ee)
|
|
2177
|
-
return retry;
|
|
2435
|
+
return this._attachGroupDispatchModeToPayload(retry);
|
|
2178
2436
|
}
|
|
2179
2437
|
}
|
|
2180
2438
|
catch (exc) {
|
|
@@ -2183,6 +2441,21 @@ export class AUNClient {
|
|
|
2183
2441
|
}
|
|
2184
2442
|
return message;
|
|
2185
2443
|
}
|
|
2444
|
+
_attachGroupDispatchModeToPayload(message) {
|
|
2445
|
+
const payload = message.payload;
|
|
2446
|
+
if (!isJsonObject(payload))
|
|
2447
|
+
return message;
|
|
2448
|
+
const rawMode = String(message.dispatch_mode ?? 'broadcast').trim().toLowerCase();
|
|
2449
|
+
const mode = rawMode === 'mention' || rawMode === 'broadcast' ? rawMode : 'broadcast';
|
|
2450
|
+
return {
|
|
2451
|
+
...message,
|
|
2452
|
+
dispatch_mode: mode,
|
|
2453
|
+
payload: {
|
|
2454
|
+
...payload,
|
|
2455
|
+
dispatch_mode: mode,
|
|
2456
|
+
},
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2186
2459
|
/** 批量解密群组消息(用于 group.pull)。跳过防重放检查。 */
|
|
2187
2460
|
async _decryptGroupMessages(messages) {
|
|
2188
2461
|
const result = [];
|
|
@@ -2194,10 +2467,104 @@ export class AUNClient {
|
|
|
2194
2467
|
this._enqueuePendingDecrypt(groupId, msg);
|
|
2195
2468
|
continue; // R3: 解密失败不入 result,不 publish 密文给应用层
|
|
2196
2469
|
}
|
|
2470
|
+
if (payload?.type !== 'e2ee.group_encrypted') {
|
|
2471
|
+
result.push(this._attachGroupDispatchModeToPayload(decrypted));
|
|
2472
|
+
continue;
|
|
2473
|
+
}
|
|
2197
2474
|
result.push(decrypted);
|
|
2198
2475
|
}
|
|
2199
2476
|
return result;
|
|
2200
2477
|
}
|
|
2478
|
+
async _decryptGroupThoughts(result) {
|
|
2479
|
+
if (!result.found) {
|
|
2480
|
+
return { ...result, thoughts: [] };
|
|
2481
|
+
}
|
|
2482
|
+
const items = (Array.isArray(result.thoughts) ? result.thoughts : []).filter(isJsonObject);
|
|
2483
|
+
if (!items.length) {
|
|
2484
|
+
return { ...result, thoughts: [] };
|
|
2485
|
+
}
|
|
2486
|
+
const groupId = String(result.group_id ?? '');
|
|
2487
|
+
const senderAid = String(result.sender_aid ?? '');
|
|
2488
|
+
const thoughts = [];
|
|
2489
|
+
for (const item of items) {
|
|
2490
|
+
const payload = isJsonObject(item.payload) ? item.payload : null;
|
|
2491
|
+
const thoughtId = String(item.thought_id ?? item.message_id ?? '');
|
|
2492
|
+
const message = {
|
|
2493
|
+
group_id: groupId,
|
|
2494
|
+
sender_aid: senderAid,
|
|
2495
|
+
from: senderAid,
|
|
2496
|
+
message_id: thoughtId,
|
|
2497
|
+
payload: payload ?? {},
|
|
2498
|
+
created_at: Number(item.created_at ?? 0),
|
|
2499
|
+
};
|
|
2500
|
+
const decrypted = await this._decryptGroupMessage(message, { skipReplay: true });
|
|
2501
|
+
if (payload?.type === 'e2ee.group_encrypted' && groupId && !decrypted.e2ee) {
|
|
2502
|
+
this._enqueuePendingDecrypt(groupId, message);
|
|
2503
|
+
continue;
|
|
2504
|
+
}
|
|
2505
|
+
thoughts.push({
|
|
2506
|
+
thought_id: thoughtId,
|
|
2507
|
+
message_id: thoughtId,
|
|
2508
|
+
reply_to: item.reply_to,
|
|
2509
|
+
payload: decrypted.payload,
|
|
2510
|
+
created_at: item.created_at,
|
|
2511
|
+
e2ee: decrypted.e2ee,
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
2514
|
+
return { ...result, thoughts };
|
|
2515
|
+
}
|
|
2516
|
+
async _decryptMessageThoughts(result) {
|
|
2517
|
+
if (!result.found) {
|
|
2518
|
+
return { ...result, thoughts: [] };
|
|
2519
|
+
}
|
|
2520
|
+
const items = (Array.isArray(result.thoughts) ? result.thoughts : []).filter(isJsonObject);
|
|
2521
|
+
if (!items.length) {
|
|
2522
|
+
return { ...result, thoughts: [] };
|
|
2523
|
+
}
|
|
2524
|
+
const senderAid = String(result.sender_aid ?? '');
|
|
2525
|
+
const peerAid = String(result.peer_aid ?? '');
|
|
2526
|
+
const thoughts = [];
|
|
2527
|
+
for (const item of items) {
|
|
2528
|
+
const payload = isJsonObject(item.payload) ? item.payload : null;
|
|
2529
|
+
const thoughtId = String(item.thought_id ?? item.message_id ?? '');
|
|
2530
|
+
const fromAid = String(item.from ?? senderAid);
|
|
2531
|
+
const toAid = String(item.to ?? peerAid);
|
|
2532
|
+
const message = {
|
|
2533
|
+
from: fromAid,
|
|
2534
|
+
to: toAid,
|
|
2535
|
+
message_id: thoughtId,
|
|
2536
|
+
payload: payload ?? {},
|
|
2537
|
+
encrypted: item.encrypted !== false,
|
|
2538
|
+
timestamp: Number(item.created_at ?? 0),
|
|
2539
|
+
};
|
|
2540
|
+
let decrypted = message;
|
|
2541
|
+
if (payload?.type === 'e2ee.encrypted') {
|
|
2542
|
+
const senderCertFingerprint = String(payload.sender_cert_fingerprint ?? payload.aad?.sender_cert_fingerprint ?? '').trim().toLowerCase();
|
|
2543
|
+
if (fromAid) {
|
|
2544
|
+
const certReady = await this._ensureSenderCertCached(fromAid, senderCertFingerprint || undefined);
|
|
2545
|
+
if (!certReady) {
|
|
2546
|
+
console.warn('[aun_core] 无法获取发送方证书,跳过 message.thought.get 解密:', fromAid);
|
|
2547
|
+
continue;
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
decrypted = await this._e2ee.decryptMessage(message, { skipReplay: true });
|
|
2551
|
+
if (decrypted === null || (isJsonObject(decrypted.payload) && decrypted.payload.type === 'e2ee.encrypted')) {
|
|
2552
|
+
continue;
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
thoughts.push({
|
|
2556
|
+
thought_id: thoughtId,
|
|
2557
|
+
message_id: thoughtId,
|
|
2558
|
+
reply_to: item.reply_to,
|
|
2559
|
+
from: fromAid,
|
|
2560
|
+
to: toAid,
|
|
2561
|
+
payload: decrypted.payload,
|
|
2562
|
+
created_at: item.created_at,
|
|
2563
|
+
e2ee: decrypted.e2ee,
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
return { ...result, thoughts };
|
|
2567
|
+
}
|
|
2201
2568
|
/**
|
|
2202
2569
|
* 尝试处理 P2P 传输的群组密钥消息。
|
|
2203
2570
|
*
|
|
@@ -3292,34 +3659,45 @@ export class AUNClient {
|
|
|
3292
3659
|
// 避免 reader 把积压 push 交给空 tracker 的 handler,触发 S2 历史 gap 误补拉。
|
|
3293
3660
|
this._refreshSeqTrackerContext();
|
|
3294
3661
|
await this._restoreSeqTrackerState();
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
this._sessionParams
|
|
3662
|
+
try {
|
|
3663
|
+
const challenge = await this._transport.connect(gatewayUrl);
|
|
3664
|
+
this._state = 'authenticating';
|
|
3665
|
+
if (allowReauth) {
|
|
3666
|
+
const authContext = await this._auth.connectSession(this._transport, challenge, gatewayUrl, {
|
|
3667
|
+
accessToken: params.access_token,
|
|
3668
|
+
deviceId: this._deviceId,
|
|
3669
|
+
slotId: this._slotId,
|
|
3670
|
+
deliveryMode: this._connectDeliveryMode,
|
|
3671
|
+
});
|
|
3672
|
+
if (isJsonObject(authContext)) {
|
|
3673
|
+
const auth = authContext;
|
|
3674
|
+
const identity = auth.identity;
|
|
3675
|
+
if (identity && isJsonObject(identity)) {
|
|
3676
|
+
this._identity = identity;
|
|
3677
|
+
this._aid = String(identity.aid ?? this._aid ?? '');
|
|
3678
|
+
if (this._sessionParams) {
|
|
3679
|
+
this._sessionParams.access_token = String(auth.token ?? params.access_token ?? '');
|
|
3680
|
+
}
|
|
3312
3681
|
}
|
|
3313
3682
|
}
|
|
3314
3683
|
}
|
|
3684
|
+
else {
|
|
3685
|
+
await this._auth.initializeWithToken(this._transport, challenge, String(params.access_token), {
|
|
3686
|
+
deviceId: this._deviceId,
|
|
3687
|
+
slotId: this._slotId,
|
|
3688
|
+
deliveryMode: this._connectDeliveryMode,
|
|
3689
|
+
});
|
|
3690
|
+
await this._syncIdentityAfterConnect(String(params.access_token));
|
|
3691
|
+
}
|
|
3315
3692
|
}
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
}
|
|
3322
|
-
|
|
3693
|
+
catch (err) {
|
|
3694
|
+
// P1-19: 首连失败时重置状态,避免半连接残留
|
|
3695
|
+
this._state = 'disconnected';
|
|
3696
|
+
try {
|
|
3697
|
+
await this._transport.close();
|
|
3698
|
+
}
|
|
3699
|
+
catch { /* 忽略关闭错误 */ }
|
|
3700
|
+
throw err;
|
|
3323
3701
|
}
|
|
3324
3702
|
this._state = 'connected';
|
|
3325
3703
|
await this._dispatcher.publish('connection.state', {
|
|
@@ -3666,6 +4044,26 @@ export class AUNClient {
|
|
|
3666
4044
|
throw new ValidationError('group.send does not accept delivery_mode; group messages are always fanout');
|
|
3667
4045
|
}
|
|
3668
4046
|
}
|
|
4047
|
+
if (method === 'group.thought.put' || method === 'group.thought.get'
|
|
4048
|
+
|| method === 'message.thought.put' || method === 'message.thought.get') {
|
|
4049
|
+
const replyTo = isJsonObject(params.reply_to) ? params.reply_to : null;
|
|
4050
|
+
const replyMsgId = String(replyTo?.message_id ?? '').trim();
|
|
4051
|
+
if (!replyMsgId) {
|
|
4052
|
+
throw new ValidationError(`${method} requires reply_to.message_id`);
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
if (method === 'group.thought.get' && !String(params.sender_aid ?? '').trim()) {
|
|
4056
|
+
throw new ValidationError('group.thought.get requires sender_aid');
|
|
4057
|
+
}
|
|
4058
|
+
if (method === 'message.thought.put') {
|
|
4059
|
+
this._validateMessageRecipient(params.to);
|
|
4060
|
+
if (!String(params.to ?? '').trim()) {
|
|
4061
|
+
throw new ValidationError('message.thought.put requires to');
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
if (method === 'message.thought.get' && !String(params.sender_aid ?? '').trim()) {
|
|
4065
|
+
throw new ValidationError('message.thought.get requires sender_aid');
|
|
4066
|
+
}
|
|
3669
4067
|
}
|
|
3670
4068
|
_currentMessageDeliveryMode() {
|
|
3671
4069
|
return { ...this._connectDeliveryMode };
|
|
@@ -3959,6 +4357,8 @@ export class AUNClient {
|
|
|
3959
4357
|
this._seqTrackerContext = null;
|
|
3960
4358
|
this._gapFillDone.clear();
|
|
3961
4359
|
this._pushedSeqs.clear();
|
|
4360
|
+
this._pendingOrderedMsgs.clear();
|
|
4361
|
+
this._pendingDecryptMsgs.clear();
|
|
3962
4362
|
this._groupSynced.clear();
|
|
3963
4363
|
this._p2pSynced = false;
|
|
3964
4364
|
}
|
|
@@ -3969,6 +4369,8 @@ export class AUNClient {
|
|
|
3969
4369
|
this._seqTracker = new SeqTracker();
|
|
3970
4370
|
this._gapFillDone.clear();
|
|
3971
4371
|
this._pushedSeqs.clear();
|
|
4372
|
+
this._pendingOrderedMsgs.clear();
|
|
4373
|
+
this._pendingDecryptMsgs.clear();
|
|
3972
4374
|
this._groupSynced.clear();
|
|
3973
4375
|
this._p2pSynced = false;
|
|
3974
4376
|
this._seqTrackerContext = nextContext;
|