mioku-plugin-help 2.3.1 → 3.0.0

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 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
  ---
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  import type { CommandRole, PluginHelp } from "mioku";
15
- import { botConfig } from "mioki";
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
- (selfId && typeof ctx?.pickBot === "function"
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?.uin || bot?.user_id || bot?.self_id;
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. Picks the right bot via
164
- * `ctx.pickBot(selfId)`, supports an optional quote-reply, and falls
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 selfId = event?.self_id != null ? Number(event.self_id) : undefined;
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
- const normalizedFile = normalizeImageSource(file);
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({ type: "reply", id: String(event.message_id) });
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(/[。.!!??,,::;;]+$/g, "");
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 selfId =
70
- event?.self_id != null ? Number(event.self_id) : undefined;
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.getGroupMemberInfo(
72
+ const info = await bot.getMemberInfo(
78
73
  event.group_id,
79
74
  event.user_id,
80
75
  );
package/index.ts CHANGED
@@ -1,9 +1,8 @@
1
- import type { ConfigService } from "mioku";
2
- import type { HelpService } from "mioku";
3
- import type { ScreenshotService } from "mioku";
4
- import { definePlugin, type MiokiContext } from "mioki";
1
+ import { definePlugin, type MiokuContext } from "mioku";
2
+ import { getService, Services } from "mioku";
5
3
  import * as path from "path";
6
4
  import { HELP_DEMO_CONFIG } from "./demo-config";
5
+ import { HELP_STATUS_DEFAULT_CONFIG } from "./status/config";
7
6
  import {
8
7
  generateHelpImage,
9
8
  replyWithImage,
@@ -12,36 +11,34 @@ import {
12
11
  resolveViewerRole,
13
12
  } from "./help";
14
13
  import { getRenderVersions } from "./utils";
15
- import { resetHelpRuntimeState, setHelpRuntimeState } from "./runtime";
16
14
  import {
17
15
  generateStatusImage,
18
16
  networkSampler,
19
17
  perfMonitor,
20
18
  resolveStatusIntent,
21
19
  } from "./status";
20
+ import { createHelpSkills } from "./skills";
22
21
 
23
22
  const helpPlugin = definePlugin({
24
23
  name: "help",
25
24
  version: "2.1.0",
26
25
  description: "帮助插件,生成帮助图片,并提供 #状态 指令",
27
26
 
28
- async setup(ctx: MiokiContext) {
27
+ async setup(ctx: MiokuContext) {
29
28
  // 启动后台采样器
30
29
  networkSampler.start();
31
30
  perfMonitor.start();
32
31
 
33
- const configService = ctx.services?.config as ConfigService | undefined;
34
- const helpService = ctx.services?.help as HelpService | undefined;
35
- const screenshotService = ctx.services?.screenshot as
36
- | ScreenshotService
37
- | undefined;
32
+ const configService = getService(ctx, Services.Config);
33
+ const helpService = getService(ctx, Services.Help);
34
+ const screenshotService = getService(ctx, Services.Screenshot);
35
+ const aiService = getService(ctx, Services.AI);
38
36
 
39
37
  if (!helpService) {
40
38
  ctx.logger.warn("help-service 未加载,帮助插件无法运行");
41
39
  return () => {
42
40
  networkSampler.stop();
43
41
  perfMonitor.stop();
44
- resetHelpRuntimeState();
45
42
  ctx.logger.info("帮助插件已卸载");
46
43
  };
47
44
  }
@@ -52,6 +49,11 @@ const helpPlugin = definePlugin({
52
49
 
53
50
  if (configService) {
54
51
  await configService.registerConfig("help", "demo", HELP_DEMO_CONFIG.demo);
52
+ await configService.registerConfig(
53
+ "help",
54
+ "status",
55
+ HELP_STATUS_DEFAULT_CONFIG,
56
+ );
55
57
  }
56
58
 
57
59
  // 注册 help manifest,让 `#help 状态` 能命中
@@ -69,12 +71,11 @@ const helpPlugin = definePlugin({
69
71
 
70
72
  const { miokiVersion, miokuVersion } = await getRenderVersions();
71
73
 
72
- setHelpRuntimeState({
73
- miokiVersion,
74
- miokuVersion,
75
- });
74
+ if (aiService) {
75
+ for (const skill of createHelpSkills()) aiService.registerSkill(skill);
76
+ }
76
77
 
77
- ctx.handle("message", async (event: any) => {
78
+ ctx.handle("message", async (event) => {
78
79
  const text = ctx.text(event);
79
80
  if (!text) {
80
81
  return;
@@ -159,7 +160,10 @@ const helpPlugin = definePlugin({
159
160
  return () => {
160
161
  networkSampler.stop();
161
162
  perfMonitor.stop();
162
- resetHelpRuntimeState();
163
+ if (aiService) {
164
+ aiService.removeSkill("help");
165
+ aiService.removeSkill("status");
166
+ }
163
167
  ctx.logger.info("帮助插件已卸载");
164
168
  };
165
169
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-plugin-help",
3
- "version": "2.3.1",
3
+ "version": "3.0.0",
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.8.0",
36
- "mioki": "^0.16.0"
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/skills/help.ts ADDED
@@ -0,0 +1,93 @@
1
+ import type { AISkill, AITool } from "mioku";
2
+ import type { HelpService, ScreenshotService } from "mioku";
3
+ import {
4
+ buildHelpInfoText,
5
+ generateHelpImage,
6
+ resolveHelpBotProfile,
7
+ resolveViewerRole,
8
+ sendImageFromSkillContext,
9
+ } from "../help";
10
+ import { getRenderVersions } from "../utils";
11
+
12
+ export function createHelpSkill(): AISkill {
13
+ return {
14
+ name: "help",
15
+ description: "获取插件帮助信息和发送帮助图片",
16
+ permission: "member",
17
+ tools: [
18
+ {
19
+ name: "get_help_info",
20
+ description:
21
+ "获取所有插件的帮助信息文本,这个仅用于用户向你询问某个功能的具体用法时使用",
22
+ parameters: {
23
+ type: "object",
24
+ properties: {},
25
+ required: [],
26
+ },
27
+ handler: async (_args: any, runtimeCtx?: any) => {
28
+ const ctx = runtimeCtx?.ctx;
29
+ const helpService = ctx?.services?.help as HelpService | undefined;
30
+ if (!helpService) {
31
+ return "help-service 未加载,无法获取帮助信息";
32
+ }
33
+
34
+ return buildHelpInfoText(helpService.getAllHelp());
35
+ },
36
+ } as AITool,
37
+ {
38
+ name: "send_help_image",
39
+ description:
40
+ "生成并发送帮助图片到群聊,如果有人说他想看帮助,优先调用图片发送而不是自己查看帮助。",
41
+ parameters: {
42
+ type: "object",
43
+ properties: {},
44
+ required: [],
45
+ },
46
+ handler: async (_args: any, runtimeCtx?: any) => {
47
+ const ctx = runtimeCtx?.ctx;
48
+ const event = runtimeCtx?.event || runtimeCtx?.rawEvent;
49
+ const helpService = ctx?.services?.help as HelpService | undefined;
50
+ const screenshotService = ctx?.services?.screenshot as
51
+ | ScreenshotService
52
+ | undefined;
53
+ const { miokiVersion, miokuVersion } = await getRenderVersions();
54
+
55
+ if (!screenshotService) {
56
+ return "screenshot 服务未加载,无法生成帮助图片";
57
+ }
58
+
59
+ try {
60
+ const { botNickname, botAvatarUrl } = resolveHelpBotProfile(
61
+ ctx,
62
+ event,
63
+ );
64
+ const viewerRole = await resolveViewerRole(ctx, event);
65
+ const imagePath = await generateHelpImage({
66
+ helpService,
67
+ screenshotService,
68
+ miokiVersion,
69
+ miokuVersion,
70
+ botNickname,
71
+ botAvatarUrl,
72
+ viewerRole,
73
+ });
74
+
75
+ if (!imagePath) {
76
+ return "生成帮助图片失败";
77
+ }
78
+
79
+ await sendImageFromSkillContext({
80
+ ctx,
81
+ event,
82
+ imagePath,
83
+ quoteReply: false,
84
+ });
85
+ return "已发送帮助图片";
86
+ } catch (error) {
87
+ return `生成帮助图片失败: ${error}`;
88
+ }
89
+ },
90
+ } as AITool,
91
+ ],
92
+ };
93
+ }
@@ -0,0 +1,7 @@
1
+ import type { AISkill } from "mioku";
2
+ import { createHelpSkill } from "./help";
3
+ import { createStatusSkill } from "./status";
4
+
5
+ export function createHelpSkills(): AISkill[] {
6
+ return [createHelpSkill(), createStatusSkill()];
7
+ }
@@ -0,0 +1,63 @@
1
+ import type { AISkill, AITool } from "mioku";
2
+ import type { ScreenshotService } from "mioku";
3
+ import {
4
+ resolveHelpBotProfile,
5
+ sendImageFromSkillContext,
6
+ } from "../help";
7
+ import { generateStatusImage } from "../status";
8
+
9
+ export function createStatusSkill(): AISkill {
10
+ return {
11
+ name: "status",
12
+ description:
13
+ "系统状态查询,生成完整的系统状态图片",
14
+ permission: "member",
15
+ tools: [
16
+ {
17
+ name: "send_status_image",
18
+ description:
19
+ "生成并发送完整的系统状态图片。当用户问「看看状态」「机器人怎么样」「服务器忙吗」「资源还够吗」「看看 AI 用了多少」时优先使用。",
20
+ parameters: {
21
+ type: "object",
22
+ properties: {},
23
+ required: [],
24
+ },
25
+ handler: async (_args: any, runtimeCtx?: any) => {
26
+ const ctx = runtimeCtx?.ctx;
27
+ const event = runtimeCtx?.event || runtimeCtx?.rawEvent;
28
+ const screenshotService = ctx?.services?.screenshot as
29
+ | ScreenshotService
30
+ | undefined;
31
+ if (!screenshotService) {
32
+ return "screenshot 服务未加载";
33
+ }
34
+ try {
35
+ const { botNickname, botAvatarUrl } = resolveHelpBotProfile(
36
+ ctx,
37
+ event,
38
+ );
39
+ const result = await generateStatusImage({
40
+ ctx,
41
+ event,
42
+ intent: { type: "full" },
43
+ botNickname,
44
+ botAvatarUrl,
45
+ });
46
+ if (!result.ok || !result.imagePath) {
47
+ return `生成状态图片失败: ${result.error || "未知错误"}`;
48
+ }
49
+ await sendImageFromSkillContext({
50
+ ctx,
51
+ event,
52
+ imagePath: result.imagePath,
53
+ quoteReply: false,
54
+ });
55
+ return "已发送状态图片";
56
+ } catch (error) {
57
+ return `生成状态图片失败: ${error}`;
58
+ }
59
+ },
60
+ } as AITool,
61
+ ],
62
+ };
63
+ }
@@ -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
+ };
@@ -1,16 +1,17 @@
1
1
  import * as os from "node:os";
2
2
  import * as path from "node:path";
3
+ import systemInfo from "systeminformation";
3
4
  import {
4
5
  connectedBots,
5
- getMiokiStatus,
6
- systemInfo,
7
- type ExtendedNapCat,
8
- type MiokiStatus,
9
- } from "mioki";
10
- import type { AIService, MiokiContext } from "mioku";
6
+ getPluginMetadataList,
7
+ getService,
8
+ Services,
9
+ } from "mioku";
10
+ import type { AIService, Bot, MiokuContext } from "mioku";
11
11
  import { getRenderVersions } from "../utils";
12
12
  import { perfMonitor } from "./performance-monitor";
13
13
  import { networkSampler } from "./network-sampler";
14
+ import type { HelpStatusConfig } from "./config";
14
15
  import type {
15
16
  AIUsageStatsLite,
16
17
  AIUsageSummary,
@@ -185,45 +186,47 @@ function formatDuration(ms: number): string {
185
186
  return `${minutes}分${String(seconds).padStart(2, "0")}秒`;
186
187
  }
187
188
 
188
- async function collectBots(): Promise<BotAccountStatus[]> {
189
+ async function collectBots(
190
+ ctx: MiokuContext,
191
+ stdinAvatar?: string,
192
+ ): Promise<BotAccountStatus[]> {
189
193
  const bots = Array.from(connectedBots.values());
190
194
  if (bots.length === 0) {
191
195
  return [];
192
196
  }
193
- return collectBotStatuses(bots);
197
+ // stdin 适配器恒排第一位(终端是本机主人通道,优先展示)
198
+ const sorted = [...bots].sort(
199
+ (a, b) =>
200
+ Number(b.adapter === "stdin") - Number(a.adapter === "stdin"),
201
+ );
202
+ return collectBotStatuses(sorted, ctx, stdinAvatar);
194
203
  }
195
204
 
196
- async function collectBotStatuses(
197
- bots: ExtendedNapCat[],
198
- ): Promise<BotAccountStatus[]> {
199
- let perBotCounts = new Map<number, { send: number; receive: number }>();
200
- try {
201
- const miokiStatus = await withTimeout(
202
- Promise.resolve().then(() => getMiokiStatus(bots)),
203
- );
204
- if (Array.isArray(miokiStatus?.bots)) {
205
- for (const b of miokiStatus.bots) {
206
- const u = safeNumber(b?.uin);
207
- if (u > 0) {
208
- perBotCounts.set(u, {
209
- send: safeNumber(b?.send),
210
- receive: safeNumber(b?.receive),
211
- });
212
- }
213
- }
214
- }
215
- } catch {
216
- // ignore
205
+ /** 把本地绝对路径 / http(s) / file:// 统一成可被截图服务加载的图片地址 */
206
+ function toImageSrc(value: string): string {
207
+ if (/^(https?:|file:|data:)/i.test(value)) return value;
208
+ if (value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value)) {
209
+ return `file://${value}`;
217
210
  }
211
+ return value;
212
+ }
218
213
 
214
+ async function collectBotStatuses(
215
+ bots: readonly Bot[],
216
+ ctx: MiokuContext,
217
+ stdinAvatar?: string,
218
+ ): Promise<BotAccountStatus[]> {
219
219
  const results: BotAccountStatus[] = [];
220
220
  for (const bot of bots) {
221
- const uin = safeNumber(bot?.bot_id || bot?.uin || bot?.user_id);
222
- const nickname = String(bot?.nickname || "Unknown Bot");
223
- const framework = String(bot?.app_name || "unknown");
224
- const avatarUrl = `https://q1.qlogo.cn/g?b=qq&nk=${uin}&s=160`;
221
+ const uin = String(bot.bot_id);
222
+ const adapter = String(bot.adapter);
223
+ 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`;
225
229
 
226
- // 并行拿 OneBot API 的 status / group / friend / version
227
230
  let online = true;
228
231
  let groupCount = 0;
229
232
  let friendCount = 0;
@@ -231,9 +234,9 @@ async function collectBotStatuses(
231
234
  let protocolVersion = "unknown";
232
235
  const [statusResult, groupsResult, friendsResult, versionResult] =
233
236
  await Promise.allSettled([
234
- withTimeout(
235
- Promise.resolve().then(() => bot.api<OneBotStatusData>("get_status")),
236
- ).catch(() => null),
237
+ withTimeout(Promise.resolve().then(() => bot.sendApi<OneBotStatusData>("get_status"))).catch(
238
+ () => null,
239
+ ),
237
240
  withTimeout(Promise.resolve().then(() => bot.getGroupList())).catch(
238
241
  () => [],
239
242
  ),
@@ -241,16 +244,18 @@ async function collectBotStatuses(
241
244
  () => [],
242
245
  ),
243
246
  withTimeout(
244
- Promise.resolve().then(() =>
245
- bot.api<OneBotVersionInfoData>("get_version_info"),
246
- ),
247
+ Promise.resolve().then(() => bot.sendApi<OneBotVersionInfoData>("get_version_info")),
247
248
  ).catch(() => null),
248
249
  ]);
249
250
  if (statusResult.status === "fulfilled" && statusResult.value) {
250
- online = statusResult.value.online;
251
+ online = Boolean(statusResult.value.online);
251
252
  }
252
- if (versionResult.status === "fulfilled" && versionResult.value) {
253
+ if (isStdin) {
254
+ // 终端适配器没有 OneBot 版本信息,直接用适配器包版本
255
+ appVersion = String(ctx.getAdapter(adapter)?.version ?? "unknown");
256
+ } else if (versionResult.status === "fulfilled" && versionResult.value) {
253
257
  const v = versionResult.value;
258
+ framework = String(v.app_name || "").trim() || framework;
254
259
  if (v.app_version && v.app_version.trim()) {
255
260
  appVersion = v.app_version.trim();
256
261
  }
@@ -271,27 +276,26 @@ async function collectBotStatuses(
271
276
  friendCount = friendsResult.value.length;
272
277
  }
273
278
 
274
- const counts = perBotCounts.get(uin) || { send: 0, receive: 0 };
275
-
276
279
  results.push({
277
280
  uin,
278
281
  nickname,
279
282
  avatarUrl,
283
+ adapter,
280
284
  framework,
281
285
  appVersion,
282
286
  protocolVersion,
283
287
  online,
284
288
  groupCount,
285
289
  friendCount,
286
- send: counts.send,
287
- receive: counts.receive,
290
+ send: 0,
291
+ receive: 0,
288
292
  });
289
293
  }
290
294
  return results;
291
295
  }
292
296
 
293
297
  async function collectFramework(
294
- rawBots: ExtendedNapCat[],
298
+ rawBots: readonly Bot[],
295
299
  botStatuses: BotAccountStatus[],
296
300
  ): Promise<FrameworkStatus> {
297
301
  const adapters = new Set<string>();
@@ -301,28 +305,22 @@ async function collectFramework(
301
305
  }
302
306
  }
303
307
 
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
308
  const runtime = detectRuntime();
315
309
  // Read both versions from the same `getRenderVersions` helper that the
316
- // help panel uses, so the status footer and the help footer agree even
317
- // when `mioki` is only installed under `mioku/node_modules`.
310
+ // help panel uses.
318
311
  const { miokiVersion, miokuVersion } = await getRenderVersions();
312
+ const pluginCount = getPluginMetadataList().length;
313
+ // 优先取第一个非 stdin 的 bot 作为平台框架版本(stdin 是本地终端,无 NapCat)
314
+ const primaryFramework =
315
+ botStatuses.find((b) => b.adapter !== "stdin")?.framework ??
316
+ botStatuses[0]?.framework ??
317
+ "unknown";
319
318
  return {
320
319
  miokuVersion,
321
320
  miokiVersion,
322
- napcatVersion:
323
- miokiStatus?.versions?.napcat ?? botStatuses[0]?.framework ?? "unknown",
324
- pluginCount: safeNumber(miokiStatus?.plugins?.total),
325
- pluginEnabled: safeNumber(miokiStatus?.plugins?.enabled),
321
+ napcatVersion: primaryFramework,
322
+ pluginCount,
323
+ pluginEnabled: pluginCount,
326
324
  adapterCount: adapters.size,
327
325
  onlineBotCount: botStatuses.filter((b) => b.online).length,
328
326
  uptimeMs: safeNumber(process.uptime()) * 1000,
@@ -632,7 +630,7 @@ async function collectSystem(): Promise<SystemInfo> {
632
630
  };
633
631
  }
634
632
 
635
- async function collectAI(ctx: MiokiContext): Promise<AIUsageStatsLite> {
633
+ async function collectAI(ctx: MiokuContext): Promise<AIUsageStatsLite> {
636
634
  const ai = ctx?.services?.ai as AIService | undefined;
637
635
  if (!ai || typeof ai.getUsageSummary !== "function") {
638
636
  return {
@@ -691,16 +689,29 @@ async function collectAI(ctx: MiokiContext): Promise<AIUsageStatsLite> {
691
689
  }
692
690
 
693
691
  export async function collectSnapshot(
694
- ctx: MiokiContext,
692
+ ctx: MiokuContext,
695
693
  options: { isNightMode: boolean } = { isNightMode: false },
696
694
  ): Promise<StatusSnapshot> {
697
695
  if (cache && Date.now() - cache.at < TTL_MS) {
698
696
  return { ...cache.snapshot, isNightMode: options.isNightMode };
699
697
  }
700
698
 
699
+ // 读取 help/status 配置(stdin 头像等),失败时回退到默认值
700
+ let statusConfig: HelpStatusConfig = {};
701
+ try {
702
+ const configService = getService(ctx, Services.Config);
703
+ statusConfig =
704
+ ((await configService?.getConfig("help", "status")) as
705
+ | HelpStatusConfig
706
+ | null
707
+ | undefined) ?? {};
708
+ } catch {
709
+ statusConfig = {};
710
+ }
711
+
701
712
  const rawBots = Array.from(connectedBots.values());
702
713
  const [bots, disk, system, ai, resources] = await Promise.all([
703
- collectBots(),
714
+ collectBots(ctx, statusConfig.stdinAvatar),
704
715
  collectDisk(),
705
716
  collectSystem(),
706
717
  collectAI(ctx),
@@ -134,10 +134,13 @@ function renderHero(
134
134
  .map((bot) => {
135
135
  const statusText = bot.online ? "在线" : "离线";
136
136
  const statusColor = bot.online ? "#10b981" : "#ef4444";
137
+ // stdin 等本地适配器没有 OneBot 版本信息,统一显示为 `<适配器>@<包版本>`
137
138
  const frameworkText =
138
- bot.appVersion && bot.appVersion !== "unknown"
139
- ? `${bot.framework} ${bot.appVersion} · 协议 ${bot.protocolVersion}`
140
- : bot.framework;
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;
141
144
  // Nickname is rendered as a bold large heading above the avatar
142
145
  // row (not as a chip). Chips below only carry the metadata.
143
146
  const tags = [
package/status/image.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { MiokiContext, ScreenshotService } from "mioku";
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: MiokiContext;
31
+ ctx: MiokuContext;
32
32
  event?: any;
33
33
  intent: StatusIntent;
34
34
  botNickname?: string;
package/status/intent.ts CHANGED
@@ -15,7 +15,7 @@ function stripStopword(input: string): string {
15
15
  return String(input || "")
16
16
  .trim()
17
17
  .replace(/^[#/\s]+/, "")
18
- .replace(/[。.!!??,,::;;]+$/g, "");
18
+ .replace(/[。.!!??,,::;);]+$/g, "");
19
19
  }
20
20
 
21
21
  function normalize(value: string): string {
@@ -1,4 +1,4 @@
1
- import { systemInfo } from "mioki";
1
+ import systemInfo from "systeminformation";
2
2
  import type { NetworkSample } from "./types";
3
3
 
4
4
  /**
package/status/types.ts CHANGED
@@ -7,9 +7,12 @@
7
7
  */
8
8
 
9
9
  export interface BotAccountStatus {
10
- uin: number;
10
+ /** 平台账号标识。QQ 机器人是数字字符串,stdin 等本地适配器是 "stdin"。 */
11
+ uin: string;
11
12
  nickname: string;
12
13
  avatarUrl: string;
14
+ /** 适配器名,如 "stdin" / "onebotv11" / "icqq"。 */
15
+ adapter: string;
13
16
  /** Underlying bot framework identifier, e.g. "QQBot" / "NapCat" / "LLOneBot". */
14
17
  framework: string;
15
18
  /** Adapter app version, e.g. "5.0.6" — from OneBot `get_version_info.app_version`. */
@@ -25,7 +28,7 @@ export interface BotAccountStatus {
25
28
 
26
29
  /**
27
30
  * OneBot v11 `get_version_info` payload (already unwrapped from the
28
- * `{ status, retcode, data, ... }` envelope by napcat-sdk's `bot.api`).
31
+ * `{ status, retcode, data, ... }` envelope by the adapter's `bot.sendApi`).
29
32
  */
30
33
  export interface OneBotVersionInfoData {
31
34
  app_name: string;
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", "mioki", "package.json"),
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", "mioki", "package.json"),
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, "mioki");
110
+ const v = await readNamedPackageVersion(candidate, "mioku");
111
111
  if (v !== "unknown") {
112
112
  miokiVersion = v;
113
113
  break;
package/runtime.ts DELETED
@@ -1,25 +0,0 @@
1
- export interface HelpPluginRuntimeState {
2
- miokiVersion?: string;
3
- miokuVersion?: string;
4
- }
5
-
6
- const runtimeState: HelpPluginRuntimeState = {};
7
-
8
- export function setHelpRuntimeState(
9
- nextState: HelpPluginRuntimeState,
10
- ): HelpPluginRuntimeState {
11
- Object.assign(runtimeState, nextState);
12
- return runtimeState;
13
- }
14
-
15
- export function getHelpRuntimeState(): HelpPluginRuntimeState {
16
- return runtimeState;
17
- }
18
-
19
- export function resetHelpRuntimeState(): void {
20
- for (const key of Object.keys(runtimeState) as Array<
21
- keyof HelpPluginRuntimeState
22
- >) {
23
- delete runtimeState[key];
24
- }
25
- }
package/skills.ts DELETED
@@ -1,152 +0,0 @@
1
- import type { AISkill, AITool } from "mioku";
2
- import type { HelpService } from "mioku";
3
- import type { ScreenshotService } from "mioku";
4
- import {
5
- buildHelpInfoText,
6
- generateHelpImage,
7
- resolveHelpBotProfile,
8
- resolveViewerRole,
9
- sendImageFromSkillContext,
10
- } from "./help";
11
- import { getRenderVersions } from "./utils";
12
- import { generateStatusImage } from "./status";
13
-
14
- const helpSkill: AISkill = {
15
- name: "help",
16
- description: "帮助系统,获取插件帮助信息和发送帮助图片",
17
- permission: "member",
18
- tools: [
19
- {
20
- name: "get_help_info",
21
- description:
22
- "获取所有插件的帮助信息文本,这个仅用于用户向你询问某个功能的具体用法时使用",
23
- parameters: {
24
- type: "object",
25
- properties: {},
26
- required: [],
27
- },
28
- handler: async (_args: any, runtimeCtx?: any) => {
29
- const ctx = runtimeCtx?.ctx;
30
- const helpService = ctx?.services?.help as HelpService | undefined;
31
- if (!helpService) {
32
- return "help-service 未加载,无法获取帮助信息";
33
- }
34
-
35
- return buildHelpInfoText(helpService.getAllHelp());
36
- },
37
- } as AITool,
38
- {
39
- name: "send_help_image",
40
- description:
41
- "生成并发送帮助图片到群聊,如果有人说他想看帮助,优先调用图片发送而不是自己查看帮助。",
42
- parameters: {
43
- type: "object",
44
- properties: {},
45
- required: [],
46
- },
47
- handler: async (_args: any, runtimeCtx?: any) => {
48
- const ctx = runtimeCtx?.ctx;
49
- const event = runtimeCtx?.event || runtimeCtx?.rawEvent;
50
- const helpService = ctx?.services?.help as HelpService | undefined;
51
- const screenshotService = ctx?.services?.screenshot as
52
- | ScreenshotService
53
- | undefined;
54
- const { miokiVersion, miokuVersion } = await getRenderVersions();
55
-
56
- if (!screenshotService) {
57
- return "screenshot 服务未加载,无法生成帮助图片";
58
- }
59
-
60
- try {
61
- const { botNickname, botAvatarUrl } = resolveHelpBotProfile(
62
- ctx,
63
- event,
64
- );
65
- const viewerRole = await resolveViewerRole(ctx, event);
66
- const imagePath = await generateHelpImage({
67
- helpService,
68
- screenshotService,
69
- miokiVersion,
70
- miokuVersion,
71
- botNickname,
72
- botAvatarUrl,
73
- viewerRole,
74
- });
75
-
76
- if (!imagePath) {
77
- return "生成帮助图片失败";
78
- }
79
-
80
- // Help skill defaults to normal send instead of quote-reply.
81
- await sendImageFromSkillContext({
82
- ctx,
83
- event,
84
- imagePath,
85
- quoteReply: false,
86
- });
87
- return "已发送帮助图片";
88
- } catch (error) {
89
- return `生成帮助图片失败: ${error}`;
90
- }
91
- },
92
- } as AITool,
93
- ],
94
- };
95
-
96
- const statusSkill: AISkill = {
97
- name: "status",
98
- description:
99
- "系统状态查询,生成完整的系统状态图片(账号、资源、网络、磁盘、AI 统计、运行时、系统信息)",
100
- permission: "member",
101
- tools: [
102
- {
103
- name: "send_status_image",
104
- description:
105
- "生成并发送完整的系统状态图片。当用户问「看看状态」「机器人怎么样」「服务器忙吗」「资源还够吗」「看看 AI 用了多少」时优先使用。",
106
- parameters: {
107
- type: "object",
108
- properties: {},
109
- required: [],
110
- },
111
- handler: async (_args: any, runtimeCtx?: any) => {
112
- const ctx = runtimeCtx?.ctx;
113
- const event = runtimeCtx?.event || runtimeCtx?.rawEvent;
114
- const screenshotService = ctx?.services?.screenshot as
115
- | ScreenshotService
116
- | undefined;
117
- if (!screenshotService) {
118
- return "screenshot 服务未加载";
119
- }
120
- try {
121
- const { botNickname, botAvatarUrl } = resolveHelpBotProfile(
122
- ctx,
123
- event,
124
- );
125
- const result = await generateStatusImage({
126
- ctx,
127
- event,
128
- intent: { type: "full" },
129
- botNickname,
130
- botAvatarUrl,
131
- });
132
- if (!result.ok || !result.imagePath) {
133
- return `生成状态图片失败: ${result.error || "未知错误"}`;
134
- }
135
- await sendImageFromSkillContext({
136
- ctx,
137
- event,
138
- imagePath: result.imagePath,
139
- quoteReply: false,
140
- });
141
- return "已发送状态图片";
142
- } catch (error) {
143
- return `生成状态图片失败: ${error}`;
144
- }
145
- },
146
- } as AITool,
147
- ],
148
- };
149
-
150
- const helpSkills: AISkill[] = [helpSkill, statusSkill];
151
-
152
- export default helpSkills;