DSH / Atlas
2026-10-02implementedfeature

Plugin market Discover page and index reader

插件市场发现页与索引读取器

The [plugin market index format](2026-10-02-plugin-market-index-format.md) lets a market publish installable bundles in one document, but DSH needs a reader that fetches indexes safely, a place where users choose index sources and browse entries, and a path from an entry to the existing installation flow. No official market exists, so DSH cannot ship a default source; the sources are user data. The plugin manager own

English

Problem

The plugin market index format lets a market publish installable bundles in one document, but DSH needs a reader that fetches indexes safely, a place where users choose index sources and browse entries, and a path from an entry to the existing installation flow. No official market exists, so DSH cannot ship a default source; the sources are user data.

The plugin manager owns profile package operations and must not remount while an installation runs. Market configuration that a user edits from the Web therefore cannot be an ordinary configuration field of plugin-manager.

Decision

The feature ships as the experimental optional custom plugin markets bundle, off by default, which mounts both rows below; with it off, neither the service, the Discover tab, nor the plugin_market tool exists, and the Plugins page shows no tabs. A separate Host service, pluginMarket, reads indexes. It requires pluginManager for read-only calls and never writes the profile. Its indexes configuration field is volatile: Settings writes only volatile fields, and the Loader commits a volatile-only change into the running reference without remounting any plugin. Other fields (fetchTimeoutMs, maxBytes, cacheTtlMs) are ordinary configuration.

Index URLs use http: or https:, without credentials or fragment. file: is excluded because relative icon references would resolve to URLs a browser cannot load. search and entry accept only a configured URL, so the Host never fetches an address a client supplies. The Host fetches through the process proxy, refuses redirects, bounds the body, and validates with the shared format package; an invalid index affects only its own URL.

Indexes can reach the default 128 MiB maxBytes, so clients never receive or filter a whole index. search filters, matches text, orders, and pages on the Host and returns at most 100 entries; entry returns one plugin by name for the tool. Text matching is fuzzy and covers the package name, author, keywords, and every language of the title, description, and category and label titles, because a person may search in any language a market writes. A Chinese, Japanese, or Korean run is indexed as overlapping character pairs and its last character, so queries match without a word dictionary. MiniSearch builds the text index on the first text query of each document and yields between chunks. Each page carries its document's fetchedAt, and a later page sent with it is answered from that document while it is cached, so paging stays consistent across cache expiry; a client whose document was replaced starts again from the first page.

Each returned entry carries the profile's installation status, its compatibility computed by evaluatePluginCompatibility with the profile's version exemptions, and install, the exact arguments from marketInstallTarget. That is the only place install arguments are built; the Web page and the model tool consume it.

The Web Discover tab is its own client plugin. ui-plugin-manager declares a plugins.tab slot and shows Installed and each contributed tab under the page header, because browsing a market and managing installed bundles are separate tasks of equal rank. The owner props provide install(spec, registry?) for a spec the person confirmed in the tab, which opens the existing install dialog at its check screen without the spec form, and openPackage(name), which opens an installed bundle's page. A registry an entry names applies to that install only and is never remembered as the dialog's choice, so one market cannot redirect later manual installs. Discover confirms each install with the entry's origin and a trust notice, and keeps source management, which adds a code origin, in a dialog with its own trust notice. Each entry has a page with its full description, source facts, and every listed version; Install on a card chooses the default version, and the page can install any other version installation accepts. Indexes are browsed one at a time; categories, labels, and sort keys stay opaque and display in localized title order; entries missing the selected sort value come last in both directions.

A read-only plugin_market tool lists indexes and pages entries, returning the target and registry that plugin_manager's install_bundle accepts. It requires no approval; installation keeps plugin_manager's approval. The tool cannot change index sources, because adding a source introduces a new code origin.

Alternatives considered

Market fields on plugin-manager. A non-volatile field would remount plugin-manager when the Web edits it and interrupt package operations it owns. Even with a volatile field, a separate service guarantees that market configuration never reaches plugin-manager, at the cost of one more package and service.

Configuring indexes only in cordis.yml. Most users do not edit cordis.yml, so Discover would stay empty for them.

Fetching indexes in the browser. CORS, the Host's proxy configuration, and validation where untrusted data enters favor Host fetches.

