mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-01 10:10:15 +08:00
Compare commits
4 Commits
v4.26.2
...
codex/host
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
724098e128 | ||
|
|
9ca08a8aca | ||
|
|
d88c68ea58 | ||
|
|
9a53500c61 |
25
.github/workflows/release.yml
vendored
25
.github/workflows/release.yml
vendored
@@ -71,6 +71,15 @@ jobs:
|
||||
echo "${{ steps.tag.outputs.tag }}" > dist/assets/version
|
||||
zip -r "AstrBot-${{ steps.tag.outputs.tag }}-dashboard.zip" dist
|
||||
|
||||
- name: Build core package
|
||||
shell: bash
|
||||
run: |
|
||||
git archive \
|
||||
--format=zip \
|
||||
--prefix="AstrBot-${{ steps.tag.outputs.tag }}/" \
|
||||
--output="AstrBot-${{ steps.tag.outputs.tag }}-core.zip" \
|
||||
HEAD
|
||||
|
||||
- name: Upload dashboard artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
@@ -78,11 +87,12 @@ jobs:
|
||||
if-no-files-found: error
|
||||
path: dashboard/AstrBot-${{ steps.tag.outputs.tag }}-dashboard.zip
|
||||
|
||||
- name: Upload dashboard package to Cloudflare R2
|
||||
- name: Upload release packages to Cloudflare R2
|
||||
if: ${{ env.R2_ACCOUNT_ID != '' && env.R2_ACCESS_KEY_ID != '' && env.R2_SECRET_ACCESS_KEY != '' }}
|
||||
env:
|
||||
R2_BUCKET_NAME: "astrbot"
|
||||
R2_OBJECT_NAME: "astrbot-webui-latest.zip"
|
||||
DASHBOARD_LATEST_OBJECT_NAME: "astrbot-webui-latest.zip"
|
||||
CORE_LATEST_OBJECT_NAME: "astrbot-core-latest.zip"
|
||||
VERSION_TAG: ${{ steps.tag.outputs.tag }}
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -98,11 +108,18 @@ jobs:
|
||||
endpoint = https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com
|
||||
EOF
|
||||
|
||||
cp "dashboard/AstrBot-${VERSION_TAG}-dashboard.zip" "dashboard/${R2_OBJECT_NAME}"
|
||||
rclone copy "dashboard/${R2_OBJECT_NAME}" "r2:${R2_BUCKET_NAME}" --progress
|
||||
cp "dashboard/AstrBot-${VERSION_TAG}-dashboard.zip" "dashboard/${DASHBOARD_LATEST_OBJECT_NAME}"
|
||||
rclone copy "dashboard/${DASHBOARD_LATEST_OBJECT_NAME}" "r2:${R2_BUCKET_NAME}" --progress
|
||||
cp "dashboard/AstrBot-${VERSION_TAG}-dashboard.zip" "dashboard/astrbot-webui-${VERSION_TAG}.zip"
|
||||
rclone copy "dashboard/astrbot-webui-${VERSION_TAG}.zip" "r2:${R2_BUCKET_NAME}" --progress
|
||||
|
||||
cp "AstrBot-${VERSION_TAG}-core.zip" "${CORE_LATEST_OBJECT_NAME}"
|
||||
rclone copy "${CORE_LATEST_OBJECT_NAME}" "r2:${R2_BUCKET_NAME}" --progress
|
||||
cp "AstrBot-${VERSION_TAG}-core.zip" "astrbot-core-${VERSION_TAG}.zip"
|
||||
rclone copy "astrbot-core-${VERSION_TAG}.zip" "r2:${R2_BUCKET_NAME}" --progress
|
||||
rclone copyto "AstrBot-${VERSION_TAG}-core.zip" "r2:${R2_BUCKET_NAME}/astrbot-core/${VERSION_TAG}/source.zip" --progress
|
||||
rclone copyto "AstrBot-${VERSION_TAG}-core.zip" "r2:${R2_BUCKET_NAME}/download/astrbot-core/${VERSION_TAG}/source.zip" --progress
|
||||
|
||||
publish-release:
|
||||
name: Publish GitHub Release
|
||||
if: github.repository == 'AstrBotDevs/AstrBot'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
import psutil
|
||||
@@ -23,6 +24,30 @@ class AstrBotUpdator(RepoZipUpdator):
|
||||
super().__init__(repo_mirror, verify=verify)
|
||||
self.MAIN_PATH = get_astrbot_path()
|
||||
self.ASTRBOT_RELEASE_API = "https://api.soulter.top/releases"
|
||||
self.CORE_PACKAGE_BASE_URL = (
|
||||
"https://astrbot-registry.soulter.top/download/astrbot-core"
|
||||
)
|
||||
|
||||
def _build_core_package_url(self, version: str | None) -> str | None:
|
||||
"""Build the hosted core package URL for a release tag.
|
||||
|
||||
Args:
|
||||
version: Release tag, such as ``v4.26.0``.
|
||||
|
||||
Returns:
|
||||
Public package URL, or None when hosted package download is disabled.
|
||||
"""
|
||||
|
||||
if not version or not str(version).startswith("v"):
|
||||
return None
|
||||
|
||||
base_url = os.environ.get(
|
||||
"ASTRBOT_CORE_PACKAGE_BASE_URL",
|
||||
self.CORE_PACKAGE_BASE_URL,
|
||||
).strip()
|
||||
if not base_url:
|
||||
return None
|
||||
return f"{base_url.rstrip('/')}/{version}/source.zip"
|
||||
|
||||
def terminate_child_processes(self) -> None:
|
||||
"""终止当前进程的所有子进程
|
||||
@@ -196,15 +221,18 @@ class AstrBotUpdator(RepoZipUpdator):
|
||||
"Error: You are running AstrBot via CLI, please use `pip` or `uv tool upgrade` to update AstrBot."
|
||||
) # 避免版本管理混乱
|
||||
|
||||
target_version = None
|
||||
if latest:
|
||||
latest_version = update_data[0]["tag_name"]
|
||||
if self.compare_version(VERSION, latest_version) >= 0:
|
||||
raise Exception("当前已经是最新版本。")
|
||||
target_version = latest_version
|
||||
file_url = update_data[0]["zipball_url"]
|
||||
elif str(version).startswith("v"):
|
||||
# 更新到指定版本
|
||||
for data in update_data:
|
||||
if data["tag_name"] == version:
|
||||
target_version = data["tag_name"]
|
||||
file_url = data["zipball_url"]
|
||||
if not file_url:
|
||||
raise Exception(f"未找到版本号为 {version} 的更新文件。")
|
||||
@@ -220,6 +248,28 @@ class AstrBotUpdator(RepoZipUpdator):
|
||||
|
||||
zip_path = Path(path)
|
||||
ensure_dir(zip_path.parent)
|
||||
hosted_package_url = self._build_core_package_url(target_version)
|
||||
if hosted_package_url:
|
||||
try:
|
||||
logger.info(
|
||||
f"优先从托管存储下载 AstrBot Core 更新包: {hosted_package_url}"
|
||||
)
|
||||
await self._download_file(
|
||||
hosted_package_url,
|
||||
str(zip_path),
|
||||
progress_callback=progress_callback,
|
||||
)
|
||||
if not zipfile.is_zipfile(zip_path):
|
||||
raise RuntimeError(
|
||||
"Downloaded hosted package is not a valid ZIP file"
|
||||
)
|
||||
return zip_path
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
f"从托管存储下载 AstrBot Core 更新包失败: {exc},"
|
||||
"将回退到当前更新源。"
|
||||
)
|
||||
|
||||
await self._download_file(
|
||||
file_url,
|
||||
str(zip_path),
|
||||
|
||||
@@ -461,6 +461,10 @@ async def download_dashboard(
|
||||
show_progress=True,
|
||||
progress_callback=progress_callback,
|
||||
)
|
||||
if not zipfile.is_zipfile(zip_path):
|
||||
raise RuntimeError(
|
||||
"Downloaded dashboard package is not a valid ZIP file"
|
||||
)
|
||||
except BaseException as _:
|
||||
if latest:
|
||||
# Resolve latest release tag from GitHub API to construct correct asset URL
|
||||
@@ -488,6 +492,10 @@ async def download_dashboard(
|
||||
show_progress=True,
|
||||
progress_callback=progress_callback,
|
||||
)
|
||||
if not zipfile.is_zipfile(zip_path):
|
||||
raise RuntimeError(
|
||||
"Downloaded dashboard package is not a valid ZIP file"
|
||||
)
|
||||
else:
|
||||
url = f"https://github.com/AstrBotDevs/astrbot-release-harbour/releases/download/release-{version}/dist.zip"
|
||||
logger.info(f"Downloading AstrBot WebUI from {url}")
|
||||
@@ -499,6 +507,8 @@ async def download_dashboard(
|
||||
show_progress=True,
|
||||
progress_callback=progress_callback,
|
||||
)
|
||||
if not zipfile.is_zipfile(zip_path):
|
||||
raise RuntimeError("Downloaded dashboard package is not a valid ZIP file")
|
||||
if extract:
|
||||
extract_dashboard(zip_path, extract_path)
|
||||
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import ntpath
|
||||
import posixpath
|
||||
import zipfile
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import certifi
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from astrbot.core.star.updator import PluginUpdator
|
||||
from astrbot.core.updator import AstrBotUpdator
|
||||
from astrbot.core.utils import io as io_utils
|
||||
from astrbot.core.zip_updator import RepoZipUpdator
|
||||
|
||||
|
||||
@@ -286,6 +290,185 @@ async def test_plugin_updator_install_prefers_download_url(
|
||||
assert calls["unzip"] == (str(expected_path) + ".zip", str(expected_path))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_astrbot_updator_prefers_hosted_core_package(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
monkeypatch.delenv("ASTRBOT_CLI", raising=False)
|
||||
monkeypatch.delenv("ASTRBOT_LAUNCHER", raising=False)
|
||||
monkeypatch.setenv("ASTRBOT_CORE_PACKAGE_BASE_URL", "https://cdn.example/core")
|
||||
|
||||
updator = AstrBotUpdator()
|
||||
calls: list[str] = []
|
||||
|
||||
async def fake_fetch_release_info(url: str, latest: bool = True): # noqa: ARG001
|
||||
return [
|
||||
{
|
||||
"version": "AstrBot v99.0.0",
|
||||
"published_at": "2026-06-19T00:00:00Z",
|
||||
"body": "hosted core package",
|
||||
"tag_name": "v99.0.0",
|
||||
"zipball_url": "https://github.example/archive.zip",
|
||||
}
|
||||
]
|
||||
|
||||
async def fake_download_file(url: str, path: str, progress_callback=None): # noqa: ARG001
|
||||
calls.append(url)
|
||||
with zipfile.ZipFile(path, "w") as archive:
|
||||
archive.writestr("AstrBot-v99.0.0/README.md", "hosted-core")
|
||||
|
||||
monkeypatch.setattr(updator, "fetch_release_info", fake_fetch_release_info)
|
||||
monkeypatch.setattr(updator, "_download_file", fake_download_file)
|
||||
|
||||
zip_path = await updator.download_update_package(
|
||||
latest=False,
|
||||
version="v99.0.0",
|
||||
path=tmp_path / "core.zip",
|
||||
)
|
||||
|
||||
assert zip_path == tmp_path / "core.zip"
|
||||
assert zipfile.is_zipfile(zip_path)
|
||||
assert calls == ["https://cdn.example/core/v99.0.0/source.zip"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_astrbot_updator_falls_back_when_hosted_core_package_fails(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
monkeypatch.delenv("ASTRBOT_CLI", raising=False)
|
||||
monkeypatch.delenv("ASTRBOT_LAUNCHER", raising=False)
|
||||
monkeypatch.setenv("ASTRBOT_CORE_PACKAGE_BASE_URL", "https://cdn.example/core")
|
||||
|
||||
updator = AstrBotUpdator()
|
||||
calls: list[str] = []
|
||||
|
||||
async def fake_fetch_release_info(url: str, latest: bool = True): # noqa: ARG001
|
||||
return [
|
||||
{
|
||||
"version": "AstrBot v99.0.0",
|
||||
"published_at": "2026-06-19T00:00:00Z",
|
||||
"body": "hosted core package",
|
||||
"tag_name": "v99.0.0",
|
||||
"zipball_url": "https://github.example/archive.zip",
|
||||
}
|
||||
]
|
||||
|
||||
async def fake_download_file(url: str, path: str, progress_callback=None): # noqa: ARG001
|
||||
calls.append(url)
|
||||
parsed = urlparse(url)
|
||||
if parsed.scheme == "https" and parsed.hostname == "cdn.example":
|
||||
raise RuntimeError("404")
|
||||
Path(path).write_bytes(b"github-core")
|
||||
|
||||
monkeypatch.setattr(updator, "fetch_release_info", fake_fetch_release_info)
|
||||
monkeypatch.setattr(updator, "_download_file", fake_download_file)
|
||||
|
||||
zip_path = await updator.download_update_package(
|
||||
latest=False,
|
||||
version="v99.0.0",
|
||||
path=tmp_path / "core.zip",
|
||||
)
|
||||
|
||||
assert zip_path == tmp_path / "core.zip"
|
||||
assert zip_path.read_bytes() == b"github-core"
|
||||
assert calls == [
|
||||
"https://cdn.example/core/v99.0.0/source.zip",
|
||||
"https://github.example/archive.zip",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_astrbot_updator_falls_back_when_hosted_core_package_is_not_zip(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
monkeypatch.delenv("ASTRBOT_CLI", raising=False)
|
||||
monkeypatch.delenv("ASTRBOT_LAUNCHER", raising=False)
|
||||
monkeypatch.setenv("ASTRBOT_CORE_PACKAGE_BASE_URL", "https://cdn.example/core")
|
||||
|
||||
updator = AstrBotUpdator()
|
||||
calls: list[str] = []
|
||||
|
||||
async def fake_fetch_release_info(url: str, latest: bool = True): # noqa: ARG001
|
||||
return [
|
||||
{
|
||||
"version": "AstrBot v99.0.0",
|
||||
"published_at": "2026-06-19T00:00:00Z",
|
||||
"body": "hosted core package",
|
||||
"tag_name": "v99.0.0",
|
||||
"zipball_url": "https://github.example/archive.zip",
|
||||
}
|
||||
]
|
||||
|
||||
async def fake_download_file(url: str, path: str, progress_callback=None): # noqa: ARG001
|
||||
calls.append(url)
|
||||
parsed = urlparse(url)
|
||||
if parsed.scheme == "https" and parsed.hostname == "cdn.example":
|
||||
Path(path).write_bytes(b"not a zip")
|
||||
return
|
||||
with zipfile.ZipFile(path, "w") as archive:
|
||||
archive.writestr("AstrBot-v99.0.0/README.md", "github-core")
|
||||
|
||||
monkeypatch.setattr(updator, "fetch_release_info", fake_fetch_release_info)
|
||||
monkeypatch.setattr(updator, "_download_file", fake_download_file)
|
||||
|
||||
zip_path = await updator.download_update_package(
|
||||
latest=False,
|
||||
version="v99.0.0",
|
||||
path=tmp_path / "core.zip",
|
||||
)
|
||||
|
||||
assert zip_path == tmp_path / "core.zip"
|
||||
assert zipfile.is_zipfile(zip_path)
|
||||
assert calls == [
|
||||
"https://cdn.example/core/v99.0.0/source.zip",
|
||||
"https://github.example/archive.zip",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_dashboard_falls_back_when_hosted_package_is_not_zip(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
calls: list[str] = []
|
||||
|
||||
async def fake_download_file(
|
||||
url: str,
|
||||
path: str,
|
||||
show_progress: bool = False, # noqa: ARG001
|
||||
progress_callback=None, # noqa: ARG001
|
||||
) -> None:
|
||||
calls.append(url)
|
||||
parsed = urlparse(url)
|
||||
if (
|
||||
parsed.scheme == "https"
|
||||
and parsed.hostname == "astrbot-registry.soulter.top"
|
||||
):
|
||||
Path(path).write_bytes(b"not a zip")
|
||||
return
|
||||
with zipfile.ZipFile(path, "w") as archive:
|
||||
archive.writestr("dist/index.html", "dashboard")
|
||||
|
||||
monkeypatch.setattr(io_utils, "download_file", fake_download_file)
|
||||
|
||||
zip_path = tmp_path / "dashboard.zip"
|
||||
await io_utils.download_dashboard(
|
||||
path=str(zip_path),
|
||||
latest=False,
|
||||
version="v99.0.0",
|
||||
extract=False,
|
||||
)
|
||||
|
||||
assert zipfile.is_zipfile(zip_path)
|
||||
assert calls == [
|
||||
"https://astrbot-registry.soulter.top/download/astrbot-dashboard/v99.0.0/dist.zip",
|
||||
"https://github.com/AstrBotDevs/AstrBot/releases/download/v99.0.0/AstrBot-v99.0.0-dashboard.zip",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_release_info_uses_httpx_client_with_env_proxy_support(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
||||
Reference in New Issue
Block a user