前言
玩原神和终末地等游戏,每天都要抽空去完成一套流程相对重复的日常任务,繁忙时经常顾不上,玩久了也会稍觉枯燥。
好在现在已经有了 BetterGI 和 MaaEnd 这类成熟的自动化工具,能帮你实现游戏内很多操作的自动化。
不过,它们需要手动点击开始,两个工具还得分别运行一次。而且,它们基本只能处理游戏内的操作,要实现真正的全自动,还需要做一些额外的配置和操作。
我的目标是实现彻底的全自动化——每天的指定时间,电脑自动开机,自动跑完两个游戏的日常,然后自动关机。这看起来不难实现,但其中真有不少细节和坑要处理。我陆陆续续折腾了近十天,才终于完整地实现了整个流程。你可以看看我的 演示视频 。我接下来就把整个过程完整复盘一遍,重点讲讲里面的难点和坑。
BetterGI / MaaEnd 简单介绍
BetterGI 的 官网 有详细的项目介绍、说明文档和下载方式。
BetterGI 功能强大,社区生态也非常丰富。你不仅能实现自动完成每日委托等基本日常,还能利用社区内的脚本自动完成物产采集、狗粮拾取、锄地打怪等进阶操作。
相应地,BetterGI 需要配置的内容也比较多。请务必详细阅读官网的用户手册,跟着说明一步步操作。
BetterGI 支持桌面分身启动,实现不占用鼠标的后台运行。这在手动运行时非常实用,让你能一边运行脚本一边用电脑干自己的事。不过我实现的全自动化能够利用夜间等电脑空闲时间运行,因此不需要用到这个功能。
BetterGI 支持命令行启动,这极大地方便了全自动化脚本的实现。
MaaEnd 与 BetterGI 类似,但更加小巧。它没有详细的使用说明,但基本开箱即用,无需复杂配置。功能虽不如 BetterGI 丰富,但也非常齐全,各类日常基本都能自动完成。
两个工具都在 GitHub 上开源,如此强大的免费工具,离不开大量为爱发电的贡献者。赞美开源精神!
在游戏外调整游戏设置
我在使用这两款工具的时候遇到了一个让我很不爽的问题。两款工具都是基于图像识别实现的,因此强制要求游戏窗口的分辨率为 16:9,然而我的电脑是 16:10,现在很多笔记本也都是这个比例。所以我每次使用工具,还要先手动在游戏内调整窗口分辨率(MaaEnd 有游戏外调整窗口大小的功能),游玩的时候又要调回去。
此外,工具可能还会要求与游玩习惯不同的鼠标灵敏度,而我也希望在工具运行时调低游戏画质,以减少资源占用和电量消耗。
这一系列麻烦都指向了同一个问题:如何在游戏外调整游戏设置。我想在游玩和工具运行时使用两套完全不同的游戏设置,并希望有一个工具能够很方便地实现这一点。
不过我搜索了网上的资源,并没有找到相关功能的完整实现。既然没有,那就自己搓一个吧!于是我分别开发了两个游戏的外部自动化配置命令行工具。
如果你也有相关需求,欢迎使用我开发的工具。使用方法非常简单,只需要定义好你想要的配置预设,就能在游戏外通过一行命令自动调整游戏设置。
我推荐你创建两套预设,一套用于平时游玩,一套用于运行脚本。运行脚本时推荐把画质调到最低,并采用尽量小的窗口(BetterGI 的最低要求为 1920×1080,MaaEnd 则可以使用 1280×720)。
就在本文章写成后不久,MaaEnd 就加入了在游戏运行前自动调整部分游戏设置的功能,在某种程度上可以替代 endfield-config-cli 工具。不过后者的配置自由度还是无法比拟的。
虚拟显示器
还记得我最开始的目标吗?实现彻底的全自动化。我希望在一段闲置的时间里,电脑自动开机并执行相关脚本。这其中有一个很关键的问题需要解决:运行的全过程中,显示器是关闭的。显示器关闭时,虽然游戏的渲染计算一般不会停止,但是显示引擎的画面合成与输出通常会被暂停。这意味着基于屏幕截图与图像识别的 BetterGI 和 MaaEnd 都将无法工作,屏幕录制也无法实现。(除非你使用台式机,并且使用 HDMI 数据线连接显示器)
要解决这个问题,如果你不介意的话,让显示器保持打开状态当然是最省事的选择。不过我可不希望大半夜的我的电脑突然亮起然后自己动起来。
另一种选择,则是使用虚拟显示器,让系统认为电脑仍然连接着显示器,从而照常渲染系统画面。
虚拟显示器的实现方式主要有两种:使用“显卡欺骗器”硬件,或者安装虚拟驱动软件。
“显卡欺骗器”是一个类似 U 盘的小设备,插入电脑的 HDMI 或 DP 接口后,会让操作系统以为连接了一个显示器。它的优点是即插即用,且兼容性和稳定性高。缺点是需要额外购买硬件(便宜的也只要几块钱),且分辨率、刷新率等显示器规格由硬件本身决定,无法自由调整。
虚拟显示驱动则可以直接在操作系统内核中逻辑模拟一个显示器,这也是我选择和推荐的方案。优点是零成本、可随时修改分辨率和刷新率。缺点是需要进行一系列配置,不过坑我都替你踩了一遍了,跟着我的操作一步步来就很简单了。
我选择的是 Virtual Display Driver (VDD) 这一开源软件,你可以直接下载它 最新的 Release 版本 。
解压后打开 VDD Control.exe 即可通过直观的图形化界面添加或删除虚拟显示驱动。你还可以在 Dependencies\vdd_settings.xml 中设置虚拟显示器数量,并分别调整它们的分辨率、刷新率等参数。一般创建一个虚拟显示器即可,分辨率推荐 1920×1080,刷新率推荐 30 Hz,这样可以把虚拟显示器的资源占用降到最低。(BetterGI 的屏幕识别要求窗口大小至少为 1920×1080)
点击 Install Driver 下载驱动后,你的屏幕应该会伸个懒腰,然后虚拟显示器就加载好了。你可以打开显示器设置界面,此时应该就能看到多个显示器了。
你可以设置三种多显示器模式:
- 仅在某块屏幕上显示:顾名思义,只显示指定的一块屏幕。当你不需要虚拟显示器时,选择仅在主屏上显示即可。不要轻易尝试仅在副屏上显示,那样主屏会全黑,导致你完全无法操作。不过试试也没事,因为 Windows 有保护机制:切换设置后,必须在一段时间内选择“保留更改”,否则会自动恢复。万一真的不小心切到了副屏,记住一个快捷键:
Win + P,它会展开显示器模式的选择,按上下键加回车就能切换。 - 复制:多块屏幕显示一样的画面。不推荐日常使用这个模式。虽然它看上去和只在主屏上显示没有区别,也只会占用少量额外的显存带宽,但可能引发一些问题,例如电脑启动后 Wallpaper 报错,或者电脑自动熄屏后持续黑屏无法恢复。不过在脚本运行、主屏关闭的情况下,我还是推荐这个模式——这样你在运行过程中打开主屏时,画面也会同步显示,而不是仍只渲染在虚拟显示器上。
- 扩展:多块屏幕构成扩展桌面。如果你要拿一块真实的显示器做副屏,就选这个模式。但你用的是虚拟显示器,所以它不仅毫无用处,还会让你把鼠标移动到一块“不存在的屏幕”上,甚至软件可能在这块“不存在的屏幕”上打开,导致你根本无法操作它。
如果你想设置虚拟显示器的缩放比例、显示方向(应该没有人会动这个吧)等,调成非复制的任意模式,你就能分别选择要调整的显示器(当前选择的显示器会变蓝)来更改其基础设置。你还可以进入高级显示器设置来调整刷新率。
这三种多显示器模式也可以通过系统自带的命令行工具 DisplaySwitch.exe(实际上放在 C:\Windows\System32)来切换:
DisplaySwitch /internalDisplaySwitch /cloneDisplaySwitch /extendDisplaySwitch /external在命令行执行程序时通常可以省略后缀名。
四个命令分别对应仅电脑屏幕、复制、扩展、仅第二屏幕。
虚拟扬声器
本节和下一节都面向想要给脚本执行全过程进行录屏的读者,如果你不需要此功能,可以直接跳过。
你应该不希望脚本运行的时候游戏会发出声音吧。正常情况下直接静音即可。不过假如你和我一样想要把脚本运行的全过程录屏,并且还希望录屏有声音,就又出现一个问题了:如何在电脑不发出声音的情况下让录屏有声音。
对比我们刚刚解决的问题:如何在电脑不显示画面的情况下让录屏 / 截图有画面。是不是很类似?同样的,我们只需安装一个虚拟扬声器,也就是让电脑认为连接了一个音频输出设备,然后把所有声音都输出到那个设备上即可。
VDD 其实内置了虚拟音频驱动的功能,但是我反复测试都安装不成功。最终我决定使用一款主流的免费虚拟声卡软件 VB-Cable 。
直接下载压缩包,解压后打开 VBCABLE_Setup_x64.exe,点击 Install Driver 按钮就可一键安装虚拟音频驱动。安装后一定要重启才能生效。
然后在音量合成器设置中,你应该就能看到三个新增设备:
- 输出设备
CABLE Input:即我们需要用到的虚拟扬声器,标准 2 声道。 - 输出设备
CABLE In 16ch:最多支持 16 声道,一般只有音频工程师、音乐制作人等专业人士才需要使用。 - 输入设备
CABLE Output:即一个虚拟麦克风,可以让软件从它那里接收声音作为输入。
CABLE Output 这个虚拟麦克风还能玩出些花样。例如和朋友语音通话时,你想把一首音乐直接播给对方听,就可以把音频输入设备调成 CABLE Output,再在音乐软件中把音乐输出到 CABLE Input,声音就会通过 CABLE 内部管道从 CABLE Output 流出,直接播给对方。
不过语音软件往往只允许选择一个音频输入设备,如果你想在播放音乐的同时自己也说话,就需要用到音频混合等高级功能,这时就要用到同一家公司 VB-Audio 出品的专业虚拟音频软件 Voicemeeter 或更强大的 Voicemeeter Banana ,当然它们的配置也更复杂。
扯远了。回到我们最开始的问题,只需要把输出设备调整为 CABLE Input,就能实现把音频输出到虚拟扬声器中而不实际播放,同时录屏软件也能正常录到音频了。
通常你不需要这些虚拟设备时,不选择它即可。你要是实在觉得它们碍眼(比如我就这么觉得),可以使用下面的命令禁用这些虚拟设备,让它们不在设备选择中出现(需要管理员权限)。
Get-PnpDevice -Class Display | Where-Object { $_.FriendlyName -like '*Virtual Display Driver*' } | Disable-PnpDevice -Confirm:$false
Get-PnpDevice -Class Media | Where-Object { $_.FriendlyName -like '*CABLE*' } | Disable-PnpDevice -Confirm:$false其中 -Class 参数用于选择设备类型。-like 配合通配符 * 进行模糊匹配(只要名字里有这几个字就会被选中)。Disable 改成 Enable 即可重新启用。
使用 obs-express-rs 实现自动录制
想要录屏,首先得选一款合适的录屏工具。N卡录屏虽占用最低,但配置少,无法录制虚拟显示器,也没有命令行启动方式,只能靠快捷键实现自动启动,不够稳定。我最终选择了一款名为 obs-express-rs 的开源工具。
obs-express(仓库已归档)是一款基于 libobs(OBS Studio 核心)的极简命令行录制工具。obs-express-rs 则是 obs-express 使用 Rust 重写的版本,是它更现代的继任者。
下载其最新的 Release 版本 ,解压到合适文件夹,即可在命令行中调用 obs-express.exe 进行录屏。其 README 文件中有详细的使用说明。不看也没事,我后面的完整脚本里有现成的录屏命令。
NirCmd 命令行工具
NirCmd 是一款免费轻量的 Windows 命令行工具。它的核心特点是:通过简单的命令行指令就能执行大量系统级操作,例如系统控制、文件操作、窗口管理等。
这使得我们能够轻松地把 NirCmd 组合进自动化脚本中,替我们实现模拟按键、设置音频输出设备、休眠和睡眠等操作。
NirCmd 的官网访问不稳定,你可以直接点击我找到的 镜像链接 下载。
下载后解压安装包,你会看到三个文件:
NirCmd.chm:软件的详细使用说明(英文)。nircmd.exe:软件主程序,在命令行中调用该程序并附加参数即可执行操作。你可以自行查看说明或者搜索各种操作的指令。nircmdc.exe:软件的控制台版本(Console Version),适合编写自动化脚本。其核心功能与nircmd.exe完全一致,但是错误信息会直接输出到命令行窗口而不是弹窗提示,且会返回ErrorLevel值,便于脚本判断命令是否成功。
每次选择两个 exe 文件中的任意一个运行即可。你可以把文件所在目录添加到系统 PATH 环境变量中,或者直接将 exe 文件放在系统 PATH 环境变量已有的目录里,例如 C:\Windows(需要管理员权限)。这样就能不带路径直接在终端中运行了。例如你可以试试:
nircmd monitor off它就能直接关闭你的显示器。
编写 PowerShell 脚本串联整个流程
准备工作已经全部做好了,接下来就是编写 PowerShell 脚本,把整个自动化流程全部串联起来。这也是最关键的部分。(虽然准备过程貌似还更加繁琐)
自动化流程
我的设计是这样的,把脚本分成三部分:
END.ps1:进行初始化配置并运行 MaaEnd。BGI.ps1:运行 BetterGI 一条龙。finish.ps1:进行收尾配置并休眠电脑。
流程只需启动 END.ps1,然后利用 MaaEnd 中的自定义程序功能,在终末地日常执行完毕后依次运行 BGI.ps1 与 finish.ps1。注意,这里要运行的程序不能直接选 .ps1 脚本,因为系统默认不会把它当作脚本执行。要选择 C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe,然后参数填写 -NoProfile -ExecutionPolicy Bypass -File "脚本路径"。
其中 -NoProfile 参数即不加载配置文件(就是你每次启动 PowerShell 会显示的“加载个人及系统配置文件用了 xxx 毫秒。”),可以省去加载耗时(虽然好像也没必要省);-ExecutionPolicy Bypass 即绕过执行策略,可以临时允许当前会话执行任何脚本。这两个参数在运行自动化脚本时一般都会加上。
脚本分成三部分的好处在于,我既可以手动运行独立模块 BGI.ps1 来执行原神一条龙,也可以在 MaaEnd 中不选择 BGI.ps1,让自动化流程只执行终末地日常。
下面放出三个脚本的具体内容,如果你只需要自动运行一个游戏的日常,或者还想运行别的游戏的日常,可以根据自己的需求选取其中需要的部分。请务必将其中各程序的路径改为你自己电脑上的真实路径,MaaEnd 快捷键等也需要根据你的实际情况进行配置。
代码由 AI 协同编写。
# 管理员权限检查与提权if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Start-Process powershell.exe -Verb RunAs -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`""; exit }
# 设置控制台输出编码chcp 65001 | Out-Null[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# 启用虚拟显示设备和虚拟音频设备Get-PnpDevice -Class Display | Where-Object { $_.FriendlyName -like '*Virtual Display Driver*' } | Enable-PnpDevice -Confirm:$falseGet-PnpDevice -Class Media | Where-Object { $_.FriendlyName -like '*CABLE*' } | Enable-PnpDevice -Confirm:$false
# 设置多显示器模式为复制DisplaySwitch /clone
# 使用 NirCmd 设置音频输出为虚拟扬声器nircmdc setdefaultsounddevice "CABLE Input"
# 设置窗口标题并等待(用于后续脚本关闭该窗口以终止录屏)$host.UI.RawUI.WindowTitle = "END"Start-Sleep -Seconds 5
# Win32 API 定义if (-not ('K' -as [type])) { Add-Type -TypeDefinition @'using System;using System.Text;using System.Runtime.InteropServices;public class K{public delegate bool CB(IntPtr h,IntPtr l);[DllImport("user32.dll")]public static extern void keybd_event(byte vk,byte scan,uint flags,UIntPtr extra);[DllImport("user32.dll")]public static extern bool EnumWindows(CB cb,IntPtr l);[DllImport("user32.dll")]public static extern uint GetWindowThreadProcessId(IntPtr h,out uint pid);[DllImport("user32.dll")]public static extern bool IsWindowVisible(IntPtr h);[DllImport("user32.dll",CharSet=CharSet.Unicode)]public static extern int GetWindowText(IntPtr h,StringBuilder s,int n);[DllImport("user32.dll")]public static extern bool SetForegroundWindow(IntPtr h);[DllImport("user32.dll")]public static extern bool SetWindowPos(IntPtr h,IntPtr a,int x,int y,int cx,int cy,uint f);}'@ }
# 开始录屏$timestamp = Get-Date -Format "yyyyMMdd_HHmmss" # 添加时间戳$outputFile = "路径\名称_$timestamp.mp4" # 填写录屏文件的名称和存放路径$psi = New-Object System.Diagnostics.ProcessStartInfo$psi.FileName = "路径\obs-express.exe" # 填写 obs-express.exe 的路径$psi.Arguments = "--output `"$outputFile`" --hw-accel --speaker default" # 启用硬件加速编码并录制声音$psi.RedirectStandardInput = $true$psi.UseShellExecute = $false$script:obsProc = [System.Diagnostics.Process]::Start($psi)Start-Sleep -Seconds 5
# 强杀可能残留的终末地进程Get-Process -Name Endfield -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinueStart-Sleep -Seconds 3
# 切换终末地配置python "路径\endfield-config.py" --config 执行脚本 # 填写 endfield-config.py 的路径和配置预设名
# 按 Esc 关闭 Win11 开始菜单(此处不能使用 NirCmd)# 由于休眠电脑前一般会先打开开始菜单,唤醒后开始菜单也会处于打开状态,需要先关闭以执行后续操作[K]::keybd_event(0x1B,0,0,[UIntPtr]::Zero)Start-Sleep -Milliseconds 50[K]::keybd_event(0x1B,0,2,[UIntPtr]::Zero)Start-Sleep -Milliseconds 300
# 重启 MaaEnd 并找到其窗口,使用 NirCmd 按下快捷键开始任务Get-Process -Name MaaEnd -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinueStart-Process "路径\MaaEnd.exe" # 填写 MaaEnd.exe 的路径$h=New-Object System.Collections.ArrayList;$f=$falsefor($i=0;$i-lt120;$i++){$h.Clear();$cb=[K+CB]{param($w,$l)$x=[uint32]0;[void][K]::GetWindowThreadProcessId($w,[ref]$x);$sb=New-Object System.Text.StringBuilder 256;[void][K]::GetWindowText($w,$sb,256);if([K]::IsWindowVisible($w)-and($sb.ToString()-match"MaaEnd")){[void]$h.Add($w)};return $true};[void][K]::EnumWindows($cb,[IntPtr]::Zero);if($h.Count-gt0){$f=$true;break};Start-Sleep -Milliseconds 500}if($f){[void][K]::SetWindowPos($h[0],[IntPtr](-1),0,0,0,0,0x13);[void][K]::SetWindowPos($h[0],[IntPtr](-2),0,0,0,0,0x13);[void][K]::SetForegroundWindow($h[0]);Start-Sleep -Milliseconds 5000;[void][K]::SetWindowPos($h[0],[IntPtr](-1),0,0,0,0,0x13);[void][K]::SetWindowPos($h[0],[IntPtr](-2),0,0,0,0,0x13);[void][K]::SetForegroundWindow($h[0]);Start-Sleep -Milliseconds 200;nircmdc sendkeypress f10} # 填写你的 MaaEnd 开始任务快捷键
# 等待终末地进程出现后置顶其窗口,防止被其它窗口遮盖# 在 BetterGI 设置中开启“游戏失去焦点的时候,强制恢复激活游戏窗口”以实现类似的效果$p = $null; for ($i = 0; $i -lt 240; $i++) { $p = Get-Process -Name Endfield -ErrorAction SilentlyContinue | Select-Object -First 1; if ($p) { break }; Start-Sleep -Milliseconds 500 }if ($p) { $tp = $p.Id $h = New-Object System.Collections.ArrayList; $f = $false for ($i = 0; $i -lt 240; $i++) { $h.Clear(); $cb = [K+CB]{ param($w, $l) $x = [uint32]0; [void][K]::GetWindowThreadProcessId($w, [ref]$x); if ($x -eq $script:tp -and [K]::IsWindowVisible($w)) { [void]$h.Add($w) }; return $true }; [void][K]::EnumWindows($cb, [IntPtr]::Zero); if ($h.Count -gt 0) { $f = $true; break }; Start-Sleep -Milliseconds 500 } if ($f) { foreach ($w in $h) { [void][K]::SetWindowPos($w, [IntPtr](-1), 0, 0, 0, 0, 0x13) } }}
# 保底:等待 30 分钟后,检查原神是否已在运行Start-Sleep -Seconds 1800if (-not (Get-Process -Name Yuanshen,GenshinImpact -ErrorAction SilentlyContinue)) { # 若原神未运行:说明流程未推进到原神阶段,关闭终末地并执行后续脚本 Get-Process -Name Endfield -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue Start-Process powershell.exe -Wait -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"路径\BGI.ps1`"" # 填写 BGI.ps1 的路径 Start-Process powershell.exe -Wait -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"路径\finish.ps1`"" # 填写 finish.ps1 的路径}
# 再等待至多 60 分钟后关闭录屏Start-Sleep -Seconds 3600# 管理员权限检查与提权if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Start-Process powershell.exe -Verb RunAs -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`""; exit }
# 设置控制台输出编码chcp 65001 | Out-Null[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# 切换原神配置python "路径\genshin-config.py" --config 执行脚本 # 填写 genshin-config.py 的路径和配置预设名
# 重启 BetterGI 并启动一条龙Stop-Process -Name "BetterGI" -Force -ErrorAction SilentlyContinueStart-Process -FilePath "路径\BetterGI.exe" -ArgumentList "--startOneDragon" # 填写 BetterGI.exe 的路径Start-Sleep -Seconds 12
# 等待游戏关闭后切换原神配置python "路径\genshin-config.py" --config 游玩 --wait-game # 填写 genshin-config.py 的路径和配置预设名# 管理员权限检查与提权if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Start-Process powershell.exe -Verb RunAs -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`""; exit }
# 设置控制台输出编码chcp 65001 | Out-Null[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# 切换终末地配置python "路径\endfield-config.py" --config 游玩 # 填写 endfield-config.py 的路径和配置预设名
# 终止 MaaEndGet-Process -Name MaaEnd -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
# 终止录屏窗口 ENDGet-Process -Name powershell | Where-Object { $_.MainWindowTitle -eq "END" } | ForEach-Object { $_.CloseMainWindow() }Start-Sleep -Seconds 3
# 使用 NirCmd 设置音频输出为扬声器nircmdc setdefaultsounddevice "扬声器" # 填写你的默认音频输出设备名
# 禁用虚拟显示设备和虚拟音频设备(若不需要,可删除这部分)Get-PnpDevice -Class Display | Where-Object { $_.FriendlyName -like '*Virtual Display Driver*' } | Disable-PnpDevice -Confirm:$falseGet-PnpDevice -Class Media | Where-Object { $_.FriendlyName -like '*CABLE*' } | Disable-PnpDevice -Confirm:$falseStart-Sleep -Seconds 5
# nircmd standby # 睡眠
nircmd hibernate # 休眠中文编码问题
在编写 PowerShell 脚本时,中文编码是一个很大的问题。我总结了一下,你只需要做好两件事即可。
- 以 UTF-8 with BOM 保存:这解决的是脚本文件中的中文问题。PowerShell 默认按 GBK 编码解析 UTF-8 编码的文件,导致中文乱码。BOM 相当于一个标识,告诉读取文件的程序“这是一个 UTF-8 编码的文件”,从而避免编码解析错误。直接用 GBK 保存虽然也能临时解决问题,但 GBK 文件的兼容性极差,非常不推荐。VSCode 右下角会显示文件编码,直接点击它就能很方便地进行修改了。
- 设置好控制台输出编码:这解决的是外部程序输出的问题。脚本会输出
genshin-config.py等外部程序的运行输出结果,如果控制台输出编码与其不统一,就会导致输出乱码。控制台输出编码默认为 GBK,而外部程序的输出编码通常为 UTF-8,所以需要使用[Console]::OutputEncoding = [System.Text.Encoding]::UTF8进行统一。注意这行代码要放在管理员权限检查与提权的代码之后,因为提权后会打开新的窗口。
自动生成任务执行概况
自动脚本运行结束后,如何知道运行是否正常呢?查看录屏当然是一种简单直观的方式,但不够快速。为此,我又专门编写了一个 PowerShell 脚本,通过读取 BetterGI、MaaEnd 与原神游戏的运行日志,总结出脚本的执行情况。
# ========== 任务执行概况 ==========# 独立脚本:直接运行即可生成并输出今日任务概况
# 共享读取日志(文件被进程占用时也能读)function Read-ShareLines($path) { if (-not (Test-Path $path)) { return } $fs = $null try { $fs = [System.IO.File]::Open($path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite) $sr = New-Object System.IO.StreamReader($fs, [System.Text.Encoding]::UTF8) while (($l = $sr.ReadLine()) -ne $null) { $l } } catch { } finally { if ($sr) { $sr.Dispose() }; if ($fs) { $fs.Dispose() } }}# 路径配置$runDate = Get-Date$maaDir = "路径\MaaEnd\debug\cpp-algo\debug" # 填写 MaaEnd 的路径$maaLog = "$maaDir\maafw.log" # MaaEnd 当前日志$maaBak = "$maaDir\maafw.bak.*.log" # MaaEnd 历史备份日志$bgiLog = "路径\BetterGI\log\better-genshin-impact$($runDate.ToString('yyyyMMdd')).log" # 填写 BetterGI 的路径$genshinLog = "C:\Users\用户名\AppData\LocalLow\miHoYo\原神\LocalLog.log" # 填写原神游戏日志的路径,用于网络断线检测$repDir = "路径" # 填写任务概况文件存放的路径function Fmt-Dur([timespan]$d) { if (-not $d) { return "未知" } $h = [Math]::Floor($d.TotalHours); $m = $d.Minutes if ($h -gt 0) { return "$h 小时 $m 分" } return "$m 分"}
# --- MaaEnd:任务事件 + 时长(数据源 = 最新含任务的 maafw.bak,无则当前 maafw.log) ---$maaSrc = $null$cand = @()if (Test-Path $maaLog) { $cand += $maaLog }$bak = Get-ChildItem $maaBak -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1if ($bak) { $cand += $bak.FullName }foreach ($c in $cand) { $has = $false foreach ($l in Read-ShareLines $c) { if ($l -match 'msg=Tasker\.Task\.(\w+)\].*"entry":"([^"]+)"' -and $Matches[2] -notmatch '^(MXU_|MaaTaskerPostStop)') { $has = $true; break } } if ($has) { $maaSrc = $c; break }}$maaTasks = @()$maaErrs = @()$maaEnds = @()$today = (Get-Date).Dateif ($maaSrc) { $evs = @() foreach ($l in Read-ShareLines $maaSrc) { $et = $null if ($l -match '^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\]') { $et = [datetime]::ParseExact($Matches[1], 'yyyy-MM-dd HH:mm:ss.fff', $null) } if ($l -match 'Parent process exited') { if (-not $et -or $et.Date -eq $today) { $maaErrs += [pscustomobject]@{ ts = $et; msg = '父进程(MaaEnd.exe)退出,cpp-algo 被关闭,任务中断' } } if ($et) { $maaEnds += [pscustomobject]@{ ts = $et; type = 'kill' } } } elseif ($l -match 'handle_shut_down_request') { if ($et) { $maaEnds += [pscustomobject]@{ ts = $et; type = 'shutdown' } } }
if ($l -match 'msg=Tasker\.Task\.(\w+)\].*"entry":"([^"]+)"') { $evtype = $Matches[1]; $entry = $Matches[2] if ($entry -match '^(MXU_|MaaTaskerPostStop)') { continue } if ($l -match '^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\]') { $ts = [datetime]::ParseExact($Matches[1], 'yyyy-MM-dd HH:mm:ss.fff', $null) $evs += [pscustomobject]@{ ev = $evtype; n = $entry; t = $ts } } } } foreach ($g in ($evs | Group-Object n)) { $s = $null foreach ($it in ($g.Group | Sort-Object t)) { if ($it.ev -eq 'Starting') { $s = $it.t } elseif ($it.ev -eq 'Succeeded' -and $s) { $maaTasks += [pscustomobject]@{ n = $g.Name; s = $s; e = $it.t; ok = $true }; $s = $null } } if ($s) { $maaTasks += [pscustomobject]@{ n = $g.Name; s = $s; e = $null; ok = $false } } } $maaTasks = @($maaTasks | Sort-Object s) $maaTasks = @($maaTasks | Where-Object { $_.s.Date -eq $today })}$lastT = $maaTasks | Sort-Object s | Select-Object -Last 1if ($lastT -and -not $lastT.ok) { $nxEnd = @($maaEnds | Where-Object { $_.ts -gt $lastT.s } | Sort-Object ts | Select-Object -First 1) if ($nxEnd -and $nxEnd.type -eq 'shutdown') { $lastNodeVerb = $null foreach ($l in Read-ShareLines $maaSrc) { if ($l -match '^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\].*msg=Node\.\w+\.(\w+)\]') { $nts = [datetime]::ParseExact($Matches[1], 'yyyy-MM-dd HH:mm:ss.fff', $null) if ($nts -gt $lastT.s -and $nts -lt $nxEnd.ts) { $lastNodeVerb = $Matches[2] } } } if ($lastNodeVerb -eq 'Succeeded') { $lastT.ok = $true $lastT.e = $nxEnd.ts } }}$maaDur = $nullif ($maaTasks.Count -gt 0) { $maaS = $maaTasks[0].s $lastOk = $maaTasks | Where-Object { $_.e } | Select-Object -Last 1 $maaE = if ($lastOk) { $lastOk.e } else { $maaTasks[-1].s } $maaDur = if ($maaS -and $maaE) { $maaE - $maaS } else { $null }}$nameMap = @{ 'VisitFriendsMain'='拜访好友'; 'DijiangRewards'='基建任务'; 'CreditShoppingMain'='信用点购物';'SellProductSchedule'='售卖商品'; 'AutoStockpileMain'='自动囤货'; 'AutoStockStapleSchedule'='购买稳定物资';'AutoSellMain'='售卖弹性物资'; 'AutoUseSpMedicationEntry'='应急理智加强剂'; 'AutoEssenceMain'='基质刷取'; 'DailyRewardStart'='日常奖励领取'}
# --- BetterGI:一条龙 + 配置组任务明细 + 路线/报错 + 会话时长 ---function Complete-Task($t) { if ($t.hasEnd -and -not ($t.cancelled -and $t.routes -eq 0)) { $t.ok = $true } else { $t.ok = $false $t.reason = if ($t.cancelled) { '被整轮取消,未执行完成' } else { '未执行到任务结束(被中断)' } }}$bLast = $null$sessStart = $null; $oneEndTs = $null$rStart = 0; $rDone = 0$oneTasks = @(); $cur = $null$dailyReward = $null$pendErr = $falseif (Test-Path $bgiLog) { foreach ($l in Read-ShareLines $bgiLog) { if ($l -match '^\[(\d{2}:\d{2}:\d{2}\.\d{3})\]') { $lastTs = [datetime]::ParseExact($Matches[1], 'HH:mm:ss.fff', $null) $bLast = $lastTs } if ($l -match '检查每日奖励结果:(.+)') { $dailyReward = $Matches[1].Trim(); continue } elseif (-not $sessStart -and $l -match '启用(?:一条龙|配置组)任务的数量: (\d+)') { $sessStart = $lastTs } $newCur = $null if ($l -match '一条龙任务执行: (\d+)/(\d+)') { $newCur = @{ kind = '内置'; idx = [int]$Matches[1]; total = [int]$Matches[2]; name = ""; start = $lastTs; end = $null; hasEnd = $false; cancelled = $false; routes = 0; errs = @(); ok = $false; reason = "" } } elseif ($l -match '配置组任务执行: (\d+)/(\d+)') { $newCur = @{ kind = '配置组'; idx = [int]$Matches[1]; total = [int]$Matches[2]; name = ""; start = $lastTs; end = $null; hasEnd = $false; cancelled = $false; routes = 0; errs = @(); ok = $false; reason = "" } } elseif ($l -match '一条龙和配置组任务结束') { $oneEndTs = $lastTs if ($cur) { $cur.end = $lastTs; Complete-Task $cur; $oneTasks += $cur; $cur = $null } } if ($newCur) { if ($cur) { $cur.end = $lastTs; Complete-Task $cur; $oneTasks += $cur } $cur = $newCur $pendErr = $false continue } if (-not $cur) { continue } if (-not $cur.name) { if ($cur.kind -eq '配置组' -and $l -match '配置组 "(.+?)" 加载完成') { $cur.name = $Matches[1] } elseif ($l -match '→ "(.+?)" 开始') { $cur.name = $Matches[1] } elseif ($l -match '^(.+?)[::]"') { $nn = $Matches[1].Trim() if ($nn -notmatch '^(切换到区域|开始传送|点击候选列表|识别到的队伍角色|对话选项|前往合成台)' -and $nn -notmatch '(上限|出错|失败|异常|超时)$') { $cur.name = $nn } } elseif ($l -match '→ "(.+?)"') { $nn = $Matches[1].TrimEnd(',', ',', '。') if ($nn -notmatch '^任务(启动|结束)') { $cur.name = $nn } } } if ($l -match '→ "任务结束"') { $cur.hasEnd = $true } elseif ($l -match '任务被取消,退出执行') { $cur.cancelled = $true } if ($l -match '开始执行任务: ') { $rStart++; $cur.routes++ } elseif ($l -match '任务完成并保存进度: ') { $rDone++ } $errText = $null if ($l -match '执行脚本时发生异常[::]?\s*(.*)') { $errText = if ($Matches[1].Trim()) { $Matches[1].Trim() } else { '执行脚本时发生异常' } } elseif ($l -match '执行异常[::](.+)') { $errText = $Matches[1].Trim() } elseif ($l -match '此路线出现3次卡死,重试一次路线或放弃此路线!') { $errText = '此路线出现3次卡死,重试一次路线或放弃此路线!' } elseif ($l -match '传送失败') { $errText = '传送失败' } elseif ($l -match '任务被取消,退出执行') { $errText = '任务被取消,退出执行' } elseif ($l -match '\[ERR\]') { $pendErr = $true } elseif ($pendErr) { if ($l -notmatch '^\s|^\[|^at |^BetterGenshinImpact|^Error:|^---|^$' -and $l -notmatch '无需重新启动') { $errText = $l } $pendErr = $false } if ($errText) { $errText = $errText.Trim().TrimStart([char]0x25B6).Trim() if ($errText.Length -gt 60) { $errText = $errText.Substring(0, 60) + "…" } $dup = $false foreach ($ee in $cur.errs) { if ($ee.text -eq $errText) { $dup = $true; break } } if (-not $dup) { $cur.errs += @{ ts = $lastTs; text = $errText } } } } if ($cur) { $cur.end = if ($cur.hasEnd) { $bLast } else { $null } Complete-Task $cur $oneTasks += $cur }}$errs = @()foreach ($t in $oneTasks) { foreach ($e in $t.errs) { $errs += [pscustomobject]@{ task = $t.name; ts = $e.ts; text = $e.text } }}$oneFinal = @()foreach ($t in $oneTasks) { $nm = if ($t.name) { $t.name } else { "任务 $($t.idx)" } $oneFinal += [pscustomobject]@{ start = $t.start; end = $t.end; name = $nm; ok = $t.ok; reason = $t.reason }}$bgiDur = if ($sessStart -and $oneEndTs) { $oneEndTs - $sessStart } elseif ($sessStart -and $bLast) { $bLast - $sessStart } else { $null }
# --- 原神:网络异常检测 ---$gsErr = @()if (Test-Path $genshinLog) { $today = (Get-Date).Date $evs = @() foreach ($l in Read-ShareLines $genshinLog) { if ($l -notmatch 'NETWORK (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})') { continue } $ts = [datetime]::ParseExact($Matches[1], 'yyyy-MM-dd HH:mm:ss', $null) if ($ts.Date -ne $today) { continue } if ($l -match 'EnetPingTimeout') { $k = 'E' } elseif ($l -match 'Disconnect token=\d+ reason=(\d+)') { $k = "D$($Matches[1])" } elseif ($l -match 'reason: EnetClientShutdown') { $k = 'S' } else { continue } $evs += [pscustomobject]@{ ts = $ts; k = $k } } $i = 0 while ($i -lt $evs.Count) { $start = $evs[$i].ts; $end = $evs[$i].ts $cEs = @(); $cDisc = $null; $cDiscTs = $null; $cShut = $false do { $e = $evs[$i]; $end = $e.ts if ($e.k -eq 'E') { $cEs += $e.ts } elseif ($e.k -like 'D*') { if (-not $cDisc) { $cDisc = $e.k.Substring(1); $cDiscTs = $e.ts } } elseif ($e.k -eq 'S') { if ($cDiscTs -and $e.ts -ge $cDiscTs) { $cShut = $true } } $i++ } while ($i -lt $evs.Count -and ($evs[$i].ts - $end).TotalSeconds -le 60) if ($cDisc) { if ($cDisc -ne '3' -or -not $cShut) { $gsErr += ("原神 {0} 网络断线(reason={1})" -f $cDiscTs.ToString('HH:mm'), $cDisc) } } else { $maxRun = 0; $run = 0; $prev = $null foreach ($t in $cEs) { if ($prev -and ($t - $prev).TotalSeconds -le 2) { $run++ } else { $run = 1 } if ($run -gt $maxRun) { $maxRun = $run } $prev = $t } if ($maxRun -ge 5) { $dur = [math]::Round(($end - $start).TotalSeconds) $gsErr += ("原神 {0} 网络持续超时({1} 秒内连发 {2} 次)" -f $start.ToString('HH:mm'), $dur, $cEs.Count) } } }}
# 输出:显示 + 写文件 + 记事本$rep = New-Object System.Collections.Generic.List[string]$rep.Add("===== 任务执行概况 =====")$total = if ($maaDur -and $bgiDur) { $maaDur + $bgiDur } elseif ($maaDur) { $maaDur } elseif ($bgiDur) { $bgiDur } else { $null }$rep.Add(("总运行时长:{0}" -f $(if ($total) { Fmt-Dur $total } else { "未知" })))if ($maaDur) { $rep.Add((" MaaEnd: {0}" -f (Fmt-Dur $maaDur))) }if ($bgiDur) { $rep.Add((" BetterGI: {0}" -f (Fmt-Dur $bgiDur))) }
$maaBad = @($maaTasks | Where-Object { -not $_.ok }).Count$oneBad = @($oneFinal | Where-Object { -not $_.ok }).Count$bgiBad = ($errs.Count -gt 0) -or ($oneBad -gt 0)if ($maaBad -gt 0 -or $oneBad -gt 0) { $verdict = "存在失败任务" }elseif ($errs.Count -gt 0 -or $maaErrs.Count -gt 0) { $verdict = "全部执行成功(存在报错)" }else { $verdict = "全部执行成功" }$rep.Add(("整体概况:{0}" -f $verdict))
$rep.Add("")$rep.Add("[MaaEnd] 任务")if ($maaTasks.Count -eq 0) { $rep.Add(" 无玩法任务执行") }else { foreach ($t in $maaTasks) { $cn = if ($nameMap.ContainsKey($t.n)) { $nameMap[$t.n] } else { $t.n } $tm = if ($t.e) { "{0} → {1}" -f $t.s.ToString('HH:mm'), $t.e.ToString('HH:mm') } else { "{0} → ?" -f $t.s.ToString('HH:mm') } $mark = if ($t.ok) { "✓" } else { "✗" } if ($t.ok) { $rep.Add((" {0} {1,-10} {2}" -f $tm, $cn, $mark)) } else { $rep.Add((" {0} {1,-10} {2}(未完成)" -f $tm, $cn, $mark)) } }}if ($maaBad -gt 0 -or $maaErrs.Count -gt 0) { $rep.Add("") $rep.Add("[MaaEnd] 报错:") foreach ($t in ($maaTasks | Where-Object { -not $_.ok })) { $cn = if ($nameMap.ContainsKey($t.n)) { $nameMap[$t.n] } else { $t.n } $reason = "未完成" foreach ($e in $maaErrs) { if ($e.ts -and $t.s -lt $e.ts) { $reason = "未完成,父进程退出中断"; break } } $rep.Add((" - {0}({1})" -f $cn, $reason)) } foreach ($e in $maaErrs) { $tstr = if ($e.ts) { $e.ts.ToString('HH:mm') } else { "?" } $rep.Add((" - {0}({1})" -f $e.msg, $tstr)) }}
$rep.Add("")$rep.Add("[BetterGI] 任务")if ($oneFinal.Count -eq 0) { $rep.Add(" 无任务执行") }else { foreach ($t in $oneFinal) { $tm = if ($t.end) { "{0} → {1}" -f $t.start.ToString('HH:mm'), $t.end.ToString('HH:mm') } else { "{0} → ?" -f $t.start.ToString('HH:mm') } $mark = if ($t.ok) { "✓" } else { "✗" } if ($t.ok) { $rep.Add((" {0} {1,-14} {2}" -f $tm, $t.name, $mark)) } else { $rep.Add((" {0} {1,-14} {2}({3})" -f $tm, $t.name, $mark, $(if ($t.reason) { $t.reason } else { "执行失败" }))) } }}$rep.Add((" 路线:开始 {0} 条,完成 {1} 条" -f $rStart, $rDone))$rep.Add("")if ($dailyReward) { $rep.Add(("每日奖励结果:{0}" -f $dailyReward)) }if ($gsErr.Count -gt 0 -and ($maaBad -gt 0 -or $oneBad -gt 0 -or $errs.Count -gt 0)) { $rep.Add("") $rep.Add("[原神] 网络异常(BetterGI 任务失败/报错的常见根因):") foreach ($e in $gsErr) { $rep.Add((" - {0}" -f $e)) }}if ($bgiBad) { $rep.Add("") $rep.Add("[BetterGI] 报错:") $shown = 0 foreach ($e in $errs) { $shown++ if ($shown -gt 8) { continue } $nm = if ($e.task) { $e.task } else { '会话' } $tstr = if ($e.ts) { $e.ts.ToString('HH:mm') } else { "?" } $rep.Add((" - {0}({1}):{2}" -f $nm, $tstr, $e.text)) } if ($errs.Count -gt 8) { $rep.Add((" - …共 {0} 条报错,其余详见 BetterGI 日志" -f $errs.Count)) }}
$repText = $rep -join "`n"New-Item -ItemType Directory -Path $repDir -Force | Out-Null$repFile = "$repDir\任务概况_$($runDate.ToString('yyyyMMdd')).txt"Set-Content -Path $repFile -Value $repText -Encoding UTF8Start-Process notepad.exe $repFile运行脚本,它就会提取最近一次 MaaEnd 和 BetterGI 的任务执行概况,并结合原神日志检测网络断线情况,将 .txt 文件输出到你指定的目录,并自动打开方便你浏览。概况文件大概是这样的:
===== 任务执行概况 =====总运行时长:1 小时 7 分 MaaEnd: 15 分 BetterGI: 52 分整体概况:全部执行成功(存在异常)
[MaaEnd] 任务 04:01 → 04:05 拜访好友 ✓ 04:05 → 04:06 基建任务 ✓ 04:06 → 04:07 信用点购物 ✓ 04:07 → 04:08 售卖商品 ✓ 04:08 → 04:09 自动囤货 ✓ 04:09 → 04:10 购买稳定物资 ✓ 04:10 → 04:11 售卖弹性物资 ✓ 04:11 → 04:11 应急理智加强剂 ✓ 04:11 → 04:14 AutoEssenceSchedule ✓ 04:14 → 04:16 日常奖励领取 ✓
[BetterGI] 任务 04:17 → 04:17 邮件 ✓ 04:17 → 04:17 前往合成台 ✓ 04:17 → 04:21 自动秘境 ✓ 04:21 → 04:22 前往冒险家协会领取奖励 ✓ 04:22 → 04:23 领取尘歌壶奖励 ✓ 04:23 → 05:08 狗粮 ✓ 路线:开始 29 条,完成 29 条
每日奖励结果:"今日奖励已领取"
[BetterGI] 报错: - 前往合成台:点击传送点后未出现交互面板,可能是传送点未激活这可以让你很方便地定位到执行失败的任务,再通过查看录屏等方式找到具体的原因。
你可以把脚本添加到 finish.ps1 的流程中:
& "$PSScriptRoot\TaskSummary.ps1" # $PSScriptRoot 表示当前脚本的目录这样电脑从休眠中唤醒后你就能直接看到任务执行的概况了。
设置 Windows 任务计划
最后一步,就是让电脑自动在设定的时间点开机并执行脚本。好消息是,Windows 自带的任务计划程序就能够方便地实现定时从睡眠 / 休眠状态(关机状态不行)中唤醒电脑并自动执行设定的文件。
事实上,我非常推荐你每天使用休眠来代替关机。与睡眠不同,休眠状态下电脑与关机一样是断电的。而相比关机,休眠的启动非常迅速,且启动后能立即恢复关闭电脑前的状态,非常方便。它的原理其实就是把内存里的运行状态保存到硬盘,然后完全断电,下次开机时再从硬盘恢复回内存。不过缺点是需要创建一个较大的 hiberfil.sys 文件来临时存放内存数据,且系统更新等操作还是必须执行关机。对于一晚上不用电脑的日常情境,休眠是绝佳的选择;如果你要很长一段时间不用电脑,还是推荐关机。
不过休眠按钮默认是隐藏的。要让它出现,只需以管理员身份打开终端,并运行:
powercfg /hibernate on即可在关机按钮的选择列表看到休眠选项。
如果你仍希望每天关机,并实现关机状态下的自动开机,仅靠 Windows 自带的任务计划程序就无法实现了,你还需要通过 BIOS 设置定时开机。不同电脑的操作不同,你可以自行搜索。
还有一个问题:如果你的电脑设置了密码,唤醒电脑后就必须要先登录才能执行脚本,从而卡在登录这一步。虽然任务计划程序中有一个“不管用户是否登录都要运行”的选项,可以在不登录的情况下直接执行脚本,但是该选项只适用于不需要显示任何图形界面的脚本(图形界面必须运行在一个已登录用户的会话中),因此无法满足我们的要求。
可见全自动执行与必须密码登录是绝对相悖的。如果你追求极致的安全,就得考虑放弃这种方案,每天手动点击运行了。不过还有一个相对折中的方案:让你的电脑从睡眠 / 休眠状态中唤醒时自动登录,而从关机状态启动时仍然需要输入密码,这也是我选择的方案。你只需要在“登录选项”设置中将“你希望 Windows 在你离开电脑多久后要求你重新登录”设置为“从不”即可。
接下来详细说说任务计划程序要怎么用。首先,点击左侧的任务计划程序库,就能看见当前已有的任务计划,其中大概已经有不少应用的自动更新程序等。
点击右侧“创建任务”,就可以进行计划任务的详细配置:
- 常规:填写任务名和描述。下方的“不管用户是否登录都要运行”讲过了,不需要选。“使用最高权限运行”最好选上(不选也没事,脚本有自动提权)。“隐藏”会让你的计划任务不在任务计划列表中显示,通常不需要选(不小心选了的话,在“查看”中选择“显示隐藏的任务”即可)。
- 触发器:计划任务的触发条件。可以添加多个。触发方式一般就用默认的“按预定计划”,然后选择计划时间和循环方式即可。
- 操作:即计划任务具体要执行的操作。一般就选默认的“启动程序”。注意这里的程序同样要选
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe,参数填写-NoProfile -ExecutionPolicy Bypass -File "脚本路径"。 - 条件:一定要勾选“唤醒计算机运行此任务”,其余配置可自行选择。
至此,日常全自动化就大功告成了!还真是不简单呢!不过别高兴太早,因为流程比较复杂,中间很容易在某个环节掉链子,最好先进行充分的测试,把小问题都修好。
然后你就可以享受每天直接解放双手,电脑就能自动清日常的雇主生活了!😄
所以说,人类的进步源于偷懒啊!