@kenz1117/dsh-ui-usage-billing 1.0.20 → 1.1.1

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
@@ -7,7 +7,6 @@ import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
7
7
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
8
8
  import { settingsNamespace } from "@deepseek-ai/dsh-settings";
9
9
  import z from "@deepseek-ai/schemastery";
10
- import { createHash, createHmac } from "node:crypto";
11
10
  //#region lib/types/client/plan-knowledge.js
12
11
  /**
13
12
  * Plan-knowledge reference (adapted from dsh-spend's `knowledge.js`, MIT):
@@ -1635,31 +1634,24 @@ function computeCost(entry, buckets, peakShare = DEFAULT_PEAK_SHARE, nowMs = Dat
1635
1634
  return peak * peakShare + off * (1 - peakShare);
1636
1635
  }
1637
1636
  /**
1638
- * v1 峰谷档判定(峰谷开闸起、周末全谷分界止):不豁免周末——该时段官方
1639
- * 高峰时段为每天 9-12 / 14-18(周六日同样计峰)。仅用于历史事件计费;
1640
- * 「当前时刻」的档位(提醒/时段条/费率展示)一律走 {@link tierAt} 现行规则。
1641
- */
1642
- function tariffV1At(timeMs) {
1643
- return isPeakHour((new Date(timeMs).getUTCHours() + 8) % 24) ? "peak" : "offPeak";
1644
- }
1645
- /**
1646
1637
  * 按调用时刻精确判定高峰/空闲档并计价(P0-1:替代固定比例混合)。时刻未知
1647
1638
  * (null/NaN,理论不发生在真实事件流)时回退 {@link DEFAULT_PEAK_SHARE} 混合,
1648
1639
  * 保持旧语义不低估。平档模型(无 offPeak)两个时段同价。限时促销与峰谷档
1649
1640
  * 同口径:按事件时刻判定该笔流量当时享受的单价。
1650
- *
1651
- * 历史正确性(按变更节点分段适用规则,不统一套现行价重算历史):
1652
- * - 早于 {@link PEAK_ERA_START_MS} 的事件按当时官方基础价
1653
- * ({@link LEGACY_DEEPSEEK_BANDS})计费;
1654
- * - 峰谷开闸至 {@link WEEKEND_OFFPEAK_START_MS} 之间按 v1 规则(周末不豁免,
1655
- * 周六日 9-12 / 14-18 计峰);
1656
- * - 周末全谷分界起按现行规则({@link tierAt},周六日全天低谷)。
1657
1641
  * @param entry - the catalog entry whose prices apply.
1658
1642
  * @param buckets - token usage counts.
1659
1643
  * @param timeMs - the call's wall-clock time (epoch ms); null falls back to the peak-share mix.
1660
1644
  * @param peakShare - fallback mix used only when `timeMs` is missing.
1661
1645
  * @returns the estimated cost in CNY(USD 计价模型已按当前汇率折算)。
1662
1646
  */
