dsh-key-usage 0.1.2 → 0.1.3

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/client/client.js CHANGED
@@ -46,12 +46,25 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
46
46
  }
47
47
  //#endregion
48
48
  //#region src/client/api.ts
49
+ /**
50
+ * 最近一次请求失败的原因,按路径记住。
51
+ * 空态文案需要能指向「怎么做」——最常见的失败是新端点 404(服务端代码要重启宿主才生效)。
52
+ */
53
+ const failureReasons = /* @__PURE__ */ new Map();
54
+ function failureReasonOf(path) {
55
+ return failureReasons.get(path) ?? null;
56
+ }
49
57
  async function fetchJson(path) {
50
58
  try {
51
59
  const response = await fetch(path, { headers: { Accept: "application/json" } });
52
- if (!response.ok) return null;
60
+ if (!response.ok) {
61
+ failureReasons.set(path, response.status === 404 ? "HTTP 404:宿主没有这个端点 —— 服务端改动需要退出重开 DSH(刷新页面不够)" : "HTTP " + String(response.status));
62
+ return null;
63
+ }
64
+ failureReasons.delete(path);
53
65
  return await response.json();
54
- } catch {
66
+ } catch (error) {
67
+ failureReasons.set(path, "请求失败:" + (error instanceof Error ? error.message : String(error)));
55
68
  return null;
56
69
  }
57
70
  }
@@ -88,6 +101,9 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
88
101
  function loadProjects(limit = 200) {
89
102
  return fetchJson("/dsh-key-usage/projects?limit=" + String(limit));
90
103
  }
104
+ function loadModels(limit = 200) {
105
+ return fetchJson("/dsh-key-usage/models?limit=" + String(limit));
106
+ }
91
107
  //#endregion
92
108
  //#region src/client/format.ts
93
109
  /** 展示层格式化:全部与统计口径无关,只负责把数字变成界面文本。 */
@@ -148,63 +164,63 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
148
164
  }
149
165
  //#endregion
150
166
  //#region \0dsh-css:D:\EasyJob\.plugin\dsh-key-usage\src\client\styles\shared.module.css.mjs
151
- const css$5 = ".LhdpGW_section{color:var(--dsw-alias-label-primary,#e8e8ec);font-size:13.5px;line-height:1.55}.LhdpGW_title{justify-content:space-between;align-items:center;gap:10px;margin-bottom:12px;font-size:18px;font-weight:650;display:flex}.LhdpGW_titleActions{align-items:center;gap:8px;display:inline-flex}.LhdpGW_refreshBtn{border:1px solid var(--dsw-alias-border-l2,#2c2c34);color:var(--dsw-alias-label-secondary,#9a9aa6);cursor:pointer;background:0 0;border-radius:999px;padding:3px 12px;font-family:inherit;font-size:11.5px}.LhdpGW_refreshBtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#80808c1a);color:var(--dsw-alias-label-primary,#e8e8ec)}.LhdpGW_refreshBtn:disabled{opacity:.6;cursor:default}.LhdpGW_tabBar{border-bottom:1px solid var(--dsw-alias-border-l1,#2c2c34);flex-wrap:wrap;gap:2px;margin-bottom:18px;display:flex}.LhdpGW_tabBar button{color:var(--dsw-alias-label-secondary,#9a9aa6);font:inherit;cursor:pointer;background:0 0;border:0;border-bottom:2px solid #0000;margin-bottom:-1px;padding:8px 14px;font-size:13px}.LhdpGW_tabBar button:hover{color:var(--dsw-alias-label-primary,#e8e8ec)}.LhdpGW_tabBar button.LhdpGW_active{color:var(--dsw-alias-state-accent-primary,#5b8cff);border-bottom-color:var(--dsw-alias-state-accent-primary,#5b8cff);font-weight:600}.LhdpGW_card{background:var(--dsw-alias-bg-layer-2,#1d1d22);border:1px solid var(--dsw-alias-border-l1,#2c2c34);border-radius:12px;padding:14px 16px}.LhdpGW_cardGrid3{grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-bottom:12px;display:grid}.LhdpGW_cardGrid4{grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-bottom:18px;display:grid}.LhdpGW_cardGridAuto{grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:10px;margin-bottom:18px;display:grid}@media (max-width:720px){.LhdpGW_cardGrid4{grid-template-columns:repeat(2,minmax(0,1fr))}}.LhdpGW_statLabel{color:var(--dsw-alias-label-tertiary,#67676f);margin-bottom:6px;font-size:11.5px}.LhdpGW_statValue{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary,#e8e8ec);font-size:20px;font-weight:650}.LhdpGW_statExtra{color:var(--dsw-alias-label-tertiary,#67676f);margin-top:4px;font-size:11.5px}.LhdpGW_groupLabel{color:var(--dsw-alias-label-secondary,#9a9aa6);margin:4px 0 10px;font-size:12.5px;font-weight:600}.LhdpGW_blockLabel{color:var(--dsw-alias-label-secondary,#9a9aa6);margin:18px 0 4px;font-size:11.5px}.LhdpGW_table{border-collapse:collapse;width:100%;margin-top:10px}.LhdpGW_table th{text-align:right;color:var(--dsw-alias-label-tertiary,#67676f);border-bottom:1px solid var(--dsw-alias-border-l1,#2c2c34);white-space:nowrap;padding:5px 8px;font-size:10.5px;font-weight:500}.LhdpGW_table td{text-align:right;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary,#9a9aa6);vertical-align:middle;border-bottom:1px solid var(--dsw-alias-border-l1,#2c2c34);white-space:normal;overflow-wrap:anywhere;padding:7px 8px;font-size:12px}.LhdpGW_table th:first-child,.LhdpGW_table td:first-child{text-align:left;padding-left:0}.LhdpGW_table td:first-child{color:var(--dsw-alias-label-primary,#e8e8ec)}.LhdpGW_table th.LhdpGW_sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.LhdpGW_table th.LhdpGW_sortable:hover{color:var(--dsw-alias-label-secondary,#9a9aa6)}.LhdpGW_table th.LhdpGW_sortActive{color:var(--dsw-alias-state-accent-primary,#5b8cff)}.LhdpGW_sortArrow{opacity:.8;margin-left:3px;font-size:8px}.LhdpGW_table tr.LhdpGW_clickable{cursor:pointer}.LhdpGW_table tr.LhdpGW_clickable:hover{background:var(--dsw-alias-interactive-bg-hover,#80808c12)}.LhdpGW_table tr.LhdpGW_total td{color:var(--dsw-alias-label-primary,#e8e8ec);border-bottom:0;border-top:1px solid var(--dsw-alias-border-l2,#2c2c34);font-weight:600}.LhdpGW_table tr.LhdpGW_disabled{opacity:.55}.LhdpGW_chipRow{flex-wrap:wrap;gap:8px;margin-bottom:14px;display:flex}.LhdpGW_chip{border:1px solid var(--dsw-alias-border-l2,#2c2c34);color:var(--dsw-alias-label-secondary,#9a9aa6);font:inherit;cursor:pointer;background:0 0;border-radius:999px;align-items:center;gap:6px;padding:5px 13px;font-size:12px;display:inline-flex}.LhdpGW_chip:hover{color:var(--dsw-alias-label-primary,#e8e8ec);border-color:var(--dsw-alias-label-tertiary,#67676f)}.LhdpGW_chip.LhdpGW_active{background:var(--dsw-alias-interactive-bg-active,#5b8cff24);color:var(--dsw-alias-state-accent-primary,#5b8cff);border-color:#5b8cff66;font-weight:600}.LhdpGW_statusDot{border-radius:50%;flex-shrink:0;width:8px;height:8px;display:inline-block}.LhdpGW_dim{color:var(--dsw-alias-label-tertiary,#67676f)}.LhdpGW_mono{color:var(--dsw-alias-label-secondary,#9a9aa6);font-family:ui-monospace,Consolas,monospace;font-size:11px}.LhdpGW_empty{color:var(--dsw-alias-label-tertiary,#67676f);padding:18px 0}.LhdpGW_warning{color:var(--dsw-alias-state-warning-primary,#e0a13c);margin-bottom:10px;font-size:11.5px}.LhdpGW_legend{color:var(--dsw-alias-label-tertiary,#67676f);flex-wrap:wrap;align-items:center;gap:10px;font-size:10.5px;display:inline-flex}.LhdpGW_legend i{border-radius:2px;width:10px;height:10px;margin-right:3px;display:inline-block}.LhdpGW_legend i.LhdpGW_ghost{background:#80808c1a;border:1px solid #80808c47}.LhdpGW_footnote{border-top:1px solid var(--dsw-alias-border-l1,#2c2c34);color:var(--dsw-alias-label-tertiary,#67676f);margin-top:26px;padding-top:14px;font-size:11.5px;line-height:1.8}.LhdpGW_footnote b{color:var(--dsw-alias-label-secondary,#9a9aa6);font-weight:500}.LhdpGW_footnoteList{margin:4px 0 0;padding-left:18px}.LhdpGW_footnoteList li{margin:1px 0}.LhdpGW_tooltip{z-index:9999;pointer-events:none;background:var(--dsw-alias-bg-layer-3,#232329);border:1px solid var(--dsw-alias-border-l2,#2c2c34);box-shadow:var(--dsw-shadow-lv2,0 8px 24px #00000059);min-width:210px;max-width:300px;color:var(--dsw-alias-label-primary,#e8e8ec);border-radius:10px;padding:10px 12px;font-family:inherit;font-size:12px;line-height:1.5;position:fixed}.LhdpGW_tipTitle{margin-bottom:2px;font-size:12px;font-weight:650}.LhdpGW_tipSub{color:var(--dsw-alias-label-secondary,#9a9aa6);margin-bottom:8px;font-size:11.5px}.LhdpGW_tipRow{color:var(--dsw-alias-label-secondary,#9a9aa6);align-items:center;gap:6px;padding:2px 0;font-size:11px;display:flex}.LhdpGW_tipRow i{border-radius:2px;flex-shrink:0;width:7px;height:7px}.LhdpGW_tipName{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.LhdpGW_tipAmount{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-tertiary,#67676f);text-align:right;width:58px}.LhdpGW_tipRate{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary,#e8e8ec);font-weight:600}.LhdpGW_tipBar{background:var(--dsw-alias-fill-l1,#80808c2e);border-radius:2px;height:3px;margin:1px 0 5px;overflow:hidden}.LhdpGW_tipBar>i{border-radius:2px;height:100%;display:block}.LhdpGW_tipDivider{border-top:1px solid var(--dsw-alias-border-l1,#2c2c34);color:var(--dsw-alias-label-secondary,#9a9aa6);justify-content:space-between;gap:12px;margin-top:6px;padding-top:6px;font-size:11px;display:flex}";
152
- const tagId$5 = "dsh-key-usage/shared.module.css";
153
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$5) + "]") === null) {
167
+ const css$6 = ".LhdpGW_section{color:var(--dsw-alias-label-primary,#e8e8ec);font-size:13.5px;line-height:1.55}.LhdpGW_title{justify-content:space-between;align-items:center;gap:10px;margin-bottom:12px;font-size:18px;font-weight:650;display:flex}.LhdpGW_titleActions{align-items:center;gap:8px;display:inline-flex}.LhdpGW_refreshBtn{border:1px solid var(--dsw-alias-border-l2,#2c2c34);color:var(--dsw-alias-label-secondary,#9a9aa6);cursor:pointer;background:0 0;border-radius:999px;padding:3px 12px;font-family:inherit;font-size:11.5px}.LhdpGW_refreshBtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#80808c1a);color:var(--dsw-alias-label-primary,#e8e8ec)}.LhdpGW_refreshBtn:disabled{opacity:.6;cursor:default}.LhdpGW_tabBar{border-bottom:1px solid var(--dsw-alias-border-l1,#2c2c34);flex-wrap:wrap;gap:2px;margin-bottom:18px;display:flex}.LhdpGW_tabBar button{color:var(--dsw-alias-label-secondary,#9a9aa6);font:inherit;cursor:pointer;background:0 0;border:0;border-bottom:2px solid #0000;margin-bottom:-1px;padding:8px 14px;font-size:13px}.LhdpGW_tabBar button:hover{color:var(--dsw-alias-label-primary,#e8e8ec)}.LhdpGW_tabBar button.LhdpGW_active{color:var(--dsw-alias-state-accent-primary,#5b8cff);border-bottom-color:var(--dsw-alias-state-accent-primary,#5b8cff);font-weight:600}.LhdpGW_card{background:var(--dsw-alias-bg-layer-2,#1d1d22);border:1px solid var(--dsw-alias-border-l1,#2c2c34);border-radius:12px;padding:14px 16px}.LhdpGW_cardGrid3{grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-bottom:12px;display:grid}.LhdpGW_cardGrid4{grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-bottom:18px;display:grid}.LhdpGW_cardGridAuto{grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:10px;margin-bottom:18px;display:grid}@media (max-width:720px){.LhdpGW_cardGrid4{grid-template-columns:repeat(2,minmax(0,1fr))}}.LhdpGW_statLabel{color:var(--dsw-alias-label-tertiary,#67676f);margin-bottom:6px;font-size:11.5px}.LhdpGW_statValue{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary,#e8e8ec);font-size:20px;font-weight:650}.LhdpGW_statExtra{color:var(--dsw-alias-label-tertiary,#67676f);margin-top:4px;font-size:11.5px}.LhdpGW_groupLabel{color:var(--dsw-alias-label-secondary,#9a9aa6);margin:4px 0 10px;font-size:12.5px;font-weight:600}.LhdpGW_blockLabel{color:var(--dsw-alias-label-secondary,#9a9aa6);margin:18px 0 4px;font-size:11.5px}.LhdpGW_table{border-collapse:collapse;width:100%;margin-top:10px}.LhdpGW_table th{text-align:right;color:var(--dsw-alias-label-tertiary,#67676f);border-bottom:1px solid var(--dsw-alias-border-l1,#2c2c34);white-space:nowrap;padding:5px 8px;font-size:10.5px;font-weight:500}.LhdpGW_table td{text-align:right;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary,#9a9aa6);vertical-align:middle;border-bottom:1px solid var(--dsw-alias-border-l1,#2c2c34);white-space:normal;overflow-wrap:anywhere;padding:7px 8px;font-size:12px}.LhdpGW_table th:first-child,.LhdpGW_table td:first-child{text-align:left;padding-left:0}.LhdpGW_table td:first-child{color:var(--dsw-alias-label-primary,#e8e8ec)}.LhdpGW_table th.LhdpGW_sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.LhdpGW_table th.LhdpGW_sortable:hover{color:var(--dsw-alias-label-secondary,#9a9aa6)}.LhdpGW_table th.LhdpGW_sortActive{color:var(--dsw-alias-state-accent-primary,#5b8cff)}.LhdpGW_sortArrow{opacity:.8;margin-left:3px;font-size:8px}.LhdpGW_table tr.LhdpGW_clickable{cursor:pointer}.LhdpGW_table tr.LhdpGW_clickable:hover{background:var(--dsw-alias-interactive-bg-hover,#80808c12)}.LhdpGW_table tr.LhdpGW_total td{color:var(--dsw-alias-label-primary,#e8e8ec);border-bottom:0;border-top:1px solid var(--dsw-alias-border-l2,#2c2c34);font-weight:600}.LhdpGW_table tr.LhdpGW_disabled{opacity:.55}.LhdpGW_chipRow{flex-wrap:wrap;gap:8px;margin-bottom:14px;display:flex}.LhdpGW_chip{border:1px solid var(--dsw-alias-border-l2,#2c2c34);color:var(--dsw-alias-label-secondary,#9a9aa6);font:inherit;cursor:pointer;background:0 0;border-radius:999px;align-items:center;gap:6px;padding:5px 13px;font-size:12px;display:inline-flex}.LhdpGW_chip:hover{color:var(--dsw-alias-label-primary,#e8e8ec);border-color:var(--dsw-alias-label-tertiary,#67676f)}.LhdpGW_chip.LhdpGW_active{background:var(--dsw-alias-interactive-bg-active,#5b8cff24);color:var(--dsw-alias-state-accent-primary,#5b8cff);border-color:#5b8cff66;font-weight:600}.LhdpGW_statusDot{border-radius:50%;flex-shrink:0;width:8px;height:8px;display:inline-block}.LhdpGW_dim{color:var(--dsw-alias-label-tertiary,#67676f)}.LhdpGW_mono{color:var(--dsw-alias-label-secondary,#9a9aa6);font-family:ui-monospace,Consolas,monospace;font-size:11px}.LhdpGW_empty{color:var(--dsw-alias-label-tertiary,#67676f);padding:18px 0}.LhdpGW_warning{color:var(--dsw-alias-state-warning-primary,#e0a13c);margin-bottom:10px;font-size:11.5px}.LhdpGW_legend{color:var(--dsw-alias-label-tertiary,#67676f);flex-wrap:wrap;align-items:center;gap:10px;font-size:10.5px;display:inline-flex}.LhdpGW_legend i{border-radius:2px;width:10px;height:10px;margin-right:3px;display:inline-block}.LhdpGW_legend i.LhdpGW_ghost{background:#80808c1a;border:1px solid #80808c47}.LhdpGW_footnote{border-top:1px solid var(--dsw-alias-border-l1,#2c2c34);color:var(--dsw-alias-label-tertiary,#67676f);margin-top:26px;padding-top:14px;font-size:11.5px;line-height:1.8}.LhdpGW_footnote b{color:var(--dsw-alias-label-secondary,#9a9aa6);font-weight:500}.LhdpGW_footnoteList{margin:4px 0 0;padding-left:18px}.LhdpGW_footnoteList li{margin:1px 0}.LhdpGW_tooltip{z-index:9999;pointer-events:none;background:var(--dsw-alias-bg-layer-3,#232329);border:1px solid var(--dsw-alias-border-l2,#2c2c34);box-shadow:var(--dsw-shadow-lv2,0 8px 24px #00000059);min-width:210px;max-width:300px;color:var(--dsw-alias-label-primary,#e8e8ec);border-radius:10px;padding:10px 12px;font-family:inherit;font-size:12px;line-height:1.5;position:fixed}.LhdpGW_tipTitle{margin-bottom:2px;font-size:12px;font-weight:650}.LhdpGW_tipSub{color:var(--dsw-alias-label-secondary,#9a9aa6);margin-bottom:8px;font-size:11.5px}.LhdpGW_tipRow{color:var(--dsw-alias-label-secondary,#9a9aa6);align-items:center;gap:6px;padding:2px 0;font-size:11px;display:flex}.LhdpGW_tipRow i{border-radius:2px;flex-shrink:0;width:7px;height:7px}.LhdpGW_tipName{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.LhdpGW_tipAmount{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-tertiary,#67676f);text-align:right;width:58px}.LhdpGW_tipRate{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary,#e8e8ec);font-weight:600}.LhdpGW_tipBar{background:var(--dsw-alias-fill-l1,#80808c2e);border-radius:2px;height:3px;margin:1px 0 5px;overflow:hidden}.LhdpGW_tipBar>i{border-radius:2px;height:100%;display:block}.LhdpGW_tipDivider{border-top:1px solid var(--dsw-alias-border-l1,#2c2c34);color:var(--dsw-alias-label-secondary,#9a9aa6);justify-content:space-between;gap:12px;margin-top:6px;padding-top:6px;font-size:11px;display:flex}";
168
+ const tagId$6 = "dsh-key-usage/shared.module.css";
169
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$6) + "]") === null) {
154
170
  const tag = document.createElement("style");
155
171
  tag.dataset.plugin = "dsh-key-usage";
156
- tag.dataset.pluginCss = tagId$5;
157
- tag.textContent = css$5;
172
+ tag.dataset.pluginCss = tagId$6;
173
+ tag.textContent = css$6;
158
174
  document.head.appendChild(tag);
159
175
  }
