dsh-cost-meter 1.5.15 → 1.5.16

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 CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  Per-conversation cost · daily totals · OpenCode Go subscription quota display · budget with usage percentage · official account balance · custom provider balance · balance progress bar · history · peak/off-peak pricing hours display (peak hours UTC 01:00–04:00, 06:00–10:00) · one-click price sync from the official docs · Codex-style token usage heat grid · multi-vendor model pricing (built-in 90+ model price catalog with auto-matching) · mainstream Coding Plan quota queries & display (Anthropic / Z.ai / MiniMax / Kimi / OpenRouter / SiliconFlow)
8
8
 
9
- [![version](https://img.shields.io/badge/version-1.5.15-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
9
+ [![version](https://img.shields.io/badge/version-1.5.16-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
10
10
  [![npm](https://img.shields.io/npm/v/dsh-cost-meter?label=npm)](https://www.npmjs.com/package/dsh-cost-meter)
11
11
  [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
12
12
  [![dsh](https://img.shields.io/badge/DeepSeek%20Harness-dsh--plugin-4176E6)](https://github.com/deepseek-ai/deepseek-harness)
@@ -40,6 +40,7 @@ English | [中文](README.md)
40
40
  | Budget settings | Settings page, top | Limit, period (today / month / cumulative / custom date range), used % |
41
41
  | Price table | Settings page | Per-model off-peak / peak prices (input/output shorthand supported; cache prices derived automatically); fully editable |
42
42
  | Peak/off-peak hours display | Settings / budget / today | Shows UTC peak hours 01:00–04:00 and 06:00–10:00 with the current tier; expanded view shows a peak/off-peak period strip (current period + countdown), collapsed (rail) view shows a vertical peak/off-peak progress bar; independently toggleable |
43
+ | Peak/off-peak switch popup alert | Global overlay | A bottom-right popup appears when the next tier switch is within the configured lead time (default 2 minutes, 1–30); alert type selectable (entering peak / entering off-peak / both); one alert per switch point; toggle lives in the peak pricing panel in Settings |
43
44
  | Official price sync | Settings page | Fetches and parses the official pricing page, applies with one click |
44
45
  | UI language | Settings → Display settings | Simplified Chinese / English / Follow browser (auto); switches instantly and auto-saves |
45
46
  | AI price sync | [prompt](docs/AI-PRICE-SYNC-PROMPT.en.md) | DeepSeek official sync; other providers use the verified official price catalog and manual configuration |
@@ -192,22 +193,22 @@ Real captures from an actual DSH sidebar of the period strip and collapsed verti
192
193
  dsh plugin --profile web add dsh-cost-meter
193
194
  ```
194
195
 
195
- **PowerShell one-click script** (copy the whole line, paste, press Enter; pnpm is provisioned automatically, git is auto-detected — no clone needed; the install chain is **pinned to the release tag `v1.5.15`** — review the script before running):
196
+ **PowerShell one-click script** (copy the whole line, paste, press Enter; pnpm is provisioned automatically, git is auto-detected — no clone needed; the install chain is **pinned to the release tag `v1.5.16`** — review the script before running):
196
197
 
197
198
  ```powershell
198
- irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.15/install.ps1 | iex
199
+ irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.16/install.ps1 | iex
199
200
  ```
200
201
 
201
202
  **Or a plain command line** (the machine must already have pnpm and git; also pinned to the tag):
202
203
 
203
204
  ```sh
204
- dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.15
205
+ dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.16
205
206
  ```
206
207
 
207
208
  Without git, use the GitHub tag archive:
208
209
 
209
210
  ```sh
210
- dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.15.tar.gz
211
+ dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.16.tar.gz
211
212
  ```
212
213
 
213
214
  After installing, **restart** `dsh web` (plugin rows, the Typert manifest and the client bundle are all scanned at startup):
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  本会话费用 · 当日费用 · OpenCode Go 订阅额度显示 · 预算与已用百分比 · 官方账户余额 · 自定义 Provider 余额查询(可配任意 HTTP 端点) · 余额三段进度条 · 历史记录 · 峰谷计价时段显示(UTC 01:00–04:00、06:00–10:00 为峰时段) · 官方价格一键同步 · 类 Codex Token 用量热图 · 多厂商多模型价格计费(内置 90+ 模型价格目录与自动匹配) · 主流 Coding Plan 额度查询与显示(Anthropic / Z.ai / MiniMax / Kimi / OpenRouter / SiliconFlow 六家)
8
8
 
9
- [![version](https://img.shields.io/badge/version-1.5.15-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
9
+ [![version](https://img.shields.io/badge/version-1.5.16-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
10
10
  [![npm](https://img.shields.io/npm/v/dsh-cost-meter?label=npm)](https://www.npmjs.com/package/dsh-cost-meter)
11
11
  [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
12
12
  [![dsh](https://img.shields.io/badge/DeepSeek%20Harness-dsh--plugin-4176E6)](https://github.com/deepseek-ai/deepseek-harness)
@@ -41,6 +41,7 @@
41
41
  | 预算设置 | 设置页顶部 | 额度、周期(今日/本月/累计/自定义日期区间)、已用% |
42
42
  | 价格表 | 设置页 | 每模型 谷时/峰时 两档价格(支持 input/output 简写,缓存价自动补齐),增删改自由 |
43
43
  | 峰谷计价时段显示 | 设置页 / 预算 / 今日费用 | 显示 UTC 峰时段 01:00–04:00、06:00–10:00 与当前档位;展开态显示峰时/平价时段条(当前时段 + 倒计时),收起(rail)态显示竖向峰谷进度条,可单独开关 |
44
+ | 峰/谷切换弹窗提醒 | 全局浮层 | 距进入峰/谷时段不足设定提前量(默认 2 分钟,1-30 可配)时右下角弹窗提醒;提醒类型可选(进入峰 / 进入谷 / 峰和谷),同一切换点只提醒一次,设置页峰谷计价面板内开关 |
44
45
  | 官方价格同步 | 设置页 | 抓取解析官方定价页,一键应用 |
45
46
  | 界面语言 | 设置页 → 显示设置 | 简体中文 / English / 跟随浏览器(自动);切换即时生效并自动保存 |
46
47
  | AI 价格同步 | [提示词](docs/AI-PRICE-SYNC-PROMPT.md) | DeepSeek 官方同步;其他 provider 使用已核对的官方价格目录与手动配置 |
@@ -194,22 +195,22 @@
194
195
  dsh plugin --profile web add dsh-cost-meter
195
196
  ```
196
197
 
197
- **PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.5.15`**,建议先下载审阅再运行):
198
+ **PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.5.16`**,建议先下载审阅再运行):
198
199
 
199
200
  ```powershell
200
- irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.15/install.ps1 | iex
201
+ irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.16/install.ps1 | iex
201
202
  ```
202
203
 
203
204
  **或直接命令行**(机器上需已有 pnpm 与 git;同样固定到 tag):
204
205
 
205
206
  ```sh
206
- dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.15
207
+ dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.16
207
208
  ```
208
209
 
209
210
  没有 git 时可用 GitHub tag 打包直链:
210
211
 
211
212
  ```sh
212
- dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.15.tar.gz
213
+ dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.16.tar.gz
213
214
  ```
214
215
 
215
216
  安装后**重启** `dsh web`(插件行、Typert 清单与客户端 bundle 均在启动时扫描):
package/lib/client.js CHANGED
@@ -110,6 +110,13 @@ window.__ModuleLoader__.load({
110
110
  '.cm-msg.err{color:var(--dsw-alias-state-error-primary)}',
111
111
  '.cm-hint{font-size:12px;color:var(--dsw-alias-label-tertiary)}',
112
112
  '.cm-budget{border:1px solid var(--dsw-alias-border-l1);border-radius:12px;padding:14px 16px;background:var(--dsw-alias-bg-layer-1);display:flex;flex-direction:column;gap:10px}',
113
+ '.cm-peak-alert{position:fixed;right:20px;bottom:20px;z-index:9999;max-width:320px;padding:12px 14px;border-radius:10px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);box-shadow:0 8px 24px rgba(0,0,0,.18);display:flex;flex-direction:column;gap:6px;font-size:12px;animation:cm-peak-alert-in .18s ease-out}',
114
+ '.cm-peak-alert-peak{border-left:3px solid #f59e0b}',
115
+ '.cm-peak-alert-offpeak{border-left:3px solid var(--dsw-alias-brand-primary)}',
116
+ '.cm-peak-alert-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}',
117
+ '.cm-peak-alert-body{color:var(--dsw-alias-label-secondary);line-height:1.5}',
118
+ '.cm-peak-alert-actions{display:flex;justify-content:flex-end}',
119
+ '@keyframes cm-peak-alert-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}',
113
120
  '.cm-budget-head{display:flex;align-items:center;justify-content:space-between;gap:8px}',
114
121
  '.cm-budget-bar{height:8px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);overflow:hidden}',
115
122
  '.cm-budget-fill{height:100%;border-radius:999px;background:var(--dsw-alias-state-business-primary);transition:width .3s ease}',
@@ -422,6 +429,18 @@ window.__ModuleLoader__.load({
422
429
  peakStyleLabel: '峰谷时段条样式',
423
430
  peakStyleCompact: '简洁(单行紧凑)',
424
431
  peakStyleClassic: '经典(分段与胶囊芯片)',
432
+ peakAlertLabel: '峰/谷切换前弹窗提醒',
433
+ peakAlertAheadLabel: '提前提醒(分钟,1-30)',
434
+ peakAlertTargetLabel: '提醒类型',
435
+ peakAlertTargetPeak: '进入峰时',
436
+ peakAlertTargetOffPeak: '进入谷时',
437
+ peakAlertTargetBoth: '峰和谷',
438
+ peakAlertTitlePeak: '即将进入峰时',
439
+ peakAlertTitleOffPeak: '即将进入谷时',
440
+ peakAlertBody: '约 {time} 后计费档位切换为{phase}价,请注意本时段调用成本。',
441
+ peakAlertPhasePeak: '峰时',
442
+ peakAlertPhaseOffPeak: '谷时',
443
+ peakAlertBtn: '知道了',
425
444
  peakPanelTitle: '峰谷计价与提示',
426
445
  peakNoticeHiddenHint: '提示已隐藏:需启用峰谷计价并开启「峰时高价时段显著提示」。',
427
446
  groupGeneral: '常规',
@@ -723,6 +742,18 @@ window.__ModuleLoader__.load({
723
742
  peakStyleLabel: 'Peak period strip style',
724
743
  peakStyleCompact: 'Compact (one-line)',
725
744
  peakStyleClassic: 'Classic (segments & chip)',
745
+ peakAlertLabel: 'Popup alert before peak/off-peak switch',
746
+ peakAlertAheadLabel: 'Lead time (minutes, 1-30)',
747
+ peakAlertTargetLabel: 'Alert type',
748
+ peakAlertTargetPeak: 'Entering peak',
749
+ peakAlertTargetOffPeak: 'Entering off-peak',
750
+ peakAlertTargetBoth: 'Both',
751
+ peakAlertTitlePeak: 'Entering peak hours soon',
752
+ peakAlertTitleOffPeak: 'Entering off-peak hours soon',
753
+ peakAlertBody: 'Billing switches to {phase} pricing in about {time}; mind your call costs in this window.',
754
+ peakAlertPhasePeak: 'peak',
755
+ peakAlertPhaseOffPeak: 'off-peak',
756
+ peakAlertBtn: 'Got it',
726
757
  peakPanelTitle: 'Peak/off-peak pricing & notice',
727
758
  peakNoticeHiddenHint: 'Notice hidden: enable peak/off-peak pricing and the “prominent notice” toggle.',
728
759
  groupGeneral: 'General',
@@ -1024,6 +1055,9 @@ window.__ModuleLoader__.load({
1024
1055
  ? v.peakWindows.map((w, i) => ({ start: needNum(w.start, path + '.peakWindows[' + i + '].start'), end: needNum(w.end, path + '.peakWindows[' + i + '].end') }))
1025
1056
  : [],
1026
1057
  peakNotice: v.peakNotice !== false,
1058
+ peakAlertEnabled: v.peakAlertEnabled !== false,
1059
+ peakAlertAhead: Number.isFinite(v.peakAlertAhead) && v.peakAlertAhead >= 1 && v.peakAlertAhead <= 30 ? v.peakAlertAhead : 2,
1060
+ peakAlertTarget: v.peakAlertTarget === 'peak' || v.peakAlertTarget === 'offpeak' ? v.peakAlertTarget : 'both',
1027
1061
  peakStyle: v.peakStyle === 'classic' ? 'classic' : 'compact',
1028
1062
  priceMatch: v.priceMatch === 'exact' ? 'exact' : 'auto',
1029
1063
  priceOverrides: (() => {
@@ -1954,6 +1988,43 @@ window.__ModuleLoader__.load({
1954
1988
  : t('countdownMinute', { m: minutes })
1955
1989
  }
1956
1990
 
1991
+ /**
1992
+ * 峰/谷切换前弹窗提醒:距下次相位切换不足配置提前量(默认 2 分钟)时,
1993
+ * 右下角弹全局浮层;提醒类型按配置过滤(进入峰/进入谷/峰和谷);
1994
+ * 同一切换点只弹一次(手动关闭即记点),切换完成后浮层自然消失。
1995
+ */
1996
+ function PeakAlert(props) {
1997
+ const costStore = props.useCost ? props.useCost(s => s) : undefined
1998
+ const config = costStore?.state?.config
1999
+ const [now, setNow] = useState(Date.now())
2000
+ const [dismissedAt, setDismissedAt] = useState(null)
2001
+ useEffect(() => {
2002
+ const timer = window.setInterval(() => setNow(Date.now()), 10000)
2003
+ return () => window.clearInterval(timer)
2004
+ }, [])
2005
+ if (!config || config.peakAlertEnabled !== true || config.peakEnabled !== true) return null
2006
+ const effectiveAtMs = Date.parse(config.peakEffectiveAt || '')
2007
+ if (Number.isFinite(effectiveAtMs) && now < effectiveAtMs) return null
2008
+ const view = peakPhaseAt(now, Array.isArray(config.peakWindows) ? config.peakWindows : [])
2009
+ if (view === null) return null
2010
+ const target = config.peakAlertTarget === 'peak' || config.peakAlertTarget === 'offpeak' ? config.peakAlertTarget : 'both'
2011
+ if (target !== 'both' && target !== (view.nextIntoPeak ? 'peak' : 'offpeak')) return null
2012
+ const aheadMinutes = Number(config.peakAlertAhead)
2013
+ const aheadMs = (Number.isFinite(aheadMinutes) && aheadMinutes >= 1 ? aheadMinutes : 2) * 60000
2014
+ if (now < view.nextAtMs - aheadMs || now >= view.nextAtMs) return null
2015
+ if (dismissedAt === view.nextAtMs) return null
2016
+ const t = makeT(resolveLocale(config.locale))
2017
+ const intoPeak = view.nextIntoPeak === true
2018
+ return el('div', { className: 'cm-peak-alert ' + (intoPeak ? 'cm-peak-alert-peak' : 'cm-peak-alert-offpeak'), role: 'alert' },
2019
+ el('div', { className: 'cm-peak-alert-title' }, t(intoPeak ? 'peakAlertTitlePeak' : 'peakAlertTitleOffPeak')),
2020
+ el('div', { className: 'cm-peak-alert-body' }, t('peakAlertBody', {
2021
+ time: countdownText(view, now, t),
2022
+ phase: t(intoPeak ? 'peakAlertPhasePeak' : 'peakAlertPhaseOffPeak'),
2023
+ })),
2024
+ el('div', { className: 'cm-peak-alert-actions' },
2025
+ el('button', { className: 'cm-btn', onClick: () => setDismissedAt(view.nextAtMs) }, t('peakAlertBtn'))))
2026
+ }
2027
+
1957
2028
  /** 展开态简洁样式:单行紧凑时段条——细轨道(左橙右蓝,非当前段淡化)+ 标记线 + 右侧倒计时文本。 */
1958
2029
  function PeakStrip(props) {
1959
2030
  const { config, t } = props
@@ -2718,6 +2789,35 @@ window.__ModuleLoader__.load({
2718
2789
  },
2719
2790
  el('option', { value: 'compact' }, t('peakStyleCompact')),
2720
2791
  el('option', { value: 'classic' }, t('peakStyleClassic'))))),
2792
+ el('div', { className: 'cm-grid' },
2793
+ el('div', { className: 'cm-field' },
2794
+ el('label', { className: 'cm-check' },
2795
+ el('input', {
2796
+ type: 'checkbox',
2797
+ checked: draft?.peakAlertEnabled !== false,
2798
+ onChange: event => setField('peakAlertEnabled', event.target.checked),
2799
+ }),
2800
+ el('span', null, t('peakAlertLabel')))),
2801
+ el('div', { className: 'cm-field' },
2802
+ el('label', null, t('peakAlertAheadLabel')),
2803
+ el('input', {
2804
+ className: 'cm-input narrow', type: 'number', min: '1', max: '30', step: '1',
2805
+ value: String(typeof draft?.peakAlertAhead === 'number' && Number.isFinite(draft.peakAlertAhead) && draft.peakAlertAhead >= 1 && draft.peakAlertAhead <= 30 ? draft.peakAlertAhead : 2),
2806
+ onChange: event => {
2807
+ const parsed = Number(event.target.value)
2808
+ if (Number.isInteger(parsed) && parsed >= 1 && parsed <= 30) setField('peakAlertAhead', parsed)
2809
+ },
2810
+ })),
2811
+ el('div', { className: 'cm-field' },
2812
+ el('label', null, t('peakAlertTargetLabel')),
2813
+ el('select', {
2814
+ className: 'cm-input',
2815
+ value: draft?.peakAlertTarget === 'peak' || draft?.peakAlertTarget === 'offpeak' ? draft.peakAlertTarget : 'both',
2816
+ onChange: event => setField('peakAlertTarget', event.target.value),
2817
+ },
2818
+ el('option', { value: 'both' }, t('peakAlertTargetBoth')),
2819
+ el('option', { value: 'peak' }, t('peakAlertTargetPeak')),
2820
+ el('option', { value: 'offpeak' }, t('peakAlertTargetOffPeak'))))),
2721
2821
  el('div', { className: 'cm-peak-preview' },
2722
2822
  draft?.peakEnabled !== false && draft?.peakNotice !== false
2723
2823
  ? peakNoticeEl(state, previewConfig, t)
@@ -4406,6 +4506,26 @@ window.__ModuleLoader__.load({
4406
4506
  })
4407
4507
  }
4408
4508
 
4509
+ // 峰/谷切换前弹窗提醒:全局 fixed 浮层,挂在 dock 插槽常驻渲染,组件内部
4510
+ // 再按配置门控(peakAlertEnabled + peakEnabled);开关变化时重挂/卸载。
4511
+ const peakAlertActive = { gen: 0, dispose: null }
4512
+ const registerPeakAlert = enabled => {
4513
+ if (peakAlertActive.dispose !== null) { peakAlertActive.dispose(); peakAlertActive.dispose = null }
4514
+ peakAlertActive.gen += 1
4515
+ const gen = peakAlertActive.gen
4516
+ if (!enabled) return
4517
+ slots.inject('conversation.composer.dock', () => {
4518
+ if (peakAlertActive.gen !== gen) return
4519
+ const dispose = slots.register({ name: 'conversation.composer.dock', id: 'cost-meter-peak-alert', order: 8, inject: injected }, PeakAlert)
4520
+ if (peakAlertActive.gen !== gen) { dispose(); return }
4521
+ peakAlertActive.dispose = dispose
4522
+ return () => {
4523
+ if (peakAlertActive.dispose === dispose) peakAlertActive.dispose = null
4524
+ dispose()
4525
+ }
4526
+ })
4527
+ }
4528
+
4409
4529
  // 设置页「费用/Cost」分节:语言变化时撤销旧注册并重建,让侧边栏标签同步。
4410
4530
  const sectionActive = { gen: 0, dispose: null }
4411
4531
  const registerSection = locale => {
@@ -4478,6 +4598,7 @@ window.__ModuleLoader__.load({
4478
4598
  let lastCorner = null
4479
4599
  let lastSectionLocale = null
4480
4600
  let lastUsagePosition = null
4601
+ let lastPeakAlert = null
4481
4602
  const sync = () => {
4482
4603
  const state = store.getSnapshot().state
4483
4604
  const position = state?.config?.position ?? 'dock'
@@ -4500,6 +4621,11 @@ window.__ModuleLoader__.load({
4500
4621
  registerCorner(cornerEnabled)
4501
4622
  lastCorner = cornerEnabled
4502
4623
  }
4624
+ const peakAlertOn = state?.config?.peakAlertEnabled !== false && state?.config?.peakEnabled === true
4625
+ if (peakAlertOn !== lastPeakAlert) {
4626
+ registerPeakAlert(peakAlertOn)
4627
+ lastPeakAlert = peakAlertOn
4628
+ }
4503
4629
  if (sectionLocale !== lastSectionLocale) {
4504
4630
  registerSection(sectionLocale)
4505
4631
  lastSectionLocale = sectionLocale
package/lib/store.js CHANGED
@@ -38,6 +38,9 @@ export function defaultConfig() {
38
38
  peakEffectiveAt: DEFAULT_PEAK_EFFECTIVE_AT,
39
39
  peakWindows: DEFAULT_PEAK_WINDOWS.map(w => ({ ...w })),
40
40
  peakNotice: true, // 峰时高价时段显著提示(侧边栏预算框/今日费用/设置页预算面板)
41
+ peakAlertEnabled: true, // 峰/谷切换前弹窗提醒(全局浮层)
42
+ peakAlertAhead: 2, // 提前提醒量(分钟,1-30)
43
+ peakAlertTarget: 'both', // 提醒类型:peak(进入峰) | offpeak(进入谷) | both(峰和谷)
41
44
  showSessionId: false, // 会话列表中附显会话 ID(默认只显示标题,需要时开启)
42
45
  legacyAutoImportedAt: 0, // 安装前历史自动导入标记(issue #27):完成时刻 ms;0 = 尚未跑过
43
46
  peakStyle: 'compact', // 峰谷时段条样式:compact(简洁单行/竖向同构) | classic(经典分段/胶囊芯片)
@@ -221,6 +224,9 @@ const VALIDATION_MESSAGES = {
221
224
  peakEffectiveAt: 'peakEffectiveAt 非法',
222
225
  peakWindows: 'peakWindows 必须是数组',
223
226
  peakNotice: 'peakNotice 必须是布尔值',
227
+ peakAlertEnabled: 'peakAlertEnabled 必须是布尔值',
228
+ peakAlertAhead: 'peakAlertAhead 必须是 1-30 的整数',
229
+ peakAlertTarget: 'peakAlertTarget 必须是 peak / offpeak / both',
224
230
  showSessionId: 'showSessionId 必须是布尔值',
225
231
  peakStyle: 'peakStyle 必须是 compact / classic',
226
232
  priceMatch: 'priceMatch 必须是 auto / exact',
@@ -282,6 +288,9 @@ const VALIDATION_MESSAGES = {
282
288
  peakEffectiveAt: 'Invalid peakEffectiveAt',
283
289
  peakWindows: 'peakWindows must be an array',
284
290
  peakNotice: 'peakNotice must be a boolean',
291
+ peakAlertEnabled: 'peakAlertEnabled must be a boolean',
292
+ peakAlertAhead: 'peakAlertAhead must be an integer between 1 and 30',
293
+ peakAlertTarget: 'peakAlertTarget must be peak / offpeak / both',
285
294
  showSessionId: 'showSessionId must be a boolean',
286
295
  peakStyle: 'peakStyle must be compact / classic',
287
296
  priceMatch: 'priceMatch must be auto / exact',
@@ -405,6 +414,11 @@ export function applyConfigPatch(current, patch) {
405
414
  if (typeof candidate.peakEffectiveAt !== 'string') errors.push(vmsg(locale, 'peakEffectiveAt'))
406
415
  if (!Array.isArray(candidate.peakWindows)) errors.push(vmsg(locale, 'peakWindows'))
407
416
  if (typeof candidate.peakNotice !== 'boolean') errors.push(vmsg(locale, 'peakNotice'))
417
+ // 峰/谷切换前弹窗提醒:开关可缺省(默认开);提前量 1-30 分钟;类型三选一。
418
+ if (candidate.peakAlertEnabled !== undefined && typeof candidate.peakAlertEnabled !== 'boolean') errors.push(vmsg(locale, 'peakAlertEnabled'))
419
+ const alertAhead = Number(candidate.peakAlertAhead)
420
+ if (!Number.isInteger(alertAhead) || alertAhead < 1 || alertAhead > 30) errors.push(vmsg(locale, 'peakAlertAhead'))
421
+ if (!['peak', 'offpeak', 'both'].includes(candidate.peakAlertTarget)) errors.push(vmsg(locale, 'peakAlertTarget'))
408
422
  if (candidate.showSessionId !== undefined && typeof candidate.showSessionId !== 'boolean') errors.push(vmsg(locale, 'showSessionId'))
409
423
  if (candidate.peakStyle !== 'compact' && candidate.peakStyle !== 'classic') errors.push(vmsg(locale, 'peakStyle'))
410
424
  if (candidate.priceMatch !== 'auto' && candidate.priceMatch !== 'exact') errors.push(vmsg(locale, 'priceMatch'))
@@ -589,6 +603,11 @@ export function sanitizeConfig(raw) {
589
603
  out.decimals = Math.max(0, Math.min(10, Math.floor(Number(out.decimals) || 0)))
590
604
  out.historyDays = Math.max(7, Math.min(3650, Math.floor(Number(out.historyDays) || 180)))
591
605
  out.showSessionId = out.showSessionId === true
606
+ // 峰/谷切换弹窗提醒:非法值定向收敛(开关默认开、提前量回 2、类型回 both)。
607
+ out.peakAlertEnabled = out.peakAlertEnabled !== false
608
+ const alertAhead = Number(out.peakAlertAhead)
609
+ out.peakAlertAhead = Number.isInteger(alertAhead) && alertAhead >= 1 && alertAhead <= 30 ? alertAhead : 2
610
+ out.peakAlertTarget = oneOf(out.peakAlertTarget, ['peak', 'offpeak', 'both'], 'both')
592
611
  // 安装前历史自动导入标记:有限正数保留,否则归零(下次启动重试)。
593
612
  out.legacyAutoImportedAt = isNum(Number(out.legacyAutoImportedAt)) && Number(out.legacyAutoImportedAt) > 0 ? Number(out.legacyAutoImportedAt) : 0
594
613
  if (!isNum(out.exchangeRate) || out.exchangeRate <= 0) out.exchangeRate = base.exchangeRate
@@ -103,6 +103,10 @@ const configSchema = z.object({
103
103
  peakEffectiveAt: z.string(),
104
104
  peakWindows: z.array(z.object({ start: num, end: num })),
105
105
  peakNotice: z.boolean().optional(),
106
+ // 峰/谷切换前弹窗提醒:开关(默认开)/提前分钟数(1-30)/类型(峰|谷|两者)。
107
+ peakAlertEnabled: z.boolean().optional(),
108
+ peakAlertAhead: num.optional(),
109
+ peakAlertTarget: z.enum(['peak', 'offpeak', 'both']).optional(),
106
110
  showSessionId: z.boolean().optional(),
107
111
  // 安装前历史自动导入完成时刻(issue #27,内部标记;0/缺席 = 尚未跑过)。
108
112
  legacyAutoImportedAt: num.optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-cost-meter",
3
- "version": "1.5.15",
3
+ "version": "1.5.16",
4
4
  "description": "DeepSeek Harness 会话费用统计插件:本会话成本、当日费用、历史记录与官方价格同步,支持多厂商多模型价格计费(内置 90+ 模型价格目录与自动匹配)、主流 Coding Plan 订阅额度查询与显示(7 家,含 SCNet Token Plan 本地 Credits 计量)、自定义 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 7 vendors (incl. local credits metering for the SCNet Token Plan), custom provider balance lookup (configurable HTTP endpoint) and balance progress bar, in a bilingual (Chinese/English) UI.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",