@ds-hu/dsh-token-quota 2.4.1 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // 展示控制器统一请求与会话订阅;面板草稿独立于展示缓存与本地偏好。
3
3
  (function () {
4
4
  window.__ModuleLoader__.load({
5
- id: "dsh-token-quota",
5
+ id: "@ds-hu/dsh-token-quota",
6
6
  factory: (require) => {
7
7
  var module = { exports: {} };
8
8
  var exports = module.exports;
@@ -98,7 +98,17 @@
98
98
  tone: supplier.state === "err" || supplier.enabled === false || status === "unknown" ? "neutral" : status,
99
99
  warning: supplier.state !== "err" && supplier.enabled !== false && ["warn", "crit"].includes(status) ? tr(status === "crit" ? "stateCrit" : "stateWarn") : null };
100
100
  };
101
- const selectPrimaryMetric = (supplier, tr = (key) => key) => {
101
+ /** 仅计量供应商:只有本地 token,没有任何可查询的额度。 */
102
+ const isMeteredSupplier = (supplier) => supplier?.queryable === false;
103
+ /** 仅计量行「近 N 天」的合计:与图表同一份 usageDays,不另算口径。 */
104
+ const weekTokensOf = (supplier) => (Array.isArray(supplier?.usageDays)
105
+ ? supplier.usageDays.reduce((sum, day) => sum + (Number(day?.total) || 0), 0) : 0);
106
+ const selectPrimaryMetric = (supplier, tr = (key) => key, windowDays = 7) => {
107
+ // 仅计量行没有指标可选:主指标就是它自己的量。今日在行尾另给一次是既有版式。
108
+ if (isMeteredSupplier(supplier)) {
109
+ const text = tr("windowTokens", { d: windowDays, n: fmtBig(weekTokensOf(supplier)) });
110
+ return { kind: "usage", value: text, label: "", name: "", reset: null, tone: "neutral", more: 0, text };
111
+ }
102
112
  const entries = supplier?.entries || [];
103
113
  const entry = tightestOf(entries) || entries.find(e => e.kind === "bal") || entries.find(e => e.kind === "cost") || entries[0];
104
114
  if (!entry) return { kind: "unknown", value: "—", label: "", name: "", reset: null, tone: "neutral", more: 0, text: "—" };
@@ -134,10 +144,12 @@
134
144
  columns: [{ key: "name", labelKey: "colMetric" }, ...METRIC_COLUMNS[kind]], rows,
135
145
  }));
136
146
  };
137
- const supplierStatus = (s, tr) => s?.state === "err"
147
+ const supplierStatus = (s, tr) => isMeteredSupplier(s) ? tr("meteredOnly") : s?.state === "err"
138
148
  ? tr(s.entries?.length ? "staleFailure" : "unavailable")
139
149
  : s?.enabled === false ? tr("disabled") : !s?.entries?.length ? tr("notQueried") : tr(stateToken(s));
140
- const todayTotal = (state) => (state?.suppliers || []).filter(s => s.current)
150
+ // 今日总量 = 列表里各行的和(口径可对账):只要这一行有 token,它就在列表里、也在总量里。
151
+ // 旧口径只加 current 行,于是「未配置但在跑」的供应商 token 会既看不见也不算数。
152
+ const todayTotal = (state) => (state?.suppliers || []).filter(s => s.added !== false)
141
153
  .reduce((sum, s) => sum + (typeof s.todayTokens === "number" ? s.todayTokens : 0), 0);
142
154
 
143
155
  // ---------- 今日 Token 分布(数据来自宿主 todayUsage / usageDays,五分量互不相交) ----------
@@ -716,6 +728,8 @@
716
728
  enabled: !!sup.enabled, baseUrl: sup.baseUrl || sup.baseUrlDefault || "",
717
729
  warnPct: sup.warnPct ?? 80, critPct: sup.critPct ?? 95,
718
730
  apiKeyEnv: sup.apiKeyEnv || "",
731
+ // 仅计量行只有一个可改的东西:显示名(存进 config.meteredNames[route])。
732
+ ...(isMeteredSupplier(sup) ? { name: sup.name || "" } : {}),
719
733
  // 关注限额:有序数组;空数组 = 未配置(卡片走默认规则)。草稿复制一份,避免直接改 state。
720
734
  displayKeys: Array.isArray(sup.displayKeys) ? [...sup.displayKeys] : [],
721
735
  ...Object.fromEntries((sup.meta?.needs || []).map((need) => [need.key, need.secret ? "" : (sup[need.key] ?? "")])),
@@ -751,9 +765,15 @@
751
765
  const compact = narrow || preference === "compact";
752
766
  const active = state?.active;
753
767
  const supplier = active && state?.suppliers?.find(s => s.id === active.supplierId);
754
- const metric = selectPrimaryMetric(supplier, t);
768
+ // 「近 N 天」的 N 来自保留期(retentionDays < 7 时窗口确实更短),不是写死的 7。
769
+ const windowDays = state?.poll?.usageHistoryDays || 7;
770
+ const metric = selectPrimaryMetric(supplier, t, windowDays);
755
771
  const name = active?.name || t("noneActive");
756
772
  const todayCount = typeof supplier?.todayTokens === "number" ? supplier.todayTokens : 0;
773
+ // 仅计量行没有条也没有百分比:第 2 行直接给这个窗口里它自己的量(今日 chip 已在第 1 行)。
774
+ const metricText = isMeteredSupplier(supplier)
775
+ ? t("windowTokens", { d: windowDays, n: fmtBig(weekTokensOf(supplier)) })
776
+ : metric.text;
757
777
  const bars = selectBarMetrics(supplier, t);
758
778
  const resetText = bars.length ? bars.map(bar => bar.reset).filter(Boolean).join(" · ") : metric.reset;
759
779
  const failure = loadError ? t(state ? "staleFailure" : "unavailable") : supplier?.state === "err" ? supplierStatus(supplier, t) : "";
@@ -766,7 +786,7 @@
766
786
  seasonUsesBeijingNote() ? t("seasonBeijingOnly") : null].filter(Boolean).join(" · ")
