deepseek-harness-wallet 0.2.4 → 0.2.6
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 +18 -8
- package/index.js +67 -20
- package/lib/client.js +176 -38
- 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.6 - 2026-08-23
|
|
6
|
+
|
|
7
|
+
- 跟随 DeepSeek 官方规则更新峰谷计费:自 2026-08-23 北京时间 00:00 起,周六、周日全天按低谷价计费;工作日高峰仍为 09:00–12:00、14:00–18:00。 / Updated peak/off-peak billing to match DeepSeek's official rule: from 2026-08-23 00:00 Beijing, Saturday and Sunday are all-day off-peak; weekday peak windows remain 09:00–12:00 and 14:00–18:00.
|
|
8
|
+
- 环形卡片同步显示周末全天低谷,并将倒计时指向下一个工作日 09:00。 / Updated the ring card to show weekend all-day off-peak and count down to the next weekday 09:00.
|
|
9
|
+
|
|
10
|
+
## 0.2.5 - 2026-08-23
|
|
11
|
+
|
|
12
|
+
- 修复浮动峰谷卡片在窗口缩放、卡片放大或历史坐标过期后可能跑出视口的问题;加载、缩放、排版切换和 resize 都会重新钳制位置。 / Fixed floating peak cards escaping the viewport after resize, scale changes, or stale saved coordinates; load, scale, layout changes, and resize now re-clamp the position.
|
|
13
|
+
- 恢复 Portal 挂载,确保自由浮动卡片和控制面板脱离侧边栏溢出裁剪。 / Restored Portal mounting so floating cards and the control panel escape sidebar overflow clipping.
|
|
14
|
+
- 完成主题回退清理,移除峰谷控制面板、浮动卡片和开关中的硬编码浅色背景/文字回退。 / Completed theme fallback cleanup for the peak panel, floating card, and switches.
|
|
15
|
+
- 保留账户安全、存储权限、刷新异常和偏好同步加固,并避免原始凭证错误文本进入日志或浏览器。 / Kept account-safety, storage-permission, refresh-failure, and preference-sync hardening without exposing raw credential errors to logs or the browser.
|
|
16
|
+
- 新增浮动坐标边界、Portal 和完整主题回退回归测试;当前测试基线为 78/78。 / Added regression coverage for floating-coordinate bounds, Portal mounting, and complete theme fallbacks; the test baseline is now 78/78.
|
|
17
|
+
|
|
5
18
|
## 0.2.4 - 2026-08-22
|
|
6
19
|
|
|
7
20
|
- 彻底修复暗色模式(Dark Mode)主题适配(Issue #26):移除所有未主题化的浅色硬编码回退(`--dsw-alias-bg-elevated,#fff`、`--dsw-alias-bg-overlay,#fff` 等),全站统一使用 DSH 原生主题变量(`--dsw-alias-bg-layer-*` 与 `--dsw-alias-label-dimmed`),在深色模式下完美融入背景。 / Fully fixed Dark Mode theme compliance (Issue #26): replaced unthemed hardcoded light fallbacks with DSH native theme variables (`--dsw-alias-bg-layer-*`), rendering dark themes seamlessly without blinding white boxes.
|
package/README.md
CHANGED
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
余额 ¥5.89 · 本场 ¥0.72 · 官 18.8M | 三方 800K · ↗充
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
- **Official DeepSeek** — live balance (60s global refresh with fast boot retries), current-session cost locked to the price active for each usage event
|
|
26
|
-
- **
|
|
25
|
+
- **Official DeepSeek** — live balance (60s global refresh with fast boot retries), an estimated current-session cost (not an official bill) locked to the price active for each usage event, including the 2026-08-17 peak/off-peak rollout, and token breakdown.
|
|
26
|
+
- **v4 peak/off-peak ring clock** — a resident 24-hour sidebar footer widget for `v4-flash` and `v4-pro`, indicating real-time pricing windows (peak vs. 50% discount off-peak), countdown to the next switch, and optional desktop switch notifications. Weekdays use 09:00–12:00 and 14:00–18:00 Beijing time; Saturday and Sunday are all-day off-peak from 2026-08-23. Other models keep their flat rates.
|
|
27
27
|
- **Third-party total** — current-session tokens (input / cache read / output). No balance guessing, no cost math, zero configuration.
|
|
28
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
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).
|
|
@@ -34,13 +34,14 @@
|
|
|
34
34
|
- **Low-balance alert** — below the threshold the chip turns red with a breathing animation and fires one desktop notification; it resets automatically once the balance recovers.
|
|
35
35
|
- **Theme-native UI** — built entirely on `--dsw-alias-*` theme variables, so light and dark themes both render correctly; the panel closes when you click outside and flips open-direction near screen edges.
|
|
36
36
|
- **Clear current-session wallet data** — one button clears only the open conversation's token/cost records; it does not delete the conversation, and every other conversation is untouched.
|
|
37
|
-
|
|
38
37
|
|
|
39
|
-
-
|
|
38
|
+
## Multi-account
|
|
39
|
+
|
|
40
|
+
- Open the wallet panel → **Account Management(账户管理)** to add accounts (name + API key), switch the active one, or remove them.
|
|
40
41
|
- The first account added becomes the active account automatically and is synced into the credentials seam.
|
|
41
42
|
- Switching prompts a confirmation because it changes **LLM billing** for subsequent requests: the switch writes the account key into the credentials seam (`credentials.set('DEEPSEEK_API_KEY', ...)`), and since the llm-deepseek provider route resolves that reference per request, the very next LLM call is billed with the new account — no restart needed.
|
|
42
|
-
- Account keys are stored plaintext in `$DSH_HOME/storages/accounts.json`; the UI only
|
|
43
|
-
- Session
|
|
43
|
+
- Account keys are stored in plaintext in `$DSH_HOME/storages/accounts.json`; the UI only shows masked keys. Treat the DSH data directory as a local credential store and protect access to it. Balance lookups prefer the active account's key and fall back to the credentials seam when no account is active.
|
|
44
|
+
- Session usage estimates follow the active account's currency: USD-settled accounts show `本约 $x`, converted from the CNY price table at the vendor's long-standing list ratio (not a live FX rate); CNY accounts show `本场 ¥x`. These are local estimates, not an official invoice.
|
|
44
45
|
- If `DEEPSEEK_API_KEY` is supplied by the launching environment, switching is refused with a clear error (the credentials provider rejects shadowed writes) — unset it in your shell to enable switching.
|
|
45
46
|
|
|
46
47
|
|
|
@@ -78,6 +79,14 @@ dsh plugin --profile web add github:feibi-mochi/deepseek-harness-control-center
|
|
|
78
79
|
|
|
79
80
|
Restart `dsh web`, then hard-refresh the page.
|
|
80
81
|
|
|
82
|
+
## Quick use
|
|
83
|
+
|
|
84
|
+
1. Click the wallet or peak/off-peak card to open its control panel.
|
|
85
|
+
2. Choose horizontal or vertical layout, then use the scale control between 100% and 120%.
|
|
86
|
+
3. Turn the official recharge button off when you need a smaller card; official and third-party rows can also be shown independently.
|
|
87
|
+
4. Drag the card to any open area. If it is hard to find after a layout change, use **Reset/Dock(归位/停靠)** in the panel to return it to the sidebar.
|
|
88
|
+
5. The card follows the host light/dark theme. A hard refresh after upgrading makes sure the new client bundle is loaded.
|
|
89
|
+
|
|
81
90
|
### Update
|
|
82
91
|
|
|
83
92
|
```sh
|
|
@@ -129,8 +138,8 @@ For buildable DSH hosts, the npm package and repository include a versioned [Age
|
|
|
129
138
|
| Item | Behavior |
|
|
130
139
|
| --- | --- |
|
|
131
140
|
| 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. |
|
|
132
|
-
| Balance | The
|
|
133
|
-
| Accounts | Keys live in `$DSH_HOME/storages/accounts.json`
|
|
141
|
+
| Balance | The wallet plugin itself sends the active key directly only to the official `/user/balance` endpoint. When multi-account switching is enabled, the selected key is also written into the DSH credentials seam; DSH may then use it for subsequent model requests. |
|
|
142
|
+
| Accounts | Keys live in `$DSH_HOME/storages/accounts.json` in plaintext, matching the harness's own credential storage. The UI only shows masked keys, and switching writes the chosen key into the credentials seam for subsequent LLM billing. Protect the DSH data directory as you would any local credential store. |
|
|
134
143
|
| Session log | The plugin writes no events; its data lives in `$DSH_HOME/storages/wallet.json`. |
|
|
135
144
|
| Local settings | Layout, scale, visibility, reminder, and panel settings stay in browser-compatible local storage. |
|
|
136
145
|
| Permanent deletion | Opt-in and host-gated. The wallet never advertises the action unless the host implements the matching session deletion path. |
|
|
@@ -146,6 +155,7 @@ CNY per 1M tokens, curated from official announcements (cache writes are not bil
|
|
|
146
155
|
- Since 2026-08-17 00:00 Beijing — peak/off-peak pricing for the v4 models (peak windows Beijing 09:00–12:00 / 14:00–18:00; off-peak is half the peak rate):
|
|
147
156
|
- v4-flash (off-peak / peak): cache read 0.05 / 0.10, input 1.5 / 3, output 4.5 / 9
|
|
148
157
|
- v4-pro (off-peak / peak): cache read 0.15 / 0.30, input 4.5 / 9, output 13.5 / 27
|
|
158
|
+
- Since 2026-08-23 00:00 Beijing — Saturday and Sunday are no longer split into peak/off-peak windows; weekend calls use the off-peak rates all day. Weekday peak windows remain 09:00–12:00 and 14:00–18:00.
|
|
149
159
|
|
|
150
160
|
deepseek-chat and deepseek-reasoner keep their flat rates. Each usage event is priced when it arrives; upgrading from 0.1.2 migrates legacy counters once using the then-current rate. Costs are estimates; the API-returned balance is authoritative.
|
|
151
161
|
|
package/index.js
CHANGED
|
@@ -35,14 +35,24 @@ const DEFAULT_THRESHOLD = 5
|
|
|
35
35
|
const BALANCE_REFRESH_MS = 60_000
|
|
36
36
|
const STORE_VERSION = 2
|
|
37
37
|
|
|
38
|
-
// Beijing (UTC+8, no DST) peak windows: 09:00-12:00 and 14:00-18:00.
|
|
38
|
+
// Beijing (UTC+8, no DST) weekday peak windows: 09:00-12:00 and 14:00-18:00.
|
|
39
|
+
// From 2026-08-23 00:00 Beijing, Saturday and Sunday are off-peak all day.
|
|
39
40
|
// Exposed to clients via snapshot.pricingWindows so the ring clock renders
|
|
40
41
|
// from the active policy instead of hard-coding hours in the bundle.
|
|
41
42
|
const PEAK_WINDOWS = [{ startHour: 9, endHour: 12 }, { startHour: 14, endHour: 18 }]
|
|
42
43
|
const OFF_PEAK_RATE = 0.5
|
|
43
44
|
const BEIJING_OFFSET_MS = 8 * 3600_000
|
|
45
|
+
const WEEKEND_OFF_PEAK_SINCE = Date.UTC(2026, 7, 22, 16)
|
|
46
|
+
|
|
47
|
+
function isBeijingWeekend(atMs) {
|
|
48
|
+
if (!Number.isFinite(atMs)) return false
|
|
49
|
+
const beijingDate = new Date(atMs + BEIJING_OFFSET_MS)
|
|
50
|
+
const day = beijingDate.getUTCDay()
|
|
51
|
+
return day === 0 || day === 6
|
|
52
|
+
}
|
|
44
53
|
|
|
45
54
|
export function isBeijingPeak(atMs) {
|
|
55
|
+
if (atMs >= WEEKEND_OFF_PEAK_SINCE && isBeijingWeekend(atMs)) return false
|
|
46
56
|
const hours = ((atMs + BEIJING_OFFSET_MS) % 86_400_000) / 3_600_000
|
|
47
57
|
return (hours >= 9 && hours < 12) || (hours >= 14 && hours < 18)
|
|
48
58
|
}
|
|
@@ -78,7 +88,9 @@ export const PRICE_POLICIES = [
|
|
|
78
88
|
export function ratesFor(model, atMs) {
|
|
79
89
|
let entry
|
|
80
90
|
for (const policy of PRICE_POLICIES) {
|
|
81
|
-
|
|
91
|
+
// Own-property check: a model named `__proto__`/`toString` would otherwise
|
|
92
|
+
// resolve to an Object.prototype member and produce NaN costs.
|
|
93
|
+
if (atMs >= policy.since && Object.hasOwn(policy.models, model)) entry = policy
|
|
82
94
|
}
|
|
83
95
|
if (entry === undefined) return null
|
|
84
96
|
const rates = entry.models[model]
|
|
@@ -258,9 +270,10 @@ function persistStore(logger) {
|
|
|
258
270
|
saveTimer = null
|
|
259
271
|
}
|
|
260
272
|
try {
|
|
261
|
-
mkdirSync(dirname(STORE_PATH), { recursive: true })
|
|
273
|
+
mkdirSync(dirname(STORE_PATH), { recursive: true, mode: 0o700 })
|
|
262
274
|
const tmp = STORE_PATH + '.tmp'
|
|
263
|
-
|
|
275
|
+
// Owner-only: the store carries usage accounting next to credential files.
|
|
276
|
+
writeFileSync(tmp, JSON.stringify(store), { mode: 0o600 })
|
|
264
277
|
renameSync(tmp, STORE_PATH)
|
|
265
278
|
} catch (error) {
|
|
266
279
|
if (logger && typeof logger.warn === 'function') {
|
|
@@ -325,9 +338,10 @@ function persistAccounts(logger) {
|
|
|
325
338
|
accountsSaveTimer = null
|
|
326
339
|
}
|
|
327
340
|
try {
|
|
328
|
-
mkdirSync(dirname(ACCOUNTS_PATH), { recursive: true })
|
|
341
|
+
mkdirSync(dirname(ACCOUNTS_PATH), { recursive: true, mode: 0o700 })
|
|
329
342
|
const tmp = ACCOUNTS_PATH + '.tmp'
|
|
330
|
-
|
|
343
|
+
// This file holds plaintext API keys: owner-only, never group/world readable.
|
|
344
|
+
writeFileSync(tmp, JSON.stringify(accounts), { mode: 0o600 })
|
|
331
345
|
renameSync(tmp, ACCOUNTS_PATH)
|
|
332
346
|
} catch (error) {
|
|
333
347
|
if (logger && typeof logger.warn === 'function') {
|
|
@@ -436,15 +450,20 @@ async function resolveBalanceKey(ctx) {
|
|
|
436
450
|
*/
|
|
437
451
|
export async function activateAccount(ctx, id) {
|
|
438
452
|
const account = findAccount(id)
|
|
439
|
-
if (account === null) return { ok: false, error: 'account
|
|
453
|
+
if (account === null) return { ok: false, error: 'account-not-found' }
|
|
440
454
|
const credentials = ctx.get('credentials')
|
|
441
|
-
if (credentials === undefined) return { ok: false, error: 'credentials
|
|
455
|
+
if (credentials === undefined) return { ok: false, error: 'credentials-unavailable' }
|
|
442
456
|
try {
|
|
443
457
|
await credentials.set(CREDENTIAL_REF, account.apiKey)
|
|
444
458
|
} catch (error) {
|
|
445
459
|
// e.g. the launching environment supplies DEEPSEEK_API_KEY read-only, so
|
|
446
|
-
// the write is shadowed and refused by the credentials provider.
|
|
447
|
-
|
|
460
|
+
// the write is shadowed and refused by the credentials provider. Keep the
|
|
461
|
+
// browser error bounded and do not log provider text that might contain
|
|
462
|
+
// paths, environment details, or key fragments.
|
|
463
|
+
if (ctx.logger && typeof ctx.logger.warn === 'function') {
|
|
464
|
+
ctx.logger.warn('dsh-wallet: credential write refused')
|
|
465
|
+
}
|
|
466
|
+
return { ok: false, error: 'credential-write-refused' }
|
|
448
467
|
}
|
|
449
468
|
accounts.activeId = account.id
|
|
450
469
|
scheduleAccountsSave(ctx.logger)
|
|
@@ -509,12 +528,12 @@ let balance = { fetchedAt: 0, available: false, balances: [], error: null }
|
|
|
509
528
|
let balanceRefresh = null
|
|
510
529
|
|
|
511
530
|
async function performBalanceRefresh(ctx) {
|
|
512
|
-
const credentials = ctx.get('credentials')
|
|
513
|
-
if (credentials === undefined) {
|
|
514
|
-
balance = { fetchedAt: Date.now(), available: false, balances: [], error: 'no-credentials' }
|
|
515
|
-
return
|
|
516
|
-
}
|
|
517
531
|
try {
|
|
532
|
+
const credentials = ctx.get('credentials')
|
|
533
|
+
if (credentials === undefined) {
|
|
534
|
+
balance = { fetchedAt: Date.now(), available: false, balances: [], error: 'no-credentials' }
|
|
535
|
+
return
|
|
536
|
+
}
|
|
518
537
|
const key = await resolveBalanceKey(ctx)
|
|
519
538
|
if (key === undefined || key === '') {
|
|
520
539
|
balance = { fetchedAt: Date.now(), available: false, balances: [], error: 'no-api-key' }
|
|
@@ -564,9 +583,24 @@ async function performBalanceRefresh(ctx) {
|
|
|
564
583
|
|
|
565
584
|
function refreshBalance(ctx) {
|
|
566
585
|
if (balanceRefresh !== null) return balanceRefresh
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
586
|
+
// Never let a refresh escape as an unhandled rejection: every caller uses
|
|
587
|
+
// `void refreshBalance(ctx)`, so a throw here would take the host process
|
|
588
|
+
// down (Node exits on unhandled rejections). Absorb it into the error state.
|
|
589
|
+
balanceRefresh = performBalanceRefresh(ctx)
|
|
590
|
+
.catch((error) => {
|
|
591
|
+
balance = {
|
|
592
|
+
fetchedAt: Date.now(),
|
|
593
|
+
available: false,
|
|
594
|
+
balances: [],
|
|
595
|
+
error: 'balance-unavailable',
|
|
596
|
+
}
|
|
597
|
+
if (ctx && ctx.logger && typeof ctx.logger.warn === 'function') {
|
|
598
|
+
ctx.logger.warn('dsh-wallet: balance refresh failed')
|
|
599
|
+
}
|
|
600
|
+
})
|
|
601
|
+
.finally(() => {
|
|
602
|
+
balanceRefresh = null
|
|
603
|
+
})
|
|
570
604
|
return balanceRefresh
|
|
571
605
|
}
|
|
572
606
|
|
|
@@ -609,8 +643,18 @@ function balanceTotal() {
|
|
|
609
643
|
|
|
610
644
|
function sessionView(sessionId) {
|
|
611
645
|
if (sessionId === undefined || sessionId === '') return { official: null, third: null }
|
|
646
|
+
// Own-property lookup only: session ids like `__proto__`, `constructor` or
|
|
647
|
+
// `toString` otherwise resolve to an Object.prototype member, slip past the
|
|
648
|
+
// undefined guard, and throw inside bucketTotals.
|
|
649
|
+
if (!Object.prototype.hasOwnProperty.call(store.sessions, sessionId)) {
|
|
650
|
+
return { official: null, third: null }
|
|
651
|
+
}
|
|
612
652
|
const session = store.sessions[sessionId]
|
|
613
|
-
if (session ===
|
|
653
|
+
if (session === null || typeof session !== 'object'
|
|
654
|
+
|| session.official === null || typeof session.official !== 'object'
|
|
655
|
+
|| session.third === null || typeof session.third !== 'object') {
|
|
656
|
+
return { official: null, third: null }
|
|
657
|
+
}
|
|
614
658
|
const official = bucketTotals(session.official)
|
|
615
659
|
const third = bucketTotals(session.third)
|
|
616
660
|
return {
|
|
@@ -627,6 +671,7 @@ function sessionView(sessionId) {
|
|
|
627
671
|
function snapshotView(sessionId) {
|
|
628
672
|
const currency = balanceCurrency(balance.balances)
|
|
629
673
|
const active = activeAccount()
|
|
674
|
+
const now = Date.now()
|
|
630
675
|
return {
|
|
631
676
|
ok: true,
|
|
632
677
|
balance: {
|
|
@@ -658,7 +703,9 @@ function snapshotView(sessionId) {
|
|
|
658
703
|
offsetMinutes: 480,
|
|
659
704
|
windows: PEAK_WINDOWS.map(w => ({ startHour: w.startHour, endHour: w.endHour })),
|
|
660
705
|
offPeakRate: OFF_PEAK_RATE,
|
|
661
|
-
isPeak: isBeijingPeak(
|
|
706
|
+
isPeak: isBeijingPeak(now),
|
|
707
|
+
weekendOffPeak: now >= WEEKEND_OFF_PEAK_SINCE && isBeijingWeekend(now),
|
|
708
|
+
weekendOffPeakSince: WEEKEND_OFF_PEAK_SINCE,
|
|
662
709
|
},
|
|
663
710
|
accounts: {
|
|
664
711
|
activeId: accounts.activeId,
|
package/lib/client.js
CHANGED
|
@@ -23,7 +23,7 @@ window.__ModuleLoader__.load({
|
|
|
23
23
|
|
|
24
24
|
var POLL_MS = 15000
|
|
25
25
|
// Keep in lockstep with package.json; a test enforces the sync.
|
|
26
|
-
var WALLET_VERSION = '0.2.
|
|
26
|
+
var WALLET_VERSION = '0.2.6'
|
|
27
27
|
var CONFIRM_KEY = 'dsh-wallet-recharge-confirmed'
|
|
28
28
|
var CHIP_LAYOUT_KEY = 'dshw-chip-layout-v4'
|
|
29
29
|
var PANEL_POS_KEY = 'dshw-panel-pos-v1'
|
|
@@ -62,11 +62,18 @@ window.__ModuleLoader__.load({
|
|
|
62
62
|
? root.__DSH_WALLET_ADAPTER__
|
|
63
63
|
: {}
|
|
64
64
|
var memory = Object.create(null)
|
|
65
|
+
// Keys whose native write was refused (quota, private mode, sandboxed
|
|
66
|
+
// WebView). Reads for those keys must come from memory, otherwise the
|
|
67
|
+
// fallback write is invisible and the UI keeps reading a stale value.
|
|
68
|
+
var memoryOnly = Object.create(null)
|
|
65
69
|
var nativeStorage = extension.storage || root.localStorage || null
|
|
66
70
|
var pageNotices = new Map()
|
|
67
71
|
|
|
68
72
|
var storage = {
|
|
69
73
|
getItem: function (key) {
|
|
74
|
+
if (memoryOnly[key] === true) {
|
|
75
|
+
return Object.prototype.hasOwnProperty.call(memory, key) ? memory[key] : null
|
|
76
|
+
}
|
|
70
77
|
if (nativeStorage && typeof nativeStorage.getItem === 'function') {
|
|
71
78
|
try { return nativeStorage.getItem(key) } catch (e) { /* storage may be disabled */ }
|
|
72
79
|
}
|
|
@@ -76,13 +83,19 @@ window.__ModuleLoader__.load({
|
|
|
76
83
|
value = String(value)
|
|
77
84
|
memory[key] = value
|
|
78
85
|
if (nativeStorage && typeof nativeStorage.setItem === 'function') {
|
|
79
|
-
try {
|
|
86
|
+
try {
|
|
87
|
+
nativeStorage.setItem(key, value)
|
|
88
|
+
delete memoryOnly[key]
|
|
89
|
+
return
|
|
90
|
+
} catch (e) { /* fall through to the memory-only marker */ }
|
|
80
91
|
}
|
|
92
|
+
memoryOnly[key] = true
|
|
81
93
|
},
|
|
82
94
|
removeItem: function (key) {
|
|
83
95
|
delete memory[key]
|
|
96
|
+
delete memoryOnly[key]
|
|
84
97
|
if (nativeStorage && typeof nativeStorage.removeItem === 'function') {
|
|
85
|
-
try { nativeStorage.removeItem(key); return } catch (e) { /*
|
|
98
|
+
try { nativeStorage.removeItem(key); return } catch (e) { /* memory copy is already gone */ }
|
|
86
99
|
}
|
|
87
100
|
}
|
|
88
101
|
}
|
|
@@ -277,7 +290,7 @@ window.__ModuleLoader__.load({
|
|
|
277
290
|
'.dshw_anchorHome{overflow:hidden;min-width:44px}',
|
|
278
291
|
'.dshw_chipLift{z-index:80!important}',
|
|
279
292
|
'.dshw_anchorHome>.dshw_chip{box-sizing:border-box;max-width:100%;min-width:0;overflow:hidden}',
|
|
280
|
-
'.dshw_chip{border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:transparent;height:22px;color:var(--dsw-alias-label-primary
|
|
293
|
+
'.dshw_chip{border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:transparent;height:22px;color:var(--dsw-alias-label-primary,inherit);white-space:nowrap;border-radius:999px;align-items:stretch;font-size:12px;line-height:1;display:inline-flex;position:relative;touch-action:none;user-select:none;cursor:grab}',
|
|
281
294
|
'.dshw_chip:hover,.dshw_chip:focus-within{border-color:var(--dsw-alias-brand-primary,#4aa3ff)}',
|
|
282
295
|
'.dshw_chipLow{border-color:var(--dsw-alias-state-error-primary,#e5534b);color:var(--dsw-alias-state-error-primary,#e5534b);animation:dshwChipPulseIn 1.8s ease-in-out infinite}',
|
|
283
296
|
'.dshw_chipLow:hover,.dshw_chipLow:focus-within{border-color:var(--dsw-alias-state-error-primary,#e5534b)}',
|
|
@@ -302,7 +315,7 @@ window.__ModuleLoader__.load({
|
|
|
302
315
|
'.dshw_chipVertical.dshw_chipNoRecharge .dshw_chipMain{border-radius:7px}',
|
|
303
316
|
'.dshw_recharge{color:var(--dsw-alias-brand-primary,#4aa3ff);border-left:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));padding:0 7px 0 6px;border-radius:0 999px 999px 0}',
|
|
304
317
|
'.dshw_chipMain:focus-visible,.dshw_recharge:focus-visible,.dshw_btn:focus-visible,.dshw_floatBtn:focus-visible,.dshw_dot:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4aa3ff);outline-offset:2px}',
|
|
305
|
-
'.dshw_panel{z-index:40;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1
|
|
318
|
+
'.dshw_panel{z-index:40;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,transparent)));box-sizing:border-box;width:min(276px,calc(100vw - 12px));max-height:calc(100vh - 16px);overflow:auto;color:var(--dsw-alias-label-primary,inherit);border-radius:8px;flex-direction:column;gap:4px;padding:8px 9px;font-size:12px;display:flex;position:fixed;box-shadow:0 4px 16px rgba(0,0,0,.35)}',
|
|
306
319
|
'.dshw_panelHeader{min-height:24px;cursor:move;touch-action:none;user-select:none}',
|
|
307
320
|
'.dshw_panelHeader .dshw_btn{cursor:pointer}',
|
|
308
321
|
'.dshw_row{justify-content:space-between;align-items:center;gap:6px;display:flex}',
|
|
@@ -319,7 +332,7 @@ window.__ModuleLoader__.load({
|
|
|
319
332
|
'.dshw_check input{margin:0;accent-color:var(--dsw-alias-brand-primary,#4aa3ff)}',
|
|
320
333
|
'.dshw_select{background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,rgba(127,127,127,.08)));border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));color:var(--dsw-alias-label-primary,inherit);border-radius:5px;padding:2px 4px;font-size:12px}',
|
|
321
334
|
'.dshw_btn{border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:var(--dsw-alias-bg-layer-1,transparent);color:var(--dsw-alias-label-primary,inherit);border-radius:5px;padding:2px 6px;font-size:12px;cursor:pointer}',
|
|
322
|
-
'.dshw_btnPrimary{background:var(--dsw-alias-brand-primary,#4aa3ff);border-color:transparent;color:var(--dsw-alias-label-primary-foreground
|
|
335
|
+
'.dshw_btnPrimary{background:var(--dsw-alias-brand-primary,#4aa3ff);border-color:transparent;color:var(--dsw-alias-label-primary-foreground,var(--dsw-alias-label-primary,inherit))}',
|
|
323
336
|
// ---- v0.2 panel restyle: balance card, settings card, account scroll ----
|
|
324
337
|
'.dshw_balanceCard{margin:2px 0 2px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.2));border-radius:8px;background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,rgba(127,127,127,.06)))}',
|
|
325
338
|
'.dshw_balLine{display:flex;align-items:center;gap:7px;white-space:nowrap}',
|
|
@@ -411,7 +424,7 @@ window.__ModuleLoader__.load({
|
|
|
411
424
|
'.dshw_switch{position:relative;flex:none;width:34px;height:20px;display:inline-flex;cursor:pointer}',
|
|
412
425
|
'.dshw_switch>input{position:absolute;inset:0;opacity:0;margin:0;cursor:pointer}',
|
|
413
426
|
'.dshw_track{position:absolute;inset:0;border-radius:999px;background:var(--dsw-alias-border-l3,rgba(127,127,127,.3));transition:background-color .15s}',
|
|
414
|
-
'.dshw_knob{position:absolute;left:2px;top:2px;width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-bg-layer-1
|
|
427
|
+
'.dshw_knob{position:absolute;left:2px;top:2px;width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,transparent));box-shadow:0 1px 3px rgba(0,0,0,.24);transition:transform .15s}',
|
|
415
428
|
'.dshw_switch>input:checked+.dshw_track{background:var(--dsw-alias-brand-primary,#4aa3ff)}',
|
|
416
429
|
'.dshw_switch>input:checked~.dshw_knob{transform:translateX(14px)}',
|
|
417
430
|
'.dshw_switch>input:focus-visible+.dshw_track{outline:2px solid var(--dsw-alias-brand-primary,#4aa3ff);outline-offset:2px}',
|
|
@@ -442,7 +455,7 @@ window.__ModuleLoader__.load({
|
|
|
442
455
|
'.dshw_footRingVertical .dshw_footRingCountdown{font-size:10.5px;overflow:visible;white-space:nowrap}',
|
|
443
456
|
'.dshw_footRingVertical .dshw_footRingBottom{justify-content:center}',
|
|
444
457
|
'.dshw_footRingNoRecharge{padding-right:10px}',
|
|
445
|
-
'.dshw_footRingFloating{position:fixed;z-index:90;width:auto;min-width:180px;max-width:260px;margin:0!important;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1
|
|
458
|
+
'.dshw_footRingFloating{position:fixed;z-index:90;width:auto;min-width:180px;max-width:260px;margin:0!important;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,transparent)));box-shadow:0 8px 24px rgba(0,0,0,.35);cursor:grab;touch-action:none}',
|
|
446
459
|
'.dshw_footRingFloating.dshw_footRingVertical{min-width:132px;max-width:152px;width:140px;padding:9px 8px;gap:4px}',
|
|
447
460
|
'.dshw_footRingFloating.dshw_footRingVertical .dshw_footRingResetBtn{position:absolute;top:4px;right:4px;font-size:9.5px;padding:1px 5px;line-height:13px}',
|
|
448
461
|
'.dshw_footRingFloating.dshw_footRingDragging{cursor:grabbing;box-shadow:0 10px 30px rgba(0,0,0,.45)}',
|
|
@@ -459,8 +472,8 @@ window.__ModuleLoader__.load({
|
|
|
459
472
|
'.dshw_footRingBottom{display:flex;align-items:center;justify-content:space-between;gap:6px;width:100%;min-height:18px}',
|
|
460
473
|
'.dshw_footRingCountdown{font-size:11px;color:var(--dsw-alias-label-secondary,rgba(127,127,127,.85));line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1 1 auto;min-width:0}',
|
|
461
474
|
'.dshw_footRingBtnRechargeInline{border:1px solid var(--dsw-alias-brand-primary,#4aa3ff);background:var(--dsw-alias-brand-soft,rgba(74,163,255,.10));color:var(--dsw-alias-brand-primary,#4aa3ff);border-radius:4px;height:18px;padding:0 6px;font-size:10.5px;font-weight:600;line-height:16px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:all .15s ease;user-select:none;flex:none}',
|
|
462
|
-
'.dshw_footRingBtnRechargeInline:hover{background:var(--dsw-alias-brand-primary,#4aa3ff);color
|
|
463
|
-
'.dshw_peakPanel{position:fixed;z-index:95;width:290px;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1
|
|
475
|
+
'.dshw_footRingBtnRechargeInline:hover{background:var(--dsw-alias-brand-primary,#4aa3ff);color:var(--dsw-alias-label-primary-foreground,var(--dsw-alias-label-primary,inherit))}',
|
|
476
|
+
'.dshw_peakPanel{position:fixed;z-index:95;width:290px;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,transparent)));color:var(--dsw-alias-label-primary,inherit);border-radius:12px;box-shadow:0 10px 32px rgba(0,0,0,.38);display:flex;flex-direction:column;gap:8px;padding:12px 14px;font-size:12px;user-select:none;touch-action:none}',
|
|
464
477
|
'.dshw_peakPanelHeader{display:flex;align-items:center;justify-content:space-between;font-weight:650;font-size:12.5px;color:var(--dsw-alias-label-primary,inherit)}',
|
|
465
478
|
'.dshw_peakPanelClose{border:none;background:transparent;color:inherit;font-size:16px;line-height:1;cursor:pointer;padding:2px 6px;border-radius:4px}',
|
|
466
479
|
'.dshw_peakPanelClose:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}',
|
|
@@ -471,23 +484,23 @@ window.__ModuleLoader__.load({
|
|
|
471
484
|
'.dshw_ringNeutral{stroke:var(--dsw-alias-border-l3,rgba(127,127,127,.3))}',
|
|
472
485
|
'.dshw_peakRing circle{transition:stroke-width .3s ease}',
|
|
473
486
|
'.dshw_ringNow{stroke-width:6;filter:brightness(1.15)}',
|
|
474
|
-
'.dshw_ringPointer{fill:var(--dsw-alias-label-primary
|
|
487
|
+
'.dshw_ringPointer{fill:var(--dsw-alias-label-primary,inherit)}',
|
|
475
488
|
'.dshw_ringTick{fill:var(--dsw-alias-label-dimmed,var(--dsw-alias-label-tertiary,#8b949e))}',
|
|
476
489
|
'@media (max-width:760px){.dshw_settingsGrid,.dshw_settingsSection .dshw_setCard{grid-template-columns:1fr}.dshw_settingsSection .dshw_setCard>.dshw_setCell.wide{grid-column:1}.dshw_accountAdd{grid-template-columns:1fr}.dshw_settingsFooter{align-items:flex-start;flex-direction:column-reverse}.dshw_settingsFooterActions{width:100%}.dshw_settingsFooterActions .dshw_btn{flex:1}.dshw_settingsHeroMeta{white-space:normal;flex-wrap:wrap}}',
|
|
477
490
|
'.dshw_actionRow{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin:2px 0}',
|
|
478
491
|
'.dshw_textDanger{display:inline-flex;align-items:center;gap:5px;height:22px;padding:0 4px;border:none;background:transparent;font:inherit;font-size:11px;color:var(--dsw-alias-label-dimmed,var(--dsw-alias-label-tertiary,#8b949e));cursor:pointer;border-radius:4px;transition:color .12s,background-color .12s}',
|
|
479
492
|
'.dshw_textDanger:hover{color:var(--dsw-alias-state-error-primary,#e5534b);background:var(--dsw-alias-state-error-soft,rgba(229,83,75,.12))}',
|
|
480
493
|
'.dshw_overlay{position:fixed;inset:0;background:var(--dsw-alias-bg-mask-drop,rgba(0,0,0,.55));display:flex;align-items:center;justify-content:center;z-index:100}',
|
|
481
|
-
'.dshw_overlayBox{background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1
|
|
494
|
+
'.dshw_overlayBox{background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,transparent)));border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));color:var(--dsw-alias-label-primary,inherit);border-radius:8px;padding:14px 18px;width:min(320px,calc(100vw - 32px));box-sizing:border-box;font-size:13px;line-height:1.7}',
|
|
482
495
|
'.dshw_overlayRow{margin-top:10px;display:flex;gap:8px;justify-content:flex-end}',
|
|
483
|
-
'.dshw_float{position:fixed;z-index:80;min-width:230px;max-width:280px;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1
|
|
496
|
+
'.dshw_float{position:fixed;z-index:80;min-width:230px;max-width:280px;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,transparent)));color:var(--dsw-alias-label-primary,inherit);border-radius:12px;box-shadow:0 6px 20px rgba(0,0,0,.25);display:flex;flex-direction:column;gap:6px;padding:10px 12px;font-size:12px;user-select:none}',
|
|
484
497
|
'.dshw_floatHeader{display:flex;align-items:center;justify-content:space-between;cursor:move;font-weight:600;opacity:.9;touch-action:none}',
|
|
485
498
|
'.dshw_floatBtn{border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:transparent;color:inherit;border-radius:5px;width:22px;height:20px;font-size:12px;line-height:1;cursor:pointer;padding:0}',
|
|
486
499
|
'.dshw_floatBtn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.1))}',
|
|
487
|
-
'.dshw_dot{position:fixed;z-index:80;width:36px;height:36px;border-radius:50%;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1
|
|
500
|
+
'.dshw_dot{position:fixed;z-index:80;width:36px;height:36px;border-radius:50%;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,transparent)));color:var(--dsw-alias-label-primary,inherit);font-size:11px;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 2px 10px rgba(0,0,0,.25);user-select:none;touch-action:none}',
|
|
488
501
|
'.dshw_dotLow{border-color:var(--dsw-alias-state-error-primary,#e5534b);color:var(--dsw-alias-state-error-primary,#e5534b)}',
|
|
489
502
|
'.dshw_noticeStack{position:fixed;z-index:120;top:max(12px,env(safe-area-inset-top));right:max(12px,env(safe-area-inset-right));width:min(340px,calc(100vw - 24px));display:flex;flex-direction:column;gap:8px;pointer-events:none}',
|
|
490
|
-
'.dshw_notice{box-sizing:border-box;width:100%;display:flex;align-items:flex-start;gap:8px;padding:10px 10px 10px 12px;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));border-radius:9px;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1
|
|
503
|
+
'.dshw_notice{box-sizing:border-box;width:100%;display:flex;align-items:flex-start;gap:8px;padding:10px 10px 10px 12px;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));border-radius:9px;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,transparent)));color:var(--dsw-alias-label-primary,inherit);box-shadow:0 6px 20px rgba(0,0,0,.25);font-size:12px;line-height:1.45;pointer-events:auto;cursor:pointer}',
|
|
491
504
|
'.dshw_noticeCopy{min-width:0;display:flex;flex:1;flex-direction:column;gap:2px}.dshw_noticeCopy span{white-space:pre-line;overflow-wrap:anywhere}',
|
|
492
505
|
'.dshw_noticeClose{flex:none;width:24px;height:24px;border:0;border-radius:5px;background:transparent;color:inherit;font-size:18px;line-height:1;cursor:pointer}.dshw_noticeClose:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.1))}',
|
|
493
506
|
// Home-fit and compact modes are toggled by measured space (see the
|
|
@@ -539,7 +552,7 @@ window.__ModuleLoader__.load({
|
|
|
539
552
|
// Noon = 0h at 12 o'clock, clockwise. Optional size scales the 76-unit
|
|
540
553
|
// viewBox down for tight hosts (sidebar foot row / rail circle); optional
|
|
541
554
|
// ariaText overrides the fallback label with the full screen-reader state.
|
|
542
|
-
function peakRingSVG(windows, nowHour, size, ariaText) {
|
|
555
|
+
function peakRingSVG(windows, nowHour, size, ariaText, allOffPeak) {
|
|
543
556
|
var sizePx = typeof size === 'number' && size > 0 ? size : 76
|
|
544
557
|
var R = 28
|
|
545
558
|
var CX = 38
|
|
@@ -548,6 +561,18 @@ window.__ModuleLoader__.load({
|
|
|
548
561
|
// No pricing policy resolved: one neutral full ring, no arcs, no pointer
|
|
549
562
|
// — the clock shows an unconfigured state and claims no price.
|
|
550
563
|
if (!Array.isArray(windows) || windows.length === 0) {
|
|
564
|
+
if (allOffPeak === true) {
|
|
565
|
+
return React.createElement('svg', {
|
|
566
|
+
width: sizePx, height: sizePx, viewBox: '0 0 76 76',
|
|
567
|
+
role: 'img', className: 'dshw_peakRing',
|
|
568
|
+
'aria-label': ariaText || '当前为周末低谷时段,全天半价'
|
|
569
|
+
},
|
|
570
|
+
React.createElement('circle', {
|
|
571
|
+
key: 'weekend-off', cx: CX, cy: CY, r: R, fill: 'none',
|
|
572
|
+
className: 'dshw_ringOff dshw_ringNow', strokeWidth: 6.5,
|
|
573
|
+
strokeDasharray: CIRC.toFixed(2) + ' 0', strokeDashoffset: '0'
|
|
574
|
+
}))
|
|
575
|
+
}
|
|
551
576
|
return React.createElement('svg', {
|
|
552
577
|
width: sizePx, height: sizePx, viewBox: '0 0 76 76',
|
|
553
578
|
role: 'img', className: 'dshw_peakRing',
|
|
@@ -1085,6 +1110,26 @@ window.__ModuleLoader__.load({
|
|
|
1085
1110
|
return (counters.input || 0) + (counters.output || 0) + (counters.cacheRead || 0) + (counters.cacheWrite || 0)
|
|
1086
1111
|
}
|
|
1087
1112
|
|
|
1113
|
+
// Keep the floating peak card reachable after a viewport resize, a
|
|
1114
|
+
// persisted position from another screen size, or a scale change. `width`
|
|
1115
|
+
// and `height` are the rendered dimensions from getBoundingClientRect(),
|
|
1116
|
+
// so transformed 120% cards are clamped using their actual footprint.
|
|
1117
|
+
function clampPeakPosition(pos, width, height, viewportWidth, viewportHeight, margin) {
|
|
1118
|
+
margin = Number.isFinite(margin) ? Math.max(0, margin) : 8
|
|
1119
|
+
viewportWidth = Number.isFinite(viewportWidth) ? viewportWidth : 1024
|
|
1120
|
+
viewportHeight = Number.isFinite(viewportHeight) ? viewportHeight : 768
|
|
1121
|
+
width = Number.isFinite(width) && width > 0 ? width : 180
|
|
1122
|
+
height = Number.isFinite(height) && height > 0 ? height : 60
|
|
1123
|
+
var x = pos && Number.isFinite(pos.x) ? pos.x : margin
|
|
1124
|
+
var y = pos && Number.isFinite(pos.y) ? pos.y : margin
|
|
1125
|
+
var maxX = Math.max(margin, viewportWidth - width - margin)
|
|
1126
|
+
var maxY = Math.max(margin, viewportHeight - height - margin)
|
|
1127
|
+
return {
|
|
1128
|
+
x: Math.round(Math.min(maxX, Math.max(margin, x))),
|
|
1129
|
+
y: Math.round(Math.min(maxY, Math.max(margin, y))),
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1088
1133
|
/**
|
|
1089
1134
|
* Host settings-panel section: the same wallet controls as the chip panel,
|
|
1090
1135
|
* as an independent card. Shares state with the chip through the same
|
|
@@ -1662,7 +1707,7 @@ window.__ModuleLoader__.load({
|
|
|
1662
1707
|
type: 'button', className: 'dshw_btn dshw_btnPrimary',
|
|
1663
1708
|
onClick: function () { if (close) close(); window.open('https://platform.deepseek.com/top_up', '_blank', 'noopener') }
|
|
1664
1709
|
}, '↗ 去官方充值'))))
|
|
1665
|
-
return React.createElement('div', { className: 'dshw_settingsSection', style: { display: 'flex', flexDirection: 'column', gap: '6px', fontSize: '12px', color: 'var(--dsw-alias-label-primary
|
|
1710
|
+
return React.createElement('div', { className: 'dshw_settingsSection', style: { display: 'flex', flexDirection: 'column', gap: '6px', fontSize: '12px', color: 'var(--dsw-alias-label-primary,inherit)' } }, rows)
|
|
1666
1711
|
}
|
|
1667
1712
|
|
|
1668
1713
|
// Wall-clock hour (0-24, fractional) inside an IANA timezone, via Intl —
|
|
@@ -1689,8 +1734,47 @@ window.__ModuleLoader__.load({
|
|
|
1689
1734
|
var windows = policy && Array.isArray(policy.windows) ? policy.windows.filter(function (w) {
|
|
1690
1735
|
return w && Number.isFinite(w.startHour) && Number.isFinite(w.endHour) && w.endHour > w.startHour
|
|
1691
1736
|
}) : []
|
|
1737
|
+
var weekendOffPeak = policy && policy.weekendOffPeak === true
|
|
1738
|
+
if (weekendOffPeak) {
|
|
1739
|
+
var offsetMinutes = policy && Number.isFinite(policy.offsetMinutes) ? policy.offsetMinutes : 480
|
|
1740
|
+
var beijingNow = Number.isFinite(nowMs) ? new Date(nowMs + offsetMinutes * 60000) : null
|
|
1741
|
+
var day = beijingNow && Number.isFinite(beijingNow.getTime()) ? beijingNow.getUTCDay() : 6
|
|
1742
|
+
var daysUntilMonday = day === 6 ? 2 : 1
|
|
1743
|
+
var nextSwitchMs = beijingNow && Number.isFinite(beijingNow.getTime())
|
|
1744
|
+
? Date.UTC(
|
|
1745
|
+
beijingNow.getUTCFullYear(), beijingNow.getUTCMonth(), beijingNow.getUTCDate() + daysUntilMonday,
|
|
1746
|
+
9 - (offsetMinutes / 60), 0, 0, 0
|
|
1747
|
+
) - offsetMinutes * 60000
|
|
1748
|
+
: NaN
|
|
1749
|
+
var weekendMsLeft = Number.isFinite(nextSwitchMs) && Number.isFinite(nowMs)
|
|
1750
|
+
? Math.max(0, nextSwitchMs - nowMs)
|
|
1751
|
+
: 0
|
|
1752
|
+
var weekendHours = Math.floor(weekendMsLeft / 3600000)
|
|
1753
|
+
var weekendMinutes = Math.floor((weekendMsLeft % 3600000) / 60000)
|
|
1754
|
+
var weekendLeftText = weekendHours > 0
|
|
1755
|
+
? weekendHours + ' 小时 ' + weekendMinutes + ' 分'
|
|
1756
|
+
: weekendMinutes + ' 分钟'
|
|
1757
|
+
var weekendLeftShort = weekendHours > 0
|
|
1758
|
+
? weekendHours + 'h' + (weekendMinutes > 0 ? weekendMinutes + 'm' : '')
|
|
1759
|
+
: weekendMinutes + 'm'
|
|
1760
|
+
var weekendDateKey = beijingNow && Number.isFinite(beijingNow.getTime())
|
|
1761
|
+
? [beijingNow.getUTCFullYear(), String(beijingNow.getUTCMonth() + 1).padStart(2, '0'), String(beijingNow.getUTCDate()).padStart(2, '0')].join('-')
|
|
1762
|
+
: 'weekend'
|
|
1763
|
+
var weekendTzName = policy.timezone || 'Asia/Shanghai'
|
|
1764
|
+
return {
|
|
1765
|
+
configured: true, windows: [], weekendOffPeak: true, inPeak: false,
|
|
1766
|
+
nextHour: 9,
|
|
1767
|
+
ariaText: '当前为周末低谷时段,全天按半价计费,周一 09:00 恢复标准价',
|
|
1768
|
+
tip: '峰谷时钟 · 当前周末低谷 · 周一 09:00 恢复标准价 · 还有 ' + weekendLeftText + ' · ' + weekendTzName,
|
|
1769
|
+
periodName: '周末低谷', rateBadge: '半价',
|
|
1770
|
+
countdownSummary: '周一 09:00 切换 · 剩 ' + weekendLeftShort,
|
|
1771
|
+
windowSummary: '周末全天低谷 · 工作日高峰 09:00–12:00 / 14:00–18:00',
|
|
1772
|
+
periodId: 'weekend-off-' + weekendDateKey,
|
|
1773
|
+
switchBody: '周末低谷结束,已进入工作日高峰时段,按标准价计费',
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1692
1776
|
if (windows.length === 0) {
|
|
1693
|
-
return { configured: false, windows: [], ariaText: '峰谷计费时段未配置', tip: '峰谷时钟 · 计费时段未配置', periodId: null }
|
|
1777
|
+
return { configured: false, windows: [], weekendOffPeak: false, ariaText: '峰谷计费时段未配置', tip: '峰谷时钟 · 计费时段未配置', periodId: null }
|
|
1694
1778
|
}
|
|
1695
1779
|
var inPeak = false
|
|
1696
1780
|
var segStart = null
|
|
@@ -1753,7 +1837,7 @@ window.__ModuleLoader__.load({
|
|
|
1753
1837
|
var tip = '峰谷时钟 · 当前' + period + ' · ' + switchText + ' · 还有 ' + leftText
|
|
1754
1838
|
+ ' · 高峰 ' + winText + '(' + tzName + ')' + localNote
|
|
1755
1839
|
return {
|
|
1756
|
-
configured: true, windows: windows, inPeak: inPeak,
|
|
1840
|
+
configured: true, windows: windows, weekendOffPeak: false, inPeak: inPeak,
|
|
1757
1841
|
nextHour: nextHour, ariaText: ariaText, tip: tip,
|
|
1758
1842
|
periodName: periodName, rateBadge: rateBadge,
|
|
1759
1843
|
countdownSummary: countdownSummary, windowSummary: windowSummary,
|
|
@@ -1809,6 +1893,12 @@ window.__ModuleLoader__.load({
|
|
|
1809
1893
|
var [nowMs, setNowMs] = React.useState(function () { return Date.now() })
|
|
1810
1894
|
var dragRef = React.useRef(null)
|
|
1811
1895
|
var didDragRef = React.useRef(false)
|
|
1896
|
+
// Guards the self-echo: every preference write dispatches the shared
|
|
1897
|
+
// change events so OTHER surfaces resync, but this component must not
|
|
1898
|
+
// re-read storage from its own dispatch — the listener would setState a
|
|
1899
|
+
// second time from a different call stack and race the update already
|
|
1900
|
+
// queued, which can drop the write that started it.
|
|
1901
|
+
var suppressSelfSyncRef = React.useRef(false)
|
|
1812
1902
|
var [panelOpen, setPanelOpen] = React.useState(false)
|
|
1813
1903
|
var [panelPos, setPanelPos] = React.useState({ left: 16, top: 16 })
|
|
1814
1904
|
var cardNodeRef = React.useRef(null)
|
|
@@ -1828,6 +1918,8 @@ window.__ModuleLoader__.load({
|
|
|
1828
1918
|
loadSnap()
|
|
1829
1919
|
var listensEvents = typeof window.addEventListener === 'function'
|
|
1830
1920
|
function onRingChange() {
|
|
1921
|
+
// Skip our own echo (see suppressSelfSyncRef).
|
|
1922
|
+
if (suppressSelfSyncRef.current) return
|
|
1831
1923
|
try {
|
|
1832
1924
|
setShown(compatibility.storage.getItem(PEAK_RING_KEY) !== 'false')
|
|
1833
1925
|
setOrient(compatibility.storage.getItem(PEAK_ORIENT_KEY) || 'horizontal')
|
|
@@ -1956,6 +2048,33 @@ window.__ModuleLoader__.load({
|
|
|
1956
2048
|
} catch (e) { /* ignore */ }
|
|
1957
2049
|
}, [policy, nowMs])
|
|
1958
2050
|
|
|
2051
|
+
var isFreeFloating = dock === 'free'
|
|
2052
|
+
var isVertical = orient === 'vertical' && (wide || isFreeFloating)
|
|
2053
|
+
var isRail = !wide && !isFreeFloating
|
|
2054
|
+
|
|
2055
|
+
// A saved floating coordinate is not trustworthy after a window resize
|
|
2056
|
+
// or scale change. Re-measure the rendered card (including transform)
|
|
2057
|
+
// and persist the corrected coordinate before it can disappear outside
|
|
2058
|
+
// the viewport. This hook must run even while the card is still loading,
|
|
2059
|
+
// otherwise React sees a changing hook count when the snapshot arrives.
|
|
2060
|
+
useLayoutEffect(function () {
|
|
2061
|
+
if (!isFreeFloating || !cardNodeRef.current) return
|
|
2062
|
+
var node = cardNodeRef.current
|
|
2063
|
+
function fitFloatingPeakCard() {
|
|
2064
|
+
if (!node || typeof node.getBoundingClientRect !== 'function') return
|
|
2065
|
+
var rect = node.getBoundingClientRect()
|
|
2066
|
+
var current = latestPosRef.current || pos || { x: rect.left, y: rect.top }
|
|
2067
|
+
var fitted = clampPeakPosition(current, rect.width, rect.height, window.innerWidth, window.innerHeight, 8)
|
|
2068
|
+
if (fitted.x === current.x && fitted.y === current.y && pos !== null) return
|
|
2069
|
+
latestPosRef.current = fitted
|
|
2070
|
+
setPos(fitted)
|
|
2071
|
+
try { compatibility.storage.setItem(PEAK_POS_KEY, JSON.stringify(fitted)) } catch (e) { /* ignore */ }
|
|
2072
|
+
}
|
|
2073
|
+
fitFloatingPeakCard()
|
|
2074
|
+
window.addEventListener('resize', fitFloatingPeakCard)
|
|
2075
|
+
return function () { window.removeEventListener('resize', fitFloatingPeakCard) }
|
|
2076
|
+
}, [isFreeFloating, pos, peakScale, orient, showRecharge])
|
|
2077
|
+
|
|
1959
2078
|
if (!shown || snapshot === undefined) return null
|
|
1960
2079
|
var tzName = policy && policy.timezone ? policy.timezone : 'Asia/Shanghai'
|
|
1961
2080
|
var offsetMinutes = policy && typeof policy.offsetMinutes === 'number' ? policy.offsetMinutes : 480
|
|
@@ -1971,33 +2090,41 @@ window.__ModuleLoader__.load({
|
|
|
1971
2090
|
var costLabel = sessionCostLabel(balCurrency)
|
|
1972
2091
|
var low = snapshot && snapshot.lowBalance === true
|
|
1973
2092
|
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2093
|
+
// Announce a preference change to the other surfaces (settings page, a
|
|
2094
|
+
// second window) while suppressing our own listener, so this component's
|
|
2095
|
+
// queued setState is the single writer for this interaction.
|
|
2096
|
+
function announcePrefs(includeRingEvent) {
|
|
2097
|
+
suppressSelfSyncRef.current = true
|
|
2098
|
+
try {
|
|
2099
|
+
try { compatibility.dispatch(SETTINGS_EVENT) } catch (e) { /* ignore */ }
|
|
2100
|
+
if (includeRingEvent !== false) {
|
|
2101
|
+
try { compatibility.dispatch(PEAK_RING_EVENT) } catch (e) { /* ignore */ }
|
|
2102
|
+
}
|
|
2103
|
+
} finally {
|
|
2104
|
+
suppressSelfSyncRef.current = false
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
1977
2107
|
|
|
1978
2108
|
function updateOrient(val) {
|
|
1979
2109
|
setOrient(val)
|
|
1980
2110
|
try { compatibility.storage.setItem(PEAK_ORIENT_KEY, val) } catch (e) { /* ignore */ }
|
|
1981
|
-
|
|
1982
|
-
try { compatibility.dispatch(PEAK_RING_EVENT) } catch (e) { /* ignore */ }
|
|
2111
|
+
announcePrefs()
|
|
1983
2112
|
}
|
|
1984
2113
|
function updateScale(val) {
|
|
1985
2114
|
val = Math.min(1.2, Math.max(1.0, Math.round(val * 20) / 20))
|
|
1986
2115
|
setPeakScale(val)
|
|
1987
2116
|
try { compatibility.storage.setItem(PEAK_SCALE_KEY, String(val)) } catch (e) { /* ignore */ }
|
|
1988
|
-
|
|
1989
|
-
try { compatibility.dispatch(PEAK_RING_EVENT) } catch (e) { /* ignore */ }
|
|
2117
|
+
announcePrefs()
|
|
1990
2118
|
}
|
|
1991
2119
|
function updateRecharge(val) {
|
|
1992
2120
|
setShowRecharge(val)
|
|
1993
2121
|
try { compatibility.storage.setItem(PEAK_RECHARGE_KEY, String(val)) } catch (e) { /* ignore */ }
|
|
1994
|
-
|
|
1995
|
-
try { compatibility.dispatch(PEAK_RING_EVENT) } catch (e) { /* ignore */ }
|
|
2122
|
+
announcePrefs()
|
|
1996
2123
|
}
|
|
1997
2124
|
function updateNotify(val) {
|
|
1998
2125
|
setPeakNotify(val)
|
|
1999
2126
|
try { compatibility.storage.setItem(PEAK_NOTIFY_KEY, String(val)) } catch (e) { /* ignore */ }
|
|
2000
|
-
|
|
2127
|
+
announcePrefs(false)
|
|
2001
2128
|
}
|
|
2002
2129
|
|
|
2003
2130
|
function handlePointerDown(e) {
|
|
@@ -2037,11 +2164,10 @@ window.__ModuleLoader__.load({
|
|
|
2037
2164
|
if (dragRef.current.dragged) {
|
|
2038
2165
|
var w = dragRef.current.rectWidth || (isVertical ? 140 : 180)
|
|
2039
2166
|
var h = dragRef.current.rectHeight || (isVertical ? 120 : 60)
|
|
2040
|
-
var
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
var nextPos = { x: Math.round(nextX), y: Math.round(nextY) }
|
|
2167
|
+
var nextPos = clampPeakPosition({
|
|
2168
|
+
x: moveEvent.clientX - dragRef.current.grabOffsetX,
|
|
2169
|
+
y: moveEvent.clientY - dragRef.current.grabOffsetY,
|
|
2170
|
+
}, w, h, window.innerWidth, window.innerHeight, 8)
|
|
2045
2171
|
latestPosRef.current = nextPos
|
|
2046
2172
|
setPos(nextPos)
|
|
2047
2173
|
}
|
|
@@ -2056,9 +2182,20 @@ window.__ModuleLoader__.load({
|
|
|
2056
2182
|
setDragging(false)
|
|
2057
2183
|
setDock('free')
|
|
2058
2184
|
setPos(finalPos)
|
|
2059
|
-
|
|
2185
|
+
announcePrefs(false)
|
|
2060
2186
|
}
|
|
2061
2187
|
dragRef.current = null
|
|
2188
|
+
// Release the "this was a drag, swallow the click" latch after the
|
|
2189
|
+
// click that follows pointerup. Without this the flag stays true
|
|
2190
|
+
// whenever the next pointerdown never arrives (pointer released
|
|
2191
|
+
// outside the window, a cancelled gesture), and every later click on
|
|
2192
|
+
// the card is swallowed — the control panel then refuses to open
|
|
2193
|
+
// until the page is reloaded.
|
|
2194
|
+
if (typeof window.setTimeout === 'function') {
|
|
2195
|
+
window.setTimeout(function () { didDragRef.current = false }, 0)
|
|
2196
|
+
} else {
|
|
2197
|
+
didDragRef.current = false
|
|
2198
|
+
}
|
|
2062
2199
|
window.removeEventListener('pointermove', onMove)
|
|
2063
2200
|
window.removeEventListener('pointerup', onUp)
|
|
2064
2201
|
window.removeEventListener('pointercancel', onUp)
|
|
@@ -2076,11 +2213,11 @@ window.__ModuleLoader__.load({
|
|
|
2076
2213
|
try {
|
|
2077
2214
|
compatibility.storage.setItem(PEAK_DOCK_KEY, 'sidebar')
|
|
2078
2215
|
compatibility.storage.removeItem(PEAK_POS_KEY)
|
|
2079
|
-
compatibility.dispatch(SETTINGS_EVENT)
|
|
2080
|
-
compatibility.dispatch(PEAK_RING_EVENT)
|
|
2081
2216
|
} catch (err) { /* ignore */ }
|
|
2217
|
+
announcePrefs()
|
|
2082
2218
|
}
|
|
2083
2219
|
|
|
2220
|
+
|
|
2084
2221
|
var cardStyle = {}
|
|
2085
2222
|
if (isFreeFloating) {
|
|
2086
2223
|
cardStyle.position = 'fixed'
|
|
@@ -2154,7 +2291,7 @@ window.__ModuleLoader__.load({
|
|
|
2154
2291
|
onClick: handleResetDock
|
|
2155
2292
|
}, '↩ 归位') : null,
|
|
2156
2293
|
React.createElement('div', { style: { flex: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center' } },
|
|
2157
|
-
peakRingSVG(state.configured ? state.windows : null, wallHourIn(tzName, offsetMinutes, new Date(nowMs)), clockSize, state.ariaText)
|
|
2294
|
+
peakRingSVG(state.configured ? state.windows : null, wallHourIn(tzName, offsetMinutes, new Date(nowMs)), clockSize, state.ariaText, state.weekendOffPeak)
|
|
2158
2295
|
),
|
|
2159
2296
|
!isRail ? React.createElement('div', { className: 'dshw_footRingLabel' },
|
|
2160
2297
|
React.createElement('div', { className: 'dshw_footRingHeader' },
|
|
@@ -3853,6 +3990,7 @@ window.__ModuleLoader__.load({
|
|
|
3853
3990
|
computeSnapPreview: computeSnapPreview,
|
|
3854
3991
|
computeSideDockX: computeSideDockX,
|
|
3855
3992
|
computePanelPosition: computePanelPosition,
|
|
3993
|
+
clampPeakPosition: clampPeakPosition,
|
|
3856
3994
|
createCompatibilityAdapter: createCompatibilityAdapter,
|
|
3857
3995
|
fmtCurrency: fmtCurrency,
|
|
3858
3996
|
selectBalanceInfo: selectBalanceInfo,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deepseek-harness-wallet",
|
|
3
3
|
"description": "Local-first account monitoring, usage accounting, official recharge, completion reminders, flexible layout, host-gated session controls, and multi-account management with hot account switching for DeepSeek Harness.",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.6",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"exports": {
|