#!/usr/bin/env bash
set -Eeuo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DATA_ROOT="${DATA_ROOT:-/root/data}"
LEHOME_REPO_URL="${LEHOME_REPO_URL:-https://github.com/lehome-official/lehome-challenge.git}"
LEHOME_DEFAULT_DIRNAME="${LEHOME_DEFAULT_DIRNAME:-lehome-challenge}"
ROOT_DIR="$SCRIPT_DIR"
if [[ ! -f "$ROOT_DIR/pyproject.toml" ]]; then
  for candidate in \
    "$SCRIPT_DIR/$LEHOME_DEFAULT_DIRNAME" \
    "$DATA_ROOT/$LEHOME_DEFAULT_DIRNAME" \
    "$(cd "$SCRIPT_DIR/.." && pwd)/$LEHOME_DEFAULT_DIRNAME" \
    "$(cd "$SCRIPT_DIR/.." && pwd)"; do
    [[ -f "$candidate/pyproject.toml" ]] || continue
    ROOT_DIR="$candidate"
    break
  done
fi
LOG_DIR="$ROOT_DIR/logs"
mkdir -p "$LOG_DIR"

# ── env.sh 框架变量 ──────────────────────────────────────────────────────────
MODE="confirm"
ASSUME_YES=0
DRY_RUN=0
WRITE_BASHRC=0
LIST_ONLY=0
HAS_GUM=0
MODE_WAS_SET=0
GUIDED_FLOW_ACTIVE=0
GUIDED_SHORTCUTS_ALL=0
GUIDED_ALLOW_AUTO_PATCH=0
LOG_FILE=""  # 延迟初始化，在 generate_log_path 函数定义后设置
SUCCESS_COUNT=0
SKIPPED_COUNT=0
FAILED_COUNT=0
REQUESTED_STEP_IDS=()
STEP_IDS=()
STEP_TITLES=()
STEP_TYPES=()
STEP_RISKS=()
STEP_DEFAULTS=()
STEP_HANDLERS=()
STEP_COMMANDS=()
STEP_DESCS=()

export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"

# ── 颜色定义 (v1/common.sh) ──────────────────────────────────────────────────
BLUE='\033[0;34m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
GRAY='\033[90m'
WHITE='\033[97m'
NC='\033[0m'

# ── 日志 ─────────────────────────────────────────────────────────────────────
log_line() {
  # 延迟初始化 LOG_FILE
  if [[ -z "$LOG_FILE" ]] && declare -f generate_log_path >/dev/null 2>&1; then
    LOG_FILE="$(generate_log_path "runner" "")"
  fi
  [[ -n "$LOG_FILE" ]] && printf '[%s] %s\n' "$(date '+%F %T')" "$*" >>"$LOG_FILE"
}
log()  { echo -e "${BLUE}$*${NC}"; }
ok()   { echo -e "${GREEN}$*${NC}"; }
warn() { echo -e "${YELLOW}$*${NC}"; }
die()  { echo -e "${RED}$*${NC}" >&2; exit 1; }
rule() { printf '%b%s%b\n' "$BLUE" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" "$NC"; }
section() { echo; rule; log "▶ $*"; rule; }
kv() { local key="$1"; shift; printf '%b%-18s%b %s\n' "$BLUE" "${key}:" "$NC" "$*"; }
cmd_preview() { log "💻 $*"; }

# ── style_* (env.sh 兼容) ────────────────────────────────────────────────────
style_dim()  { [[ "$HAS_GUM" -eq 1 ]] && gum style --foreground 245 "$*" || echo "$*"; }
style_ok()   { [[ "$HAS_GUM" -eq 1 ]] && gum style --foreground 46  "$*" || echo "$*"; }
style_warn() { [[ "$HAS_GUM" -eq 1 ]] && gum style --foreground 214 "$*" || echo "$*"; }
style_err()  { [[ "$HAS_GUM" -eq 1 ]] && gum style --foreground 196 "$*" || echo "$*" >&2; }

sync_log_dir() {
  LOG_DIR="$ROOT_DIR/logs"
  mkdir -p "$LOG_DIR"
}

prepend_path_once() {
  local var_name="$1"
  local path_value="$2"
  local current_value="${!var_name-}"
  [[ -n "$path_value" ]] || return 0
  case ":$current_value:" in
    *":$path_value:"*) return 0 ;;
  esac
  if [[ -n "$current_value" ]]; then
    printf -v "$var_name" '%s' "$path_value:$current_value"
  else
    printf -v "$var_name" '%s' "$path_value"
  fi
  export "$var_name"
}

# ── UI 函数 (env.sh) ─────────────────────────────────────────────────────────
ui_confirm() {
  local prompt="$1" default_yes="${2:-1}"
  [[ "$ASSUME_YES" -eq 1 ]] && return 0
  if [[ "$HAS_GUM" -eq 1 ]]; then
    if [[ "$default_yes" -eq 1 ]]; then
      gum confirm --affirmative "执行" --negative "跳过" --default=true "$prompt"
    else
      gum confirm --affirmative "确认" --negative "取消" --default=false "$prompt"
    fi
    return $?
  fi
  local ans
  if [[ "$default_yes" -eq 1 ]]; then
    read -r -p "$prompt [Y/n]: " ans; ans="${ans:-Y}"
  else
    read -r -p "$prompt [y/N]: " ans; ans="${ans:-N}"
  fi
  case "${ans,,}" in y|yes) return 0 ;; *) return 1 ;; esac
}

ui_confirm_yolo_mode() {
  [[ "$ASSUME_YES" -eq 1 ]] && return 0
  if [[ "$HAS_GUM" -eq 1 ]]; then
    gum confirm --affirmative "进入 YOLO" --negative "普通模式" --default=false "是否进入 YOLO 模式？"
    return $?
  fi
  local ans
  read -r -p "是否进入 YOLO 模式？[y/N]: " ans
  ans="${ans:-N}"
  case "${ans,,}" in
    y|yes) return 0 ;;
    *) return 1 ;;
  esac
}

ui_choose_one() {
  local header="$1"; shift; local options=("$@")
  if [[ "$HAS_GUM" -eq 1 ]]; then
    printf '%s\n' "${options[@]}" | gum choose --header "$header" || true; return
  fi
  echo "$header"; local i=1
  for opt in "${options[@]}"; do echo "  $i) $opt"; i=$((i+1)); done
  local idx; read -r -p "请输入序号: " idx
  if [[ "$idx" =~ ^[0-9]+$ ]] && ((idx >= 1 && idx <= ${#options[@]})); then
    echo "${options[$((idx-1))]}"; fi
}

ui_choose_many_lines() {
  local header="$1"; shift; local options=("$@")
  order_selected_lines() {
    local selected_raw="$1"
    shift
    local ordered_options=("$@")
    [[ -n "$selected_raw" ]] || return 0
    local selected=() line opt picked
    while IFS= read -r line; do
      [[ -n "$line" ]] && selected+=("$line")
    done <<<"$selected_raw"
    for opt in "${ordered_options[@]}"; do
      for picked in "${selected[@]}"; do
        [[ "$picked" == "$opt" ]] || continue
        printf '%s\n' "$opt"
        break
      done
    done
  }
  if [[ "$HAS_GUM" -eq 1 ]]; then
    local picked_lines
    picked_lines="$(printf '%s\n' "${options[@]}" | gum choose --no-limit --ordered --header "$header\n空格选中，回车确认" || true)"
    order_selected_lines "$picked_lines" "${options[@]}"
    return
  fi
  echo "$header"; local i=1
  for opt in "${options[@]}"; do echo "  $i) $opt"; i=$((i+1)); done
  read -r -p "输入序号（逗号分隔，如 1,3,5）: " raw
  IFS=',' read -r -a picks <<<"$raw"
  local valid_picks=()
  for p in "${picks[@]}"; do
    p="${p//[[:space:]]/}"
    if [[ "$p" =~ ^[0-9]+$ ]] && ((p >= 1 && p <= ${#options[@]})); then
      valid_picks+=("$p")
    fi
  done
  [[ ${#valid_picks[@]} -eq 0 ]] && return 0
  while IFS= read -r idx; do
    printf '%s\n' "${options[$((idx-1))]}"
  done < <(printf '%s\n' "${valid_picks[@]}" | sort -n -u)
}

format_key_desc_option() {
  local key="$1" desc="$2" width="${3:-11}"
  printf "%-${width}s | %s" "$key" "$desc"
}

format_step_select_option() {
  local step_id="$1" type="$2" risk="$3" title="$4"
  printf '%-24s | %-12s | %-7s | %s' "$step_id" "$type" "$risk" "$title"
}

# ── 工具函数 ─────────────────────────────────────────────────────────────────
badge_for_type() { case "$1" in official) printf '官方';; enhanced) printf '增强(非官方)';; *) printf '其他';; esac; }
risk_badge()     { case "$1" in low) printf '低';; medium) printf '中';; high) printf '高';; *) printf '未知';; esac; }

ensure_project_root() {
  bootstrap_project_root
  [[ -f "$ROOT_DIR/pyproject.toml" ]] || { style_err "未找到项目根目录: $ROOT_DIR"; exit 1; }
  cd "$ROOT_DIR"
}
ensure_repo_root() { ensure_project_root; }
ensure_path() { export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"; }

bootstrap_project_root() {
  [[ -f "$ROOT_DIR/pyproject.toml" ]] && return 0

  mkdir -p "$DATA_ROOT"
  local base_dir="$(cd "$SCRIPT_DIR/.." && pwd)"
  local candidates=(
    "$PWD"
    "$DATA_ROOT/$LEHOME_DEFAULT_DIRNAME"
    "$base_dir"
    "$base_dir/$LEHOME_DEFAULT_DIRNAME"
  )
  local candidate
  for candidate in "${candidates[@]}"; do
    [[ -f "$candidate/pyproject.toml" ]] || continue
    ROOT_DIR="$candidate"
    sync_log_dir
    LOG_FILE="$(generate_log_path "runner" "")"
    return 0
  done

  require_cmd git
  local clone_dir="$DATA_ROOT/$LEHOME_DEFAULT_DIRNAME"
  section "初始化 LeHome 项目"
  if [[ -e "$clone_dir" && ! -d "$clone_dir/.git" ]]; then
    die "❌ 自动初始化失败：$clone_dir 已存在但不是 Git 仓库，请先手动处理。"
  fi
  if [[ ! -d "$clone_dir/.git" ]]; then
    cmd_preview "git clone $LEHOME_REPO_URL $clone_dir"
    git clone "$LEHOME_REPO_URL" "$clone_dir"
  else
    style_dim "检测到现有仓库，跳过 clone：$clone_dir"
  fi

  [[ -f "$clone_dir/pyproject.toml" ]] || die "❌ 自动拉取后仍未找到 pyproject.toml：$clone_dir"
  ROOT_DIR="$clone_dir"
  sync_log_dir
  LOG_FILE="$(generate_log_path "runner" "")"
  ok "✅ LeHome 项目已就绪：$ROOT_DIR"
}

activate_venv() {
  [[ -f "$ROOT_DIR/.venv/bin/activate" ]] || { style_err "未找到虚拟环境: $ROOT_DIR/.venv/bin/activate"; return 1; }
  export PS1="${PS1-}"; set +u
  # shellcheck disable=SC1091
  source "$ROOT_DIR/.venv/bin/activate"

  local isaaclab_source="$ROOT_DIR/third_party/IsaacLab/source/isaaclab"
  local pth_file="$ROOT_DIR/.venv/lib/python3.11/site-packages/isaaclab.pth"
  local isaacsim_kit="$ROOT_DIR/.venv/lib/python3.11/site-packages/isaacsim/kit"
  if [[ -d "$isaaclab_source" ]]; then
    local desired_pth="$isaaclab_source"
    local current_pth=""
    if [[ -f "$pth_file" ]]; then
      current_pth="$(head -n 1 "$pth_file" | tr -d '\r')"
    fi
    if [[ "$current_pth" != "$desired_pth" ]]; then
      printf '%s\n' "$desired_pth" > "$pth_file"
    fi
  fi
  if [[ -d "$isaacsim_kit" ]]; then
    prepend_path_once LD_LIBRARY_PATH "$isaacsim_kit"
  fi

  set -u
}

generate_log_path() {
  local log_type="$1"
  local feature="$2"
  local date_dir time_prefix log_dir log_file

  date_dir="$(date +%m-%d)"
  time_prefix="$(date +%H:%M)"
  log_dir="$LOG_DIR/$log_type/$date_dir"

  mkdir -p "$log_dir"

  if [[ -n "$feature" ]]; then
    log_file="$log_dir/${time_prefix}-${log_type}-${feature}.log"
  else
    log_file="$log_dir/${time_prefix}-${log_type}.log"
  fi

  echo "$log_file"
}

# 初始化 all.sh 脚本运行日志
[[ -z "$LOG_FILE" ]] && LOG_FILE="$(generate_log_path "runner" "")"

require_cmd() {
  local cmd="$1"
  command -v "$cmd" >/dev/null 2>&1 || { style_err "缺少命令: $cmd"; return 1; }
}

backup_file() {
  local path="$1"; [[ -e "$path" ]] || return 0
  cp "$path" "$path.bak.$(date +%Y%m%d_%H%M%S)"
}

safe_symlink() {
  local target="$1" link_path="$2"
  mkdir -p "$(dirname -- "$link_path")"
  if [[ -L "$link_path" ]]; then
    local current; current="$(readlink -f "$link_path" || true)"
    if [[ "$current" == "$(readlink -f "$target")" ]]; then
      ok "✅ 软链接已存在: $link_path -> $target"; return 0; fi
    rm -f "$link_path"
  elif [[ -e "$link_path" ]]; then
    mv "$link_path" "$link_path.bak.$(date +%Y%m%d_%H%M%S)"; fi
  ln -s "$target" "$link_path"
  ok "✅ 已建立软链接: $link_path -> $target"
}

ensure_uv() {
  ensure_path
  if ! command -v uv >/dev/null 2>&1; then
    section "安装 uv"; cmd_preview 'curl -LsSf https://astral.sh/uv/install.sh | sh'
    curl -LsSf https://astral.sh/uv/install.sh | sh; ensure_path; fi
  ok "✅ uv 已就绪: $(command -v uv)"
}

link_uv_cache() {
  local target="$DATA_ROOT/.uv_cache" link_path="$HOME/.cache/uv"
  mkdir -p "$target" "$HOME/.cache"
  if [[ -L "$link_path" ]]; then
    local current; current="$(readlink -f "$link_path")"
    if [[ "$current" == "$target" ]]; then ok "✅ UV 缓存软链接已存在"; return 0; fi
    rm -f "$link_path"
  elif [[ -e "$link_path" ]]; then
    die "❌ $link_path 已存在且不是软链接，请先手动处理。"; fi
  ln -s "$target" "$link_path"; ok "✅ 已建立缓存软链接: $link_path -> $target"
}

install_system_libs() {
  local priv_cmd=(); [[ "$(id -u)" -eq 0 ]] || priv_cmd=(sudo)
  section "安装系统图形库"
  "${priv_cmd[@]}" apt update
  "${priv_cmd[@]}" apt install -y libglu1-mesa libgl1 libegl1 libxrandr2 libxinerama1 libxcursor1 libxi6 libxext6 libx11-6
  export __GLX_VENDOR_LIBRARY_NAME=nvidia; ok "✅ 系统库安装完成"
}

clone_isaaclab_if_missing() {
  mkdir -p "$ROOT_DIR/third_party"
  if [[ ! -d "$ROOT_DIR/third_party/IsaacLab" ]]; then
    section "克隆 IsaacLab"
    cmd_preview 'git clone https://github.com/lehome-official/IsaacLab.git third_party/IsaacLab'
    git clone https://github.com/lehome-official/IsaacLab.git "$ROOT_DIR/third_party/IsaacLab"; fi
  ok "✅ IsaacLab 目录已就绪"
}

resolve_isaacsim_package_path() {
  python - <<'PY'
import contextlib
import io
import os
import sys

buf = io.StringIO()
with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf):
    import isaacsim

candidates = []
isaac_path = os.environ.get("ISAAC_PATH")
if isaac_path:
    candidates.append(isaac_path)

module_file = getattr(isaacsim, "__file__", "")
if module_file:
    module_dir = os.path.dirname(os.path.abspath(module_file))
    candidates.append(module_dir)
    candidates.append(os.path.abspath(os.path.join(module_dir, "..", "..")))

seen = set()
for candidate in candidates:
    if not candidate:
        continue
    candidate = os.path.abspath(candidate.strip())
    if candidate in seen:
        continue
    seen.add(candidate)
    if os.path.isdir(candidate):
        print(candidate)
        sys.exit(0)

sys.exit(1)
PY
}

ensure_isaacsim_symlink_ready() {
  local isaaclab_dir="$ROOT_DIR/third_party/IsaacLab"
  local link_path="$isaaclab_dir/_isaac_sim"
  [[ -d "$isaaclab_dir" ]] || { style_err "未找到 IsaacLab 目录: $isaaclab_dir"; return 1; }

  # 检查软链接是否已存在且有效
  if [[ -L "$link_path" ]]; then
    local raw_target
    raw_target="$(readlink "$link_path" || true)"
    case "$raw_target" in
      *$'\n'*|*"Do you accept the EULA"*|*"By installing or using Omniverse Kit"*)
        local backup_path="$link_path.bak.$(date +%Y%m%d_%H%M%S)"
        mv "$link_path" "$backup_path"
        warn "⚠️ 检测到异常 _isaac_sim 软链接，已备份到: $backup_path"
        ;;
      *)
        if [[ -d "$link_path" ]]; then
          ok "✅ IsaacSim 软链接已存在且有效: $link_path -> $raw_target"
          return 0
        fi
        ;;
    esac
  fi

  # 解析 IsaacSim 路径（此时 EULA 应该已经被接受）
  local isaacsim_path
  if ! isaacsim_path="$(resolve_isaacsim_package_path)"; then
    style_err "无法解析 IsaacSim 安装路径；已停止创建 _isaac_sim 软链接。"
    return 1
  fi
  isaacsim_path="$(printf '%s' "$isaacsim_path" | tail -n 1 | tr -d '\r')"
  case "$isaacsim_path" in
    *$'\n'*|*"Do you accept the EULA"*|*"By installing or using Omniverse Kit"*|"")
      style_err "解析到的 IsaacSim 路径异常: $isaacsim_path"
      style_err "提示：请确保已执行 isaaclab.sh -i none 并接受 EULA"
      return 1
      ;;
  esac
  if [[ "${isaacsim_path#/}" == "$isaacsim_path" || ! -d "$isaacsim_path" ]]; then
    style_err "IsaacSim 路径无效或目录不存在: $isaacsim_path"
    return 1
  fi

  style_dim "检测到 IsaacSim 包路径: $isaacsim_path"
  safe_symlink "$isaacsim_path" "$link_path"
}

