diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3f59639 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.git +.github +.venv +__pycache__/ +*.pyc +.dockerignore +Dockerfile +README.md \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..34b7b9b --- /dev/null +++ b/.env.example @@ -0,0 +1,35 @@ +# 账户配置 +USER1=your_username1 +PASS1=your_password1 +# USER2=your_username2 +# PASS2=your_password2 + +# --- 验证码配置 (二选一) --- + +# 方案A: 自建 CloudFreed 服务 +SOLVER_TYPE=turnstile +API_BASE_URL=http://your_cloudflare_service_ip:3000 +CLIENTT_KEY=your_cloudfreed_client_key + +# 方案B: YesCaptcha 服务 +# SOLVER_TYPE=yescaptcha +# # YesCaptcha 国内节点: https://cn.yescaptcha.com, 国际节点: https://api.yescaptcha.com +# API_BASE_URL=https://api.yescaptcha.com +# CLIENTT_KEY=your_yescaptcha_client_key + +# 功能配置 +# NS_RANDOM=true + +# 每日运行时间配置 +# 支持固定时间 (例如 '09:00') 或随机时间范围 (例如 '08:00-10:59') +# 如果不设置,默认为 '08:00-10:59' +# RUN_AT=09:00 +RUN_AT=08:00-10:59 + +# 通知配置 +# TG_BOT_TOKEN= +# TG_USER_ID= +# TG_THREAD_ID= + +# 模拟Docker环境,用于本地测试 +# IN_DOCKER=true \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f02f44 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Python virtual environment +.venv/ + +# Python cache +__pycache__/ +*.pyc + +# Sensitive configuration +.env + +# Cookie data files +cookie/* +!cookie/.gitkeep + +# OS generated files +.DS_Store +Thumbs.db + +# IDE/Editor configuration +.vscode/ +.idea/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..35e7c2c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +# 使用轻量级的 Python 基础镜像 +FROM python:3.9-alpine + +# 设置时区为 GMT+8 +RUN apk add --no-cache tzdata +ENV TZ=Asia/Shanghai + +# 设置工作目录 +WORKDIR /app + +# 复制 requirements.txt 并安装依赖 +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# 复制所有 .py 文件到工作目录 +COPY *.py ./ + +# 设置默认启动命令 +CMD ["python", "scheduler.py"] \ No newline at end of file diff --git a/README.md b/README.md index 438657a..df770fc 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,14 @@ ## 📝 项目介绍 -这是一个用于 NodeSeek 论坛自动签到的工具,支持通过 GitHub Actions 或青龙面板进行定时自动签到操作。签到模式默认为随机签到,帮助用户轻松获取论坛每日"鸡腿"奖励。 +这是一个用于 NodeSeek 论坛自动签到的工具,支持通过 GitHub Actions、青龙面板或 Docker Compose 进行定时自动签到操作。签到模式默认为随机签到,帮助用户轻松获取论坛每日"鸡腿"奖励。 ## ✨ 功能特点 - 📅 支持 GitHub Actions 自动运行 - 🦉 支持青龙面板定时任务 +- 🐳 支持 Docker Compose 一键部署 - 🍪 支持 Cookie 或账号密码登录方式 - 👥 支持多账号批量签到 - 🔐 支持多种验证码解决方案 @@ -55,7 +56,60 @@ ql repo https://github.com/yowiv/NodeSeek-Signin.git 然后在环境变量中添加所需配置。 -### 方式三:账号密码登录(自动获取新Cookie) +### 方式三:Docker Compose + +这种部署方式可以实现本地自动化定时签到,并支持自动处理验证码。 + +**第一步:克隆项目** + +首先,将整个项目克隆到你的服务器上: + +```bash +git clone https://github.com/yowiv/NodeSeek-Signin.git +cd NodeSeek-Signin +``` + +**第二步:配置环境变量** + +将 `.env.example` 文件复制或重命名为 `.env`,然后编辑 `.env` 文件,填入你的配置信息。 + +```bash +cp .env.example .env +``` + +你需要根据注释提示,填写以下关键信息: +- **账户信息**: `USER1`, `PASS1`, `USER2`, `PASS2` 等。 +- **验证码服务**: 如果使用账号密码登录,必须配置验证码服务。推荐使用 `yescaptcha`,并填入 `CLIENTT_KEY`。 +- **定时任务**: `RUN_AT` 变量用于设置签到任务的执行时间。 + - **固定时间**: 如 `10:30`,表示每天上午10点30分执行。 + - **时间范围**: 如 `10:00-18:00`,表示在每天上午10点到下午6点之间随机选择一个时间点执行。 + - **默认值**: 如果不设置,默认为 `09:00-21:00`。 + +**第三步:启动服务** + +在存放 `docker-compose.yml` 和 `.env` 文件的目录下,执行以下命令在后台构建并启动服务: + +```bash +docker-compose up -d +``` + +**第四步:查看日志** + +你可以使用以下命令实时查看容器的日志,以确认服务是否正常运行和签到是否成功: + +```bash +docker-compose logs -f +``` + +**第五步:停止服务** + +如果需要停止并移除服务容器,可以执行以下命令: + +```bash +docker-compose down +``` + +### 方式四:账号密码登录(自动获取新Cookie) 当 Cookie 失效时,系统会尝试使用账号密码方式登录并获取新的 Cookie。登录需要通过验证码验证,支持以下两种验证码解决方案: @@ -171,6 +225,7 @@ PASS3=密码3 | `USER1`、`USER2`... | 可选 | - | NodeSeek 论坛用户名,当 Cookie 失效时使用 | | `PASS1`、`PASS2`... | 可选 | - | NodeSeek 论坛密码 | | `NS_RANDOM` | 可选 | true | 是否随机签到(true/false) | +| `RUN_AT` | 可选 | `09:00-21:00` | **仅Docker Compose可用**。设置定时任务执行时间,支持固定时间 `10:30` 或时间范围 `10:00-18:00` | | `SOLVER_TYPE` | 可选 | turnstile | 验证码解决方案(turnstile/yescaptcha) | | `API_BASE_URL` | 条件必需 | - | CloudFreed 服务地址,当 SOLVER_TYPE=turnstile 时必填 | | `CLIENTT_KEY` | 必需 | - | 验证码服务客户端密钥 | @@ -187,3 +242,45 @@ PASS3=密码3 ## ⚠️ 免责声明 本项目仅供学习交流使用,请遵守 NodeSeek 论坛的相关规定和条款。 + +## 👨‍💻 本地开发与测试 + +为了方便在本地进行开发和调试,项目提供了一套零侵入的测试方案。你可以按照以下步骤在本地环境中运行签到脚本。 + +### 第一步:创建虚拟环境 + +首先,建议创建一个独立的 Python 虚拟环境,以避免依赖冲突。 + +```bash +python -m venv .venv +# Windows +.venv\Scripts\activate +# macOS / Linux +source .venv/bin/activate +``` + +### 第二步:安装依赖 + +项目包含两份依赖文件:`requirements.txt` 用于生产环境,`requirements-dev.txt` 包含本地测试所需的额外库。 + +```bash +pip install -r requirements.txt +pip install -r requirements-dev.txt +``` + +### 第三步:配置环境变量 + +将环境变量示例文件 `.env.example` 复制一份并重命名为 `.env`,然后根据你的需求填入测试配置,例如账号密码和验证码服务密钥。 + +```bash +cp .env.example .env``` + +### 第四步:运行测试 + +配置完成后,执行以下命令即可运行一次性的签到测试。 + +```bash +python test_run.py +``` + +该脚本会自动加载 `.env` 文件中的环境变量,并执行主签到程序,让你可以在本地快速验证签到逻辑是否正常工作。 diff --git a/cookie/.gitkeep b/cookie/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e424ed7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +version: '3.8' +services: + nodeseek-signin: + build: . + environment: + - IN_DOCKER=true + env_file: + - .env + volumes: + - ./cookie:/app/cookie + restart: always \ No newline at end of file diff --git a/nodeseek_sign.py b/nodeseek_sign.py index 00f5e1d..97a2b31 100644 --- a/nodeseek_sign.py +++ b/nodeseek_sign.py @@ -2,7 +2,6 @@ import os import time -import json from datetime import datetime, timedelta from zoneinfo import ZoneInfo from curl_cffi import requests @@ -20,6 +19,10 @@ except ImportError: # ---------------- 环境检测函数 ---------------- def detect_environment(): """检测当前运行环境""" + # 优先检测是否在 Docker 环境中 + if os.environ.get("IN_DOCKER") == "true": + return "docker" + # 检测是否在青龙环境中 ql_path_markers = ['/ql/data/', '/ql/config/', '/ql/', '/.ql/'] in_ql_env = False @@ -138,12 +141,31 @@ def save_cookie_to_ql(var_name: str, cookie: str): print(f"青龙面板环境变量操作异常: {str(e)}") return False +# ---------------- Docker Cookie 文件保存 ---------------- +COOKIE_FILE_PATH = "./cookie/NS_COOKIE.txt" + +def save_cookie_to_file(cookie_str: str): + """将Cookie保存到文件""" + try: + # 确保目录存在 + os.makedirs(os.path.dirname(COOKIE_FILE_PATH), exist_ok=True) + with open(COOKIE_FILE_PATH, "w") as f: + f.write(cookie_str) + print(f"Cookie 已成功保存到文件: {COOKIE_FILE_PATH}") + return True + except Exception as e: + print(f"保存Cookie到文件失败: {e}") + return False + # ---------------- 统一变量保存函数 ---------------- def save_cookie(var_name: str, cookie: str): """根据当前环境保存Cookie到相应位置""" env_type = detect_environment() - if env_type == "qinglong": + if env_type == "docker": + print("检测到Docker环境,保存Cookie到文件...") + return save_cookie_to_file(cookie) + elif env_type == "qinglong": print("检测到青龙环境,保存变量到青龙面板...") return save_cookie_to_ql(var_name, cookie) elif env_type == "github": @@ -393,7 +415,21 @@ if __name__ == "__main__": break # 读取现有Cookie - all_cookies = os.getenv("NS_COOKIE", "") + all_cookies = "" + if detect_environment() == "docker": + print(f"Docker环境,尝试从 {COOKIE_FILE_PATH} 读取Cookie...") + if os.path.exists(COOKIE_FILE_PATH): + try: + with open(COOKIE_FILE_PATH, "r") as f: + all_cookies = f.read().strip() + print("成功从文件加载Cookie。") + except Exception as e: + print(f"从文件读取Cookie失败: {e}") + else: + print("Cookie文件不存在,将使用空Cookie。") + else: + all_cookies = os.getenv("NS_COOKIE", "") + cookie_list = all_cookies.split("&") cookie_list = [c.strip() for c in cookie_list if c.strip()] diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..3e338bf --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1 @@ +python-dotenv \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..244eeed --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +curl_cffi +requests +tzdata \ No newline at end of file diff --git a/scheduler.py b/scheduler.py new file mode 100644 index 0000000..08d4b3a --- /dev/null +++ b/scheduler.py @@ -0,0 +1,120 @@ +import os +import time +import datetime +import random +import subprocess +import re +from datetime import timezone, timedelta + +GMT8 = timezone(timedelta(hours=8)) + +def get_run_config(): + """ + 从环境变量 RUN_AT 读取并解析运行时间配置。 + - 'HH:MM': 固定时间 + - 'HH:MM-HH:MM': 随机时间范围 + - 未设置或格式错误: 默认为 '08:00-10:59' + 返回一个元组 (mode, value) + """ + run_at_env = os.environ.get('RUN_AT', '08:00-10:59') + + if re.fullmatch(r'\d{2}:\d{2}', run_at_env): + print(f"检测到固定时间模式: {run_at_env}") + return 'fixed', run_at_env + + if re.fullmatch(r'\d{2}:\d{2}-\d{2}:\d{2}', run_at_env): + print(f"检测到随机时间范围模式: {run_at_env}") + return 'range', run_at_env + + if os.environ.get('RUN_AT'): + print(f"警告: 环境变量 RUN_AT 的格式 '{run_at_env}' 无效。") + + print("将使用默认随机时间范围 '08:00-10:59'。") + return 'range', '08:00-10:59' + +def calculate_next_run_time(mode, value): + """ + 根据当前时间和配置计算下一次运行的 datetime 对象。 + 智能寻找下一个可用的运行时间点(今天或明天),并使用 GMT+8 时区。 + """ + now = datetime.datetime.now(GMT8) + + if mode == 'fixed': + h, m = map(int, value.split(':')) + next_run_attempt = now.replace(hour=h, minute=m, second=0, microsecond=0) + if next_run_attempt > now: + return next_run_attempt + else: + return next_run_attempt + datetime.timedelta(days=1) + + elif mode == 'range': + start_str, end_str = value.split('-') + start_h, start_m = map(int, start_str.split(':')) + end_h, end_m = map(int, end_str.split(':')) + + start_time = datetime.time(start_h, start_m) + end_time = datetime.time(end_h, end_m) + + start_today = now.replace(hour=start_h, minute=start_m, second=0, microsecond=0) + + if now < start_today: + target_date = now.date() + else: + target_date = now.date() + datetime.timedelta(days=1) + + start_target = datetime.datetime.combine(target_date, start_time, tzinfo=GMT8) + end_target = datetime.datetime.combine(target_date, end_time, tzinfo=GMT8) + + if start_target > end_target: + end_target += datetime.timedelta(days=1) + + start_timestamp = int(start_target.timestamp()) + end_timestamp = int(end_target.timestamp()) + + random_timestamp = random.randint(start_timestamp, end_timestamp) + return datetime.datetime.fromtimestamp(random_timestamp, tz=GMT8) + +def run_checkin_task(): + """ + 执行 nodeseek_sign.py 脚本。 + """ + print(f"[{datetime.datetime.now(GMT8).strftime('%Y-%m-%d %H:%M:%S')}] 开始执行签到任务...") + try: + subprocess.run(["python", "nodeseek_sign.py"], check=True) + print(f"[{datetime.datetime.now(GMT8).strftime('%Y-%m-%d %H:%M:%S')}] 签到任务执行完毕。") + except FileNotFoundError: + print("错误: 'nodeseek_sign.py' 未找到。请确保它与 scheduler.py 位于同一目录。") + except subprocess.CalledProcessError as e: + print(f"签到任务执行失败,返回码: {e.returncode}") + except Exception as e: + print(f"执行签到任务时发生未知错误: {e}") + +def main(): + """ + 主调度循环。 + """ + print("调度器启动...") + mode, value = get_run_config() + print(f"调度模式: '{mode}', 配置值: '{value}'") + + run_checkin_task() + + while True: + next_run_time = calculate_next_run_time(mode, value) + now = datetime.datetime.now(GMT8) + sleep_duration = (next_run_time - now).total_seconds() + + if sleep_duration > 0: + print(f"下一次签到任务计划在: {next_run_time.strftime('%Y-%m-%d %H:%M:%S')}") + hours, remainder = divmod(sleep_duration, 3600) + minutes, _ = divmod(remainder, 60) + print(f"程序将休眠 {int(hours)} 小时 {int(minutes)} 分钟。") + time.sleep(sleep_duration) + else: + print("计算出的下一个运行时间已过,等待 60 秒后重试...") + time.sleep(60) + + run_checkin_task() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/test_run.py b/test_run.py new file mode 100644 index 0000000..2a229b1 --- /dev/null +++ b/test_run.py @@ -0,0 +1,14 @@ +import sys +import subprocess +from dotenv import load_dotenv + +def main(): + """ + 加载 .env 文件并执行签到脚本 + """ + load_dotenv() + print(".env 文件已加载,正在准备执行签到脚本...") + subprocess.run([sys.executable, "nodeseek_sign.py"]) + +if __name__ == "__main__": + main() \ No newline at end of file