# -*- coding: utf-8 -*- """桌面应用版本信息与云端升级策略判断。""" from __future__ import annotations import re from typing import Any APP_VERSION = "1.4.27" _VERSION_PATTERN = re.compile( r"^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)" r"(?:[-+][0-9A-Za-z.-]+)?$" ) def normalize_version(value: Any) -> str: """校验并统一版本号格式,允许用户输入可选的 v 前缀。""" text = str(value or "").strip() match = _VERSION_PATTERN.fullmatch(text) if not match: return "" return text[1:] if text.lower().startswith("v") else text def version_key(value: Any) -> tuple[int, int, int] | None: """返回用于升级方向判断的三段版本;无效版本不参与比较。""" normalized = normalize_version(value) if not normalized: return None core = normalized.split("-", 1)[0].split("+", 1)[0] major, minor, patch = core.split(".") return int(major), int(minor), int(patch) def is_newer_version(candidate: Any, current: Any) -> bool: """只有候选版本更高才升级,避免配置错误导致客户端降级。""" candidate_key = version_key(candidate) current_key = version_key(current) return bool(candidate_key and current_key and candidate_key > current_key) def _bounded_int(value: Any, default: int, minimum: int, maximum: int) -> int: try: parsed = int(value) except (TypeError, ValueError): parsed = default return max(minimum, min(maximum, parsed)) def release_status(release: Any, *, local_version: str = APP_VERSION) -> dict[str, Any]: """把云端发布策略转换为桌面端可直接消费的状态。""" source = release if isinstance(release, dict) else {} local = normalize_version(local_version) or APP_VERSION latest = normalize_version(source.get("latest_version")) or local update_available = is_newer_version(latest, local) download_url = str(source.get("download_url") or "").strip() force_upgrade = bool(source.get("force_upgrade")) and update_available package_sha256 = str(source.get("package_sha256") or "").strip().lower() auto_install = bool(source.get("auto_install")) and update_available return { "local_version": local, "latest_version": latest, "download_url": download_url, "release_notes": str(source.get("release_notes") or "").strip(), "update_available": update_available, "force_upgrade": force_upgrade, "auto_install": auto_install, "package_sha256": package_sha256, "package_size": _bounded_int( source.get("package_size"), 0, 0, 1024 * 1024 * 1024 ), "signature_required": bool(source.get("signature_required")), "release_channel": str(source.get("release_channel") or "stable"), "rollout_percent": _bounded_int( source.get("rollout_percent"), 100, 1, 100 ), "status": str(source.get("status") or "published"), "ready_for_auto_update": bool( update_available and download_url and package_sha256 ), "updated_at": str(source.get("updated_at") or ""), }