ensure_isaaclab_deps() {
  local python_bin="$ROOT_DIR/.venv/bin/python"
  local pth_file="$ROOT_DIR/.venv/lib/python3.11/site-packages/isaaclab.pth"
  local isaaclab_source="$ROOT_DIR/third_party/IsaacLab/source/isaaclab"

  # 1. 确保 isaaclab.pth 存在
  if [[ ! -f "$pth_file" ]]; then
    style_dim "创建 isaaclab.pth 文件..."
    echo "$isaaclab_source" > "$pth_file"
    ok "✅ 已创建 isaaclab.pth"
  fi

  # 2. 检查并安装缺失的依赖
  local missing_deps=()
  if ! "$python_bin" -c "import warp" 2>/dev/null; then
    missing_deps+=("warp-lang")
  fi
  if ! "$python_bin" -c "import pxr" 2>/dev/null; then
    missing_deps+=("usd-core")
  fi
  if ! "$python_bin" -c "import prettytable" 2>/dev/null; then
    missing_deps+=("prettytable")
  fi

  if [[ ${#missing_deps[@]} -gt 0 ]]; then
    style_dim "安装缺失的依赖: ${missing_deps[*]}"
    uv pip install "${missing_deps[@]}"
    ok "✅ 已安装依赖: ${missing_deps[*]}"
  fi
}

check_imports() {
  python - "$@" <<'PY'
import importlib, sys
failed = {}
for name in sys.argv[1:]:
    try: importlib.import_module(name)
    except Exception as exc: failed[name] = f"{type(exc).__name__}: {exc}"
if failed:
    for k, v in failed.items(): print(f"{k}: {v}")
    raise SystemExit(1)
print("All imports passed:", ", ".join(sys.argv[1:]))
PY
}

local_script_path()  { printf '%s/%s\n' "$SCRIPT_DIR" "$1"; }
repo_script_path()   { printf '%s/%s\n' "$ROOT_DIR"   "$1"; }
invoke_local_script() { local s="$1"; shift; bash "$(local_script_path "$s")" "$@"; }
exec_local_script()   { local s="$1"; shift; exec bash "$(local_script_path "$s")" "$@"; }
invoke_repo_script()  { local s="$1"; shift; ensure_repo_root; bash "$(repo_script_path "$s")" "$@"; }
exec_repo_script()    { local s="$1"; shift; ensure_repo_root; exec bash "$(repo_script_path "$s")" "$@"; }

# ── Step 注册框架 (env.sh) ───────────────────────────────────────────────────
add_step() {
  STEP_IDS+=("$1"); STEP_TITLES+=("$2"); STEP_TYPES+=("$3"); STEP_RISKS+=("$4")
  STEP_DEFAULTS+=("$5"); STEP_HANDLERS+=("$6"); STEP_COMMANDS+=("$7"); STEP_DESCS+=("$8")
}

find_step_idx_by_id() {
  local target="$1" idx
  for idx in "${!STEP_IDS[@]}"; do
    [[ "${STEP_IDS[$idx]}" == "$target" ]] && { echo "$idx"; return 0; }; done
  return 1
}

show_banner() {
  cat <<'EOF'
╭──────────────────────────────────────────╮
│          LeHome Setup Assistant          │
│               LeHome CLI                 │
╰──────────────────────────────────────────╯

请选择操作：

  1. 一路配置向导
    先勾选需要的模块，再从头到尾自动执行

  2. VPN 与代理配置
    部署 Clash for Linux、写入订阅配置并输出代理端口信息


[ Environment Setup ]

  3. 环境准备
    git clone 软链修复、安装 uv、同步依赖、安装 IsaacLab / LeHome、执行导入校验

  4. 数据资源管理
    下载 Assets 与示例数据集，可选完整 dataset_challenge


[ Developer Tools ]

  5. AI 开发工具链
    安装 Codex / Claude Code / ZCF / Happy Coder

  6. github配置与Shell 快捷命令
    写入 go / train / eval / save / diff 命令并补充 PATH

[ Training Workspace ]

  7. X-VLA 工作台
    初始化训练环境、生成训练配置并支持训练 / 评估


  8. 退出

──────────────────────────────────────────
使用 ↑ ↓ 选择，Enter 确认
或输入编号直接进入对应模块
EOF
}

show_compact_main_menu_header() {
  cat <<'EOF'
╭──────────────────────────────────────────╮
│          LeHome Setup Assistant          │
│               LeHome CLI                 │
╰──────────────────────────────────────────╯

Guided Setup         : 1 一路配置向导
Network              : 2 VPN 与代理配置
Environment Setup    : 3 环境准备 / 4 数据资源管理
Developer Tools      : 5 AI 开发工具链 / 6 github配置与Shell 快捷命令
Training Workspace   : 7 X-VLA 工作台

使用 ↑ ↓ 选择，Enter 确认
EOF
}

show_steps_table() {
  local idx
  for idx in "${!STEP_IDS[@]}"; do
    local num=$((idx+1)) type risk default_mark=""
    type="$(badge_for_type "${STEP_TYPES[$idx]}")"; risk="$(risk_badge "${STEP_RISKS[$idx]}")"
    [[ "${STEP_DEFAULTS[$idx]}" == "1" ]] && default_mark="默认"
    echo "$num. ${STEP_IDS[$idx]} [$type][风险:$risk] $default_mark"
    echo "   ${STEP_TITLES[$idx]}"
    echo "   命令: ${STEP_COMMANDS[$idx]}"
    echo "   说明: ${STEP_DESCS[$idx]}"; done
}
show_step_list_only() { show_steps_table; }
is_high_risk() { [[ "$1" == "high" ]]; }

run_step_handler() {
  local handler="$1"
  declare -f "$handler" >/dev/null 2>&1 || { style_err "内部错误：未找到步骤函数 $handler"; return 1; }
  "$handler"
}

record_success() { SUCCESS_COUNT=$((SUCCESS_COUNT+1)); }
record_skip()    { SKIPPED_COUNT=$((SKIPPED_COUNT+1)); }
record_failure() { FAILED_COUNT=$((FAILED_COUNT+1)); }

choose_failure_action() {
  if [[ "$HAS_GUM" -eq 1 ]]; then
    printf '重试\n跳过\n终止' | gum choose --header "步骤失败，选择后续操作" || echo "终止"; return; fi
  echo "步骤失败，选择后续操作:"; echo "1) 重试"; echo "2) 跳过"; echo "3) 终止"
  local c; read -r -p "请输入序号: " c
  case "$c" in 1) echo "重试";; 2) echo "跳过";; *) echo "终止";; esac
}

run_step_by_idx() {
  local idx="$1" id title type risk handler cmd
  id="${STEP_IDS[$idx]}"; title="${STEP_TITLES[$idx]}"; type="${STEP_TYPES[$idx]}"
  risk="${STEP_RISKS[$idx]}"; handler="${STEP_HANDLERS[$idx]}"; cmd="${STEP_COMMANDS[$idx]}"
  echo; style_dim "Step $((idx+1)) | $id | $(badge_for_type "$type") | 风险: $(risk_badge "$risk")"
  style_dim "标题: $title"; style_dim "命令: $cmd"
  log_line "准备执行 step=$id type=$type risk=$risk"
  case "$id" in
    train_xvla|eval_xvla)
      ensure_train_xvla_yaml || return 1
      ;;
  esac
  if [[ "$DRY_RUN" -eq 1 ]]; then style_dim "DRY RUN: 未执行"; record_skip; log_line "DRY_RUN 跳过 step=$id"; return 0; fi
  if [[ "$MODE" == "confirm" ]]; then
    if ! ui_confirm "执行步骤 $id 吗？" 1; then
      style_warn "已跳过 $id"; record_skip; log_line "用户跳过 step=$id"; return 0; fi; fi
  if is_high_risk "$risk"; then
    if ! ui_confirm "高风险步骤：$id。确认继续？" 0; then
      style_warn "已跳过高风险步骤 $id"; record_skip; log_line "用户取消高风险 step=$id"; return 0; fi; fi
  while true; do
    set +e; run_step_handler "$handler"; local status=$?; set -e
    if [[ $status -eq 0 ]]; then style_ok "完成: $id"; record_success; log_line "完成 step=$id"; return 0; fi
    style_err "失败: $id (exit=$status)"; log_line "失败 step=$id exit=$status"
    if [[ "$MODE" == "auto" || "$ASSUME_YES" -eq 1 ]]; then record_failure; return "$status"; fi
    local action; action="$(choose_failure_action)"
    case "$action" in 重试) ;; 跳过) record_skip; log_line "失败后跳过 step=$id"; return 0;;
      终止|*) record_failure; return "$status";; esac; done
}

run_default_flow() {
  local idx
  for idx in "${!STEP_IDS[@]}"; do
    [[ "${STEP_DEFAULTS[$idx]}" != "1" ]] && continue
    run_step_by_idx "$idx" || return 1; done
}

run_steps_by_ids() {
  local sid idx
  for sid in "$@"; do
    idx="$(find_step_idx_by_id "$sid")" || { style_err "未知步骤 ID: $sid"; return 1; }
    run_step_by_idx "$idx" || return 1; done
}

run_multi_select() {
  local options=() idx
  for idx in "${!STEP_IDS[@]}"; do
    options+=("$(format_step_select_option \
      "${STEP_IDS[$idx]}" \
      "$(badge_for_type "${STEP_TYPES[$idx]}")" \
      "风险:$(risk_badge "${STEP_RISKS[$idx]}")" \
      "${STEP_TITLES[$idx]}")")
  done
  local picked_lines; picked_lines="$(ui_choose_many_lines "选择要执行的步骤" "${options[@]}")"
  [[ -z "$picked_lines" ]] && { style_warn "未选择任何步骤"; return 0; }
  local selected_ids=() line
  while IFS= read -r line; do
    [[ -z "$line" ]] && continue
    local selected_id="${line%% | *}"
    selected_id="${selected_id//[[:space:]]/}"
    selected_ids+=("$selected_id")
  done <<<"$picked_lines"
  [[ ${#selected_ids[@]} -eq 0 ]] && { style_warn "未解析到有效步骤"; return 0; }
  run_steps_by_ids "${selected_ids[@]}"
}

set_mode_interactive() {
  local choice; choice="$(ui_choose_one "选择执行模式" "逐步确认（每步询问）" "自动执行（不再询问）")"
  case "$choice" in "自动执行（不再询问）") MODE="auto"; ASSUME_YES=1;; *) MODE="confirm"; ASSUME_YES=0;; esac
  style_dim "当前执行模式: $MODE"; log_line "执行模式切换为 $MODE"
}

prompt_startup_mode() {
  if ui_confirm_yolo_mode; then
    MODE="auto"
    ASSUME_YES=1
    style_dim "当前模式: YOLO（自动执行）"
    log_line "启动模式: yolo-auto"
  else
    MODE="confirm"
    ASSUME_YES=0
    style_dim "当前模式: 普通（逐步确认）"
    log_line "启动模式: confirm"
  fi
}

show_gum_install_hint() {
  [[ -t 1 ]] || return 0
  local is_root=0
  [[ "$(id -u)" -eq 0 ]] && is_root=1
  echo
  printf '%b%s%b\n' "$YELLOW" "╭──────────────────────────────────────────────╮" "$NC"
  printf '%b%s%b\n' "$YELLOW" "│ 提示：未检测到 gum，当前将使用基础终端交互界面 │" "$NC"
  printf '%b%s%b\n' "$YELLOW" "│ 安装 gum 后，勾选、多选和确认界面会更直观更好用 │" "$NC"
  printf '%b%s%b\n' "$YELLOW" "╰──────────────────────────────────────────────╯" "$NC"
  if [[ "$is_root" -eq 1 ]]; then
    style_dim "当前检测到 root 用户，推荐安装命令:"
    style_dim "  mkdir -p /etc/apt/keyrings"
    style_dim "  curl -fsSL https://repo.charm.sh/apt/gpg.key | gpg --dearmor -o /etc/apt/keyrings/charm.gpg"
    style_dim "  echo \"deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *\" | tee /etc/apt/sources.list.d/charm.list"
    style_dim "  apt update && apt install -y gum"
  else
    style_dim "当前检测到普通用户，推荐安装命令:"
    style_dim "  sudo mkdir -p /etc/apt/keyrings"
    style_dim "  curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg"
    style_dim "  echo \"deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *\" | sudo tee /etc/apt/sources.list.d/charm.list"
    style_dim "  sudo apt update && sudo apt install -y gum"
  fi
}

ui_choose_numbered_option() {
  local header="$1"
  shift
  local options=("$@")
  if [[ "$HAS_GUM" -eq 1 ]]; then
    local picked
    picked="$(printf '%s\n' "${options[@]}" | gum choose --header "$header" || true)"
    [[ -n "$picked" ]] && printf '%s\n' "${picked%%.*}"
    return
  fi
  [[ -n "$header" ]] && echo "$header"
  local raw
  read -r -p "请输入编号: " raw
  if [[ "$raw" =~ ^([0-9]+) ]]; then
    printf '%s\n' "${BASH_REMATCH[1]}"
  fi
}

wizard_section() {
  echo
  echo "================================================"
  echo "  $1"
  echo "================================================"
}

wizard_prompt_value() {
  local label="$1" default_display="$2" default_value="$3" result_var="$4"
  local input value
  printf "  %-16s ${GRAY}[%s]${NC} " "${label}:" "$default_display"
  read -r input
  value="${input:-$default_value}"
  if [[ -t 1 ]]; then
    printf '\033[1A\r\033[2K  %-16s %b%s%b\n' "${label}:" "${WHITE}" "$value" "${NC}"
  else
    printf "  %-16s ${WHITE}%s${NC}\n" "${label}:" "$value"
  fi
  printf -v "$result_var" '%s' "$value"
}

wizard_choose_option() {
  local label="$1" result_var="$2"
  shift 2
  local options=("$@")

  if [[ "$HAS_GUM" -eq 1 ]]; then
    local picked
    picked="$(printf '%s\n' "${options[@]}" | gum choose --header "  ${label}:" || true)"
    if [[ -n "$picked" ]]; then
      printf '  %-16s %b%s%b\n' "${label}:" "${WHITE}" "$picked" "${NC}"
      printf -v "$result_var" '%s' "$picked"
      return 0
    fi
  fi

  echo "  ${label}:"
  local i=1
  for opt in "${options[@]}"; do
    echo "    $i) $opt"
    i=$((i+1))
  done

  local choice
  read -r -p "  请输入编号: " choice
  if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le "${#options[@]}" ]]; then
    local selected="${options[$((choice-1))]}"
    printf '  %-16s %b%s%b\n' "${label}:" "${WHITE}" "$selected" "${NC}"
    printf -v "$result_var" '%s' "$selected"
    return 0
  fi

  echo "  ${RED}无效选择${NC}"
  return 1
}

