mirror of
https://github.com/rachelos/we-mp-rss.git
synced 2026-11-03 17:01:40 +08:00
feat(weread): multi-account quota failover
- wx.lic 支持 weread_data.accounts: [{cookie, vid, name}, ...] 多账号数组
- 旧版单账号 weread_data.cookie 格式自动兼容
- 新增 _switch_weread_account() round-robin 切号
- 新增 _weread_http_get() 带 failover:检测 -2014/-2041/-2012/499 自动切下一个账号重试
- _weread_get / _get_mp_articles_page / _get_mp_cover / _get_mp_content 全部走新方法
- 不涉及 req/resp 接口变更,调用方零改动
This commit is contained in:
+125
-24
@@ -58,9 +58,16 @@ class MpsWeread(WxGather):
|
||||
self._weread_ticket: str = ""
|
||||
self._weread_vid: str = ""
|
||||
self._weread_name: str = ""
|
||||
# 多账号:wx.lic 的 weread_data.accounts 数组,或包装单账号旧格式
|
||||
self._weread_accounts: list = []
|
||||
self._current_account_idx: int = 0
|
||||
|
||||
def _load_weread_auth(self):
|
||||
"""加载微信读书的 Cookie"""
|
||||
"""加载微信读书的 Cookie
|
||||
|
||||
支持多账号:``weread_data.accounts`` 为数组,每个元素是独立 cookie/vid/name。
|
||||
旧版单账号 ``weread_data.cookie`` 格式自动兼容。
|
||||
"""
|
||||
from core.config import Config, cfg as app_cfg
|
||||
import os
|
||||
|
||||
@@ -78,33 +85,130 @@ class MpsWeread(WxGather):
|
||||
except Exception:
|
||||
weread_data = {}
|
||||
|
||||
self._weread_cookies = app_cfg.get("weread.cookie", "") or weread_data.get("cookie", "")
|
||||
self._weread_ticket = app_cfg.get("weread.ticket", "") or weread_data.get("ticket", "")
|
||||
self._weread_vid = app_cfg.get("weread.vid", "") or weread_data.get("vid", "")
|
||||
self._weread_name = weread_data.get("name", "")
|
||||
# 构建多账号列表:兼容单账号旧格式
|
||||
accounts = weread_data.get("accounts", None)
|
||||
if accounts and isinstance(accounts, list):
|
||||
self._weread_accounts = accounts
|
||||
else:
|
||||
# 单账号旧格式 → 包装成列表
|
||||
if weread_data.get("cookie"):
|
||||
self._weread_accounts = [weread_data]
|
||||
else:
|
||||
self._weread_accounts = []
|
||||
|
||||
# 选择当前激活账号:优先 env 变量覆盖,否则用第一个
|
||||
self._current_account_idx = 0
|
||||
self._weread_cookies = app_cfg.get("weread.cookie", "") or (
|
||||
self._weread_accounts[0].get("cookie", "") if self._weread_accounts else ""
|
||||
)
|
||||
self._weread_ticket = app_cfg.get("weread.ticket", "") or (
|
||||
self._weread_accounts[0].get("ticket", "") if self._weread_accounts else ""
|
||||
)
|
||||
self._weread_vid = app_cfg.get("weread.vid", "") or (
|
||||
self._weread_accounts[0].get("vid", "") if self._weread_accounts else ""
|
||||
)
|
||||
self._weread_name = (
|
||||
self._weread_accounts[0].get("name", "") if self._weread_accounts else ""
|
||||
)
|
||||
|
||||
def _switch_weread_account(self) -> bool:
|
||||
"""切换到下一个微信读书账号(round-robin)。
|
||||
|
||||
返回 ``True`` 表示成功切到了**不同**的账号;
|
||||
``False`` 表示只有一个账号或已切回原点。
|
||||
"""
|
||||
if len(self._weread_accounts) <= 1:
|
||||
return False
|
||||
prev_idx = self._current_account_idx
|
||||
self._current_account_idx = (self._current_account_idx + 1) % len(
|
||||
self._weread_accounts
|
||||
)
|
||||
acc = self._weread_accounts[self._current_account_idx]
|
||||
self._weread_cookies = acc.get("cookie", "")
|
||||
self._weread_ticket = acc.get("ticket", "")
|
||||
self._weread_vid = acc.get("vid", "")
|
||||
self._weread_name = acc.get("name", "")
|
||||
return True
|
||||
|
||||
def _weread_http_get(self, url: str, params: dict = None, include_ticket=False, timeout=(10, 30)):
|
||||
"""GET 请求微信读书 API,带多账号配额 failover。
|
||||
|
||||
检测到配额耗尽(HTTP 499 / errcode -2014 / -2041 / -2012)时,
|
||||
自动切换到 ``wx.lic`` 中下一个账号并重试。
|
||||
|
||||
返回值:``requests.Response`` 对象(像 ``requests.get`` 一样使用)。
|
||||
"""
|
||||
import requests
|
||||
from core.wx.model.weread_mp import WereadMPAPIError
|
||||
|
||||
tried_accounts = set()
|
||||
while True:
|
||||
headers = {
|
||||
"Cookie": self._weread_cookies,
|
||||
"User-Agent": self.user_agent,
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||||
"Origin": "https://weread.qq.com",
|
||||
"Referer": "https://weread.qq.com/",
|
||||
}
|
||||
if include_ticket and self._weread_ticket:
|
||||
headers["x-wr-ticket"] = self._weread_ticket
|
||||
|
||||
try:
|
||||
resp = requests.get(
|
||||
url, params=params, headers=headers,
|
||||
proxies=self._get_proxies(), timeout=timeout,
|
||||
)
|
||||
except requests.exceptions.Timeout:
|
||||
raise WereadMPAPIError("timeout", f"{url}") from None
|
||||
except requests.RequestException as exc:
|
||||
raise WereadMPAPIError("network_error", str(exc)) from exc
|
||||
|
||||
# 检查响应中的配额耗尽信号
|
||||
is_quota_exhausted = False
|
||||
if resp.status_code == 499:
|
||||
is_quota_exhausted = True
|
||||
elif resp.status_code == 200:
|
||||
try:
|
||||
payload = resp.json()
|
||||
code = payload.get("errcode", payload.get("errCode", 0))
|
||||
if code in (-2014, -2041, -2012):
|
||||
is_quota_exhausted = True
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
|
||||
if is_quota_exhausted:
|
||||
if self._current_account_idx not in tried_accounts:
|
||||
tried_accounts.add(self._current_account_idx)
|
||||
idx_before = self._current_account_idx
|
||||
if self._switch_weread_account() and self._current_account_idx not in tried_accounts:
|
||||
logger.info(
|
||||
f"配额耗尽(当前账号 {idx_before}),"
|
||||
f"切换到账号 {self._current_account_idx}({self._weread_name or '未命名'})重试"
|
||||
)
|
||||
continue
|
||||
# 所有账号都试过了
|
||||
raise WereadMPAPIError(
|
||||
499 if resp.status_code == 499 else (code if 'code' in locals() else -2041),
|
||||
f"所有账号配额均已耗尽({len(tried_accounts)} 个)",
|
||||
retriable=False,
|
||||
)
|
||||
|
||||
return resp
|
||||
|
||||
def _weread_get(self, url: str, params: dict = None) -> Optional[dict]:
|
||||
"""带微信读书 Cookie 的 GET 请求(web 域,浏览器完整请求头)"""
|
||||
import requests
|
||||
"""带微信读书 Cookie 的 GET 请求(web 域,浏览器完整请求头)
|
||||
|
||||
headers = {
|
||||
"Cookie": self._weread_cookies,
|
||||
"User-Agent": self.user_agent,
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||||
"Origin": "https://weread.qq.com",
|
||||
"Referer": "https://weread.qq.com/",
|
||||
}
|
||||
内部走 ``_weread_http_get``,带多账号配额 failover。
|
||||
"""
|
||||
try:
|
||||
resp = self._weread_http_get(url, params=params, timeout=15)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
try:
|
||||
proxies = self._get_proxies()
|
||||
resp = requests.get(
|
||||
url, params=params, headers=headers, proxies=proxies, timeout=15
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
payload = resp.json()
|
||||
# 微信读书以 HTTP 200 返回业务错误(登录态失效等),
|
||||
# 必须检查 errcode,否则「登录超时」会被当成空书架(0 本书)
|
||||
errcode = payload.get("errCode", payload.get("errcode", 0))
|
||||
if errcode in (-2012, -2010, -2041):
|
||||
print_warning(
|
||||
@@ -123,9 +227,6 @@ class MpsWeread(WxGather):
|
||||
else:
|
||||
print_warning(f"Weread API 返回 {resp.status_code}: {url}")
|
||||
return None
|
||||
except requests.exceptions.Timeout:
|
||||
print_warning(f"Weread API 超时: {url}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print_warning(f"Weread API 异常: {e}")
|
||||
return None
|
||||
|
||||
+15
-18
@@ -3,7 +3,6 @@
|
||||
import time
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from core.log import logger
|
||||
@@ -164,15 +163,17 @@ class MpsWereadMP(MpsWeread):
|
||||
def _get_mp_articles_page(self, book_id: str, offset=0):
|
||||
# /web/mp/articles 曾在部分旧 Cookie 上返回 -2041,当时据此回退到 cover 方案;
|
||||
# 实测该列表接口可用,是增量补抓的主路径。失败时由 get_Articles 回退 cover。
|
||||
# 内部走 _weread_http_get,带多账号配额 failover。
|
||||
try:
|
||||
response = requests.get(
|
||||
response = self._weread_http_get(
|
||||
f"{WEREAD_WEB_BASE}/web/mp/articles",
|
||||
params={"bookId": book_id, "offset": offset},
|
||||
headers=self._request_headers(include_ticket=True),
|
||||
proxies=self._get_proxies(),
|
||||
timeout=(10, 30),
|
||||
include_ticket=True,
|
||||
)
|
||||
except requests.RequestException as exc:
|
||||
except Exception as exc:
|
||||
from core.wx.model.weread_mp import WereadMPAPIError
|
||||
if isinstance(exc, WereadMPAPIError):
|
||||
raise
|
||||
raise WereadMPAPIError("network_error", str(exc)) from exc
|
||||
if response.status_code != 200:
|
||||
raise WereadMPAPIError(
|
||||
@@ -194,14 +195,13 @@ class MpsWereadMP(MpsWeread):
|
||||
返回: {"name", "title", "pic", "reviewId", ...};无文章或接口异常时抛错。
|
||||
"""
|
||||
try:
|
||||
response = requests.get(
|
||||
response = self._weread_http_get(
|
||||
f"{WEREAD_WEB_BASE}/api/mp/cover",
|
||||
params={"bookId": book_id},
|
||||
headers=self._request_headers(),
|
||||
proxies=self._get_proxies(),
|
||||
timeout=(10, 30),
|
||||
)
|
||||
except requests.RequestException as exc:
|
||||
except Exception as exc:
|
||||
if isinstance(exc, WereadMPAPIError):
|
||||
raise
|
||||
raise WereadMPAPIError("network_error", str(exc)) from exc
|
||||
if response.status_code != 200:
|
||||
raise WereadMPAPIError(
|
||||
@@ -248,17 +248,14 @@ class MpsWereadMP(MpsWeread):
|
||||
session.close()
|
||||
|
||||
def _get_mp_content(self, review_id: str):
|
||||
headers = self._request_headers()
|
||||
headers["Accept"] = "text/html,application/xhtml+xml,*/*"
|
||||
try:
|
||||
response = requests.get(
|
||||
response = self._weread_http_get(
|
||||
f"{WEREAD_WEB_BASE}/web/mp/content",
|
||||
params={"reviewId": review_id},
|
||||
headers=headers,
|
||||
proxies=self._get_proxies(),
|
||||
timeout=(10, 30),
|
||||
)
|
||||
except requests.RequestException as exc:
|
||||
except Exception as exc:
|
||||
if isinstance(exc, WereadMPAPIError):
|
||||
raise
|
||||
raise WereadMPAPIError("network_error", str(exc)) from exc
|
||||
if response.status_code != 200:
|
||||
raise WereadMPAPIError(
|
||||
|
||||
Reference in New Issue
Block a user