PowerShell 技能连载 - 正则表达式实战

适用于 PowerShell 5.1 及以上版本

在日常运维和数据处理中,我们经常需要从大量文本中提取特定信息、验证输入格式或批量替换内容。正则表达式(Regular Expression)是处理这类任务的利器。PowerShell 基于 .NET 的正则引擎,提供了丰富且强大的文本处理能力。

许多管理员对正则表达式望而生畏,觉得语法晦涩难懂。但实际上,掌握少数几个核心模式就能解决大部分日常工作需求。本文将从基础匹配开始,逐步深入到捕获组、替换操作和常用验证模式。

阅读更多

PowerShell 技能连载 - 本地大模型 Ollama 集成

适用于 PowerShell 7.0 及以上版本,需要安装 Ollama

前一篇我们探讨了 PowerShell 调用云端 AI API 的方式。然而在很多场景下——企业内网环境、数据合规要求、或者仅仅是不想为每一次调试付费——本地运行大语言模型是更务实的选择。Ollama 把模型下载、推理服务、REST API 打包成一条命令就能跑起来的体验,而 PowerShell 作为胶水语言,可以快速将这些能力集成到日常工作流中。

阅读更多

PowerShell 技能连载 - JSON 与 YAML 配置管理

适用于 PowerShell 7.0 及以上版本

在 DevOps 和基础设施即代码的实践中,配置文件管理是核心能力。无论是应用部署、容器编排还是 CI/CD 流水线,JSON 和 YAML 格式的配置文件无处不在。PowerShell 原生支持 JSON 的读写与转换,配合 powershell-yaml 模块也能轻松处理 YAML,包括 Kubernetes 风格的多文档格式。

本文将从实际场景出发,逐步介绍如何用 PowerShell 完成 JSON 配置读取与修改、YAML 解析、Schema 验证、模板渲染以及环境配置切换。

阅读更多

PowerShell 技能连载 - Docker 容器管理

适用于 Docker 20.10+ 及 PowerShell 7.0+

容器化部署已经从尝鲜走向了主流。无论是微服务架构还是 CI/CD 流水线,Docker 几乎无处不在。然而,日常运维中频繁手动输入 docker rundocker logs 等命令不仅效率低下,而且容易出错。PowerShell 凭借其强大的对象管道和函数封装能力,非常适合用来编排 Docker 操作——把重复性工作交给脚本,让运维人员专注于真正重要的事情。

本文将从环境检查、容器生命周期管理、批量巡检、镜像清理、日志收集五个方面,逐步展示如何用 PowerShell 高效管理 Docker 容器,最后给出一个完整的 Nginx 部署实战示例。

阅读更多

PowerShell 技能连载 - Windows 安全基线审计

适用于 Windows Server 2016+ 及 PowerShell 5.1+

很多安全事件的根源并不是零日漏洞,而是基础配置疏忽——弱密码策略、高危端口暴露、补丁长期未更新。定期执行安全基线检查是运维最基本也是最有效的防线。本文用 PowerShell 编写一套轻量审计脚本,覆盖密码策略、防火墙、RDP、补丁和服务五大维度,最终汇总为一份 HTML 审计报告。

账户与密码策略

密码策略是安全基线的第一道门。如果系统允许空密码或密码永不过期,攻击者可以轻松暴力破解。同时管理员组成员过多、Guest 账户处于启用状态,也会显著增加横向移动的风险。下面这段代码通过 net accounts 获取当前密码策略,并检查本地管理员组和 Guest 账户的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function Get-SecurityBaselineAccount {
$result = @()
$pwdPolicy = net accounts | Out-String

# 密码最短期限:防止用户改完密码后立刻改回旧密码
$result += [PSCustomObject]@{
Category = "密码策略"; Check = "密码最短期限"
Value = if ($pwdPolicy -match '密码最短期限:\s*(\d+)') { $Matches[1] + " 天" } else { "未知" }
Expected = "≥ 1 天"
}
# 密码最长期限:长期不换密码会增加凭据泄露的风险
$result += [PSCustomObject]@{
Category = "密码策略"; Check = "密码最长期限"
Value = if ($pwdPolicy -match '密码最长期限:\s*(\d+)') { $Matches[1] + " 天" } else { "未知" }
Expected = "≤ 90 天"
}
# 最小密码长度:14 位以下容易被暴力破解
$result += [PSCustomObject]@{
Category = "密码策略"; Check = "最小密码长度"
Value = if ($pwdPolicy -match '最小密码长度:\s*(\d+)') { $Matches[1] + " 字符" } else { "未知" }
Expected = "≥ 14 字符"
}
# 管理员组成员应尽量精简,避免权限滥用
$adminMembers = Get-LocalGroupMember -SID "S-1-5-32-544" -ErrorAction SilentlyContinue
$result += [PSCustomObject]@{
Category = "账户安全"; Check = "本地管理员组成员数"
Value = "$($adminMembers.Count) 个"; Expected = "尽量精简"
}
# Guest 账户启用后任何人可匿名访问系统资源
$guest = Get-LocalUser -Name "Guest" -ErrorAction SilentlyContinue
$result += [PSCustomObject]@{
Category = "账户安全"; Check = "Guest 账户状态"
Value = if ($guest.Enabled) { "已启用 (风险)" } else { "已禁用" }; Expected = "已禁用"
}
return $result
}

执行后输出类似如下结果,可以看到最小密码长度仅为 7 字符,不满足基线要求,需要重点关注:

1
2
3
4
5
6
7
Category  Check              Value        Expected
-------- ----- ----- --------
密码策略 密码最短期限 1 天 ≥ 1 天
密码策略 密码最长期限 42 天 ≤ 90 天
密码策略 最小密码长度 7 字符 ≥ 14 字符
账户安全 本地管理员组成员数 3 个 尽量精简
账户安全 Guest 账户状态 已禁用 已禁用

防火墙与高危端口检测

防火墙是系统级的网络屏障,任何一个配置文件被关闭都会让对应网络场景下的所有端口暴露。FTP(21)、Telnet(23)、SMB(445)、RDP(3389)、VNC(5800/5900)等高危端口如果监听在 0.0.0.0 上,攻击面会大幅增加。下面这段代码检查三个防火墙配置文件的启用状态,并扫描当前对外开放的高危端口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function Get-SecurityBaselineNetwork {
$result = @()

# 逐个检查域、专用、公用三个防火墙配置文件
foreach ($profile in @("Domain", "Private", "Public")) {
$fw = Get-NetFirewallProfile -Name $profile -ErrorAction SilentlyContinue
$result += [PSCustomObject]@{
Category = "防火墙"; Check = "$profile 配置文件"
Value = if ($fw.Enabled) { "已启用" } else { "已禁用 (风险)" }
Expected = "已启用"
}
}
# 扫描监听在所有网卡上的高危端口
$listeningPorts = Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue |
Where-Object { $_.LocalAddress -eq "0.0.0.0" -or $_.LocalAddress -eq "::" } |
Select-Object LocalPort, OwningProcess -Unique

$highRiskPorts = @(21, 23, 445, 3389, 5800, 5900)
$riskyPorts = $listeningPorts | Where-Object { $_.LocalPort -in $highRiskPorts }

$result += [PSCustomObject]@{
Category = "网络"; Check = "高危端口开放"
Value = if ($riskyPorts) {
"发现 $($riskyPorts.Count) 个: $($riskyPorts.LocalPort -join ', ')"
} else { "无" }
Expected = "无"
}
return $result
}

