启动器:接入 Gitea Releases 自动检查更新 + 更新日志页

- server.py: 新增 /api/update/check、/api/update/changelog 接口,
  用标准库 urllib 请求自托管仓库 Lingbao/90CNServer 的 Releases,
  带 5 分钟缓存与 8 秒超时;本地版本号写死 v2026.11.04
- shell.js: 版本卡片改为动态读取远程最新 tag,发现新版本绿色可点击
  跳转;新增 loadChangelog 渲染发布记录
- index.html/shell.css: 更新日志页改为动态容器,补充卡片样式
This commit is contained in:
hanfuit committed 2026-11-04 19:16:05 +08:00
1 parent d31a34e9aa
commit f2debaf9bf
4 files changed
+181 -3

No files matched your search

+93
View File
@@ -26,6 +26,7 @@ import uuid
import webbrowser
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.request import Request, urlopen
from urllib.parse import urlparse, parse_qs
from db import DNFDatabase, set_gm_config
@@ -40,6 +41,11 @@ DETACHED_PROCESS = 0x00000008
DEFAULT_PORT = 19527 # 启动器本地端口
# 更新检查配置:Gitea 自托管仓库的 Releases 地址
UPDATE_REPO_API = "https://git.hanfuit.com/api/v1/repos/Lingbao/90CNServer/releases"
UPDATE_REPO_WEB = "https://git.hanfuit.com/Lingbao/90CNServer/releases"
CURRENT_VERSION = "v2026.11.04" # 本地当前版本号
def base_dir():
"""程序所在目录(源码运行时 = 脚本目录,冻结运行时 = exe 目录)。"""
@@ -274,6 +280,51 @@ def log(msg):
_logq.put(str(msg))
# ------------------------------------------------------------------ 更新检查
_update_cache = {"ts": 0.0, "releases": None, "error": None}
_update_lock = threading.Lock()
def _fetch_releases(force=False):
"""拉取 Gitea Releases 列表(带 5 分钟缓存)。"""
now = time.time()
with _update_lock:
if not force and _update_cache["ts"] and now - _update_cache["ts"] < 300:
return _update_cache["releases"], _update_cache["error"]
try:
req = Request(UPDATE_REPO_API, headers={
"User-Agent": "DNF90Launcher/2.0",
"Accept": "application/json",
})
with urlopen(req, timeout=8) as resp:
data = json.loads(resp.read().decode("utf-8"))
releases = data if isinstance(data, list) else []
with _update_lock:
_update_cache.update(ts=now, releases=releases, error=None)
return releases, None
except Exception as e:
with _update_lock:
_update_cache.update(ts=now, releases=None, error=str(e))
return None, str(e)
def _release_summary(rel):
"""把一个 release 对象规整成前端需要的字段。"""
return {
"tag": rel.get("tag_name") or rel.get("name") or "",
"name": rel.get("name") or rel.get("tag_name") or "",
"body": rel.get("body") or "",
"published_at": rel.get("published_at") or "",
"prerelease": bool(rel.get("prerelease")),
"html_url": rel.get("html_url") or UPDATE_REPO_WEB,
"assets": [
{"name": a.get("name"), "size": a.get("size"),
"download_url": a.get("browser_download_url")}
for a in (rel.get("assets") or [])
],
}
def flow_start_game(account_username=None):
"""一键开始游戏;account_username 为空时使用默认账号。"""
log("== 开始游戏 ==")
@@ -515,6 +566,10 @@ class Handler(BaseHTTPRequestHandler):
self._api_logs()
elif path == "/api/gm/health":
self._api_gm_health()
elif path == "/api/update/check":
self._api_update_check()
elif path == "/api/update/changelog":
self._api_update_changelog(qs)
elif path == "/api/accounts-config":
self._api_accounts_config()
# GM 只读接口
@@ -685,6 +740,44 @@ class Handler(BaseHTTPRequestHandler):
"native_available": NATIVE is not None,
})
def _api_update_check(self):
"""检查最新发布版本(对比本地 CURRENT_VERSION)。"""
releases, err = _fetch_releases()
if err:
self._json({
"ok": True,
"current": CURRENT_VERSION,
"latest": None,
"has_update": False,
"error": err,
})
return
latest = releases[0] if releases else None
latest_tag = (latest.get("tag_name") or "").lstrip("v") if latest else ""
current = CURRENT_VERSION.lstrip("v")
self._json({
"ok": True,
"current": CURRENT_VERSION,
"latest": latest.get("tag_name") if latest else None,
"has_update": bool(latest_tag) and latest_tag != current,
"latest_url": latest.get("html_url") if latest else UPDATE_REPO_WEB,
"published_at": latest.get("published_at") if latest else None,
})
def _api_update_changelog(self, qs):
"""获取所有发布的更新日志。"""
force = qs.get("force", ["0"])[0] == "1"
releases, err = _fetch_releases(force=force)
if err:
self._json({"ok": False, "error": err, "releases": []})
return
self._json({
"ok": True,
"current": CURRENT_VERSION,
"releases": [_release_summary(r) for r in (releases or [])],
"web_url": UPDATE_REPO_WEB,
})
def _api_logs(self):
logs = []
while True: