diff --git a/web/strategy-switch-console/README.md b/web/strategy-switch-console/README.md
index 132d519..bd62fe9 100644
--- a/web/strategy-switch-console/README.md
+++ b/web/strategy-switch-console/README.md
@@ -108,6 +108,21 @@ Each account item supports:
The Worker validates dispatch inputs against this config. Keep only routing metadata here. Do not store broker passwords, tokens, or API keys in this config.
+For signed-in users, `/api/config` also reads the target repositories' current GitHub Variables. It prefers account-specific `CLOUD_RUN_SERVICE_TARGETS_JSON`, then matching `RUNTIME_TARGET_JSON.strategy_profile`, then `STRATEGY_PROFILE`; if none can be read safely, the page falls back to `default_strategy_profile`.
+
+## Strategy Profile Alignment
+
+Treat `strategy_profile` as the canonical strategy id across the switch console, runtime settings, and platform repositories.
+
+When adding or renaming a strategy profile:
+
+- Add the profile id and display label to `web/strategy-switch-console/index.html`.
+- Set each affected account's `default_strategy_profile` in `account-options.example.json` and the deployed KV account config.
+- Make sure the platform repository's current `RUNTIME_TARGET_JSON.strategy_profile` or account-specific `CLOUD_RUN_SERVICE_TARGETS_JSON` uses the same id.
+- Use lower-case ids with letters, numbers, dot, underscore, dash, or equals only. Do not encode account names or secrets in profile ids.
+
+The console can display a dynamically read unknown profile, but the profile should still be added to the catalog so the UI and docs stay aligned.
+
## GitHub OAuth App
Create a GitHub OAuth App:
diff --git a/web/strategy-switch-console/README.zh-CN.md b/web/strategy-switch-console/README.zh-CN.md
index 7c866c0..d675d9f 100644
--- a/web/strategy-switch-console/README.zh-CN.md
+++ b/web/strategy-switch-console/README.zh-CN.md
@@ -115,6 +115,21 @@ wrangler secret put STRATEGY_SWITCH_ACCOUNT_OPTIONS_JSON < /tmp/strategy-switch-
Worker 会校验 dispatch 参数必须匹配这里的某个账号项。只放路由信息,不放 broker 密码、token、API key。
+登录用户访问 `/api/config` 时,Worker 还会读取目标平台仓库的当前 GitHub Variables。读取优先级是账号匹配的 `CLOUD_RUN_SERVICE_TARGETS_JSON`、匹配的 `RUNTIME_TARGET_JSON.strategy_profile`、`STRATEGY_PROFILE`;都读不到时,页面才回退到 `default_strategy_profile`。
+
+## 策略 Profile 对齐规范
+
+`strategy_profile` 是切换页、runtime settings 和各平台仓库之间的统一策略 ID。
+
+新增或重命名策略 profile 时,需要同时做这些事:
+
+- 在 `web/strategy-switch-console/index.html` 增加 profile id 和显示名称。
+- 在 `account-options.example.json` 和已部署的 KV 账号配置里更新对应账号的 `default_strategy_profile`。
+- 确认平台仓库当前的 `RUNTIME_TARGET_JSON.strategy_profile` 或账号级 `CLOUD_RUN_SERVICE_TARGETS_JSON` 使用同一个 id。
+- profile id 只使用小写字母、数字、点、下划线、短横线或等号。不要把账号名、密码、token、密钥信息写进 profile id。
+
+切换页可以临时显示动态读取到但未登记的 profile,但后续仍应补进策略目录,保持 UI 和文档一致。
+
## GitHub OAuth App
创建 GitHub OAuth App:
diff --git a/web/strategy-switch-console/account-options.example.json b/web/strategy-switch-console/account-options.example.json
index 56138af..048974d 100644
--- a/web/strategy-switch-console/account-options.example.json
+++ b/web/strategy-switch-console/account-options.example.json
@@ -5,7 +5,7 @@
"label": "hk",
"target_name": "hk",
"account_selector": "HK",
- "default_strategy_profile": "tqqq_growth_income"
+ "default_strategy_profile": "hk_low_vol_dividend_quality_snapshot"
},
{
"key": "sg",
@@ -19,7 +19,7 @@
"label": "paper",
"target_name": "paper",
"account_selector": "PAPER",
- "default_strategy_profile": "tqqq_growth_income"
+ "default_strategy_profile": "mega_cap_leader_rotation_top50_balanced"
}
],
"ibkr": [
@@ -59,7 +59,7 @@
"key": "default",
"label": "default",
"target_name": "default",
- "default_strategy_profile": "tqqq_growth_income"
+ "default_strategy_profile": "soxl_soxx_trend_income"
}
],
"firstrade": [
@@ -67,7 +67,7 @@
"key": "default",
"label": "default",
"target_name": "default",
- "default_strategy_profile": "tqqq_growth_income"
+ "default_strategy_profile": "mega_cap_leader_rotation_top50_balanced"
}
]
}
diff --git a/web/strategy-switch-console/index.html b/web/strategy-switch-console/index.html
index 11ab6be..a9cc74d 100644
--- a/web/strategy-switch-console/index.html
+++ b/web/strategy-switch-console/index.html
@@ -612,6 +612,7 @@
切换摘要
"tqqq_growth_income",
"soxl_soxx_trend_income",
"nasdaq_sp500_smart_dca",
+ "hk_low_vol_dividend_quality_snapshot",
"global_etf_rotation",
"russell_1000_multi_factor_defensive",
"mega_cap_leader_rotation_top50_balanced",
@@ -621,6 +622,7 @@ 切换摘要
tqqq_growth_income: "TQQQ Growth Income",
soxl_soxx_trend_income: "SOXL/SOXX Trend Income",
nasdaq_sp500_smart_dca: "Nasdaq/S&P 500 Smart DCA",
+ hk_low_vol_dividend_quality_snapshot: "HK Low Vol Dividend Quality",
global_etf_rotation: "Global ETF Rotation",
russell_1000_multi_factor_defensive: "Russell 1000 Defensive",
mega_cap_leader_rotation_top50_balanced: "Mega Cap Top 50",
@@ -628,9 +630,9 @@ 切换摘要
const defaultAccountOptions = {
longbridge: [
- { key: "hk", label: "hk", target_name: "hk", account_selector: "HK", default_strategy_profile: "tqqq_growth_income" },
+ { key: "hk", label: "hk", target_name: "hk", account_selector: "HK", default_strategy_profile: "hk_low_vol_dividend_quality_snapshot" },
{ key: "sg", label: "sg", target_name: "sg", account_selector: "SG", default_strategy_profile: "tqqq_growth_income" },
- { key: "paper", label: "paper", target_name: "paper", account_selector: "PAPER", default_strategy_profile: "tqqq_growth_income" },
+ { key: "paper", label: "paper", target_name: "paper", account_selector: "PAPER", default_strategy_profile: "mega_cap_leader_rotation_top50_balanced" },
],
ibkr: [
{
@@ -665,10 +667,10 @@ 切换摘要
},
],
schwab: [
- { key: "default", label: "default", target_name: "default", default_strategy_profile: "tqqq_growth_income" },
+ { key: "default", label: "default", target_name: "default", default_strategy_profile: "soxl_soxx_trend_income" },
],
firstrade: [
- { key: "default", label: "default", target_name: "default", default_strategy_profile: "tqqq_growth_income" },
+ { key: "default", label: "default", target_name: "default", default_strategy_profile: "mega_cap_leader_rotation_top50_balanced" },
],
};
@@ -760,6 +762,7 @@ 切换摘要
lang: initialLang,
auth: { available: false, allowed: false, admin: false, login: null },
accountOptions: clone(defaultAccountOptions),
+ currentStrategies: {},
configSource: "default",
forms: {
longbridge: { accountKey: "hk", strategy: "tqqq_growth_income", executionMode: "live" },
@@ -784,9 +787,50 @@ 切换摘要
return options.find((option) => option.key === form.accountKey) || options[0];
}
- function defaultStrategyForAccount(account, fallback = "tqqq_growth_income") {
- const profile = String(account?.default_strategy_profile || account?.strategy_profile || "").trim();
- if (strategyOptions.includes(profile)) return profile;
+ function cleanStrategyProfile(value) {
+ const profile = String(value || "").trim();
+ return /^[a-z0-9._=-]{1,120}$/.test(profile) ? profile : "";
+ }
+
+ function strategyChoices() {
+ const choices = [...strategyOptions];
+ const addChoice = (value) => {
+ const profile = cleanStrategyProfile(value);
+ if (profile && !choices.includes(profile)) choices.push(profile);
+ };
+ for (const platform of Object.keys(platformMeta)) {
+ for (const account of optionsFor(platform)) {
+ addChoice(account.default_strategy_profile || account.strategy_profile);
+ }
+ for (const entry of Object.values(state.currentStrategies[platform] || {})) {
+ addChoice(entry.strategy_profile);
+ }
+ addChoice(state.forms[platform].strategy);
+ }
+ return choices;
+ }
+
+ function strategyLabel(profile) {
+ return strategyLabels[profile] || profile;
+ }
+
+ function currentStrategyForAccount(platform, account) {
+ const byPlatform = state.currentStrategies[platform] || {};
+ const keys = [account?.key, account?.target_name, account?.label]
+ .filter(Boolean)
+ .map((value) => String(value));
+ for (const key of keys) {
+ const profile = cleanStrategyProfile(byPlatform[key]?.strategy_profile);
+ if (profile) return profile;
+ }
+ return "";
+ }
+
+ function defaultStrategyForAccount(platform, account, fallback = "tqqq_growth_income") {
+ const currentProfile = currentStrategyForAccount(platform, account);
+ if (currentProfile) return currentProfile;
+ const profile = cleanStrategyProfile(account?.default_strategy_profile || account?.strategy_profile);
+ if (profile) return profile;
const hint = [
account?.key,
account?.label,
@@ -801,12 +845,18 @@ 切换摘要
return fallback;
}
+ function syncStrategyForAccount(platform) {
+ const account = selectedAccount(platform);
+ if (!account) return;
+ state.forms[platform].strategy = defaultStrategyForAccount(platform, account, state.forms[platform].strategy);
+ }
+
function ensureAccountSelection(platform) {
const options = optionsFor(platform);
if (!options.length) return;
if (!options.some((option) => option.key === state.forms[platform].accountKey)) {
state.forms[platform].accountKey = options[0].key;
- state.forms[platform].strategy = defaultStrategyForAccount(options[0], state.forms[platform].strategy);
+ state.forms[platform].strategy = defaultStrategyForAccount(platform, options[0], state.forms[platform].strategy);
}
}
@@ -859,7 +909,7 @@ 切换摘要
return [
[t("repository"), repositories[state.selected]],
[t("selectedAccount"), account.label],
- [t("selectedStrategy"), strategyLabels[inputs.strategy_profile] || inputs.strategy_profile],
+ [t("selectedStrategy"), strategyLabel(inputs.strategy_profile)],
[t("selectedMode"), inputs.execution_mode],
[t("target"), inputs.target_name],
[t("accountSelector"), inputs.account_selector || "auto"],
@@ -894,7 +944,7 @@ 切换摘要
${meta.label}
${escapeHtml(account.label)}
- ${escapeHtml(strategyLabels[form.strategy] || form.strategy)}
+ ${escapeHtml(strategyLabel(form.strategy))}
`;
strip.appendChild(button);
@@ -919,8 +969,8 @@ 切换摘要
: ``;
el("account-meta").textContent = accounts.length ? accountMetaText(platform) : "";
- strategySelect.innerHTML = strategyOptions.map((strategy) => (
- ``
+ strategySelect.innerHTML = strategyChoices().map((strategy) => (
+ ``
)).join("");
document.querySelectorAll("[data-mode]").forEach((button) => {
@@ -1002,12 +1052,17 @@ 切换摘要
const payload = await response.json();
if (payload.accountOptions) {
state.accountOptions = normalizeAccountOptions(payload.accountOptions);
+ state.currentStrategies = normalizeCurrentStrategies(payload.currentStrategies || {});
state.configSource = "private";
- for (const platform of Object.keys(platformMeta)) ensureAccountSelection(platform);
+ for (const platform of Object.keys(platformMeta)) {
+ ensureAccountSelection(platform);
+ syncStrategyForAccount(platform);
+ }
render();
}
} catch {
state.configSource = "default";
+ state.currentStrategies = {};
}
}
@@ -1034,6 +1089,24 @@ 切换摘要
return normalized;
}
+ function normalizeCurrentStrategies(raw) {
+ const normalized = {};
+ for (const platform of Object.keys(platformMeta)) {
+ if (!raw[platform] || typeof raw[platform] !== "object" || Array.isArray(raw[platform])) continue;
+ normalized[platform] = {};
+ for (const [key, entry] of Object.entries(raw[platform])) {
+ const profile = cleanStrategyProfile(entry?.strategy_profile);
+ if (!profile) continue;
+ normalized[platform][String(key)] = {
+ strategy_profile: profile,
+ source: entry?.source ? String(entry.source) : "",
+ };
+ }
+ if (!Object.keys(normalized[platform]).length) delete normalized[platform];
+ }
+ return normalized;
+ }
+
async function dispatchSwitch() {
if (!state.auth.allowed) return;
el("toast").textContent = t("dispatching");
@@ -1083,10 +1156,7 @@ 切换摘要
el("account-select").addEventListener("change", () => {
state.forms[state.selected].accountKey = el("account-select").value;
- state.forms[state.selected].strategy = defaultStrategyForAccount(
- selectedAccount(),
- state.forms[state.selected].strategy,
- );
+ syncStrategyForAccount(state.selected);
render();
});
diff --git a/web/strategy-switch-console/page_asset.js b/web/strategy-switch-console/page_asset.js
index c8bbd83..fc1dba6 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\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\n \n\n\n";
diff --git a/web/strategy-switch-console/worker.js b/web/strategy-switch-console/worker.js
index c3ad332..b12d88b 100644
--- a/web/strategy-switch-console/worker.js
+++ b/web/strategy-switch-console/worker.js
@@ -11,6 +11,18 @@ const AUDIT_LOG_KEY = "audit_log";
const AUDIT_LOG_LIMIT = 50;
const SUPPORTED_PLATFORMS = ["longbridge", "ibkr", "schwab", "firstrade"];
+const PLATFORM_REPOSITORIES = {
+ longbridge: "QuantStrategyLab/LongBridgePlatform",
+ ibkr: "QuantStrategyLab/InteractiveBrokersPlatform",
+ schwab: "QuantStrategyLab/CharlesSchwabPlatform",
+ firstrade: "QuantStrategyLab/FirstradePlatform",
+};
+const DEFAULT_VARIABLE_SCOPE = {
+ longbridge: "environment",
+ ibkr: "repository",
+ schwab: "repository",
+ firstrade: "repository",
+};
export default {
async fetch(request, env) {
@@ -411,11 +423,92 @@ async function configPayload(request, env) {
const session = await readSession(request, env);
if (!session?.allowed) return { accountOptions: null };
const accountConfig = await loadAccountOptionsConfig(env);
+ const currentStrategies = await loadCurrentStrategies(accountConfig.options, env);
return {
accountOptions: accountConfig.options,
+ currentStrategies,
};
}
+async function loadCurrentStrategies(accountOptions, env) {
+ const token = env.RUNTIME_SETTINGS_DISPATCH_TOKEN;
+ if (!token || !accountOptions) return {};
+
+ const variableCache = new Map();
+ const readVariable = (repository, scope, githubEnvironment, name) => {
+ const cacheKey = [repository, scope, githubEnvironment || "", name].join("|");
+ if (!variableCache.has(cacheKey)) {
+ variableCache.set(cacheKey, fetchGithubVariable(token, repository, scope, githubEnvironment, name));
+ }
+ return variableCache.get(cacheKey);
+ };
+
+ const currentStrategies = {};
+ for (const platform of SUPPORTED_PLATFORMS) {
+ const options = Array.isArray(accountOptions[platform]) ? accountOptions[platform] : [];
+ if (!options.length) continue;
+ const repository = PLATFORM_REPOSITORIES[platform];
+ if (!repository) continue;
+ const platformStrategies = {};
+
+ for (const option of options) {
+ const current = await resolveCurrentStrategyForAccount({
+ platform,
+ option,
+ optionsCount: options.length,
+ repository,
+ readVariable,
+ });
+ if (current) platformStrategies[option.key] = current;
+ }
+
+ if (Object.keys(platformStrategies).length) currentStrategies[platform] = platformStrategies;
+ }
+ return currentStrategies;
+}
+
+async function resolveCurrentStrategyForAccount({ platform, option, optionsCount, repository, readVariable }) {
+ const serviceTargetsValue = await readVariable(repository, "repository", "", "CLOUD_RUN_SERVICE_TARGETS_JSON");
+ const serviceTargetProfile = strategyFromServiceTargets(serviceTargetsValue, platform, option);
+ if (serviceTargetProfile) {
+ return {
+ strategy_profile: serviceTargetProfile,
+ source: "CLOUD_RUN_SERVICE_TARGETS_JSON",
+ variable_scope: "repository",
+ };
+ }
+
+ const variableScope = resolveVariableScope(platform, option);
+ const githubEnvironment = resolveGithubEnvironment(platform, option, variableScope);
+ const runtimeTargetValue = await readVariable(repository, variableScope, githubEnvironment, "RUNTIME_TARGET_JSON");
+ const runtimeTarget = parseJsonObject(runtimeTargetValue);
+ const runtimeTargetMatches = runtimeTarget && runtimeTargetMatchesAccount(runtimeTarget, platform, option);
+ const runtimeTargetProfile = runtimeTargetMatches ? cleanCurrentStrategy(runtimeTarget.strategy_profile) : "";
+ if (runtimeTargetProfile) {
+ return {
+ strategy_profile: runtimeTargetProfile,
+ source: "RUNTIME_TARGET_JSON",
+ variable_scope: variableScope,
+ github_environment: githubEnvironment || "",
+ };
+ }
+
+ if (variableScope === "environment" || optionsCount <= 1) {
+ const profileValue = await readVariable(repository, variableScope, githubEnvironment, "STRATEGY_PROFILE");
+ const profile = cleanCurrentStrategy(profileValue);
+ if (profile) {
+ return {
+ strategy_profile: profile,
+ source: "STRATEGY_PROFILE",
+ variable_scope: variableScope,
+ github_environment: githubEnvironment || "",
+ };
+ }
+ }
+
+ return null;
+}
+
function logout(request) {
requireSameOrigin(request);
return json({ ok: true }, 200, {
@@ -641,6 +734,148 @@ function requireSameOrigin(request) {
if (origin !== new URL(request.url).origin) throw new Error("cross-origin request rejected");
}
+async function fetchGithubVariable(token, repository, scope, githubEnvironment, name) {
+ const apiUrl = githubVariableUrl(repository, scope, githubEnvironment, name);
+ if (!apiUrl) return "";
+ try {
+ const response = await fetch(apiUrl, {
+ headers: githubHeaders(token),
+ });
+ if (response.status === 404 || response.status === 403) return "";
+ if (!response.ok) return "";
+ const payload = await response.json();
+ return String(payload?.value || "");
+ } catch {
+ return "";
+ }
+}
+
+function githubVariableUrl(repository, scope, githubEnvironment, name) {
+ const [owner, repo] = String(repository || "").split("/");
+ if (!owner || !repo) return "";
+ const base = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
+ const variableName = encodeURIComponent(name);
+ if (scope === "environment") {
+ if (!githubEnvironment) return "";
+ return `${base}/environments/${encodeURIComponent(githubEnvironment)}/variables/${variableName}`;
+ }
+ return `${base}/actions/variables/${variableName}`;
+}
+
+function resolveVariableScope(platform, option) {
+ const configured = String(option?.variable_scope || "").trim();
+ if (configured && configured !== "default") return configured;
+ return DEFAULT_VARIABLE_SCOPE[platform] || "repository";
+}
+
+function resolveGithubEnvironment(platform, option, variableScope) {
+ if (variableScope !== "environment") return "";
+ const configured = String(option?.github_environment || "").trim();
+ if (configured) return configured;
+ const targetName = String(option?.target_name || option?.key || "").trim();
+ if (!targetName) return "";
+ if (platform === "longbridge") return `longbridge-${targetName.toLowerCase()}`;
+ return targetName;
+}
+
+function strategyFromServiceTargets(rawValue, platform, option) {
+ const payload = parseJsonObject(rawValue);
+ const targets = Array.isArray(payload?.targets) ? payload.targets : [];
+ for (const entry of targets) {
+ if (!entry || Array.isArray(entry) || typeof entry !== "object") continue;
+ const runtimeTarget = entry.runtime_target && typeof entry.runtime_target === "object"
+ ? entry.runtime_target
+ : {};
+ if (!runtimeTargetMatchesAccount(runtimeTarget, platform, option, entry)) continue;
+ const profile = cleanCurrentStrategy(runtimeTarget.strategy_profile || entry.strategy_profile);
+ if (profile) return profile;
+ }
+ return "";
+}
+
+function runtimeTargetMatchesAccount(runtimeTarget, platform, option, entry = {}) {
+ const runtimePlatform = String(runtimeTarget?.platform_id || "").trim().toLowerCase();
+ if (runtimePlatform && runtimePlatform !== platform) return false;
+
+ const serviceName = String(option?.service_name || defaultCurrentServiceName(platform, option?.target_name || option?.key) || "");
+ if (serviceName && hasCandidate(serviceName, [
+ runtimeTarget?.service_name,
+ entry?.service,
+ entry?.service_name,
+ ])) return true;
+
+ if (hasCandidate(option?.account_scope, [
+ runtimeTarget?.account_scope,
+ entry?.ACCOUNT_GROUP,
+ entry?.account_scope,
+ ])) return true;
+
+ if (hasCandidate(option?.deployment_selector, [
+ runtimeTarget?.deployment_selector,
+ entry?.deployment_selector,
+ ])) return true;
+
+ const optionSelectors = splitSelectorValues(option?.account_selector);
+ const runtimeSelectors = splitSelectorValues(runtimeTarget?.account_selector || entry?.account_selector);
+ if (optionSelectors.some((value) => runtimeSelectors.includes(value))) return true;
+
+ const targetName = String(option?.target_name || option?.key || "").trim();
+ return Boolean(targetName && hasCandidate(targetName, [
+ runtimeTarget?.target_name,
+ runtimeTarget?.deployment_selector,
+ runtimeTarget?.account_scope,
+ entry?.target_name,
+ ]));
+}
+
+function defaultCurrentServiceName(platform, targetName) {
+ const normalized = String(targetName || "").trim().toLowerCase();
+ if (!normalized) return "";
+ if (platform === "longbridge") return `longbridge-quant-${normalized}-service`;
+ if (platform === "ibkr") return `interactive-brokers-${normalized}-service`;
+ if (platform === "schwab") return "charles-schwab-quant-service";
+ if (platform === "firstrade") return "firstrade-quant-service";
+ return "";
+}
+
+function hasCandidate(expected, candidates) {
+ const normalizedExpected = normalizeMatchValue(expected);
+ if (!normalizedExpected) return false;
+ return candidates.some((candidate) => normalizeMatchValue(candidate) === normalizedExpected);
+}
+
+function splitSelectorValues(value) {
+ if (Array.isArray(value)) return value.map(normalizeMatchValue).filter(Boolean);
+ return String(value || "")
+ .split(/[,\s]+/)
+ .map(normalizeMatchValue)
+ .filter(Boolean);
+}
+
+function normalizeMatchValue(value) {
+ return String(value || "").trim().toLowerCase();
+}
+
+function parseJsonObject(value) {
+ const text = String(value || "").trim();
+ if (!text) return null;
+ for (const candidate of [text, text.replaceAll("\\n", "\n")]) {
+ try {
+ const payload = JSON.parse(candidate);
+ return payload && !Array.isArray(payload) && typeof payload === "object" ? payload : null;
+ } catch {
+ // Try the next representation.
+ }
+ }
+ return null;
+}
+
+function cleanCurrentStrategy(value) {
+ const text = String(value || "").trim().toLowerCase();
+ if (!text || text.length > 120 || !/^[a-z0-9._=-]+$/.test(text)) return "";
+ return text;
+}
+
function cleanCsv(value, field) {
const text = String(value || "").trim();
if (text.length > 300 || !/^[A-Za-z0-9._=,\-\s]+$/.test(text)) {