Txiaochen 25ac522391 v0 plus附加功能
- 新增 unlockmusic.py:解锁(解禁)歌曲 + 解禁 Master / Re:Master 难度,整个流程复用 upsert.py 的 run()
- 买票前置闸门 api/ChargeGuard.py:必须先跑过 GetUserCharge,且同进程只允许买一次
- buy_ticket.py 票源改为 GetUserCharge,不再调用 GetGameCharge
- UpsertUserAll 与买票的冷却统一为 15 秒
- api/ 文件名统一 PascalCase(文件名 = 函数名)
- 项目改名 Astral,README 加 Logo、鸣谢与开发者说明
- 自检 284 项
2026-10-17 17:39:55 +08:00
2026-10-17 17:39:55 +08:00
2026-10-06 19:22:13 +08:00
2026-10-17 17:39:55 +08:00
2026-10-17 17:39:55 +08:00
2026-10-17 17:39:55 +08:00
2026-10-17 17:39:55 +08:00
2026-10-17 17:39:55 +08:00
2026-10-17 17:39:55 +08:00
2026-10-17 17:39:55 +08:00
2026-10-17 17:39:55 +08:00

Astral

Astral

舞萌DX 的游戏助手
宗旨:让舞萌DX的玩家能够更好地游玩游戏,获得快乐。

AI真的很好用你知道吗


这是什么

Astral 是一个用 Python 纯网络方式与舞萌DX(SDGB 国服)游戏服务器交互的工具集。

它不修改游戏、不注入进程,只是把游戏客户端做的事用脚本重做一遍: 扫码登录 → 取数据 → 上传成绩 / 解锁歌曲 → 登出。

协议实现全部来自反编译源码与真机网络日志的双向验证, 每个关键常量在代码注释里都标了源码行号。


当前状态:v0 plus 附加功能

脚本 状态
upsert.py ✅ 可用 —— 扫码登录 → 上传一局成绩 → 落库回查 → 登出
unlockmusic.py ✅ 可用 —— 解锁(解禁)歌曲 / 难度(复用 upsert.py 整套流程)
buy_ticket.py ✅ 可用 —— 购买功能票
use_ticket.py ✅ 可用 —— 消耗当前持有的功能票
reincarnate.py 🔧 已实现(未实跑)—— 账号数据迁移(转生)
netdelivery.py ✅ 可用 —— 查 / 下 .app / .opt 更新包(只读)
api/ 下 40+ 个接口模块 🔧 已实现并通过零网络自检,但未全部在真机上验证过

文档在 docs 分支,见文末文档索引。


一个教训:别把一件事分段交给 AI

v0 的开发过程中,我反复把同一个目标拆成小段丢给 AI,结果:

项目 Token 消耗
v0 有效开发 473M+
含中途废弃的分支项目 1137.5M

这 1137.5M 里有一大半是浪费掉的。

真正的教训是:

不要把一个东西分段交给 AI,而是直接设定一个目标,然后让它自己跑完。

分段交接会让 AI 反复丢失上下文、重复劳动、在已经解决的问题上打转 —— 而一个清晰的目标 + 足够长的执行窗口,反而更快也更省。


项目框架

Astral/
├── config.py            运行时可配置项(机台标识 / 歌曲数据 / 日志级别)★ 需自建
├── config.py.example    配置模板
├── icon/                Logo
├── upsert.py            ★ 主脚本:扫码登录 → 上传成绩 → 登出
├── unlockmusic.py       主脚本:解锁(解禁)歌曲(复用 upsert.py 的 run())
├── buy_ticket.py        主脚本:购买功能票
├── use_ticket.py        主脚本:消耗当前持有的功能票
├── reincarnate.py       主脚本:账号数据迁移(转生)
├── netdelivery.py       主脚本:查 / 下 .app / .opt 更新包
├── selftest.py          零网络自检(284 项,判据 = 源码 + 真机日志)
│
├── api/                 ★ 所有与服务器交互的模块(一个接口一个脚本)
│   ├── __init__.py      统一导出 + 日志自动跟踪
│   ├── MaimaiNet.py     网络核心(AES / deflate / URL 混淆 / Cookie)
│   ├── LogSetup.py      日志(INFO / DEBUG 双模式)
│   ├── ChargeGuard.py   买票前置闸门(必须先跑过 GetUserCharge)
│   ├── UpsertUserAll.py 上传成绩 / 解锁歌曲(36 节点 / 110 字段 / 差分)
│   ├── GetNeedUserData.py  结算快照编排 + 落盘 data/<userId>.json
│   └── ...                     40+ 个接口脚本
│
├── data/                每账号一份用户数据快照(<userId>.json)★ 不入库
├── download/            netdelivery.py 下载的 .app / .opt ★ 不入库
└── logs/                日志输出(latest.log + 日期.log)★ 不入库

