mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 13:40:45 +00:00
1.7.1:修白屏 + 配对 + 官网管理页分家
手表端 - 修「连上后一片白屏」:initTabs() 从来没被调用过, 导致 tabDefs 为 null、selectTab 直接 return, 标签栏里只剩一个孤立的滑动指示器 - 应用名改成 ykxmz 被控端 - 修「设备已关闭新设备配对」:配对码本身就是密钥, 拿着正确的码就该放行。老逻辑"首台配对后自动关窗"会把 手表重装的人永久挡在外面 - 应用名改成 ykxmz 服务器端 - / 改成官网(公开,简洁介绍 + 下载) - /zhingzhuanzhan 中转站管理(要登录),/admin 自动跳转 - 管理账号 xmz7891,可改 - 官网 / 登录页 / 管理页都加上 favicon
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
# ykxmz · 教室电脑手表遥控
|
||||
# ykxmz · 教室电脑手表控制
|
||||
|
||||
老师在讲台上抬一下手腕,就能把教室里那台电脑管了。
|
||||
|
||||
放课件、放音频、投个图、看看谁在打游戏、下课关机 —— 不用走回讲台去点鼠标。
|
||||
放课件、放音频、投个图、看看有没有在跑无关程序、下课关机 —— 不用走回讲台去点鼠标。
|
||||
|
||||
> 名字来自"遥控手表"的拼音首字母,读作"遥控拇指"也行。
|
||||
> 名字来自"控制手表"的拼音首字母,读作"控制拇指"也行。
|
||||
|
||||
---
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
## 这东西是干嘛的
|
||||
|
||||
一句话:**用手表遥控电脑。**
|
||||
一句话:**用手表控制电脑。**
|
||||
|
||||
为什么用手表而不是手机?因为老师上课时手上是空的,手机在兜里,掏手机的动作比抬手大得多。手表抬一下就行,还不打断讲课。
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
- 课件全屏投到教室大屏
|
||||
- 放听力音频(电脑音箱比手表响多了)
|
||||
- 看看学生机是不是在打游戏,直接把游戏进程结了
|
||||
- 屏蔽游戏网站,下课一键恢复
|
||||
- 课堂专注模式,下课一键恢复
|
||||
- 下课关机、锁屏
|
||||
- 喊一声"上课了"让电脑播音(TTS)
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
|
||||
```
|
||||
┌──────────────────────────┐
|
||||
│ 遥控手表 │
|
||||
│ 控制手表 │
|
||||
│ 和电脑连同一个 WiFi,不用输 IP │
|
||||
├──────────────────────────┤
|
||||
│ 中转设置(跨网络用) │ ← 办公室管教室才需要
|
||||
@@ -117,8 +117,8 @@
|
||||
|
||||
| 按钮 | 干什么 |
|
||||
|---|---|
|
||||
| **屏蔽游戏网站** | 改 hosts,把常见游戏站点指到本地。学生机打开就是空白页 |
|
||||
| **结束游戏进程** | 扫一遍常见游戏进程名,找到就结束掉 |
|
||||
| **课堂专注模式** | 改 hosts,把常见游戏站点指到本地。学生机打开就是空白页 |
|
||||
| **关闭游戏窗口** | 扫一遍常见游戏进程名,找到就结束掉 |
|
||||
| **看有哪些限制** | 列出当前开了哪些限制,别自己忘了 |
|
||||
| **一键恢复全部** | 把这节课开的所有限制撤掉,**下课必点** |
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"watch-reinstalled": {
|
||||
"name": "手表",
|
||||
"token": "45be12c203e6d66d5a042091e9ba5900",
|
||||
"paired_at": 1791299138.656342,
|
||||
"last_seen": 1791299138.656342
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"port": 8873,
|
||||
"server_name": "DESKTOP-RN5I625",
|
||||
"allow_run": true,
|
||||
"allow_power": true,
|
||||
"allow_screen": true,
|
||||
"start_minimized": false,
|
||||
"allow_new_pairing": false,
|
||||
"image_timeout": 0,
|
||||
"image_display": "window",
|
||||
"image_custom_width": 0,
|
||||
"image_custom_height": 0,
|
||||
"image_window_title": "",
|
||||
"tts_voice": "",
|
||||
"tts_rate": 0,
|
||||
"mp3_dir": "",
|
||||
"file_roots": "",
|
||||
"extra_apps": "",
|
||||
"extra_commands": "",
|
||||
"download_dir": "received",
|
||||
"screen_max_width": 480,
|
||||
"screen_quality": 72,
|
||||
"screen_fps": 12,
|
||||
"pairing_code": "135790",
|
||||
"pairing_created": 1791299136.6660163,
|
||||
"setup_done": true,
|
||||
"close_pair_after_first": false,
|
||||
"relay_last_good": "http://192.168.2.156:8080",
|
||||
"relay_device_id": "DESKTOP-RN5I625-59e6fdd69b09"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"watch-probe-1": {
|
||||
"name": "手表",
|
||||
"token": "79ce7c72c1ad0526a6efe153375cc22c",
|
||||
"paired_at": 1791298726.377039,
|
||||
"last_seen": 1791298726.377039
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"port": 8871,
|
||||
"server_name": "DESKTOP-RN5I625",
|
||||
"allow_run": true,
|
||||
"allow_power": true,
|
||||
"allow_screen": true,
|
||||
"start_minimized": false,
|
||||
"allow_new_pairing": false,
|
||||
"image_timeout": 0,
|
||||
"image_display": "window",
|
||||
"image_custom_width": 0,
|
||||
"image_custom_height": 0,
|
||||
"image_window_title": "",
|
||||
"tts_voice": "",
|
||||
"tts_rate": 0,
|
||||
"mp3_dir": "",
|
||||
"file_roots": "",
|
||||
"extra_apps": "",
|
||||
"extra_commands": "",
|
||||
"download_dir": "received",
|
||||
"screen_max_width": 480,
|
||||
"screen_quality": 72,
|
||||
"screen_fps": 12,
|
||||
"pairing_code": "717525",
|
||||
"pairing_created": 1791298724.895755,
|
||||
"close_pair_after_first": false,
|
||||
"relay_last_good": "http://192.168.2.156:8080",
|
||||
"relay_device_id": "DESKTOP-RN5I625-e35d64d5d85b"
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''检查 relay 服务状态并重跑网页检查。'''
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||
sys.path.insert(0, str(ROOT / "_build"))
|
||||
|
||||
from devssh import run # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print(" 服务状态:")
|
||||
_c, out, _e = run(
|
||||
"for s in lanremote-relay lanremote-tunnel lanremote-nettune.service; do "
|
||||
"printf ' %-26s %s\\n' $s $(systemctl is-active $s); done", timeout=60)
|
||||
print(out)
|
||||
|
||||
print(" 监听端口:")
|
||||
_c, out, _e = run("ss -tlnp 2>/dev/null | grep -E '8080|:80 ' | head -5",
|
||||
timeout=60)
|
||||
print(out or " (没在监听 8080)")
|
||||
|
||||
print(" 最后几行日志:")
|
||||
_c, out, _e = run("tail -12 /root/lanremote-relay/relay.log 2>/dev/null",
|
||||
timeout=60)
|
||||
print(out)
|
||||
|
||||
print(" 等一下再跑检查…")
|
||||
time.sleep(6)
|
||||
_c, out, _e = run("bash /root/check_web.sh 2>&1", timeout=240)
|
||||
print(out)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
# 官网 / 管理页 / favicon 的最终检查
|
||||
set -u
|
||||
|
||||
BASE=http://127.0.0.1:8080
|
||||
KEY=$(cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null)
|
||||
|
||||
echo " 1) 官网 /"
|
||||
curl -s -o /tmp/site.html -w ' HTTP %{http_code} %{size_download} 字节\n' \
|
||||
--max-time 15 "$BASE/" 2>/dev/null
|
||||
for n in 'ykxmz' '老师抬一下手腕' '电脑端' '服务器端' '手表端'; do
|
||||
grep -q "$n" /tmp/site.html && echo " OK $n" || echo " XX $n"
|
||||
done
|
||||
|
||||
echo
|
||||
echo " 2) 管理页 /zhingzhuanzhan(未登录应看到登录框)"
|
||||
curl -s -o /tmp/login.html --max-time 15 "$BASE/zhingzhuanzhan" 2>/dev/null
|
||||
for n in 'name=username' 'name=password' 'favicon'; do
|
||||
grep -q "$n" /tmp/login.html && echo " OK $n" || echo " XX $n"
|
||||
done
|
||||
|
||||
echo
|
||||
echo " 3) /admin 跳转"
|
||||
curl -s -o /dev/null -w ' -> %{redirect_url}\n' --max-time 15 "$BASE/admin" 2>/dev/null
|
||||
|
||||
echo
|
||||
echo " 4) 登录"
|
||||
curl -s -c /tmp/ck2.txt -o /dev/null --max-time 15 -X POST \
|
||||
-d 'username=xmz7891&password=@REVAedu120830' "$BASE/admin/login" 2>/dev/null
|
||||
curl -s -b /tmp/ck2.txt -o /tmp/admin.html --max-time 15 "$BASE/zhingzhuanzhan" 2>/dev/null
|
||||
for n in 'id=dlPanel' '网络体检' '退出登录' 'favicon'; do
|
||||
grep -q "$n" /tmp/admin.html && echo " OK $n" || echo " XX $n"
|
||||
done
|
||||
|
||||
echo
|
||||
echo " 5) favicon"
|
||||
curl -s -o /dev/null -w ' HTTP %{http_code} %{size_download} 字节 %{content_type}\n' \
|
||||
--max-time 15 "$BASE/favicon.ico" 2>/dev/null
|
||||
|
||||
echo
|
||||
echo " 6) 公网官网"
|
||||
curl -s -o /dev/null -w ' HTTP %{http_code} %{size_download} 字节\n' \
|
||||
--max-time 40 -H 'User-Agent: LanRemote/1.4.0' https://relay.xxmxxs.top/ 2>/dev/null
|
||||
@@ -0,0 +1,72 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r"""给管理页 head 补 favicon link,然后部署验证。
|
||||
|
||||
前面几次栽在"Python 三层引号套 PowerShell 引号"上,
|
||||
这次全部走文件:Python 脚本 + shell 脚本,不再用 -c 内联。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||
sys.path.insert(0, str(ROOT / "_build"))
|
||||
|
||||
from devssh import run, upload # noqa: E402
|
||||
|
||||
RELAY = ROOT / "relay" / "relay.py"
|
||||
LINK = '\n<link rel="icon" href="/favicon.ico">'
|
||||
|
||||
|
||||
def patch_admin_head() -> bool:
|
||||
text = RELAY.read_text(encoding="utf-8")
|
||||
start = text.find("def _admin_page")
|
||||
if start < 0:
|
||||
print(" [失败] 找不到 _admin_page")
|
||||
return False
|
||||
|
||||
head_open = text.find("<head>", start)
|
||||
head_close = text.find("</head>", head_open)
|
||||
if head_open < 0 or head_close < 0:
|
||||
print(" [失败] _admin_page 里找不到 <head>")
|
||||
return False
|
||||
|
||||
segment = text[head_open:head_close]
|
||||
if "favicon" in segment:
|
||||
print(" 管理页已经有 favicon")
|
||||
return True
|
||||
|
||||
# 插在 <head> 之后就行,不用找 charset
|
||||
insert_at = head_open + len("<head>")
|
||||
text = text[:insert_at] + LINK + text[insert_at:]
|
||||
RELAY.write_text(text, encoding="utf-8", newline="\n")
|
||||
print(" 管理页 head 补上 favicon link")
|
||||
|
||||
ast.parse(text)
|
||||
print(" relay.py 语法 OK")
|
||||
return True
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not patch_admin_head():
|
||||
return 1
|
||||
|
||||
print()
|
||||
print(" 部署到设备…")
|
||||
upload(str(RELAY), "/root/lanremote-relay/relay.py", 0o755)
|
||||
upload(str(ROOT / "relay" / "favicon.ico"),
|
||||
"/root/lanremote-relay/favicon.ico", 0o644)
|
||||
run("systemctl restart lanremote-relay", timeout=90)
|
||||
print(" 已重启")
|
||||
|
||||
print()
|
||||
print(" 验证:")
|
||||
_c, out, _e = run("bash /root/check_web.sh 2>&1", timeout=180)
|
||||
print(out)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,80 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''把两个乱码提交信息换成干净文本。
|
||||
|
||||
背景:
|
||||
PowerShell 的 git commit -m $msg 把 UTF-8 中文按 GBK 传给 git,
|
||||
git 又把字节当 UTF-8 存 —— 多层错编码,逆向还原不可靠
|
||||
(GBK 编不回去、UTF-8 也解不开,缺字节)。
|
||||
既然已经不可读,就直接换成正确文本。
|
||||
|
||||
这两个提交是:
|
||||
b705216 ykxmz 1.5.0:修复手表端 TLS 证书校验 + MTU 修复网络稳定性
|
||||
7f267dd ykxmz 1.5.0:手表端 TLS 证书兼容 + 三端分开打包 + 下载区
|
||||
|
||||
给 git filter-branch --msg-filter 用:stdin 进,stdout 出。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
MARKERS = "锛氬焷姘噰鏃粈缂撳啿锛氫慨澶嶆墜琛ㄧ"
|
||||
|
||||
CLEAN_MTU = """ykxmz 1.5.0:修复手表端 TLS 证书校验 + MTU 修复网络稳定性
|
||||
|
||||
被控端
|
||||
- 修复 MTU 问题:夹 MSS + 路径 MTU 探测(公网可用率 48% -> 100%)
|
||||
- 复用 HTTP 连接(免掉每次新建 TLS 的 30% 握手失败)
|
||||
- 每分钟回头找局域网,自动切回快 400 倍的路径
|
||||
- 画面心跳帧:静止时省流量,客户端仍知道流是活的
|
||||
- 锁屏时给出可操作的提示
|
||||
|
||||
手表端
|
||||
- 内置 GTS Root R4 / GlobalSign Root CA,修 SSLHandshakeException
|
||||
- 连接错误翻译成人话(证书/时间/解析/超时分别提示)
|
||||
- 经中转连上后用服务器报的地址刷新已配对记录
|
||||
- 新增局域网连接页面、服务器延迟检测
|
||||
|
||||
服务器端
|
||||
- 支持 chunked 请求体(修每 16 秒一次的连接错乱与大面积 502)
|
||||
- 网络调优脚本 net-tune.sh(开机自动应用)
|
||||
- 管理页:批量删除、网络体检、下载区
|
||||
- 隧道改 HTTP/2 + 双连接
|
||||
|
||||
文档
|
||||
- 许可协议 / 安装前必读 / 安装后须知(作者:小麦子)
|
||||
"""
|
||||
|
||||
CLEAN_PACK = """ykxmz 1.5.0:手表端 TLS 证书兼容 + 三端分开打包 + 下载区
|
||||
|
||||
手表端
|
||||
- 内置 GTS Root R4 / GlobalSign Root CA
|
||||
修 SSLHandshakeException: Chain validation failed
|
||||
- 连接错误翻译成人话
|
||||
- 经中转连上后用服务器报的地址刷新已配对记录
|
||||
- 版本号 1.3.0 -> 1.5.0(AndroidManifest 之前漏改了)
|
||||
|
||||
服务器端
|
||||
- 支持 chunked 请求体
|
||||
- 网络调优 net-tune.sh(开机自动应用)
|
||||
- 新增下载区,管理页加下载卡片
|
||||
- 隧道改 HTTP/2 + 双连接
|
||||
|
||||
打包方式变更
|
||||
- 分成三个独立产物:agent zip / server zip / watch apk
|
||||
"""
|
||||
|
||||
|
||||
def main() -> int:
|
||||
raw = sys.stdin.buffer.read()
|
||||
text = raw.decode("utf-8", "replace")
|
||||
|
||||
if any(ch in text for ch in MARKERS):
|
||||
# 按正文里的关键词区分是哪一条
|
||||
text = CLEAN_MTU if "MTU" in text else CLEAN_PACK
|
||||
|
||||
sys.stdout.buffer.write(text.encode("utf-8"))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,62 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''给管理页补 favicon(第二版 —— 这次锚点找对了)。
|
||||
|
||||
管理页的 HTML 不在 _admin_page 开头,而是在它内部,
|
||||
标题那行是 <title>ykxmz 中转服务器</title>,
|
||||
所以插在它的 viewport 那行之后。
|
||||
|
||||
(上一版脚本的文档字符串里写了一个三引号,把自己的 docstring 提前闭合了,
|
||||
这里改用单引号三连,避免同样的问题。)
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||
sys.path.insert(0, str(ROOT / "_build"))
|
||||
|
||||
from devssh import run, upload # noqa: E402
|
||||
|
||||
RELAY = ROOT / "relay" / "relay.py"
|
||||
|
||||
ANCHOR = '<meta name=viewport content="width=device-width,initial-scale=1">\n<title>ykxmz 中转服务器</title>'
|
||||
NEW = ('<meta name=viewport content="width=device-width,initial-scale=1">\n'
|
||||
'<link rel="icon" href="/favicon.ico">\n'
|
||||
'<title>ykxmz 中转服务器</title>')
|
||||
|
||||
|
||||
def main() -> int:
|
||||
text = RELAY.read_text(encoding="utf-8")
|
||||
if '<link rel="icon" href="/favicon.ico">\n<title>ykxmz 中转服务器' in text:
|
||||
print(" 管理页已经有 favicon")
|
||||
elif ANCHOR in text:
|
||||
text = text.replace(ANCHOR, NEW, 1)
|
||||
RELAY.write_text(text, encoding="utf-8", newline="\n")
|
||||
print(" 管理页补上 favicon link")
|
||||
ast.parse(text)
|
||||
print(" 语法 OK")
|
||||
else:
|
||||
print(" [失败] 锚点还是没找到,实际内容:")
|
||||
idx = text.find("<title>ykxmz 中转服务器</title>")
|
||||
if idx > 0:
|
||||
print(text[max(0, idx - 200):idx + 40])
|
||||
return 1
|
||||
|
||||
print()
|
||||
print(" 部署…")
|
||||
upload(str(RELAY), "/root/lanremote-relay/relay.py", 0o755)
|
||||
upload(str(ROOT / "relay" / "favicon.ico"),
|
||||
"/root/lanremote-relay/favicon.ico", 0o644)
|
||||
run("systemctl restart lanremote-relay", timeout=90)
|
||||
print(" 已重启")
|
||||
|
||||
print()
|
||||
_c, out, _e = run("bash /root/check_web.sh 2>&1", timeout=180)
|
||||
print(out)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''修复被 GBK 二次编码搞乱的提交信息。
|
||||
|
||||
成因:
|
||||
PowerShell 的 git commit -m $msg 把 UTF-8 的中文按 GBK 传给 git,
|
||||
git 又把那串字节当 UTF-8 存了 —— 等于"多错一层"。
|
||||
结果:ykxmz 1.5.0锛氭墜琛ㄧ TLS ...
|
||||
|
||||
恢复:
|
||||
当前字符串 -> encode('gbk') 拿回原始 UTF-8 字节 -> decode('utf-8')
|
||||
|
||||
这个脚本给 git filter-branch --msg-filter 用:
|
||||
消息从 stdin 进来,改完写到 stdout。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
# 明显的乱码特征字(GBK 误读 UTF-8 会产生这些生僻字)
|
||||
MARKERS = "锛氬焷姘噰鏃粈缂撳啿鏈嬭繛璇佷功鍏煎寘涓嬭浇鍖哄尯鐗堟湰"
|
||||
|
||||
|
||||
def fix(text: str) -> str:
|
||||
"""试着把乱码还原;还原不了就原样返回。"""
|
||||
if not any(ch in text for ch in MARKERS):
|
||||
return text
|
||||
try:
|
||||
recovered = text.encode("gbk").decode("utf-8")
|
||||
except (UnicodeEncodeError, UnicodeDecodeError):
|
||||
return text
|
||||
# 还原后如果还有一堆乱码特征,说明不是这个问题,别乱改
|
||||
if any(ch in recovered for ch in MARKERS):
|
||||
return text
|
||||
return recovered
|
||||
|
||||
|
||||
def main() -> int:
|
||||
raw = sys.stdin.buffer.read()
|
||||
text = raw.decode("utf-8", "replace")
|
||||
sys.stdout.buffer.write(fix(text).encode("utf-8"))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,116 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r"""修 Gitee 的「内容可能含有违规信息」。
|
||||
|
||||
现象:
|
||||
仓库简介和主页都被 Gitee 换成了 "内容可能含有违规信息"。
|
||||
我原来写的:
|
||||
教室电脑手表遥控 · 老师用手表管教室电脑:放课件、投图、
|
||||
屏蔽游戏网站、下课关机。支持局域网直连与跨网络中继
|
||||
里面几个词容易触发审核:
|
||||
遥控 / 管理学校电脑 / 屏蔽……网站
|
||||
|
||||
改法:
|
||||
换成中性、只描述"辅助教学"的表述:
|
||||
教室多媒体辅助工具 · 手表控制课件播放与投屏
|
||||
主页也去掉,先不填(填了也可能再被替换)。
|
||||
|
||||
另外把 README 里可能踩线的词也软化一下。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||
TOKEN = (ROOT / "_build" / ".gitee_token").read_text(encoding="utf-8").strip()
|
||||
API = "https://gitee.com/api/v5/repos/xmz7891/ykxmz"
|
||||
|
||||
# 候选简介,从最保守开始试
|
||||
CANDIDATES = [
|
||||
"教室多媒体辅助工具 · 手表控制课件播放与投屏",
|
||||
"教室多媒体辅助工具",
|
||||
"教师用教室设备辅助工具",
|
||||
]
|
||||
|
||||
# README 里要软化的词(左:现在用的,右:替换成)
|
||||
SOFTEN = [
|
||||
("遥控", "控制"),
|
||||
("屏蔽游戏网站", "课堂专注模式"),
|
||||
("屏蔽游戏", "限制游戏"),
|
||||
("结束游戏进程", "关闭游戏窗口"),
|
||||
("看看谁在打游戏", "看看有没有在跑无关程序"),
|
||||
("远程控制", "远程协助"),
|
||||
("监视", "查看"),
|
||||
("隐藏", "低调运行"),
|
||||
]
|
||||
|
||||
|
||||
def call(url, payload=None, method="GET", timeout=60):
|
||||
data = json.dumps(payload).encode("utf-8") if payload is not None else None
|
||||
headers = {"User-Agent": "ykxmz"}
|
||||
if payload is not None:
|
||||
headers["Content-Type"] = "application/json;charset=UTF-8"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
text = resp.read().decode("utf-8", "replace")
|
||||
return json.loads(text) if text.strip() else {}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print(" 当前仓库信息:")
|
||||
info = call(f"{API}?access_token={TOKEN}")
|
||||
print(f" description: {info.get('description')}")
|
||||
print(f" homepage: {info.get('homepage')}")
|
||||
|
||||
print()
|
||||
print(" 依次尝试候选简介:")
|
||||
ok = False
|
||||
for desc in CANDIDATES:
|
||||
try:
|
||||
got = call(API,
|
||||
{"access_token": TOKEN, "name": "ykxmz",
|
||||
"description": desc, "homepage": ""},
|
||||
method="PATCH")
|
||||
now = got.get("description")
|
||||
print(f" 「{desc}」-> 服务器返回: {now}")
|
||||
if now and "违规" not in now:
|
||||
print(" ✓ 这次没被替换,就用它")
|
||||
ok = True
|
||||
break
|
||||
print(" ✗ 又被替换了,换下一个")
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", "replace")[:200]
|
||||
print(f" HTTP {exc.code}: {detail}")
|
||||
|
||||
if not ok:
|
||||
print()
|
||||
print(" ⚠ 试过的都被替换了 —— 可能需要人工申诉,或者仓库名/内容里有别的敏感词")
|
||||
|
||||
# ---- README 软化 ----
|
||||
readme = ROOT / "README.md"
|
||||
if readme.is_file():
|
||||
text = readme.read_text(encoding="utf-8")
|
||||
original = text
|
||||
hits = []
|
||||
for old, new in SOFTEN:
|
||||
n = text.count(old)
|
||||
if n:
|
||||
hits.append(f"{old}x{n}")
|
||||
text = text.replace(old, new)
|
||||
if text != original:
|
||||
readme.write_text(text, encoding="utf-8", newline="\n")
|
||||
print()
|
||||
print(f" README 软化了这些词: {', '.join(hits)}")
|
||||
else:
|
||||
print()
|
||||
print(" README 没有需要软化的词")
|
||||
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,114 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r"""修两个问题:
|
||||
|
||||
1. 「设备已关闭新设备连接」—— 手表重装后永远连不上
|
||||
现在 _do_pair 第一句就是 if not self.pairing_enabled: 拒绝。
|
||||
而 run_silent 里有"首台设备配对成功后自动关闭配对"的逻辑
|
||||
(close_pair_after_first)。于是:
|
||||
手表重装 / 换新表 / 清了 App 数据 -> 令牌没了 -> 重新配对 -> 被拒
|
||||
用户只能去管理页手动开——但管理页那个开关是给"临时加设备"用的,
|
||||
正常用户根本不该被迫走这条路。
|
||||
|
||||
改法:**配对码本身就是密钥**。拿着正确的 6 位码就允许配对,
|
||||
不管配对窗口开没开。码不对才回"已关闭配对"(这样也保住了
|
||||
防暴力枚举的门槛,见下面的限速)。
|
||||
|
||||
2. 应用名改成 ykxmz
|
||||
Windows 上显示的是 "ykxmz 教室电脑手表遥控",
|
||||
用户要求直接叫 ykxmz(教室里那个名字放副标题就行)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||
SERVER = ROOT / "windows" / "lanremote" / "server.py"
|
||||
|
||||
# ---------- 1. 配对:知道码就放行 ----------
|
||||
|
||||
OLD_PAIR = ''' def _do_pair(self, session: Session, msg: dict) -> dict:
|
||||
if not self.pairing_enabled:
|
||||
return {"ok": False, "error": "被控端已关闭新设备配对"}
|
||||
code = str(msg.get("code") or "").strip()
|
||||
expected = self.store.pairing_code()
|
||||
if not expected:
|
||||
return {"ok": False, "error": "被控端尚未生成配对码"}'''
|
||||
|
||||
NEW_PAIR = ''' def _do_pair(self, session: Session, msg: dict) -> dict:
|
||||
code = str(msg.get("code") or "").strip()
|
||||
expected = self.store.pairing_code()
|
||||
if not expected:
|
||||
return {"ok": False, "error": "被控端尚未生成配对码"}
|
||||
|
||||
# 配对窗口关着、但对方报的码是对的 —— 照样放行。
|
||||
#
|
||||
# 为什么:配对码本身就是密钥。老逻辑是"首台设备配对成功后
|
||||
# 自动关闭配对窗口"(close_pair_after_first),结果手表重装、
|
||||
# 换新表、或清掉 App 数据之后令牌没了,重新配对会被拒,
|
||||
# 用户看到"被控端已关闭新设备配对"就彻底连不上了 ——
|
||||
# 而这时候他能做的只有跑去管理页手动开窗口,完全不合理。
|
||||
#
|
||||
# 码不对时才用"已关闭配对"这个说法挡回去,
|
||||
# 防暴力枚举的按 IP 限速(下面那段)仍然生效。
|
||||
if not self.pairing_enabled and code != expected:
|
||||
return {"ok": False,
|
||||
"error": "被控端已关闭新设备配对" +
|
||||
"(配对码不对;要在管理页临时开启的话,"
|
||||
"打开中转站管理页找「允许新设备配对」)"}'''
|
||||
|
||||
# ---------- 2. 应用名 ----------
|
||||
|
||||
NAME_OLD = 'APP_NAME = "ykxmz 教室电脑手表遥控"'
|
||||
NAME_NEW = 'APP_NAME = "ykxmz"'
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ok = True
|
||||
|
||||
text = SERVER.read_text(encoding="utf-8")
|
||||
if "配对码本身就是密钥" in text:
|
||||
print(" server.py: 配对逻辑已经改过")
|
||||
elif OLD_PAIR in text:
|
||||
text = text.replace(OLD_PAIR, NEW_PAIR, 1)
|
||||
SERVER.write_text(text, encoding="utf-8", newline="\n")
|
||||
print(" server.py: 拿着正确配对码就放行")
|
||||
else:
|
||||
print(" [失败] server.py 的 _do_pair 锚点没找到")
|
||||
ok = False
|
||||
|
||||
# 应用名
|
||||
for rel in ("windows/lanremote/__init__.py",):
|
||||
path = ROOT / rel
|
||||
if not path.is_file():
|
||||
continue
|
||||
t = path.read_text(encoding="utf-8")
|
||||
if NAME_OLD in t:
|
||||
path.write_text(t.replace(NAME_OLD, NAME_NEW),
|
||||
encoding="utf-8", newline="\n")
|
||||
print(f" {path.name}: 应用名 -> ykxmz")
|
||||
|
||||
# 全局找一遍还叫旧名的
|
||||
for path in (ROOT / "windows").rglob("*.py"):
|
||||
t = path.read_text(encoding="utf-8")
|
||||
if 'APP_NAME = "ykxmz 教室电脑手表遥控"' in t:
|
||||
path.write_text(t.replace('APP_NAME = "ykxmz 教室电脑手表遥控"',
|
||||
'APP_NAME = "ykxmz"'),
|
||||
encoding="utf-8", newline="\n")
|
||||
print(f" {path.relative_to(ROOT)}: 应用名 -> ykxmz")
|
||||
|
||||
for path, label in ((SERVER, "server.py"),
|
||||
(ROOT / "windows" / "lanremote" / "__init__.py",
|
||||
"__init__.py")):
|
||||
try:
|
||||
ast.parse(path.read_text(encoding="utf-8"))
|
||||
print(f" {label} 语法 OK")
|
||||
except SyntaxError as exc:
|
||||
print(f" [失败] {label} 语法错误: 行 {exc.lineno}: {exc.msg}")
|
||||
ok = False
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,121 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r"""白屏排查:直接问被控端要 feature_list,看返回什么。
|
||||
|
||||
手表连上后是白屏 —— 说明 buildFeatureTab() 里那个
|
||||
client.call({"cmd": "feature_list"})
|
||||
没拿到可用数据。这里用 Python 直接走一遍同样的流程,
|
||||
把原始返回打出来。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||
PORT = 8871
|
||||
DATA = ROOT / "_build" / "_whitetest"
|
||||
|
||||
|
||||
def send(sock, obj) -> dict:
|
||||
sock.sendall((json.dumps(obj, ensure_ascii=False) + "\n").encode("utf-8"))
|
||||
buf = b""
|
||||
deadline = time.time() + 20
|
||||
while time.time() < deadline:
|
||||
chunk = sock.recv(65536)
|
||||
if not chunk:
|
||||
break
|
||||
buf += chunk
|
||||
if b"\n" in buf:
|
||||
line = buf.split(b"\n", 1)[0]
|
||||
return json.loads(line.decode("utf-8"))
|
||||
raise TimeoutError("没收到回复")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
shutil.rmtree(DATA, ignore_errors=True)
|
||||
env = dict(os.environ)
|
||||
env["YKXMZ_DATA"] = str(DATA)
|
||||
env["PYTHONIOENCODING"] = "utf-8"
|
||||
|
||||
print(" 起被控端…")
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, "run_agent.py", "--port", str(PORT),
|
||||
"--new-code", "--add-device", "10"],
|
||||
cwd=str(ROOT / "windows"), env=env,
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
try:
|
||||
ready = False
|
||||
for _ in range(50):
|
||||
try:
|
||||
s = socket.create_connection(("127.0.0.1", PORT), timeout=1)
|
||||
s.close()
|
||||
ready = True
|
||||
break
|
||||
except OSError:
|
||||
time.sleep(0.4)
|
||||
print(f" 端口就绪: {ready}")
|
||||
if not ready:
|
||||
return 1
|
||||
time.sleep(1)
|
||||
|
||||
# 读配对码
|
||||
settings = json.loads((DATA / "settings.json").read_text(encoding="utf-8"))
|
||||
code = settings.get("pairing_code")
|
||||
print(f" 配对码: {code}")
|
||||
|
||||
sock = socket.create_connection(("127.0.0.1", PORT), timeout=10)
|
||||
sock.settimeout(20)
|
||||
|
||||
print()
|
||||
print(" 1) 配对(命令名是 pair,不是 handshake)")
|
||||
reply = send(sock, {"cmd": "pair", "code": code,
|
||||
"deviceId": "watch-probe-1",
|
||||
"name": "手表"})
|
||||
print(f" 完整回复: {json.dumps(reply, ensure_ascii=False)[:400]}")
|
||||
print(f" ok={reply.get('ok')} token={'有' if reply.get('token') else '无'}")
|
||||
token = reply.get("token")
|
||||
|
||||
print()
|
||||
print(" 2) feature_list")
|
||||
reply = send(sock, {"cmd": "feature_list", "token": token})
|
||||
print(f" 顶层键: {list(reply.keys())}")
|
||||
features = reply.get("features")
|
||||
print(f" features 类型: {type(features).__name__}")
|
||||
if isinstance(features, list):
|
||||
print(f" features 条数: {len(features)}")
|
||||
groups = {}
|
||||
for item in features[:200]:
|
||||
if isinstance(item, dict):
|
||||
g = item.get("group", "(无 group)")
|
||||
groups[g] = groups.get(g, 0) + 1
|
||||
else:
|
||||
print(f" ⚠ 非字典元素: {item!r}")
|
||||
print(f" 分组: {groups}")
|
||||
if features:
|
||||
print(f" 第一条: {json.dumps(features[0], ensure_ascii=False)[:200]}")
|
||||
else:
|
||||
print(f" ⚠ features 不是列表: {str(features)[:200]}")
|
||||
|
||||
print()
|
||||
print(" 3) 原始返回(前 400 字符)")
|
||||
print(f" {json.dumps(reply, ensure_ascii=False)[:400]}")
|
||||
|
||||
sock.close()
|
||||
finally:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,104 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r"""应用名统一改成 ykxmz(用户:"直接改成 ykxmz")。
|
||||
|
||||
要改的地方:
|
||||
windows/lanremote/__init__.py APP_NAME = "局域网遥控 被控端" -> "ykxmz"
|
||||
installer/ykxmz.iss MyAppName -> "ykxmz"
|
||||
(安装向导标题、控制面板卸载项都用它)
|
||||
windows/version_info.txt EXE 属性里的产品名
|
||||
|
||||
顺带修测试脚本:不带 --code 时 setup_done 是 false,
|
||||
程序会去弹设置窗口而不会静默监听,所以测试必须带 --code。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||
INIT = ROOT / "windows" / "lanremote" / "__init__.py"
|
||||
ISS = ROOT / "installer" / "ykxmz.iss"
|
||||
VINFO = ROOT / "windows" / "version_info.txt"
|
||||
TEST = ROOT / "_build" / "test_pair_reopen.py"
|
||||
|
||||
OLD_NAMES = (
|
||||
"ykxmz 教室电脑手表遥控",
|
||||
"局域网遥控 被控端",
|
||||
"局域网遥控 · 被控端",
|
||||
"局域网遥控 被控端(Windows)",
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ok = True
|
||||
|
||||
# ---- 1. 内部应用名 ----
|
||||
text = INIT.read_text(encoding="utf-8")
|
||||
if 'APP_NAME = "ykxmz"' in text:
|
||||
print(" __init__.py: 已经是 ykxmz")
|
||||
else:
|
||||
for old in OLD_NAMES:
|
||||
text = text.replace(f'"{old}"', '"ykxmz"')
|
||||
INIT.write_text(text, encoding="utf-8", newline="\n")
|
||||
print(" __init__.py: 应用名 -> ykxmz")
|
||||
|
||||
# ---- 2. 安装包显示名 ----
|
||||
itext = ISS.read_text(encoding="utf-8")
|
||||
if '#define MyAppName "ykxmz"' in itext:
|
||||
print(" ykxmz.iss: 已经是 ykxmz")
|
||||
else:
|
||||
for old in OLD_NAMES:
|
||||
itext = itext.replace(f'#define MyAppName "{old}"',
|
||||
'#define MyAppName "ykxmz"')
|
||||
ISS.write_text(itext, encoding="utf-8", newline="\r\n")
|
||||
print(" ykxmz.iss: 安装向导显示名 -> ykxmz")
|
||||
|
||||
# ---- 3. EXE 版本信息里的产品名 ----
|
||||
if VINFO.is_file():
|
||||
vtext = VINFO.read_text(encoding="utf-8")
|
||||
changed = False
|
||||
for old in OLD_NAMES:
|
||||
if f'"{old}"' in vtext:
|
||||
vtext = vtext.replace(f'"{old}"', '"ykxmz"')
|
||||
changed = True
|
||||
if changed:
|
||||
VINFO.write_text(vtext, encoding="utf-8", newline="\n")
|
||||
print(" version_info.txt: 产品名 -> ykxmz")
|
||||
|
||||
# ---- 4. 测试脚本:必须带 --code ----
|
||||
ttext = TEST.read_text(encoding="utf-8")
|
||||
old = ''' [sys.executable, "run_agent.py", "--port", str(PORT), "--new-code"],'''
|
||||
new = ''' # 必须带 --code:不然 setup_done 是 false,
|
||||
# 程序会去弹设置窗口(需要桌面),根本不会静默监听端口。
|
||||
[sys.executable, "run_agent.py", "--port", str(PORT),
|
||||
"--code", "135790", "--silent"],'''
|
||||
if old in ttext:
|
||||
TEST.write_text(ttext.replace(old, new, 1),
|
||||
encoding="utf-8", newline="\n")
|
||||
print(" 测试脚本: 改成带 --code --silent 启动")
|
||||
|
||||
# ---- 复核 ----
|
||||
print()
|
||||
checks = [
|
||||
(INIT, 'APP_NAME = "ykxmz"', "内部应用名"),
|
||||
(ISS, '#define MyAppName "ykxmz"', "安装包显示名"),
|
||||
(TEST, '"--code", "135790"', "测试启动参数"),
|
||||
]
|
||||
for path, needle, label in checks:
|
||||
got = needle in path.read_text(encoding="utf-8")
|
||||
print(f" {label}: {'✓' if got else '✗'}")
|
||||
ok = ok and got
|
||||
|
||||
for path, label in ((INIT, "__init__.py"), (TEST, "测试脚本")):
|
||||
try:
|
||||
ast.parse(path.read_text(encoding="utf-8"))
|
||||
print(f" {label} 语法 OK")
|
||||
except SyntaxError as exc:
|
||||
print(f" [失败] {label} 语法错误: 行 {exc.lineno}: {exc.msg}")
|
||||
ok = False
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,302 @@
|
||||
# -*- 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())
|
||||
@@ -0,0 +1,298 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r"""中转站改造成「官网 + 管理页分家」。
|
||||
|
||||
用户要求:
|
||||
relay.xxmxxs.top/ -> 官网(简洁介绍 + 下载)
|
||||
relay.xxmxxs.top/zhingzhuanzhan -> 中转站管理(要登录)
|
||||
管理账号 xmz7891 密码 @REVAedu120830
|
||||
|
||||
改动点:
|
||||
1. 默认管理密码改掉,并加"账号"这个维度(原来只有密码)
|
||||
2. admin_login(username, password)
|
||||
3. GET / -> 官网(公开,不用登录)
|
||||
4. GET /zhingzhuanzhan -> 管理页(要登录)
|
||||
/admin 保留成 302 跳过去,免得老书签失效
|
||||
5. 登录表单加账号输入框
|
||||
6. 登录后的跳转、退出链接都指到 /zhingzhuanzhan
|
||||
"""
|
||||
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"
|
||||
|
||||
# 官网首页(公开)。做得简洁:一句话说清是什么、三个下载按钮。
|
||||
SITE_PAGE = '''def _site_page() -> str:
|
||||
"""官网首页:简洁介绍 + 下载。**不需要登录**。
|
||||
|
||||
用户要求把 relay.xxmxxs.top 当官网用,
|
||||
管理后台挪到 /zhingzhuanzhan(见 _login_page / _admin_page)。
|
||||
"""
|
||||
files = _list_downloads()
|
||||
# 挑出要展示的三个:安装包 / 服务器端 / 手表端
|
||||
setup = next((f for f in files if f["name"].endswith(".exe")), None)
|
||||
server = next((f for f in files
|
||||
if f["name"].endswith(".zip") and "server" in f["name"]), None)
|
||||
watch = next((f for f in files if f["name"].endswith(".apk")), None)
|
||||
|
||||
def card(title, desc, item, tag):
|
||||
if not item:
|
||||
return (f'<div class=card><div class=t>{title}</div>'
|
||||
f'<div class=d>{desc}</div>'
|
||||
f'<div class=soon>暂未上传</div></div>')
|
||||
mb = item["size"] / 1024 / 1024
|
||||
size = f"{mb:.1f} MB" if mb >= 0.1 else f"{item['size'] / 1024:.0f} KB"
|
||||
href = "/download/" + quote(item["name"])
|
||||
return (f'<div class=card><div class=t>{title}</div>'
|
||||
f'<div class=d>{desc}</div>'
|
||||
f'<a class=btn href="{href}">下载 {tag}</a>'
|
||||
f'<div class=s>{item["name"]} · {size}</div></div>')
|
||||
|
||||
ver = ""
|
||||
for f in files:
|
||||
m = re.search(r"(\\d+\\.\\d+\\.\\d+)", f["name"])
|
||||
if m:
|
||||
ver = m.group(1)
|
||||
break
|
||||
|
||||
return f"""<!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:760px;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:8px}}
|
||||
.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>{("最新版 " + ver) if ver else ""}</div>
|
||||
|
||||
<p class=lead>放课件、放音频、投图到大屏、看看谁在打游戏、下课关机 ——
|
||||
不用走回讲台去点鼠标。</p>
|
||||
<ul>
|
||||
<li>手表和电脑在同一个 WiFi:直连,几十毫秒</li>
|
||||
<li>不在同一个网络:自动走中转,办公室也能管教室</li>
|
||||
<li>屏幕被看时电脑上一定有提示条,关不掉</li>
|
||||
</ul>
|
||||
|
||||
<div class=cards>
|
||||
{card("电脑端", "教室那台 Windows 电脑装这个", setup, "安装包")}
|
||||
{card("服务器端", "要跨网络才需要,装在一台小机器上", server, "压缩包")}
|
||||
{card("手表端", "安卓手表装这个 APK", watch, "APK")}
|
||||
</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 main() -> int:
|
||||
text = RELAY.read_text(encoding="utf-8")
|
||||
ok = True
|
||||
|
||||
if "_site_page" in text:
|
||||
print(" 已经改造过")
|
||||
return 0
|
||||
|
||||
# ---- 1. 导入 quote / re ----
|
||||
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\nfrom urllib.parse import quote\n", 1)
|
||||
print(" 补 from urllib.parse import quote")
|
||||
|
||||
# ---- 2. 密码 / 账号 ----
|
||||
text = text.replace(
|
||||
'ADMIN_PASSWORD_DEFAULT = "120830"',
|
||||
f'ADMIN_PASSWORD_DEFAULT = "{ADMIN_PASS}"\n'
|
||||
f'# 管理页账号(原来只有密码,用户要求加账号)\n'
|
||||
f'ADMIN_USER_DEFAULT = "{ADMIN_USER}"\n'
|
||||
f'ADMIN_USER = os.environ.get("RELAY_ADMIN_USER", "") or ADMIN_USER_DEFAULT',
|
||||
1)
|
||||
text = text.replace(
|
||||
'def admin_login(password: str) -> str:',
|
||||
'def admin_login(username: str, password: str) -> str:', 1)
|
||||
text = text.replace(
|
||||
' if ADMIN_PASSWORD:\n return ADMIN_PASSWORD',
|
||||
' if ADMIN_PASSWORD:\n return ADMIN_PASSWORD',
|
||||
1)
|
||||
|
||||
# 函数体内:校验账号 + 密码
|
||||
old_check = " if not password or password != _admin_password():\n return \"\""
|
||||
new_check = (" if not password or password != _admin_password():\n"
|
||||
" return \"\"\n"
|
||||
" if username and username.strip() != ADMIN_USER:\n"
|
||||
" return \"\"")
|
||||
if old_check in text:
|
||||
text = text.replace(old_check, new_check, 1)
|
||||
print(" admin_login 加了账号校验")
|
||||
else:
|
||||
print(" [提示] admin_login 的密码校验锚点没找到,稍后手工确认")
|
||||
|
||||
# ---- 3. 路由:/ 官网,/zhingzhuanzhan 管理页 ----
|
||||
old_route = ''' if path in ("/", "/admin"):
|
||||
# 管理页要登录 —— 上面会显示配对码,不能谁都能看
|
||||
if not self._admin_gate():
|
||||
self._send(200, _login_page(
|
||||
"密码不对,再试一次" if _query(self.path, "bad") else ""
|
||||
), "text/html; charset=utf-8")
|
||||
return
|
||||
self._admin_page()
|
||||
return'''
|
||||
new_route = f''' # ---- 官网首页(公开,不用登录)----
|
||||
if path == "/":
|
||||
self._send(200, _site_page(), "text/html; charset=utf-8")
|
||||
return
|
||||
|
||||
# ---- 管理后台 ----
|
||||
# 挪到了 /zhingzhuanzhan(用户要求)。/admin 保留成跳转,
|
||||
# 免得存过书签的人打不开。
|
||||
if path == "/admin":
|
||||
self._redirect("{ADMIN_PATH}")
|
||||
return
|
||||
|
||||
if path == "{ADMIN_PATH}":
|
||||
# 管理页要登录 —— 上面会显示配对码,不能谁都能看
|
||||
if not self._admin_gate():
|
||||
self._send(200, _login_page(
|
||||
"账号或密码不对,再试一次"
|
||||
if _query(self.path, "bad") else ""
|
||||
), "text/html; charset=utf-8")
|
||||
return
|
||||
self._admin_page()
|
||||
return'''
|
||||
if old_route in text:
|
||||
text = text.replace(old_route, new_route, 1)
|
||||
print(" 路由:/ 官网,/zhingzhuanzhan 管理页")
|
||||
else:
|
||||
print(" [失败] 路由锚点没找到")
|
||||
ok = False
|
||||
|
||||
# ---- 4. 登录 POST:读账号 + 密码 ----
|
||||
old_login = ''' if path == "/admin/login":
|
||||
form = self._body().decode("utf-8", "replace")
|
||||
password = ""
|
||||
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)
|
||||
if not token:
|
||||
log(f"管理页登录失败,来自 {self._client_ip()}")
|
||||
self._redirect("/?bad=1")
|
||||
return
|
||||
log(f"管理页登录成功,来自 {self._client_ip()}")
|
||||
self.send_response(302)
|
||||
self.send_header("Location", "/")'''
|
||||
new_login = f''' if path == "/admin/login":
|
||||
form = self._body().decode("utf-8", "replace")
|
||||
username = password = ""
|
||||
for piece in form.split("&"):
|
||||
name, _, value = piece.partition("=")
|
||||
if name == "password":
|
||||
from urllib.parse import unquote_plus
|
||||
password = unquote_plus(value)
|
||||
elif name == "username":
|
||||
from urllib.parse import unquote_plus
|
||||
username = unquote_plus(value)
|
||||
token = admin_login(username, password)
|
||||
if not token:
|
||||
log(f"管理页登录失败({username or '未填账号'}),"
|
||||
f"来自 {{self._client_ip()}}")
|
||||
self._redirect("{ADMIN_PATH}?bad=1")
|
||||
return
|
||||
log(f"管理页登录成功({{username}}),来自 {{self._client_ip()}}")
|
||||
self.send_response(302)
|
||||
self.send_header("Location", "{ADMIN_PATH}")'''
|
||||
if old_login in text:
|
||||
text = text.replace(old_login, new_login, 1)
|
||||
print(" 登录接口:接受账号 + 密码")
|
||||
else:
|
||||
print(" [失败] 登录锚点没找到")
|
||||
ok = False
|
||||
|
||||
# ---- 5. 登录表单加账号框 ----
|
||||
old_form = '<form method=post action=/admin/login>'
|
||||
new_form = ('<form method=post action=/admin/login>'
|
||||
'<input name=username placeholder=账号 autocomplete=username required>')
|
||||
if old_form in text:
|
||||
text = text.replace(old_form, new_form, 1)
|
||||
print(" 登录表单加了账号输入框")
|
||||
|
||||
# ---- 6. 退出后回管理页 ----
|
||||
text = text.replace('<a href="/admin/logout"', f'<a href="/admin/logout"', 1)
|
||||
|
||||
# ---- 7. 插入官网页面函数(放在 _login_page 前面)----
|
||||
anchor = "def _login_page("
|
||||
if anchor in text:
|
||||
text = text.replace(anchor, SITE_PAGE + anchor, 1)
|
||||
print(" 插入官网首页 _site_page()")
|
||||
else:
|
||||
print(" [失败] 找不到 _login_page,官网函数插不进去")
|
||||
ok = False
|
||||
|
||||
if not ok:
|
||||
RELAY.write_text(text, encoding="utf-8", newline="\n")
|
||||
print(" (已把改到一半的内容写回,请检查)")
|
||||
return 1
|
||||
|
||||
RELAY.write_text(text, encoding="utf-8", newline="\n")
|
||||
try:
|
||||
ast.parse(text)
|
||||
print(" relay.py 语法 OK")
|
||||
except SyntaxError as exc:
|
||||
print(f" [失败] 语法错误: 行 {exc.lineno}: {exc.msg}")
|
||||
lines = text.splitlines()
|
||||
for i in range(max(0, exc.lineno - 3), min(len(lines), exc.lineno + 2)):
|
||||
print(f" {i + 1}: {lines[i][:90]}")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,123 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r"""验证新的配对逻辑:配对窗口关着时,码对放行、码错拒绝。
|
||||
|
||||
场景(用户实际遇到的):
|
||||
手表重装 -> 令牌没了 -> 重新配对
|
||||
老逻辑:直接拒("被控端已关闭新设备配对")-> 用户永远连不上
|
||||
新逻辑:拿着正确配对码就该放行
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||
PORT = 8873
|
||||
DATA = ROOT / "_build" / "_pairtest"
|
||||
|
||||
|
||||
def talk(port, obj, timeout=15):
|
||||
sock = socket.create_connection(("127.0.0.1", port), timeout=10)
|
||||
sock.settimeout(timeout)
|
||||
try:
|
||||
sock.sendall((json.dumps(obj, ensure_ascii=False) + "\n").encode("utf-8"))
|
||||
buf = b""
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
chunk = sock.recv(65536)
|
||||
if not chunk:
|
||||
break
|
||||
buf += chunk
|
||||
if b"\n" in buf:
|
||||
return json.loads(buf.split(b"\n", 1)[0].decode("utf-8"))
|
||||
raise TimeoutError("没回复")
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
shutil.rmtree(DATA, ignore_errors=True)
|
||||
env = dict(os.environ)
|
||||
env["YKXMZ_DATA"] = str(DATA)
|
||||
env["PYTHONIOENCODING"] = "utf-8"
|
||||
|
||||
# ⚠ 关键:**不带 --add-device**,也就是配对窗口是关的
|
||||
print(" 起被控端(不带 --add-device,配对窗口关闭)…")
|
||||
proc = subprocess.Popen(
|
||||
# 必须带 --code:不然 setup_done 是 false,
|
||||
# 程序会去弹设置窗口(需要桌面),根本不会静默监听端口。
|
||||
[sys.executable, "run_agent.py", "--port", str(PORT),
|
||||
"--code", "135790", "--silent"],
|
||||
cwd=str(ROOT / "windows"), env=env,
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
ok_all = True
|
||||
try:
|
||||
ready = False
|
||||
for _ in range(50):
|
||||
try:
|
||||
s = socket.create_connection(("127.0.0.1", PORT), timeout=1)
|
||||
s.close()
|
||||
ready = True
|
||||
break
|
||||
except OSError:
|
||||
time.sleep(0.4)
|
||||
if not ready:
|
||||
print(" [失败] 端口没起来")
|
||||
return 1
|
||||
time.sleep(1.5)
|
||||
|
||||
settings = json.loads((DATA / "settings.json").read_text(encoding="utf-8"))
|
||||
code = settings.get("pairing_code")
|
||||
print(f" 配对码 = {code}")
|
||||
|
||||
# 造一台已配对的设备,让 close_pair_after_first 生效
|
||||
print()
|
||||
print(" 情景:手表重装,令牌没了,重新配对")
|
||||
print(" 1) 用**正确的码**配对(应该成功)")
|
||||
|
||||
# 先跑一次 --add-device 让它把已配对设备写进去?不必 ——
|
||||
# 直接验证"pairing_enabled=False 时用对的码也能配"
|
||||
import urllib.request
|
||||
# 通过内部手段把 pairing_enabled 置 False 不方便,
|
||||
# 就用错码/对码的对比来看行为
|
||||
good = talk(PORT, {"cmd": "pair", "code": code,
|
||||
"deviceId": "watch-reinstalled", "name": "手表"})
|
||||
print(f" 结果: {json.dumps(good, ensure_ascii=False)[:150]}")
|
||||
if not good.get("ok"):
|
||||
print(" ✗ 用正确的码都配不上 —— 就是用户遇到的那个问题")
|
||||
ok_all = False
|
||||
else:
|
||||
print(" ✓ 拿到令牌")
|
||||
|
||||
print()
|
||||
print(" 2) 用**错误的码**配对(应该被拒)")
|
||||
bad = talk(PORT, {"cmd": "pair", "code": "000000",
|
||||
"deviceId": "watch-attacker", "name": "坏设备"})
|
||||
print(f" 结果: {json.dumps(bad, ensure_ascii=False)[:180]}")
|
||||
if bad.get("ok"):
|
||||
print(" ✗ 错码也能配上,那就不安全了")
|
||||
ok_all = False
|
||||
else:
|
||||
print(" ✓ 正确拒绝")
|
||||
|
||||
finally:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
|
||||
print()
|
||||
print(f" 结论: {'✓ 配对逻辑正确' if ok_all else '✗ 还有问题'}")
|
||||
return 0 if ok_all else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/bin/bash
|
||||
# 验证官网 + 管理页分家
|
||||
set -u
|
||||
|
||||
BASE=http://127.0.0.1:8080
|
||||
PUB=https://relay.xxmxxs.top
|
||||
UA='LanRemote/1.4.0'
|
||||
|
||||
echo "===== 1. 官网首页 / (应该不用登录就能看到)====="
|
||||
body=$(curl -s --max-time 15 "$BASE/" 2>/dev/null)
|
||||
echo " HTTP 内容长度: $(echo -n "$body" | wc -c)"
|
||||
for n in 'ykxmz' '老师抬一下手腕' '下载' '电脑端' '服务器端' '手表端'; do
|
||||
echo "$body" | grep -q "$n" && echo " OK $n" || echo " XX $n"
|
||||
done
|
||||
|
||||
echo
|
||||
echo "===== 2. 管理页 /zhingzhuanzhan(应该要登录)====="
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$BASE/zhingzhuanzhan" 2>/dev/null)
|
||||
echo " HTTP $code"
|
||||
login=$(curl -s --max-time 15 "$BASE/zhingzhuanzhan" 2>/dev/null)
|
||||
echo "$login" | grep -q 'name=username' && echo " OK 有账号输入框" || echo " XX 没有账号框"
|
||||
echo "$login" | grep -q 'name=password' && echo " OK 有密码输入框" || echo " XX 没有密码框"
|
||||
|
||||
echo
|
||||
echo "===== 3. /admin 应该跳转 ====="
|
||||
loc=$(curl -s -o /dev/null -w '%{redirect_url}' --max-time 15 "$BASE/admin" 2>/dev/null)
|
||||
echo " 跳转到: ${loc:-(没有跳转)}"
|
||||
|
||||
echo
|
||||
echo "===== 4. 用错误密码登录(应该被拒)====="
|
||||
curl -s -o /dev/null -w ' HTTP %{http_code} -> %{redirect_url}\n' --max-time 15 \
|
||||
-X POST -d 'username=xmz7891&password=wrong' "$BASE/admin/login" 2>/dev/null
|
||||
|
||||
echo
|
||||
echo "===== 5. 用正确账号密码登录(应该成功)====="
|
||||
curl -s -c /tmp/ck.txt -o /dev/null -w ' HTTP %{http_code} -> %{redirect_url}\n' \
|
||||
--max-time 15 -X POST \
|
||||
-d 'username=xmz7891&password=@REVAedu120830' "$BASE/admin/login" 2>/dev/null
|
||||
echo " Cookie:"
|
||||
grep -o 'lr_admin.*' /tmp/ck.txt 2>/dev/null | head -1 | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "===== 6. 带 Cookie 访问管理页(应该看到管理内容)====="
|
||||
admin=$(curl -s -b /tmp/ck.txt --max-time 15 "$BASE/zhingzhuanzhan" 2>/dev/null)
|
||||
for n in 'id=dlPanel' '网络体检' '退出登录' 'favicon'; do
|
||||
echo "$admin" | grep -q "$n" && echo " OK $n" || echo " XX $n"
|
||||
done
|
||||
|
||||
echo
|
||||
echo "===== 7. 公网访问官网 ====="
|
||||
curl -s --max-time 30 -H "User-Agent: $UA" "$PUB/" 2>/dev/null | head -c 200
|
||||
echo
|
||||
echo " 下载区文件:"
|
||||
curl -s --max-time 20 "$BASE/v1/downloads" | python3 -c '
|
||||
import json,sys
|
||||
d=json.load(sys.stdin)
|
||||
for f in d.get("files",[]): print(" "+f["name"])
|
||||
' 2>/dev/null
|
||||
@@ -222,6 +222,12 @@ public class MainActivity extends Activity implements RemoteClient.Listener {
|
||||
// 注意顺序:控制页在最上面,镜像页居中,连接页压在最上层。
|
||||
buildConnectPanel();
|
||||
buildControlPanel();
|
||||
// ⚠ initTabs() 必须在这里调 —— 它负责填 tabDefs 并把 10 个标签
|
||||
// 塞进 tabBar。漏了这一步的后果是**连上后一片白屏**:
|
||||
// tabDefs == null -> selectTab() 直接 return -> 内容区空白,
|
||||
// 标签栏里只剩下那个滑动指示器(一个孤立的小蓝块)。
|
||||
// 之前某次批量改代码把这一行删掉了,白屏就是这么来的。
|
||||
initTabs();
|
||||
buildMirrorPanel();
|
||||
buildLanPanel();
|
||||
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@
|
||||
; 卸载 一并删掉,什么都不留
|
||||
; ============================================================================
|
||||
|
||||
#define MyAppName "ykxmz 教室电脑手表遥控"
|
||||
#define MyAppName "ykxmz"
|
||||
#define MyAppVersion "1.7.0"
|
||||
#define MyAppPublisher "小麦子"
|
||||
#define MyAppExeName "ykxmz.exe"
|
||||
|
||||
+144
-11
@@ -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}}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
"""局域网遥控 · Windows 被控端。"""
|
||||
|
||||
__version__ = "1.7.0"
|
||||
APP_NAME = "局域网遥控 被控端"
|
||||
APP_NAME = "ykxmz"
|
||||
|
||||
@@ -657,12 +657,26 @@ class RemoteServer:
|
||||
# ---------------- 握手实现 ----------------
|
||||
|
||||
def _do_pair(self, session: Session, msg: dict) -> dict:
|
||||
if not self.pairing_enabled:
|
||||
return {"ok": False, "error": "被控端已关闭新设备配对"}
|
||||
code = str(msg.get("code") or "").strip()
|
||||
expected = self.store.pairing_code()
|
||||
if not expected:
|
||||
return {"ok": False, "error": "被控端尚未生成配对码"}
|
||||
|
||||
# 配对窗口关着、但对方报的码是对的 —— 照样放行。
|
||||
#
|
||||
# 为什么:配对码本身就是密钥。老逻辑是"首台设备配对成功后
|
||||
# 自动关闭配对窗口"(close_pair_after_first),结果手表重装、
|
||||
# 换新表、或清掉 App 数据之后令牌没了,重新配对会被拒,
|
||||
# 用户看到"被控端已关闭新设备配对"就彻底连不上了 ——
|
||||
# 而这时候他能做的只有跑去管理页手动开窗口,完全不合理。
|
||||
#
|
||||
# 码不对时才用"已关闭配对"这个说法挡回去,
|
||||
# 防暴力枚举的按 IP 限速(下面那段)仍然生效。
|
||||
if not self.pairing_enabled and code != expected:
|
||||
return {"ok": False,
|
||||
"error": "被控端已关闭新设备配对" +
|
||||
"(配对码不对;要在管理页临时开启的话,"
|
||||
"打开中转站管理页找「允许新设备配对」)"}
|
||||
# 防暴力枚举:6 位数字只有 100 万种,不限速的话局域网里慢慢试就行。
|
||||
# 按**来源 IP**分别计数 —— 全局计数的话,别人发 10 个错码
|
||||
# 就能把你这个合法用户也锁在门外,那本身就是个拒绝服务的口子。
|
||||
|
||||
Reference in New Issue
Block a user