Files
qinglong/脚本库/web版/账密/再漫画自动签到/2026-03-02_zaimanhua_d504877a.py
2026-05-24 03:53:42 +00:00

414 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Source: https://github.com/Small-tailqwq/ql_script/blob/master/zaimanhua/zaimanhua.py
# Raw: https://raw.githubusercontent.com/Small-tailqwq/ql_script/master/zaimanhua/zaimanhua.py
# Repo: Small-tailqwq/ql_script
# Path: zaimanhua/zaimanhua.py
# UploadedAt: 2026-03-02T11:07:04Z
# SHA256: d504877ad0b3be012794bec38536cdaeaab15f83fe8bafcee5d8d9121dce5b8d
# Category: web版/账密
# Evidence: web/H5关键词 + username/password/login
# -*- coding: utf-8 -*-
"""
cron: 20 7 * * *
new Env('再漫画自动签到');
"""
from __future__ import annotations
import hashlib
import json
import logging
import os
from dataclasses import dataclass
from typing import Any, Dict, Iterable, List, Optional, Tuple
import requests
try:
from sendNotify import send # type: ignore
except Exception: # noqa: BLE001
try:
from notify import send # type: ignore
except Exception:
send = None
API_BASE = "https://i.zaimanhua.com/lpi/v1"
DEFAULT_TIMEOUT = 15
DEBUG_ENABLED = os.getenv("ZAIMANHUA_DEBUG", "0").strip().lower() in {"1", "true", "on", "yes"}
def debug_dump(label: str, payload: Any) -> None:
if not DEBUG_ENABLED:
return
try:
fragment = json.dumps(payload, ensure_ascii=False)[:1500]
except Exception: # noqa: BLE001
fragment = str(payload)[:1500]
logging.debug("%s: %s", label, fragment)
@dataclass
class SignResult:
account: str
success: bool
message: str
class ZaiManHua:
name = "再漫画"
def __init__(self, username: str, password: str, alias: Optional[str] = None) -> None:
self.username = username.strip()
self.password = password.strip()
self.alias = alias.strip() if alias else ""
self.session = self._build_session()
@staticmethod
def _build_session() -> requests.Session:
session = requests.Session()
session.headers.update(
{
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0 Safari/537.36"
),
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9",
"Origin": "https://m.zaimanhua.com",
"Referer": "https://m.zaimanhua.com/",
}
)
return session
@staticmethod
def _md5(text: str) -> str:
return hashlib.md5(text.encode("utf-8")).hexdigest()
def _request_json(
self,
method: str,
endpoint: str,
*,
params: Optional[Dict[str, Any]] = None,
json_data: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, str]] = None,
) -> Dict[str, Any]:
url = endpoint if endpoint.startswith("http") else f"{API_BASE}/{endpoint.lstrip('/') }"
if DEBUG_ENABLED:
logging.debug(
"请求 %s %s params=%s json=%s headers=%s",
method,
url,
params,
json_data,
headers,
)
response = self.session.request(
method,
url,
params=params,
json=json_data,
headers=headers,
timeout=DEFAULT_TIMEOUT,
)
response.raise_for_status()
data = response.json()
if not isinstance(data, dict): # pragma: no cover - 防御
raise ValueError("接口返回数据格式异常")
debug_dump(f"响应 {method} {url}", data)
return data
@staticmethod
def _ensure_success(payload: Dict[str, Any], allowed_errno: Iterable[int] = (0,)) -> Tuple[int, str]:
errno = int(payload.get("errno", -1))
errmsg = str(payload.get("errmsg", ""))
if errno not in allowed_errno:
raise RuntimeError(f"接口返回错误 errno={errno}, errmsg={errmsg}")
return errno, errmsg
@staticmethod
def _extract_token(payload: Dict[str, Any]) -> Optional[str]:
def _search(node: Any) -> Optional[str]:
if isinstance(node, dict):
for key, value in node.items():
key_lower = str(key).lower()
if key_lower in {"token", "access_token"} and isinstance(value, str) and value:
return value
if isinstance(value, (dict, list)):
found = _search(value)
if found:
return found
elif isinstance(node, list):
for item in node:
found = _search(item)
if found:
return found
return None
return _search(payload)
@staticmethod
def _find_first(node: Any, target_keys: Iterable[str]) -> Optional[Any]:
target = {str(key).lower() for key in target_keys}
def _search(current: Any) -> Optional[Any]:
if isinstance(current, dict):
for key, value in current.items():
if str(key).lower() in target and value is not None:
return value
if isinstance(value, (dict, list)):
found = _search(value)
if found is not None:
return found
elif isinstance(current, list):
for item in current:
found = _search(item)
if found is not None:
return found
return None
return _search(node)
def _login(self) -> str:
payload = self._request_json(
"POST",
"login/passwd",
params={"username": self.username, "passwd": self._md5(self.password)},
)
self._ensure_success(payload)
token: Optional[str] = None
token_source = "未知"
data = payload.get("data")
if isinstance(data, dict):
token = self._extract_token(data)
if token:
token_source = "data"
if not token:
token = self._extract_token(payload)
if token:
token_source = "payload"
if not token and self.session.cookies:
token = self.session.cookies.get("token") or self.session.cookies.get("Authorization")
if token:
token_source = "cookies"
if not token:
snippet = str(payload)[:300]
raise RuntimeError(f"登录成功但未返回 token响应片段{snippet}")
logging.debug("登录成功token 来源=%s", token_source)
return str(token)
def _sign_in(self, token: str) -> str:
payload = self._request_json(
"POST",
"task/sign_in",
headers={"Authorization": f"Bearer {token}"},
)
_, errmsg = self._ensure_success(payload, allowed_errno=(0, 1))
return errmsg or "签到成功"
def _fetch_user_info(self, token: str) -> Tuple[str, Optional[int]]:
payload = self._request_json(
"POST",
"u_center/passport/message",
headers={"Authorization": f"Bearer {token}"},
)
self._ensure_success(payload)
data = payload.get("data")
debug_dump("用户信息 payload", payload)
if not isinstance(data, dict):
raise RuntimeError("用户信息数据格式异常")
user_info = data.get("userInfo") or data.get("user") or data
if not isinstance(user_info, dict):
user_info = {}
nickname = str(
user_info.get("nickname")
or user_info.get("username")
or data.get("nickname")
or ""
)
level_value = user_info.get("user_level") or data.get("user_level")
if level_value is None:
level_value = self._find_first(data, {"user_level", "level"})
try:
level = int(level_value) if level_value is not None else None
except (TypeError, ValueError):
level = None
return nickname, level
def _fetch_task_info(self, token: str) -> Tuple[Optional[int], Optional[int], Optional[int]]:
payload = self._request_json(
"GET",
"task/list",
headers={"Authorization": f"Bearer {token}"},
)
self._ensure_success(payload)
data = payload.get("data")
debug_dump("任务列表 payload", payload)
if not isinstance(data, dict):
raise RuntimeError("任务列表数据格式异常")
user_currency = data.get("userCurrency")
if not isinstance(user_currency, dict):
task_section = data.get("task")
if isinstance(task_section, dict):
user_currency = task_section.get("userCurrency")
credits = self._safe_int(user_currency, "credits") if isinstance(user_currency, dict) else None
if credits is None:
found = self._find_first(data, {"credits"})
try:
credits = int(found) if found is not None else None
except (TypeError, ValueError):
credits = None
sum_sign_task = (
data.get("sumSignTask")
or data.get("signTask")
or (data.get("task") or {}).get("sumSignTask")
or (data.get("task") or {}).get("signTask")
or {}
)
continuous_days = self._safe_int(sum_sign_task, "continuousSignDays")
history_days = self._safe_int(sum_sign_task, "sumSignDays")
if continuous_days is None:
value = self._find_first(data, {"continuousSignDays", "continuous_days"})
try:
continuous_days = int(value) if value is not None else None
except (TypeError, ValueError):
continuous_days = None
if history_days is None:
value = self._find_first(data, {"sumSignDays", "totalSignDays", "history_days"})
try:
history_days = int(value) if value is not None else None
except (TypeError, ValueError):
history_days = None
return credits, continuous_days, history_days
@staticmethod
def _safe_int(source: Any, key: str) -> Optional[int]:
value: Any = None
if isinstance(source, dict):
value = source.get(key)
try:
return int(value) if value is not None else None
except (TypeError, ValueError):
return None
@staticmethod
def _mask(text: str) -> str:
text = text or ""
if len(text) <= 2:
return f"{text[:1]}*" if text else "未知"
return f"{text[0]}***{text[-1]}"
def main(self) -> SignResult:
if not self.username or not self.password:
raise RuntimeError("账号或密码未配置")
token = self._login()
sign_msg = self._sign_in(token)
nickname, level = self._fetch_user_info(token)
credits, continuous_days, history_days = self._fetch_task_info(token)
display_name = nickname or self.alias or self._mask(self.username)
level_info = f"LV{level}" if level is not None else "未知"
credit_info = str(credits) if credits is not None else "未知"
continuous_info = f"{continuous_days}" if continuous_days is not None else "未知"
history_info = f"{history_days}" if history_days is not None else "未知"
lines = [
f"签到状态:{sign_msg}",
f"用户等级:{level_info}",
f"当前积分:{credit_info}",
f"连续签到:{continuous_info}",
f"历史签到:{history_info}",
]
return SignResult(
account=display_name,
success=True,
message="\n".join(lines),
)
def load_accounts() -> List[ZaiManHua]:
accounts: List[ZaiManHua] = []
raw_accounts = os.getenv("ZAIMANHUA_ACCOUNTS", "").strip()
if raw_accounts:
for line in raw_accounts.splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
parts = line.split("#", 2)
if len(parts) < 2:
logging.warning("无效的账号配置行:%s,格式应为 用户名#密码[#别名]", line)
continue
username, password, *rest = parts
alias = rest[0] if rest else None
accounts.append(ZaiManHua(username=username, password=password, alias=alias))
username = os.getenv("ZAIMANHUA_USERNAME", "").strip()
password = os.getenv("ZAIMANHUA_PASSWORD", "").strip()
alias = os.getenv("ZAIMANHUA_ALIAS", "").strip()
if not accounts and username and password:
accounts.append(ZaiManHua(username=username, password=password, alias=alias))
return accounts
def format_report(results: List[SignResult]) -> str:
chunks = []
for item in results:
status = "✅ 成功" if item.success else "❌ 失败"
chunks.append(f"账号:{item.account} | {status}\n{item.message}")
return "\n\n".join(chunks)
def main() -> None:
logging.basicConfig(
level=logging.DEBUG if DEBUG_ENABLED else logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
)
if DEBUG_ENABLED:
logging.debug("调试模式已开启 (ZAIMANHUA_DEBUG=1)")
accounts = load_accounts()
if DEBUG_ENABLED and accounts:
masked_accounts = [
{
"username": ZaiManHua._mask(acc.username),
"alias": acc.alias,
}
for acc in accounts
]
debug_dump("已加载账号", masked_accounts)
if not accounts:
logging.error("未配置再漫画账号信息,需设置 ZAIMANHUA_ACCOUNTS 或 ZAIMANHUA_USERNAME/ZAIMANHUA_PASSWORD。")
if send:
send("再漫画签到", "未配置账号信息,任务未执行。")
return
results: List[SignResult] = []
for index, account in enumerate(accounts, start=1):
try:
logging.info("[%s] 正在执行账号 %s", index, account.alias or account.username)
results.append(account.main())
except Exception as exc: # noqa: BLE001
logging.exception("[%s] 账号 %s 执行失败:%s", index, account.alias or account.username, exc)
results.append(
SignResult(
account=account.alias or account.username,
success=False,
message=str(exc),
)
)
report = format_report(results)
logging.info("\n%s", report)
if send:
send("再漫画签到", report)
if __name__ == "__main__":
main()