@gehennawu/dsh-service 0.14.0 → 0.15.0

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/README.en.md CHANGED
@@ -8,6 +8,8 @@ A service-control and operations plugin for self-hosted DSH Web. Provides safe r
8
8
 
9
9
  ## Features
10
10
 
11
+ The Settings panel "Service Control" page has six top-level tabs: **Overview, Notifications, Health, Model stats, Backups, Restart**; the Restart tab can also enable a `Restart` quick entry at the bottom of the settings left navigation (off by default).
12
+
11
13
  ### Version and updates
12
14
 
13
15
  - Displays current DSH and plugin versions with links to GitHub Releases
@@ -31,7 +33,8 @@ A service-control and operations plugin for self-hosted DSH Web. Provides safe r
31
33
 
32
34
  - 7-day stacked bar chart of input/output/cache tokens with blue/orange/teal legend
33
35
  - Filter by project; hover for exact values
34
- - Model breakdown sorted by steps: `x times · Cache hit x% · Input xM token · Output xM token`
36
+ - Model breakdown as horizontal stacked bars (same legend colors), sorted by total tokens, largest first; each row keeps `x times · Cache hit x% · Input xM token · Output xM token`
37
+ - Steps whose provider reports no token usage are excluded from the statistics
35
38
  - Last-24-hour model/tool error statistics, collapsed by default
36
39
 
37
40
  ### Backup management
@@ -44,7 +47,7 @@ A service-control and operations plugin for self-hosted DSH Web. Provides safe r
44
47
 
45
48
  ### Task notifications
46
49
 
47
- - Browser notification when a session finishes its turn, or when your approval, plan review, or answer is needed
50
+ - Notification settings live in the top-level Notifications tab: browser notification when a session finishes its turn, or when your approval, plan review, or answer is needed
48
51
  - Clicking a notification focuses the DSH page and closes the popup
49
52
  - Three toggle switches: master switch, task completion, and approvals & questions, each controlled independently
50
53
  - Bell icon in the conversation input bar toggles the master switch quickly
package/README.md CHANGED
@@ -8,6 +8,8 @@
8
8
 
9
9
  ## 功能
10
10
 
11
+ 设置页「服务控制」面板包含六个顶部标签:**概览、通知、健康诊断、模型统计、备份维护、重启**;重启标签还可在设置页左侧标签列底部开启「重启」快捷入口(默认关闭)。
12
+
11
13
  ### 版本与更新
12
14
 
13
15
  - 显示当前 DSH 和插件版本,版本号链接到 GitHub Releases
@@ -31,7 +33,8 @@
31
33
 
32
34
  - 近 7 天输入/输出/缓存 token 堆叠柱图,蓝/橙/青图例
33
35
  - 按项目筛选,鼠标悬停显示精确数值
34
- - 模型明细按步骤数降序,格式为「x次 · 缓存命中 x% · 输入 xM token · 输出 xM token
36
+ - 模型明细为横向堆叠柱形图(沿用主图图例配色),按 token 总量从多到少排序;每行附「x次 · 缓存命中 x% · 输入 xM token · 输出 xM token」明细
37
+ - 提供方未上报 token 用量的模型步骤不纳入统计
35
38
  - 最近 24 小时模型/工具报错统计,默认折叠
36
39
 
37
40
  ### 备份管理
@@ -44,7 +47,7 @@
44
47
 
45
48
  ### 任务通知
46
49
 
47
- - 会话完成一轮任务、或需要你授权/审阅计划/选择答案时发送浏览器通知
50
+ - 通知设置位于服务控制顶部的「通知」标签:会话完成一轮任务、或需要你授权/审阅计划/选择答案时发送浏览器通知
48
51
  - 点击系统通知弹窗会聚焦到 DSH 页面并关闭该通知
49
52
  - 三档开关条:总开关、任务结束通知、授权与提问通知,各自独立控制
50
53
  - 对话栏内铃铛图标快速切换总开关
