mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 17:10:43 +00:00
手表端 - 修「连上后一片白屏」:initTabs() 从来没被调用过, 导致 tabDefs 为 null、selectTab 直接 return, 标签栏里只剩一个孤立的滑动指示器 - 应用名改成 ykxmz 被控端 - 修「设备已关闭新设备配对」:配对码本身就是密钥, 拿着正确的码就该放行。老逻辑"首台配对后自动关窗"会把 手表重装的人永久挡在外面 - 应用名改成 ykxmz 服务器端 - / 改成官网(公开,简洁介绍 + 下载) - /zhingzhuanzhan 中转站管理(要登录),/admin 自动跳转 - 管理账号 xmz7891,可改 - 官网 / 登录页 / 管理页都加上 favicon
303 lines
13 KiB
Python
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())
|