@kenz1117/dsh-ui-usage-billing 1.0.11 → 1.0.13

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,6 +7,7 @@ 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";
10
11
  //#region lib/types/client/plan-knowledge.js
11
12
  /**
12
13
  * Plan-knowledge reference (adapted from dsh-spend's `knowledge.js`, MIT):
@@ -2914,6 +2915,26 @@ const QUERIERS = [
2914
2915
  route: "zai-coding-cn",
2915
2916
  displayName: "智谱 AI",
2916
2917
  querier: queryZhipu
2918
+ },
2919
+ {
2920
+ route: "tencent-tokenhub",
2921
+ displayName: "腾讯云 TokenHub",
2922
+ querier: queryTencentTokenPlan
2923
+ },
2924
+ {
2925
+ route: "tokenhub",
2926
+ displayName: "腾讯云 TokenHub",
2927
+ querier: queryTencentTokenPlan
2928
+ },
2929
+ {
2930
+ route: "tencent",
2931
+ displayName: "腾讯云 TokenHub",
2932
+ querier: queryTencentTokenPlan
2933
+ },
2934
+ {
2935
+ route: "tencentcloud",
2936
+ displayName: "腾讯云 TokenHub",
2937
+ querier: queryTencentTokenPlan
2917
2938
  }
2918
2939
  ];
2919
2940
  /**
@@ -2946,6 +2967,200 @@ async function queryBalances(ctx, providers) {
2946
2967
  return querier(ctx, env);
2947
2968
  }));
2948
2969
  }
2970
+ /** TokenHub 管控面 API 端点(cloud.tencent.cn/document/api/1823/132270)。 */
2971
+ const TOKENHUB_HOST = "tokenhub.tencentcloudapi.com";
2972
+ /** 云 API 3.0 产品名与版本(签名的 service 段与请求头都必须一致)。 */
2973
+ const TOKENHUB_SERVICE = "tokenhub";
2974
+ const TOKENHUB_VERSION = "2026-03-22";
2975
+ /** 请求地域:管控面对地域不敏感,取默认国内地域(文档地域列表含 ap-guangzhou)。 */
2976
+ const TOKENHUB_REGION = "ap-guangzhou";
2977
+ /** 腾讯云凭据引用值格式:`<SecretId>:<SecretKey>`(分隔符取首个冒号)。 */
2978
+ function parseTencentCredential(value) {
2979
+ const sep = value.indexOf(":");
2980
+ if (sep === -1) return void 0;
2981
+ const secretId = value.slice(0, sep).trim();
2982
+ const secretKey = value.slice(sep + 1).trim();
2983
+ if (secretId === "" || secretKey === "") return void 0;
2984
+ return {
2985
+ secretId,
2986
+ secretKey
2987
+ };
2988
+ }
2989
+ /**
2990
+ * 构造云 API 3.0 TC3-HMAC-SHA256 签名(官方签名方法 v3)。导出供测试:纯函数,
2991
+ * 输入确定则签名确定。Action 不参与签名——它走 `X-TC-Action` 请求头。
2992
+ * @param secretId - 云 API SecretId。
2993
+ * @param secretKey - 云 API SecretKey。
2994
+ * @param payload - 已序列化的请求体(含 Action/Version/Region 公共参数)。
2995
+ * @param timestamp - 签名时间戳(秒)。
2996
+ * @returns Authorization 头的值。
2997
+ */
2998
+ function tc3Authorization(secretId, secretKey, payload, timestamp) {
2999
+ const date = (/* @__PURE__ */ new Date(timestamp * 1e3)).toISOString().slice(0, 10);
3000
+ 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")}`;
3001
+ const hashedCanonical = createHash("sha256").update(canonicalRequest).digest("hex");
3002
+ const stringToSign = `TC3-HMAC-SHA256\n${String(timestamp)}\n${date}/${TOKENHUB_SERVICE}/tc3_request\n${hashedCanonical}`;
3003
+ 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")}`;
3004
+ }
3005
+ /** 调用一次 TokenHub 管控面接口:TC3 签名 + 超时保护,返回响应 JSON 的 `Response`。 */
3006
+ async function callTokenHub(secretId, secretKey, action, params) {
3007
+ const payload = JSON.stringify({
3008
+ Action: action,
3009
+ Version: TOKENHUB_VERSION,
3010
+ Region: TOKENHUB_REGION,
3011
+ ...params
3012
+ });
3013
+ const timestamp = Math.floor(Date.now() / 1e3);
3014
+ const controller = new AbortController();
3015
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS$3);
3016
+ try {
3017
+ const response = await fetch(`https://${TOKENHUB_HOST}/`, {
3018
+ method: "POST",
3019
+ headers: {
3020
+ "content-type": "application/json; charset=utf-8",
3021
+ host: TOKENHUB_HOST,
3022
+ "x-tc-action": action.toLowerCase(),
3023
+ "x-tc-version": TOKENHUB_VERSION,
3024
+ "x-tc-region": TOKENHUB_REGION,
3025
+ "x-tc-timestamp": String(timestamp),
3026
+ authorization: tc3Authorization(secretId, secretKey, payload, timestamp)
3027
+ },
3028
+ body: payload,
3029
+ signal: controller.signal
3030
+ });
3031
+ if (response.status === 401 || response.status === 403) throw Object.assign(/* @__PURE__ */ new Error("unauthorized"), { code: "unauthorized" });
3032
+ if (!response.ok) throw Object.assign(/* @__PURE__ */ new Error(`HTTP ${String(response.status)}`), {
3033
+ httpStatus: response.status,
3034
+ code: "unreachable"
3035
+ });
3036
+ const inner = (await response.json()).Response;
3037
+ if (inner === void 0) throw Object.assign(/* @__PURE__ */ new Error("no Response envelope"), { code: "invalid" });
3038
+ if (inner.Error !== void 0 && inner.Error !== null) {
3039
+ const err = inner.Error;
3040
+ const code = err.Code === "AuthFailure.SignatureFailure" || err.Code === "AuthFailure.SecretIdNotFound" ? "unauthorized" : "unreachable";
3041
+ throw Object.assign(new Error(String(err.Code ?? "api-error")), { code });
3042
+ }
3043
+ return inner;
3044
+ } finally {
3045
+ clearTimeout(timer);
3046
+ }
3047
+ }
3048
+ /**
3049
+ * 在套餐余量对象里防御性提取「剩余额度」:官方 SubPackageBalance/PackageInfo
3050
+ * 的字段名未稳定公开(issue #18 调研期),按语义键名扫描——命中 remaining /
3051
+ * balance / left 语义键直接用;命中 total 与 used 则相减推导。数字一律经
3052
+ * {@link toNumber} 归一化(上游可能给字符串)。
3053
+ * 导出供测试:纯函数。
3054
+ * @param source - 套餐详情里的余量对象(PackageInfo / SubPackageBalance 等)。
3055
+ * @returns 剩余额度(上游单位,通常为 token 数或元);提取不到返回 undefined。
3056
+ */
3057
+ function pickRemainingQuota(source) {
3058
+ if (source === null || typeof source !== "object") return void 0;
3059
+ const numeric = Object.entries(source).filter(([, v]) => v !== null && toNumber$1(v) !== void 0);
3060
+ const byKey = (needles) => {
3061
+ for (const [key, value] of numeric) {
3062
+ const lower = key.toLowerCase();
3063
+ if (needles.some((n) => lower.includes(n))) {
3064
+ const num = toNumber$1(value);
3065
+ if (num !== void 0 && num >= 0) return num;
3066
+ }
3067
+ }
3068
+ };
3069
+ const remaining = byKey([
3070
+ "remain",
3071
+ "balance",
3072
+ "left",
3073
+ "available"
3074
+ ]);
3075
+ if (remaining !== void 0) return remaining;
3076
+ const total = byKey(["total"]);
3077
+ const used = byKey(["used", "consume"]);
3078
+ if (total !== void 0 && used !== void 0) return Math.max(0, total - used);
3079
+ }
3080
+ /** 套餐列表里提取第一个启用套餐的 TeamId:集合字段名做候选兼容。 */
3081
+ function firstEnabledTeamId(inner) {
3082
+ const candidates = inner.TeamSet ?? inner.TokenPlanSet ?? inner.PlanSet;
3083
+ if (!Array.isArray(candidates)) return void 0;
3084
+ for (const item of candidates) {
3085
+ if (item === null || typeof item !== "object") continue;
3086
+ const row = item;
3087
+ if (row.TeamId === void 0 && row.PlanId === void 0) continue;
3088
+ if (row.Status !== void 0 && row.Status !== "enable") continue;
3089
+ const id = row.TeamId ?? row.PlanId;
3090
+ if (typeof id === "string" && id !== "") return id;
3091
+ }
3092
+ }
3093
+ /**
3094
+ * 查询腾讯云 TokenHub Token Plan 套餐余量。凭据值格式 `<SecretId>:<SecretKey>`
3095
+ * (云 API 密钥,非 TokenHub 推理 key)。链路:套餐列表取 TeamId → 套餐详情读
3096
+ * 主额度包余量。管控面字段名未完全稳定,解析按语义键防御提取。
3097
+ * @param ctx - host context carrying the credentials seam.
3098
+ * @param apiKeyEnv - credential reference resolving the `<SecretId>:<SecretKey>` pair.
3099
+ */
3100
+ async function queryTencentTokenPlan(ctx, apiKeyEnv) {
3101
+ const provider = "腾讯云 TokenHub";
3102
+ const hit = await ctx.credentials.resolve(credentialRef(apiKeyEnv));
3103
+ if (hit === void 0) return {
3104
+ provider,
3105
+ displayName: provider,
3106
+ error: "unconfigured"
3107
+ };
3108
+ if (!balanceGate.check(provider)) return {
3109
+ provider,
3110
+ displayName: provider,
3111
+ error: "unreachable"
3112
+ };
3113
+ const credential = parseTencentCredential(hit.value);
3114
+ if (credential === void 0) return {
3115
+ provider,
3116
+ displayName: provider,
3117
+ error: "unauthorized"
3118
+ };
3119
+ const doRequest = async () => {
3120
+ const teamId = firstEnabledTeamId(await callTokenHub(credential.secretId, credential.secretKey, "DescribeTokenPlanList", {}));
3121
+ if (teamId === void 0) return {
3122
+ provider,
3123
+ displayName: provider,
3124
+ error: "invalid"
3125
+ };
3126
+ const detail = await callTokenHub(credential.secretId, credential.secretKey, "DescribeTokenPlan", { TeamId: teamId });
3127
+ const remaining = pickRemainingQuota(detail.PackageInfo) ?? pickRemainingQuota(detail);
3128
+ if (remaining === void 0) {
3129
+ console.warn(`[usage-billing] balance response drifted for ${provider}: no remaining-quota field parsed`);
3130
+ return {
3131
+ provider,
3132
+ displayName: provider,
3133
+ error: "invalid"
3134
+ };
3135
+ }
3136
+ const plan = typeof detail.Name === "string" ? detail.Name : void 0;
3137
+ const exhausted = detail.StopReason === "EXHAUSTED";
3138
+ return {
3139
+ provider,
3140
+ displayName: provider,
3141
+ currency: "CNY",
3142
+ totalBalance: remaining,
3143
+ ...plan !== void 0 ? { plan } : {},
3144
+ ...exhausted ? { isAvailable: false } : {}
3145
+ };
3146
+ };
3147
+ try {
3148
+ const row = await withRetry(doRequest, {
3149
+ retries: 1,
3150
+ baseDelayMs: 250,
3151
+ maxDelayMs: 2e3
3152
+ });
3153
+ balanceGate.success(provider);
3154
+ return row;
3155
+ } catch (error) {
3156
+ balanceGate.fail(provider);
3157
+ return {
3158
+ provider,
3159
+ displayName: provider,
3160
+ error: error.code === "unauthorized" ? "unauthorized" : "unreachable"
3161
+ };
3162
+ }
3163
+ }
2949
3164
  /** 点路径取值:`data.total_available` → 逐层下钻;任一缺失返回 undefined。 */
