VPS 一键脚本
返回目录 / speedtest

speedtest Speedtest

要用官方 Ookla CLI 给 VPS 测速,请以 root 运行下面的命令。它会从官方 tarball 安装 speedtest 到 /usr/local/bin(不添加软件源),并在你确认许可说明之后才开始测速。测速会连接 Ookla 服务器,这是 CLI 本身的行为。

命令
$ curl -fsSL xyz.sh/speedtest | bash
最后更新:
声明支持的系统:Debian, Ubuntu, CentOS, Rocky, AlmaLinux
需要 root
上游来源:https://www.speedtest.net/apps/cli
SHA256:99a187b01dc65e8d6e28a0ea6f2fb3b0e1ed4064c8950d61c351cf19f7038310 · 与 curl 拿到的脚本一致,也可访问 /speedtest.sh.sha256
阅读教程:VPS Speedtest 测速教程(官方 Ookla CLI)

所有脚本都通过 bash -n 与 ShellCheck 检查,并在 Debian 12/11、Ubuntu 24.04/22.04/20.04/18.04、Rocky 9、AlmaLinux 9、CentOS 7 的官方 Docker 镜像中以 root 运行 curl | bash 测试(结果见各页“容器测试结果”)。容器里没有 systemd、内核模块和真实公网线路,相关部分需要在真实 VPS 上确认。

它会做什么

从 Ookla 官方 Linux tarball(install.speedtest.net,当前 1.2.0,按架构自动选择 x86_64 / aarch64 / armhf / armel / i386)下载官方 `speedtest` 二进制并安装到 /usr/local/bin,不添加任何软件源。默认对 Ookla 自动选择的就近节点测一次。加 --overseas 会再测香港、东京、新加坡、美西:先用官方 CLI 按地区检索服务器,失败则回退到 speedtest-go(官方发布版,SHA256 固定,只在临时目录运行)。包装层不会在你确认(或看到说明后使用 -y)之前静默加上 --accept-license / --accept-gdpr。xyz.sh 不会另加遥测。可用 --install-only 只安装不测速,--uninstall 删除该二进制。本脚本不测试中国大陆节点。

支持的系统

  • Debian
  • Ubuntu
  • CentOS
  • Rocky
  • AlmaLinux

需要 root

参数与示例

$ curl -fsSL xyz.sh/speedtest | bash
$ curl -fsSL xyz.sh/speedtest | bash -s -- -y
$ curl -fsSL xyz.sh/speedtest | bash -s -- --overseas -y
$ curl -fsSL xyz.sh/speedtest | bash -s -- --install-only
$ curl -fsSL xyz.sh/speedtest | bash -s -- --uninstall

上游来源: https://www.speedtest.net/apps/cli

容器测试结果

在各发行版官方 Docker 镜像中以 root、无终端方式运行 curl | bash 的结果。partial 表示容器内无法完整验证(例如没有 systemd、内核模块或测速协议被测试环境的代理拦截),需要在真实 VPS 上确认。 (2026-11-11)

OS结果
debian:12pass
debian:11pass
ubuntu:24.04pass
ubuntu:22.04pass
ubuntu:20.04pass
ubuntu:18.04pass
rockylinux:9pass
almalinux:9pass
centos:7pass

常见问题

这个脚本安全吗?
xyz.sh 的包装层不埋点、不偷偷执行未说明的操作,完整源码可在页面上查看,也可以用 curl -fsSL xyz.sh/<name>.sh 下载后先读再运行。把远程脚本管道给 bash 仍有风险,请先阅读源码。测速由 Ookla 官方 CLI 完成,会连接 Ookla 服务器,并有自己的许可与隐私条款:https://www.speedtest.net/terms 与 https://www.speedtest.net/about/privacy。xyz.sh 不会在确认前静默加上接受条款的参数,也不会另加遥测。
支持哪些系统?
声明支持 Debian、Ubuntu、CentOS、Rocky、AlmaLinux。脚本会检测发行版;无法识别时会提示并征求确认。语法已用 bash -n 和 ShellCheck 检查,但安装类脚本尚未在每种发行版上做完整安装验收。
需要 root 吗?
需要。请用 root 或 sudo bash 运行。非 root 时脚本会退出并提示。
如何卸载?
运行 curl -fsSL xyz.sh/speedtest | bash -s -- --uninstall,或删除 /usr/local/bin/speedtest。许可记录可能在 ~/.config/ookla。
可以只安装不测速吗?
可以:curl -fsSL xyz.sh/speedtest | bash -s -- --install-only。

完整源码

以下就是 curl 实际拿到的内容(公共库 + 脚本正文)。运行前请先读一遍。

xyz.sh/speedtest.sh
#!/usr/bin/env bash
# Served by xyz.sh/speedtest
# Human-readable page: https://xyz.sh/speedtest
# This file is the common library followed by the script body.
# No telemetry is added by xyz.sh.
# =============================================================================
# xyz.sh common library
# Shared helpers for every script served by xyz.sh.
#
# Promises:
#   - No telemetry, analytics, or background "phone-home" calls
#   - No hidden installs or config changes beyond what the script announces
#   - Official upstream URLs are printed before any remote code is executed
#   - Destructive or system-changing steps ask for confirmation (or YES=1 / -y)
# =============================================================================

if [[ -n "${XYZ_COMMON_LOADED:-}" ]]; then
  # When sourced a second time, return. When executed as a file, return fails.
  # shellcheck disable=SC2317
  return 0 2>/dev/null || true
fi
# shellcheck disable=SC2034
XYZ_COMMON_LOADED=1

set -euo pipefail

XYZ_VERSION="1.3.0"
XYZ_BASE="${XYZ_BASE:-https://xyz.sh}"
XYZ_NAME="${XYZ_NAME:-unknown}"

# --- language ---------------------------------------------------------------
# Override with XYZ_LANG=zh or XYZ_LANG=en.
if [[ -z "${XYZ_LANG:-}" ]]; then
  case "${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}" in
    zh*|ZH*|cmn*|CMN*) XYZ_LANG="zh" ;;
    *) XYZ_LANG="en" ;;
  esac
fi

xyz_t() {
  # xyz_t "English text" "中文文本"
  if [[ "${XYZ_LANG}" == "zh" ]]; then
    printf '%s' "${2:-$1}"
  else
    printf '%s' "$1"
  fi
}

# --- colors (TTY only) ------------------------------------------------------
# XYZ_COLOR=always|never|auto (default auto: TTY and no NO_COLOR); --no-color.
_xyz_color=0
case "${XYZ_COLOR:-auto}" in
  always) _xyz_color=1 ;;
  never) _xyz_color=0 ;;
  *) [[ -t 1 && -z "${NO_COLOR:-}" ]] && _xyz_color=1 ;;
esac
[[ "${XYZ_NO_COLOR:-0}" == "1" ]] && _xyz_color=0
if [[ "${_xyz_color}" == "1" ]]; then
  XYZ_C_RESET=$'\033[0m'
  XYZ_C_BOLD=$'\033[1m'
  XYZ_C_DIM=$'\033[2m'
  XYZ_C_RED=$'\033[31m'
  XYZ_C_GREEN=$'\033[32m'
  XYZ_C_YELLOW=$'\033[33m'
  XYZ_C_CYAN=$'\033[36m'
else
  XYZ_C_RESET=""
  XYZ_C_BOLD=""
  XYZ_C_DIM=""
  XYZ_C_RED=""
  XYZ_C_GREEN=""
  XYZ_C_YELLOW=""
  XYZ_C_CYAN=""
fi

xyz_info()  { printf '%s[INFO]%s %s\n'  "${XYZ_C_GREEN}"  "${XYZ_C_RESET}" "$*"; }
xyz_warn()  { printf '%s[WARN]%s %s\n'  "${XYZ_C_YELLOW}" "${XYZ_C_RESET}" "$*" >&2; }
xyz_error() { printf '%s[ERROR]%s %s\n' "${XYZ_C_RED}"    "${XYZ_C_RESET}" "$*" >&2; }
xyz_step()  { printf '%s==>%s %s\n'     "${XYZ_C_CYAN}"   "${XYZ_C_RESET}" "$*"; }

# --- args / init ------------------------------------------------------------
XYZ_ARGS=()
XYZ_YES=0
XYZ_DRY_RUN=0

xyz_parse_global_args() {
  local filtered=()
  local arg
  for arg in "$@"; do
    case "${arg}" in
      -y|--yes)
        XYZ_YES=1
        ;;
      --lang=zh|zh)
        XYZ_LANG="zh"
        ;;
      --lang=en|en)
        XYZ_LANG="en"
        ;;
      --dry-run)
        XYZ_DRY_RUN=1
        ;;
      --no-color)
        XYZ_NO_COLOR=1
        XYZ_C_RESET=""; XYZ_C_BOLD=""; XYZ_C_DIM=""; XYZ_C_RED=""
        XYZ_C_GREEN=""; XYZ_C_YELLOW=""; XYZ_C_CYAN=""
        ;;
      --help|-h)
        XYZ_HELP=1
        filtered+=("${arg}")
        ;;
      *)
        filtered+=("${arg}")
        ;;
    esac
  done
  XYZ_ARGS=("${filtered[@]+"${filtered[@]}"}")
  if [[ "${YES:-}" =~ ^(1|yes|true|YES)$ || "${XYZ_ASSUME_YES:-}" == "1" ]]; then
    XYZ_YES=1
  fi
  # Exported so individual scripts can read flags set here.
  export XYZ_ARGS XYZ_YES XYZ_LANG XYZ_HELP="${XYZ_HELP:-}" XYZ_DRY_RUN="${XYZ_DRY_RUN:-0}"
  export XYZ_NO_COLOR="${XYZ_NO_COLOR:-0}"
  if declare -F xyz_ui_init >/dev/null; then xyz_ui_init; fi
}

xyz_banner() {
  local title="${XYZ_TITLE:-$XYZ_NAME}"
  printf '\n%s%sxyz.sh/%s%s  v%s\n' "${XYZ_C_BOLD}" "${XYZ_C_CYAN}" "${XYZ_NAME}" "${XYZ_C_RESET}" "${XYZ_VERSION}"
  printf '%s\n' "${XYZ_C_DIM}----------------------------------------${XYZ_C_RESET}"
  printf '  %s: %s/%s\n' "$(xyz_t "Page" "页面")" "${XYZ_BASE}" "${XYZ_NAME}"
  printf '  %s: %s/%s.sh\n' "$(xyz_t "Raw" "源码")" "${XYZ_BASE}" "${XYZ_NAME}"
  if [[ -n "${XYZ_UPSTREAM:-}" ]]; then
    printf '  %s: %s\n' "$(xyz_t "Upstream" "上游来源")" "${XYZ_UPSTREAM}"
  fi
  printf '  %s\n' "$(xyz_t "No telemetry, no hidden actions." "无遥测、无隐藏操作。")"
  printf '%s\n\n' "${XYZ_C_DIM}----------------------------------------${XYZ_C_RESET}"
  if [[ -n "${title}" && "${title}" != "${XYZ_NAME}" ]]; then
    xyz_step "${title}"
  fi
}

