@echo off
setlocal
title Shared Printer Tool - KeGuanJia Toolbox
rem ===========================================================
rem  Batch + script hybrid file. Everything after the marker line
rem  at the bottom is NOT batch -- cmd never parses it (we exit first).
rem  The launcher below re-reads this same file as UTF-8 and runs it.
rem  Do NOT edit with an ANSI-only editor: the payload is UTF-8.
rem ===========================================================
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "$f='%~f0'; $s=[IO.File]::ReadAllText($f,[Text.Encoding]::UTF8); $i=$s.LastIndexOf('#---PS'+'-START---#'); if($i -lt 0){Write-Host '[ERROR] script payload not found in this file'; exit 1}; & ([ScriptBlock]::Create($s.Substring($i+16)))"
set RC=%ERRORLEVEL%
endlocal & exit /b %RC%
#---PS-START---#
# =====================================================================
# 共享打印机工具（客管家百宝箱内置）· PowerShell 源
#
# ⚠️ 这个文件不直接给客户：它会被 build-netprint-bat.py 原样拼进
#    public/tool/netprint.bat 的「批处理 + PowerShell 混合体」尾部（标记行之后）。
#    改完必须重跑生成器，否则 .bat 里还是旧脚本（有测试守住）。
#
# 设计铁律（在客户的机器上改系统设置，改错了是真麻烦）：
#   ① 体检只读。一项都不改，先把结论和「去哪个界面点什么」说清楚。
#   ② 修复分两级：**安全项**（服务/防火墙/网络位置）自动改；
#      **降级项**（RPC 加密、受保护打印模式、驱动安装限制）一律逐项确认，
#      并在日志里写明「事后怎么改回」。
#   ③ 绝不把「降安全性」当常规修法。RpcAuthnLevelPrivacyEnabled=0 能解决
#      0x0000011b，但那等于重新打开 CVE-2021-1678 —— 微软自己的口径是
#      「只能在有记录的兼容性事故里临时用，事后改回 1」。工具照这个口径做。
#   ④ 输出里的装饰符号**只用 ASCII**：GBK 控制台打不出 └ ⚠ ✗ ▶，
#      会变成乱码 —— 在客户面前看起来就像工具坏了。
#   ⑤ 报错码必须翻译成人话。售后拿着「0x00000bc4」在客户面前是没法交差的。
# =====================================================================
$ErrorActionPreference = 'SilentlyContinue'
try { $Host.UI.RawUI.WindowTitle = '共享打印机工具 - 客管家' } catch { }

$script:LOG = New-Object System.Collections.ArrayList
$script:ROWS = New-Object System.Collections.ArrayList
$script:NOTES = New-Object System.Collections.ArrayList   # 事后要改回的项
$IsAdmin = $false
try {
  $IsAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
} catch { }

function W([string]$t, [string]$c = 'Gray') {
  Write-Host $t -ForegroundColor $c
  [void]$script:LOG.Add($t)
}

# 体检结果行：S = OK / WARN / BAD / INFO
function AddR([string]$n, [string]$s, [string]$d, [string]$f = '') {
  [void]$script:ROWS.Add([pscustomobject]@{ N = $n; S = $s; D = $d; F = $f })
  $tag = '[i] '
  $col = 'Gray'
  if ($s -eq 'OK') { $tag = '[OK]'; $col = 'Green' }
  if ($s -eq 'WARN') { $tag = '[!] '; $col = 'Yellow' }
  if ($s -eq 'BAD') { $tag = '[X] '; $col = 'Red' }
  W ('  ' + $tag + ' ' + $n) $col
  if ($d) { W ('         ' + $d) 'DarkGray' }
  if ($f) { W ('         修法: ' + $f) 'DarkYellow' }
}

function Show-Summary {
  $ok = @($script:ROWS | Where-Object { $_.S -eq 'OK' })
  $warn = @($script:ROWS | Where-Object { $_.S -eq 'WARN' })
  $bad = @($script:ROWS | Where-Object { $_.S -eq 'BAD' })
  W ''
  W '=== 体检结论 ===' 'Cyan'
  W ('  正常 ' + $ok.Count + ' 项 / 注意 ' + $warn.Count + ' 项 / 有问题 ' + $bad.Count + ' 项') 'Cyan'
  if ($bad.Count -eq 0 -and $warn.Count -eq 0) {
    W '  没发现明显问题。若还是连不上，用「客户机」模式在要打印的那台电脑上再跑一遍。' 'Green'
    return
  }
  if ($bad.Count -gt 0) {
    W ''
    W '  必须先处理（建议按顺序）：' 'Red'
    $i = 0
    foreach ($r in $bad) {
      $i++
      W ('   ' + $i + '. ' + $r.N) 'Red'
      if ($r.D) { W ('      ' + $r.D) 'Gray' }
      if ($r.F) { W ('      -> ' + $r.F) 'Yellow' }
    }
  }
  if ($warn.Count -gt 0) {
    W ''
    W '  可以注意：' 'Yellow'
    foreach ($r in $warn) {
      W ('   - ' + $r.N) 'Yellow'
      if ($r.D) { W ('     ' + $r.D) 'Gray' }
      if ($r.F) { W ('     -> ' + $r.F) 'Yellow' }
    }
  }
}

function AskY([string]$q) {
  $r = Read-Host ('  ' + $q + '  输入 YES 继续（直接回车 = 跳过）')
  if ($r -eq 'YES' -or $r -eq 'yes' -or $r -eq 'Yes') { return $true }
  return $false
}

function NeedAdmin([string]$what) {
  W ('  [!] 这项需要管理员权限，当前是普通用户 -> 跳过：' + $what) 'Yellow'
  W '      关掉窗口，右键这个文件选「以管理员身份运行」再来一次。' 'Yellow'
}

# ---------------- 注册表小工具 ----------------
function RegGet([string]$p, [string]$n) {
  try {
    $k = Get-Item -LiteralPath $p -ErrorAction Stop
    return $k.GetValue($n, $null)
  } catch { return $null }
}
function RegSetDword([string]$p, [string]$n, [int]$v) {
  try {
    if (-not (Test-Path -LiteralPath $p)) { New-Item -Path $p -Force | Out-Null }
    New-ItemProperty -Path $p -Name $n -Value $v -PropertyType DWord -Force | Out-Null
    return $true
  } catch { return $false }
}

# ---------------- 打印机枚举（Get-Printer 不可用时退 WMI） ----------------
function Get-PrintersSafe {
  $r = @()
  try { $r = @(Get-Printer -ErrorAction Stop) } catch { $r = @() }
  if ($r.Count -gt 0) { return $r }
  try {
    $w = @(Get-CimInstance Win32_Printer -ErrorAction SilentlyContinue)
    foreach ($p in $w) {
      $r += [pscustomobject]@{
        Name = $p.Name; Shared = [bool]$p.Shared; ShareName = $p.ShareName
        DriverName = $p.DriverName; PortName = $p.PortName
        PrinterStatus = [string]$p.PrinterStatus
        WorkOffline = [bool]$p.WorkOffline
      }
    }
  } catch { }
  return $r
}

# 共享名合规：客户机上要手打 \\主机\共享名，中文/空格/特殊符号最容易出岔子
function Test-ShareNameBad([string]$s) {
  if ([string]::IsNullOrWhiteSpace($s)) { return $true }
  if ($s -match '[^\x20-\x7E]') { return $true }      # 非 ASCII
  if ($s -match '\s') { return $true }                 # 空格
  if ($s -match '[\\/:\*\?"<>\|]') { return $true }    # Windows 非法字符
  return $false
}
function Sanitize-ShareName([string]$s) {
  if ([string]::IsNullOrWhiteSpace($s)) { return 'Printer' }
  $t = [string]$s
  $t = $t -replace '[^\x20-\x7E]', ''
  $t = $t -replace '[\s\\/:\*\?"<>\|]', ''
  if ($t.Length -gt 24) { $t = $t.Substring(0, 24) }
  if ([string]::IsNullOrWhiteSpace($t)) { $t = 'Printer' }
  return $t
}

# ---------------- 445 端口探测 ----------------
# 为什么不用 net view 扫全网段：它只列自己工作组、超时还不可控。
function Test-445([string]$ip, [int]$ms) {
  $c = New-Object System.Net.Sockets.TcpClient
  try {
    $ar = $c.BeginConnect($ip, 445, $null, $null)
    if ($ar.AsyncWaitHandle.WaitOne($ms)) { $c.EndConnect($ar); return $true }
    return $false
  } catch { return $false }
  finally { try { $c.Close() } catch { } }
}

