@linxin666/dsh-usage 0.3.22 → 0.3.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +6 -8
- package/README.zh.md +6 -8
- package/lib/client.js +749 -78
- package/lib/client.js.map +1 -1
- package/lib/index.js +171 -201
- package/lib/types/client/UsageSectionCard.d.ts +0 -1
- package/lib/types/client/UsageSidebarPanel.d.ts +34 -0
- package/lib/types/client/body-mutations.d.ts +45 -0
- package/lib/types/client/locales.d.ts +9 -4
- package/lib/types/client/sidebar-entry-core.d.ts +87 -0
- package/lib/types/client/sidebar-entry.d.ts +35 -0
- package/lib/types/client/sidebar-panel-mount.d.ts +40 -0
- package/lib/types/client/voucher.d.ts +2 -2
- package/lib/types/core/provider-routes.d.ts +26 -0
- package/lib/types/core/types.d.ts +14 -1
- package/lib/types/host/access.d.ts +17 -0
- package/lib/types/host/pair-access.d.ts +34 -0
- package/lib/types/host/routes.d.ts +16 -7
- package/lib/types/host/usage-service.d.ts +28 -73
- package/lib/types/index.d.ts +0 -2
- package/package.json +4 -3
- package/src/client/UsageSectionCard.tsx +1 -15
- package/src/client/UsageSidebarPanel.tsx +196 -0
- package/src/client/body-mutations.ts +133 -0
- package/src/client/index.ts +22 -1
- package/src/client/locales.ts +20 -10
- package/src/client/sidebar-entry-core.ts +321 -0
- package/src/client/sidebar-entry.ts +101 -0
- package/src/client/sidebar-panel-mount.tsx +113 -0
- package/src/client/usage.module.css +252 -0
- package/src/client/voucher.ts +2 -2
- package/src/core/adapters.ts +40 -6
- package/src/core/provider-routes.ts +37 -0
- package/src/core/types.ts +14 -1
- package/src/host/access.ts +21 -0
- package/src/host/pair-access.ts +49 -0
- package/src/host/routes.ts +19 -10
- package/src/host/usage-service.ts +65 -214
- package/src/index.ts +1 -6
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One trailing action button seated at the entry row's right edge (e.g. a
|
|
3
|
+
* refresh command or a collapse chevron). Rows with actions switch from the
|
|
4
|
+
* classic single-button structure to a container with a main button plus the
|
|
5
|
+
* action buttons (nested interactive elements are invalid HTML); rows without
|
|
6
|
+
* actions keep the classic structure untouched.
|
|
7
|
+
*/
|
|
8
|
+
export interface SidebarEntryAction {
|
|
9
|
+
/** Stable id stamped as data-dsh-entry-action="<id>" (CSS hooks and tests). */
|
|
10
|
+
id: string;
|
|
11
|
+
/** Inline icon markup while the row is active (always, when inactiveIcon is unset). */
|
|
12
|
+
icon: string;
|
|
13
|
+
/**
|
|
14
|
+
* Optional icon while the row is inactive. Setting it makes the action a
|
|
15
|
+
* state mirror (a collapse chevron): the core swaps the icon and stamps
|
|
16
|
+
* aria-expanded on every active-state change. Requires the active bridge.
|
|
17
|
+
*/
|
|
18
|
+
inactiveIcon?: string;
|
|
19
|
+
/** Localized accessible label (aria-label + title); receives the active state. */
|
|
20
|
+
label(active: boolean): string;
|
|
21
|
+
/**
|
|
22
|
+
* Click handler; receives the button so the caller can drive transient
|
|
23
|
+
* affordances (a refresh cooldown) without a re-render.
|
|
24
|
+
*/
|
|
25
|
+
onClick(button: HTMLButtonElement): void;
|
|
26
|
+
}
|
|
27
|
+
/** Per-package configuration for one sidebar entry row. */
|
|
28
|
+
export interface SidebarEntryOptions {
|
|
29
|
+
/** Full attribute name identifying the injected row (idempotency key), e.g. 'data-dsh-ssh-entry'. */
|
|
30
|
+
rowAttribute: string;
|
|
31
|
+
/** CSS selector matching the injected row, e.g. '[data-dsh-ssh-entry]'. */
|
|
32
|
+
rowSelector: string;
|
|
33
|
+
/**
|
|
34
|
+
* L2 semantic-attribute plugin id (issue #506, enum table:
|
|
35
|
+
* skins/skin-center/contracts/semantic-attrs-v1.md). When set, the row also
|
|
36
|
+
* outputs data-dsh-plugin="<id>" and data-dsh-part="sidebar-entry"; unset
|
|
37
|
+
* leaves the row without semantic attributes.
|
|
38
|
+
*/
|
|
39
|
+
plugin?: string;
|
|
40
|
+
/** Inline icon markup (matches the shell's 16px nav-icon look). */
|
|
41
|
+
icon: string;
|
|
42
|
+
/** CSS module class names for the row and its two spans (entry / entryIcon / entryLabel). */
|
|
43
|
+
css: Record<string, string>;
|
|
44
|
+
/** Localized row label (aria-label + visible text). */
|
|
45
|
+
label(): string;
|
|
46
|
+
/** Optional localized tooltip (title attribute). */
|
|
47
|
+
tooltip?(): string;
|
|
48
|
+
/**
|
|
49
|
+
* Optional locale-change subscription: re-applies label / aria-label /
|
|
50
|
+
* tooltip whenever the active locale changes. Plain-DOM rows would
|
|
51
|
+
* otherwise keep the label captured at mount; pass the SDK locale runtime
|
|
52
|
+
* subscription (ctx.locale.subscribe) so the row follows the language
|
|
53
|
+
* switch without a reload.
|
|
54
|
+
*/
|
|
55
|
+
refresh?: {
|
|
56
|
+
subscribe(listener: () => void): () => void;
|
|
57
|
+
};
|
|
58
|
+
/** Click action (open/toggle the owning panel). */
|
|
59
|
+
onToggle(): void;
|
|
60
|
+
/**
|
|
61
|
+
* Optional trailing action buttons at the row's right edge. Requires the
|
|
62
|
+
* package CSS to seat entryMain / entryAction; empty keeps the classic
|
|
63
|
+
* single-button row.
|
|
64
|
+
*/
|
|
65
|
+
actions?: readonly SidebarEntryAction[];
|
|
66
|
+
/** Family-block position: 'before' inserts ahead of sibling plugin rows, 'after' behind them. */
|
|
67
|
+
position: 'before' | 'after';
|
|
68
|
+
/**
|
|
69
|
+
* Selectors of the sibling plugin entry rows this package orders against
|
|
70
|
+
* (its own row included — the placement guard excludes a row that is
|
|
71
|
+
* already inside the root). Each package passes the same list it used
|
|
72
|
+
* before the consolidation so the rendered order stays stable.
|
|
73
|
+
*/
|
|
74
|
+
familySelectors: readonly string[];
|
|
75
|
+
/** Optional active-state bridge; highlights the row while the panel is open. */
|
|
76
|
+
active?: {
|
|
77
|
+
subscribe(listener: () => void): () => void;
|
|
78
|
+
isOpen(): boolean;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Mount the sidebar entry, waiting for the shell to render and self-healing
|
|
83
|
+
* on later React re-renders.
|
|
84
|
+
* @param options - the row's attribute/icon/copy/action/ordering configuration.
|
|
85
|
+
* @returns disposer removing the entry and its observers.
|
|
86
|
+
*/
|
|
87
|
+
export declare function mountSidebarEntry(options: SidebarEntryOptions): () => void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidebar entry injection for the usage panel (issue #1592) — wiring over the
|
|
3
|
+
* shared core. Unlike the panel-takeover family (task board / SSH / skill
|
|
4
|
+
* center) this row does not participate in the family block, so it is placed
|
|
5
|
+
* directly under the New Session row and ordered after those entries. The row
|
|
6
|
+
* seats the panel's controls as trailing actions: a refresh command and a
|
|
7
|
+
* collapse chevron mirroring the open state, so the panel below renders only
|
|
8
|
+
* its content.
|
|
9
|
+
* @module @linxin666/dsh-usage/client/sidebar-entry
|
|
10
|
+
*/
|
|
11
|
+
/** Stable data attribute identifying the injected entry row. */
|
|
12
|
+
export declare const ENTRY_SELECTOR = "[data-dsh-usage-entry]";
|
|
13
|
+
/** Locale-change subscription the shared core asks for (ctx.locale.subscribe shape). */
|
|
14
|
+
export interface LocaleRefreshSource {
|
|
15
|
+
subscribe(listener: () => void): () => void;
|
|
16
|
+
}
|
|
17
|
+
/** Panel-facing callbacks and state the entry row's controls drive. */
|
|
18
|
+
export interface UsageSidebarEntryHandlers {
|
|
19
|
+
/** Toggles the panel (the row's main area and the collapse chevron). */
|
|
20
|
+
onToggle(): void;
|
|
21
|
+
/** Forces one host probe cycle (the refresh action). */
|
|
22
|
+
onRefresh(): void;
|
|
23
|
+
/** Reads the panel's expanded state (row highlight and chevron direction). */
|
|
24
|
+
isOpen(): boolean;
|
|
25
|
+
/** Subscribes to expanded-state changes (the chevron mirrors them live). */
|
|
26
|
+
subscribeOpen(listener: () => void): () => void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Mount the usage sidebar entry.
|
|
30
|
+
* @param handlers - the panel's toggle/refresh callbacks and open-state face.
|
|
31
|
+
* @param locale - locale-change source; when given, re-applies the label on a
|
|
32
|
+
* language switch (the plain-DOM row otherwise keeps the mount-time copy).
|
|
33
|
+
* @returns disposer removing the entry and its observers.
|
|
34
|
+
*/
|
|
35
|
+
export declare function mountSidebarEntry(handlers: UsageSidebarEntryHandlers, locale?: LocaleRefreshSource): () => void;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidebar usage panel mounting (issue #1592).
|
|
3
|
+
*
|
|
4
|
+
* The panel is a plain DOM row inserted into the sidebar right below the
|
|
5
|
+
* usage entry row, carrying its own React root. Placement self-heals with the
|
|
6
|
+
* same page-wide body-mutation hub the entry row uses; the entry row stays the
|
|
7
|
+
* anchor, so the sidebar order never depends on this module's timing.
|
|
8
|
+
*
|
|
9
|
+
* The mount owns the panel's single open state: initialized from localStorage,
|
|
10
|
+
* flipped by the entry row's toggle controls, and mirrored to the React body
|
|
11
|
+
* through the subscribe/isOpen face (the same face the entry row's chevron
|
|
12
|
+
* and highlight read).
|
|
13
|
+
* @module @linxin666/dsh-usage/client/sidebar-panel-mount
|
|
14
|
+
*/
|
|
15
|
+
import type { UsageStoreInstance } from './usage-store.ts';
|
|
16
|
+
/** localStorage key holding the collapsed flag ('1' = collapsed). */
|
|
17
|
+
export declare const COLLAPSED_STORAGE_KEY = "dsh-usage.sidebar.collapsed";
|
|
18
|
+
/** Panel inputs the mount forwards into the React tree. */
|
|
19
|
+
export interface UsagePanelMountProps {
|
|
20
|
+
store: UsageStoreInstance;
|
|
21
|
+
/** Fetch one overview now. */
|
|
22
|
+
poll: () => void;
|
|
23
|
+
}
|
|
24
|
+
/** Mounted panel controller: the entry row drives the toggle and mirrors the state. */
|
|
25
|
+
export interface UsagePanelMount {
|
|
26
|
+
/** Whether the panel body is currently expanded. */
|
|
27
|
+
isOpen(): boolean;
|
|
28
|
+
/** Subscribe to open/collapse changes (the entry row's chevron and highlight). */
|
|
29
|
+
subscribe(listener: () => void): () => void;
|
|
30
|
+
/** Flip the open state, persist it, and notify subscribers. */
|
|
31
|
+
toggle(): void;
|
|
32
|
+
/** Unmount the panel and remove its container. */
|
|
33
|
+
dispose(): void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Mount the sidebar usage panel below its entry row.
|
|
37
|
+
* @param props - the store and poll callback of the apply body.
|
|
38
|
+
* @returns the panel controller.
|
|
39
|
+
*/
|
|
40
|
+
export declare function mountUsagePanel(props: UsagePanelMountProps): UsagePanelMount;
|
|
@@ -21,8 +21,8 @@ export interface VoucherData {
|
|
|
21
21
|
from: string;
|
|
22
22
|
to: string;
|
|
23
23
|
}
|
|
24
|
-
/** Anti-inflation exchange rate:
|
|
25
|
-
export declare const TOKENS_PER_WHALE_YUAN =
|
|
24
|
+
/** Anti-inflation exchange rate: 1,000,000 tokens mint one whale yuan. */
|
|
25
|
+
export declare const TOKENS_PER_WHALE_YUAN = 1000000;
|
|
26
26
|
/** The note's face value in whale yuan; the smallest denomination is 1. */
|
|
27
27
|
export declare function faceValue(tokens: number): number;
|
|
28
28
|
/** Local calendar day (`YYYY-MM-DD`) for an epoch ms timestamp. */
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alias folding for the enumerated provider routes. Kept in core because the
|
|
3
|
+
* rule is pure — the host enumerates routes and this decides which of two
|
|
4
|
+
* routes serving one adapter family survives.
|
|
5
|
+
* @module @linxin666/dsh-usage/core/provider-routes
|
|
6
|
+
*/
|
|
7
|
+
/** The facts alias folding needs about one enumerated provider route. */
|
|
8
|
+
export interface FoldableRoute {
|
|
9
|
+
/** Provider route key (`deepseek`, `deepseek-official`, custom routes, ...). */
|
|
10
|
+
id: string;
|
|
11
|
+
/** Whether the LLM runtime serves requests on this route; a catalog-only entry is dormant. */
|
|
12
|
+
live: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Drop dormant catalog aliases that shadow a live route of the same adapter
|
|
16
|
+
* family. The pi-ai catalog ships route keys the runtime serves nothing on
|
|
17
|
+
* until a profile declares them: for DeepSeek the catalog entry `deepseek`
|
|
18
|
+
* sits beside the llm-deepseek runtime route `deepseek-official`, and both
|
|
19
|
+
* resolve the same credential (the family's `apiKeyEnv` fallback), so probing
|
|
20
|
+
* and rendering both turns one account into two identical balance rows.
|
|
21
|
+
*
|
|
22
|
+
* A family keeps every live route it has — two configured profiles are two
|
|
23
|
+
* real accounts, not aliases. A route with no adapter, or one whose family
|
|
24
|
+
* has no live route at all, is returned untouched.
|
|
25
|
+
*/
|
|
26
|
+
export declare function foldAliasRoutes<T extends FoldableRoute>(routes: readonly T[]): T[];
|
|
@@ -118,12 +118,25 @@ export interface ProviderSnapshotView {
|
|
|
118
118
|
export interface UsageOverviewView {
|
|
119
119
|
updatedAt: number;
|
|
120
120
|
providers: ProviderSnapshotView[];
|
|
121
|
-
/** The provider the
|
|
121
|
+
/** The provider the collapsed sidebar strip and the header highlight; `source` says how it was picked. */
|
|
122
122
|
current: {
|
|
123
123
|
provider?: string;
|
|
124
124
|
model?: string;
|
|
125
125
|
/** `live` — last request seen this boot; `default` — the agent default model. */
|
|
126
126
|
source: 'live' | 'default';
|
|
127
|
+
/**
|
|
128
|
+
* Resolved display name (snapshot first, route-derived otherwise), so the
|
|
129
|
+
* strip needs no name-resolution of its own. Optional so an older host
|
|
130
|
+
* document still renders.
|
|
131
|
+
*/
|
|
132
|
+
displayName?: string;
|
|
133
|
+
/**
|
|
134
|
+
* Today's ledger totals for the provider's adapter family (the route
|
|
135
|
+
* itself when adapter-less), carrying tokens, calls, and the priced cost.
|
|
136
|
+
* Absent on a day without usage; optional for the same older-host
|
|
137
|
+
* tolerance as displayName.
|
|
138
|
+
*/
|
|
139
|
+
today?: UsageTokenTotals;
|
|
127
140
|
};
|
|
128
141
|
usage: {
|
|
129
142
|
today: {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage trust fence (issue #1592): loopback (the desktop) always passes, and a
|
|
3
|
+
* live paired-device cookie is an additional allow path when remote-web-ui is
|
|
4
|
+
* loaded — the same family fence the skill center uses. Without that service
|
|
5
|
+
* the fence stays loopback-only, so an unpaired LAN browser keeps its 403.
|
|
6
|
+
* The decision logic lives in the generated pair-access.ts copy (shared by
|
|
7
|
+
* git-graph / pet / skill-explorer).
|
|
8
|
+
*/
|
|
9
|
+
import type { IncomingMessage } from 'node:http';
|
|
10
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
11
|
+
/**
|
|
12
|
+
* Whether this request may read the usage overview or trigger a refresh.
|
|
13
|
+
* @param ctx - host context; may expose remoteWebUiPairing.
|
|
14
|
+
* @param request - the incoming HTTP request.
|
|
15
|
+
* @returns true for loopback, or a live paired-device cookie.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isUsageAllowed(ctx: Context, request: IncomingMessage): boolean;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pairing trust fence shared by plugins that expose host routes: loopback
|
|
3
|
+
* (the desktop) always passes; a live paired-device cookie is an additional
|
|
4
|
+
* allow path when remote-web-ui is loaded. The consuming plugin never
|
|
5
|
+
* depends on that plugin — without the service the fence stays
|
|
6
|
+
* loopback-only.
|
|
7
|
+
*
|
|
8
|
+
* Per-package wrappers (access.ts) call this with their own name so each
|
|
9
|
+
* plugin keeps a self-describing export; the security decision lives only
|
|
10
|
+
* here.
|
|
11
|
+
*/
|
|
12
|
+
import type { IncomingMessage } from 'node:http';
|
|
13
|
+
/** Structural pairing lookup (no package dependency on remote-web-ui). */
|
|
14
|
+
interface PairingAccess {
|
|
15
|
+
isPairedDevice(request: IncomingMessage): boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Structural host-context shape: shared sources carry no @deepseek-ai
|
|
19
|
+
* dependency (the shared package must typecheck standalone), so the fence
|
|
20
|
+
* reads only the two members it needs; cordis Context satisfies this.
|
|
21
|
+
* ctx.get is optional on the test harness; production Context always has it.
|
|
22
|
+
*/
|
|
23
|
+
interface LookupCtx {
|
|
24
|
+
get?(name: string, strict?: boolean): unknown;
|
|
25
|
+
remoteWebUiPairing?: PairingAccess;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Whether this request may enter the plugin's host routes.
|
|
29
|
+
* @param ctx - host context; may expose remoteWebUiPairing.
|
|
30
|
+
* @param request - the incoming HTTP request.
|
|
31
|
+
* @returns true for loopback, or a live paired-device cookie.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isPairedOrLoopbackAllowed(ctx: LookupCtx, request: IncomingMessage): boolean;
|
|
34
|
+
export {};
|
|
@@ -1,14 +1,23 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
1
2
|
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
|
|
2
3
|
import type { UsageService } from './usage-service.ts';
|
|
3
4
|
export declare const USAGE_API_PREFIX = "/api/dsh-usage";
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Overview route: provider balances, plan quotas, and token usage totals.
|
|
7
|
+
* Personal account data behind the family trust fence — loopback always
|
|
8
|
+
* passes, and a live paired-device cookie passes too when remote-web-ui is
|
|
9
|
+
* loaded (issue #1592: the sidebar usage panel reads this from a paired LAN
|
|
10
|
+
* browser); every unpaired LAN request keeps its 403.
|
|
11
|
+
* @param ctx - host context; may expose remoteWebUiPairing.
|
|
12
|
+
* @param service - the usage service.
|
|
13
|
+
* @returns the route.
|
|
8
14
|
*/
|
|
9
|
-
export declare function makeUsageOverviewRoute(service: UsageService): WebRoute;
|
|
15
|
+
export declare function makeUsageOverviewRoute(ctx: Context, service: UsageService): WebRoute;
|
|
10
16
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
17
|
+
* Manual refresh: forces one probe cycle now and answers with the fresh
|
|
18
|
+
* overview. Same trust fence as the overview route.
|
|
19
|
+
* @param ctx - host context; may expose remoteWebUiPairing.
|
|
20
|
+
* @param service - the usage service.
|
|
21
|
+
* @returns the route.
|
|
13
22
|
*/
|
|
14
|
-
export declare function makeUsageRefreshRoute(service: UsageService): WebRoute;
|
|
23
|
+
export declare function makeUsageRefreshRoute(ctx: Context, service: UsageService): WebRoute;
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The dsh-usage host service: folds live session usage into the persistent
|
|
3
3
|
* ledger, probes each configured provider's balance/coding-plan endpoint on
|
|
4
|
-
* a poll cycle, and
|
|
5
|
-
*
|
|
6
|
-
* overview document.
|
|
4
|
+
* a poll cycle, and serves the current provider's strip-ready view inside
|
|
5
|
+
* the overview document. Secrets stay in the host process; the browser only
|
|
6
|
+
* ever sees the overview document.
|
|
7
7
|
* @module @linxin666/dsh-usage/host/usage-service
|
|
8
8
|
*/
|
|
9
9
|
import type { Context } from '@deepseek-ai/cordis';
|
|
10
|
-
import type {
|
|
11
|
-
/** Source tag the plugin stamps onto pet announcements. */
|
|
12
|
-
export declare const USAGE_ANNOUNCE_SOURCE = "dsh-usage";
|
|
10
|
+
import type { UsageOverviewView } from '../core/types.ts';
|
|
13
11
|
/**
|
|
14
12
|
* Persisted accrual state for the official DeepSeek family's real spend:
|
|
15
13
|
* consecutive observations of the official CNY balance, decreases counted
|
|
@@ -25,54 +23,11 @@ export interface SpendWatch {
|
|
|
25
23
|
/** The balance the previous observation ended on, when one exists. */
|
|
26
24
|
lastBalanceCny?: number;
|
|
27
25
|
}
|
|
28
|
-
/** Poll-loop
|
|
26
|
+
/** Poll-loop options; re-applied live on settings change. */
|
|
29
27
|
export interface UsageServiceOptions {
|
|
30
28
|
pollIntervalSec: number;
|
|
31
|
-
bubbleMode: 'always' | 'change' | 'off';
|
|
32
29
|
retainDays: number;
|
|
33
30
|
}
|
|
34
|
-
/** Format a balance for display: symbol prefix when known, code suffix otherwise. */
|
|
35
|
-
export declare function formatMoney(currency: string, totalBalance: string): string;
|
|
36
|
-
/** Map a used percent to the announcement tone. */
|
|
37
|
-
export declare function planTone(percent: number): 'ok' | 'warn' | 'low';
|
|
38
|
-
/** Announcement context the service computes per poll: today's family spend and the DeepSeek period. */
|
|
39
|
-
export interface AnnounceContext {
|
|
40
|
-
/** Today's ledger spend for the announced provider's family (CNY; 0 = unpriced). */
|
|
41
|
-
todayCost?: number;
|
|
42
|
-
/** Whether DeepSeek peak pricing is in effect right now. */
|
|
43
|
-
peak?: boolean;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Build the raw pet announce payload for one provider snapshot, or
|
|
47
|
-
* undefined when nothing worth announcing exists. Pure: every payload this
|
|
48
|
-
* returns satisfies the pet's `parseAnnouncement` contract — plan
|
|
49
|
-
* announcements require a numeric percent, so percent-less windows never
|
|
50
|
-
* announce (the pet validator would silently drop them). A priced family
|
|
51
|
-
* (DeepSeek) with spend today announces a cost bubble first; balance-only
|
|
52
|
-
* families announce the balance; plan families announce their tightest
|
|
53
|
-
* percent window.
|
|
54
|
-
*/
|
|
55
|
-
export declare function buildAnnouncement(snapshot: Pick<ProviderSnapshotView, 'displayName' | 'balance' | 'plan'>, context?: AnnounceContext): Record<string, unknown> | undefined;
|
|
56
|
-
/**
|
|
57
|
-
* Compact token-count display for the usage fallback bubble: `9805`,
|
|
58
|
-
* `8.7万`, `1.2亿`. The host-authored bubble copy is zh (like every other
|
|
59
|
-
* line this service speaks), so the magnitudes follow the zh convention.
|
|
60
|
-
*/
|
|
61
|
-
export declare function formatTokens(count: number): string;
|
|
62
|
-
/**
|
|
63
|
-
* The usage fallback for the session's provider: today's token consumption
|
|
64
|
-
* and call count, the one fact the ledger owns for every provider. Providers
|
|
65
|
-
* without a readable balance/plan endpoint (relay stations, local runtimes,
|
|
66
|
-
* token-plan vendors — and probeable providers whose probes are failing)
|
|
67
|
-
* would otherwise leave the pet bubble permanently silent even while their
|
|
68
|
-
* sessions run. Undefined when the provider has no usage today: a bubble
|
|
69
|
-
* about nothing is noise, not information. Pure; the payload satisfies the
|
|
70
|
-
* pet's `parseAnnouncement` contract.
|
|
71
|
-
*/
|
|
72
|
-
export declare function buildLedgerAnnouncement(input: {
|
|
73
|
-
displayName: string;
|
|
74
|
-
totals: UsageTokenTotals;
|
|
75
|
-
}): Record<string, unknown> | undefined;
|
|
76
31
|
export declare class UsageService {
|
|
77
32
|
private readonly ctx;
|
|
78
33
|
private options;
|
|
@@ -85,7 +40,7 @@ export declare class UsageService {
|
|
|
85
40
|
private spendWatch;
|
|
86
41
|
/** Per-live-session route attribution (WeakMap: disposed sessions age out). */
|
|
87
42
|
private readonly sessionRoutes;
|
|
88
|
-
/** The most recent route seen this boot; the
|
|
43
|
+
/** The most recent route seen this boot; the sidebar strip and the header highlight follow it. */
|
|
89
44
|
private current;
|
|
90
45
|
private sessionListenerDisposer;
|
|
91
46
|
private pollTimer;
|
|
@@ -98,7 +53,6 @@ export declare class UsageService {
|
|
|
98
53
|
/** True once loadPersisted finished; nothing may overwrite the files before that. */
|
|
99
54
|
private loaded;
|
|
100
55
|
private disposed;
|
|
101
|
-
private lastSignature;
|
|
102
56
|
/** Last prune guard: once per local day, and whenever retention shrinks. */
|
|
103
57
|
private lastPrune;
|
|
104
58
|
constructor(ctx: Context, options: UsageServiceOptions);
|
|
@@ -116,12 +70,22 @@ export declare class UsageService {
|
|
|
116
70
|
refresh(): Promise<void>;
|
|
117
71
|
/** Assemble the overview document the browser section renders. */
|
|
118
72
|
overview(): UsageOverviewView;
|
|
73
|
+
/**
|
|
74
|
+
* The strip-ready current-provider view: the resolved display name plus
|
|
75
|
+
* today's ledger totals for the provider's adapter family (the route
|
|
76
|
+
* itself when adapter-less). `today` is absent on a day without usage —
|
|
77
|
+
* a strip about nothing is noise, not information.
|
|
78
|
+
*/
|
|
79
|
+
private currentView;
|
|
119
80
|
/** One local day aggregated per provider. */
|
|
120
81
|
private daySummary;
|
|
121
|
-
/**
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Today's ledger totals for one provider's adapter family — the exact
|
|
84
|
+
* route when adapter-less, the merged family otherwise, so aliased routes
|
|
85
|
+
* of one account (the deepseek catalog id and its runtime alias) fold
|
|
86
|
+
* together.
|
|
87
|
+
*/
|
|
88
|
+
private familyUsageToday;
|
|
125
89
|
/**
|
|
126
90
|
* Accrue the official DeepSeek family's real spend from observed CNY
|
|
127
91
|
* balance decreases; a balance rise is a top-up and never counts. The
|
|
@@ -134,7 +98,7 @@ export declare class UsageService {
|
|
|
134
98
|
/**
|
|
135
99
|
* The display name for a route key: the LLM runtime's first, then the
|
|
136
100
|
* adapter's, then the id itself — the snapshot may not exist for
|
|
137
|
-
* adapter-less routes, yet the
|
|
101
|
+
* adapter-less routes, yet the sidebar strip still needs a legible title.
|
|
138
102
|
*/
|
|
139
103
|
private routeDisplayName;
|
|
140
104
|
private onSessionEvent;
|
|
@@ -162,11 +126,17 @@ export declare class UsageService {
|
|
|
162
126
|
private persistSnapshots;
|
|
163
127
|
private rearmPoll;
|
|
164
128
|
/**
|
|
165
|
-
* One poll cycle: enumerate routes, resolve credentials, probe
|
|
129
|
+
* One poll cycle: enumerate routes, resolve credentials, probe.
|
|
166
130
|
* A call while a cycle is already running joins that cycle instead of
|
|
167
131
|
* returning immediately, so a manual refresh always waits for real probes.
|
|
168
132
|
*/
|
|
169
133
|
pollNow(): Promise<void>;
|
|
134
|
+
/**
|
|
135
|
+
* Every route the service probes and renders: the runtime's live providers
|
|
136
|
+
* plus the configurable directory's catalog entries, then alias-folded so a
|
|
137
|
+
* dormant catalog entry cannot shadow the live route of the same adapter
|
|
138
|
+
* family (see foldAliasRoutes).
|
|
139
|
+
*/
|
|
170
140
|
private listProviderRoutes;
|
|
171
141
|
private probeRoute;
|
|
172
142
|
/**
|
|
@@ -182,19 +152,4 @@ export declare class UsageService {
|
|
|
182
152
|
private piAiProfile;
|
|
183
153
|
/** The DeepSeek official adapter's credential reference name. */
|
|
184
154
|
private deepseekApiKeyEnv;
|
|
185
|
-
/**
|
|
186
|
-
* Announce the current provider's spend, balance, or plan usage to the pet.
|
|
187
|
-
* In `change` mode only meaningful value changes re-announce; `off` skips.
|
|
188
|
-
* The TTL rides the poll interval (bubble_mode `always` re-announces every
|
|
189
|
-
* cycle, so a TTL of two cycles + margin keeps the bubble continuous
|
|
190
|
-
* across polls; the pet contract caps the ceiling). A route id the
|
|
191
|
-
* catalogs spell differently than the snapshot keys falls back to its
|
|
192
|
-
* adapter family's snapshot. When the provider has no announceable probe
|
|
193
|
-
* fact (no adapter, failing probes, percent-less windows) the bubble falls
|
|
194
|
-
* back to the provider's today ledger usage, and with neither fact nor
|
|
195
|
-
* usage it stays silent. Fully guarded: a malformed snapshot or a failing
|
|
196
|
-
* pet service must never break the poll loop, and a disposed service never
|
|
197
|
-
* announces.
|
|
198
|
-
*/
|
|
199
|
-
private announceCurrent;
|
|
200
155
|
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -9,8 +9,6 @@ export interface Config {
|
|
|
9
9
|
enabled?: boolean;
|
|
10
10
|
/** Provider probe cycle in seconds; 30-3600. */
|
|
11
11
|
pollIntervalSec?: number;
|
|
12
|
-
/** Pet bubble mode: always (refreshes each poll), change (only on value change), off. */
|
|
13
|
-
bubbleMode?: string;
|
|
14
12
|
/** Ledger retention in local days. */
|
|
15
13
|
retainDays?: number;
|
|
16
14
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-usage",
|
|
3
|
-
"description": "Usage statistics plugin for the dsh web GUI: per-provider balance and coding-plan quota detection plus a live token usage ledger, with
|
|
4
|
-
"version": "0.3.
|
|
3
|
+
"description": "Usage statistics plugin for the dsh web GUI: per-provider balance and coding-plan quota detection plus a live token usage ledger, with the current session provider's today usage on the sidebar entry",
|
|
4
|
+
"version": "0.3.24",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -66,11 +66,12 @@
|
|
|
66
66
|
"@deepseek-ai/dsh-llm": "^0.1.5-rc.1",
|
|
67
67
|
"@deepseek-ai/dsh-session": "^0.1.5-rc.1",
|
|
68
68
|
"@deepseek-ai/dsh-settings": "^0.1.5-rc.1",
|
|
69
|
-
"@linxin666/dsh-pet": "0.3.
|
|
69
|
+
"@linxin666/dsh-pet": "0.3.24",
|
|
70
70
|
"@testing-library/dom": "^10.4.1",
|
|
71
71
|
"@testing-library/react": "^16.3.2",
|
|
72
72
|
"@types/node": "^22.20.0",
|
|
73
73
|
"@types/react": "~18.3.1",
|
|
74
|
+
"@types/react-dom": "^18.3.5",
|
|
74
75
|
"jsdom": "29.1.1",
|
|
75
76
|
"react": "^18.2.0",
|
|
76
77
|
"react-dom": "^18.2.0",
|
|
@@ -22,7 +22,6 @@ import type { ObservedSpendView, ProviderSnapshotView, UsageOverviewView, UsageP
|
|
|
22
22
|
export interface UsageSettings {
|
|
23
23
|
enabled?: boolean
|
|
24
24
|
pollIntervalSec?: number
|
|
25
|
-
bubbleMode?: string
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
/** The registration-side face the section's slot entry injects. */
|
|
@@ -401,7 +400,7 @@ function totalOf(totals: UsageTokenTotals): number {
|
|
|
401
400
|
|
|
402
401
|
/**
|
|
403
402
|
* The Token 银行 card: the DeepSeek official family's retained-ledger usage
|
|
404
|
-
* minted onto the whale-yuan note at
|
|
403
|
+
* minted onto the whale-yuan note at 1,000,000 tokens per whale yuan. The window
|
|
405
404
|
* prefers the host's whole-ledger aggregate and falls back to the 30-day
|
|
406
405
|
* trend when an older host serves no `all`; the spend line prefers the
|
|
407
406
|
* official balance watch and falls back to the fold-time estimate; the
|
|
@@ -541,7 +540,6 @@ function SettingsRow(props: {
|
|
|
541
540
|
}): ReactNode {
|
|
542
541
|
const { settings, snapshot, value } = props
|
|
543
542
|
const disabled = snapshot === undefined || !snapshot.writable
|
|
544
|
-
const bubbleMode = typeof value.bubbleMode === 'string' && ['always', 'change', 'off'].includes(value.bubbleMode) ? value.bubbleMode : 'always'
|
|
545
543
|
return (
|
|
546
544
|
<div className={styles.card} data-dsh-part="settings-row">
|
|
547
545
|
<span className={styles.cardTitle}>{t('usage.config.title')}</span>
|
|
@@ -569,18 +567,6 @@ function SettingsRow(props: {
|
|
|
569
567
|
}}
|
|
570
568
|
/>
|
|
571
569
|
</label>
|
|
572
|
-
<label className={styles.settingItem}>
|
|
573
|
-
{t('usage.config.bubbleMode')}
|
|
574
|
-
<select
|
|
575
|
-
value={bubbleMode}
|
|
576
|
-
disabled={disabled}
|
|
577
|
-
onChange={(event) => { void settings.set('bubbleMode', event.target.value) }}
|
|
578
|
-
>
|
|
579
|
-
<option value="always">{t('usage.config.bubbleMode.always')}</option>
|
|
580
|
-
<option value="change">{t('usage.config.bubbleMode.change')}</option>
|
|
581
|
-
<option value="off">{t('usage.config.bubbleMode.off')}</option>
|
|
582
|
-
</select>
|
|
583
|
-
</label>
|
|
584
570
|
</div>
|
|
585
571
|
</div>
|
|
586
572
|
)
|