@kenz1117/dsh-ui-usage-billing 1.0.26 → 1.0.28

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
@@ -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 { SessionLogOffset } from "@deepseek-ai/dsh-session/types";
9
10
  import { createHash, createHmac } from "node:crypto";
10
11
  //#region lib/types/client/plan-knowledge.js
11
12
  /**
@@ -1817,12 +1818,6 @@ function foldUsage(acc, usage, key, subscription, timeMs, official = false) {
1817
1818
  }
1818
1819
  }
1819
1820
  /**
1820
- * 联网搜索辅助请求的单次费用估算默认值(人民币元)。DeepSeek 官方对搜索请求
1821
- * (web_search 服务端工具注入上下文)照常计费,实测每次约 0.01~0.03 元,取中值;
1822
- * 部署可在插件配置 `searchCallEstimateCny` 覆盖(设 0 关闭估算)。
1823
- */
1824
- const DEFAULT_SEARCH_CALL_ESTIMATE_CNY = .02;
1825
- /**
1826
1821
  * Fold one auxiliary web-search LLM request (issue #15) into an accumulator.
1827
1822
  * 这类调用绕过对话通道直连官方端点,日志只记请求(无响应/用量事件),token
1828
1823
  * 不可知:按「每次估值」计入费用并单独累计 `searchCalls`,不产生 token 维度。
@@ -2029,26 +2024,22 @@ function turnState(turns, turn) {
2029
2024
  turns.set(turn, fresh);
2030
2025
  return fresh;
2031
2026
  }
2032
- /**
2033
- * Fold one session's events into a {@link SessionFold}. 每个 LLM 调用归属到
2034
- * 其 `assistant/message` 自带 `message.source` 记录的模型(agent-loop 落盘时从
2035
- * 当次请求复制,每个调用一条,不依赖稀疏的 request/header);source 缺失时
2036
- * 兜底到最近一次 request/header 的状态。同时提取最新会话标题、最后活跃时间,
2037
- * 并按轮次折叠每轮费用明细(turn/start → turn/end;调用按 (turn) 归组)。
2038
- * @param events - the session's persisted events in log order.
2039
- * @param subscriptionProviders - provider ids billed through subscription plans.
2040
- * @param officialProviderIds - provider ids treated as the official DeepSeek channel
2041
- * (default: any `deepseek`-prefixed id). Others count as third-party.
2042
- * @param routes - 当前 provider 路由视图(中转站归组)。
2043
- * @param searchCallEstimateCny - 联网搜索请求的单次费用估算(人民币元;0 关闭估算)。
2044
- * @param seedLength - 持久化的 fork 血缘边界(`SessionHeader.seedLength`,缺省 0):
2045
- * fork 子会话日志里 `seq < seedLength` 的事件拷贝自父会话、已在父会话计费,
2046
- * 折叠时跳过以避免重复计费;resume 续写复用同一会话日志(每次续写追加一个
2047
- * `session/end-seed` 标记)但 header 保持原 fork 值,历史段照常计费。
2048
- * @returns the per-session fold (cached by the incremental aggregator).
2049
- */
2050
- function foldSession(events, subscriptionProviders, officialProviderIds, routes = {}, searchCallEstimateCny = DEFAULT_SEARCH_CALL_ESTIMATE_CNY, seedLength = 0) {
2051
- const fold = {
2027
+ /** 空白折叠状态机(与全量折叠的初值逐字一致)。 */
2028
+ function freshMachine() {
2029
+ return {
2030
+ key: "other",
2031
+ subscription: false,
2032
+ official: false,
2033
+ siteBucket: "unknown",
2034
+ turns: /* @__PURE__ */ new Map(),
2035
+ steps: /* @__PURE__ */ new Map(),
2036
+ lastOpenStepKey: void 0,
2037
+ toolSeen: /* @__PURE__ */ new Set()
2038
+ };
2039
+ }
2040
+ /** 空白 fold 的统一构造:foldSession 与聚合器的分片/增量路径共用同一字段初值。 */
2041
+ function freshFold() {
2042
+ return {
2052
2043
  total: emptyUsage(),
2053
2044
  byModel: /* @__PURE__ */ new Map(),
2054
2045
  byDay: /* @__PURE__ */ new Map(),
@@ -2069,14 +2060,29 @@ function foldSession(events, subscriptionProviders, officialProviderIds, routes
2069
2060
  },
2070
2061
  lastActive: 0
2071
2062
  };
2072
- let key = "other";
2073
- let subscription = false;
2074
- let official = false;
2075
- let siteBucket = "unknown";
2076
- const turns = /* @__PURE__ */ new Map();
2077
- const steps = /* @__PURE__ */ new Map();
2078
- let lastOpenStepKey;
2079
- const toolSeen = /* @__PURE__ */ new Set();
2063
+ }
2064
+ /**
2065
+ * 把一批事件折进既有 fold(状态机跨批次延续)。全量折叠、超大会话的分片折叠、
2066
+ * 活跃会话的 seq 增量折叠共用同一段循环体,保证三种路径的字段语义逐字一致。
2067
+ * @param fold - 累计目标(原地修改)。
2068
+ * @param machine - 折叠状态机:进入时为上一批次末态,返回时为本批次末态。
2069
+ * @param events - 本批次事件(日志顺序)。
2070
+ * @param subscriptionProviders - provider ids billed through subscription plans.
2071
+ * @param officialProviderIds - 官方直连 provider 集合(undefined = 按 deepseek 前缀判定)。
2072
+ * @param routes - 当前 provider 路由视图(中转站归组)。
2073
+ * @param searchCallEstimateCny - 联网搜索请求的单次费用估算(人民币元)。
2074
+ * @param seedLength - fork 血缘边界(seq 低于它的事件是父会话种子,跳过);
2075
+ * 来源为存储元数据的 `inheritedEventCount`。
2076
+ */
2077
+ function foldInto(fold, machine, events, subscriptionProviders, officialProviderIds, routes, searchCallEstimateCny, seedLength) {
2078
+ let key = machine.key;
2079
+ let subscription = machine.subscription;
2080
+ let official = machine.official;
2081
+ let siteBucket = machine.siteBucket;
2082
+ const turns = machine.turns;
2083
+ const steps = machine.steps;
2084
+ let lastOpenStepKey = machine.lastOpenStepKey;
2085
+ const toolSeen = machine.toolSeen;
2080
2086
  for (const event of events) {
2081
2087
  if (seedLength > 0 && typeof event.seq === "number" && Number.isFinite(event.seq) && event.seq < seedLength) continue;
2082
2088
  fold.lastActive = Math.max(fold.lastActive, event.time);
@@ -2224,7 +2230,15 @@ function foldSession(events, subscriptionProviders, officialProviderIds, routes
2224
2230
  }
2225
2231
  }
2226
2232
  }
2227
- fold.turns = [...turns.values()].filter((state) => state.input > 0 || state.output > 0).sort((a, b) => a.turn - b.turn).map((state) => ({
2233
+ machine.key = key;
2234
+ machine.subscription = subscription;
2235
+ machine.official = official;
2236
+ machine.siteBucket = siteBucket;
2237
+ machine.lastOpenStepKey = lastOpenStepKey;
2238
+ }
2239
+ /** 从轮次状态派生 fold.turns(每批次结束后重派生;半开轮次跨批次保留)。 */
2240
+ function refreshTurns(fold, machine) {
2241
+ fold.turns = [...machine.turns.values()].filter((state) => state.input > 0 || state.output > 0).sort((a, b) => a.turn - b.turn).map((state) => ({
2228
2242
  turn: state.turn,
2229
2243
  model: state.model,
2230
2244
  input: state.input,
@@ -2235,7 +2249,6 @@ function foldSession(events, subscriptionProviders, officialProviderIds, routes
2235
2249
  startedAt: state.startedAt === Number.MAX_SAFE_INTEGER ? fold.lastActive : state.startedAt,
2236
2250
  ...state.endedAt === void 0 ? {} : { endedAt: state.endedAt }
2237
2251
  }));
2238
- return fold;
2239
2252
  }
2240
2253
  /**
2241
2254
  * 生成一个 step 的性能样本;无效 / 超出 sane 上限(15 分钟)时返回 undefined,
@@ -2315,6 +2328,23 @@ function createUsageAggregator(persistence, options = {}) {
2315
2328
  /** 每次聚合取最新的 provider 路由视图(中转站零配置发现);缺省按空处理(全部未知路由)。 */
2316
2329
  const routesOf = () => options.resolveRoutes?.() ?? {};
2317
2330
  const searchEstimate = options.searchCallEstimateCny ?? .02;
2331
+ const FOLD_CHUNK_EVENTS = 8e3;
2332
+ /** 把一份(全量或增量)折叠结果登记进 durable ledger;内容无变化时不落盘。 */
2333
+ const recordLedger = (id, cwd, stamp, fold) => {
2334
+ if (options.ledger === void 0) return;
2335
+ const entry = {
2336
+ id,
2337
+ ...cwd === void 0 ? {} : { cwd },
2338
+ ...stamp === null ? {} : { stamp },
2339
+ foldVersion: 7,
2340
+ fold: serializeFold(fold)
2341
+ };
2342
+ const row = ledger.get(id);
2343
+ if (row === void 0 || row.stamp !== entry.stamp || row.cwd !== entry.cwd || row.foldVersion !== entry.foldVersion || stamp === null && JSON.stringify(row.fold) !== JSON.stringify(entry.fold)) {
2344
+ ledger.set(id, entry);
2345
+ ledgerNeedsSave = true;
2346
+ }
2347
+ };
2318
2348
  const ensureLedgerLoaded = async () => {
2319
2349
  if (ledgerLoaded || options.ledger === void 0) return;
2320
2350
  ledgerLoaded = true;
@@ -2376,34 +2406,77 @@ function createUsageAggregator(persistence, options = {}) {
2376
2406
  continue;
2377
2407
  }
2378
2408
  try {
2379
- const { events } = await persistence.readFrom(meta.id, 0);
2409
+ const cwd = meta.cwd;
2410
+ const ledgerRow = ledger.get(id);
2411
+ if (ledgerRow !== void 0 && ledgerRow.stamp !== void 0 && stamp !== null && ledgerRow.stamp === stamp && (ledgerRow.foldVersion ?? 1) === 7) {
2412
+ const fold = deserializeFold(ledgerRow.fold);
2413
+ cache.set(id, {
2414
+ stamp,
2415
+ fold
2416
+ });
2417
+ folds.push({
2418
+ id,
2419
+ ...cwd === void 0 ? {} : { cwd },
2420
+ fold
2421
+ });
2422
+ included.add(id);
2423
+ continue;
2424
+ }
2425
+ const previous = cache.get(id);
2426
+ if (previous?.machine !== void 0 && previous.lastSeq !== void 0) {
2427
+ const from = previous.lastSeq + 1;
2428
+ const { events, inheritedEventCount } = await persistence.readFrom(meta.id, SessionLogOffset(from));
2429
+ const after = await stampOf(meta);
2430
+ if (stamp !== null && after !== stamp) {
2431
+ cache.delete(id);
2432
+ continue;
2433
+ }
2434
+ const first = events[0]?.seq;
2435
+ if (events.length === 0 || first !== from) {
2436
+ cache.delete(id);
2437
+ continue;
2438
+ }
2439
+ foldInto(previous.fold, previous.machine, events, subscriptionProviders, officialProviderIds, routesOf(), searchEstimate, inheritedEventCount);
2440
+ refreshTurns(previous.fold, previous.machine);
2441
+ previous.lastSeq = events[events.length - 1]?.seq ?? previous.lastSeq;
2442
+ previous.stamp = stamp;
2443
+ cache.delete(id);
2444
+ cache.set(id, previous);
2445
+ folds.push({
2446
+ id,
2447
+ ...cwd === void 0 ? {} : { cwd },
2448
+ fold: previous.fold
2449
+ });
2450
+ included.add(id);
2451
+ recordLedger(id, cwd, stamp, previous.fold);
2452
+ continue;
2453
+ }
2454
+ const { events, inheritedEventCount } = await persistence.readFrom(meta.id, SessionLogOffset(0));
2380
2455
  const after = await stampOf(meta);
2381
2456
  if (stamp !== null && after !== stamp) continue;
2382
- const fold = foldSession(events, subscriptionProviders, officialProviderIds, routesOf(), searchEstimate, meta.seedLength ?? 0);
2457
+ const fold = freshFold();
2458
+ const machine = freshMachine();
2459
+ for (let start = 0; start < events.length; start += FOLD_CHUNK_EVENTS) {
2460
+ foldInto(fold, machine, events.slice(start, start + FOLD_CHUNK_EVENTS), subscriptionProviders, officialProviderIds, routesOf(), searchEstimate, inheritedEventCount);
2461
+ if (start + FOLD_CHUNK_EVENTS < events.length) await new Promise((resolve) => {
2462
+ setImmediate(resolve);
2463
+ });
2464
+ }
2465
+ refreshTurns(fold, machine);
2466
+ const last = events[events.length - 1];
2383
2467
  cache.set(id, {
2384
2468
  stamp,
2385
- fold
2469
+ fold,
2470
+ machine,
2471
+ ...last !== void 0 && typeof last.seq === "number" ? { lastSeq: last.seq } : {}
2386
2472
  });
2387
2473
  folds.push({
2388
2474
  id,
2389
- ...meta.cwd === void 0 ? {} : { cwd: meta.cwd },
2475
+ ...cwd === void 0 ? {} : { cwd },
2390
2476
  fold
2391
2477
  });
2392
2478
  included.add(id);
2393
- if (options.ledger !== void 0) {
2394
- const entry = {
2395
- id,
2396
- ...meta.cwd === void 0 ? {} : { cwd: meta.cwd },
2397
- ...stamp === null ? {} : { stamp },
2398
- foldVersion: 7,
2399
- fold: serializeFold(fold)
2400
- };
2401
- const previous = ledger.get(id);
2402
- if (previous === void 0 || previous.stamp !== entry.stamp || previous.cwd !== entry.cwd || previous.foldVersion !== entry.foldVersion || stamp === null && JSON.stringify(previous.fold) !== JSON.stringify(entry.fold)) {
2403
- ledger.set(id, entry);
2404
- ledgerNeedsSave = true;
2405
- }
2406
- }
2479
+ recordLedger(id, cwd, stamp, fold);
2407
2480
  } catch (error) {
2408
2481
  skipped.push(id);
2409
2482
  console.warn("[usage-billing] skip unreadable session", id, error);
@@ -475,10 +475,10 @@ export declare function messageTextLength(message: unknown): number;
475
475
  * (default: any `deepseek`-prefixed id). Others count as third-party.
476
476
  * @param routes - 当前 provider 路由视图(中转站归组)。
477
477
  * @param searchCallEstimateCny - 联网搜索请求的单次费用估算(人民币元;0 关闭估算)。
478
- * @param seedLength - 持久化的 fork 血缘边界(`SessionHeader.seedLength`,缺省 0):
478
+ * @param seedLength - 持久化的 fork 血缘边界(存储元数据 `inheritedEventCount`,缺省 0):
479
479
  * fork 子会话日志里 `seq < seedLength` 的事件拷贝自父会话、已在父会话计费,
480
480
  * 折叠时跳过以避免重复计费;resume 续写复用同一会话日志(每次续写追加一个
481
- * `session/end-seed` 标记)但 header 保持原 fork 值,历史段照常计费。
481
+ * `session/end-seed` 标记)但继承切割保持原值,历史段照常计费。
482
482
  * @returns the per-session fold (cached by the incremental aggregator).
483
483
  */
484
484
  export declare function foldSession(events: readonly {
@@ -78,4 +78,14 @@ export interface LiveCostBarProps {
78
78
  * @param props - framework session identity and locale.
79
79
  */
80
80
  export declare function LiveCostBar({ sessionId, t }: LiveCostBarProps): React.ReactNode;
81
+ /**
82
+ * Render the compact inline chip for the composer tool row (position
83
+ * 「模型选择前」). Occupies a fixed minimal footprint — tier glyph + session
84
+ * spend — so it never crowds the model picker; the full breakdown (switch
85
+ * countdown, turn cost, quota alerts) rides the host Tooltip primitive (the
86
+ * native title never renders in the webview), and a low quota tints the chip
87
+ * amber/red instead of growing it.
88
+ * @param props - framework session identity and locale.
89
+ */
90
+ export declare function LiveCostChip({ sessionId, t }: LiveCostBarProps): React.ReactNode;
81
91
  //# sourceMappingURL=live-cost.d.ts.map
@@ -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' | 'trend' | 'trend7d' | 'trend30d' | 'trendMetric' | 'trendMetricCost' | 'trendMetricTokens' | 'trendEmpty' | 'budget' | 'budgetAmount' | 'budgetSummary' | 'sessions' | 'sessionTitle' | 'project' | 'lastActive' | 'sessionOverflow' | 'budgetTierBody' | 'models' | 'providerBilling' | 'actual' | 'pricing' | 'input' | 'output' | 'cacheHit' | 'peak' | 'offPeak' | 'flat' | 'band' | 'close' | 'footer' | 'footerCredit' | 'lastUpdated' | 'noData' | 'todayRate' | 'rateLive' | 'rateBuiltin' | 'promoBadge' | 'promoUntil' | 'promoOpenEnded' | 'pricingTip' | 'pricingUnit' | 'pricingNotes' | 'ubPeak' | 'ubOff' | 'ubStd' | 'peakBand' | 'pricingSource' | 'noteCache' | 'noteBand' | 'noteSource' | 'balance' | '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' | 'floatNoTargets' | 'floatNoTargetsHint' | 'cardDisplay' | 'cardDisplayHint' | 'cardMetric' | 'cardMetricMoney' | 'cardMetricTokens' | 'triggerMonthTokens' | 'subscriptionsStale' | 'staleLedgerNotice' | 'tokenCacheWrite' | 'toolRank' | 'toolName' | 'userPrices' | 'userPricesHint' | 'userPriceSave' | 'userPriceModel' | 'userPriceSource' | 'userPriceSourceHint' | 'userPriceOffPeak' | 'userPriceCurrency' | 'userPriceAdd' | 'userPriceRemove' | 'sessionStaleBadge' | 'heatmapLess' | 'heatmapMore' | 'currency' | 'currencyCny' | 'currencyUsd' | 'heatmap' | 'rounds' | 'roundsHint' | 'anomaly' | 'workspaces' | 'workspacesHint' | 'model' | 'thModel' | 'thInputMiss' | 'thInputHit' | 'costAbbr' | 'tabOverview' | 'tabTrends' | 'tabProviders' | 'tabPricing' | 'tabSettings' | 'budgetHint' | 'peakAlertHint' | 'peakAlertDescPeak' | 'peakAlertDescOff' | 'export' | 'exportCsvDay' | 'exportCsvSession' | 'exportJson' | 'peakShare' | 'peakSharePerCall' | 'offPeakSavings' | 'perfMax' | 'weekCost' | 'roleCost' | 'roleUser' | 'roleAssistant' | 'roleTool' | 'roleHint' | 'tierPeak' | 'tierOff' | 'tierToPeak' | 'tierToOff' | 'tierAlertEnterPeak' | 'tierAlertEnterOff' | 'peakAlertTitlePeak' | 'peakAlertTitleOff' | 'peakAlert' | 'peakAlertLeadMin' | 'peakAlertPos' | 'peakAlertMode' | 'peakAlertPosCorner' | 'peakAlertPosCenter' | 'peakAlertModePeak' | 'peakAlertModeOff' | 'peakAlertModeBoth' | 'peakAlertWebNotify' | 'peakAlertPreview' | 'planTypeCode' | 'planTypeToken' | 'subscriptionFeePerMonth' | 'triggerToday' | 'triggerMonth' | 'subscriptionIncluded' | 'free' | 'official' | 'thirdParty' | 'perfSamples' | 'perfTtft' | 'perfP50' | 'perfP90' | 'perfTps' | 'perfLatency' | 'perfEstimated' | 'perfEmpty' | 'perfTpsUnit' | 'perfTitle' | 'perfHint' | 'perfAll' | 'perfChartEmpty' | 'heatmapYear' | 'heatmapMonth' | 'activeDays' | 'streakDays' | 'subscriptionAutoDetect' | 'pluginVersion' | 'pluginAuthor' | 'pluginRepository' | 'pluginNpm' | 'pluginLicense' | 'tabToken' | 'tokenExport' | 'tokenExportCsv' | 'tokenCacheHitRate' | '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' | 'exportCsvSite' | 'panelRelayQuota' | 'relayBalance' | 'relayNoQuota' | 'relayWindowUsed' | 'relayKindNewApi' | 'relayKindSub2Api' | 'relayKindUnknown';
2
+ export type UsageBillingKey = 'title' | 'cost' | 'todayCost' | 'monthCost' | 'yearCost' | 'monthProjected' | 'liveTurn' | 'liveSession' | 'calls' | 'cacheHitRate' | 'tokens' | 'inputTokens' | 'outputTokens' | 'avgCost' | 'trend' | 'trend7d' | 'trend30d' | 'trendMetric' | 'trendMetricCost' | 'trendMetricTokens' | 'trendEmpty' | 'budget' | 'budgetAmount' | 'budgetSummary' | 'sessions' | 'sessionTitle' | 'project' | 'lastActive' | 'sessionOverflow' | 'budgetTierBody' | 'models' | 'providerBilling' | 'actual' | 'pricing' | 'input' | 'output' | 'cacheHit' | 'peak' | 'offPeak' | 'flat' | 'band' | 'close' | 'footer' | 'footerCredit' | 'lastUpdated' | 'noData' | 'todayRate' | 'rateLive' | 'rateBuiltin' | 'promoBadge' | 'promoUntil' | 'promoOpenEnded' | 'pricingTip' | 'pricingUnit' | 'pricingNotes' | 'ubPeak' | 'ubOff' | 'ubStd' | 'peakBand' | 'pricingSource' | 'noteCache' | 'noteBand' | 'noteSource' | 'balance' | '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' | 'floatNoTargets' | 'floatNoTargetsHint' | 'cardDisplay' | 'cardDisplayHint' | 'cardMetric' | 'cardMetricMoney' | 'cardMetricTokens' | 'triggerMonthTokens' | 'subscriptionsStale' | 'staleLedgerNotice' | 'tokenCacheWrite' | 'toolRank' | 'toolName' | 'userPrices' | 'userPricesHint' | 'userPriceSave' | 'userPriceModel' | 'userPriceSource' | 'userPriceSourceHint' | 'userPriceOffPeak' | 'userPriceNormal' | 'userPriceCurrency' | 'userPriceAdd' | 'userPriceRemoveSelected' | 'userPriceSelect' | 'sessionStaleBadge' | 'heatmapLess' | 'heatmapMore' | 'currency' | 'currencyCny' | 'currencyUsd' | 'heatmap' | 'rounds' | 'roundsHint' | 'anomaly' | 'workspaces' | 'workspacesHint' | 'model' | 'thModel' | 'thInputMiss' | 'thInputHit' | 'costAbbr' | 'tabOverview' | 'tabTrends' | 'tabProviders' | 'tabPricing' | 'tabSettings' | 'budgetHint' | 'peakAlertHint' | 'peakAlertDescPeak' | 'peakAlertDescOff' | 'export' | 'exportCsvDay' | 'exportCsvSession' | 'exportJson' | 'peakShare' | 'peakSharePerCall' | 'offPeakSavings' | 'perfMax' | 'weekCost' | 'roleCost' | 'roleUser' | 'roleAssistant' | 'roleTool' | 'roleHint' | 'tierPeak' | 'tierOff' | 'tierToPeak' | 'tierToOff' | 'tierAlertEnterPeak' | 'tierAlertEnterOff' | 'peakAlertTitlePeak' | 'peakAlertTitleOff' | 'peakAlert' | 'peakAlertLeadMin' | 'peakAlertPos' | 'peakAlertMode' | 'peakAlertPosCorner' | 'peakAlertPosCenter' | 'peakAlertModePeak' | 'peakAlertModeOff' | 'peakAlertModeBoth' | 'peakAlertWebNotify' | 'peakAlertPreview' | 'planTypeCode' | 'planTypeToken' | 'subscriptionFeePerMonth' | 'triggerToday' | 'triggerMonth' | 'subscriptionIncluded' | 'free' | 'official' | 'thirdParty' | 'perfSamples' | 'perfTtft' | 'perfP50' | 'perfP90' | 'perfTps' | 'perfLatency' | 'perfEstimated' | 'perfEmpty' | 'perfTpsUnit' | 'perfTitle' | 'perfHint' | 'perfAll' | 'perfChartEmpty' | 'heatmapYear' | 'heatmapMonth' | 'activeDays' | 'streakDays' | 'subscriptionAutoDetect' | 'pluginVersion' | 'pluginAuthor' | 'pluginRepository' | 'pluginNpm' | 'pluginLicense' | 'tabToken' | 'tokenExport' | 'tokenExportCsv' | 'tokenCacheHitRate' | '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';
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>;
@@ -165,9 +165,15 @@ export declare function isPeakHour(beijingHour: number): boolean;
165
165
  */
166
166
  export declare function tierAt(timeMs: number | null | undefined): PriceTierId;
167
167
  /**
168
- * 当前峰谷档位与距下次切换的时长。导出供测试:纯函数。
168
+ * 当前峰谷档位与距下一切换的时长。导出供测试:纯函数。
169
+ *
170
+ * 下一切换点统一定义为档位真正变化的最近边界:自当前时刻起逐天扫描工作日的
171
+ * 09:00 / 12:00 / 14:00 / 18:00,候选时刻的档位由 {@link tierAt} 判定——
172
+ * 周末(周六/周日)北京全天低谷、没有边界,扫描自然跳过;工作日深夜跨周末
173
+ * 时落到周一 09:00 而非周末伪边界(issue #33)。
174
+ * 最坏情形(周五 18:00 后 → 周一 09:00)约 63h,7 天窗口必然覆盖。
169
175
  * @param nowMs - 当前时刻(epoch 毫秒)。
170
- * @returns 当前档位与到下一个切换边界的毫秒数。
176
+ * @returns 当前档位与到下一切换边界的毫秒数。
171
177
  */
172
178
  export declare function tierCountdown(nowMs: number): {
173
179
  tier: PriceTierId;
@@ -82,8 +82,10 @@ export declare function saveSiteListPrefs(prefs: SiteListPrefs): void;
82
82
  export interface LiveCostBarPrefs {
83
83
  /** 是否显示输入框下方的即时代费条胶囊(默认 true:保持历史行为)。 */
84
84
  show: boolean;
85
+ /** 胶囊位置:below = 输入框下方(默认);above = 输入框上方;toolbar = 工具行模型选择前的内联 chip。 */
86
+ position: 'below' | 'above' | 'toolbar';
85
87
  }
86
- /** 默认即时代费条偏好:显示(升级用户零感知)。 */
88
+ /** 默认即时代费条偏好:显示在输入框下方(升级用户零感知)。 */
87
89
  export declare const DEFAULT_LIVE_COST_BAR_PREFS: LiveCostBarPrefs;
88
90
  /** localStorage key(与其他 `dsh.ui-usage-billing.*` 偏好同命名空间)。 */
89
91
  export declare const LIVE_COST_BAR_STORAGE_KEY = "dsh.ui-usage-billing.livecost";
@@ -12,7 +12,7 @@
12
12
  import type { IncomingMessage, ServerResponse } from 'node:http';
13
13
  import type { Context } from '@deepseek-ai/cordis';
14
14
  import type { CredentialProvider } from '@deepseek-ai/dsh-credentials';
15
- import { type SettingsProvider } from '@deepseek-ai/dsh-settings';
15
+ import type { SettingsProvider } from '@deepseek-ai/dsh-settings';
16
16
  import { type UsageLedgerStore } from './aggregate.ts';
17
17
  import type { CustomBalanceConfig, DeclaredEndpointConfig, SubscriptionPlanConfig } from './pricing-shared.ts';
18
18
  import { type IdentifiedSubscriptionPlan, type SubscriptionKeys } from './subscriptions.ts';
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.0.26",
4
+ "version": "1.0.28",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "deepseek",
@@ -63,6 +63,12 @@
63
63
  }
64
64
  }
65
65
  },
66
+ "scripts": {
67
+ "build": "tsc -b tsconfig.json && tsdown",
68
+ "bundle": "tsdown",
69
+ "watch": "tsdown --watch",
70
+ "test": "vitest run"
71
+ },
66
72
  "license": "MIT",
67
73
  "engines": {
68
74
  "node": "^22.19.0 || >=24.0.0"
@@ -96,6 +102,7 @@
96
102
  "@deepseek-ai/cordis": "*",
97
103
  "@deepseek-ai/cordis-plugin-include": "^1.0.7",
98
104
  "@deepseek-ai/cordis-plugin-loader": "^1.0.3",
105
+ "@deepseek-ai/dsh-api-gateway": "0.1.2-rc.1",
99
106
  "@deepseek-ai/dsh-api-remotes": "0.1.2-rc.1",
100
107
  "@deepseek-ai/dsh-api-session-controller": "0.1.2-rc.1",
101
108
  "@deepseek-ai/dsh-atomic-write": "0.1.2-rc.1",
@@ -103,6 +110,8 @@
103
110
  "@deepseek-ai/dsh-client-store": "0.1.2-rc.1",
104
111
  "@deepseek-ai/dsh-client-ui-conversation": "0.1.2-rc.1",
105
112
  "@deepseek-ai/dsh-client-ui-primitives": "0.1.2-rc.1",
113
+ "@deepseek-ai/dsh-client-ui-renderer": "0.1.2-rc.1",
114
+ "@deepseek-ai/dsh-client-ui-sidebar": "0.1.2-rc.1",
106
115
  "@deepseek-ai/dsh-client-ui-slots": "0.1.2-rc.1",
107
116
  "@deepseek-ai/dsh-credentials": "0.1.2-rc.1",
108
117
  "@deepseek-ai/dsh-host-webserver": "0.1.2-rc.1",
@@ -113,6 +122,7 @@
113
122
  "@deepseek-ai/dsh-session-persistence": "0.1.2-rc.1",
114
123
  "@deepseek-ai/dsh-settings": "0.1.2-rc.1",
115
124
  "@deepseek-ai/dsh-tools": "0.1.2-rc.1",
125
+ "@deepseek-ai/dsh-typert-protocol": "0.1.2-rc.1",
116
126
  "@deepseek-ai/schemastery": "^3.18.2",
117
127
  "@testing-library/dom": "^10.4.1",
118
128
  "@testing-library/react": "^16.1.0",
@@ -124,6 +134,7 @@
124
134
  "react-dom": "^18.2.0",
125
135
  "semver": "^7.7.2",
126
136
  "tsdown": "^0.22.2",
137
+ "typescript": "^6.0.3",
127
138
  "use-sync-external-store": "1.2.0",
128
139
  "vitest": "^4.1.8"
129
140
  },
@@ -133,10 +144,5 @@
133
144
  "lib/client.js",
134
145
  "lib/types/**/*.d.ts",
135
146
  "cordis.patch.yml"
136
- ],
137
- "scripts": {
138
- "bundle": "tsdown",
139
- "watch": "tsdown --watch",
140
- "test": "vitest run"
141
- }
142
- }
147
+ ]
148
+ }