@feiyang666/dsh-usage-plugin 1.14.0 → 1.16.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -5
- package/README.md +7 -1
- package/README.zh.md +7 -2
- package/lib/client.js +651 -81
- package/lib/index.js +72 -20
- package/package.json +2 -3
- package/RELEASE_NOTES.md +0 -46
package/lib/index.js
CHANGED
|
@@ -186,6 +186,7 @@ export default {
|
|
|
186
186
|
inputTokens: r.inputTokens, outputTokens: r.outputTokens,
|
|
187
187
|
cacheReadTokens: r.cacheReadTokens, cacheWriteTokens: r.cacheWriteTokens,
|
|
188
188
|
reasoningTokens: r.reasoningTokens, finishReason: r.finishReason,
|
|
189
|
+
interrupted: !!r.interrupted,
|
|
189
190
|
usdCnyRate: r.usdCnyRate || 0, fxDate: r.fxDate || '',
|
|
190
191
|
modelKey: modelKey(r.model),
|
|
191
192
|
baseCost: costFor(r, 'base'), peakValleyCost: costFor(r, 'peakValley'), autoCost: costFor(r, 'auto'),
|
|
@@ -254,7 +255,7 @@ export default {
|
|
|
254
255
|
function buildDays() {
|
|
255
256
|
const map = {}
|
|
256
257
|
for (const r of records) {
|
|
257
|
-
if (!isRealCall(r)) continue;
|
|
258
|
+
if (!isRealCall(r)) continue;
|
|
258
259
|
const key = bjKey(r.time)
|
|
259
260
|
let d = map[key]
|
|
260
261
|
if (!d) {
|
|
@@ -302,6 +303,8 @@ export default {
|
|
|
302
303
|
let root = ''
|
|
303
304
|
let dataPath = ''
|
|
304
305
|
let pricingPath = ''
|
|
306
|
+
let budgetPath = ''
|
|
307
|
+
let budget = null // { monthly: number|null } — 月度预算(元),null 表示未设置
|
|
305
308
|
let persistOk = false
|
|
306
309
|
let persistError = ''
|
|
307
310
|
let initPromise = null
|
|
@@ -362,6 +365,21 @@ export default {
|
|
|
362
365
|
return nfsWriteText(pricingPath, JSON.stringify(PRICING)).catch(() => {})
|
|
363
366
|
}
|
|
364
367
|
|
|
368
|
+
function persistBudget() {
|
|
369
|
+
if (!budgetPath || !persistOk) return Promise.resolve()
|
|
370
|
+
return nfsWriteText(budgetPath, JSON.stringify(budget)).catch(() => {})
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function loadBudget() {
|
|
374
|
+
if (!budgetPath) return
|
|
375
|
+
try {
|
|
376
|
+
const data = JSON.parse(await nfsReadText(budgetPath))
|
|
377
|
+
if (!data || typeof data !== 'object') return
|
|
378
|
+
const m = Number(data.monthly)
|
|
379
|
+
budget = { monthly: Number.isFinite(m) && m >= 0 ? m : null }
|
|
380
|
+
} catch (e) {}
|
|
381
|
+
}
|
|
382
|
+
|
|
365
383
|
async function loadPricing() {
|
|
366
384
|
if (!pricingPath) return
|
|
367
385
|
try {
|
|
@@ -400,6 +418,7 @@ export default {
|
|
|
400
418
|
cacheWriteTokens: toNum(raw.cacheWriteTokens),
|
|
401
419
|
reasoningTokens: toNum(raw.reasoningTokens),
|
|
402
420
|
finishReason: String(raw.finishReason || ''),
|
|
421
|
+
interrupted: !!raw.interrupted,
|
|
403
422
|
usdCnyRate: toNum(raw.usdCnyRate),
|
|
404
423
|
fxDate: String(raw.fxDate || '')
|
|
405
424
|
}
|
|
@@ -431,7 +450,9 @@ export default {
|
|
|
431
450
|
root = normPath(candidate)
|
|
432
451
|
dataPath = tryPath
|
|
433
452
|
pricingPath = joinPath(path.dirname(dataPath), 'pricing.json')
|
|
453
|
+
budgetPath = joinPath(path.dirname(dataPath), 'budget.json')
|
|
434
454
|
await loadPricing()
|
|
455
|
+
await loadBudget()
|
|
435
456
|
persistOk = true
|
|
436
457
|
persistError = ''
|
|
437
458
|
return { ok: true }
|
|
@@ -559,13 +580,14 @@ export default {
|
|
|
559
580
|
yield chunk
|
|
560
581
|
}
|
|
561
582
|
} finally {
|
|
583
|
+
if (!sessionId) {
|
|
584
|
+
try { const ag = currentAgent(); if (ag && ag.session && ag.session.id) sessionId = String(ag.session.id) } catch (e) {}
|
|
585
|
+
}
|
|
586
|
+
if (provider === 'digital-ocean' || provider === 'digitalocean') {
|
|
587
|
+
try { await refreshFxRate(false) } catch (e) {}
|
|
588
|
+
}
|
|
589
|
+
const isDO = provider === 'digital-ocean' || provider === 'digitalocean'
|
|
562
590
|
if (usage) {
|
|
563
|
-
if (!sessionId) {
|
|
564
|
-
try { const ag = currentAgent(); if (ag && ag.session && ag.session.id) sessionId = String(ag.session.id) } catch (e) {}
|
|
565
|
-
}
|
|
566
|
-
if (provider === 'digital-ocean' || provider === 'digitalocean') {
|
|
567
|
-
try { await refreshFxRate(false) } catch (e) {}
|
|
568
|
-
}
|
|
569
591
|
// 兼容性:DeepSeek 系 provider 的 usage 不单独上报 cacheWriteTokens
|
|
570
592
|
// (缓存写入发生在未命中时,即 cacheWriteTokens == inputTokens)。
|
|
571
593
|
// 当上游未提供该字段时,用未命中 token 数(inputTokens)兜底,避免
|
|
@@ -583,20 +605,42 @@ export default {
|
|
|
583
605
|
cacheWriteTokens: cacheWrite,
|
|
584
606
|
reasoningTokens: usage.reasoningTokens || 0,
|
|
585
607
|
finishReason,
|
|
586
|
-
usdCnyRate:
|
|
587
|
-
fxDate:
|
|
608
|
+
usdCnyRate: isDO ? (FX.rate || 0) : 0,
|
|
609
|
+
fxDate: isDO ? (FX.date || '') : ''
|
|
610
|
+
})
|
|
611
|
+
} else if (isRealCall({ model, provider, time: startedAt })) {
|
|
612
|
+
// 兜底:harness 在流被中断(aborted / error / timeout)时不会产出
|
|
613
|
+
// usage chunk(usage 只在收到 [DONE] 哨兵后才由 adapter yield),
|
|
614
|
+
// 但官方后台仍会按该次请求的实际 token 计费。这里补记一条 0-token
|
|
615
|
+
// 的「中断调用」,使调用次数与官方口径对齐;token 与费用均为 0,
|
|
616
|
+
// 不会虚增消耗。interrupted 标记供客户端 UI 区分展示。
|
|
617
|
+
records.push({
|
|
618
|
+
time: startedAt,
|
|
619
|
+
model,
|
|
620
|
+
provider,
|
|
621
|
+
purpose,
|
|
622
|
+
sessionId,
|
|
623
|
+
inputTokens: 0,
|
|
624
|
+
outputTokens: 0,
|
|
625
|
+
cacheReadTokens: 0,
|
|
626
|
+
cacheWriteTokens: 0,
|
|
627
|
+
reasoningTokens: 0,
|
|
628
|
+
finishReason: finishReason || 'aborted',
|
|
629
|
+
interrupted: true,
|
|
630
|
+
usdCnyRate: 0,
|
|
631
|
+
fxDate: ''
|
|
588
632
|
})
|
|
589
|
-
if (records.length > MAX_RECORDS) records.splice(0, records.length - MAX_RECORDS)
|
|
590
|
-
try {
|
|
591
|
-
const agent = currentAgent()
|
|
592
|
-
const sp = ctx.get('sandboxPolicy')
|
|
593
|
-
if (sp && typeof sp.resolve === 'function' && agent && agent.session) {
|
|
594
|
-
const policy = sp.resolve({ session: agent.session })
|
|
595
|
-
if (policy && policy.workspaceRoot) cachedPolicy = policy
|
|
596
|
-
}
|
|
597
|
-
} catch (e) {}
|
|
598
|
-
ensureSessionRoot().then(persistNow).catch(() => {})
|
|
599
633
|
}
|
|
634
|
+
if (records.length > MAX_RECORDS) records.splice(0, records.length - MAX_RECORDS)
|
|
635
|
+
try {
|
|
636
|
+
const agent = currentAgent()
|
|
637
|
+
const sp = ctx.get('sandboxPolicy')
|
|
638
|
+
if (sp && typeof sp.resolve === 'function' && agent && agent.session) {
|
|
639
|
+
const policy = sp.resolve({ session: agent.session })
|
|
640
|
+
if (policy && policy.workspaceRoot) cachedPolicy = policy
|
|
641
|
+
}
|
|
642
|
+
} catch (e) {}
|
|
643
|
+
ensureSessionRoot().then(persistNow).catch(() => {})
|
|
600
644
|
}
|
|
601
645
|
}
|
|
602
646
|
|
|
@@ -1189,7 +1233,7 @@ export default {
|
|
|
1189
1233
|
try { await refreshFxRate(false) } catch (e) {}
|
|
1190
1234
|
const items = records.filter(isRealCall).map(projectRecord)
|
|
1191
1235
|
const toolItems = records.filter((r) => !isRealCall(r)).map(projectRecord)
|
|
1192
|
-
return { ok: true, records: items, count: items.length, toolCalls: toolItems, dataPath, persistOk, persistError, pricing: PRICING, effectiveAt: EFFECTIVE_AT, days: buildDays(), fx: FX }
|
|
1236
|
+
return { ok: true, records: items, count: items.length, toolCalls: toolItems, dataPath, persistOk, persistError, pricing: PRICING, effectiveAt: EFFECTIVE_AT, days: buildDays(), fx: FX, budget }
|
|
1193
1237
|
}
|
|
1194
1238
|
case 'tokenForMessage': {
|
|
1195
1239
|
// 消息底部弹窗:分两层统计——整场对话累计(conversation)+ 本轮明细(aggregate/records)。
|
|
@@ -1247,6 +1291,14 @@ export default {
|
|
|
1247
1291
|
persistPricing()
|
|
1248
1292
|
return { ok: true }
|
|
1249
1293
|
}
|
|
1294
|
+
case 'setBudget': {
|
|
1295
|
+
// 设置月度预算(元);传入非负数字生效,null/NaN/负数视为清除预算。
|
|
1296
|
+
const raw = Number(body && body.monthly)
|
|
1297
|
+
if (Number.isFinite(raw) && raw >= 0) budget = { monthly: raw }
|
|
1298
|
+
else budget = null
|
|
1299
|
+
persistBudget()
|
|
1300
|
+
return { ok: true, budget }
|
|
1301
|
+
}
|
|
1250
1302
|
case 'fxRefresh': {
|
|
1251
1303
|
const fx = await refreshFxRate(true)
|
|
1252
1304
|
return { ok: fx.rate > 0, fx, error: fx.rate > 0 ? '' : (fx.error || '无法获取汇率') }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feiyang666/dsh-usage-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.5",
|
|
4
4
|
"description": "DeepSeek Harness usage & cost tracker plugin: per-call token/cache-hit stats, peak/off-peak billing, DeepSeek balance query, CSV/JSON/PNG export with custom destination, and persistent local storage. Ships a host half plus a web client half in one npm package; installs into a DSH profile as a dsh.bundle with one command (dsh plugin --profile web add @feiyang666/dsh-usage-plugin).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek",
|
|
@@ -71,8 +71,7 @@
|
|
|
71
71
|
"cordis.patch.yml",
|
|
72
72
|
"README.md",
|
|
73
73
|
"README.zh.md",
|
|
74
|
-
"CHANGELOG.md"
|
|
75
|
-
"RELEASE_NOTES.md"
|
|
74
|
+
"CHANGELOG.md"
|
|
76
75
|
],
|
|
77
76
|
"scripts": {
|
|
78
77
|
"wire": "node scripts/wire.js",
|
package/RELEASE_NOTES.md
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
# Release Notes (English)
|
|
2
|
-
|
|
3
|
-
The authoritative **English** release notes for `@feiyang666/dsh-usage-plugin`.
|
|
4
|
-
|
|
5
|
-
Each new version's notes are added at the **top** of this file; the **[Version index](#version-index)** at the **bottom** is the default entry point for jumping to any release.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## v1.14.0 (2026-08-27)
|
|
10
|
-
|
|
11
|
-
This release focuses on **per-message token insights**, cleaner cost breakdowns, **system-level language sync**, and **mobile adaptation**.
|
|
12
|
-
|
|
13
|
-
### Per-message "Turn tokens" popup
|
|
14
|
-
|
|
15
|
-
After a turn finishes, the assistant message's footer action row gains a **"Turn tokens"** button. Clicking it opens a `Token Details` popup with two layers:
|
|
16
|
-
|
|
17
|
-
- **Conversation total** — the whole session's total tokens, total cost, run time and cache-hit rate;
|
|
18
|
-
- **This turn** — this output's tokens, turn tokens, turn cost, turn duration, turn cache-hit rate, a cache-hit bar, and per-model cards (each model's input·miss / cache hit / output (+ reasoning) plus its peak/off-peak cost split).
|
|
19
|
-
|
|
20
|
-
Durations are shown as `Xm Ys`. Every usage record is tagged with the conversation (`sessionId`), so per-turn and per-conversation stats are computed independently.
|
|
21
|
-
|
|
22
|
-
### Internal/tool calls grouped separately
|
|
23
|
-
|
|
24
|
-
Internal/placeholder calls (e.g. `dsh2shell-*` with model `fake`) no longer clutter the model cost tables; they are collected in a collapsible **"Tool calls (internal)"** group below the cost breakdown, so only real model calls count in the cost tables.
|
|
25
|
-
|
|
26
|
-
### Language follows the harness setting
|
|
27
|
-
|
|
28
|
-
The in-panel language toggle is removed; the plugin now follows the harness's own language setting (General Settings → Language) instantly, with no `localStorage` override — this also fixes the "still English after switching back" bug.
|
|
29
|
-
|
|
30
|
-
### Mobile adaptation
|
|
31
|
-
|
|
32
|
-
Tables fit the screen width on ≤900px (no horizontal scrollbar), wide tables collapse the middle columns, and the popup adapts to the viewport.
|
|
33
|
-
|
|
34
|
-
### Also fixed
|
|
35
|
-
|
|
36
|
-
- The popup previously rendered off-screen inside transformed ancestors — now portaled to `document.body`;
|
|
37
|
-
- Per-message windowing now reads the message's real step boundaries, so "this turn" and "conversation total" stats are truly independent;
|
|
38
|
-
- Wide tables no longer wrap numbers into unreadable lines.
|
|
39
|
-
|
|
40
|
-
---
|
|
41
|
-
|
|
42
|
-
## Version index
|
|
43
|
-
|
|
44
|
-
| Version | Date | English notes |
|
|
45
|
-
| --- | --- | --- |
|
|
46
|
-
| v1.14.0 | 2026-08-27 | [What's New in v1.14.0](#v1140-2026-08-27) |
|