160
- const cssRegistry$5 = globalThis.__DSH_PLUGIN_CSS__ ??= [];
161
- if (!cssRegistry$5.some((e) => e.tagId === tagId$5)) cssRegistry$5.push({
162
- tagId: tagId$5,
163
- css: css$5
176
+ const cssRegistry$6 = globalThis.__DSH_PLUGIN_CSS__ ??= [];
177
+ if (!cssRegistry$6.some((e) => e.tagId === tagId$6)) cssRegistry$6.push({
178
+ tagId: tagId$6,
179
+ css: css$6
164
180
  });
165
181
  var shared_module_css_default = {
166
- "ghost": "LhdpGW_ghost",
167
182
  "statExtra": "LhdpGW_statExtra",
168
- "disabled": "LhdpGW_disabled",
169
- "footnote": "LhdpGW_footnote",
170
- "empty": "LhdpGW_empty",
171
- "titleActions": "LhdpGW_titleActions",
172
- "chipRow": "LhdpGW_chipRow",
173
- "tipAmount": "LhdpGW_tipAmount",
174
- "tipDivider": "LhdpGW_tipDivider",
175
183
  "groupLabel": "LhdpGW_groupLabel",
176
- "cardGrid4": "LhdpGW_cardGrid4",
177
- "title": "LhdpGW_title",
178
- "cardGridAuto": "LhdpGW_cardGridAuto",
179
- "warning": "LhdpGW_warning",
180
- "refreshBtn": "LhdpGW_refreshBtn",
181
- "footnoteList": "LhdpGW_footnoteList",
182
- "clickable": "LhdpGW_clickable",
183
184
  "statusDot": "LhdpGW_statusDot",
184
- "tipSub": "LhdpGW_tipSub",
185
- "tipTitle": "LhdpGW_tipTitle",
186
- "statLabel": "LhdpGW_statLabel",
187
- "tipRate": "LhdpGW_tipRate",
188
- "tipName": "LhdpGW_tipName",
189
185
  "statValue": "LhdpGW_statValue",
190
- "total": "LhdpGW_total",
191
- "blockLabel": "LhdpGW_blockLabel",
192
- "sortArrow": "LhdpGW_sortArrow",
186
+ "sortable": "LhdpGW_sortable",
187
+ "cardGrid4": "LhdpGW_cardGrid4",
188
+ "empty": "LhdpGW_empty",
189
+ "tooltip": "LhdpGW_tooltip",
190
+ "ghost": "LhdpGW_ghost",
191
+ "tipRate": "LhdpGW_tipRate",
192
+ "statLabel": "LhdpGW_statLabel",
193
+ "footnote": "LhdpGW_footnote",
194
+ "disabled": "LhdpGW_disabled",
195
+ "titleActions": "LhdpGW_titleActions",
196
+ "cardGrid3": "LhdpGW_cardGrid3",
193
197
  "tipRow": "LhdpGW_tipRow",
194
198
  "section": "LhdpGW_section",
195
- "cardGrid3": "LhdpGW_cardGrid3",
199
+ "refreshBtn": "LhdpGW_refreshBtn",
196
200
  "active": "LhdpGW_active",
201
+ "card": "LhdpGW_card",
202
+ "title": "LhdpGW_title",
203
+ "tabBar": "LhdpGW_tabBar",
204
+ "cardGridAuto": "LhdpGW_cardGridAuto",
205
+ "blockLabel": "LhdpGW_blockLabel",
197
206
  "sortActive": "LhdpGW_sortActive",
198
- "dim": "LhdpGW_dim",
199
- "table": "LhdpGW_table",
200
- "sortable": "LhdpGW_sortable",
207
+ "sortArrow": "LhdpGW_sortArrow",
208
+ "clickable": "LhdpGW_clickable",
209
+ "mono": "LhdpGW_mono",
210
+ "warning": "LhdpGW_warning",
201
211
  "legend": "LhdpGW_legend",
202
- "tooltip": "LhdpGW_tooltip",
212
+ "tipSub": "LhdpGW_tipSub",
213
+ "tipName": "LhdpGW_tipName",
214
+ "dim": "LhdpGW_dim",
215
+ "chip": "LhdpGW_chip",
203
216
  "tipBar": "LhdpGW_tipBar",
204
- "mono": "LhdpGW_mono",
205
- "tabBar": "LhdpGW_tabBar",
206
- "card": "LhdpGW_card",
207
- "chip": "LhdpGW_chip"
217
+ "chipRow": "LhdpGW_chipRow",
218
+ "table": "LhdpGW_table",
219
+ "footnoteList": "LhdpGW_footnoteList",
220
+ "tipTitle": "LhdpGW_tipTitle",
221
+ "total": "LhdpGW_total",
222
+ "tipAmount": "LhdpGW_tipAmount",
223
+ "tipDivider": "LhdpGW_tipDivider"
208
224
  };
209
225
  //#endregion
210
226
  //#region src/client/tooltip.tsx
@@ -355,51 +371,51 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
355
371
  }
356
372
  //#endregion
357
373
  //#region \0dsh-css:D:\EasyJob\.plugin\dsh-key-usage\src\client\styles\overview.module.css.mjs
358
- const css$4 = ".J7Idbq_keyTable{border-collapse:collapse;width:100%}.J7Idbq_keyTable th{text-align:right;color:var(--dsw-alias-label-tertiary,#67676f);border-bottom:1px solid var(--dsw-alias-border-l1,#2c2c34);white-space:nowrap;padding:6px 10px;font-size:10.5px;font-weight:500}.J7Idbq_keyTable th:first-child{text-align:left;padding-left:0}.J7Idbq_keyTable td{text-align:right;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary,#9a9aa6);border-bottom:1px solid var(--dsw-alias-fill-l1,#80808c1f);white-space:nowrap;padding:10px;font-size:12.5px}.J7Idbq_keyTable td:first-child{text-align:left;padding-left:0}.J7Idbq_keyTable tr:last-child td{border-bottom:0}.J7Idbq_keyTable tbody tr.J7Idbq_clickable{cursor:pointer}.J7Idbq_keyTable tbody tr.J7Idbq_clickable:hover{background:var(--dsw-alias-interactive-bg-hover,#80808c12)}.J7Idbq_keyTable tr.J7Idbq_disabledRow{opacity:.55}.J7Idbq_cellKey{align-items:center;gap:10px;display:flex}.J7Idbq_keyDisplay{color:var(--dsw-alias-label-primary,#e8e8ec);font-size:13px;font-weight:600}.J7Idbq_keyEnv{color:var(--dsw-alias-label-tertiary,#67676f);margin-top:1px;font-family:ui-monospace,Consolas,monospace;font-size:10px}.J7Idbq_tag{border:1px solid var(--dsw-alias-border-l2,#2c2c34);color:var(--dsw-alias-label-secondary,#9a9aa6);background:var(--dsw-alias-bg-layer-2,#1d1d22);border-radius:5px;padding:1px 7px;font-size:10.5px;display:inline-block}.J7Idbq_tag.J7Idbq_free{color:var(--dsw-alias-state-success-primary,#3fb96f);border-color:#3fb96f59}.J7Idbq_tag.J7Idbq_sub{color:var(--dsw-alias-state-accent-primary,#5b8cff);border-color:#5b8cff59}.J7Idbq_tag.J7Idbq_off{color:var(--dsw-alias-label-tertiary,#67676f)}.J7Idbq_rowLink{color:var(--dsw-alias-label-tertiary,#67676f);font-size:11px}.J7Idbq_keyTable tbody tr.J7Idbq_clickable:hover .J7Idbq_rowLink{color:var(--dsw-alias-state-accent-primary,#5b8cff)}.J7Idbq_quotaCell{align-items:center;gap:8px;display:inline-flex}.J7Idbq_miniBar{background:var(--dsw-alias-fill-l2,#80808c2e);border-radius:3px;flex-shrink:0;width:64px;height:5px;overflow:hidden}.J7Idbq_miniBar>i{border-radius:3px;height:100%;display:block}.J7Idbq_quotaText{text-align:left;min-width:74px}.J7Idbq_quotaText.J7Idbq_warn{color:var(--dsw-alias-state-warning-primary,#e0a13c);font-weight:600}.J7Idbq_quotaText.J7Idbq_danger{color:var(--dsw-alias-state-danger-primary,#e05a5a);font-weight:600}.J7Idbq_quotaText.J7Idbq_dim{color:var(--dsw-alias-label-tertiary,#67676f)}.J7Idbq_heatmapCard{background:var(--dsw-alias-bg-layer-2,#1d1d22);border:1px solid var(--dsw-alias-border-l1,#2c2c34);border-radius:12px;margin-top:24px;padding:14px 16px}.J7Idbq_heatmapHead{flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:6px;margin-bottom:10px;display:flex}.J7Idbq_heatmapTitle{color:var(--dsw-alias-label-secondary,#9a9aa6);font-size:11.5px}.J7Idbq_heatmapScroll{padding-bottom:4px;overflow-x:auto}.J7Idbq_heatmapBody{gap:6px;width:max-content;display:flex}.J7Idbq_heatWeekdays{flex-direction:column;flex-shrink:0;gap:3px;display:flex}.J7Idbq_heatWeekdays span{color:var(--dsw-alias-label-tertiary,#67676f);text-align:right;width:13px;height:28px;font-size:9.5px;line-height:28px}.J7Idbq_heatGridCol{flex-direction:column;display:flex}.J7Idbq_heatGrid{gap:3px;display:flex}.J7Idbq_heatWeek{flex-direction:column;gap:3px;display:flex}.J7Idbq_heatCell{box-sizing:border-box;background:var(--dsw-alias-fill-l1,#80808c2e);cursor:default;border-radius:5px;flex-shrink:0;width:28px;height:28px}.J7Idbq_heatCell.J7Idbq_idle{background:#80808c38}.J7Idbq_heatCell.J7Idbq_empty{background:#80808c1a;box-shadow:inset 0 0 0 1px #80808c47}.J7Idbq_heatMonths{gap:3px;margin-top:6px;display:flex}.J7Idbq_heatMonths span{width:28px;color:var(--dsw-alias-label-tertiary,#67676f);text-align:left;white-space:nowrap;flex-shrink:0;font-size:10px}";
359
- const tagId$4 = "dsh-key-usage/overview.module.css";
360
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]") === null) {
374
+ const css$5 = ".J7Idbq_keyTable{border-collapse:collapse;width:100%}.J7Idbq_keyTable th{text-align:right;color:var(--dsw-alias-label-tertiary,#67676f);border-bottom:1px solid var(--dsw-alias-border-l1,#2c2c34);white-space:nowrap;padding:6px 10px;font-size:10.5px;font-weight:500}.J7Idbq_keyTable th:first-child{text-align:left;padding-left:0}.J7Idbq_keyTable td{text-align:right;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary,#9a9aa6);border-bottom:1px solid var(--dsw-alias-fill-l1,#80808c1f);white-space:nowrap;padding:10px;font-size:12.5px}.J7Idbq_keyTable td:first-child{text-align:left;padding-left:0}.J7Idbq_keyTable tr:last-child td{border-bottom:0}.J7Idbq_keyTable tbody tr.J7Idbq_clickable{cursor:pointer}.J7Idbq_keyTable tbody tr.J7Idbq_clickable:hover{background:var(--dsw-alias-interactive-bg-hover,#80808c12)}.J7Idbq_keyTable tr.J7Idbq_disabledRow{opacity:.55}.J7Idbq_cellKey{align-items:center;gap:10px;display:flex}.J7Idbq_keyDisplay{color:var(--dsw-alias-label-primary,#e8e8ec);font-size:13px;font-weight:600}.J7Idbq_keyEnv{color:var(--dsw-alias-label-tertiary,#67676f);margin-top:1px;font-family:ui-monospace,Consolas,monospace;font-size:10px}.J7Idbq_tag{border:1px solid var(--dsw-alias-border-l2,#2c2c34);color:var(--dsw-alias-label-secondary,#9a9aa6);background:var(--dsw-alias-bg-layer-2,#1d1d22);border-radius:5px;padding:1px 7px;font-size:10.5px;display:inline-block}.J7Idbq_tag.J7Idbq_free{color:var(--dsw-alias-state-success-primary,#3fb96f);border-color:#3fb96f59}.J7Idbq_tag.J7Idbq_sub{color:var(--dsw-alias-state-accent-primary,#5b8cff);border-color:#5b8cff59}.J7Idbq_tag.J7Idbq_off{color:var(--dsw-alias-label-tertiary,#67676f)}.J7Idbq_rowLink{color:var(--dsw-alias-label-tertiary,#67676f);font-size:11px}.J7Idbq_keyTable tbody tr.J7Idbq_clickable:hover .J7Idbq_rowLink{color:var(--dsw-alias-state-accent-primary,#5b8cff)}.J7Idbq_quotaCell{align-items:center;gap:8px;display:inline-flex}.J7Idbq_miniBar{background:var(--dsw-alias-fill-l2,#80808c2e);border-radius:3px;flex-shrink:0;width:64px;height:5px;overflow:hidden}.J7Idbq_miniBar>i{border-radius:3px;height:100%;display:block}.J7Idbq_quotaText{text-align:left;min-width:74px}.J7Idbq_quotaText.J7Idbq_warn{color:var(--dsw-alias-state-warning-primary,#e0a13c);font-weight:600}.J7Idbq_quotaText.J7Idbq_danger{color:var(--dsw-alias-state-danger-primary,#e05a5a);font-weight:600}.J7Idbq_quotaText.J7Idbq_dim{color:var(--dsw-alias-label-tertiary,#67676f)}.J7Idbq_heatmapCard{background:var(--dsw-alias-bg-layer-2,#1d1d22);border:1px solid var(--dsw-alias-border-l1,#2c2c34);border-radius:12px;margin-top:24px;padding:14px 16px}.J7Idbq_heatmapHead{flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:6px;margin-bottom:10px;display:flex}.J7Idbq_heatmapTitle{color:var(--dsw-alias-label-secondary,#9a9aa6);font-size:11.5px}.J7Idbq_heatmapScroll{padding-bottom:4px;overflow-x:auto}.J7Idbq_heatmapBody{gap:6px;width:max-content;display:flex}.J7Idbq_heatWeekdays{flex-direction:column;flex-shrink:0;gap:3px;display:flex}.J7Idbq_heatWeekdays span{color:var(--dsw-alias-label-tertiary,#67676f);text-align:right;width:13px;height:28px;font-size:9.5px;line-height:28px}.J7Idbq_heatGridCol{flex-direction:column;display:flex}.J7Idbq_heatGrid{gap:3px;display:flex}.J7Idbq_heatWeek{flex-direction:column;gap:3px;display:flex}.J7Idbq_heatCell{box-sizing:border-box;background:var(--dsw-alias-fill-l1,#80808c2e);cursor:default;border-radius:5px;flex-shrink:0;width:28px;height:28px}.J7Idbq_heatCell.J7Idbq_idle{background:#80808c38}.J7Idbq_heatCell.J7Idbq_empty{background:#80808c1a;box-shadow:inset 0 0 0 1px #80808c47}.J7Idbq_heatMonths{gap:3px;margin-top:6px;display:flex}.J7Idbq_heatMonths span{width:28px;color:var(--dsw-alias-label-tertiary,#67676f);text-align:left;white-space:nowrap;flex-shrink:0;font-size:10px}";
375
+ const tagId$5 = "dsh-key-usage/overview.module.css";
376
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$5) + "]") === null) {
361
377
  const tag = document.createElement("style");
362
378
  tag.dataset.plugin = "dsh-key-usage";
363
- tag.dataset.pluginCss = tagId$4;
364
- tag.textContent = css$4;
379
+ tag.dataset.pluginCss = tagId$5;
380
+ tag.textContent = css$5;
365
381
  document.head.appendChild(tag);
366
382
  }
