完成 Ruff 全量清理

This commit is contained in:
yml2213
2026-08-31 10:55:44 +08:00
parent 840af3108e
commit 09ab80e062
68 changed files with 323 additions and 291 deletions
+8 -13
View File
@@ -2,21 +2,16 @@
## Ruff lint 清理 ## Ruff lint 清理
记录日期:2026-08-30 状态:已完成
`uv run ruff check .` 当前发现 1263 条 lint 诊断。该事项暂不在本次处理,后续按批次清理并逐批运行 pytest、Pyright 和 Ruff 检查。 完成日期:2026-08-31
建议顺序: 本轮完成了 Ruff lint 全量清理,包括未使用导入与变量、导入排序、类型注解现代化、FastAPI 依赖声明、异常处理标注、时区处理及其他规则。FastAPI 路由参数中的依赖调用通过 `web/backend/**/*.py` 的 B008 配置例外保留框架惯用写法;非 FastAPI 的可变默认对象已改为函数内惰性初始化。
1. `F401``F841`:未使用导入和变量。 最终基线:
2. `BLE001``S110``S112`:异常处理质量。
3. `B008`:FastAPI 依赖声明模式,区分真实问题与框架惯用写法。
4. `I001`:导入排序。
5. `UP045``UP007``UP017``UP035``UP006`:类型注解和 Python 版本语法现代化。
当前基线: - Ruff lint0 条
- Ruff format208 个文件全部通过
- Ruff lint1263 条 - pytest105 passed
- Ruff format207 个文件全部通过
- pytest104 passed
- Pyright0 errors / 0 warnings - Pyright0 errors / 0 warnings
- Python compileall:通过
+1 -1
View File
@@ -44,7 +44,7 @@ class CookieEnricher:
self.generate_acf_ccn_cookie() self.generate_acf_ccn_cookie()
logger.info("补CK完成") logger.info("补CK完成")
return return
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
last_error = e last_error = e
logger.warning(f"补CK失败 {attempt}/{max_attempts}: {e}") logger.warning(f"补CK失败 {attempt}/{max_attempts}: {e}")
if attempt < max_attempts: if attempt < max_attempts:
+17 -13
View File
@@ -5,7 +5,7 @@ import os
import re import re
import threading import threading
import time import time
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
import requests import requests
from loguru import logger from loguru import logger
@@ -68,8 +68,8 @@ class EmailVerifier:
f"{self.roundcube_url}?_task=logout", f"{self.roundcube_url}?_task=logout",
timeout=5, timeout=5,
) )
except Exception: except Exception as exc: # noqa: BLE001
pass logger.debug(f"Roundcube logout failed: {exc}")
self._rc_session = None self._rc_session = None
self._rc_logged_in = False self._rc_logged_in = False
@@ -176,7 +176,7 @@ class EmailVerifier:
logger.debug(f"Roundcube: {password_name}登录成功 ({self.username})") logger.debug(f"Roundcube: {password_name}登录成功 ({self.username})")
return True, False return True, False
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
logger.warning(f"Roundcube: 登录异常: {e}") logger.warning(f"Roundcube: 登录异常: {e}")
return False, False return False, False
@@ -230,7 +230,7 @@ class EmailVerifier:
return messages return messages
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
logger.warning(f"Roundcube: 获取邮件列表失败: {e}") logger.warning(f"Roundcube: 获取邮件列表失败: {e}")
return [] return []
@@ -252,7 +252,7 @@ class EmailVerifier:
) )
return resp.text return resp.text
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
logger.warning(f"Roundcube: 读取邮件 {uid} 失败: {e}") logger.warning(f"Roundcube: 读取邮件 {uid} 失败: {e}")
return None return None
@@ -270,7 +270,7 @@ class EmailVerifier:
- "2026-06-20" → 直接解析 - "2026-06-20" → 直接解析
- "06-20" → 今年的该日期 - "06-20" → 今年的该日期
""" """
now = datetime.now() now = datetime.now(UTC)
# 今天 # 今天
if date_str.startswith("今天"): if date_str.startswith("今天"):
@@ -322,32 +322,36 @@ class EmailVerifier:
# 日期格式 2026-06-20 # 日期格式 2026-06-20
try: try:
return datetime.strptime(date_str.strip(), "%Y-%m-%d") return datetime.strptime(date_str.strip(), "%Y-%m-%d").replace(tzinfo=UTC)
except ValueError: except ValueError:
pass pass
# 日期格式 2026-06-20 14:30 # 日期格式 2026-06-20 14:30
try: try:
return datetime.strptime(date_str.strip(), "%Y-%m-%d %H:%M") return datetime.strptime(date_str.strip(), "%Y-%m-%d %H:%M").replace(
tzinfo=UTC
)
except ValueError: except ValueError:
pass pass
# 日期格式 2026-06-20 14:30:00 # 日期格式 2026-06-20 14:30:00
try: try:
return datetime.strptime(date_str.strip(), "%Y-%m-%d %H:%M:%S") return datetime.strptime(date_str.strip(), "%Y-%m-%d %H:%M:%S").replace(
tzinfo=UTC
)
except ValueError: except ValueError:
pass pass
# 日期格式 06-20 # 日期格式 06-20
try: try:
dt = datetime.strptime(date_str.strip(), "%m-%d") dt = datetime.strptime(date_str.strip(), "%m-%d").replace(tzinfo=UTC)
return dt.replace(year=now.year) return dt.replace(year=now.year)
except ValueError: except ValueError:
pass pass
# 日期格式 06-20 14:30 # 日期格式 06-20 14:30
try: try:
dt = datetime.strptime(date_str.strip(), "%m-%d %H:%M") dt = datetime.strptime(date_str.strip(), "%m-%d %H:%M").replace(tzinfo=UTC)
return dt.replace(year=now.year) return dt.replace(year=now.year)
except ValueError: except ValueError:
pass pass
@@ -395,7 +399,7 @@ class EmailVerifier:
return code return code
except EmailLoginError: except EmailLoginError:
raise raise
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
last_error = str(e) last_error = str(e)
logger.warning(f"Roundcube 读邮件异常: {e}") logger.warning(f"Roundcube 读邮件异常: {e}")
+4 -4
View File
@@ -398,7 +398,7 @@ class DouyuLogin:
return LoginResult( return LoginResult(
success=False, message=str(e), code="email_login_failed" success=False, message=str(e), code="email_login_failed"
) )
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
elapsed = time.monotonic() - start_time elapsed = time.monotonic() - start_time
if self.max_login_retries > 0: if self.max_login_retries > 0:
logger.error( logger.error(
@@ -475,7 +475,7 @@ class DouyuLogin:
return LoginResult( return LoginResult(
success=False, message=str(e), code="email_login_failed" success=False, message=str(e), code="email_login_failed"
) )
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
elapsed = time.monotonic() - start_time elapsed = time.monotonic() - start_time
if self.max_login_retries > 0: if self.max_login_retries > 0:
logger.error( logger.error(
@@ -901,7 +901,7 @@ class DouyuLogin:
if response2.status_code == 200: if response2.status_code == 200:
logger.info("WebLogin成功") logger.info("WebLogin成功")
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
logger.warning(f"WebLogin请求失败(不影响登录): {e}") logger.warning(f"WebLogin请求失败(不影响登录): {e}")
# 登录成功后补齐 Web 侧 Cookie。补 CK 失败只影响完整度,不回滚已成功的登录态。 # 登录成功后补齐 Web 侧 Cookie。补 CK 失败只影响完整度,不回滚已成功的登录态。
@@ -915,7 +915,7 @@ class DouyuLogin:
).enrich_with_retry() ).enrich_with_retry()
except InterruptedError: except InterruptedError:
raise raise
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self._cookie_enrich_error = self._truncate_error(str(e), 160) self._cookie_enrich_error = self._truncate_error(str(e), 160)
logger.warning( logger.warning(
f"补CK最终失败,本次仍按登录成功保存基础CK: {self._cookie_enrich_error}" f"补CK最终失败,本次仍按登录成功保存基础CK: {self._cookie_enrich_error}"
+5 -3
View File
@@ -137,7 +137,7 @@ class BaseWhitelistAdapter(ABC):
return False, f"白名单添加失败,API响应: {resp}" return False, f"白名单添加失败,API响应: {resp}"
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
msg = f"白名单同步失败: {e}" msg = f"白名单同步失败: {e}"
logger.error(msg) logger.error(msg)
return False, msg return False, msg
@@ -165,7 +165,8 @@ def _get_local_exit_ip() -> str | None:
match = re.search(r"(\d{1,3}(?:\.\d{1,3}){3})", text) match = re.search(r"(\d{1,3}(?:\.\d{1,3}){3})", text)
if match: if match:
return match.group(1) return match.group(1)
except Exception: except Exception as exc: # noqa: BLE001
logger.debug(f"本机出口 IP 查询失败: {url}: {exc}")
continue continue
return None return None
@@ -197,6 +198,7 @@ def get_exit_ip_via_proxy(proxy: str) -> str | None:
match = re.search(r"(\d{1,3}(?:\.\d{1,3}){3})", text) match = re.search(r"(\d{1,3}(?:\.\d{1,3}){3})", text)
if match: if match:
return match.group(1) return match.group(1)
except Exception: except Exception as exc: # noqa: BLE001
logger.debug(f"代理出口 IP 查询失败: {url}: {exc}")
continue continue
return None return None
+4 -4
View File
@@ -75,7 +75,7 @@ class XiequAdapter(BaseWhitelistAdapter):
if r.get("IP") or r.get("ip") if r.get("IP") or r.get("ip")
] ]
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
logger.error(f"获取白名单失败: {e}") logger.error(f"获取白名单失败: {e}")
return [] return []
@@ -120,7 +120,7 @@ class XiequAdapter(BaseWhitelistAdapter):
logger.warning(f"白名单添加结果: {text}") logger.warning(f"白名单添加结果: {text}")
return False, text return False, text
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
logger.error(f"添加白名单失败: {e}") logger.error(f"添加白名单失败: {e}")
return False, str(e) return False, str(e)
@@ -141,7 +141,7 @@ class XiequAdapter(BaseWhitelistAdapter):
logger.warning(f"白名单删除结果: {text}") logger.warning(f"白名单删除结果: {text}")
return False, text return False, text
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
logger.error(f"删除白名单失败: {e}") logger.error(f"删除白名单失败: {e}")
return False, str(e) return False, str(e)
@@ -160,7 +160,7 @@ class XiequAdapter(BaseWhitelistAdapter):
logger.info(msg) logger.info(msg)
return True, msg return True, msg
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
msg = f"连接失败: {e}" msg = f"连接失败: {e}"
logger.error(msg) logger.error(msg)
return False, msg return False, msg
+3 -3
View File
@@ -115,7 +115,7 @@ class XkdailiAdapter(BaseWhitelistAdapter):
logger.warning(f"星空白名单添加失败: {msg}") logger.warning(f"星空白名单添加失败: {msg}")
return ok, msg return ok, msg
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
logger.error(f"星空添加白名单失败: {e}") logger.error(f"星空添加白名单失败: {e}")
return False, str(e) return False, str(e)
@@ -136,7 +136,7 @@ class XkdailiAdapter(BaseWhitelistAdapter):
logger.warning(f"星空白名单删除失败: {msg}") logger.warning(f"星空白名单删除失败: {msg}")
return ok, msg return ok, msg
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
logger.error(f"星空删除白名单失败: {e}") logger.error(f"星空删除白名单失败: {e}")
return False, str(e) return False, str(e)
@@ -159,5 +159,5 @@ class XkdailiAdapter(BaseWhitelistAdapter):
# 其他错误(如无效IP),说明认证通过了 # 其他错误(如无效IP),说明认证通过了
return True, f"连接成功,API可访问: {msg}" return True, f"连接成功,API可访问: {msg}"
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
return False, f"连接失败: {e}" return False, f"连接失败: {e}"
+1 -1
View File
@@ -167,7 +167,7 @@ class ProxyResolver:
f"代理预检 {attempt}/{max_attempts}: {last_error}: {text[:80]}", f"代理预检 {attempt}/{max_attempts}: {last_error}: {text[:80]}",
) )
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
last_error = f"代理API请求失败: {exc}" last_error = f"代理API请求失败: {exc}"
self._log("warning", f"代理预检 {attempt}/{max_attempts}: {last_error}") self._log("warning", f"代理预检 {attempt}/{max_attempts}: {last_error}")
+5 -3
View File
@@ -27,7 +27,7 @@ def verify_proxy_url(proxy_url: str, timeout: tuple = (3, 5)) -> tuple[bool, str
) )
response.raise_for_status() response.raise_for_status()
return True, "代理可用 → 斗鱼可达" return True, "代理可用 → 斗鱼可达"
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
err_msg = str(exc) err_msg = str(exc)
if "Tunnel connection failed" in err_msg or "503" in err_msg: if "Tunnel connection failed" in err_msg or "503" in err_msg:
detail = "代理拒绝连接(白名单可能未生效)" detail = "代理拒绝连接(白名单可能未生效)"
@@ -75,7 +75,8 @@ def verify_proxies_concurrent(
ok, _ = future.result() ok, _ = future.result()
if ok: if ok:
available.append(future_map[future]) available.append(future_map[future])
except Exception: except Exception as exc: # noqa: BLE001
logger.debug(f"代理并发验证失败: {future_map[future]}: {exc}")
continue continue
if available: if available:
logger.success( logger.success(
@@ -99,7 +100,8 @@ def verify_proxies_concurrent(
if item != future: if item != future:
item.cancel() item.cancel()
return proxy_url, msg return proxy_url, msg
except Exception: except Exception as exc: # noqa: BLE001
logger.debug(f"代理并发验证失败: {proxy_url}: {exc}")
continue continue
return None, f"{len(proxy_urls)} 个代理均不可用" return None, f"{len(proxy_urls)} 个代理均不可用"
-1
View File
@@ -1,6 +1,5 @@
"""代理模块使用的白名单适配器。""" """代理模块使用的白名单适配器。"""
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
+7 -7
View File
@@ -73,7 +73,7 @@ def get_challenge_gt_bak() -> tuple[str, str]:
} }
params = { params = {
"t": str(int(round(time.time() * 1000))), "t": str(round(time.time() * 1000)),
} }
response = requests.get( response = requests.get(
@@ -133,7 +133,7 @@ def get_js_address(gt: str, proxies: Mapping[str, str] | None = None) -> dict:
params = { params = {
"gt": gt, "gt": gt,
"callback": "geetest_" + str(int(round(time.time() * 1000))), "callback": "geetest_" + str(round(time.time() * 1000)),
} }
response = _get( response = _get(
@@ -173,7 +173,7 @@ def get_c_s(
+ "&lang=zh-cn&pt=0&client_type=web&w=" + "&lang=zh-cn&pt=0&client_type=web&w="
+ w + w
+ "&callback=geetest_" + "&callback=geetest_"
+ str(int(round(time.time() * 1000))), + str(round(time.time() * 1000)),
headers=headers, headers=headers,
proxies=proxies, proxies=proxies,
) )
@@ -210,7 +210,7 @@ def req_fullpage_validate(
+ "&lang=zh-cn&pt=0&client_type=web&w=" + "&lang=zh-cn&pt=0&client_type=web&w="
+ w + w
+ "&callback=geetest_" + "&callback=geetest_"
+ str(int(round(time.time() * 1000))), + str(round(time.time() * 1000)),
headers=headers, headers=headers,
proxies=proxies, proxies=proxies,
) )
@@ -241,7 +241,7 @@ def req_slide(gt: str, challenge: str, w2: str) -> None:
+ "&lang=zh-cn&pt=0&client_type=web&w=" + "&lang=zh-cn&pt=0&client_type=web&w="
+ w2 + w2
+ "&callback=geetest_" + "&callback=geetest_"
+ str(int(round(time.time() * 1000))), + str(round(time.time() * 1000)),
headers=headers, headers=headers,
timeout=REQUEST_TIMEOUT, timeout=REQUEST_TIMEOUT,
) )
@@ -277,7 +277,7 @@ def get_picture(gt: str, challenge: str) -> tuple[str, str, list[int], str, str,
"isPC": "true", "isPC": "true",
"autoReset": "true", "autoReset": "true",
"width": "100%", "width": "100%",
"callback": "geetest_" + str(int(round(time.time() * 1000))), "callback": "geetest_" + str(round(time.time() * 1000)),
} }
response = requests.get( response = requests.get(
@@ -343,7 +343,7 @@ def req_end(gt: str, challenge: str, w: str) -> dict:
+ "&lang=zh-cn&%24_BCm=0&client_type=web&w=" + "&lang=zh-cn&%24_BCm=0&client_type=web&w="
+ w + w
+ "&callback=geetest_" + "&callback=geetest_"
+ str(int(round(time.time() * 1000))), + str(round(time.time() * 1000)),
headers=headers, headers=headers,
timeout=REQUEST_TIMEOUT, timeout=REQUEST_TIMEOUT,
) )
+1 -1
View File
@@ -592,7 +592,7 @@ def H(t: int, e: str) -> str:
n = 36 * r[0] + r[1] n = 36 * r[0] + r[1]
# 计算目标值 # 计算目标值
a = round(t) + n a = (t) + n
# 构建字符池 # 构建字符池
_ = [[], [], [], [], []] _ = [[], [], [], [], []]
+2 -2
View File
@@ -85,7 +85,7 @@ def get_w2(gt: str, challenge: str, c: list[int], s: str, str_16: str) -> str:
"u": fake_timing["loadEventEnd"], "u": fake_timing["loadEventEnd"],
} }
first_time = int(round(time.time() * 1000)) # 伪造脚本开始运行时间 first_time = round(time.time() * 1000) # 伪造脚本开始运行时间
guiji_yuanshu_shuzu = generate_realistic_trajectory( guiji_yuanshu_shuzu = generate_realistic_trajectory(
start_x=random.randint(400, 600), # 起始位置随机 start_x=random.randint(400, 600), # 起始位置随机
@@ -99,7 +99,7 @@ def get_w2(gt: str, challenge: str, c: list[int], s: str, str_16: str) -> str:
compressed = compress_trajectory(trajectory) compressed = compress_trajectory(trajectory)
tt = encrypt_string(compressed, c, s) tt = encrypt_string(compressed, c, s)
passtime = str(int(round(time.time() * 1000)) - first_time) passtime = str(round(time.time() * 1000) - first_time)
rp = simple_md5(gt + challenge + passtime) rp = simple_md5(gt + challenge + passtime)
+3 -3
View File
@@ -306,7 +306,7 @@ class UserPrizeRecordItem(TafStruct):
self.exchangeDate = ins.read_int64(21, default=self.exchangeDate) self.exchangeDate = ins.read_int64(21, default=self.exchangeDate)
while True: while True:
pos = ins.buf.tell() pos = ins.buf.tell()
tag, dtype = ins.read_head() _tag, dtype = ins.read_head()
if dtype == TafType.STRUCT_END: if dtype == TafType.STRUCT_END:
ins.buf.seek(pos) ins.buf.seek(pos)
return return
@@ -406,7 +406,7 @@ class ActTaskPrizeInfo(TafStruct):
self.extra = ins.read_map(12) self.extra = ins.read_map(12)
while True: while True:
pos = ins.buf.tell() pos = ins.buf.tell()
tag, dtype = ins.read_head() _tag, dtype = ins.read_head()
if dtype == TafType.STRUCT_END: if dtype == TafType.STRUCT_END:
ins.buf.seek(pos) ins.buf.seek(pos)
return return
@@ -494,7 +494,7 @@ class ActTaskDetailItem(TafStruct):
self.endTime = ins.read_string(26, default=self.endTime) self.endTime = ins.read_string(26, default=self.endTime)
while True: while True:
pos = ins.buf.tell() pos = ins.buf.tell()
tag, dtype = ins.read_head() _tag, dtype = ins.read_head()
if dtype == TafType.STRUCT_END: if dtype == TafType.STRUCT_END:
ins.buf.seek(pos) ins.buf.seek(pos)
return return
+6 -6
View File
@@ -232,7 +232,7 @@ def solve_safe_auth(
result = solver.solve(risk_url) result = solver.solve(risk_url)
except HuyaQrAuthRequiredError: except HuyaQrAuthRequiredError:
raise raise
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
last_err = exc last_err = exc
logger.warning(f"[safe_auth] 第 {attempt + 1} 次过验异常: {exc}") logger.warning(f"[safe_auth] 第 {attempt + 1} 次过验异常: {exc}")
time.sleep(1.0) time.sleep(1.0)
@@ -425,8 +425,8 @@ class HuyaAppPasswordLogin:
from .device_profile import record_login from .device_profile import record_login
record_login(self.username, result.success, result.message) record_login(self.username, result.success, result.message)
except Exception: # 元数据记录失败不影响登录结果 except Exception as exc: # noqa: BLE001 # 元数据记录失败不影响登录结果
pass logger.debug(f"登录元数据记录失败: {exc}")
return result return result
def _login_impl(self) -> HuyaLoginResult: def _login_impl(self) -> HuyaLoginResult:
@@ -450,7 +450,7 @@ class HuyaAppPasswordLogin:
message=str(exc), message=str(exc),
code="QR_AUTH_REQUIRED", code="QR_AUTH_REQUIRED",
) )
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
return HuyaLoginResult( return HuyaLoginResult(
success=False, success=False,
message=f"App 登录凭证获取失败: {exc}", message=f"App 登录凭证获取失败: {exc}",
@@ -478,7 +478,7 @@ class HuyaAppPasswordLogin:
if env.uid != uid: if env.uid != uid:
struct.pack_into(">Q", raw, env.uid_off, uid) struct.pack_into(">Q", raw, env.uid_off, uid)
wup = base64.b64encode(bytes(raw)).decode("ascii") wup = base64.b64encode(bytes(raw)).decode("ascii")
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
return HuyaLoginResult( return HuyaLoginResult(
success=False, success=False,
message=f"证书铸造/信封补丁失败: {exc}", message=f"证书铸造/信封补丁失败: {exc}",
@@ -603,7 +603,7 @@ class HuyaAppPasswordLogin:
sdid=sdid, sdid=sdid,
context=pc.context, context=pc.context,
) )
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
return HuyaLoginResult( return HuyaLoginResult(
success=False, success=False,
message=f"扫码绑定兑换 Cookie 失败: {exc}", message=f"扫码绑定兑换 Cookie 失败: {exc}",
+4 -4
View File
@@ -6,7 +6,7 @@ import threading
import time import time
from collections.abc import Mapping from collections.abc import Mapping
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import UTC, datetime
from core.sms_provider import SmsLine, SmsProviderClient from core.sms_provider import SmsLine, SmsProviderClient
@@ -76,7 +76,7 @@ def register_huya_with_sms_line(
sms_url=item.url, sms_url=item.url,
) )
sent_at = datetime.now() sent_at = datetime.now(UTC)
try: try:
code_result = send_huya_sms_code(phone=phone, proxies=proxies) code_result = send_huya_sms_code(phone=phone, proxies=proxies)
except HuyaLoginError as exc: except HuyaLoginError as exc:
@@ -89,7 +89,7 @@ def register_huya_with_sms_line(
normalized_phone=normalized_phone, normalized_phone=normalized_phone,
sms_url=item.url, sms_url=item.url,
) )
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
return HuyaAutoRegisterResult( return HuyaAutoRegisterResult(
phone=phone, phone=phone,
provider=item.provider, provider=item.provider,
@@ -155,7 +155,7 @@ def register_huya_with_sms_line(
sms_url=item.url, sms_url=item.url,
attempts=attempts, attempts=attempts,
) )
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
return HuyaAutoRegisterResult( return HuyaAutoRegisterResult(
phone=phone, phone=phone,
provider=item.provider, provider=item.provider,
+2 -2
View File
@@ -10,7 +10,7 @@ import time
import uuid import uuid
from collections.abc import Mapping from collections.abc import Mapping
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import UTC, datetime
from urllib.parse import quote, urlsplit, urlunsplit from urllib.parse import quote, urlsplit, urlunsplit
import requests import requests
@@ -496,7 +496,7 @@ def change_huya_password_with_sms_line(
) -> HuyaChangePasswordResult: ) -> HuyaChangePasswordResult:
"""使用同一手机号接码链接完成改密短信验证。""" """使用同一手机号接码链接完成改密短信验证。"""
changer = HuyaPasswordChanger(uid=uid, cookie=cookie, proxies=proxies) changer = HuyaPasswordChanger(uid=uid, cookie=cookie, proxies=proxies)
sent_at = datetime.now() sent_at = datetime.now(UTC)
code_result = changer.send_code() code_result = changer.send_code()
if not code_result.success or not code_result.session_data: if not code_result.success or not code_result.session_data:
return HuyaChangePasswordResult( return HuyaChangePasswordResult(
+8 -6
View File
@@ -22,6 +22,8 @@ import random
from collections.abc import Mapping from collections.abc import Mapping
from pathlib import Path from pathlib import Path
from loguru import logger
ROOT = Path(__file__).resolve().parent.parent.parent ROOT = Path(__file__).resolve().parent.parent.parent
DATA_DIR = ROOT / "data" DATA_DIR = ROOT / "data"
PRIMARY_PROFILE_DB = DATA_DIR / "huya_device_profiles.json" PRIMARY_PROFILE_DB = DATA_DIR / "huya_device_profiles.json"
@@ -119,13 +121,13 @@ def _load_db() -> dict:
if PRIMARY_PROFILE_DB.exists(): if PRIMARY_PROFILE_DB.exists():
try: try:
return json.loads(PRIMARY_PROFILE_DB.read_text("utf-8")) return json.loads(PRIMARY_PROFILE_DB.read_text("utf-8"))
except Exception: except Exception as exc: # noqa: BLE001
pass logger.debug(f"读取主设备画像库失败: {exc}")
if FALLBACK_PROFILE_DB.exists(): if FALLBACK_PROFILE_DB.exists():
try: try:
return json.loads(FALLBACK_PROFILE_DB.read_text("utf-8")) return json.loads(FALLBACK_PROFILE_DB.read_text("utf-8"))
except Exception: except Exception as exc: # noqa: BLE001
pass logger.debug(f"读取备用设备画像库失败: {exc}")
return {} return {}
@@ -135,8 +137,8 @@ def _save_db(db: dict) -> None:
PRIMARY_PROFILE_DB.write_text( PRIMARY_PROFILE_DB.write_text(
json.dumps(db, indent=2, ensure_ascii=False), encoding="utf-8" json.dumps(db, indent=2, ensure_ascii=False), encoding="utf-8"
) )
except Exception: except Exception as exc: # noqa: BLE001
pass logger.debug(f"保存设备画像库失败: {exc}")
def _enrich_profile(profile: dict) -> tuple[dict, bool]: def _enrich_profile(profile: dict) -> tuple[dict, bool]:
+4 -2
View File
@@ -10,6 +10,8 @@ import json
import struct import struct
from pathlib import Path from pathlib import Path
from loguru import logger
INT8, INT16, INT32, INT64 = 0x00, 0x01, 0x02, 0x03 INT8, INT16, INT32, INT64 = 0x00, 0x01, 0x02, 0x03
STRING1, STRING4 = 0x06, 0x07 STRING1, STRING4 = 0x06, 0x07
MAP, LIST = 0x08, 0x09 MAP, LIST = 0x08, 0x09
@@ -131,8 +133,8 @@ class Envelope:
if candidate.exists(): if candidate.exists():
try: try:
return cls._load_from_path(candidate) return cls._load_from_path(candidate)
except Exception: except Exception as exc: # noqa: BLE001
pass logger.debug(f"加载证书信封候选文件失败: {candidate}: {exc}")
return cls(base64.b64decode(DEFAULT_QURL_B64)) return cls(base64.b64decode(DEFAULT_QURL_B64))
@classmethod @classmethod
+12 -9
View File
@@ -4,6 +4,8 @@ TAF/WUP 帧解码器 — 将二进制帧转为可读摘要,用于日志输出
from typing import Any, cast from typing import Any, cast
from loguru import logger
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
@@ -101,13 +103,13 @@ def _decode_taf_struct(ins: TafInputStream, depth: int = 0) -> dict:
if depth < 5: if depth < 5:
try: try:
val = _decode_taf_value(ins, dtype, depth) val = _decode_taf_value(ins, dtype, depth)
except Exception: except Exception: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
val = f"<decode_err:0x{dtype:02x}>" val = f"<decode_err:0x{dtype:02x}>"
else: else:
val = "<...>" val = "<...>"
try: try:
ins.skip_field(dtype) ins.skip_field(dtype)
except Exception: except Exception: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
break break
key = f"tag{tag}" key = f"tag{tag}"
if key in fields: if key in fields:
@@ -195,19 +197,20 @@ def _decode_wup_body(body: bytes) -> dict:
if v: if v:
try: try:
tins = TafInputStream(v) tins = TafInputStream(v)
ttag, tdt = tins.peek_head() _ttag, tdt = tins.peek_head()
if tdt == TafType.STRUCT_BEGIN: if tdt == TafType.STRUCT_BEGIN:
tins.read_head() tins.read_head()
result[k] = _decode_taf_struct(tins) result[k] = _decode_taf_struct(tins)
else: else:
result[k] = f"<{len(v)}B>" result[k] = f"<{len(v)}B>"
except Exception: except Exception as exc: # noqa: BLE001
logger.debug(f"TAF 嵌套结构解码失败: {exc}")
result[k] = f"<{len(v)}B>" result[k] = f"<{len(v)}B>"
else: else:
result[k] = _decode_taf_value(sins, vt) result[k] = _decode_taf_value(sins, vt)
except Exception: except Exception as exc: # noqa: BLE001
pass logger.debug(f"TAF 字段解码失败: {exc}")
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
result["err"] = str(e) result["err"] = str(e)
return result return result
@@ -286,7 +289,7 @@ def format_wss_log(body: bytes, cmd: int, seq: int, direction: str) -> str:
try: try:
ins = TafInputStream(clean) ins = TafInputStream(clean)
# 看第一个 head # 看第一个 head
tag, dtype = ins.peek_head() _tag, dtype = ins.peek_head()
if dtype == TafType.STRUCT_BEGIN: if dtype == TafType.STRUCT_BEGIN:
ins.read_head() ins.read_head()
fields = _decode_taf_struct(ins) fields = _decode_taf_struct(ins)
@@ -314,6 +317,6 @@ def format_wss_log(body: bytes, cmd: int, seq: int, direction: str) -> str:
if fields: if fields:
return f"{prefix} {cmd_name} {_fmt_fields(cast(dict[str, Any], _truncate(fields)))}" return f"{prefix} {cmd_name} {_fmt_fields(cast(dict[str, Any], _truncate(fields)))}"
return f"{prefix} {cmd_name}" return f"{prefix} {cmd_name}"
except Exception: except Exception: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
cmd_name = CMD_NAMES.get(cmd, f"0x{cmd:02x}") cmd_name = CMD_NAMES.get(cmd, f"0x{cmd:02x}")
return f"{prefix} {cmd_name} ({len(body)}B)" return f"{prefix} {cmd_name} ({len(body)}B)"
+7 -7
View File
@@ -92,7 +92,7 @@ class WSConnectParaInfo(TafStruct):
def _gen_trace_id() -> str: def _gen_trace_id() -> str:
"""生成 sTraceId (格式 hex8:hex8:0:0HAR 实证)""" """生成 sTraceId (格式 hex8:hex8:0:0HAR 实证)"""
h = "%016x" % random.getrandbits(64) h = f"{random.getrandbits(64):016x}"
return f"{h}:{h}:0:0" return f"{h}:{h}:0:0"
@@ -294,7 +294,7 @@ class HuyaHttpClient:
import gzip import gzip
resp_data = gzip.decompress(resp_data) resp_data = gzip.decompress(resp_data)
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self.logger(f"[HTTP] ❌ 请求失败: {type(e).__name__}: {e}") self.logger(f"[HTTP] ❌ 请求失败: {type(e).__name__}: {e}")
return None return None
@@ -611,13 +611,13 @@ class HuyaHttpClient:
or resp.headers.get("Content-Encoding") == "gzip" or resp.headers.get("Content-Encoding") == "gzip"
): ):
resp_data = gzip.decompress(resp_data) resp_data = gzip.decompress(resp_data)
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self.logger(f"[LIVELINK] ❌ 小程序码请求失败: {type(e).__name__}: {e}") self.logger(f"[LIVELINK] ❌ 小程序码请求失败: {type(e).__name__}: {e}")
return None return None
try: try:
data = json.loads(resp_data.decode("utf-8", "replace")) data = json.loads(resp_data.decode("utf-8", "replace"))
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self.logger(f"[LIVELINK] ❌ 小程序码响应解析失败: {type(e).__name__}: {e}") self.logger(f"[LIVELINK] ❌ 小程序码响应解析失败: {type(e).__name__}: {e}")
return None return None
@@ -679,13 +679,13 @@ class HuyaHttpClient:
or resp.headers.get("Content-Encoding") == "gzip" or resp.headers.get("Content-Encoding") == "gzip"
): ):
resp_data = gzip.decompress(resp_data) resp_data = gzip.decompress(resp_data)
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self.logger(f"[LIVELINK] ❌ 二维码状态请求失败: {type(e).__name__}: {e}") self.logger(f"[LIVELINK] ❌ 二维码状态请求失败: {type(e).__name__}: {e}")
return None return None
try: try:
data = json.loads(resp_data.decode("utf-8", "replace")) data = json.loads(resp_data.decode("utf-8", "replace"))
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self.logger( self.logger(
f"[LIVELINK] ❌ 二维码状态响应解析失败: {type(e).__name__}: {e}" f"[LIVELINK] ❌ 二维码状态响应解析失败: {type(e).__name__}: {e}"
) )
@@ -972,7 +972,7 @@ class HuyaHttpClient:
import gzip import gzip
resp_data = gzip.decompress(resp_data) resp_data = gzip.decompress(resp_data)
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self.logger(f"[HTTP] ❌ payOrderSubmitV5 失败: {e}") self.logger(f"[HTTP] ❌ payOrderSubmitV5 失败: {e}")
return None return None
+3 -4
View File
@@ -380,10 +380,9 @@ class HuyaPasswordLogin:
last_exc: Exception | None = None last_exc: Exception | None = None
for attempt in range(max_ip_retries + 1): for attempt in range(max_ip_retries + 1):
if attempt > 0: if attempt > 0 and not self._swap_proxy():
if not self._swap_proxy(): logger.warning("无可用代理可切换,停止换 IP 重试")
logger.warning("无可用代理可切换,停止换 IP 重试") break
break
try: try:
return self._login_once() return self._login_once()
except (HuyaQrAuthRequiredError, HuyaSmsAuthRequiredError) as exc: except (HuyaQrAuthRequiredError, HuyaSmsAuthRequiredError) as exc:
+2 -3
View File
@@ -5,7 +5,6 @@
来源: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 .taf_protocol import TafInputStream, TafOutputStream, TafStruct, TafType from .taf_protocol import TafInputStream, TafOutputStream, TafStruct, TafType
@@ -68,7 +67,7 @@ def _skip_to_struct_end(ins: TafInputStream):
"""跳过当前结构里未解析的尾部字段,停在 STRUCT_END 前。""" """跳过当前结构里未解析的尾部字段,停在 STRUCT_END 前。"""
while True: while True:
pos = ins.buf.tell() pos = ins.buf.tell()
tag, dtype = ins.read_head() _tag, dtype = ins.read_head()
if dtype == TafType.STRUCT_END: if dtype == TafType.STRUCT_END:
ins.buf.seek(pos) ins.buf.seek(pos)
return return
@@ -427,7 +426,7 @@ class GoodsPriceInfo(TafStruct):
def first_sku_id(self) -> int: def first_sku_id(self) -> int:
if not self.skuMap: if not self.skuMap:
return 0 return 0
return sorted(self.skuMap.keys())[0] return min(self.skuMap.keys())
@property @property
def sku_list(self) -> list[dict]: def sku_list(self) -> list[dict]:
+3 -3
View File
@@ -255,7 +255,7 @@ class TafInputStream:
def _read_int_len(self) -> int: def _read_int_len(self) -> int:
"""读 map/list 长度(int32 带优化)""" """读 map/list 长度(int32 带优化)"""
tag, dtype = self.read_head() _tag, dtype = self.read_head()
return self._read_int_value(dtype) return self._read_int_value(dtype)
def _read_int_value(self, dtype: int) -> int: def _read_int_value(self, dtype: int) -> int:
@@ -273,7 +273,7 @@ class TafInputStream:
def _skip_struct(self): def _skip_struct(self):
while True: while True:
tag, dtype = self.read_head() _tag, dtype = self.read_head()
if dtype == TafType.STRUCT_END: if dtype == TafType.STRUCT_END:
break break
self.skip_field(dtype) self.skip_field(dtype)
@@ -441,7 +441,7 @@ class TafInputStream:
obj = struct_class() obj = struct_class()
obj.read_from(self) obj.read_from(self)
# 消费 STRUCT_END # 消费 STRUCT_END
t, dt = self.read_head() _t, dt = self.read_head()
if dt != TafType.STRUCT_END: if dt != TafType.STRUCT_END:
raise ValueError(f"期望 STRUCT_END, 实际 0x{dt:02x}") raise ValueError(f"期望 STRUCT_END, 实际 0x{dt:02x}")
return obj return obj
+1 -1
View File
@@ -261,7 +261,7 @@ class HuyaCaptchaOcr:
target["cropped_image"], target["cropped_image"],
char["cropped_image"], char["cropped_image"],
) )
except Exception: except Exception: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
score_matrix[target_index][char_index] = 1e6 score_matrix[target_index][char_index] = 1e6
row_ind, col_ind = linear_sum_assignment(score_matrix) row_ind, col_ind = linear_sum_assignment(score_matrix)
+3 -3
View File
@@ -137,7 +137,7 @@ class HuyaVerificationSolver:
max_width = max(bg.shape[1], tip.shape[1]) max_width = max(bg.shape[1], tip.shape[1])
def pad_right(img, target_width): def pad_right(img, target_width):
height, width = img.shape[:2] _height, width = img.shape[:2]
if width >= target_width: if width >= target_width:
return img return img
return cv2.copyMakeBorder( return cv2.copyMakeBorder(
@@ -299,8 +299,8 @@ class HuyaVerificationSolver:
"虎牙登录风控strategys完整结构: {}", "虎牙登录风控strategys完整结构: {}",
json.dumps(strategies, ensure_ascii=False)[:1200], json.dumps(strategies, ensure_ascii=False)[:1200],
) )
except Exception: # noqa: BLE001 except Exception as exc: # noqa: BLE001
pass logger.debug(f"风控策略日志序列化失败: {exc}")
strategy_url_lower = strategy_url.lower() strategy_url_lower = strategy_url.lower()
# 判定依据是 URL 路径,不是 strategy 数值。 # 判定依据是 URL 路径,不是 strategy 数值。
# 实测(2026-08-25): strategy=64 时 pt_auth.html 是滑块、qr_auth.html 才是扫码, # 实测(2026-08-25): strategy=64 时 pt_auth.html 是滑块、qr_auth.html 才是扫码,
+14 -12
View File
@@ -76,7 +76,7 @@ class WssMessage:
def decode(cls, data: bytes) -> "WssMessage": def decode(cls, data: bytes) -> "WssMessage":
if len(data) < 6: if len(data) < 6:
raise ValueError(f"消息太短: {len(data)} bytes") raise ValueError(f"消息太短: {len(data)} bytes")
version, command = struct.unpack(">BB", data[0:2]) _version, command = struct.unpack(">BB", data[0:2])
sequence = struct.unpack(">I", data[2:6])[0] sequence = struct.unpack(">I", data[2:6])[0]
body = data[6:] body = data[6:]
return cls(command=command, sequence=sequence, body=body) return cls(command=command, sequence=sequence, body=body)
@@ -256,7 +256,7 @@ class HuyaWssClient:
format_wss_log(msg.body, msg.command, msg.sequence, "") format_wss_log(msg.body, msg.command, msg.sequence, "")
) )
await self._handle_message(msg) await self._handle_message(msg)
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self.logger( self.logger(
f"[WSS] 解析消息失败: {e} raw_hex={raw_bytes[:50].hex()}" f"[WSS] 解析消息失败: {e} raw_hex={raw_bytes[:50].hex()}"
) )
@@ -264,7 +264,7 @@ class HuyaWssClient:
pass pass
except websockets.exceptions.ConnectionClosed as e: except websockets.exceptions.ConnectionClosed as e:
self.logger(f"[WSS] 连接关闭: code={e.code} reason={e.reason}") self.logger(f"[WSS] 连接关闭: code={e.code} reason={e.reason}")
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self.logger(f"[WSS] 接收循环异常: {type(e).__name__}: {e}") self.logger(f"[WSS] 接收循环异常: {type(e).__name__}: {e}")
async def _handle_message(self, msg: WssMessage): async def _handle_message(self, msg: WssMessage):
@@ -289,7 +289,7 @@ class HuyaWssClient:
await self.send_heartbeat() await self.send_heartbeat()
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self.logger(f"[WSS] 心跳循环异常: {e}") self.logger(f"[WSS] 心跳循环异常: {e}")
async def send_heartbeat(self): async def send_heartbeat(self):
@@ -399,7 +399,7 @@ class HuyaWssClient:
) )
return return
ins = TafInputStream(treq) ins = TafInputStream(treq)
tag, dtype = ins.peek_head() _tag, dtype = ins.peek_head()
if dtype != 0x0A: # STRUCT_BEGIN if dtype != 0x0A: # STRUCT_BEGIN
self.logger(f"[RPC] wsLaunch tRsp 非结构体 dtype=0x{dtype:02x}") self.logger(f"[RPC] wsLaunch tRsp 非结构体 dtype=0x{dtype:02x}")
return return
@@ -432,7 +432,7 @@ class HuyaWssClient:
self.logger( self.logger(
f"[RPC] wsLaunch 解析: guid={self._launch_guid} ip={self._launch_ip}" f"[RPC] wsLaunch 解析: guid={self._launch_guid} ip={self._launch_ip}"
) )
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self.logger(f"[RPC] wsLaunch 响应解析失败: {e}") self.logger(f"[RPC] wsLaunch 响应解析失败: {e}")
@staticmethod @staticmethod
@@ -610,15 +610,17 @@ class HuyaWssClient:
if data and isinstance(data, bytes) and len(data) > 0: if data and isinstance(data, bytes) and len(data) > 0:
try: try:
ins = TafInputStream(data) ins = TafInputStream(data)
tag, dtype = ins.peek_head() _tag, dtype = ins.peek_head()
if dtype == TafType.STRUCT_BEGIN: if dtype == TafType.STRUCT_BEGIN:
ins.read_head() ins.read_head()
decoded = _decode_taf_struct(ins) decoded = _decode_taf_struct(ins)
self.logger( self.logger(
f"[←] {service}.{method} {key}: {_truncate(decoded)}" f"[←] {service}.{method} {key}: {_truncate(decoded)}"
) )
except Exception: except Exception as exc: # noqa: BLE001
pass self.logger(
f"[debug] WUP 响应字段解码失败: {service}.{method}.{key}: {exc}"
)
if rsp_class is None: if rsp_class is None:
return body return body
@@ -794,13 +796,13 @@ class HuyaWssClient:
if data and isinstance(data, bytes) and len(data) > 0: if data and isinstance(data, bytes) and len(data) > 0:
try: try:
ins = TafInputStream(data) ins = TafInputStream(data)
tag, dtype = ins.peek_head() _tag, dtype = ins.peek_head()
if dtype == TafType.STRUCT_BEGIN: if dtype == TafType.STRUCT_BEGIN:
ins.read_head() ins.read_head()
decoded = _decode_taf_struct(ins) decoded = _decode_taf_struct(ins)
self.logger(f"[←] payOrderSubmitV5 {key}: {_truncate(decoded)}") self.logger(f"[←] payOrderSubmitV5 {key}: {_truncate(decoded)}")
except Exception: except Exception as exc: # noqa: BLE001
pass self.logger(f"[debug] WUP 支付响应字段解码失败: {key}: {exc}")
result = wup_resp.readStruct("tRsp", PayOrderRes) result = wup_resp.readStruct("tRsp", PayOrderRes)
if result is None: if result is None:
result = wup_resp.readStruct("tResp", PayOrderRes) result = wup_resp.readStruct("tResp", PayOrderRes)
+3 -3
View File
@@ -211,7 +211,7 @@ class WupResponse:
_, vt = ins.read_head() _, vt = ins.read_head()
val = _read_bytes_value(ins, vt) val = _read_bytes_value(ins, vt)
self.newdata[key] = val self.newdata[key] = val
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
print(f"[WupResponse] 解析 newdata 失败: {e}") print(f"[WupResponse] 解析 newdata 失败: {e}")
def readStruct(self, key: str, struct_class=None): def readStruct(self, key: str, struct_class=None):
@@ -236,13 +236,13 @@ class WupResponse:
ins = TafInputStream(data) ins = TafInputStream(data)
# newdata 里的结构体以 STRUCT_BEGIN 开头 # newdata 里的结构体以 STRUCT_BEGIN 开头
try: try:
tag, dtype = ins.peek_head() _tag, dtype = ins.peek_head()
if dtype == TafType.STRUCT_BEGIN: if dtype == TafType.STRUCT_BEGIN:
ins.read_head() # 消费 STRUCT_BEGIN ins.read_head() # 消费 STRUCT_BEGIN
obj = struct_class() obj = struct_class()
obj.read_from(ins) obj.read_from(ins)
return obj return obj
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
print(f"[WupResponse] 解析 {struct_class.__name__} 失败: {e}") print(f"[WupResponse] 解析 {struct_class.__name__} 失败: {e}")
return None return None
+2 -2
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
import json import json
import re import re
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import UTC, datetime
from urllib.parse import urlparse from urllib.parse import urlparse
import requests import requests
@@ -98,7 +98,7 @@ def _parse_sms8_time(value: str) -> datetime | None:
return None return None
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y/%m/%d %H:%M:%S"): for fmt in ("%Y-%m-%d %H:%M:%S", "%Y/%m/%d %H:%M:%S"):
try: try:
return datetime.strptime(text, fmt) return datetime.strptime(text, fmt).replace(tzinfo=UTC)
except ValueError: except ValueError:
continue continue
return None return None
+4
View File
@@ -38,6 +38,10 @@ packages = ["core", "utils", "web"]
[tool.pytest.ini_options] [tool.pytest.ini_options]
testpaths = ["tests"] testpaths = ["tests"]
[tool.ruff.lint.per-file-ignores]
# FastAPI evaluates dependency metadata in route parameter defaults by design.
"web/backend/**/*.py" = ["B008"]
[dependency-groups] [dependency-groups]
dev = [ dev = [
"pyright>=1.1.411", "pyright>=1.1.411",
+1 -1
View File
@@ -225,6 +225,6 @@ def main() -> int:
if __name__ == "__main__": if __name__ == "__main__":
try: try:
sys.exit(main()) sys.exit(main())
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
print(f"迁移失败:{exc}", file=sys.stderr) print(f"迁移失败:{exc}", file=sys.stderr)
sys.exit(1) sys.exit(1)
+12 -8
View File
@@ -22,6 +22,7 @@ from __future__ import annotations
import argparse import argparse
import json import json
import logging
import os import os
import re import re
import secrets import secrets
@@ -49,6 +50,8 @@ from pyvm.protocol import (
) )
from pyvm.session import SessionState, load_session from pyvm.session import SessionState, load_session
logger = logging.getLogger(__name__)
REPLAY = ROOT / "replay" REPLAY = ROOT / "replay"
DEFAULT_APPID = PAY_APPID DEFAULT_APPID = PAY_APPID
DEFAULT_SAVE_URL = f"https://api.unipay.qq.com/v1/r/{DEFAULT_APPID}/web_save" DEFAULT_SAVE_URL = f"https://api.unipay.qq.com/v1/r/{DEFAULT_APPID}/web_save"
@@ -103,7 +106,8 @@ def _load_cap(path: Path) -> dict:
d = attempt() d = attempt()
if isinstance(d, dict) and "C" in d: if isinstance(d, dict) and "C" in d:
return d return d
except Exception: except Exception as exc: # noqa: BLE001
logger.debug("deepCap 候选格式解析失败: %s", exc)
continue continue
raise ValueError(f"无法解析 deepCap 文件: {path}(前 80 字符: {raw[:80]!r})") raise ValueError(f"无法解析 deepCap 文件: {path}(前 80 字符: {raw[:80]!r})")
@@ -122,8 +126,8 @@ def parse_plaintext(path: str | Path) -> dict:
d = json.loads(raw) d = json.loads(raw)
if isinstance(d, dict): if isinstance(d, dict):
return {str(k): str(v) for k, v in d.items()} return {str(k): str(v) for k, v in d.items()}
except Exception: except Exception as exc: # noqa: BLE001
pass logger.debug("订单响应 JSON 解析失败: %s", exc)
fields: dict[str, str] = {} fields: dict[str, str] = {}
for kv in raw.split("&"): for kv in raw.split("&"):
k, _, v = kv.partition("=") k, _, v = kv.partition("=")
@@ -249,7 +253,7 @@ def cmd_submit(args) -> int:
return 0 return 0
print(f"❌ web_save ret:{ret}({js.get('err_code', '')})—— 见 case 踩坑记录") print(f"❌ web_save ret:{ret}({js.get('err_code', '')})—— 见 case 踩坑记录")
return 1 return 1
except Exception: except Exception: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
return 1 return 1
@@ -399,8 +403,8 @@ def cmd_mall_submit(args) -> int:
js = json.loads(raw) js = json.loads(raw)
if isinstance(js, dict): if isinstance(js, dict):
ret = js.get("ret", js.get("result_code", js.get("code"))) ret = js.get("ret", js.get("result_code", js.get("code")))
except Exception: except Exception as exc: # noqa: BLE001
pass logger.debug("支付响应 JSON 解析失败: %s", exc)
ok = ret in (0, "0") ok = ret in (0, "0")
if ok: if ok:
_write_private_text(out, raw) _write_private_text(out, raw)
@@ -999,8 +1003,8 @@ def cmd_mall_pay(args) -> int:
print("\n ══ 微信扫码支付(终端二维码)══") print("\n ══ 微信扫码支付(终端二维码)══")
try: try:
qr.terminal(compact=False) qr.terminal(compact=False)
except Exception: # noqa: BLE001 except Exception as exc: # noqa: BLE001
pass logger.debug("终端二维码输出失败: %s", exc)
return 0 return 0
+17 -21
View File
@@ -17,6 +17,7 @@ from __future__ import annotations
import itertools import itertools
import json import json
import math import math
import random
import urllib.parse import urllib.parse
from pathlib import Path from pathlib import Path
@@ -40,7 +41,7 @@ def _ic(x):
try: try:
f = float(x) f = float(x)
return 0 if math.isnan(f) else int(f) return 0 if math.isnan(f) else int(f)
except Exception: except Exception: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
return 0 return 0
@@ -79,7 +80,7 @@ def js_typeof(v):
return "number" return "number"
if isinstance(v, str): if isinstance(v, str):
return "string" return "string"
if isinstance(v, JSFunction) or isinstance(v, HostFunction): if isinstance(v, (JSFunction, HostFunction)):
return "function" return "function"
return "object" return "object"
@@ -99,7 +100,7 @@ def js_truthy(v):
def _nan_ok(x): def _nan_ok(x):
try: try:
return not (isinstance(x, float) and math.isnan(x)) return not (isinstance(x, float) and math.isnan(x))
except Exception: except Exception: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
return True return True
@@ -207,7 +208,7 @@ def js_num(v):
return v return v
try: try:
return float(v) return float(v)
except Exception: except Exception: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
return float("nan") return float("nan")
@@ -226,12 +227,12 @@ def js_eq(a, b):
if isinstance(a, (int, float)) and isinstance(b, str): if isinstance(a, (int, float)) and isinstance(b, str):
try: try:
return a == float(b) return a == float(b)
except Exception: except Exception: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
return False return False
if isinstance(b, (int, float)) and isinstance(a, str): if isinstance(b, (int, float)) and isinstance(a, str):
try: try:
return float(a) == b return float(a) == b
except Exception: except Exception: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
return False return False
return a == b return a == b
@@ -355,9 +356,7 @@ def js_set(obj, key, val):
pass pass
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(__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}"
) )
@@ -736,7 +735,7 @@ def h_parsefloat(this, s):
def h_isnan(this, x): def h_isnan(this, x):
try: try:
return math.isnan(float(x)) return math.isnan(float(x))
except Exception: except Exception: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
return True return True
@@ -896,7 +895,7 @@ class VM:
for x in v: for x in v:
try: try:
parts.append("%02x" % (int(x) & 255)) parts.append("%02x" % (int(x) & 255))
except Exception: except Exception: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
parts.append("??") parts.append("??")
self.out_hex = "".join(parts) self.out_hex = "".join(parts)
# 仅供离线排查 webSave 输出缓冲何时变化,生产路径默认不启用。 # 仅供离线排查 webSave 输出缓冲何时变化,生产路径默认不启用。
@@ -1527,9 +1526,7 @@ class VM:
if not ( if not (
isinstance(fn, (JSFunction, HostFunction)) or callable(fn) isinstance(fn, (JSFunction, HostFunction)) or callable(fn)
): ):
raise RuntimeError( raise RuntimeError(f"op97 non-function at u={u} fn={fn!r}")
"op97 non-function at u=%s fn=%r" % (u, fn)
)
js_set(C, dest, js_apply(fn, thisv, f)) js_set(C, dest, js_apply(fn, thisv, f))
elif op == 98: elif op == 98:
a = o[u + 1] a = o[u + 1]
@@ -1647,10 +1644,10 @@ class VM:
u += 1 u += 1
js_set(C, a, None) js_set(C, a, None)
else: else:
raise RuntimeError("unknown opcode %s at %s" % (op, u)) raise RuntimeError(f"unknown opcode {op} at {u}")
except _VMThrow as e: except _VMThrow as e:
if not d: if not d:
raise RuntimeError("VM uncaught throw: %s" % (e.value,)) raise RuntimeError(f"VM uncaught throw: {e.value}")
l = e.value l = e.value
u = d.pop() u = d.pop()
continue continue
@@ -1658,9 +1655,8 @@ class VM:
if not d: if not d:
raise raise
if not isinstance(d, list): if not isinstance(d, list):
raise RuntimeError( raise TypeError(
"d corrupted: %r (type %s) at trace %s" f"d corrupted: {d!r} (type {type(d).__name__}) at trace {self._trace[-3:]}"
% (d, type(d).__name__, self._trace[-3:])
) )
l = e l = e
u = d.pop() u = d.pop()
@@ -1746,14 +1742,14 @@ def decode_d(v):
vs = p[ci + 1 :] vs = p[ci + 1 :]
try: try:
obj.set(key, decode_d(json.loads(vs))) obj.set(key, decode_d(json.loads(vs)))
except Exception: except Exception: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
obj.set(key, decode_d(vs)) obj.set(key, decode_d(vs))
return obj return obj
if v.lstrip("-").isdigit(): if v.lstrip("-").isdigit():
return int(v) return int(v)
try: try:
return float(v) return float(v)
except Exception: except Exception: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
return v return v
+7 -3
View File
@@ -72,9 +72,13 @@ class MallSession:
f"mall xMidasOps 应为 59620(非 goods 59640),实际 {len(self.xmidas_ops)}" f"mall xMidasOps 应为 59620(非 goods 59640),实际 {len(self.xmidas_ops)}"
) )
mid = self.transform_input[10] mid = self.transform_input[10]
if isinstance(mid, list) and mid and isinstance(mid[0], list): if (
if len(mid[0]) != 624: isinstance(mid, list)
raise ValueError(f"624B 中间态长度 != 624: {len(mid[0])}") and mid
and isinstance(mid[0], list)
and len(mid[0]) != 624
):
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:
@@ -38,13 +38,13 @@ def get_official_orders(cookies: dict[str, str], count: int = 20) -> dict[str, A
except ValueError as exc: except ValueError as exc:
raise RuntimeError("订单状态查询返回非 JSON") from exc raise RuntimeError("订单状态查询返回非 JSON") from exc
if not isinstance(document, dict): if not isinstance(document, dict):
raise RuntimeError("订单状态查询响应格式异常") raise TypeError("订单状态查询响应格式异常")
if document.get("ret_code") not in (None, 0, "0"): if document.get("ret_code") not in (None, 0, "0"):
raise RuntimeError( raise RuntimeError(
f"订单状态查询失败: {document.get('ret_code')} {document.get('ret_msg', '')}" f"订单状态查询失败: {document.get('ret_code')} {document.get('ret_msg', '')}"
) )
if not isinstance(document.get("list", []), list): if not isinstance(document.get("list", []), list):
raise RuntimeError("订单状态查询响应缺少 list") raise TypeError("订单状态查询响应缺少 list")
return document return document
+36 -40
View File
@@ -13,6 +13,8 @@ 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
from pathlib import Path
from .algorithm import ( from .algorithm import (
UNDEF, UNDEF,
@@ -20,6 +22,7 @@ from .algorithm import (
JSDate, JSDate,
JSFunction, JSFunction,
JSObject, JSObject,
Window,
_ic, _ic,
h_decodeuri, h_decodeuri,
h_decodeuricomponent, h_decodeuricomponent,
@@ -57,6 +60,8 @@ from .algorithm import (
ushr, ushr,
) )
REPLAY = Path(__file__).resolve().parent.parent / "replay"
__all__ = ["REPLAY", "PagedooVM", "run_frame"] __all__ = ["REPLAY", "PagedooVM", "run_frame"]
@@ -66,28 +71,28 @@ __all__ = ["REPLAY", "PagedooVM", "run_frame"]
def _cmp_lt(a, b): def _cmp_lt(a, b):
try: try:
return a < b return a < b
except Exception: except Exception: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
return False return False
def _cmp_le(a, b): def _cmp_le(a, b):
try: try:
return a <= b return a <= b
except Exception: except Exception: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
return False return False
def _cmp_gt(a, b): def _cmp_gt(a, b):
try: try:
return a > b return a > b
except Exception: except Exception: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
return False return False
def _cmp_ge(a, b): def _cmp_ge(a, b):
try: try:
return a >= b return a >= b
except Exception: except Exception: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
return False return False
@@ -122,8 +127,8 @@ def h_arr_slice(this, a=None, b=None):
a = 0 a = 0
if b is None or b is UNDEF: if b is None or b is UNDEF:
b = n b = n
a = int(a) if a == a else 0 a = int(a) if not isinstance(a, float) or not math.isnan(a) else 0
b = int(b) if b == b else n b = int(b) if not isinstance(b, float) or not math.isnan(b) else n
if a < 0: if a < 0:
a = max(0, n + a) a = max(0, n + a)
if b < 0: if b < 0:
@@ -303,30 +308,29 @@ def _pg_index(obj, key):
return HostFunction( return HostFunction(
lambda this: this.t if isinstance(this, JSDate) else obj.t, "valueOf" lambda this: this.t if isinstance(this, JSDate) else obj.t, "valueOf"
) )
if isinstance(obj, str): if isinstance(obj, str) and isinstance(key, str):
if isinstance(key, str): if key == "length":
if key == "length": return len(obj)
return len(obj) if key == "charCodeAt":
if key == "charCodeAt": return HostFunction(h_char_code_at, "charCodeAt")
return HostFunction(h_char_code_at, "charCodeAt") if key == "charAt":
if key == "charAt": return HostFunction(h_char_at, "charAt")
return HostFunction(h_char_at, "charAt") if key == "indexOf":
if key == "indexOf": return HostFunction(h_str_indexof, "indexOf")
return HostFunction(h_str_indexof, "indexOf") if key == "slice":
if key == "slice": return HostFunction(h_str_slice, "slice")
return HostFunction(h_str_slice, "slice") if key == "split":
if key == "split": return HostFunction(h_str_split, "split")
return HostFunction(h_str_split, "split") if key == "toLowerCase":
if key == "toLowerCase": return HostFunction(h_str_tolower, "toLowerCase")
return HostFunction(h_str_tolower, "toLowerCase") if key == "toUpperCase":
if key == "toUpperCase": return HostFunction(h_str_toupper, "toUpperCase")
return HostFunction(h_str_toupper, "toUpperCase") if key == "toString":
if key == "toString": return HostFunction(h_str_tostring, "toString")
return HostFunction(h_str_tostring, "toString") if key == "substr":
if key == "substr": return HostFunction(h_str_substr, "substr")
return HostFunction(h_str_substr, "substr") if key == "substring":
if key == "substring": return HostFunction(h_str_substring, "substring")
return HostFunction(h_str_substring, "substring")
return js_index(obj, key) return js_index(obj, key)
@@ -486,15 +490,7 @@ class PagedooVM:
and len(self._host_log) < 2000 and len(self._host_log) < 2000
): ):
# 记录调用目标(简化) # 记录调用目标(简化)
_tgt = ( _tgt = o[u + 2]
o[u + 2]
if op in (4, 11, 44, 50)
else (
o[u + 2]
if op in (0, 18, 23, 26, 43, 48, 84, 107)
else o[u + 2]
)
)
try: try:
_tv = C[_tgt] if 0 <= _tgt < len(C) else UNDEF _tv = C[_tgt] if 0 <= _tgt < len(C) else UNDEF
_tr = ( _tr = (
@@ -505,7 +501,7 @@ class PagedooVM:
else repr(_tv)[:30] else repr(_tv)[:30]
) )
self._host_log.append((op, u, _tr)) self._host_log.append((op, u, _tr))
except Exception: except Exception: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self._host_log.append((op, u, "?")) self._host_log.append((op, u, "?"))
if ( if (
getattr(self, "_trace", None) is not None getattr(self, "_trace", None) is not None
+2 -2
View File
@@ -86,7 +86,7 @@ def validate_goods_materials(
def validate_mall_materials(transform_fixed: dict[str, Any]) -> None: def validate_mall_materials(transform_fixed: dict[str, Any]) -> None:
"""校验 mall 固定槽模板;动态槽仍由当前会话的 GetPayToken 填充。""" """校验 mall 固定槽模板;动态槽仍由当前会话的 GetPayToken 填充。"""
if not isinstance(transform_fixed, dict): if not isinstance(transform_fixed, dict):
raise ValueError("mall transform-fixed 必须是对象") raise TypeError("mall transform-fixed 必须是对象")
required = { required = {
str(index) for index in (1, 2, 3, 4, 5, 7, 8, 9, 11, 12, 13, 14, 15, 16, 17) str(index) for index in (1, 2, 3, 4, 5, 7, 8, 9, 11, 12, 13, 14, 15, 16, 17)
} }
@@ -95,7 +95,7 @@ def validate_mall_materials(transform_fixed: dict[str, Any]) -> None:
raise ValueError(f"mall transform-fixed 缺少槽: {', '.join(missing)}") raise ValueError(f"mall transform-fixed 缺少槽: {', '.join(missing)}")
for index in required: for index in required:
if not isinstance(transform_fixed[index], list): if not isinstance(transform_fixed[index], list):
raise ValueError(f"mall transform-fixed 槽 {index} 不是数组") raise TypeError(f"mall transform-fixed 槽 {index} 不是数组")
def goods_material_diagnostics( def goods_material_diagnostics(
View File
View File
View File
View File
+5 -1
View File
@@ -81,7 +81,10 @@ 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.IGNORECASE 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+",
@@ -282,6 +285,7 @@ def _check_payment_once(job_id: str) -> int:
text=True, text=True,
env=environment, env=environment,
timeout=90, timeout=90,
check=False,
) )
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
_safe_log( _safe_log(
+9 -6
View File
@@ -175,18 +175,21 @@ class TestHuyaAppLogin:
side_effect=DfpRegistrationError("注册链超时"), side_effect=DfpRegistrationError("注册链超时"),
) as m_reg, ) as m_reg,
patch("core.huya.app_login.requests.post") as m_post, patch("core.huya.app_login.requests.post") as m_post,
pytest.raises(HuyaAppLoginError),
): ):
with pytest.raises(HuyaAppLoginError): wup_password_login_raw("300023887", "pw")
wup_password_login_raw("300023887", "pw")
m_reg.assert_called_once() m_reg.assert_called_once()
m_post.assert_not_called() m_post.assert_not_called()
def test_login_cred_flow_registration_failure_is_explicit(self): def test_login_cred_flow_registration_failure_is_explicit(self):
"""login_cred_with_flow 注册失败同样包装为 HuyaAppLoginError 显式失败。""" """login_cred_with_flow 注册失败同样包装为 HuyaAppLoginError 显式失败。"""
with patch( with (
"core.huya.app_login.register_device", patch(
side_effect=DfpRegistrationError("注册链 HTTP 500"), "core.huya.app_login.register_device",
), pytest.raises(HuyaAppLoginError, match="注册失败"): side_effect=DfpRegistrationError("注册链 HTTP 500"),
),
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):
+4 -3
View File
@@ -3,7 +3,7 @@
import gzip import gzip
import logging import logging
import tempfile import tempfile
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
from pathlib import Path from pathlib import Path
from utils.logger import ( from utils.logger import (
@@ -40,7 +40,8 @@ class TestLogger:
archives = list(Path(tmpdir).glob("app-2026-08-28.log.*.gz")) archives = list(Path(tmpdir).glob("app-2026-08-28.log.*.gz"))
assert len(archives) == 1 assert len(archives) == 1
archived_content = gzip.open(archives[0], "rt", encoding="utf-8").read() with gzip.open(archives[0], "rt", encoding="utf-8") as archive:
archived_content = archive.read()
current_content = log_path.read_text(encoding="utf-8") current_content = log_path.read_text(encoding="utf-8")
combined = archived_content + current_content combined = archived_content + current_content
assert "super-secret" not in combined assert "super-secret" not in combined
@@ -50,7 +51,7 @@ class TestLogger:
def test_daily_log_switches_to_a_new_dated_file(self): def test_daily_log_switches_to_a_new_dated_file(self):
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir:
today = datetime.now().date() today = datetime.now(UTC).date()
old_path = ( old_path = (
Path(tmpdir) / f"app-{(today - timedelta(days=1)).isoformat()}.log" Path(tmpdir) / f"app-{(today - timedelta(days=1)).isoformat()}.log"
) )
+11 -9
View File
@@ -8,7 +8,7 @@ import os
import re import re
import shutil import shutil
import sys import sys
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
from logging.handlers import BaseRotatingHandler from logging.handlers import BaseRotatingHandler
from pathlib import Path from pathlib import Path
@@ -35,7 +35,9 @@ 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.IGNORECASE) 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))
@@ -101,7 +103,7 @@ class _SizeAndDayRotatingFileHandler(BaseRotatingHandler):
super().__init__(str(filename), "a", encoding="utf-8", delay=True) super().__init__(str(filename), "a", encoding="utf-8", delay=True)
self.max_bytes = max_bytes self.max_bytes = max_bytes
self.retention_days = retention_days self.retention_days = retention_days
self._active_day = datetime.now().date() self._active_day = datetime.now(UTC).date()
path = Path(filename) path = Path(filename)
self._log_dir = path.parent self._log_dir = path.parent
self._suffix = path.suffix self._suffix = path.suffix
@@ -113,7 +115,7 @@ class _SizeAndDayRotatingFileHandler(BaseRotatingHandler):
) )
def shouldRollover(self, record: logging.LogRecord) -> bool: def shouldRollover(self, record: logging.LogRecord) -> bool:
if datetime.now().date() != self._active_day: if datetime.now(UTC).date() != self._active_day:
return True return True
if self.stream is None: if self.stream is None:
self.stream = self._open() self.stream = self._open()
@@ -126,7 +128,7 @@ class _SizeAndDayRotatingFileHandler(BaseRotatingHandler):
self.stream.close() self.stream.close()
self.stream = None self.stream = None
source = Path(self.baseFilename) source = Path(self.baseFilename)
current_day = datetime.now().date() current_day = datetime.now(UTC).date()
if current_day != self._active_day: if current_day != self._active_day:
# 每日文件本身已带日期,跨日时直接切换到新文件,无需再移动旧文件。 # 每日文件本身已带日期,跨日时直接切换到新文件,无需再移动旧文件。
self.baseFilename = os.fspath(self._path_for_day(current_day).resolve()) self.baseFilename = os.fspath(self._path_for_day(current_day).resolve())
@@ -134,7 +136,7 @@ class _SizeAndDayRotatingFileHandler(BaseRotatingHandler):
self._delete_expired_archives() self._delete_expired_archives()
return return
if source.exists() and source.stat().st_size: if source.exists() and source.stat().st_size:
stamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") stamp = datetime.now(UTC).strftime("%Y-%m-%d_%H-%M-%S")
archive = source.with_name(f"{source.name}.{stamp}.{os.getpid()}.gz") archive = source.with_name(f"{source.name}.{stamp}.{os.getpid()}.gz")
sequence = 1 sequence = 1
while archive.exists(): while archive.exists():
@@ -148,10 +150,10 @@ class _SizeAndDayRotatingFileHandler(BaseRotatingHandler):
self._delete_expired_archives() self._delete_expired_archives()
def _delete_expired_archives(self) -> None: def _delete_expired_archives(self) -> None:
cutoff = datetime.now() - timedelta(days=self.retention_days) cutoff = datetime.now(UTC) - timedelta(days=self.retention_days)
for archive in self._log_dir.glob(f"{self._filename_prefix}-*.log*.gz"): for archive in self._log_dir.glob(f"{self._filename_prefix}-*.log*.gz"):
try: try:
if datetime.fromtimestamp(archive.stat().st_mtime) < cutoff: if datetime.fromtimestamp(archive.stat().st_mtime, UTC) < cutoff:
archive.unlink() archive.unlink()
except OSError: except OSError:
continue continue
@@ -226,7 +228,7 @@ def setup_logger(
if log_file: if log_file:
file_path = Path(log_file).expanduser() file_path = Path(log_file).expanduser()
elif log_dir: elif log_dir:
file_path = Path(log_dir).expanduser() / f"app-{datetime.now():%Y-%m-%d}.log" file_path = Path(log_dir).expanduser() / f"app-{datetime.now(UTC):%Y-%m-%d}.log"
else: else:
return return
file_path.parent.mkdir(parents=True, exist_ok=True) file_path.parent.mkdir(parents=True, exist_ok=True)
+2 -2
View File
@@ -109,7 +109,7 @@ def decrypt_value(value: str | None) -> str | None:
for candidate in _key_candidates(): for candidate in _key_candidates():
try: try:
return _decrypt_with_candidate(value, candidate) return _decrypt_with_candidate(value, candidate)
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
last_error = exc last_error = exc
raise ValueError( raise ValueError(
"敏感字段解密失败,请确认 APP_ENCRYPTION_KEY 是否正确" "敏感字段解密失败,请确认 APP_ENCRYPTION_KEY 是否正确"
@@ -124,7 +124,7 @@ def decrypt_value_with_key_name(value: str) -> tuple[str, str]:
for candidate in _key_candidates(): for candidate in _key_candidates():
try: try:
return _decrypt_with_candidate(value, candidate), candidate.name return _decrypt_with_candidate(value, candidate), candidate.name
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
last_error = exc last_error = exc
raise ValueError( raise ValueError(
"敏感字段解密失败,请确认 APP_ENCRYPTION_KEY 是否正确" "敏感字段解密失败,请确认 APP_ENCRYPTION_KEY 是否正确"
-1
View File
@@ -1,6 +1,5 @@
"""FastAPI 依赖注入""" """FastAPI 依赖注入"""
from fastapi import Depends, HTTPException, Request, WebSocket, status from fastapi import Depends, HTTPException, Request, WebSocket, status
from fastapi.security import OAuth2PasswordBearer from fastapi.security import OAuth2PasswordBearer
from jose import JWTError from jose import JWTError
+2 -2
View File
@@ -483,7 +483,7 @@ def _check_cookies(ids: str, db: Session, current: User, *, detailed: bool) -> d
for future in as_completed(futures): for future in as_completed(futures):
try: try:
results.append(future.result()) results.append(future.result())
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
task = futures[future] task = futures[future]
results.append( results.append(
{ {
@@ -623,7 +623,7 @@ def _start_relogin_tasks(
def run_relogin_batch(): def run_relogin_batch():
try: try:
runner.run() runner.run()
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
runner._push_log("error", f"批量重新登录异常: {exc}") runner._push_log("error", f"批量重新登录异常: {exc}")
message = f"重新登录异常: {exc}(旧 Cookie 已保留)" message = f"重新登录异常: {exc}(旧 Cookie 已保留)"
for task_id in task_ids: for task_id in task_ids:
+5 -3
View File
@@ -852,12 +852,14 @@ def stop_auto_register_batch(
) )
def retry_auto_register_batch( def retry_auto_register_batch(
batch_id: str, batch_id: str,
req: HuyaAutoRegisterRetryRequest = HuyaAutoRegisterRetryRequest.model_construct(), req: HuyaAutoRegisterRetryRequest | None = None,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current: User = Depends(get_current_user), current: User = Depends(get_current_user),
): ):
"""继续批次:默认从停止处往下跑(跳过成功与已失败);可传 mode 改行为。""" """继续批次:默认从停止处往下跑(跳过成功与已失败);可传 mode 改行为。"""
_require_huya_perm(current, "huya:import") _require_huya_perm(current, "huya:import")
if req is None:
req = HuyaAutoRegisterRetryRequest.model_construct()
use_proxy = req.use_proxy use_proxy = req.use_proxy
# 先看历史批次是否用过代理 # 先看历史批次是否用过代理
snapshot = huya_register_registry.get_snapshot(batch_id) snapshot = huya_register_registry.get_snapshot(batch_id)
@@ -1075,7 +1077,7 @@ def password_login_selected_accounts(
"account": _account_out(account, include_cookie=include_cookie), "account": _account_out(account, include_cookie=include_cookie),
} }
) )
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
account.status = "login_failed" account.status = "login_failed"
account.updated_at = datetime.now(UTC) account.updated_at = datetime.now(UTC)
db.commit() db.commit()
@@ -1239,7 +1241,7 @@ def app_password_login_selected_accounts(
"account": _account_out(account, include_cookie=include_cookie), "account": _account_out(account, include_cookie=include_cookie),
} }
) )
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
account.status = "login_failed" account.status = "login_failed"
account.updated_at = datetime.now(UTC) account.updated_at = datetime.now(UTC)
db.commit() db.commit()
+5 -3
View File
@@ -58,9 +58,11 @@ async def create_batch(
acc = db.query(Account).filter(Account.id == aid).first() acc = db.query(Account).filter(Account.id == aid).first()
if not acc: if not acc:
continue continue
if not user_has_permission(current, "login:view_all"): if (
if acc.assigned_to != current.id: not user_has_permission(current, "login:view_all")
continue and acc.assigned_to != current.id
):
continue
valid_ids.append(aid) valid_ids.append(aid)
if not valid_ids: if not valid_ids:
+1 -1
View File
@@ -33,7 +33,7 @@ def hash_password(password: str) -> str:
def verify_password(plain: str, hashed: str) -> bool: def verify_password(plain: str, hashed: str) -> bool:
try: try:
return bcrypt.checkpw(plain.encode("utf-8")[:72], hashed.encode("utf-8")) return bcrypt.checkpw(plain.encode("utf-8")[:72], hashed.encode("utf-8"))
except Exception: except Exception: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
return False return False
@@ -258,7 +258,7 @@ class AccountCheckRunner:
stop_event=self._stop, stop_event=self._stop,
api_strategy=WgapiLoginAPI(), api_strategy=WgapiLoginAPI(),
).check_account() ).check_account()
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self._set_item( self._set_item(
index, status="error", message=f"检测异常: {exc}", finished_at=_now() index, status="error", message=f"检测异常: {exc}", finished_at=_now()
) )
@@ -319,7 +319,7 @@ class AccountCheckRunner:
with self._lock: with self._lock:
items = list(self.batch.items) items = list(self.batch.items)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") timestamp = datetime.now(UTC).strftime("%Y%m%d_%H%M%S")
buffer = io.BytesIO() buffer = io.BytesIO()
with zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_DEFLATED) as zf: with zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_DEFLATED) as zf:
for status in EXPORT_STATUS_ORDER: for status in EXPORT_STATUS_ORDER:
@@ -367,7 +367,7 @@ class AccountCheckRunner:
for future in as_completed(futures): for future in as_completed(futures):
future.result() future.result()
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self._set_batch( self._set_batch(
status="error", message=f"批次执行异常: {exc}", finished_at=_now() status="error", message=f"批次执行异常: {exc}", finished_at=_now()
) )
+2 -2
View File
@@ -52,7 +52,7 @@ def check_douyu_cookie(cookie: str) -> dict:
if isinstance(fish_data, dict) if isinstance(fish_data, dict)
else "响应异常" else "响应异常"
) )
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
fish_msg = f"请求失败: {exc}" fish_msg = f"请求失败: {exc}"
level_ok = False level_ok = False
@@ -83,7 +83,7 @@ def check_douyu_cookie(cookie: str) -> dict:
if isinstance(level_data, dict) if isinstance(level_data, dict)
else "响应异常" else "响应异常"
) )
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
level_msg = f"请求失败: {exc}" level_msg = f"请求失败: {exc}"
valid = fish_ok and level_ok valid = fish_ok and level_ok
+2 -2
View File
@@ -133,7 +133,7 @@ class DouyuBatchRunner(
if "task" in locals() and task: if "task" in locals() and task:
self._mark_task(worker_db, task, "failed", str(exc)) self._mark_task(worker_db, task, "failed", str(exc))
self._push_log("warning", f"斗鱼任务失败: {exc}") self._push_log("warning", f"斗鱼任务失败: {exc}")
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
if "task" in locals() and task: if "task" in locals() and task:
self._mark_task(worker_db, task, "error", str(exc)) self._mark_task(worker_db, task, "error", str(exc))
self._push_log("error", f"斗鱼任务异常: {exc}") self._push_log("error", f"斗鱼任务异常: {exc}")
@@ -177,7 +177,7 @@ class DouyuBatchRunner(
for future in as_completed(futures): for future in as_completed(futures):
try: try:
future.result() future.result()
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self._push_log("error", f"Worker 异常: {exc}") self._push_log("error", f"Worker 异常: {exc}")
if self._stop.is_set(): if self._stop.is_set():
+2 -2
View File
@@ -386,9 +386,9 @@ class BindMixin:
] ]
if not bound: if not bound:
return None return None
return sorted( return min(
bound, key=lambda info: prefer.index(str(info.get("act_alias") or "")) bound, key=lambda info: prefer.index(str(info.get("act_alias") or ""))
)[0] )
def _pick_baseline_bind_info( def _pick_baseline_bind_info(
self, self,
+2 -2
View File
@@ -138,7 +138,7 @@ class DouyuBatchRunnerCore:
if proxy_url: if proxy_url:
return {"http": proxy_url, "https": proxy_url} return {"http": proxy_url, "https": proxy_url}
self._push_log("warning", "代理 API 未返回可用代理, 本任务降级直连") self._push_log("warning", "代理 API 未返回可用代理, 本任务降级直连")
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self._push_log("warning", f"取代理失败, 本任务降级直连: {exc}") self._push_log("warning", f"取代理失败, 本任务降级直连: {exc}")
return None return None
@@ -193,7 +193,7 @@ class DouyuBatchRunnerCore:
return return
try: try:
payload = douyu_task_payload(task) payload = douyu_task_payload(task)
except Exception: except Exception: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
logger.exception("[douyu] 推送任务状态失败: task_id={}", task.id) logger.exception("[douyu] 推送任务状态失败: task_id={}", task.id)
return return
event = { event = {
+6 -6
View File
@@ -136,7 +136,7 @@ class DonateMixin:
) )
baseline_points = baseline["esports_points"] baseline_points = baseline["esports_points"]
db.commit() db.commit()
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self._push_log("warning", f"赠送{gift_name}前刷新电竞积分失败: {exc}") self._push_log("warning", f"赠送{gift_name}前刷新电竞积分失败: {exc}")
result = client.donate_esports_gift( result = client.donate_esports_gift(
@@ -158,7 +158,7 @@ class DonateMixin:
refresh_errors = [] refresh_errors = []
try: try:
result.update(self._refresh_account_gold_balance(client, account)) result.update(self._refresh_account_gold_balance(client, account))
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
refresh_errors.append(f"鱼翅余额: {exc}") refresh_errors.append(f"鱼翅余额: {exc}")
try: try:
points_result = self._refresh_esports_handbook( points_result = self._refresh_esports_handbook(
@@ -171,7 +171,7 @@ class DonateMixin:
and points_result["esports_points"] is not None and points_result["esports_points"] is not None
and points_result["esports_points"] != baseline_points and points_result["esports_points"] != baseline_points
) )
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
refresh_errors.append(f"电竞积分: {exc}") refresh_errors.append(f"电竞积分: {exc}")
if refresh_errors: if refresh_errors:
result["refresh_errors"] = refresh_errors result["refresh_errors"] = refresh_errors
@@ -240,7 +240,7 @@ class DonateMixin:
) )
db.commit() db.commit()
baseline_points = baseline_result["points"] baseline_points = baseline_result["points"]
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self._push_log("warning", f"赠送精英令前刷新积分失败: {exc}") self._push_log("warning", f"赠送精英令前刷新积分失败: {exc}")
result = client.donate_elite_gift( result = client.donate_elite_gift(
gift_count=gift_count, gift_count=gift_count,
@@ -252,7 +252,7 @@ class DonateMixin:
refresh_errors = [] refresh_errors = []
try: try:
result.update(self._refresh_account_gold_balance(client, account)) result.update(self._refresh_account_gold_balance(client, account))
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
refresh_errors.append(f"鱼翅余额: {exc}") refresh_errors.append(f"鱼翅余额: {exc}")
try: try:
result.update( result.update(
@@ -268,7 +268,7 @@ class DonateMixin:
gift_count, gift_count,
) )
) )
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
refresh_errors.append(f"积分: {exc}") refresh_errors.append(f"积分: {exc}")
if refresh_errors: if refresh_errors:
result["refresh_errors"] = refresh_errors result["refresh_errors"] = refresh_errors
+2 -2
View File
@@ -106,7 +106,7 @@ class GoldMixin:
f"鱼翅支付码已生成,等待到账(当前鱼翅 {last_gold if last_gold is not None else '-'}", f"鱼翅支付码已生成,等待到账(当前鱼翅 {last_gold if last_gold is not None else '-'}",
result, result,
) )
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
poll_count += 1 poll_count += 1
result["payment_poll_count"] = poll_count result["payment_poll_count"] = poll_count
result["payment_poll_error"] = str(exc) result["payment_poll_error"] = str(exc)
@@ -260,7 +260,7 @@ class GoldMixin:
baseline = self._refresh_account_gold_balance(client, account) baseline = self._refresh_account_gold_balance(client, account)
baseline_gold = baseline["gold_balance"] baseline_gold = baseline["gold_balance"]
db.commit() db.commit()
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self._push_log("warning", f"生成鱼翅码前刷新余额失败: {exc}") self._push_log("warning", f"生成鱼翅码前刷新余额失败: {exc}")
result = client.create_gold_qr( result = client.create_gold_qr(
amount=amount, pay_type=int(config["gold_pay_type"]) amount=amount, pay_type=int(config["gold_pay_type"])
+4 -4
View File
@@ -202,7 +202,7 @@ class GoodsMixin:
result.update( result.update(
self._refresh_account_points(client, account, cookie, ctn=ctn) self._refresh_account_points(client, account, cookie, ctn=ctn)
) )
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self._push_log("warning", f"支付锁单后刷新积分失败: {exc}") self._push_log("warning", f"支付锁单后刷新积分失败: {exc}")
self._mark_task( self._mark_task(
db, db,
@@ -493,7 +493,7 @@ class GoodsMixin:
points_refresh = self._refresh_account_points( points_refresh = self._refresh_account_points(
client, account, cookie, ctn=ctn client, account, cookie, ctn=ctn
) )
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self._push_log("warning", f"兑换后刷新积分失败: {exc}") self._push_log("warning", f"兑换后刷新积分失败: {exc}")
self._mark_task( self._mark_task(
db, db,
@@ -549,7 +549,7 @@ class GoodsMixin:
) )
baseline_points = baseline["esports_points"] baseline_points = baseline["esports_points"]
db.commit() db.commit()
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self._push_log("warning", f"兑换电竞皮肤前刷新积分失败: {exc}") self._push_log("warning", f"兑换电竞皮肤前刷新积分失败: {exc}")
result = client.exchange_esports_goods( result = client.exchange_esports_goods(
@@ -571,7 +571,7 @@ class GoodsMixin:
) )
result.update(points_result) result.update(points_result)
result["esports_points_after_exchange"] = points_result["esports_points"] result["esports_points_after_exchange"] = points_result["esports_points"]
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
result["esports_points_refresh_error"] = str(exc) result["esports_points_refresh_error"] = str(exc)
account.esports_bind_status = "esports_goods_exchanged" account.esports_bind_status = "esports_goods_exchanged"
+3 -3
View File
@@ -95,7 +95,7 @@ class ManualMixin:
f"精英宝典支付码已生成,等待开通到账(当前积分 {last_points if last_points is not None else '-'}", f"精英宝典支付码已生成,等待开通到账(当前积分 {last_points if last_points is not None else '-'}",
result, result,
) )
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
poll_count += 1 poll_count += 1
result["payment_poll_count"] = poll_count result["payment_poll_count"] = poll_count
result["payment_poll_error"] = str(exc) result["payment_poll_error"] = str(exc)
@@ -184,7 +184,7 @@ class ManualMixin:
f"积分 {last_manual_score if last_manual_score is not None else '-'}", f"积分 {last_manual_score if last_manual_score is not None else '-'}",
result, result,
) )
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
poll_count += 1 poll_count += 1
result["payment_poll_count"] = poll_count result["payment_poll_count"] = poll_count
result["payment_poll_error"] = str(exc) result["payment_poll_error"] = str(exc)
@@ -276,7 +276,7 @@ class ManualMixin:
baseline_manual_type = baseline_result["esports_manual_type"] baseline_manual_type = baseline_result["esports_manual_type"]
baseline_manual_score = baseline_result["esports_manual_score"] baseline_manual_score = baseline_result["esports_manual_score"]
db.commit() db.commit()
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self._push_log("warning", f"生成电竞手册支付码前查询活动状态失败: {exc}") self._push_log("warning", f"生成电竞手册支付码前查询活动状态失败: {exc}")
if baseline_manual_type is not None and baseline_manual_type >= 1: if baseline_manual_type is not None and baseline_manual_type >= 1:
+5 -5
View File
@@ -135,7 +135,7 @@ class XpdMixin:
result["before_role_name"] = str(before.get("role_name") or "") result["before_role_name"] = str(before.get("role_name") or "")
result["before_area_name"] = str(before.get("area_name") or "") result["before_area_name"] = str(before.get("area_name") or "")
result["before_plat_name"] = str(before.get("plat_name") or "") result["before_plat_name"] = str(before.get("plat_name") or "")
except Exception: except Exception: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
result["before_bound"] = False result["before_bound"] = False
result["before_role_name"] = "" result["before_role_name"] = ""
result["bind_polling"] = True result["bind_polling"] = True
@@ -208,7 +208,7 @@ class XpdMixin:
f"等待扫码绑定(第 {poll_count} 次)", f"等待扫码绑定(第 {poll_count} 次)",
result, result,
) )
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
poll_count += 1 poll_count += 1
result["bind_poll_count"] = poll_count result["bind_poll_count"] = poll_count
result["bind_poll_error"] = str(exc) result["bind_poll_error"] = str(exc)
@@ -259,7 +259,7 @@ class XpdMixin:
else: else:
account.xpd_game_name = role_name account.xpd_game_name = role_name
account.updated_at = datetime.now(UTC) account.updated_at = datetime.now(UTC)
except Exception: except Exception: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
account.xpd_game_name = role_name account.xpd_game_name = role_name
account.updated_at = datetime.now(UTC) account.updated_at = datetime.now(UTC)
account.xpd_bind_status = "xpd_bound" account.xpd_bind_status = "xpd_bound"
@@ -409,8 +409,8 @@ class XpdMixin:
plat = str(role_plat) if role_plat not in (None, "") else plat plat = str(role_plat) if role_plat not in (None, "") else plat
areaid = str(role_area) or areaid areaid = str(role_area) or areaid
self._apply_xpd_role_to_account(account, role, role_area) self._apply_xpd_role_to_account(account, role, role_area)
except Exception: except Exception as exc: # noqa: BLE001
pass self._push_log("debug", f"小店角色信息补全失败: {exc}")
if not openid or not roleid: if not openid or not roleid:
self._mark_task( self._mark_task(
db, task, "failed", "未获取到小店绑定角色,请先生成二维码扫码绑定" db, task, "failed", "未获取到小店绑定角色,请先生成二维码扫码绑定"
+2 -2
View File
@@ -690,7 +690,7 @@ class HuyaRegisterRunner:
) )
try: try:
account_id, username, uid = self._save_success(index, result) account_id, username, uid = self._save_success(index, result)
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
status = "error" status = "error"
cookie = "" cookie = ""
full_cookie = "" full_cookie = ""
@@ -779,7 +779,7 @@ class HuyaRegisterRunner:
for future in as_completed(futures): for future in as_completed(futures):
future.result() future.result()
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
with self._lock: with self._lock:
self.batch.status = "error" self.batch.status = "error"
self.batch.message = f"批次执行异常: {exc}" self.batch.message = f"批次执行异常: {exc}"
+3 -3
View File
@@ -118,7 +118,7 @@ class HuyaBatchRunner(
self._push_log("success", f"[{current}] {name} {task.message}") self._push_log("success", f"[{current}] {name} {task.message}")
else: else:
self._push_log("error", f"[{current}] {name} {task.message}") self._push_log("error", f"[{current}] {name} {task.message}")
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self._mark_task(worker_db, task, "error", f"执行异常: {exc}") self._mark_task(worker_db, task, "error", f"执行异常: {exc}")
self._push_log("error", f"[{current}] {name} 执行异常: {exc}") self._push_log("error", f"[{current}] {name} 执行异常: {exc}")
finally: finally:
@@ -196,12 +196,12 @@ class HuyaBatchRunner(
for future in as_completed(futures): for future in as_completed(futures):
try: try:
future.result() future.result()
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self._push_log("error", f"虎牙 Worker 异常: {exc}") self._push_log("error", f"虎牙 Worker 异常: {exc}")
self._push_log("info", f"虎牙批次 {self.batch_id} 完成") self._push_log("info", f"虎牙批次 {self.batch_id} 完成")
self._push_log("result", "") self._push_log("result", "")
except Exception as exc: except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self._push_log("error", f"虎牙批次执行异常: {exc}") self._push_log("error", f"虎牙批次执行异常: {exc}")
self._push_log("result", "") self._push_log("result", "")
finally: finally:
+1 -1
View File
@@ -90,7 +90,7 @@ class HuyaBatchRunnerCore:
def _format_local_time(timestamp: int) -> str: def _format_local_time(timestamp: int) -> str:
if not timestamp: if not timestamp:
return "" return ""
return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S") return datetime.fromtimestamp(timestamp, UTC).strftime("%Y-%m-%d %H:%M:%S")
@staticmethod @staticmethod
def _parse_scheduled_time(value) -> datetime | None: def _parse_scheduled_time(value) -> datetime | None:
+7 -4
View File
@@ -267,10 +267,13 @@ class GoodsMixin:
if self.payload.get("scheduled_at") and scheduled_at is None: if self.payload.get("scheduled_at") and scheduled_at is None:
self._mark_task(worker_db, task, "failed", "定时兑换时间格式无效") self._mark_task(worker_db, task, "failed", "定时兑换时间格式无效")
return return
if scheduled_at and scheduled_at.timestamp() > time.time(): if (
if not self._wait_until(scheduled_at, uid): scheduled_at
self._mark_task(worker_db, task, "stopped", "兑换任务已停止") and scheduled_at.timestamp() > time.time()
return and not self._wait_until(scheduled_at, uid)
):
self._mark_task(worker_db, task, "stopped", "兑换任务已停止")
return
client: Any = HuyaHttpClient( client: Any = HuyaHttpClient(
logger=lambda msg: self._push_log("info", f"[{uid}] {msg}") logger=lambda msg: self._push_log("info", f"[{uid}] {msg}")
+2 -3
View File
@@ -348,9 +348,8 @@ def cleanup_orphan_huya_tasks(
query = db.query(HuyaTask).filter(HuyaTask.status.in_(statuses)) query = db.query(HuyaTask).filter(HuyaTask.status.in_(statuses))
if batch_id: if batch_id:
query = query.filter(HuyaTask.batch_id == batch_id) query = query.filter(HuyaTask.batch_id == batch_id)
elif active_batch_ids is not None: elif active_batch_ids is not None and active_batch_ids:
if active_batch_ids: query = query.filter(~HuyaTask.batch_id.in_(list(active_batch_ids)))
query = query.filter(~HuyaTask.batch_id.in_(list(active_batch_ids)))
# active_batch_ids is None 且未指定 batch_id:清理全部匹配状态 # active_batch_ids is None 且未指定 batch_id:清理全部匹配状态
tasks = query.all() tasks = query.all()
+14 -10
View File
@@ -372,7 +372,7 @@ class LoginBatchRunner:
f"[{current}] {acc_info['username']} {action_name}失败: {result.message}", f"[{current}] {acc_info['username']} {action_name}失败: {result.message}",
) )
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
if self.mode == "relogin": if self.mode == "relogin":
task.status = "relogin_failed" task.status = "relogin_failed"
task.message = f"重新登录异常: {e}(旧 Cookie 已保留)" task.message = f"重新登录异常: {e}(旧 Cookie 已保留)"
@@ -462,10 +462,12 @@ class LoginBatchRunner:
if not acc: if not acc:
self._push_log("warning", f"跳过无账号的任务 #{task_id}") self._push_log("warning", f"跳过无账号的任务 #{task_id}")
continue continue
if "login:view_all" not in self.creator_permissions: if (
if acc.assigned_to != self.created_by: "login:view_all" not in self.creator_permissions
self._push_log("warning", f"跳过无权账号: {acc.username}") and acc.assigned_to != self.created_by
continue ):
self._push_log("warning", f"跳过无权账号: {acc.username}")
continue
_append_task_info(task, acc) _append_task_info(task, acc)
else: else:
seen_account_ids = set() seen_account_ids = set()
@@ -482,10 +484,12 @@ class LoginBatchRunner:
if not acc: if not acc:
continue continue
# 权限检查:客服只能跑分配给自己的 # 权限检查:客服只能跑分配给自己的
if "login:view_all" not in self.creator_permissions: if (
if acc.assigned_to != self.created_by: "login:view_all" not in self.creator_permissions
self._push_log("warning", f"跳过无权账号: {acc.username}") and acc.assigned_to != self.created_by
continue ):
self._push_log("warning", f"跳过无权账号: {acc.username}")
continue
# 一个斗鱼账号只保留一条成功 CK:再次普通登录时更新最新成功记录。 # 一个斗鱼账号只保留一条成功 CK:再次普通登录时更新最新成功记录。
latest_success_task = ( latest_success_task = (
@@ -563,7 +567,7 @@ class LoginBatchRunner:
for future in as_completed(futures): for future in as_completed(futures):
try: try:
future.result() future.result()
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
self._push_log("error", f"Worker 异常: {e}") self._push_log("error", f"Worker 异常: {e}")
self._push_log("info", f"批量{action_name}任务 {batch_id} 完成") self._push_log("info", f"批量{action_name}任务 {batch_id} 完成")
+3 -3
View File
@@ -189,7 +189,7 @@ class ProxyService:
push("error", "未配置代理地址或API") push("error", "未配置代理地址或API")
push("result", "") push("result", "")
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
push("error", f"测试异常: {e}") push("error", f"测试异常: {e}")
push("result", "") push("result", "")
@@ -254,7 +254,7 @@ class ProxyService:
if whitelist_ip: if whitelist_ip:
local_ip = whitelist_ip local_ip = whitelist_ip
push("info", f"从代理API获取到本机IP: {local_ip}") push("info", f"从代理API获取到本机IP: {local_ip}")
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
push("warning", f"代理API请求失败: {e}") push("warning", f"代理API请求失败: {e}")
if not local_ip: if not local_ip:
@@ -293,7 +293,7 @@ class ProxyService:
push("success" if sync_ok else "error", f"白名单同步: {sync_msg}") push("success" if sync_ok else "error", f"白名单同步: {sync_msg}")
push("result", "") push("result", "")
except Exception as e: except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
push("error", f"测试异常: {e}") push("error", f"测试异常: {e}")
push("result", "") push("result", "")