@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/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: (provider === 'digital-ocean' || provider === 'digitalocean') ? (FX.rate || 0) : 0,
587
- fxDate: (provider === 'digital-ocean' || provider === 'digitalocean') ? (FX.date || '') : ''
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.14.0",
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) |