fix(HEL-494): 网站市场客户端改为纯中枢 Facade,并迁移 iFinD 凭据到中枢

生产 gateway 不再读取 Tushare token 或实例化 TushareProvider/TushareClient;问财凭据经带鉴权的中枢接口加密入库,避免发版后 iFinD 未配置。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
总工
2026-09-08 23:37:53 +08:00
co-authored by Cursor multica-agent
parent 100752f43c
commit 8a7d1f3698
15 changed files with 235 additions and 83 deletions
+72 -4
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import ast
import json
import re
import unittest
from pathlib import Path
from unittest.mock import patch
@@ -143,6 +144,26 @@ def hub_payload(request) -> dict:
],
"meta": {"stale": False, "staleness_seconds": 0, "source": "tencent"},
}
if path == "/v1/auction":
return {
"schema_version": 1,
"data": [
{
"ts_code": "600000.SH",
"trade_date": "20240902",
"close": 10.2,
"vol": 1000.0,
"amount": 2000.0,
}
],
"meta": {"stale": False, "staleness_seconds": 0, "source": "datahub"},
}
if path == "/v1/credentials/ifind":
return {
"schema_version": 1,
"data": {"configured": True, "access_ready": True, "access_expires_at": ""},
"meta": {"source": "ifind"},
}
if path == "/v1/intraday/points":
return {
"schema_version": 1,
@@ -184,7 +205,7 @@ def hub_payload(request) -> dict:
],
"meta": {"source": "ifind"},
}
if api_name in {"daily", "rt_k"}:
if api_name in {"daily", "rt_k", "stk_auction"}:
return {
"schema_version": 1,
"data": [{"ts_code": "600000.SH", "trade_date": "20240902", "close": 10.2, "amount": 2000.0}],
@@ -254,10 +275,22 @@ class HubExclusiveWebsiteTests(unittest.TestCase):
self.assertNotIn("IfindHttpClient", source)
self.assertNotIn("EastmoneyChartClient", source)
self.assertNotIn("WebRealtimeAggregator", source)
self.assertNotIn("TushareProvider", source)
self.assertIsNone(re.search(r"(?<![A-Za-z])TushareClient\(", source))
self.assertIn("HubIfindProxy", source)
self.assertIn("HubRealtimeProxy", source)
self.assertIn("legacy.realtime_aggregator = None", source)
self.assertIn("DatahubAwareTushareClient", source)
facade = (ROOT / "backend" / "data" / "datahub" / "bridge.py").read_text(encoding="utf-8")
tree = ast.parse(facade)
cls = next(
node
for node in tree.body
if isinstance(node, ast.ClassDef) and node.name == "DatahubAwareTushareClient"
)
methods = {item.name for item in cls.body if isinstance(item, ast.FunctionDef)}
self.assertNotIn("__getattr__", methods)
self.assertIn("query", methods)
self.assertTrue(any(base.id == "DashboardMixin" for base in cls.bases if isinstance(base, ast.Name)))
def test_production_python_does_not_embed_blocked_hosts(self) -> None:
violations = []
@@ -273,7 +306,8 @@ class HubExclusiveWebsiteTests(unittest.TestCase):
def test_website_runtime_does_not_call_blocked_hosts_from_gateway(self) -> None:
gateway_src = (ROOT / "backend" / "data" / "gateway.py").read_text(encoding="utf-8")
self.assertIn("legacy.realtime_aggregator = None", gateway_src)
self.assertNotIn("TushareProvider", gateway_src)
self.assertIsNone(re.search(r"(?<![A-Za-z])TushareClient\(", gateway_src))
self.assertIn("DatahubAwareTushareClient", gateway_src)
def test_bridge_query_has_no_legacy_call(self) -> None:
@@ -298,13 +332,47 @@ class HubExclusiveWebsiteTests(unittest.TestCase):
settings = _enabled_settings()
with patch("urllib.request.urlopen", blocked_urlopen):
gateway = build_data_gateway({"tushare_token": "tok"}, datahub_settings=settings)
gateway.datahub.client = DatahubClient(settings, urlopen=blocked_urlopen)
hub_client = DatahubClient(settings, urlopen=blocked_urlopen)
gateway.datahub.client = hub_client
rows = gateway.ifind.wencai("涨停")
quotes = gateway.realtime_observer.tencent_indices()
chart = gateway.chart_data.stock_daily("600000", "20240902")
market = gateway.tushare()
market_quotes = market.try_quotes(["600000.SH"])
auction = market.query("stk_auction", {"trade_date": "20240902"}, "")
gateway.ifind.set_credentials("refresh-token", "access-token")
self.assertEqual(rows[0]["涨停原因"], "重组")
self.assertEqual(len(quotes), 3)
self.assertEqual(chart[-1]["close"], 10.2)
self.assertEqual(market_quotes[0]["close"], 10.2)
self.assertEqual(auction[0]["close"], 10.2)
self.assertIsNone(market.realtime_aggregator)
self.assertEqual(market.token, "datahub")
def test_set_credentials_posts_to_hub_not_ifind(self) -> None:
seen: list[str] = []
def urlopen(request, timeout=None):
url = str(getattr(request, "full_url", None) or request)
seen.append(url)
if any(host in url for host in BLOCKED_HOSTS):
raise AssertionError(f"website opened blocked host: {url}")
return _Resp(hub_payload(request))
settings = _enabled_settings()
hub_client = DatahubClient(settings, urlopen=urlopen)
proxy = HubIfindProxy(DatahubBridge(settings, hub_client))
proxy.set_credentials("refresh-token", "access-token")
self.assertTrue(any("/v1/credentials/ifind" in url for url in seen))
self.assertFalse(any("51ifind.com" in url for url in seen))
self.assertFalse(any("quantapi" in url for url in seen))
def test_compose_passes_ifind_env_to_hub(self) -> None:
overlay = (ROOT / "compose.datahub.yaml").read_text(encoding="utf-8")
standalone = (ROOT / "xiaobai-datahub" / "compose.yaml").read_text(encoding="utf-8")
for text in (overlay, standalone):
self.assertIn('IFIND_REFRESH_TOKEN: "${IFIND_REFRESH_TOKEN:-}"', text)
self.assertIn('IFIND_ACCESS_TOKEN: "${IFIND_ACCESS_TOKEN:-}"', text)
if __name__ == "__main__":