@muyichengshayu/promptx 0.1.10 → 0.1.12
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 +16 -0
- package/apps/server/src/relayClient.js +298 -49
- package/apps/server/src/relayServer.js +344 -0
- package/apps/server/src/relayUsageStore.js +242 -0
- package/apps/server/src/repository.js +1 -1
- package/apps/server/src/systemRoutes.js +16 -0
- package/apps/web/dist/assets/{CodexSessionManagerDialog-BbS67-19.js → CodexSessionManagerDialog-C_yjlnuC.js} +1 -1
- package/apps/web/dist/assets/{TaskDiffReviewDialog-BNF6rrst.js → TaskDiffReviewDialog-sQO9cujF.js} +1 -1
- package/apps/web/dist/assets/WorkbenchSettingsDialog-0PZqZQaG.js +26 -0
- package/apps/web/dist/assets/{WorkbenchView-CNmiMvb0.js → WorkbenchView-CRCR5mRp.js} +4 -4
- package/apps/web/dist/assets/{index-DnOzjxzw.js → index-RfarWdXS.js} +1 -1
- package/apps/web/dist/assets/{info-BibVhe8O.js → info-Dsbc2slY.js} +1 -1
- package/apps/web/dist/index.html +1 -1
- package/docs/relay-quickstart.md +35 -4
- package/package.json +1 -1
- package/apps/web/dist/assets/WorkbenchSettingsDialog-BRvpf4VN.js +0 -26
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.12
|
|
4
|
+
|
|
5
|
+
- Relay 新增按天聚合的租户使用统计,自动记录设备连接与真实转发请求,方便查看“今天有哪些同事实际用了 Relay”。
|
|
6
|
+
- 新增 Relay 管理统计页 `/relay/admin/usage` 与对应接口,可直接查看今日活跃租户、连接次数、请求次数、最近设备与最近活跃时间。
|
|
7
|
+
- 统计能力默认落在云端 Relay 侧,不依赖本地 PromptX client 升级;同时补充文档说明与服务端测试覆盖。
|
|
8
|
+
|
|
9
|
+
## 0.1.11
|
|
10
|
+
|
|
11
|
+
- 加固 Relay 自动重连恢复链路:固定间隔重连升级为指数退避,并在连接成功后重置重连计数,减少 Relay 重启或网络抖动时的无效重试。
|
|
12
|
+
- 补齐 Relay client 健康检查与自愈能力:新增心跳超时巡检、假死连接主动重连,以及类似 Mac 休眠/唤醒后的时间跳变检测,避免远程页面断开后必须手动保存设置才能恢复。
|
|
13
|
+
- 修复 Relay client 旧连接 `close` 回调误伤新连接的竞态问题,避免在 Windows 等时序下反复出现“旧设备连接被新连接替换”的重连循环。
|
|
14
|
+
- 对 `invalid_token`、`invalid_device`、`invalid_tenant` 等不可恢复错误改为暂停自动重连,并在状态里明确暴露暂停原因,方便快速定位配置问题。
|
|
15
|
+
- 设置页“远程”增加“立即重连”按钮,并补充等待重连 / 已暂停重连等状态反馈,便于手动触发恢复和远程排障。
|
|
16
|
+
- 新增 Relay 重连相关服务端与接口测试,覆盖拒绝后暂停、重连成功清零、心跳超时自愈、休眠恢复近似场景和手动重连。
|
|
17
|
+
- 调整左侧任务列表为固定创建顺序,不再因运行、保存、自动刷新或 `updatedAt` 变化跳动;同时修复慢网切换任务时旧草稿标题短暂显示到新任务上的问题。
|
|
18
|
+
|
|
3
19
|
## 0.1.10
|
|
4
20
|
|
|
5
21
|
- 重构执行链路为 `server + runner` 双进程架构:`server` 专注 HTTP API、任务/项目存储与 SSE,`runner` 专注 agent 子进程生命周期、事件流、stop/kill/超时回收,显著降低长任务拖垮主服务的风险。
|
|
@@ -9,9 +9,17 @@ import {
|
|
|
9
9
|
sanitizeProxyHeaders,
|
|
10
10
|
} from './relayProtocol.js'
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const DEFAULT_RECONNECT_DELAYS_MS = [1_000, 2_000, 5_000, 10_000, 30_000]
|
|
13
|
+
const DEFAULT_HEALTH_CHECK_INTERVAL_MS = 10_000
|
|
14
|
+
const DEFAULT_HEARTBEAT_TIMEOUT_MS = 65_000
|
|
15
|
+
const DEFAULT_SLEEP_RESUME_THRESHOLD_MS = 45_000
|
|
13
16
|
const MAX_RECENT_EVENTS = 200
|
|
14
17
|
const REQUEST_CANCEL_REASON = 'relay_request_cancelled'
|
|
18
|
+
const NON_RETRYABLE_CLOSE_REASONS = new Set([
|
|
19
|
+
'invalid_tenant',
|
|
20
|
+
'invalid_token',
|
|
21
|
+
'invalid_device',
|
|
22
|
+
])
|
|
15
23
|
|
|
16
24
|
function createDisabledStatus() {
|
|
17
25
|
return {
|
|
@@ -27,6 +35,9 @@ function createDisabledStatus() {
|
|
|
27
35
|
lastCloseReason: '',
|
|
28
36
|
lastError: '',
|
|
29
37
|
reconnectCount: 0,
|
|
38
|
+
reconnectPaused: false,
|
|
39
|
+
reconnectPausedReason: '',
|
|
40
|
+
nextReconnectDelayMs: 0,
|
|
30
41
|
recentEvents: [],
|
|
31
42
|
}
|
|
32
43
|
}
|
|
@@ -51,6 +62,22 @@ function normalizeCloseReason(reason = '') {
|
|
|
51
62
|
return reasonMap[normalized] || normalized
|
|
52
63
|
}
|
|
53
64
|
|
|
65
|
+
function parseCloseReason(reason = '') {
|
|
66
|
+
const rawReason = Buffer.isBuffer(reason)
|
|
67
|
+
? reason.toString('utf8').trim()
|
|
68
|
+
: String(reason || '').trim()
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
rawReason,
|
|
72
|
+
closeReason: normalizeCloseReason(rawReason),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getReconnectDelayMs(reconnectCount = 0) {
|
|
77
|
+
const normalizedCount = Math.max(1, Number(reconnectCount) || 1)
|
|
78
|
+
return DEFAULT_RECONNECT_DELAYS_MS[Math.min(normalizedCount - 1, DEFAULT_RECONNECT_DELAYS_MS.length - 1)]
|
|
79
|
+
}
|
|
80
|
+
|
|
54
81
|
function readRelayClientConfig({
|
|
55
82
|
relayUrl = process.env.PROMPTX_RELAY_URL,
|
|
56
83
|
deviceId = process.env.PROMPTX_RELAY_DEVICE_ID,
|
|
@@ -85,6 +112,12 @@ function createRelayClient({
|
|
|
85
112
|
localBaseUrl,
|
|
86
113
|
logger = console,
|
|
87
114
|
appVersion = '0.0.0',
|
|
115
|
+
createWebSocket = (url) => new WebSocket(url),
|
|
116
|
+
reconnectDelayStrategy = getReconnectDelayMs,
|
|
117
|
+
healthCheckIntervalMs = DEFAULT_HEALTH_CHECK_INTERVAL_MS,
|
|
118
|
+
heartbeatTimeoutMs = DEFAULT_HEARTBEAT_TIMEOUT_MS,
|
|
119
|
+
sleepResumeThresholdMs = DEFAULT_SLEEP_RESUME_THRESHOLD_MS,
|
|
120
|
+
getNow = () => Date.now(),
|
|
88
121
|
} = {}) {
|
|
89
122
|
let config = readRelayClientConfig({
|
|
90
123
|
relayUrl,
|
|
@@ -104,8 +137,12 @@ function createRelayClient({
|
|
|
104
137
|
let socket = null
|
|
105
138
|
let stopped = false
|
|
106
139
|
let reconnectTimer = null
|
|
140
|
+
let healthCheckTimer = null
|
|
107
141
|
let requestMap = new Map()
|
|
108
142
|
let authenticated = false
|
|
143
|
+
let pendingReconnectSource = ''
|
|
144
|
+
let lastHealthCheckTickAt = 0
|
|
145
|
+
let connectionSequence = 0
|
|
109
146
|
|
|
110
147
|
function updateStatus(patch = {}) {
|
|
111
148
|
Object.assign(status, patch)
|
|
@@ -113,7 +150,7 @@ function createRelayClient({
|
|
|
113
150
|
|
|
114
151
|
function appendRecentEvent(type, extra = {}) {
|
|
115
152
|
const nextEvent = {
|
|
116
|
-
at: new Date().toISOString(),
|
|
153
|
+
at: new Date(getNow()).toISOString(),
|
|
117
154
|
type: String(type || '').trim() || 'unknown',
|
|
118
155
|
...extra,
|
|
119
156
|
}
|
|
@@ -163,6 +200,60 @@ function createRelayClient({
|
|
|
163
200
|
logger.error?.(message)
|
|
164
201
|
}
|
|
165
202
|
|
|
203
|
+
function nowIso() {
|
|
204
|
+
return new Date(getNow()).toISOString()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function clearReconnectTimer() {
|
|
208
|
+
if (reconnectTimer) {
|
|
209
|
+
clearTimeout(reconnectTimer)
|
|
210
|
+
reconnectTimer = null
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function clearHealthCheckTimer() {
|
|
215
|
+
if (healthCheckTimer) {
|
|
216
|
+
clearInterval(healthCheckTimer)
|
|
217
|
+
healthCheckTimer = null
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function resetReconnectState() {
|
|
222
|
+
updateStatus({
|
|
223
|
+
reconnectCount: 0,
|
|
224
|
+
reconnectPaused: false,
|
|
225
|
+
reconnectPausedReason: '',
|
|
226
|
+
nextReconnectDelayMs: 0,
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function getHeartbeatAgeMs(now = getNow()) {
|
|
231
|
+
const lastHeartbeatAt = Date.parse(status.lastHeartbeatAt || status.lastConnectedAt || '')
|
|
232
|
+
if (!Number.isFinite(lastHeartbeatAt)) {
|
|
233
|
+
return Number.POSITIVE_INFINITY
|
|
234
|
+
}
|
|
235
|
+
return Math.max(0, now - lastHeartbeatAt)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function shouldPauseReconnect(rawReason = '') {
|
|
239
|
+
return NON_RETRYABLE_CLOSE_REASONS.has(String(rawReason || '').trim())
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function pauseReconnect(rawReason = '') {
|
|
243
|
+
const reason = normalizeCloseReason(rawReason)
|
|
244
|
+
updateStatus({
|
|
245
|
+
reconnectPaused: true,
|
|
246
|
+
reconnectPausedReason: reason || String(rawReason || '').trim(),
|
|
247
|
+
nextReconnectDelayMs: 0,
|
|
248
|
+
})
|
|
249
|
+
appendRecentEvent('reconnect_paused', {
|
|
250
|
+
reason: reason || String(rawReason || '').trim() || 'unknown',
|
|
251
|
+
})
|
|
252
|
+
logWarn('[relay] 检测到不可重试错误,已暂停自动重连', getLogContext({
|
|
253
|
+
reason: reason || String(rawReason || '').trim() || 'unknown',
|
|
254
|
+
}))
|
|
255
|
+
}
|
|
256
|
+
|
|
166
257
|
syncStatusFromConfig()
|
|
167
258
|
|
|
168
259
|
function sendFrame(payload = {}) {
|
|
@@ -313,43 +404,142 @@ function createRelayClient({
|
|
|
313
404
|
}
|
|
314
405
|
}
|
|
315
406
|
|
|
316
|
-
function
|
|
317
|
-
|
|
407
|
+
function connectWithRetry(source = 'start') {
|
|
408
|
+
return connect().catch((error) => {
|
|
409
|
+
updateStatus({
|
|
410
|
+
lastError: error?.message || 'Relay 连接失败。',
|
|
411
|
+
})
|
|
412
|
+
appendRecentEvent('connect_failed', {
|
|
413
|
+
source,
|
|
414
|
+
reconnectCount: Number(status.reconnectCount || 0),
|
|
415
|
+
error: error?.message || String(error || ''),
|
|
416
|
+
})
|
|
417
|
+
logError('[relay] 连接失败', getLogContext({
|
|
418
|
+
source,
|
|
419
|
+
reconnectCount: Number(status.reconnectCount || 0),
|
|
420
|
+
error: error?.message || String(error || ''),
|
|
421
|
+
}))
|
|
422
|
+
scheduleReconnect({ source })
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function scheduleReconnect({ source = 'close' } = {}) {
|
|
427
|
+
if (stopped || reconnectTimer || !config.enabled || status.reconnectPaused) {
|
|
318
428
|
return
|
|
319
429
|
}
|
|
320
430
|
|
|
321
431
|
const nextReconnectCount = Number(status.reconnectCount || 0) + 1
|
|
432
|
+
const reconnectInMs = Math.max(100, Number(reconnectDelayStrategy(nextReconnectCount)) || getReconnectDelayMs(nextReconnectCount))
|
|
322
433
|
updateStatus({
|
|
323
434
|
reconnectCount: nextReconnectCount,
|
|
435
|
+
nextReconnectDelayMs: reconnectInMs,
|
|
324
436
|
})
|
|
325
437
|
appendRecentEvent('reconnect_scheduled', {
|
|
326
|
-
reconnectInMs
|
|
438
|
+
reconnectInMs,
|
|
327
439
|
reconnectCount: nextReconnectCount,
|
|
328
440
|
authenticated,
|
|
441
|
+
source,
|
|
329
442
|
})
|
|
330
443
|
logWarn('[relay] 已计划重连', getLogContext({
|
|
331
|
-
reconnectInMs
|
|
444
|
+
reconnectInMs,
|
|
332
445
|
reconnectCount: nextReconnectCount,
|
|
333
446
|
authenticated,
|
|
447
|
+
source,
|
|
334
448
|
}))
|
|
335
449
|
|
|
336
450
|
reconnectTimer = setTimeout(() => {
|
|
337
451
|
reconnectTimer = null
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
452
|
+
connectWithRetry(`reconnect:${source}`)
|
|
453
|
+
}, reconnectInMs)
|
|
454
|
+
reconnectTimer.unref?.()
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function triggerReconnect(source = 'manual') {
|
|
458
|
+
if (stopped || !config.enabled) {
|
|
459
|
+
return false
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
clearReconnectTimer()
|
|
463
|
+
updateStatus({
|
|
464
|
+
reconnectPaused: false,
|
|
465
|
+
reconnectPausedReason: '',
|
|
466
|
+
nextReconnectDelayMs: 0,
|
|
467
|
+
lastError: '',
|
|
468
|
+
})
|
|
469
|
+
pendingReconnectSource = source
|
|
470
|
+
appendRecentEvent('reconnect_requested', {
|
|
471
|
+
source,
|
|
472
|
+
socketReadyState: socket?.readyState ?? 3,
|
|
473
|
+
})
|
|
474
|
+
logInfo('[relay] 收到重连请求', getLogContext({
|
|
475
|
+
source,
|
|
476
|
+
socketReadyState: socket?.readyState ?? 3,
|
|
477
|
+
}))
|
|
478
|
+
|
|
479
|
+
if (!socket || socket.readyState === WebSocket.CLOSED) {
|
|
480
|
+
pendingReconnectSource = ''
|
|
481
|
+
connectWithRetry(source)
|
|
482
|
+
return true
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
if (socket.readyState === WebSocket.CONNECTING || socket.readyState === WebSocket.OPEN) {
|
|
487
|
+
socket.terminate()
|
|
488
|
+
} else {
|
|
489
|
+
socket.close(1012, `${source}_reconnect`)
|
|
490
|
+
}
|
|
491
|
+
} catch {
|
|
492
|
+
socket = null
|
|
493
|
+
authenticated = false
|
|
494
|
+
pendingReconnectSource = ''
|
|
495
|
+
connectWithRetry(source)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return true
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function runHealthCheck() {
|
|
502
|
+
if (stopped || !config.enabled) {
|
|
503
|
+
return
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const now = getNow()
|
|
507
|
+
const previousTickAt = lastHealthCheckTickAt
|
|
508
|
+
lastHealthCheckTickAt = now
|
|
509
|
+
|
|
510
|
+
if (previousTickAt > 0) {
|
|
511
|
+
const elapsedSinceLastTick = now - previousTickAt
|
|
512
|
+
if (elapsedSinceLastTick > sleepResumeThresholdMs) {
|
|
513
|
+
appendRecentEvent('system_resume_detected', {
|
|
514
|
+
elapsedMs: elapsedSinceLastTick,
|
|
341
515
|
})
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
516
|
+
logInfo('[relay] 检测到系统挂起/恢复,开始检查连接健康状态', getLogContext({
|
|
517
|
+
elapsedMs: elapsedSinceLastTick,
|
|
518
|
+
socketReadyState: socket?.readyState ?? 3,
|
|
519
|
+
connected: status.connected,
|
|
520
|
+
}))
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (socket?.readyState === WebSocket.OPEN && authenticated) {
|
|
525
|
+
const heartbeatAgeMs = getHeartbeatAgeMs(now)
|
|
526
|
+
if (heartbeatAgeMs > heartbeatTimeoutMs) {
|
|
527
|
+
appendRecentEvent('heartbeat_stale', {
|
|
528
|
+
heartbeatAgeMs,
|
|
529
|
+
heartbeatTimeoutMs,
|
|
345
530
|
})
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
531
|
+
logWarn('[relay] 心跳已过期,准备主动重连', getLogContext({
|
|
532
|
+
heartbeatAgeMs,
|
|
533
|
+
heartbeatTimeoutMs,
|
|
349
534
|
}))
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
535
|
+
triggerReconnect('heartbeat_timeout')
|
|
536
|
+
return
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (!socket && !reconnectTimer && !status.connected && !status.reconnectPaused) {
|
|
541
|
+
scheduleReconnect({ source: 'health_check_idle' })
|
|
542
|
+
}
|
|
353
543
|
}
|
|
354
544
|
|
|
355
545
|
async function connect() {
|
|
@@ -362,10 +552,15 @@ function createRelayClient({
|
|
|
362
552
|
|
|
363
553
|
appendRecentEvent('connect_start')
|
|
364
554
|
logInfo('[relay] 开始连接', getLogContext())
|
|
365
|
-
|
|
555
|
+
const currentSocket = createWebSocket(config.websocketUrl)
|
|
556
|
+
const connectionId = ++connectionSequence
|
|
557
|
+
socket = currentSocket
|
|
366
558
|
authenticated = false
|
|
367
559
|
|
|
368
|
-
|
|
560
|
+
currentSocket.on('open', () => {
|
|
561
|
+
if (socket !== currentSocket) {
|
|
562
|
+
return
|
|
563
|
+
}
|
|
369
564
|
appendRecentEvent('ws_open')
|
|
370
565
|
sendFrame({
|
|
371
566
|
type: 'hello',
|
|
@@ -373,10 +568,15 @@ function createRelayClient({
|
|
|
373
568
|
deviceToken: config.deviceToken,
|
|
374
569
|
version: appVersion,
|
|
375
570
|
})
|
|
376
|
-
logInfo('[relay] WebSocket 已建立,等待设备认证', getLogContext(
|
|
571
|
+
logInfo('[relay] WebSocket 已建立,等待设备认证', getLogContext({
|
|
572
|
+
connectionId,
|
|
573
|
+
}))
|
|
377
574
|
})
|
|
378
575
|
|
|
379
|
-
|
|
576
|
+
currentSocket.on('message', (payload, isBinary) => {
|
|
577
|
+
if (socket !== currentSocket) {
|
|
578
|
+
return
|
|
579
|
+
}
|
|
380
580
|
if (isBinary) {
|
|
381
581
|
return
|
|
382
582
|
}
|
|
@@ -390,10 +590,11 @@ function createRelayClient({
|
|
|
390
590
|
|
|
391
591
|
if (message?.type === 'hello.ack') {
|
|
392
592
|
authenticated = true
|
|
593
|
+
resetReconnectState()
|
|
393
594
|
updateStatus({
|
|
394
595
|
connected: true,
|
|
395
|
-
lastConnectedAt:
|
|
396
|
-
lastHeartbeatAt:
|
|
596
|
+
lastConnectedAt: nowIso(),
|
|
597
|
+
lastHeartbeatAt: nowIso(),
|
|
397
598
|
lastCloseCode: 0,
|
|
398
599
|
lastCloseReason: '',
|
|
399
600
|
lastError: '',
|
|
@@ -401,10 +602,12 @@ function createRelayClient({
|
|
|
401
602
|
appendRecentEvent('auth_ok', {
|
|
402
603
|
tenantKey: String(message?.tenantKey || '').trim(),
|
|
403
604
|
reconnectCount: Number(status.reconnectCount || 0),
|
|
605
|
+
connectionId,
|
|
404
606
|
})
|
|
405
607
|
logInfo('[relay] 设备认证成功,连接已就绪', getLogContext({
|
|
406
608
|
tenantKey: String(message?.tenantKey || '').trim(),
|
|
407
609
|
reconnectCount: Number(status.reconnectCount || 0),
|
|
610
|
+
connectionId,
|
|
408
611
|
}))
|
|
409
612
|
return
|
|
410
613
|
}
|
|
@@ -412,28 +615,49 @@ function createRelayClient({
|
|
|
412
615
|
handleIncomingFrame(JSON.stringify(message))
|
|
413
616
|
})
|
|
414
617
|
|
|
415
|
-
|
|
618
|
+
currentSocket.on('ping', () => {
|
|
619
|
+
if (socket !== currentSocket) {
|
|
620
|
+
return
|
|
621
|
+
}
|
|
416
622
|
updateStatus({
|
|
417
|
-
lastHeartbeatAt:
|
|
623
|
+
lastHeartbeatAt: nowIso(),
|
|
418
624
|
})
|
|
419
625
|
})
|
|
420
626
|
|
|
421
|
-
|
|
627
|
+
currentSocket.on('pong', () => {
|
|
628
|
+
if (socket !== currentSocket) {
|
|
629
|
+
return
|
|
630
|
+
}
|
|
422
631
|
updateStatus({
|
|
423
|
-
lastHeartbeatAt:
|
|
632
|
+
lastHeartbeatAt: nowIso(),
|
|
424
633
|
})
|
|
425
634
|
})
|
|
426
635
|
|
|
427
|
-
|
|
636
|
+
currentSocket.on('close', (code, reason) => {
|
|
637
|
+
if (socket !== currentSocket) {
|
|
638
|
+
appendRecentEvent('stale_close_ignored', {
|
|
639
|
+
code: Number(code || 0),
|
|
640
|
+
reason: parseCloseReason(reason).closeReason || '',
|
|
641
|
+
connectionId,
|
|
642
|
+
})
|
|
643
|
+
logInfo('[relay] 已忽略旧连接的 close 事件', getLogContext({
|
|
644
|
+
code: Number(code || 0),
|
|
645
|
+
connectionId,
|
|
646
|
+
}))
|
|
647
|
+
return
|
|
648
|
+
}
|
|
649
|
+
|
|
428
650
|
const wasAuthenticated = authenticated
|
|
429
|
-
const
|
|
651
|
+
const reconnectSource = pendingReconnectSource
|
|
652
|
+
pendingReconnectSource = ''
|
|
653
|
+
const { rawReason, closeReason } = parseCloseReason(reason)
|
|
430
654
|
const nextError = closeReason && closeReason !== '配置已更新,正在重连'
|
|
431
655
|
? `${wasAuthenticated ? 'Relay 已断开' : 'Relay 连接被拒绝'}:${closeReason}`
|
|
432
656
|
: (!wasAuthenticated && code && code !== 1000 ? `Relay 连接已关闭(code=${code})` : '')
|
|
433
657
|
|
|
434
658
|
updateStatus({
|
|
435
659
|
connected: false,
|
|
436
|
-
lastDisconnectedAt:
|
|
660
|
+
lastDisconnectedAt: nowIso(),
|
|
437
661
|
lastCloseCode: Number(code || 0),
|
|
438
662
|
lastCloseReason: closeReason,
|
|
439
663
|
...(nextError ? { lastError: nextError } : {}),
|
|
@@ -441,7 +665,10 @@ function createRelayClient({
|
|
|
441
665
|
appendRecentEvent('close', {
|
|
442
666
|
code: Number(code || 0),
|
|
443
667
|
reason: closeReason || '',
|
|
668
|
+
rawReason: rawReason || '',
|
|
444
669
|
authenticated: wasAuthenticated,
|
|
670
|
+
reconnectSource: reconnectSource || '',
|
|
671
|
+
connectionId,
|
|
445
672
|
})
|
|
446
673
|
socket = null
|
|
447
674
|
authenticated = false
|
|
@@ -449,19 +676,37 @@ function createRelayClient({
|
|
|
449
676
|
code: Number(code || 0),
|
|
450
677
|
reason: closeReason || 'none',
|
|
451
678
|
authenticated: wasAuthenticated,
|
|
679
|
+
reconnectSource: reconnectSource || '',
|
|
680
|
+
connectionId,
|
|
452
681
|
}))
|
|
453
|
-
|
|
682
|
+
|
|
683
|
+
if (reconnectSource) {
|
|
684
|
+
connectWithRetry(reconnectSource)
|
|
685
|
+
return
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (shouldPauseReconnect(rawReason)) {
|
|
689
|
+
pauseReconnect(rawReason)
|
|
690
|
+
return
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
scheduleReconnect({ source: rawReason || 'close' })
|
|
454
694
|
})
|
|
455
695
|
|
|
456
|
-
|
|
696
|
+
currentSocket.on('error', (error) => {
|
|
697
|
+
if (socket !== currentSocket) {
|
|
698
|
+
return
|
|
699
|
+
}
|
|
457
700
|
updateStatus({
|
|
458
701
|
lastError: error?.message || 'Relay 连接失败。',
|
|
459
702
|
})
|
|
460
703
|
appendRecentEvent('error', {
|
|
461
704
|
error: error?.message || String(error || ''),
|
|
705
|
+
connectionId,
|
|
462
706
|
})
|
|
463
707
|
logWarn('[relay] 连接异常', getLogContext({
|
|
464
708
|
error: error?.message || String(error || ''),
|
|
709
|
+
connectionId,
|
|
465
710
|
}))
|
|
466
711
|
})
|
|
467
712
|
}
|
|
@@ -469,6 +714,8 @@ function createRelayClient({
|
|
|
469
714
|
return {
|
|
470
715
|
start() {
|
|
471
716
|
stopped = false
|
|
717
|
+
clearReconnectTimer()
|
|
718
|
+
clearHealthCheckTimer()
|
|
472
719
|
if (!config.enabled) {
|
|
473
720
|
syncStatusFromConfig()
|
|
474
721
|
appendRecentEvent('disabled')
|
|
@@ -476,34 +723,29 @@ function createRelayClient({
|
|
|
476
723
|
return
|
|
477
724
|
}
|
|
478
725
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
logError('[relay] 初次连接失败', getLogContext({
|
|
487
|
-
error: error?.message || String(error || ''),
|
|
488
|
-
}))
|
|
489
|
-
scheduleReconnect()
|
|
490
|
-
})
|
|
726
|
+
lastHealthCheckTickAt = getNow()
|
|
727
|
+
healthCheckTimer = setInterval(() => {
|
|
728
|
+
runHealthCheck()
|
|
729
|
+
}, Math.max(20, Number(healthCheckIntervalMs) || DEFAULT_HEALTH_CHECK_INTERVAL_MS))
|
|
730
|
+
healthCheckTimer.unref?.()
|
|
731
|
+
|
|
732
|
+
connectWithRetry('start')
|
|
491
733
|
},
|
|
492
734
|
stop() {
|
|
493
735
|
stopped = true
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
reconnectTimer = null
|
|
497
|
-
}
|
|
736
|
+
clearReconnectTimer()
|
|
737
|
+
clearHealthCheckTimer()
|
|
498
738
|
requestMap.forEach((record) => {
|
|
499
739
|
record.controller?.abort()
|
|
500
740
|
})
|
|
501
741
|
requestMap = new Map()
|
|
742
|
+
pendingReconnectSource = ''
|
|
502
743
|
socket?.close()
|
|
503
744
|
socket = null
|
|
504
745
|
authenticated = false
|
|
505
746
|
updateStatus({
|
|
506
747
|
connected: false,
|
|
748
|
+
nextReconnectDelayMs: 0,
|
|
507
749
|
})
|
|
508
750
|
appendRecentEvent('stopped')
|
|
509
751
|
logInfo('[relay] 已停止', getLogContext())
|
|
@@ -517,6 +759,9 @@ function createRelayClient({
|
|
|
517
759
|
syncStatusFromConfig()
|
|
518
760
|
updateStatus({
|
|
519
761
|
lastError: '',
|
|
762
|
+
reconnectPaused: false,
|
|
763
|
+
reconnectPausedReason: '',
|
|
764
|
+
nextReconnectDelayMs: 0,
|
|
520
765
|
})
|
|
521
766
|
appendRecentEvent('config_updated', {
|
|
522
767
|
previousEnabled,
|
|
@@ -543,6 +788,9 @@ function createRelayClient({
|
|
|
543
788
|
this.start()
|
|
544
789
|
}
|
|
545
790
|
},
|
|
791
|
+
reconnect() {
|
|
792
|
+
return triggerReconnect('manual')
|
|
793
|
+
},
|
|
546
794
|
getStatus() {
|
|
547
795
|
return {
|
|
548
796
|
...status,
|
|
@@ -557,5 +805,6 @@ function createRelayClient({
|
|
|
557
805
|
export {
|
|
558
806
|
createDisabledStatus,
|
|
559
807
|
createRelayClient,
|
|
808
|
+
getReconnectDelayMs,
|
|
560
809
|
readRelayClientConfig,
|
|
561
810
|
}
|