首页
/ PowerToys Command Palette 扩展画廊(Extension Gallery):Feed 格式、安装源与缓存机制深度解析

PowerToys Command Palette 扩展画廊(Extension Gallery):Feed 格式、安装源与缓存机制深度解析

2026-09-06 18:53:54作者:申梦珏Efrain

PowerToys Command Palette(CmdPal)的应用内 Extension gallery(扩展画廊) 页面用于发现、查看并安装第三方扩展。它不依赖 WinGet 发现或逐条拉取 manifest,而是将全部扩展信息收敛进一个远程 JSON feed(extensions.json),由客户端解析、渲染并做磁盘缓存。本文基于 PowerToys 仓库内的原文档,逐层拆解该 feed 的 URL 解析优先级、JSON 字段约束、安装源(install sources)语义、HTTP 缓存与离线兜底策略,并结合 Microsoft.CmdPal.Common 下的源码给出可供本地开发、投稿与自定义 feed 时直接复用的实操细节。

一、画廊运行机制总览(At a glance)

从架构上,CmdPal 的 Extension gallery 只做"一件事":从一个远程 HTTPS URL 加载名为 extensions.json 的 JSON feed,解析后渲染条目。要点如下:

  • 画廊加载单一 JSON feed,无按扩展逐个发起的网络请求;
  • 默认 feed 托管在外部仓库 microsoft/CmdPal-Extensions,其发布地址为 https://aka.ms/CmdPal-ExtensionsJson
  • feed 内容与图标图片都会落到本地磁盘缓存,页面因此可离线工作、可承受短暂网络抖动;
  • 没有 WinGet 发现机制、没有逐扩展的 manifest.json 拉取、列表渲染阶段没有任何其他网络调用。

这种"单源 + 强缓存"设计,让画廊页面可以秒开,同时把扩展元数据(标题、作者、截图、安装方式)的维护集中到 feed 仓库一侧。

二、代码路径地图(Implementation pointers)

整个画廊功能横跨 Common(无 UI 的服务与模型)和 UI(视图层)两个程序集,原文档给出了定位代码的快速索引:

关注点 文件
拉取、解析、缓存、修剪 ExtensionGalleryService.cs
解析最终请求哪个 URL GalleryFeedUrlProvider.cs + GalleryServiceRegistration.cs
HTTP + 磁盘缓存 ExtensionGalleryHttpClient.cs(内部包装 HttpCachingClient
Feed 与条目的强类型模型 Models

依赖注入层面,GalleryServiceRegistration.csExtensionGalleryHttpClientGalleryFeedUrlProviderIExtensionGalleryService 注册为单例,并把 ExtensionGalleryItemViewModelFactoryExtensionGalleryViewModel 注册为瞬态对象供页面绑定。

三、Feed URL 解析:设置值优先,默认值兜底

ExtensionGalleryService.GetFeedUrl()(见 ExtensionGalleryService.cs)返回结果时按下述顺序取值:

  1. 用户在 CmdPal 设置中配置的 URLSettingsModel.GalleryFeedUrl,通过隐藏的 InternalPage 设置页暴露)。只要非空即胜出,主要用于针对自定义 feed 做本地测试。
  2. 否则回落到内置默认值:https://aka.ms/CmdPal-ExtensionsJson

几点值得注意的细节:

  • 本地 file:// URI 同样被允许FetchFeedDocumentAsync 会直接 File.ReadAllTextAsync 读取本地文件、完全绕过 HTTP 缓存(见 ExtensionGalleryService.cs)。
  • 允许的协议被白名单限制为 httphttpsfile 三种(SupportedFeedSchemes),其他协议会被判定为非法 URL。
  • 若用户把 URL 指向一个本地目录(而非文件),服务会尝试在该目录下查找固定文件名 extensions.jsonTryGetFeedUri,见同文件 L345-L371)。
  • 配置入口是隐藏的 InternalPage 设置页:其中的 GalleryFeedUrlTextBox 在失去焦点时把新值写回设置(见 InternalPage.xamlInternalPage.xaml.cs)。
  • CI 构建会被强制忽略自定义 feedGalleryServiceRegistration 中,当 BuildInfo.IsCiBuild 为真时,URL provider 直接返回 null,保证 CI 环境行为可复现(见 GalleryServiceRegistration.cs)。

四、Feed 格式:单一包裹式 JSON

feed 是一份"包裹式(wrapped)"JSON 文档,扩展数据内联其中:

{
  "$schema": "https://raw.githubusercontent.com/microsoft/CmdPal-Extensions/main/.github/schemas/gallery.schema.json",
  "extensions": [
    {
      "id": "sample-extension",
      "title": "Sample Extension",
      "description": "A sample extension demonstrating the gallery feed format.",
      "author": { "name": "Microsoft", "url": "https://github.com/microsoft" },
      "homepage": "https://github.com/microsoft/CmdPal-Extensions",
      "iconUrl": "https://.../icon.png",
      "screenshotUrls": ["https://.../screenshot-1.png"],
      "tags": ["sample"],
      "installSources": [
        { "type": "winget",  "id":  "Contoso.SampleExtension" },
        { "type": "msstore", "id":  "9P…" },
        { "type": "url",     "uri": "https://github.com/contoso/sample/releases/latest" }
      ],
      "detection": { "packageFamilyName": "Contoso.SampleExtension_1234567890abc" }
    }
  ]
}