设计约定:api/ 里每个脚本 = 一个接口,对外暴露一个能传参的函数。 主脚本只负责编排,日志和协议细节都在 api/ 内部。

命名约定:文件名 = 函数名,都按首字母大写的驼峰切分,一一对应:

服务端接口 脚本 函数
GetUserDataApi api/GetUserData.py GetUserData()
UpsertUserAllApi api/UpsertUserAll.py UpsertUserAll()
UserLoginApi api/UserLogin.py UserLogin()

服务端 API 名里带 Get 的,脚本名与函数名一律带 Get(不要模棱两可)。

分支说明

分支 内容
mai 主分支 —— 代码
docs 孤儿分支 —— 只有 docs/,开发文档与踩坑记录

两个分支没有共同的提交历史,各自独立。

🛠 给开发者:协议实现对照的反编译源码(Assembly-CSharp / AMDaemon.NET 等) 不入库,需要自己准备一份放在 source/;代码注释里的行号都指向它。 只是用工具的话完全不需要这个目录。


快速开始

1. 装依赖

pip install requests pycryptodome

2. ★ 先填 config.py

仓库里只有模板 config.py.example,先复制一份:

cp   config.py.example config.py      # Linux / macOS
copy config.py.example config.py      # Windows

然后必须填好 config.py 第二节「机台与店铺标识」:

变量 填什么
CLIENT_ID Keychip 短值(15 位 Keychip 去掉连字符取前 11 位),形如 A63E01ABCDE
PLACE_ID 你所在店铺 ID
REGION_ID 你所在区域 ID
PLACE_NAME 店铺名
REGION_NAME 区域名
KEYCHIP_ID 由上面两个值自动推导,不要手改

⚠️ 填错的话,发出去的请求带的就是别人的机台身份,服务端会直接拒绝。

config.py 已在 .gitignore 里 —— 它是你的私有配置,不要提交。

3. 跑自检

python selftest.py        # 零网络自检,应该 284 项全绿(不依赖你的 config)

主脚本一览

脚本 干什么 副作用 需要二维码
upsert.py 上传一局成绩 ★ 有 ✅
unlockmusic.py 解锁(解禁)歌曲 / 难度 ★ 有 ✅
buy_ticket.py 购买功能票 ★ 有 ✅
use_ticket.py 消耗当前持有的功能票 ★ 有 ✅
reincarnate.py 账号数据迁移(转生) ★ 有 ✅(目标号;源号可选)
netdelivery.py 查 / 下 .app / .opt 更新包 只读 ❌
selftest.py 零网络自检 无 ❌

所有有副作用的脚本默认都不发包:不传 --send 时只构造报文(dry-run)。 想看报文而不产生任何后果,先跑一次不带 --send 的。

二维码从机台的「扫码登录」界面获取,有效期 10 分钟(不是按次数计量的)。


upsert.py —— 上传一局成绩

# 预检:登录 + 抓快照 + 组包,不发包(零副作用,建议先跑这个)
python upsert.py "<二维码>"

# 真实上传一局成绩(★ 有副作用,不可撤销)
python upsert.py "<二维码>" --send

# 常用开关
python upsert.py "<二维码>" --send --debug        # 日志里带完整收发报文
python upsert.py "<二维码>" --send --cooldown 0   # 跳过发包前 15 秒冷却
python upsert.py "<二维码>" --send --no-verify    # 跳过上传后的落库回查
python unlockmusic.py "<二维码>" --send --music 11414   # 解锁歌曲 / 解禁难度

换歌 / 换成绩只改 config.py 的 PLAY_*,不用动任何代码。

额外塞节点:--extra-data

想手工覆盖某几个节点(比如本局拿到的新 KaleidxScope 门):

python upsert.py "<二维码>" --send --extra-data extra.json

键必须是 upsertUserAll 的 36 个节点之一,写错会报错并列出可用节点名。 JSON 在扫码之前就校验,写错直接退出,不浪费二维码。

⚠️ 在 PowerShell / CMD 里写内联 JSON 很容易被吞掉双引号,建议一律用文件。

★ isNew* 位串:0 / 1 / 2 的含义

每个数组节点都配一条 isNew* 位串,一位对应数组里的一条(顺序必须一致)。

依据 source/Assembly-CSharp/Net.VO.Mai2/VOExtensions.cs:309-364 的 BuildListData():

位 含义 服务端行为
0 存量更新 —— 旧数据里已有同主键的条目 UPDATE
1 新增 —— 旧数据里没有同主键的条目 INSERT
2 无变化 —— 只有 userFavoriteList 用,其他节点不会出现 跳过

主键见 api/UpsertUserAll.py 的 DIFF_PRIMARY_KEYS (如 userKaleidxScopeList 用 gateId、userMusicDetailList 用 musicId+level)。