xyz_init() {
  xyz_parse_global_args "$@"
  xyz_detect_os
  xyz_banner
}

# --- root / OS --------------------------------------------------------------
xyz_is_root() {
  [[ "${EUID:-$(id -u)}" -eq 0 ]]
}

xyz_require_root() {
  if ! xyz_is_root; then
    xyz_error "$(xyz_t \
      "This script must be run as root. Try: sudo bash" \
      "请使用 root 运行。可尝试:sudo bash")"
    exit 1
  fi
}

xyz_prefer_root() {
  if ! xyz_is_root; then
    xyz_warn "$(xyz_t \
      "Not running as root; some information or tests may be limited." \
      "当前不是 root,部分信息或测试可能不完整。")"
  fi
}

XYZ_OS_ID=""
XYZ_OS_LIKE=""
XYZ_OS_VERSION=""
XYZ_OS_FAMILY=""   # debian | rhel | unknown
XYZ_OS_PRETTY=""

xyz_detect_os() {
  XYZ_OS_ID=""
  XYZ_OS_LIKE=""
  XYZ_OS_VERSION=""
  XYZ_OS_FAMILY="unknown"
  XYZ_OS_PRETTY="unknown"
  if [[ -r /etc/os-release ]]; then
    # shellcheck disable=SC1091
    . /etc/os-release
    XYZ_OS_ID="${ID:-}"
    XYZ_OS_LIKE="${ID_LIKE:-}"
    XYZ_OS_VERSION="${VERSION_ID:-}"
    XYZ_OS_PRETTY="${PRETTY_NAME:-$XYZ_OS_ID}"
    export XYZ_OS_ID XYZ_OS_LIKE XYZ_OS_VERSION XYZ_OS_FAMILY XYZ_OS_PRETTY
  fi
  case "${XYZ_OS_ID}" in
    debian|ubuntu)
      XYZ_OS_FAMILY="debian"
      ;;
    centos|rhel|rocky|almalinux|alma|fedora)
      XYZ_OS_FAMILY="rhel"
      ;;
    *)
      case " ${XYZ_OS_ID} ${XYZ_OS_LIKE} " in
        *debian*|*ubuntu*) XYZ_OS_FAMILY="debian" ;;
        *rhel*|*centos*|*fedora*|*rocky*|*alma*) XYZ_OS_FAMILY="rhel" ;;
        *) : ;;
      esac
      ;;
  esac
}

xyz_os_supported() {
  case "${XYZ_OS_ID}" in
    debian|ubuntu|centos|rocky|almalinux) return 0 ;;
    *) : ;;
  esac
  case " ${XYZ_OS_LIKE} " in
    *debian*|*ubuntu*|*rhel*|*centos*|*rocky*|*alma*) return 0 ;;
    *) : ;;
  esac
  return 1
}

xyz_require_supported_os() {
  xyz_detect_os
  if ! xyz_os_supported; then
    xyz_warn "$(xyz_t \
      "Unrecognized distribution: ${XYZ_OS_ID:-unknown} (${XYZ_OS_PRETTY}). Officially supported: Debian, Ubuntu, CentOS, Rocky, AlmaLinux." \
      "未能识别的发行版:${XYZ_OS_ID:-unknown}(${XYZ_OS_PRETTY})。官方支持:Debian、Ubuntu、CentOS、Rocky、AlmaLinux。")"
    if ! xyz_confirm "$(xyz_t "Continue anyway?" "仍然继续?")"; then
      exit 1
    fi
  else
    xyz_info "$(xyz_t "Detected OS:" "检测到系统:") ${XYZ_OS_PRETTY} (${XYZ_OS_FAMILY})"
  fi
}

# --- packages / commands ----------------------------------------------------
xyz_need_cmd() {
  command -v "$1" >/dev/null 2>&1
}

xyz_pkg_install() {
  xyz_detect_os
  if (( $# == 0 )); then
    return 0
  fi
  case "${XYZ_OS_FAMILY}" in
    debian)
      export DEBIAN_FRONTEND=noninteractive
      apt-get update -y || xyz_warn "$(xyz_t "apt-get update reported errors (a repository may be unreachable); trying to install anyway." "apt-get update 报错(可能有软件源无法访问),仍尝试安装。")"
      apt-get install -y --no-install-recommends "$@"
      ;;
    rhel)
      if xyz_need_cmd dnf; then
        dnf install -y "$@"
      elif xyz_need_cmd yum; then
        yum install -y "$@"
      else
        xyz_error "$(xyz_t "Neither dnf nor yum is available." "找不到 dnf 或 yum。")"
        exit 1
      fi
      ;;
    *)
      xyz_error "$(xyz_t "Cannot install packages on this OS automatically." "无法在此系统上自动安装软件包。")"
      exit 1
      ;;
  esac
}

xyz_ensure_cmds() {
  local pkg
  local missing=()
  for pkg in "$@"; do
    if ! xyz_need_cmd "${pkg}"; then
      missing+=("${pkg}")
    fi
  done
  if ((${#missing[@]} > 0)); then
    xyz_info "$(xyz_t "Installing missing commands:" "正在安装缺失命令:") ${missing[*]}"
    xyz_pkg_install "${missing[@]}"
  fi
}

# --- confirmation (safe with curl | bash) -----------------------------------
# A readable /dev/tty node is not enough: in containers or curl|bash without a
# controlling terminal, open() fails with ENXIO ("No such device or address").
# [[ -r /dev/tty ]] is true in that case and must not be used as the check.
xyz_open_tty() {
  # Opens /dev/tty into XYZ_TTY_FD if it is a real terminal. Caller must close.
  local fd
  XYZ_TTY_FD=""
  # Brace group so a failed open cannot leak "No such device or address".
  if { exec {fd}<>/dev/tty; } 2>/dev/null && [[ -t "${fd}" ]]; then
    XYZ_TTY_FD="${fd}"
    return 0
  fi
  if [[ -n "${fd:-}" ]]; then
    exec {fd}>&- 2>/dev/null || true
  fi
  return 1
}

xyz_close_tty() {
  if [[ -n "${XYZ_TTY_FD:-}" ]]; then
    exec {XYZ_TTY_FD}>&- 2>/dev/null || true
    XYZ_TTY_FD=""
  fi
}

xyz_confirm() {
  local prompt="$1"
  if [[ "${XYZ_YES}" == "1" ]]; then
    xyz_info "$(xyz_t "Auto-confirmed (-y / YES=1):" "已自动确认(-y / YES=1):") ${prompt}"
    return 0
  fi
  local reply=""
  if xyz_open_tty; then
    printf '%s [y/N] ' "${prompt}" >&"${XYZ_TTY_FD}"
    # shellcheck disable=SC2162
    read -r reply <&"${XYZ_TTY_FD}" || true
    xyz_close_tty
  else
    xyz_warn "$(xyz_t \
      "No TTY available for confirmation. Re-run with -y or YES=1 if you accept." \
      "没有可用的终端来确认。若你已了解并同意,请加 -y 或 YES=1 重试。")"
    return 1
  fi
  case "${reply}" in
    y|Y|yes|YES|Yes) return 0 ;;
    *) return 1 ;;
  esac
}

# --- download helpers -------------------------------------------------------
# Create the temp dir in the main shell. xyz_tmpdir is usually called inside
# $(...), a subshell, so creating it (and its EXIT trap) there would delete the
# directory as soon as the subshell returned.
xyz_init_tmpdir() {
  if [[ -z "${XYZ_TMPDIR:-}" || ! -d "${XYZ_TMPDIR}" ]]; then
    XYZ_TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/xyzsh.XXXXXX")"
    # shellcheck disable=SC2064
    trap "rm -rf '${XYZ_TMPDIR}'" EXIT
  fi
}

xyz_tmpdir() {
  if [[ -z "${XYZ_TMPDIR:-}" || ! -d "${XYZ_TMPDIR}" ]]; then
    xyz_error "internal: temp dir not initialised"
    exit 1
  fi
  printf '%s' "${XYZ_TMPDIR}"
}

xyz_fetch() {
  # xyz_fetch URL DEST
  local url="$1"
  local dest="$2"
  xyz_info "$(xyz_t "Downloading" "正在下载"): ${url}"
  xyz_info "$(xyz_t "Saving to" "保存到"): ${dest}"
  if xyz_need_cmd curl; then
    curl -fsSL --connect-timeout 20 --max-time 180 --retry 2 --retry-delay 1 \
      -o "${dest}" "${url}"
  elif xyz_need_cmd wget; then
    wget -q --timeout=60 -O "${dest}" "${url}"
  else
    xyz_error "$(xyz_t "Need curl or wget to download files." "需要 curl 或 wget 才能下载文件。")"
    exit 1
  fi
  if [[ ! -s "${dest}" ]]; then
    xyz_error "$(xyz_t "Download failed or file is empty:" "下载失败或文件为空:") ${url}"
    exit 1
  fi
  if xyz_need_cmd sha256sum; then
    xyz_info "SHA256: $(sha256sum "${dest}" | awk '{print $1}')"
  elif xyz_need_cmd shasum; then
    xyz_info "SHA256: $(shasum -a 256 "${dest}" | awk '{print $1}')"
  fi
}

xyz_run_upstream() {
  # xyz_run_upstream URL [args...]
  # Downloads an official installer, shows the URL and a short preview, then
  # runs it only after confirmation (unless -y / YES=1).
  local url="$1"
  shift
  local dest
  xyz_init_tmpdir
  dest="$(xyz_tmpdir)/upstream.sh"
  xyz_fetch "${url}" "${dest}"
  chmod +x "${dest}"
  xyz_info "$(xyz_t "Preview (first 20 lines):" "预览(前 20 行):")"
  sed -n '1,20p' "${dest}" || true
  printf '%s\n' "${XYZ_C_DIM}...${XYZ_C_RESET}"
  # Default --dry-run: verify the download and preview, do not execute.
  # Scripts that forward --dry-run to upstream (e.g. docker) set
  # XYZ_FORWARD_DRY_RUN=1 so the official script still runs with that flag.
  if [[ "${XYZ_DRY_RUN:-0}" == "1" && "${XYZ_FORWARD_DRY_RUN:-0}" != "1" ]]; then
    xyz_info "$(xyz_t \
      "Dry-run: official script downloaded and previewed; not executing it." \
      "演练模式:已下载并预览官方脚本,不会执行。")"
    return 0
  fi
  if ! xyz_confirm "$(xyz_t \
    "Run this official upstream script?" \
    "运行这份官方上游脚本?")"; then
    xyz_error "$(xyz_t "Aborted by user." "已取消。")"
    exit 1
  fi
  xyz_step "$(xyz_t "Executing upstream script" "正在执行上游脚本")"
  # Never let the upstream script inherit our stdin: under `curl | bash` that
  # is the rest of this very script. Give it the terminal if there is one,
  # otherwise /dev/null. Interactive installers set XYZ_UPSTREAM_NEEDS_TTY=1.
  if xyz_open_tty; then
    local tty_fd="${XYZ_TTY_FD}"
    XYZ_TTY_FD=""
    bash "${dest}" "$@" <&"${tty_fd}"
    local rc=$?
    exec {tty_fd}>&- 2>/dev/null || true
    return "${rc}"
  fi
  if [[ "${XYZ_UPSTREAM_NEEDS_TTY:-0}" == "1" ]]; then
    xyz_error "$(xyz_t \
      "This official installer is interactive and needs a terminal. Log in with SSH and run: bash <(curl -fsSL ${XYZ_BASE}/${XYZ_NAME})" \
      "这个官方安装脚本需要交互,必须在终端里运行。请用 SSH 登录后执行:bash <(curl -fsSL ${XYZ_BASE}/${XYZ_NAME})")"
    exit 1
  fi
  bash "${dest}" "$@" </dev/null
}

# --- small shared helpers ----------------------------------------------------
xyz_strip_ansi() {
  sed -e 's/\x1b\[[0-9;?]*[A-Za-z]//g' -e 's/\r//g'
}

# Effective SSH port: sshd -T (what sshd really uses) > config files > env > 22.
xyz_effective_ssh_port() {
  local port=""
  if xyz_need_cmd sshd; then
    port="$(sshd -T 2>/dev/null | awk '$1=="port" {print $2; exit}' || true)"
  fi
  if [[ -z "${port}" && -d /etc/ssh/sshd_config.d ]]; then
    port="$(cat /etc/ssh/sshd_config.d/*.conf 2>/dev/null | awk 'tolower($1)=="port" {print $2; exit}' || true)"
  fi
  if [[ -z "${port}" && -f /etc/ssh/sshd_config ]]; then
    port="$(awk 'tolower($1)=="port" {print $2; exit}' /etc/ssh/sshd_config || true)"
  fi
  if [[ -z "${port}" && -n "${SSH_CONNECTION:-}" ]]; then
    port="$(printf '%s' "${SSH_CONNECTION}" | awk '{print $4}')"
  fi
  printf '%s' "${port:-22}"
}

xyz_has_systemd() {
  [[ -d /run/systemd/system ]] && xyz_need_cmd systemctl
}

# Back up a file once per run as FILE.xyz-bak-YYYYmmddHHMMSS and print the path.
xyz_backup_file() {
  local f="$1"
  local bak
  bak="${f}.xyz-bak-$(date +%Y%m%d%H%M%S)"
  if [[ -e "${f}" ]]; then
    cp -a "${f}" "${bak}"
    printf '%s' "${bak}"
  fi
}

# --- misc -------------------------------------------------------------------
xyz_usage_exit() {
  if [[ "${1:-}" != "" ]]; then
    printf '%s\n' "$1"
  fi
  exit 0
}

# xyz.sh Ookla helper library (shared by speedtest, speedtest-cn, all).
# Official product  : https://www.speedtest.net/apps/cli
# Official tarballs : https://install.speedtest.net/app/cli/ookla-speedtest-<ver>-linux-<arch>.tgz
# Served between lib/common.sh and the script body. No telemetry.

# shellcheck disable=SC2034
XYZ_OOKLA_LOADED=1
# Current tarball version published on https://www.speedtest.net/apps/cli
# (the binary itself reports 1.2.0.84). Override with OOKLA_CLI_VERSION.
OOKLA_CLI_VERSION="${OOKLA_CLI_VERSION:-1.2.0}"
OOKLA_CLI_BASE="https://install.speedtest.net/app/cli"

# Known SHA256 of the official 1.2.0 Linux tarballs (fetched from
# install.speedtest.net, matching the links on speedtest.net/apps/cli).
xyz_ookla_tarball_sha256() {
  case "$1" in
    x86_64)  printf '%s' "5690596c54ff9bed63fa3732f818a05dbc2db19ad36ed68f21ca5f64d5cfeeb7" ;;
    aarch64) printf '%s' "3953d231da3783e2bf8904b6dd72767c5c6e533e163d3742fd0437affa431bd3" ;;
    armhf)   printf '%s' "e45fcdebbd8a185553535533dd032d6b10bc8c64eee4139b1147b9c09835d08d" ;;
    armel)   printf '%s' "629a455a2879224bd0dbd4b36d8c721dda540717937e4660b4d2c966029466bf" ;;
    i386)    printf '%s' "9ff7e18dbae7ee0e03c66108445a2fb6ceea6c86f66482e1392f55881b772fe8" ;;
    *)       return 1 ;;
  esac
}