367
- const cssRegistry$4 = globalThis.__DSH_PLUGIN_CSS__ ??= [];
368
- if (!cssRegistry$4.some((e) => e.tagId === tagId$4)) cssRegistry$4.push({
369
- tagId: tagId$4,
370
- css: css$4
383
+ const cssRegistry$5 = globalThis.__DSH_PLUGIN_CSS__ ??= [];
384
+ if (!cssRegistry$5.some((e) => e.tagId === tagId$5)) cssRegistry$5.push({
385
+ tagId: tagId$5,
386
+ css: css$5
371
387
  });
372
388
  var overview_module_css_default = {
373
- "danger": "J7Idbq_danger",
389
+ "quotaText": "J7Idbq_quotaText",
390
+ "keyDisplay": "J7Idbq_keyDisplay",
391
+ "warn": "J7Idbq_warn",
392
+ "cellKey": "J7Idbq_cellKey",
393
+ "miniBar": "J7Idbq_miniBar",
394
+ "free": "J7Idbq_free",
395
+ "sub": "J7Idbq_sub",
396
+ "heatmapTitle": "J7Idbq_heatmapTitle",
397
+ "idle": "J7Idbq_idle",
374
398
  "rowLink": "J7Idbq_rowLink",
399
+ "heatCell": "J7Idbq_heatCell",
400
+ "keyEnv": "J7Idbq_keyEnv",
401
+ "heatmapHead": "J7Idbq_heatmapHead",
402
+ "heatMonths": "J7Idbq_heatMonths",
403
+ "heatGridCol": "J7Idbq_heatGridCol",
404
+ "keyTable": "J7Idbq_keyTable",
375
405
  "heatmapCard": "J7Idbq_heatmapCard",
376
- "quotaCell": "J7Idbq_quotaCell",
406
+ "danger": "J7Idbq_danger",
377
407
  "disabledRow": "J7Idbq_disabledRow",
378
- "idle": "J7Idbq_idle",
408
+ "heatmapBody": "J7Idbq_heatmapBody",
409
+ "heatWeekdays": "J7Idbq_heatWeekdays",
410
+ "tag": "J7Idbq_tag",
411
+ "heatmapScroll": "J7Idbq_heatmapScroll",
412
+ "off": "J7Idbq_off",
379
413
  "heatGrid": "J7Idbq_heatGrid",
380
- "heatMonths": "J7Idbq_heatMonths",
381
- "free": "J7Idbq_free",
382
- "keyEnv": "J7Idbq_keyEnv",
383
- "heatCell": "J7Idbq_heatCell",
384
- "heatGridCol": "J7Idbq_heatGridCol",
385
- "keyDisplay": "J7Idbq_keyDisplay",
386
414
  "heatWeek": "J7Idbq_heatWeek",
387
415
  "dim": "J7Idbq_dim",
416
+ "quotaCell": "J7Idbq_quotaCell",
388
417
  "clickable": "J7Idbq_clickable",
389
- "quotaText": "J7Idbq_quotaText",
390
- "heatmapBody": "J7Idbq_heatmapBody",
391
- "heatmapTitle": "J7Idbq_heatmapTitle",
392
- "warn": "J7Idbq_warn",
393
- "heatmapHead": "J7Idbq_heatmapHead",
394
- "keyTable": "J7Idbq_keyTable",
395
- "sub": "J7Idbq_sub",
396
- "miniBar": "J7Idbq_miniBar",
397
- "heatmapScroll": "J7Idbq_heatmapScroll",
398
- "empty": "J7Idbq_empty",
399
- "tag": "J7Idbq_tag",
400
- "off": "J7Idbq_off",
401
- "cellKey": "J7Idbq_cellKey",
402
- "heatWeekdays": "J7Idbq_heatWeekdays"
418
+ "empty": "J7Idbq_empty"
403
419
  };
404
420
  //#endregion
405
421
  //#region src/client/components/Heatmap.tsx
@@ -487,6 +503,12 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
487
503
  monthLabels: labels
488
504
  };
489
505
  }, [days, heatmapByDate]);
