mioku-plugin-help 3.0.3 → 3.1.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/help/image.ts CHANGED
@@ -108,24 +108,40 @@ export async function generateHelpImage(options: {
108
108
  * Pick a bot's nickname + avatar URL. Used so the help card can greet
109
109
  * the user with their bot's identity rather than a generic string.
110
110
  */
111
- export function resolveHelpBotProfile(
111
+ export async function resolveHelpBotProfile(
112
112
  ctx: any,
113
113
  event?: any,
114
- ): { botNickname: string; botAvatarUrl?: string } {
114
+ ): Promise<{ botNickname: string; botAvatarUrl?: string }> {
115
115
  const fallbackNickname = "Mioku Bot";
116
116
  const selfId = event?.self_id;
117
117
  const bot =
118
118
  event?.bot ??
119
119
  (ctx?.bots instanceof Map ? Array.from(ctx.bots.values())[0] : null);
120
- const botId = selfId || bot?.bot_id;
120
+ const botId = String(selfId ?? bot?.bot_id ?? "").trim();
121
121
  const botNickname = bot?.nickname || bot?.name || fallbackNickname;
122
- const botAvatarUrl = botId
123
- ? `https://q1.qlogo.cn/g?b=qq&nk=${botId}&s=640`
124
- : undefined;
122
+ const botAvatarUrl = await resolveBotAvatar(bot, botId);
125
123
 
126
124
  return { botNickname, botAvatarUrl };
127
125
  }
128
126
 
127
+ /** QQ 系的 qlogo 头像服务只认数字 QQ 号,openid/AppID 一律走平台接口 */
128
+ export async function resolveBotAvatar(
129
+ bot: any,
130
+ botId: string,
131
+ ): Promise<string | undefined> {
132
+ try {
133
+ const avatar = await bot?.getAvatar?.();
134
+ if (typeof avatar === "string" && avatar) return avatar;
135
+ } catch {
136
+ // 平台不支持时回退
137
+ }
138
+ const adapter = String(bot?.adapter ?? "");
139
+ if ((adapter === "onebotv11" || adapter === "icqq") && /^\d+$/.test(botId)) {
140
+ return `https://q1.qlogo.cn/g?b=qq&nk=${botId}&s=640`;
141
+ }
142
+ return undefined;
143
+ }
144
+
129
145
  /**
130
146
  * Reply to a chat event with a local image file. Falls back to base64
131
147
  * encoding if the network adapter can't send a raw file path.
package/help/intent.ts CHANGED
@@ -23,7 +23,7 @@ import type {
23
23
  function sanitizeKeyword(value: string): string {
24
24
  return String(value || "")
25
25
  .trim()
26
- .replace(/^[#/\s]+/, "")
26
+ .replace(/^[#/.\s]+/, "")
27
27
  .replace(/[。.!!??,,::;);]+$/g, "");
28
28
  }
29
29
 
@@ -55,15 +55,15 @@ function extractMatchTokens(text: string): string[] {
55
55
  }
56
56
 
57
57
  /**
58
- * The first whitespace-separated token of a command, with the `#` / `/`
59
- * prefix removed. Used as an additional alias when matching plugin
58
+ * The first whitespace-separated token of a command, with the `.` / `#` /
59
+ * `/` prefix removed. Used as an additional alias when matching plugin
60
60
  * commands. Returns null if the first token is empty, contains `<>` (a
61
61
  * placeholder), or has no alphanumeric characters.
62
62
  */
63
63
  function extractCommandAlias(command: string): string | null {
64
64
  const value = String(command || "")
65
65
  .trim()
66
- .replace(/^[#/]+/, "");
66
+ .replace(/^[#/.]+/, "");
67
67
  if (!value) {
68
68
  return null;
69
69
  }
package/help/role.ts CHANGED
@@ -33,7 +33,7 @@ export function canInvokeCommand(
33
33
  * Resolve the requesting user's effective role for help filtering.
34
34
  *
35
35
  * - `master` is honored everywhere (private or group) — it comes from
36
- * mioki's `isOwner` allowlist.
36
+ * mioki's `isMaster` allowlist (the bot `owners` list).
37
37
  * - Private chat skips the `isAdmin` check and defaults to `admin`,
38
38
  * so anyone DM-ing the bot can see admin-level commands.
39
39
  * - Group chat resolves bot admin (mioki `isAdmin` allowlist), group
@@ -49,7 +49,7 @@ export async function resolveViewerRole(
49
49
  ctx: any,
50
50
  event: any,
51
51
  ): Promise<CommandRole> {
52
- if (ctx?.isOwner?.(event)) {
52
+ if (ctx?.isMaster?.(event)) {
53
53
  return "master";
54
54
  }
55
55
 
@@ -65,19 +65,20 @@ export async function resolveViewerRole(
65
65
  return "admin";
66
66
  }
67
67
 
68
- if (event?.group_id != null && event?.user_id != null) {
68
+ const groupId = String(event?.group_id ?? "").trim();
69
+ const userId = String(event?.user_id ?? "").trim();
70
+ if (groupId && userId) {
69
71
  const bot = event?.bot;
70
72
  if (bot) {
71
73
  try {
72
- const info = await bot.getMemberInfo(
73
- event.group_id,
74
- event.user_id,
75
- );
74
+ const info = await bot.getMemberInfo(groupId, userId);
76
75
  if (info?.role === "owner" || info?.role === "admin") {
77
76
  return "admin";
78
77
  }
78
+ // 拿不到成员信息(null/空)时不能断言对方是普通成员,保守给 admin
79
+ if (!info?.role) return "admin";
79
80
  } catch {
80
- // fall through to member
81
+ return "admin";
81
82
  }
82
83
  }
83
84
  }
package/index.ts CHANGED
@@ -21,8 +21,6 @@ import { createHelpSkills } from "./skills";
21
21
 
22
22
  const helpPlugin = definePlugin({
23
23
  name: "help",
24
- version: "2.1.0",
25
- description: "帮助插件,生成帮助图片,并提供 #状态 指令",
26
24
 
27
25
  async setup(ctx: MiokuContext) {
28
26
  // 启动后台采样器
@@ -75,7 +73,7 @@ const helpPlugin = definePlugin({
75
73
  for (const skill of createHelpSkills()) aiService.registerSkill(skill);
76
74
  }
77
75
 
78
- ctx.handle("message", async (event) => {
76
+ const handleHelpMessage = async (event: import("mioku").MessageEvent) => {
79
77
  const text = ctx.text(event);
80
78
  if (!text) {
81
79
  return;
@@ -89,7 +87,7 @@ const helpPlugin = definePlugin({
89
87
  return;
90
88
  }
91
89
  try {
92
- const { botNickname, botAvatarUrl } = resolveHelpBotProfile(
90
+ const { botNickname, botAvatarUrl } = await resolveHelpBotProfile(
93
91
  ctx,
94
92
  event,
95
93
  );
@@ -131,7 +129,7 @@ const helpPlugin = definePlugin({
131
129
  }
132
130
 
133
131
  try {
134
- const { botNickname, botAvatarUrl } = resolveHelpBotProfile(ctx, event);
132
+ const { botNickname, botAvatarUrl } = await resolveHelpBotProfile(ctx, event);
135
133
  const viewerRole = await resolveViewerRole(ctx, event);
136
134
  const imagePath = await generateHelpImage({
137
135
  helpService,
@@ -155,6 +153,25 @@ const helpPlugin = definePlugin({
155
153
  ctx.logger.error(`生成帮助图片失败: ${error}`);
156
154
  await event.reply(`生成帮助图片失败: ${error}`);
157
155
  }
156
+ };
157
+
158
+ ctx.command({
159
+ id: "帮助菜单",
160
+ name: "help",
161
+ aliases: ["帮助", "菜单"],
162
+ match: /^(?:help|帮助|菜单)(?:\s|$)/i,
163
+ prefixes: [".", "#", "/", ""],
164
+ description: "生成帮助图片",
165
+ handler: ({ event }) => handleHelpMessage(event),
166
+ });
167
+ ctx.command({
168
+ id: "status",
169
+ name: "status",
170
+ aliases: ["状态", "zt"],
171
+ match: /^(?:status|状态|zt)(?:\s|$)/i,
172
+ prefixes: [".", "#", "/", ""],
173
+ description: "生成系统状态图片",
174
+ handler: ({ event }) => handleHelpMessage(event),
158
175
  });
159
176
 
160
177
  return () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-plugin-help",
3
- "version": "3.0.3",
3
+ "version": "3.1.0",
4
4
  "description": "帮助插件,使用截图服务生成美观的帮助图片,并提供 #状态 指令",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -17,18 +17,6 @@
17
17
  "screenshot",
18
18
  "config",
19
19
  "ai"
20
- ],
21
- "accessHooks": [
22
- {
23
- "id": "帮助菜单",
24
- "match": "/(?:[#/]\\s*(?:帮助|菜单|help)|^(?:帮助|菜单|help)(?:\\s|$))/",
25
- "description": "匹配 #help / 帮助 / 菜单 等帮助指令"
26
- },
27
- {
28
- "id": "状态",
29
- "match": "/(?:[#/]\\s*(?:状态|zt|status)|^(?:状态|zt|status)(?:\\s|$))/",
30
- "description": "匹配 #状态 / 状态 / zt / status 等系统状态指令"
31
- }
32
20
  ]
33
21
  },
34
22
  "peerDependencies": {
package/skills/help.ts CHANGED
@@ -57,7 +57,7 @@ export function createHelpSkill(): AISkill {
57
57
  }
58
58
 
59
59
  try {
60
- const { botNickname, botAvatarUrl } = resolveHelpBotProfile(
60
+ const { botNickname, botAvatarUrl } = await resolveHelpBotProfile(
61
61
  ctx,
62
62
  event,
63
63
  );
package/skills/status.ts CHANGED
@@ -32,7 +32,7 @@ export function createStatusSkill(): AISkill {
32
32
  return "screenshot 服务未加载";
33
33
  }
34
34
  try {
35
- const { botNickname, botAvatarUrl } = resolveHelpBotProfile(
35
+ const { botNickname, botAvatarUrl } = await resolveHelpBotProfile(
36
36
  ctx,
37
37
  event,
38
38
  );
@@ -205,6 +205,14 @@ async function collectBots(
205
205
  return collectBotStatuses(sorted, ctx, stdinAvatar);
206
206
  }
207
207
 
208
+ /** QQ 系适配器的 bot_id 才是数字 QQ 号,qlogo 服务对 openid/AppID 无效 */
209
+ function qlogoAvatarUrl(adapter: string, botId: string): string {
210
+ if ((adapter === "onebotv11" || adapter === "icqq") && /^\d+$/.test(botId)) {
211
+ return `https://q1.qlogo.cn/g?b=qq&nk=${botId}&s=160`;
212
+ }
213
+ return "";
214
+ }
215
+
208
216
  /** 把本地绝对路径 / http(s) / file:// 统一成可被截图服务加载的图片地址 */
209
217
  function toImageSrc(value: string): string {
210
218
  if (/^(https?:|file:|data:)/i.test(value)) return value;
@@ -274,7 +282,7 @@ async function collectBotStatuses(
274
282
  ? toImageSrc(stdinAvatar || "")
275
283
  : avatarFromBot
276
284
  ? toImageSrc(avatarFromBot)
277
- : `https://q1.qlogo.cn/g?b=qq&nk=${uin}&s=160`,
285
+ : qlogoAvatarUrl(adapter, uin),
278
286
  adapter,
279
287
  implLabel,
280
288
  framework: isStdin ? adapter : framework,
package/status/intent.ts CHANGED
@@ -3,7 +3,7 @@ import type { StatusIntent } from "./types";
3
3
  /**
4
4
  * Parse a user message into a `StatusIntent`.
5
5
  *
6
- * Recognized prefixes: `#状态`, `/状态`, `状态`, `菜单 状态` (also the
6
+ * Recognized prefixes: `.状态` / `/状态` / `#状态` / 裸 `状态` (also the
7
7
  * Latin aliases `zt` / `status`). Anything trailing the leading token is
8
8
  * ignored — the panel always renders the full sheet.
9
9
  *
@@ -14,7 +14,7 @@ import type { StatusIntent } from "./types";
14
14
  function stripStopword(input: string): string {
15
15
  return String(input || "")
16
16
  .trim()
17
- .replace(/^[#/\s]+/, "")
17
+ .replace(/^[#/.\s]+/, "")
18
18
  .replace(/[。.!!??,,::;);]+$/g, "");
19
19
  }
20
20