下面的执行结果显示 Public 配置文件的防火墙已关闭,同时 445(SMB)和 3389(RDP)对外开放,存在较高的被利用风险:

1
2
3
4
5
6
Category  Check           Value                 Expected
-------- ----- ----- --------
防火墙 Domain 配置文件 已启用 已启用
防火墙 Private 配置文件 已启用 已启用
防火墙 Public 配置文件 已禁用 (风险) 已启用
网络 高危端口开放 发现 2 个: 445, 3389 无

RDP 安全配置

RDP 是远程管理 Windows 服务器的常用方式,但如果不加限制就会成为暴力破解的入口。需要关注两个关键点:RDP 是否开启,以及是否启用了网络级别身份验证(NLA)。NLA 在建立连接之前就要求身份验证,能有效防御未认证的拒绝服务攻击和凭据窃取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Get-SecurityBaselineRDP {
$result = @()

# 通过注册表检查 RDP 是否启用(0 = 启用,1 = 禁用)
$rdp = Get-ItemProperty -Path "HKLM:\System\CurrentControlSet\Control\Terminal Server" `
-Name "fDenyTSConnections" -ErrorAction SilentlyContinue
$result += [PSCustomObject]@{
Category = "RDP 安全"; Check = "RDP 状态"
Value = if ($rdp.fDenyTSConnections -eq 0) { "已启用" } else { "已禁用" }
Expected = "按需启用"
}

# NLA 未启用时连接建立前不做身份验证,存在安全风险
$nla = Get-ItemProperty -Path "HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" `
-Name "UserAuthentication" -ErrorAction SilentlyContinue
$result += [PSCustomObject]@{
Category = "RDP 安全"; Check = "NLA(网络级别身份验证)"
Value = if ($nla.UserAuthentication -eq 1) { "已启用" } else { "未启用 (风险)" }
Expected = "已启用"
}
return $result
}

执行结果示例中 RDP 已启用且 NLA 也已启用,处于合规状态。如果 NLA 未启用,攻击者可在身份验证之前消耗服务器资源,建议立刻启用:

1
2
3
4
Category  Check                    Value    Expected
-------- ----- ----- --------
RDP 安全 RDP 状态 已启用 按需启用
RDP 安全 NLA(网络级别身份验证) 已启用 已启用

补丁状态检查

未及时安装补丁是导致系统被入侵的常见原因。这段代码检查最近一次补丁安装的日期、Windows Update 服务状态,以及当前有多少重要更新待安装。补丁超过 30 天未更新或存在待安装的重要更新,都说明更新策略可能存在问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function Get-SecurityBaselineUpdates {
$result = @()
# 获取最近安装的补丁,按时间降序排列
$recentUpdates = Get-HotFix | Sort-Object InstalledOn -Descending | Select-Object -First 5
$latestUpdate = $recentUpdates | Select-Object -First 1
$result += [PSCustomObject]@{
Category = "系统更新"; Check = "最新补丁日期"
Value = if ($latestUpdate.InstalledOn) {
$latestUpdate.InstalledOn.ToString("yyyy-MM-dd")
} else { "未知" }
Expected = "30 天内"
}
# 检查 Windows Update 服务是否正常运行
$wuService = Get-Service -Name wuauserv -ErrorAction SilentlyContinue
$result += [PSCustomObject]@{
Category = "系统更新"; Check = "Windows Update 服务"
Value = "$($wuService.Status)"; Expected = "Running"
}
# 通过 COM 对象查询待安装的重要更新
$updateSession = New-Object -ComObject Microsoft.Update.Session
$searchResult = $updateSession.CreateUpdateSearcher().Search(
"IsInstalled=0 and Type='Software' and AutoSelectOnWebSites=1"
)
$result += [PSCustomObject]@{
Category = "系统更新"; Check = "待安装重要更新"
Value = "$($searchResult.Updates.Count) 个"; Expected = "0 个"
}
return $result
}

以下执行结果显示还有 3 个重要更新待安装,建议尽快安排补丁窗口完成更新:

1
2
3
4
5
Category  Check               Value        Expected
-------- ----- ----- --------
系统更新 最新补丁日期 2025-03-10 30 天内
系统更新 Windows Update 服务 Running Running
系统更新 待安装重要更新 30

危险服务检查

某些服务在大多数生产环境中并不需要,反而会增大攻击面。Telnet 明文传输凭据,远程注册表允许远程修改注册表,SSDP 和 UPnP 可能被用于网络发现和反射攻击。下面逐一检查这些服务的运行状态,并统计系统级自启动项数量——自启动项过多可能意味着恶意软件的持久化驻留。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function Get-SecurityBaselineServices {
$result = @()
# 常见高危服务列表:名称 => 服务标识
$dangerousServices = @{
"Telnet" = "tlntsvr"
"远程注册表" = "RemoteRegistry"
"SSDP 发现" = "ssdpsrv"
"UPnP 设备主机" = "upnphost"
}
foreach ($svc in $dangerousServices.GetEnumerator()) {
$service = Get-Service -Name $svc.Value -ErrorAction SilentlyContinue
$result += [PSCustomObject]@{
Category = "服务检查"; Check = "$($svc.Key) ($($svc.Value))"
Value = if ($service) {
"$($service.Status), 启动类型: $($service.StartType)"
} else { "未安装" }
Expected = "未安装或已禁用"
}
}
# 系统级自启动项过多可能意味着恶意软件持久化
$startupItems = Get-CimInstance -ClassName Win32_StartupCommand |
Where-Object { $_.Location -eq "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" }
$result += [PSCustomObject]@{
Category = "启动项"; Check = "系统级自启动项"
Value = "$($startupItems.Count) 个"; Expected = "审查后保留必要项"
}
return $result
}

执行结果显示远程注册表服务处于自动启动且正在运行,在生产环境中建议禁用:

1
2
3
4
5
6
7
Category  Check                         Value                         Expected
-------- ----- ----- --------
服务检查 Telnet (tlntsvr) 未安装 未安装或已禁用
服务检查 远程注册表 (RemoteRegistry) Running, 启动类型: Automatic 未安装或已禁用
服务检查 SSDP 发现 (ssdpsrv) Stopped, 启动类型: Manual 未安装或已禁用
服务检查 UPnP 设备主机 (upnphost) Stopped, 启动类型: Manual 未安装或已禁用
启动项 系统级自启动项 4 个 审查后保留必要项

生成 HTML 审计报告

