mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 18:20:43 +00:00
琚帶绔?- 淇 MTU 闂锛氬す MSS + 璺緞 MTU 鎺㈡祴锛堝叕缃戝彲鐢ㄧ巼 48% -> 100%锛?- 澶嶇敤 HTTP 杩炴帴锛堝厤鎺夋瘡娆℃柊寤?TLS 鐨?30% 鎻℃墜澶辫触锛?- 姣忓垎閽熷洖澶存壘灞€鍩熺綉锛岃嚜鍔ㄥ垏鍥炲揩 400 鍊嶇殑璺緞 - 鐢婚潰蹇冭烦甯э細闈欐鏃剁渷娴侀噺锛屽鎴风浠嶇煡閬撴祦鏄椿鐨?- 閿佸睆鏃剁粰鍑哄彲鎿嶄綔鐨勬彁绀? 鎵嬭〃绔?- 鍐呯疆 GTS Root R4 / GlobalSign Root CA锛屼慨 SSLHandshakeException - 杩炴帴閿欒缈昏瘧鎴愪汉璇濓紙璇佷功/鏃堕棿/瑙f瀽/瓒呮椂鍒嗗埆鎻愮ず锛?- 缁忎腑杞繛涓婂悗鐢ㄦ湇鍔″櫒鎶ョ殑鍦板潃鍒锋柊宸查厤瀵硅褰?- 鏂板灞€鍩熺綉杩炴帴椤甸潰銆佹湇鍔″櫒寤惰繜妫€娴? 鏈嶅姟鍣ㄧ - 鏀寔 chunked 璇锋眰浣擄紙淇瘡 16 绉掍竴娆$殑杩炴帴閿欎贡涓庡ぇ闈㈢Н 502锛?- 缃戠粶璋冧紭鑴氭湰 net-tune.sh锛堝紑鏈鸿嚜鍔ㄥ簲鐢級 - 绠$悊椤碉細鎵归噺鍒犻櫎銆佺綉缁滀綋妫€銆佷笅杞藉尯 - 闅ч亾鏀?HTTP/2 + 鍙岃繛鎺? 鏂囨。 - 璁稿彲鍗忚 / 瀹夎鍓嶅繀璇?/ 瀹夎鍚庨』鐭ワ紙浣滆€咃細灏忛害瀛愶級
513 lines
19 KiB
Python
513 lines
19 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""静默运行模式:启动时没有任何窗口。
|
|
|
|
和 AgentUI 的区别:
|
|
没有主控制窗口、没有托盘、没有终端。
|
|
但 **屏幕共享提示保留** —— 有人在看你的屏幕时,屏幕右下角会出现一条提示。
|
|
这在有配对认证的前提下依然保留,理由:让电脑前面的人知道"此刻有人在看"。
|
|
|
|
内容窗口(投图、全屏大字、白板)是**手表明确要求显示**的,所以照常工作 ——
|
|
它们不是"程序自己弹出来的窗口"。Tk 根窗口按需惰性创建并保持隐藏。
|
|
|
|
没有 Tk 也能用的能力(剪贴板、屏幕分辨率)直接用 Win32 API 实现,
|
|
不依赖界面。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import ctypes
|
|
import queue
|
|
import threading
|
|
import time
|
|
from pathlib import Path
|
|
|
|
IS_WINDOWS = False
|
|
try:
|
|
import tkinter as tk
|
|
IS_WINDOWS = True
|
|
except Exception: # noqa: BLE001
|
|
tk = None
|
|
|
|
|
|
# ======================================================================
|
|
# 不依赖 Tk 的系统能力
|
|
# ======================================================================
|
|
|
|
class _Clipboard:
|
|
"""直接读写 Windows 剪贴板,不需要 Tk。"""
|
|
|
|
CF_UNICODETEXT = 13
|
|
GMEM_MOVEABLE = 0x0002
|
|
|
|
@classmethod
|
|
def get(cls) -> str:
|
|
if not IS_WINDOWS:
|
|
return ""
|
|
user32 = ctypes.windll.user32
|
|
kernel32 = ctypes.windll.kernel32
|
|
# 64 位下必须声明,否则句柄会被截断
|
|
user32.GetClipboardData.restype = ctypes.c_void_p
|
|
kernel32.GlobalLock.restype = ctypes.c_void_p
|
|
kernel32.GlobalLock.argtypes = [ctypes.c_void_p]
|
|
kernel32.GlobalUnlock.argtypes = [ctypes.c_void_p]
|
|
|
|
if not user32.OpenClipboard(None):
|
|
return ""
|
|
try:
|
|
handle = user32.GetClipboardData(cls.CF_UNICODETEXT)
|
|
if not handle:
|
|
return ""
|
|
pointer = kernel32.GlobalLock(handle)
|
|
if not pointer:
|
|
return ""
|
|
try:
|
|
return ctypes.c_wchar_p(pointer).value or ""
|
|
finally:
|
|
kernel32.GlobalUnlock(handle)
|
|
finally:
|
|
user32.CloseClipboard()
|
|
|
|
@classmethod
|
|
def set(cls, text: str) -> bool:
|
|
if not IS_WINDOWS:
|
|
return False
|
|
user32 = ctypes.windll.user32
|
|
kernel32 = ctypes.windll.kernel32
|
|
kernel32.GlobalAlloc.restype = ctypes.c_void_p
|
|
kernel32.GlobalAlloc.argtypes = [ctypes.c_uint, ctypes.c_size_t]
|
|
kernel32.GlobalLock.restype = ctypes.c_void_p
|
|
kernel32.GlobalLock.argtypes = [ctypes.c_void_p]
|
|
kernel32.GlobalUnlock.argtypes = [ctypes.c_void_p]
|
|
user32.SetClipboardData.restype = ctypes.c_void_p
|
|
user32.SetClipboardData.argtypes = [ctypes.c_uint, ctypes.c_void_p]
|
|
|
|
data = (text + "\0").encode("utf-16-le")
|
|
handle = kernel32.GlobalAlloc(cls.GMEM_MOVEABLE, len(data))
|
|
if not handle:
|
|
return False
|
|
pointer = kernel32.GlobalLock(handle)
|
|
if not pointer:
|
|
return False
|
|
ctypes.memmove(pointer, data, len(data))
|
|
kernel32.GlobalUnlock(handle)
|
|
|
|
if not user32.OpenClipboard(None):
|
|
return False
|
|
try:
|
|
user32.EmptyClipboard()
|
|
return bool(user32.SetClipboardData(cls.CF_UNICODETEXT, handle))
|
|
finally:
|
|
user32.CloseClipboard()
|
|
|
|
|
|
# ======================================================================
|
|
# 静默 UI
|
|
# ======================================================================
|
|
|
|
class HeadlessUI:
|
|
"""给静默模式用的界面替身。
|
|
|
|
接口和 AgentUI 对齐,这样 Actions / RemoteServer 一行都不用改。
|
|
"""
|
|
|
|
def __init__(self, store, log_file: Path | None = None,
|
|
allow_content_windows: bool = True):
|
|
self.store = store
|
|
self.log_file = Path(log_file) if log_file else None
|
|
self.allow_content_windows = allow_content_windows
|
|
|
|
self._root = None # 惰性创建,见下面的 root 属性
|
|
self._tk_ready = False
|
|
self._tk_lock = threading.RLock()
|
|
self._ui_lock = threading.RLock()
|
|
self._queue: queue.Queue = queue.Queue()
|
|
|
|
self.server = None
|
|
self.state = "stopped"
|
|
self.port = 0
|
|
|
|
self._screen_on = False
|
|
self._notices = []
|
|
self._notice = None
|
|
self._notice_inner = None
|
|
self.mirroring = False
|
|
|
|
self._pump_thread: threading.Thread | None = None
|
|
self._ready = threading.Event()
|
|
self._stopping = False
|
|
|
|
# ---------------- 日志 ----------------
|
|
|
|
def log(self, message: str) -> None:
|
|
line = f"[{time.strftime('%H:%M:%S')}] {message}"
|
|
if self.log_file:
|
|
try:
|
|
with self.log_file.open("a", encoding="utf-8") as handle:
|
|
handle.write(line + "\n")
|
|
except OSError:
|
|
pass
|
|
|
|
# 别名,有些地方用 set_status
|
|
def set_status(self, text: str) -> None:
|
|
self.log(text)
|
|
|
|
def notify(self, title: str, text: str) -> None:
|
|
"""弹一个小通知窗(屏幕右下角,几秒后自动消失)。
|
|
|
|
之前这里只写日志,所以手表点「弹通知」电脑上毫无反应。
|
|
静默模式的意思是"开机不弹控制窗口",而不是
|
|
"手表明确要求显示的东西也不显示" —— 投图、全屏大字、通知
|
|
都属于"明确要求",照常显示。
|
|
"""
|
|
self.log(f"[通知] {title}: {text}")
|
|
root = self._ensure_root()
|
|
if root is None:
|
|
return
|
|
|
|
def build():
|
|
try:
|
|
win = tk.Toplevel(root)
|
|
win.title(f"通知 - {title}")
|
|
win.overrideredirect(True)
|
|
win.attributes("-topmost", True)
|
|
win.configure(bg="#ffffff")
|
|
|
|
box = tk.Frame(win, bg="#ffffff", highlightthickness=1,
|
|
highlightbackground="#d0d7de")
|
|
box.pack(fill="both", expand=True)
|
|
tk.Frame(box, bg="#185ce8", height=4).pack(fill="x")
|
|
tk.Label(box, text=title or "通知", bg="#ffffff",
|
|
fg="#1f2328", font=("Microsoft YaHei UI", 11, "bold"),
|
|
anchor="w").pack(fill="x", padx=14, pady=(10, 2))
|
|
tk.Label(box, text=text or "", bg="#ffffff", fg="#656d76",
|
|
font=("Microsoft YaHei UI", 10), anchor="w",
|
|
justify="left", wraplength=260).pack(
|
|
fill="x", padx=14, pady=(0, 12))
|
|
win.update_idletasks()
|
|
|
|
screen_w, screen_h = self.screen_size()
|
|
width = max(240, win.winfo_reqwidth())
|
|
height = win.winfo_reqheight()
|
|
win.geometry(f"+{screen_w - width - 24}+{screen_h - height - 72}")
|
|
|
|
def close(_event=None):
|
|
try:
|
|
win.destroy()
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
|
|
win.bind("<Button-1>", close)
|
|
win.after(6000, close)
|
|
except Exception as exc: # noqa: BLE001
|
|
self.log(f"通知显示失败: {exc}")
|
|
|
|
self._notices.append(None) # 先占位,build 里回填
|
|
index = len(self._notices) - 1
|
|
|
|
def build():
|
|
try:
|
|
win = tk.Toplevel(root)
|
|
win.title(f"通知 - {title}")
|
|
win.overrideredirect(True)
|
|
win.attributes("-topmost", True)
|
|
win.configure(bg="#ffffff")
|
|
self._notices[index] = win
|
|
|
|
box = tk.Frame(win, bg="#ffffff", highlightthickness=1,
|
|
highlightbackground="#d0d7de")
|
|
box.pack(fill="both", expand=True)
|
|
tk.Frame(box, bg="#185ce8", height=4).pack(fill="x")
|
|
tk.Label(box, text=title or "通知", bg="#ffffff",
|
|
fg="#1f2328", font=("Microsoft YaHei UI", 11, "bold"),
|
|
anchor="w").pack(fill="x", padx=14, pady=(10, 2))
|
|
tk.Label(box, text=text or "", bg="#ffffff", fg="#656d76",
|
|
font=("Microsoft YaHei UI", 10), anchor="w",
|
|
justify="left", wraplength=260).pack(
|
|
fill="x", padx=14, pady=(0, 12))
|
|
win.update_idletasks()
|
|
|
|
screen_w, screen_h = self.screen_size()
|
|
width = max(240, win.winfo_reqwidth())
|
|
height = win.winfo_reqheight()
|
|
win.geometry(f"+{screen_w - width - 24}+{screen_h - height - 72}")
|
|
|
|
def close(_event=None):
|
|
try:
|
|
win.destroy()
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
try:
|
|
self._notices[index] = None
|
|
except (IndexError, AttributeError):
|
|
pass
|
|
|
|
win.bind("<Button-1>", close)
|
|
win.after(6000, close)
|
|
except Exception as exc: # noqa: BLE001
|
|
self.log(f"通知显示失败: {exc}")
|
|
|
|
try:
|
|
self._queue.put(build)
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
|
|
def close_notifications(self) -> bool:
|
|
"""关掉所有还在显示的通知窗。返回是否关掉了至少一个。"""
|
|
handled = False
|
|
for index, win in enumerate(list(getattr(self, "_notices", []))):
|
|
if win is None:
|
|
continue
|
|
handled = True
|
|
|
|
def build(target=win, slot=index):
|
|
try:
|
|
target.destroy()
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
try:
|
|
self._notices[slot] = None
|
|
except (IndexError, AttributeError):
|
|
pass
|
|
|
|
try:
|
|
self._queue.put(build)
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
return handled
|
|
|
|
# ---------------- Tk 根窗口(惰性、隐藏)----------------
|
|
|
|
@property
|
|
def root(self):
|
|
"""Tk 根窗口,按需创建且**始终隐藏**。
|
|
|
|
AgentUI 已经删掉了,但 Actions 里投图/全屏大字这些还要一个父窗口。
|
|
这里惰性建一个 withdrew 的根窗口 —— 它本身永远不显示,
|
|
所以启动时屏幕上依然什么都没有。
|
|
"""
|
|
return self._ensure_root()
|
|
|
|
@root.setter
|
|
def root(self, value):
|
|
self._root = value
|
|
|
|
def _ensure_root(self):
|
|
"""按需启动界面线程,并等它把**隐藏的** Tk 根窗口建好。
|
|
|
|
关键:Tk 解释器必须在**同一个线程**里创建和使用。
|
|
所以根窗口由泵线程自己建 —— 如果在主线程建、在泵线程 update,
|
|
Tkinter 会因为线程亲和性直接抛异常,泵线程当场就死了。
|
|
"""
|
|
if not IS_WINDOWS or tk is None:
|
|
return None
|
|
if threading.current_thread() is self._pump_thread:
|
|
return self._root # 已经在界面线程里
|
|
|
|
with self._tk_lock:
|
|
if self._root is not None:
|
|
return self._root
|
|
if self._pump_thread is None:
|
|
self._ready.clear()
|
|
self._pump_thread = threading.Thread(
|
|
target=self._pump, daemon=True, name="headless-ui")
|
|
self._pump_thread.start()
|
|
self._ready.wait(8.0)
|
|
return self._root
|
|
|
|
def _pump(self) -> None:
|
|
"""界面线程:Tk 在这里建,也只有这里能碰它。"""
|
|
try:
|
|
root = tk.Tk()
|
|
root.withdraw()
|
|
root.title("")
|
|
except Exception as exc: # noqa: BLE001
|
|
self.log(f"创建界面根窗口失败: {exc}")
|
|
self._ready.set()
|
|
return
|
|
|
|
self._root = root
|
|
self._ready.set()
|
|
|
|
while not self._stopping:
|
|
try:
|
|
task = self._queue.get(timeout=0.05)
|
|
except queue.Empty:
|
|
task = None
|
|
if task is not None:
|
|
try:
|
|
task()
|
|
except Exception as exc: # noqa: BLE001
|
|
self.log(f"界面任务出错: {exc}")
|
|
try:
|
|
root.update()
|
|
except Exception: # noqa: BLE001
|
|
break
|
|
|
|
try:
|
|
root.destroy()
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
self._root = None
|
|
|
|
def ui_call(self, fn, timeout: float = 8.0):
|
|
"""在界面线程里执行 fn。
|
|
|
|
Tk 只允许在拥有它的那个线程里操作,所以这里一律丢给泵线程;
|
|
调用方本来就在泵线程里的话就地执行(否则会自己等自己)。
|
|
"""
|
|
if threading.current_thread() is self._pump_thread:
|
|
return fn()
|
|
|
|
root = self._ensure_root()
|
|
if root is None:
|
|
return fn() # 没有 Tk 环境(非 Windows),就地执行
|
|
|
|
result: list = []
|
|
error: list = []
|
|
done = threading.Event()
|
|
|
|
def task():
|
|
try:
|
|
result.append(fn())
|
|
except Exception as exc: # noqa: BLE001
|
|
error.append(exc)
|
|
finally:
|
|
done.set()
|
|
|
|
self._queue.put(task)
|
|
if not done.wait(timeout):
|
|
raise TimeoutError("界面响应超时")
|
|
if error:
|
|
raise error[0]
|
|
return result[0] if result else None
|
|
|
|
def screen_size(self) -> tuple:
|
|
if IS_WINDOWS:
|
|
try:
|
|
user32 = ctypes.windll.user32
|
|
return (user32.GetSystemMetrics(0), user32.GetSystemMetrics(1))
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
return (1920, 1080)
|
|
|
|
def center_window(self, win, width: int = 0, height: int = 0) -> None:
|
|
pass # 静默模式不做居中
|
|
|
|
# ---------------- 服务端会用到的回调 ----------------
|
|
|
|
def on_state_changed(self, state: str) -> None:
|
|
self.state = state
|
|
|
|
def flash_screen_indicator(self, seconds: float = 2.5) -> None:
|
|
self.log("(截屏)")
|
|
|
|
def set_screen_sharing(self, on: bool) -> None:
|
|
"""屏幕共享提示。
|
|
|
|
这是**故意保留**的:有人在看你的屏幕时,屏幕右下角会出现一条提示。
|
|
静默模式只是不显示主控制窗口,不是"让被看的人不知道"。
|
|
"""
|
|
if on == self._screen_on:
|
|
return
|
|
self._screen_on = on
|
|
try:
|
|
if on:
|
|
self._show_notice("屏幕正在被共享")
|
|
else:
|
|
self._hide_notice()
|
|
except Exception as exc: # noqa: BLE001
|
|
self.log(f"共享提示失败: {exc}")
|
|
|
|
def refresh_sharing_notice(self, status: dict, sharing: bool) -> None:
|
|
self.set_screen_sharing(sharing)
|
|
|
|
def refresh(self) -> None:
|
|
pass
|
|
|
|
def open_diagnostics(self, from_watch: str = "") -> None:
|
|
"""手表请求自检:静默模式下不弹窗口,只记日志。"""
|
|
self.log(f"手表 {from_watch or '?'} 请求了网络自检(静默模式不弹窗口)")
|
|
|
|
def _show_notice(self, text: str) -> None:
|
|
root = self._ensure_root()
|
|
if root is None:
|
|
return
|
|
|
|
def build():
|
|
win = tk.Toplevel(root)
|
|
# 标题要设 —— 无边框窗口在外观上看不到,
|
|
# 但系统里能查到这个标题,是"确实显示了提示"的可验证证据。
|
|
win.title(text)
|
|
win.overrideredirect(True)
|
|
win.attributes("-topmost", True)
|
|
win.configure(bg="#cf222e")
|
|
label = tk.Label(win, text=text, bg="#cf222e", fg="#ffffff",
|
|
font=("Microsoft YaHei UI", 11, "bold"),
|
|
padx=16, pady=8)
|
|
label.pack()
|
|
screen_w, screen_h = self.screen_size()
|
|
win.update_idletasks()
|
|
width = win.winfo_reqwidth()
|
|
height = win.winfo_reqheight()
|
|
win.geometry(f"+{screen_w - width - 24}+{screen_h - height - 72}")
|
|
self._notice = win
|
|
|
|
try:
|
|
self._queue.put(build)
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
|
|
def _hide_notice(self) -> None:
|
|
win = self._notice
|
|
self._notice = None
|
|
if win is None:
|
|
return
|
|
|
|
def destroy():
|
|
try:
|
|
win.destroy()
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
|
|
try:
|
|
self._queue.put(destroy)
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
|
|
# ---------------- 设置窗口等:静默模式下没有 ----------------
|
|
|
|
def open_settings(self) -> None:
|
|
self.log("静默模式没有设置窗口,请改用带界面的模式或直接编辑 data/settings.json")
|
|
|
|
def open_help(self) -> None:
|
|
self.log("静默模式没有说明窗口")
|
|
|
|
def open_log(self) -> None:
|
|
self.log("静默模式没有日志窗口;日志写在 data/silent.log")
|
|
|
|
def show_message(self, text: str) -> None:
|
|
self.log(text)
|
|
|
|
def show_text_window(self, title: str, text: str) -> None:
|
|
self.log(f"[{title}] {text}")
|
|
|
|
def quit_app(self) -> None:
|
|
self.stop()
|
|
|
|
def stop(self) -> None:
|
|
self._stopping = True
|
|
# 不要在非界面线程里 destroy 根窗口 —— Tcl 会报
|
|
# "async handler deleted by the wrong thread"。
|
|
# 让泵线程自己收尾(它的循环退出时会 destroy)。
|
|
self._hide_notice()
|
|
thread = self._pump_thread
|
|
if thread is not None and thread.is_alive():
|
|
thread.join(timeout=2.0)
|
|
self._root = None
|
|
|
|
# ---------------- 静默模式自己的控制 ----------------
|
|
|
|
def run_forever(self) -> None:
|
|
"""一直跑着,直到收到停止信号。"""
|
|
while not self._stopping:
|
|
time.sleep(0.5)
|