xyz_ookla_arch() {
  local machine
  machine="$(uname -m)"
  case "${machine}" in
    x86_64|amd64) printf 'x86_64' ;;
    aarch64|arm64) printf 'aarch64' ;;
    armv7l|armv7hl|armhf) printf 'armhf' ;;
    armv6l|armv6hl|armv5tel|armel) printf 'armel' ;;
    arm)
      # Official "arm32" / armel build.
      printf 'armel' ;;
    i386|i486|i586|i686|x86) printf 'i386' ;;
    *)
      xyz_error "$(xyz_t \
        "Unsupported architecture: ${machine}. Official Linux tarballs exist for x86_64, aarch64, armhf, armel, and i386." \
        "不支持的架构:${machine}。官方 Linux 包提供 x86_64、aarch64、armhf、armel 和 i386。")"
      exit 1
      ;;
  esac
}

xyz_ookla_url() {
  local arch="$1"
  printf '%s/ookla-speedtest-%s-linux-%s.tgz' \
    "${OOKLA_CLI_BASE}" "${OOKLA_CLI_VERSION}" "${arch}"
}

xyz_ookla_version_line() {
  local bin="$1"
  local line=""
  line="$("${bin}" --version 2>&1 || true)"
  printf '%s\n' "${line%%$'\n'*}"
}

xyz_have_official_speedtest() {
  local bin
  bin="$(command -v speedtest 2>/dev/null || true)"
  if [[ -z "${bin}" ]]; then
    return 1
  fi
  # Official Ookla binary reports "Speedtest by Ookla" and accepts
  # --accept-license. Distro Python packages often ship as speedtest-cli.
  if "${bin}" --version 2>/dev/null | grep -q 'Speedtest by Ookla'; then
    return 0
  fi
  return 1
}

# xyz_ookla_fetch_binary DIR
# Downloads the official tarball into DIR, verifies the pinned SHA256 and
# extracts it. Sets XYZ_OOKLA_EXTRACTED to the unpacked binary path.
xyz_ookla_fetch_binary() {
  local dir="$1"
  local arch url tarball expected actual
  arch="$(xyz_ookla_arch)"
  url="$(xyz_ookla_url "${arch}")"
  xyz_info "$(xyz_t "Official tarball:" "官方压缩包:") ${url}"
  tarball="${dir}/ookla-speedtest-${OOKLA_CLI_VERSION}-linux-${arch}.tgz"
  xyz_fetch "${url}" "${tarball}"
  expected=""
  if [[ "${OOKLA_CLI_VERSION}" == "1.2.0" ]]; then
    expected="$(xyz_ookla_tarball_sha256 "${arch}")"
  fi
  if [[ -n "${expected}" ]] && xyz_need_cmd sha256sum; then
    actual="$(sha256sum "${tarball}" | awk '{print $1}')"
    if [[ "${actual}" != "${expected}" ]]; then
      xyz_error "$(xyz_t "SHA256 mismatch for the official tarball." "官方压缩包的 SHA256 不匹配。")"
      xyz_error "  expected: ${expected}"
      xyz_error "  actual:   ${actual}"
      exit 1
    fi
    xyz_info "$(xyz_t "SHA256 verified." "SHA256 已校验。")"
  fi
  mkdir -p "${dir}/extract"
  tar -xzf "${tarball}" -C "${dir}/extract"
  chmod +x "${dir}/extract/speedtest" 2>/dev/null || true
  if [[ ! -x "${dir}/extract/speedtest" ]]; then
    xyz_error "$(xyz_t "The tarball did not contain an executable 'speedtest' binary." "压缩包中没有可执行的 speedtest 文件。")"
    exit 1
  fi
  XYZ_OOKLA_EXTRACTED="${dir}/extract/speedtest"
}

# xyz_ookla_ensure: make an official binary available as XYZ_OOKLA_BIN.
# Uses an installed official CLI if present. Otherwise downloads the official
# tarball; as root (after confirmation) installs it to /usr/local/bin,
# otherwise runs it from the temp dir for this run only.
xyz_ookla_ensure() {
  XYZ_OOKLA_BIN=""
  if xyz_have_official_speedtest; then
    XYZ_OOKLA_BIN="$(command -v speedtest)"
    xyz_info "$(xyz_t "Official Speedtest CLI found:" "已找到官方 Speedtest CLI:") $(xyz_ookla_version_line "${XYZ_OOKLA_BIN}")"
    return 0
  fi
  if ! xyz_need_cmd tar; then
    xyz_require_root
    xyz_ensure_cmds tar
  fi
  xyz_init_tmpdir
  xyz_ookla_fetch_binary "$(xyz_tmpdir)"
  if [[ "${XYZ_DRY_RUN:-0}" == "1" ]]; then
    XYZ_OOKLA_BIN="${XYZ_OOKLA_EXTRACTED}"
    return 0
  fi
  if xyz_is_root && xyz_confirm "$(xyz_t \
      "Install the official Speedtest CLI to /usr/local/bin/speedtest?" \
      "将官方 Speedtest CLI 安装到 /usr/local/bin/speedtest?")"; then
    install -m 0755 "${XYZ_OOKLA_EXTRACTED}" /usr/local/bin/speedtest
    XYZ_OOKLA_BIN="/usr/local/bin/speedtest"
    xyz_info "$(xyz_t "Installed:" "已安装:") ${XYZ_OOKLA_BIN}"
  else
    XYZ_OOKLA_BIN="${XYZ_OOKLA_EXTRACTED}"
    xyz_info "$(xyz_t "Using a temporary copy for this run only." "本次仅使用临时副本。")"
  fi
}

# Print Ookla's terms once and ask before passing the accept flags.
xyz_ookla_accept() {
  xyz_info "$(xyz_t \
    "Ookla's official CLI will contact Ookla servers. Ookla has its own license and privacy terms:" \
    "接下来会运行 Ookla 官方 CLI,它会连接 Ookla 服务器。Ookla 有自己的许可与隐私条款:")"
  printf '  https://www.speedtest.net/about/privacy\n'
  printf '  https://www.speedtest.net/terms\n'
  xyz_confirm "$(xyz_t \
    "Accept Ookla's terms for this run (--accept-license --accept-gdpr)?" \
    "本次运行接受 Ookla 条款(--accept-license --accept-gdpr)?")"
}

