From 88337ecdaf8f7b92dc4bf332d75e037dc4b198b1 Mon Sep 17 00:00:00 2001 From: dekun Date: Tue, 13 Oct 2026 14:07:20 +0800 Subject: [PATCH] =?UTF-8?q?feat=E6=9D=A1=E4=BB=B6=E5=8D=95=E5=80=99?= =?UTF-8?q?=E9=80=89=E5=B1=95=E7=A4=BA=E4=BA=A4=E6=98=93=E6=88=B7=E8=B5=84?= =?UTF-8?q?=E9=87=91=EF=BC=8C=E5=B9=B6=E6=94=AF=E6=8C=81=E6=89=93=E5=BC=80?= =?UTF-8?q?=E5=AE=9E=E4=BE=8B=E5=88=92=E8=BD=AC=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- app.py | 4 +- lib/cond_orders_cache_lib.py | 25 +++++++++++ lib/sso_lib.py | 7 ++++ static/cond_orders.js | 57 ++++++++++++++++++++++++- static/hub.css | 28 +++++++++++-- templates/base.html | 2 +- templates/cond_orders.html | 3 +- tests/test_cond_candidates_funds.py | 65 +++++++++++++++++++++++++++++ 8 files changed, 183 insertions(+), 8 deletions(-) create mode 100644 tests/test_cond_candidates_funds.py diff --git a/app.py b/app.py index 98c043b..5b6f869 100644 --- a/app.py +++ b/app.py @@ -586,13 +586,15 @@ def api_open_url(iid: int): inst = get_instance(iid) if not inst: return jsonify({"ok": False, "msg": "不存在"}), 404 + tab = (request.args.get("tab") or "").strip() or None url = build_embed_open_url( inst["base_url"], bridge_token=inst["bridge_token"], embed_path=inst.get("embed_path") or "/embed", ttl_sec=3600, + tab=tab, ) - return jsonify({"ok": True, "url": url, "base_url": inst["base_url"]}) + return jsonify({"ok": True, "url": url, "base_url": inst["base_url"], "tab": tab}) @app.route("/api/settings/update/hub/version", methods=["GET"]) diff --git a/lib/cond_orders_cache_lib.py b/lib/cond_orders_cache_lib.py index 8b52a7e..3228d6b 100644 --- a/lib/cond_orders_cache_lib.py +++ b/lib/cond_orders_cache_lib.py @@ -33,6 +33,15 @@ def _sse_heartbeat_sec() -> float: return 25.0 +def _safe_float(v: Any) -> float | None: + if v is None or v == "": + return None + try: + return float(v) + except (TypeError, ValueError): + return None + + def _candidates_from_snaps(rows: list[dict[str, Any]]) -> list[dict[str, Any]]: out: list[dict[str, Any]] = [] for row in rows or []: @@ -45,6 +54,20 @@ def _candidates_from_snaps(rows: list[dict[str, Any]]) -> list[dict[str, Any]]: mode = str(inst.get("mode") or "").strip().lower() if mode not in ("sim", "live"): mode = "" + funds = snap.get("funds") if isinstance(snap.get("funds"), dict) else {} + # 开仓吃交易户;优先 avail,回退总额 + trading_usdt = _safe_float(funds.get("trading_usdt_avail")) + if trading_usdt is None: + trading_usdt = _safe_float(funds.get("trading_usdt")) + if trading_usdt is None: + trading_usdt = _safe_float(snap.get("trading_usdt")) + funding_usdt = _safe_float(funds.get("funding_usdt")) + if funding_usdt is None: + funding_usdt = _safe_float(snap.get("funding_usdt")) + if trading_usdt is not None: + trading_usdt = round(float(trading_usdt), 4) + if funding_usdt is not None: + funding_usdt = round(float(funding_usdt), 4) out.append( { "id": row.get("id"), @@ -55,6 +78,8 @@ def _candidates_from_snaps(rows: list[dict[str, Any]]) -> list[dict[str, Any]]: "selectable": selectable, "mode": mode, "error": row.get("error") or "", + "trading_usdt": trading_usdt, + "funding_usdt": funding_usdt, } ) return out diff --git a/lib/sso_lib.py b/lib/sso_lib.py index 6b55598..3e29d5f 100644 --- a/lib/sso_lib.py +++ b/lib/sso_lib.py @@ -24,11 +24,18 @@ def build_embed_open_url( bridge_token: str, embed_path: str = "/embed", ttl_sec: int = 120, + tab: str | None = None, ) -> str: root = (base_url or "").rstrip("/") path = embed_path or "/embed" if not path.startswith("/"): path = "/" + path + tab_s = "".join(c for c in str(tab or "").strip().lower() if c.isalnum() or c == "_")[ + :32 + ] + if tab_s: + joiner = "&" if "?" in path else "?" + path = f"{path}{joiner}tab={tab_s}" token = make_sso_token(bridge_token, ttl_sec=ttl_sec) q = urlencode({"token": token, "next": path}) return f"{root}/hub_sso?{q}" diff --git a/static/cond_orders.js b/static/cond_orders.js index a5ec452..df7e631 100644 --- a/static/cond_orders.js +++ b/static/cond_orders.js @@ -202,6 +202,36 @@ return m === "sim" || m === "live" ? m : ""; } + function fmtTradingU(v) { + if (v == null || v === "") return "—"; + const n = Number(v); + if (Number.isNaN(n)) return "—"; + return n.toFixed(2) + "U"; + } + + function openInstanceTab(instanceId, tab) { + if (!instanceId) return; + const q = tab ? "?tab=" + encodeURIComponent(tab) : ""; + fetch("/api/instances/" + encodeURIComponent(instanceId) + "/open_url" + q) + .then(function (r) { + return r.json(); + }) + .then(function (d) { + const url = + d && d.url + ? d.url + : "/desk?id=" + encodeURIComponent(instanceId); + window.open(url, "_blank", "noopener,noreferrer"); + }) + .catch(function () { + window.open( + "/desk?id=" + encodeURIComponent(instanceId), + "_blank", + "noopener,noreferrer" + ); + }); + } + function renderCandidateCard(it, checkedIds) { const disabled = !it.selectable; const tip = disabled @@ -215,7 +245,14 @@ : "可选"; const id = String(it.id); const checked = !disabled && checkedIds.indexOf(id) >= 0; + const fundsTip = + it.funding_usdt != null + ? "资金户 " + fmtTradingU(it.funding_usdt) + : ""; return ( + '
' + '" + "" + + '交易 ' + + escapeHtml(fmtTradingU(it.trading_usdt)) + + "" + + "" + + '' + + "
" ); } @@ -287,6 +334,14 @@ renderCandidateGroup("sim", sim, checkedIds) + renderCandidateGroup("live", live, checkedIds) + renderCandidateGroup("", other, checkedIds); + box.querySelectorAll(".cond-inst-open").forEach(function (btn) { + btn.addEventListener("click", function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + const iid = decodeURIComponent(btn.getAttribute("data-inst-id") || ""); + openInstanceTab(iid, "options"); + }); + }); } function renderDesktopRows(list) { diff --git a/static/hub.css b/static/hub.css index 185aa1e..48afda5 100644 --- a/static/hub.css +++ b/static/hub.css @@ -1524,14 +1524,22 @@ input.readonly { .cond-inst-group-label { margin-bottom: 8px; } .cond-inst-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 10px; } +.cond-inst-row { + display: flex; + align-items: stretch; + gap: 8px; +} +.cond-inst-row.is-off { opacity: 0.5; } .cond-inst { display: flex; align-items: center; justify-content: space-between; gap: 8px; + flex: 1; + min-width: 0; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line); @@ -1539,9 +1547,21 @@ input.readonly { cursor: pointer; } .cond-inst input { width: auto; margin: 0; } -.cond-inst-name { flex: 1; font-weight: 600; } -.cond-inst-tag { color: var(--muted); font-size: 0.75rem; } -.cond-inst.is-off { opacity: 0.5; cursor: not-allowed; } +.cond-inst-name { flex: 1; font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.cond-inst-tag { color: var(--muted); font-size: 0.75rem; flex-shrink: 0; } +.cond-inst-funds { + color: var(--accent-2, #38bdf8); + font-size: 0.78rem; + font-weight: 600; + white-space: nowrap; + flex-shrink: 0; +} +.cond-inst-open { + flex-shrink: 0; + align-self: center; + white-space: nowrap; +} +.cond-inst.is-off { cursor: not-allowed; } .cond-actions { display: flex; align-items: center; diff --git a/templates/base.html b/templates/base.html index eafe61c..feccb1b 100644 --- a/templates/base.html +++ b/templates/base.html @@ -11,7 +11,7 @@ - + + {% endblock %} diff --git a/tests/test_cond_candidates_funds.py b/tests/test_cond_candidates_funds.py new file mode 100644 index 0000000..325f1e8 --- /dev/null +++ b/tests/test_cond_candidates_funds.py @@ -0,0 +1,65 @@ +"""条件单候选:带出交易户资金字段.""" +from __future__ import annotations + +import unittest + + +class TestCondCandidatesFunds(unittest.TestCase): + def test_candidates_include_trading_usdt(self): + from lib.cond_orders_cache_lib import _candidates_from_snaps + + rows = [ + { + "id": 1, + "name": "okx-a", + "online": True, + "error": "", + "snapshot": { + "positions_brief": [], + "cond_order_locked": False, + "instance": {"mode": "live"}, + "funds": {"trading_usdt": 123.456, "funding_usdt": 50.0}, + }, + }, + { + "id": 2, + "name": "okx-b", + "online": False, + "error": "timeout", + "snapshot": { + "positions_brief": [{"inst_id": "x"}], + "cond_order_locked": False, + "instance": {"mode": "sim"}, + "funds": {}, + }, + }, + ] + out = _candidates_from_snaps(rows) + self.assertEqual(len(out), 2) + self.assertTrue(out[0]["selectable"]) + self.assertAlmostEqual(out[0]["trading_usdt"], 123.456, places=3) + self.assertAlmostEqual(out[0]["funding_usdt"], 50.0, places=3) + self.assertFalse(out[1]["selectable"]) + self.assertIsNone(out[1]["trading_usdt"]) + + def test_open_url_tab_appended(self): + from lib.sso_lib import build_embed_open_url + + url = build_embed_open_url( + "https://okx.example.com", + bridge_token="tok", + embed_path="/embed", + tab="options", + ) + self.assertIn("hub_sso?", url) + self.assertIn("next=", url) + # next 已 urlencode,解码后应含 tab=options + from urllib.parse import parse_qs, unquote, urlparse + + q = parse_qs(urlparse(url).query) + next_path = unquote(q.get("next", [""])[0]) + self.assertIn("tab=options", next_path) + + +if __name__ == "__main__": + unittest.main()