dsh-cost-meter 1.5.14 → 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.14-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)
@@ -36,10 +36,11 @@ English | [中文](README.md)
36
36
  | Token usage stats | Settings page (Cost section) | All-time token totals (input/cache/output/calls) + a Codex-style 26-week daily usage heat grid that fills the settings width; hover a cell for that day's detail |
37
37
  | Today's sessions | Settings page | Per-session call count, input/cache/output tokens and cost |
38
38
  | History | Settings page | Per-day totals; retention days configurable (default 180) |
39
- | Pre-install history import | Settings page (Data & sync) | One-click replay of all host session logs to import conversations from before the plugin was installed (missing dates are rebuilt whole; existing dates only gain previously unknown sessions; idempotent and never double-counts live metering; costs priced at per-event historical rates) |
39
+ | Pre-install history import | Automatic on first launch | After install/upgrade, the first launch automatically replays all host session logs to import conversations from before the plugin was installed (missing dates are rebuilt whole; existing dates only gain previously unknown sessions; idempotent and never double-counts live metering; costs priced at per-event historical rates); a manual re-run entry remains in Settings |
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.14`** — 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.14/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.14
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.14.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.14-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)
@@ -37,10 +37,11 @@
37
37
  | 今日会话明细 | 设置页 | 每个会话的调用次数、输入/缓存/输出 token 与费用 |
38
38
  | 历史记录 | 设置页 | 按天汇总,保留天数可配(默认 180 天) |
39
39
  | 历史按模型统计回填 | 设置页(按模型统计) | 按模型统计上线前的旧账本自动回放宿主会话日志重建逐模型 token/费用拆分(旧调用按当时基础价),日志已清理的部分归入「早期未分模型」残差行 |
40
- | 导入安装前历史 | 设置页(数据与同步) | 一键回放宿主全部会话日志,把未装插件时期的对话导入账本(缺失日期整日重建,已有日期只补未知会话,幂等不与实时计费重复;金额按事件时刻历史价回推) |
40
+ | 导入安装前历史 | 首次启动自动 | 安装/升级后首次启动自动回放宿主全部会话日志,把未装插件时期的对话导入账本(缺失日期整日重建,已有日期只补未知会话,幂等不与实时计费重复;金额按事件时刻历史价回推);设置页保留手动重跑入口 |
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.14`**,建议先下载审阅再运行):
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.14/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.14
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.14.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: '常规',
@@ -557,7 +576,7 @@ window.__ModuleLoader__.load({
557
576
  importLegacy: '导入安装前历史',
558
577
  confirmImportLegacy: '确认导入?将回放全部会话日志,只补账本缺失的日期与会话,不影响已有记录。',
559
578
  legacyImportFailed: '导入安装前历史失败:{message}',
560
- legacyImportNote: '按当前价目表回推历史费用;已清理的日志无法回放,同一会话跨安装时刻时其安装前用量不计入。',
579
+ legacyImportNote: '插件首次启动后会自动导入一次安装前历史;此处可手动重跑(如后续拷入了旧日志)。金额按当前价目表回推,已清理的日志无法回放,同一会话跨安装时刻时其安装前用量不计入。',
561
580
  confirmClear: '确认清除',
562
581
  clearAllHistory: '清除全部历史',
563
582
  lastSync: '最近同步:{time}',
@@ -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',
@@ -858,7 +889,7 @@ window.__ModuleLoader__.load({
858
889
  importLegacy: 'Import pre-install history',
859
890
  confirmImportLegacy: 'Confirm import? Replays all session logs and only fills dates/sessions missing from the ledger; existing records are untouched.',
860
891
  legacyImportFailed: 'Failed to import pre-install history: {message}',
861
- legacyImportNote: 'Historical costs are estimated with the current price catalog; cleaned logs cannot be replayed, and for a session spanning the install moment its pre-install usage is not counted.',
892
+ legacyImportNote: 'Pre-install history is imported automatically once on first launch; this button re-runs it manually (e.g. after copying in old logs). Costs are estimated with the current price catalog; cleaned logs cannot be replayed, and for a session spanning the install moment its pre-install usage is not counted.',
862
893
  confirmClear: 'Confirm clear',
863
894
  clearAllHistory: 'Clear all history',
864
895
  lastSync: 'Last sync: {time}',
@@ -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/index.js CHANGED
@@ -1017,6 +1017,34 @@ function createService(ctx, ledger) {
1017
1017
 
1018
1018
  // ── 插件主体 ───────────────────────────────────────────────────────────────
1019
1019
 
1020
+ /**
1021
+ * 启动期历史导入(issue #27):先做按模型回填,再在**本插件首次启动**(config
1022
+ * 标记 legacyAutoImportedAt 为 0)时自动导入一次安装前历史——用户无需手动触
1023
+ * 发;之后每次启动只跑回填,自动导入不再重复。导出供测试直接调用。
1024
+ * @param ledger - 已打开的账本。
1025
+ * @param sessionsRoot - 宿主会话根目录($DSH_HOME/sessions)。
1026
+ */
1027
+ export async function runStartupImports(ledger, sessionsRoot) {
1028
+ const filled = await backfillLegacyLedger(ledger, sessionsRoot)
1029
+ if (filled.days > 0 || filled.sessions > 0 || filled.titles > 0) {
1030
+ const extras = [
1031
+ filled.days > 0 || filled.sessions > 0 ? `${filled.days} 天 / ${filled.sessions} 个会话` : null,
1032
+ filled.titles > 0 ? `${filled.titles} 个会话标题` : null,
1033
+ filled.recosted > 0 ? `重算 ${filled.recosted} 天金额` : null,
1034
+ ].filter(Boolean).join(',')
1035
+ console.log(`[dsh-cost-meter] 历史按模型统计回填完成:${extras}(扫描 ${filled.scanned} 份会话日志)`)
1036
+ }
1037
+ if (!(Number(ledger.config?.legacyAutoImportedAt) > 0)) {
1038
+ const stats = await importLegacyHistory(ledger, sessionsRoot)
1039
+ if (stats.days > 0 || stats.sessions > 0) {
1040
+ console.log(`[dsh-cost-meter] 安装前历史自动导入完成:${stats.days} 天 / ${stats.sessions} 个会话(扫描 ${stats.scanned} 份会话日志)`)
1041
+ }
1042
+ // 无论是否有可导入内容都打标:空结果同样视为已完成,避免每次启动重扫。
1043
+ ledger.config.legacyAutoImportedAt = Date.now()
1044
+ ledger.scheduleWrite()
1045
+ }
1046
+ }
1047
+
1020
1048
  /**
1021
1049
  * 挂载账本、llm/stream 计费包裹、会话投影与 costMeter 服务。
1022
1050
  * @param ctx - 宿主插件上下文。
@@ -1028,20 +1056,11 @@ export function apply(ctx) {
1028
1056
  // 卸载/退出前最终落盘。
1029
1057
  ctx.effect(() => () => ledger.close(), 'cost-meter: ledger close')
1030
1058
 
1031
- // 历史账本按模型回填:按模型统计上线前的日期只有合计,启动后延迟回放
1032
- // 宿主会话日志填补空 byProviderModel(幂等,只填空条目,不重复计数)。
1059
+ // 历史账本按模型回填 + 首次启动自动导入安装前历史(issue #27):
1060
+ // 启动后延迟执行,避免拖慢宿主启动;均幂等,只补缺失,不重复计数。
1033
1061
  const backfillTimer = setTimeout(() => {
1034
- backfillLegacyLedger(ledger, join(resolveDshHome(), 'sessions')).then(filled => {
1035
- if (filled.days > 0 || filled.sessions > 0 || filled.titles > 0) {
1036
- const extras = [
1037
- filled.days > 0 || filled.sessions > 0 ? `${filled.days} 天 / ${filled.sessions} 个会话` : null,
1038
- filled.titles > 0 ? `${filled.titles} 个会话标题` : null,
1039
- filled.recosted > 0 ? `重算 ${filled.recosted} 天金额` : null,
1040
- ].filter(Boolean).join(',')
1041
- console.log(`[dsh-cost-meter] 历史按模型统计回填完成:${extras}(扫描 ${filled.scanned} 份会话日志)`)
1042
- }
1043
- }, error => {
1044
- console.warn(`[dsh-cost-meter] 历史按模型统计回填失败: ${String(error?.message ?? error)}`)
1062
+ runStartupImports(ledger, join(resolveDshHome(), 'sessions')).catch(error => {
1063
+ console.warn(`[dsh-cost-meter] 启动期历史导入失败: ${String(error?.message ?? error)}`)
1045
1064
  })
1046
1065
  }, 3000)
1047
1066
  backfillTimer.unref?.()
package/lib/store.js CHANGED
@@ -38,7 +38,11 @@ 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(默认只显示标题,需要时开启)
45
+ legacyAutoImportedAt: 0, // 安装前历史自动导入标记(issue #27):完成时刻 ms;0 = 尚未跑过
42
46
  peakStyle: 'compact', // 峰谷时段条样式:compact(简洁单行/竖向同构) | classic(经典分段/胶囊芯片)
43
47
  priceMatch: 'auto', // 未知模型名自动匹配价格表:auto(去后缀/前缀/家族相似) | exact(仅精确)
44
48
  priceOverrides: {}, // 手动匹配覆盖:{ 'provider:modelId': '同provider模型 | provider:模型 | deepseek:__default__' }
@@ -220,6 +224,9 @@ const VALIDATION_MESSAGES = {
220
224
  peakEffectiveAt: 'peakEffectiveAt 非法',
221
225
  peakWindows: 'peakWindows 必须是数组',
222
226
  peakNotice: 'peakNotice 必须是布尔值',
227
+ peakAlertEnabled: 'peakAlertEnabled 必须是布尔值',
228
+ peakAlertAhead: 'peakAlertAhead 必须是 1-30 的整数',
229
+ peakAlertTarget: 'peakAlertTarget 必须是 peak / offpeak / both',
223
230
  showSessionId: 'showSessionId 必须是布尔值',
224
231
  peakStyle: 'peakStyle 必须是 compact / classic',
225
232
  priceMatch: 'priceMatch 必须是 auto / exact',
@@ -281,6 +288,9 @@ const VALIDATION_MESSAGES = {
281
288
  peakEffectiveAt: 'Invalid peakEffectiveAt',
282
289
  peakWindows: 'peakWindows must be an array',
283
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',
284
294
  showSessionId: 'showSessionId must be a boolean',
285
295
  peakStyle: 'peakStyle must be compact / classic',
286
296
  priceMatch: 'priceMatch must be auto / exact',
@@ -404,6 +414,11 @@ export function applyConfigPatch(current, patch) {
404
414
  if (typeof candidate.peakEffectiveAt !== 'string') errors.push(vmsg(locale, 'peakEffectiveAt'))
405
415
  if (!Array.isArray(candidate.peakWindows)) errors.push(vmsg(locale, 'peakWindows'))
406
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'))
407
422
  if (candidate.showSessionId !== undefined && typeof candidate.showSessionId !== 'boolean') errors.push(vmsg(locale, 'showSessionId'))
408
423
  if (candidate.peakStyle !== 'compact' && candidate.peakStyle !== 'classic') errors.push(vmsg(locale, 'peakStyle'))
409
424
  if (candidate.priceMatch !== 'auto' && candidate.priceMatch !== 'exact') errors.push(vmsg(locale, 'priceMatch'))
@@ -588,6 +603,13 @@ export function sanitizeConfig(raw) {
588
603
  out.decimals = Math.max(0, Math.min(10, Math.floor(Number(out.decimals) || 0)))
589
604
  out.historyDays = Math.max(7, Math.min(3650, Math.floor(Number(out.historyDays) || 180)))
590
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')
611
+ // 安装前历史自动导入标记:有限正数保留,否则归零(下次启动重试)。
612
+ out.legacyAutoImportedAt = isNum(Number(out.legacyAutoImportedAt)) && Number(out.legacyAutoImportedAt) > 0 ? Number(out.legacyAutoImportedAt) : 0
591
613
  if (!isNum(out.exchangeRate) || out.exchangeRate <= 0) out.exchangeRate = base.exchangeRate
592
614
  if (!Array.isArray(out.peakWindows)) out.peakWindows = base.peakWindows
593
615
  else out.peakWindows = out.peakWindows.filter(w => w !== null && typeof w === 'object' && isNum(Number(w.start)) && isNum(Number(w.end)))
@@ -103,7 +103,13 @@ 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(),
111
+ // 安装前历史自动导入完成时刻(issue #27,内部标记;0/缺席 = 尚未跑过)。
112
+ legacyAutoImportedAt: num.optional(),
107
113
  peakStyle: z.enum(['compact', 'classic']).optional(),
108
114
  priceMatch: z.enum(['auto', 'exact']).optional(),
109
115
  priceOverrides: z.record(z.string(), z.string()).optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-cost-meter",
3
- "version": "1.5.14",
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",