dsh-plugin-subscription-oauth 0.3.0 → 0.5.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/ccswitch.d.ts +36 -0
- package/lib/auth/ccswitch.js +75 -0
- package/lib/auth/rpc.d.ts +8 -1
- package/lib/auth/rpc.js +29 -17
- package/lib/client/UsageChip.d.ts +188 -0
- package/lib/client/UsageChip.js +347 -0
- package/lib/client/index.d.ts +28 -1
- package/lib/client/index.js +46 -1
- package/lib/client/locales.d.ts +4 -0
- package/lib/client/locales.js +4 -0
- package/lib/client/styles.d.ts +15 -1
- package/lib/client/styles.js +128 -1
- package/lib/client/subscriptions-rpc.d.ts +53 -1
- package/lib/client/subscriptions-rpc.js +56 -0
- package/lib/client.js +652 -5
- package/lib/client.js.map +1 -1
- package/lib/protocol.d.ts +50 -1
- package/lib/protocol.js +1 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -126,7 +126,9 @@ const en = {
|
|
|
126
126
|
detail_project: "Cloud project",
|
|
127
127
|
lastError: "Last login attempt failed: {message}",
|
|
128
128
|
loginFailed: "Could not start the sign-in: {message}",
|
|
129
|
-
actionFailed: "Action failed: {message}"
|
|
129
|
+
actionFailed: "Action failed: {message}",
|
|
130
|
+
usageChipTitle: "{provider} subscription usage",
|
|
131
|
+
usageChipFull: "{provider} allowance"
|
|
130
132
|
};
|
|
131
133
|
/** Chinese strings. */
|
|
132
134
|
const zh = {
|
|
@@ -218,7 +220,9 @@ const zh = {
|
|
|
218
220
|
detail_project: "云项目",
|
|
219
221
|
lastError: "上次登录失败:{message}",
|
|
220
222
|
loginFailed: "无法开始登录:{message}",
|
|
221
|
-
actionFailed: "操作失败:{message}"
|
|
223
|
+
actionFailed: "操作失败:{message}",
|
|
224
|
+
usageChipTitle: "{provider} 订阅用量",
|
|
225
|
+
usageChipFull: "{provider} 额度"
|
|
222
226
|
};
|
|
223
227
|
|
|
224
228
|
//#endregion
|
|
@@ -468,7 +472,13 @@ ${scope} .dsh-subscription-disclosure:hover:not(:disabled) {
|
|
|
468
472
|
${scope} .dsh-subscription-meter {
|
|
469
473
|
height: 6px;
|
|
470
474
|
border-radius: 999px;
|
|
471
|
-
|
|
475
|
+
/* The track must stay visible when nothing is spent — a 0 % window otherwise reads as
|
|
476
|
+
"no meter at all", which is the opposite of "nothing used". The layer token falls
|
|
477
|
+
back to the card's own colour on a host that does not define it, so the neutral is
|
|
478
|
+
explicit and translucent, and the inset ring keeps the empty track visible on a
|
|
479
|
+
light theme as well as a dark one. */
|
|
480
|
+
background: #8a8f9826;
|
|
481
|
+
box-shadow: inset 0 0 0 1px #8a8f9833;
|
|
472
482
|
overflow: hidden;
|
|
473
483
|
}
|
|
474
484
|
${scope} .dsh-subscription-meter > span {
|
|
@@ -480,6 +490,127 @@ ${scope} .dsh-subscription-meter > span[data-exhausted="true"] {
|
|
|
480
490
|
background: var(--dsw-alias-state-error-primary, #b42318);
|
|
481
491
|
}
|
|
482
492
|
`;
|
|
493
|
+
/**
|
|
494
|
+
* The composer usage chip's stylesheet.
|
|
495
|
+
*
|
|
496
|
+
* Separate from the page's, because the chip renders in the composer's own bar rather
|
|
497
|
+
* than inside either page container — the page stylesheet is scoped to those
|
|
498
|
+
* containers on purpose, so nothing here may leak into the shell.
|
|
499
|
+
*
|
|
500
|
+
* The chip inherits the bar's type and only draws a meter, so it sits beside the shell's
|
|
501
|
+
* own stats pills without looking like a second design. The trigger is a bare control:
|
|
502
|
+
* the host gives plugin markup no control classes, and a default `<button>` would
|
|
503
|
+
* otherwise read as a raised chip. Every token carries a fallback for hosts that do not
|
|
504
|
+
* define it.
|
|
505
|
+
*/
|
|
506
|
+
const usageChipCss = `
|
|
507
|
+
.dsh-subscription-usage-chip {
|
|
508
|
+
position: relative;
|
|
509
|
+
display: inline-flex;
|
|
510
|
+
align-items: center;
|
|
511
|
+
/* The bar is a centred flex row whose children are this slot's entries followed by the
|
|
512
|
+
shell's context meter, and a list slot renders its entries as bare children (no
|
|
513
|
+
wrapper). So the chip is itself a flex item: a higher order places it after the
|
|
514
|
+
context meter, and no auto margin is set, so it sits immediately beside it rather
|
|
515
|
+
than being pushed to the edge of the bar. */
|
|
516
|
+
order: 1;
|
|
517
|
+
min-width: 0;
|
|
518
|
+
font-family: inherit;
|
|
519
|
+
font-size: 12px;
|
|
520
|
+
line-height: 1;
|
|
521
|
+
}
|
|
522
|
+
.dsh-subscription-usage-chip-trigger {
|
|
523
|
+
display: inline-flex;
|
|
524
|
+
align-items: center;
|
|
525
|
+
gap: 6px;
|
|
526
|
+
margin: 0;
|
|
527
|
+
padding: 2px 6px;
|
|
528
|
+
border: 0;
|
|
529
|
+
border-radius: var(--dsw-radius-sm, 6px);
|
|
530
|
+
background: transparent;
|
|
531
|
+
color: var(--dsw-alias-label-secondary, #8a8f98);
|
|
532
|
+
font: inherit;
|
|
533
|
+
cursor: pointer;
|
|
534
|
+
}
|
|
535
|
+
.dsh-subscription-usage-chip-trigger:hover {
|
|
536
|
+
background: var(--dsw-alias-interactive-bg-hover, #ffffff14);
|
|
537
|
+
}
|
|
538
|
+
.dsh-subscription-usage-chip-trigger:focus-visible {
|
|
539
|
+
outline: 2px solid var(--dsw-alias-border-focus, #4d6bfe);
|
|
540
|
+
outline-offset: 1px;
|
|
541
|
+
}
|
|
542
|
+
.dsh-subscription-usage-chip-label {
|
|
543
|
+
overflow: hidden;
|
|
544
|
+
max-width: 6ch;
|
|
545
|
+
text-overflow: ellipsis;
|
|
546
|
+
white-space: nowrap;
|
|
547
|
+
}
|
|
548
|
+
.dsh-subscription-usage-chip-cell {
|
|
549
|
+
display: inline-flex;
|
|
550
|
+
align-items: baseline;
|
|
551
|
+
gap: 2px;
|
|
552
|
+
}
|
|
553
|
+
.dsh-subscription-usage-chip-window {
|
|
554
|
+
opacity: 0.75;
|
|
555
|
+
}
|
|
556
|
+
.dsh-subscription-usage-chip-value {
|
|
557
|
+
color: var(--dsw-alias-label-primary, #e8e8e8);
|
|
558
|
+
font-variant-numeric: tabular-nums;
|
|
559
|
+
}
|
|
560
|
+
.dsh-subscription-usage-chip-panel {
|
|
561
|
+
position: absolute;
|
|
562
|
+
bottom: calc(100% + 6px);
|
|
563
|
+
right: 0;
|
|
564
|
+
z-index: 20;
|
|
565
|
+
display: flex;
|
|
566
|
+
flex-direction: column;
|
|
567
|
+
gap: 8px;
|
|
568
|
+
min-width: 168px;
|
|
569
|
+
padding: 10px 12px;
|
|
570
|
+
border: 1px solid var(--dsw-alias-border-secondary, #ffffff1f);
|
|
571
|
+
border-radius: var(--dsw-radius-md, 10px);
|
|
572
|
+
background: var(--dsw-alias-bg-layer-2, #26262a);
|
|
573
|
+
box-shadow: var(--dsw-elevation-prominent, 0 12px 32px #0007);
|
|
574
|
+
color: var(--dsw-alias-label-primary, #e8e8e8);
|
|
575
|
+
white-space: nowrap;
|
|
576
|
+
}
|
|
577
|
+
.dsh-subscription-usage-chip-full {
|
|
578
|
+
font-size: 11px;
|
|
579
|
+
color: var(--dsw-alias-label-secondary, #8a8f98);
|
|
580
|
+
}
|
|
581
|
+
.dsh-subscription-usage-chip-meter {
|
|
582
|
+
display: flex;
|
|
583
|
+
flex-direction: column;
|
|
584
|
+
gap: 4px;
|
|
585
|
+
}
|
|
586
|
+
.dsh-subscription-usage-chip-meterHead {
|
|
587
|
+
display: flex;
|
|
588
|
+
align-items: baseline;
|
|
589
|
+
justify-content: space-between;
|
|
590
|
+
gap: 12px;
|
|
591
|
+
font-size: 12px;
|
|
592
|
+
}
|
|
593
|
+
.dsh-subscription-usage-chip-detail {
|
|
594
|
+
color: var(--dsw-alias-label-secondary, #8a8f98);
|
|
595
|
+
font-variant-numeric: tabular-nums;
|
|
596
|
+
}
|
|
597
|
+
.dsh-subscription-usage-chip-track {
|
|
598
|
+
display: block;
|
|
599
|
+
width: 100%;
|
|
600
|
+
height: 5px;
|
|
601
|
+
border-radius: 999px;
|
|
602
|
+
/* As on the settings meter: an unspent window has to show its track, so the neutral and
|
|
603
|
+
its ring are explicit rather than a layer token that may match the panel. */
|
|
604
|
+
background: #8a8f9826;
|
|
605
|
+
box-shadow: inset 0 0 0 1px #8a8f9833;
|
|
606
|
+
overflow: hidden;
|
|
607
|
+
}
|
|
608
|
+
.dsh-subscription-usage-chip-fill {
|
|
609
|
+
display: block;
|
|
610
|
+
height: 100%;
|
|
611
|
+
background: var(--dsw-alias-state-success-primary, #067647);
|
|
612
|
+
}
|
|
613
|
+
`;
|
|
483
614
|
|
|
484
615
|
//#endregion
|
|
485
616
|
//#region src/protocol.ts
|
|
@@ -501,6 +632,8 @@ const RPC_PREFIX = "subscription-oauth.";
|
|
|
501
632
|
//#region src/client/subscriptions-rpc.ts
|
|
502
633
|
/** The shared channel the node half's routes are mounted on. */
|
|
503
634
|
const CHANNEL = "/api";
|
|
635
|
+
/** The host's own DeepSeek account controller, from which the chip reads a balance. */
|
|
636
|
+
const ACCOUNT_CHANNEL = "/api";
|
|
504
637
|
/** A failed call: either the transport rejected or the endpoint answered `ok: false`. */
|
|
505
638
|
var SubscriptionsRpcError = class extends Error {
|
|
506
639
|
constructor(message, code) {
|
|
@@ -530,6 +663,55 @@ function createSubscriptionsCall(rpc) {
|
|
|
530
663
|
return result.value;
|
|
531
664
|
};
|
|
532
665
|
}
|
|
666
|
+
/**
|
|
667
|
+
* Read the DeepSeek account's balance through the host's own account controller.
|
|
668
|
+
*
|
|
669
|
+
* The host owns the DeepSeek OAuth grant and the platform endpoint, so this asks it
|
|
670
|
+
* rather than re-deriving either. It returns `null` when the account is signed out or
|
|
671
|
+
* the read fails, so a chip can simply not render in that case.
|
|
672
|
+
*
|
|
673
|
+
* @param rpc - the browser connection's RPC caller.
|
|
674
|
+
* @returns the balance, or null when there is nothing to show.
|
|
675
|
+
*/
|
|
676
|
+
function createAccountBalanceCall(rpc) {
|
|
677
|
+
return async function call() {
|
|
678
|
+
let result;
|
|
679
|
+
try {
|
|
680
|
+
result = await rpc.call(ACCOUNT_CHANNEL, "account/getBalance", {
|
|
681
|
+
version: "1",
|
|
682
|
+
locale: Intl.DateTimeFormat().resolvedOptions().locale ?? "en-US",
|
|
683
|
+
timezoneOffsetSeconds: -(/* @__PURE__ */ new Date()).getTimezoneOffset()
|
|
684
|
+
});
|
|
685
|
+
} catch (error) {
|
|
686
|
+
throw new SubscriptionsRpcError(messageOf$1(error), "RPC_TRANSPORT");
|
|
687
|
+
}
|
|
688
|
+
if (!result.ok) throw new SubscriptionsRpcError(result.error.message, result.error.code);
|
|
689
|
+
return result.value;
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Read a manually added provider's balance through the plugin's cc-switch endpoint.
|
|
694
|
+
*
|
|
695
|
+
* The caller names the credential reference the provider's profile uses (its
|
|
696
|
+
* `apiKeyEnv`), and the host's credential service resolves it to the API key. The result
|
|
697
|
+
* is `undefined` when there is no key or the provider did not answer — never a wrong
|
|
698
|
+
* figure.
|
|
699
|
+
*
|
|
700
|
+
* @param rpc - the browser connection's RPC caller.
|
|
701
|
+
* @returns a narrow caller taking the credential reference.
|
|
702
|
+
*/
|
|
703
|
+
function createCcSwitchBalanceCall(rpc) {
|
|
704
|
+
return async function call(credentialRef) {
|
|
705
|
+
let result;
|
|
706
|
+
try {
|
|
707
|
+
result = await rpc.call(CHANNEL, `${RPC_PREFIX}ccswitch-balance`, { credentialRef });
|
|
708
|
+
} catch (error) {
|
|
709
|
+
throw new SubscriptionsRpcError(messageOf$1(error), "RPC_TRANSPORT");
|
|
710
|
+
}
|
|
711
|
+
if (!result.ok) throw new SubscriptionsRpcError(result.error.message, result.error.code);
|
|
712
|
+
return result.value;
|
|
713
|
+
};
|
|
714
|
+
}
|
|
533
715
|
|
|
534
716
|
//#endregion
|
|
535
717
|
//#region src/client/SubscriptionsSection.tsx
|
|
@@ -586,7 +768,7 @@ const RESET_RELATIVE_MS = 1440 * 60 * 1e3;
|
|
|
586
768
|
* @param t - translator, so the wording follows the page language.
|
|
587
769
|
* @returns the reset text.
|
|
588
770
|
*/
|
|
589
|
-
function formatReset(at, t) {
|
|
771
|
+
function formatReset$1(at, t) {
|
|
590
772
|
const remaining = at - Date.now();
|
|
591
773
|
if (remaining <= 0) return t("usageResetDue");
|
|
592
774
|
if (remaining >= RESET_RELATIVE_MS) return t("usageReset", { time: formatTime(at) });
|
|
@@ -1051,7 +1233,7 @@ function UsageBlock(props) {
|
|
|
1051
1233
|
]
|
|
1052
1234
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1053
1235
|
style: styles.modelMeta,
|
|
1054
|
-
children: window$1.resetsAt === void 0 ? "" : formatReset(window$1.resetsAt, t)
|
|
1236
|
+
children: window$1.resetsAt === void 0 ? "" : formatReset$1(window$1.resetsAt, t)
|
|
1055
1237
|
})]
|
|
1056
1238
|
}), window$1.usedPercent !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1057
1239
|
className: "dsh-subscription-meter",
|
|
@@ -2050,13 +2232,465 @@ function SubscriptionsSection(props) {
|
|
|
2050
2232
|
});
|
|
2051
2233
|
}
|
|
2052
2234
|
|
|
2235
|
+
//#endregion
|
|
2236
|
+
//#region src/client/UsageChip.tsx
|
|
2237
|
+
/** How often a visible chip refreshes its figures. */
|
|
2238
|
+
const REFRESH_INTERVAL_MS = 6e4;
|
|
2239
|
+
/**
|
|
2240
|
+
* Abbreviations for the routes this plugin serves.
|
|
2241
|
+
*
|
|
2242
|
+
* The bar is a status line under the composer, where a full product name crowds out the
|
|
2243
|
+
* shell's own pills; the full name is one click away. The fallback shortens an unknown
|
|
2244
|
+
* route's display name rather than dropping it.
|
|
2245
|
+
*/
|
|
2246
|
+
const SHORT_LABELS = {
|
|
2247
|
+
kimi: "Kimi",
|
|
2248
|
+
grok: "Grok",
|
|
2249
|
+
antigravity: "Anti",
|
|
2250
|
+
glm: "GLM",
|
|
2251
|
+
ark: "Ark"
|
|
2252
|
+
};
|
|
2253
|
+
/**
|
|
2254
|
+
* Abbreviations for the window names the providers use.
|
|
2255
|
+
*
|
|
2256
|
+
* The bar has room for a token or two, so `weekly` becomes `7d` — the same figure in a
|
|
2257
|
+
* form that reads at a glance. An unrecognised window keeps its own name, shortened
|
|
2258
|
+
* only if it is long.
|
|
2259
|
+
*/
|
|
2260
|
+
const SHORT_WINDOWS = {
|
|
2261
|
+
"weekly": "7d",
|
|
2262
|
+
"7d": "7d",
|
|
2263
|
+
"5h": "5h",
|
|
2264
|
+
"monthly": "mo",
|
|
2265
|
+
"daily": "1d",
|
|
2266
|
+
"credits": "cr",
|
|
2267
|
+
"free": "free",
|
|
2268
|
+
"session": "s"
|
|
2269
|
+
};
|
|
2270
|
+
/** The session's provider, from its model-selection projection. */
|
|
2271
|
+
function providerOf(projection) {
|
|
2272
|
+
return (projection?.next ?? projection?.lastUsed)?.provider;
|
|
2273
|
+
}
|
|
2274
|
+
/**
|
|
2275
|
+
* Reduce the host's account-balance payload to the wallets the chip shows.
|
|
2276
|
+
*
|
|
2277
|
+
* Bonus wallets are shown after the topped-up ones, so a reader sees their own credit
|
|
2278
|
+
* first and any promotional credit as a secondary figure.
|
|
2279
|
+
*
|
|
2280
|
+
* @param value - the host's ready balance, or undefined when there is none.
|
|
2281
|
+
* @returns the wallets, topped-up first then bonus, or undefined when nothing is usable.
|
|
2282
|
+
*/
|
|
2283
|
+
function balanceOf(value) {
|
|
2284
|
+
if (value === null || value === void 0 || !("value" in value) || value.value === void 0) return void 0;
|
|
2285
|
+
const wallets = [...value.value.map((wallet) => ({
|
|
2286
|
+
currency: wallet.currency,
|
|
2287
|
+
balance: wallet.balance,
|
|
2288
|
+
bonus: false
|
|
2289
|
+
})), ...(value.bonusWallets ?? []).map((wallet) => ({
|
|
2290
|
+
currency: wallet.currency,
|
|
2291
|
+
balance: wallet.balance,
|
|
2292
|
+
bonus: true
|
|
2293
|
+
}))];
|
|
2294
|
+
return wallets.length === 0 ? void 0 : wallets;
|
|
2295
|
+
}
|
|
2296
|
+
/**
|
|
2297
|
+
* Format one wallet for the chip's compact line.
|
|
2298
|
+
*
|
|
2299
|
+
* A balance is money, not a percentage, so it renders as `¥6.89` rather than a meter;
|
|
2300
|
+
* the currency prefix keeps two currencies from being mistaken for one sum.
|
|
2301
|
+
*
|
|
2302
|
+
* @param wallet - the wallet to show.
|
|
2303
|
+
* @returns the short form, e.g. `¥6.89` or `+¥2` for a bonus wallet.
|
|
2304
|
+
*/
|
|
2305
|
+
function formatBalance(wallet) {
|
|
2306
|
+
const amount = Number.parseFloat(wallet.balance);
|
|
2307
|
+
const trimmed = Number.isFinite(amount) ? String(Math.round(amount * 100) / 100) : wallet.balance;
|
|
2308
|
+
const symbol = wallet.currency === "CNY" ? "¥" : wallet.currency === "USD" ? "$" : `${wallet.currency} `;
|
|
2309
|
+
return `${wallet.bonus ? "+" : ""}${symbol}${trimmed}`;
|
|
2310
|
+
}
|
|
2311
|
+
/**
|
|
2312
|
+
* The short form of a route's name.
|
|
2313
|
+
*
|
|
2314
|
+
* @param provider - the route id.
|
|
2315
|
+
* @param label - the route's own display name.
|
|
2316
|
+
* @returns an abbreviation that still identifies the route.
|
|
2317
|
+
*/
|
|
2318
|
+
function shortLabel(provider, label) {
|
|
2319
|
+
const known = SHORT_LABELS[provider];
|
|
2320
|
+
if (known !== void 0) return known;
|
|
2321
|
+
const [first] = label.trim().split(/\s+/);
|
|
2322
|
+
return first === void 0 || first === "" ? provider : first;
|
|
2323
|
+
}
|
|
2324
|
+
/**
|
|
2325
|
+
* The short form of a window's name.
|
|
2326
|
+
*
|
|
2327
|
+
* @param label - the window's own name, e.g. `weekly`.
|
|
2328
|
+
* @returns an abbreviation, or the label itself when it is already short.
|
|
2329
|
+
*/
|
|
2330
|
+
function shortWindow(label) {
|
|
2331
|
+
const known = SHORT_WINDOWS[label.toLowerCase()];
|
|
2332
|
+
if (known !== void 0) return known;
|
|
2333
|
+
return label.length <= 4 ? label : label.slice(0, 4);
|
|
2334
|
+
}
|
|
2335
|
+
/**
|
|
2336
|
+
* Pick the window a single line should show.
|
|
2337
|
+
*
|
|
2338
|
+
* A plan can meter several windows at once (a rolling five-hour one and a weekly one).
|
|
2339
|
+
* The most constrained is the one that will stop work soonest, so it earns the space;
|
|
2340
|
+
* ties fall to the earlier window, which is the order the providers state them in.
|
|
2341
|
+
*
|
|
2342
|
+
* @param windows - the route's metered windows.
|
|
2343
|
+
* @returns the window to show, or undefined when none states a figure.
|
|
2344
|
+
*/
|
|
2345
|
+
function pickWindow(windows) {
|
|
2346
|
+
let best;
|
|
2347
|
+
for (const window$1 of windows) {
|
|
2348
|
+
if (window$1.usedPercent === void 0) continue;
|
|
2349
|
+
if (best?.usedPercent === void 0 || window$1.usedPercent > best.usedPercent) best = window$1;
|
|
2350
|
+
}
|
|
2351
|
+
return best;
|
|
2352
|
+
}
|
|
2353
|
+
/**
|
|
2354
|
+
* Reduce one route's usage to what the chip renders.
|
|
2355
|
+
*
|
|
2356
|
+
* @param provider - the route the figures came from.
|
|
2357
|
+
* @param label - the route's display name.
|
|
2358
|
+
* @param usage - the accounts the route answered for.
|
|
2359
|
+
* @returns the reading, or undefined when the route stated no figure.
|
|
2360
|
+
*/
|
|
2361
|
+
function readingOf(provider, label, usage) {
|
|
2362
|
+
for (const entry of usage) {
|
|
2363
|
+
const window$1 = pickWindow(entry.windows);
|
|
2364
|
+
if (window$1 === void 0) continue;
|
|
2365
|
+
return {
|
|
2366
|
+
provider,
|
|
2367
|
+
label,
|
|
2368
|
+
windows: entry.windows.filter((candidate) => candidate.usedPercent !== void 0),
|
|
2369
|
+
window: window$1,
|
|
2370
|
+
...entry.plan === void 0 ? {} : { plan: entry.plan }
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
/**
|
|
2375
|
+
* Format a window's reset the way the settings cards do.
|
|
2376
|
+
*
|
|
2377
|
+
* The rule is shared with the cards on purpose: a figure reading `6d20h` here and
|
|
2378
|
+
* something else there would be two answers to one question.
|
|
2379
|
+
*
|
|
2380
|
+
* @param at - epoch milliseconds, or undefined when the provider states none.
|
|
2381
|
+
* @param now - the instant to measure from.
|
|
2382
|
+
* @returns a short phrase, or undefined when there is no reset to show.
|
|
2383
|
+
*/
|
|
2384
|
+
function formatReset(at, now) {
|
|
2385
|
+
if (at === void 0) return void 0;
|
|
2386
|
+
const remaining = at - now;
|
|
2387
|
+
if (remaining <= 0) return "now";
|
|
2388
|
+
const hours = remaining / 36e5;
|
|
2389
|
+
if (hours < 1) return `${String(Math.max(1, Math.round(remaining / 6e4)))}m`;
|
|
2390
|
+
if (hours < 24) return `${String(Math.round(hours))}h`;
|
|
2391
|
+
const days = Math.floor(hours / 24);
|
|
2392
|
+
return `${String(days)}d${String(Math.round(hours - days * 24))}h`;
|
|
2393
|
+
}
|
|
2394
|
+
/**
|
|
2395
|
+
* The composer's usage chip.
|
|
2396
|
+
*
|
|
2397
|
+
* @param props - injected RPC caller, copy, and the session's provider reader.
|
|
2398
|
+
* @returns the chip, or null when there is nothing true to say.
|
|
2399
|
+
*/
|
|
2400
|
+
function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBalance, ccSwitchCredentialRef, useProjection }) {
|
|
2401
|
+
const [reading, setReading] = (0, react.useState)(void 0);
|
|
2402
|
+
const [balance, setBalance] = (0, react.useState)(void 0);
|
|
2403
|
+
const [ccBalance, setCcBalance] = (0, react.useState)(void 0);
|
|
2404
|
+
const [now, setNow] = (0, react.useState)(() => Date.now());
|
|
2405
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
2406
|
+
const lastOwn = (0, react.useRef)(void 0);
|
|
2407
|
+
const selected = useProjection("modelSelection", (value) => providerOf(value));
|
|
2408
|
+
const isDeepSeek = selected === "deepseek-account";
|
|
2409
|
+
const ccRef = selected === void 0 || isOwnProvider(selected) || isDeepSeek ? void 0 : ccSwitchCredentialRef(selected);
|
|
2410
|
+
(0, react.useEffect)(() => {
|
|
2411
|
+
if (ccRef === void 0) {
|
|
2412
|
+
setCcBalance(void 0);
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2415
|
+
let live = true;
|
|
2416
|
+
const refresh = () => {
|
|
2417
|
+
ccSwitchBalance(ccRef).then((value) => {
|
|
2418
|
+
if (live) setCcBalance(value === void 0 ? void 0 : {
|
|
2419
|
+
currency: value.currency,
|
|
2420
|
+
total: value.total
|
|
2421
|
+
});
|
|
2422
|
+
}).catch(() => {});
|
|
2423
|
+
};
|
|
2424
|
+
refresh();
|
|
2425
|
+
const timer = setInterval(refresh, REFRESH_INTERVAL_MS);
|
|
2426
|
+
return () => {
|
|
2427
|
+
live = false;
|
|
2428
|
+
clearInterval(timer);
|
|
2429
|
+
};
|
|
2430
|
+
}, [ccRef, ccSwitchBalance]);
|
|
2431
|
+
const load = (0, react.useCallback)(async (target) => {
|
|
2432
|
+
try {
|
|
2433
|
+
const result = await call("usage", { provider: target });
|
|
2434
|
+
return readingOf(target, result.status.providers.find((entry) => entry.provider === target)?.displayName ?? target, result.usage);
|
|
2435
|
+
} catch {
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
}, [call]);
|
|
2439
|
+
(0, react.useEffect)(() => {
|
|
2440
|
+
if (!isDeepSeek) {
|
|
2441
|
+
setBalance(void 0);
|
|
2442
|
+
return;
|
|
2443
|
+
}
|
|
2444
|
+
let live = true;
|
|
2445
|
+
const refresh = () => {
|
|
2446
|
+
accountBalance().then((value) => {
|
|
2447
|
+
if (live) setBalance(balanceOf(value));
|
|
2448
|
+
}).catch(() => {});
|
|
2449
|
+
};
|
|
2450
|
+
refresh();
|
|
2451
|
+
const timer = setInterval(refresh, REFRESH_INTERVAL_MS);
|
|
2452
|
+
return () => {
|
|
2453
|
+
live = false;
|
|
2454
|
+
clearInterval(timer);
|
|
2455
|
+
};
|
|
2456
|
+
}, [isDeepSeek, accountBalance]);
|
|
2457
|
+
(0, react.useEffect)(() => {
|
|
2458
|
+
const target = selected !== void 0 && isOwnProvider(selected) ? selected : lastOwn.current;
|
|
2459
|
+
if (target === void 0) {
|
|
2460
|
+
setReading(void 0);
|
|
2461
|
+
return;
|
|
2462
|
+
}
|
|
2463
|
+
let live = true;
|
|
2464
|
+
const refresh = () => {
|
|
2465
|
+
setNow(Date.now());
|
|
2466
|
+
load(target).then((next) => {
|
|
2467
|
+
if (!live || next === void 0) return;
|
|
2468
|
+
lastOwn.current = target;
|
|
2469
|
+
setReading(next);
|
|
2470
|
+
});
|
|
2471
|
+
};
|
|
2472
|
+
refresh();
|
|
2473
|
+
const timer = setInterval(refresh, REFRESH_INTERVAL_MS);
|
|
2474
|
+
return () => {
|
|
2475
|
+
live = false;
|
|
2476
|
+
clearInterval(timer);
|
|
2477
|
+
};
|
|
2478
|
+
}, [
|
|
2479
|
+
selected,
|
|
2480
|
+
isOwnProvider,
|
|
2481
|
+
load
|
|
2482
|
+
]);
|
|
2483
|
+
(0, react.useEffect)(() => {
|
|
2484
|
+
if (!open) return;
|
|
2485
|
+
const close = () => {
|
|
2486
|
+
setOpen(false);
|
|
2487
|
+
};
|
|
2488
|
+
document.addEventListener("click", close);
|
|
2489
|
+
return () => {
|
|
2490
|
+
document.removeEventListener("click", close);
|
|
2491
|
+
};
|
|
2492
|
+
}, [open]);
|
|
2493
|
+
if (ccRef !== void 0) {
|
|
2494
|
+
if (ccBalance === void 0) return null;
|
|
2495
|
+
const short$1 = shortLabel(selected ?? "", selected ?? "");
|
|
2496
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2497
|
+
className: "dsh-subscription-usage-chip",
|
|
2498
|
+
children: [
|
|
2499
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: usageChipCss }),
|
|
2500
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2501
|
+
type: "button",
|
|
2502
|
+
className: "dsh-subscription-usage-chip-trigger",
|
|
2503
|
+
"aria-expanded": open,
|
|
2504
|
+
title: t("usageChipTitle", { provider: selected ?? "" }),
|
|
2505
|
+
onClick: (event) => {
|
|
2506
|
+
event.stopPropagation();
|
|
2507
|
+
setOpen((was) => !was);
|
|
2508
|
+
},
|
|
2509
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2510
|
+
className: "dsh-subscription-usage-chip-label",
|
|
2511
|
+
children: short$1
|
|
2512
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2513
|
+
className: "dsh-subscription-usage-chip-cell",
|
|
2514
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2515
|
+
className: "dsh-subscription-usage-chip-value",
|
|
2516
|
+
children: `${ccBalance.currency === "CNY" ? "¥" : ccBalance.currency === "USD" ? "$" : `${ccBalance.currency} `}${ccBalance.total}`
|
|
2517
|
+
})
|
|
2518
|
+
})]
|
|
2519
|
+
}),
|
|
2520
|
+
open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2521
|
+
className: "dsh-subscription-usage-chip-panel",
|
|
2522
|
+
role: "status",
|
|
2523
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2524
|
+
className: "dsh-subscription-usage-chip-full",
|
|
2525
|
+
children: t("usageChipFull", { provider: selected ?? "" })
|
|
2526
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2527
|
+
className: "dsh-subscription-usage-chip-meter",
|
|
2528
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2529
|
+
className: "dsh-subscription-usage-chip-meterHead",
|
|
2530
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: ccBalance.currency }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2531
|
+
className: "dsh-subscription-usage-chip-detail",
|
|
2532
|
+
children: ccBalance.total
|
|
2533
|
+
})]
|
|
2534
|
+
})
|
|
2535
|
+
})]
|
|
2536
|
+
})
|
|
2537
|
+
]
|
|
2538
|
+
});
|
|
2539
|
+
}
|
|
2540
|
+
if (isDeepSeek) {
|
|
2541
|
+
if (balance === void 0) return null;
|
|
2542
|
+
const short$1 = shortLabel("deepseek-account", "DeepSeek Account");
|
|
2543
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2544
|
+
className: "dsh-subscription-usage-chip",
|
|
2545
|
+
children: [
|
|
2546
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: usageChipCss }),
|
|
2547
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2548
|
+
type: "button",
|
|
2549
|
+
className: "dsh-subscription-usage-chip-trigger",
|
|
2550
|
+
"aria-expanded": open,
|
|
2551
|
+
title: t("usageChipTitle", { provider: "DeepSeek Account" }),
|
|
2552
|
+
onClick: (event) => {
|
|
2553
|
+
event.stopPropagation();
|
|
2554
|
+
setOpen((was) => !was);
|
|
2555
|
+
},
|
|
2556
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2557
|
+
className: "dsh-subscription-usage-chip-label",
|
|
2558
|
+
children: short$1
|
|
2559
|
+
}), balance.map((wallet) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2560
|
+
className: "dsh-subscription-usage-chip-cell",
|
|
2561
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2562
|
+
className: "dsh-subscription-usage-chip-value",
|
|
2563
|
+
children: formatBalance(wallet)
|
|
2564
|
+
})
|
|
2565
|
+
}, `${wallet.currency}${wallet.bonus ? ":bonus" : ""}`))]
|
|
2566
|
+
}),
|
|
2567
|
+
open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2568
|
+
className: "dsh-subscription-usage-chip-panel",
|
|
2569
|
+
role: "status",
|
|
2570
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2571
|
+
className: "dsh-subscription-usage-chip-full",
|
|
2572
|
+
children: t("usageChipFull", { provider: "DeepSeek Account" })
|
|
2573
|
+
}), balance.map((wallet) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2574
|
+
className: "dsh-subscription-usage-chip-meter",
|
|
2575
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2576
|
+
className: "dsh-subscription-usage-chip-meterHead",
|
|
2577
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: `${wallet.bonus ? "Bonus · " : ""}${wallet.currency}` }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2578
|
+
className: "dsh-subscription-usage-chip-detail",
|
|
2579
|
+
children: formatBalance(wallet)
|
|
2580
|
+
})]
|
|
2581
|
+
})
|
|
2582
|
+
}, `${wallet.currency}${wallet.bonus ? ":bonus" : ""}`))]
|
|
2583
|
+
})
|
|
2584
|
+
]
|
|
2585
|
+
});
|
|
2586
|
+
}
|
|
2587
|
+
if (reading === void 0) return null;
|
|
2588
|
+
if (reading.window.usedPercent === void 0) return null;
|
|
2589
|
+
formatReset(reading.window.resetsAt, now);
|
|
2590
|
+
const short = shortLabel(reading.provider, reading.label);
|
|
2591
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2592
|
+
className: "dsh-subscription-usage-chip",
|
|
2593
|
+
children: [
|
|
2594
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: usageChipCss }),
|
|
2595
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2596
|
+
type: "button",
|
|
2597
|
+
className: "dsh-subscription-usage-chip-trigger",
|
|
2598
|
+
"aria-expanded": open,
|
|
2599
|
+
title: t("usageChipTitle", { provider: reading.label }),
|
|
2600
|
+
onClick: (event) => {
|
|
2601
|
+
event.stopPropagation();
|
|
2602
|
+
setOpen((was) => !was);
|
|
2603
|
+
},
|
|
2604
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2605
|
+
className: "dsh-subscription-usage-chip-label",
|
|
2606
|
+
children: short
|
|
2607
|
+
}), reading.windows.map((window$1) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2608
|
+
className: "dsh-subscription-usage-chip-cell",
|
|
2609
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2610
|
+
className: "dsh-subscription-usage-chip-window",
|
|
2611
|
+
children: shortWindow(window$1.label)
|
|
2612
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2613
|
+
className: "dsh-subscription-usage-chip-value",
|
|
2614
|
+
children: `${String(Math.round(window$1.usedPercent ?? 0))}%`
|
|
2615
|
+
})]
|
|
2616
|
+
}, window$1.label))]
|
|
2617
|
+
}),
|
|
2618
|
+
open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2619
|
+
className: "dsh-subscription-usage-chip-panel",
|
|
2620
|
+
role: "status",
|
|
2621
|
+
children: [
|
|
2622
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2623
|
+
className: "dsh-subscription-usage-chip-full",
|
|
2624
|
+
children: t("usageChipFull", { provider: reading.label })
|
|
2625
|
+
}),
|
|
2626
|
+
reading.windows.map((window$1) => {
|
|
2627
|
+
const windowReset = formatReset(window$1.resetsAt, now);
|
|
2628
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2629
|
+
className: "dsh-subscription-usage-chip-meter",
|
|
2630
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2631
|
+
className: "dsh-subscription-usage-chip-meterHead",
|
|
2632
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: window$1.label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2633
|
+
className: "dsh-subscription-usage-chip-detail",
|
|
2634
|
+
children: `${String(Math.round(window$1.usedPercent ?? 0))}%${windowReset === void 0 ? "" : ` · ${windowReset}`}`
|
|
2635
|
+
})]
|
|
2636
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2637
|
+
className: "dsh-subscription-usage-chip-track",
|
|
2638
|
+
"aria-hidden": "true",
|
|
2639
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2640
|
+
className: "dsh-subscription-usage-chip-fill",
|
|
2641
|
+
style: { width: `${String(Math.max(0, Math.min(100, window$1.usedPercent ?? 0)))}%` }
|
|
2642
|
+
})
|
|
2643
|
+
})]
|
|
2644
|
+
}, window$1.label);
|
|
2645
|
+
}),
|
|
2646
|
+
reading.plan === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2647
|
+
className: "dsh-subscription-usage-chip-detail",
|
|
2648
|
+
children: reading.plan
|
|
2649
|
+
})
|
|
2650
|
+
]
|
|
2651
|
+
})
|
|
2652
|
+
]
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2053
2656
|
//#endregion
|
|
2054
2657
|
//#region src/client/index.ts
|
|
2658
|
+
/**
|
|
2659
|
+
* Routes this plugin serves.
|
|
2660
|
+
*
|
|
2661
|
+
* The chip only reports for these; a provider somebody else registered gets no chip,
|
|
2662
|
+
* because this plugin has no way to read its allowance.
|
|
2663
|
+
*/
|
|
2664
|
+
const OWN_PROVIDER_IDS = new Set([
|
|
2665
|
+
"kimi",
|
|
2666
|
+
"grok",
|
|
2667
|
+
"antigravity",
|
|
2668
|
+
"glm",
|
|
2669
|
+
"ark"
|
|
2670
|
+
]);
|
|
2055
2671
|
/** Dictionary namespace owned by this plugin. */
|
|
2056
2672
|
const NS = "settings.subscriptions";
|
|
2057
2673
|
/** Settings nav position: after the shipped sections, before plugin tooling. */
|
|
2058
2674
|
const SECTION_ORDER = 92;
|
|
2059
2675
|
/**
|
|
2676
|
+
* Composer-dock position: after the shell's own stats pills, which sit at 0.
|
|
2677
|
+
*
|
|
2678
|
+
* The shell owns that bar and registers `tok/s` and the cache-hit pill into it, so the
|
|
2679
|
+
* chip follows them rather than displacing them.
|
|
2680
|
+
*/
|
|
2681
|
+
const CHIP_ORDER = 10;
|
|
2682
|
+
/**
|
|
2683
|
+
* The credential reference a manually added provider's balance query uses, by provider
|
|
2684
|
+
* id.
|
|
2685
|
+
*
|
|
2686
|
+
* A manually added provider is not one this plugin serves, so the only way to read its
|
|
2687
|
+
* balance is cc-switch's query, which needs the provider's API key. The provider's
|
|
2688
|
+
* profile names that key by reference (`apiKeyEnv`), and the host's credential service
|
|
2689
|
+
* resolves the name; this map is the one place a provider id is tied to that name. A
|
|
2690
|
+
* provider with no entry gets no balance.
|
|
2691
|
+
*/
|
|
2692
|
+
const CC_SWITCH_CREDENTIAL_REFS = { huanling: "HUANLING_API_KEY" };
|
|
2693
|
+
/**
|
|
2060
2694
|
* Required services: `slots` carries the registration seat, `connection` the
|
|
2061
2695
|
* RPC caller, and `locale` the copy dictionaries.
|
|
2062
2696
|
*/
|
|
@@ -2091,6 +2725,19 @@ function apply(ctx) {
|
|
|
2091
2725
|
t
|
|
2092
2726
|
})
|
|
2093
2727
|
}, SubscriptionsSection));
|
|
2728
|
+
ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
|
|
2729
|
+
name: "conversation.composer.dock",
|
|
2730
|
+
id: "subscription-usage",
|
|
2731
|
+
order: CHIP_ORDER,
|
|
2732
|
+
inject: () => ({
|
|
2733
|
+
call,
|
|
2734
|
+
t,
|
|
2735
|
+
isOwnProvider: (provider) => OWN_PROVIDER_IDS.has(provider),
|
|
2736
|
+
accountBalance: createAccountBalanceCall(connection.rpc),
|
|
2737
|
+
ccSwitchBalance: createCcSwitchBalanceCall(connection.rpc),
|
|
2738
|
+
ccSwitchCredentialRef: (provider) => CC_SWITCH_CREDENTIAL_REFS[provider]
|
|
2739
|
+
})
|
|
2740
|
+
}, UsageChip));
|
|
2094
2741
|
}
|
|
2095
2742
|
|
|
2096
2743
|
//#endregion
|