@mzzsfy/dsh-usage-dash 0.2.0 → 0.4.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.md +54 -13
- package/package.json +2 -2
- package/src/archive-reader.js +138 -0
- package/src/client.js +548 -141
- package/src/collector.js +58 -12
- package/src/direct-log-reader.js +126 -0
- package/src/pricing.js +5 -6
- package/src/query.js +13 -0
- package/src/routes.js +14 -1
- package/src/store.js +70 -2
- package/test/archive-reader.test.mjs +365 -0
- package/test/client.test.mjs +261 -18
- package/test/collector.test.mjs +152 -8
- package/test/direct-log-reader.test.mjs +104 -0
- package/test/pricing-parity.test.mjs +38 -4
- package/test/pricing.test.mjs +50 -18
- package/test/query.test.mjs +22 -1
- package/test/routes.test.mjs +9 -2
- package/test/stats-line.test.mjs +6 -6
- package/test/store.test.mjs +58 -1
- package/test/turn-tail.test.mjs +95 -41
package/src/client.js
CHANGED
|
@@ -13,12 +13,12 @@ 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 = '24h'
|
|
17
17
|
|
|
18
18
|
// 天视图渲染上限;时/分上限 = 闭区间桶数(hour N+1 槽,minute N/10+1 槽)
|
|
19
19
|
const DAY_MAX_SLOTS = 180
|
|
20
20
|
const API_PREFIX = '/api/usage-dash/'
|
|
21
|
-
const ENDPOINTS = { range: 'range', hours: 'hours', minutes: 'minutes', status: 'status', reset: 'reset', pricing: 'pricing' }
|
|
21
|
+
const ENDPOINTS = { range: 'range', hours: 'hours', minutes: 'minutes', status: 'status', reset: 'reset', restore: 'restore', pricing: 'pricing' }
|
|
22
22
|
|
|
23
23
|
// 宿主语义 token 之外的插件本地模型色板容量与哨兵
|
|
24
24
|
const GROUP_TOP_COUNT = 5
|
|
@@ -126,6 +126,8 @@ const MESSAGES_ZH = {
|
|
|
126
126
|
cacheRateHint: '时间段内缓存命中 token 占输入 token 的比例',
|
|
127
127
|
cacheHitRate: '缓存命中率',
|
|
128
128
|
hitRateLegend: '缓存命中率',
|
|
129
|
+
avgSpeed: '平均生成速度',
|
|
130
|
+
speedLegend: '平均生成速度',
|
|
129
131
|
topModel: '最常用模型',
|
|
130
132
|
topModelHint: '按 token 用量排序,非调用次数',
|
|
131
133
|
heatmap: '活跃热力图',
|
|
@@ -146,6 +148,9 @@ const MESSAGES_ZH = {
|
|
|
146
148
|
'status.running': '回扫中 {done}/{total}',
|
|
147
149
|
rebuild: '重建',
|
|
148
150
|
rebuildConfirm: '确认重建',
|
|
151
|
+
restore: '回退数据',
|
|
152
|
+
restoreConfirm: '确认回退',
|
|
153
|
+
restoreMissing: '无可用回退点(重建时自动生成)',
|
|
149
154
|
hourTrend: '按小时 Token 趋势',
|
|
150
155
|
minuteTrend: '按分钟 Token 趋势',
|
|
151
156
|
'hourPreset.24h': '24 小时',
|
|
@@ -161,6 +166,7 @@ const MESSAGES_ZH = {
|
|
|
161
166
|
trendTruncated: '数据量过大,仅显示最近部分',
|
|
162
167
|
recordFailures: '{n} 条记录写入失败',
|
|
163
168
|
skippedSessions: '跳过 {n} 个无法读取的会话',
|
|
169
|
+
skipBreakdown: '宿主拒读 {d} / 存档损坏 {c} / 旧格式 {l} / 其他 {o}——宿主修复后下轮回扫自动补齐',
|
|
164
170
|
anomalyLog: '扫描异常日志',
|
|
165
171
|
logKindSkipped: '跳过会话',
|
|
166
172
|
logKindRecord: '写入失败',
|
|
@@ -171,18 +177,18 @@ const MESSAGES_ZH = {
|
|
|
171
177
|
'stats.tokensPerSecond': '{throughput} tok/s',
|
|
172
178
|
'stats.cacheHit': '缓存命中 {percent}%',
|
|
173
179
|
'stats.tokens': '输入 {input} tok · 输出 {output} tok',
|
|
180
|
+
'turnCostChip': '{cost}',
|
|
174
181
|
'stats.tokensDetail': '总 {total} tok · 输入 {input} tok · 命中缓存 {hit} tok · 未命中缓存 {miss} tok · 输出 {output} tok',
|
|
175
182
|
cachePrecision: '精确缓存命中率',
|
|
176
183
|
cachePrecisionDesc: '在会话底部信息栏以两位小数显示缓存命中率。',
|
|
177
184
|
tokenDetail: '会话 Token 明细',
|
|
178
185
|
tokenDetailDesc: '在会话底部信息栏显示总 Token、命中/未命中缓存与输出明细。',
|
|
179
186
|
costDisplay: '费用显示',
|
|
180
|
-
costDisplayDesc: '
|
|
187
|
+
costDisplayDesc: '在信息栏、趋势悬浮与回合费用芯片中显示按当前费率估算的费用。',
|
|
181
188
|
costTitle: '按当前费率对历史用量估算,精度为小时级',
|
|
182
189
|
costUnpriced: '{n} 个小时桶未计价',
|
|
183
190
|
statsCostTitle: '按当前费率对会话累计 token 估算',
|
|
184
191
|
'stats.cost': '费用 ≈ {cost}',
|
|
185
|
-
'stats.turnCost': '{summary} · 费用 ≈ {cost}',
|
|
186
192
|
turnCostTitle: '单轮用量按当前费率估算',
|
|
187
193
|
turnTokensUnreported: '该提供商未上报此桶',
|
|
188
194
|
pricing: '定价规则',
|
|
@@ -195,8 +201,8 @@ const MESSAGES_ZH = {
|
|
|
195
201
|
priceOutput: '输出',
|
|
196
202
|
priceCacheRead: '缓存读',
|
|
197
203
|
priceCacheWrite: '缓存写',
|
|
198
|
-
noCondition: '无条件 = 恒生效',
|
|
199
204
|
addCondition: '添加条件',
|
|
205
|
+
confirmDelete: '确认删除',
|
|
200
206
|
deleteCondition: '删除条件',
|
|
201
207
|
condKind: '条件类型',
|
|
202
208
|
condDailyWindow: '每日时段',
|
|
@@ -216,9 +222,17 @@ const MESSAGES_ZH = {
|
|
|
216
222
|
condWeekday: '需 0-6 整数',
|
|
217
223
|
condMonthDay: '需 1-31 整数',
|
|
218
224
|
condDate: '需 YYYY-MM-DD',
|
|
219
|
-
condRange: '
|
|
220
|
-
|
|
221
|
-
|
|
225
|
+
condRange: '结束不得早于起始(两端均含)',
|
|
226
|
+
ruleOrderHint: '附加计费规则从上到下匹配,首个命中生效;全不命中落默认价',
|
|
227
|
+
moveUp: '上移',
|
|
228
|
+
moveDown: '下移',
|
|
229
|
+
addGroup: '添加模型',
|
|
230
|
+
addRule: '添加额外计费规则',
|
|
231
|
+
addDefaultPrice: '添加默认价',
|
|
232
|
+
deleteGroup: '删除模型及其全部计费规则',
|
|
233
|
+
deleteRule: '删除计费规则',
|
|
234
|
+
addCondition: '添加条件',
|
|
235
|
+
defaultPriceHint: '默认价:附加规则全不命中时生效,不设条件',
|
|
222
236
|
save: '保存',
|
|
223
237
|
saved: '已保存',
|
|
224
238
|
required: '必填',
|
|
@@ -251,6 +265,8 @@ const MESSAGES_EN = {
|
|
|
251
265
|
cacheRateHint: 'Cache-hit tokens as a share of input tokens within the range',
|
|
252
266
|
cacheHitRate: 'Cache-hit rate',
|
|
253
267
|
hitRateLegend: 'Cache-hit rate',
|
|
268
|
+
avgSpeed: 'Avg speed',
|
|
269
|
+
speedLegend: 'Avg speed',
|
|
254
270
|
topModel: 'Top model',
|
|
255
271
|
topModelHint: 'Ranked by token usage, not call count',
|
|
256
272
|
heatmap: 'Activity heatmap',
|
|
@@ -271,6 +287,9 @@ const MESSAGES_EN = {
|
|
|
271
287
|
'status.running': 'Rescanning {done}/{total}',
|
|
272
288
|
rebuild: 'Rebuild',
|
|
273
289
|
rebuildConfirm: 'Confirm rebuild',
|
|
290
|
+
restore: 'Restore data',
|
|
291
|
+
restoreConfirm: 'Confirm restore',
|
|
292
|
+
restoreMissing: 'No restore point (created automatically on rebuild)',
|
|
274
293
|
hourTrend: 'Hourly token trend',
|
|
275
294
|
minuteTrend: 'Per-minute token trend',
|
|
276
295
|
'hourPreset.24h': '24 hours',
|
|
@@ -286,6 +305,7 @@ const MESSAGES_EN = {
|
|
|
286
305
|
trendTruncated: 'Too much data, showing only the latest part',
|
|
287
306
|
recordFailures: '{n} records failed to write',
|
|
288
307
|
skippedSessions: '{n} unreadable sessions skipped',
|
|
308
|
+
skipBreakdown: 'host-refused {d} / corrupt {c} / legacy format {l} / other {o} — auto-retried once the host can read them',
|
|
289
309
|
anomalyLog: 'Scan anomaly log',
|
|
290
310
|
logKindSkipped: 'skipped',
|
|
291
311
|
logKindRecord: 'write failed',
|
|
@@ -296,18 +316,18 @@ const MESSAGES_EN = {
|
|
|
296
316
|
'stats.tokensPerSecond': '{throughput} tok/s',
|
|
297
317
|
'stats.cacheHit': 'Cache hit {percent}%',
|
|
298
318
|
'stats.tokens': 'Input {input} tok · Output {output} tok',
|
|
319
|
+
'turnCostChip': '{cost}',
|
|
299
320
|
'stats.tokensDetail': 'Total {total} tok · Input {input} tok · Cache hit {hit} tok · Cache miss {miss} tok · Output {output} tok',
|
|
300
321
|
cachePrecision: 'Precise cache-hit rate',
|
|
301
322
|
cachePrecisionDesc: 'Show the cache-hit rate with two decimals in the session stats line.',
|
|
302
323
|
tokenDetail: 'Session token detail',
|
|
303
324
|
tokenDetailDesc: 'Show total, cache hit/miss and output tokens in the session stats line.',
|
|
304
325
|
costDisplay: 'Cost display',
|
|
305
|
-
costDisplayDesc: 'Show costs estimated at current rates in the stats line and
|
|
326
|
+
costDisplayDesc: 'Show costs estimated at current rates in the stats line, trend tooltips and the turn cost chip.',
|
|
306
327
|
costTitle: 'Estimated at current rates over historical usage, hourly precision',
|
|
307
328
|
costUnpriced: '{n} hour buckets unpriced',
|
|
308
329
|
statsCostTitle: 'Estimated at current rates over session token totals',
|
|
309
330
|
'stats.cost': 'Cost ≈ {cost}',
|
|
310
|
-
'stats.turnCost': '{summary} · Cost ≈ {cost}',
|
|
311
331
|
turnCostTitle: 'Per-turn usage estimated at current rates',
|
|
312
332
|
turnTokensUnreported: 'Not reported by this provider',
|
|
313
333
|
pricing: 'Pricing rules',
|
|
@@ -320,8 +340,8 @@ const MESSAGES_EN = {
|
|
|
320
340
|
priceOutput: 'Output',
|
|
321
341
|
priceCacheRead: 'Cache read',
|
|
322
342
|
priceCacheWrite: 'Cache write',
|
|
323
|
-
noCondition: 'No condition = always applies',
|
|
324
343
|
addCondition: 'Add condition',
|
|
344
|
+
confirmDelete: 'Confirm delete',
|
|
325
345
|
deleteCondition: 'Remove condition',
|
|
326
346
|
condKind: 'Condition kind',
|
|
327
347
|
condDailyWindow: 'Daily window',
|
|
@@ -341,9 +361,17 @@ const MESSAGES_EN = {
|
|
|
341
361
|
condWeekday: 'Requires integer 0-6',
|
|
342
362
|
condMonthDay: 'Requires integer 1-31',
|
|
343
363
|
condDate: 'Requires YYYY-MM-DD',
|
|
344
|
-
condRange: '
|
|
345
|
-
|
|
346
|
-
|
|
364
|
+
condRange: 'End must not be before start (both ends inclusive)',
|
|
365
|
+
ruleOrderHint: '附加计费规则从上到下匹配,首个命中生效;全不命中落默认价',
|
|
366
|
+
moveUp: 'Move up',
|
|
367
|
+
moveDown: 'Move down',
|
|
368
|
+
addGroup: 'Add model',
|
|
369
|
+
addRule: 'Add extra pricing rule',
|
|
370
|
+
addDefaultPrice: 'Add default price',
|
|
371
|
+
deleteGroup: 'Delete model and all its pricing rules',
|
|
372
|
+
deleteRule: 'Remove pricing rule',
|
|
373
|
+
addCondition: 'Add condition',
|
|
374
|
+
defaultPriceHint: 'Default price: applies when no extra rule above matches; no conditions',
|
|
347
375
|
save: 'Save',
|
|
348
376
|
saved: 'Saved',
|
|
349
377
|
required: 'Required',
|
|
@@ -383,13 +411,15 @@ function formatCompact(value) {
|
|
|
383
411
|
function formatPercent(value) {
|
|
384
412
|
return (Math.round(value * 10) / 10).toFixed(1) + '%'
|
|
385
413
|
}
|
|
414
|
+
// tooltip 数值缺席占位
|
|
415
|
+
const TOOLTIP_MISSING = '—'
|
|
386
416
|
function cacheRate(hit, miss) {
|
|
387
417
|
const total = hit + miss
|
|
388
418
|
return total <= 0 ? null : (hit / total) * 100
|
|
389
419
|
}
|
|
390
420
|
function cacheRateText(hit, miss) {
|
|
391
421
|
const rate = cacheRate(hit, miss)
|
|
392
|
-
return rate === null ?
|
|
422
|
+
return rate === null ? TOOLTIP_MISSING : formatPercent(rate)
|
|
393
423
|
}
|
|
394
424
|
|
|
395
425
|
// 模型键展示分段与定价/存储口径同源:首个 / 前 vendor 段,余为模型段
|
|
@@ -417,6 +447,15 @@ function minuteTickLabel(key) {
|
|
|
417
447
|
return time === MIDNIGHT_TIME ? `${shortDay(key.slice(0, DAY_KEY_LENGTH))} ${time}` : time
|
|
418
448
|
}
|
|
419
449
|
|
|
450
|
+
// 悬浮窗槽标签:完整日期去 T 分隔,小时以 h 后缀标定(避免 i18n 单位问题),分钟保留 HH:MM
|
|
451
|
+
function hourSlotLabel(key) {
|
|
452
|
+
return `${key.slice(0, DAY_KEY_LENGTH)} ${key.slice(DAY_KEY_LENGTH + 1)}h`
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function minuteSlotLabel(key) {
|
|
456
|
+
return `${key.slice(0, DAY_KEY_LENGTH)} ${key.slice(DAY_KEY_LENGTH + 1)}`
|
|
457
|
+
}
|
|
458
|
+
|
|
420
459
|
function isEmptyRange(value) {
|
|
421
460
|
return value.tokens === 0 && value.cacheHit === 0 && value.requests === 0 && value.turns === 0
|
|
422
461
|
}
|
|
@@ -488,6 +527,13 @@ const BAR_WIDTH_RATIO = 0.62
|
|
|
488
527
|
const BAR_MIN_WIDTH = 3
|
|
489
528
|
const BAR_MAX_WIDTH = 30
|
|
490
529
|
const AXIS_TICK_COUNT = 4
|
|
530
|
+
// 速度刻度上限钳底:全零或无速度防除零(速度不设轴,读数走 tooltip)
|
|
531
|
+
const SPEED_SCALE_FLOOR = 1
|
|
532
|
+
// 轴上限留白系数:数据峰不顶满绘图区,顶部留出标注空间
|
|
533
|
+
const AXIS_SCALE_HEADROOM = 1.1
|
|
534
|
+
// 图例键:折线项与模型项共处同一显隐集合
|
|
535
|
+
const LEGEND_KEY_RATE = 'rate'
|
|
536
|
+
const LEGEND_KEY_SPEED = 'speed'
|
|
491
537
|
|
|
492
538
|
function niceTicks(max, count) {
|
|
493
539
|
if (max <= 0 || count <= 0) return []
|
|
@@ -500,14 +546,43 @@ function niceTicks(max, count) {
|
|
|
500
546
|
return ticks
|
|
501
547
|
}
|
|
502
548
|
|
|
503
|
-
//
|
|
549
|
+
// 图例显隐切换:current 为 null 表示全部可见;普通点击单选/再点恢复,
|
|
550
|
+
// ctrl 单项切换,隐藏最后一项为无操作(返回原引用)
|
|
551
|
+
function legendToggle(current, key, ctrl) {
|
|
552
|
+
if (ctrl) {
|
|
553
|
+
const next = new Set(current ?? [])
|
|
554
|
+
if (next.has(key)) next.delete(key)
|
|
555
|
+
else next.add(key)
|
|
556
|
+
return next.size === 0 ? current : next
|
|
557
|
+
}
|
|
558
|
+
const solo = current !== null && current.size === 1 && current.has(key)
|
|
559
|
+
return solo ? null : new Set([key])
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// 左轴刻度:速度刻度存在时左轴标定速度(tok/s),否则标定 token;
|
|
563
|
+
// 刻度值统一换算为绘图区高度占比
|
|
564
|
+
function leftAxisTicks(tokenTicks, tokenMax, speedTicks, speedMax) {
|
|
565
|
+
const useSpeed = speedTicks.length > 0
|
|
566
|
+
return (useSpeed ? speedTicks : tokenTicks).map((tick) => ({
|
|
567
|
+
label: formatCompact(tick),
|
|
568
|
+
ratio: tick / (useSpeed ? speedMax : tokenMax),
|
|
569
|
+
}))
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// 堆叠柱几何:模型序即堆叠序(哨兵最后画柱顶),输出槽分段与左轴刻度;
|
|
573
|
+
// maxTotal 按可见模型求和,单选模型时刻度跟随归一
|
|
504
574
|
function trendLayout(slots, modelOrder, avail, labelMinPitch) {
|
|
505
575
|
const plotHeight = CHART_HEIGHT - CHART_PAD.top - CHART_PAD.bottom
|
|
506
576
|
const innerWidth = Math.max(1, avail - CHART_PAD.left - CHART_PAD.right)
|
|
507
577
|
const count = slots.length
|
|
508
578
|
const step = count > 1 ? innerWidth / (count - 1) : innerWidth
|
|
509
579
|
const barWidth = Math.max(BAR_MIN_WIDTH, Math.min(BAR_MAX_WIDTH, step * BAR_WIDTH_RATIO))
|
|
510
|
-
const
|
|
580
|
+
const slotVisibleTotal = (slot) => modelOrder.reduce((sum, model) => sum + (slot.byModel[model] ?? 0), 0)
|
|
581
|
+
const maxTotal = Math.max(1, ...slots.map(slotVisibleTotal))
|
|
582
|
+
// 轴上限 = 数据峰 × 留白系数,柱高分母与左轴刻度分母同源
|
|
583
|
+
const scaleMax = maxTotal * AXIS_SCALE_HEADROOM
|
|
584
|
+
// 可见模型无任何数据时左轴无标定对象,空刻度防钳底值漏成假刻度
|
|
585
|
+
const visibleTotal = slots.reduce((sum, slot) => sum + slotVisibleTotal(slot), 0)
|
|
511
586
|
const bars = slots.map((slot, index) => {
|
|
512
587
|
const centerX = CHART_PAD.left + barWidth / 2 + index * step
|
|
513
588
|
let yBottom = CHART_PAD.top + plotHeight
|
|
@@ -515,7 +590,7 @@ function trendLayout(slots, modelOrder, avail, labelMinPitch) {
|
|
|
515
590
|
for (const model of modelOrder) {
|
|
516
591
|
const tokens = slot.byModel[model] ?? 0
|
|
517
592
|
if (tokens === 0) continue
|
|
518
|
-
const height = (tokens /
|
|
593
|
+
const height = (tokens / scaleMax) * plotHeight
|
|
519
594
|
yBottom -= height
|
|
520
595
|
segments.push({ model, y: yBottom, height })
|
|
521
596
|
}
|
|
@@ -528,7 +603,8 @@ function trendLayout(slots, modelOrder, avail, labelMinPitch) {
|
|
|
528
603
|
step,
|
|
529
604
|
barWidth,
|
|
530
605
|
maxTotal,
|
|
531
|
-
|
|
606
|
+
scaleMax,
|
|
607
|
+
ticks: visibleTotal === 0 ? [] : niceTicks(maxTotal, AXIS_TICK_COUNT),
|
|
532
608
|
labelEvery: Math.max(1, Math.ceil(labelMinPitch / step)),
|
|
533
609
|
bars,
|
|
534
610
|
}
|
|
@@ -560,6 +636,20 @@ function trendRatePoints(slots, bars, plotHeight) {
|
|
|
560
636
|
return points
|
|
561
637
|
}
|
|
562
638
|
|
|
639
|
+
// 速度曲线点:仅带速度槽产出,高度按刻度上限归一
|
|
640
|
+
function trendSpeedPoints(slots, bars, plotHeight, scaleMax) {
|
|
641
|
+
const points = []
|
|
642
|
+
slots.forEach((slot, index) => {
|
|
643
|
+
if (slot.speed === undefined) return
|
|
644
|
+
points.push({
|
|
645
|
+
day: slot.day,
|
|
646
|
+
x: bars[index].x,
|
|
647
|
+
y: CHART_PAD.top + plotHeight - (slot.speed / scaleMax) * plotHeight,
|
|
648
|
+
})
|
|
649
|
+
})
|
|
650
|
+
return points
|
|
651
|
+
}
|
|
652
|
+
|
|
563
653
|
// Catmull-Rom 转三次贝塞尔:控制点取邻点差六分之一,端点折返
|
|
564
654
|
function smoothPath(points) {
|
|
565
655
|
if (points.length === 0) return ''
|
|
@@ -619,6 +709,16 @@ function modelSpeedText(speed) {
|
|
|
619
709
|
return `${formatTokensPerSecond(speed)} tok/s`
|
|
620
710
|
}
|
|
621
711
|
|
|
712
|
+
// tooltip 速度行文本:无速度与命中率同款占位符
|
|
713
|
+
function speedTipText(speed) {
|
|
714
|
+
return speed === undefined ? TOOLTIP_MISSING : modelSpeedText(speed)
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// 速度刻度上限:全零或无速度钳底,防除零
|
|
718
|
+
function speedScaleMax(slots) {
|
|
719
|
+
return Math.max(SPEED_SCALE_FLOOR, ...slots.map((slot) => slot.speed ?? 0))
|
|
720
|
+
}
|
|
721
|
+
|
|
622
722
|
// 热力图:窗口固定 26 周,与所选范围无关
|
|
623
723
|
const HEAT_WEEKS = 26
|
|
624
724
|
const HEAT_ROW_COUNT = 7
|
|
@@ -676,10 +776,13 @@ function heatLevel(tokens, max) {
|
|
|
676
776
|
return tokens === 0 ? 0 : 1 + Math.floor((tokens / max) * HEAT_LEVEL_BANDS)
|
|
677
777
|
}
|
|
678
778
|
|
|
679
|
-
// ChartTip
|
|
779
|
+
// ChartTip 定位:锚定区装得下贴内顶(趋势图悬停区即绘图区,悬浮窗留在图表内),装不下上翻、再下翻、末了钳边界顶
|
|
680
780
|
const TIP_GAP_PX = 8
|
|
681
781
|
const TIP_MARGIN_PX = 8
|
|
682
782
|
|
|
783
|
+
// 回合费用芯片与前置官方芯片(结束时钟)的间距
|
|
784
|
+
const TURN_COST_GAP_PX = 8
|
|
785
|
+
|
|
683
786
|
function tipPlace(anchor, tip, bounds, gap = TIP_GAP_PX, margin = TIP_MARGIN_PX) {
|
|
684
787
|
if (!tip || tip.width <= 0 || tip.height <= 0) return null
|
|
685
788
|
if (!anchor || (anchor.left === 0 && anchor.top === 0 && anchor.right === 0 && anchor.bottom === 0)) return null
|
|
@@ -688,11 +791,13 @@ function tipPlace(anchor, tip, bounds, gap = TIP_GAP_PX, margin = TIP_MARGIN_PX)
|
|
|
688
791
|
const maxX = bounds.right - margin
|
|
689
792
|
const maxY = bounds.bottom - margin
|
|
690
793
|
const left = Math.max(minX, Math.min((anchor.left + anchor.right) / 2 - tip.width / 2, maxX - tip.width))
|
|
691
|
-
const
|
|
794
|
+
const inside = anchor.top + gap
|
|
692
795
|
const above = anchor.top - gap - tip.height
|
|
796
|
+
const below = anchor.bottom + gap
|
|
693
797
|
let top
|
|
694
|
-
if (
|
|
798
|
+
if (inside + tip.height <= anchor.bottom) top = inside
|
|
695
799
|
else if (above >= minY) top = above
|
|
800
|
+
else if (below + tip.height <= maxY) top = below
|
|
696
801
|
else top = minY
|
|
697
802
|
return { left, top }
|
|
698
803
|
}
|
|
@@ -976,15 +1081,14 @@ const toMinutesOfDay = (hhmm) => {
|
|
|
976
1081
|
return Number.isFinite(h) && Number.isFinite(m) ? h * MINUTES_PER_HOUR + m : Number.NaN
|
|
977
1082
|
}
|
|
978
1083
|
|
|
979
|
-
// from
|
|
1084
|
+
// 所有范围条件统一双侧包含;from>to 跨午夜/跨月环绕;from===to 单点/单日(与宿主 pricing.js 镜像,parity 测试锁定)
|
|
980
1085
|
function dailyWindowMatches(condition, date) {
|
|
981
1086
|
const from = toMinutesOfDay(condition.from)
|
|
982
1087
|
const to = toMinutesOfDay(condition.to)
|
|
983
1088
|
if (Number.isNaN(from) || Number.isNaN(to)) return false
|
|
984
1089
|
const m = minutesOfDay(date)
|
|
985
|
-
if (from
|
|
986
|
-
|
|
987
|
-
return true
|
|
1090
|
+
if (from <= to) return m >= from && m <= to
|
|
1091
|
+
return m >= from || m <= to
|
|
988
1092
|
}
|
|
989
1093
|
|
|
990
1094
|
// days 空数组不成立;0=周日,取 getDay()
|
|
@@ -993,7 +1097,7 @@ function weekdaysMatches(condition, date) {
|
|
|
993
1097
|
return Array.isArray(days) && days.length > 0 && days.includes(date.getDay())
|
|
994
1098
|
}
|
|
995
1099
|
|
|
996
|
-
//
|
|
1100
|
+
// 号段双侧包含;from>to 跨月环绕;from===to 单日,2 月无 31 号自然不触发
|
|
997
1101
|
function monthDaysMatches(condition, date) {
|
|
998
1102
|
const { from, to } = condition
|
|
999
1103
|
if (!Number.isInteger(from) || !Number.isInteger(to)) return false
|
|
@@ -1001,7 +1105,7 @@ function monthDaysMatches(condition, date) {
|
|
|
1001
1105
|
return from <= to ? d >= from && d <= to : d >= from || d <= to
|
|
1002
1106
|
}
|
|
1003
1107
|
|
|
1004
|
-
//
|
|
1108
|
+
// 零填充 YYYY-MM-DD 字典序双侧包含;from>to 配置错误不成立(编辑器校验拦截)
|
|
1005
1109
|
const ISO_DAY_PATTERN = /^\d{4}-\d{2}-\d{2}$/
|
|
1006
1110
|
function dateRangeMatches(condition, date) {
|
|
1007
1111
|
const { from, to } = condition
|
|
@@ -1153,29 +1257,55 @@ function costTitleText(t, unpriced) {
|
|
|
1153
1257
|
return unpriced > 0 ? `${base},${t('costUnpriced', { n: unpriced })}` : base
|
|
1154
1258
|
}
|
|
1155
1259
|
|
|
1156
|
-
// ===== 注入点B
|
|
1157
|
-
//
|
|
1158
|
-
const
|
|
1159
|
-
const TURN_TAIL_PRIORITY = 1
|
|
1160
|
-
const TURN_COST_REVEAL_MS = 80
|
|
1161
|
-
const TURN_TAIL_ACTIONS_INSET_PX = -6
|
|
1260
|
+
// ===== 注入点B:回合费用芯片(官方动作行 assistant-actions 槽条目,赞踩/上下文跳转同排) =====
|
|
1261
|
+
// 排序取上下文插件条目(20)之后,贴近尾部用量/用时芯片一侧
|
|
1262
|
+
const TURN_COST_CHIP_ORDER = 20 + 10
|
|
1162
1263
|
|
|
1163
|
-
//
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1264
|
+
// messageId 反查:节点表为 chat 节点仓库(values() 可枚举,Map/仓库两态兼容);
|
|
1265
|
+
// 回合级用量优先取回合位置数据(location.turn.data.get('turn-tail')).tokenUsage(官方 tokenUsage 聚合,
|
|
1266
|
+
// 分页窗口缺 turn/start 时缺席),回退视图节点 data.closing.usage(末步用量采样,输入侧已含缓存,计费口径同源)
|
|
1267
|
+
function turnTokenUsageOfMessage(nodes, messageId) {
|
|
1268
|
+
const list = nodes && typeof nodes.values === 'function' ? [...nodes.values()] : nodes
|
|
1269
|
+
if (!Array.isArray(list)) return null
|
|
1270
|
+
for (const node of list) {
|
|
1271
|
+
try {
|
|
1272
|
+
if (node?.kind !== 'turn-tail') continue
|
|
1273
|
+
if (node.data?.closing?.finalNode?.messageId !== messageId) continue
|
|
1274
|
+
const turnData = node.location?.turn?.data
|
|
1275
|
+
const tail = turnData && typeof turnData.get === 'function' ? turnData.get('turn-tail') : null
|
|
1276
|
+
if (tail?.tokenUsage) return tail.tokenUsage
|
|
1277
|
+
if (node.data.tokenUsage) return node.data.tokenUsage
|
|
1278
|
+
const sampled = node.data?.closing?.usage
|
|
1279
|
+
if (!sampled) return null
|
|
1280
|
+
return {
|
|
1281
|
+
uncachedInputTokens: sampled.inputTokens,
|
|
1282
|
+
outputTokens: sampled.outputTokens,
|
|
1283
|
+
totalTokens: sampled.totalTokens,
|
|
1284
|
+
...sampled.cacheReadTokens === undefined ? {} : { cacheReadTokens: sampled.cacheReadTokens },
|
|
1285
|
+
...sampled.cacheWriteTokens === undefined ? {} : { cacheWriteTokens: sampled.cacheWriteTokens },
|
|
1286
|
+
...sampled.reasoningTokens === undefined ? {} : { reasoningTokens: sampled.reasoningTokens },
|
|
1287
|
+
}
|
|
1288
|
+
} catch { /* 单节点形状残缺跳过,扫描继续 */ }
|
|
1289
|
+
}
|
|
1290
|
+
return null
|
|
1168
1291
|
}
|
|
1169
1292
|
|
|
1170
|
-
// 计价模型键:routes
|
|
1171
|
-
|
|
1293
|
+
// 计价模型键:routes 首条按官方 messageRoute 分离字段拼两段键,与采集器 refOf 双实现同源
|
|
1294
|
+
// (官方 source.provider/model 是分离字段,routes[].model 是裸模型名,展示侧才拼 provider),
|
|
1295
|
+
// 仅 model 用裸名命中 */model 档,双缺回退全通配键;多 route 取首条(单轮估算口径)
|
|
1296
|
+
const turnModelOf = (tokenUsage) => {
|
|
1297
|
+
const route = tokenUsage?.routes?.[0]
|
|
1298
|
+
if (!route) return MODEL_UNROUTED
|
|
1299
|
+
if (route.provider && route.model) return `${route.provider}/${route.model}`
|
|
1300
|
+
return route.model || MODEL_UNROUTED
|
|
1301
|
+
}
|
|
1172
1302
|
|
|
1173
1303
|
// 可选桶(cacheRead/cacheWrite)仅部分 provider 上报,缺失按 0 计入摘要与费用
|
|
1174
1304
|
const turnReportedBucket = (value) => (value ?? 0)
|
|
1175
1305
|
|
|
1176
|
-
// 摘要计费输入 = prompt 侧三桶(官方 billing 分母口径)
|
|
1306
|
+
// 摘要计费输入 = prompt 侧三桶(官方 billing 分母口径),核心桶缺省同按 0 保证降级形态对称
|
|
1177
1307
|
function turnBilledInputTokens(tokenUsage) {
|
|
1178
|
-
return tokenUsage.uncachedInputTokens
|
|
1308
|
+
return turnReportedBucket(tokenUsage.uncachedInputTokens)
|
|
1179
1309
|
+ turnReportedBucket(tokenUsage.cacheReadTokens)
|
|
1180
1310
|
+ turnReportedBucket(tokenUsage.cacheWriteTokens)
|
|
1181
1311
|
}
|
|
@@ -1185,33 +1315,44 @@ function turnOptionalUnreported(tokenUsage) {
|
|
|
1185
1315
|
return tokenUsage?.cacheReadTokens === undefined || tokenUsage?.cacheWriteTokens === undefined
|
|
1186
1316
|
}
|
|
1187
1317
|
|
|
1188
|
-
//
|
|
1189
|
-
function
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
})
|
|
1194
|
-
const cost = price ? formatCost(costOf(price, pricingBucketsOf(tokenUsage)), currency) : COST_PLACEHOLDER
|
|
1195
|
-
return t('stats.turnCost', { summary, cost })
|
|
1318
|
+
// 芯片计费额:价命中才算,0 元(全零价或全零桶)返回 null 由调用方决定不渲染;价未命中同 null
|
|
1319
|
+
function turnCostAmountOf(tokenUsage, price) {
|
|
1320
|
+
if (!price) return null
|
|
1321
|
+
const cost = costOf(price, pricingBucketsOf(tokenUsage))
|
|
1322
|
+
return cost > 0 ? cost : null
|
|
1196
1323
|
}
|
|
1197
1324
|
|
|
1198
|
-
//
|
|
1325
|
+
// 芯片文本:仅金额,不带货币前缀文字(官方用量芯片弹窗已有 token 明细)
|
|
1326
|
+
function buildTurnCostChipText(t, tokenUsage, price, currency) {
|
|
1327
|
+
const cost = turnCostAmountOf(tokenUsage, price)
|
|
1328
|
+
return t('turnCostChip', { cost: cost === null ? COST_PLACEHOLDER : formatCost(cost, currency) })
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// 芯片 title:token 摘要 + 估算口径,可选桶未上报时追加标注
|
|
1199
1332
|
function turnCostTitleText(t, tokenUsage) {
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1333
|
+
const notes = [
|
|
1334
|
+
t('stats.tokens', {
|
|
1335
|
+
input: formatTokensCompact(turnBilledInputTokens(tokenUsage), t),
|
|
1336
|
+
output: formatTokensCompact(turnReportedBucket(tokenUsage.outputTokens), t),
|
|
1337
|
+
}),
|
|
1338
|
+
t('turnCostTitle'),
|
|
1339
|
+
]
|
|
1340
|
+
if (turnOptionalUnreported(tokenUsage)) notes.push(t('turnTokensUnreported'))
|
|
1341
|
+
return notes.join(',')
|
|
1202
1342
|
}
|
|
1203
1343
|
|
|
1204
1344
|
// ===== 定价编辑器纯函数(校验/规整/默认值) =====
|
|
1205
1345
|
const PRICE_KEYS = ['input', 'output', 'cacheRead', 'cacheWrite']
|
|
1206
1346
|
const HHMM_PATTERN = /^\d{1,2}:\d{2}$/
|
|
1207
1347
|
const ISO_DAY_PATTERN_CLIENT = /^\d{4}-\d{2}-\d{2}$/
|
|
1208
|
-
//
|
|
1209
|
-
const HOUR_MAX =
|
|
1348
|
+
// 时刻分量界:小时/分钟均双闭
|
|
1349
|
+
const HOUR_MAX = 23
|
|
1210
1350
|
const MINUTE_MAX = 59
|
|
1211
1351
|
const WEEKDAY_MIN = 0
|
|
1212
1352
|
const WEEKDAY_MAX = 6
|
|
1213
1353
|
const MONTH_DAY_MIN = 1
|
|
1214
1354
|
const MONTH_DAY_MAX = 31
|
|
1355
|
+
const FULL_DAY_WINDOW = { from: '00:00', to: '23:59' }
|
|
1215
1356
|
const CONDITION_KIND_OPTIONS = ['dailyWindow', 'weekdays', 'monthDays', 'dateRange']
|
|
1216
1357
|
const CONDITION_KIND_LABEL_KEYS = {
|
|
1217
1358
|
dailyWindow: 'condDailyWindow',
|
|
@@ -1224,18 +1365,18 @@ const WEEKDAY_COUNT = 7
|
|
|
1224
1365
|
const parseHHMM = (value) => {
|
|
1225
1366
|
if (typeof value !== 'string' || !HHMM_PATTERN.test(value)) return null
|
|
1226
1367
|
const [hours, minutes] = value.split(':').map(Number)
|
|
1227
|
-
return hours >= 0 && hours
|
|
1368
|
+
return hours >= 0 && hours <= HOUR_MAX && minutes >= 0 && minutes <= MINUTE_MAX ? value : null
|
|
1228
1369
|
}
|
|
1229
1370
|
|
|
1230
1371
|
const isValidMonthDay = (value) => Number.isInteger(value) && value >= MONTH_DAY_MIN && value <= MONTH_DAY_MAX
|
|
1231
1372
|
|
|
1232
1373
|
const isValidWeekday = (value) => Number.isInteger(value) && value >= WEEKDAY_MIN && value <= WEEKDAY_MAX
|
|
1233
1374
|
|
|
1234
|
-
//
|
|
1375
|
+
// 各条件类型合法默认即填即用:时段全天、周几空、号段全月、日期段当天单日
|
|
1235
1376
|
const defaultCondition = (kind, now = new Date()) => {
|
|
1236
1377
|
const today = formatDate(now)
|
|
1237
1378
|
const defaults = {
|
|
1238
|
-
dailyWindow: { kind: 'dailyWindow',
|
|
1379
|
+
dailyWindow: { kind: 'dailyWindow', ...FULL_DAY_WINDOW },
|
|
1239
1380
|
weekdays: { kind: 'weekdays', days: [] },
|
|
1240
1381
|
monthDays: { kind: 'monthDays', from: MONTH_DAY_MIN, to: MONTH_DAY_MAX },
|
|
1241
1382
|
dateRange: { kind: 'dateRange', from: today, to: today },
|
|
@@ -1243,15 +1384,18 @@ const defaultCondition = (kind, now = new Date()) => {
|
|
|
1243
1384
|
return defaults[kind] ? { ...defaults[kind] } : null
|
|
1244
1385
|
}
|
|
1245
1386
|
|
|
1246
|
-
// 条件字段级校验:路径前缀 + 键 →
|
|
1387
|
+
// 条件字段级校验:路径前缀 + 键 → 错误键;时段/号段倒序为跨午夜/跨月语义;
|
|
1388
|
+
// 所有范围双侧包含,from===to 单点/单日合法;仅日期段倒序属配置错误(condRange)
|
|
1247
1389
|
const validateCondition = (condition, path, errors) => {
|
|
1248
1390
|
if (!condition || typeof condition !== 'object') {
|
|
1249
1391
|
errors.set(path, 'required')
|
|
1250
1392
|
return
|
|
1251
1393
|
}
|
|
1252
1394
|
if (condition.kind === 'dailyWindow') {
|
|
1253
|
-
|
|
1254
|
-
|
|
1395
|
+
const from = parseHHMM(condition.from)
|
|
1396
|
+
const to = parseHHMM(condition.to)
|
|
1397
|
+
if (from === null) errors.set(`${path}.from`, condition.from === '' || condition.from == null ? 'required' : 'condTime')
|
|
1398
|
+
if (to === null) errors.set(`${path}.to`, condition.to === '' || condition.to == null ? 'required' : 'condTime')
|
|
1255
1399
|
return
|
|
1256
1400
|
}
|
|
1257
1401
|
if (condition.kind === 'weekdays') {
|
|
@@ -1326,6 +1470,58 @@ const defaultPricingRule = (currency = CURRENCIES[0]) => ({
|
|
|
1326
1470
|
|
|
1327
1471
|
const patchItemAt = (array, index, patch) => array.map((item, i) => (i === index ? { ...item, ...patch } : item))
|
|
1328
1472
|
|
|
1473
|
+
// 编辑器分组视图纯函数:wire 契约(平面数组)不变,组/槽位仅是展示层投影。
|
|
1474
|
+
// 槽位移动/增删均落回平面数组的对应位置变换,组内相对顺序即匹配优先序。
|
|
1475
|
+
|
|
1476
|
+
// 按模型键聚合:组序为首次出现序,槽位保序并携带平面索引(错误路径与移动操作的寻址键)
|
|
1477
|
+
function groupRulesOf(rules) {
|
|
1478
|
+
const groups = new Map()
|
|
1479
|
+
;(rules ?? []).forEach((rule, index) => {
|
|
1480
|
+
const key = rule.model
|
|
1481
|
+
if (!groups.has(key)) groups.set(key, { model: key, slots: [] })
|
|
1482
|
+
groups.get(key).slots.push({ rule, index })
|
|
1483
|
+
})
|
|
1484
|
+
return [...groups.values()]
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// 组内划分:末条无条件规则即该模型的默认价(恒兜底,UI 禁条件/排序/删除),其余为附加计费规则。
|
|
1488
|
+
// 全条件组默认价为 null(UI 提供"添加默认价"入口);多条无条件时末条为默认价,存量数据自然收敛
|
|
1489
|
+
function partitionGroupOf(group) {
|
|
1490
|
+
const last = group.slots[group.slots.length - 1]
|
|
1491
|
+
const isDefault = last !== undefined && (last.rule.conditions ?? []).length === 0
|
|
1492
|
+
return {
|
|
1493
|
+
defaultSlot: isDefault ? last : null,
|
|
1494
|
+
extras: isDefault ? group.slots.slice(0, -1) : group.slots,
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
// 槽位跨位移动(splice 语义):同位或越界返回原引用
|
|
1499
|
+
function moveRuleTo(rules, fromIndex, toIndex) {
|
|
1500
|
+
if (!Array.isArray(rules) || fromIndex === toIndex) return rules
|
|
1501
|
+
if (fromIndex < 0 || fromIndex >= rules.length || toIndex < 0 || toIndex >= rules.length) return rules
|
|
1502
|
+
const moved = [...rules]
|
|
1503
|
+
const [item] = moved.splice(fromIndex, 1)
|
|
1504
|
+
moved.splice(toIndex, 0, item)
|
|
1505
|
+
return moved
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// 组级改名:仅命中索引集的规则变更 model,其余保持引用不变
|
|
1509
|
+
function renameRulesAt(rules, indexes, model) {
|
|
1510
|
+
const hit = new Set(indexes)
|
|
1511
|
+
return rules.map((rule, i) => (hit.has(i) ? { ...rule, model } : rule))
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// 插入到目标平面位之后(组内添加槽位 = 组内末槽位索引 + 1)
|
|
1515
|
+
function insertRuleAt(rules, position, rule) {
|
|
1516
|
+
return [...rules.slice(0, position + 1), rule, ...rules.slice(position + 1)]
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// 批量移除(组删除)
|
|
1520
|
+
function removeRulesAt(rules, indexes) {
|
|
1521
|
+
const hit = new Set(indexes)
|
|
1522
|
+
return rules.filter((_, i) => !hit.has(i))
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1329
1525
|
|
|
1330
1526
|
if (typeof window !== 'undefined' && window.__ModuleLoader__) {
|
|
1331
1527
|
window.__ModuleLoader__.load({ id: '@mzzsfy/dsh-usage-dash', factory })
|
|
@@ -1460,6 +1656,7 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
|
|
|
1460
1656
|
model: ['M5 5h14v14H5Z', 'M9 9h6v6H9Z', 'M9 2v3', 'M15 2v3', 'M9 19v3', 'M15 19v3', 'M2 9h3', 'M2 15h3', 'M19 9h3', 'M19 15h3'],
|
|
1461
1657
|
rate: ['M22 12h-4l-3 9L9 3l-3 9H2'],
|
|
1462
1658
|
days: ['M5 5h14a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2Z', 'M16 3v4', 'M8 3v4', 'M3 11h18'],
|
|
1659
|
+
trash: ['M3 6h18', 'M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6', 'M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2', 'M10 11v6', 'M14 11v6'],
|
|
1463
1660
|
}
|
|
1464
1661
|
|
|
1465
1662
|
function Icon({ paths }) {
|
|
@@ -1470,12 +1667,35 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
|
|
|
1470
1667
|
}, paths.map((d, index) => h('path', { key: index, d })))
|
|
1471
1668
|
}
|
|
1472
1669
|
|
|
1670
|
+
// 危险删除按钮:首点进武装态(垃圾桶变"确认删除"文字),再点才执行,超时自动解除
|
|
1671
|
+
function DeleteArmedButton({ labelKey, confirmKey, onConfirm, t = defaultT }) {
|
|
1672
|
+
const [armed, setArmed] = useState(false)
|
|
1673
|
+
const armedTimerRef = useRef(null)
|
|
1674
|
+
useEffect(() => () => {
|
|
1675
|
+
if (armedTimerRef.current) clearTimeout(armedTimerRef.current)
|
|
1676
|
+
}, [])
|
|
1677
|
+
const click = () => {
|
|
1678
|
+
if (!armed) {
|
|
1679
|
+
setArmed(true)
|
|
1680
|
+
armedTimerRef.current = setTimeout(() => setArmed(false), REBUILD_CONFIRM_MS)
|
|
1681
|
+
return
|
|
1682
|
+
}
|
|
1683
|
+
if (armedTimerRef.current) clearTimeout(armedTimerRef.current)
|
|
1684
|
+
setArmed(false)
|
|
1685
|
+
onConfirm()
|
|
1686
|
+
}
|
|
1687
|
+
return h('button', {
|
|
1688
|
+
className: cx('ud-btn ud-btn--text', armed && 'ud-delete-armed'), type: 'button', onClick: click,
|
|
1689
|
+
'aria-label': t(labelKey), title: t(labelKey),
|
|
1690
|
+
}, armed ? t(confirmKey) : h(Icon, { paths: ICONS.trash }))
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1473
1693
|
const STYLE_CSS = `
|
|
1474
1694
|
.ud-panel{display:flex;flex-direction:column;gap:12px;font-size:13px;color:var(--dsw-alias-label-primary);
|
|
1475
1695
|
--ud-chart-1:color-mix(in srgb,#0576ff 70%,white);--ud-chart-2:color-mix(in srgb,#2f6f37 70%,white);--ud-chart-3:color-mix(in srgb,#c46212 70%,white);--ud-chart-4:color-mix(in srgb,#975bf1 70%,white);--ud-chart-5:color-mix(in srgb,#d34591 70%,white);--ud-chart-other:color-mix(in srgb,#576270 70%,white);
|
|
1476
|
-
--dsw-heat-0:#ebedf0;--dsw-heat-1:#dbe3ff;--dsw-heat-2:#b7c5ff;--dsw-heat-3:#8ea4ff;--dsw-heat-4:#6884ff;--dsw-heat-5:#4d6bfe;--ud-trend-line:#0576ff}
|
|
1696
|
+
--dsw-heat-0:#ebedf0;--dsw-heat-1:#dbe3ff;--dsw-heat-2:#b7c5ff;--dsw-heat-3:#8ea4ff;--dsw-heat-4:#6884ff;--dsw-heat-5:#4d6bfe;--ud-trend-line:#0576ff;--ud-trend-speed:#0ca678}
|
|
1477
1697
|
body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,white);--ud-chart-2:color-mix(in srgb,#2f6f37 65%,white);--ud-chart-3:color-mix(in srgb,#c46212 65%,white);--ud-chart-4:color-mix(in srgb,#975bf1 65%,white);--ud-chart-5:color-mix(in srgb,#d34591 65%,white);--ud-chart-other:color-mix(in srgb,#576270 65%,white);
|
|
1478
|
-
--dsw-heat-0:#21262d;--dsw-heat-1:#2f4bd0;--dsw-heat-2:#4d6bfe;--dsw-heat-3:#6e8bff;--dsw-heat-4:#93aaff;--dsw-heat-5:#c4d0ff;--ud-trend-line:#4d6bfe}
|
|
1698
|
+
--dsw-heat-0:#21262d;--dsw-heat-1:#2f4bd0;--dsw-heat-2:#4d6bfe;--dsw-heat-3:#6e8bff;--dsw-heat-4:#93aaff;--dsw-heat-5:#c4d0ff;--ud-trend-line:#4d6bfe;--ud-trend-speed:#2fbf8f}
|
|
1479
1699
|
.ud-toolbar{display:flex;align-items:flex-start;gap:8px}
|
|
1480
1700
|
.ud-toolbar-main{display:flex;align-items:center;gap:8px;flex-wrap:wrap;flex:1 1 auto;min-width:0}
|
|
1481
1701
|
.ud-group{display:flex;align-items:center;gap:2px;padding:3px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-1)}
|
|
@@ -1493,6 +1713,7 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
1493
1713
|
.ud-icon-btn{padding:0 12px}
|
|
1494
1714
|
.ud-icon{display:inline-flex;align-items:center;justify-content:center}
|
|
1495
1715
|
.ud-btn--text{border:none;background:transparent;color:var(--dsw-alias-label-tertiary);padding:2px 4px}
|
|
1716
|
+
.ud-delete-armed{color:var(--dsw-alias-state-error-primary);font-size:12px;white-space:nowrap}
|
|
1496
1717
|
.ud-error{border:1px solid var(--dsw-alias-state-warn-primary);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 12%,transparent);color:var(--dsw-alias-state-warn-label);border-radius:8px;padding:8px 12px;font-size:12px}
|
|
1497
1718
|
.ud-loading{color:var(--dsw-alias-label-tertiary);text-align:center;padding:32px 0}
|
|
1498
1719
|
.ud-empty{border:1px dashed var(--dsw-alias-border-l2);border-radius:8px;color:var(--dsw-alias-label-tertiary);text-align:center;padding:24px 16px;font-size:12px}
|
|
@@ -1535,7 +1756,8 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
1535
1756
|
.ud-grid{stroke:var(--dsw-alias-border-l1);stroke-width:1}
|
|
1536
1757
|
.ud-axis{fill:var(--dsw-alias-label-tertiary);font-size:11px;font-variant-numeric:tabular-nums}
|
|
1537
1758
|
.ud-legend{display:flex;flex-wrap:wrap;gap:4px 12px}
|
|
1538
|
-
.ud-legend-item{display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--dsw-alias-label-secondary);min-width:0}
|
|
1759
|
+
.ud-legend-item{display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--dsw-alias-label-secondary);min-width:0;user-select:none;cursor:pointer}
|
|
1760
|
+
.ud-legend-item--off{opacity:.35}
|
|
1539
1761
|
.ud-legend-item span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
1540
1762
|
.ud-legend-swatch{width:8px;height:8px;border-radius:2px;flex:none}
|
|
1541
1763
|
.ud-heat-wrap{width:100%;min-width:0;overflow:hidden}
|
|
@@ -1558,8 +1780,11 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
1558
1780
|
.ud-bar{transform-box:fill-box;transform-origin:center}
|
|
1559
1781
|
.ud-bar-hit{fill:transparent;pointer-events:all}
|
|
1560
1782
|
.ud-trend{stroke:var(--ud-trend-line);opacity:.9;fill:none;pointer-events:none}
|
|
1783
|
+
.ud-trend--speed{stroke:var(--ud-trend-speed)}
|
|
1561
1784
|
.ud-trend-dot{fill:var(--ud-trend-line);stroke:var(--dsw-alias-bg-layer-1);stroke-width:${TREND_DOT_RING}px;pointer-events:none}
|
|
1785
|
+
.ud-trend-dot--speed{fill:var(--ud-trend-speed)}
|
|
1562
1786
|
.ud-legend-swatch--line{height:2px;border-radius:1px;background:var(--ud-trend-line)}
|
|
1787
|
+
.ud-legend-swatch--line--speed{background:var(--ud-trend-speed)}
|
|
1563
1788
|
.ud-model-usage{display:flex;flex-wrap:wrap;align-items:flex-start;gap:16px}
|
|
1564
1789
|
.ud-donut-wrap{flex:0 0 auto}
|
|
1565
1790
|
.ud-donut-seg{cursor:pointer;outline:none;transition:stroke-width .12s ease}
|
|
@@ -1600,14 +1825,23 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
1600
1825
|
.ud-pref-title{font-size:13px;color:var(--dsw-alias-label-primary)}
|
|
1601
1826
|
.ud-pref-desc{font-size:12px;color:var(--dsw-alias-label-tertiary)}
|
|
1602
1827
|
.ud-rule{display:flex;flex-direction:column;gap:8px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:10px 12px}
|
|
1828
|
+
.ud-rule-group{display:flex;flex-direction:column;gap:8px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px 12px}
|
|
1829
|
+
.ud-rule-group-head{display:flex;align-items:flex-end;gap:8px}
|
|
1830
|
+
.ud-rule-group-head .ud-field{flex:1}
|
|
1831
|
+
.ud-rule-group-slots{display:flex;flex-direction:column;gap:8px}
|
|
1832
|
+
.ud-rule-group-slots .ud-rule{background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 40%,transparent)}
|
|
1833
|
+
.ud-rule-default{border-style:dashed}
|
|
1834
|
+
.ud-slot-head{display:flex;align-items:flex-end;gap:8px}
|
|
1835
|
+
.ud-slot-head .ud-price-grid{flex:1}
|
|
1603
1836
|
.ud-rule-head{display:flex;align-items:flex-end;gap:8px}
|
|
1604
1837
|
.ud-rule-head .ud-field{flex:1}
|
|
1605
1838
|
.ud-rule-cond{font-size:11px;color:var(--dsw-alias-label-tertiary)}
|
|
1606
1839
|
.ud-rule-conds{display:flex;flex-direction:column;gap:6px}
|
|
1607
|
-
.ud-cond{display:flex;align-items:
|
|
1608
|
-
.ud-
|
|
1609
|
-
.ud-cond-
|
|
1610
|
-
.ud-cond-fields
|
|
1840
|
+
.ud-cond{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
|
|
1841
|
+
/* specificity 须高于 .ud-input 的 width:100%,否则类型下拉撑满整行把字段区挤到下一行 */
|
|
1842
|
+
.ud-cond .ud-cond-kind{width:auto;min-width:88px;flex:0 0 auto}
|
|
1843
|
+
.ud-cond-fields{display:flex;align-items:center;gap:6px;flex-wrap:wrap;flex:1;min-width:0}
|
|
1844
|
+
.ud-cond-fields .ud-field{flex-direction:row;align-items:center;gap:4px;flex:0 1 auto}
|
|
1611
1845
|
.ud-cond-fields .ud-input{width:auto}
|
|
1612
1846
|
.ud-cond-add{display:flex;gap:6px;flex-wrap:wrap}
|
|
1613
1847
|
.ud-field{display:flex;flex-direction:column;gap:3px;min-width:0}
|
|
@@ -1625,8 +1859,8 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
1625
1859
|
.ud-rule-add:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
1626
1860
|
.ud-statsline-root{text-align:center;max-width:var(--dsh-chat-content-width);box-sizing:border-box;width:100%;padding:4px calc(var(--dsh-composer-side-clearance) + 16px) 0px;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,0px));color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-overflow:ellipsis;margin:0 auto;display:block;overflow:hidden}
|
|
1627
1861
|
.ud-statsline-sep{color:var(--dsw-alias-separator-primary);margin:0 10px}
|
|
1628
|
-
|
|
1629
|
-
|
|
1862
|
+
/* 芯片 portal 至动作行末尾,历史轮悬停/焦点显隐随父级 data-actions-reveal,无需自绘规则 */
|
|
1863
|
+
.ud-turn-cost{font-size:var(--dsh-content-font-size-secondary,13px);color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;white-space:nowrap;margin-left:${TURN_COST_GAP_PX}px}
|
|
1630
1864
|
`
|
|
1631
1865
|
|
|
1632
1866
|
function ensureStyle(document) {
|
|
@@ -1701,14 +1935,22 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
1701
1935
|
h(FitText, null, String(stats.activeDays))))
|
|
1702
1936
|
}
|
|
1703
1937
|
|
|
1704
|
-
function Legend({ models, colorFor, t = defaultT }) {
|
|
1938
|
+
function Legend({ models, colorFor, speedEnabled = false, isVisible, onItem, t = defaultT }) {
|
|
1939
|
+
const itemProps = (key) => ({
|
|
1940
|
+
className: cx('ud-legend-item', isVisible && !isVisible(key) && 'ud-legend-item--off'),
|
|
1941
|
+
onClick: onItem ? (event) => onItem(key, event.ctrlKey || event.metaKey) : undefined,
|
|
1942
|
+
role: onItem ? 'button' : undefined,
|
|
1943
|
+
})
|
|
1705
1944
|
return h('div', { className: 'ud-legend' },
|
|
1706
|
-
models.map((item) => h('span', { key: item.model,
|
|
1945
|
+
models.map((item) => h('span', { key: item.model, title: item.model === OTHER_MODEL ? t('other') : item.model, ...itemProps(item.model) },
|
|
1707
1946
|
h('i', { className: 'ud-legend-swatch', style: { background: colorFor(item.model) } }),
|
|
1708
1947
|
h('span', null, item.model === OTHER_MODEL ? t('other') : item.model))),
|
|
1709
|
-
h('span', { key: 'hit-rate',
|
|
1948
|
+
h('span', { key: 'hit-rate', title: t('hitRateLegend'), ...itemProps(LEGEND_KEY_RATE) },
|
|
1710
1949
|
h('i', { className: 'ud-legend-swatch ud-legend-swatch--line' }),
|
|
1711
|
-
h('span', null, t('hitRateLegend')))
|
|
1950
|
+
h('span', null, t('hitRateLegend'))),
|
|
1951
|
+
speedEnabled ? h('span', { key: 'speed', title: t('speedLegend'), ...itemProps(LEGEND_KEY_SPEED) },
|
|
1952
|
+
h('i', { className: 'ud-legend-swatch ud-legend-swatch--line ud-legend-swatch--line--speed' }),
|
|
1953
|
+
h('span', null, t('speedLegend'))) : null)
|
|
1712
1954
|
}
|
|
1713
1955
|
|
|
1714
1956
|
const colorForModel = (models) => (model) => {
|
|
@@ -1718,10 +1960,12 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
1718
1960
|
return `var(--ud-chart-${rank})`
|
|
1719
1961
|
}
|
|
1720
1962
|
|
|
1721
|
-
function TrendChart({ title, notes, slots, modelOrder, colorFor, labelFor, labelMinPitch, busy, legendModels, panelRef, costCurrency = '', costEnabled = false, t = defaultT }) {
|
|
1963
|
+
function TrendChart({ title, notes, slots, modelOrder, colorFor, labelFor, slotLabelFor, labelMinPitch, busy, legendModels, panelRef, costCurrency = '', costEnabled = false, t = defaultT }) {
|
|
1722
1964
|
const wrapRef = useRef(null)
|
|
1723
1965
|
const [avail, setAvail] = useState(CHART_NOMINAL_WIDTH)
|
|
1724
1966
|
const [hover, setHover] = useState(null)
|
|
1967
|
+
// 图例显隐:null = 全部可见;键集 = 模型项与折线项(rate/speed)
|
|
1968
|
+
const [visibleKeys, setVisibleKeys] = useState(null)
|
|
1725
1969
|
// prefs 仅用于 hover tooltip 的费用显隐,经 ref 读取:开关切换不触发本组件重渲染,
|
|
1726
1970
|
// 避免宿主设置弹窗滚动锚定被重渲染扰动而跳变
|
|
1727
1971
|
const prefsRef = useRef(statsLineState.get())
|
|
@@ -1735,11 +1979,23 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
1735
1979
|
observer.observe(element)
|
|
1736
1980
|
return () => observer.disconnect()
|
|
1737
1981
|
}, [])
|
|
1738
|
-
const
|
|
1982
|
+
const hasSpeed = slots.some((slot) => slot.speed !== undefined)
|
|
1983
|
+
const visibleSet = visibleKeys ?? new Set([...modelOrder, LEGEND_KEY_RATE, ...(hasSpeed ? [LEGEND_KEY_SPEED] : [])])
|
|
1984
|
+
const visibleModels = modelOrder.filter((model) => visibleSet.has(model))
|
|
1985
|
+
const showRate = visibleSet.has(LEGEND_KEY_RATE)
|
|
1986
|
+
const showSpeed = hasSpeed && visibleSet.has(LEGEND_KEY_SPEED)
|
|
1987
|
+
const layout = trendLayout(slots, visibleModels, avail, labelMinPitch)
|
|
1739
1988
|
const plotRight = CHART_PAD.left + (slots.length - 1) * layout.step + layout.barWidth
|
|
1740
1989
|
const ratePoints = trendRatePoints(slots, layout.bars, layout.plotHeight)
|
|
1990
|
+
const speedMax = speedScaleMax(slots)
|
|
1991
|
+
const speedAxisMax = speedMax * AXIS_SCALE_HEADROOM
|
|
1992
|
+
const speedPoints = showSpeed ? trendSpeedPoints(slots, layout.bars, layout.plotHeight, speedAxisMax) : []
|
|
1993
|
+
// 左轴标定:可见柱有数据标 token;无柱数据且速度线可见标速度(tok/s);否则空
|
|
1994
|
+
const speedAxisTicks = layout.ticks.length === 0 && showSpeed ? niceTicks(speedMax, AXIS_TICK_COUNT) : []
|
|
1995
|
+
const yTicks = leftAxisTicks(layout.ticks, layout.scaleMax, speedAxisTicks, speedAxisMax)
|
|
1741
1996
|
const hoverSlot = hover ? slots[hover.index] : null
|
|
1742
|
-
const hoverRatePoint = hoverSlot ? ratePoints.find((point) => point.day === hoverSlot.day) : null
|
|
1997
|
+
const hoverRatePoint = showRate && hoverSlot ? ratePoints.find((point) => point.day === hoverSlot.day) : null
|
|
1998
|
+
const hoverSpeedPoint = hoverSlot ? speedPoints.find((point) => point.day === hoverSlot.day) : null
|
|
1743
1999
|
const pick = (index) => (event) => setHover({ index, anchor: event.currentTarget })
|
|
1744
2000
|
const clear = () => setHover(null)
|
|
1745
2001
|
const otherEntries = hoverSlot
|
|
@@ -1754,13 +2010,16 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
1754
2010
|
className: 'ud-chart', viewBox: `0 0 ${avail} ${CHART_HEIGHT}`, width: '100%', role: 'img',
|
|
1755
2011
|
'aria-label': title, onMouseLeave: clear,
|
|
1756
2012
|
},
|
|
1757
|
-
|
|
1758
|
-
const y = CHART_PAD.top + layout.plotHeight -
|
|
1759
|
-
return h('g', { key: tick },
|
|
2013
|
+
yTicks.map((tick) => {
|
|
2014
|
+
const y = CHART_PAD.top + layout.plotHeight - tick.ratio * layout.plotHeight
|
|
2015
|
+
return h('g', { key: tick.label },
|
|
1760
2016
|
h('line', { className: 'ud-grid', x1: CHART_PAD.left, x2: plotRight, y1: y, y2: y }),
|
|
1761
|
-
h('text', { className: 'ud-axis', x: CHART_PAD.left - AXIS_LABEL_GAP, y: y + AXIS_LABEL_BASELINE, textAnchor: 'end' },
|
|
2017
|
+
h('text', { className: 'ud-axis', x: CHART_PAD.left - AXIS_LABEL_GAP, y: y + AXIS_LABEL_BASELINE, textAnchor: 'end' }, tick.label))
|
|
1762
2018
|
}),
|
|
1763
|
-
|
|
2019
|
+
speedAxisTicks.length > 0
|
|
2020
|
+
? h('text', { className: 'ud-axis', x: CHART_PAD.left - AXIS_LABEL_GAP, y: CHART_PAD.top + AXIS_LABEL_BASELINE, textAnchor: 'end' }, 'tok/s')
|
|
2021
|
+
: null,
|
|
2022
|
+
(showRate ? rateAxisTicks() : []).map((tick) => {
|
|
1764
2023
|
const y = CHART_PAD.top + layout.plotHeight - (tick / PERCENT_SCALE) * layout.plotHeight
|
|
1765
2024
|
return h('text', {
|
|
1766
2025
|
key: `rate-${tick}`, className: 'ud-axis-rate',
|
|
@@ -1778,30 +2037,42 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
1778
2037
|
slots.map((slot, index) => (index % layout.labelEvery === 0 || index === slots.length - 1)
|
|
1779
2038
|
? h('text', { key: slot.day, className: 'ud-axis', x: layout.bars[index].x, y: CHART_HEIGHT - X_LABEL_OFFSET, textAnchor: 'middle' }, labelFor(slot.day))
|
|
1780
2039
|
: null),
|
|
1781
|
-
h('path', {
|
|
2040
|
+
showRate ? h('path', {
|
|
1782
2041
|
className: 'ud-trend', d: smoothPath(ratePoints),
|
|
1783
2042
|
strokeWidth: TREND_LINE_WIDTH, strokeLinejoin: 'round', strokeLinecap: 'round',
|
|
1784
|
-
}),
|
|
2043
|
+
}) : null,
|
|
2044
|
+
showSpeed ? h('path', {
|
|
2045
|
+
className: cx('ud-trend', 'ud-trend--speed'), d: smoothPath(speedPoints),
|
|
2046
|
+
strokeWidth: TREND_LINE_WIDTH, strokeLinejoin: 'round', strokeLinecap: 'round',
|
|
2047
|
+
}) : null,
|
|
1785
2048
|
hoverRatePoint
|
|
1786
2049
|
? h('circle', { className: 'ud-trend-dot', cx: hoverRatePoint.x, cy: hoverRatePoint.y, r: TREND_DOT_RADIUS })
|
|
1787
2050
|
: null,
|
|
2051
|
+
hoverSpeedPoint
|
|
2052
|
+
? h('circle', { className: cx('ud-trend-dot', 'ud-trend-dot--speed'), cx: hoverSpeedPoint.x, cy: hoverSpeedPoint.y, r: TREND_DOT_RADIUS })
|
|
2053
|
+
: null,
|
|
1788
2054
|
slots.map((slot, index) => h('rect', {
|
|
1789
2055
|
key: `hit-${slot.day}`, className: 'ud-bar-hit',
|
|
1790
2056
|
x: layout.bars[index].x - layout.step / 2, y: CHART_PAD.top, width: layout.step, height: layout.plotHeight,
|
|
1791
2057
|
onMouseEnter: pick(index), onFocus: pick(index), onMouseLeave: clear, onBlur: clear,
|
|
1792
2058
|
})))),
|
|
1793
|
-
h(Legend, {
|
|
2059
|
+
h(Legend, {
|
|
2060
|
+
models: legendModels, colorFor, speedEnabled: hasSpeed,
|
|
2061
|
+
isVisible: (key) => visibleSet.has(key),
|
|
2062
|
+
onItem: (key, ctrl) => setVisibleKeys(legendToggle(visibleKeys, key, ctrl)),
|
|
2063
|
+
}),
|
|
1794
2064
|
h(ChartTip, { anchor: hover ? hover.anchor : null, panelRef },
|
|
1795
2065
|
hoverSlot
|
|
1796
2066
|
? [
|
|
1797
|
-
h('div', { key: 'title', className: 'ud-tip-title' }, hoverSlot.day),
|
|
2067
|
+
h('div', { key: 'title', className: 'ud-tip-title' }, slotLabelFor ? slotLabelFor(hoverSlot.day) : hoverSlot.day),
|
|
1798
2068
|
h('div', { key: 'total', className: 'ud-tip-row' }, `${t('total')}: ${formatTokens(hoverSlot.total)}`),
|
|
1799
|
-
...legendModels.map((item) => h('div', { key: `m-${item.model}`, className: 'ud-tip-row' },
|
|
2069
|
+
...legendModels.filter((item) => visibleSet.has(item.model)).map((item) => h('div', { key: `m-${item.model}`, className: 'ud-tip-row' },
|
|
1800
2070
|
h('i', { className: 'ud-legend-swatch', style: { background: colorFor(item.model) } }),
|
|
1801
2071
|
`${item.model === OTHER_MODEL ? t('other') : item.model}: ${formatTokens(hoverSlot.byModel[item.model] ?? 0)}`)),
|
|
1802
|
-
...otherEntries.map(([model, tokens]) => h('div', { key: `om-${model}`, className: 'ud-tip-row ud-tip-row--sub' },
|
|
1803
|
-
`${model}: ${formatTokens(tokens)}`)),
|
|
1804
|
-
h('div', { key: 'rate', className: 'ud-tip-row' }, `${t('cacheHitRate')}: ${cacheRateText(hoverSlot.cacheHit, hoverSlot.cacheMiss)}`),
|
|
2072
|
+
...(visibleSet.has(OTHER_MODEL) ? otherEntries.map(([model, tokens]) => h('div', { key: `om-${model}`, className: 'ud-tip-row ud-tip-row--sub' },
|
|
2073
|
+
`${model}: ${formatTokens(tokens)}`)) : []),
|
|
2074
|
+
showRate ? h('div', { key: 'rate', className: 'ud-tip-row' }, `${t('cacheHitRate')}: ${cacheRateText(hoverSlot.cacheHit, hoverSlot.cacheMiss)}`) : null,
|
|
2075
|
+
showSpeed ? h('div', { key: 'speed', className: 'ud-tip-row' }, `${t('avgSpeed')}: ${speedTipText(hoverSlot.speed)}`) : null,
|
|
1805
2076
|
costEnabled && prefsRef.current.costDisplay && hoverSlot.cost !== undefined
|
|
1806
2077
|
? h('div', { key: 'cost', className: 'ud-tip-row' }, `≈ ${formatCost(hoverSlot.cost, costCurrency)}`)
|
|
1807
2078
|
: null,
|
|
@@ -2076,10 +2347,14 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
2076
2347
|
entry.title ? h('span', { title: entry.title }, entry.text) : entry.text)))
|
|
2077
2348
|
})
|
|
2078
2349
|
|
|
2079
|
-
// 注入点B
|
|
2080
|
-
//
|
|
2081
|
-
|
|
2082
|
-
|
|
2350
|
+
// 注入点B 组件:回合费用芯片,官方动作行内渲染(复制与分支图标之间,赞踩/上下文跳转同排);
|
|
2351
|
+
// 受费用显示开关;价格异步首帧未回不渲染,回包后补渲染;显隐节奏随官方 data-actions-reveal
|
|
2352
|
+
// 官方槽容器固定在用量/用时芯片之前,末位排布由 SlotTailPortal 移交实现
|
|
2353
|
+
const CostChip = React.memo(function CostChip({ messageId, useChat, t = defaultT }) {
|
|
2354
|
+
if (typeof useChat !== 'function') return null
|
|
2355
|
+
const chatNodes = useChat((state) => (state && typeof state === 'object') ? state.nodes : undefined)
|
|
2356
|
+
const [prefs, setPrefs] = useState(() => statsLineState.get())
|
|
2357
|
+
useEffect(() => statsLineState.subscribe(() => setPrefs(statsLineState.get())), [])
|
|
2083
2358
|
const [pricingRules, setPricingRules] = useState(null)
|
|
2084
2359
|
useEffect(() => {
|
|
2085
2360
|
let alive = true
|
|
@@ -2088,14 +2363,33 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
2088
2363
|
})
|
|
2089
2364
|
return () => { alive = false }
|
|
2090
2365
|
}, [])
|
|
2091
|
-
const
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
const
|
|
2095
|
-
|
|
2096
|
-
|
|
2366
|
+
const matched = typeof messageId === 'string' && messageId !== ''
|
|
2367
|
+
? turnTokenUsageOfMessage(chatNodes, messageId)
|
|
2368
|
+
: null
|
|
2369
|
+
const price = matched && prefs.costDisplay && pricingRules !== null
|
|
2370
|
+
? matchPrice(pricingRules, turnModelOf(matched), new Date())
|
|
2371
|
+
: null
|
|
2372
|
+
// 0 元(全零价/全零桶)与价未命中同不渲染,无占位符
|
|
2373
|
+
const cost = matched && price ? turnCostAmountOf(matched, price) : null
|
|
2374
|
+
const chip = cost === null ? null : h('span', { className: 'ud-turn-cost', title: turnCostTitleText(t, matched) },
|
|
2375
|
+
t('turnCostChip', { cost: formatCost(cost, aggregateCurrencyOf(pricingRules)) }))
|
|
2376
|
+
return h(SlotTailPortal, null, chip)
|
|
2097
2377
|
})
|
|
2098
2378
|
|
|
2379
|
+
// 槽内容末位移交:锚点藏于槽容器内,portal 目标取槽容器(display:contents)的父级即官方动作行 div,
|
|
2380
|
+
// portal 子树 append 到动作行末尾(官方用量/用时芯片与结束时钟之后);portal 缺席降级锚点原位
|
|
2381
|
+
function SlotTailPortal({ children }) {
|
|
2382
|
+
const anchorRef = useRef(null)
|
|
2383
|
+
const [container, setContainer] = useState(null)
|
|
2384
|
+
useEffect(() => {
|
|
2385
|
+
const anchor = anchorRef.current
|
|
2386
|
+
const slotHost = anchor?.closest('[data-slot="conversation.chat.assistant-actions"]') ?? anchor?.parentElement
|
|
2387
|
+
setContainer(slotHost?.parentElement ?? null)
|
|
2388
|
+
}, [])
|
|
2389
|
+
const ported = container && createPortal ? createPortal(children, container) : null
|
|
2390
|
+
return h('span', { ref: anchorRef, style: ported ? { display: 'none' } : undefined }, ported ?? children)
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2099
2393
|
// 偏好卡行:说明文案承担 aria-describedby 目标
|
|
2100
2394
|
function StatsLineOptionRow({ labelKey, descKey, checked, onToggle, t = defaultT }) {
|
|
2101
2395
|
const describeId = React.useId()
|
|
@@ -2139,8 +2433,8 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
2139
2433
|
const PRICING_STATE_READY = 'ready'
|
|
2140
2434
|
const PRICING_STATE_UNAVAILABLE = 'unavailable'
|
|
2141
2435
|
|
|
2142
|
-
//
|
|
2143
|
-
function
|
|
2436
|
+
// 价格四桶 grid:默认价槽与附加规则卡共用;错误路径按规则平面索引寻址
|
|
2437
|
+
function PricingPriceGrid({ rule, currency, errors, pathPrefix, t, onPricePatch }) {
|
|
2144
2438
|
const errorTextOf = (path) => {
|
|
2145
2439
|
const key = errors.get(path)
|
|
2146
2440
|
return key ? h('span', { className: 'ud-field-error' }, t(key)) : null
|
|
@@ -2152,9 +2446,23 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
2152
2446
|
h('input', {
|
|
2153
2447
|
type: 'number', className: 'ud-input', min: 0, step: 'any',
|
|
2154
2448
|
value: rule.price?.[key] ?? '',
|
|
2155
|
-
onChange: (event) =>
|
|
2449
|
+
onChange: (event) => onPricePatch({ price: { ...rule.price, [key]: event.target.value } }),
|
|
2156
2450
|
})),
|
|
2157
2451
|
errorTextOf(`${pathPrefix}price.${key}`))
|
|
2452
|
+
return h('div', { className: 'ud-price-grid' },
|
|
2453
|
+
priceField('input', 'priceInput'),
|
|
2454
|
+
priceField('output', 'priceOutput'),
|
|
2455
|
+
priceField('cacheRead', 'priceCacheRead'),
|
|
2456
|
+
priceField('cacheWrite', 'priceCacheWrite'))
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
// 附加计费规则卡:头行 = 价格四桶 + 上移/下移/删除;下方为条件组合区(组内从上到下首个命中者生效)。
|
|
2460
|
+
// 默认价在组内独立成槽,附加规则不再承担兜底语义
|
|
2461
|
+
function PricingExtraRuleCard({ rule, currency, errors, pathPrefix, t, onPatch, onRemove, onMove, canMoveUp, canMoveDown }) {
|
|
2462
|
+
const errorTextOf = (path) => {
|
|
2463
|
+
const key = errors.get(path)
|
|
2464
|
+
return key ? h('span', { className: 'ud-field-error' }, t(key)) : null
|
|
2465
|
+
}
|
|
2158
2466
|
const patchConditions = (conditions) => onPatch({ conditions })
|
|
2159
2467
|
const patchConditionAt = (conditionIndex, patch) => patchConditions(patchItemAt(rule.conditions, conditionIndex, patch))
|
|
2160
2468
|
const removeConditionAt = (conditionIndex) => patchConditions(rule.conditions.filter((_, i) => i !== conditionIndex))
|
|
@@ -2168,10 +2476,12 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
2168
2476
|
onChange: (event) => patchCondition({ [field]: event.target.value }),
|
|
2169
2477
|
}),
|
|
2170
2478
|
errorTextOf(`${condPath}.${field}`))
|
|
2479
|
+
// 号段 from/to 同域 1~31(双闭),统一上限
|
|
2171
2480
|
const numberInput = (field, labelKey) => h('label', { key: field, className: 'ud-field' },
|
|
2172
2481
|
h('span', { className: 'ud-field-label' }, t(labelKey)),
|
|
2173
2482
|
h('input', {
|
|
2174
|
-
type: 'number', className: 'ud-input', min: MONTH_DAY_MIN,
|
|
2483
|
+
type: 'number', className: 'ud-input', min: MONTH_DAY_MIN,
|
|
2484
|
+
max: MONTH_DAY_MAX, step: 1,
|
|
2175
2485
|
value: condition[field] ?? '',
|
|
2176
2486
|
onChange: (event) => patchCondition({ [field]: event.target.value }),
|
|
2177
2487
|
}),
|
|
@@ -2208,32 +2518,74 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
2208
2518
|
}, '×'))
|
|
2209
2519
|
}
|
|
2210
2520
|
return h('div', { className: 'ud-rule' },
|
|
2211
|
-
h('div', { className: 'ud-
|
|
2521
|
+
h('div', { className: 'ud-slot-head' },
|
|
2522
|
+
h(PricingPriceGrid, {
|
|
2523
|
+
rule, currency, errors, pathPrefix, t,
|
|
2524
|
+
onPricePatch: (part) => onPatch(part),
|
|
2525
|
+
}),
|
|
2526
|
+
canMoveUp ? h('button', {
|
|
2527
|
+
className: 'ud-btn ud-btn--text', type: 'button', onClick: () => onMove(-1),
|
|
2528
|
+
'aria-label': t('moveUp'), title: t('moveUp'),
|
|
2529
|
+
}, '↑') : null,
|
|
2530
|
+
canMoveDown ? h('button', {
|
|
2531
|
+
className: 'ud-btn ud-btn--text', type: 'button', onClick: () => onMove(1),
|
|
2532
|
+
'aria-label': t('moveDown'), title: t('moveDown'),
|
|
2533
|
+
}, '↓') : null,
|
|
2534
|
+
h(DeleteArmedButton, { labelKey: 'deleteRule', confirmKey: 'confirmDelete', onConfirm: onRemove, t })),
|
|
2535
|
+
h('div', { className: 'ud-rule-conds' },
|
|
2536
|
+
(rule.conditions ?? []).map((condition, conditionIndex) => conditionRow(condition, conditionIndex)),
|
|
2537
|
+
h('div', { className: 'ud-cond-add' },
|
|
2538
|
+
h('button', {
|
|
2539
|
+
type: 'button', className: 'ud-btn ud-btn--text',
|
|
2540
|
+
onClick: () => patchConditions([...(rule.conditions ?? []), defaultCondition(CONDITION_KIND_OPTIONS[0])]),
|
|
2541
|
+
}, `+${t('addCondition')}`))))
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
// 模型组卡:组头 = 模型键输入(整组一次改名)+ 删除整组;组内 = 默认价槽(禁条件/排序/删除,恒兜底)
|
|
2545
|
+
// + 附加计费规则列表(从上到下首个命中生效,全不命中落默认价)。
|
|
2546
|
+
// 组件 key 由调用方锚定首槽位平面索引,改名引发的重新聚合不会丢焦点
|
|
2547
|
+
function PricingModelGroup({ group, currency, errors, t, onModelChange, onGroupRemove, onSlotPatch, onSlotRemove, onSlotMove, onSlotAdd }) {
|
|
2548
|
+
const modelError = group.slots.map((slot) => errors.get(`${slot.index}.model`)).find(Boolean)
|
|
2549
|
+
const { defaultSlot, extras } = partitionGroupOf(group)
|
|
2550
|
+
// 新附加规则插到默认价平面位之前(无默认价则组末),保证默认价恒居组末
|
|
2551
|
+
const insertPosition = defaultSlot ? defaultSlot.index - 1 : group.slots[group.slots.length - 1].index
|
|
2552
|
+
return h('div', { className: 'ud-rule-group' },
|
|
2553
|
+
h('div', { className: 'ud-rule-group-head' },
|
|
2212
2554
|
h('label', { className: 'ud-field' },
|
|
2213
2555
|
h('span', { className: 'ud-field-label' }, t('pricingModel')),
|
|
2214
2556
|
h('input', {
|
|
2215
|
-
type: 'text', className: 'ud-input', value:
|
|
2557
|
+
type: 'text', className: 'ud-input', value: group.model,
|
|
2216
2558
|
placeholder: t('pricingModelPlaceholder'),
|
|
2217
|
-
onChange: (event) =>
|
|
2559
|
+
onChange: (event) => onModelChange(event.target.value),
|
|
2218
2560
|
}),
|
|
2219
|
-
|
|
2220
|
-
h('
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
(
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2561
|
+
modelError ? h('span', { className: 'ud-field-error' }, t(modelError)) : null),
|
|
2562
|
+
h(DeleteArmedButton, { labelKey: 'deleteGroup', confirmKey: 'confirmDelete', onConfirm: onGroupRemove, t })),
|
|
2563
|
+
h('div', { className: 'ud-rule-group-slots' },
|
|
2564
|
+
defaultSlot ? h('div', { className: 'ud-rule ud-rule-default' },
|
|
2565
|
+
h('div', { className: 'ud-slot-head' },
|
|
2566
|
+
h(PricingPriceGrid, {
|
|
2567
|
+
rule: defaultSlot.rule, currency, errors,
|
|
2568
|
+
pathPrefix: `${defaultSlot.index}.`, t,
|
|
2569
|
+
onPricePatch: (part) => onSlotPatch(defaultSlot.index, part),
|
|
2570
|
+
})),
|
|
2571
|
+
h('span', { className: 'ud-rule-cond' }, t('defaultPriceHint'))) : null,
|
|
2572
|
+
extras.map((slot, position) => h(PricingExtraRuleCard, {
|
|
2573
|
+
key: slot.index,
|
|
2574
|
+
rule: slot.rule,
|
|
2575
|
+
currency,
|
|
2576
|
+
errors,
|
|
2577
|
+
pathPrefix: `${slot.index}.`,
|
|
2578
|
+
t,
|
|
2579
|
+
onPatch: (part) => onSlotPatch(slot.index, part),
|
|
2580
|
+
onRemove: () => onSlotRemove(slot.index),
|
|
2581
|
+
onMove: (delta) => onSlotMove(slot.index, extras[position + delta].index),
|
|
2582
|
+
canMoveUp: position > 0,
|
|
2583
|
+
canMoveDown: position < extras.length - 1,
|
|
2584
|
+
}))),
|
|
2585
|
+
h('button', {
|
|
2586
|
+
className: 'ud-rule-add', type: 'button',
|
|
2587
|
+
onClick: () => onSlotAdd(insertPosition, group.model),
|
|
2588
|
+
}, defaultSlot ? t('addRule') : t('addDefaultPrice')))
|
|
2237
2589
|
}
|
|
2238
2590
|
|
|
2239
2591
|
function PricingEditor({ t = defaultT }) {
|
|
@@ -2316,20 +2668,30 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
2316
2668
|
}, symbol))),
|
|
2317
2669
|
h('button', { className: 'ud-btn', type: 'button', disabled: saving, onClick: save }, t('save')))),
|
|
2318
2670
|
saveError ? h('div', { className: 'ud-error' }, saveError) : null,
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2671
|
+
h('span', { className: 'ud-rule-cond' }, t('ruleOrderHint')),
|
|
2672
|
+
groupRulesOf(rules).map((group) => h(PricingModelGroup, {
|
|
2673
|
+
// key 锚定首槽位平面索引:改名重聚合不重建组件,输入焦点不丢
|
|
2674
|
+
key: String(group.slots[0].index),
|
|
2675
|
+
group,
|
|
2322
2676
|
currency,
|
|
2323
2677
|
errors,
|
|
2324
|
-
pathPrefix: `${index}.`,
|
|
2325
2678
|
t,
|
|
2326
|
-
|
|
2327
|
-
|
|
2679
|
+
onModelChange: (model) => setRules((prev) => renameRulesAt(prev, group.slots.map((slot) => slot.index), model)),
|
|
2680
|
+
onGroupRemove: () => setRules((prev) => removeRulesAt(prev, group.slots.map((slot) => slot.index))),
|
|
2681
|
+
onSlotPatch: (index, part) => setRules((prev) => patchItemAt(prev, index, part)),
|
|
2682
|
+
onSlotRemove: (index) => setRules((prev) => prev.filter((_, i) => i !== index)),
|
|
2683
|
+
onSlotMove: (fromIndex, toIndex) => setRules((prev) => moveRuleTo(prev, fromIndex, toIndex)),
|
|
2684
|
+
onSlotAdd: (position, model) => setRules((prev) => insertRuleAt(prev, position, {
|
|
2685
|
+
...defaultPricingRule(currency),
|
|
2686
|
+
model,
|
|
2687
|
+
// 附加规则默认带全天时段条件:默认价已独立成槽,附加规则应以可编辑条件呈现
|
|
2688
|
+
conditions: [defaultCondition(CONDITION_KIND_OPTIONS[0])],
|
|
2689
|
+
})),
|
|
2328
2690
|
})),
|
|
2329
2691
|
h('button', {
|
|
2330
2692
|
className: 'ud-rule-add', type: 'button',
|
|
2331
2693
|
onClick: () => setRules((prev) => [...prev, defaultPricingRule(currency)]),
|
|
2332
|
-
}, t('
|
|
2694
|
+
}, t('addGroup')))
|
|
2333
2695
|
}
|
|
2334
2696
|
|
|
2335
2697
|
// 扫描异常日志入口:仅箭头标识,明细在展开的日志块中展示
|
|
@@ -2360,14 +2722,22 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
2360
2722
|
status.error ? h('span', { className: 'ud-status-err' }, status.error) : null)
|
|
2361
2723
|
}
|
|
2362
2724
|
|
|
2363
|
-
// 展开后的异常日志块:汇总计数即明细条数(单一事实源),逐条展示(时间/类型/内容)
|
|
2725
|
+
// 展开后的异常日志块:汇总计数即明细条数(单一事实源),逐条展示(时间/类型/内容);
|
|
2726
|
+
// skipped 细分归因(宿主拒读/损坏/旧格式)挂在汇总行 title,供跨宿主边界诊断
|
|
2364
2727
|
function AnomalyLog({ status, t = defaultT }) {
|
|
2365
2728
|
const lines = status.log ?? []
|
|
2366
2729
|
if (lines.length === 0) return null
|
|
2730
|
+
const breakdown = status.skippedBreakdown ?? {}
|
|
2731
|
+
const breakdownText = t('skipBreakdown', {
|
|
2732
|
+
d: breakdown.descriptor ?? 0,
|
|
2733
|
+
c: breakdown.corrupt ?? 0,
|
|
2734
|
+
l: breakdown.legacy ?? 0,
|
|
2735
|
+
o: breakdown.other ?? 0,
|
|
2736
|
+
})
|
|
2367
2737
|
return h('div', { className: 'ud-log' },
|
|
2368
2738
|
h('div', { className: 'ud-log-summary' },
|
|
2369
2739
|
(status.skippedSessions ?? 0) > 0
|
|
2370
|
-
? h('span',
|
|
2740
|
+
? h('span', { title: breakdownText }, t('skippedSessions', { n: status.skippedSessions }))
|
|
2371
2741
|
: null,
|
|
2372
2742
|
(status.recordFailures ?? 0) > 0
|
|
2373
2743
|
? h('span', { className: 'ud-status-err' }, t('recordFailures', { n: status.recordFailures }))
|
|
@@ -2406,6 +2776,40 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
2406
2776
|
armed ? t('rebuildConfirm') : t('rebuild'))
|
|
2407
2777
|
}
|
|
2408
2778
|
|
|
2779
|
+
// 回退按钮:仅当存在重建快照(status.backup.available)时可点;
|
|
2780
|
+
// 恢复动作本身也会被重新快照覆盖,回退链不断
|
|
2781
|
+
function RestoreButton({ machineRef, busy, backup, onError, t = defaultT }) {
|
|
2782
|
+
const [armed, setArmed] = useState(false)
|
|
2783
|
+
const armedTimerRef = useRef(null)
|
|
2784
|
+
|
|
2785
|
+
useEffect(() => () => {
|
|
2786
|
+
if (armedTimerRef.current) clearTimeout(armedTimerRef.current)
|
|
2787
|
+
}, [])
|
|
2788
|
+
|
|
2789
|
+
if (!backup?.available) {
|
|
2790
|
+
return h('button', { className: 'ud-btn ud-btn--text', disabled: true, title: t('restoreMissing') },
|
|
2791
|
+
t('restore'))
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
const restore = async () => {
|
|
2795
|
+
if (!armed) {
|
|
2796
|
+
setArmed(true)
|
|
2797
|
+
armedTimerRef.current = setTimeout(() => setArmed(false), REBUILD_CONFIRM_MS)
|
|
2798
|
+
return
|
|
2799
|
+
}
|
|
2800
|
+
setArmed(false)
|
|
2801
|
+
const result = await requestPost(ENDPOINTS.restore)
|
|
2802
|
+
if (!result.ok) {
|
|
2803
|
+
onError(result.message)
|
|
2804
|
+
return
|
|
2805
|
+
}
|
|
2806
|
+
machineRef.current?.restart()
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
return h('button', { className: 'ud-btn ud-btn--text', disabled: busy, onClick: restore },
|
|
2810
|
+
armed ? t('restoreConfirm') : t('restore'))
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2409
2813
|
const viewLabel = (t, id) => (id === 'day' ? t('viewDay') : id === 'hour' ? t('viewHour') : t('viewMinute'))
|
|
2410
2814
|
const trendTitle = (t, id) => (id === 'day' ? t('dailyTrend') : id === 'hour' ? t('hourTrend') : t('minuteTrend'))
|
|
2411
2815
|
const trendLimitedText = (t, id, count) => (id === 'day'
|
|
@@ -2413,6 +2817,7 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
2413
2817
|
: id === 'hour' ? t('trendLimitedHour', { n: count }) : t('trendLimitedMinute', { n: count }))
|
|
2414
2818
|
const presetLabel = (t, view, id) => t(`${view === 'hour' ? 'hourPreset' : 'minutePreset'}.${id}`)
|
|
2415
2819
|
const tickLabelFor = (view) => (view === 'day' ? shortDay : view === 'hour' ? hourTickLabel : minuteTickLabel)
|
|
2820
|
+
const slotLabelFor = (view) => (view === 'hour' ? hourSlotLabel : view === 'minute' ? minuteSlotLabel : (day) => day)
|
|
2416
2821
|
|
|
2417
2822
|
function UsageDashPanel({ t = defaultT }) {
|
|
2418
2823
|
const [view, setView] = useState('day')
|
|
@@ -2651,7 +3056,8 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
2651
3056
|
? h('div', { className: 'ud-detail' },
|
|
2652
3057
|
h(AnomalyLog, { status, t }),
|
|
2653
3058
|
h('div', { className: 'ud-detail-foot' },
|
|
2654
|
-
h(RebuildButton, { machineRef: statusMachineRef, busy: status?.running === true, onError: setError, t })
|
|
3059
|
+
h(RebuildButton, { machineRef: statusMachineRef, busy: status?.running === true, onError: setError, t }),
|
|
3060
|
+
h(RestoreButton, { machineRef: statusMachineRef, busy: status?.running === true, backup: status?.backup, onError: setError, t })))
|
|
2655
3061
|
: null,
|
|
2656
3062
|
error ? h('div', { className: 'ud-error' }, error) : null,
|
|
2657
3063
|
loadingVisible ? h('div', { className: 'ud-loading' }, `${t('loading')}…`) : null,
|
|
@@ -2667,6 +3073,7 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
2667
3073
|
modelOrder: trendSource.models.map((item) => item.model),
|
|
2668
3074
|
colorFor,
|
|
2669
3075
|
labelFor: tickLabelFor(view),
|
|
3076
|
+
slotLabelFor: slotLabelFor(view),
|
|
2670
3077
|
labelMinPitch: pointActive ? LABEL_PITCH_TIME : LABEL_PITCH_DAY,
|
|
2671
3078
|
busy,
|
|
2672
3079
|
legendModels: trendSource.models,
|
|
@@ -2704,15 +3111,15 @@ body[data-ds-dark-theme] .ud-panel{--ud-chart-1:color-mix(in srgb,#0576ff 65%,wh
|
|
|
2704
3111
|
} catch (error) {
|
|
2705
3112
|
console.warn('[usage-dash] 底部信息栏未注册(宿主无 conversation.composer.dock 插槽)', error)
|
|
2706
3113
|
}
|
|
2707
|
-
// 注入点B
|
|
3114
|
+
// 注入点B:回合费用芯片走官方动作行槽,回合结束随 messageId 出现;旧宿主无该插槽仅告警禁用
|
|
2708
3115
|
try {
|
|
2709
|
-
ctx.slots.inject('conversation.chat.
|
|
3116
|
+
ctx.slots.inject('conversation.chat.assistant-actions', () =>
|
|
2710
3117
|
ctx.slots.register(
|
|
2711
|
-
{ name: 'conversation.chat.
|
|
2712
|
-
|
|
3118
|
+
{ name: 'conversation.chat.assistant-actions', id: 'usage-dash-turn-cost', order: TURN_COST_CHIP_ORDER, locale: LOCALE_NS },
|
|
3119
|
+
CostChip,
|
|
2713
3120
|
))
|
|
2714
3121
|
} catch (error) {
|
|
2715
|
-
console.warn('[usage-dash]
|
|
3122
|
+
console.warn('[usage-dash] 回合费用芯片未注册(宿主无 conversation.chat.assistant-actions 插槽)', error)
|
|
2716
3123
|
}
|
|
2717
3124
|
// 跨实例同步:其他实例写开关经 storage 事件触发重读(同实例写入不触发该事件)
|
|
2718
3125
|
window.addEventListener('storage', (event) => {
|