⚠️ 对已存在的条目填 1 会 HTTP 500 —— 服务端会去 INSERT 一条重复记录。 真机抓包样本里 isNewFavoriteList = "22212",可以直观看到 0/1/2 混用的样子。

不确定填什么? 先看 data/<你的userId>.json 里对应的节点: 主键已存在 → 0,不存在 → 1。


unlockmusic.py —— 解锁(解禁)歌曲

python unlockmusic.py "<二维码>" --music 11414                 # 预检,不发包
python unlockmusic.py "<二维码>" --music 11414 --send          # ★ 真实解锁
python unlockmusic.py "<二维码>" --music 1085,11414 --send     # 多首,逗号分隔
python unlockmusic.py "<二维码>" --music 11414 --send --no-unlock-remaster

它没有自己的协议实现 —— 整个流程直接复用 upsert.py 的 run(): 扫码 → 预览 → 登录 → UpsertUserAll → 落库回查 → 登出,一模一样。 唯一多出来的东西是「要解锁哪些歌」这个入参。

解锁也不是单独的接口 —— 它是往 userItemList 里补几条 UserItem:

itemKind 解锁什么
5 歌曲本体(MusicUnlockList)
6 Master 难度(MusicUnlockMasterList)
7 Re:Master 难度(MusicUnlockReMasterList)

条目形如 {"itemKind":5,"itemId":1085,"stock":1,"isValid":true}。

  • 标准谱(musicId < 10000)的 Master / Re:Master 天生解锁,只写 kind5; 只有 DX 谱(>= 10000)才需要写 kind6 / kind7。
  • 服务器上已存在的条目会自动跳过 —— 重复插入会让服务端直接 HTTP 500。
  • Re:Master 还受活动开关约束,那是机台侧的事,脚本改不了;写了也可能进游戏仍是灰的。
参数 说明
--music ID[,ID...] 要解锁的 musicId,可重复、可逗号分隔(--unlock-music 也认)
--send 真正发包(不加就是预检,零副作用)
--no-unlock-master 不解禁 Master
--no-unlock-remaster 不解禁 Re:Master
--cooldown N / --no-verify / --debug 和 upsert.py 完全一样

依据反编译源码 VOExtensions.cs:981-1008(四个解锁列表并进 userItemList) 与 NotesListManager.cs:415/455(if (id < 10000) return true;)。 这部分没有真机抓包可对照(机台侧的解锁不上报),是按源码实现的。


buy_ticket.py —— 购买功能票

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
5 5倍チケット 4

默认 --charge-id 5。不同店铺上架的票不一样,买之前先 --list。

它做的安全检查

  1. 跑前查 GetUserChargeApi —— 账号已持有功能票时直接拒绝购买(重复购买有封号风险)
  2. 买票前 15 秒等待(贴近真机节奏)
  3. 跑后再查一次确认状态

票源只看 GetUserChargeApi(账号自己的票券表),不再看 GetGameChargeApi (那是机台本地的上架清单,跟账号状态无关)。

UpsertUserChargelog 内部还有两道硬闸门: 必须先跑过 GetUserCharge(api/ChargeGuard.py 记账), 以及同一账号同一进程只允许买一次票 —— 都拦在发包之前。


use_ticket.py —— 消耗当前持有的功能票

python use_ticket.py "<二维码>" --list                  # 只列当前持有的票
python use_ticket.py "<二维码>" --ticket-id 2 --send    # ★ 真实消耗

原理:GetUserChargeApi 与 UpsertUserAllApi 里的 userChargeList 格式完全相同, 所以把该节点里目标票的 stock 减 1,再让 playlog 带上 useTicketId / playCredit, 服务端就认为这张票被这一局用掉了。

典型用途:账号手里挂着票时 buy_ticket.py 会拒绝再买; 想再买一张就先用这个把它用掉。

详细原理与报文依据见 功能票使用说明。


reincarnate.py —— 账号转生

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

# 源数据来自二维码(会联网抓 A 的数据并落盘到 data/<A>.json)
python reincarnate.py --dst-qr "<B的二维码>" --src-qr "<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 …)
参数 默认 说明
--plan — 只列分批计划,不发包(建议先跑这个)
--limit N 0(全部) 只迁前 N 首(试跑用)
--per-packet N 4 每包曲目数,只能是 1 / 3 / 4
--wait N 5.0 每次上传前的等待秒数
--clone-identity 关 连 userName / accessCode 一起克隆(默认保留 B 自己的)

⚠️ 未实跑过。 建议先 --plan,再 --limit 8 小批量试。

完整流程、分批规则、续关与登出类型的依据见 账号转生说明。


netdelivery.py —— 查 / 下 .app / .opt 更新包

