deepseek-harness-wallet 0.2.1 → 0.2.3

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,22 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## 0.2.3 - 2026-08-21
6
+
7
+ - 修复 Provider 分桶实际未参与计费的问题;已勾选的包装路由现在真正进入官方 token/花费桶,并记录新观察到的 Provider。 / Fixed provider aliases not reaching the billing path: opted-in wrapper routes now enter the official token/cost bucket, and newly observed providers are recorded.
8
+ - 修复首次添加账户时凭证同步失败仍显示为当前账户的问题;失败会回滚激活状态。 / Rolled back first-account activation when the host refuses credential synchronization.
9
+ - 修复删除账户后账户专属阈值未写回钱包存储的问题。 / Persisted removal of account-specific thresholds when an account is deleted.
10
+ - 余额错误改为安全枚举,避免把上游错误原文暴露到浏览器;美元/多余额记录的明细选择也统一。 / Replaced raw balance errors with safe enums and aligned multi-currency balance selection.
11
+ - 统一提醒设置的本地存储格式,并兼容旧的毫秒字段;API Key 输入框默认隐藏。 / Unified reminder storage with migration from the legacy millisecond field and made API-key inputs password fields.
12
+
13
+ ## 0.2.2 - 2026-08-20
14
+
15
+ - 新增 24h 峰谷计费分时时钟:在侧边栏左下角常驻展示当前时段(高峰/低谷半价)、剩余倒计时、实时余额与本场/本约花费;折叠导轨模式下自动收拢为 42px 紧凑环形钟。 / Added a 24h peak/off-peak ring clock in the sidebar footer: live pricing window (peak / 50% off-peak), switch countdown, live balance and session spend; collapses into a 42px compact circle in rail mode.
16
+ - 侧边栏时钟卡片视觉重构:三行式清爽信息流,字号与容器全面放大,右侧内置紧凑竖排「充值」按钮直达官方充值后台,消除横向挤压与文本截断。 / Restyled the sidebar footer card: three-line spacious flow, larger typography, and a compact vertical recharge button that opens official top-up without text truncation.
17
+ - 峰谷切换系统通知:跨越峰谷时段时自动推送桌面系统通知(可在设置中随心开启/关闭),不错过半价调用的省钱窗口。 / Added peak/off-peak desktop switch reminders with a dedicated toggle in settings.
18
+ - 跨组件毫秒级实时同步:输入框钱包芯片与侧边栏时钟卡片通过全局事件总线(`dshw-snapshot-update` / `dshw-refresh`)毫秒级联动,账户切换与会话消耗实时双向一致。 / Cross-component event sync: wallet composer chip and sidebar clock card stay 100% in sync across account switches and live session token usage.
19
+ - 会话花费彻底杜绝 `--` 占位:始终常驻真实货币金额(无消耗显示 $0.00 / ¥0.00),随多币种账户(USD/CNY)实时切换对应格式与估算/精确标签。 / Always-visible numeric session cost: never drops to `--`, defaults to $0.00/¥0.00, and hot-adapts to active account currency (USD/CNY).
20
+
5
21
  ## 0.2.1 - 2026-08-20
6
22
 
