#!/usr/bin/env bash
set -euo pipefail
umask 077

VERSION="v113"
REPO_ZIP_URL_DEFAULT=""
REPO_BASE_URL_DEFAULT="https://nexus.infpro.me/nexus"
REPO_MANIFEST_URL_DEFAULT="https://nexus.infpro.me/release-manifest.json"
REPO_RELEASE_METADATA_URL_DEFAULT="https://nexus.infpro.me/release-manifest.json"
REPO_FALLBACK_BASE_URL_DEFAULT=""
REPO_FALLBACK_ZIP_URL_DEFAULT=""
REPO_FETCH_MODE_DEFAULT="zip"
REPO_MANIFEST_CONCURRENCY_DEFAULT="8"
REALM_RELEASE_TAG_DEFAULT="v2.9.3"
PANEL_ROOT="/opt/realm-panel"
PANEL_STATIC_REALM_DIR="${PANEL_ROOT}/panel/static/realm"
PANEL_REQ_STAMP="${PANEL_ROOT}/.requirements.sha256"
PANEL_REALM_TAG_STAMP="${PANEL_STATIC_REALM_DIR}/.realm_assets_tag"
PANEL_ASSET_CACHE_DIR="${PANEL_ROOT}/.asset-cache"
PANEL_REALM_CACHE_DIR="${PANEL_ASSET_CACHE_DIR}/realm"
PANEL_WHEEL_CACHE_DIR="${PANEL_ASSET_CACHE_DIR}/agent-wheels"
PANEL_CONFIG_DIR="/etc/realm-panel"
PANEL_LOG_DIR="/var/log/realm-panel"
PANEL_RUNTIME_DIR="/var/lib/nexus/panel"
PANEL_SYSTEMD_UNIT="/etc/systemd/system/realm-panel.service"
PANEL_SERVICE_USER="${REALM_PANEL_SERVICE_USER:-realm-panel}"
PANEL_SERVICE_GROUP="${REALM_PANEL_SERVICE_GROUP:-realm-panel}"
PANEL_PREV_VENV="${PANEL_ROOT}/.venv-prev"
PANEL_NEXT_VENV="${PANEL_ROOT}/.venv-next"
# RemoteMacAgent.app 由运维在 macOS 上 build 后 scp 到 /static/，不在仓库里。
# update_panel 会整个重写 panel/，因此必须把这些产物缓存到 .asset-cache
# 之外的目录，更新后再恢复，避免每次更新都需要重新上传。
PANEL_COMPANION_CACHE_DIR="${PANEL_ASSET_CACHE_DIR}/remote-mac-agent"
PANEL_STATIC_DIR="${PANEL_ROOT}/panel/static"
PANEL_DEPLOY_ROLLBACK_DIR=""
PANEL_DEPLOY_ROLLBACK_ACTIVE="0"
PANEL_SERVICE_WAS_ACTIVE="0"
PANEL_SYSTEMD_STATE_FILE=""
PANEL_CODE_STAGE_DIR=""
PANEL_PREV_CODE_DIR="${PANEL_ROOT}/.panel-prev"
PANEL_PREV_SHARED_DIR="${PANEL_ROOT}/.shared-prev"
PANEL_UPDATE_LOCK_PATH_DEFAULT="/run/lock/nexus-panel-update.lock"
PANEL_UPDATE_LOCK_DEPTH=0
PANEL_UPDATE_LOCK_HELD_PATH=""
PANEL_PYTHON_BIN=""
__REALM_PANEL_CLEANUP_PATHS=()
__REALM_PANEL_PRESERVE_PATHS=()

info(){ echo -e "\033[33m[提示]\033[0m $*" >&2; }
ok(){ echo -e "\033[32m[OK]\033[0m $*" >&2; }
err(){ echo -e "\033[31m[错误]\033[0m $*" >&2; }

require_secure_asset_url(){
  local url="${1:-}"
  local label="${2:-远程资产}"
  case "${url}" in
    https://*|file://*) return 0 ;;
    *)
      err "${label} 必须使用 https:// 或 file://：${url:-<empty>}"
      return 1
      ;;
  esac
}

panel_update_lock_path(){
  local lock_path="${REALM_PANEL_UPDATE_LOCK_PATH:-${PANEL_UPDATE_LOCK_PATH_DEFAULT}}"
  if [[ -z "${lock_path}" || "${lock_path}" != /* ]]; then
    err "面板更新锁必须是绝对路径：${lock_path:-<empty>}"
    return 1
  fi
  printf '%s' "${lock_path}"
}

panel_update_path_identity(){
  local path="$1"
  if stat -Lc '%d:%i:%u:%a' "${path}" 2>/dev/null; then
    return 0
  fi
  stat -f '%d:%i:%u:%Lp' "${path}" 2>/dev/null
}

panel_update_fd_identity(){
  local proc_fd="/proc/$$/fd/201"
  if [[ -e "${proc_fd}" ]]; then
    panel_update_path_identity "${proc_fd}"
    return $?
  fi
  if ! command -v lsof >/dev/null 2>&1; then
    return 1
  fi
  local line device="" inode="" owner="" mode="" lsof_output=""
  lsof_output="$(lsof -a -p "$$" -d 201 -FDiun 2>/dev/null || true)"
  while IFS= read -r line; do
    case "${line}" in
      D*) device="${line#D}" ;;
      i*) inode="${line#i}" ;;
      u*) owner="${line#u}" ;;
    esac
  done <<< "${lsof_output}"
  mode="$(stat -f '%Lp' /dev/fd/201 2>/dev/null || true)"
  if [[ "${device}" == 0x* ]]; then
    device="$((device))"
  fi
  if [[ -z "${device}" || -z "${inode}" || -z "${owner}" || -z "${mode}" ]]; then
    return 1
  fi
  printf '%s:%s:%s:%s' "${device}" "${inode}" "${owner}" "${mode}"
}

panel_update_lock_identity_is_secure(){
  local identity="$1"
  local device inode owner mode
  IFS=: read -r device inode owner mode <<< "${identity}"
  mode="${mode#0}"
  [[ "${device}" =~ ^[0-9]+$ ]] || return 1
  [[ "${inode}" =~ ^[0-9]+$ ]] || return 1
  [[ "${owner}" == "$(id -u)" ]] || return 1
  [[ "${mode}" == "600" ]]
}

verify_panel_update_lock_fd(){
  local lock_path="$1"
  local path_identity="" fd_identity=""
  if [[ -L "${lock_path}" || ! -f "${lock_path}" || ! -f /dev/fd/201 ]]; then
    err "面板更新锁必须保持为普通文件：${lock_path}"
    return 1
  fi
  path_identity="$(panel_update_path_identity "${lock_path}" || true)"
  fd_identity="$(panel_update_fd_identity || true)"
  if [[ -z "${path_identity}" || -z "${fd_identity}" ]]; then
    err "无法核验面板更新锁的 device/inode：${lock_path}"
    return 1
  fi
  if ! panel_update_lock_identity_is_secure "${path_identity}" \
    || ! panel_update_lock_identity_is_secure "${fd_identity}"; then
    err "面板更新锁 owner/mode 不安全，要求当前用户且权限 0600：${lock_path}"
    return 1
  fi
  if [[ "${path_identity}" != "${fd_identity}" ]]; then
    err "面板更新锁 path/fd device+inode 不一致，已拒绝继续：${lock_path}"
    return 1
  fi
}

prepare_panel_update_lock_file(){
  local lock_path="$1"
  local lock_parent=""
  lock_parent="$(dirname "${lock_path}")"
  if [[ -L "${lock_path}" ]]; then
    err "拒绝符号链接形式的面板更新锁：${lock_path}"
    return 1
  fi
  if [[ -e "${lock_path}" && ! -f "${lock_path}" ]]; then
    err "面板更新锁不是普通文件：${lock_path}"
    return 1
  fi
  if [[ -L "${lock_parent}" ]]; then
    err "面板更新锁目录不能是符号链接：${lock_parent}"
    return 1
  fi
  if [[ ! -d "${lock_parent}" ]]; then
    mkdir -p "${lock_parent}" || {
      err "无法创建面板更新锁目录：${lock_parent}"
      return 1
    }
  fi
  if [[ ! -e "${lock_path}" ]]; then
    if ! (umask 077; set -o noclobber; : > "${lock_path}") 2>/dev/null; then
      if [[ -L "${lock_path}" || ! -f "${lock_path}" ]]; then
        err "无法安全创建面板更新锁：${lock_path}"
        return 1
      fi
    fi
  fi
  if [[ -L "${lock_path}" || ! -f "${lock_path}" ]]; then
    err "面板更新锁必须是普通文件：${lock_path}"
    return 1
  fi
  if [[ ! -O "${lock_path}" ]]; then
    err "面板更新锁不属于当前用户，已拒绝继续：${lock_path}"
    return 1
  fi
  chmod 600 "${lock_path}" >/dev/null 2>&1 || {
    err "无法收紧面板更新锁权限：${lock_path}"
    return 1
  }
  local identity=""
  identity="$(panel_update_path_identity "${lock_path}" || true)"
  if [[ -z "${identity}" ]] || ! panel_update_lock_identity_is_secure "${identity}"; then
    err "面板更新锁 owner/mode 校验失败：${lock_path}"
    return 1
  fi
}

acquire_panel_update_lock(){
  local lock_path=""
  lock_path="$(panel_update_lock_path)" || return 1
  if (( PANEL_UPDATE_LOCK_DEPTH > 0 )); then
    if [[ "${PANEL_UPDATE_LOCK_HELD_PATH}" != "${lock_path}" ]]; then
      err "同一进程不能切换面板更新锁路径"
      return 1
    fi
    PANEL_UPDATE_LOCK_DEPTH=$((PANEL_UPDATE_LOCK_DEPTH + 1))
    return 0
  fi
  require_command flock "需要 flock 防止并发安装或更新" || return 1
  prepare_panel_update_lock_file "${lock_path}" || return 1
  if ! exec 201<> "${lock_path}"; then
    err "无法打开面板更新锁：${lock_path}"
    return 1
  fi
  if ! verify_panel_update_lock_fd "${lock_path}"; then
    exec 201>&-
    err "面板更新锁在打开时被替换，已拒绝继续：${lock_path}"
    return 1
  fi
  if ! flock -n 201; then
    exec 201>&-
    err "另一个面板安装、更新、卸载或回滚正在执行"
    return 1
  fi
  if ! verify_panel_update_lock_fd "${lock_path}"; then
    exec 201>&-
    err "面板更新锁在加锁时被替换，已拒绝继续：${lock_path}"
    return 1
  fi
  PANEL_UPDATE_LOCK_HELD_PATH="${lock_path}"
  PANEL_UPDATE_LOCK_DEPTH=1
}

release_panel_update_lock(){
  if (( PANEL_UPDATE_LOCK_DEPTH <= 0 )); then
    return 0
  fi
  PANEL_UPDATE_LOCK_DEPTH=$((PANEL_UPDATE_LOCK_DEPTH - 1))
  if (( PANEL_UPDATE_LOCK_DEPTH > 0 )); then
    return 0
  fi
  PANEL_UPDATE_LOCK_HELD_PATH=""
  exec 201>&-
}

release_all_panel_update_locks(){
  if (( PANEL_UPDATE_LOCK_DEPTH <= 0 )); then
    return 0
  fi
  PANEL_UPDATE_LOCK_DEPTH=1
  release_panel_update_lock
}

# --- 面板更新事务 journal（R26-H-04） ---
# 更新流程在删除旧 panel/、激活 code、激活 venv、重启服务前持久化
# ${PANEL_ROOT}/.update-transaction.journal，每完成一个阶段原子推进。
# SIGKILL/断电不可被 EXIT trap 捕获，下一次 install/update/restart 入口
# 在业务 preflight 之前扫描 journal：存在且未 committed 时验证快照并自动
# 恢复；快照不可用时打印明确的恢复命令并返回非零。
panel_update_journal_path(){
  printf '%s' "${PANEL_ROOT}/.update-transaction.journal"
}

read_panel_update_journal_field(){
  local journal="$1" key="$2"
  local line value=""
  [[ -f "${journal}" ]] || return 1
  while IFS= read -r line || [[ -n "${line}" ]]; do
    case "${line}" in
      "${key}"=*) value="${line#*=}" ; break ;;
    esac
  done < "${journal}"
  if [[ -z "${value}" ]]; then
    return 1
  fi
  printf '%s' "${value}"
}

write_panel_update_journal(){
  local stage="${1:-}"
  local journal="" tmp=""
  journal="$(panel_update_journal_path)"
  case "${stage}" in
    prepared|code-activated|venv-activated|restarting|restoring|committed) ;;
    *)
      err "非法面板更新事务阶段：${stage:-<empty>}"
      return 1
      ;;
  esac
  mkdir -p "${PANEL_ROOT}" || {
    err "无法创建面板更新事务 journal 目录：${PANEL_ROOT}"
    return 1
  }
  tmp="$(mktemp "${journal}.tmp.XXXXXX")" || return 1
  {
    printf 'version=1\n'
    printf 'stage=%s\n' "${stage}"
    printf 'snapshot=%s\n' "${PANEL_DEPLOY_ROLLBACK_DIR:-}"
    printf 'target=%s\n' "${PANEL_ROOT}/panel"
    printf 'service_was_active=%s\n' "${PANEL_SERVICE_WAS_ACTIVE:-0}"
  } > "${tmp}" || {
    rm -f "${tmp}" >/dev/null 2>&1 || true
    err "写入面板更新事务 journal 失败：${journal}"
    return 1
  }
  chmod 600 "${tmp}" >/dev/null 2>&1 || true
  if ! mv -f "${tmp}" "${journal}"; then
    rm -f "${tmp}" >/dev/null 2>&1 || true
    err "提交面板更新事务 journal 失败：${journal}"
    return 1
  fi
}

clear_panel_update_journal(){
  local journal=""
  journal="$(panel_update_journal_path)"
  rm -f "${journal}" >/dev/null 2>&1 || true
}

panel_update_snapshot_is_usable(){
  local snapshot="$1"
  [[ -d "${snapshot}" ]] || return 1
  [[ -r "${snapshot}" ]] || return 1
  # 结构完整：至少包含一份可用的恢复材料，防止空目录快照触发破坏性回滚。
  [[ -d "${snapshot}/panel" || -d "${snapshot}/shared" || -d "${snapshot}/etc" \
    || -f "${snapshot}/venv.present" || -f "${snapshot}/requirements.sha256" ]]
}

recover_pending_panel_update(){
  local journal="" stage="" snapshot="" service_was_active=""
  journal="$(panel_update_journal_path)"
  if [[ ! -f "${journal}" ]]; then
    return 0
  fi
  stage="$(read_panel_update_journal_field "${journal}" stage || true)"
  snapshot="$(read_panel_update_journal_field "${journal}" snapshot || true)"
  service_was_active="$(read_panel_update_journal_field "${journal}" service_was_active || true)"
  if [[ -z "${stage}" ]]; then
    err "面板更新事务 journal 损坏（缺少 stage），已停止自动恢复：${journal}"
    err "恢复命令：人工核对后删除该 journal（rm -f ${journal}）再重试"
    return 1
  fi
  if [[ "${stage}" == "committed" ]]; then
    info "面板更新事务已完成（committed），清理遗留 journal"
    clear_panel_update_journal
    return 0
  fi
  err "检测到未完成的面板更新事务（阶段：${stage}），正在自动恢复..."
  if [[ -z "${snapshot}" || ! -d "${snapshot}" || ! -r "${snapshot}" ]]; then
    err "面板更新事务快照缺失或不可读：${snapshot:-<empty>}"
    err "恢复命令："
    err "  rm -rf ${PANEL_ROOT}/panel"
    err "  cp -a ${snapshot:-<快照路径>}/panel ${PANEL_ROOT}/panel"
    err "完成后请手动删除 journal：rm -f ${journal}"
    return 1
  fi
  if ! panel_update_snapshot_is_usable "${snapshot}"; then
    err "面板更新事务快照结构不完整：${snapshot}"
    err "恢复命令："
    err "  rm -rf ${PANEL_ROOT}/panel"
    err "  cp -a ${snapshot}/panel ${PANEL_ROOT}/panel"
    err "完成后请手动删除 journal：rm -f ${journal}"
    return 1
  fi
  PANEL_DEPLOY_ROLLBACK_DIR="${snapshot}"
  PANEL_SERVICE_WAS_ACTIVE="${service_was_active:-0}"
  if ! write_panel_update_journal restoring; then
    PANEL_DEPLOY_ROLLBACK_DIR=""
    return 1
  fi
  PANEL_DEPLOY_ROLLBACK_ACTIVE="1"
  if ! restore_panel_deploy_rollback; then
    PANEL_DEPLOY_ROLLBACK_ACTIVE="0"
    PANEL_DEPLOY_ROLLBACK_DIR=""
    err "面板更新事务自动恢复失败，保留恢复材料与 journal：${journal}"
    err "恢复命令："
    err "  rm -rf ${PANEL_ROOT}/panel"
    err "  cp -a ${snapshot}/panel ${PANEL_ROOT}/panel"
    err "完成后请手动删除 journal：rm -f ${journal}"
    return 1
  fi
  ok "已从面板更新事务快照自动恢复"
  return 0
}

check_panel_update_transaction(){
  # 所有 install/update/restart 入口的恢复闸门：在业务 preflight 之前先加锁，
  # 扫描未完成的面板更新事务；自动恢复失败时返回非零并给出明确恢复命令。
  acquire_panel_update_lock || return 1
  local rc=0
  recover_pending_panel_update || rc=$?
  release_panel_update_lock
  return "${rc}"
}

register_cleanup_path(){
  local path="${1:-}"
  [[ -n "${path}" ]] || return 0
  __REALM_PANEL_CLEANUP_PATHS+=("${path}")
}

preserve_cleanup_path(){
  local path="${1:-}"
  local existing
  [[ -n "${path}" ]] || return 0
  for existing in "${__REALM_PANEL_PRESERVE_PATHS[@]:-}"; do
    [[ "${existing}" == "${path}" ]] && return 0
  done
  __REALM_PANEL_PRESERVE_PATHS+=("${path}")
}

cleanup_path_is_preserved(){
  local path="$1"
  local preserved
  for preserved in "${__REALM_PANEL_PRESERVE_PATHS[@]:-}"; do
    [[ -n "${preserved}" ]] || continue
    if [[ "${path}" == "${preserved}" || "${preserved}" == "${path%/}/"* ]]; then
      return 0
    fi
  done
  return 1
}

# Audited optional probes/cleanup only. Keep call sites explicit; do not add
# bare true-fallthrough because it hides whether the failure was reviewed.
env_enabled_default_true(){
  local raw="${1:-1}"
  raw="${raw,,}"
  case "${raw}" in
    0|false|no|off|n) return 1 ;;
    *) return 0 ;;
  esac
}
need_root(){
  if [[ "$(id -u)" -ne 0 ]]; then
    err "请使用 root 运行（sudo -i / su -）"
    exit 1
  fi
}

require_command(){
  local cmd="$1"
  local hint="${2:-}"
  if command -v "$cmd" >/dev/null 2>&1; then
    return 0
  fi
  if [[ -n "$hint" ]]; then
    err "缺少命令：${cmd}（${hint}）"
  else
    err "缺少命令：${cmd}"
  fi
  return 1
}

require_systemd(){
  require_command systemctl "需要 systemd 管理 realm-panel.service" || exit 1
  if ! systemctl --version >/dev/null 2>&1; then
    err "systemctl 不可用，无法安全安装/更新/重启面板服务"
    exit 1
  fi
}

require_existing_panel_install(){
  if [[ ! -d "${PANEL_ROOT}/panel" || ! -f "${PANEL_CONFIG_DIR}/panel.env" ]]; then
    err "未检测到已安装的面板，请先选择 [安装面板]"
    exit 1
  fi
}

strip_env_value(){
  local value="$1"
  value="${value//$'\r'/}"
  value="${value//$'\n'/}"
  printf '%s' "${value}"
}

# Append a KEY=value line to an env file *if and only if* the key is not
# already present. The value is run through strip_env_value first so a
# value containing "\nMALICIOUS_KEY=..." cannot inject a second line.
# Use this rather than `echo "KEY=$value" >> ...` for any caller-supplied
# or environment-derived value (SH-001 / SH-013 follow-up).
append_env_default(){
  local target="$1" key="$2" value="$3"
  local sanitized
  sanitized="$(strip_env_value "${value}")"
  if [[ "${key}" =~ [^A-Za-z0-9_] ]]; then
    err "拒绝写入非法 env 键：${key}"
    return 1
  fi
  if grep -q "^${key}=" "${target}" 2>/dev/null; then
    return 0
  fi
  printf '%s=%s\n' "${key}" "${sanitized}" >> "${target}"
}

should_sync_realm_assets(){
  env_enabled_default_true "${REALM_PANEL_SYNC_REALM_ASSETS:-1}"
}

panel_python_candidate_path(){
  local python_bin="${1:-}"
  [[ -n "${python_bin}" ]] || return 1
  if [[ "${python_bin}" == */* ]]; then
    [[ -x "${python_bin}" ]] || return 1
    printf '%s\n' "${python_bin}"
  else
    command -v "${python_bin}"
  fi
}

panel_python_builder_path(){
  local python_bin="${1:-}"
  local resolved_path=""
  local base_executable=""
  python_bin="$(panel_python_candidate_path "${python_bin}" || true)"
  [[ -n "${python_bin}" ]] || return 1
  resolved_path="$(readlink -f "${python_bin}" 2>/dev/null || true)"
  if [[ -n "${resolved_path}" \
    && "${resolved_path}" != "${python_bin}" \
    && -x "${resolved_path}" ]]; then
    printf '%s\n' "${resolved_path}"
    return 0
  fi
  base_executable="$(
    "${python_bin}" -c \
      'import os, sys; print(os.path.join(sys.base_prefix, "bin", f"python{sys.version_info.major}.{sys.version_info.minor}"))' \
      2>/dev/null || true
  )"
  if [[ -n "${base_executable}" \
    && "${base_executable}" != "${python_bin}" \
    && -x "${base_executable}" ]]; then
    printf '%s\n' "${base_executable}"
  else
    printf '%s\n' "${python_bin}"
  fi
}

probe_panel_python_version(){
  local python_bin="${1:-}"
  python_bin="$(panel_python_candidate_path "${python_bin}" || true)"
  [[ -n "${python_bin}" ]] || return 1
  "${python_bin}" - <<'PY' >/dev/null 2>&1
import json
import ssl
import sys

raise SystemExit(0 if sys.version_info >= (3, 9) else 1)
PY
}

probe_panel_python_runtime(){
  local python_bin="${1:-}"
  probe_panel_python_version "${python_bin}" || return 1
  python_bin="$(panel_python_candidate_path "${python_bin}")"
  "${python_bin}" - <<'PY' >/dev/null 2>&1
import ensurepip
import venv
PY
}

select_supported_panel_python_runtime(){
  local candidate=""
  if [[ -n "${REALM_PANEL_PYTHON_BIN:-}" ]]; then
    if probe_panel_python_runtime "${REALM_PANEL_PYTHON_BIN}"; then
      panel_python_candidate_path "${REALM_PANEL_PYTHON_BIN}"
      return 0
    fi
    err "REALM_PANEL_PYTHON_BIN 不可用、低于 Python 3.9 或缺少 venv/ensurepip：${REALM_PANEL_PYTHON_BIN}"
    return 2
  fi
  for candidate in \
    "${PANEL_ROOT:-/opt/realm-panel}/venv/bin/python" \
    python3 python3.12 python3.11 python3.10 python3.9; do
    if ! probe_panel_python_runtime "${candidate}"; then
      continue
    fi
    panel_python_candidate_path "${candidate}"
    return 0
  done
  return 1
}

find_panel_python_missing_venv(){
  local candidate=""
  for candidate in \
    "${PANEL_ROOT:-/opt/realm-panel}/venv/bin/python" \
    python3 python3.12 python3.11 python3.10 python3.9; do
    if probe_panel_python_version "${candidate}" \
      && ! probe_panel_python_runtime "${candidate}"; then
      panel_python_candidate_path "${candidate}"
      return 0
    fi
  done
  return 1
}

panel_python_apt_package(){
  local python_bin="${1:-}"
  python_bin="$(panel_python_candidate_path "${python_bin}" || true)"
  [[ -n "${python_bin}" ]] || return 1
  "${python_bin}" - <<'PY'
import sys

print(f"python{sys.version_info.major}.{sys.version_info.minor}")
PY
}

panel_apt_has_candidate(){
  local package="${1:-}"
  local candidate=""
  [[ -n "${package}" ]] || return 1
  command -v apt-cache >/dev/null 2>&1 || return 1
  candidate="$(
    apt-cache policy "${package}" 2>/dev/null \
      | sed -n 's/^  Candidate: //p' \
      | head -n 1
  )"
  [[ -n "${candidate}" && "${candidate}" != "(none)" ]]
}

repair_panel_python_venv_support(){
  local python_bin="${1:-}"
  local package=""
  package="$(panel_python_apt_package "${python_bin}" || true)"
  [[ -n "${package}" ]] || return 1
  if ! panel_apt_has_candidate "${package}-venv"; then
    return 1
  fi
  info "为现有 ${package} 补装 venv/ensurepip 支持..."
  apt-get install -y --no-install-recommends \
    "${package}-venv" "${package}-distutils" \
    || apt-get install -y --no-install-recommends "${package}-venv" \
    || return 1
  probe_panel_python_runtime "${python_bin}"
}

install_apt_supported_panel_python_runtime(){
  local version=""
  local package=""
  local selected_python=""
  for version in 3.9 3.10 3.11 3.12; do
    package="python${version}"
    if ! panel_apt_has_candidate "${package}"; then
      continue
    fi
    info "尝试通过 apt 安装 Python ${version} 运行时..."
    apt-get install -y --no-install-recommends \
      "${package}" "${package}-venv" "${package}-distutils" \
      || apt-get install -y --no-install-recommends \
        "${package}" "${package}-venv" \
      || true
    selected_python="$(select_supported_panel_python_runtime || true)"
    if [[ -n "${selected_python}" ]]; then
      PANEL_PYTHON_BIN="${selected_python}"
      ok "已准备可用 Python：${PANEL_PYTHON_BIN}"
      return 0
    fi
  done
  return 1
}

ensure_supported_panel_python_runtime(){
  local selected_python=""
  local selection_status=0
  local repair_candidate=""
  if selected_python="$(select_supported_panel_python_runtime)"; then
    PANEL_PYTHON_BIN="${selected_python}"
    return 0
  else
    selection_status=$?
  fi
  if [[ "${selection_status}" == "2" ]]; then
    return 1
  fi
  repair_candidate="$(find_panel_python_missing_venv || true)"
  if [[ -n "${repair_candidate}" ]] \
    && repair_panel_python_venv_support "${repair_candidate}"; then
    PANEL_PYTHON_BIN="${repair_candidate}"
    ok "已准备可用 Python：${PANEL_PYTHON_BIN}"
    return 0
  fi
  if install_apt_supported_panel_python_runtime; then
    return 0
  fi
  err "面板需要 Python 3.9+ 且必须提供 venv/ensurepip；当前系统未找到可用运行时"
  return 1
}

