feat(system): add staged release updates

This commit is contained in:
jxxghp
2026-08-24 17:58:03 +08:00
parent 5415398027
commit 84326e20de
23 changed files with 1393 additions and 350 deletions
+18 -2
View File
@@ -317,8 +317,8 @@ class ConfigModel(BaseModel):
ALIPAN_APP_ID: str = "ac1bf04dc9fd4d9aaabb65b4a668d403"
# ==================== 系统升级配置 ====================
# 重启自动升级
MOVIEPILOT_AUTO_UPDATE: str = "release"
# 开发版仍可在启动时跟踪 v3 分支;Release 更新由后台更新服务管理。
MOVIEPILOT_AUTO_UPDATE: str = "false"
# 自动检查和更新站点资源包(站点索引、认证等)
AUTO_UPDATE_RESOURCE: bool = True
@@ -899,6 +899,22 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
if not isinstance(data, dict):
return data
# Release 已迁移到后台状态机,历史 release/true 不能继续启用启动时更新。
if "MOVIEPILOT_AUTO_UPDATE" in data:
original_update_mode = data["MOVIEPILOT_AUTO_UPDATE"]
normalized_update_mode = (
"dev"
if str(original_update_mode or "").strip().lower() == "dev"
else "false"
)
if normalized_update_mode != str(original_update_mode):
cls.update_env_config(
"MOVIEPILOT_AUTO_UPDATE",
original_update_mode,
normalized_update_mode,
)
data["MOVIEPILOT_AUTO_UPDATE"] = normalized_update_mode
# 处理 API_TOKEN 特殊验证
if "API_TOKEN" in data:
converted_value, needs_update = cls.validate_api_token(
+31 -99
View File
@@ -23,7 +23,6 @@ class SystemHelper(ConfigReloadMixin):
"""
系统工具类,提供系统相关的操作和判断
"""
AUTO_UPDATE_ENABLED_VALUES = {"release", "dev"}
CONFIG_WATCH = {
"DEBUG",
"LOG_LEVEL",
@@ -36,7 +35,7 @@ class SystemHelper(ConfigReloadMixin):
__system_flag_file = "/var/log/nginx/__moviepilot__"
__local_backend_runtime_file = settings.TEMP_PATH / "moviepilot.runtime.json"
__local_restart_log_file = settings.LOG_PATH / "moviepilot.restart.stdout.log"
__one_shot_update_flag_file = settings.TEMP_PATH / "moviepilot.pending_update"
__one_shot_dev_update_flag_file = settings.TEMP_PATH / "moviepilot.pending_dev_update"
__docker_restart_intent_file = settings.TEMP_PATH / "moviepilot.intentional_restart"
__graceful_shutdown_monitor_lock = threading.Lock()
__graceful_shutdown_monitor: Optional[threading.Thread] = None
@@ -97,94 +96,41 @@ class SystemHelper(ConfigReloadMixin):
return False
@staticmethod
def normalize_auto_update_mode(mode: Optional[str]) -> str:
"""
统一自动升级模式值,兼容历史 true 表示 release。
"""
normalized = str(mode or "").strip().lower()
return "release" if normalized == "true" else normalized
@staticmethod
def get_auto_update_mode() -> str:
"""
获取当前配置中的自动升级模式。
"""
return SystemHelper.normalize_auto_update_mode(
settings.MOVIEPILOT_AUTO_UPDATE
)
@staticmethod
def is_auto_update_enabled(mode: Optional[str] = None) -> bool:
"""
判断给定模式或当前配置是否启用了启动时自动升级。
"""
effective_mode = (
SystemHelper.get_auto_update_mode()
if mode is None
else SystemHelper.normalize_auto_update_mode(mode)
)
return effective_mode in SystemHelper.AUTO_UPDATE_ENABLED_VALUES
@staticmethod
def queue_one_shot_update(mode: str = "release") -> Tuple[bool, str]:
"""
写入一次性升级标记,供重启后的启动流程消费。
"""
effective_mode = SystemHelper.normalize_auto_update_mode(mode)
if effective_mode not in SystemHelper.AUTO_UPDATE_ENABLED_VALUES:
return False, "升级模式仅支持 release 或 dev"
def queue_one_shot_dev_update() -> Tuple[bool, str]:
"""写入一次性 Dev 更新标记,供本次重启的启动流程消费。"""
try:
SystemHelper.__one_shot_update_flag_file.parent.mkdir(
SystemHelper.__one_shot_dev_update_flag_file.parent.mkdir(
parents=True, exist_ok=True
)
SystemHelper.__one_shot_update_flag_file.write_text(
effective_mode, encoding="utf-8"
SystemHelper.__one_shot_dev_update_flag_file.write_text(
"dev", encoding="utf-8"
)
logger.info(f"已写入一次性升级标记,模式: {effective_mode}")
return True, ""
except OSError as err:
logger.error(f"写入一次性升级标记失败: {err}")
return False, f"写入一次性升级标记失败:{err}"
logger.error(f"写入一次性 Dev 更新标记失败: {err}")
return False, f"写入一次性 Dev 更新标记失败:{err}"
@staticmethod
def consume_one_shot_update_mode() -> Optional[str]:
"""
读取并清除一次性升级标记,避免后续启动重复执行。
"""
path = SystemHelper.__one_shot_update_flag_file
def consume_one_shot_dev_update() -> bool:
"""读取并删除一次性 Dev 更新标记,确保普通重启不会重复更新。"""
path = SystemHelper.__one_shot_dev_update_flag_file
if not path.exists():
return None
try:
raw_mode = path.read_text(encoding="utf-8", errors="replace")
except OSError as err:
logger.warning(f"读取一次性升级标记失败: {err}")
raw_mode = ""
return False
try:
mode = path.read_text(encoding="utf-8", errors="replace").strip().lower()
path.unlink(missing_ok=True)
except OSError as err:
logger.warning(f"删除一次性升级标记失败: {err}")
effective_mode = SystemHelper.normalize_auto_update_mode(raw_mode)
if effective_mode not in SystemHelper.AUTO_UPDATE_ENABLED_VALUES:
if raw_mode:
logger.warning(f"忽略无效的一次性升级模式: {raw_mode}")
return None
logger.info(f"检测到一次性升级标记,模式: {effective_mode}")
return effective_mode
logger.warning(f"消费一次性 Dev 更新标记失败: {err}")
return False
return mode == "dev"
@staticmethod
def clear_one_shot_update_flag() -> None:
"""
删除一次性升级标记。
"""
def clear_one_shot_dev_update() -> None:
"""重启失败时撤销尚未消费的一次性 Dev 更新。"""
try:
SystemHelper.__one_shot_update_flag_file.unlink(missing_ok=True)
SystemHelper.__one_shot_dev_update_flag_file.unlink(missing_ok=True)
except OSError as err:
logger.warning(f"删除一次性升级标记失败: {err}")
logger.warning(f"清理一次性 Dev 更新标记失败: {err}")
@staticmethod
def _spawn_local_restart_helper() -> None:
@@ -332,32 +278,18 @@ class SystemHelper(ConfigReloadMixin):
return SystemHelper._docker_api_restart()
@staticmethod
def upgrade(mode: str = "release") -> Tuple[bool, str]:
"""
触发升级并重启。
- 已开启自动升级时,直接重启,沿用当前配置。
- 未开启自动升级时,写入一次性升级标记,供下次启动时执行升级。
"""
current_mode = SystemHelper.get_auto_update_mode()
if SystemHelper.is_auto_update_enabled(current_mode):
ret, msg = SystemHelper.restart()
if not ret:
return ret, msg
if current_mode == "dev":
return True, "已检测到自动升级模式 dev,正在重启并执行升级"
return True, "已检测到自动升级已开启,正在重启并执行升级"
queued, message = SystemHelper.queue_one_shot_update(mode)
if not queued:
return False, message
ret, msg = SystemHelper.restart()
def upgrade_dev() -> Tuple[bool, str]:
"""保留原 Dev 模式:重启后跟踪当前 v3 开发分支。"""
configured_mode = str(settings.MOVIEPILOT_AUTO_UPDATE or "").strip().lower()
if configured_mode != "dev":
queued, message = SystemHelper.queue_one_shot_dev_update()
if not queued:
return False, message
ret, message = SystemHelper.restart()
if not ret:
SystemHelper.clear_one_shot_update_flag()
return ret, msg
effective_mode = SystemHelper.normalize_auto_update_mode(mode)
return True, f"已安排一次性 {effective_mode} 升级并重启"
SystemHelper.clear_one_shot_dev_update()
return False, message
return True, "已安排 Dev 更新并重启"
@staticmethod
def _start_graceful_shutdown_monitor():