首页
/ PowerToys Run 插件体系解析:System 全局插件与 User 关键字插件的分类、配置与查询分发机制

PowerToys Run 插件体系解析:System 全局插件与 User 关键字插件的分类、配置与查询分发机制

2026-09-06 19:13:50作者:丁柯新Fawn

导读

PowerToys Run 是 Windows 下的快速启动器,其全部能力都来自一个可插拔的插件体系。仓库内 src/modules/launcher/PowerLauncher/Plugin/README.md 用极简的篇幅点明了整个体系最核心的分类法则:插件只有两类——无需动作关键字(Action Keyword)的 System 系统插件,与必须携带自定义动作关键字的 User 用户插件。本文以此文档为主干,结合 plugin.json 元数据、QueryBuilder 查询分发算法与 PluginConfig 配置加载管线等源码,完整还原这一分类机制如何驱动 PowerToys Run 的每一次搜索,并给出内置插件的关键字速查表与插件开发者的选型建议。

一、文档原文与体系总览:插件只有两类

原文档给出 PowerToys Run 插件分类的唯一权威定义:

  1. System plugin(系统插件):其动作关键字为 "*",这类插件不需要动作关键字即可被触发;
  2. User Plugin(用户插件):这类插件拥有自定义的动作关键字,用户必须输入该关键字前缀才能定向触发。

把这段话映射到当前仓库的代码,可以发现它正是 PowerToys Run(其核心命名空间仍沿用历史项目 Wox 的 Wox.Plugin)插件元数据与查询分发机制的设计出发点:

/// <summary>
/// '*' is used for System Plugin
/// </summary>
public const string GlobalPluginWildcardSign = "*";
  • 同时,现代的插件元数据(PluginMetadata.cs)中同时存在两个决定性属性:
public string ActionKeyword { get; set; }
public bool IsGlobal { get; set; }
  • 运行时插件管理器(PluginManager.cs)据此把全部已加载插件划分为两组:
public static List<PluginPair> GlobalPlugins
{
    get { return AllPlugins.Where(x => x.Metadata.IsGlobal).ToList(); }
}

public static IEnumerable<PluginPair> NonGlobalPlugins

从源码结构看,README 所描述的“System 插件(keyword 为 *、无需关键字)”在现行实现中对应 IsGlobal == true 的全局插件;而“User 插件(自定义关键字)”则对应 IsGlobal == false、以 ActionKeyword 前缀触发的非全局插件

二、plugin.json:插件元数据与分类的声明入口

每一个 PowerToys Run 插件都是一个独立目录,目录根下必须包含一个名为 plugin.json 的清单文件(文件名常量定义于 PluginConfig.cs)。插件是“系统型”还是“用户型”,正取决于该文件中的 IsGlobalActionKeyword 两个字段。

2.1 字段说明

plugin.json 通过 System.Text.Json 反序列化为 PluginMetadata 对象,文件内的键与公开属性一一对应,核心字段如下:

字段 类型 说明(依据源码注释与加载逻辑)
Name string 插件显示名,如 "System Commands"
ActionKeyword string 动作关键字前缀。为空串 "" 表示不依赖关键字;可配置为单字符或字符串(如 "=""??"
IsGlobal bool true 表示全局(System)插件,默认响应不命中任何关键字的一般查询
ExecuteFileName string 插件程序集文件名,加载时与插件目录拼接为 ExecuteFilePath
Disabled bool 是否被禁用;禁用插件会被分发逻辑跳过
IcoPathDark / IcoPathLight string 深/浅色主题下的图标路径;二者缺一不可,否则插件被拒载
Language string 插件实现语言,必须落在 AllowedLanguage 允许列表内
Version / Author / Website string 版本、作者与主页信息
WeightBoost int 结果权重加成,影响排序
DynamicLoading bool 是否支持动态加载

Query 层还允许一个动作关键字配置同时声明多个值——以分号分隔(见 Query.cs):

/// <summary>
/// User can set multiple action keywords separated by ';'
/// </summary>
public const string ActionKeywordSeparator = ";";

2.2 两类插件的真实清单示例

以下两例均取自仓库真实插件目录,可直观对照两种分类写法。

System(全局)插件示例 —— Microsoft.PowerToys.Run.Plugin.Systemplugin.json):关键字为空、IsGlobal: true,无需输入任何前缀即可让“系统命令”参与搜索。

