mioku-plugin-help 2.3.3 → 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/config.md +18 -0
- package/help/html-generator.ts +1 -1
- package/help/image.ts +7 -17
- package/help/intent.ts +1 -1
- package/help/role.ts +3 -8
- package/index.ts +8 -2
- package/package.json +6 -5
- package/status/config.ts +17 -0
- package/status/data-collector.ts +106 -118
- package/status/html-generator.ts +66 -54
- package/status/image.ts +2 -2
- package/status/intent.ts +1 -1
- package/status/network-sampler.ts +1 -1
- package/status/types.ts +16 -32
- package/utils.ts +3 -3
package/config.md
CHANGED
|
@@ -8,6 +8,12 @@ fields:
|
|
|
8
8
|
description: 最基础的单行文本输入。
|
|
9
9
|
placeholder: 输入任意文本
|
|
10
10
|
|
|
11
|
+
- key: status.stdinAvatar
|
|
12
|
+
label: stdin 头像
|
|
13
|
+
type: text
|
|
14
|
+
description: stdin 适配器在状态图中使用的头像,支持 http(s):// 或 file:// URL,也支持本地绝对路径(自动转 file://)。默认使用 uapis.cn 随机表情接口。
|
|
15
|
+
placeholder: https://uapis.cn/api/v1/random/image?category=bq&type=eciyuan
|
|
16
|
+
|
|
11
17
|
- key: demo.secretValue
|
|
12
18
|
label: secret 密文输入
|
|
13
19
|
type: secret
|
|
@@ -192,4 +198,16 @@ config: demo
|
|
|
192
198
|
|
|
193
199
|
用法:`mioku-file` 会直接把某个配置文件完整显示为 JSON 编辑器,适合调试或保底兜底。
|
|
194
200
|
|
|
201
|
+
## 7. `status` 状态图配置
|
|
202
|
+
|
|
203
|
+
```mioku-field
|
|
204
|
+
key: status.stdinAvatar
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`status` 配置文件(`config/help/status.json`)目前支持:
|
|
208
|
+
|
|
209
|
+
| 字段 | 类型 | 默认值 | 说明 |
|
|
210
|
+
| --- | --- | --- | --- |
|
|
211
|
+
| `stdinAvatar` | string | `https://uapis.cn/api/v1/random/image?category=bq&type=eciyuan` | stdin 适配器在状态图中的头像,支持 http(s):// / file:// URL 或本地绝对路径(自动转 file://) |
|
|
212
|
+
|
|
195
213
|
---
|
package/help/html-generator.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { CommandRole, PluginHelp } from "mioku";
|
|
15
|
-
import { botConfig } from "
|
|
15
|
+
import { botConfig } from "mioku";
|
|
16
16
|
import { escapeHtml } from "../utils";
|
|
17
17
|
import { getHelpTheme, HELP_BACKGROUND_IMAGE_URL } from "../theme";
|
|
18
18
|
import { getRenderableEntries } from "./intent";
|
package/help/image.ts
CHANGED
|
@@ -115,11 +115,9 @@ export function resolveHelpBotProfile(
|
|
|
115
115
|
const fallbackNickname = "Mioku Bot";
|
|
116
116
|
const selfId = event?.self_id;
|
|
117
117
|
const bot =
|
|
118
|
-
|
|
119
|
-
? ctx.pickBot(selfId)
|
|
120
|
-
: null) ||
|
|
118
|
+
event?.bot ??
|
|
121
119
|
(ctx?.bots instanceof Map ? Array.from(ctx.bots.values())[0] : null);
|
|
122
|
-
const botId = selfId || bot?.
|
|
120
|
+
const botId = selfId || bot?.bot_id;
|
|
123
121
|
const botNickname = bot?.nickname || bot?.name || fallbackNickname;
|
|
124
122
|
const botAvatarUrl = botId
|
|
125
123
|
? `https://q1.qlogo.cn/g?b=qq&nk=${botId}&s=640`
|
|
@@ -160,8 +158,8 @@ export async function replyWithImage(
|
|
|
160
158
|
}
|
|
161
159
|
|
|
162
160
|
/**
|
|
163
|
-
* Send an image from inside an AI skill handler.
|
|
164
|
-
* `
|
|
161
|
+
* Send an image from inside an AI skill handler. Uses the event-bound bot
|
|
162
|
+
* (`event.bot`), supports an optional quote-reply, and falls
|
|
165
163
|
* back to base64 when the adapter refuses a raw path.
|
|
166
164
|
*/
|
|
167
165
|
export async function sendImageFromSkillContext(options: {
|
|
@@ -171,28 +169,20 @@ export async function sendImageFromSkillContext(options: {
|
|
|
171
169
|
quoteReply?: boolean;
|
|
172
170
|
}): Promise<void> {
|
|
173
171
|
const { ctx, event, imagePath, quoteReply = false } = options;
|
|
174
|
-
const
|
|
175
|
-
const bot =
|
|
176
|
-
selfId != null && typeof ctx?.pickBot === "function"
|
|
177
|
-
? ctx.pickBot(selfId)
|
|
178
|
-
: undefined;
|
|
172
|
+
const bot = event?.bot;
|
|
179
173
|
|
|
180
174
|
if (!bot) {
|
|
181
175
|
throw new Error("当前上下文不支持发送图片");
|
|
182
176
|
}
|
|
183
177
|
|
|
184
178
|
const buildImageSegment = (file: string) => {
|
|
185
|
-
|
|
186
|
-
if (ctx?.segment?.image) {
|
|
187
|
-
return ctx.segment.image(normalizedFile);
|
|
188
|
-
}
|
|
189
|
-
return { type: "image", file: normalizedFile };
|
|
179
|
+
return ctx.segment.image(normalizeImageSource(file));
|
|
190
180
|
};
|
|
191
181
|
|
|
192
182
|
const sendPayload = async (file: string) => {
|
|
193
183
|
const payload: any[] = [];
|
|
194
184
|
if (quoteReply && event?.message_id != null) {
|
|
195
|
-
payload.push(
|
|
185
|
+
payload.push(ctx.segment.reply(event.message_id));
|
|
196
186
|
}
|
|
197
187
|
payload.push(buildImageSegment(file));
|
|
198
188
|
|
package/help/intent.ts
CHANGED
|
@@ -24,7 +24,7 @@ function sanitizeKeyword(value: string): string {
|
|
|
24
24
|
return String(value || "")
|
|
25
25
|
.trim()
|
|
26
26
|
.replace(/^[#/\s]+/, "")
|
|
27
|
-
.replace(/[
|
|
27
|
+
.replace(/[。.!!??,,::;);]+$/g, "");
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/** Lowercase + strip everything that isn't a-z, 0-9, or CJK. */
|
package/help/role.ts
CHANGED
|
@@ -66,15 +66,10 @@ export async function resolveViewerRole(
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
if (event?.group_id != null && event?.user_id != null) {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
const bot =
|
|
72
|
-
selfId != null && typeof ctx?.pickBot === "function"
|
|
73
|
-
? ctx.pickBot(selfId)
|
|
74
|
-
: undefined;
|
|
75
|
-
if (bot && typeof bot.getGroupMemberInfo === "function") {
|
|
69
|
+
const bot = event?.bot;
|
|
70
|
+
if (bot) {
|
|
76
71
|
try {
|
|
77
|
-
const info = await bot.
|
|
72
|
+
const info = await bot.getMemberInfo(
|
|
78
73
|
event.group_id,
|
|
79
74
|
event.user_id,
|
|
80
75
|
);
|
package/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { definePlugin, type
|
|
1
|
+
import { definePlugin, type MiokuContext } from "mioku";
|
|
2
2
|
import { getService, Services } from "mioku";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import { HELP_DEMO_CONFIG } from "./demo-config";
|
|
5
|
+
import { HELP_STATUS_DEFAULT_CONFIG } from "./status/config";
|
|
5
6
|
import {
|
|
6
7
|
generateHelpImage,
|
|
7
8
|
replyWithImage,
|
|
@@ -23,7 +24,7 @@ const helpPlugin = definePlugin({
|
|
|
23
24
|
version: "2.1.0",
|
|
24
25
|
description: "帮助插件,生成帮助图片,并提供 #状态 指令",
|
|
25
26
|
|
|
26
|
-
async setup(ctx:
|
|
27
|
+
async setup(ctx: MiokuContext) {
|
|
27
28
|
// 启动后台采样器
|
|
28
29
|
networkSampler.start();
|
|
29
30
|
perfMonitor.start();
|
|
@@ -48,6 +49,11 @@ const helpPlugin = definePlugin({
|
|
|
48
49
|
|
|
49
50
|
if (configService) {
|
|
50
51
|
await configService.registerConfig("help", "demo", HELP_DEMO_CONFIG.demo);
|
|
52
|
+
await configService.registerConfig(
|
|
53
|
+
"help",
|
|
54
|
+
"status",
|
|
55
|
+
HELP_STATUS_DEFAULT_CONFIG,
|
|
56
|
+
);
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
// 注册 help manifest,让 `#help 状态` 能命中
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mioku-plugin-help",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "帮助插件,使用截图服务生成美观的帮助图片,并提供 #状态 指令",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -32,11 +32,12 @@
|
|
|
32
32
|
]
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"mioku": "^0.
|
|
36
|
-
|
|
35
|
+
"mioku": "^1.0.0"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"systeminformation": "^5.31.6"
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|
|
39
|
-
"mioku": "workspace:*"
|
|
40
|
-
"mioki": "^0.16.0"
|
|
41
|
+
"mioku": "workspace:*"
|
|
41
42
|
}
|
|
42
43
|
}
|
package/status/config.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* help 插件状态图配置(config-service 注册名为 "help/status")。
|
|
3
|
+
* 配置文件位于 `<项目>/config/help/status.json`,可热重载。
|
|
4
|
+
*/
|
|
5
|
+
export interface HelpStatusConfig {
|
|
6
|
+
/**
|
|
7
|
+
* stdin 适配器在状态图中使用的头像。
|
|
8
|
+
* 支持 http(s):// 或 file:// URL,也支持本地绝对路径(自动转换为 file://)。
|
|
9
|
+
* 默认使用 uapis.cn 的随机表情接口。
|
|
10
|
+
*/
|
|
11
|
+
stdinAvatar?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const HELP_STATUS_DEFAULT_CONFIG: HelpStatusConfig = {
|
|
15
|
+
// 默认使用 uapis.cn 的二次元表情随机接口(每次刷新头像都不同)
|
|
16
|
+
stdinAvatar: "https://uapis.cn/api/v1/random/image?category=bq&type=eciyuan",
|
|
17
|
+
};
|
package/status/data-collector.ts
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import * as os from "node:os";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import systemInfo from "systeminformation";
|
|
3
4
|
import {
|
|
5
|
+
buildAdapterReport,
|
|
4
6
|
connectedBots,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
getPluginMetadataList,
|
|
8
|
+
getService,
|
|
9
|
+
Services,
|
|
10
|
+
} 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";
|
|
20
|
+
import type { HelpStatusConfig } from "./config";
|
|
14
21
|
import type {
|
|
15
22
|
AIUsageStatsLite,
|
|
16
23
|
AIUsageSummary,
|
|
@@ -25,8 +32,6 @@ import type {
|
|
|
25
32
|
MemoryStick,
|
|
26
33
|
NetworkStatus,
|
|
27
34
|
NodeRuntimeStatus,
|
|
28
|
-
OneBotStatusData,
|
|
29
|
-
OneBotVersionInfoData,
|
|
30
35
|
ResourceStatus,
|
|
31
36
|
StatusSnapshot,
|
|
32
37
|
SystemInfo,
|
|
@@ -185,113 +190,89 @@ function formatDuration(ms: number): string {
|
|
|
185
190
|
return `${minutes}分${String(seconds).padStart(2, "0")}秒`;
|
|
186
191
|
}
|
|
187
192
|
|
|
188
|
-
async function collectBots(
|
|
193
|
+
async function collectBots(
|
|
194
|
+
ctx: MiokuContext,
|
|
195
|
+
stdinAvatar?: string,
|
|
196
|
+
): Promise<BotAccountStatus[]> {
|
|
189
197
|
const bots = Array.from(connectedBots.values());
|
|
190
198
|
if (bots.length === 0) {
|
|
191
199
|
return [];
|
|
192
200
|
}
|
|
193
|
-
|
|
201
|
+
// stdin 适配器恒排第一位(终端是本机主人通道,优先展示)
|
|
202
|
+
const sorted = [...bots].sort(
|
|
203
|
+
(a, b) =>
|
|
204
|
+
Number(b.adapter === "stdin") - Number(a.adapter === "stdin"),
|
|
205
|
+
);
|
|
206
|
+
return collectBotStatuses(sorted, ctx, stdinAvatar);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** 把本地绝对路径 / http(s) / file:// 统一成可被截图服务加载的图片地址 */
|
|
210
|
+
function toImageSrc(value: string): string {
|
|
211
|
+
if (/^(https?:|file:|data:)/i.test(value)) return value;
|
|
212
|
+
if (value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value)) {
|
|
213
|
+
return `file://${value}`;
|
|
214
|
+
}
|
|
215
|
+
return value;
|
|
194
216
|
}
|
|
195
217
|
|
|
196
218
|
async function collectBotStatuses(
|
|
197
|
-
bots:
|
|
219
|
+
bots: readonly Bot[],
|
|
220
|
+
ctx: MiokuContext,
|
|
221
|
+
stdinAvatar?: string,
|
|
198
222
|
): Promise<BotAccountStatus[]> {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
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);
|
|
214
238
|
}
|
|
215
|
-
} catch {
|
|
216
|
-
// ignore
|
|
217
239
|
}
|
|
218
240
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
-
const
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
let groupCount = 0;
|
|
229
|
-
let friendCount = 0;
|
|
230
|
-
let appVersion = "unknown";
|
|
231
|
-
let protocolVersion = "unknown";
|
|
232
|
-
const [statusResult, groupsResult, friendsResult, versionResult] =
|
|
233
|
-
await Promise.allSettled([
|
|
234
|
-
withTimeout(
|
|
235
|
-
Promise.resolve().then(() => bot.api<OneBotStatusData>("get_status")),
|
|
236
|
-
).catch(() => null),
|
|
237
|
-
withTimeout(Promise.resolve().then(() => bot.getGroupList())).catch(
|
|
238
|
-
() => [],
|
|
239
|
-
),
|
|
240
|
-
withTimeout(Promise.resolve().then(() => bot.getFriendList())).catch(
|
|
241
|
-
() => [],
|
|
242
|
-
),
|
|
243
|
-
withTimeout(
|
|
244
|
-
Promise.resolve().then(() =>
|
|
245
|
-
bot.api<OneBotVersionInfoData>("get_version_info"),
|
|
246
|
-
),
|
|
247
|
-
).catch(() => null),
|
|
248
|
-
]);
|
|
249
|
-
if (statusResult.status === "fulfilled" && statusResult.value) {
|
|
250
|
-
online = statusResult.value.online;
|
|
251
|
-
}
|
|
252
|
-
if (versionResult.status === "fulfilled" && versionResult.value) {
|
|
253
|
-
const v = versionResult.value;
|
|
254
|
-
if (v.app_version && v.app_version.trim()) {
|
|
255
|
-
appVersion = v.app_version.trim();
|
|
256
|
-
}
|
|
257
|
-
if (v.protocol_version && v.protocol_version.trim()) {
|
|
258
|
-
protocolVersion = v.protocol_version.trim();
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
if (
|
|
262
|
-
groupsResult.status === "fulfilled" &&
|
|
263
|
-
Array.isArray(groupsResult.value)
|
|
264
|
-
) {
|
|
265
|
-
groupCount = groupsResult.value.length;
|
|
266
|
-
}
|
|
267
|
-
if (
|
|
268
|
-
friendsResult.status === "fulfilled" &&
|
|
269
|
-
Array.isArray(friendsResult.value)
|
|
270
|
-
) {
|
|
271
|
-
friendCount = friendsResult.value.length;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const counts = perBotCounts.get(uin) || { send: 0, receive: 0 };
|
|
241
|
+
return bots.map((bot) => {
|
|
242
|
+
const uin = String(bot.bot_id);
|
|
243
|
+
const adapter = String(bot.adapter);
|
|
244
|
+
const isStdin = adapter === "stdin";
|
|
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 || "";
|
|
275
250
|
|
|
276
|
-
|
|
251
|
+
return {
|
|
277
252
|
uin,
|
|
278
|
-
nickname,
|
|
279
|
-
avatarUrl
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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`,
|
|
257
|
+
adapter,
|
|
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
|
+
});
|
|
291
272
|
}
|
|
292
273
|
|
|
293
274
|
async function collectFramework(
|
|
294
|
-
rawBots:
|
|
275
|
+
rawBots: readonly Bot[],
|
|
295
276
|
botStatuses: BotAccountStatus[],
|
|
296
277
|
): Promise<FrameworkStatus> {
|
|
297
278
|
const adapters = new Set<string>();
|
|
@@ -301,28 +282,22 @@ async function collectFramework(
|
|
|
301
282
|
}
|
|
302
283
|
}
|
|
303
284
|
|
|
304
|
-
// 从 mioki 内部服务读 plugins / versions
|
|
305
|
-
let miokiStatus: MiokiStatus | null = null;
|
|
306
|
-
try {
|
|
307
|
-
miokiStatus = await withTimeout(
|
|
308
|
-
Promise.resolve().then(() => getMiokiStatus(rawBots)),
|
|
309
|
-
);
|
|
310
|
-
} catch {
|
|
311
|
-
// ignore
|
|
312
|
-
}
|
|
313
|
-
|
|
314
285
|
const runtime = detectRuntime();
|
|
315
286
|
// Read both versions from the same `getRenderVersions` helper that the
|
|
316
|
-
// help panel uses
|
|
317
|
-
// when `mioki` is only installed under `mioku/node_modules`.
|
|
287
|
+
// help panel uses.
|
|
318
288
|
const { miokiVersion, miokuVersion } = await getRenderVersions();
|
|
289
|
+
const pluginCount = getPluginMetadataList().length;
|
|
290
|
+
// 优先取第一个非 stdin 的 bot 作为平台框架版本(stdin 是本地终端,无 NapCat)
|
|
291
|
+
const primaryFramework =
|
|
292
|
+
botStatuses.find((b) => b.adapter !== "stdin")?.framework ??
|
|
293
|
+
botStatuses[0]?.framework ??
|
|
294
|
+
"unknown";
|
|
319
295
|
return {
|
|
320
296
|
miokuVersion,
|
|
321
297
|
miokiVersion,
|
|
322
|
-
napcatVersion:
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
pluginEnabled: safeNumber(miokiStatus?.plugins?.enabled),
|
|
298
|
+
napcatVersion: primaryFramework,
|
|
299
|
+
pluginCount,
|
|
300
|
+
pluginEnabled: pluginCount,
|
|
326
301
|
adapterCount: adapters.size,
|
|
327
302
|
onlineBotCount: botStatuses.filter((b) => b.online).length,
|
|
328
303
|
uptimeMs: safeNumber(process.uptime()) * 1000,
|
|
@@ -632,7 +607,7 @@ async function collectSystem(): Promise<SystemInfo> {
|
|
|
632
607
|
};
|
|
633
608
|
}
|
|
634
609
|
|
|
635
|
-
async function collectAI(ctx:
|
|
610
|
+
async function collectAI(ctx: MiokuContext): Promise<AIUsageStatsLite> {
|
|
636
611
|
const ai = ctx?.services?.ai as AIService | undefined;
|
|
637
612
|
if (!ai || typeof ai.getUsageSummary !== "function") {
|
|
638
613
|
return {
|
|
@@ -691,16 +666,29 @@ async function collectAI(ctx: MiokiContext): Promise<AIUsageStatsLite> {
|
|
|
691
666
|
}
|
|
692
667
|
|
|
693
668
|
export async function collectSnapshot(
|
|
694
|
-
ctx:
|
|
669
|
+
ctx: MiokuContext,
|
|
695
670
|
options: { isNightMode: boolean } = { isNightMode: false },
|
|
696
671
|
): Promise<StatusSnapshot> {
|
|
697
672
|
if (cache && Date.now() - cache.at < TTL_MS) {
|
|
698
673
|
return { ...cache.snapshot, isNightMode: options.isNightMode };
|
|
699
674
|
}
|
|
700
675
|
|
|
676
|
+
// 读取 help/status 配置(stdin 头像等),失败时回退到默认值
|
|
677
|
+
let statusConfig: HelpStatusConfig = {};
|
|
678
|
+
try {
|
|
679
|
+
const configService = getService(ctx, Services.Config);
|
|
680
|
+
statusConfig =
|
|
681
|
+
((await configService?.getConfig("help", "status")) as
|
|
682
|
+
| HelpStatusConfig
|
|
683
|
+
| null
|
|
684
|
+
| undefined) ?? {};
|
|
685
|
+
} catch {
|
|
686
|
+
statusConfig = {};
|
|
687
|
+
}
|
|
688
|
+
|
|
701
689
|
const rawBots = Array.from(connectedBots.values());
|
|
702
690
|
const [bots, disk, system, ai, resources] = await Promise.all([
|
|
703
|
-
collectBots(),
|
|
691
|
+
collectBots(ctx, statusConfig.stdinAvatar),
|
|
704
692
|
collectDisk(),
|
|
705
693
|
collectSystem(),
|
|
706
694
|
collectAI(ctx),
|
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,33 +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
|
-
|
|
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 `
|
|
158
170
|
<div class="status-hero__account">
|
|
159
171
|
<div class="status-hero__account-name">${escapeHtml(bot.nickname)}</div>
|
|
160
172
|
<div class="status-hero__account-body">
|
|
@@ -166,8 +178,8 @@ function renderHero(
|
|
|
166
178
|
</div>
|
|
167
179
|
</div>
|
|
168
180
|
`;
|
|
169
|
-
|
|
170
|
-
|
|
181
|
+
})
|
|
182
|
+
.join("");
|
|
171
183
|
|
|
172
184
|
return `
|
|
173
185
|
<header class="status-hero">
|
|
@@ -187,8 +199,7 @@ function renderResourceCard(
|
|
|
187
199
|
): string {
|
|
188
200
|
const linesHtml = lines
|
|
189
201
|
.map(
|
|
190
|
-
(line) =>
|
|
191
|
-
`<div class="status-pie-card__line">${escapeHtml(line)}</div>`,
|
|
202
|
+
(line) => `<div class="status-pie-card__line">${escapeHtml(line)}</div>`,
|
|
192
203
|
)
|
|
193
204
|
.join("");
|
|
194
205
|
return `
|
|
@@ -228,7 +239,10 @@ function renderResourcesSection(
|
|
|
228
239
|
"SWAP",
|
|
229
240
|
hasSwap ? r.swapPercent : 0,
|
|
230
241
|
hasSwap
|
|
231
|
-
? [
|
|
242
|
+
? [
|
|
243
|
+
`${r.swapUsedGB} / ${r.swapTotalGB} GB`,
|
|
244
|
+
`可用 ${Math.max(0, r.swapTotalGB - r.swapUsedGB).toFixed(2)} GB`,
|
|
245
|
+
]
|
|
232
246
|
: ["未配置", ""],
|
|
233
247
|
theme,
|
|
234
248
|
),
|
|
@@ -296,10 +310,10 @@ function buildSmoothPath(points: Array<{ x: number; y: number }>): string {
|
|
|
296
310
|
const p1 = points[i];
|
|
297
311
|
const p2 = points[i + 1];
|
|
298
312
|
const p3 = points[Math.min(points.length - 1, i + 2)];
|
|
299
|
-
const cp1x = p1.x + (p2.x - p0.x) * tension / 3;
|
|
300
|
-
const cp1y = p1.y + (p2.y - p0.y) * tension / 3;
|
|
301
|
-
const cp2x = p2.x - (p3.x - p1.x) * tension / 3;
|
|
302
|
-
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;
|
|
303
317
|
d += ` C ${cp1x.toFixed(2)} ${cp1y.toFixed(2)}, ${cp2x.toFixed(2)} ${cp2y.toFixed(2)}, ${p2.x.toFixed(2)} ${p2.y.toFixed(2)}`;
|
|
304
318
|
}
|
|
305
319
|
return d;
|
|
@@ -319,10 +333,7 @@ function renderNetworkChart(history: NetworkSample[]): string {
|
|
|
319
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>`;
|
|
320
334
|
}
|
|
321
335
|
|
|
322
|
-
const maxVal = Math.max(
|
|
323
|
-
1,
|
|
324
|
-
...history.map((s) => Math.max(s.rxBps, s.txBps)),
|
|
325
|
-
);
|
|
336
|
+
const maxVal = Math.max(1, ...history.map((s) => Math.max(s.rxBps, s.txBps)));
|
|
326
337
|
const yMax = maxVal * 1.2;
|
|
327
338
|
const t0 = history[0].ts;
|
|
328
339
|
const t1 = history[history.length - 1].ts;
|
|
@@ -484,9 +495,7 @@ function renderSystemSection(
|
|
|
484
495
|
} else {
|
|
485
496
|
s.memSticks.forEach((m, i) => {
|
|
486
497
|
const size =
|
|
487
|
-
m.sizeGB >= 1
|
|
488
|
-
? `${m.sizeGB} GB`
|
|
489
|
-
: `${(m.sizeGB * 1024).toFixed(0)} MB`;
|
|
498
|
+
m.sizeGB >= 1 ? `${m.sizeGB} GB` : `${(m.sizeGB * 1024).toFixed(0)} MB`;
|
|
490
499
|
const speed = m.speedMTs > 0 ? ` · ${m.speedMTs} MT/s` : "";
|
|
491
500
|
const manu =
|
|
492
501
|
m.manufacturer && m.manufacturer !== "Manufacturer"
|
|
@@ -524,7 +533,10 @@ function renderSystemSection(
|
|
|
524
533
|
: `${d.sizeGB} GB`;
|
|
525
534
|
const vendor = d.vendor && d.vendor !== d.name ? `${d.vendor} ` : "";
|
|
526
535
|
const name = d.name || d.type || "Unknown";
|
|
527
|
-
const iface =
|
|
536
|
+
const iface =
|
|
537
|
+
d.interfaceType && d.interfaceType !== "Unknown"
|
|
538
|
+
? ` · ${d.interfaceType}`
|
|
539
|
+
: "";
|
|
528
540
|
items.push({
|
|
529
541
|
label: `硬盘 ${i + 1}`,
|
|
530
542
|
value: `${vendor}${name} · ${sizeLabel}${iface}`.trim(),
|
package/status/image.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { MiokuContext, ScreenshotService } from "mioku";
|
|
2
2
|
import { checkNightMode } from "../utils";
|
|
3
3
|
import { collectSnapshot } from "./data-collector";
|
|
4
4
|
import { renderStatusHtml } from "./html-generator";
|
|
@@ -28,7 +28,7 @@ function withTimeout<T>(p: PromiseLike<T>, ms: number): Promise<T> {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export interface GenerateStatusImageOptions {
|
|
31
|
-
ctx:
|
|
31
|
+
ctx: MiokuContext;
|
|
32
32
|
event?: any;
|
|
33
33
|
intent: StatusIntent;
|
|
34
34
|
botNickname?: string;
|
package/status/intent.ts
CHANGED
package/status/types.ts
CHANGED
|
@@ -7,15 +7,22 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export interface BotAccountStatus {
|
|
10
|
-
|
|
10
|
+
/** 平台账号标识。QQ 机器人是数字字符串,stdin 等本地适配器是 "stdin"。 */
|
|
11
|
+
uin: string;
|
|
11
12
|
nickname: string;
|
|
12
13
|
avatarUrl: string;
|
|
13
|
-
/**
|
|
14
|
+
/** 适配器名,如 "stdin" / "onebotv11" / "icqq"。 */
|
|
15
|
+
adapter: string;
|
|
16
|
+
/** 实现端名称,如 "NapCat" / "LLOneBot" / "ICQQ",取不到时为空串 */
|
|
14
17
|
framework: string;
|
|
15
|
-
/**
|
|
18
|
+
/** 实现端版本,如 "5.0.6"*/
|
|
16
19
|
appVersion: string;
|
|
17
|
-
/** OneBot
|
|
20
|
+
/** 协议版本,如 OneBot 的 "v11",没有则为空串 */
|
|
18
21
|
protocolVersion: string;
|
|
22
|
+
/** 登录设备,如 icqq 的 "aPad",没有则为空串 */
|
|
23
|
+
platform: string;
|
|
24
|
+
/** 登录设备对应的客户端版本,如 "9.3.50.40225",没有则为空串 */
|
|
25
|
+
platformVersion: string;
|
|
19
26
|
online: boolean;
|
|
20
27
|
groupCount: number;
|
|
21
28
|
friendCount: number;
|
|
@@ -23,27 +30,6 @@ export interface BotAccountStatus {
|
|
|
23
30
|
receive: number;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
|
-
/**
|
|
27
|
-
* OneBot v11 `get_version_info` payload (already unwrapped from the
|
|
28
|
-
* `{ status, retcode, data, ... }` envelope by napcat-sdk's `bot.api`).
|
|
29
|
-
*/
|
|
30
|
-
export interface OneBotVersionInfoData {
|
|
31
|
-
app_name: string;
|
|
32
|
-
protocol_version: string;
|
|
33
|
-
app_version: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* OneBot v11 `get_status` payload (already unwrapped by napcat-sdk).
|
|
38
|
-
* Per napcat 官方文档: `{ online, good, stat }` —— `stat` 是空对象,
|
|
39
|
-
* 不包含 `start_time` 或任何统计字段。本接口只声明文档承诺的字段。
|
|
40
|
-
*/
|
|
41
|
-
export interface OneBotStatusData {
|
|
42
|
-
online: boolean;
|
|
43
|
-
good: boolean;
|
|
44
|
-
stat: Record<string, never>;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
33
|
/** Subset of mioku's `AIService.getUsageSummary` payload that we actually render. */
|
|
48
34
|
export interface AIUsageSummary {
|
|
49
35
|
totals?: {
|
|
@@ -123,13 +109,11 @@ export interface NodeRuntimeStatus {
|
|
|
123
109
|
arrayBuffersMB: number;
|
|
124
110
|
eventLoopDelayMs: { mean: number; p99: number };
|
|
125
111
|
/** null when `--expose-gc` is not enabled. */
|
|
126
|
-
gc:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
| null;
|
|
112
|
+
gc: {
|
|
113
|
+
available: boolean;
|
|
114
|
+
count: number;
|
|
115
|
+
lastDurationMs?: number;
|
|
116
|
+
} | null;
|
|
133
117
|
}
|
|
134
118
|
|
|
135
119
|
export interface NetworkSample {
|
package/utils.ts
CHANGED
|
@@ -96,9 +96,9 @@ export async function getRenderVersions(): Promise<{
|
|
|
96
96
|
|
|
97
97
|
const miokiCandidates = [
|
|
98
98
|
// Nested under mioku (the typical workspace case).
|
|
99
|
-
path.join(process.cwd(), "node_modules", "mioku", "node_modules", "
|
|
99
|
+
path.join(process.cwd(), "node_modules", "mioku", "node_modules", "mioku", "package.json"),
|
|
100
100
|
// Hoisted top-level (a single-package install).
|
|
101
|
-
path.join(process.cwd(), "node_modules", "
|
|
101
|
+
path.join(process.cwd(), "node_modules", "mioku", "package.json"),
|
|
102
102
|
];
|
|
103
103
|
const miokuCandidates = [
|
|
104
104
|
path.join(process.cwd(), "node_modules", "mioku", "package.json"),
|
|
@@ -107,7 +107,7 @@ export async function getRenderVersions(): Promise<{
|
|
|
107
107
|
|
|
108
108
|
let miokiVersion = "unknown";
|
|
109
109
|
for (const candidate of miokiCandidates) {
|
|
110
|
-
const v = await readNamedPackageVersion(candidate, "
|
|
110
|
+
const v = await readNamedPackageVersion(candidate, "mioku");
|
|
111
111
|
if (v !== "unknown") {
|
|
112
112
|
miokiVersion = v;
|
|
113
113
|
break;
|