style: 统一 Ruff 代码格式
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
python3 main.py sample-session # 用迁移的 live 捕获生成示例会话态
|
||||
python3 main.py sample-order # 用迁移的 order7 生成示例订单
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
@@ -50,14 +51,31 @@ from pyvm.protocol import ( # noqa: E402
|
||||
REPLAY = ROOT / "replay"
|
||||
DEFAULT_APPID = PAY_APPID
|
||||
DEFAULT_SAVE_URL = f"https://api.unipay.qq.com/v1/r/{DEFAULT_APPID}/web_save"
|
||||
ORDER_FIELDS = ["token_id", "openid", "openkey", "session_id", "session_type", "zoneid",
|
||||
"pay_method", "buy_quantity", "mb_pwd", "pay_id", "auth_key",
|
||||
"card_value", "accounttype", "provide_uin", "extend", "ts",
|
||||
"from_h5", "webversion"]
|
||||
ORDER_FIELDS = [
|
||||
"token_id",
|
||||
"openid",
|
||||
"openkey",
|
||||
"session_id",
|
||||
"session_type",
|
||||
"zoneid",
|
||||
"pay_method",
|
||||
"buy_quantity",
|
||||
"mb_pwd",
|
||||
"pay_id",
|
||||
"auth_key",
|
||||
"card_value",
|
||||
"accounttype",
|
||||
"provide_uin",
|
||||
"extend",
|
||||
"ts",
|
||||
"from_h5",
|
||||
"webversion",
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 工具
|
||||
|
||||
|
||||
def _write_private_text(path: Path, content: str) -> None:
|
||||
"""写入任务协议证据并限制为当前用户可读。"""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
@@ -65,6 +83,7 @@ def _write_private_text(path: Path, content: str) -> None:
|
||||
path.write_text(content, encoding="utf-8")
|
||||
os.chmod(path, 0o600)
|
||||
|
||||
|
||||
def _load_cap(path: Path) -> dict:
|
||||
"""加载 deepCap 捕获 JSON,兼容三种存档格式,返回顶层 dict(含 C 键)。
|
||||
|
||||
@@ -113,32 +132,47 @@ def parse_plaintext(path: str | Path) -> dict:
|
||||
|
||||
# ---------------------------------------------------------------- 命令
|
||||
|
||||
|
||||
def cmd_verify(args=None) -> int:
|
||||
"""校验当前受版本控制的 goods 协议材料,不依赖已删除的历史抓包。"""
|
||||
args_path = REPLAY / "live" / "default" / "args-template.json"
|
||||
args_template = json.loads(args_path.read_text(encoding="utf-8"))
|
||||
validate_goods_materials(args_template)
|
||||
required = ("bytecode.json", "constants.json", "xmidasops.json", "e2e/multi-1.json",
|
||||
"e2e/ws-args.json", "deepcaps2/cap-69667.json")
|
||||
required = (
|
||||
"bytecode.json",
|
||||
"constants.json",
|
||||
"xmidasops.json",
|
||||
"e2e/multi-1.json",
|
||||
"e2e/ws-args.json",
|
||||
"deepcaps2/cap-69667.json",
|
||||
)
|
||||
missing = [name for name in required if not (REPLAY / name).exists()]
|
||||
if missing:
|
||||
print(f"❌ goods 协议材料缺失: {', '.join(missing)}")
|
||||
return 1
|
||||
print("✅ goods 协议材料校验通过(18 槽模板、Te/S-box、VM 文件均完整)")
|
||||
print(" 历史逐字节黄金向量未随仓库保留;如需恢复,可放入 replay/golden 后由 CI 自动执行。")
|
||||
print(
|
||||
" 历史逐字节黄金向量未随仓库保留;如需恢复,可放入 replay/golden 后由 CI 自动执行。"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def _gen_with_session(st: SessionState, order: dict) -> str:
|
||||
"""用会话态 + 订单参数生成 encrypt_msg(args_template 会话绑定,必须用捕获值)。"""
|
||||
from pyvm.algorithm import decode_d
|
||||
|
||||
params = {k: order.get(k, "") for k in ORDER_FIELDS}
|
||||
args_tpl = decode_d(st.args_template_d)
|
||||
return generate_encrypt_msg_offline(
|
||||
params, order.get("fk_extend", ""), order.get("ts", ""), order.get("_rand", ""),
|
||||
xmidas=st.xmidas_ops, xmidas_token=st.xmidas_token,
|
||||
params,
|
||||
order.get("fk_extend", ""),
|
||||
order.get("ts", ""),
|
||||
order.get("_rand", ""),
|
||||
xmidas=st.xmidas_ops,
|
||||
xmidas_token=st.xmidas_token,
|
||||
args_template=args_tpl,
|
||||
key16=st.key16, key1=st.key1,
|
||||
key16=st.key16,
|
||||
key1=st.key1,
|
||||
)
|
||||
|
||||
|
||||
@@ -186,7 +220,8 @@ def cmd_submit(args) -> int:
|
||||
url = args.url or DEFAULT_SAVE_URL
|
||||
cookie_str = "; ".join(f"{k}={v}" for k, v in st.cookies.items())
|
||||
req = urllib.request.Request(
|
||||
url, data=body.encode("utf-8"),
|
||||
url,
|
||||
data=body.encode("utf-8"),
|
||||
headers={
|
||||
"User-Agent": GOODS_USER_AGENT,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
@@ -221,6 +256,7 @@ def cmd_submit(args) -> int:
|
||||
def cmd_sample_session(args) -> int:
|
||||
"""用迁移的 live 捕获生成示例会话态(key16/key1/xmidas_ops)。"""
|
||||
from pyvm.algorithm import decode_d
|
||||
|
||||
web_args = decode_d(_load_cap(REPLAY / "live/caps7/cap-85091.json")["C"][2])
|
||||
cap7 = _load_cap(REPLAY / "live/caps7/cap-85091.json")
|
||||
st = SessionState(
|
||||
@@ -228,7 +264,9 @@ def cmd_sample_session(args) -> int:
|
||||
key16=list(web_args[6][0]),
|
||||
key1=list(web_args[0][0]),
|
||||
xmidas_token="DE46DBA4754D42A6B66ADD4319FF80C144D9ED22ED73384C271793D9A9AA2FFBD6C104BE8D4A7F4ED2A8688FCB6F7540",
|
||||
args_template_d=cap7["C"][2] if isinstance(cap7["C"][2], str) else json.dumps(cap7["C"][2], ensure_ascii=False),
|
||||
args_template_d=cap7["C"][2]
|
||||
if isinstance(cap7["C"][2], str)
|
||||
else json.dumps(cap7["C"][2], ensure_ascii=False),
|
||||
cookies={},
|
||||
source="live/order7 (归档示例,cookies 为空)",
|
||||
)
|
||||
@@ -252,6 +290,7 @@ def cmd_sample_order(args) -> int:
|
||||
|
||||
# ---------------------------------------------------------------- mall 命令
|
||||
|
||||
|
||||
def cmd_mall_verify(args=None) -> int:
|
||||
"""校验当前受版本控制的 mall VM 和固定槽模板。"""
|
||||
fixed_path = REPLAY / "mall" / "transform-fixed.json"
|
||||
@@ -282,7 +321,9 @@ def cmd_mall_gen(args) -> int:
|
||||
|
||||
def cmd_mall_sample(args) -> int:
|
||||
"""用归档同会话黄金对生成示例 mall 会话态(config/mall-session.json)。"""
|
||||
g = json.loads((ROOT / "config/golden/golden-final.json").read_text(encoding="utf-8"))
|
||||
g = json.loads(
|
||||
(ROOT / "config/golden/golden-final.json").read_text(encoding="utf-8")
|
||||
)
|
||||
session = MallSession(g["transform_input"], g["xmidas_ops"])
|
||||
out = Path(args.output) if args.output else ROOT / "config" / "mall-session.json"
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
@@ -325,7 +366,8 @@ def cmd_mall_submit(args) -> int:
|
||||
if "midas_openkey" not in cookies and "accesstoken" in cookies:
|
||||
cookie_str += "; midas_openkey=" + cookies["accesstoken"]
|
||||
req = urllib.request.Request(
|
||||
url, data=new_body.encode("utf-8"),
|
||||
url,
|
||||
data=new_body.encode("utf-8"),
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
@@ -346,7 +388,11 @@ def cmd_mall_submit(args) -> int:
|
||||
print(f"[submit] 网络错误: {e!r}")
|
||||
return 3
|
||||
print(f"[submit] 响应: {raw[:800]}")
|
||||
out = Path(args.output) if args.output else ROOT / "config" / "mall-order-response.json"
|
||||
out = (
|
||||
Path(args.output)
|
||||
if args.output
|
||||
else ROOT / "config" / "mall-order-response.json"
|
||||
)
|
||||
js = None
|
||||
ret = None
|
||||
try:
|
||||
@@ -370,7 +416,9 @@ def cmd_mall_submit(args) -> int:
|
||||
print(f" 失败响应 → {fail_out}(保留上次成功响应)")
|
||||
if ret in ("1018", 1018):
|
||||
print(" 原因: mall 登录态失效——请重新在浏览器登录并采集 mall 会话态")
|
||||
print(" (node scripts/capture-mall-data.mjs,或手动刷新 mall-session.json 的 cookies)")
|
||||
print(
|
||||
" (node scripts/capture-mall-data.mjs,或手动刷新 mall-session.json 的 cookies)"
|
||||
)
|
||||
return 1
|
||||
|
||||
|
||||
@@ -383,11 +431,15 @@ def cmd_mall_capture(args) -> int:
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="YYB 加密参数生成框架(goods web_save + mall PlaceOrder)")
|
||||
ap = argparse.ArgumentParser(
|
||||
description="YYB 加密参数生成框架(goods web_save + mall PlaceOrder)"
|
||||
)
|
||||
sub = ap.add_subparsers(dest="module", required=True)
|
||||
|
||||
# ---- goods 组 ----
|
||||
pg = sub.add_parser("goods", help="goods 侧(web_save / web_new_encrypt,CHAOS VM 116 opcode)")
|
||||
pg = sub.add_parser(
|
||||
"goods", help="goods 侧(web_save / web_new_encrypt,CHAOS VM 116 opcode)"
|
||||
)
|
||||
gsub = pg.add_subparsers(dest="cmd", required=True)
|
||||
gsub.add_parser("verify", help="校验当前 goods 协议材料")
|
||||
p = gsub.add_parser("gen", help="仅生成 encrypt_msg(不联网)")
|
||||
@@ -416,30 +468,64 @@ def main() -> int:
|
||||
p.add_argument("--frames", default=None, help="最新捕获的 frames.jsonl(优先)")
|
||||
p.add_argument("--output", default=None)
|
||||
p = msub.add_parser("submit", help="生成 encrypt_msg + 提交 PlaceOrder(纯 Python)")
|
||||
p.add_argument("--session", required=True, help="mall-session.json(含 transform_input/xmidas/cookies)")
|
||||
p.add_argument("--order-template", default=str(ROOT / "config" / "mall-order-template.json"))
|
||||
p.add_argument(
|
||||
"--session",
|
||||
required=True,
|
||||
help="mall-session.json(含 transform_input/xmidas/cookies)",
|
||||
)
|
||||
p.add_argument(
|
||||
"--order-template", default=str(ROOT / "config" / "mall-order-template.json")
|
||||
)
|
||||
p.add_argument("--output", default=None)
|
||||
p = msub.add_parser("auto", help="仅凭 CK 全自动下单(纯HTTP GetPayToken + 纯Python encrypt_msg,无浏览器采集)")
|
||||
p = msub.add_parser(
|
||||
"auto",
|
||||
help="仅凭 CK 全自动下单(纯HTTP GetPayToken + 纯Python encrypt_msg,无浏览器采集)",
|
||||
)
|
||||
p.add_argument("--session", default=str(ROOT / "config" / "mall-session.json"))
|
||||
p.add_argument("--order-template", default=str(ROOT / "config" / "mall-order-template.json"))
|
||||
p.add_argument("--product-id", default=None, help="和平精英点券商品 ID;默认使用模板当前商品")
|
||||
p.add_argument(
|
||||
"--order-template", default=str(ROOT / "config" / "mall-order-template.json")
|
||||
)
|
||||
p.add_argument(
|
||||
"--product-id", default=None, help="和平精英点券商品 ID;默认使用模板当前商品"
|
||||
)
|
||||
p.add_argument("--offer-id", default=None, help="当前商品服务端 offer ID")
|
||||
p.add_argument("--quantity", type=int, default=None, help="当前点券商品购买份数(正整数)")
|
||||
p.add_argument(
|
||||
"--quantity", type=int, default=None, help="当前点券商品购买份数(正整数)"
|
||||
)
|
||||
p.add_argument("--role-id", default=None, help="游戏角色 ID;默认使用模板当前角色")
|
||||
p.add_argument("--role-name", default=None, help="游戏角色名称;默认使用模板当前角色")
|
||||
p.add_argument(
|
||||
"--role-name", default=None, help="游戏角色名称;默认使用模板当前角色"
|
||||
)
|
||||
p.add_argument("--zone-id", default=None, help="游戏区服 ID;默认使用模板当前区服")
|
||||
p.add_argument("--zone-name", default=None, help="游戏区服名称")
|
||||
p.add_argument("--area", default=None, help="游戏大区 ID(QQ 平台与 zoneid 不同,来自角色查询的 partition_info)")
|
||||
p.add_argument(
|
||||
"--area",
|
||||
default=None,
|
||||
help="游戏大区 ID(QQ 平台与 zoneid 不同,来自角色查询的 partition_info)",
|
||||
)
|
||||
p.add_argument("--partition", default=None, help="游戏分区 ID(QQ 平台可为空)")
|
||||
p.add_argument("--pf", default=None, help="支付平台标识;由角色选择器按 Android/iOS 写入")
|
||||
p.add_argument(
|
||||
"--pf", default=None, help="支付平台标识;由角色选择器按 Android/iOS 写入"
|
||||
)
|
||||
p.add_argument("--output", default=None)
|
||||
p = msub.add_parser("pay", help="mall 下单 -> goods web_save -> 微信支付二维码(纯 Python)")
|
||||
p.add_argument("--mall-response", default=str(ROOT / "config" / "mall-order-response.json"),
|
||||
help="PlaceOrder 响应(含 url_params/token)")
|
||||
p.add_argument("--goods-dir", default=str(ROOT / "replay" / "live" / "order10"),
|
||||
help="goods 会话态目录(plaintext/body/xmidasops/keys/cap/web-token)")
|
||||
p.add_argument("--session", default=str(ROOT / "config" / "mall-session.json"),
|
||||
help="mall 登录态(含 cookies)")
|
||||
p = msub.add_parser(
|
||||
"pay", help="mall 下单 -> goods web_save -> 微信支付二维码(纯 Python)"
|
||||
)
|
||||
p.add_argument(
|
||||
"--mall-response",
|
||||
default=str(ROOT / "config" / "mall-order-response.json"),
|
||||
help="PlaceOrder 响应(含 url_params/token)",
|
||||
)
|
||||
p.add_argument(
|
||||
"--goods-dir",
|
||||
default=str(ROOT / "replay" / "live" / "order10"),
|
||||
help="goods 会话态目录(plaintext/body/xmidasops/keys/cap/web-token)",
|
||||
)
|
||||
p.add_argument(
|
||||
"--session",
|
||||
default=str(ROOT / "config" / "mall-session.json"),
|
||||
help="mall 登录态(含 cookies)",
|
||||
)
|
||||
p.add_argument("--appid", default=DEFAULT_APPID)
|
||||
p.add_argument("--output", default=None, help="二维码 PNG 输出路径")
|
||||
msub.add_parser("capture", help="用浏览器捕获 mall 会话态(Node 脚本)")
|
||||
@@ -447,20 +533,29 @@ def main() -> int:
|
||||
args = ap.parse_args()
|
||||
try:
|
||||
if args.module == "goods":
|
||||
return {"verify": cmd_verify, "gen": cmd_gen, "submit": cmd_submit,
|
||||
"sample-session": cmd_sample_session, "sample-order": cmd_sample_order}[args.cmd](args)
|
||||
return {
|
||||
"verify": cmd_verify,
|
||||
"gen": cmd_gen,
|
||||
"submit": cmd_submit,
|
||||
"sample-session": cmd_sample_session,
|
||||
"sample-order": cmd_sample_order,
|
||||
}[args.cmd](args)
|
||||
elif args.module == "mall":
|
||||
return {"verify": cmd_mall_verify, "gen": cmd_mall_gen, "submit": cmd_mall_submit,
|
||||
"sample-session": cmd_mall_sample, "pay": cmd_mall_pay, "auto": cmd_mall_auto,
|
||||
"capture": cmd_mall_capture}[args.cmd](args)
|
||||
return {
|
||||
"verify": cmd_mall_verify,
|
||||
"gen": cmd_mall_gen,
|
||||
"submit": cmd_mall_submit,
|
||||
"sample-session": cmd_mall_sample,
|
||||
"pay": cmd_mall_pay,
|
||||
"auto": cmd_mall_auto,
|
||||
"capture": cmd_mall_capture,
|
||||
}[args.cmd](args)
|
||||
return 2
|
||||
except (FileNotFoundError, ValueError) as e:
|
||||
print(f"错误: {e}")
|
||||
return 2
|
||||
|
||||
|
||||
|
||||
|
||||
def build_mall_transform(fixed: dict) -> list:
|
||||
"""仅CK全自动:固定槽模板 + 随机 key16/槽6/明文缓冲 构造 transform_input。
|
||||
|
||||
@@ -487,6 +582,7 @@ def mall_getpaytoken(cookies: dict) -> tuple[list, str]:
|
||||
证据: evidence/getpaytoken-pure-http.json
|
||||
"""
|
||||
import urllib.request
|
||||
|
||||
login = midas_login_params(cookies)
|
||||
login["offer_id"] = "800001492"
|
||||
body = {
|
||||
@@ -494,8 +590,12 @@ def mall_getpaytoken(cookies: dict) -> tuple[list, str]:
|
||||
"call_param": {
|
||||
"call_func": "GetPayToken",
|
||||
"call_param_json": json.dumps(
|
||||
{"version": "pagedoo-v2.0.0", "app_id": "202406061128117473047424",
|
||||
"content_id": "ct1755160919_GEOCGTMN"}),
|
||||
{
|
||||
"version": "pagedoo-v2.0.0",
|
||||
"app_id": "202406061128117473047424",
|
||||
"content_id": "ct1755160919_GEOCGTMN",
|
||||
}
|
||||
),
|
||||
"call_type": "security_service",
|
||||
"login_check_param_json": json.dumps(login),
|
||||
},
|
||||
@@ -505,12 +605,19 @@ def mall_getpaytoken(cookies: dict) -> tuple[list, str]:
|
||||
for k, v in [("midas_openid", "openid"), ("midas_openkey", "accesstoken")]:
|
||||
if k not in cookies and v in cookies:
|
||||
cookie_str += f"; {k}=" + cookies[v]
|
||||
req = urllib.request.Request(url, data=json.dumps(body).encode("utf-8"), headers={
|
||||
"Content-Type": "application/json", "Accept": "application/json, text/plain, */*",
|
||||
"Origin": "https://z.iwan.yyb.qq.com", "Referer": "https://z.iwan.yyb.qq.com/",
|
||||
"Cookie": cookie_str,
|
||||
"User-Agent": MALL_USER_AGENT,
|
||||
}, method="POST")
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=json.dumps(body).encode("utf-8"),
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Origin": "https://z.iwan.yyb.qq.com",
|
||||
"Referer": "https://z.iwan.yyb.qq.com/",
|
||||
"Cookie": cookie_str,
|
||||
"User-Agent": MALL_USER_AGENT,
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
raw = r.read().decode("utf-8", "replace")
|
||||
@@ -549,7 +656,11 @@ def _apply_card_selection(payload: dict, args) -> None:
|
||||
if quantity is not None and quantity <= 0:
|
||||
raise ValueError("--quantity 必须是正整数")
|
||||
products = payload.get("product_list")
|
||||
if not isinstance(products, list) or not products or not isinstance(products[0], dict):
|
||||
if (
|
||||
not isinstance(products, list)
|
||||
or not products
|
||||
or not isinstance(products[0], dict)
|
||||
):
|
||||
raise ValueError("订单模板缺少 product_list[0]")
|
||||
product = products[0]
|
||||
product_id = getattr(args, "product_id", None)
|
||||
@@ -617,7 +728,9 @@ def cmd_mall_auto(args) -> int:
|
||||
return 1
|
||||
print(f"[auto] arrays {len(arrays)} | pay_token {pay_token[:16]}...")
|
||||
print("[auto] ② 构造 transform_input(固定槽 + 随机 key/明文缓冲)...")
|
||||
fixed = json.loads((ROOT / "replay/mall/transform-fixed.json").read_text(encoding="utf-8"))
|
||||
fixed = json.loads(
|
||||
(ROOT / "replay/mall/transform-fixed.json").read_text(encoding="utf-8")
|
||||
)
|
||||
validate_mall_materials(fixed)
|
||||
ti = build_mall_transform(fixed)
|
||||
print("[auto] ③ 纯 Python 生成 encrypt_msg...")
|
||||
@@ -639,18 +752,29 @@ def cmd_mall_auto(args) -> int:
|
||||
if k not in cookies and v in cookies:
|
||||
cookie_str += f"; {k}=" + cookies[v]
|
||||
url = MALL_API_URL + "?t=" + str(int(time.time() * 1000))
|
||||
req = urllib.request.Request(url, data=new_body.encode("utf-8"), headers={
|
||||
"Content-Type": "application/json", "Accept": "application/json, text/plain, */*",
|
||||
"Origin": "https://z.iwan.yyb.qq.com", "Referer": "https://z.iwan.yyb.qq.com/",
|
||||
"Cookie": cookie_str,
|
||||
"User-Agent": MALL_USER_AGENT,
|
||||
}, method="POST")
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=new_body.encode("utf-8"),
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Origin": "https://z.iwan.yyb.qq.com",
|
||||
"Referer": "https://z.iwan.yyb.qq.com/",
|
||||
"Cookie": cookie_str,
|
||||
"User-Agent": MALL_USER_AGENT,
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
raw = r.read().decode("utf-8", "replace")
|
||||
except urllib.error.HTTPError as e:
|
||||
raw = e.read().decode("utf-8", "replace")
|
||||
out = Path(args.output) if args.output else ROOT / "config" / "mall-order-response.json"
|
||||
out = (
|
||||
Path(args.output)
|
||||
if args.output
|
||||
else ROOT / "config" / "mall-order-response.json"
|
||||
)
|
||||
_write_private_text(out, raw)
|
||||
js = json.loads(raw)
|
||||
ret = js.get("result_code")
|
||||
@@ -669,7 +793,9 @@ def cmd_mall_auto(args) -> int:
|
||||
return 0
|
||||
detail = js.get("result_info", "") or call_reply.get("result_info", "") or ""
|
||||
print(f"❌ 下单失败 ret={ret} inner={inner_ret} ({detail})")
|
||||
print(f" {describe_payment_failure(str(cookies.get('logintype', '')), 'order', f'{ret} {inner_ret} {detail}')}")
|
||||
print(
|
||||
f" {describe_payment_failure(str(cookies.get('logintype', '')), 'order', f'{ret} {inner_ret} {detail}')}"
|
||||
)
|
||||
print(f" 响应已保存 → {out}")
|
||||
return 1
|
||||
|
||||
@@ -714,10 +840,13 @@ def cmd_mall_pay(args) -> int:
|
||||
cr = json.loads(resp["data"]["call_reply"])
|
||||
up = cr["data"]["url_params"]
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
q = parse_qs(urlparse(up).query)
|
||||
mall_tid = q.get("token_id", [""])[0]
|
||||
if mall_tid and mall_tid != token_id:
|
||||
print(f"⚠️ mall 响应订单({mall_tid[:16]}...)与捕获会话订单({token_id[:16]}...)不一致")
|
||||
print(
|
||||
f"⚠️ mall 响应订单({mall_tid[:16]}...)与捕获会话订单({token_id[:16]}...)不一致"
|
||||
)
|
||||
print(" goods web_token 绑定捕获页面订单,以捕获 body 订单为准继续")
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f"⚠️ mall 响应读取失败(忽略): {e!r}")
|
||||
@@ -727,6 +856,7 @@ def cmd_mall_pay(args) -> int:
|
||||
keys = json.loads((gd / "keys.json").read_text(encoding="utf-8"))
|
||||
xmidas = json.loads((gd / "xmidasops.json").read_text(encoding="utf-8"))
|
||||
from pyvm.algorithm import decode_d, recover_plaintext_from_buffer
|
||||
|
||||
at_file = gd / "args-template.json"
|
||||
if at_file.exists():
|
||||
# 新版采集:decoded 18参直接读取(capture-goods-session.mjs J 转储)
|
||||
@@ -740,8 +870,10 @@ def cmd_mall_pay(args) -> int:
|
||||
if not web_token:
|
||||
m = re.search(r"web_token=([0-9A-F]+)", body_tpl)
|
||||
web_token = m.group(1) if m else ""
|
||||
print(f"[pay] goods 会话态: xmidasops={len(xmidas)} key16={'有' if keys.get('key16') else '无'} "
|
||||
f"web_token={web_token[:12]}...")
|
||||
print(
|
||||
f"[pay] goods 会话态: xmidasops={len(xmidas)} key16={'有' if keys.get('key16') else '无'} "
|
||||
f"web_token={web_token[:12]}..."
|
||||
)
|
||||
|
||||
# 3. 恢复页面真实明文(ts/fk_extend/_rand),刷新 ts 为当前一致值
|
||||
try:
|
||||
@@ -761,22 +893,37 @@ def cmd_mall_pay(args) -> int:
|
||||
now_ms = int(time.time() * 1000)
|
||||
params["ts"] = str(now_ms // 1000)
|
||||
if rec_ts:
|
||||
print(f"[pay] 页面真实明文: ts={rec_ts}(捕获) -> 使用当前 ts={params['ts']} "
|
||||
f"(E3:ts 与 body t 一致即可,不必等于页面值)")
|
||||
print(
|
||||
f"[pay] 页面真实明文: ts={rec_ts}(捕获) -> 使用当前 ts={params['ts']} "
|
||||
f"(E3:ts 与 body t 一致即可,不必等于页面值)"
|
||||
)
|
||||
print("[pay] 生成 goods encrypt_msg(纯 Python)...")
|
||||
hex_msg = generate_encrypt_msg_offline(
|
||||
params, fk_extend, params["ts"], rand_val,
|
||||
xmidas=xmidas, args_template=web_args,
|
||||
key16=keys.get("key16"), key1=keys.get("key1"),
|
||||
params,
|
||||
fk_extend,
|
||||
params["ts"],
|
||||
rand_val,
|
||||
xmidas=xmidas,
|
||||
args_template=web_args,
|
||||
key16=keys.get("key16"),
|
||||
key1=keys.get("key1"),
|
||||
)
|
||||
print(f"[pay] encrypt_msg ({len(hex_msg)} hex): {hex_msg[:32]}...")
|
||||
|
||||
# 4. 构造 web_save body(会话订单 token + 当前动态值 + 纯 py encrypt_msg)
|
||||
body = body_tpl
|
||||
for k, v in [("token_id", token_id), ("transaction_id", transaction_id),
|
||||
("out_trade_no", out_trade_no), ("offer_type", offer_type)]:
|
||||
for k, v in [
|
||||
("token_id", token_id),
|
||||
("transaction_id", transaction_id),
|
||||
("out_trade_no", out_trade_no),
|
||||
("offer_type", offer_type),
|
||||
]:
|
||||
body = re.sub(rf"{k}=[^&]*", f"{k}=" + v, body, count=1)
|
||||
body = re.sub(r"pc_st=[^&]+", "pc_st=" + str(uuid.uuid4()).upper() + str(int(time.time() * 1000)), body)
|
||||
body = re.sub(
|
||||
r"pc_st=[^&]+",
|
||||
"pc_st=" + str(uuid.uuid4()).upper() + str(int(time.time() * 1000)),
|
||||
body,
|
||||
)
|
||||
body = re.sub(r"r=[0-9.]+", "r=" + str(secrets.SystemRandom().random()), body)
|
||||
body = re.sub(r"&t=[0-9]+", "&t=" + str(int(time.time() * 1000)), body)
|
||||
body = re.sub(r"encrypt_msg=[0-9a-f]+", "encrypt_msg=" + hex_msg, body)
|
||||
@@ -798,7 +945,8 @@ def cmd_mall_pay(args) -> int:
|
||||
# 6. POST web_save
|
||||
url = f"https://api.unipay.qq.com/v1/r/{args.appid}/web_save"
|
||||
req = urllib.request.Request(
|
||||
url, data=body.encode("utf-8"),
|
||||
url,
|
||||
data=body.encode("utf-8"),
|
||||
headers={
|
||||
"User-Agent": MALL_USER_AGENT,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
@@ -855,5 +1003,6 @@ def cmd_mall_pay(args) -> int:
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
Reference in New Issue
Block a user