kingOfSoySauce/dsh-skin-market
kingOfSoySauce/dsh-skin-market原生皮肤市场与生命周期管理器,发现社区皮肤、展示预览与兼容状态,并提供已验证的一键安装或手动安装入口。
设置 → 插件 分区里的插件管理 Tab:从 npm/git 安装带进度、下次启动生效的启停开关、安装冲突对账可撤销、失败一键转交修复会话。
English | 中文
Client Plugin Manager & Lifecycle Control UI for DeepSeek Harness (DSH) Installed Plugin Version Checks · One-Click Batch Updates · Runtime Compatibility Gate · Safety Rollback · Dual Channels
An update and lifecycle manager for DeepSeek Harness (DSH) Web GUI and official desktop client: adds real-time registry version comparisons with DSH runtime compatibility checks directly into the official Plugins page, providing list-level batch updates, safe rollback mechanisms, and restart-to-apply workflows.
plugins.detail.section seat that page declares), rendered on an installed bundle's page and nowhere else. There is no tab of its own in the Plugins settings section.plugins.detail.*, plugins.item, plugins.bundle.config, plugins.row.config and plugins.bundle.activation), so the toolbar is inserted into that element and kept in place through the family's shared body-mutation hub. One click checks every installed plugin; the panel lists the third-party rows that have a newer release, with the declared DSH minimum for any row this runtime is too old for; Update all applies the eligible rows one at a time with per-row progress and stops at the first failure, leaving the rest listed; Restart now then makes the update effective (see the restart note under Known limitations). Official @deepseek-ai/ packages are never updated in bulk — their version belongs to the DSH installation./plugin-installer and /plugin-control loopback RPC channels; on the npm-published web runtime those channels do not exist, so the package's host half mounts a loopback-fenced HTTP gateway that spawns the official dsh plugin CLI for installs/removals (the single writer) and writes disabled override rows for enablement. An application-owned profile (a packaged Desktop launch) takes the third writer for installs, updates and removals: the official in-process plugin manager the host mounts, because the CLI refuses to write that profile at all.@linxin666/dsh-web-ui-all and converts its update action into a transactional migration to @linxin666/dsh-web-all; the gateway removes the legacy package, installs the current package at an exact version, restores the legacy layer position, and verifies --dump-config before reporting success.dsh.engines.dsh with a top-level engines.dsh fallback), evaluate it as a node-semver range with the call the official boot gate makes — semver.satisfies(host, range, { includePrerelease: true }) — so || alternatives, ^ and ~, whitespace-separated comparators and x-ranges decide here exactly as they decide at boot, and show the requirement beside the update action. A requirement the running DSH does not satisfy disables the action and says to upgrade DSH. A requirement the host could not verify — an unreadable running version, or a declaration that is not a range node-semver parses — is a different verdict with its own copy: it names no cause and never sends the user to upgrade a DSH that was never compared. Neither verdict lets the update run, and the host update route answers 412 before starting any CLI job in both cases.--dump-config preflight; a conflicting or failing install is rolled back through the official remove path and the existing plugins are never touched.dsh plugin --profile web add @linxin666/dsh-client-ui-plugin-managergit clone https://github.com/zhu1090093659/dsh-web.git
cd dsh-web
pnpm install && pnpm -r build
dsh plugin --profile web add link:$(pwd)/packages/dsh-plugin-managerRestart dsh web; the update block appears on a bundle's page inside the official Plugins panel.
The package carries no configuration namespace. Update writes apply at the next restart.
The browser half provides the shared dual-channel face as the cordis service pluginManager, so sibling client plugins can drive and observe plugin management without re-implementing the channel detection. Consume it with ctx.inject(['pluginManager'], cb) and read ctx.pluginManager:
isLoopback: boolean — whether this browser has loopback authority to use the host routes.list(): Promise — read the installed snapshot.install(spec): Promise — install one plugin from an npm spec or git URL.uninstall(id): Promise — remove one plugin.status(): Promise — read the current install/update progress.failures(): Promise — read the recorded plugin boot-failure ring (plugin id, message, stack, install path); runtimes without a ring answer an empty snapshot.setEnabled(id, enabled): Promise — flip one plugin's next-start enablement through the active channel (official installer RPC or gateway patch disabled row); takes effect after the host restart.onChange(cb): () => void — subscribe to successful mutations; fires after install(), update(), uninstall(), or setEnabled() resolves, and returns the unsubscribe function.The contract source of truth is src/core/service.ts (PluginManagerService). The service is provided for the plugin's lifetime and disappears when the plugin is unloaded. The service and the update block share one face, so onChange subscribers observe the same mutations.
dsh from the host process PATH first, then from node_modules/.bin project roots above the running host entry, then from the running host package's own lib/bin.js, and finally from the packaged Desktop install's own CLI launcher under the Electron resource directory (runtime/cli/bin/dsh.cmd on Windows, runtime/cli/bin/dsh elsewhere) — the last one covers a packaged Desktop, where the host runs from inside app.asar, the private host package ships no lib/bin.js, every .bin shim directory is stripped from the installer, and the launcher puts only the bundled node on the host's PATH. A Node-script CLI (an npm or homebrew shim, a .bin symlink, a lib/bin.js) runs under an interpreter resolved without PATH: the node installed beside the CLI when the installation ships one, otherwise the host's own interpreter. The CLI's own directory goes first on the child PATH, so the dsh plugin run finds the pnpm installed beside it. If no source contains the CLI, CLI-backed writes remain unavailable — an application-owned profile writes through the official in-process manager and needs no binary, so its install, update and removal routes never demand one. CLI output is decoded as bytes in one pass, so a Windows console's code page (CP936/GBK) renders as readable text instead of replacement characters. Installs of git sources can take minutes and run as background jobs. Gateway updates apply only to npm registry sources, resolve the latest version on the host, and succeed only after the same installed package reports that exact version.--profile desktop outright, and the launcher hands that manager its bundled package-manager invocation. The job, its status polling and its verification are identical to the CLI path — an install must add a dependency the profile did not carry, a removal must drop one, and an update must still report the version the route resolved — while the CLI-specific guards (duplicate-mount stripping, insert-row and boot preflight) stay with the CLI writer, because there the official manager validates and applies the bundle itself. The manager's own verdict is read before the profile is: it resolves a refused run with application: 'failed' instead of rejecting, so the job reports that run's real reason (the pnpm diagnostic, or the packages an incompatible-version refusal names) rather than a no-op after an unchanged profile. Since 0.2.1-rc.2 that manager also refuses to install a dependency the profile already carries while the host mounts hmr — every profile host — so an update there runs as its two phases instead: remove the installed bundle, then install the target spec through the same manager, and a second phase that fails reinstalls the spec the profile carried before the run, so a failed update leaves a working plugin rather than an empty slot. The single call stays first, so an upstream in-place update would need no change here. Every other runtime keeps the CLI as the single writer of installs, updates and removals.dsh.engines.dsh update unchecked, and official-installer runtimes (DSHCode and the checkout web) are not gated here because their updates go through the official installer. The running DSH version is read from the installation's own @deepseek-ai/dsh manifest, which the launcher publishes as the profile's install anchor — no subprocess, no PATH, and therefore an answer on a packaged Desktop host — with dsh --version kept as the fallback for a runtime that publishes no anchor. A requirement the host cannot verify at all is reported as unverified rather than as a version below the minimum: both hold the update back, but only the second sends the user to upgrade DSH.disabled rows decide (the aggregate's inactive-by-default families), and rows no layer mentions are enabled. Enabling a row the bundle disables writes an explicit disabled: false override, because deleting the user row would only restore the bundle default; the runtime's loader honors these rows at the next start, but this path is less exercised than the official desktop writer's.GET /api/plugin-manager/restart reads the plan (no side effects, so a prefetch or a typed URL can never stop a host) and POST carries it out; every other method is refused with 405. The toolbar reads the plan first, so the confirmation names the same consequence the host will produce. On a terminal launch (dsh web) the host starts a detached helper that waits for the outgoing process to exit and then re-executes the same command line, so the port is free before the replacement binds it, and the replacement's output goes to $DSH_HOME/logs/plugin-manager-restart.log. Under the packaged Desktop app the plugin cannot relaunch anything — Electron owns the process tree, a detached respawn would fight the shell for the same port, and the official shell answers a host exit with fail("dsh desktop host stopped") and an error page, because it exposes no way to ask the shell to restart — so the host exits there only when the shell exports DSH_SHELL_RESTARTS_HOST=1, its own declaration that an exiting host is brought back. Without that guarantee the restart panel states that this process cannot restart itself, names the manual way out (quit DeepSeek Harness and open it again), and offers no confirmation button: that path would not restart anything. A launch with no terminal (a supervisor, an editor task) is left running and the toolbar says to restart manually. Without a restart, updated plugin code is loaded at the next start.disabled away: the loader's duplicate check has no disabled exemption).--dump-config) catches composition failures, and the static insert check catches insert rows naming packages that resolve nowhere; runtime import/apply failures still surface only at the next real start.dsh.profile.bundles after any install/remove — including packages the composition already mounts through a patch row (a bundle that mounts an external plugin by row), which would double-mount and fail the next boot (duplicate prefix route). After every successful CLI mutation the gateway strips exactly the newly added, already-row-mounted bundles entries back out (the manifest write goes through backup + tmp + atomic rename), reports one notice per stripped entry on the job result, and leaves normal installs' bundles entries — and every entry the user had before — untouched.sec-fetch-site), the same authority the official installer channels enforce. There is no browser-reachable path from a remote origin; rejected requests receive HTTP 403 with { ok: false, error: "forbidden: loopback-only" }.execPath + argv, dropping inspector flags), it refuses to relaunch anything it cannot identify as a terminal launch, and under the packaged Desktop app it stops this process only when the shell declares DSH_SHELL_RESTARTS_HOST=1. Any local process can therefore drive plugin installs and removals, and npm installs run package install scripts — treat the gateway as local code execution by design and never expose it beyond loopback.node.exe plus the package bin.js; packaged Desktop shims have no adjacent npm layout, so they run through cmd.exe /d /s /c with a pre-quoted, verbatim argument envelope. That command processor is addressed by absolute path (ComSpec, otherwise %SystemRoot%\System32\cmd.exe) instead of by bare name, so a host whose PATH omits the system directories still runs the shim; a spawn that still fails settles as a failed job carrying the reason instead of taking the dsh host process down. Desktop profile discovery reads the packaged launcher's profile environment value or persisted profile selection, and application-owned profile installs, updates and removals run in the host half through the mounted official pluginManager service — the same writer that profile's official Plugins page drives through its own RPC face — under the same loopback fence and user click.done when the new dependency actually landed in the profile (and a removal only when it is gone); a success exit code alone is never trusted.404 before writing, so a panel row left behind by an uninstall cannot create an orphan disabled override.disabled rows for a shared id (such rows cannot stop the loader's duplicate check and would flag the existing owner).--dump-config) composes patch layers without importing entries: it catches composition failures, not import-time failures, which still surface at the first real boot.--profile, then the launched profile the Host publishes on profileContext, then the profile directory the packaged launcher carries in its own argv (Electron strips exec switches, so the desktop host sees its profile directory positionally), then DSH_PROFILE, then the web subcommand, then the packaged app's persisted selection. The published profile outranks the variable because the packaged Desktop client exports neither and a global DSH_PROFILE workaround can name a profile the running Host never reads.cordis.patch.yml — the patch path is a write target. Patch writes go through a backup copy plus tmp-write + atomic rename (cordis.patch.yml.bak-plugin-manager).dsh.profile.bundles, under the same backup + tmp-write + atomic-rename discipline as patch writes (package.json.bak-plugin-manager). It removes only entries the CLI just added that duplicate an existing patch-row mount, and a failed safeguard write fails the job visibly rather than silently leaving a boot-breaking state.The browser half sends one anonymous install heartbeat per UTC day to dsh-market.com: a random localStorage id plus this package's name, nothing else. The server stores only a salted hash of that id, never IP addresses, and exposes aggregate counts only. See docs/telemetry.md for the full contract.
BSD-3-Clause.