From ac4ac6b2aaf60895bd3d55e29097c2981af336a0 Mon Sep 17 00:00:00 2001 From: GitHub Date: Tue, 1 Sep 2026 18:33:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(cli):=20serve=20=E6=96=B0=E5=A2=9E=20--no-?= =?UTF-8?q?ui=20=E7=BA=AF=20API=20=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - --no-ui:不托管 Web UI 前端(根路径 404),仅 /api/v1/* REST 接口, 且不自动打开浏览器——给 AI Agent / 程序化调用省去前端资源 - create_app/_create_app 新增 enable_ui 参数(默认 True 行为不变) - 测试锁定:no-ui 模式无 web-ui Mount 且 API 路由完整;四道门禁全绿 (ruff / ruff format / mypy / 1079 tests),实测根路径 404 + API 正常 --- src/easy_tdx/cli/cmd_web.py | 11 +++++++++-- src/easy_tdx/web/__init__.py | 5 ++++- src/easy_tdx/web/app.py | 10 ++++++++-- tests/unit/test_web_api.py | 21 +++++++++++++++++++++ 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/easy_tdx/cli/cmd_web.py b/src/easy_tdx/cli/cmd_web.py index ac2d780..9831f37 100644 --- a/src/easy_tdx/cli/cmd_web.py +++ b/src/easy_tdx/cli/cmd_web.py @@ -19,6 +19,12 @@ import click default=True, help="启动后自动打开浏览器(默认开启,PyInstaller 打包后老人双击即用)", ) +@click.option( + "--no-ui", + is_flag=True, + help="纯 API 模式:不托管 Web UI 前端,仅提供 /api/v1/* REST 接口" + "(给 AI Agent / 程序调用时省去前端资源;此时也不自动开浏览器)", +) def serve( host: str, port: int, @@ -26,6 +32,7 @@ def serve( tdx_port: int | None, reload: bool, open_browser: bool, + no_ui: bool, ) -> None: """启动 Web API 服务器(需要安装 easy-tdx[web])。""" try: @@ -39,7 +46,7 @@ def serve( # 启动后延迟打开浏览器:uvicorn 需要约 1-2 秒绑定端口,过早打开会 # 命中 connection refused。用后台 Timer 而非阻塞主线程。 - if open_browser and not reload: + if open_browser and not reload and not no_ui: # 0.0.0.0 / 127.0.0.1 在浏览器里用 localhost 打开(更友好)。 display_host = "localhost" if host in ("0.0.0.0", "127.0.0.1") else host url = f"http://{display_host}:{port}" @@ -57,5 +64,5 @@ def serve( else: from easy_tdx.web import create_app - app = create_app(host=tdx_host, port=tdx_port) + app = create_app(host=tdx_host, port=tdx_port, enable_ui=not no_ui) uvicorn.run(app, host=host, port=port) diff --git a/src/easy_tdx/web/__init__.py b/src/easy_tdx/web/__init__.py index a989925..61c42a2 100644 --- a/src/easy_tdx/web/__init__.py +++ b/src/easy_tdx/web/__init__.py @@ -24,6 +24,8 @@ def create_app( host: str | None = None, port: int | None = None, timeout: float | None = None, + *, + enable_ui: bool = True, ) -> FastAPI: """Create and configure the FastAPI application. @@ -31,13 +33,14 @@ def create_app( host: TDX server host (None = auto-detect best host). port: TDX server port (None = default 7709). timeout: Connection timeout in seconds. + enable_ui: False 时不托管 Web UI 前端(纯 API 模式,serve --no-ui)。 Returns: Configured FastAPI application instance. """ from easy_tdx.web.app import _create_app - return _create_app(host=host, port=port, timeout=timeout) + return _create_app(host=host, port=port, timeout=timeout, enable_ui=enable_ui) def app_factory() -> FastAPI: diff --git a/src/easy_tdx/web/app.py b/src/easy_tdx/web/app.py index 0a8525b..e0a997b 100644 --- a/src/easy_tdx/web/app.py +++ b/src/easy_tdx/web/app.py @@ -172,8 +172,14 @@ def _create_app( *, enable_mac: bool = True, enable_ex: bool = False, + enable_ui: bool = True, ) -> FastAPI: - """创建并配置 FastAPI 应用实例。""" + """创建并配置 FastAPI 应用实例。 + + Args: + enable_ui: 是否同源托管 Web UI 前端(``easy-tdx serve --no-ui`` + 纯 API 模式传 False:不挂载静态 dist,根路径 404,仅 /api/v1/*)。 + """ from easy_tdx.config import get_best_host, get_port, get_timeout if host is None: @@ -290,7 +296,7 @@ def _create_app( from fastapi.staticfiles import StaticFiles - dist_dir = _resolve_web_dist_dir() + dist_dir = _resolve_web_dist_dir() if enable_ui else None if dist_dir is not None: # SPA fallback:前端用 createWebHistory(HTML5 history 模式), # 用户直接访问 /optimize、/portfolio 等前端路由或刷新时,后端必须 diff --git a/tests/unit/test_web_api.py b/tests/unit/test_web_api.py index 393a2b2..4a58d5b 100644 --- a/tests/unit/test_web_api.py +++ b/tests/unit/test_web_api.py @@ -556,3 +556,24 @@ def test_openapi_schema_generated(): # they are verified in test_full_app_routes_registered instead. # Just ensure REST paths are present. assert "/api/v1/fund-flow" in schema["paths"] + + +def test_create_app_no_ui_mode(): + """--no-ui 纯 API 模式:不挂载 Web UI 静态托管,API 路由完整。""" + pytest.importorskip("fastapi") + from easy_tdx.web import create_app + + app = create_app(enable_ui=False) + # 无 web-ui 静态 Mount(FastAPI 0.141+ 顶层 Mount 不受 _IncludedRouter 影响) + mounts = [r for r in app.routes if type(r).__name__ == "Mount"] + assert all(getattr(m, "name", "") != "web-ui" for m in mounts) + + # API 路由完整可用 + paths = list(app.openapi()["paths"].keys()) + assert any("/api/v1/security" in p for p in paths) + assert any("/api/v1/watchlist" in p for p in paths) + + # 对照:默认模式挂载 web-ui(dist 存在时) + app_ui = create_app() + mounts_ui = [r for r in app_ui.routes if type(r).__name__ == "Mount"] + assert any(getattr(m, "name", "") == "web-ui" for m in mounts_ui)