@mzzsfy/dsh-usage-dash 0.8.0 → 0.9.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/package.json +1 -1
- package/src/client.js +61 -26
- package/src/query.js +11 -10
- package/test/client-id.test.mjs +26 -0
- package/test/client.test.mjs +79 -2
- package/test/query.test.mjs +17 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mzzsfy/dsh-usage-dash",
|
|
3
3
|
"description": "用量统计面板:天/小时/分钟三粒度 token 与请求统计,26 周活跃热力图,会话底栏信息接管与官方动作行回合费用芯片,多条件分时段定价规则与费用估算;复刻自 github.com/HaoyueQin/dsh-usage-statistics-panel,感谢原作者 HaoyueQin",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.9.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"exports": {
|
package/src/client.js
CHANGED
|
@@ -13,7 +13,7 @@ const MINUTE_PRESET_MINUTES = { '3h': 3 * 60, '24h': 24 * 60, '3d': 3 * 24 * 60,
|
|
|
13
13
|
|
|
14
14
|
const DEFAULT_RANGE = '30'
|
|
15
15
|
const DEFAULT_HOUR_PRESET = '24h'
|
|
16
|
-
const DEFAULT_MINUTE_PRESET = '
|
|
16
|
+
const DEFAULT_MINUTE_PRESET = '3h'
|
|
17
17
|
|
|
18
18
|
// 天视图渲染上限;时/分上限 = 闭区间桶数(hour N+1 槽,minute N/10+1 槽)
|
|
19
19
|
const DAY_MAX_SLOTS = 180
|
|
@@ -196,6 +196,7 @@ const MESSAGES_ZH = {
|
|
|
196
196
|
avgSpeed: '平均生成速度',
|
|
197
197
|
speedLegend: '平均生成速度',
|
|
198
198
|
ttftLegend: '首 token 延迟',
|
|
199
|
+
avgPrice: '均价',
|
|
199
200
|
topModel: '最常用模型',
|
|
200
201
|
topModelHint: '按 token 用量排序,非调用次数',
|
|
201
202
|
heatmap: '活跃热力图',
|
|
@@ -343,6 +344,7 @@ const MESSAGES_EN = {
|
|
|
343
344
|
avgSpeed: 'Avg speed',
|
|
344
345
|
speedLegend: 'Avg speed',
|
|
345
346
|
ttftLegend: 'First-token latency',
|
|
347
|
+
avgPrice: 'Avg price',
|
|
346
348
|
topModel: 'Top model',
|
|
347
349
|
topModelHint: 'Ranked by token usage, not call count',
|
|
348
350
|
heatmap: 'Activity heatmap',
|
|
@@ -634,6 +636,15 @@ function groupPointSlots(slots) {
|
|
|
634
636
|
return { models, daily: foldSlotsByTop(slots, topModels) }
|
|
635
637
|
}
|
|
636
638
|
|
|
639
|
+
// 聚合展示区(统计卡/模型用量/统计截至)数据源:随视图联动——天视图用 range 聚合,时/分视图
|
|
640
|
+
// (预设与自定义挡)用点端点聚合,字段同形;点数据未回为 null,调用方判空不渲染
|
|
641
|
+
const cardsStatsOf = (view, stats, pointView) => (view === 'day' ? stats : pointView)
|
|
642
|
+
|
|
643
|
+
// 模型用量列表:天视图用预折叠分组,时/分视图折叠点窗口模型(top5+other),点数据未回为 null
|
|
644
|
+
const usageModelsOf = (view, grouped, pointView) => (
|
|
645
|
+
view === 'day' ? grouped?.models ?? null : pointView ? topWithOther(pointView.models) : null
|
|
646
|
+
)
|
|
647
|
+
|
|
637
648
|
// 趋势图视口常量
|
|
638
649
|
const CHART_HEIGHT = 220
|
|
639
650
|
const CHART_PAD = { left: 46, right: 65, top: 10, bottom: 26 }
|
|
@@ -665,7 +676,7 @@ function niceTicks(max, count) {
|
|
|
665
676
|
return ticks
|
|
666
677
|
}
|
|
667
678
|
|
|
668
|
-
// 图例显隐切换:current
|
|
679
|
+
// 图例显隐切换:current 为当前实际生效集;普通点击单选/再点恢复,
|
|
669
680
|
// ctrl 单项切换,隐藏最后一项为无操作(返回原引用)
|
|
670
681
|
function legendToggle(current, key, ctrl) {
|
|
671
682
|
if (ctrl) {
|
|
@@ -905,6 +916,13 @@ const hasTipContent = (hoverSlot) => Boolean(hoverSlot) && hoverSlot.total > 0
|
|
|
905
916
|
const hasAnomalyNews = (status) => !!status
|
|
906
917
|
&& ((status.log?.length ?? 0) > 0 || (status.recordFailures ?? 0) > 0)
|
|
907
918
|
|
|
919
|
+
// 趋势折线默认显隐集:模型全显 + 速度线(有数据时);缓存命中率与首 token 延迟默认隐藏,
|
|
920
|
+
// 图例切换后以组件内键集接管(不持久化)
|
|
921
|
+
const trendDefaultVisibleKeys = (modelOrder, hasSpeed) => new Set([
|
|
922
|
+
...modelOrder,
|
|
923
|
+
...(hasSpeed ? [LEGEND_KEY_SPEED] : []),
|
|
924
|
+
])
|
|
925
|
+
|
|
908
926
|
// 趋势 tooltip 模型行数据:主行 = 可见且当前时段有用量的模型;OTHER 子行 = OTHER 可见
|
|
909
927
|
// 且当前时段有用量的其余模型明细,按 tokens 降序由本函数保证
|
|
910
928
|
const tipModelEntries = (hoverSlot, visibleSet) => {
|
|
@@ -918,6 +936,11 @@ const tipModelEntries = (hoverSlot, visibleSet) => {
|
|
|
918
936
|
return { main, other }
|
|
919
937
|
}
|
|
920
938
|
|
|
939
|
+
// 趋势 tooltip 槽级均价:槽费用折每百万 token 单价;无槽或无有效费用(含零值)为 null
|
|
940
|
+
const tipAvgPriceText = (hoverSlot, currency) => (
|
|
941
|
+
hoverSlot ? avgPriceText(hoverSlot.cost, hoverSlot.total, currency) || null : null
|
|
942
|
+
)
|
|
943
|
+
|
|
921
944
|
// 热力图:窗口固定 26 周,与所选范围无关
|
|
922
945
|
const HEAT_WEEKS = 26
|
|
923
946
|
const HEAT_ROW_COUNT = 7
|
|
@@ -1421,10 +1444,11 @@ const COST_MICRO_THRESHOLD = 0.01
|
|
|
1421
1444
|
const COST_PLACEHOLDER = '—'
|
|
1422
1445
|
const THOUSANDS_PATTERN = /(\d)(?=(\d{3})+(?!\d))/g
|
|
1423
1446
|
|
|
1424
|
-
// 千分位 +
|
|
1425
|
-
function formatCost(value, currency) {
|
|
1426
|
-
const
|
|
1427
|
-
const
|
|
1447
|
+
// 千分位 + 至少 decimals 位小数,正值小于 10^-decimals 时小数位 +2;货币空串不加符号,「≈」前缀由调用方拼
|
|
1448
|
+
function formatCost(value, currency, decimals = COST_DECIMALS) {
|
|
1449
|
+
const microThreshold = 10 ** -decimals
|
|
1450
|
+
const used = value > 0 && value < microThreshold ? decimals + (COST_MICRO_DECIMALS - COST_DECIMALS) : decimals
|
|
1451
|
+
const [whole, fraction] = value.toFixed(used).split('.')
|
|
1428
1452
|
return `${currency}${whole.replace(THOUSANDS_PATTERN, '$1,')}.${fraction}`
|
|
1429
1453
|
}
|
|
1430
1454
|
|
|
@@ -1439,10 +1463,16 @@ function formatCostCompact(value, currency) {
|
|
|
1439
1463
|
return `${currency}${Math.round(value * COST_MICRO_SCALE) / COST_MICRO_SCALE}`
|
|
1440
1464
|
}
|
|
1441
1465
|
|
|
1466
|
+
// 模型均价:费用折算每百万 token 单价;无费用(cost 恒正数才有效,0 视为未配价)或零 token 无均价
|
|
1467
|
+
const AVG_PRICE_DECIMALS = 3
|
|
1468
|
+
function avgPriceText(cost, tokens, currency) {
|
|
1469
|
+
if (!(cost > 0) || !(tokens > 0)) return ''
|
|
1470
|
+
return `${formatCost(cost / (tokens / TOKENS_PER_MILLION), currency, AVG_PRICE_DECIMALS)}/M`
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1442
1473
|
// 全局显示货币:规则表首个非空 currency,所有费用显示点统一取此值(全局价格定位);
|
|
1443
1474
|
// 无则空串即不带符号。数值仍按命中规则单价计算,符号不随命中规则变化
|
|
1444
|
-
function aggregateCurrencyOf(rules) {
|
|
1445
|
-
if (!Array.isArray(rules)) return ''
|
|
1475
|
+
function aggregateCurrencyOf(rules) { if (!Array.isArray(rules)) return ''
|
|
1446
1476
|
const found = rules.find((rule) => typeof rule?.currency === 'string' && rule.currency !== '')
|
|
1447
1477
|
return found ? found.currency : ''
|
|
1448
1478
|
}
|
|
@@ -1906,7 +1936,8 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
|
|
|
1906
1936
|
const ICON_STROKE_WIDTH = 2
|
|
1907
1937
|
const NOTE_SEPARATOR = ' · '
|
|
1908
1938
|
const TIP_Z_INDEX = 1100
|
|
1909
|
-
|
|
1939
|
+
// 与注册 id 同源:既作样式查重键,又作 data-plugin 标记值,短名会使自身 rebuilt 删不到旧样式
|
|
1940
|
+
const STYLE_ID = '@mzzsfy/dsh-usage-dash'
|
|
1910
1941
|
// 开关视觉常量(规约形态:隐藏 checkbox + track 胶囊 + thumb 圆点)
|
|
1911
1942
|
const SWITCH_TRACK_WIDTH = 40
|
|
1912
1943
|
const SWITCH_TRACK_HEIGHT = 22
|
|
@@ -2265,12 +2296,12 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
2265
2296
|
return `var(--ud-chart-${rank})`
|
|
2266
2297
|
}
|
|
2267
2298
|
|
|
2268
|
-
function TrendChart({ title, notes, slots, tipSlots, modelOrder, colorFor, labelFor, slotLabelFor, labelMinPitch, busy, legendModels, panelRef, costCurrency = '',
|
|
2299
|
+
function TrendChart({ title, notes, slots, tipSlots, modelOrder, colorFor, labelFor, slotLabelFor, labelMinPitch, busy, legendModels, panelRef, costCurrency = '', money = false, costAvailable = false, onMetricToggle, t = defaultT }) {
|
|
2269
2300
|
const metricId = React.useId()
|
|
2270
2301
|
const wrapRef = useRef(null)
|
|
2271
2302
|
const [avail, setAvail] = useState(CHART_NOMINAL_WIDTH)
|
|
2272
2303
|
const [hover, setHover] = useState(null)
|
|
2273
|
-
// 图例显隐:null =
|
|
2304
|
+
// 图例显隐:null = 默认集(trendDefaultVisibleKeys);切换以实际生效集为基线展开为键集
|
|
2274
2305
|
const [visibleKeys, setVisibleKeys] = useState(null)
|
|
2275
2306
|
// prefs 仅用于 hover tooltip 的费用显隐,经 ref 读取:开关切换不触发本组件重渲染,
|
|
2276
2307
|
// 避免宿主设置弹窗滚动锚定被重渲染扰动而跳变
|
|
@@ -2287,11 +2318,7 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
2287
2318
|
}, [])
|
|
2288
2319
|
const hasSpeed = slots.some((slot) => slot.speed !== undefined)
|
|
2289
2320
|
const hasTtft = slots.some((slot) => slot.ttft !== undefined)
|
|
2290
|
-
const visibleSet = visibleKeys ??
|
|
2291
|
-
...modelOrder, LEGEND_KEY_RATE,
|
|
2292
|
-
...(hasSpeed ? [LEGEND_KEY_SPEED] : []),
|
|
2293
|
-
...(hasTtft ? [LEGEND_KEY_TTFT] : []),
|
|
2294
|
-
])
|
|
2321
|
+
const visibleSet = visibleKeys ?? trendDefaultVisibleKeys(modelOrder, hasSpeed)
|
|
2295
2322
|
const visibleModels = modelOrder.filter((model) => visibleSet.has(model))
|
|
2296
2323
|
const showRate = visibleSet.has(LEGEND_KEY_RATE)
|
|
2297
2324
|
const showSpeed = hasSpeed && visibleSet.has(LEGEND_KEY_SPEED)
|
|
@@ -2319,6 +2346,7 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
2319
2346
|
const pick = (index) => (event) => setHover({ index, anchor: pointerAt(event) })
|
|
2320
2347
|
const clear = () => setHover(null)
|
|
2321
2348
|
const tipEntries = hasTipContent(hoverSlot) ? tipModelEntries(hoverSlot, visibleSet) : null
|
|
2349
|
+
const avgPrice = tipAvgPriceText(hoverSlot, costCurrency)
|
|
2322
2350
|
return h('div', { className: 'ud-section' },
|
|
2323
2351
|
h('div', { className: 'ud-section-head' },
|
|
2324
2352
|
h('span', { className: 'ud-section-title' }, title),
|
|
@@ -2387,7 +2415,7 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
2387
2415
|
h(Legend, {
|
|
2388
2416
|
models: legendModels, colorFor, speedEnabled: hasSpeed, ttftEnabled: hasTtft,
|
|
2389
2417
|
isVisible: (key) => visibleSet.has(key),
|
|
2390
|
-
onItem: (key, ctrl) => setVisibleKeys(legendToggle(
|
|
2418
|
+
onItem: (key, ctrl) => setVisibleKeys(legendToggle(visibleSet, key, ctrl)),
|
|
2391
2419
|
}),
|
|
2392
2420
|
h(ChartTip, { anchor: hover ? hover.anchor : null, panelRef },
|
|
2393
2421
|
tipEntries
|
|
@@ -2402,8 +2430,11 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
2402
2430
|
showRate ? h('div', { key: 'rate', className: 'ud-tip-row' }, `${t('cacheHitRate')}: ${cacheRateText(hoverSlot.cacheHit, hoverSlot.cacheMiss)}`) : null,
|
|
2403
2431
|
showSpeed ? h('div', { key: 'speed', className: 'ud-tip-row' }, `${t('avgSpeed')}: ${speedTipText(hoverSlot.speed)}`) : null,
|
|
2404
2432
|
showTtft ? h('div', { key: 'ttft', className: 'ud-tip-row' }, `${t('ttftLegend')}: ${ttftTipText(hoverSlot.ttft, t)}`) : null,
|
|
2405
|
-
|
|
2406
|
-
?
|
|
2433
|
+
prefsRef.current.costDisplay && hoverSlot.cost !== undefined
|
|
2434
|
+
? [
|
|
2435
|
+
h('div', { key: 'cost', className: 'ud-tip-row' }, `≈ ${formatCost(hoverSlot.cost, costCurrency)}`),
|
|
2436
|
+
avgPrice ? h('div', { key: 'avg-price', className: 'ud-tip-row' }, `${t('avgPrice')}: ${avgPrice}`) : null,
|
|
2437
|
+
]
|
|
2407
2438
|
: null,
|
|
2408
2439
|
]
|
|
2409
2440
|
: null))
|
|
@@ -2572,6 +2603,8 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
2572
2603
|
const speedText = modelSpeedText(item.speed)
|
|
2573
2604
|
const ttftText = modelTtftText(item.ttft)
|
|
2574
2605
|
const ioText = modelIoPercentText(item, t)
|
|
2606
|
+
const avgText = avgPriceText(item.cost, item.tokens, costCurrency)
|
|
2607
|
+
const tokensText = `${formatTokens(item.tokens)} (${formatPercent((item.tokens / total) * PERCENT_SCALE)})`
|
|
2575
2608
|
const metaParts = [
|
|
2576
2609
|
item.cost !== undefined ? `≈ ${formatCost(item.cost, costCurrency)}` : null,
|
|
2577
2610
|
ttftText,
|
|
@@ -2602,8 +2635,7 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
2602
2635
|
}, '›')
|
|
2603
2636
|
: null,
|
|
2604
2637
|
h('div', { className: 'ud-model-values' },
|
|
2605
|
-
h('span', { className: 'ud-model-tokens' },
|
|
2606
|
-
`${formatTokens(item.tokens)} (${formatPercent((item.tokens / total) * PERCENT_SCALE)})`),
|
|
2638
|
+
h('span', { className: 'ud-model-tokens' }, avgText ? `${tokensText} · ${avgText}` : tokensText),
|
|
2607
2639
|
ioText ? h('span', { className: 'ud-model-io' }, ioText) : null,
|
|
2608
2640
|
h('span', { className: 'ud-model-meta' }, metaParts.join(' · ')))),
|
|
2609
2641
|
isOther
|
|
@@ -3371,12 +3403,16 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
3371
3403
|
const grouped = useMemo(() => (stats ? groupStats(stats) : null), [stats])
|
|
3372
3404
|
const pointView = activeQuery && pointStatsMatches(pointStats, view, activeQuery.key) ? pointStats.value : null
|
|
3373
3405
|
const pointGrouped = useMemo(() => (pointView ? groupPointSlots(pointView.daily) : null), [pointView])
|
|
3374
|
-
const colorFor = useMemo(() => colorForModel(stats ? stats.models : []), [stats])
|
|
3375
3406
|
|
|
3376
3407
|
const pointActive = view !== 'day'
|
|
3377
3408
|
const trendSource = pointActive
|
|
3378
3409
|
? (pointGrouped ? { slots: pointGrouped.daily, models: pointGrouped.models, value: pointView } : null)
|
|
3379
3410
|
: (grouped ? { slots: grouped.daily, models: grouped.models, value: stats } : null)
|
|
3411
|
+
// 聚合展示区(统计卡/模型用量/统计截至)与趋势图同源,随视图联动;点数据未回为 null 不渲染
|
|
3412
|
+
const cardsStats = cardsStatsOf(view, stats, pointView)
|
|
3413
|
+
const usageModels = usageModelsOf(view, grouped, pointView)
|
|
3414
|
+
// 模型配色取当期窗口模型排名,防跨窗口取色错位
|
|
3415
|
+
const colorFor = useMemo(() => colorForModel(cardsStats ? cardsStats.models : []), [cardsStats])
|
|
3380
3416
|
const maxSlots = pointActive ? maxSlotsFor(view, rawPreset) : DAY_MAX_SLOTS
|
|
3381
3417
|
const trimmedSlots = trendSource ? trimSlots(trendSource.slots, maxSlots) : null
|
|
3382
3418
|
const { costReady, money, chartSlots } = moneyViewOf(trimmedSlots, costView)
|
|
@@ -3469,7 +3505,7 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
3469
3505
|
: null,
|
|
3470
3506
|
error ? h('div', { className: 'ud-error' }, error) : null,
|
|
3471
3507
|
loadingVisible ? h('div', { className: 'ud-loading' }, `${t('loading')}…`) : null,
|
|
3472
|
-
|
|
3508
|
+
cardsStats ? h(StatCards, { key: 'cards', stats: cardsStats, costCurrency, t }) : null,
|
|
3473
3509
|
// 活跃热力图仅按天视图展示:热力图口径为日桶,时/分视图无对应语义
|
|
3474
3510
|
view === 'day' ? h(HeatSection, { key: 'heat', days: heatDays, panelRef, costCurrency, t }) : null,
|
|
3475
3511
|
trimmedSlots
|
|
@@ -3488,15 +3524,14 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
3488
3524
|
legendModels: trendSource.models,
|
|
3489
3525
|
panelRef,
|
|
3490
3526
|
costCurrency,
|
|
3491
|
-
costEnabled: view === 'day',
|
|
3492
3527
|
money,
|
|
3493
3528
|
costAvailable: costReady,
|
|
3494
3529
|
onMetricToggle: setCostView,
|
|
3495
3530
|
t,
|
|
3496
3531
|
})
|
|
3497
3532
|
: null,
|
|
3498
|
-
|
|
3499
|
-
|
|
3533
|
+
usageModels ? h(ModelUsage, { key: 'models', models: usageModels, colorFor, panelRef, costCurrency, t }) : null,
|
|
3534
|
+
cardsStats?.to ? h('div', { className: 'ud-foot' }, `${t('asOf')} ${slotLabelFor(view)(cardsStats.to)}`) : null,
|
|
3500
3535
|
emptyVisible ? h('div', { className: 'ud-empty' }, t('empty')) : null,
|
|
3501
3536
|
h(StatsLineOptions, { key: 'prefs', t }),
|
|
3502
3537
|
h(PricingEditor, { key: 'pricing', t }))
|
package/src/query.js
CHANGED
|
@@ -14,7 +14,7 @@ const DAY_KEY_PATTERN = /^\d{4}-\d{2}-\d{2}$/
|
|
|
14
14
|
const HOUR_KEY_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}$/
|
|
15
15
|
const MINUTE_KEY_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/
|
|
16
16
|
|
|
17
|
-
// 妗朵覆鍓嶇紑瀹?D 娈靛畾浣嶆棩妗?M 琛屾埅鍙栫埗 H 妗?
|
|
17
|
+
// 妗朵覆鍓嶇紑瀹?D 娈靛畾浣嶆棩妗?M 琛屾埅鍙栫埗 H 妗?
|
|
18
18
|
const DAY_KEY_WIDTH = 'YYYY-MM-DD'.length
|
|
19
19
|
const HOUR_KEY_WIDTH = 'YYYY-MM-DDTHH'.length
|
|
20
20
|
|
|
@@ -39,10 +39,10 @@ const nextMinute = (date) => {
|
|
|
39
39
|
return next
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
// 鍒嗛挓妗剁矑搴?鏋氫妇涓庢《閿叡鐢ㄥ悓涓€姝ラ暱,from 蹇呴』瀵归綈妗惰竟鐣?
|
|
42
|
+
// 鍒嗛挓妗剁矑搴?鏋氫妇涓庢《閿叡鐢ㄥ悓涓€姝ラ暱,from 蹇呴』瀵归綈妗惰竟鐣?
|
|
43
43
|
const MINUTE_STEP_MINUTES = 10
|
|
44
44
|
|
|
45
|
-
// pattern 閿氬畾妗朵覆澶栧舰,suffix 琛ュ叏涓烘湰鍦版椂鍖哄彲瑙f瀽鏃ユ湡涓?format 鍥炶鏍¢獙鍒嗛噺鍚堟硶鎬?
|
|
45
|
+
// pattern 閿氬畾妗朵覆澶栧舰,suffix 琛ュ叏涓烘湰鍦版椂鍖哄彲瑙f瀽鏃ユ湡涓?format 鍥炶鏍¢獙鍒嗛噺鍚堟硶鎬?
|
|
46
46
|
const BUCKET_FORMS = {
|
|
47
47
|
[GRANULARITY_DAILY]: { pattern: DAY_KEY_PATTERN, suffix: 'T00:00:00', format: formatDate, step: nextDay },
|
|
48
48
|
[GRANULARITY_HOURLY]: { pattern: HOUR_KEY_PATTERN, suffix: ':00:00', format: formatHour, step: nextHour },
|
|
@@ -105,7 +105,8 @@ export function aggregateRange(rows, g, from, to) {
|
|
|
105
105
|
const slotByKey = new Map(slots.map((slot) => [slot.day, slot]))
|
|
106
106
|
const modelTotals = new Map()
|
|
107
107
|
const providerTotals = new Map()
|
|
108
|
-
|
|
108
|
+
// 活跃天 = 有 token 产出的自然日(桶键按日去重,跨粒度统一口径)
|
|
109
|
+
const activeDays = new Set()
|
|
109
110
|
// 妲界骇閰嶅:妗朵覆 鈫?閫熷害瀵?{decodeTokens, durationMs} 涓庨瀛楀 {ttftMs, ttftSteps},
|
|
110
111
|
const slotSpeeds = new Map()
|
|
111
112
|
const slotTtfts = new Map()
|
|
@@ -115,9 +116,9 @@ export function aggregateRange(rows, g, from, to) {
|
|
|
115
116
|
if (!slot) continue
|
|
116
117
|
const tokens = rowTokens(row)
|
|
117
118
|
addRowToSlot(slot, row, tokens)
|
|
118
|
-
// 绾?timing 琛?闆?token 妗?+ decode 閰嶅)涓嶅弬涓庡綊灞?浣嗕粛杩涢厤瀵硅仛鍚?
|
|
119
|
+
// 绾?timing 琛?闆?token 妗?+ decode 閰嶅)涓嶅弬涓庡綊灞?浣嗕粛杩涢厤瀵硅仛鍚?
|
|
119
120
|
if (tokens > 0) {
|
|
120
|
-
|
|
121
|
+
activeDays.add(row.bucket.slice(0, DAY_KEY_WIDTH))
|
|
121
122
|
slot.byModel[row.model] = (slot.byModel[row.model] ?? 0) + tokens
|
|
122
123
|
slot.byProvider[row.provider] = (slot.byProvider[row.provider] ?? 0) + tokens
|
|
123
124
|
}
|
|
@@ -175,7 +176,7 @@ export function aggregateRange(rows, g, from, to) {
|
|
|
175
176
|
totals.cacheMiss += slot.cacheMiss
|
|
176
177
|
}
|
|
177
178
|
// speed = decode 閰嶅鍙e緞(decodeTokens 梅 鏃堕暱绉?;ttft = 棣?token 寤惰繜
|
|
178
|
-
// 鍔犳潈骞冲潎(姣);浠呴厤瀵规暟鎹瓨鍦ㄧ殑鏉$洰鎸傚瓧娈?鏃犳暟鎹潯鐩笉鎸?
|
|
179
|
+
// 鍔犳潈骞冲潎(姣);浠呴厤瀵规暟鎹瓨鍦ㄧ殑鏉$洰鎸傚瓧娈?鏃犳暟鎹潯鐩笉鎸?
|
|
179
180
|
// 绾?timing 琛屽彲鑳戒骇鐢?0-token 鏉$洰,鍒楄〃淇濇寔鍙惈 token 琛?瀛橀噺濂戠害)
|
|
180
181
|
const models = [...modelTotals.entries()]
|
|
181
182
|
.filter(([, agg]) => agg.tokens > 0)
|
|
@@ -207,7 +208,7 @@ export function aggregateRange(rows, g, from, to) {
|
|
|
207
208
|
turns: totals.turns,
|
|
208
209
|
cacheHit: totals.cacheHit,
|
|
209
210
|
cacheMiss: totals.cacheMiss,
|
|
210
|
-
activeDays:
|
|
211
|
+
activeDays: activeDays.size,
|
|
211
212
|
topModel: top?.model ?? '',
|
|
212
213
|
topProvider: top?.provider ?? '',
|
|
213
214
|
daily,
|
|
@@ -218,7 +219,7 @@ export function aggregateRange(rows, g, from, to) {
|
|
|
218
219
|
return result
|
|
219
220
|
}
|
|
220
221
|
|
|
221
|
-
// 鑱氬悎璁′环鐨勬Ы瀹氫綅:D 鎶樺彔鍒版棩妲?H/M 鍗虫湰妲?璁′环涓€寰嬪彇琛屾墍灞?H 妲借捣鐐?
|
|
222
|
+
// 鑱氬悎璁′环鐨勬Ы瀹氫綅:D 鎶樺彔鍒版棩妲?H/M 鍗虫湰妲?璁′环涓€寰嬪彇琛屾墍灞?H 妲借捣鐐?
|
|
222
223
|
const COST_SLOT_KEYS = {
|
|
223
224
|
[GRANULARITY_DAILY]: (bucket) => bucket.slice(0, DAY_KEY_WIDTH),
|
|
224
225
|
[GRANULARITY_HOURLY]: (bucket) => bucket,
|
|
@@ -226,7 +227,7 @@ const COST_SLOT_KEYS = {
|
|
|
226
227
|
}
|
|
227
228
|
|
|
228
229
|
// 鑱氬悎璁′环:浠ュ彲瑙佹Ы涓哄敮涓€鍙e緞,cost 琛屾寜 H 妲借捣鐐瑰尮閰嶄环鏍煎悗绱姞;
|
|
229
|
-
// 妲界骇閫愭ā鍨嬫媶鍒?costByModel 渚涢噾棰濇煴鐘跺浘鎸夋ā鍨嬪爢鍙?
|
|
230
|
+
// 妲界骇閫愭ā鍨嬫媶鍒?costByModel 渚涢噾棰濇煴鐘跺浘鎸夋ā鍨嬪爢鍙?
|
|
230
231
|
export function attachCosts(result, costRows, granularity, rules) {
|
|
231
232
|
const slotKeyOf = COST_SLOT_KEYS[granularity]
|
|
232
233
|
const slotOfDay = new Map(result.daily.map((slot) => [slot.day, slot]))
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// client.js 注册 id 与样式标记守卫:loader 按 graph row id(完整包名)匹配注册,短名即加载失败;
|
|
2
|
+
// 样式查重键 STYLE_ID 兼作 data-plugin 标记值,短名会使自身 rebuilt 删不到旧样式,重注被跳过。
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import test from 'node:test'
|
|
7
|
+
import assert from 'node:assert/strict'
|
|
8
|
+
|
|
9
|
+
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
10
|
+
const { name } = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8'))
|
|
11
|
+
|
|
12
|
+
test('client.js 注册 id 为完整包名', () => {
|
|
13
|
+
const source = readFileSync(join(PKG_ROOT, 'src', 'client.js'), 'utf8')
|
|
14
|
+
const match = source.match(/__ModuleLoader__\.load\(\{\s*id:\s*'([^']+)'/)
|
|
15
|
+
assert.ok(match, 'client.js 缺少 __ModuleLoader__.load 注册')
|
|
16
|
+
assert.equal(match[1], name)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('client.js 样式注入自带 data-plugin 且值为注册包名', () => {
|
|
20
|
+
const source = readFileSync(join(PKG_ROOT, 'src', 'client.js'), 'utf8')
|
|
21
|
+
const injections = source.split("createElement('style')").length - 1
|
|
22
|
+
assert.ok(injections > 0, 'client.js 无样式注入点')
|
|
23
|
+
assert.ok(source.includes(`STYLE_ID = '${name}'`), 'STYLE_ID 必须等于注册包名(值=注册 id),短名会使自身 rebuilt 删不到旧样式')
|
|
24
|
+
const marked = source.split("setAttribute('data-plugin'").length - 1
|
|
25
|
+
assert.equal(marked, injections, '每个样式注入点都必须设置 data-plugin,防宿主 claimStyles 误归属后随他插件 HMR 整批误删')
|
|
26
|
+
})
|
package/test/client.test.mjs
CHANGED
|
@@ -36,7 +36,9 @@ const {
|
|
|
36
36
|
OTHER_MODEL,
|
|
37
37
|
aggregateCurrencyOf,
|
|
38
38
|
applyCurrencyToRules,
|
|
39
|
+
avgPriceText,
|
|
39
40
|
cacheRateText,
|
|
41
|
+
cardsStatsOf,
|
|
40
42
|
coerceConditions,
|
|
41
43
|
coercePricingRules,
|
|
42
44
|
costOf,
|
|
@@ -74,6 +76,8 @@ const {
|
|
|
74
76
|
hasTipContent,
|
|
75
77
|
hasAnomalyNews,
|
|
76
78
|
tipModelEntries,
|
|
79
|
+
tipAvgPriceText,
|
|
80
|
+
trendDefaultVisibleKeys,
|
|
77
81
|
modelSegmentLabel,
|
|
78
82
|
modelSpeedText,
|
|
79
83
|
modelNameOf,
|
|
@@ -111,9 +115,13 @@ const {
|
|
|
111
115
|
speedTipText,
|
|
112
116
|
speedScaleMax,
|
|
113
117
|
legendToggle,
|
|
118
|
+
LEGEND_KEY_RATE,
|
|
119
|
+
LEGEND_KEY_SPEED,
|
|
120
|
+
LEGEND_KEY_TTFT,
|
|
114
121
|
leftAxisTicks,
|
|
115
122
|
hourSlotLabel,
|
|
116
123
|
minuteSlotLabel,
|
|
124
|
+
usageModelsOf,
|
|
117
125
|
validatePricingRules,
|
|
118
126
|
} = core
|
|
119
127
|
|
|
@@ -163,10 +171,12 @@ test('预设定义表覆盖三视图', () => {
|
|
|
163
171
|
assert.deepEqual(MINUTE_PRESETS, ['3h', '24h', '3d', '7d'])
|
|
164
172
|
})
|
|
165
173
|
|
|
166
|
-
test('
|
|
174
|
+
test('默认挡位均在其挡位列表内,分钟默认挡为最短窗口 3 小时,分钟窗口可解析', () => {
|
|
167
175
|
assert.ok(DAY_PRESETS.includes(DEFAULT_RANGE))
|
|
168
176
|
assert.ok(HOUR_PRESETS.includes(DEFAULT_HOUR_PRESET))
|
|
169
177
|
assert.ok(MINUTE_PRESETS.includes(DEFAULT_MINUTE_PRESET))
|
|
178
|
+
// Given 分钟视图初始挂载 When 取默认挡 Then 为 3 小时(分钟粒度下 24 小时过粗,近端细节优先)
|
|
179
|
+
assert.equal(DEFAULT_MINUTE_PRESET, '3h')
|
|
170
180
|
assert.notEqual(resolveMinuteRange(DEFAULT_MINUTE_PRESET, NOW), null)
|
|
171
181
|
})
|
|
172
182
|
|
|
@@ -454,6 +464,35 @@ test('groupPointSlots 窗口模型超上限折叠其他', () => {
|
|
|
454
464
|
assert.deepEqual(grouped.daily[0].byModel, { m1: 1, m2: 1, m3: 1, m4: 1, m6: 1, [OTHER_MODEL]: 1 })
|
|
455
465
|
})
|
|
456
466
|
|
|
467
|
+
test('cardsStatsOf 天视图用 range 聚合,时/分视图用点聚合,点未回为 null', () => {
|
|
468
|
+
const stats = { tokens: 1 }
|
|
469
|
+
const pointView = { tokens: 2 }
|
|
470
|
+
assert.equal(cardsStatsOf('day', stats, pointView), stats)
|
|
471
|
+
assert.equal(cardsStatsOf('hour', stats, pointView), pointView)
|
|
472
|
+
assert.equal(cardsStatsOf('minute', stats, null), null)
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
test('usageModelsOf 天视图用预折叠分组,时/分视图折叠点窗口模型,点未回为 null', () => {
|
|
476
|
+
const grouped = { models: [{ model: 'p/m1', tokens: 1 }] }
|
|
477
|
+
assert.equal(usageModelsOf('day', grouped, null), grouped.models)
|
|
478
|
+
assert.equal(usageModelsOf('day', null, null), null)
|
|
479
|
+
const ranked = [
|
|
480
|
+
{ model: 'p/m1', tokens: 3 },
|
|
481
|
+
{ model: 'p/m2', tokens: 2 },
|
|
482
|
+
{ model: 'p/m3', tokens: 1 },
|
|
483
|
+
{ model: 'p/m4', tokens: 1 },
|
|
484
|
+
{ model: 'p/m5', tokens: 1 },
|
|
485
|
+
{ model: 'p/m6', tokens: 1 },
|
|
486
|
+
]
|
|
487
|
+
const folded = usageModelsOf('hour', null, { models: ranked })
|
|
488
|
+
assert.equal(folded.length, 6)
|
|
489
|
+
assert.equal(folded.at(-1).model, OTHER_MODEL)
|
|
490
|
+
assert.equal(folded.at(-1).tokens, 1)
|
|
491
|
+
// 空窗口契约:返回空数组渲染空环(与天视图空数据行为一致),非 null 隐藏
|
|
492
|
+
assert.deepEqual(usageModelsOf('minute', null, { models: [] }), [])
|
|
493
|
+
assert.equal(usageModelsOf('minute', null, null), null)
|
|
494
|
+
})
|
|
495
|
+
|
|
457
496
|
test('niceTicks 产生 1/2/5 序列且零上限为空', () => {
|
|
458
497
|
assert.deepEqual(niceTicks(0, 4), [])
|
|
459
498
|
assert.deepEqual(niceTicks(50, 4), [20, 40])
|
|
@@ -698,7 +737,7 @@ test('speedTipText 无速度为占位符有速度为官方吞吐口径', () => {
|
|
|
698
737
|
})
|
|
699
738
|
|
|
700
739
|
test('legendToggle 普通点击单选,再点恢复全部,ctrl 多选且禁全藏', () => {
|
|
701
|
-
// null
|
|
740
|
+
// null 输入(仅 historic 契约面):普通点击得单键集
|
|
702
741
|
assert.deepEqual(legendToggle(null, 'a', false), new Set(['a']))
|
|
703
742
|
// 单选态再点同项恢复全部(null)
|
|
704
743
|
assert.equal(legendToggle(new Set(['a']), 'a', false), null)
|
|
@@ -976,6 +1015,44 @@ test('formatCost 微观值四位小数', () => {
|
|
|
976
1015
|
assert.equal(formatCost(0.009999, '$'), '$0.0100')
|
|
977
1016
|
})
|
|
978
1017
|
|
|
1018
|
+
test('avgPriceText 费用折每百万 token 均价', () => {
|
|
1019
|
+
assert.equal(avgPriceText(3357.85, 6232901270, '¥'), '¥0.539/M')
|
|
1020
|
+
assert.equal(avgPriceText(2, 1000 * 1000, '$'), '$2.000/M')
|
|
1021
|
+
assert.equal(avgPriceText(0.5, 1000 * 1000, ''), '0.500/M')
|
|
1022
|
+
assert.equal(avgPriceText(0.0004, 1000 * 1000, '¥'), '¥0.00040/M')
|
|
1023
|
+
})
|
|
1024
|
+
|
|
1025
|
+
test('avgPriceText 无费用或零 token 不显示', () => {
|
|
1026
|
+
assert.equal(avgPriceText(undefined, 6232901270, '¥'), '')
|
|
1027
|
+
assert.equal(avgPriceText(0, 6232901270, '¥'), '')
|
|
1028
|
+
assert.equal(avgPriceText(3357.85, 0, '¥'), '')
|
|
1029
|
+
})
|
|
1030
|
+
|
|
1031
|
+
test('tipAvgPriceText 槽费用折每百万 token 均价,无费用或零槽为 null', () => {
|
|
1032
|
+
assert.equal(tipAvgPriceText({ total: 6232901270, cost: 3357.85 }, '¥'), '¥0.539/M')
|
|
1033
|
+
assert.equal(tipAvgPriceText({ total: 6232901270, cost: 0 }, '¥'), null)
|
|
1034
|
+
assert.equal(tipAvgPriceText({ total: 6232901270 }, '¥'), null)
|
|
1035
|
+
assert.equal(tipAvgPriceText({ total: 0, cost: 3357.85 }, '¥'), null)
|
|
1036
|
+
})
|
|
1037
|
+
|
|
1038
|
+
test('trendDefaultVisibleKeys 默认集含模型与速度线,不含缓存命中率与首 token 延迟', () => {
|
|
1039
|
+
const keys = trendDefaultVisibleKeys(['m1', 'm2'], true)
|
|
1040
|
+
assert.deepEqual([...keys], ['m1', 'm2', 'speed'])
|
|
1041
|
+
assert.deepEqual([...trendDefaultVisibleKeys(['m1'], false)], ['m1'])
|
|
1042
|
+
assert.deepEqual([...trendDefaultVisibleKeys([], false)], [])
|
|
1043
|
+
})
|
|
1044
|
+
|
|
1045
|
+
test('图例默认态点击以实际生效集为基线,ctrl 恢复隐藏线不坍缩', () => {
|
|
1046
|
+
const base = trendDefaultVisibleKeys(['m1', 'm2'], true)
|
|
1047
|
+
// ctrl 点击默认隐藏的缓存命中率:默认集其余项保持,仅追加该线;再点回默认集
|
|
1048
|
+
const withRate = legendToggle(base, LEGEND_KEY_RATE, true)
|
|
1049
|
+
assert.deepEqual([...withRate].sort(), ['m1', 'm2', LEGEND_KEY_SPEED, LEGEND_KEY_RATE].sort())
|
|
1050
|
+
assert.deepEqual([...legendToggle(withRate, LEGEND_KEY_RATE, true)].sort(), ['m1', 'm2', LEGEND_KEY_SPEED].sort())
|
|
1051
|
+
// 普通点击单选隐藏线,再点回 null 由调用方 ?? 默认集承接
|
|
1052
|
+
assert.deepEqual([...legendToggle(base, LEGEND_KEY_TTFT, false)], [LEGEND_KEY_TTFT])
|
|
1053
|
+
assert.equal(legendToggle(new Set([LEGEND_KEY_TTFT]), LEGEND_KEY_TTFT, false), null)
|
|
1054
|
+
})
|
|
1055
|
+
|
|
979
1056
|
test('镜像函数基本行为:非法输入 null 与命中计价', () => {
|
|
980
1057
|
assert.equal(matchPrice(null, 'x/m', NOW), null)
|
|
981
1058
|
assert.equal(matchPrice([], 'x/m', NOW), null)
|
package/test/query.test.mjs
CHANGED
|
@@ -183,6 +183,23 @@ test('H 粒度零值槽全枚举含无数据槽', () => {
|
|
|
183
183
|
assert.equal(out.activeDays, 1)
|
|
184
184
|
})
|
|
185
185
|
|
|
186
|
+
test('H/M 粒度活跃天数为有用量自然日数,同日多桶计 1', () => {
|
|
187
|
+
const hourRows = [
|
|
188
|
+
makeRow({ bucket: '2020-01-01T01', model: 'm1', provider: 'p1', inputTokens: 5 }),
|
|
189
|
+
makeRow({ bucket: '2020-01-01T05', model: 'm1', provider: 'p1', inputTokens: 5 }),
|
|
190
|
+
makeRow({ bucket: '2020-01-02T01', model: 'm1', provider: 'p1', inputTokens: 5 }),
|
|
191
|
+
]
|
|
192
|
+
const hourOut = aggregateRange(hourRows, 'H', '2020-01-01T00', '2020-01-02T23')
|
|
193
|
+
assert.equal(hourOut.activeDays, 2)
|
|
194
|
+
const minuteRows = [
|
|
195
|
+
makeRow({ bucket: '2020-01-01T00:00', model: 'm1', provider: 'p1', inputTokens: 5 }),
|
|
196
|
+
makeRow({ bucket: '2020-01-01T00:10', model: 'm1', provider: 'p1', inputTokens: 5 }),
|
|
197
|
+
makeRow({ bucket: '2020-01-01T00:20', model: 'm1', provider: 'p1', inputTokens: 5 }),
|
|
198
|
+
]
|
|
199
|
+
const minuteOut = aggregateRange(minuteRows, 'M', '2020-01-01T00:00', '2020-01-01T23:50')
|
|
200
|
+
assert.equal(minuteOut.activeDays, 1)
|
|
201
|
+
})
|
|
202
|
+
|
|
186
203
|
test('M 粒度纯计数行不计入模型与活跃桶,未对齐残行跳过', () => {
|
|
187
204
|
const rows = [
|
|
188
205
|
makeRow({ bucket: '2020-01-01T00:00', model: '(turns)', provider: 'default', turns: 1 }),
|