mioku-plugin-help 2.2.0 → 2.3.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.
@@ -12,12 +12,21 @@
12
12
  */
13
13
 
14
14
  import type { CommandRole, PluginHelp } from "mioku";
15
+ import { botConfig } from "mioki";
15
16
  import { escapeHtml } from "../utils";
16
17
  import { getHelpTheme, HELP_BACKGROUND_IMAGE_URL } from "../theme";
17
18
  import { getRenderableEntries } from "./intent";
18
19
  import { ROLE_CONFIG } from "./role-config";
19
20
  import type { HelpRenderableEntry } from "./types";
20
21
 
22
+ function resolvePrefixPlaceholder(cmd: string): string {
23
+ const value = String(cmd || "");
24
+ if (value.startsWith("?")) {
25
+ return `${botConfig.prefix ?? "#"}${value.slice(1)}`;
26
+ }
27
+ return value;
28
+ }
29
+
21
30
  function renderRoleBadge(
22
31
  role: CommandRole,
23
32
  isNightMode: boolean,
@@ -38,7 +47,7 @@ function renderPluginOverview(entry: HelpRenderableEntry): string {
38
47
  const commandTags = entry.commands
39
48
  .map(
40
49
  (command) =>
41
- `<span class="help-command-tag" title="${escapeHtml(command.desc || "")}" >${escapeHtml(command.cmd)}</span>`,
50
+ `<span class="help-command-tag" title="${escapeHtml(command.desc || "")}" >${escapeHtml(resolvePrefixPlaceholder(command.cmd))}</span>`,
42
51
  )
43
52
  .join("");
44
53
 
@@ -73,7 +82,7 @@ function renderPluginDetail(
73
82
  return `
74
83
  <div class="help-command">
75
84
  <div class="help-command__top">
76
- <div class="help-command__name">${escapeHtml(command.cmd)}</div>
85
+ <div class="help-command__name">${escapeHtml(resolvePrefixPlaceholder(command.cmd))}</div>
77
86
  ${roleBadge}
78
87
  </div>
79
88
  <div class="help-command__desc">${escapeHtml(command.desc || "")}</div>
@@ -109,6 +118,9 @@ function renderPluginDetail(
109
118
  * Build the full help image HTML. `targetPluginName` switches the
110
119
  * renderer to detail mode for that plugin (no-op if the name doesn't
111
120
  * match anything in `helpMap`).
121
+ *
122
+ * `viewerRole` filters out commands the requesting user can't invoke,
123
+ * so the image only surfaces what they can actually run.
112
124
  */
113
125
  export function generateHelpHtml(
114
126
  helpMap: Map<string, PluginHelp>,
@@ -118,8 +130,9 @@ export function generateHelpHtml(
118
130
  botNickname: string = "Mioku Bot",
119
131
  botAvatarUrl?: string,
120
132
  targetPluginName?: string,
133
+ viewerRole: CommandRole = "master",
121
134
  ): string {
122
- const entries = getRenderableEntries(helpMap);
135
+ const entries = getRenderableEntries(helpMap, viewerRole);
123
136
  const selectedEntry = targetPluginName
124
137
  ? entries.find((entry) => entry.pluginName === targetPluginName)
125
138
  : undefined;
package/help/image.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  */
17
17
 
18
18
  import * as fs from "node:fs/promises";
19
- import type { HelpService, ScreenshotService } from "mioku";
19
+ import type { CommandRole, HelpService, ScreenshotService } from "mioku";
20
20
  import { checkNightMode } from "../utils";
21
21
  import { generateHelpHtml } from "./html-generator";
22
22
 
@@ -52,6 +52,10 @@ export function normalizeImageSource(file: string): string {
52
52
  * Build the help image and write it to disk via the screenshot service.
53
53
  * Returns the resulting file path, or `null` if any required service is
54
54
  * missing.
55
+ *
56
+ * `viewerRole` controls which commands are rendered — the image only
57
+ * shows commands the requester is allowed to invoke, so members don't
58
+ * see admin/master-only commands cluttering the panel.
55
59
  */
56
60
  export async function generateHelpImage(options: {
57
61
  helpService?: HelpService;
@@ -61,6 +65,7 @@ export async function generateHelpImage(options: {
61
65
  botNickname?: string;
62
66
  botAvatarUrl?: string;
63
67
  targetPluginName?: string;
68
+ viewerRole?: CommandRole;
64
69
  }): Promise<string | null> {
65
70
  const {
66
71
  helpService,
@@ -70,6 +75,7 @@ export async function generateHelpImage(options: {
70
75
  botNickname,
71
76
  botAvatarUrl,
72
77
  targetPluginName,
78
+ viewerRole,
73
79
  } = options;
74
80
  if (!helpService || !screenshotService) {
75
81
  return null;
@@ -87,6 +93,7 @@ export async function generateHelpImage(options: {
87
93
  botNickname,
88
94
  botAvatarUrl,
89
95
  hasTarget ? targetPluginName : undefined,
96
+ viewerRole,
90
97
  );
91
98
 
92
99
  return screenshotService.screenshot(htmlContent, {
package/help/index.ts CHANGED
@@ -20,5 +20,6 @@ export {
20
20
  } from "./intent";
21
21
  export { buildHelpInfoText } from "./info";
22
22
  export { generateHelpHtml } from "./html-generator";
23
+ export { canInvokeCommand, resolveViewerRole } from "./role";
23
24
  export type { HelpImageIntent, HelpRenderableEntry } from "./types";
24
25
  export { ROLE_CONFIG, STOPWORDS } from "./role-config";
package/help/intent.ts CHANGED
@@ -7,7 +7,8 @@
7
7
  * plugin names, titles, command aliases, and Chinese/English substrings.
8
8
  */
9
9
 
10
- import type { PluginHelp } from "mioku";
10
+ import type { CommandRole, PluginHelp } from "mioku";
11
+ import { canInvokeCommand } from "./role";
11
12
  import { STOPWORDS } from "./role-config";
12
13
  import type {
13
14
  HelpImageIntent,
@@ -89,15 +90,27 @@ function extractCommandAlias(command: string): string | null {
89
90
  * rendered entries are the source of truth for both keyword scoring
90
91
  * (in this file) and the HTML renderer's overview list. Exported so
91
92
  * `html-generator.ts` can reuse it instead of duplicating the logic.
93
+ *
94
+ * `viewerRole` filters out commands above the requester's permission
95
+ * level so the overview/detail views don't show commands they can't
96
+ * invoke. Defaults to `"master"` so callers that don't yet know the
97
+ * viewer (e.g. AI skill listings) keep the full registry.
92
98
  */
93
99
  export function getRenderableEntries(
94
100
  helpMap: Map<string, PluginHelp>,
101
+ viewerRole: CommandRole = "master",
95
102
  ): HelpRenderableEntry[] {
96
103
  return Array.from(helpMap.entries())
97
104
  .map(([pluginName, help]) => {
98
105
  const title = String(help.title || pluginName).trim() || pluginName;
99
106
  const description = String(help.description || "").trim();
100
- const commands = Array.isArray(help.commands) ? help.commands : [];
107
+ const allCommands = Array.isArray(help.commands) ? help.commands : [];
108
+ const commands = allCommands.filter((command) =>
109
+ canInvokeCommand(
110
+ viewerRole,
111
+ command.role as CommandRole | undefined,
112
+ ),
113
+ );
101
114
  const normalizedPluginName = normalizeForMatch(pluginName);
102
115
  const normalizedTitle = normalizeForMatch(title);
103
116
 
@@ -113,7 +126,7 @@ export function getRenderableEntries(
113
126
  keys.add(token);
114
127
  }
115
128
 
116
- const commandAliases = commands
129
+ const commandAliases = allCommands
117
130
  .map((command) => extractCommandAlias(command.cmd))
118
131
  .filter((value): value is string => Boolean(value));
119
132
  for (const alias of commandAliases) {
package/help/role.ts ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Viewer role resolution for the help image.
3
+ *
4
+ * Decides which commands a given user is allowed to see based on the
5
+ * event's sender and the bot's owner/admin allowlists. The result is
6
+ * used to filter `getRenderableEntries` so the rendered help image
7
+ * matches what the requester can actually invoke.
8
+ */
9
+
10
+ import type { CommandRole } from "mioku";
11
+
12
+ const ROLE_RANK: Record<CommandRole, number> = {
13
+ master: 4,
14
+ admin: 3,
15
+ owner: 2,
16
+ member: 1,
17
+ };
18
+
19
+ /**
20
+ * Whether the given viewer can invoke a command gated at `commandRole`.
21
+ * A command without a `role` field is treated as member-level (visible
22
+ * to everyone).
23
+ */
24
+ export function canInvokeCommand(
25
+ viewerRole: CommandRole,
26
+ commandRole: CommandRole | undefined,
27
+ ): boolean {
28
+ const required: CommandRole = commandRole || "member";
29
+ return ROLE_RANK[viewerRole] >= ROLE_RANK[required];
30
+ }
31
+
32
+ /**
33
+ * Resolve the requesting user's effective role for help filtering.
34
+ *
35
+ * - `master` is honored everywhere (private or group) — it comes from
36
+ * mioki's `isOwner` allowlist.
37
+ * - Private chat skips the `isAdmin` check and defaults to `admin`,
38
+ * so anyone DM-ing the bot can see admin-level commands.
39
+ * - Group chat keeps the full hierarchy: admin (mioki allowlist) →
40
+ * owner (group owner via `getGroupMemberInfo`) → member.
41
+ */
42
+ export async function resolveViewerRole(
43
+ ctx: any,
44
+ event: any,
45
+ ): Promise<CommandRole> {
46
+ if (ctx?.isOwner?.(event)) {
47
+ return "master";
48
+ }
49
+
50
+ const isGroup = event?.message_type === "group";
51
+
52
+ if (isGroup) {
53
+ if (ctx?.isAdmin?.(event)) {
54
+ return "admin";
55
+ }
56
+
57
+ if (event?.group_id != null && event?.user_id != null) {
58
+ const selfId =
59
+ event?.self_id != null ? Number(event.self_id) : undefined;
60
+ const bot =
61
+ selfId != null && typeof ctx?.pickBot === "function"
62
+ ? ctx.pickBot(selfId)
63
+ : undefined;
64
+ if (bot && typeof bot.getGroupMemberInfo === "function") {
65
+ try {
66
+ const info = await bot.getGroupMemberInfo(
67
+ event.group_id,
68
+ event.user_id,
69
+ );
70
+ if (info?.role === "owner") {
71
+ return "owner";
72
+ }
73
+ } catch {
74
+ // fall through to member
75
+ }
76
+ }
77
+ }
78
+
79
+ return "member";
80
+ }
81
+
82
+ return "admin";
83
+ }
package/index.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  replyWithImage,
10
10
  resolveHelpBotProfile,
11
11
  resolveHelpImageIntent,
12
+ resolveViewerRole,
12
13
  } from "./help";
13
14
  import { getRenderVersions } from "./utils";
14
15
  import { resetHelpRuntimeState, setHelpRuntimeState } from "./runtime";
@@ -130,6 +131,7 @@ const helpPlugin = definePlugin({
130
131
 
131
132
  try {
132
133
  const { botNickname, botAvatarUrl } = resolveHelpBotProfile(ctx, event);
134
+ const viewerRole = await resolveViewerRole(ctx, event);
133
135
  const imagePath = await generateHelpImage({
134
136
  helpService,
135
137
  screenshotService,
@@ -139,6 +141,7 @@ const helpPlugin = definePlugin({
139
141
  botAvatarUrl,
140
142
  targetPluginName:
141
143
  intent.type === "detail" ? intent.pluginName : undefined,
144
+ viewerRole,
142
145
  });
143
146
 
144
147
  if (!imagePath) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-plugin-help",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "帮助插件,使用截图服务生成美观的帮助图片,并提供 #状态 指令",
5
5
  "main": "index.ts",
6
6
  "type": "module",