dsh-cost-meter 1.5.30 → 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.30-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,18 +19,19 @@ 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
 
26
26
  | Feature | Location | Description |
27
27
  |---|---|---|
28
28
  | Per-conversation cost | Below the composer / session title bar | Live accumulated cost + input/cache/output tokens; position configurable |
29
- | Official balance | Sidebar top / Settings page (configurable) | Total / granted / topped-up balance, auto-refresh + manual refresh; optional three-segment progress bar (blue/orange/gray) |
29
+ | Official balance | Sidebar top / Settings page (configurable) | Total / granted / topped-up balance, auto-refresh + manual refresh; optional three-segment progress bar (blue/orange/gray), whose today segment only counts official-channel spend (coding plans / custom providers excluded) |
30
30
  | Custom provider balance | Sidebar / Settings page (configurable) | Configurable HTTP balance lookup (e.g. LiteLLM); bilingual labels, currency, extract rules (dot path / number / add / subtract / divide — use divide for NewApi-style quota endpoints, see [example](#custom-provider-balance-example-newapi-template)); collapsible panel alongside Coding Plan quotas |
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.30`** — 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.30/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.30
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.30.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.30-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)
@@ -26,11 +26,12 @@
26
26
  | 功能 | 位置 | 说明 |
27
27
  |---|---|---|
28
28
  | 本会话费用 | 输入区下方 / 会话标题栏 | 实时累计费用 + 输入/缓存/输出 token,位置可配 |
29
- | 官方余额 | 侧边栏顶部 / 设置页(可配) | 总余额 / 赠送 / 充值,自动刷新 + 手动刷新;可选三段进度条(蓝/橙/灰) |
29
+ | 官方余额 | 侧边栏顶部 / 设置页(可配) | 总余额 / 赠送 / 充值,自动刷新 + 手动刷新;可选三段进度条(蓝/橙/灰),当日段只统计官方渠道费用(不含 Coding Plan / 自定义 Provider) |
30
30
  | 自定义 Provider 余额 | 侧边栏 / 设置页(可配) | 可配置 HTTP 查询任意 Provider 余额(LiteLLM 等);中/英名称、币种、extract 规则(点路径 / 数字常量 / add / subtract / divide,divide 适配 NewApi 等 quota 端点,见下方[示例](#自定义-provider-余额配置示例newapi-模板));与 Coding Plan 同区可折叠配置 |
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.30`**,建议先下载审阅再运行):
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.30/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.30
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.30.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,
@@ -1757,8 +1772,26 @@ window.__ModuleLoader__.load({
1757
1772
  return null
1758
1773
  }
1759
1774
 
1775
+ // issue #36:官方余额进度条的「当日已用」只统计会扣 DeepSeek 开放平台余额的调用
1776
+ // (byProviderModel 中 provider 前缀为 deepseek 的条目,含未标注 provider 的历史调用);
1777
+ // Coding Plan / 自定义 Provider 等渠道的费用只体现在各自的额度条/余额条上。
1778
+ // 账本无按渠道拆分的旧数据(byProviderModel 缺失/为空)退回全量,保持升级前行为。
1779
+ function todayOfficialUsd(state) {
1780
+ const today = state.today
1781
+ const by = today?.byProviderModel
1782
+ if (by === null || typeof by !== 'object' || Object.keys(by).length === 0) return Number(today?.cost) || 0
1783
+ let sum = 0
1784
+ for (const [key, value] of Object.entries(by)) {
1785
+ const idx = key.indexOf(':')
1786
+ if ((idx >= 0 ? key.slice(0, idx) : key) !== 'deepseek') continue
1787
+ sum += Number(value?.cost) || 0
1788
+ }
1789
+ return sum
1790
+ }
1791
+
1760
1792
  function todayUsedInBalanceCurrency(state, config, mode, custom) {
1761
- const usd = Number(state.today?.cost) || 0
1793
+ // 官方模式只取 deepseek 渠道费用;自定义 Provider 余额无渠道映射,维持全量(与既有行为一致)
1794
+ const usd = mode === 'official' ? todayOfficialUsd(state) : Number(state.today?.cost) || 0
1762
1795
  if (mode === 'custom') {
1763
1796
  const unit = customBalanceUnitOf(config, custom)
1764
1797
  if (unit === 'USD') return usd
@@ -1820,14 +1853,51 @@ window.__ModuleLoader__.load({
1820
1853
  return lines.join(' · ')
1821
1854
  }
1822
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
+
1823
1892
  function BalanceRowContent(props) {
1824
- const { state, wide } = props
1893
+ const { state, wide, api } = props
1825
1894
  const balance = state.balance
1826
1895
  const t = makeT(resolveLocale(state.config?.locale))
1896
+ const refresh = useClickRefresh(api ? () => api.refreshBalance() : null)
1827
1897
  if (!balance || balance.status === 'off') return null
1828
1898
  if (balance.status === 'error') {
1829
- return el(Tooltip, { label: t('balanceQueryFailed', { message: balance.message || t('unknownError') }), side: 'right', delayMs: 300 },
1830
- 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) },
1831
1901
  wide ? el(Fragment, null, t('balance'), ' ', el('span', { className: 'cm-num' }, t('queryFailed'))) : '⚠'))
1832
1902
  }
1833
1903
  const detail = [
@@ -1839,17 +1909,19 @@ window.__ModuleLoader__.load({
1839
1909
  }),
1840
1910
  t('updatedAt', { time: new Date(balance.fetchedAt).toLocaleTimeString() }),
1841
1911
  ...(state.reconcile?.ok === false ? ['⚠ ' + state.reconcile.message] : []),
1912
+ ...clickRefreshTipLines(t, refresh),
1842
1913
  ].join('; ')
1843
1914
  return el(Tooltip, { label: detail, side: 'right', delayMs: 300 },
1844
- 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) },
1845
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 })))
1846
1917
  }
