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/README.md +115 -24
- package/client/client.js +2793 -419
- package/client/client.js.map +1 -1
- package/docs/images/usage-daily.png +0 -0
- package/docs/images/usage-overview.png +0 -0
- package/docs/images/usage-quota.png +0 -0
- package/docs/images//346/214/211/346/250/241/345/236/213.png +0 -0
- package/docs/images//351/241/271/347/233/256/346/200/273/351/207/217.png +0 -0
- package/lib/index.js +73 -4
- package/lib/live-quota.js +96 -30
- package/lib/merge.js +8 -1
- package/lib/pricing-catalog.js +349 -0
- package/lib/pricing.js +500 -0
- package/lib/quota-snapshots.js +68 -0
- package/lib/routes.js +275 -28
- package/lib/session-usage.js +67 -46
- package/lib/sessions.js +98 -8
- package/package.json +18 -9
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)
|
|
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:
|
|
151
|
-
const css$
|
|
152
|
-
const tagId$
|
|
153
|
-
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$
|
|
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$
|
|
157
|
-
tag.textContent = css$
|
|
570
|
+
tag.dataset.pluginCss = tagId$8;
|
|
571
|
+
tag.textContent = css$8;
|
|
158
572
|
document.head.appendChild(tag);
|
|
159
573
|
}
|
|
160
|
-
const cssRegistry$
|
|
161
|
-
if (!cssRegistry$
|
|
162
|
-
tagId: tagId$
|
|
163
|
-
css: css$
|
|
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
|
-
"
|
|
167
|
-
"
|
|
168
|
-
"
|
|
169
|
-
"
|
|
170
|
-
"
|
|
171
|
-
"
|
|
172
|
-
"
|
|
173
|
-
"
|
|
174
|
-
"
|
|
175
|
-
"
|
|
176
|
-
"
|
|
177
|
-
"
|
|
178
|
-
"
|
|
179
|
-
"
|
|
180
|
-
"
|
|
181
|
-
"
|
|
182
|
-
"
|
|
183
|
-
"
|
|
184
|
-
"
|
|
185
|
-
"
|
|
186
|
-
"
|
|
187
|
-
"
|
|
188
|
-
"
|
|
189
|
-
"
|
|
190
|
-
"
|
|
191
|
-
"
|
|
192
|
-
"
|
|
193
|
-
"
|
|
194
|
-
"
|
|
195
|
-
"
|
|
196
|
-
"
|
|
197
|
-
"
|
|
198
|
-
"
|
|
199
|
-
"
|
|
200
|
-
"
|
|
201
|
-
"
|
|
202
|
-
"
|
|
203
|
-
"
|
|
204
|
-
"
|
|
205
|
-
"
|
|
206
|
-
"
|
|
207
|
-
"
|
|
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:
|
|
358
|
-
const css$
|
|
359
|
-
const tagId$
|
|
360
|
-
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$
|
|
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$
|
|
364
|
-
tag.textContent = css$
|
|
785
|
+
tag.dataset.pluginCss = tagId$7;
|
|
786
|
+
tag.textContent = css$7;
|
|
365
787
|
document.head.appendChild(tag);
|
|
366
788
|
}
|
|
367
|
-
const cssRegistry$
|
|
368
|
-
if (!cssRegistry$
|
|
369
|
-
tagId: tagId$
|
|
370
|
-
css: css$
|
|
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
|
-
"
|
|
374
|
-
"
|
|
375
|
-
"
|
|
376
|
-
"
|
|
377
|
-
"disabledRow": "
|
|
378
|
-
"
|
|
379
|
-
"
|
|
380
|
-
"
|
|
381
|
-
"
|
|
382
|
-
"
|
|
383
|
-
"
|
|
384
|
-
"
|
|
385
|
-
"
|
|
386
|
-
"
|
|
387
|
-
"
|
|
388
|
-
"
|
|
389
|
-
"
|
|
390
|
-
"
|
|
391
|
-
"
|
|
392
|
-
"
|
|
393
|
-
"
|
|
394
|
-
"keyTable": "
|
|
395
|
-
"
|
|
396
|
-
"
|
|
397
|
-
"
|
|
398
|
-
"
|
|
399
|
-
"
|
|
400
|
-
"
|
|
401
|
-
"
|
|
402
|
-
"
|
|
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
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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$
|
|
713
|
-
tag.textContent = css$
|
|
1274
|
+
tag.dataset.pluginCss = tagId$5;
|
|
1275
|
+
tag.textContent = css$5;
|
|
714
1276
|
document.head.appendChild(tag);
|
|
715
1277
|
}
|
|
716
|
-
const cssRegistry$
|
|
717
|
-
if (!cssRegistry$
|
|
718
|
-
tagId: tagId$
|
|
719
|
-
css: css$
|
|
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
|
-
"
|
|
723
|
-
"
|
|
724
|
-
"
|
|
725
|
-
"
|
|
726
|
-
"
|
|
727
|
-
"
|
|
728
|
-
"
|
|
729
|
-
"
|
|
730
|
-
"rangeInput": "
|
|
731
|
-
"
|
|
732
|
-
"
|
|
733
|
-
"
|
|
734
|
-
"
|
|
735
|
-
"
|
|
736
|
-
"
|
|
737
|
-
"
|
|
738
|
-
"
|
|
739
|
-
"
|
|
740
|
-
"
|
|
741
|
-
"
|
|
742
|
-
"
|
|
743
|
-
"
|
|
744
|
-
"
|
|
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
|
|
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: [
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
}),
|
|
1017
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
}),
|
|
1021
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
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
|
-
}),
|
|
1035
|
-
className: detail_module_css_default.
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
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.
|
|
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.
|
|
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: "
|
|
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:
|
|
1377
|
-
const css$2 = ".
|
|
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
|
-
"
|
|
1393
|
-
"
|
|
1394
|
-
"
|
|
1395
|
-
"
|
|
1396
|
-
"
|
|
1397
|
-
"
|
|
1398
|
-
"
|
|
1399
|
-
"listTitle": "
|
|
1400
|
-
"
|
|
1401
|
-
"
|
|
1402
|
-
"
|
|
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
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
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
|
-
|
|
1561
|
-
|
|
1562
|
-
return
|
|
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:
|
|
1566
|
-
const css$1 = ".
|
|
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
|
-
"
|
|
1582
|
-
"
|
|
1583
|
-
"
|
|
1584
|
-
"
|
|
1585
|
-
"
|
|
1586
|
-
"
|
|
1587
|
-
"
|
|
1588
|
-
"
|
|
1589
|
-
"
|
|
1590
|
-
"
|
|
1591
|
-
"
|
|
1592
|
-
"
|
|
1593
|
-
"
|
|
1594
|
-
"
|
|
1595
|
-
"
|
|
1596
|
-
"
|
|
1597
|
-
"
|
|
1598
|
-
"
|
|
1599
|
-
"
|
|
1600
|
-
"
|
|
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: [
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
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
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
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
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
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([
|
|
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:
|
|
2257
|
-
const css = ".
|
|
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": "
|
|
2273
|
-
"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;
|