deepseek-harness-wallet 0.2.4 → 0.2.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,14 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## 0.2.5 - 2026-08-23
6
+
7
+ - 修复浮动峰谷卡片在窗口缩放、卡片放大或历史坐标过期后可能跑出视口的问题;加载、缩放、排版切换和 resize 都会重新钳制位置。 / Fixed floating peak cards escaping the viewport after resize, scale changes, or stale saved coordinates; load, scale, layout changes, and resize now re-clamp the position.
8
+ - 恢复 Portal 挂载,确保自由浮动卡片和控制面板脱离侧边栏溢出裁剪。 / Restored Portal mounting so floating cards and the control panel escape sidebar overflow clipping.
9
+ - 完成主题回退清理,移除峰谷控制面板、浮动卡片和开关中的硬编码浅色背景/文字回退。 / Completed theme fallback cleanup for the peak panel, floating card, and switches.
10
+ - 保留账户安全、存储权限、刷新异常和偏好同步加固,并避免原始凭证错误文本进入日志或浏览器。 / Kept account-safety, storage-permission, refresh-failure, and preference-sync hardening without exposing raw credential errors to logs or the browser.
11
+ - 新增浮动坐标边界、Portal 和完整主题回退回归测试;当前测试基线为 78/78。 / Added regression coverage for floating-coordinate bounds, Portal mounting, and complete theme fallbacks; the test baseline is now 78/78.
12
+
5
13
  ## 0.2.4 - 2026-08-22
6
14
 