1847
1918
 
1848
1919
  function BalanceBox(props) {
1849
- const { state, wide } = props
1920
+ const { state, wide, api } = props
1850
1921
  const config = state.config
1851
1922
  const balance = state.balance
1852
1923
  const t = makeT(resolveLocale(config?.locale))
1924
+ const refresh = useClickRefresh(api ? () => api.refreshBalance() : null)
1853
1925
  if (!balance || balance.status !== 'ok') return null
1854
1926
  const segments = segmentsForOfficialBalance(state, config)
1855
1927
  const remaining = formatBalanceMoney(balance.totalBalance, config)
@@ -1863,6 +1935,7 @@ window.__ModuleLoader__.load({
1863
1935
  }),
1864
1936
  t('updatedAt', { time: new Date(balance.fetchedAt).toLocaleTimeString() }),
1865
1937
  ...(state.reconcile?.ok === false ? ['⚠ ' + state.reconcile.message] : []),
1938
+ ...clickRefreshTipLines(t, refresh),
1866
1939
  ].join('; ')
1867
1940
  const body = el(Fragment, null,
1868
1941
  el('div', { className: 'cm-bbox-head' },
@@ -1870,7 +1943,7 @@ window.__ModuleLoader__.load({
1870
1943
  el('span', { className: 'cm-bbox-pct cm-num cm-bal-amt' }, remaining)),
1871
1944
  el(BalanceBar, { segments }))
1872
1945
  return el(Tooltip, { label: detail, side: 'right', delayMs: 300 },
1873
- el('div', { className: 'cm-bbox' + (wide ? '' : ' rail') },
1946
+ el('div', { className: 'cm-bbox clickable' + (wide ? '' : ' rail') + (refresh.busy ? ' busy' : ''), ...clickableRefreshProps(refresh.busy, refresh.run) },
1874
1947
  wide ? body : el('div', { className: 'cm-bbox-rail cm-num' }, Math.round(segments.remainingPct) + '%')))
1875
1948
  }
1876
1949
 
@@ -1943,34 +2016,36 @@ window.__ModuleLoader__.load({
1943
2016
  }
1944
2017
 
1945
2018
  function CustomBalanceBox(props) {
1946
- const { state, wide } = props
2019
+ const { state, wide, api } = props
1947
2020
  const custom = state.customBalance
1948
2021
  const config = state.config
1949
2022
  const t = makeT(resolveLocale(config?.locale))
2023
+ const refresh = useClickRefresh(api ? () => api.refreshCustomBalance() : null)
1950
2024
  if (!custom || custom.status !== 'ok') return null
1951
2025
  const view = customBalanceBoxBody(state, config, t)
1952
- const detail = customBalanceDetailText(custom, config, t, state)
2026
+ const detail = [customBalanceDetailText(custom, config, t, state), ...clickRefreshTipLines(t, refresh)].join(' · ')
1953
2027
  return el(Tooltip, { label: detail, side: 'right', delayMs: 300 },
1954
- 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) },
1955
2029
  wide ? view.body : el('div', { className: 'cm-bbox-rail cm-num' }, view.rail)))
1956
2030
  }
