feat(cli): serve 新增 --no-ui 纯 API 模式

- --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 正常
This commit is contained in:
GitHub
2026-09-01 18:33:49 +08:00
parent 9d206c121b
commit ac4ac6b2aa
4 changed files with 42 additions and 5 deletions
+9 -2
View File
@@ -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)
+4 -1
View File
@@ -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:
+8 -2
View File
@@ -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:前端用 createWebHistoryHTML5 history 模式),
# 用户直接访问 /optimize、/portfolio 等前端路由或刷新时,后端必须
+21
View File
@@ -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 静态 MountFastAPI 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)