506
+ const scrollRef = (0, react.useRef)(null);
507
+ (0, react.useEffect)(() => {
508
+ const node = scrollRef.current;
509
+ if (node === null) return;
510
+ node.scrollLeft = node.scrollWidth - node.clientWidth;
511
+ }, []);
490
512
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
491
513
  className: overview_module_css_default.heatmapCard,
492
514
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -506,6 +528,7 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
506
528
  })]
507
529
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
508
530
  className: overview_module_css_default.heatmapScroll,
531
+ ref: scrollRef,
509
532
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
510
533
  className: overview_module_css_default.heatmapBody,
511
534
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -703,45 +726,87 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
703
726
  });
704
727
  }
705
728
  //#endregion
729
+ //#region src/client/export.ts
730
+ /**
731
+ * 导出工具:纯函数生成 CSV / JSON 文本 + 用 Blob 触发浏览器下载。
732
+ * 零依赖、零服务端往返,导出的永远是**客户端当前筛选后**的数据。
733
+ *
734
+ * 两条口径约束:
735
+ * - CSV 必须带 UTF-8 BOM:Excel 默认按本地代码页解码,没有 BOM 中文会变乱码;
736
+ * - JSON 不能带 BOM:`JSON.parse` 遇到 BOM 会直接抛错。
737
+ */
738
+ const UTF8_BOM = "";
739
+ const CSV_MIME = "text/csv;charset=utf-8";
740
+ const JSON_MIME = "application/json;charset=utf-8";
741
+ /** 表头 + 数据行 → CSV 文本(转义见 `csvCell`,换行用 RFC 4180 的 CRLF)。 */
742
+ function toCsv(rows, columns) {
743
+ const lines = [columns.map((column) => csvCell(column.label)).join(",")];
744
+ for (const row of rows) lines.push(columns.map((column) => csvCell(column.value(row))).join(","));
745
+ return UTF8_BOM + lines.join("\r\n") + "\r\n";
746
+ }
747
+ function toJson(value) {
748
+ return JSON.stringify(value, null, 2) + "\n";
749
+ }
750
+ /** 下载文件名:`dsh-usage-<主题>-<当天>.<扩展名>`,主题用英文短名避免跨端中文文件名乱码。 */
751
+ function exportFileName(topic, extension) {
752
+ return "dsh-usage-" + topic + "-" + isoDate(/* @__PURE__ */ new Date()) + "." + extension;
753
+ }
754
+ function downloadFile(name, text, mime) {
755
+ const objectUrl = URL.createObjectURL(new Blob([text], { type: mime }));
756
+ const anchor = document.createElement("a");
757
+ anchor.href = objectUrl;
758
+ anchor.download = name;
759
+ document.body.appendChild(anchor);
760
+ anchor.click();
761
+ anchor.remove();
762
+ window.setTimeout(() => URL.revokeObjectURL(objectUrl), 0);
763
+ }
764
+ /** 逗号、引号、换行都必须包引号并把内部引号翻倍,否则标题里的逗号会撑破列。 */
765
+ function csvCell(value) {
766
+ if (value === null) return "";
767
+ const text = String(value);
768
+ return /[",\r\n]/.test(text) ? "\"" + text.replace(/"/g, "\"\"") + "\"" : text;
769
+ }
770
+ //#endregion
706
771
  //#region \0dsh-css:D:\EasyJob\.plugin\dsh-key-usage\src\client\styles\detail.module.css.mjs
707
- const css$3 = ".V9QVRa_rangeBar{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:10px;margin-bottom:12px;display:flex}.V9QVRa_rangeLeft{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.V9QVRa_rangeTabs{border:1px solid var(--dsw-alias-border-l2,#2c2c34);border-radius:9px;gap:2px;padding:2px;display:inline-flex}.V9QVRa_rangeTabs button{color:var(--dsw-alias-label-secondary,#9a9aa6);font:inherit;cursor:pointer;background:0 0;border:0;border-radius:7px;padding:4px 13px;font-size:12px}.V9QVRa_rangeTabs button.V9QVRa_active{background:var(--dsw-alias-interactive-bg-active,#5b8cff24);color:var(--dsw-alias-state-accent-primary,#5b8cff);font-weight:600}.V9QVRa_rangeInput{border:1px solid var(--dsw-alias-border-l2,#2c2c34);color:var(--dsw-alias-label-primary,#e8e8ec);font:inherit;--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;background:0 0;border-radius:7px;padding:3px 8px;font-size:12px}@media (prefers-color-scheme:dark){.V9QVRa_rangeInput{--lightningcss-light: ;--lightningcss-dark:initial}}.V9QVRa_rangeCaption{color:var(--dsw-alias-label-tertiary,#67676f);margin-left:auto;font-size:11.5px}.V9QVRa_shareBar{border-radius:6px;gap:2px;height:12px;margin:8px 0 2px;display:flex;overflow:hidden}.V9QVRa_shareBar>i{cursor:default;height:100%;display:block}.V9QVRa_shareBar>i:hover{filter:brightness(1.15)}.V9QVRa_chartTitle{flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:10px;display:flex}.V9QVRa_bars{align-items:flex-end;gap:3px;height:110px;margin-top:8px;display:flex}.V9QVRa_barsDense{gap:2px}.V9QVRa_barColumn{cursor:default;flex-direction:column;flex:1;justify-content:flex-end;gap:1px;height:100%;display:flex}.V9QVRa_barColumn:hover{filter:brightness(1.2)}.V9QVRa_segCache{background:var(--dsw-alias-state-accent-tertiary,#5b8cff47);border-radius:2px 2px 0 0}.V9QVRa_segIn{background:var(--dsw-alias-state-accent-primary,#5b8cff)}.V9QVRa_segOut{background:var(--dsw-alias-state-business-primary,#9a7bff);border-radius:0 0 2px 2px}.V9QVRa_barLabels{margin-top:6px;display:flex}.V9QVRa_barLabels span{text-align:center;color:var(--dsw-alias-label-tertiary,#67676f);white-space:nowrap;flex:1;font-size:9.5px}.V9QVRa_subRowCell{background:var(--dsw-alias-fill-l1,#80808c0d);text-align:left!important;padding:2px 10px 10px 30px!important}.V9QVRa_subHead,.V9QVRa_subRow{grid-template-columns:minmax(150px,1.5fr) minmax(70px,1fr) minmax(70px,1fr) minmax(70px,1fr) minmax(80px,1.1fr) minmax(70px,1fr);align-items:center;gap:8px;display:grid}.V9QVRa_subHead{color:var(--dsw-alias-label-tertiary,#67676f);border-bottom:1px solid var(--dsw-alias-fill-l2,#80808c33);padding:4px 0;font-size:10px}.V9QVRa_subRow{border-bottom:1px dashed var(--dsw-alias-fill-l1,#80808c24);padding:5px 0;font-size:11.5px}.V9QVRa_subRow:last-child{border-bottom:0}.V9QVRa_subName{flex-direction:column;gap:1px;min-width:0;display:flex}.V9QVRa_subModel{color:var(--dsw-alias-label-primary,#e8e8ec);text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,Consolas,monospace;font-size:11px;overflow:hidden}.V9QVRa_subChannel{color:var(--dsw-alias-label-tertiary,#67676f);text-overflow:ellipsis;white-space:nowrap;font-family:inherit;font-size:10px;overflow:hidden}.V9QVRa_subNumber{text-align:right;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary,#9a9aa6)}.V9QVRa_toggleArrow{width:13px;color:var(--dsw-alias-label-tertiary,#67676f);font-size:9px;display:inline-block}";
708
- const tagId$3 = "dsh-key-usage/detail.module.css";
709
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
772
+ const css$4 = ".V9QVRa_rangeBar{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:10px;margin-bottom:12px;display:flex}.V9QVRa_rangeLeft{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.V9QVRa_rangeTabs{border:1px solid var(--dsw-alias-border-l2,#2c2c34);border-radius:9px;gap:2px;padding:2px;display:inline-flex}.V9QVRa_rangeTabs button{color:var(--dsw-alias-label-secondary,#9a9aa6);font:inherit;cursor:pointer;background:0 0;border:0;border-radius:7px;padding:4px 13px;font-size:12px}.V9QVRa_rangeTabs button.V9QVRa_active{background:var(--dsw-alias-interactive-bg-active,#5b8cff24);color:var(--dsw-alias-state-accent-primary,#5b8cff);font-weight:600}.V9QVRa_rangeInput{border:1px solid var(--dsw-alias-border-l2,#2c2c34);color:var(--dsw-alias-label-primary,#e8e8ec);font:inherit;--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;background:0 0;border-radius:7px;padding:3px 8px;font-size:12px}@media (prefers-color-scheme:dark){.V9QVRa_rangeInput{--lightningcss-light: ;--lightningcss-dark:initial}}.V9QVRa_rangeCaption{color:var(--dsw-alias-label-tertiary,#67676f);margin-left:auto;font-size:11.5px}.V9QVRa_shareBar{border-radius:6px;gap:2px;height:12px;margin:8px 0 2px;display:flex;overflow:hidden}.V9QVRa_shareBar>i{cursor:default;height:100%;display:block}.V9QVRa_shareBar>i:hover{filter:brightness(1.15)}.V9QVRa_chartTitle{flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:10px;display:flex}.V9QVRa_bars{align-items:flex-end;gap:3px;height:110px;margin-top:8px;display:flex}.V9QVRa_barsDense{gap:2px}.V9QVRa_barColumn{cursor:default;flex-direction:column;flex:1;justify-content:flex-end;gap:1px;height:100%;display:flex}.V9QVRa_barColumn:hover{filter:brightness(1.2)}.V9QVRa_segCache{background:var(--dsw-alias-state-accent-tertiary,#5b8cff47);border-radius:2px 2px 0 0}.V9QVRa_segIn{background:var(--dsw-alias-state-accent-primary,#5b8cff)}.V9QVRa_segOut{background:var(--dsw-alias-state-business-primary,#9a7bff);border-radius:0 0 2px 2px}.V9QVRa_barLabels{margin-top:6px;display:flex}.V9QVRa_barLabels span{text-align:center;color:var(--dsw-alias-label-tertiary,#67676f);white-space:nowrap;flex:1;font-size:9.5px}.V9QVRa_subRowCell{background:var(--dsw-alias-fill-l1,#80808c0d);text-align:left!important;padding:2px 10px 10px 30px!important}.V9QVRa_subHead,.V9QVRa_subRow{grid-template-columns:minmax(150px,1.5fr) minmax(70px,1fr) minmax(70px,1fr) minmax(70px,1fr) minmax(80px,1.1fr) minmax(70px,1fr);align-items:center;gap:8px;display:grid}.V9QVRa_subHead{color:var(--dsw-alias-label-tertiary,#67676f);border-bottom:1px solid var(--dsw-alias-fill-l2,#80808c33);padding:4px 0;font-size:10px}.V9QVRa_subRow{border-bottom:1px dashed var(--dsw-alias-fill-l1,#80808c24);padding:5px 0;font-size:11.5px}.V9QVRa_subRow:last-child{border-bottom:0}.V9QVRa_subName{flex-direction:column;gap:1px;min-width:0;display:flex}.V9QVRa_subModel{color:var(--dsw-alias-label-primary,#e8e8ec);text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,Consolas,monospace;font-size:11px;overflow:hidden}.V9QVRa_subChannel{color:var(--dsw-alias-label-tertiary,#67676f);text-overflow:ellipsis;white-space:nowrap;font-family:inherit;font-size:10px;overflow:hidden}.V9QVRa_subNumber{text-align:right;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary,#9a9aa6)}.V9QVRa_toggleArrow{width:13px;color:var(--dsw-alias-label-tertiary,#67676f);font-size:9px;display:inline-block}";
773
+ const tagId$4 = "dsh-key-usage/detail.module.css";
774
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]") === null) {
710
775
  const tag = document.createElement("style");
