Compare commits

...

10 Commits

Author SHA1 Message Date
yowiv
dac0b5ee66 Update README.md 2025-09-29 19:14:46 +08:00
yowiv
8ff20b8f85 Merge pull request #42 from hkfires/main
增加 Docker 部署方式
2025-08-03 11:42:18 +08:00
hkfires
166337713d fix(docker): 修复 Docker 环境中的日志缓冲和证书问题 2025-08-03 11:31:08 +08:00
hkfires
9df3e7c8ec feat(docker): 增加 Docker 支持以实现自动化定时任务
新增通过 `docker-compose` 进行部署和运行的方式,实现了本地自动化定时签到。

主要更新包括:
- 引入 `scheduler.py` 脚本,根据 `RUN_AT` 环境变量管理定时任务,支持固定时间或随机时间范围。
- 在 Docker 环境中,Cookie 将被持久化到 `./cookie` 卷中,以在容器重启后保持登录状态。
- 新增了本地开发和测试流程,通过 `test_run.py` 和 `.env` 文件简化了调试。
- 更新了 `README.md` 文档,包含详细的 Docker 部署和本地开发指南。
2025-08-03 11:05:52 +08:00
yowiv
e5edbf2813 Merge pull request #39 from du-mozzie/patch-1
通知变量配置说明
2025-07-18 09:00:51 +08:00
mozzie
65eb0e37f1 Update README.md
docs(blank.yml): 通知变量配置说明
2025-07-18 08:58:50 +08:00
yowiv
a360ec2053 Merge pull request #35 from HChaZZY/main
feat: 改进签到统计功能,支持自定义查询天数
2025-07-09 15:16:18 +08:00
f2bc001c74 fix: Prevent invalid value for days in get_signin_stats 2025-07-09 14:55:47 +08:00
cc84684db1 fix(timezone): Use zoneinfo for accurate timezone conversion
Replaced manual UTC+8 offset calculation with the `zoneinfo` library to ensure correct handling of timezone-aware datetimes, particularly for the Shanghai timezone.
2025-07-09 14:45:38 +08:00
4bb08f2ccf feat: Make sign-in stats query time-range flexible
Refactors `get_signin_stats` to query reward statistics over a customizable number of days, instead of being fixed to the current month. Defaults to 30 days.
2025-07-09 14:38:44 +08:00
12 changed files with 419 additions and 31 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 ca-certificates
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"]

106
README.md
View File

