Skip to content

Commit 598a739

Browse files
committed
fix: improve WebUI update stability
1 parent af70151 commit 598a739

9 files changed

Lines changed: 638 additions & 77 deletions

File tree

.github/workflows/release.yml

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,18 +71,28 @@ jobs:
7171
echo "${{ steps.tag.outputs.tag }}" > dist/assets/version
7272
zip -r "AstrBot-${{ steps.tag.outputs.tag }}-dashboard.zip" dist
7373
74+
- name: Build core package
75+
shell: bash
76+
run: |
77+
git archive \
78+
--format=zip \
79+
--prefix="AstrBot-${{ steps.tag.outputs.tag }}/" \
80+
--output="AstrBot-${{ steps.tag.outputs.tag }}-core.zip" \
81+
HEAD
82+
7483
- name: Upload dashboard artifact
7584
uses: actions/upload-artifact@v7
7685
with:
7786
name: Dashboard-${{ steps.tag.outputs.tag }}
7887
if-no-files-found: error
7988
path: dashboard/AstrBot-${{ steps.tag.outputs.tag }}-dashboard.zip
8089

81-
- name: Upload dashboard package to Cloudflare R2
90+
- name: Upload release packages to Cloudflare R2
8291
if: ${{ env.R2_ACCOUNT_ID != '' && env.R2_ACCESS_KEY_ID != '' && env.R2_SECRET_ACCESS_KEY != '' }}
8392
env:
8493
R2_BUCKET_NAME: "astrbot"
85-
R2_OBJECT_NAME: "astrbot-webui-latest.zip"
94+
DASHBOARD_LATEST_OBJECT_NAME: "astrbot-webui-latest.zip"
95+
CORE_LATEST_OBJECT_NAME: "astrbot-core-latest.zip"
8696
VERSION_TAG: ${{ steps.tag.outputs.tag }}
8797
shell: bash
8898
run: |
@@ -98,11 +108,18 @@ jobs:
98108
endpoint = https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com
99109
EOF
100110
101-
cp "dashboard/AstrBot-${VERSION_TAG}-dashboard.zip" "dashboard/${R2_OBJECT_NAME}"
102-
rclone copy "dashboard/${R2_OBJECT_NAME}" "r2:${R2_BUCKET_NAME}" --progress
111+
cp "dashboard/AstrBot-${VERSION_TAG}-dashboard.zip" "dashboard/${DASHBOARD_LATEST_OBJECT_NAME}"
112+
rclone copy "dashboard/${DASHBOARD_LATEST_OBJECT_NAME}" "r2:${R2_BUCKET_NAME}" --progress
103113
cp "dashboard/AstrBot-${VERSION_TAG}-dashboard.zip" "dashboard/astrbot-webui-${VERSION_TAG}.zip"
104114
rclone copy "dashboard/astrbot-webui-${VERSION_TAG}.zip" "r2:${R2_BUCKET_NAME}" --progress
105115
116+
cp "AstrBot-${VERSION_TAG}-core.zip" "${CORE_LATEST_OBJECT_NAME}"
117+
rclone copy "${CORE_LATEST_OBJECT_NAME}" "r2:${R2_BUCKET_NAME}" --progress
118+
cp "AstrBot-${VERSION_TAG}-core.zip" "astrbot-core-${VERSION_TAG}.zip"
119+
rclone copy "astrbot-core-${VERSION_TAG}.zip" "r2:${R2_BUCKET_NAME}" --progress
120+
rclone copyto "AstrBot-${VERSION_TAG}-core.zip" "r2:${R2_BUCKET_NAME}/astrbot-core/${VERSION_TAG}/source.zip" --progress
121+
rclone copyto "AstrBot-${VERSION_TAG}-core.zip" "r2:${R2_BUCKET_NAME}/download/astrbot-core/${VERSION_TAG}/source.zip" --progress
122+
106123
publish-release:
107124
name: Publish GitHub Release
108125
if: github.repository == 'AstrBotDevs/AstrBot'

astrbot/core/config/default.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from astrbot.core.computer.booters.cua_defaults import CUA_DEFAULT_CONFIG
66
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
77