767
787
  : "";
768
788
  // 卡片上的可见文字只留数据;完整状态(今日口径、连接、峰谷成因)进 aria/title。
769
- const title = [name, metric.text, resetText, seasonTitle, failure, t("todayScope"), t("today", { n: state ? fmtBig(todayTotal(state)) : "—" }), connText].filter(Boolean).join(" · ");
789
+ const title = [name, metricText, resetText, seasonTitle, failure, t("todayScope"), t("today", { n: state ? fmtBig(todayTotal(state)) : "—" }), connText].filter(Boolean).join(" · ");
770
790
  return h(React.Fragment, null,
771
791
  !wide ? h("button", { type: "button", ref: entryRef, "data-qm-entry": "", className: "qm-rail", "aria-label": t("title"), "aria-haspopup": "dialog", "aria-expanded": !!panel.host, title, onClick: activateEntry }, "◉") :
772
792
  h("div", { "data-qm-linerow": "", "data-qm-density": compact ? "compact" : "expanded", ref: widthRef, className: "qm-footer" },
@@ -791,7 +811,7 @@
791
811
  h("i", { className: `qm-bar-fill ${bar.tone}`, style: { width: `${Math.max(0, Math.min(100, bar.entry.pct))}%` } })),
792
812
  h("span", { key: `p${i}`, className: "qm-bar-pct" }, pctText(bar.entry.pct)),
793
813
  bar.reset ? h("span", { key: `r${i}`, className: "qm-bar-reset" }, bar.reset) : null,
794
- ])) : h("div", { className: "qm-l2" }, h("span", { className: "qm-primary", title: metric.text }, metric.text)),
814
+ ])) : h("div", { className: "qm-l2" }, h("span", { className: "qm-primary", title: metricText }, metricText)),
795
815
  season ? h("div", { className: `qm-season ${season.peak ? "peak" : "valley"}`, "data-qm-season": season.tier, title: seasonTitle },
796
816
  h("span", { className: `qm-season-dot ${season.peak ? "peak" : "valley"}` }),
797
817
  h("b", null, seasonTierOf(season, t)),
@@ -825,9 +845,10 @@
825
845
  const { state, loadError, refresh, refreshing, sessionKey } = useQuotaState();
826
846
  const snap = useSurface();
827
847
  const [expandedId, setExpandedId] = React.useState(null);
828
- // 没有本地用量行的供应商不画图,也就不必为它取额度历史。
848
+ // 没有本地用量行的供应商不画图,也就不必为它取额度历史;仅计量行从不查询,同样不必取。
829
849
  const expandedSupplier = (state?.suppliers || []).find((sup) => sup.id === expandedId);
830
- const quota = useQuotaHistory(sessionKey, Array.isArray(expandedSupplier?.usageDays) && expandedSupplier.usageDays.length > 0);
850
+ const quota = useQuotaHistory(sessionKey, expandedSupplier?.queryable !== false
851
+ && Array.isArray(expandedSupplier?.usageDays) && expandedSupplier.usageDays.length > 0);
831
852
  const dialog = host === "dialog";
832
853
  // Escape / 外部点击必须走 controller 的未保存确认;useDialog 只拿到这个稳定转发器。
833
854
  const closeRef = React.useRef(() => {});
@@ -1039,7 +1060,8 @@
1039
1060
  const sup = (state?.suppliers || []).find((s) => s.id === supplierId);
1040
1061
  if (!sup) return;
1041
1062
  const warn = Number(form.warnPct), crit = Number(form.critPct);
1042
- if (!Number.isFinite(warn) || !Number.isFinite(crit) || warn < 1 || warn >= crit || crit > 100) {
1063
+ if (!isMeteredSupplier(sup)
1064
+ && (!Number.isFinite(warn) || !Number.isFinite(crit) || warn < 1 || warn >= crit || crit > 100)) {
1043
1065
  setSaveError(t("invalidThreshold")); setAdvanced(true);
1044
1066
  requestAnimationFrame(() => advancedRef.current?.querySelector('input[type="number"]')?.focus()); return;
1045
1067
  }
@@ -1051,8 +1073,10 @@
1051
1073
  opBusyRef.current = true;
1052
1074
  setOpBusy(true);
1053
1075
  try {
1054
- const p = supplierPatch(sup, form);
1055
- await post(`${API}/settings`, { suppliers: { [supplierId]: p } });
1076
+ // 仅计量行不改任何查询参数:只有名字,写进 config.meteredNames[route]。
1077
+ await post(`${API}/settings`, isMeteredSupplier(sup)
1078
+ ? { meteredNames: { [sup.route]: String(form.name || "").trim() } }
1079
+ : { suppliers: { [supplierId]: supplierPatch(sup, form) } });
1056
1080
  await reload();
1057
1081
  outcome = { ok: true };
1058
1082
  if (!latest()) return;
@@ -1164,21 +1188,34 @@
1164
1188
  shown.length
1165
1189
  ? h("div", { className: "qm-rows" }, shown.map(sup => h(SupplierRow, {
1166
1190
  key: sup.id, sup, t, season: state.season, quota: quotaOf(sup.id),
1191
+ windowDays: state.poll?.usageHistoryDays || 7,
1167
1192
  expanded: expandedId === sup.id, onToggle: () => onExpand(expandedId === sup.id ? null : sup.id), onEdit })))
1168
1193
  : h(Empty, { text: t(filter === "attention" ? "noAttention" : "overviewEmpty"), action: filter === "all" ? t("manageSuppliers") : null, onAction: onManage }),
1169
1194
  h(HistoryTable, { rows: (state.history || []).slice(0, 50), t }));
1170
1195
  }
1171
1196
 
1172
- function SupplierRow({ sup, t, season, quota, expanded, onToggle, onEdit }) {
1173
- const metric = selectPrimaryMetric(sup, t);
1197
+ function SupplierRow({ sup, t, season, quota, windowDays, expanded, onToggle, onEdit }) {
1198
+ const metric = selectPrimaryMetric(sup, t, windowDays);
1174
1199
  const todayNumber = typeof sup.todayTokens === "number" ? sup.todayTokens : 0;
1175
1200
  const error = String(sup.error?.message || sup.error?.code || "");
1176
1201
  const detailId = `qm-detail-${sup.id}`;
1202
+ // 今日行与五分量分布条共用一份口径:切到「近 7 天」时两处一起换,图表固定 7 天不跟着变。
1203
+ // 估算口径(剩余/总量)只影响读数本身,不重新取数。
1204
+ const [range, setRange] = React.useState("today");
1205
+ const [estMode, setEstMode] = React.useState("left");
1206
+ const days = Array.isArray(sup.usageDays) ? sup.usageDays : [];
1207
+ const week = range === "7d";
1208
+ const scopedUsage = week ? sumUsageOf(days) : sup.todayUsage;
1209
+ const scopedTokens = week
1210
+ ? days.reduce((sum, day) => sum + (Number(day?.total) || 0), 0)
1211
+ : todayNumber;
1177
1212
  return h("article", { className: `qm-row${expanded ? " open" : ""}`, "data-supplier": sup.id },
1178
1213
  h("button", { type: "button", className: "qm-row-head", "data-qm-row": "", "aria-expanded": expanded, "aria-controls": detailId, onClick: onToggle },
1179
1214
  h("span", { className: `qm-dot ${stateClass(sup)}` }),
1180
1215
  h("b", { className: "qm-row-name" }, sup.name),
1181
- sup.current ? h("span", { className: "qm-cur" }, t("current")) : null,
1216
+ // 「当前」= 在用/计入今日总量的那个;仅计量行恒计入总量,但不是一个「当前供应商」,
1217
+ // 给它打这个标会让每一行都读作当前,所以只对注册表行显示。
1218
+ sup.current && !isMeteredSupplier(sup) ? h("span", { className: "qm-cur" }, t("current")) : null,
1182
1219
  h("span", { className: `qm-pill ${stateClass(sup)}` }, supplierStatus(sup, t)),
1183
1220
  h("span", { className: "qm-row-metric" }, metric.value),
1184
1221
  h("span", { className: "qm-row-today" }, `${t("todayLabel")} ${fmtBig(todayNumber)}`),
@@ -1186,12 +1223,16 @@
1186
1223
  expanded ? h("div", { id: detailId, className: "qm-row-detail", role: "region", "aria-label": `${sup.name} ${t("detailTitle")}` },
1187
1224
  sup.state === "err" ? h("div", { className: "ci-err", role: "status" }, supplierStatus(sup, t), error ? `:${error.slice(0, 180)}` : "",
1188
1225
  error.length > 180 ? h("details", null, h("summary", null, t("errorDetails")), h("p", null, error)) : null) : null,
1226
+ // 仅计量行不查询,所以没有「最后成功」也可能没有指标表——空白不是失败态,是这类行的常态。
1189
1227
  sup.lastSuccessAt ? h("div", { className: "qm-data-time" }, `${t("lastSuccess")} ${new Date(sup.lastSuccessAt).toLocaleString()}`) : null,
1190
- h(MetricTable, { supplier: sup, t }),
1191
- h(TodayLine, { sup, t }),
1192
- h(UsageSplit, { usage: sup.todayUsage, t }),
1228
+ isMeteredSupplier(sup) ? null : h(MetricTable, { supplier: sup, t }),
1229
+ h(TodayLine, {
1230
+ sup, t, tokens: scopedTokens, usage: scopedUsage,
1231
+ range, onRange: setRange, mode: estMode, onMode: setEstMode, windowDays,
1232
+ }),
1233
+ h(UsageSplit, { usage: scopedUsage, t }),
1193
1234
  h(SeasonBlock, { season, split: sup.seasonSplit, t }),
1194
- h("div", { className: "qm-row-actions" }, h("button", { type: "button", onClick: () => onEdit(sup.id) }, t("openPage"))),
1235
+ h("div", { className: "qm-row-actions" }, h("button", { type: "button", onClick: () => onEdit(sup.id) }, t(isMeteredSupplier(sup) ? "rename" : "openPage"))),
1195
1236
  Array.isArray(sup.usageDays) && sup.usageDays.length
1196
1237
  ? h(UsageChart, { sup, days: sup.usageDays, quota, windowTotal: sup.usageDays.reduce((sum, day) => sum + (Number(day?.total) || 0), 0), t })
1197
1238
  : null) : null);
@@ -1210,14 +1251,77 @@
1210
1251
  h("td", { key: column.key, className: column.key === "used" ? `qm-tone ${row.tone}` : column.key === "value" ? "qm-num" : "" }, row[column.key] ?? "—"))))))));
