很多朋友都有类似的经历开完会回到工位屏幕还亮着中午去吃饭机箱风扇还在转桌上音箱的蓝色灯珠一直亮。Windows 电源计划里明明设了“30 分钟关闭显示器”但真到了时间屏幕往往又被后台网络请求顶起来甚至一整晚都在待机耗电。标题里这个需求——连续三十分钟无操作自动锁定屏幕并关闭外设电源实现节能——听起来很直白真要做出来却有不少细节坑。难点不在“怎么锁屏”而在“怎么准确判断 30 分钟无操作”和“怎么把外设的电源真正断掉”。这篇文章我会从原理到部署完整拆一遍核心方案是 PowerShell 脚本 任务计划程序不需要安装任何第三方工具Windows 10/11 原生就能跑。适合普通办公用户、IT 运维、以及所有想减少待机浪费的人。我会把判断空闲、锁屏、关显示器、断 USB 外设电源这几个环节逐一讲清最后附上我自己的功耗估算和踩坑记录。1. 先算账三十分钟待机到底多耗电系统自带的方案为什么做不好1.1 一台典型办公机的待机功耗构成很多人觉得电脑待机没多少电这是错觉。我按一台普通办公机的情况给你拆一下部件典型待机功耗说明主机中端办公配置35~55W系统空闲但风扇、主板、内存仍在供电显示器24 寸 IPS25~45W背光是耗电大头亮屏状态下稳定消耗桌面 USB 外设音箱、摄像头、RGB 灯、USB Hub5~12W很多外设根本不进待机通电就持续耗电键鼠0.5~1W功耗不大但属于“永远带电”的那一类合计起来一台机器什么都不干、只是开着屏幕等三十分钟耗电大约在 65W 到 110W 之间。如果每天有 2 小时处于无人状态午休 开会一个月按 22 个工作日算0.08kW × 2h × 22 ≈ 3.5 度电。加上显示器功耗后一个月能到 4~6 度电。单台看一年也就几十块钱但放到几十个工位的办公室等于白白烧掉一台大功率电暖器的能耗。更重要的是节能之外的收益屏幕一直亮会加速背光老化USB 外设长期通电发热电子元件的寿命也会缩短。所以“无人时自动断电”这件事不只是省钱。1.2 为什么 Windows 自带的“空闲”工具不好用你可能想系统不是自带“关闭显示器”“睡眠”“屏保锁定”吗为什么还要自己写脚本我实际用过之后发现每一套都有明显短板。第一电源计划里的“关闭显示器”达不到锁屏效果。它只是把显示器灭了但屏幕不锁任何人过来动一下鼠标就能看到桌面内容隐私风险一点没解决。第二屏保锁定依赖屏保程序3D 屏保在运行期间本身占用 CPU/GPU待机功耗不降反升。第三系统睡眠最彻底但副作用最大睡眠会把所有会话挂起远程桌面断连、下载中断、同步服务停摆、网络唤醒不稳定。对很多办公场景来说“锁屏 关显示器 断外设”才是最优解主机保持运行但所有耗电的显示和周边设备全部休息。还有一个更隐蔽的问题任务计划程序里自带“计算机空闲时触发”这个触发器我在早期方案里也试过。它的“空闲”判定和你想的不一样——系统会参考 CPU、磁盘、网络活动后台跑个更新就可能导致任务迟迟不触发或者在你刚走开 5 分钟时就提前触发。所以后来我放弃了系统内置空闲判断改成自己写空闲检测逻辑。2. 核心模块一用 GetLastInputInfo 准确判断“连续 30 分钟没有操作”2.1 为什么是 GetLastInputInfo而不是鼠标位置判断判断用户有没有操作电脑最常见的手段是轮询鼠标坐标如果鼠标不动就认为空闲。这个方案有一个致命问题——很多无线鼠标在桌面上会有微小的漂移或者你只是把鼠标放在那里系统仍可能因为微动信号而认为有输入反过来用户在看视频、看书、做演示时鼠标可能一直不动但人明明在电脑前。Windows 其实早就有专门的 API 干这件事GetLastInputInfo。它记录的是系统范围内最后一次输入事件键盘、鼠标、触摸、手写等的时间戳不关心鼠标坐标是否变化也不关心屏幕上正在显示什么。只要这段时间里没有任何输入事件就可以判定为“无操作”。这个 API 是跨进程的任何程序都能调用。PowerShell 可以通过 P/Invoke 直接访问不需要安装额外的模块。2.2 PowerShell 实现空闲检测的最小代码思路很简单调用 GetLastInputInfo 拿到最后一次输入时间再用 GetTickCount 拿到系统当前运行时间两者差值就是空闲毫秒数。当差值大于等于 1800 秒就进入“需要节能”的分支。Add-Type -TypeDefinition using System; using System.Runtime.InteropServices; public class IdleGuardNative { [StructLayout(LayoutKind.Sequential)] public struct LASTINPUTINFO { public uint cbSize; public uint dwTime; } [DllImport(user32.dll)] public static extern bool GetLastInputInfo(ref LASTINPUTINFO plii); [DllImport(kernel32.dll)] public static extern uint GetTickCount(); } function Get-IdleSeconds { $info New-Object IdleGuardNativeLASTINPUTINFO $info.cbSize [uint32][System.Runtime.InteropServices.Marshal]::SizeOf([type][IdleGuardNativeLASTINPUTINFO]) [IdleGuardNative]::GetLastInputInfo([ref]$info) | Out-Null $currentTicks [IdleGuardNative]::GetTickCount() $idleMs $currentTicks - $info.dwTime # 处理 GetTickCount 回绕的情况 if ($idleMs -lt 0) { $idleMs [math]::Pow(2, 32) } return [math]::Floor($idleMs / 1000) } $thresholdSeconds 30 * 60 $idle Get-IdleSeconds Write-Output IdleSeconds$idle ThresholdSeconds$thresholdSeconds if ($idle -ge $thresholdSeconds) { Write-Output IDLE_TRIGGERED }这段代码有一个很容易被忽略的细节GetTickCount 是一个 32 位无符号整数最大值约 49.7 天。如果系统连续运行超过这个时间计数会回绕到 0直接做减法会得到负数。我一开始没有处理这个问题测试时发现一台开机一个多月的电脑脚本计算出的空闲时间居然是负几十亿毫秒相当尴尬。加上if ($idleMs -lt 0) { $idleMs 4294967296 }之后才稳定。2.3 三个必须处理的边界场景判断空闲看似简单实际运行中会遇到几个边界问题。第一个是睡眠唤醒后的状态。笔记本合盖再打开或者台式机从睡眠恢复系统运行时间计数会暂停唤醒瞬间最后一次输入时间还是睡前的值。这时候脚本可能一醒过来就判定“已空闲 30 分钟”然后立刻锁屏体验很差。我通常在脚本里记录一个“上次检查时间”如果离唤醒时间太近比如 3 分钟内就自动跳过执行三步LastActiveCheck重置。第二个是远程桌面会话。如果你用远程桌面连接到这台机器并正在操作GetLastInputInfo 会把远程会话的输入也算进来这是我们希望的行为。但如果这台机器是空闲的只是你从它远程连了别的电脑那本机确实没有输入锁屏是合理的。所以不需要特殊处理只要知道即可。第三个是锁屏界面的干扰。用户输入密码解锁本身就是输入事件所以解锁后 LastInputInfo 会立即更新脚本不会误判。真正要注意的是锁屏后如果有人长时间不输密码系统会再次进入空闲状态脚本会把外设又断一遍——这没问题因为持续无人就是应该继续节能。3. 核心模块二锁屏、关显示器、切断外设电源的落地手段3.1 锁屏调用 LockWorkStation 而不是模拟快捷键Windows 下锁屏最简单的办法是调系统 APILockWorkStation()。它来自 user32.dll作用就是立即锁定工作站效果等同于按 Win L。在 PowerShell 里这样调用Add-Type -TypeDefinition using System.Runtime.InteropServices; public class User32Lock { [DllImport(user32.dll)] public static extern bool LockWorkStation(); } [User32Lock]::LockWorkStation() | Out-Null有一点特别重要不要在 SYSTEM 账户下调用 LockWorkStation。如果你把计划任务的“运行账户”选成 SYSTEM锁屏调用会静默失败或者锁上了但界面和行为都不对。正确做法是用“仅在用户登录时运行”的计划任务并以最高权限运行这样脚本进程在交互会话里锁屏 API 才能正常工作。这个坑我在后面专门说。3.2 关闭显示器发送 SC_MONITORPOWER 命令锁屏之后屏幕其实还亮着停留在锁屏时钟界面。要想真正关屏需要向系统广播一个“关闭监视器”的命令。原理是Windows 对显示器电源管理提供了一条系统命令WM_SYSCOMMAND参数SC_MONITORPOWER对应值 0xF170。当lParam2时显示器进入关闭状态也就是 DPMS off。Add-Type -TypeDefinition using System; using System.Runtime.InteropServices; public class MonitorPower { [DllImport(user32.dll)] public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); } $HWND_BROADCAST New-Object IntPtr 0xffff $WM_SYSCOMMAND 0x0112 $SC_MONITORPOWER 0xF170 $MONITOR_OFF 2 [MonitorPower]::SendMessage($HWND_BROADCAST, $WM_SYSCOMMAND, $SC_MONITORPOWER, $MONITOR_OFF) | Out-Null这个命令是“软关闭”显示器会进入待机低功耗状态唤醒时只需要按下键盘任意键或移动鼠标锁屏界面会自动亮起。注意这里的顺序先锁屏再发关屏命令。如果反过来可能先黑屏但没锁别人按一下键就能看到桌面。3.3 关闭外设电源软件禁用与硬件断电如何选这是整个方案里最有争议的部分。“关闭外设电源”在 Windows 层面没有一条命令能一键切断所有 USB 口供电所以要做取舍。我实际用了三种思路方案 A软件禁用设备Disable-PnpDevice对 Windows 来说禁用某个设备节点等于让系统不再给它供电和分配资源。可以用 PowerShell 自带的Disable-PnpDevice也可以用 Sysinternals 的devcon.exe disable。# 先看系统里有哪些需要断掉的设备 Get-PnpDevice -PresentOnly | Where-Object { $_.Class -in (AudioEndpoint, Camera, HIDClass, USB) } | Select-Object FriendlyName, InstanceId # 按 InstanceId 精准禁用例如音箱 Disable-PnpDevice -InstanceId USB\VID_0D8CPID_0134\51234567 -Confirm:$false优点是不需要额外硬件纯软件控制。缺点有两个一是需要管理员权限二是千万不要把鼠标键盘也禁掉否则你再也无法输入密码解锁电脑会陷入“锁死”状态。我自己的规则是只禁用电量较大的外设——音箱、摄像头、USB Hub、RGB 灯、外接声卡键鼠永远保留。方案 B硬件断电带开关的 USB Hub / 智能插座如果你追求“彻底断电”把耗电外设全部集中到一个带独立供电的 USB Hub然后用智能插座或继电器给 Hub 单独断电这是最干净的方案。脚本里直接控制智能插座的开关断电后外设一个灯都不亮恢复也可靠。缺点很明显要花钱买硬件还要把线整理清楚。对办公室工位来说软件禁用已经能满足 90% 的节能需求对家里书房这种追求极致的人可以搭配方案 B。方案 CUSB 选择性暂停Windows 电源管理里有一个“USB 选择性暂停”选项理论上可以让 USB 设备空闲挂起。但实测下来很多设备根本不响应尤其是键盘背光、RGB 灯这类花哨外设。它只能作为辅助手段不能作为主力方案。3.4 为什么不直接调用睡眠我在前面提过睡眠确实最省电但在这个标题的场景里并不适用。核心原因在于睡眠会中断所有正在运行的程序状态。办公机器上挂着即时通讯、远程桌面、云同步、下载任务睡眠之后这些全部断线。而“锁屏 关显示器 断外设”保留了主机的完整计算能力只是把所有消耗电能的外围设备停下来。两者解决的问题不一样。如果你的使用场景是“晚上下班后完全没人用”那直接睡眠或休眠是更好的选择。但如果是“午休、开会、临时走开”这种短时间无人状态本方案是最均衡的。4. 完整脚本与计划任务配置把判断和执行串起来4.1 一个可长期运行的 IdleGuard.ps1 脚本前面是拆开的功能片段这里给一个可以直接用的完整脚本。我会把锁屏、关屏、禁用外设、恢复外设、状态标记都放进去。核心逻辑是脚本常驻运行每 30 秒检查一次空闲时间触发条件后执行节能动作并在用户回来后恢复外设。# IdleGuard.ps1 # 用法powershell -NoProfile -ExecutionPolicy Bypass -File IdleGuard.ps1 -ThresholdMinutes 30 param( [int]$ThresholdMinutes 30, [string]$StateFile $env:ProgramData\IdleGuard\state.json, [string]$PeripheralFile $env:ProgramData\IdleGuard\peripherals.txt ) Add-Type -TypeDefinition using System; using System.Runtime.InteropServices; public class NativeApi { [StructLayout(LayoutKind.Sequential)] public struct LASTINPUTINFO { public uint cbSize; public uint dwTime; } [DllImport(user32.dll)] public static extern bool GetLastInputInfo(ref LASTINPUTINFO plii); [DllImport(kernel32.dll)] public static extern uint GetTickCount(); [DllImport(user32.dll)] public static extern bool LockWorkStation(); [DllImport(user32.dll)] public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); } $script:lastCheckTick [Environment]::TickCount64 function Get-IdleSeconds { $info New-Object NativeApiLASTINPUTINFO $info.cbSize [uint32][System.Runtime.InteropServices.Marshal]::SizeOf([type][NativeApiLASTINPUTINFO]) [NativeApi]::GetLastInputInfo([ref]$info) | Out-Null $currentTicks [NativeApi]::GetTickCount() $idleMs $currentTicks - $info.dwTime if ($idleMs -lt 0) { $idleMs [math]::Pow(2, 32) } return [math]::Floor($idleMs / 1000) } function Invoke-ScreenOff { $HWND_BROADCAST New-Object IntPtr 0xffff [NativeApi]::SendMessage($HWND_BROADCAST, 0x0112, 0xF170, 2) | Out-Null } function Read-PeripheralList { if (-not (Test-Path $PeripheralFile)) { return () } return Get-Content $PeripheralFile | Where-Object { $_ -and $_ -notmatch ^\s*# } } function Disable-Peripherals { $list Read-PeripheralList foreach ($id in $list) { try { Disable-PnpDevice -InstanceId $id -Confirm:$false -ErrorAction SilentlyContinue } catch {} } Set-Content -Path $StateFile -Value (Get-Date).ToString(yyyy-MM-dd HH:mm:ss) -Force } function Enable-Peripherals { if (-not (Test-Path $StateFile)) { return } $list Read-PeripheralList foreach ($id in $list) { try { Enable-PnpDevice -InstanceId $id -Confirm:$false -ErrorAction SilentlyContinue } catch {} } Remove-Item $StateFile -Force -ErrorAction SilentlyContinue } # 主循环 while ($true) { $idle Get-IdleSeconds $threshold $ThresholdMinutes * 60 # 唤醒后的宽限期3 分钟内不做任何锁屏/断外设动作 $elapsedSinceCheck [Environment]::TickCount64 - $script:lastCheckTick if ($elapsedSinceCheck -lt 0 -or $elapsedSinceCheck -gt 180000) { $script:lastCheckTick [Environment]::TickCount64 Start-Sleep -Seconds 30 continue } if ($idle -ge $threshold -and -not (Test-Path $StateFile)) { [NativeApi]::LockWorkStation() | Out-Null Start-Sleep -Seconds 1 Invoke-ScreenOff Disable-Peripherals Write-Output [$(Get-Date)] Idle $idle sec, trigger lock power save } elseif ($idle -lt ($threshold - 60) -and (Test-Path $StateFile)) { Enable-Peripherals Write-Output [$(Get-Date)] User back, peripherals enabled } $script:lastCheckTick [Environment]::TickCount64 Start-Sleep -Seconds 30 }这个脚本里我用了一个状态文件state.json来记录“当前外设是否处于禁用状态”这样避免了循环内反复执行禁用/启用命令。peripherals.txt里维护需要断电的设备 InstanceId 列表通过#开头的行可以做注释。关于唤醒宽限期我的逻辑是通过lastCheckTick判断脚本是否刚刚经历了一段较长的间隔——比如系统睡眠唤醒后循环中的 Start-Sleep 30 可能远远超过 30 秒这时候就跳过一轮检测避免立即锁屏。4.2 任务计划程序的正确配置方式脚本准备好之后还需要让它开机自启、长期运行。我推荐用任务计划程序注册一个“用户登录时触发”的任务而不是把脚本放进启动文件夹。原因是任务计划程序可以设置“使用最高权限运行”这样Disable-PnpDevice才能真正生效。以管理员身份打开 PowerShell注册命令如下$action New-ScheduledTaskAction -Execute powershell.exe -Argument -NoProfile -ExecutionPolicy Bypass -File C:\Scripts\IdleGuard.ps1 -ThresholdMinutes 30 $trigger New-ScheduledTaskTrigger -AtLogOn -User $env:USERDOMAIN\$env:USERNAME $settings New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit ([TimeSpan]::Zero) Register-ScheduledTask -TaskName IdleGuard -Action $action -Trigger $trigger -Settings $settings -RunLevel Highest -Description 30分钟无操作自动锁屏并节能有几个关键设置需要特别解释配置项推荐值原因触发器“用户登录时”脚本本身常驻不需要定时重复运行账户当前用户LockWorkStation 必须在交互会话中生效使用最高权限运行勾选禁用 USB 设备需要管理员权限如果任务运行时间超过不勾选限制脚本是无限循环不能设超时终止电池模式允许开始运行笔记本拔电状态下也要节能图形界面配置也不难任务计划程序 → 创建任务 → “常规”选项卡勾选“使用最高权限运行”“触发器”选“登录时”“操作”填 powershell.exe 和参数。关键点就是别选 SYSTEM 账户别勾选“不管用户是否登录都要运行”。4.3 外设如何恢复状态机设计脚本里最容易被忽略的是“恢复”逻辑。很多人只做了“禁用”用户回来后发现音箱没声音、摄像头打不开只能手动去设备管理器里启用体验很差。我的做法是维护一个状态标记state.json存在说明外设正处于禁用状态不存在说明一切正常。每 30 秒循环检查空闲时间 ≥ 30 分钟且状态不存在 → 执行锁屏、关屏、禁用外设并写状态标记。空闲时间 29 分钟且状态存在 → 执行启用外设并删除状态标记。为什么不把恢复条件设为“空闲时间 30 分钟”因为用户刚回来比如按了键盘一下但马上又去接电话走开此时恢复外设没问题但如果在锁屏界面按了键准备输密码却因为某种原因没有继续操作系统会再次进入空闲状态被重复置位。所以加一个 60 秒的缓冲区间避免在临界点上反复切换。4.4 日志出问题时不至于抓瞎脚本本身输出信息但要长期运行最好把日志落到文件里。最简单的方式是在脚本开头加Start-Transcript -Path $env:ProgramData\IdleGuard\idleguard.log -Append之后所有 Write-Output 都会追加到日志文件。排查问题时我一般先看日志里Idle xxx sec, trigger lock power save或User back, peripherals enabled出现的时间点基本就能定位是判断问题还是执行问题。5. 从功耗数据到实战避坑这套方案的真实效果与六个常见问题5.1 用典型功耗估算能省多少电我用一台典型办公机做估算对比状态主机显示器USB 外设合计原始待机45W30W8W83W锁屏 关显示器45W0.5W8W53.5W锁屏 关显示器 断主要外设45W0.5W1W46.5W假设每天无人时间为 2 小时一个月 22 个工作日。原始耗电0.083kW × 2h × 22 ≈ 3.65 度启用完整方案后0.0465kW × 2h × 22 ≈ 2.05 度。单台一个月省 1.6 度左右一年约 19 度电。如果办公室里几台、几十台机器都部署同样方案省下的数字很可观。当然这是基于典型办公机的估算实际值取决于你的硬件。如果你用的是带独立显卡的高配台式机主机待机可能到 80W 以上节能效果更明显如果是轻薄笔记本整机功耗本来就低主要收益就变成了隐私保护和屏幕寿命。5.2 六个我踩过的坑逐个说清楚坑一计划任务用 SYSTEM 账户运行锁屏永远不生效这是我第一次部署时犯的错误。脚本明明跑了日志里也有LockWorkStation调用但屏幕就是不变。原因就是 SYSTEM 账户运行的程序不在任何用户会话中锁屏 API 没有可操作的工作站。解决办法就是让任务“仅在用户登录时运行”账户选当前用户。坑二把鼠标键盘也禁了电脑直接变砖在“禁用外设”这条路上我一度想做得更彻底——把键鼠也加进禁用列表。测试的时候脚本一执行鼠标和键盘立刻失效屏幕倒是锁了但我再也无法输入密码解锁。最后只能强制重启然后从命令行把设备重新启用。从此我定了一条铁律peripherals.txt 里永远不写键鼠的 InstanceId。如果你想让外设断电更彻底用硬件开关比软件禁用键鼠安全得多。坑三GetTickCount 回绕导致空闲时间判断错误这个问题我在前面原理部分也讲过这里再强调一次。系统连续运行超过 49.7 天32 位计数会归零不做溢出处理的话脚本会把“已经空闲 3 秒”算成“空闲 42 亿毫秒”瞬间触发锁屏。实际上我处理后的代码没问题但有同事拷贝脚本时把溢出处理删掉了结果一个月后电脑频繁自动锁屏排查了很久才找到原因。坑四任务计划程序的“空闲”触发器不可控早期版本我用过系统自带空闲触发器结果发现后台运行一个磁盘碎片整理任务时系统认为 CPU 一直忙30 分钟不触发而某个进程空闲时 5 分钟就触发了。后来彻底改成脚本内轮询判断任务计划程序只负责开机拉起脚本所有逻辑都放在脚本里这才稳定下来。坑五睡眠唤醒后 3 秒内立刻锁屏笔记本合盖再打开或者台式机从睡眠恢复后GetLastInputInfo 的最后输入时间还停留在睡眠前脚本立刻判断为“已空闲 30 分钟”用户刚坐下就被锁屏。解决方案就是我脚本里加的那个唤醒宽限期检查循环间隔如果异常大比如超过 3 分钟本轮直接跳过。坑六禁用设备后恢复失败设备管理器里出现黄色感叹号有几种外设比如某些 USB 声卡被 Disable-PnpDevice 禁用后再执行 Enable-PnpDevice 返回成功但设备仍然没有恢复工作。这种情况通常是设备需要重新扫描。我一般会补一条命令pnputil /scan-devices或者干脆在恢复函数里遇到启用失败后等待 5 秒再执行一次Enable-PnpDevice。实测大部分设备二次启用都能恢复。如果你的外设比较特殊建议在状态标记里把设备名也写下来方便排查。5.3 进阶一点视频会议和全屏演示时怎么放行这套方案默认对所有“无操作”场景生效但有一个问题你开会时可能开着视频会议客户端人坐着不动鼠标键盘也没有输入30 分钟后脚本照样锁屏。这是很多人后续会遇到的需求。要解决也不难在脚本主循环里加一层“豁免检查”如果当前前台进程或正在运行的进程命中会议客户端名单就跳过本轮节能动作。PowerShell 获取前台窗口标题需要调GetForegroundWindow如果你只想快速实现可以直接用进程名单过滤$exemptProcesses (zoom, teams, meeting, wemeet) $isMeeting Get-Process | Where-Object { $exemptProcesses -contains $_.ProcessName.ToLower() } if ($isMeeting) { Write-Output [$(Get-Date)] Meeting in progress, skip energy saving Start-Sleep -Seconds 60 continue }这个方法不精确但大多数情况下够用。如果要求更严谨可以判断前台窗口标题是否包含“会议”“演示”“幻灯片放映”用 GetForegroundWindow API 加上窗口标题读取即可。这一层设计本质上是让节能策略服务于真实场景而不是反过来被规则绑架。我在实际使用中还有一个体会第一次部署这套方案时别一上来就把阈值设 30 分钟。建议先改成 2 分钟完整跑一遍“锁屏 → 关屏 → 断外设 → 恢复”的链路确认没有坑之后再调回 30 分钟。另外外设列表要定期更新每新增一个设备就检查一下它是否值得纳入待机断电名单。这套脚本我用了大半年最明显的感受是桌面的灯不再彻夜亮着屏幕老化速度也慢了不少。如果你也想动手建议按照文中的顺序先把空闲检测跑通再逐步加上外设禁用最后部署到全单位。