dsh-plugin-subscription-oauth 0.7.0 → 0.8.0
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/auth/allowance-scheduler.d.ts +58 -0
- package/lib/auth/allowance-scheduler.js +72 -0
- package/lib/client/UsageChip.d.ts +58 -2
- package/lib/client/UsageChip.js +104 -22
- package/lib/client.js +83 -54
- package/lib/client.js.map +1 -1
- package/lib/providers/antigravity.d.ts +6 -0
- package/lib/providers/antigravity.js +65 -14
- package/lib/providers/ark-provider.js +8 -0
- package/lib/providers/ark.d.ts +3 -0
- package/lib/providers/ark.js +4 -1
- package/lib/providers/glm-provider.js +5 -0
- package/lib/providers/glm.d.ts +2 -0
- package/lib/providers/glm.js +4 -1
- package/lib/providers/grok.d.ts +3 -0
- package/lib/providers/grok.js +9 -1
- package/lib/providers/kimi-provider.js +5 -0
- package/lib/providers/kimi.d.ts +9 -0
- package/lib/providers/kimi.js +8 -1
- package/lib/providers/responses-stream.d.ts +44 -0
- package/lib/providers/responses-stream.js +125 -30
- package/package.json +1 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-request memory of when each provider account's allowance resets.
|
|
3
|
+
*
|
|
4
|
+
* This is what lets one model id draw on the whole account pool without a wasted
|
|
5
|
+
* request: when the default account's allowance runs out, the moment it will refill is
|
|
6
|
+
* remembered, and until then the scheduler sends work straight to another account
|
|
7
|
+
* instead of trying the spent one, failing, and only then moving on. When the reset
|
|
8
|
+
* time passes the spent account is first choice again.
|
|
9
|
+
*
|
|
10
|
+
* The holder is shared by the route's reader (which learns reset times from usage
|
|
11
|
+
* reads) and its streamer (which consults them when picking an account), so the
|
|
12
|
+
* scheduler's view is exactly the last read's.
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-plugin-subscription-oauth/auth/allowance-scheduler
|
|
15
|
+
*/
|
|
16
|
+
/** One account's remembered allowance state. */
|
|
17
|
+
export interface AllowanceState {
|
|
18
|
+
/** Epoch milliseconds at which the account's allowance resets, when known. */
|
|
19
|
+
resetsAt?: number;
|
|
20
|
+
/** Epoch milliseconds at which this state was last learned. */
|
|
21
|
+
learnedAt: number;
|
|
22
|
+
}
|
|
23
|
+
/** In-memory, per-provider memory of account reset times. */
|
|
24
|
+
export declare class AllowanceScheduler {
|
|
25
|
+
private readonly byProvider;
|
|
26
|
+
/** One provider's account states, creating the map on first use. */
|
|
27
|
+
private provider;
|
|
28
|
+
/**
|
|
29
|
+
* Record that one account's allowance resets at a given moment.
|
|
30
|
+
*
|
|
31
|
+
* @param provider - the provider route id.
|
|
32
|
+
* @param account - the account key.
|
|
33
|
+
* @param resetsAt - epoch milliseconds of the allowance reset, when known.
|
|
34
|
+
*/
|
|
35
|
+
noteReset(provider: string, account: string, resetsAt?: number): void;
|
|
36
|
+
/**
|
|
37
|
+
* Whether one account's allowance is known to be spent now.
|
|
38
|
+
*
|
|
39
|
+
* True when the account was recorded with a reset time that is still in the future:
|
|
40
|
+
* its allowance is spent and will not refill until then, so a request should go to a
|
|
41
|
+
* different account rather than fail against this one.
|
|
42
|
+
*
|
|
43
|
+
* @param provider - the provider route id.
|
|
44
|
+
* @param account - the account key.
|
|
45
|
+
* @returns whether the account's allowance is spent until its remembered reset.
|
|
46
|
+
*/
|
|
47
|
+
isSpentUntilReset(provider: string, account: string): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* The moment one account's allowance is known to reset, when it was recorded.
|
|
50
|
+
*
|
|
51
|
+
* @param provider - the provider route id.
|
|
52
|
+
* @param account - the account key.
|
|
53
|
+
* @returns the reset instant, or undefined when unknown or already passed.
|
|
54
|
+
*/
|
|
55
|
+
resetAt(provider: string, account: string): number | undefined;
|
|
56
|
+
}
|
|
57
|
+
/** The one shared scheduler every route reads and writes. */
|
|
58
|
+
export declare const allowanceScheduler: AllowanceScheduler;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-request memory of when each provider account's allowance resets.
|
|
3
|
+
*
|
|
4
|
+
* This is what lets one model id draw on the whole account pool without a wasted
|
|
5
|
+
* request: when the default account's allowance runs out, the moment it will refill is
|
|
6
|
+
* remembered, and until then the scheduler sends work straight to another account
|
|
7
|
+
* instead of trying the spent one, failing, and only then moving on. When the reset
|
|
8
|
+
* time passes the spent account is first choice again.
|
|
9
|
+
*
|
|
10
|
+
* The holder is shared by the route's reader (which learns reset times from usage
|
|
11
|
+
* reads) and its streamer (which consults them when picking an account), so the
|
|
12
|
+
* scheduler's view is exactly the last read's.
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-plugin-subscription-oauth/auth/allowance-scheduler
|
|
15
|
+
*/
|
|
16
|
+
/** In-memory, per-provider memory of account reset times. */
|
|
17
|
+
export class AllowanceScheduler {
|
|
18
|
+
byProvider = new Map();
|
|
19
|
+
/** One provider's account states, creating the map on first use. */
|
|
20
|
+
provider(provider) {
|
|
21
|
+
let map = this.byProvider.get(provider);
|
|
22
|
+
if (map === undefined) {
|
|
23
|
+
map = new Map();
|
|
24
|
+
this.byProvider.set(provider, map);
|
|
25
|
+
}
|
|
26
|
+
return map;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Record that one account's allowance resets at a given moment.
|
|
30
|
+
*
|
|
31
|
+
* @param provider - the provider route id.
|
|
32
|
+
* @param account - the account key.
|
|
33
|
+
* @param resetsAt - epoch milliseconds of the allowance reset, when known.
|
|
34
|
+
*/
|
|
35
|
+
noteReset(provider, account, resetsAt) {
|
|
36
|
+
this.provider(provider).set(account, { ...resetsAt === undefined ? {} : { resetsAt }, learnedAt: Date.now() });
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Whether one account's allowance is known to be spent now.
|
|
40
|
+
*
|
|
41
|
+
* True when the account was recorded with a reset time that is still in the future:
|
|
42
|
+
* its allowance is spent and will not refill until then, so a request should go to a
|
|
43
|
+
* different account rather than fail against this one.
|
|
44
|
+
*
|
|
45
|
+
* @param provider - the provider route id.
|
|
46
|
+
* @param account - the account key.
|
|
47
|
+
* @returns whether the account's allowance is spent until its remembered reset.
|
|
48
|
+
*/
|
|
49
|
+
isSpentUntilReset(provider, account) {
|
|
50
|
+
const state = this.byProvider.get(provider)?.get(account);
|
|
51
|
+
if (state === undefined)
|
|
52
|
+
return false;
|
|
53
|
+
if (state.resetsAt === undefined)
|
|
54
|
+
return false;
|
|
55
|
+
return Date.now() < state.resetsAt;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The moment one account's allowance is known to reset, when it was recorded.
|
|
59
|
+
*
|
|
60
|
+
* @param provider - the provider route id.
|
|
61
|
+
* @param account - the account key.
|
|
62
|
+
* @returns the reset instant, or undefined when unknown or already passed.
|
|
63
|
+
*/
|
|
64
|
+
resetAt(provider, account) {
|
|
65
|
+
const state = this.byProvider.get(provider)?.get(account);
|
|
66
|
+
if (state === undefined || state.resetsAt === undefined)
|
|
67
|
+
return undefined;
|
|
68
|
+
return Date.now() < state.resetsAt ? state.resetsAt : undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** The one shared scheduler every route reads and writes. */
|
|
72
|
+
export const allowanceScheduler = new AllowanceScheduler();
|
|
@@ -117,10 +117,12 @@ export declare function balanceOf(value: {
|
|
|
117
117
|
* Format one wallet for the chip's compact line.
|
|
118
118
|
*
|
|
119
119
|
* A balance is money, not a percentage, so it renders as `¥6.89` rather than a meter;
|
|
120
|
-
* the currency prefix keeps two currencies from being mistaken for one sum.
|
|
120
|
+
* the currency prefix keeps two currencies from being mistaken for one sum. The amount
|
|
121
|
+
* is rounded to two decimal places (a balance reads to the cent, and a figure like
|
|
122
|
+
* `1.1` for `1.10` would mislead) and always carries both places.
|
|
121
123
|
*
|
|
122
124
|
* @param wallet - the wallet to show.
|
|
123
|
-
* @returns the short form, e.g. `¥6.89` or
|
|
125
|
+
* @returns the short form, e.g. `¥6.89` or `+$2.00` for a bonus wallet.
|
|
124
126
|
*/
|
|
125
127
|
export declare function formatBalance(wallet: ChipBalance): string;
|
|
126
128
|
/**
|
|
@@ -157,6 +159,60 @@ export declare function pickWindow(windows: readonly UsageWindow[]): UsageWindow
|
|
|
157
159
|
* @param usage - the accounts the route answered for.
|
|
158
160
|
* @returns the reading, or undefined when the route stated no figure.
|
|
159
161
|
*/
|
|
162
|
+
/** What one account's allowance reduces to for the chip. */
|
|
163
|
+
export interface AccountReading {
|
|
164
|
+
/** Which account these figures belong to. */
|
|
165
|
+
account: string;
|
|
166
|
+
/** Plan name, when the provider stated one. */
|
|
167
|
+
plan?: string;
|
|
168
|
+
/** Every metered window, in the order the route stated them. */
|
|
169
|
+
windows: readonly UsageWindow[];
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Reduce a route's per-account usage to one reading per account.
|
|
173
|
+
*
|
|
174
|
+
* A provider can hold several accounts (two Antigravity sign-ins, say), and the chip
|
|
175
|
+
* shows each one's allowance on its own line, the way the settings cards do — a merged
|
|
176
|
+
* figure would hide that one account is exhausted while the other is free.
|
|
177
|
+
*
|
|
178
|
+
* @param usage - every account the route answered for.
|
|
179
|
+
* @returns one reading per account that states a figure, in the route's order.
|
|
180
|
+
*/
|
|
181
|
+
/**
|
|
182
|
+
* Reduce a route's per-account usage to one reading per account.
|
|
183
|
+
*
|
|
184
|
+
* A provider can hold several accounts (two Antigravity sign-ins, say). Every account's
|
|
185
|
+
* windows are kept as the route stated them — a percentage-metered quota and a credit
|
|
186
|
+
* balance (`credits`) alike — so the per-account panel shows the real figures, and the
|
|
187
|
+
* compact line's merge decides which to integrate.
|
|
188
|
+
*
|
|
189
|
+
* @param usage - every account the route answered for.
|
|
190
|
+
* @returns one reading per account that states any window, in the route's order.
|
|
191
|
+
*/
|
|
192
|
+
export declare function accountReadingsOf(usage: readonly ProviderUsage[]): readonly AccountReading[];
|
|
193
|
+
/**
|
|
194
|
+
* Merge every account's percentage-metered windows into one figure per label.
|
|
195
|
+
*
|
|
196
|
+
* The chip's compact line integrates the whole pool: two accounts at 50 percent read as
|
|
197
|
+
* one window at 50 percent (the same allowance spent twice as wide), not two lines. A
|
|
198
|
+
* credit balance (`credits`) is money, not a share of a shared allowance, so it is not
|
|
199
|
+
* merged — it stays on the per-account panel, where it reads correctly. Only windows
|
|
200
|
+
* that state a `usedPercent` (a share of an allowance) merge.
|
|
201
|
+
*
|
|
202
|
+
* @param readings - one per account, from {@link accountReadingsOf}.
|
|
203
|
+
* @returns one merged window per percentage-metered label, most-spent first.
|
|
204
|
+
*/
|
|
205
|
+
export declare function mergeAccountWindows(readings: readonly AccountReading[]): readonly UsageWindow[];
|
|
206
|
+
/**
|
|
207
|
+
* Reduce one route's usage to the single line the compact chip renders.
|
|
208
|
+
*
|
|
209
|
+
* Kept for the balance-free providers that still show one merged line.
|
|
210
|
+
*
|
|
211
|
+
* @param provider - the route id.
|
|
212
|
+
* @param label - the route's display name.
|
|
213
|
+
* @param usage - the accounts the route answered for.
|
|
214
|
+
* @returns the first account's reading, or undefined when none states a figure.
|
|
215
|
+
*/
|
|
160
216
|
export declare function readingOf(provider: string, label: string, usage: readonly ProviderUsage[]): ChipReading | undefined;
|
|
161
217
|
/**
|
|
162
218
|
* Format a window's reset the way the settings cards do.
|
package/lib/client/UsageChip.js
CHANGED
|
@@ -82,14 +82,16 @@ export function balanceOf(value) {
|
|
|
82
82
|
* Format one wallet for the chip's compact line.
|
|
83
83
|
*
|
|
84
84
|
* A balance is money, not a percentage, so it renders as `¥6.89` rather than a meter;
|
|
85
|
-
* the currency prefix keeps two currencies from being mistaken for one sum.
|
|
85
|
+
* the currency prefix keeps two currencies from being mistaken for one sum. The amount
|
|
86
|
+
* is rounded to two decimal places (a balance reads to the cent, and a figure like
|
|
87
|
+
* `1.1` for `1.10` would mislead) and always carries both places.
|
|
86
88
|
*
|
|
87
89
|
* @param wallet - the wallet to show.
|
|
88
|
-
* @returns the short form, e.g. `¥6.89` or
|
|
90
|
+
* @returns the short form, e.g. `¥6.89` or `+$2.00` for a bonus wallet.
|
|
89
91
|
*/
|
|
90
92
|
export function formatBalance(wallet) {
|
|
91
93
|
const amount = Number.parseFloat(wallet.balance);
|
|
92
|
-
const trimmed = Number.isFinite(amount) ?
|
|
94
|
+
const trimmed = Number.isFinite(amount) ? amount.toFixed(2) : wallet.balance;
|
|
93
95
|
const symbol = wallet.currency === 'CNY' ? '¥' : wallet.currency === 'USD' ? '$' : `${wallet.currency} `;
|
|
94
96
|
return `${wallet.bonus ? '+' : ''}${symbol}${trimmed}`;
|
|
95
97
|
}
|
|
@@ -142,12 +144,90 @@ export function pickWindow(windows) {
|
|
|
142
144
|
return best;
|
|
143
145
|
}
|
|
144
146
|
/**
|
|
145
|
-
* Reduce
|
|
147
|
+
* Reduce a route's per-account usage to one reading per account.
|
|
146
148
|
*
|
|
147
|
-
*
|
|
149
|
+
* A provider can hold several accounts (two Antigravity sign-ins, say), and the chip
|
|
150
|
+
* shows each one's allowance on its own line, the way the settings cards do — a merged
|
|
151
|
+
* figure would hide that one account is exhausted while the other is free.
|
|
152
|
+
*
|
|
153
|
+
* @param usage - every account the route answered for.
|
|
154
|
+
* @returns one reading per account that states a figure, in the route's order.
|
|
155
|
+
*/
|
|
156
|
+
/**
|
|
157
|
+
* Reduce a route's per-account usage to one reading per account.
|
|
158
|
+
*
|
|
159
|
+
* A provider can hold several accounts (two Antigravity sign-ins, say). Every account's
|
|
160
|
+
* windows are kept as the route stated them — a percentage-metered quota and a credit
|
|
161
|
+
* balance (`credits`) alike — so the per-account panel shows the real figures, and the
|
|
162
|
+
* compact line's merge decides which to integrate.
|
|
163
|
+
*
|
|
164
|
+
* @param usage - every account the route answered for.
|
|
165
|
+
* @returns one reading per account that states any window, in the route's order.
|
|
166
|
+
*/
|
|
167
|
+
export function accountReadingsOf(usage) {
|
|
168
|
+
const out = [];
|
|
169
|
+
for (const entry of usage) {
|
|
170
|
+
if (entry.windows.length === 0)
|
|
171
|
+
continue;
|
|
172
|
+
out.push({ account: entry.account, windows: entry.windows, ...entry.plan === undefined ? {} : { plan: entry.plan } });
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Merge every account's percentage-metered windows into one figure per label.
|
|
178
|
+
*
|
|
179
|
+
* The chip's compact line integrates the whole pool: two accounts at 50 percent read as
|
|
180
|
+
* one window at 50 percent (the same allowance spent twice as wide), not two lines. A
|
|
181
|
+
* credit balance (`credits`) is money, not a share of a shared allowance, so it is not
|
|
182
|
+
* merged — it stays on the per-account panel, where it reads correctly. Only windows
|
|
183
|
+
* that state a `usedPercent` (a share of an allowance) merge.
|
|
184
|
+
*
|
|
185
|
+
* @param readings - one per account, from {@link accountReadingsOf}.
|
|
186
|
+
* @returns one merged window per percentage-metered label, most-spent first.
|
|
187
|
+
*/
|
|
188
|
+
export function mergeAccountWindows(readings) {
|
|
189
|
+
const byLabel = new Map();
|
|
190
|
+
for (const reading of readings) {
|
|
191
|
+
for (const window of reading.windows) {
|
|
192
|
+
// Only a percentage-metered window merges. A credit balance (a bare used/limit
|
|
193
|
+
// pair) is money, not a share of a shared allowance, and averaging it across
|
|
194
|
+
// accounts would misread money as a quota.
|
|
195
|
+
if (window.usedPercent === undefined)
|
|
196
|
+
continue;
|
|
197
|
+
const list = byLabel.get(window.label) ?? [];
|
|
198
|
+
list.push(window);
|
|
199
|
+
byLabel.set(window.label, list);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const merged = [];
|
|
203
|
+
for (const [label, list] of byLabel) {
|
|
204
|
+
// Percentages average across accounts: the pool's spent share is the mean of each
|
|
205
|
+
// account's spent share (accounts share an allowance, they do not add to it).
|
|
206
|
+
const withPercent = list.filter((window) => window.usedPercent !== undefined);
|
|
207
|
+
if (withPercent.length === 0)
|
|
208
|
+
continue;
|
|
209
|
+
const usedPercent = withPercent.reduce((sum, window) => sum + (window.usedPercent ?? 0), 0) / withPercent.length;
|
|
210
|
+
// The pool's reset is the soonest account reset: the window refills when the first
|
|
211
|
+
// account's does.
|
|
212
|
+
const resets = list.map((window) => window.resetsAt).filter((at) => typeof at === 'number');
|
|
213
|
+
merged.push({
|
|
214
|
+
label,
|
|
215
|
+
usedPercent: Math.max(0, Math.min(100, usedPercent)),
|
|
216
|
+
...resets.length === 0 ? {} : { resetsAt: Math.min(...resets) },
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
// Most-spent first, so the line leads with the window that will stop work soonest.
|
|
220
|
+
return merged.sort((left, right) => (right.usedPercent ?? 0) - (left.usedPercent ?? 0));
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Reduce one route's usage to the single line the compact chip renders.
|
|
224
|
+
*
|
|
225
|
+
* Kept for the balance-free providers that still show one merged line.
|
|
226
|
+
*
|
|
227
|
+
* @param provider - the route id.
|
|
148
228
|
* @param label - the route's display name.
|
|
149
229
|
* @param usage - the accounts the route answered for.
|
|
150
|
-
* @returns the reading, or undefined when
|
|
230
|
+
* @returns the first account's reading, or undefined when none states a figure.
|
|
151
231
|
*/
|
|
152
232
|
export function readingOf(provider, label, usage) {
|
|
153
233
|
for (const entry of usage) {
|
|
@@ -197,6 +277,7 @@ export function formatReset(at, now) {
|
|
|
197
277
|
*/
|
|
198
278
|
export function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBalance, useProjection }) {
|
|
199
279
|
const [reading, setReading] = useState(undefined);
|
|
280
|
+
const [accountReadings, setAccountReadings] = useState(undefined);
|
|
200
281
|
const [balance, setBalance] = useState(undefined);
|
|
201
282
|
const [ccBalance, setCcBalance] = useState(undefined);
|
|
202
283
|
const [now, setNow] = useState(() => Date.now());
|
|
@@ -239,8 +320,7 @@ export function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBala
|
|
|
239
320
|
const load = useCallback(async (target) => {
|
|
240
321
|
try {
|
|
241
322
|
const result = await call('usage', { provider: target });
|
|
242
|
-
|
|
243
|
-
return readingOf(target, label, result.usage);
|
|
323
|
+
return accountReadingsOf(result.usage);
|
|
244
324
|
}
|
|
245
325
|
catch {
|
|
246
326
|
// A failed read is not worth a chip of its own: the settings page is where a user
|
|
@@ -276,7 +356,7 @@ export function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBala
|
|
|
276
356
|
// Not one of ours (or none selected): keep showing the last own reading.
|
|
277
357
|
: lastOwn.current;
|
|
278
358
|
if (target === undefined) {
|
|
279
|
-
|
|
359
|
+
setAccountReadings(undefined);
|
|
280
360
|
return;
|
|
281
361
|
}
|
|
282
362
|
let live = true;
|
|
@@ -286,7 +366,7 @@ export function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBala
|
|
|
286
366
|
if (!live || next === undefined)
|
|
287
367
|
return;
|
|
288
368
|
lastOwn.current = target;
|
|
289
|
-
|
|
369
|
+
setAccountReadings(next);
|
|
290
370
|
});
|
|
291
371
|
};
|
|
292
372
|
refresh();
|
|
@@ -327,20 +407,22 @@ export function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBala
|
|
|
327
407
|
setOpen((was) => !was);
|
|
328
408
|
}, children: [_jsx("span", { className: "dsh-subscription-usage-chip-label", children: short }), balance.map((wallet) => (_jsx("span", { className: "dsh-subscription-usage-chip-cell", children: _jsx("span", { className: "dsh-subscription-usage-chip-value", children: formatBalance(wallet) }) }, `${wallet.currency}${wallet.bonus ? ':bonus' : ''}`)))] }), open && (_jsxs("span", { className: "dsh-subscription-usage-chip-panel", role: "status", children: [_jsx("span", { className: "dsh-subscription-usage-chip-full", children: t('usageChipFull', { provider: 'DeepSeek Account' }) }), balance.map((wallet) => (_jsx("span", { className: "dsh-subscription-usage-chip-meter", children: _jsxs("span", { className: "dsh-subscription-usage-chip-meterHead", children: [_jsx("span", { children: `${wallet.bonus ? 'Bonus · ' : ''}${wallet.currency}` }), _jsx("span", { className: "dsh-subscription-usage-chip-detail", children: formatBalance(wallet) })] }) }, `${wallet.currency}${wallet.bonus ? ':bonus' : ''}`)))] }))] }));
|
|
329
409
|
}
|
|
330
|
-
if (
|
|
331
|
-
return null;
|
|
332
|
-
const percent = reading.window.usedPercent;
|
|
333
|
-
if (percent === undefined)
|
|
410
|
+
if (accountReadings === undefined || accountReadings.length === 0)
|
|
334
411
|
return null;
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
412
|
+
const short = shortLabel(selected ?? '', selected ?? '');
|
|
413
|
+
// The compact line integrates the whole pool; the panel shows each account's own
|
|
414
|
+
// figures, so a merged glance hides nothing a per-account look cannot recover.
|
|
415
|
+
const merged = mergeAccountWindows(accountReadings);
|
|
416
|
+
return (_jsxs("span", { className: "dsh-subscription-usage-chip", children: [_jsx("style", { children: usageChipCss }), _jsxs("button", { type: "button", className: "dsh-subscription-usage-chip-trigger", "aria-expanded": open, title: t('usageChipTitle', { provider: selected ?? '' }), onClick: (event) => {
|
|
339
417
|
// The document listener would otherwise close what this click just opened.
|
|
340
418
|
event.stopPropagation();
|
|
341
419
|
setOpen((was) => !was);
|
|
342
|
-
}, children: [_jsx("span", { className: "dsh-subscription-usage-chip-label", children: short }),
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
420
|
+
}, children: [_jsx("span", { className: "dsh-subscription-usage-chip-label", children: short }), merged.map((window) => (_jsxs("span", { className: "dsh-subscription-usage-chip-cell", children: [_jsx("span", { className: "dsh-subscription-usage-chip-window", children: shortWindow(window.label) }), _jsx("span", { className: "dsh-subscription-usage-chip-value", children: `${String(Math.round(window.usedPercent ?? 0))}%` })] }, window.label)))] }), open && (_jsxs("span", { className: "dsh-subscription-usage-chip-panel", role: "status", children: [_jsx("span", { className: "dsh-subscription-usage-chip-full", children: t('usageChipFull', { provider: selected ?? '' }) }), accountReadings.map((account) => (_jsxs("span", { className: "dsh-subscription-usage-chip-account", children: [_jsx("span", { className: "dsh-subscription-usage-chip-meterHead", children: _jsx("span", { children: account.plan ?? account.account }) }), account.windows.map((window) => {
|
|
421
|
+
const windowReset = formatReset(window.resetsAt, now);
|
|
422
|
+
// A credit balance reads as money (`used`/`limit`), a quota as a percent.
|
|
423
|
+
const amount = window.usedPercent === undefined && window.used !== undefined && window.limit !== undefined
|
|
424
|
+
? `${String(window.used)}/${String(window.limit)}`
|
|
425
|
+
: `${String(Math.round(window.usedPercent ?? 0))}%`;
|
|
426
|
+
return (_jsxs("span", { className: "dsh-subscription-usage-chip-meter", children: [_jsxs("span", { className: "dsh-subscription-usage-chip-meterHead", children: [_jsx("span", { children: window.label }), _jsx("span", { className: "dsh-subscription-usage-chip-detail", children: `${amount}${windowReset === undefined ? '' : ` · ${windowReset}`}` })] }), window.usedPercent !== undefined && (_jsx("span", { className: "dsh-subscription-usage-chip-track", "aria-hidden": "true", children: _jsx("span", { className: "dsh-subscription-usage-chip-fill", style: { width: `${String(Math.max(0, Math.min(100, window.usedPercent ?? 0)))}%` } }) }))] }, window.label));
|
|
427
|
+
})] }, account.account)))] }))] }));
|
|
346
428
|
}
|
package/lib/client.js
CHANGED
|
@@ -2300,14 +2300,16 @@ function balanceOf(value) {
|
|
|
2300
2300
|
* Format one wallet for the chip's compact line.
|
|
2301
2301
|
*
|
|
2302
2302
|
* A balance is money, not a percentage, so it renders as `¥6.89` rather than a meter;
|
|
2303
|
-
* the currency prefix keeps two currencies from being mistaken for one sum.
|
|
2303
|
+
* the currency prefix keeps two currencies from being mistaken for one sum. The amount
|
|
2304
|
+
* is rounded to two decimal places (a balance reads to the cent, and a figure like
|
|
2305
|
+
* `1.1` for `1.10` would mislead) and always carries both places.
|
|
2304
2306
|
*
|
|
2305
2307
|
* @param wallet - the wallet to show.
|
|
2306
|
-
* @returns the short form, e.g. `¥6.89` or
|
|
2308
|
+
* @returns the short form, e.g. `¥6.89` or `+$2.00` for a bonus wallet.
|
|
2307
2309
|
*/
|
|
2308
2310
|
function formatBalance(wallet) {
|
|
2309
2311
|
const amount = Number.parseFloat(wallet.balance);
|
|
2310
|
-
const trimmed = Number.isFinite(amount) ?
|
|
2312
|
+
const trimmed = Number.isFinite(amount) ? amount.toFixed(2) : wallet.balance;
|
|
2311
2313
|
const symbol = wallet.currency === "CNY" ? "¥" : wallet.currency === "USD" ? "$" : `${wallet.currency} `;
|
|
2312
2314
|
return `${wallet.bonus ? "+" : ""}${symbol}${trimmed}`;
|
|
2313
2315
|
}
|
|
@@ -2336,43 +2338,71 @@ function shortWindow(label) {
|
|
|
2336
2338
|
return label.length <= 4 ? label : label.slice(0, 4);
|
|
2337
2339
|
}
|
|
2338
2340
|
/**
|
|
2339
|
-
*
|
|
2341
|
+
* Reduce a route's per-account usage to one reading per account.
|
|
2340
2342
|
*
|
|
2341
|
-
* A
|
|
2342
|
-
*
|
|
2343
|
-
*
|
|
2343
|
+
* A provider can hold several accounts (two Antigravity sign-ins, say), and the chip
|
|
2344
|
+
* shows each one's allowance on its own line, the way the settings cards do — a merged
|
|
2345
|
+
* figure would hide that one account is exhausted while the other is free.
|
|
2344
2346
|
*
|
|
2345
|
-
* @param
|
|
2346
|
-
* @returns
|
|
2347
|
+
* @param usage - every account the route answered for.
|
|
2348
|
+
* @returns one reading per account that states a figure, in the route's order.
|
|
2347
2349
|
*/
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2350
|
+
/**
|
|
2351
|
+
* Reduce a route's per-account usage to one reading per account.
|
|
2352
|
+
*
|
|
2353
|
+
* A provider can hold several accounts (two Antigravity sign-ins, say). Every account's
|
|
2354
|
+
* windows are kept as the route stated them — a percentage-metered quota and a credit
|
|
2355
|
+
* balance (`credits`) alike — so the per-account panel shows the real figures, and the
|
|
2356
|
+
* compact line's merge decides which to integrate.
|
|
2357
|
+
*
|
|
2358
|
+
* @param usage - every account the route answered for.
|
|
2359
|
+
* @returns one reading per account that states any window, in the route's order.
|
|
2360
|
+
*/
|
|
2361
|
+
function accountReadingsOf(usage) {
|
|
2362
|
+
const out = [];
|
|
2363
|
+
for (const entry of usage) {
|
|
2364
|
+
if (entry.windows.length === 0) continue;
|
|
2365
|
+
out.push({
|
|
2366
|
+
account: entry.account,
|
|
2367
|
+
windows: entry.windows,
|
|
2368
|
+
...entry.plan === void 0 ? {} : { plan: entry.plan }
|
|
2369
|
+
});
|
|
2353
2370
|
}
|
|
2354
|
-
return
|
|
2371
|
+
return out;
|
|
2355
2372
|
}
|
|
2356
2373
|
/**
|
|
2357
|
-
*
|
|
2374
|
+
* Merge every account's percentage-metered windows into one figure per label.
|
|
2375
|
+
*
|
|
2376
|
+
* The chip's compact line integrates the whole pool: two accounts at 50 percent read as
|
|
2377
|
+
* one window at 50 percent (the same allowance spent twice as wide), not two lines. A
|
|
2378
|
+
* credit balance (`credits`) is money, not a share of a shared allowance, so it is not
|
|
2379
|
+
* merged — it stays on the per-account panel, where it reads correctly. Only windows
|
|
2380
|
+
* that state a `usedPercent` (a share of an allowance) merge.
|
|
2358
2381
|
*
|
|
2359
|
-
* @param
|
|
2360
|
-
* @
|
|
2361
|
-
* @param usage - the accounts the route answered for.
|
|
2362
|
-
* @returns the reading, or undefined when the route stated no figure.
|
|
2382
|
+
* @param readings - one per account, from {@link accountReadingsOf}.
|
|
2383
|
+
* @returns one merged window per percentage-metered label, most-spent first.
|
|
2363
2384
|
*/
|
|
2364
|
-
function
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
if (window$1 === void 0) continue;
|
|
2368
|
-
|
|
2369
|
-
|
|
2385
|
+
function mergeAccountWindows(readings) {
|
|
2386
|
+
const byLabel = /* @__PURE__ */ new Map();
|
|
2387
|
+
for (const reading of readings) for (const window$1 of reading.windows) {
|
|
2388
|
+
if (window$1.usedPercent === void 0) continue;
|
|
2389
|
+
const list = byLabel.get(window$1.label) ?? [];
|
|
2390
|
+
list.push(window$1);
|
|
2391
|
+
byLabel.set(window$1.label, list);
|
|
2392
|
+
}
|
|
2393
|
+
const merged = [];
|
|
2394
|
+
for (const [label, list] of byLabel) {
|
|
2395
|
+
const withPercent = list.filter((window$1) => window$1.usedPercent !== void 0);
|
|
2396
|
+
if (withPercent.length === 0) continue;
|
|
2397
|
+
const usedPercent = withPercent.reduce((sum, window$1) => sum + (window$1.usedPercent ?? 0), 0) / withPercent.length;
|
|
2398
|
+
const resets = list.map((window$1) => window$1.resetsAt).filter((at) => typeof at === "number");
|
|
2399
|
+
merged.push({
|
|
2370
2400
|
label,
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
};
|
|
2401
|
+
usedPercent: Math.max(0, Math.min(100, usedPercent)),
|
|
2402
|
+
...resets.length === 0 ? {} : { resetsAt: Math.min(...resets) }
|
|
2403
|
+
});
|
|
2375
2404
|
}
|
|
2405
|
+
return merged.sort((left, right) => (right.usedPercent ?? 0) - (left.usedPercent ?? 0));
|
|
2376
2406
|
}
|
|
2377
2407
|
/**
|
|
2378
2408
|
* Format a window's reset the way the settings cards do.
|
|
@@ -2402,6 +2432,7 @@ function formatReset(at, now) {
|
|
|
2402
2432
|
*/
|
|
2403
2433
|
function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBalance, useProjection }) {
|
|
2404
2434
|
const [reading, setReading] = (0, react.useState)(void 0);
|
|
2435
|
+
const [accountReadings, setAccountReadings] = (0, react.useState)(void 0);
|
|
2405
2436
|
const [balance, setBalance] = (0, react.useState)(void 0);
|
|
2406
2437
|
const [ccBalance, setCcBalance] = (0, react.useState)(void 0);
|
|
2407
2438
|
const [now, setNow] = (0, react.useState)(() => Date.now());
|
|
@@ -2438,8 +2469,7 @@ function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBalance, us
|
|
|
2438
2469
|
]);
|
|
2439
2470
|
const load = (0, react.useCallback)(async (target) => {
|
|
2440
2471
|
try {
|
|
2441
|
-
|
|
2442
|
-
return readingOf(target, result.status.providers.find((entry) => entry.provider === target)?.displayName ?? target, result.usage);
|
|
2472
|
+
return accountReadingsOf((await call("usage", { provider: target })).usage);
|
|
2443
2473
|
} catch {
|
|
2444
2474
|
return;
|
|
2445
2475
|
}
|
|
@@ -2465,7 +2495,7 @@ function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBalance, us
|
|
|
2465
2495
|
(0, react.useEffect)(() => {
|
|
2466
2496
|
const target = selected !== void 0 && isOwnProvider(selected) ? selected : lastOwn.current;
|
|
2467
2497
|
if (target === void 0) {
|
|
2468
|
-
|
|
2498
|
+
setAccountReadings(void 0);
|
|
2469
2499
|
return;
|
|
2470
2500
|
}
|
|
2471
2501
|
let live = true;
|
|
@@ -2474,7 +2504,7 @@ function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBalance, us
|
|
|
2474
2504
|
load(target).then((next) => {
|
|
2475
2505
|
if (!live || next === void 0) return;
|
|
2476
2506
|
lastOwn.current = target;
|
|
2477
|
-
|
|
2507
|
+
setAccountReadings(next);
|
|
2478
2508
|
});
|
|
2479
2509
|
};
|
|
2480
2510
|
refresh();
|
|
@@ -2592,10 +2622,9 @@ function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBalance, us
|
|
|
2592
2622
|
]
|
|
2593
2623
|
});
|
|
2594
2624
|
}
|
|
2595
|
-
if (
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
const short = shortLabel(reading.provider, reading.label);
|
|
2625
|
+
if (accountReadings === void 0 || accountReadings.length === 0) return null;
|
|
2626
|
+
const short = shortLabel(selected ?? "", selected ?? "");
|
|
2627
|
+
const merged = mergeAccountWindows(accountReadings);
|
|
2599
2628
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2600
2629
|
className: "dsh-subscription-usage-chip",
|
|
2601
2630
|
children: [
|
|
@@ -2604,7 +2633,7 @@ function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBalance, us
|
|
|
2604
2633
|
type: "button",
|
|
2605
2634
|
className: "dsh-subscription-usage-chip-trigger",
|
|
2606
2635
|
"aria-expanded": open,
|
|
2607
|
-
title: t("usageChipTitle", { provider:
|
|
2636
|
+
title: t("usageChipTitle", { provider: selected ?? "" }),
|
|
2608
2637
|
onClick: (event) => {
|
|
2609
2638
|
event.stopPropagation();
|
|
2610
2639
|
setOpen((was) => !was);
|
|
@@ -2612,7 +2641,7 @@ function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBalance, us
|
|
|
2612
2641
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2613
2642
|
className: "dsh-subscription-usage-chip-label",
|
|
2614
2643
|
children: short
|
|
2615
|
-
}),
|
|
2644
|
+
}), merged.map((window$1) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2616
2645
|
className: "dsh-subscription-usage-chip-cell",
|
|
2617
2646
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2618
2647
|
className: "dsh-subscription-usage-chip-window",
|
|
@@ -2626,22 +2655,26 @@ function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBalance, us
|
|
|
2626
2655
|
open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2627
2656
|
className: "dsh-subscription-usage-chip-panel",
|
|
2628
2657
|
role: "status",
|
|
2629
|
-
children: [
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2658
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2659
|
+
className: "dsh-subscription-usage-chip-full",
|
|
2660
|
+
children: t("usageChipFull", { provider: selected ?? "" })
|
|
2661
|
+
}), accountReadings.map((account) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2662
|
+
className: "dsh-subscription-usage-chip-account",
|
|
2663
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2664
|
+
className: "dsh-subscription-usage-chip-meterHead",
|
|
2665
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: account.plan ?? account.account })
|
|
2666
|
+
}), account.windows.map((window$1) => {
|
|
2635
2667
|
const windowReset = formatReset(window$1.resetsAt, now);
|
|
2668
|
+
const amount = window$1.usedPercent === void 0 && window$1.used !== void 0 && window$1.limit !== void 0 ? `${String(window$1.used)}/${String(window$1.limit)}` : `${String(Math.round(window$1.usedPercent ?? 0))}%`;
|
|
2636
2669
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2637
2670
|
className: "dsh-subscription-usage-chip-meter",
|
|
2638
2671
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2639
2672
|
className: "dsh-subscription-usage-chip-meterHead",
|
|
2640
2673
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: window$1.label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2641
2674
|
className: "dsh-subscription-usage-chip-detail",
|
|
2642
|
-
children: `${
|
|
2675
|
+
children: `${amount}${windowReset === void 0 ? "" : ` · ${windowReset}`}`
|
|
2643
2676
|
})]
|
|
2644
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2677
|
+
}), window$1.usedPercent !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2645
2678
|
className: "dsh-subscription-usage-chip-track",
|
|
2646
2679
|
"aria-hidden": "true",
|
|
2647
2680
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -2650,12 +2683,8 @@ function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBalance, us
|
|
|
2650
2683
|
})
|
|
2651
2684
|
})]
|
|
2652
2685
|
}, window$1.label);
|
|
2653
|
-
})
|
|
2654
|
-
|
|
2655
|
-
className: "dsh-subscription-usage-chip-detail",
|
|
2656
|
-
children: reading.plan
|
|
2657
|
-
})
|
|
2658
|
-
]
|
|
2686
|
+
})]
|
|
2687
|
+
}, account.account))]
|
|
2659
2688
|
})
|
|
2660
2689
|
]
|
|
2661
2690
|
});
|