mioku-plugin-help 3.0.0 → 3.0.2
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 +68 -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,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
} from "
|
|
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";
|
|
3
|
+
import { displayAdapterId } from "mioku";
|
|
8
4
|
import type {
|
|
9
5
|
AIUsageStatsLite,
|
|
10
6
|
BotAccountStatus,
|
|
@@ -76,7 +72,10 @@ function progressBar(percent: number, color: string): string {
|
|
|
76
72
|
return `<div class="status-bar"><div class="status-bar__fill" style="width:${clamped}%;background:${color};"></div></div>`;
|
|
77
73
|
}
|
|
78
74
|
|
|
79
|
-
function progressColor(
|
|
75
|
+
function progressColor(
|
|
76
|
+
percent: number,
|
|
77
|
+
theme: ReturnType<typeof getHelpTheme>,
|
|
78
|
+
): string {
|
|
80
79
|
if (percent >= 85) {
|
|
81
80
|
return "linear-gradient(90deg, #ef4444, #f97316)";
|
|
82
81
|
}
|
|
@@ -86,7 +85,10 @@ function progressColor(percent: number, theme: ReturnType<typeof getHelpTheme>):
|
|
|
86
85
|
return `linear-gradient(90deg, ${theme.eyebrow}, ${theme.commandTitle})`;
|
|
87
86
|
}
|
|
88
87
|
|
|
89
|
-
function pieColor(
|
|
88
|
+
function pieColor(
|
|
89
|
+
percent: number,
|
|
90
|
+
theme: ReturnType<typeof getHelpTheme>,
|
|
91
|
+
): string {
|
|
90
92
|
if (percent >= 85) {
|
|
91
93
|
return "#ef4444";
|
|
92
94
|
}
|
|
@@ -96,7 +98,10 @@ function pieColor(percent: number, theme: ReturnType<typeof getHelpTheme>): stri
|
|
|
96
98
|
return theme.eyebrow;
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
function renderPieChart(
|
|
101
|
+
function renderPieChart(
|
|
102
|
+
percent: number,
|
|
103
|
+
theme: ReturnType<typeof getHelpTheme>,
|
|
104
|
+
): string {
|
|
100
105
|
const clamped = Math.max(0, Math.min(100, percent));
|
|
101
106
|
const filled = (clamped / 100) * PIE_CIRCUMFERENCE;
|
|
102
107
|
const rest = PIE_CIRCUMFERENCE - filled;
|
|
@@ -128,36 +133,42 @@ function renderHero(
|
|
|
128
133
|
): string {
|
|
129
134
|
const bots = snapshot.bots;
|
|
130
135
|
|
|
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
|
-
|
|
136
|
+
const accountRows =
|
|
137
|
+
bots.length === 0
|
|
138
|
+
? `<div class="status-hero__empty">当前没有在线账号</div>`
|
|
139
|
+
: bots
|
|
140
|
+
.map((bot) => {
|
|
141
|
+
const statusText = bot.online ? "在线" : "离线";
|
|
142
|
+
const statusColor = bot.online ? "#10b981" : "#ef4444";
|
|
143
|
+
const adapterLabel = displayAdapterId(bot.adapter);
|
|
144
|
+
const implText =
|
|
145
|
+
bot.adapter === "stdin"
|
|
146
|
+
? `${adapterLabel}@${bot.appVersion}`
|
|
147
|
+
: bot.appVersion
|
|
148
|
+
? `${bot.framework} ${bot.appVersion}${bot.protocolVersion ? ` · 协议 ${bot.protocolVersion}` : ""}`
|
|
149
|
+
: bot.framework;
|
|
150
|
+
const platformText = bot.platform
|
|
151
|
+
? `${bot.platform}${bot.platformVersion ? ` v${bot.platformVersion}` : ""}`
|
|
152
|
+
: "";
|
|
153
|
+
// Nickname is rendered as a bold large heading above the avatar
|
|
154
|
+
// row (not as a chip). Chips below only carry the metadata.
|
|
155
|
+
const tags = [
|
|
156
|
+
{ text: displayAdapterId(bot.uin) || "—", kind: "data" },
|
|
157
|
+
{ text: statusText, kind: bot.online ? "ok" : "danger" },
|
|
158
|
+
{ text: `好友 ${fmtNumber(bot.friendCount)}`, kind: "data" },
|
|
159
|
+
{ text: `群聊 ${fmtNumber(bot.groupCount)}`, kind: "data" },
|
|
160
|
+
{ text: `收 ${fmtNumber(bot.receive)}`, kind: "data" },
|
|
161
|
+
{ text: `发 ${fmtNumber(bot.send)}`, kind: "data" },
|
|
162
|
+
{ text: implText, kind: "data" },
|
|
163
|
+
...(platformText ? [{ text: platformText, kind: "data" }] : []),
|
|
164
|
+
];
|
|
165
|
+
const tagsHtml = tags
|
|
166
|
+
.map(
|
|
167
|
+
(t) =>
|
|
168
|
+
`<span class="status-chip status-chip--${t.kind}">${escapeHtml(t.text)}</span>`,
|
|
169
|
+
)
|
|
170
|
+
.join("");
|
|
171
|
+
return `
|
|
161
172
|
<div class="status-hero__account">
|
|
162
173
|
<div class="status-hero__account-name">${escapeHtml(bot.nickname)}</div>
|
|
163
174
|
<div class="status-hero__account-body">
|
|
@@ -169,8 +180,8 @@ function renderHero(
|
|
|
169
180
|
</div>
|
|
170
181
|
</div>
|
|
171
182
|
`;
|
|
172
|
-
|
|
173
|
-
|
|
183
|
+
})
|
|
184
|
+
.join("");
|
|
174
185
|
|
|
175
186
|
return `
|
|
176
187
|
<header class="status-hero">
|
|
@@ -190,8 +201,7 @@ function renderResourceCard(
|
|
|
190
201
|
): string {
|
|
191
202
|
const linesHtml = lines
|
|
192
203
|
.map(
|
|
193
|
-
(line) =>
|
|
194
|
-
`<div class="status-pie-card__line">${escapeHtml(line)}</div>`,
|
|
204
|
+
(line) => `<div class="status-pie-card__line">${escapeHtml(line)}</div>`,
|
|
195
205
|
)
|
|
196
206
|
.join("");
|
|
197
207
|
return `
|
|
@@ -231,7 +241,10 @@ function renderResourcesSection(
|
|
|
231
241
|
"SWAP",
|
|
232
242
|
hasSwap ? r.swapPercent : 0,
|
|
233
243
|
hasSwap
|
|
234
|
-
? [
|
|
244
|
+
? [
|
|
245
|
+
`${r.swapUsedGB} / ${r.swapTotalGB} GB`,
|
|
246
|
+
`可用 ${Math.max(0, r.swapTotalGB - r.swapUsedGB).toFixed(2)} GB`,
|
|
247
|
+
]
|
|
235
248
|
: ["未配置", ""],
|
|
236
249
|
theme,
|
|
237
250
|
),
|
|
@@ -299,10 +312,10 @@ function buildSmoothPath(points: Array<{ x: number; y: number }>): string {
|
|
|
299
312
|
const p1 = points[i];
|
|
300
313
|
const p2 = points[i + 1];
|
|
301
314
|
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;
|
|
315
|
+
const cp1x = p1.x + ((p2.x - p0.x) * tension) / 3;
|
|
316
|
+
const cp1y = p1.y + ((p2.y - p0.y) * tension) / 3;
|
|
317
|
+
const cp2x = p2.x - ((p3.x - p1.x) * tension) / 3;
|
|
318
|
+
const cp2y = p2.y - ((p3.y - p1.y) * tension) / 3;
|
|
306
319
|
d += ` C ${cp1x.toFixed(2)} ${cp1y.toFixed(2)}, ${cp2x.toFixed(2)} ${cp2y.toFixed(2)}, ${p2.x.toFixed(2)} ${p2.y.toFixed(2)}`;
|
|
307
320
|
}
|
|
308
321
|
return d;
|
|
@@ -322,10 +335,7 @@ function renderNetworkChart(history: NetworkSample[]): string {
|
|
|
322
335
|
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
336
|
}
|
|
324
337
|
|
|
325
|
-
const maxVal = Math.max(
|
|
326
|
-
1,
|
|
327
|
-
...history.map((s) => Math.max(s.rxBps, s.txBps)),
|
|
328
|
-
);
|
|
338
|
+
const maxVal = Math.max(1, ...history.map((s) => Math.max(s.rxBps, s.txBps)));
|
|
329
339
|
const yMax = maxVal * 1.2;
|
|
330
340
|
const t0 = history[0].ts;
|
|
331
341
|
const t1 = history[history.length - 1].ts;
|
|
@@ -487,9 +497,7 @@ function renderSystemSection(
|
|
|
487
497
|
} else {
|
|
488
498
|
s.memSticks.forEach((m, i) => {
|
|
489
499
|
const size =
|
|
490
|
-
m.sizeGB >= 1
|
|
491
|
-
? `${m.sizeGB} GB`
|
|
492
|
-
: `${(m.sizeGB * 1024).toFixed(0)} MB`;
|
|
500
|
+
m.sizeGB >= 1 ? `${m.sizeGB} GB` : `${(m.sizeGB * 1024).toFixed(0)} MB`;
|
|
493
501
|
const speed = m.speedMTs > 0 ? ` · ${m.speedMTs} MT/s` : "";
|
|
494
502
|
const manu =
|
|
495
503
|
m.manufacturer && m.manufacturer !== "Manufacturer"
|
|
@@ -527,7 +535,10 @@ function renderSystemSection(
|
|
|
527
535
|
: `${d.sizeGB} GB`;
|
|
528
536
|
const vendor = d.vendor && d.vendor !== d.name ? `${d.vendor} ` : "";
|
|
529
537
|
const name = d.name || d.type || "Unknown";
|
|
530
|
-
const iface =
|
|
538
|
+
const iface =
|
|
539
|
+
d.interfaceType && d.interfaceType !== "Unknown"
|
|
540
|
+
? ` · ${d.interfaceType}`
|
|
541
|
+
: "";
|
|
531
542
|
items.push({
|
|
532
543
|
label: `硬盘 ${i + 1}`,
|
|
533
544
|
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 {
|