Files
Astral/docs/账号转生说明.md
T
Txiaochen 3e30dffb31 v0版本完成:新增 4 篇详细文档(README 精简内容的落点)
README 为了防冗余做了精简,被移出的详细内容写成文档放这里:

- 功能票使用说明.md      买票/用票的完整原理、报文依据、playCredit 公式、实测记录
- 账号转生说明.md        reincarnate 完整流程、分批规则(避开 2 首)、
                          续关登录依据、登出 type=1 依据、playlog 合成、迁移范围
- 常见坑与注意事项.md    二维码 / 账号状态 / 功能票 / 副作用 / 日志 / 网络 /
                          不入库清单 + .gitignore 行尾注释的坑
- netdelivery说明.md     DLI 协议、amdaemon.exe 字符串出处、用法与实测结论
2026-10-07 11:23:47 +08:00

7.0 KiB
Raw Blame History

账号转生说明(reincarnate.py)

本文是 README.md 里转生部分的详细版。


一、它干了什么

把一个账号(源 A)的数据克隆到另一个账号(目标 B):

源账号 A:二维码 -> ParseQRCode -> UserLogin -> GetNeedUserData
          或:直接读 data/<A>.json(离线复用)
        ↓  节点数据(19 个节点)
目标账号 B:二维码 -> ParseQRCode -> UserPreview -> UserLogin
        循环:
          等待 -> UpsertUserAll(一包曲目)
               -> UserLogout(type=1)
               -> UserLogin(isContinue=true) 续关,拿新 loginId
        收尾:最后一包登出后结束(不再续关)

二、用法

# 源数据来自二维码(会联网抓 A 的数据,并落盘到 data/<A>.json)
python reincarnate.py --dst-qr <B的二维码> --src-qr <A的二维码>

# 源数据来自已保存的 JSON(离线,推荐:不用反复消耗 A 的二维码)
python reincarnate.py --dst-qr <B的二维码> --src-json data/12345678.json

# ★ 真实上传
python reincarnate.py --dst-qr <B的二维码> --src-json data/12345678.json --send

# 只看计划,不发包(★ 建议先跑这个)
python reincarnate.py --dst-qr <B的二维码> --src-json data/12345678.json --plan
参数 默认 说明
--dst-qr 必填 目标账号 B 的二维码
--src-qr / --src-json 二选一 源数据来源
--plan — 只列分批计划,不发包
--send 关 ★ 真实发包
--limit N 0(全部) 只迁前 N 首(试跑用)
--per-packet N 4 每包曲目数,只能是 1 / 3 / 4
--wait N 5.0 每次上传前的等待秒数(0 = 不等)
--clone-identity 关 连 userName / accessCode 一起克隆(默认保留 B 自己的)
--no-continue 关 不续关,只发一包(调试用)
--debug 关 日志里带完整收发报文

三、★ 每包几首:1 / 3 / 4 可以,恰好 2 首会被服务端拒绝

真机一局(一个 credit)最多打 4 首,所以一条 UpsertUserAll 报文里的 userPlaylogList / userGamePlaylogList 最多 4 条。

但实测:每包恰好 2 首会被服务端拒绝(1 / 3 / 4 首都正常)。 所以本脚本的分批器会主动避开 2 首包:

总量 --per-packet 原始切分 修正后
10 首 4 [4,4,2] [4,3,3]
6 首 4 [4,2] [3,3]
2 首 4 [2] [1,1]
5 首 3 [3,2] [3,1,1]
8 首 3 [3,3,2] [3,3,1,1]
5 首 4 [4,1] 本来就合法,不动

修正规则(reincarnate.split_batches):

  1. 前一批 >= 4 → 从它借 1 首过来(前一批变 >=3,本批变 3)
  2. 借不到(前一批只有 3,或自己就是第一批)→ 把这个 2 拆成 1 + 1

★ 只借位是不够的:前一批是 3 时借完自己变 2,等于把 2 挪了个位置。

自检里有 1..299 首 × per∈{1,3,4} 的全量扫描,保证永不产生 2 首包。


