执行 Ruff 安全自动修复
This commit is contained in:
+10
-10
@@ -1,11 +1,11 @@
|
|||||||
"""斗鱼登录模块"""
|
"""斗鱼登录模块"""
|
||||||
|
|
||||||
from .login import DouyuLogin, CredentialError
|
|
||||||
from .login_api import LoginAPIStrategy
|
|
||||||
from .login_api_wgapi import WgapiLoginAPI
|
|
||||||
from .login_api_iframe import IframeLoginAPI
|
|
||||||
from .email_verifier import EmailVerifier
|
|
||||||
from .activity_client import DouyuActivityClient, DouyuActivityError
|
from .activity_client import DouyuActivityClient, DouyuActivityError
|
||||||
|
from .email_verifier import EmailVerifier
|
||||||
|
from .login import CredentialError, DouyuLogin
|
||||||
|
from .login_api import LoginAPIStrategy
|
||||||
|
from .login_api_iframe import IframeLoginAPI
|
||||||
|
from .login_api_wgapi import WgapiLoginAPI
|
||||||
from .recharge_api import (
|
from .recharge_api import (
|
||||||
FishFinRechargeClient,
|
FishFinRechargeClient,
|
||||||
FishFinRechargeConfig,
|
FishFinRechargeConfig,
|
||||||
@@ -14,16 +14,16 @@ from .recharge_api import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"DouyuLogin",
|
|
||||||
"CredentialError",
|
"CredentialError",
|
||||||
"LoginAPIStrategy",
|
|
||||||
"WgapiLoginAPI",
|
|
||||||
"IframeLoginAPI",
|
|
||||||
"EmailVerifier",
|
|
||||||
"DouyuActivityClient",
|
"DouyuActivityClient",
|
||||||
"DouyuActivityError",
|
"DouyuActivityError",
|
||||||
|
"DouyuLogin",
|
||||||
|
"EmailVerifier",
|
||||||
"FishFinRechargeClient",
|
"FishFinRechargeClient",
|
||||||
"FishFinRechargeConfig",
|
"FishFinRechargeConfig",
|
||||||
"FishFinRechargeConfigError",
|
"FishFinRechargeConfigError",
|
||||||
"FishFinRechargeError",
|
"FishFinRechargeError",
|
||||||
|
"IframeLoginAPI",
|
||||||
|
"LoginAPIStrategy",
|
||||||
|
"WgapiLoginAPI",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ from .cookie_utils import (
|
|||||||
normalize_douyu_cookie,
|
normalize_douyu_cookie,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
PC_UA = (
|
PC_UA = (
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||||
@@ -1446,8 +1445,7 @@ class DouyuActivityClient:
|
|||||||
if not match:
|
if not match:
|
||||||
raise DouyuActivityError(f"响应中找不到 var {varname}=: {text[:200]}")
|
raise DouyuActivityError(f"响应中找不到 var {varname}=: {text[:200]}")
|
||||||
chunk = text[match.end() :].strip()
|
chunk = text[match.end() :].strip()
|
||||||
if chunk.endswith(";"):
|
chunk = chunk.removesuffix(";")
|
||||||
chunk = chunk[:-1]
|
|
||||||
try:
|
try:
|
||||||
return json.loads(chunk)
|
return json.loads(chunk)
|
||||||
except json.JSONDecodeError as exc:
|
except json.JSONDecodeError as exc:
|
||||||
|
|||||||
@@ -2,11 +2,9 @@
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
from Crypto.PublicKey import RSA
|
|
||||||
from Crypto.Cipher import PKCS1_v1_5
|
|
||||||
from Crypto.Cipher import AES
|
|
||||||
from Crypto.Cipher import ARC4
|
|
||||||
|
|
||||||
|
from Crypto.Cipher import AES, ARC4, PKCS1_v1_5
|
||||||
|
from Crypto.PublicKey import RSA
|
||||||
|
|
||||||
# 斗鱼RSA公钥(从JS中提取)
|
# 斗鱼RSA公钥(从JS中提取)
|
||||||
DOUYU_RSA_PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
|
DOUYU_RSA_PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
|
||||||
|
|||||||
@@ -6,11 +6,9 @@ import re
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
import requests
|
import requests
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
# Roundcube Webmail 地址。可通过环境变量覆盖。空值时回退到默认。
|
# Roundcube Webmail 地址。可通过环境变量覆盖。空值时回退到默认。
|
||||||
ROUNDCUBE_URL = os.getenv("MAIL_ROUNDCUBE_URL") or "http://111.229.206.54:8000/"
|
ROUNDCUBE_URL = os.getenv("MAIL_ROUNDCUBE_URL") or "http://111.229.206.54:8000/"
|
||||||
@@ -40,7 +38,7 @@ class EmailVerifier:
|
|||||||
max_messages: int = 20,
|
max_messages: int = 20,
|
||||||
use_ssl: bool = False,
|
use_ssl: bool = False,
|
||||||
roundcube_url: str = "",
|
roundcube_url: str = "",
|
||||||
backup_passwords: Optional[list[str]] = None,
|
backup_passwords: list[str] | None = None,
|
||||||
):
|
):
|
||||||
# 兼容旧参数名
|
# 兼容旧参数名
|
||||||
self.imap_server = imap_server
|
self.imap_server = imap_server
|
||||||
@@ -54,14 +52,13 @@ class EmailVerifier:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Roundcube 会话(懒初始化)
|
# Roundcube 会话(懒初始化)
|
||||||
self._rc_session: Optional[requests.Session] = None
|
self._rc_session: requests.Session | None = None
|
||||||
self._rc_token: Optional[str] = None
|
self._rc_token: str | None = None
|
||||||
self._rc_logged_in = False
|
self._rc_logged_in = False
|
||||||
self._rc_login_error = ""
|
self._rc_login_error = ""
|
||||||
|
|
||||||
def connect(self) -> None:
|
def connect(self) -> None:
|
||||||
"""兼容接口"""
|
"""兼容接口"""
|
||||||
pass
|
|
||||||
|
|
||||||
def disconnect(self) -> None:
|
def disconnect(self) -> None:
|
||||||
"""关闭 Roundcube 会话"""
|
"""关闭 Roundcube 会话"""
|
||||||
@@ -239,7 +236,7 @@ class EmailVerifier:
|
|||||||
|
|
||||||
# ── Roundcube 读取邮件内容 ─────────────────────────────
|
# ── Roundcube 读取邮件内容 ─────────────────────────────
|
||||||
|
|
||||||
def _rc_fetch_email_body(self, uid: int) -> Optional[str]:
|
def _rc_fetch_email_body(self, uid: int) -> str | None:
|
||||||
"""通过 Roundcube 读取指定 UID 邮件的正文"""
|
"""通过 Roundcube 读取指定 UID 邮件的正文"""
|
||||||
if not self._ensure_roundcube_session():
|
if not self._ensure_roundcube_session():
|
||||||
if self._rc_login_error:
|
if self._rc_login_error:
|
||||||
@@ -262,7 +259,7 @@ class EmailVerifier:
|
|||||||
# ── Roundcube 日期解析 ─────────────────────────────────
|
# ── Roundcube 日期解析 ─────────────────────────────────
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_rc_date(date_str: str) -> Optional[datetime]:
|
def _parse_rc_date(date_str: str) -> datetime | None:
|
||||||
"""
|
"""
|
||||||
解析 Roundcube 返回的日期字符串为 datetime。
|
解析 Roundcube 返回的日期字符串为 datetime。
|
||||||
|
|
||||||
@@ -363,9 +360,9 @@ class EmailVerifier:
|
|||||||
self,
|
self,
|
||||||
max_wait: int = 60,
|
max_wait: int = 60,
|
||||||
interval: int = 3,
|
interval: int = 3,
|
||||||
after_timestamp: Optional[float] = None,
|
after_timestamp: float | None = None,
|
||||||
allow_old_seconds: int = 15,
|
allow_old_seconds: int = 15,
|
||||||
stop_event: Optional[threading.Event] = None,
|
stop_event: threading.Event | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
轮询获取斗鱼验证码。
|
轮询获取斗鱼验证码。
|
||||||
@@ -417,9 +414,9 @@ class EmailVerifier:
|
|||||||
|
|
||||||
def _fetch_code_via_roundcube(
|
def _fetch_code_via_roundcube(
|
||||||
self,
|
self,
|
||||||
after_timestamp: Optional[float] = None,
|
after_timestamp: float | None = None,
|
||||||
allow_old_seconds: int = 15,
|
allow_old_seconds: int = 15,
|
||||||
) -> Optional[str]:
|
) -> str | None:
|
||||||
"""通过 Roundcube API 获取最新斗鱼验证码"""
|
"""通过 Roundcube API 获取最新斗鱼验证码"""
|
||||||
messages = self._rc_fetch_mail_list()
|
messages = self._rc_fetch_mail_list()
|
||||||
|
|
||||||
@@ -465,7 +462,7 @@ class EmailVerifier:
|
|||||||
|
|
||||||
# ── 通用工具方法 ───────────────────────────────────────
|
# ── 通用工具方法 ───────────────────────────────────────
|
||||||
|
|
||||||
def _extract_verification_code(self, text: str) -> Optional[str]:
|
def _extract_verification_code(self, text: str) -> str | None:
|
||||||
"""从文本中提取6位验证码"""
|
"""从文本中提取6位验证码"""
|
||||||
# 清理HTML标签和实体
|
# 清理HTML标签和实体
|
||||||
text = html.unescape(text)
|
text = html.unescape(text)
|
||||||
|
|||||||
+26
-24
@@ -4,28 +4,30 @@ import json
|
|||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import requests
|
from collections.abc import Mapping
|
||||||
from typing import Mapping, Optional, Protocol, Tuple
|
from typing import Protocol
|
||||||
from urllib.parse import urlsplit, urlunsplit
|
from urllib.parse import urlsplit, urlunsplit
|
||||||
|
|
||||||
|
import requests
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from core.geetest.common.network import (
|
||||||
|
get_c_s,
|
||||||
|
get_js_address,
|
||||||
|
req_fullpage_validate,
|
||||||
|
)
|
||||||
|
from core.geetest.v3_slide.solver import (
|
||||||
|
_generate_seed,
|
||||||
|
get_w1,
|
||||||
|
get_w2,
|
||||||
|
)
|
||||||
|
|
||||||
from .cookie_enricher import CookieEnricher
|
from .cookie_enricher import CookieEnricher
|
||||||
from .email_verifier import EmailLoginError, EmailVerifier
|
from .email_verifier import EmailLoginError, EmailVerifier
|
||||||
from .login_api import LoginAPIStrategy
|
from .login_api import LoginAPIStrategy
|
||||||
from .login_api_wgapi import WgapiLoginAPI
|
from .login_api_wgapi import WgapiLoginAPI
|
||||||
from .proxy_fetcher import ProxyFetcher
|
from .proxy_fetcher import ProxyFetcher
|
||||||
|
|
||||||
from core.geetest.v3_slide.solver import (
|
|
||||||
_generate_seed,
|
|
||||||
get_w1,
|
|
||||||
get_w2,
|
|
||||||
)
|
|
||||||
from core.geetest.common.network import (
|
|
||||||
get_js_address,
|
|
||||||
get_c_s,
|
|
||||||
req_fullpage_validate,
|
|
||||||
)
|
|
||||||
|
|
||||||
# ── 全局极验并发限制:同一时刻最多2个线程做极验验证 ──
|
# ── 全局极验并发限制:同一时刻最多2个线程做极验验证 ──
|
||||||
_geetest_semaphore = threading.Semaphore(2)
|
_geetest_semaphore = threading.Semaphore(2)
|
||||||
|
|
||||||
@@ -95,14 +97,14 @@ class DouyuLogin:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
account: AccountLike,
|
account: AccountLike,
|
||||||
proxy: Optional[str | Mapping[str, str]] = None,
|
proxy: str | Mapping[str, str] | None = None,
|
||||||
proxy_api_url: Optional[str] = None,
|
proxy_api_url: str | None = None,
|
||||||
timeout: tuple[float, float] = REQUEST_TIMEOUT,
|
timeout: tuple[float, float] = REQUEST_TIMEOUT,
|
||||||
max_login_retries: int = 0,
|
max_login_retries: int = 0,
|
||||||
max_total_time: float = 0,
|
max_total_time: float = 0,
|
||||||
proxy_fetcher: Optional[ProxyFetcher] = None,
|
proxy_fetcher: ProxyFetcher | None = None,
|
||||||
stop_event: Optional[threading.Event] = None,
|
stop_event: threading.Event | None = None,
|
||||||
api_strategy: Optional[LoginAPIStrategy] = None,
|
api_strategy: LoginAPIStrategy | None = None,
|
||||||
):
|
):
|
||||||
self.account = account
|
self.account = account
|
||||||
self.proxy = proxy
|
self.proxy = proxy
|
||||||
@@ -117,7 +119,7 @@ class DouyuLogin:
|
|||||||
ProxyFetcher(api_url=proxy_api_url) if proxy_api_url else None
|
ProxyFetcher(api_url=proxy_api_url) if proxy_api_url else None
|
||||||
)
|
)
|
||||||
|
|
||||||
self._current_proxy_url: Optional[str] = None
|
self._current_proxy_url: str | None = None
|
||||||
self._cookie_enrich_error = ""
|
self._cookie_enrich_error = ""
|
||||||
self._static_retry_count = 0
|
self._static_retry_count = 0
|
||||||
self._setup_session()
|
self._setup_session()
|
||||||
@@ -565,7 +567,7 @@ class DouyuLogin:
|
|||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _first_login(self) -> Tuple[str, str, str, dict]:
|
def _first_login(self) -> tuple[str, str, str, dict]:
|
||||||
"""
|
"""
|
||||||
第一次登录,获取极验参数
|
第一次登录,获取极验参数
|
||||||
|
|
||||||
@@ -608,7 +610,7 @@ class DouyuLogin:
|
|||||||
|
|
||||||
def _solve_geetest(
|
def _solve_geetest(
|
||||||
self, gt: str, challenge: str, deadline: float = 0
|
self, gt: str, challenge: str, deadline: float = 0
|
||||||
) -> Tuple[str, str]:
|
) -> tuple[str, str]:
|
||||||
"""
|
"""
|
||||||
解决极验 fullpage 验证(最多3次尝试,失败直接抛异常回到login换新代理)
|
解决极验 fullpage 验证(最多3次尝试,失败直接抛异常回到login换新代理)
|
||||||
|
|
||||||
@@ -635,7 +637,7 @@ class DouyuLogin:
|
|||||||
|
|
||||||
def _solve_geetest_inner(
|
def _solve_geetest_inner(
|
||||||
self, gt: str, challenge: str, deadline: float = 0
|
self, gt: str, challenge: str, deadline: float = 0
|
||||||
) -> Tuple[str, str]:
|
) -> tuple[str, str]:
|
||||||
"""极验验证内部实现:最多3次尝试,失败直接抛异常。"""
|
"""极验验证内部实现:最多3次尝试,失败直接抛异常。"""
|
||||||
_MAX_ATTEMPTS = 3
|
_MAX_ATTEMPTS = 3
|
||||||
|
|
||||||
@@ -652,7 +654,7 @@ class DouyuLogin:
|
|||||||
|
|
||||||
# 超时兜底
|
# 超时兜底
|
||||||
if deadline and time.monotonic() > deadline:
|
if deadline and time.monotonic() > deadline:
|
||||||
raise ValueError(f"极验验证超时(登录整体时间耗尽)")
|
raise ValueError("极验验证超时(登录整体时间耗尽)")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info(f"极验验证尝试 {attempt}/{_MAX_ATTEMPTS}")
|
logger.info(f"极验验证尝试 {attempt}/{_MAX_ATTEMPTS}")
|
||||||
@@ -825,7 +827,7 @@ class DouyuLogin:
|
|||||||
|
|
||||||
logger.info("验证邮件已发送")
|
logger.info("验证邮件已发送")
|
||||||
|
|
||||||
def _get_email_code(self, after_timestamp: Optional[float] = None) -> str:
|
def _get_email_code(self, after_timestamp: float | None = None) -> str:
|
||||||
"""获取邮箱验证码"""
|
"""获取邮箱验证码"""
|
||||||
verifier = EmailVerifier(
|
verifier = EmailVerifier(
|
||||||
imap_server=self.account.email_imap_server,
|
imap_server=self.account.email_imap_server,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"""登录接口策略基类"""
|
"""登录接口策略基类"""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
|
|
||||||
class LoginAPIStrategy(ABC):
|
class LoginAPIStrategy(ABC):
|
||||||
@@ -78,7 +77,7 @@ class LoginAPIStrategy(ABC):
|
|||||||
"""构建提交验证码参数"""
|
"""构建提交验证码参数"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def extract_geetest_params(self, payload: dict) -> Tuple[str, str, str]:
|
def extract_geetest_params(self, payload: dict) -> tuple[str, str, str]:
|
||||||
"""从第一次登录响应提取极验参数
|
"""从第一次登录响应提取极验参数
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from .proxy_resolver import ProxyResolver
|
from .proxy_resolver import ProxyResolver
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
def parse_proxy_response(text: str) -> tuple[list[str], Optional[str]]:
|
def parse_proxy_response(text: str) -> tuple[list[str], str | None]:
|
||||||
"""
|
"""
|
||||||
解析代理 API 响应,支持 JSON 格式与旧版纯文本格式。
|
解析代理 API 响应,支持 JSON 格式与旧版纯文本格式。
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from .xiequ import XiequAdapter
|
|||||||
from .xkdaili import XkdailiAdapter
|
from .xkdaili import XkdailiAdapter
|
||||||
|
|
||||||
# ── 平台注册表 ──
|
# ── 平台注册表 ──
|
||||||
_PLATFORM_REGISTRY: dict[str, Type[BaseWhitelistAdapter]] = {
|
_PLATFORM_REGISTRY: dict[str, type[BaseWhitelistAdapter]] = {
|
||||||
"xiequ": XiequAdapter,
|
"xiequ": XiequAdapter,
|
||||||
"xkdaili": XkdailiAdapter,
|
"xkdaili": XkdailiAdapter,
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,7 @@ _PLATFORM_LABELS: dict[str, str] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def create_adapter(platform: str, credentials: dict) -> Optional[BaseWhitelistAdapter]:
|
def create_adapter(platform: str, credentials: dict) -> BaseWhitelistAdapter | None:
|
||||||
"""根据平台标识符和凭据创建适配器实例。"""
|
"""根据平台标识符和凭据创建适配器实例。"""
|
||||||
cls = _PLATFORM_REGISTRY.get(platform)
|
cls = _PLATFORM_REGISTRY.get(platform)
|
||||||
if not cls:
|
if not cls:
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import re
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -144,12 +143,12 @@ class BaseWhitelistAdapter(ABC):
|
|||||||
return False, msg
|
return False, msg
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_local_exit_ip() -> Optional[str]:
|
def get_local_exit_ip() -> str | None:
|
||||||
"""获取本机当前公网出口IP(不走代理)。"""
|
"""获取本机当前公网出口IP(不走代理)。"""
|
||||||
return _get_local_exit_ip()
|
return _get_local_exit_ip()
|
||||||
|
|
||||||
|
|
||||||
def _get_local_exit_ip() -> Optional[str]:
|
def _get_local_exit_ip() -> str | None:
|
||||||
"""获取本机当前公网出口IP(不走代理)。"""
|
"""获取本机当前公网出口IP(不走代理)。"""
|
||||||
targets = [
|
targets = [
|
||||||
"https://myip.ipip.net",
|
"https://myip.ipip.net",
|
||||||
@@ -171,7 +170,7 @@ def _get_local_exit_ip() -> Optional[str]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_exit_ip_via_proxy(proxy: str) -> Optional[str]:
|
def get_exit_ip_via_proxy(proxy: str) -> str | None:
|
||||||
"""通过代理获取出口IP。"""
|
"""通过代理获取出口IP。"""
|
||||||
targets = [
|
targets = [
|
||||||
"https://qifu-api.baidubce.com/ip/local/geo/v1/district",
|
"https://qifu-api.baidubce.com/ip/local/geo/v1/district",
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import Callable, Optional, Protocol
|
from collections.abc import Callable
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -19,7 +20,7 @@ class WhitelistSyncer(Protocol):
|
|||||||
|
|
||||||
def sync_ip(self, ip: str) -> tuple[bool, str]: ...
|
def sync_ip(self, ip: str) -> tuple[bool, str]: ...
|
||||||
|
|
||||||
def get_local_exit_ip(self) -> Optional[str]: ...
|
def get_local_exit_ip(self) -> str | None: ...
|
||||||
|
|
||||||
|
|
||||||
class ProxyResolver:
|
class ProxyResolver:
|
||||||
@@ -28,11 +29,11 @@ class ProxyResolver:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
api_url: str,
|
api_url: str,
|
||||||
whitelist_syncer: Optional[WhitelistSyncer] = None,
|
whitelist_syncer: WhitelistSyncer | None = None,
|
||||||
log_func: Optional[LogFunc] = None,
|
log_func: LogFunc | None = None,
|
||||||
sync_local_exit_ip: bool = False,
|
sync_local_exit_ip: bool = False,
|
||||||
sync_whitelist_once: bool = True,
|
sync_whitelist_once: bool = True,
|
||||||
stop_event: Optional[threading.Event] = None,
|
stop_event: threading.Event | None = None,
|
||||||
):
|
):
|
||||||
self.api_url = api_url
|
self.api_url = api_url
|
||||||
self.whitelist_syncer = whitelist_syncer
|
self.whitelist_syncer = whitelist_syncer
|
||||||
@@ -40,7 +41,7 @@ class ProxyResolver:
|
|||||||
self.sync_local_exit_ip = sync_local_exit_ip
|
self.sync_local_exit_ip = sync_local_exit_ip
|
||||||
self.sync_whitelist_once = sync_whitelist_once
|
self.sync_whitelist_once = sync_whitelist_once
|
||||||
self.stop_event = stop_event
|
self.stop_event = stop_event
|
||||||
self._last_synced_ip: Optional[str] = None
|
self._last_synced_ip: str | None = None
|
||||||
self._has_synced_whitelist = False
|
self._has_synced_whitelist = False
|
||||||
|
|
||||||
def _is_stopped(self) -> bool:
|
def _is_stopped(self) -> bool:
|
||||||
@@ -95,7 +96,7 @@ class ProxyResolver:
|
|||||||
self,
|
self,
|
||||||
max_attempts: int = 4,
|
max_attempts: int = 4,
|
||||||
return_all: bool = False,
|
return_all: bool = False,
|
||||||
) -> tuple[Optional[str | list[str]], str]:
|
) -> tuple[str | list[str] | None, str]:
|
||||||
"""
|
"""
|
||||||
获取并验证代理。
|
获取并验证代理。
|
||||||
|
|
||||||
@@ -178,8 +179,8 @@ def resolve_working_proxy(
|
|||||||
whitelist_platform: str = "xiequ",
|
whitelist_platform: str = "xiequ",
|
||||||
whitelist_credentials: dict | None = None,
|
whitelist_credentials: dict | None = None,
|
||||||
max_attempts: int = 4,
|
max_attempts: int = 4,
|
||||||
log_func: Optional[LogFunc] = None,
|
log_func: LogFunc | None = None,
|
||||||
) -> tuple[Optional[str], str]:
|
) -> tuple[str | None, str]:
|
||||||
"""从代理 API 获取可用代理,自动处理白名单同步。"""
|
"""从代理 API 获取可用代理,自动处理白名单同步。"""
|
||||||
syncer = (
|
syncer = (
|
||||||
DouyuWhitelistSyncer(
|
DouyuWhitelistSyncer(
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"""代理可用性验证。"""
|
"""代理可用性验证。"""
|
||||||
|
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -45,7 +44,7 @@ def verify_proxies_concurrent(
|
|||||||
timeout: tuple = (5, 8),
|
timeout: tuple = (5, 8),
|
||||||
max_workers: int = 5,
|
max_workers: int = 5,
|
||||||
return_all: bool = False,
|
return_all: bool = False,
|
||||||
) -> tuple[Optional[str | list[str]], str]:
|
) -> tuple[str | list[str] | None, str]:
|
||||||
"""
|
"""
|
||||||
并发验证多个代理 URL。
|
并发验证多个代理 URL。
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"""代理模块使用的白名单适配器。"""
|
"""代理模块使用的白名单适配器。"""
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from .proxy_platforms import create_adapter
|
from .proxy_platforms import create_adapter
|
||||||
from .proxy_platforms.base import BaseWhitelistAdapter, _get_local_exit_ip
|
from .proxy_platforms.base import BaseWhitelistAdapter, _get_local_exit_ip
|
||||||
@@ -17,7 +16,7 @@ class DouyuWhitelistSyncer:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
platform: str = "xiequ",
|
platform: str = "xiequ",
|
||||||
credentials: Optional[dict] = None,
|
credentials: dict | None = None,
|
||||||
uid: str = "",
|
uid: str = "",
|
||||||
ukey: str = "",
|
ukey: str = "",
|
||||||
):
|
):
|
||||||
@@ -28,7 +27,7 @@ class DouyuWhitelistSyncer:
|
|||||||
|
|
||||||
self.platform = platform
|
self.platform = platform
|
||||||
self.credentials = credentials or {}
|
self.credentials = credentials or {}
|
||||||
self._adapter: Optional[BaseWhitelistAdapter] = None
|
self._adapter: BaseWhitelistAdapter | None = None
|
||||||
|
|
||||||
if self.credentials:
|
if self.credentials:
|
||||||
self._adapter = create_adapter(platform, self.credentials)
|
self._adapter = create_adapter(platform, self.credentials)
|
||||||
@@ -38,5 +37,5 @@ class DouyuWhitelistSyncer:
|
|||||||
return False, "未配置白名单凭据"
|
return False, "未配置白名单凭据"
|
||||||
return self._adapter.sync_ip(ip)
|
return self._adapter.sync_ip(ip)
|
||||||
|
|
||||||
def get_local_exit_ip(self) -> Optional[str]:
|
def get_local_exit_ip(self) -> str | None:
|
||||||
return _get_local_exit_ip()
|
return _get_local_exit_ip()
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import hashlib
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
from collections.abc import Callable, Mapping
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
from typing import Any, Callable, Mapping
|
from typing import Any
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ class FishFinRechargeConfig:
|
|||||||
debug: bool = False
|
debug: bool = False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_env(cls) -> "FishFinRechargeConfig":
|
def from_env(cls) -> FishFinRechargeConfig:
|
||||||
"""从环境变量读取配置,不在代码或数据库中保存商户密钥。"""
|
"""从环境变量读取配置,不在代码或数据库中保存商户密钥。"""
|
||||||
timeout = float(os.getenv("FISH_FIN_RECHARGE_TIMEOUT", "20"))
|
timeout = float(os.getenv("FISH_FIN_RECHARGE_TIMEOUT", "20"))
|
||||||
return cls(
|
return cls(
|
||||||
@@ -189,7 +190,7 @@ class FishFinRechargeClient:
|
|||||||
"sign_params": sign_params,
|
"sign_params": sign_params,
|
||||||
# 不把可重放签名原文或 AppSecret 写入日志,只记录待签名串摘要。
|
# 不把可重放签名原文或 AppSecret 写入日志,只记录待签名串摘要。
|
||||||
"sign_source_digest": hashlib.sha256(
|
"sign_source_digest": hashlib.sha256(
|
||||||
f"{sign_query}{method.upper()}".encode("utf-8")
|
f"{sign_query}{method.upper()}".encode()
|
||||||
).hexdigest()[:12],
|
).hexdigest()[:12],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -240,7 +241,7 @@ class FishFinRechargeClient:
|
|||||||
return payload
|
return payload
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _amount(value: Decimal | int | float | str) -> str:
|
def _amount(value: Decimal | float | str) -> str:
|
||||||
"""规范化金额,避免浮点数表达式进入签名或订单请求。"""
|
"""规范化金额,避免浮点数表达式进入签名或订单请求。"""
|
||||||
try:
|
try:
|
||||||
price = Decimal(str(value))
|
price = Decimal(str(value))
|
||||||
@@ -251,7 +252,7 @@ class FishFinRechargeClient:
|
|||||||
return format(price.normalize(), "f")
|
return format(price.normalize(), "f")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _json_amount(cls, value: Decimal | int | float | str) -> int | float:
|
def _json_amount(cls, value: Decimal | float | str) -> int | float:
|
||||||
"""按文档以 JSON 数字发送金额,整数不附带无意义的小数位。"""
|
"""按文档以 JSON 数字发送金额,整数不附带无意义的小数位。"""
|
||||||
amount_text = cls._amount(value)
|
amount_text = cls._amount(value)
|
||||||
return int(amount_text) if "." not in amount_text else float(amount_text)
|
return int(amount_text) if "." not in amount_text else float(amount_text)
|
||||||
@@ -267,7 +268,7 @@ class FishFinRechargeClient:
|
|||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
buy_num: int,
|
buy_num: int,
|
||||||
pay_amount: Decimal | int | float | str,
|
pay_amount: Decimal | float | str,
|
||||||
out_order_id: str,
|
out_order_id: str,
|
||||||
product_id: str,
|
product_id: str,
|
||||||
recharge_arg: list[dict[str, Any]],
|
recharge_arg: list[dict[str, Any]],
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import random
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import random
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from Crypto.Cipher import AES, PKCS1_v1_5
|
||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
from Crypto.Cipher import PKCS1_v1_5, AES
|
|
||||||
|
|
||||||
|
|
||||||
# 随机产生4个字符组成的字符串
|
# 随机产生4个字符组成的字符串
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import requests
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from PIL import Image
|
import requests
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
REQUEST_TIMEOUT = (3.05, 12)
|
REQUEST_TIMEOUT = (3.05, 12)
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import time
|
|
||||||
import requests
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from typing import Mapping, Optional, Tuple
|
import time
|
||||||
|
from collections.abc import Mapping
|
||||||
|
|
||||||
|
import requests
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
REQUEST_TIMEOUT = (10, 30)
|
REQUEST_TIMEOUT = (10, 30)
|
||||||
@@ -12,9 +13,9 @@ PASSPORT_REFERER = "https://passport.douyu.com/"
|
|||||||
def _get(
|
def _get(
|
||||||
url: str,
|
url: str,
|
||||||
*,
|
*,
|
||||||
params: Optional[dict] = None,
|
params: dict | None = None,
|
||||||
headers: Optional[dict] = None,
|
headers: dict | None = None,
|
||||||
proxies: Optional[Mapping[str, str]] = None,
|
proxies: Mapping[str, str] | None = None,
|
||||||
) -> requests.Response:
|
) -> requests.Response:
|
||||||
"""发送极验 GET 请求,确保使用同一个代理出口。"""
|
"""发送极验 GET 请求,确保使用同一个代理出口。"""
|
||||||
return requests.get(
|
return requests.get(
|
||||||
@@ -55,7 +56,7 @@ def _parse_json_response(response: requests.Response, source: str) -> dict:
|
|||||||
raise ValueError(f"{source} 返回的不是有效 JSON: {preview}") from exc
|
raise ValueError(f"{source} 返回的不是有效 JSON: {preview}") from exc
|
||||||
|
|
||||||
|
|
||||||
def get_challenge_gt_bak() -> Tuple[str, str]:
|
def get_challenge_gt_bak() -> tuple[str, str]:
|
||||||
headers = {
|
headers = {
|
||||||
"accept": "application/json, text/javascript, */*; q=0.01",
|
"accept": "application/json, text/javascript, */*; q=0.01",
|
||||||
"accept-language": "zh-CN,zh;q=0.9",
|
"accept-language": "zh-CN,zh;q=0.9",
|
||||||
@@ -85,7 +86,7 @@ def get_challenge_gt_bak() -> Tuple[str, str]:
|
|||||||
return data["gt"], data["challenge"]
|
return data["gt"], data["challenge"]
|
||||||
|
|
||||||
|
|
||||||
def get_challenge_gt() -> Tuple[str, str]:
|
def get_challenge_gt() -> tuple[str, str]:
|
||||||
headers = {
|
headers = {
|
||||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
|
||||||
"Accept": "application/json, text/javascript, */*; q=0.01",
|
"Accept": "application/json, text/javascript, */*; q=0.01",
|
||||||
@@ -116,7 +117,7 @@ def get_challenge_gt() -> Tuple[str, str]:
|
|||||||
raise ValueError(f"斗鱼登录接口返回中缺少极验参数: {preview}") from exc
|
raise ValueError(f"斗鱼登录接口返回中缺少极验参数: {preview}") from exc
|
||||||
|
|
||||||
|
|
||||||
def get_js_address(gt: str, proxies: Optional[Mapping[str, str]] = None) -> dict:
|
def get_js_address(gt: str, proxies: Mapping[str, str] | None = None) -> dict:
|
||||||
headers = {
|
headers = {
|
||||||
"accept": "*/*",
|
"accept": "*/*",
|
||||||
"accept-language": "zh-CN,zh;q=0.9",
|
"accept-language": "zh-CN,zh;q=0.9",
|
||||||
@@ -149,8 +150,8 @@ def get_c_s(
|
|||||||
gt: str,
|
gt: str,
|
||||||
challenge: str,
|
challenge: str,
|
||||||
w: str,
|
w: str,
|
||||||
proxies: Optional[Mapping[str, str]] = None,
|
proxies: Mapping[str, str] | None = None,
|
||||||
) -> Tuple[list[int], str]:
|
) -> tuple[list[int], str]:
|
||||||
headers = {
|
headers = {
|
||||||
"accept": "*/*",
|
"accept": "*/*",
|
||||||
"accept-language": "zh-CN,zh;q=0.9",
|
"accept-language": "zh-CN,zh;q=0.9",
|
||||||
@@ -184,7 +185,7 @@ def req_fullpage_validate(
|
|||||||
gt: str,
|
gt: str,
|
||||||
challenge: str,
|
challenge: str,
|
||||||
w: str,
|
w: str,
|
||||||
proxies: Optional[Mapping[str, str]] = None,
|
proxies: Mapping[str, str] | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""HAR 中的 fullpage 最终校验,成功后直接返回 validate。"""
|
"""HAR 中的 fullpage 最终校验,成功后直接返回 validate。"""
|
||||||
headers = {
|
headers = {
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import time
|
|
||||||
import random
|
import random
|
||||||
from typing import Optional
|
import time
|
||||||
|
|
||||||
|
|
||||||
def generate_fake_performance_timing(base_time: Optional[int] = None) -> dict[str, int]:
|
def generate_fake_performance_timing(base_time: int | None = None) -> dict[str, int]:
|
||||||
"""
|
"""
|
||||||
生成伪造的浏览器性能时间戳数据
|
生成伪造的浏览器性能时间戳数据
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import random
|
|
||||||
import math
|
import math
|
||||||
from typing import Any, Optional
|
import random
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
# 生成类人的鼠标轨迹
|
# 生成类人的鼠标轨迹
|
||||||
@@ -91,7 +91,7 @@ def generate_realistic_trajectory(
|
|||||||
|
|
||||||
# 处理原始轨迹数组
|
# 处理原始轨迹数组
|
||||||
def process_mouse_trajectory(
|
def process_mouse_trajectory(
|
||||||
events: list[Any], max_records: Optional[int] = None
|
events: list[Any], max_records: int | None = None
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
处理鼠标/触摸轨迹数据,将绝对坐标转换为相对坐标和时间差
|
处理鼠标/触摸轨迹数据,将绝对坐标转换为相对坐标和时间差
|
||||||
|
|||||||
@@ -1,35 +1,37 @@
|
|||||||
import time
|
|
||||||
import random
|
|
||||||
import json
|
import json
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from core.geetest.common.trajectory import (
|
|
||||||
generate_realistic_trajectory,
|
|
||||||
process_mouse_trajectory,
|
|
||||||
compress_trajectory,
|
|
||||||
TrajectoryEncoder,
|
|
||||||
H,
|
|
||||||
)
|
|
||||||
from core.geetest.common.crypto import (
|
from core.geetest.common.crypto import (
|
||||||
four_random_chart,
|
|
||||||
RSA_jiami_r,
|
|
||||||
AES_O,
|
AES_O,
|
||||||
geetest_base64_encode,
|
RSA_jiami_r,
|
||||||
encrypt_string,
|
encrypt_string,
|
||||||
|
four_random_chart,
|
||||||
|
geetest_base64_encode,
|
||||||
simple_md5,
|
simple_md5,
|
||||||
)
|
)
|
||||||
from core.geetest.common.imaging import download_picture
|
from core.geetest.common.imaging import download_picture
|
||||||
from core.geetest.common.network import (
|
from core.geetest.common.network import (
|
||||||
|
get_c_s,
|
||||||
get_challenge_gt,
|
get_challenge_gt,
|
||||||
get_js_address,
|
get_js_address,
|
||||||
get_c_s,
|
|
||||||
req_slide,
|
|
||||||
get_picture,
|
get_picture,
|
||||||
req_end,
|
req_end,
|
||||||
|
req_slide,
|
||||||
)
|
)
|
||||||
from core.geetest.common.performance import (
|
from core.geetest.common.performance import (
|
||||||
generate_fake_performance_timing,
|
generate_fake_performance_timing,
|
||||||
get_slide_track,
|
get_slide_track,
|
||||||
)
|
)
|
||||||
|
from core.geetest.common.trajectory import (
|
||||||
|
H,
|
||||||
|
TrajectoryEncoder,
|
||||||
|
compress_trajectory,
|
||||||
|
generate_realistic_trajectory,
|
||||||
|
process_mouse_trajectory,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _generate_seed() -> str:
|
def _generate_seed() -> str:
|
||||||
|
|||||||
@@ -33,23 +33,23 @@ if TYPE_CHECKING:
|
|||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"HuyaHttpClient",
|
|
||||||
"HuyaWssClient",
|
|
||||||
"GetUserScoreReq",
|
"GetUserScoreReq",
|
||||||
"GetUserScoreResp",
|
"GetUserScoreResp",
|
||||||
|
"HuyaAppLoginError",
|
||||||
|
"HuyaAppPasswordLogin",
|
||||||
|
"HuyaAppQrAuthRequiredError",
|
||||||
"HuyaCredentialError",
|
"HuyaCredentialError",
|
||||||
|
"HuyaHttpClient",
|
||||||
"HuyaLoginError",
|
"HuyaLoginError",
|
||||||
"HuyaLoginResult",
|
"HuyaLoginResult",
|
||||||
"HuyaPasswordLogin",
|
"HuyaPasswordLogin",
|
||||||
"HuyaAppLoginError",
|
|
||||||
"HuyaAppQrAuthRequiredError",
|
|
||||||
"HuyaAppPasswordLogin",
|
|
||||||
"HuyaSmsCodeResult",
|
"HuyaSmsCodeResult",
|
||||||
"HuyaSmsLogin",
|
"HuyaSmsLogin",
|
||||||
"HuyaVerificationError",
|
"HuyaVerificationError",
|
||||||
"HuyaVerificationSolver",
|
"HuyaVerificationSolver",
|
||||||
"login_huya_password",
|
"HuyaWssClient",
|
||||||
"login_huya_app_password",
|
"login_huya_app_password",
|
||||||
|
"login_huya_password",
|
||||||
"login_huya_sms",
|
"login_huya_sms",
|
||||||
"send_huya_sms_code",
|
"send_huya_sms_code",
|
||||||
"solve_huya_verification",
|
"solve_huya_verification",
|
||||||
@@ -98,8 +98,8 @@ def __getattr__(name: str):
|
|||||||
}:
|
}:
|
||||||
from .app_login import (
|
from .app_login import (
|
||||||
HuyaAppLoginError,
|
HuyaAppLoginError,
|
||||||
HuyaAppQrAuthRequiredError,
|
|
||||||
HuyaAppPasswordLogin,
|
HuyaAppPasswordLogin,
|
||||||
|
HuyaAppQrAuthRequiredError,
|
||||||
login_huya_app_password,
|
login_huya_app_password,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -35,13 +35,12 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from .app_login import HuyaAppPasswordLogin
|
||||||
from .device_profile import (
|
from .device_profile import (
|
||||||
HDID32, # 登录帧 t1.t0 32hex 常量 (R15 公式结果, k1 来源见 R39; device_profile.py)
|
|
||||||
_load_db,
|
_load_db,
|
||||||
_save_db,
|
_save_db,
|
||||||
get_profile, # 幂等画像: 同账号永远复用同一套 (data/huya_device_profiles.json)
|
get_profile, # 幂等画像: 同账号永远复用同一套 (data/huya_device_profiles.json)
|
||||||
)
|
)
|
||||||
from .app_login import HuyaAppPasswordLogin
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ def wup_password_login_raw(
|
|||||||
固定 action/device_id。风控重试调用方应显式复用同一注册结果。
|
固定 action/device_id。风控重试调用方应显式复用同一注册结果。
|
||||||
注册链失败抛 ``HuyaAppLoginError``,绝不静默回退旧固定值。
|
注册链失败抛 ``HuyaAppLoginError``,绝不静默回退旧固定值。
|
||||||
"""
|
"""
|
||||||
uid_str = account[3:] if account.startswith("hy_") else account
|
uid_str = account.removeprefix("hy_")
|
||||||
dev = dict(device_info) if device_info is not None else get_profile(account)
|
dev = dict(device_info) if device_info is not None else get_profile(account)
|
||||||
mj, ua, _old_sd = _golden_session_assets()
|
mj, ua, _old_sd = _golden_session_assets()
|
||||||
if not safedeviceid:
|
if not safedeviceid:
|
||||||
@@ -304,7 +304,7 @@ def login_cred_with_flow(
|
|||||||
logger.info(f"[huya-app] 第 {rnd + 1} 轮触发安全验证: {kind}")
|
logger.info(f"[huya-app] 第 {rnd + 1} 轮触发安全验证: {kind}")
|
||||||
if "qr_auth" in risk_url:
|
if "qr_auth" in risk_url:
|
||||||
raise HuyaAppQrAuthRequiredError(
|
raise HuyaAppQrAuthRequiredError(
|
||||||
f"该账号 App 渠道要求扫码验证(qr_auth),请先在手机虎牙 App 上正常登录一次建立设备信任。"
|
"该账号 App 渠道要求扫码验证(qr_auth),请先在手机虎牙 App 上正常登录一次建立设备信任。"
|
||||||
)
|
)
|
||||||
solve_safe_auth(risk_url, proxies=proxies, device_info=dev)
|
solve_safe_auth(risk_url, proxies=proxies, device_info=dev)
|
||||||
logger.info("[huya-app] safe_auth 滑块过验成功,重发 WUP 登录...")
|
logger.info("[huya-app] safe_auth 滑块过验成功,重发 WUP 登录...")
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
|
||||||
import os
|
import os
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ from .login import (
|
|||||||
generate_request_id,
|
generate_request_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
CHANGE_PASSWORD_VERSION = "2.5"
|
CHANGE_PASSWORD_VERSION = "2.5"
|
||||||
CHANGE_PASSWORD_CHECK_URI = "60011"
|
CHANGE_PASSWORD_CHECK_URI = "60011"
|
||||||
CHANGE_PASSWORD_SEND_SMS_URI = "60003"
|
CHANGE_PASSWORD_SEND_SMS_URI = "60003"
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Iterable, Mapping
|
from collections.abc import Iterable, Mapping
|
||||||
|
|
||||||
import requests
|
|
||||||
from requests.cookies import RequestsCookieJar
|
from requests.cookies import RequestsCookieJar
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ class Envelope:
|
|||||||
self._parse()
|
self._parse()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, path: str | Path | None = None) -> "Envelope":
|
def load(cls, path: str | Path | None = None) -> Envelope:
|
||||||
"""加载信封模板,支持从文件加载或使用内嵌金样本。"""
|
"""加载信封模板,支持从文件加载或使用内嵌金样本。"""
|
||||||
if path:
|
if path:
|
||||||
p = Path(path)
|
p = Path(path)
|
||||||
@@ -136,7 +136,7 @@ class Envelope:
|
|||||||
return cls(base64.b64decode(DEFAULT_QURL_B64))
|
return cls(base64.b64decode(DEFAULT_QURL_B64))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _load_from_path(cls, p: Path) -> "Envelope":
|
def _load_from_path(cls, p: Path) -> Envelope:
|
||||||
if p.suffix == ".json":
|
if p.suffix == ".json":
|
||||||
j = json.loads(p.read_text("utf-8"))
|
j = json.loads(p.read_text("utf-8"))
|
||||||
q = next(
|
q = next(
|
||||||
@@ -236,7 +236,7 @@ class Envelope:
|
|||||||
assert self.uid_off is not None
|
assert self.uid_off is not None
|
||||||
return struct.unpack_from(">Q", self.raw, self.uid_off)[0]
|
return struct.unpack_from(">Q", self.raw, self.uid_off)[0]
|
||||||
|
|
||||||
def patch_uid(self, uid: int) -> "Envelope":
|
def patch_uid(self, uid: int) -> Envelope:
|
||||||
assert self.uid_off is not None
|
assert self.uid_off is not None
|
||||||
struct.pack_into(">Q", self.raw, self.uid_off, uid)
|
struct.pack_into(">Q", self.raw, self.uid_off, uid)
|
||||||
return self
|
return self
|
||||||
@@ -246,7 +246,7 @@ class Envelope:
|
|||||||
assert self.cert_off is not None
|
assert self.cert_off is not None
|
||||||
return bytes(self.raw[self.cert_off : self.cert_off + self.cert_len])
|
return bytes(self.raw[self.cert_off : self.cert_off + self.cert_len])
|
||||||
|
|
||||||
def patch_cert(self, cert: bytes) -> "Envelope":
|
def patch_cert(self, cert: bytes) -> Envelope:
|
||||||
assert self.cert_off is not None
|
assert self.cert_off is not None
|
||||||
b64 = base64.b64encode(cert)
|
b64 = base64.b64encode(cert)
|
||||||
if len(b64) != self.cert_len:
|
if len(b64) != self.cert_len:
|
||||||
@@ -256,7 +256,7 @@ class Envelope:
|
|||||||
self.raw[self.cert_off : self.cert_off + self.cert_len] = b64
|
self.raw[self.cert_off : self.cert_off + self.cert_len] = b64
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def patch_session(self, session: int) -> "Envelope":
|
def patch_session(self, session: int) -> Envelope:
|
||||||
if self.tag4_span:
|
if self.tag4_span:
|
||||||
struct.pack_into(">I", self.raw, self.tag4_span[0], session & 0xFFFFFFFF)
|
struct.pack_into(">I", self.raw, self.tag4_span[0], session & 0xFFFFFFFF)
|
||||||
d = self.raw
|
d = self.raw
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ TAF/WUP 帧解码器 — 将二进制帧转为可读摘要,用于日志输出
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from .taf_protocol import TafInputStream, TafType
|
from .taf_protocol import TafInputStream, TafType
|
||||||
from .wup_protocol import normalize_wup_payload
|
from .wup_protocol import normalize_wup_payload
|
||||||
|
|
||||||
@@ -103,7 +104,7 @@ def _decode_taf_struct(ins: TafInputStream, depth: int = 0) -> dict:
|
|||||||
except Exception:
|
except Exception:
|
||||||
val = f"<decode_err:0x{dtype:02x}>"
|
val = f"<decode_err:0x{dtype:02x}>"
|
||||||
else:
|
else:
|
||||||
val = f"<...>"
|
val = "<...>"
|
||||||
try:
|
try:
|
||||||
ins.skip_field(dtype)
|
ins.skip_field(dtype)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -152,13 +153,7 @@ def _decode_wup_body(body: bytes) -> dict:
|
|||||||
if tag > 10:
|
if tag > 10:
|
||||||
break
|
break
|
||||||
ins.read_head()
|
ins.read_head()
|
||||||
if tag == 1:
|
if tag == 1 or tag in (2, 3) or tag == 4:
|
||||||
if dtype != TafType.ZERO:
|
|
||||||
ins._read_int_value(dtype)
|
|
||||||
elif tag in (2, 3):
|
|
||||||
if dtype != TafType.ZERO:
|
|
||||||
ins._read_int_value(dtype)
|
|
||||||
elif tag == 4:
|
|
||||||
if dtype != TafType.ZERO:
|
if dtype != TafType.ZERO:
|
||||||
ins._read_int_value(dtype)
|
ins._read_int_value(dtype)
|
||||||
elif tag == 5:
|
elif tag == 5:
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ import base64
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
import struct
|
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from typing import Any, Optional, Callable
|
from collections.abc import Callable
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from .cookie_utils import cookie_pairs, normalize_cookie_pairs, normalize_huya_cookie
|
from .cookie_utils import cookie_pairs, normalize_cookie_pairs, normalize_huya_cookie
|
||||||
from .taf_protocol import TafOutputStream, TafInputStream, TafType, TafStruct
|
from .taf_protocol import TafInputStream, TafOutputStream, TafStruct
|
||||||
from .wup_protocol import WupRequest, WupResponse
|
from .wup_protocol import WupRequest, WupResponse
|
||||||
|
|
||||||
CDNWS_HOST = "cdnws.api.huya.com"
|
CDNWS_HOST = "cdnws.api.huya.com"
|
||||||
@@ -849,11 +849,11 @@ class HuyaHttpClient:
|
|||||||
):
|
):
|
||||||
"""order_type=None 时自动尝试从 1 到 10 找到有效值"""
|
"""order_type=None 时自动尝试从 1 到 10 找到有效值"""
|
||||||
from .shop_structs import (
|
from .shop_structs import (
|
||||||
CreateOrderReqV5,
|
CreateOrderAccountParam,
|
||||||
CreateOrderRsp,
|
|
||||||
CreateOrderExtraParam,
|
CreateOrderExtraParam,
|
||||||
CreateOrderPromotionParam,
|
CreateOrderPromotionParam,
|
||||||
CreateOrderAccountParam,
|
CreateOrderReqV5,
|
||||||
|
CreateOrderRsp,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 如果指定了具体值,直接试
|
# 如果指定了具体值,直接试
|
||||||
@@ -951,7 +951,7 @@ class HuyaHttpClient:
|
|||||||
|
|
||||||
baseinfo = self._generate_rpc_baseinfo(uid, guid, cookie)
|
baseinfo = self._generate_rpc_baseinfo(uid, guid, cookie)
|
||||||
url = f"https://{CDNWS_HOST}/?baseinfo={baseinfo}"
|
url = f"https://{CDNWS_HOST}/?baseinfo={baseinfo}"
|
||||||
self.logger(f"[HTTP] POST shopMiddleUI.payOrderSubmitV5")
|
self.logger("[HTTP] POST shopMiddleUI.payOrderSubmitV5")
|
||||||
self.logger(f"[HTTP] 发送 hex前60: {wup_data[:60].hex()}")
|
self.logger(f"[HTTP] 发送 hex前60: {wup_data[:60].hex()}")
|
||||||
|
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
|
|||||||
+2
-3
@@ -14,12 +14,11 @@ from http.cookies import SimpleCookie
|
|||||||
from urllib.parse import quote, urlsplit, urlunsplit
|
from urllib.parse import quote, urlsplit, urlunsplit
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from requests.cookies import RequestsCookieJar
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
from requests.cookies import RequestsCookieJar
|
||||||
|
|
||||||
from .cookie_utils import normalize_huya_cookie
|
from .cookie_utils import normalize_huya_cookie
|
||||||
|
|
||||||
|
|
||||||
APP_ID = "5002"
|
APP_ID = "5002"
|
||||||
APP_VERSION = "2.6"
|
APP_VERSION = "2.6"
|
||||||
APP_SIGN = "1ce3bf682483d03f146f58232ec10635"
|
APP_SIGN = "1ce3bf682483d03f146f58232ec10635"
|
||||||
@@ -101,7 +100,7 @@ def generate_context(device_id: str | None = None, mid: str | None = None) -> st
|
|||||||
|
|
||||||
def generate_request_id() -> str:
|
def generate_request_id() -> str:
|
||||||
"""生成 requestId,形态参考旧实现的日内毫秒数。"""
|
"""生成 requestId,形态参考旧实现的日内毫秒数。"""
|
||||||
now = dt.datetime.now(dt.timezone.utc)
|
now = dt.datetime.now(dt.UTC)
|
||||||
midnight = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
midnight = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
return str(int((now - midnight).total_seconds() * 1000))
|
return str(int((now - midnight).total_seconds() * 1000))
|
||||||
|
|
||||||
|
|||||||
+14
-14
@@ -5,8 +5,8 @@
|
|||||||
来源:m-shop.yaoguo.com 的 jce/ShopFacade.js、api/orderui.ts、api/mall/PlayMallNewHome.ts
|
来源:m-shop.yaoguo.com 的 jce/ShopFacade.js、api/orderui.ts、api/mall/PlayMallNewHome.ts
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import List, Dict, Optional
|
|
||||||
from .taf_protocol import TafOutputStream, TafInputStream, TafStruct, TafType
|
from .taf_protocol import TafInputStream, TafOutputStream, TafStruct, TafType
|
||||||
|
|
||||||
|
|
||||||
class OrderType:
|
class OrderType:
|
||||||
@@ -387,7 +387,7 @@ class GoodsPriceInfo(TafStruct):
|
|||||||
self.spuId: str = ""
|
self.spuId: str = ""
|
||||||
self.minPrice: int = 0
|
self.minPrice: int = 0
|
||||||
self.maxPrice: int = 0
|
self.maxPrice: int = 0
|
||||||
self.skuMap: Dict[int, GoodsSkuItem] = {}
|
self.skuMap: dict[int, GoodsSkuItem] = {}
|
||||||
self.stock: int = 0
|
self.stock: int = 0
|
||||||
self.buyLimit: int = 0
|
self.buyLimit: int = 0
|
||||||
self.price: int = 0
|
self.price: int = 0
|
||||||
@@ -500,7 +500,7 @@ class OrderListGoodsDetail(TafStruct):
|
|||||||
self.buyerUid: int = 0 # tag 18
|
self.buyerUid: int = 0 # tag 18
|
||||||
self.virtualType: int = 0 # tag 19
|
self.virtualType: int = 0 # tag 19
|
||||||
self.quantity: int = 0 # tag 20
|
self.quantity: int = 0 # tag 20
|
||||||
self.shopInfo: Optional[OrderListShopInfo] = None # tag 21
|
self.shopInfo: OrderListShopInfo | None = None # tag 21
|
||||||
self.points: int = 0 # tag 23
|
self.points: int = 0 # tag 23
|
||||||
|
|
||||||
def read_from(self, ins: TafInputStream):
|
def read_from(self, ins: TafInputStream):
|
||||||
@@ -544,7 +544,7 @@ class OrderListItem(TafStruct):
|
|||||||
self.totalPrice: int = 0 # tag 12 分
|
self.totalPrice: int = 0 # tag 12 分
|
||||||
self.createTime: int = 0 # tag 14 毫秒时间戳
|
self.createTime: int = 0 # tag 14 毫秒时间戳
|
||||||
self.payTime: int = 0 # tag 15 毫秒时间戳
|
self.payTime: int = 0 # tag 15 毫秒时间戳
|
||||||
self.goodsDetail: Optional[OrderListGoodsDetail] = None # tag 16
|
self.goodsDetail: OrderListGoodsDetail | None = None # tag 16
|
||||||
|
|
||||||
def read_from(self, ins: TafInputStream):
|
def read_from(self, ins: TafInputStream):
|
||||||
self.bizOrderId = ins.read_string(0, default=self.bizOrderId)
|
self.bizOrderId = ins.read_string(0, default=self.bizOrderId)
|
||||||
@@ -611,7 +611,7 @@ class QueryUserOrderListRsp(TafStruct):
|
|||||||
self.code: int = 0
|
self.code: int = 0
|
||||||
self.message: str = ""
|
self.message: str = ""
|
||||||
self.totalCount: int = 0
|
self.totalCount: int = 0
|
||||||
self.orders: List[OrderListItem] = []
|
self.orders: list[OrderListItem] = []
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _read_order_item(ins: TafInputStream, _tag: int):
|
def _read_order_item(ins: TafInputStream, _tag: int):
|
||||||
@@ -692,9 +692,9 @@ class CreateOrderPromotionParam(TafStruct):
|
|||||||
|
|
||||||
class CreateOrderAccountParam(TafStruct):
|
class CreateOrderAccountParam(TafStruct):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.payoutTypeList: List[int] = [] # tag 0 Vector<INT32>
|
self.payoutTypeList: list[int] = [] # tag 0 Vector<INT32>
|
||||||
self.payoutChargeAmount: int = 0 # tag 1
|
self.payoutChargeAmount: int = 0 # tag 1
|
||||||
self.cancelPayoutTypeList: List[int] = [] # tag 2
|
self.cancelPayoutTypeList: list[int] = [] # tag 2
|
||||||
self.recycleSupplierId: int = 0 # tag 3
|
self.recycleSupplierId: int = 0 # tag 3
|
||||||
self.claimPrice: int = 0 # tag 4
|
self.claimPrice: int = 0 # tag 4
|
||||||
|
|
||||||
@@ -739,20 +739,20 @@ class CreateOrderReqV5(TafStruct):
|
|||||||
self.gameId: str = "" # tag 8
|
self.gameId: str = "" # tag 8
|
||||||
self.orderId: int = 0 # tag 9
|
self.orderId: int = 0 # tag 9
|
||||||
self.src: int = 0 # tag 10
|
self.src: int = 0 # tag 10
|
||||||
self.couponUserIds: List[int] = [] # tag 11 Vector<INT64>
|
self.couponUserIds: list[int] = [] # tag 11 Vector<INT64>
|
||||||
self.orderType: int = 0 # tag 12
|
self.orderType: int = 0 # tag 12
|
||||||
self.extraParam: Optional[CreateOrderExtraParam] = None # tag 13
|
self.extraParam: CreateOrderExtraParam | None = None # tag 13
|
||||||
self.scene: int = 0 # tag 14
|
self.scene: int = 0 # tag 14
|
||||||
self.promotionItems: List = [] # tag 15 Vector<PromotionItem>
|
self.promotionItems: list = [] # tag 15 Vector<PromotionItem>
|
||||||
self.sourceId: str = "" # tag 16
|
self.sourceId: str = "" # tag 16
|
||||||
self.env: Dict[str, str] = {} # tag 17 Map<STRING,STRING>
|
self.env: dict[str, str] = {} # tag 17 Map<STRING,STRING>
|
||||||
self.orderScene: int = 0 # tag 18
|
self.orderScene: int = 0 # tag 18
|
||||||
self.watchWord: str = "" # tag 19
|
self.watchWord: str = "" # tag 19
|
||||||
self.marketingChannel: str = "" # tag 20
|
self.marketingChannel: str = "" # tag 20
|
||||||
self.promotionParam: Optional[CreateOrderPromotionParam] = None # tag 21
|
self.promotionParam: CreateOrderPromotionParam | None = None # tag 21
|
||||||
self.externalTraceKey: str = "" # tag 22
|
self.externalTraceKey: str = "" # tag 22
|
||||||
self.kefuUid: int = 0 # tag 23
|
self.kefuUid: int = 0 # tag 23
|
||||||
self.accountParam: Optional[CreateOrderAccountParam] = None # tag 24
|
self.accountParam: CreateOrderAccountParam | None = None # tag 24
|
||||||
self.parentOrderId: int = 0 # tag 25
|
self.parentOrderId: int = 0 # tag 25
|
||||||
self.vendorAccountType: str = "" # tag 26
|
self.vendorAccountType: str = "" # tag 26
|
||||||
self.vendorAccountVal: str = "" # tag 27
|
self.vendorAccountVal: str = "" # tag 27
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ from .login import (
|
|||||||
generate_request_id,
|
generate_request_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
SMS_CODE_URI = "60027"
|
SMS_CODE_URI = "60027"
|
||||||
SMS_LOGIN_URI = "60025"
|
SMS_LOGIN_URI = "60025"
|
||||||
SMS_CODE_URL = "https://udblgn.huya.com/web/v2/smsCode"
|
SMS_CODE_URL = "https://udblgn.huya.com/web/v2/smsCode"
|
||||||
@@ -604,7 +603,7 @@ class HuyaSmsLogin:
|
|||||||
phone: str = "",
|
phone: str = "",
|
||||||
proxies: Mapping[str, str] | None = None,
|
proxies: Mapping[str, str] | None = None,
|
||||||
timeout: tuple[float, float] | None = None,
|
timeout: tuple[float, float] | None = None,
|
||||||
) -> "HuyaSmsLogin":
|
) -> HuyaSmsLogin:
|
||||||
"""从发码阶段返回的 state 恢复短信登录会话。"""
|
"""从发码阶段返回的 state 恢复短信登录会话。"""
|
||||||
try:
|
try:
|
||||||
raw = base64.urlsafe_b64decode(state.encode("ascii"))
|
raw = base64.urlsafe_b64decode(state.encode("ascii"))
|
||||||
|
|||||||
+10
-10
@@ -9,9 +9,9 @@
|
|||||||
0x0c ZERO 0x0d SIMPLE_LIST
|
0x0c ZERO 0x0d SIMPLE_LIST
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import struct
|
|
||||||
import io
|
import io
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
import struct
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
class TafType:
|
class TafType:
|
||||||
@@ -136,7 +136,7 @@ class TafOutputStream:
|
|||||||
|
|
||||||
# ---- Map ----
|
# ---- Map ----
|
||||||
def write_map(
|
def write_map(
|
||||||
self, tag: int, value: Dict[Any, Any], key_writer=None, val_writer=None
|
self, tag: int, value: dict[Any, Any], key_writer=None, val_writer=None
|
||||||
):
|
):
|
||||||
self.write_head(tag, TafType.MAP)
|
self.write_head(tag, TafType.MAP)
|
||||||
self.write_int32(0, len(value))
|
self.write_int32(0, len(value))
|
||||||
@@ -151,7 +151,7 @@ class TafOutputStream:
|
|||||||
self._write_any(1, v)
|
self._write_any(1, v)
|
||||||
|
|
||||||
# ---- List ----
|
# ---- List ----
|
||||||
def write_list(self, tag: int, value: List[Any], item_writer=None):
|
def write_list(self, tag: int, value: list[Any], item_writer=None):
|
||||||
self.write_head(tag, TafType.LIST)
|
self.write_head(tag, TafType.LIST)
|
||||||
self.write_int32(0, len(value))
|
self.write_int32(0, len(value))
|
||||||
for item in value:
|
for item in value:
|
||||||
@@ -192,7 +192,7 @@ class TafInputStream:
|
|||||||
def __init__(self, data: bytes):
|
def __init__(self, data: bytes):
|
||||||
self.buf = io.BytesIO(data)
|
self.buf = io.BytesIO(data)
|
||||||
|
|
||||||
def peek_head(self) -> Tuple[int, int]:
|
def peek_head(self) -> tuple[int, int]:
|
||||||
"""读取 head 但不消费(用于探测)"""
|
"""读取 head 但不消费(用于探测)"""
|
||||||
pos = self.buf.tell()
|
pos = self.buf.tell()
|
||||||
try:
|
try:
|
||||||
@@ -200,7 +200,7 @@ class TafInputStream:
|
|||||||
finally:
|
finally:
|
||||||
self.buf.seek(pos)
|
self.buf.seek(pos)
|
||||||
|
|
||||||
def read_head(self) -> Tuple[int, int]:
|
def read_head(self) -> tuple[int, int]:
|
||||||
"""返回 (tag, type)"""
|
"""返回 (tag, type)"""
|
||||||
data = self.buf.read(1)
|
data = self.buf.read(1)
|
||||||
if not data:
|
if not data:
|
||||||
@@ -293,7 +293,7 @@ class TafInputStream:
|
|||||||
self.skip_field(it)
|
self.skip_field(it)
|
||||||
|
|
||||||
# ---- 带跳过策略的字段读取:找到 tag,否则返回默认 ----
|
# ---- 带跳过策略的字段读取:找到 tag,否则返回默认 ----
|
||||||
def _find_tag(self, target_tag: int, required: bool) -> Optional[Tuple[int, int]]:
|
def _find_tag(self, target_tag: int, required: bool) -> tuple[int, int] | None:
|
||||||
"""逐个读 head,tag 相等则返回,tag 超过则回退并返回 None"""
|
"""逐个读 head,tag 相等则返回,tag 超过则回退并返回 None"""
|
||||||
while True:
|
while True:
|
||||||
pos = self.buf.tell()
|
pos = self.buf.tell()
|
||||||
@@ -402,7 +402,7 @@ class TafInputStream:
|
|||||||
# ---- 复合类型 ----
|
# ---- 复合类型 ----
|
||||||
def read_map(
|
def read_map(
|
||||||
self, tag: int, required: bool = False, key_reader=None, val_reader=None
|
self, tag: int, required: bool = False, key_reader=None, val_reader=None
|
||||||
) -> Dict:
|
) -> dict:
|
||||||
found = self._find_tag(tag, required)
|
found = self._find_tag(tag, required)
|
||||||
if not found:
|
if not found:
|
||||||
return {}
|
return {}
|
||||||
@@ -418,7 +418,7 @@ class TafInputStream:
|
|||||||
result[k] = v
|
result[k] = v
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def read_list(self, tag: int, required: bool = False, item_reader=None) -> List:
|
def read_list(self, tag: int, required: bool = False, item_reader=None) -> list:
|
||||||
found = self._find_tag(tag, required)
|
found = self._find_tag(tag, required)
|
||||||
if not found:
|
if not found:
|
||||||
return []
|
return []
|
||||||
@@ -486,7 +486,7 @@ class TafStruct:
|
|||||||
def read_from(self, ins: TafInputStream):
|
def read_from(self, ins: TafInputStream):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> dict[str, Any]:
|
||||||
"""调试用:转字典"""
|
"""调试用:转字典"""
|
||||||
return {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
|
return {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import onnxruntime as ort
|
|||||||
from PIL import Image
|
from PIL import Image
|
||||||
from scipy.optimize import linear_sum_assignment
|
from scipy.optimize import linear_sum_assignment
|
||||||
|
|
||||||
|
|
||||||
MODEL_DIR = Path(__file__).resolve().parent / "models"
|
MODEL_DIR = Path(__file__).resolve().parent / "models"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,13 +17,12 @@ import cv2
|
|||||||
import execjs
|
import execjs
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import requests
|
import requests
|
||||||
from PIL import Image
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
from .ocr import HuyaCaptchaOcr, default_ocr
|
from .ocr import HuyaCaptchaOcr, default_ocr
|
||||||
from .track import format_track, generate_slide_track
|
from .track import format_track, generate_slide_track
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_UA = (
|
DEFAULT_UA = (
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||||
|
|||||||
+12
-12
@@ -21,14 +21,14 @@ import re
|
|||||||
import struct
|
import struct
|
||||||
import time
|
import time
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from typing import Any, Optional, Callable, cast
|
from collections.abc import Callable
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
import websockets
|
import websockets
|
||||||
|
|
||||||
|
from .frame_decoder import _decode_taf_struct, _truncate, format_wss_log
|
||||||
|
from .taf_protocol import TafInputStream, TafOutputStream, TafStruct, TafType
|
||||||
from .wup_protocol import WupRequest, WupResponse
|
from .wup_protocol import WupRequest, WupResponse
|
||||||
from .taf_protocol import TafOutputStream, TafInputStream, TafType, TafStruct
|
|
||||||
from .frame_decoder import format_wss_log, _decode_taf_struct, _truncate
|
|
||||||
|
|
||||||
|
|
||||||
SHOP_WS_HOST = "77bc035c-ws.va.huya.com"
|
SHOP_WS_HOST = "77bc035c-ws.va.huya.com"
|
||||||
# 商城端点 baseinfo (conn4, h5_/index.html) — 商城业务 shopMiddleUI 走此通道
|
# 商城端点 baseinfo (conn4, h5_/index.html) — 商城业务 shopMiddleUI 走此通道
|
||||||
@@ -225,7 +225,7 @@ class HuyaWssClient:
|
|||||||
),
|
),
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
except asyncio.TimeoutError:
|
except TimeoutError:
|
||||||
self.logger(f"[WSS] 连接超时({timeout}s)")
|
self.logger(f"[WSS] 连接超时({timeout}s)")
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -376,7 +376,7 @@ class HuyaWssClient:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
body = await asyncio.wait_for(future, timeout)
|
body = await asyncio.wait_for(future, timeout)
|
||||||
except asyncio.TimeoutError:
|
except TimeoutError:
|
||||||
if future in self.rpc_queue:
|
if future in self.rpc_queue:
|
||||||
self.rpc_queue.remove(future)
|
self.rpc_queue.remove(future)
|
||||||
self.logger("[✗] wsLaunch 超时")
|
self.logger("[✗] wsLaunch 超时")
|
||||||
@@ -526,7 +526,7 @@ class HuyaWssClient:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
body = await asyncio.wait_for(future, timeout)
|
body = await asyncio.wait_for(future, timeout)
|
||||||
except asyncio.TimeoutError:
|
except TimeoutError:
|
||||||
if future in self.rpc_queue:
|
if future in self.rpc_queue:
|
||||||
self.rpc_queue.remove(future)
|
self.rpc_queue.remove(future)
|
||||||
self.logger("[✗] getConfig 超时")
|
self.logger("[✗] getConfig 超时")
|
||||||
@@ -596,7 +596,7 @@ class HuyaWssClient:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
body = await asyncio.wait_for(future, timeout)
|
body = await asyncio.wait_for(future, timeout)
|
||||||
except asyncio.TimeoutError:
|
except TimeoutError:
|
||||||
if future in self.rpc_queue:
|
if future in self.rpc_queue:
|
||||||
self.rpc_queue.remove(future)
|
self.rpc_queue.remove(future)
|
||||||
self.logger(f"[✗] {service}.{method} 超时")
|
self.logger(f"[✗] {service}.{method} 超时")
|
||||||
@@ -696,11 +696,11 @@ class HuyaWssClient:
|
|||||||
order_type: int = 6,
|
order_type: int = 6,
|
||||||
):
|
):
|
||||||
from .shop_structs import (
|
from .shop_structs import (
|
||||||
CreateOrderReqV5,
|
CreateOrderAccountParam,
|
||||||
CreateOrderRsp,
|
|
||||||
CreateOrderExtraParam,
|
CreateOrderExtraParam,
|
||||||
CreateOrderPromotionParam,
|
CreateOrderPromotionParam,
|
||||||
CreateOrderAccountParam,
|
CreateOrderReqV5,
|
||||||
|
CreateOrderRsp,
|
||||||
)
|
)
|
||||||
|
|
||||||
req = CreateOrderReqV5()
|
req = CreateOrderReqV5()
|
||||||
@@ -781,7 +781,7 @@ class HuyaWssClient:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
body = await asyncio.wait_for(future, 15.0)
|
body = await asyncio.wait_for(future, 15.0)
|
||||||
except asyncio.TimeoutError:
|
except TimeoutError:
|
||||||
if future in self.rpc_queue:
|
if future in self.rpc_queue:
|
||||||
self.rpc_queue.remove(future)
|
self.rpc_queue.remove(future)
|
||||||
self.logger("[✗] payOrderSubmitV5 超时")
|
self.logger("[✗] payOrderSubmitV5 超时")
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import json
|
|||||||
import random
|
import random
|
||||||
import struct
|
import struct
|
||||||
import time as _time
|
import time as _time
|
||||||
from typing import Any, Dict
|
from typing import Any
|
||||||
|
|
||||||
# TAF 类型标签
|
# TAF 类型标签
|
||||||
INT8, INT16, INT32, INT64 = 0x00, 0x01, 0x02, 0x03
|
INT8, INT16, INT32, INT64 = 0x00, 0x01, 0x02, 0x03
|
||||||
@@ -97,7 +97,7 @@ class _Writer:
|
|||||||
|
|
||||||
def _build_meta_json(session: int, trace_id: str) -> str:
|
def _build_meta_json(session: int, trace_id: str) -> str:
|
||||||
"""构造 _wup_data.t0.t2 元数据 JSON。"""
|
"""构造 _wup_data.t0.t2 元数据 JSON。"""
|
||||||
meta: Dict[str, Any] = {
|
meta: dict[str, Any] = {
|
||||||
"associationId": 8193,
|
"associationId": 8193,
|
||||||
"funcName": "hypasswordLogin",
|
"funcName": "hypasswordLogin",
|
||||||
"group": 1,
|
"group": 1,
|
||||||
@@ -167,7 +167,7 @@ def _build_wup_data(
|
|||||||
session: int,
|
session: int,
|
||||||
trace_id: str,
|
trace_id: str,
|
||||||
user_action_json: str,
|
user_action_json: str,
|
||||||
device_info: Dict[str, str],
|
device_info: dict[str, str],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""编码 _wup_data struct。"""
|
"""编码 _wup_data struct。"""
|
||||||
meta_json = _build_meta_json(session, trace_id)
|
meta_json = _build_meta_json(session, trace_id)
|
||||||
@@ -235,7 +235,7 @@ def build_password_login_wup(
|
|||||||
session: int,
|
session: int,
|
||||||
trace_id: str,
|
trace_id: str,
|
||||||
user_action_json: str,
|
user_action_json: str,
|
||||||
device_info: Dict[str, str],
|
device_info: dict[str, str],
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
"""构造密码登录的 WUP TAF 请求体。"""
|
"""构造密码登录的 WUP TAF 请求体。"""
|
||||||
wd = _Writer()
|
wd = _Writer()
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ Wup 包结构:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import struct
|
import struct
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any
|
||||||
from .taf_protocol import TafOutputStream, TafInputStream, TafType, TafStruct
|
|
||||||
|
from .taf_protocol import TafInputStream, TafOutputStream, TafStruct, TafType
|
||||||
|
|
||||||
|
|
||||||
class WupRequest:
|
class WupRequest:
|
||||||
@@ -27,9 +28,9 @@ class WupRequest:
|
|||||||
self.sFuncName: str = "" # tag 6
|
self.sFuncName: str = "" # tag 6
|
||||||
self.sBuffer: bytes = b"" # tag 7
|
self.sBuffer: bytes = b"" # tag 7
|
||||||
self.iTimeout: int = 3000 # tag 8
|
self.iTimeout: int = 3000 # tag 8
|
||||||
self.context: Dict[str, str] = {} # tag 9
|
self.context: dict[str, str] = {} # tag 9
|
||||||
self.status: Dict[str, str] = {} # tag 10
|
self.status: dict[str, str] = {} # tag 10
|
||||||
self.newdata: Dict[str, bytes] = {}
|
self.newdata: dict[str, bytes] = {}
|
||||||
|
|
||||||
def setServant(self, name: str):
|
def setServant(self, name: str):
|
||||||
self.sServantName = name
|
self.sServantName = name
|
||||||
@@ -141,9 +142,9 @@ class WupResponse:
|
|||||||
self.sFuncName: str = ""
|
self.sFuncName: str = ""
|
||||||
self.sBuffer: bytes = b""
|
self.sBuffer: bytes = b""
|
||||||
self.iTimeout: int = 0
|
self.iTimeout: int = 0
|
||||||
self.context: Dict[str, str] = {}
|
self.context: dict[str, str] = {}
|
||||||
self.status: Dict[str, str] = {}
|
self.status: dict[str, str] = {}
|
||||||
self.newdata: Dict[str, bytes] = {}
|
self.newdata: dict[str, bytes] = {}
|
||||||
|
|
||||||
def decode(self, data: bytes):
|
def decode(self, data: bytes):
|
||||||
"""解码响应(不包含长度前缀;若含前缀会自动跳过)"""
|
"""解码响应(不包含长度前缀;若含前缀会自动跳过)"""
|
||||||
@@ -269,7 +270,7 @@ def _read_bytes_value(ins: TafInputStream, dtype: int) -> bytes:
|
|||||||
return ins.buf.read(length)
|
return ins.buf.read(length)
|
||||||
|
|
||||||
|
|
||||||
def _read_map_value(ins: TafInputStream, dtype: int) -> Dict:
|
def _read_map_value(ins: TafInputStream, dtype: int) -> dict:
|
||||||
if dtype != TafType.MAP:
|
if dtype != TafType.MAP:
|
||||||
return {}
|
return {}
|
||||||
count = ins._read_int_len()
|
count = ins._read_int_len()
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ from urllib.parse import urlparse
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
||||||
CODE_PATTERN = re.compile(
|
CODE_PATTERN = re.compile(
|
||||||
r"(?:verification\s+code|验证码|校验码|动态码|安全码)\D{0,20}(\d{4,8})",
|
r"(?:verification\s+code|验证码|校验码|动态码|安全码)\D{0,20}(\d{4,8})",
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ from sqlalchemy import JSON, MetaData, create_engine, func, inspect, select
|
|||||||
from sqlalchemy.engine import Connection, Engine
|
from sqlalchemy.engine import Connection, Engine
|
||||||
from sqlalchemy.schema import Table
|
from sqlalchemy.schema import Table
|
||||||
|
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||||
DEFAULT_SOURCE = PROJECT_ROOT / "data" / "web.db"
|
DEFAULT_SOURCE = PROJECT_ROOT / "data" / "web.db"
|
||||||
IGNORED_SOURCE_TABLES = {"alembic_version"}
|
IGNORED_SOURCE_TABLES = {"alembic_version"}
|
||||||
@@ -147,7 +146,7 @@ def main() -> int:
|
|||||||
if target_url:
|
if target_url:
|
||||||
os.environ["DATABASE_URL"] = target_url
|
os.environ["DATABASE_URL"] = target_url
|
||||||
from web.backend import models # noqa: F401
|
from web.backend import models # noqa: F401
|
||||||
from web.backend.database import Base, DATABASE_URL, run_migrations
|
from web.backend.database import DATABASE_URL, Base, run_migrations
|
||||||
|
|
||||||
target_url = target_url or DATABASE_URL
|
target_url = target_url or DATABASE_URL
|
||||||
if not target_url.startswith("mysql+"):
|
if not target_url.startswith("mysql+"):
|
||||||
|
|||||||
@@ -34,19 +34,20 @@ from pathlib import Path
|
|||||||
ROOT = Path(__file__).resolve().parent
|
ROOT = Path(__file__).resolve().parent
|
||||||
sys.path.insert(0, str(ROOT))
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
from pyvm.algorithm import build_plaintext, generate_encrypt_msg_offline # noqa: E402
|
from pyvm.algorithm import generate_encrypt_msg_offline
|
||||||
from pyvm.mall import MallSession, generate_encrypt_msg as mall_generate # noqa: E402
|
from pyvm.login_profile import midas_login_params
|
||||||
from pyvm.session import SessionState, load_session # noqa: E402
|
from pyvm.mall import MallSession
|
||||||
from pyvm.login_profile import midas_login_params # noqa: E402
|
from pyvm.mall import generate_encrypt_msg as mall_generate
|
||||||
from pyvm.payment_errors import describe_payment_failure # noqa: E402
|
from pyvm.payment_errors import describe_payment_failure
|
||||||
from pyvm.protocol import ( # noqa: E402
|
from pyvm.protocol import (
|
||||||
|
GOODS_USER_AGENT,
|
||||||
MALL_API_URL,
|
MALL_API_URL,
|
||||||
MALL_USER_AGENT,
|
MALL_USER_AGENT,
|
||||||
GOODS_USER_AGENT,
|
|
||||||
PAY_APPID,
|
PAY_APPID,
|
||||||
validate_goods_materials,
|
validate_goods_materials,
|
||||||
validate_mall_materials,
|
validate_mall_materials,
|
||||||
)
|
)
|
||||||
|
from pyvm.session import SessionState, load_session
|
||||||
|
|
||||||
REPLAY = ROOT / "replay"
|
REPLAY = ROOT / "replay"
|
||||||
DEFAULT_APPID = PAY_APPID
|
DEFAULT_APPID = PAY_APPID
|
||||||
|
|||||||
@@ -13,22 +13,24 @@ from .algorithm import (
|
|||||||
generate_encrypt_msg,
|
generate_encrypt_msg,
|
||||||
generate_encrypt_msg_offline,
|
generate_encrypt_msg_offline,
|
||||||
)
|
)
|
||||||
from .goods import GoodsSession, generate_encrypt_msg as goods_generate
|
from .goods import GoodsSession
|
||||||
from .mall import MallSession, generate_encrypt_msg as mall_generate
|
from .goods import generate_encrypt_msg as goods_generate
|
||||||
|
from .mall import MallSession
|
||||||
|
from .mall import generate_encrypt_msg as mall_generate
|
||||||
from .pagedoo_vm import PagedooVM, run_frame
|
from .pagedoo_vm import PagedooVM, run_frame
|
||||||
from .session import SessionState, load_session
|
from .session import SessionState, load_session
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"GoodsSession",
|
||||||
|
"MallSession",
|
||||||
|
"PagedooVM",
|
||||||
|
"SessionState",
|
||||||
"build_plaintext",
|
"build_plaintext",
|
||||||
"decode_d",
|
"decode_d",
|
||||||
"generate_encrypt_msg",
|
"generate_encrypt_msg",
|
||||||
"generate_encrypt_msg_offline",
|
"generate_encrypt_msg_offline",
|
||||||
"GoodsSession",
|
|
||||||
"goods_generate",
|
"goods_generate",
|
||||||
"MallSession",
|
|
||||||
"mall_generate",
|
|
||||||
"PagedooVM",
|
|
||||||
"run_frame",
|
|
||||||
"SessionState",
|
|
||||||
"load_session",
|
"load_session",
|
||||||
|
"mall_generate",
|
||||||
|
"run_frame",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
@@ -355,7 +356,7 @@ def js_set(obj, key, val):
|
|||||||
if isinstance(obj, str):
|
if isinstance(obj, str):
|
||||||
raise TypeError("Cannot assign to read only property")
|
raise TypeError("Cannot assign to read only property")
|
||||||
cur = getattr(
|
cur = getattr(
|
||||||
getattr(__import__("pyvm.algorithm", fromlist=["x"]), "VM"), "_last_u", None
|
__import__("pyvm.algorithm", fromlist=["x"]).VM, "_last_u", None
|
||||||
)
|
)
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
f"invalid set target obj={type(obj).__name__} key={key!r} val={type(val).__name__} u={cur}"
|
f"invalid set target obj={type(obj).__name__} key={key!r} val={type(val).__name__} u={cur}"
|
||||||
@@ -418,7 +419,7 @@ class Window:
|
|||||||
class JSFunction:
|
class JSFunction:
|
||||||
"""VM 函数(解释器实例工厂返回的 h)。"""
|
"""VM 函数(解释器实例工厂返回的 h)。"""
|
||||||
|
|
||||||
__slots__ = ("entry", "args", "s", "n", "t", "vm", "name")
|
__slots__ = ("args", "entry", "n", "name", "s", "t", "vm")
|
||||||
|
|
||||||
def __init__(self, vm, entry, args, s, n, t):
|
def __init__(self, vm, entry, args, s, n, t):
|
||||||
self.vm = vm
|
self.vm = vm
|
||||||
@@ -455,7 +456,7 @@ def js_call(fn, this, args):
|
|||||||
return fn.fn(this, *args)
|
return fn.fn(this, *args)
|
||||||
if callable(fn):
|
if callable(fn):
|
||||||
return fn(this, *args)
|
return fn(this, *args)
|
||||||
raise TypeError(f"{str(fn)} is not a function u={getattr(VM, '_last_u', None)}")
|
raise TypeError(f"{fn!s} is not a function u={getattr(VM, '_last_u', None)}")
|
||||||
|
|
||||||
|
|
||||||
def js_apply(fn, this, args):
|
def js_apply(fn, this, args):
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from .algorithm import generate_encrypt_msg_offline
|
from .algorithm import generate_encrypt_msg_offline
|
||||||
|
|
||||||
@@ -64,7 +63,7 @@ class GoodsSession:
|
|||||||
raise ValueError("key16/key1 应为 16 字节")
|
raise ValueError("key16/key1 应为 16 字节")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_session_state(cls, path: str | Path) -> "GoodsSession":
|
def from_session_state(cls, path: str | Path) -> GoodsSession:
|
||||||
"""从 scripts/capture-session.mjs 生成的 session-state.json 加载。"""
|
"""从 scripts/capture-session.mjs 生成的 session-state.json 加载。"""
|
||||||
d = json.loads(Path(path).read_text(encoding="utf-8"))
|
d = json.loads(Path(path).read_text(encoding="utf-8"))
|
||||||
return cls(
|
return cls(
|
||||||
@@ -85,7 +84,7 @@ class GoodsSession:
|
|||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls, d: dict) -> "GoodsSession":
|
def from_json(cls, d: dict) -> GoodsSession:
|
||||||
return cls(
|
return cls(
|
||||||
d["xmidas_ops"],
|
d["xmidas_ops"],
|
||||||
d["key16"],
|
d["key16"],
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import copy
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from .algorithm import UNDEF, JSObject, Window
|
from .algorithm import UNDEF, JSObject, Window
|
||||||
from .pagedoo_vm import PagedooVM
|
from .pagedoo_vm import PagedooVM
|
||||||
@@ -78,7 +77,7 @@ class MallSession:
|
|||||||
raise ValueError(f"624B 中间态长度 != 624: {len(mid[0])}")
|
raise ValueError(f"624B 中间态长度 != 624: {len(mid[0])}")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_capture_file(cls, frames_jsonl: str | Path) -> "MallSession":
|
def from_capture_file(cls, frames_jsonl: str | Path) -> MallSession:
|
||||||
"""从捕获的 frames.jsonl 提取同会话 transform_input + xMidasOps。
|
"""从捕获的 frames.jsonl 提取同会话 transform_input + xMidasOps。
|
||||||
|
|
||||||
frames.jsonl 由 scripts/capture-mall-session.mjs 实时落盘:
|
frames.jsonl 由 scripts/capture-mall-session.mjs 实时落盘:
|
||||||
@@ -127,7 +126,7 @@ class MallSession:
|
|||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls, d: dict) -> "MallSession":
|
def from_json(cls, d: dict) -> MallSession:
|
||||||
return cls(d["transform_input"], d["xmidas_ops"])
|
return cls(d["transform_input"], d["xmidas_ops"])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,56 +13,51 @@ JS 语义辅助复用 algorithm.py(_ic/i32/js_add/js_index/JSObject/JSFunction
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import math
|
|
||||||
import urllib.parse
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from .algorithm import (
|
from .algorithm import (
|
||||||
JSDate,
|
|
||||||
UNDEF,
|
UNDEF,
|
||||||
|
HostFunction,
|
||||||
|
JSDate,
|
||||||
JSFunction,
|
JSFunction,
|
||||||
JSObject,
|
JSObject,
|
||||||
HostFunction,
|
|
||||||
_ic,
|
_ic,
|
||||||
i32,
|
|
||||||
u32,
|
|
||||||
ushr,
|
|
||||||
shl,
|
|
||||||
shr,
|
|
||||||
js_typeof,
|
|
||||||
js_truthy,
|
|
||||||
js_str,
|
|
||||||
js_add,
|
|
||||||
js_num,
|
|
||||||
js_eq,
|
|
||||||
js_streq,
|
|
||||||
js_index,
|
|
||||||
js_set,
|
|
||||||
js_del,
|
|
||||||
js_keys,
|
|
||||||
js_call,
|
|
||||||
js_apply,
|
|
||||||
js_new,
|
|
||||||
h_math_floor,
|
|
||||||
h_math_round,
|
|
||||||
h_math_ceil,
|
|
||||||
h_math_min,
|
|
||||||
h_math_max,
|
|
||||||
h_math_abs,
|
|
||||||
h_math_pow,
|
|
||||||
h_math_sqrt,
|
|
||||||
h_parseint,
|
|
||||||
h_parsefloat,
|
|
||||||
h_isnan,
|
|
||||||
h_encodeuri,
|
|
||||||
h_encodeuricomponent,
|
|
||||||
h_decodeuri,
|
h_decodeuri,
|
||||||
h_decodeuricomponent,
|
h_decodeuricomponent,
|
||||||
h_string_fromcharcode,
|
h_encodeuri,
|
||||||
|
h_encodeuricomponent,
|
||||||
|
h_isnan,
|
||||||
|
h_math_abs,
|
||||||
|
h_math_ceil,
|
||||||
|
h_math_floor,
|
||||||
|
h_math_max,
|
||||||
|
h_math_min,
|
||||||
|
h_math_pow,
|
||||||
|
h_math_round,
|
||||||
|
h_math_sqrt,
|
||||||
h_new_date,
|
h_new_date,
|
||||||
|
h_parsefloat,
|
||||||
|
h_parseint,
|
||||||
|
h_string_fromcharcode,
|
||||||
|
i32,
|
||||||
|
js_add,
|
||||||
|
js_apply,
|
||||||
|
js_call,
|
||||||
|
js_del,
|
||||||
|
js_eq,
|
||||||
|
js_index,
|
||||||
|
js_keys,
|
||||||
|
js_new,
|
||||||
|
js_num,
|
||||||
|
js_set,
|
||||||
|
js_str,
|
||||||
|
js_truthy,
|
||||||
|
js_typeof,
|
||||||
|
shl,
|
||||||
|
shr,
|
||||||
|
ushr,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = ["PagedooVM", "run_frame", "REPLAY"]
|
__all__ = ["REPLAY", "PagedooVM", "run_frame"]
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- 辅助
|
# ---------------------------------------------------------------- 辅助
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import json
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
PAY_APPID = "1450243039"
|
PAY_APPID = "1450243039"
|
||||||
PAY_GOODS_URL = "https://pay.qq.com/midas/minipay_v2/views/cpay/goods.shtml"
|
PAY_GOODS_URL = "https://pay.qq.com/midas/minipay_v2/views/cpay/goods.shtml"
|
||||||
PAY_SAVE_URL = f"https://api.unipay.qq.com/v1/r/{PAY_APPID}/web_save"
|
PAY_SAVE_URL = f"https://api.unipay.qq.com/v1/r/{PAY_APPID}/web_save"
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class SessionState:
|
|||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls, d: dict) -> "SessionState":
|
def from_json(cls, d: dict) -> SessionState:
|
||||||
return cls(
|
return cls(
|
||||||
xmidas_ops=list(d.get("xmidas_ops", [])),
|
xmidas_ops=list(d.get("xmidas_ops", [])),
|
||||||
key16=list(d.get("key16", [])),
|
key16=list(d.get("key16", [])),
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ from pyvm.algorithm import (
|
|||||||
build_plaintext,
|
build_plaintext,
|
||||||
derive_key1_from_key16,
|
derive_key1_from_key16,
|
||||||
generate_encrypt_msg_offline,
|
generate_encrypt_msg_offline,
|
||||||
) # noqa: E402
|
)
|
||||||
from pyvm.login_profile import midas_login_params # noqa: E402
|
from pyvm.login_profile import midas_login_params
|
||||||
from pyvm.payment_errors import describe_payment_failure # noqa: E402
|
from pyvm.payment_errors import describe_payment_failure
|
||||||
from pyvm.protocol import ( # noqa: E402
|
from pyvm.protocol import (
|
||||||
DEFAULT_ORDER_PF,
|
DEFAULT_ORDER_PF,
|
||||||
GOODS_USER_AGENT,
|
GOODS_USER_AGENT,
|
||||||
PAY_APPID,
|
PAY_APPID,
|
||||||
@@ -450,7 +450,6 @@ def cmd_check_only(session_path: Path, out_dir: Path) -> int:
|
|||||||
from pyvm.order_status import (
|
from pyvm.order_status import (
|
||||||
completion_summary,
|
completion_summary,
|
||||||
get_official_orders,
|
get_official_orders,
|
||||||
is_finished,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
session = load_json(session_path)
|
session = load_json(session_path)
|
||||||
@@ -697,7 +696,7 @@ def main() -> int:
|
|||||||
out_dir, fields, int(fp.get("device_fp_length", 0)), plaintext_length
|
out_dir, fields, int(fp.get("device_fp_length", 0)), plaintext_length
|
||||||
)
|
)
|
||||||
body = urllib.parse.urlencode(fields)
|
body = urllib.parse.urlencode(fields)
|
||||||
from pyvm.order_status import order_completion_states, get_official_orders
|
from pyvm.order_status import get_official_orders, order_completion_states
|
||||||
|
|
||||||
# 尽量在发起支付前建立基线,缩小极快支付造成的检测窗口;失败不阻塞支付,付款码生成后重试。
|
# 尽量在发起支付前建立基线,缩小极快支付造成的检测窗口;失败不阻塞支付,付款码生成后重试。
|
||||||
baseline_document = None
|
baseline_document = None
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ from pathlib import Path
|
|||||||
from urllib.error import HTTPError
|
from urllib.error import HTTPError
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
from urllib.request import (
|
from urllib.request import (
|
||||||
HTTPRedirectHandler,
|
|
||||||
HTTPCookieProcessor,
|
HTTPCookieProcessor,
|
||||||
|
HTTPRedirectHandler,
|
||||||
Request,
|
Request,
|
||||||
build_opener,
|
build_opener,
|
||||||
)
|
)
|
||||||
@@ -43,7 +43,7 @@ USER_AGENT = (
|
|||||||
class NoRedirect(HTTPRedirectHandler):
|
class NoRedirect(HTTPRedirectHandler):
|
||||||
"""Keep OAuth redirects visible while CookieJar receives Set-Cookie headers."""
|
"""Keep OAuth redirects visible while CookieJar receives Set-Cookie headers."""
|
||||||
|
|
||||||
def redirect_request(self, req, fp, code, msg, headers, newurl): # noqa: D401
|
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ def g_tk(p_skey: str) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def parse_poll(body: str) -> tuple[int, str]:
|
def parse_poll(body: str) -> tuple[int, str]:
|
||||||
match = re.search(r"ptuiCB\((.*)\)", body, re.S)
|
match = re.search(r"ptuiCB\((.*)\)", body, re.DOTALL)
|
||||||
if not match:
|
if not match:
|
||||||
raise ValueError("QQ 二维码轮询响应缺少 ptuiCB")
|
raise ValueError("QQ 二维码轮询响应缺少 ptuiCB")
|
||||||
values = re.findall(r"'([^']*)'", match.group(1))
|
values = re.findall(r"'([^']*)'", match.group(1))
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ from pathlib import Path
|
|||||||
from urllib.error import HTTPError
|
from urllib.error import HTTPError
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
from urllib.request import (
|
from urllib.request import (
|
||||||
HTTPRedirectHandler,
|
|
||||||
HTTPCookieProcessor,
|
HTTPCookieProcessor,
|
||||||
|
HTTPRedirectHandler,
|
||||||
Request,
|
Request,
|
||||||
build_opener,
|
build_opener,
|
||||||
)
|
)
|
||||||
@@ -42,7 +42,7 @@ USER_AGENT = (
|
|||||||
class NoRedirect(HTTPRedirectHandler):
|
class NoRedirect(HTTPRedirectHandler):
|
||||||
"""保留 OAuth 回调的 302 和 Set-Cookie。"""
|
"""保留 OAuth 回调的 302 和 Set-Cookie。"""
|
||||||
|
|
||||||
def redirect_request(self, req, fp, code, msg, headers, newurl): # noqa: D401
|
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ def extract_uuid(page: str) -> str:
|
|||||||
r"connect/qrcode/([A-Za-z0-9_-]{8,64})",
|
r"connect/qrcode/([A-Za-z0-9_-]{8,64})",
|
||||||
r"uuid=([A-Za-z0-9_-]{8,64})",
|
r"uuid=([A-Za-z0-9_-]{8,64})",
|
||||||
):
|
):
|
||||||
match = re.search(pattern, page, re.I)
|
match = re.search(pattern, page, re.IGNORECASE)
|
||||||
if match:
|
if match:
|
||||||
return match.group(1)
|
return match.group(1)
|
||||||
raise ValueError("微信授权页未找到二维码 UUID")
|
raise ValueError("微信授权页未找到二维码 UUID")
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from curl_cffi import requests
|
|||||||
ROOT = Path(__file__).resolve().parent.parent
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
sys.path.insert(0, str(ROOT))
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
from pyvm.login_profile import midas_login_params # noqa: E402
|
from pyvm.login_profile import midas_login_params
|
||||||
|
|
||||||
YYB_APP_ID = 52575843
|
YYB_APP_ID = 52575843
|
||||||
SOURCE_ID = "24292013"
|
SOURCE_ID = "24292013"
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ WORKER_KEY = os.environ.get("YYB_WORKER_KEY", "")
|
|||||||
_jobs: dict[str, dict] = {}
|
_jobs: dict[str, dict] = {}
|
||||||
_lock = threading.Lock()
|
_lock = threading.Lock()
|
||||||
|
|
||||||
from pyvm.payment_errors import describe_payment_failure # noqa: E402
|
from pyvm.payment_errors import describe_payment_failure
|
||||||
|
|
||||||
|
|
||||||
def _payment_failure_message(job: dict, phase: str, fallback: str) -> str:
|
def _payment_failure_message(job: dict, phase: str, fallback: str) -> str:
|
||||||
@@ -81,13 +81,13 @@ def _safe_log(job: dict, line: str) -> None:
|
|||||||
clean = re.sub(r"HOLD_[A-Za-z0-9_-]+", "[订单已隐藏]", clean)
|
clean = re.sub(r"HOLD_[A-Za-z0-9_-]+", "[订单已隐藏]", clean)
|
||||||
clean = re.sub(r"(订单\s*[::]\s*)\S+", r"\1[订单已隐藏]", clean)
|
clean = re.sub(r"(订单\s*[::]\s*)\S+", r"\1[订单已隐藏]", clean)
|
||||||
clean = re.sub(
|
clean = re.sub(
|
||||||
r"(?:token|openid|openkey|cookie)=\S+", "[敏感字段已隐藏]", clean, flags=re.I
|
r"(?:token|openid|openkey|cookie)=\S+", "[敏感字段已隐藏]", clean, flags=re.IGNORECASE
|
||||||
)
|
)
|
||||||
clean = re.sub(
|
clean = re.sub(
|
||||||
r"(?:pay_token|web_token|anti_token|session_id|sessionid)\s*[:= ]\s*\S+",
|
r"(?:pay_token|web_token|anti_token|session_id|sessionid)\s*[:= ]\s*\S+",
|
||||||
"[敏感字段已隐藏]",
|
"[敏感字段已隐藏]",
|
||||||
clean,
|
clean,
|
||||||
flags=re.I,
|
flags=re.IGNORECASE,
|
||||||
)
|
)
|
||||||
timestamp = time.strftime("%H:%M:%S")
|
timestamp = time.strftime("%H:%M:%S")
|
||||||
with _lock:
|
with _lock:
|
||||||
@@ -523,7 +523,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
value = self.headers.get("Authorization", "")
|
value = self.headers.get("Authorization", "")
|
||||||
return value == f"Bearer {WORKER_KEY}"
|
return value == f"Bearer {WORKER_KEY}"
|
||||||
|
|
||||||
def do_POST(self) -> None: # noqa: N802
|
def do_POST(self) -> None:
|
||||||
if not self._authorized():
|
if not self._authorized():
|
||||||
return self._json(401, {"detail": "未授权"})
|
return self._json(401, {"detail": "未授权"})
|
||||||
path = urlparse(self.path).path.strip("/").split("/")
|
path = urlparse(self.path).path.strip("/").split("/")
|
||||||
@@ -687,7 +687,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
return self._json(500, {"detail": str(exc)})
|
return self._json(500, {"detail": str(exc)})
|
||||||
|
|
||||||
def do_GET(self) -> None: # noqa: N802
|
def do_GET(self) -> None:
|
||||||
if urlparse(self.path).path == "/health":
|
if urlparse(self.path).path == "/health":
|
||||||
return self._json(200, {"status": "ok"})
|
return self._json(200, {"status": "ok"})
|
||||||
if not self._authorized():
|
if not self._authorized():
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import unittest
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
|
|
||||||
|
|
||||||
_SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "jsdom-pay.py"
|
_SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "jsdom-pay.py"
|
||||||
_SPEC = importlib.util.spec_from_file_location("yyb_jsdom_pay", _SCRIPT)
|
_SPEC = importlib.util.spec_from_file_location("yyb_jsdom_pay", _SCRIPT)
|
||||||
assert _SPEC and _SPEC.loader
|
assert _SPEC and _SPEC.loader
|
||||||
@@ -16,8 +15,8 @@ _MODULE = importlib.util.module_from_spec(_SPEC)
|
|||||||
sys.modules[_SPEC.name] = _MODULE
|
sys.modules[_SPEC.name] = _MODULE
|
||||||
_SPEC.loader.exec_module(_MODULE)
|
_SPEC.loader.exec_module(_MODULE)
|
||||||
|
|
||||||
from pyvm.payment_errors import describe_payment_failure
|
|
||||||
from pyvm.algorithm import derive_key1_from_key16, generate_encrypt_msg_offline
|
from pyvm.algorithm import derive_key1_from_key16, generate_encrypt_msg_offline
|
||||||
|
from pyvm.payment_errors import describe_payment_failure
|
||||||
from pyvm.protocol import goods_material_diagnostics, validate_goods_materials
|
from pyvm.protocol import goods_material_diagnostics, validate_goods_materials
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
from fastapi import HTTPException
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from fastapi import HTTPException
|
|
||||||
|
|
||||||
from web.backend.database import Base
|
from web.backend.database import Base
|
||||||
from web.backend.models import Account, User
|
from web.backend.models import Account, User
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
"""充值审计日志的权限、查询和脱敏测试。"""
|
"""充值审计日志的权限、查询和脱敏测试。"""
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime, timedelta, timezone
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -17,7 +17,7 @@ class TestCustomCookieOrder:
|
|||||||
self.db.add(self.user)
|
self.db.add(self.user)
|
||||||
self.db.flush()
|
self.db.flush()
|
||||||
|
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(UTC)
|
||||||
accounts = {
|
accounts = {
|
||||||
name: Account(username=name, password="p", email="e", email_password="ep")
|
name: Account(username=name, password="p", email="e", email_password="ep")
|
||||||
for name in ("account-a", "account-b", "account-c")
|
for name in ("account-a", "account-b", "account-c")
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
import pytest
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
from web.backend.database import Base
|
from web.backend.database import Base
|
||||||
from web.backend.models import Account, LoginTask, ProxyConfig, User
|
from web.backend.models import Account, LoginTask, ProxyConfig, User
|
||||||
from web.backend.services.login_service import (
|
|
||||||
LoginBatchRunner,
|
|
||||||
cleanup_orphan_relogin_tasks,
|
|
||||||
)
|
|
||||||
from web.backend.services.cookie_check_service import check_douyu_cookie
|
|
||||||
from web.backend.routers.cookies import (
|
from web.backend.routers.cookies import (
|
||||||
check_cookie_operations,
|
check_cookie_operations,
|
||||||
get_cookie,
|
get_cookie,
|
||||||
@@ -21,6 +16,11 @@ from web.backend.routers.cookies import (
|
|||||||
list_cookies,
|
list_cookies,
|
||||||
relogin_invalid_cookie_operations,
|
relogin_invalid_cookie_operations,
|
||||||
)
|
)
|
||||||
|
from web.backend.services.cookie_check_service import check_douyu_cookie
|
||||||
|
from web.backend.services.login_service import (
|
||||||
|
LoginBatchRunner,
|
||||||
|
cleanup_orphan_relogin_tasks,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestCookieOperation:
|
class TestCookieOperation:
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""精英手册兑换:csrf 复用、新链路接口与浏览器状态机路由的测试。"""
|
"""精英手册兑换:csrf 复用、新链路接口与浏览器状态机路由的测试。"""
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from unittest.mock import Mock, call
|
from unittest.mock import Mock, call
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from core.douyu.activity_client import DouyuActivityClient, DouyuActivityError
|
from core.douyu.activity_client import DouyuActivityClient, DouyuActivityError
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import pytest
|
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from core.douyu.activity_client import DouyuActivityClient, DouyuActivityError
|
from core.douyu.activity_client import DouyuActivityClient, DouyuActivityError
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ from unittest.mock import ANY, AsyncMock, Mock, patch
|
|||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
from core.douyu import FishFinRechargeClient, FishFinRechargeConfig
|
||||||
from web.backend.database import Base
|
from web.backend.database import Base
|
||||||
from web.backend.models import Account, AuditLog, DouyuTask, LoginTask, User
|
from web.backend.models import Account, AuditLog, DouyuTask, LoginTask, User
|
||||||
from web.backend.routers.douyu import get_recharge_channel, supplier_recharge_callback
|
from web.backend.routers.douyu import get_recharge_channel, supplier_recharge_callback
|
||||||
|
from web.backend.services.audit_service import record_audit
|
||||||
from web.backend.services.douyu_runner import DouyuBatchRunner
|
from web.backend.services.douyu_runner import DouyuBatchRunner
|
||||||
from web.backend.services.douyu_service import ensure_douyu_config
|
from web.backend.services.douyu_service import ensure_douyu_config
|
||||||
from web.backend.services.audit_service import record_audit
|
|
||||||
from core.douyu import FishFinRechargeClient, FishFinRechargeConfig
|
|
||||||
|
|
||||||
|
|
||||||
class TestDouyuGoldRechargeChannel:
|
class TestDouyuGoldRechargeChannel:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import Mock
|
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
from core.douyu.login import DouyuLogin
|
from core.douyu.login import DouyuLogin
|
||||||
from core.douyu.login_api_wgapi import WgapiLoginAPI
|
from core.douyu.login_api_wgapi import WgapiLoginAPI
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
from core.douyu.activity_client import DouyuActivityClient
|
from core.douyu.activity_client import DouyuActivityClient
|
||||||
|
from web.backend.models import ProxyConfig
|
||||||
from web.backend.services.douyu_runner import DouyuBatchRunner
|
from web.backend.services.douyu_runner import DouyuBatchRunner
|
||||||
from web.backend.services.douyu_runner_core import (
|
from web.backend.services.douyu_runner_core import (
|
||||||
DouyuBatchRunnerCore,
|
|
||||||
DOUYU_PROXY_TASK_TYPES,
|
DOUYU_PROXY_TASK_TYPES,
|
||||||
|
DouyuBatchRunnerCore,
|
||||||
)
|
)
|
||||||
from web.backend.models import ProxyConfig
|
|
||||||
|
|
||||||
|
|
||||||
def _cfg(**overrides) -> ProxyConfig:
|
def _cfg(**overrides) -> ProxyConfig:
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sqlalchemy import create_engine, event
|
from sqlalchemy import create_engine, event
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import pytest
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from core.douyu.activity_client import DouyuActivityClient, DouyuActivityError
|
from core.douyu.activity_client import DouyuActivityClient, DouyuActivityError
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""鱼翅直充供应商 API 客户端测试。"""
|
"""鱼翅直充供应商 API 客户端测试。"""
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from core.douyu.recharge_api import (
|
from core.douyu.recharge_api import (
|
||||||
FishFinRechargeClient,
|
FishFinRechargeClient,
|
||||||
FishFinRechargeConfig,
|
FishFinRechargeConfig,
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
"""虎牙 App 密码登录及相关组件测试。"""
|
"""虎牙 App 密码登录及相关组件测试。"""
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import os
|
import os
|
||||||
import struct
|
import struct
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
from core.huya import (
|
from core.huya import (
|
||||||
HuyaAppLoginError,
|
HuyaAppLoginError,
|
||||||
HuyaAppPasswordLogin,
|
|
||||||
HuyaAppQrAuthRequiredError,
|
|
||||||
login_huya_app_password,
|
|
||||||
login_huya_password,
|
|
||||||
)
|
)
|
||||||
from core.huya.app_login import (
|
from core.huya.app_login import (
|
||||||
DEFAULT_GOLDEN_DEV,
|
DEFAULT_GOLDEN_DEV,
|
||||||
@@ -30,17 +24,16 @@ from core.huya.login import HuyaLoginResult
|
|||||||
from core.huya.nonce_forge import K1_DEFAULT, gen_nonce
|
from core.huya.nonce_forge import K1_DEFAULT, gen_nonce
|
||||||
from core.huya.udb_aes import udb_decrypt, udb_encrypt
|
from core.huya.udb_aes import udb_decrypt, udb_encrypt
|
||||||
from core.huya.wup_encoder import build_password_login_wup
|
from core.huya.wup_encoder import build_password_login_wup
|
||||||
|
|
||||||
from web.backend.database import Base
|
from web.backend.database import Base
|
||||||
from web.backend.models import User, HuyaAccount
|
from web.backend.models import HuyaAccount, User
|
||||||
from web.backend.schemas import (
|
|
||||||
HuyaAppPasswordLoginRequest,
|
|
||||||
HuyaPasswordLoginSelectedRequest,
|
|
||||||
)
|
|
||||||
from web.backend.routers.huya import (
|
from web.backend.routers.huya import (
|
||||||
app_password_login_account,
|
app_password_login_account,
|
||||||
app_password_login_selected_accounts,
|
app_password_login_selected_accounts,
|
||||||
)
|
)
|
||||||
|
from web.backend.schemas import (
|
||||||
|
HuyaAppPasswordLoginRequest,
|
||||||
|
HuyaPasswordLoginSelectedRequest,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestHuyaAppLogin:
|
class TestHuyaAppLogin:
|
||||||
@@ -193,9 +186,8 @@ class TestHuyaAppLogin:
|
|||||||
with patch(
|
with patch(
|
||||||
"core.huya.app_login.register_device",
|
"core.huya.app_login.register_device",
|
||||||
side_effect=DfpRegistrationError("注册链 HTTP 500"),
|
side_effect=DfpRegistrationError("注册链 HTTP 500"),
|
||||||
):
|
), pytest.raises(HuyaAppLoginError, match="注册失败"):
|
||||||
with pytest.raises(HuyaAppLoginError, match="注册失败"):
|
login_cred_with_flow("300023887", "pw")
|
||||||
login_cred_with_flow("300023887", "pw")
|
|
||||||
|
|
||||||
def test_router_functions(self):
|
def test_router_functions(self):
|
||||||
mock_res = HuyaLoginResult(
|
mock_res = HuyaLoginResult(
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ from datetime import datetime, timedelta
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from utils.logger import (
|
from utils.logger import (
|
||||||
|
_parse_size,
|
||||||
_SensitiveDataFilter,
|
_SensitiveDataFilter,
|
||||||
_SizeAndDayRotatingFileHandler,
|
_SizeAndDayRotatingFileHandler,
|
||||||
_parse_size,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ class TestMigrationSmoke:
|
|||||||
|
|
||||||
def test_full_chain_applies_on_fresh_sqlite_and_matches_models(self):
|
def test_full_chain_applies_on_fresh_sqlite_and_matches_models(self):
|
||||||
"""空库执行整条迁移链,校验表/列/索引与模型元数据对齐。"""
|
"""空库执行整条迁移链,校验表/列/索引与模型元数据对齐。"""
|
||||||
from web.backend.database import Base
|
|
||||||
import web.backend.database as database_module
|
import web.backend.database as database_module
|
||||||
|
from web.backend.database import Base
|
||||||
|
|
||||||
original_url = database_module.DATABASE_URL
|
original_url = database_module.DATABASE_URL
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
|||||||
+3
-4
@@ -14,7 +14,6 @@ from pathlib import Path
|
|||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
_LOG_LEVELS = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
|
_LOG_LEVELS = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
|
||||||
_DEFAULT_RETENTION_DAYS = 14
|
_DEFAULT_RETENTION_DAYS = 14
|
||||||
_DEFAULT_ROTATION_SIZE = 50 * 1024 * 1024
|
_DEFAULT_ROTATION_SIZE = 50 * 1024 * 1024
|
||||||
@@ -36,7 +35,7 @@ def _parse_positive_int(value: str | None, default: int) -> int:
|
|||||||
|
|
||||||
def _parse_size(value: str | None, default: int = _DEFAULT_ROTATION_SIZE) -> int:
|
def _parse_size(value: str | None, default: int = _DEFAULT_ROTATION_SIZE) -> int:
|
||||||
"""解析 50M、1GiB 等易读大小;非法值保持安全默认值。"""
|
"""解析 50M、1GiB 等易读大小;非法值保持安全默认值。"""
|
||||||
matched = re.fullmatch(r"\s*(\d+)\s*([kmgt]?i?b?)?\s*", str(value or ""), re.I)
|
matched = re.fullmatch(r"\s*(\d+)\s*([kmgt]?i?b?)?\s*", str(value or ""), re.IGNORECASE)
|
||||||
if not matched:
|
if not matched:
|
||||||
return default
|
return default
|
||||||
amount = int(matched.group(1))
|
amount = int(matched.group(1))
|
||||||
@@ -113,7 +112,7 @@ class _SizeAndDayRotatingFileHandler(BaseRotatingHandler):
|
|||||||
self._log_dir / f"{self._filename_prefix}-{day.isoformat()}{self._suffix}"
|
self._log_dir / f"{self._filename_prefix}-{day.isoformat()}{self._suffix}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def shouldRollover(self, record: logging.LogRecord) -> bool: # noqa: N802
|
def shouldRollover(self, record: logging.LogRecord) -> bool:
|
||||||
if datetime.now().date() != self._active_day:
|
if datetime.now().date() != self._active_day:
|
||||||
return True
|
return True
|
||||||
if self.stream is None:
|
if self.stream is None:
|
||||||
@@ -122,7 +121,7 @@ class _SizeAndDayRotatingFileHandler(BaseRotatingHandler):
|
|||||||
message = f"{self.format(record)}\n"
|
message = f"{self.format(record)}\n"
|
||||||
return self.stream.tell() + len(message.encode("utf-8")) >= self.max_bytes
|
return self.stream.tell() + len(message.encode("utf-8")) >= self.max_bytes
|
||||||
|
|
||||||
def doRollover(self) -> None: # noqa: N802
|
def doRollover(self) -> None:
|
||||||
if self.stream is not None:
|
if self.stream is not None:
|
||||||
self.stream.close()
|
self.stream.close()
|
||||||
self.stream = None
|
self.stream = None
|
||||||
|
|||||||
@@ -10,11 +10,10 @@ from dataclasses import dataclass
|
|||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
|
||||||
from Crypto.Cipher import AES
|
from Crypto.Cipher import AES
|
||||||
|
from loguru import logger
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from sqlalchemy.engine import Engine
|
from sqlalchemy.engine import Engine
|
||||||
from sqlalchemy.types import Text, TypeDecorator
|
from sqlalchemy.types import Text, TypeDecorator
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
|
|
||||||
_PREFIX = "enc:v1:"
|
_PREFIX = "enc:v1:"
|
||||||
_FALLBACK_SECRET = "douyu-login-py-dev-storage-key-change-me"
|
_FALLBACK_SECRET = "douyu-login-py-dev-storage-key-change-me"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
from sqlalchemy import create_engine, event
|
from sqlalchemy import create_engine, event
|
||||||
from sqlalchemy.orm import sessionmaker, declarative_base
|
from sqlalchemy.orm import declarative_base, sessionmaker
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
@@ -147,6 +147,7 @@ def _seed():
|
|||||||
def _encrypt_existing_sensitive_data():
|
def _encrypt_existing_sensitive_data():
|
||||||
"""启动时把历史明文敏感数据迁移为密文。"""
|
"""启动时把历史明文敏感数据迁移为密文。"""
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from .crypto_storage import encrypt_existing_sensitive_data
|
from .crypto_storage import encrypt_existing_sensitive_data
|
||||||
|
|
||||||
changed = encrypt_existing_sensitive_data(engine)
|
changed = encrypt_existing_sensitive_data(engine)
|
||||||
|
|||||||
+9
-9
@@ -1,15 +1,15 @@
|
|||||||
"""FastAPI 依赖注入"""
|
"""FastAPI 依赖注入"""
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
from fastapi import Depends, HTTPException, Request, status, WebSocket
|
|
||||||
from fastapi.security import OAuth2PasswordBearer
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from jose import JWTError
|
|
||||||
|
|
||||||
from .database import get_db, SessionLocal
|
from fastapi import Depends, HTTPException, Request, WebSocket, status
|
||||||
from .security import decode_access_token
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
|
from jose import JWTError
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from .database import SessionLocal, get_db
|
||||||
from .models import User
|
from .models import User
|
||||||
from .permissions import get_user_permissions
|
from .permissions import get_user_permissions
|
||||||
|
from .security import decode_access_token
|
||||||
|
|
||||||
# auto_error=False: 允许 token 为空(后续从 cookie 读取)
|
# auto_error=False: 允许 token 为空(后续从 cookie 读取)
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login", auto_error=False)
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login", auto_error=False)
|
||||||
@@ -17,7 +17,7 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login", auto_error=Fals
|
|||||||
|
|
||||||
def get_current_user(
|
def get_current_user(
|
||||||
request: Request,
|
request: Request,
|
||||||
token: Optional[str] = Depends(oauth2_scheme),
|
token: str | None = Depends(oauth2_scheme),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
) -> User:
|
) -> User:
|
||||||
credentials_exc = HTTPException(
|
credentials_exc = HTTPException(
|
||||||
@@ -61,7 +61,7 @@ def require_permission(permission: str):
|
|||||||
return checker
|
return checker
|
||||||
|
|
||||||
|
|
||||||
def authenticate_websocket(websocket: WebSocket) -> Optional[User]:
|
def authenticate_websocket(websocket: WebSocket) -> User | None:
|
||||||
"""WebSocket 认证:从 cookie 或 query param token 中验证用户身份。
|
"""WebSocket 认证:从 cookie 或 query param token 中验证用户身份。
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|||||||
+15
-13
@@ -1,34 +1,36 @@
|
|||||||
"""FastAPI 入口"""
|
"""FastAPI 入口"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
import uvicorn
|
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.middleware.gzip import GZipMiddleware
|
from fastapi.middleware.gzip import GZipMiddleware
|
||||||
from fastapi.staticfiles import StaticFiles
|
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
from starlette.middleware.base import BaseHTTPMiddleware
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
|
|
||||||
|
from utils import setup_logger
|
||||||
|
|
||||||
from .database import init_db
|
from .database import init_db
|
||||||
from .routers import (
|
from .routers import (
|
||||||
auth,
|
|
||||||
users,
|
|
||||||
accounts,
|
|
||||||
account_check,
|
account_check,
|
||||||
|
accounts,
|
||||||
|
audit,
|
||||||
|
auth,
|
||||||
|
cookies,
|
||||||
dashboard,
|
dashboard,
|
||||||
|
douyu,
|
||||||
|
huya,
|
||||||
login,
|
login,
|
||||||
proxy,
|
proxy,
|
||||||
cookies,
|
users,
|
||||||
huya,
|
|
||||||
douyu,
|
|
||||||
yyb,
|
yyb,
|
||||||
audit,
|
|
||||||
)
|
)
|
||||||
from .schemas import AppInfo
|
from .schemas import AppInfo
|
||||||
from .version import get_app_version
|
from .version import get_app_version
|
||||||
from utils import setup_logger
|
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -45,9 +47,9 @@ async def lifespan(app: FastAPI):
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from .database import SessionLocal
|
from .database import SessionLocal
|
||||||
from .services.login_service import cleanup_orphan_relogin_tasks
|
|
||||||
from .services.huya_service import cleanup_orphan_huya_tasks
|
|
||||||
from .services.douyu_service import cleanup_orphan_douyu_tasks
|
from .services.douyu_service import cleanup_orphan_douyu_tasks
|
||||||
|
from .services.huya_service import cleanup_orphan_huya_tasks
|
||||||
|
from .services.login_service import cleanup_orphan_relogin_tasks
|
||||||
|
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""Alembic 迁移环境。"""
|
"""Alembic 迁移环境。"""
|
||||||
|
|
||||||
from logging.config import fileConfig
|
|
||||||
from pathlib import Path
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from logging.config import fileConfig
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from alembic import context
|
from alembic import context
|
||||||
from sqlalchemy import engine_from_config, pool
|
from sqlalchemy import engine_from_config, pool
|
||||||
@@ -12,8 +12,8 @@ ROOT_DIR = Path(__file__).resolve().parents[3]
|
|||||||
if str(ROOT_DIR) not in sys.path:
|
if str(ROOT_DIR) not in sys.path:
|
||||||
sys.path.insert(0, str(ROOT_DIR))
|
sys.path.insert(0, str(ROOT_DIR))
|
||||||
|
|
||||||
from web.backend.database import Base, DATABASE_URL # noqa: E402
|
from web.backend import models # noqa: F401
|
||||||
from web.backend import models # noqa: F401,E402
|
from web.backend.database import DATABASE_URL, Base
|
||||||
|
|
||||||
config = context.config
|
config = context.config
|
||||||
config.set_main_option("sqlalchemy.url", DATABASE_URL)
|
config.set_main_option("sqlalchemy.url", DATABASE_URL)
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ Revises:
|
|||||||
Create Date: 2026-06-23
|
Create Date: 2026-06-23
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
revision: str = "20260623_0001"
|
revision: str = "20260623_0001"
|
||||||
down_revision: Union[str, None] = None
|
down_revision: str | None = None
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
def _has_table(bind, table_name: str) -> bool:
|
def _has_table(bind, table_name: str) -> bool:
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ Revises: 20260623_0001
|
|||||||
Create Date: 2026-06-24
|
Create Date: 2026-06-24
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
revision: str = "20260624_0002"
|
revision: str = "20260624_0002"
|
||||||
down_revision: Union[str, None] = "20260623_0001"
|
down_revision: str | None = "20260623_0001"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ Revises: 20260624_0002
|
|||||||
Create Date: 2026-06-24
|
Create Date: 2026-06-24
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
revision: str = "20260624_0003"
|
revision: str = "20260624_0003"
|
||||||
down_revision: Union[str, None] = "20260624_0002"
|
down_revision: str | None = "20260624_0002"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ Revises: 20260624_0003
|
|||||||
Create Date: 2026-07-04
|
Create Date: 2026-07-04
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
revision: str = "20260704_0004"
|
revision: str = "20260704_0004"
|
||||||
down_revision: Union[str, None] = "20260624_0003"
|
down_revision: str | None = "20260624_0003"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
def _has_table(bind, table_name: str) -> bool:
|
def _has_table(bind, table_name: str) -> bool:
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ Revises: 20260704_0004
|
|||||||
Create Date: 2026-07-04
|
Create Date: 2026-07-04
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
revision: str = "20260704_0005"
|
revision: str = "20260704_0005"
|
||||||
down_revision: Union[str, None] = "20260704_0004"
|
down_revision: str | None = "20260704_0004"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
def _has_table(bind, table_name: str) -> bool:
|
def _has_table(bind, table_name: str) -> bool:
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ Revises: 20260704_0005
|
|||||||
Create Date: 2026-07-05
|
Create Date: 2026-07-05
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
revision: str = "20260705_0006"
|
revision: str = "20260705_0006"
|
||||||
down_revision: Union[str, None] = "20260704_0005"
|
down_revision: str | None = "20260704_0005"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
def _has_table(bind, table_name: str) -> bool:
|
def _has_table(bind, table_name: str) -> bool:
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ Revises: 20260705_0006
|
|||||||
Create Date: 2026-07-12
|
Create Date: 2026-07-12
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
revision: str = "20260712_0007"
|
revision: str = "20260712_0007"
|
||||||
down_revision: Union[str, None] = "20260705_0006"
|
down_revision: str | None = "20260705_0006"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
def _has_table(bind, table_name: str) -> bool:
|
def _has_table(bind, table_name: str) -> bool:
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ Revises: 20260712_0007
|
|||||||
Create Date: 2026-07-24
|
Create Date: 2026-07-24
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
revision: str = "20260724_0008"
|
revision: str = "20260724_0008"
|
||||||
down_revision: Union[str, None] = "20260712_0007"
|
down_revision: str | None = "20260712_0007"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
def _has_table(bind, table_name: str) -> bool:
|
def _has_table(bind, table_name: str) -> bool:
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ Revises: 20260724_0008
|
|||||||
Create Date: 2026-07-25
|
Create Date: 2026-07-25
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
revision: str = "20260725_0009"
|
revision: str = "20260725_0009"
|
||||||
down_revision: Union[str, None] = "20260724_0008"
|
down_revision: str | None = "20260724_0008"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
def _has_table(bind, table_name: str) -> bool:
|
def _has_table(bind, table_name: str) -> bool:
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ Revises: 20260725_0009
|
|||||||
Create Date: 2026-07-28
|
Create Date: 2026-07-28
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
revision: str = "20260728_0010"
|
revision: str = "20260728_0010"
|
||||||
down_revision: Union[str, None] = "20260725_0009"
|
down_revision: str | None = "20260725_0009"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
def _columns(bind, table_name: str) -> set[str]:
|
def _columns(bind, table_name: str) -> set[str]:
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ Revises: 20260728_0010
|
|||||||
Create Date: 2026-07-28
|
Create Date: 2026-07-28
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
revision: str = "20260728_0011"
|
revision: str = "20260728_0011"
|
||||||
down_revision: Union[str, None] = "20260728_0010"
|
down_revision: str | None = "20260728_0010"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
def _columns(bind, table_name: str) -> set[str]:
|
def _columns(bind, table_name: str) -> set[str]:
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ Revises: 20260728_0011
|
|||||||
Create Date: 2026-07-28
|
Create Date: 2026-07-28
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
revision: str = "20260728_0012"
|
revision: str = "20260728_0012"
|
||||||
down_revision: Union[str, None] = "20260728_0011"
|
down_revision: str | None = "20260728_0011"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ Revises: 20260728_0012
|
|||||||
Create Date: 2026-07-28
|
Create Date: 2026-07-28
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
revision: str = "20260728_0013"
|
revision: str = "20260728_0013"
|
||||||
down_revision: Union[str, None] = "20260728_0012"
|
down_revision: str | None = "20260728_0012"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
def _columns(bind, table_name: str) -> set[str]:
|
def _columns(bind, table_name: str) -> set[str]:
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ Revises: 20260728_0013
|
|||||||
Create Date: 2026-07-28
|
Create Date: 2026-07-28
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
revision: str = "20260728_0014"
|
revision: str = "20260728_0014"
|
||||||
down_revision: Union[str, None] = "20260728_0013"
|
down_revision: str | None = "20260728_0013"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ Revises: 20260728_0014
|
|||||||
Create Date: 2026-08-05
|
Create Date: 2026-08-05
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
revision: str = "20260805_0015"
|
revision: str = "20260805_0015"
|
||||||
down_revision: Union[str, None] = "20260728_0014"
|
down_revision: str | None = "20260728_0014"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
INDEXES = [
|
INDEXES = [
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ Revises: 20260805_0015
|
|||||||
Create Date: 2026-08-06
|
Create Date: 2026-08-06
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
revision: str = "20260806_0016"
|
revision: str = "20260806_0016"
|
||||||
down_revision: Union[str, None] = "20260805_0015"
|
down_revision: str | None = "20260805_0015"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
def _columns(bind, table_name: str) -> set[str]:
|
def _columns(bind, table_name: str) -> set[str]:
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ Revises: 20260806_0016
|
|||||||
Create Date: 2026-08-07
|
Create Date: 2026-08-07
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
revision: str = "20260807_0017"
|
revision: str = "20260807_0017"
|
||||||
down_revision: Union[str, None] = "20260806_0016"
|
down_revision: str | None = "20260806_0016"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ Revises: 20260807_0017
|
|||||||
Create Date: 2026-08-07
|
Create Date: 2026-08-07
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
revision: str = "20260807_0018"
|
revision: str = "20260807_0018"
|
||||||
down_revision: Union[str, None] = "20260807_0017"
|
down_revision: str | None = "20260807_0017"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ Revises: 20260807_0018
|
|||||||
Create Date: 2026-08-08
|
Create Date: 2026-08-08
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
revision: str = "20260808_0019"
|
revision: str = "20260808_0019"
|
||||||
down_revision: Union[str, None] = "20260807_0018"
|
down_revision: str | None = "20260807_0018"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
"""增加应用宝和平精英充值任务"""
|
"""增加应用宝和平精英充值任务"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
revision: str = "20260812_0020"
|
revision: str = "20260812_0020"
|
||||||
down_revision: Union[str, None] = "20260808_0019"
|
down_revision: str | None = "20260808_0019"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
"""扩展应用宝二维码密文字段容量"""
|
"""扩展应用宝二维码密文字段容量"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
revision: str = "20260812_0021"
|
revision: str = "20260812_0021"
|
||||||
down_revision: Union[str, None] = "20260812_0020"
|
down_revision: str | None = "20260812_0020"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
def _is_mysql() -> bool:
|
def _is_mysql() -> bool:
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
"""应用宝支付元数据字段:金额与支付时间线"""
|
"""应用宝支付元数据字段:金额与支付时间线"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
revision: str = "20260812_0022"
|
revision: str = "20260812_0022"
|
||||||
down_revision: Union[str, None] = "20260812_0021"
|
down_revision: str | None = "20260812_0021"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
"""支持用户软删除并保留历史任务归属"""
|
"""支持用户软删除并保留历史任务归属"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
revision: str = "20260813_0023"
|
revision: str = "20260813_0023"
|
||||||
down_revision: Union[str, None] = "20260812_0022"
|
down_revision: str | None = "20260812_0022"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
def _columns(bind, table_name: str) -> set[str]:
|
def _columns(bind, table_name: str) -> set[str]:
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ Revises: 20260813_0023
|
|||||||
Create Date: 2026-08-13
|
Create Date: 2026-08-13
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
revision: str = "20260813_0024"
|
revision: str = "20260813_0024"
|
||||||
down_revision: Union[str, None] = "20260813_0023"
|
down_revision: str | None = "20260813_0023"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
def _columns(bind, table_name: str) -> set[str]:
|
def _columns(bind, table_name: str) -> set[str]:
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ Revises: 20260813_0024
|
|||||||
Create Date: 2026-08-13
|
Create Date: 2026-08-13
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
revision: str = "20260813_0025"
|
revision: str = "20260813_0025"
|
||||||
down_revision: Union[str, None] = "20260813_0024"
|
down_revision: str | None = "20260813_0024"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user