2950
3165
  function getPath(data, path) {
2951
3166
  let cursor = data;
@@ -220,6 +220,8 @@ export interface UsageStatsDocument {
220
220
  * 以 step/start 为起点估算并计 estimated。
221
221
  */
222
222
  perf?: PerfStats;
223
+ /** 只存在于账本、且缺 foldVersion 的旧会话数;无旧行时省略。 */
224
+ staleLedgerSessions?: number;
223
225
  }
224
226
  /** 按角色费用归因:user / tool 为输入成本的启发式摊分,assistant 为输出成本实测。 */
225
227
  export interface RoleCost {
@@ -472,7 +474,7 @@ export declare function messageTextLength(message: unknown): number;
472
474
  export declare function foldSession(events: readonly {
473
475
  type: string;
474
476
  time: number;
475
- data: never;
477
+ data: unknown;
476
478
  seq?: number;
477
479
  }[], subscriptionProviders: ReadonlySet<string>, officialProviderIds?: ReadonlySet<string>, routes?: Readonly<Record<string, ProviderRouteView>>, searchCallEstimateCny?: number): SessionFold;
478
480
  /**
@@ -25,6 +25,26 @@ 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;
28
48
  /**
29
49
  * 按 extract 规则从响应 JSON 求值。导出供测试:纯函数。
30
50
  * @param rule - 提取规则(const / path / add / subtract / divide)。
@@ -10,7 +10,7 @@
10
10
  *(billing.dashboard.decor)并注册计费指标服务(ctx.billingMetrics),主题
11
11
  * 插件主动注入装饰视觉、消费费用数据——billing 不反向依赖任何主题包。
12
12
  */
13
- import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
13
+ import type { Context } from '@deepseek-ai/cordis';
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' {
@@ -51,5 +51,5 @@ export declare const inject: string[];
51
51
  * Client plugin body: the UsageBilling entry in the sidebar footer.
52
52
  * @param ctx - client root context.
53
53
  */
54
- export declare function apply(ctx: ClientContext): void;
54
+ export declare function apply(ctx: Context): void;
55
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-runtime/client';
8
+ import { type EngineStoreHandle } from '@deepseek-ai/dsh-client-store';
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-client-runtime/client';
9
- import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-runtime/client';
8
+ import type { SessionListState } from '@deepseek-ai/dsh-api-session-controller/client';
9
+ import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store';
10
10
  /** 配置持久化 key:开启/关闭 + 提醒持续模式(0=常驻,其余=秒后自动关)。 */
11
11
  export declare const COMPLETION_NOTIFY_KEY = "dsh-billing-completion-notify-v1";
12
12
  export interface CompletionNotifyConfig {
@@ -6,17 +6,16 @@
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 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.
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.
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 { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
19
- import type { ConversationSnapshot } from '@deepseek-ai/dsh-client-runtime/client';
18
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
20
19
  import type { UsageBillingKey } from './locales.ts';
21
20
  /** The usage-stats shape the composer bar needs (a thin slice, not the whole doc). */
22
21
  export interface LiveStats {
@@ -67,15 +66,16 @@ export declare function lowQuotaChips(quotas: readonly QuotaSlice[], threshold?:
67
66
  kind: string;
68
67
  pct: number;
69
68
  }[];
70
- /** Props: the session-scope snapshot selector the framework injects. */
69
+ /** Props: the framework's session identity plus the owning dock's locale seat. */
71
70
  export interface LiveCostBarProps {
72
- useSession: SnapshotSelectorHook<ConversationSnapshot>;
71
+ /** Current Session identity supplied by the session-scope standard kit. */
72
+ sessionId: SessionId;
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 snapshot hook and locale.
78
+ * @param props - framework session identity and locale.
79
79
  */
80
- export declare function LiveCostBar({ useSession, t }: LiveCostBarProps): React.ReactNode;
80
+ export declare function LiveCostBar({ sessionId, t }: LiveCostBarProps): React.ReactNode;
81
81
  //# sourceMappingURL=live-cost.d.ts.map
@@ -1,5 +1,5 @@
1
1
  /** Locale dictionaries for the usage billing surface. */
2
- export type UsageBillingKey = 'billing.title' | 'billing.subtitle' | 'billing.cost' | 'billing.todayCost' | 'billing.monthCost' | 'billing.yearCost' | 'billing.monthProjected' | 'billing.liveTurn' | 'billing.liveSession' | 'billing.totalCost' | 'billing.calls' | 'billing.cacheHitRate' | 'billing.tokens' | 'billing.inputTokens' | 'billing.outputTokens' | 'billing.avgCost' | 'billing.trend' | 'billing.trend7d' | 'billing.trend30d' | 'billing.trendMetric' | 'billing.trendMetricCost' | 'billing.trendMetricTokens' | 'billing.trendEmpty' | 'billing.budget' | 'billing.budgetAmount' | 'billing.budgetSummary' | 'billing.sessions' | 'billing.sessionTitle' | 'billing.project' | 'billing.lastActive' | 'billing.sessionOverflow' | 'billing.budgetTierBody' | 'billing.models' | 'billing.providerBilling' | 'billing.estimated' | 'billing.actual' | 'billing.pricing' | 'billing.showPricing' | 'billing.hidePricing' | 'billing.pricePerM' | 'billing.input' | 'billing.output' | 'billing.cacheHit' | 'billing.peak' | 'billing.offPeak' | 'billing.flat' | 'billing.peakHours' | 'billing.band' | 'billing.openDashboard' | 'billing.close' | 'billing.footer' | 'billing.footerCredit' | 'billing.lastUpdated' | 'billing.noData' | 'billing.todayRate' | 'billing.rateLive' | 'billing.rateBuiltin' | 'billing.promoBadge' | 'billing.promoUntil' | 'billing.promoOpenEnded' | 'billing.pricingTip' | 'billing.pricingUnit' | 'billing.pricingNotes' | 'billing.ubPeak' | 'billing.ubOff' | 'billing.ubStd' | 'billing.peakBand' | 'billing.pricingSource' | 'billing.noteCache' | 'billing.noteBand' | 'billing.noteSource' | 'billing.balance' | 'billing.balanceUnconfigured' | 'billing.balanceUnauthorized' | 'billing.balanceUnreachable' | 'billing.uncatalogued' | 'billing.estimatedPricing' | 'billing.balanceDays' | 'billing.balanceLowBody' | 'billing.reconcileDrift' | 'billing.reconcileDismiss' | 'billing.subscriptions' | 'billing.subscriptionNotConfigured' | 'billing.subscriptionUnauthorized' | 'billing.subscriptionUnavailable' | 'billing.subscriptionInvalid' | 'billing.subscriptionRateLimited' | 'billing.subscriptionSession' | 'billing.subscriptionWeekly' | 'billing.subscriptionMonthly' | 'billing.subscriptionBilling' | 'billing.subscriptionRemaining' | 'billing.subscriptionExhausted' | 'billing.subscriptionReset' | 'billing.subscriptionNoApi' | 'billing.floatWindow' | 'billing.floatModeCombined' | 'billing.floatModeSubscription' | 'billing.floatMode' | 'billing.floatTargets' | 'billing.floatWindowHint' | 'billing.floatNoTargets' | 'billing.floatNoTargetsHint' | 'billing.cardDisplay' | 'billing.cardDisplayHint' | 'billing.cardMetric' | 'billing.cardMetricMoney' | 'billing.cardMetricTokens' | 'billing.triggerMonthTokens' | 'billing.floatPrev' | 'billing.floatNext' | 'billing.subscriptionsStale' | 'billing.staleLedgerNotice' | 'billing.tokenCacheWrite' | 'billing.toolRank' | 'billing.toolName' | 'billing.userPrices' | 'billing.userPricesHint' | 'billing.userPriceSave' | 'billing.userPriceModel' | 'billing.userPriceSource' | 'billing.userPriceSourceHint' | 'billing.userPriceCurrency' | 'billing.userPriceAdd' | 'billing.userPriceRemove' | 'billing.sessionStaleBadge' | 'billing.heatmapLess' | 'billing.heatmapMore' | 'billing.currency' | 'billing.currencyCny' | 'billing.currencyUsd' | 'billing.heatmap' | 'billing.rounds' | 'billing.roundsHint' | 'billing.anomaly' | 'billing.workspaces' | 'billing.workspacesHint' | 'billing.plan' | 'billing.remaining' | 'billing.unknownModel' | 'billing.model' | 'billing.thModel' | 'billing.thInputMiss' | 'billing.thInputHit' | 'billing.currentRound' | 'billing.costAbbr' | 'billing.tabOverview' | 'billing.tabTrends' | 'billing.tabProviders' | 'billing.tabDetails' | 'billing.tabPricing' | 'billing.tabSettings' | 'billing.settingsHead' | 'billing.settingsHint' | 'billing.budgetHint' | 'billing.peakAlertHint' | 'billing.peakAlertDescPeak' | 'billing.peakAlertDescOff' | 'billing.export' | 'billing.exportCsvDay' | 'billing.exportCsvSession' | 'billing.exportJson' | 'billing.peakShare' | 'billing.peakSharePerCall' | 'billing.offPeakSavings' | 'billing.perfMax' | 'billing.weekCost' | 'billing.roleCost' | 'billing.roleUser' | 'billing.roleAssistant' | 'billing.roleTool' | 'billing.roleHint' | 'billing.tierPeak' | 'billing.tierOff' | 'billing.tierToPeak' | 'billing.tierToOff' | 'billing.tierAlertEnterPeak' | 'billing.tierAlertEnterOff' | 'billing.peakAlertTitlePeak' | 'billing.peakAlertTitleOff' | 'billing.peakAlert' | 'billing.peakAlertLeadMin' | 'billing.peakAlertPos' | 'billing.peakAlertMode' | 'billing.peakAlertPosCorner' | 'billing.peakAlertPosCenter' | 'billing.peakAlertModePeak' | 'billing.peakAlertModeOff' | 'billing.peakAlertModeBoth' | 'billing.peakAlertWebNotify' | 'billing.peakAlertPreview' | 'billing.planTypeCode' | 'billing.planTypeToken' | 'billing.subscriptionFeePerMonth' | 'billing.triggerToday' | 'billing.triggerMonth' | 'billing.subscriptionIncluded' | 'billing.free' | 'billing.official' | 'billing.thirdParty' | 'billing.officialCost' | 'billing.thirdPartyCost' | 'billing.perfSamples' | 'billing.perfTtft' | 'billing.perfP50' | 'billing.perfP90' | 'billing.perfTps' | 'billing.perfLatency' | 'billing.perfEstimated' | 'billing.perfEmpty' | 'billing.perfTpsUnit' | 'billing.perfTitle' | 'billing.perfHint' | 'billing.heatmapYear' | 'billing.heatmapMonth' | 'billing.activeDays' | 'billing.streakDays' | 'billing.subscriptionAutoDetect' | 'billing.pluginInfo' | 'billing.pluginName' | 'billing.pluginDescription' | 'billing.pluginVersion' | 'billing.pluginAuthor' | 'billing.pluginRepository' | 'billing.pluginNpm' | 'billing.pluginLicense' | 'billing.tabToken' | 'billing.tokenExport' | 'billing.tokenExportCsv' | 'billing.tokenCacheHitRate' | 'billing.tokenReasoningShare' | 'billing.tokenReasoningShort' | 'billing.tokenIo' | 'billing.tokenPeak' | 'billing.tokenDaily' | 'billing.tokenByModel' | 'billing.tokenMiss' | 'billing.tokenHit' | 'billing.tokenOutput' | 'billing.tokenTotal' | 'billing.tokenShare' | 'billing.usageStatsTool' | 'billing.usageStatsToolHint' | 'billing.balanceGranted' | 'billing.balanceTopped' | 'billing.balanceDaily' | 'billing.balanceDaysLong' | 'billing.balanceDaysUnit' | 'billing.popTodayModel' | 'billing.popNoConsumption' | 'billing.popQuotaAlert' | 'billing.popRiskNone' | 'billing.popTitle' | 'billing.popDirectLead' | 'billing.popSubLead' | 'billing.popBalanceNormal' | 'billing.popBalanceLow' | 'billing.popQuotaNormal' | 'billing.popQuotaLow' | 'billing.alertBalanceLow' | 'billing.alertQuotaLow' | 'billing.unpricedHint' | 'billing.searchEstimateHint' | 'billing.siteListDisplay' | 'billing.siteListDisplayHint' | 'billing.exportCsvSite' | 'billing.panelRelay' | 'billing.relaySite' | 'billing.relayDirect' | 'billing.relayUnknown' | 'billing.panelRelayQuota' | 'billing.relayBalance' | 'billing.relayNoQuota' | 'billing.relayWindowUsed' | 'billing.relayKindNewApi' | 'billing.relayKindSub2Api' | 'billing.relayKindUnknown' | 'billing.relayCalls';
2
+ export type UsageBillingKey = 'billing.title' | 'billing.subtitle' | 'billing.cost' | 'billing.todayCost' | 'billing.monthCost' | 'billing.yearCost' | 'billing.monthProjected' | 'billing.liveTurn' | 'billing.liveSession' | 'billing.totalCost' | 'billing.calls' | 'billing.cacheHitRate' | 'billing.tokens' | 'billing.inputTokens' | 'billing.outputTokens' | 'billing.avgCost' | 'billing.trend' | 'billing.trend7d' | 'billing.trend30d' | 'billing.trendMetric' | 'billing.trendMetricCost' | 'billing.trendMetricTokens' | 'billing.trendEmpty' | 'billing.budget' | 'billing.budgetAmount' | 'billing.budgetSummary' | 'billing.sessions' | 'billing.sessionTitle' | 'billing.project' | 'billing.lastActive' | 'billing.sessionOverflow' | 'billing.budgetTierBody' | 'billing.models' | 'billing.providerBilling' | 'billing.estimated' | 'billing.actual' | 'billing.pricing' | 'billing.showPricing' | 'billing.hidePricing' | 'billing.pricePerM' | 'billing.input' | 'billing.output' | 'billing.cacheHit' | 'billing.peak' | 'billing.offPeak' | 'billing.flat' | 'billing.peakHours' | 'billing.band' | 'billing.openDashboard' | 'billing.close' | 'billing.footer' | 'billing.footerCredit' | 'billing.lastUpdated' | 'billing.noData' | 'billing.todayRate' | 'billing.rateLive' | 'billing.rateBuiltin' | 'billing.promoBadge' | 'billing.promoUntil' | 'billing.promoOpenEnded' | 'billing.pricingTip' | 'billing.pricingUnit' | 'billing.pricingNotes' | 'billing.ubPeak' | 'billing.ubOff' | 'billing.ubStd' | 'billing.peakBand' | 'billing.pricingSource' | 'billing.noteCache' | 'billing.noteBand' | 'billing.noteSource' | 'billing.balance' | 'billing.balanceUnconfigured' | 'billing.balanceUnauthorized' | 'billing.balanceUnreachable' | 'billing.uncatalogued' | 'billing.estimatedPricing' | 'billing.balanceDays' | 'billing.balanceLowBody' | 'billing.reconcileDrift' | 'billing.reconcileDismiss' | 'billing.subscriptions' | 'billing.subscriptionNotConfigured' | 'billing.subscriptionUnauthorized' | 'billing.subscriptionUnavailable' | 'billing.subscriptionInvalid' | 'billing.subscriptionRateLimited' | 'billing.subscriptionSession' | 'billing.subscriptionWeekly' | 'billing.subscriptionMonthly' | 'billing.subscriptionBilling' | 'billing.subscriptionRemaining' | 'billing.subscriptionExhausted' | 'billing.subscriptionReset' | 'billing.subscriptionNoApi' | 'billing.floatWindow' | 'billing.floatModeCombined' | 'billing.floatModeSubscription' | 'billing.floatMode' | 'billing.floatTargets' | 'billing.floatWindowHint' | 'billing.floatNoTargets' | 'billing.floatNoTargetsHint' | 'billing.cardDisplay' | 'billing.cardDisplayHint' | 'billing.cardMetric' | 'billing.cardMetricMoney' | 'billing.cardMetricTokens' | 'billing.triggerMonthTokens' | 'billing.floatPrev' | 'billing.floatNext' | 'billing.subscriptionsStale' | 'billing.staleLedgerNotice' | 'billing.tokenCacheWrite' | 'billing.toolRank' | 'billing.toolName' | 'billing.userPrices' | 'billing.userPricesHint' | 'billing.userPriceSave' | 'billing.userPriceModel' | 'billing.userPriceSource' | 'billing.userPriceSourceHint' | 'billing.userPriceOffPeak' | 'billing.userPriceCurrency' | 'billing.userPriceAdd' | 'billing.userPriceRemove' | 'billing.sessionStaleBadge' | 'billing.heatmapLess' | 'billing.heatmapMore' | 'billing.currency' | 'billing.currencyCny' | 'billing.currencyUsd' | 'billing.heatmap' | 'billing.rounds' | 'billing.roundsHint' | 'billing.anomaly' | 'billing.workspaces' | 'billing.workspacesHint' | 'billing.plan' | 'billing.remaining' | 'billing.unknownModel' | 'billing.model' | 'billing.thModel' | 'billing.thInputMiss' | 'billing.thInputHit' | 'billing.currentRound' | 'billing.costAbbr' | 'billing.tabOverview' | 'billing.tabTrends' | 'billing.tabProviders' | 'billing.tabDetails' | 'billing.tabPricing' | 'billing.tabSettings' | 'billing.settingsHead' | 'billing.settingsHint' | 'billing.budgetHint' | 'billing.peakAlertHint' | 'billing.peakAlertDescPeak' | 'billing.peakAlertDescOff' | 'billing.export' | 'billing.exportCsvDay' | 'billing.exportCsvSession' | 'billing.exportJson' | 'billing.peakShare' | 'billing.peakSharePerCall' | 'billing.offPeakSavings' | 'billing.perfMax' | 'billing.weekCost' | 'billing.roleCost' | 'billing.roleUser' | 'billing.roleAssistant' | 'billing.roleTool' | 'billing.roleHint' | 'billing.tierPeak' | 'billing.tierOff' | 'billing.tierToPeak' | 'billing.tierToOff' | 'billing.tierAlertEnterPeak' | 'billing.tierAlertEnterOff' | 'billing.peakAlertTitlePeak' | 'billing.peakAlertTitleOff' | 'billing.peakAlert' | 'billing.peakAlertLeadMin' | 'billing.peakAlertPos' | 'billing.peakAlertMode' | 'billing.peakAlertPosCorner' | 'billing.peakAlertPosCenter' | 'billing.peakAlertModePeak' | 'billing.peakAlertModeOff' | 'billing.peakAlertModeBoth' | 'billing.peakAlertWebNotify' | 'billing.peakAlertPreview' | 'billing.planTypeCode' | 'billing.planTypeToken' | 'billing.subscriptionFeePerMonth' | 'billing.triggerToday' | 'billing.triggerMonth' | 'billing.subscriptionIncluded' | 'billing.free' | 'billing.official' | 'billing.thirdParty' | 'billing.officialCost' | 'billing.thirdPartyCost' | 'billing.perfSamples' | 'billing.perfTtft' | 'billing.perfP50' | 'billing.perfP90' | 'billing.perfTps' | 'billing.perfLatency' | 'billing.perfEstimated' | 'billing.perfEmpty' | 'billing.perfTpsUnit' | 'billing.perfTitle' | 'billing.perfHint' | 'billing.heatmapYear' | 'billing.heatmapMonth' | 'billing.activeDays' | 'billing.streakDays' | 'billing.subscriptionAutoDetect' | 'billing.pluginInfo' | 'billing.pluginName' | 'billing.pluginDescription' | 'billing.pluginVersion' | 'billing.pluginAuthor' | 'billing.pluginRepository' | 'billing.pluginNpm' | 'billing.pluginLicense' | 'billing.tabToken' | 'billing.tokenExport' | 'billing.tokenExportCsv' | 'billing.tokenCacheHitRate' | 'billing.tokenReasoningShare' | 'billing.tokenReasoningShort' | 'billing.tokenIo' | 'billing.tokenPeak' | 'billing.tokenDaily' | 'billing.tokenByModel' | 'billing.tokenMiss' | 'billing.tokenHit' | 'billing.tokenOutput' | 'billing.tokenTotal' | 'billing.tokenShare' | 'billing.usageStatsTool' | 'billing.usageStatsToolHint' | 'billing.balanceGranted' | 'billing.balanceTopped' | 'billing.balanceDaily' | 'billing.balanceDaysLong' | 'billing.balanceDaysUnit' | 'billing.popTodayModel' | 'billing.popNoConsumption' | 'billing.popQuotaAlert' | 'billing.popRiskNone' | 'billing.popTitle' | 'billing.popDirectLead' | 'billing.popSubLead' | 'billing.popBalanceNormal' | 'billing.popBalanceLow' | 'billing.popQuotaNormal' | 'billing.popQuotaLow' | 'billing.alertBalanceLow' | 'billing.alertQuotaLow' | 'billing.unpricedHint' | 'billing.searchEstimateHint' | 'billing.siteListDisplay' | 'billing.siteListDisplayHint' | 'billing.exportCsvSite' | 'billing.panelRelay' | 'billing.relaySite' | 'billing.relayDirect' | 'billing.relayUnknown' | 'billing.panelRelayQuota' | 'billing.relayBalance' | 'billing.relayNoQuota' | 'billing.relayWindowUsed' | 'billing.relayKindNewApi' | 'billing.relayKindSub2Api' | 'billing.relayKindUnknown' | 'billing.relayCalls';
3
3
  export declare const NS = "usageBilling";
4
4
  export declare const zh: Record<UsageBillingKey, string>;
5
5
  export declare const en: Record<UsageBillingKey, string>;
@@ -41,6 +41,12 @@ export interface UserPrice {
41
41
  output: number;
42
42
  /** 计价币种;缺省 CNY。 */
43
43
  currency?: 'CNY' | 'USD';
44
+ /** 低谷档三桶(元或美元 / 每百万 token);缺省 = 平档(峰谷同价)。 */
45
+ offPeak?: {
46
+ input: number;
47
+ cacheHit: number;
48
+ output: number;
49
+ };
44
50
  }
45
51
  /** 一条用户自定义价:绑定「模型(计费目录键)+ 可选来源(中转站 origin)」。
46
52
  * origin 缺省 = 该模型的默认价;带 origin = 仅该中转站的同名模型用此价。 */
@@ -50,6 +56,22 @@ export interface UserPriceEntry extends UserPrice {
50
56
  /** 绑定来源(中转站 origin,如 `https://api.my-relay.com`);缺省 = 默认价。 */
51
57
  origin?: string;
52
58
  }
59
+ /**
60
+ * 中转站 origin 宽松匹配:双方规范化到 `protocol://host[:port]` 后比较。
61
+ * 宿主侧站点桶的 origin 来自 `new URL(baseURL).origin`,用户手填的来源常缺
62
+ * 协议、带路径或尾斜杠——精确全等会让自定义价静默失效(issue #18)。
63
+ * 规范化失败(无法解析成 URL)时回退小写去尾斜杠的字面比较。
64
+ * @param a - 用户录入的来源(可缺协议/带路径)。
65
+ * @param b - 宿主站点桶的 origin(`new URL().origin` 形态)。
66
+ */
67
+ /**
68
+ * 把用户手填的中转站来源规范化为 `protocol://host[:port]` 形态:缺协议补
69
+ * `https://`、带路径取 origin。无法解析时回退小写去尾斜杠的字面值。
70
+ * 与 {@link originsMatch} 的比较口径一致——保存前规范化一次,匹配时双向兜底。
71
+ * @param raw - 用户录入的来源(可缺协议/带路径)。
72
+ */
73
+ export declare function normalizeOriginInput(raw: string): string;
74
+ export declare function originsMatch(a: string, b: string): boolean;
53
75
  /**
54
76
  * 注入用户自定义单价列表。每条含模型目录键 + 可选来源(origin)。空数组 = 清除全部
55
77
  * 自定义价,回退内置目录。
@@ -65,6 +87,20 @@ export declare function getUserPrices(): Readonly<UserPriceEntry[]> | undefined;
65
87
  * @param origin - 调用来源(中转站 origin);缺省仅查默认价。
66
88
  */
67
89
  export declare function userPriceOf(key: string, origin?: string): UserPrice | undefined;
90
+ /**
91
+ * 查一个模型(可选来源)的完整用户价条目(含 origin 绑定信息)。
92
+ * 匹配优先级:origin 宽松精确命中(模型×来源)→ 无来源默认价。
93
+ * @param key - 计费目录键。
94
+ * @param origin - 调用来源(中转站 origin);缺省仅查默认价。
95
+ */
96
+ export declare function userPriceEntryOf(key: string, origin?: string): UserPriceEntry | undefined;
97
+ /**
98
+ * 查一个模型的「带来源」用户价条目(无视来源值,取第一条命中模型名的
99
+ * 带 origin 条目)。供 recost 在三维站点数据缺失时兜底:用户填了来源价
100
+ * 就按它重估,而不是静默回退宿主原价(issue #18)。
101
+ * @param key - 计费目录键。
102
+ */
103
+ export declare function userOriginPriceEntryOf(key: string): UserPriceEntry | undefined;
68
104
  /**
69
105
  * Apply the node half's live pricing snapshot. Absent fields keep the
70
106
  * built-in catalog and rate; callers never fabricate values.
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.11",
4
+ "version": "1.0.13",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -83,11 +83,14 @@
83
83
  "clsx": "^2.1.1"
84
84
  },
85
85
  "devDependencies": {
86
+ "@deepseek-ai/dsh-api-remotes": "*",
87
+ "@deepseek-ai/dsh-api-session-controller": "*",
86
88
  "@deepseek-ai/dsh-client-locale": "*",
87
- "@deepseek-ai/dsh-client-runtime": "*",
89
+ "@deepseek-ai/dsh-client-store": "*",
88
90
  "@deepseek-ai/dsh-client-test-runtime": "*",
89
91
  "@deepseek-ai/dsh-client-ui-conversation": "*",
90
92
  "@deepseek-ai/dsh-client-ui-primitives": "*",
93
+ "@deepseek-ai/dsh-client-ui-renderer": "*",
91
94
  "@deepseek-ai/dsh-client-ui-slots": "*",
92
95
  "@deepseek-ai/dsh-invariants": "*",
93
96
  "@testing-library/react": "^16.1.0",