# xyz.sh terminal report helpers: frames, colored badges, bar charts.
# Shared by speedtest-cn, route, unlock and all. Pure bash, no telemetry.
#
# Colors: on only when stdout is a terminal (or XYZ_COLOR=always). Off with
# --no-color, NO_COLOR=1 or XYZ_COLOR=never.
# Glyphs: Unicode box drawing when the locale is UTF-8, otherwise ASCII
# (force with XYZ_ASCII=1 or XYZ_UTF8=1). Frames are 78 columns wide.

# shellcheck disable=SC2034
XYZ_UI_LOADED=1
XYZ_UI_WIDTH=78

xyz_ui_init() {
  local want="${XYZ_COLOR:-auto}"
  [[ "${want}" == "auto" && -n "${NO_COLOR:-}" ]] && want="never"
  [[ "${XYZ_NO_COLOR:-0}" == "1" ]] && want="never"
  XYZ_UI_COLOR=0
  case "${want}" in
    always) XYZ_UI_COLOR=1 ;;
    never) XYZ_UI_COLOR=0 ;;
    *) [[ -t 1 ]] && XYZ_UI_COLOR=1 ;;
  esac
  if [[ "${XYZ_UI_COLOR}" == "1" ]]; then
    U_X=$'\033[0m'; U_B=$'\033[1m'; U_D=$'\033[2m'
    U_RED=$'\033[31m'; U_G=$'\033[32m'; U_Y=$'\033[33m'
    U_BL=$'\033[34m'; U_M=$'\033[35m'; U_C=$'\033[36m'
  else
    U_X=""; U_B=""; U_D=""; U_RED=""; U_G=""; U_Y=""; U_BL=""; U_M=""; U_C=""
  fi
  XYZ_UI_UTF8=0
  if [[ "${XYZ_ASCII:-0}" != "1" ]]; then
    case "${LC_ALL:-${LC_CTYPE:-${LANG:-}}}" in
      *[Uu][Tt][Ff]-8*|*[Uu][Tt][Ff]8*) XYZ_UI_UTF8=1 ;;
      *) XYZ_UI_UTF8=0 ;;
    esac
    [[ "${XYZ_UTF8:-0}" == "1" ]] && XYZ_UI_UTF8=1
  fi
  if [[ "${XYZ_UI_UTF8}" == "1" ]]; then
    G_H="─"; G_V="│"; G_TL="╭"; G_TR="╮"; G_BL="╰"; G_BR="╯"; G_LT="├"; G_RT="┤"
    G_FULL="█"; G_EMPTY="░"; G_OK="✔"; G_BAD="✘"; G_UNK="?"; G_DOT="●"; G_STAR="★"
    G_DOWN="↓"; G_UP="↑"; G_WARN="⚠"; G_MID="·"; G_TIMES="×"
  else
    G_H="-"; G_V="|"; G_TL="+"; G_TR="+"; G_BL="+"; G_BR="+"; G_LT="+"; G_RT="+"
    G_FULL="#"; G_EMPTY="."; G_OK="+"; G_BAD="x"; G_UNK="?"; G_DOT="*"; G_STAR="*"
    G_DOWN="v"; G_UP="^"; G_WARN="!"; G_MID="-"; G_TIMES="x"
  fi
}

# Display width of a string: ANSI codes removed, CJK counted as 2 columns.
xyz_ui_w() {
  local s="$1" pre rest
  while [[ "${s}" == *$'\033['* ]]; do
    pre="${s%%$'\033['*}"
    rest="${s#*$'\033['}"
    rest="${rest#*m}"
    s="${pre}${rest}"
  done
  local LC_ALL=C
  local a="${s//[^ -~]/}"
  local one="${s//[^$'\xc2'-$'\xdf'$'\xe2']/}"
  local two="${s//[^$'\xe3'-$'\xef']/}"
  printf '%d' $(( ${#a} + ${#one} + 2 * ${#two} ))
}

xyz_ui_rep() {
  # xyz_ui_rep CHAR N
  local out="" n="$2"
  while (( n > 0 )); do out="${out}$1"; n=$((n - 1)); done
  printf '%s' "${out}"
}

xyz_ui_pad() {
  # xyz_ui_pad TEXT WIDTH  (left aligned, never truncates)
  local w
  w="$(xyz_ui_w "$1")"
  printf '%s%s' "$1" "$(xyz_ui_rep ' ' $(( $2 - w )))"
}

xyz_ui_lpad() {
  # xyz_ui_lpad TEXT WIDTH  (right aligned)
  local w
  w="$(xyz_ui_w "$1")"
  printf '%s%s' "$(xyz_ui_rep ' ' $(( $2 - w )))" "$1"
}

xyz_ui_top() {
  local title=" $1 " w
  w="$(xyz_ui_w "${title}")"
  printf '%s%s%s%s%s%s%s\n' "${U_C}" "${G_TL}${G_H}" "${U_X}${U_B}" "${title}" "${U_X}${U_C}" \
    "$(xyz_ui_rep "${G_H}" $(( XYZ_UI_WIDTH - 3 - w )))${G_TR}" "${U_X}"
}

xyz_ui_sep() {
  local label="${1:-}" w=0
  if [[ -n "${label}" ]]; then
    label=" ${label} "
    w="$(xyz_ui_w "${label}")"
    printf '%s%s%s%s%s%s%s\n' "${U_C}" "${G_LT}${G_H}" "${U_X}${U_B}" "${label}" "${U_X}${U_C}" \
      "$(xyz_ui_rep "${G_H}" $(( XYZ_UI_WIDTH - 3 - w )))${G_RT}" "${U_X}"
  else
    printf '%s%s%s\n' "${U_C}" "${G_LT}$(xyz_ui_rep "${G_H}" $(( XYZ_UI_WIDTH - 2 )))${G_RT}" "${U_X}"
  fi
}

xyz_ui_row() {
  local text="$1" inner=$(( XYZ_UI_WIDTH - 4 )) plain
  if (( $(xyz_ui_w "${text}") > inner )); then
    # too wide: drop colors and cut to fit, ending with "~"
    plain="$(printf '%s' "${text}" | sed 's/\x1b\[[0-9;]*m//g')"
    plain="${plain:0:inner}"
    while (( $(xyz_ui_w "${plain}") > inner - 1 )); do plain="${plain%?}"; done
    text="${plain}~"
  fi
  printf '%s%s%s %s %s%s%s\n' "${U_C}" "${G_V}" "${U_X}" "$(xyz_ui_pad "${text}" "${inner}")" "${U_C}" "${G_V}" "${U_X}"
}

xyz_ui_bottom() {
  printf '%s%s%s\n' "${U_C}" "${G_BL}$(xyz_ui_rep "${G_H}" $(( XYZ_UI_WIDTH - 2 )))${G_BR}" "${U_X}"
}

xyz_ui_bar() {
  # xyz_ui_bar VALUE MAX WIDTH [COLOR]  -> filled/empty bar (at least 1 cell if VALUE > 0)
  local n color="${4:-${U_G}}"
  n="$(awk -v v="${1:-0}" -v m="${2:-0}" -v w="$3" 'BEGIN {
    if (m <= 0 || v <= 0) { print 0; exit }
    n = int(v / m * w + 0.5); if (n < 1) n = 1; if (n > w) n = w; print n }')"
  printf '%s%s%s%s%s' "${color}" "$(xyz_ui_rep "${G_FULL}" "${n}")" "${U_D}" "$(xyz_ui_rep "${G_EMPTY}" $(( $3 - n )))" "${U_X}"
}

xyz_ui_usage() {
  # xyz_ui_usage PERCENT WIDTH -> colored usage bar + percent (red > 90, yellow > 75)
  local p="${1:-0}" color="${U_G}"
  p="${p%%.*}"; [[ "${p}" =~ ^[0-9]+$ ]] || p=0
  (( p > 75 )) && color="${U_Y}"
  (( p > 90 )) && color="${U_RED}"
  printf '%s %s%3d%%%s' "$(xyz_ui_bar "${p}" 100 "$2" "${color}")" "${color}" "${p}" "${U_X}"
}

xyz_ui_badge() {
  # xyz_ui_badge yes|no|partial|unknown|ok|skip [TEXT]
  local txt="${2:-}"
  case "$1" in
    yes|ok) printf '%s%s %s%s' "${U_G}" "${G_OK}" "${txt:-$(xyz_t "yes" "解锁")}" "${U_X}" ;;
    no|fail) printf '%s%s %s%s' "${U_RED}" "${G_BAD}" "${txt:-$(xyz_t "no" "不可用")}" "${U_X}" ;;
    partial) printf '%s%s %s%s' "${U_Y}" "${G_UNK}" "${txt:-$(xyz_t "partial" "部分")}" "${U_X}" ;;
    skip) printf '%s%s %s%s' "${U_D}" "-" "${txt:-$(xyz_t "skipped" "跳过")}" "${U_X}" ;;
    *) printf '%s%s %s%s' "${U_Y}" "${G_UNK}" "${txt:-$(xyz_t "unknown" "未知")}" "${U_X}" ;;
  esac
}

xyz_ui_line_class() {
  # premium | good | ordinary | other
  case "$1" in
    CN2-GIA|9929/CUII|9929|CMIN2) printf 'premium' ;;
    CN2-GT) printf 'good' ;;
    163|4837|CMI|CMNET) printf 'ordinary' ;;
    *) printf 'other' ;;
  esac
}

xyz_ui_linetags() {
  # xyz_ui_linetags "CN2-GIA 9929/CUII" -> colored tags
  local t out="" c
  for t in $1; do
    case "$(xyz_ui_line_class "${t}")" in
      premium) c="${U_G}${U_B}" ;;
      good) c="${U_C}" ;;
      ordinary) c="${U_Y}" ;;
      *) c="${U_D}" ;;
    esac
    out="${out}${out:+ }${c}${t}${U_X}"
  done
  printf '%s' "${out:-${U_D}?${U_X}}"
}

xyz_ui_fill() {
  # Replace empty TSV fields with "-" (bash read collapses consecutive tabs).
  awk -F'\t' -v OFS='\t' '{ for (i = 1; i <= NF; i++) if ($i == "") $i = "-"; print }' "$1"
}

xyz_ui_carrier() {
  case "$1" in
    nearest) xyz_t "Local" "本地" ;;
    ct) xyz_t "Telecom" "电信" ;;
    cu) xyz_t "Unicom" "联通" ;;
    cm) xyz_t "Mobile" "移动" ;;
    custom) xyz_t "Custom" "自定义" ;;
    hk) xyz_t "HK" "香港" ;;
    tokyo) xyz_t "Tokyo" "东京" ;;
    singapore) xyz_t "SGP" "新加坡" ;;
    us-west) xyz_t "US West" "美西" ;;
    *) printf '%s' "$1" ;;
  esac
}

