From 01ef7340ff712e3b238a50ea87677d294e3b4248 Mon Sep 17 00:00:00 2001
From: Pigbibi <20649888+Pigbibi@users.noreply.github.com>
Date: Tue, 9 Jun 2026 16:06:28 +0800
Subject: [PATCH] Avoid blocking account config on status reads
---
tests/strategy_switch_worker_validation.mjs | 5 +++++
web/strategy-switch-console/index.html | 21 ++++++++++++-----
web/strategy-switch-console/page_asset.js | 2 +-
web/strategy-switch-console/worker.js | 25 +++++++++++++++++++--
4 files changed, 45 insertions(+), 8 deletions(-)
diff --git a/tests/strategy_switch_worker_validation.mjs b/tests/strategy_switch_worker_validation.mjs
index 748ac72..276ebe4 100644
--- a/tests/strategy_switch_worker_validation.mjs
+++ b/tests/strategy_switch_worker_validation.mjs
@@ -43,6 +43,11 @@ const crossOriginError = captureError(
assert.match(crossOriginError.message, /cross-origin request rejected/);
assert.equal(crossOriginError.status, 403);
+assert.equal(
+ await __test.withTimeout(new Promise((resolve) => setTimeout(() => resolve("late"), 25)), 1, "fallback"),
+ "fallback",
+);
+
function captureError(fn) {
try {
fn();
diff --git a/web/strategy-switch-console/index.html b/web/strategy-switch-console/index.html
index e209ba8..20a33f5 100644
--- a/web/strategy-switch-console/index.html
+++ b/web/strategy-switch-console/index.html
@@ -746,9 +746,11 @@
切换摘要
summary: "当前配置状态",
copySummary: "复制状态",
loginToRun: "登录后切换",
+ loadingConfig: "读取配置中",
configureAccounts: "配置账号后切换",
runSwitch: "一键切换",
readonlyNote: "登录后才可执行切换。",
+ loadingConfigNote: "正在读取账号配置和当前状态。",
missingConfigNote: "账号配置未加载,暂时不能执行。",
readyNote: "点击后会触发 workflow,并同步目标平台服务。",
invalidStrategyNote: "当前账号没有可执行策略,暂时不能切换。",
@@ -797,9 +799,11 @@ 切换摘要
summary: "Current Config",
copySummary: "Copy state",
loginToRun: "Sign in to switch",
+ loadingConfig: "Loading config",
configureAccounts: "Configure accounts",
runSwitch: "Switch now",
readonlyNote: "Sign in to switch.",
+ loadingConfigNote: "Reading account config and current state.",
missingConfigNote: "Account config is not loaded, so switching is disabled.",
readyNote: "This dispatches the workflow and syncs the target platform service.",
invalidStrategyNote: "This account has no runnable strategy, so switching is disabled.",
@@ -1108,7 +1112,9 @@ 切换摘要
const currentProfile = currentStrategyForAccount(state.selected, account);
const currentMode = normalizeExecutionMode(currentEntry?.execution_mode, currentEntry?.dry_run_only);
const source = currentEntry?.source
- || (state.configSource === "private" ? t("accountConfigLoaded") : t("publicPreview"));
+ || (state.configSource === "loading"
+ ? t("loadingConfig")
+ : (state.configSource === "private" ? t("accountConfigLoaded") : t("publicPreview")));
return [
[t("repository"), repositories[state.selected]],
[t("selectedAccount"), account.label],
@@ -1242,16 +1248,19 @@ 切换摘要
const dispatch = el("dispatch-button");
const hasPrivateAccounts = state.configSource === "private";
+ const loadingConfig = state.configSource === "loading";
const hasValidStrategy = hasValidStrategySelection();
- dispatch.disabled = !state.auth.allowed || !hasPrivateAccounts || !hasValidStrategy;
+ dispatch.disabled = !state.auth.allowed || loadingConfig || !hasPrivateAccounts || !hasValidStrategy;
dispatch.textContent = state.auth.allowed
- ? (hasPrivateAccounts ? t("runSwitch") : t("configureAccounts"))
+ ? (loadingConfig ? t("loadingConfig") : (hasPrivateAccounts ? t("runSwitch") : t("configureAccounts")))
: t("loginToRun");
const note = el("action-note");
note.textContent = state.auth.allowed
- ? (hasPrivateAccounts ? (hasValidStrategy ? t("readyNote") : t("invalidStrategyNote")) : t("missingConfigNote"))
+ ? (loadingConfig
+ ? t("loadingConfigNote")
+ : (hasPrivateAccounts ? (hasValidStrategy ? t("readyNote") : t("invalidStrategyNote")) : t("missingConfigNote")))
: t("readonlyNote");
- note.classList.toggle("warning", state.auth.allowed && (!hasPrivateAccounts || !hasValidStrategy));
+ note.classList.toggle("warning", state.auth.allowed && !loadingConfig && (!hasPrivateAccounts || !hasValidStrategy));
}
function render() {
@@ -1296,6 +1305,8 @@ 切换摘要
async function refreshConfig() {
if (!state.auth.available || !state.auth.allowed) return;
+ state.configSource = "loading";
+ render();
try {
const response = await fetch("/api/config", { cache: "no-store" });
if (!response.ok) throw new Error("no config");
diff --git a/web/strategy-switch-console/page_asset.js b/web/strategy-switch-console/page_asset.js
index 58773ce..e68cea5 100644
--- a/web/strategy-switch-console/page_asset.js
+++ b/web/strategy-switch-console/page_asset.js
@@ -1,2 +1,2 @@
// Generated by scripts/sync_strategy_switch_page_asset.py; do not edit by hand.
-export const PAGE_HTML = "\n\n\n \n \n \n QuantRuntimeSettings Strategy Switch \n \n\n\n \n \n
策略切换 \n
选平台、目标账号和策略,一次执行完成切换。
\n
\n \n \n\n \n \n\n \n \n
\n 当前平台 \n \n
\n\n
\n\n
\n
登录后切换 \n
登录后才可执行切换。
\n
\n
\n
\n\n \n \n
切换摘要 \n live \n \n \n \n 复制摘要 \n
\n \n \n \n\n \n\n\n";
+export const PAGE_HTML = "\n\n\n \n \n \n QuantRuntimeSettings Strategy Switch \n \n\n\n \n \n
策略切换 \n
选平台、目标账号和策略,一次执行完成切换。
\n
\n \n \n\n \n \n\n \n \n
\n 当前平台 \n \n
\n\n
\n\n
\n
登录后切换 \n
登录后才可执行切换。
\n
\n
\n
\n\n \n \n
切换摘要 \n live \n \n \n \n 复制摘要 \n
\n \n \n \n\n \n\n\n";
diff --git a/web/strategy-switch-console/worker.js b/web/strategy-switch-console/worker.js
index ebb0ba0..71a94e4 100644
--- a/web/strategy-switch-console/worker.js
+++ b/web/strategy-switch-console/worker.js
@@ -11,6 +11,7 @@ const ACCOUNT_OPTIONS_KEY = "account_options";
const STRATEGY_PROFILES_KEY = "strategy_profiles";
const AUDIT_LOG_KEY = "audit_log";
const AUDIT_LOG_LIMIT = 50;
+const CURRENT_STRATEGIES_TIMEOUT_MS = 3500;
const SUPPORTED_PLATFORMS = ["longbridge", "ibkr", "schwab", "firstrade"];
const SUPPORTED_STRATEGY_DOMAINS = ["us_equity", "hk_equity"];
@@ -452,11 +453,10 @@ async function configPayload(request, env) {
if (!session?.allowed) return { accountOptions: null };
const accountConfig = await loadAccountOptionsConfig(env);
const strategyProfiles = await loadStrategyProfilesConfig(env);
- const currentStrategies = await loadCurrentStrategies(accountConfig.options, env);
return {
accountOptions: accountConfig.options,
strategyProfiles,
- currentStrategies,
+ currentStrategies: await loadCurrentStrategiesSafely(accountConfig.options, env),
};
}
@@ -503,6 +503,26 @@ async function loadCurrentStrategies(accountOptions, env) {
return currentStrategies;
}
+async function loadCurrentStrategiesSafely(accountOptions, env) {
+ try {
+ return await withTimeout(
+ loadCurrentStrategies(accountOptions, env),
+ CURRENT_STRATEGIES_TIMEOUT_MS,
+ {},
+ );
+ } catch {
+ return {};
+ }
+}
+
+function withTimeout(promise, timeoutMs, fallback) {
+ let timeoutId;
+ const timeout = new Promise((resolve) => {
+ timeoutId = setTimeout(() => resolve(fallback), timeoutMs);
+ });
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timeoutId));
+}
+
async function resolveCurrentStrategyForAccount({ platform, option, optionsCount, repository, readVariable }) {
const serviceTargetsValue = await readVariable(repository, "repository", "", "CLOUD_RUN_SERVICE_TARGETS_JSON");
const serviceTarget = runtimeTargetFromServiceTargets(serviceTargetsValue, platform, option);
@@ -1488,4 +1508,5 @@ export const __test = {
requireSameOrigin,
responseHeaders,
supportedDomainsForAccount,
+ withTimeout,
};