运行时只读取 extensions 数组。在模型层,它被反序列化为强类型的 GalleryRemoteIndex——该类型只有 Extensions 一个属性(见 GalleryRemoteIndex.cs)。条目级别的权威 JSON schema 位于上游 feed 仓库 microsoft/CmdPal-Extensions,仓库文档明确提示不要在本地重复维护一份 schema,因为它会漂移。

解析实现细节:TryParseWrappedGallery 使用 source-generated GallerySerializationContext 完成反序列化(GallerySerializationContext.Default.GalleryRemoteIndex,见 GallerySerializationContext.cs)。它开启了 PropertyNameCaseInsensitive = true,并且全程无反射、对 AOT 友好;解析失败会捕获 JsonException 并返回 null,随后在 FetchWrappedFeedAsync 中抛出"feed 为空或非法"的异常。

条目的必填与可选字段

字段 必填 说明
id 小写稳定的标识符;id 为空的条目会被直接丢弃。
title 显示名称。
description 列表与详情视图都会展示。
author.name author.url 可选。
installSources 至少一个来源;详见下文"安装源"。
homepageiconUrlscreenshotUrlstagsdetection.packageFamilyName 全部可选。

完整条目模型比示例更丰富一些:GalleryExtensionEntry 还包含 ShortDescriptionReadme 等可选属性,而 installSources 数组的每个元素由 GalleryInstallSource { Type, Id, Uri } 描述,detectionGalleryDetection 描述(见 GalleryExtensionEntry.cs)。

相对 URL 处理:相对形式的 iconUrl / screenshotUrls 会基于 feed URL 所在的目录解析为绝对地址——该特性在本地开发或 file:// feed 场景下才真正有用。代码层面 NormalizeEntry 会先尝试把值当作绝对 URI,失败后再尝试以 feed 的基目录(TryGetBaseDirectoryUri 计算出的 new Uri(feedUri, "."))拼接,且拼接结果必须落在基目录前缀内,否则保持原样返回(见 ExtensionGalleryService.cs)。

运行时对条目的"清洗"

NormalizeRemoteEntries 在解析后对列表做两件事:

  • 反向遍历删除 Id 为空白字符串的条目;
  • 对保留下来的条目执行 Id.Trim() 并归一化其 icon/screenshot URL。

也就是说,即使上游数据不规范,运行时也会尽力保证 UI 层拿到的列表是"干净且带合法 id"的。

五、安装源(Install sources)

每个条目的 installSourcesExtensionGalleryItemViewModel 消费,决定界面上渲染哪种安装入口。

type 必填字段 行为
winget id 启用"通过 WinGet 安装"按钮(复用共享的 WinGet 服务),并加入进行中的安装进度以及已安装/可更新状态的判定。
msstore id 打开 ms-windows-store://pdp/?ProductId={id}
url uri 依据域名主机显示为 "GitHub" 或 "Website" 链接。

一个条目可以同时声明多种来源;对于运行时无法识别的来源类型,界面会以"unknown source(未知来源)"指示器呈现,避免静默丢弃。

六、拉取与缓存机制(Fetching and caching)

ExtensionGalleryService 使用 ExtensionGalleryHttpClient,后者在文件系统缓存之上包装了 HttpCachingClientfeed JSON 与可缓存的图标 URL 都走这套缓存。

各缓存/网络参数如下(常量定义于 ExtensionGalleryHttpClient.csExtensionGalleryService.cs):

设置 定义处
缓存根目录 {AppCache}\GalleryCache\ ExtensionGalleryHttpClient.CacheDirectoryName
Feed TTL 4 小时 ExtensionGalleryHttpClient.DefaultTimeToLive
图标 TTL 24 小时 ExtensionGalleryService.IconCacheTtlTimeSpan.FromDays(1)
HTTP 超时 15 秒 ExtensionGalleryHttpClientTimeoutSeconds
User-Agent PowerToys-CmdPal/1.0 ExtensionGalleryHttpClient

缓存路径拼装发生在 ExtensionGalleryHttpClient 构造函数:以 applicationInfoService.CacheDirectory 为根,拼接 CacheDirectoryName

{AppCache} 的实际解析规则:CmdPal 以**打包(packaged)**方式运行时对应 ApplicationData.Current.LocalCacheFolder;**未打包(unpackaged)**运行时则对应 %LOCALAPPDATA%\Microsoft\PowerToys\Microsoft.CmdPal\Cache\。读者可在 ApplicationInfoService.DetermineCacheDirectory 中核对具体分支。

抓取流程

