@kenz1117/dsh-ui-usage-billing 1.4.21 → 1.4.23

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
@@ -2,12 +2,12 @@ import { createRequire } from "node:module";
2
2
  import { lstat, mkdir, readFile, rm, stat } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
- import { SessionLogOffset } from "@deepseek-ai/dsh-session/types";
6
5
  import { defineTool } from "@deepseek-ai/dsh-tools";
7
6
  import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
8
7
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
9
8
  import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
10
9
  import z from "@deepseek-ai/schemastery";
10
+ import * as sessionTypes from "@deepseek-ai/dsh-session/types";
11
11
  import { createHash, createHmac } from "node:crypto";
12
12
  //#region lib/types/client/plan-knowledge.js
13
13
  /**
@@ -585,15 +585,15 @@ function isPriced(key) {
585
585
  return FALLBACK_RATES.some((rate) => rate.key.toLowerCase() === resolved.toLowerCase());
586
586
  }
587
587
  /**
588
- * 促销在 nowMs 是否生效:factor 必须落在 (0,1) 区间,截止时刻及之后视为过期;
589
- * endsAtMs 缺省表示长期活动,在 factor 合法期间持续生效。
588
+ * 促销在 nowMs 是否生效:factor 必须落在 [0,1) 区间(0 = 活动期免费,issue #86),
589
+ * 截止时刻及之后视为过期;endsAtMs 缺省表示长期活动,在 factor 合法期间持续生效。
590
590
  * 导出供测试:纯函数。
591
591
  * @param promo - 待判定的促销窗口。
592
592
  * @param nowMs - 判定时刻(epoch ms)。
593
593
  */
594
594
  function isPromoActive(promo, nowMs) {
595
595
  const expired = promo.endsAtMs !== void 0 && nowMs >= promo.endsAtMs;
596
- return Number.isFinite(nowMs) && !expired && promo.factor > 0 && promo.factor < 1;
596
+ return Number.isFinite(nowMs) && !expired && promo.factor >= 0 && promo.factor < 1;
597
597
  }
598
598
  /**
599
599
  * 把限时促销折入条目单价:生效期内返回 price 主档与 offPeak 逐档乘折扣系数的
@@ -606,7 +606,10 @@ function isPromoActive(promo, nowMs) {
606
606
  function applyPromo(entry, nowMs) {
607
607
  const { promo } = entry;
608
608
  if (promo === void 0 || !isPromoActive(promo, nowMs)) return entry;
609
- const factorOf = (field) => promo.factors?.[field] ?? promo.factor;
609
+ const factorOf = (field) => {
610
+ const perBand = promo.factors?.[field];
611
+ return perBand !== void 0 && perBand >= 0 && perBand < 1 ? perBand : promo.factor;
612
+ };
610
613
  const scaled = (band) => ({
611
614
  input: band.input * factorOf("input"),
612
615
  cacheHit: band.cacheHit * factorOf("cacheHit"),
@@ -1308,6 +1311,59 @@ const BUILTIN_MODEL_CATALOG = [
1308
1311
  output: 16.8
1309
1312
  }
1310
1313
  },
1314
+ {
1315
+ key: "minimax-m3.1-flash-preview",
1316
+ name: "MiniMax M3.1 Flash Preview",
1317
+ provider: "MiniMax",
1318
+ colorVar: "ds-amber",
1319
+ price: {
1320
+ currency: "CNY",
1321
+ input: 2.1,
1322
+ cacheHit: .42,
1323
+ output: 8.4
1324
+ }
1325
+ },
1326
+ {
1327
+ key: "u2-flash",
1328
+ name: "U2 Flash",
1329
+ provider: "云知声",
1330
+ colorVar: "dsw-static-violet-400",
1331
+ price: {
1332
+ currency: "CNY",
1333
+ input: 1,
1334
+ cacheHit: .2,
1335
+ output: 2
1336
+ },
1337
+ promo: {
1338
+ factor: 0,
1339
+ endsAtMs: Date.UTC(2026, 9, 31, 16, 0, 0),
1340
+ note: "限时免费至 2026-10-31"
1341
+ }
1342
+ },
1343
+ {
1344
+ key: "u2",
1345
+ name: "Unisound U2",
1346
+ provider: "云知声",
1347
+ colorVar: "dsw-static-violet-400",
1348
+ price: {
1349
+ currency: "CNY",
1350
+ input: 1,
1351
+ cacheHit: .2,
1352
+ output: 2
1353
+ }
1354
+ },
1355
+ {
1356
+ key: "u2-med",
1357
+ name: "U2-Med",
1358
+ provider: "云知声",
1359
+ colorVar: "dsw-static-violet-400",
1360
+ price: {
1361
+ currency: "CNY",
1362
+ input: 8,
1363
+ cacheHit: 2,
1364
+ output: 28
1365
+ }
1366
+ },
1311
1367
  {
1312
1368
  key: "ernie",
1313
1369
  name: "ERNIE-5.1",
@@ -2244,6 +2300,11 @@ const BUILTIN_MODEL_KEY_ALIASES = {
2244
2300
  "minimax-m2-7": "minimax-m2.7",
2245
2301
  "minimax-m2-7-highspeed": "minimax-m2.7-highspeed",
2246
2302
  "minimax-m2-7-high-speed": "minimax-m2.7-highspeed",
2303
+ "minimax-m3.1-flash-preview": "minimax-m3.1-flash-preview",
2304
+ "minimax-m3-1-flash-preview": "minimax-m3.1-flash-preview",
2305
+ "u2-flash": "u2-flash",
2306
+ "u2": "u2",
2307
+ "u2-med": "u2-med",
2247
2308
  "gpt-6": "gpt-6-astra",
2248
2309
  "gpt-6-astra": "gpt-6-astra",
2249
2310
  "gpt-6-sol": "gpt-6-sol",
@@ -2269,6 +2330,11 @@ const BUILTIN_MODEL_KEY_ALIASES = {
2269
2330
  * zero while their tokens still count. Pure functions only: the persistence
2270
2331
  * handle is injected, so the fold is unit-testable without a host.
2271
2332
  */
2333
+ /** 会话日志偏移:宿主 0.1.2-alpha.4 前的 dsh-session/types 不导出 SessionLogOffset
2334
+ * 值,具名导入让这些宿主装载即崩(v1.4.19~v1.4.22 实测,alpha.3 真机复现);
2335
+ * namespace 导入不做具名导出校验,缺导出时退化为恒等——偏移类型是
2336
+ * BrandedNumber,值域与 number 同构,老宿主的 readFrom 原样收数字。 */
2337
+ const sessionLogOffsetOf = sessionTypes.SessionLogOffset ?? ((value) => value);
2272
2338
  /**
2273
2339
  * 走订阅套餐(coding / token plan / opencode 订阅)的 provider id:这些通道的
2274
2340
  * 调用按套餐计费,不再按 token 计费,因此即使模型 id 与计费表撞名也一律豁免。
@@ -3192,6 +3258,13 @@ function configFingerprint(subscriptionMatcher, officialProviderIds, routeAliase
3192
3258
  String(searchEstimate)
3193
3259
  ].join("|");
3194
3260
  }
3261
+ /** 会话格式拒读(宿主 SessionFormatUnsupportedError):日志完好,仅本构建读不了。
3262
+ * 宿主错误类显式落 error.name;经包装/重建的路径只在 message 里保留类名,两处都认。
3263
+ * 据此把「格式不受支持」与「损坏/其他读取失败」分开计数,供面板按原因归因(issue #84)。 */
3264
+ function isFormatUnsupportedError(error) {
3265
+ if (!(error instanceof Error)) return false;
3266
+ return error.name === "SessionFormatUnsupportedError" || error.message.includes("SessionFormatUnsupportedError");
3267
+ }
3195
3268
  /**
3196
3269
  * Create the incremental usage aggregator.
3197
3270
  * @param persistence - the session persistence service.
@@ -3301,12 +3374,22 @@ function createUsageAggregator(persistence, options = {}) {
3301
3374
  const included = /* @__PURE__ */ new Set();
3302
3375
  const folds = [];
3303
3376
  const skipped = [];
3377
+ let carriedUnreadable = 0;
3378
+ let carriedFormatUnreadable = 0;
3379
+ let formatUnreadable = 0;
3380
+ let noLedgerUnreadable = 0;
3304
3381
  let staleLedgerSessions = 0;
