deepseek-harness-wallet 0.3.4 → 0.3.6

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/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## 0.3.6 - 2026-08-28
6
+
7
+ - 峰谷时钟新增明确的「透明 / 实色」背景选择,不加入自动模式;透明模式常态不铺底,悬停时使用主题实色,实色模式保持原有卡片背景。 / Added an explicit peak-clock background choice between transparent and solid, without an automatic mode; transparent mode removes the normal fill and uses the themed solid hover surface, while solid mode keeps the existing card background.
8
+ - 设置页和时钟控制面板共享该偏好,并同步保存到宿主钱包存储;覆盖默认、悬浮、竖排、浅色和深色主题,避免透明卡片文字或悬浮卡片背景异常。 / The setting is shared by the settings page and clock control panel and backed by the host wallet store; default, floating, vertical, light, and dark-theme paths are covered so transparent cards remain readable and floating cards keep a valid surface.
9
+
10
+ ## 0.3.5 - 2026-08-27
11
+
12
+ - 修复 BigModel / Z.ai 中国区 Coding Plan 接口将 `TOKENS_LIMIT` 改名为 `CREDIT_LIMIT` 后被误判为“套餐接口返回格式已变化”的问题;兼容新旧字段,支持只有新额度字段的响应,并在接口提供时显示已用/总量、剩余量和重置时间。 / Fixed BigModel / Z.ai China Coding Plan responses being misclassified as “the plan response format changed” after `TOKENS_LIMIT` was renamed to `CREDIT_LIMIT`; both names are supported, credit-only responses are accepted, and available used/total, remaining, and reset values are shown when provided.
13
+ - 增加新旧字段共存时优先保留信息更完整的额度数据,并补充接口归一化与回归测试;当前 126/126 测试通过。 / Prefer the richer quota record when old and new names coexist, with normalization and regression coverage; 126/126 tests pass.
14
+
5
15
  ## 0.3.4 - 2026-08-25
6
16
 