wizard_prompt_with_options() {
  local label="$1" default_value="$2" result_var="$3"
  shift 3
  local options=("$@")

  if [[ "$HAS_GUM" -eq 1 ]] && [[ ${#options[@]} -gt 0 ]]; then
    local all_options=("${options[@]}" "自定义输入...")
    local picked
    picked="$(printf '%s\n' "${all_options[@]}" | gum choose --header "  ${label}: (默认: ${default_value})" || true)"

    if [[ -n "$picked" ]]; then
      if [[ "$picked" == "自定义输入..." ]]; then
        local input
        printf "  %-16s ${GRAY}[%s]${NC} " "${label}:" "$default_value"
        read -r input
        local value="${input:-$default_value}"
        printf '\033[1A\r\033[2K  %-16s %b%s%b\n' "${label}:" "${WHITE}" "$value" "${NC}"
        printf -v "$result_var" '%s' "$value"
      else
        printf '  %-16s %b%s%b\n' "${label}:" "${WHITE}" "$picked" "${NC}"
        printf -v "$result_var" '%s' "$picked"
      fi
      return 0
    fi
  fi

  if [[ ${#options[@]} -gt 0 ]]; then
    echo "  ${label}: (可选值: ${options[*]})"
  fi

  local input value
  printf "  %-16s ${GRAY}[%s]${NC} " "${label}:" "$default_value"
  read -r input
  value="${input:-$default_value}"
  if [[ -t 1 ]]; then
    printf '\033[1A\r\033[2K  %-16s %b%s%b\n' "${label}:" "${WHITE}" "$value" "${NC}"
  else
    printf "  %-16s ${WHITE}%s${NC}\n" "${label}:" "$value"
  fi
  printf -v "$result_var" '%s' "$value"
}

wizard_prompt_optional() {
  local label="$1" placeholder="$2" result_var="$3"
  local input
  printf "  %-16s ${GRAY}[%s]${NC} " "${label}:" "$placeholder"
  read -r input
  if [[ -t 1 ]]; then
    if [[ -n "$input" ]]; then
      printf '\033[1A\r\033[2K  %-16s %b%s%b\n' "${label}:" "${WHITE}" "$input" "${NC}"
    else
      printf '\033[1A\r\033[2K  %-16s %b<empty>%b\n' "${label}:" "${GRAY}" "${NC}"
    fi
  else
    if [[ -n "$input" ]]; then
      printf "  %-16s ${WHITE}%s${NC}\n" "${label}:" "$input"
    else
      printf "  %-16s ${GRAY}<empty>${NC}\n" "${label}:"
    fi
  fi
  printf -v "$result_var" '%s' "$input"
}

write_default_train_xvla_yaml() {
  local path="$1"
  mkdir -p "$(dirname "$path")"
  cat >"$path" <<'EOF'
# X-VLA Final Valid Config for Lehome
# Precision-aligned with official checkpoint

dataset:
  repo_id: lehome/dataset_challenge_merged
  root: Datasets/example/top_long_merged
  video_backend: torchcodec
  use_imagenet_stats: true
  image_transforms:
    enable: true
    max_num_transforms: 3
    random_order: false
    tfs:
      brightness:
        weight: 1.0
        type: ColorJitter
        kwargs:
          brightness: [0.8, 1.2]
      contrast:
        weight: 1.0
        type: ColorJitter
        kwargs:
          contrast: [0.8, 1.2]
      saturation:
        weight: 1.0
        type: ColorJitter
        kwargs:
          saturation: [0.5, 1.5]
      hue:
        weight: 1.0
        type: ColorJitter
        kwargs:
          hue: [-0.05, 0.05]
      sharpness:
        weight: 1.0
        type: SharpnessJitter
        kwargs:
          sharpness: [0.5, 1.5]

policy:
  type: xvla
  repo_id: lehome/xvla_so100
  push_to_hub: false
  pretrained_path: lerobot/xvla-base
  n_action_steps: 30
  chunk_size: 30
  empty_cameras: 0
  action_mode: auto
  max_action_dim: 20
  max_state_dim: 20
  num_image_views: 3
  resize_imgs_with_padding: [224, 224]
  dtype: bfloat16
  florence_config:
    projection_dim: 1024
    vision_config:
      model_type: davit
      image_size: 224
      projection_dim: 1024
      patch_size: [7, 3, 3, 3]
      patch_stride: [4, 2, 2, 2]
      patch_padding: [3, 1, 1, 1]
      patch_prenorm: [false, true, true, true]
      dim_embed: [256, 512, 1024, 2048]
      num_heads: [8, 16, 32, 64]
      num_groups: [8, 16, 32, 64]
      depths: [1, 1, 9, 1]
      window_size: 12
      image_pos_embed:
        type: learned_abs_2d
        max_pos_embeddings: 50
      visual_temporal_embedding:
        type: COSINE
        max_temporal_embeddings: 100
      image_feature_source: ["spatial_avg_pool", "temporal_avg_pool"]
    text_config:
      vocab_size: 51289
      d_model: 1024
      encoder_layers: 12
      decoder_layers: 12
      encoder_attention_heads: 16
      decoder_attention_heads: 16
      max_position_embeddings: 4096
  freeze_vision_encoder: false
  freeze_language_encoder: false
  train_policy_transformer: true
  train_soft_prompts: true
  input_features:
    observation.state:
      type: STATE
      shape: [12]
    observation.images.top_rgb:
      type: VISUAL
      shape: [3, 480, 640]
    observation.images.left_rgb:
      type: VISUAL
      shape: [3, 480, 640]
    observation.images.right_rgb:
      type: VISUAL
      shape: [3, 480, 640]
  output_features:
    action:
      type: ACTION
      shape: [12]

output_dir: outputs/train/top_long/xvla_base_3w_steps_final
batch_size: 8
steps: 30000
save_freq: 5000
eval_freq: 5000
log_freq: 1000
save_checkpoint: true

optimizer:
  type: xvla-adamw
  lr: 2e-5
  weight_decay: 1e-4
  betas: [0.9, 0.999]
  eps: 1e-8
  grad_clip_norm: 10.0

scheduler:
  type: cosine_decay_with_warmup
  num_warmup_steps: 1000
  num_decay_steps: 29000
  peak_lr: 2e-5
  decay_lr: 0.0

wandb:
  enable: false
  project: lehome_xvla
  mode: disabled
EOF
}

ensure_train_xvla_yaml() {
  local path="$ROOT_DIR/configs/train_xvla.yaml"
  [[ -f "$path" ]] && return 0
  warn "⚠️ 未找到 $path，正在自动生成默认 X-VLA 配置模板。"
  write_default_train_xvla_yaml "$path"
}

run_data_download_mode() {
  local with_full="$1"
  local old_value="${DATA_WITH_FULL_DATASET-__unset__}"
  DATA_WITH_FULL_DATASET="$with_full"
  run_steps_by_ids "data_download"
  local status=$?
  if [[ "$old_value" == "__unset__" ]]; then
    unset DATA_WITH_FULL_DATASET
  else
    DATA_WITH_FULL_DATASET="$old_value"
  fi
  return "$status"
}

run_shell_shortcuts_step() {
  local old_write_bashrc="$WRITE_BASHRC"
  WRITE_BASHRC=1
  run_steps_by_ids "write_shell_shortcuts"
  local status=$?
  WRITE_BASHRC="$old_write_bashrc"
  return "$status"
}

run_guided_task() {
  local label="$1"
  shift
  section "一路配置: $label"
  set +e
  "$@"
  local status=$?
  set -e
  if [[ $status -eq 0 ]]; then
    ok "✅ 模块完成: $label"
  else
    warn "⚠️ 模块失败但将继续后续流程: $label (exit=$status)"
  fi
  return "$status"
}

run_guided_flow_wizard() {
  ensure_project_root

  local options=(
    "$(format_key_desc_option "vpn" "配置 VPN 与代理")"
    "$(format_key_desc_option "prepare" "完整环境准备（uv / IsaacLab / LeHome）")"
    "$(format_key_desc_option "data_basic" "下载基础资源（Assets + 示例数据集）")"
    "$(format_key_desc_option "data_full" "下载完整数据集（含 dataset_challenge）")"
    "$(format_key_desc_option "ai_tools" "安装 AI 工具链")"
    "$(format_key_desc_option "git_basic" "Git 基础配置")"
    "$(format_key_desc_option "shell" "写入 Shell 快捷命令（默认全部）")"
    "$(format_key_desc_option "final_test" "最终测试（ACT 1000 步 + 1 次评估）")"
    "$(format_key_desc_option "xvla_check" "检查 X-VLA 环境与兼容补丁")"
  )

  local picked_lines
  picked_lines="$(ui_choose_many_lines "选择本次要一路执行的模块" "${options[@]}")"
  [[ -z "$picked_lines" ]] && { style_warn "未选择任何模块"; return 0; }

  local want_vpn=0 want_prepare=0 want_data_basic=0 want_data_full=0
  local want_ai_tools=0 want_git_basic=0 want_shell=0 want_final_test=0 want_xvla_check=0
  local line item_id
  while IFS= read -r line; do
    [[ -z "$line" ]] && continue
    item_id="${line%% | *}"
    item_id="${item_id//[[:space:]]/}"
    case "$item_id" in
      vpn) want_vpn=1 ;;
      prepare) want_prepare=1 ;;
      data_basic) want_data_basic=1 ;;
      data_full) want_data_full=1 ;;
      ai_tools) want_ai_tools=1 ;;
      git_basic) want_git_basic=1 ;;
      shell) want_shell=1 ;;
      final_test) want_final_test=1 ;;
      xvla_check) want_xvla_check=1 ;;
    esac
  done <<<"$picked_lines"

  local prepare_install_libs=0 allow_auto_patch=0
  if [[ "$want_prepare" -eq 1 ]]; then
    ui_confirm "环境准备是否同时安装系统图形库？服务器环境推荐安装。" 0 && prepare_install_libs=1
  fi
  if [[ "$want_xvla_check" -eq 1 ]]; then
    ui_confirm "X-VLA 检查发现兼容补丁缺失时，是否允许自动打补丁？" 0 && allow_auto_patch=1
  fi

  section "一路配置摘要"
  kv "VPN"             "$([[ "$want_vpn" -eq 1 ]] && echo 是 || echo 否)"
  kv "环境准备"         "$([[ "$want_prepare" -eq 1 ]] && echo 是 || echo 否)"
  kv "系统图形库"       "$([[ "$prepare_install_libs" -eq 1 ]] && echo 是 || echo 否)"
  kv "基础资源"         "$([[ "$want_data_basic" -eq 1 || "$want_data_full" -eq 1 ]] && echo 是 || echo 否)"
  kv "完整数据集"       "$([[ "$want_data_full" -eq 1 ]] && echo 是 || echo 否)"
  kv "AI 工具链"        "$([[ "$want_ai_tools" -eq 1 ]] && echo 是 || echo 否)"
  kv "Git 基础配置"     "$([[ "$want_git_basic" -eq 1 ]] && echo 是 || echo 否)"
  kv "Shell 快捷命令"   "$([[ "$want_shell" -eq 1 ]] && echo 是 || echo 否)"
  kv "最终测试"         "$([[ "$want_final_test" -eq 1 ]] && echo 是 || echo 否)"
  kv "X-VLA 检查"      "$([[ "$want_xvla_check" -eq 1 ]] && echo 是 || echo 否)"
  kv "自动打兼容补丁"   "$([[ "$allow_auto_patch" -eq 1 ]] && echo 是 || echo 否)"
  style_dim "确认后将按顺序一路执行，中途不再逐步回主菜单。"

  ui_confirm "按以上勾选项从头到尾开始配置吗？" 1 || {
    style_warn "已取消一路配置向导。"
    return 0
  }

  local old_mode="$MODE"
  local old_assume_yes="$ASSUME_YES"
  local old_write_bashrc="$WRITE_BASHRC"
  local old_guided_flow_active="$GUIDED_FLOW_ACTIVE"
  local old_guided_shortcuts_all="$GUIDED_SHORTCUTS_ALL"
  local old_guided_allow_auto_patch="$GUIDED_ALLOW_AUTO_PATCH"
  local old_prepare_install_libs="${PREPARE_INSTALL_SYSTEM_LIBS-__unset__}"
  local old_data_with_full_dataset="${DATA_WITH_FULL_DATASET-__unset__}"
  local status=0
  local guided_failed=()

  MODE="auto"
  ASSUME_YES=1
  GUIDED_FLOW_ACTIVE=1
  GUIDED_SHORTCUTS_ALL="$want_shell"
  GUIDED_ALLOW_AUTO_PATCH="$allow_auto_patch"
  WRITE_BASHRC="$want_shell"
  if [[ "$want_prepare" -eq 1 ]]; then
    PREPARE_INSTALL_SYSTEM_LIBS="$prepare_install_libs"
  fi
  if [[ "$want_data_basic" -eq 1 || "$want_data_full" -eq 1 ]]; then
    DATA_WITH_FULL_DATASET="$want_data_full"
  fi

  if [[ "$want_vpn" -eq 1 ]]; then
    run_guided_task "VPN 与代理" run_steps_by_ids "vpn_setup" || { status=1; guided_failed+=("VPN 与代理"); }
  fi
  if [[ "$want_prepare" -eq 1 ]]; then
    run_guided_task "环境准备" run_steps_by_ids "prepare_full" || { status=1; guided_failed+=("环境准备"); }
  fi
  if [[ "$want_data_basic" -eq 1 || "$want_data_full" -eq 1 ]]; then
    run_guided_task "数据资源下载" run_steps_by_ids "data_download" || { status=1; guided_failed+=("数据资源下载"); }
  fi
  if [[ "$want_ai_tools" -eq 1 ]]; then
    run_guided_task "AI 工具链" run_steps_by_ids "ai_tools" || { status=1; guided_failed+=("AI 工具链"); }
  fi
  if [[ "$want_git_basic" -eq 1 ]]; then
    run_guided_task "Git 基础配置" run_steps_by_ids "git_basic_setup" || { status=1; guided_failed+=("Git 基础配置"); }
  fi
  if [[ "$want_shell" -eq 1 ]]; then
    run_guided_task "Shell 快捷命令" run_steps_by_ids "write_shell_shortcuts" || { status=1; guided_failed+=("Shell 快捷命令"); }
  fi
  if [[ "$want_final_test" -eq 1 ]]; then
    run_guided_task "最终测试" run_steps_by_ids "final_test" || { status=1; guided_failed+=("最终测试"); }
  fi
  if [[ "$want_xvla_check" -eq 1 ]]; then
    run_guided_task "X-VLA 环境检查" check_xvla_workspace_readiness || { status=1; guided_failed+=("X-VLA 环境检查"); }
  fi

  MODE="$old_mode"
  ASSUME_YES="$old_assume_yes"
  WRITE_BASHRC="$old_write_bashrc"
  GUIDED_FLOW_ACTIVE="$old_guided_flow_active"
  GUIDED_SHORTCUTS_ALL="$old_guided_shortcuts_all"
  GUIDED_ALLOW_AUTO_PATCH="$old_guided_allow_auto_patch"

  if [[ "$old_prepare_install_libs" == "__unset__" ]]; then
    unset PREPARE_INSTALL_SYSTEM_LIBS
  else
    PREPARE_INSTALL_SYSTEM_LIBS="$old_prepare_install_libs"
  fi
  if [[ "$old_data_with_full_dataset" == "__unset__" ]]; then
    unset DATA_WITH_FULL_DATASET
  else
    DATA_WITH_FULL_DATASET="$old_data_with_full_dataset"
  fi

  if [[ ${#guided_failed[@]} -gt 0 ]]; then
    section "一路配置结果"
    warn "⚠️ 以下模块执行失败，但脚本已继续完成剩余流程："
    local failed_item
    for failed_item in "${guided_failed[@]}"; do
      style_dim "  - $failed_item"
    done
  fi

  return "$status"
}

print_data_resources_menu() {
  cat <<'EOF'
[ 数据资源管理 ]

  1. 下载基础资源
     下载 Assets 与示例数据集

  2. 下载完整数据集
     额外下载 dataset_challenge（含 depth 信息）

  0. 返回
EOF
}

print_training_workspace_menu() {
  cat <<'EOF'
[ X-VLA 工作台 ]

  1. 检查 X-VLA 环境
     检查依赖是否齐全，并提示逐项补齐命令

  2. 交互式训练向导
     按参数向导生成训练配置并启动训练

  3. 交互式评估向导
     按参数向导生成评估命令并启动评估

  0. 返回
EOF
}

print_dev_tools_menu() {
  cat <<'EOF'
[ github配置与Shell 快捷命令 ]

  1. 写入 Shell 快捷命令
     写入 go / train / eval / save / diff 命令并补充 PATH

  2. GitHub / Git 基础配置
     初始化 Git 身份并检查远端仓库

  3. 保存新版本
     执行版本保存流程并刷新版本索引

  4. 生成 diff 日志
     对比 upstream/main 并输出 diff.log

  0. 返回
EOF
}

print_xvla_workspace_hints() {
  section "X-VLA 工作台"
  style_ok "X-VLA 环境检查通过，可继续执行以下命令："
  style_dim "训练: bash \"$ROOT_DIR/all.sh\" --step train_xvla"
  style_dim "评估: bash \"$ROOT_DIR/all.sh\" --step eval_xvla"
}

check_python_snippet() {
  local code="$1"
  [[ -x "$ROOT_DIR/.venv/bin/python" ]] || return 2
  "$ROOT_DIR/.venv/bin/python" - <<PY >/dev/null 2>&1
$code
PY
}

python_import_available() {
  local module="$1"
  [[ -x "$ROOT_DIR/.venv/bin/python" ]] || return 2
  "$ROOT_DIR/.venv/bin/python" - "$module" <<'PY' >/dev/null 2>&1
import importlib, sys
importlib.import_module(sys.argv[1])
PY
}

file_contains_patterns() {
  local file="$1"
  shift
  [[ -f "$file" ]] || return 1
  local pattern
  for pattern in "$@"; do
    grep -F "$pattern" "$file" >/dev/null 2>&1 || return 1
  done
}

apply_lerobot_policy_bfloat16_patch() {
  local file="$ROOT_DIR/scripts/eval_policy/lerobot_policy.py"
  [[ -f "$file" ]] || { style_err "未找到文件: $file"; return 1; }
  python - "$file" <<'PY'
from pathlib import Path
import sys

path = Path(sys.argv[1])
text = path.read_text(encoding="utf-8")
new = """        # 5. Convert to Numpy (Remove batch dimension)\n        # Convert bfloat16 to float32 for numpy compatibility\n        if batch_action.dtype == torch.bfloat16:\n            batch_action = batch_action.float()\n        return batch_action.squeeze(0).cpu().numpy()\n"""
old = """        # 5. Convert to Numpy (Remove batch dimension)\n        return batch_action.squeeze(0).cpu().numpy()\n"""

if new in text:
    raise SystemExit(0)
if old not in text:
    raise SystemExit("PATCH_CONTEXT_MISMATCH")
path.write_text(text.replace(old, new, 1), encoding="utf-8")
PY
}

apply_so101_leader_lazy_import_patch() {
  local file="$ROOT_DIR/source/lehome/lehome/devices/lerobot/so101_leader.py"
  [[ -f "$file" ]] || { style_err "未找到文件: $file"; return 1; }
  python - "$file" <<'PY'
from pathlib import Path
import sys

path = Path(sys.argv[1])
text = path.read_text(encoding="utf-8")

top_new = """from typing import Dict, Tuple\n# Lazy import pynput to avoid X display requirement during module import\n# from pynput.keyboard import Listener, Key\n"""
top_old = """from typing import Dict, Tuple\nfrom pynput.keyboard import Listener, Key\n"""

listener_new = """        self._additional_callbacks = {}\n\n        # Lazy import pynput only when actually needed\n        from pynput.keyboard import Listener\n        self.listener = Listener(on_press=self.on_press, on_release=self.on_release)\n"""
listener_old = """        self._additional_callbacks = {}\n\n        self.listener = Listener(on_press=self.on_press, on_release=self.on_release)\n"""

key_new = """        except AttributeError:\n            # Handle special keys (like ESC)\n            from pynput.keyboard import Key\n            if key == Key.esc and \"ESCAPE\" in self._additional_callbacks:\n"""
key_old = """        except AttributeError:\n            # Handle special keys (like ESC)\n            if key == Key.esc and \"ESCAPE\" in self._additional_callbacks:\n"""

updated = text
for old, new in ((top_old, top_new), (listener_old, listener_new), (key_old, key_new)):
    if new in updated:
        continue
    if old not in updated:
        raise SystemExit("PATCH_CONTEXT_MISMATCH")
    updated = updated.replace(old, new, 1)

if updated != text:
    path.write_text(updated, encoding="utf-8")
PY
}

maybe_offer_patch() {
  local patch_name="$1" apply_fn="$2" next_hint="${3:-}"
  if [[ "$GUIDED_FLOW_ACTIVE" -eq 1 && "$GUIDED_ALLOW_AUTO_PATCH" -eq 0 ]]; then
    style_dim "已跳过自动补丁: $patch_name"
    [[ -n "$next_hint" ]] && style_dim "$next_hint"
    return 1
  fi
  if [[ "$ASSUME_YES" -eq 1 ]]; then
    style_dim "自动模式：正在尝试应用补丁: $patch_name"
    if "$apply_fn"; then
      return 0
    fi
    style_err "❌ 自动应用补丁失败: $patch_name"
    [[ -n "$next_hint" ]] && style_dim "$next_hint"
    return 1
  fi
  if ui_confirm "检测到缺少 $patch_name，是否立即自动打补丁？这会修改仓库文件。" 0; then
    if "$apply_fn"; then
      return 0
    fi
    style_err "❌ 自动应用补丁失败: $patch_name"
    [[ -n "$next_hint" ]] && style_dim "$next_hint"
    return 1
  fi
  style_dim "已跳过自动补丁: $patch_name"
  return 1
}

ensure_lerobot_policy_bfloat16_patch() {
  local file="$ROOT_DIR/scripts/eval_policy/lerobot_policy.py"
  if file_contains_patterns "$file" \
    "# Convert bfloat16 to float32 for numpy compatibility" \
    "if batch_action.dtype == torch.bfloat16:" \
    "batch_action = batch_action.float()"; then
    ok "✅ LeRobotPolicy bfloat16 兼容补丁已就位"
    return 0
  fi

  warn "❌ 缺少 LeRobotPolicy bfloat16 兼容补丁: $file"
  if ! maybe_offer_patch \
    "LeRobotPolicy bfloat16 兼容补丁" \
    apply_lerobot_policy_bfloat16_patch \
    "补齐: 请确认目标文件仍是官方原始结构，或人工同步 batch_action bfloat16 -> float32 的评估补丁"; then
    style_dim "补齐: 同步 batch_action bfloat16 -> float32 的评估补丁"
    return 1
  fi

  if file_contains_patterns "$file" \
    "# Convert bfloat16 to float32 for numpy compatibility" \
    "if batch_action.dtype == torch.bfloat16:" \
    "batch_action = batch_action.float()"; then
    ok "✅ 已自动应用 LeRobotPolicy bfloat16 兼容补丁"
    if [[ -x "$ROOT_DIR/.venv/bin/python" ]]; then
      if python_import_available "scripts.eval_policy.lerobot_policy"; then
        ok "✅ 已回归验证 LeRobot 评估策略导入"
      else
        warn "⚠️ 补丁已写入，但 LeRobot 评估策略仍无法导入"
        style_dim "补齐: bash \"$ROOT_DIR/all.sh\" --step uv_sync"
        style_dim "补齐: bash \"$ROOT_DIR/all.sh\" --step install_lehome_pkg"
      fi
    fi
    return 0
  fi

  style_err "❌ 自动补丁后仍未检测到 LeRobotPolicy bfloat16 兼容补丁"
  return 1
}

ensure_so101_leader_lazy_import_patch() {
  local file="$ROOT_DIR/source/lehome/lehome/devices/lerobot/so101_leader.py"
  if file_contains_patterns "$file" \
    "# Lazy import pynput to avoid X display requirement during module import" \
    "from pynput.keyboard import Listener" \
    "from pynput.keyboard import Key"; then
    ok "✅ SO101 Leader 无头导入补丁已就位"
    return 0
  fi

  warn "❌ 缺少 SO101 Leader 无头导入补丁: $file"
  if ! maybe_offer_patch \
    "SO101 Leader 无头导入补丁" \
    apply_so101_leader_lazy_import_patch \
    "补齐: 请确认目标文件仍是官方原始结构，或人工同步 pynput 懒加载补丁"; then
    style_dim "补齐: 同步 pynput 懒加载补丁，避免无图形环境导入失败"
    return 1
  fi

  if file_contains_patterns "$file" \
    "# Lazy import pynput to avoid X display requirement during module import" \
    "from pynput.keyboard import Listener" \
    "from pynput.keyboard import Key"; then
    ok "✅ 已自动应用 SO101 Leader 无头导入补丁"
    if [[ -x "$ROOT_DIR/.venv/bin/python" ]]; then
      if python_import_available "lehome.devices.lerobot.so101_leader"; then
        ok "✅ 已回归验证 SO101 Leader 模块导入"
      else
        warn "⚠️ 补丁已写入，但 SO101 Leader 模块仍无法导入"
        style_dim "补齐: bash \"$ROOT_DIR/all.sh\" --step install_lehome_pkg"
      fi
    fi
    return 0
  fi

  style_err "❌ 自动补丁后仍未检测到 SO101 Leader 无头导入补丁"
  return 1
}

check_xvla_workspace_readiness() {
  ensure_project_root
  section "检查 X-VLA 环境"

  local missing=0
  local venv_activate="$ROOT_DIR/.venv/bin/activate"
  local train_xvla_yaml="$ROOT_DIR/configs/train_xvla.yaml"
  local isaaclab_dir="$ROOT_DIR/third_party/IsaacLab"
  local isaacsim_link="$isaaclab_dir/_isaac_sim"

  if [[ -f "$venv_activate" ]]; then
    ok "✅ 虚拟环境已就绪: $venv_activate"
  else
    warn "❌ 缺少虚拟环境: $venv_activate"
    style_dim "补齐: bash \"$ROOT_DIR/all.sh\" --step install_uv_if_missing"
    style_dim "补齐: bash \"$ROOT_DIR/all.sh\" --step uv_sync"
    missing=1
  fi

  if [[ -d "$isaaclab_dir" ]]; then
    ok "✅ IsaacLab 目录已就绪: $isaaclab_dir"
  else
    warn "❌ 缺少 IsaacLab 目录: $isaaclab_dir"
    style_dim "补齐: bash \"$ROOT_DIR/all.sh\" --step clone_isaaclab"
    missing=1
  fi

  if [[ -f "$train_xvla_yaml" ]]; then
    ok "✅ X-VLA 配置已就绪: $train_xvla_yaml"
  else
    warn "⚠️ 未找到 X-VLA 配置，正在自动生成: $train_xvla_yaml"
    write_default_train_xvla_yaml "$train_xvla_yaml"
    ok "✅ 已自动生成默认 X-VLA 配置: $train_xvla_yaml"
    style_dim "提示: 如需自定义数据集、输出目录或训练参数，请手动编辑该文件。"
  fi

  if [[ -x "$ROOT_DIR/.venv/bin/python" ]]; then
    if check_python_snippet "import yaml; yaml.safe_load(open(r'''$train_xvla_yaml''', 'r', encoding='utf-8'))"; then
      ok "✅ X-VLA YAML 解析正常"
    else
      warn "❌ X-VLA YAML 解析失败: $train_xvla_yaml"
      style_dim "补齐: 请检查并修复 $train_xvla_yaml"
      missing=1
    fi

    if python_import_available "lerobot.policies.xvla"; then
      ok "✅ X-VLA 导入正常: lerobot.policies.xvla"
    else
      warn "❌ X-VLA 导入失败: lerobot.policies.xvla"
      style_dim "补齐: bash \"$ROOT_DIR/all.sh\" --step uv_sync"
      missing=1
    fi

    ensure_lerobot_policy_bfloat16_patch || missing=1

    if python_import_available "scripts.eval_policy.lerobot_policy"; then
      ok "✅ LeRobot 评估策略导入正常: scripts.eval_policy.lerobot_policy"
    else
      warn "❌ LeRobot 评估策略导入失败: scripts.eval_policy.lerobot_policy"
      style_dim "补齐: bash \"$ROOT_DIR/all.sh\" --step uv_sync"
      style_dim "补齐: bash \"$ROOT_DIR/all.sh\" --step install_lehome_pkg"
      missing=1
    fi

    if python_import_available "isaacsim"; then
      ok "✅ IsaacSim 导入正常: isaacsim"
    else
      warn "❌ IsaacSim 导入失败: isaacsim"
      style_dim "补齐: bash \"$ROOT_DIR/all.sh\" --step install_system_libs"
      style_dim "补齐: bash \"$ROOT_DIR/all.sh\" --step set_glx_vendor"
      style_dim "补齐: bash \"$ROOT_DIR/all.sh\" --step uv_sync"
      missing=1
    fi

    if python_import_available "lehome"; then
      ok "✅ LeHome 导入正常: lehome"
    else
      warn "❌ LeHome 导入失败: lehome"
      style_dim "补齐: bash \"$ROOT_DIR/all.sh\" --step install_lehome_pkg"
      missing=1
    fi

    ensure_so101_leader_lazy_import_patch || missing=1

    if python_import_available "lehome.devices.lerobot.so101_leader"; then
      ok "✅ SO101 Leader 模块导入正常: lehome.devices.lerobot.so101_leader"
    else
      warn "❌ SO101 Leader 模块导入失败: lehome.devices.lerobot.so101_leader"
      style_dim "补齐: 请确认已同步懒加载 pynput 补丁，并重新安装 LeHome 包"
      style_dim "补齐: bash \"$ROOT_DIR/all.sh\" --step install_lehome_pkg"
      missing=1
    fi
  else
    style_dim "跳过 Python 导入检查：虚拟环境尚未创建。"
  fi

  if [[ -L "$isaacsim_link" && -d "$isaacsim_link" ]]; then
    ok "✅ IsaacSim 软链已就绪: $isaacsim_link -> $(readlink -f "$isaacsim_link")"
  else
    warn "❌ IsaacSim 软链缺失或无效: $isaacsim_link"
    style_dim "补齐: bash \"$ROOT_DIR/all.sh\" --step setup_isaacsim_symlink"
    missing=1
  fi

  if [[ "$missing" -eq 0 ]]; then
    print_xvla_workspace_hints
  else
    warn "⚠️ 发现未就绪项，请按上面的单步命令逐项补齐后再重试。"
  fi
}

run_module_full_flow() {
  run_steps_by_ids "prepare_full" || return 1
  run_data_download_mode 0 || return 1
  run_shell_shortcuts_step || return 1
}

run_module_environment_setup() {
  run_steps_by_ids "prepare_full"
}

run_module_data_resources() {
  while true; do
    echo
    local choice
    if [[ "$HAS_GUM" -eq 1 ]]; then
      choice="$(ui_choose_numbered_option "[ 数据资源管理 ]" \
        "1. 下载基础资源 - 下载 Assets 与示例数据集" \
        "2. 下载完整数据集 - 额外下载 dataset_challenge（含 depth 信息）" \
        "0. 返回")"
    else
      print_data_resources_menu
      choice="$(ui_choose_numbered_option "" "1. 下载基础资源" "2. 下载完整数据集" "0. 返回")"
    fi
    case "$choice" in
      1) run_data_download_mode 0 || return 1 ;;
      2) run_data_download_mode 1 || return 1 ;;
      0|"") return 0 ;;
      *) style_warn "无效选项: $choice" ;;
    esac
  done
}

