Files
DCTS/scripts/fetch_results.sh
T
fmq 43b82b1ae2 feat(all): 物理正确性五重硬门槛、输入文件结构化与 fort.55 错位修复、conv 诊断 DB 化与阶段归因修复、ORELAX 收敛修复与导入工具下线
物理正确性校验体系(common/conv_check.rs +494 行)
- 新增 5 类硬门槛:能量守恒(.6)、温度结构(.7)、emflux 积分校验(.emflux,含全 NaN 判失败)、假收敛排查(itek 轨迹首末比)、b 因子合理性(.bfac)
- runner 在 TLUSTY 阶段结束后执行全部校验,任一失败判 final_converged=false
- GridConfig 新增 8 个可配阈值,经 scheduler→executor→runner 全链路透传

输入文件配置结构化重构(config.rs +1453 行)
- TlustyInput 拆为 dot5/nst 分层结构,字段名严格映射 tlusty208.f READ 语句;SynspecInput 重构为 9 个 Fort55Line 子结构体
- 移除 ChainStep.metals 字段,元素集改由 dot5.atoms/ions 显式声明(gen_input5/nst_writer 同步重写为三源融合 / 分层覆盖)
- fort.55 修复行结构 bug:补全分子表行(7→9 行),IDSTD 50→0 错位修正(影响全部光谱线强归一化,需重算 SYNSPEC 阶段)

conv 诊断 DB 化与阶段归因修复(server)
- 单点详情 conv 面板从磁盘 conv.json 改读 DB grid_points.summary_json;grid_points 新增 summary_json/last_elapsed_sec 两列(旧库幂等 ALTER)
- record_task_report 阶段归因列加 CASE 守卫 + clear_synspec 对称处理,修复 synspec-only/TLUSTY-only 重跑污染统计
- 新增 summary_merge.rs 点级增量合并,避免重跑覆盖诊断字段

收敛性 ORELAX 修复与 seed_chain 可配(sdB_cno.yaml + node)
- nl 阶段加 orelax=0.5、seed_nc 加 orelax=0.3,阻尼中温区 relc 振荡发散
- seed_chain 块可配,executor 优先采用用户配置而非内置默认链

导入工具下线
- 删除 import_results 客户端工具及 Windows 推送脚本;移除 /admin/import_seed 端点
- 改为服务端临时 migrate_conv 端点(扫 conv.json 增量合并入库,迁移后可删)

文档与分析
- 新增 1305 失败点根因分析、fort.14 全 NaN 物理含义分析两份深度文档
- spectrum_correctness_analysis 两次修订标注已修复项;fetch_results.sh 修 trap RETURN 的 set -u 报错
2026-08-09 12:09:48 +08:00

