@kenz1117/dsh-ui-usage-billing 0.9.19 → 1.0.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/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
- import { readFile, stat } from "node:fs/promises";
2
+ import { mkdir, readFile, stat } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
- import { join } from "node:path";
4
+ import { dirname, join } from "node:path";
5
5
  import { defineTool } from "@deepseek-ai/dsh-tools";
6
6
  import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
7
7
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
@@ -184,7 +184,7 @@ let liveExtraModels;
184
184
  * @param pricing - the `/api/billing/pricing` response.
185
185
  */
186
186
  function applyLivePricing(pricing) {
187
- liveRate = pricing.rate;
187
+ liveRate = typeof pricing.rate === "number" && Number.isFinite(pricing.rate) && pricing.rate > 0 ? pricing.rate : void 0;
188
188
  livePrices = pricing.prices;
189
189
  liveExtraModels = pricing.extraModels;
190
190
  }
@@ -492,6 +492,30 @@ const MODEL_CATALOG = [
492
492
  output: 8.4
493
493
  }
494
494
  },
495
+ {
496
+ key: "minimax-m2.7",
497
+ name: "MiniMax-M2.7",
498
+ provider: "MiniMax",
499
+ colorVar: "dsw-static-amber-400",
500
+ price: {
501
+ currency: "CNY",
502
+ input: 2.1,
503
+ cacheHit: .42,
504
+ output: 8.4
505
+ }
506
+ },
507
+ {
508
+ key: "minimax-m2.7-highspeed",
509
+ name: "MiniMax-M2.7-highspeed",
510
+ provider: "MiniMax",
511
+ colorVar: "dsw-static-amber-500",
512
+ price: {
513
+ currency: "CNY",
514
+ input: 4.2,
515
+ cacheHit: .42,
516
+ output: 16.8
517
+ }
518
+ },
495
519
  {
496
520
  key: "ernie",
497
521
  name: "ERNIE-5.1",
@@ -742,7 +766,16 @@ const MODEL_KEY_ALIASES = {
742
766
  "step-3.7-flash": "step",
743
767
  "seed-2.0-mini": "doubao-mini",
744
768
  "k3": "kimi-k3",
745
- "kimi-k3": "kimi-k3"
769
+ "kimi-k3": "kimi-k3",
770
+ "minimax-m1": "minimax",
771
+ "minimax-m2": "minimax",
772
+ "minimax-m3": "minimax",
773
+ "minimax-m2.7": "minimax-m2.7",
774
+ "minimax-m2.7-highspeed": "minimax-m2.7-highspeed",
775
+ "minimax-m2.7-high-speed": "minimax-m2.7-highspeed",
776
+ "minimax-m2-7": "minimax-m2.7",
777
+ "minimax-m2-7-highspeed": "minimax-m2.7-highspeed",
778
+ "minimax-m2-7-high-speed": "minimax-m2.7-highspeed"
746
779
  };
747
780
  /**
748
781
  * 模型 id 归一化:小写、去括号附注(如 `gpt5.6 luna(go)` 只看主体)、再去所有
@@ -892,7 +925,7 @@ function computeCost(entry, buckets, peakShare = DEFAULT_PEAK_SHARE) {
892
925
  * @param buckets - token usage counts.
893
926
  * @param timeMs - the call's wall-clock time (epoch ms); null falls back to the peak-share mix.
894
927
  * @param peakShare - fallback mix used only when `timeMs` is missing.
895
- * @returns the estimated cost in the entry's native currency.
928
+ * @returns the estimated cost in CNY(USD 计价模型已按当前汇率折算)。
896
929
  */
897
930
  function computeCostAt(entry, buckets, timeMs, peakShare = DEFAULT_PEAK_SHARE) {
898
931
  if (entry.price.offPeak === void 0) return priceBandCost(entry.price, buckets, entry.price.currency);
@@ -1457,11 +1490,12 @@ function createUsageAggregator(persistence, options = {}) {
1457
1490
  console.warn("[usage-billing] failed to load durable usage ledger; rebuilding from current sessions:", error);
1458
1491
  }
1459
1492
  };
1460
- /** 失效键:日志文件的 mtime+size;拿不到(后端无 locate / 文件丢失)时每次重折。 */
1493
+ /** 失效键:日志文件的 mtime+size;拿不到(后端无 locate / 文件丢失 / locate 抛错)时返回 null,
1494
+ * 让调用方每次重折。locate 调用也纳入 try,避免单个会话的 locate 异常把整份聚合拖垮。 */
1461
1495
  const stampOf = async (meta) => {
1462
- const location = persistence.locate?.(meta);
1463
- if (location === void 0) return null;
1464
1496
  try {
1497
+ const location = persistence.locate?.(meta);
1498
+ if (location === void 0) return null;
1465
1499
  const info = await stat(location.path);
1466
1500
  return `${String(info.mtimeMs)}:${String(info.size)}`;
1467
1501
  } catch {
@@ -2081,9 +2115,20 @@ const QUERIERS = [
2081
2115
  * @returns the balance rows (one per provider).
2082
2116
  */
2083
2117
  async function queryBalances(ctx, providers) {
2084
- return await Promise.all(QUERIERS.map(({ route, querier, displayName }) => {
2118
+ const byName = /* @__PURE__ */ new Map();
2119
+ for (const { route, querier, displayName } of QUERIERS) {
2085
2120
  const env = providers[route]?.apiKeyEnv;
2086
- if (typeof env !== "string" || env === "") return Promise.resolve({
2121
+ const configured = typeof env === "string" && env !== "";
2122
+ const existing = byName.get(displayName);
2123
+ if (existing !== void 0 && existing.env !== void 0) continue;
2124
+ byName.set(displayName, {
2125
+ displayName,
2126
+ querier,
2127
+ env: configured ? env : void 0
2128
+ });
2129
+ }
2130
+ return await Promise.all([...byName.values()].map(({ querier, displayName, env }) => {
2131
+ if (env === void 0) return Promise.resolve({
2087
2132
  provider: displayName,
2088
2133
  displayName,
2089
2134
  error: "unconfigured"
@@ -3213,15 +3258,16 @@ async function collectZai(keys, config, timeoutMs) {
3213
3258
  function goWindow(value, kind) {
3214
3259
  if (value === null || typeof value !== "object") return null;
3215
3260
  const record = value;
3216
- const percentSource = record.usagePercent ?? record.usedPercent ?? record.percentUsed ?? record.percentage ?? record.percent;
3217
- let usedPercent = clampPercent$1(numberOrNull$1(percentSource));
3261
+ const ratioSource = record.usagePercent ?? record.usedPercent ?? record.percentUsed ?? record.percentage;
3262
+ const fromRatioField = ratioSource !== void 0 && ratioSource !== null;
3263
+ let usedPercent = clampPercent$1(numberOrNull$1(fromRatioField ? ratioSource : record.percent));
3218
3264
  if (usedPercent === null) {
3219
3265
  const used = numberOrNull$1(record.used ?? record.consumed);
3220
3266
  const limit = numberOrNull$1(record.limit ?? record.total ?? record.quota);
3221
3267
  if (used !== null && limit !== null && limit > 0) usedPercent = clampPercent$1(used / limit * 100);
3222
3268
  }
3223
3269
  if (usedPercent === null) return null;
3224
- if (usedPercent <= 1 && usedPercent >= 0 && record.percent === void 0 && percentSource !== void 0) usedPercent *= 100;
3270
+ if (fromRatioField && usedPercent <= 1 && usedPercent >= 0) usedPercent *= 100;
3225
3271
  const resetSeconds = numberOrNull$1(record.resetInSec ?? record.resetInSeconds ?? record.resetSeconds);
3226
3272
  const resetsAt = resetSeconds === null ? toIso(record.resetAt ?? record.resetsAt ?? record.nextReset) : new Date(Date.now() + Math.max(0, resetSeconds) * 1e3).toISOString();
3227
3273
  return {
@@ -3329,6 +3375,18 @@ function resolveMiniMaxBaseUrl(config) {
3329
3375
  function minmaxDisplayName(provider) {
3330
3376
  return SUBSCRIPTION_DISPLAY_NAMES[provider] ?? "MiniMax Coding Plan";
3331
3377
  }
3378
+ /** MiniMax Token Plan / Coding Plan 的 key 判别:普通按量 API key 以 `sk-` 开头,
3379
+ * Token Plan(订阅套餐)key 以 `sk-cp-` 开头。据此区分一个 key 是订阅还是按量,
3380
+ * 供查询失败时给出「key 类型 / region」定向提示,避免把按量 key 误当 token-plan
3381
+ * 查询后只剩一句笼统的「响应异常」。 */
3382
+ function isMiniMaxTokenPlanKey(apiKey) {
3383
+ return apiKey.startsWith("sk-cp-");
3384
+ }
3385
+ /** 构造 MiniMax Token Plan 查询端点:避免 base 已含 `/v1` 时拼出 `/v1/v1/...`。 */
3386
+ function miniMaxEndpoint(base) {
3387
+ const trimmed = base.replace(/\/+$/, "");
3388
+ return trimmed.endsWith("/v1") ? `${trimmed}/token_plan/remains` : `${trimmed}/v1/token_plan/remains`;
3389
+ }
3332
3390
  /** Collect the MiniMax Token Plan quota (CN + INTL). */
3333
3391
  async function collectMiniMax(keys, config, timeoutMs) {
3334
3392
  const apiKey = keys.minmaxApiKey.trim();
@@ -3340,16 +3398,25 @@ async function collectMiniMax(keys, config, timeoutMs) {
3340
3398
  status: "not-configured",
3341
3399
  windows: []
3342
3400
  };
3401
+ const tokenPlanKey = isMiniMaxTokenPlanKey(apiKey);
3343
3402
  try {
3344
- const windows = parseMiniMaxRemains(await requestJson(`${base}/v1/token_plan/remains`, { headers: {
3403
+ const windows = parseMiniMaxRemains(await requestJson(miniMaxEndpoint(base), { headers: {
3345
3404
  authorization: `Bearer ${apiKey}`,
3346
3405
  accept: "application/json"
3347
3406
  } }, timeoutMs));
3407
+ if (windows.length > 0) return {
3408
+ provider: config.provider,
3409
+ displayName,
3410
+ status: "ok",
3411
+ windows,
3412
+ ...tokenPlanKey ? {} : { hint: "此 key 未以 sk-cp- 开头(可能为按量 API);但 Token Plan 接口返回了额度,故按 Token Plan 展示" }
3413
+ };
3348
3414
  return {
3349
3415
  provider: config.provider,
3350
3416
  displayName,
3351
- status: windows.length > 0 ? "ok" : "invalid-response",
3352
- windows
3417
+ status: "invalid-response",
3418
+ windows,
3419
+ hint: tokenPlanKey ? "Token Plan key 未返回额度:可能是 region 不匹配,请在 provider 的 baseURL 指向国内 api.minimaxi.com 或国际 api.minimax.io" : "此 key 未以 sk-cp- 开头,可能为按量 API 而非 Token Plan;如需订阅额度请用 sk-cp- 开头的 Token Plan key"
3353
3420
  };
3354
3421
  } catch (error) {
3355
3422
  return {
@@ -3628,6 +3695,19 @@ function endpointOf(baseURL, path) {
3628
3695
  return new URL(path, baseURL).toString();
3629
3696
  }
3630
3697
  /**
3698
+ * 用已知程序类型查询该路由的额度(用当前 key)。额度按 key 独立,因此即便
3699
+ * 指纹缓存已识别出站点类型,也仍要实际发一次请求读出本 key 的余额/窗口。
3700
+ * @param baseURL - 站点端点。
3701
+ * @param apiKey - 已解析的本路由 key。
3702
+ * @param kind - 已识别出的程序类型。
3703
+ * @returns 解析出的余额/窗口;取不到时返回 null(漂移或端点不可用)。
3704
+ */
3705
+ async function readRelayByKind(baseURL, apiKey, kind) {
3706
+ const res = await fetchRelayJson(endpointOf(baseURL, kind === "sub2api" ? "/v1/usage" : "/api/status"), apiKey);
3707
+ if (!res.ok) return null;
3708
+ return kind === "sub2api" ? parseSub2ApiUsage(res.data) : parseNewApiStatus(res.data);
3709
+ }
3710
+ /**
3631
3711
  * 查询单个中转站路由的额度。先试 Sub2API,再试 New API;任一读出额度即返回。
3632
3712
  * @param ctx - host context carrying the credentials seam.
3633
3713
  * @param route - 待探测的路由(baseURL + apiKeyEnv)。
@@ -3645,19 +3725,43 @@ async function queryRelayQuota(ctx, route) {
3645
3725
  kind: "unknown",
3646
3726
  status: "unavailable"
3647
3727
  };
3648
- const hit = await ctx.credentials.resolve(credentialRef(route.apiKeyEnv));
3728
+ let hit;
3729
+ try {
3730
+ hit = await ctx.credentials.resolve(credentialRef(route.apiKeyEnv));
3731
+ } catch {
3732
+ return {
3733
+ ...base,
3734
+ kind: "unknown",
3735
+ status: "not-configured"
3736
+ };
3737
+ }
3649
3738
  if (hit === void 0 || hit.value === "") return {
3650
3739
  ...base,
3651
3740
  kind: "unknown",
3652
3741
  status: "not-configured"
3653
3742
  };
3654
3743
  const cached = fingerprintCache.get(base.origin);
3655
- if (cached !== void 0 && Date.now() - cached.at < FINGERPRINT_TTL_MS) return {
3656
- ...base,
3657
- kind: cached.kind,
3658
- status: "ok"
3659
- };
3744
+ const knownKind = cached !== void 0 && Date.now() - cached.at < FINGERPRINT_TTL_MS ? cached.kind : void 0;
3660
3745
  try {
3746
+ if (knownKind === "sub2api" || knownKind === "new-api") {
3747
+ const parsed = await readRelayByKind(route.baseURL, hit.value, knownKind);
3748
+ if (parsed !== null) {
3749
+ relayGate.success(gateKey);
3750
+ return {
3751
+ ...base,
3752
+ kind: knownKind,
3753
+ status: "ok",
3754
+ ...parsed.balance !== void 0 ? { balance: parsed.balance } : {},
3755
+ ...parsed.windows !== void 0 ? { windows: parsed.windows } : {}
3756
+ };
3757
+ }
3758
+ relayGate.fail(gateKey);
3759
+ return {
3760
+ ...base,
3761
+ kind: knownKind,
3762
+ status: "invalid-response"
3763
+ };
3764
+ }
3661
3765
  const sub2 = await fetchRelayJson(endpointOf(route.baseURL, "/v1/usage"), hit.value);
3662
3766
  if (sub2.ok) {
3663
3767
  const parsed = parseSub2ApiUsage(sub2.data);
@@ -3682,23 +3786,7 @@ async function queryRelayQuota(ctx, route) {
3682
3786
  status: "invalid-response"
3683
3787
  };
3684
3788
  }
3685
- if (sub2.status === 401 || sub2.status === 403) {
3686
- relayGate.fail(gateKey);
3687
- return {
3688
- ...base,
3689
- kind: "unknown",
3690
- status: "unauthorized"
3691
- };
3692
- }
3693
3789
  const na = await fetchRelayJson(endpointOf(route.baseURL, "/api/status"), hit.value);
3694
- if (na.status === 401 || na.status === 403) {
3695
- relayGate.fail(gateKey);
3696
- return {
3697
- ...base,
3698
- kind: "unknown",
3699
- status: "unauthorized"
3700
- };
3701
- }
3702
3790
  if (na.ok) {
3703
3791
  const parsed = parseNewApiStatus(na.data);
3704
3792
  if (parsed !== null) {
@@ -3728,11 +3816,12 @@ async function queryRelayQuota(ctx, route) {
3728
3816
  status: "unavailable"
3729
3817
  };
3730
3818
  } catch (error) {
3731
- relayGate.fail(gateKey);
3819
+ const status = statusOf(error);
3820
+ if (status !== "unauthorized") relayGate.fail(gateKey);
3732
3821
  return {
3733
3822
  ...base,
3734
3823
  kind: "unknown",
3735
- status: statusOf(error)
3824
+ status
3736
3825
  };
3737
3826
  }
3738
3827
  }
@@ -3779,12 +3868,26 @@ function isLoopbackPeer(req) {
3779
3868
  if (address === void 0) return false;
3780
3869
  return address === "::1" || address.startsWith("127.") || address.startsWith("::ffff:127.");
3781
3870
  }
3782
- /** 校验 Host 头是本机回环(127.x / ::1 / localhost 或空,供 curl 不带 Host 的极简请求)。 */
3871
+ /** 校验 Host 头是本机回环(精确 127.0.0.0/8 / ::1 / localhost 或空,供 curl 不带 Host 的极简请求)。
3872
+ * 拒绝 `127.0.0.1.attacker.com` 这类以 `127.` 开头但解析到外部的 DNS rebinding 域名:
3873
+ * 只用 `startsWith('127.')` 会被它穿透,必须精确匹配回环 IP 的字面量。 */
3783
3874
  function isLoopbackHost(req) {
3784
3875
  const host = req.headers.host;
3785
3876
  if (host === void 0 || host === "") return true;
3786
3877
  const name = host.split(":")[0];
3787
- return name === "localhost" || name === "::1" || name !== void 0 && name.startsWith("127.");
3878
+ return name === "localhost" || name === "::1" || name !== void 0 && /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(name);
3879
+ }
3880
+ /** 校验 Origin 头是否回环(写操作用,防止跨站表单/fetch 改写设置)。Origin 缺失
3881
+ * (curl 或同源 fetch 不带)视为放行,交给下方的 Content-Type 校验兜底;Origin 存在
3882
+ * 但主机非回环则拒绝——跨站脚本发起的写请求必带攻击者域名的 Origin。 */
3883
+ function isLoopbackOrigin(origin) {
3884
+ if (origin === void 0 || origin === "") return true;
3885
+ try {
3886
+ const host = new URL(origin).hostname;
3887
+ return host === "localhost" || host === "::1" || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
3888
+ } catch {
3889
+ return false;
3890
+ }
3788
3891
  }
3789
3892
  /**
3790
3893
  * 回环防护守卫:仅接受回环 GET 请求(peer socket 地址 + Host 头同时校验)。
@@ -3849,6 +3952,10 @@ function createFileUsageLedgerStore(ledgerPath) {
3849
3952
  } catch {}
3850
3953
  },
3851
3954
  async save(document) {
3955
+ await mkdir(dirname(ledgerPath), {
3956
+ recursive: true,
3957
+ mode: 448
3958
+ });
3852
3959
  await withFileLock(ledgerPath, async () => {
3853
3960
  try {
3854
3961
  const existing = await readFile(ledgerPath, "utf8");
@@ -4296,7 +4403,11 @@ function apply(ctx, config = {}) {
4296
4403
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
4297
4404
  const piProviders = await readPiAiProviders(ctx.settings);
4298
4405
  const providers = { ...piProviders };
4299
- if (providers["deepseek"] === void 0) providers["deepseek"] = { apiKeyEnv: config.balanceApiKeyEnv ?? DEFAULT_BALANCE_API_KEY_ENV };
4406
+ const deepseekKey = providers["deepseek"]?.apiKeyEnv;
4407
+ if (deepseekKey === void 0 || deepseekKey === "") providers["deepseek"] = {
4408
+ ...providers["deepseek"],
4409
+ apiKeyEnv: config.balanceApiKeyEnv ?? DEFAULT_BALANCE_API_KEY_ENV
4410
+ };
4300
4411
  const balances = await queryBalances(ctx, providers);
4301
4412
  const custom = await queryCustomBalances(ctx, config.customBalances ?? []);
4302
4413
  const declared = await queryDeclaredEndpoints(ctx, piProviders, config.declaredEndpoints ?? []);
@@ -4348,9 +4459,25 @@ function apply(ctx, config = {}) {
4348
4459
  res.end(JSON.stringify({ error: "method not allowed" }));
4349
4460
  return;
4350
4461
  }
4462
+ if (!isLoopbackOrigin(req.headers.origin)) {
4463
+ res.writeHead(403, { "content-type": "application/json; charset=utf-8" });
4464
+ res.end(JSON.stringify({ error: "forbidden: loopback only" }));
4465
+ return;
4466
+ }
4467
+ if (!(req.headers["content-type"] ?? "").toLowerCase().includes("application/json")) {
4468
+ res.writeHead(415, { "content-type": "application/json; charset=utf-8" });
4469
+ res.end(JSON.stringify({ error: "unsupported content-type" }));
4470
+ return;
4471
+ }
4351
4472
  try {
4352
4473
  let body = "";
4353
- for await (const chunk of req) body += String(chunk);
4474
+ for await (const chunk of req) {
4475
+ body += String(chunk);
4476
+ if (body.length > 512) {
4477
+ res.end(JSON.stringify({ error: "body too large" }));
4478
+ return;
4479
+ }
4480
+ }
4354
4481
  const parsed = JSON.parse(body === "" ? "{}" : body);
4355
4482
  if (usageSettingsScope === void 0) {
4356
4483
  res.end(JSON.stringify({ error: "settings unavailable" }));
@@ -4373,10 +4500,15 @@ function apply(ctx, config = {}) {
4373
4500
  };
4374
4501
  const refreshQuotas = async () => {
4375
4502
  const { keys, identified } = await resolveSubscriptionKeys(ctx.settings, ctx.credentials);
4376
- const rows = [...await collectSubscriptions(keys, identified.filter((item) => item.adapter).map((item) => ({
4377
- provider: item.provider,
4378
- ...item.region === void 0 ? {} : { region: item.region }
4379
- })))].map((row) => {
4503
+ const piProviders = await readPiAiProviders(ctx.settings);
4504
+ const rows = [...await collectSubscriptions(keys, identified.filter((item) => item.adapter).map((item) => {
4505
+ const baseURL = piProviders[item.provider]?.baseURL;
4506
+ return {
4507
+ provider: item.provider,
4508
+ ...item.region === void 0 ? {} : { region: item.region },
4509
+ ...typeof baseURL === "string" && baseURL !== "" ? { baseUrl: baseURL } : {}
4510
+ };
4511
+ }))].map((row) => {
4380
4512
  const planType = planTypeOf(row.provider);
4381
4513
  const subscriptionAmount = subscriptionFeeCnyOf(row.provider, live.rate);
4382
4514
  return {
@@ -4404,7 +4536,11 @@ function apply(ctx, config = {}) {
4404
4536
  handler: async (req, res) => {
4405
4537
  if (!guardLoopback(req, res)) return;
4406
4538
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
4407
- if (Date.now() - quotaCache.at >= SUBSCRIPTION_CACHE_MS) await refreshQuotas();
4539
+ if (Date.now() - quotaCache.at >= SUBSCRIPTION_CACHE_MS) try {
4540
+ await refreshQuotas();
4541
+ } catch (error) {
4542
+ console.warn("[usage-billing] refreshQuotas failed; serving cached quotas:", error);
4543
+ }
4408
4544
  res.end(JSON.stringify({ quotas: quotaCache.quotas }));
4409
4545
  }
4410
4546
  }), "usage-billing: subscriptions route");
@@ -4436,7 +4572,11 @@ function apply(ctx, config = {}) {
4436
4572
  handler: async (req, res) => {
4437
4573
  if (!guardLoopback(req, res)) return;
4438
4574
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
4439
- if (Date.now() - relayCache.at >= SUBSCRIPTION_CACHE_MS) await refreshRelay();
4575
+ if (Date.now() - relayCache.at >= SUBSCRIPTION_CACHE_MS) try {
4576
+ await refreshRelay();
4577
+ } catch (error) {
4578
+ console.warn("[usage-billing] refreshRelay failed; serving cached relay quotas:", error);
4579
+ }
4440
4580
  const diagnostics = relayCache.quotas.map((row) => ({
4441
4581
  route: row.route,
4442
4582
  origin: row.origin,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 峰谷切换提醒浮层:切档前状态条(右下角或居中)。用 `position: fixed` 即可在
3
- * 任意宿主容器内覆盖整个视口,因此不需要 portal。布局为「档位徽标 + 大号等宽
4
- * 倒计时 + 一句说明 + 关闭」,克制冷调、无重力阴影。渲染是受控的:父组件把命中
3
+ * 任意宿主容器内覆盖整个视口,因此不需要 portal。布局按设计 peak-card:头部
4
+ * (档位 tag + 关闭)→ 大号等宽倒计时 → 一句说明。渲染是受控的:父组件把命中
5
5
  * (hit)与偏好传入,显示剩余分钟并在切换后消失。
6
6
  */
7
7
  import type { PeakAlertConfig, PeakAlertHit } from './peak-alert.ts';
@@ -1,7 +1,8 @@
1
1
  /**
2
- * PluginInfoCard: 「设置 → 插件信息」卡——展示头像/作者、GitHub 仓库、版本号、
3
- * 许可证等插件元信息。版本号来自服务端 usage-stats 的 `pluginVersion`(读自
4
- * 包 package.json),其余元信息静态来自 `plugin-info.ts`。无版本号时显示 em dash。
2
+ * PluginInfoCard: 「设置 → 插件信息」卡——图标 + 插件名/版本 tag + 描述,下接
3
+ * 元信息网格(作者 / 仓库 / npm / 许可证)。版本号来自服务端 usage-stats 的
4
+ * `pluginVersion`(读自包 package.json),其余元信息静态来自 `plugin-info.ts`。
5
+ * 无版本号时显示 em dash。
5
6
  */
6
7
  import type { UsageBillingKey } from './locales.ts';
7
8
  /** 信息卡 props:locale 函数 + 版本号(服务端下发,可为空)。 */
@@ -6,7 +6,6 @@
6
6
  * 3. Token 结构 KPI(缓存命中率 / reasoning 占比 / 输入:输出比 / 峰值日)。
7
7
  */
8
8
  import type { UsageBillingKey } from './locales.ts';
9
- import { type CostCurrency } from './pricing.ts';
10
9
  import type { UsageStats } from './UsageBilling.tsx';
11
10
  /**
12
11
  * Token 洞察面板。
@@ -16,7 +15,6 @@ import type { UsageStats } from './UsageBilling.tsx';
16
15
  */
17
16
  export declare function TokenPanel(props: {
18
17
  stats: UsageStats;
19
- currency: CostCurrency;
20
18
  trendDays: 7 | 30;
21
19
  onTrendDays: (d: 7 | 30) => void;
22
20
  t: (key: UsageBillingKey) => string;
@@ -223,6 +223,7 @@ export interface UsageStats {
223
223
  }
224
224
  /**
225
225
  * 拉取官方余额差对账提示(drift 时非空),供余额面板展示;失败返回 undefined。
226
+ * 复用 {@link fetchBalanceDoc} 的同一响应,导出供对账提示渲染测试单独解析。
226
227
  * @returns the reconcile notice, or undefined on any failure / no drift.
227
228
  */
228
229
  export declare function fetchReconcile(): Promise<ReconcileNotice | undefined>;
@@ -1,5 +1,5 @@
1
1
  /** Locale dictionaries for the usage billing surface. */
2
- export type UsageBillingKey = 'billing.title' | 'billing.subtitle' | 'billing.cost' | 'billing.todayCost' | 'billing.monthCost' | 'billing.yearCost' | 'billing.monthProjected' | 'billing.liveTurn' | 'billing.liveSession' | 'billing.totalCost' | 'billing.calls' | 'billing.cacheHitRate' | 'billing.tokens' | 'billing.inputTokens' | 'billing.outputTokens' | 'billing.avgCost' | 'billing.trend' | 'billing.trend7d' | 'billing.trend30d' | 'billing.trendMetric' | 'billing.trendMetricCost' | 'billing.trendMetricTokens' | 'billing.trendEmpty' | 'billing.budget' | 'billing.sessions' | 'billing.project' | 'billing.lastActive' | 'billing.sessionOverflow' | 'billing.budgetTierBody' | 'billing.models' | 'billing.providerBilling' | 'billing.estimated' | 'billing.actual' | 'billing.pricing' | 'billing.showPricing' | 'billing.hidePricing' | 'billing.pricePerM' | 'billing.input' | 'billing.output' | 'billing.cacheHit' | 'billing.peak' | 'billing.offPeak' | 'billing.flat' | 'billing.peakHours' | 'billing.band' | 'billing.openDashboard' | 'billing.close' | 'billing.lastUpdated' | 'billing.noData' | 'billing.todayRate' | 'billing.rateLive' | 'billing.rateBuiltin' | 'billing.pricingTip' | 'billing.balance' | 'billing.balanceUnconfigured' | 'billing.balanceUnauthorized' | 'billing.balanceUnreachable' | 'billing.uncatalogued' | 'billing.estimatedPricing' | 'billing.balanceDays' | 'billing.balanceLowBody' | 'billing.reconcileDrift' | 'billing.reconcileDismiss' | 'billing.subscriptions' | 'billing.subscriptionNotConfigured' | 'billing.subscriptionUnauthorized' | 'billing.subscriptionUnavailable' | 'billing.subscriptionInvalid' | 'billing.subscriptionRateLimited' | 'billing.subscriptionSession' | 'billing.subscriptionWeekly' | 'billing.subscriptionMonthly' | 'billing.subscriptionBilling' | 'billing.subscriptionRemaining' | 'billing.subscriptionExhausted' | 'billing.subscriptionReset' | 'billing.subscriptionNoApi' | 'billing.floatWindow' | 'billing.floatModeCombined' | 'billing.floatModeSubscription' | 'billing.floatWindowHint' | 'billing.floatNoTargets' | 'billing.floatNoTargetsHint' | 'billing.floatPrev' | 'billing.floatNext' | 'billing.subscriptionsStale' | 'billing.heatmapLess' | 'billing.heatmapMore' | 'billing.currency' | 'billing.currencyCny' | 'billing.currencyUsd' | 'billing.heatmap' | 'billing.rounds' | 'billing.anomaly' | 'billing.workspaces' | 'billing.plan' | 'billing.remaining' | 'billing.unknownModel' | 'billing.model' | 'billing.currentRound' | 'billing.costAbbr' | 'billing.tabOverview' | 'billing.tabTrends' | 'billing.tabProviders' | 'billing.tabDetails' | 'billing.tabPricing' | 'billing.tabSettings' | 'billing.settingsHead' | 'billing.settingsHint' | 'billing.budgetHint' | 'billing.peakAlertHint' | 'billing.peakAlertDescPeak' | 'billing.peakAlertDescOff' | 'billing.export' | 'billing.exportCsvDay' | 'billing.exportCsvSession' | 'billing.exportJson' | 'billing.peakShare' | 'billing.peakShareHint' | 'billing.weekCost' | 'billing.roleCost' | 'billing.roleUser' | 'billing.roleAssistant' | 'billing.roleTool' | 'billing.roleHint' | 'billing.tierPeak' | 'billing.tierOff' | 'billing.tierToPeak' | 'billing.tierToOff' | 'billing.tierAlertEnterPeak' | 'billing.tierAlertEnterOff' | 'billing.peakAlertTitlePeak' | 'billing.peakAlertTitleOff' | 'billing.peakAlert' | 'billing.peakAlertLeadMin' | 'billing.peakAlertPosCorner' | 'billing.peakAlertPosCenter' | 'billing.peakAlertModePeak' | 'billing.peakAlertModeOff' | 'billing.peakAlertModeBoth' | 'billing.peakAlertWebNotify' | 'billing.peakAlertPreview' | 'billing.planTypeCode' | 'billing.planTypeToken' | 'billing.subscriptionFeePerMonth' | 'billing.triggerToday' | 'billing.triggerMonth' | 'billing.subscriptionIncluded' | 'billing.free' | 'billing.official' | 'billing.thirdParty' | 'billing.officialCost' | 'billing.thirdPartyCost' | 'billing.perfSamples' | 'billing.perfTtft' | 'billing.perfP50' | 'billing.perfP90' | 'billing.perfTps' | 'billing.perfLatency' | 'billing.perfEstimated' | 'billing.perfEmpty' | 'billing.perfTpsUnit' | 'billing.perfTitle' | 'billing.perfHint' | 'billing.heatmapYear' | 'billing.heatmapMonth' | 'billing.activeDays' | 'billing.streakDays' | 'billing.subscriptionAutoDetect' | 'billing.pluginInfo' | 'billing.pluginName' | 'billing.pluginDescription' | 'billing.pluginVersion' | 'billing.pluginAuthor' | 'billing.pluginRepository' | 'billing.pluginNpm' | 'billing.pluginLicense' | 'billing.tabToken' | 'billing.tokenExport' | 'billing.tokenExportCsv' | 'billing.tokenCacheHitRate' | 'billing.tokenReasoningShare' | 'billing.tokenReasoningShort' | 'billing.tokenIo' | 'billing.tokenPeak' | 'billing.tokenDaily' | 'billing.tokenByModel' | 'billing.tokenMiss' | 'billing.tokenHit' | 'billing.tokenOutput' | 'billing.tokenTotal' | 'billing.tokenShare' | 'billing.usageStatsTool' | 'billing.usageStatsToolHint' | 'billing.balanceGranted' | 'billing.balanceTopped' | 'billing.balanceDaily' | 'billing.balanceDaysLong' | 'billing.balanceDaysUnit' | 'billing.popTodayModel' | 'billing.popNoConsumption' | 'billing.popQuotaAlert' | 'billing.popRiskNone' | 'billing.popTitle' | 'billing.popDirectLead' | 'billing.popSubLead' | 'billing.popBalanceNormal' | 'billing.popBalanceLow' | 'billing.popQuotaNormal' | 'billing.popQuotaLow' | 'billing.alertBalanceLow' | 'billing.alertQuotaLow' | 'billing.unpricedHint' | 'billing.exportCsvSite' | 'billing.panelRelay' | 'billing.relaySite' | 'billing.relayDirect' | 'billing.relayUnknown' | 'billing.panelRelayQuota' | 'billing.relayBalance' | 'billing.relayNoQuota' | 'billing.relayWindowUsed' | 'billing.relayKindNewApi' | 'billing.relayKindSub2Api' | 'billing.relayKindUnknown' | 'billing.relayCalls';
2
+ export type UsageBillingKey = 'billing.title' | 'billing.subtitle' | 'billing.cost' | 'billing.todayCost' | 'billing.monthCost' | 'billing.yearCost' | 'billing.monthProjected' | 'billing.liveTurn' | 'billing.liveSession' | 'billing.totalCost' | 'billing.calls' | 'billing.cacheHitRate' | 'billing.tokens' | 'billing.inputTokens' | 'billing.outputTokens' | 'billing.avgCost' | 'billing.trend' | 'billing.trend7d' | 'billing.trend30d' | 'billing.trendMetric' | 'billing.trendMetricCost' | 'billing.trendMetricTokens' | 'billing.trendEmpty' | 'billing.budget' | 'billing.budgetAmount' | 'billing.budgetSummary' | 'billing.sessions' | 'billing.sessionTitle' | 'billing.project' | 'billing.lastActive' | 'billing.sessionOverflow' | 'billing.budgetTierBody' | 'billing.models' | 'billing.providerBilling' | 'billing.estimated' | 'billing.actual' | 'billing.pricing' | 'billing.showPricing' | 'billing.hidePricing' | 'billing.pricePerM' | 'billing.input' | 'billing.output' | 'billing.cacheHit' | 'billing.peak' | 'billing.offPeak' | 'billing.flat' | 'billing.peakHours' | 'billing.band' | 'billing.openDashboard' | 'billing.close' | 'billing.footer' | 'billing.footerCredit' | 'billing.lastUpdated' | 'billing.noData' | 'billing.todayRate' | 'billing.rateLive' | 'billing.rateBuiltin' | 'billing.pricingTip' | 'billing.pricingUnit' | 'billing.pricingNotes' | 'billing.ubPeak' | 'billing.ubOff' | 'billing.peakBand' | 'billing.pricingSource' | 'billing.noteCache' | 'billing.noteBand' | 'billing.noteSource' | 'billing.balance' | 'billing.balanceUnconfigured' | 'billing.balanceUnauthorized' | 'billing.balanceUnreachable' | 'billing.uncatalogued' | 'billing.estimatedPricing' | 'billing.balanceDays' | 'billing.balanceLowBody' | 'billing.reconcileDrift' | 'billing.reconcileDismiss' | 'billing.subscriptions' | 'billing.subscriptionNotConfigured' | 'billing.subscriptionUnauthorized' | 'billing.subscriptionUnavailable' | 'billing.subscriptionInvalid' | 'billing.subscriptionRateLimited' | 'billing.subscriptionSession' | 'billing.subscriptionWeekly' | 'billing.subscriptionMonthly' | 'billing.subscriptionBilling' | 'billing.subscriptionRemaining' | 'billing.subscriptionExhausted' | 'billing.subscriptionReset' | 'billing.subscriptionNoApi' | 'billing.floatWindow' | 'billing.floatModeCombined' | 'billing.floatModeSubscription' | 'billing.floatMode' | 'billing.floatTargets' | 'billing.floatWindowHint' | 'billing.floatNoTargets' | 'billing.floatNoTargetsHint' | 'billing.floatPrev' | 'billing.floatNext' | 'billing.subscriptionsStale' | 'billing.heatmapLess' | 'billing.heatmapMore' | 'billing.currency' | 'billing.currencyCny' | 'billing.currencyUsd' | 'billing.heatmap' | 'billing.rounds' | 'billing.roundsHint' | 'billing.anomaly' | 'billing.workspaces' | 'billing.workspacesHint' | 'billing.plan' | 'billing.remaining' | 'billing.unknownModel' | 'billing.model' | 'billing.thModel' | 'billing.thInputMiss' | 'billing.thInputHit' | 'billing.currentRound' | 'billing.costAbbr' | 'billing.tabOverview' | 'billing.tabTrends' | 'billing.tabProviders' | 'billing.tabDetails' | 'billing.tabPricing' | 'billing.tabSettings' | 'billing.settingsHead' | 'billing.settingsHint' | 'billing.budgetHint' | 'billing.peakAlertHint' | 'billing.peakAlertDescPeak' | 'billing.peakAlertDescOff' | 'billing.export' | 'billing.exportCsvDay' | 'billing.exportCsvSession' | 'billing.exportJson' | 'billing.peakShare' | 'billing.peakShareHint' | 'billing.weekCost' | 'billing.roleCost' | 'billing.roleUser' | 'billing.roleAssistant' | 'billing.roleTool' | 'billing.roleHint' | 'billing.tierPeak' | 'billing.tierOff' | 'billing.tierToPeak' | 'billing.tierToOff' | 'billing.tierAlertEnterPeak' | 'billing.tierAlertEnterOff' | 'billing.peakAlertTitlePeak' | 'billing.peakAlertTitleOff' | 'billing.peakAlert' | 'billing.peakAlertLeadMin' | 'billing.peakAlertPos' | 'billing.peakAlertMode' | 'billing.peakAlertPosCorner' | 'billing.peakAlertPosCenter' | 'billing.peakAlertModePeak' | 'billing.peakAlertModeOff' | 'billing.peakAlertModeBoth' | 'billing.peakAlertWebNotify' | 'billing.peakAlertPreview' | 'billing.planTypeCode' | 'billing.planTypeToken' | 'billing.subscriptionFeePerMonth' | 'billing.triggerToday' | 'billing.triggerMonth' | 'billing.subscriptionIncluded' | 'billing.free' | 'billing.official' | 'billing.thirdParty' | 'billing.officialCost' | 'billing.thirdPartyCost' | 'billing.perfSamples' | 'billing.perfTtft' | 'billing.perfP50' | 'billing.perfP90' | 'billing.perfTps' | 'billing.perfLatency' | 'billing.perfEstimated' | 'billing.perfEmpty' | 'billing.perfTpsUnit' | 'billing.perfTitle' | 'billing.perfHint' | 'billing.heatmapYear' | 'billing.heatmapMonth' | 'billing.activeDays' | 'billing.streakDays' | 'billing.subscriptionAutoDetect' | 'billing.pluginInfo' | 'billing.pluginName' | 'billing.pluginDescription' | 'billing.pluginVersion' | 'billing.pluginAuthor' | 'billing.pluginRepository' | 'billing.pluginNpm' | 'billing.pluginLicense' | 'billing.tabToken' | 'billing.tokenExport' | 'billing.tokenExportCsv' | 'billing.tokenCacheHitRate' | 'billing.tokenReasoningShare' | 'billing.tokenReasoningShort' | 'billing.tokenIo' | 'billing.tokenPeak' | 'billing.tokenDaily' | 'billing.tokenByModel' | 'billing.tokenMiss' | 'billing.tokenHit' | 'billing.tokenOutput' | 'billing.tokenTotal' | 'billing.tokenShare' | 'billing.usageStatsTool' | 'billing.usageStatsToolHint' | 'billing.balanceGranted' | 'billing.balanceTopped' | 'billing.balanceDaily' | 'billing.balanceDaysLong' | 'billing.balanceDaysUnit' | 'billing.popTodayModel' | 'billing.popNoConsumption' | 'billing.popQuotaAlert' | 'billing.popRiskNone' | 'billing.popTitle' | 'billing.popDirectLead' | 'billing.popSubLead' | 'billing.popBalanceNormal' | 'billing.popBalanceLow' | 'billing.popQuotaNormal' | 'billing.popQuotaLow' | 'billing.alertBalanceLow' | 'billing.alertQuotaLow' | 'billing.unpricedHint' | 'billing.exportCsvSite' | 'billing.panelRelay' | 'billing.relaySite' | 'billing.relayDirect' | 'billing.relayUnknown' | 'billing.panelRelayQuota' | 'billing.relayBalance' | 'billing.relayNoQuota' | 'billing.relayWindowUsed' | 'billing.relayKindNewApi' | 'billing.relayKindSub2Api' | 'billing.relayKindUnknown' | 'billing.relayCalls';
3
3
  export declare const NS = "usageBilling";
4
4
  export declare const zh: Record<UsageBillingKey, string>;
5
5
  export declare const en: Record<UsageBillingKey, string>;
@@ -225,10 +225,11 @@ export declare function computeCost(entry: ModelEntry, buckets: TokenUsageBucket
225
225
  * @param buckets - token usage counts.
226
226
  * @param timeMs - the call's wall-clock time (epoch ms); null falls back to the peak-share mix.
227
227
  * @param peakShare - fallback mix used only when `timeMs` is missing.
228
- * @returns the estimated cost in the entry's native currency.
228
+ * @returns the estimated cost in CNY(USD 计价模型已按当前汇率折算)。
229
229
  */
230
230
  export declare function computeCostAt(entry: ModelEntry, buckets: TokenUsageBuckets, timeMs: number | null | undefined, peakShare?: number): number;
231
- /** 人民币 → 美元(显示换算用):1 USD = {@link USD_TO_CNY} CNY。 */
231
+ /** 人民币 → 美元(显示换算用):用当前生效汇率(实时优先,缺失回退内置),
232
+ * 与计价链路的 `currentRate()` 同口径,避免实时汇率生效时 USD 显示与计价不一致。 */
232
233
  export declare function cnyToUsd(cny: number): number;
233
234
  /**
234
235
  * Format an amount with adaptive precision and the given currency symbol.
@@ -33,7 +33,8 @@ export interface FloatWindowPrefs {
33
33
  export declare const DEFAULT_FLOAT_WINDOW_PREFS: FloatWindowPrefs;
34
34
  /** localStorage key(与 budget store 的 `dsh.ui-usage-billing.*` 命名空间一致)。 */
35
35
  export declare const FLOAT_WINDOW_STORAGE_KEY = "dsh.ui-usage-billing.float";
36
- /** 读取浮窗偏好(含损坏/缺失回退到默认)。仅在浏览器半区调用。 */
36
+ /** 读取浮窗偏好(含损坏/缺失回退到默认)。仅在浏览器半区调用。
37
+ * 返回全新对象(含 targets 数组拷贝),避免调用方就地修改污染共享默认值。 */
37
38
  export declare function loadFloatWindowPrefs(): FloatWindowPrefs;
38
39
  /** 写入浮窗偏好。失败静默(展示偏好非关键)。 */
39
40
  export declare function saveFloatWindowPrefs(prefs: FloatWindowPrefs): void;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kenz1117/dsh-ui-usage-billing",
3
3
  "description": "Usage billing dashboard for DeepSeek Harness: sidebar cost metrics plus a full dashboard modal, priced from a current multi-provider catalog with real usage aggregated from session logs.",
4
- "version": "0.9.19",
4
+ "version": "1.0.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -47,42 +47,42 @@
47
47
  },
48
48
  "license": "MIT",
49
49
  "peerDependencies": {
50
- "@deepseek-ai/dsh-api-remotes": "workspace:^",
51
- "@deepseek-ai/dsh-atomic-write": "workspace:^",
52
- "@deepseek-ai/dsh-client-connection": "workspace:^",
53
- "@deepseek-ai/dsh-host-webserver": "workspace:^",
54
- "@deepseek-ai/dsh-client-locale": "workspace:^",
55
- "@deepseek-ai/dsh-client-runtime": "workspace:^",
56
- "@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
57
- "@deepseek-ai/dsh-client-ui-sidebar": "workspace:^",
58
- "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
59
- "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
60
- "@deepseek-ai/dsh-credentials": "workspace:^",
61
- "@deepseek-ai/dsh-invariants": "workspace:^",
62
- "@deepseek-ai/dsh-llm": "workspace:^",
63
- "@deepseek-ai/dsh-session": "workspace:^",
64
- "@deepseek-ai/dsh-session-persistence": "workspace:^",
65
- "@deepseek-ai/dsh-settings": "workspace:^",
66
- "@deepseek-ai/dsh-tools": "workspace:^",
67
- "@deepseek-ai/schemastery": "workspace:^",
68
- "@deepseek-ai/cordis": "workspace:^",
50
+ "@deepseek-ai/dsh-api-remotes": "*",
51
+ "@deepseek-ai/dsh-atomic-write": "*",
52
+ "@deepseek-ai/dsh-client-connection": "*",
53
+ "@deepseek-ai/dsh-host-webserver": "*",
54
+ "@deepseek-ai/dsh-client-locale": "*",
55
+ "@deepseek-ai/dsh-client-runtime": "*",
56
+ "@deepseek-ai/dsh-client-ui-conversation": "*",
57
+ "@deepseek-ai/dsh-client-ui-sidebar": "*",
58
+ "@deepseek-ai/dsh-client-ui-primitives": "*",
59
+ "@deepseek-ai/dsh-client-ui-slots": "*",
60
+ "@deepseek-ai/dsh-credentials": "*",
61
+ "@deepseek-ai/dsh-invariants": "*",
62
+ "@deepseek-ai/dsh-llm": "*",
63
+ "@deepseek-ai/dsh-session": "*",
64
+ "@deepseek-ai/dsh-session-persistence": "*",
65
+ "@deepseek-ai/dsh-settings": "*",
66
+ "@deepseek-ai/dsh-tools": "*",
67
+ "@deepseek-ai/schemastery": "*",
68
+ "@deepseek-ai/cordis": "*",
69
69
  "react": "^18.2.0"
70
70
  },
71
71
  "dependencies": {
72
72
  "clsx": "^2.1.1"
73
73
  },
74
74
  "devDependencies": {
75
- "@deepseek-ai/dsh-client-locale": "workspace:^",
76
- "@deepseek-ai/dsh-client-runtime": "workspace:^",
77
- "@deepseek-ai/dsh-client-test-runtime": "workspace:^",
78
- "@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
79
- "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
80
- "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
81
- "@deepseek-ai/dsh-invariants": "workspace:^",
75
+ "@deepseek-ai/dsh-client-locale": "*",
76
+ "@deepseek-ai/dsh-client-runtime": "*",
77
+ "@deepseek-ai/dsh-client-test-runtime": "*",
78
+ "@deepseek-ai/dsh-client-ui-conversation": "*",
79
+ "@deepseek-ai/dsh-client-ui-primitives": "*",
80
+ "@deepseek-ai/dsh-client-ui-slots": "*",
81
+ "@deepseek-ai/dsh-invariants": "*",
82
82
  "@testing-library/react": "^16.1.0",
83
83
  "@types/react": "~18.3.1",
84
84
  "@types/node": "^22.20.0",
85
- "@deepseek-ai/cordis": "workspace:^",
85
+ "@deepseek-ai/cordis": "*",
86
86
  "react": "^18.2.0",
87
87
  "react-dom": "^18.2.0",
88
88
  "lightningcss": "^1.32.0",
@@ -1,39 +0,0 @@
1
- /**
2
- * 供应商计费计划知识库(对标 dsh-spend 的 PROVIDER_KNOWLEDGE / PROVIDER_ALIASES)。
3
- *
4
- * 每个 provider 归入两种计费口径之一:
5
- * - `code` —— 订阅制(coding / token / agent plan):费用 = 订阅月费,额度按周期
6
- * 计量,token 用量不计费(不按 token 扣钱)。
7
- * - `token` —— 按量制:费用 = token × 单价(由 models.dev / OpenRouter / 内置目录
8
- * 定价)。无知识库命中的 provider 默认按 token 口径。
9
- *
10
- * `aliasOf` 把部署里杂乱的 provider id(glm / kimi / dashscope…)归一化到知识库键,
11
- * `planOf` 据此返回计费计划;日志出现的 provider 经此自动识别,无需人工配置。
12
- */
13
- /** 计费口径:code = 订阅制(计月费),token = 按量制(计 token 单价)。 */
14
- export type BillingPlanKind = 'code' | 'token';
15
- /** 订阅月费的币种(只支持单一币种计费,与仪表盘币种一致)。 */
16
- export type BillingCurrency = 'CNY' | 'USD';
17
- /** 一个 provider 的计费计划。 */
18
- export interface BillingPlan {
19
- /** 计费口径。 */
20
- kind: BillingPlanKind;
21
- /** 厂商显示名(未命中知识库时用 provider id 原样)。 */
22
- label: string;
23
- /** code 计划的订阅月费(单位 {@link currency});token 计划无此项。 */
24
- subscriptionMonthly?: number;
25
- /** 订阅月费币种。 */
26
- currency?: BillingCurrency;
27
- /** 订阅额度周期(天);额度按此周期计量。 */
28
- periodDays?: number;
29
- }
30
- /** 归一化 provider id 到知识库键;未命中的原样返回(避免误吞)。 */
31
- export declare function aliasOf(provider: string): string;
32
- /**
33
- * 某 provider 的计费计划:命中的 code 计划返回订阅口径,`PROVIDER_LABELS` 命中的
34
- * 按量厂商返回 token 口径(无月费);完全未知返回 undefined(调用方按 token 兜底)。
35
- */
36
- export declare function planOf(provider: string): BillingPlan | undefined;
37
- /** 是否已知为订阅制(code)provider。 */
38
- export declare function isCodePlan(provider: string): boolean;
39
- //# sourceMappingURL=plans.d.ts.map