1211
1252
  }
1212
1253
 
1213
- function TodayLine({ sup, t }) {
1214
- const total = fmtBig(typeof sup.todayTokens === "number" ? sup.todayTokens : 0);
1215
- const ratio = usageRatioOf(sup.todayUsage);
1254
+ /**
1255
+ * 今日行:区间选择器 + 数量 + 归一比例 + 窗口估算。
1256
+ * 「Today」两个字换成选择器后,同一行读作「哪个区间、多少 token、什么比例、各窗口还能用多少」。
1257
+ */
1258
+ function TodayLine({ sup, t, tokens, usage, range, onRange, mode, onMode, windowDays = 7 }) {
1259
+ const total = fmtBig(tokens || 0);
1260
+ const ratio = usageRatioOf(usage);
1216
1261
  const detailText = ratio ? t("ratioAria", { hit: fmtBig(ratio.hitValue), miss: fmtBig(ratio.missValue), out: fmtBig(ratio.outValue) }) : "";
1262
+ // 区间已经由左边的选择器说清楚了,这里不再重复「Today / 近 7 天」,只留数量。
1263
+ const text = `${total} tokens`;
1217
1264
  return h("div", { className: "qm-today-line" },
1218
- h("span", { className: "qm-today-text" }, t("today", { n: total })),
1265
+ h("span", { className: "qm-range" },
1266
+ h("select", {
1267
+ value: range, "aria-label": t("rangeLabel"),
1268
+ onChange: (ev) => onRange(ev.target.value === "7d" ? "7d" : "today"),
1269
+ },
1270
+ h("option", { value: "today" }, t("todayLabel")),
1271
+ h("option", { value: "7d" }, t("statsWindow", { n: windowDays })))),
1272
+ h("span", { className: "qm-today-text" }, text),
1219
1273
  ratio ? h("span", { className: "qm-ratio", title: detailText },
1220
- h("b", { className: "r-hit" }, ratio.hit), " : ", h("b", { className: "r-miss" }, ratio.miss), " : ", h("b", { className: "r-out" }, ratio.out)) : null);
1274
+ h("b", { className: "r-hit" }, ratio.hit), " : ", h("b", { className: "r-miss" }, ratio.miss), " : ", h("b", { className: "r-out" }, ratio.out)) : null,
1275
+ h(EstimateGroup, { sup, t, mode, onMode }));
1276
+ }
1277
+
1278
+ /**
1279
+ * 窗口估算:同一段折线里「本地 token ÷ Δpp」得到每 1 个百分点值多少 token,
1280
+ * 再按当前电平换算成剩余(Left)或窗口总量(Total)。
1281
+ * 分母是 Δpp 而不是已用% —— 窗口比本地留存长时,分子只覆盖最近几天,
1282
+ * 除以累计百分比会把总量算成一半(3B 降 50pp 应得 6B,不是 3B ÷ 90%)。
1283
+ * title 里给出取样区间、分子分母与 ±1pp 的区间,好让人对着图核账。
1284
+ */
1285
+ function EstimateGroup({ sup, t, mode, onMode }) {
1286
+ const entries = (Array.isArray(sup.entries) ? sup.entries : [])
1287
+ .filter((entry) => entry && entry.kind === "win" && Number.isFinite(Number(entry.pct)));
1288
+ if (!entries.length) return null;
1289
+ const kind = mode === "left" ? t("estLeft") : t("estTotal");
1290
+ const chips = [];
1291
+ let estimable = 0;
1292
+ for (const entry of entries) {
1293
+ const label = entry.short || entry.name || entry.key;
1294
+ const pct = Math.min(100, Math.max(0, Number(entry.pct)));
1295
+ const est = entry.estimate;
1296
+ const tokens = Number(est?.tokens);
1297
+ const dPct = Number(est?.dPct);
1298
+ // 不到两个百分点(整数上报)就是噪声,宁可给「—」
1299
+ if (!(tokens > 0) || !(dPct >= 2)) {
1300
+ chips.push(h("span", { key: entry.key, className: "qm-est-q is-off", title: t("estNoData") }, `${label} —`));
1301
+ continue;
1302
+ }
1303
+ estimable++;
1304
+ // 剩余看还占多少个百分点,总量看满 100 个百分点
1305
+ const level = mode === "left" ? Math.max(0, 100 - pct) : 100;
1306
+ const at = (delta) => (tokens / delta) * level;
1307
+ const main = at(dPct);
1308
+ const spanMs = Math.max(0, Number(est.to) - Number(est.from));
1309
+ const span = spanMs >= 48 * 3600_000
1310
+ ? `${Math.round(spanMs / 86400_000)}d`
1311
+ : `${Math.max(1, Math.round(spanMs / 3600_000))}h`;
1312
+ const title = `${label} ${kind} ≈${fmtBig(Math.round(main))}\n`
1313
+ + `${span}: ${fmtBig(tokens)} ÷ Δ${dPct}pp\n`
1314
+ + `Δ${dPct}pp ±1pp → ${fmtBig(Math.round(at(dPct + 1)))}–${fmtBig(Math.round(at(dPct - 1)))}`;
1315
+ chips.push(h("span", { key: entry.key, className: "qm-est-q", title },
1316
+ `${label} `, h("b", null, `≈${fmtBig(Math.round(main))}`)));
1317
+ }
1318
+ // 一个窗口都算不出来时整组不出现:不拿一排「—」占掉今日行
1319
+ if (!estimable) return null;
1320
+ return h("span", { className: "qm-est" },
1321
+ h("span", { className: "qm-est-seg", role: "group" },
1322
+ h("button", { type: "button", "aria-pressed": mode === "left", onClick: () => onMode("left") }, t("estLeft")),
1323
+ h("button", { type: "button", "aria-pressed": mode === "total", onClick: () => onMode("total") }, t("estTotal"))),
1324
+ chips);
1221
1325
  }
