Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions astrbot/builtin_stars/builtin_commands/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .provider import ProviderCommands
from .setunset import SetUnsetCommands
from .sid import SIDCommand
from .update import UpdateCommands

__all__ = [
"AdminCommands",
Expand All @@ -16,4 +17,5 @@
"ProviderCommands",
"SetUnsetCommands",
"SIDCommand",
"UpdateCommands",
]
186 changes: 186 additions & 0 deletions astrbot/builtin_stars/builtin_commands/commands/update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
"""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
self._update_task: asyncio.Task | None = None

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:
# 在后台触发更新,让当前消息先发出去
# 保存任务引用,防止被 Python 垃圾回收机制提前销毁
self._update_task = 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}")
)
Comment on lines +92 to +101

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

update_now 中,使用 asyncio.create_task 在后台异步执行更新流程。但在 Python 的 asyncio 中,如果不对创建的 Task 对象保留强引用,该任务在执行完毕前可能会被垃圾回收器(Garbage Collector)提前销毁,导致更新流程意外中断。\n\n建议将返回的 Task 对象保存到实例属性中(例如 self._update_task),以确保其生命周期安全。

Suggested change
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}")
)
try:
# 在后台触发更新,让当前消息先发出去
task = asyncio.create_task(
self._trigger_update_with_delay(auto_update_mgr, version)
)
# 保存任务引用,防止被 Python 垃圾回收机制提前销毁
self._update_task = task
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)
41 changes: 41 additions & 0 deletions astrbot/builtin_stars/builtin_commands/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
ProviderCommands,
SetUnsetCommands,
SIDCommand,
UpdateCommands,
)


Expand All @@ -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:
Expand Down Expand Up @@ -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"
)
)
Loading