3305
3382
  for (const meta of metas) {
3306
3383
  const id = String(meta.id);
3307
3384
  seen.add(id);
3308
3385
  const stamp = await stampOf(meta);
3309
- if (stamp !== null && unreadable.get(id) === stamp) continue;
3386
+ const mark = unreadable.get(id);
3387
+ if (stamp !== null && mark !== void 0 && mark.stamp === stamp) {
3388
+ carriedUnreadable += 1;
3389
+ if (mark.format) carriedFormatUnreadable += 1;
3390
+ if (!ledger.has(id)) noLedgerUnreadable += 1;
3391
+ continue;
3392
+ }
3310
3393
  const hit = cache.get(id);
3311
3394
  if (hit !== void 0 && stamp !== null && hit.stamp === stamp) {
3312
3395
  cache.delete(id);
@@ -3335,7 +3418,7 @@ function createUsageAggregator(persistence, options = {}) {
3335
3418
  const previous = cache.get(id);
3336
3419
  if (previous?.machine !== void 0 && previous.lastSeq !== void 0) {
3337
3420
  const from = previous.lastSeq + 1;
3338
- const { events, inheritedEventCount } = await persistence.readFrom(meta.id, SessionLogOffset(from));
3421
+ const { events, inheritedEventCount } = await persistence.readFrom(meta.id, sessionLogOffsetOf(from));
3339
3422
  const after = await stampOf(meta);
3340
3423
  if (stamp !== null && after !== stamp) {
3341
3424
  cache.delete(id);
@@ -3361,7 +3444,7 @@ function createUsageAggregator(persistence, options = {}) {
3361
3444
  recordLedger(id, cwd, stamp, previous.fold);
3362
3445
  continue;
3363
3446
  }
3364
- const { events, inheritedEventCount } = await persistence.readFrom(meta.id, SessionLogOffset(0));
3447
+ const { events, inheritedEventCount } = await persistence.readFrom(meta.id, sessionLogOffsetOf(0));
3365
3448
  const after = await stampOf(meta);
3366
3449
  if (stamp !== null && after !== stamp) continue;
3367
3450
  const fold = freshFold();
@@ -3389,7 +3472,13 @@ function createUsageAggregator(persistence, options = {}) {
3389
3472
  recordLedger(id, cwd, stamp, fold);
3390
3473
  } catch (error) {
3391
3474
  skipped.push(id);
3392
- if (stamp !== null) unreadable.set(id, stamp);
3475
+ const format = isFormatUnsupportedError(error);
3476
+ if (format) formatUnreadable += 1;
3477
+ if (!ledger.has(id)) noLedgerUnreadable += 1;
3478
+ if (stamp !== null) unreadable.set(id, {
3479
+ stamp,
3480
+ format
3481
+ });
3393
3482
  console.warn("[usage-billing] skip unreadable session", id, error);
3394
3483
  }
3395
3484
  }
@@ -3569,6 +3658,8 @@ function createUsageAggregator(persistence, options = {}) {
3569
3658
  }).filter((entry) => entry !== null))];
3570
3659
  }))
3571
3660
  };
