@linxin666/dsh-usage 0.4.8 → 0.4.9
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 +3 -3
- package/README.zh.md +3 -3
- package/lib/client.js +256 -110
- package/lib/client.js.map +1 -1
- package/lib/index.js +60 -6
- package/lib/types/client/UsageSectionCard.d.ts +5 -1
- package/lib/types/client/foot-card-mount.d.ts +52 -8
- package/lib/types/client/locales.d.ts +2 -0
- package/lib/types/client/voucher.d.ts +20 -25
- package/lib/types/core/spend.d.ts +46 -0
- package/lib/types/core/types.d.ts +7 -0
- package/lib/types/host/loopback.d.ts +21 -0
- package/lib/types/host/usage-service.d.ts +7 -0
- package/lib/types/index.d.ts +11 -0
- package/package.json +14 -14
- package/src/client/UsageFootCard.tsx +3 -2
- package/src/client/UsageSectionCard.tsx +50 -21
- package/src/client/foot-card-mount.tsx +161 -37
- package/src/client/index.ts +12 -3
- package/src/client/locales.ts +12 -8
- package/src/client/voucher.ts +20 -30
- package/src/core/spend.ts +62 -0
- package/src/core/types.ts +8 -0
- package/src/host/loopback.ts +32 -0
- package/src/host/pair-access.ts +26 -3
- package/src/host/usage-service.ts +9 -0
- package/src/index.ts +17 -0
package/lib/index.js
CHANGED
|
@@ -118,7 +118,7 @@ function mountOnce(packageName, fn) {
|
|
|
118
118
|
return mount;
|
|
119
119
|
}
|
|
120
120
|
//#endregion
|
|
121
|
-
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-brand@0.2.1-rc.
|
|
121
|
+
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-brand@0.2.1-rc.3_@deepseek-ai+cordis@4.0.5/node_modules/@deepseek-ai/dsh-brand/lib/index.js
|
|
122
122
|
/**
|
|
123
123
|
* Duplicate-install-safe nominal primitive helpers.
|
|
124
124
|
*
|
|
@@ -141,7 +141,7 @@ function brandString(value) {
|
|
|
141
141
|
return value;
|
|
142
142
|
}
|
|
143
143
|
//#endregion
|
|
144
|
-
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-credentials@0.2.1-rc.
|
|
144
|
+
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-credentials@0.2.1-rc.3_@deepseek-ai+cordis@4.0.5/node_modules/@deepseek-ai/dsh-credentials/lib/index.js
|
|
145
145
|
/**
|
|
146
146
|
* Service Definition for the credential-reference capability seam (`ctx.credentials`). Settings and composition files carry
|
|
147
147
|
* *references* to secrets — environment-variable names — while providers own
|
|
@@ -1351,6 +1351,30 @@ function deserializeLedger(value) {
|
|
|
1351
1351
|
return doc;
|
|
1352
1352
|
}
|
|
1353
1353
|
//#endregion
|
|
1354
|
+
//#region src/core/spend.ts
|
|
1355
|
+
/**
|
|
1356
|
+
* The rate used when the row configures none: a plain default, so an
|
|
1357
|
+
* unconfigured display still has a defined美元 figure instead of a blank card.
|
|
1358
|
+
* The plugin never fetches a live rate (its host routes are loopback-fenced and
|
|
1359
|
+
* its browser half has no network), so there is no fetch that could fail and
|
|
1360
|
+
* leave the card empty.
|
|
1361
|
+
*/
|
|
1362
|
+
const DEFAULT_CNY_PER_USD = 7.2;
|
|
1363
|
+
/**
|
|
1364
|
+
* Normalize a configured display.
|
|
1365
|
+
* An unknown currency reads as CNY, and a rate that is not a positive finite
|
|
1366
|
+
* number falls back to the default rate — the display must never be able to
|
|
1367
|
+
* blank the card.
|
|
1368
|
+
* @param input - the configured currency and rate, if any.
|
|
1369
|
+
* @returns a display that can always render.
|
|
1370
|
+
*/
|
|
1371
|
+
function resolveSpendDisplay(input) {
|
|
1372
|
+
return {
|
|
1373
|
+
currency: input?.currency === "USD" ? "USD" : "CNY",
|
|
1374
|
+
cnyPerUsd: typeof input?.cnyPerUsd === "number" && Number.isFinite(input.cnyPerUsd) && input.cnyPerUsd > 0 ? input.cnyPerUsd : DEFAULT_CNY_PER_USD
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
//#endregion
|
|
1354
1378
|
//#region src/host/usage-service.ts
|
|
1355
1379
|
/**
|
|
1356
1380
|
* The dsh-usage host service: folds live session usage into the persistent
|
|
@@ -1555,6 +1579,7 @@ var UsageService = class {
|
|
|
1555
1579
|
const all = summarizeDays(allKeys.map((key) => this.ledger.days[key] ?? {}));
|
|
1556
1580
|
return {
|
|
1557
1581
|
updatedAt: Date.now(),
|
|
1582
|
+
display: resolveSpendDisplay(this.options.display),
|
|
1558
1583
|
providers,
|
|
1559
1584
|
current: this.currentView(),
|
|
1560
1585
|
usage: {
|
|
@@ -2073,8 +2098,31 @@ function isLoopbackRequest(request) {
|
|
|
2073
2098
|
*/
|
|
2074
2099
|
function isPairedOrLoopbackAllowed(ctx, request) {
|
|
2075
2100
|
if (isLoopbackRequest(request)) return true;
|
|
2076
|
-
|
|
2077
|
-
|
|
2101
|
+
return lookupPairing(ctx)?.isPairedDevice(request) === true;
|
|
2102
|
+
}
|
|
2103
|
+
/**
|
|
2104
|
+
* The pairing service, or undefined when this composition does not expose it.
|
|
2105
|
+
*
|
|
2106
|
+
* Two lookups exist because the fence is shared with test harnesses that hand
|
|
2107
|
+
* over a bare object: a real cordis context resolves services through `get`, a
|
|
2108
|
+
* harness may carry the property directly. `get(name, false)` is the
|
|
2109
|
+
* non-strict form, so an unloaded service answers undefined rather than
|
|
2110
|
+
* throwing. Reading an un-injected service as a PROPERTY throws instead
|
|
2111
|
+
* (`cannot get property "remoteWebUiPairing" without inject`); a trust fence
|
|
2112
|
+
* must answer "not paired" in that case, never leak the error into the route
|
|
2113
|
+
* handler, where the host web server turns it into an empty-body 400 (#1942).
|
|
2114
|
+
*/
|
|
2115
|
+
function lookupPairing(ctx) {
|
|
2116
|
+
if (typeof ctx.get === "function") {
|
|
2117
|
+
const viaGet = ctx.get("remoteWebUiPairing", false);
|
|
2118
|
+
return isPairingAccess(viaGet) ? viaGet : void 0;
|
|
2119
|
+
}
|
|
2120
|
+
try {
|
|
2121
|
+
const viaProperty = ctx.remoteWebUiPairing;
|
|
2122
|
+
return isPairingAccess(viaProperty) ? viaProperty : void 0;
|
|
2123
|
+
} catch {
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2078
2126
|
}
|
|
2079
2127
|
function isPairingAccess(value) {
|
|
2080
2128
|
return value !== void 0 && value !== null && typeof value.isPairedDevice === "function";
|
|
@@ -2222,7 +2270,9 @@ const USAGE_SETTINGS_NAMESPACE = "dsh-usage";
|
|
|
2222
2270
|
const Config = z.object({
|
|
2223
2271
|
enabled: z.boolean().default(true).volatile(),
|
|
2224
2272
|
pollIntervalSec: z.number().min(30).max(3600).default(60).volatile(),
|
|
2225
|
-
retainDays: z.number().min(7).max(730).default(180).volatile()
|
|
2273
|
+
retainDays: z.number().min(7).max(730).default(180).volatile(),
|
|
2274
|
+
spendCurrency: z.union(["CNY", "USD"]).default("CNY").volatile(),
|
|
2275
|
+
cnyPerUsd: z.number().min(.01).max(1e3).default(DEFAULT_CNY_PER_USD).volatile()
|
|
2226
2276
|
});
|
|
2227
2277
|
/** Read one config field, following the live reference the schema produces. */
|
|
2228
2278
|
function readConfigField(field) {
|
|
@@ -2246,7 +2296,11 @@ function resolveConfig(config) {
|
|
|
2246
2296
|
return {
|
|
2247
2297
|
enabled: readConfigField(config?.enabled) ?? true,
|
|
2248
2298
|
pollIntervalSec: readNumber(config?.pollIntervalSec, 60),
|
|
2249
|
-
retainDays: readNumber(config?.retainDays, 180)
|
|
2299
|
+
retainDays: readNumber(config?.retainDays, 180),
|
|
2300
|
+
display: resolveSpendDisplay({
|
|
2301
|
+
currency: readConfigField(config?.spendCurrency),
|
|
2302
|
+
cnyPerUsd: readNumber(config?.cnyPerUsd, DEFAULT_CNY_PER_USD)
|
|
2303
|
+
})
|
|
2250
2304
|
};
|
|
2251
2305
|
}
|
|
2252
2306
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The usage statistics settings section: three tabs (用量: today's usage,
|
|
3
3
|
* balances, trend; 个人套餐: per-provider plan quota windows; Token 银行:
|
|
4
|
-
* the whale-yuan voucher minted from
|
|
4
|
+
* the whale-yuan voucher minted from every provider's ledger usage)
|
|
5
5
|
* plus a compact settings row. Data comes from the host's loopback-fenced
|
|
6
6
|
* /api/dsh-usage/overview document; polling runs only while the section is
|
|
7
7
|
* mounted and the tab is visible.
|
|
@@ -15,6 +15,10 @@ import type { UsageDayView } from '../core/types.ts';
|
|
|
15
15
|
export interface UsageSettings {
|
|
16
16
|
enabled?: boolean;
|
|
17
17
|
pollIntervalSec?: number;
|
|
18
|
+
/** Currency the spend figures are displayed in; the ledger stays CNY. */
|
|
19
|
+
spendCurrency?: 'CNY' | 'USD';
|
|
20
|
+
/** CNY per USD, consulted only while the display currency is USD. */
|
|
21
|
+
cnyPerUsd?: number;
|
|
18
22
|
}
|
|
19
23
|
/** The registration-side face the section's slot entry injects. */
|
|
20
24
|
export interface UsageSectionFace {
|
|
@@ -17,15 +17,59 @@ import { type UsageFootCardProps } from './UsageFootCard.tsx';
|
|
|
17
17
|
/** Stable data attribute identifying the injected card container. */
|
|
18
18
|
export declare const FOOT_CARD_SELECTOR = "[data-dsh-usage-foot-card]";
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* carrying the section's (localized) label. Every step degrades silently —
|
|
24
|
-
* with the panel already open only the nav pick runs, and a missing row
|
|
25
|
-
* leaves the panel on its default section.
|
|
26
|
-
* @param label - the usage section's nav label in the active locale.
|
|
20
|
+
* The settings shell's store handle, narrowed to the one write this module
|
|
21
|
+
* performs. A slot registration stores its seat type-erased (`StoreDecl`), so
|
|
22
|
+
* the shape the seat's occupant guarantees is restated here.
|
|
27
23
|
*/
|
|
28
|
-
export
|
|
24
|
+
export interface SettingsShellStore {
|
|
25
|
+
create(): {
|
|
26
|
+
actions: {
|
|
27
|
+
openSection(id: string): void;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** The registry read the seat lookup needs (structurally `ctx.slots`). */
|
|
32
|
+
export interface SlotSeatReader {
|
|
33
|
+
entries(key: string): readonly {
|
|
34
|
+
store?: unknown;
|
|
35
|
+
}[];
|
|
36
|
+
}
|
|
37
|
+
/** What the card knows about the settings section it opens. */
|
|
38
|
+
export interface UsageSettingsTarget {
|
|
39
|
+
/** Slot registration id of the usage section (the nav row the panel selects). */
|
|
40
|
+
id: string;
|
|
41
|
+
/** The section's nav label in the active locale (the DOM replay's row pick). */
|
|
42
|
+
label: () => string;
|
|
43
|
+
/** The settings shell's store handle, when the seat exposes one. */
|
|
44
|
+
store?: SettingsShellStore | undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the settings shell's store handle from the `sidebar.settings` seat.
|
|
48
|
+
*
|
|
49
|
+
* The seat is a single-occupant slot and the shell's own registration carries
|
|
50
|
+
* the store that owns the panel's open state and active section, so reading it
|
|
51
|
+
* is the one step that opens the panel without touching the DOM. A shell that
|
|
52
|
+
* exposes no seat, a seat without a store, or a registry answering mid-teardown
|
|
53
|
+
* all yield undefined, and the caller replays the DOM path instead.
|
|
54
|
+
* @param slots - the renderer-owned slot registry.
|
|
55
|
+
* @returns the store handle, or undefined when the seat carries none.
|
|
56
|
+
*/
|
|
57
|
+
export declare function settingsShellStore(slots: SlotSeatReader | undefined): SettingsShellStore | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Open the settings panel on the usage section.
|
|
60
|
+
*
|
|
61
|
+
* The seat's store is the primary path: `openSection` opens the dialog on the
|
|
62
|
+
* usage row in one call, with no dependence on which plugin occupies the
|
|
63
|
+
* `settings.launcher` seat — an account row there turns a trigger click into a
|
|
64
|
+
* menu, which is how this click used to land on the account menu instead of the
|
|
65
|
+
* panel. Without such a store the card replays the user's own path: activate
|
|
66
|
+
* the launcher (preferring the dialog trigger over a menu trigger), follow a
|
|
67
|
+
* menu launcher's first settings item, then pick the nav row carrying the
|
|
68
|
+
* section's label. Every step degrades silently — a missing surface leaves the
|
|
69
|
+
* page untouched, and with the panel already open only the nav pick runs.
|
|
70
|
+
* @param target - the section id, its localized label, and the seat's store.
|
|
71
|
+
*/
|
|
72
|
+
export declare function openUsageSettings(target: UsageSettingsTarget): void;
|
|
29
73
|
/**
|
|
30
74
|
* Mount the sidebar foot card directly above the Settings row.
|
|
31
75
|
* @param props - the store/poll/settings/open inputs of the apply body.
|
|
@@ -57,6 +57,8 @@ export declare const zh: {
|
|
|
57
57
|
'usage.config.title': string;
|
|
58
58
|
'usage.config.enabled': string;
|
|
59
59
|
'usage.config.pollIntervalSec': string;
|
|
60
|
+
'usage.config.spendCurrency': string;
|
|
61
|
+
'usage.config.cnyPerUsd': string;
|
|
60
62
|
'usage.config.saveFailed': string;
|
|
61
63
|
'usage.bank.title': string;
|
|
62
64
|
'usage.bank.hint': string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Whale-yuan voucher (鲸元券): the
|
|
2
|
+
* The Whale-yuan voucher (鲸元券): the profile's retained ledger totals
|
|
3
3
|
* stamped onto the banknote artwork as a denomination, ready
|
|
4
4
|
* to save or share. The canvas copy is deliberately locale-neutral (digits,
|
|
5
5
|
* latin captions, ISO dates) so the exported image needs no dictionary;
|
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
import type { UsageWindowSummary } from '../core/types.ts';
|
|
12
12
|
/** The facts stamped onto one voucher. */
|
|
13
13
|
export interface VoucherData {
|
|
14
|
-
/**
|
|
14
|
+
/** Every provider's total tokens over the retained window. */
|
|
15
15
|
tokens: number;
|
|
16
|
-
/** Provider calls that reported usage. */
|
|
16
|
+
/** Provider calls that reported usage, summed across every provider. */
|
|
17
17
|
calls: number;
|
|
18
|
-
/** Fold-time spend estimate in CNY; only the priced family contributes. */
|
|
18
|
+
/** Fold-time spend estimate in CNY; only the priced (official DeepSeek) family contributes. */
|
|
19
19
|
cost: number;
|
|
20
20
|
/** Retained window bounds (local date keys, inclusive). */
|
|
21
21
|
from: string;
|
|
@@ -28,28 +28,23 @@ export declare function faceValue(tokens: number): number;
|
|
|
28
28
|
/** Local calendar day (`YYYY-MM-DD`) for an epoch ms timestamp. */
|
|
29
29
|
export declare function formatDay(ms: number): string;
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Sum the mint family's rows out of a usage window: the whole DeepSeek
|
|
45
|
-
* official family (the `deepseek` catalog alias, the live `deepseek-official`
|
|
46
|
-
* route and the signed-in `deepseek-account` route all fold into one voucher)
|
|
47
|
-
* plus {@link EXTRA_MINT_ROUTE_IDS}. Undefined when the window is absent
|
|
48
|
-
* (older host) or the family has no usage — a zero-token note mints nothing.
|
|
31
|
+
* Sum every provider's rows out of a usage window: the note is a
|
|
32
|
+
* proof-of-spend for what the profile actually used, so the tokens and calls
|
|
33
|
+
* of every route in the ledger mint — the DeepSeek official family (the
|
|
34
|
+
* `deepseek` catalog alias, the live `deepseek-official` route and the
|
|
35
|
+
* signed-in `deepseek-account` route) beside every subscription and relay
|
|
36
|
+
* route. Undefined when the window is absent (older host) or nothing was used
|
|
37
|
+
* — a zero-token note mints nothing.
|
|
38
|
+
*
|
|
39
|
+
* Minting is an accounting decision over ledger rows, so it reads no adapter:
|
|
40
|
+
* widening it must never widen the credential or balance-probe paths, which
|
|
41
|
+
* keep reading `adapterFor()` (the split `isDeepSeekProviderRoute()` also
|
|
42
|
+
* keeps, issue #1772).
|
|
49
43
|
*
|
|
50
|
-
* Only the priced family contributes `cost`:
|
|
51
|
-
*
|
|
52
|
-
* family's fold-time estimate (and the observed-balance
|
|
44
|
+
* Only the priced family contributes `cost`: subscription and relay routes
|
|
45
|
+
* ship no price book, so they mint tokens and calls while the spend line keeps
|
|
46
|
+
* reporting the official family's fold-time estimate (and the observed-balance
|
|
47
|
+
* watch above it).
|
|
53
48
|
*/
|
|
54
49
|
export declare function voucherData(window: UsageWindowSummary | undefined): VoucherData | undefined;
|
|
55
50
|
/** Banknote denomination: full digits with thousands separators. */
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spend display: the ledger keeps every cost estimate in the priced provider's
|
|
3
|
+
* billing currency (CNY), and that stored value never moves. This module turns
|
|
4
|
+
* one CNY figure into the currency the user chose to READ it in, so switching
|
|
5
|
+
* the display is a rendering decision, not a data migration (issue #1937).
|
|
6
|
+
* @module @linxin666/dsh-usage/core/spend
|
|
7
|
+
*/
|
|
8
|
+
/** A currency the spend display can render. */
|
|
9
|
+
export type SpendCurrency = 'CNY' | 'USD';
|
|
10
|
+
/** How to read a stored CNY cost. */
|
|
11
|
+
export interface SpendDisplay {
|
|
12
|
+
/** The currency to render in. */
|
|
13
|
+
currency: SpendCurrency;
|
|
14
|
+
/** CNY per USD; consulted only when the display currency is USD. */
|
|
15
|
+
cnyPerUsd: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The rate used when the row configures none: a plain default, so an
|
|
19
|
+
* unconfigured display still has a defined美元 figure instead of a blank card.
|
|
20
|
+
* The plugin never fetches a live rate (its host routes are loopback-fenced and
|
|
21
|
+
* its browser half has no network), so there is no fetch that could fail and
|
|
22
|
+
* leave the card empty.
|
|
23
|
+
*/
|
|
24
|
+
export declare const DEFAULT_CNY_PER_USD = 7.2;
|
|
25
|
+
/** The display used when the row carries no setting: the historical CNY rendering. */
|
|
26
|
+
export declare const DEFAULT_SPEND_DISPLAY: SpendDisplay;
|
|
27
|
+
/**
|
|
28
|
+
* Normalize a configured display.
|
|
29
|
+
* An unknown currency reads as CNY, and a rate that is not a positive finite
|
|
30
|
+
* number falls back to the default rate — the display must never be able to
|
|
31
|
+
* blank the card.
|
|
32
|
+
* @param input - the configured currency and rate, if any.
|
|
33
|
+
* @returns a display that can always render.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveSpendDisplay(input?: {
|
|
36
|
+
currency?: unknown;
|
|
37
|
+
cnyPerUsd?: unknown;
|
|
38
|
+
}): SpendDisplay;
|
|
39
|
+
/**
|
|
40
|
+
* Render one stored CNY cost in the display currency.
|
|
41
|
+
* A non-finite or negative cost renders as zero rather than as `NaN`/`¥-0.10`.
|
|
42
|
+
* @param costCny - the stored cost in CNY.
|
|
43
|
+
* @param display - the display to render in; defaults to CNY.
|
|
44
|
+
* @returns the formatted amount with its currency symbol.
|
|
45
|
+
*/
|
|
46
|
+
export declare function formatSpend(costCny: number, display?: SpendDisplay): string;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* core adapters, never here.
|
|
6
6
|
* @module @linxin666/dsh-usage/core/types
|
|
7
7
|
*/
|
|
8
|
+
import type { SpendDisplay } from './spend.ts';
|
|
8
9
|
/** One disjoint token bucket total, aggregated over calls (billed input = input + cacheRead + cacheWrite). */
|
|
9
10
|
export interface UsageTokenTotals {
|
|
10
11
|
/** Uncached input tokens. */
|
|
@@ -135,6 +136,12 @@ export interface ProviderSnapshotView {
|
|
|
135
136
|
/** The overview the browser section renders. */
|
|
136
137
|
export interface UsageOverviewView {
|
|
137
138
|
updatedAt: number;
|
|
139
|
+
/**
|
|
140
|
+
* How to read every CNY cost in this document. The host stamps the row's
|
|
141
|
+
* configured display here so both browser halves format spend the same way;
|
|
142
|
+
* absent on an older host document, which renders as CNY (issue #1937).
|
|
143
|
+
*/
|
|
144
|
+
display?: SpendDisplay;
|
|
138
145
|
providers: ProviderSnapshotView[];
|
|
139
146
|
/** The provider the collapsed sidebar strip and the header highlight; `source` says how it was picked. */
|
|
140
147
|
current: {
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
* request. Forwarding headers are the signal that a hop stands in between: a
|
|
16
16
|
* request that carries CF-Connecting-IP or X-Forwarded-For is answered through
|
|
17
17
|
* a proxy and is never treated as a local one (issue #1906).
|
|
18
|
+
*
|
|
19
|
+
* An operator who runs that proxy or tunnel themselves can opt into the
|
|
20
|
+
* narrower case: isForwardedLoopbackRequest describes a forwarded request that
|
|
21
|
+
* still landed on this machine's loopback upstream, and a route family admits
|
|
22
|
+
* it only behind its own default-off switch (issue #1918).
|
|
18
23
|
* @module dsh-web-shared/host/loopback
|
|
19
24
|
*/
|
|
20
25
|
import type { IncomingMessage } from 'node:http';
|
|
@@ -41,3 +46,19 @@ export declare function isProxiedRequest(request: IncomingMessage): boolean;
|
|
|
41
46
|
* client. X-Forwarded-For is therefore never evidence *for* loopback.
|
|
42
47
|
*/
|
|
43
48
|
export declare function isLoopbackRequest(request: IncomingMessage): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Whether a forwarded request landed on this machine's loopback upstream: a
|
|
51
|
+
* forwarding header (the proxy's own stamp), a loopback socket address (the
|
|
52
|
+
* proxy dials 127.0.0.1:<port>), and a loopback Host header (the proxy rewrote
|
|
53
|
+
* the authority to its upstream literal).
|
|
54
|
+
*
|
|
55
|
+
* This is a fact about the route, not a trust decision. A public request to a
|
|
56
|
+
* tunnel satisfies all three by construction, because the proxy is the client
|
|
57
|
+
* of this process: only a caller that operates the proxy can decide it stands
|
|
58
|
+
* for the browser, which is why every consumer keeps this behind an explicit
|
|
59
|
+
* default-off switch and its own browser markers. isLoopbackRequest is
|
|
60
|
+
* unchanged and still refuses a forwarded request (issue #1906).
|
|
61
|
+
* @param request - node HTTP request.
|
|
62
|
+
* @returns true when a forwarding header, a loopback socket, and a loopback Host all hold.
|
|
63
|
+
*/
|
|
64
|
+
export declare function isForwardedLoopbackRequest(request: IncomingMessage): boolean;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { Context } from '@deepseek-ai/cordis';
|
|
10
10
|
import type { UsageDayView, UsageOverviewView } from '../core/types.ts';
|
|
11
|
+
import { type SpendDisplay } from '../core/spend.ts';
|
|
11
12
|
/**
|
|
12
13
|
* Persisted accrual state for the official DeepSeek family's real spend:
|
|
13
14
|
* consecutive observations of the official CNY balance, decreases counted
|
|
@@ -27,6 +28,12 @@ export interface SpendWatch {
|
|
|
27
28
|
export interface UsageServiceOptions {
|
|
28
29
|
pollIntervalSec: number;
|
|
29
30
|
retainDays: number;
|
|
31
|
+
/**
|
|
32
|
+
* How the browser halves should read the stored CNY costs. Optional so a
|
|
33
|
+
* caller that predates the display setting keeps the CNY rendering (issue
|
|
34
|
+
* #1937); the overview stamps the resolved value on every document.
|
|
35
|
+
*/
|
|
36
|
+
display?: SpendDisplay;
|
|
30
37
|
}
|
|
31
38
|
export declare class UsageService {
|
|
32
39
|
private readonly ctx;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Context } from '@deepseek-ai/cordis';
|
|
|
2
2
|
import type { SettingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
3
3
|
import z from '@deepseek-ai/schemastery';
|
|
4
4
|
import { type UsageServiceOptions } from './host/usage-service.ts';
|
|
5
|
+
import { type SpendDisplay } from './core/spend.ts';
|
|
5
6
|
declare module '@deepseek-ai/cordis' {
|
|
6
7
|
interface Events {
|
|
7
8
|
/**
|
|
@@ -38,6 +39,10 @@ export interface UsageConfigFields {
|
|
|
38
39
|
pollIntervalSec?: ConfigField<number>;
|
|
39
40
|
/** Ledger retention in local days. */
|
|
40
41
|
retainDays?: ConfigField<number>;
|
|
42
|
+
/** Currency the spend figures are DISPLAYED in; the ledger itself stays CNY. */
|
|
43
|
+
spendCurrency?: ConfigField<'CNY' | 'USD'>;
|
|
44
|
+
/** CNY per USD, consulted only while the display currency is USD. */
|
|
45
|
+
cnyPerUsd?: ConfigField<number>;
|
|
41
46
|
}
|
|
42
47
|
/**
|
|
43
48
|
* Plugin config schema. Under the 0.1.7 settings model this schema IS the
|
|
@@ -57,13 +62,19 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
57
62
|
enabled: z<boolean, boolean, "volatile-defined">;
|
|
58
63
|
pollIntervalSec: z<number, number, "volatile-defined">;
|
|
59
64
|
retainDays: z<number, number, "volatile-defined">;
|
|
65
|
+
spendCurrency: z<"CNY" | "USD", "CNY" | "USD", "volatile-defined">;
|
|
66
|
+
cnyPerUsd: z<number, number, "volatile-defined">;
|
|
60
67
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
61
68
|
enabled: z<boolean, boolean, "volatile-defined">;
|
|
62
69
|
pollIntervalSec: z<number, number, "volatile-defined">;
|
|
63
70
|
retainDays: z<number, number, "volatile-defined">;
|
|
71
|
+
spendCurrency: z<"CNY" | "USD", "CNY" | "USD", "volatile-defined">;
|
|
72
|
+
cnyPerUsd: z<number, number, "volatile-defined">;
|
|
64
73
|
}>>, "plain">;
|
|
65
74
|
export interface ResolvedConfig extends UsageServiceOptions {
|
|
66
75
|
enabled: boolean;
|
|
76
|
+
/** The resolved spend display; always present, unlike the optional service option. */
|
|
77
|
+
display: SpendDisplay;
|
|
67
78
|
}
|
|
68
79
|
/**
|
|
69
80
|
* Resolve the effective settings of one activation. Volatile references are
|
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.9",
|
|
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.2.1-rc.
|
|
36
|
+
"dsh": ">=0.2.1-rc.3"
|
|
37
37
|
},
|
|
38
38
|
"bundle": {
|
|
39
39
|
"patch": "./cordis.patch.yml"
|
|
@@ -53,22 +53,22 @@
|
|
|
53
53
|
"@deepseek-ai/schemastery": "^3.18.5"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@deepseek-ai/dsh": ">=0.2.1-rc.
|
|
56
|
+
"@deepseek-ai/dsh": ">=0.2.1-rc.3",
|
|
57
57
|
"react": "^18.2.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@deepseek-ai/cordis": "^4.0.5",
|
|
61
|
-
"@deepseek-ai/dsh-
|
|
62
|
-
"@deepseek-ai/dsh-client-
|
|
63
|
-
"@deepseek-ai/dsh-client-
|
|
64
|
-
"@deepseek-ai/dsh-client-
|
|
65
|
-
"@deepseek-ai/dsh-client-ui-
|
|
66
|
-
"@deepseek-ai/dsh-client-ui-
|
|
67
|
-
"@deepseek-ai/dsh-
|
|
68
|
-
"@deepseek-ai/dsh-host-webserver": "^0.2.1-rc.
|
|
69
|
-
"@deepseek-ai/dsh-llm": "^0.2.1-rc.
|
|
70
|
-
"@deepseek-ai/dsh-session": "^0.2.1-rc.
|
|
71
|
-
"@deepseek-ai/dsh-settings": "^0.2.1-rc.
|
|
61
|
+
"@deepseek-ai/dsh-client-connection": "^0.2.1-rc.3",
|
|
62
|
+
"@deepseek-ai/dsh-client-locale": "^0.2.1-rc.3",
|
|
63
|
+
"@deepseek-ai/dsh-client-store": "^0.2.1-rc.3",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.2.1-rc.3",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.2.1-rc.3",
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.2.1-rc.3",
|
|
67
|
+
"@deepseek-ai/dsh-credentials": "^0.2.1-rc.3",
|
|
68
|
+
"@deepseek-ai/dsh-host-webserver": "^0.2.1-rc.3",
|
|
69
|
+
"@deepseek-ai/dsh-llm": "^0.2.1-rc.3",
|
|
70
|
+
"@deepseek-ai/dsh-session": "^0.2.1-rc.3",
|
|
71
|
+
"@deepseek-ai/dsh-settings": "^0.2.1-rc.3",
|
|
72
72
|
"@testing-library/dom": "^10.4.1",
|
|
73
73
|
"@testing-library/react": "^16.3.2",
|
|
74
74
|
"@types/node": "^22.20.0",
|
|
@@ -32,6 +32,7 @@ import { formatTokens, type UsageSettings } from './UsageSectionCard.tsx'
|
|
|
32
32
|
import { deepseekPeriodAt } from '../core/pricing.ts'
|
|
33
33
|
import { isDeepSeekProviderRoute } from '../core/adapters.ts'
|
|
34
34
|
import { totalTokens } from '../core/ledger.ts'
|
|
35
|
+
import { formatSpend } from '../core/spend.ts'
|
|
35
36
|
import type { BalanceView, ProviderSnapshotView, UsageOverviewView } from '../core/types.ts'
|
|
36
37
|
import type { UsageStoreInstance } from './usage-store.ts'
|
|
37
38
|
|
|
@@ -148,9 +149,9 @@ function spendProvider(snapshot: UsageOverviewView): string | undefined {
|
|
|
148
149
|
*/
|
|
149
150
|
function headline(snapshot: UsageOverviewView): { label: string; value: string; tokenFallback: boolean; provider?: string } {
|
|
150
151
|
const totals = snapshot.usage.today.totals
|
|
151
|
-
if (totals.cost > 0) return { label: t('usage.foot.cost'), value:
|
|
152
|
+
if (totals.cost > 0) return { label: t('usage.foot.cost'), value: formatSpend(totals.cost, snapshot.display), tokenFallback: false, provider: spendProvider(snapshot) }
|
|
152
153
|
if (totals.calls > 0) return { label: t('usage.today'), value: formatTokens(totalTokens(totals)) + ' tokens', tokenFallback: true }
|
|
153
|
-
return { label: t('usage.foot.cost'), value:
|
|
154
|
+
return { label: t('usage.foot.cost'), value: formatSpend(0, snapshot.display), tokenFallback: false }
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
/** The tokens/calls sub-line under the headline; a quiet no-data line on an empty day. */
|