@kenz1117/dsh-ui-usage-billing 0.2.0 → 0.2.2
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/cordis.patch.yml +8 -0
- package/lib/client.js +370 -249
- package/lib/index.js +174 -27
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/client/pricing.d.ts +17 -0
- package/lib/types/pricing-fetch.d.ts +17 -0
- package/lib/types/pricing-shared.d.ts +25 -0
- package/package.json +6 -2
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# The ui-usage-billing bundle patch: inserts the billing dashboard plugin into
|
|
2
|
+
# the profile root as one entry. Applied by `dsh plugin add @kenz1117/dsh-ui-usage-billing`;
|
|
3
|
+
# the row id addresses this plugin's host half, whose `/api/billing/usage-stats`
|
|
4
|
+
# route serves the browser half rendered in the sidebar footer.
|
|
5
|
+
|
|
6
|
+
- insert:
|
|
7
|
+
- id: ui-usage-billing
|
|
8
|
+
name: '@kenz1117/dsh-ui-usage-billing'
|
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;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}}";
|
|
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-l1);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:12px;padding:10px 14px;transition:border-color .14s,background-color .14s;display:flex}.VWh0dG_trigger:hover,.VWh0dG_trigger:focus-visible{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-interactive-bg-hover);outline:none}.VWh0dG_triggerIcon{width:16px;height:16px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;display:inline-flex}.VWh0dG_triggerIcon svg{stroke-width:1.8px;width:15px;height:15px}.VWh0dG_triggerToday,.VWh0dG_triggerMonth{align-items:baseline;gap:4px;min-width:0;display:flex}.VWh0dG_triggerDivider{background:var(--dsw-alias-border-l1);align-self:stretch;width:1px}.VWh0dG_triggerAmount{color:var(--dsw-alias-label-primary);letter-spacing:-.01em;font-variant-numeric:tabular-nums;white-space:nowrap;font-size:17px;font-weight:600;line-height:22px}.VWh0dG_triggerAmountSub{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;white-space:nowrap;font-size:13px;font-weight:600;line-height:18px}.VWh0dG_triggerMeta{color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-size:10.5px;line-height:14px}.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{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:16px;justify-content:space-between;align-items:center;gap:24px;padding:20px 24px;display:flex}.VWh0dG_heroMain{flex-direction:column;gap:4px;min-width:0;display:flex}.VWh0dG_heroLabel{color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-size:12px;line-height:17px}.VWh0dG_heroValue{letter-spacing:-.02em;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;white-space:nowrap;font-size:40px;font-weight:600;line-height:48px}.VWh0dG_heroMeta{color:var(--dsw-alias-label-caption);white-space:nowrap;font-size:12px;line-height:17px}.VWh0dG_heroSide{flex-direction:column;flex:none;gap:10px;display:flex}.VWh0dG_heroSideItem{align-items:baseline;gap:8px;display:flex}.VWh0dG_heroSideLabel{color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-size:12px;line-height:17px}.VWh0dG_heroSideValue{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;white-space:nowrap;font-size:16px;font-weight:600;line-height:22px}.VWh0dG_delta{align-items:center;gap:2px;margin-left:4px;font-size:11px;font-weight:600;line-height:17px;display:inline-flex}.VWh0dG_deltaUp{color:var(--dsw-static-green-500)}.VWh0dG_deltaDown{color:var(--dsw-static-red-500)}.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-width:2px;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_rateBadge{vertical-align:1px;white-space:nowrap;border-radius:999px;align-items:center;margin-left:6px;padding:1px 6px;font-size:10px;font-weight:500;line-height:14px;display:inline-flex}.VWh0dG_rateBadgeLive{color:var(--dsw-static-green-500);background:color-mix(in srgb, var(--dsw-static-green-500) 10%, transparent)}.VWh0dG_rateBadgeBuiltin{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-module-platform)}.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:14px}.VWh0dG_heroSide{flex-direction:row;gap:20px}}";
|
|
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,112 +33,120 @@ window.__ModuleLoader__.load({
|
|
|
33
33
|
document.head.appendChild(tag);
|
|
34
34
|
}
|
|
35
35
|
var UsageBilling_module_css_default = {
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"panelTitle": "VWh0dG_panelTitle",
|
|
36
|
+
"costCol": "VWh0dG_costCol",
|
|
37
|
+
"rateBadge": "VWh0dG_rateBadge",
|
|
39
38
|
"pricingToggleText": "VWh0dG_pricingToggleText",
|
|
40
|
-
"heroValue": "VWh0dG_heroValue",
|
|
41
|
-
"trigger": "VWh0dG_trigger",
|
|
42
|
-
"pricingChevronOpen": "VWh0dG_pricingChevronOpen",
|
|
43
|
-
"kpiLabel": "VWh0dG_kpiLabel",
|
|
44
|
-
"heroDivider": "VWh0dG_heroDivider",
|
|
45
|
-
"dashboardSubtitle": "VWh0dG_dashboardSubtitle",
|
|
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",
|
|
54
|
-
"chartWrap": "VWh0dG_chartWrap",
|
|
55
|
-
"heroMeta": "VWh0dG_heroMeta",
|
|
56
|
-
"heroSide": "VWh0dG_heroSide",
|
|
57
|
-
"triggerText": "VWh0dG_triggerText",
|
|
58
|
-
"tableScroll": "VWh0dG_tableScroll",
|
|
59
|
-
"flatTag": "VWh0dG_flatTag",
|
|
60
|
-
"chartLegendBar": "VWh0dG_chartLegendBar",
|
|
61
|
-
"dashboardHead": "VWh0dG_dashboardHead",
|
|
62
|
-
"chartAxisLabel": "VWh0dG_chartAxisLabel",
|
|
63
|
-
"pricingChevron": "VWh0dG_pricingChevron",
|
|
64
|
-
"modelProvider": "VWh0dG_modelProvider",
|
|
65
|
-
"chartLine": "VWh0dG_chartLine",
|
|
66
|
-
"chartDot": "VWh0dG_chartDot",
|
|
67
|
-
"bandPrice": "VWh0dG_bandPrice",
|
|
68
39
|
"bandTagOff": "VWh0dG_bandTagOff",
|
|
69
|
-
"
|
|
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",
|
|
77
|
-
"pricingToggle": "VWh0dG_pricingToggle",
|
|
40
|
+
"healthDot": "VWh0dG_healthDot",
|
|
78
41
|
"kpiTile": "VWh0dG_kpiTile",
|
|
42
|
+
"deltaUp": "VWh0dG_deltaUp",
|
|
43
|
+
"delta": "VWh0dG_delta",
|
|
79
44
|
"panelHead": "VWh0dG_panelHead",
|
|
45
|
+
"panelTitle": "VWh0dG_panelTitle",
|
|
80
46
|
"chartBar": "VWh0dG_chartBar",
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"bandPriceOff": "VWh0dG_bandPriceOff",
|
|
84
|
-
"triggerLabel": "VWh0dG_triggerLabel",
|
|
85
|
-
"numCol": "VWh0dG_numCol",
|
|
86
|
-
"panelHint": "VWh0dG_panelHint",
|
|
87
|
-
"healthBadgeOk": "VWh0dG_healthBadgeOk",
|
|
88
|
-
"healthBadgeBad": "VWh0dG_healthBadgeBad",
|
|
89
|
-
"chartStack": "VWh0dG_chartStack",
|
|
47
|
+
"hero": "VWh0dG_hero",
|
|
48
|
+
"emptyRow": "VWh0dG_emptyRow",
|
|
90
49
|
"dashboardModal": "VWh0dG_dashboardModal",
|
|
91
|
-
"
|
|
92
|
-
"
|
|
50
|
+
"chartLegendBar": "VWh0dG_chartLegendBar",
|
|
51
|
+
"tableScroll": "VWh0dG_tableScroll",
|
|
93
52
|
"heroMain": "VWh0dG_heroMain",
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
"
|
|
53
|
+
"heroSide": "VWh0dG_heroSide",
|
|
54
|
+
"pricingTable": "VWh0dG_pricingTable",
|
|
55
|
+
"kpiDetail": "VWh0dG_kpiDetail",
|
|
56
|
+
"healthBadgeOk": "VWh0dG_healthBadgeOk",
|
|
57
|
+
"triggerDivider": "VWh0dG_triggerDivider",
|
|
58
|
+
"heroLabel": "VWh0dG_heroLabel",
|
|
59
|
+
"pricingToggle": "VWh0dG_pricingToggle",
|
|
60
|
+
"dashboardRight": "VWh0dG_dashboardRight",
|
|
61
|
+
"railButton": "VWh0dG_railButton",
|
|
62
|
+
"kpiLabel": "VWh0dG_kpiLabel",
|
|
102
63
|
"healthBadge": "VWh0dG_healthBadge",
|
|
103
|
-
"
|
|
64
|
+
"triggerMonth": "VWh0dG_triggerMonth",
|
|
65
|
+
"triggerToday": "VWh0dG_triggerToday",
|
|
66
|
+
"rateBadgeBuiltin": "VWh0dG_rateBadgeBuiltin",
|
|
67
|
+
"dashboardSubtitle": "VWh0dG_dashboardSubtitle",
|
|
68
|
+
"flatTag": "VWh0dG_flatTag",
|
|
104
69
|
"dashboard": "VWh0dG_dashboard",
|
|
105
|
-
"
|
|
106
|
-
"deltaUp": "VWh0dG_deltaUp",
|
|
107
|
-
"heroSideItem": "VWh0dG_heroSideItem",
|
|
108
|
-
"kpiGrid": "VWh0dG_kpiGrid",
|
|
109
|
-
"dashboardBody": "VWh0dG_dashboardBody",
|
|
110
|
-
"kpiDetail": "VWh0dG_kpiDetail",
|
|
70
|
+
"chartWrap": "VWh0dG_chartWrap",
|
|
111
71
|
"panel": "VWh0dG_panel",
|
|
112
|
-
"
|
|
113
|
-
"
|
|
72
|
+
"chartLegendLine": "VWh0dG_chartLegendLine",
|
|
73
|
+
"deltaDown": "VWh0dG_deltaDown",
|
|
74
|
+
"kpiGrid": "VWh0dG_kpiGrid",
|
|
75
|
+
"chartSvg": "VWh0dG_chartSvg",
|
|
76
|
+
"closeButton": "VWh0dG_closeButton",
|
|
77
|
+
"chartLegend": "VWh0dG_chartLegend",
|
|
78
|
+
"modelName": "VWh0dG_modelName",
|
|
79
|
+
"pricingChevron": "VWh0dG_pricingChevron",
|
|
80
|
+
"na": "VWh0dG_na",
|
|
81
|
+
"healthBadgeBad": "VWh0dG_healthBadgeBad",
|
|
82
|
+
"chartStack": "VWh0dG_chartStack",
|
|
114
83
|
"chartTooltipRow": "VWh0dG_chartTooltipRow",
|
|
115
|
-
"
|
|
84
|
+
"modelTable": "VWh0dG_modelTable",
|
|
85
|
+
"chartCrosshair": "VWh0dG_chartCrosshair",
|
|
86
|
+
"bandTag": "VWh0dG_bandTag",
|
|
87
|
+
"heroValue": "VWh0dG_heroValue",
|
|
88
|
+
"heroMeta": "VWh0dG_heroMeta",
|
|
89
|
+
"healthOk": "VWh0dG_healthOk",
|
|
90
|
+
"kpiValue": "VWh0dG_kpiValue",
|
|
91
|
+
"planTag": "VWh0dG_planTag",
|
|
116
92
|
"heroSideLabel": "VWh0dG_heroSideLabel",
|
|
93
|
+
"chartDot": "VWh0dG_chartDot",
|
|
94
|
+
"healthBad": "VWh0dG_healthBad",
|
|
117
95
|
"kpiGreen": "VWh0dG_kpiGreen",
|
|
118
|
-
"
|
|
96
|
+
"heroSideItem": "VWh0dG_heroSideItem",
|
|
97
|
+
"pricingChevronOpen": "VWh0dG_pricingChevronOpen",
|
|
98
|
+
"dashboardBody": "VWh0dG_dashboardBody",
|
|
99
|
+
"panelHint": "VWh0dG_panelHint",
|
|
100
|
+
"chartTooltipDate": "VWh0dG_chartTooltipDate",
|
|
101
|
+
"heroSideValue": "VWh0dG_heroSideValue",
|
|
102
|
+
"triggerAmount": "VWh0dG_triggerAmount",
|
|
103
|
+
"trigger": "VWh0dG_trigger",
|
|
104
|
+
"chartAxisLabel": "VWh0dG_chartAxisLabel",
|
|
105
|
+
"modelProvider": "VWh0dG_modelProvider",
|
|
106
|
+
"chartGrid": "VWh0dG_chartGrid",
|
|
107
|
+
"healthIdle": "VWh0dG_healthIdle",
|
|
108
|
+
"chartLine": "VWh0dG_chartLine",
|
|
109
|
+
"numCol": "VWh0dG_numCol",
|
|
110
|
+
"rateBadgeLive": "VWh0dG_rateBadgeLive",
|
|
111
|
+
"triggerAmountSub": "VWh0dG_triggerAmountSub",
|
|
112
|
+
"bandPrice": "VWh0dG_bandPrice",
|
|
113
|
+
"dashboardTitle": "VWh0dG_dashboardTitle",
|
|
114
|
+
"triggerIcon": "VWh0dG_triggerIcon",
|
|
115
|
+
"dashboardHead": "VWh0dG_dashboardHead",
|
|
116
|
+
"bandPriceOff": "VWh0dG_bandPriceOff",
|
|
117
|
+
"chartTooltip": "VWh0dG_chartTooltip",
|
|
118
|
+
"chartEmpty": "VWh0dG_chartEmpty",
|
|
119
|
+
"modelCell": "VWh0dG_modelCell",
|
|
120
|
+
"modelDot": "VWh0dG_modelDot",
|
|
121
|
+
"chartTooltipSwatch": "VWh0dG_chartTooltipSwatch",
|
|
122
|
+
"triggerMeta": "VWh0dG_triggerMeta"
|
|
119
123
|
};
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
/** 运行时实时覆盖:undefined = 用内置目录与内置汇率(默认值降级)。 */
|
|
125
|
+
let liveRate;
|
|
126
|
+
let livePrices;
|
|
122
127
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* (DeepSeek, 智谱, 通义…) publish RMB prices and store them directly;
|
|
127
|
-
* overseas providers (OpenAI, Google, xAI, Meta) publish USD.
|
|
128
|
-
* Cost is always computed and displayed in CNY — only USD-priced models go
|
|
129
|
-
* through the exchange rate, never domestic ones.
|
|
130
|
-
*
|
|
131
|
-
* Google-style two-band billing is modeled per model: Gemini's Flex tier
|
|
132
|
-
* prices spare-capacity traffic at -50%; DeepSeek V4 splits peak
|
|
133
|
-
* (09:00-12:00 / 14:00-18:00 Beijing) at 2x the off-peak rate. The estimator
|
|
134
|
-
* mixes both bands by a configured peak share ({@link DEFAULT_PEAK_SHARE}).
|
|
128
|
+
* Apply the node half's live pricing snapshot. Absent fields keep the
|
|
129
|
+
* built-in catalog and rate; callers never fabricate values.
|
|
130
|
+
* @param pricing - the `/api/billing/pricing` response.
|
|
135
131
|
*/
|
|
132
|
+
function applyLivePricing(pricing) {
|
|
133
|
+
liveRate = pricing.rate;
|
|
134
|
+
livePrices = pricing.prices;
|
|
135
|
+
}
|
|
136
|
+
/** 当前汇率:实时覆盖优先,缺省回退内置固定值。 */
|
|
137
|
+
function currentRate() {
|
|
138
|
+
return liveRate ?? 6.79;
|
|
139
|
+
}
|
|
136
140
|
/**
|
|
137
|
-
* USD → CNY
|
|
138
|
-
*
|
|
139
|
-
* USD-priced models — domestic models never pass through this rate.
|
|
141
|
+
* 当前生效的 USD → CNY 汇率及其来源:live = 启动时实时拉取成功,
|
|
142
|
+
* builtin = 实时拉取失败、正在用内置默认值。
|
|
140
143
|
*/
|
|
141
|
-
|
|
144
|
+
function getRateInfo() {
|
|
145
|
+
return {
|
|
146
|
+
rate: currentRate(),
|
|
147
|
+
live: liveRate !== void 0
|
|
148
|
+
};
|
|
149
|
+
}
|
|
142
150
|
/** Default share of traffic assumed to fall in the peak band (0..1). */
|
|
143
151
|
const DEFAULT_PEAK_SHARE = .5;
|
|
144
152
|
/**
|
|
@@ -617,11 +625,22 @@ window.__ModuleLoader__.load({
|
|
|
617
625
|
];
|
|
618
626
|
/** Lookup a model by its stats key; falls back to the generic `other` entry. */
|
|
619
627
|
function modelOf(key) {
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
628
|
+
const base = MODEL_CATALOG.find((entry) => entry.key === key) ?? (() => {
|
|
629
|
+
const fallback = MODEL_CATALOG.at(-1);
|
|
630
|
+
if (fallback !== void 0) return fallback;
|
|
631
|
+
throw new Error("MODEL_CATALOG must not be empty");
|
|
632
|
+
})();
|
|
633
|
+
const live = livePrices?.[key];
|
|
634
|
+
if (live === void 0) return base;
|
|
635
|
+
return {
|
|
636
|
+
...base,
|
|
637
|
+
price: {
|
|
638
|
+
currency: "USD",
|
|
639
|
+
input: live.input,
|
|
640
|
+
cacheHit: live.cacheHit,
|
|
641
|
+
output: live.output
|
|
642
|
+
}
|
|
643
|
+
};
|
|
625
644
|
}
|
|
626
645
|
/** Resolve a price-table row by its CSS variable name (theme token or fallback color). */
|
|
627
646
|
function resolveToken(name) {
|
|
@@ -640,7 +659,7 @@ window.__ModuleLoader__.load({
|
|
|
640
659
|
const miss = buckets.cacheMiss > 0 ? buckets.cacheMiss : Math.max(0, buckets.input - buckets.cacheHit);
|
|
641
660
|
const hit = Math.min(buckets.cacheHit, buckets.input);
|
|
642
661
|
const raw = (miss * (band.cacheMiss ?? band.input) + hit * band.cacheHit + buckets.output * band.output) / 1e6;
|
|
643
|
-
return currency === "USD" ? raw *
|
|
662
|
+
return currency === "USD" ? raw * currentRate() : raw;
|
|
644
663
|
}
|
|
645
664
|
/**
|
|
646
665
|
* Estimate the CNY cost of one model's token usage, mixing the peak and
|
|
@@ -659,10 +678,12 @@ window.__ModuleLoader__.load({
|
|
|
659
678
|
}
|
|
660
679
|
/** Format a CNY amount with adaptive precision. */
|
|
661
680
|
function formatMoney(cny) {
|
|
662
|
-
|
|
663
|
-
if (
|
|
664
|
-
if (
|
|
665
|
-
return `¥${
|
|
681
|
+
const value = Number(cny);
|
|
682
|
+
if (!Number.isFinite(value)) return "¥0";
|
|
683
|
+
if (value >= 1e3) return `¥${value.toFixed(0)}`;
|
|
684
|
+
if (value >= 10) return `¥${value.toFixed(1)}`;
|
|
685
|
+
if (value >= .1) return `¥${value.toFixed(2)}`;
|
|
686
|
+
return `¥${value.toFixed(3)}`;
|
|
666
687
|
}
|
|
667
688
|
/**
|
|
668
689
|
* Format a per-1M-token price in its native currency (free when the rate is
|
|
@@ -686,23 +707,27 @@ window.__ModuleLoader__.load({
|
|
|
686
707
|
}
|
|
687
708
|
/** Format a percentage. */
|
|
688
709
|
function formatPercent(value) {
|
|
689
|
-
|
|
710
|
+
const normalized = Number(value);
|
|
711
|
+
if (!Number.isFinite(normalized)) return "0.0%";
|
|
712
|
+
return `${normalized.toFixed(1)}%`;
|
|
690
713
|
}
|
|
691
714
|
//#endregion
|
|
692
715
|
//#region src/client/TrendChart.tsx
|
|
693
716
|
/**
|
|
694
|
-
* TrendChart: dependency-free SVG
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
717
|
+
* TrendChart: dependency-free SVG chart of daily cost + calls.
|
|
718
|
+
*
|
|
719
|
+
* The columns are GROUPED per model — one bar per model per day, each in its
|
|
720
|
+
* brand color, so per-model cost is directly comparable. The blue line is the
|
|
721
|
+
* total call volume across all models, plotted on its own right-hand axis.
|
|
722
|
+
* A hover crosshair shows the day's model breakdown. No chart library — the
|
|
723
|
+
* surface stays self-contained and offline.
|
|
699
724
|
*/
|
|
700
725
|
/** Fixed viewBox; the SVG scales to its container. */
|
|
701
726
|
const W = 680;
|
|
702
727
|
const H = 220;
|
|
703
728
|
const PAD = {
|
|
704
729
|
top: 18,
|
|
705
|
-
right:
|
|
730
|
+
right: 40,
|
|
706
731
|
bottom: 26,
|
|
707
732
|
left: 46
|
|
708
733
|
};
|
|
@@ -711,6 +736,12 @@ window.__ModuleLoader__.load({
|
|
|
711
736
|
const [, month, day] = iso.split("-");
|
|
712
737
|
return `${Number(month)}/${Number(day)}`;
|
|
713
738
|
}
|
|
739
|
+
/** Compact tick label for the calls axis: `1.2K` / `3.4M`. */
|
|
740
|
+
function shortNumber(value) {
|
|
741
|
+
if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
|
|
742
|
+
if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
|
|
743
|
+
return String(Math.round(value));
|
|
744
|
+
}
|
|
714
745
|
/** Ticks every `step` items for sparse axis labels. */
|
|
715
746
|
function tickIndexes(length, step) {
|
|
716
747
|
const out = [];
|
|
@@ -718,52 +749,56 @@ window.__ModuleLoader__.load({
|
|
|
718
749
|
if (length > 0 && out[out.length - 1] !== length - 1) out.push(length - 1);
|
|
719
750
|
return out;
|
|
720
751
|
}
|
|
752
|
+
/** Single-color fallback identity used when the stats carry no per-model detail. */
|
|
753
|
+
const TOTAL_MODEL = {
|
|
754
|
+
key: "__total__",
|
|
755
|
+
name: "总计",
|
|
756
|
+
color: ""
|
|
757
|
+
};
|
|
721
758
|
/**
|
|
722
|
-
* Render the daily
|
|
759
|
+
* Render the daily grouped cost bars plus the total-calls line.
|
|
723
760
|
* @param props.data - sorted daily rows (ascending date).
|
|
724
|
-
* @param props.models - the model legend, in
|
|
761
|
+
* @param props.models - the model legend, in bar order.
|
|
725
762
|
*/
|
|
726
763
|
function TrendChart({ data, models = [] }) {
|
|
727
764
|
const [hover, setHover] = (0, react.useState)(null);
|
|
728
765
|
const layout = (0, react.useMemo)(() => {
|
|
729
766
|
const n = data.length;
|
|
730
767
|
if (n === 0) return null;
|
|
731
|
-
const maxCost = Math.max(...data.map((d) => d.cost), 1e-4);
|
|
732
768
|
const plotW = W - PAD.left - PAD.right;
|
|
733
769
|
const plotH = H - PAD.top - PAD.bottom;
|
|
734
770
|
const inner = (i) => {
|
|
735
771
|
if (n === 1) return PAD.left + plotW / 2;
|
|
736
772
|
return PAD.left + plotW * i / (n - 1);
|
|
737
773
|
};
|
|
774
|
+
const maxCost = Math.max(...data.flatMap((d) => Object.values(d.byModel ?? {})), ...data.map((d) => d.cost), 1e-4);
|
|
738
775
|
const yCost = (value) => PAD.top + plotH - value / maxCost * plotH;
|
|
776
|
+
const maxCalls = Math.max(...data.map((d) => d.calls), 1);
|
|
777
|
+
const yCalls = (value) => PAD.top + plotH - value / maxCalls * plotH;
|
|
778
|
+
const groupW = plotW / n;
|
|
779
|
+
const barCount = Math.max(models.length, 1);
|
|
780
|
+
const barW = Math.min(14, groupW / barCount * .7);
|
|
739
781
|
return {
|
|
740
782
|
n,
|
|
741
783
|
plotW,
|
|
742
784
|
plotH,
|
|
743
785
|
inner,
|
|
744
786
|
yCost,
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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 {
|
|
787
|
+
yCalls,
|
|
788
|
+
barW,
|
|
789
|
+
bars: data.flatMap((d, i) => {
|
|
790
|
+
if (models.length === 0) return [{
|
|
762
791
|
date: d.date,
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
};
|
|
792
|
+
model: TOTAL_MODEL,
|
|
793
|
+
x: inner(i) - barW / 2,
|
|
794
|
+
value: d.cost
|
|
795
|
+
}];
|
|
796
|
+
return models.map((model, k) => ({
|
|
797
|
+
date: d.date,
|
|
798
|
+
model,
|
|
799
|
+
x: inner(i) - groupW / 2 + groupW * (k + .5) / models.length - barW / 2,
|
|
800
|
+
value: d.byModel?.[model.key] ?? 0
|
|
801
|
+
}));
|
|
767
802
|
}),
|
|
768
803
|
costTicks: [
|
|
769
804
|
0,
|
|
@@ -772,15 +807,24 @@ window.__ModuleLoader__.load({
|
|
|
772
807
|
.75,
|
|
773
808
|
1
|
|
774
809
|
].map((f) => maxCost * f).reverse(),
|
|
775
|
-
|
|
810
|
+
callsTicks: [
|
|
811
|
+
0,
|
|
812
|
+
.25,
|
|
813
|
+
.5,
|
|
814
|
+
.75,
|
|
815
|
+
1
|
|
816
|
+
].map((f) => maxCalls * f).reverse(),
|
|
817
|
+
linePath: data.map((d, i) => {
|
|
818
|
+
const y = yCalls(d.calls);
|
|
819
|
+
return `${i === 0 ? "M" : "L"}${inner(i)} ${y}`;
|
|
820
|
+
}).join(" ")
|
|
776
821
|
};
|
|
777
822
|
}, [data, models]);
|
|
778
823
|
if (layout === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
779
824
|
className: UsageBilling_module_css_default.chartEmpty,
|
|
780
825
|
children: "暂无趋势数据"
|
|
781
826
|
});
|
|
782
|
-
const { n, plotW, inner, yCost, barW,
|
|
783
|
-
const activeColumn = hover === null ? void 0 : columns[hover];
|
|
827
|
+
const { n, plotW, plotH, inner, yCost, yCalls, barW, bars, costTicks, callsTicks, linePath } = layout;
|
|
784
828
|
const activePoint = hover === null ? void 0 : data[hover];
|
|
785
829
|
const indices = tickIndexes(n, Math.max(1, Math.ceil(n / 8)));
|
|
786
830
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -790,7 +834,7 @@ window.__ModuleLoader__.load({
|
|
|
790
834
|
viewBox: `0 0 ${W} ${H}`,
|
|
791
835
|
className: UsageBilling_module_css_default.chartSvg,
|
|
792
836
|
role: "img",
|
|
793
|
-
"aria-label": "Daily cost by model",
|
|
837
|
+
"aria-label": "Daily cost by model and total calls",
|
|
794
838
|
onMouseLeave: () => {
|
|
795
839
|
setHover(null);
|
|
796
840
|
},
|
|
@@ -817,15 +861,30 @@ window.__ModuleLoader__.load({
|
|
|
817
861
|
children: formatMoney(value)
|
|
818
862
|
})] }, `cost-${idx}`);
|
|
819
863
|
}),
|
|
820
|
-
|
|
821
|
-
x:
|
|
822
|
-
y: yCost(
|
|
864
|
+
bars.map((bar) => bar.value > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
865
|
+
x: bar.x,
|
|
866
|
+
y: yCost(bar.value),
|
|
823
867
|
width: barW,
|
|
824
|
-
height:
|
|
825
|
-
rx:
|
|
826
|
-
className: UsageBilling_module_css_default.chartStack,
|
|
827
|
-
style: { fill:
|
|
828
|
-
},
|
|
868
|
+
height: yCost(0) - yCost(bar.value),
|
|
869
|
+
rx: 2,
|
|
870
|
+
className: bar.model.color === "" ? UsageBilling_module_css_default.chartBar : UsageBilling_module_css_default.chartStack,
|
|
871
|
+
style: bar.model.color === "" ? void 0 : { fill: bar.model.color }
|
|
872
|
+
}, `${bar.date}-${bar.model.key}`) : null),
|
|
873
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
874
|
+
d: linePath,
|
|
875
|
+
fill: "none",
|
|
876
|
+
className: UsageBilling_module_css_default.chartLine
|
|
877
|
+
}),
|
|
878
|
+
callsTicks.map((value, idx) => {
|
|
879
|
+
const y = yCalls(value);
|
|
880
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
881
|
+
x: W - PAD.right + 8,
|
|
882
|
+
y: y + 3,
|
|
883
|
+
textAnchor: "start",
|
|
884
|
+
className: UsageBilling_module_css_default.chartAxisLabel,
|
|
885
|
+
children: shortNumber(value)
|
|
886
|
+
}, `calls-${idx}`);
|
|
887
|
+
}),
|
|
829
888
|
indices.map((i) => {
|
|
830
889
|
const point = data[i];
|
|
831
890
|
if (point === void 0) return null;
|
|
@@ -837,44 +896,44 @@ window.__ModuleLoader__.load({
|
|
|
837
896
|
children: shortDate(point.date)
|
|
838
897
|
}, point.date);
|
|
839
898
|
}),
|
|
840
|
-
|
|
899
|
+
hover !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
|
|
841
900
|
x1: inner(hover),
|
|
842
901
|
x2: inner(hover),
|
|
843
902
|
y1: PAD.top,
|
|
844
|
-
y2: PAD.top +
|
|
903
|
+
y2: PAD.top + plotH,
|
|
845
904
|
className: UsageBilling_module_css_default.chartCrosshair
|
|
846
905
|
})
|
|
847
906
|
]
|
|
848
907
|
}),
|
|
849
|
-
|
|
908
|
+
activePoint !== void 0 && hover !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
850
909
|
className: UsageBilling_module_css_default.chartTooltip,
|
|
851
910
|
style: {
|
|
852
911
|
left: `${inner(hover) / W * 100}%`,
|
|
853
|
-
top: `${yCost(
|
|
912
|
+
top: `${yCost(activePoint.cost) / H * 100}%`
|
|
854
913
|
},
|
|
855
914
|
children: [
|
|
856
915
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
857
916
|
className: UsageBilling_module_css_default.chartTooltipDate,
|
|
858
917
|
children: activePoint.date
|
|
859
918
|
}),
|
|
860
|
-
|
|
919
|
+
models.filter((model) => (activePoint.byModel?.[model.key] ?? 0) > 0).map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
861
920
|
className: UsageBilling_module_css_default.chartTooltipRow,
|
|
862
921
|
children: [
|
|
863
922
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
864
923
|
className: UsageBilling_module_css_default.chartTooltipSwatch,
|
|
865
|
-
style: { background:
|
|
924
|
+
style: { background: model.color }
|
|
866
925
|
}),
|
|
867
|
-
|
|
926
|
+
model.name,
|
|
868
927
|
" ",
|
|
869
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: formatMoney(
|
|
928
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: formatMoney(activePoint.byModel?.[model.key] ?? 0) })
|
|
870
929
|
]
|
|
871
|
-
},
|
|
930
|
+
}, model.key)),
|
|
872
931
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
873
932
|
className: UsageBilling_module_css_default.chartTooltipRow,
|
|
874
933
|
children: [
|
|
875
934
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: UsageBilling_module_css_default.chartLegendBar }),
|
|
876
935
|
"总计 ",
|
|
877
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: formatMoney(
|
|
936
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: formatMoney(activePoint.cost) })
|
|
878
937
|
]
|
|
879
938
|
}),
|
|
880
939
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -887,12 +946,12 @@ window.__ModuleLoader__.load({
|
|
|
887
946
|
})
|
|
888
947
|
]
|
|
889
948
|
}),
|
|
890
|
-
models.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.
|
|
949
|
+
models.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
891
950
|
className: UsageBilling_module_css_default.chartLegend,
|
|
892
|
-
children: models.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
951
|
+
children: [models.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
893
952
|
className: UsageBilling_module_css_default.chartTooltipSwatch,
|
|
894
953
|
style: { background: model.color }
|
|
895
|
-
}), model.name] }, model.key))
|
|
954
|
+
}), model.name] }, model.key)), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: UsageBilling_module_css_default.chartLegendLine }), "调用"] })]
|
|
896
955
|
})
|
|
897
956
|
]
|
|
898
957
|
});
|
|
@@ -1001,6 +1060,8 @@ window.__ModuleLoader__.load({
|
|
|
1001
1060
|
}
|
|
1002
1061
|
/** Path to the usage-stats endpoint served by this plugin's node half. */
|
|
1003
1062
|
const USAGE_STATS_PATH = "/api/billing/usage-stats";
|
|
1063
|
+
/** Path to the live-pricing endpoint served by this plugin's node half. */
|
|
1064
|
+
const PRICING_PATH = "/api/billing/pricing";
|
|
1004
1065
|
/** Empty snapshot: shown before (or without) real host data — zeros, never fabricated samples. */
|
|
1005
1066
|
const EMPTY_STATS = {
|
|
1006
1067
|
total: {
|
|
@@ -1022,67 +1083,110 @@ window.__ModuleLoader__.load({
|
|
|
1022
1083
|
if (!response.ok) return null;
|
|
1023
1084
|
const text = await response.text();
|
|
1024
1085
|
const parsed = JSON.parse(text);
|
|
1025
|
-
if (parsed
|
|
1026
|
-
|
|
1086
|
+
if (parsed === null || typeof parsed !== "object" || !("total" in parsed)) return null;
|
|
1087
|
+
const candidate = parsed;
|
|
1088
|
+
return {
|
|
1089
|
+
total: candidate.total ?? EMPTY_STATS.total,
|
|
1090
|
+
byModel: candidate.byModel ?? {},
|
|
1091
|
+
byDay: candidate.byDay ?? {},
|
|
1092
|
+
...candidate.byDayModels !== void 0 ? { byDayModels: candidate.byDayModels } : {}
|
|
1093
|
+
};
|
|
1027
1094
|
} catch {
|
|
1028
1095
|
return null;
|
|
1029
1096
|
}
|
|
1030
1097
|
}
|
|
1031
1098
|
/**
|
|
1099
|
+
* Apply the node half's live pricing snapshot. The node half refreshes once
|
|
1100
|
+
* at boot, so an early `builtin` answer may just mean the refresh is still
|
|
1101
|
+
* in flight — retry briefly before settling for the built-in values.
|
|
1102
|
+
* Any final failure keeps the built-in catalog and rate — degrade, never
|
|
1103
|
+
* fabricate.
|
|
1104
|
+
* @param attempt - current retry index (0-based).
|
|
1105
|
+
*/
|
|
1106
|
+
async function loadLivePricing(attempt = 0) {
|
|
1107
|
+
const MAX_ATTEMPTS = 4;
|
|
1108
|
+
try {
|
|
1109
|
+
const response = await fetch(PRICING_PATH);
|
|
1110
|
+
if (!response.ok) return;
|
|
1111
|
+
const text = await response.text();
|
|
1112
|
+
const parsed = JSON.parse(text);
|
|
1113
|
+
if (parsed === null || typeof parsed !== "object" || !("source" in parsed)) return;
|
|
1114
|
+
const pricing = parsed;
|
|
1115
|
+
if (pricing.source === "builtin" && attempt < MAX_ATTEMPTS - 1) {
|
|
1116
|
+
setTimeout(() => {
|
|
1117
|
+
loadLivePricing(attempt + 1);
|
|
1118
|
+
}, 2e3);
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
applyLivePricing(pricing);
|
|
1122
|
+
} catch {}
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1032
1125
|
* Sidebar footer trigger: compact pill in wide mode, icon in rail mode.
|
|
1033
1126
|
* @param props - framework props plus `wide` column state.
|
|
1034
1127
|
*/
|
|
1035
1128
|
function UsageBillingTrigger(props) {
|
|
1036
|
-
const { wide, onOpen, monthCost, todayCost } = props;
|
|
1129
|
+
const { wide, t, onOpen, monthCost, todayCost } = props;
|
|
1130
|
+
const cardIcon = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1131
|
+
viewBox: "0 0 24 24",
|
|
1132
|
+
fill: "none",
|
|
1133
|
+
stroke: "currentColor",
|
|
1134
|
+
"aria-hidden": "true",
|
|
1135
|
+
children: [
|
|
1136
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
1137
|
+
x: "2.5",
|
|
1138
|
+
y: "5",
|
|
1139
|
+
width: "19",
|
|
1140
|
+
height: "14",
|
|
1141
|
+
rx: "2.5"
|
|
1142
|
+
}),
|
|
1143
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2.5 9.5h19" }),
|
|
1144
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
1145
|
+
x: "6",
|
|
1146
|
+
y: "12",
|
|
1147
|
+
width: "4",
|
|
1148
|
+
height: "3.5",
|
|
1149
|
+
rx: "0.75"
|
|
1150
|
+
})
|
|
1151
|
+
]
|
|
1152
|
+
});
|
|
1037
1153
|
if (!wide) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1038
1154
|
type: "button",
|
|
1039
1155
|
className: UsageBilling_module_css_default.railButton,
|
|
1040
1156
|
onClick: onOpen,
|
|
1041
|
-
title: formatMoney(monthCost)
|
|
1042
|
-
children:
|
|
1043
|
-
viewBox: "0 0 24 24",
|
|
1044
|
-
fill: "none",
|
|
1045
|
-
stroke: "currentColor",
|
|
1046
|
-
"aria-hidden": "true",
|
|
1047
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" })
|
|
1048
|
-
})
|
|
1157
|
+
title: `${t("billing.title")} · ${formatMoney(monthCost)}`,
|
|
1158
|
+
children: cardIcon
|
|
1049
1159
|
});
|
|
1050
1160
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1051
1161
|
type: "button",
|
|
1052
1162
|
className: UsageBilling_module_css_default.trigger,
|
|
1053
1163
|
onClick: onOpen,
|
|
1164
|
+
title: `${t("billing.title")} · 本月 ${formatMoney(monthCost)}`,
|
|
1054
1165
|
children: [
|
|
1055
1166
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1056
1167
|
className: UsageBilling_module_css_default.triggerIcon,
|
|
1057
|
-
children:
|
|
1058
|
-
viewBox: "0 0 24 24",
|
|
1059
|
-
fill: "none",
|
|
1060
|
-
stroke: "currentColor",
|
|
1061
|
-
"aria-hidden": "true",
|
|
1062
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" })
|
|
1063
|
-
})
|
|
1168
|
+
children: cardIcon
|
|
1064
1169
|
}),
|
|
1065
1170
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1066
|
-
className: UsageBilling_module_css_default.
|
|
1171
|
+
className: UsageBilling_module_css_default.triggerToday,
|
|
1067
1172
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1068
|
-
className: UsageBilling_module_css_default.triggerLabel,
|
|
1069
|
-
children: "计费仪表盘"
|
|
1070
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1071
1173
|
className: UsageBilling_module_css_default.triggerMeta,
|
|
1072
|
-
children:
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
]
|
|
1174
|
+
children: "今日"
|
|
1175
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1176
|
+
className: UsageBilling_module_css_default.triggerAmount,
|
|
1177
|
+
children: formatMoney(todayCost)
|
|
1077
1178
|
})]
|
|
1078
1179
|
}),
|
|
1079
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1180
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: UsageBilling_module_css_default.triggerDivider }),
|
|
1181
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1182
|
+
className: UsageBilling_module_css_default.triggerMonth,
|
|
1183
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1184
|
+
className: UsageBilling_module_css_default.triggerMeta,
|
|
1185
|
+
children: "当月"
|
|
1186
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1187
|
+
className: UsageBilling_module_css_default.triggerAmountSub,
|
|
1188
|
+
children: formatMoney(monthCost)
|
|
1189
|
+
})]
|
|
1086
1190
|
})
|
|
1087
1191
|
]
|
|
1088
1192
|
});
|
|
@@ -1094,9 +1198,9 @@ window.__ModuleLoader__.load({
|
|
|
1094
1198
|
function BillingDashboard({ stats, t, onClose, health }) {
|
|
1095
1199
|
const { total, byModel, byDay } = stats;
|
|
1096
1200
|
const [pricingOpen, setPricingOpen] = (0, react.useState)(false);
|
|
1201
|
+
const rateInfo = getRateInfo();
|
|
1097
1202
|
const cacheHitRate = total.cacheHit + total.cacheMiss > 0 ? total.cacheHit / (total.cacheHit + total.cacheMiss) * 100 : 0;
|
|
1098
1203
|
const dates = Object.keys(byDay).sort();
|
|
1099
|
-
const latestDate = dates.at(-1) ?? "";
|
|
1100
1204
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1101
1205
|
const todayCost = byDay[today]?.cost ?? 0;
|
|
1102
1206
|
const monthPrefix = today.slice(0, 7);
|
|
@@ -1104,7 +1208,17 @@ window.__ModuleLoader__.load({
|
|
|
1104
1208
|
const monthCost = dates.reduce((sum, d) => sum + (d.startsWith(monthPrefix) ? byDay[d]?.cost ?? 0 : 0), 0);
|
|
1105
1209
|
const monthCalls = dates.reduce((sum, d) => sum + (d.startsWith(monthPrefix) ? byDay[d]?.calls ?? 0 : 0), 0);
|
|
1106
1210
|
const yearCost = dates.reduce((sum, d) => sum + (d.startsWith(yearPrefix) ? byDay[d]?.cost ?? 0 : 0), 0);
|
|
1107
|
-
const
|
|
1211
|
+
const trendDates = (0, react.useMemo)(() => {
|
|
1212
|
+
const out = [];
|
|
1213
|
+
for (let offset = 6; offset >= 0; offset -= 1) {
|
|
1214
|
+
const day = /* @__PURE__ */ new Date();
|
|
1215
|
+
day.setDate(day.getDate() - offset);
|
|
1216
|
+
out.push(day.toISOString().slice(0, 10));
|
|
1217
|
+
}
|
|
1218
|
+
return out;
|
|
1219
|
+
}, []);
|
|
1220
|
+
const latestDate = trendDates.at(-1) ?? today;
|
|
1221
|
+
const trend = (0, react.useMemo)(() => trendDates.map((date) => {
|
|
1108
1222
|
const byModel = {};
|
|
1109
1223
|
const dayModels = stats.byDayModels?.[date];
|
|
1110
1224
|
if (dayModels !== void 0) {
|
|
@@ -1118,7 +1232,7 @@ window.__ModuleLoader__.load({
|
|
|
1118
1232
|
byModel
|
|
1119
1233
|
};
|
|
1120
1234
|
}), [
|
|
1121
|
-
|
|
1235
|
+
trendDates,
|
|
1122
1236
|
byDay,
|
|
1123
1237
|
stats.byDayModels
|
|
1124
1238
|
]);
|
|
@@ -1201,64 +1315,56 @@ window.__ModuleLoader__.load({
|
|
|
1201
1315
|
children: [
|
|
1202
1316
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1203
1317
|
className: UsageBilling_module_css_default.hero,
|
|
1204
|
-
children: [
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1318
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1319
|
+
className: UsageBilling_module_css_default.heroMain,
|
|
1320
|
+
children: [
|
|
1321
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1322
|
+
className: UsageBilling_module_css_default.heroLabel,
|
|
1323
|
+
children: t("billing.monthCost")
|
|
1324
|
+
}),
|
|
1325
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1326
|
+
className: UsageBilling_module_css_default.heroValue,
|
|
1327
|
+
children: formatMoney(monthCost)
|
|
1328
|
+
}),
|
|
1329
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1330
|
+
className: UsageBilling_module_css_default.heroMeta,
|
|
1331
|
+
children: [
|
|
1332
|
+
monthCalls.toLocaleString(),
|
|
1333
|
+
" ",
|
|
1334
|
+
t("billing.calls")
|
|
1335
|
+
]
|
|
1336
|
+
})
|
|
1337
|
+
]
|
|
1338
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1339
|
+
className: UsageBilling_module_css_default.heroSide,
|
|
1340
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1341
|
+
className: UsageBilling_module_css_default.heroSideItem,
|
|
1342
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1343
|
+
className: UsageBilling_module_css_default.heroSideLabel,
|
|
1344
|
+
children: t("billing.yearCost")
|
|
1345
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1346
|
+
className: UsageBilling_module_css_default.heroSideValue,
|
|
1347
|
+
children: formatMoney(yearCost)
|
|
1348
|
+
})]
|
|
1349
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1350
|
+
className: UsageBilling_module_css_default.heroSideItem,
|
|
1351
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1352
|
+
className: UsageBilling_module_css_default.heroSideLabel,
|
|
1353
|
+
children: t("billing.todayCost")
|
|
1354
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1355
|
+
className: UsageBilling_module_css_default.heroSideValue,
|
|
1356
|
+
children: [formatMoney(todayCost), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1357
|
+
className: clsx(UsageBilling_module_css_default.delta, deltaPct >= 0 ? UsageBilling_module_css_default.deltaUp : UsageBilling_module_css_default.deltaDown),
|
|
1218
1358
|
children: [
|
|
1219
|
-
|
|
1359
|
+
deltaPct >= 0 ? "▲" : "▼",
|
|
1220
1360
|
" ",
|
|
1221
|
-
|
|
1361
|
+
Math.abs(deltaPct).toFixed(1),
|
|
1362
|
+
"%"
|
|
1222
1363
|
]
|
|
1223
|
-
})
|
|
1224
|
-
]
|
|
1225
|
-
}),
|
|
1226
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: UsageBilling_module_css_default.heroDivider }),
|
|
1227
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1228
|
-
className: UsageBilling_module_css_default.heroSide,
|
|
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
1364
|
})]
|
|
1238
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1239
|
-
className: UsageBilling_module_css_default.heroSideItem,
|
|
1240
|
-
children: [
|
|
1241
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1242
|
-
className: UsageBilling_module_css_default.heroSideLabel,
|
|
1243
|
-
children: t("billing.todayCost")
|
|
1244
|
-
}),
|
|
1245
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1246
|
-
className: UsageBilling_module_css_default.heroSideValue,
|
|
1247
|
-
children: formatMoney(todayCost)
|
|
1248
|
-
}),
|
|
1249
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1250
|
-
className: clsx(UsageBilling_module_css_default.delta, deltaPct >= 0 ? UsageBilling_module_css_default.deltaUp : UsageBilling_module_css_default.deltaDown),
|
|
1251
|
-
children: [
|
|
1252
|
-
deltaPct >= 0 ? "▲" : "▼",
|
|
1253
|
-
" ",
|
|
1254
|
-
Math.abs(deltaPct).toFixed(1),
|
|
1255
|
-
"%"
|
|
1256
|
-
]
|
|
1257
|
-
})
|
|
1258
|
-
]
|
|
1259
1365
|
})]
|
|
1260
|
-
})
|
|
1261
|
-
]
|
|
1366
|
+
})]
|
|
1367
|
+
})]
|
|
1262
1368
|
}),
|
|
1263
1369
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1264
1370
|
className: UsageBilling_module_css_default.kpiGrid,
|
|
@@ -1483,9 +1589,17 @@ window.__ModuleLoader__.load({
|
|
|
1483
1589
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1484
1590
|
className: UsageBilling_module_css_default.panelTitle,
|
|
1485
1591
|
children: t("billing.pricing")
|
|
1486
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1592
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1487
1593
|
className: UsageBilling_module_css_default.panelHint,
|
|
1488
|
-
children:
|
|
1594
|
+
children: [
|
|
1595
|
+
t("billing.todayRate"),
|
|
1596
|
+
" 1 USD = ",
|
|
1597
|
+
formatMoney(rateInfo.rate),
|
|
1598
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1599
|
+
className: clsx(UsageBilling_module_css_default.rateBadge, rateInfo.live ? UsageBilling_module_css_default.rateBadgeLive : UsageBilling_module_css_default.rateBadgeBuiltin),
|
|
1600
|
+
children: rateInfo.live ? t("billing.rateLive") : t("billing.rateBuiltin")
|
|
1601
|
+
})
|
|
1602
|
+
]
|
|
1489
1603
|
})]
|
|
1490
1604
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1491
1605
|
className: clsx(UsageBilling_module_css_default.pricingChevron, pricingOpen && UsageBilling_module_css_default.pricingChevronOpen),
|
|
@@ -1601,6 +1715,7 @@ window.__ModuleLoader__.load({
|
|
|
1601
1715
|
loadUsageStats().then((data) => {
|
|
1602
1716
|
if (mounted && data !== null) setStats(data);
|
|
1603
1717
|
});
|
|
1718
|
+
loadLivePricing();
|
|
1604
1719
|
return () => {
|
|
1605
1720
|
mounted = false;
|
|
1606
1721
|
};
|
|
@@ -1665,7 +1780,10 @@ window.__ModuleLoader__.load({
|
|
|
1665
1780
|
"billing.openDashboard": "打开计费仪表盘",
|
|
1666
1781
|
"billing.close": "关闭",
|
|
1667
1782
|
"billing.lastUpdated": "数据更新于",
|
|
1668
|
-
"billing.noData": "暂无计费数据"
|
|
1783
|
+
"billing.noData": "暂无计费数据",
|
|
1784
|
+
"billing.todayRate": "今日汇率",
|
|
1785
|
+
"billing.rateLive": "实时",
|
|
1786
|
+
"billing.rateBuiltin": "内置"
|
|
1669
1787
|
};
|
|
1670
1788
|
const en = {
|
|
1671
1789
|
"billing.title": "Usage",
|
|
@@ -1701,7 +1819,10 @@ window.__ModuleLoader__.load({
|
|
|
1701
1819
|
"billing.openDashboard": "Open billing dashboard",
|
|
1702
1820
|
"billing.close": "Close",
|
|
1703
1821
|
"billing.lastUpdated": "Updated",
|
|
1704
|
-
"billing.noData": "No billing data yet"
|
|
1822
|
+
"billing.noData": "No billing data yet",
|
|
1823
|
+
"billing.todayRate": "Today rate",
|
|
1824
|
+
"billing.rateLive": "Live",
|
|
1825
|
+
"billing.rateBuiltin": "Built-in"
|
|
1705
1826
|
};
|
|
1706
1827
|
//#endregion
|
|
1707
1828
|
//#region src/client/apply.ts
|
package/lib/index.js
CHANGED
|
@@ -1,27 +1,10 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* Each model's price table uses its NATIVE currency: domestic providers
|
|
9
|
-
* (DeepSeek, 智谱, 通义…) publish RMB prices and store them directly;
|
|
10
|
-
* overseas providers (OpenAI, Google, xAI, Meta) publish USD.
|
|
11
|
-
* Cost is always computed and displayed in CNY — only USD-priced models go
|
|
12
|
-
* through the exchange rate, never domestic ones.
|
|
13
|
-
*
|
|
14
|
-
* Google-style two-band billing is modeled per model: Gemini's Flex tier
|
|
15
|
-
* prices spare-capacity traffic at -50%; DeepSeek V4 splits peak
|
|
16
|
-
* (09:00-12:00 / 14:00-18:00 Beijing) at 2x the off-peak rate. The estimator
|
|
17
|
-
* mixes both bands by a configured peak share ({@link DEFAULT_PEAK_SHARE}).
|
|
18
|
-
*/
|
|
19
|
-
/**
|
|
20
|
-
* USD → CNY rate for display. Source: China Foreign Exchange Trade System
|
|
21
|
-
* mid-rate 6.7878 on 2026-08-14; rounded to 6.79. Only applies to overseas
|
|
22
|
-
* USD-priced models — domestic models never pass through this rate.
|
|
23
|
-
*/
|
|
24
|
-
const USD_TO_CNY = 6.79;
|
|
4
|
+
/** 当前汇率:实时覆盖优先,缺省回退内置固定值。 */
|
|
5
|
+
function currentRate() {
|
|
6
|
+
return 6.79;
|
|
7
|
+
}
|
|
25
8
|
/** Default share of traffic assumed to fall in the peak band (0..1). */
|
|
26
9
|
const DEFAULT_PEAK_SHARE = .5;
|
|
27
10
|
/**
|
|
@@ -500,11 +483,11 @@ const MODEL_CATALOG = [
|
|
|
500
483
|
];
|
|
501
484
|
/** Lookup a model by its stats key; falls back to the generic `other` entry. */
|
|
502
485
|
function modelOf(key) {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
486
|
+
return MODEL_CATALOG.find((entry) => entry.key === key) ?? (() => {
|
|
487
|
+
const fallback = MODEL_CATALOG.at(-1);
|
|
488
|
+
if (fallback !== void 0) return fallback;
|
|
489
|
+
throw new Error("MODEL_CATALOG must not be empty");
|
|
490
|
+
})();
|
|
508
491
|
}
|
|
509
492
|
/**
|
|
510
493
|
* Price one band's token usage in CNY. The stats `input` field is the TOTAL
|
|
@@ -518,7 +501,7 @@ function priceBandCost(band, buckets, currency) {
|
|
|
518
501
|
const miss = buckets.cacheMiss > 0 ? buckets.cacheMiss : Math.max(0, buckets.input - buckets.cacheHit);
|
|
519
502
|
const hit = Math.min(buckets.cacheHit, buckets.input);
|
|
520
503
|
const raw = (miss * (band.cacheMiss ?? band.input) + hit * band.cacheHit + buckets.output * band.output) / 1e6;
|
|
521
|
-
return currency === "USD" ? raw *
|
|
504
|
+
return currency === "USD" ? raw * currentRate() : raw;
|
|
522
505
|
}
|
|
523
506
|
/**
|
|
524
507
|
* Estimate the CNY cost of one model's token usage, mixing the peak and
|
|
@@ -673,6 +656,158 @@ async function aggregateUsage(persistence, options = {}) {
|
|
|
673
656
|
};
|
|
674
657
|
}
|
|
675
658
|
//#endregion
|
|
659
|
+
//#region lib/types/pricing-fetch.js
|
|
660
|
+
/**
|
|
661
|
+
* One-shot live pricing refresh for the billing dashboard.
|
|
662
|
+
*
|
|
663
|
+
* Fetches the USD → CNY mid rate and the OpenRouter model price list, maps
|
|
664
|
+
* matched models onto the built-in catalog keys, and returns the combined
|
|
665
|
+
* LivePricing. Every fetch failure degrades to the built-in values: the node
|
|
666
|
+
* half caches whatever succeeded and the browser dashboard falls back to the
|
|
667
|
+
* catalog for the rest — a total outage answers `{ source: 'builtin' }`.
|
|
668
|
+
*/
|
|
669
|
+
/** Abort a fetch when the upstream hangs beyond this budget. */
|
|
670
|
+
const FETCH_TIMEOUT_MS = 8e3;
|
|
671
|
+
/**
|
|
672
|
+
* USD → CNY 汇率源,按顺序尝试:国内可达的腾讯财经行情(免 key、`~` 分隔
|
|
673
|
+
* 第 4 个字段为价格)优先,国外 open.er-api.com 兜底。任一源失败自动落到
|
|
674
|
+
* 下一个;全部失败由调用方降级内置汇率。
|
|
675
|
+
*/
|
|
676
|
+
const RATE_SOURCES = [{
|
|
677
|
+
url: "https://qt.gtimg.cn/q=whUSDCNY",
|
|
678
|
+
parse: (text) => {
|
|
679
|
+
const price = /"([^"]*)"/.exec(text)?.[1]?.split("~")[3];
|
|
680
|
+
return price !== void 0 && price !== "" ? Number(price) : void 0;
|
|
681
|
+
}
|
|
682
|
+
}, {
|
|
683
|
+
url: "https://open.er-api.com/v6/latest/USD",
|
|
684
|
+
parse: (text) => {
|
|
685
|
+
try {
|
|
686
|
+
const cny = JSON.parse(text).rates?.CNY;
|
|
687
|
+
return typeof cny === "number" && Number.isFinite(cny) && cny > 0 ? cny : void 0;
|
|
688
|
+
} catch {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}];
|
|
693
|
+
/** OpenRouter's public model list: per-token USD prices, no key needed. */
|
|
694
|
+
const ROUTER_URL = "https://openrouter.ai/api/v1/models";
|
|
695
|
+
/**
|
|
696
|
+
* Built-in catalog key → OpenRouter model-id candidates. Matching prefers an
|
|
697
|
+
* exact id, then a single strong substring hit (the router id contains the
|
|
698
|
+
* hint); an ambiguous hit is skipped so the built-in price stays
|
|
699
|
+
* authoritative. Hints are provider+generation words the router ids carry —
|
|
700
|
+
* correct or extend them as the market moves.
|
|
701
|
+
*/
|
|
702
|
+
const ROUTER_ID_HINTS = {
|
|
703
|
+
"flash": ["deepseek-v4-flash", "deepseek-v4"],
|
|
704
|
+
"pro": ["deepseek-v4-pro"],
|
|
705
|
+
"glm": ["glm-5"],
|
|
706
|
+
"qwen-3.8-max": ["qwen-3.8-max", "qwen3.8-max"],
|
|
707
|
+
"qwen-max": ["qwen-max"],
|
|
708
|
+
"qwen-plus": ["qwen-plus"],
|
|
709
|
+
"gemini-pro": ["gemini-3-pro", "gemini-pro"],
|
|
710
|
+
"gemini-flash": ["gemini-3-flash", "gemini-flash"],
|
|
711
|
+
"gpt-5.6-sol": ["gpt-5.6-sol"],
|
|
712
|
+
"gpt-5.6-terra": ["gpt-5.6-terra"],
|
|
713
|
+
"gpt-5.6-luna": ["gpt-5.6-luna"],
|
|
714
|
+
"grok": ["grok-4"],
|
|
715
|
+
"llama": ["llama-4"],
|
|
716
|
+
"kimi": ["kimi-k2"]
|
|
717
|
+
};
|
|
718
|
+
/** GET a URL's text body with a hard timeout; null on any failure. */
|
|
719
|
+
async function fetchText(url) {
|
|
720
|
+
const controller = new AbortController();
|
|
721
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
722
|
+
try {
|
|
723
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
724
|
+
if (!response.ok) return null;
|
|
725
|
+
return await response.text();
|
|
726
|
+
} catch {
|
|
727
|
+
return null;
|
|
728
|
+
} finally {
|
|
729
|
+
clearTimeout(timer);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
/** GET a JSON endpoint with a hard timeout; null on any failure. */
|
|
733
|
+
async function fetchJson(url) {
|
|
734
|
+
const text = await fetchText(url);
|
|
735
|
+
if (text === null) return null;
|
|
736
|
+
try {
|
|
737
|
+
return JSON.parse(text);
|
|
738
|
+
} catch {
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
/** Latest USD → CNY rate from the first working source, or undefined when none respond. */
|
|
743
|
+
async function fetchRate() {
|
|
744
|
+
for (const source of RATE_SOURCES) {
|
|
745
|
+
const text = await fetchText(source.url);
|
|
746
|
+
if (text === null) continue;
|
|
747
|
+
const value = source.parse(text);
|
|
748
|
+
if (value !== void 0 && Number.isFinite(value) && value > 0) return value;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
/** OpenRouter model rows with usable USD unit prices, or undefined on failure. */
|
|
752
|
+
async function fetchRouterModels() {
|
|
753
|
+
const data = await fetchJson(ROUTER_URL);
|
|
754
|
+
if (data === null || typeof data !== "object") return void 0;
|
|
755
|
+
const list = data.data;
|
|
756
|
+
if (!Array.isArray(list)) return void 0;
|
|
757
|
+
const models = [];
|
|
758
|
+
for (const item of list) {
|
|
759
|
+
if (item === null || typeof item !== "object") continue;
|
|
760
|
+
const { id, pricing } = item;
|
|
761
|
+
if (typeof id !== "string" || pricing === null || typeof pricing !== "object") continue;
|
|
762
|
+
const { prompt, completion } = pricing;
|
|
763
|
+
if (typeof prompt !== "number" || typeof completion !== "number") continue;
|
|
764
|
+
if (!Number.isFinite(prompt) || !Number.isFinite(completion)) continue;
|
|
765
|
+
models.push({
|
|
766
|
+
id,
|
|
767
|
+
input: prompt * 1e6,
|
|
768
|
+
output: completion * 1e6
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
return models;
|
|
772
|
+
}
|
|
773
|
+
/** Match one catalog key's candidates: exact id first, then a single strong substring hit. */
|
|
774
|
+
function matchRouterModel(hints, models) {
|
|
775
|
+
const exact = models.find((model) => hints.some((hint) => model.id === hint));
|
|
776
|
+
if (exact !== void 0) return exact;
|
|
777
|
+
const strong = models.filter((model) => hints.some((hint) => hint.length >= 8 && model.id.includes(hint)));
|
|
778
|
+
if (strong.length !== 1) return void 0;
|
|
779
|
+
return strong[0];
|
|
780
|
+
}
|
|
781
|
+
/** Map router matches onto catalog keys; undefined when nothing matched. */
|
|
782
|
+
function buildPrices(models) {
|
|
783
|
+
const result = {};
|
|
784
|
+
for (const [key, hints] of Object.entries(ROUTER_ID_HINTS)) {
|
|
785
|
+
const hit = matchRouterModel(hints, models);
|
|
786
|
+
if (hit === void 0) continue;
|
|
787
|
+
result[key] = {
|
|
788
|
+
input: hit.input,
|
|
789
|
+
cacheHit: hit.input * .1,
|
|
790
|
+
output: hit.output
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Fetch the live pricing once at boot. Both upstreams run in parallel; a
|
|
797
|
+
* failure in either degrades independently to the built-in value.
|
|
798
|
+
* @returns the live pricing snapshot (builtin when everything failed).
|
|
799
|
+
*/
|
|
800
|
+
async function fetchLivePricing() {
|
|
801
|
+
const [rate, models] = await Promise.all([fetchRate(), fetchRouterModels()]);
|
|
802
|
+
const prices = models === void 0 ? void 0 : buildPrices(models);
|
|
803
|
+
if (rate === void 0 && prices === void 0) return { source: "builtin" };
|
|
804
|
+
return {
|
|
805
|
+
source: "live",
|
|
806
|
+
...rate !== void 0 ? { rate } : {},
|
|
807
|
+
...prices !== void 0 ? { prices } : {}
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
//#endregion
|
|
676
811
|
//#region lib/types/index.js
|
|
677
812
|
/**
|
|
678
813
|
* Usage billing surface plugin, node half.
|
|
@@ -700,6 +835,18 @@ function apply(ctx, config = {}) {
|
|
|
700
835
|
join(cwd, ".dsh-usage-stats.json"),
|
|
701
836
|
join(homedir(), ".dsh/.dsh-usage-stats.json")
|
|
702
837
|
].filter((path) => typeof path === "string" && path.length > 0);
|
|
838
|
+
let live = { source: "builtin" };
|
|
839
|
+
fetchLivePricing().then((result) => {
|
|
840
|
+
live = result;
|
|
841
|
+
});
|
|
842
|
+
ctx.effect(() => ctx.webServer.register({
|
|
843
|
+
kind: "exact",
|
|
844
|
+
path: "/api/billing/pricing",
|
|
845
|
+
handler: async (_req, res) => {
|
|
846
|
+
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
847
|
+
res.end(JSON.stringify(live));
|
|
848
|
+
}
|
|
849
|
+
}), "usage-billing: pricing route");
|
|
703
850
|
ctx.effect(() => ctx.webServer.register({
|
|
704
851
|
kind: "exact",
|
|
705
852
|
path: "/api/billing/usage-stats",
|
|
@@ -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.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';
|
|
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' | 'billing.todayRate' | 'billing.rateLive' | 'billing.rateBuiltin';
|
|
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>;
|
|
@@ -12,12 +12,29 @@
|
|
|
12
12
|
* (09:00-12:00 / 14:00-18:00 Beijing) at 2x the off-peak rate. The estimator
|
|
13
13
|
* mixes both bands by a configured peak share ({@link DEFAULT_PEAK_SHARE}).
|
|
14
14
|
*/
|
|
15
|
+
import type { LivePricing } from '../pricing-shared.ts';
|
|
15
16
|
/**
|
|
16
17
|
* USD → CNY rate for display. Source: China Foreign Exchange Trade System
|
|
17
18
|
* mid-rate 6.7878 on 2026-08-14; rounded to 6.79. Only applies to overseas
|
|
18
19
|
* USD-priced models — domestic models never pass through this rate.
|
|
20
|
+
* The node half may refresh this at boot via `/api/billing/pricing`; until a
|
|
21
|
+
* live rate arrives the built-in value stays in force.
|
|
19
22
|
*/
|
|
20
23
|
export declare const USD_TO_CNY = 6.79;
|
|
24
|
+
/**
|
|
25
|
+
* Apply the node half's live pricing snapshot. Absent fields keep the
|
|
26
|
+
* built-in catalog and rate; callers never fabricate values.
|
|
27
|
+
* @param pricing - the `/api/billing/pricing` response.
|
|
28
|
+
*/
|
|
29
|
+
export declare function applyLivePricing(pricing: LivePricing): void;
|
|
30
|
+
/**
|
|
31
|
+
* 当前生效的 USD → CNY 汇率及其来源:live = 启动时实时拉取成功,
|
|
32
|
+
* builtin = 实时拉取失败、正在用内置默认值。
|
|
33
|
+
*/
|
|
34
|
+
export declare function getRateInfo(): {
|
|
35
|
+
rate: number;
|
|
36
|
+
live: boolean;
|
|
37
|
+
};
|
|
21
38
|
/** Default share of traffic assumed to fall in the peak band (0..1). */
|
|
22
39
|
export declare const DEFAULT_PEAK_SHARE = 0.5;
|
|
23
40
|
/**
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot live pricing refresh for the billing dashboard.
|
|
3
|
+
*
|
|
4
|
+
* Fetches the USD → CNY mid rate and the OpenRouter model price list, maps
|
|
5
|
+
* matched models onto the built-in catalog keys, and returns the combined
|
|
6
|
+
* LivePricing. Every fetch failure degrades to the built-in values: the node
|
|
7
|
+
* half caches whatever succeeded and the browser dashboard falls back to the
|
|
8
|
+
* catalog for the rest — a total outage answers `{ source: 'builtin' }`.
|
|
9
|
+
*/
|
|
10
|
+
import type { LivePricing } from './pricing-shared.ts';
|
|
11
|
+
/**
|
|
12
|
+
* Fetch the live pricing once at boot. Both upstreams run in parallel; a
|
|
13
|
+
* failure in either degrades independently to the built-in value.
|
|
14
|
+
* @returns the live pricing snapshot (builtin when everything failed).
|
|
15
|
+
*/
|
|
16
|
+
export declare function fetchLivePricing(): Promise<LivePricing>;
|
|
17
|
+
//# sourceMappingURL=pricing-fetch.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live-pricing wire types shared by the node half (which fetches and caches
|
|
3
|
+
* once at boot) and the browser half (which applies the overrides on top of
|
|
4
|
+
* the built-in catalog). An absent field means the built-in value still
|
|
5
|
+
* applies — the dashboard degrades to the catalog, never to fabricated data.
|
|
6
|
+
*/
|
|
7
|
+
/** One model's unit prices as reported by the router, in USD per 1M tokens. */
|
|
8
|
+
export interface LivePrice {
|
|
9
|
+
/** Uncached input price per 1M tokens. */
|
|
10
|
+
input: number;
|
|
11
|
+
/** Cache-hit input price per 1M tokens (estimated as 10% of input; the router list carries no cache band). */
|
|
12
|
+
cacheHit: number;
|
|
13
|
+
/** Output price per 1M tokens. */
|
|
14
|
+
output: number;
|
|
15
|
+
}
|
|
16
|
+
/** Response of `/api/billing/pricing` consumed by the dashboard. */
|
|
17
|
+
export interface LivePricing {
|
|
18
|
+
/** live = at least one fetch succeeded; builtin = full fallback to the catalog. */
|
|
19
|
+
source: 'live' | 'builtin';
|
|
20
|
+
/** USD → CNY mid rate (present when the rate fetch succeeded). */
|
|
21
|
+
rate?: number;
|
|
22
|
+
/** Overrides keyed by built-in catalog key (present when router matches succeeded). */
|
|
23
|
+
prices?: Record<string, LivePrice>;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=pricing-shared.d.ts.map
|
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.2.
|
|
4
|
+
"version": "0.2.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
"./package.json": "./package.json"
|
|
30
30
|
},
|
|
31
31
|
"dsh": {
|
|
32
|
+
"bundle": {
|
|
33
|
+
"patch": "./cordis.patch.yml"
|
|
34
|
+
},
|
|
32
35
|
"client": {
|
|
33
36
|
"inject": [
|
|
34
37
|
"@deepseek-ai/dsh-client-runtime",
|
|
@@ -81,6 +84,7 @@
|
|
|
81
84
|
"lib/index.js",
|
|
82
85
|
"lib/invariant.js",
|
|
83
86
|
"lib/client.js",
|
|
84
|
-
"lib/types/**/*.d.ts"
|
|
87
|
+
"lib/types/**/*.d.ts",
|
|
88
|
+
"cordis.patch.yml"
|
|
85
89
|
]
|
|
86
90
|
}
|