feat(plugin): plugin marketplace, durable install persistence, skill manager
Add an in-app plugin marketplace fed by a remote web catalog: the host plugin-inventory gateway gains marketplaceList/Install/Uninstall remotes, a durable per-user install table reconciled against the actual profile, and git/npm/tarball/bundle install paths with non-interactive git and vendored pnpm. The Web Settings surface gains a sibling 插件市场 tab that lists the catalog with recommended badges, repository links, and a combined sort (recommended first, then catalog priority, then id). Add a host + client skill manager: list local skills by direct filesystem discovery, and install/uninstall/toggle/edit their SKILL.md from git/npm/ tarball/local sources in the writable user root. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-15-plugin-marketplace.md
|
||||
2026-08-15-plugin-marketplace.md: 24b496d41a4103b8891489eb9033d05c0ea09d83
|
||||
2026-08-15-plugin-marketplace.zh.md: 7dbf44fa666f933a0340b9aa8db018643efb8008
|
||||
@@ -0,0 +1,75 @@
|
||||
# Agent Note: plugin marketplace
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-15-plugin-marketplace.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The plugin settings page only installs a plugin by typing an arbitrary pnpm specifier
|
||||
into a text field. There is no curated catalog a user can browse, and no durable record
|
||||
of which community plugins they have installed, separate from the profile's own bundle
|
||||
composition. A marketplace needs a remote catalog, a per-plugin install prescription,
|
||||
and an authoritative "is this installed" table.
|
||||
|
||||
## Decision
|
||||
|
||||
Add a plugin marketplace to `PluginInventoryGateway` (`packages/host/plugin-inventory`).
|
||||
|
||||
**Catalog** (`src/marketplace.ts`). The static web host serves one index JSON
|
||||
(`plugins/plugins.json`, default URL
|
||||
`https://deepseek.pinesound.cn/plugins/plugins.json`, overridable via `DSH_MARKETPLACE_URL`)
|
||||
listing plugins, and a per-plugin JSON beside it (`plugins/<id>.json`) prescribing the
|
||||
install method — `git`, `npm`, `tarball`, or `bundle` — with the pnpm specifier and the
|
||||
dependency name that lands in the profile's `dependencies`. The helpers are pure
|
||||
(fetch + parse + table IO) and unit-test without a Cordis context.
|
||||
|
||||
**Three direct Remotes** (`src/index.ts`):
|
||||
- `marketplaceList()` fetches the catalog and marks each entry installed from the table.
|
||||
- `marketplaceInstall(id, consentBuilds?)` fetches the per-plugin spec and maps it onto
|
||||
the existing install path — git/npm/tarball via the registry Remote, bundle via the
|
||||
offline compose — then records the install in the table on success. It honors the same
|
||||
two-phase build-consent flow: a `pendingBuilds` result pauses, and the retry carries the
|
||||
approved set in `consentBuilds`.
|
||||
- `marketplaceUninstall(id)` resolves the dependency (or bundle name) from the table row,
|
||||
runs the existing `uninstall`, and drops the row.
|
||||
|
||||
**Install table.** A small JSON document at `$DSH_HOME/plugin-marketplace/installed.json`
|
||||
(`dshHomePath('plugin-marketplace')`) maps plugin id → `{ method, spec, dependency,
|
||||
installedAt }`. It is written atomically and is the **authoritative** "is this installed"
|
||||
check for the marketplace list, per the product requirement.
|
||||
|
||||
**SSRF posture.** Per-plugin spec URLs are derived from the fixed catalog base plus the
|
||||
plugin id (`marketplaceBaseUrl` + `marketplaceSpecUrl`), never read from catalog content,
|
||||
so a hostile catalog cannot point the app at an arbitrary URL. Fetch runs in the harness
|
||||
service with `global.fetch` (the repo's convention), not in the renderer.
|
||||
|
||||
**UI** (`ui-settings-plugin-inventory`). The Plugins settings section gains a third
|
||||
sibling tab — `settings.plugins.tab` id `marketplace`, order 20 — to the right of the
|
||||
plugin-list tab `all` (order 10), both behind 插件配置 (order 0). `PluginMarketplaceSettingsTab`
|
||||
lists catalog cards (name, description, author, Installed tag, Install/Uninstall button)
|
||||
with its own build-consent modal; a fetch miss shows a retryable failure. The plugin-list
|
||||
tab stays single-column and unchanged. A catalog entry may carry an optional `recommended`
|
||||
flag (parsed into `MarketplacePluginMeta.recommended`), which renders a 推荐 badge on that
|
||||
card.
|
||||
|
||||
## Verification
|
||||
|
||||
- `tests/marketplace.spec.ts` (pure): catalog/spec parse, HTTP errors, table
|
||||
read/write/atomicity, base-URL and spec-URL derivation.
|
||||
- `tests/inventory.spec.ts`: `marketplaceList` overlays the table; `marketplaceInstall`
|
||||
(fake pnpm) records the row; `marketplaceUninstall` drops it; unknown id fails loud.
|
||||
All keyed to a temp `$DSH_HOME`.
|
||||
- `tests/marketplace-tab.client.spec.tsx`: marketplace tab render, install, uninstall,
|
||||
build-consent, and load-failure retry.
|
||||
- `pnpm run build:lib:host` regenerates the Typert Host/Client Remote artifacts.
|
||||
|
||||
## Alternatives
|
||||
|
||||
- **Fetch the catalog in the Electron main and pass it into the harness.** Rejected: the
|
||||
install must run in the harness anyway, and every other package fetches with
|
||||
`global.fetch`; keeping fetch next to install avoids a second transport.
|
||||
- **Derive installed state from profile dependencies instead of a dedicated table.**
|
||||
Rejected: the product asked for a durable table as the authoritative check, and a
|
||||
git/tarball install's resolved package name is not reliably recoverable from a spec, so
|
||||
the table records the dependency name explicitly.
|
||||
@@ -0,0 +1,65 @@
|
||||
# Agent Note:插件市场
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-15-plugin-marketplace.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
插件设置页只能通过在文本框里输入任意 pnpm specifier 来安装插件。缺少一个可供浏览的策展目录,
|
||||
也没有与 profile 自身 bundle 组合相独立的、关于用户已安装哪些社区插件的持久记录。
|
||||
市场需要一份远程目录、每个插件的安装规定,以及一张权威的"是否已安装"表。
|
||||
|
||||
## 决策
|
||||
|
||||
给 `PluginInventoryGateway`(`packages/host/plugin-inventory`)新增插件市场。
|
||||
|
||||
**目录**(`src/marketplace.ts`)。静态网站宿主提供一份索引 JSON
|
||||
(`plugins/plugins.json`,默认 URL
|
||||
`https://deepseek.pinesound.cn/plugins/plugins.json`,可用 `DSH_MARKETPLACE_URL` 覆盖)
|
||||
列出插件,旁边每个插件的 JSON(`plugins/<id>.json`)规定安装方式——`git`、`npm`、`tarball`
|
||||
或 `bundle`——以及 pnpm specifier 和落地到 profile `dependencies` 的依赖名。
|
||||
这些辅助函数是纯函数(拉取 + 解析 + 表格 IO),可在无 Cordis 上下文的单测中测试。
|
||||
|
||||
**三个直接 Remote**(`src/index.ts`):
|
||||
- `marketplaceList()` 拉取目录,并根据表格把每条标记为已安装。
|
||||
- `marketplaceInstall(id, consentBuilds?)` 拉取该插件的 spec,映射到既有安装路径——
|
||||
git/npm/tarball 走 registry Remote,bundle 走离线组合——成功后写入表格。
|
||||
沿用同样的两阶段构建脚本同意流程:`pendingBuilds` 时暂停,重试时把已同意集合放在
|
||||
`consentBuilds` 里。
|
||||
- `marketplaceUninstall(id)` 从表格行解析依赖名(或 bundle 名),执行既有 `uninstall`,
|
||||
并删除表行。
|
||||
|
||||
**安装表**。`$DSH_HOME/plugin-marketplace/installed.json`
|
||||
(`dshHomePath('plugin-marketplace')`)下的一小份 JSON 文档,映射
|
||||
插件 id → `{ method, spec, dependency, installedAt }`。原子写入,是市场列表判断
|
||||
"是否已安装"的**权威**依据(按产品要求)。
|
||||
|
||||
**SSRF 姿态**。每个插件的 spec URL 由固定目录基地址加插件 id 推导
|
||||
(`marketplaceBaseUrl` + `marketplaceSpecUrl`),绝不读取目录内容,因此恶意目录无法把
|
||||
应用指向任意 URL。拉取在 harness 服务里用 `global.fetch`(仓库惯例),而不是在 renderer。
|
||||
|
||||
**UI**(`ui-settings-plugin-inventory`)。Plugins 设置区新增第三个同级 tab——
|
||||
`settings.plugins.tab` id 为 `marketplace`,order 20——位于插件列表 tab `all`
|
||||
(order 10)右侧,两者都在 插件配置(order 0)之后。`PluginMarketplaceSettingsTab`
|
||||
列出目录卡片(名称、描述、作者、已安装标签、安装/卸载按钮),带自己的构建脚本同意弹窗;
|
||||
拉取失败只显示可重试的失败态。插件列表 tab 保持单列、不变。
|
||||
目录条目可带可选的 `recommended` 标志(解析进 `MarketplacePluginMeta.recommended`),
|
||||
该卡片会渲染「推荐」角标。
|
||||
|
||||
## 验证
|
||||
|
||||
- `tests/marketplace.spec.ts`(纯函数):目录/spec 解析、HTTP 错误、表格读写/原子性、
|
||||
基地址与 spec URL 推导。
|
||||
- `tests/inventory.spec.ts`:`marketplaceList` 叠加表格;`marketplaceInstall`(假 pnpm)
|
||||
写入表行;`marketplaceUninstall` 删除表行;未知 id 显式报错。全部以临时 `$DSH_HOME` 为根。
|
||||
- `tests/marketplace-tab.client.spec.tsx`:市场 tab 渲染、安装、卸载、构建脚本同意、
|
||||
加载失败重试。
|
||||
- `pnpm run build:lib:host` 重新生成 Typert Host/Client Remote 产物。
|
||||
|
||||
## 备选方案
|
||||
|
||||
- **在 Electron 主进程拉取目录再传给 harness。** 已否决:安装本来就必须在 harness 里进行,
|
||||
而且其它 package 都用 `global.fetch`;让拉取紧挨着安装,可避免多一条传输通道。
|
||||
- **用 profile 依赖判断已安装,而不建独立表格。** 已否决:产品要求一张持久表作为权威判断,
|
||||
而且 git/tarball 安装解析出的包名无法从 spec 可靠反推,因此表格显式记录依赖名。
|
||||
@@ -0,0 +1,71 @@
|
||||
# Agent Note: skills manager settings tab + built-in host plugin
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-15-skill-manager.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Skills are discovered by `dsh-skill-filesystem` from filesystem roots
|
||||
(`$DSH_HOME/skills`, project `.agents/skills`, bundled, …) as `SKILL.md` bundles,
|
||||
but there is **no management surface** — no way to see all local skills, install one
|
||||
from a git/npm/tarball/local source, uninstall it, or toggle its invocation. The
|
||||
plugin settings section had config/list/marketplace tabs but no skills tab.
|
||||
|
||||
## Decision
|
||||
|
||||
Add a **skills management tab** beside 插件配置 / 插件列表 / 插件市场 in the Plugins
|
||||
settings section, backed by a new built-in host plugin.
|
||||
|
||||
**Host: `packages/host/skill-manager` (`@deepseek-ai/dsh-host-skill-manager`).**
|
||||
`SkillManagerGateway` (serviceKey `skillManager`) exposes five direct Remotes:
|
||||
- `list()` — reads `ctx.skills.list({})` (the global layer: user/bundled/custom/runtime
|
||||
skills) and marks `managed` = lives in `$DSH_HOME/skills` (`source === 'user-dsh'`).
|
||||
- `install(spec)` — resolves a `git | npm | tarball | local` source into a validated
|
||||
`SKILL.md` bundle and copies it to `$DSH_HOME/skills/<name>`; the provider watcher
|
||||
discovers it with no restart.
|
||||
- `uninstall(name)` — deletes a user-root skill; refuses names not installed there.
|
||||
- `setEnabled(name, patch)` — rewrites `SKILL.md` frontmatter
|
||||
`disable-model-invocation` / `user-invocable`.
|
||||
- `setDescription(name, description)` — rewrites `SKILL.md` frontmatter `description`.
|
||||
|
||||
`src/install.ts` materializes the source (git clone, `npm pack`+tar, tarball fetch+tar,
|
||||
local dir), unwraps a single top-level dir, locates `SKILL.md` (root or `skills/<name>/`),
|
||||
and validates the provider-mandated `name` + `description`. Git runs with the same
|
||||
non-interactive environment as the plugin marketplace (`GIT_TERMINAL_PROMPT=0` +
|
||||
`StrictHostKeyChecking=accept-new`) so a first-time clone never hangs.
|
||||
|
||||
`src/skill-io.ts` parses/rewrites the frontmatter (matching the provider's key semantics)
|
||||
and does the read/install/delete filesystem work. No durable install table is needed:
|
||||
the filesystem **is** the installed state, unlike plugin installs which record a profile
|
||||
dependency.
|
||||
|
||||
**Client: `packages/client/ui-settings-skill-manager`
|
||||
(`@deepseek-ai/dsh-client-ui-settings-skill-manager`).** Registers
|
||||
`settings.plugins.tab` id `skills` (order 30). `SkillManagerSettingsTab` lists skills
|
||||
with their invocation tags and manage/read-only labels, plus an install row (source
|
||||
select + spec) and per-managed-skill toggle / edit-description / delete actions.
|
||||
|
||||
**Wiring:** the host + client packages are composed in `packages/bundle/web-app`
|
||||
(cordis.patch.yml + package.json), `@deepseek-ai/dsh-host-skill-manager` is added to
|
||||
`REQUIRED_PLUGINS` (protected, not toggleable), and the `skillManager` namespace is
|
||||
mounted in `packages/api/remotes/src/client/index.ts`.
|
||||
|
||||
## Verification
|
||||
|
||||
- Host: `tests/skill-manager.spec.ts` (Remote surface + list/install/uninstall/toggle/
|
||||
description against a temp `$DSH_HOME`), `tests/install.spec.ts` (local source
|
||||
resolution + validation), `tests/skill-io.spec.ts` (frontmatter parse/rewrite + fs).
|
||||
- Client: `tests/skill-manager.client.spec.tsx` (render, install, toggle, edit, delete,
|
||||
load-failure retry).
|
||||
- `pnpm run build:lib:host` + `pnpm run build:lib:client` (regenerates the `skillManager`
|
||||
Typert Host/Client Remote artifacts); all modified packages typecheck.
|
||||
|
||||
## Alternatives
|
||||
|
||||
- **Fold skill management into `plugin-inventory`.** Rejected: skills are filesystem
|
||||
artifacts with a different install target than plugins (skill root vs profile deps),
|
||||
so a separate host package keeps each seam's ownership clear.
|
||||
- **A durable install table like the plugin marketplace.** Rejected: a skill is
|
||||
installed iff its bundle exists in the user root; the watcher already derives the
|
||||
catalog from the filesystem, so an extra table would only drift.
|
||||
Reference in New Issue
Block a user