1222
1326
 
1223
1327
  /** 五分量分布:条与图例一一对应;今日比例只取前三项,写缓存与未细分在这里出现。 */
@@ -1276,7 +1380,7 @@
1276
1380
  h("span", { className: "chip" }, t("connectedChip", { n: added.length })),
1277
1381
  h("button", { type: "button", className: `qm-rescan-btn${ctl.scanBusy ? " busy" : ""}`, disabled: ctl.scanBusy, onClick: ctl.runRescan }, `⟳ ${t("rescan")}`))),
1278
1382
  h("div", { className: "qm-scanline" }, scanLine()),
1279
- added.length ? orderedRows(added, t, ctl) : h(Empty, { text: t("emptyAdded"), action: t("rescan"), onAction: ctl.runRescan }),
1383
+ added.length ? orderedRows(added, t, ctl, state.poll?.usageHistoryDays || 7) : h(Empty, { text: t("emptyAdded"), action: t("rescan"), onAction: ctl.runRescan }),
1280
1384
  addable.length ? h("details", { className: "qm-addable", open: addableOpen, onToggle: (e) => setAddableOpen(e.currentTarget.open) },
1281
1385
  h("summary", null, h("span", null, t("addableTitle")), h("span", { className: "cnt" }, addable.length)),
1282
1386
  h("div", { className: "addable-list" }, addable.map((a) => h("div", { key: a.id, className: `qm-add-row${ctl.freshIds.includes(a.id) ? " fresh" : ""}` },
@@ -1304,20 +1408,24 @@
1304
1408
  h("option", { value: "expanded" }, t("expanded")), h("option", { value: "compact" }, t("compact")))));
1305
1409
  }
