Skip to content

Commit 21f41c2

Browse files
authored
refactor: simplify updater architecture (#9493)
1 parent 8609037 commit 21f41c2

45 files changed

Lines changed: 3492 additions & 2300 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Makefile

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: worktree worktree-add worktree-rm pr-test-neo pr-test-full pr-test-full-fast
1+
.PHONY: worktree worktree-add worktree-rm pr-test-neo pr-test-full pr-test-full-fast update-test-sandbox
22

33
WORKTREE_DIR ?= ../astrbot_worktree
44
BRANCH ?= $(word 2,$(MAKECMDGOALS))
@@ -36,6 +36,27 @@ pr-test-full:
3636
pr-test-full-fast:
3737
./scripts/pr_test_env.sh --profile full --skip-sync --no-dashboard
3838

39+
clean-temp-deployment:
40+
@set -eu; \
41+
update_sandbox="$$(mktemp -d "$${TMPDIR:-/tmp}/astrbot-update-test.XXXXXX")"; \
42+
echo "Copying the current workspace to $$update_sandbox"; \
43+
rsync -a \
44+
--exclude='.git/' \
45+
--exclude='.venv/' \
46+
--exclude='data/' \
47+
--exclude='node_modules/' \
48+
--exclude='.pnpm-store/' \
49+
--exclude='.pytest_cache/' \
50+
--exclude='.ruff_cache/' \
51+
--exclude='__pycache__/' \
52+
./ "$$update_sandbox/"; \
53+
cd "$$update_sandbox"; \
54+
uv sync; \
55+
echo; \
56+
echo "Update test sandbox is ready: $$update_sandbox"; \
57+
echo "Start it with:"; \
58+
printf ' cd "%s" && uv run main.py\n' "$$update_sandbox"
59+
3960
# Swallow extra args (branch/base) so make doesn't treat them as targets
4061
%:
4162
@true
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from astrbot.api import star
22
from astrbot.api.event import AstrMessageEvent, MessageChain
3-
from astrbot.core.config.default import VERSION
4-
from astrbot.core.utils.io import download_dashboard
3+
from astrbot.core.updater import AstrBotUpdater
54

65

76
class AdminCommands:
@@ -11,5 +10,5 @@ def __init__(self, context: star.Context) -> None:
1110
async def update_dashboard(self, event: AstrMessageEvent) -> None:
1211
"""更新管理面板"""
1312
await event.send(MessageChain().message("⏳ Updating dashboard..."))
14-
await download_dashboard(version=f"v{VERSION}", latest=False)
13+
await AstrBotUpdater().ensure_dashboard()
1514
await event.send(MessageChain().message("✅ Dashboard updated successfully."))

astrbot/builtin_stars/builtin_commands/commands/help.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from astrbot.api import star
44
from astrbot.api.event import AstrMessageEvent, MessageEventResult
55
from astrbot.core.config.default import VERSION
6+
from astrbot.core.dashboard_assets import get_dashboard_version
67
from astrbot.core.star import command_management
7-
from astrbot.core.utils.io import get_dashboard_version
88

99

1010
class HelpCommand:

astrbot/cli/commands/cmd_plug.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
PluginStatus,
99
build_plug_list,
1010
check_astrbot_root,
11+
download_repository,
1112
get_astrbot_root,
12-
get_git_repo,
1313
install_local_plugin,
1414
manage_plugin,
1515
)
@@ -66,7 +66,7 @@ def new(name: str) -> None:
6666
raise click.ClickException("Repository URL must start with http")
6767

6868
click.echo("Downloading plugin template...")
69-
get_git_repo(
69+
download_repository(
7070
"https://github.com/Soulter/helloworld",
7171
plug_path,
7272
)

astrbot/cli/utils/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from .plugin import (
77
PluginStatus,
88
build_plug_list,
9-
get_git_repo,
9+
download_repository,
1010
install_local_plugin,
1111
manage_plugin,
1212
)
@@ -18,8 +18,8 @@
1818
"build_plug_list",
1919
"check_astrbot_root",
2020
"check_dashboard",
21+
"download_repository",
2122
"get_astrbot_root",
22-
"get_git_repo",
2323
"install_local_plugin",
2424
"manage_plugin",
2525
]

astrbot/cli/utils/basic.py

Lines changed: 5 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -24,60 +24,15 @@ def get_astrbot_root() -> Path:
2424

2525
async def check_dashboard(astrbot_root: Path) -> None:
2626
"""Check if the dashboard is installed"""
27-
from astrbot.core.config.default import VERSION
28-
from astrbot.core.utils.io import download_dashboard, get_dashboard_version
29-
30-
from .version_comparator import VersionComparator
27+
from astrbot.core.updater import AstrBotUpdater
3128