run_module_xvla_workspace() {
  while true; do
    echo
    local choice
    if [[ "$HAS_GUM" -eq 1 ]]; then
      choice="$(ui_choose_numbered_option "[ X-VLA 工作台 ]" \
        "1. 检查 X-VLA 环境 - 检查依赖并提示补齐命令" \
        "2. 交互式训练向导 - 生成训练配置并启动训练" \
        "3. 交互式评估向导 - 生成评估命令并启动评估" \
        "0. 返回")"
    else
      print_training_workspace_menu
      choice="$(ui_choose_numbered_option "" \
        "1. 检查 X-VLA 环境" \
        "2. 交互式训练向导" \
        "3. 交互式评估向导" \
        "0. 返回")"
    fi
    case "$choice" in
      1) check_xvla_workspace_readiness ;;
      2) run_steps_by_ids "train_guide" || return 1 ;;
      3) run_steps_by_ids "eval_guide" || return 1 ;;
      0|"") return 0 ;;
      *) style_warn "无效选项: $choice" ;;
    esac
  done
}

run_module_ai_tools() {
  run_steps_by_ids "ai_tools"
}

run_module_dev_tools() {
  while true; do
    echo
    local choice
    if [[ "$HAS_GUM" -eq 1 ]]; then
      choice="$(ui_choose_numbered_option "[ github配置与Shell 快捷命令 ]" \
        "1. 写入 Shell 快捷命令 - 写入 go / train / eval / save / diff 命令并补充 PATH" \
        "2. GitHub / Git 基础配置 - 初始化 Git 身份并检查远端仓库" \
        "3. 保存新版本 - 执行版本保存流程并刷新版本索引" \
        "4. 生成 diff 日志 - 对比 upstream/main 并输出 diff.log" \
        "0. 返回")"
    else
      print_dev_tools_menu
      choice="$(ui_choose_numbered_option "" "1. 写入 Shell 快捷命令" "2. GitHub / Git 基础配置" "3. 保存新版本" "4. 生成 diff 日志" "0. 返回")"
    fi
    case "$choice" in
      1) run_shell_shortcuts_step || return 1 ;;
      2) run_steps_by_ids "git_basic_setup" || return 1 ;;
      3) run_steps_by_ids "save_version" || return 1 ;;
      4) run_steps_by_ids "diff" || return 1 ;;
      0|"") return 0 ;;
      *) style_warn "无效选项: $choice" ;;
    esac
  done
}

run_module_vpn() {
  run_steps_by_ids "vpn_setup"
}

print_summary() {
  echo; style_dim "执行摘要: success=$SUCCESS_COUNT skip=$SKIPPED_COUNT failed=$FAILED_COUNT"
  style_dim "日志文件: $LOG_FILE"
  [[ "$WRITE_BASHRC" -eq 0 ]] && style_dim "提示: 默认未改写 ~/.bashrc（如需写入请加 --write-bashrc）"
}

# ── Step 处理函数：env.sh 原有 ────────────────────────────────────────────────
step_install_system_libs() {
  local priv=(); [[ "$(id -u)" -ne 0 ]] && priv=(sudo)
  "${priv[@]}" apt update
  "${priv[@]}" apt install -y libglu1-mesa libgl1 libegl1 libxrandr2 libxinerama1 libxcursor1 libxi6 libxext6 libx11-6
}
step_set_glx_vendor()        { export __GLX_VENDOR_LIBRARY_NAME=nvidia; style_ok "已设置 __GLX_VENDOR_LIBRARY_NAME=nvidia"; }
step_uv_sync()               { ensure_project_root; require_cmd uv; uv sync; }
step_clone_isaaclab() {
  ensure_project_root; mkdir -p "$ROOT_DIR/third_party"
  if [[ -d "$ROOT_DIR/third_party/IsaacLab/.git" ]]; then style_dim "IsaacLab 已存在，跳过 clone"; return 0; fi
  git clone https://github.com/lehome-official/IsaacLab.git "$ROOT_DIR/third_party/IsaacLab"
}
step_install_isaaclab() {
  ensure_project_root; activate_venv
  [[ -x "$ROOT_DIR/third_party/IsaacLab/isaaclab.sh" ]] || { style_err "未找到 isaaclab.sh"; return 1; }
  "$ROOT_DIR/third_party/IsaacLab/isaaclab.sh" -i none
}
step_install_lehome_pkg()        { ensure_project_root; activate_venv; require_cmd uv; uv pip install -e "$ROOT_DIR/source/lehome"; }
step_download_assets()           { ensure_project_root; activate_venv; require_cmd hf; hf download lehome/asset_challenge --repo-type dataset --local-dir "$ROOT_DIR/Assets"; }
step_download_example_dataset()  { ensure_project_root; activate_venv; require_cmd hf; hf download lehome/dataset_challenge_merged --repo-type dataset --local-dir "$ROOT_DIR/Datasets/example"; }
step_install_uv_if_missing() {
  command -v uv >/dev/null 2>&1 && { style_dim "uv 已存在，跳过安装"; return 0; }
  curl -LsSf https://astral.sh/uv/install.sh | sh; export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"; require_cmd uv
}
step_install_hf_cli()  { ensure_project_root; activate_venv; require_cmd uv; uv pip install "huggingface_hub[cli]"; }
step_link_uv_cache() {
  local target="$DATA_ROOT/.uv_cache" link_path="$HOME/.cache/uv"
  mkdir -p "$target" "$HOME/.cache"
  if [[ -e "$link_path" || -L "$link_path" ]]; then
    ui_confirm "将重置 $link_path 并重建软链，确认继续？" 0 || return 1; rm -rf "$link_path"; fi
  ln -s "$target" "$link_path"
}
step_setup_isaacsim_symlink() {
  ensure_project_root; activate_venv
  ensure_isaacsim_symlink_ready
}

choose_shell_shortcut_items() {
  if [[ "$GUIDED_SHORTCUTS_ALL" -eq 1 ]]; then
    printf '%s\n' path go train eval save diff
    return 0
  fi
  if [[ "$ASSUME_YES" -eq 1 ]]; then
    printf '%s\n' path go train eval save diff
    return 0
  fi

  local options=(
    "$(format_key_desc_option "path" "补充 PATH" 8)"
    "$(format_key_desc_option "go" "快速进入项目环境" 8)"
    "$(format_key_desc_option "train" "打开训练向导" 8)"
    "$(format_key_desc_option "eval" "打开评估向导（带参数时回退 bash eval）" 8)"
    "$(format_key_desc_option "save" "保存新版本" 8)"
    "$(format_key_desc_option "diff" "生成 diff 日志" 8)"
  )
  local picked_lines
  picked_lines="$(ui_choose_many_lines "选择要写入 ~/.bashrc 的配置项" "${options[@]}")"
  [[ -z "$picked_lines" ]] && return 0

  local line
  while IFS= read -r line; do
    [[ -z "$line" ]] && continue
    printf '%s\n' "${line%% | *}"
  done <<<"$picked_lines"
}

append_shell_shortcut_block() {
  local begin="$1"
  shift
  local selected=("$@")

  printf '%s\n' "$begin"
  printf '%s\n' 'cd_activate_venv() {'
  printf '%s\n' '  builtin cd "$@" || return'
  printf '%s\n' '  if [ -f ".venv/bin/activate" ]; then'
  printf '%s\n' '    if [[ "${VIRTUAL_ENV:-}" != "$(pwd)/.venv" ]]; then'
  printf '%s\n' '      source .venv/bin/activate'
  printf '%s\n' '    fi'
  printf '%s\n' '  fi'
  printf '%s\n' '}'
  printf "%s\n" "alias cd='cd_activate_venv'"

  local item
  for item in "${selected[@]}"; do
    case "$item" in
      path)
        printf '%s\n' 'export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"'
        ;;
      go)
        printf '%s\n' 'go() {'
        printf '%s\n' '  if [[ $# -gt 0 ]]; then'
        printf '%s\n' '    command go "$@"'
        printf '%s\n' '    return'
        printf '%s\n' '  fi'
        printf '  cd "%s/lehome-challenge" && source .venv/bin/activate\n' "$SCRIPT_DIR"
        printf '%s\n' '}'
        ;;
      train)
        printf 'train() { bash "%s/all.sh" --step train_guide; }\n' "$SCRIPT_DIR"
        ;;
      eval)
        printf '%s\n' 'eval() {'
        printf '  if [[ $# -eq 0 ]]; then\n'
        printf '    bash "%s/all.sh" --step eval_guide\n' "$SCRIPT_DIR"
        printf '%s\n' '  else'
        printf '%s\n' '    builtin eval "$@"'
        printf '%s\n' '  fi'
        printf '%s\n' '}'
        ;;
      save)
        printf '%s\n' 'save() {'
        printf '%s\n' '  local version="${1:-}"'
        printf '%s\n' '  if [[ -n "$version" ]]; then'
        printf '%s\n' '    shift'
        printf '    SAVE_VERSION="$version" SAVE_NOTE="$*" bash "%s/all.sh" --step save_version\n' "$SCRIPT_DIR"
        printf '%s\n' '  else'
        printf '    bash "%s/all.sh" --step save_version\n' "$SCRIPT_DIR"
        printf '%s\n' '  fi'
        printf '%s\n' '}'
        ;;
      diff)
        printf 'diff() { bash "%s/all.sh" --step diff; }\n' "$SCRIPT_DIR"
        ;;
    esac
  done
  printf '%s\n' 'if [ -f ".venv/bin/activate" ]; then source .venv/bin/activate; fi'
  printf '%s\n' '# --- End LeHome all.sh shortcuts ---'
}

step_write_shell_shortcuts() {
  [[ "$WRITE_BASHRC" -ne 1 ]] && { style_warn "未传 --write-bashrc，跳过写入 ~/.bashrc"; return 0; }
  local bashrc="$HOME/.bashrc"
  local begin="# --- LeHome all.sh shortcuts ---"
  local selected_raw
  selected_raw="$(choose_shell_shortcut_items || true)"
  [[ -n "$selected_raw" ]] || { style_warn "未选择任何配置项，跳过写入 ~/.bashrc"; return 0; }
  local selected=()
  while IFS= read -r item; do
    [[ -n "$item" ]] && selected+=("$item")
  done <<<"$selected_raw"

  touch "$bashrc"; backup_file "$bashrc"
  awk \
    -v old_begin1="# --- LeHome 环境配置 ---" \
    -v old_end1="# --- End LeHome ---" \
    -v old_begin2="# --- LeHome env v3 ---" \
    -v old_end2="# --- End LeHome env v3 ---" \
    -v new_begin="$begin" \
    -v new_end="# --- End LeHome all.sh shortcuts ---" '
      $0==old_begin1 || $0==old_begin2 || $0==new_begin {skip=1; next}
      skip && ($0==old_end1 || $0==old_end2 || $0==new_end) {skip=0; next}
      !skip {print}
    ' "$bashrc" >"$bashrc.tmp"
  append_shell_shortcut_block "$begin" "${selected[@]}" >>"$bashrc.tmp"
  mv "$bashrc.tmp" "$bashrc"
  style_ok "已写入 ~/.bashrc（已备份）: ${selected[*]}"
  style_warn "提示: 请手动执行一次 source ~/.bashrc，或重新打开终端"
  style_warn "提示: 未 source 前，当前 shell 里的 eval 仍可能是 Bash 内建命令"
  style_dim "自检: 执行 type eval，期望看到 eval is a function"
  style_dim "提示: 以后可直接输入 go / train / eval / save / diff"
}

# ── Step 处理函数：v1/prepare.sh ─────────────────────────────────────────────
resolve_prepare_install_libs() {
  if [[ -n "${PREPARE_INSTALL_SYSTEM_LIBS+x}" ]]; then
    [[ "${PREPARE_INSTALL_SYSTEM_LIBS:-0}" == "1" ]] && echo 1 || echo 0
    return 0
  fi
  if [[ "$ASSUME_YES" -eq 1 ]]; then
    echo 0
    return 0
  fi
  if ui_confirm "是否安装服务器系统依赖 (libGL, libEGL 等)？" 0; then
    echo 1
  else
    echo 0
  fi
}