1306
1410
 
1307
- function orderedRows(added, t, ctl) {
1308
- return h("div", { className: "qm-page-list" }, added.map((sup) =>
1309
- h("div", { key: sup.id, "data-supplier": sup.id, className: "qm-page-row" },
1411
+ function orderedRows(added, t, ctl, days = 7) {
1412
+ return h("div", { className: "qm-page-list" }, added.map((sup) => {
1413
+ const metered = isMeteredSupplier(sup);
1414
+ return h("div", { key: sup.id, "data-supplier": sup.id, className: "qm-page-row" },
1310
1415
  h("div", { className: "qm-page-main" },
1311
1416
  h("span", { className: `qm-dot ${stateClass(sup)}` }),
1312
- h("span", { className: "qm-page-name", title: sup.meta?.compat ? t("compatSource") : undefined }, sup.name),
1313
- h("span", { className: "qm-row-metric" }, selectPrimaryMetric(sup, t).value),
1417
+ h("span", { className: "qm-page-name", title: sup.meta?.compat ? t("compatSource") : metered ? sup.route : undefined }, sup.name),
1418
+ h("span", { className: "qm-row-metric" }, selectPrimaryMetric(sup, t, days).value),
1314
1419
  h("span", { className: `qm-pill ${stateClass(sup)}` }, supplierStatus(sup, t)),
1315
- h("span", { className: "qm-page-pill" }, t(sup.meta?.credentialLabelKey || "apiKey"))),
1420
+ // 仅计量行没有凭据类别可标:它的凭据就是宿主自己的路由,插件不持有任何密钥。
1421
+ metered ? null : h("span", { className: "qm-page-pill" }, t(sup.meta?.credentialLabelKey || "apiKey"))),
1316
1422
  h("div", { className: "qm-page-actions" },
1317
- h("label", { className: "qm-page-toggle" },
1423
+ // 没有可启用/停用的东西:行是流量的产物,不是一个可以关掉的采集器。
1424
+ metered ? null : h("label", { className: "qm-page-toggle" },
1318
1425
  h("input", { type: "checkbox", checked: !!sup.enabled, disabled: !!ctl.toggling[sup.id] || [...settingsWrites].some(operation => operation.supplierId === sup.id), onChange: () => ctl.toggleEnabled(sup) }),
1319
1426
  h("span", null, t("enable"))),
1320
- h("button", { type: "button", className: "s-test qm-pri-soft", onClick: () => ctl.openSupplier(sup.id) }, t("openPage"))))));
1427
+ h("button", { type: "button", className: "s-test qm-pri-soft", onClick: () => ctl.openSupplier(sup.id) }, t(metered ? "rename" : "openPage"))));
1428
+ }));
1321
1429
  }