1957
2031
 
1958
2032
  function CustomBalanceRowContent(props) {
1959
- const { state, wide } = props
2033
+ const { state, wide, api } = props
1960
2034
  const custom = state.customBalance
1961
2035
  const config = state.config
1962
2036
  const t = makeT(resolveLocale(config?.locale))
2037
+ const refresh = useClickRefresh(api ? () => api.refreshCustomBalance() : null)
1963
2038
  if (!custom || custom.status === 'off') return null
1964
2039
  const label = resolveCustomBalanceLabel(config.customBalance ?? {}, resolveLocale(config?.locale))
1965
2040
  if (custom.status === 'error') {
1966
- return el(Tooltip, { label: custom.message || t('unknownError'), side: 'right', delayMs: 300 },
1967
- 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) },
1968
2043
  wide ? el(Fragment, null, label, ' ', el('span', { className: 'cm-num' }, t('queryFailed'))) : '⚠'))
1969
2044
  }
1970
2045
  const amount = formatCustomBalanceMoney(custom.remaining, config, custom)
1971
- const detail = customBalanceDetailText(custom, config, t, state)
2046
+ const detail = [customBalanceDetailText(custom, config, t, state), ...clickRefreshTipLines(t, refresh)].join(' · ')
1972
2047
  return el(Tooltip, { label: detail, side: 'right', delayMs: 300 },
1973
- 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) },
1974
2049
  wide ? el(Fragment, null, label, ' ', el('span', { className: 'cm-num' }, amount)) : el(WalletIcon, { size: 16 })))
1975
2050
  }
1976
2051
 