711
776
  tag.dataset.plugin = "dsh-key-usage";
712
- tag.dataset.pluginCss = tagId$3;
713
- tag.textContent = css$3;
777
+ tag.dataset.pluginCss = tagId$4;
778
+ tag.textContent = css$4;
714
779
  document.head.appendChild(tag);
715
780
  }
716
- const cssRegistry$3 = globalThis.__DSH_PLUGIN_CSS__ ??= [];
717
- if (!cssRegistry$3.some((e) => e.tagId === tagId$3)) cssRegistry$3.push({
718
- tagId: tagId$3,
719
- css: css$3
781
+ const cssRegistry$4 = globalThis.__DSH_PLUGIN_CSS__ ??= [];
782
+ if (!cssRegistry$4.some((e) => e.tagId === tagId$4)) cssRegistry$4.push({
783
+ tagId: tagId$4,
784
+ css: css$4
720
785
  });
721
786
  var detail_module_css_default = {
722
- "subRowCell": "V9QVRa_subRowCell",
787
+ "toggleArrow": "V9QVRa_toggleArrow",
788
+ "barColumn": "V9QVRa_barColumn",
723
789
  "barsDense": "V9QVRa_barsDense",
724
- "rangeLeft": "V9QVRa_rangeLeft",
725
- "rangeBar": "V9QVRa_rangeBar",
790
+ "shareBar": "V9QVRa_shareBar",
726
791
  "subModel": "V9QVRa_subModel",
727
- "subName": "V9QVRa_subName",
792
+ "rangeTabs": "V9QVRa_rangeTabs",
793
+ "rangeCaption": "V9QVRa_rangeCaption",
794
+ "subHead": "V9QVRa_subHead",
728
795
  "subChannel": "V9QVRa_subChannel",
729
796
  "subNumber": "V9QVRa_subNumber",
730
797
  "rangeInput": "V9QVRa_rangeInput",
731
- "segCache": "V9QVRa_segCache",
732
- "segOut": "V9QVRa_segOut",
733
- "subHead": "V9QVRa_subHead",
734
- "toggleArrow": "V9QVRa_toggleArrow",
735
- "rangeTabs": "V9QVRa_rangeTabs",
736
- "active": "V9QVRa_active",
737
- "barColumn": "V9QVRa_barColumn",
738
798
  "chartTitle": "V9QVRa_chartTitle",
739
- "shareBar": "V9QVRa_shareBar",
740
- "rangeCaption": "V9QVRa_rangeCaption",
741
- "bars": "V9QVRa_bars",
742
- "barLabels": "V9QVRa_barLabels",
799
+ "segIn": "V9QVRa_segIn",
800
+ "segOut": "V9QVRa_segOut",
743
801
  "subRow": "V9QVRa_subRow",
744
- "segIn": "V9QVRa_segIn"
802
+ "bars": "V9QVRa_bars",
803
+ "segCache": "V9QVRa_segCache",
804
+ "subRowCell": "V9QVRa_subRowCell",
805
+ "rangeLeft": "V9QVRa_rangeLeft",
806
+ "active": "V9QVRa_active",
807
+ "rangeBar": "V9QVRa_rangeBar",
808
+ "subName": "V9QVRa_subName",
809
+ "barLabels": "V9QVRa_barLabels"
745
810
  };
746
811
  //#endregion
747
812
  //#region src/client/components/ShareBar.tsx
@@ -878,11 +943,31 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
878
943
  })] });
879
944
  }
880
945
  //#endregion
946
+ //#region src/client/rows.ts
947
+ function dropEmptyRows(rows, tokensOf) {
948
+ const kept = rows.filter((row) => tokensOf(row) > 0);
949
+ return {
950
+ rows: kept,
951
+ hidden: rows.length - kept.length
952
+ };
953
+ }
954
+ /** 列表底部的一行小字:说明隐藏了什么、为什么。 */
955
+ function hiddenEmptyRowsText(hidden) {
956
+ return "已隐藏 " + String(hidden) + " 行无 Token 记录(调用失败或未产生用量;调用次数仍计入合计)";
957
+ }
958
+ //#endregion
881
959
  //#region src/client/components/DailyDetailTable.tsx
882
960
  /**
883
961
  * 每日细目:以「天」为折叠单位,展开行用原型子表格展示模型维度
884
962
  * (模型 + 调用 / 输入 / 输出 / 缓存读 / 命中率,不占主表列)。
885
963
  */
964
+ /** 输入/输出为 0 时不再显示 `0`(全缓存命中或纯输出模型的 0 是噪音),改显示淡化的 `—`。 */
965
+ function tokenCellOf(value) {
966
+ return value === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
967
+ className: shared_module_css_default.dim,
968
+ children: "—"
969
+ }) : fmtTokens(value);
970
+ }
886
971
  function hitRateOf$1(cacheReadTokens, inputTokens, outputTokens) {
887
972
  const total = inputTokens + outputTokens + cacheReadTokens;
888
973
  if (total <= 0) return "—";
@@ -941,7 +1026,9 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
941
1026
  }
942
1027
  }
943
1028
  const comparer = COMPARERS[sort.key] ?? COMPARERS.period;
944
- const ordered = [...aggregates.values()].sort((first, second) => {
1029
+ const nonEmpty = [...aggregates.values()].filter((entry) => entry.totalTokens > 0);
1030
+ const hiddenEmpty = aggregates.size - nonEmpty.length;
1031
+ const ordered = nonEmpty.sort((first, second) => {
945
1032
  const compared = comparer(first, second);
946
1033
  return sort.dir === "asc" ? compared : -compared;
947
1034
  });
@@ -971,117 +1058,144 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
971
1058
  className: shared_module_css_default.empty,
972
1059
  children: "所选范围内无用量记录"
973
1060
  })] });
974
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
975
- className: shared_module_css_default.blockLabel,
976
- children: "每日细目"
977
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
978
- className: shared_module_css_default.table,
979
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
980
- header("period", "日期"),
981
- header("total", "总计"),
982
- header("calls", "调用"),
983
- header("input", "输入"),
984
- header("output", "输出"),
985
- header("cacheRead", "缓存读"),
986
- header("hit", "命中率")
987
- ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tbody", { children: [ordered.map((period) => {
988
- const modelsInPeriod = modelRows.filter((row) => row.period === period.period).sort((first, second) => second.totalTokens - first.totalTokens);
989
- const expandable = modelsInPeriod.length > 1;
990
- const isOpen = expandable && expandedPeriods[period.period] === true;
991
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
992
- className: expandable ? shared_module_css_default.clickable : "",
993
- onClick: expandable ? () => onTogglePeriod(period.period) : void 0,
994
- children: [
995
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [expandable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
996
- className: detail_module_css_default.toggleArrow,
997
- children: isOpen ? "▼" : "▶"
998
- }), period.period] }),
999
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtTokens(period.totalTokens) }),
1000
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtCalls(period.calls) }),
1001
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtTokens(period.inputTokens) }),
1002
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtTokens(period.outputTokens) }),
1003
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtTokens(period.cacheReadTokens) }),
1004
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: hitRateOf$1(period.cacheReadTokens, period.inputTokens, period.outputTokens) })
1005
- ]
1006
- }), isOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", {
1007
- className: detail_module_css_default.subRowCell,
1008
- colSpan: 7,
1009
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1010
- className: detail_module_css_default.subHead,
1011
- children: [
1012
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {}),
1013
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1014
- className: detail_module_css_default.subNumber,
1015
- children: "调用"
1016
- }),
1017
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1018
- className: detail_module_css_default.subNumber,
1019
- children: "输入"
1020
- }),
1021
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1022
- className: detail_module_css_default.subNumber,
1023
- children: "输出"
1024
- }),
1025
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1026
- className: detail_module_css_default.subNumber,
1027
- children: "缓存读"
1028
- }),
1029
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1030
- className: detail_module_css_default.subNumber,
1031
- children: "命中率"
1032
- })
1033
- ]
1034
- }), modelsInPeriod.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1035
- className: detail_module_css_default.subRow,
1061
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1062
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1063
+ className: shared_module_css_default.blockLabel,
1064
+ children: "每日细目"
1065
+ }),
1066
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
1067
+ className: shared_module_css_default.table,
1068
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
1069
+ header("period", "日期"),
1070
+ header("total", "总计"),
1071
+ header("calls", "调用"),
1072
+ header("input", "输入"),
1073
+ header("output", "输出"),
1074
+ header("cacheRead", "缓存读"),
1075
+ header("hit", "命中率")
1076
+ ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tbody", { children: [ordered.map((period) => {
1077
+ const modelsInPeriod = modelRows.filter((row) => row.period === period.period).filter((row) => row.totalTokens > 0).sort((first, second) => second.totalTokens - first.totalTokens);
1078
+ const expandable = modelsInPeriod.length > 1;
1079
+ const isOpen = expandable && expandedPeriods[period.period] === true;
1080
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
1081
+ className: expandable ? shared_module_css_default.clickable : "",
1082
+ onClick: expandable ? () => onTogglePeriod(period.period) : void 0,
1036
1083
  children: [
1037
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1038
- className: detail_module_css_default.subName,
1039
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1040
- className: detail_module_css_default.subModel,
1041
- children: row.model
1042
- }), channelFilter === "ALL" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1043
- className: detail_module_css_default.subChannel,
1044
- children: displayNameOf(channels, row.provider)
1045
- })]
1046
- }),
1047
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1048
- className: detail_module_css_default.subNumber,
1049
- children: fmtCalls(row.calls)
1050
- }),
1051
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1052
- className: detail_module_css_default.subNumber,
1053
- children: fmtTokens(row.inputTokens)
1054
- }),
1055
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1056
- className: detail_module_css_default.subNumber,
1057
- children: fmtTokens(row.outputTokens)
1058
- }),
1059
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1060
- className: detail_module_css_default.subNumber,
1061
- children: fmtTokens(row.cacheReadTokens)
1062
- }),
1063
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1064
- className: detail_module_css_default.subNumber,
1065
- children: hitRateOf$1(row.cacheReadTokens, row.inputTokens, row.outputTokens)
1066
- })
1084
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [expandable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1085
+ className: detail_module_css_default.toggleArrow,
1086
+ children: isOpen ? "▼" : "▶"
1087
+ }), period.period] }),
1088
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtTokens(period.totalTokens) }),
1089
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtCalls(period.calls) }),
1090
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: tokenCellOf(period.inputTokens) }),
1091
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: tokenCellOf(period.outputTokens) }),
1092
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtTokens(period.cacheReadTokens) }),
1093
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: hitRateOf$1(period.cacheReadTokens, period.inputTokens, period.outputTokens) })
1067
1094
  ]
