deepseek-harness-wallet 0.3.3 → 0.3.4

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,12 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## 0.3.4 - 2026-08-25
6
+
7
+ - 修复 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).
8
+ - 新增受白名单和长度限制的 `/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.
9
+ - 新增宿主完整重载、Desktop 本地键丢失恢复、恶意字段拒绝和 keepalive 双写回归测试;当前 123/123 测试通过。 / Added host-reload persistence, missing-Desktop-key hydration, malicious-field rejection, and keepalive dual-write regressions; 123/123 tests pass.
10
+
5
11
  ## 0.3.3 - 2026-08-25
6
12
 
7
13
  - 新增通用官方套餐适配器契约,并首批接入 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.4 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.4):
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.4 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.4'
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.4'
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/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.4",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "exports": {