dsh-key-usage 0.1.2 → 0.1.4

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
@@ -6,6 +6,400 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
8
  let react_jsx_runtime = require("react/jsx-runtime");
9
+ //#region src/client/pricing.ts
10
+ const COST_SOURCE_LABELS = {
11
+ official: "官方",
12
+ rule: "自定",
13
+ catalog: "官方价",
14
+ estimate: "估算",
15
+ none: "—"
16
+ };
17
+ /** 角标的悬浮说明:让「估算」不会被当成账单金额用。 */
18
+ const COST_SOURCE_HINTS = {
19
+ official: "官方:台账记录的厂商账单金额",
20
+ rule: "自定:按你维护的计费规则算出",
21
+ catalog: "官方价:按插件内置的厂商官方价目算出(可查来源;你的规则优先于它)",
22
+ estimate: "估算:按台账反推的均价推算",
23
+ none: "没有价格依据(未配置规则,台账也无成本数据)"
24
+ };
25
+ const UNIT_LABELS = {
26
+ perMillionTokens: "每百万 tokens",
27
+ perThousandTokens: "每千 tokens",
28
+ perCall: "每次调用"
29
+ };
30
+ const UNIT_OPTIONS = [
31
+ "perMillionTokens",
32
+ "perThousandTokens",
33
+ "perCall"
34
+ ];
35
+ const CURRENCY_OPTIONS = ["CNY", "USD"];
36
+ const EMPTY_CONFIG = {
37
+ version: 1,
38
+ currency: "CNY",
39
+ rules: []
40
+ };
41
+ /** 币种符号;未知币种保留代码前缀,避免把 USD 当 CNY 渲染。 */
42
+ function currencySymbolOf(currency) {
43
+ if (currency === "CNY") return "¥";
44
+ if (currency === "USD") return "$";
45
+ return currency + " ";
46
+ }
47
+ function ruleKeyOf(rule) {
48
+ return rule.provider + "|" + rule.model;
49
+ }
50
+ function ruleLabelOf(rule) {
51
+ const model = rule.model === "*" ? "全部模型" : rule.model;
52
+ return (rule.provider === "*" ? "任意通道" : rule.provider) + " · " + model;
53
+ }
54
+ /** 错峰窗口文案:`09:00–12:00、14:00–18:00`;带 `days` 的窗口补一个「(工作日)」之类的后缀。 */
55
+ function offPeakWindowsText(rule) {
56
+ if (rule.offPeak === null || rule.offPeak.windows.length === 0) return "";
57
+ return rule.offPeak.windows.map((window) => {
58
+ const base = window.from + "–" + window.to;
59
+ const suffix = dayScopeSuffixOf(window.days);
60
+ return suffix === "" ? base : base + "(" + suffix + ")";
61
+ }).join("、");
62
+ }
63
+ const DAY_SCOPE_OPTIONS = [
64
+ "every",
65
+ "weekday",
66
+ "weekend"
67
+ ];
68
+ const DAY_SCOPE_LABELS = {
69
+ every: "每天",
70
+ weekday: "仅工作日",
71
+ weekend: "仅周末"
72
+ };
73
+ /** 「每天」刻意映射成 `days` 缺省(而不是 [1..7]),保证升级前的规则原样往返。 */
74
+ const DAY_SCOPE_DAYS = {
75
+ every: void 0,
76
+ weekday: [
77
+ 1,
78
+ 2,
79
+ 3,
80
+ 4,
81
+ 5
82
+ ],
83
+ weekend: [6, 7]
84
+ };
85
+ /** 已保存的 `days` → 编辑器取值;非法值一律当「每天」(与服务端容错口径一致)。 */
86
+ function dayScopeOf(days) {
87
+ const normalized = normalizeDays(days);
88
+ if (normalized === void 0) return "every";
89
+ const key = normalized.join(",");
90
+ if (key === "1,2,3,4,5") return "weekday";
91
+ if (key === "6,7") return "weekend";
92
+ return "every";
93
+ }
94
+ /** `days` 的简短说明;缺省或非三名组合返回空串(表示「每天」)。 */
95
+ function dayScopeSuffixOf(days) {
96
+ const scope = dayScopeOf(days);
97
+ return scope === "every" ? "" : DAY_SCOPE_LABELS[scope];
98
+ }
99
+ /**
100
+ * `days` 归一:只接受 1–7 的整数、去重排序;非法值返回 undefined = 每天适用。
101
+ * 与服务端 normalizeDays 同一口径:非法值不整段作废,避免金额突然变高。
102
+ */
103
+ function normalizeDays(raw) {
104
+ if (!Array.isArray(raw)) return void 0;
105
+ const days = /* @__PURE__ */ new Set();
106
+ for (const entry of raw) {
107
+ if (typeof entry !== "number" || !Number.isInteger(entry) || entry < 1 || entry > 7) continue;
108
+ days.add(entry);
109
+ }
110
+ if (days.size === 0) return void 0;
111
+ return [...days].sort((first, second) => first - second);
112
+ }
113
+ function toNumber(value, fallback) {
114
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
115
+ }
116
+ function toText(value, fallback) {
117
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : fallback;
118
+ }
119
+ function ratesOf(raw, fallback) {
120
+ return {
121
+ input: toNumber(raw.input, fallback.input),
122
+ cacheRead: toNumber(raw.cacheRead, fallback.cacheRead),
123
+ cacheWrite: toNumber(raw.cacheWrite, fallback.cacheWrite),
124
+ output: toNumber(raw.output, fallback.output)
125
+ };
126
+ }
127
+ function windowsOf(raw) {
128
+ if (!Array.isArray(raw)) return [];
129
+ const windows = [];
130
+ for (const entry of raw) {
131
+ if (typeof entry !== "object" || entry === null) continue;
132
+ const from = entry.from;
133
+ const to = entry.to;
134
+ if (typeof from !== "string" || typeof to !== "string") continue;
135
+ if (!/^\d{1,2}:\d{2}$/.test(from.trim()) || !/^\d{1,2}:\d{2}$/.test(to.trim())) continue;
136
+ const days = normalizeDays(entry.days);
137
+ windows.push({
138
+ from: from.trim(),
139
+ to: to.trim(),
140
+ ...days === void 0 ? {} : { days }
141
+ });
142
+ }
143
+ return windows;
144
+ }
145
+ /**
146
+ * 设置服务返回的值做一次防御式归一:
147
+ * 宿主 schema 会把缺省的 `offPeak` 补成 `{windows: [], …}`,那种空壳要还原成「无错峰」。
148
+ */
149
+ function normalizeClientPricing(value) {
150
+ if (typeof value !== "object" || value === null) return EMPTY_CONFIG;
151
+ const record = value;
152
+ const currency = toText(record.currency, EMPTY_CONFIG.currency);
153
+ const rules = [];
154
+ for (const entry of Array.isArray(record.rules) ? record.rules : []) {
155
+ if (typeof entry !== "object" || entry === null) continue;
156
+ const raw = entry;
157
+ const base = ratesOf(raw, {
158
+ input: 0,
159
+ cacheRead: 0,
160
+ cacheWrite: 0,
161
+ output: 0
162
+ });
163
+ let offPeak = null;
164
+ if (typeof raw.offPeak === "object" && raw.offPeak !== null) {
165
+ const offPeakRaw = raw.offPeak;
166
+ const windows = windowsOf(offPeakRaw.windows);
167
+ if (windows.length > 0) offPeak = {
168
+ ...ratesOf(offPeakRaw, base),
169
+ windows
170
+ };
171
+ }
172
+ rules.push({
173
+ provider: toText(raw.provider, "*"),
174
+ model: toText(raw.model, "*"),
175
+ unit: UNIT_OPTIONS.includes(raw.unit) ? raw.unit : "perMillionTokens",
176
+ currency: toText(raw.currency, currency),
177
+ ...base,
178
+ offPeak,
179
+ note: typeof raw.note === "string" ? raw.note : ""
180
+ });
181
+ }
182
+ return {
183
+ version: typeof record.version === "number" ? record.version : 1,
184
+ currency,
185
+ rules
186
+ };
187
+ }
188
+ /** 编辑器产出的规则必须只含 JSON 形状:写盘走宿主,`undefined` 会被拒绝。 */
189
+ function cleanRuleForWrite(rule) {
190
+ const values = {
191
+ provider: rule.provider.trim() === "" ? "*" : rule.provider.trim(),
192
+ model: rule.model.trim() === "" ? "*" : rule.model.trim(),
193
+ unit: rule.unit,
194
+ currency: rule.currency,
195
+ input: rule.input,
196
+ cacheRead: rule.cacheRead,
197
+ cacheWrite: rule.cacheWrite,
198
+ output: rule.output,
199
+ note: rule.note
200
+ };
201
+ if (rule.offPeak !== null && rule.offPeak.windows.length > 0) values.offPeak = {
202
+ windows: rule.offPeak.windows.map((window) => {
203
+ const days = normalizeDays(window.days);
204
+ return days === void 0 ? {
205
+ from: window.from,
206
+ to: window.to
207
+ } : {
208
+ from: window.from,
209
+ to: window.to,
210
+ days
211
+ };
212
+ }),
213
+ input: rule.offPeak.input,
214
+ cacheRead: rule.offPeak.cacheRead,
215
+ cacheWrite: rule.offPeak.cacheWrite,
216
+ output: rule.offPeak.output
217
+ };
218
+ return values;
219
+ }
220
+ function configForWrite(config) {
221
+ return {
222
+ version: config.version,
223
+ currency: config.currency,
224
+ rules: config.rules.map(cleanRuleForWrite)
225
+ };
226
+ }
227
+ /** 新增规则的初始值:全 0 单价会算成免费,因此先挡在保存之前(见编辑器校验)。 */
228
+ function blankRule(config) {
229
+ return {
230
+ provider: "*",
231
+ model: "*",
232
+ unit: "perMillionTokens",
233
+ currency: config.currency,
234
+ input: 0,
235
+ cacheRead: 0,
236
+ cacheWrite: 0,
237
+ output: 0,
238
+ offPeak: null,
239
+ note: ""
240
+ };
241
+ }
242
+ /** 导入:接受整份配置(`{version,currency,rules}`)或裸规则数组;解析失败给出中文原因。 */
243
+ function parseImportedPricing(text) {
244
+ let parsed;
245
+ try {
246
+ parsed = JSON.parse(text);
247
+ } catch (error) {
248
+ return {
249
+ config: null,
250
+ error: "不是合法 JSON:" + (error instanceof Error ? error.message : String(error))
251
+ };
252
+ }
253
+ const candidate = Array.isArray(parsed) ? {
254
+ version: 1,
255
+ currency: EMPTY_CONFIG.currency,
256
+ rules: parsed
257
+ } : parsed;
258
+ if (typeof candidate !== "object" || candidate === null) return {
259
+ config: null,
260
+ error: "顶层必须是对象或规则数组"
261
+ };
262
+ const config = normalizeClientPricing(candidate);
263
+ if (config.rules.length === 0) return {
264
+ config: null,
265
+ error: "没有解析出任何规则(检查 provider / model / 单价字段)"
266
+ };
267
+ return {
268
+ config,
269
+ error: null
270
+ };
271
+ }
272
+ /** 单价全为 0 的规则会把成本算成免费,保存前必须拦住。 */
273
+ function ruleProblemOf(rule) {
274
+ const values = [
275
+ rule.input,
276
+ rule.cacheRead,
277
+ rule.cacheWrite,
278
+ rule.output
279
+ ];
280
+ if (values.every((value) => value === 0)) return "四档单价全为 0 会算成免费,至少填一个";
281
+ if (values.some((value) => !Number.isFinite(value) || value < 0)) return "单价必须是不小于 0 的数字";
282
+ if (rule.offPeak !== null && rule.offPeak.windows.length === 0) return "错峰时段至少要有一个窗口,或整段留空";
283
+ return null;
284
+ }
285
+ //#endregion
286
+ //#region src/client/pricingSettings.ts
287
+ /**
288
+ * 计费规则的读写通道:把宿主的配置表单服务(`ctx.configForms`)包成一个 React 可订阅的 store。
289
+ *
290
+ * 为什么不用自己的 HTTP 路由写文件:插件只允许写 `quota-snapshots.json` 一个文件。
291
+ * 规则的命名空间就是本插件在 profile 里的条目 id(`dsh-key-usage`),
292
+ * 写入由宿主的 config-editor 落到当前 profile 的 cordis patch 上,插件全程不碰文件系统。
293
+ */
294
+ function createPricingSettings() {
295
+ let state = {
296
+ status: "loading",
297
+ writable: false,
298
+ config: EMPTY_CONFIG,
299
+ error: null
300
+ };
301
+ let form = null;
302
+ let unsubscribe = null;
303
+ const listeners = /* @__PURE__ */ new Set();
304
+ const publish = (next) => {
305
+ if (next.status === state.status && next.writable === state.writable && next.error === state.error && next.config === state.config) return;
306
+ state = next;
307
+ for (const listener of listeners) listener();
308
+ };
309
+ const sync = () => {
310
+ if (form === null) {
311
+ publish({
312
+ status: "unavailable",
313
+ writable: false,
314
+ config: state.config,
315
+ error: state.error
316
+ });
317
+ return;
318
+ }
319
+ const snapshot = form.getSnapshot();
320
+ const ready = snapshot.status === "ready" && snapshot.value !== void 0;
321
+ publish({
322
+ status: ready ? "ready" : snapshot.status === "unavailable" ? "unavailable" : "loading",
323
+ writable: snapshot.writable === true,
324
+ config: ready ? normalizeClientPricing(snapshot.value) : state.config,
325
+ error: state.error
326
+ });
327
+ };
328
+ return {
329
+ store: {
330
+ subscribe(listener) {
331
+ listeners.add(listener);
332
+ return () => {
333
+ listeners.delete(listener);
334
+ };
335
+ },
336
+ getSnapshot: () => state
337
+ },
338
+ attach(next) {
339
+ unsubscribe?.();
340
+ unsubscribe = null;
341
+ form = next;
342
+ if (form !== null) {
343
+ unsubscribe = form.subscribe(sync);
344
+ publish({
345
+ ...state,
346
+ error: null
347
+ });
348
+ }
349
+ sync();
350
+ return () => {
351
+ unsubscribe?.();
352
+ unsubscribe = null;
353
+ form = null;
354
+ };
355
+ },
356
+ async save(config) {
357
+ if (form === null) {
358
+ publish({
359
+ ...state,
360
+ error: "宿主没有提供配置表单服务,规则只能导出 / 导入,无法保存"
361
+ });
362
+ return false;
363
+ }
364
+ publish({
365
+ ...state,
366
+ error: null
367
+ });
368
+ let accepted = false;
369
+ try {
370
+ accepted = await form.mutate([{
371
+ op: "set",
372
+ path: ["pricing"],
373
+ value: configForWrite(config)
374
+ }]);
375
+ } catch (error) {
376
+ publish({
377
+ ...state,
378
+ error: "写入失败:" + (error instanceof Error ? error.message : String(error))
379
+ });
380
+ return false;
381
+ }
382
+ if (!accepted) publish({
383
+ ...state,
384
+ error: "写入被宿主拒绝(可能是规则在别处被同时修改,请重试)"
385
+ });
386
+ return accepted;
387
+ },
388
+ isServed: () => state.status !== "unavailable",
389
+ noteError(message) {
390
+ publish({
391
+ ...state,
392
+ error: message
393
+ });
394
+ }
395
+ };
396
+ }
397
+ /**
398
+ * 进程内单例:插件在页面里只有一个实例,index.tsx 在 apply 时把它接到 `configForms` 上,
399
+ * 组件用 useSyncExternalStore 订阅即可(避免把服务对象一层层透传进 React 树)。
400
+ */
401
+ const pricingSettings = createPricingSettings();
402
+ //#endregion
9
403
  //#region src/client/UsageIcon.tsx
10
404
  function UsageIcon({ size = 18 }) {
11
405
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
@@ -46,12 +440,25 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
46
440
  }
47
441
  //#endregion
48
442
  //#region src/client/api.ts
443
+ /**
444
+ * 最近一次请求失败的原因,按路径记住。
445
+ * 空态文案需要能指向「怎么做」——最常见的失败是新端点 404(服务端代码要重启宿主才生效)。
446
+ */
447
+ const failureReasons = /* @__PURE__ */ new Map();
448
+ function failureReasonOf(path) {
449
+ return failureReasons.get(path) ?? null;
450
+ }
49
451
  async function fetchJson(path) {
50
452
  try {
51
453
  const response = await fetch(path, { headers: { Accept: "application/json" } });
52
- if (!response.ok) return null;
454
+ if (!response.ok) {
455
+ failureReasons.set(path, response.status === 404 ? "HTTP 404:宿主没有这个端点 —— 服务端改动需要退出重开 DSH(刷新页面不够)" : "HTTP " + String(response.status));
456
+ return null;
457
+ }
458
+ failureReasons.delete(path);
53
459
  return await response.json();
54
- } catch {
460
+ } catch (error) {
461
+ failureReasons.set(path, "请求失败:" + (error instanceof Error ? error.message : String(error)));
55
462
  return null;
56
463
  }
57
464
  }
@@ -88,6 +495,13 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
88
495
  function loadProjects(limit = 200) {
89
496
  return fetchJson("/dsh-key-usage/projects?limit=" + String(limit));
90
497
  }
498
+ function loadModels(limit = 200) {
499
+ return fetchJson("/dsh-key-usage/models?limit=" + String(limit));
500
+ }
501
+ /** 计费规则面板的服务端对照:内置模板、错峰状态与近 30 天覆盖度。 */
502
+ function loadPricing() {
503
+ return fetchJson("/dsh-key-usage/pricing");
504
+ }
91
505
  //#endregion
92
506
  //#region src/client/format.ts
93
507
  /** 展示层格式化:全部与统计口径无关,只负责把数字变成界面文本。 */
@@ -147,64 +561,65 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
147
561
  return `${Math.round(minutes / 1440)} 天前`;
148
562
  }
149
563
  //#endregion
150
- //#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) {
564
+ //#region \0dsh-css:src/client/styles/shared.module.css.mjs
565
+ const css$8 = ".d29QNW_section{color:var(--dsw-alias-label-primary,#e8e8ec);font-size:13.5px;line-height:1.55}.d29QNW_title{justify-content:space-between;align-items:center;gap:10px;margin-bottom:12px;font-size:18px;font-weight:650;display:flex}.d29QNW_titleActions{align-items:center;gap:8px;display:inline-flex}.d29QNW_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}.d29QNW_refreshBtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#80808c1a);color:var(--dsw-alias-label-primary,#e8e8ec)}.d29QNW_refreshBtn:disabled{opacity:.6;cursor:default}.d29QNW_tabBar{border-bottom:1px solid var(--dsw-alias-border-l1,#2c2c34);flex-wrap:wrap;gap:2px;margin-bottom:18px;display:flex}.d29QNW_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}.d29QNW_tabBar button:hover{color:var(--dsw-alias-label-primary,#e8e8ec)}.d29QNW_tabBar button.d29QNW_active{color:var(--dsw-alias-state-accent-primary,#5b8cff);border-bottom-color:var(--dsw-alias-state-accent-primary,#5b8cff);font-weight:600}.d29QNW_card{background:var(--dsw-alias-bg-layer-2,#1d1d22);border:1px solid var(--dsw-alias-border-l1,#2c2c34);border-radius:12px;padding:14px 16px}.d29QNW_cardGrid3{grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-bottom:12px;display:grid}.d29QNW_cardGrid4{grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-bottom:18px;display:grid}.d29QNW_cardGridAuto{grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:10px;margin-bottom:18px;display:grid}.d29QNW_cardGrid6{grid-template-columns:repeat(6,minmax(0,1fr));gap:10px;margin-bottom:18px;display:grid}@media (max-width:1180px){.d29QNW_cardGrid6{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (max-width:720px){.d29QNW_cardGrid6,.d29QNW_cardGrid4{grid-template-columns:repeat(2,minmax(0,1fr))}}.d29QNW_statLabel{color:var(--dsw-alias-label-tertiary,#67676f);margin-bottom:6px;font-size:11.5px}.d29QNW_statValue{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary,#e8e8ec);font-size:20px;font-weight:650}.d29QNW_statExtra{color:var(--dsw-alias-label-tertiary,#67676f);margin-top:4px;font-size:11.5px}.d29QNW_groupLabel{color:var(--dsw-alias-label-secondary,#9a9aa6);margin:4px 0 10px;font-size:12.5px;font-weight:600}.d29QNW_blockLabel{color:var(--dsw-alias-label-secondary,#9a9aa6);margin:18px 0 4px;font-size:11.5px}.d29QNW_table{border-collapse:collapse;width:100%;margin-top:10px}.d29QNW_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}.d29QNW_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}.d29QNW_table th:first-child,.d29QNW_table td:first-child{text-align:left;padding-left:0}.d29QNW_table td:first-child{color:var(--dsw-alias-label-primary,#e8e8ec)}.d29QNW_table th.d29QNW_sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.d29QNW_table th.d29QNW_sortable:hover{color:var(--dsw-alias-label-secondary,#9a9aa6)}.d29QNW_table th.d29QNW_sortActive{color:var(--dsw-alias-state-accent-primary,#5b8cff)}.d29QNW_sortArrow{opacity:.8;margin-left:3px;font-size:8px}.d29QNW_table tr.d29QNW_clickable{cursor:pointer}.d29QNW_table tr.d29QNW_clickable:hover{background:var(--dsw-alias-interactive-bg-hover,#80808c12)}.d29QNW_table tr.d29QNW_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}.d29QNW_table tr.d29QNW_disabled{opacity:.55}.d29QNW_chipRow{flex-wrap:wrap;gap:8px;margin-bottom:14px;display:flex}.d29QNW_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}.d29QNW_chip:hover{color:var(--dsw-alias-label-primary,#e8e8ec);border-color:var(--dsw-alias-label-tertiary,#67676f)}.d29QNW_chip.d29QNW_active{background:var(--dsw-alias-interactive-bg-active,#5b8cff24);color:var(--dsw-alias-state-accent-primary,#5b8cff);border-color:#5b8cff66;font-weight:600}.d29QNW_statusDot{border-radius:50%;flex-shrink:0;width:8px;height:8px;display:inline-block}.d29QNW_dim{color:var(--dsw-alias-label-tertiary,#67676f)}.d29QNW_mono{color:var(--dsw-alias-label-secondary,#9a9aa6);font-family:ui-monospace,Consolas,monospace;font-size:11px}.d29QNW_empty{color:var(--dsw-alias-label-tertiary,#67676f);padding:18px 0}.d29QNW_warning{color:var(--dsw-alias-state-warning-primary,#e0a13c);margin-bottom:10px;font-size:11.5px}.d29QNW_legend{color:var(--dsw-alias-label-tertiary,#67676f);flex-wrap:wrap;align-items:center;gap:10px;font-size:10.5px;display:inline-flex}.d29QNW_legend i{border-radius:2px;width:10px;height:10px;margin-right:3px;display:inline-block}.d29QNW_legend i.d29QNW_ghost{background:#80808c1a;border:1px solid #80808c47}.d29QNW_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}.d29QNW_footnote b{color:var(--dsw-alias-label-secondary,#9a9aa6);font-weight:500}.d29QNW_footnoteList{margin:4px 0 0;padding-left:18px}.d29QNW_footnoteList li{margin:1px 0}.d29QNW_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}.d29QNW_tipTitle{margin-bottom:2px;font-size:12px;font-weight:650}.d29QNW_tipSub{color:var(--dsw-alias-label-secondary,#9a9aa6);margin-bottom:8px;font-size:11.5px}.d29QNW_tipRow{color:var(--dsw-alias-label-secondary,#9a9aa6);align-items:center;gap:6px;padding:2px 0;font-size:11px;display:flex}.d29QNW_tipRow i{border-radius:2px;flex-shrink:0;width:7px;height:7px}.d29QNW_tipName{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.d29QNW_tipAmount{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-tertiary,#67676f);text-align:right;width:58px}.d29QNW_tipRate{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary,#e8e8ec);font-weight:600}.d29QNW_tipBar{background:var(--dsw-alias-fill-l1,#80808c2e);border-radius:2px;height:3px;margin:1px 0 5px;overflow:hidden}.d29QNW_tipBar>i{border-radius:2px;height:100%;display:block}.d29QNW_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}";
566
+ const tagId$8 = "dsh-key-usage/shared.module.css";
567
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$8) + "]") === null) {
154
568
  const tag = document.createElement("style");
155
569
  tag.dataset.plugin = "dsh-key-usage";
156
- tag.dataset.pluginCss = tagId$5;
157
- tag.textContent = css$5;
570
+ tag.dataset.pluginCss = tagId$8;
571
+ tag.textContent = css$8;
158
572
  document.head.appendChild(tag);
159
573
  }
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
574
+ const cssRegistry$8 = globalThis.__DSH_PLUGIN_CSS__ ??= [];
575
+ if (!cssRegistry$8.some((e) => e.tagId === tagId$8)) cssRegistry$8.push({
576
+ tagId: tagId$8,
577
+ css: css$8
164
578
  });