8-
VERSION = "4.25.5"
8+
VERSION = "4.25.6-rc.1"
99
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
1010
PERSONAL_WECHAT_CONFIG_METADATA = {
1111
"weixin_oc_base_url": {

astrbot/core/updator.py

Lines changed: 110 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import os
22
import sys
33
import time
4+
import zipfile
5+
from pathlib import Path
46

57
import psutil
68

79
from astrbot.core import logger
810
from astrbot.core.config.default import VERSION
911
from astrbot.core.utils.astrbot_path import get_astrbot_path
12+
from astrbot.core.utils.io import ensure_dir
1013

1114
from .zip_updator import ReleaseInfo, RepoZipUpdator
1215

@@ -21,6 +24,30 @@ def __init__(self, repo_mirror: str = "", verify: str | bool | None = None) -> N
2124
super().__init__(repo_mirror, verify=verify)
2225
self.MAIN_PATH = get_astrbot_path()
2326
self.ASTRBOT_RELEASE_API = "https://api.soulter.top/releases"
27+
self.CORE_PACKAGE_BASE_URL = (
28+
"https://astrbot-registry.soulter.top/download/astrbot-core"
29+
)
30+
31+
def _build_core_package_url(self, version: str | None) -> str | None:
32+
"""Build the hosted core package URL for a release tag.
33+
34+
Args:
35+
version: Release tag, such as ``v4.26.6``.
36+
37+
Returns:
38+
Public package URL, or None when hosted package download is disabled.
39+
"""
40+
41+
if not version or not str(version).startswith("v"):
42+
return None
43+
44+
base_url = os.environ.get(
45+
"ASTRBOT_CORE_PACKAGE_BASE_URL",
46+
self.CORE_PACKAGE_BASE_URL,
47+
).strip()
48+
if not base_url:
49+
return None
50+
return f"{base_url.rstrip('/')}/{version}/source.zip"
2451

2552
def terminate_child_processes(self) -> None:
2653
"""终止当前进程的所有子进程
@@ -151,6 +178,41 @@ async def update(
151178
proxy="",
152179
progress_callback=None,
153180
) -> None:
181+
zip_path = await self.download_update_package(
182+
latest=latest,
183+
version=version,
184+
proxy=proxy,
185+
progress_callback=progress_callback,
186+
)
187+
self.apply_update_package(zip_path)
188+
189+
if reboot:
190+
self._reboot()
191+
192+
async def download_update_package(
193+
self,
194+
latest=True,
195+
version=None,
196+
proxy="",
197+
path: str | Path = "temp.zip",
198+
progress_callback=None,
199+
) -> Path:
200+
"""Download an AstrBot core update package without applying it.
201+
202+
Args:
203+
latest: Whether to download the latest release.
204+
version: Specific release tag or commit hash to download.
205+
proxy: Optional GitHub proxy prefix.
206+
path: Destination zip path.
207+
progress_callback: Optional callback for download progress payloads.
208+
209+
Returns:
210+
Path to the downloaded update package.
211+
212+
Raises:
213+
Exception: If update metadata cannot resolve a package URL.
214+
"""
215+
154216
update_data = await self.fetch_release_info(self.ASTRBOT_RELEASE_API, latest)
155217
file_url = None
156218

@@ -159,15 +221,18 @@ async def update(
159221
"Error: You are running AstrBot via CLI, please use `pip` or `uv tool upgrade` to update AstrBot."
160222
) # 避免版本管理混乱
161223

224+
target_version = None
162225
if latest:
163226
latest_version = update_data[0]["tag_name"]
164227
if self.compare_version(VERSION, latest_version) >= 0:
165228
raise Exception("当前已经是最新版本。")
229+
target_version = latest_version
166230
file_url = update_data[0]["zipball_url"]
167231
elif str(version).startswith("v"):
168232
# 更新到指定版本
169233
for data in update_data:
170234
if data["tag_name"] == version:
235+
target_version = data["tag_name"]
171236
file_url = data["zipball_url"]
172237
if not file_url:
173238
raise Exception(f"未找到版本号为 {version} 的更新文件。")
@@ -181,16 +246,49 @@ async def update(
181246
proxy = proxy.removesuffix("/")
182247
file_url = f"{proxy}/{file_url}"
183248

184-
try:
185-
await self._download_file(
186-
file_url,
187-
"temp.zip",
188-
progress_callback=progress_callback,
189-
)
190-
logger.info("下载 AstrBot Core 更新文件完成,正在执行解压...")
191-
self.unzip_file("temp.zip", self.MAIN_PATH)
192-
except BaseException as e:
193-
raise e
249+
zip_path = Path(path)
250+
ensure_dir(zip_path.parent)
251+
hosted_package_url = self._build_core_package_url(target_version)
252+
if hosted_package_url:
253+
try:
254+
logger.info(
255+
f"优先从托管存储下载 AstrBot Core 更新包: {hosted_package_url}"
256+
)
257+
await self._download_file(
258+
hosted_package_url,
259+
str(zip_path),
260+
progress_callback=progress_callback,
261+
)
262+
if not zipfile.is_zipfile(zip_path):
263+
raise RuntimeError(
264+
"Downloaded hosted package is not a valid ZIP file"
265+
)
266+
return zip_path
267+
except Exception as exc:
268+
logger.warning(
269+
f"从托管存储下载 AstrBot Core 更新包失败: {exc},"
270+
"将回退到当前更新源。"
271+
)
194272

195-
if reboot:
196-
self._reboot()
273+
await self._download_file(
274+
file_url,
275+
str(zip_path),
276+
progress_callback=progress_callback,
277+
)
278+
return zip_path
279+
280+
def apply_update_package(self, zip_path: str | Path) -> None:
281+
"""Apply a previously downloaded AstrBot core update package.
282+
283+
Args:
284+
zip_path: Core update zip archive path.
285+
286+
Returns:
287+
None.
288+
289+
Raises:
290+
Exception: If the archive cannot be extracted or applied.
291+
"""
292+
293+
logger.info("下载 AstrBot Core 更新文件完成,正在执行解压...")
294+
self.unzip_file(str(zip_path), self.MAIN_PATH)

astrbot/core/utils/io.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -398,12 +398,27 @@ async def download_dashboard(
398398
version: str | None = None,
399399
proxy: str | None = None,
400400
progress_callback=None,
401+
extract: bool = True,
401402
) -> None:
402-
"""下载管理面板文件"""
403+
"""Download dashboard assets and optionally extract them.
404+
405+
Args:
406+
path: Destination zip path. Defaults to the AstrBot data directory.
407+
extract_path: Directory where assets should be extracted.
408+
latest: Whether to download the latest dashboard build.
409+
version: Specific release tag or commit hash to download.
410+
proxy: Optional download proxy prefix.
411+
progress_callback: Optional callback for download progress payloads.
412+
extract: Whether to extract the archive after download.
413+
414+
Returns:
415+
None.
416+
"""
403417
if path is None:
404418
zip_path = Path(get_astrbot_data_path()).absolute() / "dashboard.zip"
405419
else:
406420
zip_path = Path(path).absolute()
421+
ensure_dir(zip_path.parent)
407422

408423
if latest or len(str(version)) != 40:
409424
ver_name = "latest" if latest else version
@@ -456,5 +471,28 @@ async def download_dashboard(
456471
show_progress=True,
457472
progress_callback=progress_callback,
458473
)
474+
if extract:
475+
extract_dashboard(zip_path, extract_path)
476+
477+
478+
def extract_dashboard(zip_path: str | Path, extract_path: str | Path = "data") -> None:
479+
"""Extract a downloaded dashboard archive.
480+
481+
Args:
482+
zip_path: Dashboard zip archive path.
483+
extract_path: Directory where the archive contents should be extracted.
484+
485+
Returns:
486+
None.
487+
"""
488+
489+
extract_root = Path(extract_path).resolve()
490+
ensure_dir(extract_root)
459491
with zipfile.ZipFile(zip_path, "r") as z:
460-
z.extractall(extract_path)
492+
for member in z.infolist():
493+
target_path = (extract_root / member.filename).resolve()
494+
if not target_path.is_relative_to(extract_root):
495+
raise ValueError(
496+
f"Unsafe dashboard archive path: {member.filename}",
497+
)
498+
z.extract(member, extract_root)

0 commit comments

Comments
 (0)