@linxin666/dsh-usage 0.4.8 → 0.4.10
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/src/host/loopback.ts
CHANGED
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
* request. Forwarding headers are the signal that a hop stands in between: a
|
|
17
17
|
* request that carries CF-Connecting-IP or X-Forwarded-For is answered through
|
|
18
18
|
* a proxy and is never treated as a local one (issue #1906).
|
|
19
|
+
*
|
|
20
|
+
* An operator who runs that proxy or tunnel themselves can opt into the
|
|
21
|
+
* narrower case: isForwardedLoopbackRequest describes a forwarded request that
|
|
22
|
+
* still landed on this machine's loopback upstream, and a route family admits
|
|
23
|
+
* it only behind its own default-off switch (issue #1918).
|
|
19
24
|
* @module dsh-web-shared/host/loopback
|
|
20
25
|
*/
|
|
21
26
|
|
|
@@ -85,3 +90,30 @@ export function isLoopbackRequest(request: IncomingMessage): boolean {
|
|
|
85
90
|
return false
|
|
86
91
|
}
|
|
87
92
|
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Whether a forwarded request landed on this machine's loopback upstream: a
|
|
96
|
+
* forwarding header (the proxy's own stamp), a loopback socket address (the
|
|
97
|
+
* proxy dials 127.0.0.1:<port>), and a loopback Host header (the proxy rewrote
|
|
98
|
+
* the authority to its upstream literal).
|
|
99
|
+
*
|
|
100
|
+
* This is a fact about the route, not a trust decision. A public request to a
|
|
101
|
+
* tunnel satisfies all three by construction, because the proxy is the client
|
|
102
|
+
* of this process: only a caller that operates the proxy can decide it stands
|
|
103
|
+
* for the browser, which is why every consumer keeps this behind an explicit
|
|
104
|
+
* default-off switch and its own browser markers. isLoopbackRequest is
|
|
105
|
+
* unchanged and still refuses a forwarded request (issue #1906).
|
|
106
|
+
* @param request - node HTTP request.
|
|
107
|
+
* @returns true when a forwarding header, a loopback socket, and a loopback Host all hold.
|
|
108
|
+
*/
|
|
109
|
+
export function isForwardedLoopbackRequest(request: IncomingMessage): boolean {
|
|
110
|
+
if (!isProxiedRequest(request)) return false
|
|
111
|
+
if (!isLoopbackAddress(request.socket.remoteAddress)) return false
|
|
112
|
+
const host = request.headers.host
|
|
113
|
+
if (typeof host !== 'string') return false
|
|
114
|
+
try {
|
|
115
|
+
return isLoopbackHostname(new URL('http://' + host).hostname)
|
|
116
|
+
} catch {
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
}
|
package/src/host/pair-access.ts
CHANGED
|
@@ -37,9 +37,32 @@ interface LookupCtx {
|
|
|
37
37
|
*/
|
|
38
38
|
export function isPairedOrLoopbackAllowed(ctx: LookupCtx, request: IncomingMessage): boolean {
|
|
39
39
|
if (isLoopbackRequest(request)) return true
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
return lookupPairing(ctx)?.isPairedDevice(request) === true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The pairing service, or undefined when this composition does not expose it.
|
|
45
|
+
*
|
|
46
|
+
* Two lookups exist because the fence is shared with test harnesses that hand
|
|
47
|
+
* over a bare object: a real cordis context resolves services through `get`, a
|
|
48
|
+
* harness may carry the property directly. `get(name, false)` is the
|
|
49
|
+
* non-strict form, so an unloaded service answers undefined rather than
|
|
50
|
+
* throwing. Reading an un-injected service as a PROPERTY throws instead
|
|
51
|
+
* (`cannot get property "remoteWebUiPairing" without inject`); a trust fence
|
|
52
|
+
* must answer "not paired" in that case, never leak the error into the route
|
|
53
|
+
* handler, where the host web server turns it into an empty-body 400 (#1942).
|
|
54
|
+
*/
|
|
55
|
+
function lookupPairing(ctx: LookupCtx): PairingAccess | undefined {
|
|
56
|
+
if (typeof ctx.get === 'function') {
|
|
57
|
+
const viaGet = ctx.get('remoteWebUiPairing', false)
|
|
58
|
+
return isPairingAccess(viaGet) ? viaGet : undefined
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const viaProperty = ctx.remoteWebUiPairing
|
|
62
|
+
return isPairingAccess(viaProperty) ? viaProperty : undefined
|
|
63
|
+
} catch {
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
43
66
|
}
|
|
44
67
|
|
|
45
68
|
function isPairingAccess(value: unknown): value is PairingAccess {
|
|
@@ -22,6 +22,7 @@ import { deepseekModelSpend } from '../core/pricing.ts'
|
|
|
22
22
|
import { createLedgerDocument, deserializeLedger, foldUsage, ledgerDayKeys, localDateKey, pruneLedger, summarizeDays, totalTokens } from '../core/ledger.ts'
|
|
23
23
|
import type { BalanceView, CredentialKind, ObservedSpendView, PlanView, ProviderSnapshotState, ProviderSnapshotView, UsageDayView, UsageLedgerDocument, UsageOverviewView, UsageTokenTotals } from '../core/types.ts'
|
|
24
24
|
import { addTotals, emptyTotals } from '../core/types.ts'
|
|
25
|
+
import { resolveSpendDisplay, type SpendDisplay } from '../core/spend.ts'
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Persisted accrual state for the official DeepSeek family's real spend:
|
|
@@ -60,6 +61,12 @@ interface ProviderRoute {
|
|
|
60
61
|
export interface UsageServiceOptions {
|
|
61
62
|
pollIntervalSec: number
|
|
62
63
|
retainDays: number
|
|
64
|
+
/**
|
|
65
|
+
* How the browser halves should read the stored CNY costs. Optional so a
|
|
66
|
+
* caller that predates the display setting keeps the CNY rendering (issue
|
|
67
|
+
* #1937); the overview stamps the resolved value on every document.
|
|
68
|
+
*/
|
|
69
|
+
display?: SpendDisplay
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
/** Probe timeout per HTTP call. */
|
|
@@ -336,6 +343,8 @@ export class UsageService {
|
|
|
336
343
|
const all = summarizeDays(allKeys.map((key) => this.ledger.days[key] ?? {}))
|
|
337
344
|
return {
|
|
338
345
|
updatedAt: Date.now(),
|
|
346
|
+
// Display only: costs stay stored in CNY (issue #1937).
|
|
347
|
+
display: resolveSpendDisplay(this.options.display),
|
|
339
348
|
providers,
|
|
340
349
|
current: this.currentView(),
|
|
341
350
|
usage: {
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { SettingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
|
4
4
|
import z from '@deepseek-ai/schemastery'
|
|
5
5
|
import { mountOnce } from './mount-once.ts'
|
|
6
6
|
import { UsageService, type UsageServiceOptions } from './host/usage-service.ts'
|
|
7
|
+
import { DEFAULT_CNY_PER_USD, resolveSpendDisplay, type SpendDisplay } from './core/spend.ts'
|
|
7
8
|
import { makeUsageDayRoute, makeUsageOverviewRoute, makeUsageRefreshRoute } from './host/routes.ts'
|
|
8
9
|
|
|
9
10
|
declare module '@deepseek-ai/cordis' {
|
|
@@ -46,6 +47,10 @@ export interface UsageConfigFields {
|
|
|
46
47
|
pollIntervalSec?: ConfigField<number>
|
|
47
48
|
/** Ledger retention in local days. */
|
|
48
49
|
retainDays?: ConfigField<number>
|
|
50
|
+
/** Currency the spend figures are DISPLAYED in; the ledger itself stays CNY. */
|
|
51
|
+
spendCurrency?: ConfigField<'CNY' | 'USD'>
|
|
52
|
+
/** CNY per USD, consulted only while the display currency is USD. */
|
|
53
|
+
cnyPerUsd?: ConfigField<number>
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
/**
|
|
@@ -66,10 +71,18 @@ export const Config = z.object({
|
|
|
66
71
|
enabled: z.boolean().default(true).volatile(),
|
|
67
72
|
pollIntervalSec: z.number().min(30).max(3600).default(60).volatile(),
|
|
68
73
|
retainDays: z.number().min(7).max(730).default(180).volatile(),
|
|
74
|
+
// Display only: the ledger keeps every cost estimate in CNY, and switching
|
|
75
|
+
// this field never rewrites history (issue #1937). The plugin fetches no live
|
|
76
|
+
// rate, so an unset or unusable rate renders with the documented default
|
|
77
|
+
// instead of failing the card.
|
|
78
|
+
spendCurrency: z.union(['CNY', 'USD'] as const).default('CNY').volatile(),
|
|
79
|
+
cnyPerUsd: z.number().min(0.01).max(1000).default(DEFAULT_CNY_PER_USD).volatile(),
|
|
69
80
|
})
|
|
70
81
|
|
|
71
82
|
export interface ResolvedConfig extends UsageServiceOptions {
|
|
72
83
|
enabled: boolean
|
|
84
|
+
/** The resolved spend display; always present, unlike the optional service option. */
|
|
85
|
+
display: SpendDisplay
|
|
73
86
|
}
|
|
74
87
|
|
|
75
88
|
/** Read one config field, following the live reference the schema produces. */
|
|
@@ -99,6 +112,10 @@ export function resolveConfig(config?: UsageConfigFields): ResolvedConfig {
|
|
|
99
112
|
enabled: readConfigField(config?.enabled) ?? true,
|
|
100
113
|
pollIntervalSec: readNumber(config?.pollIntervalSec, 60),
|
|
101
114
|
retainDays: readNumber(config?.retainDays, 180),
|
|
115
|
+
display: resolveSpendDisplay({
|
|
116
|
+
currency: readConfigField(config?.spendCurrency),
|
|
117
|
+
cnyPerUsd: readNumber(config?.cnyPerUsd, DEFAULT_CNY_PER_USD),
|
|
118
|
+
}),
|
|
102
119
|
}
|
|
103
120
|
}
|
|
104
121
|
|