3229
# If the wheel ships bundled dashboard assets, no network download is needed.
3330
if _BUNDLED_DIST.exists():
3431
click.echo("Dashboard is bundled with the package – skipping download.")
3532
return
3633

3734
try:
38-
dashboard_version = await get_dashboard_version()
39-
match dashboard_version:
40-
case None:
41-
click.echo("Dashboard is not installed")
42-
if click.confirm(
43-
"Install dashboard?",
44-
default=True,
45-
):
46-
click.echo("Installing dashboard...")
47-
await download_dashboard(
48-
path="data/dashboard.zip",
49-
extract_path=str(astrbot_root),
50-
version=f"v{VERSION}",
51-
latest=False,
52-
)
53-
click.echo("Dashboard installed successfully")
54-
55-
case str():
56-
if VersionComparator.compare_version(VERSION, dashboard_version) <= 0:
57-
click.echo("Dashboard is already up to date")
58-
return
59-
try:
60-
version = dashboard_version.split("v")[1]
61-
click.echo(f"Dashboard version: {version}")
62-
await download_dashboard(
63-
path="data/dashboard.zip",
64-
extract_path=str(astrbot_root),
65-
version=f"v{VERSION}",
66-
latest=False,
67-
)
68-
except Exception as e:
69-
click.echo(f"Failed to download dashboard: {e}")
70-
return
71-
except FileNotFoundError:
72-
click.echo("Initializing dashboard directory...")
73-
try:
74-
await download_dashboard(
75-
path=str(astrbot_root / "dashboard.zip"),
76-
extract_path=str(astrbot_root),
77-
version=f"v{VERSION}",
78-
latest=False,
79-
)
80-
click.echo("Dashboard initialized successfully")
81-
except Exception as e:
82-
click.echo(f"Failed to download dashboard: {e}")
83-
return
35+
dashboard_path = await AstrBotUpdater().ensure_dashboard()
36+
click.echo(f"Dashboard is ready at {dashboard_path}")
37+
except Exception as exc:
38+
click.echo(f"Failed to prepare Dashboard: {exc}")

astrbot/cli/utils/plugin.py

Lines changed: 50 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -49,58 +49,59 @@ def _validate_plugin_dir_name(plugin_name: str, source_path: Path) -> str:
4949
return plugin_name
5050

5151

52-
def get_git_repo(url: str, target_path: Path, proxy: str | None = None) -> None:
53-
"""Download code from a Git repository and extract to the specified path"""
52+
def download_repository(
53+
url: str,
54+
target_path: Path,
55+
proxy: str | None = None,
56+
) -> None:
57+
"""Download repository source without requiring a local Git executable.
58+
59+
Args:
60+
url: Supported repository URL.
61+
target_path: Directory that will receive the repository source.
62+
proxy: Optional URL-prefix mirror for the archive request.
63+
64+
Raises:
65+
ValueError: If the repository URL is unsupported or invalid.
66+
httpx.HTTPError: If repository metadata or the archive cannot be downloaded.
67+
"""
68+
from astrbot.core.repository import GitHubRepository
69+
5470
temp_dir = Path(tempfile.mkdtemp())
5571
try:
56-
# Parse repository info
57-
repo_namespace = url.split("/")[-2:]
58-
author = repo_namespace[0]
59-
repo = repo_namespace[1]
60-
61-
# Try to get the latest release
62-
release_url = f"https://api.github.com/repos/{author}/{repo}/releases"
63-
try:
64-
with httpx.Client(
65-
proxy=proxy if proxy else None,
66-
follow_redirects=True,
67-
) as client:
68-
resp = client.get(release_url)
69-
resp.raise_for_status()
70-
releases = resp.json()
71-
72-
if releases:
73-
# Use the latest release
74-
download_url = releases[0]["zipball_url"]
75-
else:
76-
# No release found, use default branch
77-
click.echo(f"Downloading {author}/{repo} from default branch")
78-
download_url = f"https://github.com/{author}/{repo}/archive/refs/heads/master.zip"
79-
except Exception as e:
80-
click.echo(f"Failed to get release info: {e}. Using provided URL directly")
81-
download_url = url
72+
repository = GitHubRepository.parse(url)
73+
if not repository.branch:
74+
try:
75+
with httpx.Client(follow_redirects=True, trust_env=True) as client:
76+
response = client.get(repository.default_branch_api_url)
77+
response.raise_for_status()
78+
default_branch = str(
79+
response.json().get("default_branch") or ""
80+
).strip()
81+
except httpx.HTTPError as exc:
82+
default_branch = ""
83+
click.echo(
84+
f"Failed to resolve the default GitHub branch: {exc}. Trying main."
85+
)
86+
branch = default_branch or "main"
87+
repository = GitHubRepository(
88+
repository.owner,
89+
repository.name,
90+
branch,
91+
)
8292

