deepseek-harness-wallet 0.3.3 → 0.3.5

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,17 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## 0.3.5 - 2026-08-27
6
+
7
+ - 修复 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.
8
+ - 增加新旧字段共存时优先保留信息更完整的额度数据,并补充接口归一化与回归测试;当前 126/126 测试通过。 / Prefer the richer quota record when old and new names coexist, with normalization and regression coverage; 126/126 tests pass.
9
+
10
+ ## 0.3.4 - 2026-08-25
11
+
12
+ - 修复 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).
13
+ - 新增受白名单和长度限制的 `/api/wallet/preferences` 读写接口,拒绝未知键和敏感字段;偏好沿用账本的主备恢复、失败锁写和退出前刷新机制。 / Added an allowlisted, length-bounded `/api/wallet/preferences` endpoint that rejects unknown or sensitive fields and inherits the ledger's backup recovery, fail-closed writes, and shutdown flush behavior.
14
+ - 新增宿主完整重载、Desktop 本地键丢失恢复、恶意字段拒绝和 keepalive 双写回归测试;当前 123/123 测试通过。 / Added host-reload persistence, missing-Desktop-key hydration, malicious-field rejection, and keepalive dual-write regressions; 123/123 tests pass.
15
+
5
16
  ## 0.3.3 - 2026-08-25
6
17
 
7
18
  - 新增通用官方套餐适配器契约,并首批接入 Z.ai Coding Plan 全球区与中国区;每个来源固定绑定官方域名和独立的 DSH 凭据引用,浏览器不会收到 Key、接口路径或原始响应。 / Added a generic official subscription-plan adapter contract with initial Z.ai Coding Plan Global and China adapters; each source is pinned to its official domain and its own DSH credential reference, while the browser receives no key, endpoint path, or raw response.
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.3 is the current stable release on GitHub and npm.
17
+ > **Version:** v0.3.5 is the current stable release on GitHub and npm.
18
18
 
19
19
  > If DeepSeek Harness Control Center helps you, please consider leaving a ⭐ Star. Thank you!
20
20
 
@@ -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.3):
76
+ From npm (stable v0.3.5):
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.3 source):
82
+ or from GitHub `main` (current v0.3.5 source):
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.3'
36
+ const PLUGIN_VERSION = '0.3.5'
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
@@ -49,7 +49,7 @@ const ACCOUNTS_CRYPTO_VERSION = 1
49
49
  const CREDENTIAL_REF = 'DEEPSEEK_API_KEY'
50
50
  const DEFAULT_THRESHOLD = 5
51
51
  const BALANCE_REFRESH_MS = 60_000
52
- const STORE_VERSION = 4
52
+ const STORE_VERSION = 5
53
53
  const HISTORY_VERSION = 1
54
54
  const HISTORY_RETENTION_DAYS = 365
55
55
  const HISTORY_MAX_EVENTS = 20_000
@@ -59,6 +59,22 @@ const PLAN_REFRESH_MS = 5 * 60_000
59
59
  const PLAN_STALE_MS = 15 * 60_000
60
60
  const PLAN_REFRESH_TIMEOUT_MS = 12_000
61
61
  const PLAN_RESPONSE_MAX_BYTES = 1_000_000
62
+ const UI_PREFERENCE_KEYS = new Set([
63
+ 'dshw-chip-style-v1',
64
+ 'dshw-chip-balance-only-v1',
65
+ 'dshw-data-visibility-v1',
66
+ 'dshw-chip-scale-v1',
67
+ 'dshw-completion-notify-v1',
68
+ 'dshw-low-blink-v1',
69
+ 'dshw-peakring-v1',
70
+ 'dshw-peak-orient-v1',
71
+ 'dshw-peak-recharge-v1',
72
+ 'dshw-peak-scale-v1',
73
+ 'dshw-peak-dock-v1',
74
+ 'dshw-peaknotify-v1',
75
+ 'dshw-permanent-delete-v1',
76
+ ])
77
+ const UI_PREFERENCE_VALUE_MAX = 500
62
78
 
63
79
  // Beijing (UTC+8, no DST) weekday peak windows: 09:00-12:00 and 14:00-18:00.
64
80
  // From 2026-08-23 00:00 Beijing, Saturday and Sunday are off-peak all day.
