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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-plugin-help",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
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,6 @@
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";
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(percent: number, theme: ReturnType<typeof getHelpTheme>): string {
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(percent: number, theme: ReturnType<typeof getHelpTheme>): string {
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(percent: number, theme: ReturnType<typeof getHelpTheme>): string {
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 = 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 `
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
- .join("");
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
- ? [`${r.swapUsedGB} / ${r.swapTotalGB} GB`, `可用 ${Math.max(0, r.swapTotalGB - r.swapUsedGB).toFixed(2)} GB`]
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 = d.interfaceType && d.interfaceType !== "Unknown" ? ` · ${d.interfaceType}` : "";
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
- /** 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 {