[{"name":"咕咕助手【恢复】","type":"shell","groupID":7,"specCustom":false,"spec":"0 4 * * *","executor":"bash","scriptMode":"input","script":"#!/usr/bin/env bash\n# 清绝咕咕助手 v2.5\n# 作者: 清绝 | 网址: blog.qjyg.de\n\n# 定义核心操作: \"up\" (上传) 或 \"down\" (恢复)\nACTION=\"down\"\n\n# 禁止 Git 弹出交互式提示\nexport GIT_TERMINAL_PROMPT=0\n\nset -e\nset -o pipefail\n\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nRED='\\033[0;31m'\nNC='\\033[0m'\nCYAN='\\033[0;36m'\n\nreadonly TOP_LEVEL_SYSTEM_FOLDERS=(\"data/_storage\" \"data/_cache\" \"data/_uploads\" \"data/_webpack\")\n\nST_DIR=\"\"\nREPO_URL=\"\"\nREPO_TOKEN=\"\"\nSYNC_CONFIG_YAML=\"\"\nUSER_MAP=\"\"\nGIT_USER_NAME=\"\"\nGIT_USER_EMAIL=\"\"\ntemp_dir=\"\"\n\nSUMMARY_ACTION_TYPE=\"\"\nSUMMARY_TARGET_REPO=\"\"\nSUMMARY_USERS_PROCESSED=()\nSUMMARY_CONFIG_SYNCED=\"否\"\nSUMMARY_USER_MAP_APPLIED=\"无\"\nSUMMARY_REPO_PRIVACY_STATUS=\"\"\n\nlog_info() { echo -e \"${GREEN}[信息] $1${NC}\"; }\nlog_warn() { echo -e \"${YELLOW}[警告] $1${NC}\"; }\nlog_error() { echo -e \"\\n${RED}[错误] $1${NC}\\n\"; exit 1; }\n\nfn_get_user_folders() {\n local target_dir=\"$1\"\n if [[ ! -d \"$target_dir\" ]]; then return; fi\n\n mapfile -t all_subdirs \u003c \u003c(find \"$target_dir\" -mindepth 1 -maxdepth 1 -type d -exec basename {} \\;)\n local user_folders=()\n for dir in \"${all_subdirs[@]}\"; do\n local is_system_folder=false\n for sys_folder in \"${TOP_LEVEL_SYSTEM_FOLDERS[@]}\"; do\n if [[ \"data/$dir\" == \"$sys_folder\" ]]; then\n is_system_folder=true\n break\n fi\n done\n if [[ \"$is_system_folder\" == false ]]; then\n user_folders+=(\"$dir\")\n fi\n done\n echo \"${user_folders[@]}\"\n}\n\nfn_setup_paths_and_load_config() {\n log_info \"正在智能搜索配置文件 (git_sync.conf)...\"\n local config_file\n config_file=$(find /root /home /opt -maxdepth 5 -type f -name \"git_sync.conf\" 2\u003e/dev/null | head -n 1)\n if [[ -z \"$config_file\" ]]; then\n log_error \"未能找到 'git_sync.conf' 配置文件。\"\n fi\n log_info \"已找到同步配置文件: $config_file\"\n\n local config_dir\n config_dir=$(dirname \"$config_file\")\n ST_DIR=$(dirname \"$config_dir\")\n\n if [[ ! -d \"$ST_DIR\" ]] || [[ ! -f \"${ST_DIR}/docker-compose.yml\" ]]; then\n log_error \"根据配置文件推导出的 SillyTavern 目录 '$ST_DIR' 无效。\"\n fi\n log_info \"已自动推导 SillyTavern 目录为: ${ST_DIR}\"\n\n # shellcheck source=/dev/null\n source \"$config_file\"\n SUMMARY_TARGET_REPO=\"$REPO_URL\"\n if [[ -z \"$REPO_URL\" ]] || [[ -z \"$REPO_TOKEN\" ]]; then\n log_error \"配置文件 'git_sync.conf' 不完整!请确保已设置 REPO_URL 和 REPO_TOKEN。\"\n fi\n\n local sync_rules_config=\"${config_dir}/sync_rules.conf\"\n if [[ -f \"$sync_rules_config\" ]]; then\n log_info \"已找到同步规则文件: $sync_rules_config\"\n # shellcheck source=/dev/null\n source \"$sync_rules_config\"\n fi\n fn_check_repo_privacy\n log_info \"配置加载成功!\"\n}\n\nfn_check_repo_privacy() {\n if [[ \"$REPO_URL\" != *\"github.com\"* ]]; then\n SUMMARY_REPO_PRIVACY_STATUS=\"非GitHub仓库\"\n log_info \"非 GitHub 仓库,跳过隐私检查。\"\n return\n fi\n\n log_info \"正在检查远程仓库的隐私状态...\"\n local repo_path\n repo_path=$(echo \"$REPO_URL\" | sed -E 's#^https?://(www\\.)?github\\.com/##; s#(\\.git)?/?$##')\n \n if [[ -z \"$repo_path\" ]]; then\n SUMMARY_REPO_PRIVACY_STATUS=\"URL解析失败\"\n log_warn \"无法从 REPO_URL 中提取有效的仓库路径,已跳过隐私检查。\"\n return\n fi\n\n local api_url=\"https://api.github.com/repos/${repo_path}\"\n \n local response\n if ! response=$(curl -s -L -H \"Authorization: token ${REPO_TOKEN}\" \"$api_url\"); then\n SUMMARY_REPO_PRIVACY_STATUS=\"API连接失败\"\n log_warn \"无法连接到 GitHub API 检查仓库隐私状态,已跳过。请检查网络连接。\"\n return\n fi\n \n if echo \"$response\" | grep -q '\"private\": false'; then\n SUMMARY_REPO_PRIVACY_STATUS=\"公开\"\n log_warn \"检测到您的同步仓库 [${repo_path}] 是公开的!将在摘要中显示安全警告。\"\n elif echo \"$response\" | grep -q '\"private\": true'; then\n SUMMARY_REPO_PRIVACY_STATUS=\"私有\"\n log_info \"仓库是私有的,数据很安全。\"\n elif echo \"$response\" | grep -q '\"message\": \"Not Found\"'; then\n SUMMARY_REPO_PRIVACY_STATUS=\"仓库未找到\"\n log_warn \"无法找到仓库 [${repo_path}],请检查仓库 URL 是否正确。\"\n elif echo \"$response\" | grep -q '\"message\": \"Bad credentials\"'; then\n SUMMARY_REPO_PRIVACY_STATUS=\"Token无效\"\n log_warn \"GitHub Token 无效或已过期,无法检查仓库状态。请更新 REPO_TOKEN。\"\n else\n SUMMARY_REPO_PRIVACY_STATUS=\"未知状态\"\n log_warn \"无法确定仓库的隐私状态,已跳过检查。可能是未知错误或 Token 权限不足。\"\n fi\n}\n\nfn_check_deps() {\n log_info \"正在检查核心依赖 (git, rsync, zip, curl)...\"\n local missing_pkgs=()\n for pkg in git rsync zip curl; do\n if ! command -v \"$pkg\" \u0026\u003e/dev/null; then\n missing_pkgs+=(\"$pkg\")\n fi\n done\n\n if [[ ${#missing_pkgs[@]} -gt 0 ]]; then\n log_warn \"检测到核心依赖缺失: ${missing_pkgs[*]}. 正在尝试自动安装...\"\n if apt-get update -y \u0026\u0026 apt-get install -y \"${missing_pkgs[@]}\"; then\n log_info \"核心依赖已自动安装成功!\"\n else\n log_error \"自动安装依赖失败!请手动安装: apt-get install -y ${missing_pkgs[*]}\"\n fi\n else\n log_info \"核心依赖完整。\"\n fi\n}\n\nfn_construct_authed_url() {\n local clean_path\n clean_path=$(echo \"$REPO_URL\" | sed -E 's#^https?://(www\\.)?github\\.com/##; s#(\\.git)?/?$##')\n echo \"https://${REPO_TOKEN}@github.com/${clean_path}.git\"\n}\n\nfn_export_extension_links() {\n local all_links_output=\"\"\n local global_ext_path=\"$ST_DIR/public/scripts/extensions/third-party\"\n\n if [[ -d \"$global_ext_path\" ]]; then\n local global_links\n global_links=$(\n find \"$global_ext_path\" -mindepth 1 -maxdepth 1 -type d | while read -r ext_dir; do\n if [[ -d \"$ext_dir/.git\" ]]; then\n git -C \"$ext_dir\" config --get remote.origin.url 2\u003e/dev/null || true\n fi\n done\n )\n if [[ -n \"$global_links\" ]]; then\n all_links_output+=\"═══ 全局扩展 ═══\\n${global_links}\\n\"\n fi\n fi\n\n local user_folders\n user_folders=($(fn_get_user_folders \"$ST_DIR/data\"))\n for user in \"${user_folders[@]}\"; do\n local user_ext_path=\"$ST_DIR/data/$user/extensions\"\n if [[ -d \"$user_ext_path\" ]]; then\n local user_links\n user_links=$(\n find \"$user_ext_path\" -mindepth 1 -maxdepth 1 -type d | while read -r ext_dir; do\n if [[ -d \"$ext_dir/.git\" ]]; then\n git -C \"$ext_dir\" config --get remote.origin.url 2\u003e/dev/null || true\n fi\n done\n )\n if [[ -n \"$user_links\" ]]; then\n all_links_output+=\"\\n═══ 用户 [${user}] 的扩展 ═══\\n${user_links}\"\n fi\n fi\n done\n\n if [[ -n \"$all_links_output\" ]]; then\n echo -e \"${CYAN}─────────────────── 已安装扩展链接 ───────────────────${NC}\"\n printf \"%b\" \"${CYAN}${all_links_output}${NC}\"\n fi\n}\n\nfn_print_summary() {\n echo -e \"\\n${CYAN}──────────────────── 同步任务摘要 ────────────────────${NC}\"\n echo -e \"${CYAN}任务类型 : ${SUMMARY_ACTION_TYPE}${NC}\"\n echo -e \"${CYAN}目标仓库 : ${SUMMARY_TARGET_REPO}${NC}\"\n local privacy_output=\"\"\n case \"$SUMMARY_REPO_PRIVACY_STATUS\" in\n \"私有\")\n privacy_output=\"${GREEN}私有 (安全)${NC}\"\n ;;\n \"公开\")\n privacy_output=\"${RED}公开 (存在数据泄露风险!)${NC}\"\n ;;\n \"非GitHub仓库\")\n privacy_output=\"${YELLOW}非GitHub仓库 (未检查)${NC}\"\n ;;\n *)\n privacy_output=\"${YELLOW}${SUMMARY_REPO_PRIVACY_STATUS}${NC}\"\n ;;\n esac\n echo -e \"${CYAN}仓库状态 : ${privacy_output}${NC}\"\n echo -e \"${CYAN}──────────────────────────────────────────────────────${NC}\"\n if [[ ${#SUMMARY_USERS_PROCESSED[@]} -gt 0 ]]; then\n echo -e \"${CYAN}同步的用户文件夹: ${SUMMARY_USERS_PROCESSED[*]}${NC}\"\n else\n echo -e \"${CYAN}同步的用户文件夹: 无${NC}\"\n fi\n echo -e \"${CYAN}同步 config.yaml : ${SUMMARY_CONFIG_SYNCED}${NC}\"\n echo -e \"${CYAN}────────────────────── 应用规则 ──────────────────────${NC}\"\n echo -e \"${CYAN}用户映射规则 : ${SUMMARY_USER_MAP_APPLIED}${NC}\"\n echo -e \"${CYAN}──────────────────────────────────────────────────────${NC}\"\n\n if [[ \"$SUMMARY_REPO_PRIVACY_STATUS\" == \"公开\" ]]; then\n local repo_path\n repo_path=$(echo \"$REPO_URL\" | sed -E 's#^https?://(www\\.)?github\\.com/##; s#(\\.git)?/?$##')\n echo -e \"\\n${RED}===================================================================${NC}\"\n echo -e \"${RED}# #${NC}\"\n echo -e \"${RED}# [ 安全警告 ] #${NC}\"\n echo -e \"${RED}# #${NC}\"\n echo -e \"${RED}# 检测到您的同步仓库 [${repo_path}] 是公开的! #${NC}\"\n echo -e \"${RED}# 为保护您的聊天记录、角色卡等个人数据不被泄露, #${NC}\"\n echo -e \"${RED}# 强烈建议您立即将该仓库设置为【私有】! #${NC}\"\n echo -e \"${RED}# #${NC}\"\n echo -e \"${RED}===================================================================${NC}\"\n fi\n}\n\nfn_cleanup() {\n if [[ -n \"$temp_dir\" ]] \u0026\u0026 [[ -d \"$temp_dir\" ]]; then\n log_info \"正在清理临时工作目录: $temp_dir\"\n if rm -rf \"$temp_dir\"; then\n log_info \" ${GREEN}[成功] 临时目录已成功删除。${NC}\"\n else\n log_warn \" ${YELLOW}[失败] 未能删除临时目录: $temp_dir${NC}\"\n fi\n fi\n}\n\nfn_backup_local() {\n local backup_type=\"$1\"\n local backup_root_dir=\"${ST_DIR}/_SillyTavern_Backups\"\n mkdir -p \"$backup_root_dir\"\n log_info \"正在触发“${backup_type}”类型的本地备份...\"\n\n local timestamp\n timestamp=$(date +\"%Y-%m-%d_%H-%M\")\n local backup_zip_path=\"${backup_root_dir}/ST_备份_${backup_type}_${timestamp}.zip\"\n\n local paths_to_zip=()\n local user_folders\n user_folders=($(fn_get_user_folders \"$ST_DIR/data\"))\n for user in \"${user_folders[@]}\"; do\n paths_to_zip+=(\"data/$user\")\n done\n\n if [[ \"$SYNC_CONFIG_YAML\" == \"true\" ]] \u0026\u0026 [[ -f \"$ST_DIR/config.yaml\" ]]; then\n paths_to_zip+=(\"config.yaml\")\n fi\n\n if [[ ${#paths_to_zip[@]} -eq 0 ]]; then\n log_warn \"未找到任何可备份的用户数据,已跳过本地备份。\"\n return\n fi\n\n log_info \"正在压缩文件到: ${backup_zip_path}\"\n if (cd \"$ST_DIR\" \u0026\u0026 zip -rq \"$backup_zip_path\" \"${paths_to_zip[@]}\" --exclude \"*/extensions/*\" --exclude \"*/backups/*\" --exclude \"*.log\"); then\n log_info \"创建本地备份成功。\"\n else\n log_error \"创建本地备份失败!\"\n fi\n\n local max_backups=1\n log_info \"正在检查并清理旧备份 (最多保留 ${max_backups} 个)...\"\n \n mapfile -t backup_files \u003c \u003c(find \"$backup_root_dir\" -maxdepth 1 -type f -name \"*.zip\" | sort)\n \n local current_count=${#backup_files[@]}\n if [[ \"$current_count\" -gt \"$max_backups\" ]]; then\n local num_to_delete=$((current_count - max_backups))\n log_info \"备份数量 (${current_count}) 超出限制,正在删除 ${num_to_delete} 个最旧的备份。\"\n \n local files_to_delete=(\"${backup_files[@]:0:$num_to_delete}\")\n for file_to_delete in \"${files_to_delete[@]}\"; do\n rm -f \"$file_to_delete\"\n done\n log_info \"旧备份清理完毕。\"\n else\n log_info \"备份数量 (${current_count}) 未超出限制,无需清理。\"\n fi\n}\n\n# 核心功能: 上传本地数据到远程仓库\nfn_sync_up() {\n SUMMARY_ACTION_TYPE=\"上传\"\n log_info \"开始执行 [${SUMMARY_ACTION_TYPE}] 任务。\"\n temp_dir=$(mktemp -d)\n\n local authed_clone_url\n authed_clone_url=$(fn_construct_authed_url)\n \n log_info \"步骤 1/5: 正在尝试连接 GitHub 克隆仓库...\"\n \n if git -c credential.helper= clone -q --depth 1 \"$authed_clone_url\" \"$temp_dir\"; then\n log_info \" ${GREEN}[成功] 克隆成功。${NC}\"\n else\n log_error \"克隆失败!\\n可能原因:\\n1. 网络连接 GitHub 超时\\n2. Token 权限不足或无效\\n3. 仓库地址错误\"\n fi\n\n local rsync_exclude_args=('--exclude=extensions/' '--exclude=backups/' '--exclude=*.log')\n\n log_info \"步骤 2/5: 同步本地用户数据...\"\n if [[ -n \"$USER_MAP\" ]] \u0026\u0026 [[ \"$USER_MAP\" == *\":\"* ]]; then\n local local_user=\"${USER_MAP%%:*}\"\n local remote_user=\"${USER_MAP##*:}\"\n SUMMARY_USER_MAP_APPLIED=\"本地'${local_user}' -\u003e 云端'${remote_user}'\"\n log_info \"模式: 映射同步 (${SUMMARY_USER_MAP_APPLIED})\"\n if [[ -d \"$ST_DIR/data/$local_user\" ]]; then\n SUMMARY_USERS_PROCESSED=(\"$local_user\")\n mkdir -p \"$temp_dir/data/$remote_user\"\n rsync -aq --delete \"${rsync_exclude_args[@]}\" \"$ST_DIR/data/$local_user/\" \"$temp_dir/data/$remote_user/\"\n fi\n else\n log_info \"模式: 镜像同步 (同步所有本地用户)\"\n rm -rf \"${temp_dir:?}\"/*\n local local_users\n local_users=($(fn_get_user_folders \"$ST_DIR/data\"))\n SUMMARY_USERS_PROCESSED=(\"${local_users[@]}\")\n for l_user in \"${local_users[@]}\"; do\n mkdir -p \"$temp_dir/data/$l_user\"\n rsync -aq --delete \"${rsync_exclude_args[@]}\" \"$ST_DIR/data/$l_user/\" \"$temp_dir/data/$l_user/\"\n done\n fi\n\n log_info \"步骤 3/5: 处理配置文件...\"\n if [[ \"$SYNC_CONFIG_YAML\" == \"true\" ]] \u0026\u0026 [[ -f \"$ST_DIR/config.yaml\" ]]; then\n SUMMARY_CONFIG_SYNCED=\"是\"\n log_info \"正在同步 config.yaml...\"\n cp \"$ST_DIR/config.yaml\" \"$temp_dir/config.yaml\"\n fi\n\n cd \"$temp_dir\" || exit 1\n git add .\n\n if git diff-index --quiet --cached HEAD --; then\n fn_export_extension_links\n fn_print_summary\n log_info \"数据与云端一致,无需上传。\"\n return 0\n fi\n\n log_info \"步骤 4/5: 提交数据变更...\"\n if [[ -n \"$GIT_USER_NAME\" ]] \u0026\u0026 [[ -n \"$GIT_USER_EMAIL\" ]]; then\n log_info \"正在使用配置文件中指定的 Git 用户信息...\"\n git config user.name \"$GIT_USER_NAME\"\n git config user.email \"$GIT_USER_EMAIL\"\n else\n log_info \"未在配置文件中找到 Git 用户信息,将尝试使用系统全局设置。\"\n if ! git config --get user.name \u0026\u003e/dev/null || ! git config --get user.email \u0026\u003e/dev/null; then\n log_error \"Git 用户信息未配置!\\n请在 git_sync.conf 中添加 GIT_USER_NAME 和 GIT_USER_EMAIL,\\n或在系统中执行以下命令进行全局设置:\\n git config --global user.name \\\"Your Name\\\"\\n git config --global user.email \\\"you@example.com\\\"\"\n fi\n fi\n \n git commit -m \"☁️ 服务器推送: $(date +'%Y-%m-%d %H:%M:%S')\" -q\n\n log_info \"步骤 5/5: 推送数据...\"\n if git -c credential.helper= push -q; then\n log_info \" ${GREEN}[成功] 数据成功推送到云端。${NC}\"\n else\n log_error \"推送失败,请检查Token权限或网络连接!\"\n fi\n\n fn_export_extension_links\n fn_print_summary\n log_info \"${GREEN}✓ [成功] 数据已成功上传到云端!${NC}\"\n}\n\n# 核心功能: 从远程仓库恢复数据到本地\nfn_sync_down() {\n SUMMARY_ACTION_TYPE=\"恢复\"\n fn_backup_local \"恢复前\"\n log_info \"开始执行 [${SUMMARY_ACTION_TYPE}] 任务。\"\n temp_dir=$(mktemp -d)\n\n local authed_url\n authed_url=$(fn_construct_authed_url)\n \n log_info \"步骤 1/3: 正在尝试下载云端数据...\"\n if git -c credential.helper= clone -q --depth 1 \"$authed_url\" \"$temp_dir\"; then\n log_info \" ${GREEN}[成功] 下载成功。${NC}\"\n else\n log_error \"下载失败!请检查网络或Token。\"\n fi\n\n local rsync_exclude_args=('--exclude=extensions/' '--exclude=backups/' '--exclude=*.log')\n\n log_info \"步骤 2/3: 恢复用户数据...\"\n if [[ -n \"$USER_MAP\" ]] \u0026\u0026 [[ \"$USER_MAP\" == *\":\"* ]]; then\n local local_user=\"${USER_MAP%%:*}\"\n local remote_user=\"${USER_MAP##*:}\"\n SUMMARY_USER_MAP_APPLIED=\"云端'${remote_user}' -\u003e 本地'${local_user}'\"\n log_info \"模式: 映射同步 (${SUMMARY_USER_MAP_APPLIED})\"\n if [[ ! -d \"$temp_dir/data/$remote_user\" ]]; then\n log_error \"在云端仓库中找不到用户文件夹 '$remote_user'!\"\n fi\n SUMMARY_USERS_PROCESSED=(\"$remote_user\")\n mkdir -p \"$ST_DIR/data/$local_user\"\n rsync -aq --delete \"${rsync_exclude_args[@]}\" \"$temp_dir/data/$remote_user/\" \"$ST_DIR/data/$local_user/\"\n else\n log_info \"模式: 镜像同步 (恢复所有云端用户)\"\n local remote_users\n remote_users=($(fn_get_user_folders \"$temp_dir/data\"))\n SUMMARY_USERS_PROCESSED=(\"${remote_users[@]}\")\n\n local local_users\n local_users=($(fn_get_user_folders \"$ST_DIR/data\"))\n for l_user in \"${local_users[@]}\"; do\n if ! [[ \" ${remote_users[*]} \" =~ \" ${l_user} \" ]]; then\n log_warn \"清理本地多余的用户: $l_user\"\n rm -rf \"$ST_DIR/data/$l_user\"\n fi\n done\n for r_user in \"${remote_users[@]}\"; do\n mkdir -p \"$ST_DIR/data/$r_user\"\n rsync -aq --delete \"${rsync_exclude_args[@]}\" \"$temp_dir/data/$r_user/\" \"$ST_DIR/data/$r_user/\"\n done\n fi\n\n log_info \"步骤 3/3: 恢复配置文件...\"\n if [[ \"$SYNC_CONFIG_YAML\" == \"true\" ]] \u0026\u0026 [[ -f \"$temp_dir/config.yaml\" ]]; then\n SUMMARY_CONFIG_SYNCED=\"是\"\n log_info \"正在恢复 config.yaml...\"\n cp \"$temp_dir/config.yaml\" \"$ST_DIR/config.yaml\"\n fi\n\n fn_export_extension_links\n fn_print_summary\n log_info \"${GREEN}✓ [成功] 数据已从云端恢复到本地!${NC}\"\n}\n\n# 脚本主入口\nmain() {\n trap fn_cleanup EXIT\n git config --global --add safe.directory '*' 2\u003e/dev/null || true\n \n fn_check_deps\n fn_setup_paths_and_load_config\n case \"$ACTION\" in\n up)\n fn_sync_up\n ;;\n down)\n fn_sync_down\n ;;\n *)\n log_error \"未知的操作: '${ACTION}'。请检查脚本顶部的 ACTION 变量设置。\"\n ;;\n esac\n}\n\nmain\n","command":"","containerName":"","user":"root","url":"","scriptName":"","apps":null,"websites":null,"dbType":"mysql","dbName":null,"exclusionRules":"","isDir":true,"sourceDir":"","retainCopies":20,"retryTimes":3,"timeout":3600,"ignoreErr":false,"snapshotRule":{"withImage":false,"ignoreApps":null},"secret":"","sourceAccounts":null,"downloadAccount":"","alertCount":0,"alertTitle":"","alertMethod":""},{"name":"咕咕助手【上传】","type":"shell","groupID":7,"specCustom":false,"spec":"0 4 * * *","executor":"bash","scriptMode":"input","script":"#!/usr/bin/env bash\n# 清绝咕咕助手 v2.5\n# 作者: 清绝 | 网址: blog.qjyg.de\n\n# 定义核心操作: \"up\" (上传) 或 \"down\" (恢复)\nACTION=\"up\"\n\n# 禁止 Git 弹出交互式提示\nexport GIT_TERMINAL_PROMPT=0\n\nset -e\nset -o pipefail\n\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nRED='\\033[0;31m'\nNC='\\033[0m'\nCYAN='\\033[0;36m'\n\nreadonly TOP_LEVEL_SYSTEM_FOLDERS=(\"data/_storage\" \"data/_cache\" \"data/_uploads\" \"data/_webpack\")\n\nST_DIR=\"\"\nREPO_URL=\"\"\nREPO_TOKEN=\"\"\nSYNC_CONFIG_YAML=\"\"\nUSER_MAP=\"\"\nGIT_USER_NAME=\"\"\nGIT_USER_EMAIL=\"\"\ntemp_dir=\"\"\n\nSUMMARY_ACTION_TYPE=\"\"\nSUMMARY_TARGET_REPO=\"\"\nSUMMARY_USERS_PROCESSED=()\nSUMMARY_CONFIG_SYNCED=\"否\"\nSUMMARY_USER_MAP_APPLIED=\"无\"\nSUMMARY_REPO_PRIVACY_STATUS=\"\"\n\nlog_info() { echo -e \"${GREEN}[信息] $1${NC}\"; }\nlog_warn() { echo -e \"${YELLOW}[警告] $1${NC}\"; }\nlog_error() { echo -e \"\\n${RED}[错误] $1${NC}\\n\"; exit 1; }\n\nfn_get_user_folders() {\n local target_dir=\"$1\"\n if [[ ! -d \"$target_dir\" ]]; then return; fi\n\n mapfile -t all_subdirs \u003c \u003c(find \"$target_dir\" -mindepth 1 -maxdepth 1 -type d -exec basename {} \\;)\n local user_folders=()\n for dir in \"${all_subdirs[@]}\"; do\n local is_system_folder=false\n for sys_folder in \"${TOP_LEVEL_SYSTEM_FOLDERS[@]}\"; do\n if [[ \"data/$dir\" == \"$sys_folder\" ]]; then\n is_system_folder=true\n break\n fi\n done\n if [[ \"$is_system_folder\" == false ]]; then\n user_folders+=(\"$dir\")\n fi\n done\n echo \"${user_folders[@]}\"\n}\n\nfn_setup_paths_and_load_config() {\n log_info \"正在智能搜索配置文件 (git_sync.conf)...\"\n local config_file\n config_file=$(find /root /home /opt -maxdepth 5 -type f -name \"git_sync.conf\" 2\u003e/dev/null | head -n 1)\n if [[ -z \"$config_file\" ]]; then\n log_error \"未能找到 'git_sync.conf' 配置文件。\"\n fi\n log_info \"已找到同步配置文件: $config_file\"\n\n local config_dir\n config_dir=$(dirname \"$config_file\")\n ST_DIR=$(dirname \"$config_dir\")\n\n if [[ ! -d \"$ST_DIR\" ]] || [[ ! -f \"${ST_DIR}/docker-compose.yml\" ]]; then\n log_error \"根据配置文件推导出的 SillyTavern 目录 '$ST_DIR' 无效。\"\n fi\n log_info \"已自动推导 SillyTavern 目录为: ${ST_DIR}\"\n\n # shellcheck source=/dev/null\n source \"$config_file\"\n SUMMARY_TARGET_REPO=\"$REPO_URL\"\n if [[ -z \"$REPO_URL\" ]] || [[ -z \"$REPO_TOKEN\" ]]; then\n log_error \"配置文件 'git_sync.conf' 不完整!请确保已设置 REPO_URL 和 REPO_TOKEN。\"\n fi\n\n local sync_rules_config=\"${config_dir}/sync_rules.conf\"\n if [[ -f \"$sync_rules_config\" ]]; then\n log_info \"已找到同步规则文件: $sync_rules_config\"\n # shellcheck source=/dev/null\n source \"$sync_rules_config\"\n fi\n fn_check_repo_privacy\n log_info \"配置加载成功!\"\n}\n\nfn_check_repo_privacy() {\n if [[ \"$REPO_URL\" != *\"github.com\"* ]]; then\n SUMMARY_REPO_PRIVACY_STATUS=\"非GitHub仓库\"\n log_info \"非 GitHub 仓库,跳过隐私检查。\"\n return\n fi\n\n log_info \"正在检查远程仓库的隐私状态...\"\n local repo_path\n repo_path=$(echo \"$REPO_URL\" | sed -E 's#^https?://(www\\.)?github\\.com/##; s#(\\.git)?/?$##')\n \n if [[ -z \"$repo_path\" ]]; then\n SUMMARY_REPO_PRIVACY_STATUS=\"URL解析失败\"\n log_warn \"无法从 REPO_URL 中提取有效的仓库路径,已跳过隐私检查。\"\n return\n fi\n\n local api_url=\"https://api.github.com/repos/${repo_path}\"\n \n local response\n if ! response=$(curl -s -L -H \"Authorization: token ${REPO_TOKEN}\" \"$api_url\"); then\n SUMMARY_REPO_PRIVACY_STATUS=\"API连接失败\"\n log_warn \"无法连接到 GitHub API 检查仓库隐私状态,已跳过。请检查网络连接。\"\n return\n fi\n \n if echo \"$response\" | grep -q '\"private\": false'; then\n SUMMARY_REPO_PRIVACY_STATUS=\"公开\"\n log_warn \"检测到您的同步仓库 [${repo_path}] 是公开的!将在摘要中显示安全警告。\"\n elif echo \"$response\" | grep -q '\"private\": true'; then\n SUMMARY_REPO_PRIVACY_STATUS=\"私有\"\n log_info \"仓库是私有的,数据很安全。\"\n elif echo \"$response\" | grep -q '\"message\": \"Not Found\"'; then\n SUMMARY_REPO_PRIVACY_STATUS=\"仓库未找到\"\n log_warn \"无法找到仓库 [${repo_path}],请检查仓库 URL 是否正确。\"\n elif echo \"$response\" | grep -q '\"message\": \"Bad credentials\"'; then\n SUMMARY_REPO_PRIVACY_STATUS=\"Token无效\"\n log_warn \"GitHub Token 无效或已过期,无法检查仓库状态。请更新 REPO_TOKEN。\"\n else\n SUMMARY_REPO_PRIVACY_STATUS=\"未知状态\"\n log_warn \"无法确定仓库的隐私状态,已跳过检查。可能是未知错误或 Token 权限不足。\"\n fi\n}\n\nfn_check_deps() {\n log_info \"正在检查核心依赖 (git, rsync, zip, curl)...\"\n local missing_pkgs=()\n for pkg in git rsync zip curl; do\n if ! command -v \"$pkg\" \u0026\u003e/dev/null; then\n missing_pkgs+=(\"$pkg\")\n fi\n done\n\n if [[ ${#missing_pkgs[@]} -gt 0 ]]; then\n log_warn \"检测到核心依赖缺失: ${missing_pkgs[*]}. 正在尝试自动安装...\"\n if apt-get update -y \u0026\u0026 apt-get install -y \"${missing_pkgs[@]}\"; then\n log_info \"核心依赖已自动安装成功!\"\n else\n log_error \"自动安装依赖失败!请手动安装: apt-get install -y ${missing_pkgs[*]}\"\n fi\n else\n log_info \"核心依赖完整。\"\n fi\n}\n\nfn_construct_authed_url() {\n local clean_path\n clean_path=$(echo \"$REPO_URL\" | sed -E 's#^https?://(www\\.)?github\\.com/##; s#(\\.git)?/?$##')\n echo \"https://${REPO_TOKEN}@github.com/${clean_path}.git\"\n}\n\nfn_export_extension_links() {\n local all_links_output=\"\"\n local global_ext_path=\"$ST_DIR/public/scripts/extensions/third-party\"\n\n if [[ -d \"$global_ext_path\" ]]; then\n local global_links\n global_links=$(\n find \"$global_ext_path\" -mindepth 1 -maxdepth 1 -type d | while read -r ext_dir; do\n if [[ -d \"$ext_dir/.git\" ]]; then\n git -C \"$ext_dir\" config --get remote.origin.url 2\u003e/dev/null || true\n fi\n done\n )\n if [[ -n \"$global_links\" ]]; then\n all_links_output+=\"═══ 全局扩展 ═══\\n${global_links}\\n\"\n fi\n fi\n\n local user_folders\n user_folders=($(fn_get_user_folders \"$ST_DIR/data\"))\n for user in \"${user_folders[@]}\"; do\n local user_ext_path=\"$ST_DIR/data/$user/extensions\"\n if [[ -d \"$user_ext_path\" ]]; then\n local user_links\n user_links=$(\n find \"$user_ext_path\" -mindepth 1 -maxdepth 1 -type d | while read -r ext_dir; do\n if [[ -d \"$ext_dir/.git\" ]]; then\n git -C \"$ext_dir\" config --get remote.origin.url 2\u003e/dev/null || true\n fi\n done\n )\n if [[ -n \"$user_links\" ]]; then\n all_links_output+=\"\\n═══ 用户 [${user}] 的扩展 ═══\\n${user_links}\"\n fi\n fi\n done\n\n if [[ -n \"$all_links_output\" ]]; then\n echo -e \"${CYAN}─────────────────── 已安装扩展链接 ───────────────────${NC}\"\n printf \"%b\" \"${CYAN}${all_links_output}${NC}\"\n fi\n}\n\nfn_print_summary() {\n echo -e \"\\n${CYAN}──────────────────── 同步任务摘要 ────────────────────${NC}\"\n echo -e \"${CYAN}任务类型 : ${SUMMARY_ACTION_TYPE}${NC}\"\n echo -e \"${CYAN}目标仓库 : ${SUMMARY_TARGET_REPO}${NC}\"\n local privacy_output=\"\"\n case \"$SUMMARY_REPO_PRIVACY_STATUS\" in\n \"私有\")\n privacy_output=\"${GREEN}私有 (安全)${NC}\"\n ;;\n \"公开\")\n privacy_output=\"${RED}公开 (存在数据泄露风险!)${NC}\"\n ;;\n \"非GitHub仓库\")\n privacy_output=\"${YELLOW}非GitHub仓库 (未检查)${NC}\"\n ;;\n *)\n privacy_output=\"${YELLOW}${SUMMARY_REPO_PRIVACY_STATUS}${NC}\"\n ;;\n esac\n echo -e \"${CYAN}仓库状态 : ${privacy_output}${NC}\"\n echo -e \"${CYAN}──────────────────────────────────────────────────────${NC}\"\n if [[ ${#SUMMARY_USERS_PROCESSED[@]} -gt 0 ]]; then\n echo -e \"${CYAN}同步的用户文件夹: ${SUMMARY_USERS_PROCESSED[*]}${NC}\"\n else\n echo -e \"${CYAN}同步的用户文件夹: 无${NC}\"\n fi\n echo -e \"${CYAN}同步 config.yaml : ${SUMMARY_CONFIG_SYNCED}${NC}\"\n echo -e \"${CYAN}────────────────────── 应用规则 ──────────────────────${NC}\"\n echo -e \"${CYAN}用户映射规则 : ${SUMMARY_USER_MAP_APPLIED}${NC}\"\n echo -e \"${CYAN}──────────────────────────────────────────────────────${NC}\"\n\n if [[ \"$SUMMARY_REPO_PRIVACY_STATUS\" == \"公开\" ]]; then\n local repo_path\n repo_path=$(echo \"$REPO_URL\" | sed -E 's#^https?://(www\\.)?github\\.com/##; s#(\\.git)?/?$##')\n echo -e \"\\n${RED}===================================================================${NC}\"\n echo -e \"${RED}# #${NC}\"\n echo -e \"${RED}# [ 安全警告 ] #${NC}\"\n echo -e \"${RED}# #${NC}\"\n echo -e \"${RED}# 检测到您的同步仓库 [${repo_path}] 是公开的! #${NC}\"\n echo -e \"${RED}# 为保护您的聊天记录、角色卡等个人数据不被泄露, #${NC}\"\n echo -e \"${RED}# 强烈建议您立即将该仓库设置为【私有】! #${NC}\"\n echo -e \"${RED}# #${NC}\"\n echo -e \"${RED}===================================================================${NC}\"\n fi\n}\n\nfn_cleanup() {\n if [[ -n \"$temp_dir\" ]] \u0026\u0026 [[ -d \"$temp_dir\" ]]; then\n log_info \"正在清理临时工作目录: $temp_dir\"\n if rm -rf \"$temp_dir\"; then\n log_info \" ${GREEN}[成功] 临时目录已成功删除。${NC}\"\n else\n log_warn \" ${YELLOW}[失败] 未能删除临时目录: $temp_dir${NC}\"\n fi\n fi\n}\n\nfn_backup_local() {\n local backup_type=\"$1\"\n local backup_root_dir=\"${ST_DIR}/_SillyTavern_Backups\"\n mkdir -p \"$backup_root_dir\"\n log_info \"正在触发“${backup_type}”类型的本地备份...\"\n\n local timestamp\n timestamp=$(date +\"%Y-%m-%d_%H-%M\")\n local backup_zip_path=\"${backup_root_dir}/ST_备份_${backup_type}_${timestamp}.zip\"\n\n local paths_to_zip=()\n local user_folders\n user_folders=($(fn_get_user_folders \"$ST_DIR/data\"))\n for user in \"${user_folders[@]}\"; do\n paths_to_zip+=(\"data/$user\")\n done\n\n if [[ \"$SYNC_CONFIG_YAML\" == \"true\" ]] \u0026\u0026 [[ -f \"$ST_DIR/config.yaml\" ]]; then\n paths_to_zip+=(\"config.yaml\")\n fi\n\n if [[ ${#paths_to_zip[@]} -eq 0 ]]; then\n log_warn \"未找到任何可备份的用户数据,已跳过本地备份。\"\n return\n fi\n\n log_info \"正在压缩文件到: ${backup_zip_path}\"\n if (cd \"$ST_DIR\" \u0026\u0026 zip -rq \"$backup_zip_path\" \"${paths_to_zip[@]}\" --exclude \"*/extensions/*\" --exclude \"*/backups/*\" --exclude \"*.log\"); then\n log_info \"创建本地备份成功。\"\n else\n log_error \"创建本地备份失败!\"\n fi\n\n local max_backups=1\n log_info \"正在检查并清理旧备份 (最多保留 ${max_backups} 个)...\"\n \n mapfile -t backup_files \u003c \u003c(find \"$backup_root_dir\" -maxdepth 1 -type f -name \"*.zip\" | sort)\n \n local current_count=${#backup_files[@]}\n if [[ \"$current_count\" -gt \"$max_backups\" ]]; then\n local num_to_delete=$((current_count - max_backups))\n log_info \"备份数量 (${current_count}) 超出限制,正在删除 ${num_to_delete} 个最旧的备份。\"\n \n local files_to_delete=(\"${backup_files[@]:0:$num_to_delete}\")\n for file_to_delete in \"${files_to_delete[@]}\"; do\n rm -f \"$file_to_delete\"\n done\n log_info \"旧备份清理完毕。\"\n else\n log_info \"备份数量 (${current_count}) 未超出限制,无需清理。\"\n fi\n}\n\n# 核心功能: 上传本地数据到远程仓库\nfn_sync_up() {\n SUMMARY_ACTION_TYPE=\"上传\"\n log_info \"开始执行 [${SUMMARY_ACTION_TYPE}] 任务。\"\n temp_dir=$(mktemp -d)\n\n local authed_clone_url\n authed_clone_url=$(fn_construct_authed_url)\n \n log_info \"步骤 1/5: 正在尝试连接 GitHub 克隆仓库...\"\n \n if git -c credential.helper= clone -q --depth 1 \"$authed_clone_url\" \"$temp_dir\"; then\n log_info \" ${GREEN}[成功] 克隆成功。${NC}\"\n else\n log_error \"克隆失败!\\n可能原因:\\n1. 网络连接 GitHub 超时\\n2. Token 权限不足或无效\\n3. 仓库地址错误\"\n fi\n\n local rsync_exclude_args=('--exclude=extensions/' '--exclude=backups/' '--exclude=*.log')\n\n log_info \"步骤 2/5: 同步本地用户数据...\"\n if [[ -n \"$USER_MAP\" ]] \u0026\u0026 [[ \"$USER_MAP\" == *\":\"* ]]; then\n local local_user=\"${USER_MAP%%:*}\"\n local remote_user=\"${USER_MAP##*:}\"\n SUMMARY_USER_MAP_APPLIED=\"本地'${local_user}' -\u003e 云端'${remote_user}'\"\n log_info \"模式: 映射同步 (${SUMMARY_USER_MAP_APPLIED})\"\n if [[ -d \"$ST_DIR/data/$local_user\" ]]; then\n SUMMARY_USERS_PROCESSED=(\"$local_user\")\n mkdir -p \"$temp_dir/data/$remote_user\"\n rsync -aq --delete \"${rsync_exclude_args[@]}\" \"$ST_DIR/data/$local_user/\" \"$temp_dir/data/$remote_user/\"\n fi\n else\n log_info \"模式: 镜像同步 (同步所有本地用户)\"\n rm -rf \"${temp_dir:?}\"/*\n local local_users\n local_users=($(fn_get_user_folders \"$ST_DIR/data\"))\n SUMMARY_USERS_PROCESSED=(\"${local_users[@]}\")\n for l_user in \"${local_users[@]}\"; do\n mkdir -p \"$temp_dir/data/$l_user\"\n rsync -aq --delete \"${rsync_exclude_args[@]}\" \"$ST_DIR/data/$l_user/\" \"$temp_dir/data/$l_user/\"\n done\n fi\n\n log_info \"步骤 3/5: 处理配置文件...\"\n if [[ \"$SYNC_CONFIG_YAML\" == \"true\" ]] \u0026\u0026 [[ -f \"$ST_DIR/config.yaml\" ]]; then\n SUMMARY_CONFIG_SYNCED=\"是\"\n log_info \"正在同步 config.yaml...\"\n cp \"$ST_DIR/config.yaml\" \"$temp_dir/config.yaml\"\n fi\n\n cd \"$temp_dir\" || exit 1\n git add .\n\n if git diff-index --quiet --cached HEAD --; then\n fn_export_extension_links\n fn_print_summary\n log_info \"数据与云端一致,无需上传。\"\n return 0\n fi\n\n log_info \"步骤 4/5: 提交数据变更...\"\n if [[ -n \"$GIT_USER_NAME\" ]] \u0026\u0026 [[ -n \"$GIT_USER_EMAIL\" ]]; then\n log_info \"正在使用配置文件中指定的 Git 用户信息...\"\n git config user.name \"$GIT_USER_NAME\"\n git config user.email \"$GIT_USER_EMAIL\"\n else\n log_info \"未在配置文件中找到 Git 用户信息,将尝试使用系统全局设置。\"\n if ! git config --get user.name \u0026\u003e/dev/null || ! git config --get user.email \u0026\u003e/dev/null; then\n log_error \"Git 用户信息未配置!\\n请在 git_sync.conf 中添加 GIT_USER_NAME 和 GIT_USER_EMAIL,\\n或在系统中执行以下命令进行全局设置:\\n git config --global user.name \\\"Your Name\\\"\\n git config --global user.email \\\"you@example.com\\\"\"\n fi\n fi\n \n git commit -m \"☁️ 服务器推送: $(date +'%Y-%m-%d %H:%M:%S')\" -q\n\n log_info \"步骤 5/5: 推送数据...\"\n if git -c credential.helper= push -q; then\n log_info \" ${GREEN}[成功] 数据成功推送到云端。${NC}\"\n else\n log_error \"推送失败,请检查Token权限或网络连接!\"\n fi\n\n fn_export_extension_links\n fn_print_summary\n log_info \"${GREEN}✓ [成功] 数据已成功上传到云端!${NC}\"\n}\n\n# 核心功能: 从远程仓库恢复数据到本地\nfn_sync_down() {\n SUMMARY_ACTION_TYPE=\"恢复\"\n fn_backup_local \"恢复前\"\n log_info \"开始执行 [${SUMMARY_ACTION_TYPE}] 任务。\"\n temp_dir=$(mktemp -d)\n\n local authed_url\n authed_url=$(fn_construct_authed_url)\n \n log_info \"步骤 1/3: 正在尝试下载云端数据...\"\n if git -c credential.helper= clone -q --depth 1 \"$authed_url\" \"$temp_dir\"; then\n log_info \" ${GREEN}[成功] 下载成功。${NC}\"\n else\n log_error \"下载失败!请检查网络或Token。\"\n fi\n\n local rsync_exclude_args=('--exclude=extensions/' '--exclude=backups/' '--exclude=*.log')\n\n log_info \"步骤 2/3: 恢复用户数据...\"\n if [[ -n \"$USER_MAP\" ]] \u0026\u0026 [[ \"$USER_MAP\" == *\":\"* ]]; then\n local local_user=\"${USER_MAP%%:*}\"\n local remote_user=\"${USER_MAP##*:}\"\n SUMMARY_USER_MAP_APPLIED=\"云端'${remote_user}' -\u003e 本地'${local_user}'\"\n log_info \"模式: 映射同步 (${SUMMARY_USER_MAP_APPLIED})\"\n if [[ ! -d \"$temp_dir/data/$remote_user\" ]]; then\n log_error \"在云端仓库中找不到用户文件夹 '$remote_user'!\"\n fi\n SUMMARY_USERS_PROCESSED=(\"$remote_user\")\n mkdir -p \"$ST_DIR/data/$local_user\"\n rsync -aq --delete \"${rsync_exclude_args[@]}\" \"$temp_dir/data/$remote_user/\" \"$ST_DIR/data/$local_user/\"\n else\n log_info \"模式: 镜像同步 (恢复所有云端用户)\"\n local remote_users\n remote_users=($(fn_get_user_folders \"$temp_dir/data\"))\n SUMMARY_USERS_PROCESSED=(\"${remote_users[@]}\")\n\n local local_users\n local_users=($(fn_get_user_folders \"$ST_DIR/data\"))\n for l_user in \"${local_users[@]}\"; do\n if ! [[ \" ${remote_users[*]} \" =~ \" ${l_user} \" ]]; then\n log_warn \"清理本地多余的用户: $l_user\"\n rm -rf \"$ST_DIR/data/$l_user\"\n fi\n done\n for r_user in \"${remote_users[@]}\"; do\n mkdir -p \"$ST_DIR/data/$r_user\"\n rsync -aq --delete \"${rsync_exclude_args[@]}\" \"$temp_dir/data/$r_user/\" \"$ST_DIR/data/$r_user/\"\n done\n fi\n\n log_info \"步骤 3/3: 恢复配置文件...\"\n if [[ \"$SYNC_CONFIG_YAML\" == \"true\" ]] \u0026\u0026 [[ -f \"$temp_dir/config.yaml\" ]]; then\n SUMMARY_CONFIG_SYNCED=\"是\"\n log_info \"正在恢复 config.yaml...\"\n cp \"$temp_dir/config.yaml\" \"$ST_DIR/config.yaml\"\n fi\n\n fn_export_extension_links\n fn_print_summary\n log_info \"${GREEN}✓ [成功] 数据已从云端恢复到本地!${NC}\"\n}\n\n# 脚本主入口\nmain() {\n trap fn_cleanup EXIT\n git config --global --add safe.directory '*' 2\u003e/dev/null || true\n \n fn_check_deps\n fn_setup_paths_and_load_config\n case \"$ACTION\" in\n up)\n fn_sync_up\n ;;\n down)\n fn_sync_down\n ;;\n *)\n log_error \"未知的操作: '${ACTION}'。请检查脚本顶部的 ACTION 变量设置。\"\n ;;\n esac\n}\n\nmain\n","command":"","containerName":"","user":"root","url":"","scriptName":"","apps":null,"websites":null,"dbType":"mysql","dbName":null,"exclusionRules":"","isDir":true,"sourceDir":"","retainCopies":20,"retryTimes":3,"timeout":3600,"ignoreErr":false,"snapshotRule":{"withImage":false,"ignoreApps":null},"secret":"","sourceAccounts":null,"downloadAccount":"","alertCount":0,"alertTitle":"","alertMethod":""}]