@kenz1117/dsh-ui-usage-billing 1.0.28 → 1.0.29
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 +6 -3
- package/README.md +7 -4
- package/lib/client.js +5 -5
- package/lib/index.js +390 -162
- package/lib/types/aggregate.d.ts +41 -13
- package/lib/types/balance.d.ts +0 -20
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/client/pricing.d.ts +6 -8
- package/lib/types/client/provider-display.d.ts +7 -0
- package/lib/types/index.d.ts +24 -1
- package/lib/types/subscriptions.d.ts +2 -0
- package/lib/types/tc3.d.ts +60 -0
- package/package.json +3 -2
package/lib/index.js
CHANGED
|
@@ -2,11 +2,11 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { mkdir, readFile, stat } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
|
+
import { SessionLogOffset } from "@deepseek-ai/dsh-session/types";
|
|
5
6
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
6
7
|
import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
7
8
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
8
9
|
import z from "@deepseek-ai/schemastery";
|
|
9
|
-
import { SessionLogOffset } from "@deepseek-ai/dsh-session/types";
|
|
10
10
|
import { createHash, createHmac } from "node:crypto";
|
|
11
11
|
//#region lib/types/client/plan-knowledge.js
|
|
12
12
|
/**
|
|
@@ -194,6 +194,20 @@ let liveRate;
|
|
|
194
194
|
let livePrices;
|
|
195
195
|
let liveExtraModels;
|
|
196
196
|
/**
|
|
197
|
+
* 用户自定义模型别名(插件配置 `modelKeyAliases`,聚合启动时注入):真实日志
|
|
198
|
+
* model id → 计费目录键。优先级高于内置别名表——目录外的新模型无需等发版,
|
|
199
|
+
* 配置一条别名即完成识别与计价(键必须是 MODEL_CATALOG 的既有 key)。
|
|
200
|
+
*/
|
|
201
|
+
let userModelAliases;
|
|
202
|
+
/**
|
|
203
|
+
* 注入用户自定义模型别名(node 半区在插件启动时调用一次)。纯内存状态:
|
|
204
|
+
* 聚合折叠与客户端渲染共用同一份(两侧一致性由同一注入点保证)。
|
|
205
|
+
* @param aliases - `model id → 目录键` 映射;undefined/空 = 清除,回退内置表。
|
|
206
|
+
*/
|
|
207
|
+
function applyUserModelAliases(aliases) {
|
|
208
|
+
userModelAliases = aliases !== void 0 && Object.keys(aliases).length > 0 ? aliases : void 0;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
197
211
|
* Apply the node half's live pricing snapshot. Absent fields keep the
|
|
198
212
|
* built-in catalog and rate; callers never fabricate values.
|
|
199
213
|
* @param pricing - the `/api/billing/pricing` response.
|
|
@@ -1402,6 +1416,7 @@ const MODEL_KEY_ALIASES = {
|
|
|
1402
1416
|
"command-a": "command-a-03-2025",
|
|
1403
1417
|
"command-r-08-2024": "command-r-08-2024",
|
|
1404
1418
|
"command-r": "command-r-08-2024",
|
|
1419
|
+
"hy3": "hunyuan",
|
|
1405
1420
|
"longcat-2.0": "longcat-2.0",
|
|
1406
1421
|
"longcat-2": "longcat-2.0",
|
|
1407
1422
|
"minicpm-v-4.5": "minicpm-v-4.5",
|
|
@@ -1490,8 +1505,49 @@ const CATALOG_CANON_INDEX = (() => {
|
|
|
1490
1505
|
* @param id - 真实模型 id(日志里出现的形式)。
|
|
1491
1506
|
* @returns 计费目录键。
|
|
1492
1507
|
*/
|
|
1508
|
+
/**
|
|
1509
|
+
* 由一个未知模型 id 派生候选 id(仅当直接查全部未命中时才尝试):
|
|
1510
|
+
* - 剥离组织前缀(`deepseek/deepseek-v4-flash` → `deepseek-v4-flash`);
|
|
1511
|
+
* - 剥离尾部纯数字段(`deepseek-v4-flash-202605` / `-0731` → `deepseek-v4-flash`,
|
|
1512
|
+
* 覆盖 TokenHub / 官方按日期滚动的快照 id);
|
|
1513
|
+
* - 两者组合派生。目录键本身(如 `mistral-large-2512`、`command-a-03-2025`)
|
|
1514
|
+
* 在直接查就已命中,永不进入派生分支,不受剥段影响。
|
|
1515
|
+
*/
|
|
1516
|
+
function derivedKeyCandidates(id) {
|
|
1517
|
+
const out = [];
|
|
1518
|
+
const push = (value) => {
|
|
1519
|
+
if (value !== "" && !out.includes(value)) out.push(value);
|
|
1520
|
+
};
|
|
1521
|
+
const stripTrailingDigits = (value) => {
|
|
1522
|
+
let base = value;
|
|
1523
|
+
for (;;) {
|
|
1524
|
+
const next = base.replace(/[-_]\d{3,}$/u, "");
|
|
1525
|
+
if (next === base || next === "") return;
|
|
1526
|
+
base = next;
|
|
1527
|
+
push(base);
|
|
1528
|
+
}
|
|
1529
|
+
};
|
|
1530
|
+
const slash = id.lastIndexOf("/");
|
|
1531
|
+
if (slash > 0 && slash < id.length - 1) {
|
|
1532
|
+
const bare = id.slice(slash + 1);
|
|
1533
|
+
push(bare);
|
|
1534
|
+
stripTrailingDigits(bare);
|
|
1535
|
+
}
|
|
1536
|
+
stripTrailingDigits(id);
|
|
1537
|
+
return out;
|
|
1538
|
+
}
|
|
1539
|
+
/** 查一个候选 id(用户别名 → 内置别名 → 目录归一化 → models.dev 补充);未命中返回 undefined。 */
|
|
1540
|
+
function lookupCandidate(candidate) {
|
|
1541
|
+
const alias = userModelAliases?.[candidate] ?? MODEL_KEY_ALIASES[candidate];
|
|
1542
|
+
if (alias !== void 0) return alias;
|
|
1543
|
+
const canon = canonModelId(candidate);
|
|
1544
|
+
if (canon === "") return void 0;
|
|
1545
|
+
const hit = CATALOG_CANON_INDEX.get(canon);
|
|
1546
|
+
if (hit !== void 0) return hit;
|
|
1547
|
+
return (liveExtraModels ?? []).find((item) => canonModelId(item.key) === canon)?.key;
|
|
1548
|
+
}
|
|
1493
1549
|
function resolveCatalogKey(id) {
|
|
1494
|
-
const exact = MODEL_KEY_ALIASES[id] ?? id;
|
|
1550
|
+
const exact = userModelAliases?.[id] ?? MODEL_KEY_ALIASES[id] ?? id;
|
|
1495
1551
|
if (exact === id) {
|
|
1496
1552
|
const canon = canonModelId(id);
|
|
1497
1553
|
if (canon !== "") {
|
|
@@ -1500,6 +1556,10 @@ function resolveCatalogKey(id) {
|
|
|
1500
1556
|
const extraHit = (liveExtraModels ?? []).find((item) => canonModelId(item.key) === canon);
|
|
1501
1557
|
if (extraHit !== void 0) return extraHit.key;
|
|
1502
1558
|
}
|
|
1559
|
+
for (const candidate of derivedKeyCandidates(id)) {
|
|
1560
|
+
const hit = lookupCandidate(candidate);
|
|
1561
|
+
if (hit !== void 0) return hit;
|
|
1562
|
+
}
|
|
1503
1563
|
}
|
|
1504
1564
|
return exact;
|
|
1505
1565
|
}
|
|
@@ -1707,9 +1767,9 @@ function formatTokens(value) {
|
|
|
1707
1767
|
/**
|
|
1708
1768
|
* 走订阅套餐(coding / token plan / opencode 订阅)的 provider id:这些通道的
|
|
1709
1769
|
* 调用按套餐计费,不再按 token 计费,因此即使模型 id 与计费表撞名也一律豁免。
|
|
1710
|
-
*
|
|
1711
|
-
*
|
|
1712
|
-
*
|
|
1770
|
+
* 此列表保留为显式配置的参考基线;聚合层缺省改用与订阅卡一致的
|
|
1771
|
+
* `isSubscriptionProviderId` 判定(覆盖本列表与全部 `*-token-plan` / `*-coding` 变体),
|
|
1772
|
+
* 部署仍可在 plugin config 的 `subscriptionProviders` 中显式覆盖。
|
|
1713
1773
|
*/
|
|
1714
1774
|
const DEFAULT_SUBSCRIPTION_PROVIDERS = [
|
|
1715
1775
|
"kimi-coding",
|
|
@@ -1720,7 +1780,8 @@ const DEFAULT_SUBSCRIPTION_PROVIDERS = [
|
|
|
1720
1780
|
"qwen-token-plan-cn",
|
|
1721
1781
|
"xiaomi-token-plan-ams",
|
|
1722
1782
|
"xiaomi-token-plan-cn",
|
|
1723
|
-
"xiaomi-token-plan-sgp"
|
|
1783
|
+
"xiaomi-token-plan-sgp",
|
|
1784
|
+
"tencent-token-plan"
|
|
1724
1785
|
];
|
|
1725
1786
|
/**
|
|
1726
1787
|
* 官方渠道 provider id 判定:`deepseek` 前缀(DeepSeek 官方直连)视为官方,
|
|
@@ -1730,6 +1791,25 @@ const DEFAULT_SUBSCRIPTION_PROVIDERS = [
|
|
|
1730
1791
|
function isOfficialProvider(provider) {
|
|
1731
1792
|
return /^deepseek(?:-[a-z0-9-]+)?$/i.test(provider.trim());
|
|
1732
1793
|
}
|
|
1794
|
+
/** DeepSeek 官方直连端点的归一化 origin(`siteOriginOf` 口径):有 baseURL 的路由只有它算官方。 */
|
|
1795
|
+
const OFFICIAL_DEEPSEEK_ORIGIN = "https://api.deepseek.com";
|
|
1796
|
+
/**
|
|
1797
|
+
* 官方渠道判定(通道优先):显式 `officialProviderIds` 配置最优先;否则看站点归组——
|
|
1798
|
+
* 有 baseURL 的路由只有 origin 为 DeepSeek 官方域才算官方(修复:名为 `deepseek-*`
|
|
1799
|
+
* 的中转/网关路由曾被按 id 前缀误判为官方,腾讯网关的 DeepSeek 全被计成官方渠道);
|
|
1800
|
+
* 直连路由(配置在册、无 baseURL)退回按 provider id 前缀判定;**不在当前配置里的
|
|
1801
|
+
* 未知路由一律不算官方**(无法核实通道,不装确定——历史路由用 `routeAliases` 归位)。
|
|
1802
|
+
*/
|
|
1803
|
+
function officialChannelOf(provider, ref, officialProviderIds) {
|
|
1804
|
+
if (officialProviderIds !== void 0) return officialProviderIds.has(provider);
|
|
1805
|
+
if (ref.kind === "site") return ref.origin === OFFICIAL_DEEPSEEK_ORIGIN;
|
|
1806
|
+
if (ref.kind === "direct") return isOfficialProvider(provider);
|
|
1807
|
+
return false;
|
|
1808
|
+
}
|
|
1809
|
+
/** 一个 provider 是否走订阅套餐计费(豁免按 token 计价)。 */
|
|
1810
|
+
function isSubscriptionCall(subscription, provider) {
|
|
1811
|
+
return typeof subscription === "function" ? subscription(provider) : subscription.has(provider);
|
|
1812
|
+
}
|
|
1733
1813
|
/** 由 baseURL 归一化出站点 origin(协议 + 主机 + 端口);解析失败回退原值。 */
|
|
1734
1814
|
function siteOriginOf(baseURL) {
|
|
1735
1815
|
try {
|
|
@@ -2067,14 +2147,16 @@ function freshFold() {
|
|
|
2067
2147
|
* @param fold - 累计目标(原地修改)。
|
|
2068
2148
|
* @param machine - 折叠状态机:进入时为上一批次末态,返回时为本批次末态。
|
|
2069
2149
|
* @param events - 本批次事件(日志顺序)。
|
|
2070
|
-
* @param subscriptionProviders -
|
|
2071
|
-
* @param officialProviderIds - 官方直连 provider 集合(undefined = 按
|
|
2150
|
+
* @param subscriptionProviders - 订阅套餐匹配器(显式集合或判定函数)。
|
|
2151
|
+
* @param officialProviderIds - 官方直连 provider 集合(undefined = 按 {@link officialChannelOf} 通道判定)。
|
|
2072
2152
|
* @param routes - 当前 provider 路由视图(中转站归组)。
|
|
2073
2153
|
* @param searchCallEstimateCny - 联网搜索请求的单次费用估算(人民币元)。
|
|
2074
2154
|
* @param seedLength - fork 血缘边界(seq 低于它的事件是父会话种子,跳过);
|
|
2075
2155
|
* 来源为存储元数据的 `inheritedEventCount`。
|
|
2156
|
+
* @param routeAliases - 历史路由别名(旧路由名 → 当前路由名),改名/删除路由的
|
|
2157
|
+
* 历史用量按别名归位,不再落「未知路由」桶。
|
|
2076
2158
|
*/
|
|
2077
|
-
function foldInto(fold, machine, events, subscriptionProviders, officialProviderIds, routes, searchCallEstimateCny, seedLength) {
|
|
2159
|
+
function foldInto(fold, machine, events, subscriptionProviders, officialProviderIds, routes, searchCallEstimateCny, seedLength, routeAliases = {}) {
|
|
2078
2160
|
let key = machine.key;
|
|
2079
2161
|
let subscription = machine.subscription;
|
|
2080
2162
|
let official = machine.official;
|
|
@@ -2121,11 +2203,13 @@ function foldInto(fold, machine, events, subscriptionProviders, officialProvider
|
|
|
2121
2203
|
continue;
|
|
2122
2204
|
}
|
|
2123
2205
|
if (event.type === "request/header") {
|
|
2124
|
-
const { model, provider } = event.data.header.config;
|
|
2206
|
+
const { model, provider: rawProvider } = event.data.header.config;
|
|
2207
|
+
const provider = routeAliases[rawProvider] ?? rawProvider;
|
|
2125
2208
|
key = resolveCatalogKey(model);
|
|
2126
|
-
subscription = subscriptionProviders
|
|
2127
|
-
|
|
2128
|
-
|
|
2209
|
+
subscription = isSubscriptionCall(subscriptionProviders, provider);
|
|
2210
|
+
const siteRef = siteRefOf(provider, routes);
|
|
2211
|
+
official = officialChannelOf(provider, siteRef, officialProviderIds);
|
|
2212
|
+
siteBucket = siteBucketKey(siteRef);
|
|
2129
2213
|
if (lastOpenStepKey !== void 0) {
|
|
2130
2214
|
const stepState = steps.get(lastOpenStepKey);
|
|
2131
2215
|
if (stepState !== void 0 && stepState.requestTime === void 0) stepState.requestTime = event.time;
|
|
@@ -2178,10 +2262,12 @@ function foldInto(fold, machine, events, subscriptionProviders, officialProvider
|
|
|
2178
2262
|
if (usage === void 0) continue;
|
|
2179
2263
|
const source = event.data.message?.source;
|
|
2180
2264
|
if (source?.kind === "model" && typeof source.provider === "string" && typeof source.model === "string") {
|
|
2265
|
+
const provider = routeAliases[source.provider] ?? source.provider;
|
|
2181
2266
|
key = resolveCatalogKey(source.model);
|
|
2182
|
-
subscription = subscriptionProviders
|
|
2183
|
-
|
|
2184
|
-
|
|
2267
|
+
subscription = isSubscriptionCall(subscriptionProviders, provider);
|
|
2268
|
+
const siteRef = siteRefOf(provider, routes);
|
|
2269
|
+
official = officialChannelOf(provider, siteRef, officialProviderIds);
|
|
2270
|
+
siteBucket = siteBucketKey(siteRef);
|
|
2185
2271
|
}
|
|
2186
2272
|
const modelKey = key;
|
|
2187
2273
|
const day = dayStamp(event.time);
|
|
@@ -2315,8 +2401,9 @@ function percentile(values, p) {
|
|
|
2315
2401
|
* @returns the aggregator holding the per-session fold cache.
|
|
2316
2402
|
*/
|
|
2317
2403
|
function createUsageAggregator(persistence, options = {}) {
|
|
2318
|
-
const
|
|
2404
|
+
const subscriptionMatcher = options.subscriptionProviders === void 0 ? new Set(DEFAULT_SUBSCRIPTION_PROVIDERS) : new Set(options.subscriptionProviders);
|
|
2319
2405
|
const officialProviderIds = options.officialProviderIds === void 0 ? void 0 : new Set(options.officialProviderIds);
|
|
2406
|
+
const routeAliases = options.routeAliases ?? {};
|
|
2320
2407
|
const maxCacheSessions = options.maxCacheSessions ?? 400;
|
|
2321
2408
|
const cache = /* @__PURE__ */ new Map();
|
|
2322
2409
|
const ledger = /* @__PURE__ */ new Map();
|
|
@@ -2336,7 +2423,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2336
2423
|
id,
|
|
2337
2424
|
...cwd === void 0 ? {} : { cwd },
|
|
2338
2425
|
...stamp === null ? {} : { stamp },
|
|
2339
|
-
foldVersion:
|
|
2426
|
+
foldVersion: 8,
|
|
2340
2427
|
fold: serializeFold(fold)
|
|
2341
2428
|
};
|
|
2342
2429
|
const row = ledger.get(id);
|
|
@@ -2360,9 +2447,15 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2360
2447
|
console.warn("[usage-billing] failed to load durable usage ledger; rebuilding from current sessions:", error);
|
|
2361
2448
|
}
|
|
2362
2449
|
};
|
|
2363
|
-
/**
|
|
2364
|
-
*
|
|
2450
|
+
/** 失效键:宿主 0.1.3+ 走 adapter 暴露的 revision 令牌;0.1.2 形状退回
|
|
2451
|
+
* 日志文件 mtime+size(经 locate,拿不到或抛错时返回 null 让调用方重折)。
|
|
2452
|
+
* locate/stat 调用纳入 try,避免单个会话的异常把整份聚合拖垮。 */
|
|
2365
2453
|
const stampOf = async (meta) => {
|
|
2454
|
+
if (persistence.stampOf !== void 0) try {
|
|
2455
|
+
return await persistence.stampOf(meta.id);
|
|
2456
|
+
} catch {
|
|
2457
|
+
return null;
|
|
2458
|
+
}
|
|
2366
2459
|
try {
|
|
2367
2460
|
const location = persistence.locate?.(meta);
|
|
2368
2461
|
if (location === void 0) return null;
|
|
@@ -2408,7 +2501,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2408
2501
|
try {
|
|
2409
2502
|
const cwd = meta.cwd;
|
|
2410
2503
|
const ledgerRow = ledger.get(id);
|
|
2411
|
-
if (ledgerRow !== void 0 && ledgerRow.stamp !== void 0 && stamp !== null && ledgerRow.stamp === stamp && (ledgerRow.foldVersion ?? 1) ===
|
|
2504
|
+
if (ledgerRow !== void 0 && ledgerRow.stamp !== void 0 && stamp !== null && ledgerRow.stamp === stamp && (ledgerRow.foldVersion ?? 1) === 8) {
|
|
2412
2505
|
const fold = deserializeFold(ledgerRow.fold);
|
|
2413
2506
|
cache.set(id, {
|
|
2414
2507
|
stamp,
|
|
@@ -2436,7 +2529,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2436
2529
|
cache.delete(id);
|
|
2437
2530
|
continue;
|
|
2438
2531
|
}
|
|
2439
|
-
foldInto(previous.fold, previous.machine, events,
|
|
2532
|
+
foldInto(previous.fold, previous.machine, events, subscriptionMatcher, officialProviderIds, routesOf(), searchEstimate, inheritedEventCount, routeAliases);
|
|
2440
2533
|
refreshTurns(previous.fold, previous.machine);
|
|
2441
2534
|
previous.lastSeq = events[events.length - 1]?.seq ?? previous.lastSeq;
|
|
2442
2535
|
previous.stamp = stamp;
|
|
@@ -2457,7 +2550,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2457
2550
|
const fold = freshFold();
|
|
2458
2551
|
const machine = freshMachine();
|
|
2459
2552
|
for (let start = 0; start < events.length; start += FOLD_CHUNK_EVENTS) {
|
|
2460
|
-
foldInto(fold, machine, events.slice(start, start + FOLD_CHUNK_EVENTS),
|
|
2553
|
+
foldInto(fold, machine, events.slice(start, start + FOLD_CHUNK_EVENTS), subscriptionMatcher, officialProviderIds, routesOf(), searchEstimate, inheritedEventCount, routeAliases);
|
|
2461
2554
|
if (start + FOLD_CHUNK_EVENTS < events.length) await new Promise((resolve) => {
|
|
2462
2555
|
setImmediate(resolve);
|
|
2463
2556
|
});
|
|
@@ -2492,7 +2585,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2492
2585
|
for (const entry of ledger.values()) {
|
|
2493
2586
|
if (included.has(entry.id)) continue;
|
|
2494
2587
|
try {
|
|
2495
|
-
const stale = (entry.foldVersion ?? 1) <
|
|
2588
|
+
const stale = (entry.foldVersion ?? 1) < 8;
|
|
2496
2589
|
folds.push({
|
|
2497
2590
|
id: entry.id,
|
|
2498
2591
|
...entry.cwd === void 0 ? {} : { cwd: entry.cwd },
|
|
@@ -2757,6 +2850,186 @@ function createCooldownGate(options = {}) {
|
|
|
2757
2850
|
};
|
|
2758
2851
|
}
|
|
2759
2852
|
//#endregion
|
|
2853
|
+
//#region lib/types/tc3.js
|
|
2854
|
+
/**
|
|
2855
|
+
* Tencent Cloud TokenHub control-plane access (cloud API 3.0, TC3-HMAC-SHA256).
|
|
2856
|
+
*
|
|
2857
|
+
* Shared by two surfaces that read the same Token Plan:
|
|
2858
|
+
* - `balance.ts` — the balance row (absolute remaining quota);
|
|
2859
|
+
* - `subscriptions.ts` — the subscription card (monthly window + plan name).
|
|
2860
|
+
*
|
|
2861
|
+
* Credentials are a cloud API key pair (`<SecretId>:<SecretKey>`), NOT the
|
|
2862
|
+
* TokenHub inference key. Field names on the control plane are not fully
|
|
2863
|
+
* stable, so parsing stays defensive (semantic-key scanning).
|
|
2864
|
+
*/
|
|
2865
|
+
/** TokenHub 管控面 API 端点(cloud.tencent.cn/document/api/1823/132270)。 */
|
|
2866
|
+
const TOKENHUB_HOST = "tokenhub.tencentcloudapi.com";
|
|
2867
|
+
/** 云 API 3.0 产品名与版本(签名的 service 段与请求头都必须一致)。 */
|
|
2868
|
+
const TOKENHUB_SERVICE = "tokenhub";
|
|
2869
|
+
const TOKENHUB_VERSION = "2026-03-22";
|
|
2870
|
+
/** 请求地域:管控面对地域不敏感,取默认国内地域(文档地域列表含 ap-guangzhou)。 */
|
|
2871
|
+
const TOKENHUB_REGION = "ap-guangzhou";
|
|
2872
|
+
/** 管控面调用超时(毫秒):余额与订阅面板共用同一预算。 */
|
|
2873
|
+
const TOKENHUB_TIMEOUT_MS = 8e3;
|
|
2874
|
+
/** 腾讯云凭据引用值格式:`<SecretId>:<SecretKey>`(分隔符取首个冒号)。 */
|
|
2875
|
+
function parseTencentCredential(value) {
|
|
2876
|
+
const sep = value.indexOf(":");
|
|
2877
|
+
if (sep === -1) return void 0;
|
|
2878
|
+
const secretId = value.slice(0, sep).trim();
|
|
2879
|
+
const secretKey = value.slice(sep + 1).trim();
|
|
2880
|
+
if (secretId === "" || secretKey === "") return void 0;
|
|
2881
|
+
return {
|
|
2882
|
+
secretId,
|
|
2883
|
+
secretKey
|
|
2884
|
+
};
|
|
2885
|
+
}
|
|
2886
|
+
/**
|
|
2887
|
+
* 构造云 API 3.0 TC3-HMAC-SHA256 签名(官方签名方法 v3)。导出供测试:纯函数,
|
|
2888
|
+
* 输入确定则签名确定。Action 不参与签名——它走 `X-TC-Action` 请求头。
|
|
2889
|
+
* @param secretId - 云 API SecretId。
|
|
2890
|
+
* @param secretKey - 云 API SecretKey。
|
|
2891
|
+
* @param payload - 已序列化的请求体(含 Action/Version/Region 公共参数)。
|
|
2892
|
+
* @param timestamp - 签名时间戳(秒)。
|
|
2893
|
+
* @returns Authorization 头的值。
|
|
2894
|
+
*/
|
|
2895
|
+
function tc3Authorization(secretId, secretKey, payload, timestamp) {
|
|
2896
|
+
const date = (/* @__PURE__ */ new Date(timestamp * 1e3)).toISOString().slice(0, 10);
|
|
2897
|
+
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")}`;
|
|
2898
|
+
const hashedCanonical = createHash("sha256").update(canonicalRequest).digest("hex");
|
|
2899
|
+
const stringToSign = `TC3-HMAC-SHA256\n${String(timestamp)}\n${date}/${TOKENHUB_SERVICE}/tc3_request\n${hashedCanonical}`;
|
|
2900
|
+
const kDate = createHmac("sha256", date).update(secretKey).digest();
|
|
2901
|
+
const kService = createHmac("sha256", kDate).update(TOKENHUB_SERVICE).digest();
|
|
2902
|
+
const kSigning = createHmac("sha256", kService).update("tc3_request").digest();
|
|
2903
|
+
const signature = createHmac("sha256", kSigning).update(stringToSign).digest("hex");
|
|
2904
|
+
return `TC3-HMAC-SHA256 Credential=${secretId}/${date}/${TOKENHUB_SERVICE}/tc3_request, SignedHeaders=content-type;host, Signature=${signature}`;
|
|
2905
|
+
}
|
|
2906
|
+
/**
|
|
2907
|
+
* 调用一次 TokenHub 管控面接口:TC3 签名 + 超时保护,返回响应 JSON 的 `Response`。
|
|
2908
|
+
* 业务错误(Response.Error)与 HTTP 层错误都带 `code` 抛出,调用方按
|
|
2909
|
+
* unauthorized / unreachable / invalid 归类。
|
|
2910
|
+
*/
|
|
2911
|
+
async function callTokenHub(secretId, secretKey, action, params) {
|
|
2912
|
+
const payload = JSON.stringify({
|
|
2913
|
+
Action: action,
|
|
2914
|
+
Version: TOKENHUB_VERSION,
|
|
2915
|
+
Region: TOKENHUB_REGION,
|
|
2916
|
+
...params
|
|
2917
|
+
});
|
|
2918
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
2919
|
+
const controller = new AbortController();
|
|
2920
|
+
const timer = setTimeout(() => controller.abort(), TOKENHUB_TIMEOUT_MS);
|
|
2921
|
+
try {
|
|
2922
|
+
const response = await fetch(`https://${TOKENHUB_HOST}/`, {
|
|
2923
|
+
method: "POST",
|
|
2924
|
+
headers: {
|
|
2925
|
+
"content-type": "application/json; charset=utf-8",
|
|
2926
|
+
host: TOKENHUB_HOST,
|
|
2927
|
+
"x-tc-action": action.toLowerCase(),
|
|
2928
|
+
"x-tc-version": TOKENHUB_VERSION,
|
|
2929
|
+
"x-tc-region": TOKENHUB_REGION,
|
|
2930
|
+
"x-tc-timestamp": String(timestamp),
|
|
2931
|
+
authorization: tc3Authorization(secretId, secretKey, payload, timestamp)
|
|
2932
|
+
},
|
|
2933
|
+
body: payload,
|
|
2934
|
+
signal: controller.signal
|
|
2935
|
+
});
|
|
2936
|
+
if (response.status === 401 || response.status === 403) throw Object.assign(/* @__PURE__ */ new Error("unauthorized"), { code: "unauthorized" });
|
|
2937
|
+
if (!response.ok) throw Object.assign(/* @__PURE__ */ new Error(`HTTP ${String(response.status)}`), {
|
|
2938
|
+
httpStatus: response.status,
|
|
2939
|
+
code: "unreachable"
|
|
2940
|
+
});
|
|
2941
|
+
const inner = (await response.json()).Response;
|
|
2942
|
+
if (inner === void 0) throw Object.assign(/* @__PURE__ */ new Error("no Response envelope"), { code: "invalid" });
|
|
2943
|
+
if (inner.Error !== void 0 && inner.Error !== null) {
|
|
2944
|
+
const err = inner.Error;
|
|
2945
|
+
const code = err.Code === "AuthFailure.SignatureFailure" || err.Code === "AuthFailure.SecretIdNotFound" ? "unauthorized" : "unreachable";
|
|
2946
|
+
throw Object.assign(new Error(String(err.Code ?? "api-error")), { code });
|
|
2947
|
+
}
|
|
2948
|
+
return inner;
|
|
2949
|
+
} finally {
|
|
2950
|
+
clearTimeout(timer);
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
/** 数字归一化:上游可能给字符串数字;非有限数返回 undefined。 */
|
|
2954
|
+
function toNumber$1(value) {
|
|
2955
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
2956
|
+
if (typeof value === "string") {
|
|
2957
|
+
const parsed = Number(value);
|
|
2958
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
/**
|
|
2962
|
+
* 在套餐余量对象里防御性提取「剩余额度」:官方 SubPackageBalance/PackageInfo
|
|
2963
|
+
* 的字段名未稳定公开(issue #18 调研期),按语义键名扫描——命中 remaining /
|
|
2964
|
+
* balance / left 语义键直接用;命中 total 与 used 则相减推导。数字一律经
|
|
2965
|
+
* {@link toNumber} 归一化(上游可能给字符串)。
|
|
2966
|
+
* 导出供测试:纯函数。
|
|
2967
|
+
* @param source - 套餐详情里的余量对象(PackageInfo / SubPackageBalance 等)。
|
|
2968
|
+
* @returns 剩余额度(上游单位,通常为 token 数或元);提取不到返回 undefined。
|
|
2969
|
+
*/
|
|
2970
|
+
function pickRemainingQuota(source) {
|
|
2971
|
+
if (source === null || typeof source !== "object") return void 0;
|
|
2972
|
+
const numeric = Object.entries(source).filter(([, v]) => v !== null && toNumber$1(v) !== void 0);
|
|
2973
|
+
const byKey = (needles) => {
|
|
2974
|
+
for (const [key, value] of numeric) {
|
|
2975
|
+
const lower = key.toLowerCase();
|
|
2976
|
+
if (needles.some((n) => lower.includes(n))) {
|
|
2977
|
+
const num = toNumber$1(value);
|
|
2978
|
+
if (num !== void 0 && num >= 0) return num;
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
};
|
|
2982
|
+
const remaining = byKey([
|
|
2983
|
+
"remain",
|
|
2984
|
+
"balance",
|
|
2985
|
+
"left",
|
|
2986
|
+
"available"
|
|
2987
|
+
]);
|
|
2988
|
+
if (remaining !== void 0) return remaining;
|
|
2989
|
+
const total = byKey(["total"]);
|
|
2990
|
+
const used = byKey(["used", "consume"]);
|
|
2991
|
+
if (total !== void 0 && used !== void 0) return Math.max(0, total - used);
|
|
2992
|
+
}
|
|
2993
|
+
/**
|
|
2994
|
+
* 防御性提取「总额度」:语义键 total / limit / quota。与
|
|
2995
|
+
* {@link pickRemainingQuota} 配对使用——两者都能取到时订阅卡才能算出
|
|
2996
|
+
* 已用百分比窗口;只有剩余值时窗口保持为空(绝不猜总额度)。
|
|
2997
|
+
* 注意排除键名:`RemainingQuota` / `UsedQuota` 这类键同样含 quota 子串,
|
|
2998
|
+
* 必须先剔除剩余 / 已用语义,否则会把剩余值误当总额度(有单测守卫)。
|
|
2999
|
+
* @param source - 套餐详情里的额度对象(PackageInfo / SubPackageBalance 等)。
|
|
3000
|
+
*/
|
|
3001
|
+
function pickTotalQuota(source) {
|
|
3002
|
+
if (source === null || typeof source !== "object") return void 0;
|
|
3003
|
+
const forbidden = [
|
|
3004
|
+
"remain",
|
|
3005
|
+
"used",
|
|
3006
|
+
"consume",
|
|
3007
|
+
"left",
|
|
3008
|
+
"available",
|
|
3009
|
+
"balance"
|
|
3010
|
+
];
|
|
3011
|
+
for (const [key, value] of Object.entries(source)) {
|
|
3012
|
+
const lower = key.toLowerCase();
|
|
3013
|
+
if (!lower.includes("total") && !lower.includes("limit") && !lower.includes("quota")) continue;
|
|
3014
|
+
if (forbidden.some((marker) => lower.includes(marker))) continue;
|
|
3015
|
+
const num = toNumber$1(value);
|
|
3016
|
+
if (num !== void 0 && num > 0) return num;
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
/** 套餐列表里提取第一个启用套餐的 TeamId:集合字段名做候选兼容。 */
|
|
3020
|
+
function firstEnabledTeamId(inner) {
|
|
3021
|
+
const candidates = inner.TeamSet ?? inner.TokenPlanSet ?? inner.PlanSet;
|
|
3022
|
+
if (!Array.isArray(candidates)) return void 0;
|
|
3023
|
+
for (const item of candidates) {
|
|
3024
|
+
if (item === null || typeof item !== "object") continue;
|
|
3025
|
+
const row = item;
|
|
3026
|
+
if (row.TeamId === void 0 && row.PlanId === void 0) continue;
|
|
3027
|
+
if (row.Status !== void 0 && row.Status !== "enable") continue;
|
|
3028
|
+
const id = row.TeamId ?? row.PlanId;
|
|
3029
|
+
if (typeof id === "string" && id !== "") return id;
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
//#endregion
|
|
2760
3033
|
//#region lib/types/balance.js
|
|
2761
3034
|
/**
|
|
2762
3035
|
* Account-balance queries for the billing dashboard.
|
|
@@ -2801,13 +3074,6 @@ const XAI_CREDITS_URL = "https://api.x.ai/v1/billing/credits";
|
|
|
2801
3074
|
/** 智谱 GLM(大模型国内域)官方余额接口(open.bigmodel.cn/api/paas/v4/balance)。 */
|
|
2802
3075
|
const ZHIPU_BALANCE_URL = "https://open.bigmodel.cn/api/paas/v4/balance";
|
|
2803
3076
|
/** 数字归一化:接口返回的余额是字符串(如 `"110.00"`),统一转 number。 */
|
|
2804
|
-
function toNumber$1(value) {
|
|
2805
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
2806
|
-
if (typeof value === "string") {
|
|
2807
|
-
const parsed = Number(value);
|
|
2808
|
-
return Number.isFinite(parsed) ? parsed : void 0;
|
|
2809
|
-
}
|
|
2810
|
-
}
|
|
2811
3077
|
/**
|
|
2812
3078
|
* Fetch a Bearer-protected balance endpoint and normalize the HTTP outcome into
|
|
2813
3079
|
* a shared {@link ProviderBalance} row. Each provider supplies its own
|
|
@@ -3144,133 +3410,6 @@ async function queryBalances(ctx, providers) {
|
|
|
3144
3410
|
return querier(ctx, env);
|
|
3145
3411
|
}));
|
|
3146
3412
|
}
|
|
3147
|
-
/** TokenHub 管控面 API 端点(cloud.tencent.cn/document/api/1823/132270)。 */
|
|
3148
|
-
const TOKENHUB_HOST = "tokenhub.tencentcloudapi.com";
|
|
3149
|
-
/** 云 API 3.0 产品名与版本(签名的 service 段与请求头都必须一致)。 */
|
|
3150
|
-
const TOKENHUB_SERVICE = "tokenhub";
|
|
3151
|
-
const TOKENHUB_VERSION = "2026-03-22";
|
|
3152
|
-
/** 请求地域:管控面对地域不敏感,取默认国内地域(文档地域列表含 ap-guangzhou)。 */
|
|
3153
|
-
const TOKENHUB_REGION = "ap-guangzhou";
|
|
3154
|
-
/** 腾讯云凭据引用值格式:`<SecretId>:<SecretKey>`(分隔符取首个冒号)。 */
|
|
3155
|
-
function parseTencentCredential(value) {
|
|
3156
|
-
const sep = value.indexOf(":");
|
|
3157
|
-
if (sep === -1) return void 0;
|
|
3158
|
-
const secretId = value.slice(0, sep).trim();
|
|
3159
|
-
const secretKey = value.slice(sep + 1).trim();
|
|
3160
|
-
if (secretId === "" || secretKey === "") return void 0;
|
|
3161
|
-
return {
|
|
3162
|
-
secretId,
|
|
3163
|
-
secretKey
|
|
3164
|
-
};
|
|
3165
|
-
}
|
|
3166
|
-
/**
|
|
3167
|
-
* 构造云 API 3.0 TC3-HMAC-SHA256 签名(官方签名方法 v3)。导出供测试:纯函数,
|
|
3168
|
-
* 输入确定则签名确定。Action 不参与签名——它走 `X-TC-Action` 请求头。
|
|
3169
|
-
* @param secretId - 云 API SecretId。
|
|
3170
|
-
* @param secretKey - 云 API SecretKey。
|
|
3171
|
-
* @param payload - 已序列化的请求体(含 Action/Version/Region 公共参数)。
|
|
3172
|
-
* @param timestamp - 签名时间戳(秒)。
|
|
3173
|
-
* @returns Authorization 头的值。
|
|
3174
|
-
*/
|
|
3175
|
-
function tc3Authorization(secretId, secretKey, payload, timestamp) {
|
|
3176
|
-
const date = (/* @__PURE__ */ new Date(timestamp * 1e3)).toISOString().slice(0, 10);
|
|
3177
|
-
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")}`;
|
|
3178
|
-
const hashedCanonical = createHash("sha256").update(canonicalRequest).digest("hex");
|
|
3179
|
-
const stringToSign = `TC3-HMAC-SHA256\n${String(timestamp)}\n${date}/${TOKENHUB_SERVICE}/tc3_request\n${hashedCanonical}`;
|
|
3180
|
-
const kDate = createHmac("sha256", date).update(secretKey).digest();
|
|
3181
|
-
const kService = createHmac("sha256", kDate).update(TOKENHUB_SERVICE).digest();
|
|
3182
|
-
const kSigning = createHmac("sha256", kService).update("tc3_request").digest();
|
|
3183
|
-
const signature = createHmac("sha256", kSigning).update(stringToSign).digest("hex");
|
|
3184
|
-
return `TC3-HMAC-SHA256 Credential=${secretId}/${date}/${TOKENHUB_SERVICE}/tc3_request, SignedHeaders=content-type;host, Signature=${signature}`;
|
|
3185
|
-
}
|
|
3186
|
-
/** 调用一次 TokenHub 管控面接口:TC3 签名 + 超时保护,返回响应 JSON 的 `Response`。 */
|
|
3187
|
-
async function callTokenHub(secretId, secretKey, action, params) {
|
|
3188
|
-
const payload = JSON.stringify({
|
|
3189
|
-
Action: action,
|
|
3190
|
-
Version: TOKENHUB_VERSION,
|
|
3191
|
-
Region: TOKENHUB_REGION,
|
|
3192
|
-
...params
|
|
3193
|
-
});
|
|
3194
|
-
const timestamp = Math.floor(Date.now() / 1e3);
|
|
3195
|
-
const controller = new AbortController();
|
|
3196
|
-
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS$3);
|
|
3197
|
-
try {
|
|
3198
|
-
const response = await fetch(`https://${TOKENHUB_HOST}/`, {
|
|
3199
|
-
method: "POST",
|
|
3200
|
-
headers: {
|
|
3201
|
-
"content-type": "application/json; charset=utf-8",
|
|
3202
|
-
host: TOKENHUB_HOST,
|
|
3203
|
-
"x-tc-action": action.toLowerCase(),
|
|
3204
|
-
"x-tc-version": TOKENHUB_VERSION,
|
|
3205
|
-
"x-tc-region": TOKENHUB_REGION,
|
|
3206
|
-
"x-tc-timestamp": String(timestamp),
|
|
3207
|
-
authorization: tc3Authorization(secretId, secretKey, payload, timestamp)
|
|
3208
|
-
},
|
|
3209
|
-
body: payload,
|
|
3210
|
-
signal: controller.signal
|
|
3211
|
-
});
|
|
3212
|
-
if (response.status === 401 || response.status === 403) throw Object.assign(/* @__PURE__ */ new Error("unauthorized"), { code: "unauthorized" });
|
|
3213
|
-
if (!response.ok) throw Object.assign(/* @__PURE__ */ new Error(`HTTP ${String(response.status)}`), {
|
|
3214
|
-
httpStatus: response.status,
|
|
3215
|
-
code: "unreachable"
|
|
3216
|
-
});
|
|
3217
|
-
const inner = (await response.json()).Response;
|
|
3218
|
-
if (inner === void 0) throw Object.assign(/* @__PURE__ */ new Error("no Response envelope"), { code: "invalid" });
|
|
3219
|
-
if (inner.Error !== void 0 && inner.Error !== null) {
|
|
3220
|
-
const err = inner.Error;
|
|
3221
|
-
const code = err.Code === "AuthFailure.SignatureFailure" || err.Code === "AuthFailure.SecretIdNotFound" ? "unauthorized" : "unreachable";
|
|
3222
|
-
throw Object.assign(new Error(String(err.Code ?? "api-error")), { code });
|
|
3223
|
-
}
|
|
3224
|
-
return inner;
|
|
3225
|
-
} finally {
|
|
3226
|
-
clearTimeout(timer);
|
|
3227
|
-
}
|
|
3228
|
-
}
|
|
3229
|
-
/**
|
|
3230
|
-
* 在套餐余量对象里防御性提取「剩余额度」:官方 SubPackageBalance/PackageInfo
|
|
3231
|
-
* 的字段名未稳定公开(issue #18 调研期),按语义键名扫描——命中 remaining /
|
|
3232
|
-
* balance / left 语义键直接用;命中 total 与 used 则相减推导。数字一律经
|
|
3233
|
-
* {@link toNumber} 归一化(上游可能给字符串)。
|
|
3234
|
-
* 导出供测试:纯函数。
|
|
3235
|
-
* @param source - 套餐详情里的余量对象(PackageInfo / SubPackageBalance 等)。
|
|
3236
|
-
* @returns 剩余额度(上游单位,通常为 token 数或元);提取不到返回 undefined。
|
|
3237
|
-
*/
|
|
3238
|
-
function pickRemainingQuota(source) {
|
|
3239
|
-
if (source === null || typeof source !== "object") return void 0;
|
|
3240
|
-
const numeric = Object.entries(source).filter(([, v]) => v !== null && toNumber$1(v) !== void 0);
|
|
3241
|
-
const byKey = (needles) => {
|
|
3242
|
-
for (const [key, value] of numeric) {
|
|
3243
|
-
const lower = key.toLowerCase();
|
|
3244
|
-
if (needles.some((n) => lower.includes(n))) {
|
|
3245
|
-
const num = toNumber$1(value);
|
|
3246
|
-
if (num !== void 0 && num >= 0) return num;
|
|
3247
|
-
}
|
|
3248
|
-
}
|
|
3249
|
-
};
|
|
3250
|
-
const remaining = byKey([
|
|
3251
|
-
"remain",
|
|
3252
|
-
"balance",
|
|
3253
|
-
"left",
|
|
3254
|
-
"available"
|
|
3255
|
-
]);
|
|
3256
|
-
if (remaining !== void 0) return remaining;
|
|
3257
|
-
const total = byKey(["total"]);
|
|
3258
|
-
const used = byKey(["used", "consume"]);
|
|
3259
|
-
if (total !== void 0 && used !== void 0) return Math.max(0, total - used);
|
|
3260
|
-
}
|
|
3261
|
-
/** 套餐列表里提取第一个启用套餐的 TeamId:集合字段名做候选兼容。 */
|
|
3262
|
-
function firstEnabledTeamId(inner) {
|
|
3263
|
-
const candidates = inner.TeamSet ?? inner.TokenPlanSet ?? inner.PlanSet;
|
|
3264
|
-
if (!Array.isArray(candidates)) return void 0;
|
|
3265
|
-
for (const item of candidates) {
|
|
3266
|
-
if (item === null || typeof item !== "object") continue;
|
|
3267
|
-
const row = item;
|
|
3268
|
-
if (row.TeamId === void 0 && row.PlanId === void 0) continue;
|
|
3269
|
-
if (row.Status !== void 0 && row.Status !== "enable") continue;
|
|
3270
|
-
const id = row.TeamId ?? row.PlanId;
|
|
3271
|
-
if (typeof id === "string" && id !== "") return id;
|
|
3272
|
-
}
|
|
3273
|
-
}
|
|
3274
3413
|
/**
|
|
3275
3414
|
* 查询腾讯云 TokenHub Token Plan 套餐余量。凭据值格式 `<SecretId>:<SecretKey>`
|
|
3276
3415
|
* (云 API 密钥,非 TokenHub 推理 key)。链路:套餐列表取 TeamId → 套餐详情读
|
|
@@ -4158,6 +4297,7 @@ const EMPTY_SUBSCRIPTION_KEYS = {
|
|
|
4158
4297
|
opencodeApiKey: "",
|
|
4159
4298
|
minmaxApiKey: "",
|
|
4160
4299
|
openrouterApiKey: "",
|
|
4300
|
+
tencentCloudApi: "",
|
|
4161
4301
|
zaiRegion: "global"
|
|
4162
4302
|
};
|
|
4163
4303
|
/** 订阅类 provider 的显示名(未命中的回退为 id 本身)。 */
|
|
@@ -4182,7 +4322,8 @@ const SUBSCRIPTION_DISPLAY_NAMES = {
|
|
|
4182
4322
|
"minimax-token-plan": "MiniMax Token Plan",
|
|
4183
4323
|
"minimax-token-plan-cn": "MiniMax Token Plan(国内)",
|
|
4184
4324
|
"minimax-cn": "MiniMax Token Plan(国内)",
|
|
4185
|
-
"openrouter": "OpenRouter"
|
|
4325
|
+
"openrouter": "OpenRouter",
|
|
4326
|
+
"tencent-token-plan": "腾讯云 Token Plan"
|
|
4186
4327
|
};
|
|
4187
4328
|
/** 订阅类 provider id 判定:带 coding / agent-plan / token-plan 后缀,或已知订阅通道。 */
|
|
4188
4329
|
const SUBSCRIPTION_ID_RE = /* @__PURE__ */ new RegExp("(?:^|-)(?:coding|agent[-_]?plan|token[-_]?plan)(?:$|-|_)|^(?:opencode|opencode-go|kimi-coding|zai-coding|minimax|minimax-cn|minimax-token-plan|minimax-token-plan-cn|openrouter)", "i");
|
|
@@ -4201,7 +4342,8 @@ const SUBSCRIPTION_ADAPTERS = {
|
|
|
4201
4342
|
"minimax-cn": { collect: collectMiniMax },
|
|
4202
4343
|
"minimax-token-plan": { collect: collectMiniMax },
|
|
4203
4344
|
"minimax-token-plan-cn": { collect: collectMiniMax },
|
|
4204
|
-
"openrouter": { collect: collectOpenRouter }
|
|
4345
|
+
"openrouter": { collect: collectOpenRouter },
|
|
4346
|
+
"tencent-token-plan": { collect: collectTencentTokenPlan }
|
|
4205
4347
|
};
|
|
4206
4348
|
/** 有额度适配器的 provider id 集合(识别用)。 */
|
|
4207
4349
|
const ADAPTER_PROVIDER_IDS = new Set(Object.keys(SUBSCRIPTION_ADAPTERS));
|
|
@@ -4698,6 +4840,48 @@ async function collectOpenRouter(keys, config, timeoutMs) {
|
|
|
4698
4840
|
}
|
|
4699
4841
|
}
|
|
4700
4842
|
/**
|
|
4843
|
+
* 腾讯云 Token Plan(TokenHub 管控面)订阅额度。凭据是云 API 密钥对
|
|
4844
|
+
* `<SecretId>:<SecretKey>`(与余额面板同源、同格式),链路与 balance.ts 的
|
|
4845
|
+
* TokenHub 查询一致:DescribeTokenPlanList 取 TeamId → DescribeTokenPlan 读
|
|
4846
|
+
* 主额度包。管控面字段名未完全稳定:剩余与总额度都能取到才产出百分比窗口,
|
|
4847
|
+
* 只有剩余值时窗口留空(绝不猜总额度),plan 名照常展示。
|
|
4848
|
+
*/
|
|
4849
|
+
async function collectTencentTokenPlan(keys, _config, _timeoutMs) {
|
|
4850
|
+
const provider = "tencent-token-plan";
|
|
4851
|
+
const displayName = SUBSCRIPTION_DISPLAY_NAMES[provider] ?? provider;
|
|
4852
|
+
const hint = "凭据需为腾讯云云 API 密钥对(SecretId:SecretKey,控制台访问管理获取),非 TokenHub 推理 key";
|
|
4853
|
+
const quota = (partial) => ({
|
|
4854
|
+
provider,
|
|
4855
|
+
displayName,
|
|
4856
|
+
windows: [],
|
|
4857
|
+
hint,
|
|
4858
|
+
...partial
|
|
4859
|
+
});
|
|
4860
|
+
if (keys.tencentCloudApi === "") return quota({ status: "not-configured" });
|
|
4861
|
+
const credential = parseTencentCredential(keys.tencentCloudApi);
|
|
4862
|
+
if (credential === void 0) return quota({ status: "unauthorized" });
|
|
4863
|
+
try {
|
|
4864
|
+
const teamId = firstEnabledTeamId(await callTokenHub(credential.secretId, credential.secretKey, "DescribeTokenPlanList", {}));
|
|
4865
|
+
if (teamId === void 0) return quota({ status: "invalid-response" });
|
|
4866
|
+
const detail = await callTokenHub(credential.secretId, credential.secretKey, "DescribeTokenPlan", { TeamId: teamId });
|
|
4867
|
+
const remaining = pickRemainingQuota(detail.PackageInfo) ?? pickRemainingQuota(detail);
|
|
4868
|
+
const total = pickTotalQuota(detail.PackageInfo) ?? pickTotalQuota(detail);
|
|
4869
|
+
const plan = typeof detail.Name === "string" ? detail.Name : void 0;
|
|
4870
|
+
return quota({
|
|
4871
|
+
status: "ok",
|
|
4872
|
+
windows: remaining !== void 0 && total !== void 0 && total > 0 ? [{
|
|
4873
|
+
kind: "monthly",
|
|
4874
|
+
usedPercent: round1$1(clampPercent$1((1 - remaining / total) * 100) ?? 0),
|
|
4875
|
+
remainingPercent: round1$1(clampPercent$1(remaining / total * 100) ?? 0),
|
|
4876
|
+
remaining
|
|
4877
|
+
}] : [],
|
|
4878
|
+
...plan !== void 0 ? { plan } : {}
|
|
4879
|
+
});
|
|
4880
|
+
} catch (error) {
|
|
4881
|
+
return quota({ status: statusOf$1(error) });
|
|
4882
|
+
}
|
|
4883
|
+
}
|
|
4884
|
+
/**
|
|
4701
4885
|
* Collect quota for the given plans concurrently (adapter-backed plans only;
|
|
4702
4886
|
* identified plans without an adapter are surfaced by the caller as "no
|
|
4703
4887
|
* quota API" rows).
|
|
@@ -5249,6 +5433,10 @@ const SUBSCRIPTION_KEY_SOURCES = [
|
|
|
5249
5433
|
{
|
|
5250
5434
|
provider: "openrouter",
|
|
5251
5435
|
key: "openrouterApiKey"
|
|
5436
|
+
},
|
|
5437
|
+
{
|
|
5438
|
+
provider: "tencent-token-plan",
|
|
5439
|
+
key: "tencentCloudApi"
|
|
5252
5440
|
}
|
|
5253
5441
|
];
|
|
5254
5442
|
/** 读 llm-pi-ai 设置的 `providers` 字典(`<route> → { apiKeyEnv?, baseURL?, displayName? }`)。
|
|
@@ -5372,6 +5560,44 @@ async function readOpenCodeToken() {
|
|
|
5372
5560
|
return "";
|
|
5373
5561
|
}
|
|
5374
5562
|
/**
|
|
5563
|
+
* 宿主 persistence 形状适配。宿主 0.1.3 起 SessionPersistence 改为
|
|
5564
|
+
* SessionHandle 模型(open(id,'read') 后经 handle.read(offset) 读,fork 边界
|
|
5565
|
+
* 挂在 handle.inheritedEventCount,list 返回 {header, revision} 快照行,
|
|
5566
|
+
* 0.1.2 的 readFrom/locate 消失)。这里按结构探测把两种宿主形状都收敛为
|
|
5567
|
+
* 聚合层期望的 0.1.2 面貌:0.1.2 直接带 readFrom 的原样直通;0.1.3 的
|
|
5568
|
+
* 读取转为 open → handle.read,revision 令牌经 stampOf 暴露给增量缓存。
|
|
5569
|
+
* 候选时刻的运行时对象是宿主注入的外部形状,结构断言即 durable 收窄点。
|
|
5570
|
+
* 导出供测试:纯函数(不触 ctx)。
|
|
5571
|
+
*/
|
|
5572
|
+
function adaptSessionPersistence(raw) {
|
|
5573
|
+
if (typeof raw?.readFrom === "function") return raw;
|
|
5574
|
+
const host = raw;
|
|
5575
|
+
const revisions = /* @__PURE__ */ new Map();
|
|
5576
|
+
return {
|
|
5577
|
+
list: async () => {
|
|
5578
|
+
const snapshots = await host.list();
|
|
5579
|
+
revisions.clear();
|
|
5580
|
+
for (const snapshot of snapshots) revisions.set(String(snapshot.header.id), String(snapshot.revision));
|
|
5581
|
+
return snapshots.map((snapshot) => snapshot.header);
|
|
5582
|
+
},
|
|
5583
|
+
readFrom: async (id, fromSeq) => {
|
|
5584
|
+
const handle = await host.open(id, "read");
|
|
5585
|
+
try {
|
|
5586
|
+
const events = await handle.read(fromSeq);
|
|
5587
|
+
return {
|
|
5588
|
+
meta: handle.header,
|
|
5589
|
+
fromSeq: SessionLogOffset(fromSeq),
|
|
5590
|
+
inheritedEventCount: SessionLogOffset(handle.inheritedEventCount),
|
|
5591
|
+
events
|
|
5592
|
+
};
|
|
5593
|
+
} finally {
|
|
5594
|
+
await handle[Symbol.asyncDispose]?.();
|
|
5595
|
+
}
|
|
5596
|
+
},
|
|
5597
|
+
stampOf: async (id) => revisions.get(String(id)) ?? null
|
|
5598
|
+
};
|
|
5599
|
+
}
|
|
5600
|
+
/**
|
|
5375
5601
|
* Host plugin body: serve real aggregated usage to the browser dashboard.
|
|
5376
5602
|
* @param ctx - host context carrying webServer and sessionPersistence.
|
|
5377
5603
|
* @param config - optional statsPath override.
|
|
@@ -5401,8 +5627,10 @@ function apply(ctx, config = {}) {
|
|
|
5401
5627
|
}).catch(() => {});
|
|
5402
5628
|
};
|
|
5403
5629
|
const workspaceTitleResolver = buildWorkspaceTitleResolver(ctx);
|
|
5404
|
-
|
|
5630
|
+
applyUserModelAliases(config.modelKeyAliases);
|
|
5631
|
+
const aggregator = createUsageAggregator(adaptSessionPersistence(ctx.sessionPersistence), {
|
|
5405
5632
|
...config.subscriptionProviders === void 0 ? {} : { subscriptionProviders: config.subscriptionProviders },
|
|
5633
|
+
...config.routeAliases === void 0 ? {} : { routeAliases: config.routeAliases },
|
|
5406
5634
|
resolveRoutes: () => readPiAiProviderRoutes(ctx.settings),
|
|
5407
5635
|
...workspaceTitleResolver === void 0 ? {} : { resolveWorkspaceTitle: workspaceTitleResolver },
|
|
5408
5636
|
...config.searchCallEstimateCny === void 0 ? {} : { searchCallEstimateCny: config.searchCallEstimateCny },
|
|
@@ -5873,4 +6101,4 @@ function apply(ctx, config = {}) {
|
|
|
5873
6101
|
}), "usage-billing: usage-stats route");
|
|
5874
6102
|
}
|
|
5875
6103
|
//#endregion
|
|
5876
|
-
export { apply, createFileUsageLedgerStore, guardLoopback, inject, readPiAiProviderRoutes, resolveSubscriptionKeys };
|
|
6104
|
+
export { adaptSessionPersistence, apply, createFileUsageLedgerStore, guardLoopback, inject, readPiAiProviderRoutes, resolveSubscriptionKeys };
|