dsh-plugin-subscription-oauth 0.11.0 → 0.12.2
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/mimo-flow.d.ts +9 -0
- package/lib/auth/mimo-flow.js +171 -0
- package/lib/auth/registry.d.ts +10 -1
- package/lib/auth/registry.js +11 -1
- package/lib/auth/rpc.js +8 -0
- package/lib/client/SubscriptionsSection.js +73 -18
- package/lib/client/UsageChip.js +9 -1
- package/lib/client/locales.d.ts +2 -0
- package/lib/client/locales.js +2 -0
- package/lib/client.js +175 -136
- package/lib/client.js.map +1 -1
- package/lib/index.js +1 -1
- package/lib/protocol.d.ts +17 -1
- package/lib/protocol.js +2 -0
- package/lib/providers/auto.js +3 -0
- package/lib/providers/grok.d.ts +2 -0
- package/lib/providers/grok.js +31 -21
- package/lib/providers/kimi-provider.js +2 -1
- package/lib/providers/kimi.js +18 -13
- package/lib/providers/mimo.d.ts +10 -0
- package/lib/providers/mimo.js +70 -5
- package/lib/providers/workbuddy.js +5 -4
- package/lib/settings.d.ts +8 -0
- package/lib/settings.js +19 -5
- package/package.json +1 -1
package/lib/auth/mimo-flow.d.ts
CHANGED
|
@@ -63,6 +63,8 @@ export interface MimoSessionCookies {
|
|
|
63
63
|
/** The Xiaomi sign-in has lapsed and the account must sign in again. */
|
|
64
64
|
export declare class MimoSignInLapsed extends Error {
|
|
65
65
|
}
|
|
66
|
+
/** The open platform's origin (the Token Plan lives here, not at a MiMo server). */
|
|
67
|
+
export declare const MIMO_PLATFORM = "https://platform.xiaomimimo.com";
|
|
66
68
|
/**
|
|
67
69
|
* Sign the account on at a MiMo server, following the serviceLogin redirects.
|
|
68
70
|
*
|
|
@@ -118,6 +120,8 @@ export declare function refreshMimoSession(creds: MimoCredentials, fetchFn: type
|
|
|
118
120
|
export interface MimoUsageWindow {
|
|
119
121
|
label: string;
|
|
120
122
|
usedPercent?: number;
|
|
123
|
+
used?: number;
|
|
124
|
+
limit?: number;
|
|
121
125
|
resetsAt?: number;
|
|
122
126
|
}
|
|
123
127
|
/** The account's plan and weekly allowance, for the Settings card and chip. */
|
|
@@ -126,4 +130,9 @@ export declare function readMimoUsage(session: MimoSessionCookies, creds: MimoCr
|
|
|
126
130
|
renewsAt?: number;
|
|
127
131
|
windows: MimoUsageWindow[];
|
|
128
132
|
}>;
|
|
133
|
+
/** The account's Token Plan as a usable API credential, or null when it has none. */
|
|
134
|
+
export declare function readMimoPlanKey(creds: MimoCredentials, fetchFn: typeof fetch): Promise<{
|
|
135
|
+
key: string;
|
|
136
|
+
base: string;
|
|
137
|
+
} | null>;
|
|
129
138
|
export {};
|
package/lib/auth/mimo-flow.js
CHANGED
|
@@ -144,6 +144,10 @@ export class Jar {
|
|
|
144
144
|
/** The Xiaomi sign-in has lapsed and the account must sign in again. */
|
|
145
145
|
export class MimoSignInLapsed extends Error {
|
|
146
146
|
}
|
|
147
|
+
/** The open platform's origin (the Token Plan lives here, not at a MiMo server). */
|
|
148
|
+
export const MIMO_PLATFORM = 'https://platform.xiaomimimo.com';
|
|
149
|
+
/** A plan the app refused covers for this long before its endpoint is asked again. */
|
|
150
|
+
const TP_LIFE_MS = 60 * 60 * 1000;
|
|
147
151
|
const appHeaders = () => ({
|
|
148
152
|
'User-Agent': USER_AGENT,
|
|
149
153
|
'X-Client-Version': APP_VERSION,
|
|
@@ -457,5 +461,172 @@ export async function readMimoUsage(session, creds, fetchFn) {
|
|
|
457
461
|
catch {
|
|
458
462
|
// The plan alone, when the week's allowance can't be read.
|
|
459
463
|
}
|
|
464
|
+
// The Token Plan at the open platform, an aside the app's requests don't spend.
|
|
465
|
+
const tokenPlan = await readMimoTokenPlan(creds, fetchFn);
|
|
466
|
+
if (tokenPlan !== null) {
|
|
467
|
+
if (current === undefined) {
|
|
468
|
+
plan = tokenPlan.plan;
|
|
469
|
+
if (tokenPlan.until !== undefined)
|
|
470
|
+
renewsAt = tokenPlan.until;
|
|
471
|
+
}
|
|
472
|
+
if (tokenPlan.window !== undefined)
|
|
473
|
+
windows.push(tokenPlan.window);
|
|
474
|
+
}
|
|
460
475
|
return { plan, ...renewsAt === undefined ? {} : { renewsAt }, windows };
|
|
461
476
|
}
|
|
477
|
+
/** A short count of the Token Plan's credits, "6.81M" / "4.1B" / "12K". */
|
|
478
|
+
function mimoCredits(value) {
|
|
479
|
+
for (const [divisor, unit] of [[1e9, 'B'], [1e6, 'M'], [1e3, 'K']]) {
|
|
480
|
+
if (value >= divisor)
|
|
481
|
+
return `${Number((value / divisor).toPrecision(3))}${unit}`;
|
|
482
|
+
}
|
|
483
|
+
return String(value);
|
|
484
|
+
}
|
|
485
|
+
/** Read one of the platform's /api/v1 pages, signing on afresh when the session is gone. */
|
|
486
|
+
async function mimoPlatformPage(creds, path, cookies, fetchFn) {
|
|
487
|
+
const url = `${MIMO_PLATFORM}/api/v1${path}`;
|
|
488
|
+
const ask = async (cookie) => {
|
|
489
|
+
const response = await fetchFn(url, {
|
|
490
|
+
headers: cookie === '' ? {} : { Cookie: cookie },
|
|
491
|
+
redirect: 'manual',
|
|
492
|
+
signal: AbortSignal.timeout(20_000),
|
|
493
|
+
});
|
|
494
|
+
return { status: response.status, text: await response.text() };
|
|
495
|
+
};
|
|
496
|
+
const parse = (text) => {
|
|
497
|
+
try {
|
|
498
|
+
const value = JSON.parse(text);
|
|
499
|
+
return value?.code === 0 ? (isRecord(value.data) ? value.data : null) : undefined;
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
return undefined;
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
if (cookies !== undefined && Object.keys(cookies).length > 0) {
|
|
506
|
+
const { status, text } = await ask(cookieHeaderOf(cookies));
|
|
507
|
+
const data = status === 200 ? parse(text) : undefined;
|
|
508
|
+
if (data !== undefined)
|
|
509
|
+
return { cookies, data };
|
|
510
|
+
}
|
|
511
|
+
let { status, text } = await ask('');
|
|
512
|
+
let login;
|
|
513
|
+
try {
|
|
514
|
+
login = JSON.parse(text).loginUrl;
|
|
515
|
+
}
|
|
516
|
+
catch {
|
|
517
|
+
login = undefined;
|
|
518
|
+
}
|
|
519
|
+
if (status !== 401 || typeof login !== 'string' || !login.startsWith('https://account.xiaomi.com/')) {
|
|
520
|
+
throw new Error(`Xiaomi MiMo platform ${path}: HTTP ${status}`);
|
|
521
|
+
}
|
|
522
|
+
// Follow the serviceLogin redirect with the passToken, collecting the platform's cookies.
|
|
523
|
+
const jar = new Jar();
|
|
524
|
+
for (const [key, value] of [
|
|
525
|
+
['userId', creds.userId], ['passToken', creds.passToken], ['cUserId', creds.cUserId],
|
|
526
|
+
['deviceId', creds.deviceId], ['pass_ua', 'pc'], ['uLocale', 'zh_CN'],
|
|
527
|
+
]) {
|
|
528
|
+
if (value)
|
|
529
|
+
jar.seed('https://account.xiaomi.com', key, value);
|
|
530
|
+
}
|
|
531
|
+
let at = login;
|
|
532
|
+
for (let hop = 0;; hop++) {
|
|
533
|
+
if (hop > 10)
|
|
534
|
+
throw new Error('Xiaomi MiMo platform sign-in: too many redirects');
|
|
535
|
+
const cookie = jar.header(at);
|
|
536
|
+
const response = await fetchFn(at, {
|
|
537
|
+
headers: cookie === '' ? {} : { Cookie: cookie },
|
|
538
|
+
redirect: 'manual',
|
|
539
|
+
signal: AbortSignal.timeout(20_000),
|
|
540
|
+
});
|
|
541
|
+
jar.take(response, at);
|
|
542
|
+
const location = response.headers.get('location');
|
|
543
|
+
if (response.status < 300 || response.status >= 400 || !location) {
|
|
544
|
+
status = response.status;
|
|
545
|
+
text = await response.text();
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
await response.arrayBuffer().catch(() => { });
|
|
549
|
+
const next = new URL(location, at);
|
|
550
|
+
if (next.protocol === 'http:' && next.host === new URL(MIMO_PLATFORM).host)
|
|
551
|
+
next.protocol = 'https:';
|
|
552
|
+
at = next.href;
|
|
553
|
+
}
|
|
554
|
+
const data = status === 200 ? parse(text) : undefined;
|
|
555
|
+
if (data === undefined)
|
|
556
|
+
throw new Error(`Xiaomi MiMo platform ${path}: the Xiaomi account didn't sign on (HTTP ${status})`);
|
|
557
|
+
return { cookies: jar.forOrigin(`${MIMO_PLATFORM}/`), data };
|
|
558
|
+
}
|
|
559
|
+
/** Read the account's Token Plan, or null when it has none or the platform can't be read. */
|
|
560
|
+
async function readMimoTokenPlan(creds, fetchFn) {
|
|
561
|
+
try {
|
|
562
|
+
const detail = await mimoPlatformPage(creds, '/tokenPlan/detail', undefined, fetchFn);
|
|
563
|
+
const p = detail.data;
|
|
564
|
+
const name = p !== null && typeof p.planName === 'string' ? p.planName.trim() : '';
|
|
565
|
+
if (name === '' || p?.expired !== false)
|
|
566
|
+
return null;
|
|
567
|
+
const out = { plan: `Token Plan ${name}` };
|
|
568
|
+
const end = typeof p.currentPeriodEnd === 'string' && /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(p.currentPeriodEnd)
|
|
569
|
+
? Date.parse(p.currentPeriodEnd.replace(' ', 'T') + 'Z')
|
|
570
|
+
: NaN;
|
|
571
|
+
if (!Number.isNaN(end))
|
|
572
|
+
out.until = end;
|
|
573
|
+
try {
|
|
574
|
+
const usage = await mimoPlatformPage(creds, '/tokenPlan/usage', detail.cookies, fetchFn);
|
|
575
|
+
const u = usage.data;
|
|
576
|
+
const items = [
|
|
577
|
+
...(isRecord(u) && isRecord(u.monthUsage) && Array.isArray(u.monthUsage.items) ? u.monthUsage.items : []),
|
|
578
|
+
...(isRecord(u) && isRecord(u.usage) && Array.isArray(u.usage.items) ? u.usage.items : []),
|
|
579
|
+
].filter(isRecord);
|
|
580
|
+
const it = items.find((x) => x.name === 'month_total_token') ?? items.find((x) => x.name === 'plan_total_token');
|
|
581
|
+
const used = typeof it?.used === 'number' ? it.used : NaN;
|
|
582
|
+
const limit = typeof it?.limit === 'number' ? it.limit : NaN;
|
|
583
|
+
if (Number.isFinite(used) && Number.isFinite(limit) && limit > 0) {
|
|
584
|
+
const window = {
|
|
585
|
+
label: 'Token Plan',
|
|
586
|
+
usedPercent: Math.max(0, Math.min(100, (100 * used) / limit)),
|
|
587
|
+
used,
|
|
588
|
+
limit,
|
|
589
|
+
};
|
|
590
|
+
if (out.until !== undefined)
|
|
591
|
+
window.resetsAt = out.until;
|
|
592
|
+
out.window = window;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
// The plan alone, when its usage can't be read.
|
|
597
|
+
}
|
|
598
|
+
return out;
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
/** The account's Token Plan as a usable API credential, or null when it has none. */
|
|
605
|
+
export async function readMimoPlanKey(creds, fetchFn) {
|
|
606
|
+
try {
|
|
607
|
+
let cookies;
|
|
608
|
+
const read = async (path) => {
|
|
609
|
+
const result = await mimoPlatformPage(creds, path, cookies, fetchFn);
|
|
610
|
+
cookies = result.cookies;
|
|
611
|
+
return result.data;
|
|
612
|
+
};
|
|
613
|
+
const detail = await read('/tokenPlan/detail');
|
|
614
|
+
if (detail === null || typeof detail.planName !== 'string' || detail.planName.trim() === '' || detail.expired !== false) {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
const keyInfo = await read('/tokenPlan/apiKey');
|
|
618
|
+
const base = typeof keyInfo?.openaiBaseUrl === 'string' && /^https:\/\/[^/]+\.xiaomimimo\.com\//.test(keyInfo.openaiBaseUrl)
|
|
619
|
+
? keyInfo.openaiBaseUrl.replace(/\/+$/, '')
|
|
620
|
+
: '';
|
|
621
|
+
if (base === '')
|
|
622
|
+
return null;
|
|
623
|
+
const raw = await read('/tokenPlan/apiKey/raw');
|
|
624
|
+
const rawKey = typeof raw === 'string' ? raw : '';
|
|
625
|
+
if (!rawKey.startsWith('tp-'))
|
|
626
|
+
return null;
|
|
627
|
+
return { key: rawKey, base };
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
}
|
package/lib/auth/registry.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module dsh-plugin-subscription-oauth/auth/registry
|
|
10
10
|
*/
|
|
11
|
+
import type { ModelPreferences } from '../settings.js';
|
|
11
12
|
import type { LoginStart, ModelVariants, ProviderStatus, RefreshResult, SubscriptionStatus, UsageResult } from '../protocol.js';
|
|
12
13
|
import type { LlmAdapter } from '@deepseek-ai/dsh-llm';
|
|
13
14
|
import type { ProviderLoginStart, ProviderRefreshResult, ProviderUsageResult } from './provider-controller.js';
|
|
@@ -41,8 +42,9 @@ export interface ProviderRoute {
|
|
|
41
42
|
* failure: the message names the routes that do exist.
|
|
42
43
|
*/
|
|
43
44
|
export declare class SubscriptionRegistry {
|
|
45
|
+
private readonly preferences;
|
|
44
46
|
private readonly byId;
|
|
45
|
-
constructor(routes: readonly ProviderRoute[]);
|
|
47
|
+
constructor(routes: readonly ProviderRoute[], preferences: ModelPreferences);
|
|
46
48
|
/** Route keys this registry serves, in display order. */
|
|
47
49
|
ids(): readonly string[];
|
|
48
50
|
/** Every route, in registration order. */
|
|
@@ -92,4 +94,11 @@ export declare class SubscriptionRegistry {
|
|
|
92
94
|
reload(): Promise<SubscriptionStatus>;
|
|
93
95
|
/** The fast and image variants for one model, as the route's discovery reported them. */
|
|
94
96
|
modelVariants(provider: string, id: string): Promise<ModelVariants | undefined>;
|
|
97
|
+
/** Whether the `auto` provider is offered; flips it. */
|
|
98
|
+
autoState(): Promise<{
|
|
99
|
+
enabled: boolean;
|
|
100
|
+
}>;
|
|
101
|
+
setAutoOn(enabled: boolean): Promise<{
|
|
102
|
+
enabled: boolean;
|
|
103
|
+
}>;
|
|
95
104
|
}
|
package/lib/auth/registry.js
CHANGED
|
@@ -29,8 +29,10 @@ function displayRank(id) {
|
|
|
29
29
|
* failure: the message names the routes that do exist.
|
|
30
30
|
*/
|
|
31
31
|
export class SubscriptionRegistry {
|
|
32
|
+
preferences;
|
|
32
33
|
byId;
|
|
33
|
-
constructor(routes) {
|
|
34
|
+
constructor(routes, preferences) {
|
|
35
|
+
this.preferences = preferences;
|
|
34
36
|
this.byId = new Map(routes.map((route) => [route.id, route]));
|
|
35
37
|
}
|
|
36
38
|
/** Route keys this registry serves, in display order. */
|
|
@@ -151,4 +153,12 @@ export class SubscriptionRegistry {
|
|
|
151
153
|
const route = this.route(provider);
|
|
152
154
|
return route.modelVariants?.(id);
|
|
153
155
|
}
|
|
156
|
+
/** Whether the `auto` provider is offered; flips it. */
|
|
157
|
+
async autoState() {
|
|
158
|
+
return { enabled: (await this.preferences.autoPreferences()).enabled };
|
|
159
|
+
}
|
|
160
|
+
async setAutoOn(enabled) {
|
|
161
|
+
await this.preferences.setAutoOn(enabled);
|
|
162
|
+
return { enabled };
|
|
163
|
+
}
|
|
154
164
|
}
|
package/lib/auth/rpc.js
CHANGED
|
@@ -155,6 +155,14 @@ async function runEndpoint(registry, endpoint, payload, balance) {
|
|
|
155
155
|
}
|
|
156
156
|
case 'usage':
|
|
157
157
|
return registry.usage(requiredProvider(payload, endpoint), booleanField(payload, 'force'));
|
|
158
|
+
case 'auto-state':
|
|
159
|
+
return registry.autoState();
|
|
160
|
+
case 'setAutoOn': {
|
|
161
|
+
const enabled = booleanField(payload, 'enabled');
|
|
162
|
+
if (enabled === undefined)
|
|
163
|
+
throw new ProviderError('setAutoOn requires an enabled flag', BAD_REQUEST);
|
|
164
|
+
return registry.setAutoOn(enabled);
|
|
165
|
+
}
|
|
158
166
|
case 'deepseek-balance': {
|
|
159
167
|
// The DeepSeek account's balance, read on the node half through the host's own
|
|
160
168
|
// account service. The browser cannot call that service directly (it sits behind
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* Subscriptions settings page.
|
|
4
4
|
*
|
|
@@ -134,13 +134,21 @@ const styles = {
|
|
|
134
134
|
cardBody: { display: 'flex', flexDirection: 'column', gap: 10, marginTop: 2 },
|
|
135
135
|
/** The one-line state a folded card shows in place of its body. */
|
|
136
136
|
cardSummary: { fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)' },
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
137
|
+
/** The status indicator: filled when live, a hollow ring when not. */
|
|
138
|
+
dot: {
|
|
139
|
+
width: 8, height: 8, borderRadius: '50%', flexShrink: 0,
|
|
140
|
+
border: '1.5px solid var(--dsw-alias-label-dimmed)',
|
|
141
|
+
background: 'transparent',
|
|
142
|
+
},
|
|
143
|
+
dotLive: {
|
|
144
|
+
borderColor: 'var(--dsw-alias-state-success-primary, #067647)',
|
|
145
|
+
background: 'var(--dsw-alias-state-success-primary, #067647)',
|
|
143
146
|
},
|
|
147
|
+
dotBusy: {
|
|
148
|
+
borderColor: 'var(--dsw-alias-state-warn-primary, #f5a524)',
|
|
149
|
+
background: 'var(--dsw-alias-state-warn-primary, #f5a524)',
|
|
150
|
+
},
|
|
151
|
+
name: { fontWeight: 600, fontSize: 14, lineHeight: '22px', color: 'var(--dsw-alias-label-primary)' },
|
|
144
152
|
statusLine: { margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)' },
|
|
145
153
|
errorLine: { margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-state-error-primary)' },
|
|
146
154
|
successLine: { margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-state-success-primary)' },
|
|
@@ -234,6 +242,10 @@ const styles = {
|
|
|
234
242
|
padding: '8px 10px', borderRadius: 'var(--dsw-radius-md, 10px)',
|
|
235
243
|
background: 'var(--dsw-alias-bg-layer-1)', border: '0.5px solid var(--dsw-alias-border-l2)',
|
|
236
244
|
},
|
|
245
|
+
/** The metered windows under one account row: no border, just the meters. */
|
|
246
|
+
usageInline: { display: 'flex', flexDirection: 'column', gap: 6, marginTop: 4 },
|
|
247
|
+
/** The auto provider's on/off row: a label and its switch. */
|
|
248
|
+
autoSwitchRow: { display: 'flex', alignItems: 'center', gap: 8, justifyContent: 'space-between' },
|
|
237
249
|
modelRow: {
|
|
238
250
|
display: 'flex', alignItems: 'flex-start', gap: 8,
|
|
239
251
|
cursor: 'pointer', fontSize: 13, lineHeight: '20px',
|
|
@@ -275,13 +287,13 @@ const styles = {
|
|
|
275
287
|
modalActions: { display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 2 },
|
|
276
288
|
divider: { borderTop: '0.5px solid var(--dsw-alias-border-l2)' },
|
|
277
289
|
};
|
|
278
|
-
/** Status dot
|
|
279
|
-
function
|
|
290
|
+
/** Status dot style for one provider's connection state. */
|
|
291
|
+
function dotStyle(status) {
|
|
280
292
|
if (status.busy)
|
|
281
|
-
return
|
|
293
|
+
return styles.dotBusy;
|
|
282
294
|
if (status.loggedIn)
|
|
283
|
-
return
|
|
284
|
-
return
|
|
295
|
+
return styles.dotLive;
|
|
296
|
+
return {};
|
|
285
297
|
}
|
|
286
298
|
/** Localized label for one provider-owned detail row. */
|
|
287
299
|
function detailLabel(t, row) {
|
|
@@ -301,7 +313,14 @@ function windowAmount(window) {
|
|
|
301
313
|
return `${String(window.used)}/${String(window.limit)}`;
|
|
302
314
|
return '—';
|
|
303
315
|
}
|
|
304
|
-
/** One account's
|
|
316
|
+
/** One account's metered windows, rendered under its account row. */
|
|
317
|
+
function AccountUsage(props) {
|
|
318
|
+
const { entry, t } = props;
|
|
319
|
+
if (entry === undefined)
|
|
320
|
+
return null;
|
|
321
|
+
return (_jsxs("div", { style: styles.usageInline, children: [entry.windows.map((window) => (_jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageLine, children: [_jsxs("span", { style: styles.modelName, children: [windowLabel(t, window), ' · ', windowAmount(window)] }), _jsx("span", { style: styles.modelMeta, children: window.resetsAt === undefined ? '' : formatReset(window.resetsAt, t) })] }), window.usedPercent !== undefined && (_jsx("div", { className: "dsh-subscription-meter", children: _jsx("span", { style: { width: `${String(Math.min(100, Math.max(0, window.usedPercent)))}%` }, "data-exhausted": window.usedPercent >= 90 ? 'true' : 'false' }) }))] }, `${entry.account}-${window.label}`))), entry.windows.length === 0 && (_jsx("p", { style: styles.statusLine, children: t('usageNone') }))] }));
|
|
322
|
+
}
|
|
323
|
+
/** The plan-and-usage block for the whole route. */
|
|
305
324
|
function UsageBlock(props) {
|
|
306
325
|
const { usage, error, t, busyAction, onRefresh } = props;
|
|
307
326
|
return (_jsxs("div", { style: styles.usage, children: [_jsxs("div", { style: styles.usageHeader, children: [_jsx("span", { style: styles.catalogTitle, children: t('usageTitle') }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.pushRight, disabled: busyAction, onClick: onRefresh, children: busyAction ? t('refreshing') : t('usageRefresh') })] }), error !== undefined && _jsx("p", { className: "dsh-subscription-error", children: t('usageError', { message: error }) }), usage.length === 0 && error === undefined && _jsx("p", { style: styles.statusLine, children: t('usageNone') }), usage.map((entry) => (_jsxs("div", { style: styles.usageAccount, children: [_jsxs("div", { style: styles.usageLine, children: [_jsxs("span", { style: styles.modelMeta, children: [entry.plan === undefined ? entry.account : t('usagePlan', { plan: entry.plan }), entry.renewsAt === undefined ? '' : ` · ${t('usageRenews', { time: formatTime(entry.renewsAt) })}`] }), _jsx("span", { style: styles.modelMeta, children: t('usageFetched', { time: formatTime(entry.fetchedAt) }) })] }), entry.windows.map((window) => (_jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageLine, children: [_jsxs("span", { style: styles.modelName, children: [windowLabel(t, window), ' · ', windowAmount(window)] }), _jsx("span", { style: styles.modelMeta, children: window.resetsAt === undefined ? '' : formatReset(window.resetsAt, t) })] }), window.usedPercent !== undefined && (_jsx("div", { className: "dsh-subscription-meter", children: _jsx("span", { style: { width: `${String(Math.min(100, Math.max(0, window.usedPercent)))}%` }, "data-exhausted": window.usedPercent >= 90 ? 'true' : 'false' }) }))] }, `${entry.account}-${window.label}`)))] }, entry.account)))] }));
|
|
@@ -500,9 +519,12 @@ function ProviderCard(props) {
|
|
|
500
519
|
return (_jsxs("div", { style: cardOpen ? { ...styles.card, ...styles.cardOpen } : styles.card, children: [_jsxs("div", { style: styles.cardHeader, role: "button", tabIndex: 0, "aria-expanded": cardOpen, onClick: toggleCardOpen, onKeyDown: (event) => { if (event.key === 'Enter' || event.key === ' ') {
|
|
501
520
|
event.preventDefault();
|
|
502
521
|
toggleCardOpen();
|
|
503
|
-
} }, children: [_jsx("span", { style: { ...styles.dot,
|
|
504
|
-
|
|
505
|
-
|
|
522
|
+
} }, children: [_jsx("span", { style: { ...styles.dot, ...dotStyle(status) }, "aria-hidden": "true" }), _jsx("span", { style: styles.name, children: status.displayName }), status.loggedIn && _jsx("span", { style: styles.badgeDefault, children: t('badgeLive') }), _jsx("span", { style: styles.cardSummary, children: summaryLine }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.pushRight, "aria-expanded": cardOpen, "aria-controls": `dsh-subscription-card-${status.provider}`, onClick: (event) => { event.stopPropagation(); toggleCardOpen(); }, children: cardOpen ? t('collapse') : t('edit') })] }), _jsxs("div", { id: `dsh-subscription-card-${status.provider}`, style: cardOpen ? styles.cardBody : { ...styles.cardBody, display: 'none' }, children: [connectable && !status.loggedIn && !busy && _jsx("p", { style: styles.statusLine, children: t('statusSignIn') }), status.lastError !== undefined && (_jsx("p", { className: "dsh-subscription-error", children: t('lastError', { message: status.lastError }) })), message?.tone === 'error' && _jsx("p", { className: "dsh-subscription-error", children: message.text }), message?.tone === 'success' && _jsx("p", { className: "dsh-subscription-success", children: message.text }), status.provider === 'auto' && props.onSetAutoOn !== undefined && (_jsxs("label", { style: styles.autoSwitchRow, children: [_jsx("span", { style: styles.modelName, children: t('autoSwitchLabel') }), _jsx("input", { type: "checkbox", className: "dsh-subscription-switch", checked: props.autoOn ?? true, disabled: busyAction, onChange: (event) => { props.onSetAutoOn?.(event.target.checked); } })] })), accounts.map((account) => {
|
|
523
|
+
const usageEntry = (status.usage ?? []).find((entry) => entry.account === account.account);
|
|
524
|
+
return (_jsxs("div", { style: styles.accountRow, children: [_jsxs("div", { style: styles.accountHeader, children: [_jsx("button", { type: "button", style: styles.starButton, title: account.isDefault ? t('defaultBadge') : t('makeDefault'), "aria-label": account.isDefault ? t('defaultBadge') : t('makeDefault'), disabled: busyAction || account.isDefault, onClick: () => { props.onSetDefault(account.account); }, children: account.isDefault ? '★' : '☆' }), _jsx("span", { style: styles.accountName, children: account.label }), account.isDefault && _jsx("span", { style: styles.badgeDefault, children: t('defaultBadge') }), account.needsLogin === true && (_jsx("span", { style: styles.badgeWarn, title: t('accountNeedsLoginHint'), children: t('accountNeedsLogin') })), usageEntry?.plan !== undefined && (_jsx("span", { style: styles.badge, children: t('usagePlan', { plan: usageEntry.plan }) })), _jsx("span", { style: styles.statusLine, children: account.expired
|
|
525
|
+
? t('accountExpired')
|
|
526
|
+
: t('accountExpires', { time: formatTime(account.expiresAt) }) }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.pushRight, disabled: busyAction, onClick: () => { props.onLogout(account.account); }, children: t('logout') })] }), _jsx(AccountUsage, { entry: usageEntry, t: t })] }, account.account));
|
|
527
|
+
}), status.loggedIn && (_jsxs("div", { style: styles.catalog, children: [_jsxs("div", { style: styles.catalogHeader, children: [_jsxs("button", { type: "button", className: "dsh-subscription-disclosure", style: styles.disclosure, "aria-expanded": modelsOpen, "aria-controls": `dsh-subscription-models-${status.provider}`, onClick: toggleModelsOpen, children: [_jsx("span", { style: styles.chevron, "aria-hidden": "true", children: modelsOpen ? '▾' : '▸' }), _jsx("span", { style: styles.catalogTitle, children: t('modelsTitle') })] }), _jsxs("span", { style: styles.catalogMeta, children: [t('modelsCount', { count: models.length }), ' · ', liveCatalog ? t('modelsLive') : t('modelsBuiltIn'), hiddenCount === 0 ? '' : ` ${t('modelsHiddenCount', { count: hiddenCount })}`] }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.pushRight, disabled: busyAction, onClick: props.onRefreshModels, children: busyAction ? t('refreshing') : t('refresh') }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", disabled: busyAction || !connectable, title: t('modelsAddHint'), onClick: () => { setAdding((open) => !open); }, children: t('modelsAdd') })] }), adding && (_jsxs("form", { style: styles.addRow, onSubmit: (event) => {
|
|
506
528
|
event.preventDefault();
|
|
507
529
|
const id = draft.trim();
|
|
508
530
|
if (id.length === 0)
|
|
@@ -525,7 +547,7 @@ function ProviderCard(props) {
|
|
|
525
547
|
? { ...styles.modelRowGrow, cursor: 'pointer' }
|
|
526
548
|
: { ...styles.modelRowGrow, cursor: 'pointer', color: 'var(--dsw-alias-label-dimmed)' }, title: model.visible ? t('modelsHidden') : t('modelsShow'), children: [_jsx("input", { type: "checkbox", className: "dsh-subscription-switch", role: "switch", checked: model.visible, disabled: busyAction, "aria-label": `${t('modelsShow')} · ${model.name}`, onChange: (event) => { props.onSetModelVisible(model.id, event.target.checked); } }), _jsxs("span", { style: styles.modelBody, children: [_jsxs("span", { style: styles.modelName, children: [model.name, model.source === undefined ? null : _jsx("span", { style: styles.badge, children: model.source })] }), _jsxs("span", { style: styles.modelMeta, children: [model.id, ' · ', t('modelsContext', { tokens: formatTokens(model.contextWindow) }), model.defaultEffort === undefined ? '' : ` · ${model.defaultEffort}`, model.inputModalities === undefined || model.inputModalities.length === 0
|
|
527
549
|
? ''
|
|
528
|
-
: ` · ${model.inputModalities.join('/')}`] })] })] }), model.userAdded === true && (_jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.removeButton, disabled: busyAction, title: t('modelsRemove'), "aria-label": `${t('modelsRemove')} · ${model.name}`, onClick: () => { props.onRemoveModel(model.id); }, children: "\u00D7" }))] }, model.id))), models.length === 0 && (_jsx("p", { style: styles.statusLine, children: status.provider === 'auto' ? t('modelsAutoChainEmpty') : t('modelsEmpty') }))] }))] })),
|
|
550
|
+
: ` · ${model.inputModalities.join('/')}`] })] })] }), model.userAdded === true && (_jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.removeButton, disabled: busyAction, title: t('modelsRemove'), "aria-label": `${t('modelsRemove')} · ${model.name}`, onClick: () => { props.onRemoveModel(model.id); }, children: "\u00D7" }))] }, model.id))), models.length === 0 && (_jsx("p", { style: styles.statusLine, children: status.provider === 'auto' ? t('modelsAutoChainEmpty') : t('modelsEmpty') }))] }))] })), connectable && (_jsxs("div", { style: styles.actions, children: [!busy && (_jsx("button", { type: "button", disabled: busyAction, onClick: props.onLogin, children: accounts.length === 0 ? t('connect') : t('addAccount') })), !busy && (_jsx("button", { type: "button", disabled: busyAction, onClick: () => { setManageOpen(true); }, children: t('manage') })), busy && (_jsx("button", { type: "button", disabled: busyAction, onClick: props.onCancel, children: t('cancel') }))] })), connectable && accounts.length > 0 && _jsx("p", { style: styles.statusLine, children: t('hint') }), busy && status.pending !== undefined && (_jsx(PendingLogin, { prompt: status.pending, t: t, copied: copied, onCopy: (code) => { void copyCode(code); } }))] }), _jsxs("dialog", { ref: dialog, className: "dsh-subscription-manager", "aria-labelledby": `dsh-subscription-manage-${status.provider}`,
|
|
529
551
|
// A closed `<dialog>` is hidden by the user-agent stylesheet, and an
|
|
530
552
|
// inline `display` would override that rule — so the open state has to
|
|
531
553
|
// be restated here rather than left to `showModal()` alone.
|
|
@@ -543,8 +565,20 @@ export function SubscriptionsSection(props) {
|
|
|
543
565
|
const [error, setError] = useState(undefined);
|
|
544
566
|
const [messages, setMessages] = useState({});
|
|
545
567
|
const [busyAction, setBusyAction] = useState(false);
|
|
568
|
+
const [autoOn, setAutoOn] = useState(true);
|
|
546
569
|
const mounted = useRef(true);
|
|
547
570
|
const timer = useRef(undefined);
|
|
571
|
+
/** Read whether the `auto` provider is offered, so its card can render the switch. */
|
|
572
|
+
useEffect(() => {
|
|
573
|
+
if (call === undefined)
|
|
574
|
+
return;
|
|
575
|
+
let live = true;
|
|
576
|
+
void call('auto-state', undefined)
|
|
577
|
+
.then((result) => { if (live)
|
|
578
|
+
setAutoOn(result.enabled); })
|
|
579
|
+
.catch(() => { });
|
|
580
|
+
return () => { live = false; };
|
|
581
|
+
}, [call]);
|
|
548
582
|
/** Fetch the current state; a failure is shown rather than swallowed. */
|
|
549
583
|
const refresh = useCallback(async () => {
|
|
550
584
|
if (call === undefined)
|
|
@@ -561,6 +595,27 @@ export function SubscriptionsSection(props) {
|
|
|
561
595
|
setError(t('actionFailed', { message: messageOf(caught) }));
|
|
562
596
|
}
|
|
563
597
|
}, [call, t]);
|
|
598
|
+
/** Flip the `auto` provider on or off. */
|
|
599
|
+
const setAuto = useCallback(async (enabled) => {
|
|
600
|
+
if (call === undefined)
|
|
601
|
+
return;
|
|
602
|
+
setBusyAction(true);
|
|
603
|
+
try {
|
|
604
|
+
const result = await call('setAutoOn', { enabled });
|
|
605
|
+
if (!mounted.current)
|
|
606
|
+
return;
|
|
607
|
+
setAutoOn(result.enabled);
|
|
608
|
+
await refresh();
|
|
609
|
+
}
|
|
610
|
+
catch (caught) {
|
|
611
|
+
if (mounted.current)
|
|
612
|
+
setError(t('actionFailed', { message: messageOf(caught) }));
|
|
613
|
+
}
|
|
614
|
+
finally {
|
|
615
|
+
if (mounted.current)
|
|
616
|
+
setBusyAction(false);
|
|
617
|
+
}
|
|
618
|
+
}, [call, refresh, t]);
|
|
564
619
|
const stopPolling = useCallback(() => {
|
|
565
620
|
if (timer.current !== undefined) {
|
|
566
621
|
clearInterval(timer.current);
|
|
@@ -804,5 +859,5 @@ export function SubscriptionsSection(props) {
|
|
|
804
859
|
}, [act, call]);
|
|
805
860
|
if (call === undefined)
|
|
806
861
|
return _jsx("p", { style: styles.intro, children: t('unavailable') });
|
|
807
|
-
return (_jsxs("div", { className: "dsh-subscriptions-settings", style: styles.section, children: [_jsx("style", { children: subscriptionsCss }), _jsxs("div", { style: styles.sectionHeader, children: [_jsx("h2", { style: styles.heading, children: t('nav') }), _jsx("p", { style: styles.intro, children: t('intro') })] }), error !== undefined && _jsx("p", { className: "dsh-subscription-error", children: error }), status === undefined && _jsx("p", { style: styles.statusLine, children: t('loading') }), orderedProviders.map((provider) => (_jsx(ProviderCard, { status: provider, t: t, busyAction: busyAction, ...messages[provider.provider] === undefined ? {} : { message: messages[provider.provider] }, onLogin: () => { login(provider.provider); }, onCancel: () => { cancel(provider.provider); }, onLogout: (account) => { logout(provider.provider, account); }, onSetDefault: (account) => { setDefault(provider.provider, account); }, onRefreshToken: (account) => { refreshToken(provider.provider, account); }, onRefreshModels: () => { refreshModels(provider.provider); }, onSetModelVisible: (id, visible) => { setModelVisible(provider.provider, id, visible); }, onSetModelOrder: (ids) => { setModelOrder(provider.provider, ids); }, onAddModel: (id) => { addModel(provider.provider, id); }, onRemoveModel: (id) => { removeModel(provider.provider, id); }, onRefreshUsage: () => { refreshUsage(provider.provider); }, onReload: () => { reload(provider.provider); } }, provider.provider)))] }));
|
|
862
|
+
return (_jsxs("div", { className: "dsh-subscriptions-settings", style: styles.section, children: [_jsx("style", { children: subscriptionsCss }), _jsxs("div", { style: styles.sectionHeader, children: [_jsx("h2", { style: styles.heading, children: t('nav') }), _jsx("p", { style: styles.intro, children: t('intro') })] }), error !== undefined && _jsx("p", { className: "dsh-subscription-error", children: error }), status === undefined && _jsx("p", { style: styles.statusLine, children: t('loading') }), orderedProviders.map((provider) => (_jsx(ProviderCard, { status: provider, t: t, busyAction: busyAction, ...messages[provider.provider] === undefined ? {} : { message: messages[provider.provider] }, onLogin: () => { login(provider.provider); }, onCancel: () => { cancel(provider.provider); }, onLogout: (account) => { logout(provider.provider, account); }, onSetDefault: (account) => { setDefault(provider.provider, account); }, onRefreshToken: (account) => { refreshToken(provider.provider, account); }, onRefreshModels: () => { refreshModels(provider.provider); }, onSetModelVisible: (id, visible) => { setModelVisible(provider.provider, id, visible); }, onSetModelOrder: (ids) => { setModelOrder(provider.provider, ids); }, onAddModel: (id) => { addModel(provider.provider, id); }, onRemoveModel: (id) => { removeModel(provider.provider, id); }, onRefreshUsage: () => { refreshUsage(provider.provider); }, onReload: () => { reload(provider.provider); }, ...provider.provider !== 'auto' ? {} : { autoOn, onSetAutoOn: (enabled) => { void setAuto(enabled); } } }, provider.provider)))] }));
|
|
808
863
|
}
|
package/lib/client/UsageChip.js
CHANGED
|
@@ -207,12 +207,18 @@ export function mergeAccountWindows(readings) {
|
|
|
207
207
|
if (withPercent.length === 0)
|
|
208
208
|
continue;
|
|
209
209
|
const usedPercent = withPercent.reduce((sum, window) => sum + (window.usedPercent ?? 0), 0) / withPercent.length;
|
|
210
|
+
// A credit window carries the raw figures too; keep them so the compact line can read
|
|
211
|
+
// money as money (0/1500) rather than as a spent share (0%), which misreads an empty
|
|
212
|
+
// balance as nothing to meter.
|
|
213
|
+
const used = list.reduce((sum, window) => sum + (window.used ?? 0), 0);
|
|
214
|
+
const limit = list.reduce((sum, window) => sum + (window.limit ?? 0), 0);
|
|
210
215
|
// The pool's reset is the soonest account reset: the window refills when the first
|
|
211
216
|
// account's does.
|
|
212
217
|
const resets = list.map((window) => window.resetsAt).filter((at) => typeof at === 'number');
|
|
213
218
|
merged.push({
|
|
214
219
|
label,
|
|
215
220
|
usedPercent: Math.max(0, Math.min(100, usedPercent)),
|
|
221
|
+
...limit > 0 ? { used, limit } : {},
|
|
216
222
|
...resets.length === 0 ? {} : { resetsAt: Math.min(...resets) },
|
|
217
223
|
});
|
|
218
224
|
}
|
|
@@ -430,7 +436,9 @@ export function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBala
|
|
|
430
436
|
// The document listener would otherwise close what this click just opened.
|
|
431
437
|
event.stopPropagation();
|
|
432
438
|
setOpen((was) => !was);
|
|
433
|
-
}, 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:
|
|
439
|
+
}, 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: window.used !== undefined && window.limit !== undefined
|
|
440
|
+
? `${String(window.used)}/${String(window.limit)}`
|
|
441
|
+
: `${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) => {
|
|
434
442
|
const windowReset = formatReset(window.resetsAt, now);
|
|
435
443
|
// A credit balance reads as money (`used`/`limit`), a quota as a percent.
|
|
436
444
|
const amount = window.usedPercent === undefined && window.used !== undefined && window.limit !== undefined
|
package/lib/client/locales.d.ts
CHANGED
|
@@ -60,6 +60,7 @@ export declare const en: {
|
|
|
60
60
|
readonly modelsDrag: "Drag to re-order (or press ↑ / ↓ here)";
|
|
61
61
|
readonly modelsAutoChainOff: "Take out of Auto";
|
|
62
62
|
readonly modelsAutoChainEmpty: "No model is enabled, so Auto has nothing to try.";
|
|
63
|
+
readonly autoSwitchLabel: "Offer Auto in the model picker";
|
|
63
64
|
readonly modelsPinned: "pinned";
|
|
64
65
|
readonly usageTitle: "Plan and usage";
|
|
65
66
|
readonly usagePlan: "Plan: {plan}";
|
|
@@ -157,6 +158,7 @@ export declare const zh: {
|
|
|
157
158
|
modelsDrag: string;
|
|
158
159
|
modelsAutoChainOff: string;
|
|
159
160
|
modelsAutoChainEmpty: string;
|
|
161
|
+
autoSwitchLabel: string;
|
|
160
162
|
modelsPinned: string;
|
|
161
163
|
usageTitle: string;
|
|
162
164
|
usagePlan: string;
|
package/lib/client/locales.js
CHANGED
|
@@ -60,6 +60,7 @@ export const en = {
|
|
|
60
60
|
modelsDrag: 'Drag to re-order (or press ↑ / ↓ here)',
|
|
61
61
|
modelsAutoChainOff: 'Take out of Auto',
|
|
62
62
|
modelsAutoChainEmpty: 'No model is enabled, so Auto has nothing to try.',
|
|
63
|
+
autoSwitchLabel: 'Offer Auto in the model picker',
|
|
63
64
|
modelsPinned: 'pinned',
|
|
64
65
|
usageTitle: 'Plan and usage',
|
|
65
66
|
usagePlan: 'Plan: {plan}',
|
|
@@ -155,6 +156,7 @@ export const zh = {
|
|
|
155
156
|
modelsDrag: '拖动调整顺序(也可在此按 ↑ / ↓)',
|
|
156
157
|
modelsAutoChainOff: '从自动中移除',
|
|
157
158
|
modelsAutoChainEmpty: '没有启用任何模型,自动没有可尝试的模型。',
|
|
159
|
+
autoSwitchLabel: '在模型选择器中提供 Auto',
|
|
158
160
|
modelsPinned: '固定',
|
|
159
161
|
usageTitle: '套餐与额度',
|
|
160
162
|
usagePlan: '套餐:{plan}',
|