# 并发扫：254 个地址串行 × 250ms 最坏要 1 分钟，售后在客户面前等不起。
# PowerShell 5.1 没有 ForEach-Object -Parallel，用 RunspacePool（PS3+ 就有）。
function Scan-445([string[]]$ips) {
  $out = New-Object System.Collections.ArrayList
  if (-not $ips -or $ips.Count -eq 0) { return $out }
  $ms = 250
  $max = 32
  try { $max = [Math]::Min(64, [Math]::Max(8, [Environment]::ProcessorCount * 8)) } catch { $max = 32 }
  $pool = $null
  try {
    $pool = [runspacefactory]::CreateRunspacePool(1, $max)
    $pool.Open()
  } catch { $pool = $null }

  if ($null -eq $pool) {
    # Runspace 用不了就退化串行：慢，但一定跑得动
    W '  [!] 并发不可用，改为逐个探测（会慢一些）。' 'DarkYellow'
    foreach ($ip in $ips) { if (Test-445 $ip $ms) { [void]$out.Add($ip) } }
    return $out
  }

  $sb = {
    param($ip, $ms)
    $c = New-Object System.Net.Sockets.TcpClient
    try {
      $ar = $c.BeginConnect($ip, 445, $null, $null)
      if ($ar.AsyncWaitHandle.WaitOne($ms)) { $c.EndConnect($ar); return $ip }
    } catch { }
    finally { try { $c.Close() } catch { } }
    return ''
  }
  $jobs = New-Object System.Collections.ArrayList
  foreach ($ip in $ips) {
    $ps = [powershell]::Create()
    $ps.RunspacePool = $pool
    [void]$ps.AddScript($sb).AddArgument($ip).AddArgument($ms)
    [void]$jobs.Add([pscustomobject]@{ PS = $ps; H = $ps.BeginInvoke() })
  }
  foreach ($j in $jobs) {
    try {
      $r = $j.PS.EndInvoke($j.H)
      if ($r -and $r.Count -gt 0 -and $r[0]) { [void]$out.Add([string]$r[0]) }
    } catch { }
    try { $j.PS.Dispose() } catch { }
  }
  try { $pool.Close(); $pool.Dispose() } catch { }
  return $out
}

function Get-MyIPv4 {
  $res = @()
  try {
    $h = [System.Net.Dns]::GetHostAddresses([System.Net.Dns]::GetHostName())
    foreach ($a in $h) {
      if ($a.AddressFamily -ne [System.Net.Sockets.AddressFamily]::InterNetwork) { continue }
      $s = $a.ToString()
      if ($s -like '127.*' -or $s -like '169.254.*') { continue }
      $res += $s
    }
  } catch { }
  if ($res.Count -eq 0) {
    try {
      $res = @(Get-NetIPAddress -AddressFamily IPv4 | Where-Object {
        $_.IPAddress -notlike '127.*' -and $_.IPAddress -notlike '169.254.*'
      } | ForEach-Object { $_.IPAddress })
    } catch { }
  }
  return $res
}

function Get-RemotePrinterShares([string]$ip) {
  $out = @()
  $txt = @()
  try { $txt = @(& net.exe view ('\\' + $ip) 2>$null) } catch { $txt = @() }
  if (-not $txt -or $txt.Count -eq 0) { return $out }
  foreach ($ln in $txt) {
    $s = [string]$ln
    if ([string]::IsNullOrWhiteSpace($s)) { continue }
    if ($s -match '^\s*-{3,}') { continue }
    # !!! 类型列是本地化的：中文打「打印」、英文打「Print」/「Printer」。
    #     只认一种会在另一语言的机器上恒返回空（看起来像「对方没共享打印机」）。
    if ($s.Contains('打印') -or $s.Contains('Print') -or $s.Contains('print')) {
      $name = ($s -split '\s{2,}')[0].Trim()
      if ($name -and -not $name.StartsWith('-')) { $out += $name }
    }
  }
  return $out
}

# net view 在「禁止匿名枚举共享」的机器上会直接返回拒绝访问 —— Win10/11 默认就偏严，
# 现场极常见。最实用的解法是拿主机上的一个真实账号先建立 IPC$ 会话再重新枚举。
# 用 `net use ... *` 让系统自己弹口令输入：密码不经过命令行、不进命令历史、也不进日志。
function Try-AuthView([string]$ip) {
  W ''
  W ('  访问 \\' + $ip + ' 时对方不让枚举共享（Win10/11 默认禁匿名枚举，很常见）。') 'Yellow'
  W '  如果你知道【主机上】的一个账号，可以现在用它连一下。' 'Yellow'
  W '  账号写法：主机名\用户名   或   .\用户名（那台机器的本地账号）' 'Gray'
  $u = Read-Host '  账号（直接回车 = 不试）'
  if ([string]::IsNullOrWhiteSpace($u)) { return @() }
  W '  接着系统会要密码 —— 输入时不显示，这是正常的。' 'Gray'
  try { & net.exe use ('\\' + $ip + '\IPC$') ('/user:' + $u.Trim()) '*' } catch { }
  Start-Sleep -Milliseconds 700
  $shares = @(Get-RemotePrinterShares $ip)
  if ($shares.Count -eq 0) {
    W '  还是没枚举到。可能是账号不对，或主机上确实没设置共享。' 'Yellow'
  }
  try { & net.exe use ('\\' + $ip + '\IPC$') '/delete' '/y' 2>$null | Out-Null } catch { }
  return $shares
}


