Files
ykxmz/windows/lanremote/ui.py
T
xmz7891 b705216e4a ykxmz 1.5.0锛氫慨澶嶆墜琛ㄧ TLS 璇佷功鏍¢獙 + MTU 淇缃戠粶绋冲畾鎬?
琚帶绔?- 淇 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 + 鍙岃繛鎺?
鏂囨。
- 璁稿彲鍗忚 / 瀹夎鍓嶅繀璇?/ 瀹夎鍚庨』鐭ワ紙浣滆€咃細灏忛害瀛愶級
2026-10-06 19:01:05 +08:00

408 lines
15 KiB
Python
Raw 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 -*-
"""被控端唯一的窗口:首次设置窗口(重做版)。
整个程序只有这一个窗口,而且只在**第一次运行**时出现一次:
双击 exe
→ 弹出这个窗口:设 6 位配对码 + 勾「开机自启」「放行防火墙」
→ 点确定 → 先检查这个码在服务器上有没有被占
被占就弹框让你换,换好了才继续
→ 黑终端一闪(放行防火墙)
→ 防火墙没放行成功的话,**不把码交给服务器**,并说明原因
→ 之后永远后台静默,屏幕上什么都没有
→ 手表输这 6 位数字就连上了
界面重做的要点:
* 用作者给的图标(原来是代码画的一个蓝方块)
* 分区卡片:配对码单独一块,选项单独一块,不再是一堆裸控件
* 配对码框大字号、居中、字距拉开,一眼能看清
* 状态提示带颜色(红=错误,黄=提醒,绿=通过)
* 配对码统一成 **6 位数字**(中转就是要这个格式,
以前允许 6-16 位字母数字,用户填 10 位会在检查那步卡死)
"""
from __future__ import annotations
import tkinter as tk
from pathlib import Path
# ----------------------------------------------------------------------
# 配色(浅色,和手表端一致)
# ----------------------------------------------------------------------
BG = "#eef1f6"
CARD = "#ffffff"
FG = "#1f2328"
ACCENT = "#185ce8"
ACCENT_DARK = "#0f47bd"
OK = "#1a7f37"
WARN = "#9a6700"
DANGER = "#cf222e"
DIM = "#656d76"
MUTED = "#8c959f"
BORDER = "#d8dee4"
FONT_UI = ("Microsoft YaHei UI", 10)
FONT_TITLE = ("Microsoft YaHei UI", 17, "bold")
FONT_SUB = ("Microsoft YaHei UI", 9)
FONT_SMALL = ("Microsoft YaHei UI", 9)
FONT_LABEL = ("Microsoft YaHei UI", 10, "bold")
FONT_CODE = ("Consolas", 26, "bold")
FONT_BTN = ("Microsoft YaHei UI", 10, "bold")
# 图标源图(作者给的)
ICON_SOURCE = Path(__file__).resolve().parent.parent.parent / "_build" / "icon_source.png"
def make_icon_image(size: int = 64):
"""取图标。优先用作者给的那张图,读不到再退回代码画的那个。"""
try:
from PIL import Image
if ICON_SOURCE.is_file():
img = Image.open(ICON_SOURCE).convert("RGBA")
width, height = img.size
side = min(width, height)
left = (width - side) // 2
top = (height - side) // 2
img = img.crop((left, top, left + side, top + side))
return img.resize((size, size), Image.LANCZOS)
except Exception: # noqa: BLE001
pass
return _drawn_icon(size)
ICON_BLUE = (24, 92, 232, 255)
def _drawn_icon(size: int = 64):
"""兜底:代码画一个蓝底手表。"""
from PIL import Image, ImageDraw
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
margin = max(1, size // 16)
draw.rounded_rectangle([margin, margin, size - margin, size - margin],
radius=max(2, size // 5), fill=ICON_BLUE)
cx = size // 2
box_w = int(size * 0.36)
box_h = int(size * 0.30)
top = int(size * 0.35)
strap_w = int(size * 0.16)
draw.rounded_rectangle([cx - strap_w // 2, top - int(size * 0.10),
cx + strap_w // 2, top],
radius=max(1, size // 32), fill=(255, 255, 255, 255))
draw.rounded_rectangle([cx - strap_w // 2, top + box_h,
cx + strap_w // 2, top + box_h + int(size * 0.10)],
radius=max(1, size // 32), fill=(255, 255, 255, 255))
draw.rounded_rectangle([cx - box_w // 2, top, cx + box_w // 2, top + box_h],
radius=max(2, size // 16), fill=(255, 255, 255, 255))
return img
class SetupWindow:
"""首次使用的设置窗口。
用法:
result = SetupWindow(store).show()
if result is None: # 用户关掉了
...
result["code"] # 用户设的配对码
result["autostart"] # 要不要开机自启
result["firewall"] # 要不要顺手放行防火墙
"""
CODE_LEN = 6
def __init__(self, store, suggested: str = ""):
self.store = store
self.suggested = (suggested or store.pairing_code()
or store.new_pairing_code())
self.result: dict | None = None
self.root: tk.Tk | None = None
self._after_ids: list = []
self._icon = None
# ------------------------------------------------------------------
def show(self, on_ready=None) -> dict | None:
"""弹出窗口并阻塞,直到用户点确定或关掉。
on_ready 是窗口建好之后的回调(自动化测试用)。
"""
self.root = tk.Tk()
self.root.title("ykxmz · 第一次设置")
self.root.configure(bg=BG)
self.root.resizable(False, False)
self._set_icon()
self._build()
self.root.update_idletasks()
if on_ready is not None:
def _fire():
if self._alive():
on_ready(self)
self._after_ids.append(self.root.after(300, _fire))
self._center()
self.root.lift()
self.root.attributes("-topmost", True)
def _drop_topmost():
if self._alive():
try:
self.root.attributes("-topmost", False)
except tk.TclError:
pass
self._after_ids.append(self.root.after(700, _drop_topmost))
self.root.mainloop()
for after_id in self._after_ids:
try:
self.root.after_cancel(after_id)
except Exception: # noqa: BLE001
pass
self._after_ids = []
try:
self.root.destroy()
except tk.TclError:
pass
self.root = None
return self.result
def _alive(self) -> bool:
"""窗口还在吗。after 回调必须在销毁后自己退出,否则会报
invalid command name。"""
try:
return self.root is not None and bool(self.root.winfo_exists())
except tk.TclError:
return False
def _set_icon(self) -> None:
try:
from PIL import ImageTk
self._icon = ImageTk.PhotoImage(make_icon_image(64))
self.root.iconphoto(True, self._icon)
except Exception: # noqa: BLE001
pass
def _center(self) -> None:
width = self.root.winfo_reqwidth()
height = self.root.winfo_reqheight()
screen_w = self.root.winfo_screenwidth()
screen_h = self.root.winfo_screenheight()
x = max(0, (screen_w - width) // 2)
y = max(0, (screen_h - height) // 3)
self.root.geometry(f"{width}x{height}+{x}+{y}")
# ------------------------------------------------------------------
def _card(self, parent) -> tk.Frame:
"""一个白底圆角感的卡片(tkinter 没有圆角,用边框色模拟)。
返回的是**外层** frame —— 调用处要 pack 它。
踩过的坑:一开始返回 inner,而 inner 的父控件是 outer,
于是 inner.pack() 挂回 outer,outer 自己从没被 pack,
结果卡片完全不显示(窗口高度只剩 199px)。
"""
outer = tk.Frame(parent, bg=BORDER)
inner = tk.Frame(outer, bg=CARD)
inner.pack(fill="both", expand=True, padx=1, pady=1)
return outer
def _build(self) -> None:
root = self.root
# ---- 顶部强调条 ----
tk.Frame(root, bg=ACCENT, height=5).pack(fill="x")
page = tk.Frame(root, bg=BG)
page.pack(fill="both", expand=True, padx=20, pady=18)
# ================= 标题区 =================
header = tk.Frame(page, bg=BG)
header.pack(fill="x")
try:
from PIL import ImageTk
badge = ImageTk.PhotoImage(make_icon_image(46))
icon_label = tk.Label(header, image=badge, bg=BG, bd=0)
icon_label.image = badge # 留引用
icon_label.pack(side="left", padx=(0, 12))
except Exception: # noqa: BLE001
pass
titles = tk.Frame(header, bg=BG)
titles.pack(side="left", anchor="w")
tk.Label(titles, text="ykxmz", bg=BG, fg=FG, font=FONT_TITLE).pack(
anchor="w")
tk.Label(titles, text="远程控制 · 第一次设置", bg=BG, fg=DIM,
font=FONT_SUB).pack(anchor="w")
# ================= 配对码卡片 =================
card = self._card(page)
card.pack(fill="x", pady=(16, 0))
inner = tk.Frame(card.winfo_children()[0], bg=CARD)
inner.pack(fill="x", padx=16, pady=14)
tk.Label(inner, text="配对码", bg=CARD, fg=FG, font=FONT_LABEL).pack(
anchor="w")
code_row = tk.Frame(inner, bg=CARD)
code_row.pack(fill="x", pady=(8, 0))
code_box = tk.Frame(code_row, bg="#f6f8fa",
highlightthickness=1,
highlightbackground=BORDER,
highlightcolor=ACCENT)
code_box.pack(side="left")
self.entry = tk.Entry(
code_box, bg="#f6f8fa", fg=ACCENT_DARK, relief="flat",
font=FONT_CODE, width=7, justify="center",
insertbackground=ACCENT)
self.entry.insert(0, self.suggested)
self.entry.pack(padx=10, pady=6)
self.entry.select_range(0, "end")
self.entry.focus_set()
# 只允许输入数字,且最多 6 位
self.entry.bind("<KeyRelease>", self._on_code_key)
self._flat_button(code_row, "换一个", self._reroll).pack(
side="left", padx=(10, 0))
tk.Label(inner, text="6 位数字。手表上输这 6 位就能连上这台电脑。",
bg=CARD, fg=MUTED, font=FONT_SMALL).pack(
anchor="w", pady=(8, 0))
self.status = tk.Label(inner, text="", bg=CARD, fg=DANGER,
font=FONT_SMALL, wraplength=330,
justify="left", anchor="w")
self.status.pack(fill="x", pady=(6, 0))
# ================= 选项卡片 =================
options = self._card(page)
options.pack(fill="x", pady=(12, 0))
opt_inner = tk.Frame(options.winfo_children()[0], bg=CARD)
opt_inner.pack(fill="x", padx=16, pady=12)
tk.Label(opt_inner, text="选项", bg=CARD, fg=FG,
font=FONT_LABEL).pack(anchor="w", pady=(0, 6))
self.var_auto = tk.BooleanVar(value=True)
self._check(opt_inner, "开机自动运行", self.var_auto,
"开机后在后台跑,不用手动开")
self.var_firewall = tk.BooleanVar(value=True)
self._check(opt_inner, "放行防火墙", self.var_firewall,
"会弹一次管理员确认,然后黑窗口一闪而过")
tk.Label(opt_inner,
text="没放行成功的话,配对码不会提交给服务器,"
"会告诉你怎么处理。",
bg=CARD, fg=MUTED, font=FONT_SMALL, justify="left",
wraplength=396).pack(anchor="w", pady=(6, 0))
# ================= 按钮区 =================
buttons = tk.Frame(page, bg=BG)
buttons.pack(fill="x", pady=(16, 0))
primary = tk.Button(
buttons, text="确定,开始使用", command=self._confirm,
relief="flat", bd=0, font=FONT_BTN, cursor="hand2",
bg=ACCENT, fg="#ffffff",
activebackground=ACCENT_DARK, activeforeground="#ffffff",
padx=22, pady=10)
primary.pack(side="left")
tk.Button(
buttons, text="取消", command=self._cancel,
relief="flat", bd=0, font=FONT_UI, cursor="hand2",
bg=BG, fg=DIM, activebackground="#e2e6ec", activeforeground=FG,
padx=14, pady=10).pack(side="left", padx=(8, 0))
tk.Label(page, text="设完之后程序就在后台跑,不会再出现任何窗口。"
"需要重设就跑 ykxmz.exe --setup",
bg=BG, fg=MUTED, font=FONT_SMALL).pack(anchor="w",
pady=(12, 0))
root.bind("<Return>", lambda _e: self._confirm())
root.bind("<Escape>", lambda _e: self._cancel())
root.protocol("WM_DELETE_WINDOW", self._cancel)
def _check(self, parent, title: str, variable, hint: str) -> None:
"""一行勾选框 + 灰字说明。"""
row = tk.Frame(parent, bg=CARD)
row.pack(fill="x", pady=(2, 0))
tk.Checkbutton(row, text=title, variable=variable, bg=CARD, fg=FG,
activebackground=CARD, activeforeground=FG,
selectcolor="#ffffff", font=FONT_UI, bd=0,
highlightthickness=0,
cursor="hand2").pack(side="left")
tk.Label(row, text=hint, bg=CARD, fg=MUTED,
font=FONT_SMALL).pack(side="left", padx=(8, 0))
def _flat_button(self, parent, text, command, primary: bool = False):
button = tk.Button(
parent, text=text, command=command, relief="flat", bd=0,
font=FONT_UI if not primary else FONT_BTN, cursor="hand2",
bg="#eaeef2" if not primary else ACCENT,
fg=FG if not primary else "#ffffff",
activebackground="#dfe3e8" if not primary else ACCENT_DARK,
activeforeground=FG if not primary else "#ffffff",
padx=12, pady=6)
return button
# ------------------------------------------------------------------
def _on_code_key(self, _event=None) -> None:
"""输入时过滤:只留数字,最多 6 位。"""
raw = self.entry.get()
cleaned = "".join(ch for ch in raw if ch.isdigit())[:self.CODE_LEN]
if cleaned != raw:
position = self.entry.index("insert")
self.entry.delete(0, "end")
self.entry.insert(0, cleaned)
self.entry.icursor(min(position, len(cleaned)))
if len(cleaned) == self.CODE_LEN:
self._set_status("", MUTED)
def _set_status(self, text: str, color: str) -> None:
try:
self.status.configure(text=text, fg=color)
except tk.TclError:
pass
def _reroll(self) -> None:
self.entry.delete(0, "end")
self.entry.insert(0, self.store.new_pairing_code())
self._set_status("", MUTED)
self.entry.focus_set()
def _confirm(self) -> None:
typed = self.entry.get().strip()
cleaned = "".join(ch for ch in typed if ch.isdigit())
if not cleaned:
self._set_status("请填一个 6 位数字", DANGER)
return
if len(cleaned) != self.CODE_LEN:
self._set_status(
f"配对码要正好 {self.CODE_LEN} 位数字,现在是 {len(cleaned)} 位",
DANGER)
return
self.result = {
"code": cleaned,
"autostart": bool(self.var_auto.get()),
"firewall": bool(self.var_firewall.get()),
}
try:
self.root.quit()
except tk.TclError:
pass
def _cancel(self) -> None:
self.result = None
try:
self.root.quit()
except tk.TclError:
pass