PowerToys Run 插件体系解析:System 全局插件与 User 关键字插件的分类、配置与查询分发机制
导读
PowerToys Run 是 Windows 下的快速启动器,其全部能力都来自一个可插拔的插件体系。仓库内 src/modules/launcher/PowerLauncher/Plugin/README.md 用极简的篇幅点明了整个体系最核心的分类法则:插件只有两类——无需动作关键字(Action Keyword)的 System 系统插件,与必须携带自定义动作关键字的 User 用户插件。本文以此文档为主干,结合 plugin.json 元数据、QueryBuilder 查询分发算法与 PluginConfig 配置加载管线等源码,完整还原这一分类机制如何驱动 PowerToys Run 的每一次搜索,并给出内置插件的关键字速查表与插件开发者的选型建议。
一、文档原文与体系总览:插件只有两类
原文档给出 PowerToys Run 插件分类的唯一权威定义:
- System plugin(系统插件):其动作关键字为
"*",这类插件不需要动作关键字即可被触发; - User Plugin(用户插件):这类插件拥有自定义的动作关键字,用户必须输入该关键字前缀才能定向触发。
把这段话映射到当前仓库的代码,可以发现它正是 PowerToys Run(其核心命名空间仍沿用历史项目 Wox 的 Wox.Plugin)插件元数据与查询分发机制的设计出发点:
- Wox.Plugin/Query.cs 中保留着分类的“历史符号”:
/// <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)。插件是“系统型”还是“用户型”,正取决于该文件中的 IsGlobal 与 ActionKeyword 两个字段。
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.System(plugin.json):关键字为空、IsGlobal: true,无需输入任何前缀即可让“系统命令”参与搜索。
{
"ActionKeyword": "",
"IsGlobal": true,
"Name": "System Commands",
"ExecuteFileName": "Microsoft.PowerToys.Run.Plugin.System.dll"
}
User(关键字)插件示例 —— Microsoft.PowerToys.Run.Plugin.History(plugin.json):以 "!!" 作为自定义关键字,用户键入 !! 前缀时才定向触发。
{
"ActionKeyword": "!!"
}
三、系统插件为什么“不需要动作关键字”:查询分发算法源码剖析
两类插件在运行时被分发的差异,全部集中在查询构建器 QueryBuilder.cs 的 Build(string text) 方法中。它是理解整个分类机制的关键代码,核心决策流程如下:
- 裁剪输入:对用户输入
text.Trim()。 - 第一轮:只遍历非全局插件(User 插件)做关键字匹配。对每个未被禁用的非全局插件,若输入文本以其
ActionKeyword前缀开头(StringComparison.Ordinal,即区分大小写),则为该插件构造一个Query(关键字被剥离并写入Query.ActionKeyword),记入候选字典。 - 最长关键字消歧:当多个关键字存在公共前缀(例如
?与??,或>与>>),会出现“假命中”。算法记录本轮命中的最长关键字长度,随后把关键字较短的所有候选插件从字典中移除,确保??输入只归属真正的双字符插件,而不是同时误伤单字符插件。 - 第二轮:兜底引入全局插件(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.cs 中 Metadata.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,流程如下:
- 目录遍历:
Parse(string[] pluginDirectories)把传入的插件根目录下的每个子目录视作一个待解析插件; - 更新删除标记:若插件目录中存在
NeedDelete.txt,加载器会直接删除该目录——这是插件更新流程留下的“待清理”标记,删除失败会记录异常; - 读取 plugin.json:目录下缺少
plugin.json时记录错误并跳过;JSON 解析失败同样跳过; - 五项硬性校验,任一不满足即拒载并写日志:
Language必须被 AllowedLanguage 允许;IcoPathDark、IcoPathLight不得为空(缺少图标信息无法渲染结果项);ExecuteFilePath指向的程序集必须真实存在;
- 元数据登记:校验通过后写入
metadata.PluginDirectory = pluginDirectory,并把ExecuteFileName拼接为绝对执行路径(见 PluginMetadata.cs)。
这一链路保证了进入 PluginManager.AllPlugins 的每个插件都具备可渲染的图标、可加载的程序集与合法的语言标记,随后才按其 IsGlobal/ActionKeyword 进入第三、四节所述的查询分发。
六、插件作者选型与最佳实践
结合前文机制,为 PowerToys Run 编写新插件时,可依据以下准则在两类插件间做决策:
- 能力无关键字依赖 → 选 System 全局插件:需要“在任意普通输入下都能给出候选结果”的插件(如程序、文件、计算器、日期时间),应在
plugin.json中声明"ActionKeyword": ""与"IsGlobal": true。全局插件每次都会收到完整原始查询,因此必须自行判断输入是否属于自己——通常对不匹配的输入直接返回空结果集,避免在普通搜索里制造噪音。 - 只在显式关键字下工作 → 选 User 关键字插件:如“注册表浏览(
:)、历史记录(!!)、Shell(>)”这类语义明确、只在特定场景使用的功能,配置一个简短、易记、不与既有关键字产生前缀冲突的ActionKeyword。注意同一插件支持用;声明多个关键字。 - 遵守最小前缀原则:避免设计成其他关键字的前缀(如已有
?时不要再引入??之外的?x),虽然算法会以最长命中消除误判,但关键字彼此前缀重叠仍会增加歧义。 - 保证元数据完整:同时提供深/浅色图标路径、合法语言值与真实存在的程序集路径,否则插件将被 PluginConfig 直接拒载且仅在日志中留下错误记录。
关于插件完整开发流程、目录结构与逐插件说明,可继续阅读仓库内的配套文档:new-plugin-checklist.md(新建插件检查清单)、plugins/overview.md(各内置插件概览)与 architecture.md(PowerToys Run 架构总述)。
小结
PowerToys Run 把插件划分为“System 全局插件”与“User 关键字插件”两类,看似只写了一句话,背后却是元数据(plugin.json 的 IsGlobal/ActionKeyword)、分组(PluginManager)、分发(QueryBuilder 的前缀匹配、最长关键字消歧、无命中才唤醒全局插件的兜底通道)与严格加载校验(PluginConfig)四层机制的协同。理解这条分类主线,无论是日常使用内置插件的关键字,还是为 PowerToys Run 贡献新插件,都能做到有的放矢。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00