From dbf935a570cf92536e041772568a008c8c15257d Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 12 Aug 2026 22:27:34 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AE=9E=E4=BE=8B=E9=A1=B5?= =?UTF-8?q?=E9=95=BF=E6=97=B6=E9=97=B4=E5=90=8E=E4=B8=8D=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=88=B7=E6=96=B0=EF=BC=9A=E5=9B=9E=E5=89=8D=E5=8F=B0=E5=BC=BA?= =?UTF-8?q?=E5=88=B6=E6=8B=89=E6=95=B0=E3=80=81SSE=E5=81=87=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E5=85=9C=E5=BA=95=E3=80=81=E5=BF=AB=E7=85=A7=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E8=B6=85=E6=97=B6=E9=98=B2=E5=8D=A1=E6=AD=BB=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- lib/common/static/instance_live.js | 54 ++++++++++++++++--- .../templates/embed_boot_scripts.html | 23 ++++++-- lib/instance/templates/embed_shell.html | 2 +- lib/instance/templates/index.html | 24 +++++++-- 4 files changed, 87 insertions(+), 16 deletions(-) diff --git a/lib/common/static/instance_live.js b/lib/common/static/instance_live.js index f51b3be..23aa5cf 100644 --- a/lib/common/static/instance_live.js +++ b/lib/common/static/instance_live.js @@ -1,12 +1,17 @@ /** - * embed 壳:SSE 收到后台 tick 后拉 JSON 快照更新 DOM,切换 tab 不再重复请求 HTML. + * embed 壳:SSE 收到后台 tick 后拉 JSON 快照更新 DOM. + * 同时为 standalone/embed 提供:回前台强制刷新、SSE 假连接兜底. */ (function (global) { let liveEventSource = null; let liveReconnectTimer = null; let localLiveVersion = -1; let sseConnected = false; + let lastLiveEventAt = 0; let refreshTimer = null; + let pendingWhileHidden = false; + /** 超过该毫秒无 live 事件则视为断流,允许 interval 兜底轮询 */ + const LIVE_STALE_MS = 45000; function isEmbedShell() { return document.body && document.body.getAttribute("data-embed-shell") === "1"; @@ -30,20 +35,37 @@ if (tab === "options" && global.OptionsPanelLive && typeof global.OptionsPanelLive.refreshSoft === "function") { global.OptionsPanelLive.refreshSoft(options); } - // 数据看板自有 SSE + 快照,不跟 embed live tick 重拉. } function scheduleRefresh(opts) { + if (document.hidden) { + pendingWhileHidden = true; + return; + } if (refreshTimer) return; const options = opts || {}; refreshTimer = setTimeout(function () { refreshTimer = null; - if (document.hidden) return; + if (document.hidden) { + pendingWhileHidden = true; + return; + } + pendingWhileHidden = false; refreshTabData(currentTab(), { silent: true, force: !!options.force }); }, 80); } + function kickRefresh(opts) { + pendingWhileHidden = false; + if (refreshTimer) { + clearTimeout(refreshTimer); + refreshTimer = null; + } + refreshTabData(currentTab(), opts || { silent: true, force: true }); + } + function onLiveEvent(data) { + lastLiveEventAt = Date.now(); const reason = data && data.reason; const ver = Number(data && data.live_version) || 0; if (!ver) return; @@ -59,7 +81,9 @@ function closeLiveStream() { if (liveEventSource) { - liveEventSource.close(); + try { + liveEventSource.close(); + } catch (_) {} liveEventSource = null; } if (liveReconnectTimer) { @@ -80,6 +104,7 @@ }); liveEventSource.onopen = function () { sseConnected = true; + lastLiveEventAt = Date.now(); }; liveEventSource.onerror = function () { sseConnected = false; @@ -90,7 +115,23 @@ }; } + function isConnectedFresh() { + if (!sseConnected || !liveEventSource) return false; + if (!lastLiveEventAt) return false; + return Date.now() - lastLiveEventAt < LIVE_STALE_MS; + } + function startLive() { + document.addEventListener("visibilitychange", function () { + if (document.hidden) return; + if (isEmbedShell()) { + if (!isConnectedFresh()) connectLiveStream(); + } + kickRefresh({ silent: true, force: true }); + }); + window.addEventListener("pageshow", function (ev) { + if (ev.persisted) kickRefresh({ silent: true, force: true }); + }); if (!isEmbedShell()) return; connectLiveStream(); } @@ -98,9 +139,8 @@ global.InstanceLive = { start: startLive, refreshTabData: refreshTabData, - isConnected: function () { - return sseConnected; - }, + kickRefresh: kickRefresh, + isConnected: isConnectedFresh, }; if (document.readyState === "loading") { diff --git a/lib/instance/templates/embed_boot_scripts.html b/lib/instance/templates/embed_boot_scripts.html index 8241233..47581fb 100644 --- a/lib/instance/templates/embed_boot_scripts.html +++ b/lib/instance/templates/embed_boot_scripts.html @@ -1173,6 +1173,17 @@ function accountSnapshotFundingMissing(data){ return !hasFunding && !hasTotal && !hasTrading; } let accountSnapshotRetryCount = 0; +let accountSnapshotInflight = false; +let priceSnapshotInflight = false; + +function fetchJsonWithTimeout(url, timeoutMs) { + const ms = timeoutMs != null ? timeoutMs : 25000; + const ac = new AbortController(); + const timer = setTimeout(function () { ac.abort(); }, ms); + return fetch(url, { signal: ac.signal, credentials: "same-origin" }) + .then(function (r) { return r.json(); }) + .finally(function () { clearTimeout(timer); }); +} function applyAccountSnapshot(data){ if(!data || typeof data !== "object") return; if(typeof data.show_perp_funds !== "undefined"){ @@ -1263,8 +1274,10 @@ function applyAccountSnapshot(data){ } function refreshAccountSnapshot(opts){ const options = opts || {}; + if(accountSnapshotInflight && !options.force) return; + accountSnapshotInflight = true; const qs = options.force ? "?force=1" : ""; - fetch("/api/account_snapshot" + qs).then(r=>r.json()).then(data=>{ + fetchJsonWithTimeout("/api/account_snapshot" + qs, 25000).then(data=>{ applyAccountSnapshot(data); if(accountSnapshotFundingMissing(data) && !options.force && accountSnapshotRetryCount < 3){ accountSnapshotRetryCount += 1; @@ -1277,7 +1290,7 @@ function refreshAccountSnapshot(opts){ accountSnapshotRetryCount += 1; setTimeout(() => refreshAccountSnapshot({ silent: true }), 1200 * accountSnapshotRetryCount); } - }); + }).finally(()=>{ accountSnapshotInflight = false; }); } const orderSymbolEl = document.getElementById("order-symbol"); @@ -1419,8 +1432,10 @@ refreshOrderDefaults(); if(typeof initOrderEntryModelSelect === "function") initOrderEntryModelSelect(); refreshPriceSnapshotConditional(); function refreshPriceSnapshotConditional(){ + if(priceSnapshotInflight) return; + priceSnapshotInflight = true; const page = document.body.getAttribute("data-page") || ""; - fetch("/api/price_snapshot").then(r=>r.json()).then(data=>{ + fetchJsonWithTimeout("/api/price_snapshot", 25000).then(data=>{ const updatedEl = document.getElementById("price-last-updated"); if(data.updated_at && updatedEl) updatedEl.innerText = data.updated_at; if(data.force_close && window.TimeCloseUI && TimeCloseUI.paintForceCloseHeader){ @@ -1474,7 +1489,7 @@ function refreshPriceSnapshotConditional(){ } else if (typeof data.options_unrealized_pnl !== "undefined") { paintRealtimePnlFromSnapshot(data); } - }).catch(()=>{}); + }).catch(()=>{}).finally(()=>{ priceSnapshotInflight = false; }); } function formatLiveHoldDurationFromMs(openedMs, nowMs){ if(openedMs == null || openedMs === "" || !Number.isFinite(Number(openedMs))) return "—"; diff --git a/lib/instance/templates/embed_shell.html b/lib/instance/templates/embed_shell.html index 07cbbe4..619c2bc 100644 --- a/lib/instance/templates/embed_shell.html +++ b/lib/instance/templates/embed_shell.html @@ -171,7 +171,7 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj window.__INSTANCE_DISPLAY__ = {{ display | tojson }}; - + diff --git a/lib/instance/templates/index.html b/lib/instance/templates/index.html index 0153cf4..89efc5a 100644 --- a/lib/instance/templates/index.html +++ b/lib/instance/templates/index.html @@ -1654,6 +1654,17 @@ function accountSnapshotFundingMissing(data){ return !hasFunding && !hasTotal && !hasTrading; } let accountSnapshotRetryCount = 0; +let accountSnapshotInflight = false; +let priceSnapshotInflight = false; + +function fetchJsonWithTimeout(url, timeoutMs) { + const ms = timeoutMs != null ? timeoutMs : 25000; + const ac = new AbortController(); + const timer = setTimeout(function () { ac.abort(); }, ms); + return fetch(url, { signal: ac.signal, credentials: "same-origin" }) + .then(function (r) { return r.json(); }) + .finally(function () { clearTimeout(timer); }); +} function applyAccountSnapshot(data){ if(!data || typeof data !== "object") return; if(typeof data.show_perp_funds !== "undefined"){ @@ -1753,8 +1764,10 @@ function applyAccountSnapshot(data){ } function refreshAccountSnapshot(opts){ const options = opts || {}; + if(accountSnapshotInflight && !options.force) return; + accountSnapshotInflight = true; const qs = options.force ? "?force=1" : ""; - fetch("/api/account_snapshot" + qs).then(r=>r.json()).then(data=>{ + fetchJsonWithTimeout("/api/account_snapshot" + qs, 25000).then(data=>{ applyAccountSnapshot(data); if(accountSnapshotFundingMissing(data) && !options.force && accountSnapshotRetryCount < 3){ accountSnapshotRetryCount += 1; @@ -1767,7 +1780,7 @@ function refreshAccountSnapshot(opts){ accountSnapshotRetryCount += 1; setTimeout(() => refreshAccountSnapshot({ silent: true }), 1200 * accountSnapshotRetryCount); } - }); + }).finally(()=>{ accountSnapshotInflight = false; }); } {% if ui_open_guard_enabled %} @@ -1925,8 +1938,10 @@ refreshOrderDefaults(); refreshPriceSnapshotConditional(); setInterval(refreshAccountSnapshot, {{ balance_refresh_seconds * 1000 }}); function refreshPriceSnapshotConditional(){ + if(priceSnapshotInflight) return; + priceSnapshotInflight = true; const page = document.body.getAttribute("data-page") || ""; - fetch("/api/price_snapshot").then(r=>r.json()).then(data=>{ + fetchJsonWithTimeout("/api/price_snapshot", 25000).then(data=>{ const updatedEl = document.getElementById("price-last-updated"); if(data.updated_at && updatedEl) updatedEl.innerText = data.updated_at; if(data.force_close && window.TimeCloseUI && TimeCloseUI.paintForceCloseHeader){ @@ -2000,7 +2015,7 @@ function refreshPriceSnapshotConditional(){ } else if (typeof data.options_unrealized_pnl !== "undefined") { paintRealtimePnlFromSnapshot(data); } - }).catch(()=>{}); + }).catch(()=>{}).finally(()=>{ priceSnapshotInflight = false; }); } function formatLiveHoldDurationFromMs(openedMs, nowMs){ if(openedMs == null || openedMs === "" || !Number.isFinite(Number(openedMs))) return "—"; @@ -2033,6 +2048,7 @@ setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }}) +