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 + 双连接

文档
- 许可协议 / 安装前必读 / 安装后须知(作者:小麦子)
This commit is contained in:
xmz7891
2026-10-06 19:01:05 +08:00
commit b62ac865ea
3195 changed files with 203292 additions and 0 deletions
+635
View File
@@ -0,0 +1,635 @@
# -*- coding: utf-8 -*-
"""修 features.py 里审计出来的 bug。
安全(严重)
1. 删除到回收站用 PowerShell 拼路径 —— 文件名带单引号就能注入任意命令
2. 播放视频走 cmd /c start —— 路径带 & 或 | 就能执行任意命令
数据丢失(严重)
3. 记笔记前没有先 load —— 进程重启后第一次记笔记会把历史笔记全部抹掉
崩溃(应抛 ValueError 却抛别的)
4-9. rename / make_dir / copy / move / list_dir / zip_dir / folder_size
的 OSError 家族全部直接冒泡给用户
行为不对
10. 全屏功能在界面关闭时抛 RuntimeError;「关掉全屏内容」其实没关
11. 数字雨的打标签失效,画布条目无限增长
12. 网络速度显示的是开机以来累计流量,不是速度
13. 世界时钟写死时区偏移,忽略夏令时
14. on 参数传字符串 "false" 会被当成 True
15. 设置文件被改坏时,笔记/待办/收藏夹列表会崩
16. 同名复制/移动静默覆盖目标
17. 下载文件到手表返回 "READY:路径",但没人解析 → 功能名不符实
"""
from __future__ import annotations
import sys
from pathlib import Path
FEATURES = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\windows"
r"\lanremote\features.py")
FIXES = []
def fix(old, new, tag):
FIXES.append((old, new, tag))
# ======================================================================
# 1+2. 安全:不再把路径拼进 shell
# ======================================================================
fix(
''' def _do_delete_file(self, params) -> str:
"""删除到回收站,不做永久删除。"""
target = self.resolve_in_roots(str(params.get("path") or ""))
if not target.exists():
raise ValueError("文件不存在")
if IS_WINDOWS:
script = (f"Add-Type -AssemblyName Microsoft.VisualBasic;"
f"[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile("
f"'{target}', 'OnlyErrorDialogs', 'SendToRecycleBin')")
if target.is_dir():
script = (f"Add-Type -AssemblyName Microsoft.VisualBasic;"
f"[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteDirectory("
f"'{target}', 'OnlyErrorDialogs', 'SendToRecycleBin')")
code, out = _run(["powershell", "-NoProfile", "-Command", script])
if code != 0:
raise ValueError(f"删除失败: {out[:80]}")
else:
target.unlink()
return f"已删除到回收站:{target.name}"''',
''' def _recycle(self, target: Path) -> None:
"""删除到回收站。
之前是把路径用单引号拼进 PowerShell 脚本,文件名里带一个单引号
就能闭合字符串、注入任意命令(新建文件夹的清洗正则不删单引号,
所以远端两步就能造出这种文件名)。现在直接调 Windows 自己的
SHFileOperationW,不经过任何 shell,没有注入面。
"""
class SHFILEOPSTRUCTW(ctypes.Structure):
_fields_ = [("hwnd", ctypes.c_void_p),
("wFunc", ctypes.c_uint),
("pFrom", ctypes.c_wchar_p),
("pTo", ctypes.c_wchar_p),
("fFlags", ctypes.c_ushort),
("fAnyOperationsAborted", ctypes.c_int),
("hNameMappings", ctypes.c_void_p),
("lpszProgressTitle", ctypes.c_wchar_p)]
FO_DELETE = 3
FOF_ALLOWUNDO = 0x0040
FOF_NOCONFIRMATION = 0x0010
FOF_SILENT = 0x0004
FOF_NOERRORUI = 0x0400
# pFrom 要双 \0 结尾
raw = str(target) + "\\0"
buffer = ctypes.create_unicode_buffer(len(raw) + 1)
buffer[:len(raw)] = raw
op = SHFILEOPSTRUCTW()
op.wFunc = FO_DELETE
op.pFrom = ctypes.cast(buffer, ctypes.c_wchar_p)
op.fFlags = (FOF_ALLOWUNDO | FOF_NOCONFIRMATION
| FOF_SILENT | FOF_NOERRORUI)
result = ctypes.windll.shell32.SHFileOperationW(ctypes.byref(op))
if result != 0:
raise ValueError(f"删除失败(系统错误码 {result})")
def _do_delete_file(self, params) -> str:
"""删除到回收站,不做永久删除。"""
target = self.resolve_in_roots(str(params.get("path") or ""))
if not target.exists():
raise ValueError("文件不存在")
try:
if IS_WINDOWS:
self._recycle(target)
else:
raise ValueError("仅支持 Windows")
except ValueError:
raise
except OSError as exc:
raise ValueError(f"删除失败: {exc}") from exc
return f"已删除到回收站:{target.name}"''',
"删除到回收站:改用 Windows API,不再拼 shell",
)
fix(
''' 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 _do_play_video(self, params) -> str:
target = self.resolve_in_roots(str(params.get("path") or ""))
if not target.is_file():
raise ValueError("视频文件不存在")
# 之前走 cmd /c start,路径里的 & | ^ 会被 cmd 当成命令分隔符,
# 一个文件名就能变成任意命令执行。os.startfile 不经过 shell。
try:
os.startfile(str(target)) # noqa: S606
except OSError as exc:
raise ValueError(f"打不开: {exc}") from exc
return f"已用播放器打开 {target.name}"''',
"播放视频:改用 os.startfile,不再走 cmd",
)
# ======================================================================
# 3. 笔记:先 load 再写(否则会抹掉历史)
# ======================================================================
fix(
''' self._notes.insert(0, {"text": text[:2000],
"at": time.strftime("%m-%d %H:%M")})
del self._notes[200:]
self._persist("notes", self._notes)''',
''' # 必须先 load。__init__ 里 self._notes 是空的,
# 直接 insert 再 persist 会把历史笔记全抹掉(重启后第一次记笔记就中招)。
self._notes = self._load("notes")
self._notes.insert(0, {"text": text[:2000],
"at": time.strftime("%m-%d %H:%M")})
del self._notes[200:]
self._persist("notes", self._notes)''',
"记笔记:先读回来再写,不再抹掉历史",
)
# ======================================================================
# 4-6. 文件操作:OSError 一律转 ValueError + 边界检查
# ======================================================================
fix(
''' target = parent / name
target.mkdir(parents=True, exist_ok=True)
return f"已新建 {target}"''',
''' if not parent.is_dir():
raise ValueError("父目录不是文件夹")
target = parent / name
if target.exists():
raise ValueError("同名文件或文件夹已存在")
try:
target.mkdir(parents=True, exist_ok=False)
except OSError as exc:
raise ValueError(f"新建失败: {exc}") from exc
return f"已新建 {target}"''',
"新建文件夹:父目录校验 + 错误包装",
)
fix(
''' target = source.parent / name
if target.exists():
raise ValueError("同名文件已存在")
source.rename(target)
return f"已重命名为 {target.name}"''',
''' if not source.exists():
raise ValueError("文件不存在")
target = source.parent / name
if target.exists():
raise ValueError("同名文件已存在")
try:
source.rename(target)
except OSError as exc:
raise ValueError(f"重命名失败: {exc}") from exc
return f"已重命名为 {target.name}"''',
"重命名:源不存在检查 + 错误包装",
)
fix(
''' if not source.exists():
raise ValueError("源文件不存在")
if source.is_dir():
_shutil.copytree(source, target, dirs_exist_ok=True)
else:
target.parent.mkdir(parents=True, exist_ok=True)
_shutil.copy2(source, target)
return f"已复制到 {target}"''',
''' if not source.exists():
raise ValueError("源文件不存在")
if source == target:
raise ValueError("源和目标是同一个位置")
if target.exists():
raise ValueError("目标已存在,不覆盖(先改名或删掉)")
try:
if source.is_dir():
_shutil.copytree(source, target)
else:
target.parent.mkdir(parents=True, exist_ok=True)
_shutil.copy2(source, target)
except OSError as exc:
raise ValueError(f"复制失败: {exc}") from exc
return f"已复制到 {target}"''',
"复制文件:不静默覆盖 + 错误包装",
)
fix(
''' if not source.exists():
raise ValueError("源文件不存在")
target.parent.mkdir(parents=True, exist_ok=True)
_shutil.move(str(source), str(target))
return f"已移动到 {target}"''',
''' if not source.exists():
raise ValueError("源文件不存在")
if source == target:
raise ValueError("源和目标是同一个位置")
if target.exists():
raise ValueError("目标已存在,不覆盖(先改名或删掉)")
try:
target.parent.mkdir(parents=True, exist_ok=True)
_shutil.move(str(source), str(target))
except OSError as exc:
raise ValueError(f"移动失败: {exc}") from exc
return f"已移动到 {target}"''',
"移动文件:不静默覆盖 + 错误包装",
)
fix(
''' entries = sorted(target.iterdir(), key=lambda p: (p.is_file(), p.name.lower()))''',
''' try:
entries = sorted(target.iterdir(),
key=lambda p: (p.is_file(), p.name.lower()))
except OSError as exc:
raise ValueError(f"读不了这个文件夹: {exc}") from exc''',
"浏览文件夹:权限错误包装",
)
fix(
''' out = source.parent / f"{source.name}.zip"
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as zf:
for item in source.rglob("*"):
if item.is_file():
zf.write(item, item.relative_to(source.parent))
return f"已压缩为 {out.name}({out.stat().st_size // 1024} KB)"''',
''' out = source.parent / f"{source.name}.zip"
if out.exists():
raise ValueError("同名的 zip 已存在,先改名或删掉")
try:
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as zf:
for item in source.rglob("*"):
if item.is_file():
zf.write(item, item.relative_to(source.parent))
size_kb = out.stat().st_size // 1024
except OSError as exc:
raise ValueError(f"压缩失败: {exc}") from exc
return f"已压缩为 {out.name}({size_kb} KB)"''',
"压缩:不覆盖 + 错误包装",
)
fix(
''' total = 0
count = 0
for item in target.rglob("*"):
try:
if item.is_file():
total += item.stat().st_size
count += 1
except OSError:
continue
return f"{target.name}:{count} 个文件,共 {total / 1048576:.1f} MB"''',
''' total = 0
count = 0
deadline = time.time() + 15 # 大目录别把请求线程占死
try:
for item in target.rglob("*"):
if time.time() > deadline:
break
try:
if item.is_file():
total += item.stat().st_size
count += 1
except OSError:
continue
except OSError as exc:
raise ValueError(f"算不了: {exc}") from exc
return f"{target.name}:{count} 个文件,共 {total / 1048576:.1f} MB"''',
"文件夹大小:加超时 + 错误包装",
)
# ======================================================================
# 10. 全屏窗口:错误包装 + 能真的关掉
# ======================================================================
fix(
''' def _fullscreen_window(self, builder, seconds: float = 0.0):
"""在电脑上开一个全屏窗口。builder(win, close) 负责放内容。"""
def build():
import tkinter as tk
root = getattr(self.actions.ui, "root", None)
if root is None:
return
win = tk.Toplevel(root)
win.attributes("-fullscreen", True)
win.attributes("-topmost", True)
def close(_event=None):
try:
win.destroy()
except Exception: # noqa: BLE001
pass
builder(win, close)
win.bind("<Escape>", close)
win.bind("<Button-1>", close)
win.protocol("WM_DELETE_WINDOW", close)
if seconds > 0:
win.after(int(seconds * 1000), close)
self.actions.ui.ui_call(build)''',
''' def _close_fullscreen_windows(self) -> int:
"""关掉所有由本程序开的全屏窗口。"""
closed = 0
for closer in list(getattr(self, "_fullscreen_closers", [])):
try:
closer()
closed += 1
except Exception: # noqa: BLE001
pass
self._fullscreen_closers = []
return closed
def _fullscreen_window(self, builder, seconds: float = 0.0):
"""在电脑上开一个全屏窗口。builder(win, close) 负责放内容。
先关掉上一个:不然连着点两次「全屏大字」,窗口会一层层叠上去,
Esc 只能关最上面那个,下面的再也关不掉。
"""
self._close_fullscreen_windows()
def build():
import tkinter as tk
root = getattr(self.actions.ui, "root", None)
if root is None:
raise RuntimeError("界面不可用")
win = tk.Toplevel(root)
win.attributes("-fullscreen", True)
win.attributes("-topmost", True)
def close(_event=None):
try:
win.destroy()
except Exception: # noqa: BLE001
pass
try:
self._fullscreen_closers.remove(close)
except (ValueError, AttributeError):
pass
builder(win, close)
win.bind("<Escape>", close)
win.bind("<Button-1>", close)
win.protocol("WM_DELETE_WINDOW", close)
if seconds > 0:
win.after(int(seconds * 1000), close)
self._fullscreen_closers.append(close)
try:
self.actions.ui.ui_call(build)
except (RuntimeError, TimeoutError, AttributeError) as exc:
# 界面关了 / 忙超时 —— 转成用户看得懂的话,别把栈信息甩出去
raise ValueError(f"电脑上的界面不可用({exc})") from exc''',
"全屏窗口:先关旧的 + 错误转成人话",
)
fix(
''' def _do_big_text_clear(self, params) -> str:
return "按 Esc 或点一下就能关掉全屏窗口"''',
''' def _do_big_text_clear(self, params) -> str:
closed = self._close_fullscreen_windows()
return f"已关掉 {closed} 个全屏窗口" if closed else "当前没有全屏窗口"''',
"关掉全屏内容:真的关掉",
)
# ======================================================================
# 11. 数字雨:打上 tag,旧帧才会被擦掉
# ======================================================================
fix(
''' 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"))''',
''' 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"),
tags="fade")''',
"数字雨:补上 fade 标签(否则画布条目无限堆积)",
)
# ======================================================================
# 12. 网络速度:算差值,不再显示累计流量
# ======================================================================
fix(
''' def _do_network_speed(self, params) -> str:
script = ("Get-NetAdapterStatistics | Where-Object {$_.ReceivedBytes -gt 0} | "
"ForEach-Object { \\"$($_.Name): 收 $([math]::Round($_.ReceivedBytes/1MB,1))MB"
" 发 $([math]::Round($_.SentBytes/1MB,1))MB\\" }")
code, out = _run(["powershell", "-NoProfile", "-Command", script])
return out.replace("\\n", " | ")[:300] or "读不到"''',
''' def _do_network_speed(self, params) -> str:
"""真实速率:采两次求差,不是开机以来的累计流量。"""
script = ("(Get-NetAdapterStatistics | Measure-Object -Property "
"ReceivedBytes -Sum).Sum")
_code, out = _run(["powershell", "-NoProfile", "-Command", script])
digits = re.findall(r"\\d+", out or "")
now_bytes = int(digits[0]) if digits else 0
now = time.time()
last_bytes = getattr(self, "_net_last_bytes", None)
last_time = getattr(self, "_net_last_time", None)
self._net_last_bytes = now_bytes
self._net_last_time = now
if last_bytes is None or last_time is None or now <= last_time:
time.sleep(1.0)
_code, out = _run(["powershell", "-NoProfile", "-Command", script])
digits = re.findall(r"\\d+", out or "")
again = int(digits[0]) if digits else now_bytes
self._net_last_bytes = again
self._net_last_time = time.time()
return f"当前约 {(again - now_bytes) / 1048576:.2f} MB/s(采样 1 秒)"
elapsed = now - last_time
delta = max(0, now_bytes - last_bytes)
mbps = delta / elapsed / 1048576
return (f"下载 {mbps:.2f} MB/s({mbps * 8:.1f} Mbps)\\n"
f"这段时间共收了 {delta / 1048576:.1f} MB")''',
"网络速度:改成真实速率(两次采样求差)",
)
# ======================================================================
# 13. 世界时钟:用 zoneinfo,自动处理夏令时
# ======================================================================
fix(
''' def _do_world_clock(self, params) -> str:
now = time.time()
zones = [("北京", 8), ("伦敦", 0), ("纽约", -5), ("洛杉矶", -8),
("东京", 9), ("悉尼", 10)]
return "\\n".join(
f"{name} {time.strftime('%H:%M', time.gmtime(now + off * 3600))}"
for name, off in zones)''',
''' def _do_world_clock(self, params) -> str:
"""写死偏移会忽略夏令时(现在纽约/伦敦/悉尼都差 1 小时),改用 zoneinfo。"""
zones = [("北京", "Asia/Shanghai"), ("伦敦", "Europe/London"),
("纽约", "America/New_York"), ("洛杉矶", "America/Los_Angeles"),
("东京", "Asia/Tokyo"), ("悉尼", "Australia/Sydney")]
try:
from zoneinfo import ZoneInfo
import datetime as _dt
now = _dt.datetime.now(_dt.timezone.utc)
lines = []
for name, zone in zones:
local = now.astimezone(ZoneInfo(zone))
lines.append(f"{name} {local.strftime('%H:%M')}"
f"{'(夏令时)' if local.dst() else ''}")
return "\\n".join(lines)
except Exception: # noqa: BLE001
# 没有时区数据库时退回 UTC 偏移(可能差 1 小时)
now = time.time()
fallback = [("北京", 8), ("伦敦", 0), ("纽约", -5), ("洛杉矶", -8),
("东京", 9), ("悉尼", 10)]
return "\\n".join(
f"{n} {time.strftime('%H:%M', time.gmtime(now + o * 3600))}"
for n, o in fallback)''',
"世界时钟:用 zoneinfo 处理夏令时",
)
# ======================================================================
# 14. on 参数:字符串 "false" 不该被当成 True
# ======================================================================
fix(
''' def run(self, name: str, params: dict) -> str:''',
''' @staticmethod
def _truthy(value) -> bool:
"""字符串 "false"/"0"/"off" 不能被当成真 —— bool("false") 是 True。"""
if isinstance(value, bool):
return value
if isinstance(value, (int, float)):
return value != 0
if isinstance(value, str):
return value.strip().lower() in ("1", "true", "on", "yes", "是", "开")
return bool(value)
def run(self, name: str, params: dict) -> str:''',
"新增 _truthy 辅助",
)
for _old, _new, _tag in [
("want_on = bool(params.get(\"on\"))", "want_on = self._truthy(params.get(\"on\"))",
"夜间模式用 _truthy"),
("want = bool(params.get(\"on\"))", "want = self._truthy(params.get(\"on\"))",
"任务栏隐藏用 _truthy"),
("want = 1 if params.get(\"on\") else 0", "want = 1 if self._truthy(params.get(\"on\")) else 0",
"时钟秒数用 _truthy"),
("want = 1 if params.get(\"on\") else 2", "want = 2 if not self._truthy(params.get(\"on\")) else 1",
"隐藏文件用 _truthy"),
]:
fix(_old, _new, _tag)
# ======================================================================
# 15. _load:元素类型校验(设置文件被改坏时不崩)
# ======================================================================
fix(
''' def _load(self, key: str):
try:
value = self.store.get(f"feat_{key}")
return value if isinstance(value, list) else []
except Exception: # noqa: BLE001
return []''',
''' def _load(self, key: str, item_type=None):
"""读持久化列表。
设置文件可能被手改坏、或是旧版本残留,所以元素类型也要校验 ——
否则 notes/todos/clip_fav 的列表渲染会抛 TypeError 崩掉。
"""
try:
value = self.store.get(f"feat_{key}")
except Exception: # noqa: BLE001
return []
if not isinstance(value, list):
return []
if item_type is None:
return value
return [item for item in value if isinstance(item, item_type)]''',
"_load 元素类型校验",
)
fix("self._notes = self._load(\"notes\")",
"self._notes = self._load(\"notes\", dict)",
"笔记读回来校验 dict")
fix("todos = self._load(\"todos\")",
"todos = self._load(\"todos\", dict)",
"待办读回来校验 dict")
fix("favs = self._load(\"clip_fav\")",
"favs = self._load(\"clip_fav\", str)",
"收藏夹读回来校验 str")
# ======================================================================
# 17. 下载文件到手表:改成手表端真正能拉的
# ======================================================================
fix(
''' add("download_file", "下载文件到手表", "input", "文件",
hint="完整路径", key="path")''',
''' add("download_file", "下载文件到手表", "file", "文件",
hint="完整路径", key="path")''',
"下载文件:标记成 file 类型",
)
fix(
''' if target.stat().st_size > 30 * 1024 * 1024:
raise ValueError("文件超过 30MB,太大")
return f"READY:{target}"''',
''' size = target.stat().st_size
if size > 30 * 1024 * 1024:
raise ValueError("文件超过 30MB,太大")
readable = (f"{size / 1048576:.1f} MB" if size > 1048576
else f"{max(1, size // 1024)} KB")
return f"{target.name}({readable}),正在传到手表…"'''
,
"下载文件:返回可读文案(拉取由手表端发起)",
)
def main() -> int:
text = FEATURES.read_text(encoding="utf-8")
applied, skipped = 0, []
for old, new, tag in FIXES:
if old not in text:
skipped.append(tag)
print(f" [跳过] {tag} —— 锚点没找到")
continue
if text.count(old) > 1:
print(f" [注意] {tag} 匹配 {text.count(old)} 处,只改第一处")
text = text.replace(old, new, 1)
applied += 1
print(f" {tag}: OK")
FEATURES.write_text(text, encoding="utf-8")
print(f"\n 应用 {applied} / {len(FIXES)}")
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())