From d5605bb0a140a1090b21afecee715a1b36468555 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sun, 6 Sep 2026 21:50:40 +0800 Subject: [PATCH] fix(scheduler): gate release update checks --- app/application/configuration.py | 1 + app/runtime/config.py | 7 +++-- app/scheduler/catalog.py | 27 +++++++++++-------- app/scheduler/facade.py | 1 + app/startup/composition/configuration.py | 1 + docs/cli.md | 2 +- docs/rules/03-commands.md | 2 +- tests/test_database_backup_scheduler.py | 34 ++++++++++++++++++++++++ tests/test_system_utils.py | 7 +++-- 9 files changed, 61 insertions(+), 21 deletions(-) diff --git a/app/application/configuration.py b/app/application/configuration.py index c88c85e4b..889ae885f 100644 --- a/app/application/configuration.py +++ b/app/application/configuration.py @@ -178,6 +178,7 @@ class SchedulerRuntimeConfig: ai_agent_job_interval: Any usage_statistic_share: bool site_link: str | None + auto_update: bool = False @dataclass(frozen=True, slots=True) diff --git a/app/runtime/config.py b/app/runtime/config.py index ba80ef115..3efc7d19b 100644 --- a/app/runtime/config.py +++ b/app/runtime/config.py @@ -916,13 +916,12 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel): if not isinstance(data, dict): return data - # Release 已迁移到后台状态机,历史 release/true 不能继续启用启动时更新。 + # 仅 true 表示启用后台 Release 检查,其他模式不注册该定时服务。 if "MOVIEPILOT_AUTO_UPDATE" in data: original_update_mode = data["MOVIEPILOT_AUTO_UPDATE"] + mode = str(original_update_mode or "").strip().lower() normalized_update_mode = ( - "dev" - if str(original_update_mode or "").strip().lower() == "dev" - else "false" + mode if mode in {"true", "dev", "false"} else "false" ) if normalized_update_mode != str(original_update_mode): cls.update_env_config( diff --git a/app/scheduler/catalog.py b/app/scheduler/catalog.py index 866e7c681..7c844ff10 100644 --- a/app/scheduler/catalog.py +++ b/app/scheduler/catalog.py @@ -203,7 +203,11 @@ class SchedulerCatalogOwner(_SchedulerOwnerBase): JobSpec("full_gc", "主动内存回收", self.full_gc, "runtime"), JobSpec("agent_heartbeat", "智能体定时任务", self.agent_heartbeat, "agent"), JobSpec("usage_report", "安装版本统计上报", MoviePilotServerHelper.report_usage, "server"), - JobSpec("system_update_check", "检查系统更新", system_update_manager.check, "system"), + *( + [JobSpec("system_update_check", "检查系统更新", system_update_manager.check, "system")] + if config.auto_update + else [] + ), ] ).runtime_states() for job_id, job in self._jobs.items(): @@ -424,16 +428,17 @@ class SchedulerCatalogOwner(_SchedulerOwnerBase): kwargs={"job_id": "plugin_market_refresh"}, ) - # 更新检查只缓存 Release 元数据,不会在未授权时下载或重启。 - self._scheduler.add_job( - self.start, - "interval", - id="system_update_check", - name="检查系统更新", - hours=6, - next_run_time=datetime.now(pytz.timezone(config.timezone)) + timedelta(minutes=1), - kwargs={"job_id": "system_update_check"}, - ) + if config.auto_update: + # 更新检查只缓存 Release 元数据,不会在未授权时下载或重启。 + self._scheduler.add_job( + self.start, + "interval", + id="system_update_check", + name="检查系统更新", + hours=6, + next_run_time=datetime.now(pytz.timezone(config.timezone)) + timedelta(minutes=1), + kwargs={"job_id": "system_update_check"}, + ) # 订阅日历缓存 self._scheduler.add_job( diff --git a/app/scheduler/facade.py b/app/scheduler/facade.py index 3f0ac3add..2a7d871bb 100644 --- a/app/scheduler/facade.py +++ b/app/scheduler/facade.py @@ -72,6 +72,7 @@ class Scheduler( "DB_BACKUP_ENABLE", "DB_BACKUP_CRON", "USAGE_STATISTIC_SHARE", + "MOVIEPILOT_AUTO_UPDATE", } def __init__(self) -> None: diff --git a/app/startup/composition/configuration.py b/app/startup/composition/configuration.py index 16d710b92..805b21472 100644 --- a/app/startup/composition/configuration.py +++ b/app/startup/composition/configuration.py @@ -189,6 +189,7 @@ def build_scheduler_runtime_config(settings: Settings) -> SchedulerRuntimeConfig ai_agent_job_interval=settings.AI_AGENT_JOB_INTERVAL, usage_statistic_share=settings.USAGE_STATISTIC_SHARE, site_link=settings.MP_DOMAIN("#/site"), + auto_update=str(settings.MOVIEPILOT_AUTO_UPDATE).strip().lower() == "true", ) diff --git a/docs/cli.md b/docs/cli.md index b6c3fa08d..26adfd458 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -393,7 +393,7 @@ moviepilot version - `start` 会先启动后端,再启动前端 - `start --safe` 会以安全模式启动后端,本次启动跳过插件、调度器、监控、命令和工作流等后台扩展能力,不修改用户配置 -- `MOVIEPILOT_AUTO_UPDATE` 默认关闭;仅 `dev` 保留启动前跟踪当前 v3 开发分支的行为,更新失败只告警,不阻断当前启动 +- `MOVIEPILOT_AUTO_UPDATE` 默认关闭;设置为 `true` 时启用后台 Release 检查,设置为 `dev` 时保留启动前跟踪当前 v3 开发分支的行为,更新失败只告警,不阻断当前启动 - Release 更新由后台每 6 小时检查 GitHub Release;管理员确认后先静默下载安装包并显示进度,下载完成后再次确认重启,启动阶段只安装已下载且通过 SHA-256 校验的包 - 页面中的“稍后”会在当前浏览器暂停提醒 24 小时,“忽略此版本”只屏蔽当前版本;出现更高版本时会重新提示 - 通过系统内置的重启入口触发重启时,本地 CLI 安装模式也会复用同一套前后端进程管理完成重启 diff --git a/docs/rules/03-commands.md b/docs/rules/03-commands.md index 9d3d22da7..c4b29e0a4 100644 --- a/docs/rules/03-commands.md +++ b/docs/rules/03-commands.md @@ -177,7 +177,7 @@ moviepilot update all --ref latest --frontend-version latest moviepilot update all --skip-resources ``` -`MOVIEPILOT_AUTO_UPDATE` defaults to `false`. Setting it to `dev` retains branch-tracking updates during `start/restart`; stable Release updates use the authenticated background check/download/install API flow and do not use this setting. +`MOVIEPILOT_AUTO_UPDATE` defaults to `false`. Setting it to `true` enables the background Release check; setting it to `dev` retains branch-tracking updates during `start/restart`. The setting is hot-reloaded by the scheduler. --- diff --git a/tests/test_database_backup_scheduler.py b/tests/test_database_backup_scheduler.py index 0de28027c..f86727327 100644 --- a/tests/test_database_backup_scheduler.py +++ b/tests/test_database_backup_scheduler.py @@ -57,6 +57,7 @@ def _config(**changes) -> SchedulerRuntimeConfig: ai_agent_job_interval=None, usage_statistic_share=False, site_link=None, + auto_update=False, ) return replace(config, **changes) @@ -72,6 +73,11 @@ def test_database_backup_schedule_only_watches_job_shape() -> None: }) == {"DB_BACKUP_ENABLE", "DB_BACKUP_CRON"} +def test_auto_update_setting_is_hot_reloadable() -> None: + """自动更新开关变更时应触发 Scheduler 重建。""" + assert "MOVIEPILOT_AUTO_UPDATE" in Scheduler.CONFIG_WATCH + + def test_disabled_database_backup_does_not_register_job() -> None: """关闭备份时不注册作业。""" scheduler = _scheduler() @@ -102,6 +108,34 @@ def test_enabled_database_backup_registers_single_replaceable_job(monkeypatch) - assert scheduler._scheduler.jobs["database_backup"]["replace_existing"] is True +def test_auto_update_check_is_registered_only_when_enabled(monkeypatch) -> None: + """只有显式开启自动更新时才注册 Release 检查任务。""" + scheduler = _scheduler() + scheduler._services = Mock() + background_scheduler = Mock() + monkeypatch.setattr(scheduler_catalog, "BackgroundScheduler", lambda **_kwargs: background_scheduler) + monkeypatch.setattr(scheduler_catalog, "get_plugin_manager", lambda: Mock()) + monkeypatch.setattr(scheduler_catalog, "get_mediaserver_configs", lambda **_kwargs: []) + monkeypatch.setattr(scheduler, "init_workflow_jobs", lambda: None) + monkeypatch.setattr(scheduler, "init_agent_task_jobs", lambda: None) + monkeypatch.setattr(scheduler, "init_plugin_jobs", lambda: None) + + scheduler_catalog.SchedulerCatalogOwner._initialize_catalog(scheduler, _config(auto_update=False)) + assert not any( + call.kwargs.get("id") == "system_update_check" + for call in background_scheduler.add_job.call_args_list + ) + assert "system_update_check" not in scheduler._jobs + + background_scheduler.add_job.reset_mock() + scheduler_catalog.SchedulerCatalogOwner._initialize_catalog(scheduler, _config(auto_update=True)) + assert any( + call.kwargs.get("id") == "system_update_check" + for call in background_scheduler.add_job.call_args_list + ) + assert "system_update_check" in scheduler._jobs + + def test_scheduled_backup_uses_registered_database_governance(monkeypatch) -> None: governance = Mock() monkeypatch.setattr(scheduler_maintenance, "get_database_governance", lambda: governance) diff --git a/tests/test_system_utils.py b/tests/test_system_utils.py index dc30e1e73..afa223d00 100644 --- a/tests/test_system_utils.py +++ b/tests/test_system_utils.py @@ -473,8 +473,8 @@ def test_btrfs_fsid_dedup_setting_is_opt_in(): assert ConfigModel(BTRFS_FSID_DEDUP="true").BTRFS_FSID_DEDUP is True -def test_legacy_release_auto_update_mode_is_disabled(monkeypatch): - """历史 Release 启动更新值迁移为关闭,Dev 值继续保留。""" +def test_auto_update_mode_is_normalized(monkeypatch): + """自动更新仅保留 true、dev 和 false 三种运行模式。""" updates = [] monkeypatch.setattr( Settings, @@ -485,11 +485,10 @@ def test_legacy_release_auto_update_mode_is_disabled(monkeypatch): ) assert Settings(MOVIEPILOT_AUTO_UPDATE="release").MOVIEPILOT_AUTO_UPDATE == "false" - assert Settings(MOVIEPILOT_AUTO_UPDATE="true").MOVIEPILOT_AUTO_UPDATE == "false" + assert Settings(MOVIEPILOT_AUTO_UPDATE="true").MOVIEPILOT_AUTO_UPDATE == "true" assert Settings(MOVIEPILOT_AUTO_UPDATE="dev").MOVIEPILOT_AUTO_UPDATE == "dev" assert updates == [ ("MOVIEPILOT_AUTO_UPDATE", "release", "false"), - ("MOVIEPILOT_AUTO_UPDATE", "true", "false"), ]