step_prepare_full() {
  local install_libs
  install_libs="$(resolve_prepare_install_libs)"
  section "准备环境"; kv "Project root" "$ROOT_DIR"; kv "Install libs" "$install_libs"
  ensure_project_root
  if [[ $install_libs -eq 1 ]]; then install_system_libs
  else warn "⚠️ 未安装系统图形库；若后续 IsaacSim/FFmpeg 报错，请设置 PREPARE_INSTALL_SYSTEM_LIBS=1 重新执行"; fi
  ensure_uv; link_uv_cache
  section "同步 Python 依赖"; cmd_preview 'uv sync'; uv sync; ok "✅ uv sync 完成"
  section "安装 IsaacLab / LeHome"; clone_isaaclab_if_missing; activate_venv
  ok "✅ IsaacLab 目录已就绪"
  ok "✅ 虚拟环境已激活"

  # 自动接受 EULA：在 isaacsim 包的 kit 目录下创建 EULA_ACCEPTED 文件
  warn "⚠️ 自动接受 IsaacSim EULA 协议条款。"
  python - <<'PY'
import os, sys
try:
    import isaacsim
    eula_path = os.path.join(os.path.dirname(isaacsim.__file__), 'kit', 'EULA_ACCEPTED')
    os.makedirs(os.path.dirname(eula_path), exist_ok=True)
    with open(eula_path, 'w') as f:
        f.write('yes\n')
    print(f"✓ EULA 已自动接受: {eula_path}")
except Exception as e:
    print(f"警告: 无法创建 EULA_ACCEPTED 文件: {e}", file=sys.stderr)
PY

  # 在执行 isaaclab.sh 之前先配置软链接
  section "配置 IsaacSim 软链"
  if ensure_isaacsim_symlink_ready; then
    ok "✅ IsaacSim 软链接已就绪"
  else
    warn "⚠️ 软链接配置失败，将在 isaaclab.sh 执行后重试"
  fi

  cmd_preview './third_party/IsaacLab/isaaclab.sh -i none'; ./third_party/IsaacLab/isaaclab.sh -i none

  # 再次检查软链接，确保 isaaclab.sh 执行后软链接正常
  section "验证 IsaacSim 软链"
  if ! ensure_isaacsim_symlink_ready; then
    style_err "❌ IsaacSim 软链接配置失败"
    return 1
  fi
  cmd_preview 'uv pip install -e ./source/lehome'; uv pip install -e ./source/lehome
  cmd_preview 'uv pip install "huggingface_hub[cli]"'; uv pip install "huggingface_hub[cli]"
  section "校验核心导入"
  check_imports torch torchvision lerobot isaacsim lehome isaaclab isaaclab_tasks isaaclab_rl isaaclab_mimic isaaclab_assets isaaclab_contrib
  ok "✅ 核心环境安装完成"
  section "收尾配置"
  local isaacsim_link="$ROOT_DIR/third_party/IsaacLab/_isaac_sim"
  if [[ -L "$isaacsim_link" && -d "$isaacsim_link" ]]; then
    ok "✅ IsaacSim 软链已校验: $isaacsim_link -> $(readlink -f "$isaacsim_link")"
  else
    style_err "❌ IsaacSim 软链无效: $isaacsim_link"
    return 1
  fi
  if [[ "$WRITE_BASHRC" -eq 1 || "$ASSUME_YES" -eq 1 ]]; then
    step_write_shell_shortcuts
  elif ui_confirm "是否同时写入 Shell 快捷命令？" 1; then
    local old_write_bashrc="$WRITE_BASHRC"
    WRITE_BASHRC=1
    step_write_shell_shortcuts
    WRITE_BASHRC="$old_write_bashrc"
  fi
  ok "✅ 环境准备完成"
  style_dim "提示: 如需快捷命令，请执行模块 6 的“写入 Shell 快捷命令”"
  style_dim "提示: 写入后请手动执行一次 source ~/.bashrc"
}

# ── Step 处理函数：v1/data.sh ────────────────────────────────────────────────
step_data_download() {
  local with_full=0; [[ "${DATA_WITH_FULL_DATASET:-0}" == "1" ]] && with_full=1
  section "下载数据资源"; kv "Project root" "$ROOT_DIR"; kv "Full dataset" "$with_full"
  ensure_project_root; activate_venv
  command -v hf >/dev/null 2>&1 || die "❌ 未找到 hf 命令，请先执行 prepare 步骤"
  local download_stamp; download_stamp="$(date +%m-%d_%H-%M-%S)"
  local asset_log="$LOG_DIR/${download_stamp}_hf_asset_challenge.log"
  local merged_log="$LOG_DIR/${download_stamp}_hf_dataset_challenge_merged.log"
  local full_log="$LOG_DIR/${download_stamp}_hf_dataset_challenge.log"

  section "并行下载基础资源"
  cmd_preview 'hf download lehome/asset_challenge --repo-type dataset --local-dir Assets'
  hf download lehome/asset_challenge --repo-type dataset --local-dir Assets >"$asset_log" 2>&1 &
  local asset_pid=$!
  cmd_preview 'hf download lehome/dataset_challenge_merged --repo-type dataset --local-dir Datasets/example'
  hf download lehome/dataset_challenge_merged --repo-type dataset --local-dir Datasets/example >"$merged_log" 2>&1 &
  local merged_pid=$!
  style_dim "基础资源下载已并行启动"
  style_dim "日志: ${asset_log#$ROOT_DIR/} / ${merged_log#$ROOT_DIR/}"

  local asset_status merged_status
  set +e
  wait "$asset_pid"; asset_status=$?
  wait "$merged_pid"; merged_status=$?
  set -e

  if [[ $asset_status -eq 0 ]]; then
    ok "✅ 下载完成: lehome/asset_challenge"
  else
    style_err "❌ 下载失败: lehome/asset_challenge"
    style_dim "查看日志: ${asset_log#$ROOT_DIR/}"
  fi
  if [[ $merged_status -eq 0 ]]; then
    ok "✅ 下载完成: lehome/dataset_challenge_merged"
  else
    style_err "❌ 下载失败: lehome/dataset_challenge_merged"
    style_dim "查看日志: ${merged_log#$ROOT_DIR/}"
  fi

  if [[ $asset_status -ne 0 || $merged_status -ne 0 ]]; then
    return 1
  fi

  if [[ $with_full -eq 1 ]]; then
    section "下载完整数据集"
    cmd_preview 'hf download lehome/dataset_challenge --repo-type dataset --local-dir Datasets/example'
    set +e
    hf download lehome/dataset_challenge --repo-type dataset --local-dir Datasets/example >"$full_log" 2>&1
    local full_status=$?
    set -e
    if [[ $full_status -eq 0 ]]; then
      ok "✅ 下载完成: lehome/dataset_challenge"
    else
      style_err "❌ 下载失败: lehome/dataset_challenge"
      style_dim "查看日志: ${full_log#$ROOT_DIR/}"
      return 1
    fi
  else warn "⚠️ 未下载完整 dataset_challenge；如需 depth 信息，请设置 DATA_WITH_FULL_DATASET=1"; fi
  ok "✅ 数据下载完成"
}

# ── Step 处理函数：v1/eval.sh ────────────────────────────────────────────────
write_eval_env_summary() {
  local log_file="$1"
  local python_bin="${ROOT_DIR}/.venv/bin/python"
  local pth_file="${ROOT_DIR}/.venv/lib/python3.11/site-packages/isaaclab.pth"
  local pth_value="missing"
  local import_status="unknown"
  local ld_has_kit="no"

  if [[ -f "$pth_file" ]]; then
    pth_value="$(head -n 1 "$pth_file" | tr -d '\r')"
  fi
  [[ "${LD_LIBRARY_PATH:-}" == *"isaacsim/kit"* ]] && ld_has_kit="yes"
  if "$python_bin" -c "import isaaclab" >/dev/null 2>&1; then
    import_status="ok"
  else
    import_status="failed"
  fi

  {
    printf '[%s] ===== Eval Environment Summary =====\n' "$(date '+%F %T')"
    printf '[%s] ROOT_DIR=%s\n' "$(date '+%F %T')" "$ROOT_DIR"
    printf '[%s] PYTHON=%s\n' "$(date '+%F %T')" "$(command -v python)"
    printf '[%s] VIRTUAL_ENV=%s\n' "$(date '+%F %T')" "${VIRTUAL_ENV:-unset}"
    printf '[%s] isaaclab.pth=%s\n' "$(date '+%F %T')" "$pth_value"
    printf '[%s] LD_LIBRARY_PATH_has_isaacsim_kit=%s\n' "$(date '+%F %T')" "$ld_has_kit"
    printf '[%s] import_isaaclab=%s\n' "$(date '+%F %T')" "$import_status"
  } | tee -a "$log_file"
}

emit_eval_check_error() {
  local log_file="$1"
  shift
  style_err "$*"
  printf '[%s] %s\n' "$(date '+%F %T')" "$*" >>"$log_file"
}

validate_eval_environment() {
  local log_file="$1"
  local python_bin="${ROOT_DIR}/.venv/bin/python"
  local pth_file="${ROOT_DIR}/.venv/lib/python3.11/site-packages/isaaclab.pth"
  local isaaclab_source="${ROOT_DIR}/third_party/IsaacLab/source/isaaclab"
  local isaacsim_kit="${ROOT_DIR}/.venv/lib/python3.11/site-packages/isaacsim/kit"

  [[ -x "$python_bin" ]] || { emit_eval_check_error "$log_file" "❌ 虚拟环境 Python 不存在: $python_bin"; return 1; }
  [[ -d "$isaaclab_source" ]] || { emit_eval_check_error "$log_file" "❌ IsaacLab 源码目录不存在: $isaaclab_source"; return 1; }
  [[ -f "$pth_file" ]] || { emit_eval_check_error "$log_file" "❌ 缺少 isaaclab.pth: $pth_file"; return 1; }
  [[ "$(head -n 1 "$pth_file" | tr -d '\r')" == "$isaaclab_source" ]] || {
    emit_eval_check_error "$log_file" "❌ isaaclab.pth 内容异常: $(head -n 1 "$pth_file" | tr -d '\r')"
    emit_eval_check_error "$log_file" "期望值: $isaaclab_source"
    return 1
  }
  [[ -d "$isaacsim_kit" ]] || { emit_eval_check_error "$log_file" "❌ Isaac Sim kit 目录不存在: $isaacsim_kit"; return 1; }
  [[ "${LD_LIBRARY_PATH:-}" == *"$isaacsim_kit"* ]] || {
    emit_eval_check_error "$log_file" "❌ LD_LIBRARY_PATH 未包含 Isaac Sim kit 路径: $isaacsim_kit"
    return 1
  }
  "$python_bin" -c "import isaaclab" >/dev/null 2>&1 || {
    emit_eval_check_error "$log_file" "❌ Python 预检查失败：无法导入 isaaclab"
    return 1
  }
}

step_eval() {
  local model="${EVAL_MODEL:-xvla}"
  local default_run_dir
  case "$model" in
    act)      default_run_dir="outputs/train/act_top_long";;
    diffusion|dp) model="diffusion"; default_run_dir="outputs/train/dp_top_long";;
    smolvla)  default_run_dir="outputs/train/smolvla_top_long";;
    xvla)     default_run_dir="outputs/train/top_long/xvla_base_3w_steps";;
    *) die "❌ 不支持的模型: $model（可选: act / diffusion / smolvla / xvla）";;
  esac
  local policy_path="${EVAL_POLICY_PATH:-${default_run_dir}/checkpoints/last/pretrained_model}"
  local garment="${EVAL_GARMENT:-top_long}"
  local episodes="${EVAL_EPISODES:-5}"
  local dataset_root="${EVAL_DATASET_ROOT:-Datasets/example/${garment}_merged}"

  ensure_project_root
  local cache_root="$ROOT_DIR/.cache"
  local hf_cache_root="$cache_root/huggingface"
  local torch_cache_root="$cache_root/torch"
  mkdir -p "$hf_cache_root/datasets" "$hf_cache_root/hub" "$hf_cache_root/transformers" "$torch_cache_root/hub/checkpoints"
  export XDG_CACHE_HOME="${XDG_CACHE_HOME:-$cache_root}"
  export HF_HOME="${HF_HOME:-$hf_cache_root}"
  export HF_DATASETS_CACHE="${HF_DATASETS_CACHE:-$hf_cache_root/datasets}"
  export HF_HUB_CACHE="${HF_HUB_CACHE:-$hf_cache_root/hub}"
  export HUGGINGFACE_HUB_CACHE="${HUGGINGFACE_HUB_CACHE:-$HF_HUB_CACHE}"
  export TRANSFORMERS_CACHE="${TRANSFORMERS_CACHE:-$hf_cache_root/transformers}"
  export TORCH_HOME="${TORCH_HOME:-$torch_cache_root}"
  activate_venv
  if ! command -v xvfb-run >/dev/null 2>&1; then die "❌ 未找到 xvfb-run，请先安装"; fi
  local log_file
  log_file="$(generate_log_path "eval" "lerobot-${garment}")"

  section "启动评估"; kv "Model" "$model"; kv "Policy path" "$policy_path"
  kv "Dataset root" "$dataset_root"; kv "Log file" "${log_file#$ROOT_DIR/}"; kv "XVFB" "enabled"
  [[ -d "$dataset_root" ]] || warn "⚠️ dataset_root 目录不存在: $dataset_root"
  [[ -d "$policy_path"  ]] || warn "⚠️ policy_path 目录不存在: $policy_path"
  write_eval_env_summary "$log_file"
  validate_eval_environment "$log_file" || die "❌ 评估前环境自检失败，请先修复上述问题后再运行"

  local extra_args=()
  local has_headless=0
  local has_task_description=0
  local arg
  for arg in "${EVAL_EXTRA_ARGS[@]-}"; do
    [[ "$arg" == "--headless" ]] && has_headless=1
    [[ "$arg" == "--task_description" || "$arg" == --task_description=* ]] && has_task_description=1
    extra_args+=("$arg")
  done
  if [[ "$model" == "smolvla" && "$has_task_description" -eq 0 ]]; then
    extra_args+=(--task_description "fold the garment on the table")
  fi
  if [[ "$has_headless" -eq 0 ]]; then
    extra_args+=(--headless)
    style_dim "默认附加参数: --headless"
  fi
  (( ${#extra_args[@]} > 0 )) && style_dim "额外参数: ${extra_args[*]}"

  set +e
  xvfb-run -a env LEHOME_EVAL_XVFB_WRAPPED=1 \
    python -m scripts.eval \
      --policy_type lerobot \
      --policy_path "$policy_path" \
      --dataset_root "$dataset_root" \
      --garment_type "$garment" \
      --num_episodes "$episodes" \
      --enable_cameras \
      --device cpu \
      "${extra_args[@]}" \
    2>&1 | tee -a "$log_file"
  local exit_code=${PIPESTATUS[0]}; set -e
  [[ $exit_code -eq 0 ]] && ok "✅ 评估完成: ${log_file#$ROOT_DIR/}" || die "❌ 评估失败，退出码: $exit_code；日志: ${log_file#$ROOT_DIR/}"
}

# ── Step 处理函数：v1/xvla.sh ────────────────────────────────────────────────
# WandB 配置变量（可通过环境变量覆盖）
WANDB_PROJECT="${WANDB_PROJECT:-lehome}"
WANDB_ENTITY="${WANDB_ENTITY:-}"
WANDB_NOTES="${WANDB_NOTES:-}"
WANDB_RUN_ID="${WANDB_RUN_ID:-}"
WANDB_MODE="${WANDB_MODE:-online}"
WANDB_DISABLE_ARTIFACT="${WANDB_DISABLE_ARTIFACT:-false}"
WANDB_ENABLE="${WANDB_ENABLE:-false}"
HF_TOKEN="${HF_TOKEN:-YOUR_HUGGINGFACE_TOKEN_HERE}"

normalize_bool() {
  local val="${1,,}" name="${2:-value}"
  case "$val" in true|1|yes|on) echo "true";; false|0|no|off|"") echo "false";;
    *) warn "⚠️ $name 值 '$1' 无法识别，默认 false"; echo "false";; esac
}

maybe_login_hf() {
  if [[ -n "$HF_TOKEN" && "$HF_TOKEN" != "YOUR_HUGGINGFACE_TOKEN_HERE" ]]; then
    if ! command -v hf >/dev/null 2>&1; then
      log "📦 安装 Hugging Face CLI..."
      uv pip install "huggingface_hub[cli]"
    fi
    log "🔑 登录 Hugging Face..."
    hf auth login --token "$HF_TOKEN" --add-to-git-credential
    ok "✅ Hugging Face 登录完成"
  else
    warn "⚠️ 未填写 HF_TOKEN，跳过 Hugging Face 登录。若需访问私有仓库，请补全。"
  fi
}

resolve_policy_type_from_config() {
  local config_path="$1"
  python - "$config_path" <<'PY'
import sys
import yaml

with open(sys.argv[1], "r", encoding="utf-8") as f:
    cfg = yaml.safe_load(f)
print(cfg.get("policy", {}).get("type", ""))
PY
}

build_train_command_for_config() {
  local config_path="$1"
  local python_bin="$2"
  local lerobot_train_bin="$3"
  local train_bridge="$4"
  local policy_type
  policy_type="$(resolve_policy_type_from_config "$config_path")"
  case "$policy_type" in
    xvla)
      TRAIN_CMD=( "$python_bin" "$train_bridge" --config-path="$config_path" --headless )
      ;;
    *)
      TRAIN_CMD=( "$lerobot_train_bin" --config_path="$config_path" )
      ;;
  esac
  TRAIN_POLICY_TYPE="$policy_type"
}

step_train_xvla() {
  ensure_train_xvla_yaml
  ensure_project_root; activate_venv
  maybe_login_hf
  local config_template="$ROOT_DIR/configs/train_xvla.yaml"
  [[ -f "$config_template" ]] || die "❌ 找不到 X-VLA 配置模板: $config_template"

  local dataset_repo dataset_root model_repo pretrained_path output_dir train_steps
  dataset_repo="$(python -c "import yaml,sys; d=yaml.safe_load(open(sys.argv[1])); print(d['dataset']['repo_id'])" "$config_template")"
  dataset_root="$(python -c "import yaml,sys; d=yaml.safe_load(open(sys.argv[1])); print(d['dataset']['root'])" "$config_template")"
  model_repo="$(python -c "import yaml,sys; d=yaml.safe_load(open(sys.argv[1])); print(d['policy']['repo_id'])" "$config_template")"
  pretrained_path="$(python -c "import yaml,sys; d=yaml.safe_load(open(sys.argv[1])); print(d['policy']['pretrained_path'])" "$config_template")"
  output_dir="$(python -c "import yaml,sys; d=yaml.safe_load(open(sys.argv[1])); print(d['output_dir'])" "$config_template")"
  train_steps="$(python -c "import yaml,sys; d=yaml.safe_load(open(sys.argv[1])); print(d['steps'])" "$config_template")"

  dataset_repo="${XVLA_DATASET_REPO:-$dataset_repo}"
  dataset_root="${XVLA_DATASET_ROOT:-$dataset_root}"
  model_repo="${XVLA_MODEL_REPO:-$model_repo}"
  pretrained_path="${XVLA_PRETRAINED_PATH:-$pretrained_path}"
  output_dir="${XVLA_OUTPUT_DIR:-$output_dir}"
  train_steps="${XVLA_TRAIN_STEPS:-$train_steps}"

  local job_name="${XVLA_JOB_NAME:-$(basename -- "$output_dir")}"
  [[ -z "$job_name" || "$job_name" == "." ]] && job_name="xvla_$(date +'%Y%m%d_%H%M%S')"
  local wandb_enabled; wandb_enabled="$(normalize_bool "$WANDB_ENABLE" "WANDB_ENABLE")"
  local dry_run; dry_run="$(normalize_bool "${XVLA_DRY_RUN:-false}" "XVLA_DRY_RUN")"

  local tmp_config; tmp_config="$(mktemp /tmp/train_xvla_XXXX.yaml)"
  [[ "$dry_run" != "true" ]] && trap 'rm -f "$tmp_config"' EXIT

  XVLA_CONFIG_TEMPLATE="$config_template" XVLA_TMP_CONFIG="$tmp_config" \
  XVLA_DATASET_REPO="$dataset_repo" XVLA_DATASET_ROOT="$dataset_root" \
  XVLA_MODEL_REPO="$model_repo" XVLA_PRETRAINED_PATH="$pretrained_path" \
  XVLA_OUTPUT_DIR="$output_dir" XVLA_TRAIN_STEPS="$train_steps" \
  XVLA_JOB_NAME="$job_name" XVLA_WANDB_ENABLE="$wandb_enabled" \
  XVLA_WANDB_PROJECT="$WANDB_PROJECT" XVLA_WANDB_ENTITY="$WANDB_ENTITY" \
  XVLA_WANDB_NOTES="$WANDB_NOTES" XVLA_WANDB_RUN_ID="$WANDB_RUN_ID" \
  XVLA_WANDB_MODE="$WANDB_MODE" XVLA_WANDB_DISABLE_ARTIFACT="$WANDB_DISABLE_ARTIFACT" \
  python - <<'PY'
import os, yaml
def maybe_none(v): return v if v else None
src, dst = os.environ["XVLA_CONFIG_TEMPLATE"], os.environ["XVLA_TMP_CONFIG"]
with open(src, 'r', encoding='utf-8') as f: data = yaml.safe_load(f)
data['dataset']['repo_id'] = os.environ['XVLA_DATASET_REPO']
data['dataset']['root']    = os.environ['XVLA_DATASET_ROOT']
data['policy']['repo_id']  = os.environ['XVLA_MODEL_REPO']
data['policy']['pretrained_path'] = os.environ['XVLA_PRETRAINED_PATH']
data['output_dir'] = os.environ['XVLA_OUTPUT_DIR']
data['steps']      = int(os.environ['XVLA_TRAIN_STEPS'])
data['job_name']   = os.environ['XVLA_JOB_NAME']
w = data.setdefault('wandb', {})
w['enable']           = os.environ['XVLA_WANDB_ENABLE'].lower() == 'true'
w['project']          = os.environ['XVLA_WANDB_PROJECT']
w['entity']           = maybe_none(os.environ['XVLA_WANDB_ENTITY'])
w['notes']            = maybe_none(os.environ['XVLA_WANDB_NOTES'])
w['run_id']           = maybe_none(os.environ['XVLA_WANDB_RUN_ID'])
w['mode']             = os.environ['XVLA_WANDB_MODE']
w['disable_artifact'] = os.environ['XVLA_WANDB_DISABLE_ARTIFACT'].lower() == 'true'
with open(dst, 'w', encoding='utf-8') as f: yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True)
PY

  local train_cmd=(python "$ROOT_DIR/scripts/lerobot_train_lehome.py" --config-path "$tmp_config" --headless)
  log "🔥 正在启动 X-VLA 训练..."; log "📄 临时配置: $tmp_config"
  log "📦 数据集: $dataset_repo  📁 路径: $dataset_root"
  log "🗂️ 输出: $output_dir  ⏱️ 步数: $train_steps"
  if [[ "$dry_run" == "true" ]]; then
    log "🧪 DRY_RUN 已开启，未真正启动训练。"; log "🧾 最终命令: ${train_cmd[*]}"; return 0; fi
  "${train_cmd[@]}"
}

