deepseek-harness-wallet 0.2.2 → 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,14 @@
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
+
5
13
  ## 0.2.2 - 2026-08-20
6
14
 
7
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.
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.**
@@ -25,7 +25,8 @@
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
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.
27
27
  - **Third-party total** — current-session tokens (input / cache read / output). No balance guessing, no cost math, zero configuration.
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
+ - **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).
29
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.
30
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.
31
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.
@@ -127,7 +128,7 @@ For buildable DSH hosts, the npm package and repository include a versioned [Age
127
128
 
128
129
  | Item | Behavior |
129
130
  | --- | --- |
130
- | 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. |
131
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. |
132
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. |
133
134
  | Session log | The plugin writes no events; its data lives in `$DSH_HOME/storages/wallet.json`. |
package/index.js CHANGED
@@ -386,16 +386,18 @@ export function removeAccount(id) {
386
386
  const index = accounts.accounts.findIndex((account) => account.id === id)
387
387
  if (index < 0) return { ok: false, error: 'account not found' }
388
388
  accounts.accounts.splice(index, 1)
389
- // 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
390
392
  if (store.accountThresholds && Object.hasOwn(store.accountThresholds, id)) {
391
393
  delete store.accountThresholds[id]
392
- scheduleAccountsSave(null)
394
+ thresholdRemoved = true
393
395
  }
394
396
  // Deliberately NOT unsetting the credentials seam: the key currently in
395
397
  // .credentials.yaml keeps working for LLM billing; the UI just reports
396
398
  // "no active account" and balance falls back to the seam key.
397
399
  if (accounts.activeId === id) accounts.activeId = null
398
- return { ok: true }
400
+ return { ok: true, thresholdRemoved }
399
401
  }
400
402
 
401
403
  export function accountListView() {
@@ -485,12 +487,18 @@ function recordUsage(options, usage, atMs = Date.now()) {
485
487
  if (typeof sessionId !== 'string' || sessionId === '' || typeof provider !== 'string' || provider === '') return
486
488
  if (typeof model !== 'string' || model === '__proto__' || model === 'prototype' || model === 'constructor') return
487
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
+ }
488
495
  if (!Object.hasOwn(store.sessions, sessionId)) {
489
496
  store.sessions[sessionId] = { official: emptyBucket(true), third: emptyBucket(false) }
490
497
  }
491
498
  const session = store.sessions[sessionId]
492
- const bucket = provider === OFFICIAL_PROVIDER ? session.official : session.third
493
- 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)
494
502
  else {
495
503
  if (!Object.hasOwn(bucket.models, model)) bucket.models[model] = emptyCounters()
496
504
  addUsage(bucket.models[model], usage)
@@ -503,13 +511,13 @@ let balanceRefresh = null
503
511
  async function performBalanceRefresh(ctx) {
504
512
  const credentials = ctx.get('credentials')
505
513
  if (credentials === undefined) {
506
- balance = { fetchedAt: Date.now(), available: false, balances: [], error: 'no credentials service' }
514
+ balance = { fetchedAt: Date.now(), available: false, balances: [], error: 'no-credentials' }
507
515
  return
508
516
  }
509
517
  try {
510
518
  const key = await resolveBalanceKey(ctx)
511
519
  if (key === undefined || key === '') {
512
- balance = { fetchedAt: Date.now(), available: false, balances: [], error: 'no API key' }
520
+ balance = { fetchedAt: Date.now(), available: false, balances: [], error: 'no-api-key' }
513
521
  return
514
522
  }
515
523
  const controller = new AbortController()
@@ -520,23 +528,36 @@ async function performBalanceRefresh(ctx) {
520
528
  headers: { authorization: 'Bearer ' + key },
521
529
  signal: controller.signal,
522
530
  })
523
- 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
+ }
524
539
  const data = await response.json()
540
+ const available = data !== null && typeof data === 'object' && data.is_available === true
525
541
  balance = {
526
542
  fetchedAt: Date.now(),
527
- available: data.is_available === true,
528
- balances: Array.isArray(data.balance_infos) ? data.balance_infos : [],
529
- error: null,
543
+ available,
544
+ balances: available && Array.isArray(data.balance_infos) ? data.balance_infos : [],
545
+ error: available ? null : 'balance-unavailable',
530
546
  }
531
547
  } finally {
532
548
  clearTimeout(timer)
533
549
  }
534
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'
535
556
  balance = {
536
557
  fetchedAt: Date.now(),
537
558
  available: false,
538
559
  balances: [],
539
- error: error instanceof Error ? error.message : String(error),
560
+ error: code,
540
561
  }
541
562
  }
