@kenz1117/dsh-ui-usage-billing 0.9.19 → 1.0.0
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 +3 -1
- package/README.md +3 -1
- package/lib/client.js +1565 -1176
- package/lib/index.js +190 -50
- package/lib/types/client/PeakAlertBanner.d.ts +2 -2
- package/lib/types/client/PluginInfoCard.d.ts +4 -3
- package/lib/types/client/TokenPanel.d.ts +0 -2
- package/lib/types/client/UsageBilling.d.ts +1 -0
- package/lib/types/client/locales.d.ts +1 -1
- 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/types/plans.d.ts +0 -39
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
|
}
|
|
@@ -492,6 +492,30 @@ const MODEL_CATALOG = [
|
|
|
492
492
|
output: 8.4
|
|
493
493
|
}
|
|
494
494
|
},
|
|
495
|
+
{
|
|
496
|
+
key: "minimax-m2.7",
|
|
497
|
+
name: "MiniMax-M2.7",
|
|
498
|
+
provider: "MiniMax",
|
|
499
|
+
colorVar: "dsw-static-amber-400",
|
|
500
|
+
price: {
|
|
501
|
+
currency: "CNY",
|
|
502
|
+
input: 2.1,
|
|
503
|
+
cacheHit: .42,
|
|
504
|
+
output: 8.4
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
key: "minimax-m2.7-highspeed",
|
|
509
|
+
name: "MiniMax-M2.7-highspeed",
|
|
510
|
+
provider: "MiniMax",
|
|
511
|
+
colorVar: "dsw-static-amber-500",
|
|
512
|
+
price: {
|
|
513
|
+
currency: "CNY",
|
|
514
|
+
input: 4.2,
|
|
515
|
+
cacheHit: .42,
|
|
516
|
+
output: 16.8
|
|
517
|
+
}
|
|
518
|
+
},
|
|
495
519
|
{
|
|
496
520
|
key: "ernie",
|
|
497
521
|
name: "ERNIE-5.1",
|
|
@@ -742,7 +766,16 @@ const MODEL_KEY_ALIASES = {
|
|
|
742
766
|
"step-3.7-flash": "step",
|
|
743
767
|
"seed-2.0-mini": "doubao-mini",
|
|
744
768
|
"k3": "kimi-k3",
|
|
745
|
-
"kimi-k3": "kimi-k3"
|
|
769
|
+
"kimi-k3": "kimi-k3",
|
|
770
|
+
"minimax-m1": "minimax",
|
|
771
|
+
"minimax-m2": "minimax",
|
|
772
|
+
"minimax-m3": "minimax",
|
|
773
|
+
"minimax-m2.7": "minimax-m2.7",
|
|
774
|
+
"minimax-m2.7-highspeed": "minimax-m2.7-highspeed",
|
|
775
|
+
"minimax-m2.7-high-speed": "minimax-m2.7-highspeed",
|
|
776
|
+
"minimax-m2-7": "minimax-m2.7",
|
|
777
|
+
"minimax-m2-7-highspeed": "minimax-m2.7-highspeed",
|
|
778
|
+
"minimax-m2-7-high-speed": "minimax-m2.7-highspeed"
|
|
746
779
|
};
|
|
747
780
|
/**
|
|
748
781
|
* 模型 id 归一化:小写、去括号附注(如 `gpt5.6 luna(go)` 只看主体)、再去所有
|
|
@@ -892,7 +925,7 @@ function computeCost(entry, buckets, peakShare = DEFAULT_PEAK_SHARE) {
|
|
|
892
925
|
* @param buckets - token usage counts.
|
|
893
926
|
* @param timeMs - the call's wall-clock time (epoch ms); null falls back to the peak-share mix.
|
|
894
927
|
* @param peakShare - fallback mix used only when `timeMs` is missing.
|
|
895
|
-
* @returns the estimated cost in
|
|
928
|
+
* @returns the estimated cost in CNY(USD 计价模型已按当前汇率折算)。
|
|
896
929
|
*/
|
|
897
930
|
function computeCostAt(entry, buckets, timeMs, peakShare = DEFAULT_PEAK_SHARE) {
|
|
898
931
|
if (entry.price.offPeak === void 0) return priceBandCost(entry.price, buckets, entry.price.currency);
|
|
@@ -1457,11 +1490,12 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
1457
1490
|
console.warn("[usage-billing] failed to load durable usage ledger; rebuilding from current sessions:", error);
|
|
1458
1491
|
}
|
|
1459
1492
|
};
|
|
1460
|
-
/** 失效键:日志文件的 mtime+size;拿不到(后端无 locate /
|
|
1493
|
+
/** 失效键:日志文件的 mtime+size;拿不到(后端无 locate / 文件丢失 / locate 抛错)时返回 null,
|
|
1494
|
+
* 让调用方每次重折。locate 调用也纳入 try,避免单个会话的 locate 异常把整份聚合拖垮。 */
|
|
1461
1495
|
const stampOf = async (meta) => {
|
|
1462
|
-
const location = persistence.locate?.(meta);
|
|
1463
|
-
if (location === void 0) return null;
|
|
1464
1496
|
try {
|
|
1497
|
+
const location = persistence.locate?.(meta);
|
|
1498
|
+
if (location === void 0) return null;
|
|
1465
1499
|
const info = await stat(location.path);
|
|
1466
1500
|
return `${String(info.mtimeMs)}:${String(info.size)}`;
|
|
1467
1501
|
} catch {
|
|
@@ -2081,9 +2115,20 @@ const QUERIERS = [
|
|
|
2081
2115
|
* @returns the balance rows (one per provider).
|
|
2082
2116
|
*/
|
|
2083
2117
|
async function queryBalances(ctx, providers) {
|
|
2084
|
-
|
|
2118
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2119
|
+
for (const { route, querier, displayName } of QUERIERS) {
|
|
2085
2120
|
const env = providers[route]?.apiKeyEnv;
|
|
2086
|
-
|
|
2121
|
+
const configured = typeof env === "string" && env !== "";
|
|
2122
|
+
const existing = byName.get(displayName);
|
|
2123
|
+
if (existing !== void 0 && existing.env !== void 0) continue;
|
|
2124
|
+
byName.set(displayName, {
|
|
2125
|
+
displayName,
|
|
2126
|
+
querier,
|
|
2127
|
+
env: configured ? env : void 0
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2130
|
+
return await Promise.all([...byName.values()].map(({ querier, displayName, env }) => {
|
|
2131
|
+
if (env === void 0) return Promise.resolve({
|
|
2087
2132
|
provider: displayName,
|
|
2088
2133
|
displayName,
|
|
2089
2134
|
error: "unconfigured"
|
|
@@ -3213,15 +3258,16 @@ async function collectZai(keys, config, timeoutMs) {
|
|
|
3213
3258
|
function goWindow(value, kind) {
|
|
3214
3259
|
if (value === null || typeof value !== "object") return null;
|
|
3215
3260
|
const record = value;
|
|
3216
|
-
const
|
|
3217
|
-
|
|
3261
|
+
const ratioSource = record.usagePercent ?? record.usedPercent ?? record.percentUsed ?? record.percentage;
|
|
3262
|
+
const fromRatioField = ratioSource !== void 0 && ratioSource !== null;
|
|
3263
|
+
let usedPercent = clampPercent$1(numberOrNull$1(fromRatioField ? ratioSource : record.percent));
|
|
3218
3264
|
if (usedPercent === null) {
|
|
3219
3265
|
const used = numberOrNull$1(record.used ?? record.consumed);
|
|
3220
3266
|
const limit = numberOrNull$1(record.limit ?? record.total ?? record.quota);
|
|
3221
3267
|
if (used !== null && limit !== null && limit > 0) usedPercent = clampPercent$1(used / limit * 100);
|
|
3222
3268
|
}
|
|
3223
3269
|
if (usedPercent === null) return null;
|
|
3224
|
-
if (usedPercent <= 1 && usedPercent >= 0
|
|
3270
|
+
if (fromRatioField && usedPercent <= 1 && usedPercent >= 0) usedPercent *= 100;
|
|
3225
3271
|
const resetSeconds = numberOrNull$1(record.resetInSec ?? record.resetInSeconds ?? record.resetSeconds);
|
|
3226
3272
|
const resetsAt = resetSeconds === null ? toIso(record.resetAt ?? record.resetsAt ?? record.nextReset) : new Date(Date.now() + Math.max(0, resetSeconds) * 1e3).toISOString();
|
|
3227
3273
|
return {
|
|
@@ -3329,6 +3375,18 @@ function resolveMiniMaxBaseUrl(config) {
|
|
|
3329
3375
|
function minmaxDisplayName(provider) {
|
|
3330
3376
|
return SUBSCRIPTION_DISPLAY_NAMES[provider] ?? "MiniMax Coding Plan";
|
|
3331
3377
|
}
|
|
3378
|
+
/** MiniMax Token Plan / Coding Plan 的 key 判别:普通按量 API key 以 `sk-` 开头,
|
|
3379
|
+
* Token Plan(订阅套餐)key 以 `sk-cp-` 开头。据此区分一个 key 是订阅还是按量,
|
|
3380
|
+
* 供查询失败时给出「key 类型 / region」定向提示,避免把按量 key 误当 token-plan
|
|
3381
|
+
* 查询后只剩一句笼统的「响应异常」。 */
|
|
3382
|
+
function isMiniMaxTokenPlanKey(apiKey) {
|
|
3383
|
+
return apiKey.startsWith("sk-cp-");
|
|
3384
|
+
}
|
|
3385
|
+
/** 构造 MiniMax Token Plan 查询端点:避免 base 已含 `/v1` 时拼出 `/v1/v1/...`。 */
|
|
3386
|
+
function miniMaxEndpoint(base) {
|
|
3387
|
+
const trimmed = base.replace(/\/+$/, "");
|
|
3388
|
+
return trimmed.endsWith("/v1") ? `${trimmed}/token_plan/remains` : `${trimmed}/v1/token_plan/remains`;
|
|
3389
|
+
}
|
|
3332
3390
|
/** Collect the MiniMax Token Plan quota (CN + INTL). */
|
|
3333
3391
|
async function collectMiniMax(keys, config, timeoutMs) {
|
|
3334
3392
|
const apiKey = keys.minmaxApiKey.trim();
|
|
@@ -3340,16 +3398,25 @@ async function collectMiniMax(keys, config, timeoutMs) {
|
|
|
3340
3398
|
status: "not-configured",
|
|
3341
3399
|
windows: []
|
|
3342
3400
|
};
|
|
3401
|
+
const tokenPlanKey = isMiniMaxTokenPlanKey(apiKey);
|
|
3343
3402
|
try {
|
|
3344
|
-
const windows = parseMiniMaxRemains(await requestJson(
|
|
3403
|
+
const windows = parseMiniMaxRemains(await requestJson(miniMaxEndpoint(base), { headers: {
|
|
3345
3404
|
authorization: `Bearer ${apiKey}`,
|
|
3346
3405
|
accept: "application/json"
|
|
3347
3406
|
} }, timeoutMs));
|
|
3407
|
+
if (windows.length > 0) return {
|
|
3408
|
+
provider: config.provider,
|
|
3409
|
+
displayName,
|
|
3410
|
+
status: "ok",
|
|
3411
|
+
windows,
|
|
3412
|
+
...tokenPlanKey ? {} : { hint: "此 key 未以 sk-cp- 开头(可能为按量 API);但 Token Plan 接口返回了额度,故按 Token Plan 展示" }
|
|
3413
|
+
};
|
|
3348
3414
|
return {
|
|
3349
3415
|
provider: config.provider,
|
|
3350
3416
|
displayName,
|
|
3351
|
-
status:
|
|
3352
|
-
windows
|
|
3417
|
+
status: "invalid-response",
|
|
3418
|
+
windows,
|
|
3419
|
+
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
3420
|
};
|
|
3354
3421
|
} catch (error) {
|
|
3355
3422
|
return {
|
|
@@ -3628,6 +3695,19 @@ function endpointOf(baseURL, path) {
|
|
|
3628
3695
|
return new URL(path, baseURL).toString();
|
|
3629
3696
|
}
|
|
3630
3697
|
/**
|
|
3698
|
+
* 用已知程序类型查询该路由的额度(用当前 key)。额度按 key 独立,因此即便
|
|
3699
|
+
* 指纹缓存已识别出站点类型,也仍要实际发一次请求读出本 key 的余额/窗口。
|
|
3700
|
+
* @param baseURL - 站点端点。
|
|
3701
|
+
* @param apiKey - 已解析的本路由 key。
|
|
3702
|
+
* @param kind - 已识别出的程序类型。
|
|
3703
|
+
* @returns 解析出的余额/窗口;取不到时返回 null(漂移或端点不可用)。
|
|
3704
|
+
*/
|
|
3705
|
+
async function readRelayByKind(baseURL, apiKey, kind) {
|
|
3706
|
+
const res = await fetchRelayJson(endpointOf(baseURL, kind === "sub2api" ? "/v1/usage" : "/api/status"), apiKey);
|
|
3707
|
+
if (!res.ok) return null;
|
|
3708
|
+
return kind === "sub2api" ? parseSub2ApiUsage(res.data) : parseNewApiStatus(res.data);
|
|
3709
|
+
}
|
|
3710
|
+
/**
|
|
3631
3711
|
* 查询单个中转站路由的额度。先试 Sub2API,再试 New API;任一读出额度即返回。
|
|
3632
3712
|
* @param ctx - host context carrying the credentials seam.
|
|
3633
3713
|
* @param route - 待探测的路由(baseURL + apiKeyEnv)。
|
|
@@ -3645,19 +3725,43 @@ async function queryRelayQuota(ctx, route) {
|
|
|
3645
3725
|
kind: "unknown",
|
|
3646
3726
|
status: "unavailable"
|
|
3647
3727
|
};
|
|
3648
|
-
|
|
3728
|
+
let hit;
|
|
3729
|
+
try {
|
|
3730
|
+
hit = await ctx.credentials.resolve(credentialRef(route.apiKeyEnv));
|
|
3731
|
+
} catch {
|
|
3732
|
+
return {
|
|
3733
|
+
...base,
|
|
3734
|
+
kind: "unknown",
|
|
3735
|
+
status: "not-configured"
|
|
3736
|
+
};
|
|
3737
|
+
}
|
|
3649
3738
|
if (hit === void 0 || hit.value === "") return {
|
|
3650
3739
|
...base,
|
|
3651
3740
|
kind: "unknown",
|
|
3652
3741
|
status: "not-configured"
|
|
3653
3742
|
};
|
|
3654
3743
|
const cached = fingerprintCache.get(base.origin);
|
|
3655
|
-
|
|
3656
|
-
...base,
|
|
3657
|
-
kind: cached.kind,
|
|
3658
|
-
status: "ok"
|
|
3659
|
-
};
|
|
3744
|
+
const knownKind = cached !== void 0 && Date.now() - cached.at < FINGERPRINT_TTL_MS ? cached.kind : void 0;
|
|
3660
3745
|
try {
|
|
3746
|
+
if (knownKind === "sub2api" || knownKind === "new-api") {
|
|
3747
|
+
const parsed = await readRelayByKind(route.baseURL, hit.value, knownKind);
|
|
3748
|
+
if (parsed !== null) {
|
|
3749
|
+
relayGate.success(gateKey);
|
|
3750
|
+
return {
|
|
3751
|
+
...base,
|
|
3752
|
+
kind: knownKind,
|
|
3753
|
+
status: "ok",
|
|
3754
|
+
...parsed.balance !== void 0 ? { balance: parsed.balance } : {},
|
|
3755
|
+
...parsed.windows !== void 0 ? { windows: parsed.windows } : {}
|
|
3756
|
+
};
|
|
3757
|
+
}
|
|
3758
|
+
relayGate.fail(gateKey);
|
|
3759
|
+
return {
|
|
3760
|
+
...base,
|
|
3761
|
+
kind: knownKind,
|
|
3762
|
+
status: "invalid-response"
|
|
3763
|
+
};
|
|
3764
|
+
}
|
|
3661
3765
|
const sub2 = await fetchRelayJson(endpointOf(route.baseURL, "/v1/usage"), hit.value);
|
|
3662
3766
|
if (sub2.ok) {
|
|
3663
3767
|
const parsed = parseSub2ApiUsage(sub2.data);
|
|
@@ -3682,23 +3786,7 @@ async function queryRelayQuota(ctx, route) {
|
|
|
3682
3786
|
status: "invalid-response"
|
|
3683
3787
|
};
|
|
3684
3788
|
}
|
|
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
3789
|
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
3790
|
if (na.ok) {
|
|
3703
3791
|
const parsed = parseNewApiStatus(na.data);
|
|
3704
3792
|
if (parsed !== null) {
|
|
@@ -3728,11 +3816,12 @@ async function queryRelayQuota(ctx, route) {
|
|
|
3728
3816
|
status: "unavailable"
|
|
3729
3817
|
};
|
|
3730
3818
|
} catch (error) {
|
|
3731
|
-
|
|
3819
|
+
const status = statusOf(error);
|
|
3820
|
+
if (status !== "unauthorized") relayGate.fail(gateKey);
|
|
3732
3821
|
return {
|
|
3733
3822
|
...base,
|
|
3734
3823
|
kind: "unknown",
|
|
3735
|
-
status
|
|
3824
|
+
status
|
|
3736
3825
|
};
|
|
3737
3826
|
}
|
|
3738
3827
|
}
|
|
@@ -3779,12 +3868,26 @@ function isLoopbackPeer(req) {
|
|
|
3779
3868
|
if (address === void 0) return false;
|
|
3780
3869
|
return address === "::1" || address.startsWith("127.") || address.startsWith("::ffff:127.");
|
|
3781
3870
|
}
|
|
3782
|
-
/** 校验 Host
|
|
3871
|
+
/** 校验 Host 头是本机回环(精确 127.0.0.0/8 / ::1 / localhost 或空,供 curl 不带 Host 的极简请求)。
|
|
3872
|
+
* 拒绝 `127.0.0.1.attacker.com` 这类以 `127.` 开头但解析到外部的 DNS rebinding 域名:
|
|
3873
|
+
* 只用 `startsWith('127.')` 会被它穿透,必须精确匹配回环 IP 的字面量。 */
|
|
3783
3874
|
function isLoopbackHost(req) {
|
|
3784
3875
|
const host = req.headers.host;
|
|
3785
3876
|
if (host === void 0 || host === "") return true;
|
|
3786
3877
|
const name = host.split(":")[0];
|
|
3787
|
-
return name === "localhost" || name === "::1" || name !== void 0 && name
|
|
3878
|
+
return name === "localhost" || name === "::1" || name !== void 0 && /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(name);
|
|
3879
|
+
}
|
|
3880
|
+
/** 校验 Origin 头是否回环(写操作用,防止跨站表单/fetch 改写设置)。Origin 缺失
|
|
3881
|
+
* (curl 或同源 fetch 不带)视为放行,交给下方的 Content-Type 校验兜底;Origin 存在
|
|
3882
|
+
* 但主机非回环则拒绝——跨站脚本发起的写请求必带攻击者域名的 Origin。 */
|
|
3883
|
+
function isLoopbackOrigin(origin) {
|
|
3884
|
+
if (origin === void 0 || origin === "") return true;
|
|
3885
|
+
try {
|
|
3886
|
+
const host = new URL(origin).hostname;
|
|
3887
|
+
return host === "localhost" || host === "::1" || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
|
|
3888
|
+
} catch {
|
|
3889
|
+
return false;
|
|
3890
|
+
}
|
|
3788
3891
|
}
|
|
3789
3892
|
/**
|
|
3790
3893
|
* 回环防护守卫:仅接受回环 GET 请求(peer socket 地址 + Host 头同时校验)。
|
|
@@ -3849,6 +3952,10 @@ function createFileUsageLedgerStore(ledgerPath) {
|
|
|
3849
3952
|
} catch {}
|
|
3850
3953
|
},
|
|
3851
3954
|
async save(document) {
|
|
3955
|
+
await mkdir(dirname(ledgerPath), {
|
|
3956
|
+
recursive: true,
|
|
3957
|
+
mode: 448
|
|
3958
|
+
});
|
|
3852
3959
|
await withFileLock(ledgerPath, async () => {
|
|
3853
3960
|
try {
|
|
3854
3961
|
const existing = await readFile(ledgerPath, "utf8");
|
|
@@ -4296,7 +4403,11 @@ function apply(ctx, config = {}) {
|
|
|
4296
4403
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
4297
4404
|
const piProviders = await readPiAiProviders(ctx.settings);
|
|
4298
4405
|
const providers = { ...piProviders };
|
|
4299
|
-
|
|
4406
|
+
const deepseekKey = providers["deepseek"]?.apiKeyEnv;
|
|
4407
|
+
if (deepseekKey === void 0 || deepseekKey === "") providers["deepseek"] = {
|
|
4408
|
+
...providers["deepseek"],
|
|
4409
|
+
apiKeyEnv: config.balanceApiKeyEnv ?? DEFAULT_BALANCE_API_KEY_ENV
|
|
4410
|
+
};
|
|
4300
4411
|
const balances = await queryBalances(ctx, providers);
|
|
4301
4412
|
const custom = await queryCustomBalances(ctx, config.customBalances ?? []);
|
|
4302
4413
|
const declared = await queryDeclaredEndpoints(ctx, piProviders, config.declaredEndpoints ?? []);
|
|
@@ -4348,9 +4459,25 @@ function apply(ctx, config = {}) {
|
|
|
4348
4459
|
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
4349
4460
|
return;
|
|
4350
4461
|
}
|
|
4462
|
+
if (!isLoopbackOrigin(req.headers.origin)) {
|
|
4463
|
+
res.writeHead(403, { "content-type": "application/json; charset=utf-8" });
|
|
4464
|
+
res.end(JSON.stringify({ error: "forbidden: loopback only" }));
|
|
4465
|
+
return;
|
|
4466
|
+
}
|
|
4467
|
+
if (!(req.headers["content-type"] ?? "").toLowerCase().includes("application/json")) {
|
|
4468
|
+
res.writeHead(415, { "content-type": "application/json; charset=utf-8" });
|
|
4469
|
+
res.end(JSON.stringify({ error: "unsupported content-type" }));
|
|
4470
|
+
return;
|
|
4471
|
+
}
|
|
4351
4472
|
try {
|
|
4352
4473
|
let body = "";
|
|
4353
|
-
for await (const chunk of req)
|
|
4474
|
+
for await (const chunk of req) {
|
|
4475
|
+
body += String(chunk);
|
|
4476
|
+
if (body.length > 512) {
|
|
4477
|
+
res.end(JSON.stringify({ error: "body too large" }));
|
|
4478
|
+
return;
|
|
4479
|
+
}
|
|
4480
|
+
}
|
|
4354
4481
|
const parsed = JSON.parse(body === "" ? "{}" : body);
|
|
4355
4482
|
if (usageSettingsScope === void 0) {
|
|
4356
4483
|
res.end(JSON.stringify({ error: "settings unavailable" }));
|
|
@@ -4373,10 +4500,15 @@ function apply(ctx, config = {}) {
|
|
|
4373
4500
|
};
|
|
4374
4501
|
const refreshQuotas = async () => {
|
|
4375
4502
|
const { keys, identified } = await resolveSubscriptionKeys(ctx.settings, ctx.credentials);
|
|
4376
|
-
const
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4503
|
+
const piProviders = await readPiAiProviders(ctx.settings);
|
|
4504
|
+
const rows = [...await collectSubscriptions(keys, identified.filter((item) => item.adapter).map((item) => {
|
|
4505
|
+
const baseURL = piProviders[item.provider]?.baseURL;
|
|
4506
|
+
return {
|
|
4507
|
+
provider: item.provider,
|
|
4508
|
+
...item.region === void 0 ? {} : { region: item.region },
|
|
4509
|
+
...typeof baseURL === "string" && baseURL !== "" ? { baseUrl: baseURL } : {}
|
|
4510
|
+
};
|
|
4511
|
+
}))].map((row) => {
|
|
4380
4512
|
const planType = planTypeOf(row.provider);
|
|
4381
4513
|
const subscriptionAmount = subscriptionFeeCnyOf(row.provider, live.rate);
|
|
4382
4514
|
return {
|
|
@@ -4404,7 +4536,11 @@ function apply(ctx, config = {}) {
|
|
|
4404
4536
|
handler: async (req, res) => {
|
|
4405
4537
|
if (!guardLoopback(req, res)) return;
|
|
4406
4538
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
4407
|
-
if (Date.now() - quotaCache.at >= SUBSCRIPTION_CACHE_MS)
|
|
4539
|
+
if (Date.now() - quotaCache.at >= SUBSCRIPTION_CACHE_MS) try {
|
|
4540
|
+
await refreshQuotas();
|
|
4541
|
+
} catch (error) {
|
|
4542
|
+
console.warn("[usage-billing] refreshQuotas failed; serving cached quotas:", error);
|
|
4543
|
+
}
|
|
4408
4544
|
res.end(JSON.stringify({ quotas: quotaCache.quotas }));
|
|
4409
4545
|
}
|
|
4410
4546
|
}), "usage-billing: subscriptions route");
|
|
@@ -4436,7 +4572,11 @@ function apply(ctx, config = {}) {
|
|
|
4436
4572
|
handler: async (req, res) => {
|
|
4437
4573
|
if (!guardLoopback(req, res)) return;
|
|
4438
4574
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
4439
|
-
if (Date.now() - relayCache.at >= SUBSCRIPTION_CACHE_MS)
|
|
4575
|
+
if (Date.now() - relayCache.at >= SUBSCRIPTION_CACHE_MS) try {
|
|
4576
|
+
await refreshRelay();
|
|
4577
|
+
} catch (error) {
|
|
4578
|
+
console.warn("[usage-billing] refreshRelay failed; serving cached relay quotas:", error);
|
|
4579
|
+
}
|
|
4440
4580
|
const diagnostics = relayCache.quotas.map((row) => ({
|
|
4441
4581
|
route: row.route,
|
|
4442
4582
|
origin: row.origin,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 峰谷切换提醒浮层:切档前状态条(右下角或居中)。用 `position: fixed` 即可在
|
|
3
|
-
* 任意宿主容器内覆盖整个视口,因此不需要 portal
|
|
4
|
-
*
|
|
3
|
+
* 任意宿主容器内覆盖整个视口,因此不需要 portal。布局按设计 peak-card:头部
|
|
4
|
+
* (档位 tag + 关闭)→ 大号等宽倒计时 → 一句说明。渲染是受控的:父组件把命中
|
|
5
5
|
* (hit)与偏好传入,显示剩余分钟并在切换后消失。
|
|
6
6
|
*/
|
|
7
7
|
import type { PeakAlertConfig, PeakAlertHit } from './peak-alert.ts';
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PluginInfoCard: 「设置 →
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* PluginInfoCard: 「设置 → 插件信息」卡——图标 + 插件名/版本 tag + 描述,下接
|
|
3
|
+
* 元信息网格(作者 / 仓库 / npm / 许可证)。版本号来自服务端 usage-stats 的
|
|
4
|
+
* `pluginVersion`(读自包 package.json),其余元信息静态来自 `plugin-info.ts`。
|
|
5
|
+
* 无版本号时显示 em dash。
|
|
5
6
|
*/
|
|
6
7
|
import type { UsageBillingKey } from './locales.ts';
|
|
7
8
|
/** 信息卡 props:locale 函数 + 版本号(服务端下发,可为空)。 */
|
|
@@ -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>;
|
|
@@ -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.sessions' | '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.lastUpdated' | 'billing.noData' | 'billing.todayRate' | 'billing.rateLive' | 'billing.rateBuiltin' | 'billing.pricingTip' | '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.floatWindowHint' | 'billing.floatNoTargets' | 'billing.floatNoTargetsHint' | 'billing.floatPrev' | 'billing.floatNext' | 'billing.subscriptionsStale' | 'billing.heatmapLess' | 'billing.heatmapMore' | 'billing.currency' | 'billing.currencyCny' | 'billing.currencyUsd' | 'billing.heatmap' | 'billing.rounds' | 'billing.anomaly' | 'billing.workspaces' | 'billing.plan' | 'billing.remaining' | 'billing.unknownModel' | 'billing.model' | '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.peakShareHint' | '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.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.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.pricingTip' | 'billing.pricingUnit' | 'billing.pricingNotes' | 'billing.ubPeak' | 'billing.ubOff' | '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.floatPrev' | 'billing.floatNext' | 'billing.subscriptionsStale' | '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.peakShareHint' | '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.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>;
|
|
@@ -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.
|
|
4
|
+
"version": "1.0.0",
|
|
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",
|
package/lib/types/plans.d.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 供应商计费计划知识库(对标 dsh-spend 的 PROVIDER_KNOWLEDGE / PROVIDER_ALIASES)。
|
|
3
|
-
*
|
|
4
|
-
* 每个 provider 归入两种计费口径之一:
|
|
5
|
-
* - `code` —— 订阅制(coding / token / agent plan):费用 = 订阅月费,额度按周期
|
|
6
|
-
* 计量,token 用量不计费(不按 token 扣钱)。
|
|
7
|
-
* - `token` —— 按量制:费用 = token × 单价(由 models.dev / OpenRouter / 内置目录
|
|
8
|
-
* 定价)。无知识库命中的 provider 默认按 token 口径。
|
|
9
|
-
*
|
|
10
|
-
* `aliasOf` 把部署里杂乱的 provider id(glm / kimi / dashscope…)归一化到知识库键,
|
|
11
|
-
* `planOf` 据此返回计费计划;日志出现的 provider 经此自动识别,无需人工配置。
|
|
12
|
-
*/
|
|
13
|
-
/** 计费口径:code = 订阅制(计月费),token = 按量制(计 token 单价)。 */
|
|
14
|
-
export type BillingPlanKind = 'code' | 'token';
|
|
15
|
-
/** 订阅月费的币种(只支持单一币种计费,与仪表盘币种一致)。 */
|
|
16
|
-
export type BillingCurrency = 'CNY' | 'USD';
|
|
17
|
-
/** 一个 provider 的计费计划。 */
|
|
18
|
-
export interface BillingPlan {
|
|
19
|
-
/** 计费口径。 */
|
|
20
|
-
kind: BillingPlanKind;
|
|
21
|
-
/** 厂商显示名(未命中知识库时用 provider id 原样)。 */
|
|
22
|
-
label: string;
|
|
23
|
-
/** code 计划的订阅月费(单位 {@link currency});token 计划无此项。 */
|
|
24
|
-
subscriptionMonthly?: number;
|
|
25
|
-
/** 订阅月费币种。 */
|
|
26
|
-
currency?: BillingCurrency;
|
|
27
|
-
/** 订阅额度周期(天);额度按此周期计量。 */
|
|
28
|
-
periodDays?: number;
|
|
29
|
-
}
|
|
30
|
-
/** 归一化 provider id 到知识库键;未命中的原样返回(避免误吞)。 */
|
|
31
|
-
export declare function aliasOf(provider: string): string;
|
|
32
|
-
/**
|
|
33
|
-
* 某 provider 的计费计划:命中的 code 计划返回订阅口径,`PROVIDER_LABELS` 命中的
|
|
34
|
-
* 按量厂商返回 token 口径(无月费);完全未知返回 undefined(调用方按 token 兜底)。
|
|
35
|
-
*/
|
|
36
|
-
export declare function planOf(provider: string): BillingPlan | undefined;
|
|
37
|
-
/** 是否已知为订阅制(code)provider。 */
|
|
38
|
-
export declare function isCodePlan(provider: string): boolean;
|
|
39
|
-
//# sourceMappingURL=plans.d.ts.map
|