mioku-plugin-help 2.0.0 → 2.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.
@@ -0,0 +1,556 @@
1
+ /**
2
+ * Render the help image HTML.
3
+ *
4
+ * The output is a complete HTML document with inline CSS, designed to be
5
+ * piped through the screenshot service at 760px wide, fullPage.
6
+ *
7
+ * Two layouts are produced:
8
+ * - Overview (default): 2-column grid of all plugins, each as a compact
9
+ * card with command tags.
10
+ * - Detail (when `targetPluginName` matches): single full-width card with
11
+ * every command expanded (description, role badge, optional usage).
12
+ */
13
+
14
+ import type { CommandRole, PluginHelp } from "mioku";
15
+ import { escapeHtml } from "../utils";
16
+ import { getHelpTheme, HELP_BACKGROUND_IMAGE_URL } from "../theme";
17
+ import { getRenderableEntries } from "./intent";
18
+ import { ROLE_CONFIG } from "./role-config";
19
+ import type { HelpRenderableEntry } from "./types";
20
+
21
+ function renderRoleBadge(
22
+ role: CommandRole,
23
+ isNightMode: boolean,
24
+ ): string {
25
+ const config = ROLE_CONFIG[role];
26
+ if (!config) {
27
+ return "";
28
+ }
29
+
30
+ const background = isNightMode ? config.badgeBgDark : config.badgeBgLight;
31
+ const border = isNightMode ? config.badgeBorderDark : config.badgeBorderLight;
32
+ const color = isNightMode ? config.badgeTextDark : config.badgeTextLight;
33
+
34
+ return `<span class="help-role" style="background: ${background}; border-color: ${border}; color: ${color};">${config.label}</span>`;
35
+ }
36
+
37
+ function renderPluginOverview(entry: HelpRenderableEntry): string {
38
+ const commandTags = entry.commands
39
+ .map(
40
+ (command) =>
41
+ `<span class="help-command-tag" title="${escapeHtml(command.desc || "")}" >${escapeHtml(command.cmd)}</span>`,
42
+ )
43
+ .join("");
44
+
45
+ return `
46
+ <section class="help-plugin help-plugin--overview">
47
+ <div class="help-plugin__head">
48
+ <div class="help-plugin__title-row">
49
+ <h3 class="help-plugin__title">${escapeHtml(entry.title)}</h3>
50
+ <span class="help-plugin__alias">${escapeHtml(entry.pluginName)}</span>
51
+ </div>
52
+ <p class="help-plugin__desc">${escapeHtml(entry.description || "暂无插件简介")}</p>
53
+ </div>
54
+ ${
55
+ entry.commands.length > 0
56
+ ? `<div class="help-plugin__body"><div class="help-command-tags">${commandTags}</div></div>`
57
+ : `<p class="help-plugin__empty">暂无命令</p>`
58
+ }
59
+ </section>
60
+ `;
61
+ }
62
+
63
+ function renderPluginDetail(
64
+ entry: HelpRenderableEntry,
65
+ isNightMode: boolean,
66
+ ): string {
67
+ const commandsHtml = entry.commands
68
+ .map((command) => {
69
+ const roleBadge = command.role
70
+ ? renderRoleBadge(command.role as CommandRole, isNightMode)
71
+ : "";
72
+
73
+ return `
74
+ <div class="help-command">
75
+ <div class="help-command__top">
76
+ <div class="help-command__name">${escapeHtml(command.cmd)}</div>
77
+ ${roleBadge}
78
+ </div>
79
+ <div class="help-command__desc">${escapeHtml(command.desc || "")}</div>
80
+ ${
81
+ command.usage
82
+ ? `<div class="help-command__usage">示例:<code>${escapeHtml(command.usage)}</code></div>`
83
+ : ""
84
+ }
85
+ </div>
86
+ `;
87
+ })
88
+ .join("");
89
+
90
+ return `
91
+ <section class="help-plugin help-plugin--detail">
92
+ <div class="help-plugin__head">
93
+ <div class="help-plugin__title-row">
94
+ <h3 class="help-plugin__title">${escapeHtml(entry.title)}</h3>
95
+ <span class="help-plugin__alias">${escapeHtml(entry.pluginName)}</span>
96
+ </div>
97
+ <p class="help-plugin__desc">${escapeHtml(entry.description || "暂无插件简介")}</p>
98
+ </div>
99
+ ${
100
+ entry.commands.length > 0
101
+ ? `<div class="help-plugin__body">${commandsHtml}</div>`
102
+ : `<p class="help-plugin__empty">暂无命令</p>`
103
+ }
104
+ </section>
105
+ `;
106
+ }
107
+
108
+ /**
109
+ * Build the full help image HTML. `targetPluginName` switches the
110
+ * renderer to detail mode for that plugin (no-op if the name doesn't
111
+ * match anything in `helpMap`).
112
+ */
113
+ export function generateHelpHtml(
114
+ helpMap: Map<string, PluginHelp>,
115
+ isNightMode: boolean,
116
+ miokiVersion: string = "unknown",
117
+ miokuVersion: string = "unknown",
118
+ botNickname: string = "Mioku Bot",
119
+ botAvatarUrl?: string,
120
+ targetPluginName?: string,
121
+ ): string {
122
+ const entries = getRenderableEntries(helpMap);
123
+ const selectedEntry = targetPluginName
124
+ ? entries.find((entry) => entry.pluginName === targetPluginName)
125
+ : undefined;
126
+
127
+ const isDetailMode = Boolean(selectedEntry);
128
+ const logoPath = "../../plugins/help/source/miku.png";
129
+ const avatarSrc = botAvatarUrl || logoPath;
130
+ const backgroundImageUrl = HELP_BACKGROUND_IMAGE_URL;
131
+ const theme = getHelpTheme(isNightMode);
132
+
133
+ const pluginsHtml = isDetailMode
134
+ ? renderPluginDetail(selectedEntry!, isNightMode)
135
+ : entries.map((entry) => renderPluginOverview(entry)).join("");
136
+
137
+ const heroTitle = isDetailMode
138
+ ? `${botNickname} · ${selectedEntry!.title}`
139
+ : botNickname;
140
+ const heroSubtitle = isDetailMode
141
+ ? `${selectedEntry!.pluginName} 插件,共 ${selectedEntry!.commands.length} 条命令`
142
+ : `共 ${entries.length} 个插件 发送 <插件名>帮助查看详细信息`;
143
+
144
+ return `
145
+ <style>
146
+ .help-sheet {
147
+ padding: 18px;
148
+ display: flex;
149
+ flex-direction: column;
150
+ gap: 14px;
151
+ position: relative;
152
+ overflow: hidden;
153
+ background: ${theme.pageBg};
154
+ color: ${theme.panelTitle};
155
+ font-family: "SF Pro Display", "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", "Hiragino Sans GB", sans-serif;
156
+ }
157
+
158
+ .help-sheet::before,
159
+ .help-sheet::after {
160
+ content: "";
161
+ position: absolute;
162
+ inset: 0;
163
+ pointer-events: none;
164
+ }
165
+
166
+ .help-sheet::before {
167
+ background: ${theme.pageAccent};
168
+ }
169
+
170
+ .help-sheet::after {
171
+ background-image: ${theme.pageGrid};
172
+ background-size: 28px 28px;
173
+ opacity: ${isNightMode ? "0.55" : "0.35"};
174
+ }
175
+
176
+ .help-sheet__scene,
177
+ .help-sheet__scene-image,
178
+ .help-sheet__scene-overlay {
179
+ position: absolute;
180
+ inset: 0;
181
+ pointer-events: none;
182
+ }
183
+
184
+ .help-sheet__scene {
185
+ z-index: 0;
186
+ overflow: hidden;
187
+ }
188
+
189
+ .help-sheet__scene-image {
190
+ background-image: url("${backgroundImageUrl}");
191
+ background-size: cover;
192
+ background-position: center center;
193
+ opacity: ${theme.sceneOpacity};
194
+ filter: ${theme.sceneFilter};
195
+ transform: scale(1.06);
196
+ }
197
+
198
+ .help-sheet__scene-overlay {
199
+ background: ${theme.sceneGlow}, ${theme.sceneMask};
200
+ }
201
+
202
+ .help-shell {
203
+ position: relative;
204
+ z-index: 1;
205
+ display: flex;
206
+ flex-direction: column;
207
+ gap: 14px;
208
+ border-radius: 30px;
209
+ border: 1px solid ${theme.shellBorder};
210
+ box-shadow: ${theme.shellShadow};
211
+ padding: 14px;
212
+ background: ${theme.shellBg};
213
+ backdrop-filter: blur(10px) saturate(1.06);
214
+ }
215
+
216
+ .help-hero {
217
+ position: relative;
218
+ display: flex;
219
+ align-items: center;
220
+ gap: 16px;
221
+ padding: 18px 18px 16px;
222
+ border-radius: 24px;
223
+ border: 1px solid ${theme.heroBorder};
224
+ background: ${theme.heroBg};
225
+ overflow: hidden;
226
+ }
227
+
228
+ .help-hero::before {
229
+ content: "";
230
+ position: absolute;
231
+ inset: auto auto -42px -32px;
232
+ width: 180px;
233
+ height: 180px;
234
+ border-radius: 999px;
235
+ background: ${theme.heroGlow};
236
+ filter: blur(4px);
237
+ }
238
+
239
+ .help-hero::after {
240
+ content: "";
241
+ position: absolute;
242
+ inset: -70px -50px auto auto;
243
+ width: 220px;
244
+ height: 220px;
245
+ border-radius: 999px;
246
+ background: ${theme.heroGlow};
247
+ filter: blur(18px);
248
+ }
249
+
250
+ .help-hero__logo {
251
+ position: relative;
252
+ z-index: 1;
253
+ width: 92px;
254
+ height: 92px;
255
+ flex-shrink: 0;
256
+ border-radius: 999px;
257
+ overflow: hidden;
258
+ }
259
+
260
+ .help-hero__logo img {
261
+ width: 100%;
262
+ height: 100%;
263
+ object-fit: cover;
264
+ border-radius: 999px;
265
+ box-shadow: 0 10px 24px ${isNightMode ? "rgba(0, 0, 0, 0.32)" : "rgba(15, 61, 71, 0.14)"};
266
+ }
267
+
268
+ .help-hero__content {
269
+ position: relative;
270
+ z-index: 1;
271
+ min-width: 0;
272
+ }
273
+
274
+ .help-hero__eyebrow {
275
+ margin-bottom: 8px;
276
+ font-size: 12px;
277
+ font-weight: 700;
278
+ letter-spacing: 0.18em;
279
+ text-transform: uppercase;
280
+ color: ${theme.eyebrow};
281
+ }
282
+
283
+ .help-hero__title {
284
+ margin: 0;
285
+ font-size: 34px;
286
+ line-height: 1.06;
287
+ font-weight: 900;
288
+ letter-spacing: -0.04em;
289
+ color: ${theme.title};
290
+ }
291
+
292
+ .help-hero__subtitle {
293
+ margin: 10px 0 0;
294
+ max-width: 520px;
295
+ font-size: 13px;
296
+ line-height: 1.6;
297
+ color: ${theme.subtitle};
298
+ }
299
+
300
+ .help-grid {
301
+ column-count: 2;
302
+ column-gap: 12px;
303
+ }
304
+
305
+ .help-detail {
306
+ display: block;
307
+ }
308
+
309
+ .help-plugin {
310
+ display: flex;
311
+ flex-direction: column;
312
+ min-height: 0;
313
+ border-radius: 20px;
314
+ border: 1px solid ${theme.panelBorder};
315
+ background: ${theme.panelBg};
316
+ box-shadow: ${theme.panelShadow};
317
+ overflow: hidden;
318
+ break-inside: avoid;
319
+ margin-bottom: 12px;
320
+ }
321
+
322
+ .help-plugin__head {
323
+ padding: 14px 14px 12px;
324
+ border-bottom: 1px solid ${theme.panelBorder};
325
+ }
326
+
327
+ .help-plugin__title-row {
328
+ display: flex;
329
+ align-items: center;
330
+ gap: 8px;
331
+ min-width: 0;
332
+ }
333
+
334
+ .help-plugin__title {
335
+ margin: 0;
336
+ min-width: 0;
337
+ font-size: 16px;
338
+ line-height: 1.25;
339
+ font-weight: 800;
340
+ color: ${theme.panelTitle};
341
+ word-break: break-word;
342
+ }
343
+
344
+ .help-plugin__alias {
345
+ flex-shrink: 0;
346
+ padding: 2px 8px;
347
+ border-radius: 999px;
348
+ border: 1px solid ${theme.tagBorder};
349
+ background: ${theme.tagBg};
350
+ color: ${theme.tagText};
351
+ font-size: 11px;
352
+ line-height: 1.5;
353
+ font-family: "SF Mono", "JetBrains Mono", "Fira Code", monospace;
354
+ font-weight: 700;
355
+ }
356
+
357
+ .help-plugin__desc {
358
+ margin: 8px 0 0;
359
+ font-size: 12px;
360
+ line-height: 1.5;
361
+ color: ${theme.panelDesc};
362
+ }
363
+
364
+ .help-plugin__body {
365
+ padding: 10px;
366
+ }
367
+
368
+ .help-plugin__empty {
369
+ padding: 20px 16px;
370
+ text-align: center;
371
+ font-size: 12px;
372
+ color: ${theme.emptyText};
373
+ }
374
+
375
+ .help-command-tags {
376
+ display: flex;
377
+ flex-wrap: wrap;
378
+ gap: 6px;
379
+ }
380
+
381
+ .help-command-tag {
382
+ display: inline-flex;
383
+ align-items: center;
384
+ max-width: 100%;
385
+ padding: 4px 9px;
386
+ border-radius: 12px;
387
+ border: 1px solid ${theme.commandBorder};
388
+ background: ${theme.commandBg};
389
+ color: ${theme.commandTitle};
390
+ font-size: 11px;
391
+ line-height: 1.4;
392
+ font-family: "SF Mono", "JetBrains Mono", "Fira Code", monospace;
393
+ font-weight: 700;
394
+ white-space: nowrap;
395
+ overflow: hidden;
396
+ text-overflow: ellipsis;
397
+ }
398
+
399
+ .help-plugin--detail .help-plugin__head {
400
+ padding: 16px 16px 14px;
401
+ }
402
+
403
+ .help-plugin--detail .help-plugin__body {
404
+ padding: 12px;
405
+ }
406
+
407
+ .help-command {
408
+ padding: 12px;
409
+ border-radius: 16px;
410
+ background: ${theme.commandBg};
411
+ border: 1px solid ${theme.commandBorder};
412
+ }
413
+
414
+ .help-command + .help-command {
415
+ margin-top: 10px;
416
+ }
417
+
418
+ .help-command__top {
419
+ display: flex;
420
+ align-items: flex-start;
421
+ justify-content: space-between;
422
+ gap: 10px;
423
+ }
424
+
425
+ .help-command__name {
426
+ flex: 1;
427
+ min-width: 0;
428
+ font-family: "SF Mono", "JetBrains Mono", "Fira Code", monospace;
429
+ font-size: 13px;
430
+ line-height: 1.45;
431
+ font-weight: 800;
432
+ color: ${theme.commandTitle};
433
+ word-break: break-word;
434
+ }
435
+
436
+ .help-command__desc {
437
+ margin-top: 6px;
438
+ font-size: 12px;
439
+ line-height: 1.6;
440
+ color: ${theme.commandDesc};
441
+ }
442
+
443
+ .help-command__usage {
444
+ margin-top: 8px;
445
+ font-size: 11px;
446
+ line-height: 1.55;
447
+ color: ${theme.panelDesc};
448
+ }
449
+
450
+ .help-command__usage code {
451
+ font-family: "SF Mono", "JetBrains Mono", "Fira Code", monospace;
452
+ font-size: 11px;
453
+ color: ${theme.commandTitle};
454
+ }
455
+
456
+ .help-role {
457
+ flex-shrink: 0;
458
+ padding: 2px 8px;
459
+ border-radius: 999px;
460
+ font-size: 10px;
461
+ line-height: 1.5;
462
+ font-weight: 700;
463
+ letter-spacing: 0.02em;
464
+ border: 1px solid transparent;
465
+ }
466
+
467
+ .help-footer {
468
+ display: flex;
469
+ align-items: stretch;
470
+ gap: 0;
471
+ border-radius: 20px;
472
+ border: 1px solid ${theme.footerBorder};
473
+ background: ${theme.footerBg};
474
+ overflow: hidden;
475
+ }
476
+
477
+ .help-footer__item {
478
+ flex: 1;
479
+ display: flex;
480
+ align-items: center;
481
+ gap: 12px;
482
+ padding: 14px 16px;
483
+ }
484
+
485
+ .help-footer__item + .help-footer__item {
486
+ border-left: 1px solid ${theme.divider};
487
+ }
488
+
489
+ .help-footer__icon {
490
+ width: 36px;
491
+ height: 36px;
492
+ flex-shrink: 0;
493
+ display: grid;
494
+ place-items: center;
495
+ border-radius: 12px;
496
+ background: ${isNightMode ? "rgba(126, 231, 221, 0.08)" : "rgba(15, 118, 110, 0.08)"};
497
+ color: ${theme.eyebrow};
498
+ font-size: 18px;
499
+ }
500
+
501
+ .help-footer__label {
502
+ font-size: 11px;
503
+ line-height: 1.4;
504
+ color: ${theme.footerLabel};
505
+ }
506
+
507
+ .help-footer__value {
508
+ margin-top: 2px;
509
+ font-family: "SF Mono", "JetBrains Mono", "Fira Code", monospace;
510
+ font-size: 12px;
511
+ line-height: 1.45;
512
+ font-weight: 700;
513
+ color: ${theme.footerText};
514
+ }
515
+ </style>
516
+ <div class="help-sheet">
517
+ <div class="help-sheet__scene">
518
+ <div class="help-sheet__scene-image"></div>
519
+ <div class="help-sheet__scene-overlay"></div>
520
+ </div>
521
+ <div class="help-shell">
522
+ <header class="help-hero">
523
+ <div class="help-hero__logo">
524
+ <img src="${escapeHtml(avatarSrc)}" alt="logo" />
525
+ </div>
526
+ <div class="help-hero__content">
527
+ <div class="help-hero__eyebrow">Mioku Assistant</div>
528
+ <h1 class="help-hero__title">${escapeHtml(heroTitle)}</h1>
529
+ <p class="help-hero__subtitle">${escapeHtml(heroSubtitle)}</p>
530
+ </div>
531
+ </header>
532
+
533
+ <main class="${isDetailMode ? "help-detail" : "help-grid"}">
534
+ ${pluginsHtml}
535
+ </main>
536
+
537
+ <footer class="help-footer">
538
+ <div class="help-footer__item">
539
+ <div class="help-footer__icon">⚡</div>
540
+ <div>
541
+ <div class="help-footer__label">Framework</div>
542
+ <div class="help-footer__value">Mioki ${escapeHtml(miokiVersion)}</div>
543
+ </div>
544
+ </div>
545
+ <div class="help-footer__item">
546
+ <div class="help-footer__icon">🚀</div>
547
+ <div>
548
+ <div class="help-footer__label">Platform</div>
549
+ <div class="help-footer__value">Mioku ${escapeHtml(miokuVersion)}</div>
550
+ </div>
551
+ </div>
552
+ </footer>
553
+ </div>
554
+ </div>
555
+ `;
556
+ }