1068
- }, row.period + "|" + row.model + "|" + row.provider))]
1069
- }) })] }, period.period);
1070
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
1071
- className: shared_module_css_default.total,
1072
- children: [
1073
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: "合计" }),
1074
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtTokens(totalTokens) }),
1075
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtCalls(totals?.calls ?? 0) }),
1076
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtTokens(totals?.inputTokens ?? 0) }),
1077
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtTokens(totals?.outputTokens ?? 0) }),
1078
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtTokens(totals?.cacheReadTokens ?? 0) }),
1079
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: hitRateOf$1(totals?.cacheReadTokens ?? 0, totals?.inputTokens ?? 0, totals?.outputTokens ?? 0) })
1080
- ]
1081
- })] })]
1082
- })] });
1095
+ }), isOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", {
1096
+ className: detail_module_css_default.subRowCell,
1097
+ colSpan: 7,
1098
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1099
+ className: detail_module_css_default.subHead,
1100
+ children: [
1101
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {}),
1102
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1103
+ className: detail_module_css_default.subNumber,
1104
+ children: "调用"
1105
+ }),
1106
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1107
+ className: detail_module_css_default.subNumber,
1108
+ children: "输入"
1109
+ }),
1110
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1111
+ className: detail_module_css_default.subNumber,
1112
+ children: "输出"
1113
+ }),
1114
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1115
+ className: detail_module_css_default.subNumber,
1116
+ children: "缓存读"
1117
+ }),
1118
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1119
+ className: detail_module_css_default.subNumber,
1120
+ children: "命中率"
1121
+ })
1122
+ ]
1123
+ }), modelsInPeriod.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1124
+ className: detail_module_css_default.subRow,
1125
+ children: [
1126
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1127
+ className: detail_module_css_default.subName,
1128
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1129
+ className: detail_module_css_default.subModel,
1130
+ children: row.model
1131
+ }), channelFilter === "ALL" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1132
+ className: detail_module_css_default.subChannel,
1133
+ children: displayNameOf(channels, row.provider)
1134
+ })]
1135
+ }),
1136
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1137
+ className: detail_module_css_default.subNumber,
1138
+ children: fmtCalls(row.calls)
1139
+ }),
1140
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1141
+ className: detail_module_css_default.subNumber,
1142
+ children: tokenCellOf(row.inputTokens)
1143
+ }),
1144
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1145
+ className: detail_module_css_default.subNumber,
1146
+ children: tokenCellOf(row.outputTokens)
1147
+ }),
1148
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1149
+ className: detail_module_css_default.subNumber,
1150
+ children: fmtTokens(row.cacheReadTokens)
1151
+ }),
1152
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1153
+ className: detail_module_css_default.subNumber,
1154
+ children: hitRateOf$1(row.cacheReadTokens, row.inputTokens, row.outputTokens)
1155
+ })
1156
+ ]
1157
+ }, row.period + "|" + row.model + "|" + row.provider))]
1158
+ }) })] }, period.period);
1159
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
1160
+ className: shared_module_css_default.total,
1161
+ children: [
1162
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: "合计" }),
1163
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtTokens(totalTokens) }),
1164
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtCalls(totals?.calls ?? 0) }),
1165
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtTokens(totals?.inputTokens ?? 0) }),
1166
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtTokens(totals?.outputTokens ?? 0) }),
1167
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtTokens(totals?.cacheReadTokens ?? 0) }),
1168
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: hitRateOf$1(totals?.cacheReadTokens ?? 0, totals?.inputTokens ?? 0, totals?.outputTokens ?? 0) })
1169
+ ]
1170
+ })] })]
1171
+ }),
1172
+ hiddenEmpty > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1173
+ className: shared_module_css_default.statExtra,
1174
+ children: hiddenEmptyRowsText(hiddenEmpty)
1175
+ })
1176
+ ] });
1083
1177
  }
1084
1178
  //#endregion
1179
+ //#region \0dsh-css:D:\EasyJob\.plugin\dsh-key-usage\src\client\styles\export.module.css.mjs
1180
+ const css$3 = ".m5lYYW_exportGroup{align-items:center;gap:6px;margin-left:auto;display:inline-flex}.m5lYYW_exportBtn{border:1px solid var(--dsw-alias-border-l2,#2c2c34);color:var(--dsw-alias-label-secondary,#9a9aa6);font:inherit;cursor:pointer;white-space:nowrap;background:0 0;border-radius:999px;padding:4px 11px;font-size:11.5px}.m5lYYW_exportBtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#80808c1a);color:var(--dsw-alias-label-primary,#e8e8ec)}.m5lYYW_exportBtn:disabled{opacity:.5;cursor:default}";
1181
+ const tagId$3 = "dsh-key-usage/export.module.css";
1182
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
1183
+ const tag = document.createElement("style");
1184
+ tag.dataset.plugin = "dsh-key-usage";
1185
+ tag.dataset.pluginCss = tagId$3;
1186
+ tag.textContent = css$3;
1187
+ document.head.appendChild(tag);
1188
+ }
1189
+ const cssRegistry$3 = globalThis.__DSH_PLUGIN_CSS__ ??= [];
1190
+ if (!cssRegistry$3.some((e) => e.tagId === tagId$3)) cssRegistry$3.push({
1191
+ tagId: tagId$3,
1192
+ css: css$3
1193
+ });
1194
+ var export_module_css_default = {
1195
+ "exportGroup": "m5lYYW_exportGroup",
1196
+ "exportBtn": "m5lYYW_exportBtn"
1197
+ };
1198
+ //#endregion
1085
1199
  //#region src/client/tabs/DetailTab.tsx
1086
1200
  const RANGE_TABS = [
1087
1201
  ["today", "今日"],
@@ -1092,6 +1206,49 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1092
1206
  ];
1093
1207
  /** 图表最多铺开的列数:超过这个跨度的范围按天全画会挤成一团。 */
1094
1208
  const MAX_AXIS_DAYS = 366;
1209
+ /** 导出列:命中率与成本给数值(不带单位),方便在表格里继续算。 */
1210
+ const EXPORT_COLUMNS$3 = [
1211
+ {
1212
+ key: "date",
1213
+ label: "日期",
1214
+ value: (row) => row.date
1215
+ },
1216
+ {
1217
+ key: "calls",
1218
+ label: "调用",
1219
+ value: (row) => row.calls
1220
+ },
1221
+ {
1222
+ key: "inputTokens",
1223
+ label: "输入",
1224
+ value: (row) => row.inputTokens
1225
+ },
1226
+ {
1227
+ key: "outputTokens",
1228
+ label: "输出",
1229
+ value: (row) => row.outputTokens
1230
+ },
1231
+ {
1232
+ key: "cacheReadTokens",
1233
+ label: "缓存读",
1234
+ value: (row) => row.cacheReadTokens
1235
+ },
1236
+ {
1237
+ key: "cacheHitRate",
1238
+ label: "命中率(%)",
1239
+ value: (row) => hitRateValueOf(row)
1240
+ },
1241
+ {
1242
+ key: "cost",
1243
+ label: "成本(元)",
1244
+ value: (row) => row.cost
1245
+ }
1246
+ ];
1247
+ /** 命中率数值(导出用;界面上的带 % 文本由 `hitRateOf` 生成)。 */
1248
+ function hitRateValueOf(row) {
1249
+ const total = row.inputTokens + row.outputTokens + row.cacheReadTokens;
1250
+ return total > 0 ? Math.round(row.cacheReadTokens / total * 1e3) / 10 : null;
1251
+ }
1095
1252
  function hitRateOf(cacheReadTokens, inputTokens, outputTokens) {
1096
1253
  const total = inputTokens + outputTokens + cacheReadTokens;
1097
1254
  return total > 0 ? (cacheReadTokens / total * 100).toFixed(1) + "%" : "—";
@@ -1130,6 +1287,11 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1130
1287
  const costCell = costCellText(singleChannel, safeTotals.cost);
1131
1288
  const singleDay = bounds.from === bounds.to;
1132
1289
  const rangeDays = Math.min(daysBetween(bounds.from, bounds.to), MAX_AXIS_DAYS);
1290
+ /** 导出当前范围与通道筛选后的每日明细(按日期升序,不跟随表格排序)。 */
1291
+ const exportDaily = (format) => {
1292
+ if (format === "csv") downloadFile(exportFileName("daily", "csv"), toCsv(usage.days, EXPORT_COLUMNS$3), CSV_MIME);
1293
+ else downloadFile(exportFileName("daily", "json"), toJson(usage.days), JSON_MIME);
1294
+ };
1133
1295
  const activeProviders = /* @__PURE__ */ new Set();
1134
1296
  for (const day of usage.days) for (const model of day.models) if (model.inputTokens + model.outputTokens + model.cacheReadTokens > 0) activeProviders.add(model.provider);
1135
1297
  const hourlyRows = [];
@@ -1228,9 +1390,9 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1228
1390
  const chartRows = useHourAxis ? hourlyRows : dailyRows;
1229
1391
  const chartTitle = useHourAxis ? "按小时 Tokens(" + bounds.from + ")" : "按天 Tokens(" + rangeDays + " 天)";
1230
1392
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1231
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1393
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1232
1394
  className: detail_module_css_default.rangeBar,
1233
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1395
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1234
1396
  className: detail_module_css_default.rangeLeft,
1235
1397
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1236
1398
  className: detail_module_css_default.rangeTabs,
@@ -1257,7 +1419,20 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1257
1419
  onChange: (event) => props.onCustomTo(event.target.value)
1258
1420
  })
1259
1421
  ] })]
1260
- })
1422
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1423
+ className: export_module_css_default.exportGroup,
1424
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1425
+ className: export_module_css_default.exportBtn,
1426
+ disabled: usage.days.length === 0,
1427
+ onClick: () => exportDaily("csv"),
1428
+ children: "导出 CSV"
1429
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1430
+ className: export_module_css_default.exportBtn,
1431
+ disabled: usage.days.length === 0,
1432
+ onClick: () => exportDaily("json"),
1433
+ children: "导出 JSON"
1434
+ })]
1435
+ })]
1261
1436
  }),
1262
1437
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1263
1438
  className: shared_module_css_default.cardGrid3,
@@ -1389,21 +1564,329 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1389
1564
  css: css$2
1390
1565
  });
1391
1566
  var lists_module_css_default = {
1392
- "listBar": "QpFKma_listBar",
1393
- "filterBar": "QpFKma_filterBar",
1394
- "listSub": "QpFKma_listSub",
1395
- "rowArchived": "QpFKma_rowArchived",
1396
- "listMain": "QpFKma_listMain",
1397
- "sessionDay": "QpFKma_sessionDay",
1567
+ "filterSelect": "QpFKma_filterSelect",
1568
+ "dayCount": "QpFKma_dayCount",
1569
+ "searchInput": "QpFKma_searchInput",
1398
1570
  "archivedTag": "QpFKma_archivedTag",
1571
+ "listMain": "QpFKma_listMain",
1399
1572
  "listTitle": "QpFKma_listTitle",
1400
- "searchInput": "QpFKma_searchInput",
1401
- "filterSelect": "QpFKma_filterSelect",
1402
- "dayCount": "QpFKma_dayCount"
1573
+ "listBar": "QpFKma_listBar",
1574
+ "sessionDay": "QpFKma_sessionDay",
1575
+ "listSub": "QpFKma_listSub",
1576
+ "filterBar": "QpFKma_filterBar",
1577
+ "rowArchived": "QpFKma_rowArchived"
1403
1578
  };
1404
1579
  //#endregion
