From 351c07bb283b3c578a80496940331680fc129a7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E6=B4=8B?= Date: Tue, 14 Jul 2026 01:08:48 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(ths):=20get=5Fbalance=20=E6=8C=89?= =?UTF-8?q?=E5=8F=AF=E8=A7=81=E6=80=A7=E8=BF=87=E6=BB=A4=E8=B5=84=E9=87=91?= =?UTF-8?q?=E6=8E=A7=E4=BB=B6=E2=80=94=E2=80=94=E5=A4=9A=E8=B4=A6=E6=88=B7?= =?UTF-8?q?=E4=B8=8B=E8=AF=AF=E8=AF=BB=E5=85=B6=E4=BB=96=E8=B4=A6=E6=88=B7?= =?UTF-8?q?=E9=9A=90=E8=97=8F=E9=9D=A2=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2026-07-14 双账户切换演练发现:xiadan 多账户登录时每个账户各挂一套 同 ID 资金控件(0x3F4..),只有当前账户的可见。get_balance 是全代码库 唯一不带 visible 过滤的控件查找(_find_grid/_find_input/撤单按钮/ 策略下拉/提交按钮均为可见优先),Alt+2 已切到账户二后 balance 仍 返回账户一全套数字,真钱 sizing 输入被污染;position 因 _find_grid 带 visible=True 读数正确,两者对照坐实根因。 修法与既有模式对齐:可见优先、放宽兜底(单账户/旧皮肤兼容),兜底 命中时留 warning。新增跨平台回归用例锁定查找顺序。 Co-Authored-By: Claude Fable 5 --- src/trader/ths/win.py | 13 +++++++++- tests/test_balance_visible.py | 46 +++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 tests/test_balance_visible.py diff --git a/src/trader/ths/win.py b/src/trader/ths/win.py index bf08e9c..c29b93b 100644 --- a/src/trader/ths/win.py +++ b/src/trader/ths/win.py @@ -669,7 +669,18 @@ def get_balance(self): hwnd = self.get_right_hwnd() data = {} for key, cid in BALANCE_CONTROL_ID_GROUP.items(): - ctrl = self._find_ctrl_by_id(hwnd, cid) + # 多账户登录时每个账户各挂一套同 ID 资金控件,只有当前账户的可见; + # 不按可见性过滤会读到其他账户隐藏面板的数字(2026-07-14 双账户 + # 切换演练:Alt+2 已切到账户二,balance 仍返回账户一全套数字)。 + # 与 _find_grid/_find_input 同款「可见优先、放宽兜底」,兜底命中时 + # 留 warning——多账户场景下该值可能来自其他账户。 + ctrl = self._find_ctrl_by_id(hwnd, cid, visible=True) + if not ctrl: + ctrl = self._find_ctrl_by_id(hwnd, cid) + if ctrl: + logger.warning( + "balance 字段 %s(0x%X) 无可见控件,回退未过滤匹配", key, cid + ) if ctrl > 0: data[key] = get_text(ctrl) self.state.update("balance", data) diff --git a/tests/test_balance_visible.py b/tests/test_balance_visible.py new file mode 100644 index 0000000..fd15402 --- /dev/null +++ b/tests/test_balance_visible.py @@ -0,0 +1,46 @@ +"""get_balance 可见性过滤回归:多账户下必须读当前账户的可见控件。 + +2026-07-14 双账户切换演练事故:xiadan 多账户登录时每个账户各挂一套同 ID +资金控件(0x3F4..),只有当前账户的可见。get_balance 原先不过滤可见性, +Alt+2 已切到账户二后仍返回账户一的全套数字——真钱 sizing 的输入被污染。 +这里锁定「可见优先、放宽兜底」的查找顺序。Win32 层全部打桩,可跨平台跑。 +""" +from trader.ths import win as w +from trader.ths.win import WinThsBackend + +HIDDEN, VISIBLE = 111, 222 + + +def _stubbed_backend(monkeypatch, find_ctrl): + b = WinThsBackend() + monkeypatch.setattr(b, "switch_to_normal", lambda: None) + monkeypatch.setattr(b, "refresh", lambda: None) + monkeypatch.setattr(b, "get_right_hwnd", lambda: 999) + monkeypatch.setattr(b, "_find_ctrl_by_id", find_ctrl) + monkeypatch.setattr(w, "hot_key", lambda keys: None) + monkeypatch.setattr( + w, "get_text", lambda h: "1.23" if h == VISIBLE else "34915.47" + ) + return b + + +def test_prefers_visible_ctrl_over_hidden_copy(monkeypatch): + """可见副本存在时必须用它——绝不能读到其他账户隐藏面板的数字。""" + + def find_ctrl(root, cid, cls=None, visible=False): + return VISIBLE if visible else HIDDEN + + result = _stubbed_backend(monkeypatch, find_ctrl).get_balance() + assert result["code"] == 0 + assert set(result["data"].values()) == {"1.23"} + + +def test_falls_back_to_unfiltered_when_no_visible(monkeypatch): + """找不到可见控件时放宽兜底(单账户/旧皮肤兼容),不至于整块读空。""" + + def find_ctrl(root, cid, cls=None, visible=False): + return 0 if visible else HIDDEN + + result = _stubbed_backend(monkeypatch, find_ctrl).get_balance() + assert result["code"] == 0 + assert set(result["data"].values()) == {"34915.47"} From 8f4a33aba8ffe69d29fb5efacb221dbd54cac36d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E6=B4=8B?= Date: Tue, 14 Jul 2026 01:14:12 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(ths):=20get=5Fbalance=20=E5=8E=BB?= =?UTF-8?q?=E6=8E=89=E6=9C=AA=E8=BF=87=E6=BB=A4=E5=85=9C=E5=BA=95=E2=80=94?= =?UTF-8?q?=E2=80=94=E8=AF=BB=E4=B8=8D=E5=88=B0=E5=8F=AF=E8=A7=81=E6=8E=A7?= =?UTF-8?q?=E4=BB=B6=E5=8D=B3=E6=98=8E=E7=A1=AE=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 兜底在面板加载间隙同样可能抓到其他账户的隐藏副本;真钱 sizing 宁可失败重试,不可静默读错。重试结构与 get_position 对齐。 Co-Authored-By: Claude Fable 5 --- src/trader/ths/win.py | 45 ++++++++++++++++++----------------- tests/test_balance_visible.py | 18 +++++++++----- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/src/trader/ths/win.py b/src/trader/ths/win.py index c29b93b..a3c0e6b 100644 --- a/src/trader/ths/win.py +++ b/src/trader/ths/win.py @@ -663,28 +663,29 @@ def cancel_last(self): return self._bulk_cancel("last") def get_balance(self): - self.switch_to_normal() - hot_key(["F4"]) - self.refresh() - hwnd = self.get_right_hwnd() - data = {} - for key, cid in BALANCE_CONTROL_ID_GROUP.items(): - # 多账户登录时每个账户各挂一套同 ID 资金控件,只有当前账户的可见; - # 不按可见性过滤会读到其他账户隐藏面板的数字(2026-07-14 双账户 - # 切换演练:Alt+2 已切到账户二,balance 仍返回账户一全套数字)。 - # 与 _find_grid/_find_input 同款「可见优先、放宽兜底」,兜底命中时 - # 留 warning——多账户场景下该值可能来自其他账户。 - ctrl = self._find_ctrl_by_id(hwnd, cid, visible=True) - if not ctrl: - ctrl = self._find_ctrl_by_id(hwnd, cid) - if ctrl: - logger.warning( - "balance 字段 %s(0x%X) 无可见控件,回退未过滤匹配", key, cid - ) - if ctrl > 0: - data[key] = get_text(ctrl) - self.state.update("balance", data) - return {"code": 0, "status": "succeed", "data": data} + # 多账户登录时每个账户各挂一套同 ID 资金控件,只有当前账户的可见; + # 不按可见性过滤会读到其他账户隐藏面板的数字(2026-07-14 双账户 + # 切换演练:Alt+2 已切到账户二,balance 仍返回账户一全套数字)。 + # 只认可见控件、读不到重试后明确报错——不做未过滤兜底:兜底在面板 + # 加载间隙同样可能抓到其他账户的隐藏副本,真钱 sizing 宁可失败不可读错。 + for retry in range(retry_time): + self.switch_to_normal() + hot_key(["F4"]) + self.refresh() + hwnd = self.get_right_hwnd() + data = {} + for key, cid in BALANCE_CONTROL_ID_GROUP.items(): + ctrl = self._find_ctrl_by_id(hwnd, cid, visible=True) + if ctrl > 0: + data[key] = get_text(ctrl) + if data: + self.state.update("balance", data) + return {"code": 0, "status": "succeed", "data": data} + time.sleep(sleep_time) + return {"code": 1, "status": "failed", + "msg": "未找到可见的资金面板控件(面板未加载完或客户端异常)," + "已放弃读取——不回退读隐藏面板(多账户下可能是其他账户的数字)," + "请稍后重试"} def get_position(self): for retry in range(retry_time): diff --git a/tests/test_balance_visible.py b/tests/test_balance_visible.py index fd15402..8b30010 100644 --- a/tests/test_balance_visible.py +++ b/tests/test_balance_visible.py @@ -3,7 +3,10 @@ 2026-07-14 双账户切换演练事故:xiadan 多账户登录时每个账户各挂一套同 ID 资金控件(0x3F4..),只有当前账户的可见。get_balance 原先不过滤可见性, Alt+2 已切到账户二后仍返回账户一的全套数字——真钱 sizing 的输入被污染。 -这里锁定「可见优先、放宽兜底」的查找顺序。Win32 层全部打桩,可跨平台跑。 + +这里锁定两条纪律:① 只认可见控件;② 读不到就明确失败,绝不回退到未过滤 +匹配(兜底在面板加载间隙同样可能抓到其他账户的隐藏副本——宁可失败不可读错)。 +Win32 层全部打桩,可跨平台跑。 """ from trader.ths import win as w from trader.ths.win import WinThsBackend @@ -18,13 +21,14 @@ def _stubbed_backend(monkeypatch, find_ctrl): monkeypatch.setattr(b, "get_right_hwnd", lambda: 999) monkeypatch.setattr(b, "_find_ctrl_by_id", find_ctrl) monkeypatch.setattr(w, "hot_key", lambda keys: None) + monkeypatch.setattr(w, "sleep_time", 0) monkeypatch.setattr( w, "get_text", lambda h: "1.23" if h == VISIBLE else "34915.47" ) return b -def test_prefers_visible_ctrl_over_hidden_copy(monkeypatch): +def test_reads_only_visible_ctrl(monkeypatch): """可见副本存在时必须用它——绝不能读到其他账户隐藏面板的数字。""" def find_ctrl(root, cid, cls=None, visible=False): @@ -35,12 +39,14 @@ def find_ctrl(root, cid, cls=None, visible=False): assert set(result["data"].values()) == {"1.23"} -def test_falls_back_to_unfiltered_when_no_visible(monkeypatch): - """找不到可见控件时放宽兜底(单账户/旧皮肤兼容),不至于整块读空。""" +def test_fails_loudly_when_no_visible_ctrl(monkeypatch): + """无可见控件 → 明确报失败,绝不回退未过滤匹配去读隐藏面板。""" def find_ctrl(root, cid, cls=None, visible=False): return 0 if visible else HIDDEN result = _stubbed_backend(monkeypatch, find_ctrl).get_balance() - assert result["code"] == 0 - assert set(result["data"].values()) == {"34915.47"} + assert result["code"] == 1 + assert "不回退" in result["msg"] + # 隐藏副本的数字绝不能出现在任何返回里 + assert "34915.47" not in str(result)