#!/usr/bin/env bash
# 一键启动 Isaac 真实仿真闭环测试。
#
# 默认流程：
# 1. 使用 conda 环境 AutoCalib_Workshop 启动 Isaac 标定车间；
# 2. 启动仿真车端统一 agent、外部位姿桥和车间侧传感器 ingest；
# 3. 启动车间总控 demo；
# 4. 使用 Isaac 真实发布的真值/传感器数据执行 orchestrator 端到端验收；
# 5. 验收结束后自动停止后台进程。

set -Eeuo pipefail

WORKSPACE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONDA_ENV_NAME="${CONDA_ENV_NAME:-AutoCalib_Workshop}"
ROS_LOG_DIR="${ROS_LOG_DIR:-/tmp/roslog}"
RUN_LOG_ROOT="${RUN_LOG_ROOT:-${WORKSPACE_DIR}/log/isaac_real_sim_$(date +%Y%m%d_%H%M%S)}"

HEADLESS=1
DO_BUILD=0
RUN_SMOKE=1
KEEP_RUNNING=0
WITH_SENSOR_INGEST=1
WAIT_FOR_ISAAC_TOPICS=1
WORKSHOP_USE_GATEWAY=1
ISAAC_WAIT_SEC="${ISAAC_WAIT_SEC:-180}"
WORKSHOP_WARMUP_SEC="${WORKSHOP_WARMUP_SEC:-5}"
START_GRACE_SEC="${START_GRACE_SEC:-2}"
STOP_GRACE_SEC="${STOP_GRACE_SEC:-8}"
TERM_GRACE_SEC="${TERM_GRACE_SEC:-3}"
SMOKE_TASKS=""
DATA_INPUT_PARAMS_FILE=""
DATASET_INDEX_FILE=""

PIDS=()
NAMES=()
LOGS=()
CLEANED=0

usage() {
  cat <<'EOF'
用法:
  ./run_isaac_real_sim_test.sh [选项]

默认会验证:
  1. Isaac 发布外部真值、前视相机、下视相机、3D 雷达、2D 雷达、IMU topic。
  2. 仿真车端统一 agent、外部位姿桥、车间侧传感器 ingest 都能启动并通信。
  3. 车间总控能创建 session，并默认执行 external,chassis,control,sensor_intrinsic 阶段。
  4. 传感器数据能从 Isaac -> 车端 -> WiFi6/TCP -> 车间侧链路流动。

当前不验证:
  1. 真实底盘参数是否已经收敛。
  2. 真实相机内参、传感器外参是否已经被算法正确求解。
  3. 棋盘格角点数量、点云几何质量、时间同步精度等采样质量。

选项:
  --headless              以 headless 模式启动 Isaac。
  --build                 启动前先执行 colcon build。
  --no-smoke              只启动仿真链路，不执行 orchestrator 验收。
  --keep-running          验收结束后保持仿真链路运行，按 Ctrl+C 停止。
  --no-sensor-ingest      不启动车间侧传感器 ingest 轮询进程。
  --skip-topic-check      不等待 Isaac 真值/传感器 topic，直接继续启动后续链路。
  --local-calibration-services
                          底盘和运控阶段使用本地标定服务节点，便于验证四个算法服务的数据输入注入。
  --data-input-params-file PATH
                          把真实采集/ingest 生成的 site_data_input.yaml 注入算法服务。
  --dataset-index-file PATH
                          把真实采集/ingest 生成的 dataset_index.yaml 写入最终报告。
  --isaac-wait-sec SEC    等待 Isaac topic 的最长时间，默认 180 秒。
  --warmup-sec SEC        总控 demo 启动后等待采集真值历史的时间，默认 5 秒。
  --tasks LIST            覆盖 smoke_test_workshop_orchestrator.py 的任务列表。
                          例: external,chassis,control,sensor_intrinsic
  --conda-env NAME        Isaac 进程使用的 conda 环境，默认 AutoCalib_Workshop。
  -h, --help              显示帮助。

环境变量:
  CONDA_ENV_NAME          同 --conda-env。
  ROS_LOG_DIR             ROS 日志目录，默认 /tmp/roslog。
  RUN_LOG_ROOT            本脚本各后台进程日志目录。
  STOP_GRACE_SEC          SIGINT 后等待后台进程优雅退出的秒数，默认 8。
  TERM_GRACE_SEC          SIGTERM 后等待后台进程退出的秒数，默认 3。

示例:
  ./run_isaac_real_sim_test.sh --headless
  ./run_isaac_real_sim_test.sh --headless --keep-running
  ./run_isaac_real_sim_test.sh --headless --local-calibration-services --data-input-params-file /data/agv_calib/session_001/site_data_input.yaml
  ./run_isaac_real_sim_test.sh --no-smoke
EOF
}

