mioku-plugin-help 2.0.0 → 2.1.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/help/html-generator.ts +556 -0
- package/help/image.ts +216 -0
- package/help/index.ts +24 -0
- package/help/info.ts +41 -0
- package/help/intent.ts +379 -0
- package/help/role-config.ts +67 -0
- package/help/types.ts +40 -0
- package/index.ts +69 -11
- package/package.json +4 -3
- package/skills.ts +132 -77
- package/status/data-collector.ts +752 -0
- package/status/html-generator.ts +1202 -0
- package/status/image.ts +81 -0
- package/status/index.ts +28 -0
- package/status/intent.ts +43 -0
- package/status/network-sampler.ts +129 -0
- package/status/performance-monitor.ts +102 -0
- package/status/types.ts +264 -0
- package/theme.ts +138 -0
- package/utils.ts +129 -0
- package/shared.ts +0 -1276
package/shared.ts
DELETED
|
@@ -1,1276 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import type { CommandRole, PluginHelp } from "mioku";
|
|
3
|
-
import type { HelpService } from "mioku";
|
|
4
|
-
import type { ScreenshotService } from "mioku";
|
|
5
|
-
|
|
6
|
-
const ROLE_CONFIG: Record<
|
|
7
|
-
CommandRole,
|
|
8
|
-
{
|
|
9
|
-
label: string;
|
|
10
|
-
badgeBgLight: string;
|
|
11
|
-
badgeBorderLight: string;
|
|
12
|
-
badgeTextLight: string;
|
|
13
|
-
badgeBgDark: string;
|
|
14
|
-
badgeBorderDark: string;
|
|
15
|
-
badgeTextDark: string;
|
|
16
|
-
}
|
|
17
|
-
> = {
|
|
18
|
-
master: {
|
|
19
|
-
label: "主人",
|
|
20
|
-
badgeBgLight: "rgba(245, 158, 11, 0.14)",
|
|
21
|
-
badgeBorderLight: "rgba(217, 119, 6, 0.24)",
|
|
22
|
-
badgeTextLight: "#92400e",
|
|
23
|
-
badgeBgDark: "rgba(245, 158, 11, 0.18)",
|
|
24
|
-
badgeBorderDark: "rgba(251, 191, 36, 0.28)",
|
|
25
|
-
badgeTextDark: "#fcd34d",
|
|
26
|
-
},
|
|
27
|
-
admin: {
|
|
28
|
-
label: "管理",
|
|
29
|
-
badgeBgLight: "rgba(239, 68, 68, 0.12)",
|
|
30
|
-
badgeBorderLight: "rgba(220, 38, 38, 0.22)",
|
|
31
|
-
badgeTextLight: "#b91c1c",
|
|
32
|
-
badgeBgDark: "rgba(239, 68, 68, 0.16)",
|
|
33
|
-
badgeBorderDark: "rgba(248, 113, 113, 0.22)",
|
|
34
|
-
badgeTextDark: "#fca5a5",
|
|
35
|
-
},
|
|
36
|
-
owner: {
|
|
37
|
-
label: "管理",
|
|
38
|
-
badgeBgLight: "rgba(239, 68, 68, 0.12)",
|
|
39
|
-
badgeBorderLight: "rgba(220, 38, 38, 0.22)",
|
|
40
|
-
badgeTextLight: "#b91c1c",
|
|
41
|
-
badgeBgDark: "rgba(239, 68, 68, 0.16)",
|
|
42
|
-
badgeBorderDark: "rgba(248, 113, 113, 0.22)",
|
|
43
|
-
badgeTextDark: "#fca5a5",
|
|
44
|
-
},
|
|
45
|
-
member: {
|
|
46
|
-
label: "成员",
|
|
47
|
-
badgeBgLight: "rgba(14, 165, 233, 0.12)",
|
|
48
|
-
badgeBorderLight: "rgba(2, 132, 199, 0.2)",
|
|
49
|
-
badgeTextLight: "#0369a1",
|
|
50
|
-
badgeBgDark: "rgba(56, 189, 248, 0.16)",
|
|
51
|
-
badgeBorderDark: "rgba(103, 232, 249, 0.22)",
|
|
52
|
-
badgeTextDark: "#67e8f9",
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const STOPWORDS = new Set(["help", "帮助", "菜单"]);
|
|
57
|
-
|
|
58
|
-
interface HelpKeywordCandidate {
|
|
59
|
-
keyword: string;
|
|
60
|
-
strictUnknown: boolean;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
interface HelpRenderableEntry {
|
|
64
|
-
pluginName: string;
|
|
65
|
-
title: string;
|
|
66
|
-
description: string;
|
|
67
|
-
commands: PluginHelp["commands"];
|
|
68
|
-
normalizedPluginName: string;
|
|
69
|
-
normalizedTitle: string;
|
|
70
|
-
matchKeys: Set<string>;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
interface HelpTheme {
|
|
74
|
-
pageBg: string;
|
|
75
|
-
shellBg: string;
|
|
76
|
-
pageAccent: string;
|
|
77
|
-
pageGrid: string;
|
|
78
|
-
sceneOpacity: string;
|
|
79
|
-
sceneFilter: string;
|
|
80
|
-
sceneMask: string;
|
|
81
|
-
sceneGlow: string;
|
|
82
|
-
shellBorder: string;
|
|
83
|
-
shellShadow: string;
|
|
84
|
-
heroBg: string;
|
|
85
|
-
heroBorder: string;
|
|
86
|
-
heroGlow: string;
|
|
87
|
-
eyebrow: string;
|
|
88
|
-
title: string;
|
|
89
|
-
subtitle: string;
|
|
90
|
-
panelBg: string;
|
|
91
|
-
panelBorder: string;
|
|
92
|
-
panelShadow: string;
|
|
93
|
-
panelTitle: string;
|
|
94
|
-
panelDesc: string;
|
|
95
|
-
commandBg: string;
|
|
96
|
-
commandBorder: string;
|
|
97
|
-
commandTitle: string;
|
|
98
|
-
commandDesc: string;
|
|
99
|
-
tagBg: string;
|
|
100
|
-
tagBorder: string;
|
|
101
|
-
tagText: string;
|
|
102
|
-
emptyText: string;
|
|
103
|
-
footerBg: string;
|
|
104
|
-
footerBorder: string;
|
|
105
|
-
footerLabel: string;
|
|
106
|
-
footerText: string;
|
|
107
|
-
divider: string;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export type HelpImageIntent =
|
|
111
|
-
| { type: "none" }
|
|
112
|
-
| { type: "overview" }
|
|
113
|
-
| { type: "detail"; pluginName: string; pluginHelp: PluginHelp }
|
|
114
|
-
| { type: "unknown"; keyword: string };
|
|
115
|
-
|
|
116
|
-
export async function getPackageVersion(
|
|
117
|
-
packageJsonPath: string,
|
|
118
|
-
): Promise<string> {
|
|
119
|
-
try {
|
|
120
|
-
const content = await fs.promises.readFile(packageJsonPath, "utf-8");
|
|
121
|
-
const pkg = JSON.parse(content);
|
|
122
|
-
return pkg.version || "unknown";
|
|
123
|
-
} catch {
|
|
124
|
-
return "unknown";
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export async function getHelpRenderVersions(): Promise<{
|
|
129
|
-
miokiVersion: string;
|
|
130
|
-
miokuVersion: string;
|
|
131
|
-
}> {
|
|
132
|
-
const miokiVersion = await getPackageVersion(
|
|
133
|
-
`${process.cwd()}/node_modules/mioki/package.json`,
|
|
134
|
-
);
|
|
135
|
-
const miokuVersion = await getPackageVersion(`${process.cwd()}/package.json`);
|
|
136
|
-
|
|
137
|
-
return {
|
|
138
|
-
miokiVersion,
|
|
139
|
-
miokuVersion,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export function checkNightMode(): boolean {
|
|
144
|
-
const hour = new Date().getHours();
|
|
145
|
-
return hour >= 19 || hour < 7;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export function buildHelpInfoText(helpMap: Map<string, PluginHelp>): string {
|
|
149
|
-
const info: string[] = ["=== Mioku Bot 帮助信息 ===\n"];
|
|
150
|
-
|
|
151
|
-
for (const [pluginName, help] of helpMap) {
|
|
152
|
-
info.push(`【${help.title || pluginName}】${help.description || ""}`);
|
|
153
|
-
if (help.commands?.length) {
|
|
154
|
-
for (const cmd of help.commands) {
|
|
155
|
-
const roleLabel = cmd.role
|
|
156
|
-
? ` [${ROLE_CONFIG[cmd.role]?.label || cmd.role}]`
|
|
157
|
-
: "";
|
|
158
|
-
info.push(` ${cmd.cmd}${roleLabel} - ${cmd.desc}`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
info.push("");
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return info.join("\n");
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export function resolveHelpImageIntent(
|
|
168
|
-
text: string,
|
|
169
|
-
helpMap: Map<string, PluginHelp>,
|
|
170
|
-
): HelpImageIntent {
|
|
171
|
-
const source = String(text || "").trim();
|
|
172
|
-
if (!source) {
|
|
173
|
-
return { type: "none" };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (/^[#/]/.test(source) && !/^[#/]\s*(?:help|帮助|菜单)/i.test(source)) {
|
|
177
|
-
return { type: "none" };
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (/^[#/]?\s*(?:help|帮助|菜单)\s*$/i.test(source)) {
|
|
181
|
-
return { type: "overview" };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const candidates = extractHelpKeywordCandidates(source);
|
|
185
|
-
if (candidates.length === 0) {
|
|
186
|
-
return { type: "none" };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
for (const candidate of candidates) {
|
|
190
|
-
const resolved = findPluginHelpByKeyword(helpMap, candidate.keyword);
|
|
191
|
-
if (resolved) {
|
|
192
|
-
return {
|
|
193
|
-
type: "detail",
|
|
194
|
-
pluginName: resolved.pluginName,
|
|
195
|
-
pluginHelp: resolved.help,
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const strictCandidate = candidates.find(
|
|
201
|
-
(candidate) => candidate.strictUnknown,
|
|
202
|
-
);
|
|
203
|
-
if (!strictCandidate) {
|
|
204
|
-
return { type: "none" };
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const fallback = sanitizeKeyword(strictCandidate.keyword);
|
|
208
|
-
if (!fallback) {
|
|
209
|
-
return { type: "none" };
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return {
|
|
213
|
-
type: "unknown",
|
|
214
|
-
keyword: fallback,
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export function findPluginHelpByKeyword(
|
|
219
|
-
helpMap: Map<string, PluginHelp>,
|
|
220
|
-
keyword: string,
|
|
221
|
-
): { pluginName: string; help: PluginHelp } | null {
|
|
222
|
-
const normalizedQuery = normalizeForMatch(sanitizeKeyword(keyword));
|
|
223
|
-
if (!normalizedQuery || STOPWORDS.has(normalizedQuery)) {
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const entries = getRenderableEntries(helpMap);
|
|
228
|
-
if (entries.length === 0) {
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const directPlugin = entries.find(
|
|
233
|
-
(entry) => entry.normalizedPluginName === normalizedQuery,
|
|
234
|
-
);
|
|
235
|
-
if (directPlugin) {
|
|
236
|
-
return {
|
|
237
|
-
pluginName: directPlugin.pluginName,
|
|
238
|
-
help: helpMap.get(directPlugin.pluginName)!,
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const exactMatches = entries.filter((entry) =>
|
|
243
|
-
entry.matchKeys.has(normalizedQuery),
|
|
244
|
-
);
|
|
245
|
-
if (exactMatches.length === 1) {
|
|
246
|
-
return {
|
|
247
|
-
pluginName: exactMatches[0].pluginName,
|
|
248
|
-
help: helpMap.get(exactMatches[0].pluginName)!,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (normalizedQuery.length < 2) {
|
|
253
|
-
return null;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const fuzzyMatches = entries
|
|
257
|
-
.map((entry) => ({
|
|
258
|
-
entry,
|
|
259
|
-
score: scoreKeywordMatch(normalizedQuery, entry),
|
|
260
|
-
}))
|
|
261
|
-
.filter((item) => item.score > 0)
|
|
262
|
-
.sort((a, b) => b.score - a.score);
|
|
263
|
-
|
|
264
|
-
if (fuzzyMatches.length === 0) {
|
|
265
|
-
return null;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (
|
|
269
|
-
fuzzyMatches.length > 1 &&
|
|
270
|
-
fuzzyMatches[0].score === fuzzyMatches[1].score
|
|
271
|
-
) {
|
|
272
|
-
return null;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const resolved = fuzzyMatches[0].entry;
|
|
276
|
-
return {
|
|
277
|
-
pluginName: resolved.pluginName,
|
|
278
|
-
help: helpMap.get(resolved.pluginName)!,
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
export async function generateHelpImage(options: {
|
|
283
|
-
helpService?: HelpService;
|
|
284
|
-
screenshotService?: ScreenshotService;
|
|
285
|
-
miokiVersion?: string;
|
|
286
|
-
miokuVersion?: string;
|
|
287
|
-
botNickname?: string;
|
|
288
|
-
botAvatarUrl?: string;
|
|
289
|
-
targetPluginName?: string;
|
|
290
|
-
}): Promise<string | null> {
|
|
291
|
-
const {
|
|
292
|
-
helpService,
|
|
293
|
-
screenshotService,
|
|
294
|
-
miokiVersion,
|
|
295
|
-
miokuVersion,
|
|
296
|
-
botNickname,
|
|
297
|
-
botAvatarUrl,
|
|
298
|
-
targetPluginName,
|
|
299
|
-
} = options;
|
|
300
|
-
if (!helpService || !screenshotService) {
|
|
301
|
-
return null;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const allHelp = helpService.getAllHelp();
|
|
305
|
-
const hasTarget =
|
|
306
|
-
Boolean(targetPluginName) && allHelp.has(String(targetPluginName));
|
|
307
|
-
|
|
308
|
-
const htmlContent = generateHelpHtml(
|
|
309
|
-
allHelp,
|
|
310
|
-
checkNightMode(),
|
|
311
|
-
miokiVersion,
|
|
312
|
-
miokuVersion,
|
|
313
|
-
botNickname,
|
|
314
|
-
botAvatarUrl,
|
|
315
|
-
hasTarget ? targetPluginName : undefined,
|
|
316
|
-
);
|
|
317
|
-
|
|
318
|
-
return screenshotService.screenshot(htmlContent, {
|
|
319
|
-
width: 760,
|
|
320
|
-
height: 120,
|
|
321
|
-
fullPage: true,
|
|
322
|
-
type: "png",
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
export async function replyWithImage(
|
|
327
|
-
event: any,
|
|
328
|
-
segment: { image: (file: string) => any } | undefined,
|
|
329
|
-
imagePath: string,
|
|
330
|
-
): Promise<void> {
|
|
331
|
-
if (!event?.reply) {
|
|
332
|
-
throw new Error("当前上下文不支持发送图片回复");
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
try {
|
|
336
|
-
if (segment?.image) {
|
|
337
|
-
await event.reply(segment.image(imagePath));
|
|
338
|
-
} else {
|
|
339
|
-
await event.reply([{ type: "image", file: imagePath }]);
|
|
340
|
-
}
|
|
341
|
-
} catch {
|
|
342
|
-
const imageBuffer = await fs.promises.readFile(imagePath);
|
|
343
|
-
const base64Image = `base64://${imageBuffer.toString("base64")}`;
|
|
344
|
-
|
|
345
|
-
if (segment?.image) {
|
|
346
|
-
await event.reply(segment.image(base64Image));
|
|
347
|
-
} else {
|
|
348
|
-
await event.reply([{ type: "image", file: base64Image }]);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
export async function sendImageFromSkillContext(options: {
|
|
354
|
-
ctx: any;
|
|
355
|
-
event: any;
|
|
356
|
-
imagePath: string;
|
|
357
|
-
quoteReply?: boolean;
|
|
358
|
-
}): Promise<void> {
|
|
359
|
-
const { ctx, event, imagePath, quoteReply = false } = options;
|
|
360
|
-
const selfId = event?.self_id != null ? Number(event.self_id) : undefined;
|
|
361
|
-
const bot =
|
|
362
|
-
selfId != null && typeof ctx?.pickBot === "function"
|
|
363
|
-
? ctx.pickBot(selfId)
|
|
364
|
-
: undefined;
|
|
365
|
-
|
|
366
|
-
if (!bot) {
|
|
367
|
-
throw new Error("当前上下文不支持发送图片");
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const buildImageSegment = (file: string) => {
|
|
371
|
-
const normalizedFile = normalizeImageSource(file);
|
|
372
|
-
if (ctx?.segment?.image) {
|
|
373
|
-
return ctx.segment.image(normalizedFile);
|
|
374
|
-
}
|
|
375
|
-
return { type: "image", file: normalizedFile };
|
|
376
|
-
};
|
|
377
|
-
|
|
378
|
-
const sendPayload = async (file: string) => {
|
|
379
|
-
const payload: any[] = [];
|
|
380
|
-
if (quoteReply && event?.message_id != null) {
|
|
381
|
-
payload.push({ type: "reply", id: String(event.message_id) });
|
|
382
|
-
}
|
|
383
|
-
payload.push(buildImageSegment(file));
|
|
384
|
-
|
|
385
|
-
if (event?.message_type === "group" && event?.group_id != null) {
|
|
386
|
-
await bot.sendGroupMsg(event.group_id, payload);
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
if (event?.user_id != null) {
|
|
391
|
-
await bot.sendPrivateMsg(event.user_id, payload);
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
throw new Error("当前上下文不支持发送图片");
|
|
396
|
-
};
|
|
397
|
-
|
|
398
|
-
try {
|
|
399
|
-
await sendPayload(imagePath);
|
|
400
|
-
} catch (error) {
|
|
401
|
-
if (!isLocalFilePath(imagePath)) {
|
|
402
|
-
throw error;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const imageBuffer = await fs.promises.readFile(imagePath);
|
|
406
|
-
const base64Image = `base64://${imageBuffer.toString("base64")}`;
|
|
407
|
-
await sendPayload(base64Image);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
export function generateHelpHtml(
|
|
412
|
-
helpMap: Map<string, PluginHelp>,
|
|
413
|
-
isNightMode: boolean,
|
|
414
|
-
miokiVersion: string = "unknown",
|
|
415
|
-
miokuVersion: string = "unknown",
|
|
416
|
-
botNickname: string = "Mioku Bot",
|
|
417
|
-
botAvatarUrl?: string,
|
|
418
|
-
targetPluginName?: string,
|
|
419
|
-
): string {
|
|
420
|
-
const entries = getRenderableEntries(helpMap);
|
|
421
|
-
const selectedEntry = targetPluginName
|
|
422
|
-
? entries.find((entry) => entry.pluginName === targetPluginName)
|
|
423
|
-
: undefined;
|
|
424
|
-
|
|
425
|
-
const isDetailMode = Boolean(selectedEntry);
|
|
426
|
-
const logoPath = "../../plugins/help/source/miku.png";
|
|
427
|
-
const avatarSrc = botAvatarUrl || logoPath;
|
|
428
|
-
const backgroundImageUrl =
|
|
429
|
-
"https://uapis.cn/api/v1/random/image?category=acg&type=mb";
|
|
430
|
-
const theme = getHelpTheme(isNightMode);
|
|
431
|
-
|
|
432
|
-
const pluginsHtml = isDetailMode
|
|
433
|
-
? renderPluginDetail(selectedEntry!, isNightMode)
|
|
434
|
-
: entries.map((entry) => renderPluginOverview(entry)).join("");
|
|
435
|
-
|
|
436
|
-
const heroTitle = isDetailMode
|
|
437
|
-
? `${botNickname} · ${selectedEntry!.title}`
|
|
438
|
-
: botNickname;
|
|
439
|
-
const heroSubtitle = isDetailMode
|
|
440
|
-
? `${selectedEntry!.pluginName} 插件,共 ${selectedEntry!.commands.length} 条命令`
|
|
441
|
-
: `共 ${entries.length} 个插件 发送 <插件名>帮助查看详细信息`;
|
|
442
|
-
|
|
443
|
-
return `
|
|
444
|
-
<style>
|
|
445
|
-
.help-sheet {
|
|
446
|
-
padding: 18px;
|
|
447
|
-
display: flex;
|
|
448
|
-
flex-direction: column;
|
|
449
|
-
gap: 14px;
|
|
450
|
-
position: relative;
|
|
451
|
-
overflow: hidden;
|
|
452
|
-
background: ${theme.pageBg};
|
|
453
|
-
color: ${theme.panelTitle};
|
|
454
|
-
font-family: "SF Pro Display", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
.help-sheet::before,
|
|
458
|
-
.help-sheet::after {
|
|
459
|
-
content: "";
|
|
460
|
-
position: absolute;
|
|
461
|
-
inset: 0;
|
|
462
|
-
pointer-events: none;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
.help-sheet::before {
|
|
466
|
-
background: ${theme.pageAccent};
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
.help-sheet::after {
|
|
470
|
-
background-image: ${theme.pageGrid};
|
|
471
|
-
background-size: 28px 28px;
|
|
472
|
-
opacity: ${isNightMode ? "0.55" : "0.35"};
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
.help-sheet__scene,
|
|
476
|
-
.help-sheet__scene-image,
|
|
477
|
-
.help-sheet__scene-overlay {
|
|
478
|
-
position: absolute;
|
|
479
|
-
inset: 0;
|
|
480
|
-
pointer-events: none;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
.help-sheet__scene {
|
|
484
|
-
z-index: 0;
|
|
485
|
-
overflow: hidden;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
.help-sheet__scene-image {
|
|
489
|
-
background-image: url("${backgroundImageUrl}");
|
|
490
|
-
background-size: cover;
|
|
491
|
-
background-position: center center;
|
|
492
|
-
opacity: ${theme.sceneOpacity};
|
|
493
|
-
filter: ${theme.sceneFilter};
|
|
494
|
-
transform: scale(1.06);
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
.help-sheet__scene-overlay {
|
|
498
|
-
background: ${theme.sceneGlow}, ${theme.sceneMask};
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
.help-shell {
|
|
502
|
-
position: relative;
|
|
503
|
-
z-index: 1;
|
|
504
|
-
display: flex;
|
|
505
|
-
flex-direction: column;
|
|
506
|
-
gap: 14px;
|
|
507
|
-
border-radius: 30px;
|
|
508
|
-
border: 1px solid ${theme.shellBorder};
|
|
509
|
-
box-shadow: ${theme.shellShadow};
|
|
510
|
-
padding: 14px;
|
|
511
|
-
background: ${theme.shellBg};
|
|
512
|
-
backdrop-filter: blur(10px) saturate(1.06);
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
.help-hero {
|
|
516
|
-
position: relative;
|
|
517
|
-
display: flex;
|
|
518
|
-
align-items: center;
|
|
519
|
-
gap: 16px;
|
|
520
|
-
padding: 18px 18px 16px;
|
|
521
|
-
border-radius: 24px;
|
|
522
|
-
border: 1px solid ${theme.heroBorder};
|
|
523
|
-
background: ${theme.heroBg};
|
|
524
|
-
overflow: hidden;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
.help-hero::before {
|
|
528
|
-
content: "";
|
|
529
|
-
position: absolute;
|
|
530
|
-
inset: auto auto -42px -32px;
|
|
531
|
-
width: 180px;
|
|
532
|
-
height: 180px;
|
|
533
|
-
border-radius: 999px;
|
|
534
|
-
background: ${theme.heroGlow};
|
|
535
|
-
filter: blur(4px);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
.help-hero::after {
|
|
539
|
-
content: "";
|
|
540
|
-
position: absolute;
|
|
541
|
-
inset: -70px -50px auto auto;
|
|
542
|
-
width: 220px;
|
|
543
|
-
height: 220px;
|
|
544
|
-
border-radius: 999px;
|
|
545
|
-
background: ${theme.heroGlow};
|
|
546
|
-
filter: blur(18px);
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
.help-hero__logo {
|
|
550
|
-
position: relative;
|
|
551
|
-
z-index: 1;
|
|
552
|
-
width: 92px;
|
|
553
|
-
height: 92px;
|
|
554
|
-
flex-shrink: 0;
|
|
555
|
-
border-radius: 999px;
|
|
556
|
-
overflow: hidden;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
.help-hero__logo img {
|
|
560
|
-
width: 100%;
|
|
561
|
-
height: 100%;
|
|
562
|
-
object-fit: cover;
|
|
563
|
-
border-radius: 999px;
|
|
564
|
-
box-shadow: 0 10px 24px ${isNightMode ? "rgba(0, 0, 0, 0.32)" : "rgba(15, 61, 71, 0.14)"};
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
.help-hero__content {
|
|
568
|
-
position: relative;
|
|
569
|
-
z-index: 1;
|
|
570
|
-
min-width: 0;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
.help-hero__eyebrow {
|
|
574
|
-
margin-bottom: 8px;
|
|
575
|
-
font-size: 12px;
|
|
576
|
-
font-weight: 700;
|
|
577
|
-
letter-spacing: 0.18em;
|
|
578
|
-
text-transform: uppercase;
|
|
579
|
-
color: ${theme.eyebrow};
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
.help-hero__title {
|
|
583
|
-
margin: 0;
|
|
584
|
-
font-size: 34px;
|
|
585
|
-
line-height: 1.06;
|
|
586
|
-
font-weight: 900;
|
|
587
|
-
letter-spacing: -0.04em;
|
|
588
|
-
color: ${theme.title};
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
.help-hero__subtitle {
|
|
592
|
-
margin: 10px 0 0;
|
|
593
|
-
max-width: 520px;
|
|
594
|
-
font-size: 13px;
|
|
595
|
-
line-height: 1.6;
|
|
596
|
-
color: ${theme.subtitle};
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
.help-grid {
|
|
600
|
-
column-count: 2;
|
|
601
|
-
column-gap: 12px;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
.help-detail {
|
|
605
|
-
display: block;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
.help-plugin {
|
|
609
|
-
display: flex;
|
|
610
|
-
flex-direction: column;
|
|
611
|
-
min-height: 0;
|
|
612
|
-
border-radius: 20px;
|
|
613
|
-
border: 1px solid ${theme.panelBorder};
|
|
614
|
-
background: ${theme.panelBg};
|
|
615
|
-
box-shadow: ${theme.panelShadow};
|
|
616
|
-
overflow: hidden;
|
|
617
|
-
break-inside: avoid;
|
|
618
|
-
margin-bottom: 12px;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
.help-plugin__head {
|
|
622
|
-
padding: 14px 14px 12px;
|
|
623
|
-
border-bottom: 1px solid ${theme.panelBorder};
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
.help-plugin__title-row {
|
|
627
|
-
display: flex;
|
|
628
|
-
align-items: center;
|
|
629
|
-
gap: 8px;
|
|
630
|
-
min-width: 0;
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
.help-plugin__title {
|
|
634
|
-
margin: 0;
|
|
635
|
-
min-width: 0;
|
|
636
|
-
font-size: 16px;
|
|
637
|
-
line-height: 1.25;
|
|
638
|
-
font-weight: 800;
|
|
639
|
-
color: ${theme.panelTitle};
|
|
640
|
-
word-break: break-word;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
.help-plugin__alias {
|
|
644
|
-
flex-shrink: 0;
|
|
645
|
-
padding: 2px 8px;
|
|
646
|
-
border-radius: 999px;
|
|
647
|
-
border: 1px solid ${theme.tagBorder};
|
|
648
|
-
background: ${theme.tagBg};
|
|
649
|
-
color: ${theme.tagText};
|
|
650
|
-
font-size: 11px;
|
|
651
|
-
line-height: 1.5;
|
|
652
|
-
font-family: "SF Mono", "JetBrains Mono", "Fira Code", monospace;
|
|
653
|
-
font-weight: 700;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
.help-plugin__desc {
|
|
657
|
-
margin: 8px 0 0;
|
|
658
|
-
font-size: 12px;
|
|
659
|
-
line-height: 1.5;
|
|
660
|
-
color: ${theme.panelDesc};
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
.help-plugin__body {
|
|
664
|
-
padding: 10px;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
.help-plugin__empty {
|
|
668
|
-
padding: 20px 16px;
|
|
669
|
-
text-align: center;
|
|
670
|
-
font-size: 12px;
|
|
671
|
-
color: ${theme.emptyText};
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
.help-command-tags {
|
|
675
|
-
display: flex;
|
|
676
|
-
flex-wrap: wrap;
|
|
677
|
-
gap: 6px;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
.help-command-tag {
|
|
681
|
-
display: inline-flex;
|
|
682
|
-
align-items: center;
|
|
683
|
-
max-width: 100%;
|
|
684
|
-
padding: 4px 9px;
|
|
685
|
-
border-radius: 12px;
|
|
686
|
-
border: 1px solid ${theme.commandBorder};
|
|
687
|
-
background: ${theme.commandBg};
|
|
688
|
-
color: ${theme.commandTitle};
|
|
689
|
-
font-size: 11px;
|
|
690
|
-
line-height: 1.4;
|
|
691
|
-
font-family: "SF Mono", "JetBrains Mono", "Fira Code", monospace;
|
|
692
|
-
font-weight: 700;
|
|
693
|
-
white-space: nowrap;
|
|
694
|
-
overflow: hidden;
|
|
695
|
-
text-overflow: ellipsis;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
.help-plugin--detail .help-plugin__head {
|
|
699
|
-
padding: 16px 16px 14px;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
.help-plugin--detail .help-plugin__body {
|
|
703
|
-
padding: 12px;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
.help-command {
|
|
707
|
-
padding: 12px;
|
|
708
|
-
border-radius: 16px;
|
|
709
|
-
background: ${theme.commandBg};
|
|
710
|
-
border: 1px solid ${theme.commandBorder};
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
.help-command + .help-command {
|
|
714
|
-
margin-top: 10px;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
.help-command__top {
|
|
718
|
-
display: flex;
|
|
719
|
-
align-items: flex-start;
|
|
720
|
-
justify-content: space-between;
|
|
721
|
-
gap: 10px;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
.help-command__name {
|
|
725
|
-
flex: 1;
|
|
726
|
-
min-width: 0;
|
|
727
|
-
font-family: "SF Mono", "JetBrains Mono", "Fira Code", monospace;
|
|
728
|
-
font-size: 13px;
|
|
729
|
-
line-height: 1.45;
|
|
730
|
-
font-weight: 800;
|
|
731
|
-
color: ${theme.commandTitle};
|
|
732
|
-
word-break: break-word;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
.help-command__desc {
|
|
736
|
-
margin-top: 6px;
|
|
737
|
-
font-size: 12px;
|
|
738
|
-
line-height: 1.6;
|
|
739
|
-
color: ${theme.commandDesc};
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
.help-command__usage {
|
|
743
|
-
margin-top: 8px;
|
|
744
|
-
font-size: 11px;
|
|
745
|
-
line-height: 1.55;
|
|
746
|
-
color: ${theme.panelDesc};
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
.help-command__usage code {
|
|
750
|
-
font-family: "SF Mono", "JetBrains Mono", "Fira Code", monospace;
|
|
751
|
-
font-size: 11px;
|
|
752
|
-
color: ${theme.commandTitle};
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
.help-role {
|
|
756
|
-
flex-shrink: 0;
|
|
757
|
-
padding: 2px 8px;
|
|
758
|
-
border-radius: 999px;
|
|
759
|
-
font-size: 10px;
|
|
760
|
-
line-height: 1.5;
|
|
761
|
-
font-weight: 700;
|
|
762
|
-
letter-spacing: 0.02em;
|
|
763
|
-
border: 1px solid transparent;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
.help-footer {
|
|
767
|
-
display: flex;
|
|
768
|
-
align-items: stretch;
|
|
769
|
-
gap: 0;
|
|
770
|
-
border-radius: 20px;
|
|
771
|
-
border: 1px solid ${theme.footerBorder};
|
|
772
|
-
background: ${theme.footerBg};
|
|
773
|
-
overflow: hidden;
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
.help-footer__item {
|
|
777
|
-
flex: 1;
|
|
778
|
-
display: flex;
|
|
779
|
-
align-items: center;
|
|
780
|
-
gap: 12px;
|
|
781
|
-
padding: 14px 16px;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
.help-footer__item + .help-footer__item {
|
|
785
|
-
border-left: 1px solid ${theme.divider};
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
.help-footer__icon {
|
|
789
|
-
width: 36px;
|
|
790
|
-
height: 36px;
|
|
791
|
-
flex-shrink: 0;
|
|
792
|
-
display: grid;
|
|
793
|
-
place-items: center;
|
|
794
|
-
border-radius: 12px;
|
|
795
|
-
background: ${isNightMode ? "rgba(126, 231, 221, 0.08)" : "rgba(15, 118, 110, 0.08)"};
|
|
796
|
-
color: ${theme.eyebrow};
|
|
797
|
-
font-size: 18px;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
.help-footer__label {
|
|
801
|
-
font-size: 11px;
|
|
802
|
-
line-height: 1.4;
|
|
803
|
-
color: ${theme.footerLabel};
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
.help-footer__value {
|
|
807
|
-
margin-top: 2px;
|
|
808
|
-
font-family: "SF Mono", "JetBrains Mono", "Fira Code", monospace;
|
|
809
|
-
font-size: 12px;
|
|
810
|
-
line-height: 1.45;
|
|
811
|
-
font-weight: 700;
|
|
812
|
-
color: ${theme.footerText};
|
|
813
|
-
}
|
|
814
|
-
</style>
|
|
815
|
-
<div class="help-sheet">
|
|
816
|
-
<div class="help-sheet__scene">
|
|
817
|
-
<div class="help-sheet__scene-image"></div>
|
|
818
|
-
<div class="help-sheet__scene-overlay"></div>
|
|
819
|
-
</div>
|
|
820
|
-
<div class="help-shell">
|
|
821
|
-
<header class="help-hero">
|
|
822
|
-
<div class="help-hero__logo">
|
|
823
|
-
<img src="${escapeHtml(avatarSrc)}" alt="logo" />
|
|
824
|
-
</div>
|
|
825
|
-
<div class="help-hero__content">
|
|
826
|
-
<div class="help-hero__eyebrow">Mioku Assistant</div>
|
|
827
|
-
<h1 class="help-hero__title">${escapeHtml(heroTitle)}</h1>
|
|
828
|
-
<p class="help-hero__subtitle">${escapeHtml(heroSubtitle)}</p>
|
|
829
|
-
</div>
|
|
830
|
-
</header>
|
|
831
|
-
|
|
832
|
-
<main class="${isDetailMode ? "help-detail" : "help-grid"}">
|
|
833
|
-
${pluginsHtml}
|
|
834
|
-
</main>
|
|
835
|
-
|
|
836
|
-
<footer class="help-footer">
|
|
837
|
-
<div class="help-footer__item">
|
|
838
|
-
<div class="help-footer__icon">⚡</div>
|
|
839
|
-
<div>
|
|
840
|
-
<div class="help-footer__label">Framework</div>
|
|
841
|
-
<div class="help-footer__value">Mioki ${escapeHtml(miokiVersion)}</div>
|
|
842
|
-
</div>
|
|
843
|
-
</div>
|
|
844
|
-
<div class="help-footer__item">
|
|
845
|
-
<div class="help-footer__icon">🚀</div>
|
|
846
|
-
<div>
|
|
847
|
-
<div class="help-footer__label">Platform</div>
|
|
848
|
-
<div class="help-footer__value">Mioku ${escapeHtml(miokuVersion)}</div>
|
|
849
|
-
</div>
|
|
850
|
-
</div>
|
|
851
|
-
</footer>
|
|
852
|
-
</div>
|
|
853
|
-
</div>
|
|
854
|
-
`;
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
export function resolveHelpBotProfile(
|
|
858
|
-
ctx: any,
|
|
859
|
-
event?: any,
|
|
860
|
-
): { botNickname: string; botAvatarUrl?: string } {
|
|
861
|
-
const fallbackNickname = "Mioku Bot";
|
|
862
|
-
const selfId = event?.self_id;
|
|
863
|
-
const bot =
|
|
864
|
-
(selfId && typeof ctx?.pickBot === "function"
|
|
865
|
-
? ctx.pickBot(selfId)
|
|
866
|
-
: null) ||
|
|
867
|
-
(ctx?.bots instanceof Map ? Array.from(ctx.bots.values())[0] : null);
|
|
868
|
-
const botId = selfId || bot?.uin || bot?.user_id || bot?.self_id;
|
|
869
|
-
const botNickname = bot?.nickname || bot?.name || fallbackNickname;
|
|
870
|
-
const botAvatarUrl = botId
|
|
871
|
-
? `https://q1.qlogo.cn/g?b=qq&nk=${botId}&s=640`
|
|
872
|
-
: undefined;
|
|
873
|
-
|
|
874
|
-
return { botNickname, botAvatarUrl };
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
function extractHelpKeywordCandidates(text: string): HelpKeywordCandidate[] {
|
|
878
|
-
const source = text.trim();
|
|
879
|
-
const candidates: HelpKeywordCandidate[] = [];
|
|
880
|
-
|
|
881
|
-
const addCandidate = (value: string, strictUnknown: boolean) => {
|
|
882
|
-
const keyword = sanitizeKeyword(value);
|
|
883
|
-
if (!keyword) {
|
|
884
|
-
return;
|
|
885
|
-
}
|
|
886
|
-
if (
|
|
887
|
-
!candidates.some(
|
|
888
|
-
(candidate) =>
|
|
889
|
-
candidate.keyword === keyword &&
|
|
890
|
-
candidate.strictUnknown === strictUnknown,
|
|
891
|
-
)
|
|
892
|
-
) {
|
|
893
|
-
candidates.push({ keyword, strictUnknown });
|
|
894
|
-
}
|
|
895
|
-
};
|
|
896
|
-
|
|
897
|
-
const helpPrefixSeparated = source.match(
|
|
898
|
-
/^[#/]?\s*(?:help|帮助)(?:\s+|[::])\s*(.+)$/i,
|
|
899
|
-
);
|
|
900
|
-
if (helpPrefixSeparated?.[1]) {
|
|
901
|
-
addCandidate(helpPrefixSeparated[1], true);
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
const helpPrefixMerged = source.match(
|
|
905
|
-
/^[#/]?\s*(?:help|帮助)([a-z0-9\u4e00-\u9fa5]+)$/i,
|
|
906
|
-
);
|
|
907
|
-
if (helpPrefixMerged?.[1]) {
|
|
908
|
-
addCandidate(helpPrefixMerged[1], false);
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
if (/^[#/]/.test(source)) {
|
|
912
|
-
return candidates;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
const helpSuffixSeparated = source.match(/^(.+?)\s+(?:help|帮助)\s*$/i);
|
|
916
|
-
if (helpSuffixSeparated?.[1]) {
|
|
917
|
-
addCandidate(helpSuffixSeparated[1], true);
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
const helpSuffixMerged = source.match(
|
|
921
|
-
/^([a-z0-9\u4e00-\u9fa5]+)(?:help|帮助)\s*$/i,
|
|
922
|
-
);
|
|
923
|
-
if (helpSuffixMerged?.[1]) {
|
|
924
|
-
addCandidate(helpSuffixMerged[1], false);
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
const menuPrefix = source.match(
|
|
928
|
-
/^菜单\s*[::]?\s*([a-z0-9\u4e00-\u9fa5]+)$/i,
|
|
929
|
-
);
|
|
930
|
-
if (menuPrefix?.[1]) {
|
|
931
|
-
addCandidate(menuPrefix[1], false);
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
const menuSuffix = source.match(/^([a-z0-9\u4e00-\u9fa5]+)\s*菜单\s*$/i);
|
|
935
|
-
if (menuSuffix?.[1]) {
|
|
936
|
-
addCandidate(menuSuffix[1], false);
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
return candidates;
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
function getRenderableEntries(
|
|
943
|
-
helpMap: Map<string, PluginHelp>,
|
|
944
|
-
): HelpRenderableEntry[] {
|
|
945
|
-
return Array.from(helpMap.entries())
|
|
946
|
-
.map(([pluginName, help]) => {
|
|
947
|
-
const title = String(help.title || pluginName).trim() || pluginName;
|
|
948
|
-
const description = String(help.description || "").trim();
|
|
949
|
-
const commands = Array.isArray(help.commands) ? help.commands : [];
|
|
950
|
-
const normalizedPluginName = normalizeForMatch(pluginName);
|
|
951
|
-
const normalizedTitle = normalizeForMatch(title);
|
|
952
|
-
|
|
953
|
-
const keys = new Set<string>();
|
|
954
|
-
if (normalizedPluginName) {
|
|
955
|
-
keys.add(normalizedPluginName);
|
|
956
|
-
}
|
|
957
|
-
if (normalizedTitle) {
|
|
958
|
-
keys.add(normalizedTitle);
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
for (const token of extractMatchTokens(title)) {
|
|
962
|
-
keys.add(token);
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
const commandAliases = commands
|
|
966
|
-
.map((command) => extractCommandAlias(command.cmd))
|
|
967
|
-
.filter((value): value is string => Boolean(value));
|
|
968
|
-
for (const alias of commandAliases) {
|
|
969
|
-
keys.add(alias);
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
return {
|
|
973
|
-
pluginName,
|
|
974
|
-
title,
|
|
975
|
-
description,
|
|
976
|
-
commands,
|
|
977
|
-
normalizedPluginName,
|
|
978
|
-
normalizedTitle,
|
|
979
|
-
matchKeys: keys,
|
|
980
|
-
};
|
|
981
|
-
})
|
|
982
|
-
.sort((a, b) => a.pluginName.localeCompare(b.pluginName, "zh-Hans-CN"));
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
function renderPluginOverview(entry: HelpRenderableEntry): string {
|
|
986
|
-
const commandTags = entry.commands
|
|
987
|
-
.map(
|
|
988
|
-
(command) =>
|
|
989
|
-
`<span class="help-command-tag" title="${escapeHtml(command.desc || "")}" >${escapeHtml(command.cmd)}</span>`,
|
|
990
|
-
)
|
|
991
|
-
.join("");
|
|
992
|
-
|
|
993
|
-
return `
|
|
994
|
-
<section class="help-plugin help-plugin--overview">
|
|
995
|
-
<div class="help-plugin__head">
|
|
996
|
-
<div class="help-plugin__title-row">
|
|
997
|
-
<h3 class="help-plugin__title">${escapeHtml(entry.title)}</h3>
|
|
998
|
-
<span class="help-plugin__alias">${escapeHtml(entry.pluginName)}</span>
|
|
999
|
-
</div>
|
|
1000
|
-
<p class="help-plugin__desc">${escapeHtml(entry.description || "暂无插件简介")}</p>
|
|
1001
|
-
</div>
|
|
1002
|
-
${
|
|
1003
|
-
entry.commands.length > 0
|
|
1004
|
-
? `<div class="help-plugin__body"><div class="help-command-tags">${commandTags}</div></div>`
|
|
1005
|
-
: `<p class="help-plugin__empty">暂无命令</p>`
|
|
1006
|
-
}
|
|
1007
|
-
</section>
|
|
1008
|
-
`;
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
function renderPluginDetail(
|
|
1012
|
-
entry: HelpRenderableEntry,
|
|
1013
|
-
isNightMode: boolean,
|
|
1014
|
-
): string {
|
|
1015
|
-
const commandsHtml = entry.commands
|
|
1016
|
-
.map((command) => {
|
|
1017
|
-
const roleBadge = command.role
|
|
1018
|
-
? renderRoleBadge(command.role as CommandRole, isNightMode)
|
|
1019
|
-
: "";
|
|
1020
|
-
|
|
1021
|
-
return `
|
|
1022
|
-
<div class="help-command">
|
|
1023
|
-
<div class="help-command__top">
|
|
1024
|
-
<div class="help-command__name">${escapeHtml(command.cmd)}</div>
|
|
1025
|
-
${roleBadge}
|
|
1026
|
-
</div>
|
|
1027
|
-
<div class="help-command__desc">${escapeHtml(command.desc || "")}</div>
|
|
1028
|
-
${
|
|
1029
|
-
command.usage
|
|
1030
|
-
? `<div class="help-command__usage">示例:<code>${escapeHtml(command.usage)}</code></div>`
|
|
1031
|
-
: ""
|
|
1032
|
-
}
|
|
1033
|
-
</div>
|
|
1034
|
-
`;
|
|
1035
|
-
})
|
|
1036
|
-
.join("");
|
|
1037
|
-
|
|
1038
|
-
return `
|
|
1039
|
-
<section class="help-plugin help-plugin--detail">
|
|
1040
|
-
<div class="help-plugin__head">
|
|
1041
|
-
<div class="help-plugin__title-row">
|
|
1042
|
-
<h3 class="help-plugin__title">${escapeHtml(entry.title)}</h3>
|
|
1043
|
-
<span class="help-plugin__alias">${escapeHtml(entry.pluginName)}</span>
|
|
1044
|
-
</div>
|
|
1045
|
-
<p class="help-plugin__desc">${escapeHtml(entry.description || "暂无插件简介")}</p>
|
|
1046
|
-
</div>
|
|
1047
|
-
${
|
|
1048
|
-
entry.commands.length > 0
|
|
1049
|
-
? `<div class="help-plugin__body">${commandsHtml}</div>`
|
|
1050
|
-
: `<p class="help-plugin__empty">暂无命令</p>`
|
|
1051
|
-
}
|
|
1052
|
-
</section>
|
|
1053
|
-
`;
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
function renderRoleBadge(role: CommandRole, isNightMode: boolean): string {
|
|
1057
|
-
const config = ROLE_CONFIG[role];
|
|
1058
|
-
if (!config) {
|
|
1059
|
-
return "";
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
const background = isNightMode ? config.badgeBgDark : config.badgeBgLight;
|
|
1063
|
-
const border = isNightMode ? config.badgeBorderDark : config.badgeBorderLight;
|
|
1064
|
-
const color = isNightMode ? config.badgeTextDark : config.badgeTextLight;
|
|
1065
|
-
|
|
1066
|
-
return `<span class="help-role" style="background: ${background}; border-color: ${border}; color: ${color};">${config.label}</span>`;
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
function getHelpTheme(isNightMode: boolean): HelpTheme {
|
|
1070
|
-
if (isNightMode) {
|
|
1071
|
-
return {
|
|
1072
|
-
pageBg: "linear-gradient(180deg, #07141c 0%, #0b1c25 52%, #102730 100%)",
|
|
1073
|
-
shellBg: "rgba(6, 19, 25, 0.34)",
|
|
1074
|
-
pageAccent:
|
|
1075
|
-
"radial-gradient(circle at 18% 14%, rgba(76, 201, 191, 0.18), transparent 34%), radial-gradient(circle at 82% 10%, rgba(34, 211, 238, 0.12), transparent 28%), radial-gradient(circle at 50% 100%, rgba(45, 212, 191, 0.1), transparent 42%)",
|
|
1076
|
-
pageGrid:
|
|
1077
|
-
"linear-gradient(rgba(151, 214, 210, 0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(151, 214, 210, 0.04) 1px, transparent 1px)",
|
|
1078
|
-
sceneOpacity: "0.48",
|
|
1079
|
-
sceneFilter: "blur(1.5px) saturate(0.96) contrast(1.05) brightness(0.72)",
|
|
1080
|
-
sceneMask:
|
|
1081
|
-
"linear-gradient(180deg, rgba(3, 11, 15, 0.34), rgba(4, 16, 22, 0.2) 26%, rgba(4, 14, 20, 0.06) 52%, rgba(3, 10, 14, 0.4) 100%)",
|
|
1082
|
-
sceneGlow:
|
|
1083
|
-
"radial-gradient(circle at 24% 16%, rgba(126, 231, 221, 0.16), transparent 28%), radial-gradient(circle at 78% 10%, rgba(34, 211, 238, 0.14), transparent 26%)",
|
|
1084
|
-
shellBorder: "rgba(116, 202, 200, 0.18)",
|
|
1085
|
-
shellShadow: "0 32px 70px rgba(1, 11, 16, 0.45)",
|
|
1086
|
-
heroBg:
|
|
1087
|
-
"linear-gradient(135deg, rgba(16, 40, 50, 0.94), rgba(14, 32, 42, 0.88))",
|
|
1088
|
-
heroBorder: "rgba(105, 196, 194, 0.24)",
|
|
1089
|
-
heroGlow: "rgba(77, 217, 200, 0.16)",
|
|
1090
|
-
eyebrow: "#7ee7dd",
|
|
1091
|
-
title: "#ecfeff",
|
|
1092
|
-
subtitle: "#b9d7d8",
|
|
1093
|
-
panelBg: "rgba(12, 29, 38, 0.86)",
|
|
1094
|
-
panelBorder: "rgba(105, 196, 194, 0.16)",
|
|
1095
|
-
panelShadow: "0 18px 42px rgba(0, 0, 0, 0.22)",
|
|
1096
|
-
panelTitle: "#f0fdff",
|
|
1097
|
-
panelDesc: "#98babc",
|
|
1098
|
-
commandBg: "rgba(18, 41, 50, 0.92)",
|
|
1099
|
-
commandBorder: "rgba(108, 185, 182, 0.14)",
|
|
1100
|
-
commandTitle: "#83f0e1",
|
|
1101
|
-
commandDesc: "#d8eeed",
|
|
1102
|
-
tagBg: "rgba(25, 52, 62, 0.9)",
|
|
1103
|
-
tagBorder: "rgba(125, 218, 211, 0.25)",
|
|
1104
|
-
tagText: "#9af8eb",
|
|
1105
|
-
emptyText: "#78999a",
|
|
1106
|
-
footerBg: "rgba(10, 24, 32, 0.82)",
|
|
1107
|
-
footerBorder: "rgba(105, 196, 194, 0.16)",
|
|
1108
|
-
footerLabel: "#85aeb0",
|
|
1109
|
-
footerText: "#dffcf8",
|
|
1110
|
-
divider: "rgba(105, 196, 194, 0.18)",
|
|
1111
|
-
};
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
return {
|
|
1115
|
-
pageBg: "linear-gradient(180deg, #eef6f7 0%, #f6fbfb 48%, #edf5f7 100%)",
|
|
1116
|
-
shellBg: "rgba(255, 255, 255, 0.42)",
|
|
1117
|
-
pageAccent:
|
|
1118
|
-
"radial-gradient(circle at 12% 10%, rgba(45, 212, 191, 0.18), transparent 28%), radial-gradient(circle at 88% 0%, rgba(56, 189, 248, 0.14), transparent 24%), radial-gradient(circle at 50% 100%, rgba(13, 148, 136, 0.08), transparent 44%)",
|
|
1119
|
-
pageGrid:
|
|
1120
|
-
"linear-gradient(rgba(17, 94, 89, 0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(17, 94, 89, 0.05) 1px, transparent 1px)",
|
|
1121
|
-
sceneOpacity: "0.42",
|
|
1122
|
-
sceneFilter: "blur(1.5px) saturate(0.98) contrast(1.02) brightness(1.03)",
|
|
1123
|
-
sceneMask:
|
|
1124
|
-
"linear-gradient(180deg, rgba(244, 250, 251, 0.4), rgba(244, 250, 251, 0.22) 30%, rgba(244, 250, 251, 0.04) 58%, rgba(237, 245, 247, 0.46) 100%)",
|
|
1125
|
-
sceneGlow:
|
|
1126
|
-
"radial-gradient(circle at 18% 14%, rgba(45, 212, 191, 0.14), transparent 28%), radial-gradient(circle at 82% 8%, rgba(56, 189, 248, 0.12), transparent 24%)",
|
|
1127
|
-
shellBorder: "rgba(148, 196, 204, 0.62)",
|
|
1128
|
-
shellShadow: "0 26px 60px rgba(12, 50, 59, 0.12)",
|
|
1129
|
-
heroBg:
|
|
1130
|
-
"linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(240, 250, 249, 0.94))",
|
|
1131
|
-
heroBorder: "rgba(148, 196, 204, 0.7)",
|
|
1132
|
-
heroGlow: "rgba(45, 212, 191, 0.14)",
|
|
1133
|
-
eyebrow: "#0f766e",
|
|
1134
|
-
title: "#0f172a",
|
|
1135
|
-
subtitle: "#335761",
|
|
1136
|
-
panelBg: "rgba(255, 255, 255, 0.95)",
|
|
1137
|
-
panelBorder: "rgba(148, 196, 204, 0.68)",
|
|
1138
|
-
panelShadow: "0 16px 36px rgba(15, 61, 71, 0.08)",
|
|
1139
|
-
panelTitle: "#102430",
|
|
1140
|
-
panelDesc: "#4b6770",
|
|
1141
|
-
commandBg: "rgba(244, 251, 251, 0.98)",
|
|
1142
|
-
commandBorder: "rgba(185, 217, 221, 0.82)",
|
|
1143
|
-
commandTitle: "#0f766e",
|
|
1144
|
-
commandDesc: "#17353f",
|
|
1145
|
-
tagBg: "rgba(237, 248, 249, 0.98)",
|
|
1146
|
-
tagBorder: "rgba(148, 196, 204, 0.74)",
|
|
1147
|
-
tagText: "#0d6a65",
|
|
1148
|
-
emptyText: "#6f8b93",
|
|
1149
|
-
footerBg: "rgba(255, 255, 255, 0.94)",
|
|
1150
|
-
footerBorder: "rgba(148, 196, 204, 0.72)",
|
|
1151
|
-
footerLabel: "#5b7680",
|
|
1152
|
-
footerText: "#0f172a",
|
|
1153
|
-
divider: "rgba(148, 196, 204, 0.78)",
|
|
1154
|
-
};
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
function scoreKeywordMatch(query: string, entry: HelpRenderableEntry): number {
|
|
1158
|
-
let score = 0;
|
|
1159
|
-
|
|
1160
|
-
if (
|
|
1161
|
-
entry.normalizedTitle.includes(query) ||
|
|
1162
|
-
query.includes(entry.normalizedTitle)
|
|
1163
|
-
) {
|
|
1164
|
-
score = Math.max(
|
|
1165
|
-
score,
|
|
1166
|
-
46 - Math.abs(entry.normalizedTitle.length - query.length),
|
|
1167
|
-
);
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
for (const key of entry.matchKeys) {
|
|
1171
|
-
if (key.startsWith(query) || query.startsWith(key)) {
|
|
1172
|
-
score = Math.max(score, 58 - Math.abs(key.length - query.length));
|
|
1173
|
-
continue;
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
if (key.includes(query) || query.includes(key)) {
|
|
1177
|
-
score = Math.max(score, 34 - Math.abs(key.length - query.length));
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
return Math.max(0, score);
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
function extractMatchTokens(text: string): string[] {
|
|
1185
|
-
const rawTokens = String(text || "")
|
|
1186
|
-
.toLowerCase()
|
|
1187
|
-
.match(/[a-z0-9\u4e00-\u9fa5]+/gi);
|
|
1188
|
-
|
|
1189
|
-
if (!rawTokens) {
|
|
1190
|
-
return [];
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
const tokens = rawTokens
|
|
1194
|
-
.map((token) => normalizeForMatch(token))
|
|
1195
|
-
.filter((token) => token.length >= 2)
|
|
1196
|
-
.filter((token) => !STOPWORDS.has(token));
|
|
1197
|
-
|
|
1198
|
-
return Array.from(new Set(tokens));
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
function extractCommandAlias(command: string): string | null {
|
|
1202
|
-
const value = String(command || "")
|
|
1203
|
-
.trim()
|
|
1204
|
-
.replace(/^[#/]+/, "");
|
|
1205
|
-
if (!value) {
|
|
1206
|
-
return null;
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
const firstToken = value.split(/\s+/)[0];
|
|
1210
|
-
if (!firstToken || /[<>]/.test(firstToken)) {
|
|
1211
|
-
return null;
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
if (!/[a-z0-9]/i.test(firstToken)) {
|
|
1215
|
-
return null;
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
const normalized = normalizeForMatch(firstToken);
|
|
1219
|
-
if (!normalized || STOPWORDS.has(normalized)) {
|
|
1220
|
-
return null;
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
return normalized;
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
function sanitizeKeyword(value: string): string {
|
|
1227
|
-
return String(value || "")
|
|
1228
|
-
.trim()
|
|
1229
|
-
.replace(/^[#/\s]+/, "")
|
|
1230
|
-
.replace(/[。.!!??,,::;;]+$/g, "");
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
function normalizeForMatch(value: string): string {
|
|
1234
|
-
const parts = String(value || "")
|
|
1235
|
-
.toLowerCase()
|
|
1236
|
-
.match(/[a-z0-9\u4e00-\u9fa5]+/gi);
|
|
1237
|
-
|
|
1238
|
-
return parts ? parts.join("") : "";
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
function escapeHtml(text: string): string {
|
|
1242
|
-
const map: Record<string, string> = {
|
|
1243
|
-
"&": "&",
|
|
1244
|
-
"<": "<",
|
|
1245
|
-
">": ">",
|
|
1246
|
-
'"': """,
|
|
1247
|
-
"'": "'",
|
|
1248
|
-
};
|
|
1249
|
-
return String(text || "").replace(/[&<>"']/g, (match) => map[match]);
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
function normalizeImageSource(file: string): string {
|
|
1253
|
-
const value = String(file || "").trim();
|
|
1254
|
-
if (!value) {
|
|
1255
|
-
return value;
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
if (
|
|
1259
|
-
value.startsWith("file://") ||
|
|
1260
|
-
value.startsWith("base64://") ||
|
|
1261
|
-
value.startsWith("http://") ||
|
|
1262
|
-
value.startsWith("https://")
|
|
1263
|
-
) {
|
|
1264
|
-
return value;
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
if (isLocalFilePath(value)) {
|
|
1268
|
-
return `file://${value}`;
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
return value;
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
function isLocalFilePath(value: string): boolean {
|
|
1275
|
-
return value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value);
|
|
1276
|
-
}
|