@agentunion/fastaun-browser 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 +26 -0
- package/_packed_docs/CHANGELOG.md +26 -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.d.ts.map +1 -1
- package/dist/auth.js +13 -11
- package/dist/auth.js.map +1 -1
- package/dist/client.d.ts +38 -8
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +179 -97
- package/dist/client.js.map +1 -1
- package/dist/namespaces/auth.d.ts +1 -0
- package/dist/namespaces/auth.d.ts.map +1 -1
- package/dist/namespaces/auth.js +20 -6
- package/dist/namespaces/auth.js.map +1 -1
- package/dist/transport.d.ts +9 -1
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +24 -0
- package/dist/transport.js.map +1 -1
- package/package.json +40 -37
package/dist/client.js
CHANGED
|
@@ -87,7 +87,7 @@ const DEFAULT_SESSION_OPTIONS = {
|
|
|
87
87
|
},
|
|
88
88
|
timeouts: {
|
|
89
89
|
connect: 5.0,
|
|
90
|
-
call:
|
|
90
|
+
call: 35.0,
|
|
91
91
|
http: 30.0,
|
|
92
92
|
},
|
|
93
93
|
};
|
|
@@ -99,6 +99,20 @@ const GROUP_ROTATION_RETRY_MAX_DELAY_MS = 300_000;
|
|
|
99
99
|
const PENDING_DECRYPT_LIMIT = 100;
|
|
100
100
|
const PUSHED_SEQS_LIMIT = 50_000;
|
|
101
101
|
const PENDING_ORDERED_LIMIT = 50_000;
|
|
102
|
+
// 心跳间隔下/上限(秒)。0 = 关闭心跳;负值视为 0;其余值 clamp 到 [10, 600]。
|
|
103
|
+
// 服务端通过 hello.heartbeat_interval 与 meta.ping pong 中的同名字段下发。
|
|
104
|
+
const HEARTBEAT_MIN_INTERVAL_SECONDS = 10;
|
|
105
|
+
const HEARTBEAT_MAX_INTERVAL_SECONDS = 600;
|
|
106
|
+
function clampHeartbeatInterval(value) {
|
|
107
|
+
const n = Number(value);
|
|
108
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
109
|
+
return 0;
|
|
110
|
+
if (n < HEARTBEAT_MIN_INTERVAL_SECONDS)
|
|
111
|
+
return HEARTBEAT_MIN_INTERVAL_SECONDS;
|
|
112
|
+
if (n > HEARTBEAT_MAX_INTERVAL_SECONDS)
|
|
113
|
+
return HEARTBEAT_MAX_INTERVAL_SECONDS;
|
|
114
|
+
return n;
|
|
115
|
+
}
|
|
102
116
|
// P1-23: 非幂等方法使用更长超时(35s),避免 SDK 10s 超时 < gateway 30s 处理时间
|
|
103
117
|
const NON_IDEMPOTENT_TIMEOUT = 35;
|
|
104
118
|
const NON_IDEMPOTENT_METHODS = new Set([
|
|
@@ -321,6 +335,7 @@ export class AUNClient {
|
|
|
321
335
|
_gatewayUrl = null;
|
|
322
336
|
_deviceId;
|
|
323
337
|
_slotId;
|
|
338
|
+
_connectedAt = 0;
|
|
324
339
|
_connectDeliveryMode;
|
|
325
340
|
_defaultConnectDeliveryMode;
|
|
326
341
|
_closing = false;
|
|
@@ -354,6 +369,17 @@ export class AUNClient {
|
|
|
354
369
|
_groupEpochCleanupTimer = null;
|
|
355
370
|
_groupEpochRotateTimer = null;
|
|
356
371
|
_cacheCleanupTimer = null;
|
|
372
|
+
/**
|
|
373
|
+
* 本地 agent.md 内容对应的 etag(quoted sha256 hex,与服务端 _agent_md_etag 一致)。
|
|
374
|
+
*
|
|
375
|
+
* 浏览器无法直接读本地文件,因此 API 不接收 path,而是接收 markdown 字符串:
|
|
376
|
+
* 应用层(如 `<input type=file>`)读出文本后调用 setLocalAgentMdContent() 设置。
|
|
377
|
+
* 用于跟服务端 RPC 注入的 _meta.agent_md_etag 比对,触发"本地未发布到服务端"或
|
|
378
|
+
* "服务端版本更新"的 UI 提示。
|
|
379
|
+
*/
|
|
380
|
+
_localAgentMdEtag = '';
|
|
381
|
+
/** gateway 在 RPC envelope._meta.agent_md_etag 注入的服务端 etag;纯观察,无下游依赖。 */
|
|
382
|
+
_remoteAgentMdEtag = '';
|
|
357
383
|
/** 消息序列号跟踪器(群消息 + P2P 空洞检测) */
|
|
358
384
|
_seqTracker = new SeqTracker();
|
|
359
385
|
_seqTrackerContext = null;
|
|
@@ -372,8 +398,6 @@ export class AUNClient {
|
|
|
372
398
|
_groupEpochRotationRetryTimers = new Map();
|
|
373
399
|
/** Lazy group sync:首次发送群消息前自动拉取历史 */
|
|
374
400
|
_groupSynced = new Set();
|
|
375
|
-
/** Lazy P2P sync:首次发送 P2P 消息前自动拉取历史 */
|
|
376
|
-
_p2pSynced = false;
|
|
377
401
|
/** gap fill 来源标记:true 表示当前正在补洞(pull 触发),false 表示非补洞 */
|
|
378
402
|
_gapFillActive = false;
|
|
379
403
|
// 重连相关
|
|
@@ -437,6 +461,7 @@ export class AUNClient {
|
|
|
437
461
|
timeout: DEFAULT_SESSION_OPTIONS.timeouts.call,
|
|
438
462
|
onDisconnect: (error, closeCode) => this._handleTransportDisconnect(error, closeCode),
|
|
439
463
|
});
|
|
464
|
+
this._transport.setMetaObserver((meta) => this._observeRpcMeta(meta));
|
|
440
465
|
this._e2ee = new E2EEManager({
|
|
441
466
|
identityFn: () => this._identity ?? {},
|
|
442
467
|
deviceIdFn: () => this._deviceId,
|
|
@@ -507,6 +532,58 @@ export class AUNClient {
|
|
|
507
532
|
get aid() {
|
|
508
533
|
return this._aid;
|
|
509
534
|
}
|
|
535
|
+
/**
|
|
536
|
+
* 设置本地 agent.md 内容并一次性计算 etag(quoted sha256,与服务端一致)。
|
|
537
|
+
*
|
|
538
|
+
* 浏览器环境无法直接读本地文件,应用应通过 `<input type=file>` 等方式读出 markdown
|
|
539
|
+
* 文本后调用本方法。content 为空字符串时清除本地 etag。失败不抛异常(debug 日志记录),
|
|
540
|
+
* 应用可读 getLocalAgentMdEtag() 返回值或本方法返回值判断。
|
|
541
|
+
*
|
|
542
|
+
* 返回当前 etag(quoted hex 或空串)。
|
|
543
|
+
*/
|
|
544
|
+
async setLocalAgentMdContent(content) {
|
|
545
|
+
const text = String(content ?? '');
|
|
546
|
+
if (text.length === 0) {
|
|
547
|
+
this._localAgentMdEtag = '';
|
|
548
|
+
return '';
|
|
549
|
+
}
|
|
550
|
+
try {
|
|
551
|
+
const data = new TextEncoder().encode(text);
|
|
552
|
+
const buf = await crypto.subtle.digest('SHA-256', data);
|
|
553
|
+
const bytes = new Uint8Array(buf);
|
|
554
|
+
let hex = '';
|
|
555
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
556
|
+
hex += bytes[i].toString(16).padStart(2, '0');
|
|
557
|
+
}
|
|
558
|
+
this._localAgentMdEtag = `"${hex}"`;
|
|
559
|
+
}
|
|
560
|
+
catch (exc) {
|
|
561
|
+
this._clientLog.warn(`setLocalAgentMdContent sha256 failed: ${String(exc)}`);
|
|
562
|
+
this._localAgentMdEtag = '';
|
|
563
|
+
}
|
|
564
|
+
return this._localAgentMdEtag;
|
|
565
|
+
}
|
|
566
|
+
/** 返回 setLocalAgentMdContent 计算的 etag;未设置或计算失败时返回空串。 */
|
|
567
|
+
getLocalAgentMdEtag() {
|
|
568
|
+
return this._localAgentMdEtag;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* 返回 gateway 在最近一次 RPC envelope._meta 注入的服务端 agent.md etag。
|
|
572
|
+
*
|
|
573
|
+
* 未收到过则为空串;不阻塞调用,纯内存读。
|
|
574
|
+
*/
|
|
575
|
+
getRemoteAgentMdEtag() {
|
|
576
|
+
return this._remoteAgentMdEtag;
|
|
577
|
+
}
|
|
578
|
+
/** transport 的 meta observer:吸收 gateway 注入的 _meta 字段。失败不影响业务。 */
|
|
579
|
+
_observeRpcMeta(meta) {
|
|
580
|
+
if (!isJsonObject(meta))
|
|
581
|
+
return;
|
|
582
|
+
const etag = String(meta.agent_md_etag ?? '').trim();
|
|
583
|
+
if (etag) {
|
|
584
|
+
this._remoteAgentMdEtag = etag;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
510
587
|
get state() {
|
|
511
588
|
return this._state;
|
|
512
589
|
}
|
|
@@ -721,6 +798,7 @@ export class AUNClient {
|
|
|
721
798
|
return this._sendEncrypted(p);
|
|
722
799
|
}
|
|
723
800
|
// encrypt=false:明文走通用 RPC 路径;protected_headers/headers 是信封元数据,加密与否都保留
|
|
801
|
+
this._maybeAppendEchoTraceSend(p);
|
|
724
802
|
}
|
|
725
803
|
// 自动加密:group.send 默认加密(encrypt 默认 true)
|
|
726
804
|
if (method === 'group.send') {
|
|
@@ -729,6 +807,7 @@ export class AUNClient {
|
|
|
729
807
|
if (encrypt) {
|
|
730
808
|
return this._sendGroupEncrypted(p);
|
|
731
809
|
}
|
|
810
|
+
this._maybeAppendEchoTraceSend(p);
|
|
732
811
|
}
|
|
733
812
|
if (method === 'group.thought.put') {
|
|
734
813
|
const encrypt = p.encrypt !== undefined ? p.encrypt : true;
|
|
@@ -965,8 +1044,6 @@ export class AUNClient {
|
|
|
965
1044
|
}
|
|
966
1045
|
// P2P 空洞检测
|
|
967
1046
|
const seq = msg.seq;
|
|
968
|
-
// 推送路径收到 P2P 消息 → 标记已同步,后续发送无需再 lazySyncP2p
|
|
969
|
-
this._p2pSynced = true;
|
|
970
1047
|
if (seq !== undefined && seq !== null && this._aid) {
|
|
971
1048
|
const ns = `p2p:${this._aid}`;
|
|
972
1049
|
const needPull = this._seqTracker.onMessageSeq(ns, seq);
|
|
@@ -1396,8 +1473,61 @@ export class AUNClient {
|
|
|
1396
1473
|
return this._attachCurrentInstanceContext(payload);
|
|
1397
1474
|
}
|
|
1398
1475
|
async _publishAppEvent(event, payload) {
|
|
1476
|
+
if ((event === 'message.received' || event === 'group.message_created') && isJsonObject(payload)) {
|
|
1477
|
+
this._maybeAppendEchoTraceReceive(payload);
|
|
1478
|
+
}
|
|
1479
|
+
// 注入本地/远端 agent.md etag,让应用层判断版本一致性;失败不影响业务。
|
|
1480
|
+
if (isJsonObject(payload)) {
|
|
1481
|
+
try {
|
|
1482
|
+
const localEtag = this._localAgentMdEtag || '';
|
|
1483
|
+
const remoteEtag = this._remoteAgentMdEtag || '';
|
|
1484
|
+
if ((localEtag || remoteEtag) && payload._agent_md === undefined) {
|
|
1485
|
+
payload._agent_md = {
|
|
1486
|
+
local_etag: localEtag,
|
|
1487
|
+
remote_etag: remoteEtag,
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
catch (exc) {
|
|
1492
|
+
this._clientLog.debug(`agent_md etag inject skipped: ${String(exc)}`);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1399
1495
|
await this._dispatcher.publish(event, this._normalizePublishedMessagePayload(event, payload));
|
|
1400
1496
|
}
|
|
1497
|
+
_echoTimestamp() {
|
|
1498
|
+
const now = new Date();
|
|
1499
|
+
const hh = String(now.getHours()).padStart(2, '0');
|
|
1500
|
+
const mm = String(now.getMinutes()).padStart(2, '0');
|
|
1501
|
+
const ss = String(now.getSeconds()).padStart(2, '0');
|
|
1502
|
+
const ms = String(now.getMilliseconds()).padStart(3, '0');
|
|
1503
|
+
return `${hh}:${mm}:${ss}.${ms}`;
|
|
1504
|
+
}
|
|
1505
|
+
_isEchoPayload(payload) {
|
|
1506
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
1507
|
+
return false;
|
|
1508
|
+
const text = payload.text;
|
|
1509
|
+
if (typeof text !== 'string' || text.length > 4096)
|
|
1510
|
+
return false;
|
|
1511
|
+
return text.split('\n', 1)[0].toLowerCase().includes('echo');
|
|
1512
|
+
}
|
|
1513
|
+
_maybeAppendEchoTraceSend(params) {
|
|
1514
|
+
const payload = params.payload;
|
|
1515
|
+
if (!this._isEchoPayload(payload))
|
|
1516
|
+
return;
|
|
1517
|
+
const uptime = this._connectedAt ? Math.floor((Date.now() - this._connectedAt) / 1000) : 0;
|
|
1518
|
+
const trace = `${this._echoTimestamp()} [AUN-SDK.send] aid=${this._aid ?? '-'} conn_uptime=${uptime}s`;
|
|
1519
|
+
params.payload = { ...payload, text: payload.text + '\n' + trace };
|
|
1520
|
+
}
|
|
1521
|
+
_maybeAppendEchoTraceReceive(msg) {
|
|
1522
|
+
if (msg.encrypted)
|
|
1523
|
+
return;
|
|
1524
|
+
const payload = msg.payload;
|
|
1525
|
+
if (!this._isEchoPayload(payload))
|
|
1526
|
+
return;
|
|
1527
|
+
const uptime = this._connectedAt ? Math.floor((Date.now() - this._connectedAt) / 1000) : 0;
|
|
1528
|
+
const trace = `${this._echoTimestamp()} [AUN-SDK.receive] aid=${this._aid ?? '-'} conn_uptime=${uptime}s`;
|
|
1529
|
+
msg.payload = { ...payload, text: payload.text + '\n' + trace };
|
|
1530
|
+
}
|
|
1401
1531
|
_messageTargetsCurrentInstance(message) {
|
|
1402
1532
|
if (!isJsonObject(message))
|
|
1403
1533
|
return true;
|
|
@@ -1911,10 +2041,8 @@ export class AUNClient {
|
|
|
1911
2041
|
}
|
|
1912
2042
|
const persistRequired = Boolean(params.persist_required || params.durable);
|
|
1913
2043
|
const protectedHeaders = this._protectedHeadersFromParams(params);
|
|
1914
|
-
//
|
|
1915
|
-
|
|
1916
|
-
await this._lazySyncP2p();
|
|
1917
|
-
}
|
|
2044
|
+
// 惰性 P2P 同步由 connect/reconnect 完成后的 _fillP2pGap 异步触发,
|
|
2045
|
+
// 不再在 send 路径上 await(与 C++ FillP2PGap 行为对齐,避免阻塞用户发送)。
|
|
1918
2046
|
// 内部发送逻辑,refreshPeerMaterial=true 时强制清缓存重新拉取对端材料
|
|
1919
2047
|
const sendAttempt = async (refreshPeerMaterial = false) => {
|
|
1920
2048
|
const recipientPrekeys = refreshPeerMaterial
|
|
@@ -1923,22 +2051,13 @@ export class AUNClient {
|
|
|
1923
2051
|
const selfSyncCopies = await this._buildSelfSyncCopies({
|
|
1924
2052
|
logicalToAid: toAid, payload, messageId, timestamp, protectedHeaders,
|
|
1925
2053
|
});
|
|
1926
|
-
//
|
|
1927
|
-
// 占位符 PREKEY_FALLBACK_DEVICE_ID 表示服务端未分配真实设备 ID,不可用于多设备路由
|
|
2054
|
+
// 统一 multi-device 路径:必须有 routable prekey
|
|
1928
2055
|
const routablePrekeys = recipientPrekeys.filter(pk => {
|
|
1929
2056
|
const did = String(pk.device_id ?? '').trim();
|
|
1930
2057
|
return did && did !== PREKEY_FALLBACK_DEVICE_ID;
|
|
1931
2058
|
});
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
// single 路径仅在完全没有 routable prekey 时使用(legacy 兼容)。
|
|
1935
|
-
const canUseMultiDevice = routablePrekeys.length > 0;
|
|
1936
|
-
if (!canUseMultiDevice) {
|
|
1937
|
-
return await this._sendEncryptedSingle({
|
|
1938
|
-
toAid, payload, messageId, timestamp,
|
|
1939
|
-
prekey: routablePrekeys[0] ?? recipientPrekeys[0],
|
|
1940
|
-
persistRequired, protectedHeaders,
|
|
1941
|
-
});
|
|
2059
|
+
if (routablePrekeys.length === 0) {
|
|
2060
|
+
throw new Error(`no registered device prekeys for ${toAid}, cannot send encrypted message`);
|
|
1942
2061
|
}
|
|
1943
2062
|
const recipientCopies = await this._buildRecipientDeviceCopies({
|
|
1944
2063
|
toAid, payload, messageId, timestamp,
|
|
@@ -1981,63 +2100,6 @@ export class AUNClient {
|
|
|
1981
2100
|
throw err;
|
|
1982
2101
|
}
|
|
1983
2102
|
}
|
|
1984
|
-
/**
|
|
1985
|
-
* 首次发送 P2P 消息前懒拉取历史消息,同步 seqTracker 避免空洞。
|
|
1986
|
-
* 只在本连接周期内执行一次。
|
|
1987
|
-
*/
|
|
1988
|
-
async _lazySyncP2p() {
|
|
1989
|
-
this._p2pSynced = true;
|
|
1990
|
-
if (!this._aid)
|
|
1991
|
-
return;
|
|
1992
|
-
const ns = `p2p:${this._aid}`;
|
|
1993
|
-
const afterSeq = this._seqTracker.getContiguousSeq(ns);
|
|
1994
|
-
try {
|
|
1995
|
-
const result = await this._transport.call('message.pull', {
|
|
1996
|
-
after_seq: afterSeq,
|
|
1997
|
-
limit: 200,
|
|
1998
|
-
});
|
|
1999
|
-
if (isJsonObject(result)) {
|
|
2000
|
-
const messages = result.messages;
|
|
2001
|
-
if (Array.isArray(messages) && messages.length > 0) {
|
|
2002
|
-
this._seqTracker.onPullResult(ns, messages.filter(isJsonObject));
|
|
2003
|
-
this._saveSeqTrackerState();
|
|
2004
|
-
}
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2007
|
-
catch (exc) {
|
|
2008
|
-
this._clientLog.warn(`lazySyncP2p failed:${String(exc)}`);
|
|
2009
|
-
}
|
|
2010
|
-
}
|
|
2011
|
-
async _sendEncryptedSingle(opts) {
|
|
2012
|
-
let prekey = opts.prekey;
|
|
2013
|
-
if (prekey === undefined) {
|
|
2014
|
-
prekey = await this._fetchPeerPrekey(opts.toAid);
|
|
2015
|
-
}
|
|
2016
|
-
const peerCertFingerprint = typeof prekey?.cert_fingerprint === 'string' ? prekey.cert_fingerprint : undefined;
|
|
2017
|
-
const peerCertPem = await this._fetchPeerCert(opts.toAid, peerCertFingerprint);
|
|
2018
|
-
const [envelope, encryptResult] = await this._encryptCopyPayload({
|
|
2019
|
-
logicalToAid: opts.toAid,
|
|
2020
|
-
payload: opts.payload,
|
|
2021
|
-
peerCertPem,
|
|
2022
|
-
prekey,
|
|
2023
|
-
messageId: opts.messageId,
|
|
2024
|
-
timestamp: opts.timestamp,
|
|
2025
|
-
protectedHeaders: opts.protectedHeaders,
|
|
2026
|
-
});
|
|
2027
|
-
await this._ensureEncryptResult(opts.toAid, encryptResult);
|
|
2028
|
-
const sendParams = {
|
|
2029
|
-
to: opts.toAid,
|
|
2030
|
-
payload: envelope,
|
|
2031
|
-
type: 'e2ee.encrypted',
|
|
2032
|
-
encrypted: true,
|
|
2033
|
-
message_id: opts.messageId,
|
|
2034
|
-
timestamp: opts.timestamp,
|
|
2035
|
-
};
|
|
2036
|
-
if (opts.persistRequired) {
|
|
2037
|
-
sendParams.persist_required = true;
|
|
2038
|
-
}
|
|
2039
|
-
return this._transport.call('message.send', sendParams);
|
|
2040
|
-
}
|
|
2041
2103
|
async _buildRecipientDeviceCopies(opts) {
|
|
2042
2104
|
const recipientCopies = [];
|
|
2043
2105
|
const certCache = new Map();
|
|
@@ -4823,10 +4885,13 @@ export class AUNClient {
|
|
|
4823
4885
|
this._sessionParams.access_token = String(auth.token ?? params.access_token ?? '');
|
|
4824
4886
|
}
|
|
4825
4887
|
}
|
|
4888
|
+
if (isJsonObject(auth.hello) && 'heartbeat_interval' in auth.hello) {
|
|
4889
|
+
this._applyServerHeartbeatInterval(auth.hello.heartbeat_interval, 'auth');
|
|
4890
|
+
}
|
|
4826
4891
|
}
|
|
4827
4892
|
}
|
|
4828
4893
|
else {
|
|
4829
|
-
await this._auth.initializeWithToken(this._transport, challenge, String(params.access_token), {
|
|
4894
|
+
const hello = await this._auth.initializeWithToken(this._transport, challenge, String(params.access_token), {
|
|
4830
4895
|
deviceId: this._deviceId,
|
|
4831
4896
|
slotId: this._slotId,
|
|
4832
4897
|
deliveryMode: this._connectDeliveryMode,
|
|
@@ -4835,6 +4900,9 @@ export class AUNClient {
|
|
|
4835
4900
|
extraInfo: params.extra_info,
|
|
4836
4901
|
});
|
|
4837
4902
|
await this._syncIdentityAfterConnect(String(params.access_token));
|
|
4903
|
+
if (isJsonObject(hello) && 'heartbeat_interval' in hello) {
|
|
4904
|
+
this._applyServerHeartbeatInterval(hello.heartbeat_interval, 'auth');
|
|
4905
|
+
}
|
|
4838
4906
|
}
|
|
4839
4907
|
}
|
|
4840
4908
|
catch (err) {
|
|
@@ -4847,11 +4915,11 @@ export class AUNClient {
|
|
|
4847
4915
|
throw err;
|
|
4848
4916
|
}
|
|
4849
4917
|
this._state = 'connected';
|
|
4918
|
+
this._connectedAt = Date.now();
|
|
4850
4919
|
await this._dispatcher.publish('connection.state', {
|
|
4851
4920
|
state: this._state,
|
|
4852
4921
|
gateway: gatewayUrl,
|
|
4853
4922
|
});
|
|
4854
|
-
// auth 阶段 aid 可能被 identity 覆盖;若 context 发生变化,重新 refresh + restore。
|
|
4855
4923
|
if (this._seqTrackerContext !== this._currentSeqTrackerContext()) {
|
|
4856
4924
|
this._refreshSeqTrackerContext();
|
|
4857
4925
|
await this._restoreSeqTrackerState();
|
|
@@ -4976,12 +5044,8 @@ export class AUNClient {
|
|
|
4976
5044
|
}
|
|
4977
5045
|
_buildSessionOptions(params) {
|
|
4978
5046
|
const connectionKind = String(params.connection_kind ?? 'long');
|
|
4979
|
-
// 短连接默认禁用 auto_reconnect:短连接生命周期短,自动重连无意义
|
|
4980
|
-
const defaultAutoReconnect = connectionKind === 'short'
|
|
4981
|
-
? false
|
|
4982
|
-
: DEFAULT_SESSION_OPTIONS.auto_reconnect;
|
|
4983
5047
|
const options = {
|
|
4984
|
-
auto_reconnect:
|
|
5048
|
+
auto_reconnect: DEFAULT_SESSION_OPTIONS.auto_reconnect,
|
|
4985
5049
|
heartbeat_interval: DEFAULT_SESSION_OPTIONS.heartbeat_interval,
|
|
4986
5050
|
token_refresh_before: DEFAULT_SESSION_OPTIONS.token_refresh_before,
|
|
4987
5051
|
retry: { ...DEFAULT_SESSION_OPTIONS.retry },
|
|
@@ -5008,12 +5072,12 @@ export class AUNClient {
|
|
|
5008
5072
|
}
|
|
5009
5073
|
// ── 内部:后台任务 ────────────────────────────────
|
|
5010
5074
|
_startBackgroundTasks() {
|
|
5011
|
-
//
|
|
5012
|
-
|
|
5013
|
-
|
|
5075
|
+
// 短连接不启动 heartbeat 与 token 刷新(生命周期短,不需要长期会话维护);
|
|
5076
|
+
// auto_reconnect 仍允许,由 _sessionOptions.auto_reconnect 决定
|
|
5077
|
+
if (this._sessionOptions?.connection_kind !== 'short') {
|
|
5078
|
+
this._startHeartbeat();
|
|
5079
|
+
this._startTokenRefresh();
|
|
5014
5080
|
}
|
|
5015
|
-
this._startHeartbeat();
|
|
5016
|
-
this._startTokenRefresh();
|
|
5017
5081
|
this._startPrekeyRefresh();
|
|
5018
5082
|
this._startGroupEpochTasks();
|
|
5019
5083
|
}
|
|
@@ -5051,10 +5115,9 @@ export class AUNClient {
|
|
|
5051
5115
|
_startHeartbeat() {
|
|
5052
5116
|
if (this._heartbeatTimer !== null)
|
|
5053
5117
|
return;
|
|
5054
|
-
const
|
|
5055
|
-
if (
|
|
5118
|
+
const interval = clampHeartbeatInterval(this._sessionOptions.heartbeat_interval ?? DEFAULT_SESSION_OPTIONS.heartbeat_interval);
|
|
5119
|
+
if (interval <= 0)
|
|
5056
5120
|
return;
|
|
5057
|
-
const interval = Math.max(rawIntervalSeconds, 30) * 1000;
|
|
5058
5121
|
// M25: 把连续失败阈值从 3 次收窄到 2 次。既能容忍一次网络抖动/GC 暂停,
|
|
5059
5122
|
// 又把半开连接的检测延迟从 3 个心跳周期降到 2 个,避免 RPC 长时间挂起。
|
|
5060
5123
|
// 真正的 socket 死亡由 ws.on('close') 立即触发 _handleTransportDisconnect,
|
|
@@ -5065,8 +5128,12 @@ export class AUNClient {
|
|
|
5065
5128
|
if (this._state !== 'connected' || this._closing)
|
|
5066
5129
|
return;
|
|
5067
5130
|
try {
|
|
5068
|
-
await this._transport.call('meta.ping', {});
|
|
5131
|
+
const pong = await this._transport.call('meta.ping', {});
|
|
5069
5132
|
consecutiveFailures = 0;
|
|
5133
|
+
// 服务端可在 pong 中下发新的 heartbeat_interval(秒,0=关闭)
|
|
5134
|
+
if (isJsonObject(pong) && 'heartbeat_interval' in pong) {
|
|
5135
|
+
this._applyServerHeartbeatInterval(pong.heartbeat_interval, 'pong');
|
|
5136
|
+
}
|
|
5070
5137
|
}
|
|
5071
5138
|
catch (exc) {
|
|
5072
5139
|
consecutiveFailures++;
|
|
@@ -5077,7 +5144,24 @@ export class AUNClient {
|
|
|
5077
5144
|
this._handleTransportDisconnect(exc instanceof Error ? exc : new Error(String(exc)));
|
|
5078
5145
|
}
|
|
5079
5146
|
}
|
|
5080
|
-
}, interval);
|
|
5147
|
+
}, interval * 1000);
|
|
5148
|
+
}
|
|
5149
|
+
/** 服务端通过 hello/pong 下发 heartbeat_interval;clamp 后写入 session_options 并按需重启心跳。 */
|
|
5150
|
+
_applyServerHeartbeatInterval(raw, source) {
|
|
5151
|
+
const newInterval = clampHeartbeatInterval(raw);
|
|
5152
|
+
const oldInterval = clampHeartbeatInterval(this._sessionOptions.heartbeat_interval);
|
|
5153
|
+
if (newInterval === oldInterval)
|
|
5154
|
+
return;
|
|
5155
|
+
this._sessionOptions.heartbeat_interval = newInterval;
|
|
5156
|
+
this._clientLog.debug(`heartbeat_interval updated by ${source}: ${oldInterval} -> ${newInterval}`);
|
|
5157
|
+
// 重启定时器以应用新间隔(关闭/启动通过定时器有/无区分)
|
|
5158
|
+
if (this._heartbeatTimer !== null) {
|
|
5159
|
+
clearInterval(this._heartbeatTimer);
|
|
5160
|
+
this._heartbeatTimer = null;
|
|
5161
|
+
}
|
|
5162
|
+
if (newInterval > 0 && this._state === 'connected' && !this._closing) {
|
|
5163
|
+
this._startHeartbeat();
|
|
5164
|
+
}
|
|
5081
5165
|
}
|
|
5082
5166
|
/** Token 刷新定时器 */
|
|
5083
5167
|
_startTokenRefresh() {
|
|
@@ -5716,7 +5800,6 @@ export class AUNClient {
|
|
|
5716
5800
|
this._pendingOrderedMsgs.clear();
|
|
5717
5801
|
this._pendingDecryptMsgs.clear();
|
|
5718
5802
|
this._groupSynced.clear();
|
|
5719
|
-
this._p2pSynced = false;
|
|
5720
5803
|
}
|
|
5721
5804
|
_refreshSeqTrackerContext() {
|
|
5722
5805
|
const nextContext = this._currentSeqTrackerContext();
|
|
@@ -5728,7 +5811,6 @@ export class AUNClient {
|
|
|
5728
5811
|
this._pendingOrderedMsgs.clear();
|
|
5729
5812
|
this._pendingDecryptMsgs.clear();
|
|
5730
5813
|
this._groupSynced.clear();
|
|
5731
|
-
this._p2pSynced = false;
|
|
5732
5814
|
this._seqTrackerContext = nextContext;
|
|
5733
5815
|
}
|
|
5734
5816
|
/** 将 SeqTracker 状态保存到 keystore */
|