English | 简体中文
A plugin installer for DeepSeek Harness (dsh): a ready-to-install bundle that lets you install/remove profile plugins straight from the Web GUI — no terminal needed, and installs take effect without a restart by default.
- Host half (
src/installer.ts) registersPOST /dsh-plugin-installer/apion the web server — the exact same operation asdsh plugin --profile <name> add/remove(forwards topnpminside the profile directory + reconciles thedsh.profile.bundleslayer stack). - Browser half (
src/client.ts, withsrc/installer-client.ts) registers an 安装 (Install) tab under Settings → Plugins: an install input, the current bundle layers with run/needs-restart badges, and per-bundle uninstall buttons.
The package follows the official bundle distribution model: it declares dsh.bundle plus cordis.patch.yml, and dsh plugin add activates it as a config layer.
dsh-plugin-installer/
├── package.json # npm manifest + dsh.bundle / dsh.client declarations + prepare build script
├── tsconfig.json # strict type-check configuration (tsc --noEmit)
├── tsdown.config.ts # build config: Node library (lib/) + client bundle (lib/client.js), self-contained for git-install prepare
├── cordis.patch.yml # bundle config layer: client-discovery carrier row + installer host row
├── dev/cordis.yml # local dev overlay (points at source; use with dsh web --patch; host half only)
├── src/
│ ├── index.ts # package-name entry (client-half discovery carrier): no-op, host side does nothing
│ ├── installer.ts # host half of the plugin installer: webServer route → pnpm add/remove + bundle reconcile + no-restart hot activation + disable/enable
│ ├── client.ts # browser half entry: registers the 安装 tab in the settings.plugins.tab slot
│ └── installer-client.ts # the Install tab's React implementation (same-origin fetch to the host API)
└── test/smoke.mjs # smoke test on the build output (routes + API methods + hot activation)
Install this package into the GUI-bearing web profile:
# local directory (build first: a local-directory install does not run prepare)
cd /path/to/dsh-plugin-installer && pnpm build
dsh plugin --profile web add /path/to/dsh-plugin-installer
# or directly from GitHub (a git install runs prepare automatically)
dsh plugin --profile web add github:you/dsh-plugin-installerOn a GitHub install, pnpm ≥10 refuses the git dependency's prepare script the first time; add the package name pnpm prints to the profile's pnpm-workspace.yaml and retry:
allowBuilds:
dsh-plugin-installer: trueThis allowlist authorizes executing that package's code at install time — only allow source you trust, and prefer pinning a commit:
github:you/dsh-plugin-installer#<sha>.
Restart dsh web once (the installer itself must enter the tree before it can work), then open http://127.0.0.1:3080 → bottom-left Settings → Plugins → 安装 tab.
A
--patchoverlay only loads the plugin's host half (module resolution cannot reach package-level declarations), so the browser-half 安装 tab is invisible under an overlay; full functionality requires the profile install above.
From the root of a deepseek-harness source checkout, load this repo's source directly via an overlay (no install, no build; host half only):
pnpm dsh web --patch /absolute/path/to/dsh-plugin-installer/dev/cordis.ymlSet name in dev/cordis.yml to this repo's absolute path on your machine.
Run the checks yourself during development:
pnpm install --ignore-workspace # this package is standalone; install its own deps
pnpm typecheck
pnpm build
node test/smoke.mjs# After installing this bundle into the web profile and booting the GUI:
# dsh plugin --profile web add /path/to/dsh-plugin-installer && dsh web
# open http://127.0.0.1:3080 → Settings → Plugins → 安装Install sources: npm package names (dsh-my-plugin), github:user/repo, file: links, and absolute paths to local directories/tarballs (relative paths are rejected — the browser has no working directory to anchor them to).
Every non-built-in bundle row gets a 禁用/启用 (disable/enable) button: the installer keeps a marker-delimited managed block (# >>> dsh-plugin-installer: managed …) in the profile's cordis.patch.yml, writing { id, disabled: true/false } overrides for each of the bundle's config rows. That file is both the persistence (composed into the config tree at boot as the user layer) and the hot-reload entry point (dsh's config HMR watches it), so disable/enable is also restart-free. Badge states: 运行中 (running) / 已禁用 (disabled) / 需重启 (needs restart).
Built-in bundles (@deepseek-ai/* — the harness core dsh-base and the GUI itself dsh-web-app) carry an 内置 (built-in) badge and are not open to disable or removal: they are not profile dependencies, and disabling/removing them breaks the profile. To fine-control built-in features (e.g. disable one of their tool rows), edit the profile's cordis.patch.yml directly and write disabled: true on the target row.
Installs take effect without a restart by default: after pnpm add/remove + reconciliation, the installer rewrites the running tree's root-include patch list directly — appending the new bundle's patch rows (or filtering out the removed bundle's rows), which the Loader applies transactionally so the plugin starts/stops immediately. This is fully self-contained; no harness source change is needed (dsh only hot-reloads cordis.patch.yml config patches; the bundle layer is frozen at boot, so editing package.json alone cannot avoid a restart).
- When the install responds with
hot: true, the status line reads "已热生效,无需重启" and the bundle row badge flips to "运行中"; - If the live tree cannot apply the change (no Loader, no root include, or a failed update), it degrades to the old behavior: "重启 dsh 后生效" with a "需重启" badge — a restart is always the fallback, and after it the bundle is owned by
dsh.profile.bundlesas usual.
Known limitations (all recoverable by restarting):
- Hot-applied bundle rows are appended to the end of the running patch list. If you hand-edit
cordis.patch.ymlafter a hot install but before a restart, dsh's config reload recomposes from the boot-frozen bundle layer, so the hot rows leave the live tree (the bundle row falls back to "需重启") until the next restart. - Before a restart, id-targeted overrides of a hot-installed bundle's rows in
cordis.patch.yml(e.g.disabled: true) do not apply — patches apply in order and the user layer runs before the appended rows. After a restart the bundle-layer order takes over and overrides work normally. - Plugins with a browser half (
dsh.client) need a page refresh after hot activation for their UI to be discovered (client-modules scans on page load).
When hot activation fails, the status line surfaces the Loader's error verbatim — first check whether it is a plugin problem unrelated to hot activation (e.g. a tool/route name collision: two copies of the same template both registering
greet). Such conflicts fail on restart too; resolve the conflict first.
Notes:
- pnpm must be on the harness process PATH. Git installs run the package's
preparescript; on pnpm ≥10 the first one is refused and the error surfaces the exact package key to add underallowBuildsin the profile'spnpm-workspace.yaml(the GUI shows the full pnpm output). - No harness source edit needed — the installer tab goes through the public
settings.plugins.tabslot plus a plain web-server route, so it works on an unmodified harness checkout. - The host half only activates in profiles that mount
webServer(the web profile). In headless profiles it disables itself instead of failing the boot. - The route is loopback-only (the web server binds
127.0.0.1), and running it executespnpmon your machine — same trust boundary as runningdsh plugin addyourself.
package.jsondeclaresdsh.client: { platform: "web" }+exports["./client"]→ dsh's client-modules discoverslib/client.jsand loads it as a browser plugin;- Discovery carrier: client-modules resolves package.json by the Loader entry name (
require.resolve('<entry-name>/package.json')), socordis.patch.ymlmust keep one row whosenameequals the package name itself (dsh-plugin-installer, backed bysrc/index.ts, a no-op) — only then does the browser loadlib/client.js; src/client.tsregisters the 安装 tab in thesettings.plugins.tabslot;src/installer-client.tscalls the host half through a same-originfetchto/dsh-plugin-installer/api;- the host half (
src/installer.ts) registers that route viactx.webServer, forwards pnpm, reconciles the bundle layers, and performs the no-restart hot activation; - at runtime the client half depends only on
react(provided by the browser platform module table); everything else goes throughctxservices and no@deepseek-aiclient package is imported — keep that discipline when editing.
- Rename the package: keep
package.jsonname,src/index.tsname, and both rows'name/idincordis.patch.ymlconsistent. Renaming also touches the browser-half spots: the client bundleidintsdown.config.ts(__ModuleLoader__.load({ id })),dsh.clientinpackage.json, and the slot id insrc/client.ts. - Change the install target / command: edit
runPnpmcalls andROUTE_PREFIXinsrc/installer.ts. - Remember to
declare module '@deepseek-ai/cordis'to mergeContext/Eventstypes — that is what keeps cross-package boundaries type-safe.
- npm:
pnpm publish(filesalready includes the build output and the patch; no extra steps) - tarball:
pnpm pack, thendsh plugin --profile web add ./dsh-plugin-installer-0.1.0.tgz - git:
dsh plugin add github:you/dsh-plugin-installer(combined with theallowBuildsstep above)