@jxgame2020/dsh-token-quota 0.1.34 → 0.1.36
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/lib/client.js +692 -151
- package/lib/index.js +199 -4
- package/lib/types/client/TokenQuotaPanel.d.ts +5 -1
- package/lib/types/client/TokenQuotaPanel.js +303 -15
- package/lib/types/client/index.js +29 -1
- package/lib/types/client/locales.d.ts +46 -0
- package/lib/types/client/locales.js +46 -0
- package/lib/types/client/store.d.ts +11 -1
- package/lib/types/client/store.js +5 -0
- package/lib/types/index.d.ts +26 -6
- package/lib/types/index.js +231 -3
- package/lib/types/types.d.ts +48 -0
- package/lib/types/types.js +10 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -23,6 +23,10 @@ import { LlmError } from "@deepseek-ai/dsh-llm";
|
|
|
23
23
|
const TOKEN_QUOTA_NAMESPACE = "token-quota";
|
|
24
24
|
/** Stable machine code thrown from `agent/request` when a model is over its daily cap. */
|
|
25
25
|
const TOKEN_QUOTA_EXCEEDED_CODE = "TOKEN_QUOTA_EXCEEDED";
|
|
26
|
+
const TOKEN_QUOTA_DEFAULT_BALANCE = {
|
|
27
|
+
enabled: true,
|
|
28
|
+
pollMinutes: 5
|
|
29
|
+
};
|
|
26
30
|
/** Default music settings: silent until the user flips the panel switch. */
|
|
27
31
|
const TOKEN_QUOTA_DEFAULT_MUSIC = {
|
|
28
32
|
enabled: false,
|
|
@@ -236,7 +240,11 @@ const TOKEN_QUOTA_SETTINGS_SCHEMA = z.object({
|
|
|
236
240
|
"pentatonic"
|
|
237
241
|
]).default(TOKEN_QUOTA_DEFAULT_MUSIC.style),
|
|
238
242
|
onlyCurrentSession: z.boolean().default(TOKEN_QUOTA_DEFAULT_MUSIC.onlyCurrentSession)
|
|
239
|
-
}).default({ ...TOKEN_QUOTA_DEFAULT_MUSIC })
|
|
243
|
+
}).default({ ...TOKEN_QUOTA_DEFAULT_MUSIC }),
|
|
244
|
+
balance: z.object({
|
|
245
|
+
enabled: z.boolean().default(TOKEN_QUOTA_DEFAULT_BALANCE.enabled),
|
|
246
|
+
pollMinutes: z.number().min(1).max(120).default(TOKEN_QUOTA_DEFAULT_BALANCE.pollMinutes)
|
|
247
|
+
}).default({ ...TOKEN_QUOTA_DEFAULT_BALANCE })
|
|
240
248
|
});
|
|
241
249
|
/** Two-digit zero-pad helper. */
|
|
242
250
|
function pad2(value) {
|
|
@@ -274,6 +282,17 @@ function usageTokens(usage) {
|
|
|
274
282
|
* Mount it beside the other rows (`@jxgame2020/dsh-token-quota`) and write
|
|
275
283
|
* per-model limits through the `token-quota` settings namespace.
|
|
276
284
|
*/
|
|
285
|
+
/**
|
|
286
|
+
* Providers the host can query for account balance, keyed by route key. Each
|
|
287
|
+
* entry names the credential reference (environment-variable name), the
|
|
288
|
+
* balance endpoint base, and the optional `*_BASE_URL` override variable.
|
|
289
|
+
* Directory providers outside this map render as "not integrated for query".
|
|
290
|
+
*/
|
|
291
|
+
const BALANCE_SOURCES = { deepseek: {
|
|
292
|
+
keyEnv: "DEEPSEEK_API_KEY",
|
|
293
|
+
baseURL: "https://api.deepseek.com",
|
|
294
|
+
baseEnv: "DEEPSEEK_BASE_URL"
|
|
295
|
+
} };
|
|
277
296
|
var TokenQuotaService = class TokenQuotaService extends Service {
|
|
278
297
|
static Config = z.object({ storagePath: z.string().default("") });
|
|
279
298
|
storagePath;
|
|
@@ -295,7 +314,8 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
295
314
|
dimWhenIdle: false,
|
|
296
315
|
music: { ...TOKEN_QUOTA_DEFAULT_MUSIC },
|
|
297
316
|
order: [],
|
|
298
|
-
notes: []
|
|
317
|
+
notes: [],
|
|
318
|
+
balance: { ...TOKEN_QUOTA_DEFAULT_BALANCE }
|
|
299
319
|
});
|
|
300
320
|
/** Whether update checks are enabled (mirrors the settings document). */
|
|
301
321
|
checkUpdates = true;
|
|
@@ -305,6 +325,14 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
305
325
|
upgradeError = null;
|
|
306
326
|
/** Timer for the periodic update check. */
|
|
307
327
|
updateTimer;
|
|
328
|
+
/** Whether the account balance is enabled (mirrors the settings document). */
|
|
329
|
+
balanceEnabled = TOKEN_QUOTA_DEFAULT_BALANCE.enabled;
|
|
330
|
+
/** Cached per-provider account balances, served in the snapshot. */
|
|
331
|
+
balances = /* @__PURE__ */ new Map();
|
|
332
|
+
/** Timer for the periodic balance poll. */
|
|
333
|
+
balanceTimer;
|
|
334
|
+
/** In-flight balance refreshes by provider, to coalesce overlapping triggers. */
|
|
335
|
+
balanceFetches = /* @__PURE__ */ new Map();
|
|
308
336
|
/** In-flight update check, to coalesce the periodic and manual triggers. */
|
|
309
337
|
upgradeCheck;
|
|
310
338
|
/** Per-session folded model key from the latest `request/header`. */
|
|
@@ -323,6 +351,8 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
323
351
|
disposeRouteCheck;
|
|
324
352
|
/** Disposer for the optional clear-log route (`POST /token-quota/clear-log`). */
|
|
325
353
|
disposeRouteClear;
|
|
354
|
+
/** Disposer for the optional balance-refresh route (`POST /token-quota/refresh-balance`). */
|
|
355
|
+
disposeRouteBalance;
|
|
326
356
|
/** Disposer for the optional music-event route (`GET /token-quota/events`, SSE). */
|
|
327
357
|
disposeRouteEvents;
|
|
328
358
|
/** Live SSE subscribers receiving streamed music actions. */
|
|
@@ -349,6 +379,10 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
349
379
|
checkUpdates: doc.checkUpdates ?? true,
|
|
350
380
|
dimWhenIdle: doc.dimWhenIdle ?? false,
|
|
351
381
|
order: Array.isArray(doc.order) ? doc.order : [],
|
|
382
|
+
balance: {
|
|
383
|
+
enabled: doc.balance?.enabled ?? TOKEN_QUOTA_DEFAULT_BALANCE.enabled,
|
|
384
|
+
pollMinutes: typeof doc.balance?.pollMinutes === "number" ? Math.max(1, Math.min(120, Math.round(doc.balance.pollMinutes))) : TOKEN_QUOTA_DEFAULT_BALANCE.pollMinutes
|
|
385
|
+
},
|
|
352
386
|
notes: Array.isArray(doc.notes) ? doc.notes.slice(0, 5).flatMap((note) => {
|
|
353
387
|
if (typeof note.id !== "string") return [];
|
|
354
388
|
return [{
|
|
@@ -385,6 +419,12 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
385
419
|
this.monitored = doc.monitored !== void 0 && doc.monitored.length > 0 ? new Set(doc.monitored) : void 0;
|
|
386
420
|
this.checkUpdates = doc.checkUpdates;
|
|
387
421
|
this.syncUpdateChecking();
|
|
422
|
+
const balanceEnabled = doc.balance?.enabled ?? TOKEN_QUOTA_DEFAULT_BALANCE.enabled;
|
|
423
|
+
const turnedOn = balanceEnabled && !this.balanceEnabled;
|
|
424
|
+
this.balanceEnabled = balanceEnabled;
|
|
425
|
+
if (!balanceEnabled) this.balances.clear();
|
|
426
|
+
this.syncBalancePolling();
|
|
427
|
+
if (turnedOn) this.refreshAllBalances();
|
|
388
428
|
if (doc.reset !== void 0 && typeof doc.reset === "object" && doc.reset !== null && typeof doc.reset.offsetHours === "number" && typeof doc.reset.hour === "number" && typeof doc.reset.minute === "number") {
|
|
389
429
|
const configured = doc.reset;
|
|
390
430
|
this.reset = {
|
|
@@ -415,6 +455,8 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
415
455
|
agent.ctx.on("agent/request", async (payload, next) => this.onRequest(payload, next), { prepend: true });
|
|
416
456
|
});
|
|
417
457
|
this.syncUpdateChecking();
|
|
458
|
+
this.syncBalancePolling();
|
|
459
|
+
if (this.balanceEnabled) this.refreshAllBalances();
|
|
418
460
|
ctx.effect(() => () => {
|
|
419
461
|
this.disposeLocal();
|
|
420
462
|
}, "token-quota: flush on unload");
|
|
@@ -461,6 +503,39 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
461
503
|
});
|
|
462
504
|
}
|
|
463
505
|
});
|
|
506
|
+
this.disposeRouteBalance = server.register({
|
|
507
|
+
kind: "exact",
|
|
508
|
+
path: "/token-quota/refresh-balance",
|
|
509
|
+
handler: (req, res) => {
|
|
510
|
+
if (req.method !== "POST") {
|
|
511
|
+
res.writeHead(405);
|
|
512
|
+
res.end();
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const chunks = [];
|
|
516
|
+
req.on("data", (chunk) => {
|
|
517
|
+
chunks.push(Buffer.from(chunk));
|
|
518
|
+
});
|
|
519
|
+
req.on("end", () => {
|
|
520
|
+
let provider;
|
|
521
|
+
try {
|
|
522
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
523
|
+
if (raw !== "") {
|
|
524
|
+
const parsed = JSON.parse(raw);
|
|
525
|
+
if (typeof parsed === "object" && parsed !== null && typeof parsed.provider === "string") provider = parsed.provider;
|
|
526
|
+
}
|
|
527
|
+
} catch {}
|
|
528
|
+
(provider !== void 0 && provider in BALANCE_SOURCES ? this.refreshProviderBalance(provider) : this.refreshAllBalances()).finally(() => {
|
|
529
|
+
const body = JSON.stringify(this.readSnapshot());
|
|
530
|
+
res.writeHead(200, {
|
|
531
|
+
"content-type": "application/json",
|
|
532
|
+
"cache-control": "no-store"
|
|
533
|
+
});
|
|
534
|
+
res.end(body);
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
});
|
|
464
539
|
this.disposeRouteClear = server.register({
|
|
465
540
|
kind: "exact",
|
|
466
541
|
path: "/token-quota/clear-log",
|
|
@@ -544,7 +619,7 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
544
619
|
return this.upgradeCheck;
|
|
545
620
|
}
|
|
546
621
|
async doRefreshUpgrade() {
|
|
547
|
-
const current = "0.1.
|
|
622
|
+
const current = "0.1.36";
|
|
548
623
|
let latest;
|
|
549
624
|
try {
|
|
550
625
|
const response = await fetch(REGISTRY_LATEST_URL, {
|
|
@@ -595,6 +670,119 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
595
670
|
this.syncUpdateChecking();
|
|
596
671
|
}, UPDATE_CHECK_INTERVAL_MS);
|
|
597
672
|
}
|
|
673
|
+
/**
|
|
674
|
+
* Start or stop the periodic balance poll to match the current setting. The
|
|
675
|
+
* interval comes from the settings document; a poll chain re-arms itself
|
|
676
|
+
* after each refresh. No fetch happens here — callers decide (startup and
|
|
677
|
+
* the enabled-transition in `onChange` fetch immediately).
|
|
678
|
+
*/
|
|
679
|
+
syncBalancePolling() {
|
|
680
|
+
if (this.balanceTimer !== void 0) {
|
|
681
|
+
clearTimeout(this.balanceTimer);
|
|
682
|
+
this.balanceTimer = void 0;
|
|
683
|
+
}
|
|
684
|
+
if (!this.balanceEnabled) return;
|
|
685
|
+
const minutes = this.settingsSource().balance?.pollMinutes ?? TOKEN_QUOTA_DEFAULT_BALANCE.pollMinutes;
|
|
686
|
+
const schedule = () => {
|
|
687
|
+
if (!this.balanceEnabled) return;
|
|
688
|
+
this.balanceTimer = setTimeout(() => {
|
|
689
|
+
this.balanceTimer = void 0;
|
|
690
|
+
this.refreshAllBalances().finally(schedule);
|
|
691
|
+
}, Math.max(6e4, minutes * 6e4));
|
|
692
|
+
};
|
|
693
|
+
schedule();
|
|
694
|
+
}
|
|
695
|
+
/** Refresh every provider the host knows how to query. */
|
|
696
|
+
refreshAllBalances() {
|
|
697
|
+
return Promise.all(Object.keys(BALANCE_SOURCES).map((provider) => this.refreshProviderBalance(provider))).then(() => void 0);
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Refresh one provider's cached account balance. The API key is resolved
|
|
701
|
+
* through the harness credential seam (`credentials.resolve`) with the
|
|
702
|
+
* environment as a fallback, so the Models page key is used with nothing to
|
|
703
|
+
* type. A failed fetch keeps the previous value and marks it as an error.
|
|
704
|
+
*/
|
|
705
|
+
async refreshProviderBalance(provider) {
|
|
706
|
+
const source = BALANCE_SOURCES[provider];
|
|
707
|
+
if (source === void 0) return;
|
|
708
|
+
const existing = this.balanceFetches.get(provider);
|
|
709
|
+
if (existing !== void 0) return existing;
|
|
710
|
+
const run = async () => {
|
|
711
|
+
if (!this.balanceEnabled) return;
|
|
712
|
+
const credentials = this.ctx.get("credentials");
|
|
713
|
+
let key;
|
|
714
|
+
if (credentials !== void 0) try {
|
|
715
|
+
key = (await credentials.resolve(source.keyEnv))?.value;
|
|
716
|
+
} catch {}
|
|
717
|
+
if (key === void 0 || key === "") key = process.env[source.keyEnv];
|
|
718
|
+
if (key === void 0 || key === "") {
|
|
719
|
+
this.balances.set(provider, {
|
|
720
|
+
provider,
|
|
721
|
+
currency: "CNY",
|
|
722
|
+
total: 0,
|
|
723
|
+
isAvailable: false,
|
|
724
|
+
fetchedAt: Date.now(),
|
|
725
|
+
status: "unconfigured"
|
|
726
|
+
});
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
const base = source.baseEnv !== void 0 ? process.env[source.baseEnv] ?? source.baseURL : source.baseURL;
|
|
730
|
+
try {
|
|
731
|
+
const controller = new AbortController();
|
|
732
|
+
const timer = setTimeout(() => {
|
|
733
|
+
controller.abort();
|
|
734
|
+
}, 1e4);
|
|
735
|
+
let res;
|
|
736
|
+
try {
|
|
737
|
+
res = await fetch(`${base}/user/balance`, {
|
|
738
|
+
headers: {
|
|
739
|
+
authorization: `Bearer ${key}`,
|
|
740
|
+
accept: "application/json"
|
|
741
|
+
},
|
|
742
|
+
signal: controller.signal
|
|
743
|
+
});
|
|
744
|
+
} finally {
|
|
745
|
+
clearTimeout(timer);
|
|
746
|
+
}
|
|
747
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
748
|
+
const data = await res.json();
|
|
749
|
+
const first = Array.isArray(data?.balance_infos) ? data.balance_infos[0] : void 0;
|
|
750
|
+
const num = (value) => {
|
|
751
|
+
const n = typeof value === "number" ? value : typeof value === "string" && value !== "" ? Number(value) : NaN;
|
|
752
|
+
return Number.isFinite(n) ? n : 0;
|
|
753
|
+
};
|
|
754
|
+
this.balances.set(provider, {
|
|
755
|
+
provider,
|
|
756
|
+
currency: typeof first?.currency === "string" && first.currency !== "" ? first.currency : "CNY",
|
|
757
|
+
total: num(first?.total_balance),
|
|
758
|
+
granted: num(first?.granted_balance),
|
|
759
|
+
toppedUp: num(first?.topped_up_balance),
|
|
760
|
+
isAvailable: data?.is_available !== false,
|
|
761
|
+
fetchedAt: Date.now(),
|
|
762
|
+
status: "ok"
|
|
763
|
+
});
|
|
764
|
+
} catch (error) {
|
|
765
|
+
const previous = this.balances.get(provider);
|
|
766
|
+
const next = {
|
|
767
|
+
provider,
|
|
768
|
+
currency: previous?.currency ?? "CNY",
|
|
769
|
+
total: previous?.total ?? 0,
|
|
770
|
+
isAvailable: false,
|
|
771
|
+
fetchedAt: previous?.fetchedAt ?? Date.now(),
|
|
772
|
+
status: "error",
|
|
773
|
+
error: error instanceof Error ? error.message : String(error)
|
|
774
|
+
};
|
|
775
|
+
if (previous?.granted !== void 0) next.granted = previous.granted;
|
|
776
|
+
if (previous?.toppedUp !== void 0) next.toppedUp = previous.toppedUp;
|
|
777
|
+
this.balances.set(provider, next);
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
const promise = run().finally(() => {
|
|
781
|
+
this.balanceFetches.delete(provider);
|
|
782
|
+
});
|
|
783
|
+
this.balanceFetches.set(provider, promise);
|
|
784
|
+
return promise;
|
|
785
|
+
}
|
|
598
786
|
/** Read the full per-cycle usage history (log dialog data). */
|
|
599
787
|
readLog() {
|
|
600
788
|
this.rollCycleIfNeeded();
|
|
@@ -654,12 +842,14 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
654
842
|
});
|
|
655
843
|
}
|
|
656
844
|
entries.sort((left, right) => left.key.localeCompare(right.key));
|
|
657
|
-
|
|
845
|
+
const snapshot = {
|
|
658
846
|
day: this.cycle,
|
|
659
847
|
entries,
|
|
660
848
|
upgrade: this.upgrade,
|
|
661
849
|
upgradeError: this.upgradeError
|
|
662
850
|
};
|
|
851
|
+
if (this.balanceEnabled && this.balances.size > 0) snapshot.balances = [...this.balances.values()];
|
|
852
|
+
return snapshot;
|
|
663
853
|
}
|
|
664
854
|
/** Today's used tokens for one model key, or `0`. */
|
|
665
855
|
usedToday(key) {
|
|
@@ -896,6 +1086,11 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
896
1086
|
if (this.disposeRouteLog !== void 0) this.disposeRouteLog();
|
|
897
1087
|
if (this.disposeRouteCheck !== void 0) this.disposeRouteCheck();
|
|
898
1088
|
if (this.disposeRouteClear !== void 0) this.disposeRouteClear();
|
|
1089
|
+
if (this.disposeRouteBalance !== void 0) this.disposeRouteBalance();
|
|
1090
|
+
if (this.balanceTimer !== void 0) {
|
|
1091
|
+
clearTimeout(this.balanceTimer);
|
|
1092
|
+
this.balanceTimer = void 0;
|
|
1093
|
+
}
|
|
899
1094
|
if (this.disposeRouteEvents !== void 0) this.disposeRouteEvents();
|
|
900
1095
|
for (const res of this.musicClients) try {
|
|
901
1096
|
res.end();
|
|
@@ -24,6 +24,10 @@ export interface TokenQuotaPanelInjected {
|
|
|
24
24
|
setModelOrder: (order: string[]) => void;
|
|
25
25
|
/** Persist the floating scratchpad notes (content, title, placement). */
|
|
26
26
|
saveNotes: (notes: TokenQuotaNote[]) => void;
|
|
27
|
+
/** Persist the account-balance display settings. */
|
|
28
|
+
setBalanceSettings: (enabled: boolean, pollMinutes: number) => void;
|
|
29
|
+
/** Ask the Host to refresh account balances (one provider, or all). */
|
|
30
|
+
refreshBalance: (provider?: string) => void;
|
|
27
31
|
/** Toggle the live music output (requires a user gesture to start audio). */
|
|
28
32
|
setMusicEnabled: (enabled: boolean) => void;
|
|
29
33
|
/** Change the live-music master volume 0..1. */
|
|
@@ -44,5 +48,5 @@ export type TokenQuotaPanelComponentProps = PropsRuntime<'shell.overlay'> & Prop
|
|
|
44
48
|
* @param props - composed slot props.
|
|
45
49
|
* @returns the panel element tree.
|
|
46
50
|
*/
|
|
47
|
-
export declare function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle, setModelOrder, saveNotes, setMusicEnabled, setMusicVolume, setMusicStyle, setMusicOnlyCurrentSession, useStore, actions, useSessions, }: TokenQuotaPanelComponentProps): import("react").JSX.Element;
|
|
51
|
+
export declare function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle, setModelOrder, saveNotes, setBalanceSettings, refreshBalance, setMusicEnabled, setMusicVolume, setMusicStyle, setMusicOnlyCurrentSession, useStore, actions, useSessions, }: TokenQuotaPanelComponentProps): import("react").JSX.Element;
|
|
48
52
|
//# sourceMappingURL=TokenQuotaPanel.d.ts.map
|