Files
ykxmz/_build/patch_fun_features.py
xmz7891 b62ac865ea 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 + 双连接

文档
- 许可协议 / 安装前必读 / 安装后须知(作者:小麦子)
2026-10-06 19:01:05 +08:00

655 lines
26 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""再加 25 个功能,重点在娱乐和极客(总数到 105)。
娱乐(课堂课间用):
弹幕、滚动字幕、抽奖、掷骰子、抛硬币、3-2-1 倒计时、
影院模式、随机放歌、放视频、计分板、加油动画
极客:
数字雨、星空、实时监控全屏、二维码、哈希、Base64、
UUID、时间戳、JSON 格式化、屏幕取色、网络测速、端口检查
"""
from __future__ import annotations
import sys
from pathlib import Path
FEATURES = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\windows"
r"\lanremote\features.py")
DEFS_ANCHOR = ' add("gpu_info", "显卡信息", "action", "媒体")'
DEFS_ADD = ''' add("gpu_info", "显卡信息", "action", "媒体")
# ---- 娱乐 ----
add("danmaku", "全屏弹幕", "input", "娱乐", hint="弹幕内容", key="text")
add("marquee", "滚动字幕", "input", "娱乐", hint="要滚动的字", key="text")
add("lottery", "抽奖", "action", "娱乐")
add("dice", "掷骰子", "action", "娱乐")
add("coin_flip", "抛硬币", "action", "娱乐")
add("countdown_321", "3-2-1 倒计时", "action", "娱乐")
add("cinema_mode", "影院模式(黑屏)", "action", "娱乐")
add("random_song", "随机放一首歌", "action", "娱乐")
add("play_video", "播放视频文件", "input", "娱乐",
hint="视频路径", key="path")
add("score_board", "显示计分板", "action", "娱乐")
add("score_change", "改分数", "input", "娱乐",
hint="A 或 B", key="team", hint2="加几分(负数减)", key2="delta")
add("cheer", "加油动画", "action", "娱乐")
# ---- 极客 ----
add("matrix_rain", "数字雨(黑客帝国)", "action", "极客")
add("starfield", "星空穿梭", "action", "极客")
add("dashboard", "全屏实时监控", "action", "极客")
add("qr_code", "生成二维码", "input", "极客",
hint="要变成二维码的内容", key="text")
add("hash_text", "算哈希(MD5/SHA)", "input", "极客",
hint="要计算的文字", key="text")
add("base64_encode", "Base64 编码", "input", "极客",
hint="原文", key="text")
add("base64_decode", "Base64 解码", "input", "极客",
hint="Base64 内容", key="text")
add("uuid_gen", "生成 UUID", "action", "极客")
add("timestamp_now", "当前时间戳", "action", "极客")
add("json_format", "JSON 格式化", "input", "极客",
hint="JSON 文本", key="text")
add("screen_color", "鼠标位置取色", "action", "极客")
add("speed_test", "网络测速", "action", "极客")
add("port_check", "检查常用端口", "input", "极客",
hint="目标 IP 或主机名", key="host")'''
IMPL_ANCHOR = " # ---------------- 持久化 ----------------"
IMPL_ADD = ''' # ==================================================================
# 娱乐
# ==================================================================
def _do_danmaku(self, params) -> str:
text = str(params.get("text") or "").strip() or "大家好"
speed = 6
def builder(win, close):
import tkinter as tk
win.configure(bg="#000000")
width = win.winfo_screenwidth()
height = win.winfo_screenheight()
canvas = tk.Canvas(win, bg="#000000", highlightthickness=0,
width=width, height=height)
canvas.pack(fill="both", expand=True")
canvas.configure(highlightthickness=0)
items = []
colors = ["#FFFFFF", "#FFD33D", "#5AC8FA", "#FF6B6B", "#8EE36A"]
for i in range(14):
y = int(height * (i + 1) / 15)
item = canvas.create_text(width + 200, y, text=text,
fill=colors[i % len(colors)],
font=("Microsoft YaHei UI", 26, "bold"),
anchor="w")
items.append([item, speed + (i % 4)])
def tick():
alive = False
for entry in items:
canvas.move(entry[0], -entry[1], 0)
x, _y = canvas.coords(entry[0])
if x > -600:
alive = True
if alive:
win.after(30, tick)
else:
close()
tick()
self._fullscreen_window(builder)
return f"弹幕已开始:{text[:20]}"
def _do_marquee(self, params) -> str:
text = str(params.get("text") or "").strip() or "欢迎"
def builder(win, close):
import tkinter as tk
win.configure(bg="#0B1020")
width = win.winfo_screenwidth()
height = win.winfo_screenheight()
canvas = tk.Canvas(win, bg="#0B1020", highlightthickness=0)
canvas.pack(fill="both", expand=True)
item = canvas.create_text(width, height // 2, text=text,
fill="#FFD33D",
font=("Microsoft YaHei UI", 60, "bold"),
anchor="w")
def tick():
canvas.move(item, -4, 0)
x, _y = canvas.coords(item)
if x < -len(text) * 70:
close()
return
win.after(25, tick)
tick()
self._fullscreen_window(builder)
return f"字幕滚动中:{text[:20]}"
def _do_lottery(self, params) -> str:
import random
names = self.store.get("roll_call_names") or []
pool = names or [str(i) for i in range(1, 51)]
def builder(win, close):
import tkinter as tk
win.configure(bg="#0B1020")
label = tk.Label(win, text="", bg="#0B1020", fg="#FFD33D",
font=("Microsoft YaHei UI", 120, "bold"))
label.pack(expand=True, fill="both")
state = {"n": 0, "winner": random.choice(pool)}
def tick():
state["n"] += 1
if state["n"] <= 22:
label.configure(text=random.choice(pool), fg="#FFD33D")
win.after(60 + state["n"] * 12, tick)
else:
label.configure(text=state["winner"], fg="#5AC8FA")
try:
win.bell()
except Exception: # noqa: BLE001
pass
tick()
self._fullscreen_window(builder)
return "抽奖中…"
def _do_dice(self, params) -> str:
import random
faces = ["⚀", "⚁", "⚂", "⚃", "⚄", "⚅"]
result = random.randint(1, 6)
def builder(win, close):
import tkinter as tk
win.configure(bg="#0B1020")
label = tk.Label(win, text="", bg="#0B1020", fg="#FFFFFF",
font=("Segoe UI Symbol", 200, "bold"))
label.pack(expand=True, fill="both")
state = {"n": 0, "final": faces[result - 1]}
def tick():
state["n"] += 1
if state["n"] <= 14:
label.configure(text=random.choice(faces))
win.after(70, tick)
else:
label.configure(text=state["final"], fg="#5AC8FA")
tick()
self._fullscreen_window(builder)
return f"掷出 {result} 点"
def _do_coin_flip(self, params) -> str:
import random
result = random.choice(["正", "反"])
def builder(win, close):
import tkinter as tk
win.configure(bg="#0B1020")
label = tk.Label(win, text="", bg="#0B1020", fg="#FFFFFF",
font=("Microsoft YaHei UI", 200, "bold"))
label.pack(expand=True, fill="both")
state = {"n": 0}
def tick():
state["n"] += 1
if state["n"] <= 16:
label.configure(text=random.choice(["正", "反"]))
win.after(80, tick)
else:
label.configure(text=result, fg="#FFD33D")
tick()
self._fullscreen_window(builder)
return f"硬币:{result}"
def _do_countdown_321(self, params) -> str:
def builder(win, close):
import tkinter as tk
win.configure(bg="#0B1020")
label = tk.Label(win, text="", bg="#0B1020", fg="#FFFFFF",
font=("Consolas", 260, "bold"))
label.pack(expand=True, fill="both")
state = {"n": 3}
def tick():
if state["n"] <= 0:
label.configure(text="开始!", fg="#8EE36A",
font=("Microsoft YaHei UI", 120, "bold"))
win.after(900, close)
return
label.configure(text=str(state["n"]),
fg="#FF6B6B" if state["n"] == 1 else "#FFFFFF")
try:
win.bell()
except Exception: # noqa: BLE001
pass
state["n"] -= 1
win.after(1000, tick)
tick()
self._fullscreen_window(builder)
return "倒计时开始"
def _do_cinema_mode(self, params) -> str:
def builder(win, close):
import tkinter as tk
win.configure(bg="#000000")
tk.Label(win, text="影院模式", bg="#000000", fg="#1A1A1A",
font=("Microsoft YaHei UI", 40, "bold")).pack(
expand=True, fill="both")
self._fullscreen_window(builder)
return "影院模式(Esc 退出)"
def _do_random_song(self, params) -> str:
import random
files = self.actions.list_local_audio()
if not files:
raise ValueError("音乐目录里没有音频文件")
picked = random.choice(files)
self.actions.play_audio(picked["path"])
return f"正在播放:{picked['name']}"
def _do_play_video(self, params) -> str:
target = self.resolve_in_roots(str(params.get("path") or ""))
if not target.is_file():
raise ValueError("视频文件不存在")
subprocess.Popen(["cmd", "/c", "start", "", str(target)],
creationflags=_NO_WINDOW)
return f"已用播放器打开 {target.name}"
def _scores(self) -> dict:
value = self.store.get("score_board")
if not isinstance(value, dict):
value = {"A": 0, "B": 0}
value.setdefault("A", 0)
value.setdefault("B", 0)
return value
def _show_score_board(self) -> None:
scores = self._scores()
def builder(win, close):
import tkinter as tk
win.configure(bg="#0B1020")
row = tk.Frame(win, bg="#0B1020")
row.pack(expand=True, fill="both")
for team, color in (("A", "#5AC8FA"), ("B", "#FFD33D")):
box = tk.Frame(row, bg="#0B1020")
box.pack(side="left", expand=True, fill="both")
tk.Label(box, text=f"{team} 队", bg="#0B1020", fg=color,
font=("Microsoft YaHei UI", 40, "bold")).pack(pady=(60, 0))
tk.Label(box, text=str(scores.get(team, 0)), bg="#0B1020",
fg="#FFFFFF", font=("Consolas", 200, "bold")).pack(
expand=True)
self._fullscreen_window(builder)
def _do_score_board(self, params) -> str:
self._show_score_board()
scores = self._scores()
return f"A 队 {scores['A']} : {scores['B']} B 队"
def _do_score_change(self, params) -> str:
team = str(params.get("team") or "A").strip().upper()
if team not in ("A", "B"):
raise ValueError("队伍只能填 A 或 B")
try:
delta = int(params.get("delta") or 0)
except (TypeError, ValueError):
raise ValueError("分数要填数字")
scores = self._scores()
scores[team] = scores[team] + delta
self.store.set("score_board", scores)
self._show_score_board()
return f"A 队 {scores['A']} : {scores['B']} B 队"
def _do_cheer(self, params) -> str:
def builder(win, close):
import tkinter as tk
import random
win.configure(bg="#0B1020")
width = win.winfo_screenwidth()
height = win.winfo_screenheight()
canvas = tk.Canvas(win, bg="#0B1020", highlightthickness=0)
canvas.pack(fill="both", expand=True)
canvas.create_text(width // 2, height // 2, text="加 油 !",
fill="#FFD33D",
font=("Microsoft YaHei UI", 120, "bold"))
colors = ["#FF6B6B", "#FFD33D", "#5AC8FA", "#8EE36A", "#FFFFFF"]
flakes = []
for _ in range(90):
x = random.randint(0, width)
y = random.randint(-height, 0)
item = canvas.create_rectangle(x, y, x + 10, y + 16,
fill=random.choice(colors),
outline="")
flakes.append(item)
def tick():
for item in flakes:
canvas.move(item, 0, 7)
_x, y = canvas.coords(item)[:2]
if y > height:
canvas.coords(item, random.randint(0, width), -20,
random.randint(0, width) + 10, 0)
win.after(40, tick)
tick()
self._fullscreen_window(builder)
return "加油!"
# ==================================================================
# 极客
# ==================================================================
def _do_matrix_rain(self, params) -> str:
def builder(win, close):
import tkinter as tk
import random
win.configure(bg="#000000")
width = win.winfo_screenwidth()
height = win.winfo_screenheight()
canvas = tk.Canvas(win, bg="#000000", highlightthickness=0)
canvas.pack(fill="both", expand=True)
font_size = 18
columns = max(1, width // font_size)
drops = [random.randint(-40, 0) for _ in range(columns)]
chars = "01アイウエオカキクケコサシスセソタチツテト"
def tick():
canvas.delete("fade")
for i, y in enumerate(drops):
char = random.choice(chars)
canvas.create_text(i * font_size + font_size // 2,
y * font_size, text=char,
fill="#00FF66",
font=("Consolas", font_size, "bold"))
drops[i] = y + 1 if y * font_size < height else random.randint(-20, 0)
win.after(60, tick)
tick()
self._fullscreen_window(builder)
return "数字雨已开始(Esc 退出)"
def _do_starfield(self, params) -> str:
def builder(win, close):
import tkinter as tk
import random
win.configure(bg="#000010")
width = win.winfo_screenwidth()
height = win.winfo_screenheight()
canvas = tk.Canvas(win, bg="#000010", highlightthickness=0)
canvas.pack(fill="both", expand=True)
stars = []
for _ in range(240):
x = random.uniform(-width, width)
y = random.uniform(-height, height)
z = random.uniform(1, width)
item = canvas.create_oval(0, 0, 0, 0, fill="#FFFFFF", outline="")
stars.append([x, y, z, item])
def tick():
for star in stars:
star[2] -= 16
if star[2] <= 1:
star[0] = random.uniform(-width, width)
star[1] = random.uniform(-height, height)
star[2] = width
k = 220 / star[2]
px = star[0] * k + width / 2
py = star[1] * k + height / 2
size = max(1, int((1 - star[2] / width) * 4))
canvas.coords(star[3], px, py, px + size, py + size)
win.after(33, tick)
tick()
self._fullscreen_window(builder)
return "星空穿梭中(Esc 退出)"
def _do_dashboard(self, params) -> str:
def builder(win, close):
import tkinter as tk
win.configure(bg="#0B1020")
frame = tk.Frame(win, bg="#0B1020")
frame.pack(expand=True, fill="both", padx=60, pady=60)
titles = {}
for key, title, color in (("cpu", "CPU", "#5AC8FA"),
("mem", "内存", "#FFD33D"),
("net", "网络", "#8EE36A")):
box = tk.Frame(frame, bg="#0B1020")
box.pack(fill="x", pady=16)
tk.Label(box, text=title, bg="#0B1020", fg=color,
font=("Microsoft YaHei UI", 28, "bold"),
width=6, anchor="w").pack(side="left")
value = tk.Label(box, text="--", bg="#0B1020", fg="#FFFFFF",
font=("Consolas", 60, "bold"), anchor="w")
value.pack(side="left", fill="x", expand=True)
titles[key] = value
state = {"last": None, "last_bytes": None}
def tick():
info = self.actions.perf()
titles["cpu"].configure(text=f"{info.get('cpu_percent', 0)} %")
titles["mem"].configure(
text=f"{info.get('mem_percent', 0)} % "
f"({info.get('mem_used_gb')}/{info.get('mem_total_gb')} GB)")
_code, out = _run(["powershell", "-NoProfile", "-Command",
"(Get-NetAdapterStatistics | Measure-Object "
"-Property ReceivedBytes -Sum).Sum"])
digits = re.findall(r"\\d+", out or "")
now_bytes = int(digits[0]) if digits else 0
if state["last_bytes"] is not None and state["last"] is not None:
delta = now_bytes - state["last_bytes"]
elapsed = max(0.1, time.time() - state["last"])
mbps = delta / elapsed / 1048576
titles["net"].configure(text=f"{mbps:.2f} MB/s")
state["last_bytes"] = now_bytes
state["last"] = time.time()
win.after(1500, tick)
tick()
self._fullscreen_window(builder)
return "实时监控已打开(Esc 退出)"
def _do_qr_code(self, params) -> str:
text = str(params.get("text") or "").strip()
if not text:
raise ValueError("内容为空")
import tempfile
import urllib.parse
import urllib.request
url = ("https://api.qrserver.com/v1/create-qr-code/?size=520x520&data="
+ urllib.parse.quote(text))
target = Path(tempfile.gettempdir()) / "_lanremote_qr.png"
try:
with urllib.request.urlopen(url, timeout=10) as resp:
target.write_bytes(resp.read())
except Exception as exc: # noqa: BLE001
raise ValueError(f"生成二维码失败(要联网): {exc}") from exc
self.actions.show_image(str(target), f"二维码 - {text[:20]}")
return f"二维码已显示(内容 {len(text)} 字)"
def _do_hash_text(self, params) -> str:
import hashlib
text = str(params.get("text") or "")
if not text:
raise ValueError("内容为空")
data = text.encode("utf-8")
return ("MD5: " + hashlib.md5(data).hexdigest()
+ "\\nSHA1: " + hashlib.sha1(data).hexdigest()
+ "\\nSHA256: " + hashlib.sha256(data).hexdigest())
def _do_base64_encode(self, params) -> str:
import base64
text = str(params.get("text") or "")
if not text:
raise ValueError("内容为空")
result = base64.b64encode(text.encode("utf-8")).decode("ascii")
self.actions.clip_set(result)
return result
def _do_base64_decode(self, params) -> str:
import base64
text = str(params.get("text") or "").strip()
if not text:
raise ValueError("内容为空")
try:
result = base64.b64decode(text).decode("utf-8", errors="replace")
except Exception as exc: # noqa: BLE001
raise ValueError(f"不是合法的 Base64: {exc}") from exc
self.actions.clip_set(result)
return result[:500]
def _do_uuid_gen(self, params) -> str:
import uuid
value = str(uuid.uuid4())
self.actions.clip_set(value)
return f"已生成并复制:\\n{value}"
def _do_timestamp_now(self, params) -> str:
now = time.time()
return (f"秒:{int(now)}\\n毫秒:{int(now * 1000)}\\n"
f"本地:{time.strftime('%Y-%m-%d %H:%M:%S')}\\n"
f"UTC:{time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(now))}")
def _do_json_format(self, params) -> str:
import json
text = str(params.get("text") or "").strip()
if not text:
raise ValueError("内容为空")
try:
obj = json.loads(text)
except ValueError as exc:
raise ValueError(f"不是合法 JSON: {exc}") from exc
pretty = json.dumps(obj, ensure_ascii=False, indent=2)
self.actions.clip_set(pretty)
return pretty[:800]
def _do_screen_color(self, params) -> str:
if not IS_WINDOWS:
raise ValueError("仅支持 Windows")
class Point(ctypes.Structure):
_fields_ = [("x", ctypes.c_long), ("y", ctypes.c_long)]
point = Point()
ctypes.windll.user32.GetCursorPos(ctypes.byref(point))
hdc = ctypes.windll.user32.GetDC(0)
color = ctypes.windll.gdi32.GetPixel(hdc, point.x, point.y)
ctypes.windll.user32.ReleaseDC(0, hdc)
if color < 0:
raise ValueError("取色失败")
r = color & 0xFF
g = (color >> 8) & 0xFF
b = (color >> 16) & 0xFF
hex_color = f"#{r:02X}{g:02X}{b:02X}"
self.actions.clip_set(hex_color)
return (f"鼠标位置 ({point.x}, {point.y})\\n"
f"颜色 {hex_color}\\nRGB({r}, {g}, {b})")
def _do_speed_test(self, params) -> str:
import urllib.request
url = "https://speed.cloudflare.com/__down?bytes=5000000"
try:
start = time.time()
with urllib.request.urlopen(url, timeout=20) as resp:
total = 0
while True:
chunk = resp.read(65536)
if not chunk:
break
total += len(chunk)
if time.time() - start > 12:
break
elapsed = max(0.01, time.time() - start)
except Exception as exc: # noqa: BLE001
raise ValueError(f"测速失败(要联网): {exc}") from exc
mbps = total * 8 / elapsed / 1_000_000
return (f"下载 {total / 1048576:.1f} MB,用时 {elapsed:.1f} 秒\\n"
f"约 {mbps:.1f} Mbps({total / elapsed / 1048576:.2f} MB/s)")
def _do_port_check(self, params) -> str:
import socket
host = str(params.get("host") or "").strip()
if not host:
raise ValueError("地址为空")
if not re.match(r"^[A-Za-z0-9_.:\\-]+$", host):
raise ValueError("地址含有不合法字符")
common = [21, 22, 23, 25, 53, 80, 110, 135, 139, 443, 445, 1433,
3306, 3389, 5432, 5900, 6379, 8080, 8765]
open_ports = []
for port in common:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(0.45)
try:
if sock.connect_ex((host, port)) == 0:
open_ports.append(port)
except OSError:
pass
finally:
sock.close()
if not open_ports:
return f"{host}:常用端口都没开"
return f"{host} 开放端口:{', '.join(str(p) for p in open_ports)}"
''' + IMPL_ANCHOR
def main() -> int:
text = FEATURES.read_text(encoding="utf-8")
changed = 0
if '"danmaku"' in text:
print(" defs 已存在,跳过")
elif DEFS_ANCHOR in text:
text = text.replace(DEFS_ANCHOR, DEFS_ADD, 1)
changed += 1
print(" defs: 已插入娱乐/极客")
else:
print(" [失败] defs 锚点未找到")
if "_do_matrix_rain" in text:
print(" 实现已存在,跳过")
elif IMPL_ANCHOR in text:
text = text.replace(IMPL_ANCHOR, IMPL_ADD, 1)
changed += 1
print(" 实现: 已插入 25 个方法")
else:
print(" [失败] 实现锚点未找到")
if changed:
FEATURES.write_text(text, encoding="utf-8")
print(f" 已写入({changed} 处)")
print("\n语法检查:")
import ast
try:
ast.parse(FEATURES.read_text(encoding="utf-8"))
print(" features.py: OK")
except SyntaxError as exc:
print(f" features.py: 语法错误 {exc}")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())