dsh-plugin-mascot
Manifest validDeepSeek Harness mascot plugin - a clickable character sprite (Arknights' Closure / BanG Dream!'s Sengoku Yuno) showing token cache hits, context pressure and account balance.
dsh-plugin-mascot
English | 中文
A mascot for the DeepSeek Harness (DSH) Web GUI. It sits in the bottom-right corner. Click it and a panel opens with the session's token cost and cache performance.

The image above is lib/client.js running in Chromium with the built-in placeholder, i.e. a fresh clone before npm run fetch-art. With the official artwork installed it looks like docs/preview-official.png, which is generated locally and not committed.
What the panel shows
- Token cache-hit rate: cache reads over all billed input
- Token breakdown: uncached input, cache read, cache write, output, session total
- Context occupancy: current usage against the context window
- Account balance: fetched host-side, so the API key stays in the host process
- Music: the character's Siren Records album, or a 30-second official preview
- Character and look: 9 characters, 22 looks
Install
DSH Desktop loads plugins through the profile's cordis.patch.yml. cordis.yml is rewritten to [] on every boot; do not edit it.
The plugin has no runtime dependencies. Two ways in, pick one.
Option A: point at the file directly.
# ~/.dsh/profiles/desktop/cordis.patch.yml
- insert:
- id: mascot
name: 'file:///C:/Users/<you>/Desktop/kexier/dsh-plugin-mascot/lib/index.js'
The file: URL names a file, not the folder. A relative path also works and resolves against the patch file's directory.
Option B: install it as a profile dependency.
dsh plugin --profile desktop add "/absolute/path/to/dsh-plugin-mascot"
dsh plugin --profile desktop add link:/absolute/path/to/dsh-plugin-mascot
dsh plugin --profile desktop add "github:ZYAONS/dsh-plugin-mascot"
- insert:
- id: mascot
name: dsh-plugin-mascot
Add config to either form:
- insert:
- id: mascot
name: dsh-plugin-mascot
config:
baseUrl: 'https://api.deepseek.com'
cacheTtlMs: 30000
Restart DSH Desktop. The desktop composes its patch layers once per launch and does not watch the profile directory. Reloading the page is not enough. After the first mount, edits to lib/client.js hot-reload without another restart.
Console
Configure the plugin in a browser before installing anything:
https://zyaons.github.io/dsh-plugin-mascot/
Seven sections:
- 00 Live preview: the mascot, animated, wearing the look you picked
- 01 Plugin: plugin on/off, balance lookup on/off
- 02 Character: one card per character
- 03 Looks: tick which artwork may appear
- 04 Motion: bone rig, multi-frame looks
- 05 Output: a
cordis.patch.ymlto copy or download - 06 Install: five steps, including fetching artwork and restarting
The page is static, runs on GitHub Pages, and reads nothing from your machine. State lives in localStorage. ?theme= and ?character= deep-link both, and ?theme= also selects the character.
Served by the plugin at /dsh-mascot/console/ it also reads the artwork on your own machine.
Themes
10 page themes: a neutral one plus one per character.
- 中性
auto: silver, square corners - 可露希尔: blue, square
- 千石由乃: magenta, 14px
- 缪尔赛思: light green, 10px
- 丰川祥子: violet, 8px
- 谬因: deep green, 14px
- 予愿安洁莉娜: cyan, 10px
- 夕: teal, 10px
- 结城理: P3R blue, square
- 望: bone, square
Picking a character's theme selects that character, and picking a character selects its theme. The neutral theme belongs to no character; it is the only one that can disagree with the artwork, and it hides the music row.
Language
The console is English. The panel is Chinese. art/looks.json carries nameEn / roleEn / taglineEn beside name / role / tagline, and build-site.mjs copies the English set into the catalogue. check-site fails if a rendered record has no translation, or if Chinese reappears outside the rights notice.
Characters and looks
All artwork belongs to Hypergryph or Bushiroad. The repository ships the declaration, not the images.
npm run fetch-art # download, cut out where needed, re-index
npm run fetch-art -- --force # re-download regardless of hash
Nine characters, 22 looks:
- 可露希尔 Closure: chibi, portrait
- 千石由乃 Yuno: chibi in two drawn poses, anime, casual
- 缪尔赛思 Muelsyse: chibi, alt, portrait
- 丰川祥子 Sakiko: anime, casual, collab chibi
- 谬因 Miuyin: chibi, portrait
- 予愿安洁莉娜 Yuyuan: chibi, portrait
- 夕 Dusk: chibi, portrait
- 结城理 Makoto: portrait, collab chibi, P3R
- 望 Wang: chibi, portrait
Seven are Arknights; Yuno and Sakiko are BanG Dream!. Sakiko and Makoto are collab designs. art/looks.json is hand-written and art/index.json is generated; every entry carries source URLs and a sha256, checked on download.
To add a look, add an entry to looks in art/looks.json and run npm run fetch-art. No code changes.
scripts/art-sync.mjs measures each image's alpha bounding box in a browser, derives the framing for both seats (104×172 sprite, 38×50 portrait), and samples an accent colour. The results go into art/index.json, which the host half filters to what is on disk and serves to the browser.
npm run art:watch re-indexes whenever a file lands in art/.
A missing file falls back to a neutral placeholder: a dashed box with a generic glyph. It depicts no character.
Motion
Three layers. The first two are motion; the third is the browser playing a file.
Skeletal animation
A 2D bone and skinned-mesh renderer in WebGL2.
art-syncmeasures each image's silhouette profile as 32 horizontal bands. The client finds the neck from the band where the silhouette pinches.- Seven bones:
rootat the feet,spineat the hip,neckat the neck,armL/armRat the shoulders,foreL/foreRat the elbows. Children inherit parent transforms. - Vertex weights are distributed vertically, blended across a band at each joint. The arms are separate regions, masked to their own side of the centre line.
- The texture is uploaded with
UNPACK_PREMULTIPLY_ALPHA_WEBGL. - A click adds a decaying oscillation that ends within 1.6 s.
If WebGL2 is unavailable, a shader fails to compile, or the image taints the canvas, it renders a plain <img>. The rig can only add motion. The preview script prints rig: ON (canvas 140x232) to report which path ran.
Measured motion
The idle, greeting and walk come from the games' own Spine models, mirrored by Ark-Models and read with the official 3.8 runtime.
npm run measure:motion # all six animations
npm run measure:motion -- --model=4228_closur --character=closure
npm run measure:motion -- --model=4228_closur --moves
Arknights chibi have six animations: Default, Interact, Move, Relax, Sit, Sleep. All six are measured for all 8 characters that have a model; art/motion.json is keyed by character. Three are used:
Relaxas the idle: waist 0.4°, forearm 28.6°Interactas the greeting: waist 17.6°, forearm 79.5°Moveas the walk-in on a look change: IK feet ±89 units
Sit and Sleep carry almost no motion in either channel. Sleep moves three bones by at most 2.2 units, Sit's largest translation is an eyeball, and Default is a zero-length animation. They are measured and not inlined.
npm run motion:literal # regenerate the inlined block in lib/client.js
The generated block ships idle, greet, move and the blink per character. A character with no entry falls back to the default.
A character that declares gesture: "evoker" raises its right arm to its temple on click: the upper arm turns 140°, the forearm folds 120°.
Multi-frame animation
Yuno's Q-version is two drawn poses in one sheet. cutout.mjs splits them, crops each to its own bounding box, and gives both one shared scale. The browser cross-fades every 5.2 s. It stands down when the bone rig is on.
Any frame that is a GIF or animated WebP plays on its own.
Music
The panel plays the character's Siren Records album through a hotlinked official CDN. art/siren.json maps a character to the album for the event they came from; art/songs.json covers characters with a 30-second Apple Music preview and no Siren release.
npm run siren:sync # re-resolve albums and previews
npm run siren:sync -- --list
No audio file is stored. The host serves both maps through /api/looks.
Rooms
Each character's dock has a backdrop: a room the plugin draws from gradients, or official furniture art.
npm run fetch-rooms # download what is missing
npm run fetch-rooms -- --list # show the declaration
npm run fetch-rooms -- --force # re-download
Images go to art/room-<character id>.<ext>. The host finds them by filename, so a restart is all that is needed. art/rooms.json declares which set each one is and where it came from. Deleting art/room-* returns the character to the drawn room.
Usage
- Click the sprite to toggle the panel.
- The 角色 row switches character; the 形象 row switches that character's look. A 动态 badge marks a multi-frame look.
- The 动效 row toggles the bone rig. The music row's ▶ plays or stops the album.
刷新re-reads the balance. Click the backdrop or press Esc to close the panel.
The choice is stored in localStorage. With the system's reduce-motion preference set, all animation stops.
The pill under the sprite shows three figures. 余额 ¥128.42 is the account balance, fetched host-side; when the lookup fails it reads 点我 Token / 余额 instead. 命中 89.0% is this session's cache hit rate. 今天 1.2M is the tokens spent today on this machine.
The daily figure needs the host to keep books: a session that ended an hour ago takes its numbers with it. The host keeps .cache/usage-daily.json, keyed by session, and stores a high-water mark per session. The browser reports a cumulative total repeatedly, so re-reporting adds nothing; a total that goes backwards never subtracts. Midnight resets it.
Configuration
Every field has a default.
| Field | Default | Meaning |
|---|---|---|
routePrefix | /dsh-mascot | path prefix claimed on the host webserver |
baseUrl | https://api.deepseek.com | provider origin hosting the balance endpoint |
apiKeyRef | DEEPSEEK_API_KEY | credentials reference in ~/.dsh/.credentials.yaml |
cacheTtlMs | 60000 | freshness of a balance answer; failures are never cached |
timeoutMs | 10000 | upstream request budget |
allowForeignBaseUrl | false | allow the API key to go to a baseUrl that is not DeepSeek's own host |
artDir | the plugin's own art/ | directory holding the artwork |
Behind a proxy or self-hosted gateway, point baseUrl at it and set allowForeignBaseUrl: true. Without that flag the plugin refuses to send the key to any other host, and says which host it refused. The gateway must implement GET /user/balance.
Layout
dsh-plugin-mascot/
├── lib/
│ ├── index.js host half: the /dsh-mascot routes
│ └── client.js browser half: sprite, panel, themes, renderer, music
├── art/
│ ├── looks.json characters, themes, urls, hashes
│ ├── index.json generated by art-sync
│ ├── motion.json generated by measure-motion, keyed by character
│ ├── eyes.json blink boxes and the blinks' ramp
│ ├── rooms.json backdrop declarations
│ ├── siren.json character to Siren Records album
│ └── songs.json character to official preview
└── scripts/
├── fetch-art.mjs download, cut out, hand off to art-sync
├── cutout.mjs key out a background; split a multi-pose sheet
├── art-sync.mjs measure boxes and accents, write index.json; --watch
├── measure-motion.mjs read a Spine model's animations
├── motion-literal.mjs inline the measured curves into lib/client.js
├── siren-sync.mjs resolve albums and previews
├── fetch-rooms.mjs download backdrops
├── chrome.mjs Chromium discovery, shared by the browser scripts
├── check.mjs the plugin's self-test
├── check-site.mjs drives the console over CDP
├── build-site.mjs generate the console's catalogue and rig
├── preview.mjs render docs/preview*.png from the real code
└── verify-profile.mjs pre-flight a live DSH profile
How it works
The DSH GUI is a slot registry. This plugin registers one entry:
shell.overlay: a frame-wide floating layer above every column and outside their scroll containers. It is a list slot, so id: "mascot" adds an entry. The layer is click-through; the sprite opts back into pointer events.
shell.overlay is root-scoped and does not receive useProjection, so the entry declares a session-maybe-scoped child slot (mascot.panel) and renders the panel through props.renderSlot. The renderer then supplies useProjection / useSession / sessionId.
The host half owns one same-origin route prefix:
GET /dsh-mascot/api/balance account balance, cached
GET /dsh-mascot/api/health route liveness
GET /dsh-mascot/api/looks installed looks, rooms, motion, music
GET /dsh-mascot/art/<file> artwork from the plugin's art/ directory
- The key is resolved per request through
ctx.credentials.resolve(), used only as anAuthorization: Bearerheader, and never appears in a response body, log line or error message. - Routes are gated on
ctx.connection.isAuthenticated(req). Without Connection in the composition they accept only a loopbackHostheader. - The artwork route checks path containment, then the extension, then the disk. A missing file is a 404.
/api/looksre-readsart/index.jsonper request and drops any look whose files are not on disk.
Where the numbers come from
Every figure in the panel comes from DSH's own session projections. The plugin does not count tokens itself.
- Token breakdown:
useProjection("tokenUsage"), shaped{ uncachedInputTokens, outputTokens, cacheReadTokens, cacheWriteTokens } - Cache-hit rate: derived:
cacheReadTokens ÷ (uncachedInputTokens + cacheReadTokens + cacheWriteTokens) - Context occupancy:
useProjection("contextPressure"), shaped{ contextWindow?, pressureTokens?, projectedTokens? } - Account balance:
/dsh-mascot/api/balance, which callsGET {baseUrl}/user/balance - Looks, rooms, music:
/dsh-mascot/api/looks, readingart/index.jsonfiltered to files on disk
Anything unmeasurable renders as —, not 0.
Development
npm install
npm test # 56 checks — fast, no browser
npm run check # the above, plus the wiring check and a real render
npm run check:render # renders the mascot in Chromium; fails on any page error
npm run check:site # 102 checks, drives the console over CDP
npm run check:site -- https://zyaons.github.io/dsh-plugin-mascot/
npm run build:site # regenerate docs/site/catalog.json and rig.js
npm run preview # docs/preview.png — placeholder, committed
npm run preview:official # docs/preview-official.png — official art, gitignored
npm run verify # pre-flight the current DSH profile
npm test renders nothing, so it cannot see a component that throws while rendering — and a throw in MascotOverlay leaves no mascot at all. npm run check:render mounts the real component in Chromium and fails on any uncaught page error. It caught a return today left over from a return { today, error } refactor: the overlay threw on every render and the mascot vanished, while all the fast checks stayed green.
Run npm run build:site after editing lib/client.js; check:site fails if docs/site/rig.js is out of step with the plugin's own skeleton.
npm test fails if any image under art/ or docs/art/ is tracked by git.
check-site.mjs drives the page over the Chrome DevTools Protocol: clicks are dispatched with Input.dispatchMouseEvent, console errors count as failures, and both file:// and a deployed URL can be checked. The narrow viewport is set through Emulation.setDeviceMetricsOverride rather than --window-size, because a headless window has a minimum width that crops a wider layout.
To add a theme: add a key to THEMES in lib/client.js, add the character's theme in art/looks.json, then add the page theme in all four places — a button in docs/index.html, THEME_COLOURS and PAGE_THEME_NAMES in docs/site/site.js, and a [data-theme="…"] rule in docs/site/site.css. check:site fails if any of the four is missing.
Copyright
The repository contains no artwork belonging to anyone else. Characters are © their owners — Hypergryph (Arknights) and Bushiroad (BanG Dream!). npm run fetch-art downloads the images onto each machine.
- Code: MIT
- Official artwork (
art/*.png, not committed): Closure, Muelsyse, Miuyin, Yuyuan, Dusk, Makoto and Wang © Hypergryph; Sengoku Yuno and Sakiko © Bushiroad. Not distributed here. Fine for personal use; get permission before redistributing or using commercially. - Placeholder: the generic glyph in
lib/client.jsdepicts no character and is MIT. - DSH: DeepSeek Harness and its
@deepseek-ai/*packages belong to their respective authors.
Unofficial fan work, not affiliated with or endorsed by any rights holder.
See COPYRIGHT.md.
Comments
Loading…
Similar plugins
by Hades03
DeepSeek Harness plugin: DeepSeek balance and per-provider/model token usage in a draggable overlay.
★ 0
MIT
JavaScript
Aug 22, 2026
dsh plugin --profile web add dsh-model-quota-usageby falser101
Draggable animated floating companion plugin for DeepSeek Harness (dsh web)
★ 3
↓ 270/wk
MIT
TypeScript
Aug 20, 2026
dsh plugin --profile web add @falser101/mascotby gmen1057
Balance and spending of an arckep.ru account inside the DeepSeek Harness (DSH) web UI. Unofficial community plugin.
★ 0
MIT
JavaScript
Oct 10, 2026
dsh plugin --profile web add dsh-arckepby QQQW114
DeepSeek Harness (DSH) plugin - inject context when user input or model output hits configured keywords, with toggleable/configurable rules and model-editable memory.
★ 0
MIT
JavaScript
Aug 17, 2026
dsh plugin --profile web add dsh-keyword-contextby ZelinW1
A cosplay plugin for DeepSeek Harness.
★ 6
↓ 107/wk
MIT
JavaScript
Aug 19, 2026
dsh plugin --profile web add dsh-cosplayby Amakurai
A DeepSeek Harness (dsh) plugin — turns dsh web into a SillyTavern-style roleplay frontend: character cards, prompt presets, lorebooks, personas, BM25 long-term memory, world-state deltas, and rollbac
★ 118
MIT
JavaScript
Oct 10, 2026
dsh plugin --profile web add dsh-liketavern