step_eval_xvla() {
  ensure_train_xvla_yaml
  ensure_project_root
  local cache_root="$ROOT_DIR/.cache"
  local hf_cache_root="$cache_root/huggingface"
  local torch_cache_root="$cache_root/torch"
  mkdir -p "$hf_cache_root/datasets" "$hf_cache_root/hub" "$hf_cache_root/transformers" "$torch_cache_root/hub/checkpoints"
  export XDG_CACHE_HOME="${XDG_CACHE_HOME:-$cache_root}"
  export HF_HOME="${HF_HOME:-$hf_cache_root}"
  export HF_DATASETS_CACHE="${HF_DATASETS_CACHE:-$hf_cache_root/datasets}"
  export HF_HUB_CACHE="${HF_HUB_CACHE:-$hf_cache_root/hub}"
  export HUGGINGFACE_HUB_CACHE="${HUGGINGFACE_HUB_CACHE:-$HF_HUB_CACHE}"
  export TRANSFORMERS_CACHE="${TRANSFORMERS_CACHE:-$hf_cache_root/transformers}"
  export TORCH_HOME="${TORCH_HOME:-$torch_cache_root}"
  activate_venv
  local config_template="$ROOT_DIR/configs/train_xvla.yaml"
  local output_dir; output_dir="$(python -c "import yaml,sys; d=yaml.safe_load(open(sys.argv[1])); print(d['output_dir'])" "$config_template")"
  local eval_policy_path="${XVLA_EVAL_POLICY_PATH:-${output_dir}/checkpoints/last/pretrained_model}"
  local dataset_root="${XVLA_DATASET_ROOT:-$(python -c "import yaml,sys; d=yaml.safe_load(open(sys.argv[1])); print(d['dataset']['root'])" "$config_template")}"
  local garment="${EVAL_GARMENT:-top_long}"
  local episodes="${EVAL_EPISODES:-5}"
  local task_desc="${EVAL_TASK_DESCRIPTION:-fold the garment on the table}"

  command -v xvfb-run >/dev/null 2>&1 || die "❌ 未找到 xvfb-run"
  [[ -d "$eval_policy_path" ]] || die "❌ 评估模型路径不存在: $eval_policy_path"
  log "🧪 正在启动 X-VLA 评估..."; log "📦 模型路径: $eval_policy_path"; log "📚 数据路径: $dataset_root"
  xvfb-run -a python -m scripts.eval \
    --policy_type lerobot --policy_path "$eval_policy_path" \
    --dataset_root "$dataset_root" --garment_type "$garment" \
    --num_episodes "$episodes" --task_description "$task_desc" \
    --enable_cameras --device cpu --headless
}

step_final_test() {
  ensure_project_root
  activate_venv
  local python_bin="$ROOT_DIR/.venv/bin/python"
  local lerobot_train_bin="$ROOT_DIR/.venv/bin/lerobot-train"
  local test_steps="${FINAL_TEST_STEPS:-1000}"
  local test_episodes="${FINAL_TEST_EPISODES:-1}"
  local timestamp output_dir tmp_config train_log eval_log policy_path
  timestamp="$(date +%m%d)"
  output_dir="$ROOT_DIR/outputs/train/act_${timestamp}_${test_steps}steps"
  tmp_config="$(mktemp /tmp/final_test_act_XXXX.yaml)"
  train_log="$LOG_DIR/${timestamp}_final_test_train.log"
  eval_log="$LOG_DIR/${timestamp}_final_test_eval.log"
  trap 'rm -f "$tmp_config"' EXIT

  [[ -x "$lerobot_train_bin" ]] || die "❌ 未找到 $lerobot_train_bin，请先完成环境准备。"
  [[ -f "$ROOT_DIR/configs/train_act.yaml" ]] || die "❌ 未找到 ACT 配置文件: $ROOT_DIR/configs/train_act.yaml"

  section "最终测试"
  kv "Policy" "ACT"
  kv "Train steps" "$test_steps"
  kv "Eval episodes" "$test_episodes"
  kv "Output dir" "${output_dir#$ROOT_DIR/}"

  "$python_bin" - "$ROOT_DIR/configs/train_act.yaml" "$tmp_config" "$output_dir" "$test_steps" <<'PY'
import sys
import yaml

src, dst, output_dir, steps = sys.argv[1], sys.argv[2], sys.argv[3], int(sys.argv[4])
with open(src, "r", encoding="utf-8") as f:
    cfg = yaml.safe_load(f)

cfg["steps"] = steps
cfg["save_freq"] = min(steps, cfg.get("save_freq", steps))
cfg["log_freq"] = min(cfg.get("log_freq", 1000), steps)
cfg["output_dir"] = output_dir
cfg.setdefault("wandb", {})
cfg["wandb"]["enable"] = False
cfg["wandb"]["mode"] = "disabled"
cfg.setdefault("dataset", {})
cfg["dataset"]["root"] = "Datasets/example/top_long_merged"
cfg["dataset"]["repo_id"] = "lehome/dataset_challenge_merged"

with open(dst, "w", encoding="utf-8") as f:
    yaml.safe_dump(cfg, f, sort_keys=False, allow_unicode=True)
PY

  cmd_preview "$lerobot_train_bin --config_path=$tmp_config"
  set +e
  "$lerobot_train_bin" --config_path="$tmp_config" 2>&1 | tee "$train_log"
  local train_status=${PIPESTATUS[0]}
  set -e
  [[ $train_status -eq 0 ]] || die "❌ 最终测试训练失败，请查看日志: ${train_log#$ROOT_DIR/}"

  policy_path="$output_dir/checkpoints/last/pretrained_model"
  [[ -d "$policy_path" ]] || die "❌ 最终测试训练完成，但未找到 checkpoint: ${policy_path#$ROOT_DIR/}"

  command -v xvfb-run >/dev/null 2>&1 || die "❌ 未找到 xvfb-run，请先安装。"
  cmd_preview "xvfb-run -a -s \"-screen 0 1024x768x24\" python -m scripts.eval --policy_type lerobot --policy_path $policy_path --dataset_root Datasets/example/top_long_merged --garment_type top_long --num_episodes $test_episodes --enable_cameras --device cpu --headless"
  set +e
  xvfb-run -a -s "-screen 0 1024x768x24" \
    python -m scripts.eval \
      --policy_type lerobot \
      --policy_path "$policy_path" \
      --dataset_root "Datasets/example/top_long_merged" \
      --garment_type "top_long" \
      --num_episodes "$test_episodes" \
      --enable_cameras \
      --device cpu \
      --headless \
    2>&1 | tee "$eval_log"
  local eval_status=${PIPESTATUS[0]}
  set -e
  [[ $eval_status -eq 0 ]] || die "❌ 最终测试评估失败，请查看日志: ${eval_log#$ROOT_DIR/}"
  ok "✅ 最终测试完成"
  style_dim "训练日志: ${train_log#$ROOT_DIR/}"
  style_dim "评估日志: ${eval_log#$ROOT_DIR/}"
}

# ── Step 处理函数：v1/versioning.sh ─────────────────────────────────────────
DEFAULT_GITHUB_USER="sudo-yf"
DEFAULT_GITHUB_EMAIL="$DEFAULT_GITHUB_USER@users.noreply.github.com"
VERSIONS_FILE="$ROOT_DIR/VERSIONS.md"

normalize_version_tag() {
  local raw="${1:-}"; [[ -n "$raw" ]] || die "❌ 版本号不能为空。"
  [[ "$raw" == v* ]] && printf '%s\n' "$raw" || printf 'v%s\n' "$raw"
}

configure_git_identity() {
  section "检查 Git 身份"
  if [[ -z "$(git config user.name || true)" ]]; then
    git config --global user.name "$DEFAULT_GITHUB_USER"
    git config --global user.email "$DEFAULT_GITHUB_EMAIL"
    ok "✅ 已设置 Git 身份: $DEFAULT_GITHUB_USER"
  else ok "✅ 当前 Git 身份: $(git config user.name)"; fi
}

ensure_personal_origin() {
  section "检查仓库远端"
  local main_origin; main_origin="$(git remote get-url origin 2>/dev/null || echo '')"
  if [[ "$main_origin" == *"lehome-official"* ]]; then
    warn "⚠️ 发现 origin 仍指向官方仓库，自动切换到个人仓库。"
    git remote set-url origin "https://github.com/$DEFAULT_GITHUB_USER/lehome-challenge.git"
    git remote add upstream "https://github.com/lehome-official/lehome-challenge.git" 2>/dev/null || true; fi
  ok "✅ origin: $(git remote get-url origin 2>/dev/null || echo '<missing>')"
}

step_git_basic_setup() {
  ensure_repo_root
  configure_git_identity
  ensure_personal_origin
}

collect_tag_rows() {
  git for-each-ref refs/tags --sort=-creatordate --format='%(refname:short)|%(creatordate:short)|%(objectname:short)|%(subject)'
}

render_versions_file() {
  local pending_tag="${1:-}" pending_date="${2:-}" pending_commit="${3:-}" pending_note="${4:-}"
  { echo "# Versions"; echo
    printf '%s\n' '本文件记录通过 `save` 流程创建的版本标签，便于快速检索。'; echo
    echo "| Version | Date | Commit | Notes |"; echo "| --- | --- | --- | --- |"
    [[ -n "$pending_tag" ]] && printf '| `%s` | `%s` | `%s` | %s |\n' "$pending_tag" "$pending_date" "$pending_commit" "${pending_note//|/ /}"
    collect_tag_rows | while IFS='|' read -r tag date commit subject; do
      [[ -n "$tag" ]] || continue
      [[ -n "$pending_tag" && "$tag" == "$pending_tag" ]] && continue
      printf '| `%s` | `%s` | `%s` | %s |\n' "$tag" "$date" "$commit" "${subject//|/ /}"; done
  } > "$VERSIONS_FILE"
}

show_versions() { ensure_repo_root; render_versions_file; section "版本索引"; cat "$VERSIONS_FILE"; }

save_version() {
  ensure_repo_root; ensure_path
  local version_arg="" note="" force_tag=false local_only=false
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --note) shift; [[ $# -gt 0 ]] || die "❌ --note 需要一个备注字符串。"; note="$1";;
      --force-tag) force_tag=true;;
      --local-only) local_only=true;;
      -h|--help) cat <<'USAGE'
Usage: save_version <version> [--note "备注"] [--force-tag] [--local-only]
USAGE
        return 0;;
      *) if [[ -z "$version_arg" ]]; then version_arg="$1"
         else [[ -n "$note" ]] && note+=" "; note+="$1"; fi;;
    esac; shift; done
  [[ -n "$version_arg" ]] || die "❌ 忘记写版本号啦！例如: save_version 3 xvla-wandb"
  local tag; tag="$(normalize_version_tag "$version_arg")"
  local today="$(date +%Y-%m-%d)" tag_subject="${note:-Auto save $tag}"
  local commit_msg="save: $tag"; [[ -n "$note" ]] && commit_msg+=" - $note"
  section "准备保存版本"; kv "Tag" "$tag"; kv "Note" "${note:-<none>}"
  kv "Local only" "$local_only"; kv "Force tag" "$force_tag"
  if git rev-parse "$tag" >/dev/null 2>&1; then
    [[ "$force_tag" != true ]] && die "❌ 标签 $tag 已存在。如需覆盖，请传 --force-tag。"
    warn "⚠️ 标签 $tag 已存在，将按要求覆盖。"; fi
  activate_venv || true; configure_git_identity; ensure_personal_origin
  section "提交当前工作区"; git add -A
  if git diff --cached --quiet; then
    warn "⚠️ 当前没有未提交改动，将创建一个空的版本提交。"; git commit --allow-empty -m "$commit_msg"
  else git commit -m "$commit_msg"; fi
  local commit_short; commit_short="$(git rev-parse --short HEAD)"
  section "写入版本标签"
  [[ "$force_tag" == true ]] && { git tag -d "$tag" >/dev/null 2>&1 || true; }
  git tag -a "$tag" -m "$tag_subject"; ok "✅ 已创建标签: $tag"
  section "刷新版本索引"; render_versions_file "$tag" "$today" "$commit_short" "$tag_subject"
  git add "$VERSIONS_FILE"
  if git diff --cached --quiet; then warn "⚠️ VERSIONS.md 无变化，跳过索引提交。"
  else git commit -m "docs: refresh version index for $tag" >/dev/null; ok "✅ 已更新版本索引提交"; fi
  if [[ "$local_only" != true ]]; then
    section "推送远端"; local branch; branch="$(git rev-parse --abbrev-ref HEAD)"
    cmd_preview "git push origin $branch"; git push origin "$branch"
    [[ "$force_tag" == true ]] && { git push origin ":refs/tags/$tag" >/dev/null 2>&1 || true; }
    cmd_preview "git push origin $tag"; git push origin "$tag"; ok "✅ 已推送分支与标签"
  else warn "⚠️ 已跳过远端推送（--local-only）"; fi
  section "版本保存完成"; kv "Tag" "$tag"; kv "Tagged commit" "$commit_short"; kv "Index" "$VERSIONS_FILE"
}

step_save_version() {
  local version="${SAVE_VERSION:-}"
  [[ -n "$version" ]] || { read -r -p "请输入版本号: " version; }
  local note="${SAVE_NOTE:-}"
  save_version "$version" ${note:+--note "$note"}
}

# ── Step 处理函数：v1/step_git.sh ────────────────────────────────────────────
step_git_save() {
  local version="${SAVE_VERSION:-}"
  [[ -n "$version" ]] || { read -r -p "请输入版本号: " version; }
  [[ -n "$version" ]] || die "❌ 错误: 忘记写版本号啦！"
  local target_dir="$ROOT_DIR" github_user="${DEFAULT_GITHUB_USER:-sudo-yf}"
  local github_email="${DEFAULT_GITHUB_EMAIL:-$github_user@users.noreply.github.com}"
  local ver="v$version" commit_msg="Auto save version v$version"
  echo "=================================================="; echo "🛡️  开始防夺舍检测与一键备份 (目标版本: v$version)"
  echo "=================================================="; cd "$target_dir"
  [[ -f ".venv/bin/activate" ]] && { source .venv/bin/activate; echo "✅ 虚拟环境已激活"; } || echo "⚠️ 警告: 未找到 .venv，将使用系统环境继续"
  echo "👤 [1/5] 检查 Git 身份配置..."
  if [[ -z "$(git config user.name)" ]]; then
    git config --global user.name "$github_user"; git config --global user.email "$github_email"
    echo "✅ 身份已设为: $github_user"
  else echo "✅ 身份已确认: $(git config user.name)"; fi
  echo "🔍 [2/5] 检查仓库归属..."
  local main_origin; main_origin="$(git remote get-url origin 2>/dev/null || echo '')"
  if [[ "$main_origin" == *"lehome-official"* ]]; then
    echo "⚠️ 发现仓库被官方占用，正在为你执行换源..."
    git remote set-url origin "https://github.com/$github_user/lehome-challenge.git"
    git remote add upstream "https://github.com/lehome-official/lehome-challenge.git" 2>/dev/null || true
  else echo "✅ 仓库归属正常"; fi
  echo "🚀 [3/5] 处理代码变动..."; git add .
  git commit -m "$commit_msg" || echo "💤 工作区无新变动，跳过 commit 阶段..."
  echo "🏷️  [4/5] 更新版本标签..."
  git tag -d "$ver" 2>/dev/null || true; git tag "$ver"
  echo "📤 [5/5] 推送到 GitHub ($github_user)..."
  local current_branch; current_branch="$(git rev-parse --abbrev-ref HEAD)"
  git push origin "$current_branch"; git push origin "$ver" -f
  echo "=================================================="; echo "🎉 完美收工！$ver 已包含最新环境状态并同步！"
}

# ── Step 处理函数：v1/step_vpn.sh ────────────────────────────────────────────
detect_clash_shell_home() {
  local shell_home="$HOME"
  if [[ "$(id -u)" -eq 0 && -n "${SUDO_USER:-}" && "${SUDO_USER}" != "root" ]]; then
    local sudo_home
    sudo_home="$(awk -F: -v user="$SUDO_USER" '$1==user{print $6}' /etc/passwd 2>/dev/null || true)"
    [[ -n "$sudo_home" ]] && shell_home="$sudo_home"
  fi
  printf '%s\n' "$shell_home"
}

cleanup_clash_shell_hooks() {
  local shell_home="$1"
  python - "$shell_home/.bashrc" "$shell_home/.zshrc" "$shell_home/.config/fish/conf.d/clashctl.fish" <<'PY'
from pathlib import Path
import re
import sys

start_flag = "# clashctl START"
end_flag = "# clashctl END"
block_re = re.compile(rf"\n?{re.escape(start_flag)}\n.*?{re.escape(end_flag)}\n?", re.S)
changed = []

for raw in sys.argv[1:]:
    path = Path(raw)
    if path.suffix == ".fish":
        if path.exists():
            path.unlink()
            changed.append(str(path))
        continue
    if not path.exists():
        continue
    text = path.read_text(encoding="utf-8")
    updated = block_re.sub("\n", text)
    filtered = []
    for line in updated.splitlines():
        stripped = line.strip()
        if stripped == "watch_proxy":
            continue
        if stripped.startswith(". ") and "clashctl.sh" in stripped:
            continue
        filtered.append(line)
    new_text = "\n".join(filtered)
    if updated.endswith("\n") or text.endswith("\n"):
        new_text += "\n"
    if new_text != text:
        path.write_text(new_text, encoding="utf-8")
        changed.append(str(path))

for item in changed:
    print(item)
PY
}

write_clash_shell_hooks() {
  local shell_home="$1"
  python - "$shell_home/.bashrc" "$shell_home/.zshrc" <<'PY'
from pathlib import Path
import sys

block = """# clashctl START
if [ -f "$HOME/clashctl/scripts/cmd/clashctl.sh" ]; then
  . "$HOME/clashctl/scripts/cmd/clashctl.sh"
  clashon >/dev/null 2>&1 || true
fi
# clashctl END
"""

for raw in sys.argv[1:]:
    path = Path(raw)
    if path.suffix == ".zshrc" and not path.exists():
        continue
    existing = path.read_text(encoding="utf-8") if path.exists() else ""
    body = existing.rstrip("\n")
    new_text = (body + "\n\n" if body else "") + block
    path.write_text(new_text, encoding="utf-8")
    print(str(path))
PY
}

