feat: 更新源改到自建 Gitea

- /api/update-check 走 git.fragrance.moe 的 Release API(公开仓库,无需 token)
- 设置页加「检查更新 / 下载安装」,下载走 assets 的 browser_download_url
- asset.rev 由打包脚本生成,覆盖安装必定重建资源
This commit is contained in:
shouko committed 2026-10-28 22:34:14 +08:00
1 parent 8421898626
commit 4223b0cd19
3 files changed
+88 -21

No files matched your search

+6 -6
View File
@@ -11,17 +11,17 @@ MaimaiNet.py —— 舞萌DX 网络核心层(纯 HTTP 复刻)
1) 服务器基址与 API 路径 1) 服务器基址与 API 路径
source/Assembly-CSharp/Manager/OperationManager.cs:225,338 source/Assembly-CSharp/Manager/OperationManager.cs:225,338
ServerUri = Auth.GameServerUri + MovieServerUri("<REDACTED>/") ServerUri = Auth.GameServerUri + MovieServerUri("<OBFUSCATE_PARAM>/")
source/Assembly-CSharp/Net.Packet/Packet.cs:116 source/Assembly-CSharp/Net.Packet/Packet.cs:116
Client = NetHttpClient.Create(BaseUrl + Obfuscator(query.Api)) Client = NetHttpClient.Create(BaseUrl + Obfuscator(query.Api))
source/Assembly-CSharp/Net.Packet/Packet.cs:485 source/Assembly-CSharp/Net.Packet/Packet.cs:485
Obfuscator(s) = md5_hex_lower(s + "<REDACTED>") Obfuscator(s) = md5_hex_lower(s + "<OBFUSCATE_PARAM>")
source/Assembly-CSharp/Net.VO/NetQuery.cs:14 source/Assembly-CSharp/Net.VO/NetQuery.cs:14
Api = api + "MaimaiChn" Api = api + "MaimaiChn"
=> 最终 URL = BASE_URL + md5(逻辑接口名 + "MaimaiChn" + "<REDACTED>") => 最终 URL = BASE_URL + md5(逻辑接口名 + "MaimaiChn" + "<OBFUSCATE_PARAM>")
已用真实日志验证:md5("GetGameSettingApiMaimaiChn" + "<REDACTED>") 已用真实日志验证:md5("GetGameSettingApiMaimaiChn" + "<OBFUSCATE_PARAM>")
= 83a5d5a20b062c5c2ad817460b8f7f76 = 83a5d5a20b062c5c2ad817460b8f7f76
(source/2026-10-05.log:1 的请求就是打到这个尾段) (source/2026-10-05.log:1 的请求就是打到这个尾段)
@@ -121,7 +121,7 @@ from config import CLIENT_ID, MAI_ENCODING, TIMEOUT_LONG_MS, TIMEOUT_MS
#: 游戏服务器(Maimai2Servlet) #: 游戏服务器(Maimai2Servlet)
#: 依据 source/Assembly-CSharp/Manager/OperationManager.cs:225,338 #: 依据 source/Assembly-CSharp/Manager/OperationManager.cs:225,338
#: _operationData.ServerUri = Auth.GameServerUri + MovieServerUri("<REDACTED>/") #: _operationData.ServerUri = Auth.GameServerUri + MovieServerUri("<OBFUSCATE_PARAM>/")
BASE_URL = "" BASE_URL = ""
#: AES 参数,依据 source/Assembly-CSharp/Net/CipherAES.cs:11-12 #: AES 参数,依据 source/Assembly-CSharp/Net/CipherAES.cs:11-12
@@ -162,7 +162,7 @@ def obfuscate_api_name(api_name: str) -> str:
对逻辑接口名做 MD5 混淆,得到 URL 尾段(对应 Packet.Obfuscator)。 对逻辑接口名做 MD5 混淆,得到 URL 尾段(对应 Packet.Obfuscator)。
逻辑名 "GetUserPreviewApi" -> "GetUserPreviewApiMaimaiChn" 逻辑名 "GetUserPreviewApi" -> "GetUserPreviewApiMaimaiChn"
-> md5(上面 + "<REDACTED>") -> md5(上面 + "<OBFUSCATE_PARAM>")
参数: 参数:
api_name: 逻辑接口名,**不要**自己带 MaimaiChn 后缀 api_name: 逻辑接口名,**不要**自己带 MaimaiChn 后缀
+51 -15
View File
@@ -912,36 +912,72 @@ def api_update_gh():
repo=repo) repo=repo)
UPDATE_SITE = "https://git.fragrance.moe"
UPDATE_REPO = "shouko/maipulse"
@app.get("/api/update-check") @app.get("/api/update-check")
@guarded
def api_update_check(): def api_update_check():
"""拉远端 version.json 与本地比较。远端格式: """检查更新。默认走自建 Gitea 的 Release(公开仓库,不用 token)。
{"version": "1.0.1", "url": "下载地址", "notes": "更新说明", "published": "..."}
asset 用 browser_download_url —— 那是 /releases/download/<tag>/<name> 的稳定地址,
比 attachments/uuid 可靠。给了 source 参数则按老的远端 version.json 走。
""" """
import urllib.request import urllib.request
src = (request.args.get("source") or "").strip() src = (request.args.get("source") or "").strip()
if not src:
return fail("还没填更新源地址") def _v(text):
nums = [int(x) for x in re.findall(r"\d+", str(text))[:3]]
while len(nums) < 3:
nums.append(0)
return tuple(nums)
if src:
if not re.match(r"^https?://", src): if not re.match(r"^https?://", src):
return fail("更新源必须是 http(s) 地址") return fail("更新源必须是 http(s) 地址")
try: try:
req = urllib.request.Request(src, headers={"User-Agent": "maipulse/" + APP_VERSION}) req = urllib.request.Request(src, headers={"User-Agent": "maipulse/" + APP_VERSION})
raw = urllib.request.urlopen(req, timeout=20).read().decode("utf-8") remote = json.loads(urllib.request.urlopen(req, timeout=20).read().decode("utf-8"))
remote = json.loads(raw)
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
return fail("拉取更新源失败:%s" % exc) return fail("拉取更新源失败:%s" % exc)
ver = str(remote.get("version") or "") ver = str(remote.get("version") or "")
if not ver: if not ver:
return fail("更新源里没有 version 字段") return fail("更新源里没有 version 字段")
return ok(current=APP_VERSION, remote=ver, newer=_v(ver) > _v(APP_VERSION),
def _v(text):
return tuple(int(x) for x in re.findall(r"\d+", text)[:3])
newer = _v(ver) > _v(APP_VERSION)
return ok(current=APP_VERSION, remote=ver, newer=newer,
url=remote.get("url"), notes=remote.get("notes"), url=remote.get("url"), notes=remote.get("notes"),
published=remote.get("published")) published=remote.get("published"), repo="(自填)")
api = "%s/api/v1/repos/%s/releases/latest" % (UPDATE_SITE, UPDATE_REPO)
try:
req = urllib.request.Request(api, headers={"User-Agent": "maipulse/" + APP_VERSION})
rel = json.loads(urllib.request.urlopen(req, timeout=20).read().decode("utf-8"))
except urllib.error.HTTPError as exc:
return fail("Release 接口 HTTP %s(仓库或 Release 不存在)" % exc.code)
except Exception as exc: # noqa: BLE001
return fail("连不上更新仓库:%s" % exc)
tag = str(rel.get("tag_name") or "").strip()
if not tag:
return fail("Release 没有 tag")
ver = re.sub(r"^[vV]", "", tag)
assets = rel.get("assets") or []
apk = next((a for a in assets if str(a.get("name", "")).lower().endswith(".apk")), None)
if apk:
url = apk.get("browser_download_url") or ""
size = apk.get("size") or 0
name = apk.get("name") or "maipulse.apk"
else:
url = "%s/%s/releases/download/%s/maipulse.apk" % (UPDATE_SITE, UPDATE_REPO, tag)
size = 0
name = "maipulse.apk"
return ok(current=APP_VERSION, remote=ver, newer=_v(ver) > _v(APP_VERSION),
url=url, name=name, size=size, tag=tag,
notes=(rel.get("body") or "")[:500],
published=rel.get("published_at"),
page="%s/%s/releases/tag/%s" % (UPDATE_SITE, UPDATE_REPO, tag),
repo="%s/%s" % (UPDATE_REPO.split("/")[0], UPDATE_REPO.split("/")[-1]))
# ---------------------------------------------------------------- 配置 # ---------------------------------------------------------------- 配置
+31
View File
@@ -657,6 +657,12 @@
<button class="ghost" style="margin:0" onclick="updateMaster()">更新曲库</button> <button class="ghost" style="margin:0" onclick="updateMaster()">更新曲库</button>
<button class="ghost" style="margin:0;flex:0 0 auto" onclick="loadVersion()">刷新</button> <button class="ghost" style="margin:0;flex:0 0 auto" onclick="loadVersion()">刷新</button>
</div> </div>
<div class="kv"><b>最新版本</b><span id="verRemote">—</span></div>
<div class="row">
<button class="ghost" style="margin:0" onclick="checkUpdate()">检查更新</button>
<button class="ghost" style="margin:0;flex:0 0 auto;display:none" id="verDl" onclick="downloadUpdate()">下载安装</button>
</div>
<p class="desc" id="verNote" style="display:none"></p>
</section> </section>
@@ -1284,6 +1290,31 @@ function importConf(file){
/* ---------- 更新 ---------- *//* ---------- 更新 ---------- */ /* ---------- 更新 ---------- *//* ---------- 更新 ---------- */
const UP_SRC_KEY = 'maipulse.updatesrc'; const UP_SRC_KEY = 'maipulse.updatesrc';
var _upd = null;
async function checkUpdate(){
var box = document.getElementById('verRemote');
box.textContent = '检查中…';
try{
var r = await (await fetch('/api/update-check')).json();
if(!r.ok){ box.textContent = '检查失败'; log('检查更新:' + r.error, 1); return; }
_upd = r;
box.textContent = r.remote + (r.newer ? '(有新版)' : '(已是最新)');
var note = document.getElementById('verNote');
if(r.notes){ note.style.display = ''; note.textContent = r.notes; }
document.getElementById('verDl').style.display = r.newer ? '' : 'none';
}catch(e){ box.textContent = '检查失败'; log('检查更新:' + e, 1); }
}
function downloadUpdate(){
if(!_upd || !_upd.url){ log('没有可下载的地址', 1); return; }
// 安卓壳里有原生下载器,没有就新开标签
if(window.Maipulse && window.Maipulse.installApk){
window.Maipulse.installApk(_upd.url);
}else{
window.open(_upd.url, '_blank');
}
}
async function loadVersion(){ async function loadVersion(){
const r = await get('/api/version'); const r = await get('/api/version');
if(!r.ok) return; if(!r.ok) return;