deepseek-harness-wallet 0.2.0 → 0.2.2

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,19 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## 0.2.2 - 2026-08-20
6
+
7
+ - 新增 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.
8
+ - 侧边栏时钟卡片视觉重构:三行式清爽信息流,字号与容器全面放大,右侧内置紧凑竖排「充值」按钮直达官方充值后台,消除横向挤压与文本截断。 / 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.
9
+ - 峰谷切换系统通知:跨越峰谷时段时自动推送桌面系统通知(可在设置中随心开启/关闭),不错过半价调用的省钱窗口。 / Added peak/off-peak desktop switch reminders with a dedicated toggle in settings.
10
+ - 跨组件毫秒级实时同步:输入框钱包芯片与侧边栏时钟卡片通过全局事件总线(`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.
11
+ - 会话花费彻底杜绝 `--` 占位:始终常驻真实货币金额(无消耗显示 $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).
12
+
13
+ ## 0.2.1 - 2026-08-20
14
+
15
+ - 标签上「本场」恢复常显(¥0.00 也显示,新会话不再缺席);仅未定价模型仍隐藏。/ The chip shows the session cost again even at ¥0.00; only unpriced models stay hidden.
16
+ - 面板/浮动窗版本号移至底部右下角,不再与顶栏按钮挤压。 / Panel version tags moved to the footer, clear of the header buttons.
17
+
5
18
  ## 0.2.0 - 2026-08-18
6
19
 
7
20
  - 新增多账户管理与热切换:面板内“账户管理”可添加多个账户(名称 + API Key),切换后无需重启,下一次 LLM 调用即按新账户计费;key 界面掩码显示,余额查询跟随当前账户(contributed in PR #4 by mxchen-xyz)。 / Added multi-account management with hot switching: manage accounts in the panel, and the very next LLM call is billed with the newly activated key — no restart needed; keys stay masked in the UI and balance follows the active account.
package/README.md CHANGED
@@ -23,6 +23,7 @@
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
28
  - **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
29
  - **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.
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) {
@@ -110,6 +114,41 @@ export function normalizeThreshold(value) {
110
114
  return Math.min(100000, Math.max(0, Math.round(parsed * 100) / 100))
111
115
  }
112
116
 
117
+ // Low-balance thresholds are per-currency: a USD account warns against its
118
+ // own $ threshold, a CNY account against its own ¥ one; they never compare
119
+ // across currencies. The legacy single-value store migrates into CNY.
120
+ // Per-ACCOUNT thresholds: two accounts in the same currency may keep
121
+ // different warning lines ("one account is 1, another is 2"). Keyed by
122
+ // account id; the per-currency map stays as the fallback for the
123
+ // no-active-account (system key) case and as the migration source.
124
+ export function normalizeAccountThresholds(value) {
125
+ const source = value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {}
126
+ const out = {}
127
+ for (const [id, raw] of Object.entries(source)) {
128
+ if (typeof id !== 'string' || id === '' || id.length > 100) continue
129
+ if (id === '__proto__' || id === 'prototype' || id === 'constructor') continue
130
+ const parsed = Number.parseFloat(raw)
131
+ if (!Number.isFinite(parsed)) continue
132
+ out[id] = Math.min(100000, Math.max(0, Math.round(parsed * 100) / 100))
133
+ if (Object.keys(out).length >= 50) break
134
+ }
135
+ return out
136
+ }
137
+
138
+ export function normalizeThresholds(value) {
139
+ const source = value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {}
140
+ const out = {}
141
+ for (const [code, raw] of Object.entries(source)) {
142
+ if (!/^[A-Z]{3}$/.test(code)) continue
143
+ if (code === '__proto__' || code === 'prototype' || code === 'constructor') continue
144
+ const parsed = Number.parseFloat(raw)
145
+ if (!Number.isFinite(parsed)) continue
146
+ out[code] = Math.min(100000, Math.max(0, Math.round(parsed * 100) / 100))
147
+ if (Object.keys(out).length >= 20) break
148
+ }
149
+ return out
150
+ }
151
+
113
152
  function finiteCounter(value) {
114
153
  return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : 0
115
154
  }
@@ -188,7 +227,8 @@ export function normalizeStoreData(value, atMs = Date.now()) {
188
227
  }
189
228
  const normalized = {
190
229
  version: STORE_VERSION,
191
- threshold: normalizeThreshold(source.threshold),
230
+ thresholds: normalizeThresholds(source.thresholds && Object.keys(source.thresholds).length > 0 ? source.thresholds : { CNY: normalizeThreshold(source.threshold) }),
231
+ accountThresholds: normalizeAccountThresholds(source.accountThresholds),
192
232
  sessions,
193
233
  officialProviders: normalizeProviderList(source.officialProviders),
194
234
  knownProviders: normalizeProviderList(source.knownProviders),
@@ -201,7 +241,7 @@ function loadStore() {
201
241
  return normalizeStoreData(JSON.parse(readFileSync(STORE_PATH, 'utf8')))
202
242
  } catch {
203
243
  return {
204
- store: { version: STORE_VERSION, threshold: DEFAULT_THRESHOLD, sessions: {}, officialProviders: [], knownProviders: [] },
244
+ store: { version: STORE_VERSION, thresholds: { CNY: DEFAULT_THRESHOLD }, accountThresholds: {}, sessions: {}, officialProviders: [], knownProviders: [] },
205
245
  migrated: false,
206
246
  }
207
247
  }
@@ -346,6 +386,11 @@ export function removeAccount(id) {
346
386
  const index = accounts.accounts.findIndex((account) => account.id === id)
347
387
  if (index < 0) return { ok: false, error: 'account not found' }
348
388
  accounts.accounts.splice(index, 1)
389
+ // Drop the account's own threshold line along with it.
390
+ if (store.accountThresholds && Object.hasOwn(store.accountThresholds, id)) {
391
+ delete store.accountThresholds[id]
392
+ scheduleAccountsSave(null)
393
+ }
349
394
  // Deliberately NOT unsetting the credentials seam: the key currently in
350
395
  // .credentials.yaml keeps working for LLM billing; the UI just reports
351
396
  // "no active account" and balance falls back to the seam key.
@@ -551,13 +596,14 @@ function sessionView(sessionId) {
551
596
  official: {
552
597
  tokens: official,
553
598
  cost: session.official.priced === true ? session.official.cost : null,
599
+ priced: session.official.priced === true,
554
600
  models: session.official.models,
555
601
  },
556
602
  third: { tokens: third, models: session.third.models },
557
603
  }
558
604
  }
559
605
 
560
- function snapshotView(sessionId, threshold) {
606
+ function snapshotView(sessionId) {
561
607
  const currency = balanceCurrency(balance.balances)
562
608
  const active = activeAccount()
563
609
  return {
@@ -571,11 +617,28 @@ function snapshotView(sessionId, threshold) {
571
617
  error: balance.available ? null : balance.error,
572
618
  },
573
619
  session: sessionView(sessionId),
574
- threshold,
575
- // The configured threshold is CNY-denominated; do not compare unlike
576
- // currencies for international accounts that do not expose a CNY row.
577
- lowBalance: balance.available && currency === 'CNY' && threshold > 0 && balanceTotal() < threshold,
620
+ // Thresholds are per-ACCOUNT first (each account keeps its own line even
621
+ // in the same currency); the per-currency map only serves the
622
+ // no-active-account case and inherits into accounts without one yet.
623
+ threshold: active !== null
624
+ ? (store.accountThresholds[active.id] ?? store.thresholds[currency || 'CNY'] ?? 0)
625
+ : (store.thresholds[currency || 'CNY'] ?? 0),
626
+ lowBalance: balance.available && currency !== null
627
+ && (function () {
628
+ const line = active !== null
629
+ ? (store.accountThresholds[active.id] ?? store.thresholds[currency] ?? 0)
630
+ : (store.thresholds[currency] ?? 0)
631
+ return line > 0 && balanceTotal() < line
632
+ })(),
578
633
  rechargeUrl: RECHARGE_URL,
634
+ // Peak/off-peak policy for the ring clock; billed in Asia/Shanghai.
635
+ pricingWindows: {
636
+ timezone: 'Asia/Shanghai',
637
+ offsetMinutes: 480,
638
+ windows: PEAK_WINDOWS.map(w => ({ startHour: w.startHour, endHour: w.endHour })),
639
+ offPeakRate: OFF_PEAK_RATE,
640
+ isPeak: isBeijingPeak(Date.now()),
641
+ },
579
642
  accounts: {
580
643
  activeId: accounts.activeId,
581
644
  activeName: active !== null ? active.name : null,
@@ -637,12 +700,10 @@ export function apply(ctx, config) {
637
700
  storeNeedsSave = false
638
701
  scheduleSave(ctx.logger)
639
702
  }
640
- // Threshold lives ONLY in the persisted store; an explicit row config may
641
- // still override it for power users, but the bundle patch sets none.
642
- let threshold = store.threshold
703
+ // Thresholds live ONLY in the persisted store; an explicit row config may
704
+ // still override the CNY line for power users, but the bundle patch sets none.
643
705
  if (Number.isFinite(config.threshold)) {
644
- threshold = normalizeThreshold(config.threshold)
645
- store.threshold = threshold
706
+ store.thresholds = normalizeThresholds({ ...store.thresholds, CNY: normalizeThreshold(config.threshold) })
646
707
  scheduleSave(ctx.logger)
647
708
  }
648
709
 
@@ -694,7 +755,7 @@ export function apply(ctx, config) {
694
755
  path: '/api/wallet/snapshot',
695
756
  handler: (req, res) => {
696
757
  if (req.method !== 'GET') return json(res, 405, { ok: false, error: 'method-not-allowed' })
697
- return json(res, 200, snapshotView(sessionParam(req), threshold))
758
+ return json(res, 200, snapshotView(sessionParam(req)))
698
759
  },
699
760
  })
700
761
  const disposeThreshold = ctx.webServer.register({
@@ -706,10 +767,19 @@ export function apply(ctx, config) {
706
767
  if (body === null || typeof body.threshold !== 'number' || !Number.isFinite(body.threshold)) {
707
768
  return json(res, 400, { ok: false, error: 'threshold must be a number' })
708
769
  }
709
- threshold = Math.min(100000, Math.max(0, Math.round(body.threshold * 100) / 100))
710
- store.threshold = threshold
770
+ const currency = typeof body.currency === 'string' && /^[A-Z]{3}$/.test(body.currency) ? body.currency : (balanceCurrency(balance.balances) || 'CNY')
771
+ if (currency === '__proto__' || currency === 'prototype' || currency === 'constructor') return json(res, 400, { ok: false, error: 'bad currency' })
772
+ const clamped = Math.min(100000, Math.max(0, Math.round(body.threshold * 100) / 100))
773
+ // An active account keeps its own threshold line; only the system-key
774
+ // mode writes the shared per-currency map.
775
+ const active = activeAccount()
776
+ if (active !== null) {
777
+ store.accountThresholds = normalizeAccountThresholds({ ...store.accountThresholds, [active.id]: clamped })
778
+ } else {
779
+ store.thresholds = normalizeThresholds({ ...store.thresholds, [currency]: clamped })
780
+ }
711
781
  scheduleSave(ctx.logger)
712
- return json(res, 200, { ok: true, threshold: threshold })
782
+ return json(res, 200, { ok: true, threshold: clamped, currency, accountId: active !== null ? active.id : null })
713
783
  },
714
784
  })
715
785
  const disposeRefresh = ctx.webServer.register({
@@ -797,7 +867,11 @@ export function apply(ctx, config) {
797
867
  if (body === null || typeof body.id !== 'string') return json(res, 400, { ok: false, error: 'id is required' })
798
868
  const result = await activateAccount(ctx, body.id)
799
869
  if (!result.ok) return json(res, 400, { ok: false, error: result.error })
800
- return json(res, 200, { ok: true, account: result.account })
870
+ // Carry the account's own threshold so the input can jump instantly,
871
+ // before the balance refresh lands.
872
+ const currency = balanceCurrency(balance.balances) || 'CNY'
873
+ const threshold = store.accountThresholds[result.account.id] ?? store.thresholds[currency] ?? 0
874
+ return json(res, 200, { ok: true, account: result.account, threshold })
801
875
  },
802
876
  })
803
877
  const disposeRemove = ctx.webServer.register({
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "integration": "dsh-session-delete",
4
- "packageVersion": "0.1.4",
4
+ "packageVersion": "0.2.0",
5
5
  "upstream": {
6
6
  "repository": "https://github.com/deepseek-ai/DeepSeek-Harness",
7
7
  "commit": "47f943859bef60e4160492346772ded9b24f765a",