1580
+ //#region src/client/tabs/ModelsTab.tsx
1581
+ /** 按模型:`provider + model` 聚合的用量与估算成本(数据来自宿主会话事件折叠)。 */
1582
+ const MODELS_PATH = "/dsh-key-usage/models";
1583
+ /** 导出列:取原始数值(金额、命中率不带单位),方便直接进表格做计算。 */
1584
+ const EXPORT_COLUMNS$2 = [
1585
+ {
1586
+ key: "provider",
1587
+ label: "通道 id",
1588
+ value: (row) => row.provider
1589
+ },
1590
+ {
1591
+ key: "providerLabel",
1592
+ label: "通道",
1593
+ value: (row) => row.providerLabel
1594
+ },
1595
+ {
1596
+ key: "model",
1597
+ label: "模型",
1598
+ value: (row) => row.model
1599
+ },
1600
+ {
1601
+ key: "calls",
1602
+ label: "调用",
1603
+ value: (row) => row.calls
1604
+ },
1605
+ {
1606
+ key: "tokens",
1607
+ label: "Tokens",
1608
+ value: (row) => row.tokens
1609
+ },
1610
+ {
1611
+ key: "inputTokens",
1612
+ label: "输入",
1613
+ value: (row) => row.buckets.inputTokens
1614
+ },
1615
+ {
1616
+ key: "outputTokens",
1617
+ label: "输出",
1618
+ value: (row) => row.buckets.outputTokens
1619
+ },
1620
+ {
1621
+ key: "cacheReadTokens",
1622
+ label: "缓存读",
1623
+ value: (row) => row.buckets.cacheReadTokens
1624
+ },
1625
+ {
1626
+ key: "cacheHitRate",
1627
+ label: "命中率(%)",
1628
+ value: (row) => row.cacheHitRate
1629
+ },
1630
+ {
1631
+ key: "cost",
1632
+ label: "成本(估算,元)",
1633
+ value: (row) => row.cost
1634
+ },
1635
+ {
1636
+ key: "costMode",
1637
+ label: "计费模式",
1638
+ value: (row) => row.costMode
1639
+ },
1640
+ {
1641
+ key: "sessionCount",
1642
+ label: "会话数",
1643
+ value: (row) => row.sessionCount
1644
+ },
1645
+ {
1646
+ key: "lastTime",
1647
+ label: "最近活跃",
1648
+ value: (row) => fmtDateTime(row.lastTime)
1649
+ }
1650
+ ];
1651
+ function ModelsTab(props) {
1652
+ const [query, setQuery] = (0, react.useState)("");
1653
+ const [channelFilter, setChannelFilter] = (0, react.useState)("ALL");
1654
+ const [sortKey, setSortKey] = (0, react.useState)("tokensDesc");
1655
+ const [nowMs] = (0, react.useState)(() => Date.now());
1656
+ const rows = props.data?.rows ?? [];
1657
+ const visible = (0, react.useMemo)(() => {
1658
+ const keyword = query.trim().toLowerCase();
1659
+ const filtered = dropEmptyRows(rows, (row) => row.tokens).rows.filter((row) => {
1660
+ const haystack = (row.model + " " + row.provider + " " + row.providerLabel).toLowerCase();
1661
+ const matchesQuery = keyword === "" || haystack.includes(keyword);
1662
+ const matchesChannel = channelFilter === "ALL" || row.provider === channelFilter;
1663
+ return matchesQuery && matchesChannel;
1664
+ });
1665
+ const sorters = {
1666
+ tokensDesc: (first, second) => second.tokens - first.tokens,
1667
+ callsDesc: (first, second) => second.calls - first.calls,
1668
+ costDesc: (first, second) => (second.cost ?? 0) - (first.cost ?? 0),
1669
+ recent: (first, second) => (second.lastTime ?? 0) - (first.lastTime ?? 0)
1670
+ };
1671
+ return [...filtered].sort(sorters[sortKey]);
1672
+ }, [
1673
+ rows,
1674
+ query,
1675
+ channelFilter,
1676
+ sortKey
1677
+ ]);
1678
+ const totalTokens = rows.reduce((total, row) => total + row.tokens, 0);
1679
+ const hiddenEmpty = dropEmptyRows(rows, (row) => row.tokens).hidden;
1680
+ /** 导出当前筛选与排序后的行(JSON 取整行,CSV 按固定列映射)。 */
1681
+ const exportRows = (format) => {
1682
+ if (format === "csv") downloadFile(exportFileName("models", "csv"), toCsv(visible, EXPORT_COLUMNS$2), CSV_MIME);
1683
+ else downloadFile(exportFileName("models", "json"), toJson(visible), JSON_MIME);
1684
+ };
1685
+ if (props.data === null) {
1686
+ const reason = failureReasonOf(MODELS_PATH);
1687
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1688
+ className: shared_module_css_default.empty,
1689
+ children: reason === null ? "按模型数据读取中…" : "按模型数据不可用:" + reason
1690
+ });
1691
+ }
1692
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1693
+ props.data.coverage.status !== "complete" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1694
+ className: shared_module_css_default.warning,
1695
+ children: coverageText$1(props.data.coverage.status, props.data.coverage.skippedSessions)
1696
+ }),
1697
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1698
+ className: lists_module_css_default.filterBar,
1699
+ children: [
1700
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1701
+ className: lists_module_css_default.searchInput,
1702
+ placeholder: "搜索模型名、通道…",
1703
+ value: query,
1704
+ onChange: (event) => setQuery(event.target.value)
1705
+ }),
1706
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1707
+ className: lists_module_css_default.filterSelect,
1708
+ value: channelFilter,
1709
+ onChange: (event) => setChannelFilter(event.target.value),
1710
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1711
+ value: "ALL",
1712
+ children: "全部通道"
1713
+ }), props.channels.map((channel) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1714
+ value: channel.provider,
1715
+ children: channel.displayName
1716
+ }, channel.provider))]
1717
+ }),
1718
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1719
+ className: lists_module_css_default.filterSelect,
1720
+ value: sortKey,
1721
+ onChange: (event) => setSortKey(event.target.value),
1722
+ children: [
1723
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1724
+ value: "tokensDesc",
1725
+ children: "Tokens 最多"
1726
+ }),
1727
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1728
+ value: "callsDesc",
1729
+ children: "调用最多"
1730
+ }),
1731
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1732
+ value: "costDesc",
1733
+ children: "成本最高"
1734
+ }),
1735
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1736
+ value: "recent",
1737
+ children: "最近活跃"
1738
+ })
1739
+ ]
1740
+ }),
1741
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1742
+ className: export_module_css_default.exportGroup,
1743
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1744
+ className: export_module_css_default.exportBtn,
1745
+ disabled: visible.length === 0,
1746
+ onClick: () => exportRows("csv"),
1747
+ children: "导出 CSV"
1748
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1749
+ className: export_module_css_default.exportBtn,
1750
+ disabled: visible.length === 0,
1751
+ onClick: () => exportRows("json"),
1752
+ children: "导出 JSON"
1753
+ })]
1754
+ })
1755
+ ]
1756
+ }),
1757
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
1758
+ className: shared_module_css_default.table,
1759
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
1760
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "模型" }),
1761
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "调用" }),
1762
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "Tokens" }),
1763
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "占比" }),
1764
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "命中率" }),
1765
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "成本" }),
1766
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "最近活跃" })
1767
+ ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tbody", { children: [visible.map((row) => {
1768
+ const percent = totalTokens > 0 ? Math.round(row.tokens / totalTokens * 100) : 0;
1769
+ const channel = channelOf(props.channels, row.provider);
1770
+ const costCell = costCellText(channel, row.cost ?? 0);
1771
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
1772
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1773
+ className: lists_module_css_default.listTitle,
1774
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1775
+ className: lists_module_css_default.listMain,
1776
+ children: row.model
1777
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1778
+ className: lists_module_css_default.listSub,
1779
+ children: [
1780
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1781
+ className: shared_module_css_default.statusDot,
1782
+ style: {
1783
+ background: channelColorOf(props.channels, row.provider),
1784
+ marginRight: 4
1785
+ }
1786
+ }),
1787
+ row.providerLabel,
1788
+ " · ",
1789
+ row.sessionCount,
1790
+ " 个会话"
1791
+ ]
1792
+ })] })
1793
+ }) }),
1794
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtCalls(row.calls) }),
1795
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtTokens(row.tokens) }),
1796
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [
1797
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1798
+ className: lists_module_css_default.listBar,
1799
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { style: {
1800
+ width: Math.max(2, percent) + "%",
1801
+ background: channelColorOf(props.channels, row.provider)
1802
+ } })
1803
+ }),
1804
+ " ",
1805
+ percent,
1806
+ "%"
1807
+ ] }),
1808
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: row.cacheHitRate === null ? "—" : row.cacheHitRate.toFixed(1) + "%" }),
1809
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: row.cost === null || channel?.costMode !== "perToken" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1810
+ className: shared_module_css_default.dim,
1811
+ children: costCell.text
1812
+ }) : costCell.text }),
1813
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtRelative(row.lastTime, nowMs) })
1814
+ ] }, row.provider + "|" + row.model);
1815
+ }), visible.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1816
+ colSpan: 7,
1817
+ className: shared_module_css_default.empty,
1818
+ children: "没有符合条件的模型"
1819
+ }) })] })]
1820
+ }),
1821
+ hiddenEmpty > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1822
+ className: shared_module_css_default.statExtra,
1823
+ children: hiddenEmptyRowsText(hiddenEmpty)
1824
+ }),
1825
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1826
+ className: shared_module_css_default.statExtra,
1827
+ children: [
1828
+ "模型用量按宿主会话事件折叠(每个结算样本计一次调用);成本按台账模型均价估算",
1829
+ props.data.priced ? "" : "(台账无成本数据,暂不可估)",
1830
+ ";命中率 = 缓存读 ÷(输入 + 输出 + 缓存读)"
1831
+ ]
1832
+ })
1833
+ ] });
1834
+ }
1835
+ function coverageText$1(status, skipped) {
1836
+ if (status === "unavailable") return "会话数据不可用(宿主未提供会话服务或全部读取失败)";
1837
+ return "会话数据不完整:" + String(skipped) + " 个会话读取失败,未计入统计";
1838
+ }
1839
+ //#endregion
1405
1840
  //#region src/client/tabs/ProjectsTab.tsx
1406
1841
  /** 项目用量:按会话工作目录聚合(数据来自宿主会话事件折叠)。 */
1842
+ /** 导出列:成本与命中率都给原始数值,方便在表格里继续算。 */
1843
+ const EXPORT_COLUMNS$1 = [
1844
+ {
1845
+ key: "workspace",
1846
+ label: "项目路径",
1847
+ value: (row) => row.workspace
1848
+ },
1849
+ {
1850
+ key: "dominantProvider",
1851
+ label: "主要通道",
1852
+ value: (row) => row.dominantProvider
1853
+ },
1854
+ {
1855
+ key: "calls",
1856
+ label: "调用",
1857
+ value: (row) => row.calls
1858
+ },
1859
+ {
1860
+ key: "tokens",
1861
+ label: "Tokens",
1862
+ value: (row) => row.tokens
1863
+ },
1864
+ {
1865
+ key: "cacheHitRate",
1866
+ label: "命中率(%)",
1867
+ value: (row) => row.cacheHitRate
1868
+ },
1869
+ {
1870
+ key: "cost",
1871
+ label: "成本(估算,元)",
1872
+ value: (row) => row.cost
1873
+ },
1874
+ {
1875
+ key: "sessionCount",
1876
+ label: "会话数",
1877
+ value: (row) => row.sessionCount
1878
+ },
1879
+ {
1880
+ key: "archivedSessionCount",
1881
+ label: "已归档会话数",
1882
+ value: (row) => row.archivedSessionCount
1883
+ },
1884
+ {
1885
+ key: "lastTime",
1886
+ label: "最近活跃",
1887
+ value: (row) => fmtDateTime(row.lastTime)
1888
+ }
1889
+ ];
1407
1890
  function ProjectsTab(props) {
1408
1891
  const [query, setQuery] = (0, react.useState)("");
1409
1892
  const [channelFilter, setChannelFilter] = (0, react.useState)("ALL");
@@ -1412,7 +1895,7 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1412
1895
  const rows = props.data?.rows ?? [];
1413
1896
  const visible = (0, react.useMemo)(() => {
1414
1897
  const keyword = query.trim().toLowerCase();
1415
- const filtered = rows.filter((row) => {
1898
+ const filtered = dropEmptyRows(rows, (row) => row.tokens).rows.filter((row) => {
1416
1899
  const matchesQuery = keyword === "" || row.workspace.toLowerCase().includes(keyword);
1417
1900
  const matchesChannel = channelFilter === "ALL" || row.providers[channelFilter] !== void 0;
1418
1901
  return matchesQuery && matchesChannel;
@@ -1431,6 +1914,12 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1431
1914
  sortKey
1432
1915
  ]);
1433
1916
  const totalTokens = rows.reduce((total, row) => total + row.tokens, 0);
1917
+ const hiddenEmpty = dropEmptyRows(rows, (row) => row.tokens).hidden;
1918
+ /** 导出当前筛选与排序后的项目行(JSON 取整行,CSV 按固定列映射)。 */
1919
+ const exportRows = (format) => {
1920
+ if (format === "csv") downloadFile(exportFileName("projects", "csv"), toCsv(visible, EXPORT_COLUMNS$1), CSV_MIME);
1921
+ else downloadFile(exportFileName("projects", "json"), toJson(visible), JSON_MIME);
1922
+ };
1434
1923
  if (props.data === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1435
1924
  className: shared_module_css_default.empty,
1436
1925
  children: "会话数据读取中或不可用…"
@@ -1483,6 +1972,20 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1483
1972
  children: "调用最多"
1484
1973
  })
1485
1974
  ]
1975
+ }),
1976
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1977
+ className: export_module_css_default.exportGroup,
1978
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1979
+ className: export_module_css_default.exportBtn,
1980
+ disabled: visible.length === 0,
1981
+ onClick: () => exportRows("csv"),
1982
+ children: "导出 CSV"
1983
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1984
+ className: export_module_css_default.exportBtn,
1985
+ disabled: visible.length === 0,
1986
+ onClick: () => exportRows("json"),
1987
+ children: "导出 JSON"
1988
+ })]
1486
1989
  })
1487
1990
  ]
1488
1991
  }),
@@ -1543,6 +2046,10 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1543
2046
  children: "没有符合条件的项目"
1544
2047
  }) })] })]
1545
2048
  }),
2049
+ hiddenEmpty > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2050
+ className: shared_module_css_default.statExtra,
2051
+ children: hiddenEmptyRowsText(hiddenEmpty)
2052
+ }),
1546
2053
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1547
2054
  className: shared_module_css_default.statExtra,
1548
2055
  children: [
@@ -1578,26 +2085,26 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1578
2085
  css: css$1
1579
2086
  });
