@linxin666/dsh-usage 0.4.3 → 0.4.5
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 +21 -5
- package/README.zh.md +21 -5
- package/lib/client.js +466 -32
- package/lib/client.js.map +1 -1
- package/lib/index.js +434 -23
- package/lib/types/client/UsageSectionCard.d.ts +7 -0
- package/lib/types/client/locales.d.ts +8 -0
- package/lib/types/core/adapters.d.ts +28 -2
- package/lib/types/core/holidays.d.ts +25 -0
- package/lib/types/core/ledger.d.ts +8 -0
- package/lib/types/core/pricing.d.ts +34 -6
- package/lib/types/core/types.d.ts +26 -5
- package/lib/types/host/routes.d.ts +11 -0
- package/lib/types/host/usage-service.d.ts +13 -1
- package/package.json +14 -14
- package/src/client/UsageSectionCard.tsx +174 -26
- package/src/client/index.ts +10 -2
- package/src/client/locales.ts +16 -0
- package/src/client/usage.module.css +47 -0
- package/src/core/adapters.ts +34 -3
- package/src/core/holidays.ts +248 -0
- package/src/core/ledger.ts +14 -5
- package/src/core/pricing.ts +75 -15
- package/src/core/types.ts +28 -1
- package/src/host/routes.ts +31 -0
- package/src/host/usage-service.ts +145 -10
- package/src/index.ts +3 -3
- package/lib/types/host/run-guarded.d.ts +0 -39
- package/src/host/run-guarded.ts +0 -76
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chinese public-holiday dates, Beijing calendar days, as `YYYY-MM-DD`.
|
|
3
|
+
*
|
|
4
|
+
* The DeepSeek peak rule bills a public holiday off-peak in full, and the
|
|
5
|
+
* State Council publishes these dates per year, so they cannot be derived
|
|
6
|
+
* from a rule. This module is that data; `pricing.ts` keeps the arithmetic.
|
|
7
|
+
*
|
|
8
|
+
* Adjusted workdays are deliberately absent: the provider counts the
|
|
9
|
+
* calendar day, so an adjusted Saturday or Sunday is off-peak exactly like
|
|
10
|
+
* any other weekend and listing it would price it as peak.
|
|
11
|
+
*
|
|
12
|
+
* Source: the yearly schedules in bastengao/chinese-holidays-node
|
|
13
|
+
* (`data/<year>.json`, MIT). Only the `holiday` entries are taken; that
|
|
14
|
+
* package's runtime code (which fetches over the network) and its
|
|
15
|
+
* dependencies are not vendored.
|
|
16
|
+
*
|
|
17
|
+
* Coverage ends at the newest year below. A year with no entry is priced as
|
|
18
|
+
* if it had no holidays, which is the documented default of
|
|
19
|
+
* {@link PublicHolidayCalendar} in `pricing.ts`.
|
|
20
|
+
* @module @linxin666/dsh-usage/core/holidays
|
|
21
|
+
*/
|
|
22
|
+
/** Public-holiday dates keyed by Beijing calendar year. */
|
|
23
|
+
export declare const CHINESE_PUBLIC_HOLIDAYS: Readonly<Record<string, readonly string[]>>;
|
|
24
|
+
/** The newest year this table covers; later years carry no entry. */
|
|
25
|
+
export declare const HOLIDAY_TABLE_LAST_YEAR = 2026;
|
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
import { type UsageLedgerDocument, type UsageProviderSummary, type UsageTokenTotals } from './types.ts';
|
|
8
8
|
/** Local-date key (`YYYY-MM-DD`) for an epoch ms timestamp. */
|
|
9
9
|
export declare function localDateKey(ms: number): string;
|
|
10
|
+
/**
|
|
11
|
+
* Whether `key` is a real local-date key (`YYYY-MM-DD`). The shape check is
|
|
12
|
+
* round-tripped: an impossible date (9999-99-99) parses to NaN and a rollover
|
|
13
|
+
* (2026-02-30) lands on another day, so both would otherwise fold into a bogus,
|
|
14
|
+
* never-pruned bucket. Shared by the persisted-document reader and the day
|
|
15
|
+
* route's query gate.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isLedgerDateKey(key: string): boolean;
|
|
10
18
|
/** An empty ledger document. */
|
|
11
19
|
export declare function createLedgerDocument(): UsageLedgerDocument;
|
|
12
20
|
/**
|
|
@@ -4,10 +4,19 @@
|
|
|
4
4
|
* at fold time (the provider bills each request in the period the request ran
|
|
5
5
|
* in, so pricing at the fold is the honest estimate).
|
|
6
6
|
*
|
|
7
|
-
* Policy (api-docs.deepseek.com pricing page
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* Policy (api-docs.deepseek.com pricing page): peak hours are Beijing time
|
|
8
|
+
* Monday-Friday 09:00-12:00 and 14:00-18:00, excluding Chinese public
|
|
9
|
+
* holidays; every other hour is off-peak and billed at half the peak price.
|
|
10
|
+
* The published rule counts a calendar day, not the working schedule: a
|
|
11
|
+
* Saturday or Sunday is off-peak in full even on an adjusted workday, and a
|
|
12
|
+
* public holiday is off-peak in full even on a Monday-Friday. All prices
|
|
13
|
+
* here are CNY per million tokens.
|
|
14
|
+
*
|
|
15
|
+
* Public-holiday dates are not derivable from the rule, so the rule takes them as
|
|
16
|
+
* a `PublicHolidayCalendar`. The default is the published table in `./holidays.ts`,
|
|
17
|
+
* covering 2020 through ${HOLIDAY_TABLE_LAST_YEAR}; a caller with newer dates
|
|
18
|
+
* passes its own. A holiday weekday therefore prices off-peak, which is the half
|
|
19
|
+
* of the published rule that is easy to get wrong.
|
|
11
20
|
*
|
|
12
21
|
* The published rows are `deepseek-flash` (DeepSeek-V4.1-Flash) and
|
|
13
22
|
* `deepseek-v4-pro` (DeepSeek-V4-Pro-0813). The retired flash ids
|
|
@@ -17,13 +26,32 @@
|
|
|
17
26
|
* @module @linxin666/dsh-usage/core/pricing
|
|
18
27
|
*/
|
|
19
28
|
import type { UsageTokenTotals } from './types.ts';
|
|
29
|
+
/**
|
|
30
|
+
* Supplies the Chinese public-holiday dates the peak rule excludes. The dates
|
|
31
|
+
* are not derivable from any rule (the State Council publishes them per year),
|
|
32
|
+
* so this repository ships no table of its own: the default calendar is empty,
|
|
33
|
+
* and a caller that has real dates passes them in. A date is compared as a
|
|
34
|
+
* Beijing-time `YYYY-MM-DD` string, which is the granularity the published
|
|
35
|
+
* rule needs (a holiday is off-peak for the whole day, so times never matter).
|
|
36
|
+
*/
|
|
37
|
+
export interface PublicHolidayCalendar {
|
|
38
|
+
/** True when the Beijing-time calendar day `date` is a Chinese public holiday. */
|
|
39
|
+
isPublicHoliday(date: string): boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The default calendar: the published Chinese public-holiday table, so a holiday
|
|
43
|
+
* weekday prices off-peak instead of over-reporting at the peak rate. A year the
|
|
44
|
+
* table does not cover holds no dates and falls back to peak on weekdays, which
|
|
45
|
+
* is the safe direction to be wrong in: the estimate reads high rather than low.
|
|
46
|
+
*/
|
|
47
|
+
export declare const CN_PUBLIC_HOLIDAYS: PublicHolidayCalendar;
|
|
20
48
|
/**
|
|
21
49
|
* The DeepSeek billing period at `ms`, plus when it next flips. The clock is
|
|
22
50
|
* Beijing time regardless of the host timezone (UTC+8 has no DST, so a fixed
|
|
23
51
|
* shift is exact). `boundaryMs` is the instant the current period ends — the
|
|
24
52
|
* window's close while peaking, the next window's open otherwise.
|
|
25
53
|
*/
|
|
26
|
-
export declare function deepseekPeriodAt(ms: number): {
|
|
54
|
+
export declare function deepseekPeriodAt(ms: number, calendar?: PublicHolidayCalendar): {
|
|
27
55
|
peak: boolean;
|
|
28
56
|
boundaryMs: number;
|
|
29
57
|
};
|
|
@@ -33,4 +61,4 @@ export declare function deepseekPeriodAt(ms: number): {
|
|
|
33
61
|
* row; ids from other providers never reach this function (the service gates
|
|
34
62
|
* by route family). Rounded to micro-CNY so the ledger stays readable.
|
|
35
63
|
*/
|
|
36
|
-
export declare function deepseekModelSpend(model: string, totals: Readonly<UsageTokenTotals>, atMs: number): number;
|
|
64
|
+
export declare function deepseekModelSpend(model: string, totals: Readonly<UsageTokenTotals>, atMs: number, calendar?: PublicHolidayCalendar): number;
|
|
@@ -52,6 +52,24 @@ export interface UsageDaySummary {
|
|
|
52
52
|
date: string;
|
|
53
53
|
totals: UsageTokenTotals;
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* One local day aggregated per provider and model. The overview serves today
|
|
57
|
+
* through this shape and the day route serves any retained day with it, so a
|
|
58
|
+
* day the user picks in the browser and the live today card are the same
|
|
59
|
+
* document.
|
|
60
|
+
*/
|
|
61
|
+
export interface UsageDayView {
|
|
62
|
+
/** Local-date key `YYYY-MM-DD`. */
|
|
63
|
+
date: string;
|
|
64
|
+
totals: UsageTokenTotals;
|
|
65
|
+
/** Per-provider rows, heaviest by total tokens first. */
|
|
66
|
+
providers: UsageProviderSummary[];
|
|
67
|
+
}
|
|
68
|
+
/** The day route's response body. */
|
|
69
|
+
export interface UsageDayResponse {
|
|
70
|
+
ok: true;
|
|
71
|
+
day: UsageDayView;
|
|
72
|
+
}
|
|
55
73
|
/** A balance fact normalized from a provider probe. */
|
|
56
74
|
export interface BalanceView {
|
|
57
75
|
/** ISO 4217 code the provider bills in (`CNY`, `USD`, ...). */
|
|
@@ -139,13 +157,16 @@ export interface UsageOverviewView {
|
|
|
139
157
|
today?: UsageTokenTotals;
|
|
140
158
|
};
|
|
141
159
|
usage: {
|
|
142
|
-
today:
|
|
143
|
-
date: string;
|
|
144
|
-
totals: UsageTokenTotals;
|
|
145
|
-
providers: UsageProviderSummary[];
|
|
146
|
-
};
|
|
160
|
+
today: UsageDayView;
|
|
147
161
|
/** Last N local days ascending, including today. */
|
|
148
162
|
days: UsageDaySummary[];
|
|
163
|
+
/**
|
|
164
|
+
* Every retained local day that recorded usage, ascending — the day
|
|
165
|
+
* picker's option list, reaching past the trend cap to the whole
|
|
166
|
+
* retention window. Optional so an older host document still renders
|
|
167
|
+
* (the section then offers no picker).
|
|
168
|
+
*/
|
|
169
|
+
availableDays?: string[];
|
|
149
170
|
/**
|
|
150
171
|
* The same window aggregated per provider and model — the trend card's
|
|
151
172
|
* bar-chart data. Optional so an older host document still renders.
|
|
@@ -13,6 +13,17 @@ export declare const USAGE_API_PREFIX = "/api/dsh-usage";
|
|
|
13
13
|
* @returns the route.
|
|
14
14
|
*/
|
|
15
15
|
export declare function makeUsageOverviewRoute(ctx: Context, service: UsageService): WebRoute;
|
|
16
|
+
/**
|
|
17
|
+
* One retained day route: `GET /api/dsh-usage/day?date=YYYY-MM-DD` answers
|
|
18
|
+
* that local day's per-provider and per-model totals, so the section's day
|
|
19
|
+
* pickers can show a past day instead of only today. Same trust fence as the
|
|
20
|
+
* overview route, and a key that is not a real local date is refused with 400
|
|
21
|
+
* rather than folded into a bogus day.
|
|
22
|
+
* @param ctx - host context; may expose remoteWebUiPairing.
|
|
23
|
+
* @param service - the usage service.
|
|
24
|
+
* @returns the route.
|
|
25
|
+
*/
|
|
26
|
+
export declare function makeUsageDayRoute(ctx: Context, service: UsageService): WebRoute;
|
|
16
27
|
/**
|
|
17
28
|
* Manual refresh: forces one probe cycle now and answers with the fresh
|
|
18
29
|
* overview. Same trust fence as the overview route.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @module @linxin666/dsh-usage/host/usage-service
|
|
8
8
|
*/
|
|
9
9
|
import type { Context } from '@deepseek-ai/cordis';
|
|
10
|
-
import type { UsageOverviewView } from '../core/types.ts';
|
|
10
|
+
import type { UsageDayView, UsageOverviewView } from '../core/types.ts';
|
|
11
11
|
/**
|
|
12
12
|
* Persisted accrual state for the official DeepSeek family's real spend:
|
|
13
13
|
* consecutive observations of the official CNY balance, decreases counted
|
|
@@ -78,6 +78,14 @@ export declare class UsageService {
|
|
|
78
78
|
* a strip about nothing is noise, not information.
|
|
79
79
|
*/
|
|
80
80
|
private currentView;
|
|
81
|
+
/**
|
|
82
|
+
* One retained local day aggregated per provider and model, as the day
|
|
83
|
+
* route serves it. A day the ledger never held — pruned by retention, or
|
|
84
|
+
* simply without usage — reads as a zeroed day rather than an error, so a
|
|
85
|
+
* picker selection that aged out degrades to "no usage" instead of a broken
|
|
86
|
+
* panel. The route gates the key's shape before it gets here.
|
|
87
|
+
*/
|
|
88
|
+
day(dateKey: string): UsageDayView;
|
|
81
89
|
/** One local day aggregated per provider. */
|
|
82
90
|
private daySummary;
|
|
83
91
|
/**
|
|
@@ -153,4 +161,8 @@ export declare class UsageService {
|
|
|
153
161
|
private piAiProfile;
|
|
154
162
|
/** The DeepSeek official adapter's credential reference name. */
|
|
155
163
|
private deepseekApiKeyEnv;
|
|
164
|
+
/** The DeepSeek official adapter's configured baseURL. */
|
|
165
|
+
private deepseekBaseUrl;
|
|
166
|
+
/** The effective base URL for a provider route (pi-ai or deepseek). */
|
|
167
|
+
private routeBaseUrl;
|
|
156
168
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-usage",
|
|
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 a compact price glance card above the sidebar's Settings row",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.5",
|
|
5
5
|
"icon": "icon.svg",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"license": "Apache-2.0",
|
|
34
34
|
"dsh": {
|
|
35
35
|
"engines": {
|
|
36
|
-
"dsh": ">=0.
|
|
36
|
+
"dsh": ">=0.2.0-rc.2"
|
|
37
37
|
},
|
|
38
38
|
"bundle": {
|
|
39
39
|
"patch": "./cordis.patch.yml"
|
|
@@ -53,22 +53,22 @@
|
|
|
53
53
|
"@deepseek-ai/schemastery": "^3.18.4"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@deepseek-ai/dsh": ">=0.
|
|
56
|
+
"@deepseek-ai/dsh": ">=0.2.0-rc.2",
|
|
57
57
|
"react": "^18.2.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
61
|
-
"@deepseek-ai/dsh-credentials": "^0.
|
|
62
|
-
"@deepseek-ai/dsh-client-connection": "^0.
|
|
63
|
-
"@deepseek-ai/dsh-client-locale": "^0.
|
|
64
|
-
"@deepseek-ai/dsh-client-store": "^0.
|
|
65
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.
|
|
66
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.
|
|
67
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.
|
|
68
|
-
"@deepseek-ai/dsh-host-webserver": "^0.
|
|
69
|
-
"@deepseek-ai/dsh-llm": "^0.
|
|
70
|
-
"@deepseek-ai/dsh-session": "^0.
|
|
71
|
-
"@deepseek-ai/dsh-settings": "^0.
|
|
61
|
+
"@deepseek-ai/dsh-credentials": "^0.2.0-rc.2",
|
|
62
|
+
"@deepseek-ai/dsh-client-connection": "^0.2.0-rc.2",
|
|
63
|
+
"@deepseek-ai/dsh-client-locale": "^0.2.0-rc.2",
|
|
64
|
+
"@deepseek-ai/dsh-client-store": "^0.2.0-rc.2",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.2.0-rc.2",
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.2",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.2",
|
|
68
|
+
"@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.2",
|
|
69
|
+
"@deepseek-ai/dsh-llm": "^0.2.0-rc.2",
|
|
70
|
+
"@deepseek-ai/dsh-session": "^0.2.0-rc.2",
|
|
71
|
+
"@deepseek-ai/dsh-settings": "^0.2.0-rc.2",
|
|
72
72
|
"@testing-library/dom": "^10.4.1",
|
|
73
73
|
"@testing-library/react": "^16.3.2",
|
|
74
74
|
"@types/node": "^22.20.0",
|
|
@@ -16,7 +16,7 @@ import styles from './usage.module.css'
|
|
|
16
16
|
import { isDeepSeekProviderRoute } from '../core/adapters.ts'
|
|
17
17
|
import { deepseekPeriodAt } from '../core/pricing.ts'
|
|
18
18
|
import { deepseekVoucherData, drawVoucher, faceValue, formatDay, formatDenomination, loadVoucherArt } from './voucher.ts'
|
|
19
|
-
import type { ObservedSpendView, ProviderSnapshotView, UsageOverviewView, UsageProviderSummary, UsageTokenTotals, UsageWindowSummary } from '../core/types.ts'
|
|
19
|
+
import type { ObservedSpendView, ProviderSnapshotView, UsageDayView, UsageOverviewView, UsageProviderSummary, UsageTokenTotals, UsageWindowSummary } from '../core/types.ts'
|
|
20
20
|
|
|
21
21
|
/** The settings fields this section edits (immediate-apply semantics). */
|
|
22
22
|
export interface UsageSettings {
|
|
@@ -32,6 +32,12 @@ export interface UsageSectionFace {
|
|
|
32
32
|
poll: () => void
|
|
33
33
|
/** Force a host probe cycle now (resolves with the fresh overview). */
|
|
34
34
|
refresh: () => void
|
|
35
|
+
/**
|
|
36
|
+
* Fetch one retained local day (YYYY-MM-DD) from the host. The overview
|
|
37
|
+
* only carries today plus the list of recorded days, so a day the user picks
|
|
38
|
+
* is one small on-demand request.
|
|
39
|
+
*/
|
|
40
|
+
loadDay: (date: string) => Promise<UsageDayView>
|
|
35
41
|
/** The shared configuration form this section's settings row reads and writes. */
|
|
36
42
|
settings: ConfigForm<UsageSettings>
|
|
37
43
|
}
|
|
@@ -44,6 +50,12 @@ export interface UsageSectionProps extends UsageSectionFace {
|
|
|
44
50
|
/** Poll cadence while the section is open. */
|
|
45
51
|
const SECTION_POLL_MS = 10_000
|
|
46
52
|
|
|
53
|
+
/**
|
|
54
|
+
* The Token bank's default window: the whole retained ledger. It is a select
|
|
55
|
+
* value rather than a date, so it can never collide with a day key.
|
|
56
|
+
*/
|
|
57
|
+
const BANK_ALL_DAYS = 'all'
|
|
58
|
+
|
|
47
59
|
/** Compact token count: 12345 -> 12.3k, 1234567 -> 1.23M. */
|
|
48
60
|
export function formatTokens(value: number): string {
|
|
49
61
|
if (!Number.isFinite(value) || value <= 0) return '0'
|
|
@@ -149,13 +161,87 @@ function ProviderRow(props: { provider: ProviderSnapshotView; current?: string }
|
|
|
149
161
|
)
|
|
150
162
|
}
|
|
151
163
|
|
|
164
|
+
/** One day the pickers selected, as its fetch progresses. */
|
|
165
|
+
interface DayLoad {
|
|
166
|
+
status: 'loading' | 'ready' | 'error'
|
|
167
|
+
/** The served day, once the host answered. */
|
|
168
|
+
view?: UsageDayView
|
|
169
|
+
/** Transport error message, on a failed load. */
|
|
170
|
+
error?: string
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Fetch the day a picker selected, or stay idle for the default window. Today
|
|
175
|
+
* never fetches: the overview already carries it live, so switching back to
|
|
176
|
+
* today is instant and keeps counting with the poll. A superseded selection
|
|
177
|
+
* never lands (the effect cancels), and a failed load reports its own message
|
|
178
|
+
* instead of leaving the card on a stale day.
|
|
179
|
+
*/
|
|
180
|
+
function useDayView(loadDay: (date: string) => Promise<UsageDayView>, date: string | undefined, today: string | undefined): DayLoad | undefined {
|
|
181
|
+
const [load, setLoad] = useState<DayLoad | undefined>(undefined)
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (date === undefined || date === today) {
|
|
184
|
+
setLoad((previous) => (previous === undefined ? previous : undefined))
|
|
185
|
+
return undefined
|
|
186
|
+
}
|
|
187
|
+
let cancelled = false
|
|
188
|
+
setLoad({ status: 'loading' })
|
|
189
|
+
loadDay(date).then(
|
|
190
|
+
(view) => { if (!cancelled) setLoad({ status: 'ready', view }) },
|
|
191
|
+
(error: unknown) => { if (!cancelled) setLoad({ status: 'error', error: error instanceof Error ? error.message : String(error) }) },
|
|
192
|
+
)
|
|
193
|
+
return () => { cancelled = true }
|
|
194
|
+
}, [date, today, loadDay])
|
|
195
|
+
return load
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The day picker: today plus every retained day that recorded usage, newest
|
|
200
|
+
* first (the host serves the list ascending). Without that list (an older host
|
|
201
|
+
* document) no picker renders and the card stays on its default window.
|
|
202
|
+
*/
|
|
203
|
+
function DayPicker(props: {
|
|
204
|
+
value: string
|
|
205
|
+
options: string[]
|
|
206
|
+
today: string
|
|
207
|
+
/** An extra leading option; the Token bank passes its whole-ledger window. */
|
|
208
|
+
leading?: { value: string; label: string }
|
|
209
|
+
onSelect: (date: string) => void
|
|
210
|
+
/** Namespaces the element id, so the two tabs keep one control each. */
|
|
211
|
+
part: string
|
|
212
|
+
}): ReactNode {
|
|
213
|
+
const { value, options, today, leading, onSelect, part } = props
|
|
214
|
+
if (options.length === 0) return null
|
|
215
|
+
return (
|
|
216
|
+
<div className={styles.dayPicker}>
|
|
217
|
+
<label className={styles.dayPickerLabel} htmlFor={'dsh-usage-day-' + part}>{t('usage.day.label')}</label>
|
|
218
|
+
<select
|
|
219
|
+
id={'dsh-usage-day-' + part}
|
|
220
|
+
className={styles.daySelect}
|
|
221
|
+
value={value}
|
|
222
|
+
onChange={(event) => { onSelect(event.target.value) }}
|
|
223
|
+
>
|
|
224
|
+
{leading !== undefined && <option value={leading.value}>{leading.label}</option>}
|
|
225
|
+
{options.map((date) => (
|
|
226
|
+
<option key={date} value={date}>{date === today ? t('usage.day.today') : date}</option>
|
|
227
|
+
))}
|
|
228
|
+
</select>
|
|
229
|
+
</div>
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
152
233
|
/** The section component; the slot merges the face into these props. */
|
|
153
234
|
export function UsageSectionCard(props: UsageSectionProps): ReactNode {
|
|
154
|
-
const { store, poll, refresh, settings } = props
|
|
235
|
+
const { store, poll, refresh, loadDay, settings } = props
|
|
155
236
|
const ui = useSyncExternalStore(store.subscribe, store.getSnapshot)
|
|
156
237
|
const settingsSnapshot = settings.getSnapshot()
|
|
157
238
|
const settingsValue = settingsSnapshot.value ?? {}
|
|
158
239
|
const [tab, setTab] = useState<'usage' | 'plans' | 'bank'>('usage')
|
|
240
|
+
// Undefined means "the default window of this tab": today on the usage tab,
|
|
241
|
+
// the whole retained ledger in the Token bank. A date switches that tab to
|
|
242
|
+
// one retained day instead.
|
|
243
|
+
const [usageDay, setUsageDay] = useState<string | undefined>(undefined)
|
|
244
|
+
const [bankDay, setBankDay] = useState<string | undefined>(undefined)
|
|
159
245
|
const [refreshing, setRefreshing] = useState(false)
|
|
160
246
|
// The enable checkbox writes through the shared form, so subscribing here
|
|
161
247
|
// keeps the flag below live: the form republishes the Host's accepted value,
|
|
@@ -195,6 +281,31 @@ export function UsageSectionCard(props: UsageSectionProps): ReactNode {
|
|
|
195
281
|
}, [poll, enabled])
|
|
196
282
|
|
|
197
283
|
const snapshot = ui.snapshot
|
|
284
|
+
const todayDate = snapshot?.usage.today.date
|
|
285
|
+
// Every retained day that recorded usage, newest first, plus today so the
|
|
286
|
+
// default window is always one of the options even before it has usage.
|
|
287
|
+
const dayOptions = snapshot === null
|
|
288
|
+
? []
|
|
289
|
+
: [...new Set([todayDate, ...(snapshot.usage.availableDays ?? [])]
|
|
290
|
+
.filter((date): date is string => date !== undefined))].sort().reverse()
|
|
291
|
+
// A selection retention no longer holds falls back to the tab's default
|
|
292
|
+
// window, so a picker can never keep displaying a day the host dropped.
|
|
293
|
+
const usageSelection = usageDay !== undefined && dayOptions.includes(usageDay) ? usageDay : undefined
|
|
294
|
+
const bankSelection = bankDay !== undefined && dayOptions.includes(bankDay) ? bankDay : undefined
|
|
295
|
+
const selectedDay = usageSelection ?? todayDate
|
|
296
|
+
const isToday = selectedDay === todayDate
|
|
297
|
+
const usageDayLoad = useDayView(loadDay, usageSelection, todayDate)
|
|
298
|
+
const bankDayLoad = useDayView(loadDay, bankSelection, todayDate)
|
|
299
|
+
// A selected day rides its own fetch; the default windows ride the overview.
|
|
300
|
+
const day = isToday ? snapshot?.usage.today : usageDayLoad?.view
|
|
301
|
+
const bankDayView = bankSelection === undefined
|
|
302
|
+
? undefined
|
|
303
|
+
: bankSelection === todayDate ? snapshot?.usage.today : bankDayLoad?.view
|
|
304
|
+
const bankWindow = bankSelection === undefined
|
|
305
|
+
? snapshot?.usage.all ?? snapshot?.usage.range
|
|
306
|
+
: bankDayView === undefined
|
|
307
|
+
? undefined
|
|
308
|
+
: { from: bankDayView.date, to: bankDayView.date, totals: bankDayView.totals, providers: bankDayView.providers }
|
|
198
309
|
|
|
199
310
|
const onRefresh = (): void => {
|
|
200
311
|
setRefreshing(true)
|
|
@@ -227,10 +338,11 @@ export function UsageSectionCard(props: UsageSectionProps): ReactNode {
|
|
|
227
338
|
const current = snapshot.current
|
|
228
339
|
const currentProvider = snapshot.providers.find((provider) => provider.provider === current.provider)
|
|
229
340
|
// Peak-period line: only when the official DeepSeek family is in play this
|
|
230
|
-
// session (the current route, or spend recorded under one today).
|
|
341
|
+
// session (the current route, or spend recorded under one today). It
|
|
342
|
+
// describes the pricing window now, so a past day's card drops it.
|
|
231
343
|
const deepseekPeriod = deepseekPeriodAt(Date.now())
|
|
232
|
-
const deepseekVisible = (current.provider !== undefined && isDeepSeekProviderRoute(current.provider))
|
|
233
|
-
|| snapshot.usage.today.providers.some((row) => isDeepSeekProviderRoute(row.provider))
|
|
344
|
+
const deepseekVisible = isToday && ((current.provider !== undefined && isDeepSeekProviderRoute(current.provider))
|
|
345
|
+
|| snapshot.usage.today.providers.some((row) => isDeepSeekProviderRoute(row.provider)))
|
|
234
346
|
// Plans tab: only configured routes with a real coding-plan/subscription
|
|
235
347
|
// adapter (planSupported; an older host without the flag falls back to "has
|
|
236
348
|
// a plan fact"). Balance-only providers (DeepSeek, ZenMux, ...) and
|
|
@@ -268,24 +380,35 @@ export function UsageSectionCard(props: UsageSectionProps): ReactNode {
|
|
|
268
380
|
{tab === 'usage' && (
|
|
269
381
|
<>
|
|
270
382
|
<div className={styles.card} data-dsh-part="today-card">
|
|
271
|
-
<span className={styles.cardTitle}>{t('usage.today')}</span>
|
|
383
|
+
<span className={styles.cardTitle}>{isToday ? t('usage.today') : t('usage.day.title', { date: selectedDay ?? '' })}</span>
|
|
384
|
+
<DayPicker
|
|
385
|
+
part="usage"
|
|
386
|
+
value={selectedDay ?? ''}
|
|
387
|
+
options={dayOptions}
|
|
388
|
+
today={todayDate ?? ''}
|
|
389
|
+
onSelect={(date) => { setUsageDay(date === todayDate ? undefined : date) }}
|
|
390
|
+
/>
|
|
272
391
|
{deepseekVisible && (
|
|
273
392
|
<span className={styles.muted} data-dsh-part="peak-status">
|
|
274
393
|
{t(deepseekPeriod.peak ? 'usage.peak.on' : 'usage.peak.off', { time: formatClock(deepseekPeriod.boundaryMs) })}
|
|
275
394
|
</span>
|
|
276
395
|
)}
|
|
277
|
-
{
|
|
278
|
-
? <span className={styles.
|
|
279
|
-
:
|
|
280
|
-
|
|
396
|
+
{usageDayLoad?.status === 'error'
|
|
397
|
+
? <span className={styles.errorLine}>{t('usage.day.error', { error: usageDayLoad.error ?? '' })}</span>
|
|
398
|
+
: day === undefined
|
|
399
|
+
? <span className={styles.muted}>{t('usage.day.loading')}</span>
|
|
400
|
+
: day.totals.calls === 0
|
|
401
|
+
? <span className={styles.muted}>{isToday ? t('usage.noData') : t('usage.day.empty')}</span>
|
|
402
|
+
: <TotalsRow totals={day.totals} />}
|
|
403
|
+
{(day?.totals.cost ?? 0) > 0 && (
|
|
281
404
|
<div className={styles.providerRow} data-dsh-part="today-cost">
|
|
282
|
-
<span className={styles.providerName}>{t('usage.today.cost')}</span>
|
|
283
|
-
<span className={styles.providerTokens}>{formatCost(
|
|
405
|
+
<span className={styles.providerName}>{isToday ? t('usage.today.cost') : t('usage.day.cost')}</span>
|
|
406
|
+
<span className={styles.providerTokens}>{formatCost(day?.totals.cost ?? 0)}</span>
|
|
284
407
|
</div>
|
|
285
408
|
)}
|
|
286
|
-
{
|
|
409
|
+
{(day?.providers.length ?? 0) > 0 && (
|
|
287
410
|
<div data-dsh-part="provider-list">
|
|
288
|
-
{
|
|
411
|
+
{(day?.providers ?? []).map((row) => (
|
|
289
412
|
<div key={row.provider} className={styles.providerRow}>
|
|
290
413
|
<span className={styles.providerName}>
|
|
291
414
|
{snapshot.providers.find((provider) => provider.provider === row.provider)?.displayName ?? row.provider}
|
|
@@ -332,7 +455,27 @@ export function UsageSectionCard(props: UsageSectionProps): ReactNode {
|
|
|
332
455
|
: planProviders.map((provider) => <PlanCard key={provider.provider} provider={provider} current={current.provider} />)
|
|
333
456
|
)}
|
|
334
457
|
|
|
335
|
-
{tab === 'bank' &&
|
|
458
|
+
{tab === 'bank' && (
|
|
459
|
+
<>
|
|
460
|
+
<DayPicker
|
|
461
|
+
part="bank"
|
|
462
|
+
value={bankSelection ?? BANK_ALL_DAYS}
|
|
463
|
+
options={dayOptions}
|
|
464
|
+
today={todayDate ?? ''}
|
|
465
|
+
leading={{ value: BANK_ALL_DAYS, label: t('usage.bank.windowAll') }}
|
|
466
|
+
onSelect={(date) => { setBankDay(date === BANK_ALL_DAYS ? undefined : date) }}
|
|
467
|
+
/>
|
|
468
|
+
<VoucherCard
|
|
469
|
+
window={bankWindow}
|
|
470
|
+
// The observed-spend watch accrues from its own first balance
|
|
471
|
+
// reading, so it describes the whole window only: a single day's
|
|
472
|
+
// voucher falls back to that day's fold-time estimate.
|
|
473
|
+
observedSpend={bankSelection === undefined ? snapshot.usage.observedSpend : undefined}
|
|
474
|
+
loading={bankSelection !== undefined && bankDayLoad?.status === 'loading'}
|
|
475
|
+
{...(bankDayLoad?.status === 'error' ? { error: bankDayLoad.error ?? '' } : {})}
|
|
476
|
+
/>
|
|
477
|
+
</>
|
|
478
|
+
)}
|
|
336
479
|
</div>
|
|
337
480
|
)
|
|
338
481
|
}
|
|
@@ -408,15 +551,16 @@ function totalOf(totals: UsageTokenTotals): number {
|
|
|
408
551
|
|
|
409
552
|
/**
|
|
410
553
|
* The Token 银行 card: the DeepSeek official family's retained-ledger usage
|
|
411
|
-
* minted onto the whale-yuan note at 1,000,000 tokens per whale yuan. The
|
|
412
|
-
*
|
|
413
|
-
* trend when an older host serves no `all
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
* section
|
|
554
|
+
* minted onto the whale-yuan note at 1,000,000 tokens per whale yuan. The
|
|
555
|
+
* window is the whole retained ledger by default (the host's aggregate,
|
|
556
|
+
* falling back to the 30-day trend when an older host serves no `all`) or the
|
|
557
|
+
* single day the user picked; the spend line prefers the official balance
|
|
558
|
+
* watch (whole window only) and falls back to the fold-time estimate; the
|
|
559
|
+
* artwork draw failure degrades to an error line and never takes the section
|
|
560
|
+
* down.
|
|
417
561
|
*/
|
|
418
|
-
function VoucherCard(props: { window?: UsageWindowSummary; observedSpend?: ObservedSpendView }): ReactNode {
|
|
419
|
-
const { window: ledger, observedSpend } = props
|
|
562
|
+
function VoucherCard(props: { window?: UsageWindowSummary; observedSpend?: ObservedSpendView; loading?: boolean; error?: string }): ReactNode {
|
|
563
|
+
const { window: ledger, observedSpend, loading, error } = props
|
|
420
564
|
const data = deepseekVoucherData(ledger)
|
|
421
565
|
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
|
422
566
|
const [drawError, setDrawError] = useState<string | undefined>(undefined)
|
|
@@ -478,9 +622,13 @@ function VoucherCard(props: { window?: UsageWindowSummary; observedSpend?: Obser
|
|
|
478
622
|
return (
|
|
479
623
|
<div className={styles.card} data-dsh-part="bank-card">
|
|
480
624
|
<span className={styles.cardTitle}>{t('usage.bank.title')}</span>
|
|
481
|
-
{
|
|
482
|
-
? <span className={styles.
|
|
483
|
-
:
|
|
625
|
+
{error !== undefined
|
|
626
|
+
? <span className={styles.errorLine}>{t('usage.day.error', { error })}</span>
|
|
627
|
+
: loading === true
|
|
628
|
+
? <span className={styles.muted}>{t('usage.day.loading')}</span>
|
|
629
|
+
: data === undefined
|
|
630
|
+
? <span className={styles.muted}>{t('usage.bank.noUsage')}</span>
|
|
631
|
+
: <>
|
|
484
632
|
<span className={styles.muted}>{t('usage.bank.hint')}</span>
|
|
485
633
|
<div className={styles.voucherPreview} data-dsh-part="voucher-preview">
|
|
486
634
|
<canvas ref={canvasRef} aria-label={t('usage.bank.title')} />
|
package/src/client/index.ts
CHANGED
|
@@ -22,11 +22,13 @@ import { UsageSectionCard, type UsageSectionFace, type UsageSettings } from './U
|
|
|
22
22
|
import { mountUsageFootCard, openUsageSettings } from './foot-card-mount.tsx'
|
|
23
23
|
import { NS, en, zh, t } from './locales.ts'
|
|
24
24
|
import { createServedEntryForm } from './settings-entry-form.ts'
|
|
25
|
-
import type { UsageOverviewView } from '../core/types.ts'
|
|
25
|
+
import type { UsageDayResponse, UsageDayView, UsageOverviewView } from '../core/types.ts'
|
|
26
26
|
|
|
27
27
|
/** The host usage API as the browser sees it (same-origin JSON endpoints). */
|
|
28
28
|
interface UsageHttpApi {
|
|
29
29
|
overview(): Promise<UsageOverviewView>
|
|
30
|
+
/** One retained local day (YYYY-MM-DD), the day pickers' detail fetch. */
|
|
31
|
+
day(date: string): Promise<UsageDayView>
|
|
30
32
|
refresh(): Promise<UsageOverviewView>
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -47,6 +49,7 @@ const usageApi: UsageHttpApi = {
|
|
|
47
49
|
// `<base href="./">`, so a sub-path deployment resolves these against its
|
|
48
50
|
// entry directory instead of escaping to the origin root.
|
|
49
51
|
overview: () => usageFetch('api/dsh-usage/overview', 'GET'),
|
|
52
|
+
day: async (date: string) => (await usageFetch<UsageDayResponse>('api/dsh-usage/day?date=' + encodeURIComponent(date), 'GET')).day,
|
|
50
53
|
refresh: () => usageFetch('api/dsh-usage/refresh', 'POST'),
|
|
51
54
|
}
|
|
52
55
|
|
|
@@ -156,7 +159,12 @@ export function apply(ctx: ClientContext): void {
|
|
|
156
159
|
})
|
|
157
160
|
}
|
|
158
161
|
|
|
159
|
-
|
|
162
|
+
// The day pickers fetch their own day: the overview carries today (always
|
|
163
|
+
// live) plus the list of recorded days, and one selected day is a single
|
|
164
|
+
// small local request the 10 s poll has no reason to repeat.
|
|
165
|
+
const loadDay = (date: string): Promise<UsageDayView> => usageApi.day(date)
|
|
166
|
+
|
|
167
|
+
const face = (): UsageSectionFace => ({ store, poll, refresh, loadDay, settings: settingsForm })
|
|
160
168
|
|
|
161
169
|
// Sidebar foot card: the compact usage glance seated below the shell's
|
|
162
170
|
// Settings row. It shares the section's store and poll path (the sequence
|
package/src/client/locales.ts
CHANGED
|
@@ -22,6 +22,13 @@ export const zh = {
|
|
|
22
22
|
'usage.current': '当前',
|
|
23
23
|
'usage.today': '今日用量',
|
|
24
24
|
'usage.today.cost': '今日消费(估算)',
|
|
25
|
+
'usage.day.label': '查看日期',
|
|
26
|
+
'usage.day.today': '今日',
|
|
27
|
+
'usage.day.title': '{date} 用量',
|
|
28
|
+
'usage.day.cost': '当日消费(估算)',
|
|
29
|
+
'usage.day.empty': '该日没有用量记录',
|
|
30
|
+
'usage.day.loading': '正在加载该日用量…',
|
|
31
|
+
'usage.day.error': '该日用量加载失败:{error}',
|
|
25
32
|
'usage.peak.on': 'DeepSeek 高峰时段:计价 ×2,{time} 结束',
|
|
26
33
|
'usage.peak.off': 'DeepSeek 空闲时段:计价为高峰一半,{time} 进入高峰',
|
|
27
34
|
'usage.calls': '{n} 次调用',
|
|
@@ -56,6 +63,7 @@ export const zh = {
|
|
|
56
63
|
'usage.bank.spend.observed': '官方余额实测花费 ¥{cost}(自 {since} 起)',
|
|
57
64
|
'usage.bank.spend.estimated': '消费估算:约 ¥{cost}',
|
|
58
65
|
'usage.bank.window': '统计窗口 {from} ~ {to}',
|
|
66
|
+
'usage.bank.windowAll': '全部保留期',
|
|
59
67
|
'usage.bank.save': '保存图片',
|
|
60
68
|
'usage.bank.share': '分享',
|
|
61
69
|
'usage.bank.drawError': '票券生成失败:{error}',
|
|
@@ -81,6 +89,13 @@ export const en: Record<UsageKey, string> = {
|
|
|
81
89
|
'usage.current': 'Current',
|
|
82
90
|
'usage.today': 'Today',
|
|
83
91
|
'usage.today.cost': 'Today spend (estimated)',
|
|
92
|
+
'usage.day.label': 'Day',
|
|
93
|
+
'usage.day.today': 'Today',
|
|
94
|
+
'usage.day.title': 'Usage · {date}',
|
|
95
|
+
'usage.day.cost': 'Day spend (estimated)',
|
|
96
|
+
'usage.day.empty': 'No usage recorded on this day',
|
|
97
|
+
'usage.day.loading': 'Loading that day…',
|
|
98
|
+
'usage.day.error': 'Failed to load that day: {error}',
|
|
84
99
|
'usage.peak.on': 'DeepSeek peak hours: 2x pricing, ends {time}',
|
|
85
100
|
'usage.peak.off': 'DeepSeek off-peak: half of peak pricing, peak returns {time}',
|
|
86
101
|
'usage.calls': '{n} calls',
|
|
@@ -115,6 +130,7 @@ export const en: Record<UsageKey, string> = {
|
|
|
115
130
|
'usage.bank.spend.observed': 'Spent ¥{cost} observed on the official balance (watching since {since})',
|
|
116
131
|
'usage.bank.spend.estimated': 'Estimated spend: about ¥{cost}',
|
|
117
132
|
'usage.bank.window': 'Window {from} - {to}',
|
|
133
|
+
'usage.bank.windowAll': 'All retained days',
|
|
118
134
|
'usage.bank.save': 'Save image',
|
|
119
135
|
'usage.bank.share': 'Share',
|
|
120
136
|
'usage.bank.drawError': 'Failed to render the voucher: {error}',
|