7
15
  - 彻底修复暗色模式(Dark Mode)主题适配(Issue #26):移除所有未主题化的浅色硬编码回退(`--dsw-alias-bg-elevated,#fff`、`--dsw-alias-bg-overlay,#fff` 等),全站统一使用 DSH 原生主题变量(`--dsw-alias-bg-layer-*` 与 `--dsw-alias-label-dimmed`),在深色模式下完美融入背景。 / Fully fixed Dark Mode theme compliance (Issue #26): replaced unthemed hardcoded light fallbacks with DSH native theme variables (`--dsw-alias-bg-layer-*`), rendering dark themes seamlessly without blinding white boxes.
package/index.js CHANGED
@@ -78,7 +78,9 @@ export const PRICE_POLICIES = [
78
78
  export function ratesFor(model, atMs) {
79
79
  let entry
80
80
  for (const policy of PRICE_POLICIES) {
81
- if (atMs >= policy.since && policy.models[model] !== undefined) entry = policy
81
+ // Own-property check: a model named `__proto__`/`toString` would otherwise
82
+ // resolve to an Object.prototype member and produce NaN costs.
83
+ if (atMs >= policy.since && Object.hasOwn(policy.models, model)) entry = policy
82
84
  }
83
85
  if (entry === undefined) return null
84
86
  const rates = entry.models[model]
@@ -258,9 +260,10 @@ function persistStore(logger) {
258
260
  saveTimer = null
259
261
  }
260
262
  try {
261
- mkdirSync(dirname(STORE_PATH), { recursive: true })
263
+ mkdirSync(dirname(STORE_PATH), { recursive: true, mode: 0o700 })
262
264
  const tmp = STORE_PATH + '.tmp'
263
- writeFileSync(tmp, JSON.stringify(store))
265
+ // Owner-only: the store carries usage accounting next to credential files.
266
+ writeFileSync(tmp, JSON.stringify(store), { mode: 0o600 })
264
267
  renameSync(tmp, STORE_PATH)
265
268
  } catch (error) {
266
269
  if (logger && typeof logger.warn === 'function') {
@@ -325,9 +328,10 @@ function persistAccounts(logger) {
325
328
  accountsSaveTimer = null
326
329
  }
327
330
  try {
328
- mkdirSync(dirname(ACCOUNTS_PATH), { recursive: true })
331
+ mkdirSync(dirname(ACCOUNTS_PATH), { recursive: true, mode: 0o700 })
329
332
  const tmp = ACCOUNTS_PATH + '.tmp'
330
- writeFileSync(tmp, JSON.stringify(accounts))
333
+ // This file holds plaintext API keys: owner-only, never group/world readable.
334
+ writeFileSync(tmp, JSON.stringify(accounts), { mode: 0o600 })
331
335
  renameSync(tmp, ACCOUNTS_PATH)
332
336
  } catch (error) {
333
337
  if (logger && typeof logger.warn === 'function') {
@@ -436,15 +440,20 @@ async function resolveBalanceKey(ctx) {
436
440
  */
437
441
  export async function activateAccount(ctx, id) {
438
442
  const account = findAccount(id)
439
- if (account === null) return { ok: false, error: 'account not found' }
443
+ if (account === null) return { ok: false, error: 'account-not-found' }
440
444
  const credentials = ctx.get('credentials')
441
- if (credentials === undefined) return { ok: false, error: 'credentials service unavailable' }
445
+ if (credentials === undefined) return { ok: false, error: 'credentials-unavailable' }
442
446
  try {
443
447
  await credentials.set(CREDENTIAL_REF, account.apiKey)
444
448
  } catch (error) {
445
449
  // e.g. the launching environment supplies DEEPSEEK_API_KEY read-only, so
446
- // the write is shadowed and refused by the credentials provider.
447
- return { ok: false, error: error instanceof Error ? error.message : String(error) }
450
+ // the write is shadowed and refused by the credentials provider. Keep the
451
+ // browser error bounded and do not log provider text that might contain
452
+ // paths, environment details, or key fragments.
453
+ if (ctx.logger && typeof ctx.logger.warn === 'function') {
454
+ ctx.logger.warn('dsh-wallet: credential write refused')
455
+ }
456
+ return { ok: false, error: 'credential-write-refused' }
448
457
  }
449
458
  accounts.activeId = account.id
450
459
  scheduleAccountsSave(ctx.logger)
@@ -509,12 +518,12 @@ let balance = { fetchedAt: 0, available: false, balances: [], error: null }
509
518
  let balanceRefresh = null
510
519
 
511
520
  async function performBalanceRefresh(ctx) {
512
- const credentials = ctx.get('credentials')
513
- if (credentials === undefined) {
514
- balance = { fetchedAt: Date.now(), available: false, balances: [], error: 'no-credentials' }
515
- return
516
- }
517
521
  try {
522
+ const credentials = ctx.get('credentials')
523
+ if (credentials === undefined) {
524
+ balance = { fetchedAt: Date.now(), available: false, balances: [], error: 'no-credentials' }
525
+ return
526
+ }
518
527
  const key = await resolveBalanceKey(ctx)
519
528
  if (key === undefined || key === '') {
520
529
  balance = { fetchedAt: Date.now(), available: false, balances: [], error: 'no-api-key' }
@@ -564,9 +573,24 @@ async function performBalanceRefresh(ctx) {
564
573
 
565
574
  function refreshBalance(ctx) {
566
575
  if (balanceRefresh !== null) return balanceRefresh
567
- balanceRefresh = performBalanceRefresh(ctx).finally(() => {
568
- balanceRefresh = null
569
- })
576
+ // Never let a refresh escape as an unhandled rejection: every caller uses
577
+ // `void refreshBalance(ctx)`, so a throw here would take the host process
578
+ // down (Node exits on unhandled rejections). Absorb it into the error state.
579
+ balanceRefresh = performBalanceRefresh(ctx)
580
+ .catch((error) => {
581
+ balance = {
582
+ fetchedAt: Date.now(),
583
+ available: false,
584
+ balances: [],
585
+ error: 'balance-unavailable',
586
+ }
587
+ if (ctx && ctx.logger && typeof ctx.logger.warn === 'function') {
588
+ ctx.logger.warn('dsh-wallet: balance refresh failed')
589
+ }
590
+ })
591
+ .finally(() => {
592
+ balanceRefresh = null
593
+ })
570
594
  return balanceRefresh
571
595
  }
572
596
 
@@ -609,8 +633,18 @@ function balanceTotal() {
609
633
 
610
634
  function sessionView(sessionId) {
611
635
  if (sessionId === undefined || sessionId === '') return { official: null, third: null }
636
+ // Own-property lookup only: session ids like `__proto__`, `constructor` or
637
+ // `toString` otherwise resolve to an Object.prototype member, slip past the
638
+ // undefined guard, and throw inside bucketTotals.
639
+ if (!Object.prototype.hasOwnProperty.call(store.sessions, sessionId)) {
640
+ return { official: null, third: null }
641
+ }
612
642
  const session = store.sessions[sessionId]
613
- if (session === undefined) return { official: null, third: null }
643
+ if (session === null || typeof session !== 'object'
644
+ || session.official === null || typeof session.official !== 'object'
645
+ || session.third === null || typeof session.third !== 'object') {
646
+ return { official: null, third: null }
647
+ }
614
648
  const official = bucketTotals(session.official)
615
649
  const third = bucketTotals(session.third)
616
650
  return {
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.2.4'
26
+ var WALLET_VERSION = '0.2.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'
@@ -62,11 +62,18 @@ window.__ModuleLoader__.load({
62
62
  ? root.__DSH_WALLET_ADAPTER__
63
63
  : {}
64
64
  var memory = Object.create(null)
65
+ // Keys whose native write was refused (quota, private mode, sandboxed
66
+ // WebView). Reads for those keys must come from memory, otherwise the
67
+ // fallback write is invisible and the UI keeps reading a stale value.
68
+ var memoryOnly = Object.create(null)
65
69
  var nativeStorage = extension.storage || root.localStorage || null
66
70
  var pageNotices = new Map()
67
71
 
68
72
  var storage = {
69
73
  getItem: function (key) {
74
+ if (memoryOnly[key] === true) {
75
+ return Object.prototype.hasOwnProperty.call(memory, key) ? memory[key] : null
76
+ }
70
77
  if (nativeStorage && typeof nativeStorage.getItem === 'function') {
71
78
  try { return nativeStorage.getItem(key) } catch (e) { /* storage may be disabled */ }
72
79
  }
@@ -76,13 +83,19 @@ window.__ModuleLoader__.load({
76
83
  value = String(value)
77
84
  memory[key] = value
78
85
  if (nativeStorage && typeof nativeStorage.setItem === 'function') {
79
- try { nativeStorage.setItem(key, value); return } catch (e) { /* use memory below */ }
86
+ try {
87
+ nativeStorage.setItem(key, value)
88
+ delete memoryOnly[key]
89
+ return
90
+ } catch (e) { /* fall through to the memory-only marker */ }
80
91
  }
92
+ memoryOnly[key] = true
81
93
  },
82
94
  removeItem: function (key) {
83
95
  delete memory[key]
96
+ delete memoryOnly[key]
84
97
  if (nativeStorage && typeof nativeStorage.removeItem === 'function') {
85
- try { nativeStorage.removeItem(key); return } catch (e) { /* use memory below */ }
98
+ try { nativeStorage.removeItem(key); return } catch (e) { /* memory copy is already gone */ }
86
99
  }
87
100
  }
88
101
  }
@@ -277,7 +290,7 @@ window.__ModuleLoader__.load({
277
290
  '.dshw_anchorHome{overflow:hidden;min-width:44px}',
278
291
  '.dshw_chipLift{z-index:80!important}',
279
292
  '.dshw_anchorHome>.dshw_chip{box-sizing:border-box;max-width:100%;min-width:0;overflow:hidden}',
280
- '.dshw_chip{border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:transparent;height:22px;color:var(--dsw-alias-label-primary,#1f2328);white-space:nowrap;border-radius:999px;align-items:stretch;font-size:12px;line-height:1;display:inline-flex;position:relative;touch-action:none;user-select:none;cursor:grab}',
293
+ '.dshw_chip{border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:transparent;height:22px;color:var(--dsw-alias-label-primary,inherit);white-space:nowrap;border-radius:999px;align-items:stretch;font-size:12px;line-height:1;display:inline-flex;position:relative;touch-action:none;user-select:none;cursor:grab}',
281
294
  '.dshw_chip:hover,.dshw_chip:focus-within{border-color:var(--dsw-alias-brand-primary,#4aa3ff)}',
282
295
  '.dshw_chipLow{border-color:var(--dsw-alias-state-error-primary,#e5534b);color:var(--dsw-alias-state-error-primary,#e5534b);animation:dshwChipPulseIn 1.8s ease-in-out infinite}',
283
296
  '.dshw_chipLow:hover,.dshw_chipLow:focus-within{border-color:var(--dsw-alias-state-error-primary,#e5534b)}',
@@ -302,7 +315,7 @@ window.__ModuleLoader__.load({
302
315
  '.dshw_chipVertical.dshw_chipNoRecharge .dshw_chipMain{border-radius:7px}',
303
316
  '.dshw_recharge{color:var(--dsw-alias-brand-primary,#4aa3ff);border-left:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));padding:0 7px 0 6px;border-radius:0 999px 999px 0}',
304
317
  '.dshw_chipMain:focus-visible,.dshw_recharge:focus-visible,.dshw_btn:focus-visible,.dshw_floatBtn:focus-visible,.dshw_dot:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4aa3ff);outline-offset:2px}',
305
- '.dshw_panel{z-index:40;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1,#21262d));box-sizing:border-box;width:min(276px,calc(100vw - 12px));max-height:calc(100vh - 16px);overflow:auto;color:var(--dsw-alias-label-primary,#1f2328);border-radius:8px;flex-direction:column;gap:4px;padding:8px 9px;font-size:12px;display:flex;position:fixed;box-shadow:0 4px 16px rgba(0,0,0,.35)}',
318
+ '.dshw_panel{z-index:40;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,transparent)));box-sizing:border-box;width:min(276px,calc(100vw - 12px));max-height:calc(100vh - 16px);overflow:auto;color:var(--dsw-alias-label-primary,inherit);border-radius:8px;flex-direction:column;gap:4px;padding:8px 9px;font-size:12px;display:flex;position:fixed;box-shadow:0 4px 16px rgba(0,0,0,.35)}',
306
319
  '.dshw_panelHeader{min-height:24px;cursor:move;touch-action:none;user-select:none}',
307
320
  '.dshw_panelHeader .dshw_btn{cursor:pointer}',
308
321
  '.dshw_row{justify-content:space-between;align-items:center;gap:6px;display:flex}',
@@ -319,7 +332,7 @@ window.__ModuleLoader__.load({
319
332
  '.dshw_check input{margin:0;accent-color:var(--dsw-alias-brand-primary,#4aa3ff)}',
320
333
  '.dshw_select{background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,rgba(127,127,127,.08)));border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));color:var(--dsw-alias-label-primary,inherit);border-radius:5px;padding:2px 4px;font-size:12px}',
321
334
  '.dshw_btn{border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:var(--dsw-alias-bg-layer-1,transparent);color:var(--dsw-alias-label-primary,inherit);border-radius:5px;padding:2px 6px;font-size:12px;cursor:pointer}',
322
- '.dshw_btnPrimary{background:var(--dsw-alias-brand-primary,#4aa3ff);border-color:transparent;color:var(--dsw-alias-label-primary-foreground,#ffffff)}',
335
+ '.dshw_btnPrimary{background:var(--dsw-alias-brand-primary,#4aa3ff);border-color:transparent;color:var(--dsw-alias-label-primary-foreground,var(--dsw-alias-label-primary,inherit))}',
323
336
  // ---- v0.2 panel restyle: balance card, settings card, account scroll ----
324
337
  '.dshw_balanceCard{margin:2px 0 2px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.2));border-radius:8px;background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,rgba(127,127,127,.06)))}',
325
338
  '.dshw_balLine{display:flex;align-items:center;gap:7px;white-space:nowrap}',
@@ -411,7 +424,7 @@ window.__ModuleLoader__.load({
411
424
  '.dshw_switch{position:relative;flex:none;width:34px;height:20px;display:inline-flex;cursor:pointer}',
412
425
  '.dshw_switch>input{position:absolute;inset:0;opacity:0;margin:0;cursor:pointer}',
413
426
  '.dshw_track{position:absolute;inset:0;border-radius:999px;background:var(--dsw-alias-border-l3,rgba(127,127,127,.3));transition:background-color .15s}',
414
- '.dshw_knob{position:absolute;left:2px;top:2px;width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:0 1px 3px rgba(0,0,0,.24);transition:transform .15s}',
427
+ '.dshw_knob{position:absolute;left:2px;top:2px;width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,transparent));box-shadow:0 1px 3px rgba(0,0,0,.24);transition:transform .15s}',
415
428
  '.dshw_switch>input:checked+.dshw_track{background:var(--dsw-alias-brand-primary,#4aa3ff)}',
416
429
  '.dshw_switch>input:checked~.dshw_knob{transform:translateX(14px)}',
417
430
  '.dshw_switch>input:focus-visible+.dshw_track{outline:2px solid var(--dsw-alias-brand-primary,#4aa3ff);outline-offset:2px}',
@@ -442,7 +455,7 @@ window.__ModuleLoader__.load({
442
455
  '.dshw_footRingVertical .dshw_footRingCountdown{font-size:10.5px;overflow:visible;white-space:nowrap}',
443
456
  '.dshw_footRingVertical .dshw_footRingBottom{justify-content:center}',
444
457
  '.dshw_footRingNoRecharge{padding-right:10px}',
445
- '.dshw_footRingFloating{position:fixed;z-index:90;width:auto;min-width:180px;max-width:260px;margin:0!important;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1,#21262d));box-shadow:0 8px 24px rgba(0,0,0,.35);cursor:grab;touch-action:none}',
458
+ '.dshw_footRingFloating{position:fixed;z-index:90;width:auto;min-width:180px;max-width:260px;margin:0!important;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,transparent)));box-shadow:0 8px 24px rgba(0,0,0,.35);cursor:grab;touch-action:none}',
446
459
  '.dshw_footRingFloating.dshw_footRingVertical{min-width:132px;max-width:152px;width:140px;padding:9px 8px;gap:4px}',
447
460
  '.dshw_footRingFloating.dshw_footRingVertical .dshw_footRingResetBtn{position:absolute;top:4px;right:4px;font-size:9.5px;padding:1px 5px;line-height:13px}',
448
461
  '.dshw_footRingFloating.dshw_footRingDragging{cursor:grabbing;box-shadow:0 10px 30px rgba(0,0,0,.45)}',
@@ -459,8 +472,8 @@ window.__ModuleLoader__.load({
459
472
  '.dshw_footRingBottom{display:flex;align-items:center;justify-content:space-between;gap:6px;width:100%;min-height:18px}',
460
473
  '.dshw_footRingCountdown{font-size:11px;color:var(--dsw-alias-label-secondary,rgba(127,127,127,.85));line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1 1 auto;min-width:0}',
461
474
  '.dshw_footRingBtnRechargeInline{border:1px solid var(--dsw-alias-brand-primary,#4aa3ff);background:var(--dsw-alias-brand-soft,rgba(74,163,255,.10));color:var(--dsw-alias-brand-primary,#4aa3ff);border-radius:4px;height:18px;padding:0 6px;font-size:10.5px;font-weight:600;line-height:16px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:all .15s ease;user-select:none;flex:none}',
462
- '.dshw_footRingBtnRechargeInline:hover{background:var(--dsw-alias-brand-primary,#4aa3ff);color:#fff}',
463
- '.dshw_peakPanel{position:fixed;z-index:95;width:290px;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1,#21262d));color:var(--dsw-alias-label-primary,inherit);border-radius:12px;box-shadow:0 10px 32px rgba(0,0,0,.38);display:flex;flex-direction:column;gap:8px;padding:12px 14px;font-size:12px;user-select:none;touch-action:none}',
475
+ '.dshw_footRingBtnRechargeInline:hover{background:var(--dsw-alias-brand-primary,#4aa3ff);color:var(--dsw-alias-label-primary-foreground,var(--dsw-alias-label-primary,inherit))}',
476
+ '.dshw_peakPanel{position:fixed;z-index:95;width:290px;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,transparent)));color:var(--dsw-alias-label-primary,inherit);border-radius:12px;box-shadow:0 10px 32px rgba(0,0,0,.38);display:flex;flex-direction:column;gap:8px;padding:12px 14px;font-size:12px;user-select:none;touch-action:none}',
464
477
  '.dshw_peakPanelHeader{display:flex;align-items:center;justify-content:space-between;font-weight:650;font-size:12.5px;color:var(--dsw-alias-label-primary,inherit)}',
465
478
  '.dshw_peakPanelClose{border:none;background:transparent;color:inherit;font-size:16px;line-height:1;cursor:pointer;padding:2px 6px;border-radius:4px}',
466
479
  '.dshw_peakPanelClose:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}',
@@ -471,23 +484,23 @@ window.__ModuleLoader__.load({
471
484
  '.dshw_ringNeutral{stroke:var(--dsw-alias-border-l3,rgba(127,127,127,.3))}',
472
485
  '.dshw_peakRing circle{transition:stroke-width .3s ease}',
473
486
  '.dshw_ringNow{stroke-width:6;filter:brightness(1.15)}',
474
- '.dshw_ringPointer{fill:var(--dsw-alias-label-primary,#1f2328)}',
487
+ '.dshw_ringPointer{fill:var(--dsw-alias-label-primary,inherit)}',
475
488
  '.dshw_ringTick{fill:var(--dsw-alias-label-dimmed,var(--dsw-alias-label-tertiary,#8b949e))}',
476
489
  '@media (max-width:760px){.dshw_settingsGrid,.dshw_settingsSection .dshw_setCard{grid-template-columns:1fr}.dshw_settingsSection .dshw_setCard>.dshw_setCell.wide{grid-column:1}.dshw_accountAdd{grid-template-columns:1fr}.dshw_settingsFooter{align-items:flex-start;flex-direction:column-reverse}.dshw_settingsFooterActions{width:100%}.dshw_settingsFooterActions .dshw_btn{flex:1}.dshw_settingsHeroMeta{white-space:normal;flex-wrap:wrap}}',
477
490
  '.dshw_actionRow{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin:2px 0}',
478
491
  '.dshw_textDanger{display:inline-flex;align-items:center;gap:5px;height:22px;padding:0 4px;border:none;background:transparent;font:inherit;font-size:11px;color:var(--dsw-alias-label-dimmed,var(--dsw-alias-label-tertiary,#8b949e));cursor:pointer;border-radius:4px;transition:color .12s,background-color .12s}',
479
492
  '.dshw_textDanger:hover{color:var(--dsw-alias-state-error-primary,#e5534b);background:var(--dsw-alias-state-error-soft,rgba(229,83,75,.12))}',
480
493
  '.dshw_overlay{position:fixed;inset:0;background:var(--dsw-alias-bg-mask-drop,rgba(0,0,0,.55));display:flex;align-items:center;justify-content:center;z-index:100}',
481
- '.dshw_overlayBox{background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1,#21262d));border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));color:var(--dsw-alias-label-primary,#1f2328);border-radius:8px;padding:14px 18px;width:min(320px,calc(100vw - 32px));box-sizing:border-box;font-size:13px;line-height:1.7}',
494
+ '.dshw_overlayBox{background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,transparent)));border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));color:var(--dsw-alias-label-primary,inherit);border-radius:8px;padding:14px 18px;width:min(320px,calc(100vw - 32px));box-sizing:border-box;font-size:13px;line-height:1.7}',
482
495
  '.dshw_overlayRow{margin-top:10px;display:flex;gap:8px;justify-content:flex-end}',
483
- '.dshw_float{position:fixed;z-index:80;min-width:230px;max-width:280px;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1,#21262d));color:var(--dsw-alias-label-primary,#1f2328);border-radius:12px;box-shadow:0 6px 20px rgba(0,0,0,.25);display:flex;flex-direction:column;gap:6px;padding:10px 12px;font-size:12px;user-select:none}',
496
+ '.dshw_float{position:fixed;z-index:80;min-width:230px;max-width:280px;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,transparent)));color:var(--dsw-alias-label-primary,inherit);border-radius:12px;box-shadow:0 6px 20px rgba(0,0,0,.25);display:flex;flex-direction:column;gap:6px;padding:10px 12px;font-size:12px;user-select:none}',
484
497
  '.dshw_floatHeader{display:flex;align-items:center;justify-content:space-between;cursor:move;font-weight:600;opacity:.9;touch-action:none}',
485
498
  '.dshw_floatBtn{border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:transparent;color:inherit;border-radius:5px;width:22px;height:20px;font-size:12px;line-height:1;cursor:pointer;padding:0}',
486
499
  '.dshw_floatBtn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.1))}',
487
- '.dshw_dot{position:fixed;z-index:80;width:36px;height:36px;border-radius:50%;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1,#21262d));color:var(--dsw-alias-label-primary,#1f2328);font-size:11px;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 2px 10px rgba(0,0,0,.25);user-select:none;touch-action:none}',
500
+ '.dshw_dot{position:fixed;z-index:80;width:36px;height:36px;border-radius:50%;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,transparent)));color:var(--dsw-alias-label-primary,inherit);font-size:11px;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 2px 10px rgba(0,0,0,.25);user-select:none;touch-action:none}',
488
501
  '.dshw_dotLow{border-color:var(--dsw-alias-state-error-primary,#e5534b);color:var(--dsw-alias-state-error-primary,#e5534b)}',
489
502
  '.dshw_noticeStack{position:fixed;z-index:120;top:max(12px,env(safe-area-inset-top));right:max(12px,env(safe-area-inset-right));width:min(340px,calc(100vw - 24px));display:flex;flex-direction:column;gap:8px;pointer-events:none}',
490
- '.dshw_notice{box-sizing:border-box;width:100%;display:flex;align-items:flex-start;gap:8px;padding:10px 10px 10px 12px;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));border-radius:9px;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1,#21262d));color:var(--dsw-alias-label-primary,#1f2328);box-shadow:0 6px 20px rgba(0,0,0,.25);font-size:12px;line-height:1.45;pointer-events:auto;cursor:pointer}',
503
+ '.dshw_notice{box-sizing:border-box;width:100%;display:flex;align-items:flex-start;gap:8px;padding:10px 10px 10px 12px;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));border-radius:9px;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,transparent)));color:var(--dsw-alias-label-primary,inherit);box-shadow:0 6px 20px rgba(0,0,0,.25);font-size:12px;line-height:1.45;pointer-events:auto;cursor:pointer}',
491
504
  '.dshw_noticeCopy{min-width:0;display:flex;flex:1;flex-direction:column;gap:2px}.dshw_noticeCopy span{white-space:pre-line;overflow-wrap:anywhere}',
492
505
  '.dshw_noticeClose{flex:none;width:24px;height:24px;border:0;border-radius:5px;background:transparent;color:inherit;font-size:18px;line-height:1;cursor:pointer}.dshw_noticeClose:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.1))}',
493
506
  // Home-fit and compact modes are toggled by measured space (see the
@@ -1085,6 +1098,26 @@ window.__ModuleLoader__.load({
1085
1098
  return (counters.input || 0) + (counters.output || 0) + (counters.cacheRead || 0) + (counters.cacheWrite || 0)
1086
1099
  }
1087
1100
 
1101
+ // Keep the floating peak card reachable after a viewport resize, a
1102
+ // persisted position from another screen size, or a scale change. `width`
1103
+ // and `height` are the rendered dimensions from getBoundingClientRect(),
1104
+ // so transformed 120% cards are clamped using their actual footprint.
1105
+ function clampPeakPosition(pos, width, height, viewportWidth, viewportHeight, margin) {
1106
+ margin = Number.isFinite(margin) ? Math.max(0, margin) : 8
1107
+ viewportWidth = Number.isFinite(viewportWidth) ? viewportWidth : 1024
1108
+ viewportHeight = Number.isFinite(viewportHeight) ? viewportHeight : 768
1109
+ width = Number.isFinite(width) && width > 0 ? width : 180
1110
+ height = Number.isFinite(height) && height > 0 ? height : 60
1111
+ var x = pos && Number.isFinite(pos.x) ? pos.x : margin
1112
+ var y = pos && Number.isFinite(pos.y) ? pos.y : margin
1113
+ var maxX = Math.max(margin, viewportWidth - width - margin)
1114
+ var maxY = Math.max(margin, viewportHeight - height - margin)
1115
+ return {
1116
+ x: Math.round(Math.min(maxX, Math.max(margin, x))),
1117
+ y: Math.round(Math.min(maxY, Math.max(margin, y))),
1118
+ }
1119
+ }
1120
+
1088
1121
  /**
1089
1122
  * Host settings-panel section: the same wallet controls as the chip panel,
1090
1123
  * as an independent card. Shares state with the chip through the same
@@ -1662,7 +1695,7 @@ window.__ModuleLoader__.load({
1662
1695
  type: 'button', className: 'dshw_btn dshw_btnPrimary',
1663
1696
  onClick: function () { if (close) close(); window.open('https://platform.deepseek.com/top_up', '_blank', 'noopener') }
1664
1697
  }, '↗ 去官方充值'))))
1665
- return React.createElement('div', { className: 'dshw_settingsSection', style: { display: 'flex', flexDirection: 'column', gap: '6px', fontSize: '12px', color: 'var(--dsw-alias-label-primary,#1f2328)' } }, rows)
1698
+ return React.createElement('div', { className: 'dshw_settingsSection', style: { display: 'flex', flexDirection: 'column', gap: '6px', fontSize: '12px', color: 'var(--dsw-alias-label-primary,inherit)' } }, rows)
1666
1699
  }
1667
1700
 
1668
1701
  // Wall-clock hour (0-24, fractional) inside an IANA timezone, via Intl —
@@ -1809,6 +1842,12 @@ window.__ModuleLoader__.load({
1809
1842
  var [nowMs, setNowMs] = React.useState(function () { return Date.now() })
1810
1843
  var dragRef = React.useRef(null)
1811
1844
  var didDragRef = React.useRef(false)
1845
+ // Guards the self-echo: every preference write dispatches the shared
1846
+ // change events so OTHER surfaces resync, but this component must not
1847
+ // re-read storage from its own dispatch — the listener would setState a
1848
+ // second time from a different call stack and race the update already
1849
+ // queued, which can drop the write that started it.
1850
+ var suppressSelfSyncRef = React.useRef(false)
1812
1851
  var [panelOpen, setPanelOpen] = React.useState(false)
1813
1852
  var [panelPos, setPanelPos] = React.useState({ left: 16, top: 16 })
1814
1853
  var cardNodeRef = React.useRef(null)
@@ -1828,6 +1867,8 @@ window.__ModuleLoader__.load({
1828
1867
  loadSnap()
1829
1868
  var listensEvents = typeof window.addEventListener === 'function'
1830
1869
  function onRingChange() {
1870
+ // Skip our own echo (see suppressSelfSyncRef).
1871
+ if (suppressSelfSyncRef.current) return
1831
1872
  try {
1832
1873
  setShown(compatibility.storage.getItem(PEAK_RING_KEY) !== 'false')
1833
1874
  setOrient(compatibility.storage.getItem(PEAK_ORIENT_KEY) || 'horizontal')
@@ -1956,6 +1997,33 @@ window.__ModuleLoader__.load({
1956
1997
  } catch (e) { /* ignore */ }
1957
1998
  }, [policy, nowMs])
1958
1999
 
2000
+ var isFreeFloating = dock === 'free'
2001
+ var isVertical = orient === 'vertical' && (wide || isFreeFloating)
2002
+ var isRail = !wide && !isFreeFloating
2003
+
2004
+ // A saved floating coordinate is not trustworthy after a window resize
2005
+ // or scale change. Re-measure the rendered card (including transform)
2006
+ // and persist the corrected coordinate before it can disappear outside
2007
+ // the viewport. This hook must run even while the card is still loading,
2008
+ // otherwise React sees a changing hook count when the snapshot arrives.
2009
+ useLayoutEffect(function () {
2010
+ if (!isFreeFloating || !cardNodeRef.current) return
2011
+ var node = cardNodeRef.current
2012
+ function fitFloatingPeakCard() {
2013
+ if (!node || typeof node.getBoundingClientRect !== 'function') return
2014
+ var rect = node.getBoundingClientRect()
2015
+ var current = latestPosRef.current || pos || { x: rect.left, y: rect.top }
2016
+ var fitted = clampPeakPosition(current, rect.width, rect.height, window.innerWidth, window.innerHeight, 8)
2017
+ if (fitted.x === current.x && fitted.y === current.y && pos !== null) return
2018
+ latestPosRef.current = fitted
2019
+ setPos(fitted)
2020
+ try { compatibility.storage.setItem(PEAK_POS_KEY, JSON.stringify(fitted)) } catch (e) { /* ignore */ }
2021
+ }
2022
+ fitFloatingPeakCard()
2023
+ window.addEventListener('resize', fitFloatingPeakCard)
2024
+ return function () { window.removeEventListener('resize', fitFloatingPeakCard) }
2025
+ }, [isFreeFloating, pos, peakScale, orient, showRecharge])
2026
+
1959
2027
  if (!shown || snapshot === undefined) return null
1960
2028
  var tzName = policy && policy.timezone ? policy.timezone : 'Asia/Shanghai'
1961
2029
  var offsetMinutes = policy && typeof policy.offsetMinutes === 'number' ? policy.offsetMinutes : 480
@@ -1971,33 +2039,41 @@ window.__ModuleLoader__.load({
1971
2039
  var costLabel = sessionCostLabel(balCurrency)
1972
2040
  var low = snapshot && snapshot.lowBalance === true
1973
2041
 
1974
- var isFreeFloating = dock === 'free'
1975
- var isVertical = orient === 'vertical' && (wide || isFreeFloating)
1976
- var isRail = !wide && !isFreeFloating
2042
+ // Announce a preference change to the other surfaces (settings page, a
2043
+ // second window) while suppressing our own listener, so this component's
2044
+ // queued setState is the single writer for this interaction.
2045
+ function announcePrefs(includeRingEvent) {
2046
+ suppressSelfSyncRef.current = true
2047
+ try {
2048
+ try { compatibility.dispatch(SETTINGS_EVENT) } catch (e) { /* ignore */ }
2049
+ if (includeRingEvent !== false) {
2050
+ try { compatibility.dispatch(PEAK_RING_EVENT) } catch (e) { /* ignore */ }
2051
+ }
2052
+ } finally {
2053
+ suppressSelfSyncRef.current = false
2054
+ }
2055
+ }
1977
2056
 
1978
2057
  function updateOrient(val) {
1979
2058
  setOrient(val)
1980
2059
  try { compatibility.storage.setItem(PEAK_ORIENT_KEY, val) } catch (e) { /* ignore */ }
1981
- try { compatibility.dispatch(SETTINGS_EVENT) } catch (e) { /* ignore */ }
1982
- try { compatibility.dispatch(PEAK_RING_EVENT) } catch (e) { /* ignore */ }
2060
+ announcePrefs()
1983
2061
  }
1984
2062
  function updateScale(val) {
1985
2063
  val = Math.min(1.2, Math.max(1.0, Math.round(val * 20) / 20))
1986
2064
  setPeakScale(val)
1987
2065
  try { compatibility.storage.setItem(PEAK_SCALE_KEY, String(val)) } catch (e) { /* ignore */ }
1988
- try { compatibility.dispatch(SETTINGS_EVENT) } catch (e) { /* ignore */ }
1989
- try { compatibility.dispatch(PEAK_RING_EVENT) } catch (e) { /* ignore */ }
2066
+ announcePrefs()
1990
2067
  }
1991
2068
  function updateRecharge(val) {
1992
2069
  setShowRecharge(val)
1993
2070
  try { compatibility.storage.setItem(PEAK_RECHARGE_KEY, String(val)) } catch (e) { /* ignore */ }
1994
- try { compatibility.dispatch(SETTINGS_EVENT) } catch (e) { /* ignore */ }
1995
- try { compatibility.dispatch(PEAK_RING_EVENT) } catch (e) { /* ignore */ }
2071
+ announcePrefs()
1996
2072
  }
1997
2073
  function updateNotify(val) {
1998
2074
  setPeakNotify(val)
1999
2075
  try { compatibility.storage.setItem(PEAK_NOTIFY_KEY, String(val)) } catch (e) { /* ignore */ }
2000
- try { compatibility.dispatch(SETTINGS_EVENT) } catch (e) { /* ignore */ }
2076
+ announcePrefs(false)
2001
2077
  }
2002
2078
 
2003
2079
  function handlePointerDown(e) {
@@ -2037,11 +2113,10 @@ window.__ModuleLoader__.load({
2037
2113
  if (dragRef.current.dragged) {
2038
2114
  var w = dragRef.current.rectWidth || (isVertical ? 140 : 180)
2039
2115
  var h = dragRef.current.rectHeight || (isVertical ? 120 : 60)
2040
- var maxX = (window.innerWidth || 1024) - w - 8
2041
- var maxY = (window.innerHeight || 768) - h - 8
2042
- var nextX = Math.max(8, Math.min(maxX, moveEvent.clientX - dragRef.current.grabOffsetX))
2043
- var nextY = Math.max(8, Math.min(maxY, moveEvent.clientY - dragRef.current.grabOffsetY))
2044
- var nextPos = { x: Math.round(nextX), y: Math.round(nextY) }
2116
+ var nextPos = clampPeakPosition({
2117
+ x: moveEvent.clientX - dragRef.current.grabOffsetX,
2118
+ y: moveEvent.clientY - dragRef.current.grabOffsetY,
2119
+ }, w, h, window.innerWidth, window.innerHeight, 8)
2045
2120
  latestPosRef.current = nextPos
2046
2121
  setPos(nextPos)
2047
2122
  }
@@ -2056,9 +2131,20 @@ window.__ModuleLoader__.load({
2056
2131
  setDragging(false)
2057
2132
  setDock('free')
2058
2133
  setPos(finalPos)
2059
- try { compatibility.dispatch(SETTINGS_EVENT) } catch (err) { /* ignore */ }
2134
+ announcePrefs(false)
2060
2135
  }
2061
2136
  dragRef.current = null
2137
+ // Release the "this was a drag, swallow the click" latch after the
2138
+ // click that follows pointerup. Without this the flag stays true
2139
+ // whenever the next pointerdown never arrives (pointer released
2140
+ // outside the window, a cancelled gesture), and every later click on
2141
+ // the card is swallowed — the control panel then refuses to open
2142
+ // until the page is reloaded.
2143
+ if (typeof window.setTimeout === 'function') {
2144
+ window.setTimeout(function () { didDragRef.current = false }, 0)
2145
+ } else {
2146
+ didDragRef.current = false
2147
+ }
2062
2148
  window.removeEventListener('pointermove', onMove)
2063
2149
  window.removeEventListener('pointerup', onUp)
2064
2150
  window.removeEventListener('pointercancel', onUp)
@@ -2076,11 +2162,11 @@ window.__ModuleLoader__.load({
2076
2162
  try {
2077
2163
  compatibility.storage.setItem(PEAK_DOCK_KEY, 'sidebar')
2078
2164
  compatibility.storage.removeItem(PEAK_POS_KEY)
2079
- compatibility.dispatch(SETTINGS_EVENT)
2080
- compatibility.dispatch(PEAK_RING_EVENT)
2081
2165
  } catch (err) { /* ignore */ }
2166
+ announcePrefs()
2082
2167
  }
2083
2168
 
2169
+
2084
2170
  var cardStyle = {}
2085
2171
  if (isFreeFloating) {
2086
2172
  cardStyle.position = 'fixed'
@@ -3853,6 +3939,7 @@ window.__ModuleLoader__.load({
3853
3939
  computeSnapPreview: computeSnapPreview,
3854
3940
  computeSideDockX: computeSideDockX,
3855
3941
  computePanelPosition: computePanelPosition,
3942
+ clampPeakPosition: clampPeakPosition,
3856
3943
  createCompatibilityAdapter: createCompatibilityAdapter,
3857
3944
  fmtCurrency: fmtCurrency,
3858
3945
  selectBalanceInfo: selectBalanceInfo,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "deepseek-harness-wallet",
3
3
  "description": "Local-first account monitoring, usage accounting, official recharge, completion reminders, flexible layout, host-gated session controls, and multi-account management with hot account switching for DeepSeek Harness.",
4
- "version": "0.2.4",
4
+ "version": "0.2.5",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "exports": {