ErisPulse-HelpNext 0.1.0__py3-none-any.whl
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.
- HelpNext/Core.py +349 -0
- HelpNext/Templates.py +293 -0
- HelpNext/Visualizer.py +498 -0
- HelpNext/__init__.py +3 -0
- HelpNext/assets/icon.png +0 -0
- erispulse_helpnext-0.1.0.dist-info/METADATA +148 -0
- erispulse_helpnext-0.1.0.dist-info/RECORD +10 -0
- erispulse_helpnext-0.1.0.dist-info/WHEEL +4 -0
- erispulse_helpnext-0.1.0.dist-info/entry_points.txt +2 -0
- erispulse_helpnext-0.1.0.dist-info/licenses/LICENSE +7 -0
HelpNext/Core.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Dict, List, Optional, Tuple
|
|
3
|
+
|
|
4
|
+
from ErisPulse import sdk
|
|
5
|
+
from ErisPulse.Core.Bases import BaseConfig, BaseI18n, BaseModule, I18nKey
|
|
6
|
+
from ErisPulse.Core.Event import command
|
|
7
|
+
|
|
8
|
+
from .Templates import HelpTemplates
|
|
9
|
+
from .Visualizer import Visualizer
|
|
10
|
+
|
|
11
|
+
class Main(BaseModule):
|
|
12
|
+
"""ErisPulse modern help module (Takumi-rendered, i18n-aware)."""
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ConfigClass(BaseConfig):
|
|
16
|
+
show_hidden_commands: bool = field(
|
|
17
|
+
default=False,
|
|
18
|
+
metadata={
|
|
19
|
+
"description": {"i18n": "HelpNext.cfg_show_hidden", "default": "显示隐藏命令"},
|
|
20
|
+
"ui": {"widget": "switch", "group": "basic", "order": 1},
|
|
21
|
+
},
|
|
22
|
+
)
|
|
23
|
+
group_commands: bool = field(
|
|
24
|
+
default=True,
|
|
25
|
+
metadata={
|
|
26
|
+
"description": {"i18n": "HelpNext.cfg_group_commands", "default": "按分组显示命令"},
|
|
27
|
+
"ui": {"widget": "switch", "group": "basic", "order": 2},
|
|
28
|
+
},
|
|
29
|
+
)
|
|
30
|
+
theme: str = field(
|
|
31
|
+
default="auto",
|
|
32
|
+
metadata={
|
|
33
|
+
"description": {"i18n": "HelpNext.cfg_theme", "default": "图片主题"},
|
|
34
|
+
"ui": {
|
|
35
|
+
"widget": "select", "group": "render", "order": 3,
|
|
36
|
+
"options": [
|
|
37
|
+
{"label": {"i18n": "HelpNext.theme_auto", "default": "自动(跟随时间)"}, "value": "auto"},
|
|
38
|
+
{"label": {"i18n": "HelpNext.theme_light", "default": "浅色"}, "value": "light"},
|
|
39
|
+
{"label": {"i18n": "HelpNext.theme_dark", "default": "深色"}, "value": "dark"},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
utc_offset: int = field(
|
|
45
|
+
default=8,
|
|
46
|
+
metadata={
|
|
47
|
+
"description": {"i18n": "HelpNext.cfg_utc_offset", "default": "UTC 时区偏移(用于昼夜切换)"},
|
|
48
|
+
"min": -12, "max": 14,
|
|
49
|
+
"ui": {"widget": "number", "group": "render", "order": 4},
|
|
50
|
+
},
|
|
51
|
+
)
|
|
52
|
+
show_logo: bool = field(
|
|
53
|
+
default=True,
|
|
54
|
+
metadata={
|
|
55
|
+
"description": {"i18n": "HelpNext.cfg_show_logo", "default": "头部显示 ErisPulse 图标"},
|
|
56
|
+
"ui": {"widget": "switch", "group": "header", "order": 5},
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
header_title: str = field(
|
|
60
|
+
default="",
|
|
61
|
+
metadata={
|
|
62
|
+
"description": {"i18n": "HelpNext.cfg_header_title", "default": "自定义头部标题(留空使用默认)"},
|
|
63
|
+
"ui": {"widget": "text", "group": "header", "order": 6},
|
|
64
|
+
},
|
|
65
|
+
)
|
|
66
|
+
header_subtitle: str = field(
|
|
67
|
+
default="",
|
|
68
|
+
metadata={
|
|
69
|
+
"description": {"i18n": "HelpNext.cfg_header_subtitle", "default": "自定义头部副标题(留空使用默认)"},
|
|
70
|
+
"ui": {"widget": "text", "group": "header", "order": 7},
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
_schema_meta = {
|
|
75
|
+
"group_labels": {
|
|
76
|
+
"basic": {"i18n": "HelpNext.group_basic", "default": "基本"},
|
|
77
|
+
"render": {"i18n": "HelpNext.group_render", "default": "渲染"},
|
|
78
|
+
"header": {"i18n": "HelpNext.group_header", "default": "头部"},
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
class I18nClass(BaseI18n):
|
|
83
|
+
title: I18nKey = I18nKey(default="Command Help", zh_CN="命令帮助", zh_TW="命令幫助", en="Command Help", ja="コマンドヘルプ", ru="Справка по командам")
|
|
84
|
+
detail_title: I18nKey = I18nKey(default="Command Detail", zh_CN="命令详情", zh_TW="命令詳情", en="Command Detail", ja="コマンド詳細", ru="Подробности команды")
|
|
85
|
+
group_default: I18nKey = I18nKey(default="General", zh_CN="通用命令", zh_TW="一般命令", en="General", ja="一般", ru="Общие")
|
|
86
|
+
no_description: I18nKey = I18nKey(default="No description", zh_CN="暂无描述", zh_TW="暫無描述", en="No description", ja="説明なし", ru="Нет описания")
|
|
87
|
+
usage_hint: I18nKey = I18nKey(default="Use {prefix}help <index> for details", zh_CN="使用 {prefix}help <序号> 查看命令详情", zh_TW="使用 {prefix}help <序號> 查看命令詳情", en="Use {prefix}help <index> for details", ja="{prefix}help <番号> で詳細を表示", ru="Введите {prefix}help <номер> для подробностей")
|
|
88
|
+
command_count: I18nKey = I18nKey(default="{count} commands available", zh_CN="共 {count} 个可用命令", zh_TW="共 {count} 個可用命令", en="{count} commands available", ja="利用可能なコマンド {count} 件", ru="Доступно команд: {count}")
|
|
89
|
+
other_prefixes: I18nKey = I18nKey(default="Other prefixes", zh_CN="其他触发前缀", zh_TW="其他觸發前綴", en="Other prefixes", ja="その他のプレフィックス", ru="Другие префиксы")
|
|
90
|
+
|
|
91
|
+
label_description: I18nKey = I18nKey(default="Description", zh_CN="描述", zh_TW="描述", en="Description", ja="説明", ru="Описание")
|
|
92
|
+
label_aliases: I18nKey = I18nKey(default="Aliases", zh_CN="别名", zh_TW="別名", en="Aliases", ja="エイリアス", ru="Псевдонимы")
|
|
93
|
+
label_usage: I18nKey = I18nKey(default="Usage", zh_CN="用法", zh_TW="用法", en="Usage", ja="使い方", ru="Использование")
|
|
94
|
+
label_permission: I18nKey = I18nKey(default="Permission", zh_CN="权限", zh_TW="權限", en="Permission", ja="権限", ru="Права")
|
|
95
|
+
label_group: I18nKey = I18nKey(default="Group", zh_CN="分组", zh_TW="分組", en="Group", ja="グループ", ru="Группа")
|
|
96
|
+
permission_required: I18nKey = I18nKey(default="Requires permission", zh_CN="需要特殊权限", zh_TW="需要特殊權限", en="Requires permission", ja="特殊権限が必要", ru="Требуются права")
|
|
97
|
+
|
|
98
|
+
chip_commands: I18nKey = I18nKey(default="Commands", zh_CN="命令", zh_TW="命令", en="Commands", ja="コマンド", ru="Команды")
|
|
99
|
+
chip_groups: I18nKey = I18nKey(default="Groups", zh_CN="分组", zh_TW="分組", en="Groups", ja="グループ", ru="Группы")
|
|
100
|
+
|
|
101
|
+
err_out_of_range: I18nKey = I18nKey(default="Index out of range", zh_CN="序号超出范围", zh_TW="序號超出範圍", en="Index out of range", ja="番号が範囲外", ru="Номер вне диапазона")
|
|
102
|
+
err_range_hint: I18nKey = I18nKey(default="Please enter a number between 1 and {count}", zh_CN="请输入 1-{count} 之间的序号", zh_TW="請輸入 1-{count} 之間的序號", en="Please enter a number between 1 and {count}", ja="1〜{count} の番号を入力してください", ru="Введите номер от 1 до {count}")
|
|
103
|
+
err_invalid_arg: I18nKey = I18nKey(default="Invalid argument", zh_CN="参数错误", zh_TW="參數錯誤", en="Invalid argument", ja="引数エラー", ru="Неверный аргумент")
|
|
104
|
+
err_invalid_hint: I18nKey = I18nKey(default="Please enter a valid number", zh_CN="请输入有效的序号", zh_TW="請輸入有效的序號", en="Please enter a valid number", ja="有効な番号を入力してください", ru="Введите корректный номер")
|
|
105
|
+
err_fmt: I18nKey = I18nKey(default="Invalid output format: {fmt}", zh_CN="无效的输出格式:{fmt}", zh_TW="無效的輸出格式:{fmt}", en="Invalid output format: {fmt}", ja="出力形式が無効です: {fmt}", ru="Недопустимый формат вывода: {fmt}")
|
|
106
|
+
err_unknown: I18nKey = I18nKey(default="Unknown argument: {arg}", zh_CN="未知参数:{arg}", zh_TW="未知參數:{arg}", en="Unknown argument: {arg}", ja="不明な引数: {arg}", ru="Неизвестный аргумент: {arg}")
|
|
107
|
+
err_img_unavailable: I18nKey = I18nKey(default="Image output unavailable, falling back to text", zh_CN="图片输出不可用,已回退到文本", zh_TW="圖片輸出不可用,已回退到文字", en="Image output unavailable, falling back to text", ja="画像出力は利用できません。テキストにフォールバックします", ru="Вывод изображений недоступен, используется текст")
|
|
108
|
+
|
|
109
|
+
cfg_show_hidden: I18nKey = I18nKey(key="HelpNext.cfg_show_hidden", default="Show hidden commands", zh_CN="显示隐藏命令", zh_TW="顯示隱藏命令", en="Show hidden commands", ja="非表示コマンドを表示", ru="Показывать скрытые команды")
|
|
110
|
+
cfg_group_commands: I18nKey = I18nKey(key="HelpNext.cfg_group_commands", default="Group commands by category", zh_CN="按分组显示命令", zh_TW="按分組顯示命令", en="Group commands by category", ja="カテゴリ別にグループ化", ru="Группировать команды")
|
|
111
|
+
cfg_theme: I18nKey = I18nKey(key="HelpNext.cfg_theme", default="Image theme", zh_CN="图片主题", zh_TW="圖片主題", en="Image theme", ja="画像テーマ", ru="Тема изображения")
|
|
112
|
+
cfg_utc_offset: I18nKey = I18nKey(key="HelpNext.cfg_utc_offset", default="UTC offset for day/night switching", zh_CN="UTC 时区偏移(用于昼夜切换)", zh_TW="UTC 時區偏移(用於晝夜切換)", en="UTC offset (day/night switching)", ja="UTCオフセット(昼夜切替用)", ru="Смещение UTC (для смены темы)")
|
|
113
|
+
cfg_show_logo: I18nKey = I18nKey(key="HelpNext.cfg_show_logo", default="Show ErisPulse icon in header", zh_CN="头部显示 ErisPulse 图标", zh_TW="頭部顯示 ErisPulse 圖示", en="Show ErisPulse icon in header", ja="ヘッダーに ErisPulse アイコンを表示", ru="Показывать иконку ErisPulse в шапке")
|
|
114
|
+
cfg_header_title: I18nKey = I18nKey(key="HelpNext.cfg_header_title", default="Custom header title (empty for default)", zh_CN="自定义头部标题(留空使用默认)", zh_TW="自訂頭部標題(留空使用預設)", en="Custom header title (empty for default)", ja="カスタムヘッダータイトル(空でデフォルト)", ru="Свой заголовок шапки (пусто = по умолчанию)")
|
|
115
|
+
cfg_header_subtitle: I18nKey = I18nKey(key="HelpNext.cfg_header_subtitle", default="Custom header subtitle (empty for default)", zh_CN="自定义头部副标题(留空使用默认)", zh_TW="自訂頭部副標題(留空使用預設)", en="Custom header subtitle (empty for default)", ja="カスタムヘッダーサブタイトル(空でデフォルト)", ru="Свой подзаголовок шапки (пусто = по умолчанию)")
|
|
116
|
+
|
|
117
|
+
theme_auto: I18nKey = I18nKey(key="HelpNext.theme_auto", default="Auto (by time)", zh_CN="自动(跟随时间)", zh_TW="自動(跟隨時間)", en="Auto (by time)", ja="自動(時間帯)", ru="Авто (по времени)")
|
|
118
|
+
theme_light: I18nKey = I18nKey(key="HelpNext.theme_light", default="Light", zh_CN="浅色", zh_TW="淺色", en="Light", ja="ライト", ru="Светлая")
|
|
119
|
+
theme_dark: I18nKey = I18nKey(key="HelpNext.theme_dark", default="Dark", zh_CN="深色", zh_TW="深色", en="Dark", ja="ダーク", ru="Тёмная")
|
|
120
|
+
group_basic: I18nKey = I18nKey(key="HelpNext.group_basic", default="Basic", zh_CN="基本", zh_TW="基本", en="Basic", ja="基本", ru="Основные")
|
|
121
|
+
group_render: I18nKey = I18nKey(key="HelpNext.group_render", default="Rendering", zh_CN="渲染", zh_TW="渲染", en="Rendering", ja="レンダリング", ru="Отрисовка")
|
|
122
|
+
group_header: I18nKey = I18nKey(key="HelpNext.group_header", default="Header", zh_CN="头部", zh_TW="頭部", en="Header", ja="ヘッダー", ru="Шапка")
|
|
123
|
+
|
|
124
|
+
def __init__(self):
|
|
125
|
+
self.sdk = sdk
|
|
126
|
+
self.logger = sdk.logger.get_child("HelpNext")
|
|
127
|
+
self.command_map: Dict[int, Dict] = {}
|
|
128
|
+
self.visualizer = Visualizer(sdk, {})
|
|
129
|
+
self._help_handler = None
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def get_load_strategy():
|
|
133
|
+
from ErisPulse.loaders import ModuleLoadStrategy
|
|
134
|
+
return ModuleLoadStrategy(
|
|
135
|
+
lazy_load=False,
|
|
136
|
+
priority=60,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
async def on_load(self, event):
|
|
140
|
+
self._help_handler = self._make_handler()
|
|
141
|
+
command(
|
|
142
|
+
"help",
|
|
143
|
+
aliases=["h", "帮助"],
|
|
144
|
+
help="显示帮助信息",
|
|
145
|
+
usage="help [序号] [--format <image|html|markdown|text>]",
|
|
146
|
+
)(self._help_handler)
|
|
147
|
+
self.logger.info("HelpNext 已加载")
|
|
148
|
+
|
|
149
|
+
async def on_unload(self, event):
|
|
150
|
+
if self._help_handler is not None:
|
|
151
|
+
command.unregister(self._help_handler)
|
|
152
|
+
self.logger.info("HelpNext 已卸载")
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
def _cfg_view(self) -> Dict:
|
|
156
|
+
cfg = self.cfg
|
|
157
|
+
return {
|
|
158
|
+
"show_hidden_commands": cfg.show_hidden_commands,
|
|
159
|
+
"group_commands": cfg.group_commands,
|
|
160
|
+
"theme": cfg.theme,
|
|
161
|
+
"utc_offset": cfg.utc_offset,
|
|
162
|
+
"show_logo": cfg.show_logo,
|
|
163
|
+
"header_title": cfg.header_title,
|
|
164
|
+
"header_subtitle": cfg.header_subtitle,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
def _all_prefixes(self) -> List[str]:
|
|
168
|
+
# 读取框架命令处理器维护的前缀(随配置热更新,与命令解析保持一致)
|
|
169
|
+
prefix = command.prefix
|
|
170
|
+
if isinstance(prefix, list):
|
|
171
|
+
return [str(p) for p in prefix] if prefix else ["/"]
|
|
172
|
+
return [str(prefix)] if prefix else ["/"]
|
|
173
|
+
|
|
174
|
+
def _build_command_list(self, event) -> List[Dict]:
|
|
175
|
+
cfg = self._cfg_view()
|
|
176
|
+
show_hidden = cfg["show_hidden_commands"]
|
|
177
|
+
result: List[Dict] = []
|
|
178
|
+
# 会话感知:按作用域过滤当前会话不可用模块的命令(与框架静默语义一致)
|
|
179
|
+
names = command.get_commands(event=event) if show_hidden else command.get_visible_commands(event=event)
|
|
180
|
+
for name in names:
|
|
181
|
+
info = command.get_command(name, event=event)
|
|
182
|
+
if info and name == info.get("main_name"):
|
|
183
|
+
result.append({"name": name, "info": info})
|
|
184
|
+
return result
|
|
185
|
+
|
|
186
|
+
def _make_handler(self):
|
|
187
|
+
async def help_command(event):
|
|
188
|
+
await self._handle(event)
|
|
189
|
+
return help_command
|
|
190
|
+
|
|
191
|
+
async def _handle(self, event) -> None:
|
|
192
|
+
try:
|
|
193
|
+
args = event.get_command_args()
|
|
194
|
+
commands = self._build_command_list(event)
|
|
195
|
+
prefixes = self._all_prefixes()
|
|
196
|
+
prefix = prefixes[0] if prefixes else "/"
|
|
197
|
+
cfg = self._cfg_view()
|
|
198
|
+
self.visualizer.config = cfg
|
|
199
|
+
|
|
200
|
+
self._index_commands(commands, cfg["group_commands"])
|
|
201
|
+
|
|
202
|
+
index, fmt, err = self._parse_args(args)
|
|
203
|
+
|
|
204
|
+
if err:
|
|
205
|
+
if err[0] == "format":
|
|
206
|
+
msg = HelpTemplates._t("err_fmt", fmt=err[1] or "?")
|
|
207
|
+
else:
|
|
208
|
+
msg = HelpTemplates._t("err_unknown", arg=err[1])
|
|
209
|
+
title = HelpTemplates._t("err_invalid_arg")
|
|
210
|
+
await self._send(event, HelpTemplates.build_error(title, msg), None, "auto-text")
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
image = None
|
|
214
|
+
templates = None
|
|
215
|
+
want_image = fmt in ("auto", "image")
|
|
216
|
+
|
|
217
|
+
if index is not None:
|
|
218
|
+
if index in self.command_map:
|
|
219
|
+
cmd = self.command_map[index]
|
|
220
|
+
if want_image:
|
|
221
|
+
image = self.visualizer.render_command_detail(cmd, prefix, prefixes)
|
|
222
|
+
templates = HelpTemplates.build_command_detail(cmd, prefix, prefixes)
|
|
223
|
+
else:
|
|
224
|
+
title = HelpTemplates._t("err_out_of_range")
|
|
225
|
+
msg = HelpTemplates._t("err_range_hint", count=len(commands))
|
|
226
|
+
if want_image:
|
|
227
|
+
image = self.visualizer.render_error(title, msg)
|
|
228
|
+
templates = HelpTemplates.build_error(title, msg)
|
|
229
|
+
else:
|
|
230
|
+
if want_image:
|
|
231
|
+
image = self.visualizer.render_help_list(
|
|
232
|
+
commands, self.command_map, prefix, cfg["group_commands"], prefixes,
|
|
233
|
+
)
|
|
234
|
+
templates = HelpTemplates.build_help_list(
|
|
235
|
+
commands, self.command_map, prefix, cfg["group_commands"], prefixes,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
await self._send(event, templates, image, fmt)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
self.logger.error(f"处理帮助命令出错: {e}", exc_info=True)
|
|
241
|
+
|
|
242
|
+
@staticmethod
|
|
243
|
+
def _normalize_format(v) -> Optional[str]:
|
|
244
|
+
v = str(v).lower().strip()
|
|
245
|
+
return {
|
|
246
|
+
"image": "image", "img": "image",
|
|
247
|
+
"html": "html", "h5": "html",
|
|
248
|
+
"markdown": "markdown", "md": "markdown",
|
|
249
|
+
"text": "text", "txt": "text",
|
|
250
|
+
}.get(v)
|
|
251
|
+
|
|
252
|
+
@staticmethod
|
|
253
|
+
def _parse_args(args: List[str]) -> Tuple[Optional[int], str, Optional[Tuple[str, str]]]:
|
|
254
|
+
index = None
|
|
255
|
+
fmt = "auto"
|
|
256
|
+
err = None
|
|
257
|
+
i, n = 0, len(args)
|
|
258
|
+
while i < n:
|
|
259
|
+
low = str(args[i]).lower()
|
|
260
|
+
if low.isdigit():
|
|
261
|
+
if index is None:
|
|
262
|
+
index = int(low)
|
|
263
|
+
else:
|
|
264
|
+
err = ("unknown", args[i])
|
|
265
|
+
break
|
|
266
|
+
elif low == "--format":
|
|
267
|
+
if i + 1 < n:
|
|
268
|
+
v = Main._normalize_format(args[i + 1])
|
|
269
|
+
if v is None:
|
|
270
|
+
err = ("format", str(args[i + 1]))
|
|
271
|
+
break
|
|
272
|
+
fmt = v
|
|
273
|
+
i += 1
|
|
274
|
+
else:
|
|
275
|
+
err = ("format", "")
|
|
276
|
+
break
|
|
277
|
+
elif low.startswith("--format="):
|
|
278
|
+
v = Main._normalize_format(low.split("=", 1)[1])
|
|
279
|
+
if v is None:
|
|
280
|
+
err = ("format", low.split("=", 1)[1])
|
|
281
|
+
break
|
|
282
|
+
fmt = v
|
|
283
|
+
else:
|
|
284
|
+
err = ("unknown", args[i])
|
|
285
|
+
break
|
|
286
|
+
i += 1
|
|
287
|
+
return index, fmt, err
|
|
288
|
+
|
|
289
|
+
def _index_commands(self, commands: List[Dict], group_commands: bool) -> None:
|
|
290
|
+
self.command_map = {}
|
|
291
|
+
grouped: Dict[str, List[Dict]] = {}
|
|
292
|
+
if group_commands:
|
|
293
|
+
for cmd in commands:
|
|
294
|
+
g = cmd["info"].get("group") or "default"
|
|
295
|
+
grouped.setdefault(g, []).append(cmd)
|
|
296
|
+
else:
|
|
297
|
+
grouped["default"] = list(commands)
|
|
298
|
+
idx = 1
|
|
299
|
+
for _, cmds in grouped.items():
|
|
300
|
+
for cmd in cmds:
|
|
301
|
+
self.command_map[idx] = cmd
|
|
302
|
+
idx += 1
|
|
303
|
+
|
|
304
|
+
@staticmethod
|
|
305
|
+
def _supports(event, method: str) -> bool:
|
|
306
|
+
try:
|
|
307
|
+
return event.supports(method)
|
|
308
|
+
except Exception:
|
|
309
|
+
return False
|
|
310
|
+
|
|
311
|
+
def _select_text_format(self, event, templates) -> Tuple[str, str]:
|
|
312
|
+
if self._supports(event, "Html"):
|
|
313
|
+
return ("Html", templates["html"])
|
|
314
|
+
if self._supports(event, "Markdown"):
|
|
315
|
+
return ("Markdown", templates["markdown"])
|
|
316
|
+
return ("Text", templates["text"])
|
|
317
|
+
|
|
318
|
+
async def _send(self, event, templates, image: Optional[bytes], fmt: str) -> None:
|
|
319
|
+
try:
|
|
320
|
+
if fmt == "image":
|
|
321
|
+
if image and self._supports(event, "Image"):
|
|
322
|
+
await event.reply(image, method="Image")
|
|
323
|
+
return
|
|
324
|
+
note = HelpTemplates._t("err_img_unavailable")
|
|
325
|
+
await event.reply(note)
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
if fmt in ("html", "markdown", "text"):
|
|
329
|
+
await event.reply(templates[fmt], method=fmt.capitalize())
|
|
330
|
+
return
|
|
331
|
+
|
|
332
|
+
if fmt == "auto" and image and self._supports(event, "Image"):
|
|
333
|
+
try:
|
|
334
|
+
await event.reply(image, method="Image")
|
|
335
|
+
return
|
|
336
|
+
except Exception as e:
|
|
337
|
+
self.logger.warning(f"图片发送失败,回退到文本: {e}")
|
|
338
|
+
|
|
339
|
+
method, content = self._select_text_format(event, templates)
|
|
340
|
+
try:
|
|
341
|
+
await event.reply(content, method=method)
|
|
342
|
+
except Exception:
|
|
343
|
+
await event.reply(templates["text"])
|
|
344
|
+
except Exception as e:
|
|
345
|
+
self.logger.error(f"发送帮助出错: {e}")
|
|
346
|
+
try:
|
|
347
|
+
await event.reply(templates["text"])
|
|
348
|
+
except Exception:
|
|
349
|
+
pass
|
HelpNext/Templates.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
from typing import Dict, List, Optional
|
|
2
|
+
|
|
3
|
+
from ErisPulse import i18n
|
|
4
|
+
from ErisPulse.Core.Event import command
|
|
5
|
+
|
|
6
|
+
class HelpTemplates:
|
|
7
|
+
"""i18n-aware fallback templates (html / markdown / text)."""
|
|
8
|
+
|
|
9
|
+
PRIMARY_COLOR = "#0071e3"
|
|
10
|
+
WARNING_COLOR = "#ff9f0a"
|
|
11
|
+
ERROR_COLOR = "#ff453a"
|
|
12
|
+
PRIMARY_BG = "rgba(0, 113, 227, 0.06)"
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def _t(cls, key: str, **kwargs) -> str:
|
|
16
|
+
full = f"HelpNext.{key}"
|
|
17
|
+
return i18n.t(full, default=full, **kwargs)
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def _group_name(cls, group: str) -> str:
|
|
21
|
+
if not group or group == "default":
|
|
22
|
+
return cls._t("group_default")
|
|
23
|
+
return group
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def _aliases_of(cls, name: str, info: Dict) -> List[str]:
|
|
27
|
+
main_name = info.get("main_name", name)
|
|
28
|
+
return [
|
|
29
|
+
alias
|
|
30
|
+
for alias, mapped in command.aliases.items()
|
|
31
|
+
if mapped == main_name and alias != main_name
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def _other_prefixes(cls, prefixes: Optional[list], display: str) -> list:
|
|
36
|
+
if not prefixes or len(prefixes) <= 1:
|
|
37
|
+
return []
|
|
38
|
+
return [p for p in prefixes if p != display]
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def build_help_list(
|
|
42
|
+
cls,
|
|
43
|
+
commands: List[Dict],
|
|
44
|
+
command_map: Dict[int, Dict],
|
|
45
|
+
prefix: str,
|
|
46
|
+
group_commands: bool = True,
|
|
47
|
+
prefixes: Optional[list] = None,
|
|
48
|
+
) -> Dict[str, str]:
|
|
49
|
+
others = cls._other_prefixes(prefixes or [prefix], prefix)
|
|
50
|
+
grouped = cls._group(commands, group_commands)
|
|
51
|
+
|
|
52
|
+
global_idx = 1
|
|
53
|
+
for _, cmds in grouped.items():
|
|
54
|
+
for cmd in cmds:
|
|
55
|
+
command_map[global_idx] = cmd
|
|
56
|
+
global_idx += 1
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
"html": cls._list_html(commands, grouped, command_map, prefix, others),
|
|
60
|
+
"markdown": cls._list_md(commands, grouped, command_map, prefix, others),
|
|
61
|
+
"text": cls._list_text(commands, grouped, command_map, prefix, others),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def _group(commands: List[Dict], group_commands: bool) -> Dict[str, List[Dict]]:
|
|
66
|
+
if not group_commands:
|
|
67
|
+
return {"default": list(commands)}
|
|
68
|
+
grouped: Dict[str, List[Dict]] = {}
|
|
69
|
+
for cmd in commands:
|
|
70
|
+
g = cmd["info"].get("group") or "default"
|
|
71
|
+
grouped.setdefault(g, []).append(cmd)
|
|
72
|
+
return grouped
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def _list_html(cls, commands, grouped, command_map, prefix, others) -> str:
|
|
76
|
+
title = cls._t("title")
|
|
77
|
+
hint = cls._t("usage_hint", prefix=prefix)
|
|
78
|
+
count = cls._t("command_count", count=len(commands))
|
|
79
|
+
|
|
80
|
+
sections = ""
|
|
81
|
+
for group, cmds in grouped.items():
|
|
82
|
+
sections += (
|
|
83
|
+
f'<div style="font-size:13px;margin-bottom:8px;font-weight:600;'
|
|
84
|
+
f'color:{cls.PRIMARY_COLOR};">{cls._group_name(group)}</div>'
|
|
85
|
+
)
|
|
86
|
+
for cmd in cmds:
|
|
87
|
+
idx = next(
|
|
88
|
+
i for i, c in command_map.items() if c["name"] == cmd["name"]
|
|
89
|
+
)
|
|
90
|
+
name = cmd["name"]
|
|
91
|
+
help_text = cmd["info"].get("help") or cls._t("no_description")
|
|
92
|
+
sections += (
|
|
93
|
+
f'<div style="margin-bottom:6px;font-size:13px;">'
|
|
94
|
+
f'<b style="margin-right:6px;">{idx}.</b>'
|
|
95
|
+
f'<code style="background:rgba(0,0,0,0.05);padding:2px 6px;'
|
|
96
|
+
f'border-radius:4px;margin-right:6px;">{prefix}{name}</code>'
|
|
97
|
+
f'<span style="color:#666;">- {help_text}</span></div>'
|
|
98
|
+
)
|
|
99
|
+
sections += "\n"
|
|
100
|
+
|
|
101
|
+
others_html = cls._prefix_note_html(others)
|
|
102
|
+
return (
|
|
103
|
+
f'<div style="padding:12px;border-radius:8px;">'
|
|
104
|
+
f'<div style="color:{cls.PRIMARY_COLOR};font-size:16px;font-weight:700;'
|
|
105
|
+
f'margin-bottom:12px;">{title}</div>'
|
|
106
|
+
f'<div style="padding:8px;background:{cls.PRIMARY_BG};border-radius:6px;'
|
|
107
|
+
f'margin-bottom:12px;font-size:13px;">{hint}</div>'
|
|
108
|
+
f'{sections}'
|
|
109
|
+
f'<div style="font-size:12px;color:#666;margin-top:8px;">{count}</div>'
|
|
110
|
+
f'{others_html}'
|
|
111
|
+
f'</div>'
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def _list_md(cls, commands, grouped, command_map, prefix, others) -> str:
|
|
116
|
+
lines = [
|
|
117
|
+
f"**{cls._t('title')}**",
|
|
118
|
+
"",
|
|
119
|
+
cls._t("usage_hint", prefix=prefix),
|
|
120
|
+
"",
|
|
121
|
+
]
|
|
122
|
+
for group, cmds in grouped.items():
|
|
123
|
+
lines.append(f"**{cls._group_name(group)}**")
|
|
124
|
+
lines.append("")
|
|
125
|
+
for cmd in cmds:
|
|
126
|
+
idx = next(i for i, c in command_map.items() if c["name"] == cmd["name"])
|
|
127
|
+
help_text = cmd["info"].get("help") or cls._t("no_description")
|
|
128
|
+
lines.append(f"{idx}. `{prefix}{cmd['name']}` - {help_text}")
|
|
129
|
+
lines.append("")
|
|
130
|
+
|
|
131
|
+
lines.append("---")
|
|
132
|
+
lines.append(cls._t("command_count", count=len(commands)))
|
|
133
|
+
if others:
|
|
134
|
+
lines.append("")
|
|
135
|
+
lines.append(f"{cls._t('other_prefixes')}: {'、'.join(others)}")
|
|
136
|
+
return "\n".join(lines)
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def _list_text(cls, commands, grouped, command_map, prefix, others) -> str:
|
|
140
|
+
lines = [
|
|
141
|
+
cls._t("title"),
|
|
142
|
+
"----------",
|
|
143
|
+
cls._t("usage_hint", prefix=prefix),
|
|
144
|
+
"",
|
|
145
|
+
]
|
|
146
|
+
for group, cmds in grouped.items():
|
|
147
|
+
lines.append(f"[{cls._group_name(group)}]")
|
|
148
|
+
lines.append("")
|
|
149
|
+
for cmd in cmds:
|
|
150
|
+
idx = next(i for i, c in command_map.items() if c["name"] == cmd["name"])
|
|
151
|
+
help_text = cmd["info"].get("help") or cls._t("no_description")
|
|
152
|
+
lines.append(f"{idx}. {prefix}{cmd['name']} - {help_text}")
|
|
153
|
+
lines.append("")
|
|
154
|
+
|
|
155
|
+
lines.append("----------")
|
|
156
|
+
lines.append(cls._t("command_count", count=len(commands)))
|
|
157
|
+
if others:
|
|
158
|
+
lines.append("")
|
|
159
|
+
lines.append(f"{cls._t('other_prefixes')}: {'、'.join(others)}")
|
|
160
|
+
return "\n".join(lines)
|
|
161
|
+
|
|
162
|
+
@classmethod
|
|
163
|
+
def _prefix_note_html(cls, others) -> str:
|
|
164
|
+
if not others:
|
|
165
|
+
return ""
|
|
166
|
+
note = "、".join(f"<code style='font-size:11px;'>{p}</code>" for p in others)
|
|
167
|
+
return (
|
|
168
|
+
f'<div style="font-size:11px;color:#999;margin-top:4px;">'
|
|
169
|
+
f'{cls._t("other_prefixes")}: {note}</div>'
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def build_command_detail(cls, cmd: Dict, prefix: str, prefixes: Optional[list] = None) -> Dict[str, str]:
|
|
174
|
+
others = cls._other_prefixes(prefixes or [prefix], prefix)
|
|
175
|
+
return {
|
|
176
|
+
"html": cls._detail_html(cmd, prefix, others),
|
|
177
|
+
"markdown": cls._detail_md(cmd, prefix, others),
|
|
178
|
+
"text": cls._detail_text(cmd, prefix, others),
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@classmethod
|
|
182
|
+
def _detail_html(cls, cmd: Dict, prefix: str, others) -> str:
|
|
183
|
+
name = cmd["name"]
|
|
184
|
+
info = cmd["info"]
|
|
185
|
+
title = cls._t("detail_title")
|
|
186
|
+
parts = [
|
|
187
|
+
f'<div style="padding:12px;border-radius:8px;">'
|
|
188
|
+
f'<div style="color:{cls.PRIMARY_COLOR};font-size:16px;font-weight:700;'
|
|
189
|
+
f'margin-bottom:12px;">{title}: <code style="background:rgba(0,0,0,0.05);'
|
|
190
|
+
f'padding:2px 6px;border-radius:4px;">{prefix}{name}</code></div>'
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
parts.append(cls._kv_html(cls._t("label_description"),
|
|
194
|
+
info.get("help") or cls._t("no_description")))
|
|
195
|
+
|
|
196
|
+
aliases = cls._aliases_of(name, info)
|
|
197
|
+
if aliases:
|
|
198
|
+
parts.append(cls._kv_html(cls._t("label_aliases"),
|
|
199
|
+
", ".join(f"{prefix}{a}" for a in aliases)))
|
|
200
|
+
|
|
201
|
+
if info.get("usage"):
|
|
202
|
+
parts.append(cls._kv_html(cls._t("label_usage"),
|
|
203
|
+
info["usage"].replace("/", prefix), mono=True))
|
|
204
|
+
|
|
205
|
+
if info.get("permission"):
|
|
206
|
+
parts.append(cls._kv_html(cls._t("label_permission"),
|
|
207
|
+
cls._t("permission_required"), warn=True))
|
|
208
|
+
|
|
209
|
+
if info.get("group"):
|
|
210
|
+
parts.append(cls._kv_html(cls._t("label_group"),
|
|
211
|
+
cls._group_name(info["group"])))
|
|
212
|
+
|
|
213
|
+
if others:
|
|
214
|
+
parts.append(cls._prefix_note_html(others))
|
|
215
|
+
|
|
216
|
+
parts.append("</div>")
|
|
217
|
+
return "\n".join(parts)
|
|
218
|
+
|
|
219
|
+
@classmethod
|
|
220
|
+
def _kv_html(cls, label: str, value: str, mono: bool = False, warn: bool = False) -> str:
|
|
221
|
+
color = cls.WARNING_COLOR if warn else "inherit"
|
|
222
|
+
style = "font-family:monospace;background:rgba(0,0,0,0.03);padding:2px 6px;border-radius:4px;" if mono else ""
|
|
223
|
+
return (
|
|
224
|
+
f'<div style="margin-bottom:8px;">'
|
|
225
|
+
f'<div style="font-size:13px;margin-bottom:4px;"><b>{label}:</b></div>'
|
|
226
|
+
f'<div style="font-size:13px;color:{color};{style}">{value}</div></div>'
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
@classmethod
|
|
230
|
+
def _detail_md(cls, cmd: Dict, prefix: str, others) -> str:
|
|
231
|
+
name = cmd["name"]
|
|
232
|
+
info = cmd["info"]
|
|
233
|
+
lines = [
|
|
234
|
+
f"**{cls._t('detail_title')}:** `{prefix}{name}`",
|
|
235
|
+
"",
|
|
236
|
+
f"**{cls._t('label_description')}:** {info.get('help') or cls._t('no_description')}",
|
|
237
|
+
"",
|
|
238
|
+
]
|
|
239
|
+
aliases = cls._aliases_of(name, info)
|
|
240
|
+
if aliases:
|
|
241
|
+
lines.append(f"**{cls._t('label_aliases')}:** {', '.join(f'`{prefix}{a}`' for a in aliases)}")
|
|
242
|
+
lines.append("")
|
|
243
|
+
if info.get("usage"):
|
|
244
|
+
lines.append(f"**{cls._t('label_usage')}:** `{info['usage'].replace('/', prefix)}`")
|
|
245
|
+
lines.append("")
|
|
246
|
+
if info.get("permission"):
|
|
247
|
+
lines.append(f"**{cls._t('label_permission')}:** {cls._t('permission_required')}")
|
|
248
|
+
lines.append("")
|
|
249
|
+
if info.get("group"):
|
|
250
|
+
lines.append(f"**{cls._t('label_group')}:** {cls._group_name(info['group'])}")
|
|
251
|
+
lines.append("")
|
|
252
|
+
if others:
|
|
253
|
+
lines.append(f"{cls._t('other_prefixes')}: {'、'.join(others)}")
|
|
254
|
+
return "\n".join(lines)
|
|
255
|
+
|
|
256
|
+
@classmethod
|
|
257
|
+
def _detail_text(cls, cmd: Dict, prefix: str, others) -> str:
|
|
258
|
+
name = cmd["name"]
|
|
259
|
+
info = cmd["info"]
|
|
260
|
+
lines = [
|
|
261
|
+
f"{cls._t('detail_title')}: {prefix}{name}",
|
|
262
|
+
"----------",
|
|
263
|
+
f"{cls._t('label_description')}: {info.get('help') or cls._t('no_description')}",
|
|
264
|
+
"",
|
|
265
|
+
]
|
|
266
|
+
aliases = cls._aliases_of(name, info)
|
|
267
|
+
if aliases:
|
|
268
|
+
lines.append(f"{cls._t('label_aliases')}: {', '.join(f'{prefix}{a}' for a in aliases)}")
|
|
269
|
+
lines.append("")
|
|
270
|
+
if info.get("usage"):
|
|
271
|
+
lines.append(f"{cls._t('label_usage')}: {info['usage'].replace('/', prefix)}")
|
|
272
|
+
lines.append("")
|
|
273
|
+
if info.get("permission"):
|
|
274
|
+
lines.append(f"{cls._t('label_permission')}: {cls._t('permission_required')}")
|
|
275
|
+
lines.append("")
|
|
276
|
+
if info.get("group"):
|
|
277
|
+
lines.append(f"{cls._t('label_group')}: {cls._group_name(info['group'])}")
|
|
278
|
+
lines.append("")
|
|
279
|
+
if others:
|
|
280
|
+
lines.append(f"{cls._t('other_prefixes')}: {'、'.join(others)}")
|
|
281
|
+
return "\n".join(lines)
|
|
282
|
+
|
|
283
|
+
@classmethod
|
|
284
|
+
def build_error(cls, title: str, message: str) -> Dict[str, str]:
|
|
285
|
+
html = (
|
|
286
|
+
f'<div style="padding:12px;border-radius:8px;">'
|
|
287
|
+
f'<div style="color:{cls.ERROR_COLOR};font-size:14px;font-weight:700;'
|
|
288
|
+
f'margin-bottom:8px;">{title}</div>'
|
|
289
|
+
f'<div style="font-size:13px;">{message}</div></div>'
|
|
290
|
+
)
|
|
291
|
+
markdown = f"**{title}**\n\n{message}"
|
|
292
|
+
text = f"{title}\n\n{message}"
|
|
293
|
+
return {"html": html, "markdown": markdown, "text": text}
|
HelpNext/Visualizer.py
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import html
|
|
3
|
+
import math
|
|
4
|
+
import struct
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, List, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
from ErisPulse import i18n
|
|
10
|
+
from ErisPulse.Core.Event import command
|
|
11
|
+
|
|
12
|
+
class Visualizer:
|
|
13
|
+
CARD_WIDTH = 880
|
|
14
|
+
_PAGE_PAD = 36
|
|
15
|
+
_CARD_PAD_V = 16
|
|
16
|
+
_CARD_PAD_H = 18
|
|
17
|
+
_CARD_GAP = 14
|
|
18
|
+
_COL_GAP = 14
|
|
19
|
+
_CARD_BORDER = 2
|
|
20
|
+
_LOGO_W = 120
|
|
21
|
+
|
|
22
|
+
ACCENT = "#0071e3"
|
|
23
|
+
ACCENT_DARK = "#0a84ff"
|
|
24
|
+
WARN = "#ff9f0a"
|
|
25
|
+
|
|
26
|
+
PALETTE = [
|
|
27
|
+
"#0a84ff", "#5e5ce6", "#bf5af2", "#ff375f", "#ff9f0a",
|
|
28
|
+
"#34c759", "#64d2ff", "#30b0c7", "#ff453a", "#8e8e93",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
_ICON_PATH = Path(__file__).parent / "assets" / "icon.png"
|
|
32
|
+
_icon_cache: Optional[Tuple[str, Tuple[int, int]]] = None
|
|
33
|
+
|
|
34
|
+
_CSS_TPL = """
|
|
35
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
36
|
+
body {
|
|
37
|
+
font-family: "Noto Sans SC", "Source Han Sans SC", sans-serif;
|
|
38
|
+
background: __PAGE__; color: __INK__; -webkit-font-smoothing: antialiased;
|
|
39
|
+
padding: 36px;
|
|
40
|
+
}
|
|
41
|
+
.card {
|
|
42
|
+
background: __CARD__; border: 1px solid __BORDER__; border-radius: 16px;
|
|
43
|
+
padding: 16px 18px; box-shadow: __SHADOW__; margin-bottom: 12px;
|
|
44
|
+
}
|
|
45
|
+
.head-row { display: flex; align-items: center; gap: 14px; }
|
|
46
|
+
.logo-side { display: block; }
|
|
47
|
+
.head-text { min-width: 0; }
|
|
48
|
+
.title { font-size: 20px; font-weight: 700; color: __INK__; letter-spacing: -0.3px; }
|
|
49
|
+
.subtitle { font-size: 13px; color: __SUB__; margin-top: 2px; }
|
|
50
|
+
.divider { height: 1px; background: __SEP__; margin: 14px 0; }
|
|
51
|
+
.chips { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
|
|
52
|
+
.chip { padding: 7px 14px; border-radius: 9px; font-size: 13px; background: __SOFT__; color: __INK__; border: 1px solid __BORDER__; }
|
|
53
|
+
.chip b { color: __ACCENT__; font-weight: 600; margin-right: 4px; }
|
|
54
|
+
.masonry { display: flex; gap: 14px; align-items: flex-start; }
|
|
55
|
+
.mcol { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 14px; }
|
|
56
|
+
.mcol .card { margin-bottom: 0; }
|
|
57
|
+
.cmd-head { display: flex; align-items: center; gap: 10px; }
|
|
58
|
+
.num {
|
|
59
|
+
flex: 0 0 26px; width: 26px; height: 26px; border-radius: 8px;
|
|
60
|
+
background: __ACCENTBG__; border: 1px solid __ACCENTLINE__;
|
|
61
|
+
color: __ACCENT__; font-size: 12px; font-weight: 700;
|
|
62
|
+
display: flex; align-items: center; justify-content: center;
|
|
63
|
+
font-variant-numeric: tabular-nums;
|
|
64
|
+
}
|
|
65
|
+
.cmd-code {
|
|
66
|
+
font-family: "Source Code Pro", monospace; font-size: 13.5px; font-weight: 700;
|
|
67
|
+
color: __INK__; white-space: nowrap;
|
|
68
|
+
}
|
|
69
|
+
.cmd-code .pfx { color: __ACCENT__; font-weight: 600; }
|
|
70
|
+
.group-tag {
|
|
71
|
+
margin-left: auto; font-size: 11px; font-weight: 600;
|
|
72
|
+
padding: 3px 10px; border-radius: 6px; white-space: nowrap;
|
|
73
|
+
display: inline-flex; align-items: center; gap: 5px;
|
|
74
|
+
}
|
|
75
|
+
.group-tag::before {
|
|
76
|
+
content: ""; width: 6px; height: 6px; border-radius: 50%;
|
|
77
|
+
background: currentColor; flex: 0 0 auto;
|
|
78
|
+
}
|
|
79
|
+
.cmd-desc { font-size: 13px; color: __INK__; margin-top: 8px; line-height: 1.55; }
|
|
80
|
+
.cmd-aliases { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
|
81
|
+
.cmd-aliases .lbl { font-size: 11px; color: __SUB__; margin-right: 2px; }
|
|
82
|
+
.alias-tag {
|
|
83
|
+
font-family: "Source Code Pro", monospace; font-size: 12px;
|
|
84
|
+
background: __CODEBG__; color: __INK__; padding: 3px 9px; border-radius: 6px;
|
|
85
|
+
}
|
|
86
|
+
.cmd-usage {
|
|
87
|
+
margin-top: 8px; font-family: "Source Code Pro", monospace; font-size: 12.5px;
|
|
88
|
+
background: __CODEBG__; color: __SUB__; padding: 7px 10px; border-radius: 7px;
|
|
89
|
+
line-height: 1.5;
|
|
90
|
+
}
|
|
91
|
+
.detail-label { font-size: 12px; font-weight: 600; color: __SUB__; letter-spacing: 0.6px; margin-bottom: 6px; text-transform: uppercase; }
|
|
92
|
+
.detail-value { font-size: 15px; color: __INK__; line-height: 1.5; }
|
|
93
|
+
.detail-value.mono { font-family: "Source Code Pro", monospace; background: __CODEBG__; padding: 8px 12px; border-radius: 8px; font-size: 14px; }
|
|
94
|
+
.detail-value.warn { color: __WARN__; }
|
|
95
|
+
.aliases { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
96
|
+
.foot { font-size: 12.5px; color: __SUB__; text-align: center; margin-top: 14px; line-height: 1.7; }
|
|
97
|
+
.foot code { color: __ACCENT__; background: __CODEBG__; padding: 2px 7px; border-radius: 5px; font-family: "Source Code Pro", monospace; }
|
|
98
|
+
.err-title { font-size: 18px; font-weight: 700; color: __WARN__; }
|
|
99
|
+
.err-msg { font-size: 14px; color: __INK__; margin-top: 8px; line-height: 1.5; }
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(self, sdk, config: Dict):
|
|
103
|
+
self.sdk = sdk
|
|
104
|
+
self.logger = sdk.logger.get_child("HelpNext.Visualizer")
|
|
105
|
+
self.config = config
|
|
106
|
+
self._takumi_inst = None
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def _t(key: str, **kwargs) -> str:
|
|
110
|
+
full = f"HelpNext.{key}"
|
|
111
|
+
return i18n.t(full, default=full, **kwargs)
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def takumi(self):
|
|
115
|
+
if self._takumi_inst is None:
|
|
116
|
+
inst = None
|
|
117
|
+
try:
|
|
118
|
+
inst = self.sdk.module.get("Takumi")
|
|
119
|
+
except Exception:
|
|
120
|
+
inst = None
|
|
121
|
+
if inst is None:
|
|
122
|
+
inst = getattr(self.sdk, "Takumi", None)
|
|
123
|
+
self._takumi_inst = inst
|
|
124
|
+
return self._takumi_inst
|
|
125
|
+
|
|
126
|
+
def _theme(self) -> Dict:
|
|
127
|
+
mode = self.config.get("theme", "auto")
|
|
128
|
+
if mode == "auto":
|
|
129
|
+
offset = self.config.get("utc_offset", 8)
|
|
130
|
+
hour = int((time.time() / 3600 + offset) % 24)
|
|
131
|
+
mode = "dark" if (hour >= 19 or hour < 7) else "light"
|
|
132
|
+
accent = self.ACCENT_DARK if mode == "dark" else self.ACCENT
|
|
133
|
+
if mode == "dark":
|
|
134
|
+
return {
|
|
135
|
+
"page": "#000000", "card": "#1c1c1e", "ink": "#f5f5f7", "sub": "#8e8e93",
|
|
136
|
+
"sep": "#38383a", "soft": "#2c2c2e", "codebg": "rgba(255,255,255,0.08)",
|
|
137
|
+
"accent": accent, "tag_alpha": 0.22, "shadow": "none",
|
|
138
|
+
"border": "rgba(255,255,255,0.08)",
|
|
139
|
+
"accentline": self._rgba(accent, 0.35),
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
"page": "#f5f5f7", "card": "#ffffff", "ink": "#1d1d1f", "sub": "#6e6e73",
|
|
143
|
+
"sep": "#d2d2d7", "soft": "#f5f5f7", "codebg": "rgba(0,0,0,0.05)",
|
|
144
|
+
"accent": accent, "tag_alpha": 0.12, "shadow": "0 1px 2px rgba(0,0,0,0.04), 0 2px 8px rgba(0,0,0,0.04)",
|
|
145
|
+
"border": "rgba(0,0,0,0.06)",
|
|
146
|
+
"accentline": self._rgba(accent, 0.25),
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
def _css(self) -> Tuple[str, Dict]:
|
|
150
|
+
t = self._theme()
|
|
151
|
+
css = (
|
|
152
|
+
self._CSS_TPL
|
|
153
|
+
.replace("__PAGE__", t["page"]).replace("__CARD__", t["card"])
|
|
154
|
+
.replace("__INK__", t["ink"]).replace("__SUB__", t["sub"])
|
|
155
|
+
.replace("__SEP__", t["sep"]).replace("__SOFT__", t["soft"])
|
|
156
|
+
.replace("__CODEBG__", t["codebg"]).replace("__ACCENT__", t["accent"])
|
|
157
|
+
.replace("__ACCENTBG__", self._rgba(t["accent"], t["tag_alpha"]))
|
|
158
|
+
.replace("__ACCENTLINE__", t["accentline"])
|
|
159
|
+
.replace("__BORDER__", t["border"])
|
|
160
|
+
.replace("__SHADOW__", t["shadow"])
|
|
161
|
+
)
|
|
162
|
+
return css, t
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def _rgba(hexcolor: str, alpha: float) -> str:
|
|
166
|
+
h = hexcolor.lstrip("#")
|
|
167
|
+
r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
|
|
168
|
+
return f"rgba({r},{g},{b},{alpha})"
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def _png_dims(data: bytes) -> Optional[Tuple[int, int]]:
|
|
172
|
+
try:
|
|
173
|
+
if data[:8] != b"\x89PNG\r\n\x1a\n":
|
|
174
|
+
return None
|
|
175
|
+
w, h = struct.unpack(">II", data[16:24])
|
|
176
|
+
return w, h
|
|
177
|
+
except Exception:
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
@classmethod
|
|
181
|
+
def _icon(cls) -> Optional[Tuple[str, Tuple[int, int]]]:
|
|
182
|
+
if cls._icon_cache is None:
|
|
183
|
+
try:
|
|
184
|
+
data = cls._ICON_PATH.read_bytes()
|
|
185
|
+
dims = cls._png_dims(data)
|
|
186
|
+
b64 = base64.b64encode(data).decode()
|
|
187
|
+
cls._icon_cache = (b64, dims) if dims else ("", None)
|
|
188
|
+
except Exception:
|
|
189
|
+
cls._icon_cache = ("", None)
|
|
190
|
+
b64, dims = cls._icon_cache
|
|
191
|
+
if not b64 or not dims:
|
|
192
|
+
return None
|
|
193
|
+
return b64, dims
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def _esc(text) -> str:
|
|
197
|
+
return html.escape(str(text))
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def _text_lines(text: str, width: int, cjk_w: float = 13.0, latin_w: float = 7.5) -> int:
|
|
201
|
+
w = sum(cjk_w if ord(c) > 0x2E80 else latin_w for c in str(text))
|
|
202
|
+
return max(1, math.ceil(w / max(1, width)))
|
|
203
|
+
|
|
204
|
+
@staticmethod
|
|
205
|
+
def _aliases_of(name: str, info: Dict) -> List[str]:
|
|
206
|
+
main_name = info.get("main_name", name)
|
|
207
|
+
return [
|
|
208
|
+
a for a, m in command.aliases.items()
|
|
209
|
+
if m == main_name and a != main_name
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
def _render(self, body_html: str, height: int) -> Optional[bytes]:
|
|
213
|
+
takumi = self.takumi
|
|
214
|
+
if takumi is None or not hasattr(takumi, "render_html"):
|
|
215
|
+
self.logger.warning("Takumi 不可用,跳过图片渲染")
|
|
216
|
+
return None
|
|
217
|
+
css, _ = self._css()
|
|
218
|
+
try:
|
|
219
|
+
return takumi.render_html(
|
|
220
|
+
body_html, stylesheets=[css],
|
|
221
|
+
width=self.CARD_WIDTH, height=height, lang="zh-CN",
|
|
222
|
+
)
|
|
223
|
+
except Exception as e:
|
|
224
|
+
self.logger.error(f"Takumi 渲染失败: {e}")
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
def _card(self, inner: str) -> str:
|
|
228
|
+
return f"<div class='card'>{inner}</div>"
|
|
229
|
+
|
|
230
|
+
def _h_card(self, inner_h: int) -> int:
|
|
231
|
+
return self._CARD_PAD_V * 2 + inner_h + self._CARD_BORDER + self._CARD_GAP
|
|
232
|
+
|
|
233
|
+
def _total_height(self, block_heights: List[int], footer_h: int = 0) -> int:
|
|
234
|
+
return self._PAGE_PAD * 2 + sum(block_heights) + footer_h + 16
|
|
235
|
+
|
|
236
|
+
@staticmethod
|
|
237
|
+
def _num_cols(count: int) -> int:
|
|
238
|
+
if count <= 8:
|
|
239
|
+
return 1
|
|
240
|
+
if count <= 24:
|
|
241
|
+
return 2
|
|
242
|
+
return 3
|
|
243
|
+
|
|
244
|
+
def _header(self, subtitle: str, chips_html: str) -> Tuple[str, int]:
|
|
245
|
+
cfg = self.config
|
|
246
|
+
show_logo = cfg.get("show_logo", True)
|
|
247
|
+
title = cfg.get("header_title") or "ErisPulse"
|
|
248
|
+
sub = cfg.get("header_subtitle") or subtitle
|
|
249
|
+
|
|
250
|
+
logo_html = ""
|
|
251
|
+
logo_h = 0
|
|
252
|
+
if show_logo:
|
|
253
|
+
icon = self._icon()
|
|
254
|
+
if icon:
|
|
255
|
+
b64, (nw, nh) = icon
|
|
256
|
+
dw = self._LOGO_W
|
|
257
|
+
dh = round(nh * dw / nw)
|
|
258
|
+
logo_html = (
|
|
259
|
+
f"<img class='logo-side' src='data:image/png;base64,{b64}' "
|
|
260
|
+
f"width='{dw}' height='{dh}' alt='ErisPulse'/>"
|
|
261
|
+
)
|
|
262
|
+
logo_h = dh
|
|
263
|
+
|
|
264
|
+
text_html = (
|
|
265
|
+
f"<div class='head-text'><div class='title'>{self._esc(title)}</div>"
|
|
266
|
+
f"<div class='subtitle'>{self._esc(sub)}</div></div>"
|
|
267
|
+
)
|
|
268
|
+
inner = (
|
|
269
|
+
f"<div class='head-row'>{logo_html}{text_html}</div>"
|
|
270
|
+
f"<div class='divider'></div>{chips_html}"
|
|
271
|
+
)
|
|
272
|
+
row_h = max(logo_h, 44)
|
|
273
|
+
inner_h = row_h + 33 + 32
|
|
274
|
+
return inner, inner_h
|
|
275
|
+
|
|
276
|
+
def _chips(self, cmd_count: int, group_count: Optional[int] = None) -> str:
|
|
277
|
+
s = (
|
|
278
|
+
f"<div class='chips'>"
|
|
279
|
+
f"<div class='chip'><b>{cmd_count}</b>{self._esc(self._t('chip_commands'))}</div>"
|
|
280
|
+
)
|
|
281
|
+
if group_count is not None:
|
|
282
|
+
s += f"<div class='chip'><b>{group_count}</b>{self._esc(self._t('chip_groups'))}</div>"
|
|
283
|
+
s += "</div>"
|
|
284
|
+
return s
|
|
285
|
+
|
|
286
|
+
def _command_card(
|
|
287
|
+
self,
|
|
288
|
+
cmd: Dict,
|
|
289
|
+
idx: int,
|
|
290
|
+
prefix: str,
|
|
291
|
+
t: Dict,
|
|
292
|
+
avail: int,
|
|
293
|
+
group_color: Optional[str] = None,
|
|
294
|
+
show_group: bool = False,
|
|
295
|
+
) -> Tuple[str, int]:
|
|
296
|
+
info = cmd["info"]
|
|
297
|
+
name = cmd["name"]
|
|
298
|
+
desc = info.get("help") or self._t("no_description")
|
|
299
|
+
|
|
300
|
+
head = f"<div class='cmd-head'><div class='num'>{idx}</div>"
|
|
301
|
+
head += (
|
|
302
|
+
f"<div class='cmd-code'><span class='pfx'>{self._esc(prefix)}</span>"
|
|
303
|
+
f"{self._esc(name)}</div>"
|
|
304
|
+
)
|
|
305
|
+
if show_group and info.get("group") and group_color:
|
|
306
|
+
gname = self._t("group_default") if info["group"] == "default" else info["group"]
|
|
307
|
+
bg = self._rgba(group_color, t["tag_alpha"])
|
|
308
|
+
head += f"<span class='group-tag' style='color:{group_color};background:{bg}'>{self._esc(gname)}</span>"
|
|
309
|
+
head += "</div>"
|
|
310
|
+
|
|
311
|
+
desc_html = f"<div class='cmd-desc'>{self._esc(desc)}</div>"
|
|
312
|
+
desc_h = 8 + self._text_lines(desc, avail) * 20
|
|
313
|
+
|
|
314
|
+
extra_html = ""
|
|
315
|
+
extra_h = 0
|
|
316
|
+
aliases = self._aliases_of(name, info)
|
|
317
|
+
if aliases:
|
|
318
|
+
tags = "".join(
|
|
319
|
+
f"<span class='alias-tag'>{self._esc(prefix)}{self._esc(a)}</span>"
|
|
320
|
+
for a in aliases
|
|
321
|
+
)
|
|
322
|
+
extra_html += (
|
|
323
|
+
f"<div class='cmd-aliases'><span class='lbl'>{self._esc(self._t('label_aliases'))}</span>"
|
|
324
|
+
f"{tags}</div>"
|
|
325
|
+
)
|
|
326
|
+
a_w = sum((len(prefix) + len(a)) * 7.5 + 18 + 6 for a in aliases)
|
|
327
|
+
a_rows = max(1, math.ceil(a_w / max(1, avail)))
|
|
328
|
+
extra_h += 8 + a_rows * 24
|
|
329
|
+
if info.get("usage"):
|
|
330
|
+
usage = info["usage"].replace("/", prefix)
|
|
331
|
+
extra_html += f"<div class='cmd-usage'>{self._esc(usage)}</div>"
|
|
332
|
+
extra_h += 22 + self._text_lines(usage, avail, cjk_w=12.5, latin_w=7.5) * 19
|
|
333
|
+
|
|
334
|
+
inner = head + desc_html + extra_html
|
|
335
|
+
inner_h = 24 + desc_h + extra_h
|
|
336
|
+
return inner, inner_h
|
|
337
|
+
|
|
338
|
+
def _masonry(self, items: List[Tuple[str, int]], num_cols: int) -> Tuple[str, int]:
|
|
339
|
+
cols: List[List[str]] = [[] for _ in range(num_cols)]
|
|
340
|
+
col_h = [0] * num_cols
|
|
341
|
+
for html, inner_h in items:
|
|
342
|
+
card_h = inner_h + self._CARD_PAD_V * 2 + self._CARD_BORDER
|
|
343
|
+
c = min(range(num_cols), key=lambda i: col_h[i])
|
|
344
|
+
cols[c].append(html)
|
|
345
|
+
col_h[c] += card_h + self._COL_GAP
|
|
346
|
+
heights = [max(0, ch - self._COL_GAP) for ch in col_h]
|
|
347
|
+
inner = "".join(f"<div class='mcol'>{''.join(col)}</div>" for col in cols)
|
|
348
|
+
return f"<div class='masonry'>{inner}</div>", max(heights)
|
|
349
|
+
|
|
350
|
+
def render_help_list(
|
|
351
|
+
self,
|
|
352
|
+
commands: List[Dict],
|
|
353
|
+
command_map: Dict[int, Dict],
|
|
354
|
+
prefix: str,
|
|
355
|
+
group_commands: bool,
|
|
356
|
+
prefixes: Optional[list] = None,
|
|
357
|
+
) -> Optional[bytes]:
|
|
358
|
+
if not commands:
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
others = [
|
|
362
|
+
p for p in (prefixes or [prefix])
|
|
363
|
+
if p != prefix and len(prefixes or []) > 1
|
|
364
|
+
]
|
|
365
|
+
|
|
366
|
+
num_cols = self._num_cols(len(commands))
|
|
367
|
+
avail = int(
|
|
368
|
+
(self.CARD_WIDTH - self._PAGE_PAD * 2 - self._COL_GAP * (num_cols - 1))
|
|
369
|
+
/ num_cols - self._CARD_PAD_H * 2 - 2
|
|
370
|
+
)
|
|
371
|
+
_, t = self._css()
|
|
372
|
+
|
|
373
|
+
grouped: Dict[str, List[Dict]] = {}
|
|
374
|
+
for cmd in commands:
|
|
375
|
+
g = cmd["info"].get("group") or "default"
|
|
376
|
+
grouped.setdefault(g, []).append(cmd)
|
|
377
|
+
|
|
378
|
+
group_colors: Dict[str, str] = {}
|
|
379
|
+
for g in grouped:
|
|
380
|
+
group_colors[g] = self.PALETTE[len(group_colors) % len(self.PALETTE)]
|
|
381
|
+
|
|
382
|
+
ordered: List[Dict] = []
|
|
383
|
+
for g, cmds in grouped.items():
|
|
384
|
+
ordered.extend(cmds)
|
|
385
|
+
|
|
386
|
+
for i, cmd in enumerate(ordered, start=1):
|
|
387
|
+
command_map[i] = cmd
|
|
388
|
+
|
|
389
|
+
chips = self._chips(len(commands), len(grouped) if group_commands else None)
|
|
390
|
+
head_inner, head_h = self._header(self._t("title"), chips)
|
|
391
|
+
|
|
392
|
+
items = []
|
|
393
|
+
for i, cmd in enumerate(ordered, start=1):
|
|
394
|
+
g = cmd["info"].get("group") or "default"
|
|
395
|
+
color = group_colors[g] if group_commands else None
|
|
396
|
+
inner, inner_h = self._command_card(
|
|
397
|
+
cmd, i, prefix, t, avail,
|
|
398
|
+
group_color=color, show_group=group_commands,
|
|
399
|
+
)
|
|
400
|
+
items.append((self._card(inner), inner_h))
|
|
401
|
+
|
|
402
|
+
masonry_html, masonry_h = self._masonry(items, num_cols)
|
|
403
|
+
|
|
404
|
+
foot_lines = [self._t("command_count", count=len(commands))]
|
|
405
|
+
if others:
|
|
406
|
+
foot_lines.append(f"{self._t('other_prefixes')}: {'、'.join(others)}")
|
|
407
|
+
footer = "<div class='foot'>" + "<br>".join(foot_lines) + "</div>"
|
|
408
|
+
footer_h = 34 + (20 if others else 0)
|
|
409
|
+
|
|
410
|
+
body = self._card(head_inner) + masonry_html + footer
|
|
411
|
+
height = self._total_height([self._h_card(head_h), masonry_h], footer_h)
|
|
412
|
+
return self._render(body, height)
|
|
413
|
+
|
|
414
|
+
def render_command_detail(
|
|
415
|
+
self,
|
|
416
|
+
cmd: Dict,
|
|
417
|
+
prefix: str,
|
|
418
|
+
prefixes: Optional[list] = None,
|
|
419
|
+
) -> Optional[bytes]:
|
|
420
|
+
name = cmd["name"]
|
|
421
|
+
info = cmd["info"]
|
|
422
|
+
others = [
|
|
423
|
+
p for p in (prefixes or [prefix])
|
|
424
|
+
if p != prefix and len(prefixes or []) > 1
|
|
425
|
+
]
|
|
426
|
+
|
|
427
|
+
chips = (
|
|
428
|
+
f"<div class='chips'>"
|
|
429
|
+
f"<div class='chip'><code style='font-family:Source Code Pro,monospace;'>"
|
|
430
|
+
f"{self._esc(prefix)}{self._esc(name)}</code></div></div>"
|
|
431
|
+
)
|
|
432
|
+
head_inner, head_h = self._header(self._t("detail_title"), chips)
|
|
433
|
+
|
|
434
|
+
blocks = [(self._card(head_inner), self._h_card(head_h))]
|
|
435
|
+
|
|
436
|
+
def add_card(label: str, value_html: str, value_h: int, value_class: str = ""):
|
|
437
|
+
vc = f" {value_class}" if value_class else ""
|
|
438
|
+
inner = (
|
|
439
|
+
f"<div class='detail-label'>{self._esc(label)}</div>"
|
|
440
|
+
f"<div class='detail-value{vc}'>{value_html}</div>"
|
|
441
|
+
)
|
|
442
|
+
blocks.append((self._card(inner), self._h_card(30 + value_h)))
|
|
443
|
+
|
|
444
|
+
add_card(
|
|
445
|
+
self._t("label_description"),
|
|
446
|
+
self._esc(info.get("help") or self._t("no_description")),
|
|
447
|
+
24,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
aliases = self._aliases_of(name, info)
|
|
451
|
+
if aliases:
|
|
452
|
+
tags = "".join(
|
|
453
|
+
f"<span class='alias-tag'>{self._esc(prefix)}{self._esc(a)}</span>"
|
|
454
|
+
for a in aliases
|
|
455
|
+
)
|
|
456
|
+
add_card(self._t("label_aliases"), f"<div class='aliases'>{tags}</div>", 32)
|
|
457
|
+
|
|
458
|
+
if info.get("usage"):
|
|
459
|
+
add_card(
|
|
460
|
+
self._t("label_usage"),
|
|
461
|
+
self._esc(info["usage"].replace("/", prefix)),
|
|
462
|
+
24,
|
|
463
|
+
value_class="mono",
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
if info.get("permission"):
|
|
467
|
+
add_card(
|
|
468
|
+
self._t("label_permission"),
|
|
469
|
+
self._esc(self._t("permission_required")),
|
|
470
|
+
22,
|
|
471
|
+
value_class="warn",
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
if info.get("group"):
|
|
475
|
+
gname = self._t("group_default") if info["group"] == "default" else info["group"]
|
|
476
|
+
add_card(self._t("label_group"), self._esc(gname), 22)
|
|
477
|
+
|
|
478
|
+
footer, footer_h = "", 0
|
|
479
|
+
if others:
|
|
480
|
+
footer = (f"<div class='foot'>{self._t('other_prefixes')}: "
|
|
481
|
+
f"{'、'.join(self._esc(p) for p in others)}</div>")
|
|
482
|
+
footer_h = 40
|
|
483
|
+
|
|
484
|
+
body = "".join(h for h, _ in blocks) + footer
|
|
485
|
+
height = self._total_height([b for _, b in blocks], footer_h)
|
|
486
|
+
return self._render(body, height)
|
|
487
|
+
|
|
488
|
+
def render_error(self, title: str, message: str) -> Optional[bytes]:
|
|
489
|
+
head_inner, head_h = self._header(self._t("title"), "")
|
|
490
|
+
err = (f"<div class='err-title'>{self._esc(title)}</div>"
|
|
491
|
+
f"<div class='err-msg'>{self._esc(message)}</div>")
|
|
492
|
+
blocks = [
|
|
493
|
+
(self._card(head_inner), self._h_card(head_h)),
|
|
494
|
+
(self._card(err), self._h_card(70)),
|
|
495
|
+
]
|
|
496
|
+
body = "".join(h for h, _ in blocks)
|
|
497
|
+
height = self._total_height([b for _, b in blocks])
|
|
498
|
+
return self._render(body, height)
|
HelpNext/__init__.py
ADDED
HelpNext/assets/icon.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ErisPulse-HelpNext
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Modern help command module for ErisPulse, full i18n (requires ErisPulse 2.8.0+)
|
|
5
|
+
Project-URL: homepage, https://github.com/wsu2059q/ErisPulse-HelpNext
|
|
6
|
+
Author-email: wsu2059q <wsu2059@qq.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: ErisPulse,HelpModule,Takumi,help,i18n
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Communications :: Chat
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: erispulse-takumi
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# ErisPulse-HelpNext
|
|
23
|
+
|
|
24
|
+
<div align="center">
|
|
25
|
+
<img src=".github/assets/ErisPulseLogo.png" width="140" alt="ErisPulse" />
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
Renders the ErisPulse `/help` command as a card image via [ErisPulse-Takumi](https://pypi.org/project/ErisPulse-Takumi/), with day/night theme and multilingual support. Requires ErisPulse 2.8.0+.
|
|
29
|
+
|
|
30
|
+
[English](#english) | [简体中文](#简体中文)
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
<a id="english"></a>
|
|
35
|
+
|
|
36
|
+
## English
|
|
37
|
+
|
|
38
|
+
> 2.8.0+ recommends this module. The classic [`ErisPulse-HelpModule`](https://pypi.org/project/ErisPulse-HelpModule/) is still maintained for backward compatibility. Both register `/help`, so enable only one.
|
|
39
|
+
|
|
40
|
+
### Features
|
|
41
|
+
|
|
42
|
+
- Day/night theme by local time (19–7 dark), or pin light / dark
|
|
43
|
+
- Multilingual: zh-CN / zh-TW / en / ja / ru (declarative `I18nClass`)
|
|
44
|
+
- Session-aware listing: commands from modules disabled by scope in the current session are hidden
|
|
45
|
+
- Falls back to Html → Markdown → Text when images aren't supported
|
|
46
|
+
- Declarative config (`ConfigClass`), descriptions also translated
|
|
47
|
+
|
|
48
|
+
### Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
epsdk install HelpNext
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Commands
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
/help List all available commands (card image)
|
|
58
|
+
/help <index> Show detail of the command at <index>
|
|
59
|
+
/help --format <fmt> Force output format: image | html | markdown | text
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Aliases: `/h`, `/帮助`
|
|
63
|
+
|
|
64
|
+
### Config
|
|
65
|
+
|
|
66
|
+
First load writes defaults; edit the `HelpNext` section:
|
|
67
|
+
|
|
68
|
+
```toml
|
|
69
|
+
[HelpNext]
|
|
70
|
+
show_hidden_commands = false # show commands marked hidden
|
|
71
|
+
group_commands = true # group commands by category
|
|
72
|
+
theme = "auto" # auto | light | dark
|
|
73
|
+
utc_offset = 8 # UTC offset for day/night switching
|
|
74
|
+
show_logo = true # show ErisPulse icon in header
|
|
75
|
+
header_title = "" # custom header title (empty = default)
|
|
76
|
+
header_subtitle = "" # custom header subtitle (empty = default)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- `show_hidden_commands`: when `true`, shows commands marked as hidden
|
|
80
|
+
- `group_commands`: when `false`, lists all commands in a single group
|
|
81
|
+
- `theme`: `auto` (by time), or fixed `light` / `dark`
|
|
82
|
+
- `utc_offset`: UTC offset used for day/night detection
|
|
83
|
+
- `show_logo`: show the ErisPulse icon in the header
|
|
84
|
+
- `header_title` / `header_subtitle`: customize the header text (empty = defaults)
|
|
85
|
+
|
|
86
|
+
### Dependencies
|
|
87
|
+
|
|
88
|
+
- ErisPulse SDK 2.8.0+
|
|
89
|
+
- [ErisPulse-Takumi](https://pypi.org/project/ErisPulse-Takumi/) (declared as a dependency, auto-installed)
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
<a id="简体中文"></a>
|
|
94
|
+
|
|
95
|
+
## 简体中文
|
|
96
|
+
|
|
97
|
+
> 2.8.0+ 推荐使用本模块;经典版 [ErisPulse-HelpModule](https://pypi.org/project/ErisPulse-HelpModule/) 继续维护,用于向后兼容。两者都注册 `/help`,请按需启用其一。
|
|
98
|
+
|
|
99
|
+
### 功能特性
|
|
100
|
+
|
|
101
|
+
- 按本地时间自动切换昼夜主题(19–7 点深色),也可固定为浅色 / 深色
|
|
102
|
+
- 多语言:zh-CN / zh-TW / en / ja / ru(声明式 `I18nClass`)
|
|
103
|
+
- 会话感知列表:被作用域禁用的模块,其命令不在当前会话的帮助中列出
|
|
104
|
+
- 平台不支持图片时按 Html → Markdown → 文本 回退
|
|
105
|
+
- 声明式配置(`ConfigClass`),配置描述同样支持多语言
|
|
106
|
+
|
|
107
|
+
### 安装
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
epsdk install HelpNext
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 命令
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
/help 列出所有可用命令(卡片图片)
|
|
117
|
+
/help <序号> 查看指定序号命令的详情
|
|
118
|
+
/help --format <格式> 指定输出格式:image | html | markdown | text
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
别名:`/h`、`/帮助`
|
|
122
|
+
|
|
123
|
+
### 配置选项
|
|
124
|
+
|
|
125
|
+
首次加载会写入默认配置,可在 ErisPulse 配置的 `HelpNext` 节修改:
|
|
126
|
+
|
|
127
|
+
```toml
|
|
128
|
+
[HelpNext]
|
|
129
|
+
show_hidden_commands = false # 是否显示隐藏命令
|
|
130
|
+
group_commands = true # 是否按分组显示
|
|
131
|
+
theme = "auto" # auto | light | dark
|
|
132
|
+
utc_offset = 8 # 昼夜切换用的时区偏移
|
|
133
|
+
show_logo = true # 头部是否显示 ErisPulse 图标
|
|
134
|
+
header_title = "" # 自定义头部标题(留空使用默认)
|
|
135
|
+
header_subtitle = "" # 自定义头部副标题(留空使用默认)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- `show_hidden_commands`:设为 `true` 时显示标记为隐藏的命令
|
|
139
|
+
- `group_commands`:设为 `false` 时不分组,所有命令在同一列表
|
|
140
|
+
- `theme`:图片主题,`auto` 跟随时间,或固定 `light` / `dark`
|
|
141
|
+
- `utc_offset`:昼夜判定使用的 UTC 时区偏移
|
|
142
|
+
- `show_logo`:头部是否显示 ErisPulse 图标
|
|
143
|
+
- `header_title` / `header_subtitle`:自定义头部标题 / 副标题(留空使用默认)
|
|
144
|
+
|
|
145
|
+
### 依赖
|
|
146
|
+
|
|
147
|
+
- ErisPulse SDK 2.8.0+
|
|
148
|
+
- [ErisPulse-Takumi](https://pypi.org/project/ErisPulse-Takumi/)(已声明为依赖,自动安装)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
HelpNext/Core.py,sha256=pvzCIgDvHCxLRo1nBeqjmII5dVbiZjWvS65W4N4_fxU,20323
|
|
2
|
+
HelpNext/Templates.py,sha256=TjOKE0XRTsp03_JMXwTBmjUsZ0EdR71pcJJvI6VhtYU,11686
|
|
3
|
+
HelpNext/Visualizer.py,sha256=fIqdEVCmCeU82yuDFubgGTnZ_Cxe3D6UmDoAfAk3Fhw,19543
|
|
4
|
+
HelpNext/__init__.py,sha256=ZiKcK7IRyOCxNl_-TYuzKuTnz7YDQbkEDKZ0U6GVSks,43
|
|
5
|
+
HelpNext/assets/icon.png,sha256=vXYncrvAc42m6Ra5V8dpZTpZ73T7XmutozyI5bLV4P4,87722
|
|
6
|
+
erispulse_helpnext-0.1.0.dist-info/METADATA,sha256=Ye0p7y8Av3rLi1iwWC8-IIdobtSaQPGv-pTwzBIWuwA,5411
|
|
7
|
+
erispulse_helpnext-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
8
|
+
erispulse_helpnext-0.1.0.dist-info/entry_points.txt,sha256=DyXAx3ALy7r4F-iHdgFKamsU3pnf9VNgHTo2bgmUl08,44
|
|
9
|
+
erispulse_helpnext-0.1.0.dist-info/licenses/LICENSE,sha256=7BKmRD_5YpTGfc12w9L3TIskHF3A_AWDU4xuDQKE92w,1055
|
|
10
|
+
erispulse_helpnext-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2025 wsu2059q
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|