Files
ykxmz/_build/split_site_admin2.py
T
xmz7891 81e923a0aa 1.7.1:修白屏 + 配对 + 官网管理页分家
手表端
- 修「连上后一片白屏」:initTabs() 从来没被调用过,
  导致 tabDefs 为 null、selectTab 直接 return,
  标签栏里只剩一个孤立的滑动指示器
- 应用名改成 ykxmz

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

服务器端
- / 改成官网(公开,简洁介绍 + 下载)
- /zhingzhuanzhan 中转站管理(要登录),/admin 自动跳转
- 管理账号 xmz7891,可改
- 官网 / 登录页 / 管理页都加上 favicon
2026-10-06 23:08:07 +08:00

303 lines
13 KiB
Python

# -*- coding: utf-8 -*-
r"""中转站改造成「官网 + 管理页分家」(修正版)。
上一版脚本自己踩了 f-string 花括号没转义,在写文件前就崩了 ——
所以 relay.py 没被改动。这版全部改成普通字符串拼接。
用户要求:
relay.xxmxxs.top/ -> 官网(简洁介绍 + 下载)
relay.xxmxxs.top/zhingzhuanzhan -> 中转站管理(要登录)
管理账号 xmz7891 密码 @REVAedu120830
"""
from __future__ import annotations
import ast
import sys
from pathlib import Path
RELAY = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\relay" r"\relay.py")
ADMIN_PATH = "/zhingzhuanzhan"
ADMIN_USER = "xmz7891"
ADMIN_PASS = "@REVAedu120830"
def site_page_source() -> str:
"""生成 _site_page() 的源码。用拼接,避免 f-string 花括号地狱。"""
head = '''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>"""
'''
return head
def main() -> int:
text = RELAY.read_text(encoding="utf-8")
if "_site_page" in text:
print(" 已经改造过")
return 0
ok = True
# ---- 1. 补导入 ----
if "\nimport re" not in text:
text = text.replace("import os\n", "import os\nimport re\n", 1)
print(" 补 import re")
if "from urllib.parse import quote" not in text:
text = text.replace("import urllib.parse\n",
"import urllib.parse\n"
"from urllib.parse import quote\n", 1)
print(" 补 from urllib.parse import quote")
# ---- 2. 密码 + 账号 ----
old_pw = 'ADMIN_PASSWORD_DEFAULT = "120830"'
new_pw = ('ADMIN_PASSWORD_DEFAULT = "' + ADMIN_PASS + '"\n'
'# 管理页账号(原来只有密码,用户要求加上账号)\n'
'ADMIN_USER_DEFAULT = "' + ADMIN_USER + '"\n'
'ADMIN_USER = (os.environ.get("RELAY_ADMIN_USER") or\n'
' ADMIN_USER_DEFAULT)')
if old_pw in text:
text = text.replace(old_pw, new_pw, 1)
print(" 默认管理密码 -> " + ADMIN_PASS + ",账号 " + ADMIN_USER)
else:
print(" [提示] 默认密码锚点没找到")
# ---- 3. admin_login 加账号参数 ----
if "def admin_login(password: str) -> str:" in text:
text = text.replace("def admin_login(password: str) -> str:",
"def admin_login(username: str, password: str) -> str:", 1)
print(" admin_login 签名加了账号")
old_body = (' if not password or not secrets.compare_digest(password, admin_password()):\n'
' return ""')
new_body = (' # 账号也要对(用户要求用账号+密码登录)\n'
' if username and username.strip() != ADMIN_USER:\n'
' return ""\n'
' if not password or not secrets.compare_digest(password, admin_password()):\n'
' return ""')
if old_body in text:
text = text.replace(old_body, new_body, 1)
print(" admin_login 加密码校验")
else:
print(" [提示] admin_login 函数体锚点没找到")
# ---- 4. 路由 ----
old_route = (' if path in ("/", "/admin"):\n'
' # 管理页要登录 —— 上面会显示配对码,不能谁都能看\n'
' if not self._admin_gate():\n'
' self._send(200, _login_page(\n'
' "密码不对,再试一次" if _query(self.path, "bad") else ""\n'
' ), "text/html; charset=utf-8")\n'
' return\n'
' self._admin_page()\n'
' return')
new_route = (' # ---- 官网首页(公开,不用登录)----\n'
' if path == "/":\n'
' self._send(200, _site_page(), "text/html; charset=utf-8")\n'
' return\n\n'
' # ---- 管理后台 ----\n'
' # 挪到 ' + ADMIN_PATH + '(用户要求)。\n'
' # /admin 保留成跳转,免得存过书签的人打不开。\n'
' if path == "/admin":\n'
' self._redirect("' + ADMIN_PATH + '")\n'
' return\n\n'
' if path == "' + ADMIN_PATH + '":\n'
' # 管理页要登录 —— 上面会显示配对码,不能谁都能看\n'
' if not self._admin_gate():\n'
' self._send(200, _login_page(\n'
' "账号或密码不对,再试一次"\n'
' if _query(self.path, "bad") else ""\n'
' ), "text/html; charset=utf-8")\n'
' return\n'
' self._admin_page()\n'
' return')
if old_route in text:
text = text.replace(old_route, new_route, 1)
print(" 路由:/ 官网," + ADMIN_PATH + " 管理页")
else:
print(" [失败] 路由锚点没找到")
ok = False
# ---- 5. 登录接口 ----
old_login = (' if path == "/admin/login":\n'
' form = self._body().decode("utf-8", "replace")\n'
' password = ""\n'
' for piece in form.split("&"):\n'
' name, _, value = piece.partition("=")\n'
' if name == "password":\n'
' from urllib.parse import unquote_plus\n'
' password = unquote_plus(value)\n'
' token = admin_login(password)\n'
' if not token:\n'
' log(f"管理页登录失败,来自 {self._client_ip()}")\n'
' self._redirect("/?bad=1")\n'
' return\n'
' log(f"管理页登录成功,来自 {self._client_ip()}")\n'
' self.send_response(302)\n'
' self.send_header("Location", "/")')
new_login = (' if path == "/admin/login":\n'
' form = self._body().decode("utf-8", "replace")\n'
' username = ""\n'
' password = ""\n'
' from urllib.parse import unquote_plus\n'
' for piece in form.split("&"):\n'
' name, _, value = piece.partition("=")\n'
' if name == "password":\n'
' password = unquote_plus(value)\n'
' elif name == "username":\n'
' username = unquote_plus(value)\n'
' token = admin_login(username, password)\n'
' if not token:\n'
' log("管理页登录失败(%s),来自 %s"\n'
' % (username or "未填账号", self._client_ip()))\n'
' self._redirect("' + ADMIN_PATH + '?bad=1")\n'
' return\n'
' log("管理页登录成功(%s),来自 %s"\n'
' % (username, self._client_ip()))\n'
' self.send_response(302)\n'
' self.send_header("Location", "' + ADMIN_PATH + '")')
if old_login in text:
text = text.replace(old_login, new_login, 1)
print(" 登录接口:接受账号 + 密码")
else:
print(" [失败] 登录接口锚点没找到")
ok = False
# ---- 6. 登录表单加账号框 ----
old_form = '<form method=post action=/admin/login>'
if old_form in text:
text = text.replace(
old_form,
old_form + '<input name=username placeholder=账号 '
'autocomplete=username required>', 1)
print(" 登录表单加了账号框")
# ---- 7. 插入官网函数 ----
anchor = "def _login_page("
if anchor in text:
text = text.replace(anchor, site_page_source() + anchor, 1)
print(" 插入 _site_page()")
else:
print(" [失败] 找不到 _login_page")
ok = False
RELAY.write_text(text, encoding="utf-8", newline="\n")
try:
ast.parse(text)
print(" relay.py 语法 OK")
except SyntaxError as exc:
print(" [失败] 语法错误: 行 %s: %s" % (exc.lineno, exc.msg))
lines = text.splitlines()
for i in range(max(0, (exc.lineno or 1) - 3),
min(len(lines), (exc.lineno or 1) + 2)):
print(" %d: %s" % (i + 1, lines[i][:90]))
return 1
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())