mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 17:10:43 +00:00
被控端 - 修复 MTU 问题:夹 MSS + 路径 MTU 探测(公网可用率 48% -> 100%) - 复用 HTTP 连接(免掉每次新建 TLS 的 30% 握手失败) - 每分钟回头找局域网,自动切回快 400 倍的路径 - 画面心跳帧:静止时省流量,客户端仍知道流是活的 - 锁屏时给出可操作的提示 手表端 - 内置 GTS Root R4 / GlobalSign Root CA,修 SSLHandshakeException - 连接错误翻译成人话(证书/时间/解析/超时分别提示) - 经中转连上后用服务器报的地址刷新已配对记录 - 新增局域网连接页面、服务器延迟检测 服务器端 - 支持 chunked 请求体(修每 16 秒一次的连接错乱与大面积 502) - 网络调优脚本 net-tune.sh(开机自动应用) - 管理页:批量删除、网络体检、下载区 - 隧道改 HTTP/2 + 双连接 文档 - 许可协议 / 安装前必读 / 安装后须知(作者:小麦子)
698 lines
29 KiB
Python
698 lines
29 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""再加 39 个功能(总数到 78)。
|
||
|
||
重点:
|
||
教学类(全屏大字 / 倒计时 / 随机点名 / 白板 / 投影设置)—— 老师的刚需
|
||
系统类(睡眠/休眠/注销/显示桌面/任务视图/重启显卡/时钟秒数/隐藏文件)
|
||
网络类(WiFi 密码 / 公网 IP / ping / 局域网设备 / 网卡列表)
|
||
文件类(复制/移动/压缩/解压/详情/回收站/文件夹大小/按类型统计)
|
||
工具类(随机密码 / 秒表 / 世界时钟 / 硬件信息 / 启动项 / 计划任务)
|
||
媒体类(定时停止 / 音量淡出 / 显卡信息)
|
||
"""
|
||
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 追加
|
||
|
||
DEFS_OLD = ''' # ---- 文件(限定自己的目录)----'''
|
||
|
||
DEFS_NEW = ''' # ---- 教学(老师的刚需)----
|
||
add("big_text", "全屏大字", "input", "教学",
|
||
hint="要显示的文字", key="text")
|
||
add("big_text_clear", "关掉全屏大字", "action", "教学")
|
||
add("countdown_screen", "全屏倒计时", "input", "教学",
|
||
hint="几分钟", key="minutes")
|
||
add("roll_call", "随机点名", "action", "教学")
|
||
add("roll_call_set", "设置点名名单", "input", "教学",
|
||
hint="名字用逗号隔开", key="names")
|
||
add("whiteboard", "全屏白板", "action", "教学")
|
||
add("display_mode", "投影 / 显示设置", "action", "教学")
|
||
add("big_text_fixed", "全屏显示「安静」", "action", "教学")
|
||
|
||
# ---- 系统控制 ----
|
||
add("minimize_all", "显示桌面", "action", "系统")
|
||
add("task_view", "任务视图", "action", "系统")
|
||
add("close_windows", "关闭所有窗口", "action", "系统")
|
||
add("clock_seconds", "任务栏时钟显示秒", "toggle", "系统",
|
||
on=self._clock_seconds())
|
||
add("hidden_files", "显示隐藏文件", "toggle", "系统",
|
||
on=self._hidden_files())
|
||
add("sleep", "睡眠", "action", "系统")
|
||
add("hibernate", "休眠", "action", "系统")
|
||
add("lock_only", "只锁屏", "action", "系统")
|
||
add("sign_out", "注销当前用户", "action", "系统")
|
||
add("restart_graphics", "重启显卡驱动", "action", "系统")
|
||
|
||
# ---- 网络 ----
|
||
add("wifi_password", "查看 WiFi 密码", "action", "网络")
|
||
add("public_ip", "查公网 IP", "action", "网络")
|
||
add("ping_host", "Ping 一个地址", "input", "网络",
|
||
hint="如 192.168.2.1", key="host")
|
||
add("arp_devices", "局域网设备列表", "action", "网络")
|
||
add("net_adapters", "网卡列表", "action", "网络")
|
||
|
||
# ---- 文件(限定自己的目录)----'''
|
||
|
||
DEFS_TAIL_OLD = ''' add("open_file", "用默认程序打开", "action", "文件")
|
||
|
||
return items'''
|
||
|
||
DEFS_TAIL_NEW = ''' add("open_file", "用默认程序打开", "input", "文件",
|
||
hint="文件路径", key="path")
|
||
add("copy_file", "复制文件", "input", "文件",
|
||
hint="源路径", key="path", hint2="目标路径", key2="to")
|
||
add("move_file", "移动文件", "input", "文件",
|
||
hint="源路径", key="path", hint2="目标路径", key2="to")
|
||
add("zip_dir", "压缩为 zip", "input", "文件",
|
||
hint="文件夹路径", key="path")
|
||
add("unzip", "解压 zip", "input", "文件",
|
||
hint="zip 文件路径", key="path")
|
||
add("file_info", "文件详情", "input", "文件",
|
||
hint="文件路径", key="path")
|
||
add("folder_size", "文件夹大小", "input", "文件",
|
||
hint="文件夹路径", key="path")
|
||
add("open_recycle", "打开回收站", "action", "文件")
|
||
add("disk_by_type", "按类型统计磁盘", "action", "文件")
|
||
|
||
# ---- 工具 ----
|
||
add("password_gen", "生成随机密码", "action", "工具")
|
||
add("stopwatch", "秒表(电脑上显示)", "action", "工具")
|
||
add("world_clock", "世界时钟", "action", "工具")
|
||
add("hardware_info", "硬件信息", "action", "工具")
|
||
add("startup_items", "开机启动项", "action", "工具")
|
||
add("scheduled_tasks", "计划任务", "action", "工具")
|
||
add("system_detail", "系统详细信息", "action", "工具")
|
||
|
||
# ---- 媒体 ----
|
||
add("stop_after", "定时停止播放", "input", "媒体",
|
||
hint="几分钟后停", key="minutes")
|
||
add("fade_volume", "音量淡出到静音", "action", "媒体")
|
||
add("gpu_info", "显卡信息", "action", "媒体")
|
||
|
||
return items'''
|
||
|
||
# ---------------------------------------------------------------- 实现追加
|
||
|
||
IMPL_ANCHOR = ''' # ---------------- 持久化 ----------------'''
|
||
|
||
IMPL_NEW = ''' # ==================================================================
|
||
# 教学
|
||
# ==================================================================
|
||
|
||
def _fullscreen_window(self, builder, seconds: float = 0.0):
|
||
"""在电脑上开一个全屏窗口。builder(win) 负责往里放内容。"""
|
||
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 _do_big_text(self, params) -> str:
|
||
text = str(params.get("text") or "").strip()
|
||
if not text:
|
||
raise ValueError("文字为空")
|
||
size = 96 if len(text) <= 8 else (64 if len(text) <= 20 else 40)
|
||
|
||
def builder(win, close):
|
||
import tkinter as tk
|
||
win.configure(bg="#0B1020")
|
||
tk.Label(win, text=text, bg="#0B1020", fg="#FFFFFF",
|
||
font=("Microsoft YaHei UI", size, "bold"),
|
||
wraplength=win.winfo_screenwidth() - 80,
|
||
justify="center").pack(expand=True, fill="both")
|
||
tk.Label(win, text="按 Esc 或点一下关闭", bg="#0B1020", fg="#55607A",
|
||
font=("Microsoft YaHei UI", 12)).place(
|
||
relx=0.5, rely=0.97, anchor="center")
|
||
|
||
self._fullscreen_window(builder)
|
||
return f"已全屏显示「{text[:20]}」"
|
||
|
||
def _do_big_text_fixed(self, params) -> str:
|
||
return self._do_big_text({"text": "安静"})
|
||
|
||
def _do_big_text_clear(self, params) -> str:
|
||
self.actions.close_image()
|
||
return "已关闭全屏内容"
|
||
|
||
def _do_countdown_screen(self, params) -> str:
|
||
try:
|
||
minutes = max(1, min(180, int(params.get("minutes") or 5)))
|
||
except (TypeError, ValueError):
|
||
minutes = 5
|
||
total = minutes * 60
|
||
|
||
def builder(win, close):
|
||
import tkinter as tk
|
||
win.configure(bg="#0B1020")
|
||
label = tk.Label(win, text="", bg="#0B1020", fg="#FFFFFF",
|
||
font=("Consolas", 160, "bold"))
|
||
label.pack(expand=True, fill="both")
|
||
hint = tk.Label(win, text=f"倒计时 {minutes} 分钟 · 按 Esc 结束",
|
||
bg="#0B1020", fg="#55607A",
|
||
font=("Microsoft YaHei UI", 14))
|
||
hint.place(relx=0.5, rely=0.95, anchor="center")
|
||
state = {"left": total}
|
||
|
||
def tick():
|
||
if state["left"] < 0:
|
||
return
|
||
mins, secs = divmod(max(0, state["left"]), 60)
|
||
label.configure(text=f"{mins:02d}:{secs:02d}",
|
||
fg="#FF6B6B" if state["left"] <= 10 else "#FFFFFF")
|
||
if state["left"] == 0:
|
||
try:
|
||
win.bell()
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
state["left"] -= 1
|
||
win.after(1000, tick)
|
||
|
||
tick()
|
||
|
||
self._fullscreen_window(builder)
|
||
return f"已开始 {minutes} 分钟倒计时"
|
||
|
||
def _do_whiteboard(self, params) -> str:
|
||
def builder(win, close):
|
||
import tkinter as tk
|
||
win.configure(bg="#FFFFFF")
|
||
canvas = tk.Canvas(win, bg="#FFFFFF", highlightthickness=0)
|
||
canvas.pack(fill="both", expand=True)
|
||
state = {"x": None, "y": None, "color": "#1F2328", "size": 4}
|
||
|
||
def press(event):
|
||
state["x"], state["y"] = event.x, event.y
|
||
|
||
def drag(event):
|
||
if state["x"] is None:
|
||
return
|
||
canvas.create_line(state["x"], state["y"], event.x, event.y,
|
||
fill=state["color"], width=state["size"],
|
||
capstyle="round")
|
||
state["x"], state["y"] = event.x, event.y
|
||
|
||
def release(_event):
|
||
state["x"] = state["y"] = None
|
||
|
||
canvas.bind("<Button-1>", press)
|
||
canvas.bind("<B1-Motion>", drag)
|
||
canvas.bind("<ButtonRelease-1>", release)
|
||
|
||
bar = tk.Frame(win, bg="#F5F7FA")
|
||
bar.place(relx=1.0, rely=0.0, anchor="ne")
|
||
|
||
def clear():
|
||
canvas.delete("all")
|
||
|
||
for text, color in (("黑", "#1F2328"), ("红", "#CF222E"),
|
||
("蓝", "#185CE8"), ("绿", "#1A7F37")):
|
||
tk.Button(bar, text=text, bg="#FFFFFF", fg=color, relief="flat",
|
||
font=("Microsoft YaHei UI", 11, "bold"), width=3,
|
||
command=lambda c=color: state.update(color=c)).pack(
|
||
side="left", padx=2, pady=4)
|
||
tk.Button(bar, text="清空", bg="#EAEEF2", relief="flat",
|
||
font=("Microsoft YaHei UI", 10),
|
||
command=clear).pack(side="left", padx=6, pady=4)
|
||
|
||
self._fullscreen_window(builder)
|
||
return "白板已打开(鼠标画,Esc 关闭)"
|
||
|
||
def _do_display_mode(self, params) -> str:
|
||
_run(["cmd", "/c", "start", "ms-settings:display"], shell=False)
|
||
return "已打开显示设置(投影模式在这里切)"
|
||
|
||
def _do_roll_call_set(self, params) -> str:
|
||
raw = str(params.get("names") or "").strip()
|
||
if not raw:
|
||
raise ValueError("名单为空")
|
||
parts = [p.strip() for p in raw.replace(",", ",").replace("、", ",")
|
||
.replace(" ", ",").split(",") if p.strip()]
|
||
if not parts:
|
||
raise ValueError("没解析出名字")
|
||
self.store.set("roll_call_names", parts)
|
||
return f"名单已存,共 {len(parts)} 人"
|
||
|
||
def _do_roll_call(self, params) -> str:
|
||
import random
|
||
names = self.store.get("roll_call_names") or []
|
||
if not names:
|
||
raise ValueError("还没有名单,先点「设置点名名单」")
|
||
picked = random.choice(names)
|
||
# 在电脑上全屏显示,让全班看到
|
||
self._do_big_text({"text": picked})
|
||
return f"点到:{picked}(共 {len(names)} 人)"
|
||
|
||
# ==================================================================
|
||
# 系统控制
|
||
# ==================================================================
|
||
|
||
def _do_minimize_all(self, params) -> str:
|
||
ctypes.windll.user32.keybd_event(0x5B, 0, 0, 0) # Win
|
||
ctypes.windll.user32.keybd_event(0x44, 0, 0, 0) # D
|
||
ctypes.windll.user32.keybd_event(0x44, 0, 2, 0)
|
||
ctypes.windll.user32.keybd_event(0x5B, 0, 2, 0)
|
||
return "已显示桌面"
|
||
|
||
def _do_task_view(self, params) -> str:
|
||
ctypes.windll.user32.keybd_event(0x5B, 0, 0, 0) # Win
|
||
ctypes.windll.user32.keybd_event(0x09, 0, 0, 0) # Tab
|
||
ctypes.windll.user32.keybd_event(0x09, 0, 2, 0)
|
||
ctypes.windll.user32.keybd_event(0x5B, 0, 2, 0)
|
||
return "已打开任务视图"
|
||
|
||
def _do_close_windows(self, params) -> str:
|
||
"""给所有可见的顶层窗口发关闭消息(相当于 Alt+F4 一轮)。"""
|
||
WM_CLOSE = 0x0010
|
||
closed = 0
|
||
user32 = ctypes.windll.user32
|
||
EnumProc = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p,
|
||
ctypes.c_void_p)
|
||
own_pid = os.getpid()
|
||
|
||
def callback(hwnd, _lparam):
|
||
nonlocal closed
|
||
if not user32.IsWindowVisible(hwnd):
|
||
return True
|
||
length = user32.GetWindowTextLengthW(hwnd)
|
||
if length == 0:
|
||
return True
|
||
pid = ctypes.c_ulong()
|
||
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
|
||
if pid.value == own_pid:
|
||
return True
|
||
cls = ctypes.create_unicode_buffer(256)
|
||
user32.GetClassNameW(hwnd, cls, 256)
|
||
if cls.value in ("Progman", "Shell_TrayWnd", "WorkerW"):
|
||
return True
|
||
user32.PostMessageW(hwnd, WM_CLOSE, 0, 0)
|
||
closed += 1
|
||
return True
|
||
|
||
user32.EnumWindows(EnumProc(callback), 0)
|
||
return f"已向 {closed} 个窗口发出关闭"
|
||
|
||
def _clock_seconds(self) -> bool:
|
||
code, out = _run(["reg", "query",
|
||
r"HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced",
|
||
"/v", "ShowSecondsInSystemClock"])
|
||
return code == 0 and "0x1" in out
|
||
|
||
def _do_clock_seconds(self, params) -> str:
|
||
want = 1 if params.get("on") else 0
|
||
_run(["reg", "add",
|
||
r"HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced",
|
||
"/v", "ShowSecondsInSystemClock", "/t", "REG_DWORD", "/d", str(want),
|
||
"/f"])
|
||
self._restart_explorer_quiet()
|
||
return "任务栏时钟已显示秒" if want else "任务栏时钟已恢复"
|
||
|
||
def _hidden_files(self) -> bool:
|
||
code, out = _run(["reg", "query",
|
||
r"HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced",
|
||
"/v", "Hidden"])
|
||
return code == 0 and ("0x1" in out.split("REG_DWORD")[-1]
|
||
if "REG_DWORD" in out else False)
|
||
|
||
def _do_hidden_files(self, params) -> str:
|
||
want = 1 if params.get("on") else 2
|
||
_run(["reg", "add",
|
||
r"HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced",
|
||
"/v", "Hidden", "/t", "REG_DWORD", "/d", str(want), "/f"])
|
||
self._restart_explorer_quiet()
|
||
return "已显示隐藏文件" if want == 1 else "已恢复不显示隐藏文件"
|
||
|
||
def _restart_explorer_quiet(self) -> None:
|
||
_run(["taskkill", "/F", "/IM", "explorer.exe"])
|
||
time.sleep(0.8)
|
||
subprocess.Popen("explorer.exe", shell=True, creationflags=_NO_WINDOW)
|
||
|
||
def _do_sleep(self, params) -> str:
|
||
_run(["rundll32.exe", "powrprof.dll,SetSuspendState", "0,1,0"])
|
||
return "已进入睡眠"
|
||
|
||
def _do_hibernate(self, params) -> str:
|
||
_run(["shutdown", "/h"])
|
||
return "已进入休眠(如果启用了休眠)"
|
||
|
||
def _do_lock_only(self, params) -> str:
|
||
ctypes.windll.user32.LockWorkStation()
|
||
return "已锁屏"
|
||
|
||
def _do_sign_out(self, params) -> str:
|
||
_run(["shutdown", "/l"])
|
||
return "已注销"
|
||
|
||
def _do_restart_graphics(self, params) -> str:
|
||
# Win + Ctrl + Shift + B
|
||
for vk in (0x5B, 0x11, 0x10, 0x42):
|
||
ctypes.windll.user32.keybd_event(vk, 0, 0, 0)
|
||
for vk in (0x42, 0x10, 0x11, 0x5B):
|
||
ctypes.windll.user32.keybd_event(vk, 0, 2, 0)
|
||
time.sleep(0.5)
|
||
return "已重启显卡驱动"
|
||
|
||
# ==================================================================
|
||
# 网络
|
||
# ==================================================================
|
||
|
||
def _do_wifi_password(self, params) -> str:
|
||
code, out = _run(["netsh", "wlan", "show", "profiles"])
|
||
if code != 0:
|
||
return "读不到 WiFi 配置"
|
||
names = []
|
||
for raw in out.splitlines():
|
||
text = raw.strip()
|
||
if text.startswith("All User Profile") or "所有用户配置文件" in text:
|
||
names.append(text.split(":", 1)[-1].strip())
|
||
if not names:
|
||
return "没有保存过 WiFi"
|
||
lines = []
|
||
for name in names[:6]:
|
||
code2, detail = _run(["netsh", "wlan", "show", "profile",
|
||
f"name={name}", "key=clear"])
|
||
pwd = ""
|
||
for raw in detail.splitlines():
|
||
text = raw.strip()
|
||
if text.startswith("Key Content") or "关键内容" in text:
|
||
pwd = text.split(":", 1)[-1].strip()
|
||
lines.append(f"{name}:{pwd or '(读不到,可能需要管理员)'}")
|
||
return "\\n".join(lines)
|
||
|
||
def _do_public_ip(self, params) -> str:
|
||
import urllib.request
|
||
for url in ("https://api.ipify.org", "https://ifconfig.me/ip"):
|
||
try:
|
||
with urllib.request.urlopen(url, timeout=6) as resp:
|
||
text = resp.read().decode("utf-8", errors="replace").strip()
|
||
if text:
|
||
return f"公网 IP:{text}"
|
||
except Exception: # noqa: BLE001
|
||
continue
|
||
return "查不到(可能没联网)"
|
||
|
||
def _do_ping_host(self, params) -> str:
|
||
host = str(params.get("host") or "").strip()
|
||
if not host:
|
||
raise ValueError("地址为空")
|
||
if not re.match(r"^[A-Za-z0-9_.:\\-]+$", host):
|
||
raise ValueError("地址含有不合法字符")
|
||
code, out = _run(["ping", "-n", "3", host], timeout=25)
|
||
lines = [l for l in out.splitlines() if l.strip()]
|
||
return "\\n".join(lines[-8:]) or "没有输出"
|
||
|
||
def _do_arp_devices(self, params) -> str:
|
||
code, out = _run(["arp", "-a"])
|
||
lines = [l.strip() for l in out.splitlines()
|
||
if l.strip() and "---" not in l and "Interface" not in l
|
||
and "接口" not in l]
|
||
return f"局域网设备 {len(lines)} 条:\\n" + "\\n".join(lines[:40])
|
||
|
||
def _do_net_adapters(self, params) -> str:
|
||
code, out = _run(["powershell", "-NoProfile", "-Command",
|
||
"Get-NetAdapter | ForEach-Object { "
|
||
"\\"$($_.Name) $($_.Status) $($_.LinkSpeed)\\" }"])
|
||
lines = [l for l in out.splitlines() if l.strip()]
|
||
return "\\n".join(lines) or "读不到网卡"
|
||
|
||
# ==================================================================
|
||
# 文件(继续限定在自己的目录内)
|
||
# ==================================================================
|
||
|
||
def _do_copy_file(self, params) -> str:
|
||
source = self.resolve_in_roots(str(params.get("path") or ""))
|
||
target = self.resolve_in_roots(str(params.get("to") or ""))
|
||
if not source.exists():
|
||
raise ValueError("源文件不存在")
|
||
import shutil as _shutil
|
||
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}"
|
||
|
||
def _do_move_file(self, params) -> str:
|
||
source = self.resolve_in_roots(str(params.get("path") or ""))
|
||
target = self.resolve_in_roots(str(params.get("to") or ""))
|
||
if not source.exists():
|
||
raise ValueError("源文件不存在")
|
||
import shutil as _shutil
|
||
target.parent.mkdir(parents=True, exist_ok=True)
|
||
_shutil.move(str(source), str(target))
|
||
return f"已移动到 {target}"
|
||
|
||
def _do_zip_dir(self, params) -> str:
|
||
import zipfile
|
||
source = self.resolve_in_roots(str(params.get("path") or ""))
|
||
if not source.is_dir():
|
||
raise ValueError("不是文件夹")
|
||
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)"
|
||
|
||
def _do_unzip(self, params) -> str:
|
||
import zipfile
|
||
source = self.resolve_in_roots(str(params.get("path") or ""))
|
||
if not source.is_file() or source.suffix.lower() != ".zip":
|
||
raise ValueError("不是 zip 文件")
|
||
target = source.parent / source.stem
|
||
target.mkdir(parents=True, exist_ok=True)
|
||
with zipfile.ZipFile(source) as zf:
|
||
zf.extractall(target)
|
||
return f"已解压到 {target.name}"
|
||
|
||
def _do_file_info(self, params) -> str:
|
||
target = self.resolve_in_roots(str(params.get("path") or ""))
|
||
if not target.exists():
|
||
raise ValueError("文件不存在")
|
||
stat = target.stat()
|
||
kind = "文件夹" if target.is_dir() else "文件"
|
||
size = stat.st_size
|
||
readable = (f"{size / 1048576:.2f} MB" if size > 1048576
|
||
else f"{size // 1024} KB")
|
||
return (f"{kind}:{target.name}\\n"
|
||
f"大小:{readable}\\n"
|
||
f"修改:{time.strftime('%Y-%m-%d %H:%M', time.localtime(stat.st_mtime))}\\n"
|
||
f"位置:{target.parent}")
|
||
|
||
def _do_folder_size(self, params) -> str:
|
||
target = self.resolve_in_roots(str(params.get("path") or ""))
|
||
if not target.is_dir():
|
||
raise ValueError("不是文件夹")
|
||
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} 个文件,"
|
||
f"共 {total / 1048576:.1f} MB")
|
||
|
||
def _do_open_recycle(self, params) -> str:
|
||
os.startfile("shell:RecycleBinFolder") # noqa: S606
|
||
return "已打开回收站"
|
||
|
||
def _do_disk_by_type(self, params) -> str:
|
||
buckets = {"图片": 0, "视频": 0, "音频": 0, "文档": 0, "压缩包": 0, "其他": 0}
|
||
exts = {
|
||
"图片": {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"},
|
||
"视频": {".mp4", ".avi", ".mkv", ".mov", ".wmv", ".flv"},
|
||
"音频": {".mp3", ".wav", ".flac", ".m4a", ".aac"},
|
||
"文档": {".doc", ".docx", ".pdf", ".ppt", ".pptx", ".xls",
|
||
".xlsx", ".txt", ".md"},
|
||
"压缩包": {".zip", ".rar", ".7z", ".tar", ".gz"},
|
||
}
|
||
for root in self.allowed_roots():
|
||
try:
|
||
for item in root.rglob("*"):
|
||
if not item.is_file():
|
||
continue
|
||
suffix = item.suffix.lower()
|
||
for name, group in exts.items():
|
||
if suffix in group:
|
||
buckets[name] += item.stat().st_size
|
||
break
|
||
else:
|
||
buckets["其他"] += item.stat().st_size
|
||
except OSError:
|
||
continue
|
||
parts = [f"{k} {v / 1048576:.0f}MB" for k, v in buckets.items() if v]
|
||
return " | ".join(parts) or "统计不到"
|
||
|
||
# ==================================================================
|
||
# 工具
|
||
# ==================================================================
|
||
|
||
def _do_password_gen(self, params) -> str:
|
||
import secrets
|
||
import string
|
||
alphabet = string.ascii_letters + string.digits + "!@#$%^&*-_"
|
||
pwd = "".join(secrets.choice(alphabet) for _ in range(16))
|
||
self.actions.clip_set(pwd)
|
||
return f"已生成并复制到剪贴板:\\n{pwd}"
|
||
|
||
def _do_stopwatch(self, params) -> str:
|
||
def builder(win, close):
|
||
import tkinter as tk
|
||
win.configure(bg="#0B1020")
|
||
label = tk.Label(win, text="00:00.0", bg="#0B1020", fg="#FFFFFF",
|
||
font=("Consolas", 140, "bold"))
|
||
label.pack(expand=True, fill="both")
|
||
win.configure(bg="#0B1020")
|
||
state = {"start": time.time()}
|
||
|
||
def tick():
|
||
elapsed = time.time() - state["start"]
|
||
mins, secs = divmod(int(elapsed), 60)
|
||
label.configure(text=f"{mins:02d}:{secs:02d}.{int(elapsed * 10) % 10}")
|
||
win.after(100, tick)
|
||
|
||
tick()
|
||
|
||
self._fullscreen_window(builder)
|
||
return "秒表已打开(Esc 关闭)"
|
||
|
||
def _do_world_clock(self, params) -> str:
|
||
now = time.time()
|
||
zones = [("北京", 8), ("伦敦", 0), ("纽约", -5), ("洛杉矶", -8),
|
||
("东京", 9), ("悉尼", 10)]
|
||
lines = []
|
||
for name, offset in zones:
|
||
local = time.gmtime(now + offset * 3600)
|
||
lines.append(f"{name} {time.strftime('%H:%M', local)}")
|
||
return "\\n".join(lines)
|
||
|
||
def _do_hardware_info(self, params) -> str:
|
||
script = (
|
||
"$c=Get-CimInstance Win32_Processor|Select-Object -First 1;"
|
||
"$m=(Get-CimInstance Win32_PhysicalMemory|Measure-Object -Property "
|
||
"Capacity -Sum).Sum/1GB;"
|
||
"$g=Get-CimInstance Win32_VideoController|Select-Object -First 1;"
|
||
"$b=Get-CimInstance Win32_BaseBoard|Select-Object -First 1;"
|
||
"\\"CPU: $($c.Name)\\";\\"核心: $($c.NumberOfCores) 核 "
|
||
"$($c.NumberOfLogicalProcessors) 线程\\";"
|
||
"\\"内存: $([math]::Round($m,1)) GB\\";"
|
||
"\\"显卡: $($g.Name)\\";\\"主板: $($b.Manufacturer) $($b.Product)\\"")
|
||
code, out = _run(["powershell", "-NoProfile", "-Command", script], timeout=30)
|
||
lines = [l for l in out.splitlines() if l.strip()]
|
||
return "\\n".join(lines) or "读不到硬件信息"
|
||
|
||
def _do_startup_items(self, params) -> str:
|
||
script = ("Get-CimInstance Win32_StartupCommand | "
|
||
"ForEach-Object { \\"$($_.Name) -> $($_.Command)\\" }")
|
||
code, out = _run(["powershell", "-NoProfile", "-Command", script], timeout=30)
|
||
lines = [l for l in out.splitlines() if l.strip()]
|
||
return f"启动项 {len(lines)} 个:\\n" + "\\n".join(
|
||
l[:100] for l in lines[:30]) if lines else "没有启动项"
|
||
|
||
def _do_scheduled_tasks(self, params) -> str:
|
||
script = ("Get-ScheduledTask | Where-Object {$_.State -ne 'Disabled'} | "
|
||
"Select-Object -First 30 -ExpandProperty TaskName")
|
||
code, out = _run(["powershell", "-NoProfile", "-Command", script], timeout=30)
|
||
lines = [l for l in out.splitlines() if l.strip()]
|
||
return f"计划任务 {len(lines)} 个:\\n" + "\\n".join(lines)
|
||
|
||
def _do_system_detail(self, params) -> str:
|
||
import platform
|
||
code, out = _run(["wmic", "os", "get", "Caption,Version,BuildNumber,"
|
||
"InstallDate,OSArchitecture", "/format:list"])
|
||
lines = [l.strip() for l in out.splitlines() if l.strip()]
|
||
return (f"{platform.system()} {platform.release()} "
|
||
f"{platform.machine()}\\n" + "\\n".join(lines))
|
||
|
||
# ==================================================================
|
||
# 媒体
|
||
# ==================================================================
|
||
|
||
def _do_stop_after(self, params) -> str:
|
||
try:
|
||
minutes = max(1, min(180, int(params.get("minutes") or 10)))
|
||
except (TypeError, ValueError):
|
||
minutes = 10
|
||
|
||
import threading
|
||
|
||
def fire():
|
||
self.actions.stop_audio()
|
||
|
||
timer = threading.Timer(minutes * 60, fire)
|
||
timer.daemon = True
|
||
timer.start()
|
||
self._timers.append(timer)
|
||
return f"{minutes} 分钟后停止播放"
|
||
|
||
def _do_fade_volume(self, params) -> str:
|
||
for _ in range(20):
|
||
self._volume_key(0xAE, 1)
|
||
time.sleep(0.05)
|
||
return "音量已淡出到最低"
|
||
|
||
def _do_gpu_info(self, params) -> str:
|
||
script = ("Get-CimInstance Win32_VideoController | ForEach-Object { "
|
||
"\\"$($_.Name) 驱动 $($_.DriverVersion) "
|
||
"显存 $([math]::Round($_.AdapterRAM/1GB,1))GB\\" }")
|
||
code, out = _run(["powershell", "-NoProfile", "-Command", script], timeout=25)
|
||
lines = [l for l in out.splitlines() if l.strip()]
|
||
return "\\n".join(lines) or "读不到显卡信息"
|
||
|
||
''' + IMPL_ANCHOR
|
||
|
||
|
||
def main() -> int:
|
||
ok = True
|
||
text = FEATURES.read_text(encoding="utf-8")
|
||
for old, new, tag in ((DEFS_OLD, DEFS_NEW, "defs 前半"),
|
||
(DEFS_TAIL_OLD, DEFS_TAIL_NEW, "defs 后半"),
|
||
(IMPL_ANCHOR, IMPL_NEW, "实现")):
|
||
if old not in text:
|
||
print(f" [失败] {tag} 锚点未匹配")
|
||
ok = False
|
||
else:
|
||
text = text.replace(old, new, 1)
|
||
print(f" features.py / {tag}: OK")
|
||
if ok:
|
||
FEATURES.write_text(text, encoding="utf-8")
|
||
|
||
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}")
|
||
ok = False
|
||
return 0 if ok else 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|