{
  "ActionKeyword": "",
  "IsGlobal": true,
  "Name": "System Commands",
  "ExecuteFileName": "Microsoft.PowerToys.Run.Plugin.System.dll"
}

User(关键字)插件示例 —— Microsoft.PowerToys.Run.Plugin.Historyplugin.json):以 "!!" 作为自定义关键字,用户键入 !! 前缀时才定向触发。

{
  "ActionKeyword": "!!"
}

三、系统插件为什么“不需要动作关键字”:查询分发算法源码剖析

两类插件在运行时被分发的差异,全部集中在查询构建器 QueryBuilder.csBuild(string text) 方法中。它是理解整个分类机制的关键代码,核心决策流程如下:

  1. 裁剪输入:对用户输入 text.Trim()
  2. 第一轮:只遍历非全局插件(User 插件)做关键字匹配。对每个未被禁用的非全局插件,若输入文本以其 ActionKeyword 前缀开头(StringComparison.Ordinal,即区分大小写),则为该插件构造一个 Query(关键字被剥离并写入 Query.ActionKeyword),记入候选字典。
  3. 最长关键字消歧:当多个关键字存在公共前缀(例如 ???,或 >>>),会出现“假命中”。算法记录本轮命中的最长关键字长度,随后把关键字较短的所有候选插件从字典中移除,确保 ?? 输入只归属真正的双字符插件,而不是同时误伤单字符插件。
  4. 第二轮:兜底引入全局插件(System 插件)。只有当没有任何 User 插件命中关键字时,才遍历 PluginManager.GlobalPlugins,为每个全局插件构造不带关键字的 Query(text) 并加入候选。也就是说:

用户一旦输入了某个 User 插件的动作关键字,界面将只展示该插件的结果;否则所有 System 全局插件都会收到完整的原始查询文本,由其自身决定是否给出结果。

这正是 System 插件“不需要动作关键字”的本质:它们不在第一轮做前缀匹配,而是在“没有关键字命中”这个默认通道里被全体唤醒。

// If we have plugin action keywords that start with the same char we get
// false positives (Example: ? and ??)
// Here we remove each query pair that has a shorter keyword than the longest matching one
foreach (PluginPair plugin in pluginQueryPair.Keys)
{
    if (plugin.Metadata.ActionKeyword.Length < longestActionKeywordLength)
    {
        pluginQueryPair.Remove(plugin);
    }
}

// If the user has specified a matching action keyword, then do not
// add the global plugins to the list.
if (pluginQueryPair.Count == 0)
{
    foreach (PluginPair globalPlugin in PluginManager.GlobalPlugins)
    {
        ...
        var query = new Query(text);
        pluginQueryPair.Add(globalPlugin, query);
    }
}

3.1 Query 对象:插件看到的查询是什么

无论是 System 还是 User 插件,收到的都是一个 Wox.Plugin.Query 对象,插件通过其属性获取查询内容:

  • RawUserQuery / RawQuery:用户键入的原始文本(含动作关键字,去多余空白);
  • Search:剥离动作关键字后的“真正查询词”——源码注释明确指出,因为用户可能把“exclusive 插件”切换成通用插件,因此始终建议插件使用 Search 而非直接处理原始文本(Query.cs);
  • Terms:按空格切分出的只读词组集合;
  • FirstSearch / SecondSearch / ThirdSearch / SecondToEndSearch:按位置取词或取“第二词到结尾”,用于解析 关键字 子命令 参数 这类带子命令的查询(Query.cs);
  • ActionKeyword:User 插件拿到自己被命中的关键字;System 插件通常为空。

从代码可见一个值得注意的细节:IsGlobal 并非一成不变。PluginPair.csMetadata.IsGlobal = setting.IsGlobal; 表明该标记会被持久化的插件设置覆盖,即用户在 PowerToys Run 设置中对插件“全局性”的调整最终会反映到运行时分组,这也是设计上允许 System 插件“降级”为关键字插件、User 插件“升级”为全局插件的机制入口。

四、User 插件实战:内置插件动作关键字速查表

要让 User 插件按预期触发,必须精确输入其关键字前缀(区分大小写、作为输入的最前部)。下表汇总了当前仓库各插件 plugin.json 中的真实默认关键字与全局标记(来源:src/modules/launcher/Plugins 下各插件目录的 plugin.json):

