@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/CHANGELOG.md +107 -2
- package/CONTEXT.md +10 -6
- package/LICENSE +24 -0
- package/README.en.md +357 -0
- package/README.md +69 -89
- package/docs/images/overview.png +0 -0
- package/docs/images/providers.png +0 -0
- package/lib/client.js +205 -38
- package/lib/index.js +192 -14
- package/lib/ledger.js +59 -0
- package/lib/providers.js +16 -4
- package/lib/usage.js +13 -2
- package/package.json +5 -2
- package/scripts/check-package.mjs +6 -1
- package/test/browser/interactions.spec.mjs +9 -3
- package/test/client.mjs +177 -9
- package/test/copy.mjs +8 -2
- package/test/metered.mjs +245 -0
- package/test/mock-dsh.mjs +12 -1
- package/test/runtime.mjs +56 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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:
|
|
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,
|
|
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 (!
|
|
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
|
-
|
|
1055
|
-
await post(`${API}/settings`,
|
|
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
|
-
|
|
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, {
|
|
1192
|
-
|
|
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
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
1420
|
+
// 仅计量行没有凭据类别可标:它的凭据就是宿主自己的路由,插件不持有任何密钥。
|
|
1421
|
+
metered ? null : h("span", { className: "qm-page-pill" }, t(sup.meta?.credentialLabelKey || "apiKey"))),
|
|
1316
1422
|
h("div", { className: "qm-page-actions" },
|
|
1317
|
-
|
|
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: "
|
|
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
|
|
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
|
|
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;}
|