将前面所有检查函数汇总为一份带风险标记的 HTML 报告。不合规的行以红色背景高亮显示,便于快速定位问题。报告顶部汇总检查总数和风险项数量,帮助管理层一眼掌握整体安全态势。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function New-SecurityBaselineReport {
param(
[string]$OutputPath = ".\SecurityBaseline_$(Get-Date -Format 'yyyyMMdd').html"
)

# 依次调用各维度的检查函数,汇总所有结果
$allChecks = @(Get-SecurityBaselineAccount) + @(Get-SecurityBaselineNetwork) +
@(Get-SecurityBaselineRDP) +
@(Get-SecurityBaselineUpdates) + @(Get-SecurityBaselineServices)

$totalChecks = $allChecks.Count
$riskItems = ($allChecks | Where-Object {
$_.Value -match "风险|FAIL|未启用"
}).Count

$html = @"
<!DOCTYPE html><html><head><title>安全基线审计报告</title>
<style>
body{font-family:Microsoft YaHei;margin:20px}
table{border-collapse:collapse;width:100%}
th,td{border:1px solid #ddd;padding:8px;text-align:left}
th{background-color:#4CAF50;color:white}
.risk{background-color:#ffcccc}
</style></head><body>
<h1>安全基线审计报告 - $(Get-Date -Format 'yyyy-MM-dd')</h1>
<p>检查项: $totalChecks | 风险项: <span style="color:red">$riskItems</span></p>
<table><tr><th>类别</th><th>检查项</th><th>当前值</th><th>期望值</th></tr>
"@
foreach ($item in $allChecks) {
$class = if ($item.Value -match "风险|FAIL|未启用") {
' class="risk"'
} else { "" }
$html += "<tr$class><td>$($item.Category)</td><td>$($item.Check)</td>" +
"<td>$($item.Value)</td><td>$($item.Expected)</td></tr>"
}
$html += "</table></body></html>"
$html | Out-File -FilePath $OutputPath -Encoding UTF8
Write-Host "报告已生成: $OutputPath"
}

执行后在当前目录生成 HTML 文件,浏览器打开即可看到完整的审计表格。不合规的行以红色背景标出:

1
报告已生成: .\SecurityBaseline_20250331.html
1
2
3
4
5
6
7
8
检查项: 19 | 风险项: 4

| 类别 | 检查项 | 当前值 | 期望值 |
|----------|-----------------------------|-------------------------------|-----------------|
| 密码策略 | 最小密码长度 | 7 字符 | ≥ 14 字符 |
| 防火墙 | Public 配置文件 | 已禁用 (风险) | 已启用 |
| 网络 | 高危端口开放 | 发现 2 个: 445, 3389 ||
| 服务检查 | 远程注册表 (RemoteRegistry) | Running, 启动类型: Automatic | 未安装或已禁用 |

建议在运维流程中定期执行 New-SecurityBaselineReport,并将历次报告归档对比,跟踪风险项的变化趋势。安全基线不是一次性的工作,而是持续改进的过程。

PowerShell 技能连载 - 浏览器自动化实战

适用于 PowerShell 7.0 及以上版本

2025 年,AI + 浏览器(Browser Using)成为技术热点,各大模型厂商纷纷推出浏览器操控能力。浏览器自动化不再只是测试工程师的专属工具——数据采集、运维巡检、表单自动填写、可用性监控等场景都离不开它。

PowerShell 作为 Windows 和 macOS 上广泛使用的脚本语言,同样可以胜任浏览器自动化任务。本文将分别介绍 Selenium 和 Playwright 两种方案,从环境搭建到实际应用,帮助你快速上手。

方案一:Selenium

Selenium 是老牌的浏览器自动化框架,生态成熟,社区资源丰富。它的核心思路是通过 WebDriver 协议控制浏览器,PowerShell 可以直接调用其 .NET 绑定。

安装环境

首先安装 Selenium 模块和 Chrome WebDriver。以下函数会自动完成这些步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Install-SeleniumDriver {
param(
[ValidateSet("Chrome", "Firefox", "Edge")]
[string]$Browser = "Chrome"
)

# 安装 Selenium PowerShell 模块
Install-Module -Name Selenium -Scope CurrentUser -Force

# 创建 WebDriver 存放目录
$driverPath = "$env:TEMP\selenium"
if (-not (Test-Path $driverPath)) {
New-Item -Path $driverPath -ItemType Directory | Out-Null
}

Write-Host "$Browser WebDriver 已就绪"
}

执行安装:

1
2
PS> Install-SeleniumDriver
Chrome WebDriver 已就绪

创建浏览器会话

安装完成后,需要一个函数来启动浏览器。我们支持无头模式(Headless),这样在服务器或 CI 环境中也能正常运行。关键参数 --headless=new 使用 Chrome 新版无头模式,兼容性更好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Start-BrowserSession {
param(
[ValidateSet("Chrome", "Firefox", "Edge")]
[string]$Browser = "Chrome",

[switch]$Headless
)

$options = New-Object OpenQA.Selenium.Chrome.ChromeOptions
if ($Headless) {
$options.AddArgument("--headless=new")
}
$options.AddArgument("--disable-gpu")
$options.AddArgument("--no-sandbox")

$driver = New-Object OpenQA.Selenium.Chrome.ChromeDriver($options)
return $driver
}

页面操作封装

每次都手动管理 Driver 的生命周期容易遗漏资源释放。下面封装一个通用的页面操作函数,自动处理会话创建和销毁。你只需传入一个脚本块来定义具体操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function Invoke-WebPageTask {
param(
[Parameter(Mandatory)]
[string]$Url,

[scriptblock]$Action,

[switch]$Headless,

[int]$TimeoutSeconds = 30
)

$driver = Start-BrowserSession -Headless:$Headless
try {
# 设置隐式等待,避免元素未加载就操作
$driver.Manage().Timeouts().ImplicitWait = [TimeSpan]::FromSeconds($TimeoutSeconds)
$driver.Navigate().GoToUrl($Url)
Write-Host "已打开: $Url"

$result = & $Action -Driver $driver
return $result
}
finally {
# 务必释放浏览器进程
$driver.Quit()
}
}

使用示例——抓取页面标题和所有 HTTPS 链接:

1
2
3
4
5
6
7
8
9
10
11
12
$links = Invoke-WebPageTask -Url "https://learn.microsoft.com/powershell" -Headless -Action {
param($Driver)

$title = $Driver.Title
$links = $Driver.FindElements([OpenQA.Selenium.By]::TagName("a")) |
Where-Object { $_.GetAttribute("href") -match "^https://" } |
Select-Object @{ N = "Text"; E = { $_.Text } },
@{ N = "Href"; E = { $_.GetAttribute("href") } }

Write-Host "页面: $title, 链接数: $($links.Count)"
return $links
}

执行结果示例:

1
2
3
4
5
6
7
已打开: https://learn.microsoft.com/powershell
页面: PowerShell | Microsoft Learn, 链接数: 42

Text Href
---- ----
PowerShell 文档 https://learn.microsoft.com/powershell/
安装 PowerShell https://learn.microsoft.com/powershell/script...

截图功能

用 Selenium 可以轻松截取完整页面截图。以下函数设置视口大小后导航到目标页面,等待 2 秒让动态内容加载完毕,再保存截图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function Save-WebPageScreenshot {
param(
[Parameter(Mandatory)]
[string]$Url,

[Parameter(Mandatory)]
[string]$OutputPath,

[int]$Width = 1920,
[int]$Height = 1080
)

$driver = Start-BrowserSession -Headless
try {
$driver.Manage().Window.Size = New-Object System.Drawing.Size($Width, $Height)
$driver.Navigate().GoToUrl($Url)
Start-Sleep -Seconds 2

$screenshot = $driver.GetScreenshot()
$screenshot.SaveAsFile($OutputPath, [OpenQA.Selenium.ScreenshotImageFormat]::Png)
Write-Host "截图已保存: $OutputPath"
}
finally {
$driver.Quit()
}
}
1
2
PS> Save-WebPageScreenshot -Url "https://example.com" -OutputPath "$HOME/Desktop/demo.png"
截图已保存: /Users/wubo/Desktop/demo.png

方案二:Playwright(推荐)

Playwright 是微软推出的新一代浏览器自动化工具,相比 Selenium 有三大优势:自动等待(不需要手写 Sleep)、内置多浏览器支持、原生支持 PDF 导出。如果你是新项目,强烈推荐直接使用 Playwright。

安装 Playwright

Playwright 是基于 Node.js 的工具,需要先安装 Node.js 环境,然后通过 npm 安装 Playwright 并下载浏览器内核。

1
2
3
4
5
# 全局安装 Playwright 包
npm install -g playwright

# 下载 Chromium 浏览器内核
npx playwright install chromium
1
2
PS> npx playwright install chromium
Downloading Chromium 131.0.6778.33 (darwin) ... done

通用调用函数

PowerShell 本身没有 Playwright 的原生绑定,但可以通过生成临时 JS 脚本文件来调用。以下函数封装了这个流程:接收 URL 和 JS 脚本片段,自动拼接完整的 Playwright 程序,执行后删除临时文件。

注意 here-string 内不要嵌入三反引号,否则会导致 PowerShell 解析错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function Invoke-PlaywrightScript {
param(
[Parameter(Mandatory)]
[string]$Url,

[Parameter(Mandatory)]
[string]$Script
)

# 拼接完整的 Node.js 脚本
$fullScript = @"
const { chromium } = require('playwright');

(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('$Url');

$Script

await browser.close();
})();
"@

# 写入临时文件并执行
$scriptPath = [System.IO.Path]::GetTempFileName() + ".js"
$fullScript | Out-File -FilePath $scriptPath -Encoding UTF8
node $scriptPath
Remove-Item $scriptPath
}

截图与 PDF 导出

Playwright 的截图和 PDF 功能比 Selenium 更强大。特别是 PDF 导出,Selenium 需要借助 Chrome DevTools Protocol,而 Playwright 直接提供 API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function Save-PlaywrightScreenshot {
param(
[Parameter(Mandatory)]
[string]$Url,

[Parameter(Mandatory)]
[string]$OutputPath
)

$jsCode = "await page.screenshot({ path: '$($OutputPath.Replace('\','/'))', fullPage: true });"
Invoke-PlaywrightScript -Url $Url -Script $jsCode
Write-Host "截图已保存: $OutputPath"
}

function Save-PlaywrightPdf {
param(
[Parameter(Mandatory)]
[string]$Url,

[Parameter(Mandatory)]
[string]$OutputPath
)

$jsCode = "await page.pdf({ path: '$($OutputPath.Replace('\','/'))', format: 'A4' });"
Invoke-PlaywrightScript -Url $Url -Script $jsCode
Write-Host "PDF 已保存: $OutputPath"
}
1
2
3
4
5
PS> Save-PlaywrightScreenshot -Url "https://example.com" -OutputPath "$HOME/Desktop/pw.png"
截图已保存: /Users/wubo/Desktop/pw.png

PS> Save-PlaywrightPdf -Url "https://example.com" -OutputPath "$HOME/Desktop/page.pdf"
PDF 已保存: /Users/wubo/Desktop/page.pdf

网站可用性监控

无论选择哪种方案,浏览器自动化最常见的运维场景就是网站健康检查。以下函数使用 Selenium 批量检测多个 URL 的可访问性和加载耗时,输出结构化的监控数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function Test-WebsiteHealth {
param(
[Parameter(Mandatory)]
[string[]]$Urls,

[int]$TimeoutSeconds = 15
)

$driver = Start-BrowserSession -Headless
$results = @()

try {
foreach ($url in $Urls) {
$sw = [System.Diagnostics.Stopwatch]::StartNew()
try {
$driver.Manage().Timeouts().PageLoad = [TimeSpan]::FromSeconds($TimeoutSeconds)
$driver.Navigate().GoToUrl($url)
$sw.Stop()

$results += [PSCustomObject]@{
Url = $url
Status = "OK"
LoadTimeMs = $sw.ElapsedMilliseconds
Title = $driver.Title
Timestamp = Get-Date
}
}
catch {
$sw.Stop()
$results += [PSCustomObject]@{
Url = $url
Status = "FAIL"
LoadTimeMs = $sw.ElapsedMilliseconds
Title = ""
Timestamp = Get-Date
Error = $_.Exception.Message
}
}
}
}
finally {
$driver.Quit()
}

return $results | Format-Table -AutoSize
}
1
2
3
4
5
6
7
PS> Test-WebsiteHealth -Urls "https://example.com","https://httpbin.org/status/500","https://nonexist.invalid"

Url Status LoadTimeMs Title Timestamp
--- ------ ---------- ----- ---------
https://example.com OK 312 Example Domain 2025/3/28 10:00:00
https://httpbin.org/st... OK 587 2025/3/28 10:00:01
https://nonexist.invalid FAIL 5001 2025/3/28 10:00:06

小结

Selenium 适合简单的页面交互场景,依赖 .NET 生态,PowerShell 调用起来比较自然。Playwright 更现代,自动等待、原生 PDF 支持、多浏览器覆盖等特性让它在复杂场景下更可靠。无头模式下务必注意内存回收,长时间运行的任务一定要在 finally 块中调用 Quit()close() 释放浏览器进程。

PowerShell 技能连载 - 调用大语言模型 API

适用于 PowerShell 7.0 及以上版本

大语言模型(LLM)已经渗透到开发工作的方方面面。当我们需要在自动化脚本中集成 AI 能力时,直接调用 OpenAI 兼容的 REST API 是最灵活的方式。PowerShell 内置的 Invoke-RestMethod cmdlet 天然适合完成这项工作——无需安装额外 SDK,几行代码即可实现与 LLM 的交互。

本文将从零开始,逐步带你完成 API Key 配置、单次问答封装、多轮对话、代码审查场景以及 Token 用量估算。

准备工作:配置 API Key

调用任何 OpenAI 兼容接口都需要一个 API Key。出于安全考虑,我们通过环境变量来管理它,避免在代码中硬编码。

1
2
# 在 PowerShell 配置文件中添加(只需执行一次)
Add-Content -Path $PROFILE -Value '`$env:OPENAI_API_KEY = "sk-your-key-here"'

如果你使用的是国内中转代理或其他 OpenAI 兼容服务(如 Azure OpenAI、DeepSeek),还需要额外设置基础 URL:

1
2
# 设置自定义 API 端点(可选)
$env:OPENAI_API_BASE = "https://your-proxy.example.com/v1"

配置完成后,重新打开 PowerShell 会话即可生效。你可以通过以下方式验证:

1
2
PS> $env:OPENAI_API_KEY
sk-proj-xxxxxxxxxxxxxx

单次问答:封装通用函数

下面我们封装一个 Invoke-LLMChat 函数,它接受用户提问,返回模型的回复。这个函数是后续所有场景的基础。

函数内部会自动读取环境变量中的 API Key,将用户消息和系统提示组装成 OpenAI Chat Completion 格式的 JSON,然后通过 Invoke-RestMethod 发送 POST 请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function Invoke-LLMChat {
param(
[Parameter(Mandatory)]
[string]$Prompt,

[string]$Model = "gpt-4o-mini",

[string]$SystemMessage = "你是一个有帮助的 PowerShell 助手。",

[double]$Temperature = 0.7,

[int]$MaxTokens = 2048
)

# 检查 API Key 是否已配置
$apiKey = $env:OPENAI_API_KEY
if (-not $apiKey) {
throw "请先设置环境变量 OPENAI_API_KEY"
}

# 确定请求地址:优先使用自定义端点
$baseUrl = if ($env:OPENAI_API_BASE) { $env:OPENAI_API_BASE } else { "https://api.openai.com/v1" }
$uri = "$baseUrl/chat/completions"

# 构造请求体
$body = @{
model = $Model
messages = @(
@{ role = "system"; content = $SystemMessage }
@{ role = "user"; content = $Prompt }
)
temperature = $Temperature
max_tokens = $MaxTokens
} | ConvertTo-Json -Depth 5 -Compress

# 发送请求(使用 UTF8 编码避免中文乱码)
$response = Invoke-RestMethod `
-Uri $uri `
-Method Post `
-Headers @{ Authorization = "Bearer $apiKey" } `
-ContentType "application/json; charset=utf-8" `
-Body ([System.Text.Encoding]::UTF8.GetBytes($body))

return $response.choices[0].message.content
}

这里有两个值得注意的细节:一是用 -Compress 参数减少 JSON 体积(去掉多余空白),二是用 UTF8.GetBytes 确保中文字符不会在传输中变成乱码。如果你不需要自定义端点,也可以省略 $env:OPENAI_API_BASE 相关逻辑。

调用示例:

1
2
3
4
PS> Invoke-LLMChat -Prompt "用一行 PowerShell 代码获取本机所有 IP 地址"

你可以使用以下命令获取本机所有 IP 地址:
(Get-NetIPAddress -AddressFamily IPv4).IPAddress

多轮对话:维护上下文历史

单次问答没有”记忆”。要让模型理解上下文,我们需要把完整的对话历史(包括之前的用户消息和助手回复)都发给 API。下面这个函数实现了一个交互式的多轮对话循环。

关键点在于 $messages 数组——每次用户发言后追加一条 user 消息,收到模型回复后追加一条 assistant 消息,这样上下文就在数组中不断累积。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function Start-LLMConversation {
param(
[string]$Model = "gpt-4o-mini"
)

$apiKey = $env:OPENAI_API_KEY
$baseUrl = if ($env:OPENAI_API_BASE) { $env:OPENAI_API_BASE } else { "https://api.openai.com/v1" }
$uri = "$baseUrl/chat/completions"

# 初始化对话历史,包含系统提示
$messages = @(
@{ role = "system"; content = "你是一个有帮助的助手,请用中文回答。" }
)

Write-Host "多轮对话已启动,输入 'exit' 退出" -ForegroundColor Cyan

while ($true) {
$userInput = Read-Host "你"
if ($userInput -eq "exit") { break }
if ([string]::IsNullOrWhiteSpace($userInput)) { continue }

# 追加用户消息到历史
$messages += @{ role = "user"; content = $userInput }

$body = @{
model = $Model
messages = $messages
} | ConvertTo-Json -Depth 10 -Compress

$response = Invoke-RestMethod `
-Uri $uri `
-Method Post `
-Headers @{ Authorization = "Bearer $apiKey" } `
-ContentType "application/json; charset=utf-8" `
-Body ([System.Text.Encoding]::UTF8.GetBytes($body))

$assistantMessage = $response.choices[0].message.content

# 追加助手回复到历史,保持上下文连续
$messages += @{ role = "assistant"; content = $assistantMessage }

Write-Host "`n助手: $assistantMessage`n" -ForegroundColor Green

# 显示 Token 消耗,帮助控制成本
$usage = $response.usage
Write-Host "[Token] 输入: $($usage.prompt_tokens) | 输出: $($usage.completion_tokens)" -ForegroundColor DarkGray
}
}

运行效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PS> Start-LLMConversation
多轮对话已启动,输入 'exit' 退出
你: 写一个函数检查磁盘空间

助手: 这是一个检查磁盘空间的函数:

function Get-DiskSpace {
param([string]$ComputerName = $env:COMPUTERNAME)
Get-CimInstance -ClassName Win32_LogicalDisk ...
}

[Token] 输入: 28 | 输出: 156
你: 再加上邮件告警功能

助手: 好的,在原有函数基础上增加邮件告警:

function Get-DiskSpace {
param(
[string]$ComputerName = $env:COMPUTERNAME,
[double]$ThresholdGB = 10,
...

[Token] 输入: 210 | 输出: 203
你: exit

可以看到第二轮对话中,输入 Token 从 28 涨到 210,因为整个对话历史都被带上了。这也提醒我们:多轮对话的 Token 消耗是递增的,长对话时需要考虑截断历史。

实战场景:代码审查助手

将 LLM 集成到日常工作流中,最实用的场景之一就是代码审查。我们读取一个脚本文件的内容,让模型从安全性、性能、可维护性等维度进行分析。

注意这里用数组拼接代替了 here-string,避免在内容中嵌入三反引号导致语法冲突。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function Request-CodeReview {
param(
[Parameter(Mandatory)]
[string]$ScriptPath
)

# 读取脚本内容
$code = Get-Content $ScriptPath -Raw

# 用数组拼接构造提示词,避免 here-string 中出现三反引号
$promptParts = @(
"请审查以下 PowerShell 脚本,指出潜在问题并提供改进建议:"
""
"--- 脚本内容开始 ---"
$code
"--- 脚本内容结束 ---"
""
"请从以下角度分析:"
"1. 安全性(注入风险、凭据处理)"
"2. 性能(循环优化、管道使用)"
"3. 可维护性(命名规范、错误处理)"
"4. 兼容性(PowerShell 版本差异)"
)
$prompt = $promptParts -join "`n"

$review = Invoke-LLMChat -Prompt $prompt -Model "gpt-4o" -MaxTokens 4096

Write-Host "`n========== 代码审查报告 ==========`n" -ForegroundColor Yellow
Write-Host $review
Write-Host "`n==================================`n" -ForegroundColor Yellow
}

假设我们有一个脚本 cleanup.ps1,内容是简单的临时文件清理,执行审查后输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PS> Request-CodeReview -ScriptPath .\cleanup.ps1

========== 代码审查报告 ==========

## 代码审查结果

### 1. 安全性
- 第 3 行使用了硬编码路径 `C:\Temp`,建议改为参数化配置
- 缺少 `-WhatIf` 支持,建议添加 `[SupportsShouldProcess()]`

### 2. 性能
- `Get-ChildItem` 未使用 `-File` 参数,可能误删目录
- 建议添加 `-ErrorAction SilentlyContinue` 避免权限异常中断

### 3. 可维护性
- 缺少注释和帮助文档(Comment-Based Help)
- 变量 `$d` 命名不清晰,建议改为 `$daysOld`

### 4. 兼容性
- 使用了 PowerShell 7 的 `Ternernary` 运算符,Windows PowerShell 5.1 不兼容

==================================

Token 用量估算

在频繁调用 API 的场景下,了解 Token 消耗非常重要。下面这个函数基于响应中的 usage 字段进行累计统计,帮你掌握每次调用的开销。

不同模型的单价不同,函数中提供了常见模型的参考价格(美元/千 Token),你可以根据实际情况调整。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function Get-LLMTokenCost {
param(
[int]$PromptTokens,
[int]$CompletionTokens,
[string]$Model = "gpt-4o-mini"
)

# 常见模型价格表(美元/千 Token,仅供参考)
$pricing = @{
"gpt-4o" = @{ Input = 0.005; Output = 0.015 }
"gpt-4o-mini" = @{ Input = 0.00015; Output = 0.0006 }
"gpt-3.5-turbo" = @{ Input = 0.0005; Output = 0.0015 }
}

$rate = $pricing[$Model]
if (-not $rate) {
Write-Warning "未找到模型 $Model 的定价信息"
return
}

$inputCost = [math]::Round($PromptTokens * $rate.Input / 1000, 6)
$outputCost = [math]::Round($CompletionTokens * $rate.Output / 1000, 6)
$totalCost = [math]::Round($inputCost + $outputCost, 6)

[PSCustomObject]@{
模型 = $Model
输入Token = $PromptTokens
输出Token = $CompletionTokens
总Token = $PromptTokens + $CompletionTokens
输入费用 = "`$$inputCost"
输出费用 = "`$$outputCost"
总费用 = "`$$totalCost"
}
}

使用示例:

1
2
3
4
5
PS> Get-LLMTokenCost -PromptTokens 210 -CompletionTokens 203 -Model gpt-4o-mini

模型 输入Token 输出Token 总Token 输入费用 输出费用 总费用
---- -------- -------- ------- -------- -------- ------
gpt-4o-mini 210 203 413 $0.000032 $0.000122 $0.000154

可以看到,一次典型的多轮对话调用成本极低。但如果每天执行数百次自动化任务,费用仍然会累积,因此建议在脚本中加入 Token 上限控制。

注意事项

  • API Key 安全:切勿将 Key 硬编码在脚本中或提交到代码仓库。使用环境变量或 Azure Key Vault 等密钥管理服务。可以在 .gitignore 中排除包含敏感信息的配置文件。
  • Token 限制:每个模型有最大上下文窗口(如 gpt-4o-mini 为 128K)。多轮对话时注意累积的消息长度,必要时截断早期历史。建议在发送前估算 Token 数量(粗略规则:1 个汉字约 1-2 个 Token)。
  • 国内代理:如果无法直接访问 OpenAI API,可以设置 $env:OPENAI_API_BASE 指向国内代理。选择代理服务时注意数据隐私条款,避免敏感代码经第三方中转。
  • 错误处理:生产环境中建议在 Invoke-RestMethod 外包裹 try/catch,处理网络超时、API 限流(429 状态码)等异常情况。
  • 流式响应:本文示例使用非流式调用(等待完整响应后再返回)。如需实现打字机效果,可以使用 SSE(Server-Sent Events)模式,但实现复杂度较高,适合单独封装。

PowerShell 技能连载 - 图像处理技巧

在 PowerShell 中处理图像文件可能不是最常见的任务,但在某些场景下非常有用。本文将介绍一些实用的图像处理技巧。

首先,让我们看看基本的图像操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 创建图像处理函数
function Get-ImageInfo {
param(
[string]$ImagePath
)

# 使用 System.Drawing 获取图像信息
Add-Type -AssemblyName System.Drawing
$image = [System.Drawing.Image]::FromFile($ImagePath)

$info = [PSCustomObject]@{
FileName = Split-Path $ImagePath -Leaf
Width = $image.Width
Height = $image.Height
PixelFormat = $image.PixelFormat
Resolution = $image.HorizontalResolution
FileSize = (Get-Item $ImagePath).Length
}

$image.Dispose()
return $info
}

图像格式转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 创建图像格式转换函数
function Convert-ImageFormat {
param(
[string]$InputPath,
[string]$OutputPath,
[ValidateSet("jpg", "png", "bmp", "gif")]
[string]$TargetFormat
)

try {
Add-Type -AssemblyName System.Drawing
$image = [System.Drawing.Image]::FromFile($InputPath)

switch ($TargetFormat) {
"jpg" { $image.Save($OutputPath, [System.Drawing.Imaging.ImageFormat]::Jpeg) }
"png" { $image.Save($OutputPath, [System.Drawing.Imaging.ImageFormat]::Png) }
"bmp" { $image.Save($OutputPath, [System.Drawing.Imaging.ImageFormat]::Bmp) }
"gif" { $image.Save($OutputPath, [System.Drawing.Imaging.ImageFormat]::Gif) }
}

$image.Dispose()
Write-Host "图像转换完成:$OutputPath"
}
catch {
Write-Host "转换失败:$_"
}
}

图像调整:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 创建图像调整函数
function Resize-Image {
param(
[string]$InputPath,
[string]$OutputPath,
[int]$Width,
[int]$Height
)

try {
Add-Type -AssemblyName System.Drawing
$image = [System.Drawing.Image]::FromFile($InputPath)

# 创建新的位图
$newImage = New-Object System.Drawing.Bitmap($Width, $Height)
$graphics = [System.Drawing.Graphics]::FromImage($newImage)

# 设置高质量插值模式
$graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic

# 绘制调整后的图像
$graphics.DrawImage($image, 0, 0, $Width, $Height)

# 保存结果
$newImage.Save($OutputPath)

# 清理资源
$graphics.Dispose()
$newImage.Dispose()
$image.Dispose()

Write-Host "图像调整完成:$OutputPath"
}
catch {
Write-Host "调整失败:$_"
}
}

图像效果处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 创建图像效果处理函数
function Apply-ImageEffect {
param(
[string]$InputPath,
[string]$OutputPath,
[ValidateSet("grayscale", "sepia", "blur", "sharpen")]
[string]$Effect
)

try {
Add-Type -AssemblyName System.Drawing
$image = [System.Drawing.Image]::FromFile($InputPath)
$bitmap = New-Object System.Drawing.Bitmap($image)

switch ($Effect) {
"grayscale" {
for ($x = 0; $x -lt $bitmap.Width; $x++) {
for ($y = 0; $y -lt $bitmap.Height; $y++) {
$pixel = $bitmap.GetPixel($x, $y)
$gray = [int](($pixel.R * 0.3) + ($pixel.G * 0.59) + ($pixel.B * 0.11))
$bitmap.SetPixel($x, $y, [System.Drawing.Color]::FromArgb($gray, $gray, $gray))
}
}
}
"sepia" {
for ($x = 0; $x -lt $bitmap.Width; $x++) {
for ($y = 0; $y -lt $bitmap.Height; $y++) {
$pixel = $bitmap.GetPixel($x, $y)
$r = [int](($pixel.R * 0.393) + ($pixel.G * 0.769) + ($pixel.B * 0.189))
$g = [int](($pixel.R * 0.349) + ($pixel.G * 0.686) + ($pixel.B * 0.168))
$b = [int](($pixel.R * 0.272) + ($pixel.G * 0.534) + ($pixel.B * 0.131))
$bitmap.SetPixel($x, $y, [System.Drawing.Color]::FromArgb($r, $g, $b))
}
}
}
}

$bitmap.Save($OutputPath)

# 清理资源
$bitmap.Dispose()
$image.Dispose()

Write-Host "已应用效果:$Effect"
}
catch {
Write-Host "效果处理失败:$_"
}
}

这些技巧将帮助您更有效地处理图像文件。记住,在处理图像时,始终要注意内存使用和资源释放。同时,建议在处理大型图像文件时使用流式处理方式,以提高性能。

PowerShell 技能连载 - 教育设备同步系统

在教育环境中,设备同步对于确保教学资源的统一性和可访问性至关重要。本文将介绍如何使用PowerShell构建一个教育设备同步系统,包括设备管理、内容同步、状态监控等功能。

设备管理

首先,让我们创建一个用于管理教育设备的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
function Get-EducationDevices {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Location,

[Parameter()]
[string[]]$DeviceTypes,

[Parameter()]
[string]$Status,

[Parameter()]
[switch]$IncludeOffline
)

try {
$devices = [PSCustomObject]@{
Location = $Location
QueryTime = Get-Date
Devices = @()
}

# 从设备管理系统获取设备列表
$deviceList = Get-DeviceList -Location $Location `
-DeviceTypes $DeviceTypes `
-Status $Status

foreach ($device in $deviceList) {
$deviceInfo = [PSCustomObject]@{
DeviceID = $device.ID
Name = $device.Name
Type = $device.Type
Location = $device.Location
Status = $device.Status
LastSync = $device.LastSync
IPAddress = $device.IPAddress
MACAddress = $device.MACAddress
OSVersion = $device.OSVersion
Storage = Get-DeviceStorage -DeviceID $device.ID
Network = Get-DeviceNetwork -DeviceID $device.ID
}

# 检查设备在线状态
if ($IncludeOffline -or (Test-DeviceConnection -DeviceID $device.ID)) {
$devices.Devices += $deviceInfo
}
}

return $devices
}
catch {
Write-Error "获取教育设备列表失败:$_"
return $null
}
}

function Update-DeviceInventory {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Location,

[Parameter()]
[string]$InventoryPath,

[Parameter()]
[switch]$Force
)

try {
$inventory = [PSCustomObject]@{
Location = $Location
UpdateTime = Get-Date
Devices = @()
}

# 获取所有设备
$devices = Get-EducationDevices -Location $Location -IncludeOffline

# 更新设备清单
foreach ($device in $devices.Devices) {
$inventory.Devices += [PSCustomObject]@{
DeviceID = $device.DeviceID
Name = $device.Name
Type = $device.Type
Status = $device.Status
LastUpdate = Get-Date
HardwareInfo = Get-DeviceHardwareInfo -DeviceID $device.DeviceID
SoftwareInfo = Get-DeviceSoftwareInfo -DeviceID $device.DeviceID
MaintenanceHistory = Get-DeviceMaintenanceHistory -DeviceID $device.DeviceID
}
}

# 保存设备清单
if ($InventoryPath) {
$inventory | ConvertTo-Json -Depth 10 | Out-File -FilePath $InventoryPath -Force
}

return $inventory
}
catch {
Write-Error "更新设备清单失败:$_"
return $null
}
}

内容同步

接下来,创建一个用于同步教育内容的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
function Sync-EducationContent {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$DeviceID,

[Parameter(Mandatory = $true)]
[string[]]$ContentTypes,

[Parameter()]
[string]$SourcePath,

[Parameter()]
[string]$DestinationPath,

[Parameter()]
[switch]$Force,

[Parameter()]
[int]$RetryCount = 3
)

try {
$syncResult = [PSCustomObject]@{
DeviceID = $DeviceID
StartTime = Get-Date
ContentTypes = $ContentTypes
Status = "InProgress"
Details = @()
}

# 检查设备状态
$deviceStatus = Get-DeviceStatus -DeviceID $DeviceID
if (-not $deviceStatus.IsOnline) {
throw "设备 $DeviceID 当前处于离线状态"
}

# 检查存储空间
$storageStatus = Get-DeviceStorage -DeviceID $DeviceID
if (-not $storageStatus.HasEnoughSpace) {
throw "设备存储空间不足"
}

# 同步每种类型的内容
foreach ($contentType in $ContentTypes) {
$syncDetail = [PSCustomObject]@{
ContentType = $contentType
StartTime = Get-Date
Status = "InProgress"
Files = @()
}

try {
# 获取需要同步的文件列表
$files = Get-ContentFiles -ContentType $contentType `
-SourcePath $SourcePath `
-DeviceID $DeviceID

foreach ($file in $files) {
$retryCount = 0
$success = $false

while (-not $success -and $retryCount -lt $RetryCount) {
try {
$result = Copy-ContentFile -SourceFile $file.SourcePath `
-DestinationFile $file.DestinationPath `
-DeviceID $DeviceID

if ($result.Success) {
$success = $true
$syncDetail.Files += [PSCustomObject]@{
FileName = $file.FileName
Size = $file.Size
Status = "Success"
SyncTime = Get-Date
}
}
}
catch {
$retryCount++
if ($retryCount -eq $RetryCount) {
throw "文件同步失败:$_"
}
Start-Sleep -Seconds 2
}
}
}

$syncDetail.Status = "Success"
$syncDetail.EndTime = Get-Date
}
catch {
$syncDetail.Status = "Failed"
$syncDetail.Error = $_.Exception.Message
}

$syncResult.Details += $syncDetail
}

# 更新同步状态
$syncResult.Status = if ($syncResult.Details.Status -contains "Failed") { "Failed" } else { "Success" }
$syncResult.EndTime = Get-Date

return $syncResult
}
catch {
Write-Error "内容同步失败:$_"
return $null
}
}