python netdelivery.py                 # 用 config.py 里的机台查,然后**多选下载**
python netdelivery.py A63E01ABCDE     # 指定 Keychip
python netdelivery.py --list          # 只列清单 + 直链,不下载
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

交互选择支持:逗号/空格分隔、区间(1-3)、a=全选、n=全不选、q=退出。 下载带断点续传与进度/速率显示,默认存到 download/(不入库)。

这是只读操作,不写任何账号数据。 需要同目录下的 authv1.py 提供 ALL.Net 协议层。


主脚本怎么写

核心原则:不要为每个 api 调用写日志。 日志已经在 api/ 里了。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
from api import (setup_logging, teardown_logging,
                 ParseQRCode, GetUserPreview, UserLogin, UserLogout, LogoutType,
                 UpsertUserAll)
from config import REGION_ID, PLACE_ID

def main() -> int:
    setup_logging("我的脚本")          # ← 只需要这一句
    session = {"user_id": 0, "token": "", "login_dt": 0, "logged_in": False}
    try:
        user_id, token = ParseQRCode(sys.argv[1])
        session.update(user_id=user_id, token=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)

        # ★ 一个函数跑完整条结算链路
        UpsertUserAll(user_id, login.loginId,
                      login_date_time=login.loginDateTime,
                      auth_date_time=login.dateTime,
                      token=token,
                      dry_run=False, confirm=True)
        return 0
    finally:
        if session["logged_in"]:                           # ★ 一定要保证登出
            UserLogout(session["user_id"], session["token"], session["login_dt"],
                       logout_type=LogoutType.Logout,
                       region_id=REGION_ID, place_id=PLACE_ID)
        teardown_logging()

if __name__ == "__main__":
    sys.exit(main())

三条铁律:

  1. 不要写日志 —— 每个 api 函数会自动打印 === 函数名 开始运行 === / [运行结果]
  2. 一定要登出 —— 账号卡在 isLogin=True 时下次登录会 returnCode=100
  3. 写接口要显式开闸门 —— 只写 dry_run=False 不够,必须同时给 confirm=True

完整指南见 主脚本编写指南。


已完成 / 将来

已完成(v0)

  • ✅ 扫码登录 / 登出 / 心跳,完整会话链路
  • ✅ 上传一局成绩(36 节点 / 110 字段 / 差分 / 落库回查)
  • ✅ 购买功能票
  • ✅ 消耗持有的功能票
  • ✅ 账号数据快照落盘(data/<userId>.json)
  • ✅ 账号数据迁移(转生)
  • ✅ 查 / 下 .app / .opt 更新包
  • ✅ 解锁歌曲 / 解禁 Master 与 Re:Master 难度
  • ✅ 零网络自检 284 项
  • ✅ MelonLoader 抓包 Mod(gamehook/)

将来可能会加

  • 网页控制 —— 用浏览器代替命令行操作
  • 更多游戏功能(任务 / 地图 / 收藏夹 / 排行榜……)
  • 更完整的真机验证覆盖
  • reincarnate.py 的实跑验证

文档索引

全部在 docs 分支:

文档 内容
项目总览 开发向主文档,该放哪 / 该怎么写
主脚本编写指南 写新主脚本的规范
上传成绩开发文档 UpsertUserAll 的 36 节点 / 110 字段
UpsertUserAll踩坑清单 28 条踩坑记录
功能票使用说明 买票 / 用票的完整原理与报文依据
账号转生说明 reincarnate.py 完整流程
netdelivery说明 DLI 协议与运行原理
常见坑与注意事项 二维码 / 账号状态 / 副作用 / 日志 / 网络
购买功能票流程 买票接口的完整排查过程
购买功能票_存档与交接 排查存档
真机Hook对照报告 抓包 Mod 对照
实跑验证记录 每次实跑的结果
舞萌DX_扫码登录协议技术规范 aimedb 扫码协议
日志与主脚本指南 日志系统说明

特别鸣谢

  • 志成🍥 —— 提供脱壳 DLL
  • WorkBuddy AI—— 提供 AI 模型
  • DeepSeek V4.1 Flash —— 主要代码编写
  • GPT-Image-2 —— Logo 设计
  • NVIDIA NIM、 AMD、 Kilo—— 提供 AI 模型
  • eaquira—— 思路启发
  • 神秘群U —— 私有科技的 Web 启发(暂无)
  • 香水所有群友 —— 和谐帮助

(还有什么以后补充)


免责声明

本项目仅供学习与协议研究使用。使用本工具产生的任何后果由使用者自行承担。 请遵守游戏服务条款,不要用于破坏游戏公平性。

S
Description
A assistance for 舞萌DX. Code by Deepseek V4.1 Flash ;)
Readme
1.6 MiB
Languages
Python 96.9%
C# 3.1%