@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 +5 -2
- package/README.md +5 -2
- package/client.js +44 -29
- package/index.js +9 -11
- package/package.json +1 -1
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
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
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(
|
|
1641
|
-
notifyRow(
|
|
1642
|
-
notifyRow(
|
|
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 =
|
|
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,
|
|
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
|
|
578
|
+
const usage = event.data.usage
|
|
578
579
|
const delta = emptyUsageTotals()
|
|
579
580
|
delta.steps = 1
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
}
|