#!/usr/bin/env python3 import os import threading import time import gradio as gr from fastapi import FastAPI from fastapi.responses import JSONResponse from collections import deque from datetime import datetime import requests from urllib.parse import unquote # ============== 环境变量 ============== PANEL_NA1_COOKIES = os.environ.get("PANEL_NA1_COOKIES", "") TG_BOT_TOKEN = os.environ.get("TG_BOT_TOKEN", "") TG_CHAT_ID = os.environ.get("TG_CHAT_ID", "") TG_API = os.environ.get("TG_API", "https://api.telegram.org") PROJECT_URL = os.environ.get("PROJECT_URL", "") # ============== 共享状态 ============== worker_status = { "coins": 0, "checks": 0, "status": "waiting", "afk": False, "multiplier": 1, "active_users": 0, "session_coins": 0, "coins_per_minute": 1, "initial_coins": 0, "napoints": 0, "start_time": None, "keepalive": None } log_queue = deque(maxlen=100) def add_log(msg): ts = datetime.now().strftime("%H:%M:%S") line = f"[{ts}] {msg}" print(line) log_queue.append(line) # ============== 保活 ============== def add_keepalive_task(): if not PROJECT_URL: add_log("[KEEP] ⏭️ 未设置 PROJECT_URL") worker_status['keepalive'] = 'skipped' return add_log(f"[KEEP] 🔄 添加保活...") try: r = requests.post("https://trans.ct8.pl/add-url", json={"url": PROJECT_URL}, timeout=30) worker_status['keepalive'] = 'success' if r.status_code == 200 else 'failed' add_log(f"[KEEP] {'✅ 成功' if r.status_code == 200 else '❌ 失败'}") except Exception as e: worker_status['keepalive'] = 'failed' add_log(f"[KEEP] ❌ {e}") # ============== Worker ============== class AFKWorker: def __init__(self): self.session = requests.Session() self.base = "https://panel.na1.host" self.xsrf = None self.coins = self.initial = 0 self._init_session() def _init_session(self): self.session.headers.update({ "User-Agent": "Mozilla/5.0", "Accept": "application/json", "Referer": self.base, "Origin": self.base }) for item in PANEL_NA1_COOKIES.split(';'): if '=' in item: k, v = item.strip().split('=', 1) self.session.cookies.set(k.strip(), v.strip(), domain="panel.na1.host") if k.strip() == "XSRF-TOKEN": self.xsrf = unquote(v.strip()) def _h(self): return {"X-XSRF-TOKEN": self.xsrf} if self.xsrf else {} def _upd(self, r): for c in r.cookies: self.session.cookies.set(c.name, c.value, domain="panel.na1.host") if c.name == "XSRF-TOKEN": self.xsrf = unquote(c.value) def tg(self, msg): if TG_BOT_TOKEN and TG_CHAT_ID: try: requests.post(f"{TG_API}/bot{TG_BOT_TOKEN}/sendMessage", data={"chat_id": TG_CHAT_ID, "text": msg}, timeout=10) except: pass def login(self): try: r = self.session.get(f"{self.base}/api/client", headers=self._h(), timeout=30) self._upd(r) return r.status_code == 200 except: return False def wallet(self): try: r = self.session.get(f"{self.base}/api/client/account/wallet", headers=self._h(), timeout=30) self._upd(r) if r.status_code == 200: d = r.json() self.coins = d.get('coins', 0) worker_status['coins'] = self.coins worker_status['napoints'] = d.get('napoints', 0) return True except: pass return False def join(self): try: r = self.session.post(f"{self.base}/api/client/account/afk/join", headers=self._h(), timeout=30) self._upd(r) return r.status_code in [200, 409] except: return False def status(self): try: r = self.session.get(f"{self.base}/api/client/account/afk/status", headers=self._h(), timeout=30) self._upd(r) if r.status_code == 200: d = r.json() worker_status.update({ 'active_users': d.get('active_users', 0), 'multiplier': d.get('multiplier', 1), 'coins_per_minute': d.get('coins_per_minute', 1), 'session_coins': d.get('session', {}).get('coins_earned', 0), 'afk': True }) return True except: pass return False def run(self): worker_status['start_time'] = time.time() add_log("🎮 NA1 AFK Bot 启动中...") add_keepalive_task() if not self.login(): worker_status['status'] = 'cookie_expired' add_log("❌ Cookie 已过期") self.tg("❌ Cookie 已过期") return add_log("✅ 登录成功") self.wallet() self.initial = worker_status['initial_coins'] = self.coins add_log(f"💰 初始: {self.coins}") self.join() self.status() add_log(f"📊 在线 {worker_status['active_users']} 人 | {worker_status['multiplier']}x") worker_status['status'] = 'running' self.tg(f"✅ AFK 启动 | 💰 {self.coins}") last_tg = time.time() while worker_status['status'] == 'running': time.sleep(60) worker_status['checks'] += 1 n = worker_status['checks'] if self.status(): add_log(f"[{n:03d}] 👥{worker_status['active_users']} ⚡{worker_status['multiplier']}x +{worker_status['session_coins']}") if n % 5 == 0: old = self.coins self.wallet() add_log(f"[SYNC] 💰{self.coins} (+{self.coins-old})") if n % 30 == 0: self.join() if time.time() - last_tg >= 3600: last_tg = time.time() self.tg(f"📊 {self.coins} (+{self.coins-self.initial})") def auto_start(): if not PANEL_NA1_COOKIES: add_log("❌ 未设置 PANEL_NA1_COOKIES") worker_status['status'] = 'no_cookie' add_keepalive_task() return AFKWorker().run() threading.Thread(target=auto_start, daemon=True).start() # ============== FastAPI ============== app = FastAPI() @app.get("/") def health(): s = worker_status return JSONResponse({ "status": "ok", "coins": s['coins'], "earned": s['coins'] - s.get('initial_coins', 0), "checks": s['checks'], "worker": s['status'], "afk": s['afk'] }) # ============== Gradio ============== def make_page(): s = worker_status total = s['coins'] - s.get('initial_coins', 0) rt = "--" if s.get('start_time'): m = (time.time() - s['start_time']) / 60 rt = f"{m:.0f}分" if m < 60 else f"{m/60:.1f}时" status_map = { "running": ("🟢", "运行中", "#10b981", "#ecfdf5"), "waiting": ("🟡", "启动中", "#f59e0b", "#fffbeb"), "no_cookie": ("🔴", "未配置", "#ef4444", "#fef2f2"), "cookie_expired": ("🔴", "已过期", "#ef4444", "#fef2f2"), } s_icon, s_text, s_color, s_bg = status_map.get(s['status'], ("⚪", s['status'], "#6b7280", "#f3f4f6")) ka_map = { "success": ("🛡️", "保活", "#3b82f6", "#eff6ff"), "failed": ("⚠️", "失败", "#ef4444", "#fef2f2"), "skipped": ("⏭️", "跳过", "#6b7280", "#f3f4f6"), None: ("⏳", "等待", "#f59e0b", "#fffbeb"), } k_icon, k_text, k_color, k_bg = ka_map.get(s.get('keepalive'), ("⏳", "等待", "#f59e0b", "#fffbeb")) logs = "\n".join(log_queue) if log_queue else "⏳ 等待日志..." return f'''
{s_icon} {s_text}
{k_icon} {k_text}
💰 {s['coins']:,} 📈 +{total:,} 👥 {s.get('active_users', 0)} {s.get('multiplier', 1)}x ⏱️ {rt}
📋 日志 #{s['checks']}
{logs}
''' CSS = """ /* 隐藏 HuggingFace 顶部栏 */ #huggingface-space-header { display: none !important; } footer { display: none !important; } .gradio-container { background: #f8fafc !important; padding: 0 !important; } .container { max-width: 1100px; margin: 0 auto; padding: 16px; font-family: system-ui, sans-serif; } .header { display: flex; align-items: center; gap: 10px; padding: 12px 16px; background: white; border-radius: 10px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 12px; flex-wrap: wrap; } .logo { font-size: 16px; font-weight: 700; color: #1e293b; } .badge { display: inline-flex; align-items: center; gap: 5px; padding: 5px 10px; border-radius: 16px; font-size: 12px; font-weight: 600; } .stats-row { display: flex; gap: 14px; margin-left: auto; font-size: 13px; color: #64748b; flex-wrap: wrap; } .stats-row b { color: #1e293b; } .stats-row .green { color: #10b981; } .main { display: grid; grid-template-columns: 1fr 320px; gap: 12px; } @media (max-width: 800px) { .main { grid-template-columns: 1fr; } } .card { background: white; border-radius: 10px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); overflow: hidden; } .card-title { padding: 10px 14px; font-size: 13px; font-weight: 600; color: #374151; border-bottom: 1px solid #e5e7eb; display: flex; } .card-title .sub { margin-left: auto; color: #9ca3af; font-weight: 400; } .logs { padding: 12px; font-family: 'SF Mono', Consolas, monospace; font-size: 11px; line-height: 1.8; color: #475569; background: #f8fafc; height: 500px; overflow-y: auto; white-space: pre-wrap; } .sidebar { display: flex; flex-direction: column; gap: 12px; } .stat-grid { display: grid; grid-template-columns: 1fr 1fr; } .stat-item { padding: 12px; text-align: center; border-bottom: 1px solid #e5e7eb; } .stat-item:nth-child(odd) { border-right: 1px solid #e5e7eb; } .stat-item:nth-last-child(-n+2) { border-bottom: none; } .stat-val { font-size: 20px; font-weight: 700; color: #1e293b; } .stat-val.green { color: #10b981; } .stat-val.blue { color: #3b82f6; } .stat-label { font-size: 10px; color: #9ca3af; text-transform: uppercase; } /* 配置表格 */ .config-table { padding: 0; } .config-row { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; border-bottom: 1px solid #f1f5f9; font-size: 11px; } .config-row:last-child { border-bottom: none; } .config-row.header-row { background: #f8fafc; font-weight: 600; color: #64748b; font-size: 10px; } .config-row code { background: #f1f5f9; padding: 3px 6px; border-radius: 4px; font-size: 10px; color: #1e293b; font-family: 'SF Mono', Consolas, monospace; } .config-row .req { color: #ef4444; font-size: 9px; } .config-row .opt { color: #6b7280; font-size: 9px; } .config-name { font-weight: 600; } .config-desc { color: #64748b; } /* 帮助步骤 */ .help-steps { padding: 12px; } .step { display: flex; align-items: center; gap: 10px; padding: 6px 0; font-size: 12px; color: #475569; } .step-num { width: 20px; height: 20px; background: #3b82f6; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 600; flex-shrink: 0; } .step b { color: #1e293b; } .url-example { margin: 10px 0; padding: 8px 12px; background: #f1f5f9; border-radius: 6px; border-left: 3px solid #3b82f6; } .url-example code { font-size: 11px; color: #3b82f6; font-family: 'SF Mono', Consolas, monospace; word-break: break-all; } .tip { margin-top: 10px; padding: 10px 12px; background: #fffbeb; border-radius: 6px; font-size: 11px; color: #92400e; line-height: 1.5; } .tip code { background: #fef3c7; padding: 2px 5px; border-radius: 3px; font-size: 10px; color: #92400e; } """ with gr.Blocks(title="NA1 AFK", css=CSS, theme=gr.themes.Soft()) as gradio_app: html = gr.HTML(make_page) gr.Timer(5).tick(make_page, outputs=html) app = gr.mount_gradio_app(app, gradio_app, path="/na1") if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=7860)