v0版本完成:精简 README、修正转生流程、每包禁止 2 首
1) reincarnate.py 流程修正(按使用方口径)
登录 -> 等待 -> 上传 -> 登出(type=1) -> 续关登录 -> 等待 -> 上传 -> 登出 -> ...
- 每包之间**先登出再续关**(原来只在最后登出一次)
- 登出改用 type=1(LogoutType.Logout)
真机抓包实证 UserLogoutApi 就是 type=1(带 delayLog);
续关场景用 4(TestIn)会出问题 —— 那是「测试登入/顶号」的语义
- 新增 --wait N(默认 5.0 秒,每次上传前的等待)
2) 新增分批器 split_batches:★ 每包恰好 2 首会被服务端拒绝
- 1 / 3 / 4 首都正常,只有 2 首不行
- 分批时主动避开:10 首 [4,4,2] -> [4,3,3];6 首 [4,2] -> [3,3];
5 首 per=3 [3,2] -> [3,1,1]
- --per-packet 只接受 1/3/4
- 自检加了 1..299 首 × per∈{1,3,4} 的全量扫描
3) README 精简(24 KB -> 11 KB)
- 删掉「关于购买功能票(已解决)」(根因分析移到 docs)
- 删掉「常见坑与注意事项」(移到 docs)
- 功能票只列 2/3/5 倍票;只简述用票原理
- 转生只简述「干了什么」
- netdelivery 只讲用法,不讲运行原理
- 新增「文档索引」章节
4) 自检 238 -> 249 项全绿
This commit is contained in:
@@ -21,31 +21,18 @@ WaHappy 是一个用 **Python 纯网络方式**与舞萌DX(SDGB 国服)游
|
||||
|
||||
---
|
||||
|
||||
## 当前状态:v0,**未完工**
|
||||
|
||||
**v0 目前只是一个占位版本,请以「能不能用」而不是「有多少代码」来判断它。**
|
||||
## 当前状态:v0
|
||||
|
||||
| 脚本 | 状态 |
|
||||
|---|---|
|
||||
| **`upsert.py`** | ✅ **可用** —— 扫码登录 → 上传一局成绩 → 落库回查 → 登出 |
|
||||
| **`buy_ticket.py`** | ✅ **可用** —— 购买功能票(2026-10-06 真机验证通过) |
|
||||
| **`use_ticket.py`** | ✅ **可用** —— 读取当前持有的功能票并用 `UpsertUserAll` 消耗掉(已实跑) |
|
||||
| **`reincarnate.py`** | 🔧 已实现(**未实跑**)—— 账号数据迁移(转生),每包 ≤4 首 + 续关登录 |
|
||||
| **`netdelivery.py`** | ✅ **可用** —— 查 / 下 `.app` / `.opt` 更新包(ALL.Net DLI,只读,无副作用) |
|
||||
| **`buy_ticket.py`** | ✅ **可用** —— 购买功能票 |
|
||||
| **`use_ticket.py`** | ✅ **可用** —— 消耗当前持有的功能票 |
|
||||
| **`reincarnate.py`** | 🔧 已实现(**未实跑**)—— 账号数据迁移(转生) |
|
||||
| **`netdelivery.py`** | ✅ **可用** —— 查 / 下 `.app` / `.opt` 更新包(只读) |
|
||||
| `api/` 下 40+ 个接口模块 | 🔧 已实现并通过零网络自检,但**未全部在真机上验证过** |
|
||||
| **文档** | 📄 在 **`docs` 分支**(`mai` 分支不含)—— 开发过程、踩坑记录、实跑验证,内容比代码本身还多 |
|
||||
|
||||
### 关于购买功能票(已解决)
|
||||
|
||||
`UpsertUserChargelogApi` 曾经一直返回 `returnCode=0`。根因是 **请求头多发了
|
||||
`Accept` 与 `Connection`** —— 真机(Unity Mono 的 `HttpWebRequest`)**不发这两个头**:
|
||||
|
||||
```
|
||||
number / Content-Type / User-Agent / charset / Mai-Encoding / Content-Encoding / Cookie
|
||||
```
|
||||
|
||||
删掉之后一次通过(`stock` 0 → 1,落库确认)。完整排查过程见
|
||||
[购买功能票流程](https://git.fragrance.moe/Txiaochen/wahappy/src/branch/docs/docs/购买功能票流程.md)。
|
||||
**文档**在 [`docs` 分支](https://git.fragrance.moe/Txiaochen/wahappy/src/branch/docs),见文末[文档索引](#文档索引)。
|
||||
|
||||
---
|
||||
|
||||
@@ -73,14 +60,14 @@ v0 的开发过程中,我反复把同一个目标拆成小段丢给 AI,结
|
||||
|
||||
```
|
||||
WaHappy/
|
||||
├── config.py 运行时可配置项(密钥 / 机台 / 歌曲数据 / 日志级别)★ 需自建
|
||||
├── config.py.example 配置模板(机台与店铺标识留空待填)
|
||||
├── config.py 运行时可配置项(机台标识 / 歌曲数据 / 日志级别)★ 需自建
|
||||
├── config.py.example 配置模板
|
||||
├── upsert.py ★ 主脚本:扫码登录 → 上传成绩 → 登出
|
||||
├── buy_ticket.py 主脚本:购买功能票
|
||||
├── use_ticket.py 主脚本:消耗当前持有的功能票
|
||||
├── reincarnate.py 主脚本:账号数据迁移(转生)
|
||||
├── netdelivery.py 主脚本:查 / 下 .app / .opt 更新包(ALL.Net DLI)
|
||||
├── selftest.py 零网络自检(238 项,判据 = 源码 + 真机日志)
|
||||
├── netdelivery.py 主脚本:查 / 下 .app / .opt 更新包
|
||||
├── selftest.py 零网络自检(249 项,判据 = 源码 + 真机日志)
|
||||
│
|
||||
├── api/ ★ 所有与服务器交互的模块(一个接口一个脚本)
|
||||
│ ├── __init__.py 统一导出 + 日志自动跟踪
|
||||
@@ -102,18 +89,15 @@ WaHappy/
|
||||
**命名约定**:服务端 API 名里带 `Get` 的,**脚本名与函数名一律带 `get` / `Get`**
|
||||
(不要模棱两可)。所以取数脚本全是 `get_*.py`,函数是 `GetXxx`。
|
||||
|
||||
> **`docs/` 在 [`docs` 分支](https://git.fragrance.moe/Txiaochen/wahappy/src/branch/docs),`source/` 与 `logs/` 不入库。**
|
||||
>
|
||||
> `source/` 是反编译源码,**需要自备**。
|
||||
|
||||
### 分支说明
|
||||
|
||||
| 分支 | 内容 |
|
||||
|---|---|
|
||||
| **`mai`** | 主分支 —— 代码(`api/` / `upsert.py` / `config.py` / `selftest.py` / `gamehook/`) |
|
||||
| **`mai`** | 主分支 —— 代码 |
|
||||
| **`docs`** | 孤儿分支 —— 只有 `docs/`,开发文档与踩坑记录 |
|
||||
|
||||
两个分支**没有共同的提交历史**,各自独立。
|
||||
`source/` 是反编译源码,**需要自备**。
|
||||
|
||||
---
|
||||
|
||||
@@ -134,25 +118,25 @@ cp config.py.example config.py # Linux / macOS
|
||||
copy config.py.example config.py # Windows
|
||||
```
|
||||
|
||||
然后**必须**填好 **`config.py` 第二节「机台与店铺标识」的 6 个值**:
|
||||
然后**必须**填好 **`config.py` 第二节「机台与店铺标识」**:
|
||||
|
||||
| 变量 | 填什么 |
|
||||
|---|---|
|
||||
| `KEYCHIP_ID` | 你的机台 Keychip(**完整形式**,带连字符,形如 `XXXX-XXXXXXXXXXX`) |
|
||||
| `CLIENT_ID` | Keychip **短值**(去掉连字符取前 11 位) |
|
||||
| `REGION_ID` | 你所在区域 ID |
|
||||
| `CLIENT_ID` | Keychip 短值(15 位 Keychip 去掉连字符取前 11 位),形如 `A63E01ABCDE` |
|
||||
| `PLACE_ID` | 你所在店铺 ID |
|
||||
| `PLACE_NAME` | 店铺名(游戏服务器不返回,只能手填) |
|
||||
| `REGION_ID` | 你所在区域 ID |
|
||||
| `PLACE_NAME` | 店铺名 |
|
||||
| `REGION_NAME` | 区域名 |
|
||||
| `KEYCHIP_ID` | **由上面两个值自动推导**,不要手改 |
|
||||
|
||||
> ⚠️ **这 6 个值留空或填错,发出去的请求带的就是别人的机台身份,服务端会直接拒绝。**
|
||||
> ⚠️ 填错的话,发出去的请求带的就是别人的机台身份,服务端会直接拒绝。
|
||||
>
|
||||
> `config.py` 已在 `.gitignore` 里 —— 它是你的私有配置,**不要提交**。
|
||||
|
||||
### 3. 跑自检
|
||||
|
||||
```bash
|
||||
python selftest.py # 零网络自检,应该 238 项全绿(不依赖你的 config)
|
||||
python selftest.py # 零网络自检,应该 249 项全绿(不依赖你的 config)
|
||||
```
|
||||
|
||||
### 主脚本一览
|
||||
@@ -169,6 +153,8 @@ python selftest.py # 零网络自检,应该 238 项全绿(不依赖
|
||||
> **所有有副作用的脚本默认都不发包**:不传 `--send` 时只构造报文(dry-run)。
|
||||
> 想看报文而不产生任何后果,先跑一次不带 `--send` 的。
|
||||
|
||||
> 二维码从机台的「扫码登录」界面获取,**有效期 10 分钟**(不是按次数计量的)。
|
||||
|
||||
---
|
||||
|
||||
## upsert.py —— 上传一局成绩
|
||||
@@ -188,49 +174,18 @@ python upsert.py "<二维码>" --send --no-verify # 跳过上传后的落库
|
||||
|
||||
**换歌 / 换成绩只改 `config.py` 的 `PLAY_*`,不用动任何代码。**
|
||||
|
||||
> 二维码从机台的「扫码登录」界面获取,**有效期 10 分钟**。
|
||||
|
||||
### 额外塞节点:`--extra-data`
|
||||
|
||||
真机报文的 36 个节点绝大多数能由快照 + 本局演算自动推出来,
|
||||
但偶尔想手工覆盖某几个(比如本局拿到的新 KaleidxScope 门),就用 `--extra-data`:
|
||||
想手工覆盖某几个节点(比如本局拿到的新 KaleidxScope 门):
|
||||
|
||||
```bash
|
||||
# ① 从文件读(★ 推荐 —— 不用跟 shell 引号搏斗)
|
||||
python upsert.py "<二维码>" --send --extra-data extra.json
|
||||
|
||||
# ② 直接写 JSON 字符串
|
||||
python upsert.py "<二维码>" --send --extra-data '{"isNewMapList":"1"}'
|
||||
|
||||
# ③ 多个来源,后给的覆盖先给的
|
||||
python upsert.py "<二维码>" --send --extra-data base.json --extra-data patch.json
|
||||
```
|
||||
|
||||
`extra.json` 长这样:
|
||||
键必须是 `upsertUserAll` 的 **36 个节点**之一,写错会报错并列出可用节点名。
|
||||
**JSON 在扫码之前就校验**,写错直接退出,不浪费二维码。
|
||||
|
||||
```json
|
||||
{
|
||||
"userKaleidxScopeList": [
|
||||
{"gateId": 1, "isGateFound": true, "isKeyFound": true, "isClear": false,
|
||||
"totalRestLife": 0, "totalAchievement": 0, "totalDeluxscore": 0,
|
||||
"bestAchievement": 0, "bestDeluxscore": 0,
|
||||
"bestAchievementDate": "", "bestDeluxscoreDate": "",
|
||||
"playCount": 0, "clearDate": "", "lastPlayDate": "", "isInfoWatched": false}
|
||||
],
|
||||
"isNewKaleidxScopeList": "1"
|
||||
}
|
||||
```
|
||||
|
||||
| 规则 | 说明 |
|
||||
|---|---|
|
||||
| 键 | 必须是 `upsertUserAll` 的 **36 个节点**之一(含 11 条 `isNew*` 位串),写错会报错并列出可用节点名 |
|
||||
| 值形态 | 按节点形态强制转换,传错立刻报错 |
|
||||
| 位串 | 给了数组没给位串 → 按长度补 `"0"` 并提示。**新增条目请显式传 `"1"`**(`"0"` = 存量更新) |
|
||||
| 优先级 | 最高,覆盖快照演算结果 |
|
||||
| 校验时机 | ★ **扫码之前**就校验 —— JSON 写错直接退出,不浪费二维码 |
|
||||
|
||||
> ⚠ 在 PowerShell / CMD 里写内联 JSON 很容易被吞掉双引号,
|
||||
> **建议一律用文件**(`--extra-data extra.json`)。
|
||||
> ⚠️ 在 PowerShell / CMD 里写内联 JSON 很容易被吞掉双引号,**建议一律用文件**。
|
||||
|
||||
---
|
||||
|
||||
@@ -242,83 +197,41 @@ 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) |
|
||||
| chargeId | 票 | 上报的 `price` |
|
||||
|---|---|---|
|
||||
| 2 | 2倍チケット | 1 |
|
||||
| 3 | 3倍チケット | 2 |
|
||||
| 4 | 4倍チケット | 3 |
|
||||
| 5 | 5倍チケット | 4 |
|
||||
| 6 | 6倍チケット | 5 |
|
||||
|
||||
### 它做了哪些安全检查
|
||||
> 默认 `--charge-id 5`。**不同店铺上架的票不一样**,买之前先 `--list`。
|
||||
|
||||
1. **买之前**先查 `GetGameChargeApi` —— 目标票**不在当前可购买清单里就直接退出**,
|
||||
连登录都不做(不浪费二维码)。依据 `TicketManager.cs:137-150`。
|
||||
2. **跑前查一次** `GetUserChargeApi`:**账号已经持有功能票时直接拒绝购买**
|
||||
(重复购买有封号风险)。想清掉手里的票用下面的 `use_ticket.py`。
|
||||
3. **跑后再查一次**,确认票券状态符合预期。
|
||||
### 它做的安全检查
|
||||
|
||||
> ⚠️ 不同店铺上架的票不一样。**买之前先 `--list` 看一眼**,
|
||||
> 你所在的机台可能只卖其中几种。
|
||||
1. 买之前查 `GetGameChargeApi` —— 目标票不在可购买清单里就**连登录都不做**直接退出
|
||||
2. 跑前查 `GetUserChargeApi` —— **账号已持有功能票时直接拒绝购买**(重复购买有封号风险)
|
||||
3. 跑后再查一次确认状态
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
```
|
||||
**原理**:`GetUserChargeApi` 与 `UpsertUserAllApi` 里的 `userChargeList` **格式完全相同**,
|
||||
所以把该节点里目标票的 `stock` 减 1,再让 playlog 带上 `useTicketId` / `playCredit`,
|
||||
服务端就认为这张票被这一局用掉了。
|
||||
|
||||
> **典型用途**:账号手里挂着票时 `buy_ticket.py` 会拒绝再买;
|
||||
> 想再买一张就先把它用掉。也可以直接在机台上打一局并选用它。
|
||||
> 想再买一张就先用这个把它用掉。
|
||||
|
||||
详细原理与报文依据见 [功能票使用说明](https://git.fragrance.moe/Txiaochen/wahappy/src/branch/docs/docs/功能票使用说明.md)。
|
||||
|
||||
---
|
||||
|
||||
## reincarnate.py —— 账号数据迁移(转生)
|
||||
## reincarnate.py —— 账号转生
|
||||
|
||||
把一个账号(源 A)的数据克隆到另一个账号(目标 B)。
|
||||
|
||||
@@ -326,100 +239,36 @@ playCredit = 投币数(普通局 = 1) + 票的 creditNum
|
||||
# 源数据来自二维码(会联网抓 A 的数据并落盘到 data/<A>.json)
|
||||
python reincarnate.py --dst-qr "<B的二维码>" --src-qr "<A的二维码>"
|
||||
|
||||
# 源数据来自已保存的 JSON(离线,推荐:不用反复消耗 A 的二维码)
|
||||
# 源数据来自已保存的 JSON(离线,推荐)
|
||||
python reincarnate.py --dst-qr "<B的二维码>" --src-json data/12345678.json --plan
|
||||
python reincarnate.py --dst-qr "<B的二维码>" --src-json data/12345678.json --send
|
||||
```
|
||||
|
||||
**它干了什么**:
|
||||
|
||||
1. 取源账号 A 的 19 个节点数据(联网抓或读 `data/<A>.json`)
|
||||
2. 用目标账号 B 的二维码登录
|
||||
3. 把 A 的 `userMusicDetailList` **分批**上传到 B —— 每包最多 4 首,
|
||||
并自动避开**被服务端禁止的 2 首包**(`[4,4,2]` → `[4,3,3]`)
|
||||
4. 每包之间:**等待 → 上传 → 登出(type=1)→ 续关登录**,循环到传完
|
||||
5. 每包的 playlog 从曲目记录**反推合成**(`achievement` / `deluxscore` / `scoreRank` …)
|
||||
|
||||
| 参数 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `--dst-qr` | 必填 | 目标账号 B 的二维码 |
|
||||
| `--src-qr` / `--src-json` | 二选一 | 源数据来源(二维码联网抓 / 读 `data/<userId>.json`) |
|
||||
| `--plan` | — | 只列分批计划,不发包(**建议先跑这个**) |
|
||||
| `--send` | 关 | ★ 真实发包 |
|
||||
| `--limit N` | 0(全部) | 只迁前 N 首(试跑用) |
|
||||
| `--per-packet N` | 4 | 每包曲目数,**不能超过 4** |
|
||||
| `--clone-identity` | 关 | 连 `userName` / `accessCode` 一起克隆(默认**保留 B 自己的**) |
|
||||
| `--no-continue` | 关 | 不续关,只发一包(调试用) |
|
||||
| `--debug` | 关 | 日志里带完整收发报文 |
|
||||
| `--per-packet N` | 4 | 每包曲目数,**只能是 1 / 3 / 4** |
|
||||
| `--wait N` | 5.0 | 每次上传前的等待秒数 |
|
||||
| `--clone-identity` | 关 | 连 `userName` / `accessCode` 一起克隆(默认保留 B 自己的) |
|
||||
|
||||
### 流程
|
||||
> ⚠️ **未实跑过。** 建议先 `--plan`,再 `--limit 8` 小批量试。
|
||||
|
||||
```
|
||||
源账号 A:二维码 → GetNeedUserData(并落盘 data/<A>.json)
|
||||
或直接读 data/<A>.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` 小批量试,确认无误再全量。
|
||||
完整流程、分批规则、续关与登出类型的依据见 [账号转生说明](https://git.fragrance.moe/Txiaochen/wahappy/src/branch/docs/docs/账号转生说明.md)。
|
||||
|
||||
---
|
||||
|
||||
## 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=<Keychip 短值>
|
||||
|
||||
→ result=1&uri=<patch 指示書>|<option 指示書>
|
||||
```
|
||||
|
||||
指示書本身是个 INI,真正要下的包写在 `INSTALLn=` 里:
|
||||
|
||||
```ini
|
||||
[COMMON] INSTALL1=https://.../SDGB_1.56.00_..._1_1.55.01.app
|
||||
[OPTIONAL] INSTALL1=https://.../SDGB_A031_..._0.opt
|
||||
INSTALL2=https://.../SDGB_A005_..._0.opt
|
||||
```
|
||||
|
||||
> `INSTALLn=` 才是**要下载的包**;`EXISTn=` 是「本机应该已经有的文件」,
|
||||
> 只是文件名,不是下载项。
|
||||
|
||||
```bash
|
||||
python netdelivery.py # 用 config.py 里的机台查,然后**多选下载**
|
||||
python netdelivery.py A63E01ABCDE # 指定 Keychip
|
||||
@@ -428,98 +277,16 @@ python netdelivery.py --all # 全下(不交互)
|
||||
python netdelivery.py --only 1,3 # 只下第 1、3 项(不交互)
|
||||
python netdelivery.py --ver 1.55 # 指定上报版本(默认 config.MAI_ENCODING)
|
||||
python netdelivery.py --out D:\SDGB2026\update
|
||||
python netdelivery.py --insecure # 关掉 TLS 校验(CDN 证书有问题时用)
|
||||
```
|
||||
|
||||
交互选择支持:逗号/空格分隔、区间(`1-3`)、`a`=全选、`n`=全不选、`q`=退出。
|
||||
下载带**断点续传**(`.part` 临时文件 + `Range`,下完改名)与进度/速率显示。
|
||||
下载带**断点续传**与进度/速率显示,默认存到 `download/`(不入库)。
|
||||
|
||||
实测(`--ver 1.56`):
|
||||
|
||||
```
|
||||
序号 类型 大小 来源段/键 文件名
|
||||
[ 1] opt 7.25 MB COMMON/INSTALL1 SDGB_A031_20260909101347_0.opt
|
||||
[ 2] opt 3.00 MB OPTIONAL/INSTALL1 SDGB_A005_20260610180958_0.opt
|
||||
```
|
||||
|
||||
### 几点实测结论
|
||||
|
||||
| 结论 | 说明 |
|
||||
|---|---|
|
||||
| `title_ver` 决定拿哪份指示書 | `1.55` → patch 1.56 + opt A005;`1.56` → 仅 opt A031;`≤1.54` → `uri=null` |
|
||||
| 包可以断点续传 | 返回 `206` + `Content-Range` |
|
||||
| 已下架的历史包会回 **504** | 这是华立 CDN 的状态,**不是脚本的问题** |
|
||||
|
||||
> 默认下载到 `download/`(已加入 `.gitignore`,`.app` 动辄上 GB)。
|
||||
> 这是**只读**操作,不写任何账号数据。
|
||||
> 需要同目录下的 `authv1.py` 提供 ALL.Net 协议层。
|
||||
|
||||
---
|
||||
|
||||
## 常见坑与注意事项
|
||||
|
||||
### 二维码
|
||||
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| 有效期 | **10 分钟**,过期后 aimedb 返回 `errorID=2`(**不是按次数计量的**,重新生成即可) |
|
||||
| 来源 | 机台的「扫码登录」界面 |
|
||||
| 每个脚本各要一次 | `reincarnate.py` 需要目标号的二维码;源号可以用 `--src-json` 免掉 |
|
||||
|
||||
### 账号状态
|
||||
|
||||
- **一定要登出**。账号卡在 `isLogin=True` 时,下次登录会 `returnCode=100`。
|
||||
本工程所有主脚本都在 `finally` 里兜底登出,异常也不会漏。
|
||||
- 想确认状态可以看 `GetUserPreviewApi` 的 `isLogin` 字段。
|
||||
|
||||
### 功能票
|
||||
|
||||
- 账号**已经持有**功能票时,`buy_ticket.py` **会拒绝购买**(重复购买有封号风险)。
|
||||
- 想再买一张,先用 `use_ticket.py` 把手里那张用掉,或直接在机台上打一局选用它。
|
||||
- 不同店铺上架的票不一样,买之前先 `--list`。
|
||||
|
||||
### 副作用
|
||||
|
||||
| 命令 | 后果 |
|
||||
|---|---|
|
||||
| 不带 `--send` | 只构造报文,**不发包**(安全,随便跑) |
|
||||
| 带 `--send` | **真实写入服务器,不可撤销** |
|
||||
|
||||
### 日志
|
||||
|
||||
```
|
||||
logs/latest.log 只含**当次**运行(下次覆盖)
|
||||
logs/YYYY-MM-DD.log 当日全部日志(追加)
|
||||
```
|
||||
|
||||
改 `config.py` 的 `LOG_LEVEL`:
|
||||
|
||||
| 值 | 效果 |
|
||||
|---|---|
|
||||
| `"info"` | 只展示 api 脚本的运行情况和结果 |
|
||||
| `"debug"` | 额外展示向 api 发送的数据与 api 返回的数据 |
|
||||
|
||||
> 主脚本也可以加 `--debug` 临时开启,不用改 `config.py`。
|
||||
|
||||
### 网络
|
||||
|
||||
- 服务器偶发超时 / 连接重置,**重试即可**(买票那次就是这么过的)。
|
||||
- 所有请求默认 `timeout` 10 秒,登录与结算用 60 秒。
|
||||
|
||||
### 不要提交的东西
|
||||
|
||||
```
|
||||
config.py 你的机台身份,.gitignore 已忽略
|
||||
data/ 用户数据快照
|
||||
download/ .app / .opt 更新包
|
||||
source/ 反编译源码(版权)
|
||||
logs/ 运行日志
|
||||
```
|
||||
|
||||
> ⚠️ **`.gitignore` 的行尾注释不是注释**。写 `data/ # 注释` 会变成一条匹配不到任何东西的规则
|
||||
> —— 这条坑踩过一次(用户数据被误提交),已修。**注释必须独占一行。**
|
||||
|
||||
---
|
||||
|
||||
## 主脚本怎么写
|
||||
|
||||
**核心原则:不要为每个 api 调用写日志。** 日志已经在 `api/` 里了。
|
||||
@@ -529,7 +296,7 @@ logs/ 运行日志
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
from api import (setup_logging, teardown_logging,
|
||||
ParseQRCode, UserPreview, UserLogin, UserLogout, LogoutType,
|
||||
ParseQRCode, GetUserPreview, UserLogin, UserLogout, LogoutType,
|
||||
UpsertUserAll)
|
||||
from config import REGION_ID, PLACE_ID
|
||||
|
||||
@@ -540,7 +307,7 @@ def main() -> int:
|
||||
user_id, token = ParseQRCode(sys.argv[1])
|
||||
session.update(user_id=user_id, token=token)
|
||||
|
||||
UserPreview(user_id, token) # 顶号保护
|
||||
GetUserPreview(user_id, token) # 顶号保护
|
||||
login = UserLogin(user_id, token,
|
||||
region_id=REGION_ID, place_id=PLACE_ID)
|
||||
session.update(login_dt=login.loginDateTime, logged_in=True)
|
||||
@@ -555,7 +322,7 @@ def main() -> int:
|
||||
finally:
|
||||
if session["logged_in"]: # ★ 一定要保证登出
|
||||
UserLogout(session["user_id"], session["token"], session["login_dt"],
|
||||
logout_type=LogoutType.TestIn,
|
||||
logout_type=LogoutType.Logout,
|
||||
region_id=REGION_ID, place_id=PLACE_ID)
|
||||
teardown_logging()
|
||||
|
||||
@@ -579,12 +346,12 @@ if __name__ == "__main__":
|
||||
|
||||
- ✅ 扫码登录 / 登出 / 心跳,完整会话链路
|
||||
- ✅ 上传一局成绩(36 节点 / 110 字段 / 差分 / 落库回查)
|
||||
- ✅ 购买功能票(真机验证通过)
|
||||
- ✅ 消耗持有的功能票(已实跑)
|
||||
- ✅ 购买功能票
|
||||
- ✅ 消耗持有的功能票
|
||||
- ✅ 账号数据快照落盘(`data/<userId>.json`)
|
||||
- ✅ 账号数据迁移(转生)
|
||||
- ✅ 查 / 下 `.app` / `.opt` 更新包(ALL.Net DLI)
|
||||
- ✅ 零网络自检 238 项
|
||||
- ✅ 查 / 下 `.app` / `.opt` 更新包
|
||||
- ✅ 零网络自检 249 项
|
||||
- ✅ MelonLoader 抓包 Mod(`gamehook/`)
|
||||
|
||||
**将来可能会加**
|
||||
@@ -596,6 +363,29 @@ if __name__ == "__main__":
|
||||
|
||||
---
|
||||
|
||||
## 文档索引
|
||||
|
||||
全部在 [`docs` 分支](https://git.fragrance.moe/Txiaochen/wahappy/src/branch/docs):
|
||||
|
||||
| 文档 | 内容 |
|
||||
|---|---|
|
||||
| [项目总览](https://git.fragrance.moe/Txiaochen/wahappy/src/branch/docs/docs/项目总览.md) | 开发向主文档,该放哪 / 该怎么写 |
|
||||
| [主脚本编写指南](https://git.fragrance.moe/Txiaochen/wahappy/src/branch/docs/docs/主脚本编写指南.md) | 写新主脚本的规范 |
|
||||
| [上传成绩开发文档](https://git.fragrance.moe/Txiaochen/wahappy/src/branch/docs/docs/上传成绩开发文档.md) | `UpsertUserAll` 的 36 节点 / 110 字段 |
|
||||
| [UpsertUserAll踩坑清单](https://git.fragrance.moe/Txiaochen/wahappy/src/branch/docs/docs/UpsertUserAll踩坑清单.md) | 28 条踩坑记录 |
|
||||
| [功能票使用说明](https://git.fragrance.moe/Txiaochen/wahappy/src/branch/docs/docs/功能票使用说明.md) | 买票 / 用票的完整原理与报文依据 |
|
||||
| [账号转生说明](https://git.fragrance.moe/Txiaochen/wahappy/src/branch/docs/docs/账号转生说明.md) | `reincarnate.py` 完整流程 |
|
||||
| [netdelivery说明](https://git.fragrance.moe/Txiaochen/wahappy/src/branch/docs/docs/netdelivery说明.md) | DLI 协议与运行原理 |
|
||||
| [常见坑与注意事项](https://git.fragrance.moe/Txiaochen/wahappy/src/branch/docs/docs/常见坑与注意事项.md) | 二维码 / 账号状态 / 副作用 / 日志 / 网络 |
|
||||
| [购买功能票流程](https://git.fragrance.moe/Txiaochen/wahappy/src/branch/docs/docs/购买功能票流程.md) | 买票接口的完整排查过程 |
|
||||
| [购买功能票_存档与交接](https://git.fragrance.moe/Txiaochen/wahappy/src/branch/docs/docs/购买功能票_存档与交接.md) | 排查存档 |
|
||||
| [真机Hook对照报告](https://git.fragrance.moe/Txiaochen/wahappy/src/branch/docs/docs/真机Hook对照报告.md) | 抓包 Mod 对照 |
|
||||
| [实跑验证记录](https://git.fragrance.moe/Txiaochen/wahappy/src/branch/docs/docs/实跑验证记录.md) | 每次实跑的结果 |
|
||||
| [舞萌DX_扫码登录协议技术规范](https://git.fragrance.moe/Txiaochen/wahappy/src/branch/docs/docs/舞萌DX_扫码登录协议技术规范.md) | aimedb 扫码协议 |
|
||||
| [日志与主脚本指南](https://git.fragrance.moe/Txiaochen/wahappy/src/branch/docs/docs/日志与主脚本指南.md) | 日志系统说明 |
|
||||
|
||||
---
|
||||
|
||||
## 特别鸣谢
|
||||
|
||||
- **志成🍥** —— 提供脱壳 DLL
|
||||
|
||||
+119
-17
@@ -11,19 +11,27 @@ reincarnate.py —— 账号数据迁移(转生):把账号 A 的数据克
|
||||
↓ 节点数据
|
||||
┌─ 目标账号 B ───────────────────────────────────────────┐
|
||||
│ 二维码 -> ParseQRCode -> UserPreview -> UserLogin │
|
||||
│ 循环:UpsertUserAll(每包最多 4 首曲目) │
|
||||
│ 然后 UserLogin(isContinue=true) 续关,拿新 loginId│
|
||||
│ 收尾:UserLogout │
|
||||
│ 循环: │
|
||||
│ 等待 -> UpsertUserAll(一包曲目) │
|
||||
│ -> UserLogout(type=1) │
|
||||
│ -> UserLogin(isContinue=true) 续关,拿新 loginId │
|
||||
│ 收尾:最后一包登出后结束(不再续关) │
|
||||
└────────────────────────────────────────────────────────┘
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
★ 为什么一次最多传 4 首
|
||||
★ 每包几首:1 / 3 / 4 可以,**恰好 2 首会被服务端拒绝**
|
||||
--------------------------------------------------------------------------------
|
||||
真机一局(一个 credit)最多打 4 首,所以一条 UpsertUserAll 报文里的
|
||||
userPlaylogList / userGamePlaylogList **最多 4 条**。
|
||||
|
||||
本脚本把源账号的 userMusicDetailList 按 4 条一组切分,一组 = 一局。
|
||||
★ 每局都必须带**非空**的 userPlaylogList —— 踩坑清单第 2 条实测:
|
||||
★ 但实测:**每包恰好 2 首会被服务端拒绝**(1 / 3 / 4 首都正常)。
|
||||
所以本脚本的分批器会**主动避开 2 首包**:
|
||||
|
||||
10 首 -> [4, 4, 2] -> [4, 3, 3]
|
||||
6 首 -> [4, 2] -> [3, 3]
|
||||
2 首 -> [2] -> [1, 1]
|
||||
|
||||
★ 每包都必须带**非空**的 userPlaylogList —— 踩坑清单第 2 条实测:
|
||||
留空会被服务端当成无效包**静默丢弃**(returnCode=1 但不落库)。
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -41,6 +49,11 @@ reincarnate.py —— 账号数据迁移(转生):把账号 A 的数据克
|
||||
它和普通登录的唯一区别就是 UserLoginRequestVO.isContinue = true,
|
||||
会返回**新的 loginId / loginDateTime**,供下一局 UpsertUserAll 使用。
|
||||
|
||||
★ 登出必须用 **type=1(LogoutType.Logout)**。
|
||||
真机抓包实证 UserLogoutApi 就是 type=1(带 delayLog):
|
||||
{"userId":...,"type":1,"delayLog":{...}}
|
||||
续关场景下用 4(TestIn)会出问题 —— 那是「测试登入 / 顶号」的语义。
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
★ playlog 从哪来
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -75,7 +88,8 @@ reincarnate.py —— 账号数据迁移(转生):把账号 A 的数据克
|
||||
|
||||
# 常用开关
|
||||
--limit N 只迁前 N 首(试跑用)
|
||||
--per-packet N 每包曲目数(默认 4,★ 不要超过 4)
|
||||
--per-packet N 每包曲目数,只能是 1/3/4(默认 4;★ 2 首会被服务端拒绝)
|
||||
--wait N 每次上传前的等待秒数(默认 5.0,0 = 不等)
|
||||
--clone-identity 连 userName / accessCode 一起克隆(默认**保留 B 自己的**)
|
||||
--no-continue 不续关(只发一包,调试用)
|
||||
|
||||
@@ -87,6 +101,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
|
||||
from api import (
|
||||
setup_logging,
|
||||
@@ -120,6 +135,60 @@ from config import PLACE_ID, REGION_ID
|
||||
#: 真机一局最多 4 首
|
||||
MAX_TRACKS_PER_CREDIT = 4
|
||||
|
||||
#: ★ 被服务端禁止的每包曲目数(实测:**恰好 2 首会被拒**)
|
||||
#: 1 / 3 / 4 首都正常,只有 2 首不行 —— 所以分批时要避开它。
|
||||
FORBIDDEN_TRACK_COUNTS = (2,)
|
||||
|
||||
#: 允许的 --per-packet 取值
|
||||
ALLOWED_PER_PACKET = (1, 3, 4)
|
||||
|
||||
|
||||
def split_batches(total, per):
|
||||
"""
|
||||
把 total 首切成「每包 <= per」的批次,**并且不产生被禁止的 2 首包**。
|
||||
|
||||
做法:先贪心按 per 切,再修正大小为 2 的批次:
|
||||
|
||||
① 前一批 >= 4 -> 从它借 1 首过来(前一批变 >=3,本批变 3)
|
||||
② 借不到(前一批只有 3,或自己就是第一批)-> 把这个 2 拆成 1 + 1
|
||||
|
||||
★ 只借位是不够的 —— 前一批是 3 时借完自己变 2,等于把 2 挪了个位置。
|
||||
|
||||
例:
|
||||
10 首 per=4 -> [4, 4, 2] -> [4, 3, 3]
|
||||
6 首 per=4 -> [4, 2] -> [3, 3]
|
||||
2 首 per=4 -> [2] -> [1, 1]
|
||||
5 首 per=3 -> [3, 2] -> [3, 1, 1]
|
||||
8 首 per=3 -> [3, 3, 2] -> [3, 3, 1, 1]
|
||||
5 首 per=4 -> [4, 1] (本来就合法,不动)
|
||||
"""
|
||||
if total <= 0:
|
||||
return []
|
||||
per = max(1, int(per))
|
||||
out = []
|
||||
left = total
|
||||
while left > 0:
|
||||
n = min(per, left)
|
||||
out.append(n)
|
||||
left -= n
|
||||
|
||||
i = 0
|
||||
while i < len(out):
|
||||
if out[i] not in FORBIDDEN_TRACK_COUNTS:
|
||||
i += 1
|
||||
continue
|
||||
# ① 前一批 >= 4 才能借(借完前一批 >= 3,本批 = 3)
|
||||
if i > 0 and out[i - 1] >= 4:
|
||||
out[i - 1] -= 1
|
||||
out[i] += 1
|
||||
i += 1
|
||||
continue
|
||||
# ② 借不到 -> 把这个 2 拆成 1 + 1
|
||||
out[i] = 1
|
||||
out.insert(i + 1, 1)
|
||||
i += 2
|
||||
return out
|
||||
|
||||
#: 默认保留目标账号自己的身份字段(不克隆过去,避免把 B 改名)
|
||||
IDENTITY_KEYS = ("userName", "accessCode")
|
||||
|
||||
@@ -138,7 +207,8 @@ def parse_args(argv):
|
||||
opts = {"dst_qr": "", "src_qr": "", "src_json": "", "send": False,
|
||||
"plan": False, "debug": False, "limit": 0,
|
||||
"per_packet": MAX_TRACKS_PER_CREDIT,
|
||||
"clone_identity": False, "no_continue": False}
|
||||
"clone_identity": False, "no_continue": False,
|
||||
"wait": 5.0}
|
||||
i = 0
|
||||
while i < len(argv):
|
||||
a = argv[i]
|
||||
@@ -152,9 +222,12 @@ def parse_args(argv):
|
||||
opts["clone_identity"] = True
|
||||
elif a == "--no-continue":
|
||||
opts["no_continue"] = True
|
||||
elif a in ("--dst-qr", "--src-qr", "--src-json", "--limit", "--per-packet") and i + 1 < len(argv):
|
||||
elif a in ("--dst-qr", "--src-qr", "--src-json", "--limit",
|
||||
"--per-packet", "--wait") and i + 1 < len(argv):
|
||||
key = a[2:].replace("-", "_")
|
||||
opts[key] = int(argv[i + 1]) if key in ("limit", "per_packet") else argv[i + 1]
|
||||
opts[key] = (float(argv[i + 1]) if key == "wait"
|
||||
else int(argv[i + 1]) if key in ("limit", "per_packet")
|
||||
else argv[i + 1])
|
||||
i += 1
|
||||
i += 1
|
||||
return opts
|
||||
@@ -267,8 +340,9 @@ def main() -> int:
|
||||
import config as _cfg
|
||||
_cfg.LOG_LEVEL = "debug"
|
||||
|
||||
if opts["per_packet"] > MAX_TRACKS_PER_CREDIT:
|
||||
print("--per-packet 不能超过 " + str(MAX_TRACKS_PER_CREDIT) + "(真机一局最多 4 首)")
|
||||
if opts["per_packet"] not in ALLOWED_PER_PACKET:
|
||||
print("--per-packet 只能是 " + str(list(ALLOWED_PER_PACKET))
|
||||
+ " 之一(真机一局最多 4 首;★ 实测恰好 2 首会被服务端拒绝)")
|
||||
return 1
|
||||
|
||||
setup_logging("reincarnate")
|
||||
@@ -308,13 +382,20 @@ def main() -> int:
|
||||
|
||||
# ---------------- 3. 分批 ----------------
|
||||
per = int(opts["per_packet"])
|
||||
batches = [musics[i:i + per] for i in range(0, len(musics), per)]
|
||||
sizes = split_batches(len(musics), per)
|
||||
batches = []
|
||||
_pos = 0
|
||||
for _n in sizes:
|
||||
batches.append(musics[_pos:_pos + _n])
|
||||
_pos += _n
|
||||
log_info("计划:共 " + str(len(musics)) + " 首 -> " + str(len(batches))
|
||||
+ " 包(每包 " + str(per) + " 首)")
|
||||
+ " 包,每包 " + "/".join(str(n) for n in sizes[:12])
|
||||
+ ("..." if len(sizes) > 12 else "") + " 首")
|
||||
log_info(" ★ 已避开 2 首包(实测恰好 2 首会被服务端拒绝;1/3/4 首都正常)")
|
||||
|
||||
if opts["plan"]:
|
||||
for i, b in enumerate(batches):
|
||||
log_info(" 第 " + str(i + 1) + " 包:" + ", ".join(
|
||||
log_info(" 第 " + str(i + 1) + " 包(" + str(len(b)) + " 首):" + ", ".join(
|
||||
str(x.get("musicId")) + "/L" + str(x.get("level")) for x in b))
|
||||
log_info("--plan:只列计划,不发包")
|
||||
return 0
|
||||
@@ -325,12 +406,23 @@ def main() -> int:
|
||||
+ " 个将在第 1 包上传")
|
||||
|
||||
# ---------------- 4. 逐包上传 ----------------
|
||||
# ★ 流程(按使用方口径):
|
||||
# 登录 -> 等待 -> 上传 -> 登出(type=1) -> 续关登录 -> 等待 -> 上传 -> 登出 -> ...
|
||||
#
|
||||
# ★ 登出必须用 type=1(LogoutType.Logout)。
|
||||
# 真机抓包实证 UserLogoutApi 就是 type=1(带 delayLog);
|
||||
# 续关场景下用 4(TestIn)会出问题 —— 那是「测试登入/顶号」的语义。
|
||||
sent = 0
|
||||
for idx, batch in enumerate(batches):
|
||||
log_info("")
|
||||
log_info("========== 第 " + str(idx + 1) + "/" + str(len(batches))
|
||||
+ " 包(" + str(len(batch)) + " 首)==========")
|
||||
|
||||
# ---- 等待 ----
|
||||
if opts["wait"] > 0:
|
||||
log_info("上传前等待 " + str(opts["wait"]) + " 秒 ...")
|
||||
time.sleep(opts["wait"])
|
||||
|
||||
nodes = {}
|
||||
if idx == 0:
|
||||
nodes.update(json.loads(json.dumps(shared_nodes)))
|
||||
@@ -370,7 +462,17 @@ def main() -> int:
|
||||
)
|
||||
sent += 1
|
||||
|
||||
# ---- ★ 续关登录:拿下一局的 loginId ----
|
||||
# ---- ★ 登出(type=1 Logout)----
|
||||
log_info("--- 登出(type=1 Logout)---")
|
||||
try:
|
||||
UserLogout(uid, token, login.loginDateTime,
|
||||
logout_type=LogoutType.Logout,
|
||||
region_id=REGION_ID, place_id=PLACE_ID)
|
||||
except Exception as exc:
|
||||
log_warning("登出失败(继续下一包):" + str(exc))
|
||||
session["logged_in"] = False
|
||||
|
||||
# ---- ★ 续关登录:拿下一包的 loginId ----
|
||||
if idx < len(batches) - 1:
|
||||
if opts["no_continue"]:
|
||||
log_warning("--no-continue:不再续关,提前结束(只发了 1 包)")
|
||||
@@ -378,7 +480,7 @@ def main() -> int:
|
||||
log_info("--- 续关登录(isContinue=true)---")
|
||||
login = UserLogin(uid, token, region_id=REGION_ID, place_id=PLACE_ID,
|
||||
is_continue=True)
|
||||
session.update(login_dt=login.loginDateTime)
|
||||
session.update(login_dt=login.loginDateTime, logged_in=True)
|
||||
|
||||
if not opts["send"]:
|
||||
log_info("")
|
||||
|
||||
+27
@@ -1165,6 +1165,33 @@ check("会话链路 6 个函数都在",
|
||||
"UserLogin", "UserLogout")))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 19. reincarnate 分批器(★ 每包恰好 2 首会被服务端拒绝)
|
||||
# =============================================================================
|
||||
|
||||
section("19. reincarnate 分批器")
|
||||
|
||||
from reincarnate import split_batches as _split # noqa: E402
|
||||
|
||||
for _t, _p, _want in [(10, 4, [4, 3, 3]), (6, 4, [3, 3]), (2, 4, [1, 1]),
|
||||
(5, 3, [3, 1, 1]), (8, 3, [3, 3, 1, 1]), (5, 4, [4, 1]),
|
||||
(4, 4, [4]), (1, 4, [1]), (7, 4, [4, 3])]:
|
||||
_got = _split(_t, _p)
|
||||
check("分批 " + str(_t) + " 首 per=" + str(_p) + " -> " + str(_want),
|
||||
_got == _want, str(_got))
|
||||
|
||||
_bad = []
|
||||
for _t in range(1, 300):
|
||||
for _p in (1, 3, 4):
|
||||
_g = _split(_t, _p)
|
||||
if sum(_g) != _t or 2 in _g or any(x > _p or x < 1 for x in _g):
|
||||
_bad.append((_t, _p, _g))
|
||||
check("1..299 首 × per∈{1,3,4} 都不产生 2 首包", not _bad, str(_bad[:3]))
|
||||
|
||||
check("--per-packet 允许值 = (1, 3, 4)",
|
||||
__import__("reincarnate").ALLOWED_PER_PACKET == (1, 3, 4))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 汇总
|
||||
# =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user