fix: make full_margin env save/restart reliable (no empty numeric wipe).
EOF Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -215,6 +215,21 @@
|
|||||||
});
|
});
|
||||||
const cur = (field.current || field.default || "false").toLowerCase();
|
const cur = (field.current || field.default || "false").toLowerCase();
|
||||||
input.value = cur === "true" || cur === "1" ? "true" : "false";
|
input.value = cur === "true" || cur === "1" ? "true" : "false";
|
||||||
|
} else if (field.type === "enum" && Array.isArray(field.options) && field.options.length) {
|
||||||
|
input = document.createElement("select");
|
||||||
|
input.id = "env-f-" + field.key;
|
||||||
|
field.options.forEach((opt) => {
|
||||||
|
const o = document.createElement("option");
|
||||||
|
const v = typeof opt === "string" ? opt : String(opt.value || "");
|
||||||
|
o.value = v;
|
||||||
|
o.textContent = typeof opt === "string" ? opt : String(opt.label || opt.value || "");
|
||||||
|
input.appendChild(o);
|
||||||
|
});
|
||||||
|
const cur = String(field.current || field.default || "");
|
||||||
|
input.value = cur;
|
||||||
|
if (![...input.options].some((o) => o.value === cur) && input.options.length) {
|
||||||
|
input.selectedIndex = 0;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
input = document.createElement("input");
|
input = document.createElement("input");
|
||||||
input.id = "env-f-" + field.key;
|
input.id = "env-f-" + field.key;
|
||||||
|
|||||||
Vendored
+48
-1
@@ -103,6 +103,23 @@ SENSITIVE_EXACT = frozenset({
|
|||||||
SENSITIVE_SUBSTR = ("_SECRET", "_PASSPHRASE", "_API_KEY", "_PASSWORD")
|
SENSITIVE_SUBSTR = ("_SECRET", "_PASSPHRASE", "_API_KEY", "_PASSWORD")
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_position_sizing_mode(raw: str) -> str:
|
||||||
|
"""把中文/别名规范成 risk | full_margin。"""
|
||||||
|
s = (raw or "").strip().lower().replace("-", "_").replace(" ", "")
|
||||||
|
if s in ("risk", "以损", "以损定仓", "riskpercent", "risk_percent"):
|
||||||
|
return "risk"
|
||||||
|
if s in (
|
||||||
|
"full_margin",
|
||||||
|
"fullmargin",
|
||||||
|
"full",
|
||||||
|
"全仓",
|
||||||
|
"全仓杠杆",
|
||||||
|
"margin",
|
||||||
|
):
|
||||||
|
return "full_margin"
|
||||||
|
return (raw or "").strip()
|
||||||
|
|
||||||
|
|
||||||
def _is_sensitive(key: str) -> bool:
|
def _is_sensitive(key: str) -> bool:
|
||||||
if key in SENSITIVE_EXACT:
|
if key in SENSITIVE_EXACT:
|
||||||
return True
|
return True
|
||||||
@@ -275,17 +292,47 @@ def validate_env_updates(groups: list[dict], updates: dict[str, str]) -> tuple[d
|
|||||||
val = str(value).strip()
|
val = str(value).strip()
|
||||||
if allowed[key].get("sensitive") and (val == "" or (val.startswith("****") and len(val) <= 8)):
|
if allowed[key].get("sensitive") and (val == "" or (val.startswith("****") and len(val) <= 8)):
|
||||||
continue
|
continue
|
||||||
|
# 数字项留空不覆盖已有 .env,避免 RISK_PERCENT= 导致重启崩掉
|
||||||
|
ftype = allowed[key].get("type")
|
||||||
|
if val == "" and ftype in ("float", "int"):
|
||||||
|
continue
|
||||||
|
if val == "" and key in (
|
||||||
|
"POSITION_SIZING_MODE",
|
||||||
|
"TRADE_DIRECTION",
|
||||||
|
"RISK_PERCENT",
|
||||||
|
"FULL_MARGIN_BUFFER_RATIO",
|
||||||
|
"BTC_LEVERAGE",
|
||||||
|
"ALT_LEVERAGE",
|
||||||
|
"MAX_ACTIVE_POSITIONS",
|
||||||
|
):
|
||||||
|
continue
|
||||||
# API Key 被密码管理器/自动填充成登录密码时通常很短;OKX Key 一般为 36 位
|
# API Key 被密码管理器/自动填充成登录密码时通常很短;OKX Key 一般为 36 位
|
||||||
if key.endswith("_API_KEY") and 0 < len(val) < 16:
|
if key.endswith("_API_KEY") and 0 < len(val) < 16:
|
||||||
errors.append(f"{key} 长度异常,疑似自动填充;留空则不修改已有密钥")
|
errors.append(f"{key} 长度异常,疑似自动填充;留空则不修改已有密钥")
|
||||||
continue
|
continue
|
||||||
ftype = allowed[key].get("type")
|
if key == "POSITION_SIZING_MODE":
|
||||||
|
val = normalize_position_sizing_mode(val)
|
||||||
|
if val not in ("risk", "full_margin"):
|
||||||
|
errors.append("计仓模式须为 risk(以损定仓)或 full_margin(全仓杠杆)")
|
||||||
|
continue
|
||||||
if ftype == "bool":
|
if ftype == "bool":
|
||||||
low = val.lower()
|
low = val.lower()
|
||||||
if low not in ("true", "false", "1", "0", "yes", "no", "on", "off"):
|
if low not in ("true", "false", "1", "0", "yes", "no", "on", "off"):
|
||||||
errors.append(f"{key} 须为 true/false")
|
errors.append(f"{key} 须为 true/false")
|
||||||
continue
|
continue
|
||||||
val = "true" if low in ("true", "1", "yes", "on") else "false"
|
val = "true" if low in ("true", "1", "yes", "on") else "false"
|
||||||
|
elif ftype == "float" and val != "":
|
||||||
|
try:
|
||||||
|
float(val)
|
||||||
|
except ValueError:
|
||||||
|
errors.append(f"{key} 须为数字")
|
||||||
|
continue
|
||||||
|
elif ftype == "int" and val != "":
|
||||||
|
try:
|
||||||
|
int(val)
|
||||||
|
except ValueError:
|
||||||
|
errors.append(f"{key} 须为整数")
|
||||||
|
continue
|
||||||
clean[key] = val
|
clean[key] = val
|
||||||
return clean, errors
|
return clean, errors
|
||||||
|
|
||||||
|
|||||||
Vendored
+26
-3
@@ -11,6 +11,7 @@ from lib.env.env_schema import (
|
|||||||
_is_sensitive,
|
_is_sensitive,
|
||||||
_mask_value,
|
_mask_value,
|
||||||
_restart_required,
|
_restart_required,
|
||||||
|
normalize_position_sizing_mode,
|
||||||
parse_env_example_schema,
|
parse_env_example_schema,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ _SHARED_SECTIONS: list[dict[str, Any]] = [
|
|||||||
{
|
{
|
||||||
"title": "交易执行",
|
"title": "交易执行",
|
||||||
"fields": [
|
"fields": [
|
||||||
("POSITION_SIZING_MODE", "计仓模式", "risk=以损定仓,full_margin=全仓杠杆"),
|
("POSITION_SIZING_MODE", "计仓模式", "下拉选择:以损定仓 / 全仓杠杆(改后需保存并重启)"),
|
||||||
("RISK_PERCENT", "以损定仓风险%", "单笔风险占资金比例"),
|
("RISK_PERCENT", "以损定仓风险%", "单笔风险占资金比例"),
|
||||||
("FULL_MARGIN_BUFFER_RATIO", "全仓资金缓冲比例", "如 0.98"),
|
("FULL_MARGIN_BUFFER_RATIO", "全仓资金缓冲比例", "如 0.98"),
|
||||||
("BTC_LEVERAGE", "BTC 默认杠杆", ""),
|
("BTC_LEVERAGE", "BTC 默认杠杆", ""),
|
||||||
@@ -149,12 +150,25 @@ _RUNTIME_ENV_DEFAULTS: dict[str, str] = {
|
|||||||
"RISK_COOLING_HOURS_MANUAL_JOURNAL": "1",
|
"RISK_COOLING_HOURS_MANUAL_JOURNAL": "1",
|
||||||
"RISK_MANUAL_CLOSE_DAILY_LIMIT": "2",
|
"RISK_MANUAL_CLOSE_DAILY_LIMIT": "2",
|
||||||
"RISK_MOOD_ISSUES_DAILY_FREEZE": "true",
|
"RISK_MOOD_ISSUES_DAILY_FREEZE": "true",
|
||||||
|
"RISK_PERCENT": "2",
|
||||||
|
"FULL_MARGIN_BUFFER_RATIO": "0.98",
|
||||||
|
"POSITION_SIZING_MODE": "risk",
|
||||||
|
"BTC_LEVERAGE": "10",
|
||||||
|
"ALT_LEVERAGE": "5",
|
||||||
|
"MAX_ACTIVE_POSITIONS": "1",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_POSITION_SIZING_OPTIONS: list[dict[str, str]] = [
|
||||||
|
{"value": "risk", "label": "以损定仓 (risk)"},
|
||||||
|
{"value": "full_margin", "label": "全仓杠杆 (full_margin)"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _effective_env_value(key: str, file_values: dict[str, str], schema_default: str = "") -> str:
|
def _effective_env_value(key: str, file_values: dict[str, str], schema_default: str = "") -> str:
|
||||||
if key in file_values:
|
if key in file_values:
|
||||||
return file_values[key]
|
file_val = str(file_values.get(key) or "").strip()
|
||||||
|
if file_val != "":
|
||||||
|
return file_val
|
||||||
runtime = os.getenv(key)
|
runtime = os.getenv(key)
|
||||||
if runtime is not None and str(runtime).strip() != "":
|
if runtime is not None and str(runtime).strip() != "":
|
||||||
return str(runtime).strip()
|
return str(runtime).strip()
|
||||||
@@ -183,7 +197,7 @@ def _build_field(
|
|||||||
val = _effective_env_value(key, values, schema_default)
|
val = _effective_env_value(key, values, schema_default)
|
||||||
masked = _mask_value(key, val)
|
masked = _mask_value(key, val)
|
||||||
ftype = meta.get("type") or _field_type(key, val or schema_default)
|
ftype = meta.get("type") or _field_type(key, val or schema_default)
|
||||||
return {
|
out: dict[str, Any] = {
|
||||||
"key": key,
|
"key": key,
|
||||||
"label": label,
|
"label": label,
|
||||||
"note": note or meta.get("note") or "",
|
"note": note or meta.get("note") or "",
|
||||||
@@ -197,6 +211,15 @@ def _build_field(
|
|||||||
"tail": masked.get("tail") or "",
|
"tail": masked.get("tail") or "",
|
||||||
"has_value": masked["has_value"],
|
"has_value": masked["has_value"],
|
||||||
}
|
}
|
||||||
|
if key == "POSITION_SIZING_MODE":
|
||||||
|
out["type"] = "enum"
|
||||||
|
out["options"] = list(_POSITION_SIZING_OPTIONS)
|
||||||
|
cur = normalize_position_sizing_mode(out["current"] or out["default"] or "risk")
|
||||||
|
if cur not in ("risk", "full_margin"):
|
||||||
|
cur = "risk"
|
||||||
|
out["current"] = cur
|
||||||
|
out["default"] = cur
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def ui_sections_for_exchange(exchange_key: str) -> list[dict[str, Any]]:
|
def ui_sections_for_exchange(exchange_key: str) -> list[dict[str, Any]]:
|
||||||
|
|||||||
@@ -23,8 +23,15 @@ _POST_LICENSE_TARGET = "/login"
|
|||||||
|
|
||||||
|
|
||||||
def _license_public_path(path: str) -> bool:
|
def _license_public_path(path: str) -> bool:
|
||||||
"""未授权时仍可访问的路径(授权页 / 接口 / 静态资源)。"""
|
"""未授权时仍可访问的路径(授权页 / 接口 / 静态资源 / 重启探活)。"""
|
||||||
if path in ("/license", "/api/license/status", "/api/license/redeem", "/api/license/validate", "/health"):
|
if path in (
|
||||||
|
"/license",
|
||||||
|
"/api/license/status",
|
||||||
|
"/api/license/redeem",
|
||||||
|
"/api/license/validate",
|
||||||
|
"/health",
|
||||||
|
"/api/admin/health",
|
||||||
|
):
|
||||||
return True
|
return True
|
||||||
if path.startswith("/static/"):
|
if path.startswith("/static/"):
|
||||||
return True
|
return True
|
||||||
@@ -33,6 +40,7 @@ def _license_public_path(path: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _license_manage_requested() -> bool:
|
def _license_manage_requested() -> bool:
|
||||||
"""已授权时默认禁止进入 /license;续费/换机用 ?renew=1。"""
|
"""已授权时默认禁止进入 /license;续费/换机用 ?renew=1。"""
|
||||||
return (request.args.get("renew") or request.args.get("manage") or "").strip().lower() in (
|
return (request.args.get("renew") or request.args.get("manage") or "").strip().lower() in (
|
||||||
|
|||||||
Reference in New Issue
Block a user