# 更新日志 本文件记录 easy-tdx 的版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/)。 ## [1.19.7] — 2026-07-07 **新增「服务器设置」页面:web UI 上测速 + 切换通达信服务器** —— 解决"有些用户获取到的 IP 能连通、有些不能"的问题。不同地区/运营商对通达信各服务器连通性不同,之前用户只能碰运气或手动改 config.json。现在在 web UI 上新增第六个页面「服务器设置」,列出全部 50+ 候选服务器、一键并发测速、点选切换——切换后立即生效(热重连),无需重启服务。 ### 新增 - **`AsyncTdxClient.reconnect_to(host)`**(`src/easy_tdx/client.py`)—— 热切换 host 的核心方法:复用 `_execute_lock` 保证切换期间无并发请求撞半开连接,关旧连接→换 host→建新连接→重启心跳。切换失败抛异常(client 断开,路由层捕获返回友好提示)。 - **服务器设置路由**(`src/easy_tdx/web/routers/server.py`)—— 3 个端点: - `GET /api/v1/server/hosts`:列出候选 host + 当前 host(不测速,首屏秒开) - `POST /api/v1/server/test`:并发 ping 测速,返回延迟(ms)和可达性,按延迟排序 - `POST /api/v1/server/switch`:切换到指定 host(先 reconnect 成功再 save_best_host,避免连接失败污染 config) - **服务器设置页面**(`web-ui/src/views/ServerSettingsView.vue`)—— 左侧当前 host + 测速按钮,右侧 host 列表表格(IP/延迟颜色编码/状态徽章/使用按钮)。延迟 <100ms 绿色、<300ms 蓝色、≥300ms 红色、不可达灰色。 - **导航入口**:顶部导航栏新增「服务器设置」(第 6 个页面)。 ### 设计决策 - **不自动测速**:页面加载只列 host,点按钮才测速(50+ host 全 ping 要几秒,自动测速会卡首屏)。 - **切换顺序**:先 `reconnect_to` 成功 → 再 `save_best_host` 持久化(v1.19.4 host 污染 bug 的教训)。 - **host 校验**:只允许切换到候选列表里的 IP,防止任意地址注入。 ## [1.19.6] — 2026-07-07 **修复 EXE 丢失所有第三方依赖(pandas/numpy/uvicorn 等)** —— v1.19.5 的 EXE 只有 11MB(正常 44MB),双击报 `ModuleNotFoundError: No module named 'pandas'`。根因:`release.yml` 的步骤顺序是先 `pip install -e ".[web,packaging]"` 再 `Build frontend`,但 `pyproject.toml` 的 `force-include` 要求 `web-ui/dist` 在 `pip install` 时就存在——install 阶段 dist 不存在导致 editable install 静默降级,PyInstaller 收集不到第三方包。修复:调换 `release.yml` 步骤顺序,先 `npm run build` 再 `pip install`。 ### 修复 - **release.yml 步骤顺序**(`.github/workflows/release.yml`)—— `Build frontend` 移到 `Install Python deps` 之前,确保 `pip install -e .` 时 `web-ui/dist` 已存在。 ## [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 客户端,这就是污染时机。 ### 修复 - **MAC host 不再污染标准 best_host**(`src/easy_tdx/config.py` + `src/easy_tdx/mac/client.py`)—— 新增独立的 `best_mac_host` 字段 + `get_best_mac_host()` / `save_best_mac_host()`。`MacClient` / `AsyncMacClient` 的 `from_best_host()` 和 `__init__` 改用新字段(4 处),不再调 `save_best_host` / `get_best_host`。 - **best_host 交叉污染校验**(`src/easy_tdx/config.py:get_best_host`)—— 读取时检测缓存 host 是否在标准 host 候选列表(known_hosts + 源码默认)里,不在则自动重置为默认首个并持久化。**这会自动修复已被污染的 config.json**,用户无需手动删配置。 - **回归测试**(`tests/unit/test_config.py`)—— 新增 `TestBestHostPollutionGuard`(3 个测试:MAC host 被重置 / 合法 host 不重置 / 重置持久化)+ `TestMacHostSeparation`(2 个测试:save_best_mac_host 不碰 best_host / get_best_mac_host 返回独立字段)。更新既有 `test_config_json_host`(host 需在 known_hosts 里才通过校验)。 ## [1.19.3] — 2026-07-07 **修复 EXE 运行时两个问题:K 线空 body 仍 500 + 前端路由刷新 404** —— v1.19.2 在实际机器上运行日志暴露两个问题:(1) SH600519 等正常股票偶发请求 K 线时,通达信服务器返回 `ret_count>0` 但 body 完全为空(pos=2 剩余 0 字节),v1.18.3 的容错有 `if bars:` 条件——bars 为空时走 `raise` → 500,老人看到"取行情失败"。(2) 用户在 `/optimize`、`/portfolio` 等前端路由页面刷新时,后端 StaticFiles 找不到文件返回 404(SPA fallback 缺失)。 ### 修复 - **K 线空 body 不再 500**(`src/easy_tdx/commands/security_bars.py`)—— 移除 `if bars:` 条件,无论已解析条数多少,`TdxDecodeError` 都 `return bars`(空列表让前端分页重试比直接 500 友好)。日志证据:`偏移 2,实际剩余 0 字节` = body 只有 ret_count 头、第 1 条 datetime 就崩。`GetIndexBarsCmd`(指数 K 线)同改。更新 `test_security_bars_truncated_first_record_still_raises`(原断言 raise,现断言返回空列表)+ 新增 `test_security_bars_ret_count_lies_body_completely_empty` 回归守卫。 - **SPA fallback**(`src/easy_tdx/web/app.py`)—— 子类化 `StaticFiles` 为 `SPAStaticFiles`,404 时返回 `index.html` 让 Vue Router 接管。修复 `/optimize`、`/portfolio`、`/compare`、`/strategies` 等前端路由刷新 404。API 路径(`/api/v1/*`)已在路由表注册,不受影响。 ## [1.19.2] — 2026-07-07 **修复干净 Windows 上 EXE 双击后页面纯黑** —— v1.19.1 在没装开发工具的 Windows(如老人电脑)上双击 EXE,浏览器打开 `localhost:8000` 后页面纯黑、`/docs` 却能正常打开。根因:干净 Windows 的注册表里没有 `.js` 文件的 `Content Type` 映射,Python 的 `mimetypes.guess_type('.js')` 返回 `None`,FastAPI/Starlette 的 `StaticFiles` 回退到 `text/plain`。但 `index.html` 里的 `