Files
xmz7891 2061f9525f 2.3.0:手表界面精简 + 播放器 + 鼠标重做
手表端
- 功能页从 10 个杂项精简到 7 个(屏幕/鼠标/媒体/文件/电脑/语音/电源)
- 删掉:游戏封锁、待办、掷骰子、抽奖、壁纸、显示器开关、定时关机、
  剪贴板历史、性能/磁盘/电量、文件夹浏览、教室限制等
- 改成显式列按钮,不再把电脑端所有功能组无脑铺到表盘
- 远程鼠标控制重做:滑动区 + 左键/滚轮/右键三按钮
  滚轮点按=中键、上下滑=滚动
- 新增视频/音乐播放器(手表本地播放):进度拖动、左右滑快进退10秒、
  点画面播放暂停、上一/下一、播完自动下一首

电脑端
- 新增 kill_all(结束所有进程),白名单保护系统关键进程和自身
- 新增 media_list / media_pull(播放器数据源)
  安全边界:只允许媒体目录,用 relative_to 判断,防同前缀兄弟目录绕过

中转站
- known_devices 从 ID 集合改成「设备ID -> 配对码」映射
- 修复:管理员改码后关机超过 6 小时,改码会被超时清理冲掉
- 老格式 state 文件平滑升级
2026-10-18 11:39:14 +08:00

766 lines
31 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
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