@kenz1117/dsh-ui-usage-billing 1.4.0 → 1.4.2
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/README.en.md +5 -5
- package/README.md +5 -5
- package/lib/client.js +2 -2
- package/lib/index.js +56 -18
- package/lib/types/index.d.ts +4 -3
- package/package.json +6 -2
package/lib/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { SessionLogOffset } from "@deepseek-ai/dsh-session/types";
|
|
|
6
6
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
7
7
|
import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
8
8
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
9
|
+
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
9
10
|
import z from "@deepseek-ai/schemastery";
|
|
10
11
|
import { createHash, createHmac } from "node:crypto";
|
|
11
12
|
//#region lib/types/client/plan-knowledge.js
|
|
@@ -316,7 +317,7 @@ function tierAtWithBounds(timeMs, bounds) {
|
|
|
316
317
|
}
|
|
317
318
|
/** 时刻是否落在北京时间周末(周六/周日)。 */
|
|
318
319
|
function isBeijingWeekend(timeMs) {
|
|
319
|
-
const day = new Date(timeMs +
|
|
320
|
+
const day = new Date(timeMs + 288e5).getUTCDay();
|
|
320
321
|
return day === 0 || day === 6;
|
|
321
322
|
}
|
|
322
323
|
/** 峰谷切换边界(北京时间的当日分钟数):09:00 / 12:00 / 14:00 / 18:00。 */
|
|
@@ -328,7 +329,7 @@ const TIER_BOUNDARY_MINUTES = [
|
|
|
328
329
|
];
|
|
329
330
|
/** 北京时间的当日毫秒数(0–86,400,000)。 */
|
|
330
331
|
function beijingMillisOfDay(timeMs) {
|
|
331
|
-
return ((timeMs +
|
|
332
|
+
return ((timeMs + 288e5) % 864e5 + 864e5) % 864e5;
|
|
332
333
|
}
|
|
333
334
|
/**
|
|
334
335
|
* Built-in catalog of current mainstream models as of 2026-08-16, priced from
|
|
@@ -445,6 +446,18 @@ const MODEL_CATALOG = [
|
|
|
445
446
|
note: "限时 5 折"
|
|
446
447
|
}
|
|
447
448
|
},
|
|
449
|
+
{
|
|
450
|
+
key: "glm-5.3-flashx",
|
|
451
|
+
name: "GLM-5.3-FlashX",
|
|
452
|
+
provider: "智谱 AI",
|
|
453
|
+
colorVar: "dsw-static-blue-300",
|
|
454
|
+
price: {
|
|
455
|
+
currency: "CNY",
|
|
456
|
+
input: 2,
|
|
457
|
+
cacheHit: .57,
|
|
458
|
+
output: 7
|
|
459
|
+
}
|
|
460
|
+
},
|
|
448
461
|
{
|
|
449
462
|
key: "glm-4.6",
|
|
450
463
|
name: "GLM-4.6",
|
|
@@ -881,6 +894,18 @@ const MODEL_CATALOG = [
|
|
|
881
894
|
output: 4
|
|
882
895
|
}
|
|
883
896
|
},
|
|
897
|
+
{
|
|
898
|
+
key: "hunyuan-hy4-preview",
|
|
899
|
+
name: "混元 Hy4 Preview",
|
|
900
|
+
provider: "腾讯混元",
|
|
901
|
+
colorVar: "dsw-static-amber-400",
|
|
902
|
+
price: {
|
|
903
|
+
currency: "CNY",
|
|
904
|
+
input: 6,
|
|
905
|
+
cacheHit: .3,
|
|
906
|
+
output: 18
|
|
907
|
+
}
|
|
908
|
+
},
|
|
884
909
|
{
|
|
885
910
|
key: "hunyuan-t1",
|
|
886
911
|
name: "混元 T1",
|
|
@@ -1479,6 +1504,8 @@ const MODEL_KEY_ALIASES = {
|
|
|
1479
1504
|
"glm-5-turbo": "glm-5-turbo",
|
|
1480
1505
|
"glm-5.1": "glm-5.1",
|
|
1481
1506
|
"glm-5.3-flash": "glm-5.3-flash",
|
|
1507
|
+
"glm-5.3-flashx": "glm-5.3-flashx",
|
|
1508
|
+
"glm-5.3-flash-x": "glm-5.3-flashx",
|
|
1482
1509
|
"glm-5v-turbo": "glm-5v-turbo",
|
|
1483
1510
|
"glm-5v.1": "glm-5v-turbo",
|
|
1484
1511
|
"claude-opus-4-6": "claude-opus-4-6",
|
|
@@ -1500,6 +1527,10 @@ const MODEL_KEY_ALIASES = {
|
|
|
1500
1527
|
"command-r-08-2024": "command-r-08-2024",
|
|
1501
1528
|
"command-r": "command-r-08-2024",
|
|
1502
1529
|
"hy3": "hunyuan",
|
|
1530
|
+
"hy4-preview": "hunyuan-hy4-preview",
|
|
1531
|
+
"hy4": "hunyuan-hy4-preview",
|
|
1532
|
+
"hunyuan-hy4-preview": "hunyuan-hy4-preview",
|
|
1533
|
+
"hunyuan-hy4": "hunyuan-hy4-preview",
|
|
1503
1534
|
"longcat-2.0": "longcat-2.0",
|
|
1504
1535
|
"longcat-2": "longcat-2.0",
|
|
1505
1536
|
"minicpm-v-4.5": "minicpm-v-4.5",
|
|
@@ -1530,6 +1561,7 @@ const MODEL_KEY_ALIASES = {
|
|
|
1530
1561
|
"glm-5.2f": "glm-5.2-fast",
|
|
1531
1562
|
"kimi-k3-fast": "kimi-k3-fast",
|
|
1532
1563
|
"kimi-k3f": "kimi-k3-fast",
|
|
1564
|
+
"kimi-for-coding": "kimi-k2.8-preview",
|
|
1533
1565
|
"kimi-k2.7-code-fast": "kimi-k2.7-code-fast",
|
|
1534
1566
|
"kimi-k2.7-code-f": "kimi-k2.7-code-fast",
|
|
1535
1567
|
"kimi-k2.6-fast": "kimi-k2.6-fast",
|
|
@@ -3084,8 +3116,8 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
3084
3116
|
};
|
|
3085
3117
|
const toModelDayRecord = (map) => Object.fromEntries([...map].map(([day, models]) => [day, Object.fromEntries(models)]));
|
|
3086
3118
|
const toModelDaySiteRecord = (map) => Object.fromEntries([...map].map(([day, models]) => [day, Object.fromEntries([...models].map(([model, sites]) => [model, Object.fromEntries(sites)]))]));
|
|
3087
|
-
const modelKeys = new Set([...perfModel.keys(), ...perfModelDigest.keys()]);
|
|
3088
|
-
const hourKeys = new Set([...perfHourModel.keys(), ...perfHourDigest.keys()]);
|
|
3119
|
+
const modelKeys = /* @__PURE__ */ new Set([...perfModel.keys(), ...perfModelDigest.keys()]);
|
|
3120
|
+
const hourKeys = /* @__PURE__ */ new Set([...perfHourModel.keys(), ...perfHourDigest.keys()]);
|
|
3089
3121
|
const perf = modelKeys.size === 0 && hourKeys.size === 0 ? void 0 : {
|
|
3090
3122
|
byModel: Object.fromEntries([...modelKeys].map((model) => {
|
|
3091
3123
|
const live = perfModel.get(model);
|
|
@@ -3096,7 +3128,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
3096
3128
|
byHourModel: Object.fromEntries([...hourKeys].sort().map((hour) => {
|
|
3097
3129
|
const liveModels = perfHourModel.get(hour);
|
|
3098
3130
|
const digestModels = perfHourDigest.get(hour);
|
|
3099
|
-
const modelKeysInHour = new Set([...liveModels?.keys() ?? [], ...digestModels?.keys() ?? []]);
|
|
3131
|
+
const modelKeysInHour = /* @__PURE__ */ new Set([...liveModels?.keys() ?? [], ...digestModels?.keys() ?? []]);
|
|
3100
3132
|
return [hour, Object.fromEntries([...modelKeysInHour].map((model) => {
|
|
3101
3133
|
const live = liveModels?.get(model);
|
|
3102
3134
|
const restored = digestModels?.get(model);
|
|
@@ -3299,7 +3331,11 @@ function tc3Authorization(secretId, secretKey, payload, timestamp) {
|
|
|
3299
3331
|
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")}`;
|
|
3300
3332
|
const hashedCanonical = createHash("sha256").update(canonicalRequest).digest("hex");
|
|
3301
3333
|
const stringToSign = `TC3-HMAC-SHA256\n${String(timestamp)}\n${date}/${TOKENHUB_SERVICE}/tc3_request\n${hashedCanonical}`;
|
|
3302
|
-
|
|
3334
|
+
const kDate = createHmac("sha256", date).update(secretKey).digest();
|
|
3335
|
+
const kService = createHmac("sha256", kDate).update(TOKENHUB_SERVICE).digest();
|
|
3336
|
+
const kSigning = createHmac("sha256", kService).update("tc3_request").digest();
|
|
3337
|
+
const signature = createHmac("sha256", kSigning).update(stringToSign).digest("hex");
|
|
3338
|
+
return `TC3-HMAC-SHA256 Credential=${secretId}/${date}/${TOKENHUB_SERVICE}/tc3_request, SignedHeaders=content-type;host, Signature=${signature}`;
|
|
3303
3339
|
}
|
|
3304
3340
|
/**
|
|
3305
3341
|
* 调用一次 TokenHub 管控面接口:TC3 签名 + 超时保护,返回响应 JSON 的 `Response`。
|
|
@@ -4063,7 +4099,7 @@ const declaredGate = createCooldownGate({
|
|
|
4063
4099
|
cooldownMs: 6e4
|
|
4064
4100
|
});
|
|
4065
4101
|
/** 会触及原型链的路径段:一律拒绝(读结果不是文档自带字段)。 */
|
|
4066
|
-
const FORBIDDEN_SEGMENTS = new Set([
|
|
4102
|
+
const FORBIDDEN_SEGMENTS = /* @__PURE__ */ new Set([
|
|
4067
4103
|
"__proto__",
|
|
4068
4104
|
"constructor",
|
|
4069
4105
|
"prototype"
|
|
@@ -4613,7 +4649,7 @@ function asPrice(value) {
|
|
|
4613
4649
|
*/
|
|
4614
4650
|
function buildExtraModels(data) {
|
|
4615
4651
|
if (data === null || typeof data !== "object") return [];
|
|
4616
|
-
const catalogKeys = new Set([...MODEL_CATALOG.map((entry) => entry.key.toLowerCase()), ...Object.keys(MODEL_KEY_ALIASES).map((key) => MODEL_KEY_ALIASES[key]?.toLowerCase() ?? key.toLowerCase())]);
|
|
4652
|
+
const catalogKeys = /* @__PURE__ */ new Set([...MODEL_CATALOG.map((entry) => entry.key.toLowerCase()), ...Object.keys(MODEL_KEY_ALIASES).map((key) => MODEL_KEY_ALIASES[key]?.toLowerCase() ?? key.toLowerCase())]);
|
|
4617
4653
|
const extras = [];
|
|
4618
4654
|
for (const [providerId, providerDoc] of Object.entries(data)) {
|
|
4619
4655
|
if (providerDoc === null || typeof providerDoc !== "object") continue;
|
|
@@ -5463,7 +5499,7 @@ const relayGate = createCooldownGate({
|
|
|
5463
5499
|
/** Abort a relay fetch when the upstream hangs beyond this budget. */
|
|
5464
5500
|
const FETCH_TIMEOUT_MS = 8e3;
|
|
5465
5501
|
/** 指纹识别缓存 TTL(毫秒):识别结果低频变化,5 分钟内同 origin 不再重复探测。 */
|
|
5466
|
-
const FINGERPRINT_TTL_MS =
|
|
5502
|
+
const FINGERPRINT_TTL_MS = 3e5;
|
|
5467
5503
|
/** 每 origin 的识别结果缓存:`kind` 是识别出的中转站程序,`at` 是探测时刻。 */
|
|
5468
5504
|
const fingerprintCache = /* @__PURE__ */ new Map();
|
|
5469
5505
|
/** Number, or null when the value is not a finite number (nor numeric string). */
|
|
@@ -5599,7 +5635,7 @@ function originOf(baseURL) {
|
|
|
5599
5635
|
* 子路径只会得到 404/非中转站格式,因而面板应排除它们,避免误判为
|
|
5600
5636
|
* 「未识别」。中转站面板只列真正的第三方中转程序。
|
|
5601
5637
|
*/
|
|
5602
|
-
const OFFICIAL_HOSTS = new Set([
|
|
5638
|
+
const OFFICIAL_HOSTS = /* @__PURE__ */ new Set([
|
|
5603
5639
|
"api.deepseek.com",
|
|
5604
5640
|
"api.openai.com",
|
|
5605
5641
|
"open.bigmodel.cn",
|
|
@@ -5852,13 +5888,13 @@ const usageBillingSettingsNs = validateSettingsNamespace(BILLING_SETTINGS_NAMESP
|
|
|
5852
5888
|
/** 该命名空间的 wire schema:`enableUsageStatsTool` 布尔,默认关闭(issue 诉求)。 */
|
|
5853
5889
|
const UsageBillingSettingsSchema = z.object({ [ENABLE_USAGE_STATS_TOOL_FIELD]: z.boolean().default(false) });
|
|
5854
5890
|
/** 实时定价的后台刷新间隔(毫秒):汇率/模型价低频变化,6 小时一次足够。 */
|
|
5855
|
-
const PRICING_REFRESH_INTERVAL_MS =
|
|
5891
|
+
const PRICING_REFRESH_INTERVAL_MS = 216e5;
|
|
5856
5892
|
/** 历史回放预热的延迟:等宿主启动高峰(插件加载 / 路由挂载)过去再全量折叠,
|
|
5857
5893
|
* 避免抢启动期的 CPU;纯延迟不阻塞任何请求,面板提前打开也只会提前聚合。 */
|
|
5858
5894
|
const WARMUP_DELAY_MS = 3e3;
|
|
5859
5895
|
/** 订阅套餐额度缓存时长(毫秒):上游配额 API 低频变化,5 分钟足够。 */
|
|
5860
|
-
const SUBSCRIPTION_CACHE_MS =
|
|
5861
|
-
const BALANCE_CACHE_MS =
|
|
5896
|
+
const SUBSCRIPTION_CACHE_MS = 3e5;
|
|
5897
|
+
const BALANCE_CACHE_MS = 3e5;
|
|
5862
5898
|
/** DeepSeek 余额查询的默认凭据引用(与 llm-deepseek 的默认引用一致)。 */
|
|
5863
5899
|
const DEFAULT_BALANCE_API_KEY_ENV = "DEEPSEEK_API_KEY";
|
|
5864
5900
|
/**
|
|
@@ -5875,7 +5911,7 @@ const SNAPSHOT_INTERVAL_MS = 3e5;
|
|
|
5875
5911
|
* 轻量用户暖缓存下的聚合是毫秒级,预算几乎不会触发。 */
|
|
5876
5912
|
const RESPONSE_BUDGET_MS = 1500;
|
|
5877
5913
|
/** 鉴权失败告警冷却(毫秒):同一 provider 在窗口内只提示一次,避免 30 秒轮询刷屏。 */
|
|
5878
|
-
const AUTH_WARN_COOLDOWN_MS =
|
|
5914
|
+
const AUTH_WARN_COOLDOWN_MS = 18e5;
|
|
5879
5915
|
/**
|
|
5880
5916
|
* 鉴权失败分类告警(P1-5):余额 / 订阅查询返回 unauthorized 时,按
|
|
5881
5917
|
* `source:provider` 去重并冷却告警,提示检查 llm-pi-ai 里该 provider 的 apiKeyEnv。
|
|
@@ -6209,9 +6245,10 @@ function adaptSessionPersistence(raw) {
|
|
|
6209
6245
|
function apply(ctx, config = {}) {
|
|
6210
6246
|
let usageSettingsScope;
|
|
6211
6247
|
const cwd = process.cwd();
|
|
6212
|
-
const
|
|
6213
|
-
const
|
|
6214
|
-
const
|
|
6248
|
+
const dshHome = resolveDshHome();
|
|
6249
|
+
const snapshotPath = config.snapshotPath ?? join(dshHome, ".dsh-usage-stats.json");
|
|
6250
|
+
const ledgerStore = createFileUsageLedgerStore(config.ledgerPath ?? join(dshHome, ".dsh-usage-ledger.json"));
|
|
6251
|
+
const reconcilePath = config.reconcilePath ?? join(dshHome, ".dsh-usage-reconcile.json");
|
|
6215
6252
|
let reconcileRef = null;
|
|
6216
6253
|
(async () => {
|
|
6217
6254
|
try {
|
|
@@ -6224,7 +6261,8 @@ function apply(ctx, config = {}) {
|
|
|
6224
6261
|
})();
|
|
6225
6262
|
const persistReconcileRef = () => {
|
|
6226
6263
|
if (reconcileRef === null) return;
|
|
6227
|
-
|
|
6264
|
+
const payload = JSON.stringify({ ref: reconcileRef });
|
|
6265
|
+
writeFileAtomic(reconcilePath, payload, {
|
|
6228
6266
|
mode: 384,
|
|
6229
6267
|
dirMode: 448
|
|
6230
6268
|
}).catch(() => {});
|
package/lib/types/index.d.ts
CHANGED
|
@@ -31,13 +31,14 @@ export declare function guardLoopback(req: IncomingMessage, res: ServerResponse)
|
|
|
31
31
|
export interface UsageBillingConfig {
|
|
32
32
|
/** Absolute path to a `.dsh-usage-stats.json` fallback file. */
|
|
33
33
|
statsPath?: string;
|
|
34
|
-
/** 统计快照的持久化路径;默认
|
|
34
|
+
/** 统计快照的持久化路径;默认 `<harness home>/.dsh-usage-stats.json`
|
|
35
|
+
* (harness home = `DSH_HOME` 环境变量或 `~/.dsh`,见 resolveDshHome)。
|
|
35
36
|
* 测试注入临时目录以隔离真实家目录(聚合失败回退与快照落盘都走此路径)。 */
|
|
36
37
|
snapshotPath?: string;
|
|
37
|
-
/** 独立持久用量账本的绝对路径;默认
|
|
38
|
+
/** 独立持久用量账本的绝对路径;默认 `<harness home>/.dsh-usage-ledger.json`。
|
|
38
39
|
* 账本与会话日志解耦,因此永久删除会话不会抹掉已经观测到的用量。 */
|
|
39
40
|
ledgerPath?: string;
|
|
40
|
-
/** 余额差对账基准的持久化路径;默认
|
|
41
|
+
/** 余额差对账基准的持久化路径;默认 `<harness home>/.dsh-usage-reconcile.json`。 */
|
|
41
42
|
reconcilePath?: string;
|
|
42
43
|
/** 订阅制(coding / token / agent plan)provider id 列表;默认 kimi-coding、xiaomi-token-plan-cn。 */
|
|
43
44
|
subscriptionProviders?: string[];
|
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.
|
|
4
|
+
"version": "1.4.2",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
7
7
|
"deepseek",
|
|
@@ -65,7 +65,9 @@
|
|
|
65
65
|
"0.1.5-alpha.1": "compatible",
|
|
66
66
|
"0.1.5-alpha.2": "compatible",
|
|
67
67
|
"0.1.5-rc.1": "compatible",
|
|
68
|
-
"0.1.5-rc.2": "compatible"
|
|
68
|
+
"0.1.5-rc.2": "compatible",
|
|
69
|
+
"0.1.6-alpha.1": "compatible",
|
|
70
|
+
"0.1.6-alpha.2": "compatible"
|
|
69
71
|
}
|
|
70
72
|
}
|
|
71
73
|
},
|
|
@@ -93,6 +95,7 @@
|
|
|
93
95
|
"@deepseek-ai/dsh-client-ui-slots": "*",
|
|
94
96
|
"@deepseek-ai/dsh-credentials": "*",
|
|
95
97
|
"@deepseek-ai/dsh-host-webserver": "*",
|
|
98
|
+
"@deepseek-ai/dsh-home-paths": "*",
|
|
96
99
|
"@deepseek-ai/dsh-invariants": "*",
|
|
97
100
|
"@deepseek-ai/dsh-llm": "*",
|
|
98
101
|
"@deepseek-ai/dsh-session": "*",
|
|
@@ -122,6 +125,7 @@
|
|
|
122
125
|
"@deepseek-ai/dsh-client-ui-slots": "0.1.2-rc.1",
|
|
123
126
|
"@deepseek-ai/dsh-credentials": "0.1.2-rc.1",
|
|
124
127
|
"@deepseek-ai/dsh-host-webserver": "0.1.2-rc.1",
|
|
128
|
+
"@deepseek-ai/dsh-home-paths": "0.1.2-rc.1",
|
|
125
129
|
"@deepseek-ai/dsh-invariants": "0.1.2-rc.1",
|
|
126
130
|
"@deepseek-ai/dsh-llm": "0.1.2-rc.1",
|
|
127
131
|
"@deepseek-ai/dsh-scope": "0.1.2-rc.1",
|