Skip to content

Commit 3290d75

Browse files
authored
fix: prefer bundled dashboard over stale data dist (#8172)
* fix: prefer bundled dashboard over stale dist * fix: harden dashboard dist version checks
1 parent ef73d2d commit 3290d75

5 files changed

Lines changed: 173 additions & 19 deletions

File tree

astrbot/core/utils/io.py

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import inspect
33
import logging
44
import os
5+
import re
56
import shutil
67
import socket
78
import ssl
@@ -16,6 +17,7 @@
1617
from PIL import Image
1718

1819
from .astrbot_path import get_astrbot_data_path, get_astrbot_path, get_astrbot_temp_path
20+
from .version_comparator import VersionComparator
1921

2022
logger = logging.getLogger("astrbot")
2123

@@ -325,20 +327,67 @@ def get_local_ip_addresses():
325327
return network_ips
326328

327329

330+
def _read_dashboard_dist_version(dist_dir: str | Path) -> str | None:
331+
version_file = Path(dist_dir) / "assets" / "version"
332+
if version_file.exists():
333+
return version_file.read_text(encoding="utf-8").strip()
334+
return None
335+
336+
337+
def get_bundled_dashboard_dist_path() -> Path:
338+
return Path(get_astrbot_path()) / "astrbot" / "dashboard" / "dist"
339+
340+
341+
def _normalize_dashboard_version(version: str) -> str:
342+
version = version.strip()
343+
if version[:1].lower() == "v":
344+
version = version[1:]
345+
if not re.match(
346+
r"^[0-9]+(?:\.[0-9]+)*"
347+
r"(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?"
348+
r"(?:\+.+)?$",
349+
version,
350+
):
351+
raise ValueError(f"invalid dashboard version: {version!r}")
352+
return version
353+
354+
355+
def should_use_bundled_dashboard_dist(
356+
user_dist: str | Path, current_version: str
357+
) -> bool:
358+
user_version = _read_dashboard_dist_version(user_dist)
359+
bundled_dist = get_bundled_dashboard_dist_path()
360+
if user_version is None or not bundled_dist.exists():
361+
return False
362+
try:
363+
return (
364+
VersionComparator.compare_version(
365+
_normalize_dashboard_version(current_version),
366+
_normalize_dashboard_version(user_version),
367+
)
368+
> 0
369+
)
370+
except (TypeError, ValueError):
371+
return False
372+
373+
328374
async def get_dashboard_version():
329375
# First check user data directory (manually updated / downloaded dashboard).
330376
dist_dir = os.path.join(get_astrbot_data_path(), "dist")
331-
if not os.path.exists(dist_dir):
332-
# Fall back to the dist bundled inside the installed wheel.
333-
_bundled = Path(get_astrbot_path()) / "astrbot" / "dashboard" / "dist"
334-
if _bundled.exists():
335-
dist_dir = str(_bundled)
336377
if os.path.exists(dist_dir):
337-
version_file = os.path.join(dist_dir, "assets", "version")
338-
if os.path.exists(version_file):
339-
with open(version_file, encoding="utf-8") as f:
340-
v = f.read().strip()
341-
return v
378+
from astrbot.core.config.default import VERSION
379+
380+
if should_use_bundled_dashboard_dist(dist_dir, VERSION):
381+
bundled_version = _read_dashboard_dist_version(
382+
get_bundled_dashboard_dist_path()
383+
)
384+
if bundled_version is not None:
385+
return bundled_version
386+
return _read_dashboard_dist_version(dist_dir)
387+
388+
bundled = get_bundled_dashboard_dist_path()
389+
if bundled.exists():
390+
return _read_dashboard_dist_version(bundled)
342391
return None
343392

344393

astrbot/dashboard/server.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@
2323
from astrbot.core.db import BaseDatabase
2424
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
2525
from astrbot.core.utils.datetime_utils import to_utc_isoformat
26-
from astrbot.core.utils.io import get_local_ip_addresses
26+
from astrbot.core.utils.io import (
27+
get_bundled_dashboard_dist_path,
28+
get_local_ip_addresses,
29+
should_use_bundled_dashboard_dist,
30+
)
2731

2832
from .plugin_page_auth import PluginPageAuth
2933
from .routes import *
@@ -37,9 +41,6 @@
3741
from .routes.subagent import SubAgentRoute
3842
from .routes.t2i import T2iRoute
3943

40-
# Static assets shipped inside the wheel (built during `hatch build`).
41-
_BUNDLED_DIST = Path(__file__).parent / "dist"
42-
4344

4445
class _AddrWithPort(Protocol):
4546
port: int
@@ -118,10 +119,14 @@ def __init__(
118119
self.data_path = os.path.abspath(webui_dir)
119120
else:
120121
user_dist = os.path.join(get_astrbot_data_path(), "dist")
121-
if os.path.exists(user_dist):
122+
bundled_dist = get_bundled_dashboard_dist_path()
123+
if os.path.exists(user_dist) and not should_use_bundled_dashboard_dist(
124+
user_dist,
125+
VERSION,
126+
):
122127
self.data_path = os.path.abspath(user_dist)
123-
elif _BUNDLED_DIST.exists():
124-
self.data_path = str(_BUNDLED_DIST)
128+
elif bundled_dist.exists():
129+
self.data_path = str(bundled_dist)
125130
logger.info("Using bundled dashboard dist: %s", self.data_path)
126131
else:
127132
# Fall back to expected user path (will fail gracefully later)

main.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
)
2424
from astrbot.core.utils.io import ( # noqa: E402
2525
download_dashboard,
26+
get_bundled_dashboard_dist_path,
2627
get_dashboard_version,
28+
should_use_bundled_dashboard_dist,
2729
)
2830

2931
# 将父目录添加到 sys.path
@@ -77,6 +79,13 @@ async def check_dashboard_files(webui_dir: str | None = None):
7779
data_dist_path = os.path.join(get_astrbot_data_path(), "dist")
7880
if os.path.exists(data_dist_path):
7981
v = await get_dashboard_version()
82+
if should_use_bundled_dashboard_dist(data_dist_path, VERSION):
83+
bundled_dist = get_bundled_dashboard_dist_path()
84+
logger.info(
85+
"Using bundled WebUI because data/dist is older than core version v%s.",
86+
VERSION,
87+
)
88+
return str(bundled_dist)
8089
if v is not None:
8190
# 存在文件
8291
if v == f"v{VERSION}":

tests/test_dashboard.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,36 @@ def _resolve_dashboard_password(core_lifecycle_td: AstrBotCoreLifecycle) -> str:
216216
return password
217217

218218

219+
def test_dashboard_uses_bundled_dist_when_data_dist_is_stale(
220+
core_lifecycle_td: AstrBotCoreLifecycle,
221+
monkeypatch,
222+
tmp_path,
223+
):
224+
data_dir = tmp_path / "data"
225+
user_dist = data_dir / "dist"
226+
bundled_dist = tmp_path / "bundled-dist"
227+
user_dist.mkdir(parents=True)
228+
bundled_dist.mkdir()
229+
230+
monkeypatch.setattr(
231+
"astrbot.dashboard.server.get_astrbot_data_path",
232+
lambda: str(data_dir),
233+
)
234+
monkeypatch.setattr(
235+
"astrbot.dashboard.server.get_bundled_dashboard_dist_path",
236+
lambda: bundled_dist,
237+
)
238+
monkeypatch.setattr(
239+
"astrbot.dashboard.server.should_use_bundled_dashboard_dist",
240+
lambda *_args, **_kwargs: True,
241+
)
242+
243+
shutdown_event = asyncio.Event()
244+
server = AstrBotDashboard(core_lifecycle_td, core_lifecycle_td.db, shutdown_event)
245+
246+
assert server.data_path == str(bundled_dist)
247+
248+
219249
async def _set_dashboard_password_change_required(
220250
core_lifecycle_td: AstrBotCoreLifecycle,
221251
required: bool,

tests/test_main.py

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import sys
3+
from pathlib import Path
34

45
# 将项目根目录添加到 sys.path
56
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
@@ -8,6 +9,7 @@
89

910
import pytest
1011

12+
from astrbot.core.utils.io import should_use_bundled_dashboard_dist
1113
from main import check_dashboard_files, check_env
1214

1315

@@ -175,8 +177,9 @@ async def test_check_dashboard_files_exists_but_version_mismatch(monkeypatch):
175177
"""Tests that a warning is logged when dashboard version mismatches."""
176178
monkeypatch.setattr(os.path, "exists", lambda x: True)
177179

178-
with mock.patch("main.get_dashboard_version") as mock_get_version:
179-
mock_get_version.return_value = "v0.0.1" # A different version
180+
with mock.patch(
181+
"main.get_dashboard_version", mock.AsyncMock(return_value="v0.0.1")
182+
):
180183

181184
with mock.patch("main.logger.warning") as mock_logger_warning:
182185
await check_dashboard_files()
@@ -185,6 +188,64 @@ async def test_check_dashboard_files_exists_but_version_mismatch(monkeypatch):
185188
assert "WebUI version mismatch" in call_args[0]
186189

187190

191+
def test_should_use_bundled_dashboard_dist_when_data_dist_is_stale(tmp_path):
192+
user_dist = tmp_path / "user-dist"
193+
bundled_dist = tmp_path / "bundled-dist"
194+
(user_dist / "assets").mkdir(parents=True)
195+
(bundled_dist / "assets").mkdir(parents=True)
196+
(user_dist / "assets" / "version").write_text("v4.24.2", encoding="utf-8")
197+
(bundled_dist / "assets" / "version").write_text("v4.24.4", encoding="utf-8")
198+
199+
with mock.patch(
200+
"astrbot.core.utils.io.get_bundled_dashboard_dist_path",
201+
return_value=bundled_dist,
202+
):
203+
assert should_use_bundled_dashboard_dist(user_dist, "v4.24.4") is True
204+
205+
206+
def test_should_keep_data_dist_when_version_file_is_malformed(tmp_path):
207+
user_dist = tmp_path / "user-dist"
208+
bundled_dist = tmp_path / "bundled-dist"
209+
(user_dist / "assets").mkdir(parents=True)
210+
(bundled_dist / "assets").mkdir(parents=True)
211+
(user_dist / "assets" / "version").write_text("not-a-version", encoding="utf-8")
212+
213+
with mock.patch(
214+
"astrbot.core.utils.io.get_bundled_dashboard_dist_path",
215+
return_value=bundled_dist,
216+
):
217+
assert should_use_bundled_dashboard_dist(user_dist, "4.24.4") is False
218+
219+
220+
@pytest.mark.asyncio
221+
async def test_check_dashboard_files_uses_bundled_dist_when_data_dist_is_stale(
222+
tmp_path,
223+
):
224+
"""Tests that a stale data/dist does not override bundled dashboard assets."""
225+
data_dir = tmp_path / "data"
226+
data_dist = data_dir / "dist"
227+
bundled_dist = tmp_path / "bundled-dist"
228+
data_dist.mkdir(parents=True)
229+
bundled_dist.mkdir()
230+
231+
with mock.patch("main.get_astrbot_data_path", return_value=str(data_dir)):
232+
with mock.patch(
233+
"main.get_dashboard_version", mock.AsyncMock(return_value="v0.0.1")
234+
):
235+
with mock.patch(
236+
"main.should_use_bundled_dashboard_dist", return_value=True
237+
):
238+
with mock.patch(
239+
"main.get_bundled_dashboard_dist_path",
240+
return_value=Path(bundled_dist),
241+
):
242+
with mock.patch("main.download_dashboard") as mock_download:
243+
result = await check_dashboard_files()
244+
245+
assert result == str(bundled_dist)
246+
mock_download.assert_not_called()
247+
248+
188249
@pytest.mark.asyncio
189250
async def test_check_dashboard_files_with_webui_dir_arg(monkeypatch):
190251
"""Tests that providing a valid webui_dir skips all checks."""

0 commit comments

Comments
 (0)