Filtering in the browser. It needs the whole annotated index in every client, which grows with the index and recomputes compatibility for every entry on each read.

A query protocol on the index host. It would keep large indexes off the Host but requires a server for every market, which a static index file avoids. It remains the next step if indexes outgrow the Host.

Merging all configured indexes into one list. Opaque ids from different markets may collide with different meanings, and duplicate package names need a conflict rule. Per-index browsing avoids both.

Rendering Discover inside ui-plugin-manager. The page would depend on the market Remote and grow further; a slot keeps the page usable without the market and lets the market plugin own its copy and state.

Discover as a section under the Installed groups, installing through the spec form. Finding it requires scrolling past every installed bundle, and the form asks a person to review a spec they never typed. Tabs give browsing its own entry point, and a confirmation with the entry's origin followed by the check screen replaces the form.

An approval for plugin_market. Listing changes nothing and reads only sources the user configured; requiring approval would add a prompt without protecting an operation. The installation step keeps its approval.

An upgrade action. Upgrading changes the documented "uninstall, then install" behavior and is deferred; Discover shows update-available as a status.

Consequences

  • Users browse and install third-party bundles without knowing a spec, and the existing install dialog, registry fallback, build-script approval, and compatibility refusal apply unchanged.
  • The Settings form also shows indexes, because Settings lists every volatile field.
  • An http: index, including the install specs it lists, can be altered in transit; a market reached over an untrusted network needs https:.
  • URL icons are requested by the browser and disclose the user's IP address to image hosts; Discover loads them without a referrer.
  • Market text reaches users and, through plugin_market, models; the tool description marks it as third-party content.
  • A real-composition test proves that setIndexes persists through the profile without remounting the service, and unit tests cover fetch limits, cache sharing, removal, and search matching, ordering, and paging.

中文

问题

插件市场索引格式让市场能在一份文档中发布可安装的组合包(bundle),但 DSH 还需要:一个安全获取索引的读取器,一个让用户选择索引源并浏览条目的位置,以及一条从条目进入现有安装流程的路径。目前没有官方市场,因此 DSH 无法随附默认源;索引源属于用户数据。

插件管理器拥有 profile 的包操作,安装运行期间不得重新挂载。因此,用户从 Web 编辑的市场配置不能是 plugin-manager 的普通配置字段。

决策

该功能以实验性的可选自定义插件市场组合包发布,默认关闭,并挂载下面两行;关闭时,服务、「发现」标签页和 plugin_market 工具都不存在,插件页也不显示标签页。由独立的 Host 服务 pluginMarket 读取索引。它依赖 pluginManager 进行只读调用,从不写入 profile。它的 indexes 配置字段是 volatile 字段:Settings 只写入 volatile 字段,Loader 把只涉及 volatile 字段的变更提交到运行中的引用,不重新挂载任何插件。其他字段(fetchTimeoutMs、maxBytes、cacheTtlMs)是普通配置。

索引 URL 使用 http: 或 https:,不含凭据或片段。排除 file:,因为相对图标引用会解析为浏览器无法加载的 URL。search 和 entry 只接受已配置的 URL,因此 Host 从不获取客户端提供的地址。Host 通过进程代理获取,拒绝重定向,限制响应体大小,并用共享的格式包校验;无效索引只影响它自己的 URL。

索引可能达到默认 128 MiB 的 maxBytes,因此客户端从不接收或过滤整个索引。search 在 Host 上完成过滤、文本匹配、排序和分页,最多返回 100 个条目;entry 按名称为工具返回一个插件。文本匹配是模糊的,覆盖包名、作者、关键词,以及标题、描述、分类标题和标签标题的所有语言,因为用户可能使用市场所写的任意语言搜索。中文、日文或韩文片段被索引为相邻的两字组合及其最后一个字符,因此查询无需分词词典即可匹配。MiniSearch 在每份文档第一次带文本的查询时构建文本索引,并在分块之间让出事件循环。每页都携带其文档的 fetchedAt,之后带着它发送的页请求只要该文档仍在缓存中就用它回答,因此分页在缓存过期后仍保持一致;文档被替换的客户端从第一页重新开始。

