feat(all): 数据库模块化拆分与版本化迁移、任务引擎命名体系收敛、物理输出校验加固与用户配置接通

- server/db: 拆 4929 行 db.rs 单体为 db/ 目录,migrations.rs 引入 PRAGMA user_version
    版本化迁移运行器(M1~M13)
  - 任务引擎 Phase 6/7b/7c 改名收敛:EngineStageConfig→PhaseConfig、StagePolicy→ResumePolicy、
    Converged→Completed、删除 task_type 列、success_method 拆 tlusty_/synspec_ 双列、
    新增 tlusty_status/synspec_status 半失败阶段守卫
  - 科学正确性加固:conv_check 任意行 NaN/Inf/溢出判无效(0 行容忍)、新增 spec_is_valid
    校验 SYNSPEC 脏谱、itek_history 逐次迭代全量保真、fmt_abn powf 溢出饱和
  - 用户配置真正接通:tlusty_chain/tlusty_input 由死字段经 调度器→TaskSpec→executor→runner
    透传生效;config 加载期 validate + deny_unknown_fields + 解析失败记 warn
  - 调度修复:H1 活锁(pending_strategies 跳过已失败策略)、种子查找错误不再静默降级冷启动
  - dashboard: 阶段配置面板 tlusty_stage/synspec_stage、"已完成"标签、迭代诊断展示
  - docs: 新增 database_refactor_design.md,同步 database/api/PIPELINE/workflow_detail
