dsh-header-widgets

August 19, 2026 · View on GitHub

Header widgets for the DSH (DeepSeek Harness) web GUI: a peak-pricing light and a live DeepSeek API balance badge.

npm version License: MIT

Screenshot

Unofficial third-party plugin — not affiliated with or endorsed by DeepSeek.


Features

  • Peak-pricing light — a small status dot in the conversation header that shows the current DeepSeek billing tier. Pure client-side: it computes the tier from the Beijing-time clock (peak windows 09:00–12:00 and 14:00–18:00 daily, half-open intervals [start, end); peak rate 2.0×, off-peak rate 0.5×; there is no "shoulder" tier, and weekdays vs. holidays make no difference). The window table can be overridden at runtime via the dsh-peak.table localStorage key.
  • Live balance badge — a compact badge showing your DeepSeek API balance. It fetches once on mount, then refreshes on a wall-clock-aligned 60 s interval (aligned to minute boundaries), plus on visibilitychange/focus wake-ups. Fetches are single-flight and stale responses are dropped via a request sequence counter. Hovering shows the granted/topped-up breakdown; the badge opens the top-up page (https://platform.deepseek.com/top_up) in a new tab on ok/warn, and clicking an error badge retries.
  • Bilingual — zh/en dictionaries for both widgets, following the DSH locale system.
  • No build step, zero runtime dependencies, and the API key never leaves the server.

Installation

Prerequisites

  • DSH ≥ 0.1.0-rc (requires the webServer service and the web client modules, see Compatibility)
  • A web profile (dsh --profile web)

Steps

  1. Get the plugin onto disk, either way:

    Option A — npm (once published): install it into your web profile, where DSH's module loader resolves plugin names (cordis.patch.yml registers the plugin by package name):

    npm install --prefix "$DSH_HOME/profiles/web" dsh-header-widgets
    

    $DSH_HOME defaults to ~/.dsh when unset; replace web with your profile name. (Installing globally with npm i -g alone is not enough — the global node_modules is not on the profile's resolution path.)

    Option B — git clone / symlink: clone (or symlink) this repository into the plugins directory:

    git clone https://github.com/Beijongggg/dsh-header-widgets.git \
      "$DSH_HOME/plugins/dsh-header-widgets"
    

    $DSH_HOME defaults to ~/.dsh when unset.

  2. Register the plugin in cordis.patch.yml — either $DSH_HOME/cordis.patch.yml (all profiles) or <profile>/cordis.patch.yml (single profile):

    - insert:
        - id: widgets
          name: 'dsh-header-widgets'
    

    (The exact snippet ships in examples/cordis.patch.yml.)

  3. Restart the web profile:

    dsh --profile web
    
  4. Verify the plugin (and the balance route) are wired up:

    dsh --profile web --dump-config
    

Configuration

The balance widget needs a DeepSeek API key. It is resolved lazily on every request by the server-side route, so you can change the key without restarting DSH.

PrioritySourceNotes
1DSH credentials service — ctx.get("credentials").resolve("DEEPSEEK_API_KEY")Preferred when the credentials service is active
2Environment variable DEEPSEEK_API_KEYPlain string
3$DSH_HOME/.credentials.yaml (default ~/.dsh/.credentials.yaml)Flat line DEEPSEEK_API_KEY: sk-...; no quotes needed, no YAML dependency

The optional dsh-peak.table localStorage key overrides the peak-window table at runtime (values are validated; on any invalid entry the built-in table is used):

// Override the peak windows, e.g. single peak window 10:00–11:00 at 3.0×
localStorage.setItem("dsh-peak.table", JSON.stringify([
  { start: 10 * 60, end: 11 * 60, rate: 3 }
]));

Usage

Peak-pricing light

  • Windows (Beijing time, half-open [start, end)): 09:00–12:00 and 14:00–18:00, rate 2.0×; everything else is off-peak at 0.5×.
  • Weekdays and holidays make no difference; there is no shoulder tier.
  • Policy source: https://api-docs.deepseek.com/zh-cn/quick_start/pricing (policy verified as of 2026-08-17).
  • The light re-evaluates on a minute-aligned timer and on visibility/focus changes, so it flips automatically at window boundaries.
  • Accessible: exposes an aria-label like Current billing period: Peak (2.0x).

Balance badge

  • On mount it fetches once; afterwards it refreshes on a wall-clock-aligned 60 s interval plus visibilitychange/focus wake-ups. Requests are single-flight and stale responses are dropped.
  • Hover shows the full title: total balance with the granted / topped-up breakdown (plus an "unavailable" note when is_available is false).
  • Click behavior: ok/warn → open the top-up page in a new tab (https://platform.deepseek.com/top_up, noopener); error → retry immediately; loading shows .
  • Accessible: exposes an aria-label like DeepSeek API balance: ¥123.45.

Security design

  • The API key is resolved only on the server; the browser only ever sees same-origin JSON from the proxy route.
  • The proxy route enforces GET-only (non-GET → 405 with Allow: GET).
  • Upstream calls use an 8 s timeout (AbortSignal.timeout); network failures and timeouts map to 502, upstream 401/403 to 502 with code UPSTREAM_401.
  • Only successful results are cached (30 s TTL); failures are never cached, so a recovered key/network is picked up on the next request. Concurrent requests are deduplicated (single-flight).
  • Errors are sanitized before being sent to the client: newlines are folded and messages truncated to 300 characters.
  • Nothing is ever logged with the key or the Authorization header — the module logs nothing at all.
  • Note: the balance route has no separate authentication beyond the DSH web session. Do not expose your DSH web instance naked to the public internet.

Architecture

┌───────────────────────── DSH host (Node.js ≥ 18) ─────────────────────────┐
│                                                                           │
│  cordis plugin "dsh-header-widgets"  (name = "widgets", inject webServer) │
│    apply(ctx)                                                             │
│      └─ ctx.effect(...)                                                   │
│           └─ ws.register({ kind: "exact",                                 │
│                path: "/plugins/dsh-header-widgets/balance",               │
│                handler: createBalanceHandler({ ctx }) })                  │
│                                                                           │
│  createBalanceHandler({ ctx, resolveKey, fetchImpl, upstreamUrl, now })   │
│    · resolveKey: credentials service → env → ~/.dsh/.credentials.yaml     │
│    · GET https://api.deepseek.com/user/balance (8 s AbortSignal.timeout)  │
│    · 30 s success cache + single-flight dedupe, failures never cached     │
│    · node: builtins only (fs/os/path) — zero new dependencies             │
│                                                                           │
└───────────────────────────────────────────────────────────────────────────┘

                              │ same-origin JSON (GET only)
                              │ /plugins/dsh-header-widgets/balance

┌─────────────────────────── Browser (DSH web GUI) ─────────────────────────┐
│                                                                           │
│  client.js — hand-written window.__ModuleLoader__.load bundle (no build)  │
│  slots.inject("conversation.session.header.actions")                      │
│    · "peak-indicator"    order 80   (locale dict "peak")                  │
│    · "balance-indicator" order 81   (locale dict "balance")               │
│  zh/en dictionaries · minute-aligned refresh · localStorage overrides    │
│                                                                           │
└───────────────────────────────────────────────────────────────────────────┘

The host side is a tiny cordis plugin: apply() registers the exact balance route through ctx.effect, delegating to createBalanceHandler, a pure, dependency-injectable function (node builtins only). The client side is a hand-written module in the window.__ModuleLoader__.load format served verbatim by the host — deliberately no build step.

Compatibility

  • DSH0.1.0-rc, web profile — requires the webServer service and the client modules @deepseek-ai/dsh-client-locale, @deepseek-ai/dsh-client-runtime, @deepseek-ai/dsh-client-ui-layout, @deepseek-ai/dsh-client-ui-sidebar, @deepseek-ai/dsh-client-ui-conversation.
  • Node.js ≥ 18 (AbortSignal.timeout).
  • Browsers: modern evergreen browsers (ES2019+).
  • No build step, zero runtime dependencies.

Development / Testing

  • Local development: clone the repo, add the patch insert (see Installation), restart dsh --profile web, and confirm wiring with dsh --profile web --dump-config.

  • Testing the balance handler: createBalanceHandler takes injectable seams — fetchImpl, upstreamUrl, now, and resolveKey — so you can feed mocks without a live DeepSeek key or network:

    import { createBalanceHandler } from "./lib/balance.js";
    
    const handler = createBalanceHandler({
      resolveKey: async () => "sk-test",
      fetchImpl: async () => new Response(JSON.stringify({ balance_infos: [] })),
      now: () => Date.now()
    });
    
  • Contributors: the hand-written __ModuleLoader__.load format is a deliberate design decision — please do not introduce a build step.

License

MIT © 2026 Beijongggg — see LICENSE.

Third-party notices: this plugin is an independent, unofficial project. "DeepSeek" and related marks are property of their respective owners; this plugin is not affiliated with or endorsed by DeepSeek.