542
563
  }
@@ -836,7 +857,8 @@ export function apply(ctx, config) {
836
857
  if (body === null || typeof body.name !== 'string' || typeof body.apiKey !== 'string') {
837
858
  return json(res, 400, { ok: false, error: 'name and apiKey are required' })
838
859
  }
839
- const needSync = accounts.activeId === null
860
+ const previousActiveId = accounts.activeId
861
+ const needSync = previousActiveId === null
840
862
  const result = addAccount(body.name, body.apiKey)
841
863
  if (!result.ok) return json(res, 400, { ok: false, error: result.error })
842
864
  scheduleAccountsSave(ctx.logger)
@@ -844,6 +866,13 @@ export function apply(ctx, config) {
844
866
  if (needSync) {
845
867
  // First account: activate it so the LLM seam follows the new key.
846
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
+ }
847
876
  }
848
877
  return json(res, 200, {
849
878
  ok: true,
@@ -884,6 +913,7 @@ export function apply(ctx, config) {
884
913
  const result = removeAccount(body.id)
885
914
  if (!result.ok) return json(res, 400, { ok: false, error: result.error })
886
915
  scheduleAccountsSave(ctx.logger)
916
+ if (result.thresholdRemoved) scheduleSave(ctx.logger)
887
917
  return json(res, 200, { ok: true, ...accountListView() })
888
918
  },
889
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.2'
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'
@@ -599,6 +599,32 @@ window.__ModuleLoader__.load({
599
599
  }
600
600
  }
601
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
+
602
628
  function computePanelPosition(chipRect, panelRect, viewportWidth, viewportHeight) {
603
629
  var margin = 8
604
630
  var gap = 6
@@ -664,6 +690,11 @@ window.__ModuleLoader__.load({
664
690
  function normalizeNotifyConfig(value) {
665
691
  var enabled = !value || value.enabled !== false
666
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
+ }
667
698
  if ([0, 5, 10, 30, 60].indexOf(timeout) === -1) timeout = 10
668
699
  return { enabled: enabled, timeout: timeout }
669
700
  }
@@ -722,7 +753,7 @@ window.__ModuleLoader__.load({
722
753
  }
723
754
  var item = active.item
724
755
  var remaining = queue.length
725
- var body = item.title + (remaining > 0 ? '\n另有 ' + remaining + ' 个对话等待提醒' : '\n点击打开该对话')
756
+ var body = item.title + (remaining > 0 ? '\r\n另有 ' + remaining + ' 个对话等待提醒' : '\r\n点击打开该对话')
726
757
  if (active.timer !== null) clearTimeout(active.timer)
727
758
  if (active.notification) {
728
759
  active.notification.onclose = null
@@ -1051,7 +1082,7 @@ window.__ModuleLoader__.load({
1051
1082
  var [notifyEnabled, setNotifyEnabled] = React.useState(function () { return readNotifyConfig().enabled })
1052
1083
  var [notifyTimeout, setNotifyTimeout] = React.useState(function () {
1053
1084
  var cfg = readNotifyConfig()
1054
- return cfg.autoCloseMs === null ? 'keep' : String(Math.round(cfg.autoCloseMs / 1000))
1085
+ return cfg.timeout === 0 ? 'keep' : String(cfg.timeout)
1055
1086
  })
1056
1087
  var [lowBlinkEnabled, setLowBlinkEnabled] = React.useState(function () {
1057
1088
  try { return compatibility.storage.getItem(LOW_BLINK_KEY) !== 'false' } catch (e) { return true }
@@ -1120,13 +1151,13 @@ window.__ModuleLoader__.load({
1120
1151
  }
1121
1152
 
1122
1153
  function persistNotify(enabled, timeout) {
1123
- var autoCloseMs = timeout === 'keep' ? null : Number.parseInt(timeout, 10) * 1000
1124
- 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
1125
1156
  try {
1126
- 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 }))
1127
1158
  } catch (e) { /* ignore */ }
1128
1159
  setNotifyEnabled(enabled)
1129
- setNotifyTimeout(timeout)
1160
+ setNotifyTimeout(timeoutSeconds === 0 ? 'keep' : String(timeoutSeconds))
1130
1161
  compatibility.dispatch(NOTIFY_CONFIG_EVENT)
1131
1162
  }
1132
1163
 
@@ -1224,7 +1255,7 @@ window.__ModuleLoader__.load({
1224
1255
  var lowBalance = snapshot && snapshot.lowBalance === true
1225
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) : '--'
1226
1257
  var currencyCode = bal && typeof bal.currency === 'string' ? bal.currency : 'CNY'
1227
- var primaryBalance = bal && Array.isArray(bal.balances) && bal.balances.length > 0 ? bal.balances[0] : null
1258
+ var primaryBalance = selectBalanceInfo(bal)
1228
1259
  var toppedText = primaryBalance ? fmtCurrency(primaryBalance.topped_up_balance, primaryBalance.currency) : '--'
1229
1260
  var grantedText = primaryBalance ? fmtCurrency(primaryBalance.granted_balance, primaryBalance.currency) : '--'
1230
1261
  rows.push(React.createElement('div', { key: 'head', className: 'dshw_settingsHeader' },
@@ -1477,7 +1508,7 @@ window.__ModuleLoader__.load({
1477
1508
  onChange: function (event) { setNameDraft(event.target.value) }
1478
1509
  }),
1479
1510
  React.createElement('input', {
1480
- className: 'dshw_input',
1511
+ className: 'dshw_input', type: 'password',
1481
1512
  placeholder: 'sk-...', 'aria-label': 'API Key',
1482
1513
  value: keyDraft,
1483
1514
  onInput: function (event) { setKeyDraft(event.target.value) },
@@ -1952,7 +1983,8 @@ window.__ModuleLoader__.load({
1952
1983
  if (showOfficial && json.lowBalance) {
1953
1984
  if (!notifiedRef.current) {
1954
1985
  try {
1955
- 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' })
1956
1988
  if (lowNotice) notifiedRef.current = true
1957
1989
  } catch (e) { /* ignore */ }
1958
1990
  }
@@ -2554,7 +2586,7 @@ window.__ModuleLoader__.load({
2554
2586
 
2555
2587
  function activateAccount(id, name) {
2556
2588
  if (switchingId !== null) return
2557
- 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
2558
2590
  setSwitchingId(id)
2559
2591
  setAccountNotice(null)
2560
2592
  fetch('/api/wallet/accounts/activate', {
@@ -2585,7 +2617,7 @@ window.__ModuleLoader__.load({
2585
2617
  }
2586
2618
 
2587
2619
  function removeAccount(id, name) {
2588
- 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
2589
2621
  fetch('/api/wallet/accounts/remove', {
2590
2622
  method: 'POST',
2591
2623
  headers: { 'content-type': 'application/json' },
@@ -2868,7 +2900,7 @@ window.__ModuleLoader__.load({
2868
2900
  type: 'number',
2869
2901
  min: '0',
2870
2902
  step: 'any',
2871
- 'aria-label': '\u4f59\u989d\u63d0\u9192\u9608\u503c\uff08CNY\uff09',
2903
+ 'aria-label': '余额提醒阈值(' + (bal.currency || 'CNY') + ')',
2872
2904
  value: thresholdDraft === null ? '' : thresholdDraft,
2873
2905
  onChange: function (event) {
2874
2906
  thresholdInitializedRef.current = 'editing'
@@ -3003,10 +3035,10 @@ window.__ModuleLoader__.load({
3003
3035
  function balanceCard() {
3004
3036
  if (!bal.available || !bal.balances || bal.balances.length === 0) {
3005
3037
  return React.createElement('div', { className: 'dshw_balanceCard' },
3006
- 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) : '余额查询中…'))
3007
3039
  }
3008
- var first = bal.balances[0]
3009
- var others = bal.balances.slice(1)
3040
+ var first = selectBalanceInfo(bal)
3041
+ var others = bal.balances.filter(function (info) { return info !== first })
3010
3042
  var subParts = [
3011
3043
  React.createElement('span', { key: 'top' }, '\u5145\u503c ' + fmtCurrency(first.topped_up_balance, first.currency)),
3012
3044
  React.createElement('span', { key: 'd1', className: 'dshw_balDot' }, '\u00b7'),
@@ -3017,25 +3049,16 @@ window.__ModuleLoader__.load({
3017
3049
  subParts.push(React.createElement('span', { key: 'x' + i }, info.currency + ' ' + fmtCurrency(info.total_balance, info.currency)))
3018
3050
  })
3019
3051
  return React.createElement('div', {
3020
- className: low ? 'dshw_balanceCard dshw_low' : 'dshw_balanceCard',
3021
- style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px' }
3052
+ className: low ? 'dshw_balanceCard dshw_low' : 'dshw_balanceCard'
3022
3053
  },
3023
- React.createElement('div', { style: { flex: '1 1 auto', minWidth: 0 } },
3024
- React.createElement('div', { className: 'dshw_balLine' },
3025
- React.createElement('span', { className: 'dshw_muted' }, '\u4f59\u989d'),
3026
- React.createElement('span', { className: 'dshw_balNum' }, fmtCurrency(first.total_balance, first.currency)),
3027
- 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),
3028
- React.createElement('div', { className: 'dshw_balSub' },
3029
- sessionCostLabel(bal.currency) + ' ' + (official.cost === null ? '--' : sessionCostText(official.cost, bal.currency)),
3030
- React.createElement('span', { className: 'dshw_balDot' }, '\u00b7'),
3031
- subParts)),
3032
- React.createElement('button', {
3033
- type: 'button',
3034
- className: 'dshw_btn dshw_btnPrimary',
3035
- style: { height: '28px', padding: '0 10px', fontSize: '11px', flex: 'none', whiteSpace: 'nowrap' },
3036
- onClick: onRechargeClick,
3037
- 'aria-label': '前往官方充值'
3038
- }, '↗ 充值'))
3054
+ React.createElement('div', { className: 'dshw_balLine' },
3055
+ React.createElement('span', { className: 'dshw_muted' }, '\u4f59\u989d'),
3056
+ React.createElement('span', { className: 'dshw_balNum' }, fmtCurrency(first.total_balance, first.currency)),
3057
+ low ? React.createElement('span', { className: 'dshw_balWarn', title: '低于提醒阈值 ' + fmtCurrency(snapshot.threshold !== undefined ? snapshot.threshold : 0, bal.currency || 'CNY') }, '余额偏低') : null),
3058
+ React.createElement('div', { className: 'dshw_balSub' },
3059
+ sessionCostLabel(bal.currency) + ' ' + (official.cost === null ? '--' : sessionCostText(official.cost, bal.currency)),
3060
+ React.createElement('span', { className: 'dshw_balDot' }, '\u00b7'),
3061
+ subParts))
3039
3062
  }
3040
3063
 
3041
3064
  // All controls in one bordered card with compact rows; the threshold
@@ -3228,7 +3251,7 @@ window.__ModuleLoader__.load({
3228
3251
  permanentDeleteControl,
3229
3252
  showOfficial ? React.createElement(React.Fragment, null,
3230
3253
  React.createElement('div', { className: 'dshw_row' },
3231
- React.createElement('span', { className: 'dshw_muted' }, '阈值(¥,0=关)'),
3254
+ React.createElement('span', { className: 'dshw_muted' }, '阈值(' + (bal.currency || 'CNY') + ',0=关)'),
3232
3255
  thresholdInput),
3233
3256
  React.createElement('div', { className: 'dshw_row' },
3234
3257
  React.createElement('button', { type: 'button', className: 'dshw_btn', onClick: refreshBalance }, '刷新'),
@@ -3309,6 +3332,8 @@ window.__ModuleLoader__.load({
3309
3332
  computePanelPosition: computePanelPosition,
3310
3333
  createCompatibilityAdapter: createCompatibilityAdapter,
3311
3334
  fmtCurrency: fmtCurrency,
3335
+ selectBalanceInfo: selectBalanceInfo,
3336
+ balanceErrorText: balanceErrorText,
3312
3337
  installCompletionNotifier: installCompletionNotifier,
3313
3338
  normalizeDataVisibility: normalizeDataVisibility,
3314
3339
  normalizeNotifyConfig: normalizeNotifyConfig,
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.2",
4
+ "version": "0.2.3",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "exports": {