83-
# Apply proxy
93+
click.echo(
94+
f"Downloading {repository.owner}/{repository.name} "
95+
f"from GitHub branch {repository.branch}"
96+
)
97+
download_url = repository.archive_url
8498
if proxy:
85-
download_url = f"{proxy}/{download_url}"
86-
87-
# Download and extract
88-
with httpx.Client(
89-
proxy=proxy if proxy else None,
90-
follow_redirects=True,
91-
) as client:
92-
resp = client.get(download_url)
93-
if (
94-
resp.status_code == 404
95-
and "archive/refs/heads/master.zip" in download_url
96-
):
97-
alt_url = download_url.replace("master.zip", "main.zip")
98-
click.echo("Branch 'master' not found, trying 'main' branch")
99-
resp = client.get(alt_url)
100-
resp.raise_for_status()
101-
else:
102-
resp.raise_for_status()
103-
zip_content = BytesIO(resp.content)
99+
download_url = f"{proxy.rstrip('/')}/{download_url}"
100+
101+
with httpx.Client(follow_redirects=True, trust_env=True) as client:
102+
response = client.get(download_url)
103+
response.raise_for_status()
104+
zip_content = BytesIO(response.content)
104105
with ZipFile(zip_content) as z:
105106
z.extractall(temp_dir)
106107
namelist = z.namelist()
@@ -328,7 +329,7 @@ def manage_plugin(
328329
click.echo(
329330
f"{'Updating' if is_update else 'Downloading'} plugin {plugin_name} from {repo_url}...",
330331
)
331-
get_git_repo(repo_url, target_path, proxy)
332+
download_repository(repo_url, target_path, proxy)
332333

333334
# Update succeeded, delete backup
334335
if is_update and backup_path is not None and backup_path.exists():

astrbot/core/core_lifecycle.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,14 @@
2929
from astrbot.core.pipeline.scheduler import PipelineContext, PipelineScheduler
3030
from astrbot.core.platform.manager import PlatformManager
3131
from astrbot.core.platform_message_history_mgr import PlatformMessageHistoryManager
32+
from astrbot.core.process_restart import restart_process
3233
from astrbot.core.provider.manager import ProviderManager
3334
from astrbot.core.star.context import Context
3435
from astrbot.core.star.star_handler import EventType, star_handlers_registry, star_map
3536
from astrbot.core.star.star_manager import PluginManager
3637
from astrbot.core.subagent_orchestrator import SubAgentOrchestrator
3738
from astrbot.core.umop_config_router import UmopConfigRouter
38-
from astrbot.core.updator import AstrBotUpdator
39+
from astrbot.core.updater import AstrBotUpdater
3940
from astrbot.core.utils.event_loop_diagnostics import (
4041
create_event_loop_diagnostic_tasks,
4142
)
@@ -157,7 +158,7 @@ def _warn_about_unset_default_chat_provider(self) -> None:
157158
async def initialize(self) -> None:
158159
"""初始化 AstrBot 核心生命周期管理类.
159160
160-
负责初始化各个组件, 包括 ProviderManager、PlatformManager、ConversationManager、PluginManager、PipelineScheduler、EventBus、AstrBotUpdator等
161+
负责初始化各个组件, 包括 ProviderManager、PlatformManager、ConversationManager、PluginManager、PipelineScheduler、EventBus、AstrBotUpdater等
161162
"""
162163
# 初始化日志代理
163164
logger.info("AstrBot v" + VERSION)
@@ -268,7 +269,7 @@ async def initialize(self) -> None:
268269
self.pipeline_scheduler_mapping = await self.load_pipeline_scheduler()
269270

270271
# 初始化更新器
271-
self.astrbot_updator = AstrBotUpdator()
272+
self.astrbot_updater = AstrBotUpdater()
272273

273274
# 初始化事件总线
274275
self.event_bus = EventBus(
@@ -427,7 +428,7 @@ async def restart(self) -> None:
427428
await self.kb_manager.terminate()
428429
self.dashboard_shutdown_event.set()
429430
threading.Thread(
430-
target=self.astrbot_updator._reboot,
431+
target=restart_process,
431432
name="restart",
432433
daemon=True,
433434
).start()

0 commit comments

Comments
 (0)