dsh-plugin-offpeak-badge
DeepSeek API peak / off-peak billing badge for the DeepSeek Harness (DSH) Web GUI — as a plugin, in 中文 and English.
A pill in the top-left brand row tells you, at a glance, whether the DeepSeek API is
charging you the off-peak rate (half price) or the peak rate right now. Hover it
for the rule, today's day type and the exact moment of the next switch.
中文说明 →
🐳 deepseek HARNESS [Idle] / 〔空闲〕 ← green = off-peak (half price)
🐳 deepseek HARNESS [Peak] / 〔高峰〕 ← amber = peak
Collapse the sidebar and the badge becomes a dot on the collapse button, so it stays
visible in rail mode.
Tooltip with rule and next switch
What it shows
| Surface | Content |
|---|
Sidebar brand row (sidebar.brand.name) | Idle / Peak pill, colour-coded, hover tooltip |
| Tooltip | current window · today's date, weekday and day type · next switch time and countdown · the rule · which holiday calendar is in use |
Collapsed rail (shell.overlay) | 8 px status dot on the sidebar collapse button |
Screen readers (shell.overlay) | visually hidden aria-live region (the brand seat itself lives inside an aria-hidden subtree) |
Install
Requires a DSH installation (dsh --version) and Node ≥ 18. The package is a DSH
bundle — it declares dsh.bundle.patch and ships a cordis.patch.yml — which is what
makes the one-line install work:
# straight from GitHub (pnpm fetches the sources; this package needs no build step,
# so there is no install-script approval to grant)
dsh plugin --profile web add github:AK-blank/dsh-plugin-offpeak-badge
# …or from a local clone
git clone https://github.com/AK-blank/dsh-plugin-offpeak-badge.git
dsh plugin --profile web add ./dsh-plugin-offpeak-badge
The same thing from the GUI: Plugins → Add plugin, paste the repository URL — the
dialog section below lists every accepted input.
dsh plugin installs the dependency and appends the bundle to the profile's
dsh.profile.bundles; the running dsh web reloads the configuration live — no
restart. If the badge does not show up, reload the browser page once.
Alternative: script install
For deployments where the bundle path is unavailable (or if you prefer the plugin to sit in
~/.dsh/plugins/ instead of the profile's package store), install.mjs wires it up by
hand: it copies the package to ~/.dsh/plugins/dsh-plugin-offpeak-badge/, links it into
~/.dsh/profiles/node_modules/ and appends the row to
~/.dsh/profiles/web/cordis.patch.yml, after a timestamped backup. Nothing outside your
DSH home is touched.
git clone https://github.com/AK-blank/dsh-plugin-offpeak-badge.git
cd dsh-plugin-offpeak-badge
node install.mjs
Options: --home <dir> (DSH home, default $DSH_HOME or ~/.dsh), --profile <name>
(default web), --dir <path>, --copy (default), --link, --dry-run, --help.
Install from the GUI (no terminal)
Open Plugins in the sidebar, click Add plugin, paste a spec and press Install.
The installed bundle card
The dialog parses the spec exactly like dsh plugin add, so these all work:
| Paste into the box | Result |
|---|
github:AK-blank/dsh-plugin-offpeak-badge | installs from GitHub (append #v1.0.0 to pin a ref) |
https://github.com/AK-blank/dsh-plugin-offpeak-badge | the same, as a full repository URL |
/absolute/path/to/dsh-plugin-offpeak-badge | a local directory, installed as a pnpm link — no network, and edits to lib/client.js show up after a page reload |
https://…/dsh-plugin-offpeak-badge.tgz | a .tgz release tarball (none published yet) |
dsh-plugin-offpeak-badge | npm package name — a valid input, but the package is not on npm yet, so the lookup fails |
Two things that trip people up:
- A bare
owner/repo is rejected. The git shorthand needs its github: prefix, and the
npm package-name rule forbids the uppercase owner, so the dialog reports an invalid spec
before it tries anything. Write github:AK-blank/dsh-plugin-offpeak-badge or the full URL.
- The install-source (mirror) selector only affects npm installs, not GitHub ones.
If the plugin is already installed, adding the same spec reports already installed — remove
it first.
Manage it from the same page
The plugin then appears under Installed, with a toggle for the bundle and one for the row it
declares; Uninstall asks for confirmation and removes it. DSH does not auto-update plugins
yet, so upgrading means uninstalling and installing the new version. The equivalent CLI verbs:
dsh plugin --profile web list # pnpm's view of profile packages
dsh plugin --profile web remove dsh-plugin-offpeak-badge
Uninstall
dsh plugin --profile web remove dsh-plugin-offpeak-badge # bundle install
node install.mjs --uninstall [--purge] # script install
Why it is (or is not) listed in the GUI's Plugins page
That page is an inventory of packages: the profile's selected bundles, the profile's
dependencies, and the optional bundles the DSH installation itself ships. dsh plugin add
puts this plugin in all the right places, so it appears under Installed with an
enable/disable toggle.
A row written by hand into the profile's cordis.patch.yml — what install.mjs does — is a
real loader row (the badge works exactly the same), but it belongs to no package, so that
inventory has nothing to list. To get the page entry after a script install, switch to the
bundle install and drop the hand-written row:
dsh plugin --profile web add /path/to/dsh-plugin-offpeak-badge
# then delete the entry install.mjs appended to the profile's cordis.patch.yml
How the billing rule is decided
The plugin implements the footnote on DeepSeek's official pricing page, verbatim:
空闲时段价格为高峰时段价格的一半。北京时间周一至周五(不含中国法定节假日)9:00 - 12:00、14:00 - 18:00 为高峰时段;其余时段,包括周末及中国法定节假日全天均为空闲时段。
Off-peak rates are half of the peak rates. Peak hours are 01:00 - 04:00 and 06:00 - 10:00 UTC, Monday through Friday, excluding Chinese public holidays. All other hours are off-peak, including weekends and Chinese public holidays in full.
The two windows are identical (UTC + 8 h = Beijing); day classification is always the
Beijing calendar day:
peak ⇔ Beijing date is Mon–Fri ∧ not a statutory holiday ∧ time ∈ [09:00,12:00) ∪ [14:00,18:00)
idle ⇔ every other instant
Two details people ask about:
- Adjusted ("调休") weekends. A weekend that holiday adjustment turns into a working
day is still a weekend, and weekends are off-peak in full — so it stays off-peak all
day. Chinese media summarised this as "调休上班的周末、中国法定节假日全天均按空闲时段计费";
that sentence is a news headline rather than official wording, but the conclusion is
correct and needs no extra rule. The tooltip still names such a day, so the calendar
never looks wrong.
- Holidays that fall on a weekday never enter peak hours ("excluding Chinese public
holidays"). The plugin ships the statutory holiday list, so it knows the difference
between a holiday Monday and an ordinary Monday.
Calendar coverage
| Year | Notice | Holiday days | Make-up workdays |
|---|
| 2025 | 国办发明电〔2024〕12号 | 28 | 5 |
| 2026 | 国办发明电〔2025〕7号 | 33 | 6 |
For a year whose notice is not published yet (2027 at the time of writing) the rule
degrades to "Monday–Friday are peak-eligible" and the tooltip says "the 2027 holiday
notice is not published yet; Monday to Friday is assumed" instead of pretending to know.
Adding a year means appending one entry to CALENDAR in lib/client.js; the self-test
then checks its day count, duplicate-free dates, that make-up days fall on a weekend, and
that an official source URL is recorded.
Language
The plugin ships both dictionaries in one registration call (ctx.locale.register(NS, { zh, en })),
which is what the DSH locale service requires, and every seat is registered with that
namespace so the framework re-renders on a language switch:
- the pill follows the GUI language:
空闲/高峰 in Chinese, Idle/Peak in English;
- the tooltip, the countdown wording, weekday names and the screen-reader announcement are
translated too;
- the rule line quotes the official English wording on the English page and the official
Chinese wording on the Chinese one;
node selftest.mjs fails if the two dictionaries ever drift: it asserts identical key
sets, identical {placeholder} sets per key, no blank values, and renders the tooltip in
both languages.
Verify
node selftest.mjs # 89 offline assertions, no network, no browser
node selftest.mjs --now # …and print the current verdict in both languages
The self-test loads lib/client.js the same way the browser module table does (stub
window.__ModuleLoader__, stub require), then covers the module contract, all ten
window boundaries of a working day, weekends and make-up work weekends, eight statutory
holidays that fall on weekdays, the calendar-uncovered degradation, the next-switch
computation (same day, across a weekend, across a holiday), calendar integrity, timezone
independence (the verdict is identical under TZ=UTC and TZ=America/New_York), both
dictionaries, and the registration contract through a stub context.
How it plugs into DSH
This is a dual-face Cordis plugin, exactly like the official
@deepseek-ai/dsh-client-ui-* packages:
| File | Role |
|---|
lib/index.js | host half — an empty apply() that gives the Loader a host row, which is what makes dsh-client-modules' incremental dsh.client scan add the browser half to the __DSH_BOOT__ graph |
lib/client.js | browser half — a hand-written window.__ModuleLoader__.load({ id, factory }) bundle: rule, calendar, dictionaries, seats |
package.json | dsh.client (platform: web, loader inject, module external) and exports["./client"] |
lib/client.js deliberately avoids a build step: it is the same pre-bundled CJS-factory
form the official client plugins ship in, so the repository can be read, diffed and
published as-is. It requires only platform seed words (react,
@deepseek-ai/dsh-client-ui-primitives) plus the slots and locale services.
Design notes
- Why the brand seat is taken at
priority: -1. sidebar.brand.name is a single
slot: same-priority registrations throw, and only the lowest-priority registration
renders. The official brand plugin sits at 0, so a lower priority is the only way to add
anything next to the wordmark. This plugin re-renders the official BrandWordmark
itself, so the branding is unchanged — only the pill is new.
- Why the wordmark is 22 px tall instead of 24. At the default 280 px sidebar the brand
row is a fixed budget (24 icon + 8 + wordmark + 6 + pill ≤ 216 px). Trimming the wordmark
by 2 px is what lets a two-character pill fit without clipping. Below a 250 px row the
pill degrades to a dot automatically (measured with a
ResizeObserver).
- Why the rail dot lives in
shell.overlay instead of sidebar.toggle.badge: that
single slot is held by the official settings plugin's DesktopUpdateBadge, which also
reports a dropped/reconnecting host connection. Taking the slot would swallow those
signals, so the dot is drawn additively into the overlay layer, positioned from the
collapse button's viewport rectangle.
- Click-through is blocked. The whole brand row is the "new session" button; the badge
stops
click / mousedown / pointerdown, so clicking it never starts a session.
corner-shape: round is set explicitly on the dots: DSH's global design language
renders border-radius as a superellipse (squircle), which would turn a dot into a
rounded square.
Compatibility
- Built and verified against DSH
0.2.0-rc.2 (client plugin API dsh.client +
__ModuleLoader__, slot registry, locale registry) — recorded in
dsh.compatibility.dshReleases.
- Official packages are declared as peerDependencies with prerelease-carrying ranges
(
>=0.2.0-rc.1 <1.0.0-0), so -rc harness builds resolve instead of being silently
excluded. Nothing is imported from them at runtime except the shell-provided seed
@deepseek-ai/dsh-client-ui-primitives.
- No runtime dependencies, no build step, no network calls: the badge is computed locally
from the clock and the bundled calendar. The git install therefore needs no pnpm
allowBuilds approval.
Community
Unofficial plugin. Not affiliated with, endorsed by, or supported by DeepSeek.
“DeepSeek Harness” is a trademark of DeepSeek; this project uses the abbreviated “DSH”
designation for the ecosystem, as the project's brand guidelines recommend.
License
MIT. The rule and calendar data are quoted from DeepSeek's public pricing page
and the State Council holiday notices, with sources recorded in the code.