1322
1430
 
1323
1431
  // ---------- 供应商配置子页 ----------
@@ -1345,6 +1453,23 @@
1345
1453
  const editor = ctl.editor;
1346
1454
  if (!sup || !editor) return h("div", { className: "qm-sub", role: "status" }, t("loading"));
1347
1455
  const f = editor.form;
1456
+ // 仅计量子页:没有凭据、没有阈值、没有关注限额——只有名字与它挂在哪条路由上。
1457
+ if (isMeteredSupplier(sup)) {
1458
+ return h(React.Fragment, null,
1459
+ h("div", { className: "qm-page-head" },
1460
+ h("button", { type: "button", className: "s-test", onClick: ctl.closeEditor }, `← ${t("back")}`),
1461
+ h("b", null, sup.name),
1462
+ h("span", { className: "qm-pill" }, t("meteredOnly"))),
1463
+ h("div", { className: "s-group" },
1464
+ h("label", null, h("span", null, t("meteredName")),
1465
+ h("input", { type: "text", value: f.name || "", maxLength: 60, placeholder: sup.route, disabled: ctl.opBusy,
1466
+ onChange: (e) => ctl.setEditorField("name", e.target.value) })),
1467
+ h("p", { className: "qm-empty-line" }, `${t("meteredRoute")} · ${sup.route}`)),
1468
+ h("div", { className: "s-actions" },
1469
+ ctl.saved === sup.id ? h("span", { className: "s-saved" }, t("saved")) : null,
1470
+ h("button", { type: "button", onClick: ctl.closeEditor, disabled: ctl.opBusy }, t("cancel")),
1471
+ h("button", { type: "button", className: "qm-pri", onClick: ctl.saveSupplier, disabled: ctl.opBusy }, t("save"))));
1472
+ }
1348
1473
  const needsInputs = (sup.meta?.needs || []).map((need) =>
1349
1474
  h("label", { key: need.key }, needsLabel(sup, need, t),
1350
1475
  h("input", {
@@ -1635,7 +1760,7 @@
1635
1760
  disabled: "未启用",
1636
1761
  notQueried: "尚未查询",
1637
1762
  lastSuccess: "数据时间",
1638
- todayScope: "当前供应商的今日 Token 用量,按宿主本地日统计",
1763
+ todayScope: "已添加供应商的今日 Token 用量合计,按宿主本地日统计",
1639
1764
  eventsObserved: "已观测到调用事件",
1640
1765
  previewSaved: "已保存配置的最近查询结果;编辑草稿不会改变此预览。",
1641
1766
  advanced: "高级选项",
@@ -1695,6 +1820,10 @@
1695
1820
  todayShort: "今日 {n}",
1696
1821
  statsWindow: "近 {n} 天",
1697
1822
  remainLabel: "剩余 {n}%",
1823
+ rangeLabel: "统计区间",
1824
+ estLeft: "剩余",
1825
+ estTotal: "套餐总量",
1826
+ estNoData: "本地样本不足",
1698
1827
  ratioAria: "缓存命中 {hit} · 缓存未命中 {miss} · 输出 {out}",
1699
1828
  cacheHit: "缓存命中",
1700
1829
  cacheMiss: "缓存未命中",
@@ -1712,6 +1841,12 @@
1712
1841
  stateCrit: "临界",
1713
1842
  stateErr: "取数失败",
1714
1843
  stateOff: "未配置",
1844
+ // 仅计量供应商:只有经过 Harness 的 token,没有可查询的额度与百分比
1845
+ meteredOnly: "仅计量",
1846
+ windowTokens: "近 {d} 天 {n}",
1847
+ rename: "改名",
1848
+ meteredName: "名称",
1849
+ meteredRoute: "DSH 路由",
1715
1850
  quota: "限额",
1716
1851
  usedShort: "已用",
1717
1852
  detailTitle: "明细",
@@ -1768,7 +1903,7 @@
1768
1903
  critPct: "临界阈值 %",
1769
1904
  interval: "轮询间隔(秒,10–3600)",
1770
1905
  retention: "用量保留期(天,1–90)",
1771
- unmapped: "另探测到 DSH 内已添加但本插件暂不支持的供应商",
1906
+ unmapped: "另探测到 DSH 内已添加、但没有额度查询的供应商",
1772
1907
  },
1773
1908
  en: {
1774
1909
  current: "Current",
@@ -1802,7 +1937,7 @@
1802
1937
  disabled: "Disabled",
1803
1938
  notQueried: "Not queried yet",
1804
1939
  lastSuccess: "Data updated",
1805
- todayScope: "Today's Token usage for current suppliers, measured by the host local day",
1940
+ todayScope: "Today's Token usage across every listed supplier, measured by the host local day",
1806
1941
  eventsObserved: "Call events observed",
1807
1942
  previewSaved: "Latest query for the saved configuration. Draft edits do not change this preview.",
1808
1943
  advanced: "Advanced options",
@@ -1861,6 +1996,10 @@
1861
1996
  todayShort: "Today {n}",
1862
1997
  statsWindow: "Last {n} days",
1863
1998
  remainLabel: "{n}% left",
1999
+ rangeLabel: "Range",
2000
+ estLeft: "Left",
2001
+ estTotal: "Total",
2002
+ estNoData: "Not enough local samples",
1864
2003
  ratioAria: "Cache hits {hit} · cache misses {miss} · output {out}",
1865
2004
  cacheHit: "Cache hits",
1866
2005
  cacheMiss: "Cache misses",
@@ -1878,6 +2017,11 @@
1878
2017
  stateCrit: "Critical",
1879
2018
  stateErr: "Fetch failed",
1880
2019
  stateOff: "Not configured",
2020
+ meteredOnly: "Usage only",
2021
+ windowTokens: "{d}d {n}",
2022
+ rename: "Rename",
2023
+ meteredName: "Name",
2024
+ meteredRoute: "DSH route",
1881
2025
  quota: "Limit",
1882
2026
  usedShort: "Used",
1883
2027
  detailTitle: "Details",
@@ -1934,7 +2078,7 @@
1934
2078
  critPct: "Critical threshold %",
1935
2079
  interval: "Poll interval (s, 10–3600)",
1936
2080
  retention: "Usage retention (days, 1–90)",
1937
- unmapped: "Detected DSH suppliers this plugin does not support yet",
2081
+ unmapped: "Detected DSH suppliers with no quota query",
1938
2082
  },
1939
2083
  };
1940
2084
 
@@ -1978,6 +2122,26 @@ div:has(> div[data-slot="sidebar.footer.action"] > [data-qm-linerow]){flex-wrap:
1978
2122
  .qm-today-line{display:flex;align-items:baseline;flex-wrap:wrap;gap:2px 10px;margin:8px 10px 6px;
1979
2123
  color:var(--dsw-alias-label-secondary,#a9b2c5);font-size:11px;font-variant-numeric:tabular-nums;}
1980
2124
  .qm-today-text{flex:none;}
2125
+ /* 区间选择器(今天 / 近 7 天):替代原来的「今日」二字,一次点击换口径 */
2126
+ .qm-card .qm-range{position:relative;display:inline-flex;align-items:center;flex:none;}
2127
+ .qm-card .qm-range select{font:inherit;font-size:11px;line-height:15px;padding:1px 16px 1px 6px;appearance:none;
2128
+ color:var(--dsw-alias-label-primary,#dbe2ee);background:var(--dsw-alias-bg-base,#0b0e13);
2129
+ border:1px solid var(--dsw-alias-border-l2,#363d4d);border-radius:6px;cursor:pointer;}
2130
+ .qm-range::after{content:"▾";position:absolute;right:5px;top:50%;transform:translateY(-50%);font-size:8px;
2131
+ color:var(--dsw-alias-label-tertiary,#8b94a8);pointer-events:none;}
2132
+ /* 窗口估算:Left/Total 两段式 + 三个读数;窄栏自己折行,不去挤比例数字 */
2133
+ .qm-est{display:inline-flex;flex-wrap:wrap;align-items:baseline;gap:2px 8px;margin-left:auto;
2134
+ font-size:11px;color:var(--dsw-alias-label-secondary,#a9b2c5);min-width:0;}
2135
+ .qm-est-seg{display:inline-flex;flex:none;border:1px solid var(--dsw-alias-border-l2,#363d4d);
2136
+ border-radius:6px;overflow:hidden;}
2137
+ .qm-card .qm-est-seg button{font:inherit;font-size:10px;line-height:15px;padding:1px 6px;border:0;border-radius:0;
2138
+ background:none;color:var(--dsw-alias-label-tertiary,#8b94a8);cursor:pointer;}
2139
+ .qm-card .qm-est-seg button+button{border-left:1px solid var(--dsw-alias-border-l2,#363d4d);}
2140
+ .qm-card .qm-est-seg button[aria-pressed=true]{background:var(--dsw-alias-interactive-bg-hover,#20283a);
2141
+ color:var(--dsw-alias-label-primary,#dbe2ee);}
2142
+ .qm-est-q{white-space:nowrap;color:var(--dsw-alias-label-secondary,#a9b2c5);}
2143
+ .qm-est-q b{font-weight:600;color:var(--dsw-alias-label-primary,#dbe2ee);font-variant-numeric:tabular-nums;}
2144
+ .qm-est-q.is-off{color:var(--dsw-alias-label-tertiary,#5c6577);}
1981
2145
  /* 分布条:详情行只有一条五段条(命中/未命中/输出/写缓存/未细分);段宽 = 分量占比 */
1982
2146
  .qm-split-bar{display:inline-flex;flex:1 1 64px;min-width:48px;height:5px;border-radius:2.5px;overflow:hidden;
1983
2147
  background:var(--dsw-alias-border-l1,#1d2330);}
@@ -2005,8 +2169,11 @@ div:has(> div[data-slot="sidebar.footer.action"] > [data-qm-linerow]){flex-wrap:
2005
2169
  .qm-chart-plot svg{display:block;width:100%;height:100%;overflow:visible;}
2006
2170
  /* 柱值标签:按绘图区百分比定位,文字本身不参与 svg 的横向拉伸 */
2007
2171
  .qm-chart-labels{position:absolute;inset:0;pointer-events:none;}
2172
+ /* 柱值标签:按绘图区百分比定位,文字本身不参与 svg 的横向拉伸。
2173
+ 标签不可透明:底下一层卡片底色把折线挡开,数字才不会被线压花(标签层在 SVG 之上)。 */
2008
2174
  .qm-chart-value{position:absolute;transform:translateX(-50%);font-size:10px;line-height:1;white-space:nowrap;
2009
- font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary,#a9b2c5);pointer-events:none;}
2175
+ font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary,#a9b2c5);pointer-events:none;
2176
+ padding:1px 3px;border-radius:3px;background:var(--dsw-alias-bg-layer-2,#151922);}
2010
2177
  .qm-chart-bar{fill:var(--dsw-alias-state-business-primary,#4f8cff);fill-opacity:.32;}
2011
2178
  .qm-chart-grid{stroke:var(--dsw-alias-border-l2,#363d4d);stroke-width:1;stroke-dasharray:3 3;vector-effect:non-scaling-stroke;}
2012
2179
  .qm-chart-axis{stroke:var(--dsw-alias-border-l2,#363d4d);stroke-width:1;vector-effect:non-scaling-stroke;}