6620 字
507 行
46 分钟
如何实现原神 / 终末地日常全自动化

前言#

玩原神和终末地等游戏,每天都要抽空去完成一套流程相对重复的日常任务,繁忙时经常顾不上,玩久了也会稍觉枯燥。

好在现在已经有了 BetterGI 和 MaaEnd 这类成熟的自动化工具,能帮你实现游戏内很多操作的自动化。

不过,它们需要手动点击开始,两个工具还得分别运行一次。而且,它们基本只能处理游戏内的操作,要实现真正的全自动,还需要做一些额外的配置和操作。

我的目标是实现彻底的全自动化——每天的指定时间,电脑自动开机,自动跑完两个游戏的日常,然后自动关机。这看起来不难实现,但其中真有不少细节和坑要处理。我陆陆续续折腾了近十天,才终于完整地实现了整个流程。你可以看看我的 演示视频 。我接下来就把整个过程完整复盘一遍,重点讲讲里面的难点和坑。

BetterGI / MaaEnd 简单介绍#

BetterGI 的 官网 有详细的项目介绍、说明文档和下载方式。

BetterGI 功能强大,社区生态也非常丰富。你不仅能实现自动完成每日委托等基本日常,还能利用社区内的脚本自动完成物产采集、狗粮拾取、锄地打怪等进阶操作。

相应地,BetterGI 需要配置的内容也比较多。请务必详细阅读官网的用户手册,跟着说明一步步操作。

BetterGI 支持桌面分身启动,实现不占用鼠标的后台运行。这在手动运行时非常实用,让你能一边运行脚本一边用电脑干自己的事。不过我实现的全自动化能够利用夜间等电脑空闲时间运行,因此不需要用到这个功能。

BetterGI 支持命令行启动,这极大地方便了全自动化脚本的实现。

MaaEnd 与 BetterGI 类似,但更加小巧。它没有详细的使用说明,但基本开箱即用,无需复杂配置。功能虽不如 BetterGI 丰富,但也非常齐全,各类日常基本都能自动完成。

两个工具都在 GitHub 上开源,如此强大的免费工具,离不开大量为爱发电的贡献者。赞美开源精神!

在游戏外调整游戏设置#

我在使用这两款工具的时候遇到了一个让我很不爽的问题。两款工具都是基于图像识别实现的,因此强制要求游戏窗口的分辨率为 16:9,然而我的电脑是 16:10,现在很多笔记本也都是这个比例。所以我每次使用工具,还要先手动在游戏内调整窗口分辨率(MaaEnd 有游戏外调整窗口大小的功能),游玩的时候又要调回去。

此外,工具可能还会要求与游玩习惯不同的鼠标灵敏度,而我也希望在工具运行时调低游戏画质,以减少资源占用和电量消耗。

这一系列麻烦都指向了同一个问题:如何在游戏外调整游戏设置。我想在游玩和工具运行时使用两套完全不同的游戏设置,并希望有一个工具能够很方便地实现这一点。

不过我搜索了网上的资源,并没有找到相关功能的完整实现。既然没有,那就自己搓一个吧!于是我分别开发了两个游戏的外部自动化配置命令行工具。

Pre-rice
/
genshin-config-cli
Waiting for api.github.com...
00K
0K
0K
Waiting...

Pre-rice
/
endfield-config-cli
Waiting for api.github.com...
00K
0K
0K
Waiting...

如果你也有相关需求,欢迎使用我开发的工具。使用方法非常简单,只需要定义好你想要的配置预设,就能在游戏外通过一行命令自动调整游戏设置。

我推荐你创建两套预设,一套用于平时游玩,一套用于运行脚本。运行脚本时推荐把画质调到最低,并采用尽量小的窗口(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)来切换:

Terminal window
DisplaySwitch /internal
DisplaySwitch /clone
DisplaySwitch /extend
DisplaySwitch /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,就能实现把音频输出到虚拟扬声器中而不实际播放,同时录屏软件也能正常录到音频了。

通常你不需要这些虚拟设备时,不选择它即可。你要是实在觉得它们碍眼(比如我就这么觉得),可以使用下面的命令禁用这些虚拟设备,让它们不在设备选择中出现(需要管理员权限)。

Terminal window
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(需要管理员权限)。这样就能不带路径直接在终端中运行了。例如你可以试试:

Terminal window
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 协同编写。