165
579
  var shared_module_css_default = {
166
- "ghost": "LhdpGW_ghost",
167
- "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
- "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
- "statusDot": "LhdpGW_statusDot",
184
- "tipSub": "LhdpGW_tipSub",
185
- "tipTitle": "LhdpGW_tipTitle",
186
- "statLabel": "LhdpGW_statLabel",
187
- "tipRate": "LhdpGW_tipRate",
188
- "tipName": "LhdpGW_tipName",
189
- "statValue": "LhdpGW_statValue",
190
- "total": "LhdpGW_total",
191
- "blockLabel": "LhdpGW_blockLabel",
192
- "sortArrow": "LhdpGW_sortArrow",
193
- "tipRow": "LhdpGW_tipRow",
194
- "section": "LhdpGW_section",
195
- "cardGrid3": "LhdpGW_cardGrid3",
196
- "active": "LhdpGW_active",
197
- "sortActive": "LhdpGW_sortActive",
198
- "dim": "LhdpGW_dim",
199
- "table": "LhdpGW_table",
200
- "sortable": "LhdpGW_sortable",
201
- "legend": "LhdpGW_legend",
202
- "tooltip": "LhdpGW_tooltip",
203
- "tipBar": "LhdpGW_tipBar",
204
- "mono": "LhdpGW_mono",
205
- "tabBar": "LhdpGW_tabBar",
206
- "card": "LhdpGW_card",
207
- "chip": "LhdpGW_chip"
580
+ "active": "d29QNW_active",
581
+ "blockLabel": "d29QNW_blockLabel",
582
+ "card": "d29QNW_card",
583
+ "cardGrid3": "d29QNW_cardGrid3",
584
+ "cardGrid4": "d29QNW_cardGrid4",
585
+ "cardGrid6": "d29QNW_cardGrid6",
586
+ "cardGridAuto": "d29QNW_cardGridAuto",
587
+ "chip": "d29QNW_chip",
588
+ "chipRow": "d29QNW_chipRow",
589
+ "clickable": "d29QNW_clickable",
590
+ "dim": "d29QNW_dim",
591
+ "disabled": "d29QNW_disabled",
592
+ "empty": "d29QNW_empty",
593
+ "footnote": "d29QNW_footnote",
594
+ "footnoteList": "d29QNW_footnoteList",
595
+ "ghost": "d29QNW_ghost",
596
+ "groupLabel": "d29QNW_groupLabel",
597
+ "legend": "d29QNW_legend",
598
+ "mono": "d29QNW_mono",
599
+ "refreshBtn": "d29QNW_refreshBtn",
600
+ "section": "d29QNW_section",
601
+ "sortActive": "d29QNW_sortActive",
602
+ "sortArrow": "d29QNW_sortArrow",
603
+ "sortable": "d29QNW_sortable",
604
+ "statExtra": "d29QNW_statExtra",
605
+ "statLabel": "d29QNW_statLabel",
606
+ "statValue": "d29QNW_statValue",
607
+ "statusDot": "d29QNW_statusDot",
608
+ "tabBar": "d29QNW_tabBar",
609
+ "table": "d29QNW_table",
610
+ "tipAmount": "d29QNW_tipAmount",
611
+ "tipBar": "d29QNW_tipBar",
612
+ "tipDivider": "d29QNW_tipDivider",
613
+ "tipName": "d29QNW_tipName",
614
+ "tipRate": "d29QNW_tipRate",
615
+ "tipRow": "d29QNW_tipRow",
616
+ "tipSub": "d29QNW_tipSub",
617
+ "tipTitle": "d29QNW_tipTitle",
618
+ "title": "d29QNW_title",
619
+ "titleActions": "d29QNW_titleActions",
620
+ "tooltip": "d29QNW_tooltip",
621
+ "total": "d29QNW_total",
622
+ "warning": "d29QNW_warning"
208
623
  };
209
624
  //#endregion
210
625
  //#region src/client/tooltip.tsx
@@ -340,6 +755,13 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
340
755
  dim: true
341
756
  };
342
757
  }
758
+ /**
759
+ * 单元格是否该挂来源角标:只有真的显示了金额(`¥…`)时才挂,
760
+ * `订阅内` / `免费` / `无计费` / `—` 都是占位文案,挂角标只是噪音。
761
+ */
762
+ function showsAmount(cell) {
763
+ return cell.text.startsWith("¥");
764
+ }
343
765
  /** 状态点的颜色(走宿主告警/成功令牌)。 */
344
766
  function statusColorOf(status) {
345
767
  if (status === "ok") return "var(--dsw-alias-state-success-primary, #3fb96f)";
@@ -354,52 +776,52 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
354
776
  return "var(--dsw-alias-state-success-primary, #3fb96f)";
355
777
  }
356
778
  //#endregion
357
- //#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) {
779
+ //#region \0dsh-css:src/client/styles/overview.module.css.mjs
780
+ const css$7 = ".tXjxsq_keyTable{border-collapse:collapse;width:100%}.tXjxsq_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}.tXjxsq_keyTable th:first-child{text-align:left;padding-left:0}.tXjxsq_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}.tXjxsq_keyTable td:first-child{text-align:left;padding-left:0}.tXjxsq_keyTable tr:last-child td{border-bottom:0}.tXjxsq_keyTable tbody tr.tXjxsq_clickable{cursor:pointer}.tXjxsq_keyTable tbody tr.tXjxsq_clickable:hover{background:var(--dsw-alias-interactive-bg-hover,#80808c12)}.tXjxsq_keyTable tr.tXjxsq_disabledRow{opacity:.55}.tXjxsq_cellKey{align-items:center;gap:10px;display:flex}.tXjxsq_keyDisplay{color:var(--dsw-alias-label-primary,#e8e8ec);font-size:13px;font-weight:600}.tXjxsq_keyEnv{color:var(--dsw-alias-label-tertiary,#67676f);margin-top:1px;font-family:ui-monospace,Consolas,monospace;font-size:10px}.tXjxsq_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}.tXjxsq_tag.tXjxsq_free{color:var(--dsw-alias-state-success-primary,#3fb96f);border-color:#3fb96f59}.tXjxsq_tag.tXjxsq_sub{color:var(--dsw-alias-state-accent-primary,#5b8cff);border-color:#5b8cff59}.tXjxsq_tag.tXjxsq_off{color:var(--dsw-alias-label-tertiary,#67676f)}.tXjxsq_rowLink{color:var(--dsw-alias-label-tertiary,#67676f);font-size:11px}.tXjxsq_keyTable tbody tr.tXjxsq_clickable:hover .tXjxsq_rowLink{color:var(--dsw-alias-state-accent-primary,#5b8cff)}.tXjxsq_quotaCell{align-items:center;gap:8px;display:inline-flex}.tXjxsq_miniBar{background:var(--dsw-alias-fill-l2,#80808c2e);border-radius:3px;flex-shrink:0;width:64px;height:5px;overflow:hidden}.tXjxsq_miniBar>i{border-radius:3px;height:100%;display:block}.tXjxsq_quotaText{text-align:left;min-width:74px}.tXjxsq_quotaText.tXjxsq_warn{color:var(--dsw-alias-state-warning-primary,#e0a13c);font-weight:600}.tXjxsq_quotaText.tXjxsq_danger{color:var(--dsw-alias-state-danger-primary,#e05a5a);font-weight:600}.tXjxsq_quotaText.tXjxsq_dim{color:var(--dsw-alias-label-tertiary,#67676f)}.tXjxsq_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}.tXjxsq_heatmapHead{flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:6px;margin-bottom:10px;display:flex}.tXjxsq_heatmapTitle{color:var(--dsw-alias-label-secondary,#9a9aa6);font-size:11.5px}.tXjxsq_heatmapScroll{padding-bottom:4px;overflow-x:auto}.tXjxsq_heatmapBody{gap:6px;width:max-content;display:flex}.tXjxsq_heatWeekdays{flex-direction:column;flex-shrink:0;gap:3px;display:flex}.tXjxsq_heatWeekdays span{color:var(--dsw-alias-label-tertiary,#67676f);text-align:right;width:13px;height:28px;font-size:9.5px;line-height:28px}.tXjxsq_heatGridCol{flex-direction:column;display:flex}.tXjxsq_heatGrid{gap:3px;display:flex}.tXjxsq_heatWeek{flex-direction:column;gap:3px;display:flex}.tXjxsq_heatCell{box-sizing:border-box;background:var(--dsw-alias-fill-l1,#80808c2e);cursor:default;border-radius:5px;flex-shrink:0;width:28px;height:28px}.tXjxsq_heatCell.tXjxsq_idle{background:#80808c38}.tXjxsq_heatCell.tXjxsq_empty{background:#80808c1a;box-shadow:inset 0 0 0 1px #80808c47}.tXjxsq_heatMonths{gap:3px;margin-top:6px;display:flex}.tXjxsq_heatMonths span{width:28px;color:var(--dsw-alias-label-tertiary,#67676f);text-align:left;white-space:nowrap;flex-shrink:0;font-size:10px}";
781
+ const tagId$7 = "dsh-key-usage/overview.module.css";
782
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$7) + "]") === null) {
361
783
  const tag = document.createElement("style");
362
784
  tag.dataset.plugin = "dsh-key-usage";
363
- tag.dataset.pluginCss = tagId$4;
364
- tag.textContent = css$4;
785
+ tag.dataset.pluginCss = tagId$7;
786
+ tag.textContent = css$7;
365
787
  document.head.appendChild(tag);
366
788
  }
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
789
+ const cssRegistry$7 = globalThis.__DSH_PLUGIN_CSS__ ??= [];
790
+ if (!cssRegistry$7.some((e) => e.tagId === tagId$7)) cssRegistry$7.push({
791
+ tagId: tagId$7,
792
+ css: css$7
371
793
  });
372
794
  var overview_module_css_default = {
373
- "danger": "J7Idbq_danger",
374
- "rowLink": "J7Idbq_rowLink",
375
- "heatmapCard": "J7Idbq_heatmapCard",
376
- "quotaCell": "J7Idbq_quotaCell",
377
- "disabledRow": "J7Idbq_disabledRow",
378
- "idle": "J7Idbq_idle",
379
- "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
- "heatWeek": "J7Idbq_heatWeek",
387
- "dim": "J7Idbq_dim",
388
- "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"
795
+ "cellKey": "tXjxsq_cellKey",
796
+ "clickable": "tXjxsq_clickable",
797
+ "danger": "tXjxsq_danger",
798
+ "dim": "tXjxsq_dim",
799
+ "disabledRow": "tXjxsq_disabledRow",
800
+ "empty": "tXjxsq_empty",
801
+ "free": "tXjxsq_free",
802
+ "heatCell": "tXjxsq_heatCell",
803
+ "heatGrid": "tXjxsq_heatGrid",
804
+ "heatGridCol": "tXjxsq_heatGridCol",
805
+ "heatMonths": "tXjxsq_heatMonths",
806
+ "heatWeek": "tXjxsq_heatWeek",
807
+ "heatWeekdays": "tXjxsq_heatWeekdays",
808
+ "heatmapBody": "tXjxsq_heatmapBody",
809
+ "heatmapCard": "tXjxsq_heatmapCard",
810
+ "heatmapHead": "tXjxsq_heatmapHead",
811
+ "heatmapScroll": "tXjxsq_heatmapScroll",
812
+ "heatmapTitle": "tXjxsq_heatmapTitle",
813
+ "idle": "tXjxsq_idle",
814
+ "keyDisplay": "tXjxsq_keyDisplay",
815
+ "keyEnv": "tXjxsq_keyEnv",
816
+ "keyTable": "tXjxsq_keyTable",
817
+ "miniBar": "tXjxsq_miniBar",
818
+ "off": "tXjxsq_off",
819
+ "quotaCell": "tXjxsq_quotaCell",
820
+ "quotaText": "tXjxsq_quotaText",
821
+ "rowLink": "tXjxsq_rowLink",
822
+ "sub": "tXjxsq_sub",
823
+ "tag": "tXjxsq_tag",
824
+ "warn": "tXjxsq_warn"
403
825
  };
404
826
  //#endregion
405
827
  //#region src/client/components/Heatmap.tsx
@@ -409,7 +831,7 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
409
831
  * 具体数值并入页面底部的口径说明,不再单独占一行。
410
832
  * 格子用固定方形尺寸 + 横向滚动,保证「有数据 / 无数据 / 无用量」三类格子完全等大。
411
833
  */
412
- const DAY_MS = 864e5;
834
+ const DAY_MS$1 = 864e5;
413
835
  function levelThresholds(days) {
414
836
  const active = days.map((day) => day.totalTokens).filter((tokens) => tokens > 0).sort((first, second) => first - second);
415
837
  if (active.length < 3) return [5e7, 2e8];
@@ -435,12 +857,12 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
435
857
  const { columns, monthLabels } = (0, react.useMemo)(() => {
436
858
  const today = /* @__PURE__ */ new Date();
437
859
  today.setHours(0, 0, 0, 0);
438
- const endSaturday = new Date(today.getTime() + (6 - today.getDay()) * DAY_MS);
439
- const start = /* @__PURE__ */ new Date(endSaturday.getTime() - 272 * DAY_MS);
860
+ const endSaturday = new Date(today.getTime() + (6 - today.getDay()) * DAY_MS$1);
861
+ const start = /* @__PURE__ */ new Date(endSaturday.getTime() - 272 * DAY_MS$1);
440
862
  const dataStart = days.length > 0 ? days[0].date : null;
441
863
  const cells = [];
442
864
  for (let offset = 0; offset < 273; offset += 1) {
443
- const date = new Date(start.getTime() + offset * DAY_MS);
865
+ const date = new Date(start.getTime() + offset * DAY_MS$1);
444
866
  const dateKey = isoDate(date);
445
867
  if (date.getTime() > today.getTime()) {
446
868
  cells.push({
@@ -487,6 +909,12 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
487
909
  monthLabels: labels
488
910
  };
489
911
  }, [days, heatmapByDate]);
912
+ const scrollRef = (0, react.useRef)(null);
913
+ (0, react.useEffect)(() => {
914
+ const node = scrollRef.current;
915
+ if (node === null) return;
916
+ node.scrollLeft = node.scrollWidth - node.clientWidth;
917
+ }, []);
490
918
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
491
919
  className: overview_module_css_default.heatmapCard,
492
920
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -506,6 +934,7 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
506
934
  })]
507
935
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
508
936
  className: overview_module_css_default.heatmapScroll,
937
+ ref: scrollRef,
509
938
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
510
939
  className: overview_module_css_default.heatmapBody,
511
940
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -573,6 +1002,97 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
573
1002
  ] });
574
1003
  }
575
1004
  //#endregion
1005
+ //#region \0dsh-css:src/client/styles/pricing.module.css.mjs
1006
+ const css$6 = ".aymcxa_panel{border-top:1px solid var(--dsw-alias-border-l1,#2c2c34);margin-top:26px;padding-top:16px}.aymcxa_panelHead{flex-wrap:wrap;align-items:baseline;gap:10px;margin-bottom:4px;display:flex}.aymcxa_panelTitle{color:var(--dsw-alias-label-primary,#e8e8ec);font-size:14px;font-weight:650}.aymcxa_panelHint{color:var(--dsw-alias-label-tertiary,#67676f);font-size:11px}.aymcxa_panelNote{color:var(--dsw-alias-label-tertiary,#67676f);margin:6px 0 12px;font-size:11.5px;line-height:1.7}.aymcxa_statStrip{flex-wrap:wrap;gap:10px 26px;margin-bottom:12px;display:flex}.aymcxa_statItem .aymcxa_statName{color:var(--dsw-alias-label-tertiary,#67676f);margin-bottom:2px;font-size:11px}.aymcxa_statItem .aymcxa_statNum{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary,#e8e8ec);font-size:15px;font-weight:650}.aymcxa_statItem .aymcxa_statNum.aymcxa_saving{color:var(--dsw-alias-state-success-primary,#3fb96f)}.aymcxa_toolbar{flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:10px;display:flex}.aymcxa_actionBtn{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:6px;padding:4px 11px;font-size:11.5px}.aymcxa_actionBtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#80808c1a);color:var(--dsw-alias-label-primary,#e8e8ec)}.aymcxa_actionBtn:disabled{opacity:.5;cursor:default}.aymcxa_actionBtn.aymcxa_primary{color:var(--dsw-alias-state-accent-primary,#5b8cff);border-color:#5b8cff73}.aymcxa_toolbarSpacer{flex:1}.aymcxa_ruleTable{border-collapse:collapse;width:100%;margin-top:4px}.aymcxa_ruleTable 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}.aymcxa_ruleTable td{text-align:right;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary,#9a9aa6);border-bottom:1px solid var(--dsw-alias-border-l1,#2c2c34);white-space:nowrap;padding:7px 8px;font-size:11.5px}.aymcxa_ruleTable th:first-child,.aymcxa_ruleTable td:first-child,.aymcxa_ruleTable th:nth-child(2),.aymcxa_ruleTable td:nth-child(2){text-align:left}.aymcxa_ruleTable td:first-child{color:var(--dsw-alias-label-primary,#e8e8ec)}.aymcxa_ruleTable tr.aymcxa_editing{background:var(--dsw-alias-interactive-bg-active,#5b8cff24)}.aymcxa_ruleName{font-family:ui-monospace,Consolas,monospace;font-size:11px}.aymcxa_ruleNote{color:var(--dsw-alias-label-tertiary,#67676f);white-space:normal;max-width:260px;font-size:10.5px}.aymcxa_linkBtn{color:var(--dsw-alias-label-tertiary,#67676f);font:inherit;cursor:pointer;background:0 0;border:0;padding:0 6px;font-size:11px}.aymcxa_linkBtn:hover{color:var(--dsw-alias-state-accent-primary,#5b8cff)}.aymcxa_sourceChip{vertical-align:baseline;white-space:nowrap;border:1px solid;border-radius:999px;margin-left:5px;padding:0 6px;font-size:10px;line-height:1.7;display:inline-block}.aymcxa_sourceOfficial{color:var(--dsw-alias-label-tertiary,#67676f)}.aymcxa_sourceRule{color:var(--dsw-alias-state-accent-primary,#5b8cff)}.aymcxa_sourceCatalog{color:var(--dsw-alias-label-secondary,#9a9aa2)}.aymcxa_sourceEstimate{color:var(--dsw-alias-state-warning-primary,#e0a13c)}.aymcxa_catalogNote{color:var(--dsw-alias-label-secondary,#9a9aa2);background:var(--dsw-alias-bg-layer-2,#1d1d22);border:1px solid var(--dsw-alias-border-l1,#2c2c34);border-radius:8px;margin:10px 0;padding:8px 12px;font-size:12px;line-height:1.7}.aymcxa_offPeakChip{border:1px solid var(--dsw-alias-state-success-primary,#3fb96f);color:var(--dsw-alias-state-success-primary,#3fb96f);border-radius:999px;padding:0 6px;font-size:10px;line-height:1.7;display:inline-block}.aymcxa_editor{background:var(--dsw-alias-bg-layer-2,#1d1d22);border:1px solid var(--dsw-alias-border-l1,#2c2c34);border-radius:10px;margin:10px 0;padding:12px 14px}.aymcxa_editorGrid{grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px;display:grid}.aymcxa_field{flex-direction:column;gap:4px;display:flex}.aymcxa_field label{color:var(--dsw-alias-label-tertiary,#67676f);font-size:10.5px}.aymcxa_field input,.aymcxa_field select{background:var(--dsw-alias-bg-layer-3,#232329);border:1px solid var(--dsw-alias-border-l2,#2c2c34);color:var(--dsw-alias-label-primary,#e8e8ec);font:inherit;border-radius:6px;min-width:0;padding:4px 7px;font-size:11.5px}.aymcxa_field input:focus,.aymcxa_field select:focus{border-color:var(--dsw-alias-state-accent-primary,#5b8cff);outline:none}.aymcxa_fieldWide{grid-column:span 2}.aymcxa_subLabel{color:var(--dsw-alias-label-secondary,#9a9aa6);margin:12px 0 6px;font-size:11.5px;font-weight:600}.aymcxa_windowRow{align-items:center;gap:8px;margin-bottom:6px;display:flex}.aymcxa_windowRow input{width:84px}.aymcxa_editorError{color:var(--dsw-alias-state-danger-primary,#e05a5a);margin-top:10px;font-size:11.5px}.aymcxa_editorHint{color:var(--dsw-alias-label-tertiary,#67676f);margin-top:8px;font-size:10.5px}.aymcxa_editorActions{gap:8px;margin-top:12px;display:flex}.aymcxa_empty{color:var(--dsw-alias-label-tertiary,#67676f);padding:12px 0;font-size:12px}.aymcxa_warning{color:var(--dsw-alias-state-warning-primary,#e0a13c);margin:6px 0;font-size:11.5px}.aymcxa_error{color:var(--dsw-alias-state-danger-primary,#e05a5a);margin:6px 0;font-size:11.5px}.aymcxa_savingsList{color:var(--dsw-alias-label-tertiary,#67676f);margin:6px 0 0;padding:0;font-size:11.5px;line-height:1.7;list-style:none}.aymcxa_hiddenFile{display:none}.aymcxa_summaryStrip{background:var(--dsw-alias-bg-layer-2,#1d1d22);border:1px solid var(--dsw-alias-border-l1,#2c2c34);border-radius:12px;flex-wrap:wrap;align-items:center;gap:12px 28px;padding:12px 16px;display:flex}.aymcxa_summaryBasis{flex:320px;min-width:0}.aymcxa_summarySaving{flex:none}.aymcxa_summaryActions{align-items:center;gap:8px;margin-left:auto;display:flex}.aymcxa_summaryStrip .aymcxa_summaryName{color:var(--dsw-alias-label-tertiary,#67676f);margin-bottom:3px;font-size:11px}.aymcxa_summaryStrip .aymcxa_summaryValue{color:var(--dsw-alias-label-primary,#e8e8ec);font-variant-numeric:tabular-nums;font-size:12.5px;font-weight:600;line-height:1.5}.aymcxa_summaryStrip .aymcxa_summaryValue.aymcxa_saving{color:var(--dsw-alias-state-success-primary,#3fb96f)}.aymcxa_infoBtn{border:1px solid var(--dsw-alias-border-l2,#2c2c34);color:var(--dsw-alias-label-tertiary,#67676f);width:22px;height:22px;font:inherit;cursor:pointer;background:0 0;border-radius:999px;padding:0;font-size:12px;line-height:1}.aymcxa_infoBtn:hover,.aymcxa_infoBtnActive{color:var(--dsw-alias-state-accent-primary,#5b8cff);border-color:#5b8cff73}.aymcxa_explanation{background:var(--dsw-alias-bg-layer-2,#1d1d22);border:1px solid var(--dsw-alias-border-l1,#2c2c34);border-radius:10px;margin-top:8px;padding:10px 14px}.aymcxa_explanationTitle{color:var(--dsw-alias-label-secondary,#9a9aa6);margin-bottom:4px;font-size:11.5px;font-weight:600}.aymcxa_explanationList{color:var(--dsw-alias-label-tertiary,#67676f);margin:0;padding-left:18px;font-size:11.5px;line-height:1.75}.aymcxa_explanationList b{color:var(--dsw-alias-label-secondary,#9a9aa6);font-weight:500}.aymcxa_drawer{border:1px solid var(--dsw-alias-border-l1,#2c2c34);border-radius:12px;margin-top:8px;padding:12px 16px 14px}.aymcxa_sectionBlock{border-top:1px solid var(--dsw-alias-border-l1,#2c2c34);margin-top:16px;padding-top:12px}.aymcxa_statItem .aymcxa_statNote{color:var(--dsw-alias-label-tertiary,#67676f);margin-top:3px;font-size:10.5px}.aymcxa_worthList{margin:6px 0 0;padding:0;font-size:11.5px;line-height:1.9;list-style:none}.aymcxa_worthList li,.aymcxa_savingsList li{flex-wrap:wrap;align-items:baseline;gap:8px;display:flex}.aymcxa_worthMeta{color:var(--dsw-alias-label-tertiary,#67676f);font-variant-numeric:tabular-nums}.aymcxa_savingsList .aymcxa_saving{color:var(--dsw-alias-state-success-primary,#3fb96f);margin-left:auto;font-weight:600}.aymcxa_linkBtn:disabled{opacity:.5;cursor:default}";
1007
+ const tagId$6 = "dsh-key-usage/pricing.module.css";
1008
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$6) + "]") === null) {
1009
+ const tag = document.createElement("style");
1010
+ tag.dataset.plugin = "dsh-key-usage";
1011
+ tag.dataset.pluginCss = tagId$6;
1012
+ tag.textContent = css$6;
1013
+ document.head.appendChild(tag);
1014
+ }
1015
+ const cssRegistry$6 = globalThis.__DSH_PLUGIN_CSS__ ??= [];
1016
+ if (!cssRegistry$6.some((e) => e.tagId === tagId$6)) cssRegistry$6.push({
1017
+ tagId: tagId$6,
1018
+ css: css$6
1019
+ });
1020
+ var pricing_module_css_default = {
1021
+ "actionBtn": "aymcxa_actionBtn",
1022
+ "catalogNote": "aymcxa_catalogNote",
1023
+ "drawer": "aymcxa_drawer",
1024
+ "editing": "aymcxa_editing",
1025
+ "editor": "aymcxa_editor",
1026
+ "editorActions": "aymcxa_editorActions",
1027
+ "editorError": "aymcxa_editorError",
1028
+ "editorGrid": "aymcxa_editorGrid",
1029
+ "editorHint": "aymcxa_editorHint",
1030
+ "empty": "aymcxa_empty",
1031
+ "error": "aymcxa_error",
1032
+ "explanation": "aymcxa_explanation",
1033
+ "explanationList": "aymcxa_explanationList",
1034
+ "explanationTitle": "aymcxa_explanationTitle",
1035
+ "field": "aymcxa_field",
1036
+ "fieldWide": "aymcxa_fieldWide",
1037
+ "hiddenFile": "aymcxa_hiddenFile",
1038
+ "infoBtn": "aymcxa_infoBtn",
1039
+ "infoBtnActive": "aymcxa_infoBtnActive",
1040
+ "linkBtn": "aymcxa_linkBtn",
1041
+ "offPeakChip": "aymcxa_offPeakChip",
1042
+ "panel": "aymcxa_panel",
1043
+ "panelHead": "aymcxa_panelHead",
1044
+ "panelHint": "aymcxa_panelHint",
1045
+ "panelNote": "aymcxa_panelNote",
1046
+ "panelTitle": "aymcxa_panelTitle",
1047
+ "primary": "aymcxa_primary",
1048
+ "ruleName": "aymcxa_ruleName",
1049
+ "ruleNote": "aymcxa_ruleNote",
1050
+ "ruleTable": "aymcxa_ruleTable",
1051
+ "saving": "aymcxa_saving",
1052
+ "savingsList": "aymcxa_savingsList",
1053
+ "sectionBlock": "aymcxa_sectionBlock",
1054
+ "sourceCatalog": "aymcxa_sourceCatalog",
1055
+ "sourceChip": "aymcxa_sourceChip",
1056
+ "sourceEstimate": "aymcxa_sourceEstimate",
1057
+ "sourceOfficial": "aymcxa_sourceOfficial",
1058
+ "sourceRule": "aymcxa_sourceRule",
1059
+ "statItem": "aymcxa_statItem",
1060
+ "statName": "aymcxa_statName",
1061
+ "statNote": "aymcxa_statNote",
1062
+ "statNum": "aymcxa_statNum",
1063
+ "statStrip": "aymcxa_statStrip",
1064
+ "subLabel": "aymcxa_subLabel",
1065
+ "summaryActions": "aymcxa_summaryActions",
1066
+ "summaryBasis": "aymcxa_summaryBasis",
1067
+ "summaryName": "aymcxa_summaryName",
1068
+ "summarySaving": "aymcxa_summarySaving",
1069
+ "summaryStrip": "aymcxa_summaryStrip",
1070
+ "summaryValue": "aymcxa_summaryValue",
1071
+ "toolbar": "aymcxa_toolbar",
1072
+ "toolbarSpacer": "aymcxa_toolbarSpacer",
1073
+ "warning": "aymcxa_warning",
1074
+ "windowRow": "aymcxa_windowRow",
1075
+ "worthList": "aymcxa_worthList",
1076
+ "worthMeta": "aymcxa_worthMeta"
1077
+ };
1078
+ //#endregion
1079
+ //#region src/client/components/CostSourceChip.tsx
1080
+ function CostSourceChip(props) {
1081
+ const source = props.source ?? "none";
1082
+ if (source === "none") return null;
1083
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1084
+ className: pricing_module_css_default.sourceChip + " " + sourceClass(source),
1085
+ title: COST_SOURCE_HINTS[source],
1086
+ children: COST_SOURCE_LABELS[source]
1087
+ });
1088
+ }
1089
+ function sourceClass(source) {
1090
+ if (source === "official") return pricing_module_css_default.sourceOfficial;
1091
+ if (source === "rule") return pricing_module_css_default.sourceRule;
1092
+ if (source === "catalog") return pricing_module_css_default.sourceCatalog;
1093
+ return pricing_module_css_default.sourceEstimate;
1094
+ }
1095
+ //#endregion
576
1096
  //#region src/client/tabs/OverviewTab.tsx
