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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-plugin-help",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "帮助插件,使用截图服务生成美观的帮助图片,并提供 #状态 指令",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -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 { AIService, Bot, MiokuContext } from "mioku";
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
- const results: BotAccountStatus[] = [];
220
- for (const bot of bots) {
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 nickname = String(bot.nickname || "Unknown Bot");
225
- let framework = isStdin ? adapter : "unknown";
226
- const avatarUrl = isStdin
227
- ? toImageSrc(stdinAvatar || "")
228
- : `https://q1.qlogo.cn/g?b=qq&nk=${uin}&s=160`;
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
- results.push({
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
- protocolVersion,
287
- online,
288
- groupCount,
289
- friendCount,
290
- send: 0,
291
- receive: 0,
292
- });
293
- }
294
- return results;
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(
@@ -1,10 +1,5 @@
1
- import {
2
- escapeHtml,
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(percent: number, theme: ReturnType<typeof getHelpTheme>): string {
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(percent: number, theme: ReturnType<typeof getHelpTheme>): string {
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(percent: number, theme: ReturnType<typeof getHelpTheme>): string {
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 = bots.length === 0
132
- ? `<div class="status-hero__empty">当前没有在线账号</div>`
133
- : bots
134
- .map((bot) => {
135
- const statusText = bot.online ? "在线" : "离线";
136
- const statusColor = bot.online ? "#10b981" : "#ef4444";
137
- // stdin 等本地适配器没有 OneBot 版本信息,统一显示为 `<适配器>@<包版本>`
138
- const frameworkText =
139
- bot.adapter === "stdin"
140
- ? `${bot.adapter}@${bot.appVersion}`
141
- : bot.appVersion && bot.appVersion !== "unknown"
142
- ? `${bot.framework} ${bot.appVersion} · 协议 ${bot.protocolVersion}`
143
- : bot.framework;
144
- // Nickname is rendered as a bold large heading above the avatar
145
- // row (not as a chip). Chips below only carry the metadata.
146
- const tags = [
147
- { text: String(bot.uin || "—"), kind: "data" },
148
- { text: statusText, kind: bot.online ? "ok" : "danger" },
149
- { text: `好友 ${fmtNumber(bot.friendCount)}`, kind: "data" },
150
- { text: `群聊 ${fmtNumber(bot.groupCount)}`, kind: "data" },
151
- { text: `收 ${fmtNumber(bot.receive)}`, kind: "data" },
152
- { text: `发 ${fmtNumber(bot.send)}`, kind: "data" },
153
- { text: frameworkText, kind: "data" },
154
- ];
155
- const tagsHtml = tags
156
- .map(
157
- (t) => `<span class="status-chip status-chip--${t.kind}">${escapeHtml(t.text)}</span>`,
158
- )
159
- .join("");
160
- return `
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
- .join("");
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
- ? [`${r.swapUsedGB} / ${r.swapTotalGB} GB`, `可用 ${Math.max(0, r.swapTotalGB - r.swapUsedGB).toFixed(2)} GB`]
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 = d.interfaceType && d.interfaceType !== "Unknown" ? ` · ${d.interfaceType}` : "";
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
- /** Underlying bot framework identifier, e.g. "QQBot" / "NapCat" / "LLOneBot". */
16
+ /** 实现端名称,如 "NapCat" / "LLOneBot" / "ICQQ",取不到时为空串 */
17
17
  framework: string;
18
- /** Adapter app version, e.g. "5.0.6" — from OneBot `get_version_info.app_version`. */
18
+ /** 实现端版本,如 "5.0.6"*/
19
19
  appVersion: string;
20
- /** OneBot protocol version, e.g. "v11" — from OneBot `get_version_info.protocol_version`. */
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
- available: boolean;
132
- count: number;
133
- lastDurationMs?: number;
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 {