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 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)
@@ -0,0 +1,4 @@
1
+ # deepseek-harness-wallet bundle patch: mounts the wallet row (host + browser halves).
2
+ - insert:
3
+ - id: wallet
4
+ name: deepseek-harness-wallet
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
+ }