@agentunion/fastaun 0.2.19 → 0.2.20
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/CHANGELOG.md +23 -0
- package/_packed_docs/CHANGELOG.md +23 -0
- package/_packed_docs/agent.md/SCHEMA.md +173 -0
- package/_packed_docs/agent.md/examples/codeagent-claudecode.md +61 -0
- package/_packed_docs/agent.md/examples/human-developer.md +60 -0
- package/_packed_docs/agent.md/examples/openclaw-lobster.md +52 -0
- package/_packed_docs/agent.md/examples/signed-openclaw-lobster.md +43 -0
- package/_packed_docs/protocol/00-/346/200/273/350/247/210/344/270/216/345/210/206/345/261/202.md +205 -0
- package/_packed_docs/protocol/00A-/350/256/276/350/256/241/345/216/237/345/210/231-/344/270/272Agent/350/200/214/347/224/237.md +197 -0
- package/_packed_docs/protocol/01-/350/272/253/344/273/275/344/270/216/345/207/255/350/257/201/345/215/217/350/256/256-auth.md +549 -0
- package/_packed_docs/protocol/02-/350/257/201/344/271/246/344/270/216/344/277/241/344/273/273/344/275/223/347/263/273.md +810 -0
- package/_packed_docs/protocol/03-Gateway-/350/277/236/346/216/245/346/250/241/345/274/217.md +262 -0
- package/_packed_docs/protocol/04-Peer-/345/255/220/345/215/217/350/256/256.md +180 -0
- package/_packed_docs/protocol/05-Relay-/345/255/220/345/215/217/350/256/256.md +164 -0
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1135 -0
- package/_packed_docs/protocol/07-/351/224/231/350/257/257/347/240/201/344/270/216/347/212/266/346/200/201/346/234/272.md +234 -0
- package/_packed_docs/protocol/08-AUN-E2EE-Group.md +900 -0
- package/_packed_docs/protocol/08-AUN-E2EE.md +413 -0
- package/_packed_docs/protocol/09-/345/256/211/345/205/250/350/200/203/350/231/221.md +316 -0
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +804 -0
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +271 -0
- package/_packed_docs/protocol/12-Stream-/345/255/220/345/215/217/350/256/256.md +329 -0
- package/_packed_docs/protocol/13-Agent/350/241/214/344/270/272/350/247/204/350/214/203.md +141 -0
- package/_packed_docs/protocol/14-/344/272/244/344/272/222/346/234/272/345/210/266-/345/223/215/345/272/224/346/250/241/345/274/217/344/270/216/350/207/252/344/270/273/346/250/241/345/274/217.md +170 -0
- package/_packed_docs/protocol/README.md +71 -0
- package/_packed_docs/protocol/agent.md/SCHEMA.md +118 -0
- package/_packed_docs/protocol/agent.md/examples/codeagent-claudecode.md +61 -0
- package/_packed_docs/protocol/agent.md/examples/human-developer.md +60 -0
- package/_packed_docs/protocol/agent.md/examples/openclaw-lobster.md +52 -0
- package/_packed_docs/protocol/aun-docs-guide.md +49 -0
- package/_packed_docs/protocol/index.md +114 -0
- package/_packed_docs/protocol//350/215/211/346/241/210-agent.md/347/255/276/345/220/215/345/215/217/350/256/256.md +205 -0
- package/_packed_docs/protocol//350/215/211/346/241/210-/346/213/222/347/273/235/344/277/241/345/217/267/345/215/217/350/256/256.md +249 -0
- package/_packed_docs/protocol//351/231/204/345/275/225A-/346/234/257/350/257/255/350/241/250.md +337 -0
- package/_packed_docs/protocol//351/231/204/345/275/225B-/346/211/251/345/261/225/346/200/247/346/214/207/345/215/227.md +80 -0
- package/_packed_docs/protocol//351/231/204/345/275/225C-/347/247/201/351/222/245/347/256/241/347/220/206/344/270/216/350/272/253/344/273/275/346/201/242/345/244/215.md +704 -0
- package/_packed_docs/protocol//351/231/204/345/275/225D-Root_CA_/346/262/273/347/220/206/346/234/272/345/210/266.md +620 -0
- package/_packed_docs/protocol//351/231/204/345/275/225E-Root_CA_/345/207/206/345/205/245/346/265/201/347/250/213.md +605 -0
- package/_packed_docs/protocol//351/231/204/345/275/225F-Issuer_CA_/347/224/263/350/257/267/346/265/201/347/250/213.md +548 -0
- package/_packed_docs/protocol//351/231/204/345/275/225G-AID_/345/255/244/345/204/277/351/242/204/351/230/262/344/270/216/346/225/221/346/217/264/346/234/272/345/210/266.md +513 -0
- package/_packed_docs/protocol//351/231/204/345/275/225H-Identity/346/234/215/345/212/241/345/256/236/347/216/260/346/214/207/345/215/227.md +619 -0
- package/_packed_docs/protocol//351/231/204/345/275/225I-/350/267/250/345/237/237/346/266/210/346/201/257/350/267/257/347/224/261/345/256/236/347/216/260/346/214/207/345/215/227.md +492 -0
- package/_packed_docs/protocol//351/231/204/345/275/225J-/345/256/242/346/210/267/347/253/257/346/216/245/345/205/245/347/244/272/344/276/213.md +402 -0
- package/_packed_docs/protocol//351/231/204/345/275/225K-Agent_Web/345/217/221/347/216/260/345/215/217/350/256/256.md +130 -0
- package/_packed_docs/protocol//351/231/204/345/275/225L-E2EE/345/256/236/347/216/260/346/214/207/345/215/227.md +267 -0
- package/_packed_docs/protocol//351/231/204/345/275/225M-JWT/350/256/244/350/257/201/345/256/236/347/216/260/346/214/207/345/215/227.md +367 -0
- package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +223 -0
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +354 -0
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +172 -0
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +373 -0
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +611 -0
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +1152 -0
- package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +150 -0
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +89 -0
- package/_packed_docs/sdk/09-custody-api-manual.md +445 -0
- package/_packed_docs/sdk/09-group-rpc-manual.md +1895 -0
- package/_packed_docs/sdk/09-message-rpc-manual.md +597 -0
- package/_packed_docs/sdk/09-meta-rpc-manual.md +142 -0
- package/_packed_docs/sdk/09-payload-reference.md +702 -0
- package/_packed_docs/sdk/09-storage-rpc-manual.md +408 -0
- package/_packed_docs/sdk/09-stream-rpc-manual.md +275 -0
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +72 -0
- package/_packed_docs/sdk/INDEX.md +131 -0
- package/_packed_docs/sdk/README.md +307 -0
- package/dist/auth.d.ts +2 -1
- package/dist/auth.js +13 -11
- package/dist/auth.js.map +1 -1
- package/dist/client.d.ts +32 -5
- package/dist/client.js +187 -99
- package/dist/client.js.map +1 -1
- package/dist/namespaces/auth.d.ts +1 -0
- package/dist/namespaces/auth.js +20 -6
- package/dist/namespaces/auth.js.map +1 -1
- package/dist/transport.d.ts +10 -0
- package/dist/transport.js +24 -0
- package/dist/transport.js.map +1 -1
- package/package.json +45 -42
package/dist/client.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* - 群组 E2EE 全自动编排(建群/加人/踢人/退出)
|
|
12
12
|
*/
|
|
13
13
|
import * as crypto from 'node:crypto';
|
|
14
|
+
import * as fs from 'node:fs';
|
|
14
15
|
import * as http from 'node:http';
|
|
15
16
|
import * as https from 'node:https';
|
|
16
17
|
import { join } from 'node:path';
|
|
@@ -81,7 +82,7 @@ const DEFAULT_SESSION_OPTIONS = {
|
|
|
81
82
|
},
|
|
82
83
|
timeouts: {
|
|
83
84
|
connect: 5.0,
|
|
84
|
-
call:
|
|
85
|
+
call: 35.0,
|
|
85
86
|
http: 30.0,
|
|
86
87
|
},
|
|
87
88
|
};
|
|
@@ -93,6 +94,20 @@ const GROUP_ROTATION_RETRY_MAX_DELAY_MS = 300_000;
|
|
|
93
94
|
const PENDING_DECRYPT_LIMIT = 100;
|
|
94
95
|
const PUSHED_SEQS_LIMIT = 50_000;
|
|
95
96
|
const PENDING_ORDERED_LIMIT = 50_000;
|
|
97
|
+
// 心跳间隔下/上限(秒)。0 = 关闭心跳;负值视为 0;其余值 clamp 到 [10, 600]。
|
|
98
|
+
// 服务端通过 hello.heartbeat_interval 与 meta.ping pong 中的同名字段下发。
|
|
99
|
+
const HEARTBEAT_MIN_INTERVAL_SECONDS = 10;
|
|
100
|
+
const HEARTBEAT_MAX_INTERVAL_SECONDS = 600;
|
|
101
|
+
function clampHeartbeatInterval(value) {
|
|
102
|
+
const n = Number(value);
|
|
103
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
104
|
+
return 0;
|
|
105
|
+
if (n < HEARTBEAT_MIN_INTERVAL_SECONDS)
|
|
106
|
+
return HEARTBEAT_MIN_INTERVAL_SECONDS;
|
|
107
|
+
if (n > HEARTBEAT_MAX_INTERVAL_SECONDS)
|
|
108
|
+
return HEARTBEAT_MAX_INTERVAL_SECONDS;
|
|
109
|
+
return n;
|
|
110
|
+
}
|
|
96
111
|
// P1-23: 非幂等方法使用更长超时(35s),避免 SDK 10s 超时 < gateway 30s 处理时间
|
|
97
112
|
const NON_IDEMPOTENT_TIMEOUT_MS = 35_000;
|
|
98
113
|
const NON_IDEMPOTENT_METHODS = new Set([
|
|
@@ -337,6 +352,7 @@ export class AUNClient {
|
|
|
337
352
|
/** 当前实例上下文 */
|
|
338
353
|
_deviceId;
|
|
339
354
|
_slotId;
|
|
355
|
+
_connectedAt = 0;
|
|
340
356
|
_connectDeliveryMode;
|
|
341
357
|
_defaultConnectDeliveryMode;
|
|
342
358
|
/** peer 证书缓存 */
|
|
@@ -346,13 +362,18 @@ export class AUNClient {
|
|
|
346
362
|
_prekeyReplenished = new Set();
|
|
347
363
|
// 当前活跃 prekey:只有这个 prekey 被消费时才触发 replenish 上传
|
|
348
364
|
_activePrekeyId = '';
|
|
365
|
+
// 本地 agent.md 文件路径与对应 etag(quoted sha256 hex,与服务端 _agent_md_etag 一致)。
|
|
366
|
+
// 由 setLocalAgentMdPath() 设置;用于跟服务端 RPC 注入的 _meta.agent_md_etag 比对,
|
|
367
|
+
// 触发"本地未发布到服务端"或"服务端版本更新"的 UI 提示。
|
|
368
|
+
_localAgentMdPath = '';
|
|
369
|
+
_localAgentMdEtag = '';
|
|
370
|
+
// gateway 在 RPC envelope._meta.agent_md_etag 注入的服务端 etag;纯观察,无下游依赖。
|
|
371
|
+
_remoteAgentMdEtag = '';
|
|
349
372
|
/** 消息序列号跟踪器(群消息 + P2P 空洞检测) */
|
|
350
373
|
_seqTracker = new SeqTracker();
|
|
351
374
|
_seqTrackerContext = null;
|
|
352
375
|
/** 惰性群同步:已同步过的 group_id 集合 */
|
|
353
376
|
_groupSynced = new Set();
|
|
354
|
-
/** 惰性 P2P 同步:是否已同步过 */
|
|
355
|
-
_p2pSynced = false;
|
|
356
377
|
/** 补洞去重:已完成/进行中的 key -> 开始时间戳,防止重复 pull 同一区间 */
|
|
357
378
|
_gapFillDone = new Map();
|
|
358
379
|
/** 已发布到应用层的 seq 集合(按命名空间),补洞路径 publish 前检查以避免重复分发 */
|
|
@@ -432,6 +453,7 @@ export class AUNClient {
|
|
|
432
453
|
verifySsl: this._configModel.verifySsl,
|
|
433
454
|
logger: this._logger.for('aun_core.transport'),
|
|
434
455
|
});
|
|
456
|
+
this._transport.setMetaObserver((meta) => this._observeRpcMeta(meta));
|
|
435
457
|
this._e2ee = new E2EEManager({
|
|
436
458
|
identityFn: () => this._identity ?? {},
|
|
437
459
|
deviceIdFn: () => this._deviceId,
|
|
@@ -469,6 +491,65 @@ export class AUNClient {
|
|
|
469
491
|
get aid() {
|
|
470
492
|
return this._aid;
|
|
471
493
|
}
|
|
494
|
+
/**
|
|
495
|
+
* 记录本地 agent.md 文件路径并一次性计算 etag(quoted sha256,与服务端一致)。
|
|
496
|
+
*
|
|
497
|
+
* - path 为空字符串:清除本地 path 与 etag。
|
|
498
|
+
* - 文件不存在 / 读取失败:清除 etag 并返回空串,不抛异常(应用可读 getLocalAgentMdEtag()
|
|
499
|
+
* 为空判断)。
|
|
500
|
+
* - 浏览器环境无文件系统:直接返回空串,记录 warn 日志。
|
|
501
|
+
* - 文件变更后需要重新调用 setLocalAgentMdPath() 触发重算(按设计:设置时一次性计算)。
|
|
502
|
+
*
|
|
503
|
+
* 返回当前 etag(quoted hex 或空串)。
|
|
504
|
+
*/
|
|
505
|
+
setLocalAgentMdPath(path) {
|
|
506
|
+
const rawPath = String(path ?? '').trim();
|
|
507
|
+
if (!rawPath) {
|
|
508
|
+
this._localAgentMdPath = '';
|
|
509
|
+
this._localAgentMdEtag = '';
|
|
510
|
+
return '';
|
|
511
|
+
}
|
|
512
|
+
// 浏览器环境没有 fs,直接退回空串。Node 环境才尝试读文件。
|
|
513
|
+
const isNode = typeof process !== 'undefined' && !!process.versions?.node;
|
|
514
|
+
if (!isNode) {
|
|
515
|
+
this._clientLog.warn(`setLocalAgentMdPath skipped: not running in Node.js (path=${rawPath})`);
|
|
516
|
+
this._localAgentMdPath = rawPath;
|
|
517
|
+
this._localAgentMdEtag = '';
|
|
518
|
+
return '';
|
|
519
|
+
}
|
|
520
|
+
this._localAgentMdPath = rawPath;
|
|
521
|
+
try {
|
|
522
|
+
const data = fs.readFileSync(rawPath);
|
|
523
|
+
const digest = crypto.createHash('sha256').update(data).digest('hex');
|
|
524
|
+
this._localAgentMdEtag = `"${digest}"`;
|
|
525
|
+
}
|
|
526
|
+
catch (err) {
|
|
527
|
+
this._clientLog.warn(`setLocalAgentMdPath 读取失败 path=${rawPath} err=${err instanceof Error ? err.message : String(err)}`);
|
|
528
|
+
this._localAgentMdEtag = '';
|
|
529
|
+
}
|
|
530
|
+
return this._localAgentMdEtag;
|
|
531
|
+
}
|
|
532
|
+
/** 返回 setLocalAgentMdPath 计算的 etag;未设置或读取失败时返回空串。 */
|
|
533
|
+
getLocalAgentMdEtag() {
|
|
534
|
+
return this._localAgentMdEtag;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* 返回 gateway 在最近一次 RPC envelope._meta 注入的服务端 agent.md etag。
|
|
538
|
+
*
|
|
539
|
+
* 未收到过则为空串;不阻塞调用,纯内存读。
|
|
540
|
+
*/
|
|
541
|
+
getRemoteAgentMdEtag() {
|
|
542
|
+
return this._remoteAgentMdEtag;
|
|
543
|
+
}
|
|
544
|
+
/** transport 的 meta observer:吸收 gateway 注入的 _meta 字段。失败不影响业务。 */
|
|
545
|
+
_observeRpcMeta(meta) {
|
|
546
|
+
if (!meta || typeof meta !== 'object')
|
|
547
|
+
return;
|
|
548
|
+
const etag = String(meta.agent_md_etag ?? '').trim();
|
|
549
|
+
if (etag) {
|
|
550
|
+
this._remoteAgentMdEtag = etag;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
472
553
|
/** 连接状态 */
|
|
473
554
|
get state() {
|
|
474
555
|
return this._state;
|
|
@@ -519,7 +600,7 @@ export class AUNClient {
|
|
|
519
600
|
this._sessionParams = normalized;
|
|
520
601
|
this._sessionOptions = this._buildSessionOptions(normalized);
|
|
521
602
|
const callTimeoutSec = this._sessionOptions.timeouts.call;
|
|
522
|
-
this._transport.setTimeout(callTimeoutSec != null ? callTimeoutSec * 1000 :
|
|
603
|
+
this._transport.setTimeout(callTimeoutSec != null ? callTimeoutSec * 1000 : 35_000);
|
|
523
604
|
this._closing = false;
|
|
524
605
|
this._clientLog.debug(`connect enter: gateway=${String(normalized.gateway ?? '')}, device_id=${this._deviceId}`);
|
|
525
606
|
try {
|
|
@@ -666,6 +747,7 @@ export class AUNClient {
|
|
|
666
747
|
return await this._sendEncrypted(p);
|
|
667
748
|
}
|
|
668
749
|
// encrypt=false:明文走通用 RPC 路径;protected_headers/headers 是信封元数据,加密与否都保留
|
|
750
|
+
this._maybeAppendEchoTraceSend(p);
|
|
669
751
|
}
|
|
670
752
|
// 自动加密:group.send 默认加密(encrypt 默认 True)
|
|
671
753
|
if (method === 'group.send') {
|
|
@@ -674,6 +756,7 @@ export class AUNClient {
|
|
|
674
756
|
if (encrypt) {
|
|
675
757
|
return await this._sendGroupEncrypted(p);
|
|
676
758
|
}
|
|
759
|
+
this._maybeAppendEchoTraceSend(p);
|
|
677
760
|
}
|
|
678
761
|
if (method === 'group.thought.put') {
|
|
679
762
|
const encrypt = p.encrypt ?? true;
|
|
@@ -938,10 +1021,8 @@ export class AUNClient {
|
|
|
938
1021
|
const persistRequired = Boolean(params.persist_required || params.durable);
|
|
939
1022
|
const protectedHeaders = this._protectedHeadersFromParams(params);
|
|
940
1023
|
this._clientLog.debug(`_sendEncrypted enter: to=${toAid}, message_id=${messageId}`);
|
|
941
|
-
//
|
|
942
|
-
|
|
943
|
-
await this._lazySyncP2p();
|
|
944
|
-
}
|
|
1024
|
+
// 惰性 P2P 同步由 connect/reconnect 完成后的 _fillP2pGap 异步触发,
|
|
1025
|
+
// 不再在 send 路径上 await(与 C++ FillP2PGap 行为对齐,避免阻塞用户发送)。
|
|
945
1026
|
// 内部发送逻辑,refreshPeerMaterial=true 时强制清缓存重新拉取对端材料
|
|
946
1027
|
const sendAttempt = async (refreshPeerMaterial = false) => {
|
|
947
1028
|
const recipientPrekeys = refreshPeerMaterial
|
|
@@ -954,26 +1035,13 @@ export class AUNClient {
|
|
|
954
1035
|
timestamp,
|
|
955
1036
|
protectedHeaders,
|
|
956
1037
|
});
|
|
957
|
-
//
|
|
958
|
-
// 占位符 PREKEY_FALLBACK_DEVICE_ID 表示服务端未分配真实设备 ID,不可用于多设备路由
|
|
1038
|
+
// 统一 multi-device 路径:必须有 routable prekey
|
|
959
1039
|
const routablePrekeys = recipientPrekeys.filter(pk => {
|
|
960
1040
|
const did = String(pk.device_id ?? '').trim();
|
|
961
1041
|
return did && did !== PREKEY_FALLBACK_DEVICE_ID;
|
|
962
1042
|
});
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
// single 路径仅在完全没有 routable prekey 时使用(legacy 兼容)。
|
|
966
|
-
const canUseMultiDevice = routablePrekeys.length > 0;
|
|
967
|
-
if (!canUseMultiDevice) {
|
|
968
|
-
return await this._sendEncryptedSingle({
|
|
969
|
-
toAid,
|
|
970
|
-
payload,
|
|
971
|
-
messageId,
|
|
972
|
-
timestamp,
|
|
973
|
-
prekey: routablePrekeys[0] ?? recipientPrekeys[0],
|
|
974
|
-
persistRequired,
|
|
975
|
-
protectedHeaders,
|
|
976
|
-
});
|
|
1043
|
+
if (routablePrekeys.length === 0) {
|
|
1044
|
+
throw new Error(`no registered device prekeys for ${toAid}, cannot send encrypted message`);
|
|
977
1045
|
}
|
|
978
1046
|
const recipientCopies = await this._buildRecipientDeviceCopies({
|
|
979
1047
|
toAid,
|
|
@@ -1025,39 +1093,6 @@ export class AUNClient {
|
|
|
1025
1093
|
throw exc;
|
|
1026
1094
|
}
|
|
1027
1095
|
}
|
|
1028
|
-
async _sendEncryptedSingle(opts) {
|
|
1029
|
-
this._clientLog.debug(`_sendEncryptedSingle enter: to=${opts.toAid}, message_id=${opts.messageId}, has_prekey=${!!opts.prekey}, persist_required=${!!opts.persistRequired}`);
|
|
1030
|
-
let prekey = opts.prekey ?? null;
|
|
1031
|
-
if (!prekey) {
|
|
1032
|
-
this._clientLog.debug(`_sendEncryptedSingle fetching peer prekey: to=${opts.toAid}`);
|
|
1033
|
-
prekey = await this._fetchPeerPrekey(opts.toAid);
|
|
1034
|
-
}
|
|
1035
|
-
const peerCertFingerprint = typeof prekey?.cert_fingerprint === 'string' ? prekey.cert_fingerprint : undefined;
|
|
1036
|
-
const peerCertPem = await this._fetchPeerCert(opts.toAid, peerCertFingerprint);
|
|
1037
|
-
const [envelope, encryptResult] = this._encryptCopyPayload({
|
|
1038
|
-
logicalToAid: opts.toAid,
|
|
1039
|
-
payload: opts.payload,
|
|
1040
|
-
peerCertPem,
|
|
1041
|
-
prekey,
|
|
1042
|
-
messageId: opts.messageId,
|
|
1043
|
-
timestamp: opts.timestamp,
|
|
1044
|
-
protectedHeaders: opts.protectedHeaders,
|
|
1045
|
-
});
|
|
1046
|
-
this._ensureEncryptResult(opts.toAid, encryptResult);
|
|
1047
|
-
this._clientLog.debug(`_sendEncryptedSingle envelope built: to=${opts.toAid}, message_id=${opts.messageId}, scheme=${String(envelope?.scheme ?? '')}`);
|
|
1048
|
-
const sendParams = {
|
|
1049
|
-
to: opts.toAid,
|
|
1050
|
-
payload: envelope,
|
|
1051
|
-
type: 'e2ee.encrypted',
|
|
1052
|
-
encrypted: true,
|
|
1053
|
-
message_id: opts.messageId,
|
|
1054
|
-
timestamp: opts.timestamp,
|
|
1055
|
-
};
|
|
1056
|
-
if (opts.persistRequired) {
|
|
1057
|
-
sendParams.persist_required = true;
|
|
1058
|
-
}
|
|
1059
|
-
return await this._transport.call('message.send', sendParams);
|
|
1060
|
-
}
|
|
1061
1096
|
async _buildRecipientDeviceCopies(opts) {
|
|
1062
1097
|
this._clientLog.debug(`_buildRecipientDeviceCopies enter: to=${opts.toAid}, message_id=${opts.messageId}, prekey_count=${opts.prekeys.length}`);
|
|
1063
1098
|
const recipientCopies = [];
|
|
@@ -1349,33 +1384,6 @@ export class AUNClient {
|
|
|
1349
1384
|
this._clientLog.warn(`lazy sync group ${groupId} failed: ${formatCaughtError(exc)}`);
|
|
1350
1385
|
}
|
|
1351
1386
|
}
|
|
1352
|
-
/** 惰性同步:首次激活 P2P 通道时 pull 最近消息,建立 seq 基线 */
|
|
1353
|
-
async _lazySyncP2p() {
|
|
1354
|
-
this._p2pSynced = true;
|
|
1355
|
-
if (!this._aid)
|
|
1356
|
-
return;
|
|
1357
|
-
try {
|
|
1358
|
-
const ns = `p2p:${this._aid}`;
|
|
1359
|
-
const afterSeq = this._seqTracker.getContiguousSeq(ns);
|
|
1360
|
-
const result = await this._transport.call('message.pull', {
|
|
1361
|
-
after_seq: afterSeq,
|
|
1362
|
-
limit: 200,
|
|
1363
|
-
});
|
|
1364
|
-
const messages = Array.isArray(result?.messages) ? result.messages : [];
|
|
1365
|
-
for (const msg of messages) {
|
|
1366
|
-
const seq = msg?.seq;
|
|
1367
|
-
if (seq != null)
|
|
1368
|
-
this._seqTracker.onMessageSeq(ns, Number(seq));
|
|
1369
|
-
}
|
|
1370
|
-
if (messages.length > 0) {
|
|
1371
|
-
this._saveSeqTrackerState();
|
|
1372
|
-
this._clientLog.info(`lazy sync P2P: pull ${messages.length} messages, after_seq=${afterSeq}`);
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
catch (exc) {
|
|
1376
|
-
this._clientLog.warn(`lazy sync P2P failed: ${formatCaughtError(exc)}`);
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
1387
|
_isGroupEpochTooOldError(exc) {
|
|
1380
1388
|
const text = String(exc).toLowerCase();
|
|
1381
1389
|
return text.includes('e2ee epoch too old') || text.includes('epoch below sender membership floor');
|
|
@@ -1774,7 +1782,6 @@ export class AUNClient {
|
|
|
1774
1782
|
// P2P 空洞检测
|
|
1775
1783
|
const seq = msg.seq;
|
|
1776
1784
|
if (seq !== undefined && seq !== null && this._aid) {
|
|
1777
|
-
this._p2pSynced = true; // 收到推送即视为已激活
|
|
1778
1785
|
const ns = `p2p:${this._aid}`;
|
|
1779
1786
|
const needPull = this._seqTracker.onMessageSeq(ns, seq);
|
|
1780
1787
|
if (needPull) {
|
|
@@ -2108,8 +2115,64 @@ export class AUNClient {
|
|
|
2108
2115
|
return this._attachCurrentInstanceContext(payload);
|
|
2109
2116
|
}
|
|
2110
2117
|
async _publishAppEvent(event, payload) {
|
|
2118
|
+
if ((event === 'message.received' || event === 'group.message_created') && isJsonObject(payload)) {
|
|
2119
|
+
this._maybeAppendEchoTraceReceive(payload);
|
|
2120
|
+
}
|
|
2121
|
+
// 注入本地/远端 agent.md etag,让应用层判断版本一致性;失败不影响业务。
|
|
2122
|
+
if (isJsonObject(payload)) {
|
|
2123
|
+
try {
|
|
2124
|
+
const localEtag = this._localAgentMdEtag || '';
|
|
2125
|
+
const remoteEtag = this._remoteAgentMdEtag || '';
|
|
2126
|
+
if (localEtag || remoteEtag) {
|
|
2127
|
+
const obj = payload;
|
|
2128
|
+
if (!('_agent_md' in obj)) {
|
|
2129
|
+
obj._agent_md = {
|
|
2130
|
+
local_etag: localEtag,
|
|
2131
|
+
remote_etag: remoteEtag,
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
catch (err) {
|
|
2137
|
+
this._clientLog.debug(`agent_md etag inject skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2111
2140
|
await this._dispatcher.publish(event, this._normalizePublishedMessagePayload(event, payload));
|
|
2112
2141
|
}
|
|
2142
|
+
_echoTimestamp() {
|
|
2143
|
+
const now = new Date();
|
|
2144
|
+
const hh = String(now.getHours()).padStart(2, '0');
|
|
2145
|
+
const mm = String(now.getMinutes()).padStart(2, '0');
|
|
2146
|
+
const ss = String(now.getSeconds()).padStart(2, '0');
|
|
2147
|
+
const ms = String(now.getMilliseconds()).padStart(3, '0');
|
|
2148
|
+
return `${hh}:${mm}:${ss}.${ms}`;
|
|
2149
|
+
}
|
|
2150
|
+
_isEchoPayload(payload) {
|
|
2151
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
2152
|
+
return false;
|
|
2153
|
+
const text = payload.text;
|
|
2154
|
+
if (typeof text !== 'string' || text.length > 4096)
|
|
2155
|
+
return false;
|
|
2156
|
+
return text.split('\n', 1)[0].toLowerCase().includes('echo');
|
|
2157
|
+
}
|
|
2158
|
+
_maybeAppendEchoTraceSend(params) {
|
|
2159
|
+
const payload = params.payload;
|
|
2160
|
+
if (!this._isEchoPayload(payload))
|
|
2161
|
+
return;
|
|
2162
|
+
const uptime = this._connectedAt ? Math.floor((Date.now() - this._connectedAt) / 1000) : 0;
|
|
2163
|
+
const trace = `${this._echoTimestamp()} [AUN-SDK.send] aid=${this._aid ?? '-'} conn_uptime=${uptime}s`;
|
|
2164
|
+
params.payload = { ...payload, text: payload.text + '\n' + trace };
|
|
2165
|
+
}
|
|
2166
|
+
_maybeAppendEchoTraceReceive(msg) {
|
|
2167
|
+
if (msg.encrypted)
|
|
2168
|
+
return;
|
|
2169
|
+
const payload = msg.payload;
|
|
2170
|
+
if (!this._isEchoPayload(payload))
|
|
2171
|
+
return;
|
|
2172
|
+
const uptime = this._connectedAt ? Math.floor((Date.now() - this._connectedAt) / 1000) : 0;
|
|
2173
|
+
const trace = `${this._echoTimestamp()} [AUN-SDK.receive] aid=${this._aid ?? '-'} conn_uptime=${uptime}s`;
|
|
2174
|
+
msg.payload = { ...payload, text: payload.text + '\n' + trace };
|
|
2175
|
+
}
|
|
2113
2176
|
_messageTargetsCurrentInstance(message) {
|
|
2114
2177
|
if (!isJsonObject(message))
|
|
2115
2178
|
return true;
|
|
@@ -4829,7 +4892,6 @@ export class AUNClient {
|
|
|
4829
4892
|
this._pendingOrderedMsgs.clear();
|
|
4830
4893
|
this._pendingDecryptMsgs.clear();
|
|
4831
4894
|
this._groupSynced.clear();
|
|
4832
|
-
this._p2pSynced = false;
|
|
4833
4895
|
}
|
|
4834
4896
|
_refreshSeqTrackerContext() {
|
|
4835
4897
|
const nextContext = this._currentSeqTrackerContext();
|
|
@@ -4841,7 +4903,6 @@ export class AUNClient {
|
|
|
4841
4903
|
this._pendingOrderedMsgs.clear();
|
|
4842
4904
|
this._pendingDecryptMsgs.clear();
|
|
4843
4905
|
this._groupSynced.clear();
|
|
4844
|
-
this._p2pSynced = false;
|
|
4845
4906
|
this._seqTrackerContext = nextContext;
|
|
4846
4907
|
}
|
|
4847
4908
|
/** 将 SeqTracker 状态保存到 keystore */
|
|
@@ -4967,10 +5028,13 @@ export class AUNClient {
|
|
|
4967
5028
|
this._sessionParams.access_token = String(auth.token ?? params.access_token ?? '');
|
|
4968
5029
|
}
|
|
4969
5030
|
}
|
|
5031
|
+
if (isJsonObject(auth.hello) && 'heartbeat_interval' in auth.hello) {
|
|
5032
|
+
this._applyServerHeartbeatInterval(auth.hello.heartbeat_interval, 'auth');
|
|
5033
|
+
}
|
|
4970
5034
|
}
|
|
4971
5035
|
}
|
|
4972
5036
|
else {
|
|
4973
|
-
await this._auth.initializeWithToken(this._transport, challenge, String(params.access_token), {
|
|
5037
|
+
const hello = await this._auth.initializeWithToken(this._transport, challenge, String(params.access_token), {
|
|
4974
5038
|
deviceId: this._deviceId,
|
|
4975
5039
|
slotId: this._slotId,
|
|
4976
5040
|
deliveryMode: this._connectDeliveryMode,
|
|
@@ -4979,8 +5043,12 @@ export class AUNClient {
|
|
|
4979
5043
|
extraInfo,
|
|
4980
5044
|
});
|
|
4981
5045
|
this._syncIdentityAfterConnect(String(params.access_token));
|
|
5046
|
+
if (isJsonObject(hello) && 'heartbeat_interval' in hello) {
|
|
5047
|
+
this._applyServerHeartbeatInterval(hello.heartbeat_interval, 'auth');
|
|
5048
|
+
}
|
|
4982
5049
|
}
|
|
4983
5050
|
this._state = 'connected';
|
|
5051
|
+
this._connectedAt = Date.now();
|
|
4984
5052
|
this._clientLog.debug(`auth complete, connection ready: aid=${this._aid ?? ''}, gateway=${gatewayUrl}`);
|
|
4985
5053
|
await this._dispatcher.publish('connection.state', { state: this._state, gateway: gatewayUrl });
|
|
4986
5054
|
// auth 阶段 aid 可能被 identity 覆盖(上方 this._aid = identity.aid);
|
|
@@ -5105,7 +5173,7 @@ export class AUNClient {
|
|
|
5105
5173
|
_buildSessionOptions(params) {
|
|
5106
5174
|
const connectionKind = String(params.connection_kind ?? 'long');
|
|
5107
5175
|
const options = {
|
|
5108
|
-
auto_reconnect:
|
|
5176
|
+
auto_reconnect: DEFAULT_SESSION_OPTIONS.auto_reconnect,
|
|
5109
5177
|
heartbeat_interval: DEFAULT_SESSION_OPTIONS.heartbeat_interval,
|
|
5110
5178
|
token_refresh_before: DEFAULT_SESSION_OPTIONS.token_refresh_before,
|
|
5111
5179
|
retry: { ...DEFAULT_SESSION_OPTIONS.retry },
|
|
@@ -5129,11 +5197,12 @@ export class AUNClient {
|
|
|
5129
5197
|
// ── 内部:后台任务 ────────────────────────────────────────
|
|
5130
5198
|
/** 启动所有后台任务 */
|
|
5131
5199
|
_startBackgroundTasks() {
|
|
5132
|
-
//
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5200
|
+
// 短连接不启动 heartbeat 与 token 刷新(生命周期短,不需要长期会话维护);
|
|
5201
|
+
// auto_reconnect 仍允许,由 _sessionOptions.auto_reconnect 决定
|
|
5202
|
+
if (this._sessionOptions.connection_kind !== 'short') {
|
|
5203
|
+
this._startHeartbeatTask();
|
|
5204
|
+
this._startTokenRefreshTask();
|
|
5205
|
+
}
|
|
5137
5206
|
this._startGroupEpochTasks();
|
|
5138
5207
|
}
|
|
5139
5208
|
/** 停止所有后台任务 */
|
|
@@ -5171,10 +5240,9 @@ export class AUNClient {
|
|
|
5171
5240
|
_startHeartbeatTask() {
|
|
5172
5241
|
if (this._heartbeatTimer !== null)
|
|
5173
5242
|
return;
|
|
5174
|
-
const
|
|
5175
|
-
if (
|
|
5243
|
+
const interval = clampHeartbeatInterval(this._sessionOptions.heartbeat_interval ?? DEFAULT_SESSION_OPTIONS.heartbeat_interval);
|
|
5244
|
+
if (interval <= 0)
|
|
5176
5245
|
return;
|
|
5177
|
-
const interval = Math.max(rawIntervalSeconds, 30) * 1000;
|
|
5178
5246
|
// M25: 把连续失败阈值从 3 次收窄到 2 次。既能容忍一次网络抖动/GC 暂停,
|
|
5179
5247
|
// 又把半开连接的检测延迟从 3 个心跳周期降到 2 个。
|
|
5180
5248
|
// 真正的 socket 死亡由 ws.on('close') 立即触发 _handleTransportDisconnect,
|
|
@@ -5184,8 +5252,12 @@ export class AUNClient {
|
|
|
5184
5252
|
this._heartbeatTimer = setInterval(() => {
|
|
5185
5253
|
if (this._closing || this._state !== 'connected')
|
|
5186
5254
|
return;
|
|
5187
|
-
this._transport.call('meta.ping', {}).then(() => {
|
|
5255
|
+
this._transport.call('meta.ping', {}).then((pong) => {
|
|
5188
5256
|
consecutiveFailures = 0;
|
|
5257
|
+
// 服务端可在 pong 中下发新的 heartbeat_interval(秒,0=关闭)
|
|
5258
|
+
if (isJsonObject(pong) && 'heartbeat_interval' in pong) {
|
|
5259
|
+
this._applyServerHeartbeatInterval(pong.heartbeat_interval, 'pong');
|
|
5260
|
+
}
|
|
5189
5261
|
}).catch((exc) => {
|
|
5190
5262
|
consecutiveFailures++;
|
|
5191
5263
|
this._clientLog.warn(`heartbeat failed (${consecutiveFailures}/${maxFailures}): ${formatCaughtError(exc)}`);
|
|
@@ -5195,12 +5267,28 @@ export class AUNClient {
|
|
|
5195
5267
|
this._handleTransportDisconnect(exc instanceof Error ? exc : new Error(String(exc)));
|
|
5196
5268
|
}
|
|
5197
5269
|
});
|
|
5198
|
-
}, interval);
|
|
5270
|
+
}, interval * 1000);
|
|
5199
5271
|
// 允许 Node.js 进程在只剩定时器时退出
|
|
5200
5272
|
if (this._heartbeatTimer && typeof this._heartbeatTimer === 'object' && 'unref' in this._heartbeatTimer) {
|
|
5201
5273
|
this._heartbeatTimer.unref();
|
|
5202
5274
|
}
|
|
5203
5275
|
}
|
|
5276
|
+
/** 服务端通过 hello/pong 下发 heartbeat_interval;clamp 后写入 session_options 并按需重启心跳。 */
|
|
5277
|
+
_applyServerHeartbeatInterval(raw, source) {
|
|
5278
|
+
const newInterval = clampHeartbeatInterval(raw);
|
|
5279
|
+
const oldInterval = clampHeartbeatInterval(this._sessionOptions.heartbeat_interval);
|
|
5280
|
+
if (newInterval === oldInterval)
|
|
5281
|
+
return;
|
|
5282
|
+
this._sessionOptions.heartbeat_interval = newInterval;
|
|
5283
|
+
this._clientLog.debug(`heartbeat_interval updated by ${source}: ${oldInterval} -> ${newInterval}`);
|
|
5284
|
+
if (this._heartbeatTimer !== null) {
|
|
5285
|
+
clearInterval(this._heartbeatTimer);
|
|
5286
|
+
this._heartbeatTimer = null;
|
|
5287
|
+
}
|
|
5288
|
+
if (newInterval > 0 && this._state === 'connected' && !this._closing) {
|
|
5289
|
+
this._startHeartbeatTask();
|
|
5290
|
+
}
|
|
5291
|
+
}
|
|
5204
5292
|
/** 启动 token 刷新任务 */
|
|
5205
5293
|
_startTokenRefreshTask() {
|
|
5206
5294
|
if (this._tokenRefreshTimer !== null)
|