Spaces:
Running
Running
| set -euo pipefail | |
| OPENCLAW_HF_DATA_ROOT="${OPENCLAW_HF_DATA_ROOT:-/data/openclaw}" | |
| OPENCLAW_HF_LIVE_ROOT="${OPENCLAW_HF_LIVE_ROOT:-${OPENCLAW_HF_DATA_ROOT}/live/.openclaw}" | |
| OPENCLAW_HF_SYNC_ROOT="${OPENCLAW_HF_SYNC_ROOT:-${OPENCLAW_HF_DATA_ROOT}/sync}" | |
| OPENCLAW_HF_LOG_ROOT="${OPENCLAW_HF_LOG_ROOT:-${OPENCLAW_HF_DATA_ROOT}/logs}" | |
| OPENCLAW_HF_INSTALL_ROOT="${OPENCLAW_HF_INSTALL_ROOT:-${OPENCLAW_HF_DATA_ROOT}/install}" | |
| OPENCLAW_HF_HOME_LIVE_ROOT="${OPENCLAW_HF_HOME_LIVE_ROOT:-${OPENCLAW_HF_DATA_ROOT}/live/root-home}" | |
| OPENCLAW_HF_RUNTIME_HOME="${OPENCLAW_HF_RUNTIME_HOME:-${HOME:-/root}}" | |
| OPENCLAW_HF_RUNTIME_ROOT="${OPENCLAW_HF_RUNTIME_ROOT:-${OPENCLAW_HF_RUNTIME_HOME}/.openclaw}" | |
| OPENCLAW_HF_STATE_LINK_MODE="${OPENCLAW_HF_STATE_LINK_MODE:-mixed}" | |
| OPENCLAW_HF_SNAPSHOT_LIMIT="${OPENCLAW_HF_SNAPSHOT_LIMIT:-20}" | |
| OPENCLAW_HF_MANIFEST_HASH_LIMIT_BYTES="${OPENCLAW_HF_MANIFEST_HASH_LIMIT_BYTES:-10485760}" | |
| OPENCLAW_HF_CONFIG_CONFLICT_POLICY="${OPENCLAW_HF_CONFIG_CONFLICT_POLICY:-data_wins}" | |
| OPENCLAW_HF_STRONG_DIR_RESTORE_POLICY="${OPENCLAW_HF_STRONG_DIR_RESTORE_POLICY:-data_wins}" | |
| OPENCLAW_HF_LOCAL_SYNC_RESTORE_POLICY="${OPENCLAW_HF_LOCAL_SYNC_RESTORE_POLICY:-newer_wins}" | |
| OPENCLAW_HF_LIGHT_MANIFEST_MODE="${OPENCLAW_HF_LIGHT_MANIFEST_MODE:-1}" | |
| OPENCLAW_HF_LINKED_FILES=("openclaw.json" "auth-profiles.json") | |
| OPENCLAW_HF_LINKED_DIRS=("credentials" "agents" "workspace" "openclaw-weixin") | |
| OPENCLAW_HF_LOCAL_SYNC_DIRS=("cron" "media" "extensions") | |
| OPENCLAW_HF_EXTERNAL_LINKED_DIRS=() | |
| hf_log() { | |
| local level="$1" | |
| shift | |
| printf '[openclaw-hf][%s] %s\n' "$level" "$*" | |
| } | |
| hf_now() { | |
| date -u +"%Y-%m-%dT%H:%M:%SZ" | |
| } | |
| hf_now_compact() { | |
| date -u +%Y%m%dT%H%M%SZ | |
| } | |
| hf_join_by() { | |
| local delimiter="$1" | |
| shift || true | |
| local first=1 | |
| local item | |
| for item in "$@"; do | |
| if (( first == 1 )); then | |
| printf '%s' "${item}" | |
| first=0 | |
| else | |
| printf '%s%s' "${delimiter}" "${item}" | |
| fi | |
| done | |
| } | |
| hf_escape_json() { | |
| local value="$1" | |
| value=${value//\\/\\\\} | |
| value=${value//"/\\"} | |
| value=${value//$'\n'/\\n} | |
| value=${value//$'\r'/\\r} | |
| value=${value//$'\t'/\\t} | |
| printf '%s' "${value}" | |
| } | |
| hf_ensure_tree() { | |
| mkdir -p \ | |
| "${OPENCLAW_HF_LIVE_ROOT}" \ | |
| "${OPENCLAW_HF_HOME_LIVE_ROOT}" \ | |
| "${OPENCLAW_HF_SYNC_ROOT}/manifests" \ | |
| "${OPENCLAW_HF_SYNC_ROOT}/locks" \ | |
| "${OPENCLAW_HF_SYNC_ROOT}/queue" \ | |
| "${OPENCLAW_HF_SYNC_ROOT}/snapshots" \ | |
| "${OPENCLAW_HF_LOG_ROOT}/archive" \ | |
| "${OPENCLAW_HF_LOG_ROOT}/syncd" \ | |
| "${OPENCLAW_HF_LOG_ROOT}/startup" \ | |
| "${OPENCLAW_HF_INSTALL_ROOT}/plugins" \ | |
| "${OPENCLAW_HF_INSTALL_ROOT}/skills" \ | |
| "${OPENCLAW_HF_INSTALL_ROOT}/bootstrap" \ | |
| "${OPENCLAW_HF_RUNTIME_ROOT}" | |
| } | |
| hf_relative_path() { | |
| local full_path="$1" | |
| local root_path="$2" | |
| local resolved_full | |
| local resolved_root | |
| resolved_full="$(realpath -m "${full_path}")" | |
| resolved_root="$(realpath -m "${root_path}")" | |
| if [[ "${resolved_full}" == "${resolved_root}" ]]; then | |
| printf '.' | |
| return 0 | |
| fi | |
| printf '%s' "${resolved_full#${resolved_root}/}" | |
| } | |
| hf_snapshot_path_if_exists() { | |
| local source_path="$1" | |
| local label="$2" | |
| if [[ ! -e "${source_path}" ]]; then | |
| return 0 | |
| fi | |
| local stamp | |
| stamp="$(hf_now_compact)" | |
| local target_dir="${OPENCLAW_HF_SYNC_ROOT}/snapshots/${label}" | |
| mkdir -p "${target_dir}" | |
| local base_name | |
| base_name="$(basename "${source_path}")" | |
| if [[ -d "${source_path}" && ! -L "${source_path}" ]]; then | |
| cp -a "${source_path}" "${target_dir}/${base_name}.${stamp}.bak" | |
| else | |
| cp -a "${source_path}" "${target_dir}/${base_name}.${stamp}.bak" | |
| fi | |
| } | |
| hf_paths_differ() { | |
| local left_path="$1" | |
| local right_path="$2" | |
| if [[ ! -e "${left_path}" || ! -e "${right_path}" ]]; then | |
| return 1 | |
| fi | |
| if [[ -d "${left_path}" && -d "${right_path}" && ! -L "${left_path}" && ! -L "${right_path}" ]]; then | |
| if diff -qr "${left_path}" "${right_path}" >/dev/null 2>&1; then | |
| return 1 | |
| fi | |
| return 0 | |
| fi | |
| if cmp -s "${left_path}" "${right_path}" 2>/dev/null; then | |
| return 1 | |
| fi | |
| return 0 | |
| } | |
| hf_snapshot_conflict_pair() { | |
| local runtime_path="$1" | |
| local live_path="$2" | |
| local label="$3" | |
| local target_dir="${OPENCLAW_HF_SYNC_ROOT}/snapshots/${label}/$(hf_now_compact)" | |
| mkdir -p "${target_dir}" | |
| if [[ -e "${runtime_path}" ]]; then | |
| cp -a "${runtime_path}" "${target_dir}/runtime-$(basename "${runtime_path}")" | |
| fi | |
| if [[ -e "${live_path}" ]]; then | |
| cp -a "${live_path}" "${target_dir}/live-$(basename "${live_path}")" | |
| fi | |
| hf_trim_snapshot_dir "${OPENCLAW_HF_SYNC_ROOT}/snapshots/${label}" | |
| } | |
| hf_trim_snapshot_dir() { | |
| local target_dir="$1" | |
| if [[ ! -d "${target_dir}" ]]; then | |
| return 0 | |
| fi | |
| local excess | |
| excess=$(find "${target_dir}" -mindepth 1 -maxdepth 1 | wc -l) | |
| if (( excess <= OPENCLAW_HF_SNAPSHOT_LIMIT )); then | |
| return 0 | |
| fi | |
| find "${target_dir}" -mindepth 1 -maxdepth 1 -printf '%T@ %p\n' | sort -n | head -n $((excess - OPENCLAW_HF_SNAPSHOT_LIMIT)) | while read -r _old path_name; do | |
| rm -rf "${path_name}" | |
| done | |
| } | |
| hf_acquire_lock() { | |
| local name="$1" | |
| local lock_dir="${OPENCLAW_HF_SYNC_ROOT}/locks/${name}.lock" | |
| local waited=0 | |
| while ! mkdir "${lock_dir}" 2>/dev/null; do | |
| local pid_file start_file command_file held_pid held_start held_command current_start current_command | |
| pid_file="${lock_dir}/pid" | |
| start_file="${lock_dir}/starttime" | |
| command_file="${lock_dir}/command" | |
| held_pid="" | |
| held_start="" | |
| held_command="" | |
| if [[ -f "${pid_file}" ]]; then | |
| held_pid="$(tr -d '[:space:]' < "${pid_file}" 2>/dev/null || true)" | |
| fi | |
| if [[ -f "${start_file}" ]]; then | |
| held_start="$(tr -d '[:space:]' < "${start_file}" 2>/dev/null || true)" | |
| fi | |
| if [[ -f "${command_file}" ]]; then | |
| held_command="$(cat "${command_file}" 2>/dev/null || true)" | |
| fi | |
| if [[ -z "${held_pid}" || -z "${held_start}" || -z "${held_command}" ]]; then | |
| hf_log WARN "reaping stale lock ${name} with incomplete metadata" | |
| rm -rf "${lock_dir}" | |
| continue | |
| fi | |
| if ! kill -0 "${held_pid}" 2>/dev/null; then | |
| hf_log WARN "reaping stale lock ${name} held by dead pid ${held_pid}" | |
| rm -rf "${lock_dir}" | |
| continue | |
| fi | |
| current_start="$(awk '{print $22}' "/proc/${held_pid}/stat" 2>/dev/null || true)" | |
| current_command="$(tr '\0' ' ' < "/proc/${held_pid}/cmdline" 2>/dev/null || true)" | |
| if [[ -z "${current_start}" || "${current_start}" != "${held_start}" ]]; then | |
| hf_log WARN "reaping stale lock ${name} held by recycled pid ${held_pid}" | |
| rm -rf "${lock_dir}" | |
| continue | |
| fi | |
| if [[ "${current_command}" != *"openclaw-hf"* && "${current_command}" != *"start-hf.sh"* && "${current_command}" != *"syncd.sh"* && "${current_command}" != *"hf-sync.sh"* ]]; then | |
| hf_log WARN "reaping stale lock ${name} held by unrelated pid ${held_pid}" | |
| rm -rf "${lock_dir}" | |
| continue | |
| fi | |
| waited=$((waited + 1)) | |
| if (( waited > 120 )); then | |
| hf_log ERROR "timed out waiting for lock ${name}" | |
| return 1 | |
| fi | |
| sleep 1 | |
| done | |
| printf '%s\n' "$$" > "${lock_dir}/pid" | |
| awk '{print $22}' "/proc/$$/stat" > "${lock_dir}/starttime" | |
| tr '\0' ' ' < "/proc/$$/cmdline" > "${lock_dir}/command" | |
| } | |
| hf_release_lock() { | |
| local name="$1" | |
| rm -rf "${OPENCLAW_HF_SYNC_ROOT}/locks/${name}.lock" | |
| } | |
| hf_rsync_dir() { | |
| local source_dir="$1" | |
| local target_dir="$2" | |
| mkdir -p "${target_dir}" | |
| rsync -a --delete --safe-links "${source_dir}/" "${target_dir}/" | |
| } | |
| hf_rsync_dir_no_delete() { | |
| local source_dir="$1" | |
| local target_dir="$2" | |
| mkdir -p "${target_dir}" | |
| rsync -a --safe-links "${source_dir}/" "${target_dir}/" | |
| } | |
| hf_sync_file_if_newer() { | |
| local preferred="$1" | |
| local fallback="$2" | |
| local preferred_real fallback_real | |
| preferred_real="$(realpath -m "${preferred}")" | |
| fallback_real="$(realpath -m "${fallback}")" | |
| if [[ "${preferred_real}" == "${fallback_real}" ]]; then | |
| return 0 | |
| fi | |
| if [[ -f "${preferred}" && -f "${fallback}" ]]; then | |
| if [[ "${preferred}" -nt "${fallback}" ]]; then | |
| cp -f "${preferred}" "${fallback}" | |
| elif [[ "${fallback}" -nt "${preferred}" ]]; then | |
| cp -f "${fallback}" "${preferred}" | |
| fi | |
| return 0 | |
| fi | |
| if [[ -f "${preferred}" && ! -f "${fallback}" ]]; then | |
| mkdir -p "$(dirname "${fallback}")" | |
| cp -f "${preferred}" "${fallback}" | |
| return 0 | |
| fi | |
| if [[ -f "${fallback}" && ! -f "${preferred}" ]]; then | |
| mkdir -p "$(dirname "${preferred}")" | |
| cp -f "${fallback}" "${preferred}" | |
| fi | |
| } | |
| hf_hash_file_if_small() { | |
| local file_path="$1" | |
| local file_size="$2" | |
| if (( file_size > OPENCLAW_HF_MANIFEST_HASH_LIMIT_BYTES )); then | |
| printf '' | |
| return 0 | |
| fi | |
| sha256sum "${file_path}" | awk '{print $1}' | |
| } | |
| hf_manifest_for_path() { | |
| local root_path="$1" | |
| if [[ ! -e "${root_path}" ]]; then | |
| printf '[]' | |
| return 0 | |
| fi | |
| # 轻量模式只记录目录摘要,避免对大目录做文件级遍历后把 sync/ 撑爆。 | |
| if [[ "${OPENCLAW_HF_LIGHT_MANIFEST_MODE}" == "1" ]]; then | |
| local file_count total_bytes latest_mtime | |
| file_count=$(find "${root_path}" \( -type f -o -type l \) | wc -l) | |
| total_bytes=$(find "${root_path}" -type f -printf '%s\n' 2>/dev/null | awk '{sum += $1} END {print sum + 0}') | |
| latest_mtime=$(find "${root_path}" \( -type f -o -type l \) -printf '%T@\n' 2>/dev/null | sort -nr | head -n 1) | |
| latest_mtime=${latest_mtime%%.*} | |
| latest_mtime=${latest_mtime:-0} | |
| printf '[{"path":".","type":"summary","files":%s,"bytes":%s,"latestMtime":%s}]' "${file_count}" "${total_bytes}" "${latest_mtime}" | |
| return 0 | |
| fi | |
| local entries=() | |
| local item | |
| while IFS= read -r item; do | |
| [[ -z "${item}" ]] && continue | |
| if [[ -d "${item}" && ! -L "${item}" ]]; then | |
| continue | |
| fi | |
| local rel_path | |
| rel_path="$(hf_relative_path "${item}" "${root_path}")" | |
| local entry_type="file" | |
| local item_size=0 | |
| if [[ -L "${item}" ]]; then | |
| entry_type="symlink" | |
| fi | |
| if [[ -f "${item}" ]]; then | |
| item_size=$(stat -c '%s' "${item}") | |
| fi | |
| local item_mtime | |
| item_mtime=$(stat -c '%Y' "${item}") | |
| local hash_value="" | |
| if [[ "${entry_type}" == "file" ]]; then | |
| hash_value="$(hf_hash_file_if_small "${item}" "${item_size}")" | |
| fi | |
| entries+=("{\"path\":\"$(hf_escape_json "${rel_path}")\",\"type\":\"${entry_type}\",\"size\":${item_size},\"mtime\":${item_mtime},\"sha256\":\"${hash_value}\"}") | |
| done < <(find "${root_path}" \( -type f -o -type l \) | sort) | |
| if (( ${#entries[@]} == 0 )); then | |
| printf '[]' | |
| return 0 | |
| fi | |
| printf '[%s]' "$(hf_join_by ',' "${entries[@]}")" | |
| } | |
| hf_write_manifest() { | |
| local name="$1" | |
| local status="$2" | |
| local detail="$3" | |
| shift 3 | |
| local target="${OPENCLAW_HF_SYNC_ROOT}/manifests/${name}.json" | |
| local sections=() | |
| local section_path | |
| for section_path in "$@"; do | |
| [[ -z "${section_path}" ]] && continue | |
| local section_name | |
| section_name="$(basename "${section_path}")" | |
| sections+=("{\"root\":\"$(hf_escape_json "${section_path}")\",\"name\":\"$(hf_escape_json "${section_name}")\",\"entries\":$(hf_manifest_for_path "${section_path}")}") | |
| done | |
| cat > "${target}" <<EOF | |
| { | |
| "name": "$(hf_escape_json "${name}")", | |
| "status": "$(hf_escape_json "${status}")", | |
| "detail": "$(hf_escape_json "${detail}")", | |
| "updatedAt": "$(hf_now)", | |
| "stateLinkMode": "$(hf_escape_json "${OPENCLAW_HF_STATE_LINK_MODE}")", | |
| "sections": [$(hf_join_by ',' "${sections[@]}")] | |
| } | |
| EOF | |
| } | |
| hf_queue_set() { | |
| local value="$1" | |
| printf '%s\n' "${value}" > "${OPENCLAW_HF_SYNC_ROOT}/queue/current-tier" | |
| } | |
| hf_queue_get() { | |
| if [[ -f "${OPENCLAW_HF_SYNC_ROOT}/queue/current-tier" ]]; then | |
| cat "${OPENCLAW_HF_SYNC_ROOT}/queue/current-tier" | |
| return 0 | |
| fi | |
| printf 'tier1\n' | |
| } | |
| hf_runtime_path() { | |
| local subpath="$1" | |
| printf '%s/%s' "${OPENCLAW_HF_RUNTIME_ROOT}" "${subpath}" | |
| } | |
| hf_live_path() { | |
| local subpath="$1" | |
| printf '%s/%s' "${OPENCLAW_HF_LIVE_ROOT}" "${subpath}" | |
| } | |
| hf_home_live_path() { | |
| local subpath="$1" | |
| printf '%s/%s' "${OPENCLAW_HF_HOME_LIVE_ROOT}" "${subpath}" | |
| } | |
| hf_prepare_runtime_dir() { | |
| local subpath="$1" | |
| mkdir -p "$(hf_runtime_path "${subpath}")" | |
| mkdir -p "$(hf_live_path "${subpath}")" | |
| } | |
| hf_link_path() { | |
| local runtime_path="$1" | |
| local live_path="$2" | |
| mkdir -p "$(dirname "${runtime_path}")" "$(dirname "${live_path}")" | |
| if [[ -L "${runtime_path}" ]]; then | |
| local existing_target | |
| existing_target="$(readlink "${runtime_path}")" | |
| if [[ "${existing_target}" == "${live_path}" ]]; then | |
| return 0 | |
| fi | |
| rm -f "${runtime_path}" | |
| elif [[ -e "${runtime_path}" ]]; then | |
| hf_snapshot_path_if_exists "${runtime_path}" pre-link | |
| rm -rf "${runtime_path}" | |
| fi | |
| ln -s "${live_path}" "${runtime_path}" | |
| } | |
| hf_materialize_live_file() { | |
| local runtime_file="$1" | |
| local live_file="$2" | |
| mkdir -p "$(dirname "${runtime_file}")" "$(dirname "${live_file}")" | |
| if [[ "$(realpath -m "${runtime_file}")" == "$(realpath -m "${live_file}")" ]]; then | |
| return 0 | |
| fi | |
| if [[ -f "${runtime_file}" && ! -e "${live_file}" ]]; then | |
| cp -f "${runtime_file}" "${live_file}" | |
| elif [[ -f "${runtime_file}" && -f "${live_file}" ]]; then | |
| if hf_paths_differ "${runtime_file}" "${live_file}"; then | |
| hf_snapshot_conflict_pair "${runtime_file}" "${live_file}" config-conflict | |
| fi | |
| case "${OPENCLAW_HF_CONFIG_CONFLICT_POLICY}" in | |
| runtime_wins) | |
| cp -f "${runtime_file}" "${live_file}" | |
| ;; | |
| newer_wins) | |
| hf_sync_file_if_newer "${live_file}" "${runtime_file}" | |
| hf_sync_file_if_newer "${runtime_file}" "${live_file}" | |
| ;; | |
| *) | |
| cp -f "${live_file}" "${runtime_file}" | |
| ;; | |
| esac | |
| elif [[ -f "${live_file}" && ! -e "${runtime_file}" ]]; then | |
| cp -f "${live_file}" "${runtime_file}" | |
| fi | |
| } | |
| hf_materialize_live_dir() { | |
| local runtime_dir="$1" | |
| local live_dir="$2" | |
| mkdir -p "${live_dir}" | |
| if [[ -d "${runtime_dir}" && ! -L "${runtime_dir}" && ! -d "${live_dir}" ]]; then | |
| hf_rsync_dir_no_delete "${runtime_dir}" "${live_dir}" | |
| return 0 | |
| fi | |
| mkdir -p "${runtime_dir}" | |
| if [[ -d "${runtime_dir}" && ! -L "${runtime_dir}" && -d "${live_dir}" ]]; then | |
| if hf_paths_differ "${runtime_dir}" "${live_dir}"; then | |
| hf_snapshot_conflict_pair "${runtime_dir}" "${live_dir}" strong-dir-conflict | |
| fi | |
| case "${OPENCLAW_HF_STRONG_DIR_RESTORE_POLICY}" in | |
| runtime_wins) | |
| hf_rsync_dir_no_delete "${runtime_dir}" "${live_dir}" | |
| ;; | |
| newer_wins) | |
| hf_rsync_dir_no_delete "${live_dir}" "${runtime_dir}" | |
| hf_rsync_dir_no_delete "${runtime_dir}" "${live_dir}" | |
| ;; | |
| *) | |
| hf_rsync_dir_no_delete "${live_dir}" "${runtime_dir}" | |
| ;; | |
| esac | |
| fi | |
| } | |
| hf_prepare_linked_state() { | |
| local file_name | |
| for file_name in "${OPENCLAW_HF_LINKED_FILES[@]}"; do | |
| local runtime_file live_file | |
| runtime_file="$(hf_runtime_path "${file_name}")" | |
| live_file="$(hf_live_path "${file_name}")" | |
| hf_materialize_live_file "${runtime_file}" "${live_file}" | |
| if [[ "${OPENCLAW_HF_STATE_LINK_MODE}" != "copy" ]]; then | |
| hf_link_path "${runtime_file}" "${live_file}" | |
| fi | |
| done | |
| local dir_name | |
| for dir_name in "${OPENCLAW_HF_LINKED_DIRS[@]}"; do | |
| local runtime_dir live_dir | |
| runtime_dir="$(hf_runtime_path "${dir_name}")" | |
| live_dir="$(hf_live_path "${dir_name}")" | |
| hf_materialize_live_dir "${runtime_dir}" "${live_dir}" | |
| if [[ "${OPENCLAW_HF_STATE_LINK_MODE}" != "copy" ]]; then | |
| hf_link_path "${runtime_dir}" "${live_dir}" | |
| else | |
| mkdir -p "${runtime_dir}" | |
| hf_rsync_dir_no_delete "${live_dir}" "${runtime_dir}" | |
| fi | |
| done | |
| # 这类目录不在 ~/.openclaw 下,但对浏览器工具与插件运行态同样关键,需要单独持久化到 /data。 | |
| for dir_name in "${OPENCLAW_HF_EXTERNAL_LINKED_DIRS[@]}"; do | |
| local runtime_dir live_dir | |
| runtime_dir="${OPENCLAW_HF_RUNTIME_HOME}/${dir_name}" | |
| live_dir="$(hf_home_live_path "${dir_name}")" | |
| hf_materialize_live_dir "${runtime_dir}" "${live_dir}" | |
| if [[ "${OPENCLAW_HF_STATE_LINK_MODE}" != "copy" ]]; then | |
| hf_link_path "${runtime_dir}" "${live_dir}" | |
| else | |
| mkdir -p "${runtime_dir}" | |
| hf_rsync_dir_no_delete "${live_dir}" "${runtime_dir}" | |
| fi | |
| done | |
| } | |
| hf_restore_local_sync_dir() { | |
| local dir_name="$1" | |
| local runtime_dir live_dir | |
| runtime_dir="$(hf_runtime_path "${dir_name}")" | |
| live_dir="$(hf_live_path "${dir_name}")" | |
| mkdir -p "${runtime_dir}" "${live_dir}" | |
| if hf_paths_differ "${runtime_dir}" "${live_dir}"; then | |
| hf_snapshot_conflict_pair "${runtime_dir}" "${live_dir}" local-sync-restore | |
| fi | |
| case "${OPENCLAW_HF_LOCAL_SYNC_RESTORE_POLICY}" in | |
| runtime_wins) | |
| hf_rsync_dir_no_delete "${runtime_dir}" "${live_dir}" | |
| ;; | |
| data_wins) | |
| hf_rsync_dir_no_delete "${live_dir}" "${runtime_dir}" | |
| ;; | |
| *) | |
| hf_rsync_dir_no_delete "${live_dir}" "${runtime_dir}" | |
| hf_rsync_dir_no_delete "${runtime_dir}" "${live_dir}" | |
| ;; | |
| esac | |
| } | |
| hf_sync_linked_state_to_live() { | |
| local dir_name | |
| for dir_name in "${OPENCLAW_HF_LINKED_DIRS[@]}"; do | |
| local live_dir runtime_dir | |
| live_dir="$(hf_live_path "${dir_name}")" | |
| runtime_dir="$(hf_runtime_path "${dir_name}")" | |
| if [[ "${OPENCLAW_HF_STATE_LINK_MODE}" == "copy" ]]; then | |
| hf_rsync_dir "${runtime_dir}" "${live_dir}" | |
| fi | |
| done | |
| local file_name | |
| for file_name in "${OPENCLAW_HF_LINKED_FILES[@]}"; do | |
| local live_file runtime_file | |
| live_file="$(hf_live_path "${file_name}")" | |
| runtime_file="$(hf_runtime_path "${file_name}")" | |
| if [[ -f "${runtime_file}" ]]; then | |
| cp -f "${runtime_file}" "${live_file}" | |
| fi | |
| done | |
| } | |
| hf_archive_tmp_logs() { | |
| if [[ -d /tmp/openclaw ]]; then | |
| local archive_dir | |
| archive_dir="${OPENCLAW_HF_LOG_ROOT}/archive/$(date -u +%Y%m%d)" | |
| mkdir -p "${archive_dir}" | |
| rsync -a --safe-links /tmp/openclaw/ "${archive_dir}/" | |
| hf_trim_snapshot_dir "${OPENCLAW_HF_LOG_ROOT}/archive" | |
| fi | |
| } | |
| hf_sync_install_assets() { | |
| if [[ -d "${OPENCLAW_HF_RUNTIME_ROOT}/skills" && ! -L "${OPENCLAW_HF_RUNTIME_ROOT}/skills" ]]; then | |
| hf_rsync_dir_no_delete "${OPENCLAW_HF_RUNTIME_ROOT}/skills" "${OPENCLAW_HF_INSTALL_ROOT}/skills/runtime-skills" | |
| fi | |
| } | |
| hf_sync_local_sync_dir() { | |
| local dir_name="$1" | |
| local runtime_dir live_dir | |
| runtime_dir="$(hf_runtime_path "${dir_name}")" | |
| live_dir="$(hf_live_path "${dir_name}")" | |
| mkdir -p "${runtime_dir}" "${live_dir}" | |
| # extensions 目录包含大量 node_modules 文件,HF 桶上的覆盖写经常触发 rsync mkstemp 权限问题。 | |
| # 这里改成“先清空目标,再整目录复制”的方式,牺牲一点性能换稳定同步。 | |
| if [[ "${dir_name}" == "extensions" ]]; then | |
| rm -rf "${live_dir}"/* | |
| cp -a "${runtime_dir}/." "${live_dir}/" | |
| return 0 | |
| fi | |
| hf_rsync_dir_no_delete "${runtime_dir}" "${live_dir}" | |
| } | |
| hf_build_state_summary() { | |
| cat <<EOF | |
| runtimeRoot=$(hf_escape_json "${OPENCLAW_HF_RUNTIME_ROOT}") | |
| homeLiveRoot=$(hf_escape_json "${OPENCLAW_HF_HOME_LIVE_ROOT}") | |
| liveRoot=$(hf_escape_json "${OPENCLAW_HF_LIVE_ROOT}") | |
| linkMode=$(hf_escape_json "${OPENCLAW_HF_STATE_LINK_MODE}") | |
| lightManifestMode=$(hf_escape_json "${OPENCLAW_HF_LIGHT_MANIFEST_MODE}") | |
| configConflictPolicy=$(hf_escape_json "${OPENCLAW_HF_CONFIG_CONFLICT_POLICY}") | |
| strongDirRestorePolicy=$(hf_escape_json "${OPENCLAW_HF_STRONG_DIR_RESTORE_POLICY}") | |
| localSyncRestorePolicy=$(hf_escape_json "${OPENCLAW_HF_LOCAL_SYNC_RESTORE_POLICY}") | |
| linkedFiles=$(hf_escape_json "$(hf_join_by ',' "${OPENCLAW_HF_LINKED_FILES[@]}")") | |
| linkedDirs=$(hf_escape_json "$(hf_join_by ',' "${OPENCLAW_HF_LINKED_DIRS[@]}")") | |
| externalLinkedDirs=$(hf_escape_json "$(hf_join_by ',' "${OPENCLAW_HF_EXTERNAL_LINKED_DIRS[@]}")") | |
| localSyncDirs=$(hf_escape_json "$(hf_join_by ',' "${OPENCLAW_HF_LOCAL_SYNC_DIRS[@]}")") | |
| EOF | |
| } | |