状态监控

最后,创建一个用于监控教育设备状态的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
function Monitor-DeviceStatus {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Location,

[Parameter()]
[string[]]$DeviceTypes,

[Parameter()]
[int]$CheckInterval = 300,

[Parameter()]
[string]$LogPath,

[Parameter()]
[hashtable]$AlertThresholds
)

try {
$monitor = [PSCustomObject]@{
Location = $Location
StartTime = Get-Date
Devices = @()
Alerts = @()
}

while ($true) {
$checkTime = Get-Date
$devices = Get-EducationDevices -Location $Location -DeviceTypes $DeviceTypes

foreach ($device in $devices.Devices) {
$deviceStatus = [PSCustomObject]@{
DeviceID = $device.DeviceID
CheckTime = $checkTime
Status = $device.Status
Metrics = @{}
Alerts = @()
}

# 检查设备性能指标
$deviceStatus.Metrics = Get-DeviceMetrics -DeviceID $device.DeviceID

# 检查告警阈值
if ($AlertThresholds) {
foreach ($metric in $deviceStatus.Metrics.Keys) {
if ($AlertThresholds.ContainsKey($metric)) {
$threshold = $AlertThresholds[$metric]
$value = $deviceStatus.Metrics[$metric]

if ($value -gt $threshold.Max) {
$deviceStatus.Alerts += [PSCustomObject]@{
Type = "HighValue"
Metric = $metric
Value = $value
Threshold = $threshold.Max
Time = $checkTime
}
}

if ($value -lt $threshold.Min) {
$deviceStatus.Alerts += [PSCustomObject]@{
Type = "LowValue"
Metric = $metric
Value = $value
Threshold = $threshold.Min
Time = $checkTime
}
}
}
}
}

$monitor.Devices += $deviceStatus

# 处理告警
if ($deviceStatus.Alerts.Count -gt 0) {
foreach ($alert in $deviceStatus.Alerts) {
$monitor.Alerts += $alert

# 记录告警日志
if ($LogPath) {
$alert | ConvertTo-Json | Out-File -FilePath $LogPath -Append
}

# 发送告警通知
Send-DeviceAlert -Alert $alert
}
}
}

Start-Sleep -Seconds $CheckInterval
}

return $monitor
}
catch {
Write-Error "设备状态监控失败:$_"
return $null
}
}

