|
|
| save_to_hf |
| |
| trap "" SIGPIPE |
|
|
| NAP_CONFIG="/app/napcat/config" |
| NAP_PLUGINS="/app/napcat/plugins" |
| QQ_CONFIG1="/app/.config/QQ" |
| QQ_CONFIG2="/home/user/.config/QQ" |
| mkdir -p $NAP_CONFIG $QQ_CONFIG1 $QQ_CONFIG2 $NAP_PLUGINS |
|
|
| |
| _upload_to_hf() { |
| local file=$1 |
| local repo_path=$2 |
| local label=$3 |
| for i in 1 2 3; do |
| python3 -c " |
| import os |
| from huggingface_hub import HfApi |
| try: |
| HfApi().upload_file( |
| path_or_fileobj='$file', |
| path_in_repo='$repo_path', |
| repo_id=os.environ['DATASET_REPO'], |
| repo_type='dataset', |
| token=os.environ['HF_TOKEN'] |
| ) |
| print('[$label] 备份成功!') |
| except Exception as e: |
| print(f'[$label] 备份失败: {e}') |
| exit(1) |
| " && break |
| echo "[$label] 重试 $i/3..." |
| sleep 5 |
| done |
| } |
|
|
| |
| save_napcat_to_hf() { |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] --- 备份 NapCat 配置 ---" |
| rm -f /tmp/napcat.zip |
| zip -r /tmp/napcat.zip $NAP_CONFIG \ |
| --exclude "*/logs/*" \ |
| --exclude "*/webui/*" |
| _upload_to_hf /tmp/napcat.zip napcat.zip NapCat配置 |
| } |
|
|
| |
| save_qq_to_hf() { |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] --- 备份 QQ 配置 ---" |
| rm -f /tmp/qq.zip |
| zip -r /tmp/qq.zip $QQ_CONFIG1 $QQ_CONFIG2 \ |
| --exclude "*/logs/*" \ |
| --exclude "*/Cache/*" \ |
| --exclude "*/GPUCache/*" \ |
| --exclude "*/nt_db/*" \ |
| --exclude "*/nt_data/*" \ |
| --exclude "*/richMediaProxyFiles/*" \ |
| --exclude "*/cacheFile/*" \ |
| --exclude "*/crash/*" \ |
| --exclude "*/__MACOSX/*" |
| _upload_to_hf /tmp/qq.zip qq.zip QQ配置 |
| } |
|
|
| |
| save_plugins_to_hf() { |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] --- 备份插件数据 ---" |
| rm -f /tmp/plugins.zip |
| zip -r /tmp/plugins.zip $NAP_PLUGINS \ |
| --exclude "*/logs/*" \ |
| --exclude "*/Cache/*" |
| _upload_to_hf /tmp/plugins.zip plugins.zip 插件 |
| } |
|
|
| save_to_hf() { |
| save_napcat_to_hf |
| save_qq_to_hf |
| save_plugins_to_hf |
| } |
|
|
| trap 'echo "Received SIGTERM"; save_to_hf; exit 0' SIGTERM SIGINT |
|
|
| |
| restore_zip() { |
| local zip_file="$1" |
| local label="$2" |
| python3 -c " |
| import zipfile, sys |
| zip_path = sys.argv[1] |
| label = sys.argv[2] |
| try: |
| skipped = 0 |
| extracted = 0 |
| with zipfile.ZipFile(zip_path, 'r') as z: |
| members = z.namelist() |
| print(f'[恢复/{label}] 共 {len(members)} 个文件') |
| for member in members: |
| try: |
| z.extract(member, '/') |
| extracted += 1 |
| except Exception: |
| skipped += 1 |
| print(f'[恢复/{label}] 成功 {extracted} 个,跳过 {skipped} 个') |
| except Exception as e: |
| print(f'[恢复/{label}] 失败: {e}') |
| " "$zip_file" "$label" |
| } |
|
|
| |
| if [ -n "${HF_TOKEN}" ] && [ -n "${DATASET_REPO}" ]; then |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] --- 正在同步:从 HF Dataset 获取备份 ---" |
| python3 -c " |
| import os |
| from huggingface_hub import hf_hub_download |
| |
| # napcat.zip 兼容旧格式(旧版同时含 NapCat + QQ 配置) |
| # qq.zip 新格式单独的 QQ 配置(若存在则覆盖旧版内容) |
| for filename, label in [('napcat.zip', 'NapCat/QQ配置'), ('qq.zip', 'QQ配置'), ('plugins.zip', '插件')]: |
| try: |
| path = hf_hub_download( |
| repo_id=os.environ['DATASET_REPO'], |
| filename=filename, |
| repo_type='dataset', |
| token=os.environ['HF_TOKEN'] |
| ) |
| print(f'[下载] {filename} -> {path}') |
| with open(f'/tmp/restore_{filename}.path', 'w') as f: |
| f.write(path) |
| except Exception as e: |
| print(f'[下载] {filename} 不存在或失败: {e}') |
| " |
| for zip_label in "napcat.zip:NapCat/QQ配置" "qq.zip:QQ配置" "plugins.zip:插件"; do |
| zip_file="${zip_label%%:*}" |
| label="${zip_label##*:}" |
| path_file="/tmp/restore_${zip_file}.path" |
| if [ -f "$path_file" ]; then |
| restore_zip "$(cat $path_file)" "$label" |
| fi |
| done |
|
|
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] --- 恢复完成,目录状态 ---" |
| python3 -c " |
| import glob |
| for path in ['/app/.config/QQ', '/home/user/.config/QQ', '/app/napcat/plugins']: |
| files = glob.glob(path + '/**', recursive=True) |
| print(f' {path}: {len(files)} 个文件/目录') |
| " |
| fi |
|
|
| |
| CONFIG_PATH="$NAP_CONFIG/webui.json" |
| if [ ! -f "${CONFIG_PATH}" ]; then |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] 生成默认 WebUI 配置..." |
| cat > "${CONFIG_PATH}" << EOF |
| { |
| "host": "0.0.0.0", |
| "port": 6099, |
| "token": "${NAPCAT_WEBUI_SECRET_KEY:-admin}", |
| "loginRate": 3 |
| } |
| EOF |
| fi |
|
|
| rm -rf /tmp/.X* |
|
|
| |
| periodic_backup() { |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] [备份] 将在 3 分钟后执行首次备份..." |
| sleep 180 |
| save_to_hf |
| while true; do |
| sleep 1800 |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] --- 定时备份(每30分钟)---" |
| save_to_hf |
| done |
| } |
| periodic_backup & |
|
|
| |
| debug_keepalive() { |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] [保活] 等待 NapCat WebUI 启动..." |
| while ! curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:6099/api/auth/check | grep -q "200"; do |
| sleep 3 |
| done |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] [保活] WebUI 已就绪,启动保活进程..." |
|
|
| python3 -u << 'PYEOF' |
| import os, time, json, asyncio, hashlib |
| import urllib.request |
|
|
| BASE = "http://127.0.0.1:6099" |
| TOKEN = os.environ.get("NAPCAT_WEBUI_SECRET_KEY") or os.environ.get("WEBUI_TOKEN", "admin") |
|
|
| def make_hash(token): |
| return hashlib.sha256((token + ".napcat").encode()).hexdigest() |
|
|
| def login(): |
| for i in range(5): |
| try: |
| data = json.dumps({"hash": make_hash(TOKEN)}).encode() |
| req = urllib.request.Request( |
| f"{BASE}/api/auth/login", |
| data=data, |
| headers={"Content-Type": "application/json"}, |
| method="POST" |
| ) |
| with urllib.request.urlopen(req, timeout=10) as r: |
| resp = json.loads(r.read()) |
| credential = resp.get("data", {}).get("Credential") |
| if credential: |
| print(f"[保活] 登录成功", flush=True) |
| return credential |
| print(f"[保活] 登录响应: {resp}", flush=True) |
| except Exception as e: |
| print(f"[保活] 登录失败({i+1}/5): {e}", flush=True) |
| time.sleep(5) |
| return None |
|
|
| def create_debug_adapter(credential): |
| try: |
| req = urllib.request.Request( |
| f"{BASE}/api/Debug/create", |
| data=b"{}", |
| headers={ |
| "Content-Type": "application/json", |
| "Authorization": f"Bearer {credential}" |
| }, |
| method="POST" |
| ) |
| with urllib.request.urlopen(req, timeout=10) as r: |
| resp = json.loads(r.read()) |
| ws_token = resp.get("data", {}).get("token") |
| if ws_token: |
| print(f"[保活] 调试适配器创建成功", flush=True) |
| return ws_token |
| print(f"[保活] 创建适配器响应: {resp}", flush=True) |
| except Exception as e: |
| print(f"[保活] 创建适配器失败: {e}", flush=True) |
| return None |
|
|
| async def keep_ws_alive(ws_token): |
| import websockets |
| url = f"ws://127.0.0.1:6099/api/Debug/ws?token={ws_token}" |
| try: |
| async with websockets.connect( |
| url, |
| ping_interval=30, |
| ping_timeout=10, |
| ) as ws: |
| print(f"[保活] WS 连接已建立,保持中...", flush=True) |
| async for msg in ws: |
| pass |
| except Exception as e: |
| print(f"[保活] WS 连接断开: {e}", flush=True) |
|
|
| def run(): |
| while True: |
| credential = login() |
| if not credential: |
| print("[保活] 无法登录,60秒后重试...", flush=True) |
| time.sleep(60) |
| continue |
|
|
| ws_token = create_debug_adapter(credential) |
| if not ws_token: |
| print("[保活] 无法获取 WS token,30秒后重试...", flush=True) |
| time.sleep(30) |
| continue |
|
|
| asyncio.run(keep_ws_alive(ws_token)) |
| print("[保活] 连接断开,30秒后重新建立...", flush=True) |
| time.sleep(30) |
|
|
| run() |
| PYEOF |
| } |
| debug_keepalive & |
|
|
| export FFMPEG_PATH=/usr/bin/ffmpeg |
| export QT_X11_NO_MITSHM=1 |
|
|
| nginx & |
|
|
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] Run NapCat QQ..." |
| if [ -n "${ACCOUNT}" ]; then |
| xvfb-run -a /opt/QQ/qq --no-sandbox -q "${ACCOUNT}" & |
| else |
| xvfb-run -a /opt/QQ/qq --no-sandbox & |
| fi |
|
|
| QQ_PID=$! |
| wait $QQ_PID |
| save_to_hf |
|
|