feat(docker): 增加 Docker 支持以实现自动化定时任务

新增通过 `docker-compose` 进行部署和运行的方式,实现了本地自动化定时签到。

主要更新包括:
- 引入 `scheduler.py` 脚本,根据 `RUN_AT` 环境变量管理定时任务,支持固定时间或随机时间范围。
- 在 Docker 环境中,Cookie 将被持久化到 `./cookie` 卷中,以在容器重启后保持登录状态。
- 新增了本地开发和测试流程,通过 `test_run.py` 和 `.env` 文件简化了调试。
- 更新了 `README.md` 文档,包含详细的 Docker 部署和本地开发指南。
This commit is contained in:
hkfires
2025-08-03 11:05:52 +08:00
parent e5edbf2813
commit 9df3e7c8ec
12 changed files with 370 additions and 5 deletions

8
.dockerignore Normal file
View File

@@ -0,0 +1,8 @@
.git
.github
.venv
__pycache__/
*.pyc
.dockerignore
Dockerfile
README.md

35
.env.example Normal file
View File

@@ -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

21
.gitignore vendored Normal file
View File

@@ -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/

19
Dockerfile Normal file
View File

@@ -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"]

101
README.md
View File

@@ -11,13 +11,14 @@
## 📝 项目介绍 ## 📝 项目介绍
这是一个用于 NodeSeek 论坛自动签到的工具,支持通过 GitHub Actions青龙面板进行定时自动签到操作。签到模式默认为随机签到,帮助用户轻松获取论坛每日"鸡腿"奖励。 这是一个用于 NodeSeek 论坛自动签到的工具,支持通过 GitHub Actions青龙面板或 Docker Compose 进行定时自动签到操作。签到模式默认为随机签到,帮助用户轻松获取论坛每日"鸡腿"奖励。
## ✨ 功能特点 ## ✨ 功能特点
- 📅 支持 GitHub Actions 自动运行 - 📅 支持 GitHub Actions 自动运行
- 🦉 支持青龙面板定时任务 - 🦉 支持青龙面板定时任务
- 🐳 支持 Docker Compose 一键部署
- 🍪 支持 Cookie 或账号密码登录方式 - 🍪 支持 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。登录需要通过验证码验证支持以下两种验证码解决方案 当 Cookie 失效时,系统会尝试使用账号密码方式登录并获取新的 Cookie。登录需要通过验证码验证支持以下两种验证码解决方案
@@ -171,6 +225,7 @@ PASS3=密码3
| `USER1``USER2`... | 可选 | - | NodeSeek 论坛用户名,当 Cookie 失效时使用 | | `USER1``USER2`... | 可选 | - | NodeSeek 论坛用户名,当 Cookie 失效时使用 |
| `PASS1``PASS2`... | 可选 | - | NodeSeek 论坛密码 | | `PASS1``PASS2`... | 可选 | - | NodeSeek 论坛密码 |
| `NS_RANDOM` | 可选 | true | 是否随机签到true/false | | `NS_RANDOM` | 可选 | true | 是否随机签到true/false |
| `RUN_AT` | 可选 | `09:00-21:00` | **仅Docker Compose可用**。设置定时任务执行时间,支持固定时间 `10:30` 或时间范围 `10:00-18:00` |
| `SOLVER_TYPE` | 可选 | turnstile | 验证码解决方案turnstile/yescaptcha | | `SOLVER_TYPE` | 可选 | turnstile | 验证码解决方案turnstile/yescaptcha |
| `API_BASE_URL` | 条件必需 | - | CloudFreed 服务地址,当 SOLVER_TYPE=turnstile 时必填 | | `API_BASE_URL` | 条件必需 | - | CloudFreed 服务地址,当 SOLVER_TYPE=turnstile 时必填 |
| `CLIENTT_KEY` | 必需 | - | 验证码服务客户端密钥 | | `CLIENTT_KEY` | 必需 | - | 验证码服务客户端密钥 |
@@ -187,3 +242,45 @@ PASS3=密码3
## ⚠️ 免责声明 ## ⚠️ 免责声明
本项目仅供学习交流使用,请遵守 NodeSeek 论坛的相关规定和条款。 本项目仅供学习交流使用,请遵守 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` 文件中的环境变量,并执行主签到程序,让你可以在本地快速验证签到逻辑是否正常工作。

0
cookie/.gitkeep Normal file
View File

11
docker-compose.yml Normal file
View File

@@ -0,0 +1,11 @@
version: '3.8'
services:
nodeseek-signin:
build: .
environment:
- IN_DOCKER=true
env_file:
- .env
volumes:
- ./cookie:/app/cookie
restart: always

View File

@@ -2,7 +2,6 @@
import os import os
import time import time
import json
from datetime import datetime, timedelta from datetime import datetime, timedelta
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from curl_cffi import requests from curl_cffi import requests
@@ -20,6 +19,10 @@ except ImportError:
# ---------------- 环境检测函数 ---------------- # ---------------- 环境检测函数 ----------------
def detect_environment(): def detect_environment():
"""检测当前运行环境""" """检测当前运行环境"""
# 优先检测是否在 Docker 环境中
if os.environ.get("IN_DOCKER") == "true":
return "docker"
# 检测是否在青龙环境中 # 检测是否在青龙环境中
ql_path_markers = ['/ql/data/', '/ql/config/', '/ql/', '/.ql/'] ql_path_markers = ['/ql/data/', '/ql/config/', '/ql/', '/.ql/']
in_ql_env = False in_ql_env = False
@@ -138,12 +141,31 @@ def save_cookie_to_ql(var_name: str, cookie: str):
print(f"青龙面板环境变量操作异常: {str(e)}") print(f"青龙面板环境变量操作异常: {str(e)}")
return False 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): def save_cookie(var_name: str, cookie: str):
"""根据当前环境保存Cookie到相应位置""" """根据当前环境保存Cookie到相应位置"""
env_type = detect_environment() 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("检测到青龙环境,保存变量到青龙面板...") print("检测到青龙环境,保存变量到青龙面板...")
return save_cookie_to_ql(var_name, cookie) return save_cookie_to_ql(var_name, cookie)
elif env_type == "github": elif env_type == "github":
@@ -393,7 +415,21 @@ if __name__ == "__main__":
break break
# 读取现有Cookie # 读取现有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 = all_cookies.split("&")
cookie_list = [c.strip() for c in cookie_list if c.strip()] cookie_list = [c.strip() for c in cookie_list if c.strip()]

1
requirements-dev.txt Normal file
View File

@@ -0,0 +1 @@
python-dotenv

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
curl_cffi
requests
tzdata

120
scheduler.py Normal file
View File

@@ -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()

14
test_run.py Normal file
View File

@@ -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()