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.
- package/help/html-generator.ts +16 -3
- package/help/image.ts +8 -1
- package/help/index.ts +1 -0
- package/help/intent.ts +16 -3
- package/help/role.ts +83 -0
- package/index.ts +3 -0
- package/package.json +1 -1
package/help/html-generator.ts
CHANGED
|
@@ -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
|
|
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 =
|
|
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) {
|