@@ -2097,6 +2172,11 @@ window.__ModuleLoader__.load({
2097
2172
  // 预览通道事件名:设置页按钮/控制台经 window.cmPeakAlertPreview(kind) 触发真实组件。
2098
2173
  const PEAK_ALERT_PREVIEW_EVENT = 'cm-peak-alert-preview'
2099
2174
 
2175
+ // 峰谷 Web 通知去重:记录最近一次已通知的切换点时刻(nextAtMs)。
2176
+ // 必须放模块级——配置变化会重挂 PeakAlert(内部 ref 归零),若用组件内状态,
2177
+ // 重挂后同一切换点会再发一条;切换点时刻单调递增,存单值即可。
2178
+ let lastPeakNotifyAtMs = 0
2179
+
2100
2180
  /**
2101
2181
  * 峰/谷切换前弹窗提醒:距下次相位切换不足配置提前量(默认 2 分钟)时弹浮层,
2102
2182
  * 位置可选屏幕右下角 / 屏幕中心;提醒类型按配置过滤(进入峰/进入谷/峰和谷);
@@ -2111,7 +2191,6 @@ window.__ModuleLoader__.load({
2111
2191
  const [now, setNow] = useState(Date.now())
2112
2192
  const [dismissedAt, setDismissedAt] = useState(null)
2113
2193
  const [preview, setPreview] = useState(null)
2114
- const notifiedAtRef = useRef(null)
2115
2194
  useEffect(() => {
2116
2195
  const timer = window.setInterval(() => setNow(Date.now()), 10000)
2117
2196
  return () => window.clearInterval(timer)
@@ -2140,10 +2219,10 @@ window.__ModuleLoader__.load({
2140
2219
  }
2141
2220
  }, []) // eslint-disable-line react-hooks/exhaustive-deps
2142
2221
  useEffect(() => {
2143
- // Web 通知:每次 tick 自包含重算切换点,同一切换点只发一次。
2222
+ // Web 通知:每次 tick 自包含重算切换点,按切换点(nextAtMs)在模块级去重,
2223
+ // 同一切换点只发一次(旧实现比较 tick 时间戳,每 10 秒都"未通知过",会连发)。
2144
2224
  if (!config || config.peakAlertEnabled !== true || config.peakEnabled !== true) return
2145
2225
  if (config.peakAlertWebNotify !== true || !window.Notification || Notification.permission !== 'granted') return
2146
- if (notifiedAtRef.current === now) return
2147
2226
  const wv = peakPhaseAt(now, Array.isArray(config.peakWindows) ? config.peakWindows : [])
2148
2227
  if (wv === null) return
2149
2228
  const tgt = config.peakAlertTarget === 'peak' || config.peakAlertTarget === 'offpeak' ? config.peakAlertTarget : 'both'
@@ -2152,8 +2231,9 @@ window.__ModuleLoader__.load({
2152
2231
  const mins = Number(config.peakAlertAhead)
2153
2232
  const aheadMs = (Number.isFinite(mins) && mins >= 1 ? mins : 2) * 60000
2154
2233
  if (now < wv.nextAtMs - aheadMs || now >= wv.nextAtMs) return
2234
+ if (lastPeakNotifyAtMs === wv.nextAtMs) return
2155
2235
  const t = makeT(resolveLocale(config.locale))
2156
- notifiedAtRef.current = now
2236
+ lastPeakNotifyAtMs = wv.nextAtMs
2157
2237
  try {
2158
2238
  new Notification(
2159
2239
  t(intoPeak ? 'peakAlertTitlePeak' : 'peakAlertTitleOffPeak'),
@@ -2430,7 +2510,7 @@ window.__ModuleLoader__.load({
2430
2510
  }
2431
2511
 
2432
2512
  function MiniMaxPlanCard(props) {
2433
- const { five, seven, fetchedAt, t, wide } = props
2513
+ const { five, seven, fetchedAt, t, wide, refresh } = props
2434
2514
  const fiveView = miniMaxRow(t('codingPlanRemain5h'), five)
2435
2515
  const sevenView = miniMaxRow(t('codingPlanRemain7d'), seven)
2436
2516
  const level = fiveView.level === 'over' || sevenView.level === 'over' ? 'over'
@@ -2445,6 +2525,7 @@ window.__ModuleLoader__.load({
2445
2525
  lineOf(t('codingPlanRemain5h'), five, fiveView),
2446
2526
  lineOf(t('codingPlanRemain7d'), seven, sevenView),
2447
2527
  fetchedAt > 0 ? t('goQuotaFetchedAt', { time: new Date(fetchedAt).toLocaleTimeString() }) : null,
2528
+ ...(refresh ? clickRefreshTipLines(t, refresh) : []),
2448
2529
  ].filter(Boolean).join('; ')
2449
2530
  const body = el(Fragment, null,
2450
2531
  el('div', { className: 'cm-mm-title' }, t('codingPlanMinimaxTitle')),
@@ -2454,18 +2535,23 @@ window.__ModuleLoader__.load({
2454
2535
  el('div', { className: 'cm-bbox-rail cm-num' }, fiveView.remain === null ? '—' : Math.round(fiveView.remain) + '%'),
2455
2536
  el('div', { className: 'cm-bbox-rail cm-num' }, sevenView.remain === null ? '—' : Math.round(sevenView.remain) + '%'))
2456
2537
  return el(Tooltip, { label: detail, side: 'right', delayMs: 300 },
2457
- 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
+ },
2458
2543
  wide === false ? rail : body))
2459
2544
  }
2460
2545
 
2461
2546
  function MiniMaxPlanBox(props) {
2462
- const { state, wide } = props
2547
+ const { state, wide, api } = props
2463
2548
  const live = state.codingPlans?.minimax
2464
2549
  const t = makeT(resolveLocale(state.config?.locale))
2550
+ const refresh = useClickRefresh(api ? () => api.refreshCodingPlan('minimax') : null)
2465
2551
  if (!live || live.status !== 'ok') return null
2466
2552
  const { five, seven } = miniMaxWindowsOf(live.windows)
2467
2553
  if (five == null && seven == null) return null
2468
- return el(MiniMaxPlanCard, { five, seven, fetchedAt: live.fetchedAt, t, wide })
2554
+ return el(MiniMaxPlanCard, { five, seven, fetchedAt: live.fetchedAt, t, wide, refresh })
2469
2555
  }
2470
2556
 
2471
2557
  /** 通用 Coding Plan 侧边栏卡片(issue #31):每窗口一行进度条;文本窗口(余额等)整行文本。 */
@@ -2477,9 +2563,10 @@ window.__ModuleLoader__.load({
2477
2563
  }
2478
2564
 
2479
2565
  function CodingPlanBox(props) {
2480
- const { id, state, wide } = props
2566
+ const { id, state, wide, api } = props
2481
2567
  const live = state.codingPlans?.[id]
2482
2568
  const t = makeT(resolveLocale(state.config?.locale))
2569
+ const refresh = useClickRefresh(api ? () => api.refreshCodingPlan(id) : null)
2483
2570
  const rowDef = CODING_PLAN_ROWS.find(r => r.id === id)
2484
2571
  if (!rowDef || !live || live.status !== 'ok') return null
2485
2572
  const windows = live.windows !== null && typeof live.windows === 'object' ? live.windows : {}
@@ -2508,6 +2595,7 @@ window.__ModuleLoader__.load({
2508
2595
  return codingPlanWindowLabel(name, t) + ' ' + value + reset
2509
2596
  })
2510
2597
  if (live.fetchedAt > 0) detailParts.push(t('goQuotaFetchedAt', { time: new Date(live.fetchedAt).toLocaleTimeString() }))
2598
+ detailParts.push(...clickRefreshTipLines(t, refresh))
2511
2599
  const detail = [t(rowDef.labelKey), ...detailParts].join('; ')
2512
2600
  const pcts = entries.map(([, win]) => pctOf(win)).filter(p => p !== null)
2513
2601
  const level = pcts.some(p => p >= 100) ? 'over' : pcts.some(p => p >= 80) ? 'warn' : 'ok'
@@ -2515,7 +2603,7 @@ window.__ModuleLoader__.load({
2515
2603
  ? pcts.slice(0, 2).map(p => p + '%').join(' ')
2516
2604
  : (typeof entries[0][1]?.text === 'string' ? entries[0][1].text : '—')
2517
2605
  return el(Tooltip, { label: detail, side: 'right', delayMs: 300 },
2518
- 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) },
2519
2607
  wide === false
2520
2608
  ? el('div', { className: 'cm-bbox-rail cm-num' }, railText)
2521
2609
  : el(Fragment, null,
@@ -2660,6 +2748,44 @@ window.__ModuleLoader__.load({
2660
2748
  el('button', { type: 'button', className: 'cm-btn small primary', onClick: () => choose(true) }, t('quotaStripGuideOn'))))
2661
2749
  }
2662
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
+
2663
2789
  function BudgetBoxContent(props) {
2664
2790
  const { state, wide } = props
2665
2791
  const today = state.today
@@ -2791,13 +2917,13 @@ window.__ModuleLoader__.load({
2791
2917
  const showToday = config.sidebar !== false
2792
2918
  if (!showBalance && !showCustomBalance && !goOk && !plansOn && !budgetOn && !showToday) return null
2793
2919
  const nodes = []
2794
- if (showBalanceBar) nodes.push(el(BalanceBox, { state, wide }))
2795
- else if (showBalance) nodes.push(el(BalanceRowContent, { state, wide }))
2796
- if (showCustomBalanceBar) nodes.push(el(CustomBalanceBox, { state, wide }))
2797
- 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 }))
2798
2924
  if (plansOn) nodes.push(...sidebarPlanIds.map(id => id === 'minimax'
2799
- ? el(MiniMaxPlanBox, { state, wide })
2800
- : el(CodingPlanBox, { id, state, wide })))
2925
+ ? el(MiniMaxPlanBox, { state, wide, api: props.api })
2926
+ : el(CodingPlanBox, { id, state, wide, api: props.api })))
2801
2927
  if (goOk && budgetOn && wide) {
2802
2928
  // 同时出现:合并为一张卡片(Go 在上、预算在下,细分隔线),各自保留预警色与自己的详细信息开关。
2803
2929
  const goView = goBoxBody(state, config, t)
@@ -5019,6 +5145,14 @@ window.__ModuleLoader__.load({
5019
5145
  )
5020
5146
  return dispose
5021
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
+ })
5022
5156
 
5023
5157
  // 峰/谷切换前弹窗提醒:全局 fixed 浮层(fixed 定位与宿主位置无关),挂在常驻的
5024
5158
  // sidebar.footer.action 插槽——conversation.composer.dock 仅在有活跃会话的页面渲染,
package/lib/index.js CHANGED
@@ -42,7 +42,7 @@ const SERVER_MESSAGES = {
42
42
  balanceDisplayOff: '余额显示已关闭,请先在 显示设置 中开启',
43
43
  balanceRefreshed: '余额已刷新',
44
44
  balanceQueryFailed: '余额查询失败:{message}',
45
- reconcileWarn: '对账提示:本地账本今日合计 {cost} 与官方余额当日变动 {delta} 偏差较大,请核对价格表或近期账单',
45
+ reconcileWarn: '对账提示:本地账本今日官方渠道费用 {cost} 与官方余额当日变动 {delta} 偏差较大,请核对价格表或近期账单',
46
46
  goQuotaKeyMissing: '未找到 OpenCode Go API Key。有 Go 订阅的话:运行 opencode login、导出 OPENCODE_GO_API_KEY 环境变量,或在显示设置中填写 Key;没有订阅可关闭上方「启用」开关。',
47
47
  goQuotaHttp: 'OpenCode Go 额度接口 HTTP {code}',
48
48
  goQuotaNoSub: '没有检测到生效的 OpenCode Go 订阅(接口返回 {code}),或 API Key 无效。没有订阅可关闭上方「启用」开关。',
@@ -82,7 +82,7 @@ const SERVER_MESSAGES = {
82
82
  balanceDisplayOff: 'Balance display is off; enable it in Display settings first',
83
83
  balanceRefreshed: 'Balance refreshed',
84
84
  balanceQueryFailed: 'Balance query failed: {message}',
85
- reconcileWarn: 'Reconciliation notice: today\'s local ledger cost ({cost}) deviates significantly from the official balance change ({delta}); please check the price table or recent bills',
85
+ reconcileWarn: 'Reconciliation notice: today\'s local official-channel cost ({cost}) deviates significantly from the official balance change ({delta}); please check the price table or recent bills',
86
86
  goQuotaKeyMissing: 'OpenCode Go API key not found. If you have a Go subscription: run opencode login, export OPENCODE_GO_API_KEY, or set the key in Display settings; otherwise turn off the Enable switch above.',
87
87
  goQuotaHttp: 'OpenCode Go quota API returned HTTP {code}',
88
88
  goQuotaNoSub: 'No active OpenCode Go subscription detected (API returned {code}), or the API key is invalid. Turn off the Enable switch above if you have no subscription.',
@@ -605,11 +605,12 @@ function createService(ctx, ledger) {
605
605
  }
606
606
  const task = queryBalance(ctx, localeOf(ledger.config)).then(result => {
607
607
  balanceCache = { fetchedAt: Date.now(), value: { status: 'ok', message: '', fetchedAt: Date.now(), ...result } }
608
- // 余额差交叉校验(issue #18):官方余额当日变动 vs 本地账本今日合计,偏差超阈提示。
608
+ // 余额差交叉校验(issue #18):官方余额当日变动 vs 本地账本今日官方渠道费用,偏差超阈提示。
609
+ // issue #36:Coding Plan / 自定义 Provider 的费用不动官方余额,对账只统计 deepseek 渠道,否则订阅用户会恒报 drift。
609
610
  if ((ledger.config?.balance?.reconcile ?? true) === true && balanceCache.value.status === 'ok') {
610
611
  const nowMs = Date.now()
611
612
  const usd = v => '$' + Number(v).toFixed(4)
612
- const { ref, event } = reconcileBalanceDelta(ledger.balanceRef, balanceCache.value, ledger.today().cost, localDayKey(nowMs), nowMs)
613
+ const { ref, event } = reconcileBalanceDelta(ledger.balanceRef, balanceCache.value, ledger.todayOfficialCost(), localDayKey(nowMs), nowMs)
613
614
  if (ref !== ledger.balanceRef) {
614
615
  ledger.balanceRef = ref
615
616
  ledger.scheduleWrite()
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 订阅额度读取与显示(像预算开关一样的总开关)
@@ -153,6 +154,26 @@ export function zeroDay(date) {
153
154
  return { date, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, calls: 0, cost: 0, byProviderModel: {}, sessions: [] }
154
155
  }
155
156
 
157
+ /**
158
+ * 聚合某日账本中归属 DeepSeek 官方渠道的费用(USD;issue #36)。
159
+ * 只累加 byProviderModel 里 provider 前缀为 deepseek 的条目(账本记账时未标注
160
+ * provider 的调用同样归入 deepseek);Coding Plan / 自定义 Provider 等渠道不计入。
161
+ * 无按渠道拆分的旧数据(byProviderModel 缺失/为空)退回全量 cost,保持升级前行为。
162
+ * @param day - 单日账本记录(可能为 undefined)。
163
+ */
164
+ export function officialCostOfDay(day) {
165
+ if (day === undefined || day === null || typeof day !== 'object') return 0
166
+ const by = day.byProviderModel
167
+ if (by === null || typeof by !== 'object' || Object.keys(by).length === 0) return Number(day.cost) || 0
168
+ let sum = 0
169
+ for (const [key, value] of Object.entries(by)) {
170
+ const idx = key.indexOf(':')
171
+ if ((idx >= 0 ? key.slice(0, idx) : key) !== 'deepseek') continue
172
+ sum += Number(value?.cost) || 0
173
+ }
174
+ return sum
175
+ }
176
+
156
177
  function zeroSession(id) {
157
178
  return { id, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, calls: 0, cost: 0, byProviderModel: {} }
158
179
  }
@@ -262,6 +283,7 @@ const VALIDATION_MESSAGES = {
262
283
  balanceDisplay: 'balance.display 必须是 sidebar / settings / both / off',
263
284
  balanceRefresh: 'balance.refreshMinutes 必须是 1-1440 的整数',
264
285
  balanceShowBar: 'balance.showProgressBar 必须是布尔值',
286
+ balanceClickHint: 'balance.clickHintSeen 必须是布尔值',
265
287
  balanceReconcile: 'balance.reconcile 必须是布尔值',
266
288
  balanceBudgetCap: 'balance.budgetCap 必须是非负数或 null',
267
289
  goQuota: 'goQuota 非法',
@@ -330,6 +352,7 @@ const VALIDATION_MESSAGES = {
330
352
  balanceDisplay: 'balance.display must be sidebar / settings / both / off',
331
353
  balanceRefresh: 'balance.refreshMinutes must be an integer from 1 to 1440',
332
354
  balanceShowBar: 'balance.showProgressBar must be a boolean',
355
+ balanceClickHint: 'balance.clickHintSeen must be a boolean',
333
356
  balanceReconcile: 'balance.reconcile must be a boolean',
334
357
  balanceBudgetCap: 'balance.budgetCap must be a non-negative number or null',
335
358
  goQuota: 'Invalid goQuota',
@@ -497,6 +520,7 @@ export function applyConfigPatch(current, patch) {
497
520
  if (!Number.isInteger(refreshMinutes) || refreshMinutes < 1 || refreshMinutes > 1440) errors.push(vmsg(locale, 'balanceRefresh'))
498
521
  else balance.refreshMinutes = refreshMinutes
499
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'))
500
524
  if (balance.reconcile !== undefined && typeof balance.reconcile !== 'boolean') errors.push(vmsg(locale, 'balanceReconcile'))
501
525
  if (balance.budgetCap !== undefined && balance.budgetCap !== null) {
502
526
  const cap = Number(balance.budgetCap)
@@ -680,6 +704,7 @@ export function sanitizeConfig(raw) {
680
704
  : base.balance.showProgressBar === true
681
705
  }
682
706
  balance.showProgressBar = balance.showProgressBar === true
707
+ balance.clickHintSeen = balance.clickHintSeen === true
683
708
  balance.reconcile = balance.reconcile !== false
684
709
  const cap = Number(balance.budgetCap)
685
710
  balance.budgetCap = Number.isFinite(cap) && cap > 0 ? cap : null
@@ -1039,6 +1064,15 @@ export class Ledger {
1039
1064
  return day === undefined ? zeroDay(date) : this.copyDay(day)
1040
1065
  }
1041
1066
 
1067
+ /**
1068
+ * 今日账本中归属 DeepSeek 官方渠道的费用(USD,issue #36)。
1069
+ * 官方余额进度条「当日已用」与余额差对账只关心会扣 DeepSeek 开放平台余额的调用;
1070
+ * Coding Plan、自定义 Provider 等渠道的费用不计入(各自额度条/余额条自行体现)。
1071
+ */
1072
+ todayOfficialCost() {
1073
+ return officialCostOfDay(this.days[localDayKey(Date.now())])
1074
+ }
1075
+
1042
1076
  /** 历史列表(降序,轻量副本,不含会话明细)。 */
1043
1077
  history(limit = 60) {
1044
1078
  return Object.keys(this.days)
@@ -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.30",
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",