@seamnet/client 0.13.3 → 0.13.5
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 +65 -10
- 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
|
+
const existingPid = readGuardianPid();
|
|
135
|
+
if (existingPid !== null && existingPid !== process.pid && isPidAlive(existingPid)) {
|
|
136
|
+
console.error(`Guardian already running (pid: ${existingPid}). Exiting.`);
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
// 写 PID(覆盖 guardianStart 写的 child.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' };
|
|
@@ -371,13 +384,17 @@ function createImPlugin() {
|
|
|
371
384
|
return {
|
|
372
385
|
onSdkReady: () => {
|
|
373
386
|
if (gen !== chatGeneration) return;
|
|
374
|
-
//
|
|
387
|
+
// SDK 自愈成功(reConnect 或 rebuild 后)
|
|
388
|
+
sdkReady = true;
|
|
389
|
+
clearNotReadyGrace();
|
|
375
390
|
log.info('sdk_ready', { userId: myUserId, gen });
|
|
376
391
|
},
|
|
377
392
|
onSdkNotReady: () => {
|
|
378
393
|
if (gen !== chatGeneration) return;
|
|
379
394
|
sdkReady = false;
|
|
380
395
|
log.warn('sdk_not_ready', { gen });
|
|
396
|
+
// 启动 grace timer:5 分钟没自愈就 rebuild
|
|
397
|
+
startNotReadyGrace();
|
|
381
398
|
},
|
|
382
399
|
onSdkError: (event) => {
|
|
383
400
|
if (gen !== chatGeneration) return;
|
|
@@ -567,11 +584,12 @@ function createImPlugin() {
|
|
|
567
584
|
// ===== 候选模式构建新 chat(失败则隔离销毁,不污染 active chat)=====
|
|
568
585
|
async function buildChatCandidate() {
|
|
569
586
|
const myGen = ++chatGeneration;
|
|
570
|
-
//
|
|
587
|
+
// 先铺 window(带真实 EventEmitter),再 create——
|
|
588
|
+
// SDK 在 create 时注册 window.addEventListener("online", reConnect)
|
|
571
589
|
delete global.window;
|
|
572
590
|
global._imSdkGlobalsSet = false;
|
|
573
|
-
const nextChat = TencentCloudChat.create({ SDKAppID: Number(credentialsRef.sdkAppId) });
|
|
574
591
|
setupSdkGlobals();
|
|
592
|
+
const nextChat = TencentCloudChat.create({ SDKAppID: Number(credentialsRef.sdkAppId) });
|
|
575
593
|
try {
|
|
576
594
|
const TIMUploadPlugin = require('tim-upload-plugin');
|
|
577
595
|
nextChat.registerPlugin({ 'tim-upload-plugin': TIMUploadPlugin });
|
|
@@ -690,15 +708,51 @@ function createImPlugin() {
|
|
|
690
708
|
setTimeout(() => { doRebuild(reason).catch(() => {}); }, REBUILD_DEBOUNCE_MS);
|
|
691
709
|
}
|
|
692
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
|
+
|
|
693
730
|
function startKeepalive() {
|
|
694
731
|
if (keepaliveTimer) return;
|
|
695
732
|
keepaliveTimer = setInterval(async () => {
|
|
696
|
-
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
|
+
}
|
|
697
741
|
try {
|
|
698
742
|
await withTimeout(chat.getMyProfile(), KEEPALIVE_TIMEOUT_MS, 'keepalive');
|
|
743
|
+
keepaliveFailCount = 0;
|
|
699
744
|
} catch (e) {
|
|
700
|
-
|
|
701
|
-
|
|
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
|
+
}
|
|
702
756
|
}
|
|
703
757
|
}, KEEPALIVE_INTERVAL_MS);
|
|
704
758
|
}
|
|
@@ -827,6 +881,7 @@ function createImPlugin() {
|
|
|
827
881
|
clearTimeout(rebuildRetryTimer);
|
|
828
882
|
rebuildRetryTimer = null;
|
|
829
883
|
}
|
|
884
|
+
clearNotReadyGrace();
|
|
830
885
|
if (processRejectionHandler) {
|
|
831
886
|
try { process.off('unhandledRejection', processRejectionHandler); } catch {}
|
|
832
887
|
processRejectionHandler = null;
|