Files
Astral/upsert.py
T
Txiaochen 1636a79141 feat: WaHappy v0 —— 舞萌DX 纯网络游戏助手(占位版)
当前仅 upsert.py 可用:扫码登录 -> 上传一局成绩 -> 落库回查 -> 登出。

- api/ 下 40+ 个接口模块已实现,236 项零网络自检全绿
- 上传成绩链路已真机跑通,报文与真机抓包逐字节一致
- 开发文档见 docs 分支;source/ 与 logs/ 不入库
2026-10-06 14:42:06 +08:00

170 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
upsert.py —— 主脚本:扫码登录 → 上传成绩 → 登出
================================================================================
整条流程只有四步,而且**一行日志都不用自己写** ——
每个 api 函数被调用时会自动打印:
=== UserLogin 开始运行 ===
[接口说明] 用户正式登录(开启本次登录会话)。
[位置参数] 12345678, 'iUfgRmjr...'
[关键字参数] region_id=1, place_id=1000
=== UserLogin 运行结束 ===
日志模式改 config.py 一行:
LOG_LEVEL = "info" 只展示 api 脚本的运行情况和结果
LOG_LEVEL = "debug" 额外展示向 api 发送的数据与 api 返回的数据
日志文件(两种同时写):
logs/latest.log 只含**当次**主脚本运行(下次覆盖)
logs/YYYY-MM-DD.log 存放**当日**产生的所有日志(追加)
--------------------------------------------------------------------------------
用法
--------------------------------------------------------------------------------
python upsert.py <二维码> # 预检:登录 + 抓快照 + 组包,不发包
python upsert.py <二维码> --send # 真实上传成绩(★ 有副作用,不可撤销)
python upsert.py <二维码> --send --debug # 同上,日志里带完整收发报文
python upsert.py <二维码> --send --cooldown 0 # 跳过发包前的 60 秒冷却
python upsert.py <二维码> --send --no-verify # 跳过上传后的落库回查
--------------------------------------------------------------------------------
★ UpsertUserAll 一个函数就把整条结算链路跑完了
--------------------------------------------------------------------------------
1. 抓基线快照(没传 snapshot 时自动抓,用 token)
2. 从快照推本局 5 只角色(游玩前等级)
3. 构造本局成绩明细 userPlaylogList
4. 调前置接口 GetUserNewItemListApi
5. 组装 upsertUserAll 的 36 个节点(快照基线 + 本局演算 + 差分)
6. 合并 extra_data 里的外部 JSON
7. 发包前自检
8. 冷却 -> 真实发包
9. 落库回查(重新拉服务器数据,确认真的写进去了)
想额外塞几个节点(比如本局拿到的新 KaleidxScope 门)就传 extra_data:
UpsertUserAll(
user_id, login.loginId,
login_date_time=login.loginDateTime,
auth_date_time=login.dateTime,
token=token,
extra_data={
"userKaleidxScopeList": [{"gateId": 1, "isGateFound": True, ...}],
"isNewKaleidxScopeList": "1",
},
confirm=True,
)
extra_data 接受 dict / JSON 字符串 / JSON 文件路径;
键必须是 36 个节点之一,没给位串时会按数组长度自动补并给出提示。
"""
from __future__ import annotations
import sys
from api import (
setup_logging,
teardown_logging,
log_error,
ParseQRCode,
UserPreview,
UserLogin,
UserLogout,
LogoutType,
UpsertUserAll,
ConfirmRequiredError,
)
from config import PLACE_ID, REGION_ID
def parse_args(argv):
"""极简命令行解析:第一个非 -- 开头的参数是二维码。"""
opts = {"qr": "", "send": False, "debug": False, "no_verify": False, "cooldown": None}
i = 0
while i < len(argv):
a = argv[i]
if a == "--send":
opts["send"] = True
elif a == "--debug":
opts["debug"] = True
elif a == "--no-verify":
opts["no_verify"] = True
elif a == "--cooldown" and i + 1 < len(argv):
opts["cooldown"] = float(argv[i + 1])
i += 1
elif not a.startswith("--") and not opts["qr"]:
opts["qr"] = a
i += 1
return opts
def main() -> int:
opts = parse_args(sys.argv[1:])
if not opts["qr"]:
print(__doc__)
return 1
# --debug 只影响本次运行,不改 config.py
if opts["debug"]:
import config as _cfg
_cfg.LOG_LEVEL = "debug"
# 日志会话:开头一句。之后每次 api 调用都会自动记录,主脚本不用再写日志
setup_logging("upsert")
# 会话状态:保证异常时也能登出(否则账号会卡在 isLogin=True,下次登录 returnCode=100)
session = {"user_id": 0, "token": "", "login_dt": 0, "logged_in": False}
try:
# ---- 扫码:二维码 -> userId + token ----
user_id, token = ParseQRCode(opts["qr"])
session.update(user_id=user_id, token=token)
# ---- 预览:顺便挡住「账号已在游戏中」的情况,避免顶号 ----
preview = UserPreview(user_id, token)
if preview.isLogin:
log_error("账号处于已登录状态,请先在机台登出或等待会话超时")
return 2
# ---- 登录:拿到 playlogId 与 loginDateTime(结算上报要用)----
login = UserLogin(user_id, token, region_id=REGION_ID, place_id=PLACE_ID)
session.update(login_dt=login.loginDateTime, logged_in=True)
# ---- 结算:★ 一个函数搞定构造 + 发包 + 落库回查 ----
UpsertUserAll(
user_id, login.loginId,
login_date_time=login.loginDateTime,
auth_date_time=login.dateTime,
token=token,
dry_run=not opts["send"],
confirm=opts["send"],
cooldown_sec=opts["cooldown"],
verify_landing=not opts["no_verify"],
place_id=PLACE_ID, region_id=REGION_ID,
)
return 0
except ConfirmRequiredError:
return 5
except Exception as exc:
log_error(type(exc).__name__ + ": " + str(exc))
return 1
finally:
# 兜底登出:登录过且还没登出就释放会话,否则账号卡在 isLogin=True
if session["logged_in"]:
try:
UserLogout(session["user_id"], session["token"], session["login_dt"],
logout_type=LogoutType.TestIn,
region_id=REGION_ID, place_id=PLACE_ID)
except Exception as exc:
log_error("[收尾] 强制登出失败(会话可能需等待服务端超时):" + str(exc))
teardown_logging()
if __name__ == "__main__":
sys.exit(main())