@linxin666/dsh-usage 0.3.7
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/LICENSE +202 -0
- package/README.i18n.yaml +5 -0
- package/README.md +50 -0
- package/README.zh.md +50 -0
- package/cordis.patch.yml +10 -0
- package/lib/client.js +755 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +1443 -0
- package/lib/types/client/UsageSectionCard.d.ts +36 -0
- package/lib/types/client/index.d.ts +34 -0
- package/lib/types/client/locales.d.ts +62 -0
- package/lib/types/client/usage-store.d.ts +27 -0
- package/lib/types/core/adapters.d.ts +58 -0
- package/lib/types/core/ledger.d.ts +31 -0
- package/lib/types/core/types.d.ts +112 -0
- package/lib/types/dsh-home.d.ts +16 -0
- package/lib/types/host/http.d.ts +42 -0
- package/lib/types/host/loopback.d.ts +24 -0
- package/lib/types/host/routes.d.ts +14 -0
- package/lib/types/host/usage-service.d.ts +82 -0
- package/lib/types/index.d.ts +21 -0
- package/lib/types/mount-once.d.ts +24 -0
- package/package.json +92 -0
- package/src/client/UsageSectionCard.tsx +368 -0
- package/src/client/css-modules.d.ts +4 -0
- package/src/client/index.ts +131 -0
- package/src/client/locales.ts +116 -0
- package/src/client/usage-store.ts +52 -0
- package/src/client/usage.module.css +311 -0
- package/src/core/adapters.ts +370 -0
- package/src/core/ledger.ts +108 -0
- package/src/core/types.ts +127 -0
- package/src/dsh-home.ts +42 -0
- package/src/host/http.ts +105 -0
- package/src/host/loopback.ts +63 -0
- package/src/host/routes.ts +54 -0
- package/src/host/usage-service.ts +530 -0
- package/src/index.ts +90 -0
- package/src/mount-once.ts +48 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The usage statistics settings section: two tabs (用量: today's usage,
|
|
3
|
+
* balances, trend; 个人套餐: per-provider plan quota windows) plus a compact
|
|
4
|
+
* settings row. Data comes from the host's loopback-fenced
|
|
5
|
+
* /api/dsh-usage/overview document; polling runs only while the section is
|
|
6
|
+
* mounted and the tab is visible.
|
|
7
|
+
* @module @linxin666/dsh-usage/client/UsageSectionCard
|
|
8
|
+
*/
|
|
9
|
+
import { type ReactNode } from 'react';
|
|
10
|
+
import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client';
|
|
11
|
+
import type { UsageStoreInstance } from './usage-store.ts';
|
|
12
|
+
/** The settings fields this section edits (immediate-apply semantics). */
|
|
13
|
+
export interface UsageSettings {
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
pollIntervalSec?: number;
|
|
16
|
+
bubbleMode?: string;
|
|
17
|
+
}
|
|
18
|
+
/** The registration-side face the section's slot entry injects. */
|
|
19
|
+
export interface UsageSectionFace {
|
|
20
|
+
/** The section-local store (overview snapshot + lifecycle). */
|
|
21
|
+
store: UsageStoreInstance;
|
|
22
|
+
/** Fetch one overview now. */
|
|
23
|
+
poll: () => void;
|
|
24
|
+
/** Force a host probe cycle now (resolves with the fresh overview). */
|
|
25
|
+
refresh: () => void;
|
|
26
|
+
/** Whether a forced refresh is in flight (component-local state mirrors it). */
|
|
27
|
+
settings: SettingsScope<UsageSettings>;
|
|
28
|
+
}
|
|
29
|
+
export interface UsageSectionProps extends UsageSectionFace {
|
|
30
|
+
/** Close the settings panel (the shell owns the open state). */
|
|
31
|
+
close: () => void;
|
|
32
|
+
}
|
|
33
|
+
/** Compact token count: 12345 -> 12.3k, 1234567 -> 1.23M. */
|
|
34
|
+
export declare function formatTokens(value: number): string;
|
|
35
|
+
/** The section component; the slot merges the face into these props. */
|
|
36
|
+
export declare function UsageSectionCard(props: UsageSectionProps): ReactNode;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-usage browser half — seats the first-level 使用统计 settings section
|
|
3
|
+
* (below the Workshop entry) and polls the host overview only while the
|
|
4
|
+
* section is open. All provider probing and credential handling happens in
|
|
5
|
+
* the host half; this bundle only renders the overview document.
|
|
6
|
+
* @module @linxin666/dsh-usage/client
|
|
7
|
+
*/
|
|
8
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis';
|
|
9
|
+
import type { SettingsScope, SettingsScopeSpec } from '@deepseek-ai/dsh-client-ui-settings/client';
|
|
10
|
+
import { type UsageSettings } from './UsageSectionCard.tsx';
|
|
11
|
+
/** Required services. */
|
|
12
|
+
export declare const inject: string[];
|
|
13
|
+
export type { UsageSectionProps, UsageSectionFace } from './UsageSectionCard.tsx';
|
|
14
|
+
export type { UsageUiState } from './usage-store.ts';
|
|
15
|
+
export type { UsageSettings };
|
|
16
|
+
declare module '@deepseek-ai/cordis' {
|
|
17
|
+
interface Context {
|
|
18
|
+
/**
|
|
19
|
+
* Optional rc.6 compatibility binder provided by dsh-web-settings;
|
|
20
|
+
* absent when that group plugin is not installed, so callers fall back to
|
|
21
|
+
* the official settings scope.
|
|
22
|
+
*/
|
|
23
|
+
webUiSettings?: {
|
|
24
|
+
bind<S>(spec: SettingsScopeSpec<S>): SettingsScope<S>;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Client plugin body: register dictionaries and seat the settings section.
|
|
30
|
+
* The overview poll loop and the store live with the section component's
|
|
31
|
+
* mount cycle, so no background traffic exists while the page is closed.
|
|
32
|
+
* @param ctx - client root context.
|
|
33
|
+
*/
|
|
34
|
+
export declare function apply(ctx: ClientContext): void;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-usage locale dictionaries (zh/en). The zh dictionary is the key source;
|
|
3
|
+
* `en` mirrors its full key set (packages/AGENTS.md bilingual discipline).
|
|
4
|
+
* @module @linxin666/dsh-usage/client/locales
|
|
5
|
+
*/
|
|
6
|
+
/** Dictionary namespace this package registers. */
|
|
7
|
+
export declare const NS = "dsh-web-ui-usage";
|
|
8
|
+
/** Chinese copy. */
|
|
9
|
+
export declare const zh: {
|
|
10
|
+
'usage.title': string;
|
|
11
|
+
'usage.tab.usage': string;
|
|
12
|
+
'usage.tab.plans': string;
|
|
13
|
+
'usage.refresh': string;
|
|
14
|
+
'usage.refreshing': string;
|
|
15
|
+
'usage.updated': string;
|
|
16
|
+
'usage.loading': string;
|
|
17
|
+
'usage.error': string;
|
|
18
|
+
'usage.current': string;
|
|
19
|
+
'usage.today': string;
|
|
20
|
+
'usage.calls': string;
|
|
21
|
+
'usage.tokens.total': string;
|
|
22
|
+
'usage.tokens.input': string;
|
|
23
|
+
'usage.tokens.output': string;
|
|
24
|
+
'usage.tokens.cacheRead': string;
|
|
25
|
+
'usage.tokens.cacheWrite': string;
|
|
26
|
+
'usage.trend': string;
|
|
27
|
+
'usage.noData': string;
|
|
28
|
+
'usage.balance': string;
|
|
29
|
+
'usage.balance.unsupported': string;
|
|
30
|
+
'usage.balance.noCredential': string;
|
|
31
|
+
'usage.oauth': string;
|
|
32
|
+
'usage.plan.reset': string;
|
|
33
|
+
'usage.plan.noPlan': string;
|
|
34
|
+
'usage.plan.windows.5h': string;
|
|
35
|
+
'usage.plan.windows.week': string;
|
|
36
|
+
'usage.plan.windows.month': string;
|
|
37
|
+
'usage.provider.error': string;
|
|
38
|
+
'usage.config.title': string;
|
|
39
|
+
'usage.config.enabled': string;
|
|
40
|
+
'usage.config.pollIntervalSec': string;
|
|
41
|
+
'usage.config.bubbleMode': string;
|
|
42
|
+
'usage.config.bubbleMode.always': string;
|
|
43
|
+
'usage.config.bubbleMode.change': string;
|
|
44
|
+
'usage.config.bubbleMode.off': string;
|
|
45
|
+
};
|
|
46
|
+
/** English mirror; every zh key present. */
|
|
47
|
+
export declare const en: Record<UsageKey, string>;
|
|
48
|
+
export type UsageKey = keyof typeof zh;
|
|
49
|
+
/**
|
|
50
|
+
* Active dictionary, picked by the document language at call time. The
|
|
51
|
+
* section resolves its copy the same tiny way the pet's DOM-injected surface
|
|
52
|
+
* does (the settings section has no framework locale seat of its own).
|
|
53
|
+
*/
|
|
54
|
+
export declare function dictionary(): Record<UsageKey, string>;
|
|
55
|
+
/** Translate a key with optional `{name}` template params; missing keys degrade to the key. */
|
|
56
|
+
export declare function t(key: string, params?: Record<string, unknown>): string;
|
|
57
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
58
|
+
interface LocaleNamespaceMap {
|
|
59
|
+
/** dsh-usage UI copy. */
|
|
60
|
+
'dsh-web-ui-usage': UsageKey;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-side usage store: the overview snapshot polled from the host plus
|
|
3
|
+
* the fetch lifecycle. Section-local: the store lives while the settings
|
|
4
|
+
* section is mounted, so polling only runs while the page is open.
|
|
5
|
+
* @module @linxin666/dsh-usage/client/usage-store
|
|
6
|
+
*/
|
|
7
|
+
import type { EngineStoreHandle, EngineStoreInstance } from '@deepseek-ai/dsh-client-store';
|
|
8
|
+
import type { UsageOverviewView } from '../core/types.ts';
|
|
9
|
+
/** Section UI state as consumers see it. */
|
|
10
|
+
export interface UsageUiState {
|
|
11
|
+
/** Latest host overview; null before the first successful fetch. */
|
|
12
|
+
snapshot: UsageOverviewView | null;
|
|
13
|
+
/** Fetch lifecycle. */
|
|
14
|
+
status: 'loading' | 'ready' | 'error';
|
|
15
|
+
/** Transport error message, when any. */
|
|
16
|
+
error: string | null;
|
|
17
|
+
}
|
|
18
|
+
/** Store write set. */
|
|
19
|
+
export type UsageUiActions = {
|
|
20
|
+
/** Replace the overview (poll result). */
|
|
21
|
+
setSnapshot: (draft: UsageUiState, snapshot: UsageOverviewView) => void;
|
|
22
|
+
/** Mark the fetch lifecycle. */
|
|
23
|
+
setState: (draft: UsageUiState, status: UsageUiState['status'], error: string | null) => void;
|
|
24
|
+
};
|
|
25
|
+
/** Create the usage store handle (apply world only; never module-level). */
|
|
26
|
+
export declare function createUsageStore(): EngineStoreHandle<UsageUiState, UsageUiActions>;
|
|
27
|
+
export type UsageStoreInstance = EngineStoreInstance<UsageUiState, UsageUiActions>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider adapters: one entry per provider family this plugin can query for
|
|
3
|
+
* a pay-as-you-go balance or a coding-plan quota. Adapters are pure — they
|
|
4
|
+
* build an HTTP probe from a resolved credential and parse the response — so
|
|
5
|
+
* the service stays transport-only and every endpoint detail lives here.
|
|
6
|
+
*
|
|
7
|
+
* All probes run host-side (Node fetch); API keys never reach the browser.
|
|
8
|
+
* @module @linxin666/dsh-usage/core/adapters
|
|
9
|
+
*/
|
|
10
|
+
import type { PlanWindowView } from './types.ts';
|
|
11
|
+
/** One HTTP probe the service executes verbatim. */
|
|
12
|
+
export interface ProbeSpec {
|
|
13
|
+
url: string;
|
|
14
|
+
headers: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
/** Inputs an adapter may use to build a probe. */
|
|
17
|
+
export interface AdapterProbeContext {
|
|
18
|
+
/** Resolved credential value (the raw key; env refs are resolved upstream). */
|
|
19
|
+
apiKey: string;
|
|
20
|
+
}
|
|
21
|
+
/** A parsed balance fact. */
|
|
22
|
+
export interface BalanceParse {
|
|
23
|
+
currency: string;
|
|
24
|
+
totalBalance: string;
|
|
25
|
+
}
|
|
26
|
+
/** A parsed plan-quota fact. */
|
|
27
|
+
export interface PlanParse {
|
|
28
|
+
planName?: string;
|
|
29
|
+
windows: PlanWindowView[];
|
|
30
|
+
}
|
|
31
|
+
/** One provider family's probing capability. */
|
|
32
|
+
export interface ProviderAdapter {
|
|
33
|
+
/** Provider route keys this adapter serves. */
|
|
34
|
+
ids: readonly string[];
|
|
35
|
+
/** Fallback display name when the LLM runtime has none. */
|
|
36
|
+
displayName: string;
|
|
37
|
+
balance?: {
|
|
38
|
+
build(context: AdapterProbeContext): ProbeSpec;
|
|
39
|
+
parse(status: number, body: unknown): BalanceParse | undefined;
|
|
40
|
+
};
|
|
41
|
+
plan?: {
|
|
42
|
+
build(context: AdapterProbeContext): ProbeSpec;
|
|
43
|
+
parse(status: number, body: unknown): PlanParse | undefined;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The adapter registry, in no particular order. Route keys come from the
|
|
48
|
+
* pi-ai provider catalog plus the routes this deployment observed in user
|
|
49
|
+
* configuration (`zenmux`).
|
|
50
|
+
*/
|
|
51
|
+
export declare const PROVIDER_ADAPTERS: readonly ProviderAdapter[];
|
|
52
|
+
/** Find the adapter serving a provider route key, if any. */
|
|
53
|
+
export declare function adapterFor(provider: string): ProviderAdapter | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* Best-effort human message from a provider error body, for the per-provider
|
|
56
|
+
* error line. Never throws; truncated to one short sentence.
|
|
57
|
+
*/
|
|
58
|
+
export declare function providerErrorMessage(status: number, body: unknown): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The usage ledger: a pure fold from session usage facts into a per-day,
|
|
3
|
+
* per-provider, per-model totals document, plus its JSON serialization.
|
|
4
|
+
* Host-side state lives only in the document; the service owns persistence.
|
|
5
|
+
* @module @linxin666/dsh-usage/core/ledger
|
|
6
|
+
*/
|
|
7
|
+
import { type UsageLedgerDocument, type UsageTokenTotals } from './types.ts';
|
|
8
|
+
/** Local-date key (`YYYY-MM-DD`) for an epoch ms timestamp. */
|
|
9
|
+
export declare function localDateKey(ms: number): string;
|
|
10
|
+
/** An empty ledger document. */
|
|
11
|
+
export declare function createLedgerDocument(): UsageLedgerDocument;
|
|
12
|
+
/**
|
|
13
|
+
* Fold one usage report into the ledger in place. `provider` is the route key
|
|
14
|
+
* and `model` the provider-owned model id the step ran under.
|
|
15
|
+
*/
|
|
16
|
+
export declare function foldUsage(doc: UsageLedgerDocument, atMs: number, provider: string, model: string, usage: Readonly<UsageTokenTotals>): void;
|
|
17
|
+
/** Total tokens of a bucket (billed input + output; reasoning is inside output). */
|
|
18
|
+
export declare function totalTokens(totals: Readonly<UsageTokenTotals>): number;
|
|
19
|
+
/** All local-date keys in the ledger, ascending. */
|
|
20
|
+
export declare function ledgerDayKeys(doc: Readonly<UsageLedgerDocument>): string[];
|
|
21
|
+
/**
|
|
22
|
+
* Drop every day older than `retainDays` local days before `todayKey`, in
|
|
23
|
+
* place. Returns the number of pruned days.
|
|
24
|
+
*/
|
|
25
|
+
export declare function pruneLedger(doc: UsageLedgerDocument, todayKey: string, retainDays: number): number;
|
|
26
|
+
/**
|
|
27
|
+
* Parse a ledger document from untrusted JSON: unknown shapes resolve to an
|
|
28
|
+
* empty document, malformed entries are dropped, numbers are coerced to
|
|
29
|
+
* finite values. Never throws.
|
|
30
|
+
*/
|
|
31
|
+
export declare function deserializeLedger(value: unknown): UsageLedgerDocument;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-facing view types shared by the dsh-usage host service, its HTTP
|
|
3
|
+
* routes, and the browser section. Everything here crosses the wire as JSON
|
|
4
|
+
* and must stay provider-agnostic: provider quirks are normalized inside the
|
|
5
|
+
* core adapters, never here.
|
|
6
|
+
* @module @linxin666/dsh-usage/core/types
|
|
7
|
+
*/
|
|
8
|
+
/** One disjoint token bucket total, aggregated over calls (billed input = input + cacheRead + cacheWrite). */
|
|
9
|
+
export interface UsageTokenTotals {
|
|
10
|
+
/** Uncached input tokens. */
|
|
11
|
+
inputTokens: number;
|
|
12
|
+
/** Output tokens (reasoning included). */
|
|
13
|
+
outputTokens: number;
|
|
14
|
+
/** Cache read tokens. */
|
|
15
|
+
cacheReadTokens: number;
|
|
16
|
+
/** Cache write tokens. */
|
|
17
|
+
cacheWriteTokens: number;
|
|
18
|
+
/** Reasoning tokens, when the provider reported them. */
|
|
19
|
+
reasoningTokens: number;
|
|
20
|
+
/** Provider calls that reported usage. */
|
|
21
|
+
calls: number;
|
|
22
|
+
}
|
|
23
|
+
/** A zeroed totals bucket. */
|
|
24
|
+
export declare function emptyTotals(): UsageTokenTotals;
|
|
25
|
+
/** Add `right` into `left` in place. */
|
|
26
|
+
export declare function addTotals(left: UsageTokenTotals, right: Readonly<UsageTokenTotals>): UsageTokenTotals;
|
|
27
|
+
/** On-disk usage ledger: per local day, per provider route, per model. */
|
|
28
|
+
export interface UsageLedgerDocument {
|
|
29
|
+
version: 1;
|
|
30
|
+
/** Local-date key `YYYY-MM-DD` → per-provider per-model totals. */
|
|
31
|
+
days: Record<string, Record<string, Record<string, UsageTokenTotals>>>;
|
|
32
|
+
}
|
|
33
|
+
/** One provider row of the day summary the overview serves. */
|
|
34
|
+
export interface UsageProviderSummary {
|
|
35
|
+
provider: string;
|
|
36
|
+
totals: UsageTokenTotals;
|
|
37
|
+
/** Per-model breakdown, heaviest by total tokens first, capped by the route. */
|
|
38
|
+
models: Array<{
|
|
39
|
+
model: string;
|
|
40
|
+
totals: UsageTokenTotals;
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
/** One day row of the trend the overview serves. */
|
|
44
|
+
export interface UsageDaySummary {
|
|
45
|
+
date: string;
|
|
46
|
+
totals: UsageTokenTotals;
|
|
47
|
+
}
|
|
48
|
+
/** A balance fact normalized from a provider probe. */
|
|
49
|
+
export interface BalanceView {
|
|
50
|
+
/** ISO 4217 code the provider bills in (`CNY`, `USD`, ...). */
|
|
51
|
+
currency: string;
|
|
52
|
+
/** Spendable total formatted for display (no currency symbol). */
|
|
53
|
+
totalBalance: string;
|
|
54
|
+
/** Epoch ms of the successful probe. */
|
|
55
|
+
updatedAt: number;
|
|
56
|
+
}
|
|
57
|
+
/** One quota window of a coding plan. */
|
|
58
|
+
export interface PlanWindowView {
|
|
59
|
+
/** Stable window key the UI localizes (`5h`, `week`, `month`, provider strings otherwise). */
|
|
60
|
+
key: string;
|
|
61
|
+
/** Provider-supplied window name, when it sends one. */
|
|
62
|
+
name?: string;
|
|
63
|
+
/** Used percent 0-100, when computable. */
|
|
64
|
+
percent?: number;
|
|
65
|
+
/** ISO 8601 reset instant, when the provider reports one. */
|
|
66
|
+
resetsAt?: string;
|
|
67
|
+
}
|
|
68
|
+
/** A coding-plan quota fact normalized from a provider probe. */
|
|
69
|
+
export interface PlanView {
|
|
70
|
+
/** Plan tier name, when the provider reports one. */
|
|
71
|
+
planName?: string;
|
|
72
|
+
windows: PlanWindowView[];
|
|
73
|
+
updatedAt: number;
|
|
74
|
+
}
|
|
75
|
+
/** How the credential backing one provider route was resolved. */
|
|
76
|
+
export type CredentialKind = 'api-key' | 'env' | 'oauth' | 'none';
|
|
77
|
+
/** One provider row of the overview snapshot. */
|
|
78
|
+
export interface ProviderSnapshotView {
|
|
79
|
+
/** Provider route key (`deepseek`, `kimi-coding`, custom routes, ...). */
|
|
80
|
+
provider: string;
|
|
81
|
+
/** Display name from the LLM runtime, else the adapter's, else the route key. */
|
|
82
|
+
displayName: string;
|
|
83
|
+
credential: CredentialKind;
|
|
84
|
+
/** Whether a balance/plan adapter exists for this route. */
|
|
85
|
+
supported: boolean;
|
|
86
|
+
balance?: BalanceView;
|
|
87
|
+
plan?: PlanView;
|
|
88
|
+
/** Last probe failure, cleared on the next success. */
|
|
89
|
+
error?: string;
|
|
90
|
+
updatedAt?: number;
|
|
91
|
+
}
|
|
92
|
+
/** The overview the browser section renders. */
|
|
93
|
+
export interface UsageOverviewView {
|
|
94
|
+
updatedAt: number;
|
|
95
|
+
providers: ProviderSnapshotView[];
|
|
96
|
+
/** The provider the pet bubble and the header highlight; `source` says how it was picked. */
|
|
97
|
+
current: {
|
|
98
|
+
provider?: string;
|
|
99
|
+
model?: string;
|
|
100
|
+
/** `live` — last request seen this boot; `default` — the agent default model. */
|
|
101
|
+
source: 'live' | 'default';
|
|
102
|
+
};
|
|
103
|
+
usage: {
|
|
104
|
+
today: {
|
|
105
|
+
date: string;
|
|
106
|
+
totals: UsageTokenTotals;
|
|
107
|
+
providers: UsageProviderSummary[];
|
|
108
|
+
};
|
|
109
|
+
/** Last N local days ascending, including today. */
|
|
110
|
+
days: UsageDaySummary[];
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH_HOME resolution shared by the plugin family's Host halves: the
|
|
3
|
+
* environment override wins, the platform home fallback follows. Mirrors
|
|
4
|
+
* what dsh-pet and dsh-liangshen each used to implement locally.
|
|
5
|
+
*/
|
|
6
|
+
/** Expand a leading ~ (or ~user) in a path, platform-style. */
|
|
7
|
+
export declare function expandHome(path: string, home?: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the DSH home directory.
|
|
10
|
+
* @param env - process environment to read DSH_HOME from.
|
|
11
|
+
* @param home - platform home directory fallback (test seam).
|
|
12
|
+
* @returns the absolute DSH home path.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveDshHome(env?: NodeJS.ProcessEnv, home?: string): string;
|
|
15
|
+
/** Resolve the DSH home directory from the live environment. */
|
|
16
|
+
export declare function dshHome(): string;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared JSON body/response helpers for the host route families: one strict
|
|
3
|
+
* bounded body reader, one lenient bounded body reader, one JSON object
|
|
4
|
+
* narrow, and one JSON writer. Previously these were copy-pasted across the
|
|
5
|
+
* package route files (routes.ts, update-routes.ts, mobile-api.ts, and each
|
|
6
|
+
* family's route module) with drifting contracts: body caps ranging 4 KiB to
|
|
7
|
+
* 1 MiB and four distinct overflow behaviors (reject, undefined, null, throw).
|
|
8
|
+
*
|
|
9
|
+
* Packages receive this file as a generated copy via scripts/sync-shared.mjs;
|
|
10
|
+
* edit this shared source and re-run the sync instead of editing a copy.
|
|
11
|
+
* Consumer code is migrated onto it in follow-up waves; no call site changes
|
|
12
|
+
* belong in the same change as its introduction.
|
|
13
|
+
* @module dsh-web-shared/host/http
|
|
14
|
+
*/
|
|
15
|
+
import type { IncomingMessage, OutgoingHttpHeaders, ServerResponse } from 'node:http';
|
|
16
|
+
/**
|
|
17
|
+
* Strict bounded body reader: parse a request body of at most maxBytes as
|
|
18
|
+
* JSON.
|
|
19
|
+
* @throws 'body too large' past the cap, or the JSON.parse error for an
|
|
20
|
+
* invalid or empty payload.
|
|
21
|
+
*/
|
|
22
|
+
export declare function readBoundedJson(req: IncomingMessage, maxBytes: number): Promise<unknown>;
|
|
23
|
+
/**
|
|
24
|
+
* Lenient bounded body reader: parse a request body as JSON, or null on an
|
|
25
|
+
* empty body, invalid JSON, or a body past maxBytes (default 64 KiB).
|
|
26
|
+
* Overflow destroys the request instead of draining the remainder (no drain
|
|
27
|
+
* call, matching the current repo-wide behavior); callers must not keep
|
|
28
|
+
* reading the request afterwards. With objectOnly, non-JSON-object payloads
|
|
29
|
+
* also yield null.
|
|
30
|
+
*/
|
|
31
|
+
export declare function readJsonBody(req: IncomingMessage, opts?: {
|
|
32
|
+
maxBytes?: number;
|
|
33
|
+
objectOnly?: boolean;
|
|
34
|
+
}): Promise<unknown | null>;
|
|
35
|
+
/** Narrow a value to a JSON object, or undefined when it is not one. */
|
|
36
|
+
export declare function asJsonObject(value: unknown): Record<string, unknown> | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Write one JSON response. Default headers are the family defaults
|
|
39
|
+
* (content-type and referrer-policy); caller headers are appended or
|
|
40
|
+
* override them.
|
|
41
|
+
*/
|
|
42
|
+
export declare function writeJson(res: ServerResponse, status: number, body: unknown, headers?: OutgoingHttpHeaders): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loopback trust fence shared by the host route families: socket address,
|
|
3
|
+
* Host header, and browser same-origin markers. Packages receive this file as
|
|
4
|
+
* a generated copy via scripts/sync-shared.mjs; edit the shared source and
|
|
5
|
+
* re-run the sync instead of editing a copy.
|
|
6
|
+
*
|
|
7
|
+
* Semantics: RFC 5735 IPv4 127/8, ::1, IPv4-mapped ::ffff:127/8 (matching the
|
|
8
|
+
* remote-web-ui gate), localhost hostnames, plus the browser same-origin
|
|
9
|
+
* markers (sec-fetch-site and Origin) for the request-level fence.
|
|
10
|
+
* @module dsh-web-shared/host/loopback
|
|
11
|
+
*/
|
|
12
|
+
import type { IncomingMessage } from 'node:http';
|
|
13
|
+
/** IPv4 127/8 predicate (four decimal octets, first == 127). */
|
|
14
|
+
export declare function isIPv4Loopback(v4: string): boolean;
|
|
15
|
+
/** Whether a socket remote address names the loopback range (127/8, ::1, IPv4-mapped). */
|
|
16
|
+
export declare function isLoopbackAddress(address: string | undefined): boolean;
|
|
17
|
+
/** Whether a normalized URL hostname names the loopback authority (localhost, [::1], 127/8). */
|
|
18
|
+
export declare function isLoopbackHostname(hostname: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Request-level trust fence: a loopback socket address AND a loopback Host
|
|
21
|
+
* header, plus browser same-origin markers. The socket address is
|
|
22
|
+
* authoritative; X-Forwarded-For is never trusted.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isLoopbackRequest(request: IncomingMessage): boolean;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
|
|
2
|
+
import type { UsageService } from './usage-service.ts';
|
|
3
|
+
export declare const USAGE_API_PREFIX = "/api/dsh-usage";
|
|
4
|
+
/**
|
|
5
|
+
* Loopback-fenced overview route: provider balances, plan quotas, and token
|
|
6
|
+
* usage totals. Personal account data, so the loopback fence mirrors
|
|
7
|
+
* dsh-perf's stats surface; the browser runs on the same machine.
|
|
8
|
+
*/
|
|
9
|
+
export declare function makeUsageOverviewRoute(service: UsageService): WebRoute;
|
|
10
|
+
/**
|
|
11
|
+
* Loopback-fenced manual refresh: forces one probe cycle now and answers
|
|
12
|
+
* with the fresh overview.
|
|
13
|
+
*/
|
|
14
|
+
export declare function makeUsageRefreshRoute(service: UsageService): WebRoute;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dsh-usage host service: folds live session usage into the persistent
|
|
3
|
+
* ledger, probes each configured provider's balance/coding-plan endpoint on
|
|
4
|
+
* a poll cycle, and announces the current provider's status to the pet
|
|
5
|
+
* bubble. Secrets stay in the host process; the browser only ever sees the
|
|
6
|
+
* overview document.
|
|
7
|
+
* @module @linxin666/dsh-usage/host/usage-service
|
|
8
|
+
*/
|
|
9
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
10
|
+
import type { UsageOverviewView } from '../core/types.ts';
|
|
11
|
+
/** Source tag the plugin stamps onto pet announcements. */
|
|
12
|
+
export declare const USAGE_ANNOUNCE_SOURCE = "dsh-usage";
|
|
13
|
+
/** Poll-loop and announce options; re-applied live on settings change. */
|
|
14
|
+
export interface UsageServiceOptions {
|
|
15
|
+
pollIntervalSec: number;
|
|
16
|
+
bubbleMode: 'always' | 'change' | 'off';
|
|
17
|
+
retainDays: number;
|
|
18
|
+
}
|
|
19
|
+
/** Format a balance for display: symbol prefix when known, code suffix otherwise. */
|
|
20
|
+
export declare function formatMoney(currency: string, totalBalance: string): string;
|
|
21
|
+
/** Map a used percent to the announcement tone. */
|
|
22
|
+
export declare function planTone(percent: number): 'ok' | 'warn' | 'low';
|
|
23
|
+
export declare class UsageService {
|
|
24
|
+
private readonly ctx;
|
|
25
|
+
private options;
|
|
26
|
+
private readonly persistDir;
|
|
27
|
+
private readonly ledgerPath;
|
|
28
|
+
private readonly snapshotsPath;
|
|
29
|
+
private ledger;
|
|
30
|
+
private readonly snapshots;
|
|
31
|
+
/** Per-live-session route attribution (WeakMap: disposed sessions age out). */
|
|
32
|
+
private readonly sessionRoutes;
|
|
33
|
+
/** The most recent route seen this boot; the pet bubble follows it. */
|
|
34
|
+
private current;
|
|
35
|
+
private sessionListenerDisposer;
|
|
36
|
+
private pollTimer;
|
|
37
|
+
private flushTimer;
|
|
38
|
+
private pollInFlight;
|
|
39
|
+
private disposed;
|
|
40
|
+
private lastSignature;
|
|
41
|
+
constructor(ctx: Context, options: UsageServiceOptions);
|
|
42
|
+
/** Start the listeners, load persisted state, and arm the first poll. */
|
|
43
|
+
start(): void;
|
|
44
|
+
/** Stop timers and flush pending ledger writes. */
|
|
45
|
+
stop(): void;
|
|
46
|
+
/** Re-apply options live (settings change); the poll cycle picks them up. */
|
|
47
|
+
applyOptions(options: UsageServiceOptions): void;
|
|
48
|
+
/** Force one poll now (manual refresh route). */
|
|
49
|
+
refresh(): Promise<void>;
|
|
50
|
+
/** Assemble the overview document the browser section renders. */
|
|
51
|
+
overview(): UsageOverviewView;
|
|
52
|
+
/** One local day aggregated per provider. */
|
|
53
|
+
private daySummary;
|
|
54
|
+
private onSessionEvent;
|
|
55
|
+
/** Normalize a provider TokenUsage into the ledger bucket (one call). */
|
|
56
|
+
private totalsFrom;
|
|
57
|
+
private loadPersisted;
|
|
58
|
+
private scheduleFlush;
|
|
59
|
+
private flushLedger;
|
|
60
|
+
private persistSnapshots;
|
|
61
|
+
private rearmPoll;
|
|
62
|
+
/** One poll cycle: enumerate routes, resolve credentials, probe, announce. */
|
|
63
|
+
pollNow(): Promise<void>;
|
|
64
|
+
private listProviderRoutes;
|
|
65
|
+
private probeRoute;
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the credential backing one route: pi-ai credential records
|
|
68
|
+
* first, then the profile's apiKeyEnv reference, then the DeepSeek
|
|
69
|
+
* official adapter's env reference. OAuth grants resolve to a kind with
|
|
70
|
+
* no key (this plugin does not spend third-party OAuth budgets).
|
|
71
|
+
*/
|
|
72
|
+
private resolveCredential;
|
|
73
|
+
/** The llm-pi-ai profile object for one provider route, when configured. */
|
|
74
|
+
private piAiProfile;
|
|
75
|
+
/** The DeepSeek official adapter's credential reference name. */
|
|
76
|
+
private deepseekApiKeyEnv;
|
|
77
|
+
/**
|
|
78
|
+
* Announce the current provider's balance or plan usage to the pet. In
|
|
79
|
+
* `change` mode only meaningful value changes re-announce; `off` skips.
|
|
80
|
+
*/
|
|
81
|
+
private announceCurrent;
|
|
82
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import z from 'schemastery';
|
|
3
|
+
import { type UsageServiceOptions } from './host/usage-service.ts';
|
|
4
|
+
export declare const name = "dsh-usage";
|
|
5
|
+
export declare const inject: string[];
|
|
6
|
+
export declare const USAGE_SETTINGS_NAMESPACE: import("@deepseek-ai/dsh-settings").SettingsNamespace;
|
|
7
|
+
export interface Config {
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
/** Provider probe cycle in seconds; 30-3600. */
|
|
10
|
+
pollIntervalSec?: number;
|
|
11
|
+
/** Pet bubble mode: always (refreshes each poll), change (only on value change), off. */
|
|
12
|
+
bubbleMode?: string;
|
|
13
|
+
/** Ledger retention in local days. */
|
|
14
|
+
retainDays?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare const Config: z<Config>;
|
|
17
|
+
export interface ResolvedConfig extends UsageServiceOptions {
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function resolveConfig(config?: Config): ResolvedConfig;
|
|
21
|
+
export declare const apply: (ctx: Context, config?: Config) => void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host single-instance guard shared by the plugin family. The family bundle
|
|
3
|
+
* (dsh-web-all / dsh-skins) namespaces every child row id (web-ui-*), so
|
|
4
|
+
* the loader accepts a standalone install of the same package side by side;
|
|
5
|
+
* without this guard the second instance would still re-register the same
|
|
6
|
+
* webserver routes, tools, settings namespaces, and system-prompt sections
|
|
7
|
+
* and fail the boot. mountOnce makes the second host apply a no-op for the
|
|
8
|
+
* lifetime of the first instance (the browser half is already deduped by
|
|
9
|
+
* package name in the client module host).
|
|
10
|
+
*
|
|
11
|
+
* The registry rides a global symbol so two module instances of the same
|
|
12
|
+
* package (npm copy vs repository link) still share one verdict. cordis
|
|
13
|
+
* `ctx.effect` runs its callback immediately and treats the callback's
|
|
14
|
+
* return value as the fiber disposer, so the unmarker is returned, not run.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Wrap a cordis plugin apply so the package runs at most once per process.
|
|
18
|
+
* The first mount registers normally and unmarks when its fiber disposes;
|
|
19
|
+
* any later mount of the same package name is a no-op.
|
|
20
|
+
* @param packageName - npm package identity shared by every install source.
|
|
21
|
+
* @param fn - the original plugin apply.
|
|
22
|
+
* @returns an apply of the same shape.
|
|
23
|
+
*/
|
|
24
|
+
export declare function mountOnce<T extends (...args: any[]) => unknown>(packageName: string, fn: T): T;
|