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
41 changes: 41 additions & 0 deletions astrbot/core/config/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,47 @@
"kb_agentic_mode": False,
"disable_builtin_commands": False,
"disable_metrics": False,
"auto_update": {
"description": "自动更新设置",
"type": "object",
"properties": {
"enabled": {
"description": "启用自动更新",
"type": "bool",
"hint": "开启后,AstrBot 将定期检查并自动安装更新。",
},
"cron_expression": {
"description": "检查更新的 Cron 表达式",
"type": "string",
"hint": "默认为每天凌晨 3 点检查。",
},
"auto_install": {
"description": "自动安装更新",
"type": "bool",
"hint": "关闭时仅通知有新版本,不自动安装。",
},
"backup_before_update": {
"description": "更新前自动备份",
"type": "bool",
"hint": "开启后,更新前会自动创建完整数据备份。",
},
"backup_retention_days": {
"description": "备份保留天数",
"type": "int",
"hint": "自动更新创建的备份将在此天数后被自动删除。默认 14 天。",
},
"consider_prerelease": {
"description": "包含预发布版本",
"type": "bool",
"hint": "开启后,也会检查 alpha/beta/rc 等预发布版本。",
},
"timezone": {
"description": "时区",
"type": "string",
"hint": "用于 Cron 调度的时区,例如 Asia/Shanghai。留空使用系统时区。",
},
},
},
Comment on lines +311 to +351

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.

high

DEFAULT_CONFIG 中,应该只定义配置项的默认值,而不是配置项的元数据/Schema(如 descriptiontypeproperties 等)。

如果将 Schema 直接放入 DEFAULT_CONFIG,会导致 astrbot_config.get("auto_update") 返回 Schema 字典,而不是实际的默认配置值。例如,当代码尝试获取 enabled 时,会得到一个表示 Schema 的字典(在 Python 中被评估为 True),从而导致逻辑错误或类型崩溃。

解决方案

  1. DEFAULT_CONFIG 中仅保留默认值。
  2. auto_update 的 Schema 元数据移至 CONFIG_METADATA_3_SYSTEM(系统配置组)中。
    "auto_update": {
        "enabled": False,
        "cron_expression": "0 3 * * *",
        "auto_install": False,
        "backup_before_update": True,
        "backup_retention_days": 14,
        "consider_prerelease": False,
        "timezone": None,
    },

}


Expand Down
24 changes: 24 additions & 0 deletions astrbot/core/core_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
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.backup.exporter import AstrBotExporter
from astrbot.core.backup.importer import AstrBotImporter
from astrbot.core.config.default import VERSION
from astrbot.core.conversation_mgr import ConversationManager
from astrbot.core.cron import CronJobManager
Expand Down Expand Up @@ -58,6 +61,7 @@ def __init__(self, log_broker: LogBroker, db: BaseDatabase) -> None:

self.subagent_orchestrator: SubAgentOrchestrator | None = None
self.cron_manager: CronJobManager | None = None
self.auto_update_manager: AutoUpdateManager | None = None
self.temp_dir_cleaner: TempDirCleaner | None = None
self._default_chat_provider_warning_emitted = False

Expand Down Expand Up @@ -255,6 +259,26 @@ async def initialize(self) -> None:
# 初始化更新器
self.astrbot_updator = AstrBotUpdator()

# 初始化自动更新管理器
exporter = AstrBotExporter(
main_db=self.db,
kb_manager=self.kb_manager,
)
importer = AstrBotImporter(
main_db=self.db,
kb_manager=self.kb_manager,
)
self.auto_update_manager = AutoUpdateManager(
core_lifecycle=self,
db=self.db,
astrbot_config=self.astrbot_config,
updator=self.astrbot_updator,
cron_manager=self.cron_manager,
exporter=exporter,
importer=importer,
)
await self.auto_update_manager.initialize()

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

自动更新管理器(AutoUpdateManager)的初始化属于辅助功能。如果其初始化过程中抛出异常(例如:Cron 表达式解析失败、时区配置错误或数据库读取异常),直接 await 会导致整个 AstrBot 核心生命周期初始化失败,从而使机器人无法启动。

建议将 await self.auto_update_manager.initialize() 包裹在 try-except 块中,捕获异常并记录错误日志,以确保即使自动更新功能初始化失败,机器人的核心功能仍能正常启动。

        try:
            await self.auto_update_manager.initialize()
        except Exception as e:
            logger.error(f"Failed to initialize AutoUpdateManager: {e}", exc_info=True)


