deepseek-harness-wallet 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## 0.2.4 - 2026-08-22
6
+
7
+ - 彻底修复暗色模式(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.
8
+ - 新增峰谷时钟专属控制面板(Dedicated Control Panel):点击时钟卡片即可就地弹出专属浮动控制面板,支持实时查看峰谷优惠说明、账户余额明细与快速充值,并提供排版切换、缩放调节与归位管理。 / Added a dedicated peak clock control popover: clicking the clock card opens an interactive panel for live rate info, quick recharge, orientation toggles, and layout controls.
9
+ - 峰谷时钟支持自由拖拽与持久化记忆:卡片支持在屏幕上任意拖拽移动,脱离侧边栏限制(Portal 渲染),拖拽释放自动持久化坐标,并在浮动卡片右上角和控制面板提供一键「↩ 归位」回到侧边栏底部。 / Added free-floating drag & drop for the peak clock: smoothly movable across the viewport with boundary clamping, position persistence in localStorage, and one-click dock reset.
10
+ - 峰谷时钟横向与纵向排版自由切换:支持标准的横向卡片与 ~140px 纤细纵向胶囊卡片(侧边栏内居中、浮动时轻巧紧凑)。 / Added customizable layout: seamlessly toggle between horizontal card layout and slim ~140px vertical capsule layout.
11
+ - 充值按钮内嵌排版优化:将充值按钮移至第三行倒计时右侧,彻底消除竖排按钮对第二行金额造成的水平挤压与 `¥...` 截断问题,金额与本场消耗 100% 完整显示。 / Relocated recharge button inline next to the countdown row, completely eliminating text truncation on the balance and session cost line.
12
+ - 新增时钟缩放滑条(100% ~ 120%)与充值按钮独立开关:支持按需放大时钟或收缩整体卡片尺寸。 / Added a dedicated scale slider (100% ~ 120%) and recharge button visibility toggle in both the popup panel and settings page.
13
+
14
+ ## 0.2.3 - 2026-08-21
15
+
16
+ - 修复 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.
17
+ - 修复首次添加账户时凭证同步失败仍显示为当前账户的问题;失败会回滚激活状态。 / Rolled back first-account activation when the host refuses credential synchronization.
18
+ - 修复删除账户后账户专属阈值未写回钱包存储的问题。 / Persisted removal of account-specific thresholds when an account is deleted.
19
+ - 余额错误改为安全枚举,避免把上游错误原文暴露到浏览器;美元/多余额记录的明细选择也统一。 / Replaced raw balance errors with safe enums and aligned multi-currency balance selection.
20
+ - 统一提醒设置的本地存储格式,并兼容旧的毫秒字段;API Key 输入框默认隐藏。 / Unified reminder storage with migration from the legacy millisecond field and made API-key inputs password fields.
21
+
5
22
  ## 0.2.2 - 2026-08-20
6
23
 
7
24
  - 新增 24h 峰谷计费分时时钟:在侧边栏左下角常驻展示当前时段(高峰/低谷半价)、剩余倒计时、实时余额与本场/本约花费;折叠导轨模式下自动收拢为 42px 紧凑环形钟。 / Added a 24h peak/off-peak ring clock in the sidebar footer: live pricing window (peak / 50% off-peak), switch countdown, live balance and session spend; collapses into a 42px compact circle in rail mode.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/deepseek-harness-wallet?label=npm&color=5965d8)](https://www.npmjs.com/package/deepseek-harness-wallet)
4
4
  [![GitHub release](https://img.shields.io/github/v/release/feibi-mochi/deepseek-harness-control-center?label=release&color=5965d8)](https://github.com/feibi-mochi/deepseek-harness-control-center/releases)
5
5
  [![CI](https://github.com/feibi-mochi/deepseek-harness-control-center/actions/workflows/validate.yml/badge.svg)](https://github.com/feibi-mochi/deepseek-harness-control-center/actions/workflows/validate.yml)
6
- [![DeepSeek Harness](https://img.shields.io/badge/DeepSeek%20Harness-0.1.0--rc.6-4aa3ff)](https://github.com/deepseek-ai/DeepSeek-Harness)
6
+ [![DeepSeek Harness](https://img.shields.io/badge/DeepSeek%20Harness-0.1.0--rc.8-4aa3ff)](https://github.com/deepseek-ai/DeepSeek-Harness)
7
7
  [![License: MIT](https://img.shields.io/badge/license-MIT-3b7a57)](./LICENSE)
8
8
 
9
9
  **DeepSeek Harness monitoring, alerts, recharge, and session control center.**
@@ -25,7 +25,8 @@
25
25
  - **Official DeepSeek** — live balance (60s global refresh with fast boot retries), current-session cost locked to the price active for each usage event (including the 2026-08-17 peak/off-peak rollout), and token breakdown.
26
26
  - **24h peak/off-peak ring clock** — resident sidebar footer widget indicating real-time pricing windows (peak vs. 50% discount off-peak), countdown to next switch, and optional desktop switch notifications.
27
27
  - **Third-party total** — current-session tokens (input / cache read / output). No balance guessing, no cost math, zero configuration.
28
- - **Click the chip** to open the detail panel: correctly formatted per-currency balances, cost and token splits, a freely editable low-balance threshold in CNY (two decimals, persisted globally; alerts only compare a CNY balance and never mix currencies), manual refresh, and a jump to the official recharge page (first click shows the domain for confirmation — anti-phishing).
28
+ - **Provider classification** observed wrapper routes appear in the settings page; opted-in routes join the official token/cost bucket and are priced with the official table.
29
+ - **Click the chip** to open the detail panel: correctly formatted per-currency balances, cost and token splits, a freely editable low-balance threshold for the active account and currency (two decimals, persisted per account; alerts never mix currencies), manual refresh, and a jump to the official recharge page (first click shows the domain for confirmation — anti-phishing).
29
30
  - **Move, dock, and scale** — drag the chip freely, preview nearby snap targets, use compact horizontal or vertical layouts, adjust its scale from the control panel, and show official or third-party data independently. The choices are remembered locally.
30
31
  - **Floating window mode** — detach the detail panel into a draggable window with a remembered position, or minimize it directly to a freely movable dot; the dot turns red below the threshold.
31
32
  - **Completion reminders** — optionally notify when a conversation finishes, with persistent or timed modes, queueing and deduplication for simultaneous completions, cross-tab coordination, and an in-page fallback when system notifications are unavailable.
@@ -127,7 +128,7 @@ For buildable DSH hosts, the npm package and repository include a versioned [Age
127
128
 
128
129
  | Item | Behavior |
129
130
  | --- | --- |
130
- | Token accounting | Listens to the `llm/stream` event and buckets per provider (`deepseek-official` vs. everything else) and per session; each usage event also locks its contemporaneous official price, so multiple sessions and pricing windows never mix. |
131
+ | Token accounting | Listens to the `llm/stream` event and buckets per session and provider: `deepseek-official` plus explicitly opted-in wrapper routes use the official bucket; other providers stay third-party; each usage event also locks its contemporaneous official price, so multiple sessions and pricing windows never mix. |
131
132
  | Balance | The key from the credentials seam (or the active account's key) never leaves this machine except as the `Authorization` header of the official `/user/balance` request. |
132
133
  | Accounts | Keys live in `$DSH_HOME/storages/accounts.json` (plaintext, matching the harness's own credential storage); the UI only ever shows masked keys, and switching writes the chosen key into the credentials seam for LLM billing. |
133
134
  | Session log | The plugin writes no events; its data lives in `$DSH_HOME/storages/wallet.json`. |
package/index.js CHANGED
@@ -386,16 +386,18 @@ export function removeAccount(id) {
386
386
  const index = accounts.accounts.findIndex((account) => account.id === id)
387
387
  if (index < 0) return { ok: false, error: 'account not found' }
388
388
  accounts.accounts.splice(index, 1)
389
- // Drop the account's own threshold line along with it.
389
+ // Drop the account's own threshold line along with it. The caller persists
390
+ // wallet.json separately from accounts.json when this flag is true.
391
+ let thresholdRemoved = false
390
392
  if (store.accountThresholds && Object.hasOwn(store.accountThresholds, id)) {
391
393
  delete store.accountThresholds[id]
392
- scheduleAccountsSave(null)
394
+ thresholdRemoved = true
393
395
  }
394
396
  // Deliberately NOT unsetting the credentials seam: the key currently in
395
397
  // .credentials.yaml keeps working for LLM billing; the UI just reports
396
398
  // "no active account" and balance falls back to the seam key.
397
399
  if (accounts.activeId === id) accounts.activeId = null
398
- return { ok: true }
400
+ return { ok: true, thresholdRemoved }
399
401
  }
400
402
 
401
403
  export function accountListView() {
@@ -485,12 +487,18 @@ function recordUsage(options, usage, atMs = Date.now()) {
485
487
  if (typeof sessionId !== 'string' || sessionId === '' || typeof provider !== 'string' || provider === '') return
486
488
  if (typeof model !== 'string' || model === '__proto__' || model === 'prototype' || model === 'constructor') return
487
489
  if (sessionId === '__proto__' || sessionId === 'prototype' || sessionId === 'constructor') return
490
+ // Remember every non-builtin route observed by the stream tap. The settings
491
+ // page can then promote wrapper routes into the official billing bucket.
492
+ if (provider !== OFFICIAL_PROVIDER && !store.knownProviders.includes(provider)) {
493
+ store.knownProviders = normalizeProviderList([...store.knownProviders, provider])
494
+ }
488
495
  if (!Object.hasOwn(store.sessions, sessionId)) {
489
496
  store.sessions[sessionId] = { official: emptyBucket(true), third: emptyBucket(false) }
490
497
  }
491
498
  const session = store.sessions[sessionId]
492
- const bucket = provider === OFFICIAL_PROVIDER ? session.official : session.third
493
- if (provider === OFFICIAL_PROVIDER) addOfficialUsage(bucket, model, usage, atMs)
499
+ const official = isOfficialProvider(provider, store.officialProviders)
500
+ const bucket = official ? session.official : session.third
501
+ if (official) addOfficialUsage(bucket, model, usage, atMs)
494
502
  else {
495
503
  if (!Object.hasOwn(bucket.models, model)) bucket.models[model] = emptyCounters()
496
504
  addUsage(bucket.models[model], usage)
@@ -503,13 +511,13 @@ let balanceRefresh = null
503
511
  async function performBalanceRefresh(ctx) {
504
512
  const credentials = ctx.get('credentials')
505
513
  if (credentials === undefined) {
506
- balance = { fetchedAt: Date.now(), available: false, balances: [], error: 'no credentials service' }
514
+ balance = { fetchedAt: Date.now(), available: false, balances: [], error: 'no-credentials' }
507
515
  return
508
516
  }
509
517
  try {
510
518
  const key = await resolveBalanceKey(ctx)
511
519
  if (key === undefined || key === '') {
512
- balance = { fetchedAt: Date.now(), available: false, balances: [], error: 'no API key' }
520
+ balance = { fetchedAt: Date.now(), available: false, balances: [], error: 'no-api-key' }
513
521
  return
514
522
  }
515
523
  const controller = new AbortController()
@@ -520,23 +528,36 @@ async function performBalanceRefresh(ctx) {
520
528
  headers: { authorization: 'Bearer ' + key },
521
529
  signal: controller.signal,
522
530
  })
523
- if (!response.ok) throw new Error('status ' + response.status)
531
+ if (!response.ok) {
532
+ const code = response.status === 401 || response.status === 403
533
+ ? 'unauthorized'
534
+ : response.status === 429 ? 'rate-limited' : 'upstream-unavailable'
535
+ const error = new Error(code)
536
+ error.code = code
537
+ throw error
538
+ }
524
539
  const data = await response.json()
540
+ const available = data !== null && typeof data === 'object' && data.is_available === true
525
541
  balance = {
526
542
  fetchedAt: Date.now(),
527
- available: data.is_available === true,
528
- balances: Array.isArray(data.balance_infos) ? data.balance_infos : [],
529
- error: null,
543
+ available,
544
+ balances: available && Array.isArray(data.balance_infos) ? data.balance_infos : [],
545
+ error: available ? null : 'balance-unavailable',
530
546
  }
531
547
  } finally {
532
548
  clearTimeout(timer)
533
549
  }
534
550
  } catch (error) {
551
+ const code = error && typeof error === 'object' && typeof error.code === 'string'
552
+ ? error.code
553
+ : error && typeof error === 'object' && error.name === 'AbortError'
554
+ ? 'timeout'
555
+ : error instanceof SyntaxError ? 'invalid-response' : 'upstream-unavailable'
535
556
  balance = {
536
557
  fetchedAt: Date.now(),
537
558
  available: false,
538
559
  balances: [],
539
- error: error instanceof Error ? error.message : String(error),
560
+ error: code,
540
561
  }
541
562
  }
542
563
  }
@@ -836,7 +857,8 @@ export function apply(ctx, config) {
836
857
  if (body === null || typeof body.name !== 'string' || typeof body.apiKey !== 'string') {
837
858
  return json(res, 400, { ok: false, error: 'name and apiKey are required' })
838
859
  }
839
- const needSync = accounts.activeId === null
860
+ const previousActiveId = accounts.activeId
861
+ const needSync = previousActiveId === null
840
862
  const result = addAccount(body.name, body.apiKey)
841
863
  if (!result.ok) return json(res, 400, { ok: false, error: result.error })
842
864
  scheduleAccountsSave(ctx.logger)
@@ -844,6 +866,13 @@ export function apply(ctx, config) {
844
866
  if (needSync) {
845
867
  // First account: activate it so the LLM seam follows the new key.
846
868
  sync = await activateAccount(ctx, result.account.id)
869
+ // addAccount marks the first row active optimistically. If the host
870
+ // refuses the credential write, roll it back so UI/balance state does
871
+ // not claim a billing account that the LLM route is not using.
872
+ if (!sync.ok && accounts.activeId === result.account.id) {
873
+ accounts.activeId = previousActiveId
874
+ scheduleAccountsSave(ctx.logger)
875
+ }
847
876
  }
848
877
  return json(res, 200, {
849
878
  ok: true,
@@ -884,6 +913,7 @@ export function apply(ctx, config) {
884
913
  const result = removeAccount(body.id)
885
914
  if (!result.ok) return json(res, 400, { ok: false, error: result.error })
886
915
  scheduleAccountsSave(ctx.logger)
916
+ if (result.thresholdRemoved) scheduleSave(ctx.logger)
887
917
  return json(res, 200, { ok: true, ...accountListView() })
888
918
  },
889
919
  })