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 +13 -0
- package/README.md +1 -0
- package/index.js +91 -17
- package/integrations/dsh-session-delete/compatibility.json +1 -1
- package/lib/client.js +835 -160
- package/package.json +1 -1
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
575
|
-
//
|
|
576
|
-
//
|
|
577
|
-
|
|
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
|
-
//
|
|
641
|
-
// still override
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
710
|
-
|
|
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:
|
|
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
|
-
|
|
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({
|