Motrix 插件市场:Registry v2 线协议、本地化解析与安装同意流水线全解
Motrix 的插件市场(Marketplace)依赖一份发布于 https://dl.motrix.app/registry/plugins.json 的 registry v2 线协议(wire contract)。外部 registry 仓库持有源 schema 的所有权,而 Motrix 应用内部只 vendor 了一份“宽容(tolerant)”的消费端 schema,并配套维护了字节一致的 fixture 与三通道一致性语料(conformance corpus)。读完本文,你将掌握 Motrix 如何防御性地拉取与缓存注册表、如何按 BCP 47 子集解析插件本地化文案、以及如何通过“两阶段安装 + 注册表↔清单一致性门禁 + SHA-256 校验 + 保留命名空间”这条流水线,在用户明确同意之前阻止任何一次不安全的插件安装。
一、Registry v2 线协议与“宽容消费端”设计
Motrix 消费 registry v2 的核心约束可以归纳为三条:
- v2 的演进是 additive-only(仅可加性)。任何字段不得被重命名、改类型或删除;schema 变更必须在 registry 发布方、Motrix 应用与官网(website)三方之间协调,包括那份字节一致的 fixture 和所有 conformance 通道。
- 应用侧用 vendor 的 Zod schema 解析注册表数据。未知的新增字段、新增的 category slug 都仍然是“线上合法(wire-valid)”的;只在展示边界(presentation boundary)才把它们收窄(narrow)。非法响应应当保留最近一次成功的缓存(last-good cache),而不是让市场 UI 崩溃。
listing.defaultLocale与开放的listing.localizations映射遵循 registry 的 BCP 47 profile,与应用自身的SupportedLocale相互独立;展示/搜索文本必须经由共享的字段级解析器(field-level resolver)解析,且不得臆断相邻区域(例如不能因为缺zh-TW就回退到zh-CN)。
这些约定在仓库中的落点是 src/shared/schemas/registry.ts。文件头部注释直接点明了设计立场:
// Vendored tolerant wire contract for Marketplace registry v2.
//
// Source of truth: plugin-registry/schema/registry.ts. Publisher authoring is
// intentionally stricter; this consumer preserves additive wire fields.
export const REGISTRY_URL = 'https://dl.motrix.app/registry/plugins.json'
/** Last-good cache file under userData, shared by both shells. */
export const REGISTRY_CACHE_FILENAME = 'registry-cache.json'
export const REGISTRY_PLUGIN_ID_RE = /^[a-z0-9-]+(\.[a-z0-9-]+)+$/
三个导出值得逐一看懂:
REGISTRY_URL:v2 注册表的固定入口地址,是RegistryClient的默认拉取目标。REGISTRY_CACHE_FILENAME:last-good 缓存在userData下的文件名,Electron 与 server 两个 shell 共用。REGISTRY_PLUGIN_ID_RE:插件 ID 必须形如<publisher>.<slug>的反向域名式命名空间(全小写字母、数字、连字符,至少两级)。这个正则后续在 src/core/plugin/install/plugin-installer.ts 中被复用为安装目录安全校验(防止../../之类路径逃逸)。
顶层文件 schema 严格要求 version 字面量等于 2,并对重复 ID 做交叉校验:
export const RegistryFileSchema = z
.object({
version: z.literal(2),
generatedAt: z.iso.datetime(),
plugins: z.array(RegistryPluginSchema),
})
.passthrough()
.superRefine(addDuplicateIdIssues)
“宽容”体现在三处:
-
.passthrough():RegistryFileSchema、RegistryPluginSchema、RegistryListingSchema、RegistryLocalizationSchema全部启用 passthrough,未来发布方新增的自有字段会被原样保留,老消费者解析不报错——这正是“additive-only 演进”能在不 bump 大版本的情况下成立的前提。 -
遗留字段黑名单:v1 曾把
name/description/features以{ en, zh? }的形式存在插件根上;v2 里这三个 key 被显式禁止(forbidden),防止 v1/v2 混合条目被误判为“向前兼容的 v2 扩展字段”:const LEGACY_LOCALIZED_PLUGIN_ROOT_KEYS = ['name', 'description', 'features'] as const -
单条插件的结构(
commonPluginShape):id用上述命名空间正则;origin限定为builtin | community;permissions/optionalPermissions/hostPermissions三个权限数组默认[];package是可选块,内含url(合法 URL)、sha256(64 位小写十六进制)、size(正整数字节数)、可选signature;updatedAt是 ISO 日期,featured是布尔。
真实条目长什么样,可以直接看共享 fixture src/shared/schemas/registry.fixture.json:
{
"id": "example.archive-unpacker",
"listing": {
"defaultLocale": "en-US",
"localizations": {
"en-US": { "name": "Example Archive Unpacker", "description": "Unpacks finished archive downloads...", "features": ["..."], "keywords": ["archive", "extract"] },
"zh-CN": { "name": "示例压缩包解压器", "description": "下载完成后将压缩包解压到同级目录。", "features": ["支持 zip、tar、7z 压缩包"], "keywords": ["压缩包", "解压"] },
"ja-JP": { "name": "サンプル・アーカイブ展開", "features": [] }
}
},
"version": "2.1.0",
"origin": "community",
"engines": { "motrix": ">=2.1.0 <3.0.0" },
"permissions": ["fs.task.write"],
"optionalPermissions": ["notifications"],
"package": { "url": "https://...", "sha256": "4f2d...5432", "size": 48213 },
"updatedAt": "2026-07-08",
"featured": true
}
fixture 文件第一行注释写得很直白:"Shared registry v2 wire fixture. Copy byte-for-byte into motrix-turbo and motrix-website." —— 这就是规则文档所说的“byte-identical fixture”:同一份 JSON 被逐字节复制进 Motrix 主仓、发布器仓(motrix-turbo)与官网仓,三个通道共享同一份数据做一致性验证。配套的三通道语料是 src/shared/schemas/registry.conformance.json,其注释说明 wireExpected 跑在所有宽容线解析器中、authoringExpected 只跑在 plugin-registry 的严格源校验中、resolverExpected 跑在消费端;src/shared/schemas/registry.test.ts 遍历 conformance.cases 对这些行为做回归断言。
二、本地化:BCP 47 子集 profile 与字段级解析器
registry 本地化不依赖 ICU 的完整 BCP 47 知识,而是实现了一个刻意收窄的“安全 profile”。src/shared/schemas/registry.ts 中的 isMotrixLocaleTag 只接受:2–8 位小写字母语言码、可选的大写 Script(Hant 这类 4+1 形式)、可选 Region(两位大写字母或三位数字),之后是若干个不重复的 variant;extension 与 private-use 子标签被明确排除。任何候选标签再经 MotrixLocaleTagSchema(Zod refine)校验后才能作为 defaultLocale 或 localizations 的 key。
这条规则的动机在注释中写得很清楚:“ICU-independent consumer profile… Alias and registry knowledge are intentionally not required here.” 即消费端不维护别名表(如 in -> id),也绝不猜测“可能的同族区域”。
展示文本的最终取值由共享的字段级解析器 resolveRegistryListing(listing, requestedLocale) 完成,其行为是:
- 用
Intl.Locale归一化请求语言,生成候选序列:baseName→ 逐级截断的父级 →maximize()得到的language-Script组合 → 纯language→defaultLocale; - **逐字段(name / description / features / keywords 独立)**按候选顺序查找:哪个本地化记录“自有该字段”就用哪个,全部落空才回退到
defaultLocale记录的对应字段; ja-JP只给了features: []而没给keywords时,keywords会继续向前查找,不会连带ja-JP的其他字段整体接管。
这就是“不要臆断 zh-TW -> zh-CN”的具体含义:zh-TW 的候选序列是 zh-TW → zh → defaultLocale,zh-CN 永远不会出现在候选里。RegistryListingSchema 还有两条结构约束:defaultLocale 必须是 localizations 的自有 key,且默认本地化必须同时含 name 与 description;非默认本地化不能是空对象(至少含一个自有字段)。
三、防御性读侧客户端:RegistryClient
规则文档要求“非法响应保留 last-good cache 而不是让市场 UI 崩溃”。实现落在 src/core/plugin/registry/registry-client.ts,类注释原话是“Public methods never throw: failures retain a validated last-good snapshot, or return null when none exists.”
关键参数与行为(均可在源码中逐项核对):
| 常量 / 行为 | 值 | 位置 |
|---|---|---|
| 默认 TTL | 6 小时(DEFAULT_TTL_MS) |
registry-client.ts |
| 拉取超时 | 15 秒(FETCH_TIMEOUT_MS) |
registry-client.ts |
| 响应体上限 | 4 MiB(MAX_REGISTRY_BYTES),磁盘缓存上限 +64 KiB |
registry-client.ts |
| 缓存文件 | userData/registry-cache.json,cacheFormat: 2 信封(含 etag / fetchedAt / raw) |
REGISTRY_CACHE_FILENAME |
值得展开的防御细节:
- 条件请求与 304 短路:刷新时携带
if-none-match: <etag>;收到 304 时取消响应体、只更新fetchedAt并持久化,直接复用内存中已校验的文件对象。 - 有界读取:
readBoundedResponse先检查content-length声明,再对 body 做流式累积,任一时刻超过 4 MiB 立即 abort 并reader.cancel();TextDecoder以fatal: true解码,非法 UTF-8 也会失败。超时竞赛(Promise.race)若先输,迟到的响应体会被显式cancel,避免连接悬挂。 - 磁盘缓存同样有界:
loadDisk先stat再读文件,尺寸超限、JSON 损坏、信封 schema 不符、raw 再解析失败,一律把缓存置空并退化为无条件请求——绝不把坏缓存投喂给 UI。 - 原子落盘:
persist()用write-file-atomic写缓存,避免进程在写一半时留下截断文件。 - 兼容性标注:
list(hostVersion)为每条插件追加compatible: semverSatisfies(hostVersion, entry.engines.motrix),得到RegistryPluginDTO;结果按“解析文件身份 + host 版本”记忆化,304 不改变文件身份就不会重算。 - 并发去重:
refresh()用inflight单例合并重复刷新;loadDiskOnce()保证磁盘读取只发生一次。
同一目录下的 registry-fetcher.ts 与 update-scan.ts 分别承担抓取与更新扫描职责(均有对应单测 registry-client.test.ts 等覆盖)。
四、包下载:有界重定向、流式落盘与 SHA-256 前置校验
规则文档对安装边界的要求是:只允许初始 HTTPS 包 URL 走 allowlist 语义,重定向必须有界、流式,且必须在解析清单之前验证最终大小与 registry 声明的 SHA-256。
URL 下载器 src/core/plugin/install/url-fetcher.ts 的实现与之逐条对应:
const MAX_REDIRECTS = 5
// ...
const dispatcher = agent.compose(
interceptors.redirect({ maxRedirections: MAX_REDIRECTS })
)
const response = await request(url, { dispatcher })
// ...
await pipeline(response.body, createWriteStream(destFile, { mode: 0o600 }))
要点:undici Agent 每次下载独立创建并在 finally 中 destroy(),确保“释放每一个拥有的 socket”;重定向上限 5 跳;响应体通过 pipeline 流式写入目标文件(权限 0o600),失败时取消 body 并清理部分文件,非 200 抛出带状态码的 AppError。
SHA-256 前置校验发生在安装流水线的 stage 阶段,见 src/core/plugin/install/plugin-installer.ts:
const pinnedSha256 =
sourceInput.type === 'local'
? sourceInput.fileHash
: (options?.expectedSha256 ?? options?.expect?.packageSha256)
if (pinnedSha256 && loadedMoext.archiveSha256 !== pinnedSha256) {
throw new AppError(ErrorCode.PluginManifestInvalid, /* sha256_mismatch */)
}
即:从注册表安装时,钉住的摘要来自 registry 条目的 package.sha256(经由 RegistryExpectation.packageSha256 传递);本地安装则用本地文件自身哈希。只有在摘要一致后才解包、才解析 manifest.json——“先验包,后解清单”。
五、两阶段安装与注册表↔清单一致性门禁
Motrix 的安装是两阶段的(plugin-installer.ts 头部注释):
stage(moextPath, sourceInput):解压到 staging 目录、解析清单、构建同意载荷(ConsentPayload)与相对旧安装的信任面 diff(diffTrustSurface),返回{ stagingId, consent };渲染层此时弹出同意对话框,且可以无限期保持打开。commit(stagingId, grants):用户确认后落盘——升级时先onDeactivate、备份旧目录、换入新目录、写_install.json、刷新注册表并删备份;换入中途失败则回滚备份。
其中与 registry 契约直接相关的是 §6.3 一致性门禁,由 src/core/plugin/install/registry-expectation.ts 实现:
export function buildRegistryExpectation(entry: RegistryPlugin): RegistryExpectation { /* 从 registry 条目构建期望 */ }
export function assertMatchesRegistryExpectation(
manifest: PluginManifest,
expected: RegistryExpectation
): void {
if (manifest.id !== expected.id) mismatch('id')
if (manifest.version !== expected.version) mismatch('version')
if (manifest.engines.motrix !== expected.enginesMotrix) mismatch('engines.motrix')
if (!isSubset(manifest.permissions, expected.permissions)) mismatch('permissions')
if (!isSubset(manifest.optionalPermissions ?? [], expected.optionalPermissions)) mismatch('optionalPermissions')
if (!isSubset(manifest.hostPermissions ?? [], expected.hostPermissions)) mismatch('hostPermissions')
}
规则与规则文档完全一致,且语义刻意区分:
id、version、engines.motrix必须逐字相等——任何漂移都意味着“你看到的目录页”和“你拿到的包”不是同一个东西;- 三类权限(required / optional / host)只允许是 registry 预览的“子集”:打包清单可以申请更少权限,但绝不能申请比用户在目录页预览过的更多权限;
- 任一字段不匹配即抛出
PluginManifestInvalid(错误键plugin.install.registry_manifest_mismatch,并携带出错的字段名);stage()捕获该异常后会删除整个 staging 目录再向上抛,保证没有“半安装”残留。
门禁在 stage 中的调用位置也在源码中可查:解析清单成功后立刻执行
if (options?.expect) {
assertMatchesRegistryExpectation(parsedManifest, options.expect)
}
关于“grants 永远派生自解析后的清单”:commit 落盘的授权快照 ConsentSnapshot 由 parsedManifest 直接构造(permissions、optionalPermissions、invokesCommands、hostPermissions、enginesMotrix 等字段都取自包内清单),registry 条目只参与“能不能装”的判定,不参与“装完后给什么权限”。同意对话框渲染的数据则由 consent-payload.ts 统一构建:权限描述走 permission.* i18n 键(未知权限回退到 permission.<name>.description 保持前向兼容),host 权限模式若是 <all_urls>、https://*/* 等宽泛模式会被标记 broad: true 用于高亮告警。
server 运行时额外注入一道 serverAck 钩子(plugin-installer.ts 中 isServerAckSatisfied 的注入点),在构建同意载荷之前即可拒绝安装,对应 src/server/plugin/install-service.ts 与 server-ack.ts 的无签名来源管控。
六、保留发布方命名空间与兼容性门
规则文档还要求两点:保留发布方命名空间对社区清单不可用;不兼容的条目保持可见但不可安装。
前者在清单解析层强制执行。src/core/plugin/manifest/parse.ts 的 Step 3:
// Community plugins must not claim a reserved publisher
// (motrix.*, verified.*, official.*, system.*).
// Built-ins shipped inside the app bundle are exempt ...
const origin = opts.origin ?? 'community'
if (origin !== 'builtin' && isReservedPublisher(result.data.id)) {
throw new PluginManifestInvalid('plugin.manifest.id_reserved_publisher', ...)
}
即 motrix.*、verified.*、official.*、system.* 四个发布方前缀只允许 origin: 'builtin'(随应用内置、位于 <resourcesDir>/builtin-plugins/)的插件使用。src/core/plugin/plugin-registry.test.ts 中有一对镜像用例:从 builtinDir 装载 motrix.* 通过,从社区 pluginsDir 装载同名则被拒;parse.test.ts 也专门有 reserved publisher (origin gate) 的测试组。注意该约束是在 parseManifest 而非 schema 层做的(schema.test.ts 明确“schema 层接受保留发布方,由 origin 判定”),这让同一份 schema 可以服务内置与社区两条装载路径。
后者落在查询与渲染层。RegistryClient.list() 返回的每条 RegistryPluginDTO 都带 compatible 布尔(见 engines.motrix 与宿主版本的 semver 匹配);渲染端 registry-detail-panel.tsx 对 compatible: false 的条目渲染“可见但禁用”的 Install 按钮并给出提示,测试 registry-detail-panel.test.tsx 固定了“compatible 条目在 Electron 上启用 Install、incompatible 条目禁用”的行为。
七、motrix://plugins/<id> 深链:仅导航,永不触发安装
规则文档明确:motrix://plugins/<id> 是 navigation-only,不得编码或触发安装;每一次安装都必须走应用内同意流程。
Electron 侧的处理在 src/main/platform/protocol-manager.ts。依赖接口注释直接引用了这条规则:
// motrix://plugins/<id> — navigate the main window to a plugin's
// marketplace detail route. Navigation-only by contract: the deeplink must
// never carry or trigger an install (.claude/rules/plugin-registry.md).
onOpenPluginDetail: (pluginId: string) => void
协议管理器把深链解析成 onOpenPluginDetail(pluginId) 回调,主窗口随后导航到市场详情页——用户仍需在详情页点击 Install 并完整走 stage → 同意对话框 → commit。src/main/index.ts 中也有对应注释 // motrix://plugins/<id> — navigation-only by contract;冷启动深链(应用未运行时点开链接)由 src/preload/preload.ts 在启动后补发 NavigateTo。测试 protocol-manager.test.ts 覆盖了合法路由以及 no-namespace、Upper.Case、../etc 等非法/可疑 ID 的拒绝路径——与 REGISTRY_PLUGIN_ID_RE 的严格小写命名空间约束互为呼应。
八、查询契约:Electron 与 server 共享同一语义
规则文档的 Query Contract 一节要求:Electron 与 server 两侧处理器都暴露 Queries.ListRegistryPlugins / Queries.GetRegistryPlugin,委托给同一个 RegistryClient 语义,并返回兼容性信息;共享查询载荷必须保持 host-independent,禁止为渲染层或某个 shell 造“镜像”或裸 channel 名。
仓库现状与之精确对应:
-
channel 常量只在共享层定义一次,见 src/shared/protocol/queries.ts:
ListRegistryPlugins: 'query:listRegistryPlugins', GetRegistryPlugin: 'query:getRegistryPlugin', -
Electron 处理器 src/main/ipc/queries.ts:
[Queries.ListRegistryPlugins]: async () => registryClient.list(hostVersion), [Queries.GetRegistryPlugin]: async (id: string) => registryClient.get(id, hostVersion), -
server 处理器 src/server/ipc/queries.ts 使用完全相同的写法;渲染端统一通过
transport.invoke(Queries.ListRegistryPlugins)调用(见 use-registry.ts),返回的 DTO 自带compatible字段,即“return compatibility information”的落地形态。
由于两侧共享同一 RegistryClient 实例语义(TTL、etag、4 MiB 上限、last-good 回退全部一致),市场页在 Electron 桌面与 server 部署形态下看到的条目、缓存与兼容性判定不会出现分叉。
九、给插件作者与消费者的要点小结
- 消费端视角:Motrix 对 registry 的读取是“永不抛错”的——超时(15s)、超限(4 MiB)、解析失败、坏缓存都会静默回退到 last-good;6 小时 TTL 内的请求直接命中内存。理解 registry-client.test.ts 中的用例可以完整看到每种退化路径的预期行为。
- 发布端视角:条目字段只能加不能改;本地化必须走
listing.localizations(BCP 47 安全 profile key,defaultLocale必须自有且含 name/description);package.sha256与size是安装边界的硬校验,包内清单的 id/version/engines.motrix 必须与条目逐字一致,权限只减不增。 - 安全边界:下载有 5 跳重定向上限与有界流式落盘;
motrix.*等保留命名空间被parseManifest按 origin 门禁;不兼容条目可见不可装;motrix://深链只能导航。每一层都有对应的测试文件(registry-expectation.test.ts、url-fetcher.test.ts、registry.test.ts)回归锁定。
整套机制的核心思想只有一句话:用户在目录页“看见并同意”的信任面(registry 条目),必须与“实际落盘”的插件包(解析后的清单)严格一致;registry 负责预览与摘要钉住,清单负责最终授权,深链与任何自动化路径都不得绕过这道人工同意的闸门。
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