2.3.0:手表界面精简 + 播放器 + 鼠标重做

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

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

中转站
- known_devices 从 ID 集合改成「设备ID -> 配对码」映射
- 修复:管理员改码后关机超过 6 小时,改码会被超时清理冲掉
- 老格式 state 文件平滑升级
This commit is contained in:
xmz7891
2026-10-18 11:39:14 +08:00
parent 1d368979aa
commit 2061f9525f
50 changed files with 13812 additions and 584 deletions
+765
View File
@@ -0,0 +1,765 @@
# -*- 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