mioku-plugin-help 2.1.0 → 2.2.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/package.json +13 -1
- package/status/data-collector.ts +71 -26
- package/status/html-generator.ts +0 -18
- package/status/types.ts +5 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mioku-plugin-help",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "帮助插件,使用截图服务生成美观的帮助图片,并提供 #状态 指令",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -17,6 +17,18 @@
|
|
|
17
17
|
"screenshot",
|
|
18
18
|
"config",
|
|
19
19
|
"ai"
|
|
20
|
+
],
|
|
21
|
+
"accessHooks": [
|
|
22
|
+
{
|
|
23
|
+
"id": "帮助菜单",
|
|
24
|
+
"match": "/(?:[#/]\\s*(?:帮助|菜单|help)|^(?:帮助|菜单|help)(?:\\s|$))/",
|
|
25
|
+
"description": "匹配 #help / 帮助 / 菜单 等帮助指令"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "状态",
|
|
29
|
+
"match": "/(?:[#/]\\s*(?:状态|zt|status)|^(?:状态|zt|status)(?:\\s|$))/",
|
|
30
|
+
"description": "匹配 #状态 / 状态 / zt / status 等系统状态指令"
|
|
31
|
+
}
|
|
20
32
|
]
|
|
21
33
|
},
|
|
22
34
|
"peerDependencies": {
|
package/status/data-collector.ts
CHANGED
|
@@ -67,6 +67,71 @@ function safeNumber(value: unknown, fallback = 0): number {
|
|
|
67
67
|
return Number.isFinite(n) ? n : fallback;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Snapshot of the system's CPU tick counters at a point in time. The
|
|
72
|
+
* sum covers every core, so a 4-core box that's fully loaded registers
|
|
73
|
+
* `total = 4 * elapsed_in_ticks`. `idle` is the slice spent idle.
|
|
74
|
+
*/
|
|
75
|
+
interface CpuTickSample {
|
|
76
|
+
idle: number;
|
|
77
|
+
total: number;
|
|
78
|
+
ts: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function sampleCpuTicks(): CpuTickSample {
|
|
82
|
+
let idle = 0;
|
|
83
|
+
let total = 0;
|
|
84
|
+
for (const cpu of os.cpus()) {
|
|
85
|
+
const t = cpu.times;
|
|
86
|
+
idle += t.idle;
|
|
87
|
+
total += t.user + t.nice + t.sys + t.idle + t.irq;
|
|
88
|
+
}
|
|
89
|
+
return { idle, total, ts: Date.now() };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Baseline sample taken at module load. We seed the cache eagerly so the
|
|
94
|
+
* first `#状态` call (whenever it happens) already has a comparison
|
|
95
|
+
* point — without this, the first call after a long idle period would
|
|
96
|
+
* show 0% even if the system is under load.
|
|
97
|
+
*/
|
|
98
|
+
let prevCpuTicks: CpuTickSample = sampleCpuTicks();
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* If the gap between two samples is larger than this, the previous
|
|
102
|
+
* sample is too stale to produce a meaningful delta (e.g. the user
|
|
103
|
+
* called `#状态` once, then came back an hour later). Discard and
|
|
104
|
+
* start a fresh baseline.
|
|
105
|
+
*/
|
|
106
|
+
const CPU_SAMPLE_MAX_GAP_MS = 30_000;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* System-wide average CPU utilization between the previous sample and
|
|
110
|
+
* now. Returns 0 on the first call after a baseline reset, and a value
|
|
111
|
+
* in [0, 100] on subsequent calls.
|
|
112
|
+
*
|
|
113
|
+
* `os.cpus()` already sums ticks across all cores, so the result is
|
|
114
|
+
* the per-core average — no need to divide by `cpuCores` (the old
|
|
115
|
+
* `process.cpuUsage()` math did that because it summed its own usage
|
|
116
|
+
* across cores; same destination, different source).
|
|
117
|
+
*/
|
|
118
|
+
function computeSystemCpuPercent(): number {
|
|
119
|
+
const current = sampleCpuTicks();
|
|
120
|
+
const gap = current.ts - prevCpuTicks.ts;
|
|
121
|
+
if (gap > CPU_SAMPLE_MAX_GAP_MS || gap <= 0) {
|
|
122
|
+
prevCpuTicks = current;
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
const idleDiff = current.idle - prevCpuTicks.idle;
|
|
126
|
+
const totalDiff = current.total - prevCpuTicks.total;
|
|
127
|
+
prevCpuTicks = current;
|
|
128
|
+
if (totalDiff <= 0) {
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
const percent = 100 - (idleDiff / totalDiff) * 100;
|
|
132
|
+
return Math.max(0, Math.min(100, percent));
|
|
133
|
+
}
|
|
134
|
+
|
|
70
135
|
/** Detect the active JS runtime and its version. */
|
|
71
136
|
function detectRuntime(): { name: string; version: string } {
|
|
72
137
|
if (process.versions.bun) {
|
|
@@ -160,7 +225,6 @@ async function collectBotStatuses(
|
|
|
160
225
|
|
|
161
226
|
// 并行拿 OneBot API 的 status / group / friend / version
|
|
162
227
|
let online = true;
|
|
163
|
-
let onlineDurationMs = 0;
|
|
164
228
|
let groupCount = 0;
|
|
165
229
|
let friendCount = 0;
|
|
166
230
|
let appVersion = "unknown";
|
|
@@ -183,20 +247,7 @@ async function collectBotStatuses(
|
|
|
183
247
|
).catch(() => null),
|
|
184
248
|
]);
|
|
185
249
|
if (statusResult.status === "fulfilled" && statusResult.value) {
|
|
186
|
-
|
|
187
|
-
// napcat-sdk 的 api() 已经把 OneBot v11 响应的 data 字段解包出来,
|
|
188
|
-
const startTs = safeNumber(status?.stat?.start_time);
|
|
189
|
-
if (startTs > 0) {
|
|
190
|
-
// OneBot 约定:start_time 是 unix 秒;> 1e12 视为毫秒
|
|
191
|
-
const startMs = startTs > 1e12 ? startTs : startTs * 1000;
|
|
192
|
-
onlineDurationMs = Math.max(0, Date.now() - startMs);
|
|
193
|
-
}
|
|
194
|
-
if (typeof status?.online === "boolean") {
|
|
195
|
-
online = status.online;
|
|
196
|
-
} else if (typeof status?.good === "boolean") {
|
|
197
|
-
// go-cqhttp / LLOneBot 用 good 表示总体健康
|
|
198
|
-
online = status.good;
|
|
199
|
-
}
|
|
250
|
+
online = statusResult.value.online;
|
|
200
251
|
}
|
|
201
252
|
if (versionResult.status === "fulfilled" && versionResult.value) {
|
|
202
253
|
const v = versionResult.value;
|
|
@@ -232,7 +283,6 @@ async function collectBotStatuses(
|
|
|
232
283
|
online,
|
|
233
284
|
groupCount,
|
|
234
285
|
friendCount,
|
|
235
|
-
onlineDurationMs,
|
|
236
286
|
send: counts.send,
|
|
237
287
|
receive: counts.receive,
|
|
238
288
|
});
|
|
@@ -315,16 +365,11 @@ async function collectResources(): Promise<ResourceStatus> {
|
|
|
315
365
|
speedMhz >= 1000
|
|
316
366
|
? `${(speedMhz / 1000).toFixed(1)} GHz`
|
|
317
367
|
: `${Math.round(speedMhz)} MHz`;
|
|
318
|
-
// CPU%
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
const
|
|
323
|
-
const cpuTotalUsec = cpuUser.user + cpuUser.system;
|
|
324
|
-
const cpuPercent = Math.min(
|
|
325
|
-
100,
|
|
326
|
-
((cpuTotalUsec / 1e6 / Math.max(1, process.uptime())) * 100) / cpuCores,
|
|
327
|
-
);
|
|
368
|
+
// System-wide CPU% from a 2-sample delta. `collectSnapshot` has a 2s
|
|
369
|
+
// TTL, so under normal use we'll have a fresh reading each call. The
|
|
370
|
+
// first call after a baseline reset returns 0 (sentinel) — see
|
|
371
|
+
// `computeSystemCpuPercent` for the gap-discard policy.
|
|
372
|
+
const cpuPercent = computeSystemCpuPercent();
|
|
328
373
|
|
|
329
374
|
// systeminformation.mem() gives buffcache and swap fields that os can't.
|
|
330
375
|
// Wrap in withTimeout so a single slow call doesn't stall the snapshot.
|
package/status/html-generator.ts
CHANGED
|
@@ -71,23 +71,6 @@ function fmtBps(bps: number): string {
|
|
|
71
71
|
return `${fmtBytes(bps)}/s`;
|
|
72
72
|
}
|
|
73
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
74
|
function progressBar(percent: number, color: string): string {
|
|
92
75
|
const clamped = Math.max(0, Math.min(100, percent));
|
|
93
76
|
return `<div class="status-bar"><div class="status-bar__fill" style="width:${clamped}%;background:${color};"></div></div>`;
|
|
@@ -162,7 +145,6 @@ function renderHero(
|
|
|
162
145
|
{ text: statusText, kind: bot.online ? "ok" : "danger" },
|
|
163
146
|
{ text: `好友 ${fmtNumber(bot.friendCount)}`, kind: "data" },
|
|
164
147
|
{ text: `群聊 ${fmtNumber(bot.groupCount)}`, kind: "data" },
|
|
165
|
-
{ text: `运行时长 ${fmtUptime(bot.onlineDurationMs)}`, kind: "data" },
|
|
166
148
|
{ text: `收 ${fmtNumber(bot.receive)}`, kind: "data" },
|
|
167
149
|
{ text: `发 ${fmtNumber(bot.send)}`, kind: "data" },
|
|
168
150
|
{ text: frameworkText, kind: "data" },
|
package/status/types.ts
CHANGED
|
@@ -19,8 +19,6 @@ export interface BotAccountStatus {
|
|
|
19
19
|
online: boolean;
|
|
20
20
|
groupCount: number;
|
|
21
21
|
friendCount: number;
|
|
22
|
-
/** Computed from `bot.api("get_status").stat.start_time`. 0 if unavailable. */
|
|
23
|
-
onlineDurationMs: number;
|
|
24
22
|
send: number;
|
|
25
23
|
receive: number;
|
|
26
24
|
}
|
|
@@ -37,18 +35,13 @@ export interface OneBotVersionInfoData {
|
|
|
37
35
|
|
|
38
36
|
/**
|
|
39
37
|
* OneBot v11 `get_status` payload (already unwrapped by napcat-sdk).
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* - LLOneBot: same shape
|
|
38
|
+
* Per napcat 官方文档: `{ online, good, stat }` —— `stat` 是空对象,
|
|
39
|
+
* 不包含 `start_time` 或任何统计字段。本接口只声明文档承诺的字段。
|
|
43
40
|
*/
|
|
44
41
|
export interface OneBotStatusData {
|
|
45
|
-
online
|
|
46
|
-
good
|
|
47
|
-
stat
|
|
48
|
-
start_time?: number;
|
|
49
|
-
[key: string]: unknown;
|
|
50
|
-
};
|
|
51
|
-
[key: string]: unknown;
|
|
42
|
+
online: boolean;
|
|
43
|
+
good: boolean;
|
|
44
|
+
stat: Record<string, never>;
|
|
52
45
|
}
|
|
53
46
|
|
|
54
47
|
/** Subset of mioku's `AIService.getUsageSummary` payload that we actually render. */
|