577
1097
  function OverviewTab(props) {
578
1098
  const { channels, summary } = props;
@@ -627,7 +1147,7 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
627
1147
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: costCell.dim ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
628
1148
  className: shared_module_css_default.dim,
629
1149
  children: costCell.text
630
- }) : costCell.text }),
1150
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [costCell.text, showsAmount(costCell) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CostSourceChip, { source: row?.month.costSource })] }) }),
631
1151
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: disabled ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
632
1152
  className: overview_module_css_default.rowLink,
633
1153
  children: "明细 →"
@@ -703,45 +1223,87 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
703
1223
  });
704
1224
  }
705
1225
  //#endregion
706
- //#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) {
1226
+ //#region src/client/export.ts
1227
+ /**
1228
+ * 导出工具:纯函数生成 CSV / JSON 文本 + 用 Blob 触发浏览器下载。
1229
+ * 零依赖、零服务端往返,导出的永远是**客户端当前筛选后**的数据。
1230
+ *
1231
+ * 两条口径约束:
1232
+ * - CSV 必须带 UTF-8 BOM:Excel 默认按本地代码页解码,没有 BOM 中文会变乱码;
1233
+ * - JSON 不能带 BOM:`JSON.parse` 遇到 BOM 会直接抛错。
1234
+ */
1235
+ const UTF8_BOM = "";
1236
+ const CSV_MIME = "text/csv;charset=utf-8";
1237
+ const JSON_MIME = "application/json;charset=utf-8";
1238
+ /** 表头 + 数据行 → CSV 文本(转义见 `csvCell`,换行用 RFC 4180 的 CRLF)。 */
1239
+ function toCsv(rows, columns) {
1240
+ const lines = [columns.map((column) => csvCell(column.label)).join(",")];
1241
+ for (const row of rows) lines.push(columns.map((column) => csvCell(column.value(row))).join(","));
1242
+ return UTF8_BOM + lines.join("\r\n") + "\r\n";
1243
+ }
1244
+ function toJson(value) {
1245
+ return JSON.stringify(value, null, 2) + "\n";
1246
+ }
1247
+ /** 下载文件名:`dsh-usage-<主题>-<当天>.<扩展名>`,主题用英文短名避免跨端中文文件名乱码。 */
1248
+ function exportFileName(topic, extension) {
1249
+ return "dsh-usage-" + topic + "-" + isoDate(/* @__PURE__ */ new Date()) + "." + extension;
1250
+ }
1251
+ function downloadFile(name, text, mime) {
1252
+ const objectUrl = URL.createObjectURL(new Blob([text], { type: mime }));
1253
+ const anchor = document.createElement("a");
1254
+ anchor.href = objectUrl;
1255
+ anchor.download = name;
1256
+ document.body.appendChild(anchor);
1257
+ anchor.click();
1258
+ anchor.remove();
1259
+ window.setTimeout(() => URL.revokeObjectURL(objectUrl), 0);
1260
+ }
1261
+ /** 逗号、引号、换行都必须包引号并把内部引号翻倍,否则标题里的逗号会撑破列。 */
1262
+ function csvCell(value) {
1263
+ if (value === null) return "";
1264
+ const text = String(value);
1265
+ return /[",\r\n]/.test(text) ? "\"" + text.replace(/"/g, "\"\"") + "\"" : text;
1266
+ }
1267
+ //#endregion
1268
+ //#region \0dsh-css:src/client/styles/detail.module.css.mjs
1269
+ const css$5 = "._91PKKq_rangeBar{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:10px;margin-bottom:12px;display:flex}._91PKKq_rangeLeft{flex-wrap:wrap;align-items:center;gap:10px;display:flex}._91PKKq_rangeTabs{border:1px solid var(--dsw-alias-border-l2,#2c2c34);border-radius:9px;gap:2px;padding:2px;display:inline-flex}._91PKKq_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}._91PKKq_rangeTabs button._91PKKq_active{background:var(--dsw-alias-interactive-bg-active,#5b8cff24);color:var(--dsw-alias-state-accent-primary,#5b8cff);font-weight:600}._91PKKq_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){._91PKKq_rangeInput{--lightningcss-light: ;--lightningcss-dark:initial}}._91PKKq_rangeCaption{color:var(--dsw-alias-label-tertiary,#67676f);margin-left:auto;font-size:11.5px}._91PKKq_shareBar{border-radius:6px;gap:2px;height:12px;margin:8px 0 2px;display:flex;overflow:hidden}._91PKKq_shareBar>i{cursor:default;height:100%;display:block}._91PKKq_shareBar>i:hover{filter:brightness(1.15)}._91PKKq_chartTitle{flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:10px;display:flex}._91PKKq_bars{align-items:flex-end;gap:3px;height:110px;margin-top:8px;display:flex}._91PKKq_barsDense{gap:2px}._91PKKq_barColumn{cursor:default;flex-direction:column;flex:1;justify-content:flex-end;gap:1px;height:100%;display:flex}._91PKKq_barColumn:hover{filter:brightness(1.2)}._91PKKq_segCache{background:var(--dsw-alias-state-accent-tertiary,#5b8cff47);border-radius:2px 2px 0 0}._91PKKq_segIn{background:var(--dsw-alias-state-accent-primary,#5b8cff)}._91PKKq_segOut{background:var(--dsw-alias-state-business-primary,#9a7bff);border-radius:0 0 2px 2px}._91PKKq_barLabels{margin-top:6px;display:flex}._91PKKq_barLabels span{text-align:center;color:var(--dsw-alias-label-tertiary,#67676f);white-space:nowrap;flex:1;font-size:9.5px}._91PKKq_subRowCell{background:var(--dsw-alias-fill-l1,#80808c0d);text-align:left!important;padding:2px 10px 10px 30px!important}._91PKKq_subHead,._91PKKq_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}._91PKKq_subHead{color:var(--dsw-alias-label-tertiary,#67676f);border-bottom:1px solid var(--dsw-alias-fill-l2,#80808c33);padding:4px 0;font-size:10px}._91PKKq_subRow{border-bottom:1px dashed var(--dsw-alias-fill-l1,#80808c24);padding:5px 0;font-size:11.5px}._91PKKq_subRow:last-child{border-bottom:0}._91PKKq_subName{flex-direction:column;gap:1px;min-width:0;display:flex}._91PKKq_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}._91PKKq_subChannel{color:var(--dsw-alias-label-tertiary,#67676f);text-overflow:ellipsis;white-space:nowrap;font-family:inherit;font-size:10px;overflow:hidden}._91PKKq_subNumber{text-align:right;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary,#9a9aa6)}._91PKKq_toggleArrow{width:13px;color:var(--dsw-alias-label-tertiary,#67676f);font-size:9px;display:inline-block}";
1270
+ const tagId$5 = "dsh-key-usage/detail.module.css";
1271
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$5) + "]") === null) {
710
1272
  const tag = document.createElement("style");
711
1273
  tag.dataset.plugin = "dsh-key-usage";
712
- tag.dataset.pluginCss = tagId$3;
713
- tag.textContent = css$3;
1274
+ tag.dataset.pluginCss = tagId$5;
1275
+ tag.textContent = css$5;
714
1276
  document.head.appendChild(tag);
715
1277
  }
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
1278
+ const cssRegistry$5 = globalThis.__DSH_PLUGIN_CSS__ ??= [];
1279
+ if (!cssRegistry$5.some((e) => e.tagId === tagId$5)) cssRegistry$5.push({
1280
+ tagId: tagId$5,
1281
+ css: css$5
720
1282
  });
721
1283
  var detail_module_css_default = {
722
- "subRowCell": "V9QVRa_subRowCell",
723
- "barsDense": "V9QVRa_barsDense",
724
- "rangeLeft": "V9QVRa_rangeLeft",
725
- "rangeBar": "V9QVRa_rangeBar",
726
- "subModel": "V9QVRa_subModel",
727
- "subName": "V9QVRa_subName",
728
- "subChannel": "V9QVRa_subChannel",
729
- "subNumber": "V9QVRa_subNumber",
730
- "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
- "chartTitle": "V9QVRa_chartTitle",
739
- "shareBar": "V9QVRa_shareBar",
740
- "rangeCaption": "V9QVRa_rangeCaption",
741
- "bars": "V9QVRa_bars",
742
- "barLabels": "V9QVRa_barLabels",
743
- "subRow": "V9QVRa_subRow",
744
- "segIn": "V9QVRa_segIn"
1284
+ "active": "_91PKKq_active",
1285
+ "barColumn": "_91PKKq_barColumn",
1286
+ "barLabels": "_91PKKq_barLabels",
1287
+ "bars": "_91PKKq_bars",
1288
+ "barsDense": "_91PKKq_barsDense",
1289
+ "chartTitle": "_91PKKq_chartTitle",
1290
+ "rangeBar": "_91PKKq_rangeBar",
1291
+ "rangeCaption": "_91PKKq_rangeCaption",
1292
+ "rangeInput": "_91PKKq_rangeInput",
1293
+ "rangeLeft": "_91PKKq_rangeLeft",
1294
+ "rangeTabs": "_91PKKq_rangeTabs",
1295
+ "segCache": "_91PKKq_segCache",
1296
+ "segIn": "_91PKKq_segIn",
1297
+ "segOut": "_91PKKq_segOut",
1298
+ "shareBar": "_91PKKq_shareBar",
1299
+ "subChannel": "_91PKKq_subChannel",
1300
+ "subHead": "_91PKKq_subHead",
1301
+ "subModel": "_91PKKq_subModel",
1302
+ "subName": "_91PKKq_subName",
1303
+ "subNumber": "_91PKKq_subNumber",
1304
+ "subRow": "_91PKKq_subRow",
1305
+ "subRowCell": "_91PKKq_subRowCell",
1306
+ "toggleArrow": "_91PKKq_toggleArrow"
745
1307
  };
746
1308
  //#endregion
747
1309
  //#region src/client/components/ShareBar.tsx
@@ -878,11 +1440,323 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
878
1440
  })] });
879
1441
  }
880
1442
  //#endregion
1443
+ //#region \0dsh-css:src/client/styles/trend.module.css.mjs
1444
+ const css$4 = ".dJbyja_panel{margin-bottom:12px}.dJbyja_head{flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:10px;display:flex}.dJbyja_span{color:var(--dsw-alias-label-tertiary,#67676f);font-variant-numeric:tabular-nums;font-size:11.5px}.dJbyja_placeholder{color:var(--dsw-alias-label-tertiary,#67676f);padding:10px 0 2px}.dJbyja_current{color:var(--dsw-alias-label-primary,#e8e8ec);font-weight:600}.dJbyja_previous{color:var(--dsw-alias-label-secondary,#9a9aa6)}.dJbyja_change{color:var(--dsw-alias-label-secondary,#9a9aa6);white-space:nowrap}.dJbyja_flat{color:var(--dsw-alias-label-tertiary,#67676f)}.dJbyja_warn{color:var(--dsw-alias-state-warning-primary,#e0a13c)}.dJbyja_muted{color:var(--dsw-alias-label-tertiary,#67676f)}.dJbyja_notes{flex-direction:column;gap:2px;margin-top:8px;display:flex}.dJbyja_note{color:var(--dsw-alias-label-tertiary,#67676f);font-size:11px;line-height:1.5}";
1445
+ const tagId$4 = "dsh-key-usage/trend.module.css";
1446
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]") === null) {
1447
+ const tag = document.createElement("style");
1448
+ tag.dataset.plugin = "dsh-key-usage";
1449
+ tag.dataset.pluginCss = tagId$4;
1450
+ tag.textContent = css$4;
1451
+ document.head.appendChild(tag);
1452
+ }
1453
+ const cssRegistry$4 = globalThis.__DSH_PLUGIN_CSS__ ??= [];
1454
+ if (!cssRegistry$4.some((e) => e.tagId === tagId$4)) cssRegistry$4.push({
1455
+ tagId: tagId$4,
1456
+ css: css$4
1457
+ });
1458
+ var trend_module_css_default = {
1459
+ "change": "dJbyja_change",
1460
+ "current": "dJbyja_current",
1461
+ "flat": "dJbyja_flat",
1462
+ "head": "dJbyja_head",
1463
+ "muted": "dJbyja_muted",
1464
+ "note": "dJbyja_note",
1465
+ "notes": "dJbyja_notes",
1466
+ "panel": "dJbyja_panel",
1467
+ "placeholder": "dJbyja_placeholder",
1468
+ "previous": "dJbyja_previous",
1469
+ "span": "dJbyja_span",
1470
+ "warn": "dJbyja_warn"
1471
+ };
1472
+ //#endregion
1473
+ //#region src/client/components/TrendPanel.tsx
1474
+ /**
1475
+ * 环比对比:当前范围 vs 上一段等长范围。
1476
+ * 纯客户端实现 —— 复用同一个 /usage 端点按上期范围再请求一次,服务端不感知「环比」。
1477
+ * 涨跌只用中性色 + 上下箭头:用量涨不代表好、成本涨也不代表错,不引入红绿对错语义。
1478
+ */
1479
+ const DAY_MS = 864e5;
1480
+ const FLAT = {
1481
+ text: "持平",
1482
+ kind: "flat",
1483
+ warn: false
1484
+ };
1485
+ const UNAVAILABLE = {
1486
+ text: "—",
1487
+ kind: "none",
1488
+ warn: false
1489
+ };
1490
+ /** 闭区间天数,至少 1(日期键按本地日历算,与 DetailTab 的横轴口径一致)。 */
1491
+ function spanDaysOf(from, to) {
1492
+ const fromTime = (/* @__PURE__ */ new Date(from + "T00:00:00")).getTime();
1493
+ const toTime = (/* @__PURE__ */ new Date(to + "T00:00:00")).getTime();
1494
+ return Math.max(1, Math.round((toTime - fromTime) / DAY_MS) + 1);
1495
+ }
1496
+ /** 上一段等长范围:本期首日的前一天往前推同样天数(10-07~10-13 → 09-30~10-06)。 */
1497
+ function previousWindowOf(from, to) {
1498
+ const startTime = (/* @__PURE__ */ new Date(from + "T00:00:00")).getTime();
1499
+ const span = spanDaysOf(from, to);
1500
+ return {
1501
+ from: isoDate(/* @__PURE__ */ new Date(startTime - span * DAY_MS)),
1502
+ to: isoDate(/* @__PURE__ */ new Date(startTime - DAY_MS))
1503
+ };
1504
+ }
1505
+ /** 展示口径 Tokens:输入 + 输出 + 缓存读(与三张统计卡、命中率公式同一口径)。 */
1506
+ function displayTokensOf(totals) {
1507
+ return totals.inputTokens + totals.outputTokens + totals.cacheReadTokens;
1508
+ }
1509
+ /** 命中率(%);分母为 0 时返回 null,界面显示 — 而不是 NaN。 */
1510
+ function hitRateOf$2(totals) {
1511
+ const total = displayTokensOf(totals);
1512
+ return total > 0 ? totals.cacheReadTokens / total * 100 : null;
1513
+ }
1514
+ /** 该期是否有任何用量:全 0 视为「无数据」,据此显示「上期无数据」而不是 0 与 −100%。 */
1515
+ function hasAnyData(totals) {
1516
+ return totals !== null && (totals.calls > 0 || displayTokensOf(totals) > 0 || totals.cost > 0);
1517
+ }
1518
+ /** 环比百分比;上期为 0 时不给百分比(Infinity / NaN 不进界面)。 */
1519
+ function percentChangeOf(current, previous, warnOnUp) {
1520
+ if (previous <= 0) return current <= 0 ? FLAT : {
1521
+ text: "上期为 0",
1522
+ kind: "none",
1523
+ warn: false
1524
+ };
1525
+ const percent = (current - previous) / previous * 100;
1526
+ if (Math.abs(percent) < .05) return FLAT;
1527
+ return {
1528
+ text: (percent > 0 ? "+" : "") + percent.toFixed(1) + "%",
1529
+ kind: percent > 0 ? "up" : "down",
1530
+ warn: warnOnUp && percent > 0
1531
+ };
1532
+ }
1533
+ /** 命中率本身是比值,涨跌按百分点(pp)计,避免「96.2% 比 94.0% 涨 2.3%」这类误读。 */
1534
+ function pointChangeOf(current, previous) {
1535
+ const difference = current - previous;
1536
+ if (Math.abs(difference) < .05) return FLAT;
1537
+ return {
1538
+ text: (difference > 0 ? "+" : "") + difference.toFixed(1) + "pp",
1539
+ kind: difference > 0 ? "up" : "down",
1540
+ warn: false
1541
+ };
1542
+ }
1543
+ /**
1544
+ * 成本文案:订阅 / 免费 / 本地端点没有可比金额,沿用 `costCellText` 的占位文案;
1545
+ * 其余情况一律给金额(含 ¥0.00),否则「本期降到 0」会被当成不可比而丢掉 −100%。
1546
+ */
1547
+ function costTextOf(channel, cost) {
1548
+ if (channel !== null && channel.costMode !== "perToken" && channel.costMode !== "unknown") {
1549
+ const cell = costCellText(channel, cost);
1550
+ return {
1551
+ text: cell.text,
1552
+ dim: cell.dim,
1553
+ amount: false
1554
+ };
1555
+ }
1556
+ return {
1557
+ text: fmtCost(cost),
1558
+ dim: false,
1559
+ amount: true
1560
+ };
1561
+ }
1562
+ function changeNodeOf(change) {
1563
+ if (change.kind === "none") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1564
+ className: trend_module_css_default.muted,
1565
+ children: change.text
1566
+ });
1567
+ const arrow = change.kind === "up" ? "↑" : change.kind === "down" ? "↓" : "→";
1568
+ const tone = change.warn ? trend_module_css_default.warn : change.kind === "flat" ? trend_module_css_default.flat : "";
1569
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1570
+ className: tone === "" ? trend_module_css_default.change : trend_module_css_default.change + " " + tone,
1571
+ children: [
1572
+ arrow,
1573
+ " ",
1574
+ change.text
1575
+ ]
1576
+ });
1577
+ }
1578
+ function TrendPanel(props) {
1579
+ const { usage, bounds, channelFilter } = props;
1580
+ const comparable = props.range !== "total";
1581
+ const previousWindow = (0, react.useMemo)(() => previousWindowOf(bounds.from, bounds.to), [bounds.from, bounds.to]);
1582
+ const [previousUsage, setPreviousUsage] = (0, react.useState)(null);
1583
+ const [status, setStatus] = (0, react.useState)("loading");
1584
+ const pricingState = (0, react.useSyncExternalStore)(pricingSettings.store.subscribe, pricingSettings.store.getSnapshot);
1585
+ const pricingSignature = JSON.stringify(pricingState.config);
1586
+ (0, react.useEffect)(() => {
1587
+ if (!comparable) return;
1588
+ let cancelled = false;
1589
+ setStatus("loading");
1590
+ loadUsage(previousWindow.from, previousWindow.to, channelFilter).then((result) => {
1591
+ if (cancelled) return;
1592
+ setPreviousUsage(result);
1593
+ setStatus("ready");
1594
+ });
1595
+ return () => {
1596
+ cancelled = true;
1597
+ };
1598
+ }, [
1599
+ comparable,
1600
+ previousWindow.from,
1601
+ previousWindow.to,
1602
+ channelFilter,
1603
+ pricingSignature
1604
+ ]);
1605
+ const header = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1606
+ className: trend_module_css_default.head,
1607
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1608
+ className: shared_module_css_default.statLabel,
1609
+ children: "环比对比 · 与上一段等长范围"
1610
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1611
+ className: trend_module_css_default.span,
1612
+ children: [
1613
+ "本期 ",
1614
+ bounds.from,
1615
+ " ~ ",
1616
+ bounds.to,
1617
+ comparable ? " · 上期 " + previousWindow.from + " ~ " + previousWindow.to : ""
1618
+ ]
1619
+ })]
1620
+ });
1621
+ if (!comparable) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1622
+ className: shared_module_css_default.card + " " + trend_module_css_default.panel,
1623
+ children: [header, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1624
+ className: trend_module_css_default.placeholder,
1625
+ children: "总计范围没有可对比的上一段"
1626
+ })]
1627
+ });
1628
+ const currentTotals = usage.totals;
1629
+ const previousTotals = previousUsage?.totals ?? null;
1630
+ const previousHasData = status === "ready" && hasAnyData(previousTotals);
1631
+ const currentHasData = hasAnyData(currentTotals);
1632
+ const previousPlaceholder = status === "loading" ? "读取中…" : previousUsage === null ? "无法获取" : "上期无数据";
1633
+ const currentValue = (text) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1634
+ className: trend_module_css_default.current,
1635
+ children: text
1636
+ });
1637
+ const previousValue = (text) => previousHasData ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1638
+ className: trend_module_css_default.previous,
1639
+ children: text
1640
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1641
+ className: trend_module_css_default.muted,
1642
+ children: previousPlaceholder
1643
+ });
1644
+ const changeValue = (change) => previousHasData ? changeNodeOf(change) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1645
+ className: trend_module_css_default.muted,
1646
+ children: "—"
1647
+ });
1648
+ const currentCalls = currentTotals?.calls ?? 0;
1649
+ const previousCalls = previousTotals?.calls ?? 0;
1650
+ const currentTokens = currentTotals === null ? 0 : displayTokensOf(currentTotals);
1651
+ const previousTokens = previousTotals === null ? 0 : displayTokensOf(previousTotals);
1652
+ const currentHitRate = currentTotals === null ? null : hitRateOf$2(currentTotals);
1653
+ const previousHitRate = previousTotals === null ? null : hitRateOf$2(previousTotals);
1654
+ const currentCost = currentTotals?.cost ?? 0;
1655
+ const previousCost = previousTotals?.cost ?? 0;
1656
+ const singleChannel = channelFilter === "ALL" ? null : channelOf(props.channels, channelFilter);
1657
+ const currentCostText = costTextOf(singleChannel, currentCost);
1658
+ const previousCostText = costTextOf(singleChannel, previousCost);
1659
+ const costComparable = currentCostText.amount && previousCostText.amount;
1660
+ const rows = [
1661
+ {
1662
+ key: "calls",
1663
+ label: "调用次数",
1664
+ current: currentValue(fmtCalls(currentCalls)),
1665
+ previous: previousValue(fmtCalls(previousCalls)),
1666
+ change: changeValue(percentChangeOf(currentCalls, previousCalls, false))
1667
+ },
1668
+ {
1669
+ key: "tokens",
1670
+ label: "Tokens",
1671
+ current: currentValue(fmtTokens(currentTokens)),
1672
+ previous: previousValue(fmtTokens(previousTokens)),
1673
+ change: changeValue(percentChangeOf(currentTokens, previousTokens, false))
1674
+ },
1675
+ {
1676
+ key: "cost",
1677
+ label: "成本",
1678
+ current: currentCostText.amount ? currentValue(currentCostText.text) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1679
+ className: trend_module_css_default.muted,
1680
+ children: currentCostText.text
1681
+ }),
1682
+ previous: previousValue(previousCostText.text),
1683
+ change: costComparable ? changeValue(percentChangeOf(currentCost, previousCost, true)) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1684
+ className: trend_module_css_default.muted,
1685
+ children: "不可比"
1686
+ })
1687
+ },
1688
+ {
1689
+ key: "hitRate",
1690
+ label: "缓存命中率",
1691
+ current: currentValue(currentHitRate === null ? "—" : currentHitRate.toFixed(1) + "%"),
1692
+ previous: previousValue(previousHitRate === null ? "—" : previousHitRate.toFixed(1) + "%"),
1693
+ change: changeValue(currentHitRate !== null && previousHitRate !== null ? pointChangeOf(currentHitRate, previousHitRate) : UNAVAILABLE)
1694
+ }
1695
+ ];
1696
+ const notes = [];
1697
+ if ((usage.sessionDates ?? []).length > 0 || (previousUsage?.sessionDates ?? []).length > 0) notes.push("含会话补位数据,与台账口径不完全可比");
1698
+ if (previousHasData && costComparable) {
1699
+ const currentSource = currentTotals?.costSource ?? "none";
1700
+ const previousSource = previousTotals?.costSource ?? "none";
1701
+ if (currentSource !== previousSource) notes.push("两期成本来源不同(本期 " + COST_SOURCE_LABELS[currentSource] + " / 上期 " + COST_SOURCE_LABELS[previousSource] + "),口径不完全可比");
1702
+ }
1703
+ if (status === "ready" && previousUsage === null) notes.push("上一段范围读取失败,无法对比");
1704
+ if (!currentHasData) notes.push("本期范围内无用量记录");
1705
+ notes.push("命中率的变化按百分点(pp)计,其余指标为环比百分比");
1706
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1707
+ className: shared_module_css_default.card + " " + trend_module_css_default.panel,
1708
+ children: [
1709
+ header,
1710
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
1711
+ className: shared_module_css_default.table,
1712
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
1713
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "指标" }),
1714
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "本期" }),
1715
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "上期" }),
1716
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "变化" })
1717
+ ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
1718
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: row.label }),
1719
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: row.current }),
1720
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: row.previous }),
1721
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: row.change })
1722
+ ] }, row.key)) })]
1723
+ }),
1724
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1725
+ className: trend_module_css_default.notes,
1726
+ children: notes.map((note) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1727
+ className: trend_module_css_default.note,
1728
+ children: note
1729
+ }, note))
1730
+ })
1731
+ ]
1732
+ });
1733
+ }
1734
+ //#endregion
1735
+ //#region src/client/rows.ts
1736
+ function dropEmptyRows(rows, tokensOf) {
1737
+ const kept = rows.filter((row) => tokensOf(row) > 0);
1738
+ return {
1739
+ rows: kept,
1740
+ hidden: rows.length - kept.length
1741
+ };
1742
+ }
1743
+ /** 列表底部的一行小字:说明隐藏了什么、为什么。 */
1744
+ function hiddenEmptyRowsText(hidden) {
1745
+ return "已隐藏 " + String(hidden) + " 行无 Token 记录(调用失败或未产生用量;调用次数仍计入合计)";
1746
+ }
1747
+ //#endregion
881
1748
  //#region src/client/components/DailyDetailTable.tsx
