@kenz1117/dsh-ui-usage-billing 0.1.0 → 0.2.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 CHANGED
@@ -9,8 +9,8 @@ DeepSeek Harness 计费仪表盘插件。从持久化会话日志实时聚合模
9
9
 
10
10
  ## 特性
11
11
 
12
- - **侧边栏入口**:设置按钮上方的触发胶囊,显示累计费用;折叠栏自动切换为图标。
13
- - **计费仪表盘**:总费用卡片、KPI 指标(缓存命中率 / Token 总量 / 单次平均成本 / 调用次数)、按日趋势图、按模型计费明细、可展开的单价表。
12
+ - **侧边栏入口**:设置按钮上方的触发胶囊,显示**当月费用 + 今日费用**;折叠栏自动切换为图标。
13
+ - **计费仪表盘**:Hero 按 **当年 / 当月 / 当日** 三维展示费用,KPI 指标(缓存命中率 / Token 总量 / 单次平均成本 / 调用次数)、按模型分色的**堆叠趋势图**、按模型计费明细、可展开的单价表。
14
14
  - **真实用量**:服务端从会话日志实时聚合,无需手工维护统计文件。
15
15
  - **模型健康探测**:模型行的圆点反映各厂商接入状态(正常绿 / 异常红 / 未接入灰)。
16
16
  - **订阅计划豁免**:走 coding/token 套餐的模型照常统计 token,费用记 0。
@@ -119,11 +119,16 @@ cost(CNY)= (missInput × p_input + cacheHit × p_cacheHit + output × p_outp
119
119
  },
120
120
  "byDay": {
121
121
  "2026-08-15": { "calls": 74, "input": 32593373, "output": 35375, "cacheHit": 32558208, "cacheMiss": 35165, "cost": 0.52 }
122
+ },
123
+ "byDayModels": {
124
+ "2026-08-15": {
125
+ "flash": { "calls": 74, "input": 32593373, "output": 35375, "cacheHit": 32558208, "cacheMiss": 35165, "cost": 0.52 }
126
+ }
122
127
  }
123
128
  }
