feat(plugin-market): carry a plugin's source repository
Indexes may give each plugin an optional repository: the web URL of its source repository and the package directory inside it. The Discover entry page links it as Source code, and the plugin_market tool returns it.
This commit is contained in:
1 parent
3de3a977e0
commit
ab3bb74550
24 files changed
+89
-26
No files matched your search
@@ -18,8 +18,8 @@
|
||||
en: 4fac5a9b8261342d
|
||||
zh: f9168a8a4209feab
|
||||
/agent-note-plugin-market-index-format/decision/plugin-fields:
|
||||
en: a2534776b2457276
|
||||
zh: 2ca8b814a0c45938
|
||||
en: b79abd05a957948b
|
||||
zh: b6f0c203ea8e4838
|
||||
/agent-note-plugin-market-index-format/decision/version-fields:
|
||||
en: d6c0fa99e1cbb068
|
||||
zh: 3d2a7b85299c679a
|
||||
@@ -30,8 +30,8 @@
|
||||
en: 00fe507b9b5dfea7
|
||||
zh: 55de6aaeb3865e84
|
||||
/agent-note-plugin-market-index-format/decision/generator-obligations:
|
||||
en: 8e848a102fe9d86e
|
||||
zh: feff226ef8321e00
|
||||
en: 7c4f75227111309f
|
||||
zh: 1da30de171151372
|
||||
/agent-note-plugin-market-index-format/decision/reader-rules:
|
||||
en: 92903cbe87d9c0ca
|
||||
zh: 54af6cb33e9da252
|
||||
|
||||
@@ -45,6 +45,7 @@ The market reuses npm's package names, versions, ranges, distribution tags, and
|
||||
"icon": "icons/auto-review.svg",
|
||||
"author": "Example Labs",
|
||||
"homepage": "https://example.com/dsh-auto-review",
|
||||
"repository": { "url": "https://github.com/example/dsh-plugins", "directory": "auto-review" },
|
||||
"metadata": {
|
||||
"category": "productivity",
|
||||
"labels": ["official"],
|
||||
@@ -117,6 +118,7 @@ Category, label, and sort-key ids are non-empty strings. Readers look ids up as
|
||||
| `icon` | no | Either `data:<type>;base64,<payload>`, where `<type>` is `image/svg+xml`, `image/png`, `image/jpeg`, or `image/webp`, `<payload>` is canonically padded base64, and the decoded payload is at most 32 KiB; or a URL reference whose resolution against the index URL is an `https:` URL or has the index URL's origin. |
|
||||
| `author` | no | String with at least one non-whitespace character. |
|
||||
| `homepage` | no | Absolute `https:` or `http:` URL of a page about the plugin. |
|
||||
| `repository` | no | `{ "url": string, "directory"?: string }`. `url` is the absolute `https:` or `http:` URL of the source repository's web page, without credentials, query, or fragment; `directory` is the package directory inside it, with the `path` rule of a Git source. It is for display and never selects an install source. |
|
||||
| `metadata` | no | Object holding the values below. |
|
||||
| `metadata.category` | no | One declared category id. |
|
||||
| `metadata.labels` | no | Array of distinct declared label ids. |
|
||||
@@ -166,7 +168,7 @@ A reader cannot verify these rules; a generator that violates them publishes a m
|
||||
- `engines.dsh` copies the manifest's declared range unchanged. A version with a missing, blank, or invalid range is omitted with a diagnostic. A plugin with no eligible version is omitted.
|
||||
- `dist-tags` snapshots npm's [distribution tags](https://docs.npmjs.com/cli/v11/commands/npm-dist-tag/), retaining only targets present among the eligible listed versions. For Git sources, the publisher or market supplies the same exact tag-to-version mapping.
|
||||
- Each listed version declares `dsh.bundle`. Installation independently refuses a package without a bundle patch.
|
||||
- `title`, `description`, and `icon` come from the highest listed version's locale metadata, manifest fields, and icon under the installed-plugin display rules. Use npm's `node-semver.rsort()` order, retaining input order for ties. This metadata choice does not select the installation version. `homepage` copies that version's manifest `homepage` and is absent without one.
|
||||
- `title`, `description`, and `icon` come from the highest listed version's locale metadata, manifest fields, and icon under the installed-plugin display rules. Use npm's `node-semver.rsort()` order, retaining input order for ties. This metadata choice does not select the installation version. `homepage` copies that version's manifest `homepage` and is absent without one. `repository` comes from that version's manifest `repository`: a shorthand or Git URL becomes the repository's web URL, `directory` is copied, and the field is absent when the manifest has none or names no web-addressable repository.
|
||||
- `author` comes from the same highest listed version's npm author declaration. Copy an object's `name`; parse shorthand strings with a maintained npm person normalizer. Omit an absent or whitespace-only name and contact fields. Normalize a fresh projection, leaving the source manifest untouched.
|
||||
|
||||
### Reader rules
|
||||
|
||||
@@ -45,6 +45,7 @@ DSH 只能从用户已知的 spec 安装第三方组合包(bundle):npm 包
|
||||
"icon": "icons/auto-review.svg",
|
||||
"author": "Example Labs",
|
||||
"homepage": "https://example.com/dsh-auto-review",
|
||||
"repository": { "url": "https://github.com/example/dsh-plugins", "directory": "auto-review" },
|
||||
"metadata": {
|
||||
"category": "productivity",
|
||||
"labels": ["official"],
|
||||
@@ -117,6 +118,7 @@ DSH 只能从用户已知的 spec 安装第三方组合包(bundle):npm 包
|
||||
| `icon` | 否 | 以下两种形式之一:`data:<type>;base64,<payload>`,其中 `<type>` 为 `image/svg+xml`、`image/png`、`image/jpeg` 或 `image/webp`,`<payload>` 是按规范填充的 base64,解码后的载荷不超过 32 KiB;或者 URL 引用,它相对于索引 URL 解析的结果是 `https:` URL,或与索引 URL 同源。 |
|
||||
| `author` | 否 | 至少包含一个非空白字符的字符串。 |
|
||||
| `homepage` | 否 | 介绍该插件的页面的绝对 `https:` 或 `http:` URL。 |
|
||||
| `repository` | 否 | `{ "url": string, "directory"?: string }`。`url` 是源代码仓库网页的绝对 `https:` 或 `http:` URL,不含凭据、查询或片段;`directory` 是包在仓库中的目录,规则与 Git 来源的 `path` 相同。该字段仅用于展示,从不决定安装来源。 |
|
||||
| `metadata` | 否 | 包含下列值的对象。 |
|
||||
| `metadata.category` | 否 | 一个已声明的分类 id。 |
|
||||
| `metadata.labels` | 否 | 由互不相同的已声明标签 id 组成的数组。 |
|
||||
@@ -166,7 +168,7 @@ DSH 只能从用户已知的 spec 安装第三方组合包(bundle):npm 包
|
||||
- `engines.dsh` 原样复制 manifest 声明的范围。缺失、空白或无效范围的版本会被省略并给出诊断。没有合格版本的插件会被省略。
|
||||
- `dist-tags` 是 npm [发布标签](https://docs.npmjs.com/cli/v11/commands/npm-dist-tag/)的快照,只保留指向合格且已列出版本的目标。对于 Git 来源,发布者或市场提供同样的精确标签到版本映射。
|
||||
- 列出的每个版本都声明了 `dsh.bundle`。安装时也会独立拒绝没有组合包 patch 的包。
|
||||
- `title`、`description` 和 `icon` 来自所列最高版本的语言元数据、manifest 字段和图标,采用已安装插件的显示规则。使用 npm 的 `node-semver.rsort()` 顺序,相等时保留输入顺序。元数据选择不选择安装版本。`homepage` 复制该版本 manifest 的 `homepage`,manifest 没有该字段时省略。
|
||||
- `title`、`description` 和 `icon` 来自所列最高版本的语言元数据、manifest 字段和图标,采用已安装插件的显示规则。使用 npm 的 `node-semver.rsort()` 顺序,相等时保留输入顺序。元数据选择不选择安装版本。`homepage` 复制该版本 manifest 的 `homepage`,manifest 没有该字段时省略。`repository` 来自该版本 manifest 的 `repository`:简写或 Git URL 转换为仓库的网页 URL,`directory` 原样复制;manifest 没有该字段,或其指向的仓库没有网页地址时省略。
|
||||
- `author` 来自同一最高版本的 npm 作者声明。复制对象的 `name`;使用维护中的 npm person 归一化器解析简写字符串。省略缺失或全空白的名称以及联系字段。归一化新建的投影,不修改源 manifest。
|
||||
|
||||
### 读取器规则
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
en: b37011e8397c9fad
|
||||
zh: 57b5a3b9b17e357e
|
||||
/plugin-markets/index-format-reference:
|
||||
en: cd6e99c90b72f0d7
|
||||
zh: d5936fb90411f4f4
|
||||
en: d058d12c27433f64
|
||||
zh: f43b85e4934817f3
|
||||
/plugin-markets/further-exploration:
|
||||
en: 3c0a90e8bf73c89b
|
||||
zh: 95608eb4f6278cb4
|
||||
|
||||
@@ -53,6 +53,7 @@ This is a minimal index with one npm plugin and one plugin from a Git monorepo:
|
||||
"icon": "icons/foo.svg",
|
||||
"author": "Jane Doe",
|
||||
"homepage": "https://github.com/jane/dsh-plugin-foo#readme",
|
||||
"repository": { "url": "https://github.com/jane/dsh-plugin-foo" },
|
||||
"versions": [
|
||||
{ "version": "1.1.0", "publishTimestamp": 1790476800, "source": { "type": "npm" }, "engines": { "dsh": ">=0.2.0-rc.2" } },
|
||||
{ "version": "1.0.0", "publishTimestamp": 1788000000, "source": { "type": "npm" } }
|
||||
@@ -99,6 +100,7 @@ Text fields marked `LocalizedText` are a string or an object of strings keyed by
|
||||
| `icon` | no | Inline `data:` URL (SVG, PNG, JPEG, or WebP, at most 32 KiB) or URL, as described above. |
|
||||
| `author` | no | Display text. |
|
||||
| `homepage` | no | Absolute `https:` or `http:` URL of a page about the plugin; the plugin page links it. |
|
||||
| `repository` | no | `{ "url": string, "directory"?: string }`: the source repository's web page, and the package directory inside it. The plugin page links it as **Source code**. |
|
||||
| `metadata.category` | no | One declared category id. |
|
||||
| `metadata.labels` | no | Declared label ids. |
|
||||
| `metadata.sortKeys` | no | Object mapping declared sort-key ids to numbers, such as downloads or ratings. |
|
||||
|
||||
@@ -53,6 +53,7 @@ DSH 不内置任何索引源。选择**发现**,点击**添加索引源**,
|
||||
"icon": "icons/foo.svg",
|
||||
"author": "Jane Doe",
|
||||
"homepage": "https://github.com/jane/dsh-plugin-foo#readme",
|
||||
"repository": { "url": "https://github.com/jane/dsh-plugin-foo" },
|
||||
"versions": [
|
||||
{ "version": "1.1.0", "publishTimestamp": 1790476800, "source": { "type": "npm" }, "engines": { "dsh": ">=0.2.0-rc.2" } },
|
||||
{ "version": "1.0.0", "publishTimestamp": 1788000000, "source": { "type": "npm" } }
|
||||
@@ -99,6 +100,7 @@ DSH 不内置任何索引源。选择**发现**,点击**添加索引源**,
|
||||
| `icon` | 否 | 内联 `data:` URL(SVG、PNG、JPEG 或 WebP,不超过 32 KiB)或 URL,见上文。 |
|
||||
| `author` | 否 | 显示文本。 |
|
||||
| `homepage` | 否 | 介绍该插件的页面的绝对 `https:` 或 `http:` URL;插件页面会链接到它。 |
|
||||
| `repository` | 否 | `{ "url": string, "directory"?: string }`:源代码仓库的网页,以及包在其中的目录。插件页面以**源代码**链接到它。 |
|
||||
| `metadata.category` | 否 | 一个已声明的分类 id。 |
|
||||
| `metadata.labels` | 否 | 已声明的标签 id。 |
|
||||
| `metadata.sortKeys` | 否 | 将已声明的排序键 id 映射为数值的对象,例如下载量或评分。 |
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
en: b8183cb06f3e4ba9
|
||||
zh: 9a213e7a124f092d
|
||||
/deepseek-ai-dsh-plugin-market/model-experience/market-listing-tool/what-the-model-sees:
|
||||
en: 4785523157eec303
|
||||
zh: ac5a304b88c3521f
|
||||
en: bcbe6e7d5c763159
|
||||
zh: 28cc392bc8e574e4
|
||||
/deepseek-ai-dsh-plugin-market/model-experience/market-listing-tool/token-effect:
|
||||
en: 2589c52e6deb5e48
|
||||
zh: 9a71ca215165a8a5
|
||||
|
||||
@@ -85,7 +85,7 @@ Compatibility is computed per version with `evaluatePluginCompatibility`, the ve
|
||||
|
||||
#### What the model sees
|
||||
|
||||
The [`plugin_market` tool](../../../docs/tool-catalog.md#deepseek-aidsh-plugin-market) returns JSON: configured index URLs with English titles and last errors, one page of plugins, in index order or by best match for a `query`, with name, English title, description, and author each truncated to 300 characters, homepage, category, label ids, sort-key values, installation status, the default version, and the number of versions, or one page of a plugin's versions. Each returned version carries its ISO publish time, compatibility, and install arguments. Its description states that market text is third-party content, not instructions.
|
||||
The [`plugin_market` tool](../../../docs/tool-catalog.md#deepseek-aidsh-plugin-market) returns JSON: configured index URLs with English titles and last errors, one page of plugins, in index order or by best match for a `query`, with name, English title, description, and author each truncated to 300 characters, homepage, repository, category, label ids, sort-key values, installation status, the default version, and the number of versions, or one page of a plugin's versions. Each returned version carries its ISO publish time, compatibility, and install arguments. Its description states that market text is third-party content, not instructions.
|
||||
|
||||
#### Token effect
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ Host 使用进程的 `fetch` 获取,因此配置的出站代理生效,并拒
|
||||
|
||||
#### 模型看到什么
|
||||
|
||||
[`plugin_market` 工具](../../../docs/tool-catalog.zh.md#deepseek-aidsh-plugin-market)返回 JSON:已配置的索引 URL 及其英文标题和最近错误,一页插件(按索引顺序,或给出 `query` 时按最佳匹配),包含名称、英文标题、描述和作者(各截断为 300 个字符)、主页、分类、标签 id、排序键值、安装状态、默认版本和版本数量,或者某个插件的一页版本。返回的每个版本都携带 ISO 发布时间、兼容性和安装参数。其描述声明市场文本是第三方内容,不是指令。
|
||||
[`plugin_market` 工具](../../../docs/tool-catalog.zh.md#deepseek-aidsh-plugin-market)返回 JSON:已配置的索引 URL 及其英文标题和最近错误,一页插件(按索引顺序,或给出 `query` 时按最佳匹配),包含名称、英文标题、描述和作者(各截断为 300 个字符)、主页、仓库、分类、标签 id、排序键值、安装状态、默认版本和版本数量,或者某个插件的一页版本。返回的每个版本都携带 ISO 发布时间、兼容性和安装参数。其描述声明市场文本是第三方内容,不是指令。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
|
||||
@@ -109,6 +109,7 @@ function registerTool(ctx: Context): void {
|
||||
...entry.description === undefined ? {} : { description: english(entry.description) },
|
||||
...entry.author === undefined ? {} : { author: english(entry.author) },
|
||||
...entry.homepage === undefined ? {} : { homepage: entry.homepage },
|
||||
...entry.repository === undefined ? {} : { repository: entry.repository },
|
||||
...entry.metadata.category === undefined ? {} : { category: entry.metadata.category },
|
||||
labels: entry.metadata.labels,
|
||||
sortKeys: entry.metadata.sortKeys,
|
||||
|
||||
@@ -43,7 +43,7 @@ const document = {
|
||||
metadata: { categories: { tools: { title: 'Tools' } }, labels: { official: { title: 'Official' } }, sortKeys: { downloads: { title: 'Downloads', order: 'desc' } } },
|
||||
plugins: [
|
||||
{
|
||||
name: 'dsh-a', author: 'Jane', homepage: 'https://example.com/a',
|
||||
name: 'dsh-a', author: 'Jane', homepage: 'https://example.com/a', repository: { url: 'https://github.com/o/a' },
|
||||
title: { en: 'A', zh: '甲' }, description: { en: 'Does A', zh: '做甲' },
|
||||
metadata: { category: 'tools', labels: ['official'], sortKeys: { downloads: 5 } },
|
||||
versions: [
|
||||
@@ -78,7 +78,8 @@ it('pages plugins with the default version\'s install target and registry plugin
|
||||
index: 'Market',
|
||||
entries: [
|
||||
{
|
||||
name: 'dsh-a', title: 'A', description: 'Does A', author: 'Jane', homepage: 'https://example.com/a', category: 'tools',
|
||||
name: 'dsh-a', title: 'A', description: 'Does A', author: 'Jane', homepage: 'https://example.com/a',
|
||||
repository: { url: 'https://github.com/o/a' }, category: 'tools',
|
||||
labels: ['official'], sortKeys: { downloads: 5 }, installation: { status: 'not-installed' },
|
||||
defaultVersion: {
|
||||
version: '1.0.0', published: '2026-09-27T02:40:00.000Z', compatibility: { compatible: true },
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
en: 76368b6b1da79c80
|
||||
zh: 52384aee991ac542
|
||||
/deepseek-ai-dsh-client-ui-plugin-market/use-this-package:
|
||||
en: 8d509562b260419c
|
||||
zh: 2a7149c1ec7e8ea3
|
||||
en: 2690b95e156cc473
|
||||
zh: 0d9e897ad628f9dd
|
||||
/deepseek-ai-dsh-client-ui-plugin-market/understand-the-implementation:
|
||||
en: cc9e8550a01ea847
|
||||
zh: eda18f5680df3e88
|
||||
|
||||
@@ -28,7 +28,7 @@ Open **Plugins** and select **Discover**. With no index source, the tab shows an
|
||||
|
||||
The toolbar holds the market menu, which switches markets and ends with **Manage index sources…**; the search field; a **Labels** menu with multi-select checkmarks when the market declares labels; the **Sort** menu; and a refresh button. Category pills, with **All** first, follow when the market declares categories.
|
||||
|
||||
Each card shows the icon, localized title, label tags, a one-line description, and the default version, author, and publish date, with one action: **Install** for an installable entry; a disabled **Install** whose tooltip says no version is compatible with the running DSH when the plugin has no default version and its highest version is neither compatible nor exempted; or **Installed**, **Update available**, or **Provided by DSH**, which switches to the Installed tab and opens that bundle's page when that tab lists it. Selecting a card opens the entry's page with its full description, compatibility notice, and the default version's facts: author; homepage, as a link that opens in a new tab; the npm package and registry, or the Git repository, full commit, and directory; publish date; category; labels; and market. A **Versions** list follows in descending SemVer order; each row tags the recommended, prerelease, installed, incompatible, and exempted versions and offers **Install** for any other version installation accepts, except for a bundle DSH provides. The page's back link returns to the list with its search, filters, and sort intact.
|
||||
Each card shows the icon, localized title, label tags, a one-line description, and the default version, author, and publish date, with one action: **Install** for an installable entry; a disabled **Install** whose tooltip says no version is compatible with the running DSH when the plugin has no default version and its highest version is neither compatible nor exempted; or **Installed**, **Update available**, or **Provided by DSH**, which switches to the Installed tab and opens that bundle's page when that tab lists it. Selecting a card opens the entry's page with its full description, compatibility notice, and the default version's facts: author; homepage and source code repository, as links that open in a new tab, with the package directory beside the repository; the npm package and registry, or the Git repository, full commit, and directory; publish date; category; labels; and market. A **Versions** list follows in descending SemVer order; each row tags the recommended, prerelease, installed, incompatible, and exempted versions and offers **Install** for any other version installation accepts, except for a bundle DSH provides. The page's back link returns to the list with its search, filters, and sort intact.
|
||||
|
||||
**Install** chooses the default version and opens a confirmation dialog with the entry's icon, title, that version, the author, its source facts, an exemption notice when one applies, and a trust notice. Confirming opens the Plugins install dialog at its check screen with the entry's exact spec and registry; there is no spec form, and a refusal offers **Retry**. The registry an entry names applies to that install only.
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ kind: "package-reference"
|
||||
|
||||
工具栏包含:插件市场菜单,用于切换插件市场,最后一项为**管理索引源…**;搜索框;插件市场声明了标签时显示的**标签**菜单,以勾选标记支持多选;**排序**菜单;以及刷新按钮。插件市场声明了分类时,其后显示分类按钮,**全部**排在第一位。
|
||||
|
||||
每张卡片显示图标、本地化标题、标签、一行描述,以及默认版本、作者和发布日期,并只有一个操作:可安装的条目显示**安装**;没有默认版本、且最高版本既不兼容也未豁免的条目显示禁用的**安装**,其提示框说明没有与当前运行的 DSH 兼容的版本;其余条目显示**已安装**、**有新版本**或**随 DSH 提供**,点击后切换到「已安装」标签页,并在该标签页列出该组合包时打开其页面。选择卡片会打开条目页面,显示其完整描述、兼容性提示和默认版本的信息:作者;主页(在新标签页中打开的链接);npm 包和注册表,或 Git 仓库、完整提交和目录;发布日期;分类;标签;以及插件市场。其后是按 SemVer 降序排列的**版本**列表;每行标出推荐、预发布、已安装、不兼容和已豁免的版本,并为安装检查接受的其他版本提供**安装**,DSH 随附的组合包除外。页面的返回链接回到列表,搜索、筛选和排序保持不变。
|
||||
每张卡片显示图标、本地化标题、标签、一行描述,以及默认版本、作者和发布日期,并只有一个操作:可安装的条目显示**安装**;没有默认版本、且最高版本既不兼容也未豁免的条目显示禁用的**安装**,其提示框说明没有与当前运行的 DSH 兼容的版本;其余条目显示**已安装**、**有新版本**或**随 DSH 提供**,点击后切换到「已安装」标签页,并在该标签页列出该组合包时打开其页面。选择卡片会打开条目页面,显示其完整描述、兼容性提示和默认版本的信息:作者;主页和源代码仓库(在新标签页中打开的链接,仓库旁显示包所在目录);npm 包和注册表,或 Git 仓库、完整提交和目录;发布日期;分类;标签;以及插件市场。其后是按 SemVer 降序排列的**版本**列表;每行标出推荐、预发布、已安装、不兼容和已豁免的版本,并为安装检查接受的其他版本提供**安装**,DSH 随附的组合包除外。页面的返回链接回到列表,搜索、筛选和排序保持不变。
|
||||
|
||||
**安装**选择默认版本并打开确认对话框,显示条目的图标、标题、该版本、作者、来源信息、适用时的豁免提示,以及信任提示。确认后,插件页的安装对话框以该条目的精确 spec 和注册表在检查界面打开;不显示 spec 表单,被拒绝时提供**重试**。条目指定的注册表只用于这一次安装。
|
||||
|
||||
|
||||
@@ -239,6 +239,15 @@ function EntryDetail({ entry, listing, t, face, language, onBack, onInstall, onO
|
||||
<dd><a className={css.link} href={entry.homepage} target="_blank" rel="noopener noreferrer">{entry.homepage}</a></dd>
|
||||
</div>
|
||||
)}
|
||||
{entry.repository === undefined ? null : (
|
||||
<div>
|
||||
<dt>{t('factSourceCode')}</dt>
|
||||
<dd>
|
||||
<a className={css.link} href={entry.repository.url} target="_blank" rel="noopener noreferrer">{entry.repository.url}</a>
|
||||
{entry.repository.directory === undefined ? null : <> <code>{entry.repository.directory}</code></>}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{sourceFacts(entry.name, version).map(fact => (
|
||||
<div key={fact.kind}><dt>{t(FACT_KEYS[fact.kind])}</dt><dd><code>{fact.value}</code></dd></div>
|
||||
))}
|
||||
|
||||
@@ -67,6 +67,7 @@ export const zh = {
|
||||
factPublished: '发布时间',
|
||||
factAuthor: '作者',
|
||||
factHomepage: '主页',
|
||||
factSourceCode: '源代码',
|
||||
factCategory: '分类',
|
||||
factLabels: '标签',
|
||||
factMarket: '插件市场',
|
||||
@@ -151,6 +152,7 @@ export const en = {
|
||||
factPublished: 'Published',
|
||||
factAuthor: 'Author',
|
||||
factHomepage: 'Homepage',
|
||||
factSourceCode: 'Source code',
|
||||
factCategory: 'Category',
|
||||
factLabels: 'Labels',
|
||||
factMarket: 'Plugin market',
|
||||
|
||||
@@ -59,14 +59,14 @@ const LISTING: MarketSearchResult = {
|
||||
entries: [
|
||||
entry('dsh-a', {
|
||||
title: { en: 'Alpha', zh: '甲' }, description: 'Does A', author: 'Jane', icon: 'https://cdn.example/a.png',
|
||||
homepage: 'https://example.com/alpha#readme',
|
||||
homepage: 'https://example.com/alpha#readme', repository: { url: 'https://github.com/o/plugins', directory: 'packages/a' },
|
||||
}, { category: 'tools', labels: ['official', 'beta'], sortKeys: { downloads: 1 } }, {
|
||||
source: { type: 'npm', registry: 'https://r.example/' }, install: { spec: '[email protected]', registry: 'https://r.example/' },
|
||||
}),
|
||||
entry('dsh-b', { installation: { status: 'installed', installedVersion: '1.0.0' } }, { category: 'themes', labels: ['official'], sortKeys: { downloads: 9 } }, {
|
||||
publishTimestamp: 1790000000, source: { type: 'git', url: 'https://github.com/o/r', commit: COMMIT, path: 'packages/b' },
|
||||
}),
|
||||
entry('dsh-c', { installation: { status: 'update-available', installedVersion: '0.9.0' } }),
|
||||
entry('dsh-c', { installation: { status: 'update-available', installedVersion: '0.9.0' }, repository: { url: 'https://git.example/o/c' } }),
|
||||
entry('dsh-f', { installation: { status: 'provided' } }, {}, { source: { type: 'git', url: 'https://github.com/o/f', commit: COMMIT } }),
|
||||
entry('dsh-g', {}, {}, { compatibility: { ...INCOMPATIBLE, exempted: false } }),
|
||||
entry('dsh-h', {}, {}, { compatibility: { ...INCOMPATIBLE, exempted: true } }),
|
||||
@@ -287,6 +287,10 @@ describe('DiscoverSection', () => {
|
||||
const homepage = detail.getByRole('link', { name: 'https://example.com/alpha#readme' })
|
||||
expect([homepage.getAttribute('href'), homepage.getAttribute('target'), homepage.getAttribute('rel')])
|
||||
.toEqual(['https://example.com/alpha#readme', '_blank', 'noopener noreferrer'])
|
||||
const repository = detail.getByRole('link', { name: 'https://github.com/o/plugins' })
|
||||
expect([repository.getAttribute('href'), repository.getAttribute('target')]).toEqual(['https://github.com/o/plugins', '_blank'])
|
||||
expect(detail.getByText(en.factSourceCode)).toBeTruthy()
|
||||
expect(detail.getByText('packages/a')).toBeTruthy()
|
||||
fireEvent.click(detail.getByRole('button', { name: 'Install Alpha' }))
|
||||
fireEvent.click(within(screen.getByRole('dialog')).getByRole('button', { name: en.install }))
|
||||
expect(install).toHaveBeenCalledOnce()
|
||||
@@ -305,6 +309,13 @@ describe('DiscoverSection', () => {
|
||||
expect(screen.getAllByText(en.exempted)).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('links a repository without a directory', () => {
|
||||
renderTab()
|
||||
fireEvent.click(card('dsh-c').getByRole('button', { name: 'View dsh-c' }))
|
||||
const detail = within(document.querySelector('[data-market-detail="dsh-c"]') as HTMLElement)
|
||||
expect(detail.getByRole('link', { name: 'https://git.example/o/c' }).closest('dd')?.querySelector('code')).toBeNull()
|
||||
})
|
||||
|
||||
it('lists every version on the entry page and installs the one chosen', () => {
|
||||
const name = 'dsh-v'
|
||||
const versions = [
|
||||
|
||||
@@ -6075,7 +6075,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'MarketPlugin',
|
||||
declaration: 'export interface MarketPlugin {\n readonly name: string;\n readonly title?: LocalizedText;\n readonly description?: LocalizedText;\n readonly icon?: string;\n readonly author?: string;\n readonly homepage?: string;\n readonly metadata: MarketPluginMetadata;\n readonly versions: readonly MarketVersion[];\n}',
|
||||
declaration: 'export interface MarketPlugin {\n readonly name: string;\n readonly title?: LocalizedText;\n readonly description?: LocalizedText;\n readonly icon?: string;\n readonly author?: string;\n readonly homepage?: string;\n readonly repository?: MarketRepository;\n readonly metadata: MarketPluginMetadata;\n readonly versions: readonly MarketVersion[];\n}',
|
||||
},
|
||||
{
|
||||
name: 'MarketPluginMetadata',
|
||||
@@ -6085,6 +6085,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'MarketQuery',
|
||||
declaration: 'export interface MarketQuery {\n readonly text?: string;\n readonly category?: string;\n readonly labels?: readonly string[];\n readonly sort: MarketSort;\n readonly offset: number;\n readonly limit: number;\n readonly refresh?: boolean;\n readonly fetchedAt?: number;\n}',
|
||||
},
|
||||
{
|
||||
name: 'MarketRepository',
|
||||
declaration: 'export interface MarketRepository {\n readonly url: string;\n readonly directory?: string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'MarketSearchResult',
|
||||
declaration: 'export interface MarketSearchResult {\n readonly url: string;\n readonly title: LocalizedText;\n readonly description?: LocalizedText;\n readonly metadata: MarketIndexMetadata;\n readonly fetchedAt: number;\n readonly entries: readonly MarketListingEntry[];\n readonly total: number;\n readonly nextOffset: number | null;\n}',
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
en: 8eac2625e2e366a8
|
||||
zh: 652131f240e929e0
|
||||
/dsh-plugin-market-index/api:
|
||||
en: d75f1a7464f34b12
|
||||
zh: d766980adfc8b8ed
|
||||
en: e0e4444616427bf2
|
||||
zh: 3960a7602bb17705
|
||||
/dsh-plugin-market-index/model-experience:
|
||||
en: c060b1659cf43243
|
||||
zh: 4a2313822283291e
|
||||
|
||||
@@ -45,7 +45,7 @@ import { MarketIndexError, marketInstallTarget, parseMarketIndex } from '@deepse
|
||||
| `marketInstallTarget(name, version)` | `{ spec, registry? }` that installs that exact version. |
|
||||
| `MarketIndexError` | Validation failure carrying `path` and `reason`. |
|
||||
| `MAX_INLINE_ICON_BYTES` | Largest decoded payload of an inline `data:` icon, 32 KiB. |
|
||||
| `MarketIndex`, `MarketPlugin`, `MarketVersion`, `MarketEngines`, `MarketSource`, `MarketIndexMetadata`, `MarketPluginMetadata`, `MarketDeclaration`, `MarketSortKeyDeclaration`, `MarketInstallTarget` | Validated record types. |
|
||||
| `MarketIndex`, `MarketPlugin`, `MarketVersion`, `MarketEngines`, `MarketSource`, `MarketRepository`, `MarketIndexMetadata`, `MarketPluginMetadata`, `MarketDeclaration`, `MarketSortKeyDeclaration`, `MarketInstallTarget` | Validated record types. |
|
||||
|
||||
<a id="model-experience"></a>
|
||||
## Model Experience
|
||||
|
||||
@@ -45,7 +45,7 @@ import { MarketIndexError, marketInstallTarget, parseMarketIndex } from '@deepse
|
||||
| `marketInstallTarget(name, version)` | 安装该精确版本的 `{ spec, registry? }`。 |
|
||||
| `MarketIndexError` | 携带 `path` 和 `reason` 的校验失败。 |
|
||||
| `MAX_INLINE_ICON_BYTES` | 内联 `data:` 图标解码后载荷的最大字节数,即 32 KiB。 |
|
||||
| `MarketIndex`, `MarketPlugin`, `MarketVersion`, `MarketEngines`, `MarketSource`, `MarketIndexMetadata`, `MarketPluginMetadata`, `MarketDeclaration`, `MarketSortKeyDeclaration`, `MarketInstallTarget` | 经过校验的记录类型。 |
|
||||
| `MarketIndex`, `MarketPlugin`, `MarketVersion`, `MarketEngines`, `MarketSource`, `MarketRepository`, `MarketIndexMetadata`, `MarketPluginMetadata`, `MarketDeclaration`, `MarketSortKeyDeclaration`, `MarketInstallTarget` | 经过校验的记录类型。 |
|
||||
|
||||
<a id="model-experience"></a>
|
||||
## 模型体验
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { LocalizedText } from '@deepseek-ai/dsh-package-manifest'
|
||||
import semver from 'semver'
|
||||
import type {
|
||||
MarketDeclaration, MarketEngines, MarketIndex, MarketIndexMetadata, MarketInstallTarget, MarketPlugin, MarketPluginMetadata,
|
||||
MarketSortKeyDeclaration, MarketSource, MarketVersion,
|
||||
MarketRepository, MarketSortKeyDeclaration, MarketSource, MarketVersion,
|
||||
} from './types.ts'
|
||||
|
||||
export type * from './types.ts'
|
||||
@@ -181,6 +181,13 @@ function packagePath(value: unknown, path: string): string {
|
||||
return text
|
||||
}
|
||||
|
||||
function repository(value: unknown, path: string): MarketRepository {
|
||||
const object = objectAt(value, path)
|
||||
const url = httpUrl(object.url, `${path}.url`, ['https:', 'http:']).href
|
||||
const directory = optional(object, 'directory', path, packagePath)
|
||||
return { url, ...directory === undefined ? {} : { directory } }
|
||||
}
|
||||
|
||||
function source(value: unknown, path: string): MarketSource {
|
||||
const object = objectAt(value, path)
|
||||
switch (object.type) {
|
||||
@@ -267,6 +274,7 @@ function plugin(value: unknown, path: string, index: MarketIndexMetadata, indexU
|
||||
const iconUrl = optional(object, 'icon', path, (member, at) => icon(member, at, indexUrl))
|
||||
const author = optional(object, 'author', path, stringAt)
|
||||
const homepage = optional(object, 'homepage', path, homepageUrl)
|
||||
const repo = optional(object, 'repository', path, repository)
|
||||
return {
|
||||
name,
|
||||
...title === undefined ? {} : { title },
|
||||
@@ -274,6 +282,7 @@ function plugin(value: unknown, path: string, index: MarketIndexMetadata, indexU
|
||||
...iconUrl === undefined ? {} : { icon: iconUrl },
|
||||
...author === undefined ? {} : { author },
|
||||
...homepage === undefined ? {} : { homepage },
|
||||
...repo === undefined ? {} : { repository: repo },
|
||||
metadata: pluginMetadata(object.metadata, `${path}.metadata`, index),
|
||||
versions: versions(object.versions, `${path}.versions`),
|
||||
}
|
||||
|
||||
@@ -32,6 +32,14 @@ export interface MarketPluginMetadata {
|
||||
readonly sortKeys: Readonly<Record<string, number>>
|
||||
}
|
||||
|
||||
/** Where a plugin's source code is published; display only, never an install source. */
|
||||
export interface MarketRepository {
|
||||
/** Absolute http(s) URL of the repository's web page, without credentials, query, or fragment. */
|
||||
readonly url: string
|
||||
/** Relative POSIX directory of the package inside the repository; absent means the repository root. */
|
||||
readonly directory?: string
|
||||
}
|
||||
|
||||
/** Where one exact version is installed from. */
|
||||
export type MarketSource =
|
||||
| {
|
||||
@@ -76,6 +84,7 @@ export interface MarketPlugin {
|
||||
readonly author?: string
|
||||
/** Absolute http(s) URL of a page about the plugin. */
|
||||
readonly homepage?: string
|
||||
readonly repository?: MarketRepository
|
||||
readonly metadata: MarketPluginMetadata
|
||||
/** Non-empty, in descending SemVer precedence. */
|
||||
readonly versions: readonly MarketVersion[]
|
||||
|
||||
@@ -56,6 +56,7 @@ describe('parseMarketIndex', () => {
|
||||
icon: 'icons/foo.svg',
|
||||
author: 'Jane Doe',
|
||||
homepage: 'https://github.com/jane/dsh-foo#readme',
|
||||
repository: { url: 'https://github.com/jane/dsh-plugins', directory: 'packages/foo', type: 'git' },
|
||||
metadata: { category: 'productivity', keywords: ['review'], labels: ['official', 'ui'], sortKeys: { downloads: 12 }, extra: 1 },
|
||||
unknown: 'ignored',
|
||||
versions: [
|
||||
@@ -91,6 +92,7 @@ describe('parseMarketIndex', () => {
|
||||
icon: 'https://market.example/dsh/icons/foo.svg',
|
||||
author: 'Jane Doe',
|
||||
homepage: 'https://github.com/jane/dsh-foo#readme',
|
||||
repository: { url: 'https://github.com/jane/dsh-plugins', directory: 'packages/foo' },
|
||||
metadata: { category: 'productivity', keywords: ['review'], labels: ['official', 'ui'], sortKeys: { downloads: 12 } },
|
||||
versions: [
|
||||
{ version: '1.10.0', publishTimestamp: 2, source: { type: 'npm' }, engines: {} },
|
||||
@@ -174,6 +176,13 @@ describe('parseMarketIndex', () => {
|
||||
expect(entryRejection({ homepage: 'docs/foo' })).toBe('$.plugins[0].homepage must be an absolute URL')
|
||||
expect(entryRejection({ homepage: 3 })).toBe('$.plugins[0].homepage must be a string with a non-whitespace character')
|
||||
expect(parseMarketIndex(document({ plugins: [entry({ homepage: 'http://intranet.example/foo' })] }), INDEX_URL).plugins[0]?.homepage).toBe('http://intranet.example/foo')
|
||||
expect(parseMarketIndex(document({ plugins: [entry({ repository: { url: 'http://git.intranet/o/r' } })] }), INDEX_URL).plugins[0]?.repository)
|
||||
.toEqual({ url: 'http://git.intranet/o/r' })
|
||||
expect(entryRejection({ repository: 'https://github.com/o/r' })).toBe('$.plugins[0].repository must be an object')
|
||||
expect(entryRejection({ repository: { url: 'git+ssh://[email protected]/o/r.git' } })).toBe('$.plugins[0].repository.url must use https: or http:')
|
||||
expect(entryRejection({ repository: { url: 'https://github.com/o/r#main' } })).toBe('$.plugins[0].repository.url must not contain a query or fragment')
|
||||
expect(entryRejection({ repository: { url: 'https://github.com/o/r', directory: '../x' } }))
|
||||
.toBe('$.plugins[0].repository.directory must be a relative directory of letters, digits, and ._@+- segments without . or ..')
|
||||
expect(entryRejection({ versions: [] })).toBe('$.plugins[0].versions must be a non-empty array')
|
||||
expect(entryRejection({ versions: npmVersion() })).toBe('$.plugins[0].versions must be a non-empty array')
|
||||
expect(entryRejection({ versions: [npmVersion(), npmVersion({ publishTimestamp: 5 })] })).toBe('$.plugins[0].versions[1].version duplicates 1.2.3')
|
||||
|
||||
Reference in new issue
Block a user