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
+257
View File
@@ -0,0 +1,257 @@
# -*- coding: utf-8 -*-
"""新增后端能力:
1. TTS 音色可选(列出系统音色 + 指定音色/语速播报)
2. 播放本机指定音频文件(MP3 等),限定在音乐目录内
3. 投图窗口标题可自定义(设置里配模板,单次也可以覆盖)
"""
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
PROTOCOL = ROOT / "windows" / "lanremote" / "protocol.py"
ACTIONS = ROOT / "windows" / "lanremote" / "actions.py"
CONFIG = ROOT / "windows" / "lanremote" / "config.py"
# ---------------------------------------------------------------- protocol
PROTO_OLD = 'CMD_SAY = "say"\n'
PROTO_NEW = ('CMD_SAY = "say"\n'
'CMD_VOICE_LIST = "voice_list"\n'
'CMD_AUDIO_LIST = "audio_list"\n'
'CMD_PLAY_LOCAL = "play_local"\n')
# ------------------------------------------------------------------ config
CONFIG_OLD = ' "image_custom_height": 0, # 自定义高度(像素),0=自动\n'
CONFIG_NEW = (' "image_custom_height": 0, # 自定义高度(像素),0=自动\n'
' "image_window_title": "", # 投图窗口标题模板,可用 {name};空=默认\n'
' "tts_voice": "", # 默认 TTS 音色,空=系统默认\n'
' "tts_rate": 0, # 语速 -10~10\n'
' "mp3_dir": "", # 本机音乐目录,空=自动找 音乐/Music\n')
# ----------------------------------------------------------------- actions
OLD_SAY = ''' def say(self, text: str) -> str:
if not text.strip():
raise ActionError("播报内容为空")
if not IS_WINDOWS:
raise ActionError("语音播报目前仅支持 Windows")
safe = text.replace("'", "''")
script = (
"Add-Type -AssemblyName System.Speech;"
"$s = New-Object System.Speech.Synthesis.SpeechSynthesizer;"
f"$s.Speak('{safe}')"
)
try:
subprocess.Popen(
["powershell", "-NoProfile", "-WindowStyle", "Hidden", "-Command", script],
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
except Exception as exc:
raise ActionError(f"TTS 启动失败: {exc}") from exc
return "已播报"
'''
NEW_SAY = ''' def list_voices(self) -> list[str]:
"""列出系统可用的 TTS 音色。"""
if not IS_WINDOWS:
return []
script = (
"Add-Type -AssemblyName System.Speech;"
"(New-Object System.Speech.Synthesis.SpeechSynthesizer)"
".GetInstalledVoices() | ForEach-Object { $_.VoiceInfo.Name }"
)
try:
proc = subprocess.run(
["powershell", "-NoProfile", "-Command", script],
capture_output=True, timeout=20,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
encoding="mbcs", errors="replace")
except Exception:
return []
return [line.strip() for line in (proc.stdout or "").splitlines() if line.strip()]
def say(self, text: str, voice: str = "", rate: int = 0) -> str:
"""语音播报。voice 为空则用系统默认(或设置里的默认音色)。"""
if not text.strip():
raise ActionError("播报内容为空")
if not IS_WINDOWS:
raise ActionError("语音播报目前仅支持 Windows")
chosen = (voice or "").strip() or str(self.store.get("tts_voice") or "").strip()
try:
speed = int(rate) if rate else int(self.store.get("tts_rate", 0) or 0)
except (TypeError, ValueError):
speed = 0
speed = max(-10, min(10, speed))
safe = text.replace("'", "''")
parts = [
"Add-Type -AssemblyName System.Speech;",
"$s = New-Object System.Speech.Synthesis.SpeechSynthesizer;",
]
if chosen:
parts.append(f"try {{ $s.SelectVoice('{chosen.replace(chr(39), chr(39) * 2)}') }} "
"catch { };")
if speed:
parts.append(f"$s.Rate = {speed};")
parts.append(f"$s.Speak('{safe}')")
script = "".join(parts)
try:
subprocess.Popen(
["powershell", "-NoProfile", "-WindowStyle", "Hidden",
"-Command", script],
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
except Exception as exc:
raise ActionError(f"TTS 启动失败: {exc}") from exc
return f"已播报({chosen or '默认音色'})"
# ------------------------------------------------------------------
# 播放本机音频文件(MP3 等)
# ------------------------------------------------------------------
AUDIO_EXTS = (".mp3", ".wav", ".m4a", ".flac", ".aac", ".ogg", ".wma")
def audio_roots(self) -> list[Path]:
"""允许播放的目录。只认音乐相关目录,避免被用来播任意系统文件。"""
roots: list[Path] = []
configured = str(self.store.get("mp3_dir") or "").strip()
if configured:
path = Path(configured)
if path.is_dir():
roots.append(path)
home = Path.home()
for name in ("Music", "音乐", "Downloads", "下载"):
path = home / name
if path.is_dir():
roots.append(path)
return roots
def list_local_audio(self, limit: int = 300) -> list[dict]:
"""列出本机可播放的音频文件。"""
items: list[dict] = []
seen: set[str] = set()
for root in self.audio_roots():
try:
paths = sorted(root.rglob("*"))
except OSError:
continue
for path in paths:
if len(items) >= limit:
return items
try:
if not path.is_file():
continue
if path.suffix.lower() not in self.AUDIO_EXTS:
continue
key = str(path).lower()
if key in seen:
continue
seen.add(key)
items.append({
"name": path.name,
"path": str(path),
"size": path.stat().st_size,
"dir": root.name,
})
except OSError:
continue
items.sort(key=lambda it: it["name"].lower())
return items
def play_local(self, path: str) -> str:
"""播放本机某个音频文件。只允许音乐目录内的文件。"""
target = Path(path)
if not target.is_file():
raise ActionError(f"文件不存在: {path}")
if target.suffix.lower() not in self.AUDIO_EXTS:
raise ActionError(f"不支持的音频格式: {target.suffix}")
allowed = False
for root in self.audio_roots():
try:
target.resolve().relative_to(root.resolve())
allowed = True
break
except (ValueError, OSError):
continue
if not allowed:
raise ActionError("只允许播放音乐目录里的文件")
self.play_audio(str(target))
return f"正在播放 {target.name}"
'''
# 投图窗口标题
OLD_TITLE = ''' win = tk.Toplevel(self.ui.root)
win.title(f"图片 - {p.name}")'''
NEW_TITLE = ''' win = tk.Toplevel(self.ui.root)
win.title(window_title)'''
OLD_SHOW_HEAD = ''' def show_image(self, path: str) -> str:
"""显示图片。
方式由设置 image_display 决定:'''
NEW_SHOW_HEAD = ''' def show_image(self, path: str, title: str = "") -> str:
"""显示图片。
title 为窗口标题(可留空,用设置里的模板;模板支持 {name})。
方式由设置 image_display 决定:'''
NEW_SHOW_HEAD_END = ''' img.thumbnail(target, Image.LANCZOS)
fullscreen = (mode == "fullscreen")'''
NEW_SHOW_HEAD_END2 = ''' img.thumbnail(target, Image.LANCZOS)
fullscreen = (mode == "fullscreen")
# 窗口标题:单次指定 > 设置里的模板 > 默认
window_title = (title or "").strip() or str(
self.store.get("image_window_title") or "").strip() or "图片 - {name}"
try:
window_title = window_title.replace("{name}", p.name)
except Exception:
window_title = f"图片 - {p.name}"'''
def main() -> int:
ok = True
def patch(path: Path, pairs, label: str) -> None:
nonlocal ok
text = path.read_text(encoding="utf-8")
for old, new, tag in pairs:
if old not in text:
print(f" [失败] {label} / {tag} 锚点未匹配")
ok = False
return
text = text.replace(old, new, 1)
path.write_text(text, encoding="utf-8")
print(f" {label}: OK")
patch(PROTOCOL, [(PROTO_OLD, PROTO_NEW, "新增命令")], "protocol.py")
patch(CONFIG, [(CONFIG_OLD, CONFIG_NEW, "新增设置")], "config.py")
patch(ACTIONS, [
(OLD_SAY, NEW_SAY, "音色+本机播放"),
(OLD_SHOW_HEAD, NEW_SHOW_HEAD, "show_image 签名"),
(NEW_SHOW_HEAD_END, NEW_SHOW_HEAD_END2, "窗口标题"),
(OLD_TITLE, NEW_TITLE, "应用标题"),
], "actions.py")
print("\n语法检查:")
import ast
for name, path in (("protocol.py", PROTOCOL), ("config.py", CONFIG),
("actions.py", ACTIONS)):
try:
ast.parse(path.read_text(encoding="utf-8"))
print(f" {name}: OK")
except SyntaxError as exc:
print(f" {name}: 语法错误 {exc}")
ok = False
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())