每个返回的条目携带 profile 中的安装状态、由 evaluatePluginCompatibility 结合 profile 版本豁免计算出的兼容性,以及 install,即 marketInstallTarget 给出的精确参数。这是唯一构建安装参数的位置;Web 页面和模型工具都使用它。

Web 的发现标签页是独立的 Client 插件。ui-plugin-manager 声明 plugins.tab slot,并在页头下方显示已安装和每个贡献的标签页,因为浏览市场与管理已安装组合包是两项同等重要的独立任务。owner props 提供 install(spec, registry?) 和 openPackage(name):前者用于用户已在标签页中确认的 spec,在检查界面打开现有的安装对话框,不显示 spec 表单;后者打开已安装组合包的页面。条目指定的注册表只用于这一次安装,从不作为对话框的选择被记住,因此一个市场无法改变之后手动安装所用的注册表。发现标签页在每次安装前显示条目来源和信任提示以供确认,并把会引入代码来源的索引源管理放在带有自身信任提示的对话框中。每个条目都有一个页面,显示其完整描述、来源信息和列出的每个版本;卡片上的安装选择默认版本,页面可以安装安装检查接受的任何其他版本。索引一次浏览一个;分类、标签和排序键保持不透明,按本地化标题顺序显示;缺少所选排序值的条目在两个方向上都排在最后。

只读的 plugin_market 工具列出索引并分页列出条目,返回 plugin_manager 的 install_bundle 接受的 target 和 registry。它不需要批准;安装仍保留 plugin_manager 的批准。该工具不能修改索引源,因为添加索引源会引入新的代码来源。

考虑过的替代方案

把市场字段放在 plugin-manager 上。 非 volatile 字段在 Web 编辑它时会重新挂载 plugin-manager,中断它拥有的包操作。即使使用 volatile 字段,独立服务也能保证市场配置永远不会到达 plugin-manager,代价是多一个包和服务。

只在 cordis.yml 中配置索引。 大多数用户不编辑 cordis.yml,发现区对他们会一直为空。

在浏览器中获取索引。 CORS、Host 的代理配置,以及在不可信数据进入处进行校验,都支持由 Host 获取。

在浏览器中过滤。 这需要每个客户端都拿到整个带注解的索引,其大小随索引增长,并且每次读取都要为每个条目重新计算兼容性。

在索引主机上提供查询协议。 它能让大索引不进入 Host,但每个市场都需要一台服务器,而静态索引文件不需要。如果索引超出 Host 的承受能力,这是下一步。

把所有已配置索引合并成一个列表。 不同市场的不透明 id 可能相同但含义不同,重复的包名也需要冲突规则。按索引分别浏览可以避免这两个问题。

在 ui-plugin-manager 内渲染发现区。 该页面会依赖市场 Remote,代码量也会继续增加;使用 slot 时,没有市场该页面仍可使用,市场插件也能拥有自己的文案和状态。

把发现作为「已安装」分组下方的区域,并通过 spec 表单安装。 找到它需要滚动经过每个已安装组合包,而且该表单要求用户审核一个自己从未输入的 spec。标签页为浏览提供独立入口,先显示条目来源以供确认、再进入检查界面的流程取代了该表单。

为 plugin_market 要求批准。 列出操作不做任何改动,只读取用户配置的源;要求批准只会增加一次提示,而不保护任何操作。安装步骤保留其批准。

升级操作。 升级会改变已记录的「先卸载再安装」行为,因此延后;发现区把 update-available 显示为状态。

后果

  • 用户无需知道 spec 即可浏览并安装第三方组合包,现有的安装对话框、registry 回退、构建脚本批准和兼容性拒绝照常适用。
  • 设置表单也显示 indexes,因为设置会列出每个 volatile 字段。
  • http: 索引(包括其中列出的安装 spec)可能在传输途中被篡改;经由不可信网络访问的市场需要使用 https:。
  • URL 图标由浏览器请求,会向图片主机暴露用户的 IP 地址;发现区加载图标时不发送 referrer。
  • 市场文本会到达用户,并通过 plugin_market 到达模型;工具描述将其标记为第三方内容。
  • 一个真实组合测试证明 setIndexes 会经由 profile 持久化而不重新挂载服务,单元测试覆盖获取限制、缓存共享、移除,以及搜索的匹配、排序和分页。