使用示例

以下是如何使用这些函数来管理教育设备的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 配置设备监控参数
$monitorConfig = @{
Location = "教学楼A"
DeviceTypes = @("StudentPC", "TeacherPC", "Projector")
CheckInterval = 300
LogPath = "C:\Logs\device_status.json"
AlertThresholds = @{
"CPUUsage" = @{
Min = 0
Max = 90
}
"MemoryUsage" = @{
Min = 0
Max = 85
}
"DiskUsage" = @{
Min = 0
Max = 95
}
}
}

# 更新设备清单
$inventory = Update-DeviceInventory -Location $monitorConfig.Location `
-InventoryPath "C:\Inventory\devices.json" `
-Force

# 同步教育内容
$syncResult = Sync-EducationContent -DeviceID "PC001" `
-ContentTypes @("Courseware", "Assignments", "Resources") `
-SourcePath "\\Server\EducationContent" `
-DestinationPath "C:\Education" `
-RetryCount 3

# 启动设备状态监控
$monitor = Start-Job -ScriptBlock {
param($config)
Monitor-DeviceStatus -Location $config.Location `
-DeviceTypes $config.DeviceTypes `
-CheckInterval $config.CheckInterval `
-LogPath $config.LogPath `
-AlertThresholds $config.AlertThresholds
} -ArgumentList $monitorConfig