xyz_ui_carrier_color() {
  case "$1" in
    ct|tokyo) printf '%s' "${U_BL}" ;;
    cu) printf '%s' "${U_RED}" ;;
    cm|singapore) printf '%s' "${U_G}" ;;
    hk) printf '%s' "${U_Y}" ;;
    *) printf '%s' "${U_M}" ;;
  esac
}

xyz_ui_fmt_num() {
  # 2 decimals below 1000, 1 decimal above
  awk -v v="${1:-0}" 'BEGIN { if (v >= 1000) printf "%.1f", v; else printf "%.2f", v }'
}

# xyz_ui_speed_rows FILE
# FILE: carrier id label down up ping loss status [engine] [note] (TSV, Mbps / ms)
xyz_ui_speed_rows() {
  local f="$1" max carrier id label down up lat loss status engine note head
  [[ -s "${f}" ]] || return 0
  max="$(awk -F'\t' '$8=="ok" { if ($4+0 > m) m = $4+0; if ($5+0 > m) m = $5+0 } END { print m+0 }' "${f}")"
  while IFS=$'\t' read -r carrier id label down up lat loss status engine note; do
    [[ "${engine}" == "-" ]] && engine=""
    head="$(xyz_ui_carrier_color "${carrier}")$(xyz_ui_pad "$(xyz_ui_carrier "${carrier}")" 8)${U_X}${U_B}${label}${U_X}${U_D} #${id}${engine:+ ${G_MID} ${engine}}${U_X}"
    xyz_ui_row "${head}"
    if [[ "${status}" == "ok" ]]; then
      local pc="${U_G}"
      lat="${lat:-?}"
      awk -v p="${lat}" 'BEGIN { exit !(p+0 > 150) }' 2>/dev/null && pc="${U_Y}"
      awk -v p="${lat}" 'BEGIN { exit !(p+0 > 250) }' 2>/dev/null && pc="${U_RED}"
      xyz_ui_row "  ${G_DOWN} $(xyz_ui_bar "${down}" "${max}" 14 "${U_G}") $(xyz_ui_lpad "$(xyz_ui_fmt_num "${down}")" 7) Mbps  ${G_UP} $(xyz_ui_bar "${up}" "${max}" 14 "${U_C}") $(xyz_ui_lpad "$(xyz_ui_fmt_num "${up}")" 7) Mbps  ${pc}$(xyz_ui_lpad "${lat}" 6)ms${U_X}"
    else
      [[ "${note}" == "-" ]] && note=""
      xyz_ui_row "  $(xyz_ui_badge skip "$(xyz_t "skipped" "跳过")")${U_D}${note:+ ${G_MID} ${note:0:62}}${U_X}"
    fi
  done < <(xyz_ui_fill "${f}")
}

xyz_ui_speed_table() {
  # xyz_ui_speed_table FILE TITLE
  xyz_ui_top "$2"
  xyz_ui_speed_rows "$1"
  xyz_ui_bottom
}

# Route grid. FILE: target label kind hops [key]   (key like bj-ct)
xyz_ui_route_key() {
  case "$1" in
    219.141.140.10) printf 'bj-ct' ;; 202.106.195.68) printf 'bj-cu' ;; 221.179.155.161) printf 'bj-cm' ;;
    202.96.209.133) printf 'sh-ct' ;; 210.22.97.1) printf 'sh-cu' ;; 211.136.112.200) printf 'sh-cm' ;;
    58.60.188.222) printf 'gz-ct' ;; 210.21.196.6) printf 'gz-cu' ;; 120.196.165.24) printf 'gz-cm' ;;
    *) printf '' ;;
  esac
}

xyz_ui_route_rows() {
  local f="$1" target label kind hops key city c cell line extra=0
  local -A K=()
  [[ -s "${f}" ]] || return 0
  while IFS=$'\t' read -r target label kind hops key; do
    [[ -z "${key}" || "${key}" == "-" ]] && key="$(xyz_ui_route_key "${target}")"
    if [[ -n "${key}" ]]; then
      K["${key}"]="${kind}"
    else
      extra=1
    fi
  done < <(xyz_ui_fill "${f}")
  if (( ${#K[@]} > 0 )); then
    xyz_ui_row "${U_D}$(xyz_ui_pad "" 10)$(xyz_ui_pad "$(xyz_ui_carrier ct)" 21)$(xyz_ui_pad "$(xyz_ui_carrier cu)" 21)$(xyz_ui_carrier cm)${U_X}"
    for city in bj sh gz; do
      case "${city}" in
        bj) line="$(xyz_ui_pad "$(xyz_t "Beijing" "北京")" 10)" ;;
        sh) line="$(xyz_ui_pad "$(xyz_t "Shanghai" "上海")" 10)" ;;
        *) line="$(xyz_ui_pad "$(xyz_t "Guangzhou" "广州")" 10)" ;;
      esac
      for c in ct cu cm; do
        cell="${K[${city}-${c}]-}"
        [[ "${cell}" == *" ("* || "${cell}" == unknown* || "${cell}" == 未识别* ]] && cell="?"
        if [[ -z "${cell}" ]]; then cell="${U_D}-${U_X}"; else cell="$(xyz_ui_linetags "${cell}")"; fi
        line="${line}$(xyz_ui_pad "${cell}" 21)"
      done
      xyz_ui_row "${line}"
    done
  fi
  if [[ "${extra}" == "1" ]]; then
    while IFS=$'\t' read -r target label kind hops key; do
      [[ -z "${key}" || "${key}" == "-" ]] && key="$(xyz_ui_route_key "${target}")"
      [[ -n "${key}" ]] && continue
      xyz_ui_row "$(xyz_ui_pad "${label:-${target}}" 22) $(xyz_ui_linetags "${kind}")"
    done < <(xyz_ui_fill "${f}")
  fi
  xyz_ui_row "${U_D}$(xyz_t "Legend:" "图例:")${U_X} ${U_G}${U_B}$(xyz_t "premium" "优质")${U_X} CN2-GIA/9929/CMIN2  ${U_C}CN2-GT${U_X}  ${U_Y}$(xyz_t "ordinary" "普通")${U_X} 163/4837/CMI"
}

xyz_ui_route_score() {
  # prints 0-100 (average best class per target) or nothing
  local f="$1" kind t best n=0 sum=0 cls
  [[ -s "${f}" ]] || return 0
  while IFS=$'\t' read -r _ _ kind _ _; do
    best=0
    for t in ${kind}; do
      cls="$(xyz_ui_line_class "${t}")"
      case "${cls}" in premium) best=100 ;; good) (( best < 75 )) && best=75 ;; ordinary) (( best < 40 )) && best=40 ;; *) : ;; esac
    done
    n=$((n + 1)); sum=$((sum + best))
  done < "${f}"
  (( n > 0 )) && printf '%d' $(( sum / n ))
}

# Unlock grid. FILE: service status region note
xyz_ui_unlock_rows() {
  local f="$1" svc st reg note cells=() i cell
  [[ -s "${f}" ]] || return 0
  while IFS=$'\t' read -r svc st reg note; do
    cell="$(xyz_ui_badge "${st}" "$(xyz_ui_pad "${svc}" 16)")"
    [[ "${reg}" != "-" ]] && cell="${cell} ${U_D}${reg:0:12}${U_X}"
    cells+=("${cell}")
  done < <(xyz_ui_fill "${f}")
  for (( i = 0; i < ${#cells[@]}; i += 2 )); do
    xyz_ui_row "$(xyz_ui_pad "${cells[$i]}" 37)${cells[$((i + 1))]-}"
  done
}

xyz_ui_unlock_count() {
  # prints "yes total"
  awk -F'\t' '{ n++; if ($2 == "yes") y++ } END { printf "%d %d\n", y, n }' "$1"
}

xyz_ui_init

XYZ_NAME="speedtest"

# xyz.sh/speedtest — install and run the official Ookla Speedtest CLI.
#
# Official product  : https://www.speedtest.net/apps/cli
# Official tarballs : https://install.speedtest.net/app/cli/ookla-speedtest-<ver>-linux-<arch>.tgz
#
# This wrapper downloads Ookla's official Linux tarball (no package repository
# is added), installs the `speedtest` binary, then optionally runs a test.
# Ookla's CLI contacts Ookla servers (that is the test) and has its own
# license / GDPR prompts. This wrapper will not silently pass
# --accept-license or --accept-gdpr unless you confirm (or use -y after the
# notice is printed).
#
# Usage:
#   curl -fsSL xyz.sh/speedtest | bash
#   curl -fsSL xyz.sh/speedtest | bash -s -- -y
#   curl -fsSL xyz.sh/speedtest | bash -s -- --install-only
#   curl -fsSL xyz.sh/speedtest | bash -s -- --dry-run
#   curl -fsSL xyz.sh/speedtest | bash -s -- --uninstall
#
# Supported: Debian, Ubuntu, CentOS, Rocky, AlmaLinux (any Linux arch listed
# on Ookla's download page: x86_64, aarch64, armhf, armel, i386).
# Requires root to install into /usr/local/bin; --keep can copy the binary
# into the current directory without a system install.
# Running an already-installed official binary does not require root.
# No telemetry is added by this wrapper.

set -euo pipefail

if [[ -z "${XYZ_COMMON_LOADED:-}" ]]; then
  _xyz_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  if [[ -f "${_xyz_dir}/lib/common.sh" ]]; then
    # shellcheck source=lib/common.sh
    source "${_xyz_dir}/lib/common.sh"
  else
    echo "xyz.sh: missing lib/common.sh" >&2
    exit 1
  fi
fi

if [[ -z "${XYZ_OOKLA_LOADED:-}" ]]; then
  _xyz_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  # shellcheck source=lib/ookla.sh
  source "${_xyz_dir}/lib/ookla.sh"
fi
if [[ -z "${XYZ_UI_LOADED:-}" ]]; then
  _xyz_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  if [[ -f "${_xyz_dir}/lib/report.sh" ]]; then
    # shellcheck source=lib/report.sh
    source "${_xyz_dir}/lib/report.sh"
  fi
fi

XYZ_NAME="speedtest"
XYZ_TITLE="$(xyz_t "Ookla Speedtest CLI" "Ookla Speedtest 命令行")"
XYZ_UPSTREAM="https://www.speedtest.net/apps/cli"
# OOKLA_CLI_VERSION / OOKLA_CLI_BASE and the pinned SHA256 table live in lib/ookla.sh.
INSTALL_PREFIX="/usr/local/bin"
DO_INSTALL_ONLY=0
DO_UNINSTALL=0
DO_KEEP=0
DO_NO_INSTALL=0
DO_REINSTALL=0
DO_OVERSEAS=0
DO_LIST=0

xyz_init "$@"

xyz_speedtest_uninstall_help() {
  cat <<HINT
$(xyz_t "Uninstall:" "卸载:")
  curl -fsSL xyz.sh/speedtest | bash -s -- --uninstall
  $(xyz_t "or delete the binary yourself:" "或自行删除二进制文件:")
  rm -f /usr/local/bin/speedtest
  $(xyz_t "License acceptance may live in ~/.config/ookla; remove that directory if you want a clean slate." "许可接受记录可能在 ~/.config/ookla,如需彻底清理可删除该目录。")
  $(xyz_t "If an older package-manager install still exists, also run apt-get/dnf/yum remove speedtest." "若以前用包管理器安装过 speedtest,请另外执行 apt-get/dnf/yum remove speedtest。")
HINT
}

i=0
while [[ $i -lt ${#XYZ_ARGS[@]} ]]; do
  arg="${XYZ_ARGS[$i]}"
  case "${arg}" in
    --install-only) DO_INSTALL_ONLY=1 ;;
    --uninstall|--remove) DO_UNINSTALL=1 ;;
    --keep) DO_KEEP=1 ;;
    --no-install) DO_NO_INSTALL=1 ;;
    --reinstall) DO_REINSTALL=1 ;;
    --overseas|--regions) DO_OVERSEAS=1 ;;
    --list) DO_LIST=1 ;;
    --prefix)
      i=$((i + 1))
      INSTALL_PREFIX="${XYZ_ARGS[$i]:-}"
      if [[ -z "${INSTALL_PREFIX}" ]]; then
        xyz_error "$(xyz_t "--prefix requires a directory." "--prefix 需要目录。")"
        exit 1
      fi
      ;;
    --prefix=*) INSTALL_PREFIX="${arg#--prefix=}" ;;
    --help|-h) XYZ_HELP=1 ;;
    *)
      xyz_error "$(xyz_t "Unknown argument:" "未知参数:") ${arg}"
      exit 1
      ;;
  esac
  i=$((i + 1))
