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.
@@ -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 overrides = entry?.rates !== null && typeof entry?.rates === 'object' && !Array.isArray(entry?.rates) ? entry.rates : {}
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 model = pmKey.includes(':') ? pmKey.slice(pmKey.indexOf(':') + 1) : pmKey
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 groupId = slug(pickField(group, 'groupId', 'group_id', 'id', 'name') ?? `group-${gi}`)
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
- `${groupId} · ${meta.label}`,
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
- stateVersion: 8,
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 result = qwenTokenPlanWindows(ledger.days ?? {}, config, Date.now())
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) => {
@@ -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: { cacheHit: 0.007, cacheMiss: 0.22, output: 0.66 },
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: { cacheHit: 0.05, cacheMiss: 1.5, output: 4.5 },
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
- // 兜底价:取首个解析成功模型的空闲档三桶(与内置默认表的「default = flash
1150
- // 空闲档」语义一致;随价表币种同步,避免切换币种后 default 残留旧币种数字)。
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)。
@@ -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.13",
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
  }