最佳实践

  1. 实现设备分组管理
  2. 使用增量同步提高效率
  3. 建立完整的备份机制
  4. 实施访问控制策略
  5. 定期进行系统维护
  6. 保持详细的同步日志
  7. 实现自动化的状态报告
  8. 建立应急响应机制

PowerShell 技能连载 - 网络操作技巧

在 PowerShell 中处理网络操作是一项常见任务,特别是在系统管理和自动化过程中。本文将介绍一些实用的网络操作技巧。

首先,让我们看看基本的网络连接测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 测试网络连接
$hosts = @(
"www.baidu.com",
"www.qq.com",
"www.taobao.com"
)

foreach ($host in $hosts) {
$result = Test-NetConnection -ComputerName $host -Port 80
Write-Host "`n测试 $host 的连接:"
Write-Host "是否可达:$($result.TcpTestSucceeded)"
Write-Host "响应时间:$($result.PingReplyDetails.RoundtripTime)ms"
}

获取网络配置信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 获取网络适配器信息
$adapters = Get-NetAdapter | Where-Object { $_.Status -eq "Up" }

foreach ($adapter in $adapters) {
Write-Host "`n网卡名称:$($adapter.Name)"
Write-Host "连接状态:$($adapter.Status)"
Write-Host "MAC地址:$($adapter.MacAddress)"

# 获取IP配置
$ipConfig = Get-NetIPConfiguration -InterfaceIndex $adapter.ifIndex
Write-Host "IP地址:$($ipConfig.IPv4Address.IPAddress)"
Write-Host "子网掩码:$($ipConfig.IPv4Address.PrefixLength)"
Write-Host "默认网关:$($ipConfig.IPv4DefaultGateway.NextHop)"
}

