dsh-cost-meter 1.5.31 → 1.5.32

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) · pre-switch popup & system-notification alerts for peak/off-peak changes (position / lead time / alert type configurable) · 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 / CommandCode / SCNet) · quota strip above the input box (budget / Go / coding-plan usage in one row, toggleable)
8
8
 
9
- [![version](https://img.shields.io/badge/version-1.5.31-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
9
+ [![version](https://img.shields.io/badge/version-1.5.32-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)
@@ -19,7 +19,7 @@ English | [中文](README.md)
19
19
 
20
20
  ---
21
21
 
22
- ![Promo art](docs/promo.png)
22
+ ![Promo art](docs/promo.en.png)
23
23
 
24
24
  ## Feature overview
25
25
 
@@ -31,6 +31,7 @@ English | [中文](README.md)
31
31
  | OpenCode Go quota | Sidebar / Settings / bottom-right dock (configurable) | Rolling-5h / weekly / monthly usage percent and reset times, each window toggleable independently, budget used % can show alongside; key auto-discovered (DSH credential store OPENCODE_GO_API_KEY / env / opencode login) or entered manually |
32
32
  | Coding plan quotas | Sidebar / Settings page (per vendor) | Multi-vendor coding-plan quota queries (Anthropic Claude Pro/Max, Z.ai / Zhipu GLM Coding Plan, MiniMax Token Plan, Kimi/Moonshot balance, OpenRouter credits, SiliconFlow balance, CommandCode 5h/weekly windows + monthly credits balance); per-vendor enable switch, key, display position and refresh interval (sidebar card in the same box style as the Go quota; the collapsed rail shows percentages), credentials only sent to official endpoints; neutral hints when no credentials/subscription; SCNet Token Plan has no quota API — monthly usage is estimated from the local ledger via the official credits deduction table (no credentials needed) |
33
33
  | Quota strip | Above the input box (toggle in Display settings) | One compact chip row for budget used % / the Go main window / each enabled coding-plan usage window (short label + mini progress bar, ≥80% warn, ≥100% over, hover for reset times); a first-run guide card lets you decide whether to enable it; hides itself when there is no quota data |
34
+ | Click to refresh | Sidebar balance/quota boxes | Click the official balance / custom balance / coding-plan box (collapsed rail included) to fetch the latest data immediately; the box pulses while refreshing, failures keep the previous value and surface the reason in the hover tooltip; keyboard Enter/Space also triggers; a one-time guide card appears after the update |
34
35
  | Today's cost | Sidebar bottom (above the settings button) | “Today ¥x”, hover for call count and token details |
35
36
  | Budget box | Sidebar bottom (between the balance row and the settings button) | Rounded-square frame: budget, used %, progress bar, today's cost & share of budget, used/limit; ≥80% warning, ≥100% over-budget |
36
37
  | Summary cards | Settings page | Today / this month / cumulative cost and call counts |
@@ -227,22 +228,22 @@ Real captures from an actual DSH sidebar of the period strip and collapsed verti
227
228
  dsh plugin --profile web add dsh-cost-meter
228
229
  ```
229
230
 
230
- **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.31`** — review the script before running):
231
+ **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.32`** — review the script before running):
231
232
 
232
233
  ```powershell
233
- irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.31/install.ps1 | iex
234
+ irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.32/install.ps1 | iex
234
235
  ```
235
236
 
236
237
  **Or a plain command line** (the machine must already have pnpm and git; also pinned to the tag):
237
238
 
238
239
  ```sh
239
- dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.31
240
+ dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.32
240
241
  ```
241
242
 
242
243
  Without git, use the GitHub tag archive:
243
244
 
244
245
  ```sh
245
- dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.31.tar.gz
246
+ dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.32.tar.gz
246
247
  ```
247
248
 
248
249
  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 / CommandCode / SCNet 八家) · 输入框上方额度横条(预算/Go/Coding Plan 用量一条横排显示,可开关)
8
8
 
9
- [![version](https://img.shields.io/badge/version-1.5.31-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
9
+ [![version](https://img.shields.io/badge/version-1.5.32-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)
@@ -31,6 +31,7 @@
31
31
  | OpenCode Go 额度 | 侧边栏 / 设置页 / 右下角(dock,可配) | 滚动 5 小时 / 本周 / 本月用量百分比与重置时间,三档可分别开关,可同时显示预算已用%;Key 自动发现(DSH 凭据库 OPENCODE_GO_API_KEY / 环境变量 / opencode 登录态)或手动填写 |
32
32
  | Coding Plan 额度 | 侧边栏 / 设置页(每家可配) | 多厂商 coding plan 订阅额度查询(Anthropic Claude Pro/Max、Z.ai/智谱 GLM、MiniMax Token Plan、Kimi/Moonshot 余额、OpenRouter credits、SiliconFlow 余额、CommandCode 5h/周窗口与月度 Credits 余额),各家独立启用开关、Key、显示位置与刷新间隔(侧边栏卡片与 Go 额度同款,收起窄栏显示百分比),凭据只发往官方端点;无凭据/无订阅为中性提示;SCNet 超算互联网 Token Plan 无 API 额度端点,按官方 Credits 抵扣表由本地账本估算月度用量(无需凭据) |
33
33
  | 额度横条 | 输入框上方(显示设置可开关) | 一条横排 chips 实时显示预算已用% / Go 主窗口 / 各已启用 Coding Plan 用量窗口(短标签+迷你进度条,≥80% 预警、≥100% 超支,悬停见重置时刻);首次更新弹引导卡由用户自主决定开关;无可用数据自动隐藏 |
34
+ | 点击立即刷新 | 侧边栏余额/额度图框 | 官方余额 / 自定义余额 / Coding Plan 图框(含窄栏收起态)点击即触发一次查询,刷新中呼吸闪烁,失败保持原值并在悬停提示说明;键盘 Enter/Space 可触发;更新后首次进入有引导提示 |
34
35
  | 当日费用 | 侧边栏底部(设置按钮上方) | 「今日 ¥x」,悬停见调用次数与 token 明细 |
35
36
  | 预算图框 | 侧边栏底部(余额行与设置按钮之间) | 圆角方形图框:预算、已用%、进度条、今日费用与占预算%、已用/额度,≥80% 预警、≥100% 超支 |
36
37
  | 汇总卡片 | 设置页 | 今日 / 本月 / 累计费用与调用次数 |
@@ -229,22 +230,22 @@
229
230
  dsh plugin --profile web add dsh-cost-meter
230
231
  ```
231
232
 
232
- **PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.5.31`**,建议先下载审阅再运行):
233
+ **PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.5.32`**,建议先下载审阅再运行):
233
234
 
234
235
  ```powershell
235
- irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.31/install.ps1 | iex
236
+ irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.32/install.ps1 | iex
236
237
  ```
237
238
 
238
239
  **或直接命令行**(机器上需已有 pnpm 与 git;同样固定到 tag):
239
240
 
240
241
  ```sh
241
- dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.31
242
+ dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.32
242
243
  ```
243
244
 
244
245
  没有 git 时可用 GitHub tag 打包直链:
245
246
 
246
247
  ```sh
247
- dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.31.tar.gz
248
+ dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.32.tar.gz
248
249
  ```
249
250
 
250
251
  安装后**重启** `dsh web`(插件行、Typert 清单与客户端 bundle 均在启动时扫描):
package/lib/client.js CHANGED
@@ -272,6 +272,12 @@ window.__ModuleLoader__.load({
272
272
  '.cm-bbox-section.over .cm-bbox-pct{color:var(--dsw-alias-state-error-primary)}',
273
273
  '.cm-bbox-section.warn .cm-bbox-fill{background:var(--dsw-alias-state-warn-primary)}',
274
274
  '.cm-bbox-section.over .cm-bbox-fill{background:var(--dsw-alias-state-error-primary)}',
275
+ // 点击立即刷新(issue #37):余额/额度图框可点击,刷新中呼吸闪烁。
276
+ '.cm-bbox.clickable,.cm-foot.clickable{cursor:pointer}',
277
+ '.cm-bbox.clickable:hover{border-color:var(--dsw-alias-state-business-primary)}',
278
+ '.cm-bbox.clickable:focus-visible,.cm-foot.clickable:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}',
279
+ '@keyframes cm-click-refresh-pulse{0%,100%{opacity:.55}50%{opacity:1}}',
280
+ '.cm-bbox.clickable.busy,.cm-foot.clickable.busy{animation:cm-click-refresh-pulse 1.1s ease-in-out infinite}',
275
281
  '.cm-mm{padding:8px 10px;gap:4px}',
276
282
  '.cm-mm-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary)}',
277
283
  '.cm-mm-row{display:flex;align-items:center;gap:8px;padding:2px 0}',
@@ -606,6 +612,10 @@ window.__ModuleLoader__.load({
606
612
  quotaStripGuideBody: '可以在输入框上方用一条横条实时查看预算与多家订阅额度(OpenCode Go / Coding Plan)的用量百分比。要现在开启吗?可随时在「设置 → 显示设置」中更改。',
607
613
  quotaStripGuideOn: '开启横条',
608
614
  quotaStripGuideOff: '暂不开启',
615
+ clickToRefresh: '点击立即刷新',
616
+ balanceClickGuideTitle: '新增:余额图框点击刷新',
617
+ balanceClickGuideBody: '现在点击侧边栏的官方余额 / 自定义余额 / Coding Plan 图框,即可立即刷新最新数据,无需等待自动刷新间隔或进入设置页;刷新中会有呼吸闪烁,失败时保持原值并在悬停提示中说明原因。',
618
+ balanceClickGuideOk: '知道了',
609
619
  sidebarLabel: '当日费用显示',
610
620
  sidebarOn: '侧边栏底部',
611
621
  currencyLabel: '货币单位',
@@ -951,6 +961,10 @@ window.__ModuleLoader__.load({
951
961
  quotaStripGuideBody: 'Show budget and subscription quotas (OpenCode Go / coding plans) as one compact strip above the input box. Enable it now? You can change this anytime in Settings → Display.',
952
962
  quotaStripGuideOn: 'Enable the strip',
953
963
  quotaStripGuideOff: 'Not now',
964
+ clickToRefresh: 'Click to refresh now',
965
+ balanceClickGuideTitle: 'New: click the balance box to refresh',
966
+ balanceClickGuideBody: 'Click the official balance / custom balance / coding-plan box in the sidebar to fetch the latest data immediately — no need to wait for the auto-refresh interval or open Settings. While refreshing the box pulses; on failure the previous value is kept and the reason appears in the hover tooltip.',
967
+ balanceClickGuideOk: 'Got it',
954
968
  sidebarLabel: 'Today cost display',
955
969
  sidebarOn: 'Sidebar footer',
956
970
  currencyLabel: 'Currency',
@@ -1214,6 +1228,7 @@ window.__ModuleLoader__.load({
1214
1228
  refreshMinutes: typeof v.balance?.refreshMinutes === 'number' && Number.isFinite(v.balance.refreshMinutes) ? v.balance.refreshMinutes : 5,
1215
1229
  showProgressBar: v.balance?.showProgressBar === true,
1216
1230
  budgetCap: typeof v.balance?.budgetCap === 'number' && Number.isFinite(v.balance.budgetCap) && v.balance.budgetCap > 0 ? v.balance.budgetCap : null,
1231
+ clickHintSeen: v.balance?.clickHintSeen === true,
1217
1232
  },
1218
1233
  goQuota: {
1219
1234
  enabled: v.goQuota?.enabled !== false,
@@ -1838,14 +1853,51 @@ window.__ModuleLoader__.load({
1838
1853
  return lines.join(' · ')
1839
1854
  }
1840
1855
 
1856
+ // 点击立即刷新(issue #37):侧边栏余额/额度图框点击触发一次手动刷新;
1857
+ // busy 期间忽略连点防并发;失败保持原值(fast-fail 抛错不会写入 store),
1858
+ // 错误信息供 tooltip 展示,下次刷新开始时清除。
1859
+ function useClickRefresh(call) {
1860
+ const [busy, setBusy] = useState(false)
1861
+ const [err, setErr] = useState(null)
1862
+ const run = () => {
1863
+ if (busy || typeof call !== 'function') return
1864
+ setBusy(true)
1865
+ setErr(null)
1866
+ Promise.resolve().then(call)
1867
+ .catch(error => { setErr(error?.message ?? String(error)) })
1868
+ .finally(() => { setBusy(false) })
1869
+ }
1870
+ return { busy, err, run }
1871
+ }
1872
+
1873
+ // 可点击图框的通用 a11y/事件属性(键盘 Enter/Space 同样触发;aria-busy 标记刷新中)。
1874
+ const clickableRefreshProps = (busy, run) => ({
1875
+ role: 'button',
1876
+ tabIndex: 0,
1877
+ 'aria-busy': busy ? 'true' : 'false',
1878
+ onClick: run,
1879
+ onKeyDown: event => {
1880
+ if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); run() }
1881
+ },
1882
+ })
1883
+
1884
+ // 点击刷新的 tooltip 附加行:提示语 + 刷新中 + 最近一次失败原因。
1885
+ function clickRefreshTipLines(t, refresh) {
1886
+ const lines = [t('clickToRefresh')]
1887
+ if (refresh.busy) lines.push(t('refreshing'))
1888
+ if (refresh.err) lines.push('⚠ ' + t('balanceRefreshFailed', { message: refresh.err }))
1889
+ return lines
1890
+ }
1891
+
1841
1892
  function BalanceRowContent(props) {
1842
- const { state, wide } = props
1893
+ const { state, wide, api } = props
1843
1894
  const balance = state.balance
1844
1895
  const t = makeT(resolveLocale(state.config?.locale))
1896
+ const refresh = useClickRefresh(api ? () => api.refreshBalance() : null)
1845
1897
  if (!balance || balance.status === 'off') return null
1846
1898
  if (balance.status === 'error') {
1847
- return el(Tooltip, { label: t('balanceQueryFailed', { message: balance.message || t('unknownError') }), side: 'right', delayMs: 300 },
1848
- el('div', { className: 'cm-foot' + (wide ? '' : ' cm-foot-rail') + ' cm-bal-err' },
1899
+ return el(Tooltip, { label: [t('balanceQueryFailed', { message: balance.message || t('unknownError') }), ...clickRefreshTipLines(t, refresh)].join('; '), side: 'right', delayMs: 300 },
1900
+ el('div', { className: 'cm-foot clickable' + (wide ? '' : ' cm-foot-rail') + ' cm-bal-err' + (refresh.busy ? ' busy' : ''), ...clickableRefreshProps(refresh.busy, refresh.run) },
1849
1901
  wide ? el(Fragment, null, t('balance'), ' ', el('span', { className: 'cm-num' }, t('queryFailed'))) : '⚠'))
1850
1902
  }
1851
1903
  const detail = [
@@ -1857,17 +1909,19 @@ window.__ModuleLoader__.load({
1857
1909
  }),
1858
1910
  t('updatedAt', { time: new Date(balance.fetchedAt).toLocaleTimeString() }),
1859
1911
  ...(state.reconcile?.ok === false ? ['⚠ ' + state.reconcile.message] : []),
1912
+ ...clickRefreshTipLines(t, refresh),
1860
1913
  ].join('; ')
1861
1914
  return el(Tooltip, { label: detail, side: 'right', delayMs: 300 },
1862
- el('div', { className: 'cm-foot' + (wide ? '' : ' cm-foot-rail') },
1915
+ el('div', { className: 'cm-foot clickable' + (wide ? '' : ' cm-foot-rail') + (refresh.busy ? ' busy' : ''), ...clickableRefreshProps(refresh.busy, refresh.run) },
1863
1916
  wide ? el(Fragment, null, t('balance'), ' ', el('span', { className: 'cm-num' }, formatBalanceMoney(balance.totalBalance, state.config)), state.reconcile?.ok === false ? ' ⚠' : '') : el(WalletIcon, { size: 16 })))
1864
1917
  }
1865
1918
 
1866
1919
  function BalanceBox(props) {
1867
- const { state, wide } = props
1920
+ const { state, wide, api } = props
1868
1921
  const config = state.config
1869
1922
  const balance = state.balance
1870
1923
  const t = makeT(resolveLocale(config?.locale))
1924
+ const refresh = useClickRefresh(api ? () => api.refreshBalance() : null)
1871
1925
  if (!balance || balance.status !== 'ok') return null
1872
1926
  const segments = segmentsForOfficialBalance(state, config)
1873
1927
  const remaining = formatBalanceMoney(balance.totalBalance, config)
@@ -1881,6 +1935,7 @@ window.__ModuleLoader__.load({
1881
1935
  }),
1882
1936
  t('updatedAt', { time: new Date(balance.fetchedAt).toLocaleTimeString() }),
1883
1937
  ...(state.reconcile?.ok === false ? ['⚠ ' + state.reconcile.message] : []),
1938
+ ...clickRefreshTipLines(t, refresh),
1884
1939
  ].join('; ')
1885
1940
  const body = el(Fragment, null,
1886
1941
  el('div', { className: 'cm-bbox-head' },
@@ -1888,7 +1943,7 @@ window.__ModuleLoader__.load({
1888
1943
  el('span', { className: 'cm-bbox-pct cm-num cm-bal-amt' }, remaining)),
1889
1944
  el(BalanceBar, { segments }))
1890
1945
  return el(Tooltip, { label: detail, side: 'right', delayMs: 300 },
1891
- el('div', { className: 'cm-bbox' + (wide ? '' : ' rail') },
1946
+ el('div', { className: 'cm-bbox clickable' + (wide ? '' : ' rail') + (refresh.busy ? ' busy' : ''), ...clickableRefreshProps(refresh.busy, refresh.run) },
1892
1947
  wide ? body : el('div', { className: 'cm-bbox-rail cm-num' }, Math.round(segments.remainingPct) + '%')))
1893
1948
  }
1894
1949
 
@@ -1961,34 +2016,36 @@ window.__ModuleLoader__.load({
1961
2016
  }
1962
2017
 
1963
2018
  function CustomBalanceBox(props) {
1964
- const { state, wide } = props
2019
+ const { state, wide, api } = props
1965
2020
  const custom = state.customBalance
1966
2021
  const config = state.config
1967
2022
  const t = makeT(resolveLocale(config?.locale))
2023
+ const refresh = useClickRefresh(api ? () => api.refreshCustomBalance() : null)
1968
2024
  if (!custom || custom.status !== 'ok') return null
1969
2025
  const view = customBalanceBoxBody(state, config, t)
1970
- const detail = customBalanceDetailText(custom, config, t, state)
2026
+ const detail = [customBalanceDetailText(custom, config, t, state), ...clickRefreshTipLines(t, refresh)].join(' · ')
1971
2027
  return el(Tooltip, { label: detail, side: 'right', delayMs: 300 },
1972
- el('div', { className: 'cm-bbox' + (view.level === 'ok' ? '' : ' ' + view.level) + (wide ? '' : ' rail') },
2028
+ el('div', { className: 'cm-bbox clickable' + (view.level === 'ok' ? '' : ' ' + view.level) + (wide ? '' : ' rail') + (refresh.busy ? ' busy' : ''), ...clickableRefreshProps(refresh.busy, refresh.run) },
1973
2029
  wide ? view.body : el('div', { className: 'cm-bbox-rail cm-num' }, view.rail)))
1974
2030
  }
1975
2031
 
1976
2032
  function CustomBalanceRowContent(props) {
1977
- const { state, wide } = props
2033
+ const { state, wide, api } = props
1978
2034
  const custom = state.customBalance
1979
2035
  const config = state.config
1980
2036
  const t = makeT(resolveLocale(config?.locale))
2037
+ const refresh = useClickRefresh(api ? () => api.refreshCustomBalance() : null)
1981
2038
  if (!custom || custom.status === 'off') return null
1982
2039
  const label = resolveCustomBalanceLabel(config.customBalance ?? {}, resolveLocale(config?.locale))
1983
2040
  if (custom.status === 'error') {
1984
- return el(Tooltip, { label: custom.message || t('unknownError'), side: 'right', delayMs: 300 },
1985
- el('div', { className: 'cm-foot' + (wide ? '' : ' cm-foot-rail') + ' cm-bal-err' },
2041
+ return el(Tooltip, { label: [custom.message || t('unknownError'), ...clickRefreshTipLines(t, refresh)].join('; '), side: 'right', delayMs: 300 },
2042
+ el('div', { className: 'cm-foot clickable' + (wide ? '' : ' cm-foot-rail') + ' cm-bal-err' + (refresh.busy ? ' busy' : ''), ...clickableRefreshProps(refresh.busy, refresh.run) },
1986
2043
  wide ? el(Fragment, null, label, ' ', el('span', { className: 'cm-num' }, t('queryFailed'))) : '⚠'))
1987
2044
  }
1988
2045
  const amount = formatCustomBalanceMoney(custom.remaining, config, custom)
1989
- const detail = customBalanceDetailText(custom, config, t, state)
2046
+ const detail = [customBalanceDetailText(custom, config, t, state), ...clickRefreshTipLines(t, refresh)].join(' · ')
1990
2047
  return el(Tooltip, { label: detail, side: 'right', delayMs: 300 },
1991
- el('div', { className: 'cm-foot' + (wide ? '' : ' cm-foot-rail') },
2048
+ el('div', { className: 'cm-foot clickable' + (wide ? '' : ' cm-foot-rail') + (refresh.busy ? ' busy' : ''), ...clickableRefreshProps(refresh.busy, refresh.run) },
1992
2049
  wide ? el(Fragment, null, label, ' ', el('span', { className: 'cm-num' }, amount)) : el(WalletIcon, { size: 16 })))
1993
2050
  }
1994
2051
 
@@ -2115,6 +2172,11 @@ window.__ModuleLoader__.load({
2115
2172
  // 预览通道事件名:设置页按钮/控制台经 window.cmPeakAlertPreview(kind) 触发真实组件。
2116
2173
  const PEAK_ALERT_PREVIEW_EVENT = 'cm-peak-alert-preview'
2117
2174
 
2175
+ // 峰谷 Web 通知去重:记录最近一次已通知的切换点时刻(nextAtMs)。
2176
+ // 必须放模块级——配置变化会重挂 PeakAlert(内部 ref 归零),若用组件内状态,
2177
+ // 重挂后同一切换点会再发一条;切换点时刻单调递增,存单值即可。
2178
+ let lastPeakNotifyAtMs = 0
2179
+
2118
2180
  /**
2119
2181
  * 峰/谷切换前弹窗提醒:距下次相位切换不足配置提前量(默认 2 分钟)时弹浮层,
2120
2182
  * 位置可选屏幕右下角 / 屏幕中心;提醒类型按配置过滤(进入峰/进入谷/峰和谷);
@@ -2129,7 +2191,6 @@ window.__ModuleLoader__.load({
2129
2191
  const [now, setNow] = useState(Date.now())
2130
2192
  const [dismissedAt, setDismissedAt] = useState(null)
2131
2193
  const [preview, setPreview] = useState(null)
2132
- const notifiedAtRef = useRef(null)
2133
2194
  useEffect(() => {
2134
2195
  const timer = window.setInterval(() => setNow(Date.now()), 10000)
2135
2196
  return () => window.clearInterval(timer)
@@ -2158,10 +2219,10 @@ window.__ModuleLoader__.load({
2158
2219
  }
2159
2220
  }, []) // eslint-disable-line react-hooks/exhaustive-deps
2160
2221
  useEffect(() => {
2161
- // Web 通知:每次 tick 自包含重算切换点,同一切换点只发一次。
2222
+ // Web 通知:每次 tick 自包含重算切换点,按切换点(nextAtMs)在模块级去重,
2223
+ // 同一切换点只发一次(旧实现比较 tick 时间戳,每 10 秒都"未通知过",会连发)。
2162
2224
  if (!config || config.peakAlertEnabled !== true || config.peakEnabled !== true) return
2163
2225
  if (config.peakAlertWebNotify !== true || !window.Notification || Notification.permission !== 'granted') return
2164
- if (notifiedAtRef.current === now) return
2165
2226
  const wv = peakPhaseAt(now, Array.isArray(config.peakWindows) ? config.peakWindows : [])
2166
2227
  if (wv === null) return
2167
2228
  const tgt = config.peakAlertTarget === 'peak' || config.peakAlertTarget === 'offpeak' ? config.peakAlertTarget : 'both'
@@ -2170,8 +2231,9 @@ window.__ModuleLoader__.load({
2170
2231
  const mins = Number(config.peakAlertAhead)
2171
2232
  const aheadMs = (Number.isFinite(mins) && mins >= 1 ? mins : 2) * 60000
2172
2233
  if (now < wv.nextAtMs - aheadMs || now >= wv.nextAtMs) return
2234
+ if (lastPeakNotifyAtMs === wv.nextAtMs) return
2173
2235
  const t = makeT(resolveLocale(config.locale))
2174
- notifiedAtRef.current = now
2236
+ lastPeakNotifyAtMs = wv.nextAtMs
2175
2237
  try {
2176
2238
  new Notification(
2177
2239
  t(intoPeak ? 'peakAlertTitlePeak' : 'peakAlertTitleOffPeak'),
@@ -2448,7 +2510,7 @@ window.__ModuleLoader__.load({
2448
2510
  }
2449
2511
 
2450
2512
  function MiniMaxPlanCard(props) {
2451
- const { five, seven, fetchedAt, t, wide } = props
2513
+ const { five, seven, fetchedAt, t, wide, refresh } = props
2452
2514
  const fiveView = miniMaxRow(t('codingPlanRemain5h'), five)
2453
2515
  const sevenView = miniMaxRow(t('codingPlanRemain7d'), seven)
2454
2516
  const level = fiveView.level === 'over' || sevenView.level === 'over' ? 'over'
@@ -2463,6 +2525,7 @@ window.__ModuleLoader__.load({
2463
2525
  lineOf(t('codingPlanRemain5h'), five, fiveView),
2464
2526
  lineOf(t('codingPlanRemain7d'), seven, sevenView),
2465
2527
  fetchedAt > 0 ? t('goQuotaFetchedAt', { time: new Date(fetchedAt).toLocaleTimeString() }) : null,
2528
+ ...(refresh ? clickRefreshTipLines(t, refresh) : []),
2466
2529
  ].filter(Boolean).join('; ')
2467
2530
  const body = el(Fragment, null,
2468
2531
  el('div', { className: 'cm-mm-title' }, t('codingPlanMinimaxTitle')),
@@ -2472,18 +2535,23 @@ window.__ModuleLoader__.load({
2472
2535
  el('div', { className: 'cm-bbox-rail cm-num' }, fiveView.remain === null ? '—' : Math.round(fiveView.remain) + '%'),
2473
2536
  el('div', { className: 'cm-bbox-rail cm-num' }, sevenView.remain === null ? '—' : Math.round(sevenView.remain) + '%'))
2474
2537
  return el(Tooltip, { label: detail, side: 'right', delayMs: 300 },
2475
- el('div', { className: 'cm-bbox cm-mm' + (level === 'ok' ? '' : ' ' + level) + (wide === false ? ' rail' : '') },
2538
+ el('div', {
2539
+ className: 'cm-bbox cm-mm' + (level === 'ok' ? '' : ' ' + level) + (wide === false ? ' rail' : '')
2540
+ + (refresh ? ' clickable' + (refresh.busy ? ' busy' : '') : ''),
2541
+ ...(refresh ? clickableRefreshProps(refresh.busy, refresh.run) : {}),
2542
+ },
2476
2543
  wide === false ? rail : body))
2477
2544
  }
2478
2545
 
2479
2546
  function MiniMaxPlanBox(props) {
2480
- const { state, wide } = props
2547
+ const { state, wide, api } = props
2481
2548
  const live = state.codingPlans?.minimax
2482
2549
  const t = makeT(resolveLocale(state.config?.locale))
2550
+ const refresh = useClickRefresh(api ? () => api.refreshCodingPlan('minimax') : null)
2483
2551
  if (!live || live.status !== 'ok') return null
2484
2552
  const { five, seven } = miniMaxWindowsOf(live.windows)
2485
2553
  if (five == null && seven == null) return null
2486
- return el(MiniMaxPlanCard, { five, seven, fetchedAt: live.fetchedAt, t, wide })
2554
+ return el(MiniMaxPlanCard, { five, seven, fetchedAt: live.fetchedAt, t, wide, refresh })
2487
2555
  }
2488
2556
 
2489
2557
  /** 通用 Coding Plan 侧边栏卡片(issue #31):每窗口一行进度条;文本窗口(余额等)整行文本。 */
@@ -2495,9 +2563,10 @@ window.__ModuleLoader__.load({
2495
2563
  }
2496
2564
 
2497
2565
  function CodingPlanBox(props) {
2498
- const { id, state, wide } = props
2566
+ const { id, state, wide, api } = props
2499
2567
  const live = state.codingPlans?.[id]
2500
2568
  const t = makeT(resolveLocale(state.config?.locale))
2569
+ const refresh = useClickRefresh(api ? () => api.refreshCodingPlan(id) : null)
2501
2570
  const rowDef = CODING_PLAN_ROWS.find(r => r.id === id)
2502
2571
  if (!rowDef || !live || live.status !== 'ok') return null
2503
2572
  const windows = live.windows !== null && typeof live.windows === 'object' ? live.windows : {}
@@ -2526,6 +2595,7 @@ window.__ModuleLoader__.load({
2526
2595
  return codingPlanWindowLabel(name, t) + ' ' + value + reset
2527
2596
  })
2528
2597
  if (live.fetchedAt > 0) detailParts.push(t('goQuotaFetchedAt', { time: new Date(live.fetchedAt).toLocaleTimeString() }))
2598
+ detailParts.push(...clickRefreshTipLines(t, refresh))
2529
2599
  const detail = [t(rowDef.labelKey), ...detailParts].join('; ')
2530
2600
  const pcts = entries.map(([, win]) => pctOf(win)).filter(p => p !== null)
2531
2601
  const level = pcts.some(p => p >= 100) ? 'over' : pcts.some(p => p >= 80) ? 'warn' : 'ok'
@@ -2533,7 +2603,7 @@ window.__ModuleLoader__.load({
2533
2603
  ? pcts.slice(0, 2).map(p => p + '%').join(' ')
2534
2604
  : (typeof entries[0][1]?.text === 'string' ? entries[0][1].text : '—')
2535
2605
  return el(Tooltip, { label: detail, side: 'right', delayMs: 300 },
2536
- el('div', { className: 'cm-bbox cm-mm' + (level === 'ok' ? '' : ' ' + level) + (wide === false ? ' rail' : '') },
2606
+ el('div', { className: 'cm-bbox cm-mm clickable' + (level === 'ok' ? '' : ' ' + level) + (wide === false ? ' rail' : '') + (refresh.busy ? ' busy' : ''), ...clickableRefreshProps(refresh.busy, refresh.run) },
2537
2607
  wide === false
2538
2608
  ? el('div', { className: 'cm-bbox-rail cm-num' }, railText)
2539
2609
  : el(Fragment, null,
@@ -2678,6 +2748,44 @@ window.__ModuleLoader__.load({
2678
2748
  el('button', { type: 'button', className: 'cm-btn small primary', onClick: () => choose(true) }, t('quotaStripGuideOn'))))
2679
2749
  }
2680
2750
 
2751
+ // 更新后的提醒(issue #37):告知侧边栏余额/额度图框支持点击立即刷新;
2752
+ // 「知道了」写回 balance.clickHintSeen=true 后永久消失(挂常驻 sidebar.footer.action)。
2753
+ // 仅在侧边栏确有可点击图框(官方余额 / 自定义余额 / Coding Plan)时展示,避免噪音。
2754
+ function BalanceClickGuide(props) {
2755
+ // Hook 全部前置(promptSeen 类标记翻转时会提前 return null,顺序必须稳定)。
2756
+ const costStore = props.useCost ? props.useCost(s => s) : undefined
2757
+ const busyRef = useRef(false)
2758
+ const [dismissed, setDismissed] = useState(false)
2759
+ const state = costStore?.state
2760
+ if (!state) return null
2761
+ const config = state.config
2762
+ const sidebarBalanceOn = config.balance?.display === 'sidebar' || config.balance?.display === 'both'
2763
+ const sidebarCustomOn = config.customBalance?.enabled === true
2764
+ && (config.customBalance?.display === 'sidebar' || config.customBalance?.display === 'both')
2765
+ const sidebarPlansOn = CODING_PLAN_ROWS.some(r => {
2766
+ const entry = config.codingPlans?.[r.id]
2767
+ return entry?.enabled === true && (entry.display === 'sidebar' || entry.display === 'both')
2768
+ })
2769
+ if (dismissed || config.balance?.clickHintSeen === true || (!sidebarBalanceOn && !sidebarCustomOn && !sidebarPlansOn)) return null
2770
+ // 串行展示:横条引导(QuotaStripGuide)与本卡同为 fixed 顶部卡片,同屏会完全重叠,
2771
+ // 点掉一张露出另一张,视觉上像「点了没反应」。等横条引导处理完(promptSeen)再出现。
2772
+ if (config.quotaStrip?.promptSeen !== true) return null
2773
+ const t = makeT(resolveLocale(config?.locale))
2774
+ const dismiss = () => {
2775
+ if (busyRef.current === true) return
2776
+ // 乐观消失:点击立即隐藏,落盘确认失败才恢复可见(避免宿主渲染时序让卡片看起来「点不掉」)。
2777
+ setDismissed(true)
2778
+ busyRef.current = true
2779
+ props.api.updateConfig({ balance: { ...(config.balance ?? {}), clickHintSeen: true } })
2780
+ .catch(() => { busyRef.current = false; setDismissed(false) })
2781
+ }
2782
+ return el('div', { className: 'cm-qguide', role: 'dialog', 'aria-label': t('balanceClickGuideTitle') },
2783
+ el('h4', null, t('balanceClickGuideTitle')),
2784
+ el('p', null, t('balanceClickGuideBody')),
2785
+ el('div', { className: 'cm-buttons' },
2786
+ el('button', { type: 'button', className: 'cm-btn small primary', onClick: dismiss }, t('balanceClickGuideOk'))))
2787
+ }
2788
+
2681
2789
  function BudgetBoxContent(props) {
2682
2790
  const { state, wide } = props
2683
2791
  const today = state.today
@@ -2809,13 +2917,13 @@ window.__ModuleLoader__.load({
2809
2917
  const showToday = config.sidebar !== false
2810
2918
  if (!showBalance && !showCustomBalance && !goOk && !plansOn && !budgetOn && !showToday) return null
2811
2919
  const nodes = []
2812
- if (showBalanceBar) nodes.push(el(BalanceBox, { state, wide }))
2813
- else if (showBalance) nodes.push(el(BalanceRowContent, { state, wide }))
2814
- if (showCustomBalanceBar) nodes.push(el(CustomBalanceBox, { state, wide }))
2815
- else if (showCustomBalance) nodes.push(el(CustomBalanceRowContent, { state, wide }))
2920
+ if (showBalanceBar) nodes.push(el(BalanceBox, { state, wide, api: props.api }))
2921
+ else if (showBalance) nodes.push(el(BalanceRowContent, { state, wide, api: props.api }))
2922
+ if (showCustomBalanceBar) nodes.push(el(CustomBalanceBox, { state, wide, api: props.api }))
2923
+ else if (showCustomBalance) nodes.push(el(CustomBalanceRowContent, { state, wide, api: props.api }))
2816
2924
  if (plansOn) nodes.push(...sidebarPlanIds.map(id => id === 'minimax'
2817
- ? el(MiniMaxPlanBox, { state, wide })
2818
- : el(CodingPlanBox, { id, state, wide })))
2925
+ ? el(MiniMaxPlanBox, { state, wide, api: props.api })
2926
+ : el(CodingPlanBox, { id, state, wide, api: props.api })))
2819
2927
  if (goOk && budgetOn && wide) {
2820
2928
  // 同时出现:合并为一张卡片(Go 在上、预算在下,细分隔线),各自保留预警色与自己的详细信息开关。
2821
2929
  const goView = goBoxBody(state, config, t)
@@ -5037,6 +5145,14 @@ window.__ModuleLoader__.load({
5037
5145
  )
5038
5146
  return dispose
5039
5147
  })
5148
+ // 更新后的提醒(issue #37):余额/额度图框点击刷新,常驻插槽 + clickHintSeen 门控。
5149
+ slots.inject('sidebar.footer.action', () => {
5150
+ const dispose = slots.register(
5151
+ { name: 'sidebar.footer.action', id: 'cost-meter-balance-click-guide', order: 2, inject: injected },
5152
+ BalanceClickGuide,
5153
+ )
5154
+ return dispose
5155
+ })
5040
5156
 
5041
5157
  // 峰/谷切换前弹窗提醒:全局 fixed 浮层(fixed 定位与宿主位置无关),挂在常驻的
5042
5158
  // sidebar.footer.action 插槽——conversation.composer.dock 仅在有活跃会话的页面渲染,
package/lib/pricing.js CHANGED
@@ -199,7 +199,7 @@ export const DEFAULT_PROVIDER_PRICE_TABLE = {
199
199
 
200
200
  /** 拓展价格表目录的模型家族分组(展示用;未列出的模型自成一家)。 */
201
201
  export const PROVIDER_MODEL_FAMILIES = {
202
- deepseek: { 'deepseek-v4-flash': 'DeepSeek v4', 'deepseek-v4-pro': 'DeepSeek v4' },
202
+ deepseek: { 'deepseek-v4-flash': 'DeepSeek v4', 'deepseek-v4-pro': 'DeepSeek v4', 'deepseek-v4-flash-vision-exp': 'DeepSeek v4' },
203
203
  openai: {
204
204
  'gpt-5.6-sol': 'GPT-5.6', 'gpt-5.6-terra': 'GPT-5.6', 'gpt-5.6-luna': 'GPT-5.6',
205
205
  'gpt-5.5': 'GPT-5.5', 'gpt-5.5-pro': 'GPT-5.5',
@@ -283,6 +283,14 @@ export const DEFAULT_PRICE_TABLE = {
283
283
  peak: { cacheHit: 0.044, cacheMiss: 1.32, output: 3.96 },
284
284
  legacyBase: { cacheHit: 0.003625, cacheMiss: 0.435, output: 0.87 },
285
285
  },
286
+ // Vision-Exp(实验版多模态)与 flash 同价;峰谷时代后发布,无历史基础价档。
287
+ 'deepseek-v4-flash-vision-exp': {
288
+ cacheHit: 0.007,
289
+ cacheMiss: 0.22,
290
+ output: 0.66,
291
+ offPeak: { cacheHit: 0.007, cacheMiss: 0.22, output: 0.66 },
292
+ peak: { cacheHit: 0.014, cacheMiss: 0.44, output: 1.32 },
293
+ },
286
294
  },
287
295
  default: { cacheHit: 0.007, cacheMiss: 0.22, output: 0.66 },
288
296
  }
package/lib/store.js CHANGED
@@ -89,6 +89,7 @@ export function defaultConfig() {
89
89
  showProgressBar: false, // 全局:侧边栏余额以三段进度条展示(蓝=余额,橙=当日,灰=已用)
90
90
  budgetCap: null, // 可选:手动额度上限;留空则优先用 API 返回的 max_budget;仍无则整条蓝色
91
91
  reconcile: true, // 余额差交叉校验:官方余额当日变动与本地账本今日合计偏差超阈值时提示
92
+ clickHintSeen: false, // 更新后提醒:余额图框点击刷新的引导是否已处理(issue #37)
92
93
  },
93
94
  goQuota: {
94
95
  enabled: true, // 启用 OpenCode Go 订阅额度读取与显示(像预算开关一样的总开关)
@@ -282,6 +283,7 @@ const VALIDATION_MESSAGES = {
282
283
  balanceDisplay: 'balance.display 必须是 sidebar / settings / both / off',
283
284
  balanceRefresh: 'balance.refreshMinutes 必须是 1-1440 的整数',
284
285
  balanceShowBar: 'balance.showProgressBar 必须是布尔值',
286
+ balanceClickHint: 'balance.clickHintSeen 必须是布尔值',
285
287
  balanceReconcile: 'balance.reconcile 必须是布尔值',
286
288
  balanceBudgetCap: 'balance.budgetCap 必须是非负数或 null',
287
289
  goQuota: 'goQuota 非法',
@@ -350,6 +352,7 @@ const VALIDATION_MESSAGES = {
350
352
  balanceDisplay: 'balance.display must be sidebar / settings / both / off',
351
353
  balanceRefresh: 'balance.refreshMinutes must be an integer from 1 to 1440',
352
354
  balanceShowBar: 'balance.showProgressBar must be a boolean',
355
+ balanceClickHint: 'balance.clickHintSeen must be a boolean',
353
356
  balanceReconcile: 'balance.reconcile must be a boolean',
354
357
  balanceBudgetCap: 'balance.budgetCap must be a non-negative number or null',
355
358
  goQuota: 'Invalid goQuota',
@@ -517,6 +520,7 @@ export function applyConfigPatch(current, patch) {
517
520
  if (!Number.isInteger(refreshMinutes) || refreshMinutes < 1 || refreshMinutes > 1440) errors.push(vmsg(locale, 'balanceRefresh'))
518
521
  else balance.refreshMinutes = refreshMinutes
519
522
  if (balance.showProgressBar !== undefined && typeof balance.showProgressBar !== 'boolean') errors.push(vmsg(locale, 'balanceShowBar'))
523
+ if (balance.clickHintSeen !== undefined && typeof balance.clickHintSeen !== 'boolean') errors.push(vmsg(locale, 'balanceClickHint'))
520
524
  if (balance.reconcile !== undefined && typeof balance.reconcile !== 'boolean') errors.push(vmsg(locale, 'balanceReconcile'))
521
525
  if (balance.budgetCap !== undefined && balance.budgetCap !== null) {
522
526
  const cap = Number(balance.budgetCap)
@@ -700,6 +704,7 @@ export function sanitizeConfig(raw) {
700
704
  : base.balance.showProgressBar === true
701
705
  }
702
706
  balance.showProgressBar = balance.showProgressBar === true
707
+ balance.clickHintSeen = balance.clickHintSeen === true
703
708
  balance.reconcile = balance.reconcile !== false
704
709
  const cap = Number(balance.budgetCap)
705
710
  balance.budgetCap = Number.isFinite(cap) && cap > 0 ? cap : null
@@ -144,6 +144,8 @@ const configSchema = z.object({
144
144
  showProgressBar: z.boolean().optional(),
145
145
  budgetCap: z.union([num, z.null()]).optional(),
146
146
  reconcile: z.boolean().optional(),
147
+ // 更新后提醒:余额图框点击刷新引导是否已处理(issue #37,内部标记)。
148
+ clickHintSeen: z.boolean().optional(),
147
149
  }),
148
150
  goQuota: z.object({
149
151
  enabled: z.boolean(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-cost-meter",
3
- "version": "1.5.31",
3
+ "version": "1.5.32",
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, 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",