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