@kenz1117/dsh-ui-usage-billing 0.9.9 → 0.9.10
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 +8 -4
- package/README.md +9 -5
- package/lib/client.js +415 -330
- package/lib/index.js +107 -18
- package/lib/types/aggregate.d.ts +4 -0
- package/lib/types/client/UsageBilling.d.ts +4 -0
- package/lib/types/client/export.d.ts +2 -0
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/index.d.ts +9 -0
- package/lib/types/pricing-shared.d.ts +11 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1129,6 +1129,7 @@ function foldSession(events, subscriptionProviders, officialProviderIds, routes
|
|
|
1129
1129
|
byDay: /* @__PURE__ */ new Map(),
|
|
1130
1130
|
byDayModels: /* @__PURE__ */ new Map(),
|
|
1131
1131
|
bySite: /* @__PURE__ */ new Map(),
|
|
1132
|
+
unpricedModels: /* @__PURE__ */ new Set(),
|
|
1132
1133
|
planCalls: /* @__PURE__ */ new Map(),
|
|
1133
1134
|
turns: [],
|
|
1134
1135
|
perf: [],
|
|
@@ -1214,6 +1215,7 @@ function foldSession(events, subscriptionProviders, officialProviderIds, routes
|
|
|
1214
1215
|
if (usage === void 0) continue;
|
|
1215
1216
|
const modelKey = key;
|
|
1216
1217
|
const day = dayStamp(event.time);
|
|
1218
|
+
if (!subscription && !isPriced(modelKey)) fold.unpricedModels.add(modelKey);
|
|
1217
1219
|
foldUsage(fold.total, usage, modelKey, subscription, event.time, official);
|
|
1218
1220
|
foldUsage(usageCell(fold.byModel, modelKey), usage, modelKey, subscription, event.time, official);
|
|
1219
1221
|
foldUsage(usageCell(fold.byDay, day), usage, modelKey, subscription, event.time, official);
|
|
@@ -1402,6 +1404,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
1402
1404
|
const byDay = /* @__PURE__ */ new Map();
|
|
1403
1405
|
const byDayModels = /* @__PURE__ */ new Map();
|
|
1404
1406
|
const bySite = /* @__PURE__ */ new Map();
|
|
1407
|
+
const unpricedModels = /* @__PURE__ */ new Set();
|
|
1405
1408
|
const planCalls = /* @__PURE__ */ new Map();
|
|
1406
1409
|
const sessionRows = [];
|
|
1407
1410
|
const turnRows = [];
|
|
@@ -1425,6 +1428,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
1425
1428
|
for (const [day, cell] of fold.byDay) mergeUsageInto(usageCell(byDay, day), cell);
|
|
1426
1429
|
for (const [day, models] of fold.byDayModels) for (const [modelKey, cell] of models) mergeUsageInto(modelDayCell(byDayModels, day, modelKey), cell);
|
|
1427
1430
|
for (const [siteKey, cell] of fold.bySite) mergeUsageInto(usageCell(bySite, siteKey), cell);
|
|
1431
|
+
for (const id of fold.unpricedModels) unpricedModels.add(id);
|
|
1428
1432
|
for (const [modelKey, count] of fold.planCalls) planCalls.set(modelKey, (planCalls.get(modelKey) ?? 0) + count);
|
|
1429
1433
|
for (const sample of fold.perf) {
|
|
1430
1434
|
let modelAccum = perfModel.get(sample.model);
|
|
@@ -1521,6 +1525,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
1521
1525
|
byTurn: turnRows.slice(0, 200),
|
|
1522
1526
|
byWorkspace: workspaces.slice(0, 100),
|
|
1523
1527
|
...bySite.size === 0 ? {} : { bySite: toRecord(bySite) },
|
|
1528
|
+
...unpricedModels.size === 0 ? {} : { unpricedModels: [...unpricedModels].sort() },
|
|
1524
1529
|
...perf === void 0 ? {} : { perf },
|
|
1525
1530
|
byRole: (() => {
|
|
1526
1531
|
const chars = roles.userChars + roles.toolChars;
|
|
@@ -2911,6 +2916,10 @@ const relayGate = createCooldownGate({
|
|
|
2911
2916
|
});
|
|
2912
2917
|
/** Abort a relay fetch when the upstream hangs beyond this budget. */
|
|
2913
2918
|
const FETCH_TIMEOUT_MS = 8e3;
|
|
2919
|
+
/** 指纹识别缓存 TTL(毫秒):识别结果低频变化,5 分钟内同 origin 不再重复探测。 */
|
|
2920
|
+
const FINGERPRINT_TTL_MS = 300 * 1e3;
|
|
2921
|
+
/** 每 origin 的识别结果缓存:`kind` 是识别出的中转站程序,`at` 是探测时刻。 */
|
|
2922
|
+
const fingerprintCache = /* @__PURE__ */ new Map();
|
|
2914
2923
|
/** Number, or null when the value is not a finite number (nor numeric string). */
|
|
2915
2924
|
function numberOrNull(value) {
|
|
2916
2925
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
@@ -3054,7 +3063,8 @@ async function queryRelayQuota(ctx, route) {
|
|
|
3054
3063
|
origin: originOf(route.baseURL),
|
|
3055
3064
|
displayName: route.displayName ?? route.route
|
|
3056
3065
|
};
|
|
3057
|
-
|
|
3066
|
+
const gateKey = `${route.baseURL}::${route.apiKeyEnv}`;
|
|
3067
|
+
if (!relayGate.check(gateKey)) return {
|
|
3058
3068
|
...base,
|
|
3059
3069
|
kind: "unknown",
|
|
3060
3070
|
status: "unavailable"
|
|
@@ -3065,12 +3075,22 @@ async function queryRelayQuota(ctx, route) {
|
|
|
3065
3075
|
kind: "unknown",
|
|
3066
3076
|
status: "not-configured"
|
|
3067
3077
|
};
|
|
3078
|
+
const cached = fingerprintCache.get(base.origin);
|
|
3079
|
+
if (cached !== void 0 && Date.now() - cached.at < FINGERPRINT_TTL_MS) return {
|
|
3080
|
+
...base,
|
|
3081
|
+
kind: cached.kind,
|
|
3082
|
+
status: "ok"
|
|
3083
|
+
};
|
|
3068
3084
|
try {
|
|
3069
3085
|
const sub2 = await fetchRelayJson(endpointOf(route.baseURL, "/v1/usage"), hit.value);
|
|
3070
3086
|
if (sub2.ok) {
|
|
3071
3087
|
const parsed = parseSub2ApiUsage(sub2.data);
|
|
3072
3088
|
if (parsed !== null) {
|
|
3073
|
-
relayGate.success(
|
|
3089
|
+
relayGate.success(gateKey);
|
|
3090
|
+
fingerprintCache.set(base.origin, {
|
|
3091
|
+
kind: "sub2api",
|
|
3092
|
+
at: Date.now()
|
|
3093
|
+
});
|
|
3074
3094
|
return {
|
|
3075
3095
|
...base,
|
|
3076
3096
|
kind: "sub2api",
|
|
@@ -3079,7 +3099,7 @@ async function queryRelayQuota(ctx, route) {
|
|
|
3079
3099
|
...parsed.windows !== void 0 ? { windows: parsed.windows } : {}
|
|
3080
3100
|
};
|
|
3081
3101
|
}
|
|
3082
|
-
relayGate.fail(
|
|
3102
|
+
relayGate.fail(gateKey);
|
|
3083
3103
|
return {
|
|
3084
3104
|
...base,
|
|
3085
3105
|
kind: "sub2api",
|
|
@@ -3087,7 +3107,7 @@ async function queryRelayQuota(ctx, route) {
|
|
|
3087
3107
|
};
|
|
3088
3108
|
}
|
|
3089
3109
|
if (sub2.status === 401 || sub2.status === 403) {
|
|
3090
|
-
relayGate.fail(
|
|
3110
|
+
relayGate.fail(gateKey);
|
|
3091
3111
|
return {
|
|
3092
3112
|
...base,
|
|
3093
3113
|
kind: "unknown",
|
|
@@ -3096,7 +3116,7 @@ async function queryRelayQuota(ctx, route) {
|
|
|
3096
3116
|
}
|
|
3097
3117
|
const na = await fetchRelayJson(endpointOf(route.baseURL, "/api/status"), hit.value);
|
|
3098
3118
|
if (na.status === 401 || na.status === 403) {
|
|
3099
|
-
relayGate.fail(
|
|
3119
|
+
relayGate.fail(gateKey);
|
|
3100
3120
|
return {
|
|
3101
3121
|
...base,
|
|
3102
3122
|
kind: "unknown",
|
|
@@ -3106,7 +3126,11 @@ async function queryRelayQuota(ctx, route) {
|
|
|
3106
3126
|
if (na.ok) {
|
|
3107
3127
|
const parsed = parseNewApiStatus(na.data);
|
|
3108
3128
|
if (parsed !== null) {
|
|
3109
|
-
relayGate.success(
|
|
3129
|
+
relayGate.success(gateKey);
|
|
3130
|
+
fingerprintCache.set(base.origin, {
|
|
3131
|
+
kind: "new-api",
|
|
3132
|
+
at: Date.now()
|
|
3133
|
+
});
|
|
3110
3134
|
return {
|
|
3111
3135
|
...base,
|
|
3112
3136
|
kind: "new-api",
|
|
@@ -3114,21 +3138,21 @@ async function queryRelayQuota(ctx, route) {
|
|
|
3114
3138
|
...parsed.windows !== void 0 ? { windows: parsed.windows } : {}
|
|
3115
3139
|
};
|
|
3116
3140
|
}
|
|
3117
|
-
relayGate.fail(
|
|
3141
|
+
relayGate.fail(gateKey);
|
|
3118
3142
|
return {
|
|
3119
3143
|
...base,
|
|
3120
3144
|
kind: "new-api",
|
|
3121
3145
|
status: "invalid-response"
|
|
3122
3146
|
};
|
|
3123
3147
|
}
|
|
3124
|
-
relayGate.fail(
|
|
3148
|
+
relayGate.fail(gateKey);
|
|
3125
3149
|
return {
|
|
3126
3150
|
...base,
|
|
3127
3151
|
kind: "unknown",
|
|
3128
3152
|
status: "unavailable"
|
|
3129
3153
|
};
|
|
3130
3154
|
} catch (error) {
|
|
3131
|
-
relayGate.fail(
|
|
3155
|
+
relayGate.fail(gateKey);
|
|
3132
3156
|
return {
|
|
3133
3157
|
...base,
|
|
3134
3158
|
kind: "unknown",
|
|
@@ -3172,6 +3196,35 @@ const ENABLE_USAGE_STATS_TOOL_FIELD = "enableUsageStatsTool";
|
|
|
3172
3196
|
* missing file answers `{ error }` so the dashboard shows zeros, never
|
|
3173
3197
|
* fabricated samples.
|
|
3174
3198
|
*/
|
|
3199
|
+
/** Peer 地址是否为回环(本地)。回环防护:本插件的端点只供本机浏览器用,
|
|
3200
|
+
* 局域网/远端请求一律拒绝,避免面板数据(含中转站 origin 与余额)外泄。 */
|
|
3201
|
+
function isLoopbackPeer(req) {
|
|
3202
|
+
const address = req.socket.remoteAddress;
|
|
3203
|
+
if (address === void 0) return false;
|
|
3204
|
+
return address === "::1" || address.startsWith("127.") || address.startsWith("::ffff:127.");
|
|
3205
|
+
}
|
|
3206
|
+
/** 校验 Host 头是本机回环(127.x / ::1 / localhost 或空,供 curl 不带 Host 的极简请求)。 */
|
|
3207
|
+
function isLoopbackHost(req) {
|
|
3208
|
+
const host = req.headers.host;
|
|
3209
|
+
if (host === void 0 || host === "") return true;
|
|
3210
|
+
const name = host.split(":")[0];
|
|
3211
|
+
return name === "localhost" || name === "::1" || name !== void 0 && name.startsWith("127.");
|
|
3212
|
+
}
|
|
3213
|
+
/**
|
|
3214
|
+
* 回环防护守卫:仅接受回环 GET 请求(peer socket 地址 + Host 头同时校验)。
|
|
3215
|
+
* 不满足时返回 403 并结束响应;调用方在 handler 顶部调用,返回 false 即已拒绝。
|
|
3216
|
+
* @param req - 当前请求。
|
|
3217
|
+
* @param res - 当前响应。
|
|
3218
|
+
* @returns 是否放行;false = 已拒绝并结束响应。
|
|
3219
|
+
*/
|
|
3220
|
+
function guardLoopback(req, res) {
|
|
3221
|
+
if (!(req.method === "GET" || req.method === "POST") || !isLoopbackPeer(req) || !isLoopbackHost(req)) {
|
|
3222
|
+
res.writeHead(403, { "content-type": "application/json; charset=utf-8" });
|
|
3223
|
+
res.end(JSON.stringify({ error: "forbidden: loopback only" }));
|
|
3224
|
+
return false;
|
|
3225
|
+
}
|
|
3226
|
+
return true;
|
|
3227
|
+
}
|
|
3175
3228
|
/** usage_stats 工具开关的设置命名空间 id(下端与 node 共用同一常量)。 */
|
|
3176
3229
|
const usageBillingSettingsNs = settingsNamespace(BILLING_SETTINGS_NAMESPACE);
|
|
3177
3230
|
/** 该命名空间的 wire schema:`enableUsageStatsTool` 布尔,默认关闭(issue 诉求)。 */
|
|
@@ -3428,10 +3481,12 @@ function apply(ctx, config = {}) {
|
|
|
3428
3481
|
"today",
|
|
3429
3482
|
"month",
|
|
3430
3483
|
"session",
|
|
3431
|
-
"all"
|
|
3484
|
+
"all",
|
|
3485
|
+
"bySite",
|
|
3486
|
+
"relay"
|
|
3432
3487
|
],
|
|
3433
3488
|
required: true,
|
|
3434
|
-
description: "统计范围:today / month / session / all"
|
|
3489
|
+
description: "统计范围:today / month / session / all / bySite / relay"
|
|
3435
3490
|
} },
|
|
3436
3491
|
output: {
|
|
3437
3492
|
schema: {
|
|
@@ -3484,6 +3539,26 @@ function apply(ctx, config = {}) {
|
|
|
3484
3539
|
input: stats.total.input,
|
|
3485
3540
|
output: stats.total.output
|
|
3486
3541
|
};
|
|
3542
|
+
if (args.range === "bySite" || args.range === "relay") {
|
|
3543
|
+
const bySite = stats.bySite ?? {};
|
|
3544
|
+
let cost = 0;
|
|
3545
|
+
let calls = 0;
|
|
3546
|
+
let input = 0;
|
|
3547
|
+
let output = 0;
|
|
3548
|
+
for (const usage of Object.values(bySite)) {
|
|
3549
|
+
cost += usage.cost;
|
|
3550
|
+
calls += usage.calls;
|
|
3551
|
+
input += usage.input;
|
|
3552
|
+
output += usage.output;
|
|
3553
|
+
}
|
|
3554
|
+
return {
|
|
3555
|
+
range: args.range,
|
|
3556
|
+
cost,
|
|
3557
|
+
calls,
|
|
3558
|
+
input,
|
|
3559
|
+
output
|
|
3560
|
+
};
|
|
3561
|
+
}
|
|
3487
3562
|
if (args.range === "today") {
|
|
3488
3563
|
const day = stats.byDay[dayStamp(Date.now())];
|
|
3489
3564
|
return day === void 0 ? zero : {
|
|
@@ -3556,7 +3631,8 @@ function apply(ctx, config = {}) {
|
|
|
3556
3631
|
ctx.effect(() => ctx.webServer.register({
|
|
3557
3632
|
kind: "exact",
|
|
3558
3633
|
path: "/api/billing/pricing",
|
|
3559
|
-
handler: async (
|
|
3634
|
+
handler: async (req, res) => {
|
|
3635
|
+
if (!guardLoopback(req, res)) return;
|
|
3560
3636
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
3561
3637
|
res.end(JSON.stringify(live));
|
|
3562
3638
|
}
|
|
@@ -3564,7 +3640,8 @@ function apply(ctx, config = {}) {
|
|
|
3564
3640
|
ctx.effect(() => ctx.webServer.register({
|
|
3565
3641
|
kind: "exact",
|
|
3566
3642
|
path: "/api/billing/balance",
|
|
3567
|
-
handler: async (
|
|
3643
|
+
handler: async (req, res) => {
|
|
3644
|
+
if (!guardLoopback(req, res)) return;
|
|
3568
3645
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
3569
3646
|
const providers = { ...await readPiAiProviders(ctx.settings) };
|
|
3570
3647
|
if (providers["deepseek"] === void 0) providers["deepseek"] = { apiKeyEnv: config.balanceApiKeyEnv ?? DEFAULT_BALANCE_API_KEY_ENV };
|
|
@@ -3578,6 +3655,7 @@ function apply(ctx, config = {}) {
|
|
|
3578
3655
|
kind: "exact",
|
|
3579
3656
|
path: "/api/billing/usage-tool",
|
|
3580
3657
|
handler: async (req, res) => {
|
|
3658
|
+
if (!guardLoopback(req, res)) return;
|
|
3581
3659
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
3582
3660
|
const enabled = usageSettingsScope?.get().enableUsageStatsTool ?? false;
|
|
3583
3661
|
if (req.method === "GET") {
|
|
@@ -3642,7 +3720,8 @@ function apply(ctx, config = {}) {
|
|
|
3642
3720
|
ctx.effect(() => ctx.webServer.register({
|
|
3643
3721
|
kind: "exact",
|
|
3644
3722
|
path: "/api/billing/subscriptions",
|
|
3645
|
-
handler: async (
|
|
3723
|
+
handler: async (req, res) => {
|
|
3724
|
+
if (!guardLoopback(req, res)) return;
|
|
3646
3725
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
3647
3726
|
if (Date.now() - quotaCache.at >= SUBSCRIPTION_CACHE_MS) await refreshQuotas();
|
|
3648
3727
|
res.end(JSON.stringify({ quotas: quotaCache.quotas }));
|
|
@@ -3672,16 +3751,26 @@ function apply(ctx, config = {}) {
|
|
|
3672
3751
|
ctx.effect(() => ctx.webServer.register({
|
|
3673
3752
|
kind: "exact",
|
|
3674
3753
|
path: "/api/billing/relay-quotas",
|
|
3675
|
-
handler: async (
|
|
3754
|
+
handler: async (req, res) => {
|
|
3755
|
+
if (!guardLoopback(req, res)) return;
|
|
3676
3756
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
3677
3757
|
if (Date.now() - relayCache.at >= SUBSCRIPTION_CACHE_MS) await refreshRelay();
|
|
3678
|
-
|
|
3758
|
+
const diagnostics = relayCache.quotas.map((row) => ({
|
|
3759
|
+
route: row.route,
|
|
3760
|
+
origin: row.origin,
|
|
3761
|
+
kind: row.kind
|
|
3762
|
+
}));
|
|
3763
|
+
res.end(JSON.stringify({
|
|
3764
|
+
quotas: relayCache.quotas,
|
|
3765
|
+
diagnostics
|
|
3766
|
+
}));
|
|
3679
3767
|
}
|
|
3680
3768
|
}), "usage-billing: relay-quotas route");
|
|
3681
3769
|
ctx.effect(() => ctx.webServer.register({
|
|
3682
3770
|
kind: "exact",
|
|
3683
3771
|
path: "/api/billing/usage-stats",
|
|
3684
|
-
handler: async (
|
|
3772
|
+
handler: async (req, res) => {
|
|
3773
|
+
if (!guardLoopback(req, res)) return;
|
|
3685
3774
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
3686
3775
|
try {
|
|
3687
3776
|
const stats = await aggregator.aggregate();
|
|
@@ -3714,4 +3803,4 @@ function apply(ctx, config = {}) {
|
|
|
3714
3803
|
}), "usage-billing: usage-stats route");
|
|
3715
3804
|
}
|
|
3716
3805
|
//#endregion
|
|
3717
|
-
export { apply, inject, readPiAiProviderRoutes, resolveSubscriptionKeys };
|
|
3806
|
+
export { apply, guardLoopback, inject, readPiAiProviderRoutes, resolveSubscriptionKeys };
|
package/lib/types/aggregate.d.ts
CHANGED
|
@@ -147,6 +147,8 @@ export interface UsageStatsDocument {
|
|
|
147
147
|
* 属估算口径,UI 需标注)。旧快照可能缺失。
|
|
148
148
|
*/
|
|
149
149
|
byRole?: RoleCost;
|
|
150
|
+
/** 不可计价的模型 id(未收录 / 无价,费用按 0 计);供面板提示用户自查与反馈。 */
|
|
151
|
+
unpricedModels?: readonly string[];
|
|
150
152
|
/**
|
|
151
153
|
* 性能指标(TTFT / 生成速度 / 总延迟)按模型与按小时聚合;旧快照可能缺失。
|
|
152
154
|
* 口径:TTFT = request/header → 首个内容 chunk;生成速度 = 输出 token ÷ 生成时长;
|
|
@@ -269,6 +271,8 @@ interface SessionFold {
|
|
|
269
271
|
byDayModels: Map<string, Map<string, ModelUsage>>;
|
|
270
272
|
/** 中转站归组:按 provider 路由归类到站点/直连/未知路由(key = {@link siteBucketKey})。 */
|
|
271
273
|
bySite: Map<string, ModelUsage>;
|
|
274
|
+
/** 不可计价模型 id(未收录/无价,且非订阅)集合;跨会话合并后输出给面板提示。 */
|
|
275
|
+
unpricedModels: Set<string>;
|
|
272
276
|
/** 每个模型 key 在本会话内走订阅通道的调用数(合并时跨会话累加判定 plan)。 */
|
|
273
277
|
planCalls: Map<string, number>;
|
|
274
278
|
/** 每轮费用明细(按轮次号升序,不含 sessionId);sessionId 在合并时补齐。 */
|
|
@@ -59,6 +59,8 @@ export declare const PROVIDER_ALIASES: Readonly<Record<string, readonly string[]
|
|
|
59
59
|
* 导出供守卫测试:短别名(mi/yi)仅允许前缀形式,防止 minimax 等误吞。
|
|
60
60
|
*/
|
|
61
61
|
export declare function providerFromModelKey(modelKey: string): string | undefined;
|
|
62
|
+
/** 仅供测试:暴露厂商映射表(subscriptionVendorOf 仍是唯一消费入口)。 */
|
|
63
|
+
export declare const SUBSCRIPTION_VENDORS_FOR_TEST: Readonly<Record<string, string>>;
|
|
62
64
|
/**
|
|
63
65
|
* 本月预计总花费:按本月已有记录的平均日消耗 × 本月天数外推;无本月记录时
|
|
64
66
|
* 回退为最近 7 天日均 × 本月天数;无任何记录时返回 0(调用方不展示)。
|
|
@@ -200,6 +202,8 @@ export interface UsageStats {
|
|
|
200
202
|
cost: number;
|
|
201
203
|
reasoning: number;
|
|
202
204
|
}>;
|
|
205
|
+
/** 不可计价模型 id(未收录/无价,费用按 0 计);旧快照可能缺失。 */
|
|
206
|
+
unpricedModels?: readonly string[];
|
|
203
207
|
/** 按角色费用归因(估算口径:输出实测,输入按消息长度摊分);旧快照可能缺失。 */
|
|
204
208
|
byRole?: {
|
|
205
209
|
user: number;
|
|
@@ -24,6 +24,8 @@ export interface SessionExportRow {
|
|
|
24
24
|
}
|
|
25
25
|
/** 按日 CSV:日期,调用,输入,输出,缓存命中,缓存未命中,费用(元)。 */
|
|
26
26
|
export declare function dayRowsCsv(byDay: Record<string, DayExportRow>): string;
|
|
27
|
+
/** 按站点(中转站/直连/未知路由)CSV:站点,类别,调用,费用(元)。 */
|
|
28
|
+
export declare function siteRowsCsv(bySite: Record<string, DayExportRow>): string;
|
|
27
29
|
/** 按会话 CSV:会话 id,标题,项目,调用,费用(元),最后活跃(ISO)。 */
|
|
28
30
|
export declare function sessionRowsCsv(rows: readonly SessionExportRow[]): string;
|
|
29
31
|
/** 导出文件名:带日期范围(usage-2026-08-01_2026-08-22.csv);无日期时只带前缀。 */
|
|
@@ -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.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.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.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.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.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.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.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';
|
|
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>;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -9,11 +9,20 @@
|
|
|
9
9
|
* missing file answers `{ error }` so the dashboard shows zeros, never
|
|
10
10
|
* fabricated samples.
|
|
11
11
|
*/
|
|
12
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
12
13
|
import type { Context } from '@deepseek-ai/cordis';
|
|
13
14
|
import type { CredentialProvider } from '@deepseek-ai/dsh-credentials';
|
|
14
15
|
import { type SettingsProvider } from '@deepseek-ai/dsh-settings';
|
|
15
16
|
import type { CustomBalanceConfig, SubscriptionPlanConfig } from './pricing-shared.ts';
|
|
16
17
|
import { type IdentifiedSubscriptionPlan, type SubscriptionKeys } from './subscriptions.ts';
|
|
18
|
+
/**
|
|
19
|
+
* 回环防护守卫:仅接受回环 GET 请求(peer socket 地址 + Host 头同时校验)。
|
|
20
|
+
* 不满足时返回 403 并结束响应;调用方在 handler 顶部调用,返回 false 即已拒绝。
|
|
21
|
+
* @param req - 当前请求。
|
|
22
|
+
* @param res - 当前响应。
|
|
23
|
+
* @returns 是否放行;false = 已拒绝并结束响应。
|
|
24
|
+
*/
|
|
25
|
+
export declare function guardLoopback(req: IncomingMessage, res: ServerResponse): boolean;
|
|
17
26
|
/** Plugin configuration. */
|
|
18
27
|
export interface UsageBillingConfig {
|
|
19
28
|
/** Absolute path to a `.dsh-usage-stats.json` fallback file. */
|
|
@@ -123,6 +123,17 @@ export interface RelayQuota {
|
|
|
123
123
|
/** Response of `/api/billing/relay-quotas`. */
|
|
124
124
|
export interface RelayResponse {
|
|
125
125
|
quotas: readonly RelayQuota[];
|
|
126
|
+
/** 诊断:每条路由的归类(route / origin / kind / unknown),供「我的中转站为什么不显示」自查。 */
|
|
127
|
+
diagnostics?: readonly RelayDiagnostic[];
|
|
128
|
+
}
|
|
129
|
+
/** 一条路由的归类诊断(供面板/日志定位中转站识别结果)。 */
|
|
130
|
+
export interface RelayDiagnostic {
|
|
131
|
+
/** llm-pi-ai providers 路由名。 */
|
|
132
|
+
route: string;
|
|
133
|
+
/** 站点 origin(baseURL 归一化)。 */
|
|
134
|
+
origin: string;
|
|
135
|
+
/** 识别结果:new-api / sub2api(有额度读出)或 unknown(读不出)。 */
|
|
136
|
+
kind: 'new-api' | 'sub2api' | 'unknown';
|
|
126
137
|
}
|
|
127
138
|
/** Config for one subscription adapter (validated in apply). */
|
|
128
139
|
export interface SubscriptionPlanConfig {
|
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.10",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|