PowerToys Command Palette 扩展画廊(Extension Gallery):Feed 格式、安装源与缓存机制深度解析
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.cs 将 ExtensionGalleryHttpClient、GalleryFeedUrlProvider、IExtensionGalleryService 注册为单例,并把 ExtensionGalleryItemViewModelFactory、ExtensionGalleryViewModel 注册为瞬态对象供页面绑定。
三、Feed URL 解析:设置值优先,默认值兜底
ExtensionGalleryService.GetFeedUrl()(见 ExtensionGalleryService.cs)返回结果时按下述顺序取值:
- 用户在 CmdPal 设置中配置的 URL(
SettingsModel.GalleryFeedUrl,通过隐藏的InternalPage设置页暴露)。只要非空即胜出,主要用于针对自定义 feed 做本地测试。 - 否则回落到内置默认值:
https://aka.ms/CmdPal-ExtensionsJson。
几点值得注意的细节:
- 本地
file://URI 同样被允许。FetchFeedDocumentAsync会直接File.ReadAllTextAsync读取本地文件、完全绕过 HTTP 缓存(见 ExtensionGalleryService.cs)。 - 允许的协议被白名单限制为
http、https、file三种(SupportedFeedSchemes),其他协议会被判定为非法 URL。 - 若用户把 URL 指向一个本地目录(而非文件),服务会尝试在该目录下查找固定文件名
extensions.json(TryGetFeedUri,见同文件 L345-L371)。 - 配置入口是隐藏的
InternalPage设置页:其中的GalleryFeedUrlTextBox在失去焦点时把新值写回设置(见 InternalPage.xaml 与 InternalPage.xaml.cs)。 - CI 构建会被强制忽略自定义 feed:
GalleryServiceRegistration中,当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-generatedGallerySerializationContext完成反序列化(GallerySerializationContext.Default.GalleryRemoteIndex,见 GallerySerializationContext.cs)。它开启了PropertyNameCaseInsensitive = true,并且全程无反射、对 AOT 友好;解析失败会捕获JsonException并返回null,随后在FetchWrappedFeedAsync中抛出"feed 为空或非法"的异常。
条目的必填与可选字段
| 字段 | 必填 | 说明 |
|---|---|---|
id |
是 | 小写稳定的标识符;id 为空的条目会被直接丢弃。 |
title |
是 | 显示名称。 |
description |
是 | 列表与详情视图都会展示。 |
author.name |
是 | author.url 可选。 |
installSources |
是 | 至少一个来源;详见下文"安装源"。 |
homepage、iconUrl、screenshotUrls、tags、detection.packageFamilyName |
否 | 全部可选。 |
完整条目模型比示例更丰富一些:GalleryExtensionEntry 还包含 ShortDescription、Readme 等可选属性,而 installSources 数组的每个元素由 GalleryInstallSource { Type, Id, Uri } 描述,detection 由 GalleryDetection 描述(见 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)
每个条目的 installSources 由 ExtensionGalleryItemViewModel 消费,决定界面上渲染哪种安装入口。
type |
必填字段 | 行为 |
|---|---|---|
winget |
id |
启用"通过 WinGet 安装"按钮(复用共享的 WinGet 服务),并加入进行中的安装进度以及已安装/可更新状态的判定。 |
msstore |
id |
打开 ms-windows-store://pdp/?ProductId={id}。 |
url |
uri |
依据域名主机显示为 "GitHub" 或 "Website" 链接。 |
一个条目可以同时声明多种来源;对于运行时无法识别的来源类型,界面会以"unknown source(未知来源)"指示器呈现,避免静默丢弃。
六、拉取与缓存机制(Fetching and caching)
ExtensionGalleryService 使用 ExtensionGalleryHttpClient,后者在文件系统缓存之上包装了 HttpCachingClient。feed JSON 与可缓存的图标 URL 都走这套缓存。
各缓存/网络参数如下(常量定义于 ExtensionGalleryHttpClient.cs 与 ExtensionGalleryService.cs):
| 设置 | 值 | 定义处 |
|---|---|---|
| 缓存根目录 | {AppCache}\GalleryCache\ |
ExtensionGalleryHttpClient.CacheDirectoryName |
| Feed TTL | 4 小时 | ExtensionGalleryHttpClient.DefaultTimeToLive |
| 图标 TTL | 24 小时 | ExtensionGalleryService.IconCacheTtl(TimeSpan.FromDays(1)) |
| HTTP 超时 | 15 秒 | ExtensionGalleryHttpClient 中 TimeoutSeconds |
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:
- 解析 feed URL(规则见第三节)。
- 本地文件直接读盘;否则交给
HttpCachingClient.GetResourceAsync,其内部策略:- 若存在未过 TTL 的缓存副本,直接返回缓存,不触网;
- 否则发起条件 GET(ETag /
If-None-Match);收到304 Not Modified时刷新缓存元数据并返回缓存体; - 网络失败时返回"最后已知"的缓存体并置
UsedFallbackCache = true,以便 UI 展示"数据过期(stale data)"横幅。
- 用 source-generated 的
GallerySerializationContext解析 JSON(强类型GalleryRemoteIndex,无反射、AOT 友好)。 - 丢弃缺失
id的条目、归一化相对iconUrl与screenshotUrls,并通过同一 HTTP 缓存把远程图标 URI 解析为本地file://URI 交给 UI 绑定(LocalizeIconUrisAsync对每个条目并发执行)。 - 强制刷新成功后,
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 都会被捕获并转为带 HasError 的 GalleryFetchResult;其中 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 的 installSources 与 detection 字段上,不做逐条网络探测,从而兼顾了启动速度、离线可用与弱网韧性。
若你需要在本地验证自定义 feed:
- 在隐藏的
InternalPage设置页填入GalleryFeedUrl(或直接使用file:///指向本地extensions.json); - 相对
iconUrl/screenshotUrls会被解析到 feed 文件所在目录,便于组织本地素材; - 触发一次手动刷新(
RefreshAsync),即可观察缓存、304与"stale data"兜底各分支的效果。
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