882
1749
  /**
883
1750
  * 每日细目:以「天」为折叠单位,展开行用原型子表格展示模型维度
884
1751
  * (模型 + 调用 / 输入 / 输出 / 缓存读 / 命中率,不占主表列)。
885
1752
  */
1753
+ /** 输入/输出为 0 时不再显示 `0`(全缓存命中或纯输出模型的 0 是噪音),改显示淡化的 `—`。 */
1754
+ function tokenCellOf(value) {
1755
+ return value === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1756
+ className: shared_module_css_default.dim,
1757
+ children: "—"
1758
+ }) : fmtTokens(value);
1759
+ }
886
1760
  function hitRateOf$1(cacheReadTokens, inputTokens, outputTokens) {
887
1761
  const total = inputTokens + outputTokens + cacheReadTokens;
888
1762
  if (total <= 0) return "—";
@@ -941,7 +1815,9 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
941
1815
  }
942
1816
  }
943
1817
  const comparer = COMPARERS[sort.key] ?? COMPARERS.period;
944
- const ordered = [...aggregates.values()].sort((first, second) => {
1818
+ const nonEmpty = [...aggregates.values()].filter((entry) => entry.totalTokens > 0);
1819
+ const hiddenEmpty = aggregates.size - nonEmpty.length;
1820
+ const ordered = nonEmpty.sort((first, second) => {
945
1821
  const compared = comparer(first, second);
946
1822
  return sort.dir === "asc" ? compared : -compared;
947
1823
  });
@@ -971,116 +1847,143 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
971
1847
  className: shared_module_css_default.empty,
972
1848
  children: "所选范围内无用量记录"
973
1849
  })] });
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,
1850
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1851
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1852
+ className: shared_module_css_default.blockLabel,
1853
+ children: "每日细目"
1854
+ }),
1855
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
1856
+ className: shared_module_css_default.table,
1857
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
1858
+ header("period", "日期"),
1859
+ header("total", "总计"),
1860
+ header("calls", "调用"),
1861
+ header("input", "输入"),
1862
+ header("output", "输出"),
1863
+ header("cacheRead", "缓存读"),
1864
+ header("hit", "命中率")
1865
+ ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tbody", { children: [ordered.map((period) => {
1866
+ const modelsInPeriod = modelRows.filter((row) => row.period === period.period).filter((row) => row.totalTokens > 0).sort((first, second) => second.totalTokens - first.totalTokens);
1867
+ const expandable = modelsInPeriod.length > 1;
1868
+ const isOpen = expandable && expandedPeriods[period.period] === true;
1869
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
1870
+ className: expandable ? shared_module_css_default.clickable : "",
1871
+ onClick: expandable ? () => onTogglePeriod(period.period) : void 0,
1011
1872
  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
- })
1873
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [expandable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1874
+ className: detail_module_css_default.toggleArrow,
1875
+ children: isOpen ? "▼" : "▶"
1876
+ }), period.period] }),
1877
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtTokens(period.totalTokens) }),
1878
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtCalls(period.calls) }),
1879
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: tokenCellOf(period.inputTokens) }),
1880
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: tokenCellOf(period.outputTokens) }),
1881
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtTokens(period.cacheReadTokens) }),
1882
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: hitRateOf$1(period.cacheReadTokens, period.inputTokens, period.outputTokens) })
1033
1883
  ]
1034
- }), modelsInPeriod.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1035
- className: detail_module_css_default.subRow,
1036
- 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
- })
1067
- ]
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
- })] });
1884
+ }), isOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", {
1885
+ className: detail_module_css_default.subRowCell,
1886
+ colSpan: 7,
1887
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1888
+ className: detail_module_css_default.subHead,
1889
+ children: [
1890
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {}),
1891
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1892
+ className: detail_module_css_default.subNumber,
1893
+ children: "调用"
1894
+ }),
1895
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1896
+ className: detail_module_css_default.subNumber,
1897
+ children: "输入"
1898
+ }),
1899
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1900
+ className: detail_module_css_default.subNumber,
1901
+ children: "输出"
1902
+ }),
1903
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1904
+ className: detail_module_css_default.subNumber,
1905
+ children: "缓存读"
1906
+ }),
1907
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1908
+ className: detail_module_css_default.subNumber,
1909
+ children: "命中率"
1910
+ })
1911
+ ]
1912
+ }), modelsInPeriod.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1913
+ className: detail_module_css_default.subRow,
1914
+ children: [
1915
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1916
+ className: detail_module_css_default.subName,
1917
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1918
+ className: detail_module_css_default.subModel,
1919
+ children: row.model
1920
+ }), channelFilter === "ALL" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1921
+ className: detail_module_css_default.subChannel,
1922
+ children: displayNameOf(channels, row.provider)
1923
+ })]
1924
+ }),
1925
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1926
+ className: detail_module_css_default.subNumber,
1927
+ children: fmtCalls(row.calls)
1928
+ }),
1929
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1930
+ className: detail_module_css_default.subNumber,
1931
+ children: tokenCellOf(row.inputTokens)
1932
+ }),
1933
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1934
+ className: detail_module_css_default.subNumber,
1935
+ children: tokenCellOf(row.outputTokens)
1936
+ }),
1937
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1938
+ className: detail_module_css_default.subNumber,
1939
+ children: fmtTokens(row.cacheReadTokens)
1940
+ }),
1941
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1942
+ className: detail_module_css_default.subNumber,
1943
+ children: hitRateOf$1(row.cacheReadTokens, row.inputTokens, row.outputTokens)
1944
+ })
1945
+ ]
1946
+ }, row.period + "|" + row.model + "|" + row.provider))]
1947
+ }) })] }, period.period);
1948
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
1949
+ className: shared_module_css_default.total,
1950
+ children: [
1951
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: "合计" }),
1952
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtTokens(totalTokens) }),
1953
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtCalls(totals?.calls ?? 0) }),
1954
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtTokens(totals?.inputTokens ?? 0) }),
1955
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtTokens(totals?.outputTokens ?? 0) }),
1956
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtTokens(totals?.cacheReadTokens ?? 0) }),
1957
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: hitRateOf$1(totals?.cacheReadTokens ?? 0, totals?.inputTokens ?? 0, totals?.outputTokens ?? 0) })
1958
+ ]
1959
+ })] })]
1960
+ }),
1961
+ hiddenEmpty > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1962
+ className: shared_module_css_default.statExtra,
1963
+ children: hiddenEmptyRowsText(hiddenEmpty)
1964
+ })
1965
+ ] });
1966
+ }
1967
+ //#endregion
1968
+ //#region \0dsh-css:src/client/styles/export.module.css.mjs
1969
+ const css$3 = ".-\\38 z9vG_exportGroup{align-items:center;gap:6px;margin-left:auto;display:inline-flex}.-\\38 z9vG_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}.-\\38 z9vG_exportBtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#80808c1a);color:var(--dsw-alias-label-primary,#e8e8ec)}.-\\38 z9vG_exportBtn:disabled{opacity:.5;cursor:default}";
1970
+ const tagId$3 = "dsh-key-usage/export.module.css";
1971
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
1972
+ const tag = document.createElement("style");
1973
+ tag.dataset.plugin = "dsh-key-usage";
1974
+ tag.dataset.pluginCss = tagId$3;
1975
+ tag.textContent = css$3;
1976
+ document.head.appendChild(tag);
1083
1977
  }
1978
+ const cssRegistry$3 = globalThis.__DSH_PLUGIN_CSS__ ??= [];
1979
+ if (!cssRegistry$3.some((e) => e.tagId === tagId$3)) cssRegistry$3.push({
1980
+ tagId: tagId$3,
1981
+ css: css$3
1982
+ });
1983
+ var export_module_css_default = {
1984
+ "exportBtn": "-8z9vG_exportBtn",
1985
+ "exportGroup": "-8z9vG_exportGroup"
1986
+ };
1084
1987
  //#endregion
1085
1988
  //#region src/client/tabs/DetailTab.tsx
1086
1989
  const RANGE_TABS = [
@@ -1092,11 +1995,54 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1092
1995
  ];
1093
1996
  /** 图表最多铺开的列数:超过这个跨度的范围按天全画会挤成一团。 */
1094
1997
  const MAX_AXIS_DAYS = 366;
1998
+ /** 导出列:命中率与成本给数值(不带单位),方便在表格里继续算。 */
1999
+ const EXPORT_COLUMNS$3 = [
2000
+ {
2001
+ key: "date",
2002
+ label: "日期",
2003
+ value: (row) => row.date
2004
+ },
2005
+ {
2006
+ key: "calls",
2007
+ label: "调用",
2008
+ value: (row) => row.calls
2009
+ },
2010
+ {
2011
+ key: "inputTokens",
2012
+ label: "输入",
2013
+ value: (row) => row.inputTokens
2014
+ },
2015
+ {
2016
+ key: "outputTokens",
2017
+ label: "输出",
2018
+ value: (row) => row.outputTokens
2019
+ },
2020
+ {
2021
+ key: "cacheReadTokens",
2022
+ label: "缓存读",
2023
+ value: (row) => row.cacheReadTokens
2024
+ },
2025
+ {
2026
+ key: "cacheHitRate",
2027
+ label: "命中率(%)",
2028
+ value: (row) => hitRateValueOf(row)
2029
+ },
2030
+ {
2031
+ key: "cost",
2032
+ label: "成本(元)",
2033
+ value: (row) => row.cost
2034
+ }
2035
+ ];
2036
+ /** 命中率数值(导出用;界面上的带 % 文本由 `hitRateOf` 生成)。 */
2037
+ function hitRateValueOf(row) {
2038
+ const total = row.inputTokens + row.outputTokens + row.cacheReadTokens;
2039
+ return total > 0 ? Math.round(row.cacheReadTokens / total * 1e3) / 10 : null;
2040
+ }
1095
2041
  function hitRateOf(cacheReadTokens, inputTokens, outputTokens) {
1096
2042
  const total = inputTokens + outputTokens + cacheReadTokens;
1097
2043
  return total > 0 ? (cacheReadTokens / total * 100).toFixed(1) + "%" : "—";
1098
2044
  }
1099
- function daysBetween(from, to) {
2045
+ function daysBetween$1(from, to) {
1100
2046
  const fromTime = (/* @__PURE__ */ new Date(from + "T00:00:00")).getTime();
1101
2047
  const toTime = (/* @__PURE__ */ new Date(to + "T00:00:00")).getTime();
1102
2048
  return Math.max(1, Math.round((toTime - fromTime) / 864e5) + 1);
@@ -1104,7 +2050,7 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1104
2050
  /** 范围内每一天的日期键(无数据的日子也要出现在横轴上)。 */
1105
2051
  function dateKeysOfRange(from, to) {
1106
2052
  const start = (/* @__PURE__ */ new Date(from + "T00:00:00")).getTime();
1107
- const total = Math.min(daysBetween(from, to), MAX_AXIS_DAYS);
2053
+ const total = Math.min(daysBetween$1(from, to), MAX_AXIS_DAYS);
1108
2054
  const keys = [];
1109
2055
  for (let offset = 0; offset < total; offset += 1) keys.push(isoDate(new Date(start + offset * 864e5)));
1110
2056
  return keys;
@@ -1121,15 +2067,25 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1121
2067
  inputTokens: 0,
1122
2068
  outputTokens: 0,
1123
2069
  cacheReadTokens: 0,
2070
+ cacheWriteTokens: 0,
1124
2071
  reasoningTokens: 0,
1125
2072
  cost: 0
1126
2073
  };
1127
2074
  const totalTokens = safeTotals.inputTokens + safeTotals.outputTokens + safeTotals.cacheReadTokens;
1128
2075
  const hitRate = hitRateOf(safeTotals.cacheReadTokens, safeTotals.inputTokens, safeTotals.outputTokens);
2076
+ /** 缓存读在展示口径 Tokens 里的占比;分母为 0 时不给数字(避免显示 0% 误导)。 */
2077
+ const cacheReadShare = totalTokens > 0 ? (safeTotals.cacheReadTokens / totalTokens * 100).toFixed(1) + "%" : null;
2078
+ /** 范围内「有用量」的天数:日均 Tokens 的分母(拿日历天数去除会把空闲日算成用量偏低)。 */
2079
+ const activeDays = usage.days.filter((day) => day.inputTokens + day.outputTokens + day.cacheReadTokens > 0).length;
1129
2080
  const singleChannel = channelFilter === "ALL" ? null : channelOf(channels, channelFilter);
1130
2081
  const costCell = costCellText(singleChannel, safeTotals.cost);
1131
2082
  const singleDay = bounds.from === bounds.to;
1132
- const rangeDays = Math.min(daysBetween(bounds.from, bounds.to), MAX_AXIS_DAYS);
2083
+ const rangeDays = Math.min(daysBetween$1(bounds.from, bounds.to), MAX_AXIS_DAYS);
2084
+ /** 导出当前范围与通道筛选后的每日明细(按日期升序,不跟随表格排序)。 */
2085
+ const exportDaily = (format) => {
2086
+ if (format === "csv") downloadFile(exportFileName("daily", "csv"), toCsv(usage.days, EXPORT_COLUMNS$3), CSV_MIME);
2087
+ else downloadFile(exportFileName("daily", "json"), toJson(usage.days), JSON_MIME);
2088
+ };
1133
2089
  const activeProviders = /* @__PURE__ */ new Set();
1134
2090
  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
2091
  const hourlyRows = [];
@@ -1228,9 +2184,9 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1228
2184
  const chartRows = useHourAxis ? hourlyRows : dailyRows;
1229
2185
  const chartTitle = useHourAxis ? "按小时 Tokens(" + bounds.from + ")" : "按天 Tokens(" + rangeDays + " 天)";
1230
2186
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1231
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2187
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1232
2188
  className: detail_module_css_default.rangeBar,
1233
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2189
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1234
2190
  className: detail_module_css_default.rangeLeft,
1235
2191
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1236
2192
  className: detail_module_css_default.rangeTabs,
@@ -1257,10 +2213,23 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1257
2213
  onChange: (event) => props.onCustomTo(event.target.value)
1258
2214
  })
1259
2215
  ] })]
1260
- })
2216
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2217
+ className: export_module_css_default.exportGroup,
2218
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2219
+ className: export_module_css_default.exportBtn,
2220
+ disabled: usage.days.length === 0,
2221
+ onClick: () => exportDaily("csv"),
2222
+ children: "导出 CSV"
2223
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2224
+ className: export_module_css_default.exportBtn,
2225
+ disabled: usage.days.length === 0,
2226
+ onClick: () => exportDaily("json"),
2227
+ children: "导出 JSON"
2228
+ })]
2229
+ })]
1261
2230
  }),
1262
2231
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1263
- className: shared_module_css_default.cardGrid3,
2232
+ className: shared_module_css_default.cardGrid6,
1264
2233
  children: [
1265
2234
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatCard, {
1266
2235
  label: "调用次数",
@@ -1270,18 +2239,40 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1270
2239
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatCard, {
1271
2240
  label: "Tokens",
1272
2241
  value: fmtTokens(totalTokens),
1273
- extra: "缓存读 " + fmtTokens(safeTotals.cacheReadTokens) + " · 命中率 " + hitRate
2242
+ extra: "输入 " + fmtTokens(safeTotals.inputTokens) + " · 输出 " + fmtTokens(safeTotals.outputTokens)
2243
+ }),
2244
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatCard, {
2245
+ label: "缓存读",
2246
+ value: fmtTokens(safeTotals.cacheReadTokens),
2247
+ extra: cacheReadShare === null ? "与上方 Tokens 同口径" : "占 Tokens 的 " + cacheReadShare
2248
+ }),
2249
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatCard, {
2250
+ label: "缓存命中率",
2251
+ value: hitRate,
2252
+ extra: "缓存读 ÷(输入 + 输出 + 缓存读)"
2253
+ }),
2254
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatCard, {
2255
+ label: "日均 Tokens",
2256
+ value: activeDays === 0 ? "—" : fmtTokens(Math.round(totalTokens / activeDays)),
2257
+ extra: "范围内 " + String(activeDays) + " 天有用量"
1274
2258
  }),
1275
2259
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatCard, {
1276
2260
  label: "成本",
1277
2261
  value: costCell.dim ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1278
2262
  className: shared_module_css_default.dim,
1279
2263
  children: costCell.text
1280
- }) : costCell.text,
2264
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [costCell.text, showsAmount(costCell) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CostSourceChip, { source: safeTotals.costSource })] }),
1281
2265
  extra: "按所选范围与筛选聚合"
1282
2266
  })
1283
2267
  ]
1284
2268
  }),