This commit is contained in:
fmq
2026-08-06 20:51:21 +08:00
parent cd370d88e7
commit d16b3d3cdc
61 changed files with 10268 additions and 5881 deletions
+37 -2
View File
@@ -12,7 +12,7 @@
# ./scripts/deploy.sh -e remote -b compose -r all --skip-assets # 跳过静态资源同步,仅更新程序代码
# ==============================================================================
set -eo pipefail
set -euo pipefail
GREEN='\033[0;32m'
BLUE='\033[0;34m'
@@ -45,6 +45,37 @@ PROFILE_FILE=""
PROFILE_LOADED=""
PASSED_CLI_ARGS=("$@")
# 在 set -u 下,以下"可选/按需出现"的变量若从未赋值,引用即报错。这里统一预置
# 空初值,保留"笔误即报错"的语义(真正的拼写错误仍会在 set -u 下被捕获),
# 仅对确实合法的可选变量放开。DEPLOY_* 来自可能未定义的 Profile/env,最易踩坑。
ACTION=""
PROFILE_ARG=""
CLI_ENV_SET=""
CLI_BACKEND_SET=""
CLI_ROLE_SET=""
CLI_ASSETS_SET=""
SYNC_ASSETS=""
# Profile 中可能定义的运行期变量(DCTS_*/LOG_*),write_remote_runtime_env 会读取。
DEPLOY_ENV="${DEPLOY_ENV:-}"
DEPLOY_BACKEND="${DEPLOY_BACKEND:-}"
DEPLOY_ROLE="${DEPLOY_ROLE:-}"
DCTS_NODE_ID="${DCTS_NODE_ID:-}"
DCTS_SERVER_URL="${DCTS_SERVER_URL:-}"
DCTS_MAX_SLOTS="${DCTS_MAX_SLOTS:-}"
DCTS_PORT="${DCTS_PORT:-}"
DCTS_ADMIN_TOKEN="${DCTS_ADMIN_TOKEN:-}"
DCTS_AUTH_DISABLE="${DCTS_AUTH_DISABLE:-}"
LOG_DIR="${LOG_DIR:-}"
DCTS_LOG="${DCTS_LOG:-}"
DCTS_HEARTBEAT_SEC="${DCTS_HEARTBEAT_SEC:-}"
# 远程 systemd 部署时父进程通过 env 传入的预构建镜像归档(可能为空)。
PREBUILT_TAR="${PREBUILT_TAR:-}"
# SSH 复用相关:setup_ssh_control() 中赋值,但 check_remote_is_windows 等函数会先读取,
# 预置空初值避免 set -u 下"使用前未定义"。
SSH_OPTS=""
SSH_CONTROL_PATH=""
SSH_REUSED_CONNECTION=""
load_profile_file() {
local pfile="$1"
if [ -f "$pfile" ]; then
@@ -856,7 +887,11 @@ elif [ "${ENV_MODE}" = "remote" ] && [ "${BACKEND_MODE}" = "compose" ]; then
win_load_cmd="${win_load_cmd}Write-Output done"
ssh -p "${REMOTE_PORT}" ${SSH_OPTS} "${REMOTE_USER}@${REMOTE_IP}" "powershell -Command \"Set-Location '${REMOTE_DIR}'; New-Item -ItemType Directory -Force -Path 'data/logs','data/seeds','data/result','data/node_work','assets','workflows' -ErrorAction SilentlyContinue; ${win_load_cmd}; docker compose up -d --force-recreate ${SERVICES}; Start-Sleep -Seconds 3; docker compose ps\""
else
ssh -p "${REMOTE_PORT}" ${SSH_OPTS} "${REMOTE_USER}@${REMOTE_IP}" "cd '${REMOTE_DIR}' && mkdir -p ./data ./data/logs ./data/seeds ./data/result ./data/node_work ./assets ./workflows 2>/dev/null || true && (docker run --rm -v \"\$(pwd)\":/work alpine chown -R 65532:65532 /work/data 2>/dev/null || true) && ${remote_load_cmd} && docker compose up -d --force-recreate ${SERVICES} && sleep 3 && docker compose ps"
# 远端属主修正:容器内 UID 65532(dcts) 需对 ./data 有写权限才能归档产物。
# 优先 docker run alpine chown(不依赖远端 root);失败时若远端本身是 root 登录,
# 退化用系统 chown。任一成功即可,全失败则明显告警——旧版 `|| true` 会静默吞错,
# 导致节点上线后归档一律 Permission denied、默默丢失科学产物。
ssh -p "${REMOTE_PORT}" ${SSH_OPTS} "${REMOTE_USER}@${REMOTE_IP}" "cd '${REMOTE_DIR}' && mkdir -p ./data ./data/logs ./data/seeds ./data/result ./data/node_work ./assets ./workflows 2>/dev/null || true && ( { docker run --rm -v \"\$(pwd)\":/work alpine chown -R 65532:65532 /work/data && echo CHOWN_OK; } || { [ \"\$(id -u)\" = 0 ] && chown -R 65532:65532 ./data && echo CHOWN_OK_SYS; } || { echo '!! chown 失败:远端既无 docker 也非 root,data 属主未修正,节点归档将 Permission denied' >&2; exit 1; } ) && ${remote_load_cmd} && docker compose up -d --force-recreate ${SERVICES} && sleep 3 && docker compose ps"
fi
if [ -z "${PREBUILT_TAR}" ]; then
+123 -27
View File
@@ -14,14 +14,17 @@
# ./scripts/fetch_results.sh -d /backup/dcts-salvage # 覆盖备份根目录
# ./scripts/fetch_results.sh --with-work # 连带拉取 data/work 沙盒残留
# ./scripts/fetch_results.sh --include-local # 把本机节点也复制进备份树
# ./scripts/fetch_results.sh --force # 忽略完成标记,强制重新同步
#
# 传输策略:双方均可用 rsync → 增量同步(断点续传、幂等);否则退化为 scp -r 全量
# 拷贝(Windows 节点一般无 rsync,自动走 scp)。SSH 复用 deploy.sh 的 ControlMaster
# 连接复用,多次执行不重复输密码。
# 传输策略(两类节点都走增量,重复运行只补差异、不重拷全量):
# - 双方均可用 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 -eo pipefail
set -euo pipefail
GREEN='\033[0;32m'
BLUE='\033[0;34m'
@@ -43,7 +46,6 @@ BACKUP_ROOT="${BACKUP_ROOT:-${WORK_DIR}/data/salvage}"
ONLY_NODE=""
WITH_WORK=false
INCLUDE_LOCAL=false
FORCE=false
INTERACTIVE=false
HAD_ARGS=false
SELECTED_PROFILES=()
@@ -59,7 +61,6 @@ while [[ $# -gt 0 ]]; do
-i|--interactive) INTERACTIVE=true; shift ;;
--with-work) WITH_WORK=true; shift ;;
--include-local) INCLUDE_LOCAL=true; shift ;;
--force) FORCE=true; shift ;;
-h|--help)
sed -n '2,32p' "$0"
exit 0 ;;
@@ -89,44 +90,132 @@ rsync_remote_path() { # $1 = 远端绝对路径
esac
}
# 传输一个目录树(rsync 优先,退化 scp)
# 远端目录列表(仅顶层条目名,用于 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 "$(dirname "${dest}")"
# 完成标记:已成功同步过且未 --force 时跳过
if [ -f "${dest}/.fetch-complete" ] && [ "${FORCE}" != "true" ]; then
echo -e " ${CYAN}[i]${NC} 已有完成标记 ${dest}/.fetch-complete,跳过(--force 可重拉)"
return 0
fi
mkdir -p "${dest}"
# 连通性探测(BatchMode=no 允许交互输密码)。
# 注意探测命令必须用 `echo ok` 而非 `true`Windows OpenSSH 默认 shell 是
# cmd/PowerShell没有 Unix 的 true,会导致"密码正确但仍判连接失败"。
# 探测命令用 `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 可用性(本机 + 远端)
# rsync 可用性(本机 + 远端)。远端探测按平台分支:
# Windows 路径 → powershell Get-CommandPOSIX 路径 → command -v
local rs_ok=false
if command -v rsync >/dev/null 2>&1; then
if ssh -p "${port}" ${ssh_opts} "${u}@${ip}" "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
echo -e " ${YELLOW}[→]${NC} 远端无 rsync,退化 scp 全量拷贝: ${u}@${ip}:${rdir}/ → ${dest}/"
scp -P "${port}" ${ssh_opts} -r "${u}@${ip}:${rdir}/" "${dest}/"
# 无 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 的 RETURN 时清理钩子统一兜底,
# 避免下次因"目录已存在"误判为已同步而跳过、留下不完整子目录。
trap 'rm -rf "${dest}"/.*.partial 2>/dev/null || true' RETURN
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
touch "${dest}/.fetch-complete"
}
# 备份单个节点
@@ -172,15 +261,18 @@ backup_node() { # $1 = profile 文件
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}"
cp -a ./data/result/. "${dest_rc}/"
touch "${dest_rc}/.fetch-complete"
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 "${dest_rc}" | grep -cv '^\.fetch-complete$' || true) 个网格点子目录"
echo -e " ${GREEN}[√]${NC} result 已同步,共 $(ls -1 "${dest_rc}" 2>/dev/null | grep -c . || true) 个网格点子目录"
# 可选:一并拉取 data/work 沙盒残留(未清理的计算现场,含完整过程文件)
if [ "${WITH_WORK}" = "true" ]; then
@@ -188,9 +280,13 @@ backup_node() { # $1 = profile 文件
if [ "${env_mode}" = "local" ]; then
echo -e " ${BLUE}[→]${NC} 本机复制: ./data/work → ${dest_wk}"
mkdir -p "${dest_wk}"
cp -a ./data/work/. "${dest_wk}/" 2>/dev/null || true
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}" || return 1
sync_dir "${u}" "${ip}" "${port}" "${dir}/data/work" "${dest_wk}" || true
fi
fi
}