启动器:接入 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:
1 parent
d31a34e9aa
commit
f2debaf9bf
4 files changed
+181
-3
No files matched your search
+1
-1
@@ -202,7 +202,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="shell-page" id="page-changelog">
|
<section class="shell-page" id="page-changelog">
|
||||||
<div class="page-pad"><h2>更新日志</h2><p class="muted">启动器与工具集成记录。</p><div class="empty-box">2.1.0 · 单窗口整合启动器、GM、一键换装、服务端工具</div></div>
|
<div class="page-pad"><h2>更新日志</h2><p class="muted">从自托管仓库(Lingbao/90CNServer)的发布记录自动获取。</p><div id="changelogBody"></div></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="shell-page" id="page-gm">
|
<section class="shell-page" id="page-gm">
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import uuid
|
|||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
||||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
from urllib.parse import urlparse, parse_qs
|
from urllib.parse import urlparse, parse_qs
|
||||||
|
|
||||||
from db import DNFDatabase, set_gm_config
|
from db import DNFDatabase, set_gm_config
|
||||||
@@ -40,6 +41,11 @@ DETACHED_PROCESS = 0x00000008
|
|||||||
|
|
||||||
DEFAULT_PORT = 19527 # 启动器本地端口
|
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():
|
def base_dir():
|
||||||
"""程序所在目录(源码运行时 = 脚本目录,冻结运行时 = exe 目录)。"""
|
"""程序所在目录(源码运行时 = 脚本目录,冻结运行时 = exe 目录)。"""
|
||||||
@@ -274,6 +280,51 @@ def log(msg):
|
|||||||
_logq.put(str(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):
|
def flow_start_game(account_username=None):
|
||||||
"""一键开始游戏;account_username 为空时使用默认账号。"""
|
"""一键开始游戏;account_username 为空时使用默认账号。"""
|
||||||
log("== 开始游戏 ==")
|
log("== 开始游戏 ==")
|
||||||
@@ -515,6 +566,10 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self._api_logs()
|
self._api_logs()
|
||||||
elif path == "/api/gm/health":
|
elif path == "/api/gm/health":
|
||||||
self._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":
|
elif path == "/api/accounts-config":
|
||||||
self._api_accounts_config()
|
self._api_accounts_config()
|
||||||
# GM 只读接口
|
# GM 只读接口
|
||||||
@@ -685,6 +740,44 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
"native_available": NATIVE is not None,
|
"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):
|
def _api_logs(self):
|
||||||
logs = []
|
logs = []
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
@@ -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 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}
|
.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 */
|
/* buttons/input */
|
||||||
.shell-btn{background:#11271e;border:1px solid #2b5542;color:#b8d8c8;padding:6px 12px;cursor:pointer;border-radius:2px}
|
.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}
|
.shell-btn:hover{background:#19392b;color:#fff}
|
||||||
|
|||||||
+73
-2
@@ -25,6 +25,7 @@
|
|||||||
if (page === 'companion') loadCompanion();
|
if (page === 'companion') loadCompanion();
|
||||||
if (page === 'servertool') loadServerTool();
|
if (page === 'servertool') loadServerTool();
|
||||||
if (page === 'settings') loadSettings();
|
if (page === 'settings') loadSettings();
|
||||||
|
if (page === 'changelog') loadChangelog();
|
||||||
if (page === 'gm') {
|
if (page === 'gm') {
|
||||||
// 进入 GM:GM 覆盖整个窗口(隐藏顶部导航/面包屑/底部状态栏),并铺满工作区
|
// 进入 GM:GM 覆盖整个窗口(隐藏顶部导航/面包屑/底部状态栏),并铺满工作区
|
||||||
document.body.classList.add('gm-fullscreen');
|
document.body.classList.add('gm-fullscreen');
|
||||||
@@ -165,12 +166,11 @@
|
|||||||
$('statApiPort').textContent = location.port || '19527';
|
$('statApiPort').textContent = location.port || '19527';
|
||||||
$('shellBottomPort').textContent = running ? '18111 / 7001' : '—';
|
$('shellBottomPort').textContent = running ? '18111 / 7001' : '—';
|
||||||
$('statState').textContent = s.control_exists && s.client_dir_exists ? '已就绪' : '不完整';
|
$('statState').textContent = s.control_exists && s.client_dir_exists ? '已就绪' : '不完整';
|
||||||
$('statVersion').textContent = 'v2026.11.03';
|
|
||||||
$('statRemote').textContent = '当前版本';
|
|
||||||
$('statClient').textContent = s.client_dir_exists ? (running ? '运行中' : '已就绪') : '缺失';
|
$('statClient').textContent = s.client_dir_exists ? (running ? '运行中' : '已就绪') : '缺失';
|
||||||
$('shellClock').textContent = '检查于 ' + new Date().toLocaleTimeString('zh-CN', {hour12:false});
|
$('shellClock').textContent = '检查于 ' + new Date().toLocaleTimeString('zh-CN', {hour12:false});
|
||||||
$('statResource').textContent = s.admin ? '管理员' : '普通权限';
|
$('statResource').textContent = s.admin ? '管理员' : '普通权限';
|
||||||
loadAccountLabel();
|
loadAccountLabel();
|
||||||
|
loadUpdateStatus();
|
||||||
} catch (e) { /* ignore */ }
|
} catch (e) { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,6 +189,77 @@
|
|||||||
} catch (e) { /* ignore */ }
|
} 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 = '<div class="empty-box">正在获取更新日志…</div>';
|
||||||
|
try {
|
||||||
|
const r = await api('/api/update/changelog');
|
||||||
|
if (!r.ok) {
|
||||||
|
box.innerHTML = '<div class="empty-box">获取失败:' + escapeHtml(r.error || '未知错误') + '</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rels = r.releases || [];
|
||||||
|
if (!rels.length) {
|
||||||
|
box.innerHTML = '<div class="empty-box">暂无发布记录。<br>在 Gitea 仓库「发布 Release」并填写更新说明后,这里会自动显示。</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
box.innerHTML = rels.map(rel => {
|
||||||
|
const badge = rel.prerelease ? '<span class="gm-tag gm-tag-gray">预发布</span>' : '';
|
||||||
|
const body = (rel.body || '').replace(/\r?\n/g, '<br>');
|
||||||
|
const assets = (rel.assets || []).map(a =>
|
||||||
|
`<a class="changelog-asset" href="${escapeHtml(a.download_url)}" target="_blank">⬇ ${escapeHtml(a.name)} (${fmt(a.size)} B)</a>`
|
||||||
|
).join('');
|
||||||
|
const when = rel.published_at ? rel.published_at.replace('T', ' ').slice(0, 16) : '';
|
||||||
|
return `<div class="changelog-item">
|
||||||
|
<div class="changelog-head">
|
||||||
|
<span class="changelog-tag">${escapeHtml(rel.tag || rel.name || '')}</span>
|
||||||
|
<span class="changelog-name">${escapeHtml(rel.name || '')}</span>
|
||||||
|
${badge}
|
||||||
|
<span class="changelog-time">${escapeHtml(when)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="changelog-body">${body}</div>
|
||||||
|
${assets ? '<div class="changelog-assets">' + assets + '</div>' : ''}
|
||||||
|
<a class="changelog-link" href="${escapeHtml(rel.html_url)}" target="_blank">在浏览器中查看 ↗</a>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
} catch (e) {
|
||||||
|
box.innerHTML = '<div class="empty-box">获取失败:' + escapeHtml(e && e.message ? e.message : e) + '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$('shellAccountSelect').addEventListener('change', async () => {
|
$('shellAccountSelect').addEventListener('change', async () => {
|
||||||
const username = $('shellAccountSelect').value;
|
const username = $('shellAccountSelect').value;
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in new issue
Block a user