FetchExtensionsAsync(正常加载)与 RefreshAsync(用户手动刷新,forceRefresh: true)殊途同归,都走 FetchWrappedFeedAsync

  1. 解析 feed URL(规则见第三节)。
  2. 本地文件直接读盘;否则交给 HttpCachingClient.GetResourceAsync,其内部策略:
    • 若存在未过 TTL 的缓存副本,直接返回缓存,不触网
    • 否则发起条件 GET(ETag / If-None-Match;收到 304 Not Modified 时刷新缓存元数据并返回缓存体;
    • 网络失败时返回"最后已知"的缓存体并置 UsedFallbackCache = true,以便 UI 展示"数据过期(stale data)"横幅。
  3. 用 source-generated 的 GallerySerializationContext 解析 JSON(强类型 GalleryRemoteIndex,无反射、AOT 友好)。
  4. 丢弃缺失 id 的条目、归一化相对 iconUrlscreenshotUrls,并通过同一 HTTP 缓存把远程图标 URI 解析为本地 file:// URI 交给 UI 绑定(LocalizeIconUrisAsync 对每个条目并发执行)。
  5. 强制刷新成功后,PruneCachedResources删除不再被当前 feed 引用的缓存项(旧的 feed URL 与已从 feed 中消失的图标 URL)。注意清理发生在 forceRefresh && !UsedFallbackCache 时才执行,即必须确保本次拿到的是真·网络新数据而非缓存兜底,避免误删(见 ExtensionGalleryService.cs)。

此外,图标本地化遵循以下边界条件:

  • file://ms-appx:// 协议的图标 URI 原样保留,不做缓存化;
  • 无法缓存化(非 http/https)或拉取失败的图标会被置空或记录错误日志("Failed to resolve extension gallery icon '{IconUri}'."),但不会导致整个列表加载失败。

抓取结果标志

FetchExtensionsAsync 返回 GalleryFetchResult,视图模型据此渲染 UI 提示:

标志 含义
FromCache feed 来自缓存且未触网(TTL 仍有效)。
UsedFallbackCache 发起过网络请求但失败,兜底返回缓存副本;UI 显示"数据过期"信息条。
IsRateLimited(原文档称 RateLimited 源站返回 429 Too Many Requests 且无缓存兜底;UI 显示限流错误。

服务端对异常的归一化逻辑:所有 HttpRequestException/IOException/TaskCanceledException/InvalidOperationException/UriFormatException 都会被捕获并转为带 HasErrorGalleryFetchResult;其中 HttpRequestException.StatusCode == HttpStatusCode.TooManyRequests 专门映射为限流标志(见 ExtensionGalleryService.cs)。

七、为画廊投稿(Authoring)与维护建议

  • 生产画廊的条目统一添加在上游 feed 仓库 microsoft/CmdPal-Extensions,本文只描述运行时对 feed 的消费方式。
  • 条目在编辑器中的校验:通过条目的 $schema 字段引用上游仓库发布的 schema。
  • 一旦发布扩展就保持 id 稳定——用户可能已经安装过它,画廊依据 id 关联安装状态。
  • 若扩展通过 App Installer 分发,优先提供 winget 来源:画廊既用它显示状态("Installed(已安装)" / "Update available(有可用更新)"),也用它在应用内完成安装。
  • detection.packageFamilyName 让画廊能在 WinGet 元数据解析完成之前,先行识别已经安装的打包扩展。

八、深链:从 x-cmdpal:// 直达画廊

CmdPal 的 x-cmdpal 协议可以在 Settings 窗口中打开画廊首页或某个扩展的详情页:

x-cmdpal://extensions/gallery
x-cmdpal://extensions/gallery/{extension-id}

例如 x-cmdpal://extensions/gallery/jiripolasek.colors 会打开 Colors 扩展的详情页。

使用与校验规则:

  • {extension-id} 请使用画廊 feed 中稳定存在的 id,必要时将其作为单个 path segment 做 URL 编码;
  • 带首尾空白、含路径分隔符、控制字符或长度超过 256 字符的 id 会被拒绝
  • 扩展 id 匹配不区分大小写
  • 若目标 id 不在当前 feed 中,Settings 会停留在画廊页面而非报错。

这些边界行为在仓库测试中得到印证,例如 CmdPalProtocolActivationTests.cs 覆盖了画廊深链解析(含空格、%20%2F、换行 %0A#fragment 等反例),UriBreadcrumbsTests.cs 则验证了深链在面包屑导航中的行为。

九、小结与本地测试建议

总结整条链路:画廊页面 → 单一远程 feed(默认 https://aka.ms/CmdPal-ExtensionsJson)→ 条件 GET + 磁盘缓存(feed 4h / 图标 24h)→ 强类型 JSON 解析与条目清洗 → UI 渲染与安装入口分发。它把"发现、状态、安装"全部收敛到 feed 的 installSourcesdetection 字段上,不做逐条网络探测,从而兼顾了启动速度、离线可用与弱网韧性。

若你需要在本地验证自定义 feed:

  1. 在隐藏的 InternalPage 设置页填入 GalleryFeedUrl(或直接使用 file:/// 指向本地 extensions.json);
  2. 相对 iconUrl/screenshotUrls 会被解析到 feed 文件所在目录,便于组织本地素材;
  3. 触发一次手动刷新(RefreshAsync),即可观察缓存、304 与"stale data"兜底各分支的效果。
登录后查看全文
热门项目推荐
相关项目推荐