done

DEST="${INSTALL_PREFIX%/}/speedtest"

if [[ "${XYZ_HELP:-}" == "1" ]]; then
  cat <<HINT
$(xyz_t "Usage:" "用法:")
  curl -fsSL xyz.sh/speedtest | bash
  curl -fsSL xyz.sh/speedtest | bash -s -- -y
  curl -fsSL xyz.sh/speedtest | bash -s -- -y
  curl -fsSL xyz.sh/speedtest | bash -s -- --overseas -y
  curl -fsSL xyz.sh/speedtest | bash -s -- --list
  curl -fsSL xyz.sh/speedtest | bash -s -- --install-only
  curl -fsSL xyz.sh/speedtest | bash -s -- --dry-run
  curl -fsSL xyz.sh/speedtest | bash -s -- --keep --no-install
  curl -fsSL xyz.sh/speedtest | bash -s -- --uninstall

Official: https://www.speedtest.net/apps/cli
$(xyz_t "Downloads the official Linux tarball (no apt/dnf repository is added) into a temporary directory, then installs speedtest to ${DEST}." "从官方 Linux tarball 下载(不会添加 apt/dnf 软件源)到临时目录,再安装到 ${DEST}。")
$(xyz_t "Default: one test against the server Ookla picks as nearest." "默认对 Ookla 自动选择的就近节点测一次。")
$(xyz_t "--overseas also tests Hong Kong, Tokyo, Singapore and the US West. The official CLI is tried first (servers looked up by region); if that fails, speedtest-go (official release, SHA256 pinned, temp dir only) is used. Mainland China nodes are not tested." "--overseas 会再测香港、东京、新加坡、美西:先用官方 CLI 按地区检索服务器,失败则用 speedtest-go(官方发布版,SHA256 固定,只在临时目录运行)。不测试中国大陆节点。")
$(xyz_t "--keep copies the binary into the current directory as ./speedtest. --no-install skips ${DEST}." "--keep 会把二进制复制到当前目录的 ./speedtest。--no-install 不写入 ${DEST}。")

$(xyz_speedtest_uninstall_help)
HINT
  exit 0
fi

# ---- overseas multi-node test (Hong Kong / Tokyo / Singapore / US West) --------
# id|region|name-zh|name-en|host:port
OV_NODES=(
  "1536|hk|香港 HKBN|Hong Kong (HKBN)|speedtest.hkbn.net:8080"
  "40508|hk|香港|Hong Kong|"
  "28910|tokyo|东京|Tokyo|"
  "2433|tokyo|东京|Tokyo|"
  "4235|singapore|新加坡 StarHub|Singapore (StarHub)|"
  "7311|singapore|新加坡|Singapore|"
  "14236|us-west|洛杉矶|Los Angeles|"
  "17384|us-west|圣何塞|San Jose|"
)
OV_REGIONS=(hk tokyo singapore us-west)
GO_VERSION="1.8.3"
GO_BASE="https://github.com/showwin/speedtest-go/releases/download/v${GO_VERSION}"

ov_field() { printf '%s' "$1" | cut -d'|' -f"$2"; }
ov_name() { xyz_t "$(ov_field "$1" 4)" "$(ov_field "$1" 3)"; }
ov_region_label() { xyz_ui_carrier "$1" 2>/dev/null || printf '%s' "$1"; }

print_ov_nodes() {
  local n
  printf '  %-8s %-10s %s\n' "ID" "$(xyz_t "Region" "地区")" "$(xyz_t "Node" "节点")"
  for n in "${OV_NODES[@]}"; do
    printf '  %-8s %-10s %s\n' "$(ov_field "${n}" 1)" "$(ov_region_label "$(ov_field "${n}" 2)")" "$(ov_name "${n}")"
  done
}

if [[ "${DO_LIST}" == "1" ]]; then
  xyz_step "$(xyz_t "Overseas nodes (built-in candidates; a live search is used at run time)" "境外节点(内置候选;运行时还会按地区检索)")"
  print_ov_nodes
  exit 0
fi

# ---- helpers used by --overseas ------------------------------------------------
go_asset_arch() {
  case "$(uname -m)" in
    x86_64|amd64) printf 'x86_64' ;;
    aarch64|arm64) printf 'arm64' ;;
    armv7l|armv7hl|armhf) printf 'armv7' ;;
    armv6l) printf 'armv6' ;;
    i386|i486|i586|i686) printf 'i386' ;;
    *) return 1 ;;
  esac
}
go_sha256() {
  case "$1" in
    x86_64) printf 'c55caae22927cd719a2f8c0bef96ecf74b5fe8ca919ff9200d76328ae5de9477' ;;
    arm64) printf '48f51504548d76d5dc2ce7f72e713bd3b4e35554028b7131467060688754c657' ;;
    armv7) printf '29d6f7b1038d765970c4f0a24e2e26a80b2041f58a7ce3c2e1b4318317c0fbe0' ;;
    armv6) printf '395fd65509af7297b62d8942a323483b48c176cd9404889cc499870fe291d596' ;;
    i386) printf '7b9641833a94c5937d9872f589e017c0fa5e3ef9c4a36a4d88f3b5c7355dc053' ;;
    *) return 1 ;;
  esac
}
GO_BIN=""
GO_TRIED=0
fetch_go() {
  [[ -n "${GO_BIN}" ]] && return 0
  [[ "${GO_TRIED}" == "1" ]] && return 1
  GO_TRIED=1
  local arch asset want got dir
  xyz_init_tmpdir
  dir="$(xyz_tmpdir)/speedtest-go"
  arch="$(go_asset_arch)" || { xyz_warn "$(xyz_t "speedtest-go has no build for" "speedtest-go 没有对应架构:") $(uname -m)"; return 1; }
  asset="speedtest-go_${GO_VERSION}_Linux_${arch}.tar.gz"
  want="$(go_sha256 "${arch}")"
  xyz_info "$(xyz_t "speedtest-go (official release, run from a temp dir, nothing installed):" "speedtest-go(官方发布版,仅在临时目录运行,不安装):") ${GO_BASE}/${asset}"
  if [[ "${1:-}" != "noconfirm" ]] && ! xyz_confirm "$(xyz_t "Download speedtest-go as a fallback if the official CLI cannot select a server?" "若官方 CLI 无法选中节点,是否下载 speedtest-go 作为备选?")"; then
    return 1
  fi
  mkdir -p "${dir}"
  if ! curl -fsSL --connect-timeout 15 --max-time 120 -o "${dir}/${asset}" "${GO_BASE}/${asset}"; then
    xyz_warn "$(xyz_t "Could not download speedtest-go." "无法下载 speedtest-go。")"
    return 1
  fi
  got="$(sha256sum "${dir}/${asset}" | awk '{print $1}')"
  if [[ "${got}" != "${want}" ]]; then
    xyz_warn "$(xyz_t "speedtest-go SHA256 mismatch, not using it:" "speedtest-go SHA256 不匹配,不使用:") ${got}"
    return 1
  fi
  xyz_info "$(xyz_t "SHA256 verified:" "SHA256 已校验:") ${got}"
  tar -xzf "${dir}/${asset}" -C "${dir}" speedtest-go 2>/dev/null || tar -xzf "${dir}/${asset}" -C "${dir}"
  if [[ ! -x "${dir}/speedtest-go" ]]; then
    xyz_warn "$(xyz_t "The speedtest-go archive had no binary." "speedtest-go 压缩包里没有可执行文件。")"
    return 1
  fi
  GO_BIN="${dir}/speedtest-go"
}

