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