@seamnet/client 0.13.2 → 0.13.4
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/guardian.js +13 -0
- package/lib/plugins/im/index.cjs +75 -13
- package/package.json +1 -1
package/lib/guardian.js
CHANGED
|
@@ -130,6 +130,19 @@ export async function guardianRun() {
|
|
|
130
130
|
process.exit(1);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
// 单例保证:检查 PID 文件,如已有活进程则退出
|
|
134
|
+
if (isGuardianRunning()) {
|
|
135
|
+
const existingPid = readGuardianPid();
|
|
136
|
+
console.error(`Guardian already running (pid: ${existingPid}). Exiting.`);
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
// 写 PID,退出时清理
|
|
140
|
+
writeFileSync(PID_PATH, String(process.pid));
|
|
141
|
+
const cleanPid = () => { try { if (existsSync(PID_PATH)) unlinkSync(PID_PATH); } catch {} };
|
|
142
|
+
process.on('exit', cleanPid);
|
|
143
|
+
process.on('SIGTERM', () => { cleanPid(); process.exit(0); });
|
|
144
|
+
process.on('SIGINT', () => { cleanPid(); process.exit(0); });
|
|
145
|
+
|
|
133
146
|
const credentials = JSON.parse(readFileSync(CREDENTIALS_PATH, 'utf8'));
|
|
134
147
|
const ccSession = process.env.SEAM_CC_SESSION || '';
|
|
135
148
|
const ccSocket = process.env.SEAM_CC_SOCKET || resolveTmuxSocketPath() || '';
|
package/lib/plugins/im/index.cjs
CHANGED
|
@@ -74,6 +74,19 @@ const { validatePayload } = require('../../contracts/actions.cjs');
|
|
|
74
74
|
}
|
|
75
75
|
})();
|
|
76
76
|
|
|
77
|
+
// SDK 依赖 window.addEventListener("online") 触发内部 reConnect()。
|
|
78
|
+
// 用真实 EventEmitter 让 SDK 注册的 listener 能被 emit 调用。
|
|
79
|
+
const { EventEmitter: _PolyfillEmitter } = require('node:events');
|
|
80
|
+
const _windowEvents = new _PolyfillEmitter();
|
|
81
|
+
const _documentEvents = new _PolyfillEmitter();
|
|
82
|
+
|
|
83
|
+
// SDK 的 NetMonitor 检查 previous !== current 才 reConnect。
|
|
84
|
+
// 必须先 offline 再 online,否则 online→online 不触发。
|
|
85
|
+
function emitNetworkRecovery() {
|
|
86
|
+
_windowEvents.emit('offline');
|
|
87
|
+
setImmediate(() => _windowEvents.emit('online'));
|
|
88
|
+
}
|
|
89
|
+
|
|
77
90
|
function setupSdkGlobals() {
|
|
78
91
|
if (global._imSdkGlobalsSet) return;
|
|
79
92
|
global._imSdkGlobalsSet = true;
|
|
@@ -84,8 +97,8 @@ function setupSdkGlobals() {
|
|
|
84
97
|
host: 'localhost',
|
|
85
98
|
hostname: 'localhost',
|
|
86
99
|
},
|
|
87
|
-
addEventListener: () =>
|
|
88
|
-
removeEventListener: () =>
|
|
100
|
+
addEventListener: (type, fn) => _windowEvents.on(type, fn),
|
|
101
|
+
removeEventListener: (type, fn) => _windowEvents.off(type, fn),
|
|
89
102
|
URL: Object.assign(
|
|
90
103
|
function (...a) {
|
|
91
104
|
return new (require('node:url').URL)(...a);
|
|
@@ -97,8 +110,8 @@ function setupSdkGlobals() {
|
|
|
97
110
|
),
|
|
98
111
|
};
|
|
99
112
|
global.document = {
|
|
100
|
-
addEventListener: () =>
|
|
101
|
-
removeEventListener: () =>
|
|
113
|
+
addEventListener: (type, fn) => _documentEvents.on(type, fn),
|
|
114
|
+
removeEventListener: (type, fn) => _documentEvents.off(type, fn),
|
|
102
115
|
characterSet: 'UTF-8',
|
|
103
116
|
};
|
|
104
117
|
global.navigator = { userAgent: 'node', language: 'en', platform: 'linux' };
|
|
@@ -147,7 +160,7 @@ function createImPlugin() {
|
|
|
147
160
|
// 重建失败后自驱重试:指数退避 + jitter;成功后重置
|
|
148
161
|
let rebuildRetryAttempt = 0;
|
|
149
162
|
let rebuildRetryTimer = null;
|
|
150
|
-
const REBUILD_COOLDOWN_MS =
|
|
163
|
+
const REBUILD_COOLDOWN_MS = 60_000;
|
|
151
164
|
const REBUILD_DEBOUNCE_MS = 3_000;
|
|
152
165
|
const REBUILD_WATCHDOG_MS = 30_000; // 整个重建最多 30s,超时强制失败
|
|
153
166
|
const SDK_CALL_TIMEOUT_MS = 10_000; // 单个 SDK 调用超时
|
|
@@ -158,8 +171,9 @@ function createImPlugin() {
|
|
|
158
171
|
const KEEPALIVE_TIMEOUT_MS = 10_000;
|
|
159
172
|
// 重建失败退避表(最后一档持续)
|
|
160
173
|
const REBUILD_RETRY_BACKOFF_MS = [3_000, 10_000, 30_000, 60_000];
|
|
161
|
-
//
|
|
162
|
-
|
|
174
|
+
// 不再主动 rebuild 2801——SDK 内部有自己的重连,0.12.7 证明十几天不断。
|
|
175
|
+
// 0.13.0 加 rebuild 反而制造僵尸循环。只靠 keepalive(3min)和 kicked_out 兜底。
|
|
176
|
+
const SDK_REBUILD_ERROR_CODES = new Set();
|
|
163
177
|
|
|
164
178
|
function withTimeout(promise, ms, name) {
|
|
165
179
|
let tid = null;
|
|
@@ -370,13 +384,17 @@ function createImPlugin() {
|
|
|
370
384
|
return {
|
|
371
385
|
onSdkReady: () => {
|
|
372
386
|
if (gen !== chatGeneration) return;
|
|
373
|
-
//
|
|
387
|
+
// SDK 自愈成功(reConnect 或 rebuild 后)
|
|
388
|
+
sdkReady = true;
|
|
389
|
+
clearNotReadyGrace();
|
|
374
390
|
log.info('sdk_ready', { userId: myUserId, gen });
|
|
375
391
|
},
|
|
376
392
|
onSdkNotReady: () => {
|
|
377
393
|
if (gen !== chatGeneration) return;
|
|
378
394
|
sdkReady = false;
|
|
379
395
|
log.warn('sdk_not_ready', { gen });
|
|
396
|
+
// 启动 grace timer:5 分钟没自愈就 rebuild
|
|
397
|
+
startNotReadyGrace();
|
|
380
398
|
},
|
|
381
399
|
onSdkError: (event) => {
|
|
382
400
|
if (gen !== chatGeneration) return;
|
|
@@ -566,11 +584,12 @@ function createImPlugin() {
|
|
|
566
584
|
// ===== 候选模式构建新 chat(失败则隔离销毁,不污染 active chat)=====
|
|
567
585
|
async function buildChatCandidate() {
|
|
568
586
|
const myGen = ++chatGeneration;
|
|
569
|
-
//
|
|
587
|
+
// 先铺 window(带真实 EventEmitter),再 create——
|
|
588
|
+
// SDK 在 create 时注册 window.addEventListener("online", reConnect)
|
|
570
589
|
delete global.window;
|
|
571
590
|
global._imSdkGlobalsSet = false;
|
|
572
|
-
const nextChat = TencentCloudChat.create({ SDKAppID: Number(credentialsRef.sdkAppId) });
|
|
573
591
|
setupSdkGlobals();
|
|
592
|
+
const nextChat = TencentCloudChat.create({ SDKAppID: Number(credentialsRef.sdkAppId) });
|
|
574
593
|
try {
|
|
575
594
|
const TIMUploadPlugin = require('tim-upload-plugin');
|
|
576
595
|
nextChat.registerPlugin({ 'tim-upload-plugin': TIMUploadPlugin });
|
|
@@ -689,15 +708,51 @@ function createImPlugin() {
|
|
|
689
708
|
setTimeout(() => { doRebuild(reason).catch(() => {}); }, REBUILD_DEBOUNCE_MS);
|
|
690
709
|
}
|
|
691
710
|
|
|
711
|
+
let keepaliveFailCount = 0;
|
|
712
|
+
let notReadyGraceTimer = null;
|
|
713
|
+
const NOT_READY_GRACE_MS = 5 * 60 * 1000; // SDK_NOT_READY 后 5 分钟没恢复就 rebuild
|
|
714
|
+
|
|
715
|
+
function startNotReadyGrace() {
|
|
716
|
+
if (notReadyGraceTimer) return;
|
|
717
|
+
notReadyGraceTimer = setTimeout(() => {
|
|
718
|
+
notReadyGraceTimer = null;
|
|
719
|
+
if (!sdkReady && !rebuilding && !rebuildScheduled) {
|
|
720
|
+
log.warn('not_ready_grace_expired');
|
|
721
|
+
scheduleRebuild('not_ready_grace_expired');
|
|
722
|
+
}
|
|
723
|
+
}, NOT_READY_GRACE_MS);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function clearNotReadyGrace() {
|
|
727
|
+
if (notReadyGraceTimer) { clearTimeout(notReadyGraceTimer); notReadyGraceTimer = null; }
|
|
728
|
+
}
|
|
729
|
+
|
|
692
730
|
function startKeepalive() {
|
|
693
731
|
if (keepaliveTimer) return;
|
|
694
732
|
keepaliveTimer = setInterval(async () => {
|
|
695
|
-
if (
|
|
733
|
+
if (rebuilding || rebuildScheduled) return;
|
|
734
|
+
// sdkReady=false 时也检测(NOT_READY 后需要探测恢复)
|
|
735
|
+
if (!sdkReady) {
|
|
736
|
+
// 尝试 emit 网络恢复信号帮 SDK 重连
|
|
737
|
+
log.info('keepalive_nudge_not_ready');
|
|
738
|
+
emitNetworkRecovery();
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
696
741
|
try {
|
|
697
742
|
await withTimeout(chat.getMyProfile(), KEEPALIVE_TIMEOUT_MS, 'keepalive');
|
|
743
|
+
keepaliveFailCount = 0;
|
|
698
744
|
} catch (e) {
|
|
699
|
-
|
|
700
|
-
|
|
745
|
+
keepaliveFailCount += 1;
|
|
746
|
+
log.warn('keepalive_failed', { message: e.message, failCount: keepaliveFailCount });
|
|
747
|
+
if (keepaliveFailCount === 1) {
|
|
748
|
+
// 第一次失败:模拟断网恢复,让 SDK 内部 reConnect()
|
|
749
|
+
log.info('emitting_network_recovery');
|
|
750
|
+
emitNetworkRecovery();
|
|
751
|
+
} else {
|
|
752
|
+
// 连续失败:SDK 自愈没成功,升级到 rebuild
|
|
753
|
+
scheduleRebuild(`keepalive_consecutive:${keepaliveFailCount}`);
|
|
754
|
+
keepaliveFailCount = 0;
|
|
755
|
+
}
|
|
701
756
|
}
|
|
702
757
|
}, KEEPALIVE_INTERVAL_MS);
|
|
703
758
|
}
|
|
@@ -769,6 +824,12 @@ function createImPlugin() {
|
|
|
769
824
|
processRejectionHandler = (reason) => {
|
|
770
825
|
const code = reason?.code;
|
|
771
826
|
if (code && SDK_REBUILD_ERROR_CODES.has(code)) {
|
|
827
|
+
// sdkReady=true 说明当前 session 健康,2801 来自被 destroy 但没清干净的老实例
|
|
828
|
+
// 真正断了会被 keepalive(3min)或 onSdkError(generation-scoped)兜住
|
|
829
|
+
if (sdkReady) {
|
|
830
|
+
log.info('stale_rejection_ignored', { code });
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
772
833
|
scheduleRebuild(`unhandledRejection:${code}`);
|
|
773
834
|
}
|
|
774
835
|
};
|
|
@@ -820,6 +881,7 @@ function createImPlugin() {
|
|
|
820
881
|
clearTimeout(rebuildRetryTimer);
|
|
821
882
|
rebuildRetryTimer = null;
|
|
822
883
|
}
|
|
884
|
+
clearNotReadyGrace();
|
|
823
885
|
if (processRejectionHandler) {
|
|
824
886
|
try { process.off('unhandledRejection', processRejectionHandler); } catch {}
|
|
825
887
|
processRejectionHandler = null;
|