配置网络设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 配置静态IP地址
$adapterName = "以太网"
$ipAddress = "192.168.1.100"
$prefixLength = 24
$defaultGateway = "192.168.1.1"

# 获取网卡
$adapter = Get-NetAdapter -Name $adapterName

# 配置IP地址
New-NetIPAddress -InterfaceIndex $adapter.ifIndex -IPAddress $ipAddress -PrefixLength $prefixLength

# 配置默认网关
New-NetRoute -InterfaceIndex $adapter.ifIndex -NextHop $defaultGateway -DestinationPrefix "0.0.0.0/0"

网络流量监控:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 创建网络流量监控函数
function Monitor-NetworkTraffic {
param(
[string]$InterfaceName,
[int]$Duration = 60
)

$endTime = (Get-Date).AddSeconds($Duration)
$adapter = Get-NetAdapter -Name $InterfaceName

Write-Host "开始监控 $InterfaceName 的网络流量..."
Write-Host "监控时长:$Duration 秒"

while ((Get-Date) -lt $endTime) {
$stats = Get-NetAdapterStatistics -Name $InterfaceName
Write-Host "`n当前时间:$(Get-Date -Format 'HH:mm:ss')"
Write-Host "接收字节:$($stats.ReceivedBytes)"
Write-Host "发送字节:$($stats.SentBytes)"
Start-Sleep -Seconds 1
}
}