mbps_bytes() { awk -v b="${1:-0}" 'BEGIN { printf "%.2f", b * 8 / 1000000 }'; }
explain_error() {
  local f="$1" msgs
  msgs="$(grep -o '"message":"[^"]*"' "${f}" 2>/dev/null | cut -d'"' -f4 | paste -sd ';' - || true)"
  if [[ -n "${msgs}" ]]; then printf '%s' "${msgs}"
  else tr '\n\r' '  ' < "${f}" | sed 's/  */ /g; s/^ //; s/ $//'; fi
}

ov_record() {
  printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$@" >> "${OV_RESULTS}"
}
ov_skip() {
  xyz_warn "$(xyz_t "Node failed, skipping:" "节点不可用,已跳过:") $3 (#$2, $4): $5"
  ov_record "$1" "$2" "$3" "-" "-" "-" "-" "skip" "$4" "$(printf '%s' "$5" | tr '\t' ' ' | cut -c1-160)"
}
ov_probe() {
  local code
  code="$(curl -s -o /dev/null -m 5 --connect-timeout 4 -w '%{http_code}' "$1" 2>/dev/null || true)"
  [[ "${code}" =~ ^[234][0-9][0-9]$ ]]
}

run_ookla_ov() {
  local id="$1" carrier="$2" label="$3" out err runner sel
  out="${OV_TMP}/st-${carrier}-${id}.json"
  err="${OV_TMP}/st-${carrier}-${id}.err"
  runner=(); sel=(-s "${id}")
  xyz_need_cmd timeout && runner=(timeout 120)
  [[ "${id}" == "auto" ]] && sel=()
  printf '  %s %s (#%s, Ookla CLI)...\n' "$(xyz_t "Testing" "正在测试")" "${label}" "${id}"
  OOKLA_ERR=""
  if "${runner[@]+"${runner[@]}"}" "${RUN_BIN}" --accept-license --accept-gdpr \
      "${sel[@]+"${sel[@]}"}" -f json -p no >"${out}" 2>"${err}" \
      && grep -q '"type":"result"' "${out}"; then
    local lat down up loss sname sid
    lat="$(grep -o '"latency":[0-9.]*' "${out}" | head -1 | cut -d: -f2)"
    down="$(grep -o '"download":{"bandwidth":[0-9]*' "${out}" | grep -o '[0-9]*$')"
    up="$(grep -o '"upload":{"bandwidth":[0-9]*' "${out}" | grep -o '[0-9]*$')"
    loss="$(grep -o '"packetLoss":[0-9.]*' "${out}" | head -1 | cut -d: -f2)"
    if [[ "${id}" == "auto" ]]; then
      sname="$(grep -o '"server":{[^}]*' "${out}" | grep -o '"name":"[^"]*"' | head -1 | cut -d'"' -f4)"
      sid="$(grep -o '"server":{"id":[0-9]*' "${out}" | grep -o '[0-9]*$')"
      label="${label}: ${sname:-?}"
      id="${sid:-auto}"
    fi
    ov_record "${carrier}" "${id}" "${label}" "$(mbps_bytes "${down}")" "$(mbps_bytes "${up}")" "${lat:-?}" "${loss:-n/a}" ok ookla ""
    OV_USED_IDS="${OV_USED_IDS} ${id} "
    return 0
  fi
  cat "${out}" >> "${err}" 2>/dev/null || true
  OOKLA_ERR="$(explain_error "${err}")"
  [[ -z "${OOKLA_ERR}" ]] && OOKLA_ERR="$(xyz_t "no result (exit code or timeout)" "没有结果(退出或超时)")"
  return 1
}

run_go_ov() {
  local hp="$1" id="$2" carrier="$3" label="$4" out runner
  out="${OV_TMP}/go-${id}.json"
  runner=()
  GO_ERR=""
  if ! fetch_go; then GO_ERR="$(xyz_t "speedtest-go unavailable" "speedtest-go 不可用")"; return 1; fi
  xyz_need_cmd timeout && runner=(timeout 90)
  printf '  %s %s (#%s, speedtest-go %s)...\n' "$(xyz_t "Testing" "正在测试")" "${label}" "${id}" "${hp}"
  if "${runner[@]+"${runner[@]}"}" "${GO_BIN}" --custom-url="http://${hp}" --json >"${out}" 2>&1 \
      && grep -q '"dl_speed"' "${out}"; then
    local lat dl ul
    lat="$(grep -o '"latency":[0-9]*' "${out}" | head -1 | cut -d: -f2)"
    dl="$(grep -o '"dl_speed":[0-9.e+]*' "${out}" | head -1 | cut -d: -f2)"
    ul="$(grep -o '"ul_speed":[0-9.e+]*' "${out}" | head -1 | cut -d: -f2)"
    if awk -v d="${dl:-0}" -v u="${ul:-0}" 'BEGIN { exit !(d + u > 0) }'; then
      ov_record "${carrier}" "${id}" "${label}" "$(mbps_bytes "${dl}")" "$(mbps_bytes "${ul}")" \
        "$(awk -v n="${lat:-0}" 'BEGIN { printf "%.1f", n / 1000000 }')" "n/a" ok speedtest-go ""
      OV_USED_IDS="${OV_USED_IDS} ${id} "
      return 0
    fi
  fi
  GO_ERR="$(explain_error "${out}")"
  [[ -z "${GO_ERR}" ]] && GO_ERR="$(xyz_t "no result" "没有结果")"
  return 1
}

search_term() {
  case "$1" in
    hk) printf 'Hong Kong' ;;
    tokyo) printf 'Tokyo' ;;
    singapore) printf 'Singapore' ;;
    us-west) printf 'Los Angeles' ;;
    *) printf '%s' "$1" ;;
  esac
}

fetch_live_region() {
  local region="$1" term q url tmp py
  term="$(search_term "${region}")"
  q="$(printf '%s' "${term}" | sed 's/ /%20/g')"
  url="https://www.speedtest.net/api/js/servers?engine=js&https_functional=true&limit=8&search=${q}"
  tmp="${OV_TMP}/live-${region}.json"
  xyz_info "$(xyz_t "Looking up" "正在检索") ${term}: ${url}" >&2
  if ! curl -fsSL --connect-timeout 10 --max-time 20 -A "curl xyz.sh/speedtest" -o "${tmp}" "${url}"; then
    return 1
  fi
  py="$(command -v python3 || command -v python || true)"
  if [[ -z "${py}" ]]; then return 1; fi
  "${py}" - "${tmp}" "${region}" <<'PY'
import json, sys
path, region = sys.argv[1], sys.argv[2]
try:
    data = json.load(open(path, encoding="utf-8"))
except Exception:
    sys.exit(1)
if isinstance(data, dict):
    data = data.get("servers") or data.get("data") or []
for s in data or []:
    if not isinstance(s, dict):
        continue
    sid = s.get("id") or s.get("server_id")
    if sid is None:
        continue
    host = (s.get("host") or "").replace(".prod.hosts.ooklaserver.net", "")
    cc = (s.get("cc") or s.get("country_code") or s.get("country") or "")
    name = s.get("name") or s.get("sponsor") or "server"
    sponsor = s.get("sponsor") or name
    if region == "hk" and cc and cc not in ("HK", "Hong Kong"):
        continue
    if region == "tokyo" and cc and cc not in ("JP", "Japan"):
        continue
    if region == "singapore" and cc and cc not in ("SG", "Singapore"):
        continue
    if region == "us-west":
        blob = " ".join([str(cc), str(name), str(s.get("location") or ""), str(sponsor)])
        if "CN" == str(cc) or "China" in blob:
            continue
        if not any(k in blob for k in ("Los Angeles", "San Jose", "San Francisco", "Seattle", "San Diego", "Phoenix", "Las Vegas", "Fremont", "Santa Clara", "California", "Washington", "Oregon", "Arizona", "Nevada", "CA", "WA", "OR")):
            if str(cc) not in ("US", "United States", ""):
                continue
    label = "%s (%s)" % (name, sponsor) if sponsor and sponsor != name else str(name)
    sys.stdout.write("%s|%s|%s|%s|%s\n" % (sid, region, label, label, host))
PY
}

candidates_for() {
  local region="$1" n
  for n in "${OV_NODES[@]}"; do
    [[ "$(ov_field "${n}" 2)" == "${region}" ]] || continue
    printf '%s\n' "${n}"
  done
  fetch_live_region "${region}" 2>/dev/null || true
}

test_region() {
  local region="$1" ok=0 tries=0 line id zh en host label why
  xyz_step "$(xyz_t "Region" "地区"): $(ov_region_label "${region}")"
  while IFS= read -r line; do
    [[ -z "${line}" ]] && continue
    id="$(ov_field "${line}" 1)"
    [[ "${id}" =~ ^[0-9]+$ ]] || continue
    [[ "${OV_USED_IDS}" == *" ${id} "* ]] && continue
    zh="$(ov_field "${line}" 3)"
    en="$(ov_field "${line}" 4)"
    host="$(ov_field "${line}" 5)"
    label="$(xyz_t "${en}" "${zh}")"
    tries=$((tries + 1))
    if (( tries > 3 )); then break; fi
    if run_ookla_ov "${id}" "${region}" "${label}"; then ok=1; break
    else
      why="Ookla CLI: ${OOKLA_ERR}"
      if [[ -n "${host}" ]]; then
        if ! ov_probe "http://${host}/hello"; then
          ov_skip "${region}" "${id}" "${label}" "probe" "${why}; $(xyz_t "server unreachable:" "服务器无法连接:") http://${host}"
          continue
        fi
        if run_go_ov "${host}" "${id}" "${region}" "${label}"; then ok=1; break
        else
          ov_skip "${region}" "${id}" "${label}" "ookla/go" "${why}; speedtest-go: ${GO_ERR}"
        fi
      else
        ov_skip "${region}" "${id}" "${label}" "ookla" "${why}"
      fi
    fi
  done < <(candidates_for "${region}")
  if [[ "${ok}" == "0" && "${tries}" == "0" ]]; then
    ov_record "${region}" "-" "$(ov_region_label "${region}")" "-" "-" "-" "-" "skip" "-" "$(xyz_t "no candidate server" "没有候选节点")"
  fi
}

xyz_run_overseas() {
  xyz_init_tmpdir
  OV_TMP="$(xyz_tmpdir)"
  OV_RESULTS="${OV_TMP}/ov-results.tsv"
  : > "${OV_RESULTS}"
  OV_USED_IDS=" "
  xyz_step "$(xyz_t "Overseas nodes (built-in)" "境外节点(内置)")"
  print_ov_nodes
  if [[ "${XYZ_DRY_RUN:-0}" == "1" ]]; then
    fetch_go noconfirm || true
    xyz_info "$(xyz_t \
      "Dry-run: official CLI (and speedtest-go) downloaded and verified; node list printed; no test was run." \
      "演练模式:已下载并校验官方 CLI(及 speedtest-go),已列出节点,未进行测速。")"
    return 0
  fi
  xyz_step "$(xyz_t "Running overseas speed tests" "正在进行境外测速")"
  if ! run_ookla_ov auto nearest "$(xyz_t "Nearest" "就近节点")"; then
    ov_skip nearest auto "$(xyz_t "Nearest" "就近节点")" ookla "${OOKLA_ERR}"
  fi
  local r
  for r in "${OV_REGIONS[@]}"; do
    test_region "${r}"
  done
  echo
  local view="${OV_TMP}/ov-view.tsv"
  local tried_word
  tried_word="$(xyz_t "tried" "个已尝试")"
  awk -F'\t' -v OFS='\t' -v w="${tried_word}" '
    { if (!($1 in seen)) { seen[$1] = 1; order[++n] = $1 }
      if ($8 == "ok") { good[$1] = 1; rows[++m] = $0; rc[m] = $1 }
      else { cnt[$1]++; if (!($1 in first)) first[$1] = $0; last[$1] = $10 } }
    END {
      for (i = 1; i <= n; i++) {
        c = order[i]
        if (good[c]) { for (j = 1; j <= m; j++) if (rc[j] == c) print rows[j] }
        else { split(first[c], f, "\t"); print c, f[2], f[3], "-", "-", "-", "-", "skip", cnt[c] " " w, last[c] }
      }
    }' "${OV_RESULTS}" > "${view}"
  if declare -F xyz_ui_speed_table >/dev/null 2>&1; then
    xyz_ui_speed_table "${view}" "$(xyz_t "Overseas speed test" "境外测速") (Mbps / ms)"
  else
    cat "${view}"
  fi
  echo
  if [[ -n "${XYZ_RESULT_FILE:-}" ]]; then
    sed 's/^/speedtest\t/' "${view}" >> "${XYZ_RESULT_FILE}" 2>/dev/null || true
  fi
  local good
  good="$(awk -F'\t' '$8 == "ok"' "${OV_RESULTS}" | wc -l | tr -d ' ')"
  if [[ "${good}" == "0" ]]; then
    xyz_error "$(xyz_t \
      "No overseas node finished a test. The network may block speed-test traffic." \
      "没有任何境外节点完成测速。当前网络可能屏蔽了测速流量。")"
    return 2
  fi
  xyz_info "$(xyz_t "Overseas speed test finished." "境外测速完成。")"
  return 0
}

if [[ "${DO_UNINSTALL}" == "1" ]]; then
  xyz_require_root
  if [[ ! -e "${DEST}" ]]; then
    xyz_error "$(xyz_t "No speedtest binary found at:" "找不到 speedtest:") ${DEST}"
    xyz_speedtest_uninstall_help
    exit 1
  fi
  xyz_info "$(xyz_t "Will remove:" "将删除:") ${DEST}"
  if [[ "${XYZ_DRY_RUN:-0}" == "1" ]]; then
    xyz_info "$(xyz_t "Dry-run: not deleting the binary." "演练模式:不会删除该文件。")"
    xyz_speedtest_uninstall_help
    exit 0
  fi
  if ! xyz_confirm "$(xyz_t "Remove ${DEST}?" "删除 ${DEST}?")"; then
    exit 1
  fi
  rm -f "${DEST}"
  xyz_info "$(xyz_t "Removed." "已删除。")"
  xyz_speedtest_uninstall_help
  exit 0
fi

RUN_BIN=""
if [[ "${DO_REINSTALL}" != "1" ]] && xyz_have_official_speedtest; then
  RUN_BIN="$(command -v speedtest)"
  xyz_info "$(xyz_t "Official Speedtest CLI already installed:" "已安装官方 Speedtest CLI:") $(xyz_ookla_version_line "${RUN_BIN}")"
fi

if [[ -z "${RUN_BIN}" ]]; then
  ARCH="$(xyz_ookla_arch)"
  URL="$(xyz_ookla_url "${ARCH}")"
  xyz_info "$(xyz_t "Detected architecture:" "检测到的架构:") $(uname -m) -> ${ARCH}"
  xyz_info "$(xyz_t "Official tarball:" "官方压缩包:") ${URL}"
  xyz_info "$(xyz_t \
    "This wrapper downloads Ookla's official CLI tarball and does not add any package repository." \
    "本包装脚本只下载 Ookla 官方 CLI 压缩包,不会添加任何软件源。")"

  if [[ "${XYZ_DRY_RUN:-0}" != "1" && "${DO_NO_INSTALL}" != "1" ]]; then
    xyz_require_root
  elif [[ "${DO_KEEP}" != "1" && "${XYZ_DRY_RUN:-0}" != "1" && "${DO_INSTALL_ONLY}" == "1" ]]; then
    xyz_error "$(xyz_t \
      "--install-only needs a destination. Omit --no-install (installs to ${DEST}) or add --keep." \
      "--install-only 需要安装位置。请去掉 --no-install(安装到 ${DEST}),或加上 --keep。")"
    exit 1
  fi
  xyz_require_supported_os

  if ! xyz_need_cmd tar; then
    xyz_require_root
    xyz_ensure_cmds tar
  fi

  xyz_init_tmpdir
  tarball="$(xyz_tmpdir)/ookla-speedtest-${OOKLA_CLI_VERSION}-linux-${ARCH}.tgz"
  xyz_fetch "${URL}" "${tarball}"

  expected=""
  if [[ "${OOKLA_CLI_VERSION}" == "1.2.0" ]]; then
    expected="$(xyz_ookla_tarball_sha256 "${ARCH}")"
  fi
  if [[ -n "${expected}" ]] && xyz_need_cmd sha256sum; then
    actual="$(sha256sum "${tarball}" | awk '{print $1}')"
    if [[ "${actual}" != "${expected}" ]]; then
      xyz_error "$(xyz_t "SHA256 mismatch for the official tarball." "官方压缩包的 SHA256 不匹配。")"
      xyz_error "  expected: ${expected}"
      xyz_error "  actual:   ${actual}"
      exit 1
    fi
    xyz_info "$(xyz_t "SHA256 verified." "SHA256 已校验。")"
  fi

  extract="$(xyz_tmpdir)/extract"
  mkdir -p "${extract}"
  tar -xzf "${tarball}" -C "${extract}"
  if [[ ! -x "${extract}/speedtest" && -f "${extract}/speedtest" ]]; then
    chmod +x "${extract}/speedtest"
  fi
  if [[ ! -x "${extract}/speedtest" ]]; then
    xyz_error "$(xyz_t "The tarball did not contain an executable 'speedtest' binary." "压缩包中没有可执行的 speedtest 文件。")"
    tar -tzf "${tarball}" || true
    exit 1
  fi
  xyz_info "$(xyz_t "Tarball contents:" "压缩包内容:") $(tar -tzf "${tarball}" | tr '\n' ' ')"
  xyz_info "$(xyz_t "Unpacked binary:" "解压后的二进制:") $(xyz_ookla_version_line "${extract}/speedtest")"

  if [[ "${XYZ_DRY_RUN:-0}" == "1" ]]; then
    RUN_BIN="${extract}/speedtest"
    if [[ "${DO_OVERSEAS}" != "1" ]]; then
      xyz_info "$(xyz_t \
        "Dry-run: tarball downloaded and verified; not installing to ${DEST} and not running a test." \
        "演练模式:已下载并校验压缩包,不会安装到 ${DEST},也不会运行测速。")"
      xyz_speedtest_uninstall_help
      exit 0
    fi
  fi

  if [[ "${XYZ_DRY_RUN:-0}" != "1" && "${DO_NO_INSTALL}" != "1" ]]; then
    if ! xyz_confirm "$(xyz_t \
      "Install the official Speedtest CLI to ${DEST}?" \
      "将官方 Speedtest CLI 安装到 ${DEST}?")"; then
      xyz_error "$(xyz_t "Aborted by user." "已取消。")"
      exit 1
    fi
    mkdir -p "${INSTALL_PREFIX}"
    if xyz_need_cmd install; then
      install -m 0755 "${extract}/speedtest" "${DEST}"
    else
      cp "${extract}/speedtest" "${DEST}"
      chmod 0755 "${DEST}"
    fi
    xyz_info "$(xyz_t "Installed:" "已安装:") ${DEST}"
    RUN_BIN="${DEST}"
    export PATH="${INSTALL_PREFIX}:${PATH}"
  fi

  if [[ "${XYZ_DRY_RUN:-0}" != "1" && "${DO_KEEP}" == "1" ]]; then
    keep_path="${PWD}/speedtest"
    cp "${extract}/speedtest" "${keep_path}"
    chmod 0755 "${keep_path}"
    xyz_info "$(xyz_t "Kept a copy at:" "已另存副本:") ${keep_path}"
    if [[ -z "${RUN_BIN}" ]]; then
      RUN_BIN="${keep_path}"
    fi
  fi

  if [[ -z "${RUN_BIN}" ]]; then
    # Run once from the temp dir (deleted when the script exits).
    RUN_BIN="${extract}/speedtest"
    xyz_info "$(xyz_t \
      "Running from the temporary copy for this invocation only. Re-run without --no-install, or pass --keep, to keep the binary." \
      "本次仅从临时副本运行。若要保留二进制,请去掉 --no-install,或加上 --keep。")"
  fi
fi

if [[ "${XYZ_DRY_RUN:-0}" == "1" ]]; then
  if [[ "${DO_OVERSEAS}" == "1" ]]; then
    xyz_run_overseas
    exit $?
  fi
  xyz_info "$(xyz_t "Dry-run: not running a test." "演练模式:不运行测速。")"
  exit 0
fi

if [[ -z "${RUN_BIN}" || ! -x "${RUN_BIN}" ]]; then
  xyz_error "$(xyz_t "The speedtest command is still not available." "仍然找不到 speedtest 命令。")"
  exit 1
fi

xyz_info "$(xyz_t "Installed/found:" "已找到:") $(xyz_ookla_version_line "${RUN_BIN}")"
xyz_speedtest_uninstall_help

if [[ "${DO_INSTALL_ONLY}" == "1" ]]; then
  xyz_info "$(xyz_t "Install-only requested; not running a test." "仅安装,不运行测速。")"
  exit 0
fi

xyz_info "$(xyz_t \
  "The next step runs Ookla's official CLI. It will contact Ookla servers to measure latency, download, and upload. Ookla has its own license and privacy terms:" \
  "接下来将运行 Ookla 官方 CLI。它会连接 Ookla 服务器测量延迟、下载和上传。Ookla 有自己的许可与隐私条款:")"
printf '  https://www.speedtest.net/about/privacy\n'
printf '  https://www.speedtest.net/terms\n'

# Only pass accept flags after the user has seen the notice and confirmed.
# -y / YES=1 still prints this notice first, then auto-confirms.
ACCEPT_FLAGS=()
if xyz_confirm "$(xyz_t \
  "Run 'speedtest --accept-license --accept-gdpr' now?" \
  "现在运行 speedtest --accept-license --accept-gdpr?")"; then
  ACCEPT_FLAGS+=(--accept-license --accept-gdpr)
else
  xyz_error "$(xyz_t "Aborted. You can run 'speedtest' yourself later." "已取消。你可以稍后自行运行 speedtest。")"
  exit 1
fi

if [[ "${DO_OVERSEAS}" == "1" ]]; then
  if [[ "${XYZ_DRY_RUN:-0}" == "1" && -z "${RUN_BIN:-}" ]]; then
    RUN_BIN="speedtest"
  fi
  xyz_run_overseas
  exit $?
fi

xyz_step "$(xyz_t "Running Speedtest" "正在测速")"
"${RUN_BIN}" "${ACCEPT_FLAGS[@]}"

推荐 VPS

以下为占位展示,尚未接入任何商家或联盟链接,也没有可点击的购买地址。上线推广前会再填入真实套餐与合规的返佣链接。

推荐方向:美国

占位 · 美西

商家待定

机房、配置与价格将在接入后填写。当前不是购买入口。

占位 · 高性价比

商家待定

机房、配置与价格将在接入后填写。当前不是购买入口。

占位 · 大内存 / 大硬盘

商家待定

机房、配置与价格将在接入后填写。当前不是购买入口。