mioku-plugin-help 2.0.0 → 2.1.1
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/help/html-generator.ts +556 -0
- package/help/image.ts +216 -0
- package/help/index.ts +24 -0
- package/help/info.ts +41 -0
- package/help/intent.ts +379 -0
- package/help/role-config.ts +67 -0
- package/help/types.ts +40 -0
- package/index.ts +69 -11
- package/package.json +4 -3
- package/skills.ts +132 -77
- package/status/data-collector.ts +752 -0
- package/status/html-generator.ts +1202 -0
- package/status/image.ts +81 -0
- package/status/index.ts +28 -0
- package/status/intent.ts +43 -0
- package/status/network-sampler.ts +129 -0
- package/status/performance-monitor.ts +102 -0
- package/status/types.ts +264 -0
- package/theme.ts +138 -0
- package/utils.ts +129 -0
- package/shared.ts +0 -1276
|
@@ -0,0 +1,1202 @@
|
|
|
1
|
+
import {
|
|
2
|
+
escapeHtml,
|
|
3
|
+
} from "../utils";
|
|
4
|
+
import {
|
|
5
|
+
getHelpTheme,
|
|
6
|
+
HELP_BACKGROUND_IMAGE_URL,
|
|
7
|
+
} from "../theme";
|
|
8
|
+
import type {
|
|
9
|
+
AIUsageStatsLite,
|
|
10
|
+
BotAccountStatus,
|
|
11
|
+
DiskEntry,
|
|
12
|
+
NetworkSample,
|
|
13
|
+
ResourceStatus,
|
|
14
|
+
StatusSnapshot,
|
|
15
|
+
} from "./types";
|
|
16
|
+
|
|
17
|
+
const WIDTH = 760;
|
|
18
|
+
const PIE_RADIUS = 44;
|
|
19
|
+
const PIE_CIRCUMFERENCE = 2 * Math.PI * PIE_RADIUS;
|
|
20
|
+
|
|
21
|
+
function sectionTitle(text: string): string {
|
|
22
|
+
return `<div class="status-section-title"><span>${escapeHtml(text)}</span></div>`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function fmtPercent(n: number, digits = 1): string {
|
|
26
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
27
|
+
return "0%";
|
|
28
|
+
}
|
|
29
|
+
if (n > 100) {
|
|
30
|
+
return "100%";
|
|
31
|
+
}
|
|
32
|
+
if (n < 10) {
|
|
33
|
+
return `${n.toFixed(digits)}%`;
|
|
34
|
+
}
|
|
35
|
+
return `${Math.round(n)}%`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function fmtNumber(n: number): string {
|
|
39
|
+
if (!Number.isFinite(n)) {
|
|
40
|
+
return "—";
|
|
41
|
+
}
|
|
42
|
+
if (n >= 1_000_000) {
|
|
43
|
+
return `${(n / 1_000_000).toFixed(2)}M`;
|
|
44
|
+
}
|
|
45
|
+
if (n >= 10_000) {
|
|
46
|
+
return `${(n / 1000).toFixed(1)}K`;
|
|
47
|
+
}
|
|
48
|
+
return n.toLocaleString("zh-CN");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fmtBytes(n: number, digits = 2): string {
|
|
52
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
53
|
+
return "0 B";
|
|
54
|
+
}
|
|
55
|
+
if (n >= 1024 ** 3) {
|
|
56
|
+
return `${(n / 1024 ** 3).toFixed(digits)} GB`;
|
|
57
|
+
}
|
|
58
|
+
if (n >= 1024 ** 2) {
|
|
59
|
+
return `${(n / 1024 ** 2).toFixed(digits)} MB`;
|
|
60
|
+
}
|
|
61
|
+
if (n >= 1024) {
|
|
62
|
+
return `${(n / 1024).toFixed(digits)} KB`;
|
|
63
|
+
}
|
|
64
|
+
return `${n.toFixed(0)} B`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function fmtBps(bps: number): string {
|
|
68
|
+
if (!Number.isFinite(bps) || bps <= 0) {
|
|
69
|
+
return "0 B/s";
|
|
70
|
+
}
|
|
71
|
+
return `${fmtBytes(bps)}/s`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function fmtUptime(ms: number): string {
|
|
75
|
+
if (!Number.isFinite(ms) || ms <= 0) {
|
|
76
|
+
return "—";
|
|
77
|
+
}
|
|
78
|
+
const sec = Math.floor(ms / 1000);
|
|
79
|
+
const days = Math.floor(sec / 86400);
|
|
80
|
+
const hours = Math.floor((sec % 86400) / 3600);
|
|
81
|
+
const minutes = Math.floor((sec % 3600) / 60);
|
|
82
|
+
if (days > 0) {
|
|
83
|
+
return `${days}天${String(hours).padStart(2, "0")}时${String(minutes).padStart(2, "0")}分`;
|
|
84
|
+
}
|
|
85
|
+
if (hours > 0) {
|
|
86
|
+
return `${hours}时${String(minutes).padStart(2, "0")}分`;
|
|
87
|
+
}
|
|
88
|
+
return `${minutes}分`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function progressBar(percent: number, color: string): string {
|
|
92
|
+
const clamped = Math.max(0, Math.min(100, percent));
|
|
93
|
+
return `<div class="status-bar"><div class="status-bar__fill" style="width:${clamped}%;background:${color};"></div></div>`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function progressColor(percent: number, theme: ReturnType<typeof getHelpTheme>): string {
|
|
97
|
+
if (percent >= 85) {
|
|
98
|
+
return "linear-gradient(90deg, #ef4444, #f97316)";
|
|
99
|
+
}
|
|
100
|
+
if (percent >= 65) {
|
|
101
|
+
return "linear-gradient(90deg, #f59e0b, #fbbf24)";
|
|
102
|
+
}
|
|
103
|
+
return `linear-gradient(90deg, ${theme.eyebrow}, ${theme.commandTitle})`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function pieColor(percent: number, theme: ReturnType<typeof getHelpTheme>): string {
|
|
107
|
+
if (percent >= 85) {
|
|
108
|
+
return "#ef4444";
|
|
109
|
+
}
|
|
110
|
+
if (percent >= 65) {
|
|
111
|
+
return "#f59e0b";
|
|
112
|
+
}
|
|
113
|
+
return theme.eyebrow;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function renderPieChart(percent: number, theme: ReturnType<typeof getHelpTheme>): string {
|
|
117
|
+
const clamped = Math.max(0, Math.min(100, percent));
|
|
118
|
+
const filled = (clamped / 100) * PIE_CIRCUMFERENCE;
|
|
119
|
+
const rest = PIE_CIRCUMFERENCE - filled;
|
|
120
|
+
const color = pieColor(percent, theme);
|
|
121
|
+
const track = theme.isNightMode
|
|
122
|
+
? "rgba(125, 211, 197, 0.12)"
|
|
123
|
+
: "rgba(15, 118, 110, 0.12)";
|
|
124
|
+
// If percent is 0 the slice disappears; the caller already substitutes
|
|
125
|
+
// an alternate "未配置" line for SWAP-with-no-swap, but keep the visual
|
|
126
|
+
// graceful by also letting the label fall back to "—".
|
|
127
|
+
const text = clamped <= 0 ? "—" : fmtPercent(percent, 0);
|
|
128
|
+
return `
|
|
129
|
+
<svg viewBox="0 0 110 110" class="status-pie">
|
|
130
|
+
<circle cx="55" cy="55" r="${PIE_RADIUS}" fill="none" stroke="${track}" stroke-width="10"/>
|
|
131
|
+
<circle cx="55" cy="55" r="${PIE_RADIUS}" fill="none" stroke="${color}" stroke-width="10"
|
|
132
|
+
stroke-dasharray="${filled.toFixed(2)} ${rest.toFixed(2)}"
|
|
133
|
+
stroke-linecap="round"
|
|
134
|
+
transform="rotate(-90 55 55)"/>
|
|
135
|
+
<text x="55" y="55" text-anchor="middle" dominant-baseline="central"
|
|
136
|
+
font-size="26" font-weight="800" font-family="SF Mono, monospace"
|
|
137
|
+
fill="${theme.panelTitle}">${escapeHtml(text)}</text>
|
|
138
|
+
</svg>
|
|
139
|
+
`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function renderHero(
|
|
143
|
+
snapshot: StatusSnapshot,
|
|
144
|
+
theme: ReturnType<typeof getHelpTheme>,
|
|
145
|
+
): string {
|
|
146
|
+
const bots = snapshot.bots;
|
|
147
|
+
|
|
148
|
+
const accountRows = bots.length === 0
|
|
149
|
+
? `<div class="status-hero__empty">当前没有在线账号</div>`
|
|
150
|
+
: bots
|
|
151
|
+
.map((bot) => {
|
|
152
|
+
const statusText = bot.online ? "在线" : "离线";
|
|
153
|
+
const statusColor = bot.online ? "#10b981" : "#ef4444";
|
|
154
|
+
const frameworkText =
|
|
155
|
+
bot.appVersion && bot.appVersion !== "unknown"
|
|
156
|
+
? `${bot.framework} ${bot.appVersion} · 协议 ${bot.protocolVersion}`
|
|
157
|
+
: bot.framework;
|
|
158
|
+
// Nickname is rendered as a bold large heading above the avatar
|
|
159
|
+
// row (not as a chip). Chips below only carry the metadata.
|
|
160
|
+
const tags = [
|
|
161
|
+
{ text: String(bot.uin || "—"), kind: "data" },
|
|
162
|
+
{ text: statusText, kind: bot.online ? "ok" : "danger" },
|
|
163
|
+
{ text: `好友 ${fmtNumber(bot.friendCount)}`, kind: "data" },
|
|
164
|
+
{ text: `群聊 ${fmtNumber(bot.groupCount)}`, kind: "data" },
|
|
165
|
+
{ text: `运行时长 ${fmtUptime(bot.onlineDurationMs)}`, kind: "data" },
|
|
166
|
+
{ text: `收 ${fmtNumber(bot.receive)}`, kind: "data" },
|
|
167
|
+
{ text: `发 ${fmtNumber(bot.send)}`, kind: "data" },
|
|
168
|
+
{ text: frameworkText, kind: "data" },
|
|
169
|
+
];
|
|
170
|
+
const tagsHtml = tags
|
|
171
|
+
.map(
|
|
172
|
+
(t) => `<span class="status-chip status-chip--${t.kind}">${escapeHtml(t.text)}</span>`,
|
|
173
|
+
)
|
|
174
|
+
.join("");
|
|
175
|
+
return `
|
|
176
|
+
<div class="status-hero__account">
|
|
177
|
+
<div class="status-hero__account-name">${escapeHtml(bot.nickname)}</div>
|
|
178
|
+
<div class="status-hero__account-body">
|
|
179
|
+
<span class="status-hero__account-avatar-wrap">
|
|
180
|
+
<img class="status-hero__account-avatar" src="${escapeHtml(bot.avatarUrl)}" alt="${escapeHtml(bot.nickname)}" loading="lazy" onerror="this.style.visibility='hidden'"/>
|
|
181
|
+
<span class="status-hero__account-avatar-dot" style="background:${statusColor};"></span>
|
|
182
|
+
</span>
|
|
183
|
+
<span class="status-hero__account-tags">${tagsHtml}</span>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
`;
|
|
187
|
+
})
|
|
188
|
+
.join("");
|
|
189
|
+
|
|
190
|
+
return `
|
|
191
|
+
<header class="status-hero">
|
|
192
|
+
<div class="status-hero__content">
|
|
193
|
+
<div class="status-hero__eyebrow">MIOKU STATUS</div>
|
|
194
|
+
<div class="status-hero__accounts">${accountRows}</div>
|
|
195
|
+
</div>
|
|
196
|
+
</header>
|
|
197
|
+
`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function renderResourceCard(
|
|
201
|
+
label: string,
|
|
202
|
+
percent: number,
|
|
203
|
+
lines: string[],
|
|
204
|
+
theme: ReturnType<typeof getHelpTheme>,
|
|
205
|
+
): string {
|
|
206
|
+
const linesHtml = lines
|
|
207
|
+
.map(
|
|
208
|
+
(line) =>
|
|
209
|
+
`<div class="status-pie-card__line">${escapeHtml(line)}</div>`,
|
|
210
|
+
)
|
|
211
|
+
.join("");
|
|
212
|
+
return `
|
|
213
|
+
<div class="status-pie-card">
|
|
214
|
+
${renderPieChart(percent, theme)}
|
|
215
|
+
<div class="status-pie-card__label">${escapeHtml(label)}</div>
|
|
216
|
+
<div class="status-pie-card__lines">${linesHtml}</div>
|
|
217
|
+
</div>
|
|
218
|
+
`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function renderResourcesSection(
|
|
222
|
+
snapshot: StatusSnapshot,
|
|
223
|
+
theme: ReturnType<typeof getHelpTheme>,
|
|
224
|
+
): string {
|
|
225
|
+
const r = snapshot.resources;
|
|
226
|
+
const hasSwap = r.swapTotalGB > 0;
|
|
227
|
+
const cards = [
|
|
228
|
+
renderResourceCard(
|
|
229
|
+
"CPU",
|
|
230
|
+
r.cpuPercent,
|
|
231
|
+
[r.cpuModelShort, `${r.cpuCores} 核 · ${r.cpuSpeedGHz}`],
|
|
232
|
+
theme,
|
|
233
|
+
),
|
|
234
|
+
renderResourceCard(
|
|
235
|
+
"内存",
|
|
236
|
+
r.memPercent,
|
|
237
|
+
[
|
|
238
|
+
`${r.memUsedGB} / ${r.memTotalGB} GB`,
|
|
239
|
+
r.memBuffCacheGB > 0
|
|
240
|
+
? `缓存 ${r.memBuffCacheGB} GB`
|
|
241
|
+
: `${snapshot.framework.runtime} 进程视角`,
|
|
242
|
+
],
|
|
243
|
+
theme,
|
|
244
|
+
),
|
|
245
|
+
renderResourceCard(
|
|
246
|
+
"SWAP",
|
|
247
|
+
hasSwap ? r.swapPercent : 0,
|
|
248
|
+
hasSwap
|
|
249
|
+
? [`${r.swapUsedGB} / ${r.swapTotalGB} GB`, `可用 ${Math.max(0, r.swapTotalGB - r.swapUsedGB).toFixed(2)} GB`]
|
|
250
|
+
: ["未配置", ""],
|
|
251
|
+
theme,
|
|
252
|
+
),
|
|
253
|
+
].join("");
|
|
254
|
+
return `
|
|
255
|
+
${sectionTitle("系统性能")}
|
|
256
|
+
<div class="status-pie-grid">${cards}</div>
|
|
257
|
+
`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function renderRuntimeSection(
|
|
261
|
+
snapshot: StatusSnapshot,
|
|
262
|
+
theme: ReturnType<typeof getHelpTheme>,
|
|
263
|
+
): string {
|
|
264
|
+
const r = snapshot.runtime;
|
|
265
|
+
const runtimeName = snapshot.framework.runtime;
|
|
266
|
+
const gc = r.gc;
|
|
267
|
+
const items: Array<{ label: string; value: string }> = [
|
|
268
|
+
{ label: "Heap Used", value: fmtBytes(r.heapUsedMB * 1024 ** 2, 1) },
|
|
269
|
+
{ label: "Heap Total", value: fmtBytes(r.heapTotalMB * 1024 ** 2, 1) },
|
|
270
|
+
{ label: "RSS", value: fmtBytes(r.rssMB * 1024 ** 2, 1) },
|
|
271
|
+
{ label: "External", value: fmtBytes(r.externalMB * 1024 ** 2, 1) },
|
|
272
|
+
{ label: "ArrayBuffer", value: fmtBytes(r.arrayBuffersMB * 1024 ** 2, 1) },
|
|
273
|
+
{ label: "Loop Delay", value: `${r.eventLoopDelayMs.mean.toFixed(2)} ms` },
|
|
274
|
+
{ label: "Loop p99", value: `${r.eventLoopDelayMs.p99.toFixed(2)} ms` },
|
|
275
|
+
];
|
|
276
|
+
// GC count only works under V8 with --expose-gc. Skip the row entirely
|
|
277
|
+
// for Bun (which doesn't expose it) instead of showing a confusing
|
|
278
|
+
// "N/A" tile.
|
|
279
|
+
if (gc && gc.available) {
|
|
280
|
+
items.push({ label: "GC Count", value: `${gc.count} 次` });
|
|
281
|
+
}
|
|
282
|
+
const cardsHtml = items
|
|
283
|
+
.map(
|
|
284
|
+
(it) => `
|
|
285
|
+
<div class="status-runtime-block">
|
|
286
|
+
<div class="status-runtime-block__label">${escapeHtml(it.label)}</div>
|
|
287
|
+
<div class="status-runtime-block__value">${escapeHtml(it.value)}</div>
|
|
288
|
+
</div>
|
|
289
|
+
`,
|
|
290
|
+
)
|
|
291
|
+
.join("");
|
|
292
|
+
return `
|
|
293
|
+
${sectionTitle(`${runtimeName} Runtime`)}
|
|
294
|
+
<div class="status-runtime-grid">${cardsHtml}</div>
|
|
295
|
+
`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function buildSmoothPath(points: Array<{ x: number; y: number }>): string {
|
|
299
|
+
if (points.length === 0) return "";
|
|
300
|
+
if (points.length === 1) {
|
|
301
|
+
return `M ${points[0].x} ${points[0].y}`;
|
|
302
|
+
}
|
|
303
|
+
if (points.length === 2) {
|
|
304
|
+
return `M ${points[0].x} ${points[0].y} L ${points[1].x} ${points[1].y}`;
|
|
305
|
+
}
|
|
306
|
+
// Catmull-Rom → cubic Bézier. The curve passes through every data point
|
|
307
|
+
// (unlike the previous midpoint-based quadratic, which skipped them and
|
|
308
|
+
// visually flattened peaks). Tension 0.5 is the standard Catmull-Rom
|
|
309
|
+
// default; lowering it makes the curve flatter between samples.
|
|
310
|
+
let d = `M ${points[0].x.toFixed(2)} ${points[0].y.toFixed(2)}`;
|
|
311
|
+
const tension = 0.35;
|
|
312
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
313
|
+
const p0 = points[Math.max(0, i - 1)];
|
|
314
|
+
const p1 = points[i];
|
|
315
|
+
const p2 = points[i + 1];
|
|
316
|
+
const p3 = points[Math.min(points.length - 1, i + 2)];
|
|
317
|
+
const cp1x = p1.x + (p2.x - p0.x) * tension / 3;
|
|
318
|
+
const cp1y = p1.y + (p2.y - p0.y) * tension / 3;
|
|
319
|
+
const cp2x = p2.x - (p3.x - p1.x) * tension / 3;
|
|
320
|
+
const cp2y = p2.y - (p3.y - p1.y) * tension / 3;
|
|
321
|
+
d += ` C ${cp1x.toFixed(2)} ${cp1y.toFixed(2)}, ${cp2x.toFixed(2)} ${cp2y.toFixed(2)}, ${p2.x.toFixed(2)} ${p2.y.toFixed(2)}`;
|
|
322
|
+
}
|
|
323
|
+
return d;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function renderNetworkChart(history: NetworkSample[]): string {
|
|
327
|
+
const w = WIDTH - 56;
|
|
328
|
+
const h = 120;
|
|
329
|
+
const marginL = 56;
|
|
330
|
+
const marginR = 12;
|
|
331
|
+
const marginT = 10;
|
|
332
|
+
const marginB = 22;
|
|
333
|
+
const innerW = w - marginL - marginR;
|
|
334
|
+
const innerH = h - marginT - marginB;
|
|
335
|
+
|
|
336
|
+
if (history.length < 2) {
|
|
337
|
+
return `<svg viewBox="0 0 ${w} ${h}" class="status-chart"><text x="${w / 2}" y="${h / 2}" text-anchor="middle" fill="#94a3b8" font-size="11">数据采集中…</text></svg>`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const maxVal = Math.max(
|
|
341
|
+
1,
|
|
342
|
+
...history.map((s) => Math.max(s.rxBps, s.txBps)),
|
|
343
|
+
);
|
|
344
|
+
const yMax = maxVal * 1.2;
|
|
345
|
+
const t0 = history[0].ts;
|
|
346
|
+
const t1 = history[history.length - 1].ts;
|
|
347
|
+
const dt = Math.max(1, t1 - t0);
|
|
348
|
+
|
|
349
|
+
const xOf = (ts: number) => marginL + ((ts - t0) / dt) * innerW;
|
|
350
|
+
const yOf = (v: number) => marginT + innerH - (v / yMax) * innerH;
|
|
351
|
+
|
|
352
|
+
const rxPoints = history.map((s) => ({ x: xOf(s.ts), y: yOf(s.rxBps) }));
|
|
353
|
+
const txPoints = history.map((s) => ({ x: xOf(s.ts), y: yOf(s.txBps) }));
|
|
354
|
+
|
|
355
|
+
const rxPath = buildSmoothPath(rxPoints);
|
|
356
|
+
const txPath = buildSmoothPath(txPoints);
|
|
357
|
+
|
|
358
|
+
// 用同一条 path 做面积填充(淡色)
|
|
359
|
+
const rxArea =
|
|
360
|
+
rxPoints.length > 0
|
|
361
|
+
? `${rxPath} L ${rxPoints[rxPoints.length - 1].x.toFixed(2)} ${(marginT + innerH).toFixed(2)} L ${rxPoints[0].x.toFixed(2)} ${(marginT + innerH).toFixed(2)} Z`
|
|
362
|
+
: "";
|
|
363
|
+
|
|
364
|
+
// 4 条横向网格
|
|
365
|
+
const gridY: number[] = [];
|
|
366
|
+
for (let i = 0; i <= 3; i++) {
|
|
367
|
+
gridY.push(marginT + (innerH * i) / 3);
|
|
368
|
+
}
|
|
369
|
+
// 6 条竖向网格
|
|
370
|
+
const gridX: number[] = [];
|
|
371
|
+
for (let i = 0; i <= 5; i++) {
|
|
372
|
+
gridX.push(marginL + (innerW * i) / 5);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const yLabels = gridY
|
|
376
|
+
.map((gy) => {
|
|
377
|
+
const v = ((marginT + innerH - gy) / innerH) * yMax;
|
|
378
|
+
return `<text x="${marginL - 6}" y="${gy + 3}" text-anchor="end" font-size="10" fill="#94a3b8">${fmtBytes(v)}/s</text>`;
|
|
379
|
+
})
|
|
380
|
+
.join("");
|
|
381
|
+
const xLabels = gridX
|
|
382
|
+
.map((gx) => {
|
|
383
|
+
const tsAt = t0 + ((gx - marginL) / innerW) * dt;
|
|
384
|
+
const minutesAgo = Math.round(((t1 - tsAt) / 60000) * 10) / 10;
|
|
385
|
+
const label = minutesAgo <= 0.1 ? "now" : `-${minutesAgo}m`;
|
|
386
|
+
return `<text x="${gx}" y="${h - 6}" text-anchor="middle" font-size="10" fill="#94a3b8">${label}</text>`;
|
|
387
|
+
})
|
|
388
|
+
.join("");
|
|
389
|
+
|
|
390
|
+
const gridLines = gridY
|
|
391
|
+
.map(
|
|
392
|
+
(gy) =>
|
|
393
|
+
`<line x1="${marginL}" y1="${gy}" x2="${marginL + innerW}" y2="${gy}" stroke="rgba(148,163,184,0.18)" stroke-dasharray="2 3"/>`,
|
|
394
|
+
)
|
|
395
|
+
.join("");
|
|
396
|
+
|
|
397
|
+
return `
|
|
398
|
+
<svg viewBox="0 0 ${w} ${h}" class="status-chart">
|
|
399
|
+
${gridLines}
|
|
400
|
+
<path d="${rxArea}" fill="rgba(34, 211, 238, 0.12)" stroke="none"/>
|
|
401
|
+
<path d="${txPath}" fill="none" stroke="#f59e0b" stroke-width="1.8" stroke-linejoin="round" stroke-linecap="round"/>
|
|
402
|
+
<path d="${rxPath}" fill="none" stroke="#22d3ee" stroke-width="1.8" stroke-linejoin="round" stroke-linecap="round"/>
|
|
403
|
+
${yLabels}
|
|
404
|
+
${xLabels}
|
|
405
|
+
</svg>
|
|
406
|
+
`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function renderNetworkSection(
|
|
410
|
+
snapshot: StatusSnapshot,
|
|
411
|
+
theme: ReturnType<typeof getHelpTheme>,
|
|
412
|
+
): string {
|
|
413
|
+
const n = snapshot.network;
|
|
414
|
+
const cells = [
|
|
415
|
+
{ label: "↑ 速度", value: fmtBps(n.txBps) },
|
|
416
|
+
{ label: "↓ 速度", value: fmtBps(n.rxBps) },
|
|
417
|
+
{ label: "↑ 总量", value: fmtBytes(n.txTotalBytes) },
|
|
418
|
+
{ label: "↓ 总量", value: fmtBytes(n.rxTotalBytes) },
|
|
419
|
+
]
|
|
420
|
+
.map(
|
|
421
|
+
(c) => `
|
|
422
|
+
<div class="status-net-cell">
|
|
423
|
+
<div class="status-net-cell__label">${escapeHtml(c.label)}</div>
|
|
424
|
+
<div class="status-net-cell__value">${escapeHtml(c.value)}</div>
|
|
425
|
+
</div>
|
|
426
|
+
`,
|
|
427
|
+
)
|
|
428
|
+
.join("");
|
|
429
|
+
return `
|
|
430
|
+
${sectionTitle("网络状态 · 最近 30 分钟")}
|
|
431
|
+
<div class="status-chart-wrap">${renderNetworkChart(n.history)}</div>
|
|
432
|
+
<div class="status-net-grid">${cells}</div>
|
|
433
|
+
`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function renderDiskSection(
|
|
437
|
+
snapshot: StatusSnapshot,
|
|
438
|
+
theme: ReturnType<typeof getHelpTheme>,
|
|
439
|
+
): string {
|
|
440
|
+
const disk = snapshot.disk;
|
|
441
|
+
if (!disk.entries || disk.entries.length === 0) {
|
|
442
|
+
return `
|
|
443
|
+
${sectionTitle("磁盘状态")}
|
|
444
|
+
<div class="status-empty">磁盘信息不可用</div>
|
|
445
|
+
`;
|
|
446
|
+
}
|
|
447
|
+
const bars = disk.entries
|
|
448
|
+
.map((entry: DiskEntry) => {
|
|
449
|
+
return `
|
|
450
|
+
<div class="status-disk-row">
|
|
451
|
+
<div class="status-disk-row__head">
|
|
452
|
+
<span class="status-disk-row__mount">${escapeHtml(entry.mount)}</span>
|
|
453
|
+
<span class="status-disk-row__usage">${entry.usedGB}GB / ${entry.totalGB}GB · ${fmtPercent(entry.percent)}</span>
|
|
454
|
+
</div>
|
|
455
|
+
${progressBar(entry.percent, progressColor(entry.percent, theme))}
|
|
456
|
+
</div>
|
|
457
|
+
`;
|
|
458
|
+
})
|
|
459
|
+
.join("");
|
|
460
|
+
return `
|
|
461
|
+
${sectionTitle("磁盘状态")}
|
|
462
|
+
<div class="status-disk-list">${bars}</div>
|
|
463
|
+
`;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function renderSystemSection(
|
|
467
|
+
snapshot: StatusSnapshot,
|
|
468
|
+
theme: ReturnType<typeof getHelpTheme>,
|
|
469
|
+
): string {
|
|
470
|
+
const s = snapshot.system;
|
|
471
|
+
const r = snapshot.resources;
|
|
472
|
+
// Horizontal layout: label and value on the same row, label fixed-width
|
|
473
|
+
// on the left, value monospace on the right. Multi-GPU / multi-stick get
|
|
474
|
+
// their own row with an indexed label (e.g. "显卡 1", "内存条 2").
|
|
475
|
+
const items: Array<{ label: string; value: string }> = [];
|
|
476
|
+
|
|
477
|
+
items.push({ label: "OS", value: s.os });
|
|
478
|
+
items.push({ label: "内核", value: s.kernel });
|
|
479
|
+
items.push({ label: "处理器", value: `${s.cpu} · ${r.cpuCores} 核` });
|
|
480
|
+
|
|
481
|
+
// GPUs: list every controller. Many systems have an integrated + discrete
|
|
482
|
+
// pair (Apple Silicon + eGPU, Intel iGPU + NVIDIA dGPU). vramGB is 0 for
|
|
483
|
+
// integrated / unknown, in which case we skip the vram suffix.
|
|
484
|
+
if (s.gpus.length === 0) {
|
|
485
|
+
items.push({ label: "显卡", value: "N/A" });
|
|
486
|
+
} else {
|
|
487
|
+
s.gpus.forEach((g, i) => {
|
|
488
|
+
const vram = g.vramGB > 0 ? ` · ${g.vramGB} GB` : "";
|
|
489
|
+
const vendor = g.vendor && g.vendor !== g.model ? `${g.vendor} ` : "";
|
|
490
|
+
items.push({
|
|
491
|
+
label: `显卡 ${i + 1}`,
|
|
492
|
+
value: `${vendor}${g.model}${vram}`.trim(),
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Memory sticks: memLayout() is Linux/Win only. macOS returns [] and we
|
|
498
|
+
// surface "N/A" rather than hide the row. Headline on the value side;
|
|
499
|
+
// part number is appended inline when present.
|
|
500
|
+
if (s.memSticks.length === 0) {
|
|
501
|
+
items.push({ label: "内存条", value: "N/A" });
|
|
502
|
+
} else {
|
|
503
|
+
s.memSticks.forEach((m, i) => {
|
|
504
|
+
const size =
|
|
505
|
+
m.sizeGB >= 1
|
|
506
|
+
? `${m.sizeGB} GB`
|
|
507
|
+
: `${(m.sizeGB * 1024).toFixed(0)} MB`;
|
|
508
|
+
const speed = m.speedMTs > 0 ? ` · ${m.speedMTs} MT/s` : "";
|
|
509
|
+
const manu =
|
|
510
|
+
m.manufacturer && m.manufacturer !== "Manufacturer"
|
|
511
|
+
? `${m.manufacturer} `
|
|
512
|
+
: "";
|
|
513
|
+
const bank = m.bank && m.bank !== "BANK 0" ? `${m.bank} · ` : "";
|
|
514
|
+
const headline = `${bank}${manu}${m.type} ${size}${speed}`.trim();
|
|
515
|
+
const value =
|
|
516
|
+
m.partNum && m.partNum !== "Unknown" && m.partNum !== "00000000"
|
|
517
|
+
? `${headline} · PN ${m.partNum}`
|
|
518
|
+
: headline;
|
|
519
|
+
items.push({
|
|
520
|
+
label: `内存条 ${i + 1}`,
|
|
521
|
+
value,
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const biosDate = s.bios.releaseDate ? ` · ${s.bios.releaseDate}` : "";
|
|
527
|
+
items.push({
|
|
528
|
+
label: "BIOS",
|
|
529
|
+
value: `${s.bios.vendor} ${s.bios.version}${biosDate}`.trim(),
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// Physical disk drives. Show vendor + name + type + size. On a typical
|
|
533
|
+
// server this might be "Samsung SSD 990 PRO 2TB · NVMe"; on macOS disk
|
|
534
|
+
// details are often hidden so we fall back to size only.
|
|
535
|
+
if (s.disks.length === 0) {
|
|
536
|
+
items.push({ label: "硬盘", value: "N/A" });
|
|
537
|
+
} else {
|
|
538
|
+
s.disks.forEach((d, i) => {
|
|
539
|
+
const sizeLabel =
|
|
540
|
+
d.sizeGB >= 1000
|
|
541
|
+
? `${(d.sizeGB / 1000).toFixed(1)} TB`
|
|
542
|
+
: `${d.sizeGB} GB`;
|
|
543
|
+
const vendor = d.vendor && d.vendor !== d.name ? `${d.vendor} ` : "";
|
|
544
|
+
const name = d.name || d.type || "Unknown";
|
|
545
|
+
const iface = d.interfaceType && d.interfaceType !== "Unknown" ? ` · ${d.interfaceType}` : "";
|
|
546
|
+
items.push({
|
|
547
|
+
label: `硬盘 ${i + 1}`,
|
|
548
|
+
value: `${vendor}${name} · ${sizeLabel}${iface}`.trim(),
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
items.push({ label: "主机", value: s.chassis });
|
|
554
|
+
|
|
555
|
+
const itemsHtml = items
|
|
556
|
+
.map(
|
|
557
|
+
({ label, value }) => `
|
|
558
|
+
<div class="status-sys-row">
|
|
559
|
+
<span class="status-sys-row__label">${escapeHtml(label)}</span>
|
|
560
|
+
<span class="status-sys-row__value">${escapeHtml(value)}</span>
|
|
561
|
+
</div>
|
|
562
|
+
`,
|
|
563
|
+
)
|
|
564
|
+
.join("");
|
|
565
|
+
return `
|
|
566
|
+
${sectionTitle("系统信息")}
|
|
567
|
+
<div class="status-sys-list">${itemsHtml}</div>
|
|
568
|
+
`;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function renderRankingBars(
|
|
572
|
+
items: Array<{ name: string; value: number }>,
|
|
573
|
+
formatValue: (n: number) => string,
|
|
574
|
+
color: string,
|
|
575
|
+
): string {
|
|
576
|
+
if (items.length === 0) {
|
|
577
|
+
return `<div class="status-empty">暂无数据</div>`;
|
|
578
|
+
}
|
|
579
|
+
const max = Math.max(1, ...items.map((i) => i.value));
|
|
580
|
+
return items
|
|
581
|
+
.map((item) => {
|
|
582
|
+
const percent = Math.max(2, (item.value / max) * 100);
|
|
583
|
+
return `
|
|
584
|
+
<div class="status-rank-row">
|
|
585
|
+
<div class="status-rank-row__name">${escapeHtml(item.name)}</div>
|
|
586
|
+
<div class="status-rank-row__bar">
|
|
587
|
+
<div class="status-rank-row__fill" style="width:${percent}%;background:${color};"></div>
|
|
588
|
+
</div>
|
|
589
|
+
<div class="status-rank-row__value">${escapeHtml(formatValue(item.value))}</div>
|
|
590
|
+
</div>
|
|
591
|
+
`;
|
|
592
|
+
})
|
|
593
|
+
.join("");
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function renderAISection(
|
|
597
|
+
ai: AIUsageStatsLite,
|
|
598
|
+
theme: ReturnType<typeof getHelpTheme>,
|
|
599
|
+
): string {
|
|
600
|
+
if (!ai.available) {
|
|
601
|
+
return `
|
|
602
|
+
${sectionTitle("AI 统计 · 近 7 天")}
|
|
603
|
+
<div class="status-empty">AI 统计暂不可用</div>
|
|
604
|
+
`;
|
|
605
|
+
}
|
|
606
|
+
const totals = [
|
|
607
|
+
{ label: "请求数", value: fmtNumber(ai.totalRequests) },
|
|
608
|
+
{ label: "错误率", value: fmtPercent(ai.errorRate * 100) },
|
|
609
|
+
{ label: "缓存命中", value: fmtPercent(ai.cacheHitRate * 100) },
|
|
610
|
+
{ label: "输入 Token", value: fmtNumber(ai.inputTokens) },
|
|
611
|
+
{ label: "输出 Token", value: fmtNumber(ai.outputTokens) },
|
|
612
|
+
{ label: "总 Token", value: fmtNumber(ai.totalTokens) },
|
|
613
|
+
];
|
|
614
|
+
const totalsHtml = totals
|
|
615
|
+
.map(
|
|
616
|
+
(t) => `
|
|
617
|
+
<div class="status-kv">
|
|
618
|
+
<div class="status-kv__label">${escapeHtml(t.label)}</div>
|
|
619
|
+
<div class="status-kv__value">${escapeHtml(t.value)}</div>
|
|
620
|
+
</div>
|
|
621
|
+
`,
|
|
622
|
+
)
|
|
623
|
+
.join("");
|
|
624
|
+
const toolsColor = theme.isNightMode
|
|
625
|
+
? "linear-gradient(90deg, #38bdf8, #7ee7dd)"
|
|
626
|
+
: "linear-gradient(90deg, #22d3ee, #38bdf8)";
|
|
627
|
+
const toolsHtml = renderRankingBars(
|
|
628
|
+
ai.topTools.map((t) => ({ name: t.name, value: t.count })),
|
|
629
|
+
(n) => fmtNumber(n),
|
|
630
|
+
toolsColor,
|
|
631
|
+
);
|
|
632
|
+
return `
|
|
633
|
+
${sectionTitle("AI 统计 · 近 7 天")}
|
|
634
|
+
<div class="status-kv-grid">${totalsHtml}</div>
|
|
635
|
+
<div class="status-rank-block">
|
|
636
|
+
<div class="status-rank-block__title">工具调用排名</div>
|
|
637
|
+
${toolsHtml}
|
|
638
|
+
</div>
|
|
639
|
+
`;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function buildStyle(theme: ReturnType<typeof getHelpTheme>): string {
|
|
643
|
+
return `
|
|
644
|
+
<style>
|
|
645
|
+
.status-sheet {
|
|
646
|
+
position: relative;
|
|
647
|
+
padding: 18px;
|
|
648
|
+
display: flex;
|
|
649
|
+
flex-direction: column;
|
|
650
|
+
gap: 14px;
|
|
651
|
+
background: ${theme.pageBg};
|
|
652
|
+
color: ${theme.panelTitle};
|
|
653
|
+
font-family: "SF Pro Display", "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", "Hiragino Sans GB", sans-serif;
|
|
654
|
+
overflow: hidden;
|
|
655
|
+
}
|
|
656
|
+
.status-sheet::before,
|
|
657
|
+
.status-sheet::after {
|
|
658
|
+
content: "";
|
|
659
|
+
position: absolute;
|
|
660
|
+
inset: 0;
|
|
661
|
+
pointer-events: none;
|
|
662
|
+
}
|
|
663
|
+
.status-sheet::before { background: ${theme.pageAccent}; }
|
|
664
|
+
.status-sheet::after {
|
|
665
|
+
background-image: ${theme.pageGrid};
|
|
666
|
+
background-size: 28px 28px;
|
|
667
|
+
opacity: ${theme.isNightMode ? "0.55" : "0.35"};
|
|
668
|
+
}
|
|
669
|
+
.status-sheet__scene,
|
|
670
|
+
.status-sheet__scene-image,
|
|
671
|
+
.status-sheet__scene-overlay {
|
|
672
|
+
position: absolute;
|
|
673
|
+
inset: 0;
|
|
674
|
+
pointer-events: none;
|
|
675
|
+
}
|
|
676
|
+
.status-sheet__scene { z-index: 0; overflow: hidden; }
|
|
677
|
+
.status-sheet__scene-image {
|
|
678
|
+
background-image: url("${HELP_BACKGROUND_IMAGE_URL}");
|
|
679
|
+
background-size: cover;
|
|
680
|
+
background-position: center center;
|
|
681
|
+
opacity: ${theme.sceneOpacity};
|
|
682
|
+
filter: ${theme.sceneFilter};
|
|
683
|
+
transform: scale(1.06);
|
|
684
|
+
}
|
|
685
|
+
.status-sheet__scene-overlay {
|
|
686
|
+
background: ${theme.sceneGlow}, ${theme.sceneMask};
|
|
687
|
+
}
|
|
688
|
+
.status-shell {
|
|
689
|
+
position: relative;
|
|
690
|
+
z-index: 1;
|
|
691
|
+
display: flex;
|
|
692
|
+
flex-direction: column;
|
|
693
|
+
gap: 14px;
|
|
694
|
+
border-radius: 30px;
|
|
695
|
+
border: 1px solid ${theme.shellBorder};
|
|
696
|
+
box-shadow: ${theme.shellShadow};
|
|
697
|
+
padding: 14px;
|
|
698
|
+
background: ${theme.shellBg};
|
|
699
|
+
backdrop-filter: blur(10px) saturate(1.06);
|
|
700
|
+
}
|
|
701
|
+
.status-hero {
|
|
702
|
+
position: relative;
|
|
703
|
+
display: block;
|
|
704
|
+
padding: 18px;
|
|
705
|
+
border-radius: 24px;
|
|
706
|
+
border: 1px solid ${theme.heroBorder};
|
|
707
|
+
background: ${theme.heroBg};
|
|
708
|
+
overflow: hidden;
|
|
709
|
+
}
|
|
710
|
+
.status-hero::before {
|
|
711
|
+
content: "";
|
|
712
|
+
position: absolute;
|
|
713
|
+
inset: auto auto -42px -32px;
|
|
714
|
+
width: 180px;
|
|
715
|
+
height: 180px;
|
|
716
|
+
border-radius: 999px;
|
|
717
|
+
background: ${theme.heroGlow};
|
|
718
|
+
filter: blur(4px);
|
|
719
|
+
}
|
|
720
|
+
.status-hero__content { position: relative; z-index: 1; }
|
|
721
|
+
.status-hero__eyebrow {
|
|
722
|
+
margin-bottom: 12px;
|
|
723
|
+
padding-bottom: 10px;
|
|
724
|
+
font-size: 18px;
|
|
725
|
+
font-weight: 900;
|
|
726
|
+
letter-spacing: 0.18em;
|
|
727
|
+
text-transform: uppercase;
|
|
728
|
+
color: ${theme.eyebrow};
|
|
729
|
+
border-bottom: 1px solid ${theme.isNightMode ? "rgba(126, 231, 221, 0.45)" : "rgba(15, 118, 110, 0.45)"};
|
|
730
|
+
}
|
|
731
|
+
.status-hero__accounts {
|
|
732
|
+
display: flex;
|
|
733
|
+
flex-direction: column;
|
|
734
|
+
gap: 12px;
|
|
735
|
+
}
|
|
736
|
+
.status-hero__account {
|
|
737
|
+
display: flex;
|
|
738
|
+
flex-direction: column;
|
|
739
|
+
gap: 8px;
|
|
740
|
+
padding: 10px 0;
|
|
741
|
+
/* Light-green hairline separator between accounts. The first account
|
|
742
|
+
* stays flush (no top line); each subsequent one gets the divider. */
|
|
743
|
+
border-top: 1px solid ${theme.isNightMode ? "rgba(126, 231, 221, 0.45)" : "rgba(15, 118, 110, 0.45)"};
|
|
744
|
+
}
|
|
745
|
+
.status-hero__account:first-child {
|
|
746
|
+
border-top: 0;
|
|
747
|
+
padding-top: 4px;
|
|
748
|
+
}
|
|
749
|
+
.status-hero__account-name {
|
|
750
|
+
font-size: 18px;
|
|
751
|
+
font-weight: 800;
|
|
752
|
+
line-height: 1.15;
|
|
753
|
+
color: ${theme.title};
|
|
754
|
+
letter-spacing: -0.005em;
|
|
755
|
+
word-break: break-word;
|
|
756
|
+
}
|
|
757
|
+
.status-hero__account-body {
|
|
758
|
+
display: flex;
|
|
759
|
+
align-items: center;
|
|
760
|
+
gap: 14px;
|
|
761
|
+
}
|
|
762
|
+
.status-hero__account-avatar-wrap {
|
|
763
|
+
position: relative;
|
|
764
|
+
width: 88px;
|
|
765
|
+
height: 88px;
|
|
766
|
+
flex-shrink: 0;
|
|
767
|
+
display: flex;
|
|
768
|
+
align-items: center;
|
|
769
|
+
justify-content: center;
|
|
770
|
+
}
|
|
771
|
+
.status-hero__account-avatar {
|
|
772
|
+
width: 84px;
|
|
773
|
+
height: 84px;
|
|
774
|
+
border-radius: 50%;
|
|
775
|
+
aspect-ratio: 1 / 1;
|
|
776
|
+
object-fit: cover;
|
|
777
|
+
border: 2px solid ${theme.tagBorder};
|
|
778
|
+
background: ${theme.panelBg};
|
|
779
|
+
box-shadow: 0 0 0 3px ${theme.panelBg}, 0 0 0 4px ${theme.eyebrow}33;
|
|
780
|
+
display: block;
|
|
781
|
+
}
|
|
782
|
+
.status-hero__account-avatar-dot {
|
|
783
|
+
position: absolute;
|
|
784
|
+
right: 4px;
|
|
785
|
+
bottom: 4px;
|
|
786
|
+
width: 20px;
|
|
787
|
+
height: 20px;
|
|
788
|
+
border-radius: 999px;
|
|
789
|
+
border: 2px solid ${theme.panelBg};
|
|
790
|
+
box-sizing: border-box;
|
|
791
|
+
}
|
|
792
|
+
.status-hero__account-tags {
|
|
793
|
+
flex: 1 1 auto;
|
|
794
|
+
min-width: 0;
|
|
795
|
+
display: flex;
|
|
796
|
+
flex-wrap: wrap;
|
|
797
|
+
gap: 5px;
|
|
798
|
+
}
|
|
799
|
+
.status-hero__empty {
|
|
800
|
+
font-size: 12px;
|
|
801
|
+
color: ${theme.emptyText};
|
|
802
|
+
text-align: center;
|
|
803
|
+
padding: 8px 0;
|
|
804
|
+
}
|
|
805
|
+
.status-dot {
|
|
806
|
+
display: inline-block;
|
|
807
|
+
width: 8px; height: 8px;
|
|
808
|
+
border-radius: 999px;
|
|
809
|
+
}
|
|
810
|
+
.status-chip {
|
|
811
|
+
display: inline-block;
|
|
812
|
+
padding: 3px 10px;
|
|
813
|
+
border-radius: 999px;
|
|
814
|
+
font-size: 11.5px;
|
|
815
|
+
font-weight: 700;
|
|
816
|
+
font-family: "SF Mono", monospace;
|
|
817
|
+
background: ${theme.tagBg};
|
|
818
|
+
color: ${theme.tagText};
|
|
819
|
+
border: 1px solid ${theme.tagBorder};
|
|
820
|
+
}
|
|
821
|
+
.status-chip--ok {
|
|
822
|
+
background: rgba(16, 185, 129, 0.16);
|
|
823
|
+
color: #10b981;
|
|
824
|
+
border-color: rgba(16, 185, 129, 0.32);
|
|
825
|
+
}
|
|
826
|
+
.status-chip--danger {
|
|
827
|
+
background: rgba(239, 68, 68, 0.16);
|
|
828
|
+
color: #ef4444;
|
|
829
|
+
border-color: rgba(239, 68, 68, 0.32);
|
|
830
|
+
}
|
|
831
|
+
.status-section-title {
|
|
832
|
+
display: flex;
|
|
833
|
+
align-items: center;
|
|
834
|
+
margin: 6px 4px 0;
|
|
835
|
+
}
|
|
836
|
+
.status-section-title span {
|
|
837
|
+
font-size: 12px;
|
|
838
|
+
font-weight: 800;
|
|
839
|
+
letter-spacing: 0.16em;
|
|
840
|
+
text-transform: uppercase;
|
|
841
|
+
color: ${theme.eyebrow};
|
|
842
|
+
}
|
|
843
|
+
.status-empty {
|
|
844
|
+
padding: 18px;
|
|
845
|
+
text-align: center;
|
|
846
|
+
font-size: 12px;
|
|
847
|
+
color: ${theme.emptyText};
|
|
848
|
+
border-radius: 16px;
|
|
849
|
+
background: ${theme.panelBg};
|
|
850
|
+
border: 1px dashed ${theme.panelBorder};
|
|
851
|
+
}
|
|
852
|
+
.status-kv-grid {
|
|
853
|
+
display: grid;
|
|
854
|
+
grid-template-columns: repeat(3, 1fr);
|
|
855
|
+
gap: 8px;
|
|
856
|
+
}
|
|
857
|
+
.status-kv {
|
|
858
|
+
padding: 12px 14px;
|
|
859
|
+
border-radius: 16px;
|
|
860
|
+
background: ${theme.panelBg};
|
|
861
|
+
border: 1px solid ${theme.panelBorder};
|
|
862
|
+
}
|
|
863
|
+
.status-kv__label {
|
|
864
|
+
font-size: 10px;
|
|
865
|
+
font-weight: 700;
|
|
866
|
+
letter-spacing: 0.1em;
|
|
867
|
+
text-transform: uppercase;
|
|
868
|
+
color: ${theme.eyebrow};
|
|
869
|
+
}
|
|
870
|
+
.status-kv__value {
|
|
871
|
+
margin-top: 6px;
|
|
872
|
+
font-family: "SF Mono", monospace;
|
|
873
|
+
font-size: 16px;
|
|
874
|
+
font-weight: 800;
|
|
875
|
+
color: ${theme.panelTitle};
|
|
876
|
+
}
|
|
877
|
+
.status-pie-grid {
|
|
878
|
+
display: grid;
|
|
879
|
+
grid-template-columns: repeat(3, 1fr);
|
|
880
|
+
gap: 10px;
|
|
881
|
+
}
|
|
882
|
+
.status-pie-card {
|
|
883
|
+
display: flex;
|
|
884
|
+
flex-direction: column;
|
|
885
|
+
align-items: center;
|
|
886
|
+
gap: 6px;
|
|
887
|
+
padding: 16px 12px 14px;
|
|
888
|
+
border-radius: 18px;
|
|
889
|
+
background: ${theme.panelBg};
|
|
890
|
+
border: 1px solid ${theme.panelBorder};
|
|
891
|
+
}
|
|
892
|
+
.status-pie { width: 100px; height: 100px; }
|
|
893
|
+
.status-pie-card__label {
|
|
894
|
+
margin-top: 2px;
|
|
895
|
+
font-size: 16px;
|
|
896
|
+
font-weight: 800;
|
|
897
|
+
letter-spacing: 0.08em;
|
|
898
|
+
text-transform: uppercase;
|
|
899
|
+
color: ${theme.panelTitle};
|
|
900
|
+
}
|
|
901
|
+
.status-pie-card__lines {
|
|
902
|
+
width: 100%;
|
|
903
|
+
display: flex;
|
|
904
|
+
flex-direction: column;
|
|
905
|
+
align-items: center;
|
|
906
|
+
gap: 2px;
|
|
907
|
+
margin-top: 2px;
|
|
908
|
+
text-align: center;
|
|
909
|
+
}
|
|
910
|
+
.status-pie-card__line {
|
|
911
|
+
font-size: 10.5px;
|
|
912
|
+
line-height: 1.4;
|
|
913
|
+
color: ${theme.panelDesc};
|
|
914
|
+
font-family: "SF Mono", "JetBrains Mono", "Fira Code", monospace;
|
|
915
|
+
max-width: 100%;
|
|
916
|
+
overflow: hidden;
|
|
917
|
+
text-overflow: ellipsis;
|
|
918
|
+
white-space: nowrap;
|
|
919
|
+
}
|
|
920
|
+
.status-bar {
|
|
921
|
+
height: 6px;
|
|
922
|
+
border-radius: 999px;
|
|
923
|
+
background: ${theme.commandBg};
|
|
924
|
+
overflow: hidden;
|
|
925
|
+
}
|
|
926
|
+
.status-bar__fill { height: 100%; border-radius: 999px; }
|
|
927
|
+
.status-runtime-grid {
|
|
928
|
+
display: grid;
|
|
929
|
+
grid-template-columns: repeat(4, 1fr);
|
|
930
|
+
gap: 8px;
|
|
931
|
+
}
|
|
932
|
+
.status-runtime-block {
|
|
933
|
+
padding: 12px 14px;
|
|
934
|
+
border-radius: 14px;
|
|
935
|
+
background: ${theme.panelBg};
|
|
936
|
+
border: 1px solid ${theme.panelBorder};
|
|
937
|
+
display: flex;
|
|
938
|
+
flex-direction: column;
|
|
939
|
+
gap: 4px;
|
|
940
|
+
}
|
|
941
|
+
.status-runtime-block__label {
|
|
942
|
+
font-size: 10px;
|
|
943
|
+
font-weight: 700;
|
|
944
|
+
letter-spacing: 0.1em;
|
|
945
|
+
text-transform: uppercase;
|
|
946
|
+
color: ${theme.eyebrow};
|
|
947
|
+
}
|
|
948
|
+
.status-runtime-block__value {
|
|
949
|
+
font-family: "SF Mono", monospace;
|
|
950
|
+
font-size: 14px;
|
|
951
|
+
font-weight: 800;
|
|
952
|
+
color: ${theme.panelTitle};
|
|
953
|
+
}
|
|
954
|
+
.status-sys-list {
|
|
955
|
+
display: flex;
|
|
956
|
+
flex-direction: column;
|
|
957
|
+
gap: 6px;
|
|
958
|
+
padding: 12px 14px;
|
|
959
|
+
border-radius: 16px;
|
|
960
|
+
background: ${theme.panelBg};
|
|
961
|
+
border: 1px solid ${theme.panelBorder};
|
|
962
|
+
}
|
|
963
|
+
.status-sys-row {
|
|
964
|
+
display: flex;
|
|
965
|
+
align-items: baseline;
|
|
966
|
+
gap: 12px;
|
|
967
|
+
font-size: 12px;
|
|
968
|
+
line-height: 1.45;
|
|
969
|
+
}
|
|
970
|
+
.status-sys-row__label {
|
|
971
|
+
flex: 0 0 64px;
|
|
972
|
+
font-size: 11px;
|
|
973
|
+
font-weight: 700;
|
|
974
|
+
color: ${theme.eyebrow};
|
|
975
|
+
letter-spacing: 0.04em;
|
|
976
|
+
}
|
|
977
|
+
.status-sys-row__value {
|
|
978
|
+
flex: 1 1 auto;
|
|
979
|
+
min-width: 0;
|
|
980
|
+
font-family: "SF Mono", "JetBrains Mono", "Fira Code", monospace;
|
|
981
|
+
font-size: 12px;
|
|
982
|
+
font-weight: 700;
|
|
983
|
+
color: ${theme.panelTitle};
|
|
984
|
+
word-break: break-all;
|
|
985
|
+
}
|
|
986
|
+
.status-disk-list {
|
|
987
|
+
display: flex;
|
|
988
|
+
flex-direction: column;
|
|
989
|
+
gap: 6px;
|
|
990
|
+
padding: 12px 14px;
|
|
991
|
+
border-radius: 16px;
|
|
992
|
+
background: ${theme.panelBg};
|
|
993
|
+
border: 1px solid ${theme.panelBorder};
|
|
994
|
+
}
|
|
995
|
+
.status-chart-wrap {
|
|
996
|
+
padding: 8px 12px;
|
|
997
|
+
border-radius: 16px;
|
|
998
|
+
background: ${theme.panelBg};
|
|
999
|
+
border: 1px solid ${theme.panelBorder};
|
|
1000
|
+
}
|
|
1001
|
+
.status-chart { display: block; width: 100%; height: 120px; }
|
|
1002
|
+
.status-net-grid {
|
|
1003
|
+
display: grid;
|
|
1004
|
+
grid-template-columns: repeat(4, 1fr);
|
|
1005
|
+
gap: 8px;
|
|
1006
|
+
margin-top: 8px;
|
|
1007
|
+
}
|
|
1008
|
+
.status-net-cell {
|
|
1009
|
+
padding: 10px 12px;
|
|
1010
|
+
border-radius: 14px;
|
|
1011
|
+
background: ${theme.panelBg};
|
|
1012
|
+
border: 1px solid ${theme.panelBorder};
|
|
1013
|
+
}
|
|
1014
|
+
.status-net-cell__label {
|
|
1015
|
+
font-size: 10px;
|
|
1016
|
+
font-weight: 700;
|
|
1017
|
+
color: ${theme.eyebrow};
|
|
1018
|
+
letter-spacing: 0.08em;
|
|
1019
|
+
}
|
|
1020
|
+
.status-net-cell__value {
|
|
1021
|
+
margin-top: 4px;
|
|
1022
|
+
font-family: "SF Mono", monospace;
|
|
1023
|
+
font-size: 13px;
|
|
1024
|
+
font-weight: 800;
|
|
1025
|
+
color: ${theme.panelTitle};
|
|
1026
|
+
}
|
|
1027
|
+
.status-disk-row {
|
|
1028
|
+
display: flex;
|
|
1029
|
+
flex-direction: column;
|
|
1030
|
+
gap: 6px;
|
|
1031
|
+
padding: 8px 0;
|
|
1032
|
+
border-bottom: 1px dashed ${theme.divider};
|
|
1033
|
+
}
|
|
1034
|
+
.status-disk-row:last-child { border-bottom: none; }
|
|
1035
|
+
.status-disk-row__head {
|
|
1036
|
+
display: flex;
|
|
1037
|
+
justify-content: space-between;
|
|
1038
|
+
font-size: 12px;
|
|
1039
|
+
}
|
|
1040
|
+
.status-disk-row__mount {
|
|
1041
|
+
font-family: "SF Mono", monospace;
|
|
1042
|
+
color: ${theme.panelTitle};
|
|
1043
|
+
font-weight: 700;
|
|
1044
|
+
}
|
|
1045
|
+
.status-disk-row__usage {
|
|
1046
|
+
color: ${theme.panelDesc};
|
|
1047
|
+
font-family: "SF Mono", monospace;
|
|
1048
|
+
}
|
|
1049
|
+
.status-rank-block {
|
|
1050
|
+
padding: 12px 14px;
|
|
1051
|
+
border-radius: 16px;
|
|
1052
|
+
background: ${theme.panelBg};
|
|
1053
|
+
border: 1px solid ${theme.panelBorder};
|
|
1054
|
+
margin-top: 8px;
|
|
1055
|
+
}
|
|
1056
|
+
.status-rank-block__title {
|
|
1057
|
+
font-size: 11px;
|
|
1058
|
+
font-weight: 800;
|
|
1059
|
+
letter-spacing: 0.1em;
|
|
1060
|
+
text-transform: uppercase;
|
|
1061
|
+
color: ${theme.eyebrow};
|
|
1062
|
+
margin-bottom: 8px;
|
|
1063
|
+
}
|
|
1064
|
+
.status-rank-row {
|
|
1065
|
+
display: flex;
|
|
1066
|
+
align-items: center;
|
|
1067
|
+
gap: 10px;
|
|
1068
|
+
padding: 5px 0;
|
|
1069
|
+
}
|
|
1070
|
+
.status-rank-row__name {
|
|
1071
|
+
flex: 0 0 130px;
|
|
1072
|
+
font-size: 12px;
|
|
1073
|
+
color: ${theme.panelTitle};
|
|
1074
|
+
white-space: nowrap;
|
|
1075
|
+
overflow: hidden;
|
|
1076
|
+
text-overflow: ellipsis;
|
|
1077
|
+
}
|
|
1078
|
+
.status-rank-row__bar {
|
|
1079
|
+
flex: 1;
|
|
1080
|
+
height: 8px;
|
|
1081
|
+
background: ${theme.commandBg};
|
|
1082
|
+
border-radius: 999px;
|
|
1083
|
+
overflow: hidden;
|
|
1084
|
+
}
|
|
1085
|
+
.status-rank-row__fill { height: 100%; border-radius: 999px; }
|
|
1086
|
+
.status-rank-row__value {
|
|
1087
|
+
flex: 0 0 70px;
|
|
1088
|
+
text-align: right;
|
|
1089
|
+
font-family: "SF Mono", monospace;
|
|
1090
|
+
font-size: 11px;
|
|
1091
|
+
color: ${theme.panelDesc};
|
|
1092
|
+
}
|
|
1093
|
+
.status-footer {
|
|
1094
|
+
display: flex;
|
|
1095
|
+
align-items: stretch;
|
|
1096
|
+
gap: 0;
|
|
1097
|
+
border-radius: 20px;
|
|
1098
|
+
border: 1px solid ${theme.footerBorder};
|
|
1099
|
+
background: ${theme.footerBg};
|
|
1100
|
+
overflow: hidden;
|
|
1101
|
+
}
|
|
1102
|
+
.status-footer__item {
|
|
1103
|
+
flex: 1;
|
|
1104
|
+
display: flex;
|
|
1105
|
+
align-items: center;
|
|
1106
|
+
gap: 12px;
|
|
1107
|
+
padding: 14px 16px;
|
|
1108
|
+
}
|
|
1109
|
+
.status-footer__item + .status-footer__item {
|
|
1110
|
+
border-left: 1px solid ${theme.divider};
|
|
1111
|
+
}
|
|
1112
|
+
.status-footer__icon {
|
|
1113
|
+
width: 36px;
|
|
1114
|
+
height: 36px;
|
|
1115
|
+
flex-shrink: 0;
|
|
1116
|
+
display: grid;
|
|
1117
|
+
place-items: center;
|
|
1118
|
+
border-radius: 12px;
|
|
1119
|
+
background: ${theme.isNightMode ? "rgba(126, 231, 221, 0.08)" : "rgba(15, 118, 110, 0.08)"};
|
|
1120
|
+
color: ${theme.eyebrow};
|
|
1121
|
+
font-size: 18px;
|
|
1122
|
+
}
|
|
1123
|
+
.status-footer__text {
|
|
1124
|
+
display: flex;
|
|
1125
|
+
flex-direction: column;
|
|
1126
|
+
min-width: 0;
|
|
1127
|
+
}
|
|
1128
|
+
.status-footer__label {
|
|
1129
|
+
font-size: 11px;
|
|
1130
|
+
line-height: 1.4;
|
|
1131
|
+
color: ${theme.footerLabel};
|
|
1132
|
+
}
|
|
1133
|
+
.status-footer__value {
|
|
1134
|
+
margin-top: 2px;
|
|
1135
|
+
font-family: "SF Mono", "JetBrains Mono", "Fira Code", monospace;
|
|
1136
|
+
font-size: 12px;
|
|
1137
|
+
line-height: 1.45;
|
|
1138
|
+
font-weight: 700;
|
|
1139
|
+
color: ${theme.footerText};
|
|
1140
|
+
}
|
|
1141
|
+
</style>
|
|
1142
|
+
`;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function renderFooter(
|
|
1146
|
+
snapshot: StatusSnapshot,
|
|
1147
|
+
theme: ReturnType<typeof getHelpTheme>,
|
|
1148
|
+
): string {
|
|
1149
|
+
const fw = snapshot.framework;
|
|
1150
|
+
const runtimeLabel = `${fw.runtime} ${fw.runtimeVersion}`;
|
|
1151
|
+
return `
|
|
1152
|
+
<footer class="status-footer">
|
|
1153
|
+
<div class="status-footer__item">
|
|
1154
|
+
<div class="status-footer__icon">⚡</div>
|
|
1155
|
+
<div class="status-footer__text">
|
|
1156
|
+
<div class="status-footer__label">Framework</div>
|
|
1157
|
+
<div class="status-footer__value">Mioki ${escapeHtml(fw.miokiVersion)}</div>
|
|
1158
|
+
</div>
|
|
1159
|
+
</div>
|
|
1160
|
+
<div class="status-footer__item">
|
|
1161
|
+
<div class="status-footer__icon">🚀</div>
|
|
1162
|
+
<div class="status-footer__text">
|
|
1163
|
+
<div class="status-footer__label">Platform</div>
|
|
1164
|
+
<div class="status-footer__value">Mioku ${escapeHtml(fw.miokuVersion)}</div>
|
|
1165
|
+
</div>
|
|
1166
|
+
</div>
|
|
1167
|
+
<div class="status-footer__item">
|
|
1168
|
+
<div class="status-footer__icon">🥟</div>
|
|
1169
|
+
<div class="status-footer__text">
|
|
1170
|
+
<div class="status-footer__label">Runtime</div>
|
|
1171
|
+
<div class="status-footer__value">${escapeHtml(runtimeLabel)}</div>
|
|
1172
|
+
</div>
|
|
1173
|
+
</div>
|
|
1174
|
+
</footer>
|
|
1175
|
+
`;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
export function renderStatusHtml(snapshot: StatusSnapshot): string {
|
|
1179
|
+
const theme = getHelpTheme(snapshot.isNightMode);
|
|
1180
|
+
const body = [
|
|
1181
|
+
renderResourcesSection(snapshot, theme),
|
|
1182
|
+
renderNetworkSection(snapshot, theme),
|
|
1183
|
+
renderRuntimeSection(snapshot, theme),
|
|
1184
|
+
renderDiskSection(snapshot, theme),
|
|
1185
|
+
renderAISection(snapshot.ai, theme),
|
|
1186
|
+
renderSystemSection(snapshot, theme),
|
|
1187
|
+
].join("\n");
|
|
1188
|
+
return `
|
|
1189
|
+
${buildStyle(theme)}
|
|
1190
|
+
<div class="status-sheet">
|
|
1191
|
+
<div class="status-sheet__scene">
|
|
1192
|
+
<div class="status-sheet__scene-image"></div>
|
|
1193
|
+
<div class="status-sheet__scene-overlay"></div>
|
|
1194
|
+
</div>
|
|
1195
|
+
<div class="status-shell">
|
|
1196
|
+
${renderHero(snapshot, theme)}
|
|
1197
|
+
${body}
|
|
1198
|
+
${renderFooter(snapshot, theme)}
|
|
1199
|
+
</div>
|
|
1200
|
+
</div>
|
|
1201
|
+
`;
|
|
1202
|
+
}
|