dsh-cost-meter 1.5.6 → 1.5.8
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 +12 -5
- package/README.md +12 -5
- package/lib/client.js +278 -12
- package/lib/coding-plans.js +108 -57
- package/lib/index.js +37 -1
- package/lib/store.js +1 -1
- package/lib/typert.host.js +47 -0
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
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
|
+
[](https://www.npmjs.com/package/dsh-cost-meter)
|
|
10
11
|
[](LICENSE)
|
|
11
12
|
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
12
13
|
[](https://awesome-dsh-plugin.com)
|
|
@@ -184,22 +185,28 @@ Real captures from an actual DSH sidebar of the period strip and collapsed verti
|
|
|
184
185
|
|
|
185
186
|
### One-click install (recommended)
|
|
186
187
|
|
|
187
|
-
**
|
|
188
|
+
**npm package name** (published to the npm registry, always tracks the latest version; no git needed):
|
|
189
|
+
|
|
190
|
+
```sh
|
|
191
|
+
dsh plugin --profile web add dsh-cost-meter
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**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.8`** — review the script before running):
|
|
188
195
|
|
|
189
196
|
```powershell
|
|
190
|
-
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.
|
|
197
|
+
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.8/install.ps1 | iex
|
|
191
198
|
```
|
|
192
199
|
|
|
193
200
|
**Or a plain command line** (the machine must already have pnpm and git; also pinned to the tag):
|
|
194
201
|
|
|
195
202
|
```sh
|
|
196
|
-
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.
|
|
203
|
+
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.8
|
|
197
204
|
```
|
|
198
205
|
|
|
199
206
|
Without git, use the GitHub tag archive:
|
|
200
207
|
|
|
201
208
|
```sh
|
|
202
|
-
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.
|
|
209
|
+
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.8.tar.gz
|
|
203
210
|
```
|
|
204
211
|
|
|
205
212
|
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,8 @@
|
|
|
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
|
+
[](https://www.npmjs.com/package/dsh-cost-meter)
|
|
10
11
|
[](LICENSE)
|
|
11
12
|
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
12
13
|
[](https://awesome-dsh-plugin.com)
|
|
@@ -186,22 +187,28 @@
|
|
|
186
187
|
|
|
187
188
|
### 一键安装(推荐)
|
|
188
189
|
|
|
189
|
-
**
|
|
190
|
+
**npm 包名安装**(已发布到 npm registry,始终跟随最新版本;无需 git):
|
|
191
|
+
|
|
192
|
+
```sh
|
|
193
|
+
dsh plugin --profile web add dsh-cost-meter
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.5.8`**,建议先下载审阅再运行):
|
|
190
197
|
|
|
191
198
|
```powershell
|
|
192
|
-
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.8/install.ps1 | iex
|
|
193
200
|
```
|
|
194
201
|
|
|
195
202
|
**或直接命令行**(机器上需已有 pnpm 与 git;同样固定到 tag):
|
|
196
203
|
|
|
197
204
|
```sh
|
|
198
|
-
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.8
|
|
199
206
|
```
|
|
200
207
|
|
|
201
208
|
没有 git 时可用 GitHub tag 打包直链:
|
|
202
209
|
|
|
203
210
|
```sh
|
|
204
|
-
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.8.tar.gz
|
|
205
212
|
```
|
|
206
213
|
|
|
207
214
|
安装后**重启** `dsh web`(插件行、Typert 清单与客户端 bundle 均在启动时扫描):
|
package/lib/client.js
CHANGED
|
@@ -77,6 +77,8 @@ window.__ModuleLoader__.load({
|
|
|
77
77
|
'.cm-table th{color:var(--dsw-alias-label-tertiary);font-weight:500}',
|
|
78
78
|
'.cm-table td.num,.cm-table th.num{text-align:right;font-variant-numeric:tabular-nums}',
|
|
79
79
|
'.cm-table tr:last-child td{border-bottom:none}',
|
|
80
|
+
'.cm-table tr.cm-row-click{cursor:pointer}',
|
|
81
|
+
'.cm-table tr.cm-row-click:hover td{background:var(--dsw-alias-bg-hover,rgba(127,127,127,.08))}',
|
|
80
82
|
'.cm-empty{font-size:12px;color:var(--dsw-alias-label-tertiary);padding:8px 0}',
|
|
81
83
|
'.cm-scroll{max-height:320px;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:10px}',
|
|
82
84
|
'.cm-grid{display:grid;grid-template-columns:repeat(2,minmax(220px,1fr));gap:12px}',
|
|
@@ -203,7 +205,7 @@ window.__ModuleLoader__.load({
|
|
|
203
205
|
'.cm-bbox.warn{border-color:var(--dsw-alias-state-warn-primary)}',
|
|
204
206
|
'.cm-bbox.over{border-color:var(--dsw-alias-state-error-primary)}',
|
|
205
207
|
'.cm-bbox-head{display:flex;align-items:center;justify-content:space-between;gap:8px}',
|
|
206
|
-
'.cm-bbox-label{font-size:12px;color:var(--dsw-alias-label-
|
|
208
|
+
'.cm-bbox-label{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary)}',
|
|
207
209
|
'.cm-bbox-pct{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary)}',
|
|
208
210
|
'.cm-bbox.warn .cm-bbox-pct{color:var(--dsw-alias-state-warn-primary)}',
|
|
209
211
|
'.cm-bbox.over .cm-bbox-pct{color:var(--dsw-alias-state-error-primary)}',
|
|
@@ -229,6 +231,16 @@ window.__ModuleLoader__.load({
|
|
|
229
231
|
'.cm-bbox-section.over .cm-bbox-pct{color:var(--dsw-alias-state-error-primary)}',
|
|
230
232
|
'.cm-bbox-section.warn .cm-bbox-fill{background:var(--dsw-alias-state-warn-primary)}',
|
|
231
233
|
'.cm-bbox-section.over .cm-bbox-fill{background:var(--dsw-alias-state-error-primary)}',
|
|
234
|
+
'.cm-mm{padding:8px 10px;gap:4px}',
|
|
235
|
+
'.cm-mm-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary)}',
|
|
236
|
+
'.cm-mm-row{display:flex;align-items:center;gap:8px;padding:2px 0}',
|
|
237
|
+
'.cm-mm-row .cm-bbox-label{flex:none;width:22px;font-weight:400;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}',
|
|
238
|
+
'.cm-mm-row .cm-bbox-bar{flex:1;min-width:0;height:6px}',
|
|
239
|
+
'.cm-mm-row .cm-bbox-pct{flex:none;min-width:2.4em;text-align:right;font-variant-numeric:tabular-nums}',
|
|
240
|
+
'.cm-mm-row.warn .cm-bbox-label,.cm-mm-row.warn .cm-bbox-pct{color:var(--dsw-alias-state-warn-primary)}',
|
|
241
|
+
'.cm-mm-row.over .cm-bbox-label,.cm-mm-row.over .cm-bbox-pct{color:var(--dsw-alias-state-error-primary)}',
|
|
242
|
+
'.cm-mm-row.warn .cm-bbox-fill{background:var(--dsw-alias-state-warn-primary)}',
|
|
243
|
+
'.cm-mm-row.over .cm-bbox-fill{background:var(--dsw-alias-state-error-primary)}',
|
|
232
244
|
'.cm-bal-line{font-size:13px;color:var(--dsw-alias-label-secondary)}',
|
|
233
245
|
'.cm-bal-line.warn{color:var(--dsw-alias-state-warning-primary,#b45309)}',
|
|
234
246
|
'.cm-bal-line.err,.cm-bal-err{color:var(--dsw-alias-state-error-primary)}',
|
|
@@ -371,6 +383,9 @@ window.__ModuleLoader__.load({
|
|
|
371
383
|
codingPlanAnthropic: 'Anthropic(Claude Pro/Max)',
|
|
372
384
|
codingPlanZai: 'Z.ai / 智谱 GLM Coding Plan',
|
|
373
385
|
codingPlanMinimax: 'MiniMax Token Plan',
|
|
386
|
+
codingPlanMinimaxTitle: 'MiniMax Plan',
|
|
387
|
+
codingPlanRemain5h: '5h',
|
|
388
|
+
codingPlanRemain7d: '7d',
|
|
374
389
|
enableCodingPlan: '启用额度查询',
|
|
375
390
|
codingPlanKeyLabel: 'API Key(可选,留空自动发现)',
|
|
376
391
|
refreshCodingPlan: '刷新',
|
|
@@ -468,6 +483,16 @@ window.__ModuleLoader__.load({
|
|
|
468
483
|
cardTotal: '累计费用',
|
|
469
484
|
cardTotalSub: '自账本建立以来 · 调用 {calls} 次',
|
|
470
485
|
todaySessions: '今日会话',
|
|
486
|
+
historyExpandHint: '点击日期行可展开当日会话明细',
|
|
487
|
+
historySessionsLoading: '会话明细加载中…',
|
|
488
|
+
historyNoSessions: '该日无会话明细(早期数据或会话日志已清理)',
|
|
489
|
+
historySessionsError: '会话明细加载失败',
|
|
490
|
+
sessionRankTitle: '按会话统计(全部历史)',
|
|
491
|
+
sessionRankHint: '不按日期分组,全部历史会话按费用降序',
|
|
492
|
+
sessionRankEmpty: '暂无会话数据',
|
|
493
|
+
sessionRankLoading: '会话排行加载中…',
|
|
494
|
+
sessionRankError: '会话排行加载失败',
|
|
495
|
+
sessionRankLimit: '显示条数',
|
|
471
496
|
history: '历史记录',
|
|
472
497
|
usageTitle: 'Token 用量统计',
|
|
473
498
|
usageTotal: '累计 {tokens} tokens · 输入 {input} · 缓存 {cache} · 输出 {output} · {calls} 次调用',
|
|
@@ -645,6 +670,9 @@ window.__ModuleLoader__.load({
|
|
|
645
670
|
codingPlanAnthropic: 'Anthropic (Claude Pro/Max)',
|
|
646
671
|
codingPlanZai: 'Z.ai / Zhipu GLM Coding Plan',
|
|
647
672
|
codingPlanMinimax: 'MiniMax Token Plan',
|
|
673
|
+
codingPlanMinimaxTitle: 'MiniMax Plan',
|
|
674
|
+
codingPlanRemain5h: '5h',
|
|
675
|
+
codingPlanRemain7d: '7d',
|
|
648
676
|
enableCodingPlan: 'Enable quota queries',
|
|
649
677
|
codingPlanKeyLabel: 'API key (optional; empty = auto-detect)',
|
|
650
678
|
refreshCodingPlan: 'Refresh',
|
|
@@ -741,6 +769,16 @@ window.__ModuleLoader__.load({
|
|
|
741
769
|
cardTotal: 'All time',
|
|
742
770
|
cardTotalSub: 'Since the ledger was created · Calls {calls}',
|
|
743
771
|
todaySessions: "Today's sessions",
|
|
772
|
+
historyExpandHint: 'Click a date row to expand its session details',
|
|
773
|
+
historySessionsLoading: 'Loading session details…',
|
|
774
|
+
historyNoSessions: 'No session details for this day (early data or session logs cleaned)',
|
|
775
|
+
historySessionsError: 'Failed to load session details',
|
|
776
|
+
sessionRankTitle: 'By session (all history)',
|
|
777
|
+
sessionRankHint: 'All historical sessions sorted by cost, not grouped by date',
|
|
778
|
+
sessionRankEmpty: 'No session data yet',
|
|
779
|
+
sessionRankLoading: 'Loading session ranking…',
|
|
780
|
+
sessionRankError: 'Failed to load session ranking',
|
|
781
|
+
sessionRankLimit: 'Rows',
|
|
744
782
|
history: 'History',
|
|
745
783
|
usageTitle: 'Token usage stats',
|
|
746
784
|
usageTotal: 'All-time {tokens} tokens · input {input} · cache {cache} · output {output} · {calls} calls',
|
|
@@ -1127,6 +1165,22 @@ window.__ModuleLoader__.load({
|
|
|
1127
1165
|
if (typeof v !== 'string') fail('provider', 'string')
|
|
1128
1166
|
return v
|
|
1129
1167
|
})
|
|
1168
|
+
const dateCodec = codecOf(v => {
|
|
1169
|
+
if (typeof v !== 'string') fail('date', 'string')
|
|
1170
|
+
return v
|
|
1171
|
+
})
|
|
1172
|
+
const dayCodec = codecOf(v => {
|
|
1173
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v)) fail('day', 'object')
|
|
1174
|
+
return v
|
|
1175
|
+
})
|
|
1176
|
+
const limitCodec = codecOf(v => {
|
|
1177
|
+
if (!Number.isFinite(Number(v))) fail('limit', 'number')
|
|
1178
|
+
return Number(v)
|
|
1179
|
+
})
|
|
1180
|
+
const topSessionsCodec = codecOf(v => {
|
|
1181
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v)) fail('topSessions', 'object')
|
|
1182
|
+
return v
|
|
1183
|
+
})
|
|
1130
1184
|
|
|
1131
1185
|
// ── RPC 贡献(与服务端 ./typert 清单一一对应) ───────────────────────────
|
|
1132
1186
|
|
|
@@ -1175,6 +1229,18 @@ window.__ModuleLoader__.load({
|
|
|
1175
1229
|
invocation: { kind: 'direct' }, parameters: [],
|
|
1176
1230
|
result: { mode: 'strict', typeSymbol: 'dsh-cost-meter#CostState', schema: stateCodec },
|
|
1177
1231
|
},
|
|
1232
|
+
{
|
|
1233
|
+
id: 'dsh-cost-meter#costMeter/getDaySessions', service: 'costMeter', namespace: 'costMeter', method: 'getDaySessions',
|
|
1234
|
+
invocation: { kind: 'direct' },
|
|
1235
|
+
parameters: [{ name: 'date', wire: 'date', source: 'json', codec: { mode: 'strict', typeSymbol: 'dsh-cost-meter#DayKey', schema: dateCodec } }],
|
|
1236
|
+
result: { mode: 'strict', typeSymbol: 'dsh-cost-meter#DayRecord', schema: dayCodec },
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
id: 'dsh-cost-meter#costMeter/getTopSessions', service: 'costMeter', namespace: 'costMeter', method: 'getTopSessions',
|
|
1240
|
+
invocation: { kind: 'direct' },
|
|
1241
|
+
parameters: [{ name: 'limit', wire: 'limit', source: 'json', codec: { mode: 'strict', typeSymbol: 'dsh-cost-meter#SessionLimit', schema: limitCodec } }],
|
|
1242
|
+
result: { mode: 'strict', typeSymbol: 'dsh-cost-meter#TopSessions', schema: topSessionsCodec },
|
|
1243
|
+
},
|
|
1178
1244
|
],
|
|
1179
1245
|
}
|
|
1180
1246
|
|
|
@@ -2027,6 +2093,86 @@ window.__ModuleLoader__.load({
|
|
|
2027
2093
|
wide ? view.body : el('div', { className: 'cm-bbox-rail cm-num' }, view.rail)))
|
|
2028
2094
|
}
|
|
2029
2095
|
|
|
2096
|
+
/** MiniMax Token Plan:percent 为已用%,界面按余量(100-已用)展示。 */
|
|
2097
|
+
function miniMaxRemainPct(win) {
|
|
2098
|
+
if (win === null || typeof win !== 'object' || typeof win.percent !== 'number') return null
|
|
2099
|
+
return Math.max(0, Math.min(100, 100 - Number(win.percent)))
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
function miniMaxWindowsOf(windows) {
|
|
2103
|
+
const map = windows !== null && typeof windows === 'object' ? windows : {}
|
|
2104
|
+
return {
|
|
2105
|
+
five: map['5h'] ?? null,
|
|
2106
|
+
seven: map['7d'] ?? map.week ?? map.weekly ?? null,
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
function miniMaxRemainLevel(remain) {
|
|
2111
|
+
if (remain === null) return 'ok'
|
|
2112
|
+
if (remain <= 0) return 'over'
|
|
2113
|
+
if (remain <= 20) return 'warn'
|
|
2114
|
+
return 'ok'
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
function miniMaxResetText(win, t) {
|
|
2118
|
+
if (!win || typeof win.resetsAt !== 'string' || win.resetsAt.length === 0) return ''
|
|
2119
|
+
return t('goResetAt', { time: new Date(win.resetsAt).toLocaleString() })
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
function miniMaxRow(label, win) {
|
|
2123
|
+
const remain = miniMaxRemainPct(win)
|
|
2124
|
+
const pct = remain === null ? 0 : remain
|
|
2125
|
+
const level = miniMaxRemainLevel(remain)
|
|
2126
|
+
return {
|
|
2127
|
+
level,
|
|
2128
|
+
remain,
|
|
2129
|
+
row: el('div', { className: 'cm-mm-row' + (level === 'ok' ? '' : ' ' + level) },
|
|
2130
|
+
el('span', { className: 'cm-bbox-label' }, label),
|
|
2131
|
+
el('div', { className: 'cm-bbox-bar' },
|
|
2132
|
+
el('div', { className: 'cm-bbox-fill', style: { width: Math.min(100, pct) + '%' } })),
|
|
2133
|
+
el('span', { className: 'cm-bbox-pct cm-num' }, remain === null ? '—' : Math.round(pct) + '%')),
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
function MiniMaxPlanCard(props) {
|
|
2138
|
+
const { five, seven, fetchedAt, t, wide } = props
|
|
2139
|
+
const fiveView = miniMaxRow(t('codingPlanRemain5h'), five)
|
|
2140
|
+
const sevenView = miniMaxRow(t('codingPlanRemain7d'), seven)
|
|
2141
|
+
const level = fiveView.level === 'over' || sevenView.level === 'over' ? 'over'
|
|
2142
|
+
: fiveView.level === 'warn' || sevenView.level === 'warn' ? 'warn' : 'ok'
|
|
2143
|
+
const lineOf = (label, win, view) => {
|
|
2144
|
+
const pct = view.remain === null ? '—' : Math.round(view.remain) + '%'
|
|
2145
|
+
const reset = miniMaxResetText(win, t)
|
|
2146
|
+
return label + ' ' + pct + (reset ? ' · ' + reset : '')
|
|
2147
|
+
}
|
|
2148
|
+
const detail = [
|
|
2149
|
+
t('codingPlanMinimaxTitle'),
|
|
2150
|
+
lineOf(t('codingPlanRemain5h'), five, fiveView),
|
|
2151
|
+
lineOf(t('codingPlanRemain7d'), seven, sevenView),
|
|
2152
|
+
fetchedAt > 0 ? t('goQuotaFetchedAt', { time: new Date(fetchedAt).toLocaleTimeString() }) : null,
|
|
2153
|
+
].filter(Boolean).join('; ')
|
|
2154
|
+
const body = el(Fragment, null,
|
|
2155
|
+
el('div', { className: 'cm-mm-title' }, t('codingPlanMinimaxTitle')),
|
|
2156
|
+
fiveView.row,
|
|
2157
|
+
sevenView.row)
|
|
2158
|
+
const rail = el(Fragment, null,
|
|
2159
|
+
el('div', { className: 'cm-bbox-rail cm-num' }, fiveView.remain === null ? '—' : Math.round(fiveView.remain) + '%'),
|
|
2160
|
+
el('div', { className: 'cm-bbox-rail cm-num' }, sevenView.remain === null ? '—' : Math.round(sevenView.remain) + '%'))
|
|
2161
|
+
return el(Tooltip, { label: detail, side: 'right', delayMs: 300 },
|
|
2162
|
+
el('div', { className: 'cm-bbox cm-mm' + (level === 'ok' ? '' : ' ' + level) + (wide === false ? ' rail' : '') },
|
|
2163
|
+
wide === false ? rail : body))
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
function MiniMaxPlanBox(props) {
|
|
2167
|
+
const { state, wide } = props
|
|
2168
|
+
const live = state.codingPlans?.minimax
|
|
2169
|
+
const t = makeT(resolveLocale(state.config?.locale))
|
|
2170
|
+
if (!live || live.status !== 'ok') return null
|
|
2171
|
+
const { five, seven } = miniMaxWindowsOf(live.windows)
|
|
2172
|
+
if (five == null && seven == null) return null
|
|
2173
|
+
return el(MiniMaxPlanCard, { five, seven, fetchedAt: live.fetchedAt, t, wide })
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2030
2176
|
function BudgetBoxContent(props) {
|
|
2031
2177
|
const { state, wide } = props
|
|
2032
2178
|
const today = state.today
|
|
@@ -2140,14 +2286,20 @@ window.__ModuleLoader__.load({
|
|
|
2140
2286
|
const goOk = (config.goQuota?.enabled !== false)
|
|
2141
2287
|
&& (config.goQuota?.display === 'sidebar' || config.goQuota?.display === 'both')
|
|
2142
2288
|
&& state.goQuota?.status === 'ok' && state.goQuota?.[goMainKey] !== null
|
|
2289
|
+
const mmLive = state.codingPlans?.minimax
|
|
2290
|
+
const mmWins = miniMaxWindowsOf(mmLive?.windows)
|
|
2291
|
+
const mmOk = config.codingPlans?.minimax?.enabled === true
|
|
2292
|
+
&& mmLive?.status === 'ok'
|
|
2293
|
+
&& (mmWins.five != null || mmWins.seven != null)
|
|
2143
2294
|
const budgetOn = (config.budget ?? {}).enabled === true
|
|
2144
2295
|
const showToday = config.sidebar !== false
|
|
2145
|
-
if (!showBalance && !showCustomBalance && !goOk && !budgetOn && !showToday) return null
|
|
2296
|
+
if (!showBalance && !showCustomBalance && !goOk && !mmOk && !budgetOn && !showToday) return null
|
|
2146
2297
|
const nodes = []
|
|
2147
2298
|
if (showBalanceBar) nodes.push(el(BalanceBox, { state, wide }))
|
|
2148
2299
|
else if (showBalance) nodes.push(el(BalanceRowContent, { state, wide }))
|
|
2149
2300
|
if (showCustomBalanceBar) nodes.push(el(CustomBalanceBox, { state, wide }))
|
|
2150
2301
|
else if (showCustomBalance) nodes.push(el(CustomBalanceRowContent, { state, wide }))
|
|
2302
|
+
if (mmOk) nodes.push(el(MiniMaxPlanBox, { state, wide }))
|
|
2151
2303
|
if (goOk && budgetOn && wide) {
|
|
2152
2304
|
// 同时出现:合并为一张卡片(Go 在上、预算在下,细分隔线),各自保留预警色与自己的详细信息开关。
|
|
2153
2305
|
const goView = goBoxBody(state, config, t)
|
|
@@ -2179,23 +2331,123 @@ window.__ModuleLoader__.load({
|
|
|
2179
2331
|
}
|
|
2180
2332
|
|
|
2181
2333
|
function HistoryTable(props) {
|
|
2182
|
-
const { state } = props
|
|
2334
|
+
const { state, api } = props
|
|
2183
2335
|
const t = makeT(resolveLocale(state.config?.locale))
|
|
2336
|
+
// 点击日期行展开当日会话明细(issue #22):按需经 getDaySessions 拉取并缓存。
|
|
2337
|
+
const [openDate, setOpenDate] = useState(null)
|
|
2338
|
+
const [cache, setCache] = useState({})
|
|
2339
|
+
const [busyDate, setBusyDate] = useState(null)
|
|
2184
2340
|
const rows = state.history ?? []
|
|
2185
2341
|
if (rows.length === 0) return el('p', { className: 'cm-empty' }, t('noHistory'))
|
|
2342
|
+
const toggle = date => {
|
|
2343
|
+
if (openDate === date) { setOpenDate(null); return }
|
|
2344
|
+
setOpenDate(date)
|
|
2345
|
+
if (cache[date] !== undefined || busyDate !== null) return
|
|
2346
|
+
setBusyDate(date)
|
|
2347
|
+
api.getDaySessions(date)
|
|
2348
|
+
.then(day => setCache(c => ({ ...c, [date]: Array.isArray(day?.sessions) ? day.sessions : [] })))
|
|
2349
|
+
.catch(() => setCache(c => ({ ...c, [date]: 'error' })))
|
|
2350
|
+
.finally(() => setBusyDate(null))
|
|
2351
|
+
}
|
|
2352
|
+
const sessionRows = sessions => sessions.map(session => el('tr', { key: session.id },
|
|
2353
|
+
el('td', null, session.id.slice(0, 14) + '…'),
|
|
2354
|
+
el('td', { className: 'num' }, String(session.calls)),
|
|
2355
|
+
el('td', { className: 'num' }, formatTokens(session.input)),
|
|
2356
|
+
el('td', { className: 'num' }, formatTokens(session.cacheRead + session.cacheWrite)),
|
|
2357
|
+
el('td', { className: 'num' }, formatTokens(session.output)),
|
|
2358
|
+
el('td', { className: 'num' }, formatMoneyUsd(session.cost, state.config))))
|
|
2186
2359
|
return el('div', { className: 'cm-scroll' },
|
|
2360
|
+
el('p', { className: 'cm-hint', style: { padding: '6px 10px 0' } }, t('historyExpandHint')),
|
|
2187
2361
|
el('table', { className: 'cm-table' },
|
|
2188
2362
|
el('thead', null, el('tr', null,
|
|
2189
2363
|
el('th', null, t('colDate')), el('th', { className: 'num' }, t('colCalls')),
|
|
2190
2364
|
el('th', { className: 'num' }, t('colInTok')), el('th', { className: 'num' }, t('colCacheTok')), el('th', { className: 'num' }, t('colOutTok')),
|
|
2191
2365
|
el('th', { className: 'num' }, t('colCost')))),
|
|
2192
|
-
el('tbody', null, rows.
|
|
2193
|
-
el('
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2366
|
+
el('tbody', null, rows.flatMap(day => {
|
|
2367
|
+
const base = el('tr', { key: day.date, className: 'cm-row-click', onClick: () => toggle(day.date) },
|
|
2368
|
+
el('td', null, (openDate === day.date ? '▾ ' : '▸ ') + day.date),
|
|
2369
|
+
el('td', { className: 'num' }, String(day.calls)),
|
|
2370
|
+
el('td', { className: 'num' }, formatTokens(day.input)),
|
|
2371
|
+
el('td', { className: 'num' }, formatTokens(day.cacheRead + day.cacheWrite)),
|
|
2372
|
+
el('td', { className: 'num' }, formatTokens(day.output)),
|
|
2373
|
+
el('td', { className: 'num' }, formatMoneyUsd(day.cost, state.config)))
|
|
2374
|
+
if (openDate !== day.date) return [base]
|
|
2375
|
+
const cached = cache[day.date]
|
|
2376
|
+
const detail = busyDate === day.date && cached === undefined
|
|
2377
|
+
? el('p', { className: 'cm-empty' }, t('historySessionsLoading'))
|
|
2378
|
+
: cached === 'error'
|
|
2379
|
+
? el('p', { className: 'cm-empty' }, t('historySessionsError'))
|
|
2380
|
+
: !Array.isArray(cached) || cached.length === 0
|
|
2381
|
+
? el('p', { className: 'cm-empty' }, t('historyNoSessions'))
|
|
2382
|
+
: el('table', { className: 'cm-table' },
|
|
2383
|
+
el('thead', null, el('tr', null,
|
|
2384
|
+
el('th', null, t('colSession')), el('th', { className: 'num' }, t('colCalls')),
|
|
2385
|
+
el('th', { className: 'num' }, t('colInTok')), el('th', { className: 'num' }, t('colCacheTok')), el('th', { className: 'num' }, t('colOutTok')),
|
|
2386
|
+
el('th', { className: 'num' }, t('colCost')))),
|
|
2387
|
+
el('tbody', null, sessionRows(cached)))
|
|
2388
|
+
return [base, el('tr', { key: day.date + '-sessions' }, el('td', { colSpan: 6 }, detail))]
|
|
2389
|
+
}))))
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
// 按会话统计(issue #22 不分日期视角):全部历史会话按费用降序,默认收起、展开时按需拉取。
|
|
2393
|
+
function SessionRankPanel(props) {
|
|
2394
|
+
const { state, api } = props
|
|
2395
|
+
const t = makeT(resolveLocale(state.config?.locale))
|
|
2396
|
+
const [open, setOpen] = useState(false)
|
|
2397
|
+
const [limit, setLimit] = useState(100)
|
|
2398
|
+
const [rows, setRows] = useState(null)
|
|
2399
|
+
const [busy, setBusy] = useState(false)
|
|
2400
|
+
const [err, setErr] = useState(false)
|
|
2401
|
+
const load = n => {
|
|
2402
|
+
setBusy(true)
|
|
2403
|
+
setErr(false)
|
|
2404
|
+
api.getTopSessions(n)
|
|
2405
|
+
.then(res => setRows(Array.isArray(res?.sessions) ? res.sessions : []))
|
|
2406
|
+
.catch(() => setErr(true))
|
|
2407
|
+
.finally(() => setBusy(false))
|
|
2408
|
+
}
|
|
2409
|
+
const toggle = () => {
|
|
2410
|
+
setOpen(o => {
|
|
2411
|
+
const next = !o
|
|
2412
|
+
if (next && rows === null && !busy) load(limit)
|
|
2413
|
+
return next
|
|
2414
|
+
})
|
|
2415
|
+
}
|
|
2416
|
+
const changeLimit = n => {
|
|
2417
|
+
setLimit(n)
|
|
2418
|
+
if (open || rows !== null) load(n)
|
|
2419
|
+
}
|
|
2420
|
+
return el('div', { className: 'cm-budget' },
|
|
2421
|
+
el('div', { className: 'cm-budget-head' },
|
|
2422
|
+
el('button', { type: 'button', className: 'cm-collapse-h', 'aria-expanded': String(open), onClick: toggle },
|
|
2423
|
+
el('span', { className: 'cm-caret' + (open ? ' open' : '') }),
|
|
2424
|
+
el('h3', { className: 'cm-h' }, t('sessionRankTitle')))),
|
|
2425
|
+
open ? el('div', { className: 'cm-collapse-body' },
|
|
2426
|
+
el('p', { className: 'cm-hint' }, t('sessionRankHint')),
|
|
2427
|
+
el('div', { className: 'cm-field', style: { margin: '6px 0' } },
|
|
2428
|
+
el('label', null, t('sessionRankLimit')),
|
|
2429
|
+
el('select', { className: 'cm-input', value: String(limit), onChange: event => changeLimit(Number(event.target.value)) },
|
|
2430
|
+
el('option', { value: '50' }, '50'),
|
|
2431
|
+
el('option', { value: '100' }, '100'),
|
|
2432
|
+
el('option', { value: '200' }, '200'))),
|
|
2433
|
+
busy ? el('p', { className: 'cm-empty' }, t('sessionRankLoading'))
|
|
2434
|
+
: err ? el('p', { className: 'cm-empty' }, t('sessionRankError'))
|
|
2435
|
+
: rows === null || rows.length === 0 ? el('p', { className: 'cm-empty' }, t('sessionRankEmpty'))
|
|
2436
|
+
: el('div', { className: 'cm-scroll' },
|
|
2437
|
+
el('table', { className: 'cm-table' },
|
|
2438
|
+
el('thead', null, el('tr', null,
|
|
2439
|
+
el('th', null, t('colDate')), el('th', null, t('colSession')), el('th', { className: 'num' }, t('colCalls')),
|
|
2440
|
+
el('th', { className: 'num' }, t('colInTok')), el('th', { className: 'num' }, t('colCacheTok')), el('th', { className: 'num' }, t('colOutTok')),
|
|
2441
|
+
el('th', { className: 'num' }, t('colCost')))),
|
|
2442
|
+
el('tbody', null, rows.map(session => el('tr', { key: session.date + ':' + session.id },
|
|
2443
|
+
el('td', null, session.date),
|
|
2444
|
+
el('td', null, String(session.id).slice(0, 14) + '…'),
|
|
2445
|
+
el('td', { className: 'num' }, String(session.calls)),
|
|
2446
|
+
el('td', { className: 'num' }, formatTokens(session.input)),
|
|
2447
|
+
el('td', { className: 'num' }, formatTokens((session.cacheRead ?? 0) + (session.cacheWrite ?? 0))),
|
|
2448
|
+
el('td', { className: 'num' }, formatTokens(session.output)),
|
|
2449
|
+
el('td', { className: 'num' }, formatMoneyUsd(session.cost, state.config))))))))
|
|
2450
|
+
: null)
|
|
2199
2451
|
}
|
|
2200
2452
|
|
|
2201
2453
|
function TodaySessions(props) {
|
|
@@ -3151,7 +3403,15 @@ window.__ModuleLoader__.load({
|
|
|
3151
3403
|
: live.status === 'ok'
|
|
3152
3404
|
? el('div', { className: 'cm-go-list' },
|
|
3153
3405
|
Object.keys(windows).length > 0
|
|
3154
|
-
?
|
|
3406
|
+
? (id === 'minimax' && (miniMaxWindowsOf(windows).five != null || miniMaxWindowsOf(windows).seven != null)
|
|
3407
|
+
? el(MiniMaxPlanCard, {
|
|
3408
|
+
five: miniMaxWindowsOf(windows).five,
|
|
3409
|
+
seven: miniMaxWindowsOf(windows).seven,
|
|
3410
|
+
fetchedAt: live.fetchedAt,
|
|
3411
|
+
t,
|
|
3412
|
+
wide: true,
|
|
3413
|
+
})
|
|
3414
|
+
: Object.entries(windows).map(([name, win]) => windowRow(name, win)))
|
|
3155
3415
|
: el('div', { className: 'cm-bal-line' }, t('codingPlanNotQueried')),
|
|
3156
3416
|
el('div', { className: 'cm-go-time' }, t('goQuotaFetchedAt', {
|
|
3157
3417
|
time: live.fetchedAt > 0 ? new Date(live.fetchedAt).toLocaleTimeString() : '—',
|
|
@@ -3479,7 +3739,9 @@ window.__ModuleLoader__.load({
|
|
|
3479
3739
|
// 历史
|
|
3480
3740
|
el('div', null,
|
|
3481
3741
|
el('h3', { className: 'cm-h' }, t('history')),
|
|
3482
|
-
el(HistoryTable, { state,
|
|
3742
|
+
el(HistoryTable, { state, api })),
|
|
3743
|
+
// 按会话统计(全部历史,不分日期;issue #22)
|
|
3744
|
+
el(SessionRankPanel, { state, api }),
|
|
3483
3745
|
// 显示设置
|
|
3484
3746
|
el('div', null,
|
|
3485
3747
|
el('h3', { className: 'cm-h' }, t('displaySettings')),
|
|
@@ -3895,6 +4157,10 @@ window.__ModuleLoader__.load({
|
|
|
3895
4157
|
store.set({ status: 'ready', error: null, state })
|
|
3896
4158
|
return state
|
|
3897
4159
|
},
|
|
4160
|
+
// 按需拉取某天会话明细(issue #22),返回当日完整记录。
|
|
4161
|
+
getDaySessions: async date => call('getDaySessions', [date]),
|
|
4162
|
+
// 跨全部日期按费用降序的会话排行(issue #22 不分日期视角)。
|
|
4163
|
+
getTopSessions: async limit => call('getTopSessions', [limit]),
|
|
3898
4164
|
refreshBalance: async () => {
|
|
3899
4165
|
const result = await costMeter.refreshBalance()
|
|
3900
4166
|
if (result === null || typeof result !== 'object' || result.ok !== true) {
|
package/lib/coding-plans.js
CHANGED
|
@@ -143,48 +143,92 @@ export function parseZaiUsage(data) {
|
|
|
143
143
|
return Object.keys(windows).length > 0 ? windows : null
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/** 0-100 已用百分比,保留 1 位。 */
|
|
147
|
+
function clampPct(p) {
|
|
148
|
+
return Math.max(0, Math.min(100, Math.round(p * 10) / 10))
|
|
149
|
+
}
|
|
150
|
+
|
|
146
151
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* 平铺结构——根节点(或 data.data)直接含 current_interval_*(5小时固定窗)与
|
|
150
|
-
* current_weekly_*(周窗)字段,无窗口数组;status=3 表示不限量,不展示该窗;
|
|
151
|
-
* - Token Plan(旧数组形态):窗口数组字段,条目含 total/used/remain 与 interval 标签;
|
|
152
|
-
* - Coding Plan(旧计数制):GET .../v1/api/openplatform/coding_plan/remains
|
|
153
|
-
* { model_remains: [{ current_interval_total_count, current_interval_usage_count, ... }] }。
|
|
152
|
+
* MiniMax 窗口剩余百分比。优先 *_remaining_percent(现行 Token Plan,total 常为 0);
|
|
153
|
+
* 否则用 total/remain 或 total/used 反推。非法 → null。
|
|
154
154
|
*/
|
|
155
|
-
|
|
156
|
-
if (
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (Number.isFinite(total) && total > 0) {
|
|
166
|
-
if (Number.isFinite(used)) return (used / total) * 100
|
|
167
|
-
if (Number.isFinite(remain)) return ((total - remain) / total) * 100
|
|
168
|
-
}
|
|
169
|
-
// *_remaining_percent 是剩余百分比(<=1 视为比例),反推已用。
|
|
170
|
-
const rp = Number(payload[remainPctKey])
|
|
171
|
-
if (Number.isFinite(rp)) return 100 - Math.min(100, Math.max(0, rp <= 1 ? rp * 100 : rp))
|
|
172
|
-
return null
|
|
155
|
+
function remainingPercentOf(row, remainPctKey, totalKey, usedKey, remainKey) {
|
|
156
|
+
if (row === null || typeof row !== 'object') return null
|
|
157
|
+
const rp = Number(row[remainPctKey])
|
|
158
|
+
if (Number.isFinite(rp)) return Math.min(100, Math.max(0, rp <= 1 ? rp * 100 : rp))
|
|
159
|
+
const total = Number(row[totalKey])
|
|
160
|
+
const used = Number(row[usedKey])
|
|
161
|
+
const remain = Number(row[remainKey])
|
|
162
|
+
if (Number.isFinite(total) && total > 0) {
|
|
163
|
+
if (Number.isFinite(remain)) return (remain / total) * 100
|
|
164
|
+
if (Number.isFinite(used)) return ((total - used) / total) * 100
|
|
173
165
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
166
|
+
return null
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 从单条 MiniMax 记录抽出 5h / 7d 窗口。percent 存已用%(与其它厂商一致);
|
|
171
|
+
* status=3 表示不限量,跳过该窗。
|
|
172
|
+
*/
|
|
173
|
+
function windowsFromMiniMaxRecord(row) {
|
|
174
|
+
if (row === null || typeof row !== 'object') return {}
|
|
175
|
+
const windows = {}
|
|
176
|
+
if (Number(row.current_interval_status) !== 3) {
|
|
177
|
+
const remain = remainingPercentOf(
|
|
178
|
+
row,
|
|
179
|
+
'current_interval_remaining_percent',
|
|
180
|
+
'current_interval_total_count',
|
|
181
|
+
'current_interval_usage_count',
|
|
182
|
+
'current_interval_remain_count',
|
|
183
|
+
)
|
|
184
|
+
if (remain !== null) {
|
|
185
|
+
windows['5h'] = {
|
|
186
|
+
percent: clampPct(100 - remain),
|
|
187
|
+
resetsAt: normalizeResetAt(row.end_time ?? row.reset_time ?? row.next_reset_time),
|
|
188
|
+
}
|
|
179
189
|
}
|
|
180
190
|
}
|
|
181
|
-
if (Number(
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
191
|
+
if (Number(row.current_weekly_status) !== 3) {
|
|
192
|
+
const remain = remainingPercentOf(
|
|
193
|
+
row,
|
|
194
|
+
'current_weekly_remaining_percent',
|
|
195
|
+
'current_weekly_total_count',
|
|
196
|
+
'current_weekly_usage_count',
|
|
197
|
+
'current_weekly_remain_count',
|
|
198
|
+
)
|
|
199
|
+
if (remain !== null) {
|
|
200
|
+
windows['7d'] = {
|
|
201
|
+
percent: clampPct(100 - remain),
|
|
202
|
+
resetsAt: normalizeResetAt(row.weekly_end_time),
|
|
203
|
+
}
|
|
185
204
|
}
|
|
186
205
|
}
|
|
187
|
-
|
|
206
|
+
return windows
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** 选 chat/通用额度行:general → MiniMax-M* → 第一条能解析出窗口的记录。跳过仅 video/speech 的无限量行。 */
|
|
210
|
+
function pickMiniMaxModelRow(rows) {
|
|
211
|
+
const list = rows.filter(row => row !== null && typeof row === 'object')
|
|
212
|
+
const byName = name => list.find(row => String(row.model_name ?? '').toLowerCase() === name)
|
|
213
|
+
return byName('general')
|
|
214
|
+
?? list.find(row => /^minimax-m/i.test(String(row.model_name ?? '')))
|
|
215
|
+
?? list.find(row => Object.keys(windowsFromMiniMaxRecord(row)).length > 0)
|
|
216
|
+
?? list[0]
|
|
217
|
+
?? null
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 解析 MiniMax 用量响应。兼容四种官方形态:
|
|
222
|
+
* - Token Plan(2026-08 现行,model_remains):GET https://www.minimaxi.com|io/v1/token_plan/remains
|
|
223
|
+
* { model_remains: [{ model_name, current_interval_remaining_percent, current_weekly_remaining_percent, ... }] }
|
|
224
|
+
* total_count 常为 0,以 remaining_percent 为准;取 general(或 MiniMax-M*)一行抽出 5h/7d,不按模型拆条;
|
|
225
|
+
* - Token Plan 平铺结构(issue #20):根节点(或 data.data)直含 current_interval_* 与 current_weekly_*;
|
|
226
|
+
* - Token Plan 旧数组形态:窗口数组字段,条目含 total/used/remain 与 interval 标签;
|
|
227
|
+
* - Coding Plan 旧计数制:model_remains 仅有 total/used、无 remaining_percent 时汇总。
|
|
228
|
+
*/
|
|
229
|
+
export function parseMiniMaxRemains(data) {
|
|
230
|
+
if (data === null || typeof data !== 'object') return null
|
|
231
|
+
const payload = data.data !== null && typeof data.data === 'object' && !Array.isArray(data.data) ? data.data : data
|
|
188
232
|
const pickArray = (...keys) => {
|
|
189
233
|
for (const key of keys) {
|
|
190
234
|
const direct = Array.isArray(data?.[key]) ? data[key] : null
|
|
@@ -194,7 +238,36 @@ export function parseMiniMaxRemains(data) {
|
|
|
194
238
|
}
|
|
195
239
|
return null
|
|
196
240
|
}
|
|
241
|
+
|
|
242
|
+
// 现行 Token Plan:model_remains + remaining_percent(total 可为 0)。优先于平铺,避免根对象空字段误判。
|
|
243
|
+
const modelRows = pickArray('model_remains')
|
|
244
|
+
if (modelRows !== null) {
|
|
245
|
+
const row = pickMiniMaxModelRow(modelRows)
|
|
246
|
+
const fromRow = windowsFromMiniMaxRecord(row)
|
|
247
|
+
if (Object.keys(fromRow).length > 0) return fromRow
|
|
248
|
+
// 旧计数制:无 remaining_percent、靠 total>0 汇总(忽略零额度行)。
|
|
249
|
+
let total = 0
|
|
250
|
+
let used = 0
|
|
251
|
+
let found = false
|
|
252
|
+
for (const item of modelRows) {
|
|
253
|
+
if (item === null || typeof item !== 'object') continue
|
|
254
|
+
const t = Number(item.current_interval_total_count ?? item.total)
|
|
255
|
+
const u = Number(item.current_interval_usage_count ?? item.used)
|
|
256
|
+
if (!Number.isFinite(t) || t <= 0) continue
|
|
257
|
+
found = true
|
|
258
|
+
total += t
|
|
259
|
+
used += Number.isFinite(u) ? u : 0
|
|
260
|
+
}
|
|
261
|
+
if (found && total > 0) {
|
|
262
|
+
return { current: { percent: Math.min(100, Math.round((used / total) * 1000) / 10), resetsAt: '' } }
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const flat = windowsFromMiniMaxRecord(payload)
|
|
267
|
+
if (Object.keys(flat).length > 0) return flat
|
|
268
|
+
|
|
197
269
|
// Token Plan:窗口数组(字段名容错)。
|
|
270
|
+
const windows = {}
|
|
198
271
|
const planRows = pickArray('token_plan_remains', 'plan_remains', 'remains', 'windows')
|
|
199
272
|
if (planRows !== null) {
|
|
200
273
|
planRows.forEach((row, index) => {
|
|
@@ -215,28 +288,6 @@ export function parseMiniMaxRemains(data) {
|
|
|
215
288
|
}
|
|
216
289
|
})
|
|
217
290
|
}
|
|
218
|
-
// 旧 Coding Plan 计数制:model_remains。
|
|
219
|
-
const modelRows = pickArray('model_remains')
|
|
220
|
-
if (modelRows !== null) {
|
|
221
|
-
let total = 0
|
|
222
|
-
let used = 0
|
|
223
|
-
let found = false
|
|
224
|
-
for (const row of modelRows) {
|
|
225
|
-
if (row === null || typeof row !== 'object') continue
|
|
226
|
-
const t = Number(row.current_interval_total_count ?? row.total)
|
|
227
|
-
const u = Number(row.current_interval_usage_count ?? row.used)
|
|
228
|
-
if (!Number.isFinite(t) || t <= 0) continue
|
|
229
|
-
found = true
|
|
230
|
-
total += t
|
|
231
|
-
used += Number.isFinite(u) ? u : 0
|
|
232
|
-
}
|
|
233
|
-
if (found && total > 0) {
|
|
234
|
-
windows.current = {
|
|
235
|
-
percent: Math.min(100, Math.round((used / total) * 1000) / 10),
|
|
236
|
-
resetsAt: '',
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
291
|
return Object.keys(windows).length > 0 ? windows : null
|
|
241
292
|
}
|
|
242
293
|
|
package/lib/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import fs from 'node:fs'
|
|
|
18
18
|
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
|
-
import { Ledger, applyConfigPatch, localDayKey, reconcileBalanceDelta } from './store.js'
|
|
21
|
+
import { Ledger, applyConfigPatch, localDayKey, reconcileBalanceDelta, zeroDay } from './store.js'
|
|
22
22
|
import { backfillLegacyLedger } 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, emptyCustomBalance, queryCustomBalance } from './coding-plans.js'
|
|
@@ -898,6 +898,42 @@ function createService(ctx, ledger) {
|
|
|
898
898
|
ledger.scheduleWrite()
|
|
899
899
|
return build(false)
|
|
900
900
|
},
|
|
901
|
+
|
|
902
|
+
// 按需读取某一天的完整记录(含会话明细;issue #22):history() 输出为轻量副本
|
|
903
|
+
// 不含会话,历史各天的会话明细由本 RPC 展开时才拉取,避免 state 膨胀。
|
|
904
|
+
async getDaySessions(date) {
|
|
905
|
+
if (typeof date !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
906
|
+
throw new Error('invalid date')
|
|
907
|
+
}
|
|
908
|
+
const day = ledger.days[date]
|
|
909
|
+
return day === undefined ? zeroDay(date) : ledger.copyDay(day)
|
|
910
|
+
},
|
|
911
|
+
|
|
912
|
+
// 跨全部日期按费用降序返回前 N 个会话(issue #22 按会话视角,不分日期)。
|
|
913
|
+
async getTopSessions(limit) {
|
|
914
|
+
const n = Math.max(1, Math.min(500, Math.floor(Number(limit)) || 100))
|
|
915
|
+
const all = []
|
|
916
|
+
for (const [date, day] of Object.entries(ledger.days)) {
|
|
917
|
+
if (!Array.isArray(day.sessions)) continue
|
|
918
|
+
for (const s of day.sessions) {
|
|
919
|
+
if (s === null || typeof s !== 'object') continue
|
|
920
|
+
all.push({
|
|
921
|
+
date,
|
|
922
|
+
id: String(s.id ?? ''),
|
|
923
|
+
input: s.input ?? 0,
|
|
924
|
+
output: s.output ?? 0,
|
|
925
|
+
cacheRead: s.cacheRead ?? 0,
|
|
926
|
+
cacheWrite: s.cacheWrite ?? 0,
|
|
927
|
+
reasoning: s.reasoning ?? 0,
|
|
928
|
+
calls: s.calls ?? 0,
|
|
929
|
+
cost: s.cost ?? 0,
|
|
930
|
+
byProviderModel: s.byProviderModel ?? {},
|
|
931
|
+
})
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
all.sort((a, b) => b.cost - a.cost)
|
|
935
|
+
return { sessions: all.slice(0, n) }
|
|
936
|
+
},
|
|
901
937
|
}
|
|
902
938
|
Object.defineProperty(service, 'typertRemote', {
|
|
903
939
|
configurable: false,
|
package/lib/store.js
CHANGED
|
@@ -125,7 +125,7 @@ export function localDayKey(ms) {
|
|
|
125
125
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
function zeroDay(date) {
|
|
128
|
+
export function zeroDay(date) {
|
|
129
129
|
return { date, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, calls: 0, cost: 0, byProviderModel: {}, sessions: [] }
|
|
130
130
|
}
|
|
131
131
|
|
package/lib/typert.host.js
CHANGED
|
@@ -33,6 +33,13 @@ const daySchema = z.object({
|
|
|
33
33
|
sessions: z.array(sessionSchema),
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
+
// 带所属日期的会话条目(issue #22 按会话排行:不分日期视角)。
|
|
37
|
+
const datedSessionSchema = sessionSchema.extend({ date: z.string() })
|
|
38
|
+
|
|
39
|
+
const topSessionsSchema = z.object({
|
|
40
|
+
sessions: z.array(datedSessionSchema),
|
|
41
|
+
})
|
|
42
|
+
|
|
36
43
|
const priceTierSchema = z.object({
|
|
37
44
|
cacheHit: num,
|
|
38
45
|
cacheMiss: num,
|
|
@@ -241,6 +248,10 @@ const _state$codec = { mode: 'strict', typeSymbol: 'dsh-cost-meter#CostState', s
|
|
|
241
248
|
const _patch$codec = { mode: 'strict', typeSymbol: 'dsh-cost-meter#ConfigPatch', schema: patchSchema }
|
|
242
249
|
const _fetch$codec = { mode: 'strict', typeSymbol: 'dsh-cost-meter#FetchPricesResult', schema: fetchPricesSchema }
|
|
243
250
|
const _provider$codec = { mode: 'strict', typeSymbol: 'dsh-cost-meter#CodingPlanProvider', schema: z.string() }
|
|
251
|
+
const _day$codec = { mode: 'strict', typeSymbol: 'dsh-cost-meter#DayRecord', schema: daySchema }
|
|
252
|
+
const _date$codec = { mode: 'strict', typeSymbol: 'dsh-cost-meter#DayKey', schema: z.string() }
|
|
253
|
+
const _topSessions$codec = { mode: 'strict', typeSymbol: 'dsh-cost-meter#TopSessions', schema: topSessionsSchema }
|
|
254
|
+
const _limit$codec = { mode: 'strict', typeSymbol: 'dsh-cost-meter#SessionLimit', schema: num }
|
|
244
255
|
|
|
245
256
|
export const TYPERT = {
|
|
246
257
|
package: 'dsh-cost-meter',
|
|
@@ -323,6 +334,28 @@ export const TYPERT = {
|
|
|
323
334
|
parameters: [],
|
|
324
335
|
result: _state$codec,
|
|
325
336
|
},
|
|
337
|
+
{
|
|
338
|
+
id: 'dsh-cost-meter#costMeter/getDaySessions',
|
|
339
|
+
service: 'costMeter',
|
|
340
|
+
namespace: 'costMeter',
|
|
341
|
+
method: 'getDaySessions',
|
|
342
|
+
invocation: { kind: 'direct' },
|
|
343
|
+
parameters: [
|
|
344
|
+
{ name: 'date', wire: 'date', source: 'json', codec: _date$codec },
|
|
345
|
+
],
|
|
346
|
+
result: _day$codec,
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
id: 'dsh-cost-meter#costMeter/getTopSessions',
|
|
350
|
+
service: 'costMeter',
|
|
351
|
+
namespace: 'costMeter',
|
|
352
|
+
method: 'getTopSessions',
|
|
353
|
+
invocation: { kind: 'direct' },
|
|
354
|
+
parameters: [
|
|
355
|
+
{ name: 'limit', wire: 'limit', source: 'json', codec: _limit$codec },
|
|
356
|
+
],
|
|
357
|
+
result: _topSessions$codec,
|
|
358
|
+
},
|
|
326
359
|
],
|
|
327
360
|
model: {
|
|
328
361
|
services: [
|
|
@@ -390,6 +423,20 @@ export const TYPERT = {
|
|
|
390
423
|
summary: '清空全部历史记录。Clear all history records.',
|
|
391
424
|
jsDoc: '/**\n * 清空全部历史记录。\n * @returns 清空后的完整快照。\n * Clear all history records.\n * @returns The full snapshot after clearing.\n */',
|
|
392
425
|
},
|
|
426
|
+
{
|
|
427
|
+
kind: 'method',
|
|
428
|
+
name: 'getDaySessions',
|
|
429
|
+
signature: 'getDaySessions(date: string): DayRecord',
|
|
430
|
+
summary: '按需读取某一天的完整记录(含会话明细)。Read a day\'s full record on demand (with session details).',
|
|
431
|
+
jsDoc: '/**\n * 按需读取某一天的完整记录(含会话明细;历史记录列表为轻量副本不含会话)。\n * @param date - 本地日期键 YYYY-MM-DD。\n * @returns 该日完整记录;不存在时返回零值记录。\n * Read a day\'s full record on demand (with session details; the history list is a light copy without sessions).\n * @param date - Local day key YYYY-MM-DD.\n * @returns The full day record; a zero record when absent.\n */',
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
kind: 'method',
|
|
435
|
+
name: 'getTopSessions',
|
|
436
|
+
signature: 'getTopSessions(limit: number): TopSessions',
|
|
437
|
+
summary: '跨全部日期按费用降序返回前 N 个会话(不分日期视角)。Return the top N sessions by cost across all days.',
|
|
438
|
+
jsDoc: '/**\n * 跨全部日期按费用降序返回前 N 个会话(每条带所属日期;不含今日实时条目之外的去重)。\n * @param limit - 返回条数上限(服务端限制 1-500)。\n * @returns 会话列表(含 date 字段)。\n * Return the top N sessions by cost across all days (each tagged with its date).\n * @param limit - Max rows to return (server clamps to 1-500).\n * @returns Session list with a date field.\n */',
|
|
439
|
+
},
|
|
393
440
|
],
|
|
394
441
|
types: [],
|
|
395
442
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cost-meter",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.8",
|
|
4
4
|
"description": "DeepSeek Harness 会话费用统计插件:本会话成本、当日费用、历史记录与官方价格同步,支持多厂商多模型价格计费(内置 90+ 模型价格目录与自动匹配)、主流 Coding Plan 订阅额度查询与显示(6 家)、自定义 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 6 vendors, 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",
|