apt_install(){
  export DEBIAN_FRONTEND=noninteractive
  require_command apt-get "仅支持 Debian/Ubuntu 系 apt 安装依赖" || exit 1
  require_command dpkg "仅支持 Debian/Ubuntu 系 apt 安装依赖" || exit 1
  local pkgs=(curl unzip zip jq python3 python3-venv python3-pip ca-certificates)
  local missing=()
  local selected_python=""
  local selection_status=0
  local p
  for p in "${pkgs[@]}"; do
    if ! dpkg -s "$p" >/dev/null 2>&1; then
      missing+=("$p")
    fi
  done
  if selected_python="$(select_supported_panel_python_runtime)"; then
    PANEL_PYTHON_BIN="${selected_python}"
    if [[ ${#missing[@]} -eq 0 ]]; then
      ok "依赖已满足，跳过 apt 安装"
      return 0
    fi
  else
    selection_status=$?
    if [[ "${selection_status}" == "2" ]]; then
      exit 1
    fi
  fi
  if [[ ${#missing[@]} -gt 0 ]]; then
    info "安装缺失依赖: ${missing[*]}"
  else
    info "基础依赖已满足，正在准备 Python 3.9+ 运行时"
  fi
  apt-get update -y >/dev/null
  if [[ ${#missing[@]} -gt 0 ]]; then
    apt-get install -y "${missing[@]}" >/dev/null
  fi
  ensure_supported_panel_python_runtime || exit 1
}

download_file(){
  local url="$1"
  local out="$2"
  local tmp=""
  require_secure_asset_url "${url}" "下载资产" || return 1
  tmp="$(mktemp "${out}.tmp.XXXXXX")" || return 1
  if declare -F register_cleanup_path >/dev/null 2>&1; then
    register_cleanup_path "${tmp}"
  fi
  if [[ "${url}" == file://* ]]; then
    local src="${url#file://}"
    src="${src%%\?*}"
    # file:// 只允许复制普通文件本体；符号链接与特殊文件一律拒绝。
    if [[ ! -f "${src}" || -L "${src}" ]]; then
      rm -f "${tmp}" || true
      return 1
    fi
    if cp -f "${src}" "${tmp}"; then
      mv -f "${tmp}" "${out}"
      return 0
    fi
    rm -f "${tmp}" || true
    return 1
  fi
  if curl -fL --max-redirs 8 --proto-redir '=https' --silent --show-error \
    --retry 3 --retry-delay 1 \
    --connect-timeout 10 --max-time 300 \
    -H "Cache-Control: no-cache" -H "Pragma: no-cache" \
    "$url" -o "$tmp"; then
    mv -f "${tmp}" "${out}"
    return 0
  fi
  rm -f "${tmp}" || true
  return 1
}

sync_dir_mirror(){
  local src="$1"
  local dst="$2"
  [[ -d "${src}" ]] || return 1
  mkdir -p "${dst}"
  if command -v rsync >/dev/null 2>&1; then
    rsync -a --delete "${src%/}/" "${dst}/" >/dev/null 2>&1
    return $?
  fi
  find "${dst}" -mindepth 1 -maxdepth 1 -exec rm -rf {} + >/dev/null 2>&1 || true
  cp -a "${src}/." "${dst}/" >/dev/null 2>&1
}

restore_static_asset_cache(){
  local restored=0
  if [[ -d "${PANEL_REALM_CACHE_DIR}" ]]; then
    if sync_dir_mirror "${PANEL_REALM_CACHE_DIR}" "${PANEL_STATIC_REALM_DIR}"; then
      restored=1
    fi
  fi
  # RemoteMacAgent-*.zip 等 companion 产物按文件粒度恢复（不像 realm
  # 走目录镜像），直接 cp 到 /static/，保留缓存份不删。
  if [[ -d "${PANEL_COMPANION_CACHE_DIR}" ]]; then
    install -d -m 755 "${PANEL_STATIC_DIR}"
    if compgen -G "${PANEL_COMPANION_CACHE_DIR}/RemoteMacAgent-*" > /dev/null 2>&1; then
      if ! cp -af \
        "${PANEL_COMPANION_CACHE_DIR}"/RemoteMacAgent-* "${PANEL_STATIC_DIR}/"; then
        err "从 cache 恢复 RemoteMacAgent companion 失败"
        return 1
      fi
      restored=1
    fi
  fi
  if [[ "${restored}" == "1" ]]; then
    info "已从本地缓存恢复 realm / companion 资产"
  fi
}

update_static_asset_cache(){
  mkdir -p "${PANEL_ASSET_CACHE_DIR}"
  if [[ -d "${PANEL_STATIC_REALM_DIR}" ]]; then
    sync_dir_mirror "${PANEL_STATIC_REALM_DIR}" "${PANEL_REALM_CACHE_DIR}" || true
  fi
  snapshot_companion_assets_into_cache
}

# 把当前 /static/RemoteMacAgent-*.{zip,sha256,version} 入缓存。
# update_panel 在 rm -rf panel/ 之前会先调用一次，把运维 scp 上去的最新
# 文件保留到 .asset-cache；rewrite 后 restore_static_asset_cache 再拷回。
snapshot_companion_assets_into_cache(){
  if ! compgen -G "${PANEL_STATIC_DIR}/RemoteMacAgent-*" > /dev/null 2>&1; then
    return 0
  fi
  local stage="" previous=""
  install -d -m 755 "${PANEL_ASSET_CACHE_DIR}"
  stage="$(mktemp -d "${PANEL_ASSET_CACHE_DIR}/.remote-mac-agent.stage.XXXXXX")"
  previous="$(mktemp -d "${PANEL_ASSET_CACHE_DIR}/.remote-mac-agent.previous.XXXXXX")"
  rmdir "${previous}"
  chmod 700 "${stage}" >/dev/null 2>&1 || true
  if ! cp -af "${PANEL_STATIC_DIR}"/RemoteMacAgent-* "${stage}/"; then
    rm -rf "${stage}" "${previous}" >/dev/null 2>&1 || true
    err "快照 RemoteMacAgent companion 到 cache staging 失败"
    return 1
  fi
  if [[ -e "${PANEL_COMPANION_CACHE_DIR}" || -L "${PANEL_COMPANION_CACHE_DIR}" ]]; then
    if ! mv "${PANEL_COMPANION_CACHE_DIR}" "${previous}"; then
      rm -rf "${stage}" "${previous}" >/dev/null 2>&1 || true
      err "无法暂存旧 RemoteMacAgent companion cache"
      return 1
    fi
  fi
  if ! mv "${stage}" "${PANEL_COMPANION_CACHE_DIR}"; then
    if [[ -e "${previous}" || -L "${previous}" ]]; then
      if ! mv "${previous}" "${PANEL_COMPANION_CACHE_DIR}" >/dev/null 2>&1; then
        err "旧 RemoteMacAgent companion cache 保留在：${previous}"
      fi
    fi
    rm -rf "${stage}" >/dev/null 2>&1 || true
    err "无法提交 RemoteMacAgent companion cache 快照"
    return 1
  fi
  rm -rf "${previous}" >/dev/null 2>&1 || true
}

# 从本次更新解出来的仓库内 dist/remote-mac-agent/ 把 RemoteMacAgent 二进制
# 同步到 /static/。仓库上发布的版本视为权威源，会覆盖 restore_static_asset_cache
# 还原回去的旧 cache 版本；如果仓库里没有（比如本次发布没带 companion），
# 则保留 cache 还原的本地副本不动，向后兼容运维直接 scp 的传统流程。
install_companion_from_extract(){
  if [[ -z "${PANEL_DIR}" || ! -d "${PANEL_DIR}" ]]; then
    return 0
  fi
  local extracted_root companion_dir
  extracted_root="$(dirname "${PANEL_DIR}")"
  companion_dir="${extracted_root}/dist/remote-mac-agent"
  if [[ ! -d "${companion_dir}" ]]; then
    return 0
  fi
  if ! compgen -G "${companion_dir}/RemoteMacAgent-*" > /dev/null 2>&1; then
    return 0
  fi
  install -d -m 755 "${PANEL_STATIC_DIR}"
  local atomic_publisher="${extracted_root}/scripts/atomic_publish.py"
  local architecture=""
  local published_count=0
  if [[ -f "${atomic_publisher}" ]] \
    && command -v python3 >/dev/null 2>&1; then
    for architecture in arm64 x86_64; do
      local archive="${companion_dir}/RemoteMacAgent-${architecture}.zip"
      local sha_file="${archive}.sha256"
      local version_file="${archive}.version"
      [[ -f "${archive}" && -f "${sha_file}" && -f "${version_file}" ]] \
        || continue
      if ! python3 "${atomic_publisher}" publish-remote-mac-agent \
        --staging-root "${companion_dir}" \
        --target-root "${PANEL_STATIC_DIR}" \
        --architecture "${architecture}"; then
        err "原子发布 RemoteMacAgent ${architecture} companion 失败"
        return 1
      fi
      published_count=$((published_count + 1))
    done
  fi
  if (( published_count > 0 )); then
    ok "已从仓库分发的 RemoteMacAgent companion 原子更新 /static"
    return 0
  fi
  # 兼容 fallback（原子 publisher 不可用或未发布任何完整架构 triplet）：
  # 先完整校验全部架构 triplet（zip/.sha256/.version 齐备且配对），再 staging，
  # 最后单次原子提交；任何不完整集合都在目标目录零改动下失败。
  if ! install_companion_triplets_atomically "${companion_dir}"; then
    err "从更新包安装 RemoteMacAgent companion 失败"
    return 1
  fi
  ok "已从仓库分发的 RemoteMacAgent companion 更新 /static"
}

# 校验单个架构的 companion triplet：zip、.sha256、.version 三者齐备且配对。
verify_companion_triplet(){
  local archive="$1"
  local sha_file="${archive}.sha256"
  local version_file="${archive}.version"
  if [[ ! -f "${archive}" || ! -f "${sha_file}" || ! -f "${version_file}" ]]; then
    err "RemoteMacAgent companion triplet 不完整：${archive}"
    return 1
  fi
  if [[ ! -s "${version_file}" ]]; then
    err "RemoteMacAgent companion .version 为空：${version_file}"
    return 1
  fi
  local expected=""
  expected="$(read_sha256_sidecar "${sha_file}" "$(basename "${archive}")" || true)"
  if [[ -z "${expected}" ]]; then
    err "RemoteMacAgent companion SHA256 无效：${sha_file}"
    return 1
  fi
  if ! verify_file_sha256 "${archive}" "${expected}"; then
    err "RemoteMacAgent companion SHA256 不配对：${archive}"
    return 1
  fi
}

# 兼容 fallback 的原子发布：先完整校验全部 triplet，再同文件系统 staging，
# 最后单次 mv 一次性提交到 /static；任何一步失败都不触碰目标目录。
install_companion_triplets_atomically(){
  local companion_dir="$1"
  local -a archives=()
  local archive="" stage=""
  if ! compgen -G "${companion_dir}/RemoteMacAgent-*.zip" > /dev/null 2>&1; then
    return 0
  fi
  for archive in "${companion_dir}"/RemoteMacAgent-*.zip; do
    [[ -f "${archive}" ]] || continue
    if ! verify_companion_triplet "${archive}"; then
      return 1
    fi
    archives+=("${archive}")
  done
  if (( ${#archives[@]} == 0 )); then
    return 0
  fi
  stage="$(mktemp -d "${PANEL_STATIC_DIR}/.remote-mac-agent.publish.XXXXXX")" || {
    err "无法创建 RemoteMacAgent companion staging 目录"
    return 1
  }
  register_cleanup_path "${stage}"
  chmod 700 "${stage}" >/dev/null 2>&1 || true
  for archive in "${archives[@]}"; do
    if ! cp -a "${archive}" "${archive}.sha256" "${archive}.version" "${stage}/"; then
      err "暂存 RemoteMacAgent companion 失败：${archive}"
      return 1
    fi
  done
  if ! mv "${stage}"/RemoteMacAgent-* "${PANEL_STATIC_DIR}/"; then
    err "提交 RemoteMacAgent companion 失败，目标目录未改动"
    return 1
  fi
  rmdir "${stage}" >/dev/null 2>&1 || true
  return 0
}

sha256_file(){
  local path="$1"
  [[ -f "${path}" ]] || return 1
  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum "${path}" | awk '{print tolower($1)}'
  elif command -v shasum >/dev/null 2>&1; then
    shasum -a 256 "${path}" | awk '{print tolower($1)}'
  else
    err "缺少 sha256sum/shasum，无法校验文件：${path}"
    return 1
  fi
}

normalize_sha256(){
  local raw="${1:-}"
  raw="${raw%$'\r'}"
  raw="${raw%$'\n'}"
  raw="${raw#"${raw%%[![:space:]]*}"}"
  raw="${raw%"${raw##*[![:space:]]}"}"
  raw="$(printf '%s' "${raw}" | tr 'A-F' 'a-f')"
  [[ "${raw}" =~ ^[0-9a-f]{64}$ ]] || return 1
  printf '%s\n' "${raw}"
}

verify_file_sha256(){
  local path="$1"
  local expected=""
  local actual=""
  expected="$(normalize_sha256 "${2:-}" || true)"
  if [[ -z "${expected}" ]]; then
    err "缺少有效 SHA256，拒绝使用文件：${path}"
    return 1
  fi
  actual="$(sha256_file "${path}" || true)"
  if [[ -z "${actual}" || "${actual}" != "${expected}" ]]; then
    err "SHA256 不匹配：${path}（期望 ${expected}，实际 ${actual:-unknown}）"
    return 1
  fi
}

release_public_key_bits(){
  local public_key="$1"
  local key_text=""
  local bits=""
  command -v openssl >/dev/null 2>&1 || return 1
  key_text="$(openssl pkey -pubin -in "${public_key}" -text_pub -noout 2>/dev/null)" \
    || return 1
  printf '%s\n' "${key_text}" | grep -q '^[[:space:]]*Modulus:' || return 1
  bits="$(
    printf '%s\n' "${key_text}" \
      | sed -n \
        -e 's/^[[:space:]]*Public-Key: (\([0-9][0-9]*\) bit)[[:space:]]*$/\1/p' \
        -e 's/^[[:space:]]*RSA[[:space:]][[:space:]]*Public-Key: (\([0-9][0-9]*\) bit)[[:space:]]*$/\1/p' \
      | head -n 1
  )"
  [[ "${bits}" =~ ^[0-9]+$ ]] || return 1
  (( bits >= 3072 )) || return 1
  printf '%s\n' "${bits}"
}

verify_signed_release_manifest(){
  local manifest_file="$1"
  local signature_file="$2"
  local required_capability="$3"
  local records_file="${4:-}"
  local public_key="${REALM_RELEASE_PUBLIC_KEY_FILE:-}"
  local public_key_bits=""
  local signature_size=""

  if [[ -z "${public_key}" || "${public_key}" != /* \
        || ! -f "${public_key}" || -L "${public_key}" ]]; then
    err "缺少本机受信 release 公钥文件 REALM_RELEASE_PUBLIC_KEY_FILE"
    return 1
  fi
  if ! python3 - "${public_key}" "${manifest_file}" "${signature_file}" <<'PY'
import os
import pathlib
import stat
import sys

trusted_uids = {0, os.geteuid()}
for raw in sys.argv[1:]:
    path = pathlib.Path(raw)
    if not path.is_absolute():
        raise SystemExit(f"trusted release path is not absolute: {path}")
    metadata = path.lstat()
    if (
        stat.S_ISLNK(metadata.st_mode)
        or not stat.S_ISREG(metadata.st_mode)
        or metadata.st_nlink != 1
        or metadata.st_uid not in trusted_uids
        or stat.S_IMODE(metadata.st_mode) & 0o022
    ):
        raise SystemExit(f"trusted release file is unsafe: {path}")
    current = path.parent
    while True:
        parent = current.lstat()
        sticky_root = (
            parent.st_uid == 0
            and bool(stat.S_IMODE(parent.st_mode) & stat.S_ISVTX)
        )
        if (
            stat.S_ISLNK(parent.st_mode)
            or not stat.S_ISDIR(parent.st_mode)
            or parent.st_uid not in trusted_uids
            or (
                stat.S_IMODE(parent.st_mode) & 0o022
                and not sticky_root
            )
        ):
            raise SystemExit(f"trusted release parent is unsafe: {current}")
        if current == current.parent:
            break
        current = current.parent
PY
  then
    err "受信 release 公钥权限不安全"
    return 1
  fi
  public_key_bits="$(release_public_key_bits "${public_key}" || true)"
  if [[ -z "${public_key_bits}" ]]; then
    err "受信 release 公钥必须是 RSA 3072-bit 或更强"
    return 1
  fi
  signature_size="$(wc -c < "${signature_file}" 2>/dev/null | tr -d '[:space:]')"
  if [[ "${signature_size}" != "$((public_key_bits / 8))" ]]; then
    err "release manifest 签名长度与 RSA 公钥不匹配"
    return 1
  fi
  if ! openssl dgst -sha256 -verify "${public_key}" \
    -signature "${signature_file}" "${manifest_file}" >/dev/null 2>&1; then
    err "release manifest 公钥签名校验失败"
    return 1
  fi
  if ! python3 - "${manifest_file}" "${required_capability}" "${records_file}" \
    "${REALM_RELEASE_EXPECTED_VERSION:-}" \
    "${REALM_RELEASE_EXPECTED_COMMIT:-}" \
    "${REALM_RELEASE_EXPECTED_BUILD_ID:-}" <<'PY'
import json
import pathlib
import re
import sys

manifest_path = pathlib.Path(sys.argv[1])
required_capability = sys.argv[2]
records_path = pathlib.Path(sys.argv[3]) if sys.argv[3] else None
expected_version, expected_commit, expected_build_id = sys.argv[4:7]
raw = manifest_path.read_bytes()
payload = json.loads(raw)
expected_fields = {
    "schema", "version", "commit", "build_id", "capabilities",
    "source_files", "files", "release_metadata",
}
if not isinstance(payload, dict) or set(payload) != expected_fields:
    raise SystemExit("release manifest fields are invalid")
schema = payload["schema"]
if schema not in {1, 2}:
    raise SystemExit("unsupported release manifest schema")
if not re.fullmatch(r"[0-9][0-9A-Za-z._+-]{0,63}", payload["version"]):
    raise SystemExit("release version is invalid")
if not re.fullmatch(r"[0-9a-f]{40}([0-9a-f]{24})?", payload["commit"]):
    raise SystemExit("release commit is invalid")
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:-]{0,127}", payload["build_id"]):
    raise SystemExit("release build_id is invalid")
release_metadata = payload["release_metadata"]
if release_metadata is not None and (
    not isinstance(release_metadata, dict)
    or set(release_metadata) != {"path", "projection", "sha256", "size"}
    or release_metadata.get("path") != "release.json"
    or release_metadata.get("projection") != "identity-archive-site-v1"
    or not re.fullmatch(r"[0-9a-f]{64}", str(release_metadata.get("sha256", "")))
    or isinstance(release_metadata.get("size"), bool)
    or not isinstance(release_metadata.get("size"), int)
    or release_metadata["size"] < 0
):
    raise SystemExit("release metadata binding is invalid")
capabilities = payload["capabilities"]
if (
    not isinstance(capabilities, list)
    or capabilities != sorted(set(capabilities))
    or required_capability not in capabilities
    or not all(
        isinstance(item, str)
        and re.fullmatch(r"[a-z0-9][a-z0-9._-]{0,63}", item)
        for item in capabilities
    )
):
    raise SystemExit("release capabilities are invalid")
for label, actual, expected in (
    ("version", payload["version"], expected_version),
    ("commit", payload["commit"], expected_commit.lower()),
    ("build_id", payload["build_id"], expected_build_id),
):
    if expected and actual != expected:
        raise SystemExit(f"release {label} drift detected")
files = payload["files"]
if not isinstance(files, dict) or not files or list(files) != sorted(files):
    raise SystemExit("release files are invalid")
source_files = payload["source_files"]
if (
    not isinstance(source_files, list)
    or not source_files
    or source_files != sorted(set(source_files))
    or any(path not in files for path in source_files)
):
    raise SystemExit("release source_files are invalid")
records = []
for path, entry in files.items():
    parts = pathlib.PurePosixPath(path)
    if (
        not isinstance(path, str)
        or not path
        or parts.is_absolute()
        or "\\" in path
        or any(ord(character) < 32 or ord(character) == 127 for character in path)
        or any(part in {"", ".", ".."} for part in parts.parts)
        or not isinstance(entry, dict)
    ):
        raise SystemExit("release file entry is invalid")
    allowed = (
        ({"sha256", "size"}, {"sha256", "size", "url"})
        if schema == 1
        else (
            {"sha256", "size", "mode"},
            {"sha256", "size", "mode", "url"},
        )
    )
    if set(entry) not in allowed:
        raise SystemExit("release file entry is invalid")
    digest = entry.get("sha256")
    size = entry.get("size")
    if not isinstance(digest, str) or not re.fullmatch(r"[0-9a-f]{64}", digest):
        raise SystemExit("release file digest is invalid")
    if isinstance(size, bool) or not isinstance(size, int) or size < 0:
        raise SystemExit("release file size is invalid")
    mode = "0644" if schema == 1 else entry.get("mode")
    if (
        not isinstance(mode, str)
        or re.fullmatch(r"0[0-7]{3}", mode) is None
    ):
        raise SystemExit("release file mode is invalid")
    if path in source_files:
        records.append((digest, str(size), str(mode), path))
canonical = (
    json.dumps(payload, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
    + "\n"
).encode()
if raw != canonical:
    raise SystemExit("release manifest is not canonical JSON")
if records_path is not None:
    records_path.write_text(
        "".join("\t".join(row) + "\n" for row in records),
        encoding="utf-8",
    )
PY
  then
    err "release manifest 字段、能力或期望值校验失败"
    return 1
  fi
}

download_signed_release_manifest(){
  local manifest_url="$1"
  local manifest_file="$2"
  local required_capability="$3"
  local records_file="${4:-}"
  local signature_url="${REALM_RELEASE_MANIFEST_SIGNATURE_URL:-${manifest_url%%\?*}.sig}"
  local signature_file="${manifest_file}.sig"
  local manifest_sha=""

  require_secure_asset_url "${manifest_url}" "release manifest" || return 1
  require_secure_asset_url "${signature_url}" "release manifest signature" || return 1
  manifest_sha="$(normalize_sha256 "${REALM_RELEASE_MANIFEST_SHA256:-}" || true)"
  if [[ -n "${manifest_sha}" ]]; then
    download_verified_file \
      "${manifest_url}" "${manifest_file}" "${manifest_sha}" "release manifest" \
      || return 1
  else
    download_file "${manifest_url}" "${manifest_file}" || return 1
  fi
  download_file "${signature_url}" "${signature_file}" || return 1
  verify_signed_release_manifest \
    "${manifest_file}" "${signature_file}" "${required_capability}" "${records_file}"
}

replace_regular_file_atomically(){
  local source="$1"
  local target="$2"
  python3 - "${source}" "${target}" <<'PY'
import os
import pathlib
import stat
import sys

source = pathlib.Path(sys.argv[1])
target = pathlib.Path(sys.argv[2])
trusted_uids = {0, os.geteuid()}
source_meta = source.lstat()
parent_meta = target.parent.lstat()
if (
    source.is_symlink()
    or not stat.S_ISREG(source_meta.st_mode)
    or source_meta.st_nlink != 1
    or source_meta.st_uid not in trusted_uids
):
    raise SystemExit("atomic replacement source is unsafe")
if (
    target.parent.is_symlink()
    or not stat.S_ISDIR(parent_meta.st_mode)
    or parent_meta.st_uid not in trusted_uids
    or stat.S_IMODE(parent_meta.st_mode) & 0o022
):
    raise SystemExit("atomic replacement parent is unsafe")
try:
    target_meta = target.lstat()
except FileNotFoundError:
    target_meta = None
if target_meta is not None and stat.S_ISDIR(target_meta.st_mode):
    raise SystemExit("atomic replacement target is a directory")
with source.open("rb") as handle:
    os.fsync(handle.fileno())
os.replace(source, target)
directory_fd = os.open(target.parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
try:
    os.fsync(directory_fd)
finally:
    os.close(directory_fd)
PY
}

install_root_file_atomically(){
  local source="$1"
  local target="$2"
  local mode="$3"
  local stage=""

  install -d -m 755 "$(dirname "${target}")" || return 1
  stage="$(mktemp "${target}.tmp.XXXXXX")" || return 1
  register_cleanup_path "${stage}"
  install -o root -g root -m "${mode}" "${source}" "${stage}" || return 1
  replace_regular_file_atomically "${stage}" "${target}"
}

capture_release_manifest(){
  local source_manifest="$1"
  local source_signature="$2"
  local manifest_stage=""
  local signature_stage=""

  if [[ -z "${RELEASE_MANIFEST_FILE:-}" \
        || -z "${RELEASE_MANIFEST_SIGNATURE_FILE:-}" ]]; then
    err "release manifest 事务路径未初始化"
    return 1
  fi
  if [[ ! -f "${source_manifest}" || -L "${source_manifest}" \
        || ! -f "${source_signature}" || -L "${source_signature}" ]]; then
    err "release manifest 或签名文件无效"
    return 1
  fi
  manifest_stage="$(mktemp "${RELEASE_MANIFEST_FILE}.tmp.XXXXXX")" || return 1
  signature_stage="$(
    mktemp "${RELEASE_MANIFEST_SIGNATURE_FILE}.tmp.XXXXXX"
  )" || return 1
  register_cleanup_path "${manifest_stage}"
  register_cleanup_path "${signature_stage}"
  cp -f "${source_manifest}" "${manifest_stage}" || return 1
  cp -f "${source_signature}" "${signature_stage}" || return 1
  chmod 0600 "${manifest_stage}" "${signature_stage}" || return 1
  replace_regular_file_atomically \
    "${manifest_stage}" "${RELEASE_MANIFEST_FILE}" || return 1
  replace_regular_file_atomically \
    "${signature_stage}" "${RELEASE_MANIFEST_SIGNATURE_FILE}" || return 1
  verify_signed_release_manifest \
    "${RELEASE_MANIFEST_FILE}" \
    "${RELEASE_MANIFEST_SIGNATURE_FILE}" \
    "panel-install"
}

pin_release_public_key(){
  local source_public_key="${REALM_RELEASE_PUBLIC_KEY_FILE:-}"
  local pinned_public_key="${TMPDIR}/release-public-key.pem"

  if [[ -z "${source_public_key}" || "${source_public_key}" != /* ]]; then
    err "缺少安全的本机受信 release 公钥文件"
    return 1
  fi
  if ! python3 -I -S - "${source_public_key}" "${pinned_public_key}" <<'PY'
import os
import pathlib
import stat
import sys

source = pathlib.PurePosixPath(sys.argv[1])
target = pathlib.Path(sys.argv[2])
expected_uid = os.geteuid()
directory_flags = (
    os.O_RDONLY
    | getattr(os, "O_CLOEXEC", 0)
    | getattr(os, "O_DIRECTORY", 0)
    | getattr(os, "O_NOFOLLOW", 0)
)
file_flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)

if not source.is_absolute() or source.name in {"", ".", ".."}:
    raise SystemExit("release public key path is invalid")

directory_fd = os.open("/", directory_flags)
try:
    root_stat = os.fstat(directory_fd)
    if (
        root_stat.st_uid != expected_uid
        or not stat.S_ISDIR(root_stat.st_mode)
        or stat.S_IMODE(root_stat.st_mode) & 0o022
    ):
        raise SystemExit("release public key root directory is unsafe")
    for component in source.parts[1:-1]:
        if component in {"", ".", ".."}:
            raise SystemExit("release public key path component is invalid")
        next_fd = os.open(component, directory_flags, dir_fd=directory_fd)
        os.close(directory_fd)
        directory_fd = next_fd
        metadata = os.fstat(directory_fd)
        if (
            metadata.st_uid != expected_uid
            or not stat.S_ISDIR(metadata.st_mode)
            or stat.S_IMODE(metadata.st_mode) & 0o022
        ):
            raise SystemExit("release public key parent directory is unsafe")
    source_fd = os.open(source.name, file_flags, dir_fd=directory_fd)
finally:
    os.close(directory_fd)

try:
    metadata = os.fstat(source_fd)
    if (
        not stat.S_ISREG(metadata.st_mode)
        or metadata.st_uid != expected_uid
        or metadata.st_nlink != 1
        or stat.S_IMODE(metadata.st_mode) & 0o022
        or metadata.st_size <= 0
        or metadata.st_size > 1024 * 1024
    ):
        raise SystemExit("release public key file is unsafe")
    target_parent = target.parent
    target_parent_stat = os.lstat(target_parent)
    if (
        not stat.S_ISDIR(target_parent_stat.st_mode)
        or target_parent_stat.st_uid != expected_uid
        or stat.S_IMODE(target_parent_stat.st_mode) & 0o077
    ):
        raise SystemExit("release public key staging directory is unsafe")
    target_fd = os.open(
        target,
        os.O_WRONLY
        | os.O_CREAT
        | os.O_EXCL
        | getattr(os, "O_CLOEXEC", 0)
        | getattr(os, "O_NOFOLLOW", 0),
        0o600,
    )
    try:
        while True:
            chunk = os.read(source_fd, 64 * 1024)
            if not chunk:
                break
            view = memoryview(chunk)
            while view:
                written = os.write(target_fd, view)
                if written <= 0:
                    raise OSError("short release public key write")
                view = view[written:]
        os.fchmod(target_fd, 0o600)
        os.fsync(target_fd)
    finally:
        os.close(target_fd)
    target_directory_fd = os.open(target_parent, directory_flags)
    try:
        os.fsync(target_directory_fd)
    finally:
        os.close(target_directory_fd)
finally:
    os.close(source_fd)
PY
  then
    err "本机受信 release 公钥路径、所有权或权限不安全"
    return 1
  fi
  if [[ -z "$(release_public_key_bits "${pinned_public_key}" || true)" ]]; then
    err "受信 release 公钥副本必须是 RSA 3072-bit 或更强"
    return 1
  fi
  RELEASE_PUBLIC_KEY_FILE="${pinned_public_key}"
  REALM_RELEASE_PUBLIC_KEY_FILE="${pinned_public_key}"
  export REALM_RELEASE_PUBLIC_KEY_FILE
}

install_panel_release_trust_metadata(){
  local source_manifest="${RELEASE_MANIFEST_FILE:-${REALM_RELEASE_MANIFEST_FILE:-}}"
  local source_signature="${RELEASE_MANIFEST_SIGNATURE_FILE:-${REALM_RELEASE_MANIFEST_SIGNATURE_FILE:-}}"
  local source_public_key="${REALM_RELEASE_PUBLIC_KEY_FILE:-}"
  local temp_dir=""
  local manifest_file=""
  local signature_file=""
  local trusted_public_key="${PANEL_ROOT}/release-public-key.pem"
  local identity=""

  temp_dir="$(mktemp -d -t realm-panel-release-trust.XXXXXX)" || return 1
  register_cleanup_path "${temp_dir}"
  manifest_file="${temp_dir}/release-manifest.json"
  signature_file="${manifest_file}.sig"
  if [[ ! -f "${source_manifest}" || -L "${source_manifest}" \
        || ! -f "${source_signature}" || -L "${source_signature}" ]]; then
    err "本次安装事务没有已固定的 release manifest 与签名"
    return 1
  fi
  cp -f "${source_manifest}" "${manifest_file}" || return 1
  cp -f "${source_signature}" "${signature_file}" || return 1
  verify_signed_release_manifest \
    "${manifest_file}" "${signature_file}" "panel-install" || return 1

  identity="$(
    python3 - "${manifest_file}" <<'PY'
import json
import pathlib
import sys

payload = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))
print("\t".join((payload["version"], payload["commit"], payload["build_id"])))
PY
  )" || return 1
  local version=""
  local commit=""
  local build_id=""
  IFS=$'\t' read -r version commit build_id <<< "${identity}"
  install_root_file_atomically \
    "${manifest_file}" "${PANEL_ROOT}/release-manifest.json" 0644 || return 1
  install_root_file_atomically \
    "${signature_file}" "${PANEL_ROOT}/release-manifest.json.sig" 0644 || return 1
  install_root_file_atomically \
    "${source_public_key}" "${trusted_public_key}" 0644 || return 1
  REALM_RELEASE_PUBLIC_KEY_FILE="${trusted_public_key}"
  export REALM_RELEASE_PUBLIC_KEY_FILE
  verify_signed_release_manifest \
    "${PANEL_ROOT}/release-manifest.json" \
    "${PANEL_ROOT}/release-manifest.json.sig" \
    "panel-install" || return 1
  set_panel_env_value \
    "${PANEL_CONFIG_DIR}/panel.env" \
    REALM_RELEASE_MANIFEST_PATH \
    "${PANEL_ROOT}/release-manifest.json"
  set_panel_env_value \
    "${PANEL_CONFIG_DIR}/panel.env" \
    REALM_RELEASE_MANIFEST_SIGNATURE_PATH \
    "${PANEL_ROOT}/release-manifest.json.sig"
  set_panel_env_value \
    "${PANEL_CONFIG_DIR}/panel.env" \
    REALM_RELEASE_PUBLIC_KEY_FILE \
    "${trusted_public_key}"
  set_panel_env_value \
    "${PANEL_CONFIG_DIR}/panel.env" \
    REALM_RELEASE_PUBLIC_KEY_PATH \
    "${trusted_public_key}"
  set_panel_env_value \
    "${PANEL_CONFIG_DIR}/panel.env" REALM_RELEASE_EXPECTED_VERSION "${version}"
  set_panel_env_value \
    "${PANEL_CONFIG_DIR}/panel.env" REALM_RELEASE_EXPECTED_COMMIT "${commit}"
  set_panel_env_value \
    "${PANEL_CONFIG_DIR}/panel.env" REALM_RELEASE_EXPECTED_BUILD_ID "${build_id}"
}

validate_zip_archive(){
  local archive="$1"
  [[ -f "${archive}" ]] || return 1
  python3 - "${archive}" <<'PY'
import pathlib
import stat
import sys
import zipfile

archive_path = pathlib.Path(sys.argv[1])
try:
    with zipfile.ZipFile(archive_path) as archive:
        entries = archive.infolist()
        if not entries or len(entries) > 20000:
            raise SystemExit("zip entry count is invalid")
        total = 0
        files = set()
        dirs = set()
        implied_dirs = set()
        folded = set()
        for info in entries:
            raw = info.filename
            path = pathlib.PurePosixPath(raw)
            if (
                not raw
                or path.is_absolute()
                or "\\" in raw
                or any(part in {"", ".", ".."} for part in path.parts)
            ):
                raise SystemExit(f"unsafe zip path: {raw!r}")
            mode = (info.external_attr >> 16) & 0xFFFF
            if stat.S_ISLNK(mode):
                raise SystemExit(f"zip symlink is not allowed: {raw}")
            total += max(0, int(info.file_size))
            if total > 4 * 1024 * 1024 * 1024:
                raise SystemExit("zip expands beyond the safety limit")
            norm = str(path)
            key = norm.casefold()
            is_dir = raw.endswith("/")
            if key in folded:
                raise SystemExit(f"duplicate zip member: {raw!r}")
            if is_dir:
                if norm in files:
                    raise SystemExit(f"zip file/directory collision: {raw!r}")
                for i in range(1, len(path.parts)):
                    prefix = "/".join(path.parts[:i])
                    if prefix in files:
                        raise SystemExit(f"zip file/directory collision: {raw!r}")
                dirs.add(norm)
                folded.add(key)
                continue
            if norm in dirs or norm in implied_dirs:
                raise SystemExit(f"zip file/directory collision: {raw!r}")
            for i in range(1, len(path.parts)):
                prefix = "/".join(path.parts[:i])
                if prefix in files:
                    raise SystemExit(f"zip file/directory collision: {raw!r}")
            files.add(norm)
            folded.add(key)
            for i in range(1, len(path.parts)):
                implied_dirs.add("/".join(path.parts[:i]))
        bad = archive.testzip()
        if bad:
            raise SystemExit(f"corrupt zip member: {bad}")
except (OSError, zipfile.BadZipFile) as exc:
    raise SystemExit(f"invalid zip archive: {exc}") from exc
PY
}

read_sha256_sidecar(){
  local sidecar="$1"
  local asset_name="${2:-}"
  local text=""
  [[ -f "${sidecar}" ]] || return 1
  text="$(cat "${sidecar}" 2>/dev/null || true)"
  find_sha256_in_text "${text}" "${asset_name}"
}

download_verified_file(){
  local url="$1"
  local out="$2"
  local expected_sha="$3"
  local label="${4:-文件}"
  local verified_tmp=""
  expected_sha="$(normalize_sha256 "${expected_sha}" || true)"
  if [[ -z "${expected_sha}" ]]; then
    err "${label} 缺少有效 SHA256，拒绝下载执行内容：${url}"
    return 1
  fi
  verified_tmp="$(mktemp "${out}.verified.XXXXXX")" || return 1
  register_cleanup_path "${verified_tmp}"
  if ! download_file "${url}" "${verified_tmp}"; then
    rm -f "${verified_tmp}" >/dev/null 2>&1 || true
    return 1
  fi
  if ! verify_file_sha256 "${verified_tmp}" "${expected_sha}"; then
    err "${label} SHA256 校验失败：${url}"
    rm -f "${verified_tmp}" >/dev/null 2>&1 || true
    return 1
  fi
  if ! mv -f "${verified_tmp}" "${out}"; then
    rm -f "${verified_tmp}" >/dev/null 2>&1 || true
    return 1
  fi
}

expected_sha256_for_url(){
  local url="$1"
  local explicit="${2:-}"
  local asset_name="${3:-${url%%\?*}}"
  local expected=""
  local checksum_file=""

  expected="$(normalize_sha256 "${explicit}" || true)"
  if [[ -z "${expected}" && "${url}" == *\?* ]]; then
    local query="${url#*\?}"
    local pair=""
    local -a query_parts=()
    query="${query%%#*}"
    IFS='&' read -r -a query_parts <<< "${query}"
    for pair in "${query_parts[@]}"; do
      if [[ "${pair}" == sha256=* ]]; then
        expected="$(normalize_sha256 "${pair#sha256=}" || true)"
        break
      fi
    done
  fi
  if [[ -n "${expected}" ]]; then
    printf '%s\n' "${expected}"
    return 0
  fi

  asset_name="$(basename "${asset_name}")"
  if [[ "${url}" == file://* ]]; then
    local source_path="${url#file://}"
    source_path="${source_path%%\?*}"
    for checksum_file in "${source_path}.sha256" "${source_path}.sha256sum"; do
      expected="$(read_sha256_sidecar "${checksum_file}" "${asset_name}" || true)"
      if [[ -n "${expected}" ]]; then
        printf '%s\n' "${expected}"
        return 0
      fi
    done
    return 1
  fi

  # Remote sidecars are controlled by the same origin as the asset. Only an
  # explicit digest or already-verified release metadata is a trust anchor.
  return 1
}

resolve_release_archive(){
  local metadata_url="${1:-${REALM_PANEL_RELEASE_METADATA_URL:-${REPO_RELEASE_METADATA_URL:-${REPO_RELEASE_METADATA_URL_DEFAULT}}}}"
  local pinned_manifest="${2:-}"
  local expected_git_sha="${REALM_PANEL_RELEASE_GIT_SHA:-${REPO_RELEASE_GIT_SHA:-}}"
  local metadata_file=""
  local fields=""
  local git_sha=""
  local archive_url=""
  local archive_sha=""
  local archive_size=""

  if [[ -n "${pinned_manifest}" ]]; then
    metadata_file="${pinned_manifest}"
  else
    metadata_file="$(mktemp -t realm-panel-release.XXXXXX)" || return 1
    register_cleanup_path "${metadata_file}"
  fi
  if ! download_signed_release_manifest \
    "${metadata_url}" "${metadata_file}" "panel-install"; then
    return 1
  fi

  fields="$(
    python3 - "${metadata_file}" "${metadata_url}" <<'PY'
import json
import pathlib
import re
import sys
import urllib.parse

metadata_path = pathlib.Path(sys.argv[1])
metadata_url = sys.argv[2]
payload = json.loads(metadata_path.read_text(encoding="utf-8"))
git_sha = str(payload.get("commit") or "").strip().lower()
archive_path = f"nexus/archive/releases/{git_sha}.zip"
entry = payload.get("files", {}).get(archive_path, {})
archive_sha = str(entry.get("sha256") or "").strip().lower()
archive_size = entry.get("size")

if not re.fullmatch(r"[0-9a-f]{40}([0-9a-f]{24})?", git_sha):
    raise SystemExit("release metadata git_sha is invalid")
if not re.fullmatch(r"[0-9a-f]{64}", archive_sha):
    raise SystemExit("release metadata archive_sha256 is invalid")
try:
    archive_size = int(archive_size)
except (TypeError, ValueError, OverflowError) as exc:
    raise SystemExit("release metadata archive_size is invalid") from exc
if archive_size <= 0:
    raise SystemExit("release metadata archive_size must be positive")

parts = pathlib.PurePosixPath(archive_path)
if (
    not archive_path
    or parts.is_absolute()
    or "\\" in archive_path
    or any(part in {"", ".", ".."} for part in parts.parts)
):
    raise SystemExit("release metadata archive_path is unsafe")
if parts.as_posix() != f"nexus/archive/releases/{git_sha}.zip":
    raise SystemExit("release metadata archive_path is not content-addressed")

parsed = urllib.parse.urlsplit(metadata_url)
if parsed.scheme == "file":
    base = pathlib.Path(urllib.parse.unquote(parsed.path)).parent
    archive_url = (base.joinpath(*parts.parts)).resolve().as_uri()
elif parsed.scheme == "https" and parsed.netloc:
    archive_url = urllib.parse.urlunsplit(
        ("https", parsed.netloc, "/" + "/".join(parts.parts), "", "")
    )
else:
    raise SystemExit("unsupported release metadata URL")

print("\t".join((git_sha, archive_url, archive_sha, str(archive_size))))
PY
  )" || {
    err "release 元数据格式或不可变来源字段无效：${metadata_url}"
    return 1
  }

  IFS=$'\t' read -r git_sha archive_url archive_sha archive_size <<< "${fields}"
  if [[ -n "${expected_git_sha}" \
        && "$(printf '%s' "${expected_git_sha}" | tr 'A-F' 'a-f')" != "${git_sha}" ]]; then
    err "release git_sha 与预期值不一致"
    return 1
  fi
  [[ -n "${archive_url}" && -n "${archive_sha}" && "${archive_size}" =~ ^[0-9]+$ ]] || return 1
  printf '%s\t%s\t%s\t%s\n' "${git_sha}" "${archive_url}" "${archive_sha}" "${archive_size}"
}

verify_remote_control_static_asset(){
  local source_panel_dir="$1"
  local src="${source_panel_dir%/}/static/remote_control.js"
  local dst="${PANEL_ROOT}/panel/static/remote_control.js"
  if [[ ! -f "${src}" ]]; then
    err "更新包缺少远控脚本：${src}"
    return 1
  fi
  if [[ ! -f "${dst}" ]]; then
    err "面板远控脚本未落盘：${dst}"
    return 1
  fi
  local src_sha dst_sha
  src_sha="$(sha256_file "${src}")" || return 1
  dst_sha="$(sha256_file "${dst}")" || return 1
  if [[ "${src_sha}" != "${dst_sha}" ]]; then
    err "远控脚本校验失败：更新包 ${src_sha}，已安装 ${dst_sha}"
    return 1
  fi
  ok "远控脚本已更新：${dst_sha}"
}

manifest_concurrency(){
  local raw="${REPO_MANIFEST_CONCURRENCY:-${REPO_MANIFEST_CONCURRENCY_DEFAULT}}"
  if [[ ! "${raw}" =~ ^[0-9]+$ ]]; then
    echo "${REPO_MANIFEST_CONCURRENCY_DEFAULT}"
    return
  fi
  if (( raw < 1 )); then
    raw=1
  fi
  if (( raw > 32 )); then
    raw=32
  fi
  echo "${raw}"
}

download_repo_manifest_path(){
  local base_url="${1%/}"
  local out_dir="$2"
  local path="$3"
  local bust="$4"
  local expected_sha="$5"
  local expected_size="$6"
  local expected_mode="$7"
  local src="${base_url}/${path}"
  local dest="${out_dir}/${path}"
  local actual_size=""
  mkdir -p "$(dirname "${dest}")"
  if ! download_verified_file \
    "${src}?${bust}" "${dest}" "${expected_sha}" "manifest 文件 ${path}"; then
    if ! download_verified_file \
      "${src}" "${dest}" "${expected_sha}" "manifest 文件 ${path}"; then
      err "下载文件失败：${src}"
      return 1
    fi
  fi
  actual_size="$(wc -c < "${dest}" | tr -d '[:space:]')" || return 1
  if [[ "${actual_size}" != "${expected_size}" ]]; then
    err "manifest 文件大小校验失败：${path}"
    rm -f "${dest}" || true
    return 1
  fi
  if [[ ! "${expected_mode}" =~ ^0[0-7]{3}$ ]] \
    || ! python3 - "${dest}" "${expected_mode}" <<'PY'
import os
import pathlib
import stat
import sys

path = pathlib.Path(sys.argv[1])
expected = int(sys.argv[2], 8)
os.chmod(path, expected)
metadata = path.lstat()
if (
    stat.S_ISLNK(metadata.st_mode)
    or not stat.S_ISREG(metadata.st_mode)
    or metadata.st_nlink != 1
    or stat.S_IMODE(metadata.st_mode) != expected
):
    raise SystemExit("downloaded manifest file mode mismatch")
PY
  then
    err "manifest 文件模式校验失败：${path}"
    rm -f "${dest}" || true
    return 1
  fi
}

parse_manifest_paths(){
  local manifest_file="$1"
  local paths_file="$2"
  : > "${paths_file}"

  if command -v python3 >/dev/null 2>&1; then
    if python3 - "${manifest_file}" "${paths_file}" <<'PY'
import json
import pathlib
import sys

manifest_file = pathlib.Path(sys.argv[1])
paths_file = pathlib.Path(sys.argv[2])
text = manifest_file.read_text(encoding="utf-8", errors="ignore")
entries = None
hashes = {}
try:
    data = json.loads(text)
except Exception:
    data = None

if isinstance(data, dict):
    entries = data.get("files")
    raw_hashes = data.get("sha256", data.get("hashes", {}))
    if isinstance(raw_hashes, dict):
        hashes = raw_hashes

seen = set()
records = []

def add_path(raw, raw_sha=None):
    if isinstance(raw, dict):
        raw_sha = raw.get("sha256")
        raw = raw.get("path")
    if not isinstance(raw, str) or not isinstance(raw_sha, str):
        raise SystemExit(4)
    p = raw.strip().replace("\r", "")
    sha = raw_sha.strip().lower()
    parts = pathlib.PurePosixPath(p)
    if (
        not p
        or p.startswith("#")
        or parts.is_absolute()
        or "\\" in p
        or any(part in {"", ".", ".."} for part in parts.parts)
    ):
        raise SystemExit(4)
    if len(sha) != 64 or any(char not in "0123456789abcdef" for char in sha):
        raise SystemExit(4)
    if p in seen:
        raise SystemExit(4)
    seen.add(p)
    records.append((sha, p))

if isinstance(entries, list):
    for item in entries:
        if isinstance(item, str):
            add_path(item, hashes.get(item))
        else:
            add_path(item)

if not records:
    raise SystemExit(3)

paths_file.write_text(
    "".join(f"{sha}\t{path}\n" for sha, path in records),
    encoding="utf-8",
)
PY
    then
      return 0
    fi
  fi

  err "manifest 必须是 JSON，且每个 files 条目必须绑定 SHA256"
  return 1
}

render_manifest_progress(){
  local done="$1"
  local total="$2"
  local width=28
  local percent filled i
  local bar=""
  if (( total <= 0 )); then
    return
  fi
  if (( done < 0 )); then
    done=0
  fi
  if (( done > total )); then
    done="${total}"
  fi
  percent=$(( done * 100 / total ))
  filled=$(( done * width / total ))
  for ((i=0; i<filled; i++)); do
    bar="${bar}#"
  done
  for ((i=filled; i<width; i++)); do
    bar="${bar}-"
  done
  printf "\r\033[33m[提示]\033[0m 文件拉取进度 [%s] %3d%% (%d/%d)" "${bar}" "${percent}" "${done}" "${total}" >&2
  if (( done >= total )); then
    printf "\n" >&2
  fi
}

build_abs_url(){
  local base_url="${1%/}"
  local raw="$2"
  local origin
  if [[ -z "${raw}" ]]; then
    echo ""
    return
  fi
  if [[ "${raw}" == http://* || "${raw}" == https://* ]]; then
    echo "${raw}"
    return
  fi
  origin="$(echo "${base_url}" | sed -E 's#^(https?://[^/]+).*$#\1#')"
  if [[ "${raw}" == /* ]]; then
    echo "${origin}${raw}"
    return
  fi
  raw="${raw#./}"
  echo "${base_url}/${raw}"
}

probe_url_200(){
  local url="$1"
  local code
  code="$(curl -L --max-redirs 5 --connect-timeout 8 --max-time 20 -s -o /dev/null -w '%{http_code}' "${url}" 2>/dev/null || true)"
  [[ "${code}" == "200" ]]
}

discover_repo_zip_url(){
  local base_url="${1%/}"
  if [[ -n "${REPO_ZIP_URL:-}" ]]; then
    echo "${REPO_ZIP_URL}"
    return 0
  fi
  : "${base_url}"
  return 1
}

guess_repo_base_from_zip_url(){
  local zip_url="$1"
  case "$zip_url" in
    */archive/refs/heads/*.zip) echo "${zip_url%/archive/refs/heads/*.zip}" ;;
    */archive/*.zip) echo "${zip_url%/archive/*.zip}" ;;
    *) echo "${REPO_BASE_URL_DEFAULT}" ;;
  esac
}

repo_fallback_base_url(){
  local url="${REPO_FALLBACK_BASE_URL:-${REPO_FALLBACK_BASE_URL_DEFAULT}}"
  echo "${url%/}"
}

# 回退端点支持空格分隔列表（REPO_FALLBACK_BASE_URLS），
# 同时保留单个 REPO_FALLBACK_BASE_URL 的旧配置；逐行输出。
# 用 read -a 按 IFS 拆分（不展开 glob），URL 含 * ? [ 等通配符时不被
# 文件系统展开。
repo_fallback_base_urls(){
  local urls="${REPO_FALLBACK_BASE_URLS:-${REPO_FALLBACK_BASE_URL:-}}"
  local -a url_list=()
  local url=""
  read -r -a url_list <<< "${urls}" || true
  for url in "${url_list[@]}"; do
    [[ -n "${url}" ]] || continue
    printf '%s\n' "${url%/}"
  done
}

repo_fallback_manifest_url(){
  if [[ -n "${REPO_FALLBACK_MANIFEST_URL:-}" ]]; then
    echo "${REPO_FALLBACK_MANIFEST_URL}"
    return
  fi
  local base=""
  base="$(repo_fallback_base_url)"
  [[ -n "${base}" ]] || return 1
  echo "${base}/release-manifest.json"
}

repo_fallback_zip_url(){
  echo "${REPO_FALLBACK_ZIP_URL:-${REPO_FALLBACK_ZIP_URL_DEFAULT}}"
}

download_repo_from_manifest(){
  local base_url="${1%/}"
  local manifest_url="$2"
  local out_dir="$3"
  local manifest_file="${out_dir}/manifest.json"
  local paths_file="${out_dir}/manifest.paths"
  local downloaded=0
  local -a paths=()
  local -a hashes=()
  local -a sizes=()
  local -a modes=()
  local -a pids=()
  local bust concurrency running failed completed
  mkdir -p "${out_dir}"
  bust="ts=$(date +%s)"

  info "拉取并验证签名 release manifest..."
  if ! download_signed_release_manifest \
    "${manifest_url}" "${manifest_file}" "panel-install" "${paths_file}"; then
    err "签名 release manifest 下载或验证失败"
    return 1
  fi
  if ! capture_release_manifest "${manifest_file}" "${manifest_file}.sig"; then
    err "无法固定本次仓库文件事务的签名 release manifest"
    return 1
  fi

  local hash size mode path
  while IFS=$'\t' read -r hash size mode path \
    || [[ -n "${hash}${size}${mode}${path}" ]]; do
    [[ -z "${path}" ]] && continue
    [[ "${size}" =~ ^[0-9]+$ ]] || {
      err "签名 manifest size 记录无效：${path}"
      return 1
    }
    [[ "${mode}" =~ ^0[0-7]{3}$ ]] || {
      err "签名 manifest mode 记录无效：${path}"
      return 1
    }
    hashes+=("${hash}")
    sizes+=("${size}")
    modes+=("${mode}")
    paths+=("${path}")
  done < "${paths_file}"

  downloaded="${#paths[@]}"
  if [[ "${downloaded}" -eq 0 ]]; then
    err "仓库文件清单为空"
    return 1
  fi

  concurrency="$(manifest_concurrency)"
  info "开始拉取文件（并发 ${concurrency}，共 ${downloaded} 个）"
  running=0
  failed=0
  completed=0
  render_manifest_progress "${completed}" "${downloaded}"
  local index=0
  local wait_index=0
  for path in "${paths[@]}"; do
    while (( running >= concurrency )); do
      if ! wait "${pids[$wait_index]}"; then
        failed=1
      fi
      wait_index=$((wait_index+1))
      running=$((running-1))
      completed=$((completed+1))
      render_manifest_progress "${completed}" "${downloaded}"
    done
    (
      download_repo_manifest_path \
        "${base_url}" "${out_dir}" "${path}" "${bust}" \
        "${hashes[$index]}" "${sizes[$index]}" "${modes[$index]}"
    ) &
    pids+=("$!")
    running=$((running+1))
    index=$((index+1))
  done
  while (( running > 0 )); do
    if ! wait "${pids[$wait_index]}"; then
      failed=1
    fi
    wait_index=$((wait_index+1))
    running=$((running-1))
    completed=$((completed+1))
    render_manifest_progress "${completed}" "${downloaded}"
  done
  if [[ "${failed}" -ne 0 ]]; then
    return 1
  fi
  ok "仓库文件拉取完成（共 ${downloaded} 个）"
}

download_repo_from_manifest_with_fallback(){
  local base_url="${1%/}"
  local manifest_url="$2"
  local out_dir="$3"
  local fallback_base fallback_manifest
  local -a fallback_pairs=()

  if download_repo_from_manifest "${base_url}" "${manifest_url}" "${out_dir}"; then
    return 0
  fi

  # 显式 manifest URL 与单个 base 配对（旧配置）；列表形式则逐 base
  # 使用其 /release-manifest.json。不猜测任何新端点。
  if [[ -n "${REPO_FALLBACK_MANIFEST_URL:-}" ]]; then
    fallback_base="$(repo_fallback_base_url)"
    if [[ -n "${fallback_base}" ]]; then
      fallback_pairs+=("${fallback_base}|${REPO_FALLBACK_MANIFEST_URL}")
    fi
  else
    while IFS= read -r fallback_base; do
      [[ -n "${fallback_base}" ]] || continue
      fallback_pairs+=("${fallback_base}|${fallback_base}/release-manifest.json")
    done < <(repo_fallback_base_urls)
  fi
  if [[ "${#fallback_pairs[@]}" -eq 0 ]]; then
    return 1
  fi

  local pair=""
  for pair in "${fallback_pairs[@]}"; do
    fallback_base="${pair%%|*}"
    fallback_manifest="${pair#*|}"
    if [[ "${base_url}" == "${fallback_base}" \
      && "${manifest_url}" == "${fallback_manifest}" ]]; then
      continue
    fi
    info "主源清单拉取失败，切换显式固定的备用源：${fallback_base}"
    rm -rf "${out_dir}" || true
    mkdir -p "${out_dir}"
    if download_repo_from_manifest \
      "${fallback_base}" "${fallback_manifest}" "${out_dir}"; then
      ok "已从显式固定的备用源拉取仓库文件：${fallback_base}"
      return 0
    fi
    err "备用源拉取失败，继续尝试下一个：${fallback_base}"
  done
  err "全部备用源均拉取失败（共 ${#fallback_pairs[@]} 个）"
  return 1
}

latest_realm_tag(){
  local raw="${REALM_AGENT_REALM_RELEASE_TAG:-${REALM_RELEASE_TAG:-${REALM_RELEASE_TAG_DEFAULT}}}"
  raw="${raw##*/}"
  raw="${raw#v}"
  if [[ "$(printf '%s' "${raw}" | tr '[:upper:]' '[:lower:]')" == "latest" ]]; then
    err "Realm release tag 不能使用 latest；必须固定版本"
    return 1
  fi
  if [[ -z "${raw}" || ! "${raw}" =~ ^[0-9][0-9A-Za-z._-]*$ ]]; then
    raw="${REALM_RELEASE_TAG_DEFAULT#v}"
  fi
  printf 'v%s\n' "${raw}"
}

realm_release_tag(){
  local raw="${REALM_AGENT_REALM_RELEASE_TAG:-${REALM_RELEASE_TAG:-${REALM_RELEASE_TAG_DEFAULT}}}"
  raw="${raw##*/}"
  raw="${raw#v}"
  if [[ -z "${raw}" || ! "${raw}" =~ ^[0-9][0-9A-Za-z._-]*$ ]]; then
    raw="${REALM_RELEASE_TAG_DEFAULT#v}"
  fi
  printf 'v%s\n' "${raw}"
}

embedded_realm_asset_sha256(){
  local asset_name="$1"
  local release_ref="${2:-${REALM_RELEASE_TAG_DEFAULT}}"
  local tag="${release_ref##*/}"
  tag="${tag#v}"
  tag="v${tag}"
  [[ "${tag}" == "v2.9.3" ]] || return 1
  case "${asset_name}" in
    realm-aarch64-unknown-linux-gnu.tar.gz) printf '%s\n' "9937daacdcdfcac9fd78d25819f2de0a5c3357c2c49e686679d812343ab8661e" ;;
    realm-aarch64-unknown-linux-musl.tar.gz) printf '%s\n' "1e5065ae147423647a63ed2dd27b6faee9800b7882eec8656946cbda54ee796f" ;;
    realm-x86_64-unknown-linux-gnu.tar.gz) printf '%s\n' "2eba86f1a1e47c1bfe9d6fd682ef8667bd05e57c3aeb0ec37806aabe2ce74a0c" ;;
    realm-x86_64-unknown-linux-musl.tar.gz) printf '%s\n' "622932d21eb74d1683dc71bf596227038ce8f72b01aa77906e899be548bb3792" ;;
    *) return 1 ;;
  esac
}

fetch_realm_release_asset_sha256(){
  local release_tag="$1"
  local asset_name="$2"
  local embedded_sha key configured=""
  embedded_sha="$(embedded_realm_asset_sha256 "${asset_name}" "${release_tag}" || true)"
  if [[ -n "${embedded_sha}" ]]; then
    printf '%s\n' "${embedded_sha}"
    return 0
  fi
  key="$(
    printf 'REALM_PANEL_REALM_ASSET_SHA256_%s' "${asset_name}" \
      | tr '[:lower:].-' '[:upper:]__'
  )"
  configured="${!key:-}"
  configured="$(normalize_sha256 "${configured}" || true)"
  [[ -n "${configured}" ]] || return 1
  printf '%s\n' "${configured}"
}

realm_asset_valid_for_tag(){
  local dest="$1"
  local release_tag="$2"
  local current_tag="$3"
  local asset_name="$4"
  local file="${dest}/${asset_name}"
  local sidecar="${file}.sha256"
  local recorded_sha actual_sha expected_sha
  [[ -s "${file}" && -s "${sidecar}" ]] || return 1
  recorded_sha="$(awk 'NR==1 {print tolower($1)}' "${sidecar}" 2>/dev/null || true)"
  [[ "${recorded_sha}" =~ ^[0-9a-f]{64}$ ]] || return 1
  actual_sha="$(sha256sum "${file}" | awk '{print tolower($1)}')"
  [[ "${actual_sha}" == "${recorded_sha}" ]] || return 1
  expected_sha="$(fetch_realm_release_asset_sha256 "${release_tag}" "${asset_name}" || true)"
  if [[ -n "${expected_sha}" ]]; then
    [[ "${expected_sha}" == "${recorded_sha}" ]]
    return $?
  fi
  [[ "${current_tag}" == "${release_tag}" ]]
}

realm_assets_ready_for_tag(){
  local dest="$1"
  local release_tag="$2"
  local current_tag="$3"
  local archs=("x86_64" "aarch64")
  local flavors=("unknown-linux-gnu.tar.gz" "unknown-linux-musl.tar.gz")
  local a f filename
  [[ -d "${dest}" ]] || return 1
  for a in "${archs[@]}"; do
    for f in "${flavors[@]}"; do
      filename="realm-${a}-${f}"
      realm_asset_valid_for_tag "${dest}" "${release_tag}" "${current_tag}" "${filename}" || return 1
    done
  done
  return 0
}

# 发布 realm 资产清单（realm-assets.json）：{"tag": ..., "assets": {name: sha256}}，
# 并发布 realm-assets.json.sha256。Panel Join 只有在清单 sidecar、清单内容、
# 每个资产 sidecar 与资产实算摘要全部一致时，才会把 tag/清单摘要下发给 Agent。
# 发布前逐项校验 sidecar 为 64 位 hex 并与同名 tar.gz 实算摘要一致，
# 不一致即报错拒发——坏清单会阻断依赖清单的内网 agent 安装。两文件原子替换。
write_realm_assets_manifest(){
  local dest="$1"
  local release_tag="$2"
  local manifest_path="${dest}/realm-assets.json"
  local manifest_sidecar="${manifest_path}.sha256"
  local manifest_sha=""
  if ! manifest_sha="$(python3 - "${manifest_path}" "${release_tag}" "${dest}" <<'PY'
import hashlib, json, os, pathlib, re, stat, sys, tempfile
manifest_path, release_tag, dest = sys.argv[1], sys.argv[2], sys.argv[3]
expected = (
    "realm-aarch64-unknown-linux-gnu.tar.gz",
    "realm-aarch64-unknown-linux-musl.tar.gz",
    "realm-x86_64-unknown-linux-gnu.tar.gz",
    "realm-x86_64-unknown-linux-musl.tar.gz",
)
if not re.fullmatch(r"v[0-9][0-9A-Za-z._-]{0,63}", release_tag):
    raise SystemExit(1)

def snapshot(metadata):
    return (
        metadata.st_dev,
        metadata.st_ino,
        metadata.st_mode,
        metadata.st_nlink,
        metadata.st_uid,
        metadata.st_size,
        metadata.st_mtime_ns,
        metadata.st_ctime_ns,
    )

def read_regular(path, max_bytes=None):
    flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0)
    flags |= getattr(os, "O_NOFOLLOW", 0)
    fd = os.open(path, flags)
    try:
        opened = os.fstat(fd)
        if (
            not stat.S_ISREG(opened.st_mode)
            or opened.st_nlink != 1
            or stat.S_IMODE(opened.st_mode) & 0o022
        ):
            raise SystemExit(1)
        digest = hashlib.sha256()
        captured = bytearray()
        with os.fdopen(fd, "rb", closefd=True) as handle:
            fd = -1
            total = 0
            for chunk in iter(lambda: handle.read(1024 * 1024), b""):
                total += len(chunk)
                if max_bytes is not None and total > max_bytes:
                    raise SystemExit(1)
                digest.update(chunk)
                if max_bytes is not None:
                    captured.extend(chunk)
            finished = os.fstat(handle.fileno())
        current = path.lstat()
        if (
            stat.S_ISLNK(current.st_mode)
            or snapshot(opened) != snapshot(finished)
            or (opened.st_dev, opened.st_ino) != (current.st_dev, current.st_ino)
        ):
            raise SystemExit(1)
        return bytes(captured), digest.hexdigest()
    finally:
        if fd >= 0:
            os.close(fd)

def sidecar_digest(payload, expected_name):
    lines = [
        line.strip()
        for line in payload.decode("ascii").splitlines()
        if line.strip()
    ]
    if len(lines) != 1:
        raise SystemExit(1)
    fields = lines[0].split()
    if (
        len(fields) not in (1, 2)
        or not re.fullmatch(r"[0-9a-fA-F]{64}", fields[0])
        or (len(fields) == 2 and fields[1].lstrip("*") != expected_name)
    ):
        raise SystemExit(1)
    return fields[0].lower()

def atomic_write(path, payload):
    fd, tmp = tempfile.mkstemp(
        prefix=f".{path.name}.tmp.",
        dir=path.parent,
    )
    try:
        os.fchmod(fd, 0o644)
        with os.fdopen(fd, "wb", closefd=True) as handle:
            fd = -1
            handle.write(payload)
            handle.flush()
            os.fsync(handle.fileno())
        os.replace(tmp, path)
    finally:
        if fd >= 0:
            os.close(fd)
        pathlib.Path(tmp).unlink(missing_ok=True)

root = pathlib.Path(dest)
assets = {}
for name in expected:
    sidecar = root / f"{name}.sha256"
    sidecar_payload, _ = read_regular(sidecar, max_bytes=1024)
    recorded = sidecar_digest(sidecar_payload, name)
    _asset_payload, actual = read_regular(root / name)
    if actual != recorded:
        raise SystemExit(1)
    assets[name] = recorded

manifest = {"tag": release_tag, "assets": assets}
manifest_bytes = (
    json.dumps(manifest, indent=2, sort_keys=True) + "\n"
).encode("utf-8")
manifest_sha = hashlib.sha256(manifest_bytes).hexdigest()
manifest_file = pathlib.Path(manifest_path)
atomic_write(manifest_file, manifest_bytes)
atomic_write(
    manifest_file.with_name(f"{manifest_file.name}.sha256"),
    f"{manifest_sha}  {manifest_file.name}\n".encode("ascii"),
)
print(manifest_sha)
PY
  )"; then
    err "发布 realm 资产清单失败（${manifest_path}）"
    return 1
  fi
  if [[ ! "${manifest_sha}" =~ ^[0-9a-f]{64}$ ]]; then
    err "发布 realm 资产清单失败（${manifest_path}）"
    return 1
  fi
  chmod 0644 "${manifest_path}" "${manifest_sidecar}" >/dev/null 2>&1 || true
  info "realm 资产清单已发布：${manifest_path}"
  info "Panel Join 将自动下发 Realm tag=${release_tag}、manifest SHA256=${manifest_sha}"
}

prepare_realm_assets(){
  local dest="${PANEL_STATIC_REALM_DIR}"
  mkdir -p "${dest}"
  if [[ -d "${PANEL_REALM_CACHE_DIR}" ]]; then
    sync_dir_mirror "${PANEL_REALM_CACHE_DIR}" "${dest}" || true
  fi
  local latest_tag current_tag
  latest_tag="$(latest_realm_tag)"
  current_tag="$(cat "${PANEL_REALM_TAG_STAMP}" 2>/dev/null || true)"

  local archs=("x86_64" "aarch64")
  local flavors=("unknown-linux-gnu.tar.gz" "unknown-linux-musl.tar.gz")

  if [[ -n "${latest_tag}" ]] && realm_assets_ready_for_tag "${dest}" "${latest_tag}" "${current_tag}"; then
    if ! write_realm_assets_manifest "${dest}" "${latest_tag}"; then
      return 1
    fi
    echo "${latest_tag}" > "${PANEL_REALM_TAG_STAMP}" || return 1
    if ! sync_dir_mirror "${dest}" "${PANEL_REALM_CACHE_DIR}"; then
      err "realm 资源缓存同步失败（${PANEL_REALM_CACHE_DIR}）"
    fi
    ok "realm 资源已是最新（${latest_tag}），跳过下载"
    return 0
  fi

  info "同步 realm 二进制到面板..."
  local pids=()
  local names=()
  local asset_idx=0
  local total_assets=$(( ${#archs[@]} * ${#flavors[@]} ))
  for arch in "${archs[@]}"; do
    for flavor in "${flavors[@]}"; do
      asset_idx=$((asset_idx + 1))
      local filename="realm-${arch}-${flavor}"
      if realm_asset_valid_for_tag "${dest}" "${latest_tag}" "${current_tag}" "${filename}"; then
        ok "realm 二进制同步进度 (${asset_idx}/${total_assets}) 复用 ${filename}"
        continue
      fi
      local url="https://github.com/zhboner/realm/releases/download/${latest_tag}/${filename}"
      info "realm 二进制同步进度 (${asset_idx}/${total_assets}) 下载 ${filename}"
      (
        tmp_file="$(mktemp "${dest}/.${filename}.tmp.XXXXXX")" || exit 1
        trap 'rm -f "${tmp_file}" "${tmp_file}".tmp.* >/dev/null 2>&1 || true' EXIT HUP INT TERM
        if ! download_file "${url}" "${tmp_file}"; then
          exit 1
        fi
        expected_sha="$(fetch_realm_release_asset_sha256 "${latest_tag}" "${filename}" || true)"
        actual_sha="$(sha256sum "${tmp_file}" | awk '{print tolower($1)}')"
        if [[ -z "${expected_sha}" || "${actual_sha}" != "${expected_sha}" ]]; then
          rm -f "${tmp_file}" || true
          exit 1
        fi
        mv -f "${tmp_file}" "${dest}/${filename}"
        printf "%s  %s\n" "${actual_sha}" "${filename}" > "${dest}/${filename}.sha256"
        trap - EXIT HUP INT TERM
      ) &
      pids+=($!)
      names+=("${filename}")
    done
  done

  local failed=0
  local i
  for i in "${!pids[@]}"; do
    if wait "${pids[$i]}"; then
      ok "已下载 ${names[$i]}"
    else
      err "下载失败：${names[$i]}"
      failed=$((failed+1))
    fi
  done

  if [[ "$failed" -ne 0 || -z "$latest_tag" ]]; then
    err "realm 二进制同步失败（失败 ${failed}/${total_assets}）"
    if env_enabled_default_true "${REALM_PANEL_REALM_ASSETS_STRICT:-1}"; then
      return 1
    fi
    err "继续安装/更新（REALM_PANEL_REALM_ASSETS_STRICT=0，节点从面板拉取 realm 可能失败）"
    return 0
  fi

  if [[ "$failed" -eq 0 && -n "$latest_tag" ]]; then
    if ! write_realm_assets_manifest "${dest}" "${latest_tag}"; then
      return 1
    fi
    echo "${latest_tag}" > "${PANEL_REALM_TAG_STAMP}" || return 1
    if ! sync_dir_mirror "${dest}" "${PANEL_REALM_CACHE_DIR}"; then
      err "realm 资源缓存同步失败（${PANEL_REALM_CACHE_DIR}）"
    fi
    ok "realm 二进制同步完成（${total_assets}/${total_assets}）"
  fi
}

find_sha256_in_text(){
  local text="$1"
  local asset_name="$2"
  local line hash
  while IFS= read -r line || [[ -n "${line}" ]]; do
    line="${line%$'\r'}"
    line="${line#"${line%%[![:space:]]*}"}"
    line="${line%"${line##*[![:space:]]}"}"
    [[ -z "${line}" ]] && continue
    if [[ -n "${asset_name}" && "${line}" != *"${asset_name}"* ]]; then
      if ! [[ "${line}" =~ ^[[:space:]]*[0-9a-fA-F]{64}[[:space:]]*$ ]]; then
        continue
      fi
    fi
    hash="$(printf '%s' "${line}" | awk '{print tolower($1)}')"
    if [[ "${hash}" =~ ^[0-9a-f]{64}$ ]]; then
      printf '%s\n' "${hash}"
      return 0
    fi
  done <<< "${text}"
  return 1
}

find_sha256_in_release_page(){
  local text="$1"
  local asset_name="$2"
  [[ -z "${text}" || -z "${asset_name}" ]] && return 1

  local flat escaped pair hash
  # GitHub release pages may embed JSON strings with escaped quotes/slashes.
  flat="$(printf '%s' "${text}" | tr '\n' ' ' | sed 's/\\"/"/g; s#\\/#/#g; s#\\u002F#/#g; s/&quot;/"/g')"
  escaped="$(printf '%s' "${asset_name}" | sed -e 's/[][(){}.^$+*?|\\/]/\\&/g')"

  pair="$(printf '%s' "${flat}" | grep -Eo "\"name\"[[:space:]]*:[[:space:]]*\"${escaped}\".{0,1400}\"digest\"[[:space:]]*:[[:space:]]*\"sha256:[0-9a-fA-F]{64}\"" | head -n1 || true)"
  if [[ -n "${pair}" ]]; then
    hash="$(printf '%s' "${pair}" | grep -Eo "sha256:[0-9a-fA-F]{64}" | head -n1 | cut -d: -f2 | tr 'A-F' 'a-f')"
    if [[ "${hash}" =~ ^[0-9a-f]{64}$ ]]; then
      printf '%s\n' "${hash}"
      return 0
    fi
  fi

  hash="$(
    printf '%s' "${flat}" | awk -v asset="${asset_name}" '
      BEGIN { IGNORECASE=1 }
      {
        s=$0
        pos=index(tolower(s), tolower(asset))
        while (pos > 0) {
          seg=substr(s, pos, 1800)
          if (match(seg, /sha256:[0-9a-fA-F]{64}/)) {
            print tolower(substr(seg, RSTART + 7, 64))
            exit
          }
          tail=substr(s, pos + length(asset))
          nxt=index(tolower(tail), tolower(asset))
          if (nxt <= 0) break
          pos=pos + length(asset) + nxt - 1
        }
      }
    ' | head -n1
  )"
  if [[ "${hash}" =~ ^[0-9a-f]{64}$ ]]; then
    printf '%s\n' "${hash}"
    return 0
  fi
  return 1
}

# Mirror the repository-level `shared/` package next to the panel install
# so `from shared.env import ...` resolves at runtime. Without this the
# panel systemd unit (which sets `WorkingDirectory=/opt/realm-panel/panel`)
# never sees the shared package and ImportError-fails on boot.
sync_shared_helpers_to_panel_root(){
  local panel_src_dir="$1"
  local src_shared_dir
  src_shared_dir="$(cd "${panel_src_dir}/.." 2>/dev/null && pwd || true)/shared"
  if [[ ! -d "${src_shared_dir}" ]]; then
    info "未找到源码 shared/ 目录（${src_shared_dir}），跳过同步"
    return 0
  fi
  mkdir -p "${PANEL_ROOT}/shared"
  if command -v rsync >/dev/null 2>&1; then
    rsync -a --delete "${src_shared_dir%/}/" "${PANEL_ROOT}/shared/"
  else
    rm -rf "${PANEL_ROOT}/shared"
    cp -a "${src_shared_dir}" "${PANEL_ROOT}/shared"
  fi
}

requirements_file_uses_hashes(){
  local req_file="${1:-}"
  [[ -n "${req_file}" && "${req_file}" == *.lock ]]
}

install_python_deps(){
  local venv_dir="${PANEL_ROOT}/venv"
  local req_lock="${PANEL_ROOT}/panel/requirements.lock"
  local req="${req_lock}"
  local -a hash_args=(--require-hashes)
  if [[ ! -s "${req}" ]]; then
    err "缺少带哈希的 Python 依赖锁：${req}"
    return 1
  fi
  if ! probe_panel_python_runtime "${venv_dir}/bin/python"; then
    err "现有面板 venv 不是可用的 Python 3.9+ 环境：${venv_dir}"
    return 1
  fi
  local req_hash old_hash
  req_hash="$(sha256sum "$req" | awk '{print $1}')"
  old_hash="$(cat "${PANEL_REQ_STAMP}" 2>/dev/null || true)"
  if [[ -d "$venv_dir" && "$req_hash" == "$old_hash" ]]; then
    if "${venv_dir}/bin/python" -m pip check >/dev/null 2>&1; then
      ok "Python 依赖未变化且 pip check 通过，跳过安装"
      return 0
    fi
    err "依赖戳匹配但 pip check 失败，拒绝假成功"
    return 1
  fi
  info "安装/更新 Python 依赖（来自 $(basename "$req")）..."
  "${venv_dir}/bin/python" -m pip \
    install --disable-pip-version-check --prefer-binary \
    "${hash_args[@]}" -r "$req" >/dev/null
  "${venv_dir}/bin/python" -m pip check >/dev/null
  echo "$req_hash" > "${PANEL_REQ_STAMP}"
}

build_panel_venv(){
  local venv_dir="$1"
  local panel_source="${2:-${PANEL_ROOT}/panel}"
  local req_lock="${panel_source}/requirements.lock"
  local req="${req_lock}" req_hash
  local python_bin="${PANEL_PYTHON_BIN:-}"
  local -a hash_args=(--require-hashes)
  if [[ "${venv_dir}" == "${PANEL_ROOT}/venv" ]]; then
    err "拒绝在当前可用 venv 上原地构建"
    return 1
  fi
  case "${venv_dir}" in
    "${PANEL_ROOT}/.venv-next"|"${PANEL_ROOT}/.venv-next."*) ;;
    *)
      err "venv staging 路径不受信任：${venv_dir}"
      return 1
      ;;
  esac
  if [[ ! -s "${req}" ]]; then
    err "缺少带哈希的 Python 依赖锁：${req}"
    return 1
  fi
  req_hash="$(sha256sum "$req" | awk '{print $1}')"
  if [[ -z "${python_bin}" ]]; then
    ensure_supported_panel_python_runtime || return 1
    python_bin="${PANEL_PYTHON_BIN}"
  fi
  python_bin="$(panel_python_builder_path "${python_bin}" || true)"
  if [[ -z "${python_bin}" ]]; then
    err "无法解析用于创建 staging venv 的 Python 解释器"
    return 1
  fi
  if ! probe_panel_python_runtime "${python_bin}"; then
    err "面板 Python 运行时不可用或低于 3.9：${python_bin}"
    return 1
  fi
  rm -rf "${venv_dir}"
  info "使用 ${python_bin} 创建并验证 Python 虚拟环境..."
  "${python_bin}" -m venv "${venv_dir}"
  if ! probe_panel_python_runtime "${venv_dir}/bin/python"; then
    err "新面板 venv 未通过 Python 3.9+/venv/ensurepip 校验"
    return 1
  fi
  "${venv_dir}/bin/python" -m pip \
    install --disable-pip-version-check --prefer-binary \
    "${hash_args[@]}" -r "$req" >/dev/null
  "${venv_dir}/bin/python" -c "import fastapi, uvicorn" >/dev/null
  "${venv_dir}/bin/python" -m pip check >/dev/null
  printf '%s\n' "${req_hash}" > "${venv_dir}/.requirements.sha256"
}

prepare_panel_venv_stage(){
  install -d -m 755 "${PANEL_ROOT}"
  PANEL_NEXT_VENV="$(mktemp -d "${PANEL_ROOT}/.venv-next.XXXXXX")"
  chmod 700 "${PANEL_NEXT_VENV}" >/dev/null 2>&1 || true
  register_cleanup_path "${PANEL_NEXT_VENV}"
}

activate_panel_venv(){
  local had_current="0"
  if [[ ! -x "${PANEL_NEXT_VENV}/bin/python" ]]; then
    err "待切换虚拟环境不存在：${PANEL_NEXT_VENV}"
    return 1
  fi
  if [[ -d "${PANEL_PREV_VENV}" ]]; then
    if [[ ! -d "${PANEL_ROOT}/venv" ]]; then
      err "检测到未完成的 venv 切换：${PANEL_PREV_VENV}；拒绝删除唯一旧环境"
      return 1
    fi
    rm -rf "${PANEL_PREV_VENV}"
  fi
  if [[ -d "${PANEL_ROOT}/venv" ]]; then
    if ! mv "${PANEL_ROOT}/venv" "${PANEL_PREV_VENV}"; then
      err "无法暂存当前 venv，拒绝切换"
      return 1
    fi
    had_current="1"
  fi
  if ! mv "${PANEL_NEXT_VENV}" "${PANEL_ROOT}/venv"; then
    err "新 venv 原子切换失败，正在恢复当前 venv"
    if [[ "${had_current}" == "1" && -d "${PANEL_PREV_VENV}" ]]; then
      mv "${PANEL_PREV_VENV}" "${PANEL_ROOT}/venv" >/dev/null 2>&1 || true
    fi
    return 1
  fi
  if [[ -f "${PANEL_ROOT}/venv/.requirements.sha256" ]]; then
    if ! cp -f "${PANEL_ROOT}/venv/.requirements.sha256" "${PANEL_REQ_STAMP}"; then
      err "venv 依赖戳写入失败，正在恢复当前 venv"
      rm -rf "${PANEL_NEXT_VENV}" >/dev/null 2>&1 || true
      mv "${PANEL_ROOT}/venv" "${PANEL_NEXT_VENV}" >/dev/null 2>&1 || true
      if [[ "${had_current}" == "1" && -d "${PANEL_PREV_VENV}" ]]; then
        mv "${PANEL_PREV_VENV}" "${PANEL_ROOT}/venv" >/dev/null 2>&1 || true
      fi
      return 1
    fi
  fi
}

stage_panel_code(){
  local panel_source="$1"
  local extracted_root=""
  local shared_source=""
  [[ -d "${panel_source}" ]] || return 1
  install -d -m 755 "${PANEL_ROOT}"
  PANEL_CODE_STAGE_DIR="$(mktemp -d "${PANEL_ROOT}/.deploy-next.XXXXXX")"
  chmod 700 "${PANEL_CODE_STAGE_DIR}" >/dev/null 2>&1 || true
  register_cleanup_path "${PANEL_CODE_STAGE_DIR}"
  cp -a "${panel_source}" "${PANEL_CODE_STAGE_DIR}/panel"
  extracted_root="$(dirname "${panel_source}")"
  shared_source="${extracted_root}/shared"
  if [[ -d "${shared_source}" ]]; then
    cp -a "${shared_source}" "${PANEL_CODE_STAGE_DIR}/shared"
  else
    err "未找到源码 shared/ 目录（${shared_source}），拒绝切换不完整版本"
    return 1
  fi
}

activate_panel_code_stage(){
  local had_panel="0"
  local had_shared="0"
  [[ -d "${PANEL_CODE_STAGE_DIR}/panel" ]] || {
    err "待切换 panel staging 不完整：${PANEL_CODE_STAGE_DIR}"
    return 1
  }

  if [[ -d "${PANEL_PREV_CODE_DIR}" && ! -d "${PANEL_ROOT}/panel" ]]; then
    err "检测到未完成的 panel 切换：${PANEL_PREV_CODE_DIR}；拒绝删除唯一旧版本"
    return 1
  fi
  if [[ -d "${PANEL_PREV_SHARED_DIR}" && ! -d "${PANEL_ROOT}/shared" ]]; then
    err "检测到未完成的 shared 切换：${PANEL_PREV_SHARED_DIR}；拒绝删除唯一旧版本"
    return 1
  fi
  rm -rf "${PANEL_PREV_CODE_DIR}" "${PANEL_PREV_SHARED_DIR}"
  if [[ -d "${PANEL_ROOT}/panel" ]]; then
    mv "${PANEL_ROOT}/panel" "${PANEL_PREV_CODE_DIR}" || return 1
    had_panel="1"
  fi
  if ! mv "${PANEL_CODE_STAGE_DIR}/panel" "${PANEL_ROOT}/panel"; then
    if [[ "${had_panel}" == "1" ]]; then
      mv "${PANEL_PREV_CODE_DIR}" "${PANEL_ROOT}/panel" >/dev/null 2>&1 || true
    fi
    err "panel 代码原子切换失败，已保留旧版本"
    return 1
  fi

  if [[ -d "${PANEL_CODE_STAGE_DIR}/shared" ]]; then
    if [[ -d "${PANEL_ROOT}/shared" ]]; then
      if ! mv "${PANEL_ROOT}/shared" "${PANEL_PREV_SHARED_DIR}"; then
        rm -rf "${PANEL_CODE_STAGE_DIR}/panel" >/dev/null 2>&1 || true
        mv "${PANEL_ROOT}/panel" "${PANEL_CODE_STAGE_DIR}/panel" >/dev/null 2>&1 || true
        if [[ "${had_panel}" == "1" ]]; then
          mv "${PANEL_PREV_CODE_DIR}" "${PANEL_ROOT}/panel" >/dev/null 2>&1 || true
        fi
        err "shared 目录暂存失败，已恢复旧 panel"
        return 1
      fi
      had_shared="1"
    fi
    if ! mv "${PANEL_CODE_STAGE_DIR}/shared" "${PANEL_ROOT}/shared"; then
      mv "${PANEL_ROOT}/panel" "${PANEL_CODE_STAGE_DIR}/panel" >/dev/null 2>&1 || true
      if [[ "${had_panel}" == "1" ]]; then
        mv "${PANEL_PREV_CODE_DIR}" "${PANEL_ROOT}/panel" >/dev/null 2>&1 || true
      fi
      if [[ "${had_shared}" == "1" ]]; then
        mv "${PANEL_PREV_SHARED_DIR}" "${PANEL_ROOT}/shared" >/dev/null 2>&1 || true
      fi
      err "shared 目录原子切换失败，已恢复旧版本"
      return 1
    fi
  fi
  rmdir "${PANEL_CODE_STAGE_DIR}" >/dev/null 2>&1 || true
  PANEL_CODE_STAGE_DIR=""
}

verify_panel_app_import(){
  local python_bin="${PANEL_ROOT}/venv/bin/python"
  if [[ ! -x "${python_bin}" ]]; then
    err "面板 Python 不可执行，无法执行启动预检：${python_bin}"
    return 1
  fi
  info "执行面板启动导入预检..."
  (
    cd "${PANEL_ROOT}/panel" || exit 1
    REALM_PANEL_PREFLIGHT_ENV_FILE="${PANEL_CONFIG_DIR}/panel.env" \
    PYTHONPATH="${PANEL_ROOT}:${PANEL_ROOT}/panel" \
      "${python_bin}" - <<'PY'
import os
import re
import shlex
from pathlib import Path

env_path = Path(os.environ["REALM_PANEL_PREFLIGHT_ENV_FILE"])
if env_path.is_file():
    for line_number, raw_line in enumerate(
        env_path.read_text(encoding="utf-8").splitlines(),
        start=1,
    ):
        line = raw_line.strip()
        if not line or line.startswith(("#", ";")):
            continue
        if line.startswith("export "):
            line = line[7:].lstrip()
        key, separator, raw_value = line.partition("=")
        key = key.strip()
        if not separator or re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", key) is None:
            raise SystemExit(
                f"invalid panel environment entry at {env_path}:{line_number}"
            )
        lexer = shlex.shlex(raw_value, posix=True)
        lexer.commenters = ""
        lexer.whitespace_split = True
        os.environ[key] = " ".join(lexer)

import app.main  # noqa: E402
PY
  ) || {
    err "面板启动导入预检失败，拒绝切换服务"
    return 1
  }
  ok "面板启动导入预检通过"
}

validate_panel_service_account_name(){
  local value="${1:-}"
  [[ "${value}" =~ ^[a-z_][a-z0-9_-]{0,30}$ ]]
}

ensure_panel_service_account(){
  local user="${PANEL_SERVICE_USER}"
  local group="${PANEL_SERVICE_GROUP}"
  local nologin="/usr/sbin/nologin"
  validate_panel_service_account_name "${user}" || {
    err "Panel service user 名称无效：${user}"
    return 1
  }
  validate_panel_service_account_name "${group}" || {
    err "Panel service group 名称无效：${group}"
    return 1
  }
  command -v getent >/dev/null 2>&1 \
    && command -v groupadd >/dev/null 2>&1 \
    && command -v useradd >/dev/null 2>&1 || {
      err "缺少 getent/groupadd/useradd，无法创建非 root Panel 账户"
      return 1
    }
  if ! getent group "${group}" >/dev/null 2>&1; then
    groupadd --system "${group}" || {
      err "创建 Panel system group 失败：${group}"
      return 1
    }
  fi
  if id -u "${user}" >/dev/null 2>&1; then
    if [[ "$(id -gn "${user}" 2>/dev/null || true)" != "${group}" ]]; then
      err "已存在的 Panel user 主组不匹配：${user}:${group}"
      return 1
    fi
    return 0
  fi
  [[ -x "${nologin}" ]] || nologin="/sbin/nologin"
  [[ -x "${nologin}" ]] || nologin="/bin/false"
  useradd --system --gid "${group}" --home-dir "${PANEL_RUNTIME_DIR}" \
    --shell "${nologin}" --no-create-home "${user}" || {
      err "创建 Panel system user 失败：${user}"
      return 1
    }
}

ensure_panel_runtime_dirs(){
  install -d -m 755 "${PANEL_ROOT}"
  install -d -m 700 "${PANEL_CONFIG_DIR}"
  install -d -m 700 "${PANEL_LOG_DIR}"
  install -d -m 700 "${PANEL_RUNTIME_DIR}/control_plane"
}

prepare_panel_service_permissions(){
  local path=""
  ensure_panel_service_account || return 1
  ensure_panel_runtime_dirs
  for path in "${PANEL_CONFIG_DIR}" "${PANEL_LOG_DIR}" "${PANEL_RUNTIME_DIR}"; do
    if [[ -L "${path}" || ! -d "${path}" ]]; then
      err "Panel 状态路径必须是实际目录：${path}"
      return 1
    fi
    chown -hR "${PANEL_SERVICE_USER}:${PANEL_SERVICE_GROUP}" "${path}" || {
      err "无法把 Panel 状态路径交给专用用户：${path}"
      return 1
    }
    chmod -R u+rwX,go-rwx "${path}" || return 1
  done

  chown root:root "${PANEL_ROOT}" || return 1
  chmod 0755 "${PANEL_ROOT}" || return 1
  for path in \
    "${PANEL_ROOT}/panel" \
    "${PANEL_ROOT}/shared" \
    "${PANEL_ROOT}/venv" \
    "${PANEL_ROOT}/start.sh" \
    "${PANEL_ROOT}/realm_panel.sh"; do
    [[ -e "${path}" || -L "${path}" ]] || continue
    chown -hR root:root "${path}" || {
      err "无法收紧 Panel 代码所有权：${path}"
      return 1
    }
    # Signed release assets record their read-only 0444/0555 modes. Keep the
    # entire root-owned runtime tree immutable to the service process so this
    # permission pass cannot create release-manifest drift.
    chmod -R u=rX,go=rX "${path}" || {
      err "无法收紧 Panel 代码权限：${path}"
      return 1
    }
  done

  [[ -x "${PANEL_ROOT}/venv/bin/python" ]] || {
    err "非 root Panel 切换前 venv Python 不可执行"
    return 1
  }
  [[ -r "${PANEL_ROOT}/panel/app/main.py" ]] || {
    err "非 root Panel 切换前应用源码不可读"
    return 1
  }
}

validate_panel_password(){
  local password="${1:-}"
  local classes=0
  [[ "${password}" =~ [[:lower:]] ]] && classes=$((classes + 1))
  [[ "${password}" =~ [[:upper:]] ]] && classes=$((classes + 1))
  [[ "${password}" =~ [[:digit:]] ]] && classes=$((classes + 1))
  [[ "${password}" =~ [^[:alnum:]] ]] && classes=$((classes + 1))
  if (( ${#password} < 12 )); then
    err "密码至少 12 位"
    return 1
  fi
  if (( classes < 3 )); then
    err "密码需包含大写字母、小写字母、数字、符号中的至少 3 类"
    return 1
  fi
}

prompt(){
  local msg="$1"; local def="${2:-}"; local out
  if [[ -n "$def" ]]; then
    read -r -p "$msg (默认 $def): " out || true
    echo "${out:-$def}"
  else
    read -r -p "$msg: " out || true
    echo "$out"
  fi
}

prompt_secret(){
  local msg="$1"; local out
  # -s：输入不回显；用于密码/密钥类信息
  # 注意：该函数常用于命令替换：pass="$(prompt_secret ...)"
  # 如果把换行输出到 stdout，会被命令替换捕获，导致密码前多出不可见字符（\n），登录会失败。
  # 因此：换行输出到 /dev/tty（或 stderr），stdout 只输出纯密码。
  if [[ -r /dev/tty && -w /dev/tty ]]; then
    read -r -s -p "$msg: " out </dev/tty || true
    echo >/dev/tty
  else
    read -r -s -p "$msg: " out || true
    echo >&2
  fi
  printf '%s' "$out"
}

# --- Panel public URL (domain / reverse proxy) ---

normalize_public_url(){
  local v="${1:-}"
  v="${v%%[[:space:]]*}"
  v="${v%/}"
  if [[ -z "$v" ]]; then
    echo ""
    return
  fi
  if [[ "$v" == http://* || "$v" == https://* ]]; then
    echo "${v%/}"
    return
  fi
  # 默认 https（常见反代/证书场景）；如需 http 请显式输入 http://
  echo "https://${v}"
}

detect_ip(){
  local ip=""
  if command -v curl >/dev/null 2>&1; then
    ip="$(curl -fsSL -4 https://api.ipify.org 2>/dev/null || true)"
  fi
  if [[ -z "$ip" ]]; then
    ip="$(detect_route_ipv4 || true)"
  fi
  if [[ -z "$ip" ]]; then
    ip="$(detect_hostname_ipv4 || true)"
  fi
  echo "${ip:-127.0.0.1}"
}

detect_route_ipv4(){
  if ! command -v ip >/dev/null 2>&1; then
    return 0
  fi
  ip -4 route get 1.1.1.1 2>/dev/null | awk '
    {
      for (i = 1; i <= NF; i++) {
        if ($i == "src" && (i + 1) <= NF && $(i + 1) ~ /^[0-9.]+$/) {
          print $(i + 1)
          exit
        }
      }
    }
  ' || true
}

detect_hostname_ipv4(){
  hostname -I 2>/dev/null | awk '
    {
      for (i = 1; i <= NF; i++) {
        if ($i ~ /^[0-9.]+$/ && $i !~ /^127\./ && $i !~ /^169\.254\./) {
          print $i
          exit
        }
      }
    }
  ' || true
}

# 当面板机器不是公网 IP（例如内网部署/无反代暴露）时，
# 需要优先使用本机内网 IP 来生成访问地址（避免误用出口公网 IP）。
detect_local_ip(){
  local ip=""
  ip="$(detect_route_ipv4 || true)"
  if [[ -z "$ip" ]]; then
    ip="$(detect_hostname_ipv4 || true)"
  fi
  echo "${ip:-127.0.0.1}"
}

REALM_PANEL_TMP_PARENT="${REALM_PANEL_TMP_PARENT:-${TMPDIR:-/tmp}}"
TMPDIR=""
EXTRACT_ROOT=""
PANEL_DIR=""
RELEASE_MANIFEST_FILE=""
RELEASE_MANIFEST_SIGNATURE_FILE=""
RELEASE_PUBLIC_KEY_FILE=""
cleanup(){
  local exit_code=$?
  local path
  if [[ "${PANEL_DEPLOY_ROLLBACK_ACTIVE:-0}" == "1" ]]; then
    restore_panel_deploy_rollback || true
  fi
  for path in "${__REALM_PANEL_CLEANUP_PATHS[@]:-}"; do
    [[ -n "${path}" && ( -e "${path}" || -L "${path}" ) ]] || continue
    if cleanup_path_is_preserved "${path}"; then
      continue
    fi
    rm -rf "${path}" >/dev/null 2>&1 || true
  done
  if ! cleanup_path_is_preserved "${PANEL_NEXT_VENV}"; then
    rm -rf "${PANEL_NEXT_VENV}" >/dev/null 2>&1 || true
  fi
  release_all_panel_update_locks
  return "${exit_code}"
}
trap cleanup EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM

snapshot_panel_sqlite_db(){
  local src_db="$1"
  local dst_db="$2"
  local python_bin="${PANEL_ROOT}/venv/bin/python"
  if [[ ! -f "${src_db}" ]]; then
    return 0
  fi
  if [[ ! -x "${python_bin}" ]]; then
    python_bin="$(command -v python3 || true)"
  fi
  if [[ -z "${python_bin}" || ! -x "${python_bin}" ]]; then
    err "缺少可用 Python，无法创建面板数据库一致性快照"
    return 1
  fi
  PANEL_SNAPSHOT_SRC_DB="${src_db}" PANEL_SNAPSHOT_DST_DB="${dst_db}" "${python_bin}" - <<'PY'
import os
import pathlib
import sqlite3

src = pathlib.Path(os.environ["PANEL_SNAPSHOT_SRC_DB"])
dst = pathlib.Path(os.environ["PANEL_SNAPSHOT_DST_DB"])
tmp = dst.with_suffix(dst.suffix + ".tmp")
for path in (tmp, pathlib.Path(str(tmp) + "-wal"), pathlib.Path(str(tmp) + "-shm")):
    try:
        path.unlink()
    except FileNotFoundError:
        pass
src_con = sqlite3.connect(str(src))
try:
    if src_con.execute("pragma integrity_check").fetchone()[0] != "ok":
        raise SystemExit("source sqlite integrity_check failed")
    dst_con = sqlite3.connect(str(tmp))
    try:
        src_con.backup(dst_con)
        dst_con.execute("pragma wal_checkpoint(TRUNCATE)")
    finally:
        dst_con.close()
finally:
    src_con.close()
clean_con = sqlite3.connect(str(tmp))
try:
    if clean_con.execute("pragma integrity_check").fetchone()[0] != "ok":
        raise SystemExit("snapshot sqlite integrity_check failed")
finally:
    clean_con.close()
tmp.replace(dst)
for suffix in ("-wal", "-shm"):
    try:
        pathlib.Path(str(dst) + suffix).unlink()
    except FileNotFoundError:
        pass
PY
}

snapshot_panel_config_dir(){
  local src_dir="$1"
  local dst_dir="$2"
  install -d -m 700 "${dst_dir}"
  (
    shopt -s dotglob nullglob
    local item name
    for item in "${src_dir}"/*; do
      name="$(basename "${item}")"
      case "${name}" in
        panel.db|panel.db-wal|panel.db-shm|panel.db-journal) continue ;;
      esac
      cp -a "${item}" "${dst_dir}/"
    done
  )
  if [[ -f "${src_dir}/panel.db" ]]; then
    if ! snapshot_panel_sqlite_db "${src_dir}/panel.db" "${dst_dir}/panel.db"; then
      err "面板数据库快照校验失败，终止更新以避免生成不可用回滚点"
      return 1
    fi
  fi
}

restore_directory_snapshot(){
  local src_dir="$1"
  local dst_dir="$2"
  local mode="${3:-755}"
  local parent tmp_dir old_dir
  parent="$(dirname "${dst_dir}")"
  mkdir -p "${parent}"
  tmp_dir="$(mktemp -d "${parent}/.$(basename "${dst_dir}").restore.XXXXXX")"
  old_dir="$(mktemp -d "${parent}/.$(basename "${dst_dir}").failed.XXXXXX")"
  rmdir "${old_dir}"
  register_cleanup_path "${tmp_dir}"
  register_cleanup_path "${old_dir}"
  chmod "${mode}" "${tmp_dir}"
  cp -a "${src_dir}/." "${tmp_dir}/"
  if [[ -e "${dst_dir}" || -L "${dst_dir}" ]]; then
    mv "${dst_dir}" "${old_dir}"
  fi
  if ! mv "${tmp_dir}" "${dst_dir}"; then
    if [[ -e "${old_dir}" || -L "${old_dir}" ]]; then
      if ! mv "${old_dir}" "${dst_dir}" >/dev/null 2>&1; then
        preserve_cleanup_path "${old_dir}"
        err "保留未能回切的目录：${old_dir}"
      fi
    fi
    return 1
  fi
  rm -rf "${old_dir}" >/dev/null 2>&1 || true
  chmod "${mode}" "${dst_dir}" || true
}

restore_previous_directory(){
  local previous="$1"
  local current="$2"
  local parent failed
  [[ -e "${previous}" || -L "${previous}" ]] || return 1
  parent="$(dirname "${current}")"
  failed="$(mktemp -d "${parent}/.$(basename "${current}").failed.XXXXXX")"
  rmdir "${failed}"
  register_cleanup_path "${failed}"
  if [[ -e "${current}" || -L "${current}" ]]; then
    mv "${current}" "${failed}" || return 1
  fi
  if mv "${previous}" "${current}"; then
    rm -rf "${failed}" >/dev/null 2>&1 || true
    return 0
  fi
  if [[ -e "${failed}" || -L "${failed}" ]]; then
    if ! mv "${failed}" "${current}" >/dev/null 2>&1; then
      preserve_cleanup_path "${failed}"
      err "保留未能回切的目录：${failed}"
    fi
  fi
  return 1
}

snapshot_panel_release_trust(){
  local rollback_dir="$1"
  local trust_dir="${rollback_dir}/release-trust"
  local name=""
  local source=""

  install -d -m 700 "${trust_dir}" || return 1
  for name in \
    release-manifest.json \
    release-manifest.json.sig \
    release-public-key.pem; do
    source="${PANEL_ROOT}/${name}"
    if [[ -f "${source}" && ! -L "${source}" ]]; then
      cp -a "${source}" "${trust_dir}/${name}" || return 1
    elif [[ -e "${source}" || -L "${source}" ]]; then
      err "面板 release trust 路径不是安全常规文件：${source}"
      return 1
    else
      : > "${trust_dir}/${name}.absent" || return 1
    fi
  done
}

restore_panel_release_trust(){
  local rollback_dir="$1"
  local trust_dir="${rollback_dir}/release-trust"
  local name=""
  local source=""
  local target=""
  local stage=""
  local failed="0"

  # Recovery snapshots created before v81 did not include these files.
  [[ -d "${trust_dir}" ]] || return 0
  install -d -m 755 "${PANEL_ROOT}" || return 1
  for name in \
    release-manifest.json \
    release-manifest.json.sig \
    release-public-key.pem; do
    source="${trust_dir}/${name}"
    target="${PANEL_ROOT}/${name}"
    if [[ -f "${source}" && ! -L "${source}" ]]; then
      stage="$(mktemp "${PANEL_ROOT}/.${name}.restore.XXXXXX")" || {
        failed="1"
        continue
      }
      register_cleanup_path "${stage}"
      if ! cp -p "${source}" "${stage}"; then
        failed="1"
        continue
      fi
      replace_regular_file_atomically "${stage}" "${target}" || failed="1"
    elif [[ -f "${trust_dir}/${name}.absent" ]]; then
      if [[ -d "${target}" && ! -L "${target}" ]]; then
        failed="1"
      else
        rm -f -- "${target}" || failed="1"
      fi
    else
      failed="1"
    fi
  done
  [[ "${failed}" == "0" ]]
}

begin_panel_deploy_rollback(){
  local reason="${1:-deploy}"
  local recovery_parent="${REALM_PANEL_RECOVERY_PARENT:-${PANEL_RUNTIME_DIR}/recovery}"
  acquire_panel_update_lock || exit 1
  install -d -m 700 "${recovery_parent}" || {
    err "创建面板 recovery 根目录失败：${recovery_parent}"
    exit 1
  }
  PANEL_DEPLOY_ROLLBACK_DIR="$(mktemp -d "${recovery_parent%/}/realm-panel-rollback.XXXXXX")"
  if [[ -z "${PANEL_DEPLOY_ROLLBACK_DIR}" || ! -d "${PANEL_DEPLOY_ROLLBACK_DIR}" ]]; then
    err "创建回滚快照目录失败"
    exit 1
  fi
  chmod 700 "${PANEL_DEPLOY_ROLLBACK_DIR}" >/dev/null 2>&1 || true
  register_cleanup_path "${PANEL_DEPLOY_ROLLBACK_DIR}"
  PANEL_SERVICE_WAS_ACTIVE="0"
  if systemctl is-active --quiet realm-panel.service 2>/dev/null; then
    PANEL_SERVICE_WAS_ACTIVE="1"
  fi
  if [[ -d "${PANEL_ROOT}/panel" ]]; then
    cp -a "${PANEL_ROOT}/panel" "${PANEL_DEPLOY_ROLLBACK_DIR}/panel"
  fi
  if [[ -d "${PANEL_ROOT}/shared" ]]; then
    cp -a "${PANEL_ROOT}/shared" "${PANEL_DEPLOY_ROLLBACK_DIR}/shared"
  fi
  if [[ -d "${PANEL_ROOT}/venv" ]]; then
    touch "${PANEL_DEPLOY_ROLLBACK_DIR}/venv.present"
  fi
  if [[ -f "${PANEL_REQ_STAMP}" ]]; then
    cp -a "${PANEL_REQ_STAMP}" "${PANEL_DEPLOY_ROLLBACK_DIR}/requirements.sha256"
  fi
  if [[ -d "${PANEL_CONFIG_DIR}" ]]; then
    snapshot_panel_config_dir "${PANEL_CONFIG_DIR}" "${PANEL_DEPLOY_ROLLBACK_DIR}/etc"
  fi
  snapshot_panel_release_trust "${PANEL_DEPLOY_ROLLBACK_DIR}" || exit 1
  snapshot_panel_systemd_state "${PANEL_DEPLOY_ROLLBACK_DIR}"
  PANEL_DEPLOY_ROLLBACK_ACTIVE="1"
  info "已创建面板${reason}回滚快照"
}

preserve_panel_recovery_material(){
  local path
  for path in \
    "${PANEL_DEPLOY_ROLLBACK_DIR}" \
    "${PANEL_PREV_CODE_DIR}" \
    "${PANEL_PREV_SHARED_DIR}" \
    "${PANEL_PREV_VENV}" \
    "${PANEL_NEXT_VENV}" \
    "${PANEL_CODE_STAGE_DIR}"; do
    [[ -n "${path}" && ( -e "${path}" || -L "${path}" ) ]] || continue
    preserve_cleanup_path "${path}"
    err "保留恢复材料：${path}"
  done
}

restore_panel_deploy_rollback(){
  [[ "${PANEL_DEPLOY_ROLLBACK_ACTIVE:-0}" == "1" ]] || return 0
  local restore_failed="0"
  PANEL_DEPLOY_ROLLBACK_ACTIVE="0"
  if [[ -z "${PANEL_DEPLOY_ROLLBACK_DIR}" || ! -d "${PANEL_DEPLOY_ROLLBACK_DIR}" ]]; then
    err "面板回滚快照目录缺失：${PANEL_DEPLOY_ROLLBACK_DIR:-<empty>}"
    preserve_panel_recovery_material
    release_panel_update_lock
    return 1
  fi
  err "操作未完成，正在恢复上一个可用面板版本..."
  if [[ "${PANEL_SERVICE_WAS_ACTIVE:-0}" == "1" ]]; then
    systemctl stop realm-panel.service >/dev/null 2>&1 || true
  fi
  if [[ -d "${PANEL_PREV_CODE_DIR}" ]]; then
    if ! restore_previous_directory "${PANEL_PREV_CODE_DIR}" "${PANEL_ROOT}/panel"; then
      restore_failed="1"
    fi
  elif [[ -d "${PANEL_DEPLOY_ROLLBACK_DIR}/panel" ]]; then
    if ! restore_directory_snapshot \
      "${PANEL_DEPLOY_ROLLBACK_DIR}/panel" "${PANEL_ROOT}/panel" 755; then
      restore_failed="1"
    fi
  else
    rm -rf "${PANEL_ROOT}/panel"
  fi
  if [[ -d "${PANEL_PREV_SHARED_DIR}" ]]; then
    if ! restore_previous_directory "${PANEL_PREV_SHARED_DIR}" "${PANEL_ROOT}/shared"; then
      restore_failed="1"
    fi
  elif [[ -d "${PANEL_DEPLOY_ROLLBACK_DIR}/shared" ]]; then
    if ! restore_directory_snapshot \
      "${PANEL_DEPLOY_ROLLBACK_DIR}/shared" "${PANEL_ROOT}/shared" 755; then
      restore_failed="1"
    fi
  else
    rm -rf "${PANEL_ROOT}/shared"
  fi
  if [[ -d "${PANEL_DEPLOY_ROLLBACK_DIR}/venv" ]]; then
    if ! restore_directory_snapshot \
      "${PANEL_DEPLOY_ROLLBACK_DIR}/venv" "${PANEL_ROOT}/venv" 755; then
      restore_failed="1"
    fi
  elif [[ -d "${PANEL_PREV_VENV}" ]]; then
    if ! restore_previous_directory "${PANEL_PREV_VENV}" "${PANEL_ROOT}/venv"; then
      restore_failed="1"
    fi
  elif [[ -f "${PANEL_DEPLOY_ROLLBACK_DIR}/venv.present" ]]; then
    rm -rf "${PANEL_NEXT_VENV}"
  else
    rm -rf "${PANEL_ROOT}/venv" "${PANEL_PREV_VENV}" "${PANEL_NEXT_VENV}"
  fi
  if [[ -f "${PANEL_DEPLOY_ROLLBACK_DIR}/requirements.sha256" ]]; then
    if ! cp -a "${PANEL_DEPLOY_ROLLBACK_DIR}/requirements.sha256" "${PANEL_REQ_STAMP}"; then
      restore_failed="1"
    fi
  fi
  if [[ -d "${PANEL_DEPLOY_ROLLBACK_DIR}/etc" ]]; then
    mkdir -p "$(dirname "${PANEL_CONFIG_DIR}")"
    if ! restore_directory_snapshot \
      "${PANEL_DEPLOY_ROLLBACK_DIR}/etc" "${PANEL_CONFIG_DIR}" 700; then
      restore_failed="1"
    fi
  else
    rm -rf "${PANEL_CONFIG_DIR}"
  fi
  if ! restore_panel_release_trust "${PANEL_DEPLOY_ROLLBACK_DIR}"; then
    err "面板 release trust 元数据回滚失败"
    restore_failed="1"
  fi
  if [[ -d "${PANEL_DEPLOY_ROLLBACK_DIR}/panel" \
    || -d "${PANEL_PREV_CODE_DIR}" ]]; then
    if ! prepare_panel_service_permissions; then
      err "面板回滚后无法恢复服务账户权限"
      restore_failed="1"
    fi
  fi
  if ! restore_panel_systemd_state "${PANEL_DEPLOY_ROLLBACK_DIR}"; then
    restore_failed="1"
  fi
  if [[ "${restore_failed}" == "0" ]]; then
    rm -rf \
      "${PANEL_DEPLOY_ROLLBACK_DIR}" \
      "${PANEL_PREV_CODE_DIR}" \
      "${PANEL_PREV_SHARED_DIR}" \
      "${PANEL_PREV_VENV}" \
      "${PANEL_NEXT_VENV}" \
      "${PANEL_CODE_STAGE_DIR}" >/dev/null 2>&1 || true
    PANEL_DEPLOY_ROLLBACK_DIR=""
    PANEL_SYSTEMD_STATE_FILE=""
    PANEL_CODE_STAGE_DIR=""
    clear_panel_update_journal
  else
    err "部分面板回滚未完成，以下 recovery material 不会被 EXIT cleanup 删除："
    preserve_panel_recovery_material
  fi
  release_panel_update_lock
  [[ "${restore_failed}" == "0" ]]
}

commit_panel_deploy_rollback(){
  PANEL_DEPLOY_ROLLBACK_ACTIVE="0"
  rm -rf \
    "${PANEL_DEPLOY_ROLLBACK_DIR}" \
    "${PANEL_PREV_CODE_DIR}" \
    "${PANEL_PREV_SHARED_DIR}" \
    "${PANEL_PREV_VENV}" \
    "${PANEL_NEXT_VENV}" \
    "${PANEL_CODE_STAGE_DIR}" >/dev/null 2>&1 || true
  PANEL_DEPLOY_ROLLBACK_DIR=""
  PANEL_SYSTEMD_STATE_FILE=""
  PANEL_CODE_STAGE_DIR=""
  clear_panel_update_journal
  release_panel_update_lock
}

extract_repo(){
  local mode="$1"; local zip_path="$2"
  local tmp_parent="${REALM_PANEL_TMP_PARENT:-/tmp}"
  mkdir -p "${tmp_parent}" || { err "创建临时目录父路径失败：${tmp_parent}"; exit 1; }
  TMPDIR="$(mktemp -d "${tmp_parent%/}/realm-panel-repo.XXXXXX")"
  if [[ -z "$TMPDIR" || ! -d "$TMPDIR" ]]; then
    err "创建临时目录失败"
    exit 1
  fi
  chmod 700 "${TMPDIR}" >/dev/null 2>&1 || true
  register_cleanup_path "${TMPDIR}"
  RELEASE_MANIFEST_FILE="${TMPDIR}/release-manifest.json"
  RELEASE_MANIFEST_SIGNATURE_FILE="${RELEASE_MANIFEST_FILE}.sig"
  pin_release_public_key || exit 1
  EXTRACT_ROOT="$TMPDIR/extract"
  install -d -m 700 "$EXTRACT_ROOT"
  if [[ "$mode" == "online" ]]; then
    local url repo_base manifest_url fetch_mode fallback_zip_url zip_ok
    local used_manifest="0"
    local tried_manifest="0"
    local explicit_sha=""
    local release_fields=""
    local release_git_sha=""
    local release_url=""
    local release_sha=""
    local release_size=""
    local expected_sha=""
    local expected_size=""
    local actual_size=""
    local candidate=""
    local -a candidates=()
    local -a candidate_hashes=()
    local -a candidate_sizes=()
    repo_base="${REPO_BASE_URL:-$(guess_repo_base_from_zip_url "${REPO_ZIP_URL:-${REPO_ZIP_URL_DEFAULT}}")}"
    manifest_url="${REPO_MANIFEST_URL:-}"
    if [[ -z "${manifest_url}" ]]; then
      if [[ "${repo_base}" == "${REPO_BASE_URL_DEFAULT}" ]]; then
        manifest_url="${REPO_MANIFEST_URL_DEFAULT}"
      else
        manifest_url="${repo_base%/}/release-manifest.json"
      fi
    fi
    fetch_mode="${REPO_FETCH_MODE:-${REPO_FETCH_MODE_DEFAULT}}"
    fetch_mode="$(echo "${fetch_mode}" | tr '[:upper:]' '[:lower:]')"
    explicit_sha="${REALM_PANEL_REPO_ZIP_SHA256:-${REPO_ZIP_SHA256:-${ACTUAL_ZIP_SHA256:-${TARGET_ZIP_SHA256:-}}}}"
    case "${fetch_mode}" in
      manifest|zip|auto) ;;
      *) fetch_mode="${REPO_FETCH_MODE_DEFAULT}" ;;
    esac

    if [[ "${fetch_mode}" != "zip" ]]; then
      tried_manifest="1"
      info "优先使用文件清单拉取..."
      if download_repo_from_manifest_with_fallback "$repo_base" "$manifest_url" "$EXTRACT_ROOT/raw"; then
        used_manifest="1"
        EXTRACT_ROOT="$EXTRACT_ROOT/raw"
      else
        err "签名 manifest 拉取或格式校验失败，拒绝静默退回 ZIP"
        exit 1
      fi
    fi

    if [[ "${used_manifest}" != "1" ]]; then
      zip_ok="0"
      release_fields="$(resolve_release_archive \
        "${REALM_PANEL_RELEASE_METADATA_URL:-${REPO_RELEASE_METADATA_URL:-${REPO_RELEASE_METADATA_URL_DEFAULT}}}" \
        "${RELEASE_MANIFEST_FILE}" || true)"
      if [[ -z "${release_fields}" ]]; then
        err "无法锁定同一份签名 release manifest，拒绝下载仓库 ZIP"
        exit 1
      fi
      IFS=$'\t' read -r release_git_sha release_url release_sha release_size \
        <<< "${release_fields}"
      explicit_sha="$(normalize_sha256 "${explicit_sha}" || true)"
      if [[ -n "${explicit_sha}" && "${explicit_sha}" != "${release_sha}" ]]; then
        err "显式仓库 ZIP SHA256 与签名 release manifest 不一致"
        exit 1
      fi
      url="${REPO_ZIP_URL:-${release_url}}"
      candidates+=("${url}")
      candidate_hashes+=("${release_sha}")
      candidate_sizes+=("${release_size}")
      info "已锁定 release：git=${release_git_sha} sha256=${release_sha:0:12}..."
      fallback_zip_url="$(repo_fallback_zip_url)"
      if [[ -n "${fallback_zip_url}" && -n "${REPO_FALLBACK_ZIP_SHA256:-}" ]]; then
        local fallback_zip_sha=""
        fallback_zip_sha="$(
          normalize_sha256 "${REPO_FALLBACK_ZIP_SHA256}" || true
        )"
        if [[ "${fallback_zip_sha}" == "${release_sha}" ]]; then
          candidates+=("${fallback_zip_url}")
          candidate_hashes+=("${release_sha}")
          candidate_sizes+=("${release_size}")
        else
          err "备用 ZIP SHA256 未绑定到本次签名 release，已忽略"
        fi
      fi

      info "正在下载并校验 ZIP 包..."
      local candidate_index=0
      for candidate in "${candidates[@]}"; do
        expected_sha="${candidate_hashes[$candidate_index]:-}"
        expected_size="${candidate_sizes[$candidate_index]:-}"
        candidate_index=$((candidate_index + 1))
        if [[ -z "${expected_sha}" ]]; then
          err "来源未绑定 SHA256，已拒绝：${candidate}"
          continue
        fi
        if ! download_verified_file \
          "${candidate}" "${TMPDIR}/repo.zip" "${expected_sha}" "仓库 ZIP"; then
          continue
        fi
        if [[ -n "${expected_size}" ]]; then
          actual_size="$(wc -c < "${TMPDIR}/repo.zip" | tr -d '[:space:]')"
          if [[ "${actual_size}" != "${expected_size}" ]]; then
            err "仓库 ZIP 大小与 release 元数据不一致：${candidate}"
            rm -f "${TMPDIR}/repo.zip" || true
            continue
          fi
        fi
        if ! validate_zip_archive "${TMPDIR}/repo.zip"; then
          err "仓库 ZIP 结构或内容校验失败：${candidate}"
          rm -f "${TMPDIR}/repo.zip" || true
          continue
        fi
        zip_ok="1"
        url="${candidate}"
        break
      done

      if [[ "${zip_ok}" != "1" && "${tried_manifest}" != "1" ]]; then
        info "ZIP 包不可用，尝试已绑定 SHA256 的文件清单..."
        if download_repo_from_manifest_with_fallback \
          "$repo_base" "$manifest_url" "$EXTRACT_ROOT/raw"; then
          used_manifest="1"
          EXTRACT_ROOT="$EXTRACT_ROOT/raw"
        fi
      fi
      if [[ "${zip_ok}" == "1" ]]; then
        zip_path="$TMPDIR/repo.zip"
        info "解压中..."
        unzip -q "$zip_path" -d "$EXTRACT_ROOT"
      fi
    fi

    if [[ "${used_manifest}" != "1" && "${zip_ok:-0}" != "1" ]]; then
      err "仓库下载失败：ZIP 与清单模式均不可用"
      err "可手动指定 REPO_MANIFEST_URL，或设置 REPO_ZIP_URL 为可直接下载的 ZIP"
      exit 1
    fi
    if [[ "${used_manifest}" != "1" && "${zip_ok:-0}" == "1" && ! -d "${EXTRACT_ROOT}/panel" ]]; then
      # zip 下载流程在上面已尝试解压；若解压后结构不完整，仍按失败处理。
      PANEL_DIR="$(find "$EXTRACT_ROOT" -maxdepth 6 -type d -name panel -print -quit || true)"
      if [[ -z "$PANEL_DIR" ]]; then
        err "仓库 ZIP 解压后结构不正确，且清单模式不可用"
        err "可手动指定 REPO_MANIFEST_URL，或设置 REPO_ZIP_URL 为可直接下载的 ZIP"
        exit 1
      fi
    fi
  else
    [[ -f "$zip_path" ]] || { err "ZIP 文件不存在：$zip_path"; exit 1; }
    local offline_sha=""
    local offline_size=""
    local offline_manifest="${REALM_RELEASE_MANIFEST_FILE:-}"
    local offline_signature="${REALM_RELEASE_MANIFEST_SIGNATURE_FILE:-}"
    local offline_fields=""
    local offline_commit=""
    local offline_expected_sha=""
    local offline_expected_size=""
    local staged_offline_zip="${TMPDIR}/offline-repo.zip"
    if [[ -z "${offline_manifest}" && -z "${offline_signature}" ]]; then
      offline_manifest="$(dirname "${zip_path}")/release-manifest.json"
      offline_signature="${offline_manifest}.sig"
    fi
    if ! capture_release_manifest \
      "${offline_manifest}" "${offline_signature}"; then
      err "离线安装必须同时提供受信签名 release-manifest.json 与 .sig"
      exit 1
    fi
    offline_fields="$(
      python3 - "${RELEASE_MANIFEST_FILE}" <<'PY'
import json
import pathlib
import re
import sys

payload = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))
commit = str(payload.get("commit") or "").strip().lower()
path = f"nexus/archive/releases/{commit}.zip"
entry = payload.get("files", {}).get(path, {})
digest = str(entry.get("sha256") or "").strip().lower()
size = entry.get("size")
if not re.fullmatch(r"[0-9a-f]{40}([0-9a-f]{24})?", commit):
    raise SystemExit("offline release commit is invalid")
if not re.fullmatch(r"[0-9a-f]{64}", digest):
    raise SystemExit("offline release archive digest is invalid")
if isinstance(size, bool) or not isinstance(size, int) or size <= 0:
    raise SystemExit("offline release archive size is invalid")
print("\t".join((commit, digest, str(size))))
PY
    )" || {
      err "离线签名 release manifest 未绑定不可变仓库归档"
      exit 1
    }
    IFS=$'\t' read -r \
      offline_commit offline_expected_sha offline_expected_size \
      <<< "${offline_fields}"
    offline_sha="$(
      normalize_sha256 \
        "${REALM_PANEL_REPO_ZIP_SHA256:-${REPO_ZIP_SHA256:-${ACTUAL_ZIP_SHA256:-${TARGET_ZIP_SHA256:-}}}}" \
        || true
    )"
    if [[ -n "${offline_sha}" && "${offline_sha}" != "${offline_expected_sha}" ]]; then
      err "离线 ZIP 显式 SHA256 与签名 release manifest 不一致"
      exit 1
    fi
    if ! download_file "file://${zip_path}" "${staged_offline_zip}"; then
      err "无法把离线 ZIP 复制到私有 staging"
      exit 1
    fi
    chmod 600 "${staged_offline_zip}" >/dev/null 2>&1 || true
    if ! verify_file_sha256 \
      "${staged_offline_zip}" "${offline_expected_sha}"; then
      err "离线 ZIP 与签名 release manifest 不一致"
      exit 1
    fi
    offline_size="$(wc -c < "${staged_offline_zip}" | tr -d '[:space:]')"
    if [[ "${offline_size}" != "${offline_expected_size}" ]]; then
      err "离线 ZIP 大小与签名 release manifest 不一致"
      exit 1
    fi
    info "已锁定离线 release：git=${offline_commit} sha256=${offline_expected_sha:0:12}..."
    if ! validate_zip_archive "${staged_offline_zip}"; then
      err "离线 ZIP 结构或内容校验失败"
      exit 1
    fi
    info "解压中..."
    unzip -q "${staged_offline_zip}" -d "$EXTRACT_ROOT"
  fi
  PANEL_DIR="$(find "$EXTRACT_ROOT" -maxdepth 6 -type d -name panel -print -quit || true)"
  if [[ -z "$PANEL_DIR" ]]; then
    err "找不到 panel 目录。请确认仓库里包含 panel/ 或 realm-pro-suite-vXX/panel/"
    err "建议仓库结构：仓库根目录/panel  或  仓库根目录/realm-pro-suite-v33/panel"
    exit 1
  fi
}

find_agent_dir(){
  local base="$1"
  local agent_dir
  agent_dir="$(find "$base" -maxdepth 6 -type d -name agent -print -quit || true)"
  if [[ -z "$agent_dir" ]]; then
    err "找不到 agent 目录。请确认仓库里包含 agent/ 或 realm-pro-suite-vXX/agent/"
    err "建议仓库结构：仓库根目录/agent  或  仓库根目录/realm-pro-suite-v33/agent"
    exit 1
  fi
  echo "$agent_dir"
}

normalize_python_tag(){
  local raw="${1:-}"
  raw="${raw,,}"
  raw="${raw#cp}"
  raw="${raw#py}"
  raw="${raw//[[:space:]]/}"
  if [[ "${raw}" =~ ^[0-9]+\.[0-9]+$ ]]; then
    raw="${raw/./}"
  fi
  if [[ ! "${raw}" =~ ^[0-9]{2,3}$ ]]; then
    return 1
  fi
  printf "%s" "${raw}"
}

python_tag_to_version_arg(){
  local py_tag="${1:-}"
  [[ "${py_tag}" =~ ^[0-9]{2,3}$ ]] || return 1
  if [[ "${py_tag}" == "39" ]]; then
    printf "%s" "3.9.2"
    return 0
  fi
  local major="${py_tag:0:1}"
  local minor="${py_tag:1}"
  printf "%s.%s" "${major}" "${minor}"
}

current_python_tag(){
  python3 - <<'PY'
import sys
print(f"{sys.version_info.major}{sys.version_info.minor}")
PY
}

agent_wheelhouse_python_tags(){
  local current_tag="${1:-}"
  local raw="${REALM_AGENT_WHEELHOUSE_PY_VERSIONS:-39,310,311,312}"
  local seen="|"
  local picked=0
  local token=""
  local norm=""
  raw="${raw//,/ }"
  for token in ${raw}; do
    [[ -n "${token}" ]] || continue
    norm="$(normalize_python_tag "${token}" || true)"
    [[ -n "${norm}" ]] || continue
    if [[ "${seen}" == *"|${norm}|"* ]]; then
      continue
    fi
    printf "%s\n" "${norm}"
    seen="${seen}${norm}|"
    picked=1
  done
  if [[ "${picked}" != "1" && -n "${current_tag}" ]]; then
    printf "%s\n" "${current_tag}"
  fi
}

agent_wheelhouse_platform_tags(){
  local raw="${REALM_AGENT_WHEELHOUSE_PLATFORMS:-manylinux2014_x86_64,manylinux2014_aarch64}"
  local seen="|"
  local picked=0
  local token=""
  raw="${raw//,/ }"
  for token in ${raw}; do
    token="${token//[[:space:]]/}"
    [[ -n "${token}" ]] || continue
    [[ "${token}" =~ ^[A-Za-z0-9._-]+$ ]] || continue
    if [[ "${seen}" == *"|${token}|"* ]]; then
      continue
    fi
    printf "%s\n" "${token}"
    seen="${seen}${token}|"
    picked=1
  done
  if [[ "${picked}" != "1" ]]; then
    printf "%s\n" "manylinux2014_x86_64"
    printf "%s\n" "manylinux2014_aarch64"
  fi
}

agent_wheelhouse_target_supported(){
  local py_tag="${1:-}"
  local platform_tag="${2:-}"
  case "${py_tag}" in
    39|310|311|312)
      ;;
    *)
      return 1
      ;;
  esac
  case "${platform_tag}" in
    manylinux2014_x86_64|manylinux2014_aarch64)
      return 0
      ;;
    *)
      return 1
      ;;
  esac
}

materialize_target_requirements_file(){
  local src="$1"
  local dst="$2"
  local py_ver="$3"
  local platform_tag="$4"
  [[ -f "${src}" && -n "${dst}" && -n "${py_ver}" && -n "${platform_tag}" ]] || return 1
  python3 - "${src}" "${dst}" "${py_ver}" "${platform_tag}" <<'PY'
from pathlib import Path
import re
import sys

try:
    from packaging.markers import default_environment
    from packaging.requirements import InvalidRequirement, Requirement
except ImportError:
    from pip._vendor.packaging.markers import default_environment
    from pip._vendor.packaging.requirements import InvalidRequirement, Requirement


def target_machine(platform_tag: str) -> str:
    if platform_tag.endswith("_x86_64"):
        return "x86_64"
    if platform_tag.endswith("_aarch64"):
        return "aarch64"
    if platform_tag.endswith("_arm64"):
        return "arm64"
    return platform_tag.rsplit("_", 1)[-1]


def split_requirement_blocks(lines: list[str]) -> tuple[list[str], list[list[str]]]:
    header: list[str] = []
    blocks: list[list[str]] = []
    current: list[str] = []
    seen_requirement = False
    for line in lines:
        stripped = line.lstrip()
        is_requirement = bool(
            line
            and not line[0].isspace()
            and stripped
            and not stripped.startswith(("#", "-"))
        )
        if is_requirement:
            if current:
                blocks.append(current)
            current = [line]
            seen_requirement = True
        elif seen_requirement:
            current.append(line)
        else:
            header.append(line)
    if current:
        blocks.append(current)
    return header, blocks


HASH_PATTERN = re.compile(r"--hash=sha256:[0-9a-fA-F]{64}")
PATCH_VERSION_PATTERN = re.compile(r"""['"][0-9]+\.[0-9]+\.[^'"]+['"]""")


def parse_requirement_block(
    block: list[str],
) -> tuple[Requirement, str, list[str], list[str]]:
    logical_parts: list[str] = []
    for raw_line in block:
        line = raw_line.strip()
        if not line or line.startswith("#"):
            continue
        if line.endswith("\\"):
            line = line[:-1].rstrip()
        logical_parts.append(line)
    logical = " ".join(logical_parts)
    head = logical.split(" --hash=", 1)[0].rstrip()
    try:
        requirement = Requirement(head)
    except InvalidRequirement as exc:
        raise SystemExit(f"invalid requirement line: {block[0].rstrip()}: {exc}") from exc
    unconditional = head.split(";", 1)[0].strip()
    hashes = HASH_PATTERN.findall(logical)
    comments = [
        line
        for line in block[1:]
        if line.lstrip().startswith("#")
    ]
    return requirement, unconditional, hashes, comments


def render_unconditional_block(
    requirement: str,
    hashes: list[str],
    comments: list[str],
) -> list[str]:
    if not hashes:
        return [f"{requirement}\n", *comments]
    result = [f"{requirement} \\\n"]
    for index, digest in enumerate(hashes):
        suffix = " \\" if index < len(hashes) - 1 else ""
        result.append(f"    {digest}{suffix}\n")
    result.extend(comments)
    return result


source = Path(sys.argv[1])
destination = Path(sys.argv[2])
python_full_version = sys.argv[3]
platform_tag = sys.argv[4]
version_parts = python_full_version.split(".")
if len(version_parts) == 2:
    python_full_version = f"{python_full_version}.0"
elif len(version_parts) != 3:
    raise SystemExit(f"invalid target Python version: {python_full_version}")
try:
    major, minor, _patch = (int(part) for part in python_full_version.split("."))
except ValueError as exc:
    raise SystemExit(f"invalid target Python version: {python_full_version}") from exc
if major != 3 or minor not in {9, 10, 11, 12}:
    raise SystemExit(
        "unsupported offline target Python "
        f"{python_full_version}; supported: CPython 3.9-3.12"
    )
supported_platforms = (
    "manylinux2014_x86_64",
    "manylinux2014_aarch64",
)
if platform_tag not in supported_platforms:
    raise SystemExit(
        "unsupported offline target platform "
        f"{platform_tag}; supported: manylinux2014_x86_64, "
        "manylinux2014_aarch64"
    )

environment = default_environment()
environment.update(
    {
        "implementation_name": "cpython",
        "implementation_version": python_full_version,
        "os_name": "posix",
        "platform_machine": target_machine(platform_tag),
        "platform_python_implementation": "CPython",
        "platform_release": "",
        "platform_system": "Linux",
        "platform_version": "",
        "python_full_version": python_full_version,
        "python_version": ".".join(python_full_version.split(".")[:2]),
        "sys_platform": "linux",
        "extra": "",
    }
)

output, blocks = split_requirement_blocks(
    source.read_text(encoding="utf-8").splitlines(keepends=True)
)
for block in blocks:
    requirement, unconditional, hashes, comments = parse_requirement_block(block)
    if requirement.marker is not None:
        marker_text = str(requirement.marker)
        if (
            "python_full_version" in marker_text
            and PATCH_VERSION_PATTERN.search(marker_text)
        ):
            raise SystemExit(
                "patch-sensitive python_full_version marker is unsupported "
                f"for a minor-series wheelhouse: {block[0].rstrip()}"
            )
        if not requirement.marker.evaluate(environment):
            continue
    output.extend(render_unconditional_block(unconditional, hashes, comments))
destination.write_text("".join(output), encoding="utf-8")
PY
}

download_agent_wheelhouse_target(){
  local req="$1"
  local wheel_dir="$2"
  local py_tag="$3"
  local platform_tag="$4"
  local py_ver=""
  local target_req=""
  local -a hash_args=()
  agent_wheelhouse_target_supported "${py_tag}" "${platform_tag}" || return 1
  py_ver="$(python_tag_to_version_arg "${py_tag}" || true)"
  [[ -n "${py_ver}" ]] || return 1
  target_req="$(mktemp "${TMPDIR:-/tmp}/realm-agent-target-req.XXXXXX" 2>/dev/null || true)"
  [[ -n "${target_req}" ]] || return 1
  register_cleanup_path "${target_req}"
  if ! materialize_target_requirements_file \
    "${req}" "${target_req}" "${py_ver}" "${platform_tag}"; then
    rm -f "${target_req}" || true
    return 1
  fi
  if requirements_file_uses_hashes "${req}"; then
    hash_args=(--require-hashes)
  fi
  if python3 -m pip download --disable-pip-version-check --no-cache-dir \
    --prefer-binary --only-binary=:all: --no-deps \
    --platform "${platform_tag}" --python-version "${py_ver}" --implementation cp \
    "${hash_args[@]}" -r "${target_req}" -d "${wheel_dir}" >/dev/null 2>&1; then
    rm -f "${target_req}" || true
    return 0
  fi
  rm -f "${target_req}" || true
  return 1
}

verify_agent_wheelhouse(){
  local req="$1"
  local wheel_dir="$2"
  local py_tag="${3:-}"
  local platform_tag="${4:-}"
  [[ -f "${req}" && -d "${wheel_dir}" ]] || return 1
  command -v python3 >/dev/null 2>&1 || return 1
  local tmp=""
  local target_req=""
  local req_for_target="${req}"
  local -a hash_args=()
  local -a target_args=()
  tmp="$(mktemp -d "${TMPDIR:-/tmp}/realm-panel-wheels.XXXXXX" 2>/dev/null || true)"
  [[ -n "${tmp}" && -d "${tmp}" ]] || return 1
  register_cleanup_path "${tmp}"
  if [[ -n "${py_tag}" || -n "${platform_tag}" ]]; then
    local py_ver=""
    agent_wheelhouse_target_supported "${py_tag}" "${platform_tag}" || {
      rm -rf "${tmp}" || true
      return 1
    }
    py_ver="$(python_tag_to_version_arg "${py_tag}" || true)"
    if [[ -z "${py_ver}" || -z "${platform_tag}" ]]; then
      rm -rf "${tmp}" || true
      return 1
    fi
    target_req="$(mktemp "${TMPDIR:-/tmp}/realm-agent-target-req.XXXXXX" 2>/dev/null || true)"
    if [[ -z "${target_req}" ]] \
      || ! materialize_target_requirements_file \
        "${req}" "${target_req}" "${py_ver}" "${platform_tag}"; then
      rm -rf "${tmp}" || true
      rm -f "${target_req}" || true
      return 1
    fi
    register_cleanup_path "${target_req}"
    req_for_target="${target_req}"
    target_args+=(--platform "${platform_tag}" --python-version "${py_ver}" --implementation cp)
  fi
  if requirements_file_uses_hashes "${req}"; then
    hash_args=(--require-hashes)
  fi
  if python3 -m pip download --disable-pip-version-check --no-cache-dir \
    --only-binary=:all: --no-deps "${target_args[@]}" "${hash_args[@]}" \
    --no-index --find-links "${wheel_dir}" \
    -r "${req_for_target}" -d "${tmp}" >/dev/null 2>&1; then
    rm -rf "${tmp}" || true
    rm -f "${target_req}" || true
    return 0
  fi
  rm -rf "${tmp}" || true
  rm -f "${target_req}" || true
  return 1
}

strip_agent_wheelhouse_cache_metadata(){
  local wheel_dir="${1:-}"
  [[ -n "${wheel_dir}" && -d "${wheel_dir}" ]] || return 1
  if ! find "${wheel_dir}" -mindepth 1 -maxdepth 1 -name '.*' \
    -exec rm -rf -- {} +; then
    return 1
  fi
  ! find "${wheel_dir}" -mindepth 1 -maxdepth 1 -name '.*' \
    -print -quit | grep -q .
}

prune_agent_wheelhouse_cache(){
  local current_cache="${1:-}"
  local keep="${REALM_AGENT_WHEELHOUSE_CACHE_KEEP:-4}"
  python3 - "${PANEL_WHEEL_CACHE_DIR}" "${current_cache}" "${keep}" <<'PY'
import re
import shutil
import stat
import sys
from pathlib import Path

root = Path(sys.argv[1])
if not root.exists():
    raise SystemExit(0)
current = Path(sys.argv[2]).resolve() if sys.argv[2] else None
try:
    keep = min(20, max(1, int(sys.argv[3])))
except (TypeError, ValueError):
    keep = 4
candidates = []
for path in root.iterdir():
    metadata = path.lstat()
    if (
        not re.fullmatch(r"[0-9a-f]{64}", path.name)
        or stat.S_ISLNK(metadata.st_mode)
        or not stat.S_ISDIR(metadata.st_mode)
    ):
        continue
    candidates.append((metadata.st_mtime_ns, path))
candidates.sort(reverse=True)
preserved = {path.resolve() for _mtime, path in candidates[:keep]}
if current is not None:
    preserved.add(current)
for _mtime, path in candidates:
    if path.resolve() not in preserved:
        shutil.rmtree(path)
PY
}

verify_release_agent_bundle(){
  local source_panel_dir="$1"
  local installed_panel_dir="${PANEL_ROOT}/panel"
  local source_zip="${source_panel_dir}/static/realm-agent.zip"
  local source_sidecar="${source_zip}.sha256"
  local installed_zip="${installed_panel_dir}/static/realm-agent.zip"
  local installed_sidecar="${installed_zip}.sha256"

  if ! python3 - \
    "${source_zip}" "${source_sidecar}" \
    "${installed_zip}" "${installed_sidecar}" <<'PY'
import hashlib
import os
import pathlib
import re
import stat
import sys

source_zip, source_sidecar, installed_zip, installed_sidecar = map(
    pathlib.Path,
    sys.argv[1:],
)
trusted_uids = {0, os.geteuid()}


def open_checked(path: pathlib.Path) -> tuple[int, os.stat_result]:
    flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0)
    flags |= getattr(os, "O_NOFOLLOW", 0)
    descriptor = os.open(path, flags)
    metadata = os.fstat(descriptor)
    path_metadata = path.lstat()
    mode = stat.S_IMODE(metadata.st_mode)
    if (
        stat.S_ISLNK(metadata.st_mode)
        or not stat.S_ISREG(metadata.st_mode)
        or metadata.st_nlink != 1
        or metadata.st_uid not in trusted_uids
        or mode & 0o022
        or (metadata.st_dev, metadata.st_ino)
        != (path_metadata.st_dev, path_metadata.st_ino)
    ):
        os.close(descriptor)
        raise SystemExit(f"unsafe signed Agent asset: {path}")
    return descriptor, metadata


def read_file(descriptor: int) -> bytes:
    os.lseek(descriptor, 0, os.SEEK_SET)
    chunks = []
    while True:
        chunk = os.read(descriptor, 1024 * 1024)
        if not chunk:
            return b"".join(chunks)
        chunks.append(chunk)


def sha256_file(descriptor: int) -> str:
    digest = hashlib.sha256()
    os.lseek(descriptor, 0, os.SEEK_SET)
    while True:
        chunk = os.read(descriptor, 1024 * 1024)
        if not chunk:
            break
        digest.update(chunk)
    return digest.hexdigest()


opened = [
    open_checked(source_zip),
    open_checked(source_sidecar),
    open_checked(installed_zip),
    open_checked(installed_sidecar),
]
try:
    source_zip_fd, source_zip_meta = opened[0]
    source_sidecar_fd, _ = opened[1]
    installed_zip_fd, installed_zip_meta = opened[2]
    installed_sidecar_fd, _ = opened[3]
    source_digest = sha256_file(source_zip_fd)
    source_sidecar_bytes = read_file(source_sidecar_fd)
    expected_sidecar = f"{source_digest}  realm-agent.zip\n".encode("ascii")
    if source_sidecar_bytes != expected_sidecar:
        raise SystemExit("release Agent ZIP sidecar does not bind the source archive")
    if not re.fullmatch(rb"[0-9a-f]{64}  realm-agent\.zip\n", source_sidecar_bytes):
        raise SystemExit("release Agent ZIP sidecar format is invalid")
    if read_file(installed_sidecar_fd) != source_sidecar_bytes:
        raise SystemExit("installed Agent ZIP sidecar differs from the signed release")
    if installed_zip_meta.st_size != source_zip_meta.st_size:
        raise SystemExit("installed Agent ZIP size differs from the signed release")
    if sha256_file(installed_zip_fd) != source_digest:
        raise SystemExit("installed Agent ZIP bytes differ from the signed release")
    os.fchmod(installed_zip_fd, 0o444)
    os.fchmod(installed_sidecar_fd, 0o444)
    if stat.S_IMODE(os.fstat(installed_zip_fd).st_mode) != 0o444:
        raise SystemExit("installed Agent ZIP mode tightening failed")
    if stat.S_IMODE(os.fstat(installed_sidecar_fd).st_mode) != 0o444:
        raise SystemExit("installed Agent ZIP sidecar mode tightening failed")
finally:
    for descriptor, _metadata in opened:
        os.close(descriptor)
PY
  then
    err "签名 Agent ZIP 与已安装静态制品不一致，拒绝继续更新"
    return 1
  fi
  validate_zip_archive "${source_zip}" || {
    err "签名 release 中的 Agent ZIP 无效"
    return 1
  }
  validate_zip_archive "${installed_zip}" || {
    err "已安装的签名 Agent ZIP 无效"
    return 1
  }
  ok "签名 Agent ZIP 已按 release 原字节保留"
}

prepare_agent_wheelhouse(){
  local agent_dir="$1"
  local enable="${REALM_AGENT_WHEELHOUSE_ENABLE:-1}"
  local req="${agent_dir}/requirements.lock"
  local req_for_wheels="${req}"
  local req_is_lock="1"
  local wheels_dir="${agent_dir}/wheels"
  local req_hash=""
  local cache_dir=""
  local cache_ready_file=""
  local current_py_tag=""
  local py_tag=""
  local platform_tag=""
  local py_tags_key=""
  local platforms_key=""
  local py_tags_text=""
  local platform_tags_text=""
  local -a py_tags=()
  local -a platform_tags=()
  local download_ok="1"
  if [[ "${enable}" != "1" ]]; then
    rm -rf "${wheels_dir}" || true
    return 0
  fi
  if [[ ! -s "${req}" ]]; then
    err "缺少 Agent requirements.lock，拒绝生成不完整离线 wheelhouse"
    rm -rf "${wheels_dir}" || true
    return 1
  fi
  if ! command -v python3 >/dev/null 2>&1; then
    err "系统缺少 python3，无法预下载 Agent wheels"
    rm -rf "${wheels_dir}" || true
    return 1
  fi
  current_py_tag="$(current_python_tag 2>/dev/null || true)"
  if [[ -z "${current_py_tag}" ]]; then
    current_py_tag="39"
  fi
  py_tags_text="$(agent_wheelhouse_python_tags "${current_py_tag}")"
  while IFS= read -r py_tag; do
    [[ -n "${py_tag}" ]] || continue
    py_tags+=("${py_tag}")
  done <<< "${py_tags_text}"
  if (( ${#py_tags[@]} == 0 )); then
    py_tags=("${current_py_tag}")
  fi
  platform_tags_text="$(agent_wheelhouse_platform_tags)"
  while IFS= read -r platform_tag; do
    [[ -n "${platform_tag}" ]] || continue
    platform_tags+=("${platform_tag}")
  done <<< "${platform_tags_text}"
  if (( ${#platform_tags[@]} == 0 )); then
    platform_tags=("manylinux2014_x86_64" "manylinux2014_aarch64")
  fi
  for py_tag in "${py_tags[@]}"; do
    for platform_tag in "${platform_tags[@]}"; do
      if ! agent_wheelhouse_target_supported "${py_tag}" "${platform_tag}"; then
        err "不支持的 Agent 离线目标：cp${py_tag} / ${platform_tag}"
        err "默认离线支持仅限 CPython 3.9-3.12 + manylinux2014 glibc（x86_64/aarch64）；Python 3.13+、Alpine/musl 与 macOS 必须走在线回退"
        rm -rf "${wheels_dir}" || true
        return 1
      fi
    done
  done
  py_tags_key="${py_tags[*]}"
  platforms_key="${platform_tags[*]}"
  req_hash="$(
    {
      printf 'wheelhouse-v10\n'
      printf 'require_hashes=%s\n' "${req_is_lock}"
      printf 'py_tags=%s\n' "${py_tags_key}"
      printf 'platform_tags=%s\n' "${platforms_key}"
      cat "${req_for_wheels}"
    } | sha256sum | awk '{print $1}'
  )"
  cache_dir="${PANEL_WHEEL_CACHE_DIR}/${req_hash}"
  cache_ready_file="${cache_dir}/.wheelhouse-ready"
  if [[ -n "${req_hash}" && -d "${cache_dir}" ]]; then
    if find "${cache_dir}" -maxdepth 1 -type f \( -name '*.whl' -o -name '*.tar.gz' -o -name '*.zip' \) -print -quit | grep -q .; then
      local cache_ok="1"
      if [[ ! -f "${cache_ready_file}" ]]; then
        cache_ok="0"
      fi
      if [[ "${cache_ok}" == "1" ]]; then
        for py_tag in "${py_tags[@]}"; do
          for platform_tag in "${platform_tags[@]}"; do
            if ! verify_agent_wheelhouse "${req_for_wheels}" "${cache_dir}" "${py_tag}" "${platform_tag}"; then
              cache_ok="0"
              break 2
            fi
          done
        done
      fi
      if [[ "${cache_ok}" == "1" ]]; then
        rm -rf "${wheels_dir}" || true
        mkdir -p "${wheels_dir}"
        if ! sync_dir_mirror "${cache_dir}" "${wheels_dir}" \
          || ! strip_agent_wheelhouse_cache_metadata "${wheels_dir}"; then
          err "复制缓存 Agent 依赖包失败"
          rm -rf "${wheels_dir}" || true
          return 1
        fi
        if ! prune_agent_wheelhouse_cache "${cache_dir}"; then
          err "清理旧 Agent wheelhouse 缓存失败"
          return 1
        fi
        ok "复用缓存 Agent 依赖包（hash=${req_hash:0:12}, 目标: ${py_tags_key} / ${platforms_key}）"
        return 0
      fi
      err "缓存 Agent 依赖包不完整，重新下载（hash=${req_hash:0:12}）"
      rm -rf "${cache_dir}" || true
    fi
  fi
  rm -rf "${wheels_dir}" || true
  mkdir -p "${wheels_dir}"
  info "预下载 Agent 依赖 wheels（glibc Linux 离线包）：Python[${py_tags_key}] 平台[${platforms_key}]"
  for py_tag in "${py_tags[@]}"; do
    for platform_tag in "${platform_tags[@]}"; do
      if download_agent_wheelhouse_target "${req_for_wheels}" "${wheels_dir}" "${py_tag}" "${platform_tag}"; then
        ok "已下载离线 wheels：cp${py_tag} / ${platform_tag}"
      else
        err "下载离线 wheels 失败：cp${py_tag} / ${platform_tag}"
        download_ok="0"
      fi
    done
  done
  if [[ "${download_ok}" != "1" ]]; then
    err "Agent 多架构离线依赖包预下载失败"
    rm -rf "${wheels_dir}" || true
    return 1
  fi
  for py_tag in "${py_tags[@]}"; do
    for platform_tag in "${platform_tags[@]}"; do
      if ! verify_agent_wheelhouse "${req_for_wheels}" "${wheels_dir}" "${py_tag}" "${platform_tag}"; then
        err "Agent 依赖包完整性验证失败：cp${py_tag} / ${platform_tag} 缺少依赖（如 requests/charset-normalizer）"
        rm -rf "${wheels_dir}" || true
        return 1
      fi
    done
  done
  if [[ -n "${cache_dir}" ]]; then
    mkdir -p "${cache_dir}"
    rm -f "${cache_ready_file}" || true
    if ! sync_dir_mirror "${wheels_dir}" "${cache_dir}" \
      || ! : > "${cache_ready_file}"; then
      err "写入 Agent wheelhouse 缓存失败，已放弃本次缓存"
      rm -rf "${cache_dir}" || true
    elif ! prune_agent_wheelhouse_cache "${cache_dir}"; then
      err "清理旧 Agent wheelhouse 缓存失败"
      return 1
    fi
  fi
  ok "Agent 依赖包已准备完成（多架构）"
  return 0
}

prepare_agent_bundle(){
  local extract_root="$1"
  # Default strict: require the bounded glibc Linux wheelhouse to be complete.
  local wheelhouse_strict="${REALM_AGENT_WHEELHOUSE_STRICT:-1}"
  if [[ -z "$extract_root" || ! -d "$extract_root" ]]; then
    err "解压目录不存在：$extract_root"
    exit 1
  fi
  local agent_dir
  agent_dir="$(find_agent_dir "$extract_root")"
  info "打包 Agent 离线安装包..."
  if ! prepare_agent_wheelhouse "${agent_dir}"; then
    err "Agent 依赖包准备失败"
    if env_enabled_default_true "${wheelhouse_strict}"; then
      err "已启用严格模式：终止打包（可用 REALM_AGENT_WHEELHOUSE_STRICT=0 放宽）"
      exit 1
    fi
    err "继续打包（警告：离线节点安装可能失败）"
  fi
  find "$agent_dir" -type d -name "__pycache__" -prune -exec rm -rf {} +
  find "$agent_dir" -type f -name "*.pyc" -delete
  mkdir -p "${PANEL_ROOT}/panel/static"
  local agent_zip="${PANEL_ROOT}/panel/static/realm-agent.zip"
  AGENT_BUNDLE_SOURCE="${agent_dir}" \
  AGENT_BUNDLE_OUTPUT="${agent_zip}" \
    python3 - <<'PY'
import hashlib
import json
import os
import stat
import zipfile
from pathlib import Path, PurePosixPath, PureWindowsPath

source = Path(os.environ["AGENT_BUNDLE_SOURCE"]).resolve(strict=True)
output = Path(os.environ["AGENT_BUNDLE_OUTPUT"]).resolve()
root = source.parent
continuity_pairs = tuple(
    (
        f"agent/npp-go/dist/realm-continuity-{target}",
        f"agent/npp-go/dist/realm-continuity-ingress-{target}",
    )
    for target in (
        "linux-amd64",
        "linux-arm64",
        "darwin-amd64",
        "darwin-arm64",
    )
)
continuity_files = {
    relative
    for pair in continuity_pairs
    for relative in pair
}
companion_files = {
    "dist/remote-mac-agent/RemoteMacAgent-arm64.zip",
    "dist/remote-mac-agent/RemoteMacAgent-x86_64.zip",
    "dist/remote-mac-agent/RemoteMacAgent.zip",
}

manifest_path = root / "manifest.json"
try:
    manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
except FileNotFoundError as exc:
    raise SystemExit(f"manifest is missing: {manifest_path}") from exc
except json.JSONDecodeError as exc:
    raise SystemExit(f"invalid JSON in {manifest_path}: {exc}") from exc
if not isinstance(manifest, dict) or not isinstance(manifest.get("files"), list):
    raise SystemExit("manifest root must contain a files list")


def validate_relative_path(raw_path: object) -> str:
    if not isinstance(raw_path, str) or not raw_path:
        raise SystemExit("manifest file path must be a non-empty string")
    if raw_path != raw_path.strip():
        raise SystemExit(f"unsafe manifest file path: {raw_path!r}")
    if any(ord(char) < 32 or ord(char) == 127 for char in raw_path):
        raise SystemExit(f"unsafe manifest file path: {raw_path!r}")
    posix_path = PurePosixPath(raw_path)
    windows_path = PureWindowsPath(raw_path)
    if (
        posix_path.as_posix() != raw_path
        or posix_path.is_absolute()
        or bool(windows_path.drive)
        or "\\" in raw_path
        or any(part in ("", ".", "..") for part in posix_path.parts)
    ):
        raise SystemExit(f"unsafe manifest file path: {raw_path!r}")
    return raw_path


validated = []
seen = set()
for raw_path in manifest["files"]:
    relative = validate_relative_path(raw_path)
    if relative in seen:
        raise SystemExit(f"duplicate manifest file path: {relative}")
    seen.add(relative)
    candidate = root.joinpath(*PurePosixPath(relative).parts)
    try:
        resolved = candidate.resolve(strict=True)
    except FileNotFoundError as exc:
        raise SystemExit(f"manifest file path is missing: {relative}") from exc
    try:
        resolved.relative_to(root)
    except ValueError as exc:
        raise SystemExit(
            f"manifest file path resolves outside the repository: {relative}"
        ) from exc
    if not resolved.is_file():
        raise SystemExit(f"manifest file path is not a regular file: {relative}")
    validated.append((relative, resolved))

manifest_names = {relative for relative, _path in validated}
for pair in continuity_pairs:
    present = manifest_names.intersection(pair)
    if present and len(present) != len(pair):
        missing = sorted(set(pair) - present)
        raise SystemExit(
            "incomplete continuity artifact pair: " + ", ".join(missing)
        )
missing_continuity = sorted(continuity_files - manifest_names)
if missing_continuity:
    raise SystemExit(
        "manifest is missing required continuity artifacts: "
        + ", ".join(missing_continuity)
    )

manifest_hashes = manifest.get("sha256")
if not isinstance(manifest_hashes, dict):
    raise SystemExit("manifest must contain SHA-256 entries for continuity artifacts")
validated_by_name = dict(validated)
for relative in sorted(continuity_files):
    expected = manifest_hashes.get(relative)
    if (
        not isinstance(expected, str)
        or len(expected) != 64
        or any(char not in "0123456789abcdef" for char in expected)
    ):
        raise SystemExit(
            f"manifest SHA-256 is missing or invalid for {relative}"
        )
    actual = hashlib.sha256(validated_by_name[relative].read_bytes()).hexdigest()
    if actual != expected:
        raise SystemExit(
            f"manifest SHA-256 does not match continuity artifact: {relative}"
        )


def agent_zip_source(relative: str) -> bool:
    if not relative.startswith(("agent/", "shared/")):
        return False
    if relative.startswith("agent/npp-go/dist/"):
        return False
    path = PurePosixPath(relative)
    return (
        "tests" not in path.parts
        and "testdata" not in path.parts
        and not path.name.endswith("_test.go")
    )


def enumerate_runtime_wheels():
    wheels_dir = source / "wheels"
    try:
        wheels_metadata = wheels_dir.lstat()
    except FileNotFoundError:
        return []
    if stat.S_ISLNK(wheels_metadata.st_mode) or not stat.S_ISDIR(
        wheels_metadata.st_mode
    ):
        raise SystemExit(
            f"agent wheelhouse is not a regular directory: {wheels_dir}"
        )

    wheels = []
    with os.scandir(wheels_dir) as entries:
        for entry in entries:
            if not entry.name.endswith(".whl"):
                continue
            relative = validate_relative_path(f"agent/wheels/{entry.name}")
            metadata = entry.stat(follow_symlinks=False)
            if not stat.S_ISREG(metadata.st_mode):
                raise SystemExit(
                    f"agent wheelhouse entry is not a regular file: {relative}"
                )
            candidate = Path(entry.path)
            try:
                resolved = candidate.resolve(strict=True)
                resolved.relative_to(wheels_dir)
            except (FileNotFoundError, ValueError) as exc:
                raise SystemExit(
                    f"unsafe agent wheelhouse entry: {relative}"
                ) from exc
            wheels.append((relative, candidate, metadata))
    return wheels


def read_runtime_wheel(path: Path, expected: os.stat_result) -> bytes:
    flags = os.O_RDONLY
    if hasattr(os, "O_NOFOLLOW"):
        flags |= os.O_NOFOLLOW
    try:
        descriptor = os.open(path, flags)
    except OSError as exc:
        raise SystemExit(f"cannot safely open agent wheel: {path}") from exc
    try:
        actual = os.fstat(descriptor)
        if (
            not stat.S_ISREG(actual.st_mode)
            or actual.st_dev != expected.st_dev
            or actual.st_ino != expected.st_ino
        ):
            raise SystemExit(f"agent wheel changed during packaging: {path}")
        with os.fdopen(descriptor, "rb", closefd=False) as handle:
            return handle.read()
    finally:
        os.close(descriptor)


selected_by_name = {
    relative: (path, None)
    for relative, path in validated
    if (
        relative in continuity_files
        or agent_zip_source(relative)
        or relative in companion_files
    )
}
for relative, path, metadata in enumerate_runtime_wheels():
    selected_by_name[relative] = (path, metadata)
selected = sorted(selected_by_name.items(), key=lambda item: item[0])
fixed_date = (2020, 1, 1, 0, 0, 0)
temporary = output.with_name(f".{output.name}.tmp")
try:
    with zipfile.ZipFile(
        temporary,
        "w",
        compression=zipfile.ZIP_DEFLATED,
    ) as archive:
        for relative, (path, wheel_metadata) in selected:
            info = zipfile.ZipInfo(filename=relative, date_time=fixed_date)
            info.compress_type = zipfile.ZIP_DEFLATED
            mode = 0o755 if relative in continuity_files else 0o644
            info.external_attr = mode << 16
            content = (
                path.read_bytes()
                if wheel_metadata is None
                else read_runtime_wheel(path, wheel_metadata)
            )
            archive.writestr(info, content)
    os.replace(temporary, output)
finally:
    if temporary.exists():
        temporary.unlink()
PY
  local agent_zip_sha
  local sidecar_tmp
  agent_zip_sha="$(sha256sum "${agent_zip}" | awk '{print tolower($1)}')"
  sidecar_tmp="$(mktemp "${agent_zip}.sha256.tmp.XXXXXX")"
  printf "%s  realm-agent.zip\n" "${agent_zip_sha}" > "${sidecar_tmp}"
  chmod 0644 "${sidecar_tmp}"
  mv -f "${sidecar_tmp}" "${agent_zip}.sha256"
  local script_path
  if [[ -f "$extract_root/realm_agent.sh" ]]; then
    script_path="$extract_root/realm_agent.sh"
  else
    script_path="$(find "$extract_root" -maxdepth 6 -type f -name realm_agent.sh -print -quit || true)"
  fi
  if [[ -z "$script_path" ]]; then
    err "找不到 realm_agent.sh，无法生成 Agent 安装脚本"
    exit 1
  fi
  cp -f "$script_path" "${PANEL_ROOT}/panel/static/realm_agent.sh"
  local script_macos_path
  if [[ -f "$extract_root/realm_agent_macos.sh" ]]; then
    script_macos_path="$extract_root/realm_agent_macos.sh"
  else
    script_macos_path="$(find "$extract_root" -maxdepth 6 -type f -name realm_agent_macos.sh -print -quit || true)"
  fi
  if [[ -n "$script_macos_path" ]]; then
    cp -f "$script_macos_path" "${PANEL_ROOT}/panel/static/realm_agent_macos.sh"
  fi
  local panel_script_path
  if [[ -f "$extract_root/realm_panel.sh" ]]; then
    panel_script_path="$extract_root/realm_panel.sh"
  else
    panel_script_path="$(find "$extract_root" -maxdepth 6 -type f -name realm_panel.sh -print -quit || true)"
  fi
  if [[ -n "$panel_script_path" ]]; then
    cp -f "$panel_script_path" "${PANEL_ROOT}/panel/static/realm_panel.sh"
  fi
  ok "Agent 离线包就绪：${PANEL_ROOT}/panel/static/realm-agent.zip"
}

is_valid_port(){
  local p="${1:-}"
  [[ "$p" =~ ^[0-9]+$ ]] || return 1
  (( p >= 1 && p <= 65535 ))
}

normalize_panel_port(){
  local p="${1:-}"
  if is_valid_port "$p"; then
    echo "$p"
  else
    echo "6080"
  fi
}

panel_default_host(){
  echo "127.0.0.1"
}

normalize_panel_host(){
  local value="${1:-}"
  value="${value//$'\r'/}"
  value="${value//$'\n'/}"
  [[ -n "${value}" ]] || value="$(panel_default_host)"
  [[ "${value}" != "*" ]] || value="0.0.0.0"
  if [[ "${value}" == \[*\] ]]; then
    value="${value#[}"
    value="${value%]}"
  fi
  if [[ ! "${value}" =~ ^[A-Za-z0-9._:%-]+$ ]]; then
    err "面板 bind host 含非法字符：${value}"
    return 1
  fi
  printf '%s\n' "${value}"
}

panel_host_is_loopback(){
  python3 - "${1:-}" <<'PY' >/dev/null
import ipaddress
import sys

host = str(sys.argv[1] or "").strip().lower().rstrip(".")
if host == "localhost":
    raise SystemExit(0)
try:
    address = ipaddress.ip_address(host.split("%", 1)[0])
except ValueError:
    raise SystemExit(1)
raise SystemExit(0 if address.is_loopback else 1)
PY
}

panel_bool_value(){
  local raw="${1:-}"
  local normalized=""
  normalized="$(printf '%s' "${raw}" | tr '[:upper:]' '[:lower:]')"
  case "${normalized}" in
    1|true|yes|on) printf '1\n' ;;
    0|false|no|off|"") printf '0\n' ;;
    *)
      err "面板安全开关必须是布尔值：${raw}"
      return 1
      ;;
  esac
}

panel_env_value(){
  local key="$1"
  local target="${2:-${PANEL_CONFIG_DIR}/panel.env}"
  local value=""
  if [[ -f "${target}" ]]; then
    value="$(grep -E "^${key}=" "${target}" | tail -n1 | cut -d= -f2- || true)"
    value="${value//$'\r'/}"
    case "${value}" in
      \"*\") value="${value#\"}"; value="${value%\"}" ;;
      \'*\') value="${value#\'}"; value="${value%\'}" ;;
    esac
  fi
  printf '%s' "${value}"
}

set_panel_env_value(){
  local target="$1"
  local key="$2"
  local value="$3"
  local tmp=""
  local found="0"
  [[ "${key}" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || {
    err "拒绝写入非法面板 env 键：${key}"
    return 1
  }
  value="$(strip_env_value "${value}")"
  mkdir -p "$(dirname "${target}")"
  tmp="$(mktemp "${target}.tmp.XXXXXX")" || return 1
  register_cleanup_path "${tmp}"
  if [[ -f "${target}" ]]; then
    while IFS= read -r line || [[ -n "${line}" ]]; do
      case "${line}" in
        "${key}="*)
          printf '%s=%s\n' "${key}" "${value}" >> "${tmp}"
          found="1"
          ;;
        *) printf '%s\n' "${line}" >> "${tmp}" ;;
      esac
    done < "${target}"
  fi
  [[ "${found}" == "1" ]] || printf '%s=%s\n' "${key}" "${value}" >> "${tmp}"
  if [[ -f "${target}" ]]; then
    chmod "$(stat -c '%a' "${target}" 2>/dev/null || stat -f '%Lp' "${target}" 2>/dev/null || printf '640')" "${tmp}" 2>/dev/null || true
  else
    chmod 640 "${tmp}"
  fi
  mv -f "${tmp}" "${target}"
}

validate_panel_bind_policy(){
  local bind_host="$1"
  local public_url="$2"
  local insecure_http_override="${3:-0}"
  bind_host="$(normalize_panel_host "${bind_host}")" || return 1
  public_url="$(printf '%s' "${public_url}" | tr '[:upper:]' '[:lower:]')"
  if [[ "${public_url}" == https://* ]] && ! panel_host_is_loopback "${bind_host}"; then
    err "反代/HTTPS 模式的面板后端必须只绑定 loopback，拒绝公网直连：${bind_host}"
    return 1
  fi
  if [[ "${public_url}" == http://* ]] \
    && ! panel_host_is_loopback "${bind_host}" \
    && [[ "${insecure_http_override}" != "1" ]]; then
    err "非 loopback 明文 HTTP 面板已拒绝；如确需承担公网风险，请显式设置 REALM_PANEL_ALLOW_INSECURE_PUBLIC_HTTP=1"
    return 1
  fi
}

panel_systemd_override_dir(){
  local unit_dir
  unit_dir="$(dirname "${PANEL_SYSTEMD_UNIT}")"
  if [[ "${PANEL_SYSTEMD_UNIT}" == "/etc/systemd/system/realm-panel.service" ]]; then
    printf '%s\n' "/etc/systemd/system/realm-panel.service.d"
  else
    printf '%s\n' "${unit_dir}/realm-panel.service.d"
  fi
}

panel_systemd_link_roots(){
  local unit_dir
  unit_dir="$(dirname "${PANEL_SYSTEMD_UNIT}")"
  printf '%s\n' "${unit_dir}"
  if [[ "${unit_dir}" == "/etc/systemd/system" && -d "/run/systemd/system" ]]; then
    printf '%s\n' "/run/systemd/system"
  fi
}

snapshot_panel_systemd_state(){
  local rollback_dir="$1"
  local unit_path="${PANEL_SYSTEMD_UNIT}"
  local override_dir=""
  local load_state=""
  local active_state=""
  local file_state=""
  local link_root=""
  local link_path=""
  local link_target=""
  local unit_name
  unit_name="$(basename "${unit_path}")"
  override_dir="$(panel_systemd_override_dir)"
  PANEL_SYSTEMD_STATE_FILE="${rollback_dir}/systemd-state"
  : > "${PANEL_SYSTEMD_STATE_FILE}"

  if [[ -e "${unit_path}" || -L "${unit_path}" ]]; then
    cp -a "${unit_path}" "${rollback_dir}/unit"
    printf 'unit_present=1\n' >> "${PANEL_SYSTEMD_STATE_FILE}"
  else
    : > "${rollback_dir}/unit.absent"
    printf 'unit_present=0\n' >> "${PANEL_SYSTEMD_STATE_FILE}"
  fi
  if [[ -e "${override_dir}" || -L "${override_dir}" ]]; then
    cp -a "${override_dir}" "${rollback_dir}/override"
    printf 'override_present=1\n' >> "${PANEL_SYSTEMD_STATE_FILE}"
  else
    : > "${rollback_dir}/override.absent"
    printf 'override_present=0\n' >> "${PANEL_SYSTEMD_STATE_FILE}"
  fi

  load_state="$(systemctl show --property=LoadState --value "${unit_name}" 2>/dev/null || true)"
  active_state="$(systemctl show --property=ActiveState --value "${unit_name}" 2>/dev/null || true)"
  file_state="$(systemctl is-enabled "${unit_name}" 2>/dev/null || true)"
  if [[ -z "${active_state}" ]]; then
    if systemctl is-active --quiet "${unit_name}" >/dev/null 2>&1; then
      active_state="active"
    else
      active_state="inactive"
    fi
  fi
  if [[ -z "${file_state}" ]]; then
    if systemctl is-enabled --quiet "${unit_name}" >/dev/null 2>&1; then
      file_state="enabled"
    else
      file_state="disabled"
    fi
  fi
  [[ "${load_state}" != "not-found" && -n "${load_state}" ]] || load_state="not-found"
  printf 'load_state=%s\nactive_state=%s\nfile_state=%s\n' \
    "${load_state}" "${active_state}" "${file_state}" >> "${PANEL_SYSTEMD_STATE_FILE}"
  : > "${rollback_dir}/systemd-links"
  while IFS= read -r link_root; do
    [[ -d "${link_root}" ]] || continue
    while IFS= read -r -d '' link_path; do
      [[ "${link_path}" == "${unit_path}" ]] && continue
      link_target="$(readlink "${link_path}")"
      printf '%s|%s\n' "${link_path}" "${link_target}" >> "${rollback_dir}/systemd-links"
    done < <(find "${link_root}" -type l -name "${unit_name}" -print0 2>/dev/null)
  done < <(panel_systemd_link_roots)
}

remove_panel_systemd_links(){
  local link_root=""
  local link_path=""
  local unit_name
  unit_name="$(basename "${PANEL_SYSTEMD_UNIT}")"
  while IFS= read -r link_root; do
    [[ -d "${link_root}" ]] || continue
    while IFS= read -r -d '' link_path; do
      [[ "${link_path}" == "${PANEL_SYSTEMD_UNIT}" ]] && continue
      rm -f "${link_path}" || return 1
    done < <(find "${link_root}" -type l -name "${unit_name}" -print0 2>/dev/null)
  done < <(panel_systemd_link_roots)
}

panel_systemd_links_exist(){
  local link_root=""
  local unit_name
  unit_name="$(basename "${PANEL_SYSTEMD_UNIT}")"
  while IFS= read -r link_root; do
    [[ -d "${link_root}" ]] || continue
    if find "${link_root}" -type l -name "${unit_name}" -print -quit 2>/dev/null | grep -q .; then
      return 0
    fi
  done < <(panel_systemd_link_roots)
  return 1
}

wait_panel_service_active(){
  local unit="$1"
  local max_attempts="${2:-30}"
  local attempt=""
  local state=""
  if [[ ! "${max_attempts}" =~ ^[0-9]+$ ]] || (( max_attempts < 1 )); then
    max_attempts=30
  fi
  for ((attempt = 1; attempt <= max_attempts; attempt++)); do
    state="$(
      systemctl show --property=ActiveState --value "${unit}" 2>/dev/null || true
    )"
    if [[ "${state}" == "active" ]]; then
      return 0
    fi
    if [[ "${state}" == "failed" ]]; then
      break
    fi
    if [[ -z "${state}" ]] \
      && systemctl is-active --quiet "${unit}" >/dev/null 2>&1; then
      return 0
    fi
    sleep 1
  done
  err "面板回滚服务未进入 active：${unit}（state=${state:-unknown}）"
  return 1
}

restore_panel_systemd_state(){
  local rollback_dir="$1"
  local state_file="${rollback_dir}/systemd-state"
  local unit_present="0"
  local override_present="0"
  local load_state="not-found"
  local active_state="inactive"
  local file_state="disabled"
  local key value
  local override_dir=""
  local link_path link_target
  local restore_failed="0"
  local actual_load_state=""
  local actual_file_state=""
  local actual_active_state=""
  [[ -f "${state_file}" ]] || {
    err "面板 systemd 状态快照缺失：${state_file}"
    return 1
  }
  while IFS='=' read -r key value; do
    case "${key}" in
      unit_present) unit_present="${value}" ;;
      override_present) override_present="${value}" ;;
      load_state) load_state="${value}" ;;
      active_state) active_state="${value}" ;;
      file_state) file_state="${value}" ;;
    esac
  done < "${state_file}"

  if ! systemctl disable --now "$(basename "${PANEL_SYSTEMD_UNIT}")" >/dev/null 2>&1; then
    if systemctl is-active --quiet "$(basename "${PANEL_SYSTEMD_UNIT}")" >/dev/null 2>&1 \
      || panel_systemd_links_exist; then
      restore_failed=1
    fi
  fi
  remove_panel_systemd_links || restore_failed=1
  rm -rf "${PANEL_SYSTEMD_UNIT}" "$(panel_systemd_override_dir)" >/dev/null 2>&1 || restore_failed=1
  if [[ "${unit_present}" == "1" ]]; then
    install -d -m 755 "$(dirname "${PANEL_SYSTEMD_UNIT}")"
    cp -a "${rollback_dir}/unit" "${PANEL_SYSTEMD_UNIT}" || restore_failed=1
  fi
  override_dir="$(panel_systemd_override_dir)"
  if [[ "${override_present}" == "1" ]]; then
    install -d -m 755 "$(dirname "${override_dir}")"
    cp -a "${rollback_dir}/override" "${override_dir}" || restore_failed=1
  fi
  systemctl daemon-reload >/dev/null 2>&1 || restore_failed=1

  case "${file_state}" in
    enabled)
      systemctl unmask "$(basename "${PANEL_SYSTEMD_UNIT}")" >/dev/null 2>&1 || true
      systemctl enable "$(basename "${PANEL_SYSTEMD_UNIT}")" >/dev/null 2>&1 || restore_failed=1
      ;;
    enabled-runtime)
      systemctl unmask "$(basename "${PANEL_SYSTEMD_UNIT}")" >/dev/null 2>&1 || true
      systemctl enable --runtime "$(basename "${PANEL_SYSTEMD_UNIT}")" >/dev/null 2>&1 || restore_failed=1
      ;;
    masked)
      if [[ ! -L "${PANEL_SYSTEMD_UNIT}" ]]; then
        systemctl mask "$(basename "${PANEL_SYSTEMD_UNIT}")" >/dev/null 2>&1 || restore_failed=1
      fi
      ;;
    masked-runtime)
      if [[ ! -L "${PANEL_SYSTEMD_UNIT}" ]]; then
        systemctl mask --runtime "$(basename "${PANEL_SYSTEMD_UNIT}")" >/dev/null 2>&1 || restore_failed=1
      fi
      ;;
    disabled|static|indirect|generated|transient|alias|linked|linked-runtime|not-found)
      ;;
    *)
      err "无法识别面板原始 systemd UnitFileState：${file_state}"
      restore_failed=1
      ;;
  esac

  if [[ -f "${rollback_dir}/systemd-links" ]]; then
    while IFS='|' read -r link_path link_target; do
      [[ -n "${link_path}" && -n "${link_target}" ]] || continue
      if [[ -e "${link_path}" || -L "${link_path}" ]]; then
        rm -f "${link_path}" || { restore_failed=1; continue; }
      fi
      install -d -m 755 "$(dirname "${link_path}")" || { restore_failed=1; continue; }
      ln -s "${link_target}" "${link_path}" || restore_failed=1
    done < "${rollback_dir}/systemd-links"
  fi

  case "${active_state}" in
    active|activating|reloading)
      if ! systemctl start "$(basename "${PANEL_SYSTEMD_UNIT}")" >/dev/null 2>&1; then
        restore_failed=1
      elif ! wait_panel_service_active "$(basename "${PANEL_SYSTEMD_UNIT}")"; then
        restore_failed=1
      fi
      ;;
    *)
      if [[ "${unit_present}" == "1" || "${load_state}" != "not-found" ]]; then
        systemctl stop "$(basename "${PANEL_SYSTEMD_UNIT}")" >/dev/null 2>&1 || restore_failed=1
      fi
      ;;
  esac

  if [[ "${unit_present}" == "1" ]]; then
    [[ -e "${PANEL_SYSTEMD_UNIT}" || -L "${PANEL_SYSTEMD_UNIT}" ]] || {
      err "面板 systemd unit 恢复后缺失"
      restore_failed=1
    }
  elif [[ -e "${PANEL_SYSTEMD_UNIT}" || -L "${PANEL_SYSTEMD_UNIT}" ]]; then
    err "面板 systemd unit 原本不存在，回滚后仍有本地文件"
    restore_failed=1
  fi
  actual_load_state="$(
    systemctl show --property=LoadState --value "$(basename "${PANEL_SYSTEMD_UNIT}")" 2>/dev/null || true
  )"
  [[ -n "${actual_load_state}" ]] || actual_load_state="not-found"
  if [[ "${actual_load_state}" != "${load_state}" ]]; then
    err "面板 systemd LoadState 恢复不精确：期望 ${load_state}，实际 ${actual_load_state}"
    restore_failed=1
  fi
  actual_file_state="$(
    systemctl is-enabled "$(basename "${PANEL_SYSTEMD_UNIT}")" 2>/dev/null || true
  )"
  if [[ -z "${actual_file_state}" ]]; then
    if systemctl is-enabled --quiet "$(basename "${PANEL_SYSTEMD_UNIT}")" >/dev/null 2>&1; then
      actual_file_state="enabled"
    elif [[ "${load_state}" == "not-found" ]]; then
      actual_file_state="not-found"
    else
      actual_file_state="disabled"
    fi
  fi
  if [[ "${actual_file_state}" != "${file_state}" ]]; then
    err "面板 systemd UnitFileState 恢复不精确：期望 ${file_state}，实际 ${actual_file_state}"
    restore_failed=1
  fi
  actual_active_state="$(
    systemctl show --property=ActiveState --value "$(basename "${PANEL_SYSTEMD_UNIT}")" 2>/dev/null || true
  )"
  if [[ -z "${actual_active_state}" ]]; then
    if systemctl is-active --quiet "$(basename "${PANEL_SYSTEMD_UNIT}")" >/dev/null 2>&1; then
      actual_active_state="active"
    else
      actual_active_state="inactive"
    fi
  fi
  case "${active_state}:${actual_active_state}" in
    active:active|activating:active|reloading:active|inactive:inactive|failed:inactive|deactivating:inactive|maintenance:inactive) ;;
    *)
      err "面板 systemd active 状态恢复不精确：期望 ${active_state}，实际 ${actual_active_state}"
      restore_failed=1
      ;;
  esac
  [[ "${restore_failed}" == "0" ]]
}

current_panel_host(){
  local from_env=""
  if [[ -f "${PANEL_CONFIG_DIR}/panel.env" ]]; then
    from_env="$(grep -E '^REALM_PANEL_HOST=' "${PANEL_CONFIG_DIR}/panel.env" | tail -n1 | cut -d= -f2- || true)"
    from_env="$(echo "${from_env}" | tr -d '[:space:]')"
    if [[ -n "${from_env}" ]]; then
      echo "${from_env}"
      return
    fi
  fi
  panel_default_host
}

current_panel_port(){
  local from_env=""
  if [[ -f "${PANEL_CONFIG_DIR}/panel.env" ]]; then
    from_env="$(grep -E '^REALM_PANEL_PORT=' "${PANEL_CONFIG_DIR}/panel.env" | tail -n1 | cut -d= -f2- || true)"
    from_env="$(echo "${from_env}" | tr -d '[:space:]')"
    if is_valid_port "$from_env"; then
      echo "$from_env"
      return
    fi
  fi

  local from_service=""
  if [[ -f "${PANEL_SYSTEMD_UNIT}" ]]; then
    from_service="$(grep -Eo -- '--port[[:space:]]+[0-9]+' "${PANEL_SYSTEMD_UNIT}" | tail -n1 | awk '{print $2}' || true)"
    from_service="$(echo "${from_service}" | tr -d '[:space:]')"
    if is_valid_port "$from_service"; then
      echo "$from_service"
      return
    fi
  fi
  echo "6080"
}

panel_total_mem_mb(){
  local raw=""
  raw="$(awk '/MemTotal:/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || true)"
  if [[ "${raw}" =~ ^[0-9]+$ ]]; then
    printf '%s\n' "${raw}"
    return 0
  fi
  printf '0\n'
}

ensure_panel_low_memory_swap(){
  local auto_swap mem_mb
  auto_swap="${REALM_PANEL_AUTO_SWAP:-1}"
  case "$(printf '%s' "${auto_swap}" | tr '[:upper:]' '[:lower:]')" in
    0|false|no|off) return 0 ;;
  esac
  mem_mb="$(panel_total_mem_mb)"
  if [[ ! "${mem_mb}" =~ ^[0-9]+$ ]] || (( mem_mb <= 0 || mem_mb > 1536 )); then
    return 0
  fi
  if swapon --show 2>/dev/null | grep -q .; then
    return 0
  fi
  if [[ ! -f /swapfile ]]; then
    if command -v fallocate >/dev/null 2>&1; then
      fallocate -l 1G /swapfile 2>/dev/null || dd if=/dev/zero of=/swapfile bs=1M count=1024 status=none
    else
      dd if=/dev/zero of=/swapfile bs=1M count=1024 status=none
    fi
    chmod 600 /swapfile
    mkswap /swapfile >/dev/null 2>&1 || return 0
  fi
  swapon /swapfile >/dev/null 2>&1 || true
  grep -q '^/swapfile ' /etc/fstab 2>/dev/null || echo '/swapfile none swap sw 0 0' >> /etc/fstab
}

ensure_panel_env_defaults(){
  local default_port default_host env_file
  default_port="$(normalize_panel_port "${1:-6080}")"
  default_host="$(panel_default_host)"
  env_file="${PANEL_CONFIG_DIR}/panel.env"
  mkdir -p "${PANEL_CONFIG_DIR}"
  touch "${env_file}"
  append_env_default "${env_file}" REALM_PANEL_DB "${PANEL_CONFIG_DIR}/panel.db"
  append_env_default "${env_file}" REALM_PANEL_HOST "${default_host}"
  append_env_default "${env_file}" REALM_PANEL_PORT "${default_port}"
  append_env_default "${env_file}" REALM_PANEL_ALLOW_INSECURE_PUBLIC_HTTP "0"
  append_env_default "${env_file}" REALM_PANEL_V2_DATABASE_URL ""
  append_env_default "${env_file}" REALM_PANEL_V2_SQLITE_PATH "${PANEL_RUNTIME_DIR}/control_plane/runtime.sqlite3"
  append_env_default "${env_file}" REALM_NATS_URL ""
  append_env_default "${env_file}" REALM_PANEL_SESSION_HTTPS_ONLY "0"
  append_env_default "${env_file}" REALM_PANEL_TIMEOUT_KEEP_ALIVE "5"
  append_env_default "${env_file}" REALM_PANEL_LIMIT_CONCURRENCY "512"
  append_env_default "${env_file}" REALM_PANEL_BACKLOG "4096"
  append_env_default "${env_file}" REALM_PANEL_LOG_FILE "${PANEL_LOG_DIR}/panel.log"
  append_env_default "${env_file}" REALM_PANEL_LOG_MAX_BYTES "5242880"
  append_env_default "${env_file}" REALM_PANEL_LOG_BACKUP_COUNT "5"
  append_env_default "${env_file}" REALM_PANEL_CRASH_LOG_FILE "${PANEL_LOG_DIR}/crash.log"
  append_env_default "${env_file}" REALM_PANEL_CRASH_LOG_MAX_BYTES "5242880"
  append_env_default "${env_file}" REALM_PANEL_CRASH_LOG_BACKUP_COUNT "5"
  append_env_default "${env_file}" REALM_PANEL_FAULT_LOG_FILE "${PANEL_LOG_DIR}/fault.log"
}

wait_panel_ready(){
  local port max_attempts attempt crash_log panel_log host
  local login_urls=() root_urls=() url
  port="$(normalize_panel_port "${1:-$(current_panel_port)}")"
  max_attempts="${2:-25}"
  if [[ ! "${max_attempts}" =~ ^[0-9]+$ ]] || (( max_attempts < 1 )); then
    max_attempts=25
  fi
  host="$(current_panel_host)"
  login_urls+=("http://127.0.0.1:${port}/login" "http://[::1]:${port}/login" "http://localhost:${port}/login")
  root_urls+=("http://127.0.0.1:${port}/" "http://[::1]:${port}/" "http://localhost:${port}/")
  if [[ -n "${host}" && "${host}" != "0.0.0.0" && "${host}" != "::" ]]; then
    if [[ "${host}" == *:* ]]; then
      login_urls+=("http://[${host}]:${port}/login")
      root_urls+=("http://[${host}]:${port}/")
    else
      login_urls+=("http://${host}:${port}/login")
      root_urls+=("http://${host}:${port}/")
    fi
  fi
  crash_log="${PANEL_LOG_DIR}/crash.log"
  panel_log="${PANEL_LOG_DIR}/panel.log"
  for ((attempt = 1; attempt <= max_attempts; attempt++)); do
    if systemctl is-active --quiet realm-panel.service; then
      for url in "${login_urls[@]}" "${root_urls[@]}"; do
        if curl -g -fsS --max-time 5 "${url}" >/dev/null 2>&1; then
          return 0
        fi
      done
    fi
    sleep 1
  done
  err "面板服务启动后未就绪：realm-panel.service / port=${port}"
  systemctl status realm-panel.service --no-pager || true
  journalctl -u realm-panel.service -n 80 --no-pager || true
  if [[ -f "${crash_log}" ]]; then
    tail -n 80 "${crash_log}" >&2 || true
  fi
  if [[ -f "${panel_log}" ]]; then
    tail -n 80 "${panel_log}" >&2 || true
  fi
  return 1
}

write_start_shim(){
  cat > "${PANEL_ROOT}/start.sh" <<EOF
#!/usr/bin/env bash
set -euo pipefail
exec /bin/bash "${PANEL_ROOT}/panel/start.sh" "\$@"
EOF
  chmod +x "${PANEL_ROOT}/start.sh"
}

install_panel_update_script_from_extract(){
  local extract_root="$1"
  local panel_script_path=""
  if [[ -n "${extract_root}" && -f "${extract_root}/realm_panel.sh" ]]; then
    panel_script_path="${extract_root}/realm_panel.sh"
  elif [[ -n "${extract_root}" && -d "${extract_root}" ]]; then
    panel_script_path="$(find "$extract_root" -maxdepth 6 -type f -name realm_panel.sh -print -quit || true)"
  fi
  if [[ -z "${panel_script_path}" && -f "${BASH_SOURCE[0]}" ]]; then
    panel_script_path="${BASH_SOURCE[0]}"
  fi
  if [[ -z "${panel_script_path}" || ! -f "${panel_script_path}" ]]; then
    err "找不到 realm_panel.sh，跳过本地自更新脚本落盘"
    return 1
  fi
  local dst="${PANEL_ROOT}/realm_panel.sh"
  local tmp=""
  tmp="$(mktemp "${dst}.tmp.XXXXXX")" || return 1
  register_cleanup_path "${tmp}"
  if ! cp -f "${panel_script_path}" "${tmp}"; then
    rm -f "${tmp}" 2>/dev/null || true
    return 1
  fi
  if ! chmod 755 "${tmp}"; then
    rm -f "${tmp}" 2>/dev/null || true
    return 1
  fi
  if ! mv -f "${tmp}" "${dst}"; then
    rm -f "${tmp}" 2>/dev/null || true
    return 1
  fi
}

write_systemd(){
  local default_host default_port
  default_host="$(normalize_panel_host "$(current_panel_host)")" || return 1
  default_port="$(normalize_panel_port "$(current_panel_port)")"
  validate_panel_service_account_name "${PANEL_SERVICE_USER}" || return 1
  validate_panel_service_account_name "${PANEL_SERVICE_GROUP}" || return 1
  install -d -m 755 "$(dirname "${PANEL_SYSTEMD_UNIT}")"
  local unit_tmp=""
  unit_tmp="$(mktemp "${PANEL_SYSTEMD_UNIT}.tmp.XXXXXX")" || return 1
  register_cleanup_path "${unit_tmp}"
  cat > "${unit_tmp}" <<EOF
[Unit]
Description=Realm Panel Web UI
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=60
StartLimitBurst=5

[Service]
Type=simple
User=${PANEL_SERVICE_USER}
Group=${PANEL_SERVICE_GROUP}
Environment=REALM_PANEL_HOST=${default_host}
Environment=REALM_PANEL_PORT=${default_port}
Environment=REALM_PANEL_TIMEOUT_KEEP_ALIVE=5
Environment=REALM_PANEL_LIMIT_CONCURRENCY=512
Environment=REALM_PANEL_BACKLOG=4096
Environment=REALM_PANEL_REQUIRE_PERSISTENT_DB=1
Environment=REALM_PANEL_REQUIRE_STRONG_SECRET=1
Environment=REALM_PANEL_FORWARDED_ALLOW_IPS=127.0.0.1
Environment=HOME=${PANEL_RUNTIME_DIR}
Environment=PYTHONDONTWRITEBYTECODE=1
UMask=0077
# Make the repository-level shared/ package importable. It ships next to
# panel/ under ${PANEL_ROOT} and exposes shared.env (parse_bool /
# env_int / etc.) used by both panel and agent.
Environment=PYTHONPATH=${PANEL_ROOT}:${PANEL_ROOT}/panel
EnvironmentFile=-${PANEL_CONFIG_DIR}/panel.env
WorkingDirectory=${PANEL_ROOT}/panel
ExecStartPre=/usr/bin/test -r ${PANEL_CONFIG_DIR}/panel.env
ExecStartPre=/usr/bin/test -w ${PANEL_CONFIG_DIR}
ExecStartPre=/usr/bin/test -w ${PANEL_LOG_DIR}
ExecStartPre=/usr/bin/test -w ${PANEL_RUNTIME_DIR}
ExecStartPre=/usr/bin/test -x ${PANEL_ROOT}/venv/bin/python
ExecStartPre=/usr/bin/test -f ${PANEL_ROOT}/panel/app/main.py
ExecStartPre=${PANEL_ROOT}/venv/bin/python -c "import uvicorn, fastapi, app.main"
ExecStart=${PANEL_ROOT}/venv/bin/python -m uvicorn app.main:app --host \${REALM_PANEL_HOST} --port \${REALM_PANEL_PORT} --proxy-headers --forwarded-allow-ips \${REALM_PANEL_FORWARDED_ALLOW_IPS} --no-access-log --timeout-keep-alive \${REALM_PANEL_TIMEOUT_KEEP_ALIVE} --limit-concurrency \${REALM_PANEL_LIMIT_CONCURRENCY} --backlog \${REALM_PANEL_BACKLOG}
Restart=always
RestartSec=2
TimeoutStopSec=20
LimitNOFILE=1048576

# Sandbox hardening. Panel does not modify kernel tunables, load modules,
# or rewrite its own systemd unit at runtime (in-place upgrades happen via
# the operator-run realm_panel.sh script, not the service process).
NoNewPrivileges=yes
ProtectSystem=strict
ReadOnlyPaths=${PANEL_ROOT}
ReadWritePaths=${PANEL_CONFIG_DIR} ${PANEL_LOG_DIR} ${PANEL_RUNTIME_DIR}
PrivateTmp=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
ProtectHome=yes
PrivateDevices=yes
ProtectHostname=yes
CapabilityBoundingSet=
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
SystemCallArchitectures=native

[Install]
WantedBy=multi-user.target
EOF
  chmod 0644 "${unit_tmp}"
  rm -f "${PANEL_SYSTEMD_UNIT}"
  mv -f "${unit_tmp}" "${PANEL_SYSTEMD_UNIT}"
}

verify_panel_runtime_layout(){
  local panel_dir="$1"
  local required=(
    "start.sh"
    "app/main.py"
    "app/control_plane/integration.py"
  )
  local rel
  for rel in "${required[@]}"; do
    if [[ ! -f "${panel_dir}/${rel}" ]]; then
      err "panel 包不完整，缺少关键文件：${panel_dir}/${rel}"
      return 1
    fi
  done
}

clear_legacy_panel_overrides(){
  local override_dir="/etc/systemd/system/realm-panel.service.d"
  if [[ "${PANEL_SYSTEMD_UNIT}" != "/etc/systemd/system/realm-panel.service" ]]; then
    override_dir="$(panel_systemd_override_dir)"
  fi
  if [[ -f "${override_dir}/override.conf" ]]; then
    rm -f "${override_dir}/override.conf"
    rmdir "${override_dir}" >/dev/null 2>&1 || true
    info "已移除 legacy realm-panel systemd override"
  fi
}

install_panel(){
  need_root
  require_systemd
  check_panel_update_transaction || exit 1
  acquire_panel_update_lock || exit 1
  echo "Realm Pro Panel 安装向导 ${VERSION}"
  echo "------------------------------------------------------------"
  echo "1) 在线安装（推荐）"
  echo "2) 离线安装（手动下载）"
  local choice
  choice="$(prompt "请选择安装模式 [1-2]" "1")"
  local mode="online"
  local zip_path=""
  if [[ "$choice" == "2" ]]; then
    mode="offline"
    zip_path="$(prompt "请输入 ZIP 文件路径（例如 /root/Realm-main.zip）" "")"
  fi

  apt_install

  extract_repo "$mode" "$zip_path"
  if [[ -z "$EXTRACT_ROOT" || ! -d "$EXTRACT_ROOT" ]]; then
    err "解压目录不存在：$EXTRACT_ROOT"
    exit 1
  fi
  ok "已定位 panel 目录：$PANEL_DIR"
  verify_panel_runtime_layout "$PANEL_DIR" || exit 1

  local user pass port
  user="$(prompt "设置面板登录用户名" "admin")"
  while true; do
    pass="$(prompt_secret "设置面板登录密码（输入时不显示，必填）")"
    [[ -n "$pass" ]] || { err "密码不能为空"; continue; }
    validate_panel_password "$pass" || continue
    local pass2
    pass2="$(prompt_secret "再次输入密码确认")"
    [[ "$pass" == "$pass2" ]] || { err "两次输入的密码不一致"; continue; }
    break
  done
  port="$(normalize_panel_port "$(prompt "面板端口" "6080")")"

  # ✅ 新增：是否为公网 IP
  # - 是：继续询问是否输入域名（反代/HTTPS）；否则使用公网 IP+端口
  # - 否：后续给节点拉取安装文件统一走 GitHub（包括 agent），面板仅用于控制/上报
  local is_public asset_source
  is_public="$(prompt "当前面板机器是否为公网 IP？(y/n)" "y")"
  is_public="${is_public,,}"
  if [[ "$is_public" == "n" || "$is_public" == "no" || "$is_public" == "0" ]]; then
    asset_source="github"
  else
    asset_source="panel"
  fi

  local panel_domain public_url panel_host_default panel_insecure_http_override
  panel_insecure_http_override="$(
    panel_bool_value "${REALM_PANEL_ALLOW_INSECURE_PUBLIC_HTTP:-0}"
  )" || exit 1
  panel_host_default="$(
    normalize_panel_host "${REALM_PANEL_HOST:-$(panel_default_host)}"
  )" || exit 1
  if [[ "$asset_source" == "panel" ]]; then
    panel_domain="$(prompt "面板域名/外网地址（可选：反向代理/HTTPS 场景；留空使用 IP+端口）" "")"
    if [[ -n "$panel_domain" ]]; then
      public_url="$(normalize_public_url "$panel_domain")"
    elif [[ "${panel_insecure_http_override}" == "1" ]]; then
      if [[ -z "${REALM_PANEL_HOST:-}" ]]; then
        panel_host_default="0.0.0.0"
      fi
      public_url="http://$(detect_ip):${port}"
    else
      public_url="http://127.0.0.1:${port}"
      info "未配置反向代理；面板默认仅监听 loopback。公网明文直连需显式设置 REALM_PANEL_ALLOW_INSECURE_PUBLIC_HTTP=1。"
    fi
  else
    panel_domain=""
    public_url="http://127.0.0.1:${port}"
    info "检测为非公网 IP：节点安装文件将默认从 GitHub 拉取（包括 Agent/realm），面板不再作为下载源。"
  fi
  validate_panel_bind_policy \
    "${panel_host_default}" "${public_url}" "${panel_insecure_http_override}" || exit 1

  info "部署到 ${PANEL_ROOT} ..."
  begin_panel_deploy_rollback "安装"
  ensure_panel_service_account || exit 1
  ensure_panel_runtime_dirs
  snapshot_companion_assets_into_cache
  stage_panel_code "$PANEL_DIR"
  prepare_panel_venv_stage
  build_panel_venv "${PANEL_NEXT_VENV}" "${PANEL_CODE_STAGE_DIR}/panel"
  activate_panel_code_stage
  verify_remote_control_static_asset "$PANEL_DIR" || exit 1
  restore_static_asset_cache
  install_companion_from_extract
  # The release manifest signs this exact ZIP. Runtime wheelhouse enrichment
  # must be published as a separately signed artifact, never overwrite it.
  verify_release_agent_bundle "$PANEL_DIR" || exit 1

  if [[ "$asset_source" == "panel" ]]; then
    if should_sync_realm_assets; then
      prepare_realm_assets
    else
      info "已配置跳过 realm 资源同步（REALM_PANEL_SYNC_REALM_ASSETS=0）"
    fi
  else
    # 仍确保静态目录存在（面板自身静态资源需要）
    mkdir -p "${PANEL_ROOT}/panel/static"
  fi
  update_static_asset_cache

  activate_panel_venv

  info "初始化面板配置..."
  local panel_session_https_only
  panel_session_https_only="0"
  if [[ "${public_url}" == https://* ]]; then
    panel_session_https_only="1"
  fi
  mkdir -p "${PANEL_CONFIG_DIR}"
  {
    printf 'REALM_PANEL_PUBLIC_URL=%s\n' "$(strip_env_value "${public_url}")"
    printf 'REALM_PANEL_REMOTE_ALLOWED_ORIGINS=%s\n' "$(strip_env_value "${public_url}")"
    printf 'REALM_PANEL_REMOTE_STRICT_ORIGINS=%s\n' '0'
    printf 'REALM_PANEL_DB=%s\n' "${PANEL_CONFIG_DIR}/panel.db"
    printf 'REALM_PANEL_ASSET_SOURCE=%s\n' "$(strip_env_value "${asset_source}")"
    printf 'REALM_PANEL_HOST=%s\n' "$(strip_env_value "${panel_host_default}")"
    printf 'REALM_PANEL_PORT=%s\n' "$(strip_env_value "${port}")"
    printf 'REALM_PANEL_ALLOW_INSECURE_PUBLIC_HTTP=%s\n' "$(strip_env_value "${panel_insecure_http_override}")"
    printf 'REALM_PANEL_V2_DATABASE_URL=\n'
    printf 'REALM_PANEL_V2_SQLITE_PATH=%s\n' "${PANEL_RUNTIME_DIR}/control_plane/runtime.sqlite3"
    printf 'REALM_NATS_URL=\n'
    printf 'REALM_PANEL_SESSION_HTTPS_ONLY=%s\n' "$(strip_env_value "${panel_session_https_only}")"
    printf 'REALM_PANEL_TIMEOUT_KEEP_ALIVE=%s\n' '5'
    printf 'REALM_PANEL_LIMIT_CONCURRENCY=%s\n' '512'
    printf 'REALM_PANEL_BACKLOG=%s\n' '4096'
    printf 'REALM_PANEL_LOG_FILE=%s\n' "${PANEL_LOG_DIR}/panel.log"
    printf 'REALM_PANEL_LOG_MAX_BYTES=%s\n' '5242880'
    printf 'REALM_PANEL_LOG_BACKUP_COUNT=%s\n' '5'
    printf 'REALM_PANEL_CRASH_LOG_FILE=%s\n' "${PANEL_LOG_DIR}/crash.log"
    printf 'REALM_PANEL_CRASH_LOG_MAX_BYTES=%s\n' '5242880'
    printf 'REALM_PANEL_CRASH_LOG_BACKUP_COUNT=%s\n' '5'
    printf 'REALM_PANEL_FAULT_LOG_FILE=%s\n' "${PANEL_LOG_DIR}/fault.log"
  } > "${PANEL_CONFIG_DIR}/panel.env"
  export PANEL_USER="$user"
  export PANEL_PASS="$pass"
  (
    cd "${PANEL_ROOT}/panel" || exit 1
    PYTHONPATH="${PANEL_ROOT}:${PANEL_ROOT}/panel${PYTHONPATH:+:${PYTHONPATH}}" \
      "${PANEL_ROOT}/venv/bin/python" - <<'PY'
import os
from app.auth import ensure_secret_key, save_credentials
ensure_secret_key()
save_credentials(os.environ['PANEL_USER'], os.environ['PANEL_PASS'])
print('OK')
PY
  )

  write_start_shim
  ensure_panel_env_defaults "$port"
  install_panel_release_trust_metadata || exit 1
  ensure_panel_low_memory_swap
  verify_panel_app_import
  prepare_panel_service_permissions || exit 1
  write_systemd
  clear_legacy_panel_overrides
  systemctl daemon-reload
  systemctl enable realm-panel.service >/dev/null
  systemctl restart realm-panel.service
  wait_panel_ready "${port}"
  install_panel_update_script_from_extract "$EXTRACT_ROOT"
  commit_panel_deploy_rollback
  release_panel_update_lock

  ok "面板已启动"
  echo "访问地址：${public_url}"
  echo "用户名：${user}"
  echo "密码：（你刚刚设置的）"
}

update_panel(){
  need_root
  require_systemd
  check_panel_update_transaction || exit 1
  require_existing_panel_install
  acquire_panel_update_lock || exit 1
  local keep_port keep_host keep_public_url keep_insecure_http_override
  keep_port="$(current_panel_port)"
  keep_host="$(
    normalize_panel_host "${REALM_PANEL_HOST:-$(current_panel_host)}"
  )" || exit 1
  keep_public_url="$(
    panel_env_value REALM_PANEL_PUBLIC_URL "${PANEL_CONFIG_DIR}/panel.env"
  )"
  [[ -n "${keep_public_url}" ]] || keep_public_url="http://127.0.0.1:${keep_port}"
  keep_insecure_http_override="$(
    panel_bool_value "$(
      if [[ -n "${REALM_PANEL_ALLOW_INSECURE_PUBLIC_HTTP:-}" ]]; then
        printf '%s' "${REALM_PANEL_ALLOW_INSECURE_PUBLIC_HTTP}"
      else
        panel_env_value REALM_PANEL_ALLOW_INSECURE_PUBLIC_HTTP "${PANEL_CONFIG_DIR}/panel.env"
      fi
    )"
  )" || exit 1
  if [[ "$(printf '%s' "${keep_public_url}" | tr '[:upper:]' '[:lower:]')" == https://* ]] \
    && ! panel_host_is_loopback "${keep_host}"; then
    info "检测到反代/HTTPS 配置，正在把面板后端 bind 收紧到 127.0.0.1"
    keep_host="127.0.0.1"
  fi
  validate_panel_bind_policy \
    "${keep_host}" "${keep_public_url}" "${keep_insecure_http_override}" || exit 1
  local mode="online"
  local zip_path=""
  echo "1) 在线更新（推荐）"
  echo "2) 离线更新（手动下载）"
  local choice
  choice="$(prompt "请选择更新模式 [1-2]" "1")"
  if [[ "$choice" == "2" ]]; then
    mode="offline"
    zip_path="$(prompt "请输入 ZIP 文件路径（例如 /root/Realm-main.zip）" "")"
  fi
  apt_install
  extract_repo "$mode" "$zip_path"
  if [[ -z "$EXTRACT_ROOT" || ! -d "$EXTRACT_ROOT" ]]; then
    err "解压目录不存在：$EXTRACT_ROOT"
    exit 1
  fi
  ok "已定位 panel 目录：$PANEL_DIR"
  verify_panel_runtime_layout "$PANEL_DIR" || exit 1
  info "更新面板程序文件..."
  # Update jobs can be spawned from /opt/realm-panel/panel; leave it before deleting that tree.
  mkdir -p "${PANEL_ROOT}"
  cd "${PANEL_ROOT}" || exit 1
  begin_panel_deploy_rollback "更新"
  write_panel_update_journal prepared || exit 1
  ensure_panel_service_account || exit 1
  ensure_panel_runtime_dirs
  snapshot_companion_assets_into_cache
  if [[ "${PANEL_SERVICE_WAS_ACTIVE:-0}" == "1" ]]; then
    if ! systemctl stop realm-panel.service; then
      err "无法停止 realm-panel.service，拒绝删除旧面板代码"
      exit 1
    fi
  fi
  rm -rf "${PANEL_ROOT}/panel"
  stage_panel_code "$PANEL_DIR"
  prepare_panel_venv_stage
  build_panel_venv "${PANEL_NEXT_VENV}" "${PANEL_CODE_STAGE_DIR}/panel"
  activate_panel_code_stage
  write_panel_update_journal code-activated || exit 1
  verify_remote_control_static_asset "$PANEL_DIR" || exit 1
  restore_static_asset_cache
  install_companion_from_extract
  # The release manifest signs this exact ZIP. Runtime wheelhouse enrichment
  # must be published as a separately signed artifact, never overwrite it.
  verify_release_agent_bundle "$PANEL_DIR" || exit 1
  if should_sync_realm_assets; then
    prepare_realm_assets
  else
    info "已配置跳过 realm 资源同步（REALM_PANEL_SYNC_REALM_ASSETS=0）"
  fi
  update_static_asset_cache
  activate_panel_venv
  write_panel_update_journal venv-activated || exit 1
  write_start_shim
  ensure_panel_env_defaults "$keep_port"
  install_panel_release_trust_metadata || exit 1
  set_panel_env_value "${PANEL_CONFIG_DIR}/panel.env" REALM_PANEL_HOST "${keep_host}"
  set_panel_env_value \
    "${PANEL_CONFIG_DIR}/panel.env" \
    REALM_PANEL_ALLOW_INSECURE_PUBLIC_HTTP \
    "${keep_insecure_http_override}"
  ensure_panel_low_memory_swap
  verify_panel_app_import
  prepare_panel_service_permissions || exit 1
  write_systemd
  clear_legacy_panel_overrides
  systemctl daemon-reload
  info "即将重启 realm-panel.service"
  write_panel_update_journal restarting || exit 1
  systemctl restart realm-panel.service
  wait_panel_ready "${keep_port}"
  install_panel_update_script_from_extract "$EXTRACT_ROOT"
  write_panel_update_journal committed || exit 1
  commit_panel_deploy_rollback
  clear_panel_update_journal
  release_panel_update_lock
  ok "面板已更新并重启"
}

restart_panel(){
  need_root
  require_systemd
  check_panel_update_transaction || exit 1
  acquire_panel_update_lock || exit 1
  prepare_panel_service_permissions || exit 1
  write_systemd || exit 1
  systemctl daemon-reload || exit 1
  systemctl restart realm-panel.service
  wait_panel_ready "$(current_panel_port)"
  release_panel_update_lock
  ok "面板已重启"
}

uninstall_panel(){
  need_root
  acquire_panel_update_lock || exit 1
  PANEL_DEPLOY_ROLLBACK_ACTIVE="0"
  if command -v systemctl >/dev/null 2>&1; then
    systemctl disable --now realm-panel.service >/dev/null 2>&1 || true
  fi
  rm -f "${PANEL_SYSTEMD_UNIT}"
  rm -rf "$(dirname "${PANEL_SYSTEMD_UNIT}")/realm-panel.service.d"
  if command -v systemctl >/dev/null 2>&1; then
    systemctl daemon-reload >/dev/null 2>&1 || true
    systemctl reset-failed realm-panel.service >/dev/null 2>&1 || true
  fi
  rm -rf "${PANEL_ROOT}" "${PANEL_CONFIG_DIR}" "${PANEL_LOG_DIR}" "${PANEL_RUNTIME_DIR}"
  release_panel_update_lock
  ok "面板已卸载"
}

main(){
  echo "Realm Pro Panel 管理 ${VERSION}"
  echo "------------------------------------------------------------"
  echo "1) 安装面板"
  echo "2) 更新面板"
  echo "3) 重启面板"
  echo "4) 卸载面板"
  local action
  action="$(prompt "请选择操作 [1-4]" "1")"
  case "$action" in
    1) install_panel ;;
    2) update_panel ;;
    3) restart_panel ;;
    4) uninstall_panel ;;
    *) err "无效选择"; exit 1 ;;
  esac
}

main "$@"
