dsh-cost-meter 1.5.21 → 1.5.23
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/client.js +116 -55
- package/lib/coding-plans.js +5 -3
- package/lib/custom-balance.js +4 -2
- package/lib/index.js +11 -10
- package/lib/net.js +57 -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 / SCNet)
|
|
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
|
| UI language | Settings → Display settings | Simplified Chinese / English / Follow browser (auto); switches instantly and auto-saves |
|
|
46
46
|
| AI price sync | [prompt](docs/AI-PRICE-SYNC-PROMPT.en.md) | DeepSeek official sync; other providers use the verified official price catalog and manual configuration |
|
|
47
47
|
| Model & Plan adaptation guide | [adaptation doc](docs/model-and-plan-adaptation.en.md) | Adaptation matrix for per-model billing and the 7 Coding Plan vendors, the auto-matching mechanism and price sources ([中文](docs/model-and-plan-adaptation.md)) |
|
|
48
|
+
| 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)) |
|
|
48
49
|
| Multi-provider billing | Settings / ledger | OpenAI, Anthropic, Google Gemini, Mistral and other providers with input/output, cache and reasoning-token pricing isolated by provider + model |
|
|
49
50
|
| Model-name auto-matching | Settings / ledger | Unknown model ids are matched against the price table: case/spaces/hyphens/dots and bracket annotations (e.g. (go)) are ignored — a normalized-equal or containing name hits (e.g. `gpt5.6 luna(go)`); router providers (opencode/zen etc.) search across all vendors; can be restricted to exact match, and unmatched models can be pinned to a specific entry in Settings |
|
|
50
51
|
| Extended price catalog | Settings → Extended price catalog | Built-in reference catalog grouped by vendor and model family (expandable; vendors collapsed by default); mount entries into billing with one click — mounted third-party models live inside the catalog and stay editable; a per-model “Show directly in Cost settings” toggle chooses which models (DeepSeek included) appear directly in the price table |
|
|
@@ -193,22 +194,22 @@ Real captures from an actual DSH sidebar of the period strip and collapsed verti
|
|
|
193
194
|
dsh plugin --profile web add dsh-cost-meter
|
|
194
195
|
```
|
|
195
196
|
|
|
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.
|
|
197
|
+
**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.23`** — review the script before running):
|
|
197
198
|
|
|
198
199
|
```powershell
|
|
199
|
-
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.
|
|
200
|
+
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.23/install.ps1 | iex
|
|
200
201
|
```
|
|
201
202
|
|
|
202
203
|
**Or a plain command line** (the machine must already have pnpm and git; also pinned to the tag):
|
|
203
204
|
|
|
204
205
|
```sh
|
|
205
|
-
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.
|
|
206
|
+
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.23
|
|
206
207
|
```
|
|
207
208
|
|
|
208
209
|
Without git, use the GitHub tag archive:
|
|
209
210
|
|
|
210
211
|
```sh
|
|
211
|
-
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.
|
|
212
|
+
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.23.tar.gz
|
|
212
213
|
```
|
|
213
214
|
|
|
214
215
|
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 / SCNet 七家)
|
|
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
|
| 界面语言 | 设置页 → 显示设置 | 简体中文 / English / 跟随浏览器(自动);切换即时生效并自动保存 |
|
|
47
47
|
| AI 价格同步 | [提示词](docs/AI-PRICE-SYNC-PROMPT.md) | DeepSeek 官方同步;其他 provider 使用已核对的官方价格目录与手动配置 |
|
|
48
48
|
| 模型与 Plan 适配说明 | [适配文档](docs/model-and-plan-adaptation.md) | 各厂商模型计费与 7 家 Coding Plan 的适配矩阵、自动匹配机制与价格来源([English](docs/model-and-plan-adaptation.en.md)) |
|
|
49
|
+
| 峰/谷切换提醒图解 | [提醒文档](docs/peak-alert.md) | 峰谷切换前弹窗与系统通知的完整图解:效果截图(中/英)、设置项说明与使用建议([English](docs/peak-alert.en.md)) |
|
|
49
50
|
| 多 provider 计费 | 设置页 / 账本 | 支持 OpenAI、Anthropic、Google Gemini、Mistral 等 provider 的 input/output、缓存与 reasoning token 价格,按 provider+model 隔离计费 |
|
|
50
51
|
| 模型名自动匹配 | 设置页 / 账本 | 未知模型 id 自动匹配价格表:忽略大小写/空格/横杠/点号与括号附注,归一化等价或请求名包含表内模型名即命中(如 `gpt5.6 luna(go)`);路由 provider(opencode/zen 等)下跨厂商全库查找;可关闭为仅精确;未命中模型可手动指定计费条目 |
|
|
51
52
|
| 拓展价格表 | 设置页 → 拓展价格表 | 内置各厂商、按模型家族分类的参考价格目录(点开展开,厂商默认折叠);一键挂载参与计费,挂载的第三方模型默认收入表内可编辑;逐模型「在费用设置直接显示」开关自选哪些模型(含 DeepSeek)在「价格表」区直接显示 |
|
|
@@ -195,22 +196,22 @@
|
|
|
195
196
|
dsh plugin --profile web add dsh-cost-meter
|
|
196
197
|
```
|
|
197
198
|
|
|
198
|
-
**PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.5.
|
|
199
|
+
**PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.5.23`**,建议先下载审阅再运行):
|
|
199
200
|
|
|
200
201
|
```powershell
|
|
201
|
-
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.
|
|
202
|
+
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.23/install.ps1 | iex
|
|
202
203
|
```
|
|
203
204
|
|
|
204
205
|
**或直接命令行**(机器上需已有 pnpm 与 git;同样固定到 tag):
|
|
205
206
|
|
|
206
207
|
```sh
|
|
207
|
-
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.
|
|
208
|
+
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.23
|
|
208
209
|
```
|
|
209
210
|
|
|
210
211
|
没有 git 时可用 GitHub tag 打包直链:
|
|
211
212
|
|
|
212
213
|
```sh
|
|
213
|
-
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.
|
|
214
|
+
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.23.tar.gz
|
|
214
215
|
```
|
|
215
216
|
|
|
216
217
|
安装后**重启** `dsh web`(插件行、Typert 清单与客户端 bundle 均在启动时扫描):
|
package/lib/client.js
CHANGED
|
@@ -71,6 +71,13 @@ window.__ModuleLoader__.load({
|
|
|
71
71
|
'.cm-toolbar .cm-field{flex-direction:row;align-items:center;gap:8px}',
|
|
72
72
|
'.cm-toolbar .cm-field label{margin:0;font-size:12px;color:var(--dsw-alias-label-tertiary)}',
|
|
73
73
|
'.cm-toolbar .cm-input{min-width:150px;padding:4px 10px}',
|
|
74
|
+
// 设置页标签栏(issue #29):左侧分组标签,右侧常驻自动保存状态。
|
|
75
|
+
'.cm-tabs-row{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;border-bottom:1px solid var(--dsh-alias-border-l1);padding-bottom:10px}',
|
|
76
|
+
'.cm-tabs{display:flex;align-items:center;gap:4px;flex-wrap:wrap}',
|
|
77
|
+
'.cm-tab{font:inherit;font-size:13px;color:var(--dsw-alias-label-secondary);background:transparent;border:none;border-radius:8px;padding:6px 14px;cursor:pointer;white-space:nowrap}',
|
|
78
|
+
'.cm-tab:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
79
|
+
'.cm-tab.active{background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary-inverted);font-weight:600}',
|
|
80
|
+
'.cm-tabs-row .cm-msg{padding:2px 10px}',
|
|
74
81
|
'.cm-note{font-size:12px;color:var(--dsw-alias-label-tertiary);margin:0}',
|
|
75
82
|
'.cm-table{width:100%;border-collapse:collapse;font-size:12px}',
|
|
76
83
|
'.cm-table th,.cm-table td{text-align:left;padding:7px 10px;border-bottom:1px solid var(--dsw-alias-border-l1);white-space:nowrap}',
|
|
@@ -556,6 +563,13 @@ window.__ModuleLoader__.load({
|
|
|
556
563
|
usagePositionGeneral: '通用设置',
|
|
557
564
|
usagePositionSection: '独立分节(用量)',
|
|
558
565
|
displaySettings: '显示设置',
|
|
566
|
+
// 设置页标签(issue #29)
|
|
567
|
+
tabOverview: '概览',
|
|
568
|
+
tabQuotas: '额度',
|
|
569
|
+
tabUsage: '用量',
|
|
570
|
+
tabPricing: '价格',
|
|
571
|
+
tabDisplay: '显示',
|
|
572
|
+
historyDataTitle: '历史数据',
|
|
559
573
|
positionLabel: '会话费用显示位置',
|
|
560
574
|
positionDock: '输入区下方',
|
|
561
575
|
positionHeader: '会话标题栏',
|
|
@@ -880,6 +894,13 @@ window.__ModuleLoader__.load({
|
|
|
880
894
|
usagePositionGeneral: 'General settings',
|
|
881
895
|
usagePositionSection: 'Own section (Usage)',
|
|
882
896
|
displaySettings: 'Display settings',
|
|
897
|
+
// Settings tabs (issue #29)
|
|
898
|
+
tabOverview: 'Overview',
|
|
899
|
+
tabQuotas: 'Quotas',
|
|
900
|
+
tabUsage: 'Usage',
|
|
901
|
+
tabPricing: 'Pricing',
|
|
902
|
+
tabDisplay: 'Display',
|
|
903
|
+
historyDataTitle: 'History data',
|
|
883
904
|
positionLabel: 'Session cost display position',
|
|
884
905
|
positionDock: 'Below the composer',
|
|
885
906
|
positionHeader: 'Session title bar',
|
|
@@ -3892,6 +3913,8 @@ window.__ModuleLoader__.load({
|
|
|
3892
3913
|
const [busy, setBusy] = useState(false)
|
|
3893
3914
|
// 价格表折叠开关(默认收起,保持设置页简洁;三角按钮展开)。
|
|
3894
3915
|
const [priceOpen, setPriceOpen] = useState(false)
|
|
3916
|
+
// 设置页当前标签(issue #29):概览/额度/用量/价格/显示;仅切可见分区,不拆配置与保存逻辑。
|
|
3917
|
+
const [tab, setTab] = useState('overview')
|
|
3895
3918
|
// 自动保存状态:idle(无改动) | saving | saved | error。
|
|
3896
3919
|
const [saveState, setSaveState] = useState({ status: 'idle', at: 0, error: null })
|
|
3897
3920
|
const savedRef = React.useRef(null)
|
|
@@ -4028,39 +4051,34 @@ window.__ModuleLoader__.load({
|
|
|
4028
4051
|
isDefault: false, draft, setDraft, t,
|
|
4029
4052
|
})
|
|
4030
4053
|
))
|
|
4054
|
+
// 设置页标签(issue #29):按用途分五组,切换只改可见分区,不拆配置模型与保存逻辑。
|
|
4055
|
+
const tabItems = [
|
|
4056
|
+
['overview', t('tabOverview')],
|
|
4057
|
+
['quotas', t('tabQuotas')],
|
|
4058
|
+
['usage', t('tabUsage')],
|
|
4059
|
+
['pricing', t('tabPricing')],
|
|
4060
|
+
['display', t('tabDisplay')],
|
|
4061
|
+
]
|
|
4062
|
+
// 自动保存状态常驻标签栏右侧:在任意标签页改动配置都能看到保存反馈。
|
|
4063
|
+
const saveBadge = saveState.status === 'saving'
|
|
4064
|
+
? el('span', { className: 'cm-hint' }, t('saving'))
|
|
4065
|
+
: saveState.status === 'error'
|
|
4066
|
+
? el('span', { className: 'cm-msg err' }, t('autoSaveFailed', { message: saveState.error ?? '' }))
|
|
4067
|
+
: el('span', { className: 'cm-hint' }, saveState.status === 'saved' ? t('autoSavedAt', { time: new Date(saveState.at).toLocaleTimeString() }) : t('autoSaveHint'))
|
|
4031
4068
|
return el('div', { className: 'cm-section' },
|
|
4032
|
-
//
|
|
4033
|
-
el('div', { className: 'cm-
|
|
4034
|
-
el('div', { className: 'cm-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
className: 'cm-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
(!USAGE_POSITION_SWITCHABLE || (config.usage?.position ?? 'cost') === 'cost')
|
|
4046
|
-
? el(UsagePanel, { state, t, locale })
|
|
4047
|
-
: null,
|
|
4048
|
-
// 按模型统计(紧随用量统计;今日/近90天:费用、token、缓存命中率、性价比)
|
|
4049
|
-
el(ModelStatsPanel, { state, config: draft ?? config, t }),
|
|
4050
|
-
// OpenCode Go 订阅额度(含启用开关,像预算面板一样常驻)
|
|
4051
|
-
el(GoQuotaPanel, { state, api, t, draft, setDraft }),
|
|
4052
|
-
// Coding Plan 额度(Anthropic / Z.ai·GLM / MiniMax,各家独立开关)
|
|
4053
|
-
el(CodingPlansPanel, { state, api, t, draft, setDraft }),
|
|
4054
|
-
// 自定义 Provider 余额(可配置 HTTP 查询;与 Coding Plan 同区,可折叠)
|
|
4055
|
-
el(CustomBalancePanel, { state, api, t, draft, setDraft }),
|
|
4056
|
-
// 预算(紧随其后)
|
|
4057
|
-
el(BudgetPanel, { state, draft, setDraft, t }),
|
|
4058
|
-
// 峰谷计价与提示(独立面板:启用/提示开关、样式切换、时段条预览)
|
|
4059
|
-
el(PeakPanel, { state, draft, setDraft, t }),
|
|
4060
|
-
// 官方余额(按显示配置)
|
|
4061
|
-
(config.balance?.display === 'settings' || config.balance?.display === 'both')
|
|
4062
|
-
? el(BalancePanel, { state, api, t, draft, setDraft })
|
|
4063
|
-
: null,
|
|
4069
|
+
// 标签栏:概览/额度/用量/价格/显示;右侧为全局自动保存状态。
|
|
4070
|
+
el('div', { className: 'cm-tabs-row' },
|
|
4071
|
+
el('div', { className: 'cm-tabs', role: 'tablist' },
|
|
4072
|
+
tabItems.map(([id, label]) => el('button', {
|
|
4073
|
+
key: id, type: 'button', role: 'tab', 'aria-selected': String(tab === id),
|
|
4074
|
+
className: 'cm-tab' + (tab === id ? ' active' : ''),
|
|
4075
|
+
onClick: () => setTab(id),
|
|
4076
|
+
}, label))),
|
|
4077
|
+
saveBadge),
|
|
4078
|
+
// 操作结果提示(价格同步/历史导入/清除):全局展示,不随触发按钮所在标签页。
|
|
4079
|
+
message !== null ? el('div', { className: 'cm-msg ' + message.kind }, message.text) : null,
|
|
4080
|
+
// ── 概览:汇总卡片、今日会话、预算、官方余额 ──
|
|
4081
|
+
tab === 'overview' ? el(Fragment, { key: 'overview' },
|
|
4064
4082
|
// 汇总卡片
|
|
4065
4083
|
el('div', { className: 'cm-cards' },
|
|
4066
4084
|
el(Card, {
|
|
@@ -4092,10 +4110,66 @@ window.__ModuleLoader__.load({
|
|
|
4092
4110
|
el('div', null,
|
|
4093
4111
|
el('h3', { className: 'cm-h' }, t('todaySessions')),
|
|
4094
4112
|
el(TodaySessions, { state, t })),
|
|
4113
|
+
// 预算
|
|
4114
|
+
el(BudgetPanel, { state, draft, setDraft, t }),
|
|
4115
|
+
// 官方余额(按显示配置)
|
|
4116
|
+
(config.balance?.display === 'settings' || config.balance?.display === 'both')
|
|
4117
|
+
? el(BalancePanel, { state, api, t, draft, setDraft })
|
|
4118
|
+
: null)
|
|
4119
|
+
: null,
|
|
4120
|
+
// ── 额度:OpenCode Go、Coding Plan、自定义 Provider 余额 ──
|
|
4121
|
+
tab === 'quotas' ? el(Fragment, { key: 'quotas' },
|
|
4122
|
+
// OpenCode Go 订阅额度(含启用开关,像预算面板一样常驻)
|
|
4123
|
+
el(GoQuotaPanel, { state, api, t, draft, setDraft }),
|
|
4124
|
+
// Coding Plan 额度(Anthropic / Z.ai·GLM / MiniMax,各家独立开关)
|
|
4125
|
+
el(CodingPlansPanel, { state, api, t, draft, setDraft }),
|
|
4126
|
+
// 自定义 Provider 余额(可配置 HTTP 查询;与 Coding Plan 同区,可折叠)
|
|
4127
|
+
el(CustomBalancePanel, { state, api, t, draft, setDraft }))
|
|
4128
|
+
: null,
|
|
4129
|
+
// ── 用量:热图、按模型统计、历史、按会话统计、历史数据操作 ──
|
|
4130
|
+
tab === 'usage' ? el(Fragment, { key: 'usage' },
|
|
4131
|
+
// Token 用量统计(position=cost 时留在费用分节;移至通用/独立分节后不在此渲染)
|
|
4132
|
+
(!USAGE_POSITION_SWITCHABLE || (config.usage?.position ?? 'cost') === 'cost')
|
|
4133
|
+
? el(UsagePanel, { state, t, locale })
|
|
4134
|
+
: null,
|
|
4135
|
+
// 按模型统计(今日/近90天:费用、token、缓存命中率、性价比)
|
|
4136
|
+
el(ModelStatsPanel, { state, config: draft ?? config, t }),
|
|
4095
4137
|
// 历史(三角折叠面板;日期行可再展开会话明细)
|
|
4096
4138
|
el(HistoryPanel, { state, api }),
|
|
4097
4139
|
// 按会话统计(全部历史,不分日期;issue #22)
|
|
4098
4140
|
el(SessionRankPanel, { state, api }),
|
|
4141
|
+
// 历史数据操作(导入安装前历史/清除全部历史;与用量统计同组)
|
|
4142
|
+
el('div', null,
|
|
4143
|
+
el('h3', { className: 'cm-h' }, t('historyDataTitle')),
|
|
4144
|
+
el('div', { className: 'cm-buttons' },
|
|
4145
|
+
confirmImport
|
|
4146
|
+
? el(Fragment, null,
|
|
4147
|
+
el('span', { className: 'cm-hint' }, t('confirmImportLegacy')),
|
|
4148
|
+
el('button', { className: 'cm-btn', onClick: doImportLegacy, disabled: busy }, t('apply')),
|
|
4149
|
+
el('button', { className: 'cm-btn', onClick: () => setConfirmImport(false) }, t('cancel')))
|
|
4150
|
+
: el('button', { className: 'cm-btn', onClick: () => setConfirmImport(true), disabled: busy }, t('importLegacy')),
|
|
4151
|
+
confirmReset
|
|
4152
|
+
? el(Fragment, null,
|
|
4153
|
+
el('span', { className: 'cm-hint' }, t('confirmReset')),
|
|
4154
|
+
el('button', { className: 'cm-btn danger', onClick: doReset, disabled: busy }, t('confirmClear')),
|
|
4155
|
+
el('button', { className: 'cm-btn', onClick: () => setConfirmReset(false) }, t('cancel')))
|
|
4156
|
+
: el('button', { className: 'cm-btn danger', onClick: () => setConfirmReset(true), disabled: busy }, t('clearAllHistory'))),
|
|
4157
|
+
el('p', { className: 'cm-note' }, t('legacyImportNote')))
|
|
4158
|
+
: null,
|
|
4159
|
+
// ── 显示:界面语言、徽章/侧边栏位置、图框开关等 ──
|
|
4160
|
+
tab === 'display' ? el(Fragment, { key: 'display' },
|
|
4161
|
+
// 顶栏:界面语言
|
|
4162
|
+
el('div', { className: 'cm-toolbar' },
|
|
4163
|
+
el('div', { className: 'cm-field' },
|
|
4164
|
+
el('label', null, t('languageLabel')),
|
|
4165
|
+
el('select', {
|
|
4166
|
+
className: 'cm-input',
|
|
4167
|
+
value: draft?.locale ?? 'auto',
|
|
4168
|
+
onChange: event => setField('locale', event.target.value),
|
|
4169
|
+
},
|
|
4170
|
+
el('option', { value: 'auto' }, t('localeAuto')),
|
|
4171
|
+
el('option', { value: 'zh' }, t('localeZh')),
|
|
4172
|
+
el('option', { value: 'en' }, t('localeEn'))))),
|
|
4099
4173
|
// 显示设置
|
|
4100
4174
|
el('div', null,
|
|
4101
4175
|
el('h3', { className: 'cm-h' }, t('displaySettings')),
|
|
@@ -4323,7 +4397,12 @@ window.__ModuleLoader__.load({
|
|
|
4323
4397
|
},
|
|
4324
4398
|
}),
|
|
4325
4399
|
el('span', null, t('budgetDetailLabel'))))),
|
|
4326
|
-
el('p', { className: 'cm-note' }, t('badgeNote')))
|
|
4400
|
+
el('p', { className: 'cm-note' }, t('badgeNote'))))
|
|
4401
|
+
: null,
|
|
4402
|
+
// ── 价格:价格表、模型名匹配、拓展价格表、峰谷计价与提示、官方价格同步 ──
|
|
4403
|
+
tab === 'pricing' ? el(Fragment, { key: 'pricing' },
|
|
4404
|
+
// 峰谷计价与提示(独立面板:启用/提示开关、样式切换、时段条预览)
|
|
4405
|
+
el(PeakPanel, { state, draft, setDraft, t }),
|
|
4327
4406
|
// 价格表(可折叠,默认收起;priceTableDisplay 按模型门控:未勾选直接显示的模型收入拓展价格表,该开关只决定展示位置)
|
|
4328
4407
|
el('div', null,
|
|
4329
4408
|
el('button', { type: 'button', className: 'cm-collapse-h', 'aria-expanded': String(priceOpen), onClick: () => setPriceOpen(!priceOpen) },
|
|
@@ -4423,38 +4502,20 @@ window.__ModuleLoader__.load({
|
|
|
4423
4502
|
})(),
|
|
4424
4503
|
// 拓展价格表(厂商/家族分类目录;挂载 ↔ 费用设置价格表)
|
|
4425
4504
|
el(PriceCatalogPanel, { state, draft, setDraft, t }),
|
|
4426
|
-
//
|
|
4505
|
+
// 官方价格同步(与价格表同组;自动保存状态已移至标签栏常驻)
|
|
4427
4506
|
el('div', null,
|
|
4428
4507
|
el('h3', { className: 'cm-h' }, t('dataSync')),
|
|
4429
4508
|
el('div', { className: 'cm-buttons' },
|
|
4430
|
-
saveState.status === 'saving'
|
|
4431
|
-
? el('span', { className: 'cm-hint' }, t('saving'))
|
|
4432
|
-
: saveState.status === 'error'
|
|
4433
|
-
? el('span', { className: 'cm-msg err' }, t('autoSaveFailed', { message: saveState.error ?? '' }))
|
|
4434
|
-
: el('span', { className: 'cm-hint' }, saveState.status === 'saved' ? t('autoSavedAt', { time: new Date(saveState.at).toLocaleTimeString() }) : t('autoSaveHint')),
|
|
4435
4509
|
confirmFetch
|
|
4436
4510
|
? el(Fragment, null,
|
|
4437
4511
|
el('span', { className: 'cm-hint' }, t('confirmFetch')),
|
|
4438
4512
|
el('button', { className: 'cm-btn', onClick: doFetch, disabled: busy }, t('apply')),
|
|
4439
4513
|
el('button', { className: 'cm-btn', onClick: () => setConfirmFetch(false) }, t('cancel')))
|
|
4440
|
-
: el('button', { className: 'cm-btn', onClick: () => setConfirmFetch(true), disabled: busy }, t('syncFromDocs')),
|
|
4441
|
-
confirmImport
|
|
4442
|
-
? el(Fragment, null,
|
|
4443
|
-
el('span', { className: 'cm-hint' }, t('confirmImportLegacy')),
|
|
4444
|
-
el('button', { className: 'cm-btn', onClick: doImportLegacy, disabled: busy }, t('apply')),
|
|
4445
|
-
el('button', { className: 'cm-btn', onClick: () => setConfirmImport(false) }, t('cancel')))
|
|
4446
|
-
: el('button', { className: 'cm-btn', onClick: () => setConfirmImport(true), disabled: busy }, t('importLegacy')),
|
|
4447
|
-
confirmReset
|
|
4448
|
-
? el(Fragment, null,
|
|
4449
|
-
el('span', { className: 'cm-hint' }, t('confirmReset')),
|
|
4450
|
-
el('button', { className: 'cm-btn danger', onClick: doReset, disabled: busy }, t('confirmClear')),
|
|
4451
|
-
el('button', { className: 'cm-btn', onClick: () => setConfirmReset(false) }, t('cancel')))
|
|
4452
|
-
: el('button', { className: 'cm-btn danger', onClick: () => setConfirmReset(true), disabled: busy }, t('clearAllHistory'))),
|
|
4514
|
+
: el('button', { className: 'cm-btn', onClick: () => setConfirmFetch(true), disabled: busy }, t('syncFromDocs'))),
|
|
4453
4515
|
el('p', { className: 'cm-note' },
|
|
4454
4516
|
t('lastSync', { time: config.fetchedAt !== null ? new Date(config.fetchedAt).toLocaleString() : t('neverSynced') })
|
|
4455
|
-
+ t('source', { source: config.priceSource === 'official' ? t('sourceOfficial') : t('sourceBundled') }))
|
|
4456
|
-
|
|
4457
|
-
message !== null ? el('div', { className: 'cm-msg ' + message.kind }, message.text) : null))
|
|
4517
|
+
+ t('source', { source: config.priceSource === 'official' ? t('sourceOfficial') : t('sourceBundled') })))
|
|
4518
|
+
: null)
|
|
4458
4519
|
}
|
|
4459
4520
|
|
|
4460
4521
|
// ── 插件主体 ────────────────────────────────────────────────────────────
|
package/lib/coding-plans.js
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
* - 百炼 Coding Plan / OpenAI Codex / Gemini Code Assist / GitHub Copilot 个人版暂无 API-Key 化公开用量端点(仅控制台/组织级 API),不接入。
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import { fetchWithRetry } from './net.js'
|
|
25
|
+
|
|
24
26
|
export const CODING_PLAN_PROVIDERS = {
|
|
25
27
|
anthropic: {
|
|
26
28
|
label: 'Anthropic (Claude Pro/Max)',
|
|
@@ -526,13 +528,13 @@ export async function queryCodingPlan(provider, key, locale, t) {
|
|
|
526
528
|
for (const url of urls) {
|
|
527
529
|
let response
|
|
528
530
|
try {
|
|
529
|
-
|
|
531
|
+
// 瞬时网络错误先在单端点上重试(issue #28 同一封装),仍失败再换端点变体。
|
|
532
|
+
response = await fetchWithRetry(url, {
|
|
530
533
|
headers: {
|
|
531
534
|
authorization: `Bearer ${key.trim()}`,
|
|
532
535
|
'user-agent': 'dsh-cost-meter/1.4 (DeepSeek Harness plugin)',
|
|
533
536
|
},
|
|
534
|
-
|
|
535
|
-
})
|
|
537
|
+
}, { timeoutMs: 15000 })
|
|
536
538
|
} catch (error) {
|
|
537
539
|
lastError = error
|
|
538
540
|
continue // 网络错误:尝试下一个端点变体
|
package/lib/custom-balance.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* 与 coding-plans.js 固定端点 adapter 互补:共用 index.js 侧的 refresh/cache 模式。
|
|
4
4
|
*/
|
|
5
5
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
6
|
+
import { fetchWithRetry } from './net.js'
|
|
6
7
|
|
|
7
8
|
export const CUSTOM_BALANCE_ADAPTER_ID = 'custom'
|
|
8
9
|
|
|
@@ -102,14 +103,15 @@ export async function queryCustomBalance(ctx, config) {
|
|
|
102
103
|
}
|
|
103
104
|
const method = typeof request.method === 'string' ? request.method.toUpperCase() : 'GET'
|
|
104
105
|
const headers = await resolveHeaders(request.headers ?? {}, ctx)
|
|
105
|
-
const init = { method, headers
|
|
106
|
+
const init = { method, headers }
|
|
106
107
|
if (method !== 'GET' && method !== 'HEAD' && request.body !== undefined) {
|
|
107
108
|
init.body = typeof request.body === 'string' ? request.body : JSON.stringify(request.body)
|
|
108
109
|
if (!headers['content-type'] && !headers['Content-Type']) {
|
|
109
110
|
init.headers = { ...headers, 'content-type': 'application/json' }
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
|
-
|
|
113
|
+
// 瞬时网络错误自动重试(issue #28 同一封装;body 为字符串可安全重放)。
|
|
114
|
+
const response = await fetchWithRetry(request.url, init, { timeoutMs: 15000 })
|
|
113
115
|
if (!response.ok) {
|
|
114
116
|
throw new Error(`custom balance HTTP ${String(response.status)}`)
|
|
115
117
|
}
|
package/lib/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import { Ledger, applyConfigPatch, localDayKey, pickBalanceInfo, reconcileBalanc
|
|
|
22
22
|
import { backfillLegacyLedger, importLegacyHistory } from './backfill.js'
|
|
23
23
|
import { OFFICIAL_PRICING_URL, normalizePrice, parsePricingHtml, costOf, priceEntryFor, providerPriceEntryFor, buildPriceCatalog } from './pricing.js'
|
|
24
24
|
import { CODING_PLAN_PROVIDERS, CODING_PLAN_PROVIDER_IDS, queryCodingPlan, scnetTokenPlanWindows, emptyCustomBalance, queryCustomBalance } from './coding-plans.js'
|
|
25
|
+
import { fetchWithRetry } from './net.js'
|
|
25
26
|
import { stateSchema } from './typert.host.js'
|
|
26
27
|
|
|
27
28
|
export const name = 'cost-meter'
|
|
@@ -349,14 +350,15 @@ async function queryGoQuota(ctx, config, locale) {
|
|
|
349
350
|
error.soft = true // 未登录/未配置 Key 属预期场景,面板以中性提示展示
|
|
350
351
|
throw error
|
|
351
352
|
}
|
|
352
|
-
|
|
353
|
+
// Cloudflare 前置会间歇性重置连接(ECONNRESET → fetch failed):瞬时网络错误
|
|
354
|
+
// 自动重试,401/403 等业务状态仍走下方原语义(issue #28)。
|
|
355
|
+
const response = await fetchWithRetry(GO_QUOTA_URL, {
|
|
353
356
|
headers: {
|
|
354
357
|
authorization: `Bearer ${key}`,
|
|
355
358
|
// 浏览器 UA:避免被 opencode.ai 前置 Cloudflare 以 error 1010 拦截。
|
|
356
359
|
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
|
|
357
360
|
},
|
|
358
|
-
|
|
359
|
-
})
|
|
361
|
+
}, { timeoutMs: 15000 })
|
|
360
362
|
if (!response.ok) {
|
|
361
363
|
if (response.status === 401 || response.status === 403) {
|
|
362
364
|
const error = new Error(tmsg(locale, 'goQuotaNoSub', { code: String(response.status) }))
|
|
@@ -469,10 +471,10 @@ async function queryBalance(ctx, locale) {
|
|
|
469
471
|
if (endpoint === null) {
|
|
470
472
|
throw new Error(tmsg(locale, 'balanceEndpointNotOfficial', { url: String(baseURL ?? '') }))
|
|
471
473
|
}
|
|
472
|
-
|
|
474
|
+
// 瞬时网络错误自动重试(issue #28 同一封装);非 2xx 状态仍按业务错误处理。
|
|
475
|
+
const response = await fetchWithRetry(endpoint, {
|
|
473
476
|
headers: { authorization: `Bearer ${apiKey}` },
|
|
474
|
-
|
|
475
|
-
})
|
|
477
|
+
}, { timeoutMs: 15000 })
|
|
476
478
|
if (!response.ok) throw new Error(tmsg(locale, 'balanceHttp', { code: String(response.status) }))
|
|
477
479
|
const data = await response.json()
|
|
478
480
|
// 多币种账号返回 CNY/USD 两条且顺序不稳定(#24/#25):按余额与币种挑选,不固定取首条。
|
|
@@ -548,12 +550,11 @@ function buildState(ledger, balance = emptyBalance(), goQuota = emptyGoQuota(),
|
|
|
548
550
|
return state
|
|
549
551
|
}
|
|
550
552
|
|
|
551
|
-
/**
|
|
553
|
+
/** 带超时抓取官方定价页(瞬时网络错误自动重试,issue #28 同一封装)。 */
|
|
552
554
|
async function fetchPricingHtml(locale) {
|
|
553
|
-
const response = await
|
|
554
|
-
signal: AbortSignal.timeout(20000),
|
|
555
|
+
const response = await fetchWithRetry(OFFICIAL_PRICING_URL, {
|
|
555
556
|
headers: { 'user-agent': 'dsh-cost-meter/0.4 (DeepSeek Harness plugin)' },
|
|
556
|
-
})
|
|
557
|
+
}, { timeoutMs: 20000 })
|
|
557
558
|
if (!response.ok) throw new Error(`HTTP ${String(response.status)}`)
|
|
558
559
|
const text = await response.text()
|
|
559
560
|
if (text.length < 500) throw new Error(tmsg(locale, 'pageTooShort'))
|
package/lib/net.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 对外 HTTP 请求的统一重试封装(issue #28)。
|
|
3
|
+
* opencode.ai 等端点部署在 Cloudflare 之后会间歇性重置连接(ECONNRESET),
|
|
4
|
+
* Node fetch 统一抛为 `fetch failed`;对这类瞬时网络错误做少量重试即可消除
|
|
5
|
+
* 面板偶发报错,而 401/403 等业务错误重试无意义,仍由调用方原样处理。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** 视为瞬时的网络错误码(Node DNS/socket 层 + undici 内部码)。 */
|
|
9
|
+
const TRANSIENT_CODES = new Set([
|
|
10
|
+
'ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND', 'EHOSTUNREACH',
|
|
11
|
+
'ENETUNREACH', 'EPIPE', 'EAI_AGAIN',
|
|
12
|
+
'UND_ERR_SOCKET', 'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT',
|
|
13
|
+
'UND_ERR_BODY_TIMEOUT', 'UND_ERR_READ_ERROR', 'UND_ERR_ABORTED',
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 判定 fetch 抛出的错误是否为瞬时网络错误(值得重试)。
|
|
18
|
+
* @param {unknown} error - fetch reject 的错误。
|
|
19
|
+
*/
|
|
20
|
+
export function isTransientFetchError(error) {
|
|
21
|
+
if (typeof error !== 'object' || error === null) return false
|
|
22
|
+
// AbortSignal.timeout 触发的超时:DOMException TimeoutError(老版本 undici 为
|
|
23
|
+
// AbortError)。本插件对外请求只用超时信号、无手动取消,二者均可安全重试。
|
|
24
|
+
if (error.name === 'TimeoutError' || error.name === 'AbortError') return true
|
|
25
|
+
const code = error.cause?.code ?? error.code
|
|
26
|
+
// 有具体 code 时以白名单为准:证书过期(EPERM/CERT_*)等持久性错误不重试。
|
|
27
|
+
if (typeof code === 'string') return TRANSIENT_CODES.has(code)
|
|
28
|
+
// 无具体 code 的纯 'fetch failed'(TypeError)按瞬时处理。
|
|
29
|
+
return error instanceof TypeError && String(error.message ?? '').includes('fetch failed')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @param {number} ms */
|
|
33
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 带重试的 fetch:仅对瞬时网络错误自动重试(默认共 4 次尝试,退避 300/600/1200ms,
|
|
37
|
+
* 上限 1500ms),其余错误与 HTTP 状态码原样交回调用方(业务语义各自判断)。
|
|
38
|
+
* 每次尝试用 timeoutMs 新建超时信号——重试若复用已中止的 AbortSignal.timeout
|
|
39
|
+
* 会立即再抛 AbortError,重试形同虚设。
|
|
40
|
+
* @param {string} url - 请求地址。
|
|
41
|
+
* @param {RequestInit} [init] - fetch init(headers/method/body 等)。
|
|
42
|
+
* @param {{ attempts?: number, backoffMs?: number, timeoutMs?: number }} [options]
|
|
43
|
+
* - attempts:总尝试次数(含首次),默认 4。
|
|
44
|
+
* - backoffMs:退避基数,默认 300(实际等待 backoffMs * 2^(尝试序-2))。
|
|
45
|
+
* - timeoutMs:单次尝试超时;>0 时覆盖 init.signal,默认 0(沿用调用方信号)。
|
|
46
|
+
*/
|
|
47
|
+
export async function fetchWithRetry(url, init = {}, { attempts = 4, backoffMs = 300, timeoutMs = 0 } = {}) {
|
|
48
|
+
for (let attempt = 1; ; attempt++) {
|
|
49
|
+
if (attempt > 1) await sleep(Math.min(1500, backoffMs * 2 ** (attempt - 2)))
|
|
50
|
+
const perAttempt = timeoutMs > 0 ? { ...init, signal: AbortSignal.timeout(timeoutMs) } : init
|
|
51
|
+
try {
|
|
52
|
+
return await fetch(url, perAttempt)
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (!isTransientFetchError(error) || attempt >= attempts) throw error
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cost-meter",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.23",
|
|
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",
|