diff --git a/lib/common/static/instance_settings_prefs.js b/lib/common/static/instance_settings_prefs.js index e43676d..804985b 100644 --- a/lib/common/static/instance_settings_prefs.js +++ b/lib/common/static/instance_settings_prefs.js @@ -416,6 +416,41 @@ } } + function collectSettingsExchangeApiValues() { + const panel = document.getElementById("settings-exchange-api-panel"); + const values = {}; + if (!panel) return values; + panel.querySelectorAll(".env-field-input[data-env-key]").forEach((el) => { + if (el.dataset.envSensitive === "1" && el.dataset.envDirty !== "1") { + return; + } + values[el.dataset.envKey] = el.value; + }); + return values; + } + + async function saveSettingsExchangeApi(restartAfter) { + const status = document.getElementById("settings-exchange-api-status"); + setStatus(status, "保存中…"); + try { + const data = await fetchJson("/api/settings/env", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ values: collectSettingsExchangeApiValues() }), + }); + const needRestart = restartAfter || data.restart_required; + if (needRestart) { + setStatus(status, "已保存,正在重启实例…"); + await restartInstance(); + setStatus(status, "保存并重启完成,请刷新页面"); + } else { + setStatus(status, "已保存(即时生效项已应用)"); + } + } catch (e) { + setStatus(status, e.message || "保存失败", true); + } + } + function installDelegatedHandlers() { if (document.documentElement.dataset.prefsDelegateBound === "1") return; document.documentElement.dataset.prefsDelegateBound = "1"; @@ -445,6 +480,16 @@ if (target.closest("#pwd-save-btn")) { ev.preventDefault(); void savePassword(); + return; + } + if (target.closest("#settings-exchange-api-save")) { + ev.preventDefault(); + void saveSettingsExchangeApi(false); + return; + } + if (target.closest("#settings-exchange-api-save-restart")) { + ev.preventDefault(); + void saveSettingsExchangeApi(true); } }); } @@ -462,6 +507,8 @@ bindClickOnce("env-config-save-restart", () => saveEnvConfig(true)); bindClickOnce("env-config-reload", () => loadEnvConfig(true)); bindClickOnce("pwd-save-btn", savePassword); + bindClickOnce("settings-exchange-api-save", () => saveSettingsExchangeApi(false)); + bindClickOnce("settings-exchange-api-save-restart", () => saveSettingsExchangeApi(true)); } function initPage() { diff --git a/lib/env/env_ui_manifest.py b/lib/env/env_ui_manifest.py index 182dfd0..f5bf5b3 100644 --- a/lib/env/env_ui_manifest.py +++ b/lib/env/env_ui_manifest.py @@ -220,6 +220,27 @@ def ui_allowed_keys(exchange_key: str) -> frozenset[str]: return frozenset(keys) +def build_exchange_api_group( + exchange_key: str, + example_path: str, + env_path: str, +) -> dict[str, Any]: + """系统设置「交易所 API」专用:仅交易所与实盘字段.""" + schema = _schema_field_map(example_path) + values = env_get_all(read_env_lines(env_path)) + ex = (exchange_key or "").strip().lower() + live_fields = _EXCHANGE_LIVE_FIELDS.get(ex, _EXCHANGE_LIVE_FIELDS["okx"]) + fields = [ + _build_field(key, label, note, schema, values) + for key, label, note in live_fields + ] + return { + "title": "交易所 API", + "fields": fields, + "has_restart": any(f.get("restart_required") for f in fields), + } + + def build_env_ui_payload( exchange_key: str, example_path: str, diff --git a/lib/instance/instance_display_prefs_lib.py b/lib/instance/instance_display_prefs_lib.py index f75b985..d0aa105 100644 --- a/lib/instance/instance_display_prefs_lib.py +++ b/lib/instance/instance_display_prefs_lib.py @@ -21,6 +21,7 @@ DEFAULT_INSTANCE_DISPLAY: dict[str, bool] = { "show_settings_transfer": True, "show_settings_export": True, "show_settings_password": True, + "show_settings_exchange_api": True, "show_settings_options_swap": True, "show_settings_options_transfer": True, } @@ -39,6 +40,7 @@ DISPLAY_LABELS: dict[str, str] = { "show_settings_transfer": "资金划转", "show_settings_export": "数据导出", "show_settings_password": "账户密码修改", + "show_settings_exchange_api": "交易所 API", "show_settings_options_swap": "期权币种兑换", "show_settings_options_transfer": "期权资金划转", } @@ -118,6 +120,7 @@ def display_meta_for_ui() -> list[dict[str, Any]]: "show_nav_hedge_plan", ] settings_keys = [ + "show_settings_exchange_api", "show_settings_transfer", "show_settings_export", "show_settings_password", diff --git a/lib/instance/instance_settings_lib.py b/lib/instance/instance_settings_lib.py index eea0f6b..239c33b 100644 --- a/lib/instance/instance_settings_lib.py +++ b/lib/instance/instance_settings_lib.py @@ -187,6 +187,8 @@ def build_settings_tabs(display: dict[str, Any] | None, instance_settings: dict[ disp = display or {} inst = instance_settings or {} tabs: list[dict[str, str]] = [{"key": "nav", "title": "导航显示"}] + if disp.get("show_settings_exchange_api", True): + tabs.append({"key": "exchange_api", "title": "交易所 API"}) if disp.get("show_settings_password", True): tabs.append({"key": "password", "title": "账户密码"}) if inst.get("show_transfer") and disp.get("show_settings_transfer", True): @@ -205,13 +207,21 @@ def settings_page_context(page: str, *, instance_base_dir: str | None = None, ** if p not in ("settings", "risk_policy", "env_config"): return {} display = kwargs.pop("display", None) + exchange_key = str(kwargs.get("exchange_key") or "") ctx: dict[str, Any] = {"instance_settings": build_instance_settings_view(**kwargs)} if p == "settings": ctx["settings_tabs"] = build_settings_tabs(display, ctx["instance_settings"]) + if instance_base_dir: + from lib.env.env_ui_manifest import build_exchange_api_group + + env_path = os.path.join(instance_base_dir, ".env") + example_path = os.path.join(instance_base_dir, ".env.example") + ctx["exchange_api_group"] = build_exchange_api_group( + exchange_key, example_path, env_path + ) if p == "env_config" and instance_base_dir: from lib.env.env_ui_manifest import build_env_ui_payload - exchange_key = str(kwargs.get("exchange_key") or "") env_path = os.path.join(instance_base_dir, ".env") example_path = os.path.join(instance_base_dir, ".env.example") ctx["env_config_groups"] = build_env_ui_payload(exchange_key, example_path, env_path) diff --git a/lib/instance/templates/embed_shell.html b/lib/instance/templates/embed_shell.html index 02df580..85545ca 100644 --- a/lib/instance/templates/embed_shell.html +++ b/lib/instance/templates/embed_shell.html @@ -115,7 +115,7 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj - + diff --git a/lib/instance/templates/exchange_api_settings_panel.html b/lib/instance/templates/exchange_api_settings_panel.html new file mode 100644 index 0000000..f1c0123 --- /dev/null +++ b/lib/instance/templates/exchange_api_settings_panel.html @@ -0,0 +1,63 @@ +{# 系统设置 · 交易所 API(写入实例 .env,与 env 配置同源) #} +

交易所 API

+

配置本所永续 API 与实盘开关,保存写入 .env.含「需重启」项请用「保存并重启」.

+{% if exchange_api_group %} +
+ {% if exchange_api_group.has_restart %} +

本组含需重启项,修改后请点「保存并重启」.

+ {% endif %} +
+ {% for field in exchange_api_group.fields %} +
+ + {% if field.note %} +
{{ field.note }}
+ {% endif %} + {% if field.type == 'bool' %} + + {% elif field.sensitive %} + {% if field.has_value %} +
已配置 {{ field.masked }}
+ {% endif %} + + {% else %} + + {% endif %} +
+ {% endfor %} +
+
+ + + +
+
+{% else %} +

未能加载交易所 API 字段.

+{% endif %} diff --git a/lib/instance/templates/index.html b/lib/instance/templates/index.html index b4989d2..d3ca2a8 100644 --- a/lib/instance/templates/index.html +++ b/lib/instance/templates/index.html @@ -2020,6 +2020,6 @@ document.addEventListener("DOMContentLoaded", function () { }); {% endif %} - + \ No newline at end of file diff --git a/lib/instance/templates/settings_panel.html b/lib/instance/templates/settings_panel.html index c6e80a9..f5d75b6 100644 --- a/lib/instance/templates/settings_panel.html +++ b/lib/instance/templates/settings_panel.html @@ -2,7 +2,7 @@

系统设置

-

各区块说明见 docs/系统设置说明.md.

+

交易所 API、账户密码、划转与显示开关等在此维护.

{% if settings_tabs %} @@ -20,6 +20,8 @@
{% if tab.key == 'nav' %} {% include 'display_prefs_panel.html' %} + {% elif tab.key == 'exchange_api' %} + {% include 'exchange_api_settings_panel.html' %} {% elif tab.key == 'password' %} {% include 'password_settings_panel.html' %} {% elif tab.key == 'transfer' %}