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