@feiyang666/dsh-usage-plugin 1.13.1 → 1.14.1

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
@@ -26,6 +26,22 @@ import {
26
26
  resolveBalanceEndpoint
27
27
  } from './balance.js'
28
28
 
29
+ // 周末统一按空闲价计费规则生效时间:北京时间 2026-08-23 00:00(周日)。
30
+ // 生效前(含周末)仍按原峰谷分段计费;生效后周末(周六、周日)全天统一按空闲价,工作日仍按峰谷分段。
31
+ export const WEEKEND_FLAT_AT = Date.parse('2026-08-23T00:00:00+08:00')
32
+
33
+ // 高峰时段判定:新规则(2026-08-23 起)周末(周六、周日)全天不再区分峰谷,统一按空闲价;
34
+ // 工作日仍按北京时间 9:00–12:00、14:00–18:00 为高峰;新规则生效前的调用仍按原规则判定。
35
+ // 星期取自平移后的北京时间(ts + 8h),避免 UTC 日历与北京日历在 16:00–24:00 UTC 段错位。
36
+ // 导出为独立函数,供 test/period.test.js 做回归测试(issue #9)。
37
+ export function isPeakAt(ts) {
38
+ const d = new Date(ts + 8 * 3600 * 1000)
39
+ const wd = d.getUTCDay() // 0=周日 1=周一 … 6=周六
40
+ if (ts >= WEEKEND_FLAT_AT && (wd === 0 || wd === 6)) return false
41
+ const t = d.getUTCHours() * 60 + d.getUTCMinutes()
42
+ return (t >= 9 * 60 && t < 12 * 60) || (t >= 14 * 60 && t < 18 * 60)
43
+ }
44
+
29
45
  export default {
30
46
  inject: ['fs', 'webServer', 'subprocess', 'credentials', 'settings', 'sandboxPolicy', 'agents'],
31
47
  apply(ctx) {
@@ -86,9 +102,9 @@ export default {
86
102
  // 新价格表(峰谷价)生效时间:北京时间 2026-08-17 00:00。
87
103
  // 在此之前的调用按旧价格表(基础价 base)计费;之后按新价格表(峰谷价)计费。
88
104
  const EFFECTIVE_AT = Date.parse('2026-08-17T00:00:00+08:00')
89
- // 周末统一按低谷(空闲)价计费规则生效时间:北京时间 2026-08-23 00:00(周日)。
90
- // 生效前(含周末)仍按原峰谷分段计费;生效后周末(周六、周日)全天统一按空闲价,工作日仍按峰谷分段。
91
- const WEEKEND_FLAT_AT = Date.parse('2026-08-23T00:00:00+08:00')
105
+
106
+ // 高峰/空闲判定使用模块级 isPeakAt(含周末规则与生效时间保护;实现见文件顶部导出,供回归测试)
107
+ const isPeak = isPeakAt
92
108
 
93
109
  function modelKey(model) {
94
110
  const m = String(model || '').toLowerCase()
@@ -98,16 +114,6 @@ export default {
98
114
  return 'unknown'
99
115
  }
100
116
 
101
- // 高峰时段判定:新规则(2026-08-23 起)周末(周六、周日)全天不再区分峰谷,统一按空闲价;
102
- // 工作日仍按北京时间 9:00–12:00、14:00–18:00 为高峰;新规则生效前的调用仍按原规则判定。
103
- function isPeak(ts) {
104
- const d = new Date(ts + 8 * 3600 * 1000)
105
- const wd = d.getUTCDay() // 0=周日 1=周一 … 6=周六
106
- if (ts >= WEEKEND_FLAT_AT && (wd === 0 || wd === 6)) return false
107
- const t = d.getUTCHours() * 60 + d.getUTCMinutes()
108
- return (t >= 9 * 60 && t < 12 * 60) || (t >= 14 * 60 && t < 18 * 60)
109
- }
110
-
111
117
  // Third-party providers are priced only when a verified provider/model
112
118
  // mapping exists. Unknown mappings deliberately remain zero rather than
113
119
  // inheriting DeepSeek prices from a similar model name.
@@ -161,6 +167,52 @@ export default {
161
167
  return (hit * p.cacheHit + miss * p.cacheMiss + out * p.output) / 1e6
162
168
  }
163
169
 
170
+ // 过滤内部/占位调用(如 harness 内部 dsh2shell-* + model "fake"):
171
+ // 这些不是用户调用的 API 服务商,不出现在用量明细 / 消耗明细 / 日历统计里。
172
+ function isRealCall(rec) {
173
+ const model = String(rec.model || '').trim().toLowerCase()
174
+ const provider = String(rec.provider || '').trim().toLowerCase()
175
+ if (!model || model === 'fake' || model === 'unknown') return false
176
+ if (!provider || provider === 'unknown') return false
177
+ // 内部 agent/工具路由(dsh2shell-* 等)不计入用户可见明细
178
+ if (provider.indexOf('dsh') === 0) return false
179
+ return true
180
+ }
181
+
182
+ // 把一条记录投影为 API 响应形状(供 list / tokenForMessage 共用,含成本与峰谷判定)。
183
+ function projectRecord(r) {
184
+ return {
185
+ time: r.time, model: r.model, provider: r.provider, purpose: r.purpose, sessionId: r.sessionId || '',
186
+ inputTokens: r.inputTokens, outputTokens: r.outputTokens,
187
+ cacheReadTokens: r.cacheReadTokens, cacheWriteTokens: r.cacheWriteTokens,
188
+ reasoningTokens: r.reasoningTokens, finishReason: r.finishReason,
189
+ interrupted: !!r.interrupted,
190
+ usdCnyRate: r.usdCnyRate || 0, fxDate: r.fxDate || '',
191
+ modelKey: modelKey(r.model),
192
+ baseCost: costFor(r, 'base'), peakValleyCost: costFor(r, 'peakValley'), autoCost: costFor(r, 'auto'),
193
+ peak: isPeak(r.time)
194
+ }
195
+ }
196
+
197
+ // 对已投影的记录做聚合(对话累计 / 本轮明细共用)。
198
+ function computeAgg(items) {
199
+ const agg = { calls: items.length, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, peakCost: 0, offCost: 0, baseCost: 0, totalCost: 0, hitRate: 0 }
200
+ let firstTime = 0, lastTime = 0
201
+ for (const it of items) {
202
+ agg.input += it.inputTokens; agg.output += it.outputTokens
203
+ agg.cacheRead += it.cacheReadTokens; agg.cacheWrite += it.cacheWriteTokens
204
+ agg.reasoning += it.reasoningTokens
205
+ if (it.peak) agg.peakCost += it.peakValleyCost; else agg.offCost += it.peakValleyCost
206
+ agg.baseCost += it.baseCost; agg.totalCost += it.autoCost
207
+ if (!firstTime || it.time < firstTime) firstTime = it.time
208
+ if (it.time > lastTime) lastTime = it.time
209
+ }
210
+ const denom = agg.cacheRead + agg.input
211
+ agg.hitRate = denom > 0 ? (agg.cacheRead / denom * 100) : 0
212
+ agg.time = { start: firstTime, end: lastTime }
213
+ return agg
214
+ }
215
+
164
216
  const msg = (e) => String((e && e.message) || e)
165
217
  const fail = (message) => ({ ok: false, error: message })
166
218
  const pad2 = (n) => (n < 10 ? '0' : '') + n
@@ -203,6 +255,7 @@ export default {
203
255
  function buildDays() {
204
256
  const map = {}
205
257
  for (const r of records) {
258
+ if (!isRealCall(r)) continue;
206
259
  const key = bjKey(r.time)
207
260
  let d = map[key]
208
261
  if (!d) {
@@ -341,12 +394,14 @@ export default {
341
394
  model: String(raw.model || ''),
342
395
  provider: String(raw.provider || ''),
343
396
  purpose: String(raw.purpose || ''),
397
+ sessionId: String(raw.sessionId || ''),
344
398
  inputTokens: toNum(raw.inputTokens),
345
399
  outputTokens: toNum(raw.outputTokens),
346
400
  cacheReadTokens: toNum(raw.cacheReadTokens),
347
401
  cacheWriteTokens: toNum(raw.cacheWriteTokens),
348
402
  reasoningTokens: toNum(raw.reasoningTokens),
349
403
  finishReason: String(raw.finishReason || ''),
404
+ interrupted: !!raw.interrupted,
350
405
  usdCnyRate: toNum(raw.usdCnyRate),
351
406
  fxDate: String(raw.fxDate || '')
352
407
  }
@@ -489,6 +544,7 @@ export default {
489
544
  const model = (options && options.model) || ''
490
545
  const provider = (options && options.provider) || ''
491
546
  const purpose = options && options.purpose ? String(options.purpose) : ''
547
+ let sessionId = options && options.sessionId ? String(options.sessionId) : ''
492
548
  const startedAt = Date.now()
493
549
  let usage = null
494
550
  let finishReason = ''
@@ -505,10 +561,14 @@ export default {
505
561
  yield chunk
506
562
  }
507
563
  } finally {
564
+ if (!sessionId) {
565
+ try { const ag = currentAgent(); if (ag && ag.session && ag.session.id) sessionId = String(ag.session.id) } catch (e) {}
566
+ }
567
+ if (provider === 'digital-ocean' || provider === 'digitalocean') {
568
+ try { await refreshFxRate(false) } catch (e) {}
569
+ }
570
+ const isDO = provider === 'digital-ocean' || provider === 'digitalocean'
508
571
  if (usage) {
509
- if (provider === 'digital-ocean' || provider === 'digitalocean') {
510
- try { await refreshFxRate(false) } catch (e) {}
511
- }
512
572
  // 兼容性:DeepSeek 系 provider 的 usage 不单独上报 cacheWriteTokens
513
573
  // (缓存写入发生在未命中时,即 cacheWriteTokens == inputTokens)。
514
574
  // 当上游未提供该字段时,用未命中 token 数(inputTokens)兜底,避免
@@ -519,26 +579,49 @@ export default {
519
579
  model,
520
580
  provider,
521
581
  purpose,
582
+ sessionId,
522
583
  inputTokens: usage.inputTokens || 0,
523
584
  outputTokens: usage.outputTokens || 0,
524
585
  cacheReadTokens: usage.cacheReadTokens || 0,
525
586
  cacheWriteTokens: cacheWrite,
526
587
  reasoningTokens: usage.reasoningTokens || 0,
527
588
  finishReason,
528
- usdCnyRate: (provider === 'digital-ocean' || provider === 'digitalocean') ? (FX.rate || 0) : 0,
529
- fxDate: (provider === 'digital-ocean' || provider === 'digitalocean') ? (FX.date || '') : ''
589
+ usdCnyRate: isDO ? (FX.rate || 0) : 0,
590
+ fxDate: isDO ? (FX.date || '') : ''
591
+ })
592
+ } else if (isRealCall({ model, provider, time: startedAt })) {
593
+ // 兜底:harness 在流被中断(aborted / error / timeout)时不会产出
594
+ // usage chunk(usage 只在收到 [DONE] 哨兵后才由 adapter yield),
595
+ // 但官方后台仍会按该次请求的实际 token 计费。这里补记一条 0-token
596
+ // 的「中断调用」,使调用次数与官方口径对齐;token 与费用均为 0,
597
+ // 不会虚增消耗。interrupted 标记供客户端 UI 区分展示。
598
+ records.push({
599
+ time: startedAt,
600
+ model,
601
+ provider,
602
+ purpose,
603
+ sessionId,
604
+ inputTokens: 0,
605
+ outputTokens: 0,
606
+ cacheReadTokens: 0,
607
+ cacheWriteTokens: 0,
608
+ reasoningTokens: 0,
609
+ finishReason: finishReason || 'aborted',
610
+ interrupted: true,
611
+ usdCnyRate: 0,
612
+ fxDate: ''
530
613
  })
531
- if (records.length > MAX_RECORDS) records.splice(0, records.length - MAX_RECORDS)
532
- try {
533
- const agent = currentAgent()
534
- const sp = ctx.get('sandboxPolicy')
535
- if (sp && typeof sp.resolve === 'function' && agent && agent.session) {
536
- const policy = sp.resolve({ session: agent.session })
537
- if (policy && policy.workspaceRoot) cachedPolicy = policy
538
- }
539
- } catch (e) {}
540
- ensureSessionRoot().then(persistNow).catch(() => {})
541
614
  }
615
+ if (records.length > MAX_RECORDS) records.splice(0, records.length - MAX_RECORDS)
616
+ try {
617
+ const agent = currentAgent()
618
+ const sp = ctx.get('sandboxPolicy')
619
+ if (sp && typeof sp.resolve === 'function' && agent && agent.session) {
620
+ const policy = sp.resolve({ session: agent.session })
621
+ if (policy && policy.workspaceRoot) cachedPolicy = policy
622
+ }
623
+ } catch (e) {}
624
+ ensureSessionRoot().then(persistNow).catch(() => {})
542
625
  }
543
626
  }
544
627
 
@@ -1129,17 +1212,23 @@ export default {
1129
1212
  switch (action) {
1130
1213
  case 'list': {
1131
1214
  try { await refreshFxRate(false) } catch (e) {}
1132
- const items = records.map((r) => ({
1133
- time: r.time, model: r.model, provider: r.provider, purpose: r.purpose,
1134
- inputTokens: r.inputTokens, outputTokens: r.outputTokens,
1135
- cacheReadTokens: r.cacheReadTokens, cacheWriteTokens: r.cacheWriteTokens,
1136
- reasoningTokens: r.reasoningTokens, finishReason: r.finishReason,
1137
- usdCnyRate: r.usdCnyRate || 0, fxDate: r.fxDate || '',
1138
- modelKey: modelKey(r.model),
1139
- baseCost: costFor(r, 'base'), peakValleyCost: costFor(r, 'peakValley'), autoCost: costFor(r, 'auto'),
1140
- peak: isPeak(r.time)
1141
- }))
1142
- return { ok: true, records: items, count: items.length, dataPath, persistOk, persistError, pricing: PRICING, effectiveAt: EFFECTIVE_AT, days: buildDays(), fx: FX }
1215
+ const items = records.filter(isRealCall).map(projectRecord)
1216
+ const toolItems = records.filter((r) => !isRealCall(r)).map(projectRecord)
1217
+ return { ok: true, records: items, count: items.length, toolCalls: toolItems, dataPath, persistOk, persistError, pricing: PRICING, effectiveAt: EFFECTIVE_AT, days: buildDays(), fx: FX }
1218
+ }
1219
+ case 'tokenForMessage': {
1220
+ // 消息底部弹窗:分两层统计——整场对话累计(conversation)+ 本轮明细(aggregate/records)。
1221
+ const sid = body && body.sessionId ? String(body.sessionId) : ''
1222
+ const from = Number(body && body.from) || 0
1223
+ const to = Number(body && body.to) || 0
1224
+ let sessionRecs = records.filter(isRealCall)
1225
+ if (sid) sessionRecs = sessionRecs.filter((r) => String(r.sessionId || '') === sid)
1226
+ let turnRecs = sessionRecs
1227
+ if (from > 0) turnRecs = turnRecs.filter((r) => r.time >= from)
1228
+ if (to > 0) turnRecs = turnRecs.filter((r) => r.time <= to)
1229
+ const convoAgg = computeAgg(sessionRecs.map(projectRecord))
1230
+ const turnAgg = computeAgg(turnRecs.map(projectRecord))
1231
+ return { ok: true, records: turnRecs.map(projectRecord), aggregate: turnAgg, conversation: convoAgg, count: turnRecs.length, from, to }
1143
1232
  }
1144
1233
  case 'clear': {
1145
1234
  const n = records.length
package/package.json CHANGED
@@ -1,88 +1,88 @@
1
- {
2
- "name": "@feiyang666/dsh-usage-plugin",
3
- "version": "1.13.1",
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
- "keywords": [
6
- "deepseek",
7
- "harness",
8
- "dsh",
9
- "plugin",
10
- "usage",
11
- "cost",
12
- "tokens",
13
- "cache",
14
- "balance"
15
- ],
16
- "license": "MIT",
17
- "type": "module",
18
- "main": "lib/index.js",
19
- "repository": {
20
- "type": "git",
21
- "url": "git+https://github.com/feiyang-dev/dsh-usage-plugin.git"
22
- },
23
- "contributors": [
24
- {
25
- "name": "ayleen",
26
- "url": "https://github.com/ayleen"
27
- },
28
- {
29
- "name": "wuhuqif176",
30
- "url": "https://github.com/wuhuqif176"
31
- },
32
- {
33
- "name": "Martin-soaring-dev",
34
- "url": "https://github.com/Martin-soaring-dev"
35
- },
36
- {
37
- "name": "liu3734",
38
- "url": "https://github.com/liu3734"
39
- },
40
- {
41
- "name": "mumuer1024",
42
- "url": "https://github.com/mumuer1024"
43
- }
44
- ],
45
- "homepage": "https://github.com/feiyang-dev/dsh-usage-plugin#readme",
46
- "bugs": {
47
- "url": "https://github.com/feiyang-dev/dsh-usage-plugin/issues"
48
- },
49
- "exports": {
50
- ".": "./lib/index.js",
51
- "./client": "./lib/client.js",
52
- "./package.json": "./package.json"
53
- },
54
- "engines": {
55
- "node": ">=18"
56
- },
57
- "dsh": {
58
- "bundle": {
59
- "patch": "./cordis.patch.yml"
60
- },
61
- "client": {
62
- "platform": "web",
63
- "inject": [
64
- "@deepseek-ai/dsh-client-ui-conversation"
65
- ]
66
- }
67
- },
68
- "files": [
69
- "lib",
70
- "scripts",
71
- "cordis.patch.yml",
72
- "README.md",
73
- "README.zh.md",
74
- "CHANGELOG.md"
75
- ],
76
- "scripts": {
77
- "wire": "node scripts/wire.js",
78
- "check": "node scripts/check-package.js",
79
- "prepublishOnly": "node scripts/check-package.js",
80
- "pack": "npm pack"
81
- },
82
- "peerDependencies": {
83
- "@deepseek-ai/cordis": "^4.0.1"
84
- },
85
- "dependencies": {
86
- "undici": "^7.0.0"
87
- }
88
- }
1
+ {
2
+ "name": "@feiyang666/dsh-usage-plugin",
3
+ "version": "1.14.1",
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
+ "keywords": [
6
+ "deepseek",
7
+ "harness",
8
+ "dsh",
9
+ "plugin",
10
+ "usage",
11
+ "cost",
12
+ "tokens",
13
+ "cache",
14
+ "balance"
15
+ ],
16
+ "license": "MIT",
17
+ "type": "module",
18
+ "main": "lib/index.js",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/feiyang-dev/dsh-usage-plugin.git"
22
+ },
23
+ "contributors": [
24
+ {
25
+ "name": "ayleen",
26
+ "url": "https://github.com/ayleen"
27
+ },
28
+ {
29
+ "name": "wuhuqif176",
30
+ "url": "https://github.com/wuhuqif176"
31
+ },
32
+ {
33
+ "name": "Martin-soaring-dev",
34
+ "url": "https://github.com/Martin-soaring-dev"
35
+ },
36
+ {
37
+ "name": "liu3734",
38
+ "url": "https://github.com/liu3734"
39
+ },
40
+ {
41
+ "name": "mumuer1024",
42
+ "url": "https://github.com/mumuer1024"
43
+ }
44
+ ],
45
+ "homepage": "https://github.com/feiyang-dev/dsh-usage-plugin#readme",
46
+ "bugs": {
47
+ "url": "https://github.com/feiyang-dev/dsh-usage-plugin/issues"
48
+ },
49
+ "exports": {
50
+ ".": "./lib/index.js",
51
+ "./client": "./lib/client.js",
52
+ "./package.json": "./package.json"
53
+ },
54
+ "engines": {
55
+ "node": ">=18"
56
+ },
57
+ "dsh": {
58
+ "bundle": {
59
+ "patch": "./cordis.patch.yml"
60
+ },
61
+ "client": {
62
+ "platform": "web",
63
+ "inject": [
64
+ "@deepseek-ai/dsh-client-ui-conversation"
65
+ ]
66
+ }
67
+ },
68
+ "files": [
69
+ "lib",
70
+ "scripts",
71
+ "cordis.patch.yml",
72
+ "README.md",
73
+ "README.zh.md",
74
+ "CHANGELOG.md"
75
+ ],
76
+ "scripts": {
77
+ "wire": "node scripts/wire.js",
78
+ "check": "node scripts/check-package.js",
79
+ "prepublishOnly": "node scripts/check-package.js",
80
+ "pack": "npm pack"
81
+ },
82
+ "peerDependencies": {
83
+ "@deepseek-ai/cordis": "^4.0.1"
84
+ },
85
+ "dependencies": {
86
+ "undici": "^7.0.0"
87
+ }
88
+ }