#!/bin/bash set -e echo ">>> 开始自动配置 NapCat 环境..." # 1. 确保配置目录存在并写入 NapCat 配置文件 mkdir -p /app/napcat/config cat << 'EOF' > /app/napcat/config/onebot11_3823042923.json { "network": { "httpServers": [ { "name": "API_Server", "enable": true, "port": 7667, "host": "0.0.0.0", "enableCors": true } ], "httpClients": [ { "name": "Flask_Webhook", "enable": true, "url": "http://127.0.0.1:7668/" } ] } } EOF echo ">>> NapCat 配置文件已生成!" # 2. 自动生成精简版 Bot.py echo ">>> 正在生成 Bot.py..." cat << 'EOF' > /app/Bot.py import json import random import urllib.parse import threading import html import requests import time import os from flask import Flask, request from gradio_client import Client app = Flask('kitakaze') SELF = '3823042923' API_BASE = 'http://127.0.0.1:7667' HF_SPACE_URL = "https://hf.4z.autos/" HF_TOKEN = os.getenv('HF_TOKEN') # 如果你的 Hugging Face 仓库是 Private 的,必须在请求头里带上 Token 才能访问 API HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {} try: print("正在初始化 Hugging Face 云端连接...") if not HF_TOKEN: print("警告: 环境变量 HF_TOKEN 未设置!") hf_client = Client(HF_SPACE_URL, token=HF_TOKEN) print("Hugging Face 云端连接就绪!") except Exception as e: print(e) def Reply(mid): return f'[CQ:reply,id={mid}]' def Send(msg, uid, gid=None): if not msg: return if isinstance(msg, list): msg = random.choice(msg) encoded_msg = urllib.parse.quote(str(msg)) try: if gid: requests.get(f'{API_BASE}/send_group_msg?group_id={gid}&message={encoded_msg}') else: requests.get(f'{API_BASE}/send_private_msg?user_id={uid}&message={encoded_msg}') except Exception as e: pass def ContainsRoundInfo(s): return '局' in s or '本场' in s def extract_paipu_id(paipu_url_or_msg): """在本地计算牌谱 ID,确保生成的链接与云端保存的文件名完全一致""" match = re.search(r'log=([\w-]+)', paipu_url_or_msg) if match: return match.group(1) return hashlib.md5(paipu_url_or_msg.encode('utf-8')).hexdigest() def start_analyze(mid, uid, gid, msg, hanchan=False): def analyze_task(): try: # 1. 提取或生成本次请求对应的 ID paipu_id = extract_paipu_id(msg) # 2. 准备 HTTP 请求,目标是我们刚才用 FastAPI 写好的兼容接口 # 注意:如果 URL 结尾有斜杠则去掉,防止拼出双斜杠 api_endpoint = HF_SPACE_URL.rstrip('/') + "/api/predict" payload = {"data": [msg, hanchan]} # 3. 发送纯正的 POST 请求(设置超时时间,以防云端检讨太久假死) response = requests.post( api_endpoint, json=payload, headers=HEADERS, timeout=180 ) response.raise_for_status() # 如果是 401/404/500 等 HTTP 错误会直接跳入 except # 4. 解析后端返回的 JSON 数据 # 后端返回格式是 {"data": ["{真正的结果JSON字符串}"]} response_json = response.json() result_str = response_json.get("data", ["{}"])[0] result_data = json.loads(result_str) # 5. 业务错误处理 if "error" in result_data: error_msg = str(result_data['error'])[:50] Send(Reply(mid) + f"云端检讨失败,可能牌谱链接有误\n({error_msg}...)", uid, gid) return # 6. 构造最终要发送给用户的消息(不再需要把 result_data 写入本地磁盘) overall_rating = result_data.get("overall_rating", 0.0) final_msg = f'看完啦!\nhttps://online.4z.autos/?id={paipu_id}\n总体评分 {overall_rating}' Send(Reply(mid) + final_msg, uid, gid) except requests.exceptions.RequestException as e: print(f"HTTP 通信出错: {e}") Send(Reply(mid) + '有点不懂,改日再看\n(云端连接断开或超时)', uid, gid) except Exception as e: print(f"解析或调度出错: {e}") Send(Reply(mid) + '有点不懂,改日再看\n(客户端处理异常)', uid, gid) # 启动后台线程 thread = threading.Thread(target=analyze_task) thread.daemon = True thread.start() def OnPoke(uid, gid): result = random.choice(['?', '别戳了!', '轻点!', '!!', '?!', '(躲)', '(溜了)', '不要戳!', '(盯)', '……!']) Send(result, uid, gid) SELF_CALL = ['北风', 'kitakaze', f'[CQ:at,qq={SELF}]'] def DealWith(msg, uid, gid, mid): called = any(calls in msg for calls in SELF_CALL) or gid is None if not called: return if '检讨' in msg: if 'http' in msg and '://' in msg: if 'xxxxxx' in msg: Send(Reply(mid) + '天凤牌谱需要手动复制小局内容哦', uid, gid) else: Send(Reply(mid) + '吾辈琢磨琢磨(思考中)……', uid, gid) hanchan = not ContainsRoundInfo(msg) start_analyze(mid, uid, gid, msg, hanchan) else: Send(Reply(mid) + '请发送带有有效 http 链接的牌谱哦!', uid, gid) return result = random.choice(['?', '有事吗', '何事', '。?', '。', '说', '怎么了', '1', '在', '我在?', '什么事', '有事直说', '不在', '干嘛']) Send(Reply(mid) + result, uid, gid) @app.route('/', methods=["POST", "GET"]) def handle_events(): data = request.json print(data) if not data: return '{}' post_type = data.get('post_type') if post_type == 'message': message_type = data.get('message_type') mid = data.get('message_id') msg = html.unescape(data.get('raw_message', '')) uid = data.get('sender', {}).get('user_id') if message_type == 'private': threading.Thread(target=DealWith, args=(msg, uid, None, mid)).start() elif message_type == 'group': gid = data.get('group_id') threading.Thread(target=DealWith, args=(msg, uid, gid, mid)).start() elif post_type == 'notice' and data.get('notice_type') == 'notify' and data.get('sub_type') == 'poke': if str(data.get('target_id')) == SELF: OnPoke(data.get('user_id'), data.get('group_id')) return '{}' if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=7668) EOF echo ">>> Bot.py 文件已生成!" echo ">>> 检查并注入本地凭证..." if [ -f "/app/qq_session.tar.gz" ]; then echo "发现 QQ 凭证,正在恢复到系统..." cd ~ tar -xzf /app/qq_session.tar.gz fi if [ -f "/app/napcat_config.tar.gz" ]; then echo "发现 NapCat 配置文件,正在覆盖..." cd /app tar -xzf /app/napcat_config.tar.gz fi cd /app sed -i 's|LD_PRELOAD=./libnapcat_launcher.so qq --no-sandbox|& -q 3823042923|' launcher.sh cat launcher.sh # ========================================== # 启动你的进程 # ========================================== echo ">>> 使用 screen 在后台启动 Python 机器人进程..." screen -dmS qqbot bash -c "python3 /app/Bot.py" echo ">>> 使用 screen 在后台启动官方 NapCat Launcher..." screen -dmS napcat bash -c "cd /app && bash ./launcher.sh" echo ">>> 初始化完毕!正在拉起 Web Shell..." echo "=========================================================" echo "提示:打开 Web Shell 终端后,你可以输入以下命令:" echo "查看 NapCat 二维码: screen -r napcat" echo "查看 Bot 运行日志: screen -r qqbot" echo "退出屏幕 (切回后台): 按下 Ctrl+A,然后按 D" echo "=========================================================" exec uvicorn app:app --host 0.0.0.0 --port 7860