@@ -9,21 +9,25 @@
</div>
[Deepflood论坛签到](https://github.com/yowiv/deepflood-Signin)
## 📝 项目介绍
这是一个用于 NodeSeek 论坛自动签到的工具,支持通过 GitHub Actions青龙面板进行定时自动签到操作。签到模式默认为随机签到,帮助用户轻松获取论坛每日"鸡腿"奖励。
这是一个用于 NodeSeek 论坛自动签到的工具,支持通过 GitHub Actions青龙面板或 Docker Compose 进行定时自动签到操作。签到模式默认为随机签到,帮助用户轻松获取论坛每日"鸡腿"奖励。
## ✨ 功能特点
- 📅 支持 GitHub Actions 自动运行
- 🦉 支持青龙面板定时任务
- 🐳 支持 Docker Compose 一键部署
- 🍪 支持 Cookie 或账号密码登录方式
- 👥 支持多账号批量签到
- 🔐 支持多种验证码解决方案
- 自建 CloudFreed 服务(免费)
- YesCaptcha 商业服务(付费/赠送)
- 📱 支持多种通知推送渠道
- 📱 支持多种通知推送渠道(需在blank.yml添加对应变量)
- 🔄 自动更新Cookie并保存至GitHub变量
## 🚀 使用方法
@@ -55,7 +59,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 +228,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 +245,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` 文件中的环境变量,并执行主签到程序,让你可以在本地快速验证签到逻辑是否正常工作。

0
cookie/.gitkeep Normal file
View File

13
docker-compose.yml Normal file
View File

@@ -0,0 +1,13 @@
services:
nodeseek-signin:
build: .
image: nodeseek-signin:latest
container_name: nodeseek-signin
command: ["python", "scheduler.py"]
environment:
- IN_DOCKER=true
env_file:
- .env
volumes:
- ./cookie:/app/cookie
restart: always

View File

@@ -2,8 +2,8 @@
import os
import time
import json
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from curl_cffi import requests
from yescaptcha import YesCaptchaSolver, YesCaptchaSolverError
from turnstile_solver import TurnstileSolver, TurnstileSolverError
@@ -19,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
@@ -137,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":
@@ -245,10 +268,13 @@ def sign(ns_cookie, ns_random):
# ---------------- 查询签到收益统计函数 ----------------
def get_signin_stats(ns_cookie, days=30):
"""查询本月的签到收益统计"""
"""查询前days天内的签到收益统计"""
if not ns_cookie:
return None, "无有效Cookie"
if days <= 0:
days = 1
headers = {
'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0",
'origin': "https://www.nodeseek.com",
@@ -258,16 +284,17 @@ def get_signin_stats(ns_cookie, days=30):
try:
# 使用UTC+8时区上海时区
utc_offset = timedelta(hours=8)
now_utc = datetime.utcnow()
now_shanghai = now_utc + utc_offset
current_month_start = datetime(now_shanghai.year, now_shanghai.month, 1)
shanghai_tz = ZoneInfo("Asia/Shanghai")
now_shanghai = datetime.now(shanghai_tz)
# 获取多页数据以确保覆盖本月所有数据
# 计算查询开始时间:当前时间减去指定天数
query_start_time = now_shanghai - timedelta(days=days)
# 获取多页数据以确保覆盖指定天数内的所有数据
all_records = []
page = 1
while page <= 10: # 最多查询10页防止无限循环
while page <= 20: # 最多查询20页防止无限循环
url = f"https://www.nodeseek.com/api/account/credit/page-{page}"
response = requests.get(url, headers=headers, impersonate="chrome110")
data = response.json()
@@ -279,15 +306,17 @@ def get_signin_stats(ns_cookie, days=30):
if not records:
break
# 检查最后一条记录的时间,如果超出本月范围就停止
last_record_time = datetime.fromisoformat(records[-1][3].replace('Z', '+00:00'))
last_record_time_shanghai = last_record_time.replace(tzinfo=None) + utc_offset
if last_record_time_shanghai < current_month_start:
# 只添加在本月范围内的记录
# 检查最后一条记录的时间,如果超出查询范围就停止
last_record_time = datetime.fromisoformat(
records[-1][3].replace('Z', '+00:00'))
last_record_time_shanghai = last_record_time.astimezone(shanghai_tz)
if last_record_time_shanghai < query_start_time:
# 只添加在查询范围内的记录
for record in records:
record_time = datetime.fromisoformat(record[3].replace('Z', '+00:00'))
record_time_shanghai = record_time.replace(tzinfo=None) + utc_offset
if record_time_shanghai >= current_month_start:
record_time = datetime.fromisoformat(
record[3].replace('Z', '+00:00'))
record_time_shanghai = record_time.astimezone(shanghai_tz)
if record_time_shanghai >= query_start_time:
all_records.append(record)
break
else:
@@ -296,30 +325,36 @@ def get_signin_stats(ns_cookie, days=30):
page += 1
time.sleep(0.5)
# 筛选本月签到收益记录
# 筛选指定天数内的签到收益记录
signin_records = []
for record in all_records:
amount, balance, description, timestamp = record
record_time = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
record_time_shanghai = record_time.replace(tzinfo=None) + utc_offset
record_time = datetime.fromisoformat(
timestamp.replace('Z', '+00:00'))
record_time_shanghai = record_time.astimezone(shanghai_tz)
# 只统计本月的签到收益
if (record_time_shanghai >= current_month_start and
"签到收益" in description and "鸡腿" in description):
# 只统计指定天数内的签到收益
if (record_time_shanghai >= query_start_time and
"签到收益" in description and "鸡腿" in description):
signin_records.append({
'amount': amount,
'date': record_time_shanghai.strftime('%Y-%m-%d'),
'description': description
})
# 生成时间范围描述
period_desc = f"{days}"
if days == 1:
period_desc = "今天"
if not signin_records:
return {
'total_amount': 0,
'average': 0,
'days_count': 0,
'records': [],
'period': f"{now_shanghai.strftime('%Y年%m月')}"
}, "查询成功,但没有找到本月签到记录"
'period': period_desc,
}, f"查询成功,但没有找到{period_desc}签到记录"
# 统计数据
total_amount = sum(record['amount'] for record in signin_records)
@@ -331,7 +366,7 @@ def get_signin_stats(ns_cookie, days=30):
'average': average,
'days_count': days_count,
'records': signin_records,
'period': f"{now_shanghai.strftime('%Y年%m月')}"
'period': period_desc
}
return stats, "查询成功"
@@ -380,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()]

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

125
scheduler.py Normal file
View File

@@ -0,0 +1,125 @@
import os
import sys
import time
import datetime
import random
import subprocess
import re
from datetime import timezone, timedelta
# 测试程序时使用
# from dotenv import load_dotenv
# load_dotenv()
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}", flush=True)
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}", flush=True)
return 'range', run_at_env
if os.environ.get('RUN_AT'):
print(f"警告: 环境变量 RUN_AT 的格式 '{run_at_env}' 无效。", flush=True)
print("将使用默认随机时间范围 '08:00-10:59'", flush=True)
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')}] 开始执行签到任务...", flush=True)
try:
subprocess.run([sys.executable, "nodeseek_sign.py"], check=True)
print(f"[{datetime.datetime.now(GMT8).strftime('%Y-%m-%d %H:%M:%S')}] 签到任务执行完毕。", flush=True)
except FileNotFoundError:
print("错误: 'nodeseek_sign.py' 未找到。请确保它与 scheduler.py 位于同一目录。", flush=True)
except subprocess.CalledProcessError as e:
print(f"签到任务执行失败,返回码: {e.returncode}", flush=True)
except Exception as e:
print(f"执行签到任务时发生未知错误: {e}", flush=True)
def main():
"""
主调度循环。
"""
print("调度器启动...", flush=True)
mode, value = get_run_config()
print(f"调度模式: '{mode}', 配置值: '{value}'", flush=True)
# 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')}", flush=True)
hours, remainder = divmod(sleep_duration, 3600)
minutes, _ = divmod(remainder, 60)
print(f"程序将休眠 {int(hours)} 小时 {int(minutes)} 分钟。", flush=True)
time.sleep(sleep_duration)
else:
print("计算出的下一个运行时间已过,等待 60 秒后重试...", flush=True)
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()