diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 36893f4..b66d2f5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -20,6 +20,20 @@ jobs: with: python-version: "3.13" + # 构建前端 → web-ui/dist/,hatchling force-include 把它打进 wheel + # 的 easy_tdx/web/dist/,让 pip install easy-tdx[web] 开箱即用 UI。 + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: web-ui/package-lock.json + + - name: Build frontend + run: | + npm ci + npm run build + working-directory: web-ui + - run: pip install build - run: python -m build diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c44cf6..2e61867 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ 本文件记录 easy-tdx 的版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/)。 +## [1.19.5] — 2026-07-07 + +**修复 PyPI 安装后 `localhost:8000` 返回 404** —— `pip install easy-tdx[web]` 后启动 `easy-tdx serve`,浏览器打开 `localhost:8000` 直接 404。根因:PyPI wheel 不含前端 dist(只有 Python 包),`_resolve_web_dist_dir()` 三级探测全失败返回 None,StaticFiles 不挂载。v1.19.2 的 MIME 修复、v1.19.3 的 SPA fallback 都只对 EXE 打包态生效——PyPI 安装态连 dist 都没有,更谈不上 MIME 或 SPA。 + +### 修复 + +- **前端 dist 打进 wheel**(`pyproject.toml` + `.github/workflows/publish.yml`)—— hatchling 配置 `force-include` 把 `web-ui/dist` 映射到包内 `easy_tdx/web/dist`;`publish.yml` 在 `python -m build` 前先 `npm ci && npm run build` 构建前端。PyPI 用户 `pip install easy-tdx[web]` 后开箱即用 UI,无需 clone 仓库或手动构建前端。 +- **`_resolve_web_dist_dir()` 第 4 级探测**(`src/easy_tdx/web/app.py`)—— 新增"包内 `easy_tdx/web/dist`"分支,在环境变量 / _MEIPASS / 仓库根三级都失败后,回退到 PyPI 安装的包内 dist。 + ## [1.19.4] — 2026-07-07 **修复取不到行情数据的根因:MAC 客户端污染标准协议的 best_host** —— v1.19.3 在实际机器上"所有股票都取不到数据"(K 线响应偏移 2 剩余 0)。根因是 `mac/client.py` 的 `MacClient.from_best_host()` 和 `AsyncMacClient.from_best_host()` 调用了 `save_best_host(best)`——把选中的 **MAC 协议服务器**(如 `121.36.248.138`)写进了全局 `best_host` 字段,但这个字段是**标准 TDX 协议**用的。之后标准 `AsyncTdxClient` 用 `get_best_host()` 读到这个 MAC host,用标准协议请求 MAC 服务器,返回空 body。web app 启动时 lifespan 会启动 MAC 客户端,这就是污染时机。 diff --git a/pyproject.toml b/pyproject.toml index ecbb47f..1d9cab8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "easy-tdx" -version = "1.19.4" +version = "1.19.5" description = "通达信 TCP 协议行情数据客户端,支持在线行情、离线数据读取与写入同步" readme = "README.md" requires-python = ">=3.10" @@ -23,6 +23,14 @@ packaging = ["pystray>=0.19", "Pillow>=10.0"] [tool.hatch.build.targets.wheel] packages = ["src/easy_tdx"] +# 前端 dist 作为数据文件打进 wheel,让 PyPI 安装的用户也能开箱即用 web UI。 +# 前端必须先构建到 web-ui/dist/(release.yml / publish.yml 都会先 npm run build)。 +# 映射到包内 easy_tdx/web/dist/,运行时由 _resolve_web_dist_dir 第 4 级探测。 +artifacts = [ + "src/easy_tdx/web/dist", +] +[tool.hatch.build.targets.wheel.force-include] +"web-ui/dist" = "easy_tdx/web/dist" [tool.mypy] strict = true diff --git a/src/easy_tdx/web/app.py b/src/easy_tdx/web/app.py index 369804e..0e2c2fd 100644 --- a/src/easy_tdx/web/app.py +++ b/src/easy_tdx/web/app.py @@ -21,8 +21,8 @@ logger = logging.getLogger(__name__) def _resolve_web_dist_dir() -> Path | None: """定位前端构建产物目录(Vite build 输出的 ``web-ui/dist``)。 - 依次探测三处,命中即返回,全部缺失时返回 ``None``(开发期未构建前端 - 时正常,路由层照常工作,仅前端页面 404): + 依次探测四处,命中即返回,全部缺失时返回 ``None``(仅 API 可用, + 前端页面 404): 1. ``EASY_TDX_WEB_DIST`` 环境变量——部署/调试时显式指定。 2. PyInstaller 运行态:``sys._MEIPASS / "web_dist"``——单 EXE 解压 @@ -30,6 +30,9 @@ def _resolve_web_dist_dir() -> Path | None: 此分支自动跳过。 3. 开发态:仓库根目录的 ``web-ui/dist``——支持 ``pip install -e .`` 后直接 ``easy-tdx serve`` 调试,无需打包。 + 4. PyPI wheel 安装态:包内 ``easy_tdx/web/dist``——hatchling 把 + 编译好的 dist 作为数据文件打进 wheel(v1.19.5 起含),让 + ``pip install easy-tdx[web]`` 后开箱即用 web UI。 """ env_dir = os.environ.get("EASY_TDX_WEB_DIST") if env_dir: @@ -50,6 +53,11 @@ def _resolve_web_dist_dir() -> Path | None: if p.is_dir(): return p + # PyPI 安装态:包内的 web/dist(wheel 打包时 force-include 进来) + pkg_dist = Path(__file__).resolve().parent / "dist" + if pkg_dist.is_dir(): + return pkg_dist + return None