@seamnet/client 0.12.7 → 0.13.0
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/lib/plugins/im/index.cjs +289 -176
- package/package.json +1 -1
package/lib/plugins/im/index.cjs
CHANGED
|
@@ -134,6 +134,19 @@ function createImPlugin() {
|
|
|
134
134
|
// userId → nick 缓存;null 表示已查过但无 nick,避免反复拉
|
|
135
135
|
const displayNameCache = new Map();
|
|
136
136
|
|
|
137
|
+
// ===== Supervisor 状态(SDK 僵尸时自动重建 chat 实例)=====
|
|
138
|
+
let credentialsRef = null;
|
|
139
|
+
let rebuildInProgress = false;
|
|
140
|
+
let lastRebuildAt = 0;
|
|
141
|
+
let keepaliveTimer = null;
|
|
142
|
+
let processRejectionHandler = null;
|
|
143
|
+
const REBUILD_COOLDOWN_MS = 10_000;
|
|
144
|
+
const REBUILD_DEBOUNCE_MS = 3_000;
|
|
145
|
+
const KEEPALIVE_INTERVAL_MS = 3 * 60 * 1000;
|
|
146
|
+
const KEEPALIVE_TIMEOUT_MS = 10_000;
|
|
147
|
+
// SDK 错误 code 需要触发 rebuild(2801=PING 超时,WebSocket 应用层僵尸)
|
|
148
|
+
const SDK_REBUILD_ERROR_CODES = new Set([2801]);
|
|
149
|
+
|
|
137
150
|
async function prefetchDisplayName(userId) {
|
|
138
151
|
if (!chat || !userId || displayNameCache.has(userId)) return;
|
|
139
152
|
displayNameCache.set(userId, null);
|
|
@@ -326,6 +339,255 @@ function createImPlugin() {
|
|
|
326
339
|
return { ok: true };
|
|
327
340
|
}
|
|
328
341
|
|
|
342
|
+
// ===== 事件 handler(独立函数,便于 rebuild 时重新注册)=====
|
|
343
|
+
|
|
344
|
+
function onSdkReady() {
|
|
345
|
+
sdkReady = true;
|
|
346
|
+
log.info('sdk_ready', { userId: myUserId });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function onSdkNotReady() {
|
|
350
|
+
sdkReady = false;
|
|
351
|
+
log.warn('sdk_not_ready');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function onSdkError(event) {
|
|
355
|
+
const err = event?.data || event;
|
|
356
|
+
const code = err?.code;
|
|
357
|
+
log.warn('sdk_error', {
|
|
358
|
+
code,
|
|
359
|
+
message: err?.message || String(err).slice(0, 200),
|
|
360
|
+
});
|
|
361
|
+
if (SDK_REBUILD_ERROR_CODES.has(code)) {
|
|
362
|
+
scheduleRebuild(`sdk_error:${code}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function onKickedOut(event) {
|
|
367
|
+
log.warn('sdk_kicked_out', {
|
|
368
|
+
type: event?.data?.type,
|
|
369
|
+
message: event?.data?.message,
|
|
370
|
+
});
|
|
371
|
+
sdkReady = false;
|
|
372
|
+
scheduleRebuild('kicked_out');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function onNetStateChange(event) {
|
|
376
|
+
log.info('sdk_net_state', { state: event?.data?.state });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function onFriendListUpdated() {
|
|
380
|
+
refreshContactsFromFriendList().catch((err) => {
|
|
381
|
+
log.warn('refresh_contacts_on_update_failed', { message: err.message });
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function onMessageReceived(event) {
|
|
386
|
+
for (const msg of event.data) {
|
|
387
|
+
if (msg.from === myUserId) continue;
|
|
388
|
+
|
|
389
|
+
const isGroup = msg.conversationType === TencentCloudChat.TYPES.CONV_GROUP;
|
|
390
|
+
const sender = msg.from;
|
|
391
|
+
if (!msg.nick && !displayNameCache.has(sender)) {
|
|
392
|
+
prefetchDisplayName(sender).catch(() => {});
|
|
393
|
+
}
|
|
394
|
+
const senderDisplay = formatSenderDisplay(msg);
|
|
395
|
+
const groupId = isGroup ? msg.to : null;
|
|
396
|
+
const ts = new Date().toTimeString().slice(0, 5);
|
|
397
|
+
const conversationType = msg.conversationType;
|
|
398
|
+
const replyTool = isGroup ? 'seam msg group' : 'seam msg send';
|
|
399
|
+
|
|
400
|
+
// 图片消息
|
|
401
|
+
if (msg.type === TencentCloudChat.TYPES.MSG_IMAGE) {
|
|
402
|
+
const inbox = hub.service('inbox');
|
|
403
|
+
const imgInfo =
|
|
404
|
+
msg.payload?.imageInfoArray?.find((i) => i.sizeType === 3) ||
|
|
405
|
+
msg.payload?.imageInfoArray?.[0];
|
|
406
|
+
if (imgInfo?.url && inbox) {
|
|
407
|
+
(async () => {
|
|
408
|
+
try {
|
|
409
|
+
const resp = await fetch(imgInfo.url);
|
|
410
|
+
if (!resp.ok) throw new Error(`http ${resp.status}`);
|
|
411
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
412
|
+
const p = inbox.save(buf, { ext: 'jpg', src: 'im' });
|
|
413
|
+
log.info('im_image_saved', { sender, bytes: buf.length, path: p });
|
|
414
|
+
const prefix = isGroup
|
|
415
|
+
? `💬 [Seam群图片 ${ts} ${groupId} → ${replyTool}]`
|
|
416
|
+
: `💬 [Seam图片 ${ts} → ${replyTool}]`;
|
|
417
|
+
hub.inject(`${prefix} ${senderDisplay} → ${p}`);
|
|
418
|
+
} catch (e) {
|
|
419
|
+
log.error('im_image_download_failed', e, { sender });
|
|
420
|
+
hub.inject(`💬 [Seam ${ts} → ${replyTool}] ${senderDisplay}: [图片下载失败: ${e.message}]`);
|
|
421
|
+
}
|
|
422
|
+
})();
|
|
423
|
+
}
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// 文件消息
|
|
428
|
+
if (msg.type === TencentCloudChat.TYPES.MSG_FILE) {
|
|
429
|
+
const inbox = hub.service('inbox');
|
|
430
|
+
const fileUrl = msg.payload?.fileUrl;
|
|
431
|
+
const fileName = msg.payload?.fileName || `file_${Date.now()}.bin`;
|
|
432
|
+
const ext = path.extname(fileName).replace(/^\./, '') || 'bin';
|
|
433
|
+
if (fileUrl && inbox) {
|
|
434
|
+
(async () => {
|
|
435
|
+
try {
|
|
436
|
+
const resp = await fetch(fileUrl);
|
|
437
|
+
if (!resp.ok) throw new Error(`http ${resp.status}`);
|
|
438
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
439
|
+
const p = inbox.save(buf, { ext, src: 'im' });
|
|
440
|
+
log.info('im_file_saved', { sender, bytes: buf.length, path: p });
|
|
441
|
+
const prefix = isGroup
|
|
442
|
+
? `💬 [Seam群文件 ${ts} ${groupId} → ${replyTool}]`
|
|
443
|
+
: `💬 [Seam文件 ${ts} → ${replyTool}]`;
|
|
444
|
+
hub.inject(
|
|
445
|
+
`${prefix} ${senderDisplay} → ${p} (原名: ${fileName}, ${buf.length} bytes)`
|
|
446
|
+
);
|
|
447
|
+
} catch (e) {
|
|
448
|
+
log.error('im_file_download_failed', e, { sender });
|
|
449
|
+
hub.inject(`💬 [Seam ${ts} → ${replyTool}] ${sender}: [文件下载失败: ${e.message}]`);
|
|
450
|
+
}
|
|
451
|
+
})();
|
|
452
|
+
}
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// 文本消息
|
|
457
|
+
let text = '';
|
|
458
|
+
if (msg.type === TencentCloudChat.TYPES.MSG_TEXT) {
|
|
459
|
+
text = msg.payload.text;
|
|
460
|
+
} else {
|
|
461
|
+
text = `[${msg.type}]`;
|
|
462
|
+
}
|
|
463
|
+
if (!text) continue;
|
|
464
|
+
|
|
465
|
+
log.info('message_received', { from: msg.from, isGroup, textLength: text.length });
|
|
466
|
+
|
|
467
|
+
const bufferKey = isGroup ? `im:group:${msg.to}:${msg.from}` : `im:${msg.from}`;
|
|
468
|
+
const makePrefix = (ts2) =>
|
|
469
|
+
isGroup
|
|
470
|
+
? `💬 [Seam群 ${ts2} ${groupId} → ${replyTool}]`
|
|
471
|
+
: `💬 [Seam ${ts2} → ${replyTool}]`;
|
|
472
|
+
|
|
473
|
+
if (messageBuffer) {
|
|
474
|
+
messageBuffer.queueText(
|
|
475
|
+
bufferKey,
|
|
476
|
+
text,
|
|
477
|
+
(items) => {
|
|
478
|
+
const tsNow = new Date().toTimeString().slice(0, 5);
|
|
479
|
+
const prefix = makePrefix(tsNow);
|
|
480
|
+
const joined = items.map((i) => i.content).join('\n');
|
|
481
|
+
if (items.length === 1) {
|
|
482
|
+
hub.inject(`${prefix} ${senderDisplay}: ${joined}`);
|
|
483
|
+
} else {
|
|
484
|
+
hub.inject(`${prefix} ${senderDisplay}:\n${joined}`);
|
|
485
|
+
}
|
|
486
|
+
if (eventBus) {
|
|
487
|
+
eventBus.emit('im.message', { from: sender, text: joined, isGroup, groupId, conversationType });
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
{ delay: 5000, maxDelay: 15000 }
|
|
491
|
+
);
|
|
492
|
+
} else {
|
|
493
|
+
const tsNow = new Date().toTimeString().slice(0, 5);
|
|
494
|
+
hub.inject(`${makePrefix(tsNow)} ${senderDisplay}: ${text}`);
|
|
495
|
+
if (eventBus) {
|
|
496
|
+
eventBus.emit('im.message', { from: sender, text, isGroup, groupId, conversationType });
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function setupAllListeners() {
|
|
503
|
+
chat.on(TencentCloudChat.EVENT.SDK_READY, onSdkReady);
|
|
504
|
+
chat.on(TencentCloudChat.EVENT.SDK_NOT_READY, onSdkNotReady);
|
|
505
|
+
chat.on(TencentCloudChat.EVENT.ERROR, onSdkError);
|
|
506
|
+
chat.on(TencentCloudChat.EVENT.KICKED_OUT, onKickedOut);
|
|
507
|
+
chat.on(TencentCloudChat.EVENT.NET_STATE_CHANGE, onNetStateChange);
|
|
508
|
+
chat.on(TencentCloudChat.EVENT.MESSAGE_RECEIVED, onMessageReceived);
|
|
509
|
+
try {
|
|
510
|
+
chat.on(TencentCloudChat.EVENT.FRIEND_LIST_UPDATED, onFriendListUpdated);
|
|
511
|
+
} catch (err) {
|
|
512
|
+
log.warn('friend_list_listener_register_failed', { message: err.message });
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ===== 重建 chat 实例(SDK 僵尸时用)=====
|
|
517
|
+
|
|
518
|
+
async function createAndLoginChat() {
|
|
519
|
+
delete global.window;
|
|
520
|
+
chat = TencentCloudChat.create({ SDKAppID: Number(credentialsRef.sdkAppId) });
|
|
521
|
+
setupSdkGlobals();
|
|
522
|
+
try {
|
|
523
|
+
const TIMUploadPlugin = require('tim-upload-plugin');
|
|
524
|
+
chat.registerPlugin({ 'tim-upload-plugin': TIMUploadPlugin });
|
|
525
|
+
} catch {}
|
|
526
|
+
chat.setLogLevel(4);
|
|
527
|
+
setupAllListeners();
|
|
528
|
+
await chat.login({
|
|
529
|
+
userID: myUserId,
|
|
530
|
+
userSig: credentialsRef.userSig,
|
|
531
|
+
});
|
|
532
|
+
// 等 SDK_READY(最多 10s)
|
|
533
|
+
await new Promise((resolve) => {
|
|
534
|
+
if (sdkReady) return resolve();
|
|
535
|
+
const timer = setTimeout(() => resolve(), 10000);
|
|
536
|
+
const once = () => { clearTimeout(timer); resolve(); };
|
|
537
|
+
chat.on(TencentCloudChat.EVENT.SDK_READY, once);
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async function rebuildChat(reason) {
|
|
542
|
+
log.warn('chat_rebuild_start', { reason });
|
|
543
|
+
sdkReady = false;
|
|
544
|
+
try { await chat.logout(); } catch {}
|
|
545
|
+
try { chat.destroy && chat.destroy(); } catch {}
|
|
546
|
+
try {
|
|
547
|
+
await createAndLoginChat();
|
|
548
|
+
try { await refreshContactsFromFriendList(); } catch {}
|
|
549
|
+
log.info('chat_rebuild_done', { reason });
|
|
550
|
+
} catch (e) {
|
|
551
|
+
log.error('chat_rebuild_failed', e, { reason });
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function scheduleRebuild(reason) {
|
|
556
|
+
if (rebuildInProgress) return;
|
|
557
|
+
const now = Date.now();
|
|
558
|
+
if (now - lastRebuildAt < REBUILD_COOLDOWN_MS) {
|
|
559
|
+
log.info('chat_rebuild_skipped_cooldown', { reason });
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
rebuildInProgress = true;
|
|
563
|
+
setTimeout(async () => {
|
|
564
|
+
try {
|
|
565
|
+
await rebuildChat(reason);
|
|
566
|
+
lastRebuildAt = Date.now();
|
|
567
|
+
} finally {
|
|
568
|
+
rebuildInProgress = false;
|
|
569
|
+
}
|
|
570
|
+
}, REBUILD_DEBOUNCE_MS);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function startKeepalive() {
|
|
574
|
+
if (keepaliveTimer) return;
|
|
575
|
+
keepaliveTimer = setInterval(async () => {
|
|
576
|
+
if (!sdkReady || rebuildInProgress) return;
|
|
577
|
+
try {
|
|
578
|
+
await Promise.race([
|
|
579
|
+
chat.getMyProfile(),
|
|
580
|
+
new Promise((_, reject) =>
|
|
581
|
+
setTimeout(() => reject(new Error('keepalive timeout')), KEEPALIVE_TIMEOUT_MS)
|
|
582
|
+
),
|
|
583
|
+
]);
|
|
584
|
+
} catch (e) {
|
|
585
|
+
log.warn('keepalive_failed', { message: e.message });
|
|
586
|
+
scheduleRebuild(`keepalive:${e.message}`);
|
|
587
|
+
}
|
|
588
|
+
}, KEEPALIVE_INTERVAL_MS);
|
|
589
|
+
}
|
|
590
|
+
|
|
329
591
|
async function init(h) {
|
|
330
592
|
hub = h;
|
|
331
593
|
log = hub.logger('im');
|
|
@@ -345,6 +607,7 @@ function createImPlugin() {
|
|
|
345
607
|
}
|
|
346
608
|
|
|
347
609
|
myUserId = credentials.userId;
|
|
610
|
+
credentialsRef = credentials;
|
|
348
611
|
|
|
349
612
|
// 先 delete window 让 SDK 走 node 路径
|
|
350
613
|
delete global.window;
|
|
@@ -364,171 +627,8 @@ function createImPlugin() {
|
|
|
364
627
|
|
|
365
628
|
chat.setLogLevel(4);
|
|
366
629
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
log.info('sdk_ready', { userId: myUserId });
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
chat.on(TencentCloudChat.EVENT.SDK_NOT_READY, () => {
|
|
373
|
-
sdkReady = false;
|
|
374
|
-
log.warn('sdk_not_ready');
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
// SDK 错误事件:记录但不 throw,避免 guardian 整个 crash
|
|
378
|
-
chat.on(TencentCloudChat.EVENT.ERROR, (event) => {
|
|
379
|
-
const err = event?.data || event;
|
|
380
|
-
log.warn('sdk_error', {
|
|
381
|
-
code: err?.code,
|
|
382
|
-
message: err?.message || String(err).slice(0, 200),
|
|
383
|
-
});
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
// 被踢下线(2801 等多端登录场景):记录,SDK 会自动重连
|
|
387
|
-
chat.on(TencentCloudChat.EVENT.KICKED_OUT, (event) => {
|
|
388
|
-
log.warn('sdk_kicked_out', {
|
|
389
|
-
type: event?.data?.type,
|
|
390
|
-
message: event?.data?.message,
|
|
391
|
-
});
|
|
392
|
-
sdkReady = false;
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
// 网络状态变化:断线/重连都只记日志,不干扰主流程
|
|
396
|
-
chat.on(TencentCloudChat.EVENT.NET_STATE_CHANGE, (event) => {
|
|
397
|
-
log.info('sdk_net_state', { state: event?.data?.state });
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
chat.on(TencentCloudChat.EVENT.MESSAGE_RECEIVED, (event) => {
|
|
401
|
-
for (const msg of event.data) {
|
|
402
|
-
if (msg.from === myUserId) continue;
|
|
403
|
-
|
|
404
|
-
const isGroup = msg.conversationType === TencentCloudChat.TYPES.CONV_GROUP;
|
|
405
|
-
// sender 用于 event-bus(保持 userId 原始值);senderDisplay 用于注入终端(name · userId)
|
|
406
|
-
const sender = msg.from;
|
|
407
|
-
// 若无 msg.nick 且未缓存,异步拉 profile(下次消息就能用)
|
|
408
|
-
if (!msg.nick && !displayNameCache.has(sender)) {
|
|
409
|
-
prefetchDisplayName(sender).catch(() => {});
|
|
410
|
-
}
|
|
411
|
-
const senderDisplay = formatSenderDisplay(msg);
|
|
412
|
-
const groupId = isGroup ? msg.to : null;
|
|
413
|
-
const ts = new Date().toTimeString().slice(0, 5);
|
|
414
|
-
const conversationType = msg.conversationType;
|
|
415
|
-
|
|
416
|
-
const replyTool = isGroup ? 'seam msg group' : 'seam msg send';
|
|
417
|
-
|
|
418
|
-
// 图片消息:下载 URL 存 inbox
|
|
419
|
-
if (msg.type === TencentCloudChat.TYPES.MSG_IMAGE) {
|
|
420
|
-
const inbox = hub.service('inbox');
|
|
421
|
-
const imgInfo =
|
|
422
|
-
msg.payload?.imageInfoArray?.find((i) => i.sizeType === 3) ||
|
|
423
|
-
msg.payload?.imageInfoArray?.[0];
|
|
424
|
-
if (imgInfo?.url && inbox) {
|
|
425
|
-
(async () => {
|
|
426
|
-
try {
|
|
427
|
-
const resp = await fetch(imgInfo.url);
|
|
428
|
-
if (!resp.ok) throw new Error(`http ${resp.status}`);
|
|
429
|
-
const buf = Buffer.from(await resp.arrayBuffer());
|
|
430
|
-
const p = inbox.save(buf, { ext: 'jpg', src: 'im' });
|
|
431
|
-
log.info('im_image_saved', { sender, bytes: buf.length, path: p });
|
|
432
|
-
const prefix = isGroup
|
|
433
|
-
? `💬 [Seam群图片 ${ts} ${groupId} → ${replyTool}]`
|
|
434
|
-
: `💬 [Seam图片 ${ts} → ${replyTool}]`;
|
|
435
|
-
hub.inject(`${prefix} ${senderDisplay} → ${p}`);
|
|
436
|
-
} catch (e) {
|
|
437
|
-
log.error('im_image_download_failed', e, { sender });
|
|
438
|
-
hub.inject(`💬 [Seam ${ts} → ${replyTool}] ${senderDisplay}: [图片下载失败: ${e.message}]`);
|
|
439
|
-
}
|
|
440
|
-
})();
|
|
441
|
-
}
|
|
442
|
-
continue;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// 文件消息
|
|
446
|
-
if (msg.type === TencentCloudChat.TYPES.MSG_FILE) {
|
|
447
|
-
const inbox = hub.service('inbox');
|
|
448
|
-
const fileUrl = msg.payload?.fileUrl;
|
|
449
|
-
const fileName = msg.payload?.fileName || `file_${Date.now()}.bin`;
|
|
450
|
-
const ext = path.extname(fileName).replace(/^\./, '') || 'bin';
|
|
451
|
-
if (fileUrl && inbox) {
|
|
452
|
-
(async () => {
|
|
453
|
-
try {
|
|
454
|
-
const resp = await fetch(fileUrl);
|
|
455
|
-
if (!resp.ok) throw new Error(`http ${resp.status}`);
|
|
456
|
-
const buf = Buffer.from(await resp.arrayBuffer());
|
|
457
|
-
const p = inbox.save(buf, { ext, src: 'im' });
|
|
458
|
-
log.info('im_file_saved', { sender, bytes: buf.length, path: p });
|
|
459
|
-
const prefix = isGroup
|
|
460
|
-
? `💬 [Seam群文件 ${ts} ${groupId} → ${replyTool}]`
|
|
461
|
-
: `💬 [Seam文件 ${ts} → ${replyTool}]`;
|
|
462
|
-
hub.inject(
|
|
463
|
-
`${prefix} ${senderDisplay} → ${p} (原名: ${fileName}, ${buf.length} bytes)`
|
|
464
|
-
);
|
|
465
|
-
} catch (e) {
|
|
466
|
-
log.error('im_file_download_failed', e, { sender });
|
|
467
|
-
hub.inject(`💬 [Seam ${ts} → ${replyTool}] ${sender}: [文件下载失败: ${e.message}]`);
|
|
468
|
-
}
|
|
469
|
-
})();
|
|
470
|
-
}
|
|
471
|
-
continue;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// 文本消息(含其他未处理类型用占位)
|
|
475
|
-
let text = '';
|
|
476
|
-
if (msg.type === TencentCloudChat.TYPES.MSG_TEXT) {
|
|
477
|
-
text = msg.payload.text;
|
|
478
|
-
} else {
|
|
479
|
-
text = `[${msg.type}]`;
|
|
480
|
-
}
|
|
481
|
-
if (!text) continue;
|
|
482
|
-
|
|
483
|
-
log.info('message_received', {
|
|
484
|
-
from: msg.from,
|
|
485
|
-
isGroup,
|
|
486
|
-
textLength: text.length,
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
const bufferKey = isGroup
|
|
490
|
-
? `im:group:${msg.to}:${msg.from}`
|
|
491
|
-
: `im:${msg.from}`;
|
|
492
|
-
|
|
493
|
-
const makePrefix = (ts2) =>
|
|
494
|
-
isGroup
|
|
495
|
-
? `💬 [Seam群 ${ts2} ${groupId} → ${replyTool}]`
|
|
496
|
-
: `💬 [Seam ${ts2} → ${replyTool}]`;
|
|
497
|
-
|
|
498
|
-
if (messageBuffer) {
|
|
499
|
-
messageBuffer.queueText(
|
|
500
|
-
bufferKey,
|
|
501
|
-
text,
|
|
502
|
-
(items) => {
|
|
503
|
-
const ts = new Date().toTimeString().slice(0, 5);
|
|
504
|
-
const prefix = makePrefix(ts);
|
|
505
|
-
const joined = items.map((i) => i.content).join('\n');
|
|
506
|
-
if (items.length === 1) {
|
|
507
|
-
hub.inject(`${prefix} ${senderDisplay}: ${joined}`);
|
|
508
|
-
} else {
|
|
509
|
-
hub.inject(`${prefix} ${senderDisplay}:\n${joined}`);
|
|
510
|
-
}
|
|
511
|
-
if (eventBus) {
|
|
512
|
-
eventBus.emit('im.message', {
|
|
513
|
-
from: sender,
|
|
514
|
-
text: joined,
|
|
515
|
-
isGroup,
|
|
516
|
-
groupId,
|
|
517
|
-
conversationType,
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
},
|
|
521
|
-
{ delay: 5000, maxDelay: 15000 }
|
|
522
|
-
);
|
|
523
|
-
} else {
|
|
524
|
-
const ts = new Date().toTimeString().slice(0, 5);
|
|
525
|
-
hub.inject(`${makePrefix(ts)} ${senderDisplay}: ${text}`);
|
|
526
|
-
if (eventBus) {
|
|
527
|
-
eventBus.emit('im.message', { from: sender, text, isGroup, groupId, conversationType });
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
});
|
|
630
|
+
// 所有事件监听通过 setupAllListeners 统一注册(便于 rebuild 时重注册)
|
|
631
|
+
setupAllListeners();
|
|
532
632
|
|
|
533
633
|
try {
|
|
534
634
|
await chat.login({ userID: myUserId, userSig: credentials.userSig });
|
|
@@ -556,20 +656,10 @@ function createImPlugin() {
|
|
|
556
656
|
});
|
|
557
657
|
|
|
558
658
|
// 拉一次好友列表建立关系表 contacts.json
|
|
659
|
+
// FRIEND_LIST_UPDATED 监听已在 setupAllListeners 里注册
|
|
559
660
|
seedDisplayNameFromContacts();
|
|
560
661
|
await refreshContactsFromFriendList();
|
|
561
662
|
|
|
562
|
-
// 监听好友列表变化(加/删好友触发),自动更新 contacts.json
|
|
563
|
-
try {
|
|
564
|
-
chat.on(TencentCloudChat.EVENT.FRIEND_LIST_UPDATED, () => {
|
|
565
|
-
refreshContactsFromFriendList().catch((err) => {
|
|
566
|
-
log.warn('refresh_contacts_on_update_failed', { message: err.message });
|
|
567
|
-
});
|
|
568
|
-
});
|
|
569
|
-
} catch (err) {
|
|
570
|
-
log.warn('friend_list_listener_register_failed', { message: err.message });
|
|
571
|
-
}
|
|
572
|
-
|
|
573
663
|
// 首次入网:给邀请人发一条上线消息(仅一次,用 state 记录)
|
|
574
664
|
if (
|
|
575
665
|
stateScope
|
|
@@ -586,6 +676,21 @@ function createImPlugin() {
|
|
|
586
676
|
log.warn('announce_to_inviter_failed', { message: err.message });
|
|
587
677
|
}
|
|
588
678
|
}
|
|
679
|
+
|
|
680
|
+
// ===== 启动 Supervisor:keepalive + unhandled_rejection 监听 =====
|
|
681
|
+
startKeepalive();
|
|
682
|
+
|
|
683
|
+
processRejectionHandler = (reason) => {
|
|
684
|
+
const code = reason?.code;
|
|
685
|
+
if (code && SDK_REBUILD_ERROR_CODES.has(code)) {
|
|
686
|
+
scheduleRebuild(`unhandledRejection:${code}`);
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
process.on('unhandledRejection', processRejectionHandler);
|
|
690
|
+
log.info('supervisor_started', {
|
|
691
|
+
keepalive_ms: KEEPALIVE_INTERVAL_MS,
|
|
692
|
+
rebuild_codes: [...SDK_REBUILD_ERROR_CODES],
|
|
693
|
+
});
|
|
589
694
|
}
|
|
590
695
|
|
|
591
696
|
async function handleRequest(req) {
|
|
@@ -621,6 +726,14 @@ function createImPlugin() {
|
|
|
621
726
|
}
|
|
622
727
|
|
|
623
728
|
async function destroy() {
|
|
729
|
+
if (keepaliveTimer) {
|
|
730
|
+
clearInterval(keepaliveTimer);
|
|
731
|
+
keepaliveTimer = null;
|
|
732
|
+
}
|
|
733
|
+
if (processRejectionHandler) {
|
|
734
|
+
try { process.off('unhandledRejection', processRejectionHandler); } catch {}
|
|
735
|
+
processRejectionHandler = null;
|
|
736
|
+
}
|
|
624
737
|
if (chat) {
|
|
625
738
|
try {
|
|
626
739
|
await chat.logout();
|