fix: keep WebUI bind options consistent (#1922)

* fix: keep WebUI CLI bind options authoritative

* fix(review-feedback-1922): apps/dsa-web/src/locales/settingsHelp.ts 未随本次 CLI/WebUI 绑定语义更新
This commit is contained in:
zhulinsen
2026-07-05 19:19:10 +08:00
committed by GitHub
parent a43daa4331
commit 523c7b00de
5 changed files with 81 additions and 17 deletions

View File

@@ -570,7 +570,7 @@ const settingsHelpZhCN: SettingsHelpMap = {
summary: '控制 WebUI 服务绑定在哪个网络地址上。',
usage: '本机访问通常使用 127.0.0.1云服务器、Docker 或需要外部访问时通常使用 0.0.0.0。',
valueNotes: [
'当前启动逻辑会在 host 为默认 0.0.0.0 时读取 WEBUI_HOST即使显式传入 --host 0.0.0.0,也可能被 .env 中的 WEBUI_HOST 覆盖。',
'启动时显式传入的 --host 会优先生效;若未传 --host则会使用运行时配置中的 WEBUI_HOST(或其默认值)。',
'在设置页保存后,只会写入 .env 并重载运行时配置对象,不会让当前 WebUI/API 进程重新绑定监听地址。',
'Docker Compose 中通常会在容器内使用 0.0.0.0,宿主机访问还取决于端口映射。',
],
@@ -588,6 +588,7 @@ const settingsHelpZhCN: SettingsHelpMap = {
summary: '控制 WebUI 服务监听的端口。',
usage: '本地默认 8000如端口冲突可改为其他 1-65535 范围内端口。',
valueNotes: [
'启动时显式传入的 --port 会优先生效;若未传 --port则会使用运行时配置中的 WEBUI_PORT或其默认值。',
'Docker 或云服务器访问还取决于宿主机端口映射和安全组。',
'设置页保存只会写入 .env不会让当前 WebUI/API 进程重新绑定端口。',
],
@@ -1713,7 +1714,7 @@ const settingsHelpEnUS: SettingsHelpMap = {
summary: 'Controls the network address the WebUI service binds to.',
usage: 'Use 127.0.0.1 for local-only access. Use 0.0.0.0 for cloud, Docker, or external access.',
valueNotes: [
'Current startup logic reads WEBUI_HOST when the host is the default 0.0.0.0; even an explicit --host 0.0.0.0 can still be overwritten by WEBUI_HOST in .env.',
'An explicit --host has higher priority at startup; if --host is not provided, runtime-configured WEBUI_HOST (or its default) is used.',
'Saving it from the settings page writes .env and reloads runtime config objects, but the running WebUI/API process will not rebind its host.',
'Docker Compose commonly binds 0.0.0.0 inside the container; host access also depends on port mapping.',
],
@@ -1729,6 +1730,7 @@ const settingsHelpEnUS: SettingsHelpMap = {
summary: 'Controls the port the WebUI service listens on.',
usage: 'Default is 8000. Use another port in the 1-65535 range when needed.',
valueNotes: [
'An explicit --port has higher priority at startup; if --port is not provided, runtime-configured WEBUI_PORT (or its default) is used.',
'Docker or cloud access also depends on host port mappings and firewall rules.',
'Saving from the settings page only writes .env; it does not rebind the running WebUI/API process.',
],

View File

@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
> For user-friendly release highlights, see the [GitHub Releases](https://github.com/ZhuLinsen/daily_stock_analysis/releases) page.
## [Unreleased]
- [修复] WebUI 启动时显式 `--host` / `--port` 不再被 `.env` 中的 `WEBUI_HOST` / `WEBUI_PORT` 覆盖,未传 CLI 参数时统一使用解析后的运行时配置。
- [改进] GitHub Actions: 每日分析工作流(`00-daily-analysis.yml`)新增钉钉通知环境变量映射,支持在云端定时任务中直接使用钉钉机器人。
- [修复] Web 持仓页首屏快照改用 `include_realtime=false` 快速估值,跳过逐票实时行情预取后先展示持仓列表,避免外部实时行情源变慢时长时间空白等待。
- [修复] 修复任务状态接口重建报告动作字段时把合法情绪分 `0` 当成空值的问题,确保低分报告能按评分口径纠正为卖出建议。

View File

@@ -34,9 +34,7 @@ WEBUI_HOST=127.0.0.1
WEBUI_HOST=0.0.0.0
```
> `127.0.0.1` 表示只有本机能访问,`0.0.0.0` 表示允许任何来源访问。云服务器必须改成 `0.0.0.0` 才能从外网打开界面。
> **注意**:当前 `python main.py` 启动逻辑会在 host 为默认 `0.0.0.0` 时读取 `.env` 里的 `WEBUI_HOST`;即使显式传入 `--host 0.0.0.0`,如果 `.env` 里仍是 `WEBUI_HOST=127.0.0.1`,最终也可能只监听本机。云服务器请务必先把 `.env` 改成 `WEBUI_HOST=0.0.0.0`。
> `127.0.0.1` 表示只有本机能访问,`0.0.0.0` 表示允许任何来源访问。云服务器需要把 `.env` 中的 `WEBUI_HOST` 改成 `0.0.0.0`,或在启动命令里显式传入 `--host 0.0.0.0`才能从外网打开界面。
### 第二步:启动服务
@@ -229,7 +227,7 @@ sudo firewall-cmd --reload
这是第二常见原因。`.env` 里默认是 `WEBUI_HOST=127.0.0.1`,这样服务只监听本机,外网根本连不上。
改法:打开 `.env`,把 `WEBUI_HOST=127.0.0.1` 改成 `WEBUI_HOST=0.0.0.0`,然后重启服务。
改法:打开 `.env`,把 `WEBUI_HOST=127.0.0.1` 改成 `WEBUI_HOST=0.0.0.0`,然后重启服务;也可以在启动命令里显式添加 `--host 0.0.0.0`
> Docker 方式不需要改这个,可以跳过。

21
main.py
View File

@@ -104,6 +104,13 @@ def _warn_if_public_webui_without_auth(host: str) -> None:
)
def _resolve_web_service_bind(args: argparse.Namespace, config: Config) -> Tuple[str, int]:
"""Resolve the effective Web/API bind address from CLI first, then config."""
host = args.host if args.host is not None else (config.webui_host or "127.0.0.1")
port = args.port if args.port is not None else config.webui_port
return host, port
def _read_active_env_values() -> Optional[Dict[str, str]]:
env_path = _get_active_env_path()
if not env_path.exists():
@@ -374,15 +381,15 @@ def parse_arguments() -> argparse.Namespace:
parser.add_argument(
'--port',
type=int,
default=8000,
help='FastAPI 服务端口(默认 8000'
default=None,
help='FastAPI 服务端口(默认使用 WEBUI_PORT未配置时为 8000'
)
parser.add_argument(
'--host',
type=str,
default='0.0.0.0',
help='FastAPI 服务监听地址(默认 0.0.0.0'
default=None,
help='FastAPI 服务监听地址(默认使用 WEBUI_HOST未配置时为 127.0.0.1'
)
parser.add_argument(
@@ -1302,12 +1309,8 @@ def main() -> int:
# === 启动 Web 服务 (如果启用) ===
start_serve = (args.serve or args.serve_only) and os.getenv("GITHUB_ACTIONS") != "true"
# 兼容旧版 WEBUI_HOST/WEBUI_PORT如果用户未通过 --host/--port 指定,则使用旧变量
if start_serve:
if args.host == '0.0.0.0' and os.getenv('WEBUI_HOST'):
args.host = os.getenv('WEBUI_HOST')
if args.port == 8000 and os.getenv('WEBUI_PORT'):
args.port = int(os.getenv('WEBUI_PORT'))
args.host, args.port = _resolve_web_service_bind(args, config)
_warn_if_public_webui_without_auth(args.host)
bot_clients_started = False

View File

@@ -92,8 +92,8 @@ class MainScheduleModeTestCase(unittest.TestCase):
"webui_only": False,
"serve": False,
"serve_only": False,
"host": "0.0.0.0",
"port": 8000,
"host": None,
"port": None,
"backtest": False,
"market_review": False,
"schedule": False,
@@ -114,6 +114,8 @@ class MainScheduleModeTestCase(unittest.TestCase):
defaults = {
"log_dir": self.temp_dir.name,
"webui_enabled": False,
"webui_host": "127.0.0.1",
"webui_port": 8000,
"dingtalk_stream_enabled": False,
"feishu_stream_enabled": False,
"schedule_enabled": False,
@@ -203,6 +205,64 @@ class MainScheduleModeTestCase(unittest.TestCase):
warning_log.assert_not_called()
def test_web_service_bind_uses_config_when_cli_omits_host_and_port(self) -> None:
args = self._make_args(host=None, port=None)
config = self._make_config(webui_host="127.0.0.1", webui_port=18000)
host, port = main._resolve_web_service_bind(args, config)
self.assertEqual(host, "127.0.0.1")
self.assertEqual(port, 18000)
def test_web_service_bind_keeps_explicit_cli_host_and_port(self) -> None:
args = self._make_args(host="0.0.0.0", port=8000)
config = self._make_config(webui_host="127.0.0.1", webui_port=18000)
host, port = main._resolve_web_service_bind(args, config)
self.assertEqual(host, "0.0.0.0")
self.assertEqual(port, 8000)
def test_serve_only_uses_config_bind_when_cli_omits_host_and_port(self) -> None:
args = self._make_args(serve_only=True)
config = self._make_config(webui_enabled=False, webui_host="127.0.0.1", webui_port=18000)
observed_bind = []
def fake_start_api_server(host, port, config):
observed_bind.append((host, port))
with patch.dict(os.environ, {"GITHUB_ACTIONS": "false"}, clear=False), \
patch("main.parse_arguments", return_value=args), \
patch("main.get_config", return_value=config), \
patch("main.prepare_webui_frontend_assets", return_value=True), \
patch("main.start_api_server", side_effect=fake_start_api_server), \
patch("main.start_bot_stream_clients"), \
patch("main.time.sleep", side_effect=KeyboardInterrupt):
exit_code = main.main()
self.assertEqual(exit_code, 0)
self.assertEqual(observed_bind, [("127.0.0.1", 18000)])
def test_serve_only_keeps_explicit_cli_bind_over_config(self) -> None:
args = self._make_args(serve_only=True, host="0.0.0.0", port=8000)
config = self._make_config(webui_enabled=False, webui_host="127.0.0.1", webui_port=18000)
observed_bind = []
def fake_start_api_server(host, port, config):
observed_bind.append((host, port))
with patch.dict(os.environ, {"GITHUB_ACTIONS": "false"}, clear=False), \
patch("main.parse_arguments", return_value=args), \
patch("main.get_config", return_value=config), \
patch("main.prepare_webui_frontend_assets", return_value=True), \
patch("main.start_api_server", side_effect=fake_start_api_server), \
patch("main.start_bot_stream_clients"), \
patch("main.time.sleep", side_effect=KeyboardInterrupt):
exit_code = main.main()
self.assertEqual(exit_code, 0)
self.assertEqual(observed_bind, [("0.0.0.0", 8000)])
def test_start_api_server_fails_before_thread_when_port_is_busy(self) -> None:
config = self._make_config(log_level="INFO")