@kenz1117/dsh-ui-usage-billing 1.1.4 → 1.1.5

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.
Files changed (3) hide show
  1. package/lib/client.js +2 -2
  2. package/lib/index.js +258 -26
  3. package/package.json +35 -20
package/lib/index.js CHANGED
@@ -6,6 +6,7 @@ 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";
8
8
  import z from "@deepseek-ai/schemastery";
9
+ import { createHash, createHmac } from "node:crypto";
9
10
  //#region lib/types/client/plan-knowledge.js
10
11
  /**
11
12
  * Plan-knowledge reference (adapted from dsh-spend's `knowledge.js`, MIT):
@@ -266,7 +267,7 @@ function tierAt(timeMs) {
266
267
  }
267
268
  /** 时刻是否落在北京时间周末(周六/周日)。 */
268
269
  function isBeijingWeekend(timeMs) {
269
- const day = new Date(timeMs + 8 * 36e5).getUTCDay();
270
+ const day = new Date(timeMs + 288e5).getUTCDay();
270
271
  return day === 0 || day === 6;
271
272
  }
272
273
  /**
@@ -1633,24 +1634,31 @@ function computeCost(entry, buckets, peakShare = DEFAULT_PEAK_SHARE, nowMs = Dat
1633
1634
  return peak * peakShare + off * (1 - peakShare);
1634
1635
  }
1635
1636
  /**
1637
+ * v1 峰谷档判定(峰谷开闸起、周末全谷分界止):不豁免周末——该时段官方
1638
+ * 高峰时段为每天 9-12 / 14-18(周六日同样计峰)。仅用于历史事件计费;
1639
+ * 「当前时刻」的档位(提醒/时段条/费率展示)一律走 {@link tierAt} 现行规则。
1640
+ */
1641
+ function tariffV1At(timeMs) {
1642
+ return isPeakHour((new Date(timeMs).getUTCHours() + 8) % 24) ? "peak" : "offPeak";
1643
+ }
1644
+ /**
1636
1645
  * 按调用时刻精确判定高峰/空闲档并计价(P0-1:替代固定比例混合)。时刻未知
1637
1646
  * (null/NaN,理论不发生在真实事件流)时回退 {@link DEFAULT_PEAK_SHARE} 混合,
1638
1647
  * 保持旧语义不低估。平档模型(无 offPeak)两个时段同价。限时促销与峰谷档
1639
1648
  * 同口径:按事件时刻判定该笔流量当时享受的单价。
1649
+ *
1650
+ * 历史正确性(按变更节点分段适用规则,不统一套现行价重算历史):
1651
+ * - 早于 {@link PEAK_ERA_START_MS} 的事件按当时官方基础价
1652
+ * ({@link LEGACY_DEEPSEEK_BANDS})计费;
1653
+ * - 峰谷开闸至 {@link WEEKEND_OFFPEAK_START_MS} 之间按 v1 规则(周末不豁免,
1654
+ * 周六日 9-12 / 14-18 计峰);
1655
+ * - 周末全谷分界起按现行规则({@link tierAt},周六日全天低谷)。
1640
1656
  * @param entry - the catalog entry whose prices apply.
1641
1657
  * @param buckets - token usage counts.
1642
1658
  * @param timeMs - the call's wall-clock time (epoch ms); null falls back to the peak-share mix.
1643
1659
  * @param peakShare - fallback mix used only when `timeMs` is missing.
1644
1660
  * @returns the estimated cost in CNY(USD 计价模型已按当前汇率折算)。
1645
1661
  */
