fix(scheduler): gate release update checks

This commit is contained in:
jxxghp
2026-09-06 21:50:40 +08:00
parent 3f70440f5f
commit d5605bb0a1
9 changed files with 61 additions and 21 deletions
+1
View File
@@ -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)
+3 -4
View File
@@ -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(
+16 -11
View File
@@ -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(
+1
View File
@@ -72,6 +72,7 @@ class Scheduler(
"DB_BACKUP_ENABLE",
"DB_BACKUP_CRON",
"USAGE_STATISTIC_SHARE",
"MOVIEPILOT_AUTO_UPDATE",
}
def __init__(self) -> None:
+1
View File
@@ -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",
)
+1 -1
View File
@@ -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 安装模式也会复用同一套前后端进程管理完成重启
+1 -1
View File
@@ -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.
---
+34
View File
@@ -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)
+3 -4
View File
@@ -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"),
]