From b85c4835dc8534f53461637758988e47e8f8eeda Mon Sep 17 00:00:00 2001 From: zhuhjay Date: Sat, 7 Nov 2026 19:59:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(cmcc):=20=E5=86=B7=E5=8D=B4=E8=87=AA?= =?UTF-8?q?=E6=84=88=E5=A4=9A=E8=BD=AE=E9=87=8D=E6=94=BE=20+=20=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E6=AD=A5=E9=AA=A4=E4=B8=AD=E6=96=87=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 换会话冷却自愈:单轮 15 败静默 600s 后自动重开(试数归零),最多 3 轮 (覆盖 ~36 分钟,穿出冷却期;实测静默 ≥10min 恢复命中) 号2 19:57 的 88B×15 就是素材冷却期,旧逻辑静默一次就放弃 → 现在自愈 - 日志前缀 [A][B][C][D] → [①换会话][②开门][③换票][④拿钥匙](用户可读) --- cmcc/scrip_cmcc_mark.py | 106 +++++++++++++++++++++++----------------- 1 file changed, 60 insertions(+), 46 deletions(-) diff --git a/cmcc/scrip_cmcc_mark.py b/cmcc/scrip_cmcc_mark.py index 0795224..8411dc8 100644 --- a/cmcc/scrip_cmcc_mark.py +++ b/cmcc/scrip_cmcc_mark.py @@ -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()