2269
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrendPanel, {
2270
+ usage,
2271
+ channels,
2272
+ range: props.range,
2273
+ bounds,
2274
+ channelFilter
2275
+ }),
1285
2276
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShareBar, {
1286
2277
  usage,
1287
2278
  totalTokens,
@@ -1373,8 +2364,8 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1373
2364
  ] });
1374
2365
  }
1375
2366
  //#endregion
1376
- //#region \0dsh-css:D:\EasyJob\.plugin\dsh-key-usage\src\client\styles\lists.module.css.mjs
1377
- const css$2 = ".QpFKma_filterBar{flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:14px;display:flex}.QpFKma_searchInput{border:1px solid var(--dsw-alias-border-l2,#2c2c34);min-width:180px;color:var(--dsw-alias-label-primary,#e8e8ec);font:inherit;background:0 0;border-radius:9px;flex:1;padding:6px 12px;font-size:12.5px}.QpFKma_searchInput::placeholder{color:var(--dsw-alias-label-tertiary,#67676f)}.QpFKma_filterSelect{background:var(--dsw-alias-bg-layer-2,#1d1d22);border:1px solid var(--dsw-alias-border-l2,#2c2c34);color:var(--dsw-alias-label-secondary,#9a9aa6);font:inherit;cursor:pointer;border-radius:9px;padding:6px 10px;font-size:12px}.QpFKma_listTitle{align-items:center;gap:10px;min-width:0;display:flex}.QpFKma_listMain{color:var(--dsw-alias-label-primary,#e8e8ec);text-overflow:ellipsis;white-space:nowrap;font-size:12.5px;font-weight:600;overflow:hidden}.QpFKma_listSub{color:var(--dsw-alias-label-tertiary,#67676f);text-overflow:ellipsis;white-space:nowrap;margin-top:1px;font-size:10.5px;overflow:hidden}.QpFKma_listBar{background:var(--dsw-alias-fill-l2,#80808c2e);vertical-align:middle;border-radius:3px;width:70px;height:5px;display:inline-block;overflow:hidden}.QpFKma_listBar>i{border-radius:3px;height:100%;display:block}.QpFKma_sessionDay td{background:var(--dsw-alias-bg-layer-2,#1d1d22);color:var(--dsw-alias-label-primary,#e8e8ec);border-bottom:1px solid var(--dsw-alias-border-l1,#2c2c34);font-size:12px;font-weight:600;text-align:left!important;padding:7px 10px!important}.QpFKma_dayCount{color:var(--dsw-alias-label-tertiary,#67676f);margin-left:8px;font-size:11px;font-weight:500}.QpFKma_rowArchived td{opacity:.5}.QpFKma_archivedTag{color:var(--dsw-alias-label-tertiary,#67676f);border:1px solid var(--dsw-alias-border-l2,#2c2c34);border-radius:999px;margin-left:6px;padding:0 6px;font-size:10px;font-weight:500}";
2367
+ //#region \0dsh-css:src/client/styles/lists.module.css.mjs
2368
+ const css$2 = ".ysq2ha_filterBar{flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:14px;display:flex}.ysq2ha_searchInput{border:1px solid var(--dsw-alias-border-l2,#2c2c34);min-width:180px;color:var(--dsw-alias-label-primary,#e8e8ec);font:inherit;background:0 0;border-radius:9px;flex:1;padding:6px 12px;font-size:12.5px}.ysq2ha_searchInput::placeholder{color:var(--dsw-alias-label-tertiary,#67676f)}.ysq2ha_filterSelect{background:var(--dsw-alias-bg-layer-2,#1d1d22);border:1px solid var(--dsw-alias-border-l2,#2c2c34);color:var(--dsw-alias-label-secondary,#9a9aa6);font:inherit;cursor:pointer;border-radius:9px;padding:6px 10px;font-size:12px}.ysq2ha_listTitle{align-items:center;gap:10px;min-width:0;display:flex}.ysq2ha_listMain{color:var(--dsw-alias-label-primary,#e8e8ec);text-overflow:ellipsis;white-space:nowrap;font-size:12.5px;font-weight:600;overflow:hidden}.ysq2ha_listSub{color:var(--dsw-alias-label-tertiary,#67676f);text-overflow:ellipsis;white-space:nowrap;margin-top:1px;font-size:10.5px;overflow:hidden}.ysq2ha_listBar{background:var(--dsw-alias-fill-l2,#80808c2e);vertical-align:middle;border-radius:3px;width:70px;height:5px;display:inline-block;overflow:hidden}.ysq2ha_listBar>i{border-radius:3px;height:100%;display:block}.ysq2ha_sessionDay td{background:var(--dsw-alias-bg-layer-2,#1d1d22);color:var(--dsw-alias-label-primary,#e8e8ec);border-bottom:1px solid var(--dsw-alias-border-l1,#2c2c34);font-size:12px;font-weight:600;text-align:left!important;padding:7px 10px!important}.ysq2ha_dayCount{color:var(--dsw-alias-label-tertiary,#67676f);margin-left:8px;font-size:11px;font-weight:500}.ysq2ha_rowArchived td{opacity:.5}.ysq2ha_archivedTag{color:var(--dsw-alias-label-tertiary,#67676f);border:1px solid var(--dsw-alias-border-l2,#2c2c34);border-radius:999px;margin-left:6px;padding:0 6px;font-size:10px;font-weight:500}.ysq2ha_sessionTable{table-layout:fixed}.ysq2ha_sessionTable th:first-child{width:38%}.ysq2ha_sessionTable th:nth-child(2){width:13%}.ysq2ha_sessionTable th:nth-child(3){width:19%}.ysq2ha_sessionTable th:nth-child(4){width:11%}.ysq2ha_sessionTable th:nth-child(5){width:8%}.ysq2ha_sessionTable th:nth-child(6){width:11%}.ysq2ha_modelTable{table-layout:fixed}.ysq2ha_modelTable th:first-child{width:30%}.ysq2ha_modelTable th:nth-child(2){width:9%}.ysq2ha_modelTable th:nth-child(3){width:11%}.ysq2ha_modelTable th:nth-child(4){width:12%}.ysq2ha_modelTable th:nth-child(5){width:9%}.ysq2ha_modelTable th:nth-child(6){width:12%}.ysq2ha_modelTable th:nth-child(7){width:17%}.ysq2ha_projectTable{table-layout:fixed}.ysq2ha_projectTable th:first-child{width:38%}.ysq2ha_projectTable th:nth-child(2){width:10%}.ysq2ha_projectTable th:nth-child(3){width:12%}.ysq2ha_projectTable th:nth-child(4){width:13%}.ysq2ha_projectTable th:nth-child(5){width:12%}.ysq2ha_projectTable th:nth-child(6){width:15%}";
1378
2369
  const tagId$2 = "dsh-key-usage/lists.module.css";
1379
2370
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
1380
2371
  const tag = document.createElement("style");
@@ -1389,21 +2380,342 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1389
2380
  css: css$2
1390
2381
  });
1391
2382
  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",
1398
- "archivedTag": "QpFKma_archivedTag",
1399
- "listTitle": "QpFKma_listTitle",
1400
- "searchInput": "QpFKma_searchInput",
1401
- "filterSelect": "QpFKma_filterSelect",
1402
- "dayCount": "QpFKma_dayCount"
2383
+ "archivedTag": "ysq2ha_archivedTag",
2384
+ "dayCount": "ysq2ha_dayCount",
2385
+ "filterBar": "ysq2ha_filterBar",
2386
+ "filterSelect": "ysq2ha_filterSelect",
2387
+ "listBar": "ysq2ha_listBar",
2388
+ "listMain": "ysq2ha_listMain",
2389
+ "listSub": "ysq2ha_listSub",
2390
+ "listTitle": "ysq2ha_listTitle",
2391
+ "modelTable": "ysq2ha_modelTable",
2392
+ "projectTable": "ysq2ha_projectTable",
2393
+ "rowArchived": "ysq2ha_rowArchived",
2394
+ "searchInput": "ysq2ha_searchInput",
2395
+ "sessionDay": "ysq2ha_sessionDay",
2396
+ "sessionTable": "ysq2ha_sessionTable"
1403
2397
  };
1404
2398
  //#endregion
2399
+ //#region src/client/tabs/ModelsTab.tsx
2400
+ /** 按模型:`provider + model` 聚合的用量与估算成本(数据来自宿主会话事件折叠)。 */
2401
+ const MODELS_PATH = "/dsh-key-usage/models";
2402
+ /** 导出列:取原始数值(金额、命中率不带单位),方便直接进表格做计算。 */
2403
+ const EXPORT_COLUMNS$2 = [
2404
+ {
2405
+ key: "provider",
2406
+ label: "通道 id",
2407
+ value: (row) => row.provider
2408
+ },
2409
+ {
2410
+ key: "providerLabel",
2411
+ label: "通道",
2412
+ value: (row) => row.providerLabel
2413
+ },
2414
+ {
2415
+ key: "model",
2416
+ label: "模型",
2417
+ value: (row) => row.model
2418
+ },
2419
+ {
2420
+ key: "calls",
2421
+ label: "调用",
2422
+ value: (row) => row.calls
2423
+ },
2424
+ {
2425
+ key: "tokens",
2426
+ label: "Tokens",
2427
+ value: (row) => row.tokens
2428
+ },
2429
+ {
2430
+ key: "inputTokens",
2431
+ label: "输入",
2432
+ value: (row) => row.buckets.inputTokens
2433
+ },
2434
+ {
2435
+ key: "outputTokens",
2436
+ label: "输出",
2437
+ value: (row) => row.buckets.outputTokens
2438
+ },
2439
+ {
2440
+ key: "cacheReadTokens",
2441
+ label: "缓存读",
2442
+ value: (row) => row.buckets.cacheReadTokens
2443
+ },
2444
+ {
2445
+ key: "cacheHitRate",
2446
+ label: "命中率(%)",
2447
+ value: (row) => row.cacheHitRate
2448
+ },
2449
+ {
2450
+ key: "cost",
2451
+ label: "成本(元)",
2452
+ value: (row) => row.cost
2453
+ },
2454
+ {
2455
+ key: "costSource",
2456
+ label: "成本来源",
2457
+ value: (row) => row.costSource === void 0 ? null : COST_SOURCE_LABELS[row.costSource]
2458
+ },
2459
+ {
2460
+ key: "costMode",
2461
+ label: "计费模式",
2462
+ value: (row) => row.costMode
2463
+ },
2464
+ {
2465
+ key: "sessionCount",
2466
+ label: "会话数",
2467
+ value: (row) => row.sessionCount
2468
+ },
2469
+ {
2470
+ key: "lastTime",
2471
+ label: "最近活跃",
2472
+ value: (row) => fmtDateTime(row.lastTime)
2473
+ }
2474
+ ];
2475
+ function ModelsTab(props) {
2476
+ const [query, setQuery] = (0, react.useState)("");
2477
+ const [channelFilter, setChannelFilter] = (0, react.useState)("ALL");
2478
+ const [sortKey, setSortKey] = (0, react.useState)("tokensDesc");
2479
+ const [nowMs] = (0, react.useState)(() => Date.now());
2480
+ const rows = props.data?.rows ?? [];
2481
+ const visible = (0, react.useMemo)(() => {
2482
+ const keyword = query.trim().toLowerCase();
2483
+ const filtered = dropEmptyRows(rows, (row) => row.tokens).rows.filter((row) => {
2484
+ const haystack = (row.model + " " + row.provider + " " + row.providerLabel).toLowerCase();
2485
+ const matchesQuery = keyword === "" || haystack.includes(keyword);
2486
+ const matchesChannel = channelFilter === "ALL" || row.provider === channelFilter;
2487
+ return matchesQuery && matchesChannel;
2488
+ });
2489
+ const sorters = {
2490
+ tokensDesc: (first, second) => second.tokens - first.tokens,
2491
+ callsDesc: (first, second) => second.calls - first.calls,
2492
+ costDesc: (first, second) => (second.cost ?? 0) - (first.cost ?? 0),
2493
+ recent: (first, second) => (second.lastTime ?? 0) - (first.lastTime ?? 0)
2494
+ };
2495
+ return [...filtered].sort(sorters[sortKey]);
2496
+ }, [
2497
+ rows,
2498
+ query,
2499
+ channelFilter,
2500
+ sortKey
2501
+ ]);
2502
+ const totalTokens = rows.reduce((total, row) => total + row.tokens, 0);
2503
+ const hiddenEmpty = dropEmptyRows(rows, (row) => row.tokens).hidden;
2504
+ /** 导出当前筛选与排序后的行(JSON 取整行,CSV 按固定列映射)。 */
2505
+ const exportRows = (format) => {
2506
+ if (format === "csv") downloadFile(exportFileName("models", "csv"), toCsv(visible, EXPORT_COLUMNS$2), CSV_MIME);
2507
+ else downloadFile(exportFileName("models", "json"), toJson(visible), JSON_MIME);
2508
+ };
2509
+ if (props.data === null) {
2510
+ const reason = failureReasonOf(MODELS_PATH);
2511
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2512
+ className: shared_module_css_default.empty,
2513
+ children: reason === null ? "按模型数据读取中…" : "按模型数据不可用:" + reason
2514
+ });
2515
+ }
2516
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
2517
+ props.data.coverage.status !== "complete" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2518
+ className: shared_module_css_default.warning,
2519
+ children: coverageText$1(props.data.coverage.status, props.data.coverage.skippedSessions)
2520
+ }),
2521
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2522
+ className: lists_module_css_default.filterBar,
2523
+ children: [
2524
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2525
+ className: lists_module_css_default.searchInput,
2526
+ placeholder: "搜索模型名、通道…",
2527
+ value: query,
2528
+ onChange: (event) => setQuery(event.target.value)
2529
+ }),
2530
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2531
+ className: lists_module_css_default.filterSelect,
2532
+ value: channelFilter,
2533
+ onChange: (event) => setChannelFilter(event.target.value),
2534
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2535
+ value: "ALL",
2536
+ children: "全部通道"
2537
+ }), props.channels.map((channel) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2538
+ value: channel.provider,
2539
+ children: channel.displayName
2540
+ }, channel.provider))]
2541
+ }),
2542
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2543
+ className: lists_module_css_default.filterSelect,
2544
+ value: sortKey,
2545
+ onChange: (event) => setSortKey(event.target.value),
2546
+ children: [
2547
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2548
+ value: "tokensDesc",
2549
+ children: "Tokens 最多"
2550
+ }),
2551
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2552
+ value: "callsDesc",
2553
+ children: "调用最多"
2554
+ }),
2555
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2556
+ value: "costDesc",
2557
+ children: "成本最高"
2558
+ }),
2559
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2560
+ value: "recent",
2561
+ children: "最近活跃"
2562
+ })
2563
+ ]
2564
+ }),
2565
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2566
+ className: export_module_css_default.exportGroup,
2567
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2568
+ className: export_module_css_default.exportBtn,
2569
+ disabled: visible.length === 0,
2570
+ onClick: () => exportRows("csv"),
2571
+ children: "导出 CSV"
2572
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2573
+ className: export_module_css_default.exportBtn,
2574
+ disabled: visible.length === 0,
2575
+ onClick: () => exportRows("json"),
2576
+ children: "导出 JSON"
2577
+ })]
2578
+ })
2579
+ ]
2580
+ }),
2581
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
2582
+ className: shared_module_css_default.table + " " + lists_module_css_default.modelTable,
2583
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
2584
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "模型" }),
2585
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "调用" }),
2586
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "Tokens" }),
2587
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "占比" }),
2588
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "命中率" }),
2589
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "成本" }),
2590
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "最近活跃" })
2591
+ ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tbody", { children: [visible.map((row) => {
2592
+ const percent = totalTokens > 0 ? Math.round(row.tokens / totalTokens * 100) : 0;
2593
+ const channel = channelOf(props.channels, row.provider);
2594
+ const costCell = costCellText(channel, row.cost ?? 0);
2595
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
2596
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2597
+ className: lists_module_css_default.listTitle,
2598
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2599
+ className: lists_module_css_default.listMain,
2600
+ children: row.model
2601
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2602
+ className: lists_module_css_default.listSub,
2603
+ children: [
2604
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2605
+ className: shared_module_css_default.statusDot,
2606
+ style: {
2607
+ background: channelColorOf(props.channels, row.provider),
2608
+ marginRight: 4
2609
+ }
2610
+ }),
2611
+ row.providerLabel,
2612
+ " · ",
2613
+ row.sessionCount,
2614
+ " 个会话"
2615
+ ]
2616
+ })] })
2617
+ }) }),
2618
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtCalls(row.calls) }),
2619
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtTokens(row.tokens) }),
2620
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [
2621
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2622
+ className: lists_module_css_default.listBar,
2623
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { style: {
2624
+ width: Math.max(2, percent) + "%",
2625
+ background: channelColorOf(props.channels, row.provider)
2626
+ } })
2627
+ }),
2628
+ " ",
2629
+ percent,
2630
+ "%"
2631
+ ] }),
2632
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: row.cacheHitRate === null ? "—" : row.cacheHitRate.toFixed(1) + "%" }),
2633
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: row.cost === null || channel?.costMode !== "perToken" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2634
+ className: shared_module_css_default.dim,
2635
+ children: costCell.text
2636
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [costCell.text, showsAmount(costCell) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CostSourceChip, { source: row.costSource })] }) }),
2637
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtRelative(row.lastTime, nowMs) })
2638
+ ] }, row.provider + "|" + row.model);
2639
+ }), visible.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
2640
+ colSpan: 7,
2641
+ className: shared_module_css_default.empty,
2642
+ children: "没有符合条件的模型"
2643
+ }) })] })]
2644
+ }),
2645
+ hiddenEmpty > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2646
+ className: shared_module_css_default.statExtra,
2647
+ children: hiddenEmptyRowsText(hiddenEmpty)
2648
+ }),
2649
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2650
+ className: shared_module_css_default.statExtra,
2651
+ children: [
2652
+ "模型用量按宿主会话事件折叠(每个结算样本计一次调用);成本按台账模型均价估算",
2653
+ props.data.priced ? "" : "(台账无成本数据,暂不可估)",
2654
+ ";命中率 = 缓存读 ÷(输入 + 输出 + 缓存读)"
2655
+ ]
2656
+ })
2657
+ ] });
2658
+ }
2659
+ function coverageText$1(status, skipped) {
2660
+ if (status === "unavailable") return "会话数据不可用(宿主未提供会话服务或全部读取失败)";
2661
+ return "会话数据不完整:" + String(skipped) + " 个会话读取失败,未计入统计";
2662
+ }
2663
+ //#endregion
1405
2664
  //#region src/client/tabs/ProjectsTab.tsx
1406
2665
  /** 项目用量:按会话工作目录聚合(数据来自宿主会话事件折叠)。 */
2666
+ /** 导出列:成本与命中率都给原始数值,方便在表格里继续算。 */
2667
+ const EXPORT_COLUMNS$1 = [
2668
+ {
2669
+ key: "workspace",
2670
+ label: "项目路径",
2671
+ value: (row) => row.workspace
2672
+ },
2673
+ {
2674
+ key: "dominantProvider",
2675
+ label: "主要通道",
2676
+ value: (row) => row.dominantProvider
2677
+ },
2678
+ {
2679
+ key: "calls",
2680
+ label: "调用",
2681
+ value: (row) => row.calls
2682
+ },
2683
+ {
2684
+ key: "tokens",
2685
+ label: "Tokens",
2686
+ value: (row) => row.tokens
2687
+ },
2688
+ {
2689
+ key: "cacheHitRate",
2690
+ label: "命中率(%)",
2691
+ value: (row) => row.cacheHitRate
2692
+ },
2693
+ {
2694
+ key: "cost",
2695
+ label: "成本(元)",
2696
+ value: (row) => row.cost
2697
+ },
2698
+ {
2699
+ key: "costSource",
2700
+ label: "成本来源",
2701
+ value: (row) => row.costSource === void 0 ? null : COST_SOURCE_LABELS[row.costSource]
2702
+ },
2703
+ {
2704
+ key: "sessionCount",
2705
+ label: "会话数",
2706
+ value: (row) => row.sessionCount
2707
+ },
2708
+ {
2709
+ key: "archivedSessionCount",
2710
+ label: "已归档会话数",
2711
+ value: (row) => row.archivedSessionCount
2712
+ },
2713
+ {
2714
+ key: "lastTime",
2715
+ label: "最近活跃",
2716
+ value: (row) => fmtDateTime(row.lastTime)
2717
+ }
2718
+ ];
1407
2719
  function ProjectsTab(props) {
1408
2720
  const [query, setQuery] = (0, react.useState)("");
1409
2721
  const [channelFilter, setChannelFilter] = (0, react.useState)("ALL");
@@ -1412,7 +2724,7 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1412
2724
  const rows = props.data?.rows ?? [];
1413
2725
  const visible = (0, react.useMemo)(() => {
1414
2726
  const keyword = query.trim().toLowerCase();
1415
- const filtered = rows.filter((row) => {
2727
+ const filtered = dropEmptyRows(rows, (row) => row.tokens).rows.filter((row) => {
1416
2728
  const matchesQuery = keyword === "" || row.workspace.toLowerCase().includes(keyword);
1417
2729
  const matchesChannel = channelFilter === "ALL" || row.providers[channelFilter] !== void 0;
1418
2730
  return matchesQuery && matchesChannel;
@@ -1431,6 +2743,12 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1431
2743
  sortKey
1432
2744
  ]);
1433
2745
  const totalTokens = rows.reduce((total, row) => total + row.tokens, 0);
2746
+ const hiddenEmpty = dropEmptyRows(rows, (row) => row.tokens).hidden;
2747
+ /** 导出当前筛选与排序后的项目行(JSON 取整行,CSV 按固定列映射)。 */
2748
+ const exportRows = (format) => {
2749
+ if (format === "csv") downloadFile(exportFileName("projects", "csv"), toCsv(visible, EXPORT_COLUMNS$1), CSV_MIME);
2750
+ else downloadFile(exportFileName("projects", "json"), toJson(visible), JSON_MIME);
2751
+ };
1434
2752
  if (props.data === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1435
2753
  className: shared_module_css_default.empty,
1436
2754
  children: "会话数据读取中或不可用…"
@@ -1483,11 +2801,25 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1483
2801
  children: "调用最多"
1484
2802
  })
1485
2803
  ]
2804
+ }),
2805
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2806
+ className: export_module_css_default.exportGroup,
2807
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2808
+ className: export_module_css_default.exportBtn,
2809
+ disabled: visible.length === 0,
2810
+ onClick: () => exportRows("csv"),
2811
+ children: "导出 CSV"
2812
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2813
+ className: export_module_css_default.exportBtn,
2814
+ disabled: visible.length === 0,
2815
+ onClick: () => exportRows("json"),
2816
+ children: "导出 JSON"
2817
+ })]
1486
2818
  })
1487
2819
  ]
1488
2820
  }),
1489
2821
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
1490
- className: shared_module_css_default.table,
2822
+ className: shared_module_css_default.table + " " + lists_module_css_default.projectTable,
1491
2823
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
1492
2824
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "项目" }),
1493
2825
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "调用" }),
@@ -1534,7 +2866,7 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1534
2866
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: row.cost === null || channel?.costMode === "sub" || channel?.costMode === "free" || channel?.costMode === "none" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1535
2867
  className: shared_module_css_default.dim,
1536
2868
  children: costCell.text
1537
- }) : costCell.text }),
2869
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [costCell.text, showsAmount(costCell) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CostSourceChip, { source: row.costSource })] }) }),
1538
2870
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtRelative(row.lastTime, nowMs) })
1539
2871
  ] }, row.workspace);
1540
2872
  }), visible.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
@@ -1543,27 +2875,877 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1543
2875
  children: "没有符合条件的项目"
1544
2876
  }) })] })]
1545
2877
  }),
2878
+ hiddenEmpty > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2879
+ className: shared_module_css_default.statExtra,
2880
+ children: hiddenEmptyRowsText(hiddenEmpty)
2881
+ }),
1546
2882
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1547
2883
  className: shared_module_css_default.statExtra,