3661
+ const unreadableTotal = skipped.length + carriedUnreadable;
3662
+ const formatUnreadableTotal = formatUnreadable + carriedFormatUnreadable;
3572
3663
  lastDoc = {
3573
3664
  version: 4,
3574
3665
  updatedAt: now,
@@ -3592,7 +3683,9 @@ function createUsageAggregator(persistence, options = {}) {
3592
3683
  ...searchEstimate > 0 ? { searchCallEstimateCny: searchEstimate } : {},
3593
3684
  ...perf === void 0 ? {} : { perf },
3594
3685
  ...staleLedgerSessions > 0 ? { staleLedgerSessions } : {},
3595
- ...skipped.length > 0 ? { unreadableSessions: skipped.length } : {},
3686
+ ...unreadableTotal > 0 ? { unreadableSessions: unreadableTotal } : {},
3687
+ ...formatUnreadableTotal > 0 ? { unreadableFormatSessions: formatUnreadableTotal } : {},
3688
+ ...noLedgerUnreadable > 0 ? { unreadableNoLedgerSessions: noLedgerUnreadable } : {},
3596
3689
  byRole: (() => {
3597
3690
  const chars = roles.userChars + roles.toolChars;
3598
3691
  const userShare = chars > 0 ? roles.userChars / chars : .5;
@@ -6894,8 +6987,8 @@ function adaptSessionPersistence(raw) {
6894
6987
  const events = Array.isArray(slice) ? slice : slice.events;
6895
6988
  return {
6896
6989
  meta: handle.header,
6897
- fromSeq: SessionLogOffset(fromSeq),
6898
- inheritedEventCount: SessionLogOffset(handle.inheritedEventCount),
6990
+ fromSeq: sessionLogOffsetOf(fromSeq),
6991
+ inheritedEventCount: sessionLogOffsetOf(handle.inheritedEventCount),
6899
6992
  events
6900
6993
  };
6901
6994
  } finally {
@@ -10,11 +10,15 @@
10
10
  * zero while their tokens still count. Pure functions only: the persistence
11
11
  * handle is injected, so the fold is unit-testable without a host.
12
12
  */
13
- import { SessionLogOffset } from '@deepseek-ai/dsh-session/types';
14
- import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session/types';
13
+ import type { SessionEvent, SessionHeader, SessionId, SessionLogOffset as SessionLogOffsetT } from '@deepseek-ai/dsh-session/types';
15
14
  import type { TokenUsage } from '@deepseek-ai/dsh-llm';
16
15
  import { resolveCatalogKey } from './client/pricing.ts';
17
16
  import { BUILTIN_MODEL_KEY_ALIASES } from './builtin-catalog.ts';
17
+ /** 会话日志偏移:宿主 0.1.2-alpha.4 前的 dsh-session/types 不导出 SessionLogOffset
18
+ * 值,具名导入让这些宿主装载即崩(v1.4.19~v1.4.22 实测,alpha.3 真机复现);
19
+ * namespace 导入不做具名导出校验,缺导出时退化为恒等——偏移类型是
20
+ * BrandedNumber,值域与 number 同构,老宿主的 readFrom 原样收数字。 */
21
+ export declare const sessionLogOffsetOf: (value: number) => SessionLogOffsetT;
18
22
  export { BUILTIN_MODEL_KEY_ALIASES, resolveCatalogKey };
19
23
  /**
20
24
  * 走订阅套餐(coding / token plan / opencode 订阅)的 provider id:这些通道的
@@ -212,8 +216,8 @@ export declare function workspaceNameOf(cwd: string | undefined): string;
212
216
  /** readFrom 返回的后缀切片:与宿主 0.1.2 的 SessionEventSuffix 同构,本地结构声明。 */
213
217
  export interface UsageEventSuffix {
214
218
  readonly meta: SessionHeader;
215
- readonly inheritedEventCount: SessionLogOffset;
216
- readonly fromSeq: SessionLogOffset;
219
+ readonly inheritedEventCount: SessionLogOffsetT;
220
+ readonly fromSeq: SessionLogOffsetT;
217
221
  readonly events: readonly SessionEvent[];
218
222
  }
219
223
  /**
@@ -227,7 +231,7 @@ export interface UsageEventSuffix {
227
231
  */
228
232
  export interface UsagePersistence {
229
233
  list(): Promise<readonly SessionHeader[]>;
230
- readFrom(id: SessionId, fromSeq: SessionLogOffset): Promise<UsageEventSuffix>;
234
+ readFrom(id: SessionId, fromSeq: SessionLogOffsetT): Promise<UsageEventSuffix>;
231
235
  locate?(meta: SessionHeader): {
232
236
  path: string;
233
237
  } | undefined;
@@ -295,8 +299,12 @@ export interface UsageStatsDocument {
295
299
  perf?: PerfStats;
296
300
  /** 只存在于账本、且缺 foldVersion 的旧会话数;无旧行时省略。 */
297
301
  staleLedgerSessions?: number;
298
- /** 读时迁移拒读而未统计的会话数(原始日志未动,上游修复后自动恢复);无拒读时省略。 */
302
+ /** 本轮未完整计入的会话数(新失败 + 负缓存命中,跨轮次稳定;有账本旧行的按存档计入最近可用点);无失败时省略。 */
299
303
  unreadableSessions?: number;
304
+ /** 未统计中因会话格式拒读(SessionFormatUnsupportedError)的子集;无格式拒读时省略。 */
305
+ unreadableFormatSessions?: number;
306
+ /** 未统计中无任何账本存档、完全未计入的子集(区别于有存档按最近可用点计入的);无存档缺失时省略。 */
307
+ unreadableNoLedgerSessions?: number;
300
308
  }
301
309
  /** 按角色费用归因:user / tool 为输入成本的启发式摊分,assistant 为输出成本实测。 */
302
310
  export interface RoleCost {
@@ -26,7 +26,7 @@ export interface TrendPoint {
26
26
  cost: number;
27
27
  /** API calls that day (total across models). */
28
28
  calls: number;
29
- /** Total tokens that day (input+output+cache); used by the `tokens` metric. */
29
+ /** Total tokens that day (input+output — stats.input already includes cache hit/miss); used by the `tokens` metric. */
30
30
  tokens?: number;
31
31
  /** Per-model cost that day (stats key → CNY); absent entries plot zero. */
32
32
  byModel?: Readonly<Record<string, number>>;
@@ -110,6 +110,15 @@ export declare function activeDaysOf(byDay: Record<string, {
110
110
  export declare function streakDaysOf(byDay: Record<string, {
111
111
  cost: number;
112
112
  }>, now?: number): number;
113
+ /** 按日 Token 口径:输入 + 输出。stats.input 本身已含缓存命中与未命中
114
+ * (aggregate foldUsage:input = cacheHit + cacheMiss),再叠加缓存字段即重复
115
+ * 计数——趋势图/热力图/触发卡/峰值日的按日 Token 一律走本函数(issue #85);
116
+ * 跨日合计在各行之上累加(KPI 见 kpiAgg,触发卡见 month/weekTokens)。
117
+ * 导出供测试:纯函数。 */
118
+ export declare function dayTokensOf(day: {
119
+ input: number;
120
+ output: number;
121
+ }): number;
113
122
  /**
114
123
  * 近 7 天费用序列(含今天,缺日补 0):触发卡 hover 速览的迷你柱数据源。
115
124
  * 导出供测试:纯函数(日期取本地时区)。
@@ -315,8 +324,12 @@ export interface UsageStats {
315
324
  perf?: ClientPerf;
316
325
  /** 旧版算法账本行兜底的会话数(模型归属可能失真);0 或缺省 = 全部数据可信。 */
317
326
  staleLedgerSessions?: number;
318
- /** 读时迁移拒读而未统计的会话数;0 或缺省 = 全部会话已统计。 */
327
+ /** 本轮未完整计入的会话数(跨轮次稳定);0 或缺省 = 全部会话已统计。 */
319
328
  unreadableSessions?: number;
329
+ /** 未统计中因会话格式拒读的子集(过新待宿主升级、过旧需迁移);0 或缺省 = 无格式拒读。 */
330
+ unreadableFormatSessions?: number;
331
+ /** 未统计中无账本存档、完全未计入的子集;0 或缺省 = 拒读会话均有存档兜底。 */
332
+ unreadableNoLedgerSessions?: number;
320
333
  /** 插件版本号(服务端读自包 package.json;旧快照缺失)。 */
321
334
  pluginVersion?: string;
322
335
  }
@@ -1,5 +1,5 @@
1
1
  /** Locale dictionaries for the usage billing surface. */
2
- export type UsageBillingKey = 'title' | 'cost' | 'todayCost' | 'monthCost' | 'yearCost' | 'monthProjected' | 'liveTurn' | 'liveSession' | 'calls' | 'cacheHitRate' | 'tokens' | 'inputTokens' | 'outputTokens' | 'avgCost' | 'kpiRange' | 'avgRangeToday' | 'avgRange7d' | 'avgRangeWeek' | 'avgRangeMonth' | 'avgRangeAll' | 'trend' | 'trend7d' | 'trend30d' | 'trendMetric' | 'trendMetricCost' | 'trendMetricTokens' | 'pricingSearch' | 'bandPinHint' | 'trendEmpty' | 'trendTotal' | 'roundsUnit' | 'healthModelsLive' | 'healthVendorsDown' | 'healthVendorsUnavailable' | 'budget' | 'budgetRemain' | 'budgetAmount' | 'budgetSummary' | 'sessions' | 'sessionTitle' | 'lastActive' | 'sessionOverflow' | 'budgetTierBody' | 'models' | 'providerBilling' | 'channelUnknown' | 'directTag' | 'relayTag' | 'unknownTag' | 'unknownChannelHint' | 'planCountUnit' | 'actual' | 'pricing' | 'input' | 'output' | 'cacheHit' | 'peak' | 'offPeak' | 'flat' | 'band' | 'close' | 'footer' | 'footerCredit' | 'lastUpdated' | 'noData' | 'todayRate' | 'rateLive' | 'rateBuiltin' | 'pricingSyncedAt' | 'pricingSyncNow' | 'pricingSyncing' | 'promoBadge' | 'promoUntil' | 'promoOpenEnded' | 'pricingTip' | 'pricingUnit' | 'pricingNotes' | 'ubPeak' | 'ubOff' | 'ubStd' | 'peakBand' | 'pricingSource' | 'noteCache' | 'noteBand' | 'noteSource' | 'balance' | 'recharge' | 'rechargeHint' | 'balanceUnconfigured' | 'balanceUnauthorized' | 'balanceUnreachable' | 'uncatalogued' | 'estimatedPricing' | 'balanceDays' | 'balanceLowBody' | 'reconcileDrift' | 'reconcileDismiss' | 'subscriptionNotConfigured' | 'subscriptionUnauthorized' | 'subscriptionUnavailable' | 'subscriptionInvalid' | 'subscriptionRateLimited' | 'subscriptionSession' | 'subscriptionWeekly' | 'subscriptionMonthly' | 'subscriptionBilling' | 'subscriptionRemaining' | 'subscriptionExhausted' | 'subscriptionReset' | 'subscriptionNoApi' | 'floatWindow' | 'floatModeCombined' | 'floatModeSubscription' | 'floatMode' | 'floatTargets' | 'floatWindowHint' | 'floatPrimaryToday' | 'floatPrimaryWeek' | 'floatPrimaryMonth' | 'floatNoTargets' | 'floatNoTargetsHint' | 'cardDisplay' | 'cardDisplayHint' | 'cardMetric' | 'cardMetricMoney' | 'cardMetricTokens' | 'cardSpan' | 'cardSpanDay' | 'cardSpanWeek' | 'cardSpanMonth' | 'triggerMonthTokens' | 'triggerTodayTokens' | 'triggerWeekTokens' | 'subscriptionsStale' | 'staleLedgerNotice' | 'unreadableNotice' | 'tokenCacheWrite' | 'toolRank' | 'toolName' | 'userPrices' | 'userPricesHint' | 'userPriceSave' | 'userPriceModel' | 'userPriceSource' | 'userPriceSourceHint' | 'userPriceOffPeak' | 'userPriceNormal' | 'userPriceCurrency' | 'userPriceAdd' | 'userPriceRemoveSelected' | 'userPriceSelect' | 'sessionStaleBadge' | 'sessionUntitled' | 'heatmapLess' | 'heatmapMore' | 'currency' | 'currencyCny' | 'currencyUsd' | 'currencyEur' | 'language' | 'heatmap' | 'rounds' | 'roundsHint' | 'anomaly' | 'model' | 'thModel' | 'thInputMiss' | 'thInputHit' | 'costAbbr' | 'tabOverview' | 'tabTrends' | 'tabProviders' | 'tabPricing' | 'tabSettings' | 'budgetHint' | 'peakAlertHint' | 'peakAlertDescPeak' | 'peakAlertDescOff' | 'peakAlertDescPeakZhipu' | 'peakAlertDescOffZhipu' | 'export' | 'exportCsvDay' | 'exportCsvSession' | 'exportJson' | 'peakShare' | 'peakSharePerCall' | 'offPeakSavings' | 'perfMax' | 'weekCost' | 'roleCost' | 'roleUser' | 'roleAssistant' | 'roleTool' | 'roleHint' | 'tierPeak' | 'tierOff' | 'tierToPeak' | 'tierToOff' | 'tierAlertEnterPeak' | 'tierAlertEnterOff' | 'tierAlertEnterPeakZhipu' | 'tierAlertEnterOffZhipu' | 'peakAlertTitlePeak' | 'peakAlertTitleOff' | 'peakAlert' | 'peakAlertLeadMin' | 'peakAlertPos' | 'peakAlertMode' | 'peakAlertPosCorner' | 'peakAlertPosCenter' | 'peakAlertModePeak' | 'peakAlertModeOff' | 'peakAlertModeBoth' | 'peakAlertWebNotify' | 'peakAlertPreview' | 'planTypeCode' | 'planTypeToken' | 'subscriptionFeePerMonth' | 'triggerToday' | 'triggerMonth' | 'subscriptionTag' | 'cellEstimateHint' | 'free' | 'perfSamples' | 'perfTtft' | 'perfP50' | 'perfP90' | 'perfTps' | 'perfLatency' | 'perfEstimated' | 'perfEmpty' | 'perfTpsUnit' | 'perfTitle' | 'perfHint' | 'perfAll' | 'perfChartEmpty' | 'heatmapYear' | 'heatmapHalf' | 'heatmapMonth' | 'metricCost' | 'metricTokens' | 'heatmapTotal' | 'activeDays' | 'streakDays' | 'subscriptionAutoDetect' | 'pluginAuthor' | 'pluginRepository' | 'pluginNpm' | 'pluginLicense' | 'tabToken' | 'tokenExport' | 'tokenExportCsv' | 'tokenReasoningShare' | 'tokenReasoningShort' | 'tokenIo' | 'tokenPeak' | 'tokenDaily' | 'tokenByModel' | 'tokenMiss' | 'tokenHit' | 'tokenOutput' | 'tokenViewStructure' | 'tokenViewModel' | 'tokenHitShort' | 'tokenMissShort' | 'tokenTotal' | 'tokenShare' | 'usageStatsTool' | 'usageStatsToolHint' | 'balanceGranted' | 'balanceTopped' | 'balanceDaily' | 'balanceDaysLong' | 'balanceDaysUnit' | 'popTodayModel' | 'popNoConsumption' | 'popTitle' | 'popDirectLead' | 'popSubLead' | 'unpricedHint' | 'searchEstimateHint' | 'siteListDisplay' | 'siteListDisplayHint' | 'liveCostBar' | 'liveCostBarHint' | 'liveCostBarPosition' | 'liveCostBarPosBelow' | 'liveCostBarPosAbove' | 'liveCostBarPosToolbar' | 'exportCsvSite' | 'panelRelayQuota' | 'relayBalance' | 'relayNoQuota' | 'relayWindowUsed' | 'relayKindNewApi' | 'relayKindSub2Api' | 'relayKindUnknown' | 'expandAll' | 'collapseAll' | 'todayOnly' | 'todayNoConsumption';
2
+ export type UsageBillingKey = 'title' | 'cost' | 'todayCost' | 'monthCost' | 'yearCost' | 'monthProjected' | 'liveTurn' | 'liveSession' | 'calls' | 'cacheHitRate' | 'tokens' | 'inputTokens' | 'outputTokens' | 'avgCost' | 'kpiRange' | 'avgRangeToday' | 'avgRange7d' | 'avgRangeWeek' | 'avgRangeMonth' | 'avgRangeAll' | 'trend' | 'trend7d' | 'trend30d' | 'trendMetric' | 'trendMetricCost' | 'trendMetricTokens' | 'pricingSearch' | 'bandPinHint' | 'trendEmpty' | 'trendTotal' | 'roundsUnit' | 'healthModelsLive' | 'healthVendorsDown' | 'healthVendorsUnavailable' | 'budget' | 'budgetRemain' | 'budgetAmount' | 'budgetSummary' | 'sessions' | 'sessionTitle' | 'lastActive' | 'sessionOverflow' | 'budgetTierBody' | 'models' | 'providerBilling' | 'channelUnknown' | 'directTag' | 'relayTag' | 'unknownTag' | 'unknownChannelHint' | 'planCountUnit' | 'actual' | 'pricing' | 'input' | 'output' | 'cacheHit' | 'peak' | 'offPeak' | 'flat' | 'band' | 'close' | 'footer' | 'footerCredit' | 'lastUpdated' | 'noData' | 'todayRate' | 'rateLive' | 'rateBuiltin' | 'pricingSyncedAt' | 'pricingSyncNow' | 'pricingSyncing' | 'promoBadge' | 'promoUntil' | 'promoOpenEnded' | 'pricingTip' | 'pricingUnit' | 'pricingNotes' | 'ubPeak' | 'ubOff' | 'ubStd' | 'peakBand' | 'pricingSource' | 'noteCache' | 'noteBand' | 'noteSource' | 'balance' | 'recharge' | 'rechargeHint' | 'balanceUnconfigured' | 'balanceUnauthorized' | 'balanceUnreachable' | 'uncatalogued' | 'estimatedPricing' | 'balanceDays' | 'balanceLowBody' | 'reconcileDrift' | 'reconcileDismiss' | 'subscriptionNotConfigured' | 'subscriptionUnauthorized' | 'subscriptionUnavailable' | 'subscriptionInvalid' | 'subscriptionRateLimited' | 'subscriptionSession' | 'subscriptionWeekly' | 'subscriptionMonthly' | 'subscriptionBilling' | 'subscriptionRemaining' | 'subscriptionExhausted' | 'subscriptionReset' | 'subscriptionNoApi' | 'floatWindow' | 'floatModeCombined' | 'floatModeSubscription' | 'floatMode' | 'floatTargets' | 'floatWindowHint' | 'floatPrimaryToday' | 'floatPrimaryWeek' | 'floatPrimaryMonth' | 'floatNoTargets' | 'floatNoTargetsHint' | 'cardDisplay' | 'cardDisplayHint' | 'cardMetric' | 'cardMetricMoney' | 'cardMetricTokens' | 'cardSpan' | 'cardSpanDay' | 'cardSpanWeek' | 'cardSpanMonth' | 'triggerMonthTokens' | 'triggerTodayTokens' | 'triggerWeekTokens' | 'subscriptionsStale' | 'staleLedgerNotice' | 'unreadableNotice' | 'unreadableFormatNotice' | 'unreadableNoLedgerNotice' | 'tokenCacheWrite' | 'toolRank' | 'toolName' | 'userPrices' | 'userPricesHint' | 'userPriceSave' | 'userPriceModel' | 'userPriceSource' | 'userPriceSourceHint' | 'userPriceOffPeak' | 'userPriceNormal' | 'userPriceCurrency' | 'userPriceAdd' | 'userPriceRemoveSelected' | 'userPriceSelect' | 'sessionStaleBadge' | 'sessionUntitled' | 'heatmapLess' | 'heatmapMore' | 'currency' | 'currencyCny' | 'currencyUsd' | 'currencyEur' | 'language' | 'heatmap' | 'rounds' | 'roundsHint' | 'anomaly' | 'model' | 'thModel' | 'thInputMiss' | 'thInputHit' | 'costAbbr' | 'tabOverview' | 'tabTrends' | 'tabProviders' | 'tabPricing' | 'tabSettings' | 'budgetHint' | 'peakAlertHint' | 'peakAlertDescPeak' | 'peakAlertDescOff' | 'peakAlertDescPeakZhipu' | 'peakAlertDescOffZhipu' | 'export' | 'exportCsvDay' | 'exportCsvSession' | 'exportJson' | 'peakShare' | 'peakSharePerCall' | 'offPeakSavings' | 'perfMax' | 'weekCost' | 'roleCost' | 'roleUser' | 'roleAssistant' | 'roleTool' | 'roleHint' | 'tierPeak' | 'tierOff' | 'tierToPeak' | 'tierToOff' | 'tierAlertEnterPeak' | 'tierAlertEnterOff' | 'tierAlertEnterPeakZhipu' | 'tierAlertEnterOffZhipu' | 'peakAlertTitlePeak' | 'peakAlertTitleOff' | 'peakAlert' | 'peakAlertLeadMin' | 'peakAlertPos' | 'peakAlertMode' | 'peakAlertPosCorner' | 'peakAlertPosCenter' | 'peakAlertModePeak' | 'peakAlertModeOff' | 'peakAlertModeBoth' | 'peakAlertWebNotify' | 'peakAlertPreview' | 'planTypeCode' | 'planTypeToken' | 'subscriptionFeePerMonth' | 'triggerToday' | 'triggerMonth' | 'subscriptionTag' | 'cellEstimateHint' | 'free' | 'perfSamples' | 'perfTtft' | 'perfP50' | 'perfP90' | 'perfTps' | 'perfLatency' | 'perfEstimated' | 'perfEmpty' | 'perfTpsUnit' | 'perfTitle' | 'perfHint' | 'perfAll' | 'perfChartEmpty' | 'heatmapYear' | 'heatmapHalf' | 'heatmapMonth' | 'metricCost' | 'metricTokens' | 'heatmapTotal' | 'activeDays' | 'streakDays' | 'subscriptionAutoDetect' | 'pluginAuthor' | 'pluginRepository' | 'pluginNpm' | 'pluginLicense' | 'tabToken' | 'tokenExport' | 'tokenExportCsv' | 'tokenReasoningShare' | 'tokenReasoningShort' | 'tokenIo' | 'tokenPeak' | 'tokenDaily' | 'tokenByModel' | 'tokenMiss' | 'tokenHit' | 'tokenOutput' | 'tokenViewStructure' | 'tokenViewModel' | 'tokenHitShort' | 'tokenMissShort' | 'tokenTotal' | 'tokenShare' | 'usageStatsTool' | 'usageStatsToolHint' | 'balanceGranted' | 'balanceTopped' | 'balanceDaily' | 'balanceDaysLong' | 'balanceDaysUnit' | 'popTodayModel' | 'popNoConsumption' | 'popTitle' | 'popDirectLead' | 'popSubLead' | 'unpricedHint' | 'searchEstimateHint' | 'siteListDisplay' | 'siteListDisplayHint' | 'liveCostBar' | 'liveCostBarHint' | 'liveCostBarPosition' | 'liveCostBarPosBelow' | 'liveCostBarPosAbove' | 'liveCostBarPosToolbar' | 'exportCsvSite' | 'panelRelayQuota' | 'relayBalance' | 'relayNoQuota' | 'relayWindowUsed' | 'relayKindNewApi' | 'relayKindSub2Api' | 'relayKindUnknown' | 'expandAll' | 'collapseAll' | 'todayOnly' | 'todayNoConsumption';
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>;
@@ -278,12 +278,12 @@ export interface ModelPrice extends PriceBand {
278
278
  * (主档与 offPeak)单价按 factor 折扣计价与显示,截止时刻起自动恢复刊例价。
279
279
  */
280
280
  export interface PricePromo {
281
- /** 折扣系数(0.5 = 五折);仅 (0,1) 区间有效,非法值视为无促销。 */
281
+ /** 折扣系数(0.5 = 五折,0 = 活动期免费);仅 [0,1) 区间有效,非法值视为无促销。 */
282
282
  factor: number;
283
283
  /**
284
284
  * 分档折扣覆盖:厂商对不同档位给不同折扣时逐档指定(如 GPT-5.6 Sol 促销为
285
285
  * 缓存 0.8 / 输入 0.8 / 输出 2/3)。缺省档位沿用 {@link factor};单档取值
286
- * 仅 (0,1) 区间有效,非法值回落 factor。
286
+ * 仅 [0,1) 区间有效(0 = 该档活动期免费),非法值回落 factor。
287
287
  */
288
288
  factors?: Partial<Record<'input' | 'cacheHit' | 'cacheMiss' | 'output', number>>;
289
289
  /**
@@ -382,8 +382,8 @@ export declare function modelOf(key: string): ModelEntry;
382
382
  */
383
383
  export declare function isPriced(key: string): boolean;
384
384
  /**
385
- * 促销在 nowMs 是否生效:factor 必须落在 (0,1) 区间,截止时刻及之后视为过期;
386
- * endsAtMs 缺省表示长期活动,在 factor 合法期间持续生效。
385
+ * 促销在 nowMs 是否生效:factor 必须落在 [0,1) 区间(0 = 活动期免费,issue #86),
386
+ * 截止时刻及之后视为过期;endsAtMs 缺省表示长期活动,在 factor 合法期间持续生效。
387
387
  * 导出供测试:纯函数。
388
388
  * @param promo - 待判定的促销窗口。
389
389
  * @param nowMs - 判定时刻(epoch ms)。
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.4.21",
4
+ "version": "1.4.23",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "deepseek",