dsh-web
zhu1090093659
DeepSeek Harness (DSH) Web 插件聚合生态 · 万物皆插件,通过创意工坊分发||DeepSeek Harness (DSH) Web Plugin Aggregation Ecosystem · Everything is a plugin, distributed via the Creative Workshop
PROJECT TOPICS
INSTALL REFERENCE
dsh plugin --profile web add github:lilcandi/dsh-any-background-plus
该命令指向仓库当前默认分支;尚无绑定当前 commit 的完整验证结果。
PROJECT README
English | 中文
This is a fork, and this half of it is mine. Modified from Tkingxiao/dsh-any-background (MIT), which built the colour wheel, the per-surface opacity/blur system, the generated backgrounds and the rotation engine. Everything from v0.4.0 onward is this fork's own work — see What this fork adds below. Upstream: github.com/Tkingxiao/dsh-any-background.
A DeepSeek Harness appearance plugin: custom theme colour, background wallpaper (image / algorithmically generated), and fine-grained per-surface opacity & blur controls — plus this fork's dual-lane wallpaper, two-folder rotation and a floating rotate-now orb with a live countdown ring. Compatible with DSH 0.1.5-rc.2 ~ 0.2.0-rc.1.
These are the features I built on top of upstream. They are the reason this fork exists.
dual toggle paints two different pictures of the same rotation at once, one down each side of the viewport, clear of the centre column. The motivation is geometric rather than decorative: the wallpaper is painted behind the whole app while the host's conversation column sits on top of the middle, so a picture whose subject is dead centre has that subject exactly where it cannot be seen. Two lanes hug the edges instead, and the picture that used to be half-hidden becomes the one you actually look at. Off by default.pickRotationIndex() (same shuffle/order semantics, same "never repeat the current picture"), and the right index is taken from nextLaneIndex(), which steps forward from the left one. Stepping rather than drawing again is deliberate — the shuffle RNG is unseeded, so two independent draws would occasionally collide and paint the same picture twice, which is precisely what dual mode exists to avoid. In order mode the pair is simply adjacent pictures. The pair is persisted as laneItems, so it survives a reload and cannot drift apart.current, the right one is recovered by looking up the name the right lane is already showing in its own listing. That matters as soon as the two directories differ in length, because a single shared index would walk the shorter listing off its end. Only once both directories exist does the wall change; picking just one stores it without previewing, so the wall is never left half split with a stale lane on the other side. The single-folder mode is still there — the pair is a third source, not a replacement.wpLeftEl and wpRightEl are peer position:fixed; width:50%; z-index:-1 elements, one pinned to each edge, and both are given the same pixel width — Math.floor(innerWidth * 0.5) — so an odd viewport width cannot leave one lane a pixel wider than the other. The overhang that lets a picture reach the seam is applied only in center mode: in fit the picture is contained inside its lane by construction, so an overhang there would make the two lanes overlap at the seam and whichever painted second would win the strip — one picture visibly encroaching on the other, which is the asymmetry this mode exists to prevent.minutes cadence — that is the only interval whose remaining time is knowable in the browser (reload is due on every read, daily/weekly are settled by the node half as the page loads, and a disabled rotation never falls due at all), so for those the button is drawn alone rather than showing a ring that would always read "full" and be a lie. Clicking advances the rotation, and because the advance stamps lastRotate the ring refills by itself. It is mounted as its own React root that portals to <body>, so neither the settings dialog's scroll column nor a host transform can clip it. Drag it and it stays where you put it (the position is remembered per window, re-clamped into the viewport on every resize); right-click it to send it back to the corner. It is drawn with the host's own floating-button recipe rather than a lookalike — --dsw-alias-button-floating-fill / -hover, --dsw-elevation-panel, --dsw-elevation-stroke-color: var(--dsw-alias-border-l3) and --dsw-alias-label-primary, the same tokens the host's scroll-to-bottom control resolves against, so it follows the theme colour and the light/dark switch by construction. Dropping it onto <html> instead — a sibling of <body>, so on the far side of every design token — would have left it a transparent outline, which is measured, not assumed.BackgroundType no longer carries 'video', the rotation no longer carries media / videoFolder / videoName / videoVolume / videoAlign / ended, and the whole src/client/utils/video.ts module is removed. A stored config that still names a video rotation falls back to images on normalize.peerDependencies before importing a single module, so the seven @deepseek-ai/dsh-* peers, engines.dsh and dsh.compatibility.dshReleases all name nine releases through 0.2.0-rc.1, and the release table gained a matching row. Diffing the 0.2.0-rc.1 packages showed ThemeRuntime and SidebarRightTabRegistry unchanged, so this build needs no new adapter folder — it joins the folder whose facts held.
Dual-lane wallpaper · two different pictures of one rotation, split at the window's absolute centre, clear of the conversation column
The background page of the theme settings · source, folder picker, layout mode and wallpaper rotation
Theme colour and per-surface opacity · PS-style wheel, smart extraction, per-surface sliders
Rotate-now orb · the host's own floating-button styling, draggable (right-click to reset), with a countdown ring on the minutes cadence
backdrop-filter blur (0–60 px) per surface, including a real backdrop on the composer, the Cordis panel and popover surfaces via stable host selectors.code highlight chips and produced chips share one opacity + blur slider. The opacity is the alpha of each surface's own background color (no second color stacked on top of the original), and the blur frosts that same layer so the wallpaper shows through the content.panelOpacity / blurs.panel), two identities: without dsh-better-sidebar it reads "右方侧边栏" (Right sidebar) and drives the official right Sidebar's surface tokens and frosted blur (works on 0.1.5-rc.2 through 0.1.7); with dsh-better-sidebar installed it reads "bettersidebar" and takes over that plugin's bottom workbench panel (the official sidebar keeps responding too). The row is always visible.conversation.session.header* slot anchors and tags the open popover at runtime instead of relying on class shapes.sidebarRightTabs + the sidebar.right.pane.tab keyed seat); with dsh-better-sidebar installed, the page registers in that plugin's sidebar instead and the official guide card withdraws itself, so the two never duplicate. Settings panel, official sidebar and better-sidebar all share one page implementation and one state store — a change in any of them shows up everywhere. The shell adapts to the panel width, and a narrow panel tightens padding and falls back to a single column. On a host without the right Sidebar the registration silently never happens.dsh-any-theme.json (config + wallpaper, embedded as data URLs) and import to restore it anywhere.@font-face; toggle it off or remove it at any time. Fonts stream as raw bytes and persist in the plugin data dir; code blocks keep their monospace stack.-webkit-text-stroke: width 0–4 px (0 = off) and a color of auto-contrast / gray / black / white / accent / custom. Code blocks, inline code, icons and the host's background-clip: text shimmer chrome (the "深度求索中" turn-status line and the turn-process rows) are exempted automatically, so multi-color syntax never smears and gradient text is never flattened into a stroke-coloured blob.Auto both the surface and font directions follow the accent's lightness (dark pick → light fonts, light pick → dark fonts), falling back to the wallpaper's perceived brightness when no color is picked.~/.dsh/.dsh-any-background-data/, not localStorage.{x, y} to localStorage under dab-orb-pos — not to the plugin config. That split is deliberate: the position belongs to this window on this screen, not to the theme, and putting it in the config would drag the Node half into a purely presentational concern and make every drag need a host restart to take effect. The corner is the default, and it stays the default in the CSS — the stored position is only consulted when one exists, and right-clicking the orb deletes it and drops the button back to the corner rather than to a pinned approximation of it. A stored position is re-clamped after every resize (the button keeps its last known size in a ref, so the clamp has something to measure even on the first frame), which is what stops a position saved on a wide monitor from parking the orb off-screen on a narrow one.ChatView.module.css's own EvIC1a_toBottom scroll-to-bottom control — --dsw-alias-button-floating-fill with -hover behind it, --dsw-elevation-panel as the shadow with --dsw-elevation-stroke-color: var(--dsw-alias-border-l3) supplying its hairline, --dsw-alias-label-primary for the glyph, border-radius: 100px with corner-shape: round, 34px square. The old orb was a hand-picked dark rounded square; the new one is drawn from the same tokens the host's own floating controls resolve against, so it follows the theme colour, the light/dark switch and the elevation recipe without any of them being restated. Measured on the live page rather than assumed: computed border-radius: 100px, corner-shape: superellipse(1), cursor: grab, and a glyph colour that matches --dsw-alias-label-primary exactly.@keyframes dab-orb-in was applied with animation-fill-mode: both, which keeps the last keyframe applied after the animation ends — so the final transform: none went on pinning the button's transform forever and neither :hover nor :active could scale it. The fill mode is backwards now, which is what you want from an entrance animation: it holds the starting frame before the animation begins and then releases the property completely..dab-root, and the orb is portalled out of that subtree onto <body>, so the one element that animates on every rotation and repaints once a second was exempt from the preference. Its own selector is spelled out in that media block now.Portal mounts to <body>, and this was a real bug rather than a hygiene fix: every --dsw-alias-* token is declared only on body / body[data-ds-dark-theme] (209 declaration sites, no :root, no html), so a portal host appended to document.documentElement — <body>'s sibling, not its ancestor — inherited none of them and fell back to nothing: the toast's background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-layer-2)) resolved to no value at all and the toast rendered fully transparent, its border black, its z-index: 10001 intact. Measured by putting the real .dab-toast class in a replica portal host under each parent: under <body> the same class computes background: rgba(92, 95, 69, 0.5) and border: 1px solid rgba(117, 119, 95, 0.22); under <html> it computes background: rgba(0, 0, 0, 0) with both token lookups empty. The orb was portalled the same way and was equally token-blind — the only reason it looked styled is that the default styling was rebuilt with hard-coded values in v0.4.3.立即切换壁纸 · 还剩 1:21 · 拖动可移动,右键归位 — the existing countdown hint with the drag/right-click line appended, in both locales.ensureUiCss() was otherwise called from exactly one place: ThemeSection, which only mounts when the settings dialog opens. The orb mounts at plugin start, so the button was in the DOM with no position:fixed on it and fell back to browser-default styling inside the portal host. mountRotateOrb now calls ensureUiCss() itself before building the button; the call is idempotent, so opening the settings page afterwards costs one string comparison.rotateNow off storeInstance.actions, but that object carries only the three sync actions the defineStore block declares — rotateNow is added later by buildFace for the settings page, a different object entirely. The lookup returned undefined and the guard returned silently, so a missing function was indistinguishable from a dead button. The settings page's own 「立即切换」 button worked throughout, which is what proved the advance logic itself was sound and localised the fault to the floating button's action source. rotateOnceNow is now passed in as a required parameter, which makes that state unrepresentable rather than merely fixed, and a .catch reports any future rejection through console.error instead of swallowing it.BoundActions.rotateNow? was added while chasing the first theory (that the bound actions were stale) and was wrong — it invited exactly the silent lookup that caused the second defect. It is deleted, along with the comment that claimed the store and the settings slot hand out the same action object. They do not.BackgroundType no longer carries 'video', the rotation no longer carries media / videoFolder / videoName / videoVolume / videoAlign / ended, and the whole src/client/utils/video.ts module is removed. The wallpaper is an image, a mesh, a shader or a pattern again, which is what the previous three releases had grown around. A stored config that still names a video rotation falls back to images on normalize: the whitelist dropped the token, so the switch reads as "off" rather than persisting a mode that cannot draw.dual toggle on the rotation panel paints two different pictures of the same rotation at once, one down each side of the viewport — left and right, clear of the centre column. The motivation is geometric, not decorative: the wallpaper is painted behind the whole app while the host's conversation column sits on top of the middle, so a picture whose subject is dead centre has that subject exactly where it cannot be seen. Two lanes hug the edges instead, and the picture that used to be half-hidden is now the one you actually look at.pickRotationPair() draws the left index with the existing pickRotationIndex() (same shuffle/order semantics, same "never repeat the current picture") and then takes the right index from nextLaneIndex(), which steps forward from the left one. Stepping rather than drawing again is deliberate: the shuffle RNG is unseeded, so two independent draws would occasionally collide and paint the same picture twice, which is precisely what dual mode exists to avoid. In order mode the pair is simply adjacent pictures. The selection is persisted as laneItems, the two names in the order the lanes were painted, so the pair survives a reload and cannot drift apart.~/.dsh/.dsh-any-background-data/wallpaper-right.jpg) and its own serve route (/dsh-any-background/wallpaper-right) beside the existing wallpaper.jpg, rather than framing one picture twice. Each pane is drawn in its own box with its own arithmetic — in particular the containment maths for the right lane runs against half the viewport width, because it is a picture in its own half, not a half of a picture: containing against the full window and then clipping would eat the right-hand side of the subject, which is the very thing this feature is for. A single-candidate pool, or dual switched off, reports no right URL at all, which is the signal the browser half uses to drop the second pane instead of leaving a stale picture up.wpLeftEl and wpRightEl are peer position:fixed; width:50%; z-index:-1 elements, one pinned to each edge, and both are given the same pixel width — Math.floor(innerWidth * 0.5) — so an odd viewport width cannot leave one lane a pixel wider than the other. The full-viewport layer is kept but hidden while dual mode is on, and only because the effects pass still writes blur and opacity through it. The overhang that lets a picture reach the seam is applied only in center mode, where a picture wider than its lane genuinely needs to: in fit the picture is contained inside its lane by construction, so an overhang there would make the two lanes overlap at the seam and whichever painted second would win the strip — one picture visibly encroaching on the other, which is the asymmetry this mode exists to prevent.wpLeftEl and wpRightEl are peers in the same stacking context, each pinned to its own edge, and the original full-viewport layer is set visibility:hidden while dual mode is on rather than being cut in two — that keeps the single-picture framing path, its edge feathering and its drag downsampling intact for the one thing that still needs it, which is the effects pass writing blur and opacity through the hidden layer. Turning dual off, adopting a manually chosen single picture, switching to a generated background, or tearing the wall down all remove both lanes, so no stale picture can survive a mode change.current, and the right one is recovered by looking up the name the right lane is already showing in its own listing. That matters as soon as the two directories differ in length, because a single shared index would walk the shorter listing off its end. Only once both directories exist does the wall change; picking just one stores it without previewing, so the wall is never left half split with a stale lane on the other side. The single-folder mode is still there — the pair is a third source, not a replacement.ended event, relayed from the video layer to the rotation, which makes it mutually exclusive with looping: loop has to be off under this cadence, because a looping element never fires ended and the setting would silently degrade into "loop the first clip forever". Having no clock also means it reports "not due" on every timestamp comparison — otherwise a single page load would advance straight past the first clip. Image rotations never offer it (there is no playback to finish), and if a video rotation that had selected it loses its folder and falls back to images, the cadence falls back to daily too rather than persisting a trigger that can never fire.no videos) rather than adopting a rotation that would never play. Images and videos deliberately share one engine and one switch: choosing a video folder sets media: 'video', choosing an image folder or the pool sets it back to 'image', so there is exactly one cadence, one shuffle/order setting and one timestamp to reason about. A video rotation whose folder is missing (the drive unplugged, the directory deleted) falls back to images on normalize rather than persisting a switch that looks on and silently plays nothing.linear-gradient derived from the live accent colour (the same hsl() the theme uses, second stop rotated 40° and darkened 12%), under the same 30 px blur and the same opacity slider, and it appears only for the modes that actually leave a margin (Fit and Center). Zero decoding, and it tracks the theme colour when that changes.resolveServedVideo() sits between the two video sources: the /video route now resolves the rotation's videoFolder + videoName first and only then falls back to the uploaded single slot, so an empty rotation folder never blacks out a wallpaper that was working. The persisted folder is absolute and the persisted name is a bare filename that safeVideoName() (extension whitelist, / and \ rejected) already keeps separator-free, so the join cannot escape the chosen directory — that whitelist is the whole containment boundary.minutes5 was a fixed 5-minute cadence; the rotation now stores interval: 'minutes' plus an intervalMinutes from 1 to 1440, and a stored minutes5 is migrated to minutes/5 on load so existing configs keep their behaviour. The page's 30 s poll is unchanged, so the worst-case lateness stays 30 s even at a one-minute cadence.verify-host.mjs now checks whether the running process actually carries the feature before probing it — because the Node half is imported at boot, a host started before the current build answers the new endpoints with unknown endpoint, and reporting three bare failures for code that is correct on disk is worse than useless. The probe now says which build the host booted from and names the remedy.src/host-compat/ (the Node half reads the release out of the very @deepseek-ai/dsh/package.json the process was composed from — ctx.profileContext.installAnchor — falling back to the manifest beside the launcher's homes/<ver> and then to the directory name; the verdict travels in the read RPC payload), with src/client/host-compat/ receiving it on the client, broadcasting changes, and re-cutting the static stylesheet the moment the verdict lands. Version logic that used to be spread across src/client/host.ts and its call sites is consolidated there; that module is gone.src/client/host-compat/versions/, the v0-1-5-rc-2-3 / v0-1-6-alpha-1-2 / v0-1-7-alpha-1-2-rc-1 / unknown folders each describe that version's panel mechanics (which layer carries the promotion and the blur) and its header slot keys, and versions/registry.ts is the plugin's only release → code mapping. Base code only asks the adapter questions (who owns the guide surface, which slot selectors apply) and never compares version strings — supporting a new host means adding a folder and registering it, leaving base code untouched.0.1.5 but "patch line + prerelease channel" — 0.1.5-rc, 0.1.6-alpha, 0.1.7-alpha. Each folder's mechanics were checked tag by tag against a specific channel, so recording 0.1.5-rc.3 and a hypothetical 0.1.5-beta.1 under one key would let an unverified shape inherit a verified conclusion. Folders and adapter ids now name the exact tags behind them (v0-1-6-alpha-1-2 reporting 0.1.6-alpha.1/alpha.2), so how far a table reaches is readable without opening it — and where two builds on one line genuinely differ (leading header slot and the plugin-manager page only exist from 0.1.6-alpha.2), the folder carries that prerelease gate itself.SUPPORTED_RELEASES table in src/host-compat/channel.ts (0.1.5-rc.2 / 0.1.6-alpha.2 / 0.1.7-alpha.2 / 0.1.7-rc.1, oldest first — one row per build whose facts were checked, so one line may carry two) is matched by PATCH LINE first: a machine on 0.1.6-alpha.4 gets the 0.1.6-alpha adapter even though verification stopped at alpha.2, because the number after the channel is only a build counter on that line — and 0.1.6-beta.1 or a future channel-less 0.1.6 count as the same line too. Only when no verified line covers the patch number does it clamp: newer than the newest → that newest adapter, older than the oldest → the oldest, strictly between two → the LOWER one, since an adapter may only claim what it was verified for. A clamped verdict logs one line saying the host is outside the verified range and which adapter is standing in, so a report against a new host build starts from that fact. Falling into unknown — where the DOM arbitrates — is left for the case where no release parses at all (a plain ~/.dsh install, for instance).0.1.7-rc.1 joins the verified range without an adapter change: diffed against 0.1.7-alpha.2, every anchor this plugin matches is still emitted where it was, so the 0.1.7 folder only gained that build in its name (v0-1-7-alpha-1-2-rc-1) plus a SUPPORTED_RELEASES row — which turns an rc.1 host's verdict from line into exact. What did change is how the host reads the manifest: from rc.1 the profile composition checks each plugin's @deepseek-ai/dsh* peerDependencies against the running release and disables its row before importing a single module when a declared range does not match, the only way past that being an exact-version exemption the plugin manager stores in the profile's own compatibility.json. engines.dsh remains declarative, so the peer list is what decides whether the plugin runs — hence || 0.1.7-rc.1 on all seven @deepseek-ai/dsh-* peers.:has() dependency once the host is known: a resolved release emits its own targeted branches, ungated. That also fixes a latent problem: on an engine without :has() the previous gate dropped the panel promotion entirely on 0.1.5 / 0.1.6. An unresolvable release still falls into unknown and lets the DOM shape arbitrate (:has() dual arms) instead of guessing a version.wallpaper.ts; it is now composed once and shared by the opacity rule and the outline rule (the latter deliberately keeps the bare tag without [role], with the reason documented in place). Slot keys come from the adapter, so the leading slot unique to 0.1.6-alpha.2 no longer leaks into base code.Settings → Plugins renders each group's plugins in a ul that has no surface of its own, so with the settings surfaces faded the whole table floated straight on the wallpaper. Each group's list now gets what the composer capsule gets — a rounded frosted block on an ::before underlay, painted from the settings-interface opacity and frosted by the settings blur (the page is a sibling of the settings dialog on 0.1.7, so reading the dialog's own token re-scope would have left it on the homepage alpha). It is contributed only by the adapters whose release ships that page, so the selector never rides a host that cannot match it.background: transparent, and the host's style rewrite during the sidebar open/close animation dropped it — the frame snapped back to an opaque layer, hiding the wallpaper and flattening every frost above it, because a backdrop-filter with nothing behind it has nothing to blur. The clear is a class rule with !important now, which the host's style writes cannot reach (measured on the live frame: rgb(200,207,218) opaque → rgba(0,0,0,0) with the class, opaque again without it).<div class="dab-part-underlay">) spread over the whole window. The fallback can now only refine a conversation that already exists: with none of [data-chat-flow] / [data-conversation-scroll] / [data-composer-seat] / [data-conversation-composer-overlay] present in the center column it no longer guesses, and candidates inside the settings dialog are still rejected.dab-part-blur{isolation:isolate}), so that layer never created a stacking context and the z-index:-1 frost underlay fell into the page-level one — its sampling region and its painted position both stopped matching the surface (measured live: the host element's computed isolation was auto). With the dot restored, the frost stays inside its own surface. This defect predates v0.3.0.ctx.sidebarRightTabs are not a new-host feature (0.1.5-rc.2 already ships them), and dockkit itself predates 0.1.7 — only its host / empty attributes are 0.1.7 markers.[data-dockkit-host="dock"] / [data-dockkit-empty]) carry the slide transform, and the panel no longer paints its own background. The plugin follows the new shape: the blur rides the sliding children so it travels with the sidebar instead of staying pinned, and the surface tokens are re-scoped where the panel actually renders. The previous unconditional position:fixed promotion was removed — on 0.1.7 it detached the panel from the animated track.[data-dockkit-host] only exists from 0.1.7, so the child-based blur selector matched nothing on 0.1.6 (which slides the panel itself). A second, :has()-gated arm now frosts the panel wrapper there, and the pre-0.1.7 promotion is re-applied only where it is needed.-webkit-text-stroke is inherited, so the conversation-frame rule reached the background-clip: text activity chrome — the "深度求索中" turn-status line (0.1.5/0.1.6) and the turn-process/shimmer rows (0.1.7) turned into a flat stroke-coloured blob. They are now explicitly exempt, matched by [role="status"], [data-turn-process] and the TextShimmer marker.<body> (severed from the header) and 0.1.7 moved open-in-app to a portal and made the session-row menu a dynamic slot, a runtime tagger watches the stable conversation.session.header* slot anchors and marks the open popover. The exempt confirm dialogs and session-row menu are pinned opaque with real color literals — no self-referencing var() fallback, which is a CSS cycle that would render them fully transparent.window.__DSH_BOOT__.version is a module-table tag, not a release), so the release plus a generation bucket is resolved on the Node half from the launcher's on-disk layout and handed to the client through the read RPC payload. Feature gates use it; when it cannot be determined they fall back to capability probing rather than guessing.ctx.sidebarRightTabs plus the keyed sidebar.right.pane.tab body seat), opening the same five pages as the settings panel. Active without dsh-better-sidebar; when that plugin is present, its own "Theme" page takes over and the official guide card withdraws itself. Registration waits on the service at runtime — hosts without the Sidebar registry API skip it silently, so older hosts are unaffected.panelOpacity / blurs.panel) reads "右方侧边栏" (Right sidebar) without better-sidebar — driving the official right Sidebar's surface tokens and frosted blur on 0.1.5-rc.2 through 0.1.7 — and "bettersidebar" with it. The row is now always visible instead of hiding when better-sidebar is absent.[data-sidebar-right-panel]: on every host generation that is the official right Sidebar's stable marker (present whenever a session is open), so counting it pinned the "bettersidebar" verdict to true forever.0.1.5-rc.2, 0.1.5-rc.3, 0.1.6-alpha.1, 0.1.6-alpha.2 and 0.1.7-alpha.1; peerDependencies widened to span every generation of the client packages; @deepseek-ai/dsh-home-paths stays at the lockfile-consistent ^0.1.0-rc.6 (build-time only — the host injects its own copy at runtime).dsh plugin --profile web add github:lilcandi/dsh-any-background-plus
Then launch:
dsh web
The plugin appears as a "Theme" section in Settings.
npx @deepseek-ai/dsh plugin --profile web add github:lilcandi/dsh-any-background-plus
npx @deepseek-ai/dsh web
The lib/ directory is committed, so installs need no build step. To rebuild after editing src/:
git clone https://github.com/lilcandi/dsh-any-background-plus.git
cd dsh-any-background-plus
pnpm install
pnpm run bundle
pnpm dsh plugin --profile web add .
pnpm dsh web
dsh web 0.1.5-rc.2 ~ 0.2.0-rc.1 — The range covers all nine published releases (0.1.6-alpha.2 and 0.1.7-alpha.1 / 0.1.7-alpha.2 verified hands-on, 0.1.7-rc.1 and 0.2.0-rc.1 verified by diffing them against the build before); engines.dsh, the @deepseek-ai/dsh-* peerDependencies and dsh.compatibility.dshReleases list them explicitly — from 0.1.7-rc.1 the host itself enforces the peer list, so naming a release there is what makes the plugin load, not documentation. The host release is resolved on the Node half at runtime, and features that depend on a specific host release channel (the right Sidebar's panel blur, the official Sidebar's "Theme" card) enable themselves only where the corresponding host structure exists; everything else behaves identically across the range.ctx.profileContext.installAnchor, with the launcher's on-disk layout behind it — the client context exposes no version) and, once handed down through the read RPC, is routed only by the front-layer adapter — src/host-compat/ detects and buckets channels, while src/client/host-compat/versions/ holds one folder per release (v0-1-5-rc-2-3 / v0-1-6-alpha-1-2 / v0-1-7-alpha-1-2-rc-1 / unknown), each describing that version's panel mechanics and header slot keys. Base code just asks the adapter questions (who owns the guide surface, which layer carries the blur) and never compares version strings. Another build on a verified patch line (0.1.6-alpha.4 against a table checked at alpha.2) keeps that line's adapter, and a patch line nothing was checked against clamps to the nearest one with a log line saying so. Only a release that will not parse at all falls into unknown and probes the DOM shape instead of guessing (:has() dual arms); supporting a new host means adding one folder and registering it.dsh is 0.1.5-rc.2, inside the supported range; the mobile UI shell is provided by dsh-web-mobile.package.json declares dsh.bundle.patch: ./cordis.patch.yml (a loader insert layer), the repository ships prebuilt runtime artifacts ready to use (lib/index.js, lib/invariant.js, lib/client.js), and there are no install scripts, no postinstall, no native binaries, and no build step at install time.<dsh home>/.dsh-any-background-data/ (config JSON, wallpaper, rotation pool, font) and touches nothing outside it; config writes are atomic (temp file + rename). These files live on the real disk, so they are outside generation restore — it neither captures nor rolls them back; deleting the directory is a full plugin reset.child_process, no native modules, no dynamically downloaded executables./dsh-any-background/{wallpaper,wallpaper-right,font} (GET/HEAD streaming) with matching */upload POST routes (100 MB cap) and the dedicated RPC channel /dsh-any-background under the local dsh web server; no extra listening ports.dsh web to load the client bundle; settings changes afterwards apply live and persist automatically. Updating the plugin requires a restart to pick up the new lib/client.js.pnpm run typecheck (full tsc check) and pnpm run bundle (tsdown emits lib/); no automated unit tests — behavior is verified manually.[data-sidebar-right-panel], [data-dsh-bottom-panel], …) and CSS token names; a host restyle of those layers can leave a slider ineffective for its surface (cosmetic only — nothing breaks). The version-specific half of those selectors lives inside its own version folder, so a host revision normally means editing that one file. -webkit-text-stroke may clip about 1px at the edge of some single-line ellipsis containers.MIT
CLASSIFICATION EVIDENCE
系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: theme、wallpaper。