1548
2884
  children: [
1549
- "成本按台账模型中位均价估算",
1550
- props.data.priced ? "" : "(台账无成本数据,暂不可估)",
1551
- ";命中率 = 缓存读 /(输入 + 输出 + 缓存读)"
2885
+ "成本按台账模型中位均价估算",
2886
+ props.data.priced ? "" : "(台账无成本数据,暂不可估)",
2887
+ ";命中率 = 缓存读 /(输入 + 输出 + 缓存读)"
2888
+ ]
2889
+ })
2890
+ ] });
2891
+ }
2892
+ function coverageText(status, skipped) {
2893
+ if (status === "unavailable") return "会话数据不可用(宿主未提供会话服务或全部读取失败)";
2894
+ return "会话数据不完整:" + String(skipped) + " 个会话读取失败,未计入统计";
2895
+ }
2896
+ function baseNameOf(path) {
2897
+ const parts = path.replace(/[\\/]+$/, "").split(/[\\/]/);
2898
+ return parts[parts.length - 1] === "" ? path : parts[parts.length - 1];
2899
+ }
2900
+ //#endregion
2901
+ //#region src/client/components/PricingRulesPanel.tsx
2902
+ /**
2903
+ * 计费规则面板:并入「余额与配额」页下半部分(不新增 Tab),插件页卡片复用同一份。
2904
+ *
2905
+ * 层级:默认只显示一行摘要(依据 / 已省 / 口径 + 「配置规则」),细节全部收进抽屉。
2906
+ * 目的是让「这一格钱凭什么」一眼可读,同时把矛盾提示合并成一条带优先级的提示。
2907
+ * 规则本体存在宿主设置服务里,成本计算全在服务端(src/server/pricing.ts),界面不重复算。
2908
+ */
2909
+ /** 金额一律两位小数;服务端给的 4 位是计算精度,不是展示精度。 */
2910
+ const AMOUNT_DIGITS = 2;
2911
+ /** 「最值得补规则」最多列几条 —— 多于一屏就失去「最值得」的意义。 */
2912
+ const WORTH_RULE_LIMIT = 3;
2913
+ function PricingRulesPanel(props) {
2914
+ const { settings, state, pricing, channels } = props;
2915
+ const [expanded, setExpanded] = (0, react.useState)(false);
2916
+ const [draft, setDraft] = (0, react.useState)(null);
2917
+ const [editingKey, setEditingKey] = (0, react.useState)(null);
2918
+ const [editorError, setEditorError] = (0, react.useState)(null);
2919
+ const [templateId, setTemplateId] = (0, react.useState)("");
2920
+ const [notice, setNotice] = (0, react.useState)(null);
2921
+ const [showAllSavings, setShowAllSavings] = (0, react.useState)(false);
2922
+ const [showExplanation, setShowExplanation] = (0, react.useState)(false);
2923
+ const fileInput = (0, react.useRef)(null);
2924
+ const config = state.config;
2925
+ const rules = config.rules;
2926
+ const offPeakNowOf = (rule) => {
2927
+ if (pricing === null) return false;
2928
+ const index = rules.indexOf(rule);
2929
+ return index >= 0 && pricing.offPeakNow[index] === true;
2930
+ };
2931
+ const templates = pricing?.templates ?? [];
2932
+ const catalog = pricing?.catalog ?? null;
2933
+ const providerOptions = (0, react.useMemo)(() => channels.map((channel) => channel.provider), [channels]);
2934
+ const modelOptions = (0, react.useMemo)(() => {
2935
+ const ids = /* @__PURE__ */ new Set();
2936
+ for (const channel of channels) for (const model of channel.models) ids.add(model.id);
2937
+ for (const row of props.models?.rows ?? []) ids.add(row.model);
2938
+ return [...ids];
2939
+ }, [channels, props.models]);
2940
+ const save = async (next, done) => {
2941
+ if (await settings.save(next)) {
2942
+ setNotice(done);
2943
+ setDraft(null);
2944
+ setEditingKey(null);
2945
+ setEditorError(null);
2946
+ }
2947
+ };
2948
+ const startNew = () => {
2949
+ setDraft(blankRule(config));
2950
+ setEditingKey(null);
2951
+ setEditorError(null);
2952
+ };
2953
+ /** 「为它新增规则」:直接把 provider / model 预填进表单,省掉一次手抄。 */
2954
+ const startNewFor = (provider, model) => {
2955
+ setDraft({
2956
+ ...blankRule(config),
2957
+ provider,
2958
+ model
2959
+ });
2960
+ setEditingKey(null);
2961
+ setEditorError(null);
2962
+ };
2963
+ const startEdit = (rule) => {
2964
+ setDraft(structuredClone(rule));
2965
+ setEditingKey(ruleKeyOf(rule));
2966
+ setEditorError(null);
2967
+ };
2968
+ const commitDraft = async () => {
2969
+ if (draft === null) return;
2970
+ const problem = ruleProblemOf(draft);
2971
+ if (problem !== null) {
2972
+ setEditorError(problem);
2973
+ return;
2974
+ }
2975
+ const nextRules = [...rules];
2976
+ const editedIndex = editingKey === null ? -1 : nextRules.findIndex((rule) => ruleKeyOf(rule) === editingKey);
2977
+ const duplicateIndex = nextRules.findIndex((rule) => ruleKeyOf(rule) === ruleKeyOf(draft));
2978
+ if (editedIndex >= 0) {
2979
+ nextRules[editedIndex] = draft;
2980
+ if (duplicateIndex >= 0 && duplicateIndex !== editedIndex) nextRules.splice(duplicateIndex, 1);
2981
+ } else if (duplicateIndex >= 0) nextRules[duplicateIndex] = draft;
2982
+ else nextRules.push(draft);
2983
+ await save({
2984
+ ...config,
2985
+ rules: nextRules
2986
+ }, "规则已保存");
2987
+ };
2988
+ const removeRule = async (rule) => {
2989
+ await save({
2990
+ ...config,
2991
+ rules: rules.filter((candidate) => candidate !== rule)
2992
+ }, "规则已删除");
2993
+ };
2994
+ const applyTemplate = async () => {
2995
+ const template = templates.find((candidate) => candidate.id === templateId);
2996
+ if (template === void 0) return;
2997
+ const nextRules = [...rules];
2998
+ for (const rule of template.rules) {
2999
+ const index = nextRules.findIndex((candidate) => ruleKeyOf(candidate) === ruleKeyOf(rule));
3000
+ if (index >= 0) nextRules[index] = rule;
3001
+ else nextRules.push(rule);
3002
+ }
3003
+ await save({
3004
+ ...config,
3005
+ rules: nextRules
3006
+ }, "已套用模板「" + template.label + "」,数值请自行核对");
3007
+ };
3008
+ const exportRules = () => {
3009
+ downloadFile(exportFileName("pricing-rules", "json"), toJson(config), JSON_MIME);
3010
+ };
3011
+ const importRules = async (file) => {
3012
+ const parsed = parseImportedPricing(await file.text());
3013
+ if (parsed.config === null) {
3014
+ settings.noteError(parsed.error ?? "导入失败:文件内容无法识别");
3015
+ return;
3016
+ }
3017
+ await save(parsed.config, "规则已导入(整份替换)");
3018
+ };
3019
+ const coverage = pricing?.coverage ?? null;
3020
+ const savingsEntries = Object.entries(coverage?.savingsByCurrency ?? {}).filter(([, amount]) => amount > 0);
3021
+ const savingsDetail = coverage?.savingsDetail ?? [];
3022
+ const visibleSavings = showAllSavings ? savingsDetail : savingsDetail.slice(0, 3);
3023
+ const ruleCostEntries = Object.entries(coverage?.ruleCostByCurrency ?? {}).filter(([, amount]) => amount > 0);
3024
+ const worthRules = worthRuleCandidates(props.models, rules);
3025
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3026
+ className: pricing_module_css_default.panel,
3027
+ children: [
3028
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SummaryStrip, {
3029
+ coverage,
3030
+ rulesCount: rules.length,
3031
+ loading: state.status === "loading",
3032
+ savingsEntries,
3033
+ expanded,
3034
+ onToggle: () => setExpanded((previous) => !previous),
3035
+ showExplanation,
3036
+ onToggleExplanation: () => setShowExplanation((previous) => !previous)
3037
+ }),
3038
+ showExplanation && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExplanationBlock, {}),
3039
+ expanded && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3040
+ className: pricing_module_css_default.drawer,
3041
+ children: [
3042
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusNotice, { state }),
3043
+ notice !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3044
+ className: pricing_module_css_default.panelHint,
3045
+ children: notice
3046
+ }),
3047
+ ruleCostEntries.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3048
+ className: pricing_module_css_default.statStrip,
3049
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3050
+ className: pricing_module_css_default.statItem,
3051
+ children: [
3052
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3053
+ className: pricing_module_css_default.statName,
3054
+ children: "按当前规则试算(近 30 天,仅参照)"
3055
+ }),
3056
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3057
+ className: pricing_module_css_default.statNum,
3058
+ children: ruleCostEntries.map(([currency, amount]) => moneyText(currency, amount)).join(" / ")
3059
+ }),
3060
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3061
+ className: pricing_module_css_default.statNote,
3062
+ children: "不是账单:实际成本按「官方 > 规则 > 官方价 > 均价」逐格取值"
3063
+ })
3064
+ ]
3065
+ })
3066
+ }),
3067
+ catalog !== null && catalog.entries > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3068
+ className: pricing_module_css_default.catalogNote,
3069
+ children: [
3070
+ "已内置 ",
3071
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: String(catalog.entries) }),
3072
+ " 条厂商官方价目,",
3073
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: "无需套用模板即自动生效" }),
3074
+ "(你的规则优先于它)。来源:",
3075
+ catalog.sources.map((entry, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
3076
+ index > 0 && ";",
3077
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
3078
+ className: pricing_module_css_default.linkBtn,
3079
+ href: entry.source,
3080
+ target: "_blank",
3081
+ rel: "noreferrer",
3082
+ children: entry.providers.join(" / ")
3083
+ }),
3084
+ "(",
3085
+ entry.count,
3086
+ " 条,",
3087
+ entry.fetchedAt,
3088
+ " 抓取)"
3089
+ ] }, entry.source))
3090
+ ]
3091
+ }),
3092
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3093
+ className: pricing_module_css_default.toolbar,
3094
+ children: [
3095
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3096
+ className: pricing_module_css_default.actionBtn,
3097
+ value: templateId,
3098
+ onChange: (event) => setTemplateId(event.target.value),
3099
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3100
+ value: "",
3101
+ children: "选择厂商模板…"
3102
+ }), templates.map((template) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3103
+ value: template.id,
3104
+ children: template.label
3105
+ }, template.id))]
3106
+ }),
3107
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3108
+ className: pricing_module_css_default.actionBtn,
3109
+ disabled: templateId === "",
3110
+ onClick: () => void applyTemplate(),
3111
+ children: "套用模板"
3112
+ }),
3113
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3114
+ className: pricing_module_css_default.actionBtn + " " + pricing_module_css_default.primary,
3115
+ onClick: startNew,
3116
+ children: "+ 新增规则"
3117
+ }),
3118
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: pricing_module_css_default.toolbarSpacer }),
3119
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3120
+ className: pricing_module_css_default.linkBtn,
3121
+ disabled: rules.length === 0,
3122
+ onClick: exportRules,
3123
+ children: "导出 JSON"
3124
+ }),
3125
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3126
+ className: pricing_module_css_default.linkBtn,
3127
+ onClick: () => fileInput.current?.click(),
3128
+ children: "导入 JSON"
3129
+ }),
3130
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3131
+ className: pricing_module_css_default.linkBtn,
3132
+ disabled: rules.length === 0,
3133
+ onClick: () => void save({
3134
+ ...EMPTY_CONFIG,
3135
+ currency: config.currency
3136
+ }, "已清空规则:成本回到台账反推均价"),
3137
+ children: "清空规则"
3138
+ }),
3139
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3140
+ ref: fileInput,
3141
+ className: pricing_module_css_default.hiddenFile,
3142
+ type: "file",
3143
+ accept: "application/json,.json",
3144
+ onChange: (event) => {
3145
+ const file = event.target.files?.[0];
3146
+ event.target.value = "";
3147
+ if (file !== void 0) importRules(file);
3148
+ }
3149
+ })
3150
+ ]
3151
+ }),
3152
+ rules.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3153
+ className: pricing_module_css_default.empty,
3154
+ children: "还没有规则。点「+ 新增规则」手填,或从上方的厂商模板起步。"
3155
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
3156
+ className: pricing_module_css_default.ruleTable,
3157
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
3158
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "通道" }),
3159
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "模型" }),
3160
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "单位" }),
3161
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "输入" }),
3162
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "缓存读" }),
3163
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "缓存写" }),
3164
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "输出" }),
3165
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "币种" }),
3166
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "错峰" }),
3167
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "备注" }),
3168
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "操作" })
3169
+ ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: rules.map((rule) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
3170
+ className: editingKey === ruleKeyOf(rule) ? pricing_module_css_default.editing : "",
3171
+ children: [
3172
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3173
+ className: pricing_module_css_default.ruleName,
3174
+ children: rule.provider
3175
+ }) }),
3176
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3177
+ className: pricing_module_css_default.ruleName,
3178
+ children: rule.model
3179
+ }) }),
3180
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: UNIT_LABELS[rule.unit] }),
3181
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: rule.input }),
3182
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: rule.cacheRead }),
3183
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: rule.cacheWrite }),
3184
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: rule.output }),
3185
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: rule.currency }),
3186
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: rule.offPeak === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3187
+ className: shared_module_css_default.dim,
3188
+ children: "—"
3189
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3190
+ title: offPeakTooltipOf(rule),
3191
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3192
+ className: pricing_module_css_default.ruleName,
3193
+ children: offPeakWindowsText(rule)
3194
+ }), offPeakNowOf(rule) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3195
+ className: pricing_module_css_default.offPeakChip,
3196
+ children: "错峰中"
3197
+ })]
3198
+ }) }),
3199
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
3200
+ className: pricing_module_css_default.ruleNote,
3201
+ children: rule.note === "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3202
+ className: shared_module_css_default.dim,
3203
+ children: "—"
3204
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3205
+ title: rule.note,
3206
+ children: rule.note
3207
+ })
3208
+ }),
3209
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3210
+ className: pricing_module_css_default.linkBtn,
3211
+ onClick: () => startEdit(rule),
3212
+ children: "编辑"
3213
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3214
+ className: pricing_module_css_default.linkBtn,
3215
+ onClick: () => void removeRule(rule),
3216
+ children: "删除"
3217
+ })] })
3218
+ ]
3219
+ }, ruleKeyOf(rule))) })]
3220
+ }),
3221
+ draft !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RuleEditor, {
3222
+ draft,
3223
+ onChange: setDraft,
3224
+ onCancel: () => {
3225
+ setDraft(null);
3226
+ setEditorError(null);
3227
+ setEditingKey(null);
3228
+ },
3229
+ onSave: () => void commitDraft(),
3230
+ error: editorError,
3231
+ providerOptions,
3232
+ modelOptions
3233
+ }),
3234
+ worthRules.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3235
+ className: pricing_module_css_default.sectionBlock,
3236
+ children: [
3237
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3238
+ className: pricing_module_css_default.subLabel,
3239
+ children: "最值得补规则的模型"
3240
+ }),
3241
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3242
+ className: pricing_module_css_default.panelHint,
3243
+ children: "这些模型要么按台账均价估算、要么没有任何价格依据,但用量占比最高 —— 给它们补规则最能提升准确度。"
3244
+ }),
3245
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
3246
+ className: pricing_module_css_default.worthList,
3247
+ children: worthRules.map((candidate) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
3248
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3249
+ className: pricing_module_css_default.ruleName,
3250
+ children: candidate.model
3251
+ }),
3252
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3253
+ className: shared_module_css_default.dim,
3254
+ children: [" · ", candidate.provider]
3255
+ }),
3256
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3257
+ className: pricing_module_css_default.worthMeta,
3258
+ children: [
3259
+ fmtTokens(candidate.tokens),
3260
+ " tokens(",
3261
+ percentText(candidate.share),
3262
+ ")",
3263
+ candidate.estimated ? " · 现为估算" : " · 无价格依据"
3264
+ ]
3265
+ }),
3266
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3267
+ className: pricing_module_css_default.linkBtn,
3268
+ onClick: () => startNewFor(candidate.provider, candidate.model),
3269
+ children: "为它新增规则"
3270
+ })
3271
+ ] }, candidate.provider + "|" + candidate.model))
3272
+ })
3273
+ ]
3274
+ }),
3275
+ savingsDetail.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3276
+ className: pricing_module_css_default.sectionBlock,
3277
+ children: [
3278
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3279
+ className: pricing_module_css_default.subLabel,
3280
+ children: ["缓存命中省钱明细", coverage === null ? "" : "(" + coverage.from + " 起)"]
3281
+ }),
3282
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
3283
+ className: pricing_module_css_default.savingsList,
3284
+ children: visibleSavings.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
3285
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3286
+ className: pricing_module_css_default.ruleName,
3287
+ children: entry.model
3288
+ }),
3289
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3290
+ className: shared_module_css_default.dim,
3291
+ children: [" · ", entry.provider]
3292
+ }),
3293
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3294
+ className: pricing_module_css_default.worthMeta,
3295
+ children: [
3296
+ "命中缓存 ",
3297
+ fmtTokens(entry.cacheReadTokens),
3298
+ " tokens"
3299
+ ]
3300
+ }),
3301
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3302
+ className: pricing_module_css_default.saving,
3303
+ children: ["省 ", moneyText(entry.currency, entry.amount)]
3304
+ })
3305
+ ] }, entry.provider + "|" + entry.model))
3306
+ }),
3307
+ savingsDetail.length > 3 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3308
+ className: pricing_module_css_default.linkBtn,
3309
+ onClick: () => setShowAllSavings((previous) => !previous),
3310
+ children: showAllSavings ? "收起" : "展开全部(" + String(savingsDetail.length) + " 条)"
3311
+ })
3312
+ ]
3313
+ })
3314
+ ]
3315
+ })
3316
+ ]
3317
+ });
3318
+ }
3319
+ /**
3320
+ * 摘要行:默认收起的全部内容就这一行。
3321
+ * 「依据」用 /pricing 的 coverage 数模型个数,让用户一眼看出哪些模型可信、哪些只是估算。
3322
+ */
3323
+ function SummaryStrip(props) {
3324
+ const { coverage } = props;
3325
+ const days = coverage === null ? null : daysBetween(coverage.from, coverage.to);
3326
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3327
+ className: pricing_module_css_default.summaryStrip,
3328
+ children: [
3329
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3330
+ className: pricing_module_css_default.summaryBasis,
3331
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3332
+ className: pricing_module_css_default.summaryName,
3333
+ children: "这一格钱的依据"
3334
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3335
+ className: pricing_module_css_default.summaryValue,
3336
+ children: coverage === null ? props.loading ? "正在读取规则与覆盖度…" : "还没读到服务端覆盖度 —— 稍后自动重试" : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3337
+ "官方账单 ",
3338
+ days === null ? "—" : String(days),
3339
+ " 天 · 你的规则 ",
3340
+ String(coverage.ruleModels),
3341
+ " 个模型 · 内置官方价 ",
3342
+ String(coverage.catalogModels ?? 0),
3343
+ " 个模型 · 台账均价估算 ",
3344
+ String(coverage.averagedModels),
3345
+ " 个模型",
3346
+ coverage.unpricedModels > 0 && " · 无依据 " + String(coverage.unpricedModels) + " 个"
3347
+ ] })
3348
+ })]
3349
+ }),
3350
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3351
+ className: pricing_module_css_default.summarySaving,
3352
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3353
+ className: pricing_module_css_default.summaryName,
3354
+ children: "缓存命中已省"
3355
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3356
+ className: pricing_module_css_default.summaryValue + (props.savingsEntries.length > 0 ? " " + pricing_module_css_default.saving : ""),
3357
+ children: props.savingsEntries.length === 0 ? "—" : props.savingsEntries.map(([currency, amount]) => moneyText(currency, amount)).join(" / ")
3358
+ })]
3359
+ }),
3360
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3361
+ className: pricing_module_css_default.summaryActions,
3362
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3363
+ className: pricing_module_css_default.infoBtn + (props.showExplanation ? " " + pricing_module_css_default.infoBtnActive : ""),
3364
+ title: "来源角标、单位、错峰与多币种口径",
3365
+ onClick: props.onToggleExplanation,
3366
+ children: "ⓘ"
3367
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3368
+ className: pricing_module_css_default.actionBtn + " " + pricing_module_css_default.primary,
3369
+ onClick: props.onToggle,
3370
+ children: props.expanded ? "收起规则 ◂" : "配置规则 ▸"
3371
+ })]
3372
+ })
3373
+ ]
3374
+ });
3375
+ }
3376
+ /** 完整口径说明:默认不占正文,点 ⓘ 才出现。 */
3377
+ function ExplanationBlock() {
3378
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3379
+ className: pricing_module_css_default.explanation,
3380
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3381
+ className: pricing_module_css_default.explanationTitle,
3382
+ children: "口径说明"
3383
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
3384
+ className: pricing_module_css_default.explanationList,
3385
+ children: [
3386
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: "官方" }), ":台账记录的厂商账单金额,是既成事实,不会被规则覆盖。"] }),
3387
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: "自定" }), ":按你填的规则算出,匹配顺序 精确 → 通道通配 → 模型通配 → 全通配。"] }),
3388
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
3389
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: "官方价" }),
3390
+ ":按插件",
3391
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: "内置的厂商官方价目" }),
3392
+ "算出(自动生效,无需套用模板);你的规则优先于它。匹配只做精确 / 忽略大小写 / 去 ",
3393
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: "vendor/" }),
3394
+ " 前缀,不做模糊匹配。"
3395
+ ] }),
3396
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: "估算" }), ":按台账反推的均价推算,仅供参照,不是账单。"] }),
3397
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: "—" }), ":没有价格依据(规则与内置价目都没命中,台账也没有成本数据)。"] }),
3398
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "单位与按次计费:规则里显式声明「每百万 / 每千 tokens / 每次调用」;按次计费只用「输入」档当每次调用价。" }),
3399
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
3400
+ "错峰:窗口按",
3401
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: "本地时间" }),
3402
+ "判断,起止相同表示全天,",
3403
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: "from" }),
3404
+ " 大于 ",
3405
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: "to" }),
3406
+ " 表示跨午夜;每个窗口可按",
3407
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: "每天 / 仅工作日 / 仅周末" }),
3408
+ "限定生效的星期(不选即每天)。跨天范围拿不到小时分布,按标准价算。"
3409
+ ] }),
3410
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "多币种不换算、不合计:规则与内置价目各按文档原币种算(DeepSeek 取中文文档的 CNY,Kimi 为 CNY,OpenRouter / Z.ai 为 USD),金额按币种分行显示。" }),
3411
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
3412
+ "规则存在宿主设置服务里(命名空间 ",
3413
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: "dsh-key-usage" }),
3414
+ "),插件不写任何文件。"
3415
+ ] })
1552
3416
  ]
1553
- })
1554
- ] });
3417
+ })]
3418
+ });
1555
3419
  }
1556
- function coverageText(status, skipped) {
1557
- if (status === "unavailable") return "会话数据不可用(宿主未提供会话服务或全部读取失败)";
1558
- return "会话数据不完整:" + String(skipped) + " 个会话读取失败,未计入统计";
3420
+ /**
3421
+ * 状态提示:优先级从高到低只渲染一条,避免「写入被拒绝」和「无法保存」同时出现。
3422
+ * 1) 命名空间不可写 / 取不到配置表单服务 —— 规则根本存不下去,说清楚「重启 DSH 后可用」;
3423
+ * 2) 刚发生的写入失败(如乐观锁冲突)—— 可重试;
3424
+ * 3) 正常 —— 不显示任何提示。
3425
+ */
3426
+ function StatusNotice(props) {
3427
+ const { state } = props;
3428
+ if (state.status === "unavailable" || state.status === "ready" && !state.writable) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3429
+ className: pricing_module_css_default.warning,
3430
+ children: [
3431
+ "规则暂不可编辑:宿主还没加载本插件的配置(",
3432
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: "重启 DSH 后可用" }),
3433
+ ");现在仍可导入 / 导出 JSON。"
3434
+ ]
3435
+ });
3436
+ if (state.error !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3437
+ className: pricing_module_css_default.error,
3438
+ children: [
3439
+ "保存失败:",
3440
+ state.error,
3441
+ ",可重试"
3442
+ ]
3443
+ });
3444
+ return null;
1559
3445
  }
