一,从零搭建守护环境 1,在 root 目录下新建 all-n1-install.sh 文件,内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 #!/bin/bash set -eecho "==== N1盒子自愈全套【无jq版·重新生成守护环境】 ====" echo "[1/7] 跳过jq依赖,使用pm2 id返回码判断状态" echo "[2/7] 生成 /root/n1.sh" cat > /root/n1.sh <<'INNER_EOF' QL_CONTAINER="qinglong" LOCK_DIR="/root/.n1-deploy" LOG_FILE="/root/ql/logs/selfheal.log" CRON_LOG="/root/ql/logs/cron-n1.log" PM2_LOG_DIR="/root/.pm2/logs" mkdir -p "${LOCK_DIR} " mkdir -p "/root/ql/logs" mkdir -p "${PM2_LOG_DIR} " log_max_size=$((20 *1024 *1024 )) log_keep_days=3 docker_exec_timeout=8 for logfile in "${LOG_FILE} " "${CRON_LOG} " ;do if [ -f "${logfile} " ];then filesize=$(stat -c%s "${logfile} " ) if [ "${filesize} " -gt "${log_max_size} " ];then > "${logfile} " fi fi done find /root/ql/logs -type f -name "*.log" ! -name "selfheal.log" ! -name "cron-n1.log" -mtime +${log_keep_days} -delete for logfile in ${PM2_LOG_DIR} /*.log ;do if [ -f "${logfile} " ];then filesize=$(stat -c%s "${logfile} " ) if [ "${filesize} " -gt "${log_max_size} " ];then > "${logfile} " fi fi done find "${PM2_LOG_DIR} " -type f -name "*.log" -mtime +${log_keep_days} -delete docker_log_path=$(docker inspect --format='{{.LogPath}}' "${QL_CONTAINER} " 2>/dev/null || true ) if [ -n "${docker_log_path} " ] && [ -f "${docker_log_path} " ];then docker_log_size=$(stat -c%s "${docker_log_path} " ) if [ "${docker_log_size} " -gt "${log_max_size} " ];then truncate -s 0 "${docker_log_path} " fi fi exec 9>"${LOCK_DIR} /n1.lock" if ! flock -n 9 ; then echo "$(date '+%Y-%m-%d %H:%M:%S') n1.sh 已在运行,放弃本次执行" >> ${LOG_FILE} exit 0 fi log (){ echo "$(date '+%Y-%m-%d %H:%M:%S') $*" >> ${LOG_FILE} echo "$*" } host_is_online (){ local app="$1 " pm2 id "${app} " >/dev/null 2>&1 return $? } ql_is_online (){ local app="$1 " docker exec --timeout ${docker_exec_timeout} "${QL_CONTAINER} " pm2 id "${app} " >/dev/null 2>&1 return $? } HOST_APPS=("go-cqhttp" "sillyGirl" "xdd" ) QL_APPS=() log "=== N1 夜维 $(date '+%Y-%m-%d_%H:%M') ===" host_ok=0 host_total=${#HOST_APPS[@]} for app in "${HOST_APPS[@]} " ;do if host_is_online "${app} " ;then log "✅ 腿[${app} ] online" host_ok=$((host_ok+1 )) else log "⚠️ 腿[${app} ] 异常,执行重启" log "🔧 重启宿主机 ${app} " pm2 restart "${app} " >>${LOG_FILE} 2>&1 fi done log "${host_ok} /${host_total} " ql_ok=0 ql_total=${#QL_APPS[@]} for app in "${QL_APPS[@]} " ;do if ql_is_online "${app} " ;then log "✅ ql[${app} ] online" ql_ok=$((ql_ok+1 )) else log "⚠️ ql[${app} ] 异常,执行重启" log "🔧 重启容器 ${app} " docker exec --timeout ${docker_exec_timeout} "${QL_CONTAINER} " pm2 restart "${app} " >>${LOG_FILE} 2>&1 fi done log "${ql_ok} /${ql_total} " disk_used=$(df -P / | awk 'NR==2{print $5}' | tr -d '%' ) log "✅ 磁盘 ${disk_used} %" log "=== 巡检结束 ===" exit 0INNER_EOF chmod +x /root/n1.shecho "[3/7] 生成 systemd 开机延时恢复服务" cat > /etc/systemd/system/n1-ql-restore.service <<'UNIT_EOF' [Unit] Description=N1开机延时恢复宿主机&青龙容器pm2 After=docker.service Requires=docker.service [Service] Type=oneshot ExecStart=/bin/sh -c 'sleep 40; pm2 resurrect >/dev/null 2>&1; pm2 save >/dev/null 2>&1; docker exec qinglong pm2 resurrect >/dev/null 2>&1; docker exec qinglong pm2 save >/dev/null 2>&1;' [Install] WantedBy=multi-user.target UNIT_EOF systemctl daemon-reload systemctl enable n1-ql-restore.service echo "[4/7] 设置crontab定时任务,覆盖旧定时" cat > /root/tmp_cron.txt <<'CRON_EOF' 05 3 * * * /root/ql-scrub.sh >> /root/ql/logs/night.log 2>&1 15 3 * * * /root/n1.sh */5 * * * * /bin/bash /root/n1.sh CRON_EOF crontab /root/tmp_cron.txt rm -f /root/tmp_cron.txtecho "[5/7] 生成日志裁剪脚本 ql‑scrub.sh" cat > /root/ql-scrub.sh <<'SCRUB_EOF' LOGDIR="/root/ql/logs" MAX_SIZE=$((1024 *1024 *20 )) mkdir -p "${LOGDIR} " for logfile in "${LOGDIR} " /*.log ;do if [ -f "${logfile} " ];then filesize=$(stat -c%s "${logfile} " ) if [ "${filesize} " -gt "${MAX_SIZE} " ];then mv -f "${logfile} " "${logfile} .old" touch "${logfile} " fi fi done SCRUB_EOF chmod +x /root/ql-scrub.shecho "[6/7] 清理旧锁文件,创建目录" rm -f /root/.n1-deploy/n1.lockmkdir -p /root/ql/logsmkdir -p /root/.pm2/logsecho "[7/7] 执行首次巡检测试" bash /root/n1.sh echo "" echo "==================== 部署完成 ====================" echo "✅ 完全无jq依赖,pm2 id返回码检测进程" echo "✅ QL_APPS=() 默认关闭容器内pm2巡检,无需手动修改" echo "✅ systemd:开机40秒后恢复宿主机+青龙容器pm2" echo "✅ crontab:5分钟巡检;凌晨3:05日志清理;3:15额外巡检" echo "✅ 日志策略:单文件>20MB轮转;碎片日志保留3天" echo "✅ 巡检对象:宿主机go‑cqhttp/sillyGirl/xdd" echo "✅ flock防并发、docker exec超时8秒保护" echo "" echo "手动巡检命令:bash /root/n1.sh" echo "查看自愈日志:tail -f /root/ql/logs/selfheal.log" echo "查看开机恢复服务状态:systemctl status n1-ql-restore.service" echo "查看定时任务:crontab -l" echo "" echo "👉 青龙容器内部建议执行:pm2 resurrect && pm2 save" echo "👉 整机重启测试:reboot"
2,脚本跑完校验
1 2 3 crontab -l systemctl status n1-ql-restore.service bash /root/n1.sh
只会输出宿主机三条 ✅ xxx online,容器部分不会输出任何内容
使用场景:N1 彻底重装 Armbian 从零部署守护环境就运行这个脚本
二,备份守护环境 1,确认全部 5 个文件都存在
1 2 3 4 5 6 ls -l \/root/n1.sh \ /root/ql-scrub.sh \ /etc/systemd/system/n1-ql-restore.service \ /root/cron_backup.txt \ /root/pm2_ecosystem_backup.json
上面命令不报错,列出全部文件,就可以打包
1 2 3 4 5 6 7 8 crontab -l > /root/cron_backup.txt pm2 ecosystem > /root/pm2_ecosystem_backup.json tar -zcvf /root/n1_backup.tar.gz \ /root/n1.sh \ /root/ql-scrub.sh \ /etc/systemd/system/n1-ql-restore.service \ /root/cron_backup.txt \ /root/pm2_ecosystem_backup.json
2,查看包里文件清单确认
1 tar -ztvf /root/n1_backup.tar.gz
3,能看到 5 个文件就全部正常
1 2 3 4 5 root/n1.sh root/ql-scrub.sh etc/systemd/system/n1-ql-restore.service root/cron_backup.txt root/pm2_ecosystem_backup.json
4,看备份包大小
1 ls -lh /root/n1_backup.tar.gz
📌现在这个备份包,是基于无‑jq 一键脚本生成出来的环境打包出来的
n1.sh:QL_APPS=() 默认关闭容器巡检、pm2 id 判断、无 jq
ql‑scrub.sh:日志轮转脚本
systemd 服务:开机 40s 同时恢复宿主机 pm2 + 青龙容器 pm2
cron_backup.txt:保存当前干净的 3 条定时
pm2_ecosystem_backup.json:pm2 进程清单备份
三,N1 重装 Armbian 之后完整恢复整套守护环境 这个备份只保存自愈守护相关:巡检脚本、日志清理、systemd 开机服务、定时任务、pm2 进程清单
✅ n1.sh 自愈脚本:使用pm2 id判断进程,彻底避开 pm2 jq 解析 bug,go‑cqhttp/sillyGirl/xdd 识别 online,不会误重启 ✅ ql‑scrub.sh 日志裁剪脚本重建完成,防止日志打爆磁盘 ✅ systemd n1‑ql‑restore.service:开机延时恢复 pm2 ✅ crontab 定时已精简,无重复任务,无宝塔旧垃圾定时 ✅ flock 锁防止脚本并发重复运行 ✅ 备份包包含全部守护逻辑脚本、systemd 服务、定时导出、pm2 进程清单
1,N1 重装 Armbian ,点我
2,安装好宝塔、docker、pm2、青龙容器、go‑cqhttp、sillyGirl、xdd
3,把备份的 n1_backup.tar.gz 上传到宝塔 root 目录下,然后依次执行
1 2 3 4 5 6 7 8 9 10 tar -zxvf /root/n1_backup.tar.gz -C / chmod +x /root/n1.shchmod +x /root/ql-scrub.shcrontab /root/cron_backup.txt systemctl daemon-reload systemctl enable n1-ql-restore.service pm2 start /root/pm2_ecosystem_backup.json pm2 save mkdir -p /root/ql/logsbash /root/n1.sh
执行完最后一条 bash /root/n1.sh,输出三条 ✅ xxx online,代表整套自愈恢复成功
4,恢复完成后校验
1 2 3 4 5 6 crontab -l systemctl status n1-ql-restore.service bash /root/n1.sh
✅预期输出
1 2 3 4 5 6 7 8 === N1 夜维 xxxx === ✅ 腿[go-cqhttp] online ✅ 腿[sillyGirl] online ✅ 腿[xdd] online 3/3 0/0 ✅ 磁盘 xx% === 巡检结束 ===
1 2 tail -n 20 /root/ql/logs/selfheal.log
5,可选,整机重启测试(最重要的验证)
盒子重启,SSH 重新登录进来
1 2 3 pm2 list bash /root/n1.sh
重启后 pm2 三个进程能自动拉起来、自愈脚本识别全部 online,代表完整恢复成功
后台自动工作:
①每 5 分钟自动运行 /root/n1.sh,进程挂掉自动重启 ②凌晨 3:05 运行日志清理脚本,防止日志占磁盘 ③开机 systemd 服务自动恢复 pm2 进程
6,日常排查命令
1 2 3 4 crontab -l systemctl status n1-ql-restore.service bash /root/n1.sh tail -n 20 /root/ql/logs/selfheal.log