# -*- coding: utf-8 -*- r'''ykxmz 诊断工具 —— 在出问题的机器上跑一遍,把所有可疑点问清楚。 == 为什么做这个 == 用户反馈「Win7 上语音播报不能用」。但远程猜不出来 —— 可能是: * 没装任何 TTS 语音(精简版系统常见) * 没有音频设备 / 音频服务没启动(SAPI 会**静默失败**,退出码还是 0) * cscript / powershell 被删了 * 权限问题(写不了临时文件) * 或者是别的完全没想到的原因 **猜来猜去不如让它自己说。** 这个程序把每一项都试一遍, 输出一段文本,用户复制回来,我照着改。 == 设计原则 == 1. **绝不改动机器上的任何东西** —— 只读 + 在 %TEMP% 里写临时文件(跑完删掉) 2. **每一项都要有明确结论**(可用/不可用 + 为什么),不是只打印一堆原始数据 3. **每一项都单独 try** —— 某一项崩了不能影响后面的 4. **输出要能复制** —— 同时打印到屏幕 + 写到桌面上的 .txt 5. **32 位 + Python 3.8** —— 和主程序一样,保证 Win7 能跑 == 探测什么 == 一、系统环境 版本/位数/代码页/管理员/临时目录可写 二、语音能力 ★ 重点:5 种方案逐个试 + 语音列表 + 音频设备 + 音频服务 三、外部程序 cscript / wscript / powershell / cmd 在不在、什么版本 四、.NET 环境 System.Speech 依赖它 五、屏幕和输入 分辨率 / DPI / 能否截屏 / 能否注入鼠标 六、网络 能不能连上中转站 七、主程序状态 装在哪、数据目录、版本、有没有在跑 ''' from __future__ import annotations import ctypes import datetime import os import shutil import subprocess import sys import tempfile import time import traceback from pathlib import Path IS_WINDOWS = os.name == "nt" NO_WINDOW = getattr(subprocess, "CREATE_NO_WINDOW", 0) OUT: list = [] REPORT_PATH = None # ---------------------------------------------------------------- 输出 def _console_encode(text: str) -> bytes: r'''把要打印的文字编码成**控制台能正确显示**的字节。 == 为什么不用 UTF-8 == 第一版我强行 `chcp 65001` + `sys.stdout.reconfigure(encoding="utf-8")`, 结果在 Win7 上**满屏问号**。 原因有两层: 1. Win7 的 cmd 即使切到 65001,**默认点阵字体也不含中文字形** -> 显示成方块或问号 2. 用户看到的 `???` 是字体层的问题,不是编码层的 **最稳的做法**:按**系统 ANSI 代码页**(中文 Windows = GBK/936)输出。 这正是 cmd 默认期望的编码,配合默认字体一定能显示。 ''' for enc in ("mbcs", "gbk", "utf-8"): try: return text.encode(enc, errors="replace") except Exception: # noqa: BLE001 continue return text.encode("ascii", errors="replace") def say(line: str = "") -> None: """同时打印到屏幕(本机代码页)和报告(UTF-8)。""" try: sys.stdout.buffer.write(_console_encode(line) + b"\r\n") sys.stdout.buffer.flush() except Exception: # noqa: BLE001 try: print(line, flush=True) except Exception: # noqa: BLE001 pass OUT.append(line) def head(title: str) -> None: say() say("=" * 64) say(" " + title) say("=" * 64) def item(name: str, ok, detail: str = "") -> None: """一行结论。ok 可以是 True/False/None(None = 只是信息)。""" if ok is True: mark = "[ 可用 ]" elif ok is False: mark = "[不可用]" else: mark = "[ 信息 ]" say(" %s %-34s %s" % (mark, name, detail)) def safe(fn, *a, **kw): """跑一个探测,出错不让它影响后面的。""" try: return fn(*a, **kw) except Exception as exc: # noqa: BLE001 return "【探测出错】%s: %s" % (type(exc).__name__, exc) def run(cmd, timeout=30, encoding=None): """跑命令,返回 (退出码, 输出)。异常都吃掉,转成文字。""" try: p = subprocess.run(cmd, capture_output=True, timeout=timeout, creationflags=NO_WINDOW, encoding=encoding or "mbcs", errors="replace") out = ((p.stdout or "") + (p.stderr or "")).strip() return p.returncode, out except FileNotFoundError: return -1, "(找不到这个程序)" except subprocess.TimeoutExpired: return -2, "(超时 %d 秒)" % timeout except Exception as exc: # noqa: BLE001 return -3, "%s: %s" % (type(exc).__name__, exc) # ================================================================ 一、系统 def probe_system() -> None: head("一、系统环境") try: v = sys.getwindowsversion() item("Windows 版本", True, "%d.%d (build %d)" % (v.major, v.minor, v.build)) # Win7 = 6.1 if (v.major, v.minor) == (6, 1): say(" ^ 这是 Windows 7") elif (v.major, v.minor) == (6, 0): say(" ^ 这是 Windows Vista") except Exception: # noqa: BLE001 item("Windows 版本", None, "取不到") item("Python 位数", True, "%d 位" % (64 if sys.maxsize > 2 ** 32 else 32)) item("Python 版本", True, sys.version.split()[0]) item("进程是否 64 位", True, "是" if ctypes.sizeof(ctypes.c_void_p) == 8 else "否(32 位进程)") # 管理员? try: admin = bool(ctypes.windll.shell32.IsUserAnAdmin()) except Exception: # noqa: BLE001 admin = None item("管理员权限", admin, "有" if admin else "没有(普通用户)") # 代码页 —— 中文乱码问题跟它有关 try: cp = ctypes.windll.kernel32.GetACP() item("系统 ANSI 代码页", True, "%d(%s)" % (cp, "中文 GBK" if cp == 936 else "其它")) except Exception: # noqa: BLE001 item("系统 ANSI 代码页", None, "取不到") # 临时目录能不能写 try: t = Path(tempfile.gettempdir()) p = t / "_ykxmz_diag_test.txt" p.write_text("ok", encoding="utf-8") p.unlink() item("临时目录可写", True, str(t)) except Exception as exc: # noqa: BLE001 item("临时目录可写", False, "%s: %s" % (type(exc).__name__, exc)) item("程序自身路径", None, sys.executable) item("当前工作目录", None, os.getcwd()) # ================================================================ 二、语音 VBS_TEMPLATE = ( 'Dim v\r\n' 'Set v = CreateObject("SAPI.SpVoice")\r\n' 'v.Speak "{text}"\r\n' ) def probe_tts() -> None: head("二、语音播报能力(重点)") TEXT = "语音测试一二三" # ---- 2.1 外部程序在不在 ---- say() say(" --- 2.1 依赖的外部程序 ---") sysroot = os.environ.get("SystemRoot", r"C:\Windows") for name in ("cscript", "wscript", "powershell", "cmd"): found = shutil.which(name) # 再按绝对路径找一遍(PATH 里没有时) abs_guess = { "cscript": sysroot + r"\System32\cscript.exe", "wscript": sysroot + r"\System32\wscript.exe", "powershell": sysroot + r"\System32\WindowsPowerShell\v1.0\powershell.exe", "cmd": sysroot + r"\System32\cmd.exe", }.get(name, "") abs_ok = bool(abs_guess) and Path(abs_guess).is_file() if found: item(name, True, "PATH 里有: %s" % found) elif abs_ok: item(name, True, "PATH 里没有,但绝对路径有: %s" % abs_guess) else: item(name, False, "PATH 和绝对路径都找不到") # powershell 版本 ps = shutil.which("powershell") or ( sysroot + r"\System32\WindowsPowerShell\v1.0\powershell.exe") if Path(ps).is_file(): code, out = run([ps, "-NoProfile", "-Command", "$PSVersionTable.PSVersion.Major"], timeout=20) item("PowerShell 主版本", code == 0, out.strip() or "查不到") code, out = run([ps, "-NoProfile", "-Command", "[System.Environment]::Version.ToString()"], timeout=20) item(".NET CLR 版本", code == 0, out.strip() or "查不到") # ---- 2.2 音频设备和音频服务 ---- say() say(" --- 2.2 音频环境(SAPI 没声音的头号嫌疑)---") code, out = run(["sc", "query", "Audiosrv"], timeout=20) running = "RUNNING" in (out or "").upper() item("音频服务 Audiosrv", running, "正在运行" if running else "没在运行(没声音多半是它)") # ⚠ 第一版这里用了 `Get-CimInstance` —— 那是 **PowerShell 3.0+** 的 cmdlet。 # Win7 自带 **PowerShell 2.0**,直接报 # "The term 'Get-CimInstance' is not recognized"。 # 改用 **`Get-WmiObject`**(PS 2.0 就有,3.0+ 也还在)。 say(" (以下用 Get-WmiObject / wmic,兼容 PowerShell 2.0)") if Path(ps).is_file(): code, out = run([ ps, "-NoProfile", "-Command", "Get-WmiObject Win32_SoundDevice | " "ForEach-Object { $_.Name + ' | ' + $_.Status }"], timeout=30) lines = [x.strip() for x in (out or "").splitlines() if x.strip()] bad = [x for x in lines if "not recognized" in x or "CategoryInfo" in x or "At line" in x or "FullyQualified" in x] if bad: item("音频输出设备(WMI)", False, bad[0][:90]) elif lines: item("音频输出设备(WMI)", True, "共 %d 个" % len(lines)) for x in lines[:5]: say(" - " + x) else: item("音频输出设备(WMI)", False, "**一个都没有** —— 虚拟机常见(没加声卡),SAPI 会静默失败") # 再用 wmic 独立验一遍(完全不依赖 PowerShell) code, out = run(["wmic", "sounddev", "get", "Name,Status"], timeout=30, encoding="mbcs") if code == 0: rows = [x.strip() for x in (out or "").splitlines() if x.strip() and x.strip().lower() not in ("name status", "name", "status")] item("音频输出设备(wmic)", bool(rows), ("共 %d 个: %s" % (len(rows), " / ".join(rows[:3]))) if rows else "**一个都没有**") # SAPI 自己怎么看音频输出 if Path(ps).is_file(): code, out = run([ ps, "-NoProfile", "-Command", "$v = New-Object -ComObject SAPI.SpVoice;" "try { $v.AudioOutput.GetDescription() } " "catch { 'ERR: ' + $_.Exception.Message }"], timeout=30) txt = (out or "").strip() item("SAPI 默认音频输出", code == 0 and not txt.startswith("ERR"), txt[:100] or "拿不到") # ---- 2.3 SAPI 语音列表 ---- say() say(" --- 2.3 装了哪些语音 ---") voices_ps = [] if Path(ps).is_file(): code, out = run([ ps, "-NoProfile", "-Command", "$v = New-Object -ComObject SAPI.SpVoice;" "$v.GetVoices() | ForEach-Object { $_.GetDescription() }"], timeout=25) voices_ps = [x.strip() for x in (out or "").splitlines() if x.strip()] if code == 0 and voices_ps: item("SAPI5 语音(COM 方式)", True, "共 %d 个" % len(voices_ps)) for v in voices_ps[:10]: say(" - " + v) else: item("SAPI5 语音(COM 方式)", False, "列不出来 (退出码 %s) %s" % (code, out[:80])) # System.Speech 那条路 if Path(ps).is_file(): code, out = run([ ps, "-NoProfile", "-Command", "Add-Type -AssemblyName System.Speech;" "(New-Object System.Speech.Synthesis.SpeechSynthesizer)" ".GetInstalledVoices() | ForEach-Object { $_.VoiceInfo.Name }"], timeout=30) names = [x.strip() for x in (out or "").splitlines() if x.strip()] item("System.Speech 语音", code == 0 and bool(names), "共 %d 个: %s" % (len(names), "、".join(names[:5])) if names else "列不出来 (退出码 %s) %s" % (code, (out or "")[:80])) # ---- 2.4 五种方案逐个试 ---- say() say(" --- 2.4 五种发声方案逐个试 ---") say(" (注意:能出声才算数。这里只能看退出码 + 耗时)") vbs = Path(tempfile.gettempdir()) / "_ykxmz_diag_say.vbs" try: vbs.write_bytes( (VBS_TEMPLATE.replace("{text}", TEXT)).encode("mbcs", errors="replace")) vbs_ok = True item("写 VBS 文件", True, "%s (%d 字节)" % (vbs, vbs.stat().st_size)) except Exception as exc: # noqa: BLE001 vbs_ok = False item("写 VBS 文件", False, "%s: %s" % (type(exc).__name__, exc)) attempts = [] if vbs_ok: attempts.append(("1. cscript + VBScript", ["cscript", "//nologo", str(vbs)])) attempts.append(("2. wscript + VBScript", ["wscript", "//nologo", str(vbs)])) if Path(ps).is_file(): attempts.append(("3. powershell + SAPI COM", [ ps, "-NoProfile", "-WindowStyle", "Hidden", "-Command", "$v = New-Object -ComObject SAPI.SpVoice; $v.Speak('%s')" % TEXT])) attempts.append(("4. powershell + System.Speech", [ ps, "-NoProfile", "-WindowStyle", "Hidden", "-Command", "Add-Type -AssemblyName System.Speech;" "$s = New-Object System.Speech.Synthesis.SpeechSynthesizer;" "$s.Speak('%s')" % TEXT])) for label, cmd in attempts: exe = cmd[0] # 用绝对路径兜底 if not shutil.which(exe): cand = { "cscript": sysroot + r"\System32\cscript.exe", "wscript": sysroot + r"\System32\wscript.exe", }.get(exe) if cand and Path(cand).is_file(): cmd = [cand] + list(cmd[1:]) exe = cand t0 = time.perf_counter() code, out = run(cmd, timeout=60) secs = time.perf_counter() - t0 ok = code == 0 item(label, ok, "退出码 %s,耗时 %.1fs%s" % (code, secs, (" " + out[:70]) if out else "")) # 清理 try: if vbs.exists(): vbs.unlink() except Exception: # noqa: BLE001 pass say() say(" ⚠ 如果上面全部「可用」但实际没声音,问题在音频侧:") say(" * 音频服务没运行") say(" * 没有音频输出设备(耳机孔也没插)") say(" * 或者装了语音但系统音量/静音设置有问题") # ================================================================ 三、屏幕输入 def probe_ui() -> None: head("三、屏幕和输入") if not IS_WINDOWS: say(" 非 Windows,跳过") return u = ctypes.windll.user32 try: w = u.GetSystemMetrics(0) h = u.GetSystemMetrics(1) item("主屏分辨率", True, "%dx%d" % (w, h)) except Exception as exc: # noqa: BLE001 item("主屏分辨率", False, str(exc)) try: vw = u.GetSystemMetrics(78) # SM_CXVIRTUALSCREEN vh = u.GetSystemMetrics(79) item("虚拟屏尺寸", True, "%dx%d(多显示器会更大)" % (vw, vh)) except Exception: # noqa: BLE001 pass # DPI try: dc = u.GetDC(0) dpi = ctypes.windll.gdi32.GetDeviceCaps(dc, 88) # LOGPIXELSX u.ReleaseDC(0, dc) item("DPI", True, "%d(100%% 是 96)" % dpi) except Exception: # noqa: BLE001 pass # 截屏能力 try: gdi = ctypes.windll.gdi32 dc = u.GetDC(0) mem = gdi.CreateCompatibleDC(dc) bmp = gdi.CreateCompatibleBitmap(dc, 100, 100) gdi.SelectObject(mem, bmp) ok = bool(gdi.BitBlt(mem, 0, 0, 100, 100, dc, 0, 0, 0x00CC0020)) gdi.DeleteObject(bmp) gdi.DeleteDC(mem) u.ReleaseDC(0, dc) item("能否截屏(BitBlt)", ok, "成功" if ok else "失败") except Exception as exc: # noqa: BLE001 item("能否截屏(BitBlt)", False, "%s: %s" % (type(exc).__name__, exc)) # 输入桌面是不是锁屏 try: h = u.OpenInputDesktop(0, False, 0x0001) name = "" if h: buf = ctypes.create_unicode_buffer(256) need = ctypes.c_int(0) u.GetUserObjectInformationW(h, 2, buf, 512, ctypes.byref(need)) name = buf.value u.CloseDesktop(h) item("当前输入桌面", name != "Winlogon", "%s%s" % (name or "(拿不到)", "(锁屏中)" if name == "Winlogon" else "")) except Exception: # noqa: BLE001 pass # 能不能注入鼠标(只测 API 在不在,不动鼠标) try: ctypes.windll.user32.SendInput item("SendInput 可用", True, "函数在") except Exception as exc: # noqa: BLE001 item("SendInput 可用", False, str(exc)) # ================================================================ 四、网络 def probe_net() -> None: head("四、网络") import socket for host, port in (("yk.xxmxxs.top", 443), ("1.1.1.1", 53)): t0 = time.perf_counter() try: s = socket.create_connection((host, port), timeout=10) s.close() item("连 %s:%d" % (host, port), True, "通,%.2fs" % (time.perf_counter() - t0)) except Exception as exc: # noqa: BLE001 item("连 %s:%d" % (host, port), False, "%s(%.2fs)" % (type(exc).__name__, time.perf_counter() - t0)) # 本机 IP try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(("1.1.1.1", 80)) item("本机局域网 IP", True, s.getsockname()[0]) s.close() except Exception: # noqa: BLE001 pass # ============================================================ 五、环境与工具链 def probe_env() -> None: r'''采集**环境与工具链**信息 —— 这是决定"我能怎么写代码"的依据。 == 为什么要这一段 == 用户要的是:**搞清楚这台 Win7 上有哪些工具/组件可用**, 好在写兼容代码时不踩空。所以这一段不谈某个具体软件, 只回答"这台机器**能提供什么**"。 关注点: * 系统到底哪个版本、什么 SP、什么架构 * .NET 装到几(System.Speech / WPF 依赖它) * VC++ 运行库有没有(很多打包程序依赖) * 命令行工具有哪些(cscript/wscript/powershell/wmic/mshta…) * 中文语音在不在(SAPI 已在第二节,这里确认语音包) * 已装程序清单(判断这是不是精简版系统) * 硬件规模(内存 / 磁盘)—— 决定程序能不能跑 ''' head("五、环境与工具链") # ---- 5.1 系统细节 ---- say() say(" --- 5.1 系统细节 ---") try: v = sys.getwindowsversion() item("内核版本", True, "%d.%d build %d" % (v.major, v.minor, v.build)) names = {(6, 1): "Windows 7", (6, 2): "Windows 8", (6, 3): "Windows 8.1", (10, 0): "Windows 10/11"} item("对应产品名", None, names.get((v.major, v.minor), "未知")) except Exception: # noqa: BLE001 pass item("架构", None, os.environ.get("PROCESSOR_ARCHITECTURE", "?")) item("处理器标识", None, os.environ.get("PROCESSOR_IDENTIFIER", "?")[:60]) item("CPU 核心数", None, str(os.cpu_count())) item("系统盘/Windows", None, os.environ.get("SystemRoot", "?")) item("Program Files", None, os.environ.get("ProgramFiles", "?")) # 内存 / 磁盘 try: class MEMORYSTATUSEX(ctypes.Structure): _fields_ = [("dwLength", ctypes.c_ulong), ("dwMemoryLoad", ctypes.c_ulong), ("ullTotalPhys", ctypes.c_ulonglong), ("ullAvailPhys", ctypes.c_ulonglong), ("ullTotalPageFile", ctypes.c_ulonglong), ("ullAvailPageFile", ctypes.c_ulonglong), ("ullTotalVirtual", ctypes.c_ulonglong), ("ullAvailVirtual", ctypes.c_ulonglong), ("ullAvailExtendedVirtual", ctypes.c_ulonglong)] m = MEMORYSTATUSEX() m.dwLength = ctypes.sizeof(m) if ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(m)): item("物理内存", True, "%.2f GB(可用 %.2f GB,占用 %d%%)" % (m.ullTotalPhys / 2 ** 30, m.ullAvailPhys / 2 ** 30, m.dwMemoryLoad)) except Exception as exc: # noqa: BLE001 item("物理内存", False, str(exc)) for drive in ("C:\\",): try: free = ctypes.c_ulonglong(0) total = ctypes.c_ulonglong(0) ctypes.windll.kernel32.GetDiskFreeSpaceExW( ctypes.c_wchar_p(drive), None, ctypes.byref(total), ctypes.byref(free)) item("磁盘 %s" % drive, True, "总 %.1f GB,剩 %.1f GB" % (total.value / 2 ** 30, free.value / 2 ** 30)) except Exception: # noqa: BLE001 pass # ---- 5.2 .NET ---- say() say(" --- 5.2 .NET 运行库(System.Speech 依赖)---") ps = shutil.which("powershell") or ( os.environ.get("SystemRoot", r"C:\Windows") + r"\System32\WindowsPowerShell\v1.0\powershell.exe") if Path(ps).is_file(): code, out = run([ps, "-NoProfile", "-Command", "[System.Environment]::Version.ToString()"], timeout=25) item("CLR 版本", code == 0, (out or "").strip()) # 注册表里的 .NET 版本 for hive in (r"HKLM\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full", r"HKLM\SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5", r"HKLM\SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.0"): code, out = run(["reg", "query", hive, "/v", "Version"], timeout=20) if code == 0 and out: ver = "" for ln in out.splitlines(): if "Version" in ln and "REG_SZ" in ln: ver = ln.split("REG_SZ")[-1].strip() item(hive.split("\\")[-2] + " / " + hive.split("\\")[-1], True, ver or "在") # ---- 5.3 命令行工具 ---- say() say(" --- 5.3 命令行工具 ---") sysroot = os.environ.get("SystemRoot", r"C:\Windows") tools = [ ("cscript", r"\System32\cscript.exe"), ("wscript", r"\System32\wscript.exe"), ("cmd", r"\System32\cmd.exe"), ("mshta", r"\System32\mshta.exe"), # 可跑 VBS/JS ("rundll32", r"\System32\rundll32.exe"), ("reg", r"\System32\reg.exe"), ("wmic", r"\System32\wbem\wmic.exe"), # 查硬件 ("tasklist", r"\System32\tasklist.exe"), ("netsh", r"\System32\netsh.exe"), ("sc", r"\System32\sc.exe"), ("taskkill", r"\System32\taskkill.exe"), ("certutil", r"\System32\certutil.exe"), ] for name, rel in tools: found = shutil.which(name) abs_p = sysroot + rel if found: item(name, True, found) elif Path(abs_p).is_file(): item(name, True, "PATH 外,绝对路径在: %s" % abs_p) else: item(name, False, "找不到") # ---- 5.4 Windows 组件 ---- say() say(" --- 5.4 Windows 组件 ---") # 语音(确认中文语音包) if Path(ps).is_file(): code, out = run([ ps, "-NoProfile", "-Command", "$v=New-Object -ComObject SAPI.SpVoice;" "$v.GetVoices()|ForEach-Object{$_.GetDescription()}"], timeout=25) vs = [x.strip() for x in (out or "").splitlines() if x.strip()] cn = [x for x in vs if "Chinese" in x or "Huihui" in x or "中文" in x] item("中文语音包", bool(cn), "、".join(cn) if cn else "没有中文语音(只有: %s)" % ("、".join(vs[:3]) if vs else "一个语音都没有")) # 媒体功能 code, out = run(["reg", "query", r"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion", "/v", "ProductName"], timeout=20) name = "" if code == 0: for ln in (out or "").splitlines(): if "ProductName" in ln and "REG_SZ" in ln: name = ln.split("REG_SZ")[-1].strip() item("系统产品名(注册表)", None, name or "查不到") # ---- 5.5 已装程序(判断是不是精简版)---- say() say(" --- 5.5 已装程序(看是不是精简版系统)---") # ⚠ 第一版用 `Get-ItemProperty` 配通配符 + `Select-Object -ExpandProperty`: # 在 PowerShell 2.0 上会往输出里混进一堆报错文本 # ("Property DisplayName cannot be found"、"CategoryInfo"、"At line:1" …), # 结果列表里全是垃圾行。**报错和结果混在同一个 stdout 里**, # 光看行数根本分不出来 —— 这次报告里就出现了。 # # 改法: # 1. `Get-WmiObject Win32_Product`(PS 2.0 兼容,但只有 MSI 装的程序) # 2. `reg query` 兜底 —— **完全不经过 PowerShell**,不受版本影响 # 3. 过滤掉所有已知的报错关键字 apps = set() if Path(ps).is_file(): code, out = run([ ps, "-NoProfile", "-Command", "Get-WmiObject -Class Win32_Product -ErrorAction SilentlyContinue | " "ForEach-Object { $_.Name }"], timeout=90) for x in (out or "").splitlines(): apps.add(x.strip()) for hive in (r"HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", r"HKLM\SOFTWARE\WOW6432Node\Microsoft\Windows" r"\CurrentVersion\Uninstall"): code, out = run(["reg", "query", hive, "/s", "/v", "DisplayName"], timeout=60, encoding="mbcs") for ln in (out or "").splitlines(): if "DisplayName" in ln and "REG_SZ" in ln: apps.add(ln.split("REG_SZ")[-1].strip()) # 过滤报错噪音 NOISE = ("CategoryInfo", "FullyQualifiedErrorId", "At line:", "+ ", "PSArgumentException", "ExpandPropertyNotFound", "SelectObject", "Get-ItemProperty", "Get-WmiObject :", "InvalidArgument", "PSObject", "cannot be found", "not recognized", "?????") clean = sorted({a for a in apps if a and not any(n in a for n in NOISE) and "HKLM:" not in a and "\\" not in a[:6]}) if clean: item("已装程序数量", True, "共 %d 个" % len(clean)) for a in clean[:30]: say(" - " + a) if len(clean) > 30: say(" ...(还有 %d 个)" % (len(clean) - 30)) else: item("已装程序数量", False, "一个都列不出来") # ---- 5.6 环境变量(关键几个)---- say() say(" --- 5.6 环境变量 ---") for k in ("PATH", "TEMP", "TMP", "SystemRoot", "windir", "ProgramFiles", "ProgramData", "USERPROFILE", "APPDATA", "LOCALAPPDATA", "COMPUTERNAME", "USERNAME"): v = os.environ.get(k) if k == "PATH": item(k, None, "%d 个路径" % len((v or "").split(";"))) for p in (v or "").split(";")[:8]: if p.strip(): say(" " + p.strip()) else: item(k, None, v or "(没设)") # ================================================================ main def main() -> int: say("=" * 64) say(" ykxmz 诊断报告") say(" 生成时间: %s" % datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")) say(" 机器名: %s" % os.environ.get("COMPUTERNAME", "?")) say(" 用户名: %s" % os.environ.get("USERNAME", "?")) say("=" * 64) say() say(" ★ 把这一整段复制发回去就行。程序不改动系统任何东西。") for fn in (probe_system, probe_tts, probe_ui, probe_net, probe_env): try: fn() except Exception: # noqa: BLE001 say() say(" 【这一段探测崩了】") for ln in traceback.format_exc().splitlines()[-6:]: say(" " + ln) say() say("=" * 64) say(" 报告结束") say("=" * 64) # 同时写到桌面和 exe 旁边 text = "\r\n".join(OUT) written = [] targets = [] try: targets.append(Path(os.path.expanduser("~")) / "Desktop" / "ykxmz诊断.txt") except Exception: # noqa: BLE001 pass try: targets.append(Path(sys.executable).parent / "ykxmz诊断.txt") except Exception: # noqa: BLE001 pass targets.append(Path(tempfile.gettempdir()) / "ykxmz诊断.txt") for t in targets: # ⚠ 写两份:UTF-8-BOM(记事本能认)+ GBK(老编辑器/控制台重定向能认)。 # Win7 自带的记事本从 Vista 起就支持 UTF-8,但为了万无一失都写。 for enc, suffix in (("utf-8-sig", ""), ("gbk", "_gbk")): try: t.parent.mkdir(parents=True, exist_ok=True) p = t if not suffix else t.with_name(t.stem + suffix + t.suffix) p.write_text(text, encoding=enc, errors="replace") written.append(str(p)) except Exception: # noqa: BLE001 pass print() print(" 报告已保存到:") for w in written: print(" " + w) print() print(" 按回车键关闭…") try: input() except Exception: # noqa: BLE001 time.sleep(30) return 0 if __name__ == "__main__": try: sys.exit(main()) except Exception: # noqa: BLE001 print(traceback.format_exc()) try: input(" 出错了,按回车关闭…") except Exception: # noqa: BLE001 pass