1560
- function baseNameOf(path) {
1561
- const parts = path.replace(/[\\/]+$/, "").split(/[\\/]/);
1562
- return parts[parts.length - 1] === "" ? path : parts[parts.length - 1];
3446
+ /** 两位小数 + 币种符号;未知币种保留代码前缀,避免把 USD 当 CNY 渲染。 */
3447
+ function moneyText(currency, amount) {
3448
+ return currencySymbolOf(currency) + amount.toFixed(AMOUNT_DIGITS);
3449
+ }
3450
+ function percentText(share) {
3451
+ return (share * 100).toFixed(1) + "%";
3452
+ }
3453
+ /** 覆盖区间的自然天数(含首尾),用作「官方账单 X 天」的口径。 */
3454
+ function daysBetween(from, to) {
3455
+ const start = (/* @__PURE__ */ new Date(from + "T00:00:00")).getTime();
3456
+ const end = (/* @__PURE__ */ new Date(to + "T00:00:00")).getTime();
3457
+ if (Number.isNaN(start) || Number.isNaN(end) || end < start) return null;
3458
+ return Math.round((end - start) / 864e5) + 1;
3459
+ }
3460
+ /** 错峰说明进 tooltip:规则表因此不被一整列文字撑开。 */
3461
+ function offPeakTooltipOf(rule) {
3462
+ const windows = offPeakWindowsText(rule);
3463
+ if (rule.offPeak === null) return windows;
3464
+ const peak = "标准价 " + String(rule.input) + " / " + String(rule.cacheRead) + " / " + String(rule.cacheWrite) + " / " + String(rule.output);
3465
+ const offPeak = "错峰价 " + String(rule.offPeak.input) + " / " + String(rule.offPeak.cacheRead) + " / " + String(rule.offPeak.cacheWrite) + " / " + String(rule.offPeak.output);
3466
+ return "错峰时段(本地时间,跨午夜用 from > to 表示;括号内是生效的星期):" + windows + "\n" + peak + "\n" + offPeak + "\n四档顺序:输入 / 缓存读 / 缓存写 / 输出";
3467
+ }
3468
+ /**
3469
+ * 「最值得补规则」的判定:先筛出**没有价格依据**的模型,再按近 30 天 tokens 占比倒序取前 3。
3470
+ * 「没有价格依据」用两处数据交叉确认:/pricing 的 coverage 计数只给总数,
3471
+ * 逐模型是否命中则用 /models 的 costSource —— 它正是成本单元格上那个角标,口径一致。
3472
+ * `catalog`(内置官方价目)已有官方价,不算缺口,因此不列入候选。
3473
+ */
3474
+ function worthRuleCandidates(models, rules) {
3475
+ const rows = (models?.rows ?? []).filter((row) => row.tokens > 0);
3476
+ if (rows.length === 0) return [];
3477
+ const unpriced = rows.filter((row) => {
3478
+ if (row.costSource === "rule" || row.costSource === "official" || row.costSource === "catalog") return false;
3479
+ return row.costSource === "estimate" || row.costSource === "none" || !matchesAnyRule(rules, row.provider, row.model);
3480
+ });
3481
+ const total = unpriced.reduce((sum, row) => sum + row.tokens, 0);
3482
+ if (total <= 0) return [];
3483
+ return unpriced.map((row) => ({
3484
+ provider: row.provider,
3485
+ model: row.model,
3486
+ tokens: row.tokens,
3487
+ share: row.tokens / total,
3488
+ estimated: row.costSource === "estimate"
3489
+ })).sort((first, second) => second.tokens - first.tokens).slice(0, WORTH_RULE_LIMIT);
3490
+ }
3491
+ /** 与服务端 matchPricingRule 同序的粗判:精确 → 通道通配 → 模型通配 → 全通配。 */
3492
+ function matchesAnyRule(rules, provider, model) {
3493
+ return [
3494
+ (rule) => rule.provider === provider && rule.model === model,
3495
+ (rule) => rule.provider === provider && rule.model === "*",
3496
+ (rule) => rule.provider === "*" && rule.model === model,
3497
+ (rule) => rule.provider === "*" && rule.model === "*"
3498
+ ].some((matches) => rules.some(matches));
3499
+ }
3500
+ /** 单条规则的编辑器:单位换算、错峰窗口都在这里显式呈现,避免「按千 / 按百万」填错 1000 倍。 */
3501
+ function RuleEditor(props) {
3502
+ const { draft, onChange } = props;
3503
+ const numberField = (label, key, value, extra) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3504
+ className: pricing_module_css_default.field,
3505
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3506
+ type: "number",
3507
+ min: 0,
3508
+ step: "any",
3509
+ value: String(value),
3510
+ onChange: (event) => {
3511
+ const next = Number(event.target.value);
3512
+ const safe = Number.isFinite(next) && next >= 0 ? next : 0;
3513
+ if (extra?.onChange !== void 0) extra.onChange(safe);
3514
+ else onChange({
3515
+ ...draft,
3516
+ [key]: safe
3517
+ });
3518
+ }
3519
+ })]
3520
+ }, label);
3521
+ const windows = draft.offPeak?.windows ?? [];
3522
+ const setWindows = (next) => {
3523
+ if (next.length === 0) {
3524
+ onChange({
3525
+ ...draft,
3526
+ offPeak: null
3527
+ });
3528
+ return;
3529
+ }
3530
+ onChange({
3531
+ ...draft,
3532
+ offPeak: {
3533
+ windows: next,
3534
+ input: draft.offPeak?.input ?? draft.input,
3535
+ cacheRead: draft.offPeak?.cacheRead ?? draft.cacheRead,
3536
+ cacheWrite: draft.offPeak?.cacheWrite ?? draft.cacheWrite,
3537
+ output: draft.offPeak?.output ?? draft.output
3538
+ }
3539
+ });
3540
+ };
3541
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3542
+ className: pricing_module_css_default.editor,
3543
+ children: [
3544
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3545
+ className: pricing_module_css_default.editorGrid,
3546
+ children: [
3547
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3548
+ className: pricing_module_css_default.field,
3549
+ children: [
3550
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", { children: "通道 id(* = 任意通道)" }),
3551
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3552
+ list: "dsh-ku-providers",
3553
+ value: draft.provider,
3554
+ onChange: (event) => onChange({
3555
+ ...draft,
3556
+ provider: event.target.value
3557
+ })
3558
+ }),
3559
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("datalist", {
3560
+ id: "dsh-ku-providers",
3561
+ children: props.providerOptions.map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", { value: provider }, provider))
3562
+ })
3563
+ ]
3564
+ }),
3565
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3566
+ className: pricing_module_css_default.field,
3567
+ children: [
3568
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", { children: "模型 id(* = 该通道全部)" }),
3569
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3570
+ list: "dsh-ku-models",
3571
+ value: draft.model,
3572
+ onChange: (event) => onChange({
3573
+ ...draft,
3574
+ model: event.target.value
3575
+ })
3576
+ }),
3577
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("datalist", {
3578
+ id: "dsh-ku-models",
3579
+ children: props.modelOptions.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", { value: model }, model))
3580
+ })
3581
+ ]
3582
+ }),
3583
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3584
+ className: pricing_module_css_default.field,
3585
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", { children: "计价单位" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
3586
+ value: draft.unit,
3587
+ onChange: (event) => onChange({
3588
+ ...draft,
3589
+ unit: event.target.value
3590
+ }),
3591
+ children: UNIT_OPTIONS.map((unit) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3592
+ value: unit,
3593
+ children: UNIT_LABELS[unit]
3594
+ }, unit))
3595
+ })]
3596
+ }),
3597
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3598
+ className: pricing_module_css_default.field,
3599
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", { children: "币种" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
3600
+ value: draft.currency,
3601
+ onChange: (event) => onChange({
3602
+ ...draft,
3603
+ currency: event.target.value
3604
+ }),
3605
+ children: CURRENCY_OPTIONS.map((currency) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3606
+ value: currency,
3607
+ children: currency
3608
+ }, currency))
3609
+ })]
3610
+ }),
3611
+ numberField("输入(未命中缓存)", "input", draft.input),
3612
+ numberField("缓存读(命中)", "cacheRead", draft.cacheRead),
3613
+ numberField("缓存写", "cacheWrite", draft.cacheWrite),
3614
+ numberField("输出", "output", draft.output),
3615
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3616
+ className: pricing_module_css_default.field + " " + pricing_module_css_default.fieldWide,
3617
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", { children: "备注(建议写价目来源与日期)" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3618
+ value: draft.note,
3619
+ onChange: (event) => onChange({
3620
+ ...draft,
3621
+ note: event.target.value
3622
+ })
3623
+ })]
3624
+ })
3625
+ ]
3626
+ }),
3627
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3628
+ className: pricing_module_css_default.subLabel,
3629
+ children: "错峰时段(本地时间;留空表示没有错峰价)"
3630
+ }),
3631
+ windows.map((window, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3632
+ className: pricing_module_css_default.windowRow,
3633
+ children: [
3634
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3635
+ value: window.from,
3636
+ placeholder: "00:00",
3637
+ onChange: (event) => setWindows(windows.map((candidate, position) => position === index ? {
3638
+ ...candidate,
3639
+ from: event.target.value
3640
+ } : candidate))
3641
+ }),
3642
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3643
+ className: shared_module_css_default.dim,
3644
+ children: "至"
3645
+ }),
3646
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3647
+ value: window.to,
3648
+ placeholder: "09:00",
3649
+ onChange: (event) => setWindows(windows.map((candidate, position) => position === index ? {
3650
+ ...candidate,
3651
+ to: event.target.value
3652
+ } : candidate))
3653
+ }),
3654
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
3655
+ value: dayScopeOf(window.days),
3656
+ onChange: (event) => setWindows(windows.map((candidate, position) => position === index ? withDayScope(candidate, event.target.value) : candidate)),
3657
+ children: DAY_SCOPE_OPTIONS.map((scope) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3658
+ value: scope,
3659
+ children: DAY_SCOPE_LABELS[scope]
3660
+ }, scope))
3661
+ }),
3662
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3663
+ className: pricing_module_css_default.linkBtn,
3664
+ onClick: () => setWindows(windows.filter((_, position) => position !== index)),
3665
+ children: "移除"
3666
+ })
3667
+ ]
3668
+ }, index)),
3669
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3670
+ className: pricing_module_css_default.actionBtn,
3671
+ onClick: () => setWindows([...windows, {
3672
+ from: "00:00",
3673
+ to: "08:00"
3674
+ }]),
3675
+ children: "+ 加时间窗"
3676
+ }),
3677
+ draft.offPeak !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3678
+ className: pricing_module_css_default.subLabel,
3679
+ children: "错峰价(留空即与标准价相同)"
3680
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3681
+ className: pricing_module_css_default.editorGrid,
3682
+ children: [
3683
+ numberField("错峰输入", "input", draft.offPeak.input, { onChange: (value) => patchOffPeak(draft, onChange, { input: value }) }),
3684
+ numberField("错峰缓存读", "cacheRead", draft.offPeak.cacheRead, { onChange: (value) => patchOffPeak(draft, onChange, { cacheRead: value }) }),
3685
+ numberField("错峰缓存写", "cacheWrite", draft.offPeak.cacheWrite, { onChange: (value) => patchOffPeak(draft, onChange, { cacheWrite: value }) }),
3686
+ numberField("错峰输出", "output", draft.offPeak.output, { onChange: (value) => patchOffPeak(draft, onChange, { output: value }) })
3687
+ ]
3688
+ })] }),
3689
+ props.error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3690
+ className: pricing_module_css_default.editorError,
3691
+ children: props.error
3692
+ }),
3693
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3694
+ className: pricing_module_css_default.editorHint,
3695
+ children: [
3696
+ "正在编辑:",
3697
+ ruleLabelOf(draft),
3698
+ " · 单位 ",
3699
+ UNIT_LABELS[draft.unit]
3700
+ ]
3701
+ }),
3702
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3703
+ className: pricing_module_css_default.editorActions,
3704
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3705
+ className: pricing_module_css_default.actionBtn + " " + pricing_module_css_default.primary,
3706
+ onClick: props.onSave,
3707
+ children: "保存规则"
3708
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3709
+ className: pricing_module_css_default.actionBtn,
3710
+ onClick: props.onCancel,
3711
+ children: "取消"
3712
+ })]
3713
+ })
3714
+ ]
3715
+ });
3716
+ }
3717
+ function patchOffPeak(draft, onChange, patch) {
3718
+ if (draft.offPeak === null) return;
3719
+ onChange({
3720
+ ...draft,
3721
+ offPeak: {
3722
+ ...draft.offPeak,
3723
+ ...patch
3724
+ }
3725
+ });
3726
+ }
3727
+ /**
3728
+ * 单个窗口的星期范围:三选一直接改写 `days`。
3729
+ * 「每天」删掉该字段(而不是写 [1..7]),这样升级前保存的规则往返后字节一致;
3730
+ * 手改 JSON 得到的自定义组合在下拉里显示为「每天」,但只要用户不碰这个下拉,原值原样保留。
3731
+ */
3732
+ function withDayScope(window, scope) {
3733
+ const days = DAY_SCOPE_DAYS[scope];
3734
+ if (days === void 0) {
3735
+ const { from, to } = window;
3736
+ return {
3737
+ from,
3738
+ to
3739
+ };
3740
+ }
3741
+ return {
3742
+ ...window,
3743
+ days: [...days]
3744
+ };
1563
3745
  }
1564
3746
  //#endregion
1565
- //#region \0dsh-css:D:\EasyJob\.plugin\dsh-key-usage\src\client\styles\quota.module.css.mjs
1566
- const css$1 = ".F7iUHa_quotaList{flex-direction:column;gap:12px;display:flex}.F7iUHa_quotaCard{background:var(--dsw-alias-bg-layer-2,#1d1d22);border:1px solid var(--dsw-alias-border-l1,#2c2c34);border-radius:12px;padding:16px 18px}.F7iUHa_quotaHead{flex-wrap:wrap;align-items:center;gap:10px;margin-bottom:14px;display:flex}.F7iUHa_quotaTitle{color:var(--dsw-alias-label-primary,#e8e8ec);font-size:13px;font-weight:600}.F7iUHa_quotaEnv{color:var(--dsw-alias-label-tertiary,#67676f);margin-left:8px;font-family:ui-monospace,Consolas,monospace;font-size:10px}.F7iUHa_quotaProvider{color:var(--dsw-alias-label-tertiary,#67676f);margin-left:auto;font-size:11.5px}.F7iUHa_liveChip{border:1px solid var(--dsw-alias-state-success-primary,#3fb950);color:var(--dsw-alias-state-success-primary,#3fb950);border-radius:999px;padding:1px 7px;font-size:10px;line-height:1.6}.F7iUHa_balanceStrip{flex-wrap:wrap;align-items:baseline;gap:10px 44px;display:flex}.F7iUHa_balanceItem .F7iUHa_bsLabel{color:var(--dsw-alias-label-tertiary,#67676f);margin-bottom:2px;font-size:11px}.F7iUHa_balanceItem .F7iUHa_bsValue{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary,#e8e8ec);font-size:16px;font-weight:650}.F7iUHa_windowRow{flex-wrap:wrap;gap:18px;display:flex}.F7iUHa_windowItem{flex:1;min-width:150px}.F7iUHa_windowHead{justify-content:space-between;align-items:baseline;margin-bottom:5px;display:flex}.F7iUHa_windowName{color:var(--dsw-alias-label-secondary,#9a9aa6);font-size:11.5px}.F7iUHa_windowValue{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary,#e8e8ec);font-size:12px;font-weight:600}.F7iUHa_windowReset{color:var(--dsw-alias-label-tertiary,#67676f);margin-top:3px;font-size:10.5px}.F7iUHa_windowBar{background:var(--dsw-alias-fill-l2,#80808c2e);border-radius:4px;height:6px;overflow:hidden}.F7iUHa_windowBar>i{border-radius:4px;height:100%;display:block}.F7iUHa_snapshotHint{color:var(--dsw-alias-label-tertiary,#67676f);margin-top:10px;font-size:10.5px}.F7iUHa_snapshotStale{color:var(--dsw-alias-state-warning-primary,#e0a13c);margin-top:10px;font-size:10.5px}";
3747
+ //#region \0dsh-css:src/client/styles/quota.module.css.mjs
3748
+ const css$1 = ".ohwLxW_quotaList{flex-direction:column;gap:12px;display:flex}.ohwLxW_quotaCard{background:var(--dsw-alias-bg-layer-2,#1d1d22);border:1px solid var(--dsw-alias-border-l1,#2c2c34);border-radius:12px;padding:16px 18px}.ohwLxW_quotaHead{flex-wrap:wrap;align-items:center;gap:10px;margin-bottom:14px;display:flex}.ohwLxW_quotaTitle{color:var(--dsw-alias-label-primary,#e8e8ec);font-size:13px;font-weight:600}.ohwLxW_quotaEnv{color:var(--dsw-alias-label-tertiary,#67676f);margin-left:8px;font-family:ui-monospace,Consolas,monospace;font-size:10px}.ohwLxW_quotaProvider{color:var(--dsw-alias-label-tertiary,#67676f);margin-left:auto;font-size:11.5px}.ohwLxW_liveChip{border:1px solid var(--dsw-alias-state-success-primary,#3fb950);color:var(--dsw-alias-state-success-primary,#3fb950);border-radius:999px;padding:1px 7px;font-size:10px;line-height:1.6}.ohwLxW_balanceStrip{flex-wrap:wrap;align-items:baseline;gap:10px 44px;display:flex}.ohwLxW_balanceItem .ohwLxW_bsLabel{color:var(--dsw-alias-label-tertiary,#67676f);margin-bottom:2px;font-size:11px}.ohwLxW_balanceItem .ohwLxW_bsValue{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary,#e8e8ec);font-size:16px;font-weight:650}.ohwLxW_windowRow{flex-wrap:wrap;gap:18px;display:flex}.ohwLxW_windowItem{flex:1;min-width:150px}.ohwLxW_windowHead{justify-content:space-between;align-items:baseline;margin-bottom:5px;display:flex}.ohwLxW_windowName{color:var(--dsw-alias-label-secondary,#9a9aa6);font-size:11.5px}.ohwLxW_windowValue{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary,#e8e8ec);font-size:12px;font-weight:600}.ohwLxW_windowReset{color:var(--dsw-alias-label-tertiary,#67676f);margin-top:3px;font-size:10.5px}.ohwLxW_windowBar{background:var(--dsw-alias-fill-l2,#80808c2e);border-radius:4px;height:6px;overflow:hidden}.ohwLxW_windowBar>i{border-radius:4px;height:100%;display:block}.ohwLxW_snapshotHint{color:var(--dsw-alias-label-tertiary,#67676f);margin-top:10px;font-size:10.5px}.ohwLxW_snapshotStale{color:var(--dsw-alias-state-warning-primary,#e0a13c);margin-top:10px;font-size:10.5px}";
1567
3749
  const tagId$1 = "dsh-key-usage/quota.module.css";
1568
3750
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
1569
3751
  const tag = document.createElement("style");
@@ -1578,26 +3760,26 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1578
3760
  css: css$1
1579
3761
  });
1580
3762
  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",
1588
- "windowRow": "F7iUHa_windowRow",
1589
- "snapshotHint": "F7iUHa_snapshotHint",
1590
- "bsLabel": "F7iUHa_bsLabel",
1591
- "snapshotStale": "F7iUHa_snapshotStale",
1592
- "balanceStrip": "F7iUHa_balanceStrip",
1593
- "windowValue": "F7iUHa_windowValue",
1594
- "quotaHead": "F7iUHa_quotaHead",
1595
- "balanceItem": "F7iUHa_balanceItem",
1596
- "quotaCard": "F7iUHa_quotaCard",
1597
- "windowHead": "F7iUHa_windowHead",
1598
- "windowBar": "F7iUHa_windowBar",
1599
- "quotaTitle": "F7iUHa_quotaTitle",
1600
- "bsValue": "F7iUHa_bsValue"
3763
+ "balanceItem": "ohwLxW_balanceItem",
3764
+ "balanceStrip": "ohwLxW_balanceStrip",
3765
+ "bsLabel": "ohwLxW_bsLabel",
3766
+ "bsValue": "ohwLxW_bsValue",
3767
+ "liveChip": "ohwLxW_liveChip",
3768
+ "quotaCard": "ohwLxW_quotaCard",
3769
+ "quotaEnv": "ohwLxW_quotaEnv",
3770
+ "quotaHead": "ohwLxW_quotaHead",
3771
+ "quotaList": "ohwLxW_quotaList",
3772
+ "quotaProvider": "ohwLxW_quotaProvider",
3773
+ "quotaTitle": "ohwLxW_quotaTitle",
3774
+ "snapshotHint": "ohwLxW_snapshotHint",
3775
+ "snapshotStale": "ohwLxW_snapshotStale",
3776
+ "windowBar": "ohwLxW_windowBar",
3777
+ "windowHead": "ohwLxW_windowHead",
3778
+ "windowItem": "ohwLxW_windowItem",
3779
+ "windowName": "ohwLxW_windowName",
3780
+ "windowReset": "ohwLxW_windowReset",
3781
+ "windowRow": "ohwLxW_windowRow",
3782
+ "windowValue": "ohwLxW_windowValue"
1601
3783
  };
1602
3784
  //#endregion
1603
3785
  //#region src/client/tabs/QuotaTab.tsx
@@ -1639,113 +3821,123 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1639
3821
  const snapshotOf = (provider) => props.quotas.find((item) => item.provider === provider) ?? null;
1640
3822
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1641
3823
  className: quota_module_css_default.quotaList,
1642
- children: [props.channels.map((channel) => {
1643
- const snapshot = snapshotOf(channel.provider);
1644
- const balance = snapshot?.balance ?? null;
1645
- const plan = snapshot?.plan ?? null;
1646
- const isLive = snapshot?.source === "live";
1647
- const windows = plan !== null && Array.isArray(plan.windows) ? plan.windows : null;
1648
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1649
- className: quota_module_css_default.quotaCard,
1650
- children: [
1651
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1652
- className: quota_module_css_default.quotaHead,
1653
- children: [
1654
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1655
- className: shared_module_css_default.statusDot,
1656
- style: { background: statusColorOf(channel.status) }
1657
- }),
1658
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1659
- className: quota_module_css_default.quotaTitle,
1660
- children: channel.displayName
1661
- }),
1662
- isLive && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1663
- className: quota_module_css_default.liveChip,
1664
- children: "实时"
1665
- }),
1666
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1667
- className: quota_module_css_default.quotaProvider,
1668
- children: [
1669
- channel.provider,
1670
- " · ",
1671
- channel.costLabel
1672
- ]
3824
+ children: [
3825
+ props.channels.map((channel) => {
3826
+ const snapshot = snapshotOf(channel.provider);
3827
+ const balance = snapshot?.balance ?? null;
3828
+ const plan = snapshot?.plan ?? null;
3829
+ const isLive = snapshot?.source === "live";
3830
+ const windows = plan !== null && Array.isArray(plan.windows) ? plan.windows : null;
3831
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3832
+ className: quota_module_css_default.quotaCard,
3833
+ children: [
3834
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3835
+ className: quota_module_css_default.quotaHead,
3836
+ children: [
3837
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3838
+ className: shared_module_css_default.statusDot,
3839
+ style: { background: statusColorOf(channel.status) }
3840
+ }),
3841
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3842
+ className: quota_module_css_default.quotaTitle,
3843
+ children: channel.displayName
3844
+ }),
3845
+ isLive && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3846
+ className: quota_module_css_default.liveChip,
3847
+ children: "实时"
3848
+ }),
3849
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3850
+ className: quota_module_css_default.quotaProvider,
3851
+ children: [
3852
+ channel.provider,
3853
+ " · ",
3854
+ channel.costLabel
3855
+ ]
3856
+ })
3857
+ ]
3858
+ }),
3859
+ balance !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3860
+ className: quota_module_css_default.balanceStrip,
3861
+ children: orderedBalanceEntries(balance).map(([key, value]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3862
+ className: quota_module_css_default.balanceItem,
3863
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3864
+ className: quota_module_css_default.bsLabel,
3865
+ children: BALANCE_LABELS[key] ?? key
3866
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3867
+ className: quota_module_css_default.bsValue,
3868
+ children: String(value)
3869
+ })]
3870
+ }, key))
3871
+ }),
3872
+ balance === null && windows !== null && windows.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3873
+ className: quota_module_css_default.windowRow,
3874
+ children: orderedWindows(windows).map((window) => {
3875
+ const percent = typeof window.percent === "number" ? window.percent : 0;
3876
+ const key = typeof window.key === "string" ? window.key : "window";
3877
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3878
+ className: quota_module_css_default.windowItem,
3879
+ children: [
3880
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3881
+ className: quota_module_css_default.windowHead,
3882
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3883
+ className: quota_module_css_default.windowName,
3884
+ children: WINDOW_LABELS[key] ?? key
3885
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3886
+ className: quota_module_css_default.windowValue,
3887
+ children: [percent, "%"]
3888
+ })]
3889
+ }),
3890
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3891
+ className: quota_module_css_default.windowBar,
3892
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { style: {
3893
+ width: Math.max(percent, 0) + "%",
3894
+ background: quotaBarColor(percent)
3895
+ } })
3896
+ }),
3897
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3898
+ className: quota_module_css_default.windowReset,
3899
+ children: resetTextOf(window.resetsAt)
3900
+ })
3901
+ ]
3902
+ }, key);
1673
3903
  })