1647
+ /**
1648
+ * v1 峰谷档判定(峰谷开闸起、周末全谷分界止):不豁免周末——该时段官方
1649
+ * 高峰时段为每天 9-12 / 14-18(周六日同样计峰)。仅用于历史事件计费;
1650
+ * 「当前时刻」的档位(提醒/时段条/费率展示)一律走 {@link tierAt} 现行规则。
1651
+ */
1652
+ function tariffV1At(timeMs) {
1653
+ return isPeakHour((new Date(timeMs).getUTCHours() + 8) % 24) ? "peak" : "offPeak";
1654
+ }
1663
1655
  function computeCostAt(entry, buckets, timeMs, peakShare = DEFAULT_PEAK_SHARE) {
1664
1656
  if (timeMs === null || timeMs === void 0 || !Number.isFinite(timeMs)) return computeCost(entry, buckets, peakShare);
1665
1657
  const priced = applyPromo(entry, timeMs);
@@ -2397,7 +2389,7 @@ function createUsageAggregator(persistence, options = {}) {
2397
2389
  id,
2398
2390
  ...meta.cwd === void 0 ? {} : { cwd: meta.cwd },
2399
2391
  ...stamp === null ? {} : { stamp },
2400
- foldVersion: 6,
2392
+ foldVersion: 5,
2401
2393
  fold: serializeFold(fold)
2402
2394
  };
2403
2395
  const previous = ledger.get(id);
@@ -2421,7 +2413,7 @@ function createUsageAggregator(persistence, options = {}) {
2421
2413
  for (const entry of ledger.values()) {
2422
2414
  if (included.has(entry.id)) continue;
2423
2415
  try {
2424
- const stale = (entry.foldVersion ?? 1) < 6;
2416
+ const stale = (entry.foldVersion ?? 1) < 5;
2425
2417
  folds.push({
2426
2418
  id: entry.id,
2427
2419
  ...entry.cwd === void 0 ? {} : { cwd: entry.cwd },
@@ -2467,7 +2459,7 @@ function createUsageAggregator(persistence, options = {}) {
2467
2459
  outputCost: 0
2468
2460
  };
2469
2461
  const perfModel = /* @__PURE__ */ new Map();
2470
- const perfHourModel = /* @__PURE__ */ new Map();
2462
+ const perfHour = /* @__PURE__ */ new Map();
2471
2463
  for (const { id: sessionId, cwd, fold, staleLedger } of folds) {
2472
2464
  mergeUsageInto(total, fold.total);
2473
2465
  roles.userChars += fold.roles.userChars;
@@ -2498,18 +2490,13 @@ function createUsageAggregator(persistence, options = {}) {
2498
2490
  if (sample.tps !== void 0) modelAccum.tps.push(sample.tps);
2499
2491
  if (sample.latencyMs !== void 0) modelAccum.latencies.push(sample.latencyMs);
2500
2492
  if (sample.estimated) modelAccum.estimated += 1;
2501
- let hourModels = perfHourModel.get(sample.hour);
2502
- if (hourModels === void 0) {
2503
- hourModels = /* @__PURE__ */ new Map();
2504
- perfHourModel.set(sample.hour, hourModels);
2505
- }
2506
- let hourAccum = hourModels.get(sample.model);
2493
+ let hourAccum = perfHour.get(sample.hour);
2507
2494
  if (hourAccum === void 0) {
2508
2495
  hourAccum = {
2509
2496
  ttfts: [],
2510
2497
  tps: []
2511
2498
  };
2512
- hourModels.set(sample.model, hourAccum);
2499
+ perfHour.set(sample.hour, hourAccum);
2513
2500
  }
2514
2501
  hourAccum.ttfts.push(sample.ttftMs);
2515
2502
  if (sample.tps !== void 0) hourAccum.tps.push(sample.tps);
@@ -2569,14 +2556,14 @@ function createUsageAggregator(persistence, options = {}) {
2569
2556
  latencyAvg: acc.latencies.length === 0 ? 0 : mean(acc.latencies),
2570
2557
  estimatedSamples: acc.estimated
2571
2558
  }])),
2572
- byHourModel: Object.fromEntries([...perfHourModel].map(([hour, models]) => [hour, Object.fromEntries([...models].map(([model, acc]) => [model, {
2559
+ byHour: Object.fromEntries([...perfHour].map(([hour, acc]) => [hour, {
2573
2560
  samples: acc.ttfts.length,
2574
2561
  ttftAvg: mean(acc.ttfts),
2575
2562
  ...acc.tps.length === 0 ? {} : { tpsAvg: mean(acc.tps) }
2576
- }]))]))
2563
+ }]))
2577
2564
  };
2578
2565
  lastDoc = {
2579
- version: 4,
2566
+ version: 3,
2580
2567
  updatedAt: now,
2581
2568
  source: "session-logs",
2582
2569
  timezone: hostTimeZone(),
@@ -2975,26 +2962,6 @@ const QUERIERS = [
2975
2962
  route: "zai-coding-cn",
2976
2963
  displayName: "智谱 AI",
2977
2964
  querier: queryZhipu
2978
- },
2979
- {
2980
- route: "tencent-tokenhub",
2981
- displayName: "腾讯云 TokenHub",
2982
- querier: queryTencentTokenPlan
2983
- },
2984
- {
2985
- route: "tokenhub",
2986
- displayName: "腾讯云 TokenHub",
2987
- querier: queryTencentTokenPlan
2988
- },
2989
- {
2990
- route: "tencent",
2991
- displayName: "腾讯云 TokenHub",
2992
- querier: queryTencentTokenPlan
2993
- },
2994
- {
2995
- route: "tencentcloud",
2996
- displayName: "腾讯云 TokenHub",
2997
- querier: queryTencentTokenPlan
2998
2965
  }
2999
2966
  ];
3000
2967
  /**
@@ -3027,200 +2994,6 @@ async function queryBalances(ctx, providers) {
3027
2994
  return querier(ctx, env);
3028
2995
  }));
3029
2996
  }
3030
- /** TokenHub 管控面 API 端点(cloud.tencent.cn/document/api/1823/132270)。 */
3031
- const TOKENHUB_HOST = "tokenhub.tencentcloudapi.com";
3032
- /** 云 API 3.0 产品名与版本(签名的 service 段与请求头都必须一致)。 */
3033
- const TOKENHUB_SERVICE = "tokenhub";
3034
- const TOKENHUB_VERSION = "2026-03-22";
3035
- /** 请求地域:管控面对地域不敏感,取默认国内地域(文档地域列表含 ap-guangzhou)。 */
3036
- const TOKENHUB_REGION = "ap-guangzhou";
3037
- /** 腾讯云凭据引用值格式:`<SecretId>:<SecretKey>`(分隔符取首个冒号)。 */
3038
- function parseTencentCredential(value) {
3039
- const sep = value.indexOf(":");
3040
- if (sep === -1) return void 0;
3041
- const secretId = value.slice(0, sep).trim();
3042
- const secretKey = value.slice(sep + 1).trim();
3043
- if (secretId === "" || secretKey === "") return void 0;
3044
- return {
3045
- secretId,
3046
- secretKey
3047
- };
3048
- }
3049
- /**
3050
- * 构造云 API 3.0 TC3-HMAC-SHA256 签名(官方签名方法 v3)。导出供测试:纯函数,
3051
- * 输入确定则签名确定。Action 不参与签名——它走 `X-TC-Action` 请求头。
3052
- * @param secretId - 云 API SecretId。
3053
- * @param secretKey - 云 API SecretKey。
3054
- * @param payload - 已序列化的请求体(含 Action/Version/Region 公共参数)。
3055
- * @param timestamp - 签名时间戳(秒)。
3056
- * @returns Authorization 头的值。
3057
- */
3058
- function tc3Authorization(secretId, secretKey, payload, timestamp) {
3059
- const date = (/* @__PURE__ */ new Date(timestamp * 1e3)).toISOString().slice(0, 10);
3060
- 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")}`;
3061
- const hashedCanonical = createHash("sha256").update(canonicalRequest).digest("hex");
3062
- const stringToSign = `TC3-HMAC-SHA256\n${String(timestamp)}\n${date}/${TOKENHUB_SERVICE}/tc3_request\n${hashedCanonical}`;
3063
- return `TC3-HMAC-SHA256 Credential=${secretId}/${date}/${TOKENHUB_SERVICE}/tc3_request, SignedHeaders=content-type;host, Signature=${createHmac("sha256", createHmac("sha256", createHmac("sha256", createHmac("sha256", date).update(secretKey).digest()).update(TOKENHUB_SERVICE).digest()).update("tc3_request").digest()).update(stringToSign).digest("hex")}`;
3064
- }
3065
- /** 调用一次 TokenHub 管控面接口:TC3 签名 + 超时保护,返回响应 JSON 的 `Response`。 */
3066
- async function callTokenHub(secretId, secretKey, action, params) {
3067
- const payload = JSON.stringify({
3068
- Action: action,
3069
- Version: TOKENHUB_VERSION,
3070
- Region: TOKENHUB_REGION,
3071
- ...params
3072
- });
3073
- const timestamp = Math.floor(Date.now() / 1e3);
3074
- const controller = new AbortController();
3075
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS$3);
3076
- try {
3077
- const response = await fetch(`https://${TOKENHUB_HOST}/`, {
3078
- method: "POST",
3079
- headers: {
3080
- "content-type": "application/json; charset=utf-8",
3081
- host: TOKENHUB_HOST,
3082
- "x-tc-action": action.toLowerCase(),
3083
- "x-tc-version": TOKENHUB_VERSION,
3084
- "x-tc-region": TOKENHUB_REGION,
3085
- "x-tc-timestamp": String(timestamp),
3086
- authorization: tc3Authorization(secretId, secretKey, payload, timestamp)
3087
- },
3088
- body: payload,
3089
- signal: controller.signal
3090
- });
3091
- if (response.status === 401 || response.status === 403) throw Object.assign(/* @__PURE__ */ new Error("unauthorized"), { code: "unauthorized" });
3092
- if (!response.ok) throw Object.assign(/* @__PURE__ */ new Error(`HTTP ${String(response.status)}`), {
3093
- httpStatus: response.status,
3094
- code: "unreachable"
3095
- });
3096
- const inner = (await response.json()).Response;
3097
- if (inner === void 0) throw Object.assign(/* @__PURE__ */ new Error("no Response envelope"), { code: "invalid" });
3098
- if (inner.Error !== void 0 && inner.Error !== null) {
3099
- const err = inner.Error;
3100
- const code = err.Code === "AuthFailure.SignatureFailure" || err.Code === "AuthFailure.SecretIdNotFound" ? "unauthorized" : "unreachable";
3101
- throw Object.assign(new Error(String(err.Code ?? "api-error")), { code });
3102
- }
3103
- return inner;
3104
- } finally {
3105
- clearTimeout(timer);
3106
- }
3107
- }
3108
- /**
3109
- * 在套餐余量对象里防御性提取「剩余额度」:官方 SubPackageBalance/PackageInfo
3110
- * 的字段名未稳定公开(issue #18 调研期),按语义键名扫描——命中 remaining /
3111
- * balance / left 语义键直接用;命中 total 与 used 则相减推导。数字一律经
3112
- * {@link toNumber} 归一化(上游可能给字符串)。
3113
- * 导出供测试:纯函数。
3114
- * @param source - 套餐详情里的余量对象(PackageInfo / SubPackageBalance 等)。
3115
- * @returns 剩余额度(上游单位,通常为 token 数或元);提取不到返回 undefined。
3116
- */
3117
- function pickRemainingQuota(source) {
3118
- if (source === null || typeof source !== "object") return void 0;
3119
- const numeric = Object.entries(source).filter(([, v]) => v !== null && toNumber$1(v) !== void 0);
3120
- const byKey = (needles) => {
3121
- for (const [key, value] of numeric) {
3122
- const lower = key.toLowerCase();
3123
- if (needles.some((n) => lower.includes(n))) {
3124
- const num = toNumber$1(value);
3125
- if (num !== void 0 && num >= 0) return num;
3126
- }
3127
- }
3128
- };
3129
- const remaining = byKey([
3130
- "remain",
3131
- "balance",
3132
- "left",
3133
- "available"
3134
- ]);
3135
- if (remaining !== void 0) return remaining;
3136
- const total = byKey(["total"]);
3137
- const used = byKey(["used", "consume"]);
3138
- if (total !== void 0 && used !== void 0) return Math.max(0, total - used);
3139
- }
3140
- /** 套餐列表里提取第一个启用套餐的 TeamId:集合字段名做候选兼容。 */
3141
- function firstEnabledTeamId(inner) {
3142
- const candidates = inner.TeamSet ?? inner.TokenPlanSet ?? inner.PlanSet;
3143
- if (!Array.isArray(candidates)) return void 0;
3144
- for (const item of candidates) {
3145
- if (item === null || typeof item !== "object") continue;
3146
- const row = item;
3147
- if (row.TeamId === void 0 && row.PlanId === void 0) continue;
3148
- if (row.Status !== void 0 && row.Status !== "enable") continue;
3149
- const id = row.TeamId ?? row.PlanId;
3150
- if (typeof id === "string" && id !== "") return id;
3151
- }
3152
- }
3153
- /**
3154
- * 查询腾讯云 TokenHub Token Plan 套餐余量。凭据值格式 `<SecretId>:<SecretKey>`
3155
- * (云 API 密钥,非 TokenHub 推理 key)。链路:套餐列表取 TeamId → 套餐详情读
3156
- * 主额度包余量。管控面字段名未完全稳定,解析按语义键防御提取。
3157
- * @param ctx - host context carrying the credentials seam.
3158
- * @param apiKeyEnv - credential reference resolving the `<SecretId>:<SecretKey>` pair.
3159
- */
3160
- async function queryTencentTokenPlan(ctx, apiKeyEnv) {
3161
- const provider = "腾讯云 TokenHub";
3162
- const hit = await ctx.credentials.resolve(credentialRef(apiKeyEnv));
3163
- if (hit === void 0) return {
3164
- provider,
3165
- displayName: provider,
3166
- error: "unconfigured"
3167
- };
3168
- if (!balanceGate.check(provider)) return {
3169
- provider,
3170
- displayName: provider,
3171
- error: "unreachable"
3172
- };
3173
- const credential = parseTencentCredential(hit.value);
3174
- if (credential === void 0) return {
3175
- provider,
3176
- displayName: provider,
3177
- error: "unauthorized"
3178
- };
3179
- const doRequest = async () => {
3180
- const teamId = firstEnabledTeamId(await callTokenHub(credential.secretId, credential.secretKey, "DescribeTokenPlanList", {}));
3181
- if (teamId === void 0) return {
3182
- provider,
3183
- displayName: provider,
3184
- error: "invalid"
3185
- };
3186
- const detail = await callTokenHub(credential.secretId, credential.secretKey, "DescribeTokenPlan", { TeamId: teamId });
3187
- const remaining = pickRemainingQuota(detail.PackageInfo) ?? pickRemainingQuota(detail);
3188
- if (remaining === void 0) {
3189
- console.warn(`[usage-billing] balance response drifted for ${provider}: no remaining-quota field parsed`);
3190
- return {
3191
- provider,
3192
- displayName: provider,
3193
- error: "invalid"
3194
- };
3195
- }
3196
- const plan = typeof detail.Name === "string" ? detail.Name : void 0;
3197
- const exhausted = detail.StopReason === "EXHAUSTED";
3198
- return {
3199
- provider,
3200
- displayName: provider,
3201
- currency: "CNY",
3202
- totalBalance: remaining,
3203
- ...plan !== void 0 ? { plan } : {},
3204
- ...exhausted ? { isAvailable: false } : {}
3205
- };
3206
- };
3207
- try {
3208
- const row = await withRetry(doRequest, {
3209
- retries: 1,
3210
- baseDelayMs: 250,
3211
- maxDelayMs: 2e3
3212
- });
3213
- balanceGate.success(provider);
3214
- return row;
3215
- } catch (error) {
3216
- balanceGate.fail(provider);
3217
- return {
3218
- provider,
3219
- displayName: provider,
3220
- error: error.code === "unauthorized" ? "unauthorized" : "unreachable"
3221
- };
3222
- }
3223
- }
3224
2997
  /** 点路径取值:`data.total_available` → 逐层下钻;任一缺失返回 undefined。 */
3225
2998
  function getPath(data, path) {
3226
2999
  let cursor = data;
@@ -220,8 +220,6 @@ export interface UsageStatsDocument {
220
220
  * 以 step/start 为起点估算并计 estimated。
221
221
  */
222
222
  perf?: PerfStats;
223
- /** 只存在于账本、且缺 foldVersion 的旧会话数;无旧行时省略。 */
224
- staleLedgerSessions?: number;
225
223
  }
226
224
  /** 按角色费用归因:user / tool 为输入成本的启发式摊分,assistant 为输出成本实测。 */
227
225
  export interface RoleCost {
@@ -236,8 +234,8 @@ export interface RoleCost {
236
234
  export interface PerfStats {
237
235
  /** 按模型聚合(键 = 计费目录键;未收录模型原样保留)。 */
238
236
  byModel: Record<string, ModelPerf>;
239
- /** 按小时×模型聚合(外键 = {@link hourStamp},内键 = 模型目录键;北京时间)。 */
240
- byHourModel: Record<string, Record<string, HourModelPerf>>;
237
+ /** 按小时聚合(键 = {@link hourStamp},北京时间)。 */
238
+ byHour: Record<string, HourPerf>;
241
239
  }
242
240
  /** 一个模型的性能统计:首字延时均值 / P50 / P90、生成速度均值、总延迟均值。 */
243
241
  export interface ModelPerf {
@@ -260,11 +258,11 @@ export interface ModelPerf {
260
258
  /** 以 step/start 估算的样本数(工具续写步骤无独立 request/header)。 */
261
259
  estimatedSamples: number;
262
260
  }
263
- /** 一个小时单模型的性能统计(外键 = {@link hourStamp},内键 = 模型目录键)。 */
264
- export interface HourModelPerf {
261
+ /** 一个小时的性能统计(键 = {@link hourStamp})。 */
262
+ export interface HourPerf {
265
263
  samples: number;
266
264
  ttftAvg: number;
267
- /** 平均生成速度(tokens/s);该小时该模型无可测生成窗口时缺失。 */
265
+ /** 平均生成速度(tokens/s);该小时无可测生成窗口时缺失。 */
268
266
  tpsAvg?: number;
269
267
  }
270
268
  /** 会话明细行:仪表盘「会话明细」面板的数据源。 */
@@ -420,13 +418,10 @@ export interface UsageLedgerDocument {
420
418
  * 两次 header 之间的用量串到上一个模型,订阅模型首当其冲,issue #14);v2 =
421
419
  * `assistant/message` 自带 source 归账(1.0.7 起);v3 = 联网搜索请求按次估算
422
420
  * 计费(issue #15,1.0.9 起);v4 = 模型×日期×站点三维桶(issue #16,按 origin
423
- * 绑定自定义价的显示层重估)——旧行缺该维度,按无 origin 价处理;v5 = 峰谷
424
- * 时代之前(2026-08-16T16:00Z)的 DeepSeek 事件按当时基础价计费(v4 把全部
425
- * 历史套现行峰/谷档价,高估约 50%);v6 = v1 峰谷窗口(至 2026-08-22T16:00Z)
426
- * 的周末不豁免峰时(v5 错套现行周末全谷规则,低估该窗口周六日的峰时费用)。
421
+ * 绑定自定义价的显示层重估)——旧行缺该维度,按无 origin 价处理。
427
422
  * 持久账本行据此区分新旧算法:日志已删/不可读而只能沿用旧行时,UI 标注置信度提示。
428
423
  */
429
- export declare const FOLD_VERSION = 6;
424
+ export declare const FOLD_VERSION = 5;
430
425
  /**
431
426
  * 一次性账本迁移:id 唯一,apply 在加载边界对原始文档执行,已应用过的跳过。
432
427
  * 未来账本/schema 字段变更(重命名、拆桶、语义调整)时,在此追加一条迁移并
@@ -477,7 +472,7 @@ export declare function messageTextLength(message: unknown): number;
477
472
  export declare function foldSession(events: readonly {
478
473
  type: string;
479
474
  time: number;
480
- data: unknown;
475
+ data: never;
481
476
  seq?: number;
482
477
  }[], subscriptionProviders: ReadonlySet<string>, officialProviderIds?: ReadonlySet<string>, routes?: Readonly<Record<string, ProviderRouteView>>, searchCallEstimateCny?: number): SessionFold;
483
478
  /**
@@ -25,26 +25,6 @@ import type { CustomBalanceConfig, CustomBalanceExtract, ProviderBalance } from
25
25
  export declare function queryBalances(ctx: Context, providers: Readonly<Record<string, {
26
26
  apiKeyEnv?: string;
27
27
  }>>): Promise<readonly ProviderBalance[]>;
28
- /**
29
- * 构造云 API 3.0 TC3-HMAC-SHA256 签名(官方签名方法 v3)。导出供测试:纯函数,
30
- * 输入确定则签名确定。Action 不参与签名——它走 `X-TC-Action` 请求头。
31
- * @param secretId - 云 API SecretId。
32
- * @param secretKey - 云 API SecretKey。
33
- * @param payload - 已序列化的请求体(含 Action/Version/Region 公共参数)。
34
- * @param timestamp - 签名时间戳(秒)。
35
- * @returns Authorization 头的值。
36
- */
37
- export declare function tc3Authorization(secretId: string, secretKey: string, payload: string, timestamp: number): string;
38
- /**
39
- * 在套餐余量对象里防御性提取「剩余额度」:官方 SubPackageBalance/PackageInfo
40
- * 的字段名未稳定公开(issue #18 调研期),按语义键名扫描——命中 remaining /
41
- * balance / left 语义键直接用;命中 total 与 used 则相减推导。数字一律经
42
- * {@link toNumber} 归一化(上游可能给字符串)。
43
- * 导出供测试:纯函数。
44
- * @param source - 套餐详情里的余量对象(PackageInfo / SubPackageBalance 等)。
45
- * @returns 剩余额度(上游单位,通常为 token 数或元);提取不到返回 undefined。
46
- */
47
- export declare function pickRemainingQuota(source: unknown): number | undefined;
48
28
  /**
49
29
  * 按 extract 规则从响应 JSON 求值。导出供测试:纯函数。
50
30
  * @param rule - 提取规则(const / path / add / subtract / divide)。
@@ -1,13 +1,12 @@
1
1
  /**
2
- * PerfPanel: per-model latency/perf table + per-hour per-model comparison curve.
2
+ * PerfPanel: per-model latency/perf table + per-hour TTFT/generation-speed curve.
3
3
  *
4
4
  * Reads the optional `perf` field of the usage-stats document (aggregated by
5
5
  * the host from session logs). Renders a per-model table of TTFT mean/P50/P90,
6
6
  * generation speed, total latency and estimated-step count, plus a small
7
- * dependency-free SVG hourly curve: one colored polyline per lit model with a
8
- * metric tab (TTFT ms / tok/s) and clickable model chips; hovering snaps to the
9
- * nearest hour and shows a crosshair + tooltip listing every lit model's value.
10
- * Absent `perf` renders an empty state; the panel never fabricates samples.
7
+ * dependency-free SVG twin-series hourly curve (TTFT in ms on the left axis,
8
+ * tokens/s on the right). Absent `perf` (older snapshot or stream-less logs)
9
+ * renders an empty state; the panel never fabricates samples.
11
10
  */
12
11
  import type { TrendSeriesModel } from './TrendChart.tsx';
13
12
  import type { UsageBillingKey } from './locales.ts';
@@ -25,8 +24,8 @@ export interface PerfModelData {
25
24
  latencyAvg: number;
26
25
  estimatedSamples: number;
27
26
  }
28
- /** 小时×模型性能统计(与服务端 `HourModelPerf` 同形)。 */
29
- export interface PerfHourModelData {
27
+ /** 每小时性能统计(与服务端 `HourPerf` 同形)。 */
28
+ export interface PerfHourData {
30
29
  samples: number;
31
30
  ttftAvg: number;
32
31
  tpsAvg?: number;
@@ -34,13 +33,12 @@ export interface PerfHourModelData {
34
33
  /** 性能指标文档(服务端可选 `perf` 字段;旧快照缺失)。 */
35
34
  export interface ClientPerf {
36
35
  byModel: Record<string, PerfModelData>;
37
- /** 小时 → 模型 → 小时统计;旧 host 文档缺失时图表区按空态兜底。 */
38
- byHourModel: Record<string, Record<string, PerfHourModelData>>;
36
+ byHour: Record<string, PerfHourData>;
39
37
  }
40
38
  /**
41
39
  * Render the performance panel.
42
40
  * @param props.perf - the optional perf doc; `undefined`/empty renders an empty state.
43
- * @param props.models - model legend (key/name/color) for the table swatches and curve chips.
41
+ * @param props.models - model legend (key/name/color) for the table swatches and curve legend.
44
42
  * @param props.t - locale function.
45
43
  */
46
44
  export declare function PerfPanel({ perf, models, t, }: {
@@ -1,73 +1,23 @@
1
1
  /**
2
2
  * TokenPanel: 「Token」分区——把 token 从费用里独立出来洞察。
3
3
  * 四个板块 + 导出,全部由 `UsageStats` 派生,服务端零改动:
4
- * 1. 每日 Token 趋势,双视角切换(7/30 天):
5
- * - 按结构:未命中输入 / 缓存命中 / 输出[含 reasoning] 三桶堆叠;
6
- * - 按模型:每天按模型堆叠,段 = 该模型当日「模型总 Token」(输入[含命中] + 输出),
7
- * 分色复用趋势页 `chartModels`(同模型同色跨页一致);
8
- * 旧快照缺 `byDayModels` 时隐藏切换钮,仅保留结构视角。
9
- * 悬停显示当日精确明细;图例 / 「模型 Token」表格行可点击聚焦单个模型
10
- * (目标段保持原色,其余段弱化,y 轴不变)。
11
- * 归因边界:reasoning 无按日 × 模型明细(`byDayModels` 无该字段),图与导出不含此列。
12
- * 2. 模型 Token 总量排行 + 占比(行点击 = 聚焦该模型并切到按模型视角);
4
+ * 1. 每日 Token 堆叠趋势(未命中输入 / 缓存命中 / 输出[含 reasoning]),7/30 天切换;
5
+ * 2. 模型 Token 总量排行 + 占比;
13
6
  * 3. Token 结构 KPI(缓存命中率 / reasoning 占比 / 输入:输出比 / 峰值日)+ 显式缓存写入;
14
7
  * 4. 工具调用排行(byTool 计次;token 无法按工具归因)。
15
8
  */
16
9
  import type { UsageBillingKey } from './locales.ts';
17
- import type { TrendSeriesModel } from './TrendChart.tsx';
18
10
  import type { UsageStats } from './UsageBilling.tsx';
19
- /** 每日 token 堆叠图元(按结构视角)。 */
20
- interface DailyBucket {
21
- date: string;
22
- miss: number;
23
- hit: number;
24
- output: number;
25
- reasoning: number;
26
- }
27
- /** 按模型视角下某模型某日的 token 明细(`byDayModels` 无 reasoning 列)。 */
28
- interface ModelDayCell {
29
- hit: number;
30
- miss: number;
31
- output: number;
32
- /** 命中 + 未命中 + 输出(= 模型总 Token 口径)。 */
33
- total: number;
34
- }
35
- /** 按模型视角的一天:各模型段 + 当日合计。 */
36
- interface DayModelsRow {
37
- date: string;
38
- models: Record<string, ModelDayCell>;
39
- total: number;
40
- }
41
- /** 模型 token 行。 */
42
- interface ModelTokenRow {
43
- key: string;
44
- name: string;
45
- input: number;
46
- output: number;
47
- reasoning: number;
48
- calls: number;
49
- cacheHit: number;
50
- cacheMiss: number;
51
- cacheHitRate: number;
52
- total: number;
53
- /** 占总 token 比例(0..1)。 */
54
- share: number;
55
- }
56
- /** 导出全量 JSON 文档:按日结构 + 模型排行 + 按日×模型明细 + 总量(无 reasoning 列)。 */
57
- export declare function tokenDailyJson(days: readonly DailyBucket[], models: readonly ModelTokenRow[], dayModels: readonly DayModelsRow[], total: UsageStats['total']): string;
58
11
  /**
59
12
  * Token 洞察面板。
60
- * @param props.stats - usage-stats 文档(byDay/byModel/byDayModels/total)。
13
+ * @param props.stats - usage-stats 文档(byDay/byModel/total)。
61
14
  * @param props.trendDays - 每日 token 窗口(7/30 天)。
62
15
  * @param props.onTrendDays - 切换趋势窗口。
63
- * @param props.models - 趋势页同款模型图例(key/name/色):按模型视角的分色来源;缺省用兜底灰。
64
16
  */
65
17
  export declare function TokenPanel(props: {
66
18
  stats: UsageStats;
67
19
  trendDays: 7 | 30;
68
20
  onTrendDays: (d: 7 | 30) => void;
69
- models?: readonly TrendSeriesModel[];
70
21
  t: (key: UsageBillingKey) => string;
71
22
  }): React.ReactNode;
72
- export {};
73
23
  //# sourceMappingURL=TokenPanel.d.ts.map
@@ -10,7 +10,7 @@
10
10
  *(billing.dashboard.decor)并注册计费指标服务(ctx.billingMetrics),主题
11
11
  * 插件主动注入装饰视觉、消费费用数据——billing 不反向依赖任何主题包。
12
12
  */
13
- import type { Context } from '@deepseek-ai/cordis';
13
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
14
14
  import { type UsageBillingKey } from './locales.ts';
15
15
  import { type BillingMetricsService } from './billing-service.ts';
16
16
  declare module '@deepseek-ai/dsh-client-ui-slots' {
@@ -45,18 +45,11 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
45
45
  usageBilling: UsageBillingKey;
46
46
  }
47
47
  }
48
- /** Required services for the usage billing surface.
49
- *
50
- * `remote.llm` is deliberately NOT injected: the LLM remote namespace only
51
- * exists on 0.1.2-alpha.1 hosts, and a pending injection blocks the whole
52
- * web boot on older hosts. `checkModels` probes `ctx.remote.llm` defensively
53
- * instead — on hosts without it the model health check degrades to the
54
- * static catalog result (the surrounding try/catch already swallows the
55
- * undefined access). */
48
+ /** Required services for the usage billing surface. */
56
49
  export declare const inject: string[];
57
50
  /**
58
51
  * Client plugin body: the UsageBilling entry in the sidebar footer.
59
52
  * @param ctx - client root context.
60
53
  */
61
- export declare function apply(ctx: Context): void;
54
+ export declare function apply(ctx: ClientContext): void;
62
55
  //# sourceMappingURL=apply.d.ts.map
@@ -5,7 +5,7 @@
5
5
  * store 引擎持久化到 localStorage(persist key 即存储身份),重启后保留。
6
6
  * 宿主 Config 的 monthlyBudget 仅作为金额未设置时的默认值,用户输入优先。
7
7
  */
8
- import { type EngineStoreHandle } from '@deepseek-ai/dsh-client-store';
8
+ import { type EngineStoreHandle } from '@deepseek-ai/dsh-client-runtime/client';
9
9
  /** 预算偏好状态。 */
10
10
  export interface BudgetPrefsState {
11
11
  /** 预算条开关:关 = 只显示标题行与开关,不显示进度。 */
@@ -5,8 +5,8 @@
5
5
  * 可选:跨 tab 只保留一个提醒 leader(Web Locks 优先,降级 localStorage 租约),
6
6
  * 避免多窗口同时弹同一条。配置持久化在 localStorage(默认关闭,用户在面板设置开启)。
7
7
  */
8
- import type { SessionListState } from '@deepseek-ai/dsh-api-session-controller/client';
9
- import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store';
8
+ import type { SessionListState } from '@deepseek-ai/dsh-client-runtime/client';
9
+ import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-runtime/client';
10
10
  /** 配置持久化 key:开启/关闭 + 提醒持续模式(0=常驻,其余=秒后自动关)。 */
11
11
  export declare const COMPLETION_NOTIFY_KEY = "dsh-billing-completion-notify-v1";
12
12
  export interface CompletionNotifyConfig {
@@ -32,6 +32,6 @@ export declare function UsageHeatmap({ days, currency, now, t, range }: {
32
32
  currency: CostCurrency;
33
33
  now?: Date;
34
34
  range?: 'month' | 'year';
35
- t: (key: 'costAbbr' | 'noData' | 'heatmapLess' | 'heatmapMore') => string;
35
+ t: (key: 'billing.costAbbr' | 'billing.noData' | 'billing.heatmapLess' | 'billing.heatmapMore') => string;
36
36
  }): React.ReactNode;
37
37
  //# sourceMappingURL=heatmap.d.ts.map
@@ -6,16 +6,17 @@
6
6
  * composer card, same posture as ui-conversation's own StatsLine), so it stays
7
7
  * visible while working without opening the full dashboard. Data comes from the
8
8
  * same `/api/billing/usage-stats` endpoint the dashboard polls; the bar reads
9
- * the current session id from the session-scope standard kit and matches
10
- * `bySession` (session total) and `byTurn` (latest turn cost). Rendering is a
11
- * pure function of props and polled data, never a side effect.
9
+ * the current session id off the framework snapshot (`useSession` parent of
10
+ * `sessionId`) and matches `bySession` (session total) and `byTurn` (latest
11
+ * turn cost). Rendering is a pure function of the snapshot, never a side effect.
12
12
  *
13
13
  * The bar also carries two ambient signals: the current peak/off-peak pricing
14
14
  * tier with a switch countdown (DeepSeek time-of-day pricing), and quota chips
15
15
  * for subscription plans running low (≤20% remaining), so cost pressure is
16
16
  * visible without opening the dashboard.
17
17
  */
18
- import type { SessionId } from '@deepseek-ai/dsh-session/types';
18
+ import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
19
+ import type { ConversationSnapshot } from '@deepseek-ai/dsh-client-runtime/client';
19
20
  import type { UsageBillingKey } from './locales.ts';
20
21
  /** The usage-stats shape the composer bar needs (a thin slice, not the whole doc). */
21
22
  export interface LiveStats {
@@ -66,16 +67,15 @@ export declare function lowQuotaChips(quotas: readonly QuotaSlice[], threshold?:
66
67
  kind: string;
67
68
  pct: number;
68
69
  }[];
69
- /** Props: the framework's session identity plus the owning dock's locale seat. */
70
+ /** Props: the session-scope snapshot selector the framework injects. */
70
71
  export interface LiveCostBarProps {
71
- /** Current Session identity supplied by the session-scope standard kit. */
72
- sessionId: SessionId;
72
+ useSession: SnapshotSelectorHook<ConversationSnapshot>;
73
73
  /** The owning dock's locale seat (bound to the billing NS). */
74
74
  t: (key: UsageBillingKey) => string;
75
75
  }
76
76
  /**
77
77
  * Render the live cost ticker for the current session.
78
- * @param props - framework session identity and locale.
78
+ * @param props - framework session snapshot hook and locale.
79
79
  */
80
- export declare function LiveCostBar({ sessionId, t }: LiveCostBarProps): React.ReactNode;
80
+ export declare function LiveCostBar({ useSession, t }: LiveCostBarProps): React.ReactNode;
81
81
  //# sourceMappingURL=live-cost.d.ts.map