dsh-cost-meter 1.5.16 → 1.5.18

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.16-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
9
+ [![version](https://img.shields.io/badge/version-1.5.18-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,7 +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
+ | Peak/off-peak switch popup alert | Global overlay | A full-width bracketed popup appears when the next tier switch is within the configured lead time (default 2 minutes, 1–30), with an alert-colored badge distinguishing entering peak vs off-peak; position selectable (**bottom-right / screen center**), alert type selectable (entering peak / entering off-peak / both), one alert per switch point; optionally **sends a browser (system) notification** (so you still get alerted when the page is backgrounded; requires granting notification permission); configured in the peak pricing panel in Settings |
44
44
  | Official price sync | Settings page | Fetches and parses the official pricing page, applies with one click |
45
45
  | UI language | Settings → Display settings | Simplified Chinese / English / Follow browser (auto); switches instantly and auto-saves |
46
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 |
@@ -193,22 +193,22 @@ Real captures from an actual DSH sidebar of the period strip and collapsed verti
193
193
  dsh plugin --profile web add dsh-cost-meter
194
194
  ```
195
195
 
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
+ **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.18`** — review the script before running):
197
197
 
198
198
  ```powershell
199
- irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.16/install.ps1 | iex
199
+ irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.18/install.ps1 | iex
200
200
  ```
201
201
 
202
202
  **Or a plain command line** (the machine must already have pnpm and git; also pinned to the tag):
203
203
 
204
204
  ```sh
205
- dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.16
205
+ dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.18
206
206
  ```
207
207
 
208
208
  Without git, use the GitHub tag archive:
209
209
 
210
210
  ```sh
211
- dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.16.tar.gz
211
+ dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.18.tar.gz
212
212
  ```
213
213
 
214
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.16-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
9
+ [![version](https://img.shields.io/badge/version-1.5.18-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,7 +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
+ | 峰/谷切换弹窗提醒 | 全局浮层 | 距进入峰/谷时段不足设定提前量(默认 2 分钟,1-30 可配)时全屏色条徽标弹窗(提醒色区分进入峰/谷);弹窗位置可选**右下角 / 屏幕中心**,提醒类型可选(进入峰 / 进入谷 / 峰和谷),同一切换点只提醒一次;可选**同步发送浏览器(系统)通知**(页面最小化也能收到,需授权通知权限),设置页峰谷计价面板内配置 |
45
45
  | 官方价格同步 | 设置页 | 抓取解析官方定价页,一键应用 |
46
46
  | 界面语言 | 设置页 → 显示设置 | 简体中文 / English / 跟随浏览器(自动);切换即时生效并自动保存 |
47
47
  | AI 价格同步 | [提示词](docs/AI-PRICE-SYNC-PROMPT.md) | DeepSeek 官方同步;其他 provider 使用已核对的官方价格目录与手动配置 |
@@ -195,22 +195,22 @@
195
195
  dsh plugin --profile web add dsh-cost-meter
196
196
  ```
197
197
 
198
- **PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.5.16`**,建议先下载审阅再运行):
198
+ **PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.5.18`**,建议先下载审阅再运行):
199
199
 
200
200
  ```powershell
201
- irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.16/install.ps1 | iex
201
+ irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.18/install.ps1 | iex
202
202
  ```
203
203
 
204
204
  **或直接命令行**(机器上需已有 pnpm 与 git;同样固定到 tag):
205
205
 
206
206
  ```sh
207
- dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.16
207
+ dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.18
208
208
  ```
209
209
 
210
210
  没有 git 时可用 GitHub tag 打包直链:
211
211
 
212
212
  ```sh
213
- dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.16.tar.gz
213
+ dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.18.tar.gz
214
214
  ```
215
215
 
216
216
  安装后**重启** `dsh web`(插件行、Typert 清单与客户端 bundle 均在启动时扫描):
package/lib/client.js CHANGED
@@ -110,13 +110,20 @@ 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
+ '.cm-peak-alert{position:fixed;z-index:9999;width:340px;max-width:calc(100vw - 32px);padding:16px;border-radius:14px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);box-shadow:0 14px 36px rgba(0,0,0,.22);display:flex;flex-direction:column;gap:8px;font-size:12px;animation:cm-peak-alert-in .22s cubic-bezier(.2,.8,.2,1)}',
114
+ '.cm-peak-alert.cm-peak-alert-corner{right:20px;bottom:20px}',
115
+ '.cm-peak-alert.cm-peak-alert-center{top:50%;left:50%;transform:translate(-50%,-50%);animation-name:cm-peak-alert-in-center}',
116
+ '.cm-peak-alert-peak{border-top:3px solid var(--dsw-alias-state-warn-primary)}',
117
+ '.cm-peak-alert-offpeak{border-top:3px solid var(--dsw-alias-state-info-primary,#3b82f6)}',
118
+ '.cm-peak-alert-badge{display:inline-flex;align-items:center;gap:6px;font-size:11px;font-weight:600;letter-spacing:.4px;text-transform:uppercase}',
119
+ '.cm-peak-alert-badge::before{content:"";width:8px;height:8px;border-radius:50%;background:currentColor;box-shadow:0 0 0 4px color-mix(in srgb,currentColor 16%,transparent)}',
120
+ '.cm-peak-alert-peak .cm-peak-alert-badge{color:var(--dsw-alias-state-warn-primary)}',
121
+ '.cm-peak-alert-offpeak .cm-peak-alert-badge{color:var(--dsw-alias-state-info-primary,#3b82f6)}',
122
+ '.cm-peak-alert-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}',
123
+ '.cm-peak-alert-body{color:var(--dsw-alias-label-secondary);line-height:1.55}',
124
+ '.cm-peak-alert-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:2px}',
125
+ '@keyframes cm-peak-alert-in{from{opacity:0;transform:translateY(10px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}',
126
+ '@keyframes cm-peak-alert-in-center{from{opacity:0;transform:translate(-50%,calc(-50% + 10px))}to{opacity:1;transform:translate(-50%,-50%)}}',
120
127
  '.cm-budget-head{display:flex;align-items:center;justify-content:space-between;gap:8px}',
121
128
  '.cm-budget-bar{height:8px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);overflow:hidden}',
122
129
  '.cm-budget-fill{height:100%;border-radius:999px;background:var(--dsw-alias-state-business-primary);transition:width .3s ease}',
@@ -441,6 +448,13 @@ window.__ModuleLoader__.load({
441
448
  peakAlertPhasePeak: '峰时',
442
449
  peakAlertPhaseOffPeak: '谷时',
443
450
  peakAlertBtn: '知道了',
451
+ peakAlertBadgePeak: '峰价提醒',
452
+ peakAlertBadgeOffPeak: '谷价提醒',
453
+ peakAlertPositionLabel: '弹窗位置',
454
+ peakAlertPositionCorner: '右下角',
455
+ peakAlertPositionCenter: '屏幕中心',
456
+ peakAlertWebNotifyLabel: '同步发送系统通知',
457
+ peakAlertWebNotifyHint: '开关后请在浏览器地址栏允许通知权限,才能在页面最小化/切走时收到提醒。',
444
458
  peakPanelTitle: '峰谷计价与提示',
445
459
  peakNoticeHiddenHint: '提示已隐藏:需启用峰谷计价并开启「峰时高价时段显著提示」。',
446
460
  groupGeneral: '常规',
@@ -754,6 +768,13 @@ window.__ModuleLoader__.load({
754
768
  peakAlertPhasePeak: 'peak',
755
769
  peakAlertPhaseOffPeak: 'off-peak',
756
770
  peakAlertBtn: 'Got it',
771
+ peakAlertBadgePeak: 'Peak alert',
772
+ peakAlertBadgeOffPeak: 'Off-peak alert',
773
+ peakAlertPositionLabel: 'Popup position',
774
+ peakAlertPositionCorner: 'Bottom-right',
775
+ peakAlertPositionCenter: 'Screen center',
776
+ peakAlertWebNotifyLabel: 'Also send a system notification',
777
+ peakAlertWebNotifyHint: 'When enabled, allow notification permission in the browser address bar so you get alerted even when the page is minimized or backgrounded.',
757
778
  peakPanelTitle: 'Peak/off-peak pricing & notice',
758
779
  peakNoticeHiddenHint: 'Notice hidden: enable peak/off-peak pricing and the “prominent notice” toggle.',
759
780
  groupGeneral: 'General',
@@ -1058,6 +1079,8 @@ window.__ModuleLoader__.load({
1058
1079
  peakAlertEnabled: v.peakAlertEnabled !== false,
1059
1080
  peakAlertAhead: Number.isFinite(v.peakAlertAhead) && v.peakAlertAhead >= 1 && v.peakAlertAhead <= 30 ? v.peakAlertAhead : 2,
1060
1081
  peakAlertTarget: v.peakAlertTarget === 'peak' || v.peakAlertTarget === 'offpeak' ? v.peakAlertTarget : 'both',
1082
+ peakAlertPosition: v.peakAlertPosition === 'center' ? 'center' : 'corner',
1083
+ peakAlertWebNotify: v.peakAlertWebNotify === true,
1061
1084
  peakStyle: v.peakStyle === 'classic' ? 'classic' : 'compact',
1062
1085
  priceMatch: v.priceMatch === 'exact' ? 'exact' : 'auto',
1063
1086
  priceOverrides: (() => {
@@ -1989,19 +2012,42 @@ window.__ModuleLoader__.load({
1989
2012
  }
1990
2013
 
1991
2014
  /**
1992
- * 峰/谷切换前弹窗提醒:距下次相位切换不足配置提前量(默认 2 分钟)时,
1993
- * 右下角弹全局浮层;提醒类型按配置过滤(进入峰/进入谷/峰和谷);
2015
+ * 峰/谷切换前弹窗提醒:距下次相位切换不足配置提前量(默认 2 分钟)时弹浮层,
2016
+ * 位置可选屏幕右下角 / 屏幕中心;提醒类型按配置过滤(进入峰/进入谷/峰和谷);
1994
2017
  * 同一切换点只弹一次(手动关闭即记点),切换完成后浮层自然消失。
2018
+ * 若开启 Web 通知且有授权,还会在同一切换点向系统发送一次浏览器通知。
1995
2019
  */
1996
2020
  function PeakAlert(props) {
1997
2021
  const costStore = props.useCost ? props.useCost(s => s) : undefined
1998
2022
  const config = costStore?.state?.config
1999
2023
  const [now, setNow] = useState(Date.now())
2000
2024
  const [dismissedAt, setDismissedAt] = useState(null)
2025
+ const notifiedAtRef = useRef(null)
2001
2026
  useEffect(() => {
2002
2027
  const timer = window.setInterval(() => setNow(Date.now()), 10000)
2003
2028
  return () => window.clearInterval(timer)
2004
2029
  }, [])
2030
+ useEffect(() => {
2031
+ // Web 通知:每次 tick 自包含重算切换点,同一切换点只发一次。
2032
+ if (!config || config.peakAlertEnabled !== true || config.peakEnabled !== true) return
2033
+ if (config.peakAlertWebNotify !== true || !window.Notification || Notification.permission !== 'granted') return
2034
+ if (notifiedAtRef.current === now) return
2035
+ const wv = peakPhaseAt(now, Array.isArray(config.peakWindows) ? config.peakWindows : [])
2036
+ if (wv === null) return
2037
+ const tgt = config.peakAlertTarget === 'peak' || config.peakAlertTarget === 'offpeak' ? config.peakAlertTarget : 'both'
2038
+ const intoPeak = wv.nextIntoPeak === true
2039
+ if (tgt !== 'both' && tgt !== (intoPeak ? 'peak' : 'offpeak')) return
2040
+ const mins = Number(config.peakAlertAhead)
2041
+ const aheadMs = (Number.isFinite(mins) && mins >= 1 ? mins : 2) * 60000
2042
+ if (now < wv.nextAtMs - aheadMs || now >= wv.nextAtMs) return
2043
+ const t = makeT(resolveLocale(config.locale))
2044
+ notifiedAtRef.current = now
2045
+ try {
2046
+ new Notification(
2047
+ t(intoPeak ? 'peakAlertTitlePeak' : 'peakAlertTitleOffPeak'),
2048
+ { body: t('peakAlertBody', { time: countdownText(wv, now, t), phase: t(intoPeak ? 'peakAlertPhasePeak' : 'peakAlertPhaseOffPeak') }) })
2049
+ } catch (_) { /* 通知被系统拒绝时静默 */ }
2050
+ }, [now]) // eslint-disable-line react-hooks/exhaustive-deps
2005
2051
  if (!config || config.peakAlertEnabled !== true || config.peakEnabled !== true) return null
2006
2052
  const effectiveAtMs = Date.parse(config.peakEffectiveAt || '')
2007
2053
  if (Number.isFinite(effectiveAtMs) && now < effectiveAtMs) return null
@@ -2015,14 +2061,17 @@ window.__ModuleLoader__.load({
2015
2061
  if (dismissedAt === view.nextAtMs) return null
2016
2062
  const t = makeT(resolveLocale(config.locale))
2017
2063
  const intoPeak = view.nextIntoPeak === true
2018
- return el('div', { className: 'cm-peak-alert ' + (intoPeak ? 'cm-peak-alert-peak' : 'cm-peak-alert-offpeak'), role: 'alert' },
2064
+ const position = config.peakAlertPosition === 'center' ? 'cm-peak-alert-center' : 'cm-peak-alert-corner'
2065
+ const actionBtn = (className, label, fn) => el('button', { className, onClick: fn }, label)
2066
+ return el('div', { className: 'cm-peak-alert ' + position + ' ' + (intoPeak ? 'cm-peak-alert-peak' : 'cm-peak-alert-offpeak'), role: 'alert' },
2067
+ el('div', { className: 'cm-peak-alert-badge' }, t(intoPeak ? 'peakAlertBadgePeak' : 'peakAlertBadgeOffPeak')),
2019
2068
  el('div', { className: 'cm-peak-alert-title' }, t(intoPeak ? 'peakAlertTitlePeak' : 'peakAlertTitleOffPeak')),
2020
2069
  el('div', { className: 'cm-peak-alert-body' }, t('peakAlertBody', {
2021
2070
  time: countdownText(view, now, t),
2022
2071
  phase: t(intoPeak ? 'peakAlertPhasePeak' : 'peakAlertPhaseOffPeak'),
2023
2072
  })),
2024
2073
  el('div', { className: 'cm-peak-alert-actions' },
2025
- el('button', { className: 'cm-btn', onClick: () => setDismissedAt(view.nextAtMs) }, t('peakAlertBtn'))))
2074
+ actionBtn('cm-btn', t('peakAlertBtn'), () => setDismissedAt(view.nextAtMs))))
2026
2075
  }
2027
2076
 
2028
2077
  /** 展开态简洁样式:单行紧凑时段条——细轨道(左橙右蓝,非当前段淡化)+ 标记线 + 右侧倒计时文本。 */
@@ -2818,6 +2867,32 @@ window.__ModuleLoader__.load({
2818
2867
  el('option', { value: 'both' }, t('peakAlertTargetBoth')),
2819
2868
  el('option', { value: 'peak' }, t('peakAlertTargetPeak')),
2820
2869
  el('option', { value: 'offpeak' }, t('peakAlertTargetOffPeak'))))),
2870
+ el('div', { className: 'cm-grid' },
2871
+ el('div', { className: 'cm-field' },
2872
+ el('label', null, t('peakAlertPositionLabel')),
2873
+ el('select', {
2874
+ className: 'cm-input',
2875
+ value: draft?.peakAlertPosition === 'center' ? 'center' : 'corner',
2876
+ onChange: event => setField('peakAlertPosition', event.target.value),
2877
+ },
2878
+ el('option', { value: 'corner' }, t('peakAlertPositionCorner')),
2879
+ el('option', { value: 'center' }, t('peakAlertPositionCenter')))),
2880
+ el('div', { className: 'cm-field' },
2881
+ el('label', { className: 'cm-check' },
2882
+ el('input', {
2883
+ type: 'checkbox',
2884
+ checked: draft?.peakAlertWebNotify === true,
2885
+ onChange: event => {
2886
+ setField('peakAlertWebNotify', event.target.checked)
2887
+ // 开启需用户手势申请浏览器通知权限(地址栏授权)。
2888
+ if (event.target.checked && window.Notification && Notification.permission === 'default') {
2889
+ // 旧 Safari 的 requestPermission 不返回 Promise,包一层防抛错。
2890
+ Promise.resolve(Notification.requestPermission()).catch(() => {})
2891
+ }
2892
+ },
2893
+ }),
2894
+ el('span', null, t('peakAlertWebNotifyLabel'))),
2895
+ el('p', { className: 'cm-hint' }, t('peakAlertWebNotifyHint')))),
2821
2896
  el('div', { className: 'cm-peak-preview' },
2822
2897
  draft?.peakEnabled !== false && draft?.peakNotice !== false
2823
2898
  ? peakNoticeEl(state, previewConfig, t)
package/lib/store.js CHANGED
@@ -41,6 +41,8 @@ export function defaultConfig() {
41
41
  peakAlertEnabled: true, // 峰/谷切换前弹窗提醒(全局浮层)
42
42
  peakAlertAhead: 2, // 提前提醒量(分钟,1-30)
43
43
  peakAlertTarget: 'both', // 提醒类型:peak(进入峰) | offpeak(进入谷) | both(峰和谷)
44
+ peakAlertPosition: 'corner', // 弹窗位置:corner(右下角) | center(屏幕中心)
45
+ peakAlertWebNotify: false, // 弹窗时额外发浏览器(系统)通知,需用户在地址栏授权
44
46
  showSessionId: false, // 会话列表中附显会话 ID(默认只显示标题,需要时开启)
45
47
  legacyAutoImportedAt: 0, // 安装前历史自动导入标记(issue #27):完成时刻 ms;0 = 尚未跑过
46
48
  peakStyle: 'compact', // 峰谷时段条样式:compact(简洁单行/竖向同构) | classic(经典分段/胶囊芯片)
@@ -227,6 +229,8 @@ const VALIDATION_MESSAGES = {
227
229
  peakAlertEnabled: 'peakAlertEnabled 必须是布尔值',
228
230
  peakAlertAhead: 'peakAlertAhead 必须是 1-30 的整数',
229
231
  peakAlertTarget: 'peakAlertTarget 必须是 peak / offpeak / both',
232
+ peakAlertPosition: 'peakAlertPosition 必须是 corner / center',
233
+ peakAlertWebNotify: 'peakAlertWebNotify 必须是布尔值',
230
234
  showSessionId: 'showSessionId 必须是布尔值',
231
235
  peakStyle: 'peakStyle 必须是 compact / classic',
232
236
  priceMatch: 'priceMatch 必须是 auto / exact',
@@ -291,6 +295,8 @@ const VALIDATION_MESSAGES = {
291
295
  peakAlertEnabled: 'peakAlertEnabled must be a boolean',
292
296
  peakAlertAhead: 'peakAlertAhead must be an integer between 1 and 30',
293
297
  peakAlertTarget: 'peakAlertTarget must be peak / offpeak / both',
298
+ peakAlertPosition: 'peakAlertPosition must be corner / center',
299
+ peakAlertWebNotify: 'peakAlertWebNotify must be a boolean',
294
300
  showSessionId: 'showSessionId must be a boolean',
295
301
  peakStyle: 'peakStyle must be compact / classic',
296
302
  priceMatch: 'priceMatch must be auto / exact',
@@ -419,6 +425,8 @@ export function applyConfigPatch(current, patch) {
419
425
  const alertAhead = Number(candidate.peakAlertAhead)
420
426
  if (!Number.isInteger(alertAhead) || alertAhead < 1 || alertAhead > 30) errors.push(vmsg(locale, 'peakAlertAhead'))
421
427
  if (!['peak', 'offpeak', 'both'].includes(candidate.peakAlertTarget)) errors.push(vmsg(locale, 'peakAlertTarget'))
428
+ if (!['corner', 'center'].includes(candidate.peakAlertPosition)) errors.push(vmsg(locale, 'peakAlertPosition'))
429
+ if (typeof candidate.peakAlertWebNotify !== 'boolean') errors.push(vmsg(locale, 'peakAlertWebNotify'))
422
430
  if (candidate.showSessionId !== undefined && typeof candidate.showSessionId !== 'boolean') errors.push(vmsg(locale, 'showSessionId'))
423
431
  if (candidate.peakStyle !== 'compact' && candidate.peakStyle !== 'classic') errors.push(vmsg(locale, 'peakStyle'))
424
432
  if (candidate.priceMatch !== 'auto' && candidate.priceMatch !== 'exact') errors.push(vmsg(locale, 'priceMatch'))
@@ -608,6 +616,8 @@ export function sanitizeConfig(raw) {
608
616
  const alertAhead = Number(out.peakAlertAhead)
609
617
  out.peakAlertAhead = Number.isInteger(alertAhead) && alertAhead >= 1 && alertAhead <= 30 ? alertAhead : 2
610
618
  out.peakAlertTarget = oneOf(out.peakAlertTarget, ['peak', 'offpeak', 'both'], 'both')
619
+ out.peakAlertPosition = oneOf(out.peakAlertPosition, ['corner', 'center'], 'corner')
620
+ out.peakAlertWebNotify = out.peakAlertWebNotify === true
611
621
  // 安装前历史自动导入标记:有限正数保留,否则归零(下次启动重试)。
612
622
  out.legacyAutoImportedAt = isNum(Number(out.legacyAutoImportedAt)) && Number(out.legacyAutoImportedAt) > 0 ? Number(out.legacyAutoImportedAt) : 0
613
623
  if (!isNum(out.exchangeRate) || out.exchangeRate <= 0) out.exchangeRate = base.exchangeRate
@@ -107,6 +107,8 @@ const configSchema = z.object({
107
107
  peakAlertEnabled: z.boolean().optional(),
108
108
  peakAlertAhead: num.optional(),
109
109
  peakAlertTarget: z.enum(['peak', 'offpeak', 'both']).optional(),
110
+ peakAlertPosition: z.enum(['corner', 'center']).optional(),
111
+ peakAlertWebNotify: z.boolean().optional(),
110
112
  showSessionId: z.boolean().optional(),
111
113
  // 安装前历史自动导入完成时刻(issue #27,内部标记;0/缺席 = 尚未跑过)。
112
114
  legacyAutoImportedAt: num.optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-cost-meter",
3
- "version": "1.5.16",
3
+ "version": "1.5.18",
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",