dsh-cost-meter 1.5.14 → 1.5.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +7 -6
- package/README.md +7 -6
- package/lib/client.js +128 -2
- package/lib/index.js +32 -13
- package/lib/store.js +22 -0
- package/lib/typert.host.js +6 -0
- package/package.json +1 -1
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
|
-
[](https://github.com/Han-1413141/dsh-cost-meter)
|
|
10
10
|
[](https://www.npmjs.com/package/dsh-cost-meter)
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
@@ -36,10 +36,11 @@ English | [中文](README.md)
|
|
|
36
36
|
| Token usage stats | Settings page (Cost section) | All-time token totals (input/cache/output/calls) + a Codex-style 26-week daily usage heat grid that fills the settings width; hover a cell for that day's detail |
|
|
37
37
|
| Today's sessions | Settings page | Per-session call count, input/cache/output tokens and cost |
|
|
38
38
|
| History | Settings page | Per-day totals; retention days configurable (default 180) |
|
|
39
|
-
| Pre-install history import |
|
|
39
|
+
| Pre-install history import | Automatic on first launch | After install/upgrade, the first launch automatically replays all host session logs to import conversations from before the plugin was installed (missing dates are rebuilt whole; existing dates only gain previously unknown sessions; idempotent and never double-counts live metering; costs priced at per-event historical rates); a manual re-run entry remains in Settings |
|
|
40
40
|
| Budget settings | Settings page, top | Limit, period (today / month / cumulative / custom date range), used % |
|
|
41
41
|
| Price table | Settings page | Per-model off-peak / peak prices (input/output shorthand supported; cache prices derived automatically); fully editable |
|
|
42
42
|
| Peak/off-peak hours display | Settings / budget / today | Shows UTC peak hours 01:00–04:00 and 06:00–10:00 with the current tier; expanded view shows a peak/off-peak period strip (current period + countdown), collapsed (rail) view shows a vertical peak/off-peak progress bar; independently toggleable |
|
|
43
|
+
| Peak/off-peak switch popup alert | Global overlay | A bottom-right popup appears when the next tier switch is within the configured lead time (default 2 minutes, 1–30); alert type selectable (entering peak / entering off-peak / both); one alert per switch point; toggle lives in the peak pricing panel in Settings |
|
|
43
44
|
| Official price sync | Settings page | Fetches and parses the official pricing page, applies with one click |
|
|
44
45
|
| UI language | Settings → Display settings | Simplified Chinese / English / Follow browser (auto); switches instantly and auto-saves |
|
|
45
46
|
| AI price sync | [prompt](docs/AI-PRICE-SYNC-PROMPT.en.md) | DeepSeek official sync; other providers use the verified official price catalog and manual configuration |
|
|
@@ -192,22 +193,22 @@ Real captures from an actual DSH sidebar of the period strip and collapsed verti
|
|
|
192
193
|
dsh plugin --profile web add dsh-cost-meter
|
|
193
194
|
```
|
|
194
195
|
|
|
195
|
-
**PowerShell one-click script** (copy the whole line, paste, press Enter; pnpm is provisioned automatically, git is auto-detected — no clone needed; the install chain is **pinned to the release tag `v1.5.
|
|
196
|
+
**PowerShell one-click script** (copy the whole line, paste, press Enter; pnpm is provisioned automatically, git is auto-detected — no clone needed; the install chain is **pinned to the release tag `v1.5.16`** — review the script before running):
|
|
196
197
|
|
|
197
198
|
```powershell
|
|
198
|
-
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.
|
|
199
|
+
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.16/install.ps1 | iex
|
|
199
200
|
```
|
|
200
201
|
|
|
201
202
|
**Or a plain command line** (the machine must already have pnpm and git; also pinned to the tag):
|
|
202
203
|
|
|
203
204
|
```sh
|
|
204
|
-
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.
|
|
205
|
+
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.16
|
|
205
206
|
```
|
|
206
207
|
|
|
207
208
|
Without git, use the GitHub tag archive:
|
|
208
209
|
|
|
209
210
|
```sh
|
|
210
|
-
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.
|
|
211
|
+
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.16.tar.gz
|
|
211
212
|
```
|
|
212
213
|
|
|
213
214
|
After installing, **restart** `dsh web` (plugin rows, the Typert manifest and the client bundle are all scanned at startup):
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
本会话费用 · 当日费用 · OpenCode Go 订阅额度显示 · 预算与已用百分比 · 官方账户余额 · 自定义 Provider 余额查询(可配任意 HTTP 端点) · 余额三段进度条 · 历史记录 · 峰谷计价时段显示(UTC 01:00–04:00、06:00–10:00 为峰时段) · 官方价格一键同步 · 类 Codex Token 用量热图 · 多厂商多模型价格计费(内置 90+ 模型价格目录与自动匹配) · 主流 Coding Plan 额度查询与显示(Anthropic / Z.ai / MiniMax / Kimi / OpenRouter / SiliconFlow 六家)
|
|
8
8
|
|
|
9
|
-
[](https://github.com/Han-1413141/dsh-cost-meter)
|
|
10
10
|
[](https://www.npmjs.com/package/dsh-cost-meter)
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
@@ -37,10 +37,11 @@
|
|
|
37
37
|
| 今日会话明细 | 设置页 | 每个会话的调用次数、输入/缓存/输出 token 与费用 |
|
|
38
38
|
| 历史记录 | 设置页 | 按天汇总,保留天数可配(默认 180 天) |
|
|
39
39
|
| 历史按模型统计回填 | 设置页(按模型统计) | 按模型统计上线前的旧账本自动回放宿主会话日志重建逐模型 token/费用拆分(旧调用按当时基础价),日志已清理的部分归入「早期未分模型」残差行 |
|
|
40
|
-
| 导入安装前历史 |
|
|
40
|
+
| 导入安装前历史 | 首次启动自动 | 安装/升级后首次启动自动回放宿主全部会话日志,把未装插件时期的对话导入账本(缺失日期整日重建,已有日期只补未知会话,幂等不与实时计费重复;金额按事件时刻历史价回推);设置页保留手动重跑入口 |
|
|
41
41
|
| 预算设置 | 设置页顶部 | 额度、周期(今日/本月/累计/自定义日期区间)、已用% |
|
|
42
42
|
| 价格表 | 设置页 | 每模型 谷时/峰时 两档价格(支持 input/output 简写,缓存价自动补齐),增删改自由 |
|
|
43
43
|
| 峰谷计价时段显示 | 设置页 / 预算 / 今日费用 | 显示 UTC 峰时段 01:00–04:00、06:00–10:00 与当前档位;展开态显示峰时/平价时段条(当前时段 + 倒计时),收起(rail)态显示竖向峰谷进度条,可单独开关 |
|
|
44
|
+
| 峰/谷切换弹窗提醒 | 全局浮层 | 距进入峰/谷时段不足设定提前量(默认 2 分钟,1-30 可配)时右下角弹窗提醒;提醒类型可选(进入峰 / 进入谷 / 峰和谷),同一切换点只提醒一次,设置页峰谷计价面板内开关 |
|
|
44
45
|
| 官方价格同步 | 设置页 | 抓取解析官方定价页,一键应用 |
|
|
45
46
|
| 界面语言 | 设置页 → 显示设置 | 简体中文 / English / 跟随浏览器(自动);切换即时生效并自动保存 |
|
|
46
47
|
| AI 价格同步 | [提示词](docs/AI-PRICE-SYNC-PROMPT.md) | DeepSeek 官方同步;其他 provider 使用已核对的官方价格目录与手动配置 |
|
|
@@ -194,22 +195,22 @@
|
|
|
194
195
|
dsh plugin --profile web add dsh-cost-meter
|
|
195
196
|
```
|
|
196
197
|
|
|
197
|
-
**PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.5.
|
|
198
|
+
**PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.5.16`**,建议先下载审阅再运行):
|
|
198
199
|
|
|
199
200
|
```powershell
|
|
200
|
-
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.
|
|
201
|
+
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.16/install.ps1 | iex
|
|
201
202
|
```
|
|
202
203
|
|
|
203
204
|
**或直接命令行**(机器上需已有 pnpm 与 git;同样固定到 tag):
|
|
204
205
|
|
|
205
206
|
```sh
|
|
206
|
-
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.
|
|
207
|
+
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.16
|
|
207
208
|
```
|
|
208
209
|
|
|
209
210
|
没有 git 时可用 GitHub tag 打包直链:
|
|
210
211
|
|
|
211
212
|
```sh
|
|
212
|
-
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.
|
|
213
|
+
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.16.tar.gz
|
|
213
214
|
```
|
|
214
215
|
|
|
215
216
|
安装后**重启** `dsh web`(插件行、Typert 清单与客户端 bundle 均在启动时扫描):
|
package/lib/client.js
CHANGED
|
@@ -110,6 +110,13 @@ window.__ModuleLoader__.load({
|
|
|
110
110
|
'.cm-msg.err{color:var(--dsw-alias-state-error-primary)}',
|
|
111
111
|
'.cm-hint{font-size:12px;color:var(--dsw-alias-label-tertiary)}',
|
|
112
112
|
'.cm-budget{border:1px solid var(--dsw-alias-border-l1);border-radius:12px;padding:14px 16px;background:var(--dsw-alias-bg-layer-1);display:flex;flex-direction:column;gap:10px}',
|
|
113
|
+
'.cm-peak-alert{position:fixed;right:20px;bottom:20px;z-index:9999;max-width:320px;padding:12px 14px;border-radius:10px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);box-shadow:0 8px 24px rgba(0,0,0,.18);display:flex;flex-direction:column;gap:6px;font-size:12px;animation:cm-peak-alert-in .18s ease-out}',
|
|
114
|
+
'.cm-peak-alert-peak{border-left:3px solid #f59e0b}',
|
|
115
|
+
'.cm-peak-alert-offpeak{border-left:3px solid var(--dsw-alias-brand-primary)}',
|
|
116
|
+
'.cm-peak-alert-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}',
|
|
117
|
+
'.cm-peak-alert-body{color:var(--dsw-alias-label-secondary);line-height:1.5}',
|
|
118
|
+
'.cm-peak-alert-actions{display:flex;justify-content:flex-end}',
|
|
119
|
+
'@keyframes cm-peak-alert-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}',
|
|
113
120
|
'.cm-budget-head{display:flex;align-items:center;justify-content:space-between;gap:8px}',
|
|
114
121
|
'.cm-budget-bar{height:8px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);overflow:hidden}',
|
|
115
122
|
'.cm-budget-fill{height:100%;border-radius:999px;background:var(--dsw-alias-state-business-primary);transition:width .3s ease}',
|
|
@@ -422,6 +429,18 @@ window.__ModuleLoader__.load({
|
|
|
422
429
|
peakStyleLabel: '峰谷时段条样式',
|
|
423
430
|
peakStyleCompact: '简洁(单行紧凑)',
|
|
424
431
|
peakStyleClassic: '经典(分段与胶囊芯片)',
|
|
432
|
+
peakAlertLabel: '峰/谷切换前弹窗提醒',
|
|
433
|
+
peakAlertAheadLabel: '提前提醒(分钟,1-30)',
|
|
434
|
+
peakAlertTargetLabel: '提醒类型',
|
|
435
|
+
peakAlertTargetPeak: '进入峰时',
|
|
436
|
+
peakAlertTargetOffPeak: '进入谷时',
|
|
437
|
+
peakAlertTargetBoth: '峰和谷',
|
|
438
|
+
peakAlertTitlePeak: '即将进入峰时',
|
|
439
|
+
peakAlertTitleOffPeak: '即将进入谷时',
|
|
440
|
+
peakAlertBody: '约 {time} 后计费档位切换为{phase}价,请注意本时段调用成本。',
|
|
441
|
+
peakAlertPhasePeak: '峰时',
|
|
442
|
+
peakAlertPhaseOffPeak: '谷时',
|
|
443
|
+
peakAlertBtn: '知道了',
|
|
425
444
|
peakPanelTitle: '峰谷计价与提示',
|
|
426
445
|
peakNoticeHiddenHint: '提示已隐藏:需启用峰谷计价并开启「峰时高价时段显著提示」。',
|
|
427
446
|
groupGeneral: '常规',
|
|
@@ -557,7 +576,7 @@ window.__ModuleLoader__.load({
|
|
|
557
576
|
importLegacy: '导入安装前历史',
|
|
558
577
|
confirmImportLegacy: '确认导入?将回放全部会话日志,只补账本缺失的日期与会话,不影响已有记录。',
|
|
559
578
|
legacyImportFailed: '导入安装前历史失败:{message}',
|
|
560
|
-
legacyImportNote: '
|
|
579
|
+
legacyImportNote: '插件首次启动后会自动导入一次安装前历史;此处可手动重跑(如后续拷入了旧日志)。金额按当前价目表回推,已清理的日志无法回放,同一会话跨安装时刻时其安装前用量不计入。',
|
|
561
580
|
confirmClear: '确认清除',
|
|
562
581
|
clearAllHistory: '清除全部历史',
|
|
563
582
|
lastSync: '最近同步:{time}',
|
|
@@ -723,6 +742,18 @@ window.__ModuleLoader__.load({
|
|
|
723
742
|
peakStyleLabel: 'Peak period strip style',
|
|
724
743
|
peakStyleCompact: 'Compact (one-line)',
|
|
725
744
|
peakStyleClassic: 'Classic (segments & chip)',
|
|
745
|
+
peakAlertLabel: 'Popup alert before peak/off-peak switch',
|
|
746
|
+
peakAlertAheadLabel: 'Lead time (minutes, 1-30)',
|
|
747
|
+
peakAlertTargetLabel: 'Alert type',
|
|
748
|
+
peakAlertTargetPeak: 'Entering peak',
|
|
749
|
+
peakAlertTargetOffPeak: 'Entering off-peak',
|
|
750
|
+
peakAlertTargetBoth: 'Both',
|
|
751
|
+
peakAlertTitlePeak: 'Entering peak hours soon',
|
|
752
|
+
peakAlertTitleOffPeak: 'Entering off-peak hours soon',
|
|
753
|
+
peakAlertBody: 'Billing switches to {phase} pricing in about {time}; mind your call costs in this window.',
|
|
754
|
+
peakAlertPhasePeak: 'peak',
|
|
755
|
+
peakAlertPhaseOffPeak: 'off-peak',
|
|
756
|
+
peakAlertBtn: 'Got it',
|
|
726
757
|
peakPanelTitle: 'Peak/off-peak pricing & notice',
|
|
727
758
|
peakNoticeHiddenHint: 'Notice hidden: enable peak/off-peak pricing and the “prominent notice” toggle.',
|
|
728
759
|
groupGeneral: 'General',
|
|
@@ -858,7 +889,7 @@ window.__ModuleLoader__.load({
|
|
|
858
889
|
importLegacy: 'Import pre-install history',
|
|
859
890
|
confirmImportLegacy: 'Confirm import? Replays all session logs and only fills dates/sessions missing from the ledger; existing records are untouched.',
|
|
860
891
|
legacyImportFailed: 'Failed to import pre-install history: {message}',
|
|
861
|
-
legacyImportNote: '
|
|
892
|
+
legacyImportNote: 'Pre-install history is imported automatically once on first launch; this button re-runs it manually (e.g. after copying in old logs). Costs are estimated with the current price catalog; cleaned logs cannot be replayed, and for a session spanning the install moment its pre-install usage is not counted.',
|
|
862
893
|
confirmClear: 'Confirm clear',
|
|
863
894
|
clearAllHistory: 'Clear all history',
|
|
864
895
|
lastSync: 'Last sync: {time}',
|
|
@@ -1024,6 +1055,9 @@ window.__ModuleLoader__.load({
|
|
|
1024
1055
|
? v.peakWindows.map((w, i) => ({ start: needNum(w.start, path + '.peakWindows[' + i + '].start'), end: needNum(w.end, path + '.peakWindows[' + i + '].end') }))
|
|
1025
1056
|
: [],
|
|
1026
1057
|
peakNotice: v.peakNotice !== false,
|
|
1058
|
+
peakAlertEnabled: v.peakAlertEnabled !== false,
|
|
1059
|
+
peakAlertAhead: Number.isFinite(v.peakAlertAhead) && v.peakAlertAhead >= 1 && v.peakAlertAhead <= 30 ? v.peakAlertAhead : 2,
|
|
1060
|
+
peakAlertTarget: v.peakAlertTarget === 'peak' || v.peakAlertTarget === 'offpeak' ? v.peakAlertTarget : 'both',
|
|
1027
1061
|
peakStyle: v.peakStyle === 'classic' ? 'classic' : 'compact',
|
|
1028
1062
|
priceMatch: v.priceMatch === 'exact' ? 'exact' : 'auto',
|
|
1029
1063
|
priceOverrides: (() => {
|
|
@@ -1954,6 +1988,43 @@ window.__ModuleLoader__.load({
|
|
|
1954
1988
|
: t('countdownMinute', { m: minutes })
|
|
1955
1989
|
}
|
|
1956
1990
|
|
|
1991
|
+
/**
|
|
1992
|
+
* 峰/谷切换前弹窗提醒:距下次相位切换不足配置提前量(默认 2 分钟)时,
|
|
1993
|
+
* 右下角弹全局浮层;提醒类型按配置过滤(进入峰/进入谷/峰和谷);
|
|
1994
|
+
* 同一切换点只弹一次(手动关闭即记点),切换完成后浮层自然消失。
|
|
1995
|
+
*/
|
|
1996
|
+
function PeakAlert(props) {
|
|
1997
|
+
const costStore = props.useCost ? props.useCost(s => s) : undefined
|
|
1998
|
+
const config = costStore?.state?.config
|
|
1999
|
+
const [now, setNow] = useState(Date.now())
|
|
2000
|
+
const [dismissedAt, setDismissedAt] = useState(null)
|
|
2001
|
+
useEffect(() => {
|
|
2002
|
+
const timer = window.setInterval(() => setNow(Date.now()), 10000)
|
|
2003
|
+
return () => window.clearInterval(timer)
|
|
2004
|
+
}, [])
|
|
2005
|
+
if (!config || config.peakAlertEnabled !== true || config.peakEnabled !== true) return null
|
|
2006
|
+
const effectiveAtMs = Date.parse(config.peakEffectiveAt || '')
|
|
2007
|
+
if (Number.isFinite(effectiveAtMs) && now < effectiveAtMs) return null
|
|
2008
|
+
const view = peakPhaseAt(now, Array.isArray(config.peakWindows) ? config.peakWindows : [])
|
|
2009
|
+
if (view === null) return null
|
|
2010
|
+
const target = config.peakAlertTarget === 'peak' || config.peakAlertTarget === 'offpeak' ? config.peakAlertTarget : 'both'
|
|
2011
|
+
if (target !== 'both' && target !== (view.nextIntoPeak ? 'peak' : 'offpeak')) return null
|
|
2012
|
+
const aheadMinutes = Number(config.peakAlertAhead)
|
|
2013
|
+
const aheadMs = (Number.isFinite(aheadMinutes) && aheadMinutes >= 1 ? aheadMinutes : 2) * 60000
|
|
2014
|
+
if (now < view.nextAtMs - aheadMs || now >= view.nextAtMs) return null
|
|
2015
|
+
if (dismissedAt === view.nextAtMs) return null
|
|
2016
|
+
const t = makeT(resolveLocale(config.locale))
|
|
2017
|
+
const intoPeak = view.nextIntoPeak === true
|
|
2018
|
+
return el('div', { className: 'cm-peak-alert ' + (intoPeak ? 'cm-peak-alert-peak' : 'cm-peak-alert-offpeak'), role: 'alert' },
|
|
2019
|
+
el('div', { className: 'cm-peak-alert-title' }, t(intoPeak ? 'peakAlertTitlePeak' : 'peakAlertTitleOffPeak')),
|
|
2020
|
+
el('div', { className: 'cm-peak-alert-body' }, t('peakAlertBody', {
|
|
2021
|
+
time: countdownText(view, now, t),
|
|
2022
|
+
phase: t(intoPeak ? 'peakAlertPhasePeak' : 'peakAlertPhaseOffPeak'),
|
|
2023
|
+
})),
|
|
2024
|
+
el('div', { className: 'cm-peak-alert-actions' },
|
|
2025
|
+
el('button', { className: 'cm-btn', onClick: () => setDismissedAt(view.nextAtMs) }, t('peakAlertBtn'))))
|
|
2026
|
+
}
|
|
2027
|
+
|
|
1957
2028
|
/** 展开态简洁样式:单行紧凑时段条——细轨道(左橙右蓝,非当前段淡化)+ 标记线 + 右侧倒计时文本。 */
|
|
1958
2029
|
function PeakStrip(props) {
|
|
1959
2030
|
const { config, t } = props
|
|
@@ -2718,6 +2789,35 @@ window.__ModuleLoader__.load({
|
|
|
2718
2789
|
},
|
|
2719
2790
|
el('option', { value: 'compact' }, t('peakStyleCompact')),
|
|
2720
2791
|
el('option', { value: 'classic' }, t('peakStyleClassic'))))),
|
|
2792
|
+
el('div', { className: 'cm-grid' },
|
|
2793
|
+
el('div', { className: 'cm-field' },
|
|
2794
|
+
el('label', { className: 'cm-check' },
|
|
2795
|
+
el('input', {
|
|
2796
|
+
type: 'checkbox',
|
|
2797
|
+
checked: draft?.peakAlertEnabled !== false,
|
|
2798
|
+
onChange: event => setField('peakAlertEnabled', event.target.checked),
|
|
2799
|
+
}),
|
|
2800
|
+
el('span', null, t('peakAlertLabel')))),
|
|
2801
|
+
el('div', { className: 'cm-field' },
|
|
2802
|
+
el('label', null, t('peakAlertAheadLabel')),
|
|
2803
|
+
el('input', {
|
|
2804
|
+
className: 'cm-input narrow', type: 'number', min: '1', max: '30', step: '1',
|
|
2805
|
+
value: String(typeof draft?.peakAlertAhead === 'number' && Number.isFinite(draft.peakAlertAhead) && draft.peakAlertAhead >= 1 && draft.peakAlertAhead <= 30 ? draft.peakAlertAhead : 2),
|
|
2806
|
+
onChange: event => {
|
|
2807
|
+
const parsed = Number(event.target.value)
|
|
2808
|
+
if (Number.isInteger(parsed) && parsed >= 1 && parsed <= 30) setField('peakAlertAhead', parsed)
|
|
2809
|
+
},
|
|
2810
|
+
})),
|
|
2811
|
+
el('div', { className: 'cm-field' },
|
|
2812
|
+
el('label', null, t('peakAlertTargetLabel')),
|
|
2813
|
+
el('select', {
|
|
2814
|
+
className: 'cm-input',
|
|
2815
|
+
value: draft?.peakAlertTarget === 'peak' || draft?.peakAlertTarget === 'offpeak' ? draft.peakAlertTarget : 'both',
|
|
2816
|
+
onChange: event => setField('peakAlertTarget', event.target.value),
|
|
2817
|
+
},
|
|
2818
|
+
el('option', { value: 'both' }, t('peakAlertTargetBoth')),
|
|
2819
|
+
el('option', { value: 'peak' }, t('peakAlertTargetPeak')),
|
|
2820
|
+
el('option', { value: 'offpeak' }, t('peakAlertTargetOffPeak'))))),
|
|
2721
2821
|
el('div', { className: 'cm-peak-preview' },
|
|
2722
2822
|
draft?.peakEnabled !== false && draft?.peakNotice !== false
|
|
2723
2823
|
? peakNoticeEl(state, previewConfig, t)
|
|
@@ -4406,6 +4506,26 @@ window.__ModuleLoader__.load({
|
|
|
4406
4506
|
})
|
|
4407
4507
|
}
|
|
4408
4508
|
|
|
4509
|
+
// 峰/谷切换前弹窗提醒:全局 fixed 浮层,挂在 dock 插槽常驻渲染,组件内部
|
|
4510
|
+
// 再按配置门控(peakAlertEnabled + peakEnabled);开关变化时重挂/卸载。
|
|
4511
|
+
const peakAlertActive = { gen: 0, dispose: null }
|
|
4512
|
+
const registerPeakAlert = enabled => {
|
|
4513
|
+
if (peakAlertActive.dispose !== null) { peakAlertActive.dispose(); peakAlertActive.dispose = null }
|
|
4514
|
+
peakAlertActive.gen += 1
|
|
4515
|
+
const gen = peakAlertActive.gen
|
|
4516
|
+
if (!enabled) return
|
|
4517
|
+
slots.inject('conversation.composer.dock', () => {
|
|
4518
|
+
if (peakAlertActive.gen !== gen) return
|
|
4519
|
+
const dispose = slots.register({ name: 'conversation.composer.dock', id: 'cost-meter-peak-alert', order: 8, inject: injected }, PeakAlert)
|
|
4520
|
+
if (peakAlertActive.gen !== gen) { dispose(); return }
|
|
4521
|
+
peakAlertActive.dispose = dispose
|
|
4522
|
+
return () => {
|
|
4523
|
+
if (peakAlertActive.dispose === dispose) peakAlertActive.dispose = null
|
|
4524
|
+
dispose()
|
|
4525
|
+
}
|
|
4526
|
+
})
|
|
4527
|
+
}
|
|
4528
|
+
|
|
4409
4529
|
// 设置页「费用/Cost」分节:语言变化时撤销旧注册并重建,让侧边栏标签同步。
|
|
4410
4530
|
const sectionActive = { gen: 0, dispose: null }
|
|
4411
4531
|
const registerSection = locale => {
|
|
@@ -4478,6 +4598,7 @@ window.__ModuleLoader__.load({
|
|
|
4478
4598
|
let lastCorner = null
|
|
4479
4599
|
let lastSectionLocale = null
|
|
4480
4600
|
let lastUsagePosition = null
|
|
4601
|
+
let lastPeakAlert = null
|
|
4481
4602
|
const sync = () => {
|
|
4482
4603
|
const state = store.getSnapshot().state
|
|
4483
4604
|
const position = state?.config?.position ?? 'dock'
|
|
@@ -4500,6 +4621,11 @@ window.__ModuleLoader__.load({
|
|
|
4500
4621
|
registerCorner(cornerEnabled)
|
|
4501
4622
|
lastCorner = cornerEnabled
|
|
4502
4623
|
}
|
|
4624
|
+
const peakAlertOn = state?.config?.peakAlertEnabled !== false && state?.config?.peakEnabled === true
|
|
4625
|
+
if (peakAlertOn !== lastPeakAlert) {
|
|
4626
|
+
registerPeakAlert(peakAlertOn)
|
|
4627
|
+
lastPeakAlert = peakAlertOn
|
|
4628
|
+
}
|
|
4503
4629
|
if (sectionLocale !== lastSectionLocale) {
|
|
4504
4630
|
registerSection(sectionLocale)
|
|
4505
4631
|
lastSectionLocale = sectionLocale
|
package/lib/index.js
CHANGED
|
@@ -1017,6 +1017,34 @@ function createService(ctx, ledger) {
|
|
|
1017
1017
|
|
|
1018
1018
|
// ── 插件主体 ───────────────────────────────────────────────────────────────
|
|
1019
1019
|
|
|
1020
|
+
/**
|
|
1021
|
+
* 启动期历史导入(issue #27):先做按模型回填,再在**本插件首次启动**(config
|
|
1022
|
+
* 标记 legacyAutoImportedAt 为 0)时自动导入一次安装前历史——用户无需手动触
|
|
1023
|
+
* 发;之后每次启动只跑回填,自动导入不再重复。导出供测试直接调用。
|
|
1024
|
+
* @param ledger - 已打开的账本。
|
|
1025
|
+
* @param sessionsRoot - 宿主会话根目录($DSH_HOME/sessions)。
|
|
1026
|
+
*/
|
|
1027
|
+
export async function runStartupImports(ledger, sessionsRoot) {
|
|
1028
|
+
const filled = await backfillLegacyLedger(ledger, sessionsRoot)
|
|
1029
|
+
if (filled.days > 0 || filled.sessions > 0 || filled.titles > 0) {
|
|
1030
|
+
const extras = [
|
|
1031
|
+
filled.days > 0 || filled.sessions > 0 ? `${filled.days} 天 / ${filled.sessions} 个会话` : null,
|
|
1032
|
+
filled.titles > 0 ? `${filled.titles} 个会话标题` : null,
|
|
1033
|
+
filled.recosted > 0 ? `重算 ${filled.recosted} 天金额` : null,
|
|
1034
|
+
].filter(Boolean).join(',')
|
|
1035
|
+
console.log(`[dsh-cost-meter] 历史按模型统计回填完成:${extras}(扫描 ${filled.scanned} 份会话日志)`)
|
|
1036
|
+
}
|
|
1037
|
+
if (!(Number(ledger.config?.legacyAutoImportedAt) > 0)) {
|
|
1038
|
+
const stats = await importLegacyHistory(ledger, sessionsRoot)
|
|
1039
|
+
if (stats.days > 0 || stats.sessions > 0) {
|
|
1040
|
+
console.log(`[dsh-cost-meter] 安装前历史自动导入完成:${stats.days} 天 / ${stats.sessions} 个会话(扫描 ${stats.scanned} 份会话日志)`)
|
|
1041
|
+
}
|
|
1042
|
+
// 无论是否有可导入内容都打标:空结果同样视为已完成,避免每次启动重扫。
|
|
1043
|
+
ledger.config.legacyAutoImportedAt = Date.now()
|
|
1044
|
+
ledger.scheduleWrite()
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1020
1048
|
/**
|
|
1021
1049
|
* 挂载账本、llm/stream 计费包裹、会话投影与 costMeter 服务。
|
|
1022
1050
|
* @param ctx - 宿主插件上下文。
|
|
@@ -1028,20 +1056,11 @@ export function apply(ctx) {
|
|
|
1028
1056
|
// 卸载/退出前最终落盘。
|
|
1029
1057
|
ctx.effect(() => () => ledger.close(), 'cost-meter: ledger close')
|
|
1030
1058
|
|
|
1031
|
-
//
|
|
1032
|
-
//
|
|
1059
|
+
// 历史账本按模型回填 + 首次启动自动导入安装前历史(issue #27):
|
|
1060
|
+
// 启动后延迟执行,避免拖慢宿主启动;均幂等,只补缺失,不重复计数。
|
|
1033
1061
|
const backfillTimer = setTimeout(() => {
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
const extras = [
|
|
1037
|
-
filled.days > 0 || filled.sessions > 0 ? `${filled.days} 天 / ${filled.sessions} 个会话` : null,
|
|
1038
|
-
filled.titles > 0 ? `${filled.titles} 个会话标题` : null,
|
|
1039
|
-
filled.recosted > 0 ? `重算 ${filled.recosted} 天金额` : null,
|
|
1040
|
-
].filter(Boolean).join(',')
|
|
1041
|
-
console.log(`[dsh-cost-meter] 历史按模型统计回填完成:${extras}(扫描 ${filled.scanned} 份会话日志)`)
|
|
1042
|
-
}
|
|
1043
|
-
}, error => {
|
|
1044
|
-
console.warn(`[dsh-cost-meter] 历史按模型统计回填失败: ${String(error?.message ?? error)}`)
|
|
1062
|
+
runStartupImports(ledger, join(resolveDshHome(), 'sessions')).catch(error => {
|
|
1063
|
+
console.warn(`[dsh-cost-meter] 启动期历史导入失败: ${String(error?.message ?? error)}`)
|
|
1045
1064
|
})
|
|
1046
1065
|
}, 3000)
|
|
1047
1066
|
backfillTimer.unref?.()
|
package/lib/store.js
CHANGED
|
@@ -38,7 +38,11 @@ export function defaultConfig() {
|
|
|
38
38
|
peakEffectiveAt: DEFAULT_PEAK_EFFECTIVE_AT,
|
|
39
39
|
peakWindows: DEFAULT_PEAK_WINDOWS.map(w => ({ ...w })),
|
|
40
40
|
peakNotice: true, // 峰时高价时段显著提示(侧边栏预算框/今日费用/设置页预算面板)
|
|
41
|
+
peakAlertEnabled: true, // 峰/谷切换前弹窗提醒(全局浮层)
|
|
42
|
+
peakAlertAhead: 2, // 提前提醒量(分钟,1-30)
|
|
43
|
+
peakAlertTarget: 'both', // 提醒类型:peak(进入峰) | offpeak(进入谷) | both(峰和谷)
|
|
41
44
|
showSessionId: false, // 会话列表中附显会话 ID(默认只显示标题,需要时开启)
|
|
45
|
+
legacyAutoImportedAt: 0, // 安装前历史自动导入标记(issue #27):完成时刻 ms;0 = 尚未跑过
|
|
42
46
|
peakStyle: 'compact', // 峰谷时段条样式:compact(简洁单行/竖向同构) | classic(经典分段/胶囊芯片)
|
|
43
47
|
priceMatch: 'auto', // 未知模型名自动匹配价格表:auto(去后缀/前缀/家族相似) | exact(仅精确)
|
|
44
48
|
priceOverrides: {}, // 手动匹配覆盖:{ 'provider:modelId': '同provider模型 | provider:模型 | deepseek:__default__' }
|
|
@@ -220,6 +224,9 @@ const VALIDATION_MESSAGES = {
|
|
|
220
224
|
peakEffectiveAt: 'peakEffectiveAt 非法',
|
|
221
225
|
peakWindows: 'peakWindows 必须是数组',
|
|
222
226
|
peakNotice: 'peakNotice 必须是布尔值',
|
|
227
|
+
peakAlertEnabled: 'peakAlertEnabled 必须是布尔值',
|
|
228
|
+
peakAlertAhead: 'peakAlertAhead 必须是 1-30 的整数',
|
|
229
|
+
peakAlertTarget: 'peakAlertTarget 必须是 peak / offpeak / both',
|
|
223
230
|
showSessionId: 'showSessionId 必须是布尔值',
|
|
224
231
|
peakStyle: 'peakStyle 必须是 compact / classic',
|
|
225
232
|
priceMatch: 'priceMatch 必须是 auto / exact',
|
|
@@ -281,6 +288,9 @@ const VALIDATION_MESSAGES = {
|
|
|
281
288
|
peakEffectiveAt: 'Invalid peakEffectiveAt',
|
|
282
289
|
peakWindows: 'peakWindows must be an array',
|
|
283
290
|
peakNotice: 'peakNotice must be a boolean',
|
|
291
|
+
peakAlertEnabled: 'peakAlertEnabled must be a boolean',
|
|
292
|
+
peakAlertAhead: 'peakAlertAhead must be an integer between 1 and 30',
|
|
293
|
+
peakAlertTarget: 'peakAlertTarget must be peak / offpeak / both',
|
|
284
294
|
showSessionId: 'showSessionId must be a boolean',
|
|
285
295
|
peakStyle: 'peakStyle must be compact / classic',
|
|
286
296
|
priceMatch: 'priceMatch must be auto / exact',
|
|
@@ -404,6 +414,11 @@ export function applyConfigPatch(current, patch) {
|
|
|
404
414
|
if (typeof candidate.peakEffectiveAt !== 'string') errors.push(vmsg(locale, 'peakEffectiveAt'))
|
|
405
415
|
if (!Array.isArray(candidate.peakWindows)) errors.push(vmsg(locale, 'peakWindows'))
|
|
406
416
|
if (typeof candidate.peakNotice !== 'boolean') errors.push(vmsg(locale, 'peakNotice'))
|
|
417
|
+
// 峰/谷切换前弹窗提醒:开关可缺省(默认开);提前量 1-30 分钟;类型三选一。
|
|
418
|
+
if (candidate.peakAlertEnabled !== undefined && typeof candidate.peakAlertEnabled !== 'boolean') errors.push(vmsg(locale, 'peakAlertEnabled'))
|
|
419
|
+
const alertAhead = Number(candidate.peakAlertAhead)
|
|
420
|
+
if (!Number.isInteger(alertAhead) || alertAhead < 1 || alertAhead > 30) errors.push(vmsg(locale, 'peakAlertAhead'))
|
|
421
|
+
if (!['peak', 'offpeak', 'both'].includes(candidate.peakAlertTarget)) errors.push(vmsg(locale, 'peakAlertTarget'))
|
|
407
422
|
if (candidate.showSessionId !== undefined && typeof candidate.showSessionId !== 'boolean') errors.push(vmsg(locale, 'showSessionId'))
|
|
408
423
|
if (candidate.peakStyle !== 'compact' && candidate.peakStyle !== 'classic') errors.push(vmsg(locale, 'peakStyle'))
|
|
409
424
|
if (candidate.priceMatch !== 'auto' && candidate.priceMatch !== 'exact') errors.push(vmsg(locale, 'priceMatch'))
|
|
@@ -588,6 +603,13 @@ export function sanitizeConfig(raw) {
|
|
|
588
603
|
out.decimals = Math.max(0, Math.min(10, Math.floor(Number(out.decimals) || 0)))
|
|
589
604
|
out.historyDays = Math.max(7, Math.min(3650, Math.floor(Number(out.historyDays) || 180)))
|
|
590
605
|
out.showSessionId = out.showSessionId === true
|
|
606
|
+
// 峰/谷切换弹窗提醒:非法值定向收敛(开关默认开、提前量回 2、类型回 both)。
|
|
607
|
+
out.peakAlertEnabled = out.peakAlertEnabled !== false
|
|
608
|
+
const alertAhead = Number(out.peakAlertAhead)
|
|
609
|
+
out.peakAlertAhead = Number.isInteger(alertAhead) && alertAhead >= 1 && alertAhead <= 30 ? alertAhead : 2
|
|
610
|
+
out.peakAlertTarget = oneOf(out.peakAlertTarget, ['peak', 'offpeak', 'both'], 'both')
|
|
611
|
+
// 安装前历史自动导入标记:有限正数保留,否则归零(下次启动重试)。
|
|
612
|
+
out.legacyAutoImportedAt = isNum(Number(out.legacyAutoImportedAt)) && Number(out.legacyAutoImportedAt) > 0 ? Number(out.legacyAutoImportedAt) : 0
|
|
591
613
|
if (!isNum(out.exchangeRate) || out.exchangeRate <= 0) out.exchangeRate = base.exchangeRate
|
|
592
614
|
if (!Array.isArray(out.peakWindows)) out.peakWindows = base.peakWindows
|
|
593
615
|
else out.peakWindows = out.peakWindows.filter(w => w !== null && typeof w === 'object' && isNum(Number(w.start)) && isNum(Number(w.end)))
|
package/lib/typert.host.js
CHANGED
|
@@ -103,7 +103,13 @@ const configSchema = z.object({
|
|
|
103
103
|
peakEffectiveAt: z.string(),
|
|
104
104
|
peakWindows: z.array(z.object({ start: num, end: num })),
|
|
105
105
|
peakNotice: z.boolean().optional(),
|
|
106
|
+
// 峰/谷切换前弹窗提醒:开关(默认开)/提前分钟数(1-30)/类型(峰|谷|两者)。
|
|
107
|
+
peakAlertEnabled: z.boolean().optional(),
|
|
108
|
+
peakAlertAhead: num.optional(),
|
|
109
|
+
peakAlertTarget: z.enum(['peak', 'offpeak', 'both']).optional(),
|
|
106
110
|
showSessionId: z.boolean().optional(),
|
|
111
|
+
// 安装前历史自动导入完成时刻(issue #27,内部标记;0/缺席 = 尚未跑过)。
|
|
112
|
+
legacyAutoImportedAt: num.optional(),
|
|
107
113
|
peakStyle: z.enum(['compact', 'classic']).optional(),
|
|
108
114
|
priceMatch: z.enum(['auto', 'exact']).optional(),
|
|
109
115
|
priceOverrides: z.record(z.string(), z.string()).optional(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cost-meter",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.16",
|
|
4
4
|
"description": "DeepSeek Harness 会话费用统计插件:本会话成本、当日费用、历史记录与官方价格同步,支持多厂商多模型价格计费(内置 90+ 模型价格目录与自动匹配)、主流 Coding Plan 订阅额度查询与显示(7 家,含 SCNet Token Plan 本地 Credits 计量)、自定义 Provider 余额查询(可配任意 HTTP 端点)与余额进度条,界面中英双语。Session cost tracking plugin for DeepSeek Harness: per-conversation cost, daily totals, history and official price sync, with multi-vendor model pricing (built-in 90+ model catalog with auto-matching), Coding Plan quota queries & display for 7 vendors (incl. local credits metering for the SCNet Token Plan), custom provider balance lookup (configurable HTTP endpoint) and balance progress bar, in a bilingual (Chinese/English) UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|