@kenz1117/dsh-ui-usage-billing 1.6.7 → 1.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +5 -5
- package/lib/index.js +156 -38
- package/lib/types/aggregate.d.ts +19 -0
- package/lib/types/client/api/balance.d.ts +8 -4
- package/lib/types/client/api/http.d.ts +36 -0
- package/lib/types/client/api/subscriptions.d.ts +7 -1
- package/lib/types/client/domain/types.d.ts +6 -0
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/pricing-shared.d.ts +6 -0
- package/lib/types/routes/balance.d.ts +18 -2
- package/lib/types/routes/relay-quotas.d.ts +8 -0
- package/lib/types/routes/subscriptions.d.ts +8 -0
- package/lib/types/routes/usage-stats.d.ts +7 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -3558,6 +3558,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
3558
3558
|
let ledgerNeedsSave = false;
|
|
3559
3559
|
let lastLedgerSaveAt = 0;
|
|
3560
3560
|
const unreadable = /* @__PURE__ */ new Map();
|
|
3561
|
+
const titleHints = /* @__PURE__ */ new Map();
|
|
3561
3562
|
let inflight;
|
|
3562
3563
|
let ledgerAppliedMigrations;
|
|
3563
3564
|
let lastDoc;
|
|
@@ -3590,6 +3591,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
3590
3591
|
const foldFingerprintOf = (registered, routes) => configFingerprint(subscriptionMatcher, officialProviderIds, routeAliases, searchEstimate, options.modelKeyAliases, registered, routeOriginsOf(routes));
|
|
3591
3592
|
let foldFingerprint = foldFingerprintOf(/* @__PURE__ */ new Set(), {});
|
|
3592
3593
|
const FOLD_CHUNK_EVENTS = 8e3;
|
|
3594
|
+
const FOLD_YIELD_SESSIONS = 8;
|
|
3593
3595
|
/** 惰性失效(价目表时序):账本/缓存行若以「当时不可计价」的模型落盘,
|
|
3594
3596
|
* 而价目表现在能计价了,该行必须重折——否则显示层按实时价算出的
|
|
3595
3597
|
* 「目录价预估」与服务端 cost=0 并存,出现「明细比总额高」的双口径
|
|
@@ -3692,7 +3694,10 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
3692
3694
|
let formatUnreadable = 0;
|
|
3693
3695
|
let noLedgerUnreadable = 0;
|
|
3694
3696
|
let staleLedgerSessions = 0;
|
|
3695
|
-
for (const meta of metas) {
|
|
3697
|
+
for (const [metaIndex, meta] of metas.entries()) {
|
|
3698
|
+
if (metaIndex > 0 && metaIndex % FOLD_YIELD_SESSIONS === 0) await new Promise((resolve) => {
|
|
3699
|
+
setImmediate(resolve);
|
|
3700
|
+
});
|
|
3696
3701
|
const id = String(meta.id);
|
|
3697
3702
|
seen.add(id);
|
|
3698
3703
|
const stamp = await stampOf(meta);
|
|
@@ -3798,8 +3803,13 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
3798
3803
|
stamp,
|
|
3799
3804
|
format
|
|
3800
3805
|
});
|
|
3806
|
+
if (options.resolveSessionTitle !== void 0) {
|
|
3807
|
+
const hint = options.resolveSessionTitle(meta);
|
|
3808
|
+
if (hint !== void 0) titleHints.set(id, hint);
|
|
3809
|
+
}
|
|
3801
3810
|
console.warn("[usage-billing] skip unreadable session", id, error);
|
|
3802
3811
|
}
|
|
3812
|
+
if (included.has(id)) titleHints.delete(id);
|
|
3803
3813
|
}
|
|
3804
3814
|
for (const key of [...cache.keys()]) if (!seen.has(key)) cache.delete(key);
|
|
3805
3815
|
for (const key of [...unreadable.keys()]) if (!seen.has(key)) unreadable.delete(key);
|
|
@@ -3813,12 +3823,17 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
3813
3823
|
if (included.has(entry.id)) continue;
|
|
3814
3824
|
try {
|
|
3815
3825
|
const stale = (entry.foldVersion ?? 1) < 23 || entry.fingerprint !== foldFingerprint;
|
|
3826
|
+
const fold = deserializedFoldOf(entry.fold);
|
|
3827
|
+
const hint = titleHints.get(entry.id);
|
|
3816
3828
|
folds.push({
|
|
3817
3829
|
id: entry.id,
|
|
3818
3830
|
...entry.cwd === void 0 ? {} : { cwd: entry.cwd },
|
|
3819
3831
|
...entry.subOf === void 0 ? {} : { subOf: entry.subOf },
|
|
3820
3832
|
...stale ? { staleLedger: true } : {},
|
|
3821
|
-
fold:
|
|
3833
|
+
fold: hint === void 0 ? fold : {
|
|
3834
|
+
...fold,
|
|
3835
|
+
title: hint
|
|
3836
|
+
}
|
|
3822
3837
|
});
|
|
3823
3838
|
included.add(entry.id);
|
|
3824
3839
|
if (stale) staleLedgerSessions += 1;
|
|
@@ -4055,6 +4070,9 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
4055
4070
|
async flush() {
|
|
4056
4071
|
if (inflight !== void 0) await inflight.catch(() => {});
|
|
4057
4072
|
await saveLedger(true);
|
|
4073
|
+
},
|
|
4074
|
+
lastDocOf() {
|
|
4075
|
+
return lastDoc;
|
|
4058
4076
|
}
|
|
4059
4077
|
};
|
|
4060
4078
|
}
|
|
@@ -7296,8 +7314,9 @@ function registerPricingRoutes(ctx, deps) {
|
|
|
7296
7314
|
* TokenHub 等)+ 用户自定义余额 + 声明端点 + 余额差对账。5 分钟缓存(避免
|
|
7297
7315
|
* 每 30s 后台轮询打上游),in-flight 去重(并发请求共享同一次进行中的刷新)。
|
|
7298
7316
|
*
|
|
7299
|
-
* 路由文件只关心 HTTP 缓存逻辑:5 分钟过期判定 +
|
|
7300
|
-
*
|
|
7317
|
+
* 路由文件只关心 HTTP 缓存逻辑:5 分钟过期判定 + 刷新预算(超预算回旧
|
|
7318
|
+
* 缓存并标 stale,不把响应挂在长查询上)+ 失败时仍发旧缓存(不挂起已
|
|
7319
|
+
* writeHead 的响应)。上游查询(queryBalances / queryCustomBalances /
|
|
7301
7320
|
* queryDeclaredEndpoints / reconcileBalanceDelta)在装配器里跑,路由只通过
|
|
7302
7321
|
* `cached()` 与 `refreshOnce()` 拿到结果。
|
|
7303
7322
|
*/
|
|
@@ -7312,12 +7331,21 @@ function registerBalanceRoute(ctx, deps) {
|
|
|
7312
7331
|
res.end(JSON.stringify(deps.cached().doc));
|
|
7313
7332
|
return;
|
|
7314
7333
|
}
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
} catch (error) {
|
|
7334
|
+
let timedOut = false;
|
|
7335
|
+
const onFailure = (error) => {
|
|
7318
7336
|
console.warn("[usage-billing] refreshBalances failed; serving cached balances:", error);
|
|
7319
|
-
}
|
|
7320
|
-
|
|
7337
|
+
};
|
|
7338
|
+
await Promise.race([deps.refreshOnce().then(() => void 0, onFailure), new Promise((resolve) => {
|
|
7339
|
+
setTimeout(() => {
|
|
7340
|
+
timedOut = true;
|
|
7341
|
+
resolve();
|
|
7342
|
+
}, deps.responseBudgetMs).unref();
|
|
7343
|
+
})]);
|
|
7344
|
+
const doc = deps.cached().doc;
|
|
7345
|
+
res.end(JSON.stringify(timedOut ? {
|
|
7346
|
+
...doc,
|
|
7347
|
+
stale: true
|
|
7348
|
+
} : doc));
|
|
7321
7349
|
}
|
|
7322
7350
|
}), "usage-billing: balance route");
|
|
7323
7351
|
}
|
|
@@ -7422,12 +7450,19 @@ function registerSubscriptionsRoute(ctx, deps) {
|
|
|
7422
7450
|
handler: async (req, res) => {
|
|
7423
7451
|
if (!guardLoopback(req, res, deps.trustedHosts)) return;
|
|
7424
7452
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
} catch (error) {
|
|
7453
|
+
let timedOut = false;
|
|
7454
|
+
if (Date.now() - deps.cached().at >= deps.cacheMs) await Promise.race([deps.refreshOnce().then(() => void 0, (error) => {
|
|
7428
7455
|
console.warn("[usage-billing] refreshQuotas failed; serving cached quotas:", error);
|
|
7429
|
-
}
|
|
7430
|
-
|
|
7456
|
+
}), new Promise((resolve) => {
|
|
7457
|
+
setTimeout(() => {
|
|
7458
|
+
timedOut = true;
|
|
7459
|
+
resolve();
|
|
7460
|
+
}, deps.responseBudgetMs).unref();
|
|
7461
|
+
})]);
|
|
7462
|
+
res.end(JSON.stringify({
|
|
7463
|
+
quotas: deps.cached().quotas,
|
|
7464
|
+
...timedOut ? { stale: true } : {}
|
|
7465
|
+
}));
|
|
7431
7466
|
}
|
|
7432
7467
|
}), "usage-billing: subscriptions route");
|
|
7433
7468
|
}
|
|
@@ -7450,11 +7485,15 @@ function registerRelayQuotasRoute(ctx, deps) {
|
|
|
7450
7485
|
handler: async (req, res) => {
|
|
7451
7486
|
if (!guardLoopback(req, res, deps.trustedHosts)) return;
|
|
7452
7487
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
} catch (error) {
|
|
7488
|
+
let timedOut = false;
|
|
7489
|
+
if (Date.now() - deps.cached().at >= deps.cacheMs) await Promise.race([deps.refreshOnce().then(() => void 0, (error) => {
|
|
7456
7490
|
console.warn("[usage-billing] refreshRelay failed; serving cached relay quotas:", error);
|
|
7457
|
-
}
|
|
7491
|
+
}), new Promise((resolve) => {
|
|
7492
|
+
setTimeout(() => {
|
|
7493
|
+
timedOut = true;
|
|
7494
|
+
resolve();
|
|
7495
|
+
}, deps.responseBudgetMs).unref();
|
|
7496
|
+
})]);
|
|
7458
7497
|
const diagnostics = deps.cached().quotas.map((row) => ({
|
|
7459
7498
|
route: row.route,
|
|
7460
7499
|
origin: row.origin,
|
|
@@ -7462,7 +7501,8 @@ function registerRelayQuotasRoute(ctx, deps) {
|
|
|
7462
7501
|
}));
|
|
7463
7502
|
res.end(JSON.stringify({
|
|
7464
7503
|
quotas: deps.cached().quotas,
|
|
7465
|
-
diagnostics
|
|
7504
|
+
diagnostics,
|
|
7505
|
+
...timedOut ? { stale: true } : {}
|
|
7466
7506
|
}));
|
|
7467
7507
|
}
|
|
7468
7508
|
}), "usage-billing: relay-quotas route");
|
|
@@ -7617,10 +7657,21 @@ function registerUsageStatsRoute(ctx, deps) {
|
|
|
7617
7657
|
res.end(statsBodyOf(fresh));
|
|
7618
7658
|
return;
|
|
7619
7659
|
}
|
|
7660
|
+
const recent = deps.lastDoc?.();
|
|
7661
|
+
if (recent !== void 0) {
|
|
7662
|
+
res.end(JSON.stringify({
|
|
7663
|
+
...decorate(recent),
|
|
7664
|
+
staleSnapshot: true
|
|
7665
|
+
}));
|
|
7666
|
+
return;
|
|
7667
|
+
}
|
|
7620
7668
|
for (const candidate of deps.candidates) {
|
|
7621
7669
|
const doc = await deps.readSnapshot(candidate);
|
|
7622
7670
|
if (doc === null) continue;
|
|
7623
|
-
res.end(JSON.stringify(
|
|
7671
|
+
res.end(JSON.stringify({
|
|
7672
|
+
...decorate(doc),
|
|
7673
|
+
staleSnapshot: true
|
|
7674
|
+
}));
|
|
7624
7675
|
return;
|
|
7625
7676
|
}
|
|
7626
7677
|
res.end(JSON.stringify({ error: "usage stats unavailable" }));
|
|
@@ -7693,6 +7744,24 @@ const WARMUP_DELAY_MS = 3e3;
|
|
|
7693
7744
|
/** 订阅套餐额度缓存时长(毫秒):上游配额 API 低频变化,5 分钟足够。 */
|
|
7694
7745
|
const SUBSCRIPTION_CACHE_MS = 3e5;
|
|
7695
7746
|
const BALANCE_CACHE_MS = 3e5;
|
|
7747
|
+
/**
|
|
7748
|
+
* 余额路由的总等待预算(毫秒):超预算立刻回上一次成功的余额(stale 标记),
|
|
7749
|
+
* 刷新在后台继续。取 8s——与上游单次查询超时(FETCH_TIMEOUT_MS)对齐:上游
|
|
7750
|
+
* 真的不可达时多等没有意义,先把响应还给浏览器,别占着同源连接(issue #104)。
|
|
7751
|
+
*/
|
|
7752
|
+
const BALANCE_RESPONSE_BUDGET_MS = 8e3;
|
|
7753
|
+
/**
|
|
7754
|
+
* 订阅 / 中转站额度路由的总等待预算(毫秒):与余额路由同口径(issue #104)。
|
|
7755
|
+
* 上游额度接口不可达时(8s 超时 × 重试 ≈ 10~16s),响应被长期挂住会占满浏览器
|
|
7756
|
+
* 的同源连接池——多标签页各挂一条即让宿主自己的资源请求排队,界面整体卡死。
|
|
7757
|
+
*/
|
|
7758
|
+
const QUOTA_RESPONSE_BUDGET_MS = 8e3;
|
|
7759
|
+
/**
|
|
7760
|
+
* 对账基线的最大可用年龄(毫秒):超过则「今日官方费用」偏小,用它对账会
|
|
7761
|
+
* 报假漂移——此时跳过本轮对账,等后台补算完成后再恢复(issue #104)。
|
|
7762
|
+
* 取 5 分钟:与 usage-stats 的 30s 轮询 + 5 分钟快照节流同一量级。
|
|
7763
|
+
*/
|
|
7764
|
+
const RECONCILE_BASIS_MAX_AGE_MS = 3e5;
|
|
7696
7765
|
/** DeepSeek 余额查询的默认凭据引用(与 llm-deepseek 的默认引用一致)。 */
|
|
7697
7766
|
const DEFAULT_BALANCE_API_KEY_ENV = "DEEPSEEK_API_KEY";
|
|
7698
7767
|
/**
|
|
@@ -7932,6 +8001,48 @@ function buildWorkspaceTitleResolver(ctx) {
|
|
|
7932
8001
|
};
|
|
7933
8002
|
}
|
|
7934
8003
|
/**
|
|
8004
|
+
* 构造「会话头 → 宿主侧会话标题」解析器(issue #103,可选依赖)。
|
|
8005
|
+
*
|
|
8006
|
+
* 插件的会话标题取自日志里的 `session/title` 事件——**日志读不动**时(宿主拒绝
|
|
8007
|
+
* 该格式/代际,插件折叠失败),只能吃账本旧行,标题会永远停在重命名前的值。
|
|
8008
|
+
* 宿主自己的会话投影缓存仍可能记得这个标题(`cachedSnapshot` / 既有能力
|
|
8009
|
+
* `cachedPredecessorTitle`:格式代际边缘的标题仍能跨边缘存活),用它兜底可让
|
|
8010
|
+
* 账单明细的标题跟随工作区。
|
|
8011
|
+
*
|
|
8012
|
+
* 宿主没有该服务(0.1.x 代际)或读取失败都返回 undefined:绝不抛错,行为与
|
|
8013
|
+
* 现状一致(可选依赖,不影响主流程)。
|
|
8014
|
+
* @param ctx - host context carrying the optional session projection cache.
|
|
8015
|
+
* @returns 标题解析函数;服务不可用时 undefined。
|
|
8016
|
+
*/
|
|
8017
|
+
function buildSessionTitleResolver(ctx) {
|
|
8018
|
+
let cache;
|
|
8019
|
+
try {
|
|
8020
|
+
cache = ctx.get("sessionProjectionCache");
|
|
8021
|
+
} catch {
|
|
8022
|
+
return;
|
|
8023
|
+
}
|
|
8024
|
+
if (cache === void 0) return void 0;
|
|
8025
|
+
const titleOf = (values) => {
|
|
8026
|
+
const title = values?.title;
|
|
8027
|
+
return typeof title === "string" && title !== "" ? title : void 0;
|
|
8028
|
+
};
|
|
8029
|
+
const cachedSnapshot = cache.cachedSnapshot;
|
|
8030
|
+
const cachedPredecessorTitle = cache.cachedPredecessorTitle;
|
|
8031
|
+
if (typeof cachedSnapshot !== "function" && typeof cachedPredecessorTitle !== "function") return void 0;
|
|
8032
|
+
return (header) => {
|
|
8033
|
+
try {
|
|
8034
|
+
if (typeof cachedSnapshot === "function") {
|
|
8035
|
+
const title = titleOf(cachedSnapshot(header, ["title"])?.values);
|
|
8036
|
+
if (title !== void 0) return title;
|
|
8037
|
+
}
|
|
8038
|
+
if (typeof cachedPredecessorTitle === "function") return titleOf(cachedPredecessorTitle(header)?.values);
|
|
8039
|
+
return;
|
|
8040
|
+
} catch {
|
|
8041
|
+
return;
|
|
8042
|
+
}
|
|
8043
|
+
};
|
|
8044
|
+
}
|
|
8045
|
+
/**
|
|
7935
8046
|
* 凭据 seam 的宽容解析:ref 名 → 非空值;失败/缺失返回 undefined(多 surface 共用,
|
|
7936
8047
|
* 失败按结构化静默记录)。
|
|
7937
8048
|
*/
|
|
@@ -8086,6 +8197,7 @@ function apply(ctx, config = {}) {
|
|
|
8086
8197
|
}).catch(() => {});
|
|
8087
8198
|
};
|
|
8088
8199
|
const workspaceTitleResolver = buildWorkspaceTitleResolver(ctx);
|
|
8200
|
+
const sessionTitleResolver = buildSessionTitleResolver(ctx);
|
|
8089
8201
|
applyBuiltinCatalog(BUILTIN_MODEL_CATALOG, BUILTIN_MODEL_KEY_ALIASES);
|
|
8090
8202
|
applyUserModelAliases(config.modelKeyAliases);
|
|
8091
8203
|
let llmRegistryWarned = false;
|
|
@@ -8119,6 +8231,7 @@ function apply(ctx, config = {}) {
|
|
|
8119
8231
|
resolveRegisteredProviders: registeredProvidersOf,
|
|
8120
8232
|
...config.modelKeyAliases === void 0 ? {} : { modelKeyAliases: config.modelKeyAliases },
|
|
8121
8233
|
...workspaceTitleResolver === void 0 ? {} : { resolveWorkspaceTitle: workspaceTitleResolver },
|
|
8234
|
+
...sessionTitleResolver === void 0 ? {} : { resolveSessionTitle: sessionTitleResolver },
|
|
8122
8235
|
...config.searchCallEstimateCny === void 0 ? {} : { searchCallEstimateCny: config.searchCallEstimateCny },
|
|
8123
8236
|
ledger: ledgerStore
|
|
8124
8237
|
});
|
|
@@ -8414,21 +8527,22 @@ function apply(ctx, config = {}) {
|
|
|
8414
8527
|
if (official !== void 0 && reconcilable) {
|
|
8415
8528
|
const now = Date.now();
|
|
8416
8529
|
const today = dayStamp(now);
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
todayOfficialCost =
|
|
8420
|
-
|
|
8421
|
-
|
|
8530
|
+
const recent = aggregator.lastDocOf();
|
|
8531
|
+
if (recent !== void 0 && now - recent.updatedAt <= RECONCILE_BASIS_MAX_AGE_MS) {
|
|
8532
|
+
const todayOfficialCost = recent?.byDay[today]?.officialCost ?? 0;
|
|
8533
|
+
const result = reconcileBalanceDelta(reconcileRef, official, todayOfficialCost, today, now);
|
|
8534
|
+
const refChanged = result.ref !== null && result.ref !== reconcileRef;
|
|
8535
|
+
reconcileRef = result.ref;
|
|
8536
|
+
if (result.event !== null && result.event.kind !== "flat") reconcile = {
|
|
8537
|
+
...result.event,
|
|
8538
|
+
provider: official.displayName,
|
|
8539
|
+
...official.currency === void 0 ? {} : { currency: official.currency }
|
|
8540
|
+
};
|
|
8541
|
+
if (refChanged) persistReconcileRef();
|
|
8542
|
+
} else {
|
|
8543
|
+
aggregator.aggregate().catch(() => {});
|
|
8544
|
+
swallowed("reconcile-basis-stale");
|
|
8422
8545
|
}
|
|
8423
|
-
const result = reconcileBalanceDelta(reconcileRef, official, todayOfficialCost, today, now);
|
|
8424
|
-
const refChanged = result.ref !== null && result.ref !== reconcileRef;
|
|
8425
|
-
reconcileRef = result.ref;
|
|
8426
|
-
if (result.event !== null && result.event.kind !== "flat") reconcile = {
|
|
8427
|
-
...result.event,
|
|
8428
|
-
provider: official.displayName,
|
|
8429
|
-
...official.currency === void 0 ? {} : { currency: official.currency }
|
|
8430
|
-
};
|
|
8431
|
-
if (refChanged) persistReconcileRef();
|
|
8432
8546
|
}
|
|
8433
8547
|
const doc = {
|
|
8434
8548
|
balances: [
|
|
@@ -8453,7 +8567,8 @@ function apply(ctx, config = {}) {
|
|
|
8453
8567
|
trustedHosts,
|
|
8454
8568
|
cached: () => balanceCache,
|
|
8455
8569
|
refreshOnce: refreshBalanceOnce,
|
|
8456
|
-
cacheMs: BALANCE_CACHE_MS
|
|
8570
|
+
cacheMs: BALANCE_CACHE_MS,
|
|
8571
|
+
responseBudgetMs: BALANCE_RESPONSE_BUDGET_MS
|
|
8457
8572
|
});
|
|
8458
8573
|
registerUsageToolRoute(ctx, {
|
|
8459
8574
|
trustedHosts,
|
|
@@ -8513,7 +8628,8 @@ function apply(ctx, config = {}) {
|
|
|
8513
8628
|
trustedHosts,
|
|
8514
8629
|
cached: () => quotaCache,
|
|
8515
8630
|
refreshOnce: refreshQuotasOnce,
|
|
8516
|
-
cacheMs: SUBSCRIPTION_CACHE_MS
|
|
8631
|
+
cacheMs: SUBSCRIPTION_CACHE_MS,
|
|
8632
|
+
responseBudgetMs: QUOTA_RESPONSE_BUDGET_MS
|
|
8517
8633
|
});
|
|
8518
8634
|
let relayCache = {
|
|
8519
8635
|
at: 0,
|
|
@@ -8552,7 +8668,8 @@ function apply(ctx, config = {}) {
|
|
|
8552
8668
|
trustedHosts,
|
|
8553
8669
|
cached: () => relayCache,
|
|
8554
8670
|
refreshOnce: refreshRelayOnce,
|
|
8555
|
-
cacheMs: SUBSCRIPTION_CACHE_MS
|
|
8671
|
+
cacheMs: SUBSCRIPTION_CACHE_MS,
|
|
8672
|
+
responseBudgetMs: QUOTA_RESPONSE_BUDGET_MS
|
|
8556
8673
|
});
|
|
8557
8674
|
registerUsageStatsRoute(ctx, {
|
|
8558
8675
|
trustedHosts,
|
|
@@ -8562,7 +8679,8 @@ function apply(ctx, config = {}) {
|
|
|
8562
8679
|
persistSnapshot,
|
|
8563
8680
|
readSnapshot,
|
|
8564
8681
|
packageVersion: PACKAGE_VERSION,
|
|
8565
|
-
responseBudgetMs: RESPONSE_BUDGET_MS
|
|
8682
|
+
responseBudgetMs: RESPONSE_BUDGET_MS,
|
|
8683
|
+
lastDoc: () => aggregator.lastDocOf()
|
|
8566
8684
|
});
|
|
8567
8685
|
}
|
|
8568
8686
|
//#endregion
|
package/lib/types/aggregate.d.ts
CHANGED
|
@@ -164,6 +164,15 @@ export interface AggregateOptions {
|
|
|
164
164
|
/** 工作区标题解析:给定会话 cwd 返回项目显示标题;undefined = 回退到 cwd 末级目录名
|
|
165
165
|
* (host 的 workspaceRegistry 为可选依赖,缺失时不注入,行为保持不变)。 */
|
|
166
166
|
resolveWorkspaceTitle?: (cwd: string) => string | undefined;
|
|
167
|
+
/**
|
|
168
|
+
* 会话标题兜底解析:给定会话头(宿主持久层列表元数据)返回宿主侧的会话标题。
|
|
169
|
+
*
|
|
170
|
+
* 只在**日志读不动**的会话上用(宿主拒绝该格式/代际,插件折叠失败):此时插件
|
|
171
|
+
* 只能吃账本里的旧行,标题会永远停在重命名前的值——宿主自己的投影缓存仍可能
|
|
172
|
+
* 记得这个标题(格式代际边缘的既定能力),用它兜底可让标题跟随工作区
|
|
173
|
+
* (issue #103)。读得动的会话一律以日志为准,本回调不参与。
|
|
174
|
+
*/
|
|
175
|
+
resolveSessionTitle?: (header: SessionHeader) => string | undefined;
|
|
167
176
|
/** 独立的持久用量账本。启用后,已经成功折叠过的会话即使随后从
|
|
168
177
|
* sessionPersistence 中永久删除,也会继续计入累计用量。 */
|
|
169
178
|
ledger?: UsageLedgerStore;
|
|
@@ -735,6 +744,16 @@ export interface UsageAggregator {
|
|
|
735
744
|
* @returns 会话累计;日志与账本均无该会话时 undefined(调用方自行兜底)。
|
|
736
745
|
*/
|
|
737
746
|
sessionUsageOf(sessionId: string): Promise<SessionUsageSummary | undefined>;
|
|
747
|
+
/**
|
|
748
|
+
* 最近一次**已完成**的聚合文档(不触发新的折叠)。
|
|
749
|
+
*
|
|
750
|
+
* 供路由的 stale-while-revalidate 兜底用:当本轮折叠超过响应预算(宿主单进程
|
|
751
|
+
* 事件循环被重活折叠占住)时,落盘快照最多落后一个节流窗口(分钟级),而这里
|
|
752
|
+
* 的内存文档只落后一个聚合周期——两者都是「上次算好的」,但内存那份更新。
|
|
753
|
+
* 从未完成过一次聚合时返回 undefined(调用方回落到磁盘快照)。
|
|
754
|
+
* @returns 最近一次完成的聚合文档;尚无完成聚合时 undefined。
|
|
755
|
+
*/
|
|
756
|
+
lastDocOf(): UsageStatsDocument | undefined;
|
|
738
757
|
/** 立刻落盘未保存的账本改动(无视节流),供插件卸载时调用;
|
|
739
758
|
* 进行中的聚合先等完再存,折叠失败仍保存已成功部分。 */
|
|
740
759
|
flush(): Promise<void>;
|
|
@@ -5,16 +5,20 @@
|
|
|
5
5
|
import type { ProviderBalance, ReconcileNotice } from '../../pricing-shared.ts';
|
|
6
6
|
/** Path to the account-balance endpoint served by this plugin's node half. */
|
|
7
7
|
export declare const BALANCE_PATH = "/api/billing/balance";
|
|
8
|
+
/** 余额文档(服务端响应体的白名单切片)。 */
|
|
9
|
+
export interface BalanceDocSlice {
|
|
10
|
+
balances: readonly ProviderBalance[];
|
|
11
|
+
reconcile?: ReconcileNotice;
|
|
12
|
+
/** 服务端刷新超预算:本次余额来自上一次成功的查询(issue #104)。 */
|
|
13
|
+
stale?: boolean;
|
|
14
|
+
}
|
|
8
15
|
/**
|
|
9
16
|
* 一次拉取 `/api/billing/balance` 的完整响应(余额行 + 对账提示)。余额与对账
|
|
10
17
|
* 提示来自同一响应体,拆成两个函数会导致每 30 秒对同一端点发两次请求,故合并
|
|
11
18
|
* 为单次 fetch;失败返回空值(余额 []、对账 undefined),由调用方降级。
|
|
12
19
|
* @returns the balances and reconcile notice (both degraded on any failure).
|
|
13
20
|
*/
|
|
14
|
-
export declare function fetchBalanceDoc(): Promise<
|
|
15
|
-
balances: readonly ProviderBalance[];
|
|
16
|
-
reconcile?: ReconcileNotice;
|
|
17
|
-
}>;
|
|
21
|
+
export declare function fetchBalanceDoc(): Promise<BalanceDocSlice>;
|
|
18
22
|
/**
|
|
19
23
|
* 拉取官方余额差对账提示(drift 时非空),供余额面板展示;失败返回 undefined。
|
|
20
24
|
* 复用 {@link fetchBalanceDoc} 的同一响应,导出供对账提示渲染测试单独解析。
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 插件自有端点(/api/billing/*)的统一取数封装:超时底座。
|
|
3
|
+
*
|
|
4
|
+
* 背景(issue #104):浏览器对同源最多 6 条并发连接,而旧实现里所有 fetch
|
|
5
|
+
* 都没有超时——服务端某次响应被上游查询拖住(报告者实测 45~60s)时,这条
|
|
6
|
+
* 连接会被长期占住;多标签页各挂一条即占满连接池,宿主自己的资源请求排队,
|
|
7
|
+
* 用户看到的是「整个 DSH 界面卡死」。
|
|
8
|
+
*
|
|
9
|
+
* 超时上限必须小于轮询间隔(30s),否则「超时中的请求」会与下一轮轮询重叠,
|
|
10
|
+
* 在途请求越堆越多。
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* 客户端取插件端点的默认超时(毫秒):小于 30s 轮询间隔,保证不与下一轮重叠。
|
|
14
|
+
*
|
|
15
|
+
* 取 12s:服务端三条轮询路由的总预算都是 8s(余额 / 订阅额度 / 中转站额度),
|
|
16
|
+
* usage-stats 是 1.5s——12s 给服务端留了 4s 余量,又远小于 30s 的轮询间隔,
|
|
17
|
+
* 超时的请求不会与下一轮重叠(issue #104 报告者建议值同为 12s)。
|
|
18
|
+
*/
|
|
19
|
+
export declare const BILLING_FETCH_TIMEOUT_MS = 12000;
|
|
20
|
+
/** 取数选项:在标准 RequestInit 之上加一个超时覆盖。 */
|
|
21
|
+
export interface BillingFetchInit extends RequestInit {
|
|
22
|
+
/** 超时(毫秒);缺省 {@link BILLING_FETCH_TIMEOUT_MS}。 */
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 带超时的 fetch(插件端点专用)。
|
|
27
|
+
*
|
|
28
|
+
* 超时以 AbortController 实现(不用 AbortSignal.timeout):宿主 UI 跑在
|
|
29
|
+
* 浏览器里,但插件的客户端代码也会被测试与宿主内嵌环境加载,显式
|
|
30
|
+
* AbortController 在任何实现上都可用。
|
|
31
|
+
* @param path - 端点路径(相对当前源)。
|
|
32
|
+
* @param init - 请求选项;`timeoutMs` 覆盖默认超时,`signal` 透传外部取消。
|
|
33
|
+
* @returns the response; rejects when the request times out or fails.
|
|
34
|
+
*/
|
|
35
|
+
export declare function fetchBilling(path: string, init?: BillingFetchInit): Promise<Response>;
|
|
36
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -5,9 +5,15 @@
|
|
|
5
5
|
import type { SubscriptionQuota } from '../../pricing-shared.ts';
|
|
6
6
|
/** Path to the subscription-plan quota endpoint served by this plugin's node half. */
|
|
7
7
|
export declare const SUBSCRIPTIONS_PATH = "/api/billing/subscriptions";
|
|
8
|
+
/** 订阅额度响应(白名单切片)。 */
|
|
9
|
+
export interface SubscriptionsDoc {
|
|
10
|
+
quotas: readonly SubscriptionQuota[];
|
|
11
|
+
/** 服务端刷新超预算:额度来自上一次成功的查询(issue #104)。 */
|
|
12
|
+
stale?: boolean;
|
|
13
|
+
}
|
|
8
14
|
/**
|
|
9
15
|
* 拉取订阅套餐剩余额度(供订阅面板);失败返回空列表。
|
|
10
16
|
* @returns the quota rows, or an empty list on any failure.
|
|
11
17
|
*/
|
|
12
|
-
export declare function fetchSubscriptions(): Promise<
|
|
18
|
+
export declare function fetchSubscriptions(): Promise<SubscriptionsDoc>;
|
|
13
19
|
//# sourceMappingURL=subscriptions.d.ts.map
|
|
@@ -166,6 +166,12 @@ export interface UsageBillingInjected {
|
|
|
166
166
|
export interface UsageStats {
|
|
167
167
|
/** 服务端聚合时间戳(毫秒);旧快照可能缺失。 */
|
|
168
168
|
updatedAt?: number;
|
|
169
|
+
/**
|
|
170
|
+
* 本次下发的是「上次算好的」文档(聚合超响应预算或失败时的回落):
|
|
171
|
+
* 内存里最近一次完成的聚合,或落盘快照。面板据此提示数据滞后,
|
|
172
|
+
* 避免把旧值(如重命名前的会话标题)当成最新值读。
|
|
173
|
+
*/
|
|
174
|
+
staleSnapshot?: boolean;
|
|
169
175
|
/** 宿主进程时区(IANA 名 + UTC 偏移):天按此切分,副标题据此标注;旧快照可能缺失。 */
|
|
170
176
|
timezone?: {
|
|
171
177
|
name: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Locale dictionaries for the usage billing surface. */
|
|
2
|
-
export type UsageBillingKey = 'title' | 'cost' | 'todayCost' | 'monthCost' | 'yearCost' | 'monthProjected' | 'liveTurn' | 'liveSession' | 'liveSessionSub' | 'calls' | 'cacheHitRate' | 'tokens' | 'inputTokens' | 'outputTokens' | 'avgCost' | 'kpiRange' | 'avgRangeToday' | 'avgRange7d' | 'avgRangeWeek' | 'avgRangeMonth' | 'avgRangeAll' | 'trend' | 'trend7d' | 'trend30d' | 'trendMetric' | 'trendMetricCost' | 'trendMetricTokens' | 'pricingSearch' | 'bandPinHint' | 'trendEmpty' | 'trendTotal' | 'roundsUnit' | 'healthModelsLive' | 'healthVendorsDown' | 'healthVendorsUnavailable' | 'budget' | 'budgetRemain' | 'budgetAmount' | 'budgetSummary' | 'sessions' | 'sessionTitle' | 'lastActive' | 'sessionOverflow' | 'budgetTierBody' | 'models' | 'providerBilling' | 'channelUnknown' | 'directTag' | 'relayTag' | 'unknownTag' | 'unknownChannelHint' | 'planCountUnit' | 'actual' | 'pricing' | 'input' | 'output' | 'cacheHit' | 'peak' | 'offPeak' | 'flat' | 'band' | 'close' | 'footer' | 'footerCredit' | 'lastUpdated' | 'noData' | 'todayRate' | 'rateLive' | 'rateBuiltin' | 'pricingSyncedAt' | 'pricingSyncNow' | 'pricingSyncing' | 'promoBadge' | 'promoUntil' | 'promoOpenEnded' | 'pricingTip' | 'pricingUnit' | 'pricingNotes' | 'ubPeak' | 'ubOff' | 'ubStd' | 'peakBand' | 'pricingSource' | 'noteCache' | 'noteBand' | 'noteSource' | 'balance' | 'recharge' | 'rechargeHint' | 'balanceUnconfigured' | 'balanceUnauthorized' | 'balanceUnreachable' | 'uncatalogued' | 'estimatedPricing' | 'balanceDays' | 'balanceLowBody' | 'reconcileDrift' | 'reconcileDismiss' | 'subscriptionNotConfigured' | 'subscriptionUnauthorized' | 'subscriptionUnavailable' | 'subscriptionInvalid' | 'subscriptionRateLimited' | 'subscriptionSession' | 'subscriptionWeekly' | 'subscriptionMonthly' | 'subscriptionBilling' | 'subscriptionRemaining' | 'subscriptionExhausted' | 'subscriptionReset' | 'subscriptionNoApi' | 'floatWindow' | 'floatModeCombined' | 'floatModeSubscription' | 'floatMode' | 'floatTargets' | 'floatWindowHint' | 'floatPrimaryToday' | 'floatPrimaryWeek' | 'floatPrimaryMonth' | 'floatNoTargets' | 'floatNoTargetsHint' | 'cardDisplay' | 'cardDisplayHint' | 'cardMetric' | 'cardMetricMoney' | 'cardMetricTokens' | 'cardSpan' | 'cardSpanDay' | 'cardSpanWeek' | 'cardSpanMonth' | 'cardDeltaBadge' | 'cardDeltaShow' | 'cardDeltaHide' | 'triggerMonthTokens' | 'triggerTodayTokens' | 'triggerWeekTokens' | 'subscriptionsStale' | 'staleLedgerNotice' | 'unreadableNotice' | 'unreadableFormatNotice' | 'unreadableNoLedgerNotice' | 'tokenCacheWrite' | 'toolRank' | 'toolName' | 'userPrices' | 'userPricesHint' | 'userPriceSave' | 'userPriceModel' | 'userPriceSource' | 'userPriceSourceHint' | 'userPriceOffPeak' | 'userPriceNormal' | 'userPriceCurrency' | 'userPriceAdd' | 'userPriceRemoveSelected' | 'userPriceSelect' | 'sessionStaleBadge' | 'sessionUntitled' | 'sessionSubCost' | 'sessionSubCalls' | 'sessionSubRow' | 'heatmapLess' | 'heatmapMore' | 'currency' | 'currencyCny' | 'currencyUsd' | 'currencyEur' | 'language' | 'heatmap' | 'rounds' | 'roundsHint' | 'anomaly' | 'model' | 'thModel' | 'thInputMiss' | 'thInputHit' | 'costAbbr' | 'tabOverview' | 'tabTrends' | 'tabProviders' | 'tabPricing' | 'tabSettings' | 'budgetHint' | 'peakAlertHint' | 'peakAlertDescPeak' | 'peakAlertDescOff' | 'peakAlertDescPeakZhipu' | 'peakAlertDescOffZhipu' | 'export' | 'exportCsvDay' | 'exportCsvSession' | 'exportJson' | 'peakShare' | 'peakSharePerCall' | 'offPeakSavings' | 'perfMax' | 'weekCost' | 'roleCost' | 'roleUser' | 'roleAssistant' | 'roleTool' | 'roleHint' | 'tierPeak' | 'tierOff' | 'tierToPeak' | 'tierToOff' | 'tierAlertEnterPeak' | 'tierAlertEnterOff' | 'tierAlertEnterPeakZhipu' | 'tierAlertEnterOffZhipu' | 'peakAlertTitlePeak' | 'peakAlertTitleOff' | 'peakAlert' | 'peakAlertLeadMin' | 'peakAlertPos' | 'peakAlertMode' | 'peakAlertPosCorner' | 'peakAlertPosCenter' | 'peakAlertModePeak' | 'peakAlertModeOff' | 'peakAlertModeBoth' | 'peakAlertWebNotify' | 'peakAlertPreview' | 'planTypeCode' | 'planTypeToken' | 'subscriptionFeePerMonth' | 'triggerToday' | 'triggerMonth' | 'subscriptionTag' | 'cellEstimateHint' | 'free' | 'perfSamples' | 'perfTtft' | 'perfP50' | 'perfP90' | 'perfTps' | 'perfLatency' | 'perfEstimated' | 'perfEmpty' | 'perfTpsUnit' | 'perfTitle' | 'perfHint' | 'perfAll' | 'perfChartEmpty' | 'heatmapYear' | 'heatmapHalf' | 'heatmapMonth' | 'metricCost' | 'metricTokens' | 'heatmapTotal' | 'activeDays' | 'streakDays' | 'subscriptionAutoDetect' | 'pluginAuthor' | 'pluginRepository' | 'pluginNpm' | 'pluginLicense' | 'tabToken' | 'tokenExport' | 'tokenExportCsv' | 'tokenReasoningShare' | 'tokenReasoningShort' | 'tokenIo' | 'tokenPeak' | 'densityLabel' | 'densityHint' | 'densityCompact' | 'densityDefault' | 'densityComfortable' | 'motionLabel' | 'motionHint' | 'motionOff' | 'motionSubtle' | 'motionRich' | 'settingsNavDisplayPref' | 'settingsNavBudget' | 'settingsNavAlert' | 'settingsNavDisplay' | 'settingsNavAdvanced' | 'settingsNavAbout' | 'settingsSearchPlaceholder' | 'settingsSearchEmpty' | 'sessionsViewAll' | 'sessionViewerSearch' | 'modelBudgetTitle' | 'modelBudgetHint' | 'modelBudgetBody' | 'modelBudgetPlaceholder' | 'moreWorksTitle' | 'adEngramName' | 'adEngramDesc' | 'adRainbowName' | 'adRainbowDesc' | 'kpiDescCacheHit' | 'kpiDescTokens' | 'kpiDescAvgCost' | 'kpiDescCalls' | 'kpiDescReasoning' | 'kpiDescIo' | 'kpiDescPeak' | 'healthDotLive' | 'healthDotFailed' | 'healthDotUnknown' | 'settingsSectionDisplay' | 'emptyHint' | 'tokenDaily' | 'tokenByModel' | 'tokenMiss' | 'tokenHit' | 'tokenOutput' | 'tokenViewStructure' | 'tokenViewModel' | 'tokenHitShort' | 'tokenMissShort' | 'tokenTotal' | 'tokenShare' | 'usageStatsTool' | 'usageStatsToolHint' | 'balanceGranted' | 'balanceTopped' | 'balanceDaily' | 'balanceDaysLong' | 'balanceDaysUnit' | 'popTodayModel' | 'popNoConsumption' | 'popTitle' | 'popDirectLead' | 'popSubLead' | 'unpricedHint' | 'searchEstimateHint' | 'siteListDisplay' | 'siteListDisplayHint' | 'liveCostBar' | 'liveCostBarHint' | 'liveCostBarPosition' | 'liveCostBarPosBelow' | 'liveCostBarPosAbove' | 'liveCostBarPosToolbar' | 'exportCsvSite' | 'panelRelayQuota' | 'relayBalance' | 'relayNoQuota' | 'relayWindowUsed' | 'relayKindNewApi' | 'relayKindSub2Api' | 'relayKindUnknown' | 'expandAll' | 'collapseAll' | 'todayOnly' | 'todayNoConsumption';
|
|
2
|
+
export type UsageBillingKey = 'title' | 'cost' | 'todayCost' | 'monthCost' | 'yearCost' | 'monthProjected' | 'liveTurn' | 'liveSession' | 'liveSessionSub' | 'calls' | 'cacheHitRate' | 'tokens' | 'inputTokens' | 'outputTokens' | 'avgCost' | 'kpiRange' | 'avgRangeToday' | 'avgRange7d' | 'avgRangeWeek' | 'avgRangeMonth' | 'avgRangeAll' | 'trend' | 'trend7d' | 'trend30d' | 'trendMetric' | 'trendMetricCost' | 'trendMetricTokens' | 'pricingSearch' | 'bandPinHint' | 'trendEmpty' | 'trendTotal' | 'roundsUnit' | 'healthModelsLive' | 'healthVendorsDown' | 'healthVendorsUnavailable' | 'budget' | 'budgetRemain' | 'budgetAmount' | 'budgetSummary' | 'sessions' | 'sessionTitle' | 'lastActive' | 'sessionOverflow' | 'budgetTierBody' | 'models' | 'providerBilling' | 'channelUnknown' | 'directTag' | 'relayTag' | 'unknownTag' | 'unknownChannelHint' | 'planCountUnit' | 'actual' | 'pricing' | 'input' | 'output' | 'cacheHit' | 'peak' | 'offPeak' | 'flat' | 'band' | 'close' | 'footer' | 'footerCredit' | 'lastUpdated' | 'noData' | 'todayRate' | 'rateLive' | 'rateBuiltin' | 'pricingSyncedAt' | 'pricingSyncNow' | 'pricingSyncing' | 'promoBadge' | 'promoUntil' | 'promoOpenEnded' | 'pricingTip' | 'pricingUnit' | 'pricingNotes' | 'ubPeak' | 'ubOff' | 'ubStd' | 'peakBand' | 'pricingSource' | 'noteCache' | 'noteBand' | 'noteSource' | 'balance' | 'recharge' | 'rechargeHint' | 'balanceUnconfigured' | 'balanceUnauthorized' | 'balanceUnreachable' | 'uncatalogued' | 'estimatedPricing' | 'balanceDays' | 'balanceLowBody' | 'reconcileDrift' | 'reconcileDismiss' | 'subscriptionNotConfigured' | 'subscriptionUnauthorized' | 'subscriptionUnavailable' | 'subscriptionInvalid' | 'subscriptionRateLimited' | 'subscriptionSession' | 'subscriptionWeekly' | 'subscriptionMonthly' | 'subscriptionBilling' | 'subscriptionRemaining' | 'subscriptionExhausted' | 'subscriptionReset' | 'subscriptionNoApi' | 'floatWindow' | 'floatModeCombined' | 'floatModeSubscription' | 'floatMode' | 'floatTargets' | 'floatWindowHint' | 'floatPrimaryToday' | 'floatPrimaryWeek' | 'floatPrimaryMonth' | 'floatNoTargets' | 'floatNoTargetsHint' | 'cardDisplay' | 'cardDisplayHint' | 'cardMetric' | 'cardMetricMoney' | 'cardMetricTokens' | 'cardSpan' | 'cardSpanDay' | 'cardSpanWeek' | 'cardSpanMonth' | 'cardDeltaBadge' | 'cardDeltaShow' | 'cardDeltaHide' | 'triggerMonthTokens' | 'triggerTodayTokens' | 'triggerWeekTokens' | 'subscriptionsStale' | 'balanceStaleNotice' | 'staleLedgerNotice' | 'staleSnapshotNotice' | 'unreadableNotice' | 'unreadableFormatNotice' | 'unreadableNoLedgerNotice' | 'tokenCacheWrite' | 'toolRank' | 'toolName' | 'userPrices' | 'userPricesHint' | 'userPriceSave' | 'userPriceModel' | 'userPriceSource' | 'userPriceSourceHint' | 'userPriceOffPeak' | 'userPriceNormal' | 'userPriceCurrency' | 'userPriceAdd' | 'userPriceRemoveSelected' | 'userPriceSelect' | 'sessionStaleBadge' | 'sessionUntitled' | 'sessionSubCost' | 'sessionSubCalls' | 'sessionSubRow' | 'heatmapLess' | 'heatmapMore' | 'currency' | 'currencyCny' | 'currencyUsd' | 'currencyEur' | 'language' | 'heatmap' | 'rounds' | 'roundsHint' | 'anomaly' | 'model' | 'thModel' | 'thInputMiss' | 'thInputHit' | 'costAbbr' | 'tabOverview' | 'tabTrends' | 'tabProviders' | 'tabPricing' | 'tabSettings' | 'budgetHint' | 'peakAlertHint' | 'peakAlertDescPeak' | 'peakAlertDescOff' | 'peakAlertDescPeakZhipu' | 'peakAlertDescOffZhipu' | 'export' | 'exportCsvDay' | 'exportCsvSession' | 'exportJson' | 'peakShare' | 'peakSharePerCall' | 'offPeakSavings' | 'perfMax' | 'weekCost' | 'roleCost' | 'roleUser' | 'roleAssistant' | 'roleTool' | 'roleHint' | 'tierPeak' | 'tierOff' | 'tierToPeak' | 'tierToOff' | 'tierAlertEnterPeak' | 'tierAlertEnterOff' | 'tierAlertEnterPeakZhipu' | 'tierAlertEnterOffZhipu' | 'peakAlertTitlePeak' | 'peakAlertTitleOff' | 'peakAlert' | 'peakAlertLeadMin' | 'peakAlertPos' | 'peakAlertMode' | 'peakAlertPosCorner' | 'peakAlertPosCenter' | 'peakAlertModePeak' | 'peakAlertModeOff' | 'peakAlertModeBoth' | 'peakAlertWebNotify' | 'peakAlertPreview' | 'planTypeCode' | 'planTypeToken' | 'subscriptionFeePerMonth' | 'triggerToday' | 'triggerMonth' | 'subscriptionTag' | 'cellEstimateHint' | 'free' | 'perfSamples' | 'perfTtft' | 'perfP50' | 'perfP90' | 'perfTps' | 'perfLatency' | 'perfEstimated' | 'perfEmpty' | 'perfTpsUnit' | 'perfTitle' | 'perfHint' | 'perfAll' | 'perfChartEmpty' | 'heatmapYear' | 'heatmapHalf' | 'heatmapMonth' | 'metricCost' | 'metricTokens' | 'heatmapTotal' | 'activeDays' | 'streakDays' | 'subscriptionAutoDetect' | 'pluginAuthor' | 'pluginRepository' | 'pluginNpm' | 'pluginLicense' | 'tabToken' | 'tokenExport' | 'tokenExportCsv' | 'tokenReasoningShare' | 'tokenReasoningShort' | 'tokenIo' | 'tokenPeak' | 'densityLabel' | 'densityHint' | 'densityCompact' | 'densityDefault' | 'densityComfortable' | 'motionLabel' | 'motionHint' | 'motionOff' | 'motionSubtle' | 'motionRich' | 'settingsNavDisplayPref' | 'settingsNavBudget' | 'settingsNavAlert' | 'settingsNavDisplay' | 'settingsNavAdvanced' | 'settingsNavAbout' | 'settingsSearchPlaceholder' | 'settingsSearchEmpty' | 'sessionsViewAll' | 'sessionViewerSearch' | 'modelBudgetTitle' | 'modelBudgetHint' | 'modelBudgetBody' | 'modelBudgetPlaceholder' | 'moreWorksTitle' | 'adEngramName' | 'adEngramDesc' | 'adRainbowName' | 'adRainbowDesc' | 'kpiDescCacheHit' | 'kpiDescTokens' | 'kpiDescAvgCost' | 'kpiDescCalls' | 'kpiDescReasoning' | 'kpiDescIo' | 'kpiDescPeak' | 'healthDotLive' | 'healthDotFailed' | 'healthDotUnknown' | 'settingsSectionDisplay' | 'emptyHint' | 'tokenDaily' | 'tokenByModel' | 'tokenMiss' | 'tokenHit' | 'tokenOutput' | 'tokenViewStructure' | 'tokenViewModel' | 'tokenHitShort' | 'tokenMissShort' | 'tokenTotal' | 'tokenShare' | 'usageStatsTool' | 'usageStatsToolHint' | 'balanceGranted' | 'balanceTopped' | 'balanceDaily' | 'balanceDaysLong' | 'balanceDaysUnit' | 'popTodayModel' | 'popNoConsumption' | 'popTitle' | 'popDirectLead' | 'popSubLead' | 'unpricedHint' | 'searchEstimateHint' | 'siteListDisplay' | 'siteListDisplayHint' | 'liveCostBar' | 'liveCostBarHint' | 'liveCostBarPosition' | 'liveCostBarPosBelow' | 'liveCostBarPosAbove' | 'liveCostBarPosToolbar' | 'exportCsvSite' | 'panelRelayQuota' | 'relayBalance' | 'relayNoQuota' | 'relayWindowUsed' | 'relayKindNewApi' | 'relayKindSub2Api' | 'relayKindUnknown' | 'expandAll' | 'collapseAll' | 'todayOnly' | 'todayNoConsumption';
|
|
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>;
|
|
@@ -78,6 +78,12 @@ export interface BalanceResponse {
|
|
|
78
78
|
balances: readonly ProviderBalance[];
|
|
79
79
|
/** 官方余额差对账提示(对账开启且当日有余额消费时下发)。 */
|
|
80
80
|
reconcile?: ReconcileNotice;
|
|
81
|
+
/**
|
|
82
|
+
* 本次余额来自上一次成功的查询(服务端刷新超预算,issue #104):
|
|
83
|
+
* 上游查询被拖住时不再把响应长期挂起,改为回旧值并标记,客户端据此提示
|
|
84
|
+
* 「余额数据来自上次查询」。
|
|
85
|
+
*/
|
|
86
|
+
stale?: boolean;
|
|
81
87
|
}
|
|
82
88
|
/** 官方余额差对账提示(drift 时携带供 UI 展示的已用/本地费用)。 */
|
|
83
89
|
export interface ReconcileNotice {
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* TokenHub 等)+ 用户自定义余额 + 声明端点 + 余额差对账。5 分钟缓存(避免
|
|
6
6
|
* 每 30s 后台轮询打上游),in-flight 去重(并发请求共享同一次进行中的刷新)。
|
|
7
7
|
*
|
|
8
|
-
* 路由文件只关心 HTTP 缓存逻辑:5 分钟过期判定 +
|
|
9
|
-
*
|
|
8
|
+
* 路由文件只关心 HTTP 缓存逻辑:5 分钟过期判定 + 刷新预算(超预算回旧
|
|
9
|
+
* 缓存并标 stale,不把响应挂在长查询上)+ 失败时仍发旧缓存(不挂起已
|
|
10
|
+
* writeHead 的响应)。上游查询(queryBalances / queryCustomBalances /
|
|
10
11
|
* queryDeclaredEndpoints / reconcileBalanceDelta)在装配器里跑,路由只通过
|
|
11
12
|
* `cached()` 与 `refreshOnce()` 拿到结果。
|
|
12
13
|
*/
|
|
@@ -17,6 +18,11 @@ import type { ReconcileEvent } from '../reconcile.ts';
|
|
|
17
18
|
export interface BalanceDoc {
|
|
18
19
|
balances: readonly ProviderBalance[];
|
|
19
20
|
reconcile?: ReconcileEvent;
|
|
21
|
+
/**
|
|
22
|
+
* 本次余额来自上一次成功的查询(刷新超预算,见 {@link BalanceDeps.responseBudgetMs})。
|
|
23
|
+
* 客户端据此提示「余额数据来自上次查询」,别把旧值当最新读。
|
|
24
|
+
*/
|
|
25
|
+
stale?: boolean;
|
|
20
26
|
}
|
|
21
27
|
interface CachedBalance {
|
|
22
28
|
doc: BalanceDoc;
|
|
@@ -31,6 +37,16 @@ export interface BalanceDeps {
|
|
|
31
37
|
refreshOnce: () => Promise<void>;
|
|
32
38
|
/** 5 分钟缓存间隔(毫秒)。 */
|
|
33
39
|
cacheMs: number;
|
|
40
|
+
/**
|
|
41
|
+
* 刷新的总等待预算(毫秒):超预算立刻回上一次成功的余额。
|
|
42
|
+
*
|
|
43
|
+
* 存在的理由(issue #104):浏览器对同源最多 6 条并发连接,而余额响应被
|
|
44
|
+
* 上游查询(最坏 8s 超时 × 重试)拖住时,这条连接会被长期占住;多标签页
|
|
45
|
+
* 各挂一条即占满连接池,宿主自己的资源请求排队——用户看到的是「整个 DSH
|
|
46
|
+
* 界面卡死」。预算到期后刷新在后台继续(in-flight 去重共享同一次刷新),
|
|
47
|
+
* 下一轮轮询即可拿到新值。
|
|
48
|
+
*/
|
|
49
|
+
responseBudgetMs: number;
|
|
34
50
|
}
|
|
35
51
|
export declare function registerBalanceRoute(ctx: Context, deps: BalanceDeps): void;
|
|
36
52
|
export {};
|
|
@@ -21,6 +21,14 @@ export interface RelayQuotasDeps {
|
|
|
21
21
|
refreshOnce: () => Promise<void>;
|
|
22
22
|
/** 5 分钟缓存间隔(毫秒)。 */
|
|
23
23
|
cacheMs: number;
|
|
24
|
+
/**
|
|
25
|
+
* 刷新的总等待预算(毫秒):超预算立刻回上一次成功的额度(stale 标记)。
|
|
26
|
+
*
|
|
27
|
+
* 与 balance 路由同因(issue #104):中转站不可达时(8s 超时 × 重试)响应
|
|
28
|
+
* 会被长期挂住,占着浏览器的同源连接(每源 6 条)——多标签页各挂一条即让
|
|
29
|
+
* 宿主自己的资源请求排队,界面整体卡死。
|
|
30
|
+
*/
|
|
31
|
+
responseBudgetMs: number;
|
|
24
32
|
}
|
|
25
33
|
export declare function registerRelayQuotasRoute(ctx: Context, deps: RelayQuotasDeps): void;
|
|
26
34
|
export {};
|
|
@@ -22,6 +22,14 @@ export interface SubscriptionsDeps {
|
|
|
22
22
|
refreshOnce: () => Promise<void>;
|
|
23
23
|
/** 5 分钟缓存间隔(毫秒)。 */
|
|
24
24
|
cacheMs: number;
|
|
25
|
+
/**
|
|
26
|
+
* 刷新的总等待预算(毫秒):超预算立刻回上一次成功的额度(stale 标记)。
|
|
27
|
+
*
|
|
28
|
+
* 与 balance 路由同因(issue #104):上游额度接口不可达时(8s 超时 × 重试)
|
|
29
|
+
* 响应会被长期挂住,占着浏览器的同源连接(每源 6 条)——多标签页各挂一条
|
|
30
|
+
* 即让宿主自己的资源请求排队,界面整体卡死。
|
|
31
|
+
*/
|
|
32
|
+
responseBudgetMs: number;
|
|
25
33
|
}
|
|
26
34
|
export declare function registerSubscriptionsRoute(ctx: Context, deps: SubscriptionsDeps): void;
|
|
27
35
|
export {};
|
|
@@ -37,6 +37,13 @@ export interface UsageStatsDeps {
|
|
|
37
37
|
packageVersion: string;
|
|
38
38
|
/** 等待预算(毫秒):聚合在此预算内完成就回实时数据,超预算立即回最近快照。 */
|
|
39
39
|
responseBudgetMs: number;
|
|
40
|
+
/**
|
|
41
|
+
* 最近一次**已完成**的聚合文档(只读,不触发新折叠)。
|
|
42
|
+
*
|
|
43
|
+
* 超预算时的第一回落:它比磁盘快照新(磁盘快照受落盘节流影响,最多落后数分钟;
|
|
44
|
+
* 内存文档只落后一个聚合周期)。从未完成过一次聚合时 undefined,继续回落磁盘快照。
|
|
45
|
+
*/
|
|
46
|
+
lastDoc?: () => Record<string, unknown> | undefined;
|
|
40
47
|
}
|
|
41
48
|
/** 校验一份聚合文档;返回漂移原因(null = 形状正常)。纯只读,不改 doc。 */
|
|
42
49
|
export declare function usageStatsShapeError(doc: unknown): string | null;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kenz1117/dsh-ui-usage-billing",
|
|
3
3
|
"description": "Usage billing dashboard for DeepSeek Harness: sidebar cost metrics plus a full dashboard modal, priced from a current multi-provider catalog with real usage aggregated from session logs.",
|
|
4
|
-
"version": "1.6.
|
|
4
|
+
"version": "1.6.8",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
7
7
|
"deepseek",
|