当前位置:网站首页 > 更多 > 玩电脑 > 正文

[玩转系统] 如何创建反馈提供者

作者:精品下载站 日期:2024-12-14 02:56:31 浏览:14 分类:玩电脑

如何创建反馈提供者


PowerShell 7.4 引入了反馈提供者的概念。反馈提供程序是一个 PowerShell 模块,它实现了 IFeedbackProvider 接口,以根据用户命令执行尝试提供命令建议。当执行成功或失败时,将触发提供程序。反馈提供者使用成功或失败的信息来提供反馈。

先决条件

要创建反馈提供者,您必须满足以下先决条件:

  • 安装 PowerShell 7.4 或更高版本

    • 您必须启用 PSFeedbackProvider 实验性功能才能启用对反馈提供程序和预测器的支持。有关更多信息,请参阅使用实验功能。
  • 安装 .NET 8 SDK - 8.0.0 或更高版本

    • 请参阅下载 .NET 8.0 页面以获取最新版本的 SDK。
  • 反馈提供者概述

    反馈提供程序是一个实现 System.Management.Automation.Subsystem.Feedback.IFeedbackProvider 接口的 PowerShell 二进制模块。该接口声明了根据命令行输入获取反馈的方法。反馈接口可以根据用户调用的命令的成功或失败来提供建议。这些建议可以是您想要的任何内容。例如,您可能会建议解决错误的方法或更好的做法,例如避免使用别名。有关更多信息,请参阅什么是反馈提供者?博客文章。

    下图显示了反馈提供者的架构:

    [玩转系统] 如何创建反馈提供者

    以下示例将引导您完成创建简单反馈提供程序的过程。此外,您还可以使用命令预测器界面注册提供程序,以向命令行预测器体验添加反馈建议。有关预测器的更多信息,请参阅在 PSReadLine 中使用预测器和如何创建命令行预测器。

    第 1 步 - 创建一个新的类库项目

    使用以下命令在项目目录中创建一个新项目:

    dotnet new classlib --name MyFeedbackProvider
    

    System.Management.Automation 包的包引用添加到 .csproj 文件中。以下示例显示了更新后的 .csproj 文件:

    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <TargetFramework>net8.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="System.Management.Automation" Version="7.4.0-preview.3">
            <ExcludeAssets>contentFiles</ExcludeAssets>
            <PrivateAssets>All</PrivateAssets>
        </PackageReference>
      </ItemGroup>
    </Project>
    

    笔记

    您应该更改 System.Management.Automation 程序集的版本,以匹配您所定位的 PowerShell 预览版的版本。最低版本是 7.4.0-preview.3。

    第 2 步 - 添加提供商的类定义

    更改 Class1.cs 文件的名称以匹配您的提供商的名称。此示例使用 myFeedbackProvider.cs。该文件包含定义反馈提供者的两个主要类。以下示例显示了类定义的基本模板。

    using System.Management.Automation;
    using System.Management.Automation.Subsystem;
    using System.Management.Automation.Subsystem.Feedback;
    using System.Management.Automation.Subsystem.Prediction;
    using System.Management.Automation.Language;
    
    namespace myFeedbackProvider;
    
    public sealed class myFeedbackProvider : IFeedbackProvider, ICommandPredictor
    {
    
    }
    
    public class Init : IModuleAssemblyInitializer, IModuleAssemblyCleanup
    {
    
    }
    

    第 3 步 - 实现 Init 类

    Init 类向子系统管理器注册和取消注册反馈提供者。 OnImport() 方法在加载二进制模块时运行。 OnRemove() 方法在删除二进制模块时运行。此示例注册了反馈提供程序和命令预测器子系统。

    public class Init : IModuleAssemblyInitializer, IModuleAssemblyCleanup
    {
        private const string Id = "<ADD YOUR GUID HERE>";
    
        public void OnImport()
        {
            var feedback = new myFeedbackProvider(Id);
            SubsystemManager.RegisterSubsystem(SubsystemKind.FeedbackProvider, feedback);
            SubsystemManager.RegisterSubsystem(SubsystemKind.CommandPredictor, feedback);
        }
    
        public void OnRemove(PSModuleInfo psModuleInfo)
        {
            SubsystemManager.UnregisterSubsystem<ICommandPredictor>(new Guid(Id));
            SubsystemManager.UnregisterSubsystem<IFeedbackProvider>(new Guid(Id));
        }
    }
    

    <ADD YOUR GUID HERE> 占位符值替换为唯一的 Guid。您可以使用 New-Guid cmdlet 生成 Guid。

    New-Guid
    

    Guid 是您的提供商的唯一标识符。提供者必须有一个唯一的 ID 才能向子系统注册。

    第 4 步 - 添加类成员并定义构造函数

    以下代码实现接口中定义的属性,添加所需的类成员,并创建 myFeedbackProvider 类的构造函数。

    /// <summary>
    /// Gets the global unique identifier for the subsystem implementation.
    /// </summary>
    private readonly Guid _guid;
    public Guid Id => _guid;
    
    /// <summary>
    /// Gets the name of a subsystem implementation, this will be the name displayed when triggered
    /// </summary>
    public string Name => "myFeedbackProvider";
    
    /// <summary>
    /// Gets the description of a subsystem implementation.
    /// </summary>
    public string Description => "This is very simple feedback provider";
    
    /// <summary>
    /// Default implementation. No function is required for a feedback provider.
    /// </summary>
    Dictionary<string, string>? ISubsystem.FunctionsToDefine => null;
    
    /// <summary>
    /// Gets the types of trigger for this feedback provider.
    /// </summary>
    /// <remarks>
    /// The default implementation triggers a feedback provider by <see cref="FeedbackTrigger.CommandNotFound"/> only.
    /// </remarks>
    public FeedbackTrigger Trigger => FeedbackTrigger.All;
    
    /// <summary>
    /// List of candidates from the feedback provider to be passed as predictor results
    /// </summary>
    private List<string>? _candidates;
    
    /// <summary>
    /// PowerShell session used to run PowerShell commands that help create suggestions.
    /// </summary>
    private PowerShell _powershell;
    
    internal myFeedbackProvider(string guid)
    {
        _guid = new Guid(guid); // Save guid
        _powershell = PowerShell.Create(); // Create PowerShell instance
    }
    

    第 5 步 - 创建 GetFeedback() 方法

    GetFeedback 方法采用两个参数:contexttokencontext 参数接收有关触发器的信息,以便您可以决定如何响应建议。 token 参数用于取消。此函数返回包含建议的 FeedbackItem

    /// <summary>
    /// Gets feedback based on the given commandline and error record.
    /// </summary>
    /// <param name="context">The context for the feedback call.</param>
    /// <param name="token">The cancellation token to cancel the operation.</param>
    /// <returns>The feedback item.</returns>
    public FeedbackItem? GetFeedback(FeedbackContext context, CancellationToken token)
    {
        // Target describes the different kinds of triggers to activate on,
        var target = context.Trigger;
        var commandLine = context.CommandLine;
        var ast = context.CommandLineAst;
    
        // defining the header and footer variables
        string header;
        string footer;
    
        // List of the actions
        List<string>? actions = new List<string>();
    
        // Trigger on success code goes here
    
        // Trigger on error code goes here
    
        return null;
    }
    

    下图显示了如何在向用户显示的建议中使用这些字段。

    [玩转系统] 如何创建反馈提供者

    为成功触发器创建建议

    为了成功调用,我们希望扩展上次执行中使用的所有别名。使用 CommandLineAst,我们可以识别任何别名命令,并创建建议以使用完全限定的命令名称。

    // Trigger on success
    if (target == FeedbackTrigger.Success)
    {
        // Getting the commands from the AST and only finding those that are Commands
        var astCmds = ast.FindAll((cAst) => cAst is CommandAst, true);
    
        // Inspect each of the commands
        foreach(var command in astCmds)
        {
    
            // Get the command name
            var aliasedCmd = ((CommandAst) command).GetCommandName();
    
            // Check if its an alias or not, if so then add it to the list of actions
            if(TryGetAlias(aliasedCmd, out string commandString))
            {
                actions.Add($"{aliasedCmd} --> {commandString}");
            }
        }
    
        // If no alias was found return null
        if(actions.Count == 0)
        {
            return null;
        }
    
        // If aliases are found, set the header to a description and return a new FeedbackItem.
        header = "You have used an aliased command:";
        // Copy actions to _candidates for the predictor
        _candidates = actions;
    
        return new FeedbackItem(header, actions);
    }
    

    实现 TryGetAlias() 方法

    TryGetAlias() 方法是一个私有辅助函数,它返回一个布尔值来指示该命令是否是别名。在类构造函数中,我们创建了一个可用于运行 PowerShell 命令的 PowerShell 实例。 TryGetAlias() 方法使用此 PowerShell 实例调用 GetCommand 方法来确定该命令是否是别名。 GetCommand 返回的 AliasInfo 对象包含别名命令的完整名称。

    /// <summary>
    /// Checks if a command is an alias.
    /// </summary>
    /// <param name="command">The command to check if alias</param>
    /// <param name="targetCommand">The referenced command by the aliased command</param>
    /// <returns>True if an alias and false if not</returns>
    private bool TryGetAlias(string command, out string targetCommand)
    {
        // Create PowerShell runspace as a session state proxy to run GetCommand and check
        // if its an alias
        AliasInfo? pwshAliasInfo =
            _powershell.Runspace.SessionStateProxy.InvokeCommand.GetCommand(command, CommandTypes.Alias) as AliasInfo;
    
        // if its null then it is not an aliased command so just return false
        if(pwshAliasInfo is null)
        {
            targetCommand = String.Empty;
            return false;
        }
    
        // Set targetCommand to referenced command name
        targetCommand = pwshAliasInfo.ReferencedCommand.Name;
        return true;
    }
    

    为失败触发器创建建议

    当命令执行失败时,我们希望建议用户Get-Help来获取有关如何使用该命令的更多信息。

    // Trigger on error
    if (target == FeedbackTrigger.Error)
    {
        // Gets the command that caused the error.
        var erroredCommand = context.LastError?.InvocationInfo.MyCommand;
        if (erroredCommand is null)
        {
            return null;
        }
    
        header = $"You have triggered an error with the command {erroredCommand}. Try using the following command to get help:";
    
        actions.Add($"Get-Help {erroredCommand}");
        footer = $"You can also check online documentation at https://learn.microsoft.com/en-us/powershell/module/?term={erroredCommand}";
    
        // Copy actions to _candidates for the predictor
        _candidates = actions;
        return new FeedbackItem(header, actions, footer, FeedbackDisplayLayout.Portrait);
    }
    

    第 6 步 - 向命令行预测器发送建议

    反馈提供程序增强用户体验的另一种方法是向 ICommandPredictor 界面提供命令建议。有关创建命令行预测器的更多信息,请参阅如何创建命令行预测器。

    以下代码实现 ICommandPredictor 接口所需的方法,以将预测器行为添加到反馈提供程序。

    • CanAcceptFeedback() - 此方法返回一个布尔值,指示预测器是否接受特定类型的反馈。
    • GetSuggestion() - 此方法返回一个 SuggestionPackage 对象,其中包含预测器要显示的建议。
    • OnCommandLineAccepted() - 当命令行被接受执行时调用此方法。
    /// <summary>
    /// Gets a value indicating whether the predictor accepts a specific kind of feedback.
    /// </summary>
    /// <param name="client">Represents the client that initiates the call.</param>
    /// <param name="feedback">A specific type of feedback.</param>
    /// <returns>True or false, to indicate whether the specific feedback is accepted.</returns>
    public bool CanAcceptFeedback(PredictionClient client, PredictorFeedbackKind feedback)
    {
        return feedback switch
        {
            PredictorFeedbackKind.CommandLineAccepted => true,
            _ => false,
        };
    }
    
    /// <summary>
    /// Get the predictive suggestions. It indicates the start of a suggestion rendering session.
    /// </summary>
    /// <param name="client">Represents the client that initiates the call.</param>
    /// <param name="context">The <see cref="PredictionContext"/> object to be used for prediction.</param>
    /// <param name="cancellationToken">The cancellation token to cancel the prediction.</param>
    /// <returns>An instance of <see cref="SuggestionPackage"/>.</returns>
    public SuggestionPackage GetSuggestion(
        PredictionClient client,
        PredictionContext context,
        CancellationToken cancellationToken)
    {
        if (_candidates is not null)
        {
            string input = context.InputAst.Extent.Text;
            List<PredictiveSuggestion>? result = null;
    
            foreach (string c in _candidates)
            {
                if (c.StartsWith(input, StringComparison.OrdinalIgnoreCase))
                {
                    result ??= new List<PredictiveSuggestion>(_candidates.Count);
                    result.Add(new PredictiveSuggestion(c));
                }
            }
    
            if (result is not null)
            {
                return new SuggestionPackage(result);
            }
        }
    
        return default;
    }
    
    /// <summary>
    /// A command line was accepted to execute.
    /// The predictor can start processing early as needed with the latest history.
    /// </summary>
    /// <param name="client">Represents the client that initiates the call.</param>
    /// <param name="history">History command lines provided as references for prediction.</param>
    public void OnCommandLineAccepted(PredictionClient client, IReadOnlyList<string> history)
    {
        // Reset the candidate state once the command is accepted.
        _candidates = null;
    }
    

    第 7 步 - 构建反馈提供者

    现在您已准备好构建并开始使用您的反馈提供程序!要构建项目,请运行以下命令:

    dotnet build
    

    此命令在项目文件夹的以下路径中将 PowerShell 模块创建为 DLL 文件:bin/Debug/net8.0/myFeedbackProvider

    在 Windows 计算机上构建时,您可能会遇到错误 error NU1101: Unable to find package System.Management.Automation.。要解决此问题,请将 nuget.config 文件添加到项目目录中并添加以下内容:

    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
      <packageSources>
        <clear />
        <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
      </packageSources>
      <disabledPackageSources>
        <clear />
      </disabledPackageSources>
    </configuration>
    

    使用反馈提供者

    要测试新的反馈提供程序,请将编译的模块导入到 PowerShell 会话中。这可以通过导入构建成功后描述的文件夹来完成:

    Import-Module ./bin/Debug/net8.0/myFeedbackProvider
    

    一旦您对模块感到满意,您应该创建一个模块清单,将其发布到 PowerShell 库,并将其安装在您的 $env:PSModulePath 中。有关更多信息,请参阅如何创建模块清单。您可以将 Import-Module 命令添加到 $PROFILE 脚本中,以便该模块在 PowerShell 会话中可用。

    您可以使用以下命令获取已安装的反馈提供程序的列表:

    Get-PSSubsystem -Kind FeedbackProvider
    
    Kind              SubsystemType      IsRegistered Implementations
    ----              -------------      ------------ ---------------
    FeedbackProvider  IFeedbackProvider          True {general}
    

    笔记

    Get-PSSubsystem 是 PowerShell 7.1 中引入的实验性 cmdlet。您必须启用 PSSubsystemPluginModel 实验性功能才能使用此 cmdlet。有关更多信息,请参阅使用实验功能。

    以下屏幕截图显示了新提供商的一些示例建议。

    [玩转系统] 如何创建反馈提供者

    以下 GIF 显示了新提供商的预测器集成如何工作。

    [玩转系统] 如何创建反馈提供者

    其他反馈提供者

    我们创建了其他反馈提供程序,可以用作更深入示例的良好参考。

    未找到命令

    command-not-found 反馈提供程序利用 Linux 系统上的 command-not-found 实用工具在尝试运行本机命令但丢失时提供建议。您可以在 GitHub 存储库中找到代码,也可以在 PowerShell Gallery 上自行下载。

    PowerShell 适配器

    Microsoft.PowerShell.PowerShellAdapter 是一个反馈提供程序,可帮助您将本机命令的文本输出转换为 PowerShell 对象。它会检测您系统上的“适配器”,并建议您在使用本机命令时使用它们。您可以从 PowerShell 适配器反馈提供者博客文章中了解有关 PowerShell 适配器的更多信息。您还可以在 GitHub 存储库中找到代码,或者可以在 PowerShell Gallery 上自行下载。

    附录 - 完整的实现代码

    以下代码将前面的示例合并到提供程序类的 find 完整实现中。

    using System.Management.Automation;
    using System.Management.Automation.Subsystem;
    using System.Management.Automation.Subsystem.Feedback;
    using System.Management.Automation.Subsystem.Prediction;
    using System.Management.Automation.Language;
    
    namespace myFeedbackProvider;
    
    public sealed class myFeedbackProvider : IFeedbackProvider, ICommandPredictor
    {
        /// <summary>
        /// Gets the global unique identifier for the subsystem implementation.
        /// </summary>
        private readonly Guid _guid;
        public Guid Id => _guid;
    
        /// <summary>
        /// Gets the name of a subsystem implementation, this will be the name displayed when triggered
        /// </summary>
        public string Name => "myFeedbackProvider";
    
        /// <summary>
        /// Gets the description of a subsystem implementation.
        /// </summary>
        public string Description => "This is very simple feedback provider";
    
        /// <summary>
        /// Default implementation. No function is required for a feedback provider.
        /// </summary>
        Dictionary<string, string>? ISubsystem.FunctionsToDefine => null;
    
        /// <summary>
        /// Gets the types of trigger for this feedback provider.
        /// </summary>
        /// <remarks>
        /// The default implementation triggers a feedback provider by <see cref="FeedbackTrigger.CommandNotFound"/> only.
        /// </remarks>
        public FeedbackTrigger Trigger => FeedbackTrigger.All;
    
        /// <summary>
        /// List of candidates from the feedback provider to be passed as predictor results
        /// </summary>
        private List<string>? _candidates;
    
        /// <summary>
        /// PowerShell session used to run PowerShell commands that help create suggestions.
        /// </summary>
        private PowerShell _powershell;
    
        // Constructor
        internal myFeedbackProvider(string guid)
        {
            _guid = new Guid(guid); // Save guid
            _powershell = PowerShell.Create(); // Create PowerShell instance
        }
    
        #region IFeedbackProvider
        /// <summary>
        /// Gets feedback based on the given commandline and error record.
        /// </summary>
        /// <param name="context">The context for the feedback call.</param>
        /// <param name="token">The cancellation token to cancel the operation.</param>
        /// <returns>The feedback item.</returns>
        public FeedbackItem? GetFeedback(FeedbackContext context, CancellationToken token)
        {
            // Target describes the different kinds of triggers to activate on,
            var target = context.Trigger;
            var commandLine = context.CommandLine;
            var ast = context.CommandLineAst;
    
            // defining the header and footer variables
            string header;
            string footer;
    
            // List of the actions
            List<string>? actions = new List<string>();
    
            // Trigger on success
            if (target == FeedbackTrigger.Success)
            {
                // Getting the commands from the AST and only finding those that are Commands
                var astCmds = ast.FindAll((cAst) => cAst is CommandAst, true);
    
                // Inspect each of the commands
                foreach(var command in astCmds)
                {
    
                    // Get the command name
                    var aliasedCmd = ((CommandAst) command).GetCommandName();
    
                    // Check if its an alias or not, if so then add it to the list of actions
                    if(TryGetAlias(aliasedCmd, out string commandString))
                    {
                        actions.Add($"{aliasedCmd} --> {commandString}");
                    }
                }
    
                // If no alias was found return null
                if(actions.Count == 0)
                {
                    return null;
                }
    
                // If aliases are found, set the header to a description and return a new FeedbackItem.
                header = "You have used an aliased command:";
                // Copy actions to _candidates for the predictor
                _candidates = actions;
    
                return new FeedbackItem(header, actions);
            }
    
            // Trigger on error
            if (target == FeedbackTrigger.Error)
            {
                // Gets the command that caused the error.
                var erroredCommand = context.LastError?.InvocationInfo.MyCommand;
                if (erroredCommand is null)
                {
                    return null;
                }
    
                header = $"You have triggered an error with the command {erroredCommand}. Try using the following command to get help:";
    
                actions.Add($"Get-Help {erroredCommand}");
                footer = $"You can also check online documentation at https://learn.microsoft.com/en-us/powershell/module/?term={erroredCommand}";
    
                // Copy actions to _candidates for the predictor
                _candidates = actions;
                return new FeedbackItem(header, actions, footer, FeedbackDisplayLayout.Portrait);
            }
            return null;
        }
    
        /// <summary>
        /// Checks if a command is an alias.
        /// </summary>
        /// <param name="command">The command to check if alias</param>
        /// <param name="targetCommand">The referenced command by the aliased command</param>
        /// <returns>True if an alias and false if not</returns>
        private bool TryGetAlias(string command, out string targetCommand)
        {
            // Create PowerShell runspace as a session state proxy to run GetCommand and check
            // if its an alias
            AliasInfo? pwshAliasInfo =
                _powershell.Runspace.SessionStateProxy.InvokeCommand.GetCommand(command, CommandTypes.Alias) as AliasInfo;
    
            // if its null then it is not an aliased command so just return false
            if(pwshAliasInfo is null)
            {
                targetCommand = String.Empty;
                return false;
            }
    
            // Set targetCommand to referenced command name
            targetCommand = pwshAliasInfo.ReferencedCommand.Name;
            return true;
        }
        #endregion IFeedbackProvider
    
        #region ICommandPredictor
    
        /// <summary>
        /// Gets a value indicating whether the predictor accepts a specific kind of feedback.
        /// </summary>
        /// <param name="client">Represents the client that initiates the call.</param>
        /// <param name="feedback">A specific type of feedback.</param>
        /// <returns>True or false, to indicate whether the specific feedback is accepted.</returns>
        public bool CanAcceptFeedback(PredictionClient client, PredictorFeedbackKind feedback)
        {
            return feedback switch
            {
                PredictorFeedbackKind.CommandLineAccepted => true,
                _ => false,
            };
        }
    
        /// <summary>
        /// Get the predictive suggestions. It indicates the start of a suggestion rendering session.
        /// </summary>
        /// <param name="client">Represents the client that initiates the call.</param>
        /// <param name="context">The <see cref="PredictionContext"/> object to be used for prediction.</param>
        /// <param name="cancellationToken">The cancellation token to cancel the prediction.</param>
        /// <returns>An instance of <see cref="SuggestionPackage"/>.</returns>
        public SuggestionPackage GetSuggestion(
            PredictionClient client,
            PredictionContext context,
            CancellationToken cancellationToken)
        {
            if (_candidates is not null)
            {
                string input = context.InputAst.Extent.Text;
                List<PredictiveSuggestion>? result = null;
    
                foreach (string c in _candidates)
                {
                    if (c.StartsWith(input, StringComparison.OrdinalIgnoreCase))
                    {
                        result ??= new List<PredictiveSuggestion>(_candidates.Count);
                        result.Add(new PredictiveSuggestion(c));
                    }
                }
    
                if (result is not null)
                {
                    return new SuggestionPackage(result);
                }
            }
    
            return default;
        }
    
        /// <summary>
        /// A command line was accepted to execute.
        /// The predictor can start processing early as needed with the latest history.
        /// </summary>
        /// <param name="client">Represents the client that initiates the call.</param>
        /// <param name="history">History command lines provided as references for prediction.</param>
        public void OnCommandLineAccepted(PredictionClient client, IReadOnlyList<string> history)
        {
            // Reset the candidate state once the command is accepted.
            _candidates = null;
        }
    
        #endregion;
    }
    
    public class Init : IModuleAssemblyInitializer, IModuleAssemblyCleanup
    {
        private const string Id = "<ADD YOUR GUID HERE>";
    
        public void OnImport()
        {
            var feedback = new myFeedbackProvider(Id);
            SubsystemManager.RegisterSubsystem(SubsystemKind.FeedbackProvider, feedback);
            SubsystemManager.RegisterSubsystem(SubsystemKind.CommandPredictor, feedback);
        }
    
        public void OnRemove(PSModuleInfo psModuleInfo)
        {
            SubsystemManager.UnregisterSubsystem<ICommandPredictor>(new Guid(Id));
            SubsystemManager.UnregisterSubsystem<IFeedbackProvider>(new Guid(Id));
        }
    }
    

    您需要 登录账户 后才能发表评论

    取消回复欢迎 发表评论:

    关灯