1.7.1:修白屏 + 配对 + 官网管理页分家

手表端
- 修「连上后一片白屏」:initTabs() 从来没被调用过,
  导致 tabDefs 为 null、selectTab 直接 return,
  标签栏里只剩一个孤立的滑动指示器
- 应用名改成 ykxmz

被控端
- 修「设备已关闭新设备配对」:配对码本身就是密钥,
  拿着正确的码就该放行。老逻辑"首台配对后自动关窗"会把
  手表重装的人永久挡在外面
- 应用名改成 ykxmz

服务器端
- / 改成官网(公开,简洁介绍 + 下载)
- /zhingzhuanzhan 中转站管理(要登录),/admin 自动跳转
- 管理账号 xmz7891,可改
- 官网 / 登录页 / 管理页都加上 favicon
This commit is contained in:
xmz7891
2026-10-06 23:08:07 +08:00
parent bc7a022c6e
commit 81e923a0aa
24 changed files with 1829 additions and 23 deletions
+144 -11
View File
@@ -53,6 +53,7 @@ import sys
import threading
import time
import urllib.parse
from urllib.parse import quote
import urllib.request
from collections import deque
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
@@ -118,7 +119,11 @@ def admin_key() -> str:
# 管理页密码。用户指定的默认值,可以在 data/admin_password.txt 里改。
ADMIN_PASSWORD_DEFAULT = "120830"
ADMIN_PASSWORD_DEFAULT = "@REVAedu120830"
# 管理页账号(原来只有密码,用户要求加上账号)
ADMIN_USER_DEFAULT = "xmz7891"
ADMIN_USER = (os.environ.get("RELAY_ADMIN_USER") or
ADMIN_USER_DEFAULT)
ADMIN_PASSWORD = os.environ.get("RELAY_ADMIN_PASSWORD", "")
# 登录会话:cookie 令牌 -> 过期时间
# 成品下载目录(管理页上给人下载 APK / 整包用)。
@@ -194,8 +199,11 @@ def admin_password() -> str:
return ADMIN_PASSWORD
def admin_login(password: str) -> str:
def admin_login(username: str, password: str) -> str:
"""验密码,对了就发一个会话令牌。"""
# 账号也要对(用户要求用账号+密码登录)
if username and username.strip() != ADMIN_USER:
return ""
if not password or not secrets.compare_digest(password, admin_password()):
return ""
token = secrets.token_urlsafe(24)
@@ -773,6 +781,111 @@ def _query(url: str, name: str) -> str:
return ""
def _site_page() -> str:
"""官网首页:简洁介绍 + 下载。**不需要登录**。
用户要求把 relay.xxmxxs.top 当官网用,
管理后台挪到 ADMIN_PATH(见 _login_page / _admin_page)。
"""
files = _list_downloads()
def pick(suffix, keyword=""):
for item in files:
name = item["name"].lower()
if name.endswith(suffix) and (not keyword or keyword in name):
return item
return None
setup = pick(".exe")
server = pick(".zip", "server")
watch = pick(".apk")
def card(title, desc, item, tag):
if not item:
return ('<div class=card><div class=t>' + title + '</div>'
'<div class=d>' + desc + '</div>'
'<div class=soon>暂未上传</div></div>')
mb = item["size"] / 1024 / 1024
size = ("%.1f MB" % mb) if mb >= 0.1 else ("%.0f KB" % (item["size"] / 1024))
href = "/download/" + quote(item["name"])
return ('<div class=card><div class=t>' + title + '</div>'
'<div class=d>' + desc + '</div>'
'<a class=btn href="' + href + '">下载 ' + tag + '</a>'
'<div class=s>' + item["name"] + ' · ' + size + '</div></div>')
ver = ""
for item in files:
found = re.search(r"(\d+\.\d+\.\d+)", item["name"])
if found:
ver = found.group(1)
break
cards = (card("电脑端", "教室那台 Windows 电脑装这个", setup, "安装包")
+ card("服务器端", "要跨网络才需要,装在一台小机器上", server, "压缩包")
+ card("手表端", "安卓手表装这个 APK", watch, "APK"))
badge = ("最新版 " + ver) if ver else ""
return """<!DOCTYPE html>
<html lang=zh-CN><head>
<meta charset=utf-8>
<meta name=viewport content="width=device-width,initial-scale=1">
<title>ykxmz · 教室电脑手表遥控</title>
<link rel="icon" href="/favicon.ico">
<style>
*{box-sizing:border-box}
body{margin:0;font-family:-apple-system,"Segoe UI","Microsoft YaHei",sans-serif;
background:#0d1117;color:#e6edf3;line-height:1.7}
.wrap{max-width:780px;margin:0 auto;padding:56px 20px 80px}
h1{font-size:34px;margin:0 0 10px;letter-spacing:-.5px}
.sub{color:#8b949e;font-size:17px;margin-bottom:10px}
.ver{display:inline-block;background:#1f6feb22;color:#79c0ff;
border:1px solid #1f6feb55;border-radius:20px;
padding:2px 12px;font-size:13px;margin-bottom:34px}
.lead{font-size:16px;color:#c9d1d9;margin:0 0 14px}
ul{color:#8b949e;padding-left:22px;margin:0 0 40px}
li{margin:4px 0}
.cards{display:grid;gap:14px;grid-template-columns:1fr}
@media(min-width:640px){.cards{grid-template-columns:1fr 1fr 1fr}}
.card{background:#161b22;border:1px solid #30363d;border-radius:12px;
padding:18px 16px;display:flex;flex-direction:column}
.card .t{font-weight:600;font-size:15px;margin-bottom:6px}
.card .d{color:#8b949e;font-size:13px;flex:1;margin-bottom:14px}
.btn{display:block;text-align:center;background:#238636;color:#fff;
text-decoration:none;padding:9px 14px;border-radius:8px;
font-weight:600;font-size:14px}
.btn:hover{background:#2ea043}
.soon{color:#6e7681;font-size:13px;text-align:center;padding:9px 0}
.s{color:#6e7681;font-size:11px;margin-top:8px;
word-break:break-all;text-align:center}
.foot{margin-top:46px;padding-top:20px;border-top:1px solid #21262d;
color:#6e7681;font-size:13px}
.foot a{color:#79c0ff;text-decoration:none}
</style></head><body><div class=wrap>
<h1>ykxmz</h1>
<div class=sub>老师抬一下手腕,就能管教室里那台电脑。</div>
<div class=ver>""" + badge + """</div>
<p class=lead>放课件、放音频、投图到大屏、看看谁在打游戏、下课关机 ——
不用走回讲台去点鼠标。</p>
<ul>
<li>手表和电脑在同一个 WiFi:直连,几十毫秒</li>
<li>不在同一个网络:自动走中转,办公室也能管教室</li>
<li>屏幕被看时电脑上一定有提示条,关不掉</li>
</ul>
<div class=cards>""" + cards + """</div>
<div class=foot>
源码在 <a href="https://gitee.com/xmz7891/ykxmz" target=_blank rel=noopener>Gitee</a>
· 历史版本在 <a href="https://gitee.com/xmz7891/ykxmz/releases" target=_blank rel=noopener>发行版</a>
· <a href="""" + ADMIN_PATH + """">管理入口</a>
</div>
</div></body></html>"""
def _login_page(message: str = "") -> bytes:
"""管理页登录页(输密码,不再是 URL 里带钥匙)。"""
notice = ('<p class="bad">' + message + '</p>') if message else ''
@@ -801,7 +914,7 @@ def _login_page(message: str = "") -> bytes:
<div class=box>
<h1>中转服务器</h1>
<div class=sub>局域网遥控 · 管理后台</div>
<form method=post action=/admin/login>
<form method=post action=/admin/login><input name=username placeholder=账号 autocomplete=username required>
<input type=password name=password placeholder=管理密码 autofocus
autocomplete=current-password>
<button type=submit>登 录</button>
@@ -1161,11 +1274,25 @@ class Handler(BaseHTTPRequestHandler):
self._json(404, {"ok": False, "error": "没有这个管理接口"})
return
if path in ("/", "/admin"):
# ---- 官网首页(公开,不用登录)----
if path == "/":
self._send(200, _site_page().encode("utf-8"),
"text/html; charset=utf-8")
return
# ---- 管理后台 ----
# 挪到 /zhingzhuanzhan(用户要求)。
# /admin 保留成跳转,免得存过书签的人打不开。
if path == "/admin":
self._redirect("/zhingzhuanzhan")
return
if path == "/zhingzhuanzhan":
# 管理页要登录 —— 上面会显示配对码,不能谁都能看
if not self._admin_gate():
self._send(200, _login_page(
"密码不对,再试一次" if _query(self.path, "bad") else ""
"账号或密码不对,再试一次"
if _query(self.path, "bad") else ""
), "text/html; charset=utf-8")
return
self._admin_page()
@@ -1266,20 +1393,25 @@ class Handler(BaseHTTPRequestHandler):
# ---- 登录 ----
if path == "/admin/login":
form = self._body().decode("utf-8", "replace")
username = ""
password = ""
from urllib.parse import unquote_plus
for piece in form.split("&"):
name, _, value = piece.partition("=")
if name == "password":
from urllib.parse import unquote_plus
password = unquote_plus(value)
token = admin_login(password)
elif name == "username":
username = unquote_plus(value)
token = admin_login(username, password)
if not token:
log(f"管理页登录失败,来自 {self._client_ip()}")
self._redirect("/?bad=1")
log("管理页登录失败(%s),来自 %s"
% (username or "未填账号", self._client_ip()))
self._redirect("/zhingzhuanzhan?bad=1")
return
log(f"管理页登录成功,来自 {self._client_ip()}")
log("管理页登录成功(%s),来自 %s"
% (username, self._client_ip()))
self.send_response(302)
self.send_header("Location", "/")
self.send_header("Location", "/zhingzhuanzhan")
self.send_header(
"Set-Cookie",
f"lr_admin={token}; Path=/; HttpOnly; SameSite=Lax; "
@@ -1442,6 +1574,7 @@ class Handler(BaseHTTPRequestHandler):
html = f"""<!doctype html><html lang=zh>
<meta charset=utf-8>
<meta name=viewport content="width=device-width,initial-scale=1">
<link rel="icon" href="/favicon.ico">
<title>ykxmz 中转服务器</title>
<style>
*{{box-sizing:border-box}}