mioku-plugin-help 3.0.0 → 3.0.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/package.json +1 -1
- package/status/data-collector.ts +50 -73
- package/status/html-generator.ts +66 -57
- package/status/types.ts +12 -31
package/package.json
CHANGED
package/status/data-collector.ts
CHANGED
|
@@ -2,12 +2,18 @@ import * as os from "node:os";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import systemInfo from "systeminformation";
|
|
4
4
|
import {
|
|
5
|
+
buildAdapterReport,
|
|
5
6
|
connectedBots,
|
|
6
7
|
getPluginMetadataList,
|
|
7
8
|
getService,
|
|
8
9
|
Services,
|
|
9
10
|
} from "mioku";
|
|
10
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
AdapterInstanceStatus,
|
|
13
|
+
AIService,
|
|
14
|
+
Bot,
|
|
15
|
+
MiokuContext,
|
|
16
|
+
} from "mioku";
|
|
11
17
|
import { getRenderVersions } from "../utils";
|
|
12
18
|
import { perfMonitor } from "./performance-monitor";
|
|
13
19
|
import { networkSampler } from "./network-sampler";
|
|
@@ -26,8 +32,6 @@ import type {
|
|
|
26
32
|
MemoryStick,
|
|
27
33
|
NetworkStatus,
|
|
28
34
|
NodeRuntimeStatus,
|
|
29
|
-
OneBotStatusData,
|
|
30
|
-
OneBotVersionInfoData,
|
|
31
35
|
ResourceStatus,
|
|
32
36
|
StatusSnapshot,
|
|
33
37
|
SystemInfo,
|
|
@@ -216,82 +220,55 @@ async function collectBotStatuses(
|
|
|
216
220
|
ctx: MiokuContext,
|
|
217
221
|
stdinAvatar?: string,
|
|
218
222
|
): Promise<BotAccountStatus[]> {
|
|
219
|
-
|
|
220
|
-
|
|
223
|
+
// 统一走框架的适配器上报,不再按 OneBot 的 get_version_info / get_status 猜
|
|
224
|
+
const adapters = ctx.adapters.map((adapter) => ({
|
|
225
|
+
name: adapter.name,
|
|
226
|
+
version: adapter.version,
|
|
227
|
+
impl: adapter.impl,
|
|
228
|
+
}));
|
|
229
|
+
const report = await withTimeout(buildAdapterReport({ bots, adapters })).catch(
|
|
230
|
+
() => null,
|
|
231
|
+
);
|
|
232
|
+
const instances = new Map<string, AdapterInstanceStatus>();
|
|
233
|
+
const implOf = new Map<string, { name: string; version?: string } | undefined>();
|
|
234
|
+
for (const entry of report?.adapters ?? []) {
|
|
235
|
+
implOf.set(entry.name, entry.impl);
|
|
236
|
+
for (const instance of entry.instances) {
|
|
237
|
+
instances.set(`${entry.name}:${instance.bot_id}`, instance);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return bots.map((bot) => {
|
|
221
242
|
const uin = String(bot.bot_id);
|
|
222
243
|
const adapter = String(bot.adapter);
|
|
223
244
|
const isStdin = adapter === "stdin";
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
let online = true;
|
|
231
|
-
let groupCount = 0;
|
|
232
|
-
let friendCount = 0;
|
|
233
|
-
let appVersion = "unknown";
|
|
234
|
-
let protocolVersion = "unknown";
|
|
235
|
-
const [statusResult, groupsResult, friendsResult, versionResult] =
|
|
236
|
-
await Promise.allSettled([
|
|
237
|
-
withTimeout(Promise.resolve().then(() => bot.sendApi<OneBotStatusData>("get_status"))).catch(
|
|
238
|
-
() => null,
|
|
239
|
-
),
|
|
240
|
-
withTimeout(Promise.resolve().then(() => bot.getGroupList())).catch(
|
|
241
|
-
() => [],
|
|
242
|
-
),
|
|
243
|
-
withTimeout(Promise.resolve().then(() => bot.getFriendList())).catch(
|
|
244
|
-
() => [],
|
|
245
|
-
),
|
|
246
|
-
withTimeout(
|
|
247
|
-
Promise.resolve().then(() => bot.sendApi<OneBotVersionInfoData>("get_version_info")),
|
|
248
|
-
).catch(() => null),
|
|
249
|
-
]);
|
|
250
|
-
if (statusResult.status === "fulfilled" && statusResult.value) {
|
|
251
|
-
online = Boolean(statusResult.value.online);
|
|
252
|
-
}
|
|
253
|
-
if (isStdin) {
|
|
254
|
-
// 终端适配器没有 OneBot 版本信息,直接用适配器包版本
|
|
255
|
-
appVersion = String(ctx.getAdapter(adapter)?.version ?? "unknown");
|
|
256
|
-
} else if (versionResult.status === "fulfilled" && versionResult.value) {
|
|
257
|
-
const v = versionResult.value;
|
|
258
|
-
framework = String(v.app_name || "").trim() || framework;
|
|
259
|
-
if (v.app_version && v.app_version.trim()) {
|
|
260
|
-
appVersion = v.app_version.trim();
|
|
261
|
-
}
|
|
262
|
-
if (v.protocol_version && v.protocol_version.trim()) {
|
|
263
|
-
protocolVersion = v.protocol_version.trim();
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
if (
|
|
267
|
-
groupsResult.status === "fulfilled" &&
|
|
268
|
-
Array.isArray(groupsResult.value)
|
|
269
|
-
) {
|
|
270
|
-
groupCount = groupsResult.value.length;
|
|
271
|
-
}
|
|
272
|
-
if (
|
|
273
|
-
friendsResult.status === "fulfilled" &&
|
|
274
|
-
Array.isArray(friendsResult.value)
|
|
275
|
-
) {
|
|
276
|
-
friendCount = friendsResult.value.length;
|
|
277
|
-
}
|
|
245
|
+
const instance = instances.get(`${adapter}:${uin}`);
|
|
246
|
+
const lib = implOf.get(adapter);
|
|
247
|
+
// 实现端优先用平台自报的(NapCat / LLOneBot),否则退回适配器的底层库(ICQQ)
|
|
248
|
+
const framework = instance?.impl || lib?.name || adapter;
|
|
249
|
+
const appVersion = instance?.version || lib?.version || "";
|
|
278
250
|
|
|
279
|
-
|
|
251
|
+
return {
|
|
280
252
|
uin,
|
|
281
|
-
nickname,
|
|
282
|
-
avatarUrl
|
|
253
|
+
nickname: String(bot.nickname || "Unknown Bot"),
|
|
254
|
+
avatarUrl: isStdin
|
|
255
|
+
? toImageSrc(stdinAvatar || "")
|
|
256
|
+
: `https://q1.qlogo.cn/g?b=qq&nk=${uin}&s=160`,
|
|
283
257
|
adapter,
|
|
284
|
-
framework,
|
|
285
|
-
appVersion
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
258
|
+
framework: isStdin ? adapter : framework,
|
|
259
|
+
appVersion: isStdin
|
|
260
|
+
? String(ctx.getAdapter(adapter)?.version ?? "")
|
|
261
|
+
: appVersion,
|
|
262
|
+
protocolVersion: instance?.protocol ?? "",
|
|
263
|
+
platform: instance?.platform ?? "",
|
|
264
|
+
platformVersion: instance?.platformVersion ?? "",
|
|
265
|
+
online: bot.online,
|
|
266
|
+
groupCount: instance?.stats.groups ?? 0,
|
|
267
|
+
friendCount: instance?.stats.friends ?? 0,
|
|
268
|
+
send: instance?.stats.sent ?? 0,
|
|
269
|
+
receive: instance?.stats.received ?? 0,
|
|
270
|
+
};
|
|
271
|
+
});
|
|
295
272
|
}
|
|
296
273
|
|
|
297
274
|
async function collectFramework(
|
package/status/html-generator.ts
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
} from "../utils";
|
|
4
|
-
import {
|
|
5
|
-
getHelpTheme,
|
|
6
|
-
HELP_BACKGROUND_IMAGE_URL,
|
|
7
|
-
} from "../theme";
|
|
1
|
+
import { escapeHtml } from "../utils";
|
|
2
|
+
import { getHelpTheme, HELP_BACKGROUND_IMAGE_URL } from "../theme";
|
|
8
3
|
import type {
|
|
9
4
|
AIUsageStatsLite,
|
|
10
5
|
BotAccountStatus,
|
|
@@ -76,7 +71,10 @@ function progressBar(percent: number, color: string): string {
|
|
|
76
71
|
return `<div class="status-bar"><div class="status-bar__fill" style="width:${clamped}%;background:${color};"></div></div>`;
|
|
77
72
|
}
|
|
78
73
|
|
|
79
|
-
function progressColor(
|
|
74
|
+
function progressColor(
|
|
75
|
+
percent: number,
|
|
76
|
+
theme: ReturnType<typeof getHelpTheme>,
|
|
77
|
+
): string {
|
|
80
78
|
if (percent >= 85) {
|
|
81
79
|
return "linear-gradient(90deg, #ef4444, #f97316)";
|
|
82
80
|
}
|
|
@@ -86,7 +84,10 @@ function progressColor(percent: number, theme: ReturnType<typeof getHelpTheme>):
|
|
|
86
84
|
return `linear-gradient(90deg, ${theme.eyebrow}, ${theme.commandTitle})`;
|
|
87
85
|
}
|
|
88
86
|
|
|
89
|
-
function pieColor(
|
|
87
|
+
function pieColor(
|
|
88
|
+
percent: number,
|
|
89
|
+
theme: ReturnType<typeof getHelpTheme>,
|
|
90
|
+
): string {
|
|
90
91
|
if (percent >= 85) {
|
|
91
92
|
return "#ef4444";
|
|
92
93
|
}
|
|
@@ -96,7 +97,10 @@ function pieColor(percent: number, theme: ReturnType<typeof getHelpTheme>): stri
|
|
|
96
97
|
return theme.eyebrow;
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
function renderPieChart(
|
|
100
|
+
function renderPieChart(
|
|
101
|
+
percent: number,
|
|
102
|
+
theme: ReturnType<typeof getHelpTheme>,
|
|
103
|
+
): string {
|
|
100
104
|
const clamped = Math.max(0, Math.min(100, percent));
|
|
101
105
|
const filled = (clamped / 100) * PIE_CIRCUMFERENCE;
|
|
102
106
|
const rest = PIE_CIRCUMFERENCE - filled;
|
|
@@ -128,36 +132,41 @@ function renderHero(
|
|
|
128
132
|
): string {
|
|
129
133
|
const bots = snapshot.bots;
|
|
130
134
|
|
|
131
|
-
const accountRows =
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
135
|
+
const accountRows =
|
|
136
|
+
bots.length === 0
|
|
137
|
+
? `<div class="status-hero__empty">当前没有在线账号</div>`
|
|
138
|
+
: bots
|
|
139
|
+
.map((bot) => {
|
|
140
|
+
const statusText = bot.online ? "在线" : "离线";
|
|
141
|
+
const statusColor = bot.online ? "#10b981" : "#ef4444";
|
|
142
|
+
const implText =
|
|
143
|
+
bot.adapter === "stdin"
|
|
144
|
+
? `${bot.adapter}@${bot.appVersion}`
|
|
145
|
+
: bot.appVersion
|
|
146
|
+
? `${bot.framework} ${bot.appVersion}${bot.protocolVersion ? ` · 协议 ${bot.protocolVersion}` : ""}`
|
|
147
|
+
: bot.framework;
|
|
148
|
+
const platformText = bot.platform
|
|
149
|
+
? `${bot.platform}${bot.platformVersion ? ` v${bot.platformVersion}` : ""}`
|
|
150
|
+
: "";
|
|
151
|
+
// Nickname is rendered as a bold large heading above the avatar
|
|
152
|
+
// row (not as a chip). Chips below only carry the metadata.
|
|
153
|
+
const tags = [
|
|
154
|
+
{ text: String(bot.uin || "—"), kind: "data" },
|
|
155
|
+
{ text: statusText, kind: bot.online ? "ok" : "danger" },
|
|
156
|
+
{ text: `好友 ${fmtNumber(bot.friendCount)}`, kind: "data" },
|
|
157
|
+
{ text: `群聊 ${fmtNumber(bot.groupCount)}`, kind: "data" },
|
|
158
|
+
{ text: `收 ${fmtNumber(bot.receive)}`, kind: "data" },
|
|
159
|
+
{ text: `发 ${fmtNumber(bot.send)}`, kind: "data" },
|
|
160
|
+
{ text: implText, kind: "data" },
|
|
161
|
+
...(platformText ? [{ text: platformText, kind: "data" }] : []),
|
|
162
|
+
];
|
|
163
|
+
const tagsHtml = tags
|
|
164
|
+
.map(
|
|
165
|
+
(t) =>
|
|
166
|
+
`<span class="status-chip status-chip--${t.kind}">${escapeHtml(t.text)}</span>`,
|
|
167
|
+
)
|
|
168
|
+
.join("");
|
|
169
|
+
return `
|
|
161
170
|
<div class="status-hero__account">
|
|
162
171
|
<div class="status-hero__account-name">${escapeHtml(bot.nickname)}</div>
|
|
163
172
|
<div class="status-hero__account-body">
|
|
@@ -169,8 +178,8 @@ function renderHero(
|
|
|
169
178
|
</div>
|
|
170
179
|
</div>
|
|
171
180
|
`;
|
|
172
|
-
|
|
173
|
-
|
|
181
|
+
})
|
|
182
|
+
.join("");
|
|
174
183
|
|
|
175
184
|
return `
|
|
176
185
|
<header class="status-hero">
|
|
@@ -190,8 +199,7 @@ function renderResourceCard(
|
|
|
190
199
|
): string {
|
|
191
200
|
const linesHtml = lines
|
|
192
201
|
.map(
|
|
193
|
-
(line) =>
|
|
194
|
-
`<div class="status-pie-card__line">${escapeHtml(line)}</div>`,
|
|
202
|
+
(line) => `<div class="status-pie-card__line">${escapeHtml(line)}</div>`,
|
|
195
203
|
)
|
|
196
204
|
.join("");
|
|
197
205
|
return `
|
|
@@ -231,7 +239,10 @@ function renderResourcesSection(
|
|
|
231
239
|
"SWAP",
|
|
232
240
|
hasSwap ? r.swapPercent : 0,
|
|
233
241
|
hasSwap
|
|
234
|
-
? [
|
|
242
|
+
? [
|
|
243
|
+
`${r.swapUsedGB} / ${r.swapTotalGB} GB`,
|
|
244
|
+
`可用 ${Math.max(0, r.swapTotalGB - r.swapUsedGB).toFixed(2)} GB`,
|
|
245
|
+
]
|
|
235
246
|
: ["未配置", ""],
|
|
236
247
|
theme,
|
|
237
248
|
),
|
|
@@ -299,10 +310,10 @@ function buildSmoothPath(points: Array<{ x: number; y: number }>): string {
|
|
|
299
310
|
const p1 = points[i];
|
|
300
311
|
const p2 = points[i + 1];
|
|
301
312
|
const p3 = points[Math.min(points.length - 1, i + 2)];
|
|
302
|
-
const cp1x = p1.x + (p2.x - p0.x) * tension / 3;
|
|
303
|
-
const cp1y = p1.y + (p2.y - p0.y) * tension / 3;
|
|
304
|
-
const cp2x = p2.x - (p3.x - p1.x) * tension / 3;
|
|
305
|
-
const cp2y = p2.y - (p3.y - p1.y) * tension / 3;
|
|
313
|
+
const cp1x = p1.x + ((p2.x - p0.x) * tension) / 3;
|
|
314
|
+
const cp1y = p1.y + ((p2.y - p0.y) * tension) / 3;
|
|
315
|
+
const cp2x = p2.x - ((p3.x - p1.x) * tension) / 3;
|
|
316
|
+
const cp2y = p2.y - ((p3.y - p1.y) * tension) / 3;
|
|
306
317
|
d += ` C ${cp1x.toFixed(2)} ${cp1y.toFixed(2)}, ${cp2x.toFixed(2)} ${cp2y.toFixed(2)}, ${p2.x.toFixed(2)} ${p2.y.toFixed(2)}`;
|
|
307
318
|
}
|
|
308
319
|
return d;
|
|
@@ -322,10 +333,7 @@ function renderNetworkChart(history: NetworkSample[]): string {
|
|
|
322
333
|
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>`;
|
|
323
334
|
}
|
|
324
335
|
|
|
325
|
-
const maxVal = Math.max(
|
|
326
|
-
1,
|
|
327
|
-
...history.map((s) => Math.max(s.rxBps, s.txBps)),
|
|
328
|
-
);
|
|
336
|
+
const maxVal = Math.max(1, ...history.map((s) => Math.max(s.rxBps, s.txBps)));
|
|
329
337
|
const yMax = maxVal * 1.2;
|
|
330
338
|
const t0 = history[0].ts;
|
|
331
339
|
const t1 = history[history.length - 1].ts;
|
|
@@ -487,9 +495,7 @@ function renderSystemSection(
|
|
|
487
495
|
} else {
|
|
488
496
|
s.memSticks.forEach((m, i) => {
|
|
489
497
|
const size =
|
|
490
|
-
m.sizeGB >= 1
|
|
491
|
-
? `${m.sizeGB} GB`
|
|
492
|
-
: `${(m.sizeGB * 1024).toFixed(0)} MB`;
|
|
498
|
+
m.sizeGB >= 1 ? `${m.sizeGB} GB` : `${(m.sizeGB * 1024).toFixed(0)} MB`;
|
|
493
499
|
const speed = m.speedMTs > 0 ? ` · ${m.speedMTs} MT/s` : "";
|
|
494
500
|
const manu =
|
|
495
501
|
m.manufacturer && m.manufacturer !== "Manufacturer"
|
|
@@ -527,7 +533,10 @@ function renderSystemSection(
|
|
|
527
533
|
: `${d.sizeGB} GB`;
|
|
528
534
|
const vendor = d.vendor && d.vendor !== d.name ? `${d.vendor} ` : "";
|
|
529
535
|
const name = d.name || d.type || "Unknown";
|
|
530
|
-
const iface =
|
|
536
|
+
const iface =
|
|
537
|
+
d.interfaceType && d.interfaceType !== "Unknown"
|
|
538
|
+
? ` · ${d.interfaceType}`
|
|
539
|
+
: "";
|
|
531
540
|
items.push({
|
|
532
541
|
label: `硬盘 ${i + 1}`,
|
|
533
542
|
value: `${vendor}${name} · ${sizeLabel}${iface}`.trim(),
|
package/status/types.ts
CHANGED
|
@@ -13,12 +13,16 @@ export interface BotAccountStatus {
|
|
|
13
13
|
avatarUrl: string;
|
|
14
14
|
/** 适配器名,如 "stdin" / "onebotv11" / "icqq"。 */
|
|
15
15
|
adapter: string;
|
|
16
|
-
/**
|
|
16
|
+
/** 实现端名称,如 "NapCat" / "LLOneBot" / "ICQQ",取不到时为空串 */
|
|
17
17
|
framework: string;
|
|
18
|
-
/**
|
|
18
|
+
/** 实现端版本,如 "5.0.6"*/
|
|
19
19
|
appVersion: string;
|
|
20
|
-
/** OneBot
|
|
20
|
+
/** 协议版本,如 OneBot 的 "v11",没有则为空串 */
|
|
21
21
|
protocolVersion: string;
|
|
22
|
+
/** 登录设备,如 icqq 的 "aPad",没有则为空串 */
|
|
23
|
+
platform: string;
|
|
24
|
+
/** 登录设备对应的客户端版本,如 "9.3.50.40225",没有则为空串 */
|
|
25
|
+
platformVersion: string;
|
|
22
26
|
online: boolean;
|
|
23
27
|
groupCount: number;
|
|
24
28
|
friendCount: number;
|
|
@@ -26,27 +30,6 @@ export interface BotAccountStatus {
|
|
|
26
30
|
receive: number;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
/**
|
|
30
|
-
* OneBot v11 `get_version_info` payload (already unwrapped from the
|
|
31
|
-
* `{ status, retcode, data, ... }` envelope by the adapter's `bot.sendApi`).
|
|
32
|
-
*/
|
|
33
|
-
export interface OneBotVersionInfoData {
|
|
34
|
-
app_name: string;
|
|
35
|
-
protocol_version: string;
|
|
36
|
-
app_version: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* OneBot v11 `get_status` payload (already unwrapped by napcat-sdk).
|
|
41
|
-
* Per napcat 官方文档: `{ online, good, stat }` —— `stat` 是空对象,
|
|
42
|
-
* 不包含 `start_time` 或任何统计字段。本接口只声明文档承诺的字段。
|
|
43
|
-
*/
|
|
44
|
-
export interface OneBotStatusData {
|
|
45
|
-
online: boolean;
|
|
46
|
-
good: boolean;
|
|
47
|
-
stat: Record<string, never>;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
33
|
/** Subset of mioku's `AIService.getUsageSummary` payload that we actually render. */
|
|
51
34
|
export interface AIUsageSummary {
|
|
52
35
|
totals?: {
|
|
@@ -126,13 +109,11 @@ export interface NodeRuntimeStatus {
|
|
|
126
109
|
arrayBuffersMB: number;
|
|
127
110
|
eventLoopDelayMs: { mean: number; p99: number };
|
|
128
111
|
/** null when `--expose-gc` is not enabled. */
|
|
129
|
-
gc:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
| null;
|
|
112
|
+
gc: {
|
|
113
|
+
available: boolean;
|
|
114
|
+
count: number;
|
|
115
|
+
lastDurationMs?: number;
|
|
116
|
+
} | null;
|
|
136
117
|
}
|
|
137
118
|
|
|
138
119
|
export interface NetworkSample {
|