# 初始化事件总线
self.event_bus = EventBus(
self.event_queue,
Expand Down
21 changes: 21 additions & 0 deletions astrbot/core/tools/message_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,27 @@ async def call(
else:
return f"error: invalid session: {session}"

# Apply forward_threshold for aiocqhttp platform.
# The normal reply path applies this check in ResultDecorateStage,
# but tool-sent messages bypass the pipeline, so we replicate it here.
# See https://github.com/AstrBotDevs/AstrBot/issues/8678
if target_session.platform_name == "aiocqhttp":
cfg = context.context.context.get_config(
umo=context.context.event.unified_msg_origin
)
threshold = cfg.get("platform_settings", {}).get("forward_threshold", 1500)
word_cnt = 0
for comp in components:
if isinstance(comp, Comp.Plain):
word_cnt += len(comp.text)
if word_cnt > threshold:
node = Comp.Node(
uin=context.context.event.get_self_id(),
name="AstrBot",
content=[*components],
)
components = [node]

await context.context.context.send_message(
target_session,
MessageChain(chain=components),
Expand Down
146 changes: 146 additions & 0 deletions astrbot/dashboard/routes/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,30 @@ def __init__(
"/update/dashboard": ("POST", self.update_dashboard),
"/update/pip-install": ("POST", self.install_pip_package),
"/update/migration": ("POST", self.do_migration),
"/update/auto-update/settings": (
"GET",
self.get_auto_update_settings,
),
"/update/auto-update/settings/update": (
"POST",
self.update_auto_update_settings,
),
"/update/auto-update/check": (
"POST",
self.trigger_auto_update_check,
),
"/update/auto-update/history": (
"GET",
self.get_auto_update_history,
),
"/update/auto-update/backups": (
"GET",
self.get_auto_update_backups,
),
"/update/auto-update/rollback": (
"POST",
self.rollback_auto_update,
),
}
self.astrbot_updator = astrbot_updator
self.core_lifecycle = core_lifecycle
Expand Down Expand Up @@ -352,3 +376,125 @@ async def install_pip_package(self):
except Exception as e:
logger.error(f"/api/update_pip: {traceback.format_exc()}")
return Response().error(e.__str__()).__dict__

async def _get_auto_update_mgr(self):
"""Get the auto-update manager, returning an error response if unavailable."""
mgr = getattr(self.core_lifecycle, "auto_update_manager", None)
if not mgr:
return None, Response().error(
"Auto-update manager is not available."
).__dict__
return mgr, None

async def get_auto_update_settings(self):
"""Get current auto-update configuration."""
mgr, err = await self._get_auto_update_mgr()
if err:
return err
try:
settings = await mgr.get_settings()
return Response().ok(settings).__dict__
except Exception as e:
logger.error(
f"/api/update/auto-update/settings GET: {traceback.format_exc()}"
)
return Response().error(str(e)).__dict__

async def update_auto_update_settings(self):
"""Update auto-update configuration."""
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode")
.__dict__
)
mgr, err = await self._get_auto_update_mgr()
if err:
return err
try:
data = await request.json
if not data:
return Response().error("Missing request body.").__dict__
settings = await mgr.update_settings(data)
return Response().ok(settings, "Auto-update settings updated.").__dict__
except Exception as e:
logger.error(
f"/api/update/auto-update/settings POST: {traceback.format_exc()}"
)
return Response().error(str(e)).__dict__
Comment thread
sourcery-ai[bot] marked this conversation as resolved.

async def trigger_auto_update_check(self):
"""Trigger an immediate update check."""
mgr, err = await self._get_auto_update_mgr()
if err:
return err
try:
result = await mgr.trigger_manual_check()
return Response().ok(result, "Update check completed.").__dict__
except Exception as e:
logger.error(f"/api/update/auto-update/check: {traceback.format_exc()}")
return Response().error(str(e)).__dict__

async def get_auto_update_history(self):
"""Get auto-update event history."""
mgr, err = await self._get_auto_update_mgr()
if err:
return err
try:
limit = request.args.get("limit", 20, type=int)
history = await mgr.get_history(limit=limit)
return Response().ok(history).__dict__
except Exception as e:
logger.error(f"/api/update/auto-update/history: {traceback.format_exc()}")
return Response().error(str(e)).__dict__

async def get_auto_update_backups(self):
"""List available backup files for rollback."""
mgr, err = await self._get_auto_update_mgr()
if err:
return err
try:
backups = await mgr.get_backup_files()
return Response().ok(backups).__dict__
except Exception as e:
logger.error(f"/api/update/auto-update/backups: {traceback.format_exc()}")
return Response().error(str(e)).__dict__

async def rollback_auto_update(self):
"""Manually rollback to a specified backup."""
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode")
.__dict__
)
mgr, err = await self._get_auto_update_mgr()
if err:
return err
try:
data = await request.json
if not data:
return Response().error("Missing request body.").__dict__
backup_path = data.get("backup_path", "")
if not backup_path:
return Response().error("Missing backup_path parameter.").__dict__

import os
from astrbot.core.utils.astrbot_path import get_astrbot_backups_path

# 安全校验:仅提取文件名,防止路径穿越
filename = os.path.basename(backup_path)
safe_backup_path = os.path.join(get_astrbot_backups_path(), filename)

if not os.path.exists(safe_backup_path):
return Response().error("Backup file does not exist.").__dict__

result = await mgr.rollback_to_backup(safe_backup_path)
if result["success"]:
return (
Response().ok(result, "Rollback initiated. Restarting...").__dict__
)
return Response().error(result.get("error", "Rollback failed.")).__dict__
except Exception as e:
logger.error(f"/api/update/auto-update/rollback: {traceback.format_exc()}")
return Response().error(str(e)).__dict__