1646
- /**
1647
- * v1 峰谷档判定(峰谷开闸起、周末全谷分界止):不豁免周末——该时段官方
1648
- * 高峰时段为每天 9-12 / 14-18(周六日同样计峰)。仅用于历史事件计费;
1649
- * 「当前时刻」的档位(提醒/时段条/费率展示)一律走 {@link tierAt} 现行规则。
1650
- */
1651
- function tariffV1At(timeMs) {
1652
- return isPeakHour((new Date(timeMs).getUTCHours() + 8) % 24) ? "peak" : "offPeak";
1653
- }
1654
1662
  function computeCostAt(entry, buckets, timeMs, peakShare = DEFAULT_PEAK_SHARE) {
1655
1663
  if (timeMs === null || timeMs === void 0 || !Number.isFinite(timeMs)) return computeCost(entry, buckets, peakShare);
1656
1664
  const priced = applyPromo(entry, timeMs);
@@ -2387,7 +2395,7 @@ function createUsageAggregator(persistence, options = {}) {
2387
2395
  id,
2388
2396
  ...meta.cwd === void 0 ? {} : { cwd: meta.cwd },
2389
2397
  ...stamp === null ? {} : { stamp },
2390
- foldVersion: 6,
2398
+ foldVersion: 7,
2391
2399
  fold: serializeFold(fold)
2392
2400
  };
2393
2401
  const previous = ledger.get(id);
@@ -2411,7 +2419,7 @@ function createUsageAggregator(persistence, options = {}) {
2411
2419
  for (const entry of ledger.values()) {
2412
2420
  if (included.has(entry.id)) continue;
2413
2421
  try {
2414
- const stale = (entry.foldVersion ?? 1) < 6;
2422
+ const stale = (entry.foldVersion ?? 1) < 7;
2415
2423
  folds.push({
2416
2424
  id: entry.id,
2417
2425
  ...entry.cwd === void 0 ? {} : { cwd: entry.cwd },
@@ -2457,7 +2465,7 @@ function createUsageAggregator(persistence, options = {}) {
2457
2465
  outputCost: 0
2458
2466
  };
2459
2467
  const perfModel = /* @__PURE__ */ new Map();
2460
- const perfHour = /* @__PURE__ */ new Map();
2468
+ const perfHourModel = /* @__PURE__ */ new Map();
2461
2469
  for (const { id: sessionId, cwd, fold, staleLedger } of folds) {
2462
2470
  mergeUsageInto(total, fold.total);
2463
2471
  roles.userChars += fold.roles.userChars;
@@ -2488,13 +2496,18 @@ function createUsageAggregator(persistence, options = {}) {
2488
2496
  if (sample.tps !== void 0) modelAccum.tps.push(sample.tps);
2489
2497
  if (sample.latencyMs !== void 0) modelAccum.latencies.push(sample.latencyMs);
2490
2498
  if (sample.estimated) modelAccum.estimated += 1;
2491
- let hourAccum = perfHour.get(sample.hour);
2499
+ let hourModels = perfHourModel.get(sample.hour);
2500
+ if (hourModels === void 0) {
2501
+ hourModels = /* @__PURE__ */ new Map();
2502
+ perfHourModel.set(sample.hour, hourModels);
2503
+ }
2504
+ let hourAccum = hourModels.get(sample.model);
2492
2505
  if (hourAccum === void 0) {
2493
2506
  hourAccum = {
2494
2507
  ttfts: [],
2495
2508
  tps: []
2496
2509
  };
2497
- perfHour.set(sample.hour, hourAccum);
2510
+ hourModels.set(sample.model, hourAccum);
2498
2511
  }
2499
2512
  hourAccum.ttfts.push(sample.ttftMs);
2500
2513
  if (sample.tps !== void 0) hourAccum.tps.push(sample.tps);
@@ -2554,14 +2567,14 @@ function createUsageAggregator(persistence, options = {}) {
2554
2567
  latencyAvg: acc.latencies.length === 0 ? 0 : mean(acc.latencies),
2555
2568
  estimatedSamples: acc.estimated
2556
2569
  }])),
2557
- byHour: Object.fromEntries([...perfHour].map(([hour, acc]) => [hour, {
2570
+ byHourModel: Object.fromEntries([...perfHourModel].map(([hour, models]) => [hour, Object.fromEntries([...models].map(([model, acc]) => [model, {
2558
2571
  samples: acc.ttfts.length,
2559
2572
  ttftAvg: mean(acc.ttfts),
2560
2573
  ...acc.tps.length === 0 ? {} : { tpsAvg: mean(acc.tps) }
2561
- }]))
2574
+ }]))]))
2562
2575
  };
2563
2576
  lastDoc = {
2564
- version: 3,
2577
+ version: 4,
2565
2578
  updatedAt: now,
2566
2579
  source: "session-logs",
2567
2580
  timezone: hostTimeZone(),
@@ -3006,6 +3019,26 @@ const QUERIERS = [
3006
3019
  route: "tokendance-space",
3007
3020
  displayName: "TokenDance",
3008
3021
  querier: queryTokenDance
3022
+ },
3023
+ {
3024
+ route: "tencent-tokenhub",
3025
+ displayName: "腾讯云 TokenHub",
3026
+ querier: queryTencentTokenPlan
3027
+ },
3028
+ {
3029
+ route: "tokenhub",
3030
+ displayName: "腾讯云 TokenHub",
3031
+ querier: queryTencentTokenPlan
3032
+ },
3033
+ {
3034
+ route: "tencent",
3035
+ displayName: "腾讯云 TokenHub",
3036
+ querier: queryTencentTokenPlan
3037
+ },
3038
+ {
3039
+ route: "tencentcloud",
3040
+ displayName: "腾讯云 TokenHub",
3041
+ querier: queryTencentTokenPlan
3009
3042
  }
3010
3043
  ];
3011
3044
  /**
@@ -3038,6 +3071,204 @@ async function queryBalances(ctx, providers) {
3038
3071
  return querier(ctx, env);
3039
3072
  }));
3040
3073
  }
3074
+ /** TokenHub 管控面 API 端点(cloud.tencent.cn/document/api/1823/132270)。 */
3075
+ const TOKENHUB_HOST = "tokenhub.tencentcloudapi.com";
3076
+ /** 云 API 3.0 产品名与版本(签名的 service 段与请求头都必须一致)。 */
3077
+ const TOKENHUB_SERVICE = "tokenhub";
3078
+ const TOKENHUB_VERSION = "2026-03-22";
3079
+ /** 请求地域:管控面对地域不敏感,取默认国内地域(文档地域列表含 ap-guangzhou)。 */
3080
+ const TOKENHUB_REGION = "ap-guangzhou";
3081
+ /** 腾讯云凭据引用值格式:`<SecretId>:<SecretKey>`(分隔符取首个冒号)。 */
3082
+ function parseTencentCredential(value) {
3083
+ const sep = value.indexOf(":");
3084
+ if (sep === -1) return void 0;
3085
+ const secretId = value.slice(0, sep).trim();
3086
+ const secretKey = value.slice(sep + 1).trim();
3087
+ if (secretId === "" || secretKey === "") return void 0;
3088
+ return {
3089
+ secretId,
3090
+ secretKey
3091
+ };
3092
+ }
3093
+ /**
3094
+ * 构造云 API 3.0 TC3-HMAC-SHA256 签名(官方签名方法 v3)。导出供测试:纯函数,
3095
+ * 输入确定则签名确定。Action 不参与签名——它走 `X-TC-Action` 请求头。
3096
+ * @param secretId - 云 API SecretId。
3097
+ * @param secretKey - 云 API SecretKey。
3098
+ * @param payload - 已序列化的请求体(含 Action/Version/Region 公共参数)。
3099
+ * @param timestamp - 签名时间戳(秒)。
3100
+ * @returns Authorization 头的值。
3101
+ */
3102
+ function tc3Authorization(secretId, secretKey, payload, timestamp) {
3103
+ const date = (/* @__PURE__ */ new Date(timestamp * 1e3)).toISOString().slice(0, 10);
3104
+ const canonicalRequest = `POST\n/\n\n${`content-type:application/json; charset=utf-8\nhost:${TOKENHUB_HOST}\n`}\ncontent-type;host\n${createHash("sha256").update(payload).digest("hex")}`;
3105
+ const hashedCanonical = createHash("sha256").update(canonicalRequest).digest("hex");
3106
+ const stringToSign = `TC3-HMAC-SHA256\n${String(timestamp)}\n${date}/${TOKENHUB_SERVICE}/tc3_request\n${hashedCanonical}`;
3107
+ const kDate = createHmac("sha256", date).update(secretKey).digest();
3108
+ const kService = createHmac("sha256", kDate).update(TOKENHUB_SERVICE).digest();
3109
+ const kSigning = createHmac("sha256", kService).update("tc3_request").digest();
3110
+ const signature = createHmac("sha256", kSigning).update(stringToSign).digest("hex");
3111
+ return `TC3-HMAC-SHA256 Credential=${secretId}/${date}/${TOKENHUB_SERVICE}/tc3_request, SignedHeaders=content-type;host, Signature=${signature}`;
3112
+ }
3113
+ /** 调用一次 TokenHub 管控面接口:TC3 签名 + 超时保护,返回响应 JSON 的 `Response`。 */
3114
+ async function callTokenHub(secretId, secretKey, action, params) {
3115
+ const payload = JSON.stringify({
3116
+ Action: action,
3117
+ Version: TOKENHUB_VERSION,
3118
+ Region: TOKENHUB_REGION,
3119
+ ...params
3120
+ });
3121
+ const timestamp = Math.floor(Date.now() / 1e3);
3122
+ const controller = new AbortController();
3123
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS$3);
3124
+ try {
3125
+ const response = await fetch(`https://${TOKENHUB_HOST}/`, {
3126
+ method: "POST",
3127
+ headers: {
3128
+ "content-type": "application/json; charset=utf-8",
3129
+ host: TOKENHUB_HOST,
3130
+ "x-tc-action": action.toLowerCase(),
3131
+ "x-tc-version": TOKENHUB_VERSION,
3132
+ "x-tc-region": TOKENHUB_REGION,
3133
+ "x-tc-timestamp": String(timestamp),
3134
+ authorization: tc3Authorization(secretId, secretKey, payload, timestamp)
3135
+ },
3136
+ body: payload,
3137
+ signal: controller.signal
3138
+ });
3139
+ if (response.status === 401 || response.status === 403) throw Object.assign(/* @__PURE__ */ new Error("unauthorized"), { code: "unauthorized" });
3140
+ if (!response.ok) throw Object.assign(/* @__PURE__ */ new Error(`HTTP ${String(response.status)}`), {
3141
+ httpStatus: response.status,
3142
+ code: "unreachable"
3143
+ });
3144
+ const inner = (await response.json()).Response;
3145
+ if (inner === void 0) throw Object.assign(/* @__PURE__ */ new Error("no Response envelope"), { code: "invalid" });
3146
+ if (inner.Error !== void 0 && inner.Error !== null) {
3147
+ const err = inner.Error;
3148
+ const code = err.Code === "AuthFailure.SignatureFailure" || err.Code === "AuthFailure.SecretIdNotFound" ? "unauthorized" : "unreachable";
3149
+ throw Object.assign(new Error(String(err.Code ?? "api-error")), { code });
3150
+ }
3151
+ return inner;
3152
+ } finally {
3153
+ clearTimeout(timer);
3154
+ }
3155
+ }
3156
+ /**
3157
+ * 在套餐余量对象里防御性提取「剩余额度」:官方 SubPackageBalance/PackageInfo
3158
+ * 的字段名未稳定公开(issue #18 调研期),按语义键名扫描——命中 remaining /
3159
+ * balance / left 语义键直接用;命中 total 与 used 则相减推导。数字一律经
3160
+ * {@link toNumber} 归一化(上游可能给字符串)。
3161
+ * 导出供测试:纯函数。
3162
+ * @param source - 套餐详情里的余量对象(PackageInfo / SubPackageBalance 等)。
3163
+ * @returns 剩余额度(上游单位,通常为 token 数或元);提取不到返回 undefined。
3164
+ */
3165
+ function pickRemainingQuota(source) {
3166
+ if (source === null || typeof source !== "object") return void 0;
3167
+ const numeric = Object.entries(source).filter(([, v]) => v !== null && toNumber$1(v) !== void 0);
3168
+ const byKey = (needles) => {
3169
+ for (const [key, value] of numeric) {
3170
+ const lower = key.toLowerCase();
3171
+ if (needles.some((n) => lower.includes(n))) {
3172
+ const num = toNumber$1(value);
3173
+ if (num !== void 0 && num >= 0) return num;
3174
+ }
3175
+ }
3176
+ };
3177
+ const remaining = byKey([
3178
+ "remain",
3179
+ "balance",
3180
+ "left",
3181
+ "available"
3182
+ ]);
3183
+ if (remaining !== void 0) return remaining;
3184
+ const total = byKey(["total"]);
3185
+ const used = byKey(["used", "consume"]);
3186
+ if (total !== void 0 && used !== void 0) return Math.max(0, total - used);
3187
+ }
3188
+ /** 套餐列表里提取第一个启用套餐的 TeamId:集合字段名做候选兼容。 */
3189
+ function firstEnabledTeamId(inner) {
3190
+ const candidates = inner.TeamSet ?? inner.TokenPlanSet ?? inner.PlanSet;
3191
+ if (!Array.isArray(candidates)) return void 0;
3192
+ for (const item of candidates) {
3193
+ if (item === null || typeof item !== "object") continue;
3194
+ const row = item;
3195
+ if (row.TeamId === void 0 && row.PlanId === void 0) continue;
3196
+ if (row.Status !== void 0 && row.Status !== "enable") continue;
3197
+ const id = row.TeamId ?? row.PlanId;
3198
+ if (typeof id === "string" && id !== "") return id;
3199
+ }
3200
+ }
3201
+ /**
3202
+ * 查询腾讯云 TokenHub Token Plan 套餐余量。凭据值格式 `<SecretId>:<SecretKey>`
3203
+ * (云 API 密钥,非 TokenHub 推理 key)。链路:套餐列表取 TeamId → 套餐详情读
3204
+ * 主额度包余量。管控面字段名未完全稳定,解析按语义键防御提取。
3205
+ * @param ctx - host context carrying the credentials seam.
3206
+ * @param apiKeyEnv - credential reference resolving the `<SecretId>:<SecretKey>` pair.
3207
+ */
3208
+ async function queryTencentTokenPlan(ctx, apiKeyEnv) {
3209
+ const provider = "腾讯云 TokenHub";
3210
+ const hit = await ctx.credentials.resolve(credentialRef(apiKeyEnv));
3211
+ if (hit === void 0) return {
3212
+ provider,
3213
+ displayName: provider,
3214
+ error: "unconfigured"
3215
+ };
3216
+ if (!balanceGate.check(provider)) return {
3217
+ provider,
3218
+ displayName: provider,
3219
+ error: "unreachable"
3220
+ };
3221
+ const credential = parseTencentCredential(hit.value);
3222
+ if (credential === void 0) return {
3223
+ provider,
3224
+ displayName: provider,
3225
+ error: "unauthorized"
3226
+ };
3227
+ const doRequest = async () => {
3228
+ const teamId = firstEnabledTeamId(await callTokenHub(credential.secretId, credential.secretKey, "DescribeTokenPlanList", {}));
3229
+ if (teamId === void 0) return {
3230
+ provider,
3231
+ displayName: provider,
3232
+ error: "invalid"
3233
+ };
3234
+ const detail = await callTokenHub(credential.secretId, credential.secretKey, "DescribeTokenPlan", { TeamId: teamId });
3235
+ const remaining = pickRemainingQuota(detail.PackageInfo) ?? pickRemainingQuota(detail);
3236
+ if (remaining === void 0) {
3237
+ console.warn(`[usage-billing] balance response drifted for ${provider}: no remaining-quota field parsed`);
3238
+ return {
3239
+ provider,
3240
+ displayName: provider,
3241
+ error: "invalid"
3242
+ };
3243
+ }
3244
+ const plan = typeof detail.Name === "string" ? detail.Name : void 0;
3245
+ const exhausted = detail.StopReason === "EXHAUSTED";
3246
+ return {
3247
+ provider,
3248
+ displayName: provider,
3249
+ currency: "CNY",
3250
+ totalBalance: remaining,
3251
+ ...plan !== void 0 ? { plan } : {},
3252
+ ...exhausted ? { isAvailable: false } : {}
3253
+ };
3254
+ };
3255
+ try {
3256
+ const row = await withRetry(doRequest, {
3257
+ retries: 1,
3258
+ baseDelayMs: 250,
3259
+ maxDelayMs: 2e3
3260
+ });
3261
+ balanceGate.success(provider);
3262
+ return row;
3263
+ } catch (error) {
3264
+ balanceGate.fail(provider);
3265
+ return {
3266
+ provider,
3267
+ displayName: provider,
3268
+ error: error.code === "unauthorized" ? "unauthorized" : "unreachable"
3269
+ };
3270
+ }
3271
+ }
3041
3272
  /** 点路径取值:`data.total_available` → 逐层下钻;任一缺失返回 undefined。 */
3042
3273
  function getPath(data, path) {
3043
3274
  let cursor = data;
@@ -3222,7 +3453,7 @@ const declaredGate = createCooldownGate({
3222
3453
  cooldownMs: 6e4
3223
3454
  });
3224
3455
  /** 会触及原型链的路径段:一律拒绝(读结果不是文档自带字段)。 */
3225
- const FORBIDDEN_SEGMENTS = new Set([
3456
+ const FORBIDDEN_SEGMENTS = /* @__PURE__ */ new Set([
3226
3457
  "__proto__",
3227
3458
  "constructor",
3228
3459
  "prototype"
@@ -3772,7 +4003,7 @@ function asPrice(value) {
3772
4003
  */
3773
4004
  function buildExtraModels(data) {
3774
4005
  if (data === null || typeof data !== "object") return [];
3775
- const catalogKeys = new Set([...MODEL_CATALOG.map((entry) => entry.key.toLowerCase()), ...Object.keys(MODEL_KEY_ALIASES).map((key) => MODEL_KEY_ALIASES[key]?.toLowerCase() ?? key.toLowerCase())]);
4006
+ const catalogKeys = /* @__PURE__ */ new Set([...MODEL_CATALOG.map((entry) => entry.key.toLowerCase()), ...Object.keys(MODEL_KEY_ALIASES).map((key) => MODEL_KEY_ALIASES[key]?.toLowerCase() ?? key.toLowerCase())]);
3776
4007
  const extras = [];
3777
4008
  for (const [providerId, providerDoc] of Object.entries(data)) {
3778
4009
  if (providerDoc === null || typeof providerDoc !== "object") continue;
@@ -4446,7 +4677,7 @@ const relayGate = createCooldownGate({
4446
4677
  /** Abort a relay fetch when the upstream hangs beyond this budget. */
4447
4678
  const FETCH_TIMEOUT_MS = 8e3;
4448
4679
  /** 指纹识别缓存 TTL(毫秒):识别结果低频变化,5 分钟内同 origin 不再重复探测。 */
4449
- const FINGERPRINT_TTL_MS = 300 * 1e3;
4680
+ const FINGERPRINT_TTL_MS = 3e5;
4450
4681
  /** 每 origin 的识别结果缓存:`kind` 是识别出的中转站程序,`at` 是探测时刻。 */
4451
4682
  const fingerprintCache = /* @__PURE__ */ new Map();
4452
4683
  /** Number, or null when the value is not a finite number (nor numeric string). */
@@ -4582,7 +4813,7 @@ function originOf(baseURL) {
4582
4813
  * 子路径只会得到 404/非中转站格式,因而面板应排除它们,避免误判为
4583
4814
  * 「未识别」。中转站面板只列真正的第三方中转程序。
4584
4815
  */
4585
- const OFFICIAL_HOSTS = new Set([
4816
+ const OFFICIAL_HOSTS = /* @__PURE__ */ new Set([
4586
4817
  "api.deepseek.com",
4587
4818
  "api.openai.com",
4588
4819
  "open.bigmodel.cn",
@@ -4832,10 +5063,10 @@ const usageBillingSettingsNs = validateSettingsNamespace(BILLING_SETTINGS_NAMESP
4832
5063
  /** 该命名空间的 wire schema:`enableUsageStatsTool` 布尔,默认关闭(issue 诉求)。 */
4833
5064
  const UsageBillingSettingsSchema = z.object({ [ENABLE_USAGE_STATS_TOOL_FIELD]: z.boolean().default(false) });
4834
5065
  /** 实时定价的后台刷新间隔(毫秒):汇率/模型价低频变化,6 小时一次足够。 */
4835
- const PRICING_REFRESH_INTERVAL_MS = 360 * 60 * 1e3;
5066
+ const PRICING_REFRESH_INTERVAL_MS = 216e5;
4836
5067
  /** 订阅套餐额度缓存时长(毫秒):上游配额 API 低频变化,5 分钟足够。 */
4837
- const SUBSCRIPTION_CACHE_MS = 300 * 1e3;
4838
- const BALANCE_CACHE_MS = 300 * 1e3;
5068
+ const SUBSCRIPTION_CACHE_MS = 3e5;
5069
+ const BALANCE_CACHE_MS = 3e5;
4839
5070
  /** DeepSeek 余额查询的默认凭据引用(与 llm-deepseek 的默认引用一致)。 */
4840
5071
  const DEFAULT_BALANCE_API_KEY_ENV = "DEEPSEEK_API_KEY";
4841
5072
  /**
@@ -4846,7 +5077,7 @@ const PACKAGE_VERSION = createRequire(import.meta.url)("../package.json").versio
4846
5077
  /** 统计快照的落盘节流(毫秒):前端 30 秒轮询,快照最多每 30 秒写一次。 */
4847
5078
  const SNAPSHOT_INTERVAL_MS = 3e4;
4848
5079
  /** 鉴权失败告警冷却(毫秒):同一 provider 在窗口内只提示一次,避免 30 秒轮询刷屏。 */
4849
- const AUTH_WARN_COOLDOWN_MS = 1800 * 1e3;
5080
+ const AUTH_WARN_COOLDOWN_MS = 18e5;
4850
5081
  /**
4851
5082
  * 鉴权失败分类告警(P1-5):余额 / 订阅查询返回 unauthorized 时,按
4852
5083
  * `source:provider` 去重并冷却告警,提示检查 llm-pi-ai 里该 provider 的 apiKeyEnv。
@@ -5090,7 +5321,8 @@ function apply(ctx, config = {}) {
5090
5321
  })();
5091
5322
  const persistReconcileRef = () => {
5092
5323
  if (reconcileRef === null) return;
5093
- writeFileAtomic(reconcilePath, JSON.stringify({ ref: reconcileRef }), {
5324
+ const payload = JSON.stringify({ ref: reconcileRef });
5325
+ writeFileAtomic(reconcilePath, payload, {
5094
5326
  mode: 384,
5095
5327
  dirMode: 448
5096
5328
  }).catch(() => {});
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": "1.1.4",
4
+ "version": "1.1.5",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -34,7 +34,6 @@
34
34
  },
35
35
  "client": {
36
36
  "inject": [
37
- "@deepseek-ai/dsh-client-runtime",
38
37
  "@deepseek-ai/dsh-client-locale",
39
38
  "@deepseek-ai/dsh-client-ui-conversation"
40
39
  ],
@@ -51,25 +50,25 @@
51
50
  "scripts": {
52
51
  "bundle": "tsdown",
53
52
  "watch": "tsdown --watch",
54
- "test": "vitest run",
55
- "build": "bash build.sh"
53
+ "test": "vitest run"
56
54
  },
57
55
  "license": "MIT",
58
56
  "engines": {
59
57
  "node": "^22.19.0 || >=24.0.0"
60
58
  },
61
59
  "peerDependencies": {
60
+ "@deepseek-ai/cordis": "*",
62
61
  "@deepseek-ai/dsh-api-remotes": "*",
62
+ "@deepseek-ai/dsh-api-session-controller": "*",
63
63
  "@deepseek-ai/dsh-atomic-write": "*",
64
- "@deepseek-ai/dsh-client-connection": "*",
65
- "@deepseek-ai/dsh-host-webserver": "*",
66
64
  "@deepseek-ai/dsh-client-locale": "*",
67
- "@deepseek-ai/dsh-client-runtime": "*",
65
+ "@deepseek-ai/dsh-client-store": "*",
68
66
  "@deepseek-ai/dsh-client-ui-conversation": "*",
69
- "@deepseek-ai/dsh-client-ui-sidebar": "*",
70
67
  "@deepseek-ai/dsh-client-ui-primitives": "*",
68
+ "@deepseek-ai/dsh-client-ui-sidebar": "*",
71
69
  "@deepseek-ai/dsh-client-ui-slots": "*",
72
70
  "@deepseek-ai/dsh-credentials": "*",
71
+ "@deepseek-ai/dsh-host-webserver": "*",
73
72
  "@deepseek-ai/dsh-invariants": "*",
74
73
  "@deepseek-ai/dsh-llm": "*",
75
74
  "@deepseek-ai/dsh-session": "*",
@@ -77,28 +76,44 @@
77
76
  "@deepseek-ai/dsh-settings": "*",
78
77
  "@deepseek-ai/dsh-tools": "*",
79
78
  "@deepseek-ai/schemastery": "*",
80
- "@deepseek-ai/cordis": "*",
81
79
  "react": "^18.2.0"
82
80
  },
83
81
  "dependencies": {
84
82
  "clsx": "^2.1.1"
85
83
  },
86
84
  "devDependencies": {
87
- "@deepseek-ai/dsh-client-locale": "*",
88
- "@deepseek-ai/dsh-client-runtime": "*",
89
- "@deepseek-ai/dsh-client-test-runtime": "*",
90
- "@deepseek-ai/dsh-client-ui-conversation": "*",
91
- "@deepseek-ai/dsh-client-ui-primitives": "*",
92
- "@deepseek-ai/dsh-client-ui-slots": "*",
93
- "@deepseek-ai/dsh-invariants": "*",
85
+ "@deepseek-ai/cordis": "*",
86
+ "@deepseek-ai/cordis-plugin-include": "^1.0.7",
87
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.3",
88
+ "@deepseek-ai/dsh-api-remotes": "0.1.2-alpha.5",
89
+ "@deepseek-ai/dsh-api-session-controller": "0.1.2-alpha.5",
90
+ "@deepseek-ai/dsh-atomic-write": "0.1.2-alpha.5",
91
+ "@deepseek-ai/dsh-client-locale": "0.1.2-alpha.5",
92
+ "@deepseek-ai/dsh-client-store": "0.1.2-alpha.5",
93
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.2-alpha.5",
94
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.2-alpha.5",
95
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.2-alpha.5",
96
+ "@deepseek-ai/dsh-credentials": "0.1.2-alpha.5",
97
+ "@deepseek-ai/dsh-host-webserver": "0.1.2-alpha.5",
98
+ "@deepseek-ai/dsh-invariants": "0.1.2-alpha.5",
99
+ "@deepseek-ai/dsh-llm": "0.1.2-alpha.5",
100
+ "@deepseek-ai/dsh-scope": "0.1.2-alpha.5",
101
+ "@deepseek-ai/dsh-session": "0.1.2-alpha.5",
102
+ "@deepseek-ai/dsh-session-persistence": "0.1.2-alpha.5",
103
+ "@deepseek-ai/dsh-settings": "0.1.2-alpha.5",
104
+ "@deepseek-ai/dsh-tools": "0.1.2-alpha.5",
105
+ "@deepseek-ai/schemastery": "^3.18.2",
106
+ "@testing-library/dom": "^10.4.1",
94
107
  "@testing-library/react": "^16.1.0",
95
- "@types/react": "~18.3.1",
96
108
  "@types/node": "^22.20.0",
97
- "@deepseek-ai/cordis": "*",
109
+ "@types/react": "~18.3.1",
110
+ "jsdom": "^26.1.0",
111
+ "lightningcss": "^1.32.0",
98
112
  "react": "^18.2.0",
99
113
  "react-dom": "^18.2.0",
100
- "lightningcss": "^1.32.0",
101
- "tsdown": "^0.22.2"
114
+ "tsdown": "^0.22.2",
115
+ "use-sync-external-store": "1.2.0",
116
+ "vitest": "^4.1.8"
102
117
  },
103
118
  "files": [
104
119
  "lib/index.js",