7
23
  - 标签上「本场」恢复常显(¥0.00 也显示,新会话不再缺席);仅未定价模型仍隐藏。/ The chip shows the session cost again even at ¥0.00; only unpriced models stay hidden.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/deepseek-harness-wallet?label=npm&color=5965d8)](https://www.npmjs.com/package/deepseek-harness-wallet)
4
4
  [![GitHub release](https://img.shields.io/github/v/release/feibi-mochi/deepseek-harness-control-center?label=release&color=5965d8)](https://github.com/feibi-mochi/deepseek-harness-control-center/releases)
5
5
  [![CI](https://github.com/feibi-mochi/deepseek-harness-control-center/actions/workflows/validate.yml/badge.svg)](https://github.com/feibi-mochi/deepseek-harness-control-center/actions/workflows/validate.yml)
6
- [![DeepSeek Harness](https://img.shields.io/badge/DeepSeek%20Harness-0.1.0--rc.6-4aa3ff)](https://github.com/deepseek-ai/DeepSeek-Harness)
6
+ [![DeepSeek Harness](https://img.shields.io/badge/DeepSeek%20Harness-0.1.0--rc.8-4aa3ff)](https://github.com/deepseek-ai/DeepSeek-Harness)
7
7
  [![License: MIT](https://img.shields.io/badge/license-MIT-3b7a57)](./LICENSE)
8
8
 
9
9
  **DeepSeek Harness monitoring, alerts, recharge, and session control center.**
@@ -23,8 +23,10 @@
23
23
  ```
24
24
 
25
25
  - **Official DeepSeek** — live balance (60s global refresh with fast boot retries), current-session cost locked to the price active for each usage event (including the 2026-08-17 peak/off-peak rollout), and token breakdown.
26
+ - **24h peak/off-peak ring clock** — resident sidebar footer widget indicating real-time pricing windows (peak vs. 50% discount off-peak), countdown to next switch, and optional desktop switch notifications.
26
27
  - **Third-party total** — current-session tokens (input / cache read / output). No balance guessing, no cost math, zero configuration.
27
- - **Click the chip** to open the detail panel: correctly formatted per-currency balances, cost and token splits, a freely editable low-balance threshold in CNY (two decimals, persisted globally; alerts only compare a CNY balance and never mix currencies), manual refresh, and a jump to the official recharge page (first click shows the domain for confirmation — anti-phishing).
28
+ - **Provider classification** observed wrapper routes appear in the settings page; opted-in routes join the official token/cost bucket and are priced with the official table.
29
+ - **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).
28
30
  - **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.
29
31
  - **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.
30
32
  - **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.
@@ -126,7 +128,7 @@ For buildable DSH hosts, the npm package and repository include a versioned [Age
126
128
 
127
129
  | Item | Behavior |
128
130
  | --- | --- |
129
- | Token accounting | Listens to the `llm/stream` event and buckets per provider (`deepseek-official` vs. everything else) and per session; each usage event also locks its contemporaneous official price, so multiple sessions and pricing windows never mix. |
131
+ | Token accounting | Listens to the `llm/stream` event and buckets per session and provider: `deepseek-official` plus explicitly opted-in wrapper routes use the official bucket; other providers stay third-party; each usage event also locks its contemporaneous official price, so multiple sessions and pricing windows never mix. |
130
132
  | Balance | The key from the credentials seam (or the active account's key) never leaves this machine except as the `Authorization` header of the official `/user/balance` request. |
131
133
  | Accounts | Keys live in `$DSH_HOME/storages/accounts.json` (plaintext, matching the harness's own credential storage); the UI only ever shows masked keys, and switching writes the chosen key into the credentials seam for LLM billing. |
132
134
  | Session log | The plugin writes no events; its data lives in `$DSH_HOME/storages/wallet.json`. |
package/index.js CHANGED
@@ -36,6 +36,10 @@ const BALANCE_REFRESH_MS = 60_000
36
36
  const STORE_VERSION = 2
37
37
 
38
38
  // Beijing (UTC+8, no DST) peak windows: 09:00-12:00 and 14:00-18:00.
39
+ // Exposed to clients via snapshot.pricingWindows so the ring clock renders
40
+ // from the active policy instead of hard-coding hours in the bundle.
41
+ const PEAK_WINDOWS = [{ startHour: 9, endHour: 12 }, { startHour: 14, endHour: 18 }]
42
+ const OFF_PEAK_RATE = 0.5
39
43
  const BEIJING_OFFSET_MS = 8 * 3600_000
40
44
 
41
45
  export function isBeijingPeak(atMs) {
@@ -382,16 +386,18 @@ export function removeAccount(id) {
382
386
  const index = accounts.accounts.findIndex((account) => account.id === id)
383
387
  if (index < 0) return { ok: false, error: 'account not found' }
384
388
  accounts.accounts.splice(index, 1)
385
- // Drop the account's own threshold line along with it.
389
+ // Drop the account's own threshold line along with it. The caller persists
390
+ // wallet.json separately from accounts.json when this flag is true.
391
+ let thresholdRemoved = false
386
392
  if (store.accountThresholds && Object.hasOwn(store.accountThresholds, id)) {
387
393
  delete store.accountThresholds[id]
388
- scheduleAccountsSave(null)
394
+ thresholdRemoved = true
389
395
  }
390
396
  // Deliberately NOT unsetting the credentials seam: the key currently in
391
397
  // .credentials.yaml keeps working for LLM billing; the UI just reports
392
398
  // "no active account" and balance falls back to the seam key.
393
399
  if (accounts.activeId === id) accounts.activeId = null
394
- return { ok: true }
400
+ return { ok: true, thresholdRemoved }
395
401
  }
396
402
 
397
403
  export function accountListView() {
@@ -481,12 +487,18 @@ function recordUsage(options, usage, atMs = Date.now()) {
481
487
  if (typeof sessionId !== 'string' || sessionId === '' || typeof provider !== 'string' || provider === '') return
482
488
  if (typeof model !== 'string' || model === '__proto__' || model === 'prototype' || model === 'constructor') return
483
489
  if (sessionId === '__proto__' || sessionId === 'prototype' || sessionId === 'constructor') return
490
+ // Remember every non-builtin route observed by the stream tap. The settings
491
+ // page can then promote wrapper routes into the official billing bucket.
492
+ if (provider !== OFFICIAL_PROVIDER && !store.knownProviders.includes(provider)) {
493
+ store.knownProviders = normalizeProviderList([...store.knownProviders, provider])
494
+ }
484
495
  if (!Object.hasOwn(store.sessions, sessionId)) {
485
496
  store.sessions[sessionId] = { official: emptyBucket(true), third: emptyBucket(false) }
486
497
  }
487
498
  const session = store.sessions[sessionId]
488
- const bucket = provider === OFFICIAL_PROVIDER ? session.official : session.third
489
- if (provider === OFFICIAL_PROVIDER) addOfficialUsage(bucket, model, usage, atMs)
499
+ const official = isOfficialProvider(provider, store.officialProviders)
500
+ const bucket = official ? session.official : session.third
501
+ if (official) addOfficialUsage(bucket, model, usage, atMs)
490
502
  else {
491
503
  if (!Object.hasOwn(bucket.models, model)) bucket.models[model] = emptyCounters()
492
504
  addUsage(bucket.models[model], usage)
@@ -499,13 +511,13 @@ let balanceRefresh = null
499
511
  async function performBalanceRefresh(ctx) {
500
512
  const credentials = ctx.get('credentials')
501
513
  if (credentials === undefined) {
502
- balance = { fetchedAt: Date.now(), available: false, balances: [], error: 'no credentials service' }
514
+ balance = { fetchedAt: Date.now(), available: false, balances: [], error: 'no-credentials' }
503
515
  return
504
516
  }
505
517
  try {
506
518
  const key = await resolveBalanceKey(ctx)
507
519
  if (key === undefined || key === '') {
508
- balance = { fetchedAt: Date.now(), available: false, balances: [], error: 'no API key' }
520
+ balance = { fetchedAt: Date.now(), available: false, balances: [], error: 'no-api-key' }
509
521
  return
510
522
  }
511
523
  const controller = new AbortController()
@@ -516,23 +528,36 @@ async function performBalanceRefresh(ctx) {
516
528
  headers: { authorization: 'Bearer ' + key },
517
529
  signal: controller.signal,
518
530
  })
519
- if (!response.ok) throw new Error('status ' + response.status)
531
+ if (!response.ok) {
532
+ const code = response.status === 401 || response.status === 403
533
+ ? 'unauthorized'
534
+ : response.status === 429 ? 'rate-limited' : 'upstream-unavailable'
535
+ const error = new Error(code)
536
+ error.code = code
537
+ throw error
538
+ }
520
539
  const data = await response.json()
540
+ const available = data !== null && typeof data === 'object' && data.is_available === true
521
541
  balance = {
522
542
  fetchedAt: Date.now(),
523
- available: data.is_available === true,
524
- balances: Array.isArray(data.balance_infos) ? data.balance_infos : [],
525
- error: null,
543
+ available,
544
+ balances: available && Array.isArray(data.balance_infos) ? data.balance_infos : [],
545
+ error: available ? null : 'balance-unavailable',
526
546
  }
527
547
  } finally {
528
548
  clearTimeout(timer)
529
549
  }
530
550
  } catch (error) {
551
+ const code = error && typeof error === 'object' && typeof error.code === 'string'
552
+ ? error.code
553
+ : error && typeof error === 'object' && error.name === 'AbortError'
554
+ ? 'timeout'
555
+ : error instanceof SyntaxError ? 'invalid-response' : 'upstream-unavailable'
531
556
  balance = {
532
557
  fetchedAt: Date.now(),
533
558
  available: false,
534
559
  balances: [],
535
- error: error instanceof Error ? error.message : String(error),
560
+ error: code,
536
561
  }
537
562
  }
538
563
  }
@@ -627,6 +652,14 @@ function snapshotView(sessionId) {
627
652
  return line > 0 && balanceTotal() < line
628
653
  })(),
629
654
  rechargeUrl: RECHARGE_URL,
655
+ // Peak/off-peak policy for the ring clock; billed in Asia/Shanghai.
656
+ pricingWindows: {
657
+ timezone: 'Asia/Shanghai',
658
+ offsetMinutes: 480,
659
+ windows: PEAK_WINDOWS.map(w => ({ startHour: w.startHour, endHour: w.endHour })),
660
+ offPeakRate: OFF_PEAK_RATE,
661
+ isPeak: isBeijingPeak(Date.now()),
662
+ },
630
663
  accounts: {
631
664
  activeId: accounts.activeId,
632
665
  activeName: active !== null ? active.name : null,
@@ -824,7 +857,8 @@ export function apply(ctx, config) {
824
857
  if (body === null || typeof body.name !== 'string' || typeof body.apiKey !== 'string') {
825
858
  return json(res, 400, { ok: false, error: 'name and apiKey are required' })
826
859
  }
827
- const needSync = accounts.activeId === null
860
+ const previousActiveId = accounts.activeId
861
+ const needSync = previousActiveId === null
828
862
  const result = addAccount(body.name, body.apiKey)
829
863
  if (!result.ok) return json(res, 400, { ok: false, error: result.error })
830
864
  scheduleAccountsSave(ctx.logger)
@@ -832,6 +866,13 @@ export function apply(ctx, config) {
832
866
  if (needSync) {
833
867
  // First account: activate it so the LLM seam follows the new key.
834
868
  sync = await activateAccount(ctx, result.account.id)
869
+ // addAccount marks the first row active optimistically. If the host
870
+ // refuses the credential write, roll it back so UI/balance state does
871
+ // not claim a billing account that the LLM route is not using.
872
+ if (!sync.ok && accounts.activeId === result.account.id) {
873
+ accounts.activeId = previousActiveId
874
+ scheduleAccountsSave(ctx.logger)
875
+ }
835
876
  }
836
877
  return json(res, 200, {
837
878
  ok: true,
@@ -872,6 +913,7 @@ export function apply(ctx, config) {
872
913
  const result = removeAccount(body.id)
873
914
  if (!result.ok) return json(res, 400, { ok: false, error: result.error })
874
915
  scheduleAccountsSave(ctx.logger)
916
+ if (result.thresholdRemoved) scheduleSave(ctx.logger)
875
917
  return json(res, 200, { ok: true, ...accountListView() })
876
918
  },
877
919
  })
package/lib/client.js CHANGED
@@ -14,7 +14,7 @@ window.__ModuleLoader__.load({
14
14
 
15
15
  var POLL_MS = 15000
16
16
  // Keep in lockstep with package.json; a test enforces the sync.
17
- var WALLET_VERSION = '0.2.1'
17
+ var WALLET_VERSION = '0.2.3'
18
18
  var CONFIRM_KEY = 'dsh-wallet-recharge-confirmed'
19
19
  var CHIP_LAYOUT_KEY = 'dshw-chip-layout-v4'
20
20
  var PANEL_POS_KEY = 'dshw-panel-pos-v1'
@@ -24,6 +24,11 @@ window.__ModuleLoader__.load({
24
24
  var NOTIFY_CONFIG_EVENT = 'dshw-completion-notify-change'
25
25
  var SETTINGS_EVENT = 'dshw-settings-change'
26
26
  var LOW_BLINK_KEY = 'dshw-low-blink-v1'
27
+ var PEAK_RING_KEY = 'dshw-peakring-v1'
28
+ var CLASSIC_CARD_KEY = 'dshw-classic-card-v1'
29
+ var PEAK_NOTIFY_KEY = 'dshw-peaknotify-v1'
30
+ var PEAK_NOTIFY_LAST_KEY = 'dshw-peaknotify-last-v1'
31
+ var PEAK_RING_EVENT = 'dshw-peakring-change'
27
32
  var NOTIFY_LEADER_KEY = 'dshw-completion-notify-leader-v1'
28
33
  var PERMANENT_DELETE_KEY = 'dshw-permanent-delete-v1'
29
34
  var PERMANENT_DELETE_EVENT = 'dshw-permanent-delete-change'
@@ -409,6 +414,30 @@ window.__ModuleLoader__.load({
409
414
  '.dshw_settingsFooter{display:flex;align-items:center;justify-content:space-between;gap:10px;padding-top:2px}',
410
415
  '.dshw_settingsFooterActions{display:flex;align-items:center;gap:7px}',
411
416
  '.dshw_settingsFooter .dshw_btn{height:30px;padding:0 14px}',
417
+ '.dshw_footRing{width:100%;box-sizing:border-box;display:flex;align-items:center;gap:9px;min-height:70px;margin:2px 0 5px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.08));border-radius:11px;background:var(--dsw-alias-bg-l1,rgba(127,127,127,.035));color:var(--dsw-alias-label-primary,#1f2328);font-size:12.5px;cursor:default;user-select:none;transition:all .15s ease}',
418
+ '.dshw_footRing:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.06));border-color:var(--dsw-alias-border-l2,rgba(0,0,0,.15))}',
419
+ '.dshw_footRing.dshw_low{border-color:var(--dsw-alias-state-error-primary,#e5534b)}',
420
+ '.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}',
421
+ '.dshw_footRingRail:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.06))}',
422
+ '.dshw_footRingLabel{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;justify-content:center;gap:3px;overflow:hidden}',
423
+ '.dshw_footRingHeader{display:flex;align-items:center;gap:6px;white-space:nowrap}',
424
+ '.dshw_footRingTitle{font-size:13.5px;font-weight:700;line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
425
+ '.dshw_footRingBadge{font-size:10px;font-weight:600;padding:1px 5px;line-height:15px;border-radius:3px;flex:none}',
426
+ '.dshw_footRingBadgePeak{background:var(--dsw-alias-state-error-soft,rgba(229,83,75,.12));color:var(--dsw-alias-state-error-primary,#e5534b)}',
427
+ '.dshw_footRingBadgeOff{background:var(--dsw-alias-state-success-soft,rgba(26,127,55,.12));color:var(--dsw-alias-state-success-primary,#1a7f37)}',
428
+ '.dshw_footRingMoney{font-size:12.5px;font-variant-numeric:tabular-nums;line-height:1.25;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
429
+ '.dshw_footRingMoney.dshw_low .dshw_footBalNum{color:var(--dsw-alias-state-error-primary,#e5534b);font-weight:700}',
430
+ '.dshw_footRingCountdown{font-size:11px;color:var(--dsw-alias-label-secondary,rgba(31,35,40,.72));line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
431
+ '.dshw_footRingRecharge{flex:none;display:flex;align-items:center}',
432
+ '.dshw_footRingBtnRechargeVertical{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:6px;width:24px;min-height:48px;padding:5px 0;font-size:12px;font-weight:650;cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;line-height:1;transition:all .15s ease;flex:none;box-sizing:border-box;user-select:none}',
433
+ '.dshw_footRingBtnRechargeVertical:hover{background:var(--dsw-alias-brand-primary,#4aa3ff);color:#fff}',
434
+ '.dshw_ringPeak{stroke:var(--dsw-alias-state-error-primary,#e5534b)}',
435
+ '.dshw_ringOff{stroke:var(--dsw-alias-state-success-primary,#1a7f37)}',
436
+ '.dshw_ringNeutral{stroke:var(--dsw-alias-border-l3,rgba(0,0,0,.18))}',
437
+ '.dshw_peakRing circle{transition:stroke-width .3s ease}',
438
+ '.dshw_ringNow{stroke-width:6;filter:brightness(1.15)}',
439
+ '.dshw_ringPointer{fill:var(--dsw-alias-label-primary,#1f2328)}',
440
+ '.dshw_ringTick{fill:var(--dsw-alias-label-quaternary,rgba(31,35,40,.45))}',
412
441
  '@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}}',
413
442
  '.dshw_actionRow{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin:2px 0}',
414
443
  '.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-quaternary,rgba(31,35,40,.45));cursor:pointer;border-radius:4px;transition:color .12s,background-color .12s}',
@@ -458,12 +487,12 @@ window.__ModuleLoader__.load({
458
487
  // ratio, not a live FX quote.
459
488
  var USD_ESTIMATE_PER_CNY = 7.25
460
489
  function sessionCostText(costCNY, currency) {
461
- if (costCNY === null || costCNY === undefined) return '--'
490
+ var val = (costCNY === null || costCNY === undefined) ? 0 : costCNY
462
491
  if (currency === 'USD') {
463
- var usd = costCNY / USD_ESTIMATE_PER_CNY
492
+ var usd = val / USD_ESTIMATE_PER_CNY
464
493
  return fmtCurrency(usd, 'USD')
465
494
  }
466
- return fmtCurrency(costCNY, currency || 'CNY')
495
+ return fmtCurrency(val, currency || 'CNY')
467
496
  }
468
497
  // 本约 = "本次为估算值":美元账户的花费由 CNY 价折算,标签本身承担
469
498
  // 约算含义,数字前不再加 ≈ 符号;CNY 账户是精确值,用"本场"。
@@ -471,7 +500,92 @@ window.__ModuleLoader__.load({
471
500
  return currency === 'USD' ? '本约' : '本场'
472
501
  }
473
502
 
474
- function fmtCurrency(value, currency) {
503
+ // Ring clock: 24h circle, peak arcs from snapshot.pricingWindows (never
504
+ // hard-coded in the bundle), pointer at current time, active arc bolded.
505
+ // Noon = 0h at 12 o'clock, clockwise. Optional size scales the 76-unit
506
+ // viewBox down for tight hosts (sidebar foot row / rail circle); optional
507
+ // ariaText overrides the fallback label with the full screen-reader state.
508
+ function peakRingSVG(windows, nowHour, size, ariaText) {
509
+ var sizePx = typeof size === 'number' && size > 0 ? size : 76
510
+ var R = 28
511
+ var CX = 38
512
+ var CY = 38
513
+ var CIRC = 2 * Math.PI * R
514
+ // No pricing policy resolved: one neutral full ring, no arcs, no pointer
515
+ // — the clock shows an unconfigured state and claims no price.
516
+ if (!Array.isArray(windows) || windows.length === 0) {
517
+ return React.createElement('svg', {
518
+ width: sizePx, height: sizePx, viewBox: '0 0 76 76',
519
+ role: 'img', className: 'dshw_peakRing',
520
+ 'aria-label': ariaText || '峰谷计费时段未配置'
521
+ },
522
+ React.createElement('circle', { key: 'neutral', cx: CX, cy: CY, r: R, fill: 'none', className: 'dshw_ringNeutral', strokeWidth: 4 }))
523
+ }
524
+ var segs = []
525
+ // Build segments: which arcs are peak vs off
526
+ var cursor = 0
527
+ var sorted = (windows || []).slice().sort(function (a, b) { return a.startHour - b.startHour })
528
+ var marks = []
529
+ for (var i = 0; i < sorted.length; i++) {
530
+ var w = sorted[i]
531
+ if (w.startHour > cursor) segs.push({ start: cursor, end: w.startHour, peak: false })
532
+ segs.push({ start: w.startHour, end: w.endHour, peak: true })
533
+ marks.push(w.startHour, w.endHour)
534
+ cursor = w.endHour
535
+ }
536
+ if (cursor < 24) segs.push({ start: cursor, end: 24, peak: false })
537
+ // Is the current hour inside a peak window?
538
+ var inPeak = false
539
+ for (var j = 0; j < sorted.length; j++) {
540
+ if (nowHour >= sorted[j].startHour && nowHour < sorted[j].endHour) { inPeak = true; break }
541
+ }
542
+ var children = []
543
+ for (var k = 0; k < segs.length; k++) {
544
+ var seg = segs[k]
545
+ var frac = (seg.end - seg.start) / 24
546
+ var offset = 1 - (seg.start / 24) // stroke-dashoffset rotates clockwise from 3 o'clock; we want 0h at 12
547
+ var isNow = (nowHour >= seg.start && nowHour < seg.end)
548
+ children.push(React.createElement('circle', {
549
+ key: 'seg' + k,
550
+ cx: CX, cy: CY, r: R,
551
+ fill: 'none',
552
+ className: (seg.peak ? 'dshw_ringPeak' : 'dshw_ringOff') + (isNow ? ' dshw_ringNow' : ''),
553
+ strokeWidth: isNow ? 6.5 : 4,
554
+ strokeDasharray: (frac * CIRC).toFixed(2) + ' ' + (CIRC - frac * CIRC).toFixed(2),
555
+ strokeDashoffset: (offset * CIRC).toFixed(2),
556
+ transform: 'rotate(-90 ' + CX + ' ' + CY + ')' // 0h at 12 o'clock
557
+ }))
558
+ }
559
+ // Boundary ticks: dots at rest, the exact times ride the hover tooltip
560
+ for (var m = 0; m < marks.length; m++) {
561
+ var angle = (marks[m] / 24) * 360 - 90
562
+ var rad = angle * Math.PI / 180
563
+ var tx = CX + (R + 7) * Math.cos(rad)
564
+ var ty = CY + (R + 7) * Math.sin(rad)
565
+ children.push(React.createElement('circle', { key: 'tick' + m, cx: tx.toFixed(1), cy: ty.toFixed(1), r: 2, className: 'dshw_ringTick' }))
566
+ }
567
+ // Pointer: a small triangle riding the ring, apex touching the arc —
568
+ // secondary to the bolded current segment (a 1-min move is 0.25°, invisible)
569
+ var pAngle = (nowHour / 24) * 360 - 90
570
+ var pRad = pAngle * Math.PI / 180
571
+ var cosR = Math.cos(pRad)
572
+ var sinR = Math.sin(pRad)
573
+ var apexX = CX + (R - 1) * cosR
574
+ var apexY = CY + (R - 1) * sinR
575
+ var baseCx = CX + (R + 7) * cosR
576
+ var baseCy = CY + (R + 7) * sinR
577
+ var tri = (baseCx + 3.5 * -sinR).toFixed(1) + ',' + (baseCy + 3.5 * cosR).toFixed(1)
578
+ + ' ' + apexX.toFixed(1) + ',' + apexY.toFixed(1)
579
+ + ' ' + (baseCx - 3.5 * -sinR).toFixed(1) + ',' + (baseCy - 3.5 * cosR).toFixed(1)
580
+ children.push(React.createElement('polygon', { key: 'ptr', points: tri, className: 'dshw_ringPointer' }))
581
+ return React.createElement('svg', {
582
+ width: sizePx, height: sizePx, viewBox: '0 0 76 76',
583
+ role: 'img', className: 'dshw_peakRing',
584
+ 'aria-label': ariaText || (inPeak ? '当前为高峰时段' : '当前为低谷时段(半价)')
585
+ }, children)
586
+ }
587
+
588
+ function fmtCurrency(value, currency) {
475
589
  var number = typeof value === 'number' ? value : Number.parseFloat(value)
476
590
  if (!Number.isFinite(number)) return '--'
477
591
  var code = typeof currency === 'string' && /^[A-Z]{3}$/.test(currency.toUpperCase()) ? currency.toUpperCase() : 'CNY'
@@ -485,6 +599,32 @@ function fmtCurrency(value, currency) {
485
599
  }
486
600
  }
487
601
 
602
+ function selectBalanceInfo(balance) {
603
+ if (!balance || !Array.isArray(balance.balances) || balance.balances.length === 0) return null
604
+ var currency = typeof balance.currency === 'string' ? balance.currency.toUpperCase() : null
605
+ if (currency) {
606
+ var matching = balance.balances.find(function (info) {
607
+ return info && typeof info.currency === 'string' && info.currency.toUpperCase() === currency
608
+ })
609
+ if (matching) return matching
610
+ }
611
+ return balance.balances[0]
612
+ }
613
+
614
+ function balanceErrorText(error) {
615
+ switch (error) {
616
+ case 'no-credentials': return '宿主凭证服务不可用'
617
+ case 'no-api-key': return '未配置 DeepSeek API Key'
618
+ case 'unauthorized': return 'API Key 无效或已过期'
619
+ case 'rate-limited': return '余额接口请求过于频繁'
620
+ case 'timeout': return '余额接口请求超时'
621
+ case 'invalid-response': return '余额接口返回无效数据'
622
+ case 'balance-unavailable': return '余额暂不可用'
623
+ case 'upstream-unavailable': return '余额接口暂时不可用'
624
+ default: return '余额暂不可用'
625
+ }
626
+ }
627
+
488
628
  function computePanelPosition(chipRect, panelRect, viewportWidth, viewportHeight) {
489
629
  var margin = 8
490
630
  var gap = 6
@@ -550,6 +690,11 @@ function fmtCurrency(value, currency) {
550
690
  function normalizeNotifyConfig(value) {
551
691
  var enabled = !value || value.enabled !== false
552
692
  var timeout = value && Number.parseInt(value.timeout, 10)
693
+ // Migrate the brief settings-page schema that stored milliseconds under
694
+ // autoCloseMs. The canonical shared shape is { enabled, timeout }.
695
+ if ([0, 5, 10, 30, 60].indexOf(timeout) === -1 && value && Object.hasOwn(value, 'autoCloseMs')) {
696
+ timeout = value.autoCloseMs === null ? 0 : Math.round(Number(value.autoCloseMs) / 1000)
697
+ }
553
698
  if ([0, 5, 10, 30, 60].indexOf(timeout) === -1) timeout = 10
554
699
  return { enabled: enabled, timeout: timeout }
555
700
  }
@@ -608,7 +753,7 @@ function fmtCurrency(value, currency) {
608
753
  }
609
754
  var item = active.item
610
755
  var remaining = queue.length
611
- var body = item.title + (remaining > 0 ? '\n另有 ' + remaining + ' 个对话等待提醒' : '\n点击打开该对话')
756
+ var body = item.title + (remaining > 0 ? '\r\n另有 ' + remaining + ' 个对话等待提醒' : '\r\n点击打开该对话')
612
757
  if (active.timer !== null) clearTimeout(active.timer)
613
758
  if (active.notification) {
614
759
  active.notification.onclose = null
@@ -937,7 +1082,7 @@ function fmtCurrency(value, currency) {
937
1082
  var [notifyEnabled, setNotifyEnabled] = React.useState(function () { return readNotifyConfig().enabled })
938
1083
  var [notifyTimeout, setNotifyTimeout] = React.useState(function () {
939
1084
  var cfg = readNotifyConfig()
940
- return cfg.autoCloseMs === null ? 'keep' : String(Math.round(cfg.autoCloseMs / 1000))
1085
+ return cfg.timeout === 0 ? 'keep' : String(cfg.timeout)
941
1086
  })
942
1087
  var [lowBlinkEnabled, setLowBlinkEnabled] = React.useState(function () {
943
1088
  try { return compatibility.storage.getItem(LOW_BLINK_KEY) !== 'false' } catch (e) { return true }
@@ -948,6 +1093,12 @@ function fmtCurrency(value, currency) {
948
1093
  var [pdSupported, setPdSupported] = React.useState(function () {
949
1094
  return compatibility.hasCapability('permanentDelete')
950
1095
  })
1096
+ var [ringEnabled, setRingEnabled] = React.useState(function () {
1097
+ try { return compatibility.storage.getItem(PEAK_RING_KEY) !== 'false' } catch (e) { return true }
1098
+ })
1099
+ var [peakNotifyEnabled, setPeakNotifyEnabled] = React.useState(function () {
1100
+ try { return compatibility.storage.getItem(PEAK_NOTIFY_KEY) === 'true' } catch (e) { return false }
1101
+ })
951
1102
  React.useEffect(function () {
952
1103
  if (typeof window.addEventListener !== 'function') return
953
1104
  function refreshCap() { setPdSupported(compatibility.hasCapability('permanentDelete')) }
@@ -1000,13 +1151,13 @@ function fmtCurrency(value, currency) {
1000
1151
  }
1001
1152
 
1002
1153
  function persistNotify(enabled, timeout) {
1003
- var autoCloseMs = timeout === 'keep' ? null : Number.parseInt(timeout, 10) * 1000
1004
- if (!Number.isFinite(autoCloseMs) && timeout !== 'keep') autoCloseMs = 10000
1154
+ var timeoutSeconds = timeout === 'keep' ? 0 : Number.parseInt(timeout, 10)
1155
+ if ([0, 5, 10, 30, 60].indexOf(timeoutSeconds) === -1) timeoutSeconds = 10
1005
1156
  try {
1006
- compatibility.storage.setItem(NOTIFY_CONFIG_KEY, JSON.stringify({ enabled: enabled, autoCloseMs: autoCloseMs }))
1157
+ compatibility.storage.setItem(NOTIFY_CONFIG_KEY, JSON.stringify({ enabled: enabled, timeout: timeoutSeconds }))
1007
1158
  } catch (e) { /* ignore */ }
1008
1159
  setNotifyEnabled(enabled)
1009
- setNotifyTimeout(timeout)
1160
+ setNotifyTimeout(timeoutSeconds === 0 ? 'keep' : String(timeoutSeconds))
1010
1161
  compatibility.dispatch(NOTIFY_CONFIG_EVENT)
1011
1162
  }
1012
1163
 
@@ -1104,7 +1255,7 @@ function fmtCurrency(value, currency) {
1104
1255
  var lowBalance = snapshot && snapshot.lowBalance === true
1105
1256
  var sessionCost = snapshot && snapshot.session && snapshot.session.official && snapshot.session.official.cost !== null && snapshot.session.official.cost !== undefined ? sessionCostText(snapshot.session.official.cost, snapshot && snapshot.balance ? snapshot.balance.currency : null) : '--'
1106
1257
  var currencyCode = bal && typeof bal.currency === 'string' ? bal.currency : 'CNY'
1107
- var primaryBalance = bal && Array.isArray(bal.balances) && bal.balances.length > 0 ? bal.balances[0] : null
1258
+ var primaryBalance = selectBalanceInfo(bal)
1108
1259
  var toppedText = primaryBalance ? fmtCurrency(primaryBalance.topped_up_balance, primaryBalance.currency) : '--'
1109
1260
  var grantedText = primaryBalance ? fmtCurrency(primaryBalance.granted_balance, primaryBalance.currency) : '--'
1110
1261
  rows.push(React.createElement('div', { key: 'head', className: 'dshw_settingsHeader' },
@@ -1232,6 +1383,41 @@ function fmtCurrency(value, currency) {
1232
1383
  }
1233
1384
  }),
1234
1385
  React.createElement('span', { className: 'dshw_track', 'aria-hidden': 'true' }),
1386
+ React.createElement('span', { className: 'dshw_knob', 'aria-hidden': 'true' }))),
1387
+ React.createElement('div', { className: 'dshw_settingChoice' },
1388
+ React.createElement('span', { className: 'dshw_settingChoiceCopy' },
1389
+ React.createElement('strong', null, '峰谷时钟'),
1390
+ React.createElement('span', null, '侧边栏底部环形钟(零文字)')),
1391
+ React.createElement('label', { className: 'dshw_switch', title: '在侧边栏设置按钮上方显示峰谷环形钟' },
1392
+ React.createElement('input', {
1393
+ type: 'checkbox', checked: ringEnabled,
1394
+ 'aria-label': '显示侧边栏峰谷时钟',
1395
+ onChange: function (event) {
1396
+ var enabled = event.target.checked
1397
+ setRingEnabled(enabled)
1398
+ try { compatibility.storage.setItem(PEAK_RING_KEY, String(enabled)) } catch (e) { /* ignore */ }
1399
+ try { compatibility.dispatch(SETTINGS_EVENT) } catch (e) { /* ignore */ }
1400
+ try { compatibility.dispatch(PEAK_RING_EVENT) } catch (e) { /* ignore */ }
1401
+ }
1402
+ }),
1403
+ React.createElement('span', { className: 'dshw_track', 'aria-hidden': 'true' }),
1404
+ React.createElement('span', { className: 'dshw_knob', 'aria-hidden': 'true' }))),
1405
+ React.createElement('div', { className: 'dshw_settingChoice' },
1406
+ React.createElement('span', { className: 'dshw_settingChoiceCopy' },
1407
+ React.createElement('strong', null, '峰谷切换提醒'),
1408
+ React.createElement('span', null, '进入高峰/低谷时通知一次')),
1409
+ React.createElement('label', { className: 'dshw_switch', title: '峰谷切换时间点各通知一次,不重复弹出' },
1410
+ React.createElement('input', {
1411
+ type: 'checkbox', checked: peakNotifyEnabled,
1412
+ 'aria-label': '开启峰谷切换提醒',
1413
+ onChange: function (event) {
1414
+ var enabled = event.target.checked
1415
+ setPeakNotifyEnabled(enabled)
1416
+ try { compatibility.storage.setItem(PEAK_NOTIFY_KEY, String(enabled)) } catch (e) { /* ignore */ }
1417
+ try { compatibility.dispatch(SETTINGS_EVENT) } catch (e) { /* ignore */ }
1418
+ }
1419
+ }),
1420
+ React.createElement('span', { className: 'dshw_track', 'aria-hidden': 'true' }),
1235
1421
  React.createElement('span', { className: 'dshw_knob', 'aria-hidden': 'true' })))))
1236
1422
  )
1237
1423
  rows.push(React.createElement('div', { key: 'setcard', className: 'dshw_setCard' }, cells))
@@ -1322,7 +1508,7 @@ function fmtCurrency(value, currency) {
1322
1508
  onChange: function (event) { setNameDraft(event.target.value) }
1323
1509
  }),
1324
1510
  React.createElement('input', {
1325
- className: 'dshw_input',
1511
+ className: 'dshw_input', type: 'password',
1326
1512
  placeholder: 'sk-...', 'aria-label': 'API Key',
1327
1513
  value: keyDraft,
1328
1514
  onInput: function (event) { setKeyDraft(event.target.value) },
@@ -1352,6 +1538,246 @@ function fmtCurrency(value, currency) {
1352
1538
  return React.createElement('div', { className: 'dshw_settingsSection', style: { display: 'flex', flexDirection: 'column', gap: '6px', fontSize: '12px', color: 'var(--dsw-alias-label-primary,#1f2328)' } }, rows)
1353
1539
  }
1354
1540
 
1541
+ // Wall-clock hour (0-24, fractional) inside an IANA timezone, via Intl —
1542
+ // never a fixed UTC offset, so DST-observing zones stay correct. Falls back
1543
+ // to the policy's declared offset only if Intl cannot resolve the zone.
1544
+ function wallHourIn(tz, offsetMinutes, date) {
1545
+ try {
1546
+ var text = new Intl.DateTimeFormat('en-GB', {
1547
+ timeZone: tz, hour: '2-digit', minute: '2-digit', hour12: false
1548
+ }).format(date)
1549
+ var parts = text.split(':')
1550
+ var h = Number.parseFloat(parts[0])
1551
+ var m = Number.parseFloat(parts[1])
1552
+ if (Number.isFinite(h) && Number.isFinite(m)) return (h % 24) + m / 60
1553
+ } catch (e) { /* fall through to the fixed offset */ }
1554
+ return (date.getTime() + (offsetMinutes || 0) * 60000) % 86400000 / 3600000
1555
+ }
1556
+
1557
+ // Pure peak-clock math shared by the sidebar ring: current period, next
1558
+ // boundary (wrapping past midnight), the reminder dedup id, and the
1559
+ // zero-text-friendly tooltip / screen-reader strings. windows come from
1560
+ // the wallet snapshot; anything malformed collapses to the neutral state.
1561
+ function peakClockState(policy, nowHour, nowMs) {
1562
+ var windows = policy && Array.isArray(policy.windows) ? policy.windows.filter(function (w) {
1563
+ return w && Number.isFinite(w.startHour) && Number.isFinite(w.endHour) && w.endHour > w.startHour
1564
+ }) : []
1565
+ if (windows.length === 0) {
1566
+ return { configured: false, windows: [], ariaText: '峰谷计费时段未配置', tip: '峰谷时钟 · 计费时段未配置', periodId: null }
1567
+ }
1568
+ var inPeak = false
1569
+ var segStart = null
1570
+ var segEnd = null
1571
+ for (var i = 0; i < windows.length; i++) {
1572
+ if (nowHour >= windows[i].startHour && nowHour < windows[i].endHour) {
1573
+ inPeak = true; segStart = windows[i].startHour; segEnd = windows[i].endHour; break
1574
+ }
1575
+ }
1576
+ if (!inPeak) {
1577
+ // Off-peak segment runs from the last passed boundary to the next one.
1578
+ var bounds = []
1579
+ for (var b = 0; b < windows.length; b++) bounds.push(windows[b].startHour, windows[b].endHour)
1580
+ bounds.sort(function (a, b2) { return a - b2 })
1581
+ segStart = -1
1582
+ segEnd = 25
1583
+ for (var c = 0; c < bounds.length; c++) {
1584
+ if (bounds[c] <= nowHour && bounds[c] > segStart) segStart = bounds[c]
1585
+ if (bounds[c] > nowHour && bounds[c] < segEnd) segEnd = bounds[c]
1586
+ }
1587
+ if (segStart < 0) segStart = bounds[bounds.length - 1] - 24 // late evening wraps to yesterday's last edge
1588
+ }
1589
+ // Late evening off-peak is past every boundary: the next switch is the
1590
+ // FIRST boundary of tomorrow (segEnd still sits at its 25 sentinel).
1591
+ var nextHour = !inPeak && segEnd > 24 ? bounds[0] : segEnd
1592
+ function hh(h) { return String(Math.floor(((h % 24) + 24) % 24)).padStart(2, '0') + ':00' }
1593
+ var winText = windows.map(function (w) { return hh(w.startHour) + '–' + hh(w.endHour) }).join(' / ')
1594
+ var tzName = policy.timezone || 'Asia/Shanghai'
1595
+ var rate = typeof policy.offPeakRate === 'number' && policy.offPeakRate > 0 ? policy.offPeakRate : 0.5
1596
+ var rateWord = rate === 0.5 ? '半价' : '×' + rate
1597
+ var hoursLeft = (nextHour - nowHour + 24) % 24
1598
+ var msLeft = Math.round(hoursLeft * 3600000)
1599
+ var hLeft = Math.floor(msLeft / 3600000)
1600
+ var mLeft = Math.floor((msLeft % 3600000) / 60000)
1601
+ var leftText = hLeft > 0 ? hLeft + ' 小时 ' + mLeft + ' 分' : mLeft + ' 分钟'
1602
+ var leftShort = hLeft > 0 ? (hLeft + 'h' + (mLeft > 0 ? mLeft + 'm' : '')) : (mLeft + 'm')
1603
+ var switchText = inPeak ? (hh(nextHour) + ' 后' + rateWord) : (hh(nextHour) + ' 恢复标准价')
1604
+ var period = inPeak ? '高峰' : '低谷' + rateWord
1605
+ var periodName = inPeak ? '高峰时段' : '低谷时段'
1606
+ var rateBadge = inPeak ? '标准价' : (rate === 0.5 ? '半价' : '×' + rate)
1607
+ var countdownSummary = hh(nextHour) + ' 切换 · 剩 ' + leftShort
1608
+ var windowSummary = '高峰 ' + winText
1609
+ // Dual timezone: billing is judged in the policy's base zone; a device
1610
+ // elsewhere also sees the local-clock span of the CURRENT segment.
1611
+ var localNote = ''
1612
+ try {
1613
+ var localTz = Intl.DateTimeFormat().resolvedOptions().timeZone
1614
+ if (localTz && localTz !== tzName && Number.isFinite(nowMs)) {
1615
+ var hoursAgo = (nowHour - segStart + 24) % 24
1616
+ var segLenH = ((nextHour - segStart) % 24 + 24) % 24 || 24
1617
+ var startInstant = nowMs - hoursAgo * 3600000
1618
+ var endInstant = startInstant + segLenH * 3600000
1619
+ var fmtLocal = new Intl.DateTimeFormat('en-GB', { hour: '2-digit', minute: '2-digit', hour12: false })
1620
+ localNote = ' · 当地 ' + fmtLocal.format(startInstant) + '–' + fmtLocal.format(endInstant)
1621
+ }
1622
+ } catch (e) { /* timezone introspection is best-effort */ }
1623
+ var ariaText = inPeak
1624
+ ? '当前为高峰时段,' + tzName + ' ' + winText + ',按标准价计费'
1625
+ : '当前为低谷时段,按平价的 ' + rate + ' 倍计费,' + tzName + ' 高峰 ' + winText
1626
+ var tip = '峰谷时钟 · 当前' + period + ' · ' + switchText + ' · 还有 ' + leftText
1627
+ + ' · 高峰 ' + winText + '(' + tzName + ')' + localNote
1628
+ return {
1629
+ configured: true, windows: windows, inPeak: inPeak,
1630
+ nextHour: nextHour, ariaText: ariaText, tip: tip,
1631
+ periodName: periodName, rateBadge: rateBadge,
1632
+ countdownSummary: countdownSummary, windowSummary: windowSummary,
1633
+ // Reminder dedup id: entering this period at this boundary fires once.
1634
+ periodId: (inPeak ? 'p' : 'o') + Math.round(segStart * 100),
1635
+ switchBody: inPeak ? '已进入高峰时段,按标准价计费' : '已进入低谷时段,按平价的 ' + rate + ' 倍计费',
1636
+ }
1637
+ }
1638
+
1639
+ // Sidebar foot occupant: the 24h peak/off-peak ring clock, registered on
1640
+ // the host's sidebar.footer.action list so it sits in the strip the
1641
+ // Sidebar foot occupant: the 24h peak/off-peak ring clock, registered on
1642
+ // the host's sidebar.footer.action list so it sits in the strip the
1643
+ // sidebar keeps right above its Settings row (bottom left). Zero in-ring text —
1644
+ // the arcs, the bolded current segment and the hover tooltip carry the meaning.
1645
+ // In wide mode, it displays the 46px ring clock alongside status, balance/cost, and countdown.
1646
+ // In rail mode, it collapses to a 34px centered circular widget.
1647
+ function PeakRingFooter(props) {
1648
+ props = props || {}
1649
+ var wide = props.wide !== false
1650
+ var [shown, setShown] = React.useState(function () {
1651
+ try { return compatibility.storage.getItem(PEAK_RING_KEY) !== 'false' } catch (e) { return true }
1652
+ })
1653
+ var [snapshot, setSnapshot] = React.useState(undefined) // undefined = loading
1654
+ var [nowMs, setNowMs] = React.useState(function () { return Date.now() })
1655
+
1656
+ React.useEffect(function () {
1657
+ var stopped = false
1658
+ function loadSnap() {
1659
+ fetch('/api/wallet/snapshot').then(function (resp) { return resp.json() }).then(function (json) {
1660
+ if (!stopped && json && json.ok) setSnapshot(json)
1661
+ }).catch(function () { if (!stopped) setSnapshot({ ok: false }) })
1662
+ }
1663
+ loadSnap()
1664
+ var listensEvents = typeof window.addEventListener === 'function'
1665
+ function onRingChange() {
1666
+ try { setShown(compatibility.storage.getItem(PEAK_RING_KEY) !== 'false') } catch (e) { /* ignore */ }
1667
+ loadSnap()
1668
+ }
1669
+ function onSnapUpdate(event) {
1670
+ if (!stopped && event && event.detail && event.detail.ok) {
1671
+ setSnapshot(event.detail)
1672
+ }
1673
+ }
1674
+ if (listensEvents) {
1675
+ window.addEventListener(PEAK_RING_EVENT, onRingChange)
1676
+ window.addEventListener(SETTINGS_EVENT, onRingChange)
1677
+ window.addEventListener('dshw-refresh', onRingChange)
1678
+ window.addEventListener('dshw-snapshot-update', onSnapUpdate)
1679
+ }
1680
+ var timer = window.setInterval(function () {
1681
+ setNowMs(Date.now())
1682
+ loadSnap()
1683
+ }, 30000)
1684
+ return function () {
1685
+ stopped = true
1686
+ window.clearInterval(timer)
1687
+ if (listensEvents) {
1688
+ window.removeEventListener(PEAK_RING_EVENT, onRingChange)
1689
+ window.removeEventListener(SETTINGS_EVENT, onRingChange)
1690
+ window.removeEventListener('dshw-refresh', onRingChange)
1691
+ window.removeEventListener('dshw-snapshot-update', onSnapUpdate)
1692
+ }
1693
+ }
1694
+ }, [])
1695
+
1696
+ var policy = snapshot && snapshot.pricingWindows ? snapshot.pricingWindows : null
1697
+
1698
+ React.useEffect(function () {
1699
+ if (policy === undefined || policy === null) return
1700
+ try {
1701
+ if (compatibility.storage.getItem(PEAK_NOTIFY_KEY) !== 'true') return
1702
+ } catch (e) { return }
1703
+ var tzName = policy.timezone || 'Asia/Shanghai'
1704
+ var state = peakClockState(policy, wallHourIn(tzName, policy.offsetMinutes, new Date(nowMs)), nowMs)
1705
+ if (!state.periodId) return
1706
+ var last = null
1707
+ try { last = compatibility.storage.getItem(PEAK_NOTIFY_LAST_KEY) } catch (e) { /* ignore */ }
1708
+ if (last === state.periodId) return
1709
+ try { compatibility.storage.setItem(PEAK_NOTIFY_LAST_KEY, state.periodId) } catch (e) { /* ignore */ }
1710
+ if (last === null) return
1711
+ try {
1712
+ compatibility.notify('DeepSeek Harness · 峰谷切换', {
1713
+ body: state.switchBody, tag: 'dsh-wallet-peak'
1714
+ })
1715
+ } catch (e) { /* ignore */ }
1716
+ }, [policy, nowMs])
1717
+
1718
+ if (!shown || snapshot === undefined) return null
1719
+ var tzName = policy && policy.timezone ? policy.timezone : 'Asia/Shanghai'
1720
+ var offsetMinutes = policy && typeof policy.offsetMinutes === 'number' ? policy.offsetMinutes : 480
1721
+ var state = peakClockState(policy, wallHourIn(tzName, offsetMinutes, new Date(nowMs)), nowMs)
1722
+
1723
+ var bal = snapshot && snapshot.balance ? snapshot.balance : {}
1724
+ var balCurrency = bal && bal.currency ? bal.currency : 'CNY'
1725
+ var balText = bal.total !== null && bal.total !== undefined ? fmtCurrency(bal.total, balCurrency) : fmtCurrency(0, balCurrency)
1726
+ var session = snapshot && snapshot.session ? snapshot.session : {}
1727
+ var official = session && session.official ? session.official : {}
1728
+ var costValue = (official.cost === null || official.cost === undefined) ? 0 : official.cost
1729
+ var costText = sessionCostText(costValue, balCurrency)
1730
+ var costLabel = sessionCostLabel(balCurrency)
1731
+ var low = snapshot && snapshot.lowBalance === true
1732
+
1733
+ return React.createElement('div', {
1734
+ className: 'dshw_footRing' + (wide ? '' : ' dshw_footRingRail') + (low ? ' dshw_low' : ''),
1735
+ title: state.tip + ' · 余额 ' + balText + ' · ' + costLabel + ' ' + costText + (wide ? '' : ' · 点击前往官方充值'),
1736
+ 'aria-label': state.ariaText + ',余额 ' + balText + ',' + costLabel + ' ' + costText,
1737
+ role: 'region',
1738
+ onClick: wide ? undefined : function () { window.open('https://platform.deepseek.com/top_up', '_blank', 'noopener') }
1739
+ },
1740
+ React.createElement('div', { style: { flex: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center' } },
1741
+ peakRingSVG(state.configured ? state.windows : null, wallHourIn(tzName, offsetMinutes, new Date(nowMs)), wide ? 50 : 36, state.ariaText)
1742
+ ),
1743
+ wide ? React.createElement('div', { className: 'dshw_footRingLabel' },
1744
+ React.createElement('div', { className: 'dshw_footRingHeader' },
1745
+ React.createElement('span', {
1746
+ className: 'dshw_footRingTitle',
1747
+ style: { color: state.configured ? (state.inPeak ? 'var(--dsw-alias-state-error-primary,#e5534b)' : 'var(--dsw-alias-state-success-primary,#1a7f37)') : 'inherit' }
1748
+ }, state.configured ? state.periodName : '峰谷时钟'),
1749
+ state.configured && !state.inPeak ? React.createElement('span', {
1750
+ className: 'dshw_footRingBadge dshw_footRingBadgeOff'
1751
+ }, state.rateBadge) : null
1752
+ ),
1753
+ React.createElement('div', { className: 'dshw_footRingMoney' + (low ? ' dshw_low' : '') },
1754
+ React.createElement('span', { className: 'dshw_muted' }, '余额 '),
1755
+ React.createElement('span', { className: 'dshw_footBalNum', style: { fontWeight: '600' } }, balText),
1756
+ React.createElement('span', { className: 'dshw_balDot', style: { margin: '0 3px' } }, '·'),
1757
+ React.createElement('span', { className: 'dshw_muted' }, costLabel + ' '),
1758
+ React.createElement('span', { style: { fontWeight: '600' } }, costText)
1759
+ ),
1760
+ React.createElement('span', { className: 'dshw_footRingCountdown' },
1761
+ state.configured ? state.countdownSummary : '计费时段未配置')
1762
+ ) : null,
1763
+ wide ? React.createElement('div', { className: 'dshw_footRingRecharge' },
1764
+ React.createElement('button', {
1765
+ type: 'button',
1766
+ className: 'dshw_footRingBtnRechargeVertical',
1767
+ title: 'DeepSeek 开放平台 · 前往官方充值',
1768
+ 'aria-label': '前往官方充值',
1769
+ onClick: function (e) {
1770
+ if (e && typeof e.stopPropagation === 'function') e.stopPropagation()
1771
+ window.open('https://platform.deepseek.com/top_up', '_blank', 'noopener')
1772
+ }
1773
+ },
1774
+ React.createElement('span', { style: { lineHeight: 1 } }, '充'),
1775
+ React.createElement('span', { style: { lineHeight: 1 } }, '值')
1776
+ )
1777
+ ) : null
1778
+ )
1779
+ }
1780
+
1355
1781
  function WalletChip(props) {
1356
1782
  props = props || {}
1357
1783
  var sessionId = props.sessionId
@@ -1411,7 +1837,7 @@ function fmtCurrency(value, currency) {
1411
1837
  try { return normalizeChipScale(compatibility.storage.getItem(CHIP_SCALE_KEY)) } catch (e) { return 1 }
1412
1838
  })
1413
1839
  var [homeMode, setHomeMode] = React.useState('full')
1414
- var chipLiftHostRef = React.useRef(null)
1840
+ var chipLiftHostRef = React.useRef(null)
1415
1841
  var [dataVisibility, setDataVisibility] = React.useState(function () {
1416
1842
  try {
1417
1843
  var savedVisibility = compatibility.storage.getItem(DATA_VISIBILITY_KEY)
@@ -1452,6 +1878,34 @@ function fmtCurrency(value, currency) {
1452
1878
  refreshHostCapabilities()
1453
1879
  return function () { window.removeEventListener(HOST_CAPABILITY_EVENT, refreshHostCapabilities) }
1454
1880
  }, [])
1881
+
1882
+ React.useEffect(function () {
1883
+ var pw = data && data.pricingWindows
1884
+ if (!pw || !pw.windows) return
1885
+ var checkNotify = function () {
1886
+ try {
1887
+ if (compatibility.storage.getItem(PEAK_NOTIFY_KEY) !== 'true') return
1888
+ } catch (e) { return }
1889
+ var now = Date.now()
1890
+ var tz = pw.timezone || 'Asia/Shanghai'
1891
+ var offMin = typeof pw.offsetMinutes === 'number' ? pw.offsetMinutes : 480
1892
+ var st = peakClockState(pw, wallHourIn(tz, offMin, new Date(now)), now)
1893
+ if (!st.periodId) return
1894
+ var last = null
1895
+ try { last = compatibility.storage.getItem(PEAK_NOTIFY_LAST_KEY) } catch (e) { /* ignore */ }
1896
+ if (last === st.periodId) return
1897
+ try { compatibility.storage.setItem(PEAK_NOTIFY_LAST_KEY, st.periodId) } catch (e) { /* ignore */ }
1898
+ if (last === null) return
1899
+ try {
1900
+ compatibility.notify('DeepSeek Harness · 峰谷切换', {
1901
+ body: st.switchBody, tag: 'dsh-wallet-peak'
1902
+ })
1903
+ } catch (e) { /* ignore */ }
1904
+ }
1905
+ checkNotify()
1906
+ var timer = setInterval(checkNotify, 60000)
1907
+ return function () { clearInterval(timer) }
1908
+ }, [data])
1455
1909
  var showOfficial = dataVisibility.official
1456
1910
  var showThird = dataVisibility.third
1457
1911
  var chipScaleRef = React.useRef(chipScale)
@@ -1512,6 +1966,11 @@ function fmtCurrency(value, currency) {
1512
1966
  if (!alive) return
1513
1967
  dataRef.current = json
1514
1968
  setData(json)
1969
+ try {
1970
+ if (typeof window.dispatchEvent === 'function' && typeof CustomEvent === 'function') {
1971
+ window.dispatchEvent(new CustomEvent('dshw-snapshot-update', { detail: json }))
1972
+ }
1973
+ } catch (e) { /* ignore */ }
1515
1974
  if (json.threshold !== undefined && json.threshold !== null) {
1516
1975
  // Follow the active currency's own threshold line: switching
1517
1976
  // accounts (and thus currency) refreshes the draft unless the
@@ -1524,7 +1983,8 @@ function fmtCurrency(value, currency) {
1524
1983
  if (showOfficial && json.lowBalance) {
1525
1984
  if (!notifiedRef.current) {
1526
1985
  try {
1527
- var lowNotice = compatibility.notify('DeepSeek \u4f59\u989d\u4e0d\u8db3', { body: '\u4f59\u989d\u5df2\u4f4e\u4e8e\u63d0\u9192\u7ebf ' + json.threshold + ' \u5143', tag: 'dsh-wallet-low' })
1986
+ var lowCurrency = json.balance && json.balance.currency ? json.balance.currency : 'CNY'
1987
+ var lowNotice = compatibility.notify('DeepSeek 余额不足', { body: '余额已低于提醒线 ' + fmtCurrency(json.threshold, lowCurrency), tag: 'dsh-wallet-low' })
1528
1988
  if (lowNotice) notifiedRef.current = true
1529
1989
  } catch (e) { /* ignore */ }
1530
1990
  }
@@ -2110,6 +2570,11 @@ function fmtCurrency(value, currency) {
2110
2570
  : '\u5df2\u6dfb\u52a0' + (json.syncError ? '\uff08\u540c\u6b65\u5931\u8d25: ' + json.syncError + '\uff09' : ''))
2111
2571
  loadAccounts()
2112
2572
  refreshBalance()
2573
+ try {
2574
+ if (typeof window.dispatchEvent === 'function' && typeof CustomEvent === 'function') {
2575
+ window.dispatchEvent(new CustomEvent('dshw-refresh'))
2576
+ }
2577
+ } catch (e) { /* ignore */ }
2113
2578
  } else {
2114
2579
  setAccountNotice((json && json.error) || '\u6dfb\u52a0\u5931\u8d25')
2115
2580
  }
@@ -2121,7 +2586,7 @@ function fmtCurrency(value, currency) {
2121
2586
 
2122
2587
  function activateAccount(id, name) {
2123
2588
  if (switchingId !== null) return
2124
- if (!window.confirm('\u5207\u6362\u5230\u8d26\u6237\u300c' + name + '\u300d\uff1f\n\u5207\u6362\u540e\uff0c\u540e\u7eed LLM \u8bf7\u6c42\u5c06\u4f7f\u7528\u8be5\u8d26\u6237\u7684 Key \u8ba1\u8d39\u3002')) return
2589
+ if (!window.confirm('\u5207\u6362\u5230\u8d26\u6237\u300c' + name + '\u300d\uff1f\r\n\u5207\u6362\u540e\uff0c\u540e\u7eed LLM \u8bf7\u6c42\u5c06\u4f7f\u7528\u8be5\u8d26\u6237\u7684 Key \u8ba1\u8d39\u3002')) return
2125
2590
  setSwitchingId(id)
2126
2591
  setAccountNotice(null)
2127
2592
  fetch('/api/wallet/accounts/activate', {
@@ -2134,6 +2599,11 @@ function fmtCurrency(value, currency) {
2134
2599
  setAccountNotice('\u5df2\u5207\u6362\u5230\u300c' + json.account.name + '\u300d')
2135
2600
  loadAccounts()
2136
2601
  refreshBalance()
2602
+ try {
2603
+ if (typeof window.dispatchEvent === 'function' && typeof CustomEvent === 'function') {
2604
+ window.dispatchEvent(new CustomEvent('dshw-refresh'))
2605
+ }
2606
+ } catch (e) { /* ignore */ }
2137
2607
  // 切换账号: 解锁 + 激活响应自带阈值, 输入框零等待跳转
2138
2608
  thresholdInitializedRef.current = false
2139
2609
  if (json.threshold !== undefined && json.threshold !== null) setThresholdDraft(json.threshold.toFixed(2))
@@ -2147,7 +2617,7 @@ function fmtCurrency(value, currency) {
2147
2617
  }
2148
2618
 
2149
2619
  function removeAccount(id, name) {
2150
- if (!window.confirm('\u5220\u9664\u8d26\u6237\u300c' + name + '\u300d\uff1f\n\u4ec5\u5220\u9664\u672c\u63d2\u4ef6\u7684\u8d26\u6237\u8bb0\u5f55\uff0c\u4e0d\u5f71\u54cd .credentials.yaml \u4e2d\u5df2\u6709\u7684 Key\u3002')) return
2620
+ if (!window.confirm('\u5220\u9664\u8d26\u6237\u300c' + name + '\u300d\uff1f\r\n\u4ec5\u5220\u9664\u672c\u63d2\u4ef6\u7684\u8d26\u6237\u8bb0\u5f55\uff0c\u4e0d\u5f71\u54cd .credentials.yaml \u4e2d\u5df2\u6709\u7684 Key\u3002')) return
2151
2621
  fetch('/api/wallet/accounts/remove', {
2152
2622
  method: 'POST',
2153
2623
  headers: { 'content-type': 'application/json' },
@@ -2157,6 +2627,11 @@ function fmtCurrency(value, currency) {
2157
2627
  setAccountNotice('\u5df2\u5220\u9664')
2158
2628
  loadAccounts()
2159
2629
  refreshBalance()
2630
+ try {
2631
+ if (typeof window.dispatchEvent === 'function' && typeof CustomEvent === 'function') {
2632
+ window.dispatchEvent(new CustomEvent('dshw-refresh'))
2633
+ }
2634
+ } catch (e) { /* ignore */ }
2160
2635
  } else {
2161
2636
  setAccountNotice((json && json.error) || '\u5220\u9664\u5931\u8d25')
2162
2637
  }
@@ -2425,7 +2900,7 @@ function fmtCurrency(value, currency) {
2425
2900
  type: 'number',
2426
2901
  min: '0',
2427
2902
  step: 'any',
2428
- 'aria-label': '\u4f59\u989d\u63d0\u9192\u9608\u503c\uff08CNY\uff09',
2903
+ 'aria-label': '余额提醒阈值(' + (bal.currency || 'CNY') + ')',
2429
2904
  value: thresholdDraft === null ? '' : thresholdDraft,
2430
2905
  onChange: function (event) {
2431
2906
  thresholdInitializedRef.current = 'editing'
@@ -2560,10 +3035,10 @@ function fmtCurrency(value, currency) {
2560
3035
  function balanceCard() {
2561
3036
  if (!bal.available || !bal.balances || bal.balances.length === 0) {
2562
3037
  return React.createElement('div', { className: 'dshw_balanceCard' },
2563
- React.createElement('div', { className: 'dshw_muted' }, bal.error ? ('\u4f59\u989d\u4e0d\u53ef\u7528: ' + bal.error) : '\u4f59\u989d\u67e5\u8be2\u4e2d\u2026'))
3038
+ React.createElement('div', { className: 'dshw_muted' }, bal.error ? balanceErrorText(bal.error) : '余额查询中…'))
2564
3039
  }
2565
- var first = bal.balances[0]
2566
- var others = bal.balances.slice(1)
3040
+ var first = selectBalanceInfo(bal)
3041
+ var others = bal.balances.filter(function (info) { return info !== first })
2567
3042
  var subParts = [
2568
3043
  React.createElement('span', { key: 'top' }, '\u5145\u503c ' + fmtCurrency(first.topped_up_balance, first.currency)),
2569
3044
  React.createElement('span', { key: 'd1', className: 'dshw_balDot' }, '\u00b7'),
@@ -2573,11 +3048,13 @@ function fmtCurrency(value, currency) {
2573
3048
  subParts.push(React.createElement('span', { key: 'd' + i, className: 'dshw_balDot' }, '\u00b7'))
2574
3049
  subParts.push(React.createElement('span', { key: 'x' + i }, info.currency + ' ' + fmtCurrency(info.total_balance, info.currency)))
2575
3050
  })
2576
- return React.createElement('div', { className: low ? 'dshw_balanceCard dshw_low' : 'dshw_balanceCard' },
3051
+ return React.createElement('div', {
3052
+ className: low ? 'dshw_balanceCard dshw_low' : 'dshw_balanceCard'
3053
+ },
2577
3054
  React.createElement('div', { className: 'dshw_balLine' },
2578
3055
  React.createElement('span', { className: 'dshw_muted' }, '\u4f59\u989d'),
2579
3056
  React.createElement('span', { className: 'dshw_balNum' }, fmtCurrency(first.total_balance, first.currency)),
2580
- low ? React.createElement('span', { className: 'dshw_balWarn', title: '\u4f4e\u4e8e\u63d0\u9192\u9608\u503c \u00a5' + (snapshot.threshold !== undefined ? snapshot.threshold : '') }, '\u4f59\u989d\u504f\u4f4e') : null),
3057
+ low ? React.createElement('span', { className: 'dshw_balWarn', title: '低于提醒阈值 ' + fmtCurrency(snapshot.threshold !== undefined ? snapshot.threshold : 0, bal.currency || 'CNY') }, '余额偏低') : null),
2581
3058
  React.createElement('div', { className: 'dshw_balSub' },
2582
3059
  sessionCostLabel(bal.currency) + ' ' + (official.cost === null ? '--' : sessionCostText(official.cost, bal.currency)),
2583
3060
  React.createElement('span', { className: 'dshw_balDot' }, '\u00b7'),
@@ -2689,7 +3166,7 @@ function fmtCurrency(value, currency) {
2689
3166
  },
2690
3167
  floatBtnRow,
2691
3168
  showOfficial ? React.createElement(React.Fragment, null,
2692
- React.createElement('div', { className: 'dshw_title' }, (snapshot.accounts && snapshot.accounts.activeName) ? '\u5b98\u65b9 DeepSeek \u00b7 ' + snapshot.accounts.activeName : '\u5b98\u65b9 DeepSeek'),
3169
+ React.createElement('div', { className: 'dshw_title' }, (snapshot.accounts && snapshot.accounts.activeName) ? '官方 DeepSeek · ' + snapshot.accounts.activeName : '官方 DeepSeek'),
2693
3170
  balanceCard(),
2694
3171
  official.tokens ? React.createElement('div', { key: 'o-t', className: 'dshw_row' },
2695
3172
  React.createElement('span', { className: 'dshw_muted' }, '\u5b98\u65b9 token'),
@@ -2707,7 +3184,7 @@ function fmtCurrency(value, currency) {
2707
3184
  className: 'dshw_btn dshw_btnPrimary',
2708
3185
  onClick: onRechargeClick
2709
3186
  }, '\u2197 \u5145\u503c'),
2710
- React.createElement('button', { type: 'button', className: 'dshw_btn', onClick: refreshBalance }, '\u5237\u65b0\u4f59\u989d'),
3187
+ React.createElement('button', { type: 'button', className: 'dshw_btn', onClick: refreshBalance }, '刷新余额'),
2711
3188
  React.createElement('button', {
2712
3189
  type: 'button',
2713
3190
  className: 'dshw_btn',
@@ -2716,7 +3193,7 @@ function fmtCurrency(value, currency) {
2716
3193
  onClick: function () {
2717
3194
  if (window.confirm('确认清除本会话的余额与 token 数据?不可恢复。')) clearSession()
2718
3195
  }
2719
- }, '\u6e05\u9664')),
3196
+ }, '清除')),
2720
3197
  React.createElement('div', { className: 'dshw_divider' })) : null,
2721
3198
  accountsSection(),
2722
3199
  React.createElement('div', { style: { marginTop: '8px', textAlign: 'right', fontSize: '10px', color: 'var(--dsw-alias-label-quaternary,rgba(31,35,40,.45))', userSelect: 'none' } }, 'DeepSeek Harness Control Center v' + WALLET_VERSION)
@@ -2774,7 +3251,7 @@ function fmtCurrency(value, currency) {
2774
3251
  permanentDeleteControl,
2775
3252
  showOfficial ? React.createElement(React.Fragment, null,
2776
3253
  React.createElement('div', { className: 'dshw_row' },
2777
- React.createElement('span', { className: 'dshw_muted' }, '阈值(¥,0=关)'),
3254
+ React.createElement('span', { className: 'dshw_muted' }, '阈值(' + (bal.currency || 'CNY') + ',0=关)'),
2778
3255
  thresholdInput),
2779
3256
  React.createElement('div', { className: 'dshw_row' },
2780
3257
  React.createElement('button', { type: 'button', className: 'dshw_btn', onClick: refreshBalance }, '刷新'),
@@ -2831,6 +3308,16 @@ function fmtCurrency(value, currency) {
2831
3308
  inject: function () { return {} }
2832
3309
  }, WalletSettingsSection)
2833
3310
  })
3311
+ try {
3312
+ ctx.slots.inject('sidebar.footer.action', function () {
3313
+ return ctx.slots.register({
3314
+ name: 'sidebar.footer.action',
3315
+ id: 'wallet-peak-ring',
3316
+ order: 50,
3317
+ inject: function () { return {} }
3318
+ }, PeakRingFooter)
3319
+ })
3320
+ } catch (e) { /* host without the sidebar footer slot */ }
2834
3321
  }
2835
3322
  }
2836
3323
 
@@ -2845,11 +3332,17 @@ function fmtCurrency(value, currency) {
2845
3332
  computePanelPosition: computePanelPosition,
2846
3333
  createCompatibilityAdapter: createCompatibilityAdapter,
2847
3334
  fmtCurrency: fmtCurrency,
3335
+ selectBalanceInfo: selectBalanceInfo,
3336
+ balanceErrorText: balanceErrorText,
2848
3337
  installCompletionNotifier: installCompletionNotifier,
2849
3338
  normalizeDataVisibility: normalizeDataVisibility,
2850
3339
  normalizeNotifyConfig: normalizeNotifyConfig,
2851
3340
  normalizeChipLayout: normalizeChipLayout,
2852
3341
  normalizeChipScale: normalizeChipScale,
3342
+ peakClockState: peakClockState,
3343
+ wallHourIn: wallHourIn,
3344
+ peakRingSVG: peakRingSVG,
3345
+ PeakRingFooter: PeakRingFooter,
2853
3346
  WalletSettingsSection: WalletSettingsSection,
2854
3347
  settleDotPosition: settleDotPosition
2855
3348
  }
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.1",
4
+ "version": "0.2.3",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "exports": {