dsh-plugin-usage-stats 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/README.zh.md +51 -0
- package/cordis.patch.yml +7 -0
- package/lib/client.js +716 -0
- package/package.json +71 -0
- package/src/aggregate.ts +175 -0
- package/src/cordis.ts +72 -0
- package/src/scanner.ts +391 -0
- package/src/store.ts +152 -0
- package/tests/contract-pair.test.ts +108 -0
- package/tests/cordis.test.ts +96 -0
- package/tests/fold.test.ts +122 -0
- package/tests/realscan.test.ts +24 -0
- package/tests/scan.test.ts +168 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
/** dsh-plugin-usage-stats 浏览器半(手写 __ModuleLoader__ 工厂,无构建链),v3 单页形态:
|
|
2
|
+
* 「设置 → Token 用量」独立 section 页(宿主 General 分区左列表的新条目,settings.section keyed slot)。
|
|
3
|
+
* 会话级视图宿主自带(底部计数条 + 会话统计对话框),本包不重复造——v2 的右侧栏 tab 已删。
|
|
4
|
+
*
|
|
5
|
+
* 要点(与设计文档一致):
|
|
6
|
+
* - 形态 = window.__ModuleLoader__.load({id, factory}),与官方 lib/client.js 同格式;
|
|
7
|
+
* 只用基线模块表(react),无 JSX(React.createElement),故无需构建。
|
|
8
|
+
* - ctx.remote 官方装配是 build 期固定能力集,不带 usageStats 命名空间:
|
|
9
|
+
* 本包自己 ctx.remote.$mount(CONTRIBUTION) 挂手写 strict descriptor
|
|
10
|
+
* (参数 codec 的 schema.parse 为透传,服务端 SRC 模式做 JSON-safe 校验;结果客户端不校验)。
|
|
11
|
+
* 实测(真宿主):$mount resolve 后服务与 accessor 都注册了,但**属性式 ctx.remote.<ns>
|
|
12
|
+
* 在第三方 fiber 被可见性过滤("without inject" throw),必须用名字解析 ctx.get("remote.usageStats")**。
|
|
13
|
+
* - 两端契约:descriptors 与 src/cordis.ts 的 SRC 方法/参数名一一对应(overview/filter),改名须同步。
|
|
14
|
+
* - 日期区间 = 自绘 RangePicker(单触发器 + 预设 + 月历),不用原生 <input type=date>(宿主深色主题下样式失控);
|
|
15
|
+
* 交互对标 agent-token-stats 的时间选择(预设即选即得 + 日历两段式范围),全走 --dsw-* 设计令牌。
|
|
16
|
+
* - label 用 () => 字符串直出中文(宿主 locale 命名空间是官方包机制,三方插件不自带词典)。
|
|
17
|
+
*/
|
|
18
|
+
window.__ModuleLoader__.load({
|
|
19
|
+
id: "dsh-plugin-usage-stats",
|
|
20
|
+
factory: (require) => {
|
|
21
|
+
var module = { exports: {} };
|
|
22
|
+
var exports = module.exports;
|
|
23
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
24
|
+
var React = require("react");
|
|
25
|
+
var h = React.createElement;
|
|
26
|
+
var useState = React.useState;
|
|
27
|
+
var useEffect = React.useEffect;
|
|
28
|
+
var useCallback = React.useCallback;
|
|
29
|
+
var useRef = React.useRef;
|
|
30
|
+
|
|
31
|
+
/** settings 命名空间(src/settings.ts USAGE_STATS_SETTINGS_NS 的镜像;section id 与 slot key)。 */
|
|
32
|
+
var SETTINGS_NS = "usage-stats";
|
|
33
|
+
|
|
34
|
+
/* ---------- Remote contribution(手写 strict 描述符;与 src/cordis.ts 的 SRC 方法一一对应) ---------- */
|
|
35
|
+
var passthrough = {
|
|
36
|
+
parse: function (v) {
|
|
37
|
+
return v === undefined ? {} : v;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
function codec(sym) {
|
|
41
|
+
return { mode: "strict", typeSymbol: "dsh-plugin-usage-stats#" + sym, schema: passthrough };
|
|
42
|
+
}
|
|
43
|
+
function descriptor(method, param, location) {
|
|
44
|
+
return {
|
|
45
|
+
id: SETTINGS_NS + "#usageStats/" + method,
|
|
46
|
+
service: "usageStats",
|
|
47
|
+
namespace: "usageStats",
|
|
48
|
+
method: method,
|
|
49
|
+
invocation: { kind: "direct" },
|
|
50
|
+
// acceptsUndefined:filter 可省略(官方 codegen 对可选边界的显式字段,不塞 codec 兜底)。
|
|
51
|
+
// sourceLocation:契约出处锚点(官方产物恒带);行号漂移由 contract-pair 测试当场抓住。
|
|
52
|
+
parameters: [{ name: param, wire: param, source: "json", acceptsUndefined: true, codec: codec(method + ":" + param) }],
|
|
53
|
+
result: codec(method + ":result"),
|
|
54
|
+
sourceLocation: location,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
var CONTRIBUTION = {
|
|
58
|
+
package: "dsh-plugin-usage-stats",
|
|
59
|
+
descriptors: [descriptor("overview", "filter", { file: "src/cordis.ts", line: 44, column: 9 })],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/* ---------- 样式(materialize 时注入一次) ----------
|
|
63
|
+
* 1:1 对齐宿主官方设置页规范(verbatim 来源:dsh-client-ui-settings-plugins 注入的
|
|
64
|
+
* fields.module.css):表单控件 = 高 34px、圆角 8px、.5px 细边 border-l4、底 bg-layer-3、
|
|
65
|
+
* 13px/1.5 文字、focus 时 border 变 brand-primary;字段分隔线 .5px border-l2;
|
|
66
|
+
* 报错 = 无边框 label-error 12px 文字;文本按钮 = 无边框 label-secondary hover 提亮。
|
|
67
|
+
* token 名以 dsh-client-ui-theme 运行时定义表为准(harness 的 __TOKEN_AUDIT__ 兜底,
|
|
68
|
+
* 曾因引用 --dsw-alias-separator/fill-quinary 等未定义名导致真页浮层全黑)。 */
|
|
69
|
+
var CSS = [
|
|
70
|
+
".usg-root{display:flex;flex-direction:column;gap:14px;padding:16px 18px;overflow:auto;height:100%;box-sizing:border-box;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-primary);max-width:980px}",
|
|
71
|
+
".usg-head{display:flex;flex-direction:column;gap:2px}",
|
|
72
|
+
".usg-head b{font-size:16px;font-weight:600}",
|
|
73
|
+
".usg-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
|
|
74
|
+
".usg-kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px}",
|
|
75
|
+
".usg-kpi{border:.5px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px 10px;display:flex;flex-direction:column;gap:2px;background:var(--dsw-alias-bg-layer-1)}",
|
|
76
|
+
".usg-kpi b{font-size:17px;font-weight:600;font-variant-numeric:tabular-nums}",
|
|
77
|
+
".usg-kpi span{font-size:11px;color:var(--dsw-alias-label-tertiary)}",
|
|
78
|
+
".usg-table{width:100%;border-collapse:collapse;font-size:12px}",
|
|
79
|
+
".usg-table th{text-align:right;font-weight:500;color:var(--dsw-alias-label-tertiary);padding:4px 6px;border-bottom:.5px solid var(--dsw-alias-border-l2);white-space:nowrap}",
|
|
80
|
+
".usg-table td{text-align:right;padding:4px 6px;border-bottom:.5px solid var(--dsw-alias-border-l2);white-space:nowrap;font-variant-numeric:tabular-nums}",
|
|
81
|
+
".usg-table th:first-child,.usg-table td:first-child{text-align:left}",
|
|
82
|
+
".usg-muted{color:var(--dsw-alias-label-tertiary)}",
|
|
83
|
+
".usg-err{color:var(--dsw-alias-label-error);font-size:12px}",
|
|
84
|
+
/* 触发器 = 宿主表单控件质感(官方 input 规范逐参数照抄) */
|
|
85
|
+
".usg-btn{all:unset;box-sizing:border-box;cursor:pointer;display:inline-flex;align-items:center;height:34px;padding:0 12px;border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3)}",
|
|
86
|
+
".usg-btn:hover{border-color:var(--dsw-alias-border-l3)}",
|
|
87
|
+
".usg-btn:focus-visible{border-color:var(--dsw-alias-brand-primary)}",
|
|
88
|
+
".usg-btn[aria-pressed=true]{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary)}",
|
|
89
|
+
".usg-title{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary);margin-bottom:4px}",
|
|
90
|
+
/* 浮层(RangePicker / DimSelect 共用):透明 mask 兜外点关闭,panel 用宿主菜单底+阴影+同规格圆角 */
|
|
91
|
+
".usg-anchor{position:relative}",
|
|
92
|
+
".usg-mask{position:fixed;inset:0;z-index:60}",
|
|
93
|
+
/* 抬升面官方规范(docs/web-styling.md + ui-theme elevation spec):border:0 +
|
|
94
|
+
* --dsw-elevation-*,0.5px 描边是阴影第一层(stroke-color 可换绑),
|
|
95
|
+
* 禁止 alias-border 真边框与 elevation/lv 阴影并配。 */
|
|
96
|
+
".usg-pop{position:absolute;top:calc(100% + 4px);left:0;z-index:61;border:0;--dsw-elevation-stroke-color:var(--dsw-alias-border-l2);box-shadow:var(--dsw-elevation-panel);border-radius:8px;background:var(--dsw-specific-menu);padding:10px;font-size:12px;min-width:230px}",
|
|
97
|
+
/* 预设 = 官方文本按钮(reset 规范:无边框无底色,secondary hover 提亮) */
|
|
98
|
+
".usg-pre{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px}",
|
|
99
|
+
".usg-pre>button{all:unset;cursor:pointer;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary)}",
|
|
100
|
+
".usg-pre>button:hover{color:var(--dsw-alias-label-primary)}",
|
|
101
|
+
".usg-cal-h{display:flex;align-items:center;justify-content:space-between;margin-bottom:4px}",
|
|
102
|
+
".usg-cal-h b{font-size:12px}",
|
|
103
|
+
".usg-cal-h>button{all:unset;cursor:pointer;padding:0 8px;color:var(--dsw-alias-label-secondary)}",
|
|
104
|
+
".usg-cal-h>button:hover{color:var(--dsw-alias-label-primary)}",
|
|
105
|
+
".usg-cal{display:grid;grid-template-columns:repeat(7,28px);gap:1px}",
|
|
106
|
+
".usg-cal>span{text-align:center;font-size:10px;color:var(--dsw-alias-label-tertiary);line-height:20px}",
|
|
107
|
+
".usg-pv{text-align:center;line-height:26px;border-radius:6px;cursor:pointer;color:var(--dsw-alias-label-primary)}",
|
|
108
|
+
".usg-pv[aria-disabled=true]{color:var(--dsw-alias-label-dimmed);cursor:default}",
|
|
109
|
+
".usg-pv:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
110
|
+
".usg-pv.usg-in{background:var(--dsw-alias-bg-layer-2);border-radius:0}",
|
|
111
|
+
".usg-pv.usg-e{background:var(--dsw-alias-button-ghost-active-fill);border-radius:0}",
|
|
112
|
+
".usg-pv.usg-e.usg-in{border-radius:0}",
|
|
113
|
+
".usg-pop .usg-pv[aria-selected=true]{background:var(--dsw-alias-button-ghost-active-fill);color:var(--dsw-alias-label-primary)}",
|
|
114
|
+
".usg-pop-ft{display:flex;justify-content:space-between;margin-top:8px;border-top:.5px solid var(--dsw-alias-border-l2);padding-top:8px}",
|
|
115
|
+
".usg-list{list-style:none;margin:0;padding:0;max-height:260px;overflow:auto;min-width:180px}",
|
|
116
|
+
".usg-list>li{padding:3px 8px;border-radius:6px;cursor:pointer;color:var(--dsw-alias-label-secondary)}",
|
|
117
|
+
".usg-list>li:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
|
|
118
|
+
".usg-list>li[aria-selected=true]{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}",
|
|
119
|
+
].join("\n");
|
|
120
|
+
var cssDone = false;
|
|
121
|
+
function ensureCss() {
|
|
122
|
+
if (cssDone || typeof document === "undefined") return;
|
|
123
|
+
cssDone = true;
|
|
124
|
+
var el = document.createElement("style");
|
|
125
|
+
el.setAttribute("data-plugin", SETTINGS_NS);
|
|
126
|
+
el.textContent = CSS;
|
|
127
|
+
document.head.appendChild(el);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* ---------- 格式化与本地日期工具 ---------- */
|
|
131
|
+
function fmt(n) {
|
|
132
|
+
if (typeof n !== "number" || !isFinite(n)) return "—";
|
|
133
|
+
// 刻度阶梯:具体数 <1K → K(≥1e3, 1 位小数) → M(≥1e6, 2 位) → B(≥100M, 2 位);尾零去除(10K 不写 10.0K)。
|
|
134
|
+
if (n >= 1e8) return trimZero((n / 1e9).toFixed(2)) + "B";
|
|
135
|
+
if (n >= 1e6) return trimZero((n / 1e6).toFixed(2)) + "M";
|
|
136
|
+
if (n >= 1e3) return trimZero((n / 1e3).toFixed(1)) + "K";
|
|
137
|
+
return String(Math.round(n));
|
|
138
|
+
}
|
|
139
|
+
function trimZero(s) {
|
|
140
|
+
return s.indexOf(".") >= 0 ? s.replace(/0+$/u, "").replace(/\.$/u, "") : s;
|
|
141
|
+
}
|
|
142
|
+
function fmtPct(r) {
|
|
143
|
+
return r === null || r === undefined ? "—" : (r * 100).toFixed(1) + "%";
|
|
144
|
+
}
|
|
145
|
+
function p2(n) {
|
|
146
|
+
return (n < 10 ? "0" : "") + n;
|
|
147
|
+
}
|
|
148
|
+
function localYmd(d) {
|
|
149
|
+
return d.getFullYear() + "-" + p2(d.getMonth() + 1) + "-" + p2(d.getDate());
|
|
150
|
+
}
|
|
151
|
+
function ymdDate(ymd) {
|
|
152
|
+
var a = ymd.split("-");
|
|
153
|
+
return new Date(Number(a[0]), Number(a[1]) - 1, Number(a[2]));
|
|
154
|
+
}
|
|
155
|
+
function addDays(ymd, n) {
|
|
156
|
+
var d = ymdDate(ymd);
|
|
157
|
+
d.setDate(d.getDate() + n);
|
|
158
|
+
return localYmd(d);
|
|
159
|
+
}
|
|
160
|
+
function todayYmd() {
|
|
161
|
+
return localYmd(new Date());
|
|
162
|
+
}
|
|
163
|
+
/** 预设窗口(win=null 表示全部)。 */
|
|
164
|
+
var PRESETS = [
|
|
165
|
+
[
|
|
166
|
+
"今天",
|
|
167
|
+
function () {
|
|
168
|
+
var t = todayYmd();
|
|
169
|
+
return [t, t];
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
[
|
|
173
|
+
"近3天",
|
|
174
|
+
function () {
|
|
175
|
+
return [addDays(todayYmd(), -2), todayYmd()];
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
[
|
|
179
|
+
"近7天",
|
|
180
|
+
function () {
|
|
181
|
+
return [addDays(todayYmd(), -6), todayYmd()];
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
[
|
|
185
|
+
"近30天",
|
|
186
|
+
function () {
|
|
187
|
+
return [addDays(todayYmd(), -29), todayYmd()];
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
];
|
|
191
|
+
function winLabel(win) {
|
|
192
|
+
if (!win) return "全部";
|
|
193
|
+
if (win.from === win.to) return win.from === todayYmd() ? "今天" : win.from;
|
|
194
|
+
for (var i = 0; i < PRESETS.length; i++) {
|
|
195
|
+
var w = PRESETS[i][1]();
|
|
196
|
+
if (w[0] === win.from && w[1] === win.to) return PRESETS[i][0];
|
|
197
|
+
}
|
|
198
|
+
return win.from.slice(5) + " → " + win.to.slice(5);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function kpi(label, value, sub) {
|
|
202
|
+
return h("div", { className: "usg-kpi" }, h("b", null, value), h("span", null, label), sub ? h("span", { className: "usg-muted" }, sub) : null);
|
|
203
|
+
}
|
|
204
|
+
function table(title, head, rows) {
|
|
205
|
+
return h(
|
|
206
|
+
"div",
|
|
207
|
+
null,
|
|
208
|
+
h("div", { className: "usg-title" }, title),
|
|
209
|
+
h(
|
|
210
|
+
"table",
|
|
211
|
+
{ className: "usg-table" },
|
|
212
|
+
h(
|
|
213
|
+
"thead",
|
|
214
|
+
null,
|
|
215
|
+
h(
|
|
216
|
+
"tr",
|
|
217
|
+
null,
|
|
218
|
+
head.map(function (x, i) {
|
|
219
|
+
return h("th", { key: i }, x);
|
|
220
|
+
}),
|
|
221
|
+
),
|
|
222
|
+
),
|
|
223
|
+
h(
|
|
224
|
+
"tbody",
|
|
225
|
+
null,
|
|
226
|
+
rows.map(function (r, i) {
|
|
227
|
+
return h(
|
|
228
|
+
"tr",
|
|
229
|
+
{ key: r.key || i },
|
|
230
|
+
r.cells.map(function (c, j) {
|
|
231
|
+
return h("td", { key: j, title: c.t }, c.v);
|
|
232
|
+
}),
|
|
233
|
+
);
|
|
234
|
+
}),
|
|
235
|
+
),
|
|
236
|
+
),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* ---------- 浮层原语:anchor + 全屏 mask(点外即关)+ panel ---------- */
|
|
241
|
+
/** 弹层基元:trigger 永远渲染(关=只收 mask+panel)。曾经 !open 时 return null
|
|
242
|
+
* 连触发器一起删掉——静态 harness 第一轮抓到这个 v3 bug。 */
|
|
243
|
+
function Popover(props) {
|
|
244
|
+
return h(
|
|
245
|
+
"span",
|
|
246
|
+
{ className: "usg-anchor" },
|
|
247
|
+
props.trigger,
|
|
248
|
+
props.open
|
|
249
|
+
? h(
|
|
250
|
+
React.Fragment,
|
|
251
|
+
null,
|
|
252
|
+
h("div", { className: "usg-mask", onMouseDown: props.onClose }),
|
|
253
|
+
h(
|
|
254
|
+
"div",
|
|
255
|
+
{
|
|
256
|
+
className: "usg-pop",
|
|
257
|
+
onMouseDown: function (e) {
|
|
258
|
+
e.stopPropagation();
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
props.children,
|
|
262
|
+
),
|
|
263
|
+
)
|
|
264
|
+
: null,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/* ---------- RangePicker:单触发器 + 预设 + 月历范围选择(默认今天;两段式:点起点→点终点) ---------- */
|
|
269
|
+
function RangePicker(props) {
|
|
270
|
+
var win = props.value;
|
|
271
|
+
var _o = useState(false),
|
|
272
|
+
open = _o[0],
|
|
273
|
+
setOpen = _o[1];
|
|
274
|
+
var _p = useState(null),
|
|
275
|
+
pick = _p[0],
|
|
276
|
+
setPick = _p[1]; // 起点暂存(等终点)
|
|
277
|
+
var _h = useState(null),
|
|
278
|
+
hov = _h[0],
|
|
279
|
+
setHov = _h[1];
|
|
280
|
+
var _m = useState(function () {
|
|
281
|
+
var base = win ? ymdDate(win.to) : new Date();
|
|
282
|
+
return { y: base.getFullYear(), m: base.getMonth() };
|
|
283
|
+
}),
|
|
284
|
+
month = _m[0],
|
|
285
|
+
setMonth = _m[1];
|
|
286
|
+
|
|
287
|
+
function shift(n) {
|
|
288
|
+
setMonth(function (x) {
|
|
289
|
+
var d = new Date(x.y, x.m + n, 1);
|
|
290
|
+
return { y: d.getFullYear(), m: d.getMonth() };
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
function commit(a, b) {
|
|
294
|
+
var from = a <= b ? a : b;
|
|
295
|
+
var to = a <= b ? b : a;
|
|
296
|
+
props.onChange({ from: from, to: to });
|
|
297
|
+
setPick(null);
|
|
298
|
+
setOpen(false);
|
|
299
|
+
}
|
|
300
|
+
function pickDay(d) {
|
|
301
|
+
if (pick === null) setPick(d);
|
|
302
|
+
else commit(pick, d);
|
|
303
|
+
}
|
|
304
|
+
// 预览范围:已选起点 + hover 中
|
|
305
|
+
var lo = null,
|
|
306
|
+
hi = null;
|
|
307
|
+
if (pick !== null) {
|
|
308
|
+
lo = hov !== null && hov < pick ? hov : pick;
|
|
309
|
+
hi = hov !== null && hov < pick ? pick : hov;
|
|
310
|
+
}
|
|
311
|
+
var today = todayYmd();
|
|
312
|
+
var cells = [];
|
|
313
|
+
var first = new Date(month.y, month.m, 1);
|
|
314
|
+
var lead = (first.getDay() + 6) % 7; // 周一起始偏移
|
|
315
|
+
var days = new Date(month.y, month.m + 1, 0).getDate();
|
|
316
|
+
var WEEK = ["一", "二", "三", "四", "五", "六", "日"];
|
|
317
|
+
var i;
|
|
318
|
+
for (i = 0; i < lead; i++) cells.push(h("span", { key: "b" + i }));
|
|
319
|
+
for (i = 1; i <= days; i++) {
|
|
320
|
+
var d = month.y + "-" + p2(month.m + 1) + "-" + p2(i);
|
|
321
|
+
var disabled = d > today;
|
|
322
|
+
var isSel = win && d >= win.from && d <= win.to;
|
|
323
|
+
var isEdge = win && (d === win.from || d === win.to);
|
|
324
|
+
var isMid = lo !== null && hi !== null && d >= lo && d <= hi;
|
|
325
|
+
cells.push(
|
|
326
|
+
h(
|
|
327
|
+
"span",
|
|
328
|
+
{
|
|
329
|
+
key: d,
|
|
330
|
+
className: "usg-pv" + (isEdge ? " usg-e" : isSel && !isEdge ? " usg-in" : isMid ? " usg-e" : ""),
|
|
331
|
+
"aria-disabled": disabled ? "true" : undefined,
|
|
332
|
+
onClick: function () {
|
|
333
|
+
if (!disabled) pickDay(d);
|
|
334
|
+
},
|
|
335
|
+
onMouseEnter: function () {
|
|
336
|
+
setHov(d);
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
String(i),
|
|
340
|
+
),
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
return h(
|
|
344
|
+
Popover,
|
|
345
|
+
{
|
|
346
|
+
open: open,
|
|
347
|
+
onClose: function () {
|
|
348
|
+
setOpen(false);
|
|
349
|
+
setPick(null);
|
|
350
|
+
},
|
|
351
|
+
trigger: h(
|
|
352
|
+
"button",
|
|
353
|
+
{
|
|
354
|
+
className: "usg-btn",
|
|
355
|
+
title: "选择日期范围(本地时区,按天)",
|
|
356
|
+
onClick: function () {
|
|
357
|
+
setOpen(!open);
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
winLabel(win),
|
|
361
|
+
),
|
|
362
|
+
},
|
|
363
|
+
h(
|
|
364
|
+
"div",
|
|
365
|
+
{ className: "usg-pre" },
|
|
366
|
+
PRESETS.map(function (p) {
|
|
367
|
+
return h(
|
|
368
|
+
"button",
|
|
369
|
+
{
|
|
370
|
+
key: p[0],
|
|
371
|
+
onClick: function () {
|
|
372
|
+
var w = p[1]();
|
|
373
|
+
props.onChange({ from: w[0], to: w[1] });
|
|
374
|
+
setOpen(false);
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
p[0],
|
|
378
|
+
);
|
|
379
|
+
}),
|
|
380
|
+
h(
|
|
381
|
+
"button",
|
|
382
|
+
{
|
|
383
|
+
onClick: function () {
|
|
384
|
+
props.onChange(null);
|
|
385
|
+
setOpen(false);
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
"全部",
|
|
389
|
+
),
|
|
390
|
+
),
|
|
391
|
+
h(
|
|
392
|
+
"div",
|
|
393
|
+
{ className: "usg-cal-h" },
|
|
394
|
+
h(
|
|
395
|
+
"button",
|
|
396
|
+
{
|
|
397
|
+
onClick: function () {
|
|
398
|
+
shift(-1);
|
|
399
|
+
},
|
|
400
|
+
"aria-label": "上一月",
|
|
401
|
+
},
|
|
402
|
+
"‹",
|
|
403
|
+
),
|
|
404
|
+
h("b", null, month.y + " 年 " + (month.m + 1) + " 月"),
|
|
405
|
+
h(
|
|
406
|
+
"button",
|
|
407
|
+
{
|
|
408
|
+
onClick: function () {
|
|
409
|
+
shift(1);
|
|
410
|
+
},
|
|
411
|
+
"aria-label": "下一月",
|
|
412
|
+
},
|
|
413
|
+
"›",
|
|
414
|
+
),
|
|
415
|
+
),
|
|
416
|
+
h(
|
|
417
|
+
"div",
|
|
418
|
+
{ className: "usg-cal" },
|
|
419
|
+
WEEK.map(function (w) {
|
|
420
|
+
return h("span", { key: "w" + w }, w);
|
|
421
|
+
}),
|
|
422
|
+
cells,
|
|
423
|
+
),
|
|
424
|
+
h(
|
|
425
|
+
"div",
|
|
426
|
+
{ className: "usg-pop-ft" },
|
|
427
|
+
h(
|
|
428
|
+
"button",
|
|
429
|
+
{
|
|
430
|
+
className: "usg-muted",
|
|
431
|
+
style: { cursor: "pointer" },
|
|
432
|
+
onClick: function () {
|
|
433
|
+
setPick(null);
|
|
434
|
+
props.onChange(null);
|
|
435
|
+
setOpen(false);
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
"清除(全部)",
|
|
439
|
+
),
|
|
440
|
+
h("span", { className: "usg-muted" }, pick === null ? "点选起止日" : "已选起点,再点终点"),
|
|
441
|
+
),
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/* ---------- DimSelect:服务商/模型下拉(含「全部」项) ---------- */
|
|
446
|
+
function DimSelect(props) {
|
|
447
|
+
var _o = useState(false),
|
|
448
|
+
open = _o[0],
|
|
449
|
+
setOpen = _o[1];
|
|
450
|
+
var items = props.items;
|
|
451
|
+
return h(
|
|
452
|
+
Popover,
|
|
453
|
+
{
|
|
454
|
+
open: open,
|
|
455
|
+
onClose: function () {
|
|
456
|
+
setOpen(false);
|
|
457
|
+
},
|
|
458
|
+
trigger: h(
|
|
459
|
+
"button",
|
|
460
|
+
{
|
|
461
|
+
className: "usg-btn",
|
|
462
|
+
title: props.title,
|
|
463
|
+
onClick: function () {
|
|
464
|
+
setOpen(!open);
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
props.label + ":" + (props.value || "全部") + " ▾",
|
|
468
|
+
),
|
|
469
|
+
},
|
|
470
|
+
h(
|
|
471
|
+
"ul",
|
|
472
|
+
{ className: "usg-list" },
|
|
473
|
+
[{ v: "", l: "全部" }].concat(items).map(function (o) {
|
|
474
|
+
return h(
|
|
475
|
+
"li",
|
|
476
|
+
{
|
|
477
|
+
key: o.v,
|
|
478
|
+
"aria-selected": (props.value || "") === o.v ? "true" : undefined,
|
|
479
|
+
title: o.v,
|
|
480
|
+
onClick: function () {
|
|
481
|
+
props.onChange(o.v || null);
|
|
482
|
+
setOpen(false);
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
o.l,
|
|
486
|
+
);
|
|
487
|
+
}),
|
|
488
|
+
),
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/* ---------- Section 页:筛选栏 + KPI + 按模型明细 + 数据截至脚注 ---------- */
|
|
493
|
+
function UsageStatsSection(props) {
|
|
494
|
+
var ctx = props.ctx;
|
|
495
|
+
var mounted = props.mounted;
|
|
496
|
+
var _w = useState(function () {
|
|
497
|
+
var t = todayYmd();
|
|
498
|
+
return { from: t, to: t };
|
|
499
|
+
}),
|
|
500
|
+
win = _w[0],
|
|
501
|
+
setWin = _w[1];
|
|
502
|
+
var _pr = useState(null),
|
|
503
|
+
provider = _pr[0],
|
|
504
|
+
setProvider = _pr[1];
|
|
505
|
+
var _md = useState(null),
|
|
506
|
+
model = _md[0],
|
|
507
|
+
setModel = _md[1];
|
|
508
|
+
var _d = useState(null),
|
|
509
|
+
data = _d[0],
|
|
510
|
+
setData = _d[1];
|
|
511
|
+
var _e = useState(null),
|
|
512
|
+
err = _e[0],
|
|
513
|
+
setErr = _e[1];
|
|
514
|
+
var _l = useState(false),
|
|
515
|
+
loading = _l[0],
|
|
516
|
+
setLoading = _l[1];
|
|
517
|
+
var _t = useState(0),
|
|
518
|
+
tick = _t[0],
|
|
519
|
+
setTick = _t[1];
|
|
520
|
+
var alive = useRef(0);
|
|
521
|
+
/** 下拉选项池:只在无维度过滤的查询后刷新(过滤结果里选项会枯竭)。 */
|
|
522
|
+
var _pl = useState({ providers: [], models: [] }),
|
|
523
|
+
pool = _pl[0],
|
|
524
|
+
setPool = _pl[1];
|
|
525
|
+
|
|
526
|
+
var load = useCallback(
|
|
527
|
+
async function () {
|
|
528
|
+
var my = ++alive.current;
|
|
529
|
+
setLoading(true);
|
|
530
|
+
setErr(null);
|
|
531
|
+
try {
|
|
532
|
+
await mounted;
|
|
533
|
+
var f = {};
|
|
534
|
+
if (win) {
|
|
535
|
+
f.from = win.from;
|
|
536
|
+
f.to = win.to;
|
|
537
|
+
}
|
|
538
|
+
if (provider) f.provider = provider;
|
|
539
|
+
if (model) f.model = model;
|
|
540
|
+
var r = await ctx.get("remote.usageStats").overview(f);
|
|
541
|
+
if (!r.ok) throw new Error(r.error.message);
|
|
542
|
+
if (alive.current === my) {
|
|
543
|
+
setData(r.value);
|
|
544
|
+
if (!provider && !model && r.value && r.value.byModel) setPool(derivePool(r.value.byModel));
|
|
545
|
+
}
|
|
546
|
+
} catch (e) {
|
|
547
|
+
if (alive.current === my) setErr(humanizeError(String((e && e.message) || e)));
|
|
548
|
+
} finally {
|
|
549
|
+
if (alive.current === my) setLoading(false);
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
[ctx, mounted, win, provider, model],
|
|
553
|
+
);
|
|
554
|
+
useEffect(
|
|
555
|
+
function () {
|
|
556
|
+
load();
|
|
557
|
+
},
|
|
558
|
+
[load, tick],
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
var t = (data && data.totals) || {};
|
|
562
|
+
var filtered = !!(provider || model);
|
|
563
|
+
|
|
564
|
+
return h(
|
|
565
|
+
"div",
|
|
566
|
+
{ className: "usg-root" },
|
|
567
|
+
h(
|
|
568
|
+
"div",
|
|
569
|
+
{ className: "usg-head" },
|
|
570
|
+
h("b", null, "Token 用量"),
|
|
571
|
+
h("span", { className: "usg-muted" }, "全部 workspace 会话的全局用量(本地时区按天;单会话数据请看聊天区自带统计)"),
|
|
572
|
+
),
|
|
573
|
+
h(
|
|
574
|
+
"div",
|
|
575
|
+
{ className: "usg-bar" },
|
|
576
|
+
h(RangePicker, {
|
|
577
|
+
value: win,
|
|
578
|
+
onChange: function (w) {
|
|
579
|
+
setWin(w);
|
|
580
|
+
},
|
|
581
|
+
}),
|
|
582
|
+
h(DimSelect, {
|
|
583
|
+
label: "服务商",
|
|
584
|
+
title: "按服务商过滤",
|
|
585
|
+
value: provider,
|
|
586
|
+
items: pool.providers,
|
|
587
|
+
onChange: function (v) {
|
|
588
|
+
setProvider(v);
|
|
589
|
+
setModel(null);
|
|
590
|
+
},
|
|
591
|
+
}),
|
|
592
|
+
h(DimSelect, {
|
|
593
|
+
label: "模型",
|
|
594
|
+
title: "按模型过滤",
|
|
595
|
+
value: model,
|
|
596
|
+
items: pool.models.filter(function (o) {
|
|
597
|
+
return !provider || o.v.slice(0, o.v.indexOf("/")) === provider;
|
|
598
|
+
}),
|
|
599
|
+
onChange: function (v) {
|
|
600
|
+
setModel(v);
|
|
601
|
+
},
|
|
602
|
+
}),
|
|
603
|
+
h("span", { style: { flex: 1 } }),
|
|
604
|
+
loading ? h("span", { className: "usg-muted" }, "统计中…") : null,
|
|
605
|
+
h(
|
|
606
|
+
"button",
|
|
607
|
+
{
|
|
608
|
+
className: "usg-btn",
|
|
609
|
+
onClick: function () {
|
|
610
|
+
setTick(function (x) {
|
|
611
|
+
return x + 1;
|
|
612
|
+
});
|
|
613
|
+
},
|
|
614
|
+
disabled: loading,
|
|
615
|
+
},
|
|
616
|
+
"刷新",
|
|
617
|
+
),
|
|
618
|
+
),
|
|
619
|
+
err ? h("div", { className: "usg-err" }, "读取失败:" + err) : null,
|
|
620
|
+
data
|
|
621
|
+
? h(
|
|
622
|
+
"div",
|
|
623
|
+
{ className: "usg-kpis" },
|
|
624
|
+
kpi("会话数", fmt(data.sessionCount), "扫描 " + fmt(data.scannedFiles) + " 个文件"),
|
|
625
|
+
kpi("总 tokens(含缓存)", fmt(t.total), fmt(t.requests || 0) + " 次请求"),
|
|
626
|
+
kpi("输入(未缓存)", fmt(t.input)),
|
|
627
|
+
kpi("输出", fmt(t.output)),
|
|
628
|
+
kpi("缓存读", fmt(t.cacheRead), "命中率 " + fmtPct(data.hitRate)),
|
|
629
|
+
kpi("缓存写", fmt(t.cacheWrite)),
|
|
630
|
+
)
|
|
631
|
+
: null,
|
|
632
|
+
data && data.byModel && data.byModel.length
|
|
633
|
+
? table(
|
|
634
|
+
"按模型" + (filtered ? "(已过滤)" : ""),
|
|
635
|
+
["模型", "请求", "输入", "输出", "缓存读", "命中率"],
|
|
636
|
+
data.byModel.map(function (m) {
|
|
637
|
+
return {
|
|
638
|
+
key: m.key,
|
|
639
|
+
cells: [
|
|
640
|
+
{ v: m.key, t: m.key },
|
|
641
|
+
{ v: fmt(m.requests) },
|
|
642
|
+
{ v: fmt(m.input) },
|
|
643
|
+
{ v: fmt(m.output) },
|
|
644
|
+
{ v: fmt(m.cacheRead) },
|
|
645
|
+
{ v: fmtPct(m.hitRate) },
|
|
646
|
+
],
|
|
647
|
+
};
|
|
648
|
+
}),
|
|
649
|
+
)
|
|
650
|
+
: null,
|
|
651
|
+
data ? h("div", { className: "usg-muted" }, "数据截至 " + new Date(data.generatedAt).toLocaleTimeString()) : null,
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
/** mount/调用失败的原始报错翻译:without inject = 两端契约错位(浏览器半已刷新、host 仍是旧服务端),
|
|
655
|
+
* 明确告诉用户重启该 profile 的 host;其余错误原样透出。 */
|
|
656
|
+
function humanizeError(msg) {
|
|
657
|
+
if (/without inject|no longer mounted|is not a function|undefined/.test(msg)) {
|
|
658
|
+
return (
|
|
659
|
+
"读取用量服务失败:浏览器半调用的是 $mount 注册到本插件 fiber 的 usageStats 服务。" +
|
|
660
|
+
"若 host 仍在运行旧版服务端(未含 v3 overview),需由你重启当前 profile 的 host 后刷新本页(服务端改动必须重启生效)。原始错误:" +
|
|
661
|
+
msg
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
return msg;
|
|
665
|
+
}
|
|
666
|
+
/** 从 byModel 行派生两个维度的选项池(保序去重)。 */
|
|
667
|
+
function derivePool(byModel) {
|
|
668
|
+
var seenP = {},
|
|
669
|
+
seenM = {},
|
|
670
|
+
providers = [],
|
|
671
|
+
models = [];
|
|
672
|
+
(byModel || []).forEach(function (m) {
|
|
673
|
+
if (!seenP[m.provider]) {
|
|
674
|
+
seenP[m.provider] = 1;
|
|
675
|
+
providers.push({ v: m.provider, l: m.provider });
|
|
676
|
+
}
|
|
677
|
+
if (!seenM[m.key]) {
|
|
678
|
+
seenM[m.key] = 1;
|
|
679
|
+
models.push({ v: m.key, l: m.key });
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
return { providers: providers, models: models };
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/* ---------- cordis 客户端插件入口 ---------- */
|
|
686
|
+
var inject = ["slots", "remote"];
|
|
687
|
+
|
|
688
|
+
function apply(ctx) {
|
|
689
|
+
ensureCss();
|
|
690
|
+
var mounted = ctx.remote.$mount(CONTRIBUTION);
|
|
691
|
+
mounted.then(null, function () {}); // 无人等待时兜底,防未处理 rejection;页内 await 仍会把错误抛给 try/catch
|
|
692
|
+
// 宿主 General 分区左列表的独立 section 页(order 30:general0/models10/plugins15/agent-presets20 之后)。
|
|
693
|
+
ctx.effect(function () {
|
|
694
|
+
return ctx.slots.inject("settings.section", function () {
|
|
695
|
+
return ctx.slots.register(
|
|
696
|
+
{
|
|
697
|
+
name: "settings.section",
|
|
698
|
+
id: SETTINGS_NS,
|
|
699
|
+
order: 30,
|
|
700
|
+
label: function () {
|
|
701
|
+
return "Token 用量";
|
|
702
|
+
},
|
|
703
|
+
},
|
|
704
|
+
function () {
|
|
705
|
+
return h(UsageStatsSection, { ctx: ctx, mounted: mounted });
|
|
706
|
+
},
|
|
707
|
+
);
|
|
708
|
+
});
|
|
709
|
+
}, "usage-stats: settings section");
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
exports.apply = apply;
|
|
713
|
+
exports.inject = inject;
|
|
714
|
+
return module.exports;
|
|
715
|
+
},
|
|
716
|
+
});
|