四、★ 为什么要「续关登录」

UpsertUserAll 的 playlogId 必须等于本次登录的 loginId (PacketUpsertUserAll.cs:31 / UserAllRequestVO)。 一局结束后这个会话就被消费掉了,要继续上传必须重新登录 —— 真机走的就是「续关」:

// source/Assembly-CSharp/Process/ContinueProcess.cs:403
new PacketUserLogin(userID, accessCode, isContinue: true, GameManager.IsEventMode ? 1 : 0, ...)

对应本工程:UserLogin(user_id, token, is_continue=True) —— 和普通登录的唯一区别就是 UserLoginRequestVO.isContinue = true, 会返回新的 loginId / loginDateTime。


五、★ 登出必须用 type=1(Logout),不要用 4(TestIn)

Net.VO.Mai2/LogoutType.cs:

public enum LogoutType { None=0, Logout=1, Cancel=2, Error=3, TestIn=4, Quit=5 }

真机抓包实证 UserLogoutApi 用的是 type=1:

{"userId":12570152,"accessCode":"","regionId":8,"placeId":1007,
 "clientId":"A63E01C3047","loginDateTime":1791325354,"type":1,"delayLog":{...}}

源码侧的正常会话结束也是 type=1:

// source/Assembly-CSharp/Process.UserDataNet.State.UserDataULState/StateULUserLogout.cs:24
PacketHelper.StartPacket(new PacketUserLogout(_index, _userId, LogoutType.Logout, ...));

续关场景下用 4(TestIn)会出问题 —— 那是「测试登入 / 顶号」的语义 (GameMain.cs:104 退出游戏时才用)。

UserLogout 对 type=1 会自动带上完整 delayLog (PacketUserLogout.cs:21:logoutType == LogoutType.Logout 时才收集会话统计)。


六、playlog 从哪来

GetNeedUserData 抓不到 userPlaylogList(那是逐局记录,没有对应的 Get 接口), 所以本脚本从曲目记录反推合成:

UserMusicDetail(9 字段) → UserPlaylog(110 字段)
musicId / level musicId / level
achievement achievement
deluxscoreMax deluxscore
scoreRank scoreRank / isClear
comboStatus / syncStatus 同名
其余 config.PLAY_* 默认值

UserMusicDetail 的全部字段(Net.VO.Mai2/UserMusicDetail.cs): musicId / level / playCount / achievement / comboStatus / syncStatus / deluxscoreMax / scoreRank / extNum1。


七、迁移范围

默认迁全部节点(整账号克隆):

  • 第 1 包带全部非曲目节点(userData / userExtend / userOption / userCharacterList / userMapList / …)
  • 之后每包只带这一批曲目 + 合成的 userGamePlaylogList
  • 位串 isNew* 按 B 的基线逐条算(B 没有 → "1",已有 → "0"), 主键表见 api/upsert_user_all.py 的 DIFF_PRIMARY_KEYS

不迁的节点(SKIP_NODES):

节点 原因
userMusicDetailList 单独分批
userGamePlaylogList 本脚本按曲目合成
user2pPlaylog 2P 记录,迁移无意义
userGetPointList 里程/奖励,迁过去会凭空发点
userTradeItemList 无 Get 来源
userFriendSeasonRankingList 无 Get 来源

默认不克隆 userName / accessCode(避免把 B 改名);要连身份一起搬就加 --clone-identity。


八、⚠️ 注意事项

  1. 未实跑过。 建议先用 --plan 看计划,再 --limit 8 小批量试,确认无误再全量。
  2. 会真实写入账号 B(不可撤销)。 不传 --send 时只构造报文。
  3. 耗时:曲目多的话包数很多(每包最多 4 首),加上 --wait 的等待, 几百首会跑很久。想快可以调小 --wait。
  4. 二维码有效期 10 分钟,但转生过程中只用一次(登录时)—— 续关用的是 token,不需要新二维码。
  5. 基线快照只在第一次登录后抓一次,后续包沿用同一份做差分。 曲目节点由 extra_data 整体覆盖,位串也单独算,不受影响。