dsh-cost-meter 1.5.37 → 1.5.38
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 +6 -5
- package/README.md +6 -5
- package/lib/backfill.js +63 -0
- package/lib/billing-stream.js +63 -0
- package/lib/client.js +21 -2
- package/lib/index.js +31 -30
- package/lib/store.js +6 -0
- package/lib/typert.host.js +2 -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) · 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
|
-
[](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)
|
|
@@ -45,6 +45,7 @@ English | [中文](README.md)
|
|
|
45
45
|
| Peak/off-peak switch popup alert | Global overlay | A full-width bracketed popup appears when the next tier switch is within the configured lead time (default 2 minutes, 1–30), with an alert-colored badge distinguishing entering peak vs off-peak; position selectable (**bottom-right / screen center**), alert type selectable (entering peak / entering off-peak / both), one alert per switch point; optionally **sends a browser (system) notification** (so you still get alerted when the page is backgrounded; requires granting notification permission); configured in the peak pricing panel in Settings, with a **one-click popup preview** (rendered by the real component — copy, position and notifications exactly as they will fire) |
|
|
46
46
|
| Official price sync | Settings page | Fetches and parses the official pricing page, applies with one click |
|
|
47
47
|
| UI language | Settings → Display settings | Simplified Chinese / English / Follow browser (auto); switches instantly and auto-saves |
|
|
48
|
+
| Hide amounts (privacy mode) | Settings → Display settings | One toggle masks every amount (costs / balances / budget / history) as “$ ***” while token stats stay visible — safe for screen sharing and screenshots |
|
|
48
49
|
| 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 |
|
|
49
50
|
| Model & Plan adaptation guide | [adaptation doc](docs/model-and-plan-adaptation.en.md) | Adaptation matrix for per-model billing and the 8 Coding Plan vendors, the auto-matching mechanism and price sources ([中文](docs/model-and-plan-adaptation.md)) |
|
|
50
51
|
| Peak/off-peak alert guide | [alert doc](docs/peak-alert.en.md) | Fully illustrated guide to the pre-switch popup and system notification: effect screenshots (EN/中文), settings reference and usage tips ([中文](docs/peak-alert.md)) |
|
|
@@ -228,22 +229,22 @@ Real captures from an actual DSH sidebar of the period strip and collapsed verti
|
|
|
228
229
|
dsh plugin --profile web add dsh-cost-meter
|
|
229
230
|
```
|
|
230
231
|
|
|
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.
|
|
232
|
+
**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.38`** — review the script before running):
|
|
232
233
|
|
|
233
234
|
```powershell
|
|
234
|
-
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.
|
|
235
|
+
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.38/install.ps1 | iex
|
|
235
236
|
```
|
|
236
237
|
|
|
237
238
|
**Or a plain command line** (the machine must already have pnpm and git; also pinned to the tag):
|
|
238
239
|
|
|
239
240
|
```sh
|
|
240
|
-
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.
|
|
241
|
+
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.38
|
|
241
242
|
```
|
|
242
243
|
|
|
243
244
|
Without git, use the GitHub tag archive:
|
|
244
245
|
|
|
245
246
|
```sh
|
|
246
|
-
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.
|
|
247
|
+
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.38.tar.gz
|
|
247
248
|
```
|
|
248
249
|
|
|
249
250
|
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
|
-
[](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)
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
| 峰/谷切换弹窗提醒 | 全局浮层 | 距进入峰/谷时段不足设定提前量(默认 2 分钟,1-30 可配)时全屏色条徽标弹窗(提醒色区分进入峰/谷);弹窗位置可选**右下角 / 屏幕中心**,提醒类型可选(进入峰 / 进入谷 / 峰和谷),同一切换点只提醒一次;可选**同步发送浏览器(系统)通知**(页面最小化也能收到,需授权通知权限);设置页峰谷计价面板内配置,并可**一键预览弹窗效果**(真实组件渲染,文案/位置/通知与实际触发完全一致) |
|
|
47
47
|
| 官方价格同步 | 设置页 | 抓取解析官方定价页,一键应用 |
|
|
48
48
|
| 界面语言 | 设置页 → 显示设置 | 简体中文 / English / 跟随浏览器(自动);切换即时生效并自动保存 |
|
|
49
|
+
| 隐藏金额(隐私模式) | 设置页 → 显示设置 | 一键把全部金额(费用/余额/预算/历史等)遮罩为「$ ***」,token 统计不受影响,共享屏幕/截图防泄露 |
|
|
49
50
|
| AI 价格同步 | [提示词](docs/AI-PRICE-SYNC-PROMPT.md) | DeepSeek 官方同步;其他 provider 使用已核对的官方价格目录与手动配置 |
|
|
50
51
|
| 模型与 Plan 适配说明 | [适配文档](docs/model-and-plan-adaptation.md) | 各厂商模型计费与 8 家 Coding Plan 的适配矩阵、自动匹配机制与价格来源([English](docs/model-and-plan-adaptation.en.md)) |
|
|
51
52
|
| 峰/谷切换提醒图解 | [提醒文档](docs/peak-alert.md) | 峰谷切换前弹窗与系统通知的完整图解:效果截图(中/英)、设置项说明与使用建议([English](docs/peak-alert.en.md)) |
|
|
@@ -230,22 +231,22 @@
|
|
|
230
231
|
dsh plugin --profile web add dsh-cost-meter
|
|
231
232
|
```
|
|
232
233
|
|
|
233
|
-
**PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.5.
|
|
234
|
+
**PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.5.38`**,建议先下载审阅再运行):
|
|
234
235
|
|
|
235
236
|
```powershell
|
|
236
|
-
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.
|
|
237
|
+
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.38/install.ps1 | iex
|
|
237
238
|
```
|
|
238
239
|
|
|
239
240
|
**或直接命令行**(机器上需已有 pnpm 与 git;同样固定到 tag):
|
|
240
241
|
|
|
241
242
|
```sh
|
|
242
|
-
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.
|
|
243
|
+
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.38
|
|
243
244
|
```
|
|
244
245
|
|
|
245
246
|
没有 git 时可用 GitHub tag 打包直链:
|
|
246
247
|
|
|
247
248
|
```sh
|
|
248
|
-
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.
|
|
249
|
+
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.38.tar.gz
|
|
249
250
|
```
|
|
250
251
|
|
|
251
252
|
安装后**重启** `dsh web`(插件行、Typert 清单与客户端 bundle 均在启动时扫描):
|
package/lib/backfill.js
CHANGED
|
@@ -682,3 +682,66 @@ export function repairForkSeed(ledger, sessionsRoot) {
|
|
|
682
682
|
}
|
|
683
683
|
return result
|
|
684
684
|
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* 包装路由重复计费一次性清洗(issue #48):modlens / vision-router 这类包装
|
|
688
|
+
* 插件在自身 stream() 体内再发起 ctx.llm.stream(),旧版计费监听器在瀑布
|
|
689
|
+
* 每层都记账,同一次请求在 byProviderModel 里留下 official / modlens /
|
|
690
|
+
* modlens-vision 等多份 token 逐位相同的条目(报告者 08-22 账本:三行
|
|
691
|
+
* 40 次调用 token 逐位相同)。本函数对每个日期与其下每个会话的
|
|
692
|
+
* byProviderModel 按指纹分组——键的模型后缀(首个冒号之后,嵌套包装链
|
|
693
|
+
* 每层透传同一 model)与六值桶(input/output/cacheRead/cacheWrite/
|
|
694
|
+
* reasoning/calls)完全一致才算同组;每组保留字母序第一个,其余条目从
|
|
695
|
+
* 所在容器(day 或 session)的顶层合计(含 cost、calls)中扣除后删除。
|
|
696
|
+
*
|
|
697
|
+
* 幂等由调用方用账本 migrations 标记(provider-dedup-v1)保证只跑一次;
|
|
698
|
+
* 扣除逐字段 clamp ≥ 0,指纹不全同的条目(不同 token 量的真实调用,如
|
|
699
|
+
* 报告者 deepseek-vision 的 24 次独立记录)不碰。
|
|
700
|
+
* @param ledger - 已打开的账本。
|
|
701
|
+
* @returns { groups, removedCost } 合并的重复组数与扣除的金额合计(USD)。
|
|
702
|
+
*/
|
|
703
|
+
export function repairProviderDupes(ledger) {
|
|
704
|
+
const result = { groups: 0, removedCost: 0 }
|
|
705
|
+
// 清洗单个容器(day 或 session):返回是否发生扣除。
|
|
706
|
+
const repairContainer = (container) => {
|
|
707
|
+
const pm = container?.byProviderModel
|
|
708
|
+
if (pm === null || typeof pm !== 'object') return false
|
|
709
|
+
// 指纹 → 键列表。键形如 `${provider}:${model}`,provider 名不含冒号,
|
|
710
|
+
// 首个冒号之后即模型 id;跨 provider 指纹相同且模型相同才是包装链重复。
|
|
711
|
+
const groups = new Map()
|
|
712
|
+
for (const [key, bucket] of Object.entries(pm)) {
|
|
713
|
+
if (bucket === null || typeof bucket !== 'object') continue
|
|
714
|
+
const model = key.slice(key.indexOf(':') + 1)
|
|
715
|
+
const fingerprint = `${model}|${bucket.input ?? 0}|${bucket.output ?? 0}|${bucket.cacheRead ?? 0}|${bucket.cacheWrite ?? 0}|${bucket.reasoning ?? 0}|${bucket.calls ?? 0}`
|
|
716
|
+
const list = groups.get(fingerprint)
|
|
717
|
+
if (list === undefined) groups.set(fingerprint, [key])
|
|
718
|
+
else list.push(key)
|
|
719
|
+
}
|
|
720
|
+
let touched = false
|
|
721
|
+
for (const keys of groups.values()) {
|
|
722
|
+
if (keys.length < 2) continue
|
|
723
|
+
keys.sort()
|
|
724
|
+
for (const key of keys.slice(1)) {
|
|
725
|
+
const bucket = pm[key]
|
|
726
|
+
subtractBucketInto(container, bucket)
|
|
727
|
+
delete pm[key]
|
|
728
|
+
result.removedCost += bucket.cost ?? 0
|
|
729
|
+
touched = true
|
|
730
|
+
}
|
|
731
|
+
result.groups += 1
|
|
732
|
+
}
|
|
733
|
+
return touched
|
|
734
|
+
}
|
|
735
|
+
let scheduled = false
|
|
736
|
+
for (const day of Object.values(ledger.days ?? {})) {
|
|
737
|
+
if (day === null || typeof day !== 'object') continue
|
|
738
|
+
if (repairContainer(day)) scheduled = true
|
|
739
|
+
if (Array.isArray(day.sessions)) {
|
|
740
|
+
for (const session of day.sessions) {
|
|
741
|
+
if (session !== null && typeof session === 'object' && repairContainer(session)) scheduled = true
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (scheduled) ledger.scheduleWrite()
|
|
746
|
+
return result
|
|
747
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* llm/stream 计费包裹(issue #48 包装路由重复计费修复)。
|
|
3
|
+
*
|
|
4
|
+
* 宿主的 `llm/stream` 是一条监听器瀑布:每次 ctx.llm.stream() 都会把全部
|
|
5
|
+
* 监听器按注册序串起来。modlens / vision-router 这类包装路由插件的适配器
|
|
6
|
+
* 在自己 stream() 体内再次调用 ctx.llm.stream({ ..., provider: upstream })
|
|
7
|
+
* ——于是同一次请求沿 `wrapper-vision → wrapper → official` 每层都完整走
|
|
8
|
+
* 一遍瀑布,链尾的计费监听器每层都把 usage 记进账本(同 token 逐位相同 ×3)。
|
|
9
|
+
*
|
|
10
|
+
* 修复:用 AsyncLocalStorage 标记「正在计费消费的流」。外层计费包装在标记
|
|
11
|
+
* 内拉取下游 chunk;下游适配器体内再发起的 ctx.llm.stream() 在同一异步上
|
|
12
|
+
* 下文中同步分发瀑布,内层计费监听器读到标记即判定为嵌套调用,直接透传
|
|
13
|
+
* next() 不再包一层(也就不再记账)。usage 只由最外层记一次。
|
|
14
|
+
*
|
|
15
|
+
* ALS 按异步上下文隔离:两个并发请求各自从无标记的根上下文进入,互不误伤。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
19
|
+
|
|
20
|
+
/** 嵌套深度标记:值恒为 1(存在即嵌套);按异步上下文隔离,并发流互不串扰。 */
|
|
21
|
+
const llmStreamDepth = new AsyncLocalStorage()
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 创建 llm/stream 计费监听器。
|
|
25
|
+
* @param {object} deps
|
|
26
|
+
* @param {(usage: object, model: string, sessionId: string, atMs: number, provider: string) => void} deps.account
|
|
27
|
+
* 计费回调:流结束时以捕获的 usage 块调用(五参签名与 ledger.account 对齐,
|
|
28
|
+
* 由 index.js 负责把 usage 五桶映射过去)。
|
|
29
|
+
* @returns {(options: object, next: () => AsyncIterable) => AsyncIterable} 宿主监听器。
|
|
30
|
+
*/
|
|
31
|
+
export function createLlmStreamBilling({ account }) {
|
|
32
|
+
return (options, next) => {
|
|
33
|
+
const downstream = next()
|
|
34
|
+
// 嵌套内层(包装路由在上层计费流的消费上下文中再发起的 llm/stream):
|
|
35
|
+
// 外层已记账,透传下游流,不再包裹。
|
|
36
|
+
if (llmStreamDepth.getStore() !== undefined) return downstream
|
|
37
|
+
return (async function* costMeterStream() {
|
|
38
|
+
let usage = null
|
|
39
|
+
const iterator = downstream[Symbol.asyncIterator]()
|
|
40
|
+
try {
|
|
41
|
+
for (;;) {
|
|
42
|
+
// 在深度标记内拉取下游:下游适配器(modlens 等)体内再发起的
|
|
43
|
+
// ctx.llm.stream() 同步瀑布分发继承 depth=1,其监听器判定为嵌套。
|
|
44
|
+
const result = await llmStreamDepth.run(1, () => iterator.next())
|
|
45
|
+
if (result.done) break
|
|
46
|
+
const chunk = result.value
|
|
47
|
+
if (chunk !== null && chunk !== undefined && chunk.type === 'usage' && chunk.usage !== undefined) {
|
|
48
|
+
usage = chunk.usage
|
|
49
|
+
}
|
|
50
|
+
yield chunk
|
|
51
|
+
}
|
|
52
|
+
} finally {
|
|
53
|
+
if (usage !== null) {
|
|
54
|
+
try {
|
|
55
|
+
account(usage, options?.model, options?.sessionId, Date.now(), options?.provider)
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.warn(`[dsh-cost-meter] 计费失败: ${String(error)}`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
})()
|
|
62
|
+
}
|
|
63
|
+
}
|
package/lib/client.js
CHANGED
|
@@ -574,6 +574,7 @@ window.__ModuleLoader__.load({
|
|
|
574
574
|
sessionRankError: '会话排行加载失败',
|
|
575
575
|
sessionRankLimit: '显示条数',
|
|
576
576
|
showSessionIdLabel: '会话列表附显会话 ID',
|
|
577
|
+
hideAmountsLabel: '隐藏金额(隐私模式:余额与费用金额显示为 ***)',
|
|
577
578
|
sessionRankSort: '排序',
|
|
578
579
|
sessionSortCostDesc: '费用 高→低',
|
|
579
580
|
sessionSortCostAsc: '费用 低→高',
|
|
@@ -923,6 +924,7 @@ window.__ModuleLoader__.load({
|
|
|
923
924
|
sessionRankError: 'Failed to load session ranking',
|
|
924
925
|
sessionRankLimit: 'Rows',
|
|
925
926
|
showSessionIdLabel: 'Show session IDs in session lists',
|
|
927
|
+
hideAmountsLabel: 'Hide amounts (privacy mode: balances and costs render as ***)',
|
|
926
928
|
sessionRankSort: 'Sort',
|
|
927
929
|
sessionSortCostDesc: 'Cost high→low',
|
|
928
930
|
sessionSortCostAsc: 'Cost low→high',
|
|
@@ -1156,6 +1158,10 @@ window.__ModuleLoader__.load({
|
|
|
1156
1158
|
locale: v.locale === 'zh' || v.locale === 'en' || v.locale === 'auto' ? v.locale : 'auto',
|
|
1157
1159
|
position: v.position === 'header' || v.position === 'off' ? v.position : 'dock',
|
|
1158
1160
|
sidebar: v.sidebar !== false,
|
|
1161
|
+
// showSessionId 曾遗漏于本白名单:checkbox 能保存但读侧恒 undefined,
|
|
1162
|
+
// 会话列表附显 ID 自上线以来实际从未生效(随 hideAmounts 一并修复)。
|
|
1163
|
+
showSessionId: v.showSessionId === true,
|
|
1164
|
+
hideAmounts: v.hideAmounts === true,
|
|
1159
1165
|
currency: typeof v.currency === 'string' ? v.currency : 'CNY',
|
|
1160
1166
|
symbol: typeof v.symbol === 'string' ? v.symbol : '¥',
|
|
1161
1167
|
decimals: needNum(v.decimals, path + '.decimals'),
|
|
@@ -1498,6 +1504,9 @@ window.__ModuleLoader__.load({
|
|
|
1498
1504
|
/** 已换算币种金额 → 显示字符串(符号 + 可调小数位)。 */
|
|
1499
1505
|
function formatMoneyValue(value, config) {
|
|
1500
1506
|
const symbol = typeof config?.symbol === 'string' && config.symbol.length > 0 ? config.symbol : '$'
|
|
1507
|
+
// 隐私模式(issues #45/#46):全部金额统一遮罩为「符号 ***」——保留币种符号与布局,
|
|
1508
|
+
// 数字不泄露;会话费用/今日费用/历史/余额/预算等所有调用方自动生效。
|
|
1509
|
+
if (config?.hideAmounts === true) return symbol + '***'
|
|
1501
1510
|
const decimals = Math.max(0, Math.min(10, Math.floor(Number(config?.decimals) || 2)))
|
|
1502
1511
|
let effective = decimals
|
|
1503
1512
|
if (value > 0 && value < Math.pow(10, -decimals)) effective = decimals + 2
|
|
@@ -1762,8 +1771,8 @@ window.__ModuleLoader__.load({
|
|
|
1762
1771
|
// ── 侧边栏:余额行 + 预算图框/今日徽章(纵向堆叠,位于设置按钮上方) ──────
|
|
1763
1772
|
|
|
1764
1773
|
function formatBalanceMoney(value, config) {
|
|
1765
|
-
// 余额是官方接口返回的记账币种金额(如 CNY)
|
|
1766
|
-
return formatMoneyValue(value, { symbol: config.symbol, decimals: Math.max(2, Math.min(10, Math.floor(Number(config.decimals) || 2))) })
|
|
1774
|
+
// 余额是官方接口返回的记账币种金额(如 CNY),不经过汇率换算;隐私模式透传给 formatMoneyValue。
|
|
1775
|
+
return formatMoneyValue(value, { symbol: config.symbol, decimals: Math.max(2, Math.min(10, Math.floor(Number(config.decimals) || 2))), hideAmounts: config.hideAmounts })
|
|
1767
1776
|
}
|
|
1768
1777
|
|
|
1769
1778
|
function resolveBalanceCap(config, custom) {
|
|
@@ -1966,6 +1975,8 @@ window.__ModuleLoader__.load({
|
|
|
1966
1975
|
const unit = customBalanceUnitOf(config, custom)
|
|
1967
1976
|
const decimals = Math.max(2, Math.min(6, Math.floor(Number(config?.decimals) || 4)))
|
|
1968
1977
|
const symbol = unit === 'CNY' ? '¥' : unit === 'EUR' ? '€' : '$'
|
|
1978
|
+
// 隐私模式(issues #45/#46):自定义 Provider 余额同口径遮罩。
|
|
1979
|
+
if (config?.hideAmounts === true) return symbol + '***'
|
|
1969
1980
|
const value = Number(amount)
|
|
1970
1981
|
if (!Number.isFinite(value)) return '—'
|
|
1971
1982
|
let fixed = value.toFixed(decimals)
|
|
@@ -4661,6 +4672,14 @@ window.__ModuleLoader__.load({
|
|
|
4661
4672
|
el('div', { className: 'cm-field' },
|
|
4662
4673
|
el('label', null, t('decimalsLabel')),
|
|
4663
4674
|
numInput({ value: draft?.decimals ?? 2 }, v => setField('decimals', Math.min(10, Math.floor(v))))),
|
|
4675
|
+
// 隐私模式(issues #45/#46):全部余额/费用金额遮罩为 ***,共享屏幕/截图防泄露。
|
|
4676
|
+
el('label', { className: 'cm-check' },
|
|
4677
|
+
el('input', {
|
|
4678
|
+
type: 'checkbox',
|
|
4679
|
+
checked: draft?.hideAmounts === true,
|
|
4680
|
+
onChange: event => setField('hideAmounts', event.target.checked),
|
|
4681
|
+
}),
|
|
4682
|
+
el('span', null, t('hideAmountsLabel'))),
|
|
4664
4683
|
el('div', { className: 'cm-grid-group' }, t('groupSidebar')),
|
|
4665
4684
|
el('div', { className: 'cm-field' },
|
|
4666
4685
|
el('label', null, t('balanceDisplayLabel')),
|
package/lib/index.js
CHANGED
|
@@ -19,7 +19,8 @@ import { join } from 'node:path'
|
|
|
19
19
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
20
20
|
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
|
|
21
21
|
import { Ledger, applyConfigPatch, localDayKey, pickBalanceInfo, reconcileBalanceDelta, zeroDay } from './store.js'
|
|
22
|
-
import { backfillLegacyLedger, importLegacyHistory, repairForkSeed } from './backfill.js'
|
|
22
|
+
import { backfillLegacyLedger, importLegacyHistory, repairForkSeed, repairProviderDupes } from './backfill.js'
|
|
23
|
+
import { createLlmStreamBilling } from './billing-stream.js'
|
|
23
24
|
import { OFFICIAL_PRICING_URL, normalizePrice, parsePricingHtml, costOf, priceEntryFor, providerPriceEntryFor, buildPriceCatalog } from './pricing.js'
|
|
24
25
|
import { CODING_PLAN_PROVIDERS, CODING_PLAN_PROVIDER_IDS, queryCodingPlan, scnetTokenPlanWindows, emptyCustomBalance, queryCustomBalance } from './coding-plans.js'
|
|
25
26
|
import { fetchWithRetry } from './net.js'
|
|
@@ -698,7 +699,8 @@ function createService(ctx, ledger) {
|
|
|
698
699
|
// issue #36:Coding Plan / 自定义 Provider 的费用不动官方余额,对账只统计 deepseek 渠道,否则订阅用户会恒报 drift。
|
|
699
700
|
if ((ledger.config?.balance?.reconcile ?? true) === true && balanceCache.value.status === 'ok') {
|
|
700
701
|
const nowMs = Date.now()
|
|
701
|
-
|
|
702
|
+
// 隐私模式(issues #45/#46):对账提示中的金额同步遮罩,避免悬浮提示泄露。
|
|
703
|
+
const usd = v => ledger.config?.hideAmounts === true ? '$***' : '$' + Number(v).toFixed(4)
|
|
702
704
|
const { ref, event } = reconcileBalanceDelta(ledger.balanceRef, balanceCache.value, ledger.todayOfficialCost(), localDayKey(nowMs), nowMs)
|
|
703
705
|
if (ref !== ledger.balanceRef) {
|
|
704
706
|
ledger.balanceRef = ref
|
|
@@ -1155,6 +1157,18 @@ export async function runStartupImports(ledger, sessionsRoot) {
|
|
|
1155
1157
|
console.log(`[dsh-cost-meter] fork 会话重复计费清洗完成:扣除 ${repaired.sessions} 个会话 / ${repaired.days} 天的种子污染(扫描 ${repaired.scanned} 份会话日志)`)
|
|
1156
1158
|
}
|
|
1157
1159
|
}
|
|
1160
|
+
// 包装路由重复计费一次性清洗(issue #48):旧版在 llm/stream 瀑布每层都
|
|
1161
|
+
// 记账,modlens/vision-router 嵌套链把同一次请求记成 official/modlens/
|
|
1162
|
+
// modlens-vision 三份(byProviderModel 六值指纹逐位相同)。嵌套去重
|
|
1163
|
+
// (billing-stream ALS 标记)已堵住新污染,此处扣除历史存量重复条目。
|
|
1164
|
+
if (!ledger.migrations.includes('provider-dedup-v1')) {
|
|
1165
|
+
const repaired = repairProviderDupes(ledger)
|
|
1166
|
+
ledger.migrations.push('provider-dedup-v1')
|
|
1167
|
+
ledger.scheduleWrite()
|
|
1168
|
+
if (repaired.groups > 0) {
|
|
1169
|
+
console.log(`[dsh-cost-meter] 包装路由重复计费清洗完成:合并 ${repaired.groups} 组重复条目,扣除 ${repaired.removedCost.toFixed(4)} USD`)
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1158
1172
|
if (!(Number(ledger.config?.legacyAutoImportedAt) > 0)) {
|
|
1159
1173
|
const stats = await importLegacyHistory(ledger, sessionsRoot)
|
|
1160
1174
|
if (stats.days > 0 || stats.sessions > 0) {
|
|
@@ -1188,34 +1202,21 @@ export function apply(ctx) {
|
|
|
1188
1202
|
|
|
1189
1203
|
// 包裹 llm/stream:捕获 usage 块(位于 finish 之前),按官方价格计入账本。
|
|
1190
1204
|
// 本插件是链尾监听者,next() 即适配器流;仅透传数据块,不改变流协议。
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
input: usage.inputTokens ?? 0,
|
|
1207
|
-
output: usage.outputTokens ?? 0,
|
|
1208
|
-
cacheRead: usage.cacheReadTokens ?? 0,
|
|
1209
|
-
cacheWrite: usage.cacheWriteTokens ?? 0,
|
|
1210
|
-
reasoning: usage.reasoningTokens ?? 0,
|
|
1211
|
-
}, options?.model, options?.sessionId, Date.now(), options?.provider)
|
|
1212
|
-
} catch (error) {
|
|
1213
|
-
ctx.logger?.warn?.(`[dsh-cost-meter] 计费失败: ${String(error)}`)
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
})()
|
|
1218
|
-
})
|
|
1205
|
+
// 嵌套去重(issue #48):modlens/vision-router 等包装路由在自身 stream()
|
|
1206
|
+
// 体内再发起 ctx.llm.stream(),旧实现在瀑布每层都记账(同请求 ×2~3);
|
|
1207
|
+
// createLlmStreamBilling 用 AsyncLocalStorage 深度标记识别嵌套调用,
|
|
1208
|
+
// 只由最外层记一次。历史污染由启动期 provider-dedup-v1 清洗兜底。
|
|
1209
|
+
ctx.on('llm/stream', createLlmStreamBilling({
|
|
1210
|
+
account: (usage, model, sessionId, atMs, provider) => {
|
|
1211
|
+
ledger.account({
|
|
1212
|
+
input: usage.inputTokens ?? 0,
|
|
1213
|
+
output: usage.outputTokens ?? 0,
|
|
1214
|
+
cacheRead: usage.cacheReadTokens ?? 0,
|
|
1215
|
+
cacheWrite: usage.cacheWriteTokens ?? 0,
|
|
1216
|
+
reasoning: usage.reasoningTokens ?? 0,
|
|
1217
|
+
}, model, sessionId, atMs, provider)
|
|
1218
|
+
},
|
|
1219
|
+
}))
|
|
1219
1220
|
|
|
1220
1221
|
// costUsage 投影:向会话历史页/推送帧提供 token 桶(客户端计价)。
|
|
1221
1222
|
ctx.inject(['sessionProjections'], (projectionCtx) => {
|
package/lib/store.js
CHANGED
|
@@ -44,6 +44,7 @@ export function defaultConfig() {
|
|
|
44
44
|
peakAlertPosition: 'corner', // 弹窗位置:corner(右下角) | center(屏幕中心)
|
|
45
45
|
peakAlertWebNotify: false, // 弹窗时额外发浏览器(系统)通知,需用户在地址栏授权
|
|
46
46
|
showSessionId: false, // 会话列表中附显会话 ID(默认只显示标题,需要时开启)
|
|
47
|
+
hideAmounts: false, // 隐藏金额(隐私模式,issues #45/#46):全部余额/费用金额显示为 ***,共享屏幕或截图不泄露
|
|
47
48
|
legacyAutoImportedAt: 0, // 安装前历史自动导入标记(issue #27):完成时刻 ms;0 = 尚未跑过
|
|
48
49
|
peakStyle: 'compact', // 峰谷时段条样式:compact(简洁单行/竖向同构) | classic(经典分段/胶囊芯片)
|
|
49
50
|
priceMatch: 'auto', // 未知模型名自动匹配价格表:auto(去后缀/前缀/家族相似) | exact(仅精确)
|
|
@@ -266,6 +267,7 @@ const VALIDATION_MESSAGES = {
|
|
|
266
267
|
peakAlertPosition: 'peakAlertPosition 必须是 corner / center',
|
|
267
268
|
peakAlertWebNotify: 'peakAlertWebNotify 必须是布尔值',
|
|
268
269
|
showSessionId: 'showSessionId 必须是布尔值',
|
|
270
|
+
hideAmounts: 'hideAmounts 必须是布尔值',
|
|
269
271
|
peakStyle: 'peakStyle 必须是 compact / classic',
|
|
270
272
|
priceMatch: 'priceMatch 必须是 auto / exact',
|
|
271
273
|
priceOverrides: 'priceOverrides 必须是字符串→字符串映射',
|
|
@@ -335,6 +337,7 @@ const VALIDATION_MESSAGES = {
|
|
|
335
337
|
peakAlertPosition: 'peakAlertPosition must be corner / center',
|
|
336
338
|
peakAlertWebNotify: 'peakAlertWebNotify must be a boolean',
|
|
337
339
|
showSessionId: 'showSessionId must be a boolean',
|
|
340
|
+
hideAmounts: 'hideAmounts must be a boolean',
|
|
338
341
|
peakStyle: 'peakStyle must be compact / classic',
|
|
339
342
|
priceMatch: 'priceMatch must be auto / exact',
|
|
340
343
|
priceOverrides: 'priceOverrides must be a string→string map',
|
|
@@ -468,6 +471,8 @@ export function applyConfigPatch(current, patch) {
|
|
|
468
471
|
if (!['corner', 'center'].includes(candidate.peakAlertPosition)) errors.push(vmsg(locale, 'peakAlertPosition'))
|
|
469
472
|
if (typeof candidate.peakAlertWebNotify !== 'boolean') errors.push(vmsg(locale, 'peakAlertWebNotify'))
|
|
470
473
|
if (candidate.showSessionId !== undefined && typeof candidate.showSessionId !== 'boolean') errors.push(vmsg(locale, 'showSessionId'))
|
|
474
|
+
// 隐藏金额(隐私模式,issues #45/#46):布尔开关,可缺省(默认关)。
|
|
475
|
+
if (candidate.hideAmounts !== undefined && typeof candidate.hideAmounts !== 'boolean') errors.push(vmsg(locale, 'hideAmounts'))
|
|
471
476
|
if (candidate.peakStyle !== 'compact' && candidate.peakStyle !== 'classic') errors.push(vmsg(locale, 'peakStyle'))
|
|
472
477
|
if (candidate.priceMatch !== 'auto' && candidate.priceMatch !== 'exact') errors.push(vmsg(locale, 'priceMatch'))
|
|
473
478
|
const overrides = candidate.priceOverrides
|
|
@@ -661,6 +666,7 @@ export function sanitizeConfig(raw) {
|
|
|
661
666
|
out.decimals = Math.max(0, Math.min(10, Math.floor(Number(out.decimals) || 0)))
|
|
662
667
|
out.historyDays = Math.max(7, Math.min(3650, Math.floor(Number(out.historyDays) || 180)))
|
|
663
668
|
out.showSessionId = out.showSessionId === true
|
|
669
|
+
out.hideAmounts = out.hideAmounts === true
|
|
664
670
|
// 峰/谷切换弹窗提醒:非法值定向收敛(开关默认开、提前量回 2、类型回 both)。
|
|
665
671
|
out.peakAlertEnabled = out.peakAlertEnabled !== false
|
|
666
672
|
const alertAhead = Number(out.peakAlertAhead)
|
package/lib/typert.host.js
CHANGED
|
@@ -110,6 +110,8 @@ const configSchema = z.object({
|
|
|
110
110
|
peakAlertPosition: z.enum(['corner', 'center']).optional(),
|
|
111
111
|
peakAlertWebNotify: z.boolean().optional(),
|
|
112
112
|
showSessionId: z.boolean().optional(),
|
|
113
|
+
// 隐藏金额(隐私模式,issues #45/#46):全部余额/费用金额遮罩为 ***。
|
|
114
|
+
hideAmounts: z.boolean().optional(),
|
|
113
115
|
// 安装前历史自动导入完成时刻(issue #27,内部标记;0/缺席 = 尚未跑过)。
|
|
114
116
|
legacyAutoImportedAt: num.optional(),
|
|
115
117
|
peakStyle: z.enum(['compact', 'classic']).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.38",
|
|
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",
|