END.ps1
# 管理员权限检查与提权
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:$false
Get-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 SilentlyContinue
Start-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 SilentlyContinue
Start-Process "路径\MaaEnd.exe" # 填写 MaaEnd.exe 的路径
$h=New-Object System.Collections.ArrayList;$f=$false
for($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 1800
if (-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
BGI.ps1
# 管理员权限检查与提权
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 SilentlyContinue
Start-Process -FilePath "路径\BetterGI.exe" -ArgumentList "--startOneDragon" # 填写 BetterGI.exe 的路径
Start-Sleep -Seconds 12
# 等待游戏关闭后切换原神配置
python "路径\genshin-config.py" --config 游玩 --wait-game # 填写 genshin-config.py 的路径和配置预设名
finish.ps1
# 管理员权限检查与提权
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 的路径和配置预设名
# 终止 MaaEnd
Get-Process -Name MaaEnd -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
# 终止录屏窗口 END
Get-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:$false
Get-PnpDevice -Class Media | Where-Object { $_.FriendlyName -like '*CABLE*' } | Disable-PnpDevice -Confirm:$false
Start-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 与原神游戏的运行日志,总结出脚本的执行情况。

TaskSummary.ps1
# ========== 任务执行概况 ==========
# 独立脚本:直接运行即可生成并输出今日任务概况
# 共享读取日志(文件被进程占用时也能读)
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 1
if ($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).Date
if ($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 1
if ($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 = $null
if ($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 = $false
if (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 UTF8
Start-Process notepad.exe $repFile

运行脚本,它就会提取最近一次 MaaEnd 和 BetterGI 的任务执行概况,并结合原神日志检测网络断线情况,将 .txt 文件输出到你指定的目录,并自动打开方便你浏览。概况文件大概是这样的:

任务概况_20260828.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 的流程中:

Terminal window
& "$PSScriptRoot\TaskSummary.ps1" # $PSScriptRoot 表示当前脚本的目录

这样电脑从休眠中唤醒后你就能直接看到任务执行的概况了。

设置 Windows 任务计划#

最后一步,就是让电脑自动在设定的时间点开机并执行脚本。好消息是,Windows 自带的任务计划程序就能够方便地实现定时从睡眠 / 休眠状态(关机状态不行)中唤醒电脑并自动执行设定的文件。

事实上,我非常推荐你每天使用休眠来代替关机。与睡眠不同,休眠状态下电脑与关机一样是断电的。而相比关机,休眠的启动非常迅速,且启动后能立即恢复关闭电脑前的状态,非常方便。它的原理其实就是把内存里的运行状态保存到硬盘,然后完全断电,下次开机时再从硬盘恢复回内存。不过缺点是需要创建一个较大的 hiberfil.sys 文件来临时存放内存数据,且系统更新等操作还是必须执行关机。对于一晚上不用电脑的日常情境,休眠是绝佳的选择;如果你要很长一段时间不用电脑,还是推荐关机。

不过休眠按钮默认是隐藏的。要让它出现,只需以管理员身份打开终端,并运行:

Terminal window
powercfg /hibernate on

即可在关机按钮的选择列表看到休眠选项。

如果你仍希望每天关机,并实现关机状态下的自动开机,仅靠 Windows 自带的任务计划程序就无法实现了,你还需要通过 BIOS 设置定时开机。不同电脑的操作不同,你可以自行搜索。

还有一个问题:如果你的电脑设置了密码,唤醒电脑后就必须要先登录才能执行脚本,从而卡在登录这一步。虽然任务计划程序中有一个“不管用户是否登录都要运行”的选项,可以在不登录的情况下直接执行脚本,但是该选项只适用于不需要显示任何图形界面的脚本(图形界面必须运行在一个已登录用户的会话中),因此无法满足我们的要求。

可见全自动执行与必须密码登录是绝对相悖的。如果你追求极致的安全,就得考虑放弃这种方案,每天手动点击运行了。不过还有一个相对折中的方案:让你的电脑从睡眠 / 休眠状态中唤醒时自动登录,而从关机状态启动时仍然需要输入密码,这也是我选择的方案。你只需要在“登录选项”设置中将“你希望 Windows 在你离开电脑多久后要求你重新登录”设置为“从不”即可。

接下来详细说说任务计划程序要怎么用。首先,点击左侧的任务计划程序库,就能看见当前已有的任务计划,其中大概已经有不少应用的自动更新程序等。

点击右侧“创建任务”,就可以进行计划任务的详细配置:

  • 常规:填写任务名和描述。下方的“不管用户是否登录都要运行”讲过了,不需要选。“使用最高权限运行”最好选上(不选也没事,脚本有自动提权)。“隐藏”会让你的计划任务不在任务计划列表中显示,通常不需要选(不小心选了的话,在“查看”中选择“显示隐藏的任务”即可)。
  • 触发器:计划任务的触发条件。可以添加多个。触发方式一般就用默认的“按预定计划”,然后选择计划时间和循环方式即可。
  • 操作:即计划任务具体要执行的操作。一般就选默认的“启动程序”。注意这里的程序同样要选 C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe,参数填写 -NoProfile -ExecutionPolicy Bypass -File "脚本路径"。
  • 条件:一定要勾选“唤醒计算机运行此任务”,其余配置可自行选择。

至此,日常全自动化就大功告成了!还真是不简单呢!不过别高兴太早,因为流程比较复杂,中间很容易在某个环节掉链子,最好先进行充分的测试,把小问题都修好。

然后你就可以享受每天直接解放双手,电脑就能自动清日常的雇主生活了!😄

所以说,人类的进步源于偷懒啊!