From f2debaf9bfc5e95dfc3d7d55052c3d6b5f38d381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=B5=E5=AE=9D=E4=BB=99=E5=B0=8A?= Date: Wed, 4 Nov 2026 19:16:05 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=AF=E5=8A=A8=E5=99=A8=EF=BC=9A=E6=8E=A5?= =?UTF-8?q?=E5=85=A5=20Gitea=20Releases=20=E8=87=AA=E5=8A=A8=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E6=9B=B4=E6=96=B0=20+=20=E6=9B=B4=E6=96=B0=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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: 更新日志页改为动态容器,补充卡片样式 --- launcher/index.html | 2 +- launcher/server.py | 93 +++++++++++++++++++++++++++++++++++++++++++++ launcher/shell.css | 14 +++++++ launcher/shell.js | 75 +++++++++++++++++++++++++++++++++++- 4 files changed, 181 insertions(+), 3 deletions(-) diff --git a/launcher/index.html b/launcher/index.html index 3fe564f..116e574 100644 --- a/launcher/index.html +++ b/launcher/index.html @@ -202,7 +202,7 @@
-

更新日志

启动器与工具集成记录。

2.1.0 · 单窗口整合启动器、GM、一键换装、服务端工具
+

更新日志

从自托管仓库(Lingbao/90CNServer)的发布记录自动获取。

diff --git a/launcher/server.py b/launcher/server.py index 207471e..82b6679 100644 --- a/launcher/server.py +++ b/launcher/server.py @@ -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: diff --git a/launcher/shell.css b/launcher/shell.css index b0d0d95..15c7e68 100644 --- a/launcher/shell.css +++ b/launcher/shell.css @@ -126,6 +126,20 @@ button,input,select{font:inherit} .change-row b{font-weight:400;color:#bdd5c8;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .change-row em{font-style:normal;color:#5e7e6f;width:70px;text-align:right} +/* 更新日志(从 Gitea Releases 拉取) */ +#changelogBody{margin-top:14px;display:flex;flex-direction:column;gap:12px} +.changelog-item{border:1px solid #1b3529;background:#0b1712;border-radius:8px;padding:14px 16px} +.changelog-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:10px} +.changelog-tag{font-family:Consolas,monospace;font-size:12px;color:#4c9c76;background:#12241c;padding:2px 8px;border-radius:4px;border:1px solid #1e3a2e} +.changelog-name{font-size:13px;font-weight:600;color:#d1e6dc} +.changelog-time{font-size:11px;color:#607f70;margin-left:auto} +.changelog-body{font-size:12px;color:#8eada0;line-height:1.9} +.changelog-assets{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px} +.changelog-asset{font-size:11px;color:#5fc08d;text-decoration:none;border:1px solid #1e3a2e;padding:3px 10px;border-radius:4px;background:#0e1d17} +.changelog-asset:hover{background:#132a20} +.changelog-link{display:inline-block;margin-top:10px;font-size:11px;color:#5f8272;text-decoration:none} +.changelog-link:hover{color:#8fc9ae} + /* buttons/input */ .shell-btn{background:#11271e;border:1px solid #2b5542;color:#b8d8c8;padding:6px 12px;cursor:pointer;border-radius:2px} .shell-btn:hover{background:#19392b;color:#fff} diff --git a/launcher/shell.js b/launcher/shell.js index 238b758..5e0a066 100644 --- a/launcher/shell.js +++ b/launcher/shell.js @@ -25,6 +25,7 @@ if (page === 'companion') loadCompanion(); if (page === 'servertool') loadServerTool(); if (page === 'settings') loadSettings(); + if (page === 'changelog') loadChangelog(); if (page === 'gm') { // 进入 GM:GM 覆盖整个窗口(隐藏顶部导航/面包屑/底部状态栏),并铺满工作区 document.body.classList.add('gm-fullscreen'); @@ -165,12 +166,11 @@ $('statApiPort').textContent = location.port || '19527'; $('shellBottomPort').textContent = running ? '18111 / 7001' : '—'; $('statState').textContent = s.control_exists && s.client_dir_exists ? '已就绪' : '不完整'; - $('statVersion').textContent = 'v2026.11.03'; - $('statRemote').textContent = '当前版本'; $('statClient').textContent = s.client_dir_exists ? (running ? '运行中' : '已就绪') : '缺失'; $('shellClock').textContent = '检查于 ' + new Date().toLocaleTimeString('zh-CN', {hour12:false}); $('statResource').textContent = s.admin ? '管理员' : '普通权限'; loadAccountLabel(); + loadUpdateStatus(); } catch (e) { /* ignore */ } } @@ -189,6 +189,77 @@ } catch (e) { /* ignore */ } } + // 检查更新:填充「版本」卡片里的「本地 / 远程最新 / 状态」 + async function loadUpdateStatus() { + const remoteEl = $('statRemote'); + const versionEl = $('statVersion'); + try { + const r = await api('/api/update/check'); + if (versionEl) versionEl.textContent = r.current || '—'; + if (!remoteEl) return; + if (r.error) { + remoteEl.textContent = '检查失败'; + remoteEl.className = ''; + remoteEl.title = '无法连接更新服务器:' + r.error; + } else if (r.has_update && r.latest) { + remoteEl.textContent = r.latest; + remoteEl.className = 'green'; + remoteEl.title = '发现新版本,点击查看'; + remoteEl.style.cursor = 'pointer'; + remoteEl.onclick = () => { window.open(r.latest_url, '_blank'); }; + } else if (r.latest) { + remoteEl.textContent = r.latest; + remoteEl.className = 'green'; + remoteEl.title = '已是最新版本'; + } else { + remoteEl.textContent = '暂无发布'; + remoteEl.className = ''; + } + } catch (e) { + if (remoteEl) { remoteEl.textContent = '检查失败'; remoteEl.className = ''; } + } + } + + // 更新日志页面:拉取所有 Release 的 changelog + async function loadChangelog() { + const box = $('changelogBody'); + if (!box) return; + box.innerHTML = '
正在获取更新日志…
'; + try { + const r = await api('/api/update/changelog'); + if (!r.ok) { + box.innerHTML = '
获取失败:' + escapeHtml(r.error || '未知错误') + '
'; + return; + } + const rels = r.releases || []; + if (!rels.length) { + box.innerHTML = '
暂无发布记录。
在 Gitea 仓库「发布 Release」并填写更新说明后,这里会自动显示。
'; + return; + } + box.innerHTML = rels.map(rel => { + const badge = rel.prerelease ? '预发布' : ''; + const body = (rel.body || '').replace(/\r?\n/g, '
'); + const assets = (rel.assets || []).map(a => + `⬇ ${escapeHtml(a.name)} (${fmt(a.size)} B)` + ).join(''); + const when = rel.published_at ? rel.published_at.replace('T', ' ').slice(0, 16) : ''; + return `
+
+ ${escapeHtml(rel.tag || rel.name || '')} + ${escapeHtml(rel.name || '')} + ${badge} + ${escapeHtml(when)} +
+
${body}
+ ${assets ? '
' + assets + '
' : ''} + 在浏览器中查看 ↗ +
`; + }).join(''); + } catch (e) { + box.innerHTML = '
获取失败:' + escapeHtml(e && e.message ? e.message : e) + '
'; + } + } + $('shellAccountSelect').addEventListener('change', async () => { const username = $('shellAccountSelect').value; try {