dsh-cost-meter 1.5.6 → 1.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -6,7 +6,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
- [![version](https://img.shields.io/badge/version-1.5.6-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
9
+ [![version](https://img.shields.io/badge/version-1.5.7-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
10
+ [![npm](https://img.shields.io/npm/v/dsh-cost-meter?label=npm)](https://www.npmjs.com/package/dsh-cost-meter)
10
11
  [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
11
12
  [![dsh](https://img.shields.io/badge/DeepSeek%20Harness-dsh--plugin-4176E6)](https://github.com/deepseek-ai/deepseek-harness)
12
13
  [![awesome · DSH plugin](https://awesome-dsh-plugin.com/badge.svg)](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
- **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.6`** — review the script before running):
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.7`** — review the script before running):
188
195
 
189
196
  ```powershell
190
- irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.6/install.ps1 | iex
197
+ irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.7/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.6
203
+ dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.7
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.6.tar.gz
209
+ dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.7.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
- [![version](https://img.shields.io/badge/version-1.5.6-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
9
+ [![version](https://img.shields.io/badge/version-1.5.7-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
10
+ [![npm](https://img.shields.io/npm/v/dsh-cost-meter?label=npm)](https://www.npmjs.com/package/dsh-cost-meter)
10
11
  [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
11
12
  [![dsh](https://img.shields.io/badge/DeepSeek%20Harness-dsh--plugin-4176E6)](https://github.com/deepseek-ai/deepseek-harness)
12
13
  [![awesome · DSH plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
@@ -186,22 +187,28 @@
186
187
 
187
188
  ### 一键安装(推荐)
188
189
 
189
- **PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.5.6`**,建议先下载审阅再运行):
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.7`**,建议先下载审阅再运行):
190
197
 
191
198
  ```powershell
192
- irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.6/install.ps1 | iex
199
+ irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.7/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.6
205
+ dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.7
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.6.tar.gz
211
+ dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.7.tar.gz
205
212
  ```
206
213
 
207
214
  安装后**重启** `dsh web`(插件行、Typert 清单与客户端 bundle 均在启动时扫描):
package/lib/client.js CHANGED
@@ -203,7 +203,7 @@ window.__ModuleLoader__.load({
203
203
  '.cm-bbox.warn{border-color:var(--dsw-alias-state-warn-primary)}',
204
204
  '.cm-bbox.over{border-color:var(--dsw-alias-state-error-primary)}',
205
205
  '.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-secondary)}',
206
+ '.cm-bbox-label{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary)}',
207
207
  '.cm-bbox-pct{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary)}',
208
208
  '.cm-bbox.warn .cm-bbox-pct{color:var(--dsw-alias-state-warn-primary)}',
209
209
  '.cm-bbox.over .cm-bbox-pct{color:var(--dsw-alias-state-error-primary)}',
@@ -229,6 +229,16 @@ window.__ModuleLoader__.load({
229
229
  '.cm-bbox-section.over .cm-bbox-pct{color:var(--dsw-alias-state-error-primary)}',
230
230
  '.cm-bbox-section.warn .cm-bbox-fill{background:var(--dsw-alias-state-warn-primary)}',
231
231
  '.cm-bbox-section.over .cm-bbox-fill{background:var(--dsw-alias-state-error-primary)}',
232
+ '.cm-mm{padding:8px 10px;gap:4px}',
233
+ '.cm-mm-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary)}',
234
+ '.cm-mm-row{display:flex;align-items:center;gap:8px;padding:2px 0}',
235
+ '.cm-mm-row .cm-bbox-label{flex:none;width:22px;font-weight:400;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}',
236
+ '.cm-mm-row .cm-bbox-bar{flex:1;min-width:0;height:6px}',
237
+ '.cm-mm-row .cm-bbox-pct{flex:none;min-width:2.4em;text-align:right;font-variant-numeric:tabular-nums}',
238
+ '.cm-mm-row.warn .cm-bbox-label,.cm-mm-row.warn .cm-bbox-pct{color:var(--dsw-alias-state-warn-primary)}',
239
+ '.cm-mm-row.over .cm-bbox-label,.cm-mm-row.over .cm-bbox-pct{color:var(--dsw-alias-state-error-primary)}',
240
+ '.cm-mm-row.warn .cm-bbox-fill{background:var(--dsw-alias-state-warn-primary)}',
241
+ '.cm-mm-row.over .cm-bbox-fill{background:var(--dsw-alias-state-error-primary)}',
232
242
  '.cm-bal-line{font-size:13px;color:var(--dsw-alias-label-secondary)}',
233
243
  '.cm-bal-line.warn{color:var(--dsw-alias-state-warning-primary,#b45309)}',
234
244
  '.cm-bal-line.err,.cm-bal-err{color:var(--dsw-alias-state-error-primary)}',
@@ -371,6 +381,9 @@ window.__ModuleLoader__.load({
371
381
  codingPlanAnthropic: 'Anthropic(Claude Pro/Max)',
372
382
  codingPlanZai: 'Z.ai / 智谱 GLM Coding Plan',
373
383
  codingPlanMinimax: 'MiniMax Token Plan',
384
+ codingPlanMinimaxTitle: 'MiniMax Plan',
385
+ codingPlanRemain5h: '5h',
386
+ codingPlanRemain7d: '7d',
374
387
  enableCodingPlan: '启用额度查询',
375
388
  codingPlanKeyLabel: 'API Key(可选,留空自动发现)',
376
389
  refreshCodingPlan: '刷新',
@@ -645,6 +658,9 @@ window.__ModuleLoader__.load({
645
658
  codingPlanAnthropic: 'Anthropic (Claude Pro/Max)',
646
659
  codingPlanZai: 'Z.ai / Zhipu GLM Coding Plan',
647
660
  codingPlanMinimax: 'MiniMax Token Plan',
661
+ codingPlanMinimaxTitle: 'MiniMax Plan',
662
+ codingPlanRemain5h: '5h',
663
+ codingPlanRemain7d: '7d',
648
664
  enableCodingPlan: 'Enable quota queries',
649
665
  codingPlanKeyLabel: 'API key (optional; empty = auto-detect)',
650
666
  refreshCodingPlan: 'Refresh',
@@ -2027,6 +2043,86 @@ window.__ModuleLoader__.load({
2027
2043
  wide ? view.body : el('div', { className: 'cm-bbox-rail cm-num' }, view.rail)))
2028
2044
  }
2029
2045
 
2046
+ /** MiniMax Token Plan:percent 为已用%,界面按余量(100-已用)展示。 */
2047
+ function miniMaxRemainPct(win) {
2048
+ if (win === null || typeof win !== 'object' || typeof win.percent !== 'number') return null
2049
+ return Math.max(0, Math.min(100, 100 - Number(win.percent)))
2050
+ }
2051
+
2052
+ function miniMaxWindowsOf(windows) {
2053
+ const map = windows !== null && typeof windows === 'object' ? windows : {}
2054
+ return {
2055
+ five: map['5h'] ?? null,
2056
+ seven: map['7d'] ?? map.week ?? map.weekly ?? null,
2057
+ }
2058
+ }
2059
+
2060
+ function miniMaxRemainLevel(remain) {
2061
+ if (remain === null) return 'ok'
2062
+ if (remain <= 0) return 'over'
2063
+ if (remain <= 20) return 'warn'
2064
+ return 'ok'
2065
+ }
2066
+
2067
+ function miniMaxResetText(win, t) {
2068
+ if (!win || typeof win.resetsAt !== 'string' || win.resetsAt.length === 0) return ''
2069
+ return t('goResetAt', { time: new Date(win.resetsAt).toLocaleString() })
2070
+ }
2071
+
2072
+ function miniMaxRow(label, win) {
2073
+ const remain = miniMaxRemainPct(win)
2074
+ const pct = remain === null ? 0 : remain
2075
+ const level = miniMaxRemainLevel(remain)
2076
+ return {
2077
+ level,
2078
+ remain,
2079
+ row: el('div', { className: 'cm-mm-row' + (level === 'ok' ? '' : ' ' + level) },
2080
+ el('span', { className: 'cm-bbox-label' }, label),
2081
+ el('div', { className: 'cm-bbox-bar' },
2082
+ el('div', { className: 'cm-bbox-fill', style: { width: Math.min(100, pct) + '%' } })),
2083
+ el('span', { className: 'cm-bbox-pct cm-num' }, remain === null ? '—' : Math.round(pct) + '%')),
2084
+ }
2085
+ }
2086
+
2087
+ function MiniMaxPlanCard(props) {
2088
+ const { five, seven, fetchedAt, t, wide } = props
2089
+ const fiveView = miniMaxRow(t('codingPlanRemain5h'), five)
2090
+ const sevenView = miniMaxRow(t('codingPlanRemain7d'), seven)
2091
+ const level = fiveView.level === 'over' || sevenView.level === 'over' ? 'over'
2092
+ : fiveView.level === 'warn' || sevenView.level === 'warn' ? 'warn' : 'ok'
2093
+ const lineOf = (label, win, view) => {
2094
+ const pct = view.remain === null ? '—' : Math.round(view.remain) + '%'
2095
+ const reset = miniMaxResetText(win, t)
2096
+ return label + ' ' + pct + (reset ? ' · ' + reset : '')
2097
+ }
2098
+ const detail = [
2099
+ t('codingPlanMinimaxTitle'),
2100
+ lineOf(t('codingPlanRemain5h'), five, fiveView),
2101
+ lineOf(t('codingPlanRemain7d'), seven, sevenView),
2102
+ fetchedAt > 0 ? t('goQuotaFetchedAt', { time: new Date(fetchedAt).toLocaleTimeString() }) : null,
2103
+ ].filter(Boolean).join('; ')
2104
+ const body = el(Fragment, null,
2105
+ el('div', { className: 'cm-mm-title' }, t('codingPlanMinimaxTitle')),
2106
+ fiveView.row,
2107
+ sevenView.row)
2108
+ const rail = el(Fragment, null,
2109
+ el('div', { className: 'cm-bbox-rail cm-num' }, fiveView.remain === null ? '—' : Math.round(fiveView.remain) + '%'),
2110
+ el('div', { className: 'cm-bbox-rail cm-num' }, sevenView.remain === null ? '—' : Math.round(sevenView.remain) + '%'))
2111
+ return el(Tooltip, { label: detail, side: 'right', delayMs: 300 },
2112
+ el('div', { className: 'cm-bbox cm-mm' + (level === 'ok' ? '' : ' ' + level) + (wide === false ? ' rail' : '') },
2113
+ wide === false ? rail : body))
2114
+ }
2115
+
2116
+ function MiniMaxPlanBox(props) {
2117
+ const { state, wide } = props
2118
+ const live = state.codingPlans?.minimax
2119
+ const t = makeT(resolveLocale(state.config?.locale))
2120
+ if (!live || live.status !== 'ok') return null
2121
+ const { five, seven } = miniMaxWindowsOf(live.windows)
2122
+ if (five == null && seven == null) return null
2123
+ return el(MiniMaxPlanCard, { five, seven, fetchedAt: live.fetchedAt, t, wide })
2124
+ }
2125
+
2030
2126
  function BudgetBoxContent(props) {
2031
2127
  const { state, wide } = props
2032
2128
  const today = state.today
@@ -2140,14 +2236,20 @@ window.__ModuleLoader__.load({
2140
2236
  const goOk = (config.goQuota?.enabled !== false)
2141
2237
  && (config.goQuota?.display === 'sidebar' || config.goQuota?.display === 'both')
2142
2238
  && state.goQuota?.status === 'ok' && state.goQuota?.[goMainKey] !== null
2239
+ const mmLive = state.codingPlans?.minimax
2240
+ const mmWins = miniMaxWindowsOf(mmLive?.windows)
2241
+ const mmOk = config.codingPlans?.minimax?.enabled === true
2242
+ && mmLive?.status === 'ok'
2243
+ && (mmWins.five != null || mmWins.seven != null)
2143
2244
  const budgetOn = (config.budget ?? {}).enabled === true
2144
2245
  const showToday = config.sidebar !== false
2145
- if (!showBalance && !showCustomBalance && !goOk && !budgetOn && !showToday) return null
2246
+ if (!showBalance && !showCustomBalance && !goOk && !mmOk && !budgetOn && !showToday) return null
2146
2247
  const nodes = []
2147
2248
  if (showBalanceBar) nodes.push(el(BalanceBox, { state, wide }))
2148
2249
  else if (showBalance) nodes.push(el(BalanceRowContent, { state, wide }))
2149
2250
  if (showCustomBalanceBar) nodes.push(el(CustomBalanceBox, { state, wide }))
2150
2251
  else if (showCustomBalance) nodes.push(el(CustomBalanceRowContent, { state, wide }))
2252
+ if (mmOk) nodes.push(el(MiniMaxPlanBox, { state, wide }))
2151
2253
  if (goOk && budgetOn && wide) {
2152
2254
  // 同时出现:合并为一张卡片(Go 在上、预算在下,细分隔线),各自保留预警色与自己的详细信息开关。
2153
2255
  const goView = goBoxBody(state, config, t)
@@ -3151,7 +3253,15 @@ window.__ModuleLoader__.load({
3151
3253
  : live.status === 'ok'
3152
3254
  ? el('div', { className: 'cm-go-list' },
3153
3255
  Object.keys(windows).length > 0
3154
- ? Object.entries(windows).map(([name, win]) => windowRow(name, win))
3256
+ ? (id === 'minimax' && (miniMaxWindowsOf(windows).five != null || miniMaxWindowsOf(windows).seven != null)
3257
+ ? el(MiniMaxPlanCard, {
3258
+ five: miniMaxWindowsOf(windows).five,
3259
+ seven: miniMaxWindowsOf(windows).seven,
3260
+ fetchedAt: live.fetchedAt,
3261
+ t,
3262
+ wide: true,
3263
+ })
3264
+ : Object.entries(windows).map(([name, win]) => windowRow(name, win)))
3155
3265
  : el('div', { className: 'cm-bal-line' }, t('codingPlanNotQueried')),
3156
3266
  el('div', { className: 'cm-go-time' }, t('goQuotaFetchedAt', {
3157
3267
  time: live.fetchedAt > 0 ? new Date(live.fetchedAt).toLocaleTimeString() : '—',
@@ -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
- * 解析 MiniMax 用量响应。兼容三种官方形态:
148
- * - Token Plan(2026-08 现行,issue #20):GET https://www.minimaxi.com|io/v1/token_plan/remains
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
- export function parseMiniMaxRemains(data) {
156
- if (data === null || typeof data !== 'object') return null
157
- const windows = {}
158
- const payload = data.data !== null && typeof data.data === 'object' && !Array.isArray(data.data) ? data.data : data
159
- // 平铺窗口字段(现行形态,issue #20):优先计数推导,其次剩余百分比反推。
160
- const clampPct = p => Math.max(0, Math.min(100, Math.round(p * 10) / 10))
161
- const usedPercentOf = (totalKey, usedKey, remainKey, remainPctKey) => {
162
- const total = Number(payload[totalKey])
163
- const used = Number(payload[usedKey])
164
- const remain = Number(payload[remainKey])
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
- // status=3 = 不限量:进度条无意义,跳过该窗(与 OpenClaw 同策略)。
175
- if (Number(payload.current_interval_status) !== 3) {
176
- const intervalPct = usedPercentOf('current_interval_total_count', 'current_interval_usage_count', 'current_interval_remain_count', 'current_interval_remaining_percent')
177
- if (intervalPct !== null) {
178
- windows['5h'] = { percent: clampPct(intervalPct), resetsAt: normalizeResetAt(payload.end_time ?? payload.reset_time ?? payload.next_reset_time) }
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(payload.current_weekly_status) !== 3) {
182
- const weeklyPct = usedPercentOf('current_weekly_total_count', 'current_weekly_usage_count', 'current_weekly_remain_count', 'current_weekly_remaining_percent')
183
- if (weeklyPct !== null) {
184
- windows.week = { percent: clampPct(weeklyPct), resetsAt: normalizeResetAt(payload.weekly_end_time) }
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
- if (Object.keys(windows).length > 0) return windows
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-cost-meter",
3
- "version": "1.5.6",
3
+ "version": "1.5.7",
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",