diff --git a/.agents/notes/implemented/feature/2026-10-02-plugin-market-index-format.i18n.yaml b/.agents/notes/implemented/feature/2026-10-02-plugin-market-index-format.i18n.yaml index e250cfe3fa..f6516eb25c 100644 --- a/.agents/notes/implemented/feature/2026-10-02-plugin-market-index-format.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-10-02-plugin-market-index-format.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/feature/2026-10-02-plugin-market-index-format.md b/.agents/notes/implemented/feature/2026-10-02-plugin-market-index-format.md index 0de60f2f0a..3a70b63f0f 100644 --- a/.agents/notes/implemented/feature/2026-10-02-plugin-market-index-format.md +++ b/.agents/notes/implemented/feature/2026-10-02-plugin-market-index-format.md @@ -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:;base64,`, where `` is `image/svg+xml`, `image/png`, `image/jpeg`, or `image/webp`, `` 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 diff --git a/.agents/notes/implemented/feature/2026-10-02-plugin-market-index-format.zh.md b/.agents/notes/implemented/feature/2026-10-02-plugin-market-index-format.zh.md index 0ceec617c1..94dfdd5ad0 100644 --- a/.agents/notes/implemented/feature/2026-10-02-plugin-market-index-format.zh.md +++ b/.agents/notes/implemented/feature/2026-10-02-plugin-market-index-format.zh.md @@ -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:;base64,`,其中 `` 为 `image/svg+xml`、`image/png`、`image/jpeg` 或 `image/webp`,`` 是按规范填充的 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。 ### 读取器规则 diff --git a/docs/user/guide/plugin-market.i18n.yaml b/docs/user/guide/plugin-market.i18n.yaml index b18b20f8d4..1043aa1f63 100644 --- a/docs/user/guide/plugin-market.i18n.yaml +++ b/docs/user/guide/plugin-market.i18n.yaml @@ -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 diff --git a/docs/user/guide/plugin-market.md b/docs/user/guide/plugin-market.md index 4f654eace9..4d19052e17 100644 --- a/docs/user/guide/plugin-market.md +++ b/docs/user/guide/plugin-market.md @@ -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. | diff --git a/docs/user/guide/plugin-market.zh.md b/docs/user/guide/plugin-market.zh.md index 7a1bc2ac06..e4b9529b8a 100644 --- a/docs/user/guide/plugin-market.zh.md +++ b/docs/user/guide/plugin-market.zh.md @@ -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 映射为数值的对象,例如下载量或评分。 | diff --git a/packages/boot/plugin-market/README.i18n.yaml b/packages/boot/plugin-market/README.i18n.yaml index 5e045fdfae..8e54eca06a 100644 --- a/packages/boot/plugin-market/README.i18n.yaml +++ b/packages/boot/plugin-market/README.i18n.yaml @@ -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 diff --git a/packages/boot/plugin-market/README.md b/packages/boot/plugin-market/README.md index 971f3684fe..4e191bf8c0 100644 --- a/packages/boot/plugin-market/README.md +++ b/packages/boot/plugin-market/README.md @@ -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 diff --git a/packages/boot/plugin-market/README.zh.md b/packages/boot/plugin-market/README.zh.md index 9f6076fa30..6d590412b0 100644 --- a/packages/boot/plugin-market/README.zh.md +++ b/packages/boot/plugin-market/README.zh.md @@ -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 影响 diff --git a/packages/boot/plugin-market/src/tools.ts b/packages/boot/plugin-market/src/tools.ts index 08ecae4000..f1783a2969 100644 --- a/packages/boot/plugin-market/src/tools.ts +++ b/packages/boot/plugin-market/src/tools.ts @@ -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, diff --git a/packages/boot/plugin-market/tests/tools.spec.ts b/packages/boot/plugin-market/tests/tools.spec.ts index 7485248084..ae1b929b15 100644 --- a/packages/boot/plugin-market/tests/tools.spec.ts +++ b/packages/boot/plugin-market/tests/tools.spec.ts @@ -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 }, diff --git a/packages/client/ui-plugin-market/README.i18n.yaml b/packages/client/ui-plugin-market/README.i18n.yaml index 61b8d1cec7..2b12760fe5 100644 --- a/packages/client/ui-plugin-market/README.i18n.yaml +++ b/packages/client/ui-plugin-market/README.i18n.yaml @@ -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 diff --git a/packages/client/ui-plugin-market/README.md b/packages/client/ui-plugin-market/README.md index d37764db9e..02a88fd003 100644 --- a/packages/client/ui-plugin-market/README.md +++ b/packages/client/ui-plugin-market/README.md @@ -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. diff --git a/packages/client/ui-plugin-market/README.zh.md b/packages/client/ui-plugin-market/README.zh.md index f4e672a491..69b0b32d64 100644 --- a/packages/client/ui-plugin-market/README.zh.md +++ b/packages/client/ui-plugin-market/README.zh.md @@ -28,7 +28,7 @@ kind: "package-reference" 工具栏包含:插件市场菜单,用于切换插件市场,最后一项为**管理索引源…**;搜索框;插件市场声明了标签时显示的**标签**菜单,以勾选标记支持多选;**排序**菜单;以及刷新按钮。插件市场声明了分类时,其后显示分类按钮,**全部**排在第一位。 -每张卡片显示图标、本地化标题、标签、一行描述,以及默认版本、作者和发布日期,并只有一个操作:可安装的条目显示**安装**;没有默认版本、且最高版本既不兼容也未豁免的条目显示禁用的**安装**,其提示框说明没有与当前运行的 DSH 兼容的版本;其余条目显示**已安装**、**有新版本**或**随 DSH 提供**,点击后切换到「已安装」标签页,并在该标签页列出该组合包时打开其页面。选择卡片会打开条目页面,显示其完整描述、兼容性提示和默认版本的信息:作者;主页(在新标签页中打开的链接);npm 包和注册表,或 Git 仓库、完整提交和目录;发布日期;分类;标签;以及插件市场。其后是按 SemVer 降序排列的**版本**列表;每行标出推荐、预发布、已安装、不兼容和已豁免的版本,并为安装检查接受的其他版本提供**安装**,DSH 随附的组合包除外。页面的返回链接回到列表,搜索、筛选和排序保持不变。 +每张卡片显示图标、本地化标题、标签、一行描述,以及默认版本、作者和发布日期,并只有一个操作:可安装的条目显示**安装**;没有默认版本、且最高版本既不兼容也未豁免的条目显示禁用的**安装**,其提示框说明没有与当前运行的 DSH 兼容的版本;其余条目显示**已安装**、**有新版本**或**随 DSH 提供**,点击后切换到「已安装」标签页,并在该标签页列出该组合包时打开其页面。选择卡片会打开条目页面,显示其完整描述、兼容性提示和默认版本的信息:作者;主页和源代码仓库(在新标签页中打开的链接,仓库旁显示包所在目录);npm 包和注册表,或 Git 仓库、完整提交和目录;发布日期;分类;标签;以及插件市场。其后是按 SemVer 降序排列的**版本**列表;每行标出推荐、预发布、已安装、不兼容和已豁免的版本,并为安装检查接受的其他版本提供**安装**,DSH 随附的组合包除外。页面的返回链接回到列表,搜索、筛选和排序保持不变。 **安装**选择默认版本并打开确认对话框,显示条目的图标、标题、该版本、作者、来源信息、适用时的豁免提示,以及信任提示。确认后,插件页的安装对话框以该条目的精确 spec 和注册表在检查界面打开;不显示 spec 表单,被拒绝时提供**重试**。条目指定的注册表只用于这一次安装。 diff --git a/packages/client/ui-plugin-market/src/client/DiscoverSection.tsx b/packages/client/ui-plugin-market/src/client/DiscoverSection.tsx index b099f213fa..011b8a119c 100644 --- a/packages/client/ui-plugin-market/src/client/DiscoverSection.tsx +++ b/packages/client/ui-plugin-market/src/client/DiscoverSection.tsx @@ -239,6 +239,15 @@ function EntryDetail({ entry, listing, t, face, language, onBack, onInstall, onO
{entry.homepage}
)} + {entry.repository === undefined ? null : ( +
+
{t('factSourceCode')}
+
+ {entry.repository.url} + {entry.repository.directory === undefined ? null : <> {entry.repository.directory}} +
+
+ )} {sourceFacts(entry.name, version).map(fact => (
{t(FACT_KEYS[fact.kind])}
{fact.value}
))} diff --git a/packages/client/ui-plugin-market/src/client/locales.ts b/packages/client/ui-plugin-market/src/client/locales.ts index d645745efc..63bc504cea 100644 --- a/packages/client/ui-plugin-market/src/client/locales.ts +++ b/packages/client/ui-plugin-market/src/client/locales.ts @@ -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', diff --git a/packages/client/ui-plugin-market/tests/discover.client.spec.tsx b/packages/client/ui-plugin-market/tests/discover.client.spec.tsx index 18e907b2a3..d7b64e9ea1 100644 --- a/packages/client/ui-plugin-market/tests/discover.client.spec.tsx +++ b/packages/client/ui-plugin-market/tests/discover.client.spec.tsx @@ -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: 'dsh-a@1.0.0', 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 = [ diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 152a50cca8..8e8ec8eaf0 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -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}', diff --git a/packages/util/plugin-market-index/README.i18n.yaml b/packages/util/plugin-market-index/README.i18n.yaml index 0dcd5737df..9f56252a3b 100644 --- a/packages/util/plugin-market-index/README.i18n.yaml +++ b/packages/util/plugin-market-index/README.i18n.yaml @@ -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 diff --git a/packages/util/plugin-market-index/README.md b/packages/util/plugin-market-index/README.md index 02d49cb1f1..2255022c8e 100644 --- a/packages/util/plugin-market-index/README.md +++ b/packages/util/plugin-market-index/README.md @@ -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. | ## Model Experience diff --git a/packages/util/plugin-market-index/README.zh.md b/packages/util/plugin-market-index/README.zh.md index 729249ade8..e19d6cfcb9 100644 --- a/packages/util/plugin-market-index/README.zh.md +++ b/packages/util/plugin-market-index/README.zh.md @@ -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` | 经过校验的记录类型。 | ## 模型体验 diff --git a/packages/util/plugin-market-index/src/index.ts b/packages/util/plugin-market-index/src/index.ts index a5c0267071..ae700b5419 100644 --- a/packages/util/plugin-market-index/src/index.ts +++ b/packages/util/plugin-market-index/src/index.ts @@ -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`), } diff --git a/packages/util/plugin-market-index/src/types.ts b/packages/util/plugin-market-index/src/types.ts index e340c2cad7..5203d790a2 100644 --- a/packages/util/plugin-market-index/src/types.ts +++ b/packages/util/plugin-market-index/src/types.ts @@ -32,6 +32,14 @@ export interface MarketPluginMetadata { readonly sortKeys: Readonly> } +/** 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[] diff --git a/packages/util/plugin-market-index/tests/plugin-market-index.spec.ts b/packages/util/plugin-market-index/tests/plugin-market-index.spec.ts index 757cd582d0..d42849961e 100644 --- a/packages/util/plugin-market-index/tests/plugin-market-index.spec.ts +++ b/packages/util/plugin-market-index/tests/plugin-market-index.spec.ts @@ -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://git@github.com/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')