PowerShell 技能连载 - REST API 开发

适用于 PowerShell 7.0 及以上版本

当我们谈论 PowerShell 与 REST API 时,通常是在讨论如何用 Invoke-RestMethod 调用外部服务。但 PowerShell 的能力不止于此——借助 .NET 的 HttpListener 类或社区开发的 Web 框架,你完全可以用 PowerShell 构建自己的 REST API 服务。

这种能力在内部工具场景中尤为实用。比如 CI/CD 流水线需要一个状态查询端点、监控系统需要一个数据聚合接口、第三方 Webhook 需要一个接收器——这些都不需要引入一整套 ASP.NET 或 Node.js 项目,几十行 PowerShell 脚本就能搞定。本文将从底层到高层,逐步演示三种方案。

使用 HttpListener 创建基础 REST API

System.Net.HttpListener 是 .NET 内置的 HTTP 服务器类,无需安装任何额外模块即可使用。下面这段代码创建了一个支持 GET 和 POST 路由的简易 API,返回 JSON 格式的数据。

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
using namespace System.Net
using namespace System.Text

# 创建 API 数据存储
$script:dataStore = @{
tasks = [System.Collections.ArrayList]::new()
1..5 | ForEach-Object {
@{ id = $_; title = "任务 $_"; status = if ($_ -le 3) { 'done' } else { 'pending' } }
}
}

# 路由表
$routes = @{
'GET /api/tasks' = {
param($ctx)
$json = $script:dataStore.tasks | ConvertTo-Json -Depth 3
Send-JsonResponse $ctx $json 200
}
'GET /api/tasks/id' = {
param($ctx)
$id = [int]($ctx.Request.Url.Segments | Select-Object -Last 1)
$task = $script:dataStore.tasks | Where-Object { $_.id -eq $id }
if ($task) {
Send-JsonResponse $ctx ($task | ConvertTo-Json) 200
} else {
Send-JsonResponse $ctx '{"error":"任务不存在"}' 404
}
}
'POST /api/tasks' = {
param($ctx)
$reader = [StreamReader]::new($ctx.Request.InputStream)
$body = $reader.ReadToEnd()
$reader.Close()
$newTask = $body | ConvertFrom-Json
$script:dataStore.tasks.Add(@{
id = ($script:dataStore.tasks.Count + 1)
title = $newTask.title
status = 'pending'
}) | Out-Null
Send-JsonResponse $ctx '{"message":"任务已创建"}' 201
}
}

# 响应辅助函数
function Send-JsonResponse {
param([HttpListenerContext]$Context, [string]$Body, [int]$StatusCode)
$buffer = [Encoding]::UTF8.GetBytes($Body)
$Context.Response.StatusCode = $StatusCode
$Context.Response.ContentType = 'application/json; charset=utf-8'
$Context.Response.ContentLength64 = $buffer.Length
$Context.Response.OutputStream.Write($buffer, 0, $buffer.Length)
$Context.Response.Close()
}

# 启动监听器
$listener = [HttpListener]::new()
$listener.Prefixes.Add('http://localhost:8080/')
$listener.Start()
Write-Host "API 服务已启动: http://localhost:8080/" -ForegroundColor Green

try {
while ($listener.IsListening) {
$ctx = $listener.GetContext()
$method = $ctx.Request.HttpMethod
$path = $ctx.Request.Url.AbsolutePath.TrimEnd('/')
Write-Host " [$method] $path" -ForegroundColor Cyan

# 路由匹配
$routeKey = "$method $path"
$handler = $routes[$routeKey]
if (-not $handler -and $path -match '/api/tasks/\d+$') {
$handler = $routes['GET /api/tasks/id']
}

if ($handler) {
& $handler $ctx
} else {
Send-JsonResponse $ctx '{"error":"路由不存在"}' 404
}
}
} finally {
$listener.Stop()
Write-Host "API 服务已停止" -ForegroundColor Yellow
}

在另一个终端中测试 API:

1
2
3
4
5
6
7
8
# 查询所有任务
Invoke-RestMethod http://localhost:8080/api/tasks

# 查询单个任务
Invoke-RestMethod http://localhost:8080/api/tasks/2

# 创建新任务
Invoke-RestMethod http://localhost:8080/api/tasks -Method Post -Body '{"title":"新任务"}' -ContentType 'application/json'

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
id title  status
-- ----- ------
1 任务 1 done
2 任务 2 done
3 任务 3 done
4 任务 4 pending
5 任务 5 pending

id title status
-- ----- ------
2 任务 2 done

message
-------
任务已创建

使用 Pode 框架构建完整 API

HttpListener 虽然灵活,但需要手动处理路由、中间件等逻辑。Pode 是一个专为 PowerShell 设计的跨平台 Web 框架,提供了路由、中间件、认证、静态文件、日志等开箱即用的功能。

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
# 安装 Pode 模块(首次使用)
Install-Module Pode -Force -Scope CurrentUser

# 创建 api-server.ps1
Start-PodeServer {
# 监听端口
Add-PodeEndpoint -Address localhost -Port 3000 -Protocol Http

# 配置日志
New-PodeLoggingMethod -File -Name 'api-errors' | Enable-PodeErrorLogging
New-PodeLoggingMethod -File -Name 'api-requests' | New-PodeRequestLogging

# JWT 认证中间件
New-PodeAuthScheme -Bearer | Add-PodeAuth -Name 'jwt-auth' -Sessionless {
param($token)
try {
$payload = $token | Invoke-RestMethod -Uri 'https://your-idp/.well-known/jwks.json'
# 简化演示:直接验证 token 非空
if ([string]::IsNullOrEmpty($token)) {
return @{ Message = 'Token 无效' }
}
return @{ User = @{ Name = 'api-user'; Role = 'admin' } }
} catch {
return @{ Message = "认证失败: $_" }
}
}

# 全局中间件:请求计时
Add-PodeMiddleware -Name 'timer' -ScriptBlock {
$WebEvent.Metadata['StartTime'] = [datetime]::UtcNow
}

# 全局中间件:CORS 支持
Add-PodeMiddleware -Name 'cors' -ScriptBlock {
$WebEvent.Response.Headers.Add('Access-Control-Allow-Origin', '*')
$WebEvent.Response.Headers.Add('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE')
if ($WebEvent.Method -eq 'OPTIONS') {
Set-PodeResponseStatus -Code 204
}
}

# 健康检查端点(无需认证)
Add-PodeRoute -Method Get -Path '/health' -ScriptBlock {
Write-PodeJsonResponse -Value @{
status = 'healthy'
uptime = (Get-Date) - $PodeContext.Server.StartTime
version = '1.0.0'
hostname = $env:COMPUTERNAME
}
}

# GET /api/items - 获取列表
Add-PodeRoute -Method Get -Path '/api/items' -Authentication 'jwt-auth' -ScriptBlock {
$page = [int]($WebEvent.Query['page'] ?? '1')
$size = [int]($WebEvent.Query['size'] ?? '20')
Write-PodeJsonResponse -Value @{
data = @(
@{ id = 1; name = '项目 A'; status = 'active' }
@{ id = 2; name = '项目 B'; status = 'archived' }
@{ id = 3; name = '项目 C'; status = 'active' }
)
page = $page
size = $size
total = 3
}
}

# POST /api/items - 创建项目
Add-PodeRoute -Method Post -Path '/api/items' -Authentication 'jwt-auth' -ScriptBlock {
$body = $WebEvent.Data
if (-not $body.name) {
Set-PodeResponseStatus -Code 400
Write-PodeJsonResponse -Value @{ error = 'name 字段必填' }
return
}
Write-PodeJsonResponse -Value @{
id = Get-Random -Maximum 1000
name = $body.name
status = 'created'
message = '项目创建成功'
} -StatusCode 201
}

# 静态文件托管
Add-PodeStaticRoute -Path '/docs' -Source './public'

Write-Host "Pode API 服务运行在 http://localhost:3000" -ForegroundColor Green
}

