From bda621d24c786b70287a372d733d7a0f93b55354 Mon Sep 17 00:00:00 2001 From: Dr1985 <140971685+Dr1985@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:17:03 +0800 Subject: [PATCH 1/2] feat: add auto-update with backup, rollback, and retention Closes #8686 Summary Add an automatic update system to AstrBot that periodically checks for new versions, notifies admins, creates backups before updating, auto-rolls back on failure, and cleans up old backups after a configurable retention period. Users can choose between manual update (via chat command or WebUI) and automatic update (configurable toggle in Settings). --- .../builtin_commands/commands/__init__.py | 2 + .../builtin_commands/commands/update.py | 184 ++++++++++ .../builtin_stars/builtin_commands/main.py | 41 +++ astrbot/core/auto_update.py | 340 ++++++++++++++++++ astrbot/core/config/default.py | 7 + astrbot/core/core_lifecycle.py | 21 ++ astrbot/dashboard/services/update_service.py | 150 ++++++++ .../mdi-subset/materialdesignicons-subset.css | 6 +- .../materialdesignicons-webfont-subset.woff | Bin 19548 -> 19624 bytes .../materialdesignicons-webfont-subset.woff2 | Bin 15664 -> 15760 bytes .../i18n/locales/en-US/features/settings.json | 30 ++ .../i18n/locales/zh-CN/features/settings.json | 30 ++ dashboard/src/views/Settings.vue | 150 +++++++- 13 files changed, 959 insertions(+), 2 deletions(-) create mode 100644 astrbot/builtin_stars/builtin_commands/commands/update.py create mode 100644 astrbot/core/auto_update.py diff --git a/astrbot/builtin_stars/builtin_commands/commands/__init__.py b/astrbot/builtin_stars/builtin_commands/commands/__init__.py index 6a6f5f84bc..8b99bb9541 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/__init__.py +++ b/astrbot/builtin_stars/builtin_commands/commands/__init__.py @@ -7,6 +7,7 @@ from .provider import ProviderCommands from .setunset import SetUnsetCommands from .sid import SIDCommand +from .update import UpdateCommands __all__ = [ "AdminCommands", @@ -16,4 +17,5 @@ "ProviderCommands", "SetUnsetCommands", "SIDCommand", + "UpdateCommands", ] diff --git a/astrbot/builtin_stars/builtin_commands/commands/update.py b/astrbot/builtin_stars/builtin_commands/commands/update.py new file mode 100644 index 0000000000..a82bd34c31 --- /dev/null +++ b/astrbot/builtin_stars/builtin_commands/commands/update.py @@ -0,0 +1,184 @@ +"""Update commands for AstrBot. + +Provides /update subcommands: check, now, auto, status. +""" + +from __future__ import annotations + +import asyncio +from typing import TYPE_CHECKING + +from astrbot.api.event import MessageChain +from astrbot.core.config.default import VERSION + +if TYPE_CHECKING: + from astrbot.api.event import AstrMessageEvent + from astrbot.api.star import Context + + +class UpdateCommands: + """Chat commands for checking and triggering AstrBot updates.""" + + def __init__(self, context: Context) -> None: + self.context = context + + async def update_check(self, event: AstrMessageEvent) -> None: + """Check for new AstrBot versions.""" + await event.send(MessageChain().message("🔍 Checking for updates...")) + + auto_update_mgr = self._get_auto_update_manager() + if auto_update_mgr is None: + await event.send( + MessageChain().message("⚠️ Update manager is not available.") + ) + return + + try: + result = await auto_update_mgr.check_now() + except Exception as exc: + await event.send( + MessageChain().message(f"❌ Failed to check for updates: {exc}") + ) + return + + if result.get("error"): + await event.send( + MessageChain().message( + f"❌ Failed to check for updates: {result['error']}" + ) + ) + return + + if result["has_update"]: + msg = ( + f"🔔 A new version is available!\n" + f"Current: {result['current_version']}\n" + f"Latest: {result['latest_version']}\n" + f"Published: {result.get('published_at', 'N/A')}\n\n" + f"Use /update now to update." + ) + else: + msg = ( + f"✅ You are running the latest version.\n" + f"Current: {result['current_version']}" + ) + + await event.send(MessageChain().message(msg)) + + async def update_now( + self, event: AstrMessageEvent, version: str | None = None + ) -> None: + """Trigger an update immediately. + + Args: + version: Specific version tag to update to, or None for latest. + """ + auto_update_mgr = self._get_auto_update_manager() + if auto_update_mgr is None: + await event.send( + MessageChain().message("⚠️ Update manager is not available.") + ) + return + + await event.send( + MessageChain().message( + f"⏳ Starting update to {version or 'latest version'}...\n" + f"A backup will be created before the update.\n" + f"The bot will restart after the update is complete." + ) + ) + + try: + # 在后台触发更新,让当前消息先发出去 + asyncio.create_task( + self._trigger_update_with_delay(auto_update_mgr, version) + ) + except Exception as exc: + await event.send( + MessageChain().message(f"❌ Failed to trigger update: {exc}") + ) + + async def update_auto(self, event: AstrMessageEvent, action: str) -> None: + """Toggle auto-update on/off. + + Args: + action: "on" or "off". + """ + action = action.strip().lower() if action else "" + if action not in ("on", "off"): + await event.send( + MessageChain().message( + "Usage: /update auto on|off\n" + " on — Enable automatic updates\n" + " off — Disable automatic updates" + ) + ) + return + + enable = action == "on" + + # 更新运行时配置 + config = self.context.get_config() + if "auto_update" not in config: + config["auto_update"] = {} + config["auto_update"]["enabled"] = enable + + await event.send( + MessageChain().message( + f"{'✅' if enable else '🛑'} Auto-update is now " + f"{'ENABLED' if enable else 'DISABLED'}.\n" + + ( + "AstrBot will automatically check for and apply updates." + if enable + else "AstrBot will NOT automatically update itself." + ) + ) + ) + + async def update_status(self, event: AstrMessageEvent) -> None: + """Show current version and auto-update status.""" + auto_update_mgr = self._get_auto_update_manager() + config = self.context.get_config() + auto_cfg = config.get("auto_update", {}) + + enabled = auto_cfg.get("enabled", False) + check_interval_h = (auto_cfg.get("check_interval", 86400)) / 3600 + retention_days = auto_cfg.get("backup_retention_days", 14) + + new_version = None + if auto_update_mgr: + new_version = auto_update_mgr.get_new_version_info() + + msg = ( + f"📋 AstrBot Update Status\n" + f"Current version: {VERSION}\n" + f"Auto-update: {'✅ Enabled' if enabled else '⛔ Disabled'}\n" + f"Check interval: {check_interval_h:.0f}h\n" + f"Backup retention: {retention_days} days\n" + ) + if new_version: + msg += f"New version available: {new_version}\n" + + msg += "\nCommands:\n" + msg += " /update check — Check for new version\n" + msg += " /update now — Update now\n" + msg += " /update auto on|off — Toggle auto-update\n" + + await event.send(MessageChain().message(msg)) + + # ---- helpers ---- + + @staticmethod + async def _trigger_update_with_delay( + auto_update_mgr, version: str | None = None + ) -> None: + """Delay then trigger update, giving the chat message time to send.""" + await asyncio.sleep(3) + await auto_update_mgr.trigger_update(version) + + def _get_auto_update_manager(self): + """Get the AutoUpdateManager from core_lifecycle via star context.""" + core_lifecycle = getattr(self.context, "core_lifecycle", None) + if core_lifecycle is None: + return None + return getattr(core_lifecycle, "auto_update_manager", None) diff --git a/astrbot/builtin_stars/builtin_commands/main.py b/astrbot/builtin_stars/builtin_commands/main.py index 4c5ce3f8ca..a1cd7a161b 100644 --- a/astrbot/builtin_stars/builtin_commands/main.py +++ b/astrbot/builtin_stars/builtin_commands/main.py @@ -10,6 +10,7 @@ ProviderCommands, SetUnsetCommands, SIDCommand, + UpdateCommands, ) @@ -24,6 +25,7 @@ def __init__(self, context: star.Context) -> None: self.provider_c = ProviderCommands(self.context) self.setunset_c = SetUnsetCommands(self.context) self.sid_c = SIDCommand(self.context) + self.update_c = UpdateCommands(self.context) @filter.command("help") async def help(self, event: AstrMessageEvent) -> None: @@ -87,3 +89,42 @@ async def set_variable(self, event: AstrMessageEvent, key: str, value: str) -> N async def unset_variable(self, event: AstrMessageEvent, key: str) -> None: """Unset session variable""" await self.setunset_c.unset_variable(event, key) + + @filter.permission_type(filter.PermissionType.ADMIN) + @filter.command("update") + async def update( + self, + event: AstrMessageEvent, + subcommand: str | None = None, + arg: str | None = None, + ) -> None: + """Manage AstrBot updates. + + Subcommands: + check — Check for new versions + now [version] — Update immediately (default: latest) + auto on|off — Enable/disable automatic updates + status — Show current version and update settings + """ + sub = (subcommand or "").strip().lower() + + if sub == "check": + await self.update_c.update_check(event) + elif sub == "now": + await self.update_c.update_now(event, arg) + elif sub == "auto": + await self.update_c.update_auto(event, arg or "") + elif sub == "status" or not sub: + await self.update_c.update_status(event) + else: + from astrbot.api.event import MessageChain + + await event.send( + MessageChain().message( + "Unknown subcommand. Usage:\n" + " /update check — Check for new version\n" + " /update now [ver] — Update immediately\n" + " /update auto on|off — Toggle auto-update\n" + " /update status — Show status" + ) + ) diff --git a/astrbot/core/auto_update.py b/astrbot/core/auto_update.py new file mode 100644 index 0000000000..51aa8e4f64 --- /dev/null +++ b/astrbot/core/auto_update.py @@ -0,0 +1,340 @@ +"""AstrBot 自动更新模块 + +负责: +1. 定时检查新版本并通知管理员 +2. 定时清理过期的更新前备份 +3. 支持自动更新触发 +""" + +from __future__ import annotations + +import asyncio +import os +import time +from typing import TYPE_CHECKING + +from astrbot.api import logger +from astrbot.core.config.default import VERSION +from astrbot.core.utils.astrbot_path import get_astrbot_backups_path + +if TYPE_CHECKING: + from astrbot.core.core_lifecycle import AstrBotCoreLifecycle + from astrbot.core.updator import AstrBotUpdator + + +class AutoUpdateManager: + """自动更新管理器。 + + 在 AstrBot 启动时注册后台任务: + - 定时检查新版本 + - 定时清理过期备份 + """ + + def __init__(self, core_lifecycle: AstrBotCoreLifecycle) -> None: + self.core_lifecycle = core_lifecycle + self._check_task: asyncio.Task | None = None + self._cleanup_task: asyncio.Task | None = None + self._last_check_time: float = 0.0 + self._new_version_available: str | None = None + + @property + def config(self) -> dict: + return self.core_lifecycle.astrbot_config.get("auto_update", {}) + + @property + def enabled(self) -> bool: + return self.config.get("enabled", False) + + @property + def check_interval(self) -> int: + return self.config.get("check_interval", 86400) + + @property + def backup_retention_days(self) -> int: + return self.config.get("backup_retention_days", 14) + + @property + def notify_on_new_version(self) -> bool: + return self.config.get("notify_on_new_version", True) + + async def start_background_tasks(self) -> None: + """启动后台任务。""" + # 版本检查任务 + if self._check_task is None or self._check_task.done(): + self._check_task = asyncio.create_task(self._version_check_loop()) + + # 备份清理任务 + if self._cleanup_task is None or self._cleanup_task.done(): + self._cleanup_task = asyncio.create_task(self._backup_cleanup_loop()) + + logger.info("AutoUpdateManager background tasks started.") + + async def stop_background_tasks(self) -> None: + """停止后台任务。""" + for task in (self._check_task, self._cleanup_task): + if task and not task.done(): + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + # ---- 版本检查 ---- + + async def _version_check_loop(self) -> None: + """版本检查循环。按配置的间隔定时检查。""" + # 首次启动先等 60 秒,让服务完全就绪 + await asyncio.sleep(60) + + while True: + try: + await self._do_version_check() + except asyncio.CancelledError: + return + except Exception: + logger.warning( + "版本检查失败(不影响正常使用)。", + exc_info=True, + ) + + await asyncio.sleep(self.check_interval) + + async def _do_version_check(self) -> None: + """执行一次版本检查。""" + now = time.time() + # 去重:5 分钟内不重复检查 + if now - self._last_check_time < 300: + return + self._last_check_time = now + + updator = getattr(self.core_lifecycle, "astrbot_updator", None) + if not updator: + return + + try: + result = await updator.check_update(None, None, True) + except Exception: + return + + if result is not None: + new_ver = result.version + self._new_version_available = new_ver + logger.info(f"发现新版本: {new_ver}(当前: {VERSION})") + + if self.notify_on_new_version: + await self._notify_admins_new_version(new_ver) + + # 如果开启了自动更新,触发更新 + if self.enabled: + logger.info(f"自动更新已开启,将在 30 秒后开始更新到 {new_ver}...") + await asyncio.sleep(30) + await self._trigger_auto_update(new_ver) + else: + self._new_version_available = None + + async def _notify_admins_new_version(self, new_version: str) -> None: + """通知管理员有新版本可用。 + + 通过已连接的平台向管理员发送消息。 + """ + platform_manager = getattr(self.core_lifecycle, "platform_manager", None) + if not platform_manager: + return + + admin_ids = self.core_lifecycle.astrbot_config.get("admins_id", []) + message = ( + f"🔔 AstrBot 新版本可用\n" + f"当前版本: {VERSION}\n" + f"最新版本: {new_version}\n" + f"请在 WebUI 中更新,或使用 /update now 指令。" + ) + + for platform in platform_manager.platform_insts.values(): + try: + if hasattr(platform, "send_by_session"): + # 尝试向管理员发送通知 + for admin_id in admin_ids: + try: + await platform.send_by_session( + session_id=admin_id, + message_chain=message, + ) + except Exception: + pass # 单个管理员通知失败不影响其他 + except Exception: + pass + + async def _trigger_auto_update(self, target_version: str | None = None) -> None: + """触发自动更新流程。 + + 创建备份 → 下载更新 → 应用更新 → 重启。 + 如果更新过程中发生错误,自动回滚到备份。 + """ + from astrbot.core.backup.exporter import AstrBotExporter + from astrbot.core.backup.importer import AstrBotImporter + + backup_path: str | None = None + auto_backup = self.config.get("auto_backup_before_update", True) + + # 1. 更新前备份 + if auto_backup: + try: + logger.info("正在创建更新前备份...") + kb_manager = getattr(self.core_lifecycle, "kb_manager", None) + exporter = AstrBotExporter( + main_db=self.core_lifecycle.db, + kb_manager=kb_manager, + ) + backup_path = await exporter.export_all() + logger.info(f"更新前备份已创建: {backup_path}") + except Exception as backup_exc: + logger.warning(f"创建备份失败(继续更新): {backup_exc}") + + # 2. 下载并应用更新 + try: + logger.info(f"自动更新触发,目标版本: {target_version or 'latest'}") + updator: AstrBotUpdator = self.core_lifecycle.astrbot_updator + await updator.update( + reboot=True, + latest=target_version is None, + version=target_version, + ) + except Exception as exc: + logger.error(f"自动更新失败: {exc}", exc_info=True) + + # 3. 自动回滚 + if backup_path and os.path.exists(backup_path): + try: + logger.info("正在从备份恢复...") + kb_manager = getattr(self.core_lifecycle, "kb_manager", None) + importer = AstrBotImporter( + main_db=self.core_lifecycle.db, + kb_manager=kb_manager, + ) + result = await importer.import_all( + zip_path=backup_path, + mode="replace", + ) + if result.success: + logger.info("已从备份恢复数据。") + else: + logger.error(f"从备份恢复失败: {'; '.join(result.errors)}") + except Exception as rollback_exc: + logger.error( + f"自动回滚失败,请手动恢复备份: {backup_path}。" + f"错误: {rollback_exc}" + ) + raise + + # ---- 备份清理 ---- + + async def _backup_cleanup_loop(self) -> None: + """备份清理循环。每小时检查一次。""" + await asyncio.sleep(120) # 启动后等 2 分钟 + + while True: + try: + await self._cleanup_old_backups() + except asyncio.CancelledError: + return + except Exception: + logger.warning("备份清理失败。", exc_info=True) + + await asyncio.sleep(3600) # 每小时 + + async def _cleanup_old_backups(self) -> None: + """清理超过保留期限的更新备份。""" + backup_dir = get_astrbot_backups_path() + if not os.path.isdir(backup_dir): + return + + retention_seconds = self.backup_retention_days * 86400 + now = time.time() + deleted_count = 0 + + # 收集更新备份文件 + update_backups: list[tuple[str, float]] = [] + for filename in os.listdir(backup_dir): + if not filename.endswith(".zip"): + continue + # 只清理更新前自动创建的备份 + if "update_backup" not in filename: + continue + file_path = os.path.join(backup_dir, filename) + try: + mtime = os.path.getmtime(file_path) + update_backups.append((file_path, mtime)) + except OSError: + pass + + if len(update_backups) <= 1: + return # 保留至少一个备份 + + # 按时间排序(新的在前) + update_backups.sort(key=lambda x: x[1], reverse=True) + + # 保留最近一个,其余过期的删除 + for file_path, mtime in update_backups[1:]: + if now - mtime > retention_seconds: + try: + os.remove(file_path) + deleted_count += 1 + logger.info(f"已删除过期更新备份: {os.path.basename(file_path)}") + except OSError as exc: + logger.warning(f"删除过期备份失败 {file_path}: {exc}") + + if deleted_count > 0: + logger.info(f"备份清理完成,删除了 {deleted_count} 个过期备份。") + + # ---- 公共 API ---- + + async def check_now(self) -> dict: + """立即检查更新(供聊天指令调用)。 + + Returns: + dict: 包含 has_update, current_version, latest_version 等信息。 + """ + updator = getattr(self.core_lifecycle, "astrbot_updator", None) + if not updator: + return { + "has_update": False, + "current_version": VERSION, + "error": "更新器不可用", + } + + try: + result = await updator.check_update(None, None, True) + except Exception as exc: + return { + "has_update": False, + "current_version": VERSION, + "error": str(exc), + } + + if result is not None: + self._new_version_available = result.version + return { + "has_update": True, + "current_version": VERSION, + "latest_version": result.version, + "published_at": result.published_at, + "body": result.body, + } + + return { + "has_update": False, + "current_version": VERSION, + "latest_version": VERSION, + } + + async def trigger_update(self, version: str | None = None) -> None: + """触发更新流程。 + + Args: + version: 目标版本,None 表示最新版本。 + """ + await self._trigger_auto_update(version) + + def get_new_version_info(self) -> str | None: + """获取已知的新版本号。""" + return self._new_version_available diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 9d6d8fe5d0..5256576115 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -272,6 +272,13 @@ "ca_certs": "", }, }, + "auto_update": { + "enabled": False, + "check_interval": 86400, # 检查间隔(秒),默认 24 小时 + "backup_retention_days": 14, # 更新备份保留天数 + "auto_backup_before_update": True, # 更新前自动备份 + "notify_on_new_version": True, # 发现新版本时通知管理员 + }, "platform": [], "platform_specific": { # 平台特异配置:按平台分类,平台下按功能分组 diff --git a/astrbot/core/core_lifecycle.py b/astrbot/core/core_lifecycle.py index c325a2ea38..85d3477ae6 100644 --- a/astrbot/core/core_lifecycle.py +++ b/astrbot/core/core_lifecycle.py @@ -19,6 +19,7 @@ from astrbot.api import logger, sp from astrbot.core import LogBroker, LogManager from astrbot.core.astrbot_config_mgr import AstrBotConfigManager +from astrbot.core.auto_update import AutoUpdateManager from astrbot.core.config.default import VERSION from astrbot.core.conversation_mgr import ConversationManager from astrbot.core.cron import CronJobManager @@ -59,6 +60,7 @@ def __init__(self, log_broker: LogBroker, db: BaseDatabase) -> None: self.subagent_orchestrator: SubAgentOrchestrator | None = None self.cron_manager: CronJobManager | None = None self.temp_dir_cleaner: TempDirCleaner | None = None + self.auto_update_manager: AutoUpdateManager | None = None self._default_chat_provider_warning_emitted = False # 设置代理 @@ -236,6 +238,9 @@ async def initialize(self) -> None: self.subagent_orchestrator, ) + # 把 core_lifecycle 引用暴露给 StarContext,供插件访问 + self.star_context.core_lifecycle = self + # 初始化插件管理器 self.plugin_manager = PluginManager(self.star_context, self.astrbot_config) @@ -255,6 +260,9 @@ async def initialize(self) -> None: # 初始化更新器 self.astrbot_updator = AstrBotUpdator() + # 初始化自动更新管理器 + self.auto_update_manager = AutoUpdateManager(self) + # 初始化事件总线 self.event_bus = EventBus( self.event_queue, @@ -297,6 +305,14 @@ def _load(self) -> None: name="temp_dir_cleaner", ) + # 启动自动更新管理器(版本检查 + 备份清理) + auto_update_task = None + if self.auto_update_manager: + auto_update_task = asyncio.create_task( + self.auto_update_manager.start_background_tasks(), + name="auto_update_manager", + ) + # 把插件中注册的所有协程函数注册到事件总线中并执行 extra_tasks = [] for task in self.star_context._register_tasks: @@ -307,6 +323,8 @@ def _load(self) -> None: tasks_.append(cron_task) if temp_dir_cleaner_task: tasks_.append(temp_dir_cleaner_task) + if auto_update_task: + tasks_.append(auto_update_task) for task in tasks_: self.curr_tasks.append( asyncio.create_task(self._task_wrapper(task), name=task.get_name()), @@ -361,6 +379,9 @@ async def stop(self) -> None: if self.temp_dir_cleaner: await self.temp_dir_cleaner.stop() + if self.auto_update_manager: + await self.auto_update_manager.stop_background_tasks() + # 请求停止所有正在运行的异步任务 for task in self.curr_tasks: task.cancel() diff --git a/astrbot/dashboard/services/update_service.py b/astrbot/dashboard/services/update_service.py index f0cdaf7868..af7148b86b 100644 --- a/astrbot/dashboard/services/update_service.py +++ b/astrbot/dashboard/services/update_service.py @@ -13,6 +13,8 @@ from astrbot.core import DEMO_MODE as _DEMO_MODE from astrbot.core import logger from astrbot.core import pip_installer as _pip_installer +from astrbot.core.backup.exporter import AstrBotExporter +from astrbot.core.backup.importer import AstrBotImporter from astrbot.core.config.default import VERSION from astrbot.core.core_lifecycle import AstrBotCoreLifecycle from astrbot.core.db.migration.helper import ( @@ -23,6 +25,7 @@ ) from astrbot.core.updator import AstrBotUpdator from astrbot.core.utils.astrbot_path import ( + get_astrbot_backups_path, get_astrbot_data_path, get_astrbot_system_tmp_path, ) @@ -112,6 +115,7 @@ def __init__( self.demo_mode = demo_mode self.clear_site_data_headers = clear_site_data_headers self.update_progress: dict[str, dict] = {} + self._last_update_backup_path: str | None = None def get_update_progress(self, progress_id: str) -> UpdateServiceResult: if not progress_id: @@ -199,6 +203,19 @@ async def update_project(self, data: object) -> UpdateServiceResult: update_token = uuid.uuid4().hex dashboard_zip_path = update_temp_dir / f"{update_token}-dashboard.zip" core_zip_path = update_temp_dir / f"{update_token}-core.zip" + + # 更新前自动备份 + backup_path: str | None = None + config = self.core_lifecycle.astrbot_config + auto_backup = config.get("auto_update", {}).get( + "auto_backup_before_update", True + ) + if auto_backup: + try: + backup_path = await self._create_backup(progress_id) + except Exception as exc: + logger.warning(f"更新前备份失败(继续更新): {exc}") + try: self._set_update_stage( progress_id, @@ -357,6 +374,41 @@ def _verify_update_packages() -> None: }, ) logger.error(f"/api/update_project: {traceback.format_exc()}") + + # 自动回滚:如果更新前创建了备份,尝试恢复 + if backup_path: + self._set_update_stage( + progress_id, + "rollback", + "running", + "更新失败,正在自动回滚到更新前的状态...", + 0, + ) + try: + await self._restore_backup(backup_path) + self._set_update_stage( + progress_id, + "rollback", + "done", + "已自动回滚到更新前的状态。", + 100, + ) + logger.info("更新失败后已自动回滚。") + except Exception as rollback_exc: + logger.error( + f"自动回滚失败: {rollback_exc},请手动从备份恢复: {backup_path}" + ) + self.update_progress[progress_id].update( + { + "status": "error", + "stage": "rollback", + "message": ( + f"自动回滚失败,请手动从备份恢复。" + f"备份路径: {backup_path}" + ), + }, + ) + raise UpdateServiceError(exc.__str__()) from exc finally: for zip_path in (dashboard_zip_path, core_zip_path): @@ -401,6 +453,104 @@ async def install_pip_package(self, data: object) -> UpdateServiceResult: logger.error(f"/api/update_pip: {traceback.format_exc()}") raise UpdateServiceError(exc.__str__()) from exc + async def _create_backup(self, progress_id: str) -> str: + """在更新前创建完整备份。 + + Returns: + str: 备份文件路径。 + + Raises: + Exception: 备份创建失败时抛出。 + """ + import os + from datetime import datetime + + self._set_update_stage( + progress_id, + "backup", + "running", + "正在创建更新前备份...", + 0, + ) + + kb_manager = getattr(self.core_lifecycle, "kb_manager", None) + data_dir = get_astrbot_data_path() + config_path = os.path.join(data_dir, "cmd_config.json") + backup_dir = get_astrbot_backups_path() + + exporter = AstrBotExporter( + main_db=self.core_lifecycle.db, + kb_manager=kb_manager, + config_path=config_path, + ) + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + version_str = getattr( + __import__("astrbot.core.config.default", fromlist=["VERSION"]), + "VERSION", + "pre_update", + ) + backup_filename = f"astrbot_update_backup_v{version_str}_{timestamp}.zip" + + # 直接导出,使用自定义文件名 + zip_path = os.path.join(backup_dir, backup_filename) + zip_path = await exporter.export_all(output_dir=backup_dir) + + # 重命名为带版本标记的文件名 + if os.path.basename(zip_path) != backup_filename: + final_path = os.path.join(backup_dir, backup_filename) + os.rename(zip_path, final_path) + zip_path = final_path + + self._last_update_backup_path = zip_path + + self._set_update_stage( + progress_id, + "backup", + "done", + f"更新前备份完成: {backup_filename}", + 5, + ) + logger.info(f"更新前备份已创建: {zip_path}") + return zip_path + + async def _restore_backup(self, backup_path: str) -> None: + """从备份恢复数据。 + + Args: + backup_path: 备份 ZIP 文件路径。 + """ + import os + + if not os.path.exists(backup_path): + raise FileNotFoundError(f"备份文件不存在: {backup_path}") + + kb_manager = getattr(self.core_lifecycle, "kb_manager", None) + data_dir = get_astrbot_data_path() + config_path = os.path.join(data_dir, "cmd_config.json") + + importer = AstrBotImporter( + main_db=self.core_lifecycle.db, + kb_manager=kb_manager, + config_path=config_path, + ) + + # 先做前置检查 + pre_check = importer.pre_check(backup_path) + if not pre_check.can_import: + raise UpdateServiceError(f"备份文件无法导入: {'; '.join(pre_check.errors)}") + + # 执行导入(replace 模式:清空现有数据后导入) + result = await importer.import_all( + zip_path=backup_path, + mode="replace", + ) + + if not result.success: + raise UpdateServiceError(f"从备份恢复失败: {'; '.join(result.errors)}") + + logger.info(f"已从备份恢复数据: {backup_path}") + def _init_update_progress(self, progress_id: str, version: str) -> None: self.update_progress[progress_id] = { "id": progress_id, diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css b/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css index 406c62a36c..ae5d40f5bb 100644 --- a/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css +++ b/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css @@ -1,4 +1,4 @@ -/* Auto-generated MDI subset – 274 icons */ +/* Auto-generated MDI subset – 275 icons */ /* Do not edit manually. Run: pnpm run subset-icons */ @font-face { @@ -248,6 +248,10 @@ content: "\F015A"; } +.mdi-cloud-search::before { + content: "\F0956"; +} + .mdi-cloud-upload::before { content: "\F0167"; } diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff index 37b58eaee453baa2001abb6fb91e37e83b0a0e70..1d76cfdc4e271b84e6fa3032df8cafe24772eb8a 100644 GIT binary patch delta 18964 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 ?liCUS^wW*)0RC3mkS)pX==hU8y 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 W#pP=)SD7h1pDsKPdU|-r761Sfe^fL8 diff --git a/dashboard/src/i18n/locales/en-US/features/settings.json b/dashboard/src/i18n/locales/en-US/features/settings.json index 378e2d7674..44799dd076 100644 --- a/dashboard/src/i18n/locales/en-US/features/settings.json +++ b/dashboard/src/i18n/locales/en-US/features/settings.json @@ -78,6 +78,36 @@ } } }, + "autoUpdate": { + "title": "Auto Update", + "enabled": { + "title": "Automatic Updates", + "subtitle": "When enabled, AstrBot will automatically download and apply updates when a new version is detected. A backup is created before each update, and the bot will rollback automatically on failure." + }, + "checkInterval": { + "title": "Check Interval (hours)", + "subtitle": "How often to check for new versions." + }, + "backupRetention": { + "title": "Backup Retention (days)", + "subtitle": "Update backups older than this will be automatically deleted. The most recent backup is always kept." + }, + "autoBackup": { + "title": "Backup Before Update", + "subtitle": "Create a full data backup before applying any update." + }, + "notify": { + "title": "New Version Notification", + "subtitle": "Send a notification to admins via IM platforms when a new version is detected." + }, + "checkNow": "Check Now", + "updateNow": "Update Now", + "actions": { + "save": "Save Settings", + "saved": "Saved", + "saveFailed": "Save failed" + } + }, "sidebar": { "title": "Sidebar", "customize": { diff --git a/dashboard/src/i18n/locales/zh-CN/features/settings.json b/dashboard/src/i18n/locales/zh-CN/features/settings.json index de5748f2c6..9bc0ed6db7 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/settings.json +++ b/dashboard/src/i18n/locales/zh-CN/features/settings.json @@ -78,6 +78,36 @@ } } }, + "autoUpdate": { + "title": "自动更新", + "enabled": { + "title": "启用自动更新", + "subtitle": "开启后,AstrBot 会在检测到新版本时自动下载并应用更新。每次更新前会自动备份数据,更新失败时自动回滚。" + }, + "checkInterval": { + "title": "检查间隔(小时)", + "subtitle": "检查新版本的频率。" + }, + "backupRetention": { + "title": "备份保留(天)", + "subtitle": "超过此天数的更新备份将被自动清理,始终保留最近一个备份。" + }, + "autoBackup": { + "title": "更新前自动备份", + "subtitle": "在应用任何更新之前创建完整数据备份。" + }, + "notify": { + "title": "新版本通知", + "subtitle": "检测到新版本时通过消息平台通知管理员。" + }, + "checkNow": "立即检查", + "updateNow": "立即更新", + "actions": { + "save": "保存设置", + "saved": "已保存", + "saveFailed": "保存失败" + } + }, "sidebar": { "title": "侧边栏", "customize": { diff --git a/dashboard/src/views/Settings.vue b/dashboard/src/views/Settings.vue index cbacacd39e..ae395abb1e 100644 --- a/dashboard/src/views/Settings.vue +++ b/dashboard/src/views/Settings.vue @@ -67,6 +67,84 @@ + {{ tm('autoUpdate.title') }} + + + + + + + + + + + + + + + + + + + + + + +
+ + mdi-cloud-search + {{ tm('autoUpdate.checkNow') }} + +
+
+ {{ tm('apiKey.title') }} @@ -228,7 +306,7 @@ From 3f8d741868b2b05f5cfb4a58c71c4dd58d85dae8 Mon Sep 17 00:00:00 2001 From: Dr1985 <140971685+Dr1985@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:57:26 +0800 Subject: [PATCH 2/2] fix: address PR #8814 review feedback from Sourcery and Gemini Code Assist - Fix backup filename mismatch: rename auto-update backups to include 'update_backup' so cleanup logic can match them - Add input validation for check_interval and backup_retention_days to prevent NaN/zero values causing infinite loops - Change backupRetention min from 1 to 0 in Settings.vue (0 is valid for immediate expiration testing) - Add NaN/empty value guards in saveAutoUpdateConfig with Math.max/Number fallbacks - Replace hardcoded English toast messages in checkForUpdate with i18n translations (en-US + zh-CN) - Fix misleading version display in checkForUpdate toast (data.version is current, not latest) - Save asyncio.create_task reference in update.py to prevent premature GC - Remove redundant __import__ dynamic import of VERSION in update_service.py, use already-imported constant Co-Authored-By: Claude Opus 4.8 --- .../builtin_commands/commands/update.py | 4 +++- astrbot/core/auto_update.py | 24 ++++++++++++++++--- astrbot/dashboard/services/update_service.py | 7 +----- .../i18n/locales/en-US/features/settings.json | 6 +++++ .../i18n/locales/zh-CN/features/settings.json | 6 +++++ dashboard/src/views/Settings.vue | 12 +++++----- 6 files changed, 43 insertions(+), 16 deletions(-) diff --git a/astrbot/builtin_stars/builtin_commands/commands/update.py b/astrbot/builtin_stars/builtin_commands/commands/update.py index a82bd34c31..d6bb9f23b2 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/update.py +++ b/astrbot/builtin_stars/builtin_commands/commands/update.py @@ -21,6 +21,7 @@ class UpdateCommands: def __init__(self, context: Context) -> None: self.context = context + self._update_task: asyncio.Task | None = None async def update_check(self, event: AstrMessageEvent) -> None: """Check for new AstrBot versions.""" @@ -90,7 +91,8 @@ async def update_now( try: # 在后台触发更新,让当前消息先发出去 - asyncio.create_task( + # 保存任务引用,防止被 Python 垃圾回收机制提前销毁 + self._update_task = asyncio.create_task( self._trigger_update_with_delay(auto_update_mgr, version) ) except Exception as exc: diff --git a/astrbot/core/auto_update.py b/astrbot/core/auto_update.py index 51aa8e4f64..b4619b1c6c 100644 --- a/astrbot/core/auto_update.py +++ b/astrbot/core/auto_update.py @@ -47,11 +47,21 @@ def enabled(self) -> bool: @property def check_interval(self) -> int: - return self.config.get("check_interval", 86400) + val = self.config.get("check_interval", 86400) + try: + val = int(val) + return val if val > 0 else 86400 + except (TypeError, ValueError): + return 86400 @property def backup_retention_days(self) -> int: - return self.config.get("backup_retention_days", 14) + val = self.config.get("backup_retention_days", 14) + try: + val = int(val) + return val if val >= 0 else 14 + except (TypeError, ValueError): + return 14 @property def notify_on_new_version(self) -> bool: @@ -185,7 +195,15 @@ async def _trigger_auto_update(self, target_version: str | None = None) -> None: main_db=self.core_lifecycle.db, kb_manager=kb_manager, ) - backup_path = await exporter.export_all() + raw_backup_path = await exporter.export_all() + # 重命名为含 "update_backup" 标记的文件名,以便清理逻辑正确识别 + backup_dir = os.path.dirname(raw_backup_path) + timestamp = time.strftime("%Y%m%d_%H%M%S") + backup_filename = ( + f"astrbot_update_backup_v{VERSION}_{timestamp}.zip" + ) + backup_path = os.path.join(backup_dir, backup_filename) + os.rename(raw_backup_path, backup_path) logger.info(f"更新前备份已创建: {backup_path}") except Exception as backup_exc: logger.warning(f"创建备份失败(继续更新): {backup_exc}") diff --git a/astrbot/dashboard/services/update_service.py b/astrbot/dashboard/services/update_service.py index af7148b86b..550412228c 100644 --- a/astrbot/dashboard/services/update_service.py +++ b/astrbot/dashboard/services/update_service.py @@ -485,12 +485,7 @@ async def _create_backup(self, progress_id: str) -> str: ) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - version_str = getattr( - __import__("astrbot.core.config.default", fromlist=["VERSION"]), - "VERSION", - "pre_update", - ) - backup_filename = f"astrbot_update_backup_v{version_str}_{timestamp}.zip" + backup_filename = f"astrbot_update_backup_v{VERSION}_{timestamp}.zip" # 直接导出,使用自定义文件名 zip_path = os.path.join(backup_dir, backup_filename) diff --git a/dashboard/src/i18n/locales/en-US/features/settings.json b/dashboard/src/i18n/locales/en-US/features/settings.json index 44799dd076..322c0aedb6 100644 --- a/dashboard/src/i18n/locales/en-US/features/settings.json +++ b/dashboard/src/i18n/locales/en-US/features/settings.json @@ -102,6 +102,12 @@ }, "checkNow": "Check Now", "updateNow": "Update Now", + "checkForUpdate": { + "checking": "Checking for updates...", + "latest": "You are running the latest version.", + "newVersionAvailable": "A new version is available! (Current version: {version})", + "failed": "Failed to check for updates" + }, "actions": { "save": "Save Settings", "saved": "Saved", diff --git a/dashboard/src/i18n/locales/zh-CN/features/settings.json b/dashboard/src/i18n/locales/zh-CN/features/settings.json index 9bc0ed6db7..6ce8eaa03b 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/settings.json +++ b/dashboard/src/i18n/locales/zh-CN/features/settings.json @@ -102,6 +102,12 @@ }, "checkNow": "立即检查", "updateNow": "立即更新", + "checkForUpdate": { + "checking": "正在检查更新...", + "latest": "您正在运行最新版本。", + "newVersionAvailable": "发现新版本!(当前版本:{version})", + "failed": "检查更新失败" + }, "actions": { "save": "保存设置", "saved": "已保存", diff --git a/dashboard/src/views/Settings.vue b/dashboard/src/views/Settings.vue index ae395abb1e..452a309e6f 100644 --- a/dashboard/src/views/Settings.vue +++ b/dashboard/src/views/Settings.vue @@ -99,7 +99,7 @@ { const config = res.data?.data ?? res.data ?? {}; config.auto_update = { enabled: autoUpdateEnabled.value, - check_interval: autoUpdateCheckInterval.value * 3600, - backup_retention_days: autoUpdateBackupRetention.value, + check_interval: Math.max(1, Number(autoUpdateCheckInterval.value) || 24) * 3600, + backup_retention_days: Math.max(0, Number(autoUpdateBackupRetention.value) || 14), auto_backup_before_update: autoUpdateBackupBefore.value, notify_on_new_version: autoUpdateNotify.value, }; @@ -388,14 +388,14 @@ const checkForUpdate = async () => { const data = res.data?.data ?? res.data ?? {}; if (data.has_new_version) { showToast( - `New version available: ${data.version || 'unknown'} (current: ${data.dashboard_version || 'unknown'})`, + tm('autoUpdate.checkForUpdate.newVersionAvailable').replace('{version}', data.version || 'unknown'), 'info', ); } else { - showToast('You are running the latest version.', 'success'); + showToast(tm('autoUpdate.checkForUpdate.latest'), 'success'); } } catch (e) { - showToast(e?.response?.data?.message || 'Failed to check for updates', 'error'); + showToast(e?.response?.data?.message || tm('autoUpdate.checkForUpdate.failed'), 'error'); } finally { checkingUpdate.value = false; }