deepseek-harness-wallet 0.3.10 → 0.3.12
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 +12 -0
- package/README.md +18 -25
- package/index.js +43 -12
- package/lib/client.js +1 -1
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.3.12 - Unreleased
|
|
6
|
+
|
|
7
|
+
- 修复新版宿主中钱包模型目录调用缺少 `remote.session` 注入而中断工作区/会话界面的问题。 / Declare the remote session dependency required by model-directory calls so workspace/session rendering does not fail on newer hosts.
|
|
8
|
+
- 增加 DSH `0.1.5-alpha.1` 的精确兼容声明;健康检查只对清单中明确记录的版本显示兼容,未来版本保持“尚未验证”。 / Declare exact DSH `0.1.5-alpha.1` compatibility; health checks keep unverified future releases unknown instead of assuming every newer host is compatible.
|
|
9
|
+
- 更新中英文介绍、兼容验证范围、源码位置、账本容量和汇率估算说明,区分钱包功能与宿主删除能力。 / Refresh bilingual feature and compatibility descriptions, source paths, ledger bounds, and currency-estimate limitations; distinguish wallet features from host-provided deletion.
|
|
10
|
+
|
|
11
|
+
## 0.3.11 - 2026-09-05
|
|
12
|
+
|
|
13
|
+
- 切换账户时清除旧余额并启动独立刷新,取消旧请求并忽略其迟到成功或失败;删除当前账户后重新读取宿主凭据对应的余额。 / Clear cached balance on account switches, start a fresh request, and discard stale successes or failures; refresh the host-credential balance after removing the active account.
|
|
14
|
+
- 设置备份按顺序发送,失败重试保留最新编辑,避免旧请求覆盖已保存的新设置,同时保留有限次数重试。 / Serialize preference backups and retain the newest queued edits across bounded retries so older requests cannot overwrite newer settings.
|
|
15
|
+
- 浏览器拒绝访问 localStorage 属性时,客户端仍可初始化并使用内存回退和宿主设置备份;桌面存储桥保持优先。 / Keep client initialization, memory storage, and host preference hydration working when access to the localStorage property is denied, preserving desktop storage bridge precedence.
|
|
16
|
+
|
|
5
17
|
## 0.3.10 - 2026-09-05
|
|
6
18
|
|
|
7
19
|
- 拆分可读客户端源码并提交可复现的精简加载产物,使每个文件低于 DSH STORE 的 256 KiB 自动审核上限;安装仍不需要构建或运行依赖。 / Split readable client sources and commit a reproducible compact loader artifact below DSH STORE's 256 KiB per-file review bound, retaining build-free installation and zero runtime dependencies.
|
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.**
|
|
@@ -14,12 +14,14 @@
|
|
|
14
14
|
|
|
15
15
|
> A local-first companion that keeps account status, per-conversation usage, completion reminders, official recharge, flexible layout, and host-gated session controls beside the DSH composer.
|
|
16
16
|
|
|
17
|
-
> **Version:** v0.3.
|
|
17
|
+
> **Version:** v0.3.12 (unreleased).
|
|
18
18
|
|
|
19
19
|
> If DeepSeek Harness Control Center helps you, please consider leaving a ⭐ Star. Thank you!
|
|
20
20
|
|
|
21
21
|
## What it does
|
|
22
22
|
|
|
23
|
+
The wallet UI is currently primarily Chinese and does not yet follow the host language setting (#32). DeepSeek balances come from the official API; cost accounting is a local estimate.
|
|
24
|
+
|
|
23
25
|
```
|
|
24
26
|
余额 ¥5.89 · 本场 ¥0.72 · 官 18.8M | 三方 800K · ↗充
|
|
25
27
|
```
|
|
@@ -30,7 +32,7 @@
|
|
|
30
32
|
- **Official pricing sync** — periodically checks the official DeepSeek pricing page and applies only a fully validated table. Network failures retain the last validated rule (or the built-in rule before the first successful sync); an unrecognized page structure is marked for review instead of silently changing billing.
|
|
31
33
|
- **Z.ai Coding Plan quotas** — a generic official-plan adapter monitors configured Global and China plans without exposing credentials. It separates the 5-hour model-token window from monthly MCP-tool usage, leads with quota remaining from 100% down while keeping usage as secondary context, retains the last successful snapshot on failure, and never converts subscription quota into CNY balance.
|
|
32
34
|
- **Provider-aware composer surfaces** — the chip and sidebar clock follow the session's selected provider/model. Z.ai—including transparent `vision-toolkit-` variants—replaces DeepSeek balance, recharge, and peak pricing with plan-window summaries; unrelated providers show only their own session tokens, and DeepSeek V4 restores the wallet and peak clock.
|
|
33
|
-
- **365
|
|
35
|
+
- **Local usage ledger (up to 365 days / 20,000 events)** — Wallet settings keeps the heatmap visible, while compact wallet panels keep it collapsible. Stable request identities are deduplicated, official cost is locked at usage time, official and third-party data stay separate, and prompts or responses are never stored. Collection begins after upgrading to v0.3.2; older aggregate counters have no trustworthy dates and are not backfilled.
|
|
34
36
|
- **Third-party total** — current-session tokens (input / cache read / output) remain available with zero configuration.
|
|
35
37
|
- **Custom third-party pricing** — enter an exact Provider/model route, currency, and base per-million input/cache-read/cache-write/output rates, then optionally add multiple IANA-timezone, weekday, and cross-midnight pricing windows. Current-session and 365-day ledger costs are recalculated locally by occurrence time and clearly labeled as user-defined estimates, never as provider balances or invoices.
|
|
36
38
|
- **Provider classification** — observed wrapper routes appear in the settings page; opted-in routes join the official token/cost bucket for subsequent calls and are priced with the official table. Existing history is not retroactively reclassified.
|
|
@@ -39,7 +41,6 @@
|
|
|
39
41
|
- **Composer-label controls and skin compatibility** — independently show/hide the composer label or reduce it to the primary remaining value (DeepSeek balance, Z.ai five-hour quota) without disabling reminders, settings, plan monitoring, or history. The standard chip also resists broad skin button rules and aligns with maid-atelier's 38px navy-and-gold composer toolbar.
|
|
40
42
|
- **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.
|
|
41
43
|
- **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.
|
|
42
|
-
- **Optional permanent deletion** — when the DSH host advertises a real deletion capability, an opt-in setting enables a confirmed permanent-delete action in the session menu; unsupported hosts keep the control disabled.
|
|
43
44
|
- **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.
|
|
44
45
|
- **Theme-native UI** — uses DSH `--dsw-alias-*` variables with safe fallback colors, so light and dark themes both render correctly; the panel closes when you click outside and flips open-direction near screen edges.
|
|
45
46
|
- **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. Historical ledger clearing is a separate action.
|
|
@@ -47,30 +48,16 @@
|
|
|
47
48
|
## Multi-account
|
|
48
49
|
|
|
49
50
|
- Open the wallet panel → **Account Management(账户管理)** to add accounts (name + API key), switch the active one, or remove them.
|
|
50
|
-
-
|
|
51
|
+
- Adding the first account attempts to synchronize its key with the host credential store. It activates only if that write succeeds; a refused write retains the saved account without marking it active for billing.
|
|
51
52
|
- 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.
|
|
52
53
|
- Account keys are encrypted at rest in `$DSH_HOME/storages/accounts.json`: Windows uses the current user's DPAPI; other platforms use an owner-only AES-GCM key file. An encrypted `.bak` recovers a missing, corrupt, or undecryptable primary file; if neither copy can be read, writes are locked instead of overwriting account data. The UI only shows masked keys.
|
|
53
|
-
- Session usage estimates follow the active account's currency: USD-settled accounts show `本约 $x`, converted from the CNY price table
|
|
54
|
+
- Session usage estimates follow the active account's currency: USD-settled accounts show `本约 $x`, converted from the CNY price table using a fixed estimate of 7.25 CNY per USD (neither a live exchange rate nor the official USD tariff); CNY accounts show `本场 ¥x`. These are local estimates, not an official invoice.
|
|
54
55
|
- 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.
|
|
55
56
|
|
|
56
57
|
|
|
57
58
|
## Project overview
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
DeepSeek Harness can keep several conversations and model providers active at once, but balance, usage, background-task status, and session actions normally live in different places. Control Center brings the information worth checking repeatedly beside the composer, so the current workflow can answer three questions at a glance: **How much official balance remains? What has this conversation used? Does anything need attention?**
|
|
62
|
-
|
|
63
|
-
### Present when needed, quiet when not
|
|
64
|
-
|
|
65
|
-
The project is designed around quick reading and in-context action rather than another full-page dashboard. Its compact surface expands only when needed, adapts to the available space, and leaves layout and reminder behavior under the user's control. Accounting remains separated by conversation and provider, while wallet-data cleanup and permanent session deletion remain intentionally different operations.
|
|
66
|
-
|
|
67
|
-
### Extensible without hiding the boundaries
|
|
68
|
-
|
|
69
|
-
The npm package handles monitoring and interface behavior; optional host powers are enabled only when DSH actually provides them. That capability-based boundary keeps unsupported actions visibly unavailable and gives browsers or desktop wrappers a small, reviewable adaptation surface. Future providers and controls can therefore be added without changing the established `deepseek-harness-wallet` package identity or silently expanding what the plugin is trusted to do.
|
|
70
|
-
|
|
71
|
-
> **Want permanent session deletion?** It cannot be enabled by configuring the plugin alone. Give the [integration guide](./integrations/dsh-session-delete/README.md) and [Agent adaptation prompt](./integrations/dsh-session-delete/AGENT_PROMPT.md) to an Agent with access to the buildable DSH source. The control-panel switch becomes available only after the host implementation is built, tested, and advertises the capability.
|
|
72
|
-
|
|
73
|
-
Details: [compatibility](#browser-desktop-and-os-compatibility) · [data and trust](#data--trust) · [pricing](#pricing-timeline)
|
|
60
|
+
DeepSeek Harness Wallet is for users who want to check balances during a conversation, compare model costs, or manage multiple accounts. A chip beside the composer shows the selected model’s balance, remaining plan quota, or token usage; open it for details or detach it into a floating panel. Settings brings together encrypted accounts, low-balance and completion reminders, a usage heatmap, and fixed or time-of-use prices for third-party APIs. DeepSeek balances and Z.ai quotas come from their respective APIs. Costs are estimated locally from host-reported usage, helping you track consumption without replacing provider invoices.
|
|
74
61
|
|
|
75
62
|
## Install
|
|
76
63
|
|
|
@@ -112,12 +99,14 @@ dsh plugin --profile web remove deepseek-harness-wallet
|
|
|
112
99
|
|
|
113
100
|
## Browser, desktop, and OS compatibility
|
|
114
101
|
|
|
102
|
+
See [0.3.12 compatibility evidence](https://github.com/feibi-mochi/deepseek-harness-control-center/blob/main/docs/compatibility-0.3.12.md). The official 0.1.5-alpha.1 host does not provide this plugin’s permanent-delete capability, so that switch stays disabled; the old source integration patch is not a drop-in update.
|
|
103
|
+
|
|
115
104
|
The client contains no operating-system-specific feature branch; it checks the Web and host capabilities it needs. That makes the same code portable, but **portable code is not the same as real-device verification**:
|
|
116
105
|
|
|
117
106
|
| Verification level | Coverage |
|
|
118
107
|
| --- | --- |
|
|
119
|
-
|
|
|
120
|
-
|
|
|
108
|
+
| Current local verification | Windows + Node 24.18.1 + DSH 0.1.5-alpha.1; keyless Web UI and isolated lifecycle checks. No live paid-API acceptance |
|
|
109
|
+
| Exact host coverage | 0.1.5-alpha.1 checked for this update; earlier 0.1.2-alpha.3/alpha.4/alpha.5/rc.1 evidence belongs to previous wallet releases. Other releases remain unverified |
|
|
121
110
|
| Automated compatibility checks | Browser notification failure, in-page fallback, cross-tab fallback, storage fallback, CSS-scale fallback, and synchronous/asynchronous desktop adapters |
|
|
122
111
|
| Capability-compatible targets | Current Chrome, Edge, and Firefox on Windows/macOS/Linux; Safari on macOS; Electron/Tauri-style DSH wrappers that provide the requirements below |
|
|
123
112
|
|
|
@@ -139,10 +128,14 @@ window.__DSH_WALLET_ADAPTER__ = {
|
|
|
139
128
|
}
|
|
140
129
|
```
|
|
141
130
|
|
|
142
|
-
`notify()` may return a notification-like handle, a Promise for one, or nothing for fire-and-forget native APIs. The payload also includes `onClick` / `onClose` callbacks so Electron IPC, Tauri notification actions, and other desktop bridges can return events without copying wallet logic; returning `false` asks the wallet to use its browser fallback. `requestNotificationPermission()` is optional for hosts such as Tauri and macOS that require a native permission request. Returning `false` from `openExternal()` likewise asks the wallet to try the browser fallback. Declare `permanentDelete` only when the host actually implements the wallet preference and session-menu action; compatible hosts advertise it automatically, while unsupported hosts show a disabled control instead of a switch that has no effect. Platform adaptations are intentionally confined to `createCompatibilityAdapter()` in `
|
|
131
|
+
`notify()` may return a notification-like handle, a Promise for one, or nothing for fire-and-forget native APIs. The payload also includes `onClick` / `onClose` callbacks so Electron IPC, Tauri notification actions, and other desktop bridges can return events without copying wallet logic; returning `false` asks the wallet to use its browser fallback. `requestNotificationPermission()` is optional for hosts such as Tauri and macOS that require a native permission request. Returning `false` from `openExternal()` likewise asks the wallet to try the browser fallback. Declare `permanentDelete` only when the host actually implements the wallet preference and session-menu action; compatible hosts advertise it automatically, while unsupported hosts show a disabled control instead of a switch that has no effect. Platform adaptations are intentionally confined to `createCompatibilityAdapter()` in `src/client/core.js`, so an Agent can add a new wrapper without editing wallet accounting or UI logic.
|
|
143
132
|
|
|
144
133
|
For buildable DSH hosts, the npm package and repository include a versioned [Agent-assisted permanent-delete integration kit](./integrations/dsh-session-delete/README.md) with a Chinese guide, complete Agent prompt, read-only preflight, compatibility manifest, upstream notice, and an exact-baseline reference patch. The patch is not a universal installer: a different DSH commit must be inspected and adapted by semantics, and closed or non-rebuildable desktop applications remain unsupported.
|
|
145
134
|
|
|
135
|
+
## Optional host integration
|
|
136
|
+
|
|
137
|
+
Permanent session deletion is implemented by the host, not the wallet. Its switch remains disabled on the official 0.1.5-alpha.1 host. For a source integration, see the [guide](./integrations/dsh-session-delete/README.md) and [Agent adaptation prompt](./integrations/dsh-session-delete/AGENT_PROMPT.md); the old reference patch is not a drop-in update for newer hosts.
|
|
138
|
+
|
|
146
139
|
## Data & trust
|
|
147
140
|
|
|
148
141
|
Client development now uses five readable files under `src/client/` and a committed `lib/client.js` artifact; run `npm ci`, `npm run build:client`, and `npm run check:client` after source edits. Installation runs no build scripts. Exact-version smoke checks are documented in [0.3.10 compatibility evidence](https://github.com/feibi-mochi/deepseek-harness-control-center/blob/main/docs/compatibility-0.3.10.md).
|
|
@@ -152,7 +145,7 @@ Client development now uses five readable files under `src/client/` and a commit
|
|
|
152
145
|
| 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. |
|
|
153
146
|
| 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. |
|
|
154
147
|
| Accounts | Keys live encrypted in `$DSH_HOME/storages/accounts.json`, with an encrypted `accounts.json.bak` fallback for a missing, corrupt, or undecryptable primary. Windows uses current-user DPAPI; other platforms use an owner-only AES-GCM key file, so move `accounts.json`, `.bak`, and `.key` together. If neither copy can be read, account writes fail closed. |
|
|
155
|
-
| Usage ledger | Local events and custom third-party price rules live in `$DSH_HOME/storages/wallet.json` with a `wallet.json.bak` recovery copy. Missing/corrupt primaries recover automatically; if neither copy is readable, wallet writes fail closed. Up to 365 days of session/provider/model/token metadata and official locked cost are kept—never prompts, tool arguments, response bodies, or API keys. Third-party estimates are recalculated from the current custom rule and each retained event's occurrence time; aggregate usage without a retained timestamp safely falls back to the base rate. |
|
|
148
|
+
| Usage ledger | Local events and custom third-party price rules live in `$DSH_HOME/storages/wallet.json` with a `wallet.json.bak` recovery copy. Missing/corrupt primaries recover automatically; if neither copy is readable, wallet writes fail closed. Up to 365 days and 20,000 events of session/provider/model/token metadata and official locked cost are kept—never prompts, tool arguments, response bodies, or API keys. Third-party estimates are recalculated from the current custom rule and each retained event's occurrence time; aggregate usage without a retained timestamp safely falls back to the base rate. |
|
|
156
149
|
| Local settings | Layout, scale, visibility, reminder, and panel settings stay in browser-compatible local storage. |
|
|
157
150
|
| Permanent deletion | Opt-in and host-gated. The wallet never advertises the action unless the host implements the matching session deletion path. |
|
|
158
151
|
| Model surface | No tools registered, no prompt injection, zero token cost. |
|
package/index.js
CHANGED
|
@@ -34,11 +34,11 @@ const TRANSPARENT_PROVIDER_PREFIX = 'vision-toolkit-'
|
|
|
34
34
|
// DeepSeek's official paid-API bucket by the wrapper-provider alias control.
|
|
35
35
|
const PLAN_PROVIDER_IDS = new Set(PLAN_ADAPTERS.map((adapter) => adapter.provider))
|
|
36
36
|
const RECHARGE_URL = 'https://platform.deepseek.com/top_up'
|
|
37
|
-
const PLUGIN_VERSION = '0.3.
|
|
37
|
+
const PLUGIN_VERSION = '0.3.12'
|
|
38
38
|
const PRICING_SOURCE_URL = 'https://api-docs.deepseek.com/zh-cn/quick_start/pricing/'
|
|
39
39
|
const PRICING_SYNC_INTERVAL_MS = 6 * 60 * 60_000
|
|
40
40
|
const PRICING_SYNC_TIMEOUT_MS = 8_000
|
|
41
|
-
const MIN_HOST_VERSION = '0.1.
|
|
41
|
+
const MIN_HOST_VERSION = '0.1.2-alpha.3'
|
|
42
42
|
const DSH_HOME = process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
43
43
|
const STORE_PATH = join(DSH_HOME, 'storages', 'wallet.json')
|
|
44
44
|
const STORE_BACKUP_PATH = STORE_PATH + '.bak'
|
|
@@ -414,6 +414,7 @@ function detectHostManifest() {
|
|
|
414
414
|
|
|
415
415
|
function compareVersions(left, right) {
|
|
416
416
|
const parse = (value) => {
|
|
417
|
+
if (typeof value !== 'string') return null
|
|
417
418
|
const match = typeof value === 'string' && value.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/)
|
|
418
419
|
if (match === null) return null
|
|
419
420
|
return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), pre: match[4] || '' }
|
|
@@ -431,15 +432,22 @@ function compareVersions(left, right) {
|
|
|
431
432
|
}
|
|
432
433
|
|
|
433
434
|
const HOST_MANIFEST = detectHostManifest()
|
|
435
|
+
const COMPATIBILITY_RELEASES = readPackageManifest(new URL('./package.json', import.meta.url))?.dsh?.compatibility?.dshReleases || {}
|
|
434
436
|
|
|
435
|
-
export function
|
|
436
|
-
const hostVersion = HOST_MANIFEST.version
|
|
437
|
+
export function hostCompatibility(hostVersion) {
|
|
437
438
|
const comparison = compareVersions(hostVersion, MIN_HOST_VERSION)
|
|
438
|
-
|
|
439
|
+
return comparison === null
|
|
439
440
|
? { status: 'unknown', minimumVersion: MIN_HOST_VERSION, message: '无法读取 Harness 版本' }
|
|
440
|
-
: comparison
|
|
441
|
-
? { status: '
|
|
442
|
-
:
|
|
441
|
+
: comparison < 0
|
|
442
|
+
? { status: 'upgrade-recommended', minimumVersion: MIN_HOST_VERSION, message: 'Harness 版本低于插件建议版本' }
|
|
443
|
+
: Object.hasOwn(COMPATIBILITY_RELEASES, hostVersion) && COMPATIBILITY_RELEASES[hostVersion] === 'compatible'
|
|
444
|
+
? { status: 'compatible', minimumVersion: MIN_HOST_VERSION, message: '该精确版本已声明兼容;验证范围见兼容文档' }
|
|
445
|
+
: { status: 'unknown', minimumVersion: MIN_HOST_VERSION, message: '该 Harness 版本尚未验证兼容性' }
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function hostHealthSnapshot() {
|
|
449
|
+
const hostVersion = HOST_MANIFEST.version
|
|
450
|
+
const compatibility = hostCompatibility(hostVersion)
|
|
443
451
|
return {
|
|
444
452
|
name: HOST_MANIFEST.name,
|
|
445
453
|
version: hostVersion,
|
|
@@ -1913,7 +1921,10 @@ export function removeAccount(id) {
|
|
|
1913
1921
|
// Deliberately NOT unsetting the credentials seam: the key currently in
|
|
1914
1922
|
// .credentials.yaml keeps working for LLM billing; the UI just reports
|
|
1915
1923
|
// "no active account" and balance falls back to the seam key.
|
|
1916
|
-
if (accounts.activeId === id)
|
|
1924
|
+
if (accounts.activeId === id) {
|
|
1925
|
+
accounts.activeId = null
|
|
1926
|
+
invalidateBalance()
|
|
1927
|
+
}
|
|
1917
1928
|
return { ok: true, thresholdRemoved }
|
|
1918
1929
|
}
|
|
1919
1930
|
|
|
@@ -1971,6 +1982,7 @@ export async function activateAccount(ctx, id) {
|
|
|
1971
1982
|
}
|
|
1972
1983
|
accounts.activeId = account.id
|
|
1973
1984
|
scheduleAccountsSave(ctx.logger)
|
|
1985
|
+
invalidateBalance()
|
|
1974
1986
|
void refreshBalance(ctx)
|
|
1975
1987
|
return { ok: true, account: { id: account.id, name: account.name, maskedKey: maskKey(account.apiKey) } }
|
|
1976
1988
|
}
|
|
@@ -2043,8 +2055,18 @@ function recordUsage(options, usage, atMs = Date.now()) {
|
|
|
2043
2055
|
|
|
2044
2056
|
let balance = { fetchedAt: 0, available: false, balances: [], error: null }
|
|
2045
2057
|
let balanceRefresh = null
|
|
2058
|
+
let balanceGeneration = 0
|
|
2059
|
+
let balanceController = null
|
|
2060
|
+
|
|
2061
|
+
function invalidateBalance() {
|
|
2062
|
+
balanceGeneration += 1
|
|
2063
|
+
balance = { fetchedAt: 0, available: false, balances: [], error: null }
|
|
2064
|
+
balanceRefresh = null
|
|
2065
|
+
if (balanceController !== null) balanceController.abort()
|
|
2066
|
+
balanceController = null
|
|
2067
|
+
}
|
|
2046
2068
|
|
|
2047
|
-
async function performBalanceRefresh(ctx) {
|
|
2069
|
+
async function performBalanceRefresh(ctx, generation) {
|
|
2048
2070
|
try {
|
|
2049
2071
|
const credentials = ctx.get('credentials')
|
|
2050
2072
|
if (credentials === undefined) {
|
|
@@ -2052,11 +2074,13 @@ async function performBalanceRefresh(ctx) {
|
|
|
2052
2074
|
return
|
|
2053
2075
|
}
|
|
2054
2076
|
const key = await resolveBalanceKey(ctx)
|
|
2077
|
+
if (generation !== balanceGeneration) return
|
|
2055
2078
|
if (key === undefined || key === '') {
|
|
2056
2079
|
balance = { fetchedAt: Date.now(), available: false, balances: [], error: 'no-api-key' }
|
|
2057
2080
|
return
|
|
2058
2081
|
}
|
|
2059
2082
|
const controller = new AbortController()
|
|
2083
|
+
balanceController = controller
|
|
2060
2084
|
const timer = setTimeout(() => controller.abort(), 15000)
|
|
2061
2085
|
try {
|
|
2062
2086
|
const response = await fetch('https://api.deepseek.com/user/balance', {
|
|
@@ -2073,6 +2097,7 @@ async function performBalanceRefresh(ctx) {
|
|
|
2073
2097
|
throw error
|
|
2074
2098
|
}
|
|
2075
2099
|
const data = await response.json()
|
|
2100
|
+
if (generation !== balanceGeneration) return
|
|
2076
2101
|
const available = data !== null && typeof data === 'object' && data.is_available === true
|
|
2077
2102
|
balance = {
|
|
2078
2103
|
fetchedAt: Date.now(),
|
|
@@ -2082,8 +2107,10 @@ async function performBalanceRefresh(ctx) {
|
|
|
2082
2107
|
}
|
|
2083
2108
|
} finally {
|
|
2084
2109
|
clearTimeout(timer)
|
|
2110
|
+
if (balanceController === controller) balanceController = null
|
|
2085
2111
|
}
|
|
2086
2112
|
} catch (error) {
|
|
2113
|
+
if (generation !== balanceGeneration) return
|
|
2087
2114
|
const code = error && typeof error === 'object' && typeof error.code === 'string'
|
|
2088
2115
|
? error.code
|
|
2089
2116
|
: error && typeof error === 'object' && error.name === 'AbortError'
|
|
@@ -2100,11 +2127,13 @@ async function performBalanceRefresh(ctx) {
|
|
|
2100
2127
|
|
|
2101
2128
|
function refreshBalance(ctx) {
|
|
2102
2129
|
if (balanceRefresh !== null) return balanceRefresh
|
|
2130
|
+
const generation = balanceGeneration
|
|
2103
2131
|
// Never let a refresh escape as an unhandled rejection: every caller uses
|
|
2104
2132
|
// `void refreshBalance(ctx)`, so a throw here would take the host process
|
|
2105
2133
|
// down (Node exits on unhandled rejections). Absorb it into the error state.
|
|
2106
|
-
balanceRefresh = performBalanceRefresh(ctx)
|
|
2134
|
+
balanceRefresh = performBalanceRefresh(ctx, generation)
|
|
2107
2135
|
.catch((error) => {
|
|
2136
|
+
if (generation !== balanceGeneration) return
|
|
2108
2137
|
balance = {
|
|
2109
2138
|
fetchedAt: Date.now(),
|
|
2110
2139
|
available: false,
|
|
@@ -2116,7 +2145,7 @@ function refreshBalance(ctx) {
|
|
|
2116
2145
|
}
|
|
2117
2146
|
})
|
|
2118
2147
|
.finally(() => {
|
|
2119
|
-
balanceRefresh = null
|
|
2148
|
+
if (generation === balanceGeneration) balanceRefresh = null
|
|
2120
2149
|
})
|
|
2121
2150
|
return balanceRefresh
|
|
2122
2151
|
}
|
|
@@ -2668,10 +2697,12 @@ historyEventCount = store.history && store.history.events ? Object.keys(store.hi
|
|
|
2668
2697
|
if (accountStorageLocked) return json(res, 423, { ok: false, error: 'account-storage-locked' })
|
|
2669
2698
|
const body = await readBody(req)
|
|
2670
2699
|
if (body === null || typeof body.id !== 'string') return json(res, 400, { ok: false, error: 'id is required' })
|
|
2700
|
+
const wasActive = accounts.activeId === body.id
|
|
2671
2701
|
const result = removeAccount(body.id)
|
|
2672
2702
|
if (!result.ok) return json(res, 400, { ok: false, error: result.error })
|
|
2673
2703
|
scheduleAccountsSave(ctx.logger)
|
|
2674
2704
|
if (result.thresholdRemoved) scheduleSave(ctx.logger)
|
|
2705
|
+
if (wasActive) void refreshBalance(ctx)
|
|
2675
2706
|
return json(res, 200, { ok: true, ...accountListView() })
|
|
2676
2707
|
},
|
|
2677
2708
|
})
|