feat(cmcc): 冷却自愈多轮重放 + 日志步骤中文化

- 换会话冷却自愈:单轮 15 败静默 600s 后自动重开(试数归零),最多 3 轮
  (覆盖 ~36 分钟,穿出冷却期;实测静默 ≥10min 恢复命中)
  号2 19:57 的 88B×15 就是素材冷却期,旧逻辑静默一次就放弃 → 现在自愈
- 日志前缀 [A][B][C][D] → [①换会话][②开门][③换票][④拿钥匙](用户可读)
This commit is contained in:
2026-11-07 19:59:41 +08:00
parent 55e2299efa
commit b85c4835dc

View File

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