1674
- ]
1675
- }),
1676
- balance !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1677
- className: quota_module_css_default.balanceStrip,
1678
- children: orderedBalanceEntries(balance).map(([key, value]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1679
- className: quota_module_css_default.balanceItem,
1680
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1681
- className: quota_module_css_default.bsLabel,
1682
- children: BALANCE_LABELS[key] ?? key
1683
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1684
- className: quota_module_css_default.bsValue,
1685
- children: String(value)
1686
- })]
1687
- }, key))
1688
- }),
1689
- balance === null && windows !== null && windows.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1690
- className: quota_module_css_default.windowRow,
1691
- children: orderedWindows(windows).map((window) => {
1692
- const percent = typeof window.percent === "number" ? window.percent : 0;
1693
- const key = typeof window.key === "string" ? window.key : "window";
1694
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1695
- className: quota_module_css_default.windowItem,
1696
- children: [
1697
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1698
- className: quota_module_css_default.windowHead,
1699
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1700
- className: quota_module_css_default.windowName,
1701
- children: WINDOW_LABELS[key] ?? key
1702
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1703
- className: quota_module_css_default.windowValue,
1704
- children: [percent, "%"]
1705
- })]
1706
- }),
1707
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1708
- className: quota_module_css_default.windowBar,
1709
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { style: {
1710
- width: Math.max(percent, 0) + "%",
1711
- background: quotaBarColor(percent)
1712
- } })
1713
- }),
1714
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1715
- className: quota_module_css_default.windowReset,
1716
- children: resetTextOf(window.resetsAt)
1717
- })
1718
- ]
1719
- }, key);
3904
+ }),
3905
+ balance === null && (windows === null || windows.length === 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3906
+ className: shared_module_css_default.dim,
3907
+ children: emptyReasonOf(channel, snapshot)
3908
+ }),
3909
+ isLive && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3910
+ className: quota_module_css_default.snapshotHint,
3911
+ children: [
3912
+ "实时查询于 ",
3913
+ freshTextOf(snapshot?.fetchedAt ?? null),
3914
+ "(来自官方接口,最多缓存 1 分钟)"
3915
+ ]
3916
+ }),
3917
+ snapshot !== null && !isLive && snapshot.supported && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3918
+ className: staleDaysOf(snapshot.updatedAt) >= 1 ? quota_module_css_default.snapshotStale : quota_module_css_default.snapshotHint,
3919
+ children: [
3920
+ "实时查询未成功(",
3921
+ errorTextOf(snapshot.error),
3922
+ ")",
3923
+ snapshot.updatedAt === null ? ",且暂无可用快照 —— 点右上角「刷新」重试" : ",显示快照 · 快照时间 " + fmtDateTime(snapshot.updatedAt) + (staleDaysOf(snapshot.updatedAt) >= 1 ? ",已 " + staleDaysOf(snapshot.updatedAt) + " 天未更新" : "")
3924
+ ]
1720
3925
  })
1721
- }),
1722
- balance === null && (windows === null || windows.length === 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1723
- className: shared_module_css_default.dim,
1724
- children: emptyReasonOf(channel, snapshot)
1725
- }),
1726
- isLive && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1727
- className: quota_module_css_default.snapshotHint,
1728
- children: [
1729
- "实时查询于 ",
1730
- freshTextOf(snapshot?.fetchedAt ?? null),
1731
- "(来自官方接口,最多缓存 1 分钟)"
1732
- ]
1733
- }),
1734
- snapshot !== null && !isLive && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1735
- className: staleDaysOf(snapshot.updatedAt) >= 1 ? quota_module_css_default.snapshotStale : quota_module_css_default.snapshotHint,
1736
- children: [
1737
- "实时查询未成功(",
1738
- errorTextOf(snapshot.error),
1739
- "),显示本机快照",
1740
- snapshot.updatedAt === null ? "" : " · 快照时间 " + fmtDateTime(snapshot.updatedAt) + (staleDaysOf(snapshot.updatedAt) >= 1 ? ",已 " + staleDaysOf(snapshot.updatedAt) + " 天未更新" : "")
1741
- ]
1742
- })
1743
- ]
1744
- }, channel.provider);
1745
- }), props.channels.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1746
- className: shared_module_css_default.empty,
1747
- children: "未读取到通道配置"
1748
- })]
3926
+ ]
3927
+ }, channel.provider);
3928
+ }),
3929
+ props.channels.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3930
+ className: shared_module_css_default.empty,
3931
+ children: "未读取到通道配置"
3932
+ }),
3933
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PricingRulesPanel, {
3934
+ settings: props.pricingSettings,
3935
+ state: props.pricingState,
3936
+ pricing: props.pricing,
3937
+ channels: props.channels,
3938
+ models: props.models
3939
+ })
3940
+ ]
1749
3941
  });
1750
3942
  }
1751
3943
  /** 实时结果的相对时间文案。 */
@@ -1804,6 +3996,69 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1804
3996
  //#endregion
1805
3997
  //#region src/client/tabs/SessionsTab.tsx
1806
3998
  /** 会话用量:按会话列出 tokens / 命中率 / 时长,按最近活跃时按日分组。 */
3999
+ /** 导出列:标题可能带逗号或换行,由 toCsv 统一转义。 */
4000
+ const EXPORT_COLUMNS = [
4001
+ {
4002
+ key: "id",
4003
+ label: "会话 id",
4004
+ value: (row) => row.id
4005
+ },
4006
+ {
4007
+ key: "title",
4008
+ label: "标题",
4009
+ value: (row) => row.title
4010
+ },
4011
+ {
4012
+ key: "workspace",
4013
+ label: "工作区",
4014
+ value: (row) => row.workspace
4015
+ },
4016
+ {
4017
+ key: "provider",
4018
+ label: "通道",
4019
+ value: (row) => row.provider
4020
+ },
4021
+ {
4022
+ key: "firstTime",
4023
+ label: "首次时间",
4024
+ value: (row) => fmtDateTime(row.firstTime)
4025
+ },
4026
+ {
4027
+ key: "lastTime",
4028
+ label: "最近时间",
4029
+ value: (row) => fmtDateTime(row.lastTime)
4030
+ },
4031
+ {
4032
+ key: "calls",
4033
+ label: "调用",
4034
+ value: (row) => row.calls
4035
+ },
4036
+ {
4037
+ key: "tokens",
4038
+ label: "Tokens",
4039
+ value: (row) => row.tokens
4040
+ },
4041
+ {
4042
+ key: "cacheHitRate",
4043
+ label: "命中率(%)",
4044
+ value: (row) => row.cacheHitRate
4045
+ },
4046
+ {
4047
+ key: "cost",
4048
+ label: "成本(元)",
4049
+ value: (row) => row.cost
4050
+ },
4051
+ {
4052
+ key: "costSource",
4053
+ label: "成本来源",
4054
+ value: (row) => row.costSource === void 0 ? null : COST_SOURCE_LABELS[row.costSource]
4055
+ },
4056
+ {
4057
+ key: "archived",
4058
+ label: "已归档",
4059
+ value: (row) => row.archived ? "是" : "否"
4060
+ }
4061
+ ];
1807
4062
  function SessionsTab(props) {
1808
4063
  const [query, setQuery] = (0, react.useState)("");
1809
4064
  const [channelFilter, setChannelFilter] = (0, react.useState)("ALL");
@@ -1813,10 +4068,11 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1813
4068
  const [nowMs] = (0, react.useState)(() => Date.now());
1814
4069
  const rows = props.data?.rows ?? [];
1815
4070
  const archivedCount = rows.filter((row) => row.archived).length;
4071
+ const hiddenEmpty = dropEmptyRows(rows, (row) => row.tokens).hidden;
1816
4072
  const visible = (0, react.useMemo)(() => {
1817
4073
  const keyword = query.trim().toLowerCase();
1818
4074
  const maxAgeDays = Number(daysFilter);
1819
- const filtered = rows.filter((row) => {
4075
+ const filtered = dropEmptyRows(rows, (row) => row.tokens).rows.filter((row) => {
1820
4076
  const title = (row.title ?? "") + " " + row.id + " " + (row.workspace ?? "");
1821
4077
  const matchesQuery = keyword === "" || title.toLowerCase().includes(keyword);
1822
4078
  const matchesChannel = channelFilter === "ALL" || row.provider === channelFilter;
@@ -1840,6 +4096,11 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1840
4096
  nowMs,
1841
4097
  archiveFilter
1842
4098
  ]);
4099
+ /** 只导当前筛选后的会话行(归档筛选、时间窗口、搜索都算数)。 */
4100
+ const exportRows = (format) => {
4101
+ if (format === "csv") downloadFile(exportFileName("sessions", "csv"), toCsv(visible, EXPORT_COLUMNS), CSV_MIME);
4102
+ else downloadFile(exportFileName("sessions", "json"), toJson(visible), JSON_MIME);
4103
+ };
1843
4104
  if (props.data === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1844
4105
  className: shared_module_css_default.empty,
1845
4106
  children: "会话数据读取中或不可用…"
@@ -1941,6 +4202,20 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1941
4202
  children: "仅显示已归档"
1942
4203
  })
1943
4204
  ]
4205
+ }),
4206
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4207
+ className: export_module_css_default.exportGroup,
4208
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4209
+ className: export_module_css_default.exportBtn,
4210
+ disabled: visible.length === 0,
4211
+ onClick: () => exportRows("csv"),
4212
+ children: "导出 CSV"
4213
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4214
+ className: export_module_css_default.exportBtn,
4215
+ disabled: visible.length === 0,
4216
+ onClick: () => exportRows("json"),
4217
+ children: "导出 JSON"
4218
+ })]
1944
4219
  })
1945
4220
  ]
1946
4221
  }),
@@ -1953,7 +4228,7 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1953
4228
  ]
1954
4229
  }),
1955
4230
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
1956
- className: shared_module_css_default.table,
4231
+ className: shared_module_css_default.table + " " + lists_module_css_default.sessionTable,
1957
4232
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
1958
4233
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "会话" }),
1959
4234
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "通道" }),
@@ -1987,6 +4262,10 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
1987
4262
  }) })
1988
4263
  ] })]
1989
4264
  }),
4265
+ hiddenEmpty > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4266
+ className: shared_module_css_default.statExtra,
4267
+ children: hiddenEmptyRowsText(hiddenEmpty)
4268
+ }),
1990
4269
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1991
4270
  className: shared_module_css_default.statExtra,
1992
4271
  children: ["会话用量按宿主会话事件折叠(每个结算样本计一次调用);成本按台账模型均价估算", props.data.priced ? "" : "(台账无成本数据,暂不可估)"]
@@ -2019,7 +4298,7 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
2019
4298
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: row.cost === null || channel === null || channel.costMode !== "perToken" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2020
4299
  className: shared_module_css_default.dim,
2021
4300
  children: costCell.text
2022
- }) : costCell.text })
4301
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [costCell.text, showsAmount(costCell) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CostSourceChip, { source: row.costSource })] }) })
2023
4302
  ]
2024
4303
  }, row.id);
2025
4304
  }
@@ -2049,6 +4328,7 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
2049
4328
  ["overview", "总览"],
2050
4329
  ["detail", "使用明细"],
2051
4330
  ["projects", "项目用量"],
4331
+ ["models", "按模型"],
2052
4332
  ["sessions", "会话用量"],
2053
4333
  ["quota", "余额与配额"]
2054
4334
  ];
@@ -2070,9 +4350,13 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
2070
4350
  const [usage, setUsage] = (0, react.useState)(null);
2071
4351
  const [sessions, setSessions] = (0, react.useState)(null);
2072
4352
  const [projects, setProjects] = (0, react.useState)(null);
4353
+ const [models, setModels] = (0, react.useState)(null);
4354
+ const [pricing, setPricing] = (0, react.useState)(null);
2073
4355
  const [available, setAvailable] = (0, react.useState)(null);
2074
4356
  const [loading, setLoading] = (0, react.useState)(false);
2075
4357
  const tooltip = useTooltip();
4358
+ const pricingState = (0, react.useSyncExternalStore)(pricingSettings.store.subscribe, pricingSettings.store.getSnapshot);
4359
+ const pricingSignature = JSON.stringify(pricingState.config);
2076
4360
  const reload = (0, react.useCallback)(async (includeSessionData, forceQuota = false) => {
2077
4361
  setLoading(true);
2078
4362
  const quotaResult = await loadQuota(forceQuota);
@@ -2086,22 +4370,38 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
2086
4370
  setSummary(summaryResult);
2087
4371
  setHeatmap(heatmapResult ?? []);
2088
4372
  if (includeSessionData) {
2089
- const [sessionResult, projectResult] = await Promise.all([loadSessions(), loadProjects()]);
4373
+ const [sessionResult, projectResult, modelResult] = await Promise.all([
4374
+ loadSessions(),
4375
+ loadProjects(),
4376
+ loadModels()
4377
+ ]);
2090
4378
  setSessions(sessionResult);
2091
4379
  setProjects(projectResult);
4380
+ setModels(modelResult);
2092
4381
  }
2093
4382
  setLoading(false);
2094
- }, []);
4383
+ }, [pricingSignature]);
2095
4384
  (0, react.useEffect)(() => {
2096
4385
  reload(false);
2097
4386
  }, [reload]);
4387
+ (0, react.useEffect)(() => {
4388
+ let cancelled = false;
4389
+ loadPricing().then((result) => {
4390
+ if (!cancelled) setPricing(result);
4391
+ });
4392
+ return () => {
4393
+ cancelled = true;
4394
+ };
4395
+ }, [pricingSignature]);
2098
4396
  (0, react.useEffect)(() => {
2099
4397
  if (activeTab === "sessions" && sessions === null) loadSessions().then(setSessions);
2100
4398
  if (activeTab === "projects" && projects === null) loadProjects().then(setProjects);
4399
+ if ((activeTab === "models" || activeTab === "quota") && models === null) loadModels().then(setModels);
2101
4400
  }, [
2102
4401
  activeTab,
2103
4402
  sessions,
2104
- projects
4403
+ projects,
4404
+ models
2105
4405
  ]);
2106
4406
  const bounds = (0, react.useMemo)(() => {
2107
4407
  const today = isoDate(/* @__PURE__ */ new Date());
@@ -2147,10 +4447,11 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
2147
4447
  }, [
2148
4448
  bounds.from,
2149
4449
  bounds.to,
2150
- channelFilter
4450
+ channelFilter,
4451
+ pricingSignature
2151
4452
  ]);
2152
4453
  const refresh = (0, react.useCallback)(() => {
2153
- reload(activeTab === "sessions" || activeTab === "projects" || sessions !== null || projects !== null, true);
4454
+ reload(activeTab === "sessions" || activeTab === "projects" || activeTab === "models" || sessions !== null || projects !== null || models !== null, true);
2154
4455
  loadUsage(bounds.from, bounds.to, channelFilter).then((result) => {
2155
4456
  if (result !== null) setUsage(result);
2156
4457
  });
@@ -2159,6 +4460,7 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
2159
4460
  activeTab,
2160
4461
  sessions,
2161
4462
  projects,
4463
+ models,
2162
4464
  bounds.from,
2163
4465
  bounds.to,
2164
4466
  channelFilter
@@ -2222,13 +4524,21 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
2222
4524
  data: projects,
2223
4525
  channels
2224
4526
  }),
4527
+ activeTab === "models" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelsTab, {
4528
+ data: models,
4529
+ channels
4530
+ }),
2225
4531
  activeTab === "sessions" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionsTab, {
2226
4532
  data: sessions,
2227
4533
  channels
2228
4534
  }),
2229
4535
  activeTab === "quota" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuotaTab, {
2230
4536
  channels,
2231
- quotas
4537
+ quotas,
4538
+ pricing,
4539
+ pricingState,
4540
+ pricingSettings,
4541
+ models
2232
4542
  }),
2233
4543
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2234
4544
  className: shared_module_css_default.footnote,
@@ -2241,9 +4551,24 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
2241
4551
  "。两套来源按天取舍、不相加。"
2242
4552
  ] }),
2243
4553
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "热力图按可见区间内活跃日的三分位分档(少 / 中 / 高)。" }),
2244
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "项目用量、会话用量与单日「按小时」均来自会话事件,每个结算样本计一次调用(不计成本)。" }),
4554
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "项目用量、会话用量、按模型与单日「按小时」均来自会话事件,每个结算样本计一次调用(不计成本)。" }),
2245
4555
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "已归档会话只是标记:用量照常计入,会话列表默认隐藏、可切换显示(显示时淡化并标注)。" }),
2246
4556
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "命中率 = 缓存读 ÷(输入 + 输出 + 缓存读)。" }),
4557
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
4558
+ "成本优先级:",
4559
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: "台账当天的官方成本" }),
4560
+ " > ",
4561
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: "你的计费规则" }),
4562
+ " > ",
4563
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: "台账反推均价" }),
4564
+ " > 无依据; 每个成本单元格旁的角标写明来源(",
4565
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: "官方" }),
4566
+ " / ",
4567
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: "自定" }),
4568
+ " / ",
4569
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: "估算" }),
4570
+ ",无依据显示 —)。 规则在「余额与配额」页下半部分维护,存在宿主设置服务里,改完立即重算。"
4571
+ ] }),
2247
4572
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: "余额与订阅窗口实时查询官方接口(失败回落本机快照),原样展示,不做换算与建议。" })
2248
4573
  ]
2249
4574
  })]
@@ -2253,8 +4578,8 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
2253
4578
  });
2254
4579
  }
2255
4580
  //#endregion
2256
- //#region \0dsh-css:D:\EasyJob\.plugin\dsh-key-usage\src\client\styles\page.module.css.mjs
2257
- const css = "._2fl9Ga_page{box-sizing:border-box;overscroll-behavior:contain;width:100%;height:100%;min-height:0;overflow-x:hidden;overflow-y:auto}._2fl9Ga_inner{box-sizing:border-box;width:100%;max-width:1240px;margin:0 auto;padding:20px 24px 28px}";
4581
+ //#region \0dsh-css:src/client/styles/page.module.css.mjs
4582
+ const css = ".Zd1J4W_page{box-sizing:border-box;overscroll-behavior:contain;width:100%;height:100%;min-height:0;overflow-x:hidden;overflow-y:auto}.Zd1J4W_inner{box-sizing:border-box;width:100%;max-width:1240px;margin:0 auto;padding:20px 24px 28px}";
2258
4583
  const tagId = "dsh-key-usage/page.module.css";
2259
4584
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
2260
4585
  const tag = document.createElement("style");
@@ -2269,8 +4594,8 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
2269
4594
  css
2270
4595
  });
2271
4596
  var page_module_css_default = {
2272
- "inner": "_2fl9Ga_inner",
2273
- "page": "_2fl9Ga_page"
4597
+ "inner": "Zd1J4W_inner",
4598
+ "page": "Zd1J4W_page"
2274
4599
  };
2275
4600
  //#endregion
2276
4601
  //#region src/client/UsagePage.tsx
@@ -2285,14 +4610,54 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
2285
4610
  });
2286
4611
  }
2287
4612
  //#endregion
4613
+ //#region src/client/components/PricingCard.tsx
4614
+ /**
4615
+ * 插件页(`plugins.bundle.config` 槽,以包名为键)里的计费规则卡片。
4616
+ * 与「余额与配额」页下半部分共用同一个面板与同一份状态(存在宿主设置服务里),
4617
+ * 这里只是多一个入口,不做第二套逻辑。
4618
+ */
4619
+ function PricingCard() {
4620
+ const state = (0, react.useSyncExternalStore)(pricingSettings.store.subscribe, pricingSettings.store.getSnapshot);
4621
+ const [pricing, setPricing] = (0, react.useState)(null);
4622
+ const [channels, setChannels] = (0, react.useState)([]);
4623
+ const [models, setModels] = (0, react.useState)(null);
4624
+ (0, react.useEffect)(() => {
4625
+ let cancelled = false;
4626
+ loadPricing().then((result) => {
4627
+ if (!cancelled) setPricing(result);
4628
+ });
4629
+ loadChannels().then((result) => {
4630
+ if (!cancelled) setChannels(result ?? []);
4631
+ });
4632
+ loadModels().then((result) => {
4633
+ if (!cancelled) setModels(result);
4634
+ });
4635
+ return () => {
4636
+ cancelled = true;
4637
+ };
4638
+ }, [state.config]);
4639
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PricingRulesPanel, {
4640
+ settings: pricingSettings,
4641
+ state,
4642
+ pricing,
4643
+ channels,
4644
+ models
4645
+ });
4646
+ }
4647
+ //#endregion
2288
4648
  //#region src/client/index.tsx
2289
4649
  /**
2290
- * dsh-key-usage 客户端入口:注册「使用情况」侧边栏导航项 + 主区域页面。
4650
+ * dsh-key-usage 客户端入口:注册「使用情况」侧边栏导航项 + 主区域页面,
4651
+ * 并把计费规则面板接到宿主的配置表单服务上。
2291
4652
  * UI 逻辑在 UsagePage / KeyUsageSection 里;本文件只做 cordis apply 接线。
2292
4653
  *
2293
4654
  * slot 契约(对齐已装的 dsh-context「上下文洞察」写法):
2294
4655
  * - `sidebar.panellist`:宿主自持整行(标签 / hover / active),插件只提供图标,`id` 是面板标识;
2295
- * - `main`:同一标识放在 `key` 上,宿主在主内容区按它渲染整页 —— 因此不是弹窗、也不占设置页。
4656
+ * - `main`:同一标识放在 `key` 上,宿主在主内容区按它渲染整页 —— 因此不是弹窗、也不占设置页;
4657
+ * - `plugins.bundle.config`:插件页(以包名为键)里的配置卡片,与页面里的规则面板共用同一份状态。
4658
+ *
4659
+ * 计费规则的存储:命名空间就是本包在 profile 里的条目 id,写入由宿主的 config-editor
4660
+ * 落到当前 profile 的 cordis patch 上;本插件不写任何文件(见 AGENTS.md 硬约束)。
2296
4661
  */
2297
4662
  const NS = "dsh-key-usage";
2298
4663
  /** 侧边栏条目与 main 页面共用的面板标识(宿主 MainPanelId 域,命名空间化避免撞车)。 */
@@ -2310,6 +4675,15 @@ window.__ModuleLoader__.load({ id: "dsh-key-usage", factory: (require) => {
2310
4675
  order: 30,
2311
4676
  label: () => "使用情况"
2312
4677
  }, (props) => (0, react.createElement)(UsageIcon, { size: props.size })));
4678
+ ctx.inject?.(["configForms"], (scope) => {
4679
+ const forms = scope.configForms;
4680
+ if (forms === void 0) return;
4681
+ scope.effect?.(() => pricingSettings.attach(forms.get(NS)), "dsh-key-usage: pricing rules binding");
4682
+ scope.effect?.(() => forms.whileServed([NS], () => scope.slots.inject("plugins.bundle.config", () => scope.slots.register({
4683
+ name: "plugins.bundle.config",
4684
+ key: NS
4685
+ }, PricingCard))), "dsh-key-usage: plugins page card");
4686
+ });
2313
4687
  }
2314
4688
  //#endregion
2315
4689
  exports.apply = apply;