一些实用的网络操作技巧:

  1. DNS 解析:
1
2
3
4
5
6
7
8
9
10
11
12
13
# DNS 解析和反向解析
$hostname = "www.baidu.com"
$ip = "8.8.8.8"

# 正向解析
$dnsResult = Resolve-DnsName -Name $hostname
Write-Host "`n$hostname 的IP地址:"
$dnsResult | ForEach-Object { $_.IPAddress }

# 反向解析
$reverseResult = Resolve-DnsName -Name $ip -Type PTR
Write-Host "`n$ip 的主机名:"
$reverseResult.NameHost
  1. 端口扫描:
1
2
3
4
5
6
7
8
9
10
11
12
# 简单的端口扫描函数
function Test-Port {
param(
[string]$ComputerName,
[int[]]$Ports = @(80,443,3389,22)
)

foreach ($port in $Ports) {
$result = Test-NetConnection -ComputerName $ComputerName -Port $port -WarningAction SilentlyContinue
Write-Host "端口 $port$($result.TcpTestSucceeded)"
}
}
  1. 网络共享管理:
1
2
3
4
5
6
7
8
9
10
11
12
13
# 创建网络共享
$shareName = "DataShare"
$path = "C:\SharedData"
$description = "数据共享文件夹"

# 创建文件夹
New-Item -ItemType Directory -Path $path -Force

# 创建共享
New-SmbShare -Name $shareName -Path $path -Description $description -FullAccess "Everyone"

# 设置共享权限
Grant-SmbShareAccess -Name $shareName -AccountName "Domain\Users" -AccessRight Read

这些技巧将帮助您更有效地处理网络操作。记住,在进行网络配置时,始终要注意网络安全性和性能影响。同时,建议在测试环境中先验证网络配置的正确性。

PowerShell 技术 QQ 群