@@ -886,6 +902,17 @@ export function isOfficialProvider(provider, extraProviders) {
886
902
  return Array.isArray(extraProviders) && extraProviders.includes(provider)
887
903
  }
888
904
 
905
+ export function normalizeUiPreferences(value) {
906
+ const source = value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {}
907
+ const normalized = {}
908
+ for (const [key, entry] of Object.entries(source)) {
909
+ if (!UI_PREFERENCE_KEYS.has(key)) continue
910
+ if (typeof entry !== 'string' || entry.length > UI_PREFERENCE_VALUE_MAX || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(entry)) continue
911
+ normalized[key] = entry
912
+ }
913
+ return normalized
914
+ }
915
+
889
916
  function repairPlanProviderSessionUsage(sessions, historySource, atMs) {
890
917
  const rawEvents = historySource !== null && typeof historySource === 'object' && !Array.isArray(historySource)
891
918
  && historySource.events !== null && typeof historySource.events === 'object' && !Array.isArray(historySource.events)
@@ -949,6 +976,7 @@ export function normalizeStoreData(value, atMs = Date.now()) {
949
976
  officialProviders: normalizeProviderList(source.officialProviders),
950
977
  knownProviders: normalizeProviderList(source.knownProviders),
951
978
  plans: normalizePlanSnapshotCache(source.plans, atMs),
979
+ preferences: normalizeUiPreferences(source.preferences),
952
980
  }
953
981
  if (Object.hasOwn(source, 'history')) {
954
982
  normalized.history = normalizeHistory(source.history, atMs)
@@ -964,7 +992,7 @@ let usageStorageRecovered = false
964
992
  let usageStoreSkipBackupOnce = false
965
993
 
966
994
  function emptyStoreData() {
967
- return { version: STORE_VERSION, thresholds: { CNY: DEFAULT_THRESHOLD }, accountThresholds: {}, sessions: {}, officialProviders: [], knownProviders: [], plans: {}, history: emptyHistory() }
995
+ return { version: STORE_VERSION, thresholds: { CNY: DEFAULT_THRESHOLD }, accountThresholds: {}, sessions: {}, officialProviders: [], knownProviders: [], plans: {}, preferences: {}, history: emptyHistory() }
968
996
  }
969
997
 
970
998
  function readStoreFile(path) {
@@ -2025,6 +2053,27 @@ export function apply(ctx, config) {
2025
2053
  return json(res, 200, { ok: true, pricing: pricingSnapshot() })
2026
2054
  },
2027
2055
  })
2056
+ const disposePreferences = ctx.webServer.register({
2057
+ kind: 'exact',
2058
+ path: '/api/wallet/preferences',
2059
+ handler: async (req, res) => {
2060
+ if (req.method === 'GET') return json(res, 200, { ok: true, entries: { ...store.preferences } })
2061
+ if (req.method !== 'POST') return json(res, 405, { ok: false, error: 'method-not-allowed' })
2062
+ if (usageStorageLocked) return json(res, 423, { ok: false, error: 'usage-storage-locked' })
2063
+ const body = await readBody(req)
2064
+ const entries = body && body.entries
2065
+ if (entries === null || typeof entries !== 'object' || Array.isArray(entries)) {
2066
+ return json(res, 400, { ok: false, error: 'entries object is required' })
2067
+ }
2068
+ const keys = Object.keys(entries)
2069
+ if (keys.length > UI_PREFERENCE_KEYS.size) return json(res, 400, { ok: false, error: 'invalid-preferences' })
2070
+ const patch = normalizeUiPreferences(entries)
2071
+ if (Object.keys(patch).length !== keys.length) return json(res, 400, { ok: false, error: 'invalid-preferences' })
2072
+ store.preferences = normalizeUiPreferences({ ...store.preferences, ...patch })
2073
+ scheduleSave(ctx.logger)
2074
+ return json(res, 200, { ok: true, entries: { ...store.preferences } })
2075
+ },
2076
+ })
2028
2077
  const disposeThreshold = ctx.webServer.register({
2029
2078
  kind: 'exact',
2030
2079
  path: '/api/wallet/threshold',
@@ -2226,6 +2275,7 @@ historyEventCount = store.history && store.history.events ? Object.keys(store.hi
2226
2275
  disposeSnapshot()
2227
2276
  disposeHealth()
2228
2277
  disposePricingRefresh()
2278
+ disposePreferences()
2229
2279
  disposeThreshold()
2230
2280
  disposeRefresh()
2231
2281
  disposeClear()
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.3'
26
+ var WALLET_VERSION = '0.3.5'
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'
@@ -52,6 +52,12 @@ window.__ModuleLoader__.load({
52
52
  var CHIP_EDGE_MARGIN = 2
53
53
  var CHIP_EDGE_SNAP = 32
54
54
  var CHIP_HOME_SNAP = 96
55
+ var BACKED_PREFERENCE_KEYS = new Set([
56
+ 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
+ PEAK_RECHARGE_KEY, PEAK_SCALE_KEY, PEAK_DOCK_KEY, PEAK_NOTIFY_KEY,
59
+ PERMANENT_DELETE_KEY
60
+ ])
55
61
 
56
62
  /**
57
63
  * Platform-specific behavior lives here. Desktop wrappers may define
@@ -88,10 +94,12 @@ window.__ModuleLoader__.load({
88
94
  try {
89
95
  nativeStorage.setItem(key, value)
90
96
  delete memoryOnly[key]
97
+ queuePersistentPreference(key, value)
91
98
  return
92
99
  } catch (e) { /* fall through to the memory-only marker */ }
93
100
  }
94
101
  memoryOnly[key] = true
102
+ queuePersistentPreference(key, value)
95
103
  },
96
104
  removeItem: function (key) {
97
105
  delete memory[key]
@@ -293,6 +301,116 @@ window.__ModuleLoader__.load({
293
301
  }
294
302
 
295
303
  var compatibility = createCompatibilityAdapter(window)
304
+ var preferenceBackupQueue = Object.create(null)
305
+ var preferenceBackupTimer = null
306
+ var preferenceBackupRetries = 0
307
+ var preferenceHydrationPromise = null
308
+ var preferenceTouchedKeys = new Set()
309
+
310
+ function queuePersistentPreference(key, value) {
311
+ if (!BACKED_PREFERENCE_KEYS.has(key) || typeof value !== 'string') return
312
+ preferenceTouchedKeys.add(key)
313
+ preferenceBackupQueue[key] = value
314
+ preferenceBackupRetries = 0
315
+ if (preferenceBackupTimer === null) {
316
+ if (typeof window.setTimeout === 'function') preferenceBackupTimer = window.setTimeout(flushPersistentPreferences, 80)
317
+ else void flushPersistentPreferences()
318
+ }
319
+ }
320
+
321
+ function flushPersistentPreferences() {
322
+ if (preferenceBackupTimer !== null) {
323
+ if (typeof window.clearTimeout === 'function') window.clearTimeout(preferenceBackupTimer)
324
+ preferenceBackupTimer = null
325
+ }
326
+ var entries = preferenceBackupQueue
327
+ preferenceBackupQueue = Object.create(null)
328
+ if (Object.keys(entries).length === 0 || typeof fetch !== 'function') return Promise.resolve(null)
329
+ return fetch('/api/wallet/preferences', {
330
+ method: 'POST',
331
+ headers: { 'content-type': 'application/json' },
332
+ body: JSON.stringify({ entries: entries }),
333
+ keepalive: true
334
+ }).then(function (response) {
335
+ if (!response || response.ok === false) throw new Error('preference-backup-failed')
336
+ preferenceBackupRetries = 0
337
+ return typeof response.json === 'function' ? response.json() : null
338
+ }).catch(function () {
339
+ Object.keys(entries).forEach(function (key) {
340
+ if (!Object.hasOwn(preferenceBackupQueue, key)) preferenceBackupQueue[key] = entries[key]
341
+ })
342
+ if (preferenceBackupRetries < 3 && preferenceBackupTimer === null && typeof window.setTimeout === 'function') {
343
+ preferenceBackupRetries += 1
344
+ preferenceBackupTimer = window.setTimeout(flushPersistentPreferences, preferenceBackupRetries * 750)
345
+ }
346
+ return null
347
+ })
348
+ }
349
+
350
+ function localPreferenceEntries() {
351
+ var entries = {}
352
+ BACKED_PREFERENCE_KEYS.forEach(function (key) {
353
+ var value = compatibility.storage.getItem(key)
354
+ if (typeof value === 'string') entries[key] = value
355
+ })
356
+ return entries
357
+ }
358
+
359
+ function canonicalPreferenceEntries(value) {
360
+ var entries = Object.assign({}, value || {})
361
+ if (entries[CHIP_STYLE_KEY] === 'text') {
362
+ entries[CHIP_STYLE_KEY] = 'standard'
363
+ if (!Object.hasOwn(entries, CHIP_BALANCE_ONLY_KEY)) entries[CHIP_BALANCE_ONLY_KEY] = 'true'
364
+ }
365
+ return entries
366
+ }
367
+
368
+ function hydratePersistentPreferences() {
369
+ if (preferenceHydrationPromise !== null) return preferenceHydrationPromise
370
+ if (typeof fetch !== 'function') return Promise.resolve(null)
371
+ preferenceHydrationPromise = fetch('/api/wallet/preferences').then(function (response) {
372
+ if (!response || response.ok === false || typeof response.json !== 'function') throw new Error('preference-read-failed')
373
+ return response.json()
374
+ }).then(function (payload) {
375
+ var serverEntries = canonicalPreferenceEntries(payload && payload.ok && payload.entries && typeof payload.entries === 'object' ? payload.entries : {})
376
+ var rawLocalEntries = localPreferenceEntries()
377
+ var localEntries = canonicalPreferenceEntries(rawLocalEntries)
378
+ var merged = {}
379
+ Object.keys(serverEntries).forEach(function (key) {
380
+ if (BACKED_PREFERENCE_KEYS.has(key) && typeof serverEntries[key] === 'string') merged[key] = serverEntries[key]
381
+ })
382
+ Object.keys(localEntries).forEach(function (key) {
383
+ if (!Object.hasOwn(merged, key) || preferenceTouchedKeys.has(key)) merged[key] = localEntries[key]
384
+ })
385
+ Object.keys(merged).forEach(function (key) {
386
+ if (!Object.hasOwn(rawLocalEntries, key) || rawLocalEntries[key] !== merged[key]) compatibility.storage.setItem(key, merged[key])
387
+ else queuePersistentPreference(key, merged[key])
388
+ })
389
+ compatibility.dispatch(SETTINGS_EVENT)
390
+ compatibility.dispatch(NOTIFY_CONFIG_EVENT)
391
+ compatibility.dispatch(PEAK_RING_EVENT)
392
+ compatibility.dispatch(PERMANENT_DELETE_EVENT)
393
+ return merged
394
+ }).catch(function () {
395
+ preferenceHydrationPromise = null
396
+ return null
397
+ })
398
+ return preferenceHydrationPromise
399
+ }
400
+
401
+ function installPersistentPreferenceSync() {
402
+ hydratePersistentPreferences()
403
+ var retries = typeof window.setTimeout === 'function' ? [1000, 4000].map(function (delay) {
404
+ return window.setTimeout(function () { if (preferenceHydrationPromise === null) hydratePersistentPreferences() }, delay)
405
+ }) : []
406
+ function flushOnHide() { void flushPersistentPreferences() }
407
+ if (typeof window.addEventListener === 'function') window.addEventListener('pagehide', flushOnHide)
408
+ return function () {
409
+ if (typeof window.clearTimeout === 'function') retries.forEach(function (timer) { window.clearTimeout(timer) })
410
+ if (typeof window.removeEventListener === 'function') window.removeEventListener('pagehide', flushOnHide)
411
+ void flushPersistentPreferences()
412
+ }
413
+ }
296
414
 
297
415
  function openOfficialRecharge() {
298
416
  var url = 'https://platform.deepseek.com/top_up'
@@ -1791,6 +1909,36 @@ window.__ModuleLoader__.load({
1791
1909
  try { return compatibility.storage.getItem(PEAK_DOCK_KEY) || 'sidebar' } catch (e) { return 'sidebar' }
1792
1910
  })
1793
1911
  var settingsModelSelection = useCurrentModelSelection(props.sessionsService, props.modelDirectories, props.modelAware === true)
1912
+ React.useEffect(function () {
1913
+ if (typeof window.addEventListener !== 'function') return
1914
+ function syncPreferencesFromStorage() {
1915
+ try {
1916
+ var savedVisibility = compatibility.storage.getItem(DATA_VISIBILITY_KEY)
1917
+ setVisibility(savedVisibility === null ? normalizeDataVisibility(null) : normalizeDataVisibility(JSON.parse(savedVisibility)))
1918
+ } catch (e) { /* ignore */ }
1919
+ try { setChipStyle(normalizeChipStyle(compatibility.storage.getItem(CHIP_STYLE_KEY))) } catch (e) { /* ignore */ }
1920
+ try { setBalanceOnly(readBalanceOnly()) } catch (e) { /* ignore */ }
1921
+ try { setScale(normalizeChipScale(compatibility.storage.getItem(CHIP_SCALE_KEY))) } catch (e) { /* ignore */ }
1922
+ try {
1923
+ var notify = readNotifyConfig()
1924
+ setNotifyEnabled(notify.enabled)
1925
+ setNotifyTimeout(notify.timeout === 0 ? 'keep' : String(notify.timeout))
1926
+ } catch (e) { /* ignore */ }
1927
+ try { setLowBlinkEnabled(compatibility.storage.getItem(LOW_BLINK_KEY) !== 'false') } catch (e) { /* ignore */ }
1928
+ try { setPdEnabled(compatibility.storage.getItem(PERMANENT_DELETE_KEY) === 'true') } catch (e) { /* ignore */ }
1929
+ try { setRingEnabled(compatibility.storage.getItem(PEAK_RING_KEY) !== 'false') } catch (e) { /* ignore */ }
1930
+ try { setPeakNotifyEnabled(compatibility.storage.getItem(PEAK_NOTIFY_KEY) === 'true') } catch (e) { /* ignore */ }
1931
+ try { setPeakOrient(compatibility.storage.getItem(PEAK_ORIENT_KEY) || 'horizontal') } catch (e) { /* ignore */ }
1932
+ try { setPeakRecharge(compatibility.storage.getItem(PEAK_RECHARGE_KEY) !== 'false') } catch (e) { /* ignore */ }
1933
+ try {
1934
+ var savedPeakScale = Number.parseFloat(compatibility.storage.getItem(PEAK_SCALE_KEY))
1935
+ setPeakScale(Number.isFinite(savedPeakScale) ? Math.min(1.2, Math.max(1, savedPeakScale)) : 1)
1936
+ } catch (e) { /* ignore */ }
1937
+ try { setPeakDock(compatibility.storage.getItem(PEAK_DOCK_KEY) || 'sidebar') } catch (e) { /* ignore */ }
1938
+ }
1939
+ window.addEventListener(SETTINGS_EVENT, syncPreferencesFromStorage)
1940
+ return function () { window.removeEventListener(SETTINGS_EVENT, syncPreferencesFromStorage) }
1941
+ }, [])
1794
1942
  useLayoutEffect(function () {
1795
1943
  var node = settingsSectionRef.current
1796
1944
  var dialog = node && typeof node.closest === 'function' ? node.closest('[role="dialog"]') : null
@@ -4968,6 +5116,7 @@ window.__ModuleLoader__.load({
4968
5116
  ctx.inject(['sessions'], function (scope) {
4969
5117
  scope.effect(function () { return installCompletionNotifier(scope) }, 'dsh-wallet: completion notifications')
4970
5118
  scope.effect(function () { return installMaidModelMenuGuard() }, 'dsh-wallet: maid model-menu repaint guard')
5119
+ scope.effect(function () { return installPersistentPreferenceSync() }, 'dsh-wallet: durable preference sync')
4971
5120
  })
4972
5121
  ctx.inject(['slots', 'conversation', 'modelDirectories'], function (scope) {
4973
5122
  scope.effect(function () {
@@ -5039,6 +5188,8 @@ window.__ModuleLoader__.load({
5039
5188
  selectBalanceInfo: selectBalanceInfo,
5040
5189
  balanceErrorText: balanceErrorText,
5041
5190
  installCompletionNotifier: installCompletionNotifier,
5191
+ hydratePersistentPreferences: hydratePersistentPreferences,
5192
+ flushPersistentPreferences: flushPersistentPreferences,
5042
5193
  normalizeDataVisibility: normalizeDataVisibility,
5043
5194
  normalizeNotifyConfig: normalizeNotifyConfig,
5044
5195
  normalizeChipLayout: normalizeChipLayout,
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.3",
4
+ "version": "0.3.5",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "exports": {