启动服务后测试各端点:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 健康检查
Invoke-RestMethod http://localhost:3000/health | Format-List

# 获取项目列表(带认证头)
$headers = @{ Authorization = 'Bearer your-jwt-token-here' }
Invoke-RestMethod http://localhost:3000/api/items -Headers $headers

# 分页查询
Invoke-RestMethod 'http://localhost:3000/api/items?page=1&size=10' -Headers $headers

# 创建新项目
$body = @{ name = '项目 D' } | ConvertTo-Json
Invoke-RestMethod http://localhost:3000/api/items -Method Post -Headers $headers -Body $body -ContentType 'application/json'

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
status  : healthy
uptime : 00:15:32.0180000
version : 1.0.0
hostname: SERVER01

data : {@{id=1; name=项目 A; status=active}, @{id=2; name=项目 B;
status=archived}, @{id=3; name=项目 C; status=active}}
page : 1
size : 20
total : 3

id : 847
name : 项目 D
status : created
message : 项目创建成功

API 部署与运维

将 API 从开发脚本变成生产服务,需要考虑日志记录、健康检查、自动重启等运维需求。以下脚本展示了如何将 Pode API 注册为 Windows 服务,并添加运维监控端点。

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
# 将 API 注册为 Windows 服务
function Register-ApiService {
param(
[string]$Name = 'PowershellApi',
[string]$ScriptPath = 'C:\api\server.ps1'
)

# 使用 NSSM(Non-Sucking Service Manager)注册服务
$nssmPath = 'C:\tools\nssm.exe'
if (-not (Test-Path $nssmPath)) {
Write-Warning '请先安装 NSSM: choco install nssm'
return
}

# 注册服务
& $nssmPath install $Name pwsh.exe "-NoProfile -ExecutionPolicy Bypass -File `"$ScriptPath`""
& $nssmPath set $Name DisplayName "PowerShell REST API Service"
& $nssmPath set $Name Description "内部工具 REST API 服务"
& $nssmPath set $Name Start SERVICE_AUTO_START
& $nssmPath set $Name AppDirectory (Split-Path $ScriptPath)
& $nssmPath set $Name AppStdout (Join-Path (Split-Path $ScriptPath) 'stdout.log')
& $nssmPath set $Name AppStderr (Join-Path (Split-Path $ScriptPath) 'stderr.log')
& $nssmPath set $Name AppRotateFiles 1
& $nssmPath set $Name AppRotateBytes 10485760

Write-Host "服务 [$Name] 已注册,使用以下命令管理:" -ForegroundColor Green
Write-Host " 启动: nssm start $Name"
Write-Host " 停止: nssm stop $Name"
Write-Host " 状态: nssm status $Name"
Write-Host " 删除: nssm remove $Name confirm"
}

# 运维监控脚本:定时检查 API 可用性
function Test-ApiHealth {
param(
[string]$BaseUrl = 'http://localhost:3000',
[int]$IntervalSeconds = 30
)

$logFile = "C:\api\health-$(Get-Date -Format 'yyyyMMdd').log"

while ($true) {
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
try {
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$response = Invoke-RestMethod "$BaseUrl/health" -TimeoutSec 5
$sw.Stop()
$latency = $sw.ElapsedMilliseconds

$logEntry = "[$timestamp] OK | latency: ${latency}ms | uptime: $($response.uptime)"
Write-Host $logEntry -ForegroundColor Green
Add-Content -Path $logFile -Value $logEntry

} catch {
$logEntry = "[$timestamp] FAIL | error: $($_.Exception.Message)"
Write-Host $logEntry -ForegroundColor Red
Add-Content -Path $logFile -Value $logEntry

# 可选:自动重启服务
# Restart-Service -Name 'PowershellApi' -Force
}

Start-Sleep -Seconds $IntervalSeconds
}
}

# 一键部署函数
function Deploy-ApiService {
param([string]$ApiDir = 'C:\api')

# 创建目录
$dirs = @($ApiDir, "$ApiDir\logs", "$ApiDir\public")
$dirs | ForEach-Object {
if (-not (Test-Path $_)) {
New-Item -ItemType Directory -Path $_ -Force | Out-Null
}
}

# 部署最新代码
Copy-Item '.\api-server.ps1' "$ApiDir\server.ps1" -Force
Copy-Item '.\public\*' "$ApiDir\public\" -Recurse -Force

# 注册并启动服务
Register-ApiService -ScriptPath "$ApiDir\server.ps1"

Write-Host "部署完成!API 地址: http://localhost:3000" -ForegroundColor Green
Write-Host "健康检查: http://localhost:3000/health" -ForegroundColor Cyan
Write-Host "API 文档: http://localhost:3000/docs" -ForegroundColor Cyan
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
服务 [PowershellApi] 已注册,使用以下命令管理:
启动: nssm start PowershellApi
停止: nssm stop PowershellApi
状态: nssm status PowershellApi
删除: nssm remove PowershellApi confirm

[2026-01-14 10:00:00] OK | latency: 12ms | uptime: 00:45:12.0000000
[2026-01-14 10:00:30] OK | latency: 8ms | uptime: 00:45:42.0000000
[2026-01-14 10:01:00] OK | latency: 15ms | uptime: 00:46:12.0000000
[2026-01-14 10:01:30] FAIL | error: Unable to connect to the remote server
[2026-01-14 10:02:00] OK | latency: 10ms | uptime: 00:00:30.0000000

部署完成!API 地址: http://localhost:3000
健康检查: http://localhost:3000/health
API 文档: http://localhost:3000/docs

注意事项

  1. HttpListener 权限:在 Windows 上监听非 localhost 地址时,需要以管理员身份运行 netsh http add urlacl url=http://+:8080/ user=Everyone 来预留 URL 命名空间,否则会抛出”拒绝访问”异常。

  2. 并发处理HttpListenerGetContext() 是同步阻塞调用,只能一次处理一个请求。生产环境应使用 BeginGetContext / EndGetContext 异步模式,或者直接采用 Pode 框架——它内部已处理好并发问题。

  3. HTTPS 支持:生产环境务必启用 TLS。Pode 支持 Add-PodeEndpoint -ProtocolHttps -Certificate 参数直接绑定证书;HttpListener 则需要通过 netsh 绑定 SSL 证书到端口。

  4. 内存泄漏防范:长时间运行的 API 服务需要注意 IDisposable 对象(如 HttpListenerStreamReader)的释放。使用 try/finally 确保资源被正确清理,或使用 using 语句(PowerShell 7.0+ 支持)。

  5. 错误处理与日志:永远不要让未捕获的异常终止整个服务进程。在路由处理器中使用 try/catch,并将错误写入结构化日志文件,方便后续通过 ELK 或 Splunk 等工具分析。

  6. 跨平台部署:如果需要在 Linux 上运行,HttpListener 方案可以工作(依赖 .NET 的 Unix Socket 实现),但更推荐使用 Pode——它原生支持 Linux,可以搭配 systemd 或 Docker 容器化部署,实现与 Windows 服务对等的运维体验。

PowerShell 技能连载 - Pode Web API 开发

适用于 PowerShell 7.0 及以上版本

在日常运维和内部工具开发中,我们经常需要一个轻量级的 HTTP 服务来暴露数据或接收指令。传统做法是搭建 IIS、写 C# Web API 项目,但这对于一个简单的查询接口来说未免过于笨重。Pode 是一个纯 PowerShell 实现的跨平台 Web 框架,它内置了路由、中间件、认证、日志等功能,让你用熟悉的 PowerShell 语法就能快速构建 REST API。

Pode 的典型使用场景包括:为运维脚本提供 HTTP 调用入口、搭建内部微服务接口、接收 Webhook 回调、构建简单的管理仪表盘后端等。本文将从零开始,逐步演示如何用 Pode 搭建一个具备完整 CRUD 功能的任务管理 API。

基础 API 服务搭建

首先安装 Pode 模块并创建一个最基础的服务器,包含健康检查接口和路由组织结构。Pode 使用 Start-PodeServer 作为入口,通过 Add-PodeRoute 注册路由处理逻辑,整体风格类似于 Express.js 或 Flask。

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
# 安装 Pode 模块
Install-Module -Name Pode -Force -Scope CurrentUser

# 创建基础 API 服务器
Start-PodeServer {
# 监听本地 8090 端口
Add-PodeEndpoint -Address localhost -Port 8090 -Protocol Http

# 全局日志中间件 - 记录每个请求的方法、路径和耗时
Add-PodeMiddleware -Name 'RequestLogger' -ScriptBlock {
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$WebEvent.Data['__stopwatch'] = $sw
# 继续处理下一个中间件
}

Add-PodeMiddleware -Name 'ResponseTimer' -ScriptBlock {
$sw = $WebEvent.Data['__stopwatch']
if ($sw) {
$sw.Stop()
$elapsed = $sw.ElapsedMilliseconds
Write-PodeHost "[API] $($WebEvent.Method) $($WebEvent.Path) - ${elapsed}ms"
}
}

# GET /api/health - 健康检查端点
Add-PodeRoute -Method Get -Path '/api/health' -ScriptBlock {
Write-PodeJsonResponse -Value @{
status = 'healthy'
timestamp = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
version = '1.0.0'
machine = $env:COMPUTERNAME
pwsh = $PSVersionTable.PSVersion.ToString()
}
}

# GET /api/info - 服务器信息
Add-PodeRoute -Method Get -Path '/api/info' -ScriptBlock {
$os = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue
Write-PodeJsonResponse -Value @{
computer = $env:COMPUTERNAME
os = if ($os) { $os.Caption } else { 'Unknown' }
psVersion = $PSVersionTable.PSVersion.ToString()
podeVersion = (Get-Module Pode).Version.ToString()
uptime = if ($os) {
[math]::Round(((Get-Date) - $os.LastBootUpTime).TotalDays, 1)
} else { 0 }
}
}
}

启动服务器后,用浏览器或 Invoke-RestMethod 访问即可获取响应。

1
2
3
4
5
6
7
PS> Invoke-RestMethod http://localhost:8090/api/health

status : healthy
timestamp : 2025-10-20T08:00:00.123Z
version : 1.0.0
machine : WORKSTATION-01
pwsh : 7.4.6
1
2
3
4
5
6
7
PS> Invoke-RestMethod http://localhost:8090/api/info

computer : WORKSTATION-01
os : Microsoft Windows 11 Pro
psVersion : 7.4.6
podeVersion : 2.12.0
uptime : 15.3

内存数据存储与 CRUD 操作

接下来我们实现一个完整的任务管理 API,使用内存哈希表作为数据存储。Pode 支持 Add-PodeState 来维护服务端状态,这种状态在服务器运行期间持久存在于内存中。我们将实现标准的 RESTful 风格 CRUD 操作。

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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
Start-PodeServer {
Add-PodeEndpoint -Address localhost -Port 8090 -Protocol Http

# 初始化内存数据存储
Set-PodeState -Name 'Tasks' -Value @(
@{
Id = 1
Title = '部署生产环境补丁'
Description = '安装 2025 年 10 月安全更新'
Priority = 'High'
Status = 'Pending'
CreatedAt = '2025-10-18T09:00:00'
}
@{
Id = 2
Title = '备份数据库'
Description = '执行每周全量备份'
Priority = 'Medium'
Status = 'Completed'
CreatedAt = '2025-10-17T14:30:00'
}
)

# 设置下一个可用 ID
Set-PodeState -Name 'NextTaskId' -Value 3

# GET /api/tasks - 获取所有任务,支持筛选和分页
Add-PodeRoute -Method Get -Path '/api/tasks' -ScriptBlock {
$tasks = Get-PodeState -Name 'Tasks'
$status = $WebEvent.Query['status']
$priority = $WebEvent.Query['priority']
$limit = if ($WebEvent.Query['limit']) { [int]$WebEvent.Query['limit'] } else { 50 }

# 按条件筛选
$filtered = $tasks
if ($status) {
$filtered = $filtered | Where-Object { $_.Status -eq $status }
}
if ($priority) {
$filtered = $filtered | Where-Object { $_.Priority -eq $priority }
}

# 限制返回数量
$result = $filtered | Select-Object -First $limit

Write-PodeJsonResponse -Value @{
total = $filtered.Count
count = $result.Count
limit = $limit
data = @($result)
}
}

# GET /api/tasks/:id - 获取单个任务
Add-PodeRoute -Method Get -Path '/api/tasks/:id' -ScriptBlock {
$taskId = [int]$WebEvent.Parameters['id']
$tasks = Get-PodeState -Name 'Tasks'
$task = $tasks | Where-Object { $_.Id -eq $taskId } | Select-Object -First 1

if ($task) {
Write-PodeJsonResponse -Value $task
} else {
Set-PodeResponseStatus -Code 404
Write-PodeJsonResponse -Value @{
error = "Task with Id=$taskId not found"
}
}
}

# POST /api/tasks - 创建新任务
Add-PodeRoute -Method Post -Path '/api/tasks' -ScriptBlock {
$body = $WebEvent.Data
$nextId = Get-PodeState -Name 'NextTaskId'
$tasks = Get-PodeState -Name 'Tasks'

$newTask = @{
Id = $nextId
Title = $body.Title
Description = if ($body.Description) { $body.Description } else { '' }
Priority = if ($body.Priority) { $body.Priority } else { 'Medium' }
Status = 'Pending'
CreatedAt = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ss')
}

# 添加到列表并更新状态
$tasks = @($tasks) + $newTask
Set-PodeState -Name 'Tasks' -Value $tasks
Set-PodeState -Name 'NextTaskId' -Value ($nextId + 1)

Set-PodeResponseStatus -Code 201
Write-PodeJsonResponse -Value $newTask
}

# PUT /api/tasks/:id - 更新任务
Add-PodeRoute -Method Put -Path '/api/tasks/:id' -ScriptBlock {
$taskId = [int]$WebEvent.Parameters['id']
$body = $WebEvent.Data
$tasks = Get-PodeState -Name 'Tasks'
$found = $false

$updatedTasks = foreach ($t in $tasks) {
if ($t.Id -eq $taskId) {
$found = $true
@{
Id = $t.Id
Title = if ($body.Title) { $body.Title } else { $t.Title }
Description = if ($body.Description) { $body.Description } else { $t.Description }
Priority = if ($body.Priority) { $body.Priority } else { $t.Priority }
Status = if ($body.Status) { $body.Status } else { $t.Status }
CreatedAt = $t.CreatedAt
UpdatedAt = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ss')
}
} else {
$t
}
}

if ($found) {
Set-PodeState -Name 'Tasks' -Value @($updatedTasks)
$updated = $updatedTasks | Where-Object { $_.Id -eq $taskId } | Select-Object -First 1
Write-PodeJsonResponse -Value $updated
} else {
Set-PodeResponseStatus -Code 404
Write-PodeJsonResponse -Value @{
error = "Task with Id=$taskId not found"
}
}
}

# DELETE /api/tasks/:id - 删除任务
Add-PodeRoute -Method Delete -Path '/api/tasks/:id' -ScriptBlock {
$taskId = [int]$WebEvent.Parameters['id']
$tasks = Get-PodeState -Name 'Tasks'
$original = $tasks

$remaining = @($tasks | Where-Object { $_.Id -ne $taskId })

if ($remaining.Count -lt $original.Count) {
Set-PodeState -Name 'Tasks' -Value $remaining
Write-PodeJsonResponse -Value @{
message = "Task $taskId deleted"
remaining = $remaining.Count
}
} else {
Set-PodeResponseStatus -Code 404
Write-PodeJsonResponse -Value @{
error = "Task with Id=$taskId not found"
}
}
}
}

以下是调用这些 API 的示例。先创建一条新任务,再查询所有任务列表。

1
2
3
4
5
6
7
8
9
PS> $body = @{ Title = '检查 SSL 证书过期'; Priority = 'High' } | ConvertTo-Json
PS> Invoke-RestMethod -Method Post -Uri http://localhost:8090/api/tasks -Body $body -ContentType 'application/json'

Id : 3
Title : 检查 SSL 证书过期
Description :
Priority : High
Status : Pending
CreatedAt : 2025-10-20T08:15:00
1
2
3
4
5
6
PS> Invoke-RestMethod http://localhost:8090/api/tasks?status=Pending

total : 2
count : 2
limit : 50
data : {@{Id=1; Title=部署生产环境补丁; ...}, @{Id=3; Title=检查 SSL 证书过期; ...}}

JWT 认证与请求验证

在生产环境中,API 需要认证机制来保护敏感操作。Pode 内置了对 JWT (JSON Web 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
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
Start-PodeServer {
Add-PodeEndpoint -Address localhost -Port 8090 -Protocol Http

# 配置 JWT 认证 - 使用自定义密钥签名
New-PodeAuthScheme -ApiKey -Location Header -Name 'Authorization' |
Add-PodeAuth -Name 'JwtAuth' -Sessionless -ScriptBlock {
param($token)

# 移除 "Bearer " 前缀
if ($token.StartsWith('Bearer ')) {
$token = $token.Substring(7)
}

try {
# 验证 JWT(此处用对称密钥示例,生产环境应使用证书)
$parts = $token.Split('.')
if ($parts.Count -ne 3) {
return @{ Message = 'Invalid token format' }
}

# 解码 Payload(Base64Url)
$payloadBytes = [System.Convert]::FromBase64String(
$parts[1].Replace('-', '+').Replace('_', '/')
)
$payloadJson = [System.Text.Encoding]::UTF8.GetString($payloadBytes)
$payload = $payloadJson | ConvertFrom-Json

# 检查过期时间
if ($payload.exp -and $payload.exp -lt [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()) {
return @{ Message = 'Token expired' }
}

# 认证成功,返回用户信息
return @{
User = @{
Name = $payload.sub
Role = $payload.role
Issue = $payload.iss
}
}
} catch {
return @{ Message = "Token validation failed: $_" }
}
}

# POST /api/auth/login - 登录获取 Token
Add-PodeRoute -Method Post -Path '/api/auth/login' -ScriptBlock {
$body = $WebEvent.Data
$username = $body.Username
$password = $body.Password

# 简化示例 - 生产环境应查询数据库验证
$validUsers = @{
'admin' = @{ Password = 'P@ssw0rd'; Role = 'Admin' }
'reader' = @{ Password = 'Read0nly'; Role = 'Reader' }
}

if (-not $validUsers.ContainsKey($username)) {
Set-PodeResponseStatus -Code 401
Write-PodeJsonResponse -Value @{ error = 'Unknown user' }
return
}

if ($validUsers[$username].Password -ne $password) {
Set-PodeResponseStatus -Code 401
Write-PodeJsonResponse -Value @{ error = 'Invalid password' }
return
}

# 构建 JWT Payload
$header = @{ alg = 'HS256'; typ = 'JWT' } | ConvertTo-Json -Compress
$now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
$payload = @{
sub = $username
role = $validUsers[$username].Role
iss = 'pode-api-server'
iat = $now
exp = $now + 3600
} | ConvertTo-Json -Compress

# Base64Url 编码
$headerB64 = [System.Convert]::ToBase64String(
[System.Text.Encoding]::UTF8.GetBytes($header)
).TrimEnd('=').Replace('+', '-').Replace('/', '_')

$payloadB64 = [System.Convert]::ToBase64String(
[System.Text.Encoding]::UTF8.GetBytes($payload)
).TrimEnd('=').Replace('+', '-').Replace('/', '_')

# 生成签名(简化示例 - 生产环境应使用 HMAC-SHA256)
$secret = 'my-secret-key-2025'
$signInput = "$headerB64.$payloadB64"
$hmac = [System.Security.Cryptography.HMACSHA256]::new(
[System.Text.Encoding]::UTF8.GetBytes($secret)
)
$signatureBytes = $hmac.ComputeHash(
[System.Text.Encoding]::UTF8.GetBytes($signInput)
)
$signatureB64 = [System.Convert]::ToBase64String($signatureBytes).
TrimEnd('=').Replace('+', '-').Replace('/', '_')

$jwt = "$headerB64.$payloadB64.$signatureB64"

Write-PodeJsonResponse -Value @{
token = $jwt
tokenType = 'Bearer'
expiresIn = 3600
role = $validUsers[$username].Role
}
}

# 请求体验证中间件
Add-PodeMiddleware -Name 'TaskValidation' -Route '/api/tasks' -ScriptBlock {
if ($WebEvent.Method -in @('Post', 'Put')) {
$body = $WebEvent.Data
$errors = @()

if (-not $body.Title -or $body.Title.Trim().Length -eq 0) {
$errors += 'Title is required'
}
if ($body.Title -and $body.Title.Length -gt 200) {
$errors += 'Title must not exceed 200 characters'
}
if ($body.Priority -and $body.Priority -notin @('Low', 'Medium', 'High', 'Critical')) {
$errors += "Priority must be one of: Low, Medium, High, Critical"
}
if ($body.Status -and $body.Status -notin @('Pending', 'InProgress', 'Completed', 'Cancelled')) {
$errors += "Status must be one of: Pending, InProgress, Completed, Cancelled"
}

if ($errors.Count -gt 0) {
Set-PodeResponseStatus -Code 400
Write-PodeJsonResponse -Value @{
error = 'Validation failed'
detail = $errors
}
# 阻止请求继续传递到路由处理
return $false
}
}
}

# 需要认证的受保护路由 - GET /api/tasks
Add-PodeRoute -Method Get -Path '/api/tasks' -Authentication 'JwtAuth' -ScriptBlock {
$tasks = Get-PodeState -Name 'Tasks'
Write-PodeJsonResponse -Value @{
user = $WebEvent.Auth.User.Name
count = $tasks.Count
data = @($tasks)
}
}
}

下面演示完整的认证流程:先登录获取 Token,再用 Token 访问受保护的 API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PS> $cred = @{ Username = 'admin'; Password = 'P@ssw0rd' } | ConvertTo-Json
PS> $resp = Invoke-RestMethod -Method Post -Uri http://localhost:8090/api/auth/login -Body $cred -ContentType 'application/json'

token : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIi...
tokenType : Bearer
expiresIn : 3600
role : Admin

PS> $headers = @{ Authorization = "Bearer $($resp.token)" }
PS> Invoke-RestMethod -Uri http://localhost:8090/api/tasks -Headers $headers

user : admin
count : 3
data : {@{Id=1; Title=部署生产环境补丁; ...}, ...}
1
2
3
PS> # 不带 Token 访问受保护路由会被拒绝
PS> Invoke-RestMethod http://localhost:8090/api/tasks
Invoke-RestMethod: 401 Unauthorized
1
2
3
4
PS> # 提交无效数据会触发验证中间件
PS> $bad = @{ Title = ''; Priority = 'Invalid' } | ConvertTo-Json
PS> Invoke-RestMethod -Method Post -Uri http://localhost:8090/api/tasks -Body $bad -ContentType 'application/json'
Invoke-RestMethod: 400 Bad Request

注意事项

  1. 生产环境应使用 HTTPS:Pode 支持通过 Add-PodeEndpoint -Protocol Https -Certificate 配置 SSL/TLS 证书。在公网部署时务必启用 HTTPS,避免认证凭据和 API 数据以明文传输。

  2. 状态存储不跨重启Set-PodeState 维护的数据存储在内存中,服务器重启后数据丢失。如需持久化,应结合文件、SQLite 或外部数据库。Pode 社区有 Pode.Web 和各种数据库连接器可以配合使用。

  3. 并发与线程模型:Pode 默认使用多线程处理请求(通过 runspace pool),但 Get-PodeState / Set-PodeState 对共享状态的访问已内置线程安全机制。如果直接操作全局变量或文件,需要自行处理并发问题。

  4. JWT 示例仅为演示:本文中的 JWT 实现是简化版本,仅用于说明 Pode 认证流程的工作原理。在实际项目中,应使用成熟的 JWT 库(如 System.IdentityModel.Tokens.Jwt)来生成和验证令牌,并妥善保管签名密钥。

  5. 错误处理应覆盖全面:API 中每个可能失败的操作都应有 try/catch 保护,避免未处理异常导致整个请求线程崩溃。建议在全局中间件中统一捕获异常并返回标准化的错误响应格式。

  6. 跨平台运行:Pode 完全基于 PowerShell 编写,在 Windows、Linux 和 macOS 上均可运行。使用 dotnet 命令或 Docker 容器即可将 Pode API 部署到 Linux 服务器,适合构建轻量级的跨平台运维微服务。

PowerShell 技能连载 - REST API 设计与实现

适用于 PowerShell 7.0 及以上版本

PowerShell 不仅能调用 API,还能创建 API。PolarisPode 等模块让 PowerShell 可以快速搭建 Web 服务器,处理 HTTP 请求。虽然不建议用 PowerShell 替代专业的 Web 框架,但在内网工具、运维自动化接口、轻量级中间服务等场景中,PowerShell API 服务器非常实用——快速实现一个健康检查接口、暴露系统信息给监控平台、提供配置查询 API。

本文将讲解如何使用 PowerShell 构建轻量级 REST API 服务。

使用 Pode 搭建 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
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
# 安装 Pode 模块
Install-Module -Name Pode -Force -Scope CurrentUser

# 创建 API 服务器脚本
$startApi = {
Start-PodeServer {
Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http

# 日志中间件
Add-PodeMiddleware -Name 'Logger' -ScriptBlock {
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$method = $WebEvent.Method
$path = $WebEvent.Path
Write-PodeHost "[$timestamp] $method $path"
}

# GET /api/health —— 健康检查
Add-PodeRoute -Method Get -Path '/api/health' -ScriptBlock {
$os = Get-CimInstance Win32_OperatingSystem
$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
$uptime = [math]::Round(((Get-Date) - $os.LastBootUpTime).TotalDays, 1)

Write-PodeJsonResponse -Value @{
status = 'healthy'
timestamp = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ss')
computer = $env:COMPUTERNAME
uptime = "$uptime 天"
cpuUsage = "$($cpu.LoadPercentage)%"
memoryUsage = "$([math]::Round(($os.TotalVisibleMemorySize - $os.FreePhysicalMemory) / $os.TotalVisibleMemorySize * 100, 1))%"
}
}

# GET /api/processes —— 进程列表
Add-PodeRoute -Method Get -Path '/api/processes' -ScriptBlock {
$top = if ($WebEvent.Query['top']) { [int]$WebEvent.Query['top'] } else { 10 }
$sortBy = if ($WebEvent.Query['sort']) { $WebEvent.Query['sort'] } else { 'Memory' }

$processes = Get-Process | Sort-Object WorkingSet64 -Descending |
Select-Object -First $top |
ForEach-Object {
@{
name = $_.Name
pid = $_.Id
memoryMB = [math]::Round($_.WorkingSet64 / 1MB, 1)
cpu = [math]::Round($_.CPU, 2)
}
}

Write-PodeJsonResponse -Value @{
count = $processes.Count
data = $processes
}
}

# GET /api/disks —— 磁盘信息
Add-PodeRoute -Method Get -Path '/api/disks' -ScriptBlock {
$disks = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" |
ForEach-Object {
$totalGB = [math]::Round($_.Size / 1GB, 2)
$freeGB = [math]::Round($_.FreeSpace / 1GB, 2)
@{
drive = $_.DeviceID
totalGB = $totalGB
freeGB = $freeGB
usagePct = [math]::Round(($_.Size - $_.FreeSpace) / $_.Size * 100, 1)
}
}

Write-PodeJsonResponse -Value @{ data = $disks }
}
}
}

Write-Host "API 服务器启动中..." -ForegroundColor Cyan
Write-Host "端点:" -ForegroundColor Yellow
Write-Host " GET http://localhost:8080/api/health" -ForegroundColor Green
Write-Host " GET http://localhost:8080/api/processes?top=5" -ForegroundColor Green
Write-Host " GET http://localhost:8080/api/disks" -ForegroundColor Green

执行结果示例:

1
2
3
4
5
6
API 服务器启动中...
端点:
GET http://localhost:8080/api/health
GET http://localhost:8080/api/processes?top=5
GET http://localhost:8080/api/disks
[2025-07-23 08:30:15] GET /api/health

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
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
# 在同一 Pode 服务器中添加 POST 接口
$apiWithPost = {
Start-PodeServer {
Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http

# POST /api/execute —— 执行命令(需要认证)
Add-PodeRoute -Method Post -Path '/api/execute' -ScriptBlock {
$body = $WebEvent.Data

if (-not $body.command) {
Set-PodeResponseStatus -Code 400
Write-PodeJsonResponse -Value @{ error = '缺少 command 参数' }
return
}

# 安全限制:只允许特定命令
$allowed = @('Get-Process', 'Get-Service', 'Get-EventLog', 'Get-CimInstance')
$cmdName = ($body.command -split '\s+')[0]
if ($cmdName -notin $allowed) {
Set-PodeResponseStatus -Code 403
Write-PodeJsonResponse -Value @{ error = "命令不允许:$cmdName" }
return
}

try {
$result = Invoke-Expression $body.command -ErrorAction Stop
$jsonResult = $result | ConvertTo-Json -Depth 3 -Compress

Write-PodeJsonResponse -Value @{
success = $true
command = $body.command
result = $result
}
} catch {
Set-PodeResponseStatus -Code 500
Write-PodeJsonResponse -Value @{
success = $false
error = $_.Exception.Message
}
}
}

# POST /api/alert —— 接收告警
Add-PodeRoute -Method Post -Path '/api/alert' -ScriptBlock {
$body = $WebEvent.Data

$alertInfo = @{
timestamp = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ss')
source = $body.source
level = $body.level
message = $body.message
hostname = $body.hostname
receivedAt = Get-Date -Format 'HH:mm:ss'
}

# 记录到文件
$alertInfo | ConvertTo-Json | Add-Content "C:\Logs\alerts.json" -Encoding UTF8

Write-PodeJsonResponse -Value @{
received = $true
id = [guid]::NewGuid().ToString()
}
}

# GET /api/alerts —— 查询告警
Add-PodeRoute -Method Get -Path '/api/alerts' -ScriptBlock {
$count = if ($WebEvent.Query['count']) { [int]$WebEvent.Query['count'] } else { 10 }
$level = $WebEvent.Query['level']

if (Test-Path "C:\Logs\alerts.json") {
$alerts = Get-Content "C:\Logs\alerts.json" -Tail $count |
ConvertFrom-Json |
Where-Object { if ($level) { $_.level -eq $level } else { $true } }

Write-PodeJsonResponse -Value @{
count = $alerts.Count
data = $alerts
}
} else {
Write-PodeJsonResponse -Value @{ count = 0; data = @() }
}
}
}
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
# 调用 POST 接口
$body = @{ command = "Get-Process | Select-Object -First 3 Name,Id" } | ConvertTo-Json
Invoke-RestMethod -Uri "http://localhost:8081/api/execute" -Method Post -Body $body -ContentType "application/json"

# 发送告警
$alert = @{
source = "monitor"
level = "ERROR"
message = "磁盘空间不足"
hostname = "SRV01"
} | ConvertTo-Json
Invoke-RestMethod -Uri "http://localhost:8081/api/alert" -Method Post -Body $alert -ContentType "application/json"

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
28
29
30
31
32
33
34
35
36
37
38
39
40
# 封装为可复用的 API 客户端函数
function Get-ServerHealth {
param([string]$ServerUrl = "http://localhost:8080")

try {
$response = Invoke-RestMethod -Uri "$ServerUrl/api/health" -TimeoutSec 5
return $response
} catch {
Write-Host "健康检查失败:$($_.Exception.Message)" -ForegroundColor Red
return $null
}
}

function Get-ServerProcesses {
param(
[string]$ServerUrl = "http://localhost:8080",
[int]$Top = 10
)

return Invoke-RestMethod -Uri "$ServerUrl/api/processes?top=$Top" -TimeoutSec 10
}

# 批量检查多台服务器
$servers = @(
"http://web-01:8080",
"http://web-02:8080",
"http://db-01:8080"
)

Write-Host "集群健康检查:" -ForegroundColor Cyan
foreach ($server in $servers) {
$health = Get-ServerHealth -ServerUrl $server
if ($health) {
$status = $health.status
$color = if ($status -eq "healthy") { "Green" } else { "Red" }
Write-Host " $server : $status (CPU: $($health.cpuUsage), MEM: $($health.memoryUsage))" -ForegroundColor $color
} else {
Write-Host " $server : 不可达" -ForegroundColor Red
}
}

执行结果示例:

1
2
3
4
集群健康检查:
http://web-01:8080 : healthy (CPU: 35%, MEM: 72.4%)
http://web-02:8080 : healthy (CPU: 28%, MEM: 65.1%)
http://db-01:8080 : healthy (CPU: 42%, MEM: 85.3%)

注意事项

  1. 安全风险:暴露命令执行接口极度危险,生产环境必须加认证、限白名单、审计日志
  2. 性能限制:PowerShell HTTP 服务器性能有限,不适合高并发场景(建议使用反向代理)
  3. 认证:生产 API 必须添加认证(API Key、JWT、Basic Auth),Pode 内置认证中间件
  4. HTTPS:生产环境使用 HTTPS,Pode 支持自签名证书和 Let’s Encrypt
  5. 服务化:长期运行的 API 应注册为 Windows 服务,使用 nssmRegister-PodeService
  6. 错误处理:API 接口必须有完善的错误处理和统一的错误响应格式

PowerShell 技能连载 - 网络编程与 REST API

适用于 PowerShell 5.1 及以上版本

在现代运维和自动化场景中,与 REST API 交互已成为 PowerShell 的核心能力之一——无论是调用云服务 API 管理 Azure/AWS 资源、与 GitHub/GitLab 交互管理代码仓库、对接企业内部的 ITSM 系统创建工单,还是查询第三方服务获取天气、汇率等数据,都离不开 HTTP 请求。PowerShell 内置的 Invoke-RestMethodInvoke-WebRequest 提供了强大且易用的 HTTP 客户端功能。

本文将从基础 HTTP 请求讲起,逐步深入到认证、JSON 处理、分页请求和错误重试等进阶话题。

基础 HTTP 请求

PowerShell 提供两个主要的 HTTP 命令:Invoke-RestMethod(自动解析响应)和 Invoke-WebRequest(返回原始响应对象):

1
2
3
4
5
6
7
8
9
10
11
12
13
# GET 请求(Invoke-RestMethod 自动解析 JSON)
$response = Invoke-RestMethod -Uri 'https://jsonplaceholder.typicode.com/posts/1'
$response | Format-List

# GET 请求(Invoke-WebRequest 返回完整响应对象)
$webResponse = Invoke-WebRequest -Uri 'https://jsonplaceholder.typicode.com/posts/1'
Write-Host "状态码:$($webResponse.StatusCode)"
Write-Host "内容类型:$($webResponse.Headers.'Content-Type')"
Write-Host "内容长度:$($webResponse.Headers.'Content-Length') 字节"

# 将响应内容转换为 JSON 对象
$content = $webResponse.Content | ConvertFrom-Json
$content | Select-Object id, title

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
userId : 1
id : 1
title : sunt aut facere repellat provident occaecati excepturi optio
body : quia et suscipit...

状态码:200
内容类型:application/json; charset=utf-8
内容长度:292 字节

id title
-- -----
1 sunt aut facere repellat provident occaecati excepturi optio

POST/PUT/DELETE 请求

REST API 的核心是 CRUD 操作——创建(POST)、读取(GET)、更新(PUT)、删除(DELETE):

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
# POST 请求(创建资源)
$newPost = @{
title = 'PowerShell REST API 指南'
body = '这是一篇关于 PowerShell 调用 REST API 的文章'
userId = 1
} | ConvertTo-Json

$created = Invoke-RestMethod -Uri 'https://jsonplaceholder.typicode.com/posts' `
-Method Post `
-Body $newPost `
-ContentType 'application/json; charset=utf-8'

Write-Host "已创建文章,ID: $($created.id)"
$created | Format-List

# PUT 请求(更新资源)
$updateData = @{
id = 1
title = '更新后的标题'
body = '更新后的内容'
userId = 1
} | ConvertTo-Json

$updated = Invoke-RestMethod -Uri 'https://jsonplaceholder.typicode.com/posts/1' `
-Method Put `
-Body $updateData `
-ContentType 'application/json; charset=utf-8'

# DELETE 请求
$deleteResponse = Invoke-RestMethod -Uri 'https://jsonplaceholder.typicode.com/posts/1' `
-Method Delete
Write-Host "删除状态码:$($deleteResponse.StatusCode)"

执行结果示例:

1
2
3
4
5
6
已创建文章,ID: 101

title : PowerShell REST API 指南
body : 这是一篇关于 PowerShell 调用 REST API 的文章
userId : 1
id : 101

认证方式

大多数生产 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
28
29
30
31
32
33
34
35
36
# 方式一:Bearer Token 认证
$token = 'ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
$headers = @{
Authorization = "Bearer $token"
Accept = 'application/vnd.github.v3+json'
}

$repos = Invoke-RestMethod -Uri 'https://api.github.com/user/repos' `
-Headers $headers
$repos | Select-Object name, private, language, stargazers_count |
Format-Table -AutoSize

# 方式二:API Key 作为查询参数
$apiKey = 'your-api-key'
$weather = Invoke-RestMethod -Uri "https://api.openweathermap.org/data/2.5/weather?q=Beijing&appid=$apiKey&units=metric"
Write-Host "北京当前温度:$($weather.main.temp)°C"
Write-Host "天气描述:$($weather.weather[0].description)"

# 方式三:基本认证
$cred = Get-Credential
Invoke-RestMethod -Uri 'https://api.example.com/data' `
-Authentication Basic `
-Credential $cred

# 方式四:OAuth2 客户端凭据流
$tokenResponse = Invoke-RestMethod -Uri 'https://login.microsoftonline.com/tenant-id/oauth2/v2.0/token' `
-Method Post `
-Body @{
client_id = 'app-client-id'
client_secret = 'app-client-secret'
scope = 'https://graph.microsoft.com/.default'
grant_type = 'client_credentials'
}

$accessToken = $tokenResponse.access_token
Write-Host "获取到 Token,有效期:$($tokenResponse.expires_in) 秒"

执行结果示例:

1
2
3
4
5
6
7
8
9
name             private language stargazers_count
---- -------- -------- ----------------
my-project False PowerShell 12
internal-tools True C# 5

北京当前温度:22.3°C
天气描述:晴

获取到 Token,有效期:3599

JSON 处理进阶

PowerShell 的 JSON 处理有一些常见的陷阱,了解它们可以避免很多调试时间:

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
# ConvertTo-Json 的深度问题(默认只序列化 2 层)
$data = @{
Level1 = @{
Level2 = @{
Level3 = '深层嵌套数据'
}
}
}

# 默认深度 2,Level3 会丢失
$json shallow = $data | ConvertTo-Json
Write-Host "默认深度:`n$jsonshallow"

# 设置深度为 10,保留完整结构
$jsonDeep = $data | ConvertTo-Json -Depth 10
Write-Host "`n深度 10:`n$jsonDeep"

# 处理数组中的空值
$arrayData = @(
@{ Name = 'Item1'; Value = 100 }
@{ Name = 'Item2'; Value = $null }
@{ Name = 'Item3'; Value = 300 }
)

# 默认会忽略空值属性,使用 -EnumsAsStrings 保持完整
$jsonArray = $arrayData | ConvertTo-Json -Depth 5
Write-Host "`n数组 JSON:`n$jsonArray"

# 从 API 响应中提取嵌套数据
$response = Invoke-RestMethod -Uri 'https://jsonplaceholder.typicode.com/users'
$response | Select-Object name, email,
@{N='城市'; E={$_.address.city}},
@{N='公司'; E={$_.company.name}} |
Format-Table -AutoSize

执行结果示例:

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
默认深度:
{
"Level1": {
"Level2": "System.Collections.Hashtable"
}
}

深度 10:
{
"Level1": {
"Level2": {
"Level3": "深层嵌套数据"
}
}
}

数组 JSON:
[
{ "Name": "Item1", "Value": 100 },
{ "Name": "Item2" },
{ "Name": "Item3", "Value": 300 }
]

name email 城市 公司
---- ----- ---- ----
Leanne Graham Sincere@april.biz Gwenborough Romaguera-Crona
Ervin Howell Shanna@melissa.tv Wisokyburgh Deckow-Crist

分页请求处理

当 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
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
function Invoke-RestMethodPaged {
<#
.SYNOPSIS
自动处理分页 API 请求
#>
param(
[Parameter(Mandatory)]
[string]$Uri,

[int]$PageSize = 100,
[int]$MaxPages = 100,
[hashtable]$Headers,
[string]$PageParam = 'page',
[string]$PerPageParam = 'per_page'
)

$allResults = [System.Collections.Generic.List[PSObject]]::new()
$page = 1

while ($page -le $MaxPages) {
$separator = if ($Uri -match '\?') { '&' } else { '?' }
$pageUri = "${Uri}${separator}${PerPageParam}=${PageSize}&${PageParam}=${page}"

$params = @{
Uri = $pageUri
Method = 'Get'
}
if ($Headers) { $params.Headers = $Headers }

try {
$response = Invoke-RestMethod @params
} catch {
Write-Warning "第 ${page} 页请求失败:$($_.Exception.Message)"
break
}

if (-not $response -or $response.Count -eq 0) {
break
}

$allResults.AddRange($response)
Write-Host "已获取第 ${page} 页,累计 $($allResults.Count) 条" -ForegroundColor Cyan

if ($response.Count -lt $PageSize) {
break
}

$page++
}

Write-Host "`n总计获取 $($allResults.Count) 条数据" -ForegroundColor Green
return $allResults
}

# 示例:获取 GitHub 仓库的所有 Issues
$issues = Invoke-RestMethodPaged `
-Uri 'https://api.github.com/repos/PowerShell/PowerShell/issues' `
-Headers @{ Authorization = "Bearer $token" } `
-PageSize 50

$issues | Select-Object number, title, state,
@{N='labels'; E={$_.labels.name -join ', '}} |
Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
已获取第 1 页,累计 50
已获取第 2 页,累计 100
已获取第 3 页,累计 112

总计获取 112 条数据

number title state labels
------ ----- ----- -------
24501 Fix null coalescing in pipelines open Bug, Help Wanted
24498 Add -AsHashtable to ConvertFrom... open Enhancement
24495 Update ReadMe with new links closed Documentation

错误处理与重试

网络请求不可避免会遇到超时、限流等问题。良好的错误处理和重试机制是健壮 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
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
function Invoke-ApiWithRetry {
<#
.SYNOPSIS
带重试机制的 API 调用
#>
param(
[Parameter(Mandatory)]
[string]$Uri,

[ValidateSet('Get','Post','Put','Delete','Patch')]
[string]$Method = 'Get',

$Body,
[hashtable]$Headers,
[int]$MaxRetries = 3,
[int]$RetryDelaySeconds = 2
)

$attempt = 0

while ($attempt -lt $MaxRetries) {
$attempt++
try {
$params = @{
Uri = $Uri
Method = $Method
}
if ($Body) { $params.Body = $Body }
if ($Headers) { $params.Headers = $Headers }

$response = Invoke-RestMethod @params -ErrorAction Stop
return $response

} catch {
$statusCode = $null
if ($_.Exception.Response) {
$statusCode = [int]$_.Exception.Response.StatusCode
}

if ($statusCode -eq 429) {
# 限流:使用 Retry-After 头的值
$retryAfter = $_.Exception.Response.Headers['Retry-After']
$waitTime = if ($retryAfter) { [int]$retryAfter } else { $RetryDelaySeconds * 2 }
Write-Warning "API 限流 (429),等待 ${waitTime} 秒后重试(第 ${attempt} 次)"
Start-Sleep -Seconds $waitTime
}
elseif ($statusCode -ge 500) {
# 服务器错误:指数退避重试
$waitTime = $RetryDelaySeconds * [math]::Pow(2, $attempt - 1)
Write-Warning "服务器错误 ($statusCode),${waitTime} 秒后重试(第 ${attempt} 次)"
Start-Sleep -Seconds $waitTime
}
elseif ($statusCode -ge 400) {
# 客户端错误:不重试
Write-Error "请求失败 ($statusCode):$($_.Exception.Message)"
return $null
}
else {
# 其他错误(网络超时等):重试
Write-Warning "请求异常:$($_.Exception.Message)(第 ${attempt} 次)"
Start-Sleep -Seconds $RetryDelaySeconds
}
}
}

Write-Error "已达最大重试次数 ($MaxRetries),请求失败"
return $null
}

# 使用带重试的 API 调用
$result = Invoke-ApiWithRetry -Uri 'https://api.github.com/rate_limit' `
-Headers @{ Authorization = "Bearer $token" } `
-MaxRetries 3

执行结果示例:

1
2
WARNING: API 限流 (429),等待 60 秒后重试(第 1 次)
WARNING: 服务器错误 (503),2 秒后重试(第 2 次)

注意事项

  1. TLS 版本:某些 API 要求 TLS 1.2,使用 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 强制设置
  2. JSON 序列化深度ConvertTo-Json 默认深度为 2,嵌套对象会被截断,务必根据实际结构调整 -Depth 参数
  3. 编码问题:发送中文内容时,确保 -ContentType 包含 charset=utf-8,避免乱码
  4. 大型响应:处理大量数据时,考虑使用流式读取或将结果分页处理,避免一次性加载到内存
  5. 敏感信息:不要在脚本中硬编码 API Key 和 Token,使用环境变量或 Azure Key Vault 等安全存储
  6. 超时设置:生产环境建议设置合理的超时 -TimeoutSec,避免请求无限等待

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 REST API高级集成技术

OAuth2认证流程

1
2
3
4
5
6
7
8
9
10
11
12
13
$tokenParams = @{
Uri = 'https://login.microsoftonline.com/tenant/oauth2/v2.0/token'
Method = 'POST'
Body = @{
client_id = $clientId
scope = 'https://graph.microsoft.com/.default'
client_secret = $secret
grant_type = 'client_credentials'
}
}

$token = Invoke-RestMethod @tokenParams
$headers = @{Authorization = "Bearer $($token.access_token)"}

分页数据获取

1
2
3
4
5
6
$result = while ($true) {
$response = Invoke-RestMethod -Uri $url -Headers $headers
$response.value
if (-not $response.'@odata.nextLink') { break }
$url = $response.'@odata.nextLink'
}

错误重试机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Invoke-RetryRequest {
param(
[scriptblock]$Action,
[int]$MaxRetries = 3
)

$attempt = 0
do {
try {
return & $Action
}
catch {
$attempt++
if ($attempt -ge $MaxRetries) { throw }
Start-Sleep -Seconds (2 * $attempt)
}
} while ($true)
}

最佳实践

  1. 令牌缓存与自动刷新
  2. 请求速率限制处理
  3. 异步批量数据处理
  4. 响应数据验证机制

安全规范

  • 敏感信息加密存储
  • 使用HTTPS严格模式
  • 限制API权限范围
  • 定期轮换访问凭证