step_vpn_setup() {
  ensure_project_root
  (
    cd "$ROOT_DIR"
    local shell_home shell_hook_output
    shell_home="$(detect_clash_shell_home)"
    trap - RETURN
    unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY
    unset all_proxy ALL_PROXY

    # --- 颜色定义 (让终端不再枯燥) ---
    GREEN='\033[0;32m'
    BLUE='\033[0;34m'
    YELLOW='\033[1;33m'
    RED='\033[0;31m'
    PURPLE='\033[0;35m'
    NC='\033[0m' # No Color

    echo -e "${PURPLE}==================================================${NC}"
    echo -e "${PURPLE}🌐  Clash-for-Linux 自动化部署工具 (学术加速增强版)${NC}"
    echo -e "${PURPLE}==================================================${NC}"

    # --- 1. 开启学术加速 ---
    echo -e "${BLUE}📡 [1/5] 正在激活学术加速通道...${NC}"
    if [ -f "/public/bin/network_accelerate" ]; then
      source /public/bin/network_accelerate
      echo -e "${GREEN}✅ 加速已开启，正在直连 GitHub...${NC}"
    else
      echo -e "${YELLOW}⚠️  提示: 未找到加速脚本，将尝试普通连接。${NC}"
    fi

    # --- 2. 克隆项目仓库 ---
    echo -e "${BLUE}🚚 [2/5] 正在拉取 clash-for-linux-install 仓库...${NC}"
    # 彻底清理旧目录，确保版本最干净
    rm -rf clash-for-linux-install
    git clone --branch master --depth 1 https://github.com/nelvko/clash-for-linux-install.git

    if [ ! -d "clash-for-linux-install" ]; then
      echo -e "${RED}❌ 错误: 仓库拉取失败，请检查网络连接！${NC}"
      exit 1
    fi
    cd clash-for-linux-install

    # --- 3. 环境配置与去代理化 ---
    echo -e "${BLUE}⚙️  [3/5] 正在优化安装配置 (移除 gh-proxy)...${NC}"

    # 移除安装脚本中的 gh-proxy 代理（既然开了学术加速，直连才是王道）
    # 使用 | 作为分隔符防止 URL 冲突
    sed -i 's|https://gh-proxy.org||g' install.sh 2>/dev/null || true

    echo -e "${BLUE}📝 [3/5] 正在注入自定义 .env 订阅配置...${NC}"
    cat <<EOF > .env
# 安装内核 可选：mihomo、clash
KERNEL_NAME=mihomo

# 安装路径
CLASH_BASE_DIR=~/clashctl

# 机场订阅地址
CLASH_CONFIG_URL=https://dash.knjc.cfd/api/v1/client/subscribe?token=a3bf795ae9fbca04056b2d4e74a38ac0

# 下载订阅时的 UserAgent
CLASH_SUB_UA=clash-verge/v2.4.0

# 软件版本号锁定
VERSION_MIHOMO=v1.19.17
VERSION_YQ=v4.49.2
VERSION_SUBCONVERTER=v0.9.0

# 控制面板相关
URL_CLASH_UI=http://board.zash.run.place
ZIP_UI=resources/zip/dist.zip
URL_GH_PROXY=
EOF
    echo -e "${GREEN}✅ .env 配置写入完成。${NC}"

    # --- 4. 执行正式安装 ---
    echo -e "${YELLOW}🚀 [4/5] 正在启动正式安装程序，请稍候...${NC}"
    echo -e "${YELLOW}------------------------------------------------${NC}"
    bash install.sh
    echo -e "${YELLOW}------------------------------------------------${NC}"

    cleanup_clash_shell_hooks "$shell_home" >/dev/null 2>&1 || true
    shell_hook_output="$(write_clash_shell_hooks "$shell_home" || true)"
    if [[ -n "$shell_hook_output" ]]; then
      echo -e "${GREEN}✅ 已写入默认代理 shell 配置。${NC}"
    fi

    # --- 5. 关闭学术加速 ---
    echo -e "${BLUE}🧹 [5/5] 安装完毕，正在清理学术加速状态...${NC}"
    if [ -f "/public/bin/network_accelerate_stop" ]; then
      source /public/bin/network_accelerate_stop
      echo -e "${GREEN}✅ 加速已关闭，回归标准网络环境。${NC}"
    fi

    # --- 最终指引 ---
    echo -e "${PURPLE}==================================================${NC}"
    echo -e "${GREEN}🎉 代理部署大功告成！${NC}"
    echo -e "${BLUE}🌐 管理面板：${YELLOW}http://board.zash.run.place${NC}"
    echo -e "${BLUE}🔌 本地端口：${YELLOW}7890${NC}"
    echo -e "${BLUE}📂 安装目录：${YELLOW}~/clashctl${NC}"
    echo -e "${PURPLE}==================================================${NC}"
    echo -e "${YELLOW}💡 提示: 记得在终端输入 'proxy_on' (如果安装脚本提供了此别名) 开启代理。${NC}"
  )
}

# ── Step 处理函数：v1/ai.sh ──────────────────────────────────────────────────
step_ai_tools() {
  echo -e "\033[0;32m=== 🚀 开始全自动配置 AI 编程环境 ===\033[0m"
  if ! command -v node &>/dev/null; then
    if command -v conda &>/dev/null; then conda install -c conda-forge nodejs=20 -y
  else die "错误: 未检测到 Conda 环境。请先安装 Conda。"; fi
  else echo -e "Node.js 已安装: \033[0;32m$(node -v)\033[0m"; fi
  npm config set registry https://registry.npmmirror.com
  npm install -g @anthropic-ai/claude-code @openai/codex zcf happy-coder
  echo -e "\033[0;32mAI 工具链安装完成！\033[0m"
  local config_heartbeat="n"
  if [[ "$ASSUME_YES" -eq 0 ]]; then
    ui_confirm "是否需要配置 Tmux + 定时心跳保活机制？" 0 && config_heartbeat="y"
  fi
  if [[ "$config_heartbeat" =~ ^[Yy]$ ]]; then
    echo -e "\n\033[0;32m正在配置保活机制...\033[0m"
    if ! command -v tmux &>/dev/null; then
      echo "未检测到 tmux，正在安装..."
      if command -v conda &>/dev/null; then
        conda install -c conda-forge tmux -y
      else
        die "错误: 未检测到 Conda 环境，无法自动安装 tmux。"
      fi
    fi
    local heartbeat_file="$ROOT_DIR/heartbeat.sh"
    cat >"$heartbeat_file" <<'EOF'
#!/bin/bash
tmux send-keys -t ai_session "进行常规的代码检查和状态汇报" C-m
EOF
    chmod +x "$heartbeat_file"
    (crontab -l 2>/dev/null | grep -v "$heartbeat_file"; echo "0 * * * * $heartbeat_file") | crontab -
    echo -e "\033[0;32mCrontab 定时任务配置成功！(每小时触发一次)\033[0m"
    echo -e "\033[1;33m1. 输入 tmux new -s ai_session 启动后台会话\033[0m"
    echo -e "\033[1;33m2. 在该会话里启动 claude / codex 等 Agent\033[0m"
    echo -e "\033[1;33m3. Ctrl+B 再按 D 可挂到后台\033[0m"
    echo -e "\033[1;33m4. 回来查看时输入 tmux attach -t ai_session\033[0m"
  else
    echo "已跳过心跳保活机制的配置。"
  fi
}