package/client.js CHANGED
@@ -172,11 +172,14 @@ window.__ModuleLoader__.load({
172
172
  'usage.axis': 'token 纵轴',
173
173
  'usage.models.more': '展开其余 {count} 个模型',
174
174
  'usage.models.less': '收起模型列表',
175
+ 'usage.modelSortHint': '按 token 总量从多到少排列',
176
+ 'usage.modelBar': '{model}:共 {total} token',
175
177
  'usage.refresh': '刷新统计',
176
178
  'usage.refreshing': '刷新中…',
177
179
  'usage.empty': '尚未建立使用统计索引。点击刷新统计开始只读建立索引。',
178
180
  'usage.error': '无法读取模型使用统计',
179
181
  'usage.allProjects': '全部项目',
182
+ 'usage.total': 'token 总量',
180
183
  'usage.steps': '成功模型步骤',
181
184
  'usage.stepsValue': '{count} 次',
182
185
  'usage.modelLine': '{steps}次 · 缓存命中 {hitRate} · 输入 {input} token · 输出 {output} token',
@@ -186,7 +189,6 @@ window.__ModuleLoader__.load({
186
189
  'usage.hitRate': '缓存命中率',
187
190
  'usage.today': '今天',
188
191
  'usage.sevenDays': '近 7 天',
189
- 'usage.missing': '{count} 个步骤没有 token 数据',
190
192
  'usage.errors.title': '模型报错',
191
193
  'usage.errors.toggle': '模型报错({count} 类)',
192
194
  'usage.errors.recent': '最近 24 小时',
@@ -196,17 +198,14 @@ window.__ModuleLoader__.load({
196
198
  'usage.toolErrors.empty': '最近 24 小时没有记录到工具报错。',
197
199
  'usage.errors.count': '{count} 次',
198
200
  'notification.title': '通知',
199
- 'notification.description': '任务结束或需要你授权、选择答案时发送浏览器通知。需要授权浏览器通知权限;各开关在页面刷新后保持。',
201
+ 'notification.description': '任务结束或需要你授权、抉择时发送浏览器通知。需要授权浏览器通知权限。',
200
202
  'notification.enable': '开启通知',
201
203
  'notification.enabled': '通知已开启',
202
204
  'notification.disable': '关闭通知',
203
205
  'notification.denied': '通知权限被拒绝',
204
206
  'notification.master': '通知总开关',
205
- 'notification.masterHint': '关闭时下面两个开关暂停生效',
206
207
  'notification.done': '任务结束通知',
207
- 'notification.doneHint': '会话完成一轮任务时提醒',
208
208
  'notification.input': '授权与提问通知',
209
- 'notification.inputHint': '需要授权、审阅计划或选择答案时提醒',
210
209
  'notification.doneTitle': '任务完成',
211
210
  'notification.doneBody': '{title} 已完成本轮任务',
212
211
  'notification.inputTitle': '需要你的确认',
@@ -381,11 +380,14 @@ window.__ModuleLoader__.load({
381
380
  'usage.axis': 'token vertical axis',
382
381
  'usage.models.more': 'Show {count} more models',
383
382
  'usage.models.less': 'Collapse model list',
383
+ 'usage.modelSortHint': 'Sorted by total tokens, largest first',
384
+ 'usage.modelBar': '{model}: {total} tokens in total',
384
385
  'usage.refresh': 'Refresh usage',
385
386
  'usage.refreshing': 'Refreshing…',
386
387
  'usage.empty': 'No usage index yet. Select Refresh usage to build the read-only index.',
387
388
  'usage.error': 'Could not read model usage',
388
389
  'usage.allProjects': 'All projects',
390
+ 'usage.total': 'Total tokens',
389
391
  'usage.steps': 'Successful model steps',
390
392
  'usage.stepsValue': '{count} times',
391
393
  'usage.modelLine': '{steps} times · Cache hit {hitRate} · Input {input} token · Output {output} token',
@@ -395,7 +397,6 @@ window.__ModuleLoader__.load({
395
397
  'usage.hitRate': 'Cache hit rate',
396
398
  'usage.today': 'Today',
397
399
  'usage.sevenDays': 'Last 7 days',
398
- 'usage.missing': '{count} step(s) have no token data',
399
400
  'usage.errors.title': 'Model errors',
400
401
  'usage.errors.toggle': 'Model errors ({count} types)',
401
402
  'usage.errors.recent': 'Last 24 hours',
@@ -405,17 +406,14 @@ window.__ModuleLoader__.load({
405
406
  'usage.toolErrors.empty': 'No tool errors were recorded in the last 24 hours.',
406
407
  'usage.errors.count': '{count} occurrence(s)',
407
408
  'notification.title': 'Notifications',
408
- 'notification.description': 'Send a browser notification when a task finishes or when your approval or answer is needed. Requires browser notification permission; the toggles persist across page reloads.',
409
+ 'notification.description': 'Send a browser notification when a task finishes or when your approval or decision is needed. Requires browser notification permission.',
409
410
  'notification.enable': 'Enable notifications',
410
411
  'notification.enabled': 'Notifications enabled',
411
412
  'notification.disable': 'Disable notifications',
412
413
  'notification.denied': 'Notification permission denied',
413
414
  'notification.master': 'Master switch',
414
- 'notification.masterHint': 'Both kinds below stay paused while this is off',
415
415
  'notification.done': 'Task completion',
416
- 'notification.doneHint': 'Notify when a session finishes its turn',
417
416
  'notification.input': 'Approvals & questions',
418
- 'notification.inputHint': 'Notify when approval, plan review, or an answer is requested',
419
417
  'notification.doneTitle': 'Task complete',
420
418
  'notification.doneBody': '{title} has finished its turn',
421
419
  'notification.inputTitle': 'Your attention needed',
@@ -1264,15 +1262,19 @@ window.__ModuleLoader__.load({
1264
1262
  ]
1265
1263
  const usageTotalsFor = (day) => {
1266
1264
  const source = usage && usage.days ? usage.days[day] : null
1267
- if (!source) return { steps: 0, missingUsage: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, cacheHitRate: 0 }
1265
+ if (!source) return { steps: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, cacheHitRate: 0 }
1268
1266
  if (usageProject === 'all') return source.totals
1269
1267
  const project = source.projects.find((item) => item.id === usageProject)
1270
- return project ? project.totals : { steps: 0, missingUsage: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, cacheHitRate: 0 }
1268
+ return project ? project.totals : { steps: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, cacheHitRate: 0 }
1271
1269
  }
1272
1270
  const modelCacheHitRate = (model) => {
1273
1271
  const denominator = Number(model.inputTokens || 0) + Number(model.cacheReadTokens || 0) + Number(model.cacheWriteTokens || 0)
1274
1272
  return denominator === 0 ? 0 : Number(model.cacheReadTokens || 0) / denominator
1275
1273
  }
1274
+ const modelTotalTokens = (model) => Number(model.inputTokens || 0) + Number(model.outputTokens || 0) + Number(model.cacheReadTokens || 0) + Number(model.cacheWriteTokens || 0)
1275
+ const modelSegmentValue = (model, metric) => metric === 'cacheTokens'
1276
+ ? Number(model.cacheReadTokens || 0) + Number(model.cacheWriteTokens || 0)
1277
+ : Number(model[metric] || 0)
1276
1278
  const usageValue = (totals, metricName) => metricName === 'cacheTokens'
1277
1279
  ? totals.cacheReadTokens + totals.cacheWriteTokens
1278
1280
  : totals[metricName] || 0
@@ -1295,13 +1297,12 @@ window.__ModuleLoader__.load({
1295
1297
  const sevenTotals = usageDays.reduce((total, day) => {
1296
1298
  const source = usageTotalsFor(day.key)
1297
1299
  total.steps += source.steps || 0
1298
- total.missingUsage += source.missingUsage || 0
1299
1300
  total.inputTokens += source.inputTokens || 0
1300
1301
  total.outputTokens += source.outputTokens || 0
1301
1302
  total.cacheReadTokens += source.cacheReadTokens || 0
1302
1303
  total.cacheWriteTokens += source.cacheWriteTokens || 0
1303
1304
  return total
1304
- }, { steps: 0, missingUsage: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 })
1305
+ }, { steps: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 })
1305
1306
  const sevenDenominator = sevenTotals.inputTokens + sevenTotals.cacheReadTokens + sevenTotals.cacheWriteTokens
1306
1307
  sevenTotals.cacheHitRate = sevenDenominator === 0 ? 0 : sevenTotals.cacheReadTokens / sevenDenominator
1307
1308
  const selectedProjects = usageProject === 'all'
@@ -1340,6 +1341,7 @@ window.__ModuleLoader__.load({
1340
1341
  return translate('health.detail.generic', { status: translate(`health.status.${check.status}`) })
1341
1342
  }
1342
1343
  const summaryItems = (totals) => [
1344
+ ['usage.total', formatTokenValue(usageValue(totals, 'inputTokens') + usageValue(totals, 'outputTokens') + usageValue(totals, 'cacheTokens'))],
1343
1345
  ...usageSegments.map(([metricName, label]) => [label, formatTokenValue(usageValue(totals, metricName))]),
1344
1346
  ['usage.steps', formatUsageValue(totals.steps, 'steps')],
1345
1347
  ['usage.hitRate', formatUsageValue(totals.cacheHitRate, 'cacheHitRate')],
@@ -1350,9 +1352,10 @@ window.__ModuleLoader__.load({
1350
1352
  React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)' } }, translate(label)),
1351
1353
  React.createElement('span', { style: { fontWeight: 650 } }, value)))
1352
1354
  )
1353
- const sortedModels = [...modelTotals.values()].sort((a, b) => b.steps - a.steps || a.id.localeCompare(b.id))
1355
+ const sortedModels = [...modelTotals.values()].sort((a, b) => modelTotalTokens(b) - modelTotalTokens(a) || b.steps - a.steps || a.id.localeCompare(b.id))
1354
1356
  const visibleModels = modelsOpen ? sortedModels : sortedModels.slice(0, 3)
1355
1357
  const hiddenModelCount = Math.max(0, sortedModels.length - 3)
1358
+ const maxModelTokens = Math.max(1, ...visibleModels.map((model) => modelTotalTokens(model)))
1356
1359
  const selectedErrors = (list) => (list || [])
1357
1360
  .filter((error) => usageProject === 'all' || error.projectId === usageProject)
1358
1361
  .sort((a, b) => b.count - a.count || a.key.localeCompare(b.key))
@@ -1411,16 +1414,28 @@ window.__ModuleLoader__.load({
1411
1414
  React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '8px', marginTop: '10px' } },
1412
1415
  summaryBlock('today', 'usage.today', todayTotals),
1413
1416
  summaryBlock('seven', 'usage.sevenDays', sevenTotals)),
1414
- sevenTotals.missingUsage > 0 ? React.createElement('p', { style: hint }, translate('usage.missing', { count: sevenTotals.missingUsage })) : null,
1415
1417
  React.createElement('div', { 'data-testid': 'usage-model-list', style: { marginTop: '10px', padding: '8px 10px', borderRadius: '8px', background: 'var(--dsw-alias-bg-layer-2)', border: '1px solid var(--dsw-alias-border-l1)' } },
1416
- visibleModels.map((model, index) => React.createElement('div', { key: model.id, style: { display: 'flex', justifyContent: 'space-between', gap: '12px', fontSize: '12px', padding: '8px 2px', borderTop: index === 0 ? 0 : '1px solid var(--dsw-alias-border-l1)' } },
1417
- React.createElement('span', null, model.id),
1418
- React.createElement('span', { style: { textAlign: 'right' } }, translate('usage.modelLine', {
1419
- steps: Number(model.steps || 0).toLocaleString(),
1420
- hitRate: formatUsageValue(modelCacheHitRate(model), 'cacheHitRate'),
1421
- input: formatTokenValue(model.inputTokens),
1422
- output: formatTokenValue(model.outputTokens),
1423
- })))),
1418
+ React.createElement('div', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', marginBottom: '4px' } }, translate('usage.modelSortHint')),
1419
+ visibleModels.map((model, index) => {
1420
+ const total = modelTotalTokens(model)
1421
+ const fillWidth = `${Math.round(total / maxModelTokens * 10000) / 100}%`
1422
+ return React.createElement('div', { key: model.id, 'data-testid': `usage-model-row-${model.id}`, style: { padding: '8px 2px', borderTop: index === 0 ? 0 : '1px solid var(--dsw-alias-border-l1)' } },
1423
+ React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', gap: '12px', alignItems: 'baseline', fontSize: '12px' } },
1424
+ React.createElement('span', { style: { overflowWrap: 'anywhere' } }, model.id),
1425
+ React.createElement('span', { style: { fontWeight: 650, whiteSpace: 'nowrap' } }, formatTokenValue(total))),
1426
+ React.createElement('div', { 'data-testid': `usage-model-bar-${model.id}`, 'data-value': total, 'aria-label': translate('usage.modelBar', { model: model.id, total: formatTokenValue(total) }), style: { height: '8px', borderRadius: '4px', marginTop: '6px', overflow: 'hidden', background: 'var(--dsw-alias-border-l1)' } },
1427
+ React.createElement('div', { style: { display: 'flex', height: '100%', width: fillWidth } },
1428
+ usageSegments.map(([metricName, , color]) => {
1429
+ const value = modelSegmentValue(model, metricName)
1430
+ return value > 0 ? React.createElement('div', { key: metricName, 'data-testid': `usage-model-segment-${model.id}-${metricName}`, style: { width: `${value / total * 100}%`, background: color } }) : null
1431
+ }))),
1432
+ React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: '11px', marginTop: '4px', textAlign: 'right' } }, translate('usage.modelLine', {
1433
+ steps: Number(model.steps || 0).toLocaleString(),
1434
+ hitRate: formatUsageValue(modelCacheHitRate(model), 'cacheHitRate'),
1435
+ input: formatTokenValue(model.inputTokens),
1436
+ output: formatTokenValue(model.outputTokens),
1437
+ })))
1438
+ }),
1424
1439
  hiddenModelCount > 0 ? React.createElement('button', { style: Object.assign({}, toggle, { borderTop: '1px solid var(--dsw-alias-border-l1)', marginTop: '2px' }), onClick: () => setModelsOpen((value) => !value) }, `${modelsOpen ? '▾' : '▸'} ${translate(modelsOpen ? 'usage.models.less' : 'usage.models.more', { count: hiddenModelCount })}`) : null))
1425
1440
  : React.createElement('p', { style: hint }, usageError || translate('usage.empty')),
1426
1441
  React.createElement('div', { style: row }, React.createElement('button', { style: neutral, 'data-variant': 'neutral', onClick: refreshUsage, disabled: usageBusy }, translate(usageBusy ? 'usage.refreshing' : 'usage.refresh'))))
@@ -1622,10 +1637,10 @@ window.__ModuleLoader__.load({
1622
1637
  cursor: disabled ? 'default' : 'pointer', opacity: disabled ? 0.5 : 1, lineHeight: 0,
1623
1638
  },
1624
1639
  }, React.createElement('span', { style: { position: 'absolute', top: '1px', left: on ? '15px' : '1px', width: '16px', height: '16px', borderRadius: '50%', background: on ? '#fff' : 'var(--dsw-alias-label-tertiary)' } }))
1625
- const notifyRow = (label, labelHint, on, onChange, disabled) => React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px', padding: '5px 0' } },
1640
+ const notifyRow = (testId, label, labelHint, on, onChange, disabled) => React.createElement('div', { 'data-testid': testId, style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px', padding: '5px 0' } },
1626
1641
  React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
1627
1642
  React.createElement('span', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-primary)' } }, label),
1628
- React.createElement('span', { style: hint }, labelHint)),
1643
+ labelHint ? React.createElement('span', { style: hint }, labelHint) : null),
1629
1644
  notifySwitch(on, onChange, disabled))
1630
1645
  const notificationBlock = !notifSupported ? null
1631
1646
  : React.createElement('div', { style: { marginTop: '18px' } },
@@ -1637,9 +1652,9 @@ window.__ModuleLoader__.load({
1637
1652
  React.createElement('button', { style: neutral, onClick: () => { Notification.requestPermission().then((p) => { if (p === 'granted') setNotifyOn(true) }) } }, translate('notification.enable')),
1638
1653
  React.createElement('span', { style: hint }, notifPermission === 'denied' ? translate('notification.denied') : ''))
1639
1654
  : React.createElement('div', { style: { marginTop: '8px', display: 'flex', flexDirection: 'column' } },
1640
- notifyRow(translate('notification.master'), translate('notification.masterHint'), notifyOn, setNotifyOn, false),
1641
- notifyRow(translate('notification.done'), translate('notification.doneHint'), notifyDoneOn, setNotifyDoneOn, !notifyOn),
1642
- notifyRow(translate('notification.input'), translate('notification.inputHint'), notifyInputOn, setNotifyInputOn, !notifyOn))))
1655
+ notifyRow('notify-row-master', translate('notification.master'), null, notifyOn, setNotifyOn, false),
1656
+ notifyRow('notify-row-done', translate('notification.done'), null, notifyDoneOn, setNotifyDoneOn, !notifyOn),
1657
+ notifyRow('notify-row-input', translate('notification.input'), null, notifyInputOn, setNotifyInputOn, !notifyOn))))
1643
1658
  const overviewBlock = React.createElement('div', null, versionBlock, containerInfoBlock, overviewErrorsBlock)
1644
1659
  // 任务通知独立成顶部标签(v0.14 起不再混在概览里)
1645
1660
  const notifyBlock = React.createElement('div', null, notificationBlock)
package/index.js CHANGED
@@ -21,7 +21,7 @@ const MAX_BACKUP_TRANSFER_BYTES = 128 * 1024 * 1024
21
21
  const instanceId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`
22
22
  const backupIdSecret = randomBytes(32)
23
23
  const BACKUP_NAME = /^dsh-backup-\d{8}-\d{6}\.tar\.gz$/
24
- const USAGE_INDEX_VERSION = 4
24
+ const USAGE_INDEX_VERSION = 5
25
25
  const USAGE_INDEX_FILE = 'dsh-service-usage-index.json'
26
26
 
27
27
  // 升级目标白名单:命令与包名全部来自宿主常量,浏览器不传任何输入。
@@ -376,12 +376,11 @@ function localDayForHour(hour, timezoneOffsetMinutes = 0) {
376
376
  }
377
377
 
378
378
  function emptyUsageTotals() {
379
- return { steps: 0, missingUsage: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }
379
+ return { steps: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }
380
380
  }
381
381
 
382
382
  function addUsageTotals(target, source) {
383
383
  target.steps += source.steps || 0
384
- target.missingUsage += source.missingUsage || 0
385
384
  target.inputTokens += source.inputTokens || 0
386
385
  target.outputTokens += source.outputTokens || 0
387
386
  target.cacheReadTokens += source.cacheReadTokens || 0
@@ -571,19 +570,18 @@ function foldUsageEvents(ctx, record, previous, events) {
571
570
  if (addToolError(session, event)) continue
572
571
  if (addUsageError(session, event, model)) continue
573
572
  if (event.type !== 'assistant/message') continue
573
+ // 提供方未上报 token 用量的步骤直接忽略:不计步骤、不建模型桶、不提示。
574
+ if (event.data?.usage === undefined) continue
574
575
  const hour = usageHour(event.time)
575
576
  const bucket = session.hours[hour] || (session.hours[hour] = { totals: emptyUsageTotals(), models: {} })
576
577
  const modelBucket = bucket.models[model.id] || (bucket.models[model.id] = { id: model.id, provider: model.provider, model: model.model, totals: emptyUsageTotals() })
577
- const usage = event.data?.usage
578
+ const usage = event.data.usage
578
579
  const delta = emptyUsageTotals()
579
580
  delta.steps = 1
580
- if (usage === undefined) delta.missingUsage = 1
581
- else {
582
- delta.inputTokens = Number(usage.inputTokens) || 0
583
- delta.outputTokens = Number(usage.outputTokens) || 0
584
- delta.cacheReadTokens = Number(usage.cacheReadTokens) || 0
585
- delta.cacheWriteTokens = Number(usage.cacheWriteTokens) || 0
586
- }
581
+ delta.inputTokens = Number(usage.inputTokens) || 0
582
+ delta.outputTokens = Number(usage.outputTokens) || 0
583
+ delta.cacheReadTokens = Number(usage.cacheReadTokens) || 0
584
+ delta.cacheWriteTokens = Number(usage.cacheWriteTokens) || 0
587
585
  addUsageTotals(bucket.totals, delta)
588
586
  addUsageTotals(modelBucket.totals, delta)
589
587
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gehennawu/dsh-service",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "DSH Web 自托管运维面板:安全重启、健康监控、备份和 Linux 权限维护。",
5
5
  "type": "module",
6
6
  "scripts": {