diff --git a/server/status.py b/server/status.py index 644efbe..85f4ea6 100644 --- a/server/status.py +++ b/server/status.py @@ -69,6 +69,28 @@ def _is_host_mode_request() -> bool: return bool(session.get("host_mode")) and (TERMINAL_SANDBOX_MODE or "").lower() == "host" +@status_bp.route('/api/health') +@api_login_required +def get_health(): + """轻量健康检查:用于前端连接心跳,避免触发重型 status 计算。""" + started_at = time.perf_counter() + heartbeat_id = request.headers.get("X-Connection-Heartbeat", "").strip() + username = session.get("username") or "-" + payload = { + "success": True, + "status": "ok", + "server_time_ms": int(time.time() * 1000), + } + elapsed_ms = (time.perf_counter() - started_at) * 1000 + if request.args.get("diag") == "1" or elapsed_ms >= 800: + log_conn_diag( + f"health hb={heartbeat_id or '-'} user={username} " + f"elapsed_ms={elapsed_ms:.1f} host_mode={bool(session.get('host_mode'))} " + f"workspace_id={session.get('workspace_id') or '-'}" + ) + return jsonify(payload) + + @status_bp.route('/api/status') @api_login_required @with_terminal diff --git a/static/src/app/methods/ui.ts b/static/src/app/methods/ui.ts index 60908ce..1dba2f6 100644 --- a/static/src/app/methods/ui.ts +++ b/static/src/app/methods/ui.ts @@ -2025,18 +2025,32 @@ export const uiMethods = { }, async checkConnectionHealth() { + if (this.connectionHeartbeatInFlight) { + connectionDiag('log', 'health-skip-inflight', { + seq: this.connectionHeartbeatSeq, + isConnected: !!this.isConnected, + failCount: this.connectionHeartbeatFailCount + }); + return; + } + this.connectionHeartbeatInFlight = true; const seq = Number(this.connectionHeartbeatSeq || 0) + 1; this.connectionHeartbeatSeq = seq; const requestId = `${Date.now()}-${seq}`; const startedAt = Date.now(); const diagEnabled = isConnectionDiagEnabled(); - const statusUrl = diagEnabled ? '/api/status?diag=1' : '/api/status'; + const healthUrl = diagEnabled ? '/api/health?diag=1' : '/api/health'; const wasConnected = !!this.isConnected; let responseStatus: number | null = null; const controller = typeof AbortController !== 'undefined' ? new AbortController() : null; - const timeoutId = controller ? window.setTimeout(() => controller.abort(), 5000) : null; + const timeoutMs = + typeof this.connectionHeartbeatRequestTimeoutMs === 'number' && + this.connectionHeartbeatRequestTimeoutMs > 0 + ? this.connectionHeartbeatRequestTimeoutMs + : 5000; + const timeoutId = controller ? window.setTimeout(() => controller.abort(), timeoutMs) : null; try { - const response = await fetch(statusUrl, { + const response = await fetch(healthUrl, { method: 'GET', cache: 'no-store', headers: { @@ -2066,6 +2080,7 @@ export const uiMethods = { failCountBeforeRecover, status: responseStatus, diagEnabled, + endpoint: healthUrl, conversationId: this.currentConversationId || null, taskInProgress: !!this.taskInProgress, streamingMessage: !!this.streamingMessage, @@ -2083,6 +2098,7 @@ export const uiMethods = { seq, elapsedMs: this.connectionHeartbeatLastLatencyMs, status: responseStatus, + endpoint: healthUrl, visibility: document?.visibilityState || 'unknown' }); } @@ -2094,12 +2110,20 @@ export const uiMethods = { const errName = error?.name || 'Error'; const errMessage = error?.message || String(error); this.connectionHeartbeatLastError = `${errName}: ${errMessage}`; - // 一次失败就先置灰,避免“服务已断但常亮绿色”的误导 - this.isConnected = false; - if (wasConnected) { + const failThreshold = + typeof this.connectionHeartbeatFailThreshold === 'number' && + this.connectionHeartbeatFailThreshold > 0 + ? this.connectionHeartbeatFailThreshold + : 3; + const shouldDisconnect = nextFailCount >= failThreshold; + // 改为连续失败阈值后再置灰,避免偶发请求超时导致“假断连” + if (shouldDisconnect) { + this.isConnected = false; + } + if (wasConnected && shouldDisconnect) { this.connectionHeartbeatLastChangeAt = Date.now(); } - const shouldLogFail = wasConnected || nextFailCount <= 3 || nextFailCount % 10 === 0; + const shouldLogFail = wasConnected || nextFailCount <= failThreshold || nextFailCount % 10 === 0; connectionDiag( shouldLogFail ? 'warn' : 'log', 'health-failed', @@ -2109,9 +2133,12 @@ export const uiMethods = { elapsedMs: this.connectionHeartbeatLastLatencyMs, failCount: nextFailCount, status: responseStatus, + endpoint: healthUrl, diagEnabled, wasConnected, nowConnected: !!this.isConnected, + failThreshold, + shouldDisconnect, errorName: errName, errorMessage: errMessage, timeout: errName === 'AbortError', @@ -2125,6 +2152,7 @@ export const uiMethods = { { force: shouldLogFail } ); } finally { + this.connectionHeartbeatInFlight = false; if (timeoutId) { clearTimeout(timeoutId); } diff --git a/static/src/app/state.ts b/static/src/app/state.ts index 76b298f..d2e60b2 100644 --- a/static/src/app/state.ts +++ b/static/src/app/state.ts @@ -144,6 +144,9 @@ export function dataState() { connectionHeartbeatLastError: '', connectionHeartbeatLastStatusCode: null, connectionHeartbeatLastChangeAt: 0, + connectionHeartbeatInFlight: false, + connectionHeartbeatFailThreshold: 3, + connectionHeartbeatRequestTimeoutMs: 5000, connectionHeartbeatIntervalMs: 8000, connectionHeartbeatDisconnectedIntervalMs: 1000,