1580
2087
  var quota_module_css_default = {
1581
- "quotaList": "F7iUHa_quotaList",
1582
- "quotaProvider": "F7iUHa_quotaProvider",
1583
- "windowReset": "F7iUHa_windowReset",
1584
- "quotaEnv": "F7iUHa_quotaEnv",
1585
- "windowName": "F7iUHa_windowName",
1586
- "liveChip": "F7iUHa_liveChip",
1587
- "windowItem": "F7iUHa_windowItem",
2088
+ "windowBar": "F7iUHa_windowBar",
1588
2089
  "windowRow": "F7iUHa_windowRow",
1589
- "snapshotHint": "F7iUHa_snapshotHint",
2090
+ "liveChip": "F7iUHa_liveChip",
1590
2091
  "bsLabel": "F7iUHa_bsLabel",
2092
+ "quotaHead": "F7iUHa_quotaHead",
2093
+ "quotaTitle": "F7iUHa_quotaTitle",
1591
2094
  "snapshotStale": "F7iUHa_snapshotStale",
1592
- "balanceStrip": "F7iUHa_balanceStrip",
2095
+ "windowItem": "F7iUHa_windowItem",
1593
2096
  "windowValue": "F7iUHa_windowValue",
1594
- "quotaHead": "F7iUHa_quotaHead",
1595
- "balanceItem": "F7iUHa_balanceItem",
1596
- "quotaCard": "F7iUHa_quotaCard",
2097
+ "windowReset": "F7iUHa_windowReset",
2098
+ "snapshotHint": "F7iUHa_snapshotHint",
2099
+ "quotaProvider": "F7iUHa_quotaProvider",
1597
2100
  "windowHead": "F7iUHa_windowHead",
1598
- "windowBar": "F7iUHa_windowBar",
1599
- "quotaTitle": "F7iUHa_quotaTitle",
1600
- "bsValue": "F7iUHa_bsValue"
2101
+ "quotaList": "F7iUHa_quotaList",
2102
+ "balanceItem": "F7iUHa_balanceItem",
2103
+ "balanceStrip": "F7iUHa_balanceStrip",
2104
+ "windowName": "F7iUHa_windowName",
2105
+ "bsValue": "F7iUHa_bsValue",
2106
+ "quotaEnv": "F7iUHa_quotaEnv",
2107
+ "quotaCard": "F7iUHa_quotaCard"
1601
2108
  };
1602
2109
  //#endregion
1603
2110
  //#region src/client/tabs/QuotaTab.tsx
@@ -1731,13 +2238,13 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1731
2238
  "(来自官方接口,最多缓存 1 分钟)"
1732
2239
  ]
1733
2240
  }),
1734
- snapshot !== null && !isLive && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2241
+ snapshot !== null && !isLive && snapshot.supported && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1735
2242
  className: staleDaysOf(snapshot.updatedAt) >= 1 ? quota_module_css_default.snapshotStale : quota_module_css_default.snapshotHint,
1736
2243
  children: [
1737
2244
  "实时查询未成功(",
1738
2245
  errorTextOf(snapshot.error),
1739
- "),显示本机快照",
1740
- snapshot.updatedAt === null ? "" : " · 快照时间 " + fmtDateTime(snapshot.updatedAt) + (staleDaysOf(snapshot.updatedAt) >= 1 ? ",已 " + staleDaysOf(snapshot.updatedAt) + " 天未更新" : "")
2246
+ ")",
2247
+ snapshot.updatedAt === null ? ",且暂无可用快照 —— 点右上角「刷新」重试" : ",显示快照 · 快照时间 " + fmtDateTime(snapshot.updatedAt) + (staleDaysOf(snapshot.updatedAt) >= 1 ? ",已 " + staleDaysOf(snapshot.updatedAt) + " 天未更新" : "")
1741
2248
  ]
1742
2249
  })
1743
2250
  ]
@@ -1804,6 +2311,64 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1804
2311
  //#endregion
1805
2312
  //#region src/client/tabs/SessionsTab.tsx
1806
2313
  /** 会话用量:按会话列出 tokens / 命中率 / 时长,按最近活跃时按日分组。 */
2314
+ /** 导出列:标题可能带逗号或换行,由 toCsv 统一转义。 */
2315
+ const EXPORT_COLUMNS = [
2316
+ {
2317
+ key: "id",
2318
+ label: "会话 id",
2319
+ value: (row) => row.id
2320
+ },
2321
+ {
2322
+ key: "title",
2323
+ label: "标题",
2324
+ value: (row) => row.title
2325
+ },
2326
+ {
2327
+ key: "workspace",
2328
+ label: "工作区",
2329
+ value: (row) => row.workspace
2330
+ },
2331
+ {
2332
+ key: "provider",
2333
+ label: "通道",
2334
+ value: (row) => row.provider
2335
+ },
2336
+ {
2337
+ key: "firstTime",
2338
+ label: "首次时间",
2339
+ value: (row) => fmtDateTime(row.firstTime)
2340
+ },
2341
+ {
2342
+ key: "lastTime",
2343
+ label: "最近时间",
2344
+ value: (row) => fmtDateTime(row.lastTime)
2345
+ },
2346
+ {
2347
+ key: "calls",
2348
+ label: "调用",
2349
+ value: (row) => row.calls
2350
+ },
2351
+ {
2352
+ key: "tokens",
2353
+ label: "Tokens",
2354
+ value: (row) => row.tokens
2355
+ },
2356
+ {
2357
+ key: "cacheHitRate",
2358
+ label: "命中率(%)",
2359
+ value: (row) => row.cacheHitRate
2360
+ },
2361
+ {
2362
+ key: "cost",
2363
+ label: "成本(估算,元)",
2364
+ value: (row) => row.cost
2365
+ },
2366
+ {
2367
+ key: "archived",
2368
+ label: "已归档",
2369
+ value: (row) => row.archived ? "是" : "否"
2370
+ }
2371
+ ];
1807
2372
  function SessionsTab(props) {
1808
2373
  const [query, setQuery] = (0, react.useState)("");
1809
2374
  const [channelFilter, setChannelFilter] = (0, react.useState)("ALL");
@@ -1813,10 +2378,11 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1813
2378
  const [nowMs] = (0, react.useState)(() => Date.now());
1814
2379
  const rows = props.data?.rows ?? [];
1815
2380
  const archivedCount = rows.filter((row) => row.archived).length;
2381
+ const hiddenEmpty = dropEmptyRows(rows, (row) => row.tokens).hidden;
1816
2382
  const visible = (0, react.useMemo)(() => {
1817
2383
  const keyword = query.trim().toLowerCase();
1818
2384
  const maxAgeDays = Number(daysFilter);
1819
- const filtered = rows.filter((row) => {
2385
+ const filtered = dropEmptyRows(rows, (row) => row.tokens).rows.filter((row) => {
1820
2386
  const title = (row.title ?? "") + " " + row.id + " " + (row.workspace ?? "");
1821
2387
  const matchesQuery = keyword === "" || title.toLowerCase().includes(keyword);
1822
2388
  const matchesChannel = channelFilter === "ALL" || row.provider === channelFilter;
@@ -1840,6 +2406,11 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1840
2406
  nowMs,
1841
2407
  archiveFilter
1842
2408
  ]);
2409
+ /** 只导当前筛选后的会话行(归档筛选、时间窗口、搜索都算数)。 */
2410
+ const exportRows = (format) => {
2411
+ if (format === "csv") downloadFile(exportFileName("sessions", "csv"), toCsv(visible, EXPORT_COLUMNS), CSV_MIME);
2412
+ else downloadFile(exportFileName("sessions", "json"), toJson(visible), JSON_MIME);
2413
+ };
1843
2414
  if (props.data === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1844
2415
  className: shared_module_css_default.empty,
1845
2416
  children: "会话数据读取中或不可用…"
@@ -1941,6 +2512,20 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1941
2512
  children: "仅显示已归档"
1942
2513
  })
1943
2514
  ]
2515
+ }),
2516
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2517
+ className: export_module_css_default.exportGroup,
2518
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2519
+ className: export_module_css_default.exportBtn,
2520
+ disabled: visible.length === 0,
2521
+ onClick: () => exportRows("csv"),
2522
+ children: "导出 CSV"
2523
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2524
+ className: export_module_css_default.exportBtn,
2525
+ disabled: visible.length === 0,
2526
+ onClick: () => exportRows("json"),
2527
+ children: "导出 JSON"
2528
+ })]
1944
2529
  })
1945
2530
  ]
1946
2531
  }),
@@ -1987,6 +2572,10 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1987
2572
  }) })
1988
2573
  ] })]
1989
2574
  }),
2575
+ hiddenEmpty > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2576
+ className: shared_module_css_default.statExtra,
2577
+ children: hiddenEmptyRowsText(hiddenEmpty)
2578
+ }),
1990
2579
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1991
2580
  className: shared_module_css_default.statExtra,
1992
2581
  children: ["会话用量按宿主会话事件折叠(每个结算样本计一次调用);成本按台账模型均价估算", props.data.priced ? "" : "(台账无成本数据,暂不可估)"]
@@ -2049,6 +2638,7 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
2049
2638
  ["overview", "总览"],
2050
2639
  ["detail", "使用明细"],
2051
2640
  ["projects", "项目用量"],
2641
+ ["models", "按模型"],
2052
2642
  ["sessions", "会话用量"],
2053
2643
  ["quota", "余额与配额"]
2054
2644
  ];
@@ -2070,6 +2660,7 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
2070
2660
  const [usage, setUsage] = (0, react.useState)(null);
2071
2661
  const [sessions, setSessions] = (0, react.useState)(null);
2072
2662
  const [projects, setProjects] = (0, react.useState)(null);
2663
+ const [models, setModels] = (0, react.useState)(null);
2073
2664
  const [available, setAvailable] = (0, react.useState)(null);
2074
2665
  const [loading, setLoading] = (0, react.useState)(false);
2075
2666
  const tooltip = useTooltip();
@@ -2086,9 +2677,14 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
2086
2677
  setSummary(summaryResult);
2087
2678
  setHeatmap(heatmapResult ?? []);
2088
2679
  if (includeSessionData) {
2089
- const [sessionResult, projectResult] = await Promise.all([loadSessions(), loadProjects()]);
2680
+ const [sessionResult, projectResult, modelResult] = await Promise.all([
2681
+ loadSessions(),
2682
+ loadProjects(),
2683
+ loadModels()
2684
+ ]);
2090
2685
  setSessions(sessionResult);
2091
2686
  setProjects(projectResult);
2687
+ setModels(modelResult);
2092
2688
  }
2093
2689
  setLoading(false);
2094
2690
  }, []);
@@ -2098,10 +2694,12 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
2098
2694
  (0, react.useEffect)(() => {
2099
2695
  if (activeTab === "sessions" && sessions === null) loadSessions().then(setSessions);
2100
2696
  if (activeTab === "projects" && projects === null) loadProjects().then(setProjects);
2697
+ if (activeTab === "models" && models === null) loadModels().then(setModels);
2101
2698
  }, [
2102
2699
  activeTab,
2103
2700
  sessions,
2104
- projects
2701
+ projects,
2702
+ models
2105
2703
  ]);
2106
2704
  const bounds = (0, react.useMemo)(() => {
2107
2705
  const today = isoDate(/* @__PURE__ */ new Date());
@@ -2150,7 +2748,7 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
2150
2748
  channelFilter
2151
2749
  ]);
2152
2750
  const refresh = (0, react.useCallback)(() => {
2153
- reload(activeTab === "sessions" || activeTab === "projects" || sessions !== null || projects !== null, true);
2751
+ reload(activeTab === "sessions" || activeTab === "projects" || activeTab === "models" || sessions !== null || projects !== null || models !== null, true);
2154
2752
  loadUsage(bounds.from, bounds.to, channelFilter).then((result) => {
2155
2753
  if (result !== null) setUsage(result);
2156
2754
  });
@@ -2159,6 +2757,7 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
2159
2757
  activeTab,
2160
2758
  sessions,
2161
2759
  projects,
2760
+ models,
2162
2761
  bounds.from,
2163
2762
  bounds.to,
2164
2763
  channelFilter
@@ -2222,6 +2821,10 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
2222
2821
  data: projects,
2223
2822
  channels
2224
2823
  }),
2824
+ activeTab === "models" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelsTab, {
2825
+ data: models,
2826
+ channels
2827
+ }),
2225
2828
  activeTab === "sessions" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionsTab, {
2226
2829
  data: sessions,
2227
2830
  channels
@@ -2241,7 +2844,7 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
2241
2844
  "。两套来源按天取舍、不相加。"
2242
2845
  ] }),
2243
2846
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "热力图按可见区间内活跃日的三分位分档(少 / 中 / 高)。" }),
2244
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "项目用量、会话用量与单日「按小时」均来自会话事件,每个结算样本计一次调用(不计成本)。" }),
2847
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "项目用量、会话用量、按模型与单日「按小时」均来自会话事件,每个结算样本计一次调用(不计成本)。" }),
2245
2848
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "已归档会话只是标记:用量照常计入,会话列表默认隐藏、可切换显示(显示时淡化并标注)。" }),
2246
2849
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "命中率 = 缓存读 ÷(输入 + 输出 + 缓存读)。" }),
2247
2850
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "余额与订阅窗口实时查询官方接口(失败回落本机快照),原样展示,不做换算与建议。" })