7
17
  - 修复 DSH Desktop 重启后部分插件设置因 WebView origin/localStorage 命名空间变化而丢失的问题:显示内容、输入框标签、仅显示余额、标签比例、提醒、峰谷卡和永久删除偏好现在会即时写入浏览器并同步备份到本地 `wallet.json`;宿主已有值优先恢复,宿主缺失的旧设置才从浏览器迁移,避免旧标签页在重启后反向覆盖新选择。 / Fixed wallet preferences disappearing after a DSH Desktop restart when the WebView origin/localStorage namespace changes: display, composer-label, primary-value, scale, reminder, peak-card, and permanent-delete preferences now write locally and mirror into `wallet.json`; existing host values restore authoritatively, while browser values migrate only for host-missing keys so stale tabs cannot undo newer choices after restart (Fixes #31).
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  > A local-first companion that keeps account status, per-conversation usage, completion reminders, official recharge, flexible layout, and host-gated session controls beside the DSH composer.
16
16
 
17
- > **Version:** v0.3.4 is the current stable release on GitHub and npm.
17
+ > **Version:** v0.3.6 is the current local patch candidate; v0.3.5 remains the published stable release on GitHub and npm until release approval.
18
18
 
19
19
  > If DeepSeek Harness Control Center helps you, please consider leaving a ⭐ Star. Thank you!
20
20
 
@@ -34,7 +34,7 @@
34
34
  - **Third-party total** — current-session tokens (input / cache read / output). No balance guessing, no cost math, zero configuration.
35
35
  - **Provider classification** — observed wrapper routes appear in the settings page; opted-in routes join the official token/cost bucket for subsequent calls and are priced with the official table. Existing history is not retroactively reclassified.
36
36
  - **Click the chip** to open the detail panel: correctly formatted per-currency balances, cost and token splits, a freely editable low-balance threshold for the active account and currency (two decimals, persisted per account; alerts never mix currencies), manual refresh, and a jump to the official recharge page (first click shows the domain for confirmation — anti-phishing).
37
- - **Move, dock, and scale** — drag the chip freely, preview nearby snap targets, use compact horizontal or vertical layouts, adjust its scale from the control panel, and show official or third-party data independently. The choices are remembered locally.
37
+ - **Move, dock, and scale** — drag the chip freely, preview nearby snap targets, use compact horizontal or vertical layouts, adjust its scale from the control panel, and show official or third-party data independently. The peak clock background can be explicitly set to transparent (solid on hover) or solid; there is no automatic mode. The choices are remembered locally.
38
38
  - **Composer-label controls and skin compatibility** — independently show/hide the composer label or reduce it to the primary remaining value (DeepSeek balance, Z.ai five-hour quota) without disabling reminders, settings, plan monitoring, or history. The standard chip also resists broad skin button rules and aligns with maid-atelier's 38px navy-and-gold composer toolbar.
39
39
  - **Floating window mode** — detach the detail panel into a draggable window with a remembered position, or minimize it directly to a freely movable dot; the dot turns red below the threshold.
40
40
  - **Completion reminders** — optionally notify when a conversation finishes, with persistent or timed modes, queueing and deduplication for simultaneous completions, cross-tab coordination, and an in-page fallback when system notifications are unavailable.
@@ -73,13 +73,13 @@ Details: [compatibility](#browser-desktop-and-os-compatibility) · [data and tru
73
73
 
74
74
  ## Install
75
75
 
76
- From npm (stable v0.3.4):
76
+ From npm (published stable v0.3.5; v0.3.6 is the local patch candidate until release approval):
77
77
 
78
78
  ```sh
79
79
  dsh plugin --profile web add deepseek-harness-wallet
80
80
  ```
81
81
 
82
- or from GitHub `main` (current v0.3.4 source):
82
+ or from GitHub `main` (published v0.3.5 source until release approval):
83
83
 
84
84
  ```sh
85
85
  dsh plugin --profile web add github:feibi-mochi/deepseek-harness-control-center
package/index.js CHANGED
@@ -33,7 +33,7 @@ const OFFICIAL_PROVIDER = 'deepseek-official'
33
33
  // DeepSeek's official paid-API bucket by the wrapper-provider alias control.
34
34
  const PLAN_PROVIDER_IDS = new Set(PLAN_ADAPTERS.map((adapter) => adapter.provider))
35
35
  const RECHARGE_URL = 'https://platform.deepseek.com/top_up'
36
- const PLUGIN_VERSION = '0.3.4'
36
+ const PLUGIN_VERSION = '0.3.6'
37
37
  const PRICING_SOURCE_URL = 'https://api-docs.deepseek.com/zh-cn/quick_start/pricing/'
38
38
  const PRICING_SYNC_INTERVAL_MS = 6 * 60 * 60_000
39
39
  const PRICING_SYNC_TIMEOUT_MS = 8_000
@@ -68,6 +68,7 @@ const UI_PREFERENCE_KEYS = new Set([
68
68
  'dshw-low-blink-v1',
69
69
  'dshw-peakring-v1',
70
70
  'dshw-peak-orient-v1',
71
+ 'dshw-peak-background-v1',
71
72
  'dshw-peak-recharge-v1',
72
73
  'dshw-peak-scale-v1',
73
74
  'dshw-peak-dock-v1',
package/lib/client.js CHANGED
@@ -23,7 +23,7 @@ window.__ModuleLoader__.load({
23
23
 
24
24
  var POLL_MS = 15000
25
25
  // Keep in lockstep with package.json; a test enforces the sync.
26
- var WALLET_VERSION = '0.3.4'
26
+ var WALLET_VERSION = '0.3.6'
27
27
  var CONFIRM_KEY = 'dsh-wallet-recharge-confirmed'
28
28
  var CHIP_LAYOUT_KEY = 'dshw-chip-layout-v4'
29
29
  var PANEL_POS_KEY = 'dshw-panel-pos-v1'
@@ -37,6 +37,7 @@ window.__ModuleLoader__.load({
37
37
  var LOW_BLINK_KEY = 'dshw-low-blink-v1'
38
38
  var PEAK_RING_KEY = 'dshw-peakring-v1'
39
39
  var PEAK_ORIENT_KEY = 'dshw-peak-orient-v1'
40
+ var PEAK_BACKGROUND_KEY = 'dshw-peak-background-v1'
40
41
  var PEAK_RECHARGE_KEY = 'dshw-peak-recharge-v1'
41
42
  var PEAK_SCALE_KEY = 'dshw-peak-scale-v1'
42
43
  var PEAK_DOCK_KEY = 'dshw-peak-dock-v1'
@@ -54,7 +55,7 @@ window.__ModuleLoader__.load({
54
55
  var CHIP_HOME_SNAP = 96
55
56
  var BACKED_PREFERENCE_KEYS = new Set([
56
57
  CHIP_STYLE_KEY, CHIP_BALANCE_ONLY_KEY, DATA_VISIBILITY_KEY, CHIP_SCALE_KEY,
57
- NOTIFY_CONFIG_KEY, LOW_BLINK_KEY, PEAK_RING_KEY, PEAK_ORIENT_KEY,
58
+ NOTIFY_CONFIG_KEY, LOW_BLINK_KEY, PEAK_RING_KEY, PEAK_ORIENT_KEY, PEAK_BACKGROUND_KEY,
58
59
  PEAK_RECHARGE_KEY, PEAK_SCALE_KEY, PEAK_DOCK_KEY, PEAK_NOTIFY_KEY,
59
60
  PERMANENT_DELETE_KEY
60
61
  ])
@@ -643,6 +644,11 @@ window.__ModuleLoader__.load({
643
644
  '.dshw_footRing{width:100%;box-sizing:border-box;display:flex;align-items:center;gap:8px;margin:2px 0 8px;padding:7px 9px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.18));border-radius:11px;background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,rgba(127,127,127,.04)));color:var(--dsw-alias-label-primary,inherit);font-size:12.5px;cursor:grab;user-select:none;touch-action:none;transition:border-color .15s ease,background-color .15s ease}',
644
645
  'body[data-dsh-maid-atelier]:not([data-ds-dark-theme]) .dshw_footRing{--dsw-alias-label-primary:#f8f3e8;--dsw-alias-label-secondary:#d7def0;--dsw-alias-label-tertiary:#b5c1dd;--dsw-alias-border-l1:rgba(225,191,124,.62);--dsw-alias-border-l3:rgba(181,196,229,.34);--dsw-alias-interactive-bg-hover:rgba(41,61,111,.98);color:#f8f3e8;background:linear-gradient(145deg,rgba(34,55,106,.95),rgba(10,25,62,.97));border-color:rgba(225,191,124,.62);box-shadow:inset 0 0 0 2px rgba(5,15,45,.44),0 4px 11px rgba(1,7,24,.22)}',
645
646
  'body[data-dsh-maid-atelier][data-ds-dark-theme] .dshw_footRing{--dsw-alias-label-primary:#f8f3e8;--dsw-alias-label-secondary:#d7def0;--dsw-alias-label-tertiary:#b5c1dd;--dsw-alias-border-l1:rgba(211,180,119,.52);--dsw-alias-border-l3:rgba(181,196,229,.34);--dsw-alias-interactive-bg-hover:rgba(41,61,111,.96);color:#f8f3e8;background:rgba(18,32,70,.92);border-color:rgba(211,180,119,.52);box-shadow:inset 0 1px rgba(255,255,255,.07),0 4px 12px rgba(0,0,0,.24)}',
647
+ '.dshw_footRing[data-dshw-peak-background="transparent"]:not(.dshw_footRingFloating){background:transparent;box-shadow:none}',
648
+ '.dshw_footRing[data-dshw-peak-background="transparent"]:not(.dshw_footRingFloating):hover{background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,rgba(127,127,127,.04)));box-shadow:none}',
649
+ 'body[data-dsh-maid-atelier] .dshw_footRing[data-dshw-peak-background="transparent"]:not(.dshw_footRingFloating){background:transparent;box-shadow:none}',
650
+ 'body[data-dsh-maid-atelier]:not([data-ds-dark-theme]) .dshw_footRing[data-dshw-peak-background="transparent"]:not(.dshw_footRingFloating):hover{background:linear-gradient(145deg,rgba(34,55,106,.95),rgba(10,25,62,.97));box-shadow:inset 0 0 0 2px rgba(5,15,45,.44),0 4px 11px rgba(1,7,24,.22)}',
651
+ 'body[data-dsh-maid-atelier][data-ds-dark-theme] .dshw_footRing[data-dshw-peak-background="transparent"]:not(.dshw_footRingFloating):hover{background:rgba(18,32,70,.92);box-shadow:inset 0 1px rgba(255,255,255,.07),0 4px 12px rgba(0,0,0,.24)}',
646
652
  '.dshw_footRing:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.1));border-color:var(--dsw-alias-border-l2,rgba(127,127,127,.28))}',
647
653
  '.dshw_footRing.dshw_low{border-color:var(--dsw-alias-state-error-primary,#e5534b)}',
648
654
  '.dshw_footRingRail{width:42px;height:42px;min-height:42px;margin:3px 0 6px;padding:0;justify-content:center;border-radius:50%;background:transparent;border:none;cursor:pointer}',
@@ -987,6 +993,10 @@ window.__ModuleLoader__.load({
987
993
  return value === 'hidden' ? 'hidden' : 'standard'
988
994
  }
989
995
 
996
+ function normalizePeakBackground(value) {
997
+ return value === 'solid' ? 'solid' : 'transparent'
998
+ }
999
+
990
1000
  function readBalanceOnly() {
991
1001
  try {
992
1002
  var saved = compatibility.storage.getItem(CHIP_BALANCE_ONLY_KEY)
@@ -1896,6 +1906,9 @@ window.__ModuleLoader__.load({
1896
1906
  var [peakOrient, setPeakOrient] = React.useState(function () {
1897
1907
  try { return compatibility.storage.getItem(PEAK_ORIENT_KEY) || 'horizontal' } catch (e) { return 'horizontal' }
1898
1908
  })
1909
+ var [peakBackground, setPeakBackground] = React.useState(function () {
1910
+ try { return normalizePeakBackground(compatibility.storage.getItem(PEAK_BACKGROUND_KEY)) } catch (e) { return 'transparent' }
1911
+ })
1899
1912
  var [peakRecharge, setPeakRecharge] = React.useState(function () {
1900
1913
  try { return compatibility.storage.getItem(PEAK_RECHARGE_KEY) !== 'false' } catch (e) { return true }
1901
1914
  })
@@ -1929,6 +1942,7 @@ window.__ModuleLoader__.load({
1929
1942
  try { setRingEnabled(compatibility.storage.getItem(PEAK_RING_KEY) !== 'false') } catch (e) { /* ignore */ }
1930
1943
  try { setPeakNotifyEnabled(compatibility.storage.getItem(PEAK_NOTIFY_KEY) === 'true') } catch (e) { /* ignore */ }
1931
1944
  try { setPeakOrient(compatibility.storage.getItem(PEAK_ORIENT_KEY) || 'horizontal') } catch (e) { /* ignore */ }
1945
+ try { setPeakBackground(normalizePeakBackground(compatibility.storage.getItem(PEAK_BACKGROUND_KEY))) } catch (e) { /* ignore */ }
1932
1946
  try { setPeakRecharge(compatibility.storage.getItem(PEAK_RECHARGE_KEY) !== 'false') } catch (e) { /* ignore */ }
1933
1947
  try {
1934
1948
  var savedPeakScale = Number.parseFloat(compatibility.storage.getItem(PEAK_SCALE_KEY))
@@ -2095,6 +2109,14 @@ window.__ModuleLoader__.load({
2095
2109
  compatibility.dispatch(PEAK_RING_EVENT)
2096
2110
  }
2097
2111
 
2112
+ function persistPeakBackground(next) {
2113
+ next = normalizePeakBackground(next)
2114
+ setPeakBackground(next)
2115
+ try { compatibility.storage.setItem(PEAK_BACKGROUND_KEY, next) } catch (e) { /* ignore */ }
2116
+ compatibility.dispatch(SETTINGS_EVENT)
2117
+ compatibility.dispatch(PEAK_RING_EVENT)
2118
+ }
2119
+
2098
2120
  function persistPeakRecharge(next) {
2099
2121
  setPeakRecharge(next)
2100
2122
  try { compatibility.storage.setItem(PEAK_RECHARGE_KEY, String(next)) } catch (e) { /* ignore */ }
@@ -2488,6 +2510,18 @@ window.__ModuleLoader__.load({
2488
2510
  },
2489
2511
  React.createElement('option', { value: 'horizontal' }, '横向排列'),
2490
2512
  React.createElement('option', { value: 'vertical' }, '纵向排列'))),
2513
+ React.createElement('div', { className: 'dshw_settingChoice' },
2514
+ React.createElement('span', { className: 'dshw_settingChoiceCopy' },
2515
+ React.createElement('strong', null, '时钟背景'),
2516
+ React.createElement('span', null, peakBackground === 'solid' ? '侧边栏常态保持实色' : '侧边栏透明,悬停实色;悬浮保持实色')),
2517
+ React.createElement('select', {
2518
+ className: 'dshw_select',
2519
+ 'aria-label': '峰谷时钟背景',
2520
+ value: peakBackground,
2521
+ onChange: function (event) { persistPeakBackground(event.target.value) }
2522
+ },
2523
+ React.createElement('option', { value: 'transparent' }, '透明(悬停实色)'),
2524
+ React.createElement('option', { value: 'solid' }, '实色'))),
2491
2525
  React.createElement('div', { className: 'dshw_settingChoice' },
2492
2526
  React.createElement('span', { className: 'dshw_settingChoiceCopy' },
2493
2527
  React.createElement('strong', null, '时钟大小'),
@@ -2833,6 +2867,9 @@ window.__ModuleLoader__.load({
2833
2867
  var [orient, setOrient] = React.useState(function () {
2834
2868
  try { return compatibility.storage.getItem(PEAK_ORIENT_KEY) || 'horizontal' } catch (e) { return 'horizontal' }
2835
2869
  })
2870
+ var [peakBackground, setPeakBackground] = React.useState(function () {
2871
+ try { return normalizePeakBackground(compatibility.storage.getItem(PEAK_BACKGROUND_KEY)) } catch (e) { return 'transparent' }
2872
+ })
2836
2873
  var [showRecharge, setShowRecharge] = React.useState(function () {
2837
2874
  try { return compatibility.storage.getItem(PEAK_RECHARGE_KEY) !== 'false' } catch (e) { return true }
2838
2875
  })
@@ -2893,6 +2930,7 @@ window.__ModuleLoader__.load({
2893
2930
  try {
2894
2931
  setShown(compatibility.storage.getItem(PEAK_RING_KEY) !== 'false')
2895
2932
  setOrient(compatibility.storage.getItem(PEAK_ORIENT_KEY) || 'horizontal')
2933
+ setPeakBackground(normalizePeakBackground(compatibility.storage.getItem(PEAK_BACKGROUND_KEY)))
2896
2934
  setShowRecharge(compatibility.storage.getItem(PEAK_RECHARGE_KEY) !== 'false')
2897
2935
  var val = Number.parseFloat(compatibility.storage.getItem(PEAK_SCALE_KEY))
2898
2936
  setPeakScale(Number.isFinite(val) ? Math.min(1.2, Math.max(1.0, val)) : 1.0)
@@ -3132,6 +3170,12 @@ window.__ModuleLoader__.load({
3132
3170
  try { compatibility.storage.setItem(PEAK_ORIENT_KEY, val) } catch (e) { /* ignore */ }
3133
3171
  announcePrefs()
3134
3172
  }
3173
+ function updateBackground(val) {
3174
+ val = normalizePeakBackground(val)
3175
+ setPeakBackground(val)
3176
+ try { compatibility.storage.setItem(PEAK_BACKGROUND_KEY, val) } catch (e) { /* ignore */ }
3177
+ announcePrefs()
3178
+ }
3135
3179
  function updateScale(val) {
3136
3180
  val = Math.min(1.2, Math.max(1.0, Math.round(val * 20) / 20))
3137
3181
  setPeakScale(val)
@@ -3284,6 +3328,7 @@ window.__ModuleLoader__.load({
3284
3328
  var cardElement = React.createElement('div', {
3285
3329
  ref: cardNodeRef,
3286
3330
  className: containerClasses.join(' '),
3331
+ 'data-dshw-peak-background': peakBackground,
3287
3332
  style: Object.keys(cardStyle).length > 0 ? cardStyle : undefined,
3288
3333
  title: state.tip + ' · 余额 ' + balText + ' · ' + costLabel + ' ' + costText + (isRail ? ' · 点击前往官方充值' : ' · 点击展开专属控制面板 / 按住拖拽'),
3289
3334
  'aria-label': state.ariaText + ',余额 ' + balText + ',' + costLabel + ' ' + costText,
@@ -3406,6 +3451,17 @@ window.__ModuleLoader__.load({
3406
3451
  },
3407
3452
  React.createElement('option', { value: 'horizontal' }, '横向排列'),
3408
3453
  React.createElement('option', { value: 'vertical' }, '纵向排列'))),
3454
+ React.createElement('div', { className: 'dshw_peakRow' },
3455
+ React.createElement('span', { className: 'dshw_muted' }, '时钟背景'),
3456
+ React.createElement('select', {
3457
+ className: 'dshw_select',
3458
+ style: { height: '24px', fontSize: '11px' },
3459
+ 'aria-label': '控制面板时钟背景',
3460
+ value: peakBackground,
3461
+ onChange: function (e) { updateBackground(e.target.value) }
3462
+ },
3463
+ React.createElement('option', { value: 'transparent' }, '透明(悬停实色)'),
3464
+ React.createElement('option', { value: 'solid' }, '实色'))),
3409
3465
  React.createElement('div', { className: 'dshw_peakRow' },
3410
3466
  React.createElement('span', { className: 'dshw_muted' }, '时钟大小 (' + Math.round(peakScale * 100) + '%)'),
3411
3467
  React.createElement('span', { className: 'dshw_scaleControl' },
package/lib/plans.js CHANGED
@@ -8,8 +8,15 @@
8
8
 
9
9
  export const PLAN_CACHE_VERSION = 2
10
10
 
11
+ const TOKEN_LIMIT = Object.freeze({ id: 'tokens-5h', kind: 'tokens', window: '5h', amounts: false, reset: false })
12
+ const CREDIT_LIMIT = Object.freeze({ id: 'tokens-5h', kind: 'tokens', window: '5h', amounts: true, reset: true })
13
+
11
14
  const ZAI_LIMITS = Object.freeze({
12
- TOKENS_LIMIT: Object.freeze({ id: 'tokens-5h', kind: 'tokens', window: '5h' }),
15
+ // BigModel renamed this response type to CREDIT_LIMIT without changing
16
+ // the 5-hour model-credit semantics. Keep both names for old snapshots and
17
+ // new China-region responses.
18
+ TOKENS_LIMIT: TOKEN_LIMIT,
19
+ CREDIT_LIMIT,
13
20
  TIME_LIMIT: Object.freeze({ id: 'tools-month', kind: 'tools', window: 'month' }),
14
21
  })
15
22
 
@@ -81,9 +88,9 @@ function normalizedLimit(adapter, value, atMs) {
81
88
  if (value === null || typeof value !== 'object' || Array.isArray(value)) return null
82
89
  const descriptor = adapter.limitTypes[value.type]
83
90
  if (descriptor === undefined) return null
84
- const used = descriptor.kind === 'tools' ? finiteNonNegative(value.currentValue) : null
85
- const total = descriptor.kind === 'tools' ? finiteNonNegative(value.usage) : null
86
- const remaining = descriptor.kind === 'tools' ? finiteNonNegative(value.remaining) : null
91
+ const used = descriptor.amounts ? finiteNonNegative(value.currentValue) : descriptor.kind === 'tools' ? finiteNonNegative(value.currentValue) : null
92
+ const total = descriptor.amounts ? finiteNonNegative(value.usage) : descriptor.kind === 'tools' ? finiteNonNegative(value.usage) : null
93
+ const remaining = descriptor.amounts ? finiteNonNegative(value.remaining) : descriptor.kind === 'tools' ? finiteNonNegative(value.remaining) : null
87
94
  const percentages = percentagePair(value.percentage, used, total)
88
95
  return {
89
96
  id: descriptor.id,
@@ -93,18 +100,26 @@ function normalizedLimit(adapter, value, atMs) {
93
100
  used,
94
101
  total,
95
102
  remaining,
96
- resetAt: descriptor.kind === 'tools' ? resetTimestamp(value.nextResetTime, atMs) : null,
103
+ resetAt: descriptor.reset || descriptor.kind === 'tools' ? resetTimestamp(value.nextResetTime, atMs) : null,
97
104
  }
98
105
  }
99
106
 
107
+ function preferRicherLimit(next, current) {
108
+ if (current === undefined) return true
109
+ if (current.used === null && next.used !== null) return true
110
+ if (current.total === null && next.total !== null) return true
111
+ if (current.remaining === null && next.remaining !== null) return true
112
+ return current.resetAt === null && next.resetAt !== null
113
+ }
114
+
100
115
  function normalizeCachedLimit(value, atMs) {
101
116
  if (value === null || typeof value !== 'object' || Array.isArray(value)) return null
102
117
  if (!['tokens-5h', 'tools-month'].includes(value.id)) return null
103
118
  const kind = value.id === 'tokens-5h' ? 'tokens' : 'tools'
104
119
  const window = value.id === 'tokens-5h' ? '5h' : 'month'
105
- const used = kind === 'tools' ? finiteNonNegative(value.used) : null
106
- const total = kind === 'tools' ? finiteNonNegative(value.total) : null
107
- const remaining = kind === 'tools' ? finiteNonNegative(value.remaining) : null
120
+ const used = finiteNonNegative(value.used)
121
+ const total = finiteNonNegative(value.total)
122
+ const remaining = finiteNonNegative(value.remaining)
108
123
  const legacyUsedPercentage = value.usedPercentage ?? value.percentage
109
124
  const percentages = percentagePair(legacyUsedPercentage, used, total)
110
125
  return {
@@ -115,7 +130,7 @@ function normalizeCachedLimit(value, atMs) {
115
130
  used,
116
131
  total,
117
132
  remaining,
118
- resetAt: kind === 'tools' ? resetTimestamp(value.resetAt, atMs) : null,
133
+ resetAt: resetTimestamp(value.resetAt, atMs),
119
134
  }
120
135
  }
121
136
 
@@ -127,14 +142,12 @@ export function normalizePlanPayload(adapter, payload, atMs = Date.now()) {
127
142
  }
128
143
  const data = payload.data !== null && typeof payload.data === 'object' && !Array.isArray(payload.data) ? payload.data : null
129
144
  if (data === null || !Array.isArray(data.limits)) throw new Error('invalid-plan-response')
130
- const limits = []
131
- const seen = new Set()
145
+ const limitsById = new Map()
132
146
  for (const item of data.limits) {
133
147
  const limit = normalizedLimit(adapter, item, atMs)
134
- if (limit === null || seen.has(limit.id)) continue
135
- seen.add(limit.id)
136
- limits.push(limit)
148
+ if (limit !== null && preferRicherLimit(limit, limitsById.get(limit.id))) limitsById.set(limit.id, limit)
137
149
  }
150
+ const limits = [...limitsById.values()]
138
151
  limits.sort((left, right) => left.id === 'tokens-5h' ? -1 : right.id === 'tokens-5h' ? 1 : left.id.localeCompare(right.id))
139
152
  if (limits.length === 0) throw new Error('invalid-plan-response')
140
153
  return {
@@ -156,14 +169,12 @@ export function normalizePlanSnapshotCache(value, atMs = Date.now()) {
156
169
  ? Math.trunc(raw.fetchedAt)
157
170
  : null
158
171
  if (fetchedAt === null || !Array.isArray(raw.limits)) continue
159
- const limits = []
160
- const seen = new Set()
172
+ const limitsById = new Map()
161
173
  for (const item of raw.limits) {
162
174
  const limit = normalizeCachedLimit(item, atMs)
163
- if (limit === null || seen.has(limit.id)) continue
164
- seen.add(limit.id)
165
- limits.push(limit)
175
+ if (limit !== null && preferRicherLimit(limit, limitsById.get(limit.id))) limitsById.set(limit.id, limit)
166
176
  }
177
+ const limits = [...limitsById.values()]
167
178
  if (limits.length === 0) continue
168
179
  limits.sort((left, right) => left.id === 'tokens-5h' ? -1 : right.id === 'tokens-5h' ? 1 : left.id.localeCompare(right.id))
169
180
  out[adapter.id] = {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "deepseek-harness-wallet",
3
3
  "description": "Local-first DeepSeek Harness control center for balance monitoring, usage accounting, a 365-day local token ledger, Z.ai Coding Plan quota monitoring, official pricing sync, peak/off-peak status, encrypted multi-account switching, health checks, completion reminders, recharge, and host-gated session controls.",
4
- "version": "0.3.4",
4
+ "version": "0.3.6",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "exports": {