@kenz1117/dsh-ui-usage-billing 0.9.19 → 0.9.20
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 +2 -0
- package/README.md +2 -0
- package/lib/client.js +460 -416
- package/lib/index.js +160 -50
- package/lib/types/client/TokenPanel.d.ts +0 -2
- package/lib/types/client/UsageBilling.d.ts +1 -0
- package/lib/types/client/pricing.d.ts +3 -2
- package/lib/types/client/usage-billing-settings.d.ts +2 -1
- package/package.json +28 -28
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, stat } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
5
|
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";
|
|
@@ -184,7 +184,7 @@ let liveExtraModels;
|
|
|
184
184
|
* @param pricing - the `/api/billing/pricing` response.
|
|
185
185
|
*/
|
|
186
186
|
function applyLivePricing(pricing) {
|
|
187
|
-
liveRate = pricing.rate;
|
|
187
|
+
liveRate = typeof pricing.rate === "number" && Number.isFinite(pricing.rate) && pricing.rate > 0 ? pricing.rate : void 0;
|
|
188
188
|
livePrices = pricing.prices;
|
|
189
189
|
liveExtraModels = pricing.extraModels;
|
|
190
190
|
}
|
|
@@ -742,7 +742,10 @@ const MODEL_KEY_ALIASES = {
|
|
|
742
742
|
"step-3.7-flash": "step",
|
|
743
743
|
"seed-2.0-mini": "doubao-mini",
|
|
744
744
|
"k3": "kimi-k3",
|
|
745
|
-
"kimi-k3": "kimi-k3"
|
|
745
|
+
"kimi-k3": "kimi-k3",
|
|
746
|
+
"minimax-m1": "minimax",
|
|
747
|
+
"minimax-m2": "minimax",
|
|
748
|
+
"minimax-m3": "minimax"
|
|
746
749
|
};
|
|
747
750
|
/**
|
|
748
751
|
* 模型 id 归一化:小写、去括号附注(如 `gpt5.6 luna(go)` 只看主体)、再去所有
|
|
@@ -892,7 +895,7 @@ function computeCost(entry, buckets, peakShare = DEFAULT_PEAK_SHARE) {
|
|
|
892
895
|
* @param buckets - token usage counts.
|
|
893
896
|
* @param timeMs - the call's wall-clock time (epoch ms); null falls back to the peak-share mix.
|
|
894
897
|
* @param peakShare - fallback mix used only when `timeMs` is missing.
|
|
895
|
-
* @returns the estimated cost in
|
|
898
|
+
* @returns the estimated cost in CNY(USD 计价模型已按当前汇率折算)。
|
|
896
899
|
*/
|
|
897
900
|
function computeCostAt(entry, buckets, timeMs, peakShare = DEFAULT_PEAK_SHARE) {
|
|
898
901
|
if (entry.price.offPeak === void 0) return priceBandCost(entry.price, buckets, entry.price.currency);
|
|
@@ -1457,11 +1460,12 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
1457
1460
|
console.warn("[usage-billing] failed to load durable usage ledger; rebuilding from current sessions:", error);
|
|
1458
1461
|
}
|
|
1459
1462
|
};
|
|
1460
|
-
/** 失效键:日志文件的 mtime+size;拿不到(后端无 locate /
|
|
1463
|
+
/** 失效键:日志文件的 mtime+size;拿不到(后端无 locate / 文件丢失 / locate 抛错)时返回 null,
|
|
1464
|
+
* 让调用方每次重折。locate 调用也纳入 try,避免单个会话的 locate 异常把整份聚合拖垮。 */
|
|
1461
1465
|
const stampOf = async (meta) => {
|
|
1462
|
-
const location = persistence.locate?.(meta);
|
|
1463
|
-
if (location === void 0) return null;
|
|
1464
1466
|
try {
|
|
1467
|
+
const location = persistence.locate?.(meta);
|
|
1468
|
+
if (location === void 0) return null;
|
|
1465
1469
|
const info = await stat(location.path);
|
|
1466
1470
|
return `${String(info.mtimeMs)}:${String(info.size)}`;
|
|
1467
1471
|
} catch {
|
|
@@ -2081,9 +2085,20 @@ const QUERIERS = [
|
|
|
2081
2085
|
* @returns the balance rows (one per provider).
|
|
2082
2086
|
*/
|
|
2083
2087
|
async function queryBalances(ctx, providers) {
|
|
2084
|
-
|
|
2088
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2089
|
+
for (const { route, querier, displayName } of QUERIERS) {
|
|
2085
2090
|
const env = providers[route]?.apiKeyEnv;
|
|
2086
|
-
|
|
2091
|
+
const configured = typeof env === "string" && env !== "";
|
|
2092
|
+
const existing = byName.get(displayName);
|
|
2093
|
+
if (existing !== void 0 && existing.env !== void 0) continue;
|
|
2094
|
+
byName.set(displayName, {
|
|
2095
|
+
displayName,
|
|
2096
|
+
querier,
|
|
2097
|
+
env: configured ? env : void 0
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
return await Promise.all([...byName.values()].map(({ querier, displayName, env }) => {
|
|
2101
|
+
if (env === void 0) return Promise.resolve({
|
|
2087
2102
|
provider: displayName,
|
|
2088
2103
|
displayName,
|
|
2089
2104
|
error: "unconfigured"
|
|
@@ -3213,15 +3228,16 @@ async function collectZai(keys, config, timeoutMs) {
|
|
|
3213
3228
|
function goWindow(value, kind) {
|
|
3214
3229
|
if (value === null || typeof value !== "object") return null;
|
|
3215
3230
|
const record = value;
|
|
3216
|
-
const
|
|
3217
|
-
|
|
3231
|
+
const ratioSource = record.usagePercent ?? record.usedPercent ?? record.percentUsed ?? record.percentage;
|
|
3232
|
+
const fromRatioField = ratioSource !== void 0 && ratioSource !== null;
|
|
3233
|
+
let usedPercent = clampPercent$1(numberOrNull$1(fromRatioField ? ratioSource : record.percent));
|
|
3218
3234
|
if (usedPercent === null) {
|
|
3219
3235
|
const used = numberOrNull$1(record.used ?? record.consumed);
|
|
3220
3236
|
const limit = numberOrNull$1(record.limit ?? record.total ?? record.quota);
|
|
3221
3237
|
if (used !== null && limit !== null && limit > 0) usedPercent = clampPercent$1(used / limit * 100);
|
|
3222
3238
|
}
|
|
3223
3239
|
if (usedPercent === null) return null;
|
|
3224
|
-
if (usedPercent <= 1 && usedPercent >= 0
|
|
3240
|
+
if (fromRatioField && usedPercent <= 1 && usedPercent >= 0) usedPercent *= 100;
|
|
3225
3241
|
const resetSeconds = numberOrNull$1(record.resetInSec ?? record.resetInSeconds ?? record.resetSeconds);
|
|
3226
3242
|
const resetsAt = resetSeconds === null ? toIso(record.resetAt ?? record.resetsAt ?? record.nextReset) : new Date(Date.now() + Math.max(0, resetSeconds) * 1e3).toISOString();
|
|
3227
3243
|
return {
|
|
@@ -3329,6 +3345,18 @@ function resolveMiniMaxBaseUrl(config) {
|
|
|
3329
3345
|
function minmaxDisplayName(provider) {
|
|
3330
3346
|
return SUBSCRIPTION_DISPLAY_NAMES[provider] ?? "MiniMax Coding Plan";
|
|
3331
3347
|
}
|
|
3348
|
+
/** MiniMax Token Plan / Coding Plan 的 key 判别:普通按量 API key 以 `sk-` 开头,
|
|
3349
|
+
* Token Plan(订阅套餐)key 以 `sk-cp-` 开头。据此区分一个 key 是订阅还是按量,
|
|
3350
|
+
* 供查询失败时给出「key 类型 / region」定向提示,避免把按量 key 误当 token-plan
|
|
3351
|
+
* 查询后只剩一句笼统的「响应异常」。 */
|
|
3352
|
+
function isMiniMaxTokenPlanKey(apiKey) {
|
|
3353
|
+
return apiKey.startsWith("sk-cp-");
|
|
3354
|
+
}
|
|
3355
|
+
/** 构造 MiniMax Token Plan 查询端点:避免 base 已含 `/v1` 时拼出 `/v1/v1/...`。 */
|
|
3356
|
+
function miniMaxEndpoint(base) {
|
|
3357
|
+
const trimmed = base.replace(/\/+$/, "");
|
|
3358
|
+
return trimmed.endsWith("/v1") ? `${trimmed}/token_plan/remains` : `${trimmed}/v1/token_plan/remains`;
|
|
3359
|
+
}
|
|
3332
3360
|
/** Collect the MiniMax Token Plan quota (CN + INTL). */
|
|
3333
3361
|
async function collectMiniMax(keys, config, timeoutMs) {
|
|
3334
3362
|
const apiKey = keys.minmaxApiKey.trim();
|
|
@@ -3340,16 +3368,25 @@ async function collectMiniMax(keys, config, timeoutMs) {
|
|
|
3340
3368
|
status: "not-configured",
|
|
3341
3369
|
windows: []
|
|
3342
3370
|
};
|
|
3371
|
+
const tokenPlanKey = isMiniMaxTokenPlanKey(apiKey);
|
|
3343
3372
|
try {
|
|
3344
|
-
const windows = parseMiniMaxRemains(await requestJson(
|
|
3373
|
+
const windows = parseMiniMaxRemains(await requestJson(miniMaxEndpoint(base), { headers: {
|
|
3345
3374
|
authorization: `Bearer ${apiKey}`,
|
|
3346
3375
|
accept: "application/json"
|
|
3347
3376
|
} }, timeoutMs));
|
|
3377
|
+
if (windows.length > 0) return {
|
|
3378
|
+
provider: config.provider,
|
|
3379
|
+
displayName,
|
|
3380
|
+
status: "ok",
|
|
3381
|
+
windows,
|
|
3382
|
+
...tokenPlanKey ? {} : { hint: "此 key 未以 sk-cp- 开头(可能为按量 API);但 Token Plan 接口返回了额度,故按 Token Plan 展示" }
|
|
3383
|
+
};
|
|
3348
3384
|
return {
|
|
3349
3385
|
provider: config.provider,
|
|
3350
3386
|
displayName,
|
|
3351
|
-
status:
|
|
3352
|
-
windows
|
|
3387
|
+
status: "invalid-response",
|
|
3388
|
+
windows,
|
|
3389
|
+
hint: tokenPlanKey ? "Token Plan key 未返回额度:可能是 region 不匹配,请在 provider 的 baseURL 指向国内 api.minimaxi.com 或国际 api.minimax.io" : "此 key 未以 sk-cp- 开头,可能为按量 API 而非 Token Plan;如需订阅额度请用 sk-cp- 开头的 Token Plan key"
|
|
3353
3390
|
};
|
|
3354
3391
|
} catch (error) {
|
|
3355
3392
|
return {
|
|
@@ -3628,6 +3665,19 @@ function endpointOf(baseURL, path) {
|
|
|
3628
3665
|
return new URL(path, baseURL).toString();
|
|
3629
3666
|
}
|
|
3630
3667
|
/**
|
|
3668
|
+
* 用已知程序类型查询该路由的额度(用当前 key)。额度按 key 独立,因此即便
|
|
3669
|
+
* 指纹缓存已识别出站点类型,也仍要实际发一次请求读出本 key 的余额/窗口。
|
|
3670
|
+
* @param baseURL - 站点端点。
|
|
3671
|
+
* @param apiKey - 已解析的本路由 key。
|
|
3672
|
+
* @param kind - 已识别出的程序类型。
|
|
3673
|
+
* @returns 解析出的余额/窗口;取不到时返回 null(漂移或端点不可用)。
|
|
3674
|
+
*/
|
|
3675
|
+
async function readRelayByKind(baseURL, apiKey, kind) {
|
|
3676
|
+
const res = await fetchRelayJson(endpointOf(baseURL, kind === "sub2api" ? "/v1/usage" : "/api/status"), apiKey);
|
|
3677
|
+
if (!res.ok) return null;
|
|
3678
|
+
return kind === "sub2api" ? parseSub2ApiUsage(res.data) : parseNewApiStatus(res.data);
|
|
3679
|
+
}
|
|
3680
|
+
/**
|
|
3631
3681
|
* 查询单个中转站路由的额度。先试 Sub2API,再试 New API;任一读出额度即返回。
|
|
3632
3682
|
* @param ctx - host context carrying the credentials seam.
|
|
3633
3683
|
* @param route - 待探测的路由(baseURL + apiKeyEnv)。
|
|
@@ -3645,19 +3695,43 @@ async function queryRelayQuota(ctx, route) {
|
|
|
3645
3695
|
kind: "unknown",
|
|
3646
3696
|
status: "unavailable"
|
|
3647
3697
|
};
|
|
3648
|
-
|
|
3698
|
+
let hit;
|
|
3699
|
+
try {
|
|
3700
|
+
hit = await ctx.credentials.resolve(credentialRef(route.apiKeyEnv));
|
|
3701
|
+
} catch {
|
|
3702
|
+
return {
|
|
3703
|
+
...base,
|
|
3704
|
+
kind: "unknown",
|
|
3705
|
+
status: "not-configured"
|
|
3706
|
+
};
|
|
3707
|
+
}
|
|
3649
3708
|
if (hit === void 0 || hit.value === "") return {
|
|
3650
3709
|
...base,
|
|
3651
3710
|
kind: "unknown",
|
|
3652
3711
|
status: "not-configured"
|
|
3653
3712
|
};
|
|
3654
3713
|
const cached = fingerprintCache.get(base.origin);
|
|
3655
|
-
|
|
3656
|
-
...base,
|
|
3657
|
-
kind: cached.kind,
|
|
3658
|
-
status: "ok"
|
|
3659
|
-
};
|
|
3714
|
+
const knownKind = cached !== void 0 && Date.now() - cached.at < FINGERPRINT_TTL_MS ? cached.kind : void 0;
|
|
3660
3715
|
try {
|
|
3716
|
+
if (knownKind === "sub2api" || knownKind === "new-api") {
|
|
3717
|
+
const parsed = await readRelayByKind(route.baseURL, hit.value, knownKind);
|
|
3718
|
+
if (parsed !== null) {
|
|
3719
|
+
relayGate.success(gateKey);
|
|
3720
|
+
return {
|
|
3721
|
+
...base,
|
|
3722
|
+
kind: knownKind,
|
|
3723
|
+
status: "ok",
|
|
3724
|
+
...parsed.balance !== void 0 ? { balance: parsed.balance } : {},
|
|
3725
|
+
...parsed.windows !== void 0 ? { windows: parsed.windows } : {}
|
|
3726
|
+
};
|
|
3727
|
+
}
|
|
3728
|
+
relayGate.fail(gateKey);
|
|
3729
|
+
return {
|
|
3730
|
+
...base,
|
|
3731
|
+
kind: knownKind,
|
|
3732
|
+
status: "invalid-response"
|
|
3733
|
+
};
|
|
3734
|
+
}
|
|
3661
3735
|
const sub2 = await fetchRelayJson(endpointOf(route.baseURL, "/v1/usage"), hit.value);
|
|
3662
3736
|
if (sub2.ok) {
|
|
3663
3737
|
const parsed = parseSub2ApiUsage(sub2.data);
|
|
@@ -3682,23 +3756,7 @@ async function queryRelayQuota(ctx, route) {
|
|
|
3682
3756
|
status: "invalid-response"
|
|
3683
3757
|
};
|
|
3684
3758
|
}
|
|
3685
|
-
if (sub2.status === 401 || sub2.status === 403) {
|
|
3686
|
-
relayGate.fail(gateKey);
|
|
3687
|
-
return {
|
|
3688
|
-
...base,
|
|
3689
|
-
kind: "unknown",
|
|
3690
|
-
status: "unauthorized"
|
|
3691
|
-
};
|
|
3692
|
-
}
|
|
3693
3759
|
const na = await fetchRelayJson(endpointOf(route.baseURL, "/api/status"), hit.value);
|
|
3694
|
-
if (na.status === 401 || na.status === 403) {
|
|
3695
|
-
relayGate.fail(gateKey);
|
|
3696
|
-
return {
|
|
3697
|
-
...base,
|
|
3698
|
-
kind: "unknown",
|
|
3699
|
-
status: "unauthorized"
|
|
3700
|
-
};
|
|
3701
|
-
}
|
|
3702
3760
|
if (na.ok) {
|
|
3703
3761
|
const parsed = parseNewApiStatus(na.data);
|
|
3704
3762
|
if (parsed !== null) {
|
|
@@ -3728,11 +3786,12 @@ async function queryRelayQuota(ctx, route) {
|
|
|
3728
3786
|
status: "unavailable"
|
|
3729
3787
|
};
|
|
3730
3788
|
} catch (error) {
|
|
3731
|
-
|
|
3789
|
+
const status = statusOf(error);
|
|
3790
|
+
if (status !== "unauthorized") relayGate.fail(gateKey);
|
|
3732
3791
|
return {
|
|
3733
3792
|
...base,
|
|
3734
3793
|
kind: "unknown",
|
|
3735
|
-
status
|
|
3794
|
+
status
|
|
3736
3795
|
};
|
|
3737
3796
|
}
|
|
3738
3797
|
}
|
|
@@ -3779,12 +3838,26 @@ function isLoopbackPeer(req) {
|
|
|
3779
3838
|
if (address === void 0) return false;
|
|
3780
3839
|
return address === "::1" || address.startsWith("127.") || address.startsWith("::ffff:127.");
|
|
3781
3840
|
}
|
|
3782
|
-
/** 校验 Host
|
|
3841
|
+
/** 校验 Host 头是本机回环(精确 127.0.0.0/8 / ::1 / localhost 或空,供 curl 不带 Host 的极简请求)。
|
|
3842
|
+
* 拒绝 `127.0.0.1.attacker.com` 这类以 `127.` 开头但解析到外部的 DNS rebinding 域名:
|
|
3843
|
+
* 只用 `startsWith('127.')` 会被它穿透,必须精确匹配回环 IP 的字面量。 */
|
|
3783
3844
|
function isLoopbackHost(req) {
|
|
3784
3845
|
const host = req.headers.host;
|
|
3785
3846
|
if (host === void 0 || host === "") return true;
|
|
3786
3847
|
const name = host.split(":")[0];
|
|
3787
|
-
return name === "localhost" || name === "::1" || name !== void 0 && name
|
|
3848
|
+
return name === "localhost" || name === "::1" || name !== void 0 && /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(name);
|
|
3849
|
+
}
|
|
3850
|
+
/** 校验 Origin 头是否回环(写操作用,防止跨站表单/fetch 改写设置)。Origin 缺失
|
|
3851
|
+
* (curl 或同源 fetch 不带)视为放行,交给下方的 Content-Type 校验兜底;Origin 存在
|
|
3852
|
+
* 但主机非回环则拒绝——跨站脚本发起的写请求必带攻击者域名的 Origin。 */
|
|
3853
|
+
function isLoopbackOrigin(origin) {
|
|
3854
|
+
if (origin === void 0 || origin === "") return true;
|
|
3855
|
+
try {
|
|
3856
|
+
const host = new URL(origin).hostname;
|
|
3857
|
+
return host === "localhost" || host === "::1" || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
|
|
3858
|
+
} catch {
|
|
3859
|
+
return false;
|
|
3860
|
+
}
|
|
3788
3861
|
}
|
|
3789
3862
|
/**
|
|
3790
3863
|
* 回环防护守卫:仅接受回环 GET 请求(peer socket 地址 + Host 头同时校验)。
|
|
@@ -3849,6 +3922,10 @@ function createFileUsageLedgerStore(ledgerPath) {
|
|
|
3849
3922
|
} catch {}
|
|
3850
3923
|
},
|
|
3851
3924
|
async save(document) {
|
|
3925
|
+
await mkdir(dirname(ledgerPath), {
|
|
3926
|
+
recursive: true,
|
|
3927
|
+
mode: 448
|
|
3928
|
+
});
|
|
3852
3929
|
await withFileLock(ledgerPath, async () => {
|
|
3853
3930
|
try {
|
|
3854
3931
|
const existing = await readFile(ledgerPath, "utf8");
|
|
@@ -4296,7 +4373,11 @@ function apply(ctx, config = {}) {
|
|
|
4296
4373
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
4297
4374
|
const piProviders = await readPiAiProviders(ctx.settings);
|
|
4298
4375
|
const providers = { ...piProviders };
|
|
4299
|
-
|
|
4376
|
+
const deepseekKey = providers["deepseek"]?.apiKeyEnv;
|
|
4377
|
+
if (deepseekKey === void 0 || deepseekKey === "") providers["deepseek"] = {
|
|
4378
|
+
...providers["deepseek"],
|
|
4379
|
+
apiKeyEnv: config.balanceApiKeyEnv ?? DEFAULT_BALANCE_API_KEY_ENV
|
|
4380
|
+
};
|
|
4300
4381
|
const balances = await queryBalances(ctx, providers);
|
|
4301
4382
|
const custom = await queryCustomBalances(ctx, config.customBalances ?? []);
|
|
4302
4383
|
const declared = await queryDeclaredEndpoints(ctx, piProviders, config.declaredEndpoints ?? []);
|
|
@@ -4348,9 +4429,25 @@ function apply(ctx, config = {}) {
|
|
|
4348
4429
|
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
4349
4430
|
return;
|
|
4350
4431
|
}
|
|
4432
|
+
if (!isLoopbackOrigin(req.headers.origin)) {
|
|
4433
|
+
res.writeHead(403, { "content-type": "application/json; charset=utf-8" });
|
|
4434
|
+
res.end(JSON.stringify({ error: "forbidden: loopback only" }));
|
|
4435
|
+
return;
|
|
4436
|
+
}
|
|
4437
|
+
if (!(req.headers["content-type"] ?? "").toLowerCase().includes("application/json")) {
|
|
4438
|
+
res.writeHead(415, { "content-type": "application/json; charset=utf-8" });
|
|
4439
|
+
res.end(JSON.stringify({ error: "unsupported content-type" }));
|
|
4440
|
+
return;
|
|
4441
|
+
}
|
|
4351
4442
|
try {
|
|
4352
4443
|
let body = "";
|
|
4353
|
-
for await (const chunk of req)
|
|
4444
|
+
for await (const chunk of req) {
|
|
4445
|
+
body += String(chunk);
|
|
4446
|
+
if (body.length > 512) {
|
|
4447
|
+
res.end(JSON.stringify({ error: "body too large" }));
|
|
4448
|
+
return;
|
|
4449
|
+
}
|
|
4450
|
+
}
|
|
4354
4451
|
const parsed = JSON.parse(body === "" ? "{}" : body);
|
|
4355
4452
|
if (usageSettingsScope === void 0) {
|
|
4356
4453
|
res.end(JSON.stringify({ error: "settings unavailable" }));
|
|
@@ -4373,10 +4470,15 @@ function apply(ctx, config = {}) {
|
|
|
4373
4470
|
};
|
|
4374
4471
|
const refreshQuotas = async () => {
|
|
4375
4472
|
const { keys, identified } = await resolveSubscriptionKeys(ctx.settings, ctx.credentials);
|
|
4376
|
-
const
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4473
|
+
const piProviders = await readPiAiProviders(ctx.settings);
|
|
4474
|
+
const rows = [...await collectSubscriptions(keys, identified.filter((item) => item.adapter).map((item) => {
|
|
4475
|
+
const baseURL = piProviders[item.provider]?.baseURL;
|
|
4476
|
+
return {
|
|
4477
|
+
provider: item.provider,
|
|
4478
|
+
...item.region === void 0 ? {} : { region: item.region },
|
|
4479
|
+
...typeof baseURL === "string" && baseURL !== "" ? { baseUrl: baseURL } : {}
|
|
4480
|
+
};
|
|
4481
|
+
}))].map((row) => {
|
|
4380
4482
|
const planType = planTypeOf(row.provider);
|
|
4381
4483
|
const subscriptionAmount = subscriptionFeeCnyOf(row.provider, live.rate);
|
|
4382
4484
|
return {
|
|
@@ -4404,7 +4506,11 @@ function apply(ctx, config = {}) {
|
|
|
4404
4506
|
handler: async (req, res) => {
|
|
4405
4507
|
if (!guardLoopback(req, res)) return;
|
|
4406
4508
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
4407
|
-
if (Date.now() - quotaCache.at >= SUBSCRIPTION_CACHE_MS)
|
|
4509
|
+
if (Date.now() - quotaCache.at >= SUBSCRIPTION_CACHE_MS) try {
|
|
4510
|
+
await refreshQuotas();
|
|
4511
|
+
} catch (error) {
|
|
4512
|
+
console.warn("[usage-billing] refreshQuotas failed; serving cached quotas:", error);
|
|
4513
|
+
}
|
|
4408
4514
|
res.end(JSON.stringify({ quotas: quotaCache.quotas }));
|
|
4409
4515
|
}
|
|
4410
4516
|
}), "usage-billing: subscriptions route");
|
|
@@ -4436,7 +4542,11 @@ function apply(ctx, config = {}) {
|
|
|
4436
4542
|
handler: async (req, res) => {
|
|
4437
4543
|
if (!guardLoopback(req, res)) return;
|
|
4438
4544
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
4439
|
-
if (Date.now() - relayCache.at >= SUBSCRIPTION_CACHE_MS)
|
|
4545
|
+
if (Date.now() - relayCache.at >= SUBSCRIPTION_CACHE_MS) try {
|
|
4546
|
+
await refreshRelay();
|
|
4547
|
+
} catch (error) {
|
|
4548
|
+
console.warn("[usage-billing] refreshRelay failed; serving cached relay quotas:", error);
|
|
4549
|
+
}
|
|
4440
4550
|
const diagnostics = relayCache.quotas.map((row) => ({
|
|
4441
4551
|
route: row.route,
|
|
4442
4552
|
origin: row.origin,
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* 3. Token 结构 KPI(缓存命中率 / reasoning 占比 / 输入:输出比 / 峰值日)。
|
|
7
7
|
*/
|
|
8
8
|
import type { UsageBillingKey } from './locales.ts';
|
|
9
|
-
import { type CostCurrency } from './pricing.ts';
|
|
10
9
|
import type { UsageStats } from './UsageBilling.tsx';
|
|
11
10
|
/**
|
|
12
11
|
* Token 洞察面板。
|
|
@@ -16,7 +15,6 @@ import type { UsageStats } from './UsageBilling.tsx';
|
|
|
16
15
|
*/
|
|
17
16
|
export declare function TokenPanel(props: {
|
|
18
17
|
stats: UsageStats;
|
|
19
|
-
currency: CostCurrency;
|
|
20
18
|
trendDays: 7 | 30;
|
|
21
19
|
onTrendDays: (d: 7 | 30) => void;
|
|
22
20
|
t: (key: UsageBillingKey) => string;
|
|
@@ -223,6 +223,7 @@ export interface UsageStats {
|
|
|
223
223
|
}
|
|
224
224
|
/**
|
|
225
225
|
* 拉取官方余额差对账提示(drift 时非空),供余额面板展示;失败返回 undefined。
|
|
226
|
+
* 复用 {@link fetchBalanceDoc} 的同一响应,导出供对账提示渲染测试单独解析。
|
|
226
227
|
* @returns the reconcile notice, or undefined on any failure / no drift.
|
|
227
228
|
*/
|
|
228
229
|
export declare function fetchReconcile(): Promise<ReconcileNotice | undefined>;
|
|
@@ -225,10 +225,11 @@ export declare function computeCost(entry: ModelEntry, buckets: TokenUsageBucket
|
|
|
225
225
|
* @param buckets - token usage counts.
|
|
226
226
|
* @param timeMs - the call's wall-clock time (epoch ms); null falls back to the peak-share mix.
|
|
227
227
|
* @param peakShare - fallback mix used only when `timeMs` is missing.
|
|
228
|
-
* @returns the estimated cost in
|
|
228
|
+
* @returns the estimated cost in CNY(USD 计价模型已按当前汇率折算)。
|
|
229
229
|
*/
|
|
230
230
|
export declare function computeCostAt(entry: ModelEntry, buckets: TokenUsageBuckets, timeMs: number | null | undefined, peakShare?: number): number;
|
|
231
|
-
/** 人民币 →
|
|
231
|
+
/** 人民币 → 美元(显示换算用):用当前生效汇率(实时优先,缺失回退内置),
|
|
232
|
+
* 与计价链路的 `currentRate()` 同口径,避免实时汇率生效时 USD 显示与计价不一致。 */
|
|
232
233
|
export declare function cnyToUsd(cny: number): number;
|
|
233
234
|
/**
|
|
234
235
|
* Format an amount with adaptive precision and the given currency symbol.
|
|
@@ -33,7 +33,8 @@ export interface FloatWindowPrefs {
|
|
|
33
33
|
export declare const DEFAULT_FLOAT_WINDOW_PREFS: FloatWindowPrefs;
|
|
34
34
|
/** localStorage key(与 budget store 的 `dsh.ui-usage-billing.*` 命名空间一致)。 */
|
|
35
35
|
export declare const FLOAT_WINDOW_STORAGE_KEY = "dsh.ui-usage-billing.float";
|
|
36
|
-
/** 读取浮窗偏好(含损坏/缺失回退到默认)。仅在浏览器半区调用。
|
|
36
|
+
/** 读取浮窗偏好(含损坏/缺失回退到默认)。仅在浏览器半区调用。
|
|
37
|
+
* 返回全新对象(含 targets 数组拷贝),避免调用方就地修改污染共享默认值。 */
|
|
37
38
|
export declare function loadFloatWindowPrefs(): FloatWindowPrefs;
|
|
38
39
|
/** 写入浮窗偏好。失败静默(展示偏好非关键)。 */
|
|
39
40
|
export declare function saveFloatWindowPrefs(prefs: FloatWindowPrefs): void;
|
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": "0.9.
|
|
4
|
+
"version": "0.9.20",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -47,42 +47,42 @@
|
|
|
47
47
|
},
|
|
48
48
|
"license": "MIT",
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@deepseek-ai/dsh-api-remotes": "
|
|
51
|
-
"@deepseek-ai/dsh-atomic-write": "
|
|
52
|
-
"@deepseek-ai/dsh-client-connection": "
|
|
53
|
-
"@deepseek-ai/dsh-host-webserver": "
|
|
54
|
-
"@deepseek-ai/dsh-client-locale": "
|
|
55
|
-
"@deepseek-ai/dsh-client-runtime": "
|
|
56
|
-
"@deepseek-ai/dsh-client-ui-conversation": "
|
|
57
|
-
"@deepseek-ai/dsh-client-ui-sidebar": "
|
|
58
|
-
"@deepseek-ai/dsh-client-ui-primitives": "
|
|
59
|
-
"@deepseek-ai/dsh-client-ui-slots": "
|
|
60
|
-
"@deepseek-ai/dsh-credentials": "
|
|
61
|
-
"@deepseek-ai/dsh-invariants": "
|
|
62
|
-
"@deepseek-ai/dsh-llm": "
|
|
63
|
-
"@deepseek-ai/dsh-session": "
|
|
64
|
-
"@deepseek-ai/dsh-session-persistence": "
|
|
65
|
-
"@deepseek-ai/dsh-settings": "
|
|
66
|
-
"@deepseek-ai/dsh-tools": "
|
|
67
|
-
"@deepseek-ai/schemastery": "
|
|
68
|
-
"@deepseek-ai/cordis": "
|
|
50
|
+
"@deepseek-ai/dsh-api-remotes": "*",
|
|
51
|
+
"@deepseek-ai/dsh-atomic-write": "*",
|
|
52
|
+
"@deepseek-ai/dsh-client-connection": "*",
|
|
53
|
+
"@deepseek-ai/dsh-host-webserver": "*",
|
|
54
|
+
"@deepseek-ai/dsh-client-locale": "*",
|
|
55
|
+
"@deepseek-ai/dsh-client-runtime": "*",
|
|
56
|
+
"@deepseek-ai/dsh-client-ui-conversation": "*",
|
|
57
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "*",
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-primitives": "*",
|
|
59
|
+
"@deepseek-ai/dsh-client-ui-slots": "*",
|
|
60
|
+
"@deepseek-ai/dsh-credentials": "*",
|
|
61
|
+
"@deepseek-ai/dsh-invariants": "*",
|
|
62
|
+
"@deepseek-ai/dsh-llm": "*",
|
|
63
|
+
"@deepseek-ai/dsh-session": "*",
|
|
64
|
+
"@deepseek-ai/dsh-session-persistence": "*",
|
|
65
|
+
"@deepseek-ai/dsh-settings": "*",
|
|
66
|
+
"@deepseek-ai/dsh-tools": "*",
|
|
67
|
+
"@deepseek-ai/schemastery": "*",
|
|
68
|
+
"@deepseek-ai/cordis": "*",
|
|
69
69
|
"react": "^18.2.0"
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
72
|
"clsx": "^2.1.1"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
|
-
"@deepseek-ai/dsh-client-locale": "
|
|
76
|
-
"@deepseek-ai/dsh-client-runtime": "
|
|
77
|
-
"@deepseek-ai/dsh-client-test-runtime": "
|
|
78
|
-
"@deepseek-ai/dsh-client-ui-conversation": "
|
|
79
|
-
"@deepseek-ai/dsh-client-ui-primitives": "
|
|
80
|
-
"@deepseek-ai/dsh-client-ui-slots": "
|
|
81
|
-
"@deepseek-ai/dsh-invariants": "
|
|
75
|
+
"@deepseek-ai/dsh-client-locale": "*",
|
|
76
|
+
"@deepseek-ai/dsh-client-runtime": "*",
|
|
77
|
+
"@deepseek-ai/dsh-client-test-runtime": "*",
|
|
78
|
+
"@deepseek-ai/dsh-client-ui-conversation": "*",
|
|
79
|
+
"@deepseek-ai/dsh-client-ui-primitives": "*",
|
|
80
|
+
"@deepseek-ai/dsh-client-ui-slots": "*",
|
|
81
|
+
"@deepseek-ai/dsh-invariants": "*",
|
|
82
82
|
"@testing-library/react": "^16.1.0",
|
|
83
83
|
"@types/react": "~18.3.1",
|
|
84
84
|
"@types/node": "^22.20.0",
|
|
85
|
-
"@deepseek-ai/cordis": "
|
|
85
|
+
"@deepseek-ai/cordis": "*",
|
|
86
86
|
"react": "^18.2.0",
|
|
87
87
|
"react-dom": "^18.2.0",
|
|
88
88
|
"lightningcss": "^1.32.0",
|