插件 默认 ActionKeyword IsGlobal(System)
Calculator 计算器 =
Indexer 文件搜索 ?
Program 程序启动 .
WindowWalker 窗口切换 <
System 系统命令 ""(无)
TimeDate 时间日期 )
Uri //
OneNote o:
WindowsTerminal _
Folder 文件夹 ""(无)
PowerToys 设置 @
WebSearch 网页搜索 ??
Shell 命令行 >
Registry 注册表 :
History 历史记录 !!
ValueGenerator 值生成 #
UnitConverter 单位换算 %%
VSCodeWorkspaces {
WindowsSettings $
Service 服务 !

使用要点(结合源码规则):

  • 关键字匹配是前缀匹配: 会命中 Registry,但若存在与 : 共用前缀的更长的关键字命中项,短前缀候选会被自动剔除,保证唯一归属;
  • 关键字区分大小写StringComparison.Ordinal);
  • 默认无关键字("")的全局插件在每次普通搜索中都会收到查询文本;
  • 每个 User 插件既可保持默认关键字,也可通过插件设置改用其他关键字甚至切换为全局模式。

五、插件配置的加载与校验管线:从目录到可运行插件

了解分类与匹配后,有必要知道一个插件如何被 PowerToys Run 接受。整个加载校验集中在 PluginConfig.cs,流程如下:

  1. 目录遍历Parse(string[] pluginDirectories) 把传入的插件根目录下的每个子目录视作一个待解析插件;
  2. 更新删除标记:若插件目录中存在 NeedDelete.txt,加载器会直接删除该目录——这是插件更新流程留下的“待清理”标记,删除失败会记录异常;
  3. 读取 plugin.json:目录下缺少 plugin.json 时记录错误并跳过;JSON 解析失败同样跳过;
  4. 五项硬性校验,任一不满足即拒载并写日志:
    • Language 必须被 AllowedLanguage 允许;
    • IcoPathDarkIcoPathLight 不得为空(缺少图标信息无法渲染结果项);
    • ExecuteFilePath 指向的程序集必须真实存在;
  5. 元数据登记:校验通过后写入 metadata.PluginDirectory = pluginDirectory,并把 ExecuteFileName 拼接为绝对执行路径(见 PluginMetadata.cs)。

这一链路保证了进入 PluginManager.AllPlugins 的每个插件都具备可渲染的图标、可加载的程序集与合法的语言标记,随后才按其 IsGlobal/ActionKeyword 进入第三、四节所述的查询分发。

六、插件作者选型与最佳实践

结合前文机制,为 PowerToys Run 编写新插件时,可依据以下准则在两类插件间做决策:

  1. 能力无关键字依赖 → 选 System 全局插件:需要“在任意普通输入下都能给出候选结果”的插件(如程序、文件、计算器、日期时间),应在 plugin.json 中声明 "ActionKeyword": """IsGlobal": true。全局插件每次都会收到完整原始查询,因此必须自行判断输入是否属于自己——通常对不匹配的输入直接返回空结果集,避免在普通搜索里制造噪音。
  2. 只在显式关键字下工作 → 选 User 关键字插件:如“注册表浏览(:)、历史记录(!!)、Shell(>)”这类语义明确、只在特定场景使用的功能,配置一个简短、易记、不与既有关键字产生前缀冲突的 ActionKeyword。注意同一插件支持用 ; 声明多个关键字。
  3. 遵守最小前缀原则:避免设计成其他关键字的前缀(如已有 ? 时不要再引入 ?? 之外的 ?x),虽然算法会以最长命中消除误判,但关键字彼此前缀重叠仍会增加歧义。
  4. 保证元数据完整:同时提供深/浅色图标路径、合法语言值与真实存在的程序集路径,否则插件将被 PluginConfig 直接拒载且仅在日志中留下错误记录。

关于插件完整开发流程、目录结构与逐插件说明,可继续阅读仓库内的配套文档:new-plugin-checklist.md(新建插件检查清单)、plugins/overview.md(各内置插件概览)与 architecture.md(PowerToys Run 架构总述)。

小结

PowerToys Run 把插件划分为“System 全局插件”与“User 关键字插件”两类,看似只写了一句话,背后却是元数据(plugin.jsonIsGlobal/ActionKeyword)、分组(PluginManager)、分发(QueryBuilder 的前缀匹配、最长关键字消歧、无命中才唤醒全局插件的兜底通道)与严格加载校验(PluginConfig)四层机制的协同。理解这条分类主线,无论是日常使用内置插件的关键字,还是为 PowerToys Run 贡献新插件,都能做到有的放矢。

登录后查看全文
热门项目推荐
相关项目推荐