@kenz1117/dsh-ui-usage-billing 1.1.3 → 1.1.5
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 +2 -2
- package/lib/index.js +311 -34
- package/lib/types/aggregate.d.ts +10 -3
- package/package.json +35 -20
package/lib/index.js
CHANGED
|
@@ -6,6 +6,7 @@ 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";
|
|
8
8
|
import z from "@deepseek-ai/schemastery";
|
|
9
|
+
import { createHash, createHmac } from "node:crypto";
|
|
9
10
|
//#region lib/types/client/plan-knowledge.js
|
|
10
11
|
/**
|
|
11
12
|
* Plan-knowledge reference (adapted from dsh-spend's `knowledge.js`, MIT):
|
|
@@ -266,7 +267,7 @@ function tierAt(timeMs) {
|
|
|
266
267
|
}
|
|
267
268
|
/** 时刻是否落在北京时间周末(周六/周日)。 */
|
|
268
269
|
function isBeijingWeekend(timeMs) {
|
|
269
|
-
const day = new Date(timeMs +
|
|
270
|
+
const day = new Date(timeMs + 288e5).getUTCDay();
|
|
270
271
|
return day === 0 || day === 6;
|
|
271
272
|
}
|
|
272
273
|
/**
|
|
@@ -1633,24 +1634,31 @@ function computeCost(entry, buckets, peakShare = DEFAULT_PEAK_SHARE, nowMs = Dat
|
|
|
1633
1634
|
return peak * peakShare + off * (1 - peakShare);
|
|
1634
1635
|
}
|
|
1635
1636
|
/**
|
|
1637
|
+
* v1 峰谷档判定(峰谷开闸起、周末全谷分界止):不豁免周末——该时段官方
|
|
1638
|
+
* 高峰时段为每天 9-12 / 14-18(周六日同样计峰)。仅用于历史事件计费;
|
|
1639
|
+
* 「当前时刻」的档位(提醒/时段条/费率展示)一律走 {@link tierAt} 现行规则。
|
|
1640
|
+
*/
|
|
1641
|
+
function tariffV1At(timeMs) {
|
|
1642
|
+
return isPeakHour((new Date(timeMs).getUTCHours() + 8) % 24) ? "peak" : "offPeak";
|
|
1643
|
+
}
|
|
1644
|
+
/**
|
|
1636
1645
|
* 按调用时刻精确判定高峰/空闲档并计价(P0-1:替代固定比例混合)。时刻未知
|
|
1637
1646
|
* (null/NaN,理论不发生在真实事件流)时回退 {@link DEFAULT_PEAK_SHARE} 混合,
|
|
1638
1647
|
* 保持旧语义不低估。平档模型(无 offPeak)两个时段同价。限时促销与峰谷档
|
|
1639
1648
|
* 同口径:按事件时刻判定该笔流量当时享受的单价。
|
|
1649
|
+
*
|
|
1650
|
+
* 历史正确性(按变更节点分段适用规则,不统一套现行价重算历史):
|
|
1651
|
+
* - 早于 {@link PEAK_ERA_START_MS} 的事件按当时官方基础价
|
|
1652
|
+
* ({@link LEGACY_DEEPSEEK_BANDS})计费;
|
|
1653
|
+
* - 峰谷开闸至 {@link WEEKEND_OFFPEAK_START_MS} 之间按 v1 规则(周末不豁免,
|
|
1654
|
+
* 周六日 9-12 / 14-18 计峰);
|
|
1655
|
+
* - 周末全谷分界起按现行规则({@link tierAt},周六日全天低谷)。
|
|
1640
1656
|
* @param entry - the catalog entry whose prices apply.
|
|
1641
1657
|
* @param buckets - token usage counts.
|
|
1642
1658
|
* @param timeMs - the call's wall-clock time (epoch ms); null falls back to the peak-share mix.
|
|
1643
1659
|
* @param peakShare - fallback mix used only when `timeMs` is missing.
|
|
1644
1660
|
* @returns the estimated cost in CNY(USD 计价模型已按当前汇率折算)。
|
|
1645
1661
|
*/
|
|
1646
|
-
/**
|
|
1647
|
-
* v1 峰谷档判定(峰谷开闸起、周末全谷分界止):不豁免周末——该时段官方
|
|
1648
|
-
* 高峰时段为每天 9-12 / 14-18(周六日同样计峰)。仅用于历史事件计费;
|
|
1649
|
-
* 「当前时刻」的档位(提醒/时段条/费率展示)一律走 {@link tierAt} 现行规则。
|
|
1650
|
-
*/
|
|
1651
|
-
function tariffV1At(timeMs) {
|
|
1652
|
-
return isPeakHour((new Date(timeMs).getUTCHours() + 8) % 24) ? "peak" : "offPeak";
|
|
1653
|
-
}
|
|
1654
1662
|
function computeCostAt(entry, buckets, timeMs, peakShare = DEFAULT_PEAK_SHARE) {
|
|
1655
1663
|
if (timeMs === null || timeMs === void 0 || !Number.isFinite(timeMs)) return computeCost(entry, buckets, peakShare);
|
|
1656
1664
|
const priced = applyPromo(entry, timeMs);
|
|
@@ -2033,14 +2041,13 @@ function turnState(turns, turn) {
|
|
|
2033
2041
|
* (default: any `deepseek`-prefixed id). Others count as third-party.
|
|
2034
2042
|
* @param routes - 当前 provider 路由视图(中转站归组)。
|
|
2035
2043
|
* @param searchCallEstimateCny - 联网搜索请求的单次费用估算(人民币元;0 关闭估算)。
|
|
2044
|
+
* @param seedLength - 持久化的 fork 血缘边界(`SessionHeader.seedLength`,缺省 0):
|
|
2045
|
+
* fork 子会话日志里 `seq < seedLength` 的事件拷贝自父会话、已在父会话计费,
|
|
2046
|
+
* 折叠时跳过以避免重复计费;resume 续写复用同一会话日志(每次续写追加一个
|
|
2047
|
+
* `session/end-seed` 标记)但 header 保持原 fork 值,历史段照常计费。
|
|
2036
2048
|
* @returns the per-session fold (cached by the incremental aggregator).
|
|
2037
2049
|
*/
|
|
2038
|
-
function foldSession(events, subscriptionProviders, officialProviderIds, routes = {}, searchCallEstimateCny = DEFAULT_SEARCH_CALL_ESTIMATE_CNY) {
|
|
2039
|
-
let seedBoundary = -1;
|
|
2040
|
-
for (const event of events) if (event.type === "session/end-seed" && typeof event.seq === "number" && Number.isFinite(event.seq)) seedBoundary = Math.max(seedBoundary, event.seq);
|
|
2041
|
-
if (seedBoundary >= 0) {
|
|
2042
|
-
if (!events.some((event) => typeof event.seq === "number" && Number.isFinite(event.seq) && event.seq > seedBoundary)) seedBoundary = -1;
|
|
2043
|
-
}
|
|
2050
|
+
function foldSession(events, subscriptionProviders, officialProviderIds, routes = {}, searchCallEstimateCny = DEFAULT_SEARCH_CALL_ESTIMATE_CNY, seedLength = 0) {
|
|
2044
2051
|
const fold = {
|
|
2045
2052
|
total: emptyUsage(),
|
|
2046
2053
|
byModel: /* @__PURE__ */ new Map(),
|
|
@@ -2071,7 +2078,7 @@ function foldSession(events, subscriptionProviders, officialProviderIds, routes
|
|
|
2071
2078
|
let lastOpenStepKey;
|
|
2072
2079
|
const toolSeen = /* @__PURE__ */ new Set();
|
|
2073
2080
|
for (const event of events) {
|
|
2074
|
-
if (
|
|
2081
|
+
if (seedLength > 0 && typeof event.seq === "number" && Number.isFinite(event.seq) && event.seq < seedLength) continue;
|
|
2075
2082
|
fold.lastActive = Math.max(fold.lastActive, event.time);
|
|
2076
2083
|
if (event.type === "session/title") {
|
|
2077
2084
|
const title = event.data.title;
|
|
@@ -2372,7 +2379,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2372
2379
|
const { events } = await persistence.readFrom(meta.id, 0);
|
|
2373
2380
|
const after = await stampOf(meta);
|
|
2374
2381
|
if (stamp !== null && after !== stamp) continue;
|
|
2375
|
-
const fold = foldSession(events, subscriptionProviders, officialProviderIds, routesOf(), searchEstimate);
|
|
2382
|
+
const fold = foldSession(events, subscriptionProviders, officialProviderIds, routesOf(), searchEstimate, meta.seedLength ?? 0);
|
|
2376
2383
|
cache.set(id, {
|
|
2377
2384
|
stamp,
|
|
2378
2385
|
fold
|
|
@@ -2388,7 +2395,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2388
2395
|
id,
|
|
2389
2396
|
...meta.cwd === void 0 ? {} : { cwd: meta.cwd },
|
|
2390
2397
|
...stamp === null ? {} : { stamp },
|
|
2391
|
-
foldVersion:
|
|
2398
|
+
foldVersion: 7,
|
|
2392
2399
|
fold: serializeFold(fold)
|
|
2393
2400
|
};
|
|
2394
2401
|
const previous = ledger.get(id);
|
|
@@ -2412,7 +2419,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2412
2419
|
for (const entry of ledger.values()) {
|
|
2413
2420
|
if (included.has(entry.id)) continue;
|
|
2414
2421
|
try {
|
|
2415
|
-
const stale = (entry.foldVersion ?? 1) <
|
|
2422
|
+
const stale = (entry.foldVersion ?? 1) < 7;
|
|
2416
2423
|
folds.push({
|
|
2417
2424
|
id: entry.id,
|
|
2418
2425
|
...entry.cwd === void 0 ? {} : { cwd: entry.cwd },
|
|
@@ -2458,7 +2465,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2458
2465
|
outputCost: 0
|
|
2459
2466
|
};
|
|
2460
2467
|
const perfModel = /* @__PURE__ */ new Map();
|
|
2461
|
-
const
|
|
2468
|
+
const perfHourModel = /* @__PURE__ */ new Map();
|
|
2462
2469
|
for (const { id: sessionId, cwd, fold, staleLedger } of folds) {
|
|
2463
2470
|
mergeUsageInto(total, fold.total);
|
|
2464
2471
|
roles.userChars += fold.roles.userChars;
|
|
@@ -2489,13 +2496,18 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2489
2496
|
if (sample.tps !== void 0) modelAccum.tps.push(sample.tps);
|
|
2490
2497
|
if (sample.latencyMs !== void 0) modelAccum.latencies.push(sample.latencyMs);
|
|
2491
2498
|
if (sample.estimated) modelAccum.estimated += 1;
|
|
2492
|
-
let
|
|
2499
|
+
let hourModels = perfHourModel.get(sample.hour);
|
|
2500
|
+
if (hourModels === void 0) {
|
|
2501
|
+
hourModels = /* @__PURE__ */ new Map();
|
|
2502
|
+
perfHourModel.set(sample.hour, hourModels);
|
|
2503
|
+
}
|
|
2504
|
+
let hourAccum = hourModels.get(sample.model);
|
|
2493
2505
|
if (hourAccum === void 0) {
|
|
2494
2506
|
hourAccum = {
|
|
2495
2507
|
ttfts: [],
|
|
2496
2508
|
tps: []
|
|
2497
2509
|
};
|
|
2498
|
-
|
|
2510
|
+
hourModels.set(sample.model, hourAccum);
|
|
2499
2511
|
}
|
|
2500
2512
|
hourAccum.ttfts.push(sample.ttftMs);
|
|
2501
2513
|
if (sample.tps !== void 0) hourAccum.tps.push(sample.tps);
|
|
@@ -2555,14 +2567,14 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2555
2567
|
latencyAvg: acc.latencies.length === 0 ? 0 : mean(acc.latencies),
|
|
2556
2568
|
estimatedSamples: acc.estimated
|
|
2557
2569
|
}])),
|
|
2558
|
-
|
|
2570
|
+
byHourModel: Object.fromEntries([...perfHourModel].map(([hour, models]) => [hour, Object.fromEntries([...models].map(([model, acc]) => [model, {
|
|
2559
2571
|
samples: acc.ttfts.length,
|
|
2560
2572
|
ttftAvg: mean(acc.ttfts),
|
|
2561
2573
|
...acc.tps.length === 0 ? {} : { tpsAvg: mean(acc.tps) }
|
|
2562
|
-
}]))
|
|
2574
|
+
}]))]))
|
|
2563
2575
|
};
|
|
2564
2576
|
lastDoc = {
|
|
2565
|
-
version:
|
|
2577
|
+
version: 4,
|
|
2566
2578
|
updatedAt: now,
|
|
2567
2579
|
source: "session-logs",
|
|
2568
2580
|
timezone: hostTimeZone(),
|
|
@@ -2905,6 +2917,42 @@ function queryXai(ctx, apiKeyEnv) {
|
|
|
2905
2917
|
};
|
|
2906
2918
|
});
|
|
2907
2919
|
}
|
|
2920
|
+
/** TokenDance 钱包端点(issue #27:GET + Bearer,与模型调用同一把 key)。 */
|
|
2921
|
+
const TOKENDANCE_BALANCE_URL = "https://tokendance.space/portal/api/v1/user/balance";
|
|
2922
|
+
/** 微元 → 元:TokenDance 全部金额字段以 1 元 = 1,000,000 微元计。 */
|
|
2923
|
+
const TOKENDANCE_MICRO_PER_YUAN = 1e6;
|
|
2924
|
+
/**
|
|
2925
|
+
* 从 TokenDance 余额响应提取剩余余额并换算为元。导出供测试:纯函数。
|
|
2926
|
+
* 优先用服务端现成的 `balance.balance`(= credits - credits_used);缺失时
|
|
2927
|
+
* 按两个明细字段相减推导,字段全部缺失返回 undefined。
|
|
2928
|
+
* @param data - 余额端点的 JSON 响应(`{ balance: { credits, credits_used, balance } }`,微元)。
|
|
2929
|
+
* @returns 剩余余额(元);提取不到返回 undefined。
|
|
2930
|
+
*/
|
|
2931
|
+
function pickTokenDanceBalanceCny(data) {
|
|
2932
|
+
const doc = data;
|
|
2933
|
+
const direct = toNumber$1(doc?.balance?.balance);
|
|
2934
|
+
if (direct !== void 0) return direct / TOKENDANCE_MICRO_PER_YUAN;
|
|
2935
|
+
const credits = toNumber$1(doc?.balance?.credits);
|
|
2936
|
+
const used = toNumber$1(doc?.balance?.credits_used);
|
|
2937
|
+
if (credits !== void 0 && used !== void 0) return (credits - used) / TOKENDANCE_MICRO_PER_YUAN;
|
|
2938
|
+
}
|
|
2939
|
+
/**
|
|
2940
|
+
* 查询 TokenDance Space 钱包余额(issue #26/#27)。`queryBearerBalance` 已覆盖
|
|
2941
|
+
* 认证失败(401 → unauthorized)与熔断/超时/重试,这里只负责微元 → 元换算。
|
|
2942
|
+
* @param ctx - host context carrying the credentials seam.
|
|
2943
|
+
* @param apiKeyEnv - credential reference resolving the TokenDance API key.
|
|
2944
|
+
*/
|
|
2945
|
+
function queryTokenDance(ctx, apiKeyEnv) {
|
|
2946
|
+
return queryBearerBalance(ctx, TOKENDANCE_BALANCE_URL, apiKeyEnv, "TokenDance", "TokenDance", (data) => {
|
|
2947
|
+
const cny = pickTokenDanceBalanceCny(data);
|
|
2948
|
+
return {
|
|
2949
|
+
provider: "TokenDance",
|
|
2950
|
+
displayName: "TokenDance",
|
|
2951
|
+
currency: "CNY",
|
|
2952
|
+
...cny !== void 0 ? { totalBalance: cny } : {}
|
|
2953
|
+
};
|
|
2954
|
+
});
|
|
2955
|
+
}
|
|
2908
2956
|
/**
|
|
2909
2957
|
* Query the Zhipu GLM / Z.ai (国内 bigmodel-cn 域) account balance.
|
|
2910
2958
|
* 与订阅(zai-coding-cn 的 Coding Plan)互补:一个平台可同时有钱包余额与订阅
|
|
@@ -2961,6 +3009,36 @@ const QUERIERS = [
|
|
|
2961
3009
|
route: "zai-coding-cn",
|
|
2962
3010
|
displayName: "智谱 AI",
|
|
2963
3011
|
querier: queryZhipu
|
|
3012
|
+
},
|
|
3013
|
+
{
|
|
3014
|
+
route: "tokendance",
|
|
3015
|
+
displayName: "TokenDance",
|
|
3016
|
+
querier: queryTokenDance
|
|
3017
|
+
},
|
|
3018
|
+
{
|
|
3019
|
+
route: "tokendance-space",
|
|
3020
|
+
displayName: "TokenDance",
|
|
3021
|
+
querier: queryTokenDance
|
|
3022
|
+
},
|
|
3023
|
+
{
|
|
3024
|
+
route: "tencent-tokenhub",
|
|
3025
|
+
displayName: "腾讯云 TokenHub",
|
|
3026
|
+
querier: queryTencentTokenPlan
|
|
3027
|
+
},
|
|
3028
|
+
{
|
|
3029
|
+
route: "tokenhub",
|
|
3030
|
+
displayName: "腾讯云 TokenHub",
|
|
3031
|
+
querier: queryTencentTokenPlan
|
|
3032
|
+
},
|
|
3033
|
+
{
|
|
3034
|
+
route: "tencent",
|
|
3035
|
+
displayName: "腾讯云 TokenHub",
|
|
3036
|
+
querier: queryTencentTokenPlan
|
|
3037
|
+
},
|
|
3038
|
+
{
|
|
3039
|
+
route: "tencentcloud",
|
|
3040
|
+
displayName: "腾讯云 TokenHub",
|
|
3041
|
+
querier: queryTencentTokenPlan
|
|
2964
3042
|
}
|
|
2965
3043
|
];
|
|
2966
3044
|
/**
|
|
@@ -2993,6 +3071,204 @@ async function queryBalances(ctx, providers) {
|
|
|
2993
3071
|
return querier(ctx, env);
|
|
2994
3072
|
}));
|
|
2995
3073
|
}
|
|
3074
|
+
/** TokenHub 管控面 API 端点(cloud.tencent.cn/document/api/1823/132270)。 */
|
|
3075
|
+
const TOKENHUB_HOST = "tokenhub.tencentcloudapi.com";
|
|
3076
|
+
/** 云 API 3.0 产品名与版本(签名的 service 段与请求头都必须一致)。 */
|
|
3077
|
+
const TOKENHUB_SERVICE = "tokenhub";
|
|
3078
|
+
const TOKENHUB_VERSION = "2026-03-22";
|
|
3079
|
+
/** 请求地域:管控面对地域不敏感,取默认国内地域(文档地域列表含 ap-guangzhou)。 */
|
|
3080
|
+
const TOKENHUB_REGION = "ap-guangzhou";
|
|
3081
|
+
/** 腾讯云凭据引用值格式:`<SecretId>:<SecretKey>`(分隔符取首个冒号)。 */
|
|
3082
|
+
function parseTencentCredential(value) {
|
|
3083
|
+
const sep = value.indexOf(":");
|
|
3084
|
+
if (sep === -1) return void 0;
|
|
3085
|
+
const secretId = value.slice(0, sep).trim();
|
|
3086
|
+
const secretKey = value.slice(sep + 1).trim();
|
|
3087
|
+
if (secretId === "" || secretKey === "") return void 0;
|
|
3088
|
+
return {
|
|
3089
|
+
secretId,
|
|
3090
|
+
secretKey
|
|
3091
|
+
};
|
|
3092
|
+
}
|
|
3093
|
+
/**
|
|
3094
|
+
* 构造云 API 3.0 TC3-HMAC-SHA256 签名(官方签名方法 v3)。导出供测试:纯函数,
|
|
3095
|
+
* 输入确定则签名确定。Action 不参与签名——它走 `X-TC-Action` 请求头。
|
|
3096
|
+
* @param secretId - 云 API SecretId。
|
|
3097
|
+
* @param secretKey - 云 API SecretKey。
|
|
3098
|
+
* @param payload - 已序列化的请求体(含 Action/Version/Region 公共参数)。
|
|
3099
|
+
* @param timestamp - 签名时间戳(秒)。
|
|
3100
|
+
* @returns Authorization 头的值。
|
|
3101
|
+
*/
|
|
3102
|
+
function tc3Authorization(secretId, secretKey, payload, timestamp) {
|
|
3103
|
+
const date = (/* @__PURE__ */ new Date(timestamp * 1e3)).toISOString().slice(0, 10);
|
|
3104
|
+
const canonicalRequest = `POST\n/\n\n${`content-type:application/json; charset=utf-8\nhost:${TOKENHUB_HOST}\n`}\ncontent-type;host\n${createHash("sha256").update(payload).digest("hex")}`;
|
|
3105
|
+
const hashedCanonical = createHash("sha256").update(canonicalRequest).digest("hex");
|
|
3106
|
+
const stringToSign = `TC3-HMAC-SHA256\n${String(timestamp)}\n${date}/${TOKENHUB_SERVICE}/tc3_request\n${hashedCanonical}`;
|
|
3107
|
+
const kDate = createHmac("sha256", date).update(secretKey).digest();
|
|
3108
|
+
const kService = createHmac("sha256", kDate).update(TOKENHUB_SERVICE).digest();
|
|
3109
|
+
const kSigning = createHmac("sha256", kService).update("tc3_request").digest();
|
|
3110
|
+
const signature = createHmac("sha256", kSigning).update(stringToSign).digest("hex");
|
|
3111
|
+
return `TC3-HMAC-SHA256 Credential=${secretId}/${date}/${TOKENHUB_SERVICE}/tc3_request, SignedHeaders=content-type;host, Signature=${signature}`;
|
|
3112
|
+
}
|
|
3113
|
+
/** 调用一次 TokenHub 管控面接口:TC3 签名 + 超时保护,返回响应 JSON 的 `Response`。 */
|
|
3114
|
+
async function callTokenHub(secretId, secretKey, action, params) {
|
|
3115
|
+
const payload = JSON.stringify({
|
|
3116
|
+
Action: action,
|
|
3117
|
+
Version: TOKENHUB_VERSION,
|
|
3118
|
+
Region: TOKENHUB_REGION,
|
|
3119
|
+
...params
|
|
3120
|
+
});
|
|
3121
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
3122
|
+
const controller = new AbortController();
|
|
3123
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS$3);
|
|
3124
|
+
try {
|
|
3125
|
+
const response = await fetch(`https://${TOKENHUB_HOST}/`, {
|
|
3126
|
+
method: "POST",
|
|
3127
|
+
headers: {
|
|
3128
|
+
"content-type": "application/json; charset=utf-8",
|
|
3129
|
+
host: TOKENHUB_HOST,
|
|
3130
|
+
"x-tc-action": action.toLowerCase(),
|
|
3131
|
+
"x-tc-version": TOKENHUB_VERSION,
|
|
3132
|
+
"x-tc-region": TOKENHUB_REGION,
|
|
3133
|
+
"x-tc-timestamp": String(timestamp),
|
|
3134
|
+
authorization: tc3Authorization(secretId, secretKey, payload, timestamp)
|
|
3135
|
+
},
|
|
3136
|
+
body: payload,
|
|
3137
|
+
signal: controller.signal
|
|
3138
|
+
});
|
|
3139
|
+
if (response.status === 401 || response.status === 403) throw Object.assign(/* @__PURE__ */ new Error("unauthorized"), { code: "unauthorized" });
|
|
3140
|
+
if (!response.ok) throw Object.assign(/* @__PURE__ */ new Error(`HTTP ${String(response.status)}`), {
|
|
3141
|
+
httpStatus: response.status,
|
|
3142
|
+
code: "unreachable"
|
|
3143
|
+
});
|
|
3144
|
+
const inner = (await response.json()).Response;
|
|
3145
|
+
if (inner === void 0) throw Object.assign(/* @__PURE__ */ new Error("no Response envelope"), { code: "invalid" });
|
|
3146
|
+
if (inner.Error !== void 0 && inner.Error !== null) {
|
|
3147
|
+
const err = inner.Error;
|
|
3148
|
+
const code = err.Code === "AuthFailure.SignatureFailure" || err.Code === "AuthFailure.SecretIdNotFound" ? "unauthorized" : "unreachable";
|
|
3149
|
+
throw Object.assign(new Error(String(err.Code ?? "api-error")), { code });
|
|
3150
|
+
}
|
|
3151
|
+
return inner;
|
|
3152
|
+
} finally {
|
|
3153
|
+
clearTimeout(timer);
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
/**
|
|
3157
|
+
* 在套餐余量对象里防御性提取「剩余额度」:官方 SubPackageBalance/PackageInfo
|
|
3158
|
+
* 的字段名未稳定公开(issue #18 调研期),按语义键名扫描——命中 remaining /
|
|
3159
|
+
* balance / left 语义键直接用;命中 total 与 used 则相减推导。数字一律经
|
|
3160
|
+
* {@link toNumber} 归一化(上游可能给字符串)。
|
|
3161
|
+
* 导出供测试:纯函数。
|
|
3162
|
+
* @param source - 套餐详情里的余量对象(PackageInfo / SubPackageBalance 等)。
|
|
3163
|
+
* @returns 剩余额度(上游单位,通常为 token 数或元);提取不到返回 undefined。
|
|
3164
|
+
*/
|
|
3165
|
+
function pickRemainingQuota(source) {
|
|
3166
|
+
if (source === null || typeof source !== "object") return void 0;
|
|
3167
|
+
const numeric = Object.entries(source).filter(([, v]) => v !== null && toNumber$1(v) !== void 0);
|
|
3168
|
+
const byKey = (needles) => {
|
|
3169
|
+
for (const [key, value] of numeric) {
|
|
3170
|
+
const lower = key.toLowerCase();
|
|
3171
|
+
if (needles.some((n) => lower.includes(n))) {
|
|
3172
|
+
const num = toNumber$1(value);
|
|
3173
|
+
if (num !== void 0 && num >= 0) return num;
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
};
|
|
3177
|
+
const remaining = byKey([
|
|
3178
|
+
"remain",
|
|
3179
|
+
"balance",
|
|
3180
|
+
"left",
|
|
3181
|
+
"available"
|
|
3182
|
+
]);
|
|
3183
|
+
if (remaining !== void 0) return remaining;
|
|
3184
|
+
const total = byKey(["total"]);
|
|
3185
|
+
const used = byKey(["used", "consume"]);
|
|
3186
|
+
if (total !== void 0 && used !== void 0) return Math.max(0, total - used);
|
|
3187
|
+
}
|
|
3188
|
+
/** 套餐列表里提取第一个启用套餐的 TeamId:集合字段名做候选兼容。 */
|
|
3189
|
+
function firstEnabledTeamId(inner) {
|
|
3190
|
+
const candidates = inner.TeamSet ?? inner.TokenPlanSet ?? inner.PlanSet;
|
|
3191
|
+
if (!Array.isArray(candidates)) return void 0;
|
|
3192
|
+
for (const item of candidates) {
|
|
3193
|
+
if (item === null || typeof item !== "object") continue;
|
|
3194
|
+
const row = item;
|
|
3195
|
+
if (row.TeamId === void 0 && row.PlanId === void 0) continue;
|
|
3196
|
+
if (row.Status !== void 0 && row.Status !== "enable") continue;
|
|
3197
|
+
const id = row.TeamId ?? row.PlanId;
|
|
3198
|
+
if (typeof id === "string" && id !== "") return id;
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
/**
|
|
3202
|
+
* 查询腾讯云 TokenHub Token Plan 套餐余量。凭据值格式 `<SecretId>:<SecretKey>`
|
|
3203
|
+
* (云 API 密钥,非 TokenHub 推理 key)。链路:套餐列表取 TeamId → 套餐详情读
|
|
3204
|
+
* 主额度包余量。管控面字段名未完全稳定,解析按语义键防御提取。
|
|
3205
|
+
* @param ctx - host context carrying the credentials seam.
|
|
3206
|
+
* @param apiKeyEnv - credential reference resolving the `<SecretId>:<SecretKey>` pair.
|
|
3207
|
+
*/
|
|
3208
|
+
async function queryTencentTokenPlan(ctx, apiKeyEnv) {
|
|
3209
|
+
const provider = "腾讯云 TokenHub";
|
|
3210
|
+
const hit = await ctx.credentials.resolve(credentialRef(apiKeyEnv));
|
|
3211
|
+
if (hit === void 0) return {
|
|
3212
|
+
provider,
|
|
3213
|
+
displayName: provider,
|
|
3214
|
+
error: "unconfigured"
|
|
3215
|
+
};
|
|
3216
|
+
if (!balanceGate.check(provider)) return {
|
|
3217
|
+
provider,
|
|
3218
|
+
displayName: provider,
|
|
3219
|
+
error: "unreachable"
|
|
3220
|
+
};
|
|
3221
|
+
const credential = parseTencentCredential(hit.value);
|
|
3222
|
+
if (credential === void 0) return {
|
|
3223
|
+
provider,
|
|
3224
|
+
displayName: provider,
|
|
3225
|
+
error: "unauthorized"
|
|
3226
|
+
};
|
|
3227
|
+
const doRequest = async () => {
|
|
3228
|
+
const teamId = firstEnabledTeamId(await callTokenHub(credential.secretId, credential.secretKey, "DescribeTokenPlanList", {}));
|
|
3229
|
+
if (teamId === void 0) return {
|
|
3230
|
+
provider,
|
|
3231
|
+
displayName: provider,
|
|
3232
|
+
error: "invalid"
|
|
3233
|
+
};
|
|
3234
|
+
const detail = await callTokenHub(credential.secretId, credential.secretKey, "DescribeTokenPlan", { TeamId: teamId });
|
|
3235
|
+
const remaining = pickRemainingQuota(detail.PackageInfo) ?? pickRemainingQuota(detail);
|
|
3236
|
+
if (remaining === void 0) {
|
|
3237
|
+
console.warn(`[usage-billing] balance response drifted for ${provider}: no remaining-quota field parsed`);
|
|
3238
|
+
return {
|
|
3239
|
+
provider,
|
|
3240
|
+
displayName: provider,
|
|
3241
|
+
error: "invalid"
|
|
3242
|
+
};
|
|
3243
|
+
}
|
|
3244
|
+
const plan = typeof detail.Name === "string" ? detail.Name : void 0;
|
|
3245
|
+
const exhausted = detail.StopReason === "EXHAUSTED";
|
|
3246
|
+
return {
|
|
3247
|
+
provider,
|
|
3248
|
+
displayName: provider,
|
|
3249
|
+
currency: "CNY",
|
|
3250
|
+
totalBalance: remaining,
|
|
3251
|
+
...plan !== void 0 ? { plan } : {},
|
|
3252
|
+
...exhausted ? { isAvailable: false } : {}
|
|
3253
|
+
};
|
|
3254
|
+
};
|
|
3255
|
+
try {
|
|
3256
|
+
const row = await withRetry(doRequest, {
|
|
3257
|
+
retries: 1,
|
|
3258
|
+
baseDelayMs: 250,
|
|
3259
|
+
maxDelayMs: 2e3
|
|
3260
|
+
});
|
|
3261
|
+
balanceGate.success(provider);
|
|
3262
|
+
return row;
|
|
3263
|
+
} catch (error) {
|
|
3264
|
+
balanceGate.fail(provider);
|
|
3265
|
+
return {
|
|
3266
|
+
provider,
|
|
3267
|
+
displayName: provider,
|
|
3268
|
+
error: error.code === "unauthorized" ? "unauthorized" : "unreachable"
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
2996
3272
|
/** 点路径取值:`data.total_available` → 逐层下钻;任一缺失返回 undefined。 */
|
|
2997
3273
|
function getPath(data, path) {
|
|
2998
3274
|
let cursor = data;
|
|
@@ -3177,7 +3453,7 @@ const declaredGate = createCooldownGate({
|
|
|
3177
3453
|
cooldownMs: 6e4
|
|
3178
3454
|
});
|
|
3179
3455
|
/** 会触及原型链的路径段:一律拒绝(读结果不是文档自带字段)。 */
|
|
3180
|
-
const FORBIDDEN_SEGMENTS = new Set([
|
|
3456
|
+
const FORBIDDEN_SEGMENTS = /* @__PURE__ */ new Set([
|
|
3181
3457
|
"__proto__",
|
|
3182
3458
|
"constructor",
|
|
3183
3459
|
"prototype"
|
|
@@ -3727,7 +4003,7 @@ function asPrice(value) {
|
|
|
3727
4003
|
*/
|
|
3728
4004
|
function buildExtraModels(data) {
|
|
3729
4005
|
if (data === null || typeof data !== "object") return [];
|
|
3730
|
-
const catalogKeys = new Set([...MODEL_CATALOG.map((entry) => entry.key.toLowerCase()), ...Object.keys(MODEL_KEY_ALIASES).map((key) => MODEL_KEY_ALIASES[key]?.toLowerCase() ?? key.toLowerCase())]);
|
|
4006
|
+
const catalogKeys = /* @__PURE__ */ new Set([...MODEL_CATALOG.map((entry) => entry.key.toLowerCase()), ...Object.keys(MODEL_KEY_ALIASES).map((key) => MODEL_KEY_ALIASES[key]?.toLowerCase() ?? key.toLowerCase())]);
|
|
3731
4007
|
const extras = [];
|
|
3732
4008
|
for (const [providerId, providerDoc] of Object.entries(data)) {
|
|
3733
4009
|
if (providerDoc === null || typeof providerDoc !== "object") continue;
|
|
@@ -4401,7 +4677,7 @@ const relayGate = createCooldownGate({
|
|
|
4401
4677
|
/** Abort a relay fetch when the upstream hangs beyond this budget. */
|
|
4402
4678
|
const FETCH_TIMEOUT_MS = 8e3;
|
|
4403
4679
|
/** 指纹识别缓存 TTL(毫秒):识别结果低频变化,5 分钟内同 origin 不再重复探测。 */
|
|
4404
|
-
const FINGERPRINT_TTL_MS =
|
|
4680
|
+
const FINGERPRINT_TTL_MS = 3e5;
|
|
4405
4681
|
/** 每 origin 的识别结果缓存:`kind` 是识别出的中转站程序,`at` 是探测时刻。 */
|
|
4406
4682
|
const fingerprintCache = /* @__PURE__ */ new Map();
|
|
4407
4683
|
/** Number, or null when the value is not a finite number (nor numeric string). */
|
|
@@ -4537,7 +4813,7 @@ function originOf(baseURL) {
|
|
|
4537
4813
|
* 子路径只会得到 404/非中转站格式,因而面板应排除它们,避免误判为
|
|
4538
4814
|
* 「未识别」。中转站面板只列真正的第三方中转程序。
|
|
4539
4815
|
*/
|
|
4540
|
-
const OFFICIAL_HOSTS = new Set([
|
|
4816
|
+
const OFFICIAL_HOSTS = /* @__PURE__ */ new Set([
|
|
4541
4817
|
"api.deepseek.com",
|
|
4542
4818
|
"api.openai.com",
|
|
4543
4819
|
"open.bigmodel.cn",
|
|
@@ -4787,10 +5063,10 @@ const usageBillingSettingsNs = validateSettingsNamespace(BILLING_SETTINGS_NAMESP
|
|
|
4787
5063
|
/** 该命名空间的 wire schema:`enableUsageStatsTool` 布尔,默认关闭(issue 诉求)。 */
|
|
4788
5064
|
const UsageBillingSettingsSchema = z.object({ [ENABLE_USAGE_STATS_TOOL_FIELD]: z.boolean().default(false) });
|
|
4789
5065
|
/** 实时定价的后台刷新间隔(毫秒):汇率/模型价低频变化,6 小时一次足够。 */
|
|
4790
|
-
const PRICING_REFRESH_INTERVAL_MS =
|
|
5066
|
+
const PRICING_REFRESH_INTERVAL_MS = 216e5;
|
|
4791
5067
|
/** 订阅套餐额度缓存时长(毫秒):上游配额 API 低频变化,5 分钟足够。 */
|
|
4792
|
-
const SUBSCRIPTION_CACHE_MS =
|
|
4793
|
-
const BALANCE_CACHE_MS =
|
|
5068
|
+
const SUBSCRIPTION_CACHE_MS = 3e5;
|
|
5069
|
+
const BALANCE_CACHE_MS = 3e5;
|
|
4794
5070
|
/** DeepSeek 余额查询的默认凭据引用(与 llm-deepseek 的默认引用一致)。 */
|
|
4795
5071
|
const DEFAULT_BALANCE_API_KEY_ENV = "DEEPSEEK_API_KEY";
|
|
4796
5072
|
/**
|
|
@@ -4801,7 +5077,7 @@ const PACKAGE_VERSION = createRequire(import.meta.url)("../package.json").versio
|
|
|
4801
5077
|
/** 统计快照的落盘节流(毫秒):前端 30 秒轮询,快照最多每 30 秒写一次。 */
|
|
4802
5078
|
const SNAPSHOT_INTERVAL_MS = 3e4;
|
|
4803
5079
|
/** 鉴权失败告警冷却(毫秒):同一 provider 在窗口内只提示一次,避免 30 秒轮询刷屏。 */
|
|
4804
|
-
const AUTH_WARN_COOLDOWN_MS =
|
|
5080
|
+
const AUTH_WARN_COOLDOWN_MS = 18e5;
|
|
4805
5081
|
/**
|
|
4806
5082
|
* 鉴权失败分类告警(P1-5):余额 / 订阅查询返回 unauthorized 时,按
|
|
4807
5083
|
* `source:provider` 去重并冷却告警,提示检查 llm-pi-ai 里该 provider 的 apiKeyEnv。
|
|
@@ -5045,7 +5321,8 @@ function apply(ctx, config = {}) {
|
|
|
5045
5321
|
})();
|
|
5046
5322
|
const persistReconcileRef = () => {
|
|
5047
5323
|
if (reconcileRef === null) return;
|
|
5048
|
-
|
|
5324
|
+
const payload = JSON.stringify({ ref: reconcileRef });
|
|
5325
|
+
writeFileAtomic(reconcilePath, payload, {
|
|
5049
5326
|
mode: 384,
|
|
5050
5327
|
dirMode: 448
|
|
5051
5328
|
}).catch(() => {});
|
package/lib/types/aggregate.d.ts
CHANGED
|
@@ -418,10 +418,13 @@ export interface UsageLedgerDocument {
|
|
|
418
418
|
* 两次 header 之间的用量串到上一个模型,订阅模型首当其冲,issue #14);v2 =
|
|
419
419
|
* `assistant/message` 自带 source 归账(1.0.7 起);v3 = 联网搜索请求按次估算
|
|
420
420
|
* 计费(issue #15,1.0.9 起);v4 = 模型×日期×站点三维桶(issue #16,按 origin
|
|
421
|
-
* 绑定自定义价的显示层重估)——旧行缺该维度,按无 origin
|
|
421
|
+
* 绑定自定义价的显示层重估)——旧行缺该维度,按无 origin 价处理;
|
|
422
|
+
* v6 = fork 种子边界改用持久化 `SessionHeader.seedLength`(5 及更早扫描最后一个
|
|
423
|
+
* `session/end-seed`,resume 续写后继续对话时把本会话历史段误判为种子丢弃,
|
|
424
|
+
* 会话费用只剩最近一段,issue #29)。
|
|
422
425
|
* 持久账本行据此区分新旧算法:日志已删/不可读而只能沿用旧行时,UI 标注置信度提示。
|
|
423
426
|
*/
|
|
424
|
-
export declare const FOLD_VERSION =
|
|
427
|
+
export declare const FOLD_VERSION = 6;
|
|
425
428
|
/**
|
|
426
429
|
* 一次性账本迁移:id 唯一,apply 在加载边界对原始文档执行,已应用过的跳过。
|
|
427
430
|
* 未来账本/schema 字段变更(重命名、拆桶、语义调整)时,在此追加一条迁移并
|
|
@@ -467,6 +470,10 @@ export declare function messageTextLength(message: unknown): number;
|
|
|
467
470
|
* (default: any `deepseek`-prefixed id). Others count as third-party.
|
|
468
471
|
* @param routes - 当前 provider 路由视图(中转站归组)。
|
|
469
472
|
* @param searchCallEstimateCny - 联网搜索请求的单次费用估算(人民币元;0 关闭估算)。
|
|
473
|
+
* @param seedLength - 持久化的 fork 血缘边界(`SessionHeader.seedLength`,缺省 0):
|
|
474
|
+
* fork 子会话日志里 `seq < seedLength` 的事件拷贝自父会话、已在父会话计费,
|
|
475
|
+
* 折叠时跳过以避免重复计费;resume 续写复用同一会话日志(每次续写追加一个
|
|
476
|
+
* `session/end-seed` 标记)但 header 保持原 fork 值,历史段照常计费。
|
|
470
477
|
* @returns the per-session fold (cached by the incremental aggregator).
|
|
471
478
|
*/
|
|
472
479
|
export declare function foldSession(events: readonly {
|
|
@@ -474,7 +481,7 @@ export declare function foldSession(events: readonly {
|
|
|
474
481
|
time: number;
|
|
475
482
|
data: never;
|
|
476
483
|
seq?: number;
|
|
477
|
-
}[], subscriptionProviders: ReadonlySet<string>, officialProviderIds?: ReadonlySet<string>, routes?: Readonly<Record<string, ProviderRouteView>>, searchCallEstimateCny?: number): SessionFold;
|
|
484
|
+
}[], subscriptionProviders: ReadonlySet<string>, officialProviderIds?: ReadonlySet<string>, routes?: Readonly<Record<string, ProviderRouteView>>, searchCallEstimateCny?: number, seedLength?: number): SessionFold;
|
|
478
485
|
/**
|
|
479
486
|
* 增量聚合器:按会话缓存折叠结果,用日志文件的 mtime+size 作失效键——
|
|
480
487
|
* 日志没动的会话直接复用,只有写过的会话重新折叠;整份文档另有短 TTL
|
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.1.
|
|
4
|
+
"version": "1.1.5",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -34,7 +34,6 @@
|
|
|
34
34
|
},
|
|
35
35
|
"client": {
|
|
36
36
|
"inject": [
|
|
37
|
-
"@deepseek-ai/dsh-client-runtime",
|
|
38
37
|
"@deepseek-ai/dsh-client-locale",
|
|
39
38
|
"@deepseek-ai/dsh-client-ui-conversation"
|
|
40
39
|
],
|
|
@@ -51,25 +50,25 @@
|
|
|
51
50
|
"scripts": {
|
|
52
51
|
"bundle": "tsdown",
|
|
53
52
|
"watch": "tsdown --watch",
|
|
54
|
-
"test": "vitest run"
|
|
55
|
-
"build": "bash build.sh"
|
|
53
|
+
"test": "vitest run"
|
|
56
54
|
},
|
|
57
55
|
"license": "MIT",
|
|
58
56
|
"engines": {
|
|
59
57
|
"node": "^22.19.0 || >=24.0.0"
|
|
60
58
|
},
|
|
61
59
|
"peerDependencies": {
|
|
60
|
+
"@deepseek-ai/cordis": "*",
|
|
62
61
|
"@deepseek-ai/dsh-api-remotes": "*",
|
|
62
|
+
"@deepseek-ai/dsh-api-session-controller": "*",
|
|
63
63
|
"@deepseek-ai/dsh-atomic-write": "*",
|
|
64
|
-
"@deepseek-ai/dsh-client-connection": "*",
|
|
65
|
-
"@deepseek-ai/dsh-host-webserver": "*",
|
|
66
64
|
"@deepseek-ai/dsh-client-locale": "*",
|
|
67
|
-
"@deepseek-ai/dsh-client-
|
|
65
|
+
"@deepseek-ai/dsh-client-store": "*",
|
|
68
66
|
"@deepseek-ai/dsh-client-ui-conversation": "*",
|
|
69
|
-
"@deepseek-ai/dsh-client-ui-sidebar": "*",
|
|
70
67
|
"@deepseek-ai/dsh-client-ui-primitives": "*",
|
|
68
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "*",
|
|
71
69
|
"@deepseek-ai/dsh-client-ui-slots": "*",
|
|
72
70
|
"@deepseek-ai/dsh-credentials": "*",
|
|
71
|
+
"@deepseek-ai/dsh-host-webserver": "*",
|
|
73
72
|
"@deepseek-ai/dsh-invariants": "*",
|
|
74
73
|
"@deepseek-ai/dsh-llm": "*",
|
|
75
74
|
"@deepseek-ai/dsh-session": "*",
|
|
@@ -77,28 +76,44 @@
|
|
|
77
76
|
"@deepseek-ai/dsh-settings": "*",
|
|
78
77
|
"@deepseek-ai/dsh-tools": "*",
|
|
79
78
|
"@deepseek-ai/schemastery": "*",
|
|
80
|
-
"@deepseek-ai/cordis": "*",
|
|
81
79
|
"react": "^18.2.0"
|
|
82
80
|
},
|
|
83
81
|
"dependencies": {
|
|
84
82
|
"clsx": "^2.1.1"
|
|
85
83
|
},
|
|
86
84
|
"devDependencies": {
|
|
87
|
-
"@deepseek-ai/
|
|
88
|
-
"@deepseek-ai/
|
|
89
|
-
"@deepseek-ai/
|
|
90
|
-
"@deepseek-ai/dsh-
|
|
91
|
-
"@deepseek-ai/dsh-
|
|
92
|
-
"@deepseek-ai/dsh-
|
|
93
|
-
"@deepseek-ai/dsh-
|
|
85
|
+
"@deepseek-ai/cordis": "*",
|
|
86
|
+
"@deepseek-ai/cordis-plugin-include": "^1.0.7",
|
|
87
|
+
"@deepseek-ai/cordis-plugin-loader": "^1.0.3",
|
|
88
|
+
"@deepseek-ai/dsh-api-remotes": "0.1.2-alpha.5",
|
|
89
|
+
"@deepseek-ai/dsh-api-session-controller": "0.1.2-alpha.5",
|
|
90
|
+
"@deepseek-ai/dsh-atomic-write": "0.1.2-alpha.5",
|
|
91
|
+
"@deepseek-ai/dsh-client-locale": "0.1.2-alpha.5",
|
|
92
|
+
"@deepseek-ai/dsh-client-store": "0.1.2-alpha.5",
|
|
93
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.1.2-alpha.5",
|
|
94
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.1.2-alpha.5",
|
|
95
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.2-alpha.5",
|
|
96
|
+
"@deepseek-ai/dsh-credentials": "0.1.2-alpha.5",
|
|
97
|
+
"@deepseek-ai/dsh-host-webserver": "0.1.2-alpha.5",
|
|
98
|
+
"@deepseek-ai/dsh-invariants": "0.1.2-alpha.5",
|
|
99
|
+
"@deepseek-ai/dsh-llm": "0.1.2-alpha.5",
|
|
100
|
+
"@deepseek-ai/dsh-scope": "0.1.2-alpha.5",
|
|
101
|
+
"@deepseek-ai/dsh-session": "0.1.2-alpha.5",
|
|
102
|
+
"@deepseek-ai/dsh-session-persistence": "0.1.2-alpha.5",
|
|
103
|
+
"@deepseek-ai/dsh-settings": "0.1.2-alpha.5",
|
|
104
|
+
"@deepseek-ai/dsh-tools": "0.1.2-alpha.5",
|
|
105
|
+
"@deepseek-ai/schemastery": "^3.18.2",
|
|
106
|
+
"@testing-library/dom": "^10.4.1",
|
|
94
107
|
"@testing-library/react": "^16.1.0",
|
|
95
|
-
"@types/react": "~18.3.1",
|
|
96
108
|
"@types/node": "^22.20.0",
|
|
97
|
-
"@
|
|
109
|
+
"@types/react": "~18.3.1",
|
|
110
|
+
"jsdom": "^26.1.0",
|
|
111
|
+
"lightningcss": "^1.32.0",
|
|
98
112
|
"react": "^18.2.0",
|
|
99
113
|
"react-dom": "^18.2.0",
|
|
100
|
-
"
|
|
101
|
-
"
|
|
114
|
+
"tsdown": "^0.22.2",
|
|
115
|
+
"use-sync-external-store": "1.2.0",
|
|
116
|
+
"vitest": "^4.1.8"
|
|
102
117
|
},
|
|
103
118
|
"files": [
|
|
104
119
|
"lib/index.js",
|