124
129
  ```
125
130
 
126
- 字段含义:`input` 为总输入 token;`cacheHit` / `cacheMiss` 为缓存命中 / 未命中分桶;`cost` 为人民币估算费用。`sessionPersistence` 不可用时回退到配置文件(见下)。
131
+ 字段含义:`input` 为总输入 token;`cacheHit` / `cacheMiss` 为缓存命中 / 未命中分桶;`cost` 为人民币估算费用。`byDayModels` 是 **模型 × 日期** 二维统计(`[date][modelKey]`),趋势图按模型堆叠的输入;当年 / 当月 / 当日三维费用由浏览器端按 `byDay` 日期前缀归并。`sessionPersistence` 不可用时回退到配置文件(见下)。
127
132
 
128
133
  ## 配置
129
134
 
package/lib/client.js CHANGED
@@ -23,7 +23,7 @@ window.__ModuleLoader__.load({
23
23
  }
24
24
  //#endregion
25
25
  //#region \0dsh-css:/Users/ken/deepseek-harness/packages/client/ui-usage-billing/src/client/UsageBilling.module.css.mjs
26
- const css = ".VWh0dG_railButton{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);width:36px;height:36px;color:var(--dsw-static-blue-500);cursor:pointer;border-radius:10px;justify-content:center;align-items:center;padding:0;transition:background-color .14s,border-color .14s,color .14s;display:flex}.VWh0dG_railButton:hover,.VWh0dG_railButton:focus-visible{background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l3);color:var(--dsw-static-blue-400);outline:none}.VWh0dG_railButton svg{stroke-width:2px;width:18px;height:18px}.VWh0dG_trigger{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);width:100%;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;border-radius:12px;align-items:center;gap:10px;padding:9px 12px;transition:border-color .14s,background-color .14s,transform .12s;display:flex}.VWh0dG_trigger:hover,.VWh0dG_trigger:focus-visible{border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover);outline:none}.VWh0dG_trigger:active{transform:scale(.985)}.VWh0dG_triggerIcon{background:linear-gradient(135deg, var(--dsw-static-blue-500), var(--dsw-static-deepseek-500));width:30px;height:30px;color:var(--dsw-static-neutral-bluish-00);border-radius:9px;flex:none;justify-content:center;align-items:center;display:inline-flex}.VWh0dG_triggerIcon svg{stroke-width:2.2px;width:15px;height:15px}.VWh0dG_triggerText{flex-direction:column;gap:1px;min-width:0;display:flex}.VWh0dG_triggerLabel{color:var(--dsw-alias-label-primary);font-size:12px;font-weight:500;line-height:16px}.VWh0dG_triggerMeta{color:var(--dsw-alias-label-tertiary);white-space:nowrap;align-items:baseline;gap:6px;font-size:11px;line-height:15px;display:flex}.VWh0dG_triggerMeta strong{color:var(--dsw-static-blue-500);font-variant-numeric:tabular-nums;font-size:13px;font-weight:600}.VWh0dG_triggerMeta em{color:var(--dsw-alias-label-caption);font-style:normal}.VWh0dG_triggerChevron{width:14px;height:14px;color:var(--dsw-alias-label-caption);flex:none;margin-left:auto}.VWh0dG_dashboardModal{width:min(760px,100vw - 48px);max-height:min(760px,88vh)}.VWh0dG_dashboard{flex-direction:column;width:100%;max-height:min(760px,88vh);display:flex}.VWh0dG_dashboardHead{border-bottom:1px solid var(--dsw-alias-border-l1);justify-content:space-between;align-items:flex-start;gap:12px;padding:20px 24px 14px;display:flex}.VWh0dG_dashboardTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:17px;font-weight:600;line-height:24px}.VWh0dG_dashboardSubtitle{color:var(--dsw-alias-label-caption);margin:3px 0 0;font-size:12px;line-height:17px}.VWh0dG_closeButton{width:30px;height:30px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:8px;flex:none;justify-content:center;align-items:center;transition:background-color .14s;display:inline-flex}.VWh0dG_closeButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.VWh0dG_closeButton svg{stroke-width:2px;width:16px;height:16px}.VWh0dG_dashboardBody{flex-direction:column;gap:14px;padding:16px 24px 24px;display:flex;overflow-y:auto}.VWh0dG_hero{background:linear-gradient(135deg, var(--dsw-static-blue-800) 0%, var(--dsw-static-deepseek-600) 55%, var(--dsw-static-deepseek-500) 100%);min-height:118px;color:var(--dsw-static-neutral-bluish-00);border-radius:16px;align-items:center;gap:24px;padding:18px 22px;display:flex;position:relative;overflow:hidden}.VWh0dG_hero:after{content:\"\";pointer-events:none;background:radial-gradient(circle,#ffffff29 0%,#fff0 70%);border-radius:50%;width:200px;height:200px;position:absolute;top:-60px;right:-40px}.VWh0dG_heroMain{flex-direction:column;flex:auto;justify-content:center;gap:3px;min-width:0;display:flex}.VWh0dG_heroLabel{opacity:.82;white-space:nowrap;font-size:12px;font-weight:500;line-height:17px}.VWh0dG_heroValue{letter-spacing:-.02em;font-variant-numeric:tabular-nums;white-space:nowrap;font-size:38px;font-weight:700;line-height:46px}.VWh0dG_heroMeta{opacity:.82;white-space:nowrap;align-items:center;gap:6px;font-size:12px;line-height:17px;display:flex}.VWh0dG_heroMeta em{background:#ffffff29;border-radius:6px;margin-left:2px;padding:1px 6px;font-style:normal}.VWh0dG_heroDivider{background:#ffffff38;flex:none;align-self:stretch;width:1px}.VWh0dG_heroSide{flex:none;align-items:center;min-width:140px;display:flex}.VWh0dG_heroSideItem{flex-direction:column;justify-content:center;gap:3px;display:flex}.VWh0dG_heroSideLabel{opacity:.82;white-space:nowrap;font-size:12px;line-height:17px}.VWh0dG_heroSideValue{font-variant-numeric:tabular-nums;white-space:nowrap;font-size:22px;font-weight:600;line-height:28px}.VWh0dG_delta{align-items:center;gap:2px;font-size:12px;font-weight:600;line-height:17px;display:inline-flex}.VWh0dG_deltaUp{color:#9ff0b8}.VWh0dG_deltaDown{color:#ffb4b4}.VWh0dG_kpiGrid{grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;display:grid}.VWh0dG_kpiTile{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:14px;flex-direction:column;gap:3px;padding:14px 15px;transition:border-color .14s,background-color .14s;display:flex}.VWh0dG_kpiTile:hover{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-interactive-bg-hover)}.VWh0dG_kpiLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:15px}.VWh0dG_kpiValue{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;font-size:21px;font-weight:600;line-height:27px}.VWh0dG_kpiGreen{color:var(--dsw-static-green-500)}.VWh0dG_kpiDetail{color:var(--dsw-alias-label-caption);white-space:nowrap;text-overflow:ellipsis;font-size:11px;line-height:15px;overflow:hidden}.VWh0dG_panel{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:16px;flex-direction:column;gap:10px;padding:16px 18px;display:flex}.VWh0dG_panelHead{justify-content:space-between;align-items:baseline;gap:10px;display:flex}.VWh0dG_panelTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:14px;font-weight:600;line-height:20px}.VWh0dG_panelHint{color:var(--dsw-alias-label-caption);white-space:nowrap;font-size:11px;line-height:16px}.VWh0dG_chartWrap{width:100%;position:relative}.VWh0dG_chartSvg{width:100%;height:auto;display:block}.VWh0dG_chartEmpty{height:140px;color:var(--dsw-alias-label-caption);justify-content:center;align-items:center;font-size:13px;display:flex}.VWh0dG_emptyRow{text-align:center;color:var(--dsw-alias-label-caption);padding:28px 0;font-size:13px}.VWh0dG_chartGrid{stroke:var(--dsw-alias-border-l1);stroke-width:1px}.VWh0dG_chartAxisLabel{fill:var(--dsw-alias-label-tertiary);font-size:10px}.VWh0dG_chartBar{fill:var(--dsw-static-blue-500);opacity:.35}.VWh0dG_chartBar:hover{opacity:.6}.VWh0dG_chartLine{stroke:var(--dsw-static-blue-500);stroke-linecap:round;stroke-linejoin:round}.VWh0dG_chartCrosshair{stroke:var(--dsw-alias-label-dimmed);stroke-width:1px;stroke-dasharray:3 3}.VWh0dG_chartDot{fill:var(--dsw-static-neutral-bluish-00);stroke:var(--dsw-static-blue-500);stroke-width:2.5px}.VWh0dG_chartTooltip{pointer-events:none;z-index:2;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);box-shadow:var(--dsw-shadow-lv2);white-space:nowrap;border-radius:10px;padding:8px 10px;position:absolute;transform:translate(-50%,calc(-100% - 10px))}.VWh0dG_chartTooltipDate{color:var(--dsw-alias-label-primary);margin-bottom:3px;font-size:11px;font-weight:600}.VWh0dG_chartTooltipRow{color:var(--dsw-alias-label-secondary);align-items:center;gap:5px;font-size:11px;line-height:17px;display:flex}.VWh0dG_chartTooltipRow strong{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;font-weight:600}.VWh0dG_chartLegendLine{background:var(--dsw-static-blue-500);border-radius:2px;width:10px;height:3px;display:inline-block}.VWh0dG_chartLegendBar{background:var(--dsw-static-blue-500);opacity:.5;border-radius:2px;width:7px;height:7px;display:inline-block}.VWh0dG_chartLegend{color:var(--dsw-alias-label-tertiary);justify-content:flex-end;gap:14px;margin-top:2px;font-size:11px;display:flex}.VWh0dG_chartLegend span{align-items:center;gap:5px;display:inline-flex}.VWh0dG_tableScroll{border:1px solid var(--dsw-alias-border-l1);border-radius:12px;overflow:auto}.VWh0dG_modelTable,.VWh0dG_pricingTable{border-collapse:collapse;width:100%;font-size:12.5px}.VWh0dG_modelTable th,.VWh0dG_modelTable td,.VWh0dG_pricingTable th,.VWh0dG_pricingTable td{border-bottom:1px solid var(--dsw-alias-border-l1);text-align:left;white-space:nowrap;padding:9px 12px}.VWh0dG_modelTable tbody tr:last-child td,.VWh0dG_pricingTable tbody tr:last-child td{border-bottom:0}.VWh0dG_modelTable thead th,.VWh0dG_pricingTable thead th{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-module-platform);font-size:11px;font-weight:500}.VWh0dG_modelTable tbody tr,.VWh0dG_pricingTable tbody tr{transition:background-color .12s}.VWh0dG_modelTable tbody tr:hover,.VWh0dG_pricingTable tbody tr:hover{background:var(--dsw-alias-interactive-bg-hover)}.VWh0dG_numCol{text-align:right;font-variant-numeric:tabular-nums}.VWh0dG_costCol{color:var(--dsw-static-blue-500);font-weight:600}.VWh0dG_na{color:var(--dsw-alias-label-dimmed)}.VWh0dG_modelCell{align-items:center;gap:8px;display:inline-flex}.VWh0dG_modelDot{width:9px;height:9px;box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-bg-layer-1) 60%, transparent);border-radius:50%;flex:none}.VWh0dG_modelName{color:var(--dsw-alias-label-primary);font-weight:500;line-height:16px;display:block}.VWh0dG_modelProvider{color:var(--dsw-alias-label-caption);font-size:10.5px;line-height:14px;display:block}.VWh0dG_pricingToggle{cursor:pointer;text-align:left;background:0 0;border:none;justify-content:space-between;align-items:center;gap:10px;width:100%;padding:0;display:flex}.VWh0dG_pricingToggle:hover .VWh0dG_panelTitle,.VWh0dG_pricingToggle:focus-visible .VWh0dG_panelTitle{color:var(--dsw-static-blue-500)}.VWh0dG_pricingToggleText{align-items:baseline;gap:10px;min-width:0;display:flex}.VWh0dG_pricingChevron{width:16px;height:16px;color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.VWh0dG_pricingChevronOpen{transform:rotate(180deg)}.VWh0dG_bandPrice{align-items:baseline;gap:6px;display:inline-flex}.VWh0dG_bandPriceOff{color:var(--dsw-alias-label-caption)}.VWh0dG_bandPriceOff:before{content:\"/\";color:var(--dsw-alias-label-dimmed);margin-right:4px}.VWh0dG_bandTag{color:var(--dsw-alias-label-secondary);flex-direction:column;gap:1px;font-size:10.5px;line-height:14px;display:inline-flex}.VWh0dG_bandTagOff{color:var(--dsw-static-green-500);font-weight:600}.VWh0dG_flatTag{color:var(--dsw-alias-label-caption);font-size:10.5px;line-height:14px}.VWh0dG_healthDot{border-radius:50%;flex:none;width:8px;height:8px;display:inline-block}.VWh0dG_healthOk{background:var(--dsw-static-green-500);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-static-green-500) 18%, transparent)}.VWh0dG_healthBad{background:var(--dsw-static-red-500);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-static-red-500) 18%, transparent)}.VWh0dG_healthIdle{background:var(--dsw-static-neutral-bluish-400)}.VWh0dG_dashboardRight{align-items:center;gap:8px;min-width:0;display:flex}.VWh0dG_healthBadge{white-space:nowrap;border-radius:999px;align-items:center;gap:6px;padding:3px 9px;font-size:11px;line-height:16px;display:inline-flex}.VWh0dG_healthBadgeOk{color:var(--dsw-static-green-500);background:color-mix(in srgb, var(--dsw-static-green-500) 10%, transparent)}.VWh0dG_healthBadgeBad{color:var(--dsw-static-red-500);background:color-mix(in srgb, var(--dsw-static-red-500) 10%, transparent)}.VWh0dG_planTag{color:var(--dsw-static-green-500);background:color-mix(in srgb, var(--dsw-static-green-500) 12%, transparent);border-radius:999px;align-items:center;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;display:inline-flex}@media (width<=640px){.VWh0dG_kpiGrid{grid-template-columns:repeat(2,minmax(0,1fr))}.VWh0dG_hero{flex-direction:column;align-items:flex-start;gap:12px}.VWh0dG_heroMain{width:100%;min-width:0}.VWh0dG_heroDivider{align-self:stretch;width:100%;height:1px}.VWh0dG_heroSide{min-width:0}}";
26
+ const css = ".VWh0dG_railButton{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);width:36px;height:36px;color:var(--dsw-static-blue-500);cursor:pointer;border-radius:10px;justify-content:center;align-items:center;padding:0;transition:background-color .14s,border-color .14s,color .14s;display:flex}.VWh0dG_railButton:hover,.VWh0dG_railButton:focus-visible{background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l3);color:var(--dsw-static-blue-400);outline:none}.VWh0dG_railButton svg{stroke-width:2px;width:18px;height:18px}.VWh0dG_trigger{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);width:100%;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;border-radius:12px;align-items:center;gap:10px;padding:9px 12px;transition:border-color .14s,background-color .14s,transform .12s;display:flex}.VWh0dG_trigger:hover,.VWh0dG_trigger:focus-visible{border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover);outline:none}.VWh0dG_trigger:active{transform:scale(.985)}.VWh0dG_triggerIcon{background:linear-gradient(135deg, var(--dsw-static-blue-500), var(--dsw-static-deepseek-500));width:30px;height:30px;color:var(--dsw-static-neutral-bluish-00);border-radius:9px;flex:none;justify-content:center;align-items:center;display:inline-flex}.VWh0dG_triggerIcon svg{stroke-width:2.2px;width:15px;height:15px}.VWh0dG_triggerText{flex-direction:column;gap:1px;min-width:0;display:flex}.VWh0dG_triggerLabel{color:var(--dsw-alias-label-primary);font-size:12px;font-weight:500;line-height:16px}.VWh0dG_triggerMeta{color:var(--dsw-alias-label-tertiary);white-space:nowrap;align-items:baseline;gap:6px;font-size:11px;line-height:15px;display:flex}.VWh0dG_triggerMeta strong{color:var(--dsw-static-blue-500);font-variant-numeric:tabular-nums;font-size:13px;font-weight:600}.VWh0dG_triggerMeta em{color:var(--dsw-alias-label-caption);font-style:normal}.VWh0dG_triggerChevron{width:14px;height:14px;color:var(--dsw-alias-label-caption);flex:none;margin-left:auto}.VWh0dG_dashboardModal{width:min(760px,100vw - 48px);max-height:min(760px,88vh)}.VWh0dG_dashboard{flex-direction:column;width:100%;max-height:min(760px,88vh);display:flex}.VWh0dG_dashboardHead{border-bottom:1px solid var(--dsw-alias-border-l1);justify-content:space-between;align-items:flex-start;gap:12px;padding:20px 24px 14px;display:flex}.VWh0dG_dashboardTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:17px;font-weight:600;line-height:24px}.VWh0dG_dashboardSubtitle{color:var(--dsw-alias-label-caption);margin:3px 0 0;font-size:12px;line-height:17px}.VWh0dG_closeButton{width:30px;height:30px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:8px;flex:none;justify-content:center;align-items:center;transition:background-color .14s;display:inline-flex}.VWh0dG_closeButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.VWh0dG_closeButton svg{stroke-width:2px;width:16px;height:16px}.VWh0dG_dashboardBody{flex-direction:column;gap:14px;padding:16px 24px 24px;display:flex;overflow-y:auto}.VWh0dG_hero{background:linear-gradient(135deg, var(--dsw-static-blue-800) 0%, var(--dsw-static-deepseek-600) 55%, var(--dsw-static-deepseek-500) 100%);min-height:118px;color:var(--dsw-static-neutral-bluish-00);border-radius:16px;align-items:center;gap:24px;padding:18px 22px;display:flex;position:relative;overflow:hidden}.VWh0dG_hero:after{content:\"\";pointer-events:none;background:radial-gradient(circle,#ffffff29 0%,#fff0 70%);border-radius:50%;width:200px;height:200px;position:absolute;top:-60px;right:-40px}.VWh0dG_heroMain{flex-direction:column;flex:auto;justify-content:center;gap:3px;min-width:0;display:flex}.VWh0dG_heroLabel{opacity:.82;white-space:nowrap;font-size:12px;font-weight:500;line-height:17px}.VWh0dG_heroValue{letter-spacing:-.02em;font-variant-numeric:tabular-nums;white-space:nowrap;font-size:38px;font-weight:700;line-height:46px}.VWh0dG_heroMeta{opacity:.82;white-space:nowrap;align-items:center;gap:6px;font-size:12px;line-height:17px;display:flex}.VWh0dG_heroMeta em{background:#ffffff29;border-radius:6px;margin-left:2px;padding:1px 6px;font-style:normal}.VWh0dG_heroDivider{background:#ffffff38;flex:none;align-self:stretch;width:1px}.VWh0dG_heroSide{flex:none;align-items:center;gap:28px;min-width:140px;display:flex}.VWh0dG_heroSideItem{flex-direction:column;justify-content:center;gap:3px;display:flex}.VWh0dG_heroSideLabel{opacity:.82;white-space:nowrap;font-size:12px;line-height:17px}.VWh0dG_heroSideValue{font-variant-numeric:tabular-nums;white-space:nowrap;font-size:22px;font-weight:600;line-height:28px}.VWh0dG_delta{align-items:center;gap:2px;font-size:12px;font-weight:600;line-height:17px;display:inline-flex}.VWh0dG_deltaUp{color:#9ff0b8}.VWh0dG_deltaDown{color:#ffb4b4}.VWh0dG_kpiGrid{grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;display:grid}.VWh0dG_kpiTile{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:14px;flex-direction:column;gap:3px;padding:14px 15px;transition:border-color .14s,background-color .14s;display:flex}.VWh0dG_kpiTile:hover{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-interactive-bg-hover)}.VWh0dG_kpiLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:15px}.VWh0dG_kpiValue{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;font-size:21px;font-weight:600;line-height:27px}.VWh0dG_kpiGreen{color:var(--dsw-static-green-500)}.VWh0dG_kpiDetail{color:var(--dsw-alias-label-caption);white-space:nowrap;text-overflow:ellipsis;font-size:11px;line-height:15px;overflow:hidden}.VWh0dG_panel{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:16px;flex-direction:column;gap:10px;padding:16px 18px;display:flex}.VWh0dG_panelHead{justify-content:space-between;align-items:baseline;gap:10px;display:flex}.VWh0dG_panelTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:14px;font-weight:600;line-height:20px}.VWh0dG_panelHint{color:var(--dsw-alias-label-caption);white-space:nowrap;font-size:11px;line-height:16px}.VWh0dG_chartWrap{width:100%;position:relative}.VWh0dG_chartSvg{width:100%;height:auto;display:block}.VWh0dG_chartEmpty{height:140px;color:var(--dsw-alias-label-caption);justify-content:center;align-items:center;font-size:13px;display:flex}.VWh0dG_emptyRow{text-align:center;color:var(--dsw-alias-label-caption);padding:28px 0;font-size:13px}.VWh0dG_chartGrid{stroke:var(--dsw-alias-border-l1);stroke-width:1px}.VWh0dG_chartAxisLabel{fill:var(--dsw-alias-label-tertiary);font-size:10px}.VWh0dG_chartBar{fill:var(--dsw-static-blue-500);opacity:.35}.VWh0dG_chartBar:hover{opacity:.6}.VWh0dG_chartStack{stroke:var(--dsw-alias-bg-layer-1);stroke-width:.75px}.VWh0dG_chartStack:hover{opacity:.85}.VWh0dG_chartLine{stroke:var(--dsw-static-blue-500);stroke-linecap:round;stroke-linejoin:round}.VWh0dG_chartCrosshair{stroke:var(--dsw-alias-label-dimmed);stroke-width:1px;stroke-dasharray:3 3}.VWh0dG_chartDot{fill:var(--dsw-static-neutral-bluish-00);stroke:var(--dsw-static-blue-500);stroke-width:2.5px}.VWh0dG_chartTooltip{pointer-events:none;z-index:2;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);box-shadow:var(--dsw-shadow-lv2);white-space:nowrap;border-radius:10px;padding:8px 10px;position:absolute;transform:translate(-50%,calc(-100% - 10px))}.VWh0dG_chartTooltipDate{color:var(--dsw-alias-label-primary);margin-bottom:3px;font-size:11px;font-weight:600}.VWh0dG_chartTooltipRow{color:var(--dsw-alias-label-secondary);align-items:center;gap:5px;font-size:11px;line-height:17px;display:flex}.VWh0dG_chartTooltipRow strong{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;font-weight:600}.VWh0dG_chartLegendLine{background:var(--dsw-static-blue-500);border-radius:2px;width:10px;height:3px;display:inline-block}.VWh0dG_chartLegendBar{background:var(--dsw-static-blue-500);opacity:.5;border-radius:2px;width:7px;height:7px;display:inline-block}.VWh0dG_chartTooltipSwatch{border-radius:3px;flex:none;width:8px;height:8px;display:inline-block}.VWh0dG_chartLegend{color:var(--dsw-alias-label-tertiary);justify-content:flex-end;gap:14px;margin-top:2px;font-size:11px;display:flex}.VWh0dG_chartLegend span{align-items:center;gap:5px;display:inline-flex}.VWh0dG_tableScroll{border:1px solid var(--dsw-alias-border-l1);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border-radius:12px;overflow:auto}.VWh0dG_modelTable,.VWh0dG_pricingTable{border-collapse:collapse;width:100%;font-size:12.5px}.VWh0dG_modelTable th,.VWh0dG_modelTable td,.VWh0dG_pricingTable th,.VWh0dG_pricingTable td{border-bottom:1px solid var(--dsw-alias-border-l1);text-align:left;white-space:nowrap;padding:9px 12px}.VWh0dG_modelTable tbody tr:last-child td,.VWh0dG_pricingTable tbody tr:last-child td{border-bottom:0}.VWh0dG_modelTable thead th,.VWh0dG_pricingTable thead th{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-module-platform);font-size:11px;font-weight:500}.VWh0dG_modelTable tbody tr,.VWh0dG_pricingTable tbody tr{transition:background-color .12s}.VWh0dG_modelTable tbody tr:hover,.VWh0dG_pricingTable tbody tr:hover{background:var(--dsw-alias-interactive-bg-hover)}.VWh0dG_numCol{text-align:right;font-variant-numeric:tabular-nums}.VWh0dG_costCol{color:var(--dsw-static-blue-500);font-weight:600}.VWh0dG_na{color:var(--dsw-alias-label-dimmed)}.VWh0dG_modelCell{align-items:center;gap:8px;display:inline-flex}.VWh0dG_modelDot{width:9px;height:9px;box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-bg-layer-1) 60%, transparent);border-radius:50%;flex:none}.VWh0dG_modelName{color:var(--dsw-alias-label-primary);font-weight:500;line-height:16px;display:block}.VWh0dG_modelProvider{color:var(--dsw-alias-label-caption);font-size:10.5px;line-height:14px;display:block}.VWh0dG_pricingToggle{cursor:pointer;text-align:left;background:0 0;border:none;justify-content:space-between;align-items:center;gap:10px;width:100%;padding:0;display:flex}.VWh0dG_pricingToggle:hover .VWh0dG_panelTitle,.VWh0dG_pricingToggle:focus-visible .VWh0dG_panelTitle{color:var(--dsw-static-blue-500)}.VWh0dG_pricingToggleText{align-items:baseline;gap:10px;min-width:0;display:flex}.VWh0dG_pricingChevron{width:16px;height:16px;color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.VWh0dG_pricingChevronOpen{transform:rotate(180deg)}.VWh0dG_bandPrice{align-items:baseline;gap:6px;display:inline-flex}.VWh0dG_bandPriceOff{color:var(--dsw-alias-label-caption)}.VWh0dG_bandPriceOff:before{content:\"/\";color:var(--dsw-alias-label-dimmed);margin-right:4px}.VWh0dG_bandTag{color:var(--dsw-alias-label-secondary);flex-direction:column;gap:1px;font-size:10.5px;line-height:14px;display:inline-flex}.VWh0dG_bandTagOff{color:var(--dsw-static-green-500);font-weight:600}.VWh0dG_flatTag{color:var(--dsw-alias-label-caption);font-size:10.5px;line-height:14px}.VWh0dG_healthDot{border-radius:50%;flex:none;width:8px;height:8px;display:inline-block}.VWh0dG_healthOk{background:var(--dsw-static-green-500);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-static-green-500) 18%, transparent)}.VWh0dG_healthBad{background:var(--dsw-static-red-500);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-static-red-500) 18%, transparent)}.VWh0dG_healthIdle{background:var(--dsw-static-neutral-bluish-400)}.VWh0dG_dashboardRight{align-items:center;gap:8px;min-width:0;display:flex}.VWh0dG_healthBadge{white-space:nowrap;border-radius:999px;align-items:center;gap:6px;padding:3px 9px;font-size:11px;line-height:16px;display:inline-flex}.VWh0dG_healthBadgeOk{color:var(--dsw-static-green-500);background:color-mix(in srgb, var(--dsw-static-green-500) 10%, transparent)}.VWh0dG_healthBadgeBad{color:var(--dsw-static-red-500);background:color-mix(in srgb, var(--dsw-static-red-500) 10%, transparent)}.VWh0dG_planTag{color:var(--dsw-static-green-500);background:color-mix(in srgb, var(--dsw-static-green-500) 12%, transparent);border-radius:999px;align-items:center;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;display:inline-flex}@media (width<=640px){.VWh0dG_kpiGrid{grid-template-columns:repeat(2,minmax(0,1fr))}.VWh0dG_hero{flex-direction:column;align-items:flex-start;gap:12px}.VWh0dG_heroMain{width:100%;min-width:0}.VWh0dG_heroDivider{align-self:stretch;width:100%;height:1px}.VWh0dG_heroSide{min-width:0}}";
27
27
  const tagId = "@kenz1117/dsh-ui-usage-billing/UsageBilling.module.css";
28
28
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
29
29
  const tag = document.createElement("style");
@@ -33,87 +33,89 @@ window.__ModuleLoader__.load({
33
33
  document.head.appendChild(tag);
34
34
  }
35
35
  var UsageBilling_module_css_default = {
36
- "delta": "VWh0dG_delta",
37
- "modelCell": "VWh0dG_modelCell",
38
- "healthBad": "VWh0dG_healthBad",
39
- "dashboardModal": "VWh0dG_dashboardModal",
40
- "numCol": "VWh0dG_numCol",
41
- "healthOk": "VWh0dG_healthOk",
42
- "chartAxisLabel": "VWh0dG_chartAxisLabel",
43
- "triggerText": "VWh0dG_triggerText",
44
- "panelTitle": "VWh0dG_panelTitle",
45
- "triggerChevron": "VWh0dG_triggerChevron",
46
- "triggerIcon": "VWh0dG_triggerIcon",
36
+ "chartTooltipDate": "VWh0dG_chartTooltipDate",
47
37
  "chartCrosshair": "VWh0dG_chartCrosshair",
48
- "chartLegendLine": "VWh0dG_chartLegendLine",
49
- "kpiTile": "VWh0dG_kpiTile",
50
- "heroSideValue": "VWh0dG_heroSideValue",
51
- "pricingChevron": "VWh0dG_pricingChevron",
52
- "deltaDown": "VWh0dG_deltaDown",
53
- "healthDot": "VWh0dG_healthDot",
38
+ "panelTitle": "VWh0dG_panelTitle",
39
+ "pricingToggleText": "VWh0dG_pricingToggleText",
40
+ "heroValue": "VWh0dG_heroValue",
41
+ "trigger": "VWh0dG_trigger",
42
+ "pricingChevronOpen": "VWh0dG_pricingChevronOpen",
54
43
  "kpiLabel": "VWh0dG_kpiLabel",
55
- "kpiGreen": "VWh0dG_kpiGreen",
56
- "tableScroll": "VWh0dG_tableScroll",
57
- "panel": "VWh0dG_panel",
44
+ "heroDivider": "VWh0dG_heroDivider",
58
45
  "dashboardSubtitle": "VWh0dG_dashboardSubtitle",
59
- "heroSideItem": "VWh0dG_heroSideItem",
60
- "heroLabel": "VWh0dG_heroLabel",
61
- "chartBar": "VWh0dG_chartBar",
62
- "chartTooltipRow": "VWh0dG_chartTooltipRow",
46
+ "modelCell": "VWh0dG_modelCell",
47
+ "bandTag": "VWh0dG_bandTag",
48
+ "healthDot": "VWh0dG_healthDot",
49
+ "deltaDown": "VWh0dG_deltaDown",
50
+ "triggerChevron": "VWh0dG_triggerChevron",
51
+ "dashboardRight": "VWh0dG_dashboardRight",
52
+ "chartLegend": "VWh0dG_chartLegend",
53
+ "heroSideValue": "VWh0dG_heroSideValue",
63
54
  "chartWrap": "VWh0dG_chartWrap",
64
- "healthBadgeBad": "VWh0dG_healthBadgeBad",
65
- "heroMain": "VWh0dG_heroMain",
66
- "chartEmpty": "VWh0dG_chartEmpty",
67
- "kpiGrid": "VWh0dG_kpiGrid",
68
- "chartTooltip": "VWh0dG_chartTooltip",
69
55
  "heroMeta": "VWh0dG_heroMeta",
70
- "chartTooltipDate": "VWh0dG_chartTooltipDate",
71
- "modelDot": "VWh0dG_modelDot",
72
- "healthBadgeOk": "VWh0dG_healthBadgeOk",
73
- "planTag": "VWh0dG_planTag",
74
- "kpiValue": "VWh0dG_kpiValue",
75
- "healthBadge": "VWh0dG_healthBadge",
76
- "modelName": "VWh0dG_modelName",
77
- "emptyRow": "VWh0dG_emptyRow",
78
- "heroValue": "VWh0dG_heroValue",
79
- "na": "VWh0dG_na",
80
- "dashboardRight": "VWh0dG_dashboardRight",
56
+ "heroSide": "VWh0dG_heroSide",
57
+ "triggerText": "VWh0dG_triggerText",
58
+ "tableScroll": "VWh0dG_tableScroll",
59
+ "flatTag": "VWh0dG_flatTag",
60
+ "chartLegendBar": "VWh0dG_chartLegendBar",
81
61
  "dashboardHead": "VWh0dG_dashboardHead",
62
+ "chartAxisLabel": "VWh0dG_chartAxisLabel",
63
+ "pricingChevron": "VWh0dG_pricingChevron",
64
+ "modelProvider": "VWh0dG_modelProvider",
82
65
  "chartLine": "VWh0dG_chartLine",
83
- "healthIdle": "VWh0dG_healthIdle",
84
- "costCol": "VWh0dG_costCol",
85
- "bandTag": "VWh0dG_bandTag",
86
- "panelHead": "VWh0dG_panelHead",
87
- "dashboardBody": "VWh0dG_dashboardBody",
88
- "chartSvg": "VWh0dG_chartSvg",
89
66
  "chartDot": "VWh0dG_chartDot",
67
+ "bandPrice": "VWh0dG_bandPrice",
90
68
  "bandTagOff": "VWh0dG_bandTagOff",
91
- "dashboardTitle": "VWh0dG_dashboardTitle",
92
- "heroDivider": "VWh0dG_heroDivider",
93
- "chartGrid": "VWh0dG_chartGrid",
94
- "pricingToggleText": "VWh0dG_pricingToggleText",
95
- "heroSide": "VWh0dG_heroSide",
96
- "kpiDetail": "VWh0dG_kpiDetail",
97
- "chartLegend": "VWh0dG_chartLegend",
98
- "modelTable": "VWh0dG_modelTable",
99
- "pricingTable": "VWh0dG_pricingTable",
100
- "modelProvider": "VWh0dG_modelProvider",
101
69
  "hero": "VWh0dG_hero",
102
- "heroSideLabel": "VWh0dG_heroSideLabel",
103
- "triggerMeta": "VWh0dG_triggerMeta",
104
- "chartLegendBar": "VWh0dG_chartLegendBar",
105
- "deltaUp": "VWh0dG_deltaUp",
70
+ "modelTable": "VWh0dG_modelTable",
71
+ "planTag": "VWh0dG_planTag",
72
+ "chartGrid": "VWh0dG_chartGrid",
73
+ "triggerIcon": "VWh0dG_triggerIcon",
74
+ "healthOk": "VWh0dG_healthOk",
75
+ "chartTooltipSwatch": "VWh0dG_chartTooltipSwatch",
76
+ "healthIdle": "VWh0dG_healthIdle",
106
77
  "pricingToggle": "VWh0dG_pricingToggle",
107
- "railButton": "VWh0dG_railButton",
108
- "pricingChevronOpen": "VWh0dG_pricingChevronOpen",
109
- "bandPrice": "VWh0dG_bandPrice",
78
+ "kpiTile": "VWh0dG_kpiTile",
79
+ "panelHead": "VWh0dG_panelHead",
80
+ "chartBar": "VWh0dG_chartBar",
81
+ "na": "VWh0dG_na",
82
+ "chartTooltip": "VWh0dG_chartTooltip",
83
+ "bandPriceOff": "VWh0dG_bandPriceOff",
110
84
  "triggerLabel": "VWh0dG_triggerLabel",
85
+ "numCol": "VWh0dG_numCol",
111
86
  "panelHint": "VWh0dG_panelHint",
87
+ "healthBadgeOk": "VWh0dG_healthBadgeOk",
88
+ "healthBadgeBad": "VWh0dG_healthBadgeBad",
89
+ "chartStack": "VWh0dG_chartStack",
90
+ "dashboardModal": "VWh0dG_dashboardModal",
91
+ "kpiValue": "VWh0dG_kpiValue",
92
+ "chartSvg": "VWh0dG_chartSvg",
93
+ "heroMain": "VWh0dG_heroMain",
94
+ "chartLegendLine": "VWh0dG_chartLegendLine",
95
+ "dashboardTitle": "VWh0dG_dashboardTitle",
112
96
  "closeButton": "VWh0dG_closeButton",
113
- "bandPriceOff": "VWh0dG_bandPriceOff",
114
- "flatTag": "VWh0dG_flatTag",
97
+ "costCol": "VWh0dG_costCol",
98
+ "healthBad": "VWh0dG_healthBad",
99
+ "triggerMeta": "VWh0dG_triggerMeta",
100
+ "modelName": "VWh0dG_modelName",
101
+ "delta": "VWh0dG_delta",
102
+ "healthBadge": "VWh0dG_healthBadge",
103
+ "chartEmpty": "VWh0dG_chartEmpty",
115
104
  "dashboard": "VWh0dG_dashboard",
116
- "trigger": "VWh0dG_trigger"
105
+ "heroLabel": "VWh0dG_heroLabel",
106
+ "deltaUp": "VWh0dG_deltaUp",
107
+ "heroSideItem": "VWh0dG_heroSideItem",
108
+ "kpiGrid": "VWh0dG_kpiGrid",
109
+ "dashboardBody": "VWh0dG_dashboardBody",
110
+ "kpiDetail": "VWh0dG_kpiDetail",
111
+ "panel": "VWh0dG_panel",
112
+ "railButton": "VWh0dG_railButton",
113
+ "emptyRow": "VWh0dG_emptyRow",
114
+ "chartTooltipRow": "VWh0dG_chartTooltipRow",
115
+ "pricingTable": "VWh0dG_pricingTable",
116
+ "heroSideLabel": "VWh0dG_heroSideLabel",
117
+ "kpiGreen": "VWh0dG_kpiGreen",
118
+ "modelDot": "VWh0dG_modelDot"
117
119
  };
118
120
  //#endregion
119
121
  //#region src/client/pricing.ts
@@ -615,7 +617,11 @@ window.__ModuleLoader__.load({
615
617
  ];
616
618
  /** Lookup a model by its stats key; falls back to the generic `other` entry. */
617
619
  function modelOf(key) {
618
- return MODEL_CATALOG.find((entry) => entry.key === key) ?? MODEL_CATALOG[MODEL_CATALOG.length - 1];
620
+ const found = MODEL_CATALOG.find((entry) => entry.key === key);
621
+ if (found !== void 0) return found;
622
+ const fallback = MODEL_CATALOG.at(-1);
623
+ if (fallback !== void 0) return fallback;
624
+ throw new Error("MODEL_CATALOG must not be empty");
619
625
  }
620
626
  /** Resolve a price-table row by its CSS variable name (theme token or fallback color). */
621
627
  function resolveToken(name) {
@@ -685,9 +691,11 @@ window.__ModuleLoader__.load({
685
691
  //#endregion
686
692
  //#region src/client/TrendChart.tsx
687
693
  /**
688
- * TrendChart: dependency-free SVG combo chart of daily cost (area line) and
689
- * daily call volume (bars) with a hover crosshair. No chart library the
690
- * surface stays self-contained and offline.
694
+ * TrendChart: dependency-free SVG stacked bar chart of daily cost per model.
695
+ * Each day's column stacks every model's share in its brand color, so the
696
+ * total trend and the per-model composition are visible at once. A hover
697
+ * crosshair shows the day's model breakdown. No chart library — the surface
698
+ * stays self-contained and offline.
691
699
  */
692
700
  /** Fixed viewBox; the SVG scales to its container. */
693
701
  const W = 680;
@@ -703,19 +711,6 @@ window.__ModuleLoader__.load({
703
711
  const [, month, day] = iso.split("-");
704
712
  return `${Number(month)}/${Number(day)}`;
705
713
  }
706
- /** Build an SVG path string through points with a smooth monotone-ish curve. */
707
- function linePath(points) {
708
- if (points.length === 0) return "";
709
- if (points.length === 1) return `M ${points[0].x} ${points[0].y}`;
710
- let d = `M ${points[0].x} ${points[0].y}`;
711
- for (let i = 1; i < points.length; i += 1) {
712
- const prev = points[i - 1];
713
- const curr = points[i];
714
- const mx = (prev.x + curr.x) / 2;
715
- d += ` C ${mx} ${prev.y}, ${mx} ${curr.y}, ${curr.x} ${curr.y}`;
716
- }
717
- return d;
718
- }
719
714
  /** Ticks every `step` items for sparse axis labels. */
720
715
  function tickIndexes(length, step) {
721
716
  const out = [];
@@ -724,16 +719,16 @@ window.__ModuleLoader__.load({
724
719
  return out;
725
720
  }
726
721
  /**
727
- * Render the daily trend chart.
722
+ * Render the daily per-model stacked cost chart.
728
723
  * @param props.data - sorted daily rows (ascending date).
724
+ * @param props.models - the model legend, in stack order (bottom first).
729
725
  */
730
- function TrendChart({ data }) {
726
+ function TrendChart({ data, models = [] }) {
731
727
  const [hover, setHover] = (0, react.useState)(null);
732
728
  const layout = (0, react.useMemo)(() => {
733
729
  const n = data.length;
734
730
  if (n === 0) return null;
735
731
  const maxCost = Math.max(...data.map((d) => d.cost), 1e-4);
736
- const maxCalls = Math.max(...data.map((d) => d.calls), 1);
737
732
  const plotW = W - PAD.left - PAD.right;
738
733
  const plotH = H - PAD.top - PAD.bottom;
739
734
  const inner = (i) => {
@@ -741,22 +736,35 @@ window.__ModuleLoader__.load({
741
736
  return PAD.left + plotW * i / (n - 1);
742
737
  };
743
738
  const yCost = (value) => PAD.top + plotH - value / maxCost * plotH;
744
- const yCalls = (value) => PAD.top + plotH - value / maxCalls * plotH;
745
- const barW = Math.min(16, plotW / n * .5);
746
- const line = linePath(data.map((d, i) => ({
747
- x: inner(i),
748
- y: yCost(d.cost)
749
- })));
750
739
  return {
751
740
  n,
752
741
  plotW,
753
742
  plotH,
754
743
  inner,
755
744
  yCost,
756
- yCalls,
757
- barW,
758
- line,
759
- area: line.length === 0 ? "" : `${line} L ${inner(n - 1)} ${PAD.top + plotH} L ${inner(0)} ${PAD.top + plotH} Z`,
745
+ barW: Math.min(18, plotW / n * .5),
746
+ columns: data.map((d, i) => {
747
+ let acc = 0;
748
+ const segments = models.map((model) => {
749
+ const value = d.byModel?.[model.key] ?? 0;
750
+ const y0 = acc;
751
+ acc += value;
752
+ return {
753
+ model,
754
+ y0,
755
+ y1: acc,
756
+ rounded: false
757
+ };
758
+ });
759
+ const top = segments.at(-1);
760
+ if (top !== void 0) top.rounded = true;
761
+ return {
762
+ date: d.date,
763
+ x: inner(i),
764
+ segments,
765
+ total: acc
766
+ };
767
+ }),
760
768
  costTicks: [
761
769
  0,
762
770
  .25,
@@ -764,23 +772,16 @@ window.__ModuleLoader__.load({
764
772
  .75,
765
773
  1
766
774
  ].map((f) => maxCost * f).reverse(),
767
- callTicks: [
768
- 0,
769
- .25,
770
- .5,
771
- .75,
772
- 1
773
- ].map((f) => maxCalls * f).reverse(),
774
- maxCost,
775
- maxCalls
775
+ maxCost
776
776
  };
777
- }, [data]);
777
+ }, [data, models]);
778
778
  if (layout === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
779
779
  className: UsageBilling_module_css_default.chartEmpty,
780
780
  children: "暂无趋势数据"
781
781
  });
782
- const { n, plotW, inner, yCost, yCalls, barW, line, area, costTicks, callTicks } = layout;
783
- const active = hover === null ? null : data[hover];
782
+ const { n, plotW, inner, yCost, barW, columns, costTicks } = layout;
783
+ const activeColumn = hover === null ? void 0 : columns[hover];
784
+ const activePoint = hover === null ? void 0 : data[hover];
784
785
  const indices = tickIndexes(n, Math.max(1, Math.ceil(n / 8)));
785
786
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
786
787
  className: UsageBilling_module_css_default.chartWrap,
@@ -789,7 +790,7 @@ window.__ModuleLoader__.load({
789
790
  viewBox: `0 0 ${W} ${H}`,
790
791
  className: UsageBilling_module_css_default.chartSvg,
791
792
  role: "img",
792
- "aria-label": "Daily cost and call volume trend",
793
+ "aria-label": "Daily cost by model",
793
794
  onMouseLeave: () => {
794
795
  setHover(null);
795
796
  },
@@ -800,22 +801,6 @@ window.__ModuleLoader__.load({
800
801
  setHover(Math.min(Math.max(index, 0), n - 1));
801
802
  },
802
803
  children: [
803
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("linearGradient", {
804
- id: "billing-cost-fill",
805
- x1: "0",
806
- y1: "0",
807
- x2: "0",
808
- y2: "1",
809
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("stop", {
810
- offset: "0%",
811
- stopColor: "var(--dsw-static-blue-500)",
812
- stopOpacity: "0.28"
813
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("stop", {
814
- offset: "100%",
815
- stopColor: "var(--dsw-static-blue-500)",
816
- stopOpacity: "0.02"
817
- })]
818
- }) }),
819
804
  costTicks.map((value, idx) => {
820
805
  const y = yCost(value);
821
806
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
@@ -832,86 +817,82 @@ window.__ModuleLoader__.load({
832
817
  children: formatMoney(value)
833
818
  })] }, `cost-${idx}`);
834
819
  }),
835
- callTicks.map((value, idx) => {
836
- const y = yCalls(value);
820
+ columns.map((column) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("g", { children: column.segments.map((segment) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
821
+ x: column.x - barW / 2,
822
+ y: yCost(segment.y1),
823
+ width: barW,
824
+ height: Math.max(segment.y1 - segment.y0 > 0 ? 1 : 0, yCost(segment.y0) - yCost(segment.y1)),
825
+ rx: segment.rounded ? 3 : 0,
826
+ className: UsageBilling_module_css_default.chartStack,
827
+ style: { fill: segment.model.color }
828
+ }, segment.model.key)) }, column.date)),
829
+ indices.map((i) => {
830
+ const point = data[i];
831
+ if (point === void 0) return null;
837
832
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
838
- x: W - PAD.right + 8,
839
- y: y + 3,
833
+ x: inner(i),
834
+ y: H - 6,
835
+ textAnchor: "middle",
840
836
  className: UsageBilling_module_css_default.chartAxisLabel,
841
- children: value >= 1e3 ? `${Math.round(value / 1e3)}k` : String(Math.round(value))
842
- }, `calls-${idx}`);
843
- }),
844
- data.map((d, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
845
- x: inner(i) - barW / 2,
846
- y: yCalls(d.calls),
847
- width: barW,
848
- height: Math.max(1, PAD.top + layout.plotH - yCalls(d.calls)),
849
- rx: 2,
850
- className: UsageBilling_module_css_default.chartBar
851
- }, d.date)),
852
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
853
- d: area,
854
- fill: "url(#billing-cost-fill)"
837
+ children: shortDate(point.date)
838
+ }, point.date);
855
839
  }),
856
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
857
- d: line,
858
- fill: "none",
859
- className: UsageBilling_module_css_default.chartLine,
860
- strokeWidth: 2
861
- }),
862
- indices.map((i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
863
- x: inner(i),
864
- y: H - 6,
865
- textAnchor: "middle",
866
- className: UsageBilling_module_css_default.chartAxisLabel,
867
- children: shortDate(data[i].date)
868
- }, data[i].date)),
869
- active !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
840
+ activeColumn !== void 0 && hover !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
870
841
  x1: inner(hover),
871
842
  x2: inner(hover),
872
843
  y1: PAD.top,
873
844
  y2: PAD.top + layout.plotH,
874
845
  className: UsageBilling_module_css_default.chartCrosshair
875
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
876
- cx: inner(hover),
877
- cy: yCost(active.cost),
878
- r: 4.5,
879
- className: UsageBilling_module_css_default.chartDot
880
- })] })
846
+ })
881
847
  ]
882
848
  }),
883
- active !== null && hover !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
849
+ activeColumn !== void 0 && activePoint !== void 0 && hover !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
884
850
  className: UsageBilling_module_css_default.chartTooltip,
885
851
  style: {
886
852
  left: `${inner(hover) / W * 100}%`,
887
- top: `${yCost(active.cost) / H * 100}%`
853
+ top: `${yCost(activeColumn.total) / H * 100}%`
888
854
  },
889
855
  children: [
890
856
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
891
857
  className: UsageBilling_module_css_default.chartTooltipDate,
892
- children: active.date
858
+ children: activePoint.date
893
859
  }),
860
+ activeColumn.segments.filter((segment) => segment.y1 - segment.y0 > 0).map((segment) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
861
+ className: UsageBilling_module_css_default.chartTooltipRow,
862
+ children: [
863
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
864
+ className: UsageBilling_module_css_default.chartTooltipSwatch,
865
+ style: { background: segment.model.color }
866
+ }),
867
+ segment.model.name,
868
+ " ",
869
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: formatMoney(segment.y1 - segment.y0) })
870
+ ]
871
+ }, segment.model.key)),
894
872
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
895
873
  className: UsageBilling_module_css_default.chartTooltipRow,
896
874
  children: [
897
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: UsageBilling_module_css_default.chartLegendLine }),
898
- "费用 ",
899
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: formatMoney(active.cost) })
875
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: UsageBilling_module_css_default.chartLegendBar }),
876
+ "总计 ",
877
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: formatMoney(activeColumn.total) })
900
878
  ]
901
879
  }),
902
880
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
903
881
  className: UsageBilling_module_css_default.chartTooltipRow,
904
882
  children: [
905
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: UsageBilling_module_css_default.chartLegendBar }),
883
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: UsageBilling_module_css_default.chartLegendLine }),
906
884
  "调用 ",
907
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: active.calls.toLocaleString() })
885
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: activePoint.calls.toLocaleString() })
908
886
  ]
909
887
  })
910
888
  ]
911
889
  }),
912
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
890
+ models.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
913
891
  className: UsageBilling_module_css_default.chartLegend,
914
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: UsageBilling_module_css_default.chartLegendLine }), "每日费用"] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: UsageBilling_module_css_default.chartLegendBar }), "调用次数"] })]
892
+ children: models.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
893
+ className: UsageBilling_module_css_default.chartTooltipSwatch,
894
+ style: { background: model.color }
895
+ }), model.name] }, model.key))
915
896
  })
916
897
  ]
917
898
  });
@@ -1031,7 +1012,8 @@ window.__ModuleLoader__.load({
1031
1012
  cost: 0
1032
1013
  },
1033
1014
  byModel: {},
1034
- byDay: {}
1015
+ byDay: {},
1016
+ byDayModels: {}
1035
1017
  };
1036
1018
  /** Try to load stats from the server; returns null when no valid JSON stats are served. */
1037
1019
  async function loadUsageStats() {
@@ -1051,12 +1033,12 @@ window.__ModuleLoader__.load({
1051
1033
  * @param props - framework props plus `wide` column state.
1052
1034
  */
1053
1035
  function UsageBillingTrigger(props) {
1054
- const { wide, onOpen, totalCost, todayCost } = props;
1036
+ const { wide, onOpen, monthCost, todayCost } = props;
1055
1037
  if (!wide) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1056
1038
  type: "button",
1057
1039
  className: UsageBilling_module_css_default.railButton,
1058
1040
  onClick: onOpen,
1059
- title: formatMoney(totalCost),
1041
+ title: formatMoney(monthCost),
1060
1042
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1061
1043
  viewBox: "0 0 24 24",
1062
1044
  fill: "none",
@@ -1088,8 +1070,8 @@ window.__ModuleLoader__.load({
1088
1070
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1089
1071
  className: UsageBilling_module_css_default.triggerMeta,
1090
1072
  children: [
1091
- "总计 ",
1092
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: formatMoney(totalCost) }),
1073
+ "当月 ",
1074
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: formatMoney(monthCost) }),
1093
1075
  todayCost > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("em", { children: ["今日 ", formatMoney(todayCost)] })
1094
1076
  ]
1095
1077
  })]
@@ -1115,12 +1097,31 @@ window.__ModuleLoader__.load({
1115
1097
  const cacheHitRate = total.cacheHit + total.cacheMiss > 0 ? total.cacheHit / (total.cacheHit + total.cacheMiss) * 100 : 0;
1116
1098
  const dates = Object.keys(byDay).sort();
1117
1099
  const latestDate = dates.at(-1) ?? "";
1118
- const todayCost = byDay[(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)]?.cost ?? 0;
1119
- const trend = (0, react.useMemo)(() => dates.map((date) => ({
1120
- date,
1121
- cost: byDay[date].cost,
1122
- calls: byDay[date].calls
1123
- })), [dates, byDay]);
1100
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1101
+ const todayCost = byDay[today]?.cost ?? 0;
1102
+ const monthPrefix = today.slice(0, 7);
1103
+ const yearPrefix = today.slice(0, 4);
1104
+ const monthCost = dates.reduce((sum, d) => sum + (d.startsWith(monthPrefix) ? byDay[d]?.cost ?? 0 : 0), 0);
1105
+ const monthCalls = dates.reduce((sum, d) => sum + (d.startsWith(monthPrefix) ? byDay[d]?.calls ?? 0 : 0), 0);
1106
+ const yearCost = dates.reduce((sum, d) => sum + (d.startsWith(yearPrefix) ? byDay[d]?.cost ?? 0 : 0), 0);
1107
+ const trend = (0, react.useMemo)(() => dates.map((date) => {
1108
+ const byModel = {};
1109
+ const dayModels = stats.byDayModels?.[date];
1110
+ if (dayModels !== void 0) {
1111
+ for (const [key, data] of Object.entries(dayModels)) if (data.cost > 0) byModel[key] = data.cost;
1112
+ }
1113
+ const day = byDay[date];
1114
+ return {
1115
+ date,
1116
+ cost: day?.cost ?? 0,
1117
+ calls: day?.calls ?? 0,
1118
+ byModel
1119
+ };
1120
+ }), [
1121
+ dates,
1122
+ byDay,
1123
+ stats.byDayModels
1124
+ ]);
1124
1125
  const modelRows = (0, react.useMemo)(() => Object.entries(byModel).filter(([, data]) => data.calls > 0).map(([key, data]) => {
1125
1126
  const entry = modelOf(key);
1126
1127
  const buckets = {
@@ -1146,7 +1147,12 @@ window.__ModuleLoader__.load({
1146
1147
  const estimatedTotal = modelRows.reduce((sum, row) => sum + row.estimated, 0);
1147
1148
  const displayTotal = total.cost > 0 ? total.cost : estimatedTotal;
1148
1149
  const avgPerCall = total.calls > 0 ? displayTotal / total.calls : 0;
1149
- const prevDayCost = trend.length >= 2 ? trend[trend.length - 2].cost : 0;
1150
+ const chartModels = (0, react.useMemo)(() => modelRows.map((row) => ({
1151
+ key: row.key,
1152
+ name: row.name,
1153
+ color: row.color
1154
+ })), [modelRows]);
1155
+ const prevDayCost = trend.length >= 2 ? trend.at(-2)?.cost ?? 0 : 0;
1150
1156
  const deltaPct = prevDayCost > 0 ? (todayCost - prevDayCost) / prevDayCost * 100 : 0;
1151
1157
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
1152
1158
  open: true,
@@ -1201,27 +1207,35 @@ window.__ModuleLoader__.load({
1201
1207
  children: [
1202
1208
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1203
1209
  className: UsageBilling_module_css_default.heroLabel,
1204
- children: t("billing.totalCost")
1210
+ children: t("billing.monthCost")
1205
1211
  }),
1206
1212
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1207
1213
  className: UsageBilling_module_css_default.heroValue,
1208
- children: formatMoney(displayTotal)
1214
+ children: formatMoney(monthCost)
1209
1215
  }),
1210
1216
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1211
1217
  className: UsageBilling_module_css_default.heroMeta,
1212
1218
  children: [
1213
- total.calls.toLocaleString(),
1219
+ monthCalls.toLocaleString(),
1214
1220
  " ",
1215
- t("billing.calls"),
1216
- total.cost > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("em", { children: "· 实际" })
1221
+ t("billing.calls")
1217
1222
  ]
1218
1223
  })
1219
1224
  ]
1220
1225
  }),
1221
1226
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: UsageBilling_module_css_default.heroDivider }),
1222
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1227
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1223
1228
  className: UsageBilling_module_css_default.heroSide,
1224
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1229
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1230
+ className: UsageBilling_module_css_default.heroSideItem,
1231
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1232
+ className: UsageBilling_module_css_default.heroSideLabel,
1233
+ children: t("billing.yearCost")
1234
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1235
+ className: UsageBilling_module_css_default.heroSideValue,
1236
+ children: formatMoney(yearCost)
1237
+ })]
1238
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1225
1239
  className: UsageBilling_module_css_default.heroSideItem,
1226
1240
  children: [
1227
1241
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1242,7 +1256,7 @@ window.__ModuleLoader__.load({
1242
1256
  ]
1243
1257
  })
1244
1258
  ]
1245
- })
1259
+ })]
1246
1260
  })
1247
1261
  ]
1248
1262
  }),
@@ -1350,7 +1364,10 @@ window.__ModuleLoader__.load({
1350
1364
  className: UsageBilling_module_css_default.panelHint,
1351
1365
  children: latestDate
1352
1366
  })]
1353
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrendChart, { data: trend })]
1367
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrendChart, {
1368
+ data: trend,
1369
+ models: chartModels
1370
+ })]
1354
1371
  }),
1355
1372
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1356
1373
  className: UsageBilling_module_css_default.panel,
@@ -1523,7 +1540,7 @@ window.__ModuleLoader__.load({
1523
1540
  }),
1524
1541
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1525
1542
  className: UsageBilling_module_css_default.numCol,
1526
- children: entry.price.offPeak !== void 0 && entry.price.offPeak.cacheHit !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1543
+ children: entry.price.offPeak !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1527
1544
  className: UsageBilling_module_css_default.bandPrice,
1528
1545
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatUnitPrice(entry.price.cacheHit, entry.price.currency) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1529
1546
  className: UsageBilling_module_css_default.bandPriceOff,
@@ -1598,10 +1615,11 @@ window.__ModuleLoader__.load({
1598
1615
  };
1599
1616
  }, [checkModels]);
1600
1617
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1618
+ const monthCost = Object.entries(stats.byDay).filter(([date]) => date.startsWith(today.slice(0, 7))).reduce((sum, [, day]) => sum + day.cost, 0);
1601
1619
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageBillingTrigger, {
1602
1620
  ...props,
1603
1621
  onOpen: openDashboard,
1604
- totalCost: stats.total.cost > 0 ? stats.total.cost : 0,
1622
+ monthCost,
1605
1623
  todayCost: stats.byDay[today]?.cost ?? 0
1606
1624
  }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BillingDashboard, {
1607
1625
  stats,
@@ -1618,6 +1636,8 @@ window.__ModuleLoader__.load({
1618
1636
  "billing.subtitle": "计费仪表盘",
1619
1637
  "billing.cost": "费用",
1620
1638
  "billing.todayCost": "今日费用",
1639
+ "billing.monthCost": "本月费用",
1640
+ "billing.yearCost": "本年费用",
1621
1641
  "billing.totalCost": "总费用",
1622
1642
  "billing.calls": "调用",
1623
1643
  "billing.cacheHitRate": "缓存命中率",
@@ -1652,6 +1672,8 @@ window.__ModuleLoader__.load({
1652
1672
  "billing.subtitle": "Billing dashboard",
1653
1673
  "billing.cost": "Cost",
1654
1674
  "billing.todayCost": "Today",
1675
+ "billing.monthCost": "This month",
1676
+ "billing.yearCost": "This year",
1655
1677
  "billing.totalCost": "Total",
1656
1678
  "billing.calls": "Calls",
1657
1679
  "billing.cacheHitRate": "Cache Hit",
package/lib/index.js CHANGED
@@ -500,7 +500,11 @@ const MODEL_CATALOG = [
500
500
  ];
501
501
  /** Lookup a model by its stats key; falls back to the generic `other` entry. */
502
502
  function modelOf(key) {
503
- return MODEL_CATALOG.find((entry) => entry.key === key) ?? MODEL_CATALOG[MODEL_CATALOG.length - 1];
503
+ const found = MODEL_CATALOG.find((entry) => entry.key === key);
504
+ if (found !== void 0) return found;
505
+ const fallback = MODEL_CATALOG.at(-1);
506
+ if (fallback !== void 0) return fallback;
507
+ throw new Error("MODEL_CATALOG must not be empty");
504
508
  }
505
509
  /**
506
510
  * Price one band's token usage in CNY. The stats `input` field is the TOTAL
@@ -607,6 +611,23 @@ function dayStamp(time) {
607
611
  const pad = (n) => String(n).padStart(2, "0");
608
612
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
609
613
  }
614
+ /** Get-or-create one model cell inside a usage map (avoids non-null assertions). */
615
+ function usageCell(map, key) {
616
+ const existing = map.get(key);
617
+ if (existing !== void 0) return existing;
618
+ const fresh = emptyUsage();
619
+ map.set(key, fresh);
620
+ return fresh;
621
+ }
622
+ /** Get-or-create one day's model cell inside the two-dimensional map. */
623
+ function modelDayCell(map, day, modelKey) {
624
+ let models = map.get(day);
625
+ if (models === void 0) {
626
+ models = /* @__PURE__ */ new Map();
627
+ map.set(day, models);
628
+ }
629
+ return usageCell(models, modelKey);
630
+ }
610
631
  /**
611
632
  * Aggregate real usage from every persisted session log.
612
633
  * @param persistence - the session persistence service.
@@ -618,6 +639,7 @@ async function aggregateUsage(persistence, options = {}) {
618
639
  const total = emptyUsage();
619
640
  const byModel = /* @__PURE__ */ new Map();
620
641
  const byDay = /* @__PURE__ */ new Map();
642
+ const byModelDay = /* @__PURE__ */ new Map();
621
643
  for (const meta of await persistence.list()) {
622
644
  const { events } = await persistence.readFrom(meta.id, 0);
623
645
  let key = "other";
@@ -633,18 +655,21 @@ async function aggregateUsage(persistence, options = {}) {
633
655
  const modelKey = key;
634
656
  const day = dayStamp(event.time);
635
657
  foldUsage(total, event.data.usage, modelKey, subscription);
636
- foldUsage(byModel.get(modelKey) ?? byModel.set(modelKey, emptyUsage()).get(modelKey), event.data.usage, modelKey, subscription);
637
- foldUsage(byDay.get(day) ?? byDay.set(day, emptyUsage()).get(day), event.data.usage, modelKey, subscription);
658
+ foldUsage(usageCell(byModel, modelKey), event.data.usage, modelKey, subscription);
659
+ foldUsage(usageCell(byDay, day), event.data.usage, modelKey, subscription);
660
+ foldUsage(modelDayCell(byModelDay, day, modelKey), event.data.usage, modelKey, subscription);
638
661
  }
639
662
  }
640
663
  const toRecord = (map) => Object.fromEntries(map);
664
+ const toModelDayRecord = (map) => Object.fromEntries([...map].map(([day, models]) => [day, Object.fromEntries(models)]));
641
665
  return {
642
- version: 1,
666
+ version: 2,
643
667
  updatedAt: Date.now(),
644
668
  source: "session-logs",
645
669
  total,
646
670
  byModel: toRecord(byModel),
647
- byDay: toRecord(byDay)
671
+ byDay: toRecord(byDay),
672
+ byDayModels: toModelDayRecord(byModelDay)
648
673
  };
649
674
  }
650
675
  //#endregion
@@ -56,11 +56,22 @@ export declare function dayStamp(time: number): string;
56
56
  * `SessionPersistence` to list sessions and read each log once.
57
57
  */
58
58
  export type UsagePersistence = Pick<SessionPersistence, 'list' | 'readFrom'>;
59
+ /** The usage-stats document served to the billing dashboard. */
60
+ export interface UsageStatsDocument {
61
+ version: number;
62
+ updatedAt: number;
63
+ source: 'session-logs';
64
+ total: ModelUsage;
65
+ byModel: Record<string, ModelUsage>;
66
+ byDay: Record<string, ModelUsage>;
67
+ /** 模型 × 日期 二维统计:趋势图按模型堆叠的输入([date][modelKey])。 */
68
+ byDayModels: Record<string, Record<string, ModelUsage>>;
69
+ }
59
70
  /**
60
71
  * Aggregate real usage from every persisted session log.
61
72
  * @param persistence - the session persistence service.
62
73
  * @param options - aggregation tuning (e.g. subscription-plan providers).
63
74
  * @returns the usage-stats document (same shape the dashboard expects).
64
75
  */
65
- export declare function aggregateUsage(persistence: UsagePersistence, options?: AggregateOptions): Promise<unknown>;
76
+ export declare function aggregateUsage(persistence: UsagePersistence, options?: AggregateOptions): Promise<UsageStatsDocument>;
66
77
  //# sourceMappingURL=aggregate.d.ts.map
@@ -1,22 +1,37 @@
1
1
  /**
2
- * TrendChart: dependency-free SVG combo chart of daily cost (area line) and
3
- * daily call volume (bars) with a hover crosshair. No chart library the
4
- * surface stays self-contained and offline.
2
+ * TrendChart: dependency-free SVG stacked bar chart of daily cost per model.
3
+ * Each day's column stacks every model's share in its brand color, so the
4
+ * total trend and the per-model composition are visible at once. A hover
5
+ * crosshair shows the day's model breakdown. No chart library — the surface
6
+ * stays self-contained and offline.
5
7
  */
8
+ /** One model's legend identity: key, display name, and brand color. */
9
+ export interface TrendSeriesModel {
10
+ /** Stats key (`byModel` key), also the `byModel` map key. */
11
+ key: string;
12
+ /** Human-readable model name. */
13
+ name: string;
14
+ /** Resolved brand color for the stack segment and legend swatch. */
15
+ color: string;
16
+ }
6
17
  /** One day row fed to the chart. */
7
18
  export interface TrendPoint {
8
19
  /** ISO date `YYYY-MM-DD`. */
9
20
  date: string;
10
- /** USD cost that day. */
21
+ /** Total cost that day. */
11
22
  cost: number;
12
23
  /** API calls that day. */
13
24
  calls: number;
25
+ /** Per-model cost that day (stats key → CNY); absent entries stack zero. */
26
+ byModel?: Readonly<Record<string, number>>;
14
27
  }
15
28
  /**
16
- * Render the daily trend chart.
29
+ * Render the daily per-model stacked cost chart.
17
30
  * @param props.data - sorted daily rows (ascending date).
31
+ * @param props.models - the model legend, in stack order (bottom first).
18
32
  */
19
- export declare function TrendChart({ data }: {
33
+ export declare function TrendChart({ data, models }: {
20
34
  data: readonly TrendPoint[];
35
+ models?: readonly TrendSeriesModel[];
21
36
  }): React.ReactNode;
22
37
  //# sourceMappingURL=TrendChart.d.ts.map
@@ -1,5 +1,5 @@
1
1
  /** Locale dictionaries for the usage billing surface. */
2
- export type UsageBillingKey = 'billing.title' | 'billing.subtitle' | 'billing.cost' | 'billing.todayCost' | 'billing.totalCost' | 'billing.calls' | 'billing.cacheHitRate' | 'billing.tokens' | 'billing.inputTokens' | 'billing.outputTokens' | 'billing.avgCost' | 'billing.trend' | 'billing.trendEmpty' | 'billing.models' | 'billing.estimated' | 'billing.actual' | 'billing.pricing' | 'billing.showPricing' | 'billing.hidePricing' | 'billing.pricePerM' | 'billing.input' | 'billing.output' | 'billing.cacheHit' | 'billing.peak' | 'billing.offPeak' | 'billing.flat' | 'billing.peakHours' | 'billing.band' | 'billing.openDashboard' | 'billing.close' | 'billing.lastUpdated' | 'billing.noData';
2
+ export type UsageBillingKey = 'billing.title' | 'billing.subtitle' | 'billing.cost' | 'billing.todayCost' | 'billing.monthCost' | 'billing.yearCost' | 'billing.totalCost' | 'billing.calls' | 'billing.cacheHitRate' | 'billing.tokens' | 'billing.inputTokens' | 'billing.outputTokens' | 'billing.avgCost' | 'billing.trend' | 'billing.trendEmpty' | 'billing.models' | 'billing.estimated' | 'billing.actual' | 'billing.pricing' | 'billing.showPricing' | 'billing.hidePricing' | 'billing.pricePerM' | 'billing.input' | 'billing.output' | 'billing.cacheHit' | 'billing.peak' | 'billing.offPeak' | 'billing.flat' | 'billing.peakHours' | 'billing.band' | 'billing.openDashboard' | 'billing.close' | 'billing.lastUpdated' | 'billing.noData';
3
3
  export declare const NS = "usageBilling";
4
4
  export declare const zh: Record<UsageBillingKey, string>;
5
5
  export declare const en: Record<UsageBillingKey, string>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kenz1117/dsh-ui-usage-billing",
3
3
  "description": "Usage billing dashboard for DeepSeek Harness: sidebar cost metrics plus a full dashboard modal, priced from a current multi-provider catalog with real usage aggregated from session logs.",
4
- "version": "0.1.0",
4
+ "version": "0.2.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -64,16 +64,16 @@
64
64
  "clsx": "^2.1.1"
65
65
  },
66
66
  "devDependencies": {
67
- "@deepseek-ai/dsh-client-locale": "workspace:^",
68
- "@deepseek-ai/dsh-client-runtime": "workspace:^",
69
- "@deepseek-ai/dsh-client-test-runtime": "workspace:^",
70
- "@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
71
- "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
72
- "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
73
- "@deepseek-ai/dsh-invariants": "workspace:^",
67
+ "@deepseek-ai/dsh-client-locale": "*",
68
+ "@deepseek-ai/dsh-client-runtime": "*",
69
+ "@deepseek-ai/dsh-client-test-runtime": "*",
70
+ "@deepseek-ai/dsh-client-ui-conversation": "*",
71
+ "@deepseek-ai/dsh-client-ui-primitives": "*",
72
+ "@deepseek-ai/dsh-client-ui-slots": "*",
73
+ "@deepseek-ai/dsh-invariants": "*",
74
74
  "@testing-library/react": "^16.1.0",
75
75
  "@types/react": "~18.3.1",
76
- "@deepseek-ai/cordis": "workspace:^",
76
+ "@deepseek-ai/cordis": "*",
77
77
  "react": "^18.2.0",
78
78
  "react-dom": "^18.2.0"
79
79
  },