Microsoft Agent Framework脚本执行与自动化实战
1. Microsoft Agent Framework 技能执行脚本实战指南Microsoft Agent Framework 是一个强大的自动化工具集它允许开发者创建和执行各种技能Skills来扩展系统的功能。在这个实战指南中我将分享如何利用这个框架来执行脚本实现自动化任务。1.1 框架概述与核心概念Microsoft Agent Framework 的核心在于其模块化设计。技能Skills是这个框架的基本构建块每个技能都是一个独立的、可重用的功能单元。这些技能可以像乐高积木一样组合起来构建复杂的自动化流程。脚本执行是其中最基础也最强大的功能之一。通过脚本我们可以实现自动化重复性任务集成不同系统间的数据流扩展框架的默认功能实现复杂的业务逻辑提示虽然框架本身功能强大但真正发挥其潜力的是通过脚本实现的定制化功能。这也是为什么掌握脚本执行技巧如此重要。1.2 环境准备与基础配置在开始之前我们需要确保开发环境准备就绪。以下是基础配置步骤安装框架核心组件Install-Module -Name MicrosoftAgentFramework -AllowClobber -Force验证安装Get-Module -Name MicrosoftAgentFramework -ListAvailable初始化技能目录New-Item -Path C:\AgentSkills -ItemType Directory配置执行策略仅限首次使用Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser注意执行策略设置会影响脚本运行权限建议在生产环境中根据安全要求进行更严格的配置。2. 技能开发基础2.1 创建第一个技能让我们从创建一个简单的Hello World技能开始在技能目录中创建新文件New-Item -Path C:\AgentSkills\HelloWorld.ps1 -ItemType File编辑文件内容param( [string]$Name World ) Write-Output Hello, $Name!注册技能到框架Register-AgentSkill -Name HelloWorld -Path C:\AgentSkills\HelloWorld.ps1 -Description 简单的问候技能测试技能Invoke-AgentSkill -Name HelloWorld -Parameters {NameDeveloper}2.2 技能参数处理高级技能通常需要处理多种参数类型。框架支持以下参数类型参数类型示例说明字符串[string]$Input基本文本输入整数[int]$Count数值参数布尔值[bool]$Force开关参数数组[array]$Items多值输入哈希表[hashtable]$Options复杂配置最佳实践为所有参数设置默认值添加参数验证逻辑使用注释说明参数用途对敏感参数进行加密处理3. 高级脚本执行技巧3.1 错误处理与日志记录健壮的脚本必须包含完善的错误处理机制。框架提供了多种错误处理方式try { # 可能出错的代码 $result Invoke-Command -ScriptBlock { ... } # 记录成功日志 Write-AgentLog -Message 操作成功 -Level Information -Data $result } catch { # 捕获特定异常 if ($_.Exception -is [System.IO.IOException]) { Write-AgentLog -Message IO错误: $($_.Exception.Message) -Level Error } else { # 通用错误处理 Write-AgentLog -Message 未预期错误: $($_.Exception) -Level Critical throw # 重新抛出异常 } } finally { # 清理资源 if ($null -ne $resource) { $resource.Dispose() } }3.2 性能优化技巧脚本性能直接影响自动化效率。以下是一些关键优化点批量处理代替循环# 不佳做法 foreach ($item in $items) { Process-Item $item } # 更好做法 $items | Process-Item -Batch并行执行$jobs () foreach ($task in $tasks) { $jobs Start-ThreadJob -ScriptBlock { param($t) Execute-Task $t } -ArgumentList $task } $results $jobs | Receive-Job -Wait -AutoRemoveJob缓存常用数据$cache {} if (-not $cache.ContainsKey($key)) { $cache[$key] Get-ExpensiveData $key }4. 实战案例自动化部署技能4.1 场景分析假设我们需要创建一个自动化部署技能实现以下功能从版本控制系统获取代码运行构建脚本执行单元测试部署到目标环境发送部署通知4.2 技能实现param( [Parameter(Mandatory$true)] [string]$RepositoryUrl, [Parameter(Mandatory$true)] [string]$Branch, [string]$Environment Staging, [bool]$RunTests $true, [string]$NotificationEmail ) # 1. 准备工作目录 $workspace New-Workspace -Name Deploy_$(Get-Date -Format yyyyMMddHHmmss) try { # 2. 获取代码 Clone-Repository -Url $RepositoryUrl -Branch $Branch -Destination $workspace # 3. 安装依赖 Invoke-Build -Path $workspace\install.ps1 # 4. 运行构建 $buildResult Invoke-Build -Path $workspace\build.ps1 # 5. 执行测试如果启用 if ($RunTests) { $testResult Invoke-Test -Path $workspace\tests if ($testResult.Failed -gt 0) { throw $($testResult.Failed) 个测试失败部署中止 } } # 6. 部署到环境 Deploy-Artifact -BuildResult $buildResult -Environment $Environment # 7. 发送通知 if ($NotificationEmail) { Send-DeploymentNotification -Email $NotificationEmail -Status Success -Environment $Environment } Write-Output 部署成功完成 } catch { # 错误处理 if ($NotificationEmail) { Send-DeploymentNotification -Email $NotificationEmail -Status Failed -Environment $Environment -Error $_ } throw } finally { # 清理工作目录 if (Test-Path $workspace) { Remove-Item $workspace -Recurse -Force } }4.3 技能注册与使用注册这个部署技能Register-AgentSkill -Name AutoDeploy -Path C:\AgentSkills\AutoDeploy.ps1 -Description 自动化部署技能 -Parameters { RepositoryUrl Git仓库URL; Branch 分支名称; Environment 目标环境; RunTests 是否运行测试; NotificationEmail 通知邮箱 }使用示例Invoke-AgentSkill -Name AutoDeploy -Parameters { RepositoryUrl https://github.com/example/repo.git Branch main Environment Production NotificationEmail teamexample.com }5. 技能管理与维护5.1 技能生命周期管理框架提供完整的技能管理命令集命令用途示例Get-AgentSkill列出所有技能Get-AgentSkill -Name Deploy*Register-AgentSkill注册新技能见上文示例Update-AgentSkill更新技能Update-AgentSkill -Name Hello -Path new_path.ps1Unregister-AgentSkill移除技能Unregister-AgentSkill -Name OldSkillEnable-AgentSkill启用技能Enable-AgentSkill -Name BackupDisable-AgentSkill禁用技能Disable-AgentSkill -Name TempSkill5.2 技能版本控制对于生产环境建议实现技能版本控制在技能脚本中添加版本信息# 技能元数据 $SkillMetadata { Name AutoDeploy Version 1.0.2 Author DevTeam LastUpdated 2023-11-15 }使用Git管理技能代码# 初始化技能仓库 git init C:\AgentSkills # 添加新技能 git add AutoDeploy.ps1 git commit -m 添加AutoDeploy技能 v1.0.2 # 创建发布标签 git tag -a v1.0.2 -m 稳定版本实现自动版本检查$latestVersion Get-LatestSkillVersion -Name AutoDeploy if ($SkillMetadata.Version -lt $latestVersion) { Write-Warning 发现新版本技能 ($latestVersion)建议更新 }6. 安全最佳实践6.1 权限管理脚本执行涉及系统安全必须严格控制最小权限原则# 创建专用执行账户 $executionAccount New-LocalUser -Name AgentRunner -Password (ConvertTo-SecureString ComplexPssw0rd -AsPlainText -Force) # 限制账户权限 Add-LocalGroupMember -Group Remote Management Users -Member AgentRunner凭据安全存储# 加密存储凭据 $credential Get-Credential $credential | Export-Clixml -Path C:\secure\deploy_cred.xml # 使用时解密 $secureCred Import-Clixml -Path C:\secure\deploy_cred.xml6.2 输入验证所有外部输入必须验证param( [string]$FileName ) # 验证文件路径 if (-not (Test-Path -Path $FileName -IsValid)) { throw 无效的文件路径: $FileName } # 防止路径遍历攻击 if ($FileName -match \.\.) { throw 路径包含非法字符 } # 限制文件类型 $extension [System.IO.Path]::GetExtension($FileName) if ($extension -notin (.txt,.csv,.log)) { throw 不支持的文件类型: $extension }7. 调试与故障排除7.1 调试技巧使用调试模式# 启用详细日志 $DebugPreference Continue # 在脚本中添加调试点 Write-Debug 正在处理文件: $file远程调试# 进入远程会话 Enter-PSSession -ComputerName TargetServer -Credential $adminCred # 设置远程调试 Set-PSBreakpoint -Script C:\AgentSkills\Deploy.ps1 -Line 42性能分析# 记录执行时间 $stopwatch [System.Diagnostics.Stopwatch]::StartNew() # 执行代码 Invoke-Skill -Name DataProcessing # 输出耗时 $stopwatch.Stop() Write-Output 执行耗时: $($stopwatch.Elapsed.TotalSeconds)秒7.2 常见问题解决问题现象可能原因解决方案技能执行超时脚本死循环/网络延迟设置超时参数-Timeout 300权限不足执行账户权限限制检查执行策略和账户权限参数传递失败参数类型不匹配使用Get-Type检查参数类型内存泄漏未释放资源确保所有资源在finally块中释放跨平台问题平台特定命令使用兼容性包装函数8. 集成与扩展8.1 与其他系统集成框架可以通过多种方式扩展REST API集成$response Invoke-RestMethod -Uri https://api.example.com/data -Method Get $processedData $response | ConvertTo-AgentData数据库连接$connection New-Object System.Data.SqlClient.SqlConnection $connection.ConnectionString Serversql.example.com;DatabaseAppDB;Integrated SecurityTrue try { $connection.Open() $command $connection.CreateCommand() $command.CommandText SELECT * FROM Deployments $reader $command.ExecuteReader() while ($reader.Read()) { # 处理数据 } } finally { $connection.Close() }消息队列# RabbitMQ示例 $factory New-Object RabbitMQ.Client.ConnectionFactory $factory.HostName mq.example.com $connection $factory.CreateConnection() $channel $connection.CreateModel() $channel.QueueDeclare(deployments, $true, $false, $false, $null) $body [System.Text.Encoding]::UTF8.GetBytes($deploymentData) $channel.BasicPublish(, deployments, $null, $body)8.2 创建复合技能将多个简单技能组合成复杂工作流param( [string]$ProjectName ) # 1. 获取代码 $source Invoke-AgentSkill -Name GetSourceCode -Parameters { Project $ProjectName } # 2. 运行构建 $buildResult Invoke-AgentSkill -Name RunBuild -Parameters { SourcePath $source.Path } # 3. 部署到测试环境 $testDeploy Invoke-AgentSkill -Name DeployToTest -Parameters { Artifact $buildResult.ArtifactPath } # 4. 运行自动化测试 $testResults Invoke-AgentSkill -Name RunIntegrationTests -Parameters { Environment Test } # 5. 如果测试通过部署到生产 if ($testResults.Passed) { Invoke-AgentSkill -Name DeployToProduction -Parameters { Artifact $buildResult.ArtifactPath } } else { throw 集成测试失败部署中止 }9. 性能监控与优化9.1 监控技能执行实现全面的监控体系基础指标收集$executionStats { StartTime Get-Date SkillName $MyInvocation.MyCommand.Name Parameters $PSBoundParameters } try { # 执行技能逻辑 } finally { $executionStats.EndTime Get-Date $executionStats.Duration $executionStats.EndTime - $executionStats.StartTime $executionStats.Success $? # 发送到监控系统 Send-MonitoringData -Data $executionStats }性能计数器# 创建自定义计数器 if (-not (Get-Counter -ListSet AgentSkills -ErrorAction SilentlyContinue)) { New-Counter -CounterName AgentSkills\Executions -Description 技能执行次数 New-Counter -CounterName AgentSkills\Duration -Description 平均执行时间(ms) } # 更新计数器 $counterData { AgentSkills\Executions 1 AgentSkills\Duration $duration.TotalMilliseconds } Update-Counter -Counter $counterData9.2 优化策略缓存常用数据$cache {} function Get-Configuration { param($key) if (-not $cache.ContainsKey($key)) { $cache[$key] Invoke-RestMethod https://api.example.com/config/$key } return $cache[$key] }异步执行$jobs () foreach ($item in $items) { $jobs Start-Job -ScriptBlock { param($i) Process-Item $i } -ArgumentList $item } $results $jobs | Receive-Job -Wait -AutoRemoveJob批量处理# 不佳做法单条处理 foreach ($user in $users) { Update-UserRecord $user } # 优化做法批量处理 Update-UserRecord -Batch $users10. 实际应用案例10.1 自动化测试流水线创建一个端到端的测试自动化技能param( [string]$TestSuite, [string]$Environment, [int]$RetryCount 3 ) # 1. 准备测试环境 $envStatus Invoke-AgentSkill -Name PrepareTestEnvironment -Parameters { Environment $Environment } if (-not $envStatus.IsReady) { throw 测试环境准备失败 } # 2. 执行测试套件 $attempt 1 do { $testResult Invoke-AgentSkill -Name ExecuteTestSuite -Parameters { Suite $TestSuite Environment $Environment } if ($testResult.Passed) { break } $attempt if ($attempt -le $RetryCount) { Write-Warning 测试失败正在重试 ($attempt/$RetryCount)... Start-Sleep -Seconds 10 } } while ($attempt -le $RetryCount) # 3. 生成测试报告 $report Invoke-AgentSkill -Name GenerateTestReport -Parameters { Result $testResult Environment $Environment } # 4. 清理环境 Invoke-AgentSkill -Name CleanupTestEnvironment -Parameters { Environment $Environment } return $report10.2 智能监控告警系统结合AI技能创建智能监控param( [string]$ResourceType, [datetime]$StartTime (Get-Date).AddHours(-1), [datetime]$EndTime Get-Date ) # 1. 获取监控数据 $metrics Invoke-AgentSkill -Name GetMetrics -Parameters { ResourceType $ResourceType StartTime $StartTime EndTime $EndTime } # 2. 异常检测 $anomalies Invoke-AgentSkill -Name DetectAnomalies -Parameters { Metrics $metrics } # 3. 根本原因分析 if ($anomalies.Count -gt 0) { $rootCause Invoke-AgentSkill -Name AnalyzeRootCause -Parameters { Anomalies $anomalies Metrics $metrics } # 4. 自动修复尝试 if ($rootCause.SuggestedFix) { $fixResult Invoke-AgentSkill -Name $rootCause.SuggestedFix.SkillName -Parameters $rootCause.SuggestedFix.Parameters if (-not $fixResult.Success) { # 5. 人工介入通知 Invoke-AgentSkill -Name SendAlert -Parameters { Message 自动修复失败需要人工介入 Priority High Details { Anomalies $anomalies RootCause $rootCause FixAttempt $fixResult } } } } } # 返回监控摘要 return { Metrics $metrics Anomalies $anomalies ActionsTaken if ($rootCause) { $rootCause.SuggestedFix } else { $null } }11. 技能开发高级主题11.1 动态参数生成实现根据上下文动态生成参数function Get-DynamicParameters { param( [string]$SkillName ) # 根据当前环境生成不同参数 $envParams if ($env:COMPUTERNAME -like PROD-*) { { SafetyCheck $true NotifyOnFailure $true } } else { { SafetyCheck $false } } # 根据技能类型添加特定参数 $skillSpecific switch ($SkillName) { DatabaseBackup { { BackupType Full Compression $true } } CodeDeploy { { RollbackEnabled $true VerifyDeployment $true } } default { {} } } # 合并参数 return $envParams $skillSpecific } # 使用动态参数 $dynamicParams Get-DynamicParameters -SkillName DatabaseBackup Invoke-AgentSkill -Name DatabaseBackup -Parameters $dynamicParams11.2 技能组合与编排使用DSL定义复杂工作流# .SYNOPSIS 部署流水线定义 # configuration DeploymentPipeline { param( [Parameter(Mandatory)] [string]$ProjectName, [ValidateSet(Dev,Test,Prod)] [string]$Environment Test ) # 1. 代码获取阶段 Stage GetCode { Execute GetSourceCode { Project $ProjectName Branch if ($Environment -eq Prod) { main } else { dev } } } # 2. 构建阶段 Stage Build { Execute RunBuild { SourcePath $StageResults[GetCode].SourcePath Configuration if ($Environment -eq Prod) { Release } else { Debug } } # 并行执行静态分析 Parallel { Execute RunCodeAnalysis { SourcePath $StageResults[GetCode].SourcePath } Execute RunSecurityScan { Artifact $StageResults[Build].ArtifactPath } } } # 3. 部署阶段 Stage Deploy { Execute DeployToEnvironment { Artifact $StageResults[Build].ArtifactPath Environment $Environment } } # 4. 验证阶段仅生产环境 if ($Environment -eq Prod) { Stage Verify { Execute RunSmokeTests { Environment $Environment } Execute MonitorPerformance { Duration 30m } } } } # 执行流水线 Invoke-Pipeline -Configuration DeploymentPipeline -Parameters { ProjectName WebApp Environment Prod }12. 故障恢复与容错设计12.1 重试机制实现智能重试策略对于自动化脚本至关重要function Invoke-WithRetry { param( [ScriptBlock]$ScriptBlock, [int]$MaxRetries 3, [int]$InitialDelay 1, [string]$RetryCondition { $LASTEXITCODE -ne 0 } ) $attempt 1 $delay $InitialDelay do { try { $result $ScriptBlock # 检查是否满足成功条件 if (-not ( $RetryCondition)) { return $result } } catch { $lastError $_ } if ($attempt -le $MaxRetries) { Write-Warning 尝试 $attempt/$MaxRetries 失败将在 ${delay}秒后重试... Start-Sleep -Seconds $delay # 指数退避 $delay [math]::Min($delay * 2, 30) # 最大延迟30秒 } $attempt } while ($attempt -le $MaxRetries) throw 所有重试尝试均失败。最后错误: $lastError } # 使用示例 Invoke-WithRetry -ScriptBlock { Invoke-RestMethod -Uri https://api.example.com/data -Method Get } -RetryCondition { $null -eq $_ } -MaxRetries 512.2 状态检查点实现断点续执行功能function Invoke-WithCheckpoints { param( [Parameter(Mandatory)] [string]$OperationId, [Parameter(Mandatory)] [ScriptBlock]$ScriptBlock, [string]$StatePath C:\AgentState ) # 初始化状态目录 if (-not (Test-Path $StatePath)) { New-Item -Path $StatePath -ItemType Directory | Out-Null } $stateFile Join-Path $StatePath $OperationId.state # 加载已有状态 $state if (Test-Path $stateFile) { Import-Clixml -Path $stateFile } else { { CurrentStep 0 CompletedSteps () Data {} } } try { # 执行脚本块并传递状态 $ScriptBlock -State $state # 成功后删除状态文件 Remove-Item $stateFile -ErrorAction SilentlyContinue } catch { # 失败时保存状态 $state.LastError $_.Exception.Message $state | Export-Clixml -Path $stateFile throw } } # 使用示例 Invoke-WithCheckpoints -OperationId DataImport_$(Get-Date -Format yyyyMMdd) { param($State) # 步骤1准备数据仅在第一次执行 if (0 -notin $State.CompletedSteps) { Write-Output 执行步骤1准备数据 $State.Data.SourceFiles Get-ChildItem C:\import\*.csv $State.CompletedSteps 0 } # 步骤2处理文件断点续传 foreach ($file in $State.Data.SourceFiles) { $fileId $file.Name if ($fileId -in $State.Data.ProcessedFiles) { continue # 跳过已处理的文件 } Write-Output 处理文件: $fileId Import-File $file.FullName $State.Data.ProcessedFiles $fileId } # 步骤3完成处理最后执行 if (2 -notin $State.CompletedSteps) { Write-Output 执行步骤3完成处理 Send-CompletionNotification $State.CompletedSteps 2 } }13. 安全加固措施13.1 脚本签名验证确保只有受信任的脚本可以执行# 生成签名证书开发环境 $cert New-SelfSignedCertificate -Type CodeSigningCert -Subject CNAgentSkillsSigner -KeyUsage DigitalSignature # 导出公钥 Export-Certificate -Cert $cert -FilePath C:\certs\AgentSkillsSigner.cer # 签名脚本 Set-AuthenticodeSignature -FilePath C:\AgentSkills\Deploy.ps1 -Certificate $cert # 验证签名 Get-AuthenticodeSignature -FilePath C:\AgentSkills\Deploy.ps1 | Where-Object { $_.Status -eq Valid -and $_.SignerCertificate.Subject -eq CNAgentSkillsSigner } # 配置执行策略只允许签名脚本 Set-ExecutionPolicy -ExecutionPolicy AllSigned -Scope LocalMachine13.2 敏感数据处理安全地处理密码、密钥等敏感信息# 加密字符串 $secureString ConvertTo-SecureString MySecretPss -AsPlainText -Force $encrypted ConvertFrom-SecureString -SecureString $secureString -Key (1..16) # 保存加密数据 $config { DatabasePassword $encrypted ApiKey (ConvertFrom-SecureString -SecureString (ConvertTo-SecureString API_KEY_123 -AsPlainText -Force) -Key (1..16)) } $config | Export-Clixml -Path C:\secure\config.xml # 使用加密数据 $secureConfig Import-Clixml -Path C:\secure\config.xml $dbPassword ConvertTo-SecureString -String $secureConfig.DatabasePassword -Key (1..16) $credential New-Object System.Management.Automation.PSCredential(dbuser, $dbPassword) # 内存清理防止内存转储泄露 [System.GC]::Collect() [System.GC]::WaitForPendingFinalizers()14. 测试驱动开发实践14.1 为技能编写单元测试使用Pester框架创建测试# 安装测试框架 Install-Module -Name Pester -Force -SkipPublisherCheck # 测试脚本示例 (Tests/HelloWorld.Tests.ps1) Describe HelloWorld 技能测试 { BeforeAll { . $PSScriptRoot/../Skills/HelloWorld.ps1 } Context 默认参数 { It 应该返回默认问候语 { $result HelloWorld $result | Should -Be Hello, World! } } Context 自定义名称 { It 应该返回个性化问候语 { $testCases ( {Name Alice; Expected Hello, Alice!} {Name Bob; Expected Hello, Bob!} {Name ; Expected Hello, !} ) It 对名称 Name 应该返回 Expected -TestCases $testCases { param($Name, $Expected) HelloWorld -Name $Name | Should -Be $Expected } } } Context 输入验证 { It 应该拒绝空名称参数 { { HelloWorld -Name $null } | Should -Throw 不能为null } } } # 运行测试 Invoke-Pester -Path Tests/HelloWorld.Tests.ps114.2 集成测试策略创建端到端测试场景Describe 部署流水线集成测试 { BeforeAll { # 启动测试环境 $testEnv Start-TestEnvironment -Name DeployTest # 注册测试用技能 Register-TestSkill -Path TestSkills/MockSourceControl.ps1 Register-TestSkill -Path TestSkills/MockBuilder.ps1 Register-TestSkill -Path TestSkills/MockDeployer.ps1 } It 应该成功完成完整部署流程 { # 执行部署流水线 $result Invoke-DeploymentPipeline -Project TestApp -Environment Test # 验证结果 $result.BuildStatus | Should -Be Success $result.DeploymentStatus | Should -Be Completed $result.TestsPassed | Should -Be $true } It 应该在构建失败时中止部署 { # 配置构建失败 Set-MockBehavior -Skill MockBuilder -Scenario Failure # 执行并验证 { Invoke-DeploymentPipeline -Project TestApp -Environment Test } | Should -Throw } AfterAll { # 清理测试环境 Stop-TestEnvironment -Name DeployTest } }15. 持续集成与交付15.1 CI/CD流水线集成将技能开发纳入CI/CD流程# azure-pipelines.yml 示例 trigger: - main pool: vmImage: windows-latest steps: - task: PowerShell2 displayName: 安装依赖 inputs: targetType: inline script: | Install-Module -Name MicrosoftAgentFramework -Force -AllowClobber Install-Module -Name Pester -Force -SkipPublisherCheck - task: PowerShell2 displayName: 运行单元测试 inputs: targetType: inline script: | Import-Module Pester $testResults Invoke-Pester -Path Tests -OutputFile TestResults.xml -OutputFormat NUnitXml -PassThru if ($testResults.FailedCount -gt 0) { exit 1 } - task: PublishTestResults2 displayName: 发布测试结果 inputs: testResultsFormat: NUnit testResultsFiles: TestResults.xml failTaskOnFailedTests: true - task: PowerShell2 displayName: 签名技能脚本 inputs: targetType: inline script: | $cert Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert | Select-Object -First 1 if (-not $cert) { throw 未找到代码签名证书 } Get-ChildItem Skills/*.ps1 | ForEach-Object { Set-AuthenticodeSignature -FilePath $_.FullName -Certificate $cert } - task: PowerShell2 displayName: 部署到技能库 inputs: targetType: inline script: | $artifacts $(Build.ArtifactStagingDirectory)/Skills New-Item -Path $artifacts -ItemType Directory -Force Copy-Item -Path Skills/*.ps1 -Destination $artifacts Publish-BuildArtifact -Path $artifacts -Art