411 lines
17 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# ==============================================================================
# fetch_results.sh — 从全部计算节点同步 data/result 完整科学产物到本地备份
# ==============================================================================
# 背景:node 端 result 目录永久保留全部网格点的完整产物(.spec/.cont/.iden/
# 各阶段快照/conv.json/日志等,撤销了早期的 MAX_RESULT_MODELS=200 LRU 上限)。
# 为做异地备份、以及便于失败归因与收敛验证,把各节点 result 目录整树拉回本地,
# 按节点分目录存放。
#
# 用法:
# ./scripts/fetch_results.sh # 无参数 → 交互式向导选择节点
# ./scripts/fetch_results.sh -i # 显式进入交互式向导
# ./scripts/fetch_results.sh -n node-fmq-dckj-02 # 非交互:仅同步指定节点(DCTS_NODE_ID)
# ./scripts/fetch_results.sh -d /backup/dcts-salvage # 覆盖备份根目录
# ./scripts/fetch_results.sh --with-work # 连带拉取 data/work 沙盒残留
# ./scripts/fetch_results.sh --include-local # 把本机节点也复制进备份树
#
# 传输策略(两类节点都走增量,重复运行只补差异、不重拷全量):
# - 双方均可用 rsync → rsync 增量(断点续传、幂等,自动补新增/变更文件)
# - 否则(典型 Windows:无 rsync)→ scp 目录级增量:先列举远端顶层网格点子目录,
# 只 scp 本地缺失的目录,已存在的不重拷(避免 win-01 的 52GB 全量重拷)
# 远端 rsync/列目录探测均用多 shell 兼容命令(POSIX/command-v/where/Get-Command、
# ls/dir/Get-ChildItem),适配 Linux + Windows(cmd/PowerShell 默认 shell)。
# SSH 复用 deploy.sh 的 ControlMaster,多次执行不重复输密码。
# ==============================================================================
set -euo pipefail
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
# 定位至项目根目录(与 deploy.sh 一致)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${WORK_DIR}"
# =============================================================================
# 默认配置(可被命令行参数覆盖)
# =============================================================================
SSH_CONTROL_DIR="${SSH_CONTROL_DIR:-$HOME/.ssh/cm}"
BACKUP_ROOT="${BACKUP_ROOT:-${WORK_DIR}/data/salvage}"
ONLY_NODE=""
WITH_WORK=false
INCLUDE_LOCAL=false
INTERACTIVE=false
HAD_ARGS=false
SELECTED_PROFILES=()
# =============================================================================
# 解析命令行参数
# =============================================================================
while [[ $# -gt 0 ]]; do
HAD_ARGS=true
case "$1" in
-n|--node) ONLY_NODE="$2"; shift 2 ;;
-d|--dest) BACKUP_ROOT="$2"; shift 2 ;;
-i|--interactive) INTERACTIVE=true; shift ;;
--with-work) WITH_WORK=true; shift ;;
--include-local) INCLUDE_LOCAL=true; shift ;;
-h|--help)
sed -n '2,32p' "$0"
exit 0 ;;
*) echo -e "${RED}未知参数: $1${NC}"; exit 1 ;;
esac
done
mkdir -p "${BACKUP_ROOT}"
# =============================================================================
# 工具函数
# =============================================================================
# 从 profile 文件读取键值(避免 source 造成变量污染;写法对齐 deploy.sh)
read_profile_var() { # $1=profile $2=键 $3=默认值
local p="$1" k="$2" d="${3:-}"
local v
v=$(grep -E "^${k}=" "$p" 2>/dev/null | head -n1 | cut -d'=' -f2- | tr -d '"' | tr -d "'" || true)
[ -n "$v" ] && printf '%s' "$v" || printf '%s' "$d"
}
# rsync 远端路径转义:含空格目录需单引号包住(现有路径均无空格,此处兜底)
rsync_remote_path() { # $1 = 远端绝对路径
local p="$1"
case "$p" in
*\ *) printf "'%s'" "$p" ;;
*) printf '%s' "$p" ;;
esac
}
# 远端目录列表(仅顶层条目名,用于 scp 目录级增量比对)。
# 按远端路径形态选择对应的列举命令:
# - 盘符开头(如 E:/...) → Windowspowershell Get-ChildItem(兼容 cmd/PowerShell 默认 shell
# - 否则 → POSIXls -1
# 返回名字列表(每行一个),失败返回空。
remote_list_dir() { # $1=user $2=ip $3=port $4=ssh_opts $5=远端绝对目录
local u="$1" ip="$2" port="$3" opts="$4" rdir="$5"
# 统一 tr -d '\r'Windows powershell/openssh 输出带 CRLF,会让 grep -Fxq 精确匹配
# 永远失败(已同步目录每次重传)并产生带 CR 的畸形 scp 路径。
if [[ "${rdir}" =~ ^[A-Za-z]:[\\/] ]]; then
# Windows 路径:经 powershell 列举,-Name 直接返回每项一行
ssh -p "${port}" ${opts} "${u}@${ip}" \
"powershell -NoProfile -Command \"Get-ChildItem -Name -Path '${rdir}'\"" 2>/dev/null | tr -d '\r'
else
ssh -p "${port}" ${opts} "${u}@${ip}" "ls -1 '${rdir}'" 2>/dev/null | tr -d '\r'
fi
}
# scp 单个远端目录到本地,采用"先落地 .partial 再原子 mv"模式:
# 1. 目标目录 ${dest}/${d} 已存在 → 视为已同步,跳过(保持原增量语义)
# 2. 否则 scp 远端目录到 ${dest}/${d}.partial
# 3. scp 成功后 mv 为 ${dest}/${d}(同 dest 文件系统,原子语义)
# 4. scp 失败 → 清理残留 .partial,return 非零,由调用方决定是否继续
# 这样即便 scp 传输过程中断,也不会留下"伪完成"目录导致下次被跳过漏传。
scp_dir() { # $1=u $2=ip $3=port $4=ssh_opts $5=远端根目录 $6=本地目标 $7=目录名
local u="$1" ip="$2" port="$3" opts="$4" rdir="$5" dest="$6" d="$7"
local final="${dest}/${d}"
if [ -e "${final}" ]; then
return 0
fi
local tmp="${dest}/.${d}.partial"
# 兜底清理可能的历史残留(上次中断留下的半成品)
rm -rf "${tmp}"
if scp -P "${port}" ${opts} -r "${u}@${ip}:${rdir}/${d}" "${tmp}"; then
mv "${tmp}" "${final}"
else
echo -e " ${RED}[!]${NC} scp 失败: ${d},清理残留 ${d}.partial"
rm -rf "${tmp}"
return 1
fi
}
# 传输一个目录树(rsync 优先,退化 scp 目录级增量)
sync_dir() { # $1=user $2=ip $3=port $4=远端绝对目录 $5=本地目标
local u="$1" ip="$2" port="$3" rdir="$4" dest="$5"
local ssh_opts="-o ControlMaster=auto -o ControlPath=${SSH_CONTROL_DIR}/cm-${u}@${ip}:${port} -o ControlPersist=1800"
mkdir -p "${dest}"
# 连通性探测(BatchMode=no 允许交互输密码)。
# 探测命令用 `echo ok` 而非 `true`Windows OpenSSH 默认 shell 是 cmd/PowerShell
# 没有 Unix 的 true,会导致"密码正确但仍判连接失败"。
if ! ssh -p "${port}" -o ConnectTimeout=8 ${ssh_opts} "${u}@${ip}" "echo ok" >/dev/null 2>&1; then
echo -e " ${RED}[!] 无法连接 ${u}@${ip},跳过本节点${NC}"
return 1
fi
# rsync 可用性(本机 + 远端)。远端探测按平台分支:
# Windows 路径 → powershell Get-CommandPOSIX 路径 → command -v
local rs_ok=false
if command -v rsync >/dev/null 2>&1; then
local probe
if [[ "${rdir}" =~ ^[A-Za-z]:[\\/] ]]; then
probe="powershell -NoProfile -Command \"Get-Command rsync -ErrorAction SilentlyContinue | Out-Null\""
else
probe="command -v rsync"
fi
if ssh -p "${port}" ${ssh_opts} "${u}@${ip}" "${probe}" >/dev/null 2>&1; then
rs_ok=true
fi
fi
if [ "${rs_ok}" = "true" ]; then
# rsync 天然增量幂等:每次都跑,自动补齐新增/变更的文件。
echo -e " ${BLUE}[→]${NC} rsync 增量同步: ${u}@${ip}:${rdir}/ → ${dest}/"
rsync -a --partial --info=progress2 \
-e "ssh -p ${port} ${ssh_opts}" \
"${u}@${ip}:$(rsync_remote_path "${rdir}")/" "${dest}/"
else
# 无 rsync(典型 Windows):scp 目录级增量——只拉本地缺失的网格点子目录,
# 不重复拷贝已存在的目录(避免 win-01 的 52GB 全量重拷)。
local remote_dirs existing missing
remote_dirs=$(remote_list_dir "${u}" "${ip}" "${port}" "${ssh_opts}" "${rdir}")
if [ -z "${remote_dirs}" ]; then
echo -e " ${YELLOW}[!]${NC} 远端目录 ${rdir} 为空或无法列举,跳过"
return 1
fi
existing=$(ls -1 "${dest}" 2>/dev/null)
missing=""
local d
# 远端目录名白名单校验:scp 路径会把 ${d} 直接拼进 scp 的 remote 路径,
# 这里只允许字母/数字/._-,禁止 / 与 . .. —— 防御性过滤畸形或注入目录名。
while IFS= read -r d; do
[ -n "$d" ] || continue
case "$d" in
"."|"..")
echo -e " ${YELLOW}[!]${NC} 跳过可疑远端目录名: ${d}"
continue ;;
esac
if [[ "$d" == */* ]] || ! [[ "$d" =~ ^[A-Za-z0-9._-]+$ ]]; then
echo -e " ${YELLOW}[!]${NC} 跳过不符合白名单的远端目录名: ${d}"
continue
fi
if ! grep -Fxq -- "$d" <<<"${existing}"; then
missing="${missing}${d}"$'\n'
fi
done <<<"${remote_dirs}"
local total new
total=$(printf '%s\n' "${remote_dirs}" | grep -c .)
new=$(printf '%s' "${missing}" | grep -c .)
echo -e " ${YELLOW}[→]${NC} scp 目录级增量: ${u}@${ip}:${rdir}/ 共 ${total} 个,缺失 ${new} 个 → ${dest}/"
if [ "${new}" -eq 0 ]; then
echo -e " ${GREEN}[√]${NC} 本地已是最新,无新增目录"
return 0
fi
# scp 原子写入:先落地到 .partial 临时目录,成功后同文件系统原子 mv 为最终目录名。
# 开拷前先清理可能的历史 .partial 残留(上次中断留下的半成品),scp_dir 自身失败
# 时也会清理各自的 .partial;这样避免下次因"目录已存在"误判为已同步而漏传。
# (原先用 trap RETURN 兜底,但 RETURN 陷阱会被外层 backup_node 继承,函数返回时
# dest 已出作用域,触发 set -u 的"未绑定变量"报错,故改为前置清理。)
rm -rf "${dest}"/.*.partial 2>/dev/null || true
while IFS= read -r d; do
[ -n "$d" ] || continue
echo -e " ${YELLOW}[→]${NC} scp 新增: ${d}"
scp_dir "${u}" "${ip}" "${port}" "${ssh_opts}" "${rdir}" "${dest}" "$d"
done <<<"${missing}"
fi
}
# 备份单个节点
backup_node() { # $1 = profile 文件
local p="$1" node_id role env_mode u ip port dir
node_id=$(read_profile_var "$p" DCTS_NODE_ID)
role=$(read_profile_var "$p" DEPLOY_ROLE node)
env_mode=$(read_profile_var "$p" DEPLOY_ENV "")
u=$(read_profile_var "$p" REMOTE_USER "fmq")
ip=$(read_profile_var "$p" REMOTE_IP "")
port=$(read_profile_var "$p" REMOTE_PORT 22)
dir=$(read_profile_var "$p" REMOTE_DIR "")
# 跳过 server 角色(server 的 result 不是计算产物,且 seeds 另行备份)
if [ "${role}" = "server" ]; then
echo -e "${YELLOW}== ${p##*/}: 角色 server,跳过(seed 备份不属本脚本职责)${NC}"
return 0
fi
# 本地节点:result 就在本机 ./data/result,默认不重复备份
if [ "${env_mode}" = "local" ]; then
if [ "${INCLUDE_LOCAL}" = "true" ]; then
[ -n "${node_id}" ] || node_id="node-local"
else
echo -e "${YELLOW}== ${p##*/}: 本地节点(${node_id:-localhost})result 已在 ./data/result,跳过(--include-local 可纳入备份树)${NC}"
return 0
fi
fi
# 按节点过滤
if [ -n "${ONLY_NODE}" ] && [ "${node_id}" != "${ONLY_NODE}" ]; then
return 0
fi
if [ -z "${node_id}" ]; then
node_id="${u}@${ip}"
fi
local loc=""
if [ "${env_mode}" = "local" ]; then
loc="(本机)"
else
loc=" ${u}@${ip}:${dir}"
fi
echo -e "\n${GREEN}== ${p##*/} → 节点 ${node_id}${NC} ${loc}"
local dest_rc="${BACKUP_ROOT}/${node_id}/result"
if [ "${env_mode}" = "local" ]; then
# 本机节点:优先 rsync 增量(避免每次 cp -a 全量重拷),无 rsync 才退化 cp -a。
echo -e " ${BLUE}[→]${NC} 本机复制: ./data/result → ${dest_rc}"
mkdir -p "${dest_rc}"
if command -v rsync >/dev/null 2>&1; then
rsync -a --info=progress2 ./data/result/ "${dest_rc}/"
else
cp -a ./data/result/. "${dest_rc}/"
fi
else
sync_dir "${u}" "${ip}" "${port}" "${dir}/data/result" "${dest_rc}" || return 1
fi
echo -e " ${GREEN}[√]${NC} result 已同步,共 $(ls -1 "${dest_rc}" 2>/dev/null | grep -c . || true) 个网格点子目录"
# 可选:一并拉取 data/work 沙盒残留(未清理的计算现场,含完整过程文件)
if [ "${WITH_WORK}" = "true" ]; then
local dest_wk="${BACKUP_ROOT}/${node_id}/work"
if [ "${env_mode}" = "local" ]; then
echo -e " ${BLUE}[→]${NC} 本机复制: ./data/work → ${dest_wk}"
mkdir -p "${dest_wk}"
if command -v rsync >/dev/null 2>&1; then
rsync -a ./data/work/ "${dest_wk}/" 2>/dev/null || true
else
cp -a ./data/work/. "${dest_wk}/" 2>/dev/null || true
fi
else
sync_dir "${u}" "${ip}" "${port}" "${dir}/data/work" "${dest_wk}" || true
fi
fi
}
# =============================================================================
# 交互式选择(无参数或 -i 时进入,风格对齐 deploy.sh 的 Profile 向导)
# =============================================================================
interactive_select() {
# 构建可同步列表:非 *.example 且 DEPLOY_ROLE=node 的 profileserver 不在菜单)
local menu=()
for p in deploy.env.d/*.env; do
[ -f "$p" ] || continue
[[ "$p" != *.example ]] || continue
if [ "$(read_profile_var "$p" DEPLOY_ROLE node)" = "node" ]; then
menu+=("$p")
fi
done
if [ ${#menu[@]} -eq 0 ]; then
echo -e "${RED}错误: deploy.env.d/ 下没有可用的 node profile${NC}"
exit 1
fi
echo -e "${BLUE}==============================================================${NC}"
echo -e "${CYAN} 🚀 DCTS 计算结果产物下载控制台 ${NC}"
echo -e "${BLUE}==============================================================${NC}"
echo -e "${YELLOW}【节点选择】检测到 ${#menu[@]} 个可用的计算节点 Profile 文件:${NC}"
local i p node_id env_mode ip user
i=1
for p in "${menu[@]}"; do
node_id=$(read_profile_var "$p" DCTS_NODE_ID)
env_mode=$(read_profile_var "$p" DEPLOY_ENV "")
ip=$(read_profile_var "$p" REMOTE_IP "")
user=$(read_profile_var "$p" REMOTE_USER "fmq")
[ -n "$node_id" ] || node_id="${user}@${ip:-localhost}"
local tag="远程" ipshow="$ip"
if [ "${env_mode}" = "local" ]; then tag="本机"; ipshow="(本机)"; fi
printf " %d) %s (%s) %s@%s [%s]\n" "$i" "$node_id" "${p##*/}" "$user" "$ipshow" "$tag"
((i++))
done
echo -e " a) [全部节点] 依次同步上述全部 ${#menu[@]} 个节点"
read -r -p "请选择节点 [序号如 1,2 或 1-2; 全部如 a; 默认 0=退出]: " CHOICE
CHOICE=${CHOICE:-0}
SELECTED_PROFILES=()
if [[ "${CHOICE,,}" == "a" ]] || [[ "${CHOICE,,}" == "all" ]]; then
SELECTED_PROFILES=("${menu[@]}")
elif [[ "${CHOICE}" =~ ^[0-9] ]]; then
local part from to n
IFS=',' read -r -a ADDR <<< "${CHOICE}"
for part in "${ADDR[@]}"; do
if [[ "$part" =~ ^([0-9]+)-([0-9]+)$ ]]; then
from="${BASH_REMATCH[1]}"; to="${BASH_REMATCH[2]}"
for ((n=from; n<=to; n++)); do
if [ "$n" -ge 1 ] && [ "$n" -le "${#menu[@]}" ]; then
SELECTED_PROFILES+=("${menu[$((n-1))]}")
fi
done
elif [[ "$part" =~ ^[0-9]+$ ]]; then
if [ "$part" -ge 1 ] && [ "$part" -le "${#menu[@]}" ]; then
SELECTED_PROFILES+=("${menu[$((part-1))]}")
fi
fi
done
fi
if [ ${#SELECTED_PROFILES[@]} -eq 0 ]; then
echo -e "${YELLOW}未选择任何节点,退出。${NC}"
exit 0
fi
# 是否连带同步 data/work 沙盒残留
if [ "${WITH_WORK}" != "true" ]; then
read -r -p "是否连带同步 data/work 沙盒残留(未清理的计算现场,含完整过程文件)? [y/N]: " WK_CHOICE
[[ "${WK_CHOICE,,}" == "y" || "${WK_CHOICE,,}" == "yes" ]] && WITH_WORK=true
fi
# 用户显式勾选本机节点 → 自动纳入备份树(否则 backup_node 会跳过本机)
local p2 env2
for p2 in "${SELECTED_PROFILES[@]}"; do
env2=$(read_profile_var "$p2" DEPLOY_ENV "")
[ "${env2}" = "local" ] && INCLUDE_LOCAL=true
done
echo -e "${CYAN}将同步 ${#SELECTED_PROFILES[@]} 个节点 → ${BACKUP_ROOT}${NC}"
}
if [ "${HAD_ARGS}" = "false" ] || [ "${INTERACTIVE}" = "true" ]; then
interactive_select
fi
# =============================================================================
# 主流程:遍历 deploy.env.d/*.env(排除 *.example
# =============================================================================
echo -e "${CYAN}备份根目录: ${BACKUP_ROOT}${NC}"
echo -e "${CYAN}SSH 复用目录: ${SSH_CONTROL_DIR}${NC}"
mkdir -p "${SSH_CONTROL_DIR}"
profiles=()
for p in deploy.env.d/*.env; do
[ -f "$p" ] || continue
[[ "$p" != *.example ]] || continue
profiles+=("$p")
done
if [ ${#profiles[@]} -eq 0 ]; then
echo -e "${RED}错误: deploy.env.d/ 下没有可用的 node profile${NC}"
exit 1
fi
# 交互式模式下仅同步用户勾选的节点
if [ ${#SELECTED_PROFILES[@]} -gt 0 ]; then
profiles=("${SELECTED_PROFILES[@]}")
fi
for p in "${profiles[@]}"; do
backup_node "$p" || true
done
echo -e "\n${GREEN}===== 全部节点处理完成,备份位于 ${BACKUP_ROOT} =====${NC}"