step_train_guide() {
  ensure_project_root
  local python_bin="$ROOT_DIR/.venv/bin/python"
  local lerobot_train_bin="$ROOT_DIR/.venv/bin/lerobot-train"
  local train_bridge="$ROOT_DIR/scripts/lerobot_train_lehome.py"
  [[ -x "$python_bin" ]] || die "❌ 未找到 $python_bin，请先完成环境准备。"
  activate_venv
  ensure_isaaclab_deps

  echo "================================================"
  echo "  LeHome 训练参数配置向导"
  echo "  使用光标选择或手动输入"
  echo "================================================"
  echo ""

  wizard_section "基础配置"
  local base_config
  local base_config_default="${TRAIN_BASE_CONFIG:-configs/train_act.yaml}"
  local available_configs=()

  for cfg in "$ROOT_DIR/configs"/train_*.yaml; do
    [[ -f "$cfg" ]] && available_configs+=("${cfg#$ROOT_DIR/}")
  done

  if [[ ${#available_configs[@]} -gt 0 ]]; then
    wizard_prompt_with_options "Base config" "$base_config_default" base_config "${available_configs[@]}"
  else
    wizard_prompt_value "Base config" "$base_config_default" "$base_config_default" base_config
  fi

  [[ -f "$ROOT_DIR/$base_config" || -f "$base_config" ]] || die "❌ 配置文件不存在: $base_config"
  [[ -f "$ROOT_DIR/$base_config" ]] && base_config="$ROOT_DIR/$base_config"

  # 从配置文件名提取策略类型（如 train_act.yaml -> act）
  local policy_type
  policy_type="$(basename "$base_config" .yaml | sed 's/^train_//')"

  wizard_section "训练超参数"
  local steps batch_size save_freq log_freq
  wizard_prompt_with_options "Steps" "30000" steps "1000" "5000" "10000" "30000" "50000" "100000"

  # 根据策略类型和步数生成默认输出目录
  local default_output="${XVLA_OUTPUT_DIR:-outputs/train/${policy_type}_$(date +%m%d)_${steps}steps}"
  local output_dir dataset_root
  wizard_prompt_value "Output dir" "$default_output" "$default_output" output_dir

  local dataset_default="${XVLA_DATASET_ROOT:-${EVAL_DATASET_ROOT:-Datasets/example/top_long_merged}}"
  local available_datasets=()
  if [[ -d "$ROOT_DIR/Datasets/example" ]]; then
    for ds in "$ROOT_DIR/Datasets/example"/*; do
      [[ -d "$ds" ]] && available_datasets+=("${ds#$ROOT_DIR/}")
    done
  fi

  if [[ ${#available_datasets[@]} -gt 0 ]]; then
    wizard_prompt_with_options "Dataset path" "$dataset_default" dataset_root "${available_datasets[@]}"
  else
    wizard_prompt_value "Dataset path" "$dataset_default" "$dataset_default" dataset_root
  fi

  wizard_prompt_with_options "Batch size" "8" batch_size "4" "8" "16" "32"
  wizard_prompt_with_options "Save freq" "5000" save_freq "1000" "2500" "5000" "10000"
  wizard_prompt_with_options "Log freq" "1000" log_freq "100" "500" "1000" "2000"

  wizard_section "WandB 配置"
  local wandb_enable wandb_project="" wandb_entity=""
  wizard_prompt_with_options "Enable WandB" "n" wandb_enable "n" "y"
  if [[ "$wandb_enable" =~ ^[Yy] ]]; then
    wizard_prompt_value "WandB project" "lehome_xvla" "lehome_xvla" wandb_project
    wizard_prompt_optional "WandB entity" "leave empty" wandb_entity
  fi

  local timestamp gen_config
  timestamp="$(date +%m%d_%H%M%S)"
  gen_config="$ROOT_DIR/configs/train_generated_${timestamp}.yaml"

  echo
  echo "正在生成配置文件..."
  "$python_bin" - <<PYEOF
import yaml

with open(r"$base_config", "r", encoding="utf-8") as f:
    cfg = yaml.safe_load(f)

cfg["output_dir"] = r"$output_dir"
cfg["steps"] = int("$steps")
cfg["batch_size"] = int("$batch_size")
cfg["save_freq"] = int("$save_freq")
cfg["log_freq"] = int("$log_freq")

cfg.setdefault("dataset", {})
cfg["dataset"]["root"] = r"$dataset_root"

cfg.setdefault("wandb", {})
wandb_on = "$wandb_enable".lower().startswith("y")
cfg["wandb"]["enable"] = wandb_on
if wandb_on:
    cfg["wandb"]["project"] = r"$wandb_project"
    if r"$wandb_entity":
        cfg["wandb"]["entity"] = r"$wandb_entity"
    cfg["wandb"]["mode"] = "online"
else:
    cfg["wandb"]["mode"] = "disabled"

with open(r"$gen_config", "w", encoding="utf-8") as f:
    yaml.safe_dump(cfg, f, sort_keys=False, allow_unicode=True)
PYEOF

  build_train_command_for_config "$gen_config" "$python_bin" "$lerobot_train_bin" "$train_bridge"
  [[ "$TRAIN_POLICY_TYPE" == "xvla" ]] && {
    [[ -f "$train_bridge" ]] || die "❌ 未找到 $train_bridge，请先确认训练桥脚本存在。"
    maybe_login_hf
  }

  local cmd log_file policy_type
  cmd="${TRAIN_CMD[*]}"
  policy_type="${TRAIN_POLICY_TYPE:-unknown}"
  log_file="$(generate_log_path "train" "${policy_type}-${steps}steps")"

  echo
  echo "================================================"
  echo "  ${GREEN}配置摘要${NC}"
  echo "================================================"
  printf "  %-18s ${WHITE}%s${NC}\n" "Base config:" "${base_config#$ROOT_DIR/}"
  printf "  %-18s ${WHITE}%s${NC}\n" "Generated config:" "${gen_config#$ROOT_DIR/}"
  printf "  %-18s ${WHITE}%s${NC}\n" "Policy type:" "$policy_type"
  printf "  %-18s ${WHITE}%s${NC}\n" "Dataset path:" "$dataset_root"
  printf "  %-18s ${WHITE}%s${NC}\n" "Output dir:" "$output_dir"
  printf "  %-18s ${WHITE}%s${NC}\n" "Steps:" "$steps"
  printf "  %-18s ${WHITE}%s${NC}\n" "Batch size:" "$batch_size"
  printf "  %-18s ${WHITE}%s${NC}\n" "Save freq:" "$save_freq"
  printf "  %-18s ${WHITE}%s${NC}\n" "Log freq:" "$log_freq"
  printf "  %-18s ${WHITE}%s${NC}\n" "WandB:" "$wandb_enable"
  [[ "$wandb_enable" =~ ^[Yy] ]] && printf "  %-18s ${WHITE}%s${NC}\n" "WandB project:" "$wandb_project"
  echo "================================================"
  echo "  ${YELLOW}训练命令${NC}"
  echo "================================================"

  # 美化命令显示：根据 policy_type 展开参数
  if [[ "$policy_type" == "xvla" ]]; then
    echo "  cd $ROOT_DIR && \\"
    echo "  nohup $python_bin $train_bridge \\"
    echo "    --config-path=\"$gen_config\" \\"
    echo "    --headless \\"
    echo "    > ${log_file#$ROOT_DIR/} 2>&1 &"
  else
    echo "  cd $ROOT_DIR && \\"
    echo "  nohup $lerobot_train_bin \\"
    echo "    --config_path=\"$gen_config\" \\"
    echo "    > ${log_file#$ROOT_DIR/} 2>&1 &"
  fi
  echo "  echo \"训练已启动，PID: \$!\""
  echo "================================================"
  echo ""

  if ! ui_confirm "确认以上配置并开始训练？" 0; then
    warn "已取消训练"
    return 0
  fi

  (
    cd "$ROOT_DIR"
    nohup "${TRAIN_CMD[@]}" >"$log_file" 2>&1 &
    local pid=$!
    ok "✅ 训练已启动，PID: $pid"
    style_dim "日志文件: ${log_file#$ROOT_DIR/}"
    style_dim "查看日志: tail -f ${log_file#$ROOT_DIR/}"
  )
}

step_eval_guide() {
  ensure_project_root
  activate_venv
  ensure_isaaclab_deps

  # 设置缓存环境变量（参考 step_eval）
  local cache_root="$ROOT_DIR/.cache"
  local hf_cache_root="$cache_root/huggingface"
  local torch_cache_root="$cache_root/torch"
  mkdir -p "$hf_cache_root/datasets" "$hf_cache_root/hub" "$hf_cache_root/transformers" "$torch_cache_root/hub/checkpoints"
  export XDG_CACHE_HOME="${XDG_CACHE_HOME:-$cache_root}"
  export HF_HOME="${HF_HOME:-$hf_cache_root}"
  export HF_DATASETS_CACHE="${HF_DATASETS_CACHE:-$hf_cache_root/datasets}"
  export HF_HUB_CACHE="${HF_HUB_CACHE:-$hf_cache_root/hub}"
  export HUGGINGFACE_HUB_CACHE="${HUGGINGFACE_HUB_CACHE:-$HF_HUB_CACHE}"
  export TRANSFORMERS_CACHE="${TRANSFORMERS_CACHE:-$hf_cache_root/transformers}"
  export TORCH_HOME="${TORCH_HOME:-$torch_cache_root}"

  # 激活虚拟环境
  activate_venv

  local python_bin="$ROOT_DIR/.venv/bin/python"
  [[ -x "$python_bin" ]] || die "❌ 未找到 $python_bin，请先完成环境准备。"

  echo "================================================"
  echo "  LeHome 评估参数配置向导"
  echo "  使用光标选择或手动输入"
  echo "================================================"
  echo ""

  wizard_section "策略配置"
  local policy_type policy_path
  wizard_prompt_with_options "Policy type" "lerobot" policy_type "lerobot" "custom"

  local policy_default="${EVAL_POLICY_PATH:-${XVLA_EVAL_POLICY_PATH:-outputs/train/xvla_30k/checkpoints/last/pretrained_model}}"
  local available_policies=()
  if [[ -d "$ROOT_DIR/outputs/train" ]]; then
    for policy_dir in "$ROOT_DIR/outputs/train"/*/checkpoints/last/pretrained_model; do
      [[ -d "$policy_dir" ]] && available_policies+=("${policy_dir#$ROOT_DIR/}")
    done
  fi

  if [[ ${#available_policies[@]} -gt 0 ]]; then
    wizard_prompt_with_options "Policy path" "$policy_default" policy_path "${available_policies[@]}"
  else
    wizard_prompt_value "Policy path" "$policy_default" "$policy_default" policy_path
  fi

  wizard_section "衣物与数据"
  local garment_type dataset_root
  wizard_prompt_with_options "Garment type" "top_long" garment_type "top_long" "top_short" "pant_long" "pant_short" "custom"

  local dataset_default="${EVAL_DATASET_ROOT:-${XVLA_DATASET_ROOT:-Datasets/example/top_long_merged}}"
  local available_datasets=()
  if [[ -d "$ROOT_DIR/Datasets/example" ]]; then
    for ds in "$ROOT_DIR/Datasets/example"/*; do
      [[ -d "$ds" ]] && available_datasets+=("${ds#$ROOT_DIR/}")
    done
  fi

  if [[ ${#available_datasets[@]} -gt 0 ]]; then
    wizard_prompt_with_options "Dataset path" "$dataset_default" dataset_root "${available_datasets[@]}"
  else
    wizard_prompt_value "Dataset path" "$dataset_default" "$dataset_default" dataset_root
  fi

  wizard_section "评估参数"
  local num_episodes device seed use_random_seed task_desc=""
  wizard_prompt_with_options "Episodes" "5" num_episodes "1" "5" "10" "24" "50" "100"
  wizard_prompt_with_options "Device" "cpu" device "cpu" "cuda" "cuda:0" "cuda:1"
  wizard_prompt_with_options "Seed" "42" seed "42" "123" "456" "789"
  wizard_prompt_with_options "Random seed" "n" use_random_seed "n" "y"

  # Task description 仅用于 VLA 模型（参考 docs/policy_eval.md）
  if [[ "$policy_type" =~ vla ]] || [[ "$policy_path" =~ vla ]]; then
    wizard_prompt_value "Task desc" "fold the garment on the table" "fold the garment on the table" task_desc
  fi

  wizard_section "运行模式"
  local headless enable_cameras
  wizard_prompt_with_options "Headless" "Y" headless "Y" "n"
  wizard_prompt_with_options "Cameras" "Y" enable_cameras "Y" "n"

  wizard_section "录制选项"
  local save_video video_dir="" save_datasets eval_dataset_path=""
  wizard_prompt_with_options "Save video" "n" save_video "n" "y"
  if [[ "$save_video" =~ ^[Yy] ]]; then
    wizard_prompt_value "Video dir" "outputs/eval_videos" "outputs/eval_videos" video_dir
  fi
  wizard_prompt_with_options "Save datasets" "n" save_datasets "n" "y"
  if [[ "$save_datasets" =~ ^[Yy] ]]; then
    wizard_prompt_value "Eval dataset" "Datasets/eval" "Datasets/eval" eval_dataset_path
  fi

  local cmd log_file xvfb_prefix=""

  # 无头评估使用 xvfb-run 前缀（参考 AGENTS.md）
  if [[ "$headless" =~ ^[Yy] ]]; then
    xvfb_prefix="xvfb-run -a -s \"-screen 0 1024x768x24\""
  fi

  cmd="$python_bin -u -m scripts.eval"
  cmd+=" --policy_type \"$policy_type\""
  cmd+=" --policy_path \"$policy_path\""
  cmd+=" --garment_type \"$garment_type\""
  cmd+=" --dataset_root \"$dataset_root\""
  cmd+=" --num_episodes $num_episodes"
  cmd+=" --device $device"
  cmd+=" --seed $seed"
  # Task description 仅用于 VLA 模型
  [[ -n "$task_desc" ]] && cmd+=" --task_description \"$task_desc\""
  [[ "$use_random_seed" =~ ^[Yy] ]] && cmd+=" --use_random_seed"
  [[ "$headless" =~ ^[Yy] ]] && cmd+=" --headless"
  [[ "$enable_cameras" =~ ^[Yy] ]] && cmd+=" --enable_cameras"
  [[ "$save_video" =~ ^[Yy] ]] && cmd+=" --save_video --video_dir \"$video_dir\""
  [[ "$save_datasets" =~ ^[Yy] ]] && cmd+=" --save_datasets --eval_dataset_path \"$eval_dataset_path\""

  log_file="$(generate_log_path "eval" "${policy_type}-${garment_type}")"

  echo
  echo "================================================"
  echo "  ${GREEN}评估配置摘要${NC}"
  echo "================================================"
  printf "  %-18s ${WHITE}%s${NC}\n" "Policy type:" "$policy_type"
  printf "  %-18s ${WHITE}%s${NC}\n" "Policy path:" "$policy_path"
  printf "  %-18s ${WHITE}%s${NC}\n" "Garment type:" "$garment_type"
  printf "  %-18s ${WHITE}%s${NC}\n" "Dataset path:" "$dataset_root"
  printf "  %-18s ${WHITE}%s${NC}\n" "Episodes:" "$num_episodes"
  printf "  %-18s ${WHITE}%s${NC}\n" "Device:" "$device"
  printf "  %-18s ${WHITE}%s${NC}\n" "Seed:" "$seed"
  [[ -n "$task_desc" ]] && printf "  %-18s ${WHITE}%s${NC}\n" "Task desc:" "$task_desc"
  printf "  %-18s ${WHITE}%s${NC}\n" "Random seed:" "$use_random_seed"
  printf "  %-18s ${WHITE}%s${NC}\n" "Headless:" "$headless"
  printf "  %-18s ${WHITE}%s${NC}\n" "Cameras:" "$enable_cameras"
  printf "  %-18s ${WHITE}%s${NC}\n" "Save video:" "$save_video"
  [[ "$save_video" =~ ^[Yy] ]] && printf "  %-18s ${WHITE}%s${NC}\n" "Video dir:" "$video_dir"
  printf "  %-18s ${WHITE}%s${NC}\n" "Save datasets:" "$save_datasets"
  [[ "$save_datasets" =~ ^[Yy] ]] && printf "  %-18s ${WHITE}%s${NC}\n" "Eval dataset:" "$eval_dataset_path"
  echo "================================================"
  echo "  ${YELLOW}评估命令${NC}"
  echo "================================================"
  echo "  cd $ROOT_DIR && \\"
  if [[ "$headless" =~ ^[Yy] ]]; then
    echo "  nohup xvfb-run -a -s \"-screen 0 1024x768x24\" \\"
    echo "    $python_bin -u -m scripts.eval \\"
  else
    echo "  nohup $python_bin -u -m scripts.eval \\"
  fi
  echo "    --policy_type \"$policy_type\" \\"
  echo "    --policy_path \"$policy_path\" \\"
  echo "    --garment_type \"$garment_type\" \\"
  echo "    --dataset_root \"$dataset_root\" \\"
  echo "    --num_episodes $num_episodes \\"
  echo "    --device $device \\"
  echo "    --seed $seed \\"
  [[ -n "$task_desc" ]] && echo "    --task_description \"$task_desc\" \\"
  [[ "$use_random_seed" =~ ^[Yy] ]] && echo "    --use_random_seed \\"
  [[ "$headless" =~ ^[Yy] ]] && echo "    --headless \\"
  [[ "$enable_cameras" =~ ^[Yy] ]] && echo "    --enable_cameras \\"
  [[ "$save_video" =~ ^[Yy] ]] && echo "    --save_video --video_dir \"$video_dir\" \\"
  [[ "$save_datasets" =~ ^[Yy] ]] && echo "    --save_datasets --eval_dataset_path \"$eval_dataset_path\" \\"
  echo "    > ${log_file#$ROOT_DIR/} 2>&1 &"
  echo "  echo \"评估已启动，PID: \$!\""
  echo "================================================"
  echo ""

  if ! ui_confirm "确认以上配置并开始评估？" 0; then
    warn "已取消评估"
    return 0
  fi

  (
    cd "$ROOT_DIR"
    # shellcheck disable=SC2086
    if [[ -n "$xvfb_prefix" ]]; then
      eval "nohup $xvfb_prefix $cmd > \"$log_file\" 2>&1 &"
    else
      eval "nohup $cmd > \"$log_file\" 2>&1 &"
    fi
    local pid=$!
    ok "✅ 评估已启动，PID: $pid"
    style_dim "日志文件: ${log_file#$ROOT_DIR/}"
    style_dim "查看日志: tail -f ${log_file#$ROOT_DIR/}"
  )
}

# ── Step 处理函数：diff.sh ───────────────────────────────────────────────────
step_diff() {
  local repo="$ROOT_DIR" log_file="$ROOT_DIR/lehome-challenge/diff.log" upstream="upstream/main"
  cd "$repo" || die "❌ 无法进入项目目录: $repo"
  if ! git remote | grep -q "^upstream$"; then
    git remote add upstream https://github.com/lehome-official/lehome-challenge.git; fi
  git fetch upstream --quiet
  { echo "生成时间: $(date '+%Y-%m-%d %H:%M:%S')"; echo ""
    echo "【M 修改】"; git diff "$upstream" HEAD --name-status | grep "^M" || echo "  (无)"; echo ""
    echo "【A 新增】"; git diff "$upstream" HEAD --name-status | grep "^A" || echo "  (无)"; echo ""
    echo "【D 删除】"; git diff "$upstream" HEAD --name-status | grep "^D" || echo "  (无)"; echo ""
    echo "【修改详情】"; echo "----------------------------------------"
    git diff "$upstream" HEAD --diff-filter=M -- scripts/ source/ | while IFS= read -r line; do
      if [[ "$line" =~ ^"diff --git" ]]; then echo ""; echo ">>> ${line#diff --git a/}"
      elif [[ "$line" =~ ^@@ ]]; then echo "$line"
      elif [[ "$line" =~ ^\+\+\+ ]] || [[ "$line" =~ ^"---" ]] || [[ "$line" =~ ^"index" ]]; then :
      elif [[ "$line" =~ ^\+ ]]; then echo "新增: ${line:1}"
      elif [[ "$line" =~ ^\- ]]; then echo "删除: ${line:1}"; fi; done
  } > "$log_file"
  ok "✅ diff 日志已更新: ${log_file#$ROOT_DIR/}"
}

# ── register_steps ────────────────────────────────────────────────────────────
register_steps() {
  # ── 官方安装步骤 (env.sh 原有) ──
  add_step "install_system_libs"       "安装系统图形依赖"         "official" "high"   "1" "step_install_system_libs"       "apt install -y libglu1-mesa libgl1 ..."                    "服务器环境依赖，来自 docs/installation.md"
  add_step "set_glx_vendor"            "设置 NVIDIA GLX 变量"     "official" "low"    "1" "step_set_glx_vendor"            "export __GLX_VENDOR_LIBRARY_NAME=nvidia"                   "来自 docs/installation.md"
  add_step "uv_sync"                   "同步 Python 依赖"         "official" "medium" "1" "step_uv_sync"                   "uv sync"                                                   "来自 docs/installation.md"
  add_step "clone_isaaclab"            "克隆 IsaacLab"            "official" "low"    "1" "step_clone_isaaclab"            "git clone .../IsaacLab.git third_party/IsaacLab"           "来自 docs/installation.md"
  add_step "install_isaaclab"          "安装 IsaacLab"            "official" "medium" "1" "step_install_isaaclab"          "source .venv/bin/activate && ./third_party/IsaacLab/isaaclab.sh -i none" "来自 docs/installation.md"
  add_step "install_lehome_pkg"        "安装 LeHome 包"           "official" "medium" "1" "step_install_lehome_pkg"        "uv pip install -e ./source/lehome"                         "来自 docs/installation.md"
  add_step "download_assets"           "下载仿真资产"             "official" "medium" "1" "step_download_assets"           "hf download lehome/asset_challenge ..."                    "来自 docs/readme.md"
  add_step "download_example_dataset"  "下载示例数据集"           "official" "medium" "1" "step_download_example_dataset"  "hf download lehome/dataset_challenge_merged ..."           "来自 docs/readme.md"
  # ── 增强安装步骤 (env.sh 原有) ──
  add_step "install_uv_if_missing"     "安装 uv（缺失时）"        "enhanced" "medium" "0" "step_install_uv_if_missing"     "curl -LsSf https://astral.sh/uv/install.sh | sh"           "非官方增强：自动安装 uv"
  add_step "install_hf_cli"            "安装 HuggingFace CLI"     "enhanced" "medium" "0" "step_install_hf_cli"            "uv pip install \"huggingface_hub[cli]\""                   "非官方增强：确保 hf 命令可用"
  add_step "link_uv_cache"             "配置 uv 缓存软链"         "enhanced" "high"   "0" "step_link_uv_cache"             "ln -sf /root/data/.uv_cache ~/.cache/uv"                   "非官方增强：迁移缓存到数据盘"
  add_step "setup_isaacsim_symlink"    "配置 IsaacSim 软链"       "enhanced" "medium" "0" "step_setup_isaacsim_symlink"    "python -c \"import isaacsim\" && ln -sf <path> _isaac_sim" "非官方增强：补齐 IsaacLab 与 IsaacSim 软链"
  add_step "write_shell_shortcuts"     "写入 shell 快捷配置"      "enhanced" "high"   "0" "step_write_shell_shortcuts"     "write ~/.bashrc block"                                     "非官方增强：仅在 --write-bashrc 时执行"
  # ── v1 新增步骤 ──
  add_step "prepare_full"              "完整环境准备（v1 流程）"  "enhanced" "high"   "0" "step_prepare_full"              "uv sync --locked && isaaclab.sh -i none && ..."            "v1/prepare.sh：完整安装流程含导入校验"
  add_step "data_download"             "下载数据资源（v1 流程）"  "enhanced" "medium" "0" "step_data_download"             "hf download assets + dataset_challenge_merged"             "v1/data.sh：下载资产与数据集，可选完整版"
  add_step "eval"                      "运行评估"                 "enhanced" "medium" "0" "step_eval"                      "xvfb-run python -m scripts.eval ..."                       "v1/eval.sh：支持 act/diffusion/smolvla/xvla"
  add_step "train_guide"               "交互式训练向导"           "enhanced" "medium" "0" "step_train_guide"               "内置训练向导"                                              "all.sh 内置：交互式训练参数配置向导"
  add_step "eval_guide"                "交互式评估向导"           "enhanced" "medium" "0" "step_eval_guide"                "内置评估向导"                                              "all.sh 内置：交互式评估参数配置向导"
  add_step "final_test"                "最终测试（ACT 1000步）"   "enhanced" "medium" "0" "step_final_test"                "lerobot-train --config_path=<tmp_act_test.yaml> && xvfb-run python -m scripts.eval ..." "环境就绪后执行 ACT 1000 步训练并做一次评估"
  add_step "train_xvla"                "训练 X-VLA"               "enhanced" "high"   "0" "step_train_xvla"                "lerobot-train --config_path=<tmp_config>"                  "v1/xvla.sh：从 configs/train_xvla.yaml 生成临时配置并训练"
  add_step "eval_xvla"                 "评估 X-VLA"               "enhanced" "medium" "0" "step_eval_xvla"                 "xvfb-run python -m scripts.eval (xvla)"                    "v1/xvla.sh：评估 X-VLA checkpoint"
  add_step "git_basic_setup"           "Git 基础配置"             "enhanced" "medium" "0" "step_git_basic_setup"           "git config --global user.name ... && git remote set-url ..." "初始化 Git 身份并校正仓库远端"
  add_step "save_version"              "保存版本（versioning）"   "enhanced" "high"   "0" "step_save_version"              "git commit + tag + push"                                   "v1/versioning.sh：提交、打 tag、推送远端"
  add_step "git_save"                  "Git 快速保存（step_git）" "enhanced" "high"   "0" "step_git_save"                  "git add . && git commit && git tag && git push"            "v1/step_git.sh：一键备份到 GitHub"
  add_step "vpn_setup"                 "配置 VPN（Clash）"        "enhanced" "high"   "0" "step_vpn_setup"                 "git clone clash-for-linux-install && bash install.sh"      "v1/step_vpn.sh：部署 Clash 代理"
  add_step "ai_tools"                  "安装 AI 工具链"           "enhanced" "medium" "0" "step_ai_tools"                  "npm install -g claude-code codex zcf happy-coder"          "v1/ai.sh：安装 Claude Code / Codex / ZCF"
  add_step "diff"                      "生成 diff 日志"           "enhanced" "low"    "0" "step_diff"                      "git diff upstream/main HEAD > diff.log"                    "diff.sh：对比 upstream/main 生成变更报告"
}

# ── main_menu ─────────────────────────────────────────────────────────────────
main_menu() {
  while true; do
    local choice
    if [[ "$HAS_GUM" -eq 1 ]]; then
      choice="$(ui_choose_numbered_option "$(show_compact_main_menu_header)" \
        "1. 一路配置向导" \
        "2. VPN 与代理配置" \
        "3. 环境准备" \
        "4. 数据资源管理" \
        "5. AI 开发工具链" \
        "6. github配置与Shell 快捷命令" \
        "7. X-VLA 工作台" \
        "8. 退出")"
    else
      echo
      show_banner
      choice="$(ui_choose_numbered_option "" \
        "1. 一路配置向导" \
        "2. VPN 与代理配置" \
        "3. 环境准备" \
        "4. 数据资源管理" \
        "5. AI 开发工具链" \
        "6. github配置与Shell 快捷命令" \
        "7. X-VLA 工作台" \
        "8. 退出")"
    fi
    case "$choice" in
      1) run_guided_flow_wizard || return 1 ;;
      2) run_module_vpn || return 1 ;;
      3) run_module_environment_setup || return 1 ;;
      4) run_module_data_resources || return 1 ;;
      5) run_module_ai_tools || return 1 ;;
      6) run_module_dev_tools || return 1 ;;
      7) run_module_xvla_workspace || return 1 ;;
      8|"") style_dim "退出。"; return 0 ;;
      *) style_warn "无效选项: $choice" ;;
    esac; done
}

# ── print_usage ───────────────────────────────────────────────────────────────
print_usage() {
  cat <<'USAGE'
Usage: bash all.sh [options]

Options:
  --mode confirm|auto     执行模式 (default: confirm)
  --yes, -y               自动模式，跳过确认
  --dry-run               仅打印步骤，不执行
  --list                  打印步骤列表并退出
  --step <step_id>        执行指定步骤（可重复）
  --write-bashrc          允许写入 ~/.bashrc
  --log-file <path>       自定义日志路径
  -h, --help              显示帮助

环境变量（控制各步骤行为）:
  PREPARE_INSTALL_SYSTEM_LIBS=1   prepare_full 步骤安装系统库
  DATA_WITH_FULL_DATASET=1        data_download 步骤下载完整数据集
  EVAL_MODEL=xvla                 eval 步骤使用的模型 (act/diffusion/smolvla/xvla)
  EVAL_GARMENT=top_long           评估衣物类型
  EVAL_EPISODES=5                 评估 episode 数量
  EVAL_POLICY_PATH=<path>         评估模型路径
  EVAL_DATASET_ROOT=<path>        评估数据集路径
  XVLA_DATASET_REPO=<repo>        X-VLA 训练数据集仓库
  XVLA_DATASET_ROOT=<path>        X-VLA 训练数据集路径
  XVLA_OUTPUT_DIR=<path>          X-VLA 训练输出目录
  XVLA_TRAIN_STEPS=<n>            X-VLA 训练步数
  XVLA_DRY_RUN=true               X-VLA 训练 dry-run 模式
  WANDB_ENABLE=true               启用 WandB
  WANDB_PROJECT=<project>         WandB 项目名
  SAVE_VERSION=<version>          保存版本号
  SAVE_NOTE=<note>                保存版本备注
  CLASH_CONFIG_URL=<url>          VPN 订阅链接
USAGE
}

# ── parse_args ────────────────────────────────────────────────────────────────
parse_args() {
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --mode) [[ $# -ge 2 ]] || { style_err "--mode 需要参数"; exit 1; }
        MODE="$2"; MODE_WAS_SET=1; case "$MODE" in confirm|auto) ;; *) style_err "--mode 仅支持 confirm|auto"; exit 1;; esac; shift 2;;
      --yes|-y) ASSUME_YES=1; MODE="auto"; MODE_WAS_SET=1; shift;;
      --dry-run) DRY_RUN=1; shift;;
      --list) LIST_ONLY=1; shift;;
      --step) [[ $# -ge 2 ]] || { style_err "--step 需要 step_id"; exit 1; }
        REQUESTED_STEP_IDS+=("$2"); shift 2;;
      --write-bashrc) WRITE_BASHRC=1; shift;;
      --log-file) [[ $# -ge 2 ]] || { style_err "--log-file 需要路径"; exit 1; }
        LOG_FILE="$2"; mkdir -p "$(dirname "$LOG_FILE")"; shift 2;;
      -h|--help) print_usage; exit 0;;
      *) style_err "不支持的参数: $1"; print_usage; exit 1;;
    esac; done
}

# ── main ──────────────────────────────────────────────────────────────────────
main() {
  command -v gum >/dev/null 2>&1 && HAS_GUM=1
  parse_args "$@"; register_steps
  log_line "启动 all.sh mode=$MODE dry_run=$DRY_RUN write_bashrc=$WRITE_BASHRC has_gum=$HAS_GUM"
  if [[ "$LIST_ONLY" -eq 1 ]]; then show_step_list_only; exit 0; fi
  if [[ ${#REQUESTED_STEP_IDS[@]} -gt 0 ]]; then
    local status=0
    run_steps_by_ids "${REQUESTED_STEP_IDS[@]}" || status=$?
    print_summary; exit "$status"; fi
  if [[ "$MODE_WAS_SET" -eq 0 && -t 0 && -t 1 ]]; then
    prompt_startup_mode
  fi
  if [[ "$HAS_GUM" -eq 0 && -t 0 && -t 1 ]]; then
    show_gum_install_hint
  fi
  main_menu; print_summary
}

main "$@"
