dsh-cost-meter 1.7.13 → 1.7.15
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 +12 -8
- package/README.md +12 -8
- package/lib/backfill.js +18 -14
- package/lib/client.js +2 -2
- package/lib/coding-plans.js +16 -3
- package/lib/gateway-quota-adapters.js +6 -2
- package/lib/index.js +57 -5
- package/lib/plan-billing.js +2 -2
- package/lib/pricing.js +25 -7
- package/lib/store.js +35 -0
- package/lib/typert.host.js +2 -0
- package/package.json +11 -4
package/lib/coding-plans.js
CHANGED
|
@@ -893,17 +893,27 @@ const QWEN_CREDIT_RATES_BY_CANON = Object.fromEntries(
|
|
|
893
893
|
Object.entries(QWEN_CREDIT_RATES).map(([id, rate]) => [scnetCanonModelId(id), rate]),
|
|
894
894
|
)
|
|
895
895
|
|
|
896
|
+
// 只识别明确的订阅渠道;qianwen/dashscope 等按量渠道不能仅凭同名模型归入。
|
|
897
|
+
export const QWEN_TOKEN_PLAN_PROVIDER_IDS = ['qwen', 'qwen-tokenplan', 'qianwen-tokenplan', 'qwen-token-plan', 'qianwen-token-plan']
|
|
898
|
+
|
|
899
|
+
export function isQwenTokenPlanProvider(provider) {
|
|
900
|
+
const name = String(provider ?? '').trim().toLowerCase().replace(/^llm-/, '')
|
|
901
|
+
return QWEN_TOKEN_PLAN_PROVIDER_IDS.includes(name)
|
|
902
|
+
}
|
|
903
|
+
|
|
896
904
|
/**
|
|
897
905
|
* 汇总本地账本当前计费周期(自然月,与 SCNet 同款 scnetPlanPeriod)的千问
|
|
898
906
|
* Credits 用量。entry 为 codingPlans.qwen 配置:{ planCredits(必填,月度总额),
|
|
899
907
|
* planStart(可选,计费周期锚日), rates(可选,模型名 → 抵扣率覆盖,键为归一模型名) }。
|
|
908
|
+
* 仅汇总千问订阅 provider;includeCall 可进一步按 Plan/API 显式配置排除调用。
|
|
900
909
|
* 返回 null(planCredits 非法)或 { used, total, percent, resetsAt, byModel, windows }。
|
|
901
910
|
*/
|
|
902
|
-
export function qwenTokenPlanWindows(days, entry, nowMs) {
|
|
911
|
+
export function qwenTokenPlanWindows(days, entry, nowMs, includeCall) {
|
|
903
912
|
const total = Number(entry?.planCredits)
|
|
904
913
|
if (!Number.isFinite(total) || total <= 0) return null
|
|
905
914
|
// 用户覆盖的抵扣率优先,内置表兜底;键与模型名均做字母数字归一比较。
|
|
906
|
-
const
|
|
915
|
+
const rawOverrides = entry?.rates !== null && typeof entry?.rates === 'object' && !Array.isArray(entry?.rates) ? entry.rates : {}
|
|
916
|
+
const overrides = Object.fromEntries(Object.entries(rawOverrides).map(([model, rate]) => [scnetCanonModelId(model), rate]))
|
|
907
917
|
const rateOf = canon => {
|
|
908
918
|
if (overrides[canon] !== undefined && overrides[canon] !== null && typeof overrides[canon] === 'object') return overrides[canon]
|
|
909
919
|
return QWEN_CREDIT_RATES_BY_CANON[canon]
|
|
@@ -914,7 +924,10 @@ export function qwenTokenPlanWindows(days, entry, nowMs) {
|
|
|
914
924
|
for (const [date, day] of Object.entries(days ?? {})) {
|
|
915
925
|
if (typeof date !== 'string' || date < period.fromKey || date > period.toKeyInclusive) continue
|
|
916
926
|
for (const [pmKey, buckets] of Object.entries(day?.byProviderModel ?? {})) {
|
|
917
|
-
const
|
|
927
|
+
const sep = pmKey.indexOf(':')
|
|
928
|
+
const provider = sep > 0 ? pmKey.slice(0, sep) : ''
|
|
929
|
+
const model = sep > 0 ? pmKey.slice(sep + 1) : pmKey
|
|
930
|
+
if (!isQwenTokenPlanProvider(provider) || (includeCall && !includeCall(provider, model))) continue
|
|
918
931
|
const canon = scnetCanonModelId(model)
|
|
919
932
|
const rate = rateOf(canon)
|
|
920
933
|
if (rate === undefined || buckets === null || typeof buckets !== 'object') continue
|
|
@@ -270,7 +270,11 @@ export function parseAntigravityQuota(payload, ctx = {}) {
|
|
|
270
270
|
for (let gi = 0; gi < groups.length; gi++) {
|
|
271
271
|
const group = groups[gi]
|
|
272
272
|
if (group === null || typeof group !== 'object' || Array.isArray(group)) continue
|
|
273
|
-
const
|
|
273
|
+
const explicitId = pickField(group, 'groupId', 'group_id', 'id')
|
|
274
|
+
const rawGroupTitle = pickField(group, 'displayName', 'display_name', 'name') ?? explicitId
|
|
275
|
+
let groupTitle = rawGroupTitle ? String(rawGroupTitle).replace(/\s+models$/i, '').trim() : `group-${gi}`
|
|
276
|
+
if (groupTitle.toLowerCase().includes('claude') && groupTitle.toLowerCase().includes('gpt')) groupTitle = 'Claude / GPT'
|
|
277
|
+
const groupId = slug(explicitId ?? groupTitle)
|
|
274
278
|
const buckets = Array.isArray(group.buckets) ? group.buckets : []
|
|
275
279
|
let added = 0
|
|
276
280
|
for (const bucket of buckets) {
|
|
@@ -288,7 +292,7 @@ export function parseAntigravityQuota(payload, ctx = {}) {
|
|
|
288
292
|
const percent = clampPct((1 - fraction) * 100)
|
|
289
293
|
windows.push(makeWindow(
|
|
290
294
|
id,
|
|
291
|
-
`${
|
|
295
|
+
`${groupTitle} · ${meta.label}`,
|
|
292
296
|
percent,
|
|
293
297
|
absoluteResetAt(pickField(bucket, 'resetTime', 'reset_time', 'resetTime')),
|
|
294
298
|
meta.periodHours,
|
package/lib/index.js
CHANGED
|
@@ -22,10 +22,10 @@ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
|
|
|
22
22
|
import { Ledger, applyConfigPatch, localDayKey, pickBalanceInfo, reconcileBalanceDelta, zeroDay, splitLedgerApiCost, repairLedgerPricing, dedupeWrapperProviderDays, unpriceLocalOriginModels, stripSecrets, stripSecretPatch, secretRefOf, readSecret, writeSecret, SECRET_TARGETS, looksLikeSecretHeaderValue } from './store.js'
|
|
23
23
|
import { backfillLegacyLedger, importLegacyHistory, repairForkSeed, repairProviderDupes, recomputeLedgerPricingBasis } from './backfill.js'
|
|
24
24
|
import { createLlmStreamBilling } from './billing-stream.js'
|
|
25
|
-
import { OFFICIAL_PRICING_URL, OFFICIAL_PRICING_URL_ZH, LEGACY_BASE_BOUNDARY, normalizePrice, parsePricingHtml, costOf, providerPriceEntryFor, buildPriceCatalog, usdFromCost, isWrapperProviderId, wrapperUpstreamProvider } from './pricing.js'
|
|
25
|
+
import { OFFICIAL_PRICING_URL, OFFICIAL_PRICING_URL_ZH, LEGACY_BASE_BOUNDARY, normalizePrice, parsePricingHtml, repairDefaultPeakPrice, costOf, providerPriceEntryFor, buildPriceCatalog, usdFromCost, isWrapperProviderId, wrapperUpstreamProvider } from './pricing.js'
|
|
26
26
|
import { createUsageDeduper, USAGE_DEDUP_WINDOW_MS, usageFingerprint } from './usage-dedup.js'
|
|
27
27
|
import { CODING_PLAN_PROVIDERS, CODING_PLAN_PROVIDER_IDS, queryCodingPlan, scnetTokenPlanWindows, qwenTokenPlanWindows, emptyCustomBalance, queryCustomBalance, normalizeVolcengineKey } from './coding-plans.js'
|
|
28
|
-
import { recordSamples, buildPlanStats, canonicalWindowKey, periodStartOf, aggregateUsageSince, enabledPlanSetOf, pruneHourBuckets, suggestPlanAutoClasses } from './plan-billing.js'
|
|
28
|
+
import { recordSamples, buildPlanStats, canonicalWindowKey, periodStartOf, aggregateUsageSince, enabledPlanSetOf, billingClassOf, pruneHourBuckets, suggestPlanAutoClasses } from './plan-billing.js'
|
|
29
29
|
import { fetchWithRetry } from './net.js'
|
|
30
30
|
import { queryGatewayQuota, emptyGatewayQuota, managementKeyVarOf, gatewaySourceFingerprint } from './gateway-quotas.js'
|
|
31
31
|
import { stateSchema } from './typert.host.js'
|
|
@@ -375,7 +375,8 @@ function makeCostUsageProjection(ledger) {
|
|
|
375
375
|
// v6→v7(issue #63):seedLength 仅显式取值,普通会话的 length 不再误作种子边界,避免非 fork 会话代币漏计。
|
|
376
376
|
// v7→v8(issue #77):折叠计入 compaction/summary(压缩摘要调用,此前漏计);
|
|
377
377
|
// 升版本触发宿主对旧 checkpoint 全量重放,历史摘要调用量随之补齐。
|
|
378
|
-
|
|
378
|
+
// v8→v9(issue #109):默认回退补齐峰谷档位,旧金额 checkpoint 需按事件重放。
|
|
379
|
+
stateVersion: 9,
|
|
379
380
|
init: () => ({ provider: 'deepseek', model: 'default', totals: zeroBuckets(), byModel: {}, byProviderModel: {}, last: null, createdAt: 0, seedEndSeq: -1, shadow: emptyShadow(), seedLength: -1, seedDeducted: false, recent: [] }),
|
|
380
381
|
apply(state, event) {
|
|
381
382
|
// 兼容旧 checkpoint 的缺字段(版本升级前持久化的 v5 状态):缺省回落。
|
|
@@ -1593,7 +1594,9 @@ function createService(ctx, ledger) {
|
|
|
1593
1594
|
// 千问 Token Plan(issue #78)与 SCNet 同型:平台无 API-Key 化额度端点(额度仅
|
|
1594
1595
|
// 控制台可见,网关需 cookie+sec_token),按官方 Credits 抵扣率本地估算,跳过缓存间隔。
|
|
1595
1596
|
if (id === 'qwen') {
|
|
1596
|
-
const
|
|
1597
|
+
const enabledPlans = enabledPlanSetOf(ledger.config)
|
|
1598
|
+
const result = qwenTokenPlanWindows(ledger.days ?? {}, config, Date.now(),
|
|
1599
|
+
(provider, model) => billingClassOf(provider, model, ledger.config.planBilling, enabledPlans, ledger.config.prices) === 'plan')
|
|
1597
1600
|
codingPlanCaches[id] = {
|
|
1598
1601
|
fetchedAt: Date.now(),
|
|
1599
1602
|
value: result === null
|
|
@@ -2461,6 +2464,18 @@ export async function migrateCustomBalanceHeaderSecrets(ctx, ledger) {
|
|
|
2461
2464
|
return { imported, pending }
|
|
2462
2465
|
}
|
|
2463
2466
|
|
|
2467
|
+
const DEFAULT_PRICE_MIGRATION = 'default-peak-price-v1'
|
|
2468
|
+
const DEFAULT_PRICE_MIGRATION_PENDING = 'default-peak-price-pending-v1'
|
|
2469
|
+
|
|
2470
|
+
function prepareDefaultPeakPrice(ledger) {
|
|
2471
|
+
if (ledger.migrations.includes(DEFAULT_PRICE_MIGRATION) || ledger.migrations.includes(DEFAULT_PRICE_MIGRATION_PENDING)) return
|
|
2472
|
+
if (repairDefaultPeakPrice(ledger.config.prices)) {
|
|
2473
|
+
if (!ledger.migrations.includes(DEFAULT_PRICE_MIGRATION_PENDING)) ledger.migrations.push(DEFAULT_PRICE_MIGRATION_PENDING)
|
|
2474
|
+
// 同步修正新调用和投影使用的价格;重放若被退出打断,pending 随账本保存后可重试。
|
|
2475
|
+
ledger.scheduleWrite()
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2464
2479
|
/**
|
|
2465
2480
|
* 启动期历史导入(issue #27):先做按模型回填,再在**本插件首次启动**(config
|
|
2466
2481
|
* 标记 legacyAutoImportedAt 为 0)时自动导入一次安装前历史——用户无需手动触
|
|
@@ -2469,6 +2484,14 @@ export async function migrateCustomBalanceHeaderSecrets(ctx, ledger) {
|
|
|
2469
2484
|
* @param sessionsRoot - 宿主会话根目录($DSH_HOME/sessions)。
|
|
2470
2485
|
*/
|
|
2471
2486
|
export async function runStartupImports(ledger, sessionsRoot) {
|
|
2487
|
+
prepareDefaultPeakPrice(ledger)
|
|
2488
|
+
const needsDefaultPriceMigration = !ledger.migrations.includes(DEFAULT_PRICE_MIGRATION)
|
|
2489
|
+
const defaultPriceRepaired = ledger.migrations.includes(DEFAULT_PRICE_MIGRATION_PENDING)
|
|
2490
|
+
const oldPrices = defaultPriceRepaired ? structuredClone(ledger.config.prices) : null
|
|
2491
|
+
if (oldPrices) {
|
|
2492
|
+
const { cacheHit, cacheMiss, output } = oldPrices.default
|
|
2493
|
+
oldPrices.default = { cacheHit, cacheMiss, output }
|
|
2494
|
+
}
|
|
2472
2495
|
const filled = await backfillLegacyLedger(ledger, sessionsRoot)
|
|
2473
2496
|
if (filled.days > 0 || filled.sessions > 0 || filled.titles > 0) {
|
|
2474
2497
|
const extras = [
|
|
@@ -2617,6 +2640,31 @@ export async function runStartupImports(ledger, sessionsRoot) {
|
|
|
2617
2640
|
ledger.config.legacyAutoImportedAt = Date.now()
|
|
2618
2641
|
ledger.scheduleWrite()
|
|
2619
2642
|
}
|
|
2643
|
+
if (needsDefaultPriceMigration) {
|
|
2644
|
+
if (defaultPriceRepaired) {
|
|
2645
|
+
const options = { mode: ledger.config.priceMatch, overrides: ledger.config.priceOverrides }
|
|
2646
|
+
const stats = await recomputeLedgerPricingBasis(ledger, sessionsRoot, key => {
|
|
2647
|
+
const sep = key.indexOf(':')
|
|
2648
|
+
const provider = sep > 0 ? key.slice(0, sep) : 'deepseek'
|
|
2649
|
+
const model = sep > 0 ? key.slice(sep + 1) : key
|
|
2650
|
+
// 唯一发生变化的是 default;明确命中的模型及自定义映射不受重算影响。
|
|
2651
|
+
const before = providerPriceEntryFor(provider, model, oldPrices, options)
|
|
2652
|
+
const after = providerPriceEntryFor(provider, model, ledger.config.prices, options)
|
|
2653
|
+
return JSON.stringify(before) !== JSON.stringify(after)
|
|
2654
|
+
})
|
|
2655
|
+
splitLedgerApiCost(ledger)
|
|
2656
|
+
console.log(`[dsh-cost-meter] 已补齐默认价格峰谷档位,按日志重算 ${stats.recostedSessions} 个会话;跳过 ${stats.skippedSessions} 个覆盖不全的会话`)
|
|
2657
|
+
}
|
|
2658
|
+
ledger.migrations = ledger.migrations.filter(marker => marker !== DEFAULT_PRICE_MIGRATION_PENDING)
|
|
2659
|
+
ledger.migrations.push(DEFAULT_PRICE_MIGRATION)
|
|
2660
|
+
ledger.scheduleWrite()
|
|
2661
|
+
}
|
|
2662
|
+
// 新识别的千问订阅别名同步更新历史 API/订阅金额,token 与原始 provider 键保留。
|
|
2663
|
+
if (!ledger.migrations.includes('qwen-provider-split-v1')) {
|
|
2664
|
+
splitLedgerApiCost(ledger)
|
|
2665
|
+
ledger.migrations.push('qwen-provider-split-v1')
|
|
2666
|
+
ledger.scheduleWrite()
|
|
2667
|
+
}
|
|
2620
2668
|
}
|
|
2621
2669
|
|
|
2622
2670
|
/**
|
|
@@ -2625,6 +2673,8 @@ export async function runStartupImports(ledger, sessionsRoot) {
|
|
|
2625
2673
|
*/
|
|
2626
2674
|
export function apply(ctx) {
|
|
2627
2675
|
const ledger = Ledger.open()
|
|
2676
|
+
// 必须先于监听器/投影注册,避免延迟历史修复期间的新调用仍使用旧默认谷价。
|
|
2677
|
+
prepareDefaultPeakPrice(ledger)
|
|
2628
2678
|
console.log(`[dsh-cost-meter] 已加载,账本:${ledger.path}`)
|
|
2629
2679
|
|
|
2630
2680
|
// 卸载/退出前最终落盘。
|
|
@@ -2668,6 +2718,8 @@ export function apply(ctx) {
|
|
|
2668
2718
|
// 指纹窗口去重:包装层样本改挂上游 id 后,与上游真实流按 (model, 五桶指纹)
|
|
2669
2719
|
// 10s 窗互斥入账,无论到达顺序只记一次;包装层单链照常入账(不再漏计)。
|
|
2670
2720
|
const usageDeduper = createUsageDeduper()
|
|
2721
|
+
// 记忆插件等可创建隔离的 llm 服务;账本是全局观察者,须跨服务作用域接收用量。
|
|
2722
|
+
// 子会话仍按自己的 sessionId 入账;无 sessionId 的后台调用只进入日/月总计。
|
|
2671
2723
|
ctx.on('llm/stream', createLlmStreamBilling({
|
|
2672
2724
|
account: (usage, model, sessionId, atMs, provider) => {
|
|
2673
2725
|
const buckets = {
|
|
@@ -2683,7 +2735,7 @@ export function apply(ctx) {
|
|
|
2683
2735
|
if (effective === null) return
|
|
2684
2736
|
ledger.account(buckets, model, sessionId, atMs, effective)
|
|
2685
2737
|
},
|
|
2686
|
-
}))
|
|
2738
|
+
}), { global: true })
|
|
2687
2739
|
|
|
2688
2740
|
// costUsage 投影:向会话历史页/推送帧提供 token 桶(客户端计价)。
|
|
2689
2741
|
ctx.inject(['sessionProjections'], (projectionCtx) => {
|
package/lib/plan-billing.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* 全部为纯函数(可单测);持久化与网络在宿主侧(store.js / index.js)。
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import { CODING_PLAN_PROVIDER_IDS } from './coding-plans.js'
|
|
23
|
+
import { CODING_PLAN_PROVIDER_IDS, QWEN_TOKEN_PLAN_PROVIDER_IDS } from './coding-plans.js'
|
|
24
24
|
import { canonModelId } from './pricing.js'
|
|
25
25
|
|
|
26
26
|
/** Plan 统计支持的提供商 id:9 家 Coding Plan + OpenCode Go。 */
|
|
@@ -29,6 +29,7 @@ export const PLAN_PROVIDER_IDS = [...CODING_PLAN_PROVIDER_IDS, 'go']
|
|
|
29
29
|
/** 请求 provider 名 → Plan 提供商 id 的别名归并(路由渠道 zen/opencode 都是 Go)。 */
|
|
30
30
|
export const PLAN_PROVIDER_ALIASES = {
|
|
31
31
|
go: ['go', 'zen', 'opencode', 'opencode-go'],
|
|
32
|
+
qwen: QWEN_TOKEN_PLAN_PROVIDER_IDS,
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
/** 各 Plan 提供商的默认计费类别(auto = 跟随该家启用开关)。 */
|
|
@@ -558,4 +559,3 @@ export function buildPlanStats({ days, hourBuckets, samples, codingPlans, goQuot
|
|
|
558
559
|
collect('go', goQuota?.status, goQuota ?? undefined)
|
|
559
560
|
return { generatedAt: now, providers }
|
|
560
561
|
}
|
|
561
|
-
|
package/lib/pricing.js
CHANGED
|
@@ -372,7 +372,12 @@ export const DEFAULT_PRICE_TABLE = {
|
|
|
372
372
|
peak: { cacheHit: 0.014, cacheMiss: 0.44, output: 1.32 },
|
|
373
373
|
},
|
|
374
374
|
},
|
|
375
|
-
default: {
|
|
375
|
+
default: {
|
|
376
|
+
cacheHit: 0.007, cacheMiss: 0.22, output: 0.66,
|
|
377
|
+
offPeak: { cacheHit: 0.007, cacheMiss: 0.22, output: 0.66 },
|
|
378
|
+
peak: { cacheHit: 0.014, cacheMiss: 0.44, output: 1.32 },
|
|
379
|
+
legacyBase: { cacheHit: 0.0028, cacheMiss: 0.14, output: 0.28 },
|
|
380
|
+
},
|
|
376
381
|
}
|
|
377
382
|
|
|
378
383
|
/**
|
|
@@ -409,7 +414,23 @@ export const DEFAULT_PRICE_TABLE_CNY = {
|
|
|
409
414
|
peak: { cacheHit: 0.10, cacheMiss: 3, output: 9 },
|
|
410
415
|
},
|
|
411
416
|
},
|
|
412
|
-
default: {
|
|
417
|
+
default: {
|
|
418
|
+
cacheHit: 0.05, cacheMiss: 1.5, output: 4.5,
|
|
419
|
+
offPeak: { cacheHit: 0.05, cacheMiss: 1.5, output: 4.5 },
|
|
420
|
+
peak: { cacheHit: 0.10, cacheMiss: 3, output: 9 },
|
|
421
|
+
legacyBase: { cacheHit: 0.02, cacheMiss: 1, output: 2 },
|
|
422
|
+
},
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** 仅补齐旧内置/官方三桶默认价;自定义单价、已有档位和显式 flat 价保持原样。 */
|
|
426
|
+
export function repairDefaultPeakPrice(prices) {
|
|
427
|
+
const current = prices?.default
|
|
428
|
+
const expected = (prices?.currency === 'CNY' ? DEFAULT_PRICE_TABLE_CNY : DEFAULT_PRICE_TABLE).default
|
|
429
|
+
const fields = ['cacheHit', 'cacheMiss', 'output']
|
|
430
|
+
if (!current || Object.keys(current).some(key => !fields.includes(key))
|
|
431
|
+
|| !fields.every(key => current[key] === expected[key])) return false
|
|
432
|
+
prices.default = structuredClone(expected)
|
|
433
|
+
return true
|
|
413
434
|
}
|
|
414
435
|
|
|
415
436
|
/**
|
|
@@ -1106,7 +1127,6 @@ export function parsePricingHtml(html) {
|
|
|
1106
1127
|
|
|
1107
1128
|
const currency = sawCny ? 'CNY' : 'USD'
|
|
1108
1129
|
const models = {}
|
|
1109
|
-
let firstOffPeak = null
|
|
1110
1130
|
for (let k = 0; k < modelIds.length; k += 1) {
|
|
1111
1131
|
const id = modelIds[k].toLowerCase()
|
|
1112
1132
|
const off = {
|
|
@@ -1131,7 +1151,6 @@ export function parsePricingHtml(html) {
|
|
|
1131
1151
|
output: pk.output ?? off.output,
|
|
1132
1152
|
},
|
|
1133
1153
|
}
|
|
1134
|
-
if (firstOffPeak === null) firstOffPeak = off
|
|
1135
1154
|
// 峰谷时代前的历史基础价(官方页面已不再列出,按历史公告数字附带;
|
|
1136
1155
|
// 人民币页附带人民币基础价,与价表币种一致)。
|
|
1137
1156
|
const legacy = (currency === 'CNY' ? LEGACY_BASE_PRICES_CNY : LEGACY_BASE_PRICES)[id]
|
|
@@ -1146,9 +1165,8 @@ export function parsePricingHtml(html) {
|
|
|
1146
1165
|
}
|
|
1147
1166
|
// 生效时间:页面已不再给出(两档方案即时生效)→ null。
|
|
1148
1167
|
const effectiveAt = null
|
|
1149
|
-
//
|
|
1150
|
-
|
|
1151
|
-
const fallback = firstOffPeak === null ? undefined : { cacheHit: firstOffPeak.cacheHit, cacheMiss: firstOffPeak.cacheMiss, output: firstOffPeak.output }
|
|
1168
|
+
// 默认回退沿用首个成功模型的完整档位;只复制谷价会让峰时费用少算一半。
|
|
1169
|
+
const fallback = structuredClone(Object.values(models)[0])
|
|
1152
1170
|
// 峰时段窗口。
|
|
1153
1171
|
let peakWindows = null
|
|
1154
1172
|
const plain = stripTags(html)
|
package/lib/store.js
CHANGED
|
@@ -135,9 +135,11 @@ export function defaultConfig() {
|
|
|
135
135
|
hideOfficialBalance: false, // 隐藏官方账户余额(issue #45):开启后侧边栏/会话页/设置页的官方余额 UI 整体不渲染
|
|
136
136
|
hideTodayCost: false, // 隐藏今日消耗金额(issue #46):开启后侧栏今日费用行/预算明细今日行/概览今日卡片不渲染
|
|
137
137
|
showTotalWithPlan: false, // 「含 Plan 总额」全局开关(v1.6.0):开启后全部金额展示按总等值(cost)计;默认按真金白银(apiCost)
|
|
138
|
+
sidebarStyle: 'standard', // 侧边栏进度条样式:standard(标准,纵向单列) | compact(紧凑,额度卡两列网格)
|
|
138
139
|
legacyAutoImportedAt: 0, // 安装前历史自动导入标记(issue #27):完成时刻 ms;0 = 尚未跑过
|
|
139
140
|
peakStyle: 'compact', // 峰谷时段条样式:compact(简洁单行/竖向同构) | classic(经典分段/胶囊芯片)
|
|
140
141
|
priceMatch: 'auto', // 未知模型名自动匹配价格表:auto(去后缀/前缀/家族相似) | exact(仅精确)
|
|
142
|
+
priceMatchDismissed: [], // 从匹配设置中移除的 provider:model 行;不删除用量或改变计价规则。
|
|
141
143
|
priceOverrides: {}, // 手动匹配覆盖:{ 'provider:modelId': 'provider:模型 | deepseek:__default__' };裸模型名 = 同渠道换名(旧版遗留,跨渠道裸 DeepSeek 名由查价兜底自愈,issue #56)
|
|
142
144
|
priceTableDisplay: {}, // 费用设置直接显示(按模型):键 'provider:modelId' → 布尔;缺省 = DeepSeek 模型直接显示、第三方收入拓展价格表(含 DeepSeek 模型也可逐模型收入)
|
|
143
145
|
prices: {
|
|
@@ -722,9 +724,11 @@ const VALIDATION_MESSAGES = {
|
|
|
722
724
|
hideOfficialBalance: 'hideOfficialBalance 必须是布尔值',
|
|
723
725
|
hideTodayCost: 'hideTodayCost 必须是布尔值',
|
|
724
726
|
showTotalWithPlan: 'showTotalWithPlan 必须是布尔值',
|
|
727
|
+
sidebarStyle: 'sidebarStyle 必须是 standard / compact',
|
|
725
728
|
peakStyle: 'peakStyle 必须是 compact / classic',
|
|
726
729
|
priceMatch: 'priceMatch 必须是 auto / exact',
|
|
727
730
|
priceOverrides: 'priceOverrides 必须是字符串→字符串映射',
|
|
731
|
+
priceMatchDismissed: 'priceMatchDismissed 必须是模型键字符串数组(最多 1000 项,每项 1–512 字符)',
|
|
728
732
|
historyDays: 'historyDays 必须是 7-3650 的整数',
|
|
729
733
|
locale: 'locale 必须是 auto / zh / en',
|
|
730
734
|
budget: 'budget 非法',
|
|
@@ -818,9 +822,11 @@ const VALIDATION_MESSAGES = {
|
|
|
818
822
|
hideOfficialBalance: 'hideOfficialBalance must be a boolean',
|
|
819
823
|
hideTodayCost: 'hideTodayCost must be a boolean',
|
|
820
824
|
showTotalWithPlan: 'showTotalWithPlan must be a boolean',
|
|
825
|
+
sidebarStyle: 'sidebarStyle must be standard / compact',
|
|
821
826
|
peakStyle: 'peakStyle must be compact / classic',
|
|
822
827
|
priceMatch: 'priceMatch must be auto / exact',
|
|
823
828
|
priceOverrides: 'priceOverrides must be a string→string map',
|
|
829
|
+
priceMatchDismissed: 'priceMatchDismissed must contain at most 1000 model keys of 1–512 characters',
|
|
824
830
|
historyDays: 'historyDays must be an integer from 7 to 3650',
|
|
825
831
|
locale: 'locale must be auto / zh / en',
|
|
826
832
|
budget: 'Invalid budget',
|
|
@@ -990,9 +996,20 @@ export function applyConfigPatch(current, rawPatch) {
|
|
|
990
996
|
if (errors.length > 0) return { config: current, errors }
|
|
991
997
|
// 结构化克隆,拒绝补丁时不得污染活配置(mergeDeep 共享未触及子树,校验期的就地收敛会泄漏进活配置)。
|
|
992
998
|
const candidate = mergeDeep(structuredClone(current), patch)
|
|
999
|
+
// 这两张编辑表随对应设置整表提交;逐键深合并会把用户移除的键补回来。
|
|
1000
|
+
if (Object.hasOwn(patch, 'priceOverrides')) candidate.priceOverrides = structuredClone(patch.priceOverrides)
|
|
1001
|
+
const qwenPatch = patch.codingPlans?.qwen
|
|
1002
|
+
if (qwenPatch !== null && typeof qwenPatch === 'object' && !Array.isArray(qwenPatch) && Object.hasOwn(qwenPatch, 'rates')) {
|
|
1003
|
+
candidate.codingPlans.qwen.rates = structuredClone(qwenPatch.rates)
|
|
1004
|
+
}
|
|
993
1005
|
// prices.models 是可编辑列表:客户端提交完整列表时必须按替换语义处理,
|
|
994
1006
|
// 否则 mergeDeep 会把已删除的旧模型重新合并回来。
|
|
995
1007
|
if (patch.prices !== null && typeof patch.prices === 'object' && !Array.isArray(patch.prices)) {
|
|
1008
|
+
// 默认价卡片也是完整记录,清空某档位时不能被深合并复活。
|
|
1009
|
+
const defaultPatch = patch.prices.default
|
|
1010
|
+
if (defaultPatch && ['cacheHit', 'cacheMiss', 'output'].every(key => Object.hasOwn(defaultPatch, key))) {
|
|
1011
|
+
candidate.prices.default = structuredClone(defaultPatch)
|
|
1012
|
+
}
|
|
996
1013
|
if (patch.prices.models !== null && typeof patch.prices.models === 'object' && !Array.isArray(patch.prices.models)) {
|
|
997
1014
|
candidate.prices.models = patch.prices.models
|
|
998
1015
|
}
|
|
@@ -1092,9 +1109,16 @@ export function applyConfigPatch(current, rawPatch) {
|
|
|
1092
1109
|
if (candidate.hideTodayCost !== undefined && typeof candidate.hideTodayCost !== 'boolean') errors.push(vmsg(locale, 'hideTodayCost'))
|
|
1093
1110
|
// 「含 Plan 总额」全局开关(v1.6.0):布尔,可缺省(默认关)。
|
|
1094
1111
|
if (candidate.showTotalWithPlan !== undefined && typeof candidate.showTotalWithPlan !== 'boolean') errors.push(vmsg(locale, 'showTotalWithPlan'))
|
|
1112
|
+
// 侧边栏进度条样式:standard(默认,纵向单列) | compact(额度卡两列网格)。
|
|
1113
|
+
if (candidate.sidebarStyle !== undefined && candidate.sidebarStyle !== 'standard' && candidate.sidebarStyle !== 'compact') errors.push(vmsg(locale, 'sidebarStyle'))
|
|
1095
1114
|
if (candidate.peakStyle !== 'compact' && candidate.peakStyle !== 'classic') errors.push(vmsg(locale, 'peakStyle'))
|
|
1096
1115
|
if (candidate.priceMatch !== 'auto' && candidate.priceMatch !== 'exact') errors.push(vmsg(locale, 'priceMatch'))
|
|
1097
1116
|
const overrides = candidate.priceOverrides
|
|
1117
|
+
if (candidate.priceMatchDismissed !== undefined && (!Array.isArray(candidate.priceMatchDismissed)
|
|
1118
|
+
|| candidate.priceMatchDismissed.length > 1000
|
|
1119
|
+
|| candidate.priceMatchDismissed.some(key => typeof key !== 'string' || key.length === 0 || key.length > 512))) {
|
|
1120
|
+
errors.push(vmsg(locale, 'priceMatchDismissed'))
|
|
1121
|
+
}
|
|
1098
1122
|
if (overrides === null || typeof overrides !== 'object' || Array.isArray(overrides)
|
|
1099
1123
|
|| Object.entries(overrides).some(([k, v]) => typeof k !== 'string' || typeof v !== 'string')) {
|
|
1100
1124
|
errors.push(vmsg(locale, 'priceOverrides'))
|
|
@@ -1361,6 +1385,13 @@ export function sanitizeConfig(raw) {
|
|
|
1361
1385
|
const base = defaultConfig()
|
|
1362
1386
|
const cfg = raw !== null && typeof raw === 'object' && !Array.isArray(raw) ? raw : {}
|
|
1363
1387
|
const out = mergeDeep(base, cfg)
|
|
1388
|
+
// 新版默认价带峰谷档位;加载已有完整记录时保留用户省略/清空的档位。
|
|
1389
|
+
// 旧官方默认价的补齐由带迁移标记的启动流程负责,避免每次加载覆盖自定义。
|
|
1390
|
+
const storedDefault = cfg.prices?.default
|
|
1391
|
+
if (storedDefault && typeof storedDefault === 'object' && !Array.isArray(storedDefault)) {
|
|
1392
|
+
const { cacheHit, cacheMiss, output } = base.prices.default
|
|
1393
|
+
out.prices.default = mergeDeep({ cacheHit, cacheMiss, output }, storedDefault)
|
|
1394
|
+
}
|
|
1364
1395
|
const isNum = v => typeof v === 'number' && Number.isFinite(v)
|
|
1365
1396
|
const oneOf = (v, list, fallback) => (typeof v === 'string' && list.includes(v) ? v : fallback)
|
|
1366
1397
|
// 顶层标量:类型不符回落默认。
|
|
@@ -1392,6 +1423,10 @@ export function sanitizeConfig(raw) {
|
|
|
1392
1423
|
out.hideOfficialBalance = out.hideOfficialBalance === true
|
|
1393
1424
|
out.hideTodayCost = out.hideTodayCost === true
|
|
1394
1425
|
out.showTotalWithPlan = out.showTotalWithPlan === true
|
|
1426
|
+
out.sidebarStyle = oneOf(out.sidebarStyle, ['standard', 'compact'], 'standard')
|
|
1427
|
+
out.priceMatchDismissed = Array.isArray(out.priceMatchDismissed)
|
|
1428
|
+
? [...new Set(out.priceMatchDismissed.filter(key => typeof key === 'string' && key.length > 0 && key.length <= 512))].slice(0, 1000)
|
|
1429
|
+
: []
|
|
1395
1430
|
// v1.5.38 的隐私模式字段已废弃(改为 UI 隐藏开关):清洗旧账本残留,避免僵尸配置长期留存。
|
|
1396
1431
|
delete out.hideAmounts
|
|
1397
1432
|
// 峰/谷切换弹窗提醒:非法值定向收敛(开关默认开、提前量回 2、类型回 both)。
|
package/lib/typert.host.js
CHANGED
|
@@ -119,6 +119,8 @@ const configSchema = z.object({
|
|
|
119
119
|
hideTodayCost: z.boolean().optional(),
|
|
120
120
|
// 「含 Plan 总额」全局开关(v1.6.0):开启后全部金额展示按总等值(cost)计。
|
|
121
121
|
showTotalWithPlan: z.boolean().optional(),
|
|
122
|
+
sidebarStyle: z.enum(['standard', 'compact']).optional(),
|
|
123
|
+
priceMatchDismissed: z.array(z.string()).optional(),
|
|
122
124
|
// 安装前历史自动导入完成时刻(issue #27,内部标记;0/缺席 = 尚未跑过)。
|
|
123
125
|
legacyAutoImportedAt: num.optional(),
|
|
124
126
|
peakStyle: z.enum(['compact', 'classic']).optional(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cost-meter",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.15",
|
|
4
4
|
"description": "DeepSeek Harness 会话费用统计插件:本会话成本、当日费用、历史记录与官方价格同步,支持多厂商多模型价格计费(内置 90+ 模型价格目录与自动匹配)、主流 Coding Plan 订阅额度查询与显示(9 家,含 SCNet Token Plan 本地 Credits 计量与火山方舟 Volcano Ark Coding Plan AK/SK 签名查询)、自定义 Provider 余额查询(可配任意 HTTP 端点)与余额进度条、峰谷计价时段显示与切换前弹窗/系统通知提醒(位置/提前量/类型可配),界面中英双语。Session cost tracking plugin for DeepSeek Harness: per-conversation cost, daily totals, history and official price sync, with multi-vendor model pricing (built-in 90+ model catalog with auto-matching), Coding Plan quota queries & display for 9 vendors (incl. local credits metering for the SCNet Token Plan and Volcano Ark Coding Plan via AK/SK signature), plan/API dual-track billing with per-1% & full-window Token Plan usage estimates, custom provider balance lookup (configurable HTTP endpoint) and balance progress bar, plus peak/off-peak pricing display with pre-switch popup & system-notification alerts (position / lead time / type configurable), in a bilingual (Chinese/English) UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
"dshReleases": {
|
|
25
25
|
"0.1.2-alpha.3": "compatible",
|
|
26
26
|
"0.1.2-alpha.4": "compatible",
|
|
27
|
-
"0.1.2-alpha.5": "compatible"
|
|
27
|
+
"0.1.2-alpha.5": "compatible",
|
|
28
|
+
"0.1.2-rc.1": "compatible",
|
|
29
|
+
"0.1.3-alpha.1": "unknown",
|
|
30
|
+
"0.1.3-alpha.2": "compatible"
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
33
|
},
|
|
@@ -80,10 +83,12 @@
|
|
|
80
83
|
"docs/provider-pricing.json"
|
|
81
84
|
],
|
|
82
85
|
"dependencies": {
|
|
83
|
-
"@deepseek-ai/dsh-credentials": "0.1.0-rc.8",
|
|
84
|
-
"@deepseek-ai/dsh-home-paths": "0.1.0-rc.8",
|
|
85
86
|
"zod": "4.5.1"
|
|
86
87
|
},
|
|
88
|
+
"peerDependencies": {
|
|
89
|
+
"@deepseek-ai/dsh-credentials": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.3-0",
|
|
90
|
+
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.3-0"
|
|
91
|
+
},
|
|
87
92
|
"repository": {
|
|
88
93
|
"type": "git",
|
|
89
94
|
"url": "git+https://github.com/Han-1413141/dsh-cost-meter.git"
|
|
@@ -115,6 +120,8 @@
|
|
|
115
120
|
"test": "node test/verify.mjs"
|
|
116
121
|
},
|
|
117
122
|
"devDependencies": {
|
|
123
|
+
"@deepseek-ai/dsh-credentials": "0.1.0-rc.8",
|
|
124
|
+
"@deepseek-ai/dsh-home-paths": "0.1.0-rc.8",
|
|
118
125
|
"esbuild": "^0.28.2"
|
|
119
126
|
}
|
|
120
127
|
}
|