feat(cmcc): 冷却自愈多轮重放 + 日志步骤中文化
- 换会话冷却自愈:单轮 15 败静默 600s 后自动重开(试数归零),最多 3 轮 (覆盖 ~36 分钟,穿出冷却期;实测静默 ≥10min 恢复命中) 号2 19:57 的 88B×15 就是素材冷却期,旧逻辑静默一次就放弃 → 现在自愈 - 日志前缀 [A][B][C][D] → [①换会话][②开门][③换票][④拿钥匙](用户可读)
This commit is contained in:
@@ -122,11 +122,14 @@ SSO = "https://wx.10086.cn/qwhdsso"
|
|||||||
UTC8 = timezone(timedelta(hours=8)) # ★ 显式 UTC+8:青龙容器时区可能不是 +8
|
UTC8 = timezone(timedelta(hours=8)) # ★ 显式 UTC+8:青龙容器时区可能不是 +8
|
||||||
STRIP_HEADERS = ("content-length", "accept-encoding", "host", "connection")
|
STRIP_HEADERS = ("content-length", "accept-encoding", "host", "connection")
|
||||||
|
|
||||||
REPLAY_MAX = 30 # 命中即停:实测中位 1~9 发;30 发封顶,由错峰补签 cron 兜底
|
REPLAY_MAX = 30 # 单轮命中即停:实测中位 1~9 发
|
||||||
REPLAY_GAP = 5.0 # 3~5s 快节拍(慢节拍推向 236B 通道)
|
REPLAY_GAP = 5.0 # 3~5s 快节拍(慢节拍推向 236B 通道)
|
||||||
WALL_LIMIT = 6 * 60 # [A] 墙钟上限(含长静默),防止极端情况拖死任务
|
WALL_LIMIT = 6 * 60 # 单轮 [A] 墙钟上限(含长静默)
|
||||||
BACKOFF_AFTER = 15 # 连续 15 败 → 一次 10min 静默(防风暴冷却,只插一次)
|
BACKOFF_AFTER = 15 # 连续 15 败 → 静默(防风暴冷却)
|
||||||
BACKOFF_SECS = 600
|
BACKOFF_SECS = 600
|
||||||
|
COOLDOWN_ROUNDS = 3 # ★ 冷却自愈:整轮未命中时,静默后重开一轮,最多 3 轮
|
||||||
|
# (素材在冷却期时 88B×15 + 600s 静默 ≈ 12min/轮;
|
||||||
|
# 实测静默 ≥10 分钟恢复命中,3 轮覆盖 ~36 分钟,足够穿出冷却期)
|
||||||
THROTTLE = 1.2 # 业务请求间隔
|
THROTTLE = 1.2 # 业务请求间隔
|
||||||
|
|
||||||
MASK = lambda s: (s[:3] + "****" + s[-4:]) if s and len(s) >= 7 else (s or "?")
|
MASK = lambda s: (s[:3] + "****" + s[-4:]) if s and len(s) >= 7 else (s or "?")
|
||||||
@@ -302,41 +305,52 @@ def classify(resp):
|
|||||||
|
|
||||||
|
|
||||||
def replay_auto_login(seed, stats):
|
def replay_auto_login(seed, stats):
|
||||||
"""重放至 492 命中,命中即停。无状态 requests.post ——
|
"""重放至 492 命中,命中即停。无状态 post(Session 的 jar 会污染素材 Cookie)。
|
||||||
★ Session 的 prepare_cookies 会用 jar 无条件覆盖手工 Cookie 头,素材 cookie 一被污染就 64B。"""
|
★ 冷却自愈:单轮 15 败 → 静默 600s → 重开一轮(试数归零),最多 COOLDOWN_ROUNDS 轮。
|
||||||
start = time.time()
|
素材在冷却期(88B)时靠这个穿出去;3 轮全 88B 才认输(诊断文案会说「稍后重跑」。)"""
|
||||||
tried = 0
|
total_tried = 0
|
||||||
backed = False
|
for round_no in range(1, COOLDOWN_ROUNDS + 1):
|
||||||
while tried < REPLAY_MAX and time.time() - start < WALL_LIMIT:
|
start = time.time()
|
||||||
tried += 1
|
tried = 0
|
||||||
try:
|
backed = False
|
||||||
r = post_seed(seed["url"], seed["body"].encode("utf-8"),
|
while tried < REPLAY_MAX and time.time() - start < WALL_LIMIT:
|
||||||
seed["headers"], timeout=30)
|
tried += 1
|
||||||
except Exception as e:
|
total_tried += 1
|
||||||
stats["net_error"] = stats.get("net_error", 0) + 1
|
|
||||||
log("[A] #%d 网络错误 %r" % (tried, e))
|
|
||||||
time.sleep(REPLAY_GAP)
|
|
||||||
continue
|
|
||||||
form = classify(r)
|
|
||||||
stats[form] = stats.get(form, 0) + 1
|
|
||||||
if form == "492_hit":
|
|
||||||
try:
|
try:
|
||||||
raw_list = r.raw.headers.getlist("Set-Cookie")
|
r = post_seed(seed["url"], seed["body"].encode("utf-8"),
|
||||||
except Exception:
|
seed["headers"], timeout=30)
|
||||||
raw_list = [r.headers.get("set-cookie", "")]
|
except Exception as e:
|
||||||
blob = "; ".join(x.strip() for x in raw_list if x.strip())
|
stats["net_error"] = stats.get("net_error", 0) + 1
|
||||||
sess = blob.split("; Secure")[0].strip() # ★ 原始全值(含 Comment/Path 碎片)
|
log("[换会话] 第%d轮 #%d 网络错误 %r" % (round_no, tried, e))
|
||||||
log("[A] 第 %d 发命中(统计=%s)" % (tried, stats))
|
time.sleep(REPLAY_GAP)
|
||||||
return sess, tried
|
continue
|
||||||
if tried % 5 == 0 or form.startswith(("403", "150")):
|
form = classify(r)
|
||||||
log("[A] 第 %d 发 %s" % (tried, form))
|
stats[form] = stats.get(form, 0) + 1
|
||||||
if tried >= BACKOFF_AFTER and not backed:
|
if form == "492_hit":
|
||||||
log("[A] 连续 %d 败 → 静默 %ds 防风暴冷却" % (tried, BACKOFF_SECS))
|
try:
|
||||||
time.sleep(BACKOFF_SECS)
|
raw_list = r.raw.headers.getlist("Set-Cookie")
|
||||||
backed = True
|
except Exception:
|
||||||
else:
|
raw_list = [r.headers.get("set-cookie", "")]
|
||||||
time.sleep(REPLAY_GAP)
|
blob = "; ".join(x.strip() for x in raw_list if x.strip())
|
||||||
return None, tried
|
sess = blob.split("; Secure")[0].strip() # ★ 原始全值(含 Comment/Path 碎片)
|
||||||
|
log("[换会话] 第%d轮 第%d发命中(统计=%s)" % (round_no, tried, stats))
|
||||||
|
return sess, total_tried
|
||||||
|
if tried % 5 == 0 or form.startswith(("403", "150")):
|
||||||
|
log("[换会话] 第%d轮 第%d发 %s" % (round_no, tried, form))
|
||||||
|
if tried >= BACKOFF_AFTER and not backed:
|
||||||
|
backed = True
|
||||||
|
if round_no < COOLDOWN_ROUNDS:
|
||||||
|
log("[换会话] 连续 %d 败 → 疑似冷却期,静默 %ds 后自动重开一轮" % (tried, BACKOFF_SECS))
|
||||||
|
time.sleep(BACKOFF_SECS)
|
||||||
|
break # break 出 while → 下一轮(tried 归零)
|
||||||
|
else:
|
||||||
|
log("[换会话] 末轮连续 %d 败,不再静默" % tried)
|
||||||
|
else:
|
||||||
|
time.sleep(REPLAY_GAP)
|
||||||
|
# while 正常跑满 REPLAY_MAX 也进入下一轮(若还有轮次)
|
||||||
|
if round_no < COOLDOWN_ROUNDS:
|
||||||
|
log("[换会话] 第%d轮 %d 发未命中,重开新一轮" % (round_no, tried))
|
||||||
|
return None, total_tried
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- [B]~[D] 换票
|
# ---------------------------------------------------------------- [B]~[D] 换票
|
||||||
@@ -350,11 +364,11 @@ def mint_token(sess, stats):
|
|||||||
r = s.get(ACT_URL, timeout=30, allow_redirects=True)
|
r = s.get(ACT_URL, timeout=30, allow_redirects=True)
|
||||||
m = re.search(r"sid=([A-Za-z0-9]+)", r.text)
|
m = re.search(r"sid=([A-Za-z0-9]+)", r.text)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log("[B] 活动页请求异常 %r" % e)
|
log("[②开门] 活动页请求异常 %r" % e)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
continue
|
continue
|
||||||
if not m:
|
if not m:
|
||||||
log("[B] 没取到 sid")
|
log("[②开门] 没取到 sid")
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
continue
|
continue
|
||||||
sid = m.group(1)
|
sid = m.group(1)
|
||||||
@@ -365,22 +379,22 @@ def mint_token(sess, stats):
|
|||||||
"referer": r.url, "origin": "https://wx.10086.cn"})
|
"referer": r.url, "origin": "https://wx.10086.cn"})
|
||||||
j = r2.json()
|
j = r2.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log("[C] 异常 %r" % e)
|
log("[③换票] 异常 %r" % e)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
continue
|
continue
|
||||||
url = (j.get("data") or {}).get("url", "")
|
url = (j.get("data") or {}).get("url", "")
|
||||||
log("[C] %s %s" % (j.get("code"), j.get("msg", "")))
|
log("[③换票] %s %s" % (j.get("code"), j.get("msg", "")))
|
||||||
if "404" in url or not url: # ★ 判据是 url 含 404,code 仍是 SUCCESS
|
if "404" in url or not url: # ★ 判据是 url 含 404,code 仍是 SUCCESS
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
s.get(url, timeout=30, allow_redirects=True)
|
s.get(url, timeout=30, allow_redirects=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log("[D] 异常 %r" % e)
|
log("[④拿钥匙] 异常 %r" % e)
|
||||||
continue
|
continue
|
||||||
tok = next((c.value for c in s.cookies if c.name == "QWHD_SESSION_TOKEN"), None)
|
tok = next((c.value for c in s.cookies if c.name == "QWHD_SESSION_TOKEN"), None)
|
||||||
if tok:
|
if tok:
|
||||||
log("[D] token=%s…%s" % (tok[:6], tok[-4:]))
|
log("[④拿钥匙] token=%s…%s" % (tok[:6], tok[-4:]))
|
||||||
return tok
|
return tok
|
||||||
stats["mint_fail"] = stats.get("mint_fail", 0) + 1
|
stats["mint_fail"] = stats.get("mint_fail", 0) + 1
|
||||||
return None
|
return None
|
||||||
@@ -486,13 +500,13 @@ def run_one_account(name, payload, stats):
|
|||||||
else:
|
else:
|
||||||
sess, tried = replay_auto_login(payload, stats)
|
sess, tried = replay_auto_login(payload, stats)
|
||||||
if not sess:
|
if not sess:
|
||||||
acct["detail"] = ("autoLogin %d 发未命中(%s)。诊断:64B多→素材/会话死,重抓;"
|
acct["detail"] = ("换会话 %d 发未命中(%s)。诊断:64B多→素材/会话死,重抓;"
|
||||||
"88B多→素材活着未中,1小时后重跑;236B→通道异常,快节拍重试;"
|
"88B多→素材活着未中,1小时后重跑;236B→通道异常,快节拍重试;"
|
||||||
"403/150→cURL 解析损坏" % (tried, stats))
|
"403/150→cURL 解析损坏" % (tried, stats))
|
||||||
return 2, acct
|
return 2, acct
|
||||||
tok = mint_token(sess, stats)
|
tok = mint_token(sess, stats)
|
||||||
if not tok:
|
if not tok:
|
||||||
acct["detail"] = "换票失败([C] 3 次重建 sid 未成)"
|
acct["detail"] = "换票失败(③换票 3 次重建 sid 未成)"
|
||||||
return 3, acct
|
return 3, acct
|
||||||
|
|
||||||
m = Mark(tok)
|
m = Mark(tok)
|
||||||
@@ -516,7 +530,7 @@ def run_one_account(name, payload, stats):
|
|||||||
if not st["signed"]:
|
if not st["signed"]:
|
||||||
mk = m.mark()
|
mk = m.mark()
|
||||||
if mk.get("_token_invalid"):
|
if mk.get("_token_invalid"):
|
||||||
acct["detail"] = "domark 时 token 失效"
|
acct["detail"] = "④拿钥匙后签到时 token 失效"
|
||||||
return 3, acct
|
return 3, acct
|
||||||
log("[{}] 签到 code={} status={} msg={}".format(name, mk.get("code"), mk.get("status"), mk.get("msg")))
|
log("[{}] 签到 code={} status={} msg={}".format(name, mk.get("code"), mk.get("status"), mk.get("msg")))
|
||||||
st2 = m.state()
|
st2 = m.state()
|
||||||
|
|||||||
Reference in New Issue
Block a user