dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
DSH Web UI plugin: model provider chip beside the composer plus DeepSeek and OpenCode Go balances below it, only for providers the profile configures.
A DeepSeek Harness Web UI plugin that adds two ambient facts to the conversation:
opencode-go, lm-studio, deepseek, …). Hovering it names the exact
provider · model pair. The chip follows the same durable selection the
selector renders, so switching a model switches the chip.Layout contract: the strip claims the full dock row (flex: 1 1 100%) and is
order: 1, so it renders after the shipped stats pills and the context meter and
the dock row wraps it onto its own line. That line keeps a small gap from the row
above (row-gap: 2px on the dock row) and its own margin-bottom: 8px below. It
never wraps internally: text truncates with an ellipsis while the rings and
numbers stay legible, so sidebars can squeeze the composer without the strip
growing to two or three lines. Every text run — the DeepSeek amount included —
uses the same tertiary label tone; only a ring past 70%/90% switches to the warn
or error tone.
The dock row is the strip's box-tree parent but not its DOM parent: slot
entries render inside the slot's display: contents anchor
(<div data-slot="conversation.composer.dock">). The stylesheet therefore
addresses the row through that anchor
(div:has(> [data-slot="conversation.composer.dock"]):has([data-easy-balance])),
and the component also checks the row's computed flex-wrap after mount and
falls back to setting the row's inline flex-wrap when the selector could not
reach it (an engine without :has(), or a changed anchor), restoring the
previous value on unmount.
dsh plugin --profile <profile> add /absolute/path/to/dsh-easy-balance
or install the local directory through the Plugin Manager (install_bundle).
The bundle patch inserts one Host row (easy-balance); the package also declares a
dsh.client half, so the browser bundle is served from /plugins.
| Half | File | Role |
|---|---|---|
| Host | index.js | Registers GET /easy-balance/usage, reads both providers, caches a successful read for 45 s |
| Client | client.js | Registers conversation.input.right (provider chip) and conversation.composer.dock (balance strip) |
Both upstream reads need credentials, so they happen on the Host and never in the browser:
| Fact | Endpoint | Credential reference |
|---|---|---|
| DeepSeek balance | GET https://api.deepseek.com/user/balance | DEEPSEEK_API_KEY |
| OpenCode Go usage | GET https://opencode.ai/zen/go/v1/usage | OPENCODE_GO_API_KEY |
Credentials resolve through ctx.credentials (the same seam that fills
$DSH_HOME/.credentials.yaml), so a rotated key reaches the next read without a
restart. The route answers only requests the connection layer admits, sends
cache-control: no-store, and never includes a key in the response body.
GET /easy-balance/usage?refresh=1 skips a cached value; a request that arrives
while a read is in flight joins that read instead of starting another.
A category is reported only when its provider is actually part of the profile:
the composition must expose a registered llm route for it (any deepseek*
route for the balance, opencode-go for the plan usage) and resolve that
provider's credential. Without both, the Host answers { "configured": false }
without calling upstream at all, and the Client renders nothing for that
category — no balance, no usage window, not even an error placeholder; the strip
disappears entirely when no category is configured. A configured provider that
fails still reports its error, so a real failure stays visible.
{
"fetchedAt": 1759330000000,
"deepseek": {
"configured": true,
"ok": true,
"available": true,
"balances": [
{ "currency": "CNY", "total": "68.04", "granted": "0.00", "toppedUp": "68.04" }
]
},
"opencodeGo": {
"configured": true,
"ok": true,
"windows": {
"rolling": { "status": "ok", "percent": 6, "resetsAt": "2026-10-01T16:30:03.186Z" },
"weekly": { "status": "ok", "percent": 6, "resetsAt": "2026-10-05T00:00:00.000Z" },
"monthly": { "status": "ok", "percent": 16, "resetsAt": "2026-10-20T08:07:46.000Z" }
}
}
}
| Result | Meaning |
|---|---|
{ "configured": false } | Provider not configured (no route, or no credential) — the Client hides the category |
{ "configured": true, "ok": true, ... } | Current value |
{ "configured": true, "ok": false, "error": "<code>" } | Configured but the read failed: unauthorized, network, timeout, bad-json, http-<status> |
One category failing never hides the other.
node test/smoke.mjs runs both halves offline: it materializes the Client
bundle against a React stub (slot ids, rendered text, error mapping), drives the
Host route handler against a stubbed fetch, and asserts the credential
references, cache, ?refresh=1 bypass, method guard and connection fence. It is
a logic test, not visual verification.
conversation.input.right, an
ordered list slot with no shipped occupants, so nothing is shadowed. The strip
uses conversation.composer.dock beside the shipped stats entry, and claims
its own row by addressing that slot's own anchor (see above); no shipped class
name is referenced.<style> element
inside its own row (removed with the component), referencing only
--dsw-alias-* tokens and the host's --dsh-content-font-* properties, so
light and dark palettes and host font scaling both apply. No Harness Client
package is imported at runtime.directoryFor keys directories by the
session's current binding and hands out a fresh one after a re-attach, disposing
the old store, so a memoized directory freezes on a dead store — reads the
provider from the durable modelSelection projection when the shared model
catalog has been cleared (a Host generation reset nulls the directory's
current), and asks a cleared catalog to reload on its own (15 s throttle) so
the label returns without the user opening the model menu.DEEPSEEK_API_KEY. A DSH account sign-in alone (the deepseek-account route)
does not show a balance, because that figure comes from a different service./easy-balance/usage is the only path this plugin adds. Renaming
it means changing USAGE_PATH in both halves.displayName
in llm-pi-ai falls back to its route id (opencode-go), so adding
displayName: OpenCode Go to that provider entry makes both the chip and the
model menu's group heading read nicer.