log() {
  echo "[INFO] $*"
}

warn() {
  echo "[WARN] $*" >&2
}

die() {
  echo "[ERROR] $*" >&2
  exit 1
}

resolve_workspace_path() {
  local path="$1"
  case "${path}" in
    /*)
      echo "${path}"
      ;;
    *)
      echo "${WORKSPACE_DIR}/${path}"
      ;;
  esac
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    --headless)
      HEADLESS=1
      shift
      ;;
    --build)
      DO_BUILD=1
      shift
      ;;
    --no-smoke)
      RUN_SMOKE=0
      KEEP_RUNNING=1
      shift
      ;;
    --keep-running)
      KEEP_RUNNING=1
      shift
      ;;
    --no-sensor-ingest)
      WITH_SENSOR_INGEST=0
      shift
      ;;
    --skip-topic-check)
      WAIT_FOR_ISAAC_TOPICS=0
      shift
      ;;
    --local-calibration-services)
      WORKSHOP_USE_GATEWAY=0
      shift
      ;;
    --data-input-params-file)
      [[ $# -ge 2 ]] || die "--data-input-params-file 需要参数"
      DATA_INPUT_PARAMS_FILE="$2"
      shift 2
      ;;
    --dataset-index-file)
      [[ $# -ge 2 ]] || die "--dataset-index-file 需要参数"
      DATASET_INDEX_FILE="$2"
      shift 2
      ;;
    --isaac-wait-sec)
      [[ $# -ge 2 ]] || die "--isaac-wait-sec 需要参数"
      ISAAC_WAIT_SEC="$2"
      shift 2
      ;;
    --warmup-sec)
      [[ $# -ge 2 ]] || die "--warmup-sec 需要参数"
      WORKSHOP_WARMUP_SEC="$2"
      shift 2
      ;;
    --tasks)
      [[ $# -ge 2 ]] || die "--tasks 需要参数"
      SMOKE_TASKS="$2"
      shift 2
      ;;
    --conda-env)
      [[ $# -ge 2 ]] || die "--conda-env 需要参数"
      CONDA_ENV_NAME="$2"
      shift 2
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      die "未知参数: $1"
      ;;
  esac
done

if [[ -n "${DATA_INPUT_PARAMS_FILE}" ]]; then
  DATA_INPUT_PARAMS_FILE="$(resolve_workspace_path "${DATA_INPUT_PARAMS_FILE}")"
  [[ -f "${DATA_INPUT_PARAMS_FILE}" ]] || die "找不到数据输入参数文件: ${DATA_INPUT_PARAMS_FILE}"
  [[ "${DATA_INPUT_PARAMS_FILE}" != *"'"* ]] || die "data_input_params_file 路径不能包含单引号"
  if [[ -z "${DATASET_INDEX_FILE}" ]]; then
    DATASET_INDEX_FILE="$(dirname "${DATA_INPUT_PARAMS_FILE}")/dataset_index.yaml"
  fi
fi

if [[ -n "${DATASET_INDEX_FILE}" ]]; then
  DATASET_INDEX_FILE="$(resolve_workspace_path "${DATASET_INDEX_FILE}")"
  [[ "${DATASET_INDEX_FILE}" != *"'"* ]] || die "dataset_index_file 路径不能包含单引号"
fi

if [[ "${WORKSHOP_USE_GATEWAY}" -eq 1 ]]; then
  WORKSHOP_USE_GATEWAY_VALUE="true"
else
  WORKSHOP_USE_GATEWAY_VALUE="false"
fi

resolve_conda_sh() {
  if [[ -n "${CONDA_SH:-}" && -f "${CONDA_SH}" ]]; then
    echo "${CONDA_SH}"
    return 0
  fi

  local candidate
  for candidate in \
    "${HOME}/anaconda3/etc/profile.d/conda.sh" \
    "${HOME}/miniconda3/etc/profile.d/conda.sh" \
    "/opt/conda/etc/profile.d/conda.sh"
  do
    if [[ -f "${candidate}" ]]; then
      echo "${candidate}"
      return 0
    fi
  done

  if command -v conda >/dev/null 2>&1; then
    local base
    base="$(conda info --base 2>/dev/null || true)"
    if [[ -n "${base}" && -f "${base}/etc/profile.d/conda.sh" ]]; then
      echo "${base}/etc/profile.d/conda.sh"
      return 0
    fi
  fi

  return 1
}

source_ros_env() {
  set +u
  if [[ -f /opt/ros/humble/setup.bash ]]; then
    source /opt/ros/humble/setup.bash
  fi
  source "${WORKSPACE_DIR}/install/setup.bash"
  set -u
}

wait_for_background_exit() {
  local timeout_sec="$1"
  local deadline=$((SECONDS + timeout_sec))
  local pid

  while (( SECONDS < deadline )); do
    local any_alive=0
    for pid in "${PIDS[@]}"; do
      if kill -0 "${pid}" >/dev/null 2>&1; then
        any_alive=1
        break
      fi
    done
    if [[ "${any_alive}" -eq 0 ]]; then
      return 0
    fi
    sleep 1
  done

  return 1
}

cleanup() {
  if [[ "${CLEANED}" -eq 1 ]]; then
    return
  fi
  CLEANED=1

  if [[ "${#PIDS[@]}" -eq 0 ]]; then
    return
  fi

  log "正在停止后台进程..."
  local i pid name
  for ((i=${#PIDS[@]}-1; i>=0; i--)); do
    pid="${PIDS[$i]}"
    name="${NAMES[$i]}"
    if kill -0 "${pid}" >/dev/null 2>&1; then
      log "发送 SIGINT: ${name} pid=${pid}"
      kill -INT "${pid}" >/dev/null 2>&1 || true
    fi
  done

  wait_for_background_exit "${STOP_GRACE_SEC}" || true
  for ((i=${#PIDS[@]}-1; i>=0; i--)); do
    pid="${PIDS[$i]}"
    name="${NAMES[$i]}"
    if kill -0 "${pid}" >/dev/null 2>&1; then
      log "${name} 未在 ${STOP_GRACE_SEC}s 内退出，发送 SIGTERM: pid=${pid}"
      kill -TERM "${pid}" >/dev/null 2>&1 || true
    fi
  done

  wait_for_background_exit "${TERM_GRACE_SEC}" || true
  for ((i=${#PIDS[@]}-1; i>=0; i--)); do
    pid="${PIDS[$i]}"
    name="${NAMES[$i]}"
    if kill -0 "${pid}" >/dev/null 2>&1; then
      warn "${name} 仍未退出，发送 SIGKILL: pid=${pid}"
      kill -KILL "${pid}" >/dev/null 2>&1 || true
    fi
  done
}

on_signal() {
  trap - INT TERM
  cleanup
  exit 130
}

trap cleanup EXIT
trap on_signal INT TERM

start_bg() {
  local name="$1"
  shift
  local logfile="${RUN_LOG_ROOT}/${name}.log"

  log "启动 ${name}，日志: ${logfile}"
  "$@" >"${logfile}" 2>&1 &
  local pid=$!
  PIDS+=("${pid}")
  NAMES+=("${name}")
  LOGS+=("${logfile}")

  sleep "${START_GRACE_SEC}"
  if ! kill -0 "${pid}" >/dev/null 2>&1; then
    warn "${name} 启动后立即退出，最近日志如下:"
    tail -n 120 "${logfile}" >&2 || true
    exit 1
  fi
}

wait_for_tcp_port() {
  local host="$1"
  local port="$2"
  local timeout_sec="$3"
  local name="$4"
  local deadline=$((SECONDS + timeout_sec))

  log "等待 ${name} TCP ${host}:${port}"
  while (( SECONDS < deadline )); do
    if timeout 1 bash -c "</dev/tcp/${host}/${port}" >/dev/null 2>&1; then
      log "${name} 已就绪"
      return 0
    fi
    sleep 1
  done

  warn "等待 ${name} 超时"
  return 1
}

wait_for_ros_topic_once() {
  local topic="$1"
  local timeout_sec="$2"
  local deadline=$((SECONDS + timeout_sec))
  local echo_args=(--qos-reliability best_effort --once "${topic}")

  case "${topic}" in
    /sensor/front_camera/*|/sensor/down_camera/*|/sensor/lidar_3d/*)
      echo_args=(--qos-reliability best_effort --once "${topic}" --field header)
      ;;
  esac

  log "等待 Isaac topic: ${topic}"
  while (( SECONDS < deadline )); do
    if timeout 8s ros2 topic echo "${echo_args[@]}" >/dev/null 2>&1; then
      log "已收到 topic: ${topic}"
      return 0
    fi
    sleep 2
  done

  warn "等待 topic 超时: ${topic}"
  warn "可检查 Isaac 日志: ${RUN_LOG_ROOT}/isaac.log"
  if [[ -f "${RUN_LOG_ROOT}/isaac.log" ]]; then
    warn "Isaac 最近日志:"
    tail -n 120 "${RUN_LOG_ROOT}/isaac.log" >&2 || true
  fi
  return 1
}

wait_for_ros_service() {
  local service="$1"
  local timeout_sec="$2"
  local deadline=$((SECONDS + timeout_sec))

  log "等待 ROS service: ${service}"
  while (( SECONDS < deadline )); do
    if ros2 service list 2>/dev/null | grep -qx "${service}"; then
      log "service 已就绪: ${service}"
      return 0
    fi
    sleep 1
  done

  warn "等待 service 超时: ${service}"
  return 1
}

wait_for_ros_param() {
  local node="$1"
  local param="$2"
  local timeout_sec="$3"
  local deadline=$((SECONDS + timeout_sec))

  log "等待 ROS 参数: ${node} ${param}"
  while (( SECONDS < deadline )); do
    if ros2 param get "${node}" "${param}" >/dev/null 2>&1; then
      log "参数已就绪: ${node} ${param}"
      return 0
    fi
    sleep 1
  done

  warn "等待参数超时: ${node} ${param}"
  return 1
}

require_ros_param_non_empty() {
  local node="$1"
  local param="$2"
  local output

  output="$(ros2 param get "${node}" "${param}" 2>/dev/null || true)"
  if [[ -z "${output}" || "${output}" == *"[]"* || "${output}" == *"not set"* ]]; then
    warn "数据输入参数为空: ${node} ${param}; 输出: ${output}"
    return 1
  fi
  log "数据输入参数已注入: ${node} ${param}"
  return 0
}

verify_data_input_params() {
  if [[ -z "${DATA_INPUT_PARAMS_FILE}" ]]; then
    return 0
  fi

  log "验证 data_input 参数注入: ${DATA_INPUT_PARAMS_FILE}"
  wait_for_ros_param "/external_localization_service" "data_input.data_window_start_timestamp_us" 30 || return 1
  wait_for_ros_param "/sensor_calibration_service" "data_input.data_window_start_timestamp_us" 30 || return 1
  require_ros_param_non_empty "/external_localization_service" "data_input.external_observation_files" || return 1
  require_ros_param_non_empty "/sensor_calibration_service" "data_input.synchronized_dataset_files" || return 1
  wait_for_ros_param "/workshop_orchestrator_v2" "data_input_params_file" 30 || return 1
  require_ros_param_non_empty "/workshop_orchestrator_v2" "data_input_params_file" || return 1

  if [[ "${WORKSHOP_USE_GATEWAY}" -eq 0 ]]; then
    wait_for_ros_param "/chassis_calibration_service" "data_input.data_window_start_timestamp_us" 30 || return 1
    wait_for_ros_param "/control_calibration_service" "data_input.data_window_start_timestamp_us" 30 || return 1
    require_ros_param_non_empty "/chassis_calibration_service" "data_input.chassis_motion_data_files" || return 1
    require_ros_param_non_empty "/control_calibration_service" "data_input.control_evaluation_data_files" || return 1
  else
    warn "当前使用 vehicle_agent_gateway，底盘/运控 data_input 由车端 agent 侧负责，本脚本只验证 external/sensor 注入。"
  fi
}

check_no_existing_workshop_orchestrator() {
  local nodes
  nodes="$(timeout 5s ros2 node list 2>/dev/null || true)"
  if [[ -z "${nodes}" ]]; then
    return
  fi

  local count
  count="$(printf '%s\n' "${nodes}" | grep -xc "/workshop_orchestrator_v2" || true)"
  if [[ "${count}" -gt 0 ]]; then
    die "检测到已有 /workshop_orchestrator_v2 节点。请先关闭旧的 workshop-demo/仿真脚本，或执行 ./stop_isaac_real_sim_stack.sh --kill 清理遗留进程，否则 create_session 和 execute_session 可能连到不同实例。"
  fi
}

mkdir -p "${ROS_LOG_DIR}" "${RUN_LOG_ROOT}"

if [[ "${DO_BUILD}" -eq 1 ]]; then
  log "执行 colcon build"
  (
    cd "${WORKSPACE_DIR}"
    set +u
    if [[ -f /opt/ros/humble/setup.bash ]]; then
      source /opt/ros/humble/setup.bash
    fi
    set -u
    colcon build
  )
fi

[[ -f "${WORKSPACE_DIR}/install/setup.bash" ]] || die "找不到 install/setup.bash，请先执行 colcon build，或使用 --build"

CONDA_SH_PATH="$(resolve_conda_sh || true)"
[[ -n "${CONDA_SH_PATH}" ]] || die "找不到 conda.sh，无法激活 conda 环境 ${CONDA_ENV_NAME}"

source_ros_env

log "工作空间: ${WORKSPACE_DIR}"
log "ROS_LOG_DIR: ${ROS_LOG_DIR}"
log "进程日志目录: ${RUN_LOG_ROOT}"
log "Isaac conda 环境: ${CONDA_ENV_NAME}"
log "总控底盘/运控模式 use_gateway=${WORKSHOP_USE_GATEWAY_VALUE}"
if [[ -n "${DATA_INPUT_PARAMS_FILE}" ]]; then
  log "数据输入参数文件: ${DATA_INPUT_PARAMS_FILE}"
fi

export PYTHONUNBUFFERED=1
check_no_existing_workshop_orchestrator

ISAAC_HEADLESS_ARG=""
if [[ "${HEADLESS}" -eq 1 ]]; then
  ISAAC_HEADLESS_ARG="--headless"
fi

start_bg "isaac" bash -lc "
  set -Eeuo pipefail
  cd '${WORKSPACE_DIR}'
  set +u
  source '${CONDA_SH_PATH}'
  conda activate '${CONDA_ENV_NAME}'
  if [[ -f /opt/ros/humble/setup.bash ]]; then
    source /opt/ros/humble/setup.bash
  fi
  source '${WORKSPACE_DIR}/install/setup.bash'
  set -u
  export ROS_LOG_DIR='${ROS_LOG_DIR}'
  export PYTHONNOUSERSITE=1
  exec python3 src/simulation/tools/launch_sim_stack.py --component isaac ${ISAAC_HEADLESS_ARG}
"

if [[ "${WAIT_FOR_ISAAC_TOPICS}" -eq 1 ]]; then
  wait_for_ros_topic_once "/isaac/external_localization/telemetry" "${ISAAC_WAIT_SEC}" || exit 1
  wait_for_ros_topic_once "/sensor/front_camera/image_raw" "${ISAAC_WAIT_SEC}" || exit 1
  wait_for_ros_topic_once "/sensor/down_camera/image_raw" "${ISAAC_WAIT_SEC}" || exit 1
  wait_for_ros_topic_once "/sensor/lidar_3d/pointcloud" "${ISAAC_WAIT_SEC}" || exit 1
  wait_for_ros_topic_once "/sensor/imu/data" "${ISAAC_WAIT_SEC}" || exit 1
  wait_for_ros_topic_once "/sensor/lidar_2d/scan" "${ISAAC_WAIT_SEC}" || exit 1
fi

start_bg "vehicle-agent" bash -lc "
  set -Eeuo pipefail
  cd '${WORKSPACE_DIR}'
  set +u
  if [[ -f /opt/ros/humble/setup.bash ]]; then
    source /opt/ros/humble/setup.bash
  fi
  source '${WORKSPACE_DIR}/install/setup.bash'
  set -u
  export ROS_LOG_DIR='${ROS_LOG_DIR}'
  exec python3 src/simulation/tools/launch_sim_stack.py --component vehicle-agent
"
wait_for_tcp_port "127.0.0.1" 9000 30 "vehicle-agent unified WiFi6" || exit 1

start_bg "external-pose-bridge" bash -lc "
  set -Eeuo pipefail
  cd '${WORKSPACE_DIR}'
  set +u
  if [[ -f /opt/ros/humble/setup.bash ]]; then
    source /opt/ros/humble/setup.bash
  fi
  source '${WORKSPACE_DIR}/install/setup.bash'
  set -u
  export ROS_LOG_DIR='${ROS_LOG_DIR}'
  exec python3 src/simulation/tools/launch_sim_stack.py --component external-pose-bridge
"

if [[ "${WITH_SENSOR_INGEST}" -eq 1 ]]; then
  start_bg "sensor-ingest" bash -lc "
    set -Eeuo pipefail
    cd '${WORKSPACE_DIR}'
    set +u
    if [[ -f /opt/ros/humble/setup.bash ]]; then
      source /opt/ros/humble/setup.bash
    fi
    source '${WORKSPACE_DIR}/install/setup.bash'
    set -u
    export ROS_LOG_DIR='${ROS_LOG_DIR}'
    exec python3 src/simulation/tools/launch_sim_stack.py --component sensor-ingest
  "
fi

start_bg "workshop-demo" bash -lc "
  set -Eeuo pipefail
  cd '${WORKSPACE_DIR}'
  set +u
  if [[ -f /opt/ros/humble/setup.bash ]]; then
    source /opt/ros/humble/setup.bash
  fi
  source '${WORKSPACE_DIR}/install/setup.bash'
  set -u
  export ROS_LOG_DIR='${ROS_LOG_DIR}'
  exec ros2 launch win_ubuntu_bridge minimal_workshop_demo.launch.py \
    use_gateway:=${WORKSHOP_USE_GATEWAY_VALUE} \
    chassis_host:=127.0.0.1 \
    control_host:=127.0.0.1 \
    sensor_registry:=demo_front_camera,demo_down_camera,demo_lidar_3d,demo_lidar_2d,demo_imu \
    data_input_params_file:='${DATA_INPUT_PARAMS_FILE}' \
    dataset_index_file:='${DATASET_INDEX_FILE}'
"

wait_for_ros_service "/workshop_v2/create_session" 60 || exit 1
verify_data_input_params || exit 1

log "等待 ${WORKSHOP_WARMUP_SEC}s，让 external_localization_service 累积 Isaac 真值历史"
sleep "${WORKSHOP_WARMUP_SEC}"

SMOKE_STATUS=0
if [[ "${RUN_SMOKE}" -eq 1 ]]; then
  SMOKE_ARGS=(
    --verbose-feedback
    --no-publish-fake-external-telemetry
  )
  if [[ "${WORKSHOP_USE_GATEWAY}" -eq 0 ]]; then
    SMOKE_ARGS+=(--disable-wifi6-precheck)
  fi
  if [[ -n "${SMOKE_TASKS}" ]]; then
    SMOKE_ARGS+=(--tasks "${SMOKE_TASKS}")
  fi

  SMOKE_LOG="${RUN_LOG_ROOT}/smoke.log"
  log "执行 orchestrator 端到端验收，日志: ${SMOKE_LOG}"
  set +e
  (
    cd "${WORKSPACE_DIR}"
    source_ros_env
    export ROS_LOG_DIR="${ROS_LOG_DIR}"
    python3 src/simulation/tools/smoke_test_workshop_orchestrator.py "${SMOKE_ARGS[@]}"
  ) 2>&1 | tee "${SMOKE_LOG}"
  SMOKE_STATUS=${PIPESTATUS[0]}
  set -e

  if [[ "${SMOKE_STATUS}" -eq 0 ]]; then
    if [[ -n "${DATA_INPUT_PARAMS_FILE}" ]]; then
      grep -q "site_data_input.yaml" "${SMOKE_LOG}" || die "报告中没有记录 site_data_input.yaml"
      if [[ -f "${DATASET_INDEX_FILE}" ]]; then
        grep -q "dataset_index.yaml" "${SMOKE_LOG}" || die "报告中没有记录 dataset_index.yaml"
      fi
    fi
    log "仿真闭环验收通过"
  else
    warn "仿真闭环验收失败，退出码: ${SMOKE_STATUS}"
  fi
else
  log "已按 --no-smoke 跳过 orchestrator 验收"
fi

if [[ "${KEEP_RUNNING}" -eq 1 ]]; then
  log "仿真链路保持运行中。按 Ctrl+C 停止。"
  while true; do
    sleep 3600
  done
fi

exit "${SMOKE_STATUS}"