# ---------------- 错误码 -> 人话 ----------------
function Show-CodeTable([string]$txt) {
  W ''
  W '  连接失败。把下面这张表对着屏幕上那个错误码看：' 'Yellow'
  W ''
  W '   0x0000011b  主机那边的 RPC 加密要求太严（2021-09 补丁之后的默认）' 'White'
  W '               -> 把本工具拿到【主机】上跑一次「主机模式」，让它处理' 'DarkGray'
  W '   0x00000709  共享名/主机名对不上（改过名、或用域名别名最常见）' 'White'
  W '               -> 到「设备和打印机」里删掉旧的那台，再用 \\IP\共享名 重连' 'DarkGray'
  # ⚠️ 这句纠正必须留在代码里：网上流传最广的说法是「0709 = 默认打印机配置异常」，
  #    照着那个方向去改「默认打印机」纯属白折腾。售后在客户面前照着这张表念，
  #    表里不写清楚，他就只能凭印象猜。
  W '                  （它不是「默认打印机配置」的问题，网上那个说法是错的）' 'DarkGray'
  W '   0x00000bc4  找不到打印机，或驱动安装被拦（限制管理员装驱动）' 'White'
  W '               -> 右键本文件「以管理员身份运行」；仍不行则用管理员账号连接' 'DarkGray'
  W '   0x0000052e  用户名或密码不对（多半是主机改过密码，凭据还存着旧的）' 'White'
  W '               -> 客户机模式体检会列出这些残留凭据，删掉后重连' 'DarkGray'
  W '   0x800706b9  RPC 连不上：主机名解析失败' 'White'
  W '               -> 改用 IP 连接（本工具扫出来的就是 IP，不用主机名）' 'DarkGray'
  W '   0x00000002 / 0x80070005  拒绝访问：共享权限或防火墙挡了' 'White'
  W '               -> 主机模式体检会看防火墙规则与共享状态' 'DarkGray'
  W '   打印机已添加但显示「脱机」/ 打不出来' 'White'
  W '               -> 清空卡住的打印队列（客户机模式修复里有一项）' 'DarkGray'
  W ''
  if ($txt) {
    $t = [string]$txt
    W '  这次系统给的原话（抄下来给客户看也行）：' 'DarkGray'
    $lines = @($t -split "`r?`n" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
    $n = 0
    foreach ($l in $lines) {
      if ($n -ge 4) { break }
      W ('    ' + $l.Trim()) 'DarkGray'
      $n++
    }
    W ''
  }
}

# =====================================================================
# 主机侧：把打印机共享出去
# =====================================================================
function Diag-Host {
  $script:ROWS.Clear()
  $cn = $env:COMPUTERNAME
  W ''
  W '========== 主机体检（这台要把打印机共享出去） ==========' 'Cyan'
  W ''

  try {
    $os = [Environment]::OSVersion.Version
    AddR '系统版本' 'INFO' ('Windows ' + $os.Major + '.' + $os.Minor + '  build ' + $os.Build) ''
  } catch { }

  # --- 1. Print Spooler ---
  $sp = $null
  try { $sp = Get-Service -Name Spooler -ErrorAction Stop } catch { }
  if ($null -eq $sp) {
    AddR '打印后台服务 (Spooler)' 'BAD' '读不到这个服务，系统可能被精简过。' '重装打印组件，或换一台机器做主机。'
  } elseif ($sp.Status -ne 'Running') {
    AddR '打印后台服务 (Spooler)' 'BAD' ('当前状态：' + $sp.Status) '修复模式会自动启动它。'
  } else {
    $mode = ''
    try { $mode = (Get-CimInstance Win32_Service -Filter "Name='Spooler'").StartMode } catch { }
    if ($mode -and $mode -ne 'Auto') {
      AddR '打印后台服务 (Spooler)' 'WARN' ('正在运行，但启动方式不是「自动」（' + $mode + '）') '修复模式会改成自动，否则重启电脑后共享就没了。'
    } else {
      AddR '打印后台服务 (Spooler)' 'OK' '正在运行' ''
    }
  }

  # --- 2. 文件共享服务 ---
  $sv = $null
  try { $sv = Get-Service -Name LanmanServer -ErrorAction Stop } catch { }
  if ($null -ne $sv -and $sv.Status -ne 'Running') {
    AddR '文件共享服务 (LanmanServer)' 'BAD' ('当前状态：' + $sv.Status) '修复模式会自动启动它。没有它共享根本不通。'
  } elseif ($null -ne $sv) {
    AddR '文件共享服务 (LanmanServer)' 'OK' '正在运行' ''
  }

  # --- 3. 本机打印机与共享状态 ---
  $prs = @(Get-PrintersSafe)
  if ($prs.Count -eq 0) {
    AddR '本机打印机' 'BAD' '这台电脑上没有装任何打印机。' '先在「设置 - 蓝牙和其他设备 - 打印机和扫描仪」里把打印机装好、能打测试页，再来共享。'
  } else {
    $shared = @($prs | Where-Object { $_.Shared -eq $true })
    if ($shared.Count -eq 0) {
      AddR '本机打印机' 'BAD' ('装了 ' + $prs.Count + ' 台，但一台都没共享。') '修复模式会问你共享哪一台。'
    } else {
      AddR '本机打印机' 'OK' ('装了 ' + $prs.Count + ' 台，已共享 ' + $shared.Count + ' 台') ''
    }
    foreach ($p in $prs) {
      if ($p.Shared -eq $true) {
        $sn = ''
        if ($p.ShareName) { $sn = [string]$p.ShareName } else { $sn = [string]$p.Name }
        W ('         >> 其他电脑要连这个：\\' + $cn + '\' + $sn) 'Green'
        if (Test-ShareNameBad $sn) {
          AddR ('共享名不合规：' + $sn) 'WARN' '含中文、空格或特殊符号。客户机上要手打这个路径，很容易打错。' ('建议改成 ' + (Sanitize-ShareName $sn) + '（注意：改了之后已经连过的电脑要重新连）')
        }
      } elseif ($prs.Count -le 3) {
        W ('         -- 未共享：' + $p.Name) 'DarkGray'
      }
    }
  }

  # --- 4. 网络位置 ---
  $pub = @()
  $priv = @()
  try {
    $nps = @(Get-NetConnectionProfile)
    foreach ($np in $nps) { if ($np.NetworkCategory -ne 'Private') { $pub += $np } else { $priv += $np } }
    if ($pub.Count -gt 0) {
      $names = ($pub | ForEach-Object { [string]$_.Name }) -join '、'
      AddR '网络位置' 'BAD' ('「' + $names + '」被识别成公用网络 —— 公用网络默认不准共享。') '修复模式会自动改成「专用网络」。'
    } elseif ($priv.Count -gt 0) {
      AddR '网络位置' 'OK' ('「' + (($priv | ForEach-Object { [string]$_.Name }) -join '、') + '」是专用网络') ''
    }
  } catch { }

  # --- 5. 防火墙规则 ---
  # !!! 别把整组规则都算进来。实测这两个组加起来 82 条，其中 Outbound 和 Public profile 的
  #     本来就该是禁用的 —— 真把它们全开，等于在公用网络上把文件共享暴露出去。
  #     只关心「入站 + Profile 含 Private」：这才是「连在办公室内网时能不能共享」。
  $fwNeed = @()
  $fwOn = 0
  $fwErr = ''
  try {
    $g1 = @(Get-NetFirewallRule -Group '@FirewallAPI.dll,-28502' -ErrorAction Stop)   # 文件和打印机共享
    $in1 = @($g1 | Where-Object { $_.Direction -eq 'Inbound' -and ([string]$_.Profile) -match 'Private' })
    foreach ($r in $in1) { if ($r.Enabled -eq 'True') { $fwOn++ } else { $fwNeed += $r } }
    if ($in1.Count -eq 0) { $fwErr = '没读到可用规则' }
  } catch { $fwErr = '读不到防火墙规则' }
  if ($fwErr) {
    AddR '防火墙（文件和打印机共享）' 'INFO' ($fwErr + '，系统可能没有 Windows 防火墙模块。') '手动检查：控制面板 - Windows Defender 防火墙 - 允许应用通过防火墙，勾「文件和打印机共享」的专用网络。'
  } elseif ($fwNeed.Count -eq 0) {
    AddR '防火墙（文件和打印机共享）' 'OK' ('专用网络下的入站规则已启用（' + $fwOn + ' 条）') ''
  } else {
    AddR '防火墙（文件和打印机共享）' 'BAD' ('专用网络下还有 ' + $fwNeed.Count + ' 条入站规则没启用（共 ' + $in1.Count + ' 条）') '修复模式只会启用这批；公用网络的那些不动。'
  }

  # 网络发现（让别的电脑能在「网络」里看到这台）——只提示，不判 BAD：
  # 用 \\IP\共享名 直连时它不是必需的，但依赖「按主机名连接」和 net view 枚举时要靠它。
  try {
    $g2 = @(Get-NetFirewallRule -Group '@FirewallAPI.dll,-32752' -ErrorAction Stop)
    $in2 = @($g2 | Where-Object { $_.Direction -eq 'Inbound' -and ([string]$_.Profile) -match 'Private' })
    $on2 = @($in2 | Where-Object { $_.Enabled -eq 'True' }).Count
    if ($in2.Count -gt 0) {
      if ($on2 -eq 0) {
        AddR '网络发现' 'INFO' '专用网络下的入站规则一条都没启用。' '直连 \\IP\共享名 不受影响；但客户按主机名找、或在「网络」里看不到这台时会用到它。'
      } else {
        AddR '网络发现' 'OK' ('专用网络下已启用 ' + $on2 + '/' + $in2.Count + ' 条入站规则') ''
      }
    }
  } catch { }

  # --- 6. 第三方安全软件 ---
  $av = @()
  foreach ($pn in @('360tray', '360Safe', 'ZhuDongFangYu', 'HipsTray', 'HipsDaemon', 'QQPCTray', 'QMPersonalCenter', 'kxetray')) {
    try {
      $p = Get-Process -Name $pn -ErrorAction SilentlyContinue
      if ($p) { $av += $pn }
    } catch { }
  }
  if ($av.Count -gt 0) {
    AddR '第三方安全软件' 'WARN' ('检测到：' + (($av | Select-Object -Unique) -join '、')) '这类软件常自己接管防火墙并拦 445/135。共享不通时先把它退出来再试一次 —— 这一步工具替你做不了。'
  }

  # --- 7. 关键端口是否在监听 ---
  $listen = @()
  try { $listen = @(Get-NetTCPConnection -State Listen -LocalPort 135, 139, 445 -ErrorAction Stop) } catch { }
  $ports = @{}
  foreach ($c in $listen) { $ports[[int]$c.LocalPort] = $true }
  if ($ports.Count -eq 0) {
    AddR '共享端口监听 (135/139/445)' 'INFO' '读不到端口状态（权限不足或系统较老）。' '以管理员身份运行可看。'
  } elseif (-not $ports.ContainsKey(445)) {
    AddR '共享端口监听 (135/139/445)' 'BAD' '445 端口没有在监听 —— 客户机根本连不过来。' '多半是文件共享服务没起来（见上一项），或 SMB 被安全软件关了。'
  } else {
    AddR '共享端口监听 (135/139/445)' 'OK' (($ports.Keys | Sort-Object | ForEach-Object { [string]$_ }) -join ' / ') ''
  }

  # --- 8. 空密码 / Guest ---
  $lb = RegGet 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' 'LimitBlankPasswordUse'
  if ($null -ne $lb -and [int]$lb -eq 1) {
    AddR '空密码账户限制' 'INFO' '开着（默认）。主机上的账户必须有密码，否则客户机用 Guest/空密码连不进来。' '给客户机一个「主机上真实存在且带密码」的账户最省事。'
  }
  $guestOn = $false
  try {
    $g = Get-LocalUser -Name 'Guest' -ErrorAction Stop
    if ($g -and $g.Enabled) { $guestOn = $true }
  } catch { }
  if ($guestOn) {
    AddR 'Guest 账户' 'WARN' 'Guest 是启用的。Win11 24H2 起 SMB 默认要求签名，靠 Guest 访问会失效。' '别为了共享去开 Guest。用带密码的账号更稳、也更安全。'
  }

  # --- 9. 「拒绝从网络访问这台计算机」 ---
  if ($IsAdmin) {
    $sec = Join-Path $env:TEMP ('np_sec_' + [Guid]::NewGuid().ToString('N') + '.inf')
    $hit = ''
    try {
      & secedit.exe /export /cfg $sec /areas USER_RIGHTS 2>$null | Out-Null
      if (Test-Path -LiteralPath $sec) {
        $txt = Get-Content -LiteralPath $sec -Encoding Unicode -ErrorAction SilentlyContinue
        foreach ($ln in @($txt)) {
          if ([string]$ln -match '^\s*SeDenyNetworkLogonRight\s*=\s*(.*)$') { $hit = [string]$matches[1] }
        }
      }
    } catch { }
    try { if (Test-Path -LiteralPath $sec) { Remove-Item -LiteralPath $sec -Force } } catch { }
    # ⚠️ 顺序不能反：Everyone / Authenticated Users 才是真·拦路虎；
    #    `Guest` 是 **Windows 的默认值**（实测 19045 上 secedit 导出的就是 Guest），
    #    把它报成「有问题」会让售后去改一个本来不该动的设置。
    if ($hit -match 'Everyone|S-1-1-0|Authenticated Users|NT AUTHORITY\\Network|S-1-5-5-') {
      AddR '「拒绝从网络访问这台计算机」' 'BAD' ('策略里有：' + $hit + ' —— 这会把所有网络登录都挡掉。') '打开 secpol.msc - 本地策略 - 用户权限分配 - 「拒绝从网络访问这台计算机」，把 Everyone / Authenticated Users 删掉。'
    } elseif ($hit -match 'Guest') {
      AddR '「拒绝从网络访问这台计算机」' 'INFO' ('有 Guest（' + $hit + '）—— 这是 Windows 的默认值。') '不用动它。要用账号访问共享，这条不影响；就算你想用 Guest，也建议改成用带密码的账号。'
    } elseif ($hit) {
      AddR '「拒绝从网络访问这台计算机」' 'OK' ('只有：' + $hit) ''
    } else {
      AddR '「拒绝从网络访问这台计算机」' 'OK' '没有限制条目' ''
    }
  }

  # --- 10. RPC 加密（0x0000011b 的头号原因） ---
  $rpc = RegGet 'HKLM:\SYSTEM\CurrentControlSet\Control\Print' 'RpcAuthnLevelPrivacyEnabled'
  if ($null -eq $rpc) {
    AddR 'RPC 加密（0x0000011b）' 'INFO' '未设置 —— 2021-09-14 之后的系统等同于「强制开启」。老系统的客户机可能因此连不上。' '先试「命名管道」那条路（不需要降安全）；实在不行才考虑降级，且事后要改回。'
  } elseif ([int]$rpc -eq 1) {
    AddR 'RPC 加密（0x0000011b）' 'INFO' '强制开启在（安全状态）。如果客户机报 0x0000011b，就是这一项在起作用。' '优先走命名管道；降级要客户同意，且事后必须改回 1。'
  } else {
    AddR 'RPC 加密（0x0000011b）' 'WARN' '已经被改成 0 —— 这是**安全降级**，等于重新打开了 CVE-2021-1678 那个漏洞。' '确认打印机现在能用了之后，用管理员命令行改回：reg add "HKLM\SYSTEM\CurrentControlSet\Control\Print" /v RpcAuthnLevelPrivacyEnabled /t REG_DWORD /d 1 /f 然后重启 Spooler。'
  }

  # --- 11. 命名管道策略 ---
  $rp = RegGet 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Printers\RPC' 'RpcProtocols'
  if ($null -eq $rp) {
    AddR 'RPC 传输协议' 'INFO' '默认（只走 RPC over TCP）' '客户机报 0x0000011b 时，可让主机同时接受命名管道：RpcProtocols = 7'
  } else {
    AddR 'RPC 传输协议' 'INFO' ('RpcProtocols = ' + [int]$rp + '（7 = TCP + 命名管道都接受）') ''
  }

  # --- 12. SMB 签名（Win11 24H2 起默认要求） ---
  $osb = 0
  try { $osb = [Environment]::OSVersion.Version.Build } catch { }
  $signed = $null
  try {
    $smb = Get-SmbServerConfiguration -ErrorAction Stop
    $signed = $smb.RequireSecuritySignature
  } catch { }
  if ($osb -ge 26100) {
    AddR 'SMB 签名' 'INFO' ('系统是 24H2 或更新（build ' + $osb + '），SMB 默认要求签名' + $(if ($signed -eq $true) { '，当前已强制' } else { '' }) + '。') '后果：靠 Guest / 无密码访问的老办法会失效。修法是用带密码的账号，不要去关 SMB 签名。'
  }

  # --- 13. Windows 受保护打印模式 ---
  $wpp = RegGet 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Printers\WPP' 'WindowsProtectedPrintMode'
  if ($null -ne $wpp -and [int]$wpp -eq 1) {
    AddR 'Windows 受保护打印模式' 'BAD' '开着。它只认 Mopria 认证的打印机，第三方驱动的打印机会被移除或装不上。' '修复模式会问你要不要关掉。注意：关掉之后，之前被移除的打印机要手工重新装一遍。'
  } else {
    AddR 'Windows 受保护打印模式' 'OK' '没开（兼容性最好）' ''
  }
}

function Fix-Host {
  W ''
  W '========== 主机修复 ==========' 'Cyan'
  W '  规则：安全的直接改；会降低安全性或改变共享路径的，逐项问你。' 'Gray'

  # 1. Spooler
  $sp = $null
  try { $sp = Get-Service -Name Spooler -ErrorAction Stop } catch { }
  if ($null -ne $sp -and $sp.Status -ne 'Running') {
    if (-not $IsAdmin) { NeedAdmin '启动打印后台服务' }
    else {
      try { Start-Service -Name Spooler -ErrorAction Stop; W '  已启动打印后台服务 (Spooler)。' 'Green' }
      catch { W '  启动 Spooler 失败，可能需要重启电脑。' 'Red' }
    }
  }
  if ($IsAdmin) {
    try {
      $m = (Get-CimInstance Win32_Service -Filter "Name='Spooler'").StartMode
      if ($m -and $m -ne 'Auto') {
        Set-Service -Name Spooler -StartupType Automatic
        W '  已把 Spooler 启动方式改成「自动」。' 'Green'
      }
    } catch { }
  }

  # 2. LanmanServer
  $sv = $null
  try { $sv = Get-Service -Name LanmanServer -ErrorAction Stop } catch { }
  if ($null -ne $sv -and $sv.Status -ne 'Running') {
    if (-not $IsAdmin) { NeedAdmin '启动文件共享服务' }
    else {
      try { Start-Service -Name LanmanServer -ErrorAction Stop; W '  已启动文件共享服务 (LanmanServer)。' 'Green' }
      catch { W '  启动 LanmanServer 失败。' 'Red' }
    }
  }

  # 3. 网络位置 -> 专用
  $pub = @()
  try {
    $pub = @(Get-NetConnectionProfile | Where-Object { $_.NetworkCategory -ne 'Private' })
  } catch { }
  if ($pub.Count -gt 0) {
    if (-not $IsAdmin) { NeedAdmin '把网络位置改成专用' }
    else {
      W ('  即将把这些网络改成「专用」：' + (($pub | ForEach-Object { [string]$_.Name }) -join '、')) 'Yellow'
      W '  说明：专用网络才允许共享。如果是客户在咖啡厅/酒店的网络，改完等于对外暴露共享 —— 请先确认这是办公室内网。' 'Yellow'
      if (AskY '确认改成专用网络？') {
        foreach ($np in $pub) {
          try {
            Set-NetConnectionProfile -InterfaceIndex $np.InterfaceIndex -NetworkCategory Private -ErrorAction Stop
            W ('  已改成专用：' + $np.Name) 'Green'
          } catch { W ('  改失败：' + $np.Name) 'Red' }
        }
      } else { W '  已跳过网络位置。' 'Gray' }
    }
  }

  # 4. 防火墙规则 —— 只启用「文件和打印机共享」的入站 + 专用网络那批
  $fwNeed = @()
  try {
    $g1 = @(Get-NetFirewallRule -Group '@FirewallAPI.dll,-28502' -ErrorAction SilentlyContinue)
    $fwNeed = @($g1 | Where-Object { $_.Direction -eq 'Inbound' -and ([string]$_.Profile) -match 'Private' -and $_.Enabled -ne 'True' })
  } catch { }
  if ($fwNeed.Count -gt 0) {
    if (-not $IsAdmin) { NeedAdmin '启用防火墙放行规则' }
    else {
      try {
        # 这个 cmdlet 会画进度条，在「体检报告」中间突然冒出来很突兀，关掉它
        $oldProg = $ProgressPreference
        $ProgressPreference = 'SilentlyContinue'
        $fwNeed | Enable-NetFirewallRule -ErrorAction Stop
        $ProgressPreference = $oldProg
        W ('  已启用 ' + $fwNeed.Count + ' 条「文件和打印机共享」入站规则。') 'Green'
        W '    只动了专用网络那批 —— 公用网络（咖啡厅/酒店 Wi-Fi）下仍然不放行，这是对的。' 'Gray'
      } catch { W '  启用失败，请到防火墙界面手动勾「文件和打印机共享」。' 'Red' }
    }
  }

  # 5. 共享打印机
  $prs = @(Get-PrintersSafe)
  $notShared = @($prs | Where-Object { $_.Shared -ne $true })
  $shared = @($prs | Where-Object { $_.Shared -eq $true })
  $cn = $env:COMPUTERNAME
  if ($prs.Count -gt 0 -and $shared.Count -eq 0 -and $notShared.Count -gt 0) {
    W ''
    W '  这台电脑上的打印机：' 'Cyan'
    $i = 0
    foreach ($p in $prs) { $i++; W ('   [' + $i + '] ' + $p.Name) 'White' }
    $sel = Read-Host '  要把哪一台共享出去？（输入序号，直接回车 = 不共享）'
    $idx = 0
    if ([int]::TryParse($sel, [ref]$idx) -and $idx -ge 1 -and $idx -le $prs.Count) {
      $p = $prs[$idx - 1]
      $sug = Sanitize-ShareName ([string]$p.Name)
      $sn = Read-Host ('  共享名（回车用「' + $sug + '」；建议别用中文和空格）')
      if ([string]::IsNullOrWhiteSpace($sn)) { $sn = $sug }
      W ('  将把「' + $p.Name + '」共享为 \\' + $cn + '\' + $sn) 'Yellow'
      if (AskY '确认共享这一台？') {
        try {
          Set-Printer -Name $p.Name -Shared $true -ShareName $sn -ErrorAction Stop
          W ('  已共享。其他电脑连：\\' + $cn + '\' + $sn) 'Green'
          W ('  用 IP 更稳：\\<本机IP>\' + $sn + '   （本机 IP 见下面「怎么用」里的说明）') 'Green'
        } catch {
          W '  设置共享失败。请在「打印机属性 - 共享」里手动勾选。' 'Red'
        }
      } else { W '  已跳过共享。' 'Gray' }
    }
  }

  # 6. 共享名不合规 -> 建议改
  foreach ($p in $shared) {
    $sn = ''
    if ($p.ShareName) { $sn = [string]$p.ShareName } else { $sn = [string]$p.Name }
    if (Test-ShareNameBad $sn) {
      $newsn = Sanitize-ShareName $sn
      W ''
      W ('  共享名「' + $sn + '」含中文/空格/特殊符号，客户机上很容易打错。') 'Yellow'
      W ('  建议改成「' + $newsn + '」。注意：改完已经连过的电脑会连不上，要重新连。') 'Yellow'
      if (AskY '确认改共享名？') {
        try {
          Set-Printer -Name $p.Name -ShareName $newsn -ErrorAction Stop
          W ('  已改成：' + $newsn) 'Green'
        } catch { W '  改共享名失败。' 'Red' }
      } else { W '  已跳过。' 'Gray' }
    }
  }

  # 7. RPC：先给更安全的路，再问降级
  $rpc = RegGet 'HKLM:\SYSTEM\CurrentControlSet\Control\Print' 'RpcAuthnLevelPrivacyEnabled'
  $needRpc = $false
  if ($null -eq $rpc) { $needRpc = $true } elseif ([int]$rpc -eq 1) { $needRpc = $true }
  if ($needRpc) {
    W ''
    W '  ------------------------------------------------------------' 'DarkGray'
    W '  客户机报 0x0000011b 时，主机这边有两个办法：' 'Cyan'
    W '    A. 先试「让主机同时接受命名管道」—— 不降低安全性，优先走这个。' 'Green'
    W '    B. 把 RPC 加密关掉（RpcAuthnLevelPrivacyEnabled = 0）。' 'Yellow'
    W '       这是**安全降级**：等于重新打开 CVE-2021-1678 那个打印漏洞。' 'Yellow'
    W '       微软的口径是「只能在确认的兼容性事故里临时用，事后必须改回 1」。' 'Yellow'
    W '  ------------------------------------------------------------' 'DarkGray'
    if (AskY '先做 A：让主机同时接受命名管道？') {
      if (-not $IsAdmin) { NeedAdmin '修改 RPC 传输协议策略' }
      else {
        if (RegSetDword 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Printers\RPC' 'RpcProtocols' 7) {
          W '  已设置 RpcProtocols = 7（TCP + 命名管道）。已重启打印服务。' 'Green'
          try { Restart-Service -Name Spooler -Force -ErrorAction SilentlyContinue } catch { }
        } else { W '  设置失败。' 'Red' }
      }
    }
    W ''
    W '  只有在 A 试过仍不行、且你确认要为了这台打印机降一次安全时，才做 B。' 'Yellow'
    if (AskY '做 B：把 RPC 加密关掉（安全降级）？') {
      if (-not $IsAdmin) { NeedAdmin '关闭 RPC 加密' }
      else {
        if (RegSetDword 'HKLM:\SYSTEM\CurrentControlSet\Control\Print' 'RpcAuthnLevelPrivacyEnabled' 0) {
          try { Restart-Service -Name Spooler -Force -ErrorAction SilentlyContinue } catch { }
          W '  已设成 0 并重启打印服务。现在再让客户机连一次。' 'Green'
          W '  [!] 记住这一条：打印机恢复正常之后，要把它改回 1。' 'Yellow'
          W '      reg add "HKLM\SYSTEM\CurrentControlSet\Control\Print" /v RpcAuthnLevelPrivacyEnabled /t REG_DWORD /d 1 /f' 'White'
          W '      然后：net stop spooler & net start spooler' 'White'
          [void]$script:NOTES.Add('主机 RPC 加密已降级为 0（为了 0x0000011b）—— 事后必须改回 1。命令：reg add "HKLM\SYSTEM\CurrentControlSet\Control\Print" /v RpcAuthnLevelPrivacyEnabled /t REG_DWORD /d 1 /f  （然后 net stop spooler & net start spooler）')
        } else { W '  设置失败。' 'Red' }
      }
    }
  }

  # 8. 受保护打印模式
  $wpp = RegGet 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Printers\WPP' 'WindowsProtectedPrintMode'
  if ($null -ne $wpp -and [int]$wpp -eq 1) {
    W ''
    W '  Windows 受保护打印模式开着 —— 第三方驱动的打印机会被移除或装不上。' 'Yellow'
    W '  关掉它会让这台电脑更容易装老打印机，但也少了一层保护。' 'Yellow'
    W '  注意：关掉之后，之前被它移除的打印机要手工重新装一遍。' 'Yellow'
    if (AskY '确认关掉受保护打印模式？') {
      if (-not $IsAdmin) { NeedAdmin '关闭受保护打印模式' }
      else {
        if (RegSetDword 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Printers\WPP' 'WindowsProtectedPrintMode' 0) {
          try { Restart-Service -Name Spooler -Force -ErrorAction SilentlyContinue } catch { }
          W '  已关闭。重启打印服务完成。' 'Green'
          [void]$script:NOTES.Add('已关闭 Windows 受保护打印模式（WPP）—— 之前被移除的打印机需要手工重新安装。')
        } else { W '  设置失败。' 'Red' }
      }
    } else { W '  已跳过。' 'Gray' }
  }

  W ''
  W '  主机修复结束。' 'Cyan'
  # ⚠️ 结尾这句话必须按「修完之后到底有没有共享出去」来说。
  #    无条件打「用 \\主机\<共享名> 连接」的话，一台都没共享时（第 5 步被跳过）
  #    等于把售后和客户一起带偏：客户照着连，必然连不上。
  $nowShared = @()
  try { $nowShared = @(Get-PrintersSafe | Where-Object { $_.Shared -eq $true }) } catch { }
  if ($nowShared.Count -gt 0) {
    $mi = @(Get-MyIPv4)
    foreach ($p in $nowShared) {
      $sn = ''
      if ($p.ShareName) { $sn = [string]$p.ShareName } else { $sn = [string]$p.Name }
      W ('  告诉客户：在要打印的那台电脑上，用 \\' + $cn + '\' + $sn + ' 连接。') 'Green'
      foreach ($one in $mi) {
        W ('            连不上就换 IP：\\' + $one + '\' + $sn) 'Green'
      }
    }
  } else {
    W '  这台现在还**没有**共享出去的打印机。' 'Yellow'
    W '  上面第 5 步如果跳过了，就先别让客户去连 —— 拿本工具到「客户机」那台去连也是连不上的。' 'Yellow'
  }
}

# =====================================================================
# 客户机侧：连到别人共享的打印机
# =====================================================================
function Diag-Client {
  $script:ROWS.Clear()
  W ''
  W '========== 客户机体检（这台要连别人的打印机） ==========' 'Cyan'
  W ''

  try {
    $os = [Environment]::OSVersion.Version
    AddR '系统版本' 'INFO' ('Windows ' + $os.Major + '.' + $os.Minor + '  build ' + $os.Build) ''
  } catch { }

  $sp = $null
  try { $sp = Get-Service -Name Spooler -ErrorAction Stop } catch { }
  if ($null -eq $sp) {
    AddR '打印后台服务 (Spooler)' 'BAD' '读不到这个服务。' '系统打印组件可能被精简了。'
  } elseif ($sp.Status -ne 'Running') {
    AddR '打印后台服务 (Spooler)' 'BAD' ('当前状态：' + $sp.Status) '修复模式会自动启动。它没起来，什么打印机都用不了。'
  } else {
    AddR '打印后台服务 (Spooler)' 'OK' '正在运行' ''
  }

  # 已装打印机与状态
  $prs = @(Get-PrintersSafe)
  if ($prs.Count -eq 0) {
    AddR '已装打印机' 'INFO' '这台还没有装任何打印机。' '用菜单里的「找局域网里的共享打印机」。'
  } else {
    $off = @()
    foreach ($p in $prs) {
      $st = ''
      try { $st = [string]$p.PrinterStatus } catch { }
      $wo = $false
      try { if ($p.WorkOffline) { $wo = $true } } catch { }
      # ⚠️ 脱机有两条线索：PrinterStatus（Get-Printer 给枚举名）与 WorkOffline（WMI 给布尔）。
      #    只查一个会漏 —— 老系统上 Get-Printer 可能不存在，走的是 WMI 兜底。
      if ($wo -or ($st -match 'Offline|脱机|Error|错误|Paused|暂停')) { $off += $p }
    }
    if ($off.Count -gt 0) {
      $names = (($off | ForEach-Object { [string]$_.Name }) -join '；')
      AddR '已装打印机' 'WARN' ($names + '  处于脱机/错误/暂停状态') '修复模式会清空卡住的打印队列；仍不行就删掉这台重新连。'
    } else {
      AddR '已装打印机' 'OK' ('共 ' + $prs.Count + ' 台') ''
    }
  }

  # 卡住的打印队列
  $spoolDir = Join-Path $env:SystemRoot 'System32\spool\PRINTERS'
  $cnt = 0
  try { $cnt = @(Get-ChildItem -LiteralPath $spoolDir -Force -File -ErrorAction SilentlyContinue).Count } catch { $cnt = 0 }
  if ($cnt -gt 0) {
    AddR '打印队列' 'WARN' ('排队/卡住的任务 ' + $cnt + ' 个') '修复模式会清空它（这些任务本来也打不出来）。'
  } else {
    AddR '打印队列' 'OK' '是空的' ''
  }

  # 残留凭据
  $creds = @()
  try {
    $out = @(& cmdkey.exe /list 2>$null)
    foreach ($ln in $out) {
      $s = [string]$ln
      if ($s -match '目标[:：]\s*(\S+)') { $creds += [string]$matches[1] }
      elseif ($s -match 'Target:\s*(\S+)') { $creds += [string]$matches[1] }
    }
  } catch { }
  # !!! 方言陷阱：cmdkey 的输出是本地化的（中文「目标:」/ 英文「Target:」），两种都要认。
  $creds = @($creds | Where-Object { $_ -and $_ -notmatch '^MicrosoftAccount|^WindowsLive|^virtualapp' })
  if ($creds.Count -gt 0) {
    AddR '已保存的访问凭据' 'WARN' ($creds -join ' ; ') '主机改过密码时，这里存着旧密码就会一直报 0x0000052e。修复模式会问你要不要清掉。'
  } else {
    AddR '已保存的访问凭据' 'OK' '没有多余的凭据' ''
  }

  # 驱动安装限制
  $rd = RegGet 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Printers\PointAndPrint' 'RestrictDriverInstallationToAdministrators'
  if ($null -eq $rd -or [int]$rd -eq 1) {
    AddR '驱动安装限制' 'INFO' '「只允许管理员装打印驱动」在生效（2021-08 之后的默认）。' '普通账号点连接会报 0x00000bc4。右键本文件「以管理员身份运行」，或用管理员账号连接。'
  } else {
    AddR '驱动安装限制' 'WARN' '已被改成 0 —— 允许非管理员装打印驱动，安全性略低（CVE-2021-34481）。' '打印机装好后，建议改回 1。'
  }

  # RPC 传输协议（客户机侧）
  $rn = RegGet 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Printers\RPC' 'RpcUseNamedPipeProtocol'
  if ($null -eq $rn) {
    AddR 'RPC 传输方式' 'INFO' '默认（走 RPC over TCP）' '报 0x0000011b 时可改成走命名管道：RpcUseNamedPipeProtocol = 1'
  } elseif ([int]$rn -eq 1) {
    AddR 'RPC 传输方式' 'INFO' '已改成命名管道（RpcUseNamedPipeProtocol = 1）' ''
  } else {
    AddR 'RPC 传输方式' 'INFO' ('RpcUseNamedPipeProtocol = ' + [int]$rn) ''
  }

  # 受保护打印模式
  $wpp = RegGet 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Printers\WPP' 'WindowsProtectedPrintMode'
  if ($null -ne $wpp -and [int]$wpp -eq 1) {
    AddR 'Windows 受保护打印模式' 'BAD' '开着 —— 第三方驱动的打印机会装不上，之前装好的也可能被移除。' '修复模式会问你要不要关掉（关掉后要重新装一次打印机）。'
  } else {
    AddR 'Windows 受保护打印模式' 'OK' '没开' ''
  }

  # 网络位置
  try {
    $pub = @(Get-NetConnectionProfile | Where-Object { $_.NetworkCategory -ne 'Private' })
    if ($pub.Count -gt 0) {
      AddR '网络位置' 'WARN' ('「' + (($pub | ForEach-Object { [string]$_.Name }) -join '、') + '」是公用网络') '公用网络默认不放行文件共享。修复模式会改成专用。'
    } else {
      AddR '网络位置' 'OK' '专用网络' ''
    }
  } catch { }

  # 本机地址（同一网段才好连）
  $mine = @(Get-MyIPv4)
  if ($mine.Count -gt 0) {
    AddR '本机 IP' 'INFO' ($mine -join ' / ') '要和主机在同一个网段（前三段相同）才连得上。'
  }
}

function Fix-Client {
  W ''
  W '========== 客户机修复 ==========' 'Cyan'

  $sp = $null
  try { $sp = Get-Service -Name Spooler -ErrorAction Stop } catch { }
  if ($null -ne $sp -and $sp.Status -ne 'Running') {
    if (-not $IsAdmin) { NeedAdmin '启动打印后台服务' }
    else {
      try { Start-Service -Name Spooler -ErrorAction Stop; W '  已启动打印后台服务。' 'Green' }
      catch { W '  启动失败。' 'Red' }
    }
  }

  # 清空卡住的队列
  $spoolDir = Join-Path $env:SystemRoot 'System32\spool\PRINTERS'
  $files = @()
  try { $files = @(Get-ChildItem -LiteralPath $spoolDir -Force -File -ErrorAction SilentlyContinue) } catch { $files = @() }
  if ($files.Count -gt 0) {
    W ''
    W ('  打印队列里有 ' + $files.Count + ' 个卡住的任务。清空它们会丢掉这些没打出来的活页。') 'Yellow'
    if (AskY '确认清空打印队列？') {
      if (-not $IsAdmin) { NeedAdmin '清空打印队列' }
      else {
        try {
          Stop-Service -Name Spooler -Force -ErrorAction SilentlyContinue
          Start-Sleep -Milliseconds 600
          foreach ($f in $files) { Remove-Item -LiteralPath $f.FullName -Force -ErrorAction SilentlyContinue }
          Start-Service -Name Spooler -ErrorAction SilentlyContinue
          W '  已清空并重启打印服务。' 'Green'
        } catch { W '  清空失败，可能任务被占用。' 'Red' }
      }
    } else { W '  已跳过。' 'Gray' }
  }

  # 网络位置
  $pub = @()
  try { $pub = @(Get-NetConnectionProfile | Where-Object { $_.NetworkCategory -ne 'Private' }) } catch { }
  if ($pub.Count -gt 0) {
    if (-not $IsAdmin) { NeedAdmin '把网络位置改成专用' }
    else {
      W ('  即将把这些网络改成「专用」：' + (($pub | ForEach-Object { [string]$_.Name }) -join '、')) 'Yellow'
      if (AskY '确认改成专用网络？') {
        foreach ($np in $pub) {
          try {
            Set-NetConnectionProfile -InterfaceIndex $np.InterfaceIndex -NetworkCategory Private -ErrorAction Stop
            W ('  已改成专用：' + $np.Name) 'Green'
          } catch { W ('  改失败：' + $np.Name) 'Red' }
        }
      } else { W '  已跳过。' 'Gray' }
    }
  }

  # 清残留凭据
  $creds = @()
  try {
    $out = @(& cmdkey.exe /list 2>$null)
    foreach ($ln in $out) {
      $s = [string]$ln
      if ($s -match '目标[:：]\s*(\S+)') { $creds += [string]$matches[1] }
      elseif ($s -match 'Target:\s*(\S+)') { $creds += [string]$matches[1] }
    }
  } catch { }
  $creds = @($creds | Where-Object { $_ -and $_ -notmatch '^MicrosoftAccount|^WindowsLive|^virtualapp' } | Select-Object -Unique)
  if ($creds.Count -gt 0) {
    W ''
    W '  这些已保存的访问凭据，可能就是连不上的原因：' 'Yellow'
    foreach ($c in $creds) { W ('    - ' + $c) 'White' }
    W '  主机改过密码/账户名的话，这里存着旧密码会一直报 0x0000052e。' 'Yellow'
    if (AskY '确认清掉这些凭据？（只影响访问共享，不影响本机登录）') {
      foreach ($c in $creds) {
        try { & cmdkey.exe ('/delete:' + $c) 2>$null | Out-Null; W ('  已删除：' + $c) 'Green' } catch { }
      }
    } else { W '  已跳过。' 'Gray' }
  }

  # 受保护打印模式
  $wpp = RegGet 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Printers\WPP' 'WindowsProtectedPrintMode'
  if ($null -ne $wpp -and [int]$wpp -eq 1) {
    W ''
    W '  Windows 受保护打印模式开着，第三方驱动的打印机装不上。' 'Yellow'
    if (AskY '确认关掉受保护打印模式？') {
      if (-not $IsAdmin) { NeedAdmin '关闭受保护打印模式' }
      else {
        if (RegSetDword 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Printers\WPP' 'WindowsProtectedPrintMode' 0) {
          try { Restart-Service -Name Spooler -Force -ErrorAction SilentlyContinue } catch { }
          W '  已关闭，打印服务已重启。' 'Green'
          [void]$script:NOTES.Add('客户机已关闭 Windows 受保护打印模式（WPP）。')
        } else { W '  设置失败。' 'Red' }
      }
    } else { W '  已跳过。' 'Gray' }
  }

  W ''
  W '  客户机修复结束。' 'Cyan'
}

# ---------------- 找局域网里的共享打印机 ----------------
function Find-Printers {
  W ''
  W '========== 找局域网里的共享打印机 ==========' 'Cyan'
  $mine = @(Get-MyIPv4)
  if ($mine.Count -eq 0) {
    W '  [!] 没读到本机 IP —— 可能没连网。先确认网线和 Wi-Fi。' 'Red'
    return
  }
  # 同一个网段可能有多个（有线 + 无线），逐个扫
  $segments = @()
  foreach ($ip in $mine) {
    $p = $ip.Split('.')
    if ($p.Count -eq 4) { $segments += ($p[0] + '.' + $p[1] + '.' + $p[2]) }
  }
  $segments = @($segments | Select-Object -Unique)
  W ('  本机 IP：' + ($mine -join ' / ')) 'Yellow'
  W ('  将扫描网段：' + (($segments | ForEach-Object { $_ + '.1-254' }) -join ' , ')) 'Yellow'
  W '  只探测 445 端口（不登录、不猜密码、不读任何文件），大约 10~40 秒。' 'Gray'
  W ''
  if (-not (AskY '开始扫描？')) { return }

  $all = @()
  foreach ($seg in $segments) {
    $ips = @()
    for ($i = 1; $i -le 254; $i++) {
      $one = $seg + '.' + $i
      # 把自己排除掉：扫到自己没意义，还会让人以为「找到主机的打印机了」，白高兴一场。
      if ($mine -contains $one) { continue }
      $ips += $one
    }
    W ('  正在扫 ' + $seg + '.0/24 ...') 'Cyan'
    $hit = @(Scan-445 $ips)
    $all += $hit
    W ('    ' + $seg + ' 网段：' + $hit.Count + ' 台其他电脑在线') 'Green'
  }
  $all = @($all | Select-Object -Unique)
  if ($all.Count -eq 0) {
    W ''
    W '  [!] 这个网段里一台其他电脑都没找到。可能：' 'Red'
    W '      1. 这台电脑和主机不在同一个网络（比如一个连 Wi-Fi、一个连有线）' 'Gray'
    W '      2. 主机没开机，或主机的防火墙把 445 挡住了' 'Gray'
    W '      3. 路由器开了「AP 隔离 / 访客网络隔离」，设备之间互相看不见' 'Gray'
    W '      这时只能问客户主机 IP，用 \\IP\共享名 直连试试。' 'Gray'
    $manual0 = Read-Host '  要手动输入一个共享路径吗？形如 \\192.168.1.5\HP1020（直接回车 = 放弃）'
    if (-not [string]::IsNullOrWhiteSpace($manual0)) { [void](Connect-Share $manual0.Trim()) }
    return
  }

  $hosts = @()
  $found = @()
  foreach ($ip in $all) {
    $hn = ''
    try { $hn = [string]([System.Net.Dns]::GetHostEntry($ip)).HostName } catch { $hn = '' }
    $shares = @(Get-RemotePrinterShares $ip)
    $hosts += [pscustomobject]@{ IP = $ip; Host = $hn; Shares = $shares }
    foreach ($s in $shares) {
      $found += [pscustomobject]@{ IP = $ip; Host = $hn; Share = $s; UNC = ('\\' + $ip + '\' + $s) }
    }
  }

  # 扫到机器但一个共享都没列出来 —— 最常见的原因不是「没共享」，
  # 而是对方禁止匿名枚举（Win10/11 默认就偏严）。所以给一次带账号重试的机会。
  if ($found.Count -eq 0) {
    W ''
    W ('  扫到 ' + $all.Count + ' 台电脑，但没列出任何共享打印机。') 'Yellow'
    W '  这通常不是「没共享」，而是对方不允许匿名查看共享列表。' 'Yellow'
    W ''
    $i = 0
    foreach ($h in $hosts) {
      $i++
      $hnTxt = ''
      if ($h.Host) { $hnTxt = '  （' + $h.Host + '）' }
      W ('   [' + $i + '] ' + $h.IP + $hnTxt) 'White'
    }
    W ''
    $pick = Read-Host '  用主机上的账号试哪一台？（输入序号，直接回车 = 不试）'
    $pi = 0
    if ([int]::TryParse($pick, [ref]$pi) -and $pi -ge 1 -and $pi -le $hosts.Count) {
      $shares = @(Try-AuthView $hosts[$pi - 1].IP)
      foreach ($s in $shares) {
        $found += [pscustomobject]@{ IP = $hosts[$pi - 1].IP; Host = $hosts[$pi - 1].Host; Share = $s; UNC = ('\\' + $hosts[$pi - 1].IP + '\' + $s) }
      }
    }
  }

  if ($found.Count -eq 0) {
    W ''
    W '  没找到共享打印机。可能：' 'Yellow'
    W '   1. 主机上的打印机还没共享出去 -> 拿本工具到主机上跑一次「主机模式」' 'Gray'
    W '   2. 主机名/IP 变了 -> 直接手输路径试' 'Gray'
    W '   3. 网络设备做了隔离（访客 Wi-Fi、AP 隔离），两台机器互相看不见' 'Gray'
    $manual = Read-Host '  要手动输入一个共享路径吗？形如 \\192.168.1.5\HP1020（直接回车 = 放弃）'
    if (-not [string]::IsNullOrWhiteSpace($manual)) { [void](Connect-Share $manual.Trim()) }
    return
  }

  W ''
  W ('  找到 ' + $found.Count + ' 个共享打印机：') 'Green'
  $i = 0
  foreach ($f in $found) {
    $i++
    $hnTxt = ''
    if ($f.Host) { $hnTxt = '  （' + $f.Host + '）' }
    W ('   [' + $i + '] ' + $f.UNC + $hnTxt) 'White'
  }
  W ''
  $sel = Read-Host '  连哪一台？（输入序号，直接回车 = 不连）'
  $idx = 0
  if ([int]::TryParse($sel, [ref]$idx) -and $idx -ge 1 -and $idx -le $found.Count) {
    [void](Connect-Share $found[$idx - 1].UNC)
  }
}

function Connect-Share([string]$unc) {
  W ''
  W ('  正在连接：' + $unc) 'Cyan'
  $errs = @()
  $ok = $false

  # 已经是装好的就直接说
  $ex = @()
  try { $ex = @(Get-PrintersSafe | Where-Object { $_.Name -eq $unc }) } catch { }
  if ($ex.Count -gt 0) {
    W '  这台已经装过了，不用重复安装。' 'Yellow'
    W '  如果它打不出来，试试：打开「设置 - 打印机和扫描仪」→ 这台 → 打开打印队列，清掉卡住的任务。' 'Gray'
    return $true
  }

  try {
    Add-Printer -ConnectionName $unc -ErrorAction SilentlyContinue -ErrorVariable +errs | Out-Null
  } catch { $errs += $_ }
  Start-Sleep -Milliseconds 900
  try { $ex = @(Get-PrintersSafe | Where-Object { $_.Name -eq $unc }) } catch { }
  if ($ex.Count -gt 0) { $ok = $true }

  if (-not $ok) {
    # 回退：printui（老系统 / 组策略限得比较死时反而好使）
    W '  换一种方式再试一次...' 'DarkGray'
    try { & rundll32.exe printui.dll,PrintUIEntry /in /n $unc | Out-Null } catch { }
    Start-Sleep -Milliseconds 1500
    try { $ex = @(Get-PrintersSafe | Where-Object { $_.Name -eq $unc }) } catch { }
    if ($ex.Count -gt 0) { $ok = $true }
  }

  if ($ok) {
    W '  连上了。' 'Green'
    $dn = Read-Host '  要把它设为默认打印机吗？（输入 Y 设为默认，直接回车跳过）'
    if ($dn -eq 'Y' -or $dn -eq 'y') {
      try {
        $p = @(Get-PrintersSafe | Where-Object { $_.Name -eq $unc })[0]
        Set-Printer -Name $p.Name -ErrorAction SilentlyContinue | Out-Null
        try { (New-Object -ComObject WScript.Network).SetDefaultPrinter($unc) } catch { }
        W '  已设为默认打印机。' 'Green'
      } catch { W '  设默认失败，到打印机设置里手动设一下。' 'Yellow' }
    }
    W ''
    W '  建议现在打一张测试页：设置 - 打印机和扫描仪 - 这台 - 打印测试页。' 'Cyan'
    return $true
  }

  $txt = ''
  if ($errs.Count -gt 0) { $txt = (($errs | ForEach-Object { [string]$_ }) -join "`r`n") }
  Show-CodeTable $txt
  return $false
}

# ---------------- 日志 ----------------
function Save-Log {
  $dir = [Environment]::GetFolderPath('Desktop')
  if (-not $dir -or -not (Test-Path -LiteralPath $dir)) { $dir = $env:TEMP }
  $f = Join-Path $dir ('共享打印机日志_' + (Get-Date -Format 'yyyyMMdd_HHmmss') + '.txt')
  try {
    $script:LOG | Out-File -FilePath $f -Encoding UTF8
    # 事后要改回的项单独列在末尾，免得翻半天找不到
    if ($script:NOTES.Count -gt 0) {
      Add-Content -Path $f -Value '' -Encoding UTF8
      Add-Content -Path $f -Value '========== [!] 本次改动里需要事后处理的 ==========' -Encoding UTF8
      foreach ($n in $script:NOTES) { Add-Content -Path $f -Value ('  * ' + $n) -Encoding UTF8 }
    }
    return $f
  } catch {
    return ''
  }
}

# ---------------- 主流程 ----------------
Clear-Host
W '============================================================' 'Cyan'
W '  共享打印机工具  -  客管家百宝箱' 'Cyan'
W '============================================================' 'Cyan'
W ''
W ('  当前权限：' + $(if ($IsAdmin) { '管理员（可以改系统设置）' } else { '普通用户（改系统设置的项会跳过，建议右键「以管理员身份运行」）' })) 'Yellow'
W ('  本机名称：' + $env:COMPUTERNAME) 'Yellow'
$mine0 = @(Get-MyIPv4)
if ($mine0.Count -gt 0) { W ('  本机 IP ：' + ($mine0 -join ' / ')) 'Yellow' }
W ''
W '  这个工具只做两件事：告诉你哪里不对、按你的确认把该开的开起来。' 'Gray'
W '  不会上传任何东西，不会读取文件内容，不会猜密码。' 'Green'
W ''

# 角色用 0/1/2 表示（0=还没选），并在**同一个循环里**按角色显示不同菜单。
# 之前写成「if host {...} if client {...}」两段并列 —— 在主机菜单里按 [9] 切到客户机后
# 第二段会**再跑一遍客户机菜单**，用户看到菜单闪两次，断点也很难讲清。
$role = 0
while ($true) {
  if ($role -eq 0) {
    W '  ------------------------------------------------------------' 'DarkGray'
    W '   先确认：你现在坐在哪台电脑前？' 'Cyan'
    W '   [1]  这台插着打印机 —— 要把它共享给别人' 'White'
    W '   [2]  这台要连别人的打印机 —— 要连到局域网里的共享打印机' 'White'
    W '   [0]  退出' 'White'
    W '  ------------------------------------------------------------' 'DarkGray'
    $c = Read-Host '  请选择'
    if ($c -eq '1') { $role = 1 }
    elseif ($c -eq '2') { $role = 2 }
    elseif ($c -eq '0') { break }
    else { W '  请输入 0 / 1 / 2。' 'DarkYellow' }
    continue
  }

  W ''
  W '  ------------------------------------------------------------' 'DarkGray'
  if ($role -eq 1) {
    W '   [1]  体检：把主机该查的都查一遍（只看，不改任何设置）' 'White'
    W '   [2]  体检 + 修复（安全的直接改，会影响安全的逐项问你）' 'White'
  } else {
    W '   [1]  体检：把这台电脑该查的都查一遍（只看，不改）' 'White'
    W '   [2]  找局域网里的共享打印机（扫网段，可一键连接）' 'White'
    W '   [3]  体检 + 修复' 'White'
  }
  W '   [9]  换个角色（主机 <-> 客户机）' 'White'
  W '   [0]  退出' 'White'
  W '  ------------------------------------------------------------' 'DarkGray'
  $c = Read-Host '  请选择'

  if ($c -eq '0') { break }
  elseif ($c -eq '9') { if ($role -eq 1) { $role = 2 } else { $role = 1 } }
  elseif ($role -eq 1) {
    if ($c -eq '1') { Diag-Host; Show-Summary }
    elseif ($c -eq '2') { Diag-Host; Show-Summary; Fix-Host }
    else { W '  请输入 0 / 1 / 2 / 9。' 'DarkYellow' }
  } else {
    if ($c -eq '1') { Diag-Client; Show-Summary }
    elseif ($c -eq '2') { Find-Printers }
    elseif ($c -eq '3') { Diag-Client; Show-Summary; Fix-Client }
    else { W '  请输入 0 / 1 / 2 / 3 / 9。' 'DarkYellow' }
  }
}

$lf = Save-Log
W ''
if ($lf) {
  W ('  本次记录已保存到：' + $lf) 'Cyan'
} else {
  W '  日志保存失败（不影响你已经做过的操作）。' 'DarkYellow'
}
if ($script:NOTES.Count -gt 0) {
  W ''
  W '  [!] 有一件事要记住，事后必须处理：' 'Yellow'
  foreach ($n in $script:NOTES) { W ('      ' + $n) 'Yellow' }
}
W '  谢谢使用。' 'Green'
Read-Host '  按回车键关闭窗口'
