From 4959487a71d0cc9dd934387e93aa74742a16bf7b Mon Sep 17 00:00:00 2001 From: Txiaochen Date: Wed, 7 Oct 2026 01:22:38 +0800 Subject: [PATCH] =?UTF-8?q?docs(README):=20=E8=A1=A5=E5=85=A8=E4=B8=BB?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E7=94=A8=E6=B3=95=EF=BC=8C=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=20netdelivery=20=E7=AB=A0=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增「主脚本一览」总表(副作用 / 是否需要二维码) - upsert.py:独立成章,--extra-data 归位 - buy_ticket.py:补参数表 + 三道安全检查说明 - use_ticket.py:补参数表 + 真机消耗票券的报文依据 - reincarnate.py:补参数表 + 流程/分批/续关/playlog 合成原理 - netdelivery.py:新增章节(ALL.Net DLI,查/下 .app 与 .opt) - 新增「常见坑与注意事项」(二维码 / 账号状态 / 功能票 / 副作用 / 日志 / 网络 / 不入库清单) - 「将来」改为「已完成 / 将来」,列出 v0 全部交付 - 项目框架补上 netdelivery.py 与 download/ --- .gitignore | 9 + README.md | 387 ++++++++++++++++++++----- authv1.py | 563 +++++++++++++++++++++++++++++++++++++ config.py.example | 45 ++- netdelivery.py | 703 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1631 insertions(+), 76 deletions(-) create mode 100644 authv1.py create mode 100644 netdelivery.py diff --git a/.gitignore b/.gitignore index 7c0dab2..536de61 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,15 @@ docs/ gamehook/bin/ gamehook/obj/ +# ---- netdelivery 下载目录(.app / .opt 更新包很大,不入库)---- +download/ + +# ---- get_auth_list:clientId 遍历脚本与其产物(默认不入库、不推送)---- +# 产物默认落在 data/ 下(data/ 本就已经整目录忽略); +# authentication.* 再兜一层,防止用 --csv/--state 指到别处 +get_auth_list.py +authentication.* + # ---- 编辑器 / 系统 ---- .vs/ .vscode/ diff --git a/README.md b/README.md index dc79466..75e9cf1 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,9 @@ WaHappy 是一个用 **Python 纯网络方式**与舞萌DX(SDGB 国服)游 |---|---| | **`upsert.py`** | ✅ **可用** —— 扫码登录 → 上传一局成绩 → 落库回查 → 登出 | | **`buy_ticket.py`** | ✅ **可用** —— 购买功能票(2026-10-06 真机验证通过) | -| **`use_ticket.py`** | ✅ **可用** —— 读取当前持有的功能票并用 `UpsertUserAll` 消耗掉 | -| **`reincarnate.py`** | 🔧 已实现(未实跑)—— 账号数据迁移(转生),每包 ≤4 首 + 续关登录 | +| **`use_ticket.py`** | ✅ **可用** —— 读取当前持有的功能票并用 `UpsertUserAll` 消耗掉(已实跑) | +| **`reincarnate.py`** | 🔧 已实现(**未实跑**)—— 账号数据迁移(转生),每包 ≤4 首 + 续关登录 | +| **`netdelivery.py`** | ✅ **可用** —— 查 / 下 `.app` / `.opt` 更新包(ALL.Net DLI,只读,无副作用) | | `api/` 下 40+ 个接口模块 | 🔧 已实现并通过零网络自检,但**未全部在真机上验证过** | | **文档** | 📄 在 **`docs` 分支**(`mai` 分支不含)—— 开发过程、踩坑记录、实跑验证,内容比代码本身还多 | @@ -78,6 +79,7 @@ WaHappy/ ├── buy_ticket.py 主脚本:购买功能票 ├── use_ticket.py 主脚本:消耗当前持有的功能票 ├── reincarnate.py 主脚本:账号数据迁移(转生) +├── netdelivery.py 主脚本:查 / 下 .app / .opt 更新包(ALL.Net DLI) ├── selftest.py 零网络自检(238 项,判据 = 源码 + 真机日志) │ ├── api/ ★ 所有与服务器交互的模块(一个接口一个脚本) @@ -89,6 +91,7 @@ WaHappy/ │ └── ... 40+ 个接口脚本 │ ├── data/ 每账号一份用户数据快照(.json)★ 不入库 +├── download/ netdelivery.py 下载的 .app / .opt ★ 不入库 ├── logs/ 日志输出(latest.log + 日期.log)★ 不入库 └── source/ 反编译源码 ★ 不入库,需自备 ``` @@ -149,10 +152,26 @@ copy config.py.example config.py # Windows ### 3. 跑自检 ```bash -python selftest.py # 零网络自检,应该 236 项全绿(不依赖你的 config) +python selftest.py # 零网络自检,应该 238 项全绿(不依赖你的 config) ``` -### upsert.py 用法 +### 主脚本一览 + +| 脚本 | 干什么 | 副作用 | 需要二维码 | +|---|---|---|---| +| `upsert.py` | 上传一局成绩 | ★ 有 | ✅ | +| `buy_ticket.py` | 购买功能票 | ★ 有 | ✅ | +| `use_ticket.py` | 消耗当前持有的功能票 | ★ 有 | ✅ | +| `reincarnate.py` | 账号数据迁移(转生) | ★ 有 | ✅(目标号;源号可选) | +| `netdelivery.py` | 查 / 下 `.app` / `.opt` 更新包 | 只读 | ❌ | +| `selftest.py` | 零网络自检 | 无 | ❌ | + +> **所有有副作用的脚本默认都不发包**:不传 `--send` 时只构造报文(dry-run)。 +> 想看报文而不产生任何后果,先跑一次不带 `--send` 的。 + +--- + +## upsert.py —— 上传一局成绩 ```bash # 预检:登录 + 抓快照 + 组包,不发包(零副作用,建议先跑这个) @@ -171,63 +190,6 @@ python upsert.py "<二维码>" --send --no-verify # 跳过上传后的落库 > 二维码从机台的「扫码登录」界面获取,**有效期 10 分钟**。 -### buy_ticket.py 用法 - -```bash -python buy_ticket.py "<二维码>" --list # 只列当前票券和可买的票 -python buy_ticket.py "<二维码>" --charge-id 2 # 预检:只看报文 -python buy_ticket.py "<二维码>" --charge-id 2 --send # ★ 真实购买 -``` - -| chargeId | 票 | 上报的 price | -|---|---|---| -| 2 | 2倍チケット | 1 | -| 3 | 3倍チケット | 2 | -| 4 | 4倍チケット | 3 | -| 5 | 5倍チケット | 4 | -| 6 | 6倍チケット | 5 | - -> ⚠️ **账号已经持有功能票时,本脚本会直接拒绝购买**(重复购买有封号风险)。 -> 想清掉手里的票,用下面的 `use_ticket.py`。 - -### use_ticket.py 用法 - -读取账号当前**持有**的功能票,然后用 `UpsertUserAll` 表达「用这张票打了一局」把它消耗掉。 - -```bash -python use_ticket.py "<二维码>" --list # 只列当前持有的票 -python use_ticket.py "<二维码>" --ticket-id 2 # 预检:只看报文 -python use_ticket.py "<二维码>" --ticket-id 2 --send # ★ 真实消耗 -``` - -原理:`GetUserChargeApi` 与 `UpsertUserAllApi` 里的 `userChargeList` -**格式完全相同**,所以只要把该节点里目标票的 `stock` 减 1, -再让 playlog 带上 `useTicketId` / `playCredit`,服务端就认为这张票被这一局用掉了。 - -`playCredit` 的算法(`VOExtensions.cs:838-845`): - -``` -playCredit = 投币数(普通局 = 1) + 票的 creditNum -``` - -### reincarnate.py 用法(账号数据迁移) - -把一个账号的数据克隆到另一个账号。**每包最多 4 首**(真机一局最多 4 首), -每包之间用**续关登录**(`UserLogin(is_continue=True)`)换新的 `loginId`。 - -```bash -# 源数据来自二维码(会联网抓 A 的数据并落盘到 data/.json) -python reincarnate.py --dst-qr "" --src-qr "" - -# 源数据来自已保存的 JSON(离线,推荐) -python reincarnate.py --dst-qr "" --src-json data/12345678.json --plan -python reincarnate.py --dst-qr "" --src-json data/12345678.json --send - -# 试跑 ---limit 20 # 只迁前 20 首 ---clone-identity # 连 userName / accessCode 一起克隆(默认保留 B 自己的) -``` - ### 额外塞节点:`--extra-data` 真机报文的 36 个节点绝大多数能由快照 + 本局演算自动推出来, @@ -272,6 +234,292 @@ python upsert.py "<二维码>" --send --extra-data base.json --extra-data patch. --- +## buy_ticket.py —— 购买功能票 + +```bash +python buy_ticket.py "<二维码>" --list # 只列当前票券和可买的票 +python buy_ticket.py "<二维码>" --charge-id 2 # 预检:只看报文 +python buy_ticket.py "<二维码>" --charge-id 2 --send # ★ 真实购买 +``` + +| 参数 | 默认 | 说明 | +|---|---|---| +| `--charge-id N` | **5** | 买哪张票 | +| `--list` | — | 只列票券与可购买清单,不做任何写操作 | +| `--send` | 关 | ★ 真实发包 | +| `--debug` | 关 | 日志里带完整收发报文 | +| `--skip-entry` | 关 | 跳过登录后的「入场全量下载」 | +| `--wait-before N` | **30** | 买票前等待秒数(真机 login→买票是 26 秒) | + +| chargeId | 票 | 上报的 `price`(= 票的 creditNum) | +|---|---|---| +| 2 | 2倍チケット | 1 | +| 3 | 3倍チケット | 2 | +| 4 | 4倍チケット | 3 | +| 5 | 5倍チケット | 4 | +| 6 | 6倍チケット | 5 | + +### 它做了哪些安全检查 + +1. **买之前**先查 `GetGameChargeApi` —— 目标票**不在当前可购买清单里就直接退出**, + 连登录都不做(不浪费二维码)。依据 `TicketManager.cs:137-150`。 +2. **跑前查一次** `GetUserChargeApi`:**账号已经持有功能票时直接拒绝购买** + (重复购买有封号风险)。想清掉手里的票用下面的 `use_ticket.py`。 +3. **跑后再查一次**,确认票券状态符合预期。 + +> ⚠️ 不同店铺上架的票不一样。**买之前先 `--list` 看一眼**, +> 你所在的机台可能只卖其中几种。 + +--- + +## use_ticket.py —— 消耗当前持有的功能票 + +读取账号**持有**的功能票,然后用 `UpsertUserAll` 表达「用这张票打了一局」把它消耗掉。 + +```bash +python use_ticket.py "<二维码>" --list # 只列当前持有的票 +python use_ticket.py "<二维码>" --ticket-id 2 # 预检:只看报文 +python use_ticket.py "<二维码>" --ticket-id 2 --send # ★ 真实消耗 +``` + +| 参数 | 默认 | 说明 | +|---|---|---| +| `--ticket-id N` | 第一张持有的 | 消耗哪一张 | +| `--list` | — | 只列票券,不做任何写操作 | +| `--send` | 关 | ★ 真实发包 | +| `--music-id N` / `--level N` | `config.PLAY_MUSIC_ID` / `PLAY_MUSIC_LEVEL` | 「这一局」打的歌 | +| `--skip-entry` | 关 | 跳过入场全量下载 | +| `--debug` | 关 | 日志里带完整收发报文 | + +### 原理 + +`GetUserChargeApi` 与 `UpsertUserAllApi` 里的 `userChargeList` +**格式完全相同**,所以只要把该节点里目标票的 `stock` 减 1, +再让 playlog 带上 `useTicketId` / `playCredit`,服务端就认为这张票被这一局用掉了。 + +真机实证(2026-10-06 22:23 那局正好用了 2 倍券): + +``` +userChargeList: chargeId=2 stock 1 -> 0 ← 票被消耗 +userGamePlaylogList[0].useTicketId = 2 ← 用了哪张票 +userGamePlaylogList[0].playCredit = 2 +userPlaylogList[0].playCredit = 2 +userData.lastPlayCredit = 2 +``` + +`playCredit` 的算法(`VOExtensions.cs:838-845`): + +``` +playCredit = 投币数(普通局 = 1) + 票的 creditNum +``` + +> **典型用途**:账号手里挂着票时 `buy_ticket.py` 会拒绝再买; +> 想再买一张就先把它用掉。也可以直接在机台上打一局并选用它。 + +--- + +## reincarnate.py —— 账号数据迁移(转生) + +把一个账号(源 A)的数据克隆到另一个账号(目标 B)。 + +```bash +# 源数据来自二维码(会联网抓 A 的数据并落盘到 data/.json) +python reincarnate.py --dst-qr "" --src-qr "" + +# 源数据来自已保存的 JSON(离线,推荐:不用反复消耗 A 的二维码) +python reincarnate.py --dst-qr "" --src-json data/12345678.json --plan +python reincarnate.py --dst-qr "" --src-json data/12345678.json --send +``` + +| 参数 | 默认 | 说明 | +|---|---|---| +| `--dst-qr` | 必填 | 目标账号 B 的二维码 | +| `--src-qr` / `--src-json` | 二选一 | 源数据来源(二维码联网抓 / 读 `data/.json`) | +| `--plan` | — | 只列分批计划,不发包(**建议先跑这个**) | +| `--send` | 关 | ★ 真实发包 | +| `--limit N` | 0(全部) | 只迁前 N 首(试跑用) | +| `--per-packet N` | 4 | 每包曲目数,**不能超过 4** | +| `--clone-identity` | 关 | 连 `userName` / `accessCode` 一起克隆(默认**保留 B 自己的**) | +| `--no-continue` | 关 | 不续关,只发一包(调试用) | +| `--debug` | 关 | 日志里带完整收发报文 | + +### 流程 + +``` +源账号 A:二维码 → GetNeedUserData(并落盘 data/.json) + 或直接读 data/.json + ↓ +目标账号 B:二维码 → UserPreview → UserLogin + 循环: + UpsertUserAll(每包 ≤4 首 + 从曲目记录反推的 playlog) + UserLogin(is_continue=True) ← ★ 续关,换新 loginId + 收尾:UserLogout +``` + +### 为什么一次最多 4 首 + +真机一局(一个 credit)最多打 4 首,所以一条 `UpsertUserAll` 报文里的 +`userPlaylogList` / `userGamePlaylogList` **最多 4 条**。 +本脚本把源账号的 `userMusicDetailList` 按 4 条一组切分,一组 = 一局。 + +### 为什么要「续关登录」 + +`UpsertUserAll` 的 `playlogId` 必须等于**本次登录**的 `loginId`。 +一局结束后这个会话就被消费掉了,要继续上传必须重新登录 —— +真机走的就是「续关」(`Process/ContinueProcess.cs:403`): + +```csharp +new PacketUserLogin(userID, accessCode, isContinue: true, ...) +``` + +本工程对应 `UserLogin(user_id, token, is_continue=True)`。 + +### playlog 从哪来 + +`GetNeedUserData` 抓不到 `userPlaylogList`(那是逐局记录,没有对应的 Get 接口), +所以本脚本**从曲目记录反推合成**: + +| `UserMusicDetail`(9 字段) | → | `UserPlaylog`(110 字段) | +|---|---|---| +| `musicId` / `level` | | `musicId` / `level` | +| `achievement` | | `achievement` | +| `deluxscoreMax` | | `deluxscore` | +| `scoreRank` | | `scoreRank` / `isClear` | +| `comboStatus` / `syncStatus` | | 同名 | +| 其余 | | `config.PLAY_*` 默认值 | + +### 迁移范围 + +默认迁**全部节点**(整账号克隆)。第 1 包带全部非曲目节点,之后每包只带这一批曲目; +位串 `isNew*` 按 **B 的基线**逐条算(B 没有 → `"1"`,已有 → `"0"`)。 + +> ⚠️ **未实跑过。** 建议先用 `--plan` 看计划,再 `--limit 8` 小批量试,确认无误再全量。 + +--- + +## netdelivery.py —— 查 / 下 `.app` / `.opt` 更新包 + +机台做完 ALL.Net 认证后,AMDaemon 会再问一次「我该下什么包」—— +这就是 **DLI(Download Instruction,配信指示書)**: + +``` +POST http://at.sys-allnet.cn/net/delivery/instruction +body = title_id=SDGB&title_ver=<版本>&client_id= + +→ result=1&uri=|