dsh-plugin-subscription-oauth 0.4.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/client.js CHANGED
@@ -632,6 +632,8 @@ const RPC_PREFIX = "subscription-oauth.";
632
632
  //#region src/client/subscriptions-rpc.ts
633
633
  /** The shared channel the node half's routes are mounted on. */
634
634
  const CHANNEL = "/api";
635
+ /** The host's own DeepSeek account controller, from which the chip reads a balance. */
636
+ const ACCOUNT_CHANNEL = "/api";
635
637
  /** A failed call: either the transport rejected or the endpoint answered `ok: false`. */
636
638
  var SubscriptionsRpcError = class extends Error {
637
639
  constructor(message, code) {
@@ -661,6 +663,55 @@ function createSubscriptionsCall(rpc) {
661
663
  return result.value;
662
664
  };
663
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
+ }
664
715
 
665
716
  //#endregion
666
717
  //#region src/client/SubscriptionsSection.tsx
@@ -2221,6 +2272,43 @@ function providerOf(projection) {
2221
2272
  return (projection?.next ?? projection?.lastUsed)?.provider;
2222
2273
  }
2223
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
+ /**
2224
2312
  * The short form of a route's name.
2225
2313
  *
2226
2314
  * @param provider - the route id.
@@ -2309,12 +2397,37 @@ function formatReset(at, now) {
2309
2397
  * @param props - injected RPC caller, copy, and the session's provider reader.
2310
2398
  * @returns the chip, or null when there is nothing true to say.
2311
2399
  */
2312
- function UsageChip({ call, t, isOwnProvider, useProjection }) {
2400
+ function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBalance, ccSwitchCredentialRef, useProjection }) {
2313
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);
2314
2404
  const [now, setNow] = (0, react.useState)(() => Date.now());
2315
2405
  const [open, setOpen] = (0, react.useState)(false);
2316
2406
  const lastOwn = (0, react.useRef)(void 0);
2317
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]);
2318
2431
  const load = (0, react.useCallback)(async (target) => {
2319
2432
  try {
2320
2433
  const result = await call("usage", { provider: target });
@@ -2323,6 +2436,24 @@ function UsageChip({ call, t, isOwnProvider, useProjection }) {
2323
2436
  return;
2324
2437
  }
2325
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]);
2326
2457
  (0, react.useEffect)(() => {
2327
2458
  const target = selected !== void 0 && isOwnProvider(selected) ? selected : lastOwn.current;
2328
2459
  if (target === void 0) {
@@ -2359,6 +2490,100 @@ function UsageChip({ call, t, isOwnProvider, useProjection }) {
2359
2490
  document.removeEventListener("click", close);
2360
2491
  };
2361
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
+ }
2362
2587
  if (reading === void 0) return null;
2363
2588
  if (reading.window.usedPercent === void 0) return null;
2364
2589
  formatReset(reading.window.resetsAt, now);
@@ -2455,6 +2680,17 @@ const SECTION_ORDER = 92;
2455
2680
  */
2456
2681
  const CHIP_ORDER = 10;
2457
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
+ /**
2458
2694
  * Required services: `slots` carries the registration seat, `connection` the
2459
2695
  * RPC caller, and `locale` the copy dictionaries.
2460
2696
  */
@@ -2496,7 +2732,10 @@ function apply(ctx) {
2496
2732
  inject: () => ({
2497
2733
  call,
2498
2734
  t,
2499
- isOwnProvider: (provider) => OWN_PROVIDER_IDS.has(provider)
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]
2500
2739
  })
2501
2740
  }, UsageChip));
2502
2741
  }