deepseek-harness-wallet 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +96 -0
- package/cordis.patch.yml +4 -0
- package/index.js +419 -0
- package/lib/client.js +469 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 feibi-mochi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# deepseek-harness-wallet
|
|
2
|
+
|
|
3
|
+
### DeepSeek Harness Balance Monitor & Recharge Plugin · 余额监控和充值插件
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="./README.md">English</a> ·
|
|
7
|
+
<a href="./docs/i18n/README.zh-CN.md">简体中文</a>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
<img alt="Version 0.1.2" src="https://img.shields.io/badge/version-0.1.2-5965d8">
|
|
12
|
+
<img alt="DeepSeek Harness rc.6" src="https://img.shields.io/badge/dsh-0.1.0--rc.6-4aa3ff">
|
|
13
|
+
<img alt="MIT License" src="https://img.shields.io/badge/license-MIT-3b7a57">
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
**The multi-provider wallet chip for the DeepSeek Harness Web GUI.**
|
|
17
|
+
|
|
18
|
+
A resident one-line chip beside the composer: official DeepSeek (balance, session cost, tokens, one-click recharge, low-balance alert) plus the third-party token total. Accounting is bucketed per provider — a GLM session never shows a DeepSeek balance, and DeepSeek costs are never computed from GLM tokens.
|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
If deepseek-harness-wallet helps you, please consider leaving a ⭐ Star. Thank you!
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
## What it shows
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
余额 5.89 · 本场 0.72 · 官 18.8M | 三方 800K · ↗充
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- **Official DeepSeek** — live balance (60s global refresh with fast boot retries), current-session cost (official pricing timeline, including the 2026-08-17 peak/off-peak rollout), and token breakdown.
|
|
31
|
+
- **Third-party total** — current-session tokens (input / cache read / output). No balance guessing, no cost math, zero configuration.
|
|
32
|
+
- **Click the chip** to open the detail panel: per-currency balance breakdown, cost and token splits, a freely editable low-balance threshold in CNY (two decimals, persisted globally; the chip compares the CNY balance, never a mixed-currency sum), manual refresh, and a jump to the official recharge page (first click shows the domain for confirmation — anti-phishing).
|
|
33
|
+
- **Floating window mode** — from the detail panel, detach the wallet into a floating window you can drag anywhere (position remembered across reloads), or minimize it to a small dot; the dot turns red below the threshold.
|
|
34
|
+
- **Low-balance alert** — below the threshold the chip turns red with a breathing animation and fires one desktop notification; it resets automatically once the balance recovers.
|
|
35
|
+
- **Theme-native UI** — built entirely on `--dsw-alias-*` theme variables, so light and dark themes both render correctly; the panel closes when you click outside and flips open-direction near screen edges.
|
|
36
|
+
- **Clear current session** — one button clears only the open conversation's token/cost records; every other conversation is untouched.
|
|
37
|
+
|
|
38
|
+
### Screenshots
|
|
39
|
+
|
|
40
|
+
| Floating window (draggable) | Minimized dot | Below threshold (alert on) | Above threshold (normal) |
|
|
41
|
+
| --- | --- | --- | --- |
|
|
42
|
+
| <img alt="Floating wallet window" src="docs/assets/floating-window.png" width="340"> | <img alt="Minimized floating dot" src="docs/assets/floating-dot.png" width="340"> | <img alt="Below threshold" src="docs/assets/below-threshold.png" width="340"> | <img alt="Above threshold" src="docs/assets/above-threshold.png" width="340"> |
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
dsh plugin --profile web add github:feibi-mochi/deepseek-harness-wallet
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Restart `dsh web`, then hard-refresh the page.
|
|
51
|
+
|
|
52
|
+
### Update
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
dsh plugin --profile web update deepseek-harness-wallet
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Remove
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
dsh plugin --profile web remove deepseek-harness-wallet
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
> The package was renamed from `dsh-wallet` to `deepseek-harness-wallet` in 0.1.1. If you installed the old name, remove it with `dsh plugin --profile web remove dsh-wallet` first.
|
|
65
|
+
|
|
66
|
+
## Data & trust
|
|
67
|
+
|
|
68
|
+
| Item | Behavior |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| Token accounting | Listens to the `llm/stream` event and buckets per provider (`deepseek-official` vs. everything else) and per session; multiple sessions never mix accounts. |
|
|
71
|
+
| Balance | The `DEEPSEEK_API_KEY` from the credentials seam never leaves this machine except as the `Authorization` header of the official `/user/balance` request. |
|
|
72
|
+
| Session log | The plugin writes no events; its data lives in `$DSH_HOME/storages/wallet.json`. |
|
|
73
|
+
| Model surface | No tools registered, no prompt injection, zero token cost. |
|
|
74
|
+
| Recharge | The URL is hardcoded to the official `https://platform.deepseek.com/top_up` and is not user-configurable (anti-phishing). |
|
|
75
|
+
|
|
76
|
+
## Pricing timeline
|
|
77
|
+
|
|
78
|
+
CNY per 1M tokens, curated from official announcements (cache writes are not billed):
|
|
79
|
+
|
|
80
|
+
- Since 2025-02-09 — deepseek-chat 2/8 (cache read 0.5), deepseek-reasoner 4/16 (cache read 1)
|
|
81
|
+
- Since 2026-04-24 — v4-flash 1/2 (cache read 0.02), v4-pro 3/6 (cache read 0.025)
|
|
82
|
+
- Since 2026-08-17 00:00 Beijing — peak/off-peak pricing for the v4 models (peak windows Beijing 09:00–12:00 / 14:00–18:00; off-peak is half the peak rate):
|
|
83
|
+
- v4-flash (off-peak / peak): cache read 0.05 / 0.10, input 1.5 / 3, output 4.5 / 9
|
|
84
|
+
- v4-pro (off-peak / peak): cache read 0.15 / 0.30, input 4.5 / 9, output 13.5 / 27
|
|
85
|
+
|
|
86
|
+
deepseek-chat and deepseek-reasoner keep their flat rates. Costs are estimates; the API-returned balance is authoritative.
|
|
87
|
+
|
|
88
|
+
## Roadmap
|
|
89
|
+
|
|
90
|
+
- [ ] Third-party price tables (cost per token)
|
|
91
|
+
- [ ] Balance history chart
|
|
92
|
+
- [ ] Balance-API adapters for other providers (e.g. Zhipu)
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
[MIT](LICENSE)
|
package/cordis.patch.yml
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-wallet host half.
|
|
3
|
+
*
|
|
4
|
+
* What it owns (all host-plane, shared by every session):
|
|
5
|
+
* - per-session, per-provider token accounting harvested from the 'llm/stream'
|
|
6
|
+
* waterfall (DeepSeek official bucket vs. every other provider bucket);
|
|
7
|
+
* - a global DeepSeek account balance cache (official Get User Balance
|
|
8
|
+
* endpoint, refreshed every 60s);
|
|
9
|
+
* - a global low-balance threshold, persisted under DSH_HOME storages;
|
|
10
|
+
* - the /api/wallet routes the browser chip polls.
|
|
11
|
+
*/
|
|
12
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
|
|
13
|
+
import { homedir } from 'node:os'
|
|
14
|
+
import { dirname, join } from 'node:path'
|
|
15
|
+
|
|
16
|
+
export const name = 'wallet'
|
|
17
|
+
export const inject = ['webServer']
|
|
18
|
+
|
|
19
|
+
const OFFICIAL_PROVIDER = 'deepseek-official'
|
|
20
|
+
const RECHARGE_URL = 'https://platform.deepseek.com/top_up'
|
|
21
|
+
const STORE_PATH = join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'storages', 'wallet.json')
|
|
22
|
+
const DEFAULT_THRESHOLD = 5
|
|
23
|
+
const BALANCE_REFRESH_MS = 60_000
|
|
24
|
+
|
|
25
|
+
// Beijing (UTC+8, no DST) peak windows: 09:00-12:00 and 14:00-18:00.
|
|
26
|
+
const BEIJING_OFFSET_MS = 8 * 3600_000
|
|
27
|
+
|
|
28
|
+
export function isBeijingPeak(atMs) {
|
|
29
|
+
const hours = ((atMs + BEIJING_OFFSET_MS) % 86_400_000) / 3_600_000
|
|
30
|
+
return (hours >= 9 && hours < 12) || (hours >= 14 && hours < 18)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Pricing timeline (CNY per 1M tokens; cacheWrite is not billed by DeepSeek).
|
|
34
|
+
// Curated from the official announcements; later policies win per model.
|
|
35
|
+
export const PRICE_POLICIES = [
|
|
36
|
+
{
|
|
37
|
+
since: Date.UTC(2025, 1, 9),
|
|
38
|
+
models: {
|
|
39
|
+
'deepseek-chat': { cacheHit: 0.5, input: 2, output: 8 },
|
|
40
|
+
'deepseek-reasoner': { cacheHit: 1, input: 4, output: 16 },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
since: Date.UTC(2026, 3, 24),
|
|
45
|
+
models: {
|
|
46
|
+
'deepseek-v4-flash': { cacheHit: 0.02, input: 1, output: 2 },
|
|
47
|
+
'deepseek-v4-pro': { cacheHit: 0.025, input: 3, output: 6 },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
// 2026-08-17 00:00 Beijing = 2026-08-16T16:00Z; off-peak is half the peak rate.
|
|
52
|
+
since: Date.UTC(2026, 7, 16, 16),
|
|
53
|
+
peakOffPeak: true,
|
|
54
|
+
models: {
|
|
55
|
+
'deepseek-v4-flash': { cacheHit: [0.05, 0.1], input: [1.5, 3], output: [4.5, 9] },
|
|
56
|
+
'deepseek-v4-pro': { cacheHit: [0.15, 0.3], input: [4.5, 9], output: [13.5, 27] },
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
export function ratesFor(model, atMs) {
|
|
62
|
+
let entry
|
|
63
|
+
for (const policy of PRICE_POLICIES) {
|
|
64
|
+
if (atMs >= policy.since && policy.models[model] !== undefined) entry = policy
|
|
65
|
+
}
|
|
66
|
+
if (entry === undefined) return null
|
|
67
|
+
const rates = entry.models[model]
|
|
68
|
+
if (entry.peakOffPeak === true) {
|
|
69
|
+
const peak = isBeijingPeak(atMs) ? 1 : 0
|
|
70
|
+
return { cacheHit: rates.cacheHit[peak], input: rates.input[peak], output: rates.output[peak] }
|
|
71
|
+
}
|
|
72
|
+
return rates
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function costOf(model, usage, atMs) {
|
|
76
|
+
const rates = ratesFor(model, atMs)
|
|
77
|
+
if (rates === null) return null
|
|
78
|
+
const input = (usage.inputTokens ?? usage.input ?? 0) * rates.input
|
|
79
|
+
const cacheRead = (usage.cacheReadTokens ?? usage.cacheRead ?? 0) * rates.cacheHit
|
|
80
|
+
const output = (usage.outputTokens ?? usage.output ?? 0) * rates.output
|
|
81
|
+
return (input + cacheRead + output) / 1e6
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function emptyCounters() {
|
|
85
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0 }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function emptyBucket() {
|
|
89
|
+
return { models: {} }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function normalizeThreshold(value) {
|
|
93
|
+
const parsed = Number.parseFloat(value)
|
|
94
|
+
if (!Number.isFinite(parsed)) return DEFAULT_THRESHOLD
|
|
95
|
+
return Math.min(100000, Math.max(0, Math.round(parsed * 100) / 100))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function loadStore() {
|
|
99
|
+
try {
|
|
100
|
+
const parsed = JSON.parse(readFileSync(STORE_PATH, 'utf8'))
|
|
101
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
102
|
+
return { threshold: DEFAULT_THRESHOLD, sessions: {} }
|
|
103
|
+
}
|
|
104
|
+
const sessions = parsed.sessions !== null && typeof parsed.sessions === 'object' && !Array.isArray(parsed.sessions)
|
|
105
|
+
? parsed.sessions
|
|
106
|
+
: {}
|
|
107
|
+
return { threshold: normalizeThreshold(parsed.threshold), sessions }
|
|
108
|
+
} catch {
|
|
109
|
+
return { threshold: DEFAULT_THRESHOLD, sessions: {} }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let saveTimer = null
|
|
114
|
+
let store = loadStore()
|
|
115
|
+
|
|
116
|
+
function scheduleSave(logger) {
|
|
117
|
+
if (saveTimer !== null) return
|
|
118
|
+
saveTimer = setTimeout(() => {
|
|
119
|
+
saveTimer = null
|
|
120
|
+
try {
|
|
121
|
+
mkdirSync(dirname(STORE_PATH), { recursive: true })
|
|
122
|
+
const tmp = STORE_PATH + '.tmp'
|
|
123
|
+
writeFileSync(tmp, JSON.stringify(store))
|
|
124
|
+
renameSync(tmp, STORE_PATH)
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (logger && typeof logger.warn === 'function') {
|
|
127
|
+
logger.warn('dsh-wallet: failed to persist store: ' + String(error))
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}, 500)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function addUsage(counters, usage) {
|
|
134
|
+
counters.input += usage.inputTokens ?? 0
|
|
135
|
+
counters.output += usage.outputTokens ?? 0
|
|
136
|
+
counters.cacheRead += usage.cacheReadTokens ?? 0
|
|
137
|
+
counters.cacheWrite += usage.cacheWriteTokens ?? 0
|
|
138
|
+
counters.reasoning += usage.reasoningTokens ?? 0
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function bucketTotals(bucket) {
|
|
142
|
+
const totals = emptyCounters()
|
|
143
|
+
for (const counters of Object.values(bucket.models)) {
|
|
144
|
+
totals.input += counters.input
|
|
145
|
+
totals.output += counters.output
|
|
146
|
+
totals.cacheRead += counters.cacheRead
|
|
147
|
+
totals.cacheWrite += counters.cacheWrite
|
|
148
|
+
totals.reasoning += counters.reasoning
|
|
149
|
+
}
|
|
150
|
+
return totals
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function recordUsage(options, usage) {
|
|
154
|
+
const sessionId = options.sessionId
|
|
155
|
+
const provider = options.provider
|
|
156
|
+
const model = options.model ?? options.modelName ?? ''
|
|
157
|
+
if (sessionId === undefined || provider === undefined || provider === '') return
|
|
158
|
+
if (store.sessions[sessionId] === undefined) {
|
|
159
|
+
store.sessions[sessionId] = { official: emptyBucket(), third: emptyBucket() }
|
|
160
|
+
}
|
|
161
|
+
const session = store.sessions[sessionId]
|
|
162
|
+
const bucket = provider === OFFICIAL_PROVIDER ? session.official : session.third
|
|
163
|
+
if (bucket.models[model] === undefined) {
|
|
164
|
+
bucket.models[model] = emptyCounters()
|
|
165
|
+
}
|
|
166
|
+
addUsage(bucket.models[model], usage)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let balance = { fetchedAt: 0, available: false, balances: [], error: null }
|
|
170
|
+
|
|
171
|
+
async function refreshBalance(ctx) {
|
|
172
|
+
const credentials = ctx.get('credentials')
|
|
173
|
+
if (credentials === undefined) {
|
|
174
|
+
balance = { fetchedAt: Date.now(), available: false, balances: [], error: 'no credentials service' }
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const resolved = await credentials.resolve('DEEPSEEK_API_KEY')
|
|
179
|
+
const key = resolved ? resolved.value : undefined
|
|
180
|
+
if (key === undefined || key === '') {
|
|
181
|
+
balance = { fetchedAt: Date.now(), available: false, balances: [], error: 'no API key' }
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
const controller = new AbortController()
|
|
185
|
+
const timer = setTimeout(() => controller.abort(), 15000)
|
|
186
|
+
try {
|
|
187
|
+
const response = await fetch('https://api.deepseek.com/user/balance', {
|
|
188
|
+
method: 'GET',
|
|
189
|
+
headers: { authorization: 'Bearer ' + key },
|
|
190
|
+
signal: controller.signal,
|
|
191
|
+
})
|
|
192
|
+
if (!response.ok) throw new Error('status ' + response.status)
|
|
193
|
+
const data = await response.json()
|
|
194
|
+
balance = {
|
|
195
|
+
fetchedAt: Date.now(),
|
|
196
|
+
available: data.is_available === true,
|
|
197
|
+
balances: Array.isArray(data.balance_infos) ? data.balance_infos : [],
|
|
198
|
+
error: null,
|
|
199
|
+
}
|
|
200
|
+
} finally {
|
|
201
|
+
clearTimeout(timer)
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
balance = {
|
|
205
|
+
fetchedAt: Date.now(),
|
|
206
|
+
available: false,
|
|
207
|
+
balances: [],
|
|
208
|
+
error: error instanceof Error ? error.message : String(error),
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function sumBalances(infos) {
|
|
214
|
+
if (!Array.isArray(infos) || infos.length === 0) return 0
|
|
215
|
+
// The chip and the threshold are denominated in CNY: prefer the CNY record
|
|
216
|
+
// instead of mixing it with e.g. a USD record for international accounts.
|
|
217
|
+
const cny = infos.find((info) => info.currency === 'CNY')
|
|
218
|
+
if (cny !== undefined) {
|
|
219
|
+
const value = Number.parseFloat(cny.total_balance)
|
|
220
|
+
if (Number.isFinite(value)) return value
|
|
221
|
+
}
|
|
222
|
+
// No (finite) CNY record: sum only when every record shares one currency.
|
|
223
|
+
if (!infos.every((info) => info.currency === infos[0].currency)) {
|
|
224
|
+
for (const info of infos) {
|
|
225
|
+
const value = Number.parseFloat(info.total_balance)
|
|
226
|
+
if (Number.isFinite(value)) return value
|
|
227
|
+
}
|
|
228
|
+
return 0
|
|
229
|
+
}
|
|
230
|
+
let total = 0
|
|
231
|
+
for (const info of infos) {
|
|
232
|
+
const value = Number.parseFloat(info.total_balance)
|
|
233
|
+
if (Number.isFinite(value)) total += value
|
|
234
|
+
}
|
|
235
|
+
return total
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function balanceTotal() {
|
|
239
|
+
return sumBalances(balance.balances)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function sessionView(sessionId) {
|
|
243
|
+
if (sessionId === undefined || sessionId === '') return { official: null, third: null }
|
|
244
|
+
const session = store.sessions[sessionId]
|
|
245
|
+
if (session === undefined) return { official: null, third: null }
|
|
246
|
+
const official = bucketTotals(session.official)
|
|
247
|
+
const third = bucketTotals(session.third)
|
|
248
|
+
const now = Date.now()
|
|
249
|
+
let cost = 0
|
|
250
|
+
let priced = true
|
|
251
|
+
for (const model of Object.keys(session.official.models)) {
|
|
252
|
+
const value = costOf(model, session.official.models[model], now)
|
|
253
|
+
if (value === null) priced = false
|
|
254
|
+
else cost += value
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
official: { tokens: official, cost: priced ? cost : null, models: session.official.models },
|
|
258
|
+
third: { tokens: third, models: session.third.models },
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function snapshotView(sessionId, threshold) {
|
|
263
|
+
return {
|
|
264
|
+
ok: true,
|
|
265
|
+
balance: {
|
|
266
|
+
available: balance.available,
|
|
267
|
+
fetchedAt: balance.fetchedAt,
|
|
268
|
+
balances: balance.balances,
|
|
269
|
+
total: balance.available ? balanceTotal() : null,
|
|
270
|
+
error: balance.available ? null : balance.error,
|
|
271
|
+
},
|
|
272
|
+
session: sessionView(sessionId),
|
|
273
|
+
threshold,
|
|
274
|
+
lowBalance: balance.available && threshold > 0 && balanceTotal() < threshold,
|
|
275
|
+
rechargeUrl: RECHARGE_URL,
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function json(res, status, body) {
|
|
280
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
|
|
281
|
+
res.end(JSON.stringify(body))
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function readBody(req, cap) {
|
|
285
|
+
cap = cap || 4096
|
|
286
|
+
return new Promise((resolve) => {
|
|
287
|
+
let size = 0
|
|
288
|
+
const parts = []
|
|
289
|
+
req.on('data', (chunk) => {
|
|
290
|
+
size += chunk.length
|
|
291
|
+
if (size <= cap) parts.push(chunk)
|
|
292
|
+
})
|
|
293
|
+
req.on('end', () => {
|
|
294
|
+
try {
|
|
295
|
+
resolve(JSON.parse(Buffer.concat(parts).toString('utf8') || '{}'))
|
|
296
|
+
} catch {
|
|
297
|
+
resolve(null)
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
req.on('error', () => resolve(null))
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function sessionParam(req) {
|
|
305
|
+
const raw = req.url || ''
|
|
306
|
+
const query = raw.indexOf('?')
|
|
307
|
+
if (query < 0) return undefined
|
|
308
|
+
const value = new URLSearchParams(raw.slice(query + 1)).get('session')
|
|
309
|
+
return value === null || value === '' ? undefined : value
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function apply(ctx, config) {
|
|
313
|
+
config = config || {}
|
|
314
|
+
// Threshold lives ONLY in the persisted store; an explicit row config may
|
|
315
|
+
// still override it for power users, but the bundle patch sets none.
|
|
316
|
+
let threshold = store.threshold
|
|
317
|
+
if (Number.isFinite(config.threshold)) {
|
|
318
|
+
threshold = normalizeThreshold(config.threshold)
|
|
319
|
+
store.threshold = threshold
|
|
320
|
+
scheduleSave(ctx.logger)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const usageTap = (options, next) => {
|
|
324
|
+
const downstream = next()
|
|
325
|
+
return (async function* () {
|
|
326
|
+
let usage = null
|
|
327
|
+
try {
|
|
328
|
+
for await (const chunk of downstream) {
|
|
329
|
+
if (chunk !== null && chunk !== undefined && chunk.type === 'usage' && chunk.usage !== undefined) {
|
|
330
|
+
usage = chunk.usage
|
|
331
|
+
}
|
|
332
|
+
yield chunk
|
|
333
|
+
}
|
|
334
|
+
} finally {
|
|
335
|
+
if (usage !== null) {
|
|
336
|
+
recordUsage(options, usage)
|
|
337
|
+
scheduleSave(ctx.logger)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
})()
|
|
341
|
+
}
|
|
342
|
+
ctx.on('llm/stream', usageTap, { global: true })
|
|
343
|
+
|
|
344
|
+
void refreshBalance(ctx)
|
|
345
|
+
// Staggered boot retries: the credentials seam or network may not be ready
|
|
346
|
+
// at first mount; retry a few times so the chip shows a value without
|
|
347
|
+
// waiting for the 60s cadence (or for the user to send a message).
|
|
348
|
+
ctx.effect(() => {
|
|
349
|
+
const retries = [2000, 6000, 15000, 30000].map((delay) => {
|
|
350
|
+
const retry = setTimeout(() => {
|
|
351
|
+
if (!balance.available) void refreshBalance(ctx)
|
|
352
|
+
}, delay)
|
|
353
|
+
if (typeof retry.unref === 'function') retry.unref()
|
|
354
|
+
return retry
|
|
355
|
+
})
|
|
356
|
+
return () => {
|
|
357
|
+
for (const retry of retries) clearTimeout(retry)
|
|
358
|
+
}
|
|
359
|
+
}, 'dsh-wallet: boot balance retries')
|
|
360
|
+
const balanceTimer = setInterval(() => void refreshBalance(ctx), BALANCE_REFRESH_MS)
|
|
361
|
+
if (typeof balanceTimer.unref === 'function') balanceTimer.unref()
|
|
362
|
+
|
|
363
|
+
ctx.effect(() => {
|
|
364
|
+
const disposeSnapshot = ctx.webServer.register({
|
|
365
|
+
kind: 'exact',
|
|
366
|
+
path: '/api/wallet/snapshot',
|
|
367
|
+
handler: (req, res) => {
|
|
368
|
+
if (req.method !== 'GET') return json(res, 405, { ok: false, error: 'method-not-allowed' })
|
|
369
|
+
return json(res, 200, snapshotView(sessionParam(req), threshold))
|
|
370
|
+
},
|
|
371
|
+
})
|
|
372
|
+
const disposeThreshold = ctx.webServer.register({
|
|
373
|
+
kind: 'exact',
|
|
374
|
+
path: '/api/wallet/threshold',
|
|
375
|
+
handler: async (req, res) => {
|
|
376
|
+
if (req.method !== 'POST') return json(res, 405, { ok: false, error: 'method-not-allowed' })
|
|
377
|
+
const body = await readBody(req)
|
|
378
|
+
if (body === null || typeof body.threshold !== 'number' || !Number.isFinite(body.threshold)) {
|
|
379
|
+
return json(res, 400, { ok: false, error: 'threshold must be a number' })
|
|
380
|
+
}
|
|
381
|
+
threshold = Math.min(100000, Math.max(0, Math.round(body.threshold * 100) / 100))
|
|
382
|
+
store.threshold = threshold
|
|
383
|
+
scheduleSave(ctx.logger)
|
|
384
|
+
return json(res, 200, { ok: true, threshold: threshold })
|
|
385
|
+
},
|
|
386
|
+
})
|
|
387
|
+
const disposeRefresh = ctx.webServer.register({
|
|
388
|
+
kind: 'exact',
|
|
389
|
+
path: '/api/wallet/refresh',
|
|
390
|
+
handler: (req, res) => {
|
|
391
|
+
if (req.method !== 'POST') return json(res, 405, { ok: false, error: 'method-not-allowed' })
|
|
392
|
+
void refreshBalance(ctx).then(() => json(res, 200, { ok: true, fetchedAt: balance.fetchedAt }))
|
|
393
|
+
},
|
|
394
|
+
})
|
|
395
|
+
const disposeClear = ctx.webServer.register({
|
|
396
|
+
kind: 'exact',
|
|
397
|
+
path: '/api/wallet/clear-session',
|
|
398
|
+
handler: async (req, res) => {
|
|
399
|
+
if (req.method !== 'POST') return json(res, 405, { ok: false, error: 'method-not-allowed' })
|
|
400
|
+
const body = await readBody(req)
|
|
401
|
+
const sid = body && body.session
|
|
402
|
+
if (sid && store.sessions[sid]) {
|
|
403
|
+
delete store.sessions[sid]
|
|
404
|
+
scheduleSave(ctx.logger)
|
|
405
|
+
}
|
|
406
|
+
return json(res, 200, { ok: true })
|
|
407
|
+
},
|
|
408
|
+
})
|
|
409
|
+
return () => {
|
|
410
|
+
disposeSnapshot()
|
|
411
|
+
disposeThreshold()
|
|
412
|
+
disposeRefresh()
|
|
413
|
+
disposeClear()
|
|
414
|
+
clearInterval(balanceTimer)
|
|
415
|
+
clearTimeout(saveTimer)
|
|
416
|
+
saveTimer = null
|
|
417
|
+
}
|
|
418
|
+
}, 'dsh-wallet: routes')
|
|
419
|
+
}
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/* deepseek-harness-wallet client half: the composer-dock chip.
|
|
2
|
+
* Loaded through the client module loader (CJS wrapper). The loader id MUST
|
|
3
|
+
* equal the package name: client-modules verifies the boot graph row id
|
|
4
|
+
* (the package name) against the id registered via __ModuleLoader__.load. */
|
|
5
|
+
window.__ModuleLoader__.load({
|
|
6
|
+
id: 'deepseek-harness-wallet',
|
|
7
|
+
factory: (require) => {
|
|
8
|
+
var module = { exports: {} }
|
|
9
|
+
var exports = module.exports
|
|
10
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
11
|
+
var React = require('react')
|
|
12
|
+
|
|
13
|
+
var POLL_MS = 15000
|
|
14
|
+
var CONFIRM_KEY = 'dsh-wallet-recharge-confirmed'
|
|
15
|
+
|
|
16
|
+
var css = [
|
|
17
|
+
'.dshw_chip{border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.15));background:transparent;height:22px;color:var(--dsw-alias-label-primary,#1f2328);cursor:pointer;white-space:nowrap;border-radius:999px;align-items:center;gap:6px;padding:0 8px;font-size:12px;line-height:1;display:inline-flex;position:relative}',
|
|
18
|
+
'.dshw_chip:hover{border-color:var(--dsw-alias-brand-primary,#4aa3ff)}',
|
|
19
|
+
'.dshw_chipLow{border-color:var(--dsw-alias-state-error-primary,#e5534b);color:var(--dsw-alias-state-error-primary,#e5534b);animation:dshwPulse 1.8s infinite}',
|
|
20
|
+
'@keyframes dshwPulse{0%,100%{box-shadow:0 0 0 0 rgba(229,83,75,.45)}50%{box-shadow:0 0 0 5px rgba(229,83,75,0)}}',
|
|
21
|
+
'.dshw_sep{opacity:.45}',
|
|
22
|
+
'.dshw_recharge{color:var(--dsw-alias-brand-primary,#4aa3ff);text-decoration:none;border-left:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.15));padding-left:6px}',
|
|
23
|
+
'.dshw_panel{z-index:40;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.15));background:var(--dsw-alias-bg-overlay,#ffffff);min-width:240px;max-width:300px;color:var(--dsw-alias-label-primary,#1f2328);border-radius:8px;flex-direction:column;gap:6px;padding:10px 12px;font-size:12px;display:flex;position:absolute;top:calc(100% + 6px);right:0;box-shadow:0 4px 12px #0000004d}',
|
|
24
|
+
'.dshw_row{justify-content:space-between;align-items:center;gap:10px;display:flex}',
|
|
25
|
+
'.dshw_title{font-weight:600;opacity:.9}',
|
|
26
|
+
'.dshw_muted{color:var(--dsw-alias-label-tertiary,#6b7280)}',
|
|
27
|
+
'.dshw_divider{border-top:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));margin-top:2px;padding-top:6px}',
|
|
28
|
+
'.dshw_input{background:var(--dsw-alias-bg-layer-1,#ffffff);border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.15));color:var(--dsw-alias-label-primary,#1f2328);border-radius:5px;padding:3px 6px;width:56px;font-size:12px}',
|
|
29
|
+
'.dshw_btn{border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.15));background:transparent;color:var(--dsw-alias-label-primary,#1f2328);border-radius:5px;padding:3px 8px;font-size:12px;cursor:pointer}',
|
|
30
|
+
'.dshw_btnPrimary{background:var(--dsw-alias-brand-primary,#4aa3ff);border-color:transparent;color:var(--dsw-alias-label-primary-foreground,#ffffff)}',
|
|
31
|
+
'.dshw_overlay{position:fixed;inset:0;background:var(--dsw-alias-bg-mask-drop,rgba(0,0,0,.55));display:flex;align-items:center;justify-content:center;z-index:60}',
|
|
32
|
+
'.dshw_overlayBox{background:var(--dsw-alias-bg-overlay,#ffffff);border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.15));color:var(--dsw-alias-label-primary,#1f2328);border-radius:8px;padding:14px 18px;max-width:320px;font-size:13px;line-height:1.7}',
|
|
33
|
+
'.dshw_overlayRow{margin-top:10px;display:flex;gap:8px;justify-content:flex-end}',
|
|
34
|
+
|
|
35
|
+
'.dshw_float{position:fixed;z-index:80;min-width:230px;max-width:280px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.15));background:var(--dsw-alias-bg-overlay,#fff);color:var(--dsw-alias-label-primary,#1f2328);border-radius:12px;box-shadow:0 6px 20px rgba(0,0,0,.18);display:flex;flex-direction:column;gap:6px;padding:10px 12px;font-size:12px;user-select:none}',
|
|
36
|
+
'.dshw_floatHeader{display:flex;align-items:center;justify-content:space-between;cursor:move;font-weight:600;opacity:.9;touch-action:none}',
|
|
37
|
+
'.dshw_floatBtn{border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.15));background:transparent;color:inherit;border-radius:5px;width:22px;height:20px;font-size:12px;line-height:1;cursor:pointer;padding:0}',
|
|
38
|
+
'.dshw_floatBtn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.06))}',
|
|
39
|
+
'.dshw_dot{position:fixed;z-index:80;width:36px;height:36px;border-radius:50%;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.15));background:var(--dsw-alias-bg-overlay,#fff);color:var(--dsw-alias-label-primary,#1f2328);font-size:11px;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 2px 10px rgba(0,0,0,.2);user-select:none;touch-action:none}',
|
|
40
|
+
'.dshw_dotLow{border-color:var(--dsw-alias-state-error-primary,#e5534b);color:var(--dsw-alias-state-error-primary,#e5534b)}'
|
|
41
|
+
].join('')
|
|
42
|
+
|
|
43
|
+
if (typeof document !== 'undefined' && document.querySelector('style[data-plugin="deepseek-harness-wallet"]') === null) {
|
|
44
|
+
var tag = document.createElement('style')
|
|
45
|
+
tag.dataset.plugin = 'deepseek-harness-wallet'
|
|
46
|
+
tag.textContent = css
|
|
47
|
+
document.head.appendChild(tag)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function fmtTokens(n) {
|
|
51
|
+
n = n || 0
|
|
52
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1).replace(/\.0$/, '') + 'M'
|
|
53
|
+
if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'K'
|
|
54
|
+
return String(n)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function fmtMoney(n) {
|
|
58
|
+
if (n === null || n === undefined || Number.isNaN(n)) return '--'
|
|
59
|
+
return n.toFixed(2)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function totalTokens(counters) {
|
|
63
|
+
if (!counters) return 0
|
|
64
|
+
return (counters.input || 0) + (counters.output || 0) + (counters.cacheRead || 0) + (counters.cacheWrite || 0)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function WalletChip(props) {
|
|
68
|
+
props = props || {}
|
|
69
|
+
var sessionId = props.sessionId
|
|
70
|
+
var dataRef = React.useRef(null)
|
|
71
|
+
var notifiedRef = React.useRef(false)
|
|
72
|
+
var chipRef = React.useRef(null)
|
|
73
|
+
var panelRef = React.useRef(null)
|
|
74
|
+
var [alignLeft, setAlignLeft] = React.useState(false)
|
|
75
|
+
var [data, setData] = React.useState(null)
|
|
76
|
+
var [open, setOpen] = React.useState(false)
|
|
77
|
+
var [thresholdDraft, setThresholdDraft] = React.useState(null)
|
|
78
|
+
var [confirming, setConfirming] = React.useState(false)
|
|
79
|
+
var [floated, setFloated] = React.useState(function () {
|
|
80
|
+
try { return window.localStorage.getItem('dshw-float-mode') === 'float' || window.localStorage.getItem('dshw-float-mode') === 'dot' } catch (e) { return false }
|
|
81
|
+
})
|
|
82
|
+
var [minimized, setMinimized] = React.useState(function () {
|
|
83
|
+
try { return window.localStorage.getItem('dshw-float-mode') === 'dot' } catch (e) { return false }
|
|
84
|
+
})
|
|
85
|
+
var [floatPos, setFloatPos] = React.useState(null)
|
|
86
|
+
var floatRef = React.useRef(null)
|
|
87
|
+
var dragRef = React.useRef(null)
|
|
88
|
+
var didDragRef = React.useRef(false)
|
|
89
|
+
|
|
90
|
+
React.useEffect(function () {
|
|
91
|
+
var alive = true
|
|
92
|
+
function tick() {
|
|
93
|
+
var url = '/api/wallet/snapshot'
|
|
94
|
+
if (sessionId) url = url + '?session=' + encodeURIComponent(sessionId)
|
|
95
|
+
fetch(url).then(function (resp) {
|
|
96
|
+
if (!resp.ok) throw new Error(String(resp.status))
|
|
97
|
+
return resp.json()
|
|
98
|
+
}).then(function (json) {
|
|
99
|
+
if (!alive) return
|
|
100
|
+
dataRef.current = json
|
|
101
|
+
setData(json)
|
|
102
|
+
if (thresholdDraft === null && json.threshold !== undefined && json.threshold !== null) {
|
|
103
|
+
setThresholdDraft(json.threshold.toFixed(2))
|
|
104
|
+
}
|
|
105
|
+
if (json.lowBalance) {
|
|
106
|
+
if (!notifiedRef.current && typeof Notification !== 'undefined' && Notification.permission === 'granted') {
|
|
107
|
+
notifiedRef.current = true
|
|
108
|
+
try {
|
|
109
|
+
new Notification('DeepSeek \u4f59\u989d\u4e0d\u8db3', { body: '\u4f59\u989d\u5df2\u4f4e\u4e8e\u63d0\u9192\u7ebf ' + json.threshold + ' \u5143', tag: 'dsh-wallet-low' })
|
|
110
|
+
} catch (e) { /* ignore */ }
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
notifiedRef.current = false
|
|
114
|
+
}
|
|
115
|
+
}).catch(function () {
|
|
116
|
+
if (alive) setData({ ok: false })
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
tick()
|
|
120
|
+
var timer = setInterval(tick, POLL_MS)
|
|
121
|
+
return function () { alive = false; clearInterval(timer) }
|
|
122
|
+
}, [])
|
|
123
|
+
React.useEffect(function () {
|
|
124
|
+
function onMove(event) {
|
|
125
|
+
var d = dragRef.current
|
|
126
|
+
if (d === null) return
|
|
127
|
+
didDragRef.current = true
|
|
128
|
+
var x = Math.max(4, Math.min(window.innerWidth - d.w - 4, event.clientX - d.dx))
|
|
129
|
+
var y = Math.max(4, Math.min(window.innerHeight - d.h - 4, event.clientY - d.dy))
|
|
130
|
+
setFloatPos({ x: x, y: y })
|
|
131
|
+
}
|
|
132
|
+
function onUp() {
|
|
133
|
+
if (dragRef.current === null) return
|
|
134
|
+
dragRef.current = null
|
|
135
|
+
try { window.localStorage.setItem('dshw-float-pos', floatPos.x + ',' + floatPos.y) } catch (e) { /* ignore */ }
|
|
136
|
+
}
|
|
137
|
+
document.addEventListener('mousemove', onMove)
|
|
138
|
+
document.addEventListener('mouseup', onUp)
|
|
139
|
+
return function () {
|
|
140
|
+
document.removeEventListener('mousemove', onMove)
|
|
141
|
+
document.removeEventListener('mouseup', onUp)
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
React.useEffect(function () {
|
|
146
|
+
if (!open) return
|
|
147
|
+
function onDocDown(event) {
|
|
148
|
+
var t = event.target
|
|
149
|
+
if (chipRef.current && chipRef.current.contains(t)) return
|
|
150
|
+
if (panelRef.current && panelRef.current.contains(t)) return
|
|
151
|
+
setOpen(false)
|
|
152
|
+
}
|
|
153
|
+
document.addEventListener('mousedown', onDocDown)
|
|
154
|
+
return function () { document.removeEventListener('mousedown', onDocDown) }
|
|
155
|
+
}, [open])
|
|
156
|
+
|
|
157
|
+
function readSavedPos() {
|
|
158
|
+
try {
|
|
159
|
+
var raw = window.localStorage.getItem('dshw-float-pos')
|
|
160
|
+
if (raw === null) return null
|
|
161
|
+
var parts = raw.split(',')
|
|
162
|
+
var x = Number.parseFloat(parts[0]); var y = Number.parseFloat(parts[1])
|
|
163
|
+
if (Number.isFinite(x) && Number.isFinite(y)) {
|
|
164
|
+
var maxX = Math.max(4, window.innerWidth - 300)
|
|
165
|
+
var maxY = Math.max(4, window.innerHeight - 40)
|
|
166
|
+
return { x: Math.min(Math.max(4, x), maxX), y: Math.min(Math.max(4, y), maxY) }
|
|
167
|
+
}
|
|
168
|
+
} catch (e) { /* ignore */ }
|
|
169
|
+
return null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function onFloatDown(event) {
|
|
173
|
+
if (event.button !== 0) return
|
|
174
|
+
var target = event.target
|
|
175
|
+
if (target && target.closest && target.closest('button,input,a')) return
|
|
176
|
+
var node = floatRef.current
|
|
177
|
+
if (node === null) return
|
|
178
|
+
var rect = node.getBoundingClientRect()
|
|
179
|
+
dragRef.current = { dx: event.clientX - rect.left, dy: event.clientY - rect.top, w: rect.width, h: rect.height }
|
|
180
|
+
didDragRef.current = false
|
|
181
|
+
event.preventDefault()
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
React.useEffect(function () {
|
|
185
|
+
try {
|
|
186
|
+
var mode = 'chip'
|
|
187
|
+
if (floated) mode = minimized ? 'dot' : 'float'
|
|
188
|
+
window.localStorage.setItem('dshw-float-mode', mode)
|
|
189
|
+
} catch (e) { /* ignore */ }
|
|
190
|
+
}, [floated, minimized])
|
|
191
|
+
|
|
192
|
+
function requestNotify() {
|
|
193
|
+
if (typeof Notification !== 'undefined' && Notification.permission === 'default') {
|
|
194
|
+
try { Notification.requestPermission() } catch (e) { /* ignore */ }
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function saveThreshold() {
|
|
199
|
+
var value = Number.parseFloat(thresholdDraft)
|
|
200
|
+
if (!Number.isFinite(value)) return
|
|
201
|
+
value = Math.min(100000, Math.max(0, Math.round(value * 100) / 100))
|
|
202
|
+
fetch('/api/wallet/threshold', {
|
|
203
|
+
method: 'POST',
|
|
204
|
+
headers: { 'content-type': 'application/json' },
|
|
205
|
+
body: JSON.stringify({ threshold: value })
|
|
206
|
+
}).then(function (resp) { return resp.json() }).then(function (json) {
|
|
207
|
+
if (json && json.ok) {
|
|
208
|
+
setThresholdDraft(json.threshold.toFixed(2))
|
|
209
|
+
if (dataRef.current) { dataRef.current.threshold = json.threshold; setData(Object.assign({}, dataRef.current)) }
|
|
210
|
+
}
|
|
211
|
+
}).catch(function () { /* ignore */ })
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function clearSession() {
|
|
215
|
+
if (!sessionId) return
|
|
216
|
+
fetch('/api/wallet/clear-session', {
|
|
217
|
+
method: 'POST',
|
|
218
|
+
headers: { 'content-type': 'application/json' },
|
|
219
|
+
body: JSON.stringify({ session: sessionId })
|
|
220
|
+
}).then(function () {
|
|
221
|
+
var url = '/api/wallet/snapshot'
|
|
222
|
+
if (sessionId) url = url + '?session=' + encodeURIComponent(sessionId)
|
|
223
|
+
fetch(url).then(function (r) { return r.json() }).then(function (json) { setData(json) }).catch(function () {})
|
|
224
|
+
}).catch(function () {})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function refreshBalance() {
|
|
228
|
+
fetch('/api/wallet/refresh', { method: 'POST' }).then(function () {
|
|
229
|
+
setTimeout(function () {
|
|
230
|
+
var url = '/api/wallet/snapshot'
|
|
231
|
+
if (sessionId) url = url + '?session=' + encodeURIComponent(sessionId)
|
|
232
|
+
fetch(url).then(function (r2) { return r2.json() }).then(function (json) { setData(json) }).catch(function () { /* ignore */ })
|
|
233
|
+
}, 300)
|
|
234
|
+
}).catch(function () { /* ignore */ })
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function goRecharge() {
|
|
238
|
+
setConfirming(false)
|
|
239
|
+
try { window.localStorage.setItem(CONFIRM_KEY, '1') } catch (e) { /* ignore */ }
|
|
240
|
+
var url = data && data.rechargeUrl ? data.rechargeUrl : 'https://platform.deepseek.com/top_up'
|
|
241
|
+
window.open(url, '_blank', 'noopener,noreferrer')
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function onRechargeClick(event) {
|
|
245
|
+
if (event && event.stopPropagation) event.stopPropagation()
|
|
246
|
+
if (event && event.preventDefault) event.preventDefault()
|
|
247
|
+
var confirmed = false
|
|
248
|
+
try { confirmed = window.localStorage.getItem(CONFIRM_KEY) === '1' } catch (e) { /* ignore */ }
|
|
249
|
+
if (confirmed) goRecharge()
|
|
250
|
+
else setConfirming(true)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
var snapshot = data || {}
|
|
254
|
+
var bal = snapshot.balance || {}
|
|
255
|
+
var session = snapshot.session || {}
|
|
256
|
+
var official = session.official || {}
|
|
257
|
+
var third = session.third || {}
|
|
258
|
+
var officialTokens = official.tokens ? totalTokens(official.tokens) : 0
|
|
259
|
+
var thirdTokens = third.tokens ? totalTokens(third.tokens) : 0
|
|
260
|
+
var low = snapshot.lowBalance === true
|
|
261
|
+
|
|
262
|
+
var chipTextParts = []
|
|
263
|
+
chipTextParts.push(React.createElement('span', { key: 'bal' }, '余额 ' + (bal.total === null || bal.total === undefined ? '--' : fmtMoney(bal.total))))
|
|
264
|
+
if (official.cost !== undefined && official.cost !== null) {
|
|
265
|
+
chipTextParts.push(React.createElement('span', { key: 'cost' }, '\u672c\u573a ' + fmtMoney(official.cost)))
|
|
266
|
+
}
|
|
267
|
+
chipTextParts.push(React.createElement('span', { key: 'off' }, '\u5b98 ' + fmtTokens(officialTokens)))
|
|
268
|
+
chipTextParts.push(React.createElement('span', { key: 'sep' }, '|'))
|
|
269
|
+
chipTextParts.push(React.createElement('span', { key: 'third' }, '\u4e09\u65b9 ' + fmtTokens(thirdTokens)))
|
|
270
|
+
chipTextParts.push(React.createElement('a', {
|
|
271
|
+
key: 'recharge',
|
|
272
|
+
className: 'dshw_recharge',
|
|
273
|
+
href: snapshot.rechargeUrl || 'https://platform.deepseek.com/top_up',
|
|
274
|
+
title: 'DeepSeek \u5f00\u653e\u5e73\u53f0 \u00b7 \u5b98\u65b9\u5145\u503c\u9875',
|
|
275
|
+
onClick: onRechargeClick
|
|
276
|
+
}, '\u2197\u5145'))
|
|
277
|
+
|
|
278
|
+
var chip = React.createElement('span', {
|
|
279
|
+
ref: chipRef,
|
|
280
|
+
className: low ? 'dshw_chip dshw_chipLow' : 'dshw_chip',
|
|
281
|
+
title: 'deepseek-harness-wallet \u00b7 \u70b9\u51fb\u67e5\u770b\u660e\u7ec6',
|
|
282
|
+
onClick: function (event) {
|
|
283
|
+
requestNotify()
|
|
284
|
+
if (floated) { setFloated(false); setOpen(true); return }
|
|
285
|
+
if (!open && chipRef.current) {
|
|
286
|
+
var rect = chipRef.current.getBoundingClientRect()
|
|
287
|
+
setAlignLeft(rect.left > rect.right - rect.left + 200)
|
|
288
|
+
}
|
|
289
|
+
setOpen(!open)
|
|
290
|
+
}
|
|
291
|
+
}, chipTextParts)
|
|
292
|
+
|
|
293
|
+
if (!open && !floated) return chip
|
|
294
|
+
|
|
295
|
+
var balanceRows = []
|
|
296
|
+
if (bal.available && bal.balances && bal.balances.length > 0) {
|
|
297
|
+
bal.balances.forEach(function (info) {
|
|
298
|
+
balanceRows.push(React.createElement('div', { key: info.currency, className: 'dshw_row' },
|
|
299
|
+
React.createElement('span', { className: 'dshw_muted' }, '\u4f59\u989d (' + info.currency + ')'),
|
|
300
|
+
React.createElement('span', null, '\u00a5' + String(info.total_balance) + ' (\u5145\u503c ' + String(info.topped_up_balance) + ' / \u8d60\u9001 ' + String(info.granted_balance) + ')')
|
|
301
|
+
))
|
|
302
|
+
})
|
|
303
|
+
} else {
|
|
304
|
+
balanceRows.push(React.createElement('div', { key: 'no-bal', className: 'dshw_row dshw_muted' }, bal.error ? ('\u4f59\u989d\u4e0d\u53ef\u7528: ' + bal.error) : '\u4f59\u989d\u67e5\u8be2\u4e2d\u2026'))
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
var officialRows = []
|
|
308
|
+
if (official.tokens) {
|
|
309
|
+
officialRows.push(React.createElement('div', { key: 'o-t', className: 'dshw_row' },
|
|
310
|
+
React.createElement('span', { className: 'dshw_muted' }, '\u5b98\u65b9 token'),
|
|
311
|
+
React.createElement('span', null, '\u8f93\u5165 ' + fmtTokens(official.tokens.input) + ' \u00b7 \u7f13\u5b58\u8bfb ' + fmtTokens(official.tokens.cacheRead) + ' \u00b7 \u8f93\u51fa ' + fmtTokens(official.tokens.output))))
|
|
312
|
+
}
|
|
313
|
+
officialRows.push(React.createElement('div', { key: 'o-c', className: 'dshw_row' },
|
|
314
|
+
React.createElement('span', { className: 'dshw_muted' }, '\u672c\u4f1a\u8bdd\u82b1\u8d39'),
|
|
315
|
+
React.createElement('span', null, official.cost === null ? '\u672a\u5b9a\u4ef7\u6a21\u578b' : '\u00a5' + fmtMoney(official.cost))))
|
|
316
|
+
|
|
317
|
+
var thirdRows = []
|
|
318
|
+
thirdRows.push(React.createElement('div', { key: 't-t', className: 'dshw_row' },
|
|
319
|
+
React.createElement('span', { className: 'dshw_muted' }, '\u7b2c\u4e09\u65b9 token'),
|
|
320
|
+
React.createElement('span', null, '\u8f93\u5165 ' + fmtTokens(third.tokens && third.tokens.input) + ' \u00b7 \u7f13\u5b58\u8bfb ' + fmtTokens(third.tokens && third.tokens.cacheRead) + ' \u00b7 \u8f93\u51fa ' + fmtTokens(third.tokens && third.tokens.output))))
|
|
321
|
+
|
|
322
|
+
var thresholdInput = React.createElement('input', {
|
|
323
|
+
className: 'dshw_input',
|
|
324
|
+
type: 'number',
|
|
325
|
+
min: '0',
|
|
326
|
+
step: 'any',
|
|
327
|
+
value: thresholdDraft === null ? '' : thresholdDraft,
|
|
328
|
+
onChange: function (event) { setThresholdDraft(event.target.value) }
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
var floatBtnRow = React.createElement('div', { className: 'dshw_row', style: { justifyContent: 'flex-end', marginTop: '2px' } },
|
|
332
|
+
React.createElement('button', {
|
|
333
|
+
className: 'dshw_btn',
|
|
334
|
+
style: { height: '22px', fontSize: '11px', padding: '0 10px' },
|
|
335
|
+
title: '浮动窗口模式:可拖动、可最小化',
|
|
336
|
+
onClick: function () { setFloated(true); setOpen(false) }
|
|
337
|
+
}, '◳ 浮动'))
|
|
338
|
+
|
|
339
|
+
var panel = React.createElement('div', {
|
|
340
|
+
ref: panelRef,
|
|
341
|
+
className: 'dshw_panel',
|
|
342
|
+
style: alignLeft ? { left: 0, right: 'auto' } : undefined,
|
|
343
|
+
onClick: function (e) { e.stopPropagation() }
|
|
344
|
+
},
|
|
345
|
+
floatBtnRow,
|
|
346
|
+
React.createElement('div', { className: 'dshw_title' }, '\u5b98\u65b9 DeepSeek'),
|
|
347
|
+
balanceRows,
|
|
348
|
+
officialRows,
|
|
349
|
+
React.createElement('div', { className: 'dshw_divider' }),
|
|
350
|
+
React.createElement('div', { className: 'dshw_title' }, '\u7b2c\u4e09\u65b9\u5408\u8ba1'),
|
|
351
|
+
thirdRows,
|
|
352
|
+
React.createElement('div', { className: 'dshw_divider' }),
|
|
353
|
+
React.createElement('div', { className: 'dshw_row' },
|
|
354
|
+
React.createElement('span', { className: 'dshw_muted' }, '\u4f59\u989d\u63d0\u9192\u9608\u503c (\u00a5, 0=\u5173)'),
|
|
355
|
+
thresholdInput),
|
|
356
|
+
React.createElement('div', { className: 'dshw_row' },
|
|
357
|
+
React.createElement('button', { className: 'dshw_btn', onClick: refreshBalance }, '\u5237\u65b0\u4f59\u989d'),
|
|
358
|
+
React.createElement('button', { className: 'dshw_btn dshw_btnPrimary', onClick: saveThreshold }, '\u4fdd\u5b58\u9608\u503c')),
|
|
359
|
+
React.createElement('a', {
|
|
360
|
+
className: 'dshw_btn dshw_btnPrimary',
|
|
361
|
+
href: snapshot.rechargeUrl || 'https://platform.deepseek.com/top_up',
|
|
362
|
+
style: { textDecoration: 'none', textAlign: 'center' },
|
|
363
|
+
onClick: onRechargeClick
|
|
364
|
+
}, '\u2197 \u53bb\u5b98\u65b9\u5145\u503c'),
|
|
365
|
+
React.createElement('div', { className: 'dshw_divider' }),
|
|
366
|
+
React.createElement('button', {
|
|
367
|
+
className: 'dshw_btn',
|
|
368
|
+
style: { width: '100%' },
|
|
369
|
+
onClick: function () {
|
|
370
|
+
if (window.confirm('\u786e\u8ba4\u6e05\u9664\u672c\u4f1a\u8bdd\u7684 token \u548c\u82b1\u8d39\u8bb0\u5f55\uff1f\u4e0d\u53ef\u6062\u590d\u3002')) clearSession()
|
|
371
|
+
}
|
|
372
|
+
}, '\u6e05\u9664\u672c\u4f1a\u8bdd\u6570\u636e')
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
var confirmOverlay = null
|
|
376
|
+
if (confirming) {
|
|
377
|
+
confirmOverlay = React.createElement('div', { className: 'dshw_overlay', onClick: function () { setConfirming(false) } },
|
|
378
|
+
React.createElement('div', { className: 'dshw_overlayBox', onClick: function (e) { e.stopPropagation() } },
|
|
379
|
+
React.createElement('div', null, '\u5c06\u6253\u5f00 DeepSeek \u5f00\u653e\u5e73\u53f0\u5b98\u65b9\u5145\u503c\u9875\uff1a'),
|
|
380
|
+
React.createElement('div', { className: 'dshw_muted' }, 'https://platform.deepseek.com/top_up'),
|
|
381
|
+
React.createElement('div', null, '\u767b\u5f55\u4e0e\u652f\u4ed8\u7531\u5b98\u65b9\u9875\u9762\u5b8c\u6210\uff0c\u672c\u63d2\u4ef6\u4e0d\u63a5\u89e6\u8d26\u53f7\u4e0e\u652f\u4ed8\u4fe1\u606f\u3002'),
|
|
382
|
+
React.createElement('div', { className: 'dshw_overlayRow' },
|
|
383
|
+
React.createElement('button', { className: 'dshw_btn', onClick: function () { setConfirming(false) } }, '\u53d6\u6d88'),
|
|
384
|
+
React.createElement('button', { className: 'dshw_btn dshw_btnPrimary', onClick: goRecharge }, '\u53bb\u5145\u503c'))))
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (floated) {
|
|
388
|
+
if (minimized) {
|
|
389
|
+
var dotPos = floatPos || readSavedPos() || { x: window.innerWidth - 300, y: 80 }
|
|
390
|
+
var dot = React.createElement('div', {
|
|
391
|
+
ref: floatRef,
|
|
392
|
+
className: low ? 'dshw_dot dshw_dotLow' : 'dshw_dot',
|
|
393
|
+
style: { left: dotPos.x, top: dotPos.y },
|
|
394
|
+
title: '钱包 · 点击展开',
|
|
395
|
+
onMouseDown: onFloatDown,
|
|
396
|
+
onClick: function () { if (didDragRef.current !== true) setMinimized(false) }
|
|
397
|
+
}, '¥' + (bal.total === null || bal.total === undefined ? '--' : fmtMoney(bal.total)))
|
|
398
|
+
return React.createElement(React.Fragment, null, chip, dot, confirmOverlay)
|
|
399
|
+
}
|
|
400
|
+
var winPos = floatPos || readSavedPos() || { x: window.innerWidth - 300, y: 80 }
|
|
401
|
+
var floatPanel = React.createElement('div', {
|
|
402
|
+
ref: floatRef,
|
|
403
|
+
className: 'dshw_float',
|
|
404
|
+
style: { left: winPos.x, top: winPos.y },
|
|
405
|
+
onMouseDown: onFloatDown
|
|
406
|
+
},
|
|
407
|
+
React.createElement('div', { className: 'dshw_floatHeader' },
|
|
408
|
+
React.createElement('span', null, '钱包'),
|
|
409
|
+
React.createElement('span', null,
|
|
410
|
+
React.createElement('button', {
|
|
411
|
+
className: 'dshw_floatBtn',
|
|
412
|
+
title: '最小化为圆点',
|
|
413
|
+
onClick: function (e) { e.stopPropagation(); setMinimized(true) }
|
|
414
|
+
}, '–'),
|
|
415
|
+
React.createElement('button', {
|
|
416
|
+
className: 'dshw_floatBtn',
|
|
417
|
+
style: { marginLeft: '4px' },
|
|
418
|
+
title: '收回标签模式',
|
|
419
|
+
onClick: function (e) { e.stopPropagation(); setFloated(false); setOpen(false) }
|
|
420
|
+
}, '×'))),
|
|
421
|
+
balanceRows,
|
|
422
|
+
officialRows,
|
|
423
|
+
React.createElement('div', { className: 'dshw_divider' }),
|
|
424
|
+
thirdRows,
|
|
425
|
+
React.createElement('div', { className: 'dshw_divider' }),
|
|
426
|
+
React.createElement('div', { className: 'dshw_row' },
|
|
427
|
+
React.createElement('span', { className: 'dshw_muted' }, '阈值(¥,0=关)'),
|
|
428
|
+
thresholdInput),
|
|
429
|
+
React.createElement('div', { className: 'dshw_row' },
|
|
430
|
+
React.createElement('button', { className: 'dshw_btn', onClick: refreshBalance }, '刷新'),
|
|
431
|
+
React.createElement('button', { className: 'dshw_btn dshw_btnPrimary', onClick: saveThreshold }, '保存')),
|
|
432
|
+
React.createElement('a', {
|
|
433
|
+
className: 'dshw_btn dshw_btnPrimary',
|
|
434
|
+
href: snapshot.rechargeUrl || 'https://platform.deepseek.com/top_up',
|
|
435
|
+
style: { textDecoration: 'none', textAlign: 'center' },
|
|
436
|
+
onClick: onRechargeClick
|
|
437
|
+
}, '↗ 充值'),
|
|
438
|
+
React.createElement('button', {
|
|
439
|
+
className: 'dshw_btn', style: { width: '100%' },
|
|
440
|
+
onClick: function () {
|
|
441
|
+
if (window.confirm('确认清除本会话的 token 和花费记录?不可恢复。')) clearSession()
|
|
442
|
+
}
|
|
443
|
+
}, '清除本会话')
|
|
444
|
+
)
|
|
445
|
+
return React.createElement(React.Fragment, null, chip, floatPanel, confirmOverlay)
|
|
446
|
+
}
|
|
447
|
+
return React.createElement(React.Fragment, null, chip, panel, confirmOverlay)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
var inject = ['slots']
|
|
451
|
+
|
|
452
|
+
function apply(ctx) {
|
|
453
|
+
ctx.inject(['slots', 'conversation'], function (scope) {
|
|
454
|
+
scope.effect(function () {
|
|
455
|
+
return scope.slots.register({
|
|
456
|
+
name: 'conversation.composer.dock',
|
|
457
|
+
id: 'wallet',
|
|
458
|
+
order: 130,
|
|
459
|
+
inject: function () { return {} }
|
|
460
|
+
}, WalletChip)
|
|
461
|
+
}, 'dsh-wallet: chip registration')
|
|
462
|
+
})
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
exports.apply = apply
|
|
466
|
+
exports.inject = inject
|
|
467
|
+
return module.exports
|
|
468
|
+
}
|
|
469
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "deepseek-harness-wallet",
|
|
3
|
+
"description": "Balance monitoring, per-session spend tracking, token statistics, low-balance alerts, and an official recharge shortcut for DeepSeek Harness.",
|
|
4
|
+
"version": "0.1.2",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./client": "./lib/client.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"index.js",
|
|
14
|
+
"lib/client.js",
|
|
15
|
+
"cordis.patch.yml"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "node --test"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"deepseek",
|
|
23
|
+
"deepseek-harness",
|
|
24
|
+
"balance-monitor",
|
|
25
|
+
"recharge",
|
|
26
|
+
"token-tracking",
|
|
27
|
+
"cost-tracking",
|
|
28
|
+
"wallet",
|
|
29
|
+
"llm",
|
|
30
|
+
"agentic-ai"
|
|
31
|
+
],
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/feibi-mochi/deepseek-harness-wallet.git"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/feibi-mochi/deepseek-harness-wallet#readme",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/feibi-mochi/deepseek-harness-wallet/issues"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
42
|
+
},
|
|
43
|
+
"dsh": {
|
|
44
|
+
"bundle": {
|
|
45
|
+
"patch": "./cordis.patch.yml"
|
|
46
|
+
},
|
|
47
|
+
"client": {
|
|
48
|
+
"platform": "web",
|
|
49
|
+
"inject": [
|
|
50
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
52
|
+
"@deepseek-ai/dsh-client-ui-conversation"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|