# -*- coding: utf-8 -*- """电脑端功能注册表。 为什么用注册表而不是一个功能一条协议命令: 功能会越加越多,每加一个就改协议、改安卓端、改测试,成本太高。 这里做成"电脑端自己声明有哪些功能",手表端只是拉列表 + 按名字调用。 以后加功能只改这一个文件。 功能分三类: toggle 开关型(夜间模式、任务栏隐藏…) action 执行型(清空回收站、锁屏+关屏…) value 带数值(音量、亮度、鼠标速度…) 安全边界: 文件类操作一律**限定在用户自己的目录内**(家目录下的桌面/文档/下载/图片…)。 这是"远程控制"和"任意文件读写后门"的分界线,不会解除。 但允许的目录、允许的程序、允许的命令都可以**在电脑端设置里自己加**。 """ from __future__ import annotations import ctypes import os import re import subprocess import time from pathlib import Path IS_WINDOWS = os.name == "nt" _NO_WINDOW = getattr(subprocess, "CREATE_NO_WINDOW", 0) # 文件操作允许的根目录(可在家目录内自由扩展) DEFAULT_ROOTS = ("Desktop", "Documents", "Downloads", "Pictures", "Music", "Videos") def _run(cmd: list[str], timeout: int = 20, shell: bool = False) -> tuple[int, str]: try: proc = subprocess.run( cmd, capture_output=True, timeout=timeout, shell=shell, creationflags=_NO_WINDOW, encoding="mbcs" if IS_WINDOWS else "utf-8", errors="replace") return proc.returncode, ((proc.stdout or "") + (proc.stderr or "")).strip() except Exception as exc: # noqa: BLE001 return -1, str(exc) class Features: """所有"电脑端能做的小事"都从这里走。""" def __init__(self, actions): self.actions = actions self.store = actions.store self._notes: list[dict] = [] self._todos: list[dict] = [] self._clip_fav: list[str] = [] self._timers: list = [] # ================================================================== # 设置(可在界面里改的那些) # ================================================================== def extra_roots(self) -> list[Path]: """用户额外允许的目录。""" raw = str(self.store.get("file_roots") or "").strip() out: list[Path] = [] for piece in raw.split(";"): piece = piece.strip() if piece: path = Path(piece) if path.is_dir(): out.append(path) return out def allowed_roots(self) -> list[Path]: home = Path.home() roots = [home / name for name in DEFAULT_ROOTS if (home / name).is_dir()] roots.append(home) roots.extend(self.extra_roots()) return roots def sensitive_dirs(self) -> list: """程序自己的数据目录 —— 里面有配对码和设备令牌,任何情况下都不给碰。""" out = [] try: out.append(Path(self.store.dir)) except Exception: # noqa: BLE001 pass try: from .config import app_dir out.append(Path(app_dir())) except Exception: # noqa: BLE001 pass return out def is_sensitive(self, path: Path) -> bool: try: resolved = path.resolve() except OSError: return False # 接收目录要豁免!它就在程序目录里面(app_dir()/received), # 如果不豁免,每个要拉回手表的文件都会被当成"敏感目录"挡掉, # 表现为「接收文件」永远拿不到东西、报"文件不存在"。 try: received = Path(self.store.received_dir()).resolve() resolved.relative_to(received) return False except (ValueError, OSError): pass for folder in self.sensitive_dirs(): try: folder = folder.resolve() except OSError: continue if resolved == folder or folder in resolved.parents: return True return False def assert_not_root(self, path: Path) -> None: """不允许对「允许目录」本身动手。 否则一条 rename_file(path="Downloads", name="x") 就能把整个 下载目录改名(delete_file / move_file 同理,能直接删掉)。 """ try: resolved = path.resolve() except OSError: return for root in self.allowed_roots(): try: if resolved == root.resolve(): raise ValueError("不能对允许目录本身做这个操作") except OSError: continue def resolve_in_roots(self, raw: str) -> Path: """把用户给的路径解析成一个允许访问的绝对路径。""" text = (raw or "").strip().strip('"') if not text: raise ValueError("路径为空") candidate = Path(text) if not candidate.is_absolute(): # 相对路径:先按家目录试(Downloads、Desktop 这种直接写得通), # 不存在再退到"下载目录里的相对路径" home = Path.home() direct = home / text candidate = direct if direct.exists() else home / "Downloads" / text try: resolved = candidate.resolve() except OSError as exc: raise ValueError(f"路径不合法: {exc}") from exc if self.is_sensitive(resolved): raise ValueError("这是程序自己的数据目录,不允许访问") for root in self.allowed_roots(): try: resolved.relative_to(root.resolve()) return resolved except ValueError: continue raise ValueError(f"只允许访问自己的目录({resolved} 不在允许范围内)") def extra_apps(self) -> dict: """用户自定义的快捷程序。格式:显示名=程序路径;显示名2=路径2""" raw = str(self.store.get("extra_apps") or "").strip() out: dict = {} for piece in raw.split(";"): if "=" not in piece: continue label, _, target = piece.partition("=") label = label.strip() target = target.strip() if label and target: out[label] = target return out def extra_commands(self) -> dict: """用户自定义的命令别名。手表只能选这些,不能随便执行。""" raw = str(self.store.get("extra_commands") or "").strip() out: dict = {} for piece in raw.split(";"): if "=" not in piece: continue label, _, command = piece.partition("=") label = label.strip() command = command.strip() if label and command: out[label] = command return out # ================================================================== # 功能声明 # ================================================================== def defs(self) -> list[dict]: items: list[dict] = [] def add(name, label, kind, group, **extra): item = {"name": name, "label": label, "kind": kind, "group": group} item.update(extra) items.append(item) # ---- 显示 ---- add("night_mode", "夜间模式", "toggle", "显示", on=self._night_mode()) add("taskbar_hide", "任务栏自动隐藏", "toggle", "显示", on=self._taskbar_hidden()) add("brightness", "屏幕亮度", "value", "显示", min=10, max=100, value=self._brightness()) add("mouse_speed", "鼠标速度", "value", "显示", min=1, max=20, value=self._mouse_speed()) add("monitor_on", "唤醒屏幕", "action", "显示") # ---- 音量 ---- add("mute_all", "全部静音", "action", "音量") # ---- 静默运行 ---- add("autostart", "开机自启(静默)", "toggle", "静默", on=self._autostart_on()) add("autostart_status", "运行状态", "action", "静默") add("quit_agent", "退出被控端", "action", "静默") add("silent_help", "静默模式说明", "action", "静默") # ---- 查看(续)---- add("screen_resolution", "显示器分辨率", "action", "查看") add("list_printers", "打印机列表", "action", "查看") add("list_drives", "驱动器详情", "action", "查看") add("audio_devices", "音频设备", "action", "查看") add("usb_devices", "USB 设备", "action", "查看") add("installed_drivers", "已装驱动", "action", "查看") add("windows_detail", "Windows 版本详情", "action", "查看") add("recycle_size", "回收站大小", "action", "查看") add("temp_size", "临时文件大小", "action", "查看") add("certificates", "系统证书", "action", "查看") # ---- 网络(续)---- add("open_ports", "本机监听端口", "action", "网络") add("firewall_status", "防火墙状态", "action", "网络") add("dns_servers", "DNS 服务器", "action", "网络") add("hosts_file", "查看 hosts", "action", "网络") add("wifi_networks", "附近 WiFi", "action", "网络") add("bluetooth_devices", "蓝牙设备", "action", "网络") # ---- 系统(续)---- add("open_services", "打开服务", "action", "系统") add("open_task_scheduler", "打开任务计划", "action", "系统") add("open_device_manager", "打开设备管理器", "action", "系统") add("open_disk_cleanup", "打开磁盘清理", "action", "系统") add("open_windows_update", "打开系统更新", "action", "系统") add("clear_temp", "清理临时文件", "action", "系统") add("env_vars", "环境变量", "action", "系统") add("volume_up", "音量 +5", "action", "音量") add("volume_down", "音量 -5", "action", "音量") # ---- 系统 ---- add("lock_and_off", "锁屏 + 关屏", "action", "系统") add("empty_recycle", "清空回收站", "action", "系统") add("flush_dns", "刷新 DNS", "action", "系统") add("restart_explorer", "重启资源管理器", "action", "系统") add("clear_clip", "清空电脑剪贴板", "action", "系统") # ---- 查看 ---- add("uptime", "开机时长", "action", "查看") add("perf", "性能(CPU/内存)", "action", "查看") add("disk", "磁盘占用", "action", "查看") add("battery", "电量", "action", "查看") add("network_speed", "网络速度", "action", "查看") add("installed_apps", "已安装软件", "action", "查看") add("top_services", "运行中的服务", "action", "查看") add("recent_files", "最近打开的文件", "action", "查看") add("event_errors", "系统错误日志", "action", "查看") add("temperature", "温度 / 负载", "action", "查看") # ---- 效率 ---- add("note_add", "记一条笔记", "input", "效率", hint="内容", key="text") add("note_list", "看所有笔记", "action", "效率") add("note_clear", "清空笔记", "action", "效率") add("todo_add", "加一条待办", "input", "效率", hint="内容", key="text") add("todo_list", "看所有待办", "action", "效率") add("reminder", "定时提醒", "input", "效率", hint="几分钟后", key="minutes", hint2="提醒内容", key2="text") add("pomodoro", "番茄钟 25 分钟", "action", "效率") add("clip_fav_add", "收藏当前剪贴板", "action", "效率") add("clip_fav_list", "剪贴板收藏夹", "action", "效率") # ---- 教学(老师的刚需)---- add("big_text", "全屏大字", "input", "教学", hint="要显示的文字", key="text") add("big_text_clear", "关掉全屏内容", "action", "教学") add("close_visuals", "关掉画面", "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_quiet", "全屏显示「安静」", "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", "网络") # ---- 文件(限定自己的目录)---- add("list_dir", "浏览文件夹", "input", "文件", hint="如 Downloads", key="path") add("download_file", "下载文件到手表", "file", "文件", hint="完整路径", key="path") add("search_files", "搜索文件", "input", "文件", hint="关键词", key="keyword") add("make_dir", "新建文件夹", "input", "文件", hint="父目录", key="path", hint2="文件夹名", key2="name") add("rename_file", "重命名", "input", "文件", hint="文件路径", key="path", hint2="新名字", key2="name") add("delete_file", "删除到回收站", "input", "文件", hint="文件路径", key="path") 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", "媒体") # ---- 教室管控 ---- add("hide_icons", "隐藏桌面图标", "toggle", "教室", on=self._reg_on( "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer" "\\Advanced", "HideIcons", 1)) add("restrict_taskmgr", "禁用任务管理器", "toggle", "教室", on=self._reg_on( "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion" "\\Policies\\System", "DisableTaskMgr", 1)) add("restrict_regedit", "禁用注册表编辑器", "toggle", "教室", on=self._reg_on( "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion" "\\Policies\\System", "DisableRegistryTools", 1)) add("restrict_cmd", "禁用命令提示符", "toggle", "教室", on=self._reg_on( "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion" "\\Policies\\System", "DisableCMD", 1)) add("restrict_wallpaper", "禁止换壁纸", "toggle", "教室", on=self._reg_on( "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion" "\\Policies\\System", "NoDispBackgroundPage", 1)) add("restrict_power", "藏掉关机按钮", "toggle", "教室", on=self._reg_on( "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion" "\\Policies\\Explorer", "NoClose", 1)) add("lock_input", "锁键盘鼠标", "toggle", "教室", on=False) add("game_block_common", "屏蔽常见游戏网站", "action", "教室") add("site_block", "屏蔽一个网站", "input", "教室", hint="如 game.com", key="site") add("site_unblock", "解除屏蔽一个网站", "input", "教室", hint="如 game.com", key="site") add("site_list", "看屏蔽列表", "action", "教室") add("site_clear", "清空屏蔽列表", "action", "教室") add("kill_games", "结束游戏进程", "action", "教室") add("kill_by_name", "按名字结束进程", "input", "教室", hint="进程名,如 game.exe", key="name") add("bell", "下课铃", "action", "教室") add("class_attention", "全屏「请安静」", "action", "教室") add("class_start", "上课(锁屏+提示)", "action", "教室") add("class_timer", "课堂倒计时", "input", "教室", hint="几分钟", key="minutes") add("restore_all", "一键恢复全部限制", "action", "教室") add("undo_last", "回退上一步", "action", "系统") add("undo_all", "回退全部", "action", "系统") add("undo_list", "看能回退什么", "action", "系统") add("restrict_status", "看当前有哪些限制", "action", "教室") # ---- 教学(续)---- add("roll_group", "随机分组", "input", "教学", hint="几个人一组", key="size") add("roll_call_no_repeat", "点名(不重复)", "action", "教学") add("roll_call_reset", "重置点名记录", "action", "教学") add("class_schedule", "显示课程表", "input", "教学", hint="用逗号隔开,如 语文,数学,英语", key="text") add("hand_count", "举手计数", "action", "教学") add("hand_reset", "举手清零", "action", "教学") add("answer_poll", "答题统计", "action", "教学") add("magnifier", "屏幕放大镜", "action", "教学") add("projector_switch", "切换投影模式", "action", "教学") add("lesson_timer", "上课计时", "action", "教学") # ---- 教室(续)---- add("lock_now", "立刻锁屏", "action", "教室") add("game_watch_start", "开始盯游戏进程", "action", "教室") add("game_watch_stop", "停止盯游戏进程", "action", "教室") add("usb_check", "检查有没有插U盘", "action", "教室") add("login_history", "最近登录记录", "action", "教室") add("boot_history", "开关机记录", "action", "教室") add("clean_desktop", "整理桌面(收进文件夹)", "action", "教室") add("block_new_app", "禁止新程序启动", "toggle", "教室", on=False) add("show_desktop_now", "立刻显示桌面", "action", "教室") add("count_windows", "看目前开了几个窗口", "action", "教室") add("class_end_cleanup", "下课清理(关游戏+恢复)", "action", "教室") add("who_is_playing", "谁在玩游戏", "action", "教室") # ---- 系统(续)---- add("open_taskmgr2", "打开任务管理器", "action", "系统") add("open_resmon", "打开资源监视器", "action", "系统") add("power_plan", "切换电源计划", "action", "系统") add("window_top", "当前窗口置顶", "action", "系统") add("screen_off_lock", "关屏并锁定", "action", "系统") add("clear_all_clip", "清空剪贴板历史", "action", "系统") add("restart_audio", "重启音频服务", "action", "系统") add("system_sounds", "打开声音设置", "action", "系统") # ---- 查看(续)---- add("cpu_model", "CPU 型号", "action", "查看") add("memory_sticks", "内存条信息", "action", "查看") add("disk_serial", "硬盘序列号", "action", "查看") add("gpu_temp", "显卡温度", "action", "查看") add("net_total", "网络累计流量", "action", "查看") add("running_games", "正在运行的游戏", "action", "查看") add("activation", "系统激活状态", "action", "查看") add("disk_health", "硬盘健康", "action", "查看") # ---- 效率(续)---- add("sticky_note", "便签", "input", "效率", hint="写点什么", key="text") add("search_note", "搜索笔记", "input", "效率", hint="关键词", key="word") add("translator", "打开翻译", "input", "效率", hint="要翻译的词", key="text") add("calc_open", "打开计算器2", "action", "效率") add("unit_convert", "单位换算", "input", "效率", hint="如 100cm", key="text") add("countdown_day", "倒数日", "input", "效率", hint="几月几号,如 06-20", key="date") # ---- 媒体(续)---- add("loop_play", "循环播放", "toggle", "媒体", on=False) add("shuffle_play", "随机播放开关", "toggle", "媒体", on=False) add("stop_all_sound", "停掉所有声音", "action", "媒体") # ---- 网络(续)---- add("net_traffic", "实时网速", "action", "网络") add("relay_saver", "跨网络省流量", "toggle", "网络", on=bool(self.store.get("relay_saver"))) add("traceroute", "路由追踪", "input", "网络", hint="如 8.8.8.8", key="host") add("net_reset", "重置网络", "action", "网络") # ---- 娱乐 ---- add("danmaku", "全屏弹幕", "input", "娱乐", hint="弹幕内容", key="text") add("marquee", "滚动字幕", "input", "娱乐", hint="要滚动的字", key="text") add("lottery", "抽奖", "action", "娱乐") add("dice", "掷骰子", "action", "娱乐") add("coin_flip", "抛硬币", "action", "娱乐") add("countdown_321", "3-2-1 倒计时", "action", "娱乐") add("cinema_mode", "影院模式(黑屏)", "action", "娱乐") add("random_song", "随机放一首歌", "action", "娱乐") add("play_video", "播放视频文件", "input", "娱乐", hint="视频路径", key="path") add("score_board", "显示计分板", "action", "娱乐") add("score_change", "改分数", "input", "娱乐", hint="A 或 B", key="team", hint2="加几分(负数减)", key2="delta") add("cheer", "加油动画", "action", "娱乐") # ---- 极客 ---- add("matrix_rain", "数字雨(黑客帝国)", "action", "极客") add("starfield", "星空穿梭", "action", "极客") add("dashboard", "全屏实时监控", "action", "极客") add("qr_code", "生成二维码", "input", "极客", hint="要变成二维码的内容", key="text") add("hash_text", "算哈希(MD5/SHA)", "input", "极客", hint="要计算的文字", key="text") add("base64_encode", "Base64 编码", "input", "极客", hint="原文", key="text") add("base64_decode", "Base64 解码", "input", "极客", hint="Base64 内容", key="text") add("uuid_gen", "生成 UUID", "action", "极客") add("timestamp_now", "当前时间戳", "action", "极客") add("json_format", "JSON 格式化", "input", "极客", hint="JSON 文本", key="text") add("screen_color", "鼠标位置取色", "action", "极客") add("speed_test", "网络测速", "action", "极客") add("port_check", "检查常用端口", "input", "极客", hint="目标 IP 或主机名", key="host") return items # ================================================================== # 执行 # ================================================================== @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: handler = getattr(self, f"_do_{name}", None) if handler is None: raise ValueError(f"没有这个功能: {name}") result = handler(params or {}) # 执行完顺手记一条"怎么退回去"。 # 判断逻辑集中在 _remember_undo 里,各个功能自己不用管。 try: self._remember_undo(name, params or {}, result) except Exception: # noqa: BLE001 pass return result # ================================================================== # 回退(撤销) # ================================================================== # 这些**撤不回来**。记了只会给用户"还能退回去"的错觉,反而更危险。 IRREVERSIBLE = { # 电源 "shutdown", "restart", "sleep", "hibernate", "power", "lock_now", "screen_off_lock", "shutdown_timer", "reboot_now", "lock_and_off", "class_end_cleanup", # 破坏性 "delete_file", "empty_recycle", "clean_temp", "clean_desktop", "kill_process", "kill_games", "kill_by_name", "quit_agent", "net_reset", "restart_audio", "usb_check", # 一次性只读 "netcheck", "selfcheck", "site_list", "restrict_status", } # 这些会在电脑上开一个全屏窗口 —— 回退就是把它关掉 VISUAL_FEATURES = { "big_text", "big_text_quiet", "class_attention", "class_start", "bell", "countdown_screen", "class_timer", "whiteboard", "stopwatch", "danmaku", "marquee", "lottery", "dice", "coin_flip", "countdown_321", "cinema_mode", "cheer", "matrix_rain", "starfield", "dashboard", "roll_group", "roll_call_no_repeat", "hand_count", "class_schedule", "countdown_day", "sticky_note", } def _spec_map(self) -> dict: """name -> 功能声明。用来查 kind / label。""" cached = getattr(self, "_spec_cache", None) if isinstance(cached, dict): return cached out: dict = {} try: for item in self.defs(): key = item.get("name") if key: out[key] = item except Exception: # noqa: BLE001 pass self._spec_cache = out return out def _undo_stack(self) -> list: stack = getattr(self, "_undo", None) if stack is None: stack = [] self._undo = stack return stack def _remember_undo(self, name: str, params: dict, result) -> None: if not result or name in self.IRREVERSIBLE: return if name in ("undo_last", "undo_all", "undo_list"): return spec = self._spec_map().get(name) if spec is None: return label = str(spec.get("label") or name) entry = None if spec.get("kind") == "toggle": back = not self._truthy(params.get("on")) entry = { "name": name, "params": {"on": back}, "label": label, "what": f"把「{label}」{'打开' if back else '关掉'}回去", } elif name in self.VISUAL_FEATURES: entry = { "name": "close_visuals", "params": {}, "label": label, "what": f"关掉「{label}」显示的画面", } if entry is None: # 其它功能(打开程序、改设置…)暂时不记, # 免得给用户"什么都能退"的错误预期 return entry["at"] = time.strftime("%H:%M:%S") stack = self._undo_stack() stack.append(entry) # 只留最近 20 条,别让内存涨(这台机器还要长时间跑) if len(stack) > 20: del stack[:-20] def _apply_undo(self, entry: dict) -> str: """执行一条回退。 注意:这里**直接调 _do_xxx,不走 run()** —— 走 run 的话又会记一条新的回退,点一下就来回横跳。 """ target = str(entry.get("name") or "") what = str(entry.get("what") or target) if not target: return "这条回退记录不完整" handler = getattr(self, f"_do_{target}", None) if handler is None: return f"回退失败:找不到 {target}" try: detail = handler(dict(entry.get("params") or {})) except Exception as exc: # noqa: BLE001 return f"回退失败:{what} —— {exc}" return f"已回退:{what}" + (f"({detail})" if detail else "") def _do_undo_last(self, params) -> str: stack = self._undo_stack() if not stack: return "没有可以回退的操作" entry = stack.pop() return self._apply_undo(entry) def _do_undo_all(self, params) -> str: stack = self._undo_stack() if not stack: return "没有可以回退的操作" count = len(stack) lines = [] # 倒序:后做的先撤 for entry in reversed(stack): lines.append(self._apply_undo(entry)) stack.clear() return f"已回退 {count} 步:\n" + "\n".join(lines) def _do_undo_list(self, params) -> str: stack = self._undo_stack() if not stack: return "没有可以回退的操作" lines = [f"{i + 1}. [{e.get('at', '')}] {e.get('what', '')}" for i, e in enumerate(reversed(stack))] return (f"可以回退 {len(stack)} 步(最近的在前):\n" + "\n".join(lines)) # ---------------- 显示 ---------------- def _night_mode(self) -> bool: code, out = _run(["reg", "query", r"HKCU\Software\Microsoft\Windows\CurrentVersion\CloudStore\Store" r"\DefaultAccount\Current\default$windows.data.bluelightreduction" r".bluelightreductionstate\windows.data.bluelightreduction.bluelightreductionstate"]) return code == 0 and "02 01" in out def _do_night_mode(self, params) -> str: want_on = self._truthy(params.get("on")) # 用 PowerShell 切注册表,Win10/11 通用 if want_on: script = ( "$p='HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\CloudStore\\Store" "\\DefaultAccount\\Current\\default$windows.data.bluelightreduction" ".bluelightreductionstate\\windows.data.bluelightreduction.bluelightreductionstate';" "if(-not(Test-Path $p)){New-Item -Path $p -Force|Out-Null;" "New-ItemProperty -Path $p -Name Data -PropertyType Binary " "-Value ([byte[]](0x02,0x01,0x00)) -Force|Out-Null};" "$d=(Get-ItemProperty -Path $p -Name Data).Data;" "if($d.Length -lt 4){Set-ItemProperty -Path $p -Name Data " "-Value ([byte[]](0x10,0x00,0x00,0x00,0x43,0x42,0x01,0x00,0x0A,0x02,0x01,0x00))};" "else{Set-ItemProperty -Path $p -Name Data -Value " "([byte[]](0x10,0x00,0x00,0x00,0x43,0x42,0x01,0x00,0x0A,0x02,0x01,0x00))}") else: script = ( "$p='HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\CloudStore\\Store" "\\DefaultAccount\\Current\\default$windows.data.bluelightreduction" ".bluelightreductionstate\\windows.data.bluelightreduction.bluelightreductionstate';" "if(Test-Path $p){Set-ItemProperty -Path $p -Name Data " "-Value ([byte[]](0x10,0x00,0x00,0x00,0x43,0x42,0x01,0x00,0x0A,0x02,0x00,0x00))}") _run(["powershell", "-NoProfile", "-Command", script]) self._broadcast_setting() return "夜间模式已开" if want_on else "夜间模式已关" def _taskbar_hidden(self) -> bool: """任务栏是不是自动隐藏。 reg query 把 REG_BINARY 输出成逗号分隔的十六进制: Settings REG_BINARY 30,00,00,00,00,00,00,00,03,00,00,00,... 下标 8 那个字节的最低位就是「自动隐藏」开关。 以前是拿字符串 "03,00,00,00" 去整段匹配 —— 大小写和位置都不稳, 状态永远读成 False,于是手表每次点都发"隐藏", 表现成"能隐藏但恢复不了"。这里老老实实按字节解析。 """ code, out = _run(["reg", "query", r"HKCU\Software\Microsoft\Windows\CurrentVersion" r"\Explorer\StuckRects3", "/v", "Settings"]) if code != 0 or not out: return False match = re.search(r"REG_BINARY\s+([0-9a-fA-F,\s]+)", out) if not match: return False values = [v for v in re.split(r"[,\s]+", match.group(1).strip()) if v] if len(values) <= 8: return False try: return bool(int(values[8], 16) & 0x01) except ValueError: return False def _do_taskbar_hide(self, params) -> str: want = self._truthy(params.get("on")) script = ( "$p='HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\StuckRects3';" "$s=(Get-ItemProperty -Path $p -Name Settings).Settings;" f"$s[8]={3 if want else 2};" "Set-ItemProperty -Path $p -Name Settings -Value $s -Type Binary;" "Stop-Process -Name explorer -Force -ErrorAction SilentlyContinue") _run(["powershell", "-NoProfile", "-Command", script]) return "任务栏已设为自动隐藏" if want else "任务栏已恢复常显" def _brightness(self) -> int: code, out = _run(["powershell", "-NoProfile", "-Command", "(Get-CimInstance -Namespace root/WMI " "-ClassName WmiMonitorBrightness -ErrorAction SilentlyContinue)" ".CurrentBrightness"]) digits = re.findall(r"\d+", out or "") return int(digits[0]) if digits else 50 def _do_brightness(self, params) -> str: try: level = max(10, min(100, int(params.get("value") or 50))) except (TypeError, ValueError): raise ValueError("亮度要 10~100") _run(["powershell", "-NoProfile", "-Command", f"(Get-CimInstance -Namespace root/WMI -ClassName " f"WmiMonitorBrightnessMethods).WmiSetBrightness(1,{level})"]) return f"亮度已设为 {level}%" def _mouse_speed(self) -> int: if not IS_WINDOWS: return 10 try: value = ctypes.c_int(0) ctypes.windll.user32.SystemParametersInfoW(0x0070, 0, ctypes.byref(value), 0) return int(value.value) except Exception: # noqa: BLE001 return 10 def _do_mouse_speed(self, params) -> str: try: level = max(1, min(20, int(params.get("value") or 10))) except (TypeError, ValueError): raise ValueError("鼠标速度要 1~20") ctypes.windll.user32.SystemParametersInfoW(0x0071, 0, level, 3) return f"鼠标速度已设为 {level}" def _do_monitor_on(self, params) -> str: return self.actions.monitor_power(True) def _broadcast_setting(self) -> None: HWND_BROADCAST = 0xFFFF WM_SETTINGCHANGE = 0x001A ctypes.windll.user32.SendMessageW(HWND_BROADCAST, WM_SETTINGCHANGE, 0, 0) # ---------------- 音量 ---------------- def _volume_key(self, vk: int, times: int = 1) -> None: VK_VOLUME_MUTE = 0xAD VK_VOLUME_DOWN = 0xAE VK_VOLUME_UP = 0xAF for _ in range(max(1, min(50, times))): ctypes.windll.user32.keybd_event(vk, 0, 0, 0) ctypes.windll.user32.keybd_event(vk, 0, 2, 0) time.sleep(0.02) def _do_volume_up(self, params) -> str: self._volume_key(0xAF, 5) return "音量 +5" def _do_volume_down(self, params) -> str: self._volume_key(0xAE, 5) return "音量 -5" def _do_mute_all(self, params) -> str: self._volume_key(0xAD, 1) return "已切换静音" # ---------------- 系统 ---------------- def _do_lock_and_off(self, params) -> str: ctypes.windll.user32.LockWorkStation() time.sleep(0.4) self.actions.monitor_power(False) return "已锁屏并关屏" def _do_empty_recycle(self, params) -> str: code, out = _run(["powershell", "-NoProfile", "-Command", "Clear-RecycleBin -Force -ErrorAction SilentlyContinue; 'ok'"]) return "回收站已清空" if "ok" in out else f"清空失败: {out[:60]}" def _do_flush_dns(self, params) -> str: _run(["ipconfig", "/flushdns"]) return "DNS 缓存已刷新" def _do_restart_explorer(self, params) -> str: _run(["taskkill", "/F", "/IM", "explorer.exe"]) time.sleep(1.0) subprocess.Popen("explorer.exe", shell=True, creationflags=_NO_WINDOW) return "资源管理器已重启" def _do_clear_clip(self, params) -> str: self.actions.clip_set("") return "电脑剪贴板已清空" # ---------------- 查看 ---------------- def _do_uptime(self, params) -> str: info = self.actions.perf() return f"已开机 {info.get('uptime', '-')}" def _do_perf(self, params) -> str: info = self.actions.perf() return (f"CPU {info.get('cpu_percent')}% " f"内存 {info.get('mem_percent')}%" f"({info.get('mem_used_gb')}/{info.get('mem_total_gb')} GB)") def _do_disk(self, params) -> str: return " | ".join(f"{d['drive']} 剩 {d['free_gb']}GB" for d in self.actions.disk()) or "读不到" def _do_battery(self, params) -> str: info = self.actions.battery() if not info.get("present"): return info.get("note") or "没有电池" return f"{info['percent']}%" + ("(充电中)" if info.get("charging") else "") 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") def _do_installed_apps(self, params) -> str: if not IS_WINDOWS: return "仅支持 Windows" import winreg names: list[str] = [] for hive, path in ( (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"), (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"), (winreg.HKEY_CURRENT_USER, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"), ): try: with winreg.OpenKey(hive, path) as key: for i in range(winreg.QueryInfoKey(key)[0]): try: sub = winreg.EnumKey(key, i) with winreg.OpenKey(key, sub) as item: name = winreg.QueryValueEx(item, "DisplayName")[0] if name and name not in names: names.append(str(name)) except OSError: continue except OSError: continue names.sort() return f"共 {len(names)} 个:\n" + "\n".join(names[:60]) def _do_top_services(self, params) -> str: code, out = _run(["powershell", "-NoProfile", "-Command", "Get-Service | Where-Object {$_.Status -eq 'Running'} | " "Select-Object -First 40 -ExpandProperty Name"]) lines = [l for l in out.splitlines() if l.strip()] return f"运行中 {len(lines)} 个(前 40):\n" + "\n".join(lines) def _do_recent_files(self, params) -> str: recent = Path(os.environ.get("APPDATA", "")) / "Microsoft/Windows/Recent" if not recent.is_dir(): return "读不到最近文件" items = sorted(recent.glob("*.lnk"), key=lambda p: p.stat().st_mtime, reverse=True)[:25] names = [p.stem for p in items] return f"最近 {len(names)} 个:\n" + "\n".join(names) def _do_event_errors(self, params) -> str: script = ("Get-EventLog -LogName System -EntryType Error -Newest 15 " "-ErrorAction SilentlyContinue | " "ForEach-Object { \"$($_.TimeGenerated.ToString('MM-dd HH:mm')) " "$($_.Source)\" }") 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) if lines else "没有系统错误记录") def _do_temperature(self, params) -> str: script = ("$t=Get-CimInstance -Namespace root/WMI -ClassName " "MSAcpi_ThermalZoneTemperature -ErrorAction SilentlyContinue | " "Select-Object -First 1;" "if($t){[math]::Round(($t.CurrentTemperature/10)-273.15,1)}else{'n/a'}") code, out = _run(["powershell", "-NoProfile", "-Command", script], timeout=25) cpu_temp = out.strip().splitlines()[-1] if out.strip() else "n/a" info = self.actions.perf() return (f"CPU {info.get('cpu_percent')}% 内存 {info.get('mem_percent')}% " f"温度 {cpu_temp}°C") # ---------------- 效率 ---------------- def _do_note_add(self, params) -> str: text = str(params.get("text") or "").strip() if not text: raise ValueError("笔记内容为空") # 必须先 load。__init__ 里 self._notes 是空的, # 直接 insert 再 persist 会把历史笔记全抹掉(重启后第一次记笔记就中招)。 self._notes = self._load("notes", dict) self._notes.insert(0, {"text": text[:2000], "at": time.strftime("%m-%d %H:%M")}) del self._notes[200:] self._persist("notes", self._notes) return f"已记下(共 {len(self._notes)} 条)" def _do_note_list(self, params) -> str: self._notes = self._load("notes") if not self._notes: return "还没有笔记" return f"共 {len(self._notes)} 条:\n" + "\n".join( f"[{n['at']}] {n['text'][:80]}" for n in self._notes[:30]) def _do_note_clear(self, params) -> str: self._notes = [] self._persist("notes", []) return "笔记已清空" def _do_todo_add(self, params) -> str: text = str(params.get("text") or "").strip() if not text: raise ValueError("待办内容为空") todos = self._load("todos", dict) todos.append({"text": text[:500], "done": False}) self._persist("todos", todos) return f"已添加(共 {len(todos)} 条待办)" def _do_todo_list(self, params) -> str: todos = self._load("todos") if not todos: return "待办是空的" done = sum(1 for t in todos if t.get("done")) lines = [f"{'✓' if t.get('done') else '○'} {t['text'][:60]}" for t in todos[:40]] return f"共 {len(todos)} 条(完成 {done}):\n" + "\n".join(lines) def _do_reminder(self, params) -> str: try: minutes = max(1, min(600, int(params.get("minutes") or 5))) except (TypeError, ValueError): minutes = 5 text = str(params.get("text") or "时间到了").strip() import threading def fire(): self.actions.notify("定时提醒", text) self.actions.say(text) timer = threading.Timer(minutes * 60, fire) timer.daemon = True timer.start() self._timers.append(timer) return f"{minutes} 分钟后提醒你:{text}" def _do_pomodoro(self, params) -> str: import threading def fire(): self.actions.say("番茄钟结束,休息一下") self.actions.notify("番茄钟", "25 分钟到了,休息 5 分钟") timer = threading.Timer(25 * 60, fire) timer.daemon = True timer.start() self._timers.append(timer) self._do_lock_and_off({}) return "番茄钟开始:25 分钟,已锁屏关屏" def _do_clip_fav_add(self, params) -> str: text = str(self.store.get("last_clip") or "").strip() if not text: raise ValueError("电脑剪贴板是空的") favs = self._load("clip_fav", str) favs.insert(0, text[:2000]) self._persist("clip_fav", favs[:50]) return f"已收藏(共 {len(favs[:50])} 条)" def _do_clip_fav_list(self, params) -> str: favs = self._load("clip_fav") if not favs: return "收藏夹是空的" return f"共 {len(favs)} 条:\n" + "\n".join( f"{i + 1}. {t[:60]}" for i, t in enumerate(favs[:30])) # ---------------- 文件 ---------------- def _do_list_dir(self, params) -> str: target = self.resolve_in_roots(str(params.get("path") or "Downloads")) if not target.is_dir(): raise ValueError(f"不是文件夹: {target}") try: entries = sorted(target.iterdir(), key=lambda p: (p.is_file(), p.name.lower())) except OSError as exc: raise ValueError(f"读不了这个文件夹: {exc}") from exc lines = [] for entry in entries[:60]: if entry.is_dir(): lines.append(f"[目录] {entry.name}") else: try: size = entry.stat().st_size except OSError: size = 0 lines.append(f"{size // 1024} KB {entry.name}") return f"{target}\n共 {len(entries)} 项:\n" + "\n".join(lines) def _do_download_file(self, params) -> str: target = self.resolve_in_roots(str(params.get("path") or "")) if not target.is_file(): raise ValueError(f"不是文件: {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 _do_search_files(self, params) -> str: keyword = str(params.get("keyword") or "").strip().lower() if not keyword: raise ValueError("关键词为空") found: list[str] = [] for root in self.allowed_roots(): try: for path in root.rglob(f"*{keyword}*"): if path.is_file(): found.append(str(path)) if len(found) >= 40: break except OSError: continue if len(found) >= 40: break return (f"找到 {len(found)} 个:\n" + "\n".join(found) if found else "没找到") def _do_make_dir(self, params) -> str: parent = self.resolve_in_roots(str(params.get("path") or "Downloads")) name = re.sub(r'[<>:"/\\|?*]', "_", str(params.get("name") or "").strip()) if not name: raise ValueError("文件夹名为空") 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}" def _do_rename_file(self, params) -> str: source = self.resolve_in_roots(str(params.get("path") or "")) name = re.sub(r'[<>:"/\\|?*]', "_", str(params.get("name") or "").strip()) if not name: raise ValueError("新名字为空") if not source.exists(): raise ValueError("文件不存在") self.assert_not_root(source) 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}" 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("文件不存在") self.assert_not_root(target) 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}" def _do_open_file(self, params) -> str: target = self.resolve_in_roots(str(params.get("path") or "")) if not target.exists(): raise ValueError("文件不存在") os.startfile(str(target)) # noqa: S606 return f"已用默认程序打开 {target.name}" # ================================================================== # 教学 # ================================================================== def _close_fullscreen_windows(self) -> int: """关掉所有由本程序开的全屏窗口。 注意:**必须回到 Tk 线程去 destroy**。Tk 的对象只能在创建它的 线程里操作,直接在服务端线程里调 win.destroy() 会静默失败, 表现成"全屏大字关不掉"。 """ closers = list(getattr(self, "_fullscreen_closers", [])) self._fullscreen_closers = [] if not closers: return 0 def run_all(): for closer in closers: try: closer() except Exception: # noqa: BLE001 pass ui = getattr(self.actions, "ui", None) if ui is not None: try: ui.ui_call(run_all) return len(closers) except Exception: # noqa: BLE001 pass # 界面拿不到(或超时)才退回直接调,至少试一下 closed = 0 for closer in closers: try: closer() closed += 1 except Exception: # noqa: BLE001 pass return closed def _fullscreen_window(self, builder, seconds: float = 0.0, title: str = ""): """在电脑上开一个全屏窗口。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) # 给窗口一个标题:不然枚举窗口、任务栏、Alt+Tab 都看不到它, # 用户也不知道电脑上那个大黑屏是什么。 try: win.title(title or "局域网遥控") except Exception: # noqa: BLE001 pass win.bind("", close) win.bind("", 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 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, title=text[:40]) return f"已全屏显示「{text[:20]}」" def _do_big_text_quiet(self, params) -> str: return self._do_big_text({"text": "安静"}) def _do_big_text_clear(self, params) -> str: closed = self._close_fullscreen_windows() return f"已关掉 {closed} 个全屏窗口" if closed else "当前没有全屏窗口" def _do_close_visuals(self, params) -> str: """把本程序在电脑上开出来的画面全部收掉。 覆盖:全屏大字 / 倒计时 / 请安静 / 下课铃 / 投图 / 通知 / 屏幕共享提示。 手表能打开的东西,必须也能关掉。 """ done = [] closed = self._close_fullscreen_windows() if closed: done.append(f"全屏窗口 {closed} 个") for label, method in (("投图", "close_image"), ("通知", "close_notifications")): func = getattr(self.actions.ui, method, None) if func is None: continue try: if func(): done.append(label) except Exception: # noqa: BLE001 pass # 屏幕镜像提示条也一起收掉(镜像本身由手表那个开关控制) try: hide = getattr(self.actions.ui, "_hide_notice", None) if hide is not None and getattr(self.actions.ui, "_notice", None): hide() done.append("共享提示") except Exception: # noqa: BLE001 pass return "已关掉:" + "、".join(done) if done else "电脑上现在没有本程序开的画面" 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", 150, "bold")) label.pack(expand=True, fill="both") tk.Label(win, text=f"倒计时 {minutes} 分钟 · Esc 结束", bg="#0B1020", fg="#55607A", font=("Microsoft YaHei UI", 14)).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"} 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=4, capstyle="round") state["x"], state["y"] = event.x, event.y def release(_event): state["x"] = state["y"] = None canvas.bind("", press) canvas.bind("", drag) canvas.bind("", release) bar = tk.Frame(win, bg="#F5F7FA") bar.place(relx=1.0, rely=0.0, anchor="ne") 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=lambda: canvas.delete("all")).pack( side="left", padx=6, pady=4) self._fullscreen_window(builder) return "白板已打开(鼠标画,Esc 关闭)" def _do_display_mode(self, params) -> str: subprocess.Popen(["cmd", "/c", "start", "ms-settings:display"], creationflags=_NO_WINDOW) return "已打开显示设置" def _do_roll_call_set(self, params) -> str: raw = str(params.get("names") or "").strip() if not raw: raise ValueError("名单为空") for sep in (",", "、", " ", ";", ";"): raw = raw.replace(sep, ",") parts = [p.strip() for p in raw.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 _key_combo(self, *vks) -> None: for vk in vks: ctypes.windll.user32.keybd_event(vk, 0, 0, 0) for vk in reversed(vks): ctypes.windll.user32.keybd_event(vk, 0, 2, 0) def _do_minimize_all(self, params) -> str: self._key_combo(0x5B, 0x44) # Win + D return "已显示桌面" def _do_task_view(self, params) -> str: self._key_combo(0x5B, 0x09) # Win + Tab return "已打开任务视图" def _do_close_windows(self, params) -> str: WM_CLOSE = 0x0010 user32 = ctypes.windll.user32 EnumProc = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p) own_pid = os.getpid() state = {"n": 0} def callback(hwnd, _lparam): if not user32.IsWindowVisible(hwnd): return True if user32.GetWindowTextLengthW(hwnd) == 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) state["n"] += 1 return True user32.EnumWindows(EnumProc(callback), 0) return f"已向 {state['n']} 个窗口发出关闭" def _clock_seconds(self) -> bool: code, out = _run(["reg", "query", r"HKCU\Software\Microsoft\Windows\CurrentVersion" r"\Explorer\Advanced", "/v", "ShowSecondsInSystemClock"]) return code == 0 and "0x1" in out def _do_clock_seconds(self, params) -> str: want = 1 if self._truthy(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" r"\Explorer\Advanced", "/v", "Hidden"]) if code != 0 or "REG_DWORD" not in out: return False tail = out.split("REG_DWORD")[-1] return "0x1" in tail def _do_hidden_files(self, params) -> str: want = 2 if not self._truthy(params.get("on")) else 1 _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: code, out = _run(["shutdown", "/h"]) return "已进入休眠" if code == 0 else "这台机器没启用休眠" 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: self._key_combo(0x5B, 0x11, 0x10, 0x42) # Win+Ctrl+Shift+B 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]: _code, 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()] lines = [l for l in lines if "---" 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: script = ("Get-NetAdapter | ForEach-Object { " "\"$($_.Name) $($_.Status) $($_.LinkSpeed)\" }") _code, out = _run(["powershell", "-NoProfile", "-Command", script]) lines = [l for l in out.splitlines() if l.strip()] return "\n".join(lines) or "读不到网卡" # ================================================================== # 文件(继续限定在自己的目录内) # ================================================================== def _do_copy_file(self, params) -> str: import shutil as _shutil 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("源文件不存在") self.assert_not_root(source) self.assert_not_root(target) 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}" def _do_move_file(self, params) -> str: import shutil as _shutil 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("源文件不存在") self.assert_not_root(source) self.assert_not_root(target) 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}" 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" 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)" 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() size = stat.st_size readable = (f"{size / 1048576:.2f} MB" if size > 1048576 else f"{size // 1024} KB") kind = "文件夹" if target.is_dir() else "文件" return (f"{kind}:{target.name}\n大小:{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 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" def _do_open_recycle(self, params) -> str: subprocess.Popen(["explorer.exe", "shell:RecycleBinFolder"], creationflags=_NO_WINDOW) 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", 130, "bold")) label.pack(expand=True, fill="both") 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: """写死偏移会忽略夏令时(现在纽约/伦敦/悉尼都差 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) 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;" "\"CPU: $($c.Name)\";" "\"核心: $($c.NumberOfCores) 核 $($c.NumberOfLogicalProcessors) 线程\";" "\"内存: $([math]::Round($m,1)) GB\";" "\"显卡: $($g.Name)\"") _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()] if not lines: return "没有启动项" return f"启动项 {len(lines)} 个:\n" + "\n".join(l[:100] for l in lines[:30]) 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,OSArchitecture", "/format:list"]) lines = [l.strip() for l in out.splitlines() if l.strip()] return (f"{platform.system()} {platform.release()} {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)\" }") _code, out = _run(["powershell", "-NoProfile", "-Command", script], timeout=25) lines = [l for l in out.splitlines() if l.strip()] return "\n".join(lines) or "读不到显卡信息" # ================================================================== # 娱乐 # ================================================================== def _do_danmaku(self, params) -> str: text = str(params.get("text") or "").strip() or "大家好" speed = 6 def builder(win, close): import tkinter as tk win.configure(bg="#000000") width = win.winfo_screenwidth() height = win.winfo_screenheight() canvas = tk.Canvas(win, bg="#000000", highlightthickness=0, width=width, height=height) canvas.pack(fill="both", expand=True) canvas.configure(highlightthickness=0) items = [] colors = ["#FFFFFF", "#FFD33D", "#5AC8FA", "#FF6B6B", "#8EE36A"] for i in range(14): y = int(height * (i + 1) / 15) item = canvas.create_text(width + 200, y, text=text, fill=colors[i % len(colors)], font=("Microsoft YaHei UI", 26, "bold"), anchor="w") items.append([item, speed + (i % 4)]) def tick(): alive = False for entry in items: canvas.move(entry[0], -entry[1], 0) x, _y = canvas.coords(entry[0]) if x > -600: alive = True if alive: win.after(30, tick) else: close() tick() self._fullscreen_window(builder) return f"弹幕已开始:{text[:20]}" def _do_marquee(self, params) -> str: text = str(params.get("text") or "").strip() or "欢迎" def builder(win, close): import tkinter as tk win.configure(bg="#0B1020") width = win.winfo_screenwidth() height = win.winfo_screenheight() canvas = tk.Canvas(win, bg="#0B1020", highlightthickness=0) canvas.pack(fill="both", expand=True) item = canvas.create_text(width, height // 2, text=text, fill="#FFD33D", font=("Microsoft YaHei UI", 60, "bold"), anchor="w") def tick(): canvas.move(item, -4, 0) x, _y = canvas.coords(item) if x < -len(text) * 70: close() return win.after(25, tick) tick() self._fullscreen_window(builder) return f"字幕滚动中:{text[:20]}" def _do_lottery(self, params) -> str: import random names = self.store.get("roll_call_names") or [] pool = names or [str(i) for i in range(1, 51)] def builder(win, close): import tkinter as tk win.configure(bg="#0B1020") label = tk.Label(win, text="", bg="#0B1020", fg="#FFD33D", font=("Microsoft YaHei UI", 120, "bold")) label.pack(expand=True, fill="both") state = {"n": 0, "winner": random.choice(pool)} def tick(): state["n"] += 1 if state["n"] <= 22: label.configure(text=random.choice(pool), fg="#FFD33D") win.after(60 + state["n"] * 12, tick) else: label.configure(text=state["winner"], fg="#5AC8FA") try: win.bell() except Exception: # noqa: BLE001 pass tick() self._fullscreen_window(builder) return "抽奖中…" def _do_dice(self, params) -> str: import random faces = ["⚀", "⚁", "⚂", "⚃", "⚄", "⚅"] result = random.randint(1, 6) def builder(win, close): import tkinter as tk win.configure(bg="#0B1020") label = tk.Label(win, text="", bg="#0B1020", fg="#FFFFFF", font=("Segoe UI Symbol", 200, "bold")) label.pack(expand=True, fill="both") state = {"n": 0, "final": faces[result - 1]} def tick(): state["n"] += 1 if state["n"] <= 14: label.configure(text=random.choice(faces)) win.after(70, tick) else: label.configure(text=state["final"], fg="#5AC8FA") tick() self._fullscreen_window(builder) return f"掷出 {result} 点" def _do_coin_flip(self, params) -> str: import random result = random.choice(["正", "反"]) def builder(win, close): import tkinter as tk win.configure(bg="#0B1020") label = tk.Label(win, text="", bg="#0B1020", fg="#FFFFFF", font=("Microsoft YaHei UI", 200, "bold")) label.pack(expand=True, fill="both") state = {"n": 0} def tick(): state["n"] += 1 if state["n"] <= 16: label.configure(text=random.choice(["正", "反"])) win.after(80, tick) else: label.configure(text=result, fg="#FFD33D") tick() self._fullscreen_window(builder) return f"硬币:{result}" def _do_countdown_321(self, params) -> str: def builder(win, close): import tkinter as tk win.configure(bg="#0B1020") label = tk.Label(win, text="", bg="#0B1020", fg="#FFFFFF", font=("Consolas", 260, "bold")) label.pack(expand=True, fill="both") state = {"n": 3} def tick(): if state["n"] <= 0: label.configure(text="开始!", fg="#8EE36A", font=("Microsoft YaHei UI", 120, "bold")) win.after(900, close) return label.configure(text=str(state["n"]), fg="#FF6B6B" if state["n"] == 1 else "#FFFFFF") try: win.bell() except Exception: # noqa: BLE001 pass state["n"] -= 1 win.after(1000, tick) tick() self._fullscreen_window(builder) return "倒计时开始" def _do_cinema_mode(self, params) -> str: def builder(win, close): import tkinter as tk win.configure(bg="#000000") tk.Label(win, text="影院模式", bg="#000000", fg="#1A1A1A", font=("Microsoft YaHei UI", 40, "bold")).pack( expand=True, fill="both") self._fullscreen_window(builder) return "影院模式(Esc 退出)" def _do_random_song(self, params) -> str: import random files = self.actions.list_local_audio() if not files: raise ValueError("音乐目录里没有音频文件") picked = random.choice(files) self.actions.play_audio(picked["path"]) return f"正在播放:{picked['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}" def _scores(self) -> dict: value = self.store.get("score_board") if not isinstance(value, dict): value = {"A": 0, "B": 0} value.setdefault("A", 0) value.setdefault("B", 0) return value def _show_score_board(self) -> None: scores = self._scores() def builder(win, close): import tkinter as tk win.configure(bg="#0B1020") row = tk.Frame(win, bg="#0B1020") row.pack(expand=True, fill="both") for team, color in (("A", "#5AC8FA"), ("B", "#FFD33D")): box = tk.Frame(row, bg="#0B1020") box.pack(side="left", expand=True, fill="both") tk.Label(box, text=f"{team} 队", bg="#0B1020", fg=color, font=("Microsoft YaHei UI", 40, "bold")).pack(pady=(60, 0)) tk.Label(box, text=str(scores.get(team, 0)), bg="#0B1020", fg="#FFFFFF", font=("Consolas", 200, "bold")).pack( expand=True) self._fullscreen_window(builder) def _do_score_board(self, params) -> str: self._show_score_board() scores = self._scores() return f"A 队 {scores['A']} : {scores['B']} B 队" def _do_score_change(self, params) -> str: team = str(params.get("team") or "A").strip().upper() if team not in ("A", "B"): raise ValueError("队伍只能填 A 或 B") try: delta = int(params.get("delta") or 0) except (TypeError, ValueError): raise ValueError("分数要填数字") scores = self._scores() scores[team] = scores[team] + delta self.store.set("score_board", scores) self._show_score_board() return f"A 队 {scores['A']} : {scores['B']} B 队" def _do_cheer(self, params) -> str: def builder(win, close): import tkinter as tk import random win.configure(bg="#0B1020") width = win.winfo_screenwidth() height = win.winfo_screenheight() canvas = tk.Canvas(win, bg="#0B1020", highlightthickness=0) canvas.pack(fill="both", expand=True) canvas.create_text(width // 2, height // 2, text="加 油 !", fill="#FFD33D", font=("Microsoft YaHei UI", 120, "bold")) colors = ["#FF6B6B", "#FFD33D", "#5AC8FA", "#8EE36A", "#FFFFFF"] flakes = [] for _ in range(90): x = random.randint(0, width) y = random.randint(-height, 0) item = canvas.create_rectangle(x, y, x + 10, y + 16, fill=random.choice(colors), outline="") flakes.append(item) def tick(): for item in flakes: canvas.move(item, 0, 7) _x, y = canvas.coords(item)[:2] if y > height: canvas.coords(item, random.randint(0, width), -20, random.randint(0, width) + 10, 0) win.after(40, tick) tick() self._fullscreen_window(builder) return "加油!" # ================================================================== # 极客 # ================================================================== def _do_matrix_rain(self, params) -> str: def builder(win, close): import tkinter as tk import random win.configure(bg="#000000") width = win.winfo_screenwidth() height = win.winfo_screenheight() canvas = tk.Canvas(win, bg="#000000", highlightthickness=0) canvas.pack(fill="both", expand=True) font_size = 18 columns = max(1, width // font_size) drops = [random.randint(-40, 0) for _ in range(columns)] chars = "01アイウエオカキクケコサシスセソタチツテト" def tick(): 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") drops[i] = y + 1 if y * font_size < height else random.randint(-20, 0) win.after(60, tick) tick() self._fullscreen_window(builder) return "数字雨已开始(Esc 退出)" def _do_starfield(self, params) -> str: def builder(win, close): import tkinter as tk import random win.configure(bg="#000010") width = win.winfo_screenwidth() height = win.winfo_screenheight() canvas = tk.Canvas(win, bg="#000010", highlightthickness=0) canvas.pack(fill="both", expand=True) stars = [] for _ in range(240): x = random.uniform(-width, width) y = random.uniform(-height, height) z = random.uniform(1, width) item = canvas.create_oval(0, 0, 0, 0, fill="#FFFFFF", outline="") stars.append([x, y, z, item]) def tick(): for star in stars: star[2] -= 16 if star[2] <= 1: star[0] = random.uniform(-width, width) star[1] = random.uniform(-height, height) star[2] = width k = 220 / star[2] px = star[0] * k + width / 2 py = star[1] * k + height / 2 size = max(1, int((1 - star[2] / width) * 4)) canvas.coords(star[3], px, py, px + size, py + size) win.after(33, tick) tick() self._fullscreen_window(builder) return "星空穿梭中(Esc 退出)" def _do_dashboard(self, params) -> str: def builder(win, close): import tkinter as tk win.configure(bg="#0B1020") frame = tk.Frame(win, bg="#0B1020") frame.pack(expand=True, fill="both", padx=60, pady=60) titles = {} for key, title, color in (("cpu", "CPU", "#5AC8FA"), ("mem", "内存", "#FFD33D"), ("net", "网络", "#8EE36A")): box = tk.Frame(frame, bg="#0B1020") box.pack(fill="x", pady=16) tk.Label(box, text=title, bg="#0B1020", fg=color, font=("Microsoft YaHei UI", 28, "bold"), width=6, anchor="w").pack(side="left") value = tk.Label(box, text="--", bg="#0B1020", fg="#FFFFFF", font=("Consolas", 60, "bold"), anchor="w") value.pack(side="left", fill="x", expand=True) titles[key] = value state = {"last": None, "last_bytes": None} def tick(): info = self.actions.perf() titles["cpu"].configure(text=f"{info.get('cpu_percent', 0)} %") titles["mem"].configure( text=f"{info.get('mem_percent', 0)} % " f"({info.get('mem_used_gb')}/{info.get('mem_total_gb')} GB)") _code, out = _run(["powershell", "-NoProfile", "-Command", "(Get-NetAdapterStatistics | Measure-Object " "-Property ReceivedBytes -Sum).Sum"]) digits = re.findall(r"\d+", out or "") now_bytes = int(digits[0]) if digits else 0 if state["last_bytes"] is not None and state["last"] is not None: delta = now_bytes - state["last_bytes"] elapsed = max(0.1, time.time() - state["last"]) mbps = delta / elapsed / 1048576 titles["net"].configure(text=f"{mbps:.2f} MB/s") state["last_bytes"] = now_bytes state["last"] = time.time() win.after(1500, tick) tick() self._fullscreen_window(builder) return "实时监控已打开(Esc 退出)" def _do_qr_code(self, params) -> str: text = str(params.get("text") or "").strip() if not text: raise ValueError("内容为空") import tempfile import urllib.parse import urllib.request url = ("https://api.qrserver.com/v1/create-qr-code/?size=520x520&data=" + urllib.parse.quote(text)) target = Path(tempfile.gettempdir()) / "_lanremote_qr.png" try: with urllib.request.urlopen(url, timeout=10) as resp: target.write_bytes(resp.read()) except Exception as exc: # noqa: BLE001 raise ValueError(f"生成二维码失败(要联网): {exc}") from exc self.actions.show_image(str(target), f"二维码 - {text[:20]}") return f"二维码已显示(内容 {len(text)} 字)" def _do_hash_text(self, params) -> str: import hashlib text = str(params.get("text") or "") if not text: raise ValueError("内容为空") data = text.encode("utf-8") return ("MD5: " + hashlib.md5(data).hexdigest() + "\nSHA1: " + hashlib.sha1(data).hexdigest() + "\nSHA256: " + hashlib.sha256(data).hexdigest()) def _do_base64_encode(self, params) -> str: import base64 text = str(params.get("text") or "") if not text: raise ValueError("内容为空") result = base64.b64encode(text.encode("utf-8")).decode("ascii") self.actions.clip_set(result) return result def _do_base64_decode(self, params) -> str: import base64 text = str(params.get("text") or "").strip() if not text: raise ValueError("内容为空") try: result = base64.b64decode(text).decode("utf-8", errors="replace") except Exception as exc: # noqa: BLE001 raise ValueError(f"不是合法的 Base64: {exc}") from exc self.actions.clip_set(result) return result[:500] def _do_uuid_gen(self, params) -> str: import uuid value = str(uuid.uuid4()) self.actions.clip_set(value) return f"已生成并复制:\n{value}" def _do_timestamp_now(self, params) -> str: now = time.time() return (f"秒:{int(now)}\n毫秒:{int(now * 1000)}\n" f"本地:{time.strftime('%Y-%m-%d %H:%M:%S')}\n" f"UTC:{time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(now))}") def _do_json_format(self, params) -> str: import json text = str(params.get("text") or "").strip() if not text: raise ValueError("内容为空") try: obj = json.loads(text) except ValueError as exc: raise ValueError(f"不是合法 JSON: {exc}") from exc pretty = json.dumps(obj, ensure_ascii=False, indent=2) self.actions.clip_set(pretty) return pretty[:800] def _do_screen_color(self, params) -> str: if not IS_WINDOWS: raise ValueError("仅支持 Windows") class Point(ctypes.Structure): _fields_ = [("x", ctypes.c_long), ("y", ctypes.c_long)] point = Point() ctypes.windll.user32.GetCursorPos(ctypes.byref(point)) hdc = ctypes.windll.user32.GetDC(0) color = ctypes.windll.gdi32.GetPixel(hdc, point.x, point.y) ctypes.windll.user32.ReleaseDC(0, hdc) if color < 0: raise ValueError("取色失败") r = color & 0xFF g = (color >> 8) & 0xFF b = (color >> 16) & 0xFF hex_color = f"#{r:02X}{g:02X}{b:02X}" self.actions.clip_set(hex_color) return (f"鼠标位置 ({point.x}, {point.y})\n" f"颜色 {hex_color}\nRGB({r}, {g}, {b})") def _do_speed_test(self, params) -> str: import urllib.request url = "https://speed.cloudflare.com/__down?bytes=5000000" try: start = time.time() with urllib.request.urlopen(url, timeout=20) as resp: total = 0 while True: chunk = resp.read(65536) if not chunk: break total += len(chunk) if time.time() - start > 12: break elapsed = max(0.01, time.time() - start) except Exception as exc: # noqa: BLE001 raise ValueError(f"测速失败(要联网): {exc}") from exc mbps = total * 8 / elapsed / 1_000_000 return (f"下载 {total / 1048576:.1f} MB,用时 {elapsed:.1f} 秒\n" f"约 {mbps:.1f} Mbps({total / elapsed / 1048576:.2f} MB/s)") def _do_port_check(self, params) -> str: import socket host = str(params.get("host") or "").strip() if not host: raise ValueError("地址为空") if not re.match(r"^[A-Za-z0-9_.:\-]+$", host): raise ValueError("地址含有不合法字符") common = [21, 22, 23, 25, 53, 80, 110, 135, 139, 443, 445, 1433, 3306, 3389, 5432, 5900, 6379, 8080, 8765] open_ports = [] for port in common: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(0.45) try: if sock.connect_ex((host, port)) == 0: open_ports.append(port) except OSError: pass finally: sock.close() if not open_ports: return f"{host}:常用端口都没开" return f"{host} 开放端口:{', '.join(str(p) for p in open_ports)}" # ================================================================== # 静默运行 # ================================================================== def _autostart_on(self) -> bool: try: from .autostart import is_enabled return is_enabled() except Exception: # noqa: BLE001 return False def _do_autostart(self, params) -> str: from .autostart import toggle return toggle(self._truthy(params.get("on"))) def _do_autostart_status(self, params) -> str: from .autostart import status as auto_status info = auto_status() lines = [ f"开机自启:{'已开启' if info['enabled'] else '未开启'}", f"已配设备:{len(self.store.device_list())} 台", f"允许新配对:" f"{'是' if self.store.get('allow_new_pairing', True) else '否'}", f"监听端口:{self.store.get('port', 8765)}", ] if info["enabled"]: lines.append(f"自启命令:{info['command']}") return "\n".join(lines) def _do_quit_agent(self, params) -> str: """退出被控端进程。""" import os import threading def later(): time.sleep(0.6) # 先让这条回复发出去 os._exit(0) threading.Thread(target=later, daemon=True, name="quit").start() return "被控端正在退出" def _do_silent_help(self, params) -> str: return ("静默模式:没有任何窗口,也没有终端,只在后台监听。\n" "已经配过的手表会自动重连,不用再输配对码。\n" "加新设备要先退出,用带窗口的方式启动," "或运行 ykxmz.exe --add-device 10") # ================================================================== # 查看(续) # ================================================================== def _do_screen_resolution(self, params) -> str: if not IS_WINDOWS: return "仅支持 Windows" user32 = ctypes.windll.user32 user32.SetProcessDPIAware() width = user32.GetSystemMetrics(0) height = user32.GetSystemMetrics(1) monitors = user32.GetSystemMetrics(80) return f"主显示器 {width} x {height}\n显示器数量:{monitors}" def _do_list_printers(self, params) -> str: script = ("Get-Printer | ForEach-Object { " "\"$($_.Name) [$($_.Status)]\" }") _code, out = _run(["powershell", "-NoProfile", "-Command", script], timeout=25) lines = [l for l in out.splitlines() if l.strip()] return f"打印机 {len(lines)} 台:\n" + "\n".join(lines) if lines else "没有打印机" def _do_list_drives(self, params) -> str: script = ("Get-Volume | Where-Object {$_.DriveLetter} | ForEach-Object { " "\"$($_.DriveLetter): $($_.FileSystemLabel) " "$([math]::Round($_.SizeRemaining/1GB,1))/" "$([math]::Round($_.Size/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 "读不到驱动器" def _do_audio_devices(self, params) -> str: script = ("Get-CimInstance Win32_SoundDevice | ForEach-Object { " "\"$($_.Name) [$($_.Status)]\" }") _code, out = _run(["powershell", "-NoProfile", "-Command", script], timeout=25) lines = [l for l in out.splitlines() if l.strip()] return "\n".join(lines) or "读不到音频设备" def _do_usb_devices(self, params) -> str: script = ("Get-CimInstance Win32_USBControllerDevice | " "ForEach-Object { [wmi]$_.Dependent } | " "Select-Object -ExpandProperty Name -Unique") _code, out = _run(["powershell", "-NoProfile", "-Command", script], timeout=30) lines = [l for l in out.splitlines() if l.strip()] return f"USB 设备 {len(lines)} 个:\n" + "\n".join(lines[:30]) def _do_installed_drivers(self, params) -> str: script = ("Get-CimInstance Win32_PnPSignedDriver | " "Where-Object {$_.DeviceName} | " "Select-Object -ExpandProperty DeviceName -Unique") _code, out = _run(["powershell", "-NoProfile", "-Command", script], timeout=35) lines = [l for l in out.splitlines() if l.strip()] return f"驱动 {len(lines)} 个(前 30):\n" + "\n".join(lines[:30]) def _do_windows_detail(self, params) -> str: script = ("$o=Get-CimInstance Win32_OperatingSystem;" "\"版本: $($o.Caption)\";" "\"构建: $($o.BuildNumber)\";" "\"架构: $($o.OSArchitecture)\";" "\"安装日期: $($o.InstallDate)\";" "\"最后启动: $($o.LastBootUpTime)\"") _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_recycle_size(self, params) -> str: script = ( "$s=(New-Object -ComObject Shell.Application)" ".NameSpace(10).Items() | " "Measure-Object -Property Size -Sum;" "\"$($s.Count) 项,共 " "$([math]::Round($s.Sum/1MB,1)) MB\"") _code, out = _run(["powershell", "-NoProfile", "-Command", script], timeout=30) text = out.strip().splitlines() return text[-1] if text else "读不到回收站" def _do_temp_size(self, params) -> str: import os as _os total = 0 count = 0 for root in (_os.environ.get("TEMP", ""), r"C:\Windows\Temp"): if not root or not Path(root).is_dir(): continue try: for item in Path(root).rglob("*"): try: if item.is_file(): total += item.stat().st_size count += 1 except OSError: continue except OSError: continue return f"临时文件 {count} 个,共 {total / 1048576:.1f} MB" def _do_certificates(self, params) -> str: script = ("Get-ChildItem Cert:\LocalMachine\Root | " "Select-Object -First 25 -ExpandProperty Subject") _code, out = _run(["powershell", "-NoProfile", "-Command", script], timeout=30) lines = [l for l in out.splitlines() if l.strip()] return f"受信任根证书(前 25 / 共若干):\n" + "\n".join(lines) # ================================================================== # 网络(续) # ================================================================== def _do_open_ports(self, params) -> str: script = ("Get-NetTCPConnection -State Listen | " "Where-Object {$_.LocalAddress -eq '0.0.0.0'} | " "Select-Object -ExpandProperty LocalPort -Unique | " "Sort-Object") _code, out = _run(["powershell", "-NoProfile", "-Command", script], timeout=25) ports = [p.strip() for p in out.splitlines() if p.strip().isdigit()] return f"对外监听端口 {len(ports)} 个:\n" + ", ".join(ports) def _do_firewall_status(self, params) -> str: script = ("Get-NetFirewallProfile | ForEach-Object { " "\"$($_.Name): $($_.Enabled)\" }") _code, out = _run(["powershell", "-NoProfile", "-Command", script], timeout=25) lines = [l for l in out.splitlines() if l.strip()] if not lines: # NetSecurity 模块坏掉时退化到 netsh _code, out = _run(["netsh", "advfirewall", "show", "allprofiles", "state"], timeout=25) lines = [l.strip() for l in out.splitlines() if "ON" in l or "OFF" in l or "开" in l or "关" in l] return "\n".join(lines) or "读不到防火墙状态" def _do_dns_servers(self, params) -> str: script = ("Get-DnsClientServerAddress | " "Where-Object {$_.ServerAddresses} | ForEach-Object { " "\"$($_.InterfaceAlias): " "$($_.ServerAddresses -join ', ')\" }") _code, out = _run(["powershell", "-NoProfile", "-Command", script], timeout=25) lines = [l for l in out.splitlines() if l.strip()] return "\n".join(lines) or "读不到 DNS" def _do_hosts_file(self, params) -> str: path = Path(r"C:\Windows\System32\drivers\etc\hosts") if not path.is_file(): return "读不到 hosts" try: lines = [l.rstrip() for l in path.read_text( encoding="utf-8", errors="replace").splitlines() if l.strip() and not l.strip().startswith("#")] except OSError as exc: raise ValueError(f"读不到: {exc}") from exc return (f"hosts 里 {len(lines)} 条有效记录:\n" + "\n".join(lines[:40]) if lines else "hosts 里没有自定义记录") def _do_wifi_networks(self, params) -> str: _code, out = _run(["netsh", "wlan", "show", "networks"], timeout=25) lines = [] for raw in out.splitlines(): text = raw.strip() if text.startswith("SSID") and "BSSID" not in text: lines.append(text) return (f"附近 {len(lines)} 个 WiFi:\n" + "\n".join(lines[:25]) if lines else "附近没扫到 WiFi") def _do_bluetooth_devices(self, params) -> str: script = ("Get-PnpDevice -Class Bluetooth -Status OK | " "Select-Object -ExpandProperty FriendlyName") _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[:25]) if lines else "没有蓝牙设备" # ================================================================== # 系统(续) # ================================================================== def _open_msc(self, name: str, label: str) -> str: try: subprocess.Popen([name], shell=True, creationflags=_NO_WINDOW) except Exception as exc: # noqa: BLE001 raise ValueError(f"打不开{label}: {exc}") from exc return f"已打开{label}" def _do_open_services(self, params) -> str: return self._open_msc("services.msc", "服务") def _do_open_task_scheduler(self, params) -> str: return self._open_msc("taskschd.msc", "任务计划") def _do_open_device_manager(self, params) -> str: return self._open_msc("devmgmt.msc", "设备管理器") def _do_open_disk_cleanup(self, params) -> str: return self._open_msc("cleanmgr.exe", "磁盘清理") def _do_open_windows_update(self, params) -> str: subprocess.Popen(["cmd", "/c", "start", "ms-settings:windowsupdate"], creationflags=_NO_WINDOW) return "已打开系统更新" def _do_clear_temp(self, params) -> str: import os as _os removed = 0 freed = 0 for root in (_os.environ.get("TEMP", ""), r"C:\Windows\Temp"): if not root or not Path(root).is_dir(): continue try: for item in Path(root).iterdir(): try: if item.is_file(): size = item.stat().st_size item.unlink() removed += 1 freed += size elif item.is_dir(): import shutil as _shutil _shutil.rmtree(item, ignore_errors=True) removed += 1 except OSError: continue # 正在被占用的跳过 except OSError: continue return f"清理了 {removed} 项,释放约 {freed / 1048576:.1f} MB" def _do_env_vars(self, params) -> str: names = ["PATH", "TEMP", "USERPROFILE", "COMPUTERNAME", "USERNAME", "OS", "PROCESSOR_IDENTIFIER", "SystemRoot"] lines = [] for name in names: value = os.environ.get(name, "") if name == "PATH": value = f"({len(value.split(os.pathsep))} 个路径)" lines.append(f"{name} = {value[:80]}") return "\n".join(lines) # ================================================================== # 教室管控 # ================================================================== # 这些策略写在 HKCU 下,改完广播一次让 explorer 立刻生效 RESTRICT_KEYS = { "hide_icons": ("HKCU\\Software\\Microsoft\\Windows\\CurrentVersion" "\\Explorer\\Advanced", "HideIcons"), "restrict_taskmgr": ("HKCU\\Software\\Microsoft\\Windows" "\\CurrentVersion\\Policies\\System", "DisableTaskMgr"), "restrict_regedit": ("HKCU\\Software\\Microsoft\\Windows" "\\CurrentVersion\\Policies\\System", "DisableRegistryTools"), "restrict_cmd": ("HKCU\\Software\\Microsoft\\Windows" "\\CurrentVersion\\Policies\\System", "DisableCMD"), "restrict_wallpaper": ("HKCU\\Software\\Microsoft\\Windows" "\\CurrentVersion\\Policies\\System", "NoDispBackgroundPage"), "restrict_power": ("HKCU\\Software\\Microsoft\\Windows" "\\CurrentVersion\\Policies\\Explorer", "NoClose"), } RESTRICT_LABELS = { "hide_icons": "隐藏桌面图标", "restrict_taskmgr": "禁用任务管理器", "restrict_regedit": "禁用注册表编辑器", "restrict_cmd": "禁用命令提示符", "restrict_wallpaper": "禁止换壁纸", "restrict_power": "藏掉关机按钮", "lock_input": "锁键盘鼠标", "hosts": "网站屏蔽", } def _reg_on(self, path: str, name: str, on_value) -> bool: code, out = _run(["reg", "query", path, "/v", name]) return code == 0 and str(on_value) in out.replace("0x", "") def _reg_set(self, path: str, name: str, value) -> bool: code, _out = _run(["reg", "add", path, "/v", name, "/t", "REG_DWORD", "/d", str(value), "/f"]) return code == 0 def _do_hide_icons(self, params) -> str: return self._toggle_restrict("hide_icons", params) def _do_restrict_taskmgr(self, params) -> str: return self._toggle_restrict("restrict_taskmgr", params) def _do_restrict_regedit(self, params) -> str: return self._toggle_restrict("restrict_regedit", params) def _do_restrict_cmd(self, params) -> str: return self._toggle_restrict("restrict_cmd", params) def _do_restrict_wallpaper(self, params) -> str: return self._toggle_restrict("restrict_wallpaper", params) def _do_restrict_power(self, params) -> str: return self._toggle_restrict("restrict_power", params) def _toggle_restrict(self, name: str, params) -> str: path, value_name = self.RESTRICT_KEYS[name] label = self.RESTRICT_LABELS[name] on = self._truthy(params.get("on")) if not self._reg_set(path, value_name, 1 if on else 0): raise ValueError(f"{label}设置失败(可能被策略锁住)") self._refresh_explorer() return f"{label}:{'已开启' if on else '已关闭'}" def _refresh_explorer(self) -> None: try: self.actions._mci # noqa: B018 仅确认对象存在 except Exception: # noqa: BLE001 pass HWND_BROADCAST = 0xFFFF WM_SETTINGCHANGE = 0x001A try: ctypes.windll.user32.SendMessageW(HWND_BROADCAST, WM_SETTINGCHANGE, 0, 0) except Exception: # noqa: BLE001 pass def _do_lock_input(self, params) -> str: """锁键盘鼠标。注意:这会连老师自己也动不了,必须靠手表解开。""" on = self._truthy(params.get("on")) try: ok = ctypes.windll.user32.BlockInput(bool(on)) except Exception as exc: # noqa: BLE001 raise ValueError(f"锁输入失败(需要管理员):{exc}") from exc if not ok: raise ValueError("锁输入失败,需要以管理员身份运行被控端") return "键盘鼠标已锁定(用手表解锁)" if on else "键盘鼠标已解锁" # ---------------- 网站屏蔽(hosts)---------------- HOSTS = Path(r"C:\Windows\System32\drivers\etc\hosts") MARK = "# ==== LanRemote 屏蔽 ====" COMMON_GAMES = [ "4399.com", "www.4399.com", "7k7k.com", "www.7k7k.com", "xiaoyouxi.4399.com", "qq.com", "www.qq.com", "game.qq.com", "steamcommunity.com", "store.steampowered.com", "bilibili.com", "www.bilibili.com", "douyin.com", "www.douyin.com", "huya.com", "www.huya.com", "douyu.com", "www.douyu.com", ] def _hosts_read(self) -> list: try: text = self.HOSTS.read_text(encoding="utf-8", errors="replace") except OSError as exc: raise ValueError(f"读不了 hosts(需要管理员):{exc}") from exc return text.splitlines() def _hosts_blocked(self) -> list: sites = [] try: for line in self._hosts_read(): stripped = line.strip() if not stripped.startswith("0.0.0.0"): continue parts = stripped.split() if len(parts) >= 2: sites.append(parts[-1]) except ValueError: return [] return sites def _hosts_write(self, sites: list) -> None: lines = [l for l in self._hosts_read() if not l.strip().startswith("0.0.0.0") and self.MARK not in l] if sites: lines.append("") lines.append(self.MARK) for site in sorted(set(sites)): lines.append(f"0.0.0.0 {site}") lines.append(f"0.0.0.0 www.{site}" if not site.startswith("www.") else f"0.0.0.0 {site[4:]}") try: self.HOSTS.write_text("\n".join(lines) + "\n", encoding="utf-8") except OSError as exc: raise ValueError( f"写不了 hosts(需要管理员)。以管理员身份运行被控端再试:{exc}" ) from exc _run(["ipconfig", "/flushdns"]) def _do_site_block(self, params) -> str: site = str(params.get("site") or "").strip().lower() site = site.replace("http://", "").replace("https://", "").strip("/") if not site or "." not in site: raise ValueError("网站格式不对,例如 game.com") sites = self._hosts_blocked() if site not in sites: sites.append(site) self._hosts_write(sites) return f"已屏蔽 {site}(共 {len(set(sites))} 个)" def _do_site_unblock(self, params) -> str: site = str(params.get("site") or "").strip().lower() site = site.replace("http://", "").replace("https://", "").strip("/") if not site: raise ValueError("网站为空") sites = [s for s in self._hosts_blocked() if s != site] self._hosts_write(sites) return f"已解除 {site}(剩 {len(set(sites))} 个)" def _do_site_list(self, params) -> str: sites = sorted(set(self._hosts_blocked())) if not sites: return "当前没有屏蔽任何网站" return f"已屏蔽 {len(sites)} 个:\n" + "\n".join(sites[:60]) def _do_site_clear(self, params) -> str: self._hosts_write([]) return "已清空全部网站屏蔽" def _do_game_block_common(self, params) -> str: sites = set(self._hosts_blocked()) sites.update(self.COMMON_GAMES) self._hosts_write(list(sites)) return f"已屏蔽 {len(sites)} 个常见游戏/视频网站" # ---------------- 进程 ---------------- GAME_WORDS = ["game", "steam", "wegame", "minecraft", "league", "lol", "dota", "csgo", "cf", "dnf", "battle.net", "epicgames", "原神", "yuanshen", "genshin", "4399", "7k7k", "flash"] def _do_kill_games(self, params) -> str: killed = [] for proc in self.actions.list_processes(limit=200): name = (proc.get("name") or "").lower() if any(word in name for word in self.GAME_WORDS): try: self.actions.kill_process(int(proc["pid"])) killed.append(proc["name"]) except Exception: # noqa: BLE001 continue if not killed: return "没有发现正在运行的游戏进程" return f"已结束 {len(killed)} 个:{', '.join(killed[:10])}" def _do_kill_by_name(self, params) -> str: name = str(params.get("name") or "").strip() if not name: raise ValueError("进程名为空") return self.actions.kill_process(0, name) # ---------------- 课堂 ---------------- def _do_bell(self, params) -> str: try: import winsound for freq in (880, 660, 990): winsound.Beep(freq, 260) except Exception: # noqa: BLE001 pass self._do_big_text({"text": "下课"}) return "下课铃" def _do_class_attention(self, params) -> str: self._do_big_text({"text": "请 安 静"}) return "已全屏提示" def _do_class_start(self, params) -> str: self.actions.media_control("mute") self._do_big_text({"text": "上课"}) return "已开始上课(全屏提示 + 静音)" def _do_class_timer(self, params) -> str: return self._do_countdown_screen(params) # ---------------- 恢复 / 状态 ---------------- def _do_restore_all(self, params) -> str: restored = [] for name in self.RESTRICT_KEYS: path, value_name = self.RESTRICT_KEYS[name] if self._reg_set(path, value_name, 0): restored.append(self.RESTRICT_LABELS[name]) try: ctypes.windll.user32.BlockInput(False) restored.append("锁键盘鼠标") except Exception: # noqa: BLE001 pass try: if self._hosts_blocked(): self._hosts_write([]) restored.append("网站屏蔽") except ValueError: pass self._refresh_explorer() if not restored: return "没有需要恢复的限制" return "已恢复:\n" + "\n".join(restored) def _do_restrict_status(self, params) -> str: active = [] for name, (path, value_name) in self.RESTRICT_KEYS.items(): if self._reg_on(path, value_name, 1): active.append(self.RESTRICT_LABELS[name]) try: if ctypes.windll.user32.GetAsyncKeyState(0) != 0: pass except Exception: # noqa: BLE001 pass sites = self._hosts_blocked() if sites: active.append(f"网站屏蔽 {len(set(sites))} 个") if not active: return "当前没有任何限制" return f"生效中的限制 {len(active)} 项:\n" + "\n".join(active) # ================================================================== # 教学(续) # ================================================================== def _roll_names(self) -> list: names = self.store.get("roll_call_names") or [] return [str(n) for n in names if str(n).strip()] def _do_roll_group(self, params) -> str: import random names = self._roll_names() if not names: raise ValueError("还没有名单,先点「设置点名名单」") try: size = max(2, min(20, int(params.get("size") or 4))) except (TypeError, ValueError): size = 4 pool = names[:] random.shuffle(pool) groups = [pool[i:i + size] for i in range(0, len(pool), size)] lines = [f"第 {i + 1} 组:" + "、".join(g) for i, g in enumerate(groups)] self._do_big_text({"text": f"分 {len(groups)} 组"}) return "\n".join(lines) def _do_roll_call_no_repeat(self, params) -> str: import random names = self._roll_names() if not names: raise ValueError("还没有名单,先点「设置点名名单」") used = self.store.get("roll_call_used") or [] left = [n for n in names if n not in used] if not left: return "全班都点过了(点「重置点名记录」重来)" picked = random.choice(left) used.append(picked) self.store.set("roll_call_used", used) self._do_big_text({"text": picked}) return f"点到:{picked}(还剩 {len(left) - 1} 人没点过)" def _do_roll_call_reset(self, params) -> str: self.store.set("roll_call_used", []) return "点名记录已重置" def _do_class_schedule(self, params) -> str: raw = str(params.get("text") or "").strip() if not raw: self.store.set("class_schedule", "") return "课程表已清空" for sep in (",", "、", ";", ";"): raw = raw.replace(sep, ",") items = [x.strip() for x in raw.split(",") if x.strip()] self.store.set("class_schedule", items) self._do_big_text({"text": "\n".join( f"{i + 1}. {x}" for i, x in enumerate(items))}) return f"课程表已保存 {len(items)} 节" def _do_hand_count(self, params) -> str: count = int(self.store.get("hand_count") or 0) + 1 self.store.set("hand_count", count) self._do_big_text({"text": str(count)}) return f"举手计数:{count}" def _do_hand_reset(self, params) -> str: self.store.set("hand_count", 0) return "举手计数已清零" def _do_answer_poll(self, params) -> str: counts = self.store.get("answer_poll") or {} if not isinstance(counts, dict): counts = {} lines = [f"{k}:{v} 人" for k, v in sorted(counts.items())] or ["还没记录"] lines.append("") lines.append("(在电脑上点画面对应的选项还可以继续加)") return "\n".join(lines) def _do_magnifier(self, params) -> str: subprocess.Popen("magnify.exe", shell=True, creationflags=_NO_WINDOW) return "已打开屏幕放大镜" def _do_projector_switch(self, params) -> str: # Win + P self._key_combo(0x5B, 0x50) return "已弹出投影模式选择(Win+P)" def _do_lesson_timer(self, params) -> str: started = self.store.get("lesson_started") if started: import time as _t mins = int((_t.time() - float(started)) / 60) self.store.set("lesson_started", 0) return f"本节课上了 {mins} 分钟" import time as _t self.store.set("lesson_started", _t.time()) return "开始计时(再点一次看上了多久)" # ================================================================== # 教室(续) # ================================================================== def _do_lock_now(self, params) -> str: ctypes.windll.user32.LockWorkStation() return "已锁屏" def _game_watch_loop(self) -> None: import time as _t while self.store.get("game_watch"): try: for proc in self.actions.list_processes(limit=200): name = (proc.get("name") or "").lower() if any(w in name for w in self.GAME_WORDS): self._do_big_text({"text": f"别玩游戏\n{proc['name']}"}) break except Exception: # noqa: BLE001 pass _t.sleep(15) def _do_game_watch_start(self, params) -> str: if self.store.get("game_watch"): return "已经在盯着了" self.store.set("game_watch", True) import threading threading.Thread(target=self._game_watch_loop, daemon=True, name="game-watch").start() return "开始盯游戏进程(每 15 秒查一次,发现就全屏提醒)" def _do_game_watch_stop(self, params) -> str: self.store.set("game_watch", False) return "已停止盯游戏进程" def _do_usb_check(self, params) -> str: script = ("Get-CimInstance Win32_DiskDrive | Where-Object " "{$_.InterfaceType -eq 'USB'} | ForEach-Object { " "\"$($_.Model) $([math]::Round($_.Size/1GB,1))GB\" }") _code, out = _run(["powershell", "-NoProfile", "-Command", script], timeout=25) lines = [l for l in out.splitlines() if l.strip()] return ("发现 U 盘:" + " | ".join(lines)) if lines else "没有插 U 盘" def _do_login_history(self, params) -> str: script = ("Get-WinEvent -LogName Security -MaxEvents 200 " "-FilterXPath '*[System[EventID=4624]]' " "-ErrorAction SilentlyContinue | Select-Object -First 10 | " "ForEach-Object { $_.TimeCreated.ToString('MM-dd HH:mm') }") _code, out = _run(["powershell", "-NoProfile", "-Command", script], timeout=30) lines = [l for l in out.splitlines() if l.strip()] if not lines: return "读不到登录记录(需要管理员权限)" return "最近登录:\n" + "\n".join(lines) def _do_boot_history(self, params) -> str: script = ("Get-WinEvent -LogName System -MaxEvents 500 " "-ErrorAction SilentlyContinue | " "Where-Object {$_.Id -eq 6005 -or $_.Id -eq 6006} | " "Select-Object -First 8 | " "ForEach-Object { $_.TimeCreated.ToString('MM-dd HH:mm') }") _code, out = _run(["powershell", "-NoProfile", "-Command", script], timeout=30) lines = [l for l in out.splitlines() if l.strip()] if not lines: return f"读不到开关机记录;本次已开机 {self.actions._uptime()}" return "最近开关机:\n" + "\n".join(lines) def _do_clean_desktop(self, params) -> str: desktop = Path.home() / "Desktop" if not desktop.is_dir(): raise ValueError("找不到桌面目录") box = desktop / "_桌面整理" box.mkdir(exist_ok=True) moved = 0 for item in desktop.iterdir(): if item == box or item.is_dir(): continue try: item.rename(box / item.name) moved += 1 except OSError: continue return f"把 {moved} 个文件收进了「_桌面整理」文件夹" def _do_block_new_app(self, params) -> str: on = self._truthy(params.get("on")) # 用软件限制策略(HKCU 下不需要管理员) path = r"Software\Microsoft\Windows\CurrentVersion\Policies\Explorer" ok = self._reg_set(f"HKCU\\{path}", "DisallowRun", 1 if on else 0) if not ok: raise ValueError("设置失败,可能被组策略锁住") return f"禁止新程序启动:{'已开启' if on else '已关闭'}" def _do_show_desktop_now(self, params) -> str: self._key_combo(0x5B, 0x44) return "已显示桌面" def _do_count_windows(self, params) -> str: user32 = ctypes.windll.user32 EnumProc = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p) own = os.getpid() titles = [] def callback(hwnd, _lparam): 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: return True buf = ctypes.create_unicode_buffer(length + 1) user32.GetWindowTextW(hwnd, buf, length + 1) titles.append(buf.value) return True user32.EnumWindows(EnumProc(callback), 0) if not titles: return "没有打开的窗口" return f"开着 {len(titles)} 个窗口:\n" + "\n".join(titles[:15]) def _do_class_end_cleanup(self, params) -> str: parts = [] try: parts.append(self._do_kill_games({})) except Exception: # noqa: BLE001 pass try: self.store.set("game_watch", False) parts.append("已停止盯进程") except Exception: # noqa: BLE001 pass try: parts.append(self._do_restore_all({})) except Exception: # noqa: BLE001 pass return "下课清理完成:\n" + "\n".join(parts) def _do_who_is_playing(self, params) -> str: found = [] for proc in self.actions.list_processes(limit=200): name = (proc.get("name") or "").lower() if any(w in name for w in self.GAME_WORDS): found.append(f"{proc['name']} (PID {proc['pid']})") if not found: return "没有发现游戏进程" return f"发现 {len(found)} 个:\n" + "\n".join(found[:15]) # ================================================================== # 系统(续) # ================================================================== def _do_open_taskmgr2(self, params) -> str: return self._open_msc("taskmgr.exe", "任务管理器") def _do_open_resmon(self, params) -> str: return self._open_msc("resmon.exe", "资源监视器") def _do_power_plan(self, params) -> str: _code, out = _run(["powercfg", "/list"]) plans = re.findall(r"([0-9a-fA-F-]{36})\s*\(([^)]+)\)", out or "") if not plans: return "读不到电源计划" lines = [f"{name}" for _guid, name in plans] return "可用的电源计划:\n" + "\n".join(lines) def _do_window_top(self, params) -> str: """把当前前台窗口切换置顶。""" user32 = ctypes.windll.user32 hwnd = user32.GetForegroundWindow() if not hwnd: raise ValueError("没有前台窗口") HWND_TOPMOST = -1 HWND_NOTOPMOST = -2 SWP_NOMOVE = 0x0002 SWP_NOSIZE = 0x0001 # 先试着取消置顶,成功就说明本来是置顶的 user32.SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE) user32.SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE) return "已把当前窗口置顶" def _do_screen_off_lock(self, params) -> str: ctypes.windll.user32.LockWorkStation() import time as _t _t.sleep(0.4) self.actions.monitor_power(False) return "已关屏并锁定" def _do_clear_all_clip(self, params) -> str: self._clip_history = [] self.store.set("feat_clip_fav", []) return "剪贴板历史和收藏夹已清空" def _do_restart_audio(self, params) -> str: _run(["net", "stop", "Audiosrv", "/y"]) _run(["net", "start", "Audiosrv"]) return "音频服务已重启(可能需要管理员权限)" def _do_system_sounds(self, params) -> str: subprocess.Popen(["cmd", "/c", "start", "ms-settings:sound"], creationflags=_NO_WINDOW) return "已打开声音设置" # ================================================================== # 查看(续) # ================================================================== def _do_cpu_model(self, params) -> str: script = ("$c=Get-CimInstance Win32_Processor|Select-Object -First 1;" "\"$($c.Name)\";\"$($c.NumberOfCores) 核 " "$($c.NumberOfLogicalProcessors) 线程\";" "\"主频 $($c.MaxClockSpeed) MHz\"") _code, out = _run(["powershell", "-NoProfile", "-Command", script], timeout=25) lines = [l for l in out.splitlines() if l.strip()] return "\n".join(lines) or "读不到 CPU 信息" def _do_memory_sticks(self, params) -> str: script = ("Get-CimInstance Win32_PhysicalMemory | ForEach-Object { " "\"$([math]::Round($_.Capacity/1GB,0))GB " "$($_.Speed)MHz $($_.Manufacturer)\" }") _code, out = _run(["powershell", "-NoProfile", "-Command", script], timeout=25) lines = [l for l in out.splitlines() if l.strip()] return "\n".join(lines) or "读不到内存条信息" def _do_disk_serial(self, params) -> str: script = ("Get-CimInstance Win32_DiskDrive | ForEach-Object { " "\"$($_.Model) SN=$($_.SerialNumber)\" }") _code, out = _run(["powershell", "-NoProfile", "-Command", script], timeout=25) lines = [l for l in out.splitlines() if l.strip()] return "\n".join(lines) or "读不到硬盘序列号" def _do_gpu_temp(self, params) -> str: script = ("$t=Get-CimInstance -Namespace root/WMI -ClassName " "MSAcpi_ThermalZoneTemperature -ErrorAction SilentlyContinue | " "Select-Object -First 1;" "if($t){[math]::Round(($t.CurrentTemperature/10)-273.15,1)}" "else{'n/a'}") _code, out = _run(["powershell", "-NoProfile", "-Command", script], timeout=25) temp = (out or "").strip().splitlines() gpu = self._do_gpu_info({}) return f"温度:{temp[-1] if temp else 'n/a'}°C\n{gpu}" def _do_net_total(self, params) -> str: script = ("$s=Get-NetAdapterStatistics | Measure-Object -Property " "ReceivedBytes,SentBytes -Sum;" "\"收 $([math]::Round($s[0].Sum/1GB,2)) GB 发 " "$([math]::Round($s[1].Sum/1GB,2)) GB\"") _code, out = _run(["powershell", "-NoProfile", "-Command", script], timeout=25) return (out or "").strip().splitlines()[-1] if out.strip() else "读不到" def _do_running_games(self, params) -> str: return self._do_who_is_playing(params) def _do_activation(self, params) -> str: script = ("$a = Get-CimInstance SoftwareLicensingProduct " "-ErrorAction SilentlyContinue | " "Where-Object {$_.PartialProductKey} | " "Select-Object -First 1; " "if ($a) { $a.Name; 'LicenseStatus=' + $a.LicenseStatus } " "else { 'none' }") _code, out = _run(["powershell", "-NoProfile", "-Command", script], timeout=30) lines = [l.strip() for l in out.splitlines() if l.strip()] if not lines or lines[-1] == "none": return "读不到激活信息" text = "\n".join(lines) if "LicenseStatus=1" in text: text += "\n(LicenseStatus=1 表示已激活)" return text def _do_disk_health(self, params) -> str: script = ("Get-PhysicalDisk | ForEach-Object { " "\"$($_.FriendlyName) 健康=$($_.HealthStatus) " "状态=$($_.OperationalStatus)\" }") _code, out = _run(["powershell", "-NoProfile", "-Command", script], timeout=25) lines = [l for l in out.splitlines() if l.strip()] return "\n".join(lines) or "读不到硬盘健康信息" # ================================================================== # 效率(续) # ================================================================== def _do_sticky_note(self, params) -> str: text = str(params.get("text") or "").strip() if not text: raise ValueError("内容为空") notes = self._load("sticky", dict) notes.insert(0, {"text": text[:500], "at": time.strftime("%m-%d %H:%M")}) self._persist("sticky", notes[:100]) self._do_big_text({"text": text[:40]}) return f"便签已贴(共 {len(notes[:100])} 条)" def _do_search_note(self, params) -> str: word = str(params.get("word") or "").strip().lower() if not word: raise ValueError("关键词为空") hits = [] for key, label in (("notes", "笔记"), ("sticky", "便签"), ("todos", "待办")): for item in self._load(key, dict): if word in str(item.get("text", "")).lower(): hits.append(f"[{label}] {item.get('text', '')[:60]}") if not hits: return f"没有含「{word}」的记录" return f"找到 {len(hits)} 条:\n" + "\n".join(hits[:30]) def _do_translator(self, params) -> str: text = str(params.get("text") or "").strip() if not text: raise ValueError("内容为空") from urllib.parse import quote url = f"https://translate.google.com/?text={quote(text)}" subprocess.Popen(["cmd", "/c", "start", url], creationflags=_NO_WINDOW) return f"已在浏览器打开「{text[:20]}」的翻译" def _do_calc_open(self, params) -> str: subprocess.Popen("calc.exe", shell=True, creationflags=_NO_WINDOW) return "已打开计算器" def _do_unit_convert(self, params) -> str: text = str(params.get("text") or "").strip().lower().replace(" ", "") match = re.match(r"^(-?\d+(?:\.\d+)?)([a-z厘米寸尺斤公斤克磅度]*)$", text) if not match: raise ValueError("格式如 100cm / 5kg / 30度") value = float(match.group(1)) unit = match.group(2) table = { "cm": ("英寸", value / 2.54), "厘米": ("英寸", value / 2.54), "m": ("英尺", value * 3.2808), "米": ("英尺", value * 3.2808), "km": ("英里", value * 0.6214), "公里": ("英里", value * 0.6214), "kg": ("磅", value * 2.2046), "公斤": ("磅", value * 2.2046), "克": ("盎司", value * 0.0353), "斤": ("公斤", value * 0.5), "度": ("华氏度", value * 9 / 5 + 32), "c": ("华氏度", value * 9 / 5 + 32), } item = table.get(unit) if item is None: raise ValueError(f"不认识的单位:{unit}") return f"{value}{unit} = {item[1]:.2f}{item[0]}" def _do_countdown_day(self, params) -> str: date_text = str(params.get("date") or "").strip() match = re.match(r"^(\d{1,2})[-/月](\d{1,2})", date_text) if not match: raise ValueError("格式如 06-20") import datetime as _dt now = _dt.date.today() try: target = _dt.date(now.year, int(match.group(1)), int(match.group(2))) except ValueError as exc: raise ValueError(f"日期不对:{exc}") from exc if target < now: target = _dt.date(now.year + 1, target.month, target.day) days = (target - now).days self._do_big_text({"text": f"还有 {days} 天"}) return f"距离 {target} 还有 {days} 天" # ================================================================== # 媒体(续) # ================================================================== def _do_loop_play(self, params) -> str: on = self._truthy(params.get("on")) self.store.set("loop_play", bool(on)) # 真正的循环由 Actions 的看门线程负责,这里只转达开关。 # 之前直接用了 self._lock / self._audio_alias / self._mci —— # 那些是 Actions 的属性,Features 上没有,一点就崩。 setter = getattr(self.actions, "set_loop", None) if setter is not None: try: setter(bool(on)) except Exception: # noqa: BLE001 pass return f"循环播放:{'已开启' if on else '已关闭'}" def _do_shuffle_play(self, params) -> str: on = self._truthy(params.get("on")) self.store.set("shuffle_play", bool(on)) return f"随机播放:{'已开启' if on else '已关闭'}" def _do_stop_all_sound(self, params) -> str: self.actions.stop_audio() self._volume_key(0xAD, 1) return "已停掉被控端播放的音频,并切换系统静音" # ================================================================== # 网络(续) # ================================================================== def _do_relay_saver(self, params) -> str: """跨网络省流量:把屏幕流降一档。 实测:经隧道上行只有 ~37 KB/s,而 480p/55 的一帧约 350KB, 传一帧要 9 秒 —— 屏幕镜像等于不能用。 降到 360p/40 之后一帧约 60KB,快 5 倍以上。 关掉时会**还原成开之前的设置**,所以先把原值存下来。 """ on = self._truthy(params.get("on")) if on: saved = self.store.get("relay_saver_saved") if not isinstance(saved, dict) or not saved: # 第一次开:记下原值,方便关的时候还原 saved = { "width": self.store.get("screen_max_width"), "quality": self.store.get("screen_quality"), } self.store.set("relay_saver_saved", saved) self.store.set("relay_saver", True) self.store.set("screen_max_width", 360) self.store.set("screen_quality", 40) note = (f"已开启省流量:屏幕 360p / 画质 40" f"(原来 {(saved or {}).get('width') or 480}p / " f"{(saved or {}).get('quality') or 55})") else: saved = self.store.get("relay_saver_saved") self.store.set("relay_saver", False) if isinstance(saved, dict) and saved: if saved.get("width"): self.store.set("screen_max_width", saved["width"]) if saved.get("quality"): self.store.set("screen_quality", saved["quality"]) note = (f"已关闭省流量,还原为 {saved.get('width')}p / " f"画质 {saved.get('quality')}") self.store.set("relay_saver_saved", {}) else: note = "已关闭省流量(没有记录到原设置,保持当前值)" # 采集器那边会自己检测参数变化并重建(见 server._ensure_capturer), # 这里不用额外做什么。 return note def _do_net_traffic(self, params) -> str: return self._do_network_speed(params) def _do_traceroute(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(["tracert", "-d", "-h", "12", host], timeout=60) lines = [l for l in out.splitlines() if l.strip()] return "\n".join(lines[-14:]) or "没有输出" def _do_net_reset(self, params) -> str: _run(["ipconfig", "/flushdns"]) _run(["netsh", "winsock", "reset"]) _run(["netsh", "int", "ip", "reset"]) return "网络已重置(部分项目需要重启才生效)" # ---------------- 持久化 ---------------- def _persist(self, key: str, value) -> None: try: self.store.set(f"feat_{key}", value) except Exception: # noqa: BLE001 pass 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)]