#!/usr/bin/env bash set -u APP_NAME="telegram-speedtest" APP_VERSION="0.5.0" REPO_URL="https://github.com/cazi-cc/Telegram-Speedtest" RAW_URL="https://raw.githubusercontent.com/cazi-cc/Telegram-Speedtest/main/telegram-speedtest.sh" TDL_INSTALL_URL="https://docs.iyear.me/tdl/install.sh" SHORTCUT_NAME="tst" SHORTCUT_PATH="/usr/local/bin/${SHORTCUT_NAME}" CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/${APP_NAME}" CONFIG_FILE="${CONFIG_DIR}/config" SESSION_DIR="${HOME}/.tdl/${APP_NAME}" TMP_ROOT="${TMPDIR:-/tmp}/${APP_NAME}-${UID:-0}-$$" RESULT_FILE="${HOME}/${APP_NAME}-result.txt" NAMESPACE="tst" TG_URL="" PROXY="" PROFILE_NAME="推荐低资源" TEST_SECONDS=20 MULTI_THREADS=4 MULTI_POOL=4 LIMIT_MIB=128 KEEP_TDL=0 LAST_SINGLE_MIBS="" LAST_SINGLE_MBPS="" LAST_MULTI_MIBS="" LAST_MULTI_MBPS="" CURRENT_TDL_PID="" TDL_INSTALLED_BY_THIS_RUN=0 TDL_INSTALLED_PATH="" NO_COLOR="${NO_COLOR:-0}" if [ -t 1 ] && [ "$NO_COLOR" != "1" ]; then C_RESET="$(printf '\033[0m')" C_BOLD="$(printf '\033[1m')" C_DIM="$(printf '\033[2m')" C_BLUE="$(printf '\033[34m')" C_CYAN="$(printf '\033[36m')" C_GREEN="$(printf '\033[32m')" C_YELLOW="$(printf '\033[33m')" C_RED="$(printf '\033[31m')" else C_RESET=""; C_BOLD=""; C_DIM=""; C_BLUE=""; C_CYAN=""; C_GREEN=""; C_YELLOW=""; C_RED="" fi print_origin() { cat <&2 } pause() { printf "\n按 Enter 返回..." read -r _ || true } ensure_dirs() { mkdir -p "$CONFIG_DIR" "$SESSION_DIR" "$TMP_ROOT" } load_config() { [ -f "$CONFIG_FILE" ] || return 0 # shellcheck disable=SC1090 . "$CONFIG_FILE" } save_config() { ensure_dirs umask 077 cat > "$CONFIG_FILE" <= 1024 && i < 4) { b /= 1024; i++ } printf "%.2f %s", b, u[i] }' } dir_size_bytes() { local path="$1" [ -e "$path" ] || { printf "0"; return; } if du -sb "$path" >/dev/null 2>&1; then du -sb "$path" | awk '{print $1}' else du -sk "$path" | awk '{print $1 * 1024}' fi } cleanup() { save_config >/dev/null 2>&1 || true if [ -n "${CURRENT_TDL_PID:-}" ] && kill -0 "$CURRENT_TDL_PID" >/dev/null 2>&1; then kill "$CURRENT_TDL_PID" >/dev/null 2>&1 || true sleep 1 kill -9 "$CURRENT_TDL_PID" >/dev/null 2>&1 || true fi rm -rf "$TMP_ROOT" >/dev/null 2>&1 || true if [ "$TDL_INSTALLED_BY_THIS_RUN" = "1" ] && [ "$KEEP_TDL" != "1" ] && [ -n "$TDL_INSTALLED_PATH" ]; then rm -f "$TDL_INSTALLED_PATH" >/dev/null 2>&1 || true fi } trap cleanup EXIT trap 'cleanup; exit 130' INT TERM HUP install_shortcut() { local target="${1:-$SHORTCUT_PATH}" local tmp tmp="$(mktemp)" cat > "$tmp" </dev/null 2>&1; then sudo mkdir -p "$(dirname "$target")" sudo install -m 755 "$tmp" "$target" rm -f "$tmp" return 0 fi rm -f "$tmp" return 1 } run_as_root() { if [ "$(id -u)" = "0" ]; then "$@" elif command -v sudo >/dev/null 2>&1; then sudo "$@" else return 1 fi } install_packages_if_possible() { local packages="" if command -v apt-get >/dev/null 2>&1; then packages="curl ca-certificates coreutils gawk sed" run_as_root apt-get update && run_as_root apt-get install -y $packages elif command -v dnf >/dev/null 2>&1; then packages="curl ca-certificates coreutils gawk sed" run_as_root dnf install -y $packages elif command -v yum >/dev/null 2>&1; then packages="curl ca-certificates coreutils gawk sed" run_as_root yum install -y $packages elif command -v zypper >/dev/null 2>&1; then packages="curl ca-certificates coreutils gawk sed" run_as_root zypper --non-interactive install $packages elif command -v pacman >/dev/null 2>&1; then packages="curl ca-certificates coreutils gawk sed" run_as_root pacman -Sy --noconfirm $packages elif command -v apk >/dev/null 2>&1; then packages="curl ca-certificates coreutils gawk sed" run_as_root apk add --no-cache $packages elif command -v pkg >/dev/null 2>&1; then packages="curl ca_root_nss coreutils gawk gsed" run_as_root pkg install -y $packages elif command -v brew >/dev/null 2>&1; then packages="curl coreutils gawk gnu-sed" brew install $packages else return 1 fi } ensure_runtime_dependencies() { local missing="" command -v curl >/dev/null 2>&1 || missing="$missing curl" command -v awk >/dev/null 2>&1 || missing="$missing awk" command -v sed >/dev/null 2>&1 || missing="$missing sed" command -v date >/dev/null 2>&1 || missing="$missing coreutils" if [ -z "$missing" ]; then return 0 fi printf "%s缺少基础命令:%s%s\n" "$C_YELLOW" "$missing" "$C_RESET" printf "将尝试使用当前系统的软件包管理器安装。\n" install_packages_if_possible $missing || { printf "%s无法自动安装依赖。请手动安装 curl、awk、sed、date/coreutils 后重试。%s\n" "$C_RED" "$C_RESET" >&2 return 1 } } maybe_install_shortcut() { if command -v "$SHORTCUT_NAME" >/dev/null 2>&1; then return 0 fi printf "正在安装联网快捷命令:%s\n" "$SHORTCUT_NAME" if install_shortcut "$SHORTCUT_PATH"; then printf "已安装:%s\n" "$SHORTCUT_PATH" printf "后续可直接运行:%s\n\n" "$SHORTCUT_NAME" else cat <&2 exit 1 fi } install_tdl_if_needed() { ensure_runtime_dependencies || return 1 if command -v tdl >/dev/null 2>&1; then return 0 fi printf "未检测到 tdl,将临时安装 iyear/tdl。\n" printf "退出脚本时默认会删除本次临时安装的 tdl 主程序。\n\n" if [ "$(id -u)" = "0" ]; then curl -fsSL "$TDL_INSTALL_URL" | bash elif command -v sudo >/dev/null 2>&1; then curl -fsSL "$TDL_INSTALL_URL" | sudo bash else printf "当前用户不是 root,且没有 sudo,无法自动安装 tdl。\n" >&2 return 1 fi if command -v tdl >/dev/null 2>&1; then TDL_INSTALLED_BY_THIS_RUN=1 TDL_INSTALLED_PATH="$(command -v tdl)" return 0 fi printf "tdl 安装后仍不可用,请检查 PATH。\n" >&2 return 1 } tdl_base_args() { printf "%s\0" "-n" "$NAMESPACE" printf "%s\0" "--storage" "type=bolt,path=${SESSION_DIR}/data" printf "%s\0" "--disable-progress-ps" if [ -n "$PROXY" ]; then printf "%s\0" "--proxy" "$PROXY" fi } run_tdl_login() { local mode="$1" install_tdl_if_needed || return 1 ensure_dirs printf "\n登录方式:%s\n" "$mode" printf "登录数据目录:%s\n\n" "$SESSION_DIR" local args=() while IFS= read -r -d '' item; do args+=("$item"); done < <(tdl_base_args) tdl "${args[@]}" login -T "$mode" } set_video_url() { printf "\n粘贴 Telegram 频道/群组中具体资源消息链接(建议使用较大的视频或文件):\n> " read -r TG_URL || TG_URL="" save_config } set_proxy_menu() { clear_screen print_origin cat < " read -r PROXY || PROXY="" ;; 0) return ;; *) printf "无效选择。\n"; pause; return ;; esac save_config } login_menu() { clear_screen print_origin cat </dev/null && [ "$choice" -le "${#values[@]}" ]; then printf "%s" "${values[$((choice - 1))]}" else printf "%s" "$default" fi } custom_profile() { PROFILE_NAME="自定义" TEST_SECONDS="$(choose_from "每轮测速时长(秒)" "20" 10 20 30 60)" MULTI_THREADS="$(choose_from "多连接线程数" "4" 2 4 8 12)" MULTI_POOL="$MULTI_THREADS" LIMIT_MIB="$(choose_from "每轮最大下载占用(MiB)" "128" 64 128 256 512)" } confirm_ready() { if [ -z "$TG_URL" ]; then set_video_url fi [ -n "$TG_URL" ] || return 1 clear_screen print_origin cat <"$log_file" 2>&1 & CURRENT_TDL_PID=$! while kill -0 "$CURRENT_TDL_PID" >/dev/null 2>&1; do sleep 1 size="$(dir_size_bytes "$run_dir")" end="$(date +%s)" elapsed=$((end - start)) draw_progress "$label" "$elapsed" "$seconds" "$size" "$limit_bytes" if [ "$size" -ge "$limit_bytes" ] || [ "$elapsed" -ge "$seconds" ]; then kill "$CURRENT_TDL_PID" >/dev/null 2>&1 || true break fi done wait "$CURRENT_TDL_PID" >/dev/null 2>&1 || true CURRENT_TDL_PID="" printf "\n" >&2 end="$(date +%s)" elapsed=$((end - start)) [ "$elapsed" -lt 1 ] && elapsed=1 size="$(dir_size_bytes "$run_dir")" status="$(tail -n 12 "$log_file" 2>/dev/null | tr '\n' ' ' | sed 's/[[:space:]][[:space:]]*/ /g')" rm -rf "$run_dir" "$log_file" >/dev/null 2>&1 || true awk -v b="$size" -v s="$elapsed" 'BEGIN { mibs=b/s/1048576; mbps=b*8/s/1000000; printf "%.2f %.2f", mibs, mbps }' if [ "$size" -lt 1048576 ]; then printf " WARN:%s" "$status" fi } start_benchmark() { install_tdl_if_needed || { pause; return; } confirm_ready || return ensure_dirs rm -rf "$TMP_ROOT" mkdir -p "$TMP_ROOT" printf "\n开始单连接敏感性测试...\n" local single single="$(run_one_test "single" 1 1 "$TEST_SECONDS" "$LIMIT_MIB")" LAST_SINGLE_MIBS="$(printf "%s" "$single" | awk '{print $1}')" LAST_SINGLE_MBPS="$(printf "%s" "$single" | awk '{print $2}')" printf "\n开始多连接总吞吐测试...\n" local multi multi="$(run_one_test "multi" "$MULTI_THREADS" "$MULTI_POOL" "$TEST_SECONDS" "$LIMIT_MIB")" LAST_MULTI_MIBS="$(printf "%s" "$multi" | awk '{print $1}')" LAST_MULTI_MBPS="$(printf "%s" "$multi" | awk '{print $2}')" show_result pause } show_result() { clear_screen print_origin cat <= 50) { print "判断:总吞吐能跑起来,但单连接弱,容易表现为视频首开慢、拖动后缓冲久。" } else if (s < 10 && m < 20) { print "判断:VPS 到 Telegram 文件方向整体偏弱,普通 Speedtest 快也不能排除此问题。" } else if (m > s * 2.5) { print "判断:线路明显依赖并发,实际 Telegram 客户端体验可能低于多连接结果。" } else { print "判断:VPS 到 Telegram 的文件下载方向没有明显单连接瓶颈。" } }' fi } result_menu() { clear show_result cat < "$RESULT_FILE" printf "已导出。\n" pause ;; esac } status_menu() { clear_screen print_origin local tmp_size session_size config_size tdl_path tmp_size="$(dir_size_bytes "$TMP_ROOT")" session_size="$(dir_size_bytes "$SESSION_DIR")" config_size="$(dir_size_bytes "$CONFIG_DIR")" tdl_path="$(command -v tdl 2>/dev/null || true)" cat </dev/null || printf "当前系统没有 free 命令\n") 磁盘: $(df -h "$HOME" "${TMPDIR:-/tmp}" 2>/dev/null || true) 脚本临时目录:$TMP_ROOT ($(human_size "$tmp_size")) 配置目录:$CONFIG_DIR ($(human_size "$config_size")) Telegram 登录数据:$SESSION_DIR ($(human_size "$session_size")) tdl:${tdl_path:-未安装} 退出时删除本次临时安装的 tdl:$([ "$KEEP_TDL" = "1" ] && printf "否" || printf "是") 资源链接:${TG_URL:-未设置} 连接方式:$(redact_proxy "$PROXY") 测试方案:$PROFILE_NAME EOF pause } clean_menu() { clear_screen print_origin cat <