nonebot-plugin-picmenu-next 0.1.0.dev1__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.
- nonebot_plugin_picmenu_next/__init__.py +37 -0
- nonebot_plugin_picmenu_next/__main__.py +183 -0
- nonebot_plugin_picmenu_next/config.py +13 -0
- nonebot_plugin_picmenu_next/data_source.py +341 -0
- nonebot_plugin_picmenu_next/templates/__init__.py +77 -0
- nonebot_plugin_picmenu_next/templates/default/__init__.py +147 -0
- nonebot_plugin_picmenu_next/templates/default/res/base.html.jinja +28 -0
- nonebot_plugin_picmenu_next/templates/default/res/css/base.css +137 -0
- nonebot_plugin_picmenu_next/templates/default/res/css/dark.css +10 -0
- nonebot_plugin_picmenu_next/templates/default/res/css/detail.css +16 -0
- nonebot_plugin_picmenu_next/templates/default/res/css/index.css +9 -0
- nonebot_plugin_picmenu_next/templates/default/res/detail.html.jinja +68 -0
- nonebot_plugin_picmenu_next/templates/default/res/index.html.jinja +21 -0
- nonebot_plugin_picmenu_next-0.1.0.dev1.dist-info/METADATA +175 -0
- nonebot_plugin_picmenu_next-0.1.0.dev1.dist-info/RECORD +18 -0
- nonebot_plugin_picmenu_next-0.1.0.dev1.dist-info/WHEEL +4 -0
- nonebot_plugin_picmenu_next-0.1.0.dev1.dist-info/entry_points.txt +4 -0
- nonebot_plugin_picmenu_next-0.1.0.dev1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# ruff: noqa: E402
|
|
2
|
+
|
|
3
|
+
from nonebot import get_driver
|
|
4
|
+
from nonebot.plugin import PluginMetadata, inherit_supported_adapters, require
|
|
5
|
+
|
|
6
|
+
require("nonebot_plugin_alconna")
|
|
7
|
+
require("nonebot_plugin_waiter")
|
|
8
|
+
require("nonebot_plugin_htmlrender")
|
|
9
|
+
|
|
10
|
+
from . import __main__ as __main__
|
|
11
|
+
from .config import ConfigModel
|
|
12
|
+
from .data_source import refresh_infos
|
|
13
|
+
from .templates import load_builtin_templates
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0.dev1"
|
|
16
|
+
__plugin_meta__ = PluginMetadata(
|
|
17
|
+
name="PicMenu Next",
|
|
18
|
+
description="新一代的图片帮助插件",
|
|
19
|
+
usage="发送“帮助”查看所有所有插件功能",
|
|
20
|
+
type="application",
|
|
21
|
+
homepage="https://github.com/lgc-NB2Dev/nonebot-plugin-picmenu-next",
|
|
22
|
+
config=ConfigModel,
|
|
23
|
+
supported_adapters=inherit_supported_adapters(
|
|
24
|
+
"nonebot_plugin_alconna",
|
|
25
|
+
"nonebot_plugin_waiter",
|
|
26
|
+
),
|
|
27
|
+
extra={"License": "MIT", "Author": "LgCookie"},
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
load_builtin_templates()
|
|
31
|
+
|
|
32
|
+
driver = get_driver()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@driver.on_startup
|
|
36
|
+
async def _():
|
|
37
|
+
await refresh_infos()
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from contextlib import suppress
|
|
3
|
+
from typing import Optional, TypeVar
|
|
4
|
+
|
|
5
|
+
from arclet.alconna import Alconna, Arg, Args, CommandMeta, Option, store_true
|
|
6
|
+
from loguru import logger
|
|
7
|
+
from nonebot_plugin_alconna import Query, on_alconna
|
|
8
|
+
from nonebot_plugin_alconna.uniseg import UniMessage
|
|
9
|
+
from thefuzz import process
|
|
10
|
+
|
|
11
|
+
from .data_source import (
|
|
12
|
+
PMDataItem,
|
|
13
|
+
PMNPluginInfo,
|
|
14
|
+
get_resolved_infos,
|
|
15
|
+
transform_to_pinyin,
|
|
16
|
+
)
|
|
17
|
+
from .templates import detail_templates, func_detail_templates, index_templates
|
|
18
|
+
|
|
19
|
+
alc = Alconna(
|
|
20
|
+
"help",
|
|
21
|
+
Args(
|
|
22
|
+
Arg("plugin?", str, notice="插件序号或名称"),
|
|
23
|
+
Arg("function?", str, notice="插件功能序号或名称"),
|
|
24
|
+
),
|
|
25
|
+
Option(
|
|
26
|
+
"-H|--show-hidden",
|
|
27
|
+
action=store_true,
|
|
28
|
+
help_text="显示隐藏的插件",
|
|
29
|
+
),
|
|
30
|
+
meta=CommandMeta(
|
|
31
|
+
description="新一代的图片帮助插件",
|
|
32
|
+
author="LgCookie",
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
m_cls = on_alconna(
|
|
36
|
+
alc,
|
|
37
|
+
aliases={"帮助", "菜单"},
|
|
38
|
+
skip_for_unmatch=False,
|
|
39
|
+
auto_send_output=True,
|
|
40
|
+
use_cmd_start=True,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_name_similarities(
|
|
45
|
+
query: str,
|
|
46
|
+
query_pinyin: str,
|
|
47
|
+
choices: list[str],
|
|
48
|
+
choices_pinyin: list[str],
|
|
49
|
+
raw_weight: float = 0.6,
|
|
50
|
+
pinyin_weight: float = 0.4,
|
|
51
|
+
) -> list[float]:
|
|
52
|
+
similarities: list[float] = []
|
|
53
|
+
raw_iter = process.extractWithoutOrder(query, choices)
|
|
54
|
+
pinyin_iter = process.extractWithoutOrder(query_pinyin, choices_pinyin)
|
|
55
|
+
with suppress(StopIteration):
|
|
56
|
+
while True:
|
|
57
|
+
raw_score = next(raw_iter)[1]
|
|
58
|
+
pinyin_score = next(pinyin_iter)[1]
|
|
59
|
+
similarities.append(raw_weight * raw_score + pinyin_weight * pinyin_score)
|
|
60
|
+
logger.opt(lazy=True).debug(
|
|
61
|
+
"Query: {}, similarities: {}",
|
|
62
|
+
lambda: query,
|
|
63
|
+
lambda: "; ".join(
|
|
64
|
+
f"{choices[i]}: {s}"
|
|
65
|
+
for i, s in sorted(
|
|
66
|
+
enumerate(similarities),
|
|
67
|
+
key=lambda x: x[1],
|
|
68
|
+
reverse=True,
|
|
69
|
+
)
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
return similarities
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
T = TypeVar("T")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def handle_query_index(query: str, infos: Sequence[T]) -> Optional[tuple[int, T]]:
|
|
79
|
+
if query.isdigit() and query.strip("0"):
|
|
80
|
+
return (
|
|
81
|
+
((i := qn - 1), infos[i])
|
|
82
|
+
if (1 <= (qn := int(query)) <= len(infos))
|
|
83
|
+
else None
|
|
84
|
+
)
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def query_plugin(
|
|
89
|
+
infos: list[PMNPluginInfo],
|
|
90
|
+
query: str,
|
|
91
|
+
score_cutoff: float = 60,
|
|
92
|
+
) -> Optional[tuple[int, PMNPluginInfo]]:
|
|
93
|
+
if r := handle_query_index(query, infos):
|
|
94
|
+
return r
|
|
95
|
+
|
|
96
|
+
choices: list[str] = []
|
|
97
|
+
choices_pinyin: list[str] = []
|
|
98
|
+
for info in infos:
|
|
99
|
+
choices.append(info.name.casefold())
|
|
100
|
+
choices_pinyin.append(info.name_pinyin.casefold())
|
|
101
|
+
|
|
102
|
+
similarities = get_name_similarities(
|
|
103
|
+
query.casefold(),
|
|
104
|
+
transform_to_pinyin(query).casefold(),
|
|
105
|
+
choices,
|
|
106
|
+
choices_pinyin,
|
|
107
|
+
)
|
|
108
|
+
i, s = max(enumerate(similarities), key=lambda x: x[1])
|
|
109
|
+
if s >= score_cutoff:
|
|
110
|
+
return i, infos[i]
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def query_func_detail(
|
|
115
|
+
pm_data: list[PMDataItem],
|
|
116
|
+
query: str,
|
|
117
|
+
score_cutoff: float = 60,
|
|
118
|
+
) -> Optional[tuple[int, PMDataItem]]:
|
|
119
|
+
if r := handle_query_index(query, pm_data):
|
|
120
|
+
return r
|
|
121
|
+
|
|
122
|
+
choices: list[str] = []
|
|
123
|
+
choices_pinyin: list[str] = []
|
|
124
|
+
for data in pm_data:
|
|
125
|
+
choices.append(data.func.casefold())
|
|
126
|
+
choices_pinyin.append(data.func_pinyin.casefold())
|
|
127
|
+
|
|
128
|
+
similarities = get_name_similarities(
|
|
129
|
+
query.casefold(),
|
|
130
|
+
transform_to_pinyin(query).casefold(),
|
|
131
|
+
choices,
|
|
132
|
+
choices_pinyin,
|
|
133
|
+
)
|
|
134
|
+
i, s = max(enumerate(similarities), key=lambda x: x[1])
|
|
135
|
+
if s >= score_cutoff:
|
|
136
|
+
return i, pm_data[i]
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@m_cls.handle()
|
|
141
|
+
async def _(
|
|
142
|
+
q_plugin: Query[Optional[str]] = Query("~plugin", None),
|
|
143
|
+
q_function: Query[Optional[str]] = Query("~function", None),
|
|
144
|
+
q_show_hidden: Query[bool] = Query("~show-hidden.value", default=False),
|
|
145
|
+
):
|
|
146
|
+
infos = await get_resolved_infos()
|
|
147
|
+
if not q_show_hidden.result:
|
|
148
|
+
infos = [x for x in infos if not x.pmn_v.hidden]
|
|
149
|
+
|
|
150
|
+
if not q_plugin.result:
|
|
151
|
+
m = await index_templates.get()(infos)
|
|
152
|
+
await m.finish()
|
|
153
|
+
|
|
154
|
+
r = await query_plugin(infos, q_plugin.result)
|
|
155
|
+
if not r:
|
|
156
|
+
await UniMessage.text("好像没有找到对应插件呢……").finish(reply_to=True)
|
|
157
|
+
info_index, info = r
|
|
158
|
+
if not q_function.result:
|
|
159
|
+
m = await detail_templates.get(info.pmn.template)(info, info_index)
|
|
160
|
+
await m.finish()
|
|
161
|
+
|
|
162
|
+
if not info.pm_data:
|
|
163
|
+
await UniMessage.text(
|
|
164
|
+
f"插件 `{info.name}` 没有详细功能介绍哦",
|
|
165
|
+
).finish(reply_to=True)
|
|
166
|
+
|
|
167
|
+
if (not (plugin := info.plugin)) or (
|
|
168
|
+
not (pm_data := await info.resolve_pm_data(plugin))
|
|
169
|
+
):
|
|
170
|
+
await UniMessage.text("啊哦,遇到了意外情况……").finish(reply_to=True)
|
|
171
|
+
|
|
172
|
+
if not q_show_hidden.result:
|
|
173
|
+
pm_data = [x for x in pm_data if not x.hidden]
|
|
174
|
+
r = await query_func_detail(pm_data, q_function.result)
|
|
175
|
+
if not r:
|
|
176
|
+
await UniMessage.text(
|
|
177
|
+
f"好像没有找到插件 `{info.name}` 的对应功能呢……",
|
|
178
|
+
).finish(reply_to=True)
|
|
179
|
+
func_index, func = r
|
|
180
|
+
m = await func_detail_templates.get(
|
|
181
|
+
func.template,
|
|
182
|
+
)(info, info_index, func, func_index)
|
|
183
|
+
await m.finish()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from cookit.pyd import model_with_alias_generator
|
|
2
|
+
from nonebot import get_plugin_config
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@model_with_alias_generator(lambda x: f"pmn_{x}")
|
|
7
|
+
class ConfigModel(BaseModel):
|
|
8
|
+
index_template: str = "default"
|
|
9
|
+
detail_template: str = "default"
|
|
10
|
+
func_detail_template: str = "default"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
config: ConfigModel = get_plugin_config(ConfigModel)
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import importlib
|
|
3
|
+
from asyncio import iscoroutinefunction
|
|
4
|
+
from collections.abc import Awaitable, Iterable
|
|
5
|
+
from importlib.metadata import Distribution, distribution
|
|
6
|
+
from typing import Any, Optional, Union
|
|
7
|
+
from typing_extensions import Self
|
|
8
|
+
from weakref import ref
|
|
9
|
+
|
|
10
|
+
import jieba
|
|
11
|
+
from cookit.loguru import warning_suppress
|
|
12
|
+
from cookit.pyd import model_validator, type_dump_python, type_validate_python
|
|
13
|
+
from nonebot import logger
|
|
14
|
+
from nonebot.plugin import Plugin, get_loaded_plugins
|
|
15
|
+
from pydantic import BaseModel, Field, PrivateAttr
|
|
16
|
+
from pypinyin import Style, pinyin
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def call_entrypoint(plugin: Plugin, entrypoint: str) -> Any:
|
|
20
|
+
"""`module_name:function_name` string.
|
|
21
|
+
You can Use `~` in module name to replace your plugin module name."""
|
|
22
|
+
|
|
23
|
+
module_path, func_name = entrypoint.split(":")
|
|
24
|
+
module_path = module_path.replace("~", plugin.module_name)
|
|
25
|
+
module = importlib.import_module(module_path)
|
|
26
|
+
func = getattr(module, func_name)
|
|
27
|
+
return (await func()) if iscoroutinefunction(func) else func()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def resolve_func_hidden(plugin: Plugin, entrypoint: str) -> bool:
|
|
31
|
+
"""should only be called from event handler,
|
|
32
|
+
so hidden func can get current bot, event, etc."""
|
|
33
|
+
|
|
34
|
+
with warning_suppress(
|
|
35
|
+
f"Failed to resole hidden status `{entrypoint}` from plugin {plugin.id_}",
|
|
36
|
+
):
|
|
37
|
+
return bool(await call_entrypoint(plugin, entrypoint))
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PMDataItemRaw(BaseModel):
|
|
42
|
+
func: str
|
|
43
|
+
func_pinyin: str
|
|
44
|
+
trigger_method: str
|
|
45
|
+
trigger_condition: str
|
|
46
|
+
brief_des: str
|
|
47
|
+
detail_des: str
|
|
48
|
+
|
|
49
|
+
# extension properties
|
|
50
|
+
hidden: Union[bool, str] = Field(default=False, alias="pmn_hidden")
|
|
51
|
+
template: Optional[str] = Field(default=None, alias="pmn_template")
|
|
52
|
+
|
|
53
|
+
@model_validator(mode="before")
|
|
54
|
+
def init_func_pinyin(cls, values: Any): # noqa: N805
|
|
55
|
+
if isinstance(values, BaseModel):
|
|
56
|
+
values = type_dump_python(values, exclude_unset=True)
|
|
57
|
+
if isinstance((func := values.get("func")), str) and (
|
|
58
|
+
not values.get("func_pinyin")
|
|
59
|
+
):
|
|
60
|
+
values["func_pinyin"] = transform_to_pinyin(func)
|
|
61
|
+
return values
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class PMDataItem(PMDataItemRaw):
|
|
65
|
+
pmn_hidden_v: bool = False
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
async def resolve(cls, plugin: Plugin, data: PMDataItemRaw) -> Self:
|
|
69
|
+
data_dict: dict = type_dump_python(data, exclude_unset=True)
|
|
70
|
+
if isinstance((hidden := data_dict.get("pmn_hidden")), str):
|
|
71
|
+
data_dict["pmn_hidden_v"] = await resolve_func_hidden(plugin, hidden)
|
|
72
|
+
return cls(**data_dict)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class PMNDataRaw(BaseModel):
|
|
76
|
+
hidden: Union[bool, str] = False
|
|
77
|
+
markdown: bool = False
|
|
78
|
+
template: Optional[str] = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class PMNData(PMNDataRaw):
|
|
82
|
+
hidden_v: bool = False
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
async def resolve(cls, plugin: Plugin, data: PMNDataRaw) -> Self:
|
|
86
|
+
data_dict: dict = type_dump_python(data, exclude_unset=True)
|
|
87
|
+
if isinstance((hidden := data_dict.get("hidden")), str):
|
|
88
|
+
data_dict["hidden_v"] = await resolve_func_hidden(plugin, hidden)
|
|
89
|
+
return cls(**data_dict)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class PMNPluginExtra(BaseModel):
|
|
93
|
+
author: Union[str, list[str], None] = None
|
|
94
|
+
version: Optional[str] = None
|
|
95
|
+
menu_data: Optional[list[PMDataItemRaw]] = None
|
|
96
|
+
pmn: Optional[PMNDataRaw] = None
|
|
97
|
+
|
|
98
|
+
@model_validator(mode="before")
|
|
99
|
+
def normalize_input(cls, values: dict[str, Any]): # noqa: N805
|
|
100
|
+
should_normalize_keys = {x for x in values if x.lower() in {"author"}}
|
|
101
|
+
for key in should_normalize_keys:
|
|
102
|
+
value = values[key]
|
|
103
|
+
del values[key]
|
|
104
|
+
values[key.lower()] = value
|
|
105
|
+
return values
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class PMNPluginInfoRaw(BaseModel):
|
|
109
|
+
name: str
|
|
110
|
+
name_pinyin: str
|
|
111
|
+
author: Optional[str] = None
|
|
112
|
+
version: Optional[str] = None
|
|
113
|
+
description: Optional[str] = None
|
|
114
|
+
usage: Optional[str] = None
|
|
115
|
+
pm_data: Optional[list[PMDataItemRaw]] = None
|
|
116
|
+
pmn: PMNDataRaw = PMNDataRaw()
|
|
117
|
+
|
|
118
|
+
_resolved_pm_data: Optional[list[PMDataItem]] = PrivateAttr(None)
|
|
119
|
+
|
|
120
|
+
@model_validator(mode="before")
|
|
121
|
+
def init_name_pinyin(cls, values: dict[str, Any]): # noqa: N805
|
|
122
|
+
if isinstance((name := values.get("name")), str) and (
|
|
123
|
+
not values.get("name_pinyin")
|
|
124
|
+
):
|
|
125
|
+
values["name_pinyin"] = transform_to_pinyin(name)
|
|
126
|
+
return values
|
|
127
|
+
|
|
128
|
+
async def resolve_pm_data(self, plugin: Plugin):
|
|
129
|
+
if self._resolved_pm_data is not None:
|
|
130
|
+
return self._resolved_pm_data
|
|
131
|
+
if not self.pm_data:
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
async def _ts(x: PMDataItemRaw):
|
|
135
|
+
with warning_suppress(
|
|
136
|
+
f"Failed to resolve plugin menu item `{x.func}` of {plugin.id_}",
|
|
137
|
+
):
|
|
138
|
+
return await PMDataItem.resolve(plugin, x)
|
|
139
|
+
|
|
140
|
+
self._resolved_pm_data = [
|
|
141
|
+
x for x in await asyncio.gather(*(_ts(x) for x in self.pm_data)) if x
|
|
142
|
+
]
|
|
143
|
+
return self._resolved_pm_data
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class PMNPluginInfo(PMNPluginInfoRaw):
|
|
147
|
+
pmn_v: PMNData = PMNData()
|
|
148
|
+
|
|
149
|
+
_plugin: Optional[ref[Plugin]] = PrivateAttr(None)
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def plugin(self) -> Optional[Plugin]:
|
|
153
|
+
if self._plugin:
|
|
154
|
+
return self._plugin()
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
@plugin.setter
|
|
158
|
+
def plugin(self, plugin: Plugin):
|
|
159
|
+
self._plugin = ref(plugin)
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
async def resolve(cls, plugin: Plugin, data: PMNPluginInfoRaw) -> Self:
|
|
163
|
+
data_dict: dict = type_dump_python(data, exclude_unset=True)
|
|
164
|
+
tasks: list[Awaitable] = []
|
|
165
|
+
|
|
166
|
+
if pmn := data_dict.get("pmn"):
|
|
167
|
+
|
|
168
|
+
async def _t():
|
|
169
|
+
v = None
|
|
170
|
+
with warning_suppress(
|
|
171
|
+
f"Failed to resolve PicMenu Next data of {plugin.id_}",
|
|
172
|
+
):
|
|
173
|
+
v = await PMNData.resolve(plugin, pmn)
|
|
174
|
+
data_dict["pmn_v"] = v
|
|
175
|
+
|
|
176
|
+
tasks.append(_t())
|
|
177
|
+
|
|
178
|
+
await asyncio.gather(*tasks)
|
|
179
|
+
ins = cls(**data_dict)
|
|
180
|
+
ins.plugin = plugin
|
|
181
|
+
return ins
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def normalize_plugin_name(name: str) -> str:
|
|
185
|
+
if pfx := next(
|
|
186
|
+
(x for x in ("nonebot_plugin_", "nonebot-plugin-") if name.startswith(x)),
|
|
187
|
+
None,
|
|
188
|
+
):
|
|
189
|
+
name = name[len(pfx) :].replace("_", " ").title()
|
|
190
|
+
return name
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def normalize_metadata_user(info: str, allow_multi: bool = False) -> str:
|
|
194
|
+
infos = info.split(",")
|
|
195
|
+
if not allow_multi:
|
|
196
|
+
infos = infos[:1]
|
|
197
|
+
return " & ".join(x.split("<")[0].strip().strip("'\"") for x in infos)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
async def get_info_from_plugin(plugin: Plugin) -> PMNPluginInfoRaw:
|
|
201
|
+
meta = plugin.metadata
|
|
202
|
+
extra: Optional[PMNPluginExtra] = None
|
|
203
|
+
if meta:
|
|
204
|
+
with warning_suppress(f"Failed to parse plugin metadata of {plugin.id_}"):
|
|
205
|
+
extra = type_validate_python(PMNPluginExtra, meta.extra)
|
|
206
|
+
|
|
207
|
+
name = (
|
|
208
|
+
normalize_plugin_name(meta.name)
|
|
209
|
+
if meta
|
|
210
|
+
else normalize_plugin_name(
|
|
211
|
+
plugin.id_.replace(".", " ").replace(":", " "),
|
|
212
|
+
).title()
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
_dist = ...
|
|
216
|
+
|
|
217
|
+
def get_dist() -> Optional[Distribution]:
|
|
218
|
+
nonlocal _dist
|
|
219
|
+
if _dist is ...:
|
|
220
|
+
_dist = None
|
|
221
|
+
with warning_suppress(
|
|
222
|
+
f"Failed to get info of package {plugin.module_name}",
|
|
223
|
+
):
|
|
224
|
+
_dist = distribution(plugin.module_name)
|
|
225
|
+
return _dist
|
|
226
|
+
|
|
227
|
+
ver = extra.version if extra else None
|
|
228
|
+
if not ver:
|
|
229
|
+
ver = getattr(plugin, "__version__", None)
|
|
230
|
+
if not ver and (dist := get_dist()):
|
|
231
|
+
ver = dist.version
|
|
232
|
+
|
|
233
|
+
author = (
|
|
234
|
+
(" & ".join(extra.author) if isinstance(extra.author, list) else extra.author)
|
|
235
|
+
if extra
|
|
236
|
+
else None
|
|
237
|
+
)
|
|
238
|
+
if not author and (dist := get_dist()):
|
|
239
|
+
if author := dist.metadata.get("Author") or dist.metadata.get("Maintainer"):
|
|
240
|
+
author = normalize_metadata_user(author)
|
|
241
|
+
elif author := dist.metadata.get("Author-Email") or dist.metadata.get(
|
|
242
|
+
"Maintainer-Email",
|
|
243
|
+
):
|
|
244
|
+
author = normalize_metadata_user(author, allow_multi=True)
|
|
245
|
+
|
|
246
|
+
description = (
|
|
247
|
+
meta.description
|
|
248
|
+
if meta
|
|
249
|
+
else (dist.metadata.get("Summary") if (dist := get_dist()) else None)
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
pmn = (extra.pmn if extra else None) or PMNDataRaw()
|
|
253
|
+
if ("hidden" not in pmn.model_fields_set) and meta and meta.type == "library":
|
|
254
|
+
pmn = PMNDataRaw(hidden=True)
|
|
255
|
+
|
|
256
|
+
logger.debug(f"Completed to get info of plugin {plugin.id_}")
|
|
257
|
+
return PMNPluginInfoRaw(
|
|
258
|
+
name=name,
|
|
259
|
+
name_pinyin="", # avoid type error
|
|
260
|
+
author=author,
|
|
261
|
+
version=ver,
|
|
262
|
+
description=description,
|
|
263
|
+
usage=meta.usage if meta else None,
|
|
264
|
+
pm_data=extra.menu_data if extra else None,
|
|
265
|
+
pmn=pmn,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class _NotCHNStr(str): # noqa: SLOT000
|
|
270
|
+
pass
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def pinyin_sorter_k(text: str):
|
|
274
|
+
return tuple(
|
|
275
|
+
(
|
|
276
|
+
is_pinyin := not isinstance((x := v[0]), _NotCHNStr),
|
|
277
|
+
x[:-1] if is_pinyin else x,
|
|
278
|
+
int(x[-1]) if is_pinyin else 0,
|
|
279
|
+
)
|
|
280
|
+
for v in pinyin(
|
|
281
|
+
jieba.lcut(text),
|
|
282
|
+
style=Style.TONE3,
|
|
283
|
+
errors=lambda x: _NotCHNStr(x),
|
|
284
|
+
neutral_tone_with_five=True,
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
async def collect_plugin_infos(plugins: Iterable[Plugin]):
|
|
290
|
+
async def _get(p: Plugin):
|
|
291
|
+
with warning_suppress(f"Failed to get plugin info of {p.id_}"):
|
|
292
|
+
return await get_info_from_plugin(p)
|
|
293
|
+
|
|
294
|
+
infos = await asyncio.gather(
|
|
295
|
+
*(_get(plugin) for plugin in plugins),
|
|
296
|
+
)
|
|
297
|
+
infos = [x for x in infos if x]
|
|
298
|
+
logger.success(f"Collected {len(infos)} plugin infos")
|
|
299
|
+
infos.sort(key=lambda x: x.name)
|
|
300
|
+
return infos
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def transform_to_pinyin(text: str) -> str:
|
|
304
|
+
return " ".join(
|
|
305
|
+
v[0]
|
|
306
|
+
for v in pinyin(
|
|
307
|
+
jieba.lcut(text),
|
|
308
|
+
style=Style.TONE3,
|
|
309
|
+
neutral_tone_with_five=True,
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
_infos: list[PMNPluginInfoRaw] = []
|
|
315
|
+
_plugin_refs: list[ref[Plugin]] = []
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def get_infos() -> list[PMNPluginInfoRaw]:
|
|
319
|
+
return _infos
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def get_plugin_refs() -> list[ref[Plugin]]:
|
|
323
|
+
return _plugin_refs
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
async def refresh_infos() -> list[PMNPluginInfoRaw]:
|
|
327
|
+
global _plugin_refs, _infos
|
|
328
|
+
plugins = get_loaded_plugins()
|
|
329
|
+
_infos = await collect_plugin_infos(plugins)
|
|
330
|
+
_plugin_refs = [ref(plugin) for plugin in plugins]
|
|
331
|
+
return _infos
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
async def get_resolved_infos() -> list[PMNPluginInfo]:
|
|
335
|
+
return await asyncio.gather(
|
|
336
|
+
*(
|
|
337
|
+
PMNPluginInfo.resolve(p, x)
|
|
338
|
+
for r, x in zip(_plugin_refs, _infos)
|
|
339
|
+
if (p := r())
|
|
340
|
+
),
|
|
341
|
+
)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Callable, Optional, Protocol, TypeVar
|
|
3
|
+
|
|
4
|
+
from cookit import HasNameProtocol, NameDecoCollector, auto_import
|
|
5
|
+
from nonebot import logger
|
|
6
|
+
from nonebot_plugin_alconna.uniseg import UniMessage
|
|
7
|
+
|
|
8
|
+
from ..config import config
|
|
9
|
+
from ..data_source import PMDataItem, PMNPluginInfo
|
|
10
|
+
|
|
11
|
+
TN = TypeVar("TN", bound=HasNameProtocol)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class IndexTemplateHandler(HasNameProtocol, Protocol):
|
|
15
|
+
async def __call__(self, infos: list[PMNPluginInfo]) -> UniMessage: ...
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DetailTemplateHandler(HasNameProtocol, Protocol):
|
|
19
|
+
async def __call__(self, info: PMNPluginInfo, info_index: int) -> UniMessage: ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FuncDetailTemplateHandler(HasNameProtocol, Protocol):
|
|
23
|
+
async def __call__(
|
|
24
|
+
self,
|
|
25
|
+
info: PMNPluginInfo,
|
|
26
|
+
info_index: int,
|
|
27
|
+
func: PMDataItem,
|
|
28
|
+
func_index: int,
|
|
29
|
+
) -> UniMessage: ...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TemplateDecoCollector(NameDecoCollector[TN]):
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
template_type: str,
|
|
36
|
+
template_name_getter: Callable[[], str],
|
|
37
|
+
data: Optional[dict[str, TN]] = None,
|
|
38
|
+
allow_overwrite: bool = False,
|
|
39
|
+
) -> None:
|
|
40
|
+
super().__init__(data, allow_overwrite)
|
|
41
|
+
self.template_type = template_type
|
|
42
|
+
self.name_getter = template_name_getter
|
|
43
|
+
|
|
44
|
+
def get(self, name: Optional[str] = None) -> TN:
|
|
45
|
+
if name and name not in self.data:
|
|
46
|
+
logger.warning(
|
|
47
|
+
f"Plugin configured {self.template_type} template '{name}' not found"
|
|
48
|
+
", falling back to user configured default",
|
|
49
|
+
)
|
|
50
|
+
name = None
|
|
51
|
+
if not name:
|
|
52
|
+
name = self.name_getter()
|
|
53
|
+
if name not in self.data:
|
|
54
|
+
logger.warning(
|
|
55
|
+
f"User configured {self.template_type} template '{name}' not found"
|
|
56
|
+
", falling back to plugin default",
|
|
57
|
+
)
|
|
58
|
+
name = "default"
|
|
59
|
+
return self.data[name]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
index_templates = TemplateDecoCollector[IndexTemplateHandler](
|
|
63
|
+
"index",
|
|
64
|
+
lambda: config.index_template,
|
|
65
|
+
)
|
|
66
|
+
detail_templates = TemplateDecoCollector[DetailTemplateHandler](
|
|
67
|
+
"detail",
|
|
68
|
+
lambda: config.detail_template,
|
|
69
|
+
)
|
|
70
|
+
func_detail_templates = TemplateDecoCollector[FuncDetailTemplateHandler](
|
|
71
|
+
"func detail",
|
|
72
|
+
lambda: config.func_detail_template,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def load_builtin_templates():
|
|
77
|
+
auto_import(Path(__file__).parent, __package__)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from functools import cached_property
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
import jinja2 as jj
|
|
7
|
+
from cookit import DebugFileWriter
|
|
8
|
+
from cookit.jinja import register_all_filters
|
|
9
|
+
from cookit.pw import RouterGroup, make_real_path_router, screenshot_html
|
|
10
|
+
from cookit.pw.loguru import log_router_err
|
|
11
|
+
from cookit.pyd import model_with_alias_generator
|
|
12
|
+
from nonebot import get_plugin_config
|
|
13
|
+
from nonebot_plugin_alconna.uniseg import UniMessage
|
|
14
|
+
from nonebot_plugin_htmlrender import get_new_page
|
|
15
|
+
from pydantic import BaseModel, Field
|
|
16
|
+
|
|
17
|
+
from ...data_source import PMDataItem, PMNPluginInfo
|
|
18
|
+
from .. import detail_templates, func_detail_templates, index_templates
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from playwright.async_api import Route
|
|
22
|
+
from yarl import URL
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@model_with_alias_generator(lambda x: f"pmn_default_{x}")
|
|
26
|
+
class TemplateConfigModel(BaseModel):
|
|
27
|
+
command_start: set[str] = Field(alias="command_start")
|
|
28
|
+
|
|
29
|
+
dark: bool = False
|
|
30
|
+
additional_css: list[str] = []
|
|
31
|
+
additional_js: list[str] = []
|
|
32
|
+
|
|
33
|
+
@cached_property
|
|
34
|
+
def pfx(self) -> str:
|
|
35
|
+
return next(iter(self.command_start), "")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
template_config = get_plugin_config(TemplateConfigModel)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
RES_DIR = Path(__file__).parent / "res"
|
|
42
|
+
ROUTE_BASE_URL = "https://picmenu-next.nonebot"
|
|
43
|
+
debug = DebugFileWriter(Path.cwd() / "debug", "picmenu-next", "default")
|
|
44
|
+
|
|
45
|
+
jj_env = jj.Environment(
|
|
46
|
+
loader=jj.FileSystemLoader(Path(__file__).parent / "res"),
|
|
47
|
+
autoescape=True,
|
|
48
|
+
enable_async=True,
|
|
49
|
+
)
|
|
50
|
+
register_all_filters(jj_env)
|
|
51
|
+
|
|
52
|
+
base_routers = RouterGroup()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@base_routers.router(f"{ROUTE_BASE_URL}/")
|
|
56
|
+
@log_router_err()
|
|
57
|
+
async def _(route: "Route", **_):
|
|
58
|
+
await route.fulfill(content_type="text/html", body="<h1>Hello World!</h1>")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@base_routers.router(re.compile(rf"^{ROUTE_BASE_URL}/local-file\?path=[^/]+"))
|
|
62
|
+
@make_real_path_router
|
|
63
|
+
@log_router_err()
|
|
64
|
+
async def _(url: "URL", **_):
|
|
65
|
+
return Path(url.query["path"]).resolve()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@base_routers.router(f"{ROUTE_BASE_URL}/**/*", 99)
|
|
69
|
+
@make_real_path_router
|
|
70
|
+
@log_router_err()
|
|
71
|
+
async def _(url: "URL", **_):
|
|
72
|
+
return RES_DIR.joinpath(*url.parts[1:])
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def version():
|
|
76
|
+
from ... import __version__
|
|
77
|
+
|
|
78
|
+
return __version__
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def render(template: str, routers: RouterGroup, **kwargs):
|
|
82
|
+
template_obj = jj_env.get_template(template)
|
|
83
|
+
html = await template_obj.render_async(
|
|
84
|
+
**kwargs,
|
|
85
|
+
cfg=template_config,
|
|
86
|
+
version=version(),
|
|
87
|
+
)
|
|
88
|
+
if debug.enabled:
|
|
89
|
+
debug.write(html, f"{template.replace('.html.jinja', '')}_{{time}}.html")
|
|
90
|
+
|
|
91
|
+
async with get_new_page() as page:
|
|
92
|
+
await routers.apply(page)
|
|
93
|
+
await page.goto(f"{ROUTE_BASE_URL}/")
|
|
94
|
+
pic = await screenshot_html(page, html, selector="main")
|
|
95
|
+
return UniMessage.image(raw=pic)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@index_templates("default")
|
|
99
|
+
async def render_index(infos: list[PMNPluginInfo]) -> UniMessage:
|
|
100
|
+
routers = base_routers.copy()
|
|
101
|
+
return await render(
|
|
102
|
+
"index.html.jinja",
|
|
103
|
+
routers,
|
|
104
|
+
infos=infos,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_plugin_desc(info: PMNPluginInfo):
|
|
109
|
+
return " | ".join(
|
|
110
|
+
x
|
|
111
|
+
for x in (
|
|
112
|
+
f"By {info.author}" if info.author else None,
|
|
113
|
+
f"v{info.version}" if info.version else None,
|
|
114
|
+
)
|
|
115
|
+
if x
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@detail_templates("default")
|
|
120
|
+
async def render_detail(info: PMNPluginInfo, info_index: int) -> UniMessage:
|
|
121
|
+
routers = base_routers.copy()
|
|
122
|
+
return await render(
|
|
123
|
+
"detail.html.jinja",
|
|
124
|
+
routers,
|
|
125
|
+
info=info,
|
|
126
|
+
info_index=info_index,
|
|
127
|
+
desc=get_plugin_desc(info),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@func_detail_templates("default")
|
|
132
|
+
async def render_func_detail(
|
|
133
|
+
info: PMNPluginInfo,
|
|
134
|
+
info_index: int,
|
|
135
|
+
func: PMDataItem,
|
|
136
|
+
func_index: int,
|
|
137
|
+
) -> UniMessage:
|
|
138
|
+
routers = base_routers.copy()
|
|
139
|
+
return await render(
|
|
140
|
+
"detail.html.jinja",
|
|
141
|
+
routers,
|
|
142
|
+
info=info,
|
|
143
|
+
info_index=info_index,
|
|
144
|
+
desc=get_plugin_desc(info),
|
|
145
|
+
func=func,
|
|
146
|
+
func_index=func_index,
|
|
147
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<link rel="stylesheet" href="./css/base.css" />
|
|
8
|
+
{% if cfg.dark %}<link rel="stylesheet" href="./css/dark.css" />{% endif -%}
|
|
9
|
+
{% block head %}{% endblock %}
|
|
10
|
+
{% for f in cfg.additional_css -%}
|
|
11
|
+
<link rel="stylesheet" href="./local-file/?path={{ f | url_encode | safe }}" />
|
|
12
|
+
{% endfor -%}
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<main>
|
|
16
|
+
<div class="main-wrapper">
|
|
17
|
+
{% block main %}{% endblock %}
|
|
18
|
+
<div class="main-footer">
|
|
19
|
+
Generated by PicMenu-Next v{{ version }} | Made with ❤️ by LgCookie
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</main>
|
|
23
|
+
</body>
|
|
24
|
+
{% for f in cfg.additional_js -%}
|
|
25
|
+
<script src="./local-file/?path={{ f | url_encode | safe }}"></script>
|
|
26
|
+
{% endfor -%}
|
|
27
|
+
{% block script %}{% endblock %}
|
|
28
|
+
</html>
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--background-color: unset;
|
|
3
|
+
--background-image: linear-gradient(120deg, #e0c3fc 0%, #8ec5fc 100%);
|
|
4
|
+
|
|
5
|
+
--background-color-wrapper: #fff3;
|
|
6
|
+
--background-image-wrapper: unset;
|
|
7
|
+
|
|
8
|
+
--card-background-color: unset;
|
|
9
|
+
--card-background-image: linear-gradient(120deg, #fdfbfb99 0%, #ebedee99 100%);
|
|
10
|
+
--card-background-border: #00000014 1px solid;
|
|
11
|
+
--card-shadow: rgba(0, 0, 0, 0.05) 0px 6px 24px 0px;
|
|
12
|
+
|
|
13
|
+
--text-shadow: 0 0 4px #3332;
|
|
14
|
+
|
|
15
|
+
--font-color-main: #000d;
|
|
16
|
+
--font-color-sub: #666d;
|
|
17
|
+
|
|
18
|
+
--font-color-card-index: #3336;
|
|
19
|
+
--text-shadow-card-index: 0 0 4px #3331;
|
|
20
|
+
|
|
21
|
+
--title-deco-color: #8174a0;
|
|
22
|
+
|
|
23
|
+
--font-family-main: 'HarmonyOS Sans SC';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
* {
|
|
27
|
+
box-sizing: border-box;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
h1,
|
|
31
|
+
h2,
|
|
32
|
+
h3,
|
|
33
|
+
h4,
|
|
34
|
+
h5,
|
|
35
|
+
h6,
|
|
36
|
+
p {
|
|
37
|
+
margin: 0;
|
|
38
|
+
padding: 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
h1.deco::before,
|
|
42
|
+
h2.deco::before,
|
|
43
|
+
h3.deco::before,
|
|
44
|
+
h4.deco::before,
|
|
45
|
+
h5.deco::before,
|
|
46
|
+
h6.deco::before {
|
|
47
|
+
content: '';
|
|
48
|
+
display: inline-block;
|
|
49
|
+
vertical-align: -10%;
|
|
50
|
+
width: 4px;
|
|
51
|
+
height: 1em;
|
|
52
|
+
margin-left: 2px;
|
|
53
|
+
margin-right: 6px;
|
|
54
|
+
border-radius: 9999px;
|
|
55
|
+
background-color: var(--title-deco-color);
|
|
56
|
+
box-shadow: var(--text-shadow);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.sub {
|
|
60
|
+
color: var(--font-color-sub);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
main {
|
|
64
|
+
background-color: var(--background-color);
|
|
65
|
+
background-image: var(--background-image);
|
|
66
|
+
background-size: cover;
|
|
67
|
+
background-position: center;
|
|
68
|
+
background-repeat: no-repeat;
|
|
69
|
+
|
|
70
|
+
color: var(--font-color-main);
|
|
71
|
+
font-family: var(--font-family-main), sans-serif;
|
|
72
|
+
text-shadow: var(--text-shadow);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
main > .main-wrapper {
|
|
76
|
+
padding: 16px;
|
|
77
|
+
display: flex;
|
|
78
|
+
flex-direction: column;
|
|
79
|
+
gap: 16px;
|
|
80
|
+
|
|
81
|
+
background-color: var(--background-color-wrapper);
|
|
82
|
+
background-image: var(--background-image-wrapper);
|
|
83
|
+
background-size: cover;
|
|
84
|
+
background-position: center;
|
|
85
|
+
background-repeat: no-repeat;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.main-footer {
|
|
89
|
+
text-align: end;
|
|
90
|
+
color: var(--font-color-sub);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.card-grid {
|
|
94
|
+
display: grid;
|
|
95
|
+
gap: 8px;
|
|
96
|
+
grid-template-columns: repeat(var(--grid-columns), 1fr);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.card {
|
|
100
|
+
border-radius: 8px;
|
|
101
|
+
padding: 8px;
|
|
102
|
+
|
|
103
|
+
background-color: var(--card-background-color);
|
|
104
|
+
background-image: var(--card-background-image);
|
|
105
|
+
background-size: cover;
|
|
106
|
+
background-position: center;
|
|
107
|
+
background-repeat: no-repeat;
|
|
108
|
+
border: var(--card-background-border);
|
|
109
|
+
box-shadow: var(--card-shadow);
|
|
110
|
+
|
|
111
|
+
text-shadow: none;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.card.flex {
|
|
115
|
+
display: flex;
|
|
116
|
+
flex-direction: column;
|
|
117
|
+
gap: 8px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.card.relative {
|
|
121
|
+
position: relative;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.card .index {
|
|
125
|
+
position: absolute;
|
|
126
|
+
bottom: 0;
|
|
127
|
+
right: 10px;
|
|
128
|
+
font-size: 36px;
|
|
129
|
+
font-style: italic;
|
|
130
|
+
color: var(--font-color-card-index);
|
|
131
|
+
text-shadow: var(--text-shadow-card-index);
|
|
132
|
+
z-index: 2;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.card .index > .no {
|
|
136
|
+
font-size: 24px;
|
|
137
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--background-image: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
3
|
+
--background-color-wrapper: #3336;
|
|
4
|
+
--card-background-color: unset;
|
|
5
|
+
--card-background-image: linear-gradient(60deg, #29323c99 0%, #373c4299 100%);
|
|
6
|
+
--font-color-main: #fffd;
|
|
7
|
+
--font-color-sub: #cccd;
|
|
8
|
+
--font-color-card-index: #ccc6;
|
|
9
|
+
--title-deco-color: #ab7dc5;
|
|
10
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{%- extends "base.html.jinja" -%}
|
|
2
|
+
|
|
3
|
+
{%- block head -%}
|
|
4
|
+
<link rel="stylesheet" href="./css/detail.css" />
|
|
5
|
+
{%- endblock -%}
|
|
6
|
+
|
|
7
|
+
{%- block main -%}
|
|
8
|
+
{% if func -%}
|
|
9
|
+
<div class="main-header">
|
|
10
|
+
<h1>{{ info.name }} <span class="sub">></span> {{ func.func }}</h1>
|
|
11
|
+
<p>{{ desc }}</p>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="inner-flex">
|
|
14
|
+
<h2 class="deco">触发条件</h2>
|
|
15
|
+
<div class="card">{{ func.trigger_method }}</div>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="inner-flex">
|
|
18
|
+
<h2 class="deco">触发方式</h2>
|
|
19
|
+
<div class="card">{{ func.trigger_condition }}</div>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="inner-flex">
|
|
22
|
+
<h2 class="deco">简要介绍</h2>
|
|
23
|
+
<div class="card">{{ func.brief_des | br }}</div>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="inner-flex">
|
|
26
|
+
<h2 class="deco">详细用法</h2>
|
|
27
|
+
<div class="card">{{ func.detail_des | br }}</div>
|
|
28
|
+
</div>
|
|
29
|
+
{% else -%}
|
|
30
|
+
<div class="main-header">
|
|
31
|
+
<h1>{{ info.name }}</h1>
|
|
32
|
+
<p>{{ desc }}</p>
|
|
33
|
+
</div>
|
|
34
|
+
{% if info.description -%}
|
|
35
|
+
<div class="inner-flex">
|
|
36
|
+
<h2 class="deco">简介</h2>
|
|
37
|
+
<div class="card">{{ info.description }}</div>
|
|
38
|
+
</div>
|
|
39
|
+
{% endif -%}
|
|
40
|
+
{% if info.usage -%}
|
|
41
|
+
<div class="inner-flex">
|
|
42
|
+
<h2 class="deco">用法</h2>
|
|
43
|
+
<div class="card">{{ info.usage | br }}</div>
|
|
44
|
+
</div>
|
|
45
|
+
{% endif -%}
|
|
46
|
+
{% if info.pm_data -%}
|
|
47
|
+
<div class="inner-flex">
|
|
48
|
+
<div>
|
|
49
|
+
<h2 class="deco">功能</h2>
|
|
50
|
+
<p>发送 <b>菜单 {{ info_index + 1 }} 功能名称或序号</b> 获取某功能详细信息</p>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="card-grid functions">
|
|
53
|
+
{% for x in info.pm_data -%}
|
|
54
|
+
<div class="card flex relative">
|
|
55
|
+
<div class="index"><span class="no">No.</span>{{ loop.index }}</div>
|
|
56
|
+
<h3>{{ x.func }}</h3>
|
|
57
|
+
<p>
|
|
58
|
+
触发条件:{{ x.trigger_method }}<br />
|
|
59
|
+
触发方式:{{ x.trigger_condition }}<br />
|
|
60
|
+
{{ x.brief_des | br }}
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
63
|
+
{% endfor -%}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
{% endif -%}
|
|
67
|
+
{% endif -%}
|
|
68
|
+
{%- endblock -%}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{%- extends "base.html.jinja" -%}
|
|
2
|
+
|
|
3
|
+
{%- block head -%}
|
|
4
|
+
<link rel="stylesheet" href="./css/index.css" />
|
|
5
|
+
{%- endblock -%}
|
|
6
|
+
|
|
7
|
+
{%- block main -%}
|
|
8
|
+
<div class="main-header">
|
|
9
|
+
<h1>机器人帮助菜单</h1>
|
|
10
|
+
<p>发送 <b>{{ cfg.pfx }}帮助 插件名或序号</b> 获取关于某插件的更多信息</p>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="card-grid">
|
|
13
|
+
{% for it in infos -%}
|
|
14
|
+
<div class="card flex relative">
|
|
15
|
+
<div class="index"><span class="no">No.</span>{{ loop.index }}</div>
|
|
16
|
+
<h3>{{ it.name }}</h3>
|
|
17
|
+
<p>{{ it.description | br }}</p>
|
|
18
|
+
</div>
|
|
19
|
+
{% endfor -%}
|
|
20
|
+
</div>
|
|
21
|
+
{%- endblock -%}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: nonebot-plugin-picmenu-next
|
|
3
|
+
Version: 0.1.0.dev1
|
|
4
|
+
Summary: Template plugin project
|
|
5
|
+
Author-Email: LgCookie <lgc2333@126.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: homepage, https://github.com/lgc-NB2Dev/nonebot-plugin-picmenu-next
|
|
8
|
+
Requires-Python: <4.0,>=3.9
|
|
9
|
+
Requires-Dist: nonebot2>=2.4.1
|
|
10
|
+
Requires-Dist: nonebot-plugin-alconna>=0.54.2
|
|
11
|
+
Requires-Dist: cookit[jinja,loguru,nonebot-alconna,nonebot-localstore,pw,pyd]>=0.11.2
|
|
12
|
+
Requires-Dist: pypinyin>=0.54.0
|
|
13
|
+
Requires-Dist: jieba>=0.42.1
|
|
14
|
+
Requires-Dist: thefuzz>=0.22.1
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
<!-- markdownlint-disable MD031 MD033 MD036 MD041 -->
|
|
18
|
+
|
|
19
|
+
<div align="center">
|
|
20
|
+
|
|
21
|
+
<a href="https://v2.nonebot.dev/store">
|
|
22
|
+
<img src="https://raw.githubusercontent.com/A-kirami/nonebot-plugin-template/resources/nbp_logo.png" width="180" height="180" alt="NoneBotPluginLogo">
|
|
23
|
+
</a>
|
|
24
|
+
|
|
25
|
+
<p>
|
|
26
|
+
<img src="https://raw.githubusercontent.com/lgc-NB2Dev/readme/main/template/plugin.svg" alt="NoneBotPluginText">
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
# NoneBot-Plugin-PicMenu-Next
|
|
30
|
+
|
|
31
|
+
_✨ NoneBot 插件简单描述 ✨_
|
|
32
|
+
|
|
33
|
+
<img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="python">
|
|
34
|
+
<a href="https://pdm.fming.dev">
|
|
35
|
+
<img src="https://img.shields.io/badge/pdm-managed-blueviolet" alt="pdm-managed">
|
|
36
|
+
</a>
|
|
37
|
+
<a href="https://wakatime.com/badge/user/b61b0f9a-f40b-4c82-bc51-0a75c67bfccf/project/f4778875-45a4-4688-8e1b-b8c844440abb">
|
|
38
|
+
<img src="https://wakatime.com/badge/user/b61b0f9a-f40b-4c82-bc51-0a75c67bfccf/project/f4778875-45a4-4688-8e1b-b8c844440abb.svg" alt="wakatime">
|
|
39
|
+
</a>
|
|
40
|
+
|
|
41
|
+
<br />
|
|
42
|
+
|
|
43
|
+
<a href="https://pydantic.dev">
|
|
44
|
+
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/lgc-NB2Dev/readme/main/template/pyd-v1-or-v2.json" alt="Pydantic Version 1 Or 2" >
|
|
45
|
+
</a>
|
|
46
|
+
<a href="./LICENSE">
|
|
47
|
+
<img src="https://img.shields.io/github/license/lgc-NB2Dev/nonebot-plugin-picmenu-next.svg" alt="license">
|
|
48
|
+
</a>
|
|
49
|
+
<a href="https://pypi.python.org/pypi/nonebot-plugin-picmenu-next">
|
|
50
|
+
<img src="https://img.shields.io/pypi/v/nonebot-plugin-picmenu-next.svg" alt="pypi">
|
|
51
|
+
</a>
|
|
52
|
+
<a href="https://pypi.python.org/pypi/nonebot-plugin-picmenu-next">
|
|
53
|
+
<img src="https://img.shields.io/pypi/dm/nonebot-plugin-picmenu-next" alt="pypi download">
|
|
54
|
+
</a>
|
|
55
|
+
|
|
56
|
+
<br />
|
|
57
|
+
|
|
58
|
+
<a href="https://registry.nonebot.dev/plugin/nonebot-plugin-picmenu-next:nonebot_plugin_picmenu_next">
|
|
59
|
+
<img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fnbbdg.lgc2333.top%2Fplugin%2Fnonebot-plugin-picmenu-next" alt="NoneBot Registry">
|
|
60
|
+
</a>
|
|
61
|
+
<a href="https://registry.nonebot.dev/plugin/nonebot-plugin-picmenu-next:nonebot_plugin_picmenu_next">
|
|
62
|
+
<img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fnbbdg.lgc2333.top%2Fplugin-adapters%2Fnonebot-plugin-picmenu-next" alt="Supported Adapters">
|
|
63
|
+
</a>
|
|
64
|
+
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
## 📖 介绍
|
|
68
|
+
|
|
69
|
+
这里是插件的详细介绍部分
|
|
70
|
+
|
|
71
|
+
## 💿 安装
|
|
72
|
+
|
|
73
|
+
以下提到的方法 任选**其一** 即可
|
|
74
|
+
|
|
75
|
+
<details open>
|
|
76
|
+
<summary>[推荐] 使用 nb-cli 安装</summary>
|
|
77
|
+
在 nonebot2 项目的根目录下打开命令行, 输入以下指令即可安装
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
nb plugin install nonebot-plugin-picmenu-next
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
</details>
|
|
84
|
+
|
|
85
|
+
<details>
|
|
86
|
+
<summary>使用包管理器安装</summary>
|
|
87
|
+
在 nonebot2 项目的插件目录下, 打开命令行, 根据你使用的包管理器, 输入相应的安装命令
|
|
88
|
+
|
|
89
|
+
<details>
|
|
90
|
+
<summary>pip</summary>
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pip install nonebot-plugin-picmenu-next
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
</details>
|
|
97
|
+
<details>
|
|
98
|
+
<summary>pdm</summary>
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pdm add nonebot-plugin-picmenu-next
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
</details>
|
|
105
|
+
<details>
|
|
106
|
+
<summary>poetry</summary>
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
poetry add nonebot-plugin-picmenu-next
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
</details>
|
|
113
|
+
<details>
|
|
114
|
+
<summary>conda</summary>
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
conda install nonebot-plugin-picmenu-next
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
</details>
|
|
121
|
+
|
|
122
|
+
打开 nonebot2 项目根目录下的 `pyproject.toml` 文件, 在 `[tool.nonebot]` 部分的 `plugins` 项里追加写入
|
|
123
|
+
|
|
124
|
+
```toml
|
|
125
|
+
[tool.nonebot]
|
|
126
|
+
plugins = [
|
|
127
|
+
# ...
|
|
128
|
+
"nonebot_plugin_picmenu_next"
|
|
129
|
+
]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
</details>
|
|
133
|
+
|
|
134
|
+
## ⚙️ 配置
|
|
135
|
+
|
|
136
|
+
在 nonebot2 项目的 `.env` 文件中添加下表中的必填配置
|
|
137
|
+
|
|
138
|
+
| 配置项 | 必填 | 默认值 | 说明 |
|
|
139
|
+
| :------: | :--: | :----: | :------: |
|
|
140
|
+
| 配置项 1 | 是 | 无 | 配置说明 |
|
|
141
|
+
| 配置项 2 | 否 | 无 | 配置说明 |
|
|
142
|
+
|
|
143
|
+
## 🎉 使用
|
|
144
|
+
|
|
145
|
+
### 指令表
|
|
146
|
+
|
|
147
|
+
| 指令 | 权限 | 需要@ | 范围 | 说明 |
|
|
148
|
+
| :----: | :--: | :---: | :--: | :------: |
|
|
149
|
+
| 指令 1 | 主人 | 否 | 私聊 | 指令说明 |
|
|
150
|
+
| 指令 2 | 群员 | 是 | 群聊 | 指令说明 |
|
|
151
|
+
|
|
152
|
+
### 效果图
|
|
153
|
+
|
|
154
|
+
如果有效果图的话
|
|
155
|
+
|
|
156
|
+
## 📞 联系
|
|
157
|
+
|
|
158
|
+
QQ:3076823485
|
|
159
|
+
Telegram:[@lgc2333](https://t.me/lgc2333)
|
|
160
|
+
吹水群:[1105946125](https://jq.qq.com/?_wv=1027&k=Z3n1MpEp)
|
|
161
|
+
邮箱:<lgc2333@126.com>
|
|
162
|
+
|
|
163
|
+
## 💡 鸣谢
|
|
164
|
+
|
|
165
|
+
如果有要鸣谢的人的话
|
|
166
|
+
|
|
167
|
+
## 💰 赞助
|
|
168
|
+
|
|
169
|
+
**[赞助我](https://blog.lgc2333.top/donate)**
|
|
170
|
+
|
|
171
|
+
感谢大家的赞助!你们的赞助将是我继续创作的动力!
|
|
172
|
+
|
|
173
|
+
## 📝 更新日志
|
|
174
|
+
|
|
175
|
+
芝士刚刚发布的插件,还没有更新日志的说 qwq~
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
nonebot_plugin_picmenu_next-0.1.0.dev1.dist-info/METADATA,sha256=NIIQQWbDfD-kFu_onX1KW0Ja4Hp0B7yf0dGloEa5gQ8,4847
|
|
2
|
+
nonebot_plugin_picmenu_next-0.1.0.dev1.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
|
|
3
|
+
nonebot_plugin_picmenu_next-0.1.0.dev1.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
|
4
|
+
nonebot_plugin_picmenu_next-0.1.0.dev1.dist-info/licenses/LICENSE,sha256=gPUiJ9TSIxT1ttYJOGCkZBk1ikEIeCLbUvTf1PU_pAE,1065
|
|
5
|
+
nonebot_plugin_picmenu_next/__init__.py,sha256=48NhCwYfb28FxFcOkfibjlWQr_BrpzJWwYhMnRYmvV8,999
|
|
6
|
+
nonebot_plugin_picmenu_next/__main__.py,sha256=iXur11G0oBce9FXOFBorNRMVIFvlGe_RhPRRfmpaVCw,5336
|
|
7
|
+
nonebot_plugin_picmenu_next/config.py,sha256=qrjkdcMA8MwYWkCTlI7xkFIxlQHkifCRMndCnK2wB3o,371
|
|
8
|
+
nonebot_plugin_picmenu_next/data_source.py,sha256=U8syGzqToSFery-W28dG-1rc-j0bW4GEn4vgqe0YCIc,10425
|
|
9
|
+
nonebot_plugin_picmenu_next/templates/__init__.py,sha256=g7d_2djgV_5ZC34GslliqvZPabtL4CdcEY7xoiGXNas,2356
|
|
10
|
+
nonebot_plugin_picmenu_next/templates/default/__init__.py,sha256=IFNZ95MN0whCVjGpf9B3jC3N16qOk_WsPmZl5rZsvT0,3906
|
|
11
|
+
nonebot_plugin_picmenu_next/templates/default/res/base.html.jinja,sha256=a9V0Gi1rLYN7jmMsH-1sLWu3k8M9mRKbK-HcmThH-k0,953
|
|
12
|
+
nonebot_plugin_picmenu_next/templates/default/res/css/base.css,sha256=xO-Y8-1H_mH3M5bCgxz0rJOJxOKHUzpnSoIcJN2sbVM,2606
|
|
13
|
+
nonebot_plugin_picmenu_next/templates/default/res/css/dark.css,sha256=SguUeVccY-Qcx8-9HgBmXbUqamXatqSYrZHuMmaUH7Q,355
|
|
14
|
+
nonebot_plugin_picmenu_next/templates/default/res/css/detail.css,sha256=_CZP6u-satvZxRCZbOeWKrqeuv5Cn9hHmvwkUr7tYtQ,213
|
|
15
|
+
nonebot_plugin_picmenu_next/templates/default/res/css/index.css,sha256=lJgPQhdodPZp5hJXPHmalRv0ppYwcW85WFDTz5s2P3o,97
|
|
16
|
+
nonebot_plugin_picmenu_next/templates/default/res/detail.html.jinja,sha256=qScKDS9_mvHv9tThmQhETeHNO_qtER2UP7t9yvePVPI,1816
|
|
17
|
+
nonebot_plugin_picmenu_next/templates/default/res/index.html.jinja,sha256=GJz7heyAszbjS7lbZKpdYswak6LP34SMXygS5pY73SM,575
|
|
18
|
+
nonebot_plugin_picmenu_next-0.1.0.dev1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 LgCookie
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|