nonebot-plugin-picmenu-next 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.
- nonebot_plugin_picmenu_next/__init__.py +33 -0
- nonebot_plugin_picmenu_next/__main__.py +203 -0
- nonebot_plugin_picmenu_next/config.py +20 -0
- nonebot_plugin_picmenu_next/data_source/__init__.py +16 -0
- nonebot_plugin_picmenu_next/data_source/collect.py +126 -0
- nonebot_plugin_picmenu_next/data_source/mixin.py +225 -0
- nonebot_plugin_picmenu_next/data_source/models.py +93 -0
- nonebot_plugin_picmenu_next/data_source/pinyin.py +51 -0
- nonebot_plugin_picmenu_next/ft_parser.py +222 -0
- nonebot_plugin_picmenu_next/res/gan_shen_me.jpg +0 -0
- nonebot_plugin_picmenu_next/templates/__init__.py +87 -0
- nonebot_plugin_picmenu_next/templates/default/__init__.py +126 -0
- nonebot_plugin_picmenu_next/templates/default/res/base.html.jinja +35 -0
- nonebot_plugin_picmenu_next/templates/default/res/css/base.css +230 -0
- nonebot_plugin_picmenu_next/templates/default/res/css/dark.css +11 -0
- nonebot_plugin_picmenu_next/templates/default/res/css/detail.css +16 -0
- nonebot_plugin_picmenu_next/templates/default/res/css/index.css +8 -0
- nonebot_plugin_picmenu_next/templates/default/res/detail.html.jinja +70 -0
- nonebot_plugin_picmenu_next/templates/default/res/index.html.jinja +25 -0
- nonebot_plugin_picmenu_next/templates/default/res/js/base.js +7 -0
- nonebot_plugin_picmenu_next/templates/default/res/js/tsconfig.json +8 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/code-colorful.css +69 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/code-github-dark.css +81 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_AMS-Regular.ttf +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_AMS-Regular.woff +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Main-Bold.ttf +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Main-Bold.woff +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Main-Bold.woff2 +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Main-Italic.ttf +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Main-Italic.woff +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Main-Italic.woff2 +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Main-Regular.ttf +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Main-Regular.woff +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Main-Regular.woff2 +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Math-Italic.ttf +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Math-Italic.woff +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Math-Italic.woff2 +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Script-Regular.ttf +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Script-Regular.woff +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Script-Regular.woff2 +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Size1-Regular.ttf +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Size1-Regular.woff +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Size2-Regular.ttf +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Size2-Regular.woff +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Size3-Regular.ttf +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Size3-Regular.woff +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Size4-Regular.ttf +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Size4-Regular.woff +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/katex.min.css +1 -0
- nonebot_plugin_picmenu_next/templates/default/res/third-party/katex.min.js +1 -0
- nonebot_plugin_picmenu_next/templates/pw_utils.py +82 -0
- nonebot_plugin_picmenu_next/utils.py +18 -0
- nonebot_plugin_picmenu_next-0.1.0.dist-info/METADATA +225 -0
- nonebot_plugin_picmenu_next-0.1.0.dist-info/RECORD +92 -0
- nonebot_plugin_picmenu_next-0.1.0.dist-info/WHEEL +4 -0
- nonebot_plugin_picmenu_next-0.1.0.dist-info/entry_points.txt +4 -0
- nonebot_plugin_picmenu_next-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,33 @@
|
|
|
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_htmlrender")
|
|
8
|
+
|
|
9
|
+
from . import __main__ as __main__
|
|
10
|
+
from .config import ConfigModel
|
|
11
|
+
from .data_source import refresh_infos
|
|
12
|
+
from .templates import load_builtin_templates
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.0"
|
|
15
|
+
__plugin_meta__ = PluginMetadata(
|
|
16
|
+
name="PicMenu Next",
|
|
17
|
+
description="新一代的图片帮助插件",
|
|
18
|
+
usage="发送“帮助”查看所有所有插件功能",
|
|
19
|
+
type="application",
|
|
20
|
+
homepage="https://github.com/lgc-NB2Dev/nonebot-plugin-picmenu-next",
|
|
21
|
+
config=ConfigModel,
|
|
22
|
+
supported_adapters=inherit_supported_adapters("nonebot_plugin_alconna"),
|
|
23
|
+
extra={"License": "MIT", "Author": "LgCookie"},
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
load_builtin_templates()
|
|
27
|
+
|
|
28
|
+
driver = get_driver()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@driver.on_startup
|
|
32
|
+
async def _():
|
|
33
|
+
await refresh_infos()
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from pathlib import Path
|
|
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.adapters import Bot as BaseBot, Event as BaseEvent
|
|
8
|
+
from nonebot.permission import SUPERUSER
|
|
9
|
+
from nonebot_plugin_alconna import Query, on_alconna
|
|
10
|
+
from nonebot_plugin_alconna.uniseg import UniMessage
|
|
11
|
+
from thefuzz import process
|
|
12
|
+
|
|
13
|
+
from .config import config
|
|
14
|
+
from .data_source import get_infos
|
|
15
|
+
from .data_source.mixin import resolve_detail_mixin, resolve_main_mixin
|
|
16
|
+
from .data_source.models import PinyinChunkSequence, PMDataItem, PMNPluginInfo
|
|
17
|
+
from .templates import detail_templates, func_detail_templates, index_templates
|
|
18
|
+
|
|
19
|
+
RES_DIR = Path(__file__).parent / "res"
|
|
20
|
+
TIP_IMG_PATH = RES_DIR / "gan_shen_me.jpg"
|
|
21
|
+
|
|
22
|
+
alc = Alconna(
|
|
23
|
+
"help",
|
|
24
|
+
Args(
|
|
25
|
+
Arg("plugin?", str, notice="插件序号或名称"),
|
|
26
|
+
Arg("function?", str, notice="插件功能序号或名称"),
|
|
27
|
+
),
|
|
28
|
+
Option(
|
|
29
|
+
"-H|--show-hidden",
|
|
30
|
+
action=store_true,
|
|
31
|
+
help_text="显示隐藏的插件",
|
|
32
|
+
),
|
|
33
|
+
meta=CommandMeta(
|
|
34
|
+
description="新一代的图片帮助插件",
|
|
35
|
+
author="LgCookie",
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
m_cls = on_alconna(
|
|
39
|
+
alc,
|
|
40
|
+
aliases={"帮助", "菜单"},
|
|
41
|
+
skip_for_unmatch=False,
|
|
42
|
+
auto_send_output=True,
|
|
43
|
+
use_cmd_start=True,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_name_similarities(
|
|
48
|
+
query: str,
|
|
49
|
+
query_pinyin: str,
|
|
50
|
+
choices: list[str],
|
|
51
|
+
choices_pinyin: list[str],
|
|
52
|
+
raw_weight: float = 0.6,
|
|
53
|
+
pinyin_weight: float = 0.4,
|
|
54
|
+
) -> list[float]:
|
|
55
|
+
raw_scores = [x[1] for x in process.extractWithoutOrder(query, choices)]
|
|
56
|
+
pinyin_scores = [
|
|
57
|
+
x[1] for x in process.extractWithoutOrder(query_pinyin, choices_pinyin)
|
|
58
|
+
]
|
|
59
|
+
similarities = [
|
|
60
|
+
raw_weight * raw + pinyin_weight * pinyin
|
|
61
|
+
for raw, pinyin in zip(raw_scores, pinyin_scores)
|
|
62
|
+
]
|
|
63
|
+
logger.opt(lazy=True).debug(
|
|
64
|
+
"Query: {}, similarities:\n{}",
|
|
65
|
+
lambda: f"{query} ({query_pinyin})",
|
|
66
|
+
lambda: ";\n".join(
|
|
67
|
+
(
|
|
68
|
+
f"{choices[i]} ({choices_pinyin[i]})"
|
|
69
|
+
f": ({raw} * {raw_weight}) + ({pin} * {pinyin_weight}) = {sim}"
|
|
70
|
+
)
|
|
71
|
+
for i, (raw, pin, sim) in sorted(
|
|
72
|
+
enumerate(zip(raw_scores, pinyin_scores, similarities)),
|
|
73
|
+
key=lambda x: x[1],
|
|
74
|
+
reverse=True,
|
|
75
|
+
)
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
return similarities
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
T = TypeVar("T")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def handle_query_index(query: str, infos: Sequence[T]) -> Optional[tuple[int, T]]:
|
|
85
|
+
if query.isdigit() and query.strip("0"):
|
|
86
|
+
return (
|
|
87
|
+
((i := qn - 1), infos[i])
|
|
88
|
+
if (1 <= (qn := int(query)) <= len(infos))
|
|
89
|
+
else None
|
|
90
|
+
)
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
async def query_plugin(
|
|
95
|
+
infos: list[PMNPluginInfo],
|
|
96
|
+
query: str,
|
|
97
|
+
score_cutoff: float = 60,
|
|
98
|
+
) -> Optional[tuple[int, PMNPluginInfo]]:
|
|
99
|
+
if r := handle_query_index(query, infos):
|
|
100
|
+
return r
|
|
101
|
+
|
|
102
|
+
choices: list[str] = []
|
|
103
|
+
choices_pinyin: list[str] = []
|
|
104
|
+
for info in infos:
|
|
105
|
+
choices.append(info.casefold_name)
|
|
106
|
+
choices_pinyin.append(info.name_pinyin.casefold_str)
|
|
107
|
+
|
|
108
|
+
similarities = get_name_similarities(
|
|
109
|
+
query.casefold(),
|
|
110
|
+
PinyinChunkSequence.from_raw(query).casefold_str,
|
|
111
|
+
choices,
|
|
112
|
+
choices_pinyin,
|
|
113
|
+
)
|
|
114
|
+
i, s = max(enumerate(similarities), key=lambda x: x[1])
|
|
115
|
+
if s >= score_cutoff:
|
|
116
|
+
return i, infos[i]
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async def query_func_detail(
|
|
121
|
+
pm_data: list[PMDataItem],
|
|
122
|
+
query: str,
|
|
123
|
+
score_cutoff: float = 60,
|
|
124
|
+
) -> Optional[tuple[int, PMDataItem]]:
|
|
125
|
+
if r := handle_query_index(query, pm_data):
|
|
126
|
+
return r
|
|
127
|
+
|
|
128
|
+
choices: list[str] = []
|
|
129
|
+
choices_pinyin: list[str] = []
|
|
130
|
+
for data in pm_data:
|
|
131
|
+
choices.append(data.casefold_func)
|
|
132
|
+
choices_pinyin.append(data.func_pinyin.casefold_str)
|
|
133
|
+
|
|
134
|
+
similarities = get_name_similarities(
|
|
135
|
+
query.casefold(),
|
|
136
|
+
PinyinChunkSequence.from_raw(query).casefold_str,
|
|
137
|
+
choices,
|
|
138
|
+
choices_pinyin,
|
|
139
|
+
)
|
|
140
|
+
i, s = max(enumerate(similarities), key=lambda x: x[1])
|
|
141
|
+
if s >= score_cutoff:
|
|
142
|
+
return i, pm_data[i]
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@m_cls.handle()
|
|
147
|
+
async def _(
|
|
148
|
+
bot: BaseBot,
|
|
149
|
+
ev: BaseEvent,
|
|
150
|
+
q_plugin: Query[Optional[str]] = Query("~plugin", None),
|
|
151
|
+
q_function: Query[Optional[str]] = Query("~function", None),
|
|
152
|
+
q_show_hidden: Query[bool] = Query("~show-hidden.value", default=False),
|
|
153
|
+
):
|
|
154
|
+
show_hidden = q_show_hidden.result
|
|
155
|
+
if (
|
|
156
|
+
show_hidden
|
|
157
|
+
and config.only_superuser_see_hidden
|
|
158
|
+
and (not await SUPERUSER(bot, ev))
|
|
159
|
+
):
|
|
160
|
+
await (
|
|
161
|
+
UniMessage.image(raw=TIP_IMG_PATH.read_bytes())
|
|
162
|
+
.text("别想看咱藏起来的东西!")
|
|
163
|
+
.finish(reply_to=True)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
infos = await resolve_main_mixin(get_infos())
|
|
167
|
+
if not show_hidden:
|
|
168
|
+
infos = [x for x in infos if not x.pmn.hidden]
|
|
169
|
+
|
|
170
|
+
if not q_plugin.result:
|
|
171
|
+
m = await index_templates.get()(infos, show_hidden)
|
|
172
|
+
await m.finish()
|
|
173
|
+
|
|
174
|
+
r = await query_plugin(infos, q_plugin.result)
|
|
175
|
+
if not r:
|
|
176
|
+
await UniMessage.text("好像没有找到对应插件呢……").finish(reply_to=True)
|
|
177
|
+
info_index, info = r
|
|
178
|
+
if not q_function.result:
|
|
179
|
+
m = await detail_templates.get(
|
|
180
|
+
info.pmn.template,
|
|
181
|
+
)(info, info_index, show_hidden)
|
|
182
|
+
await m.finish()
|
|
183
|
+
|
|
184
|
+
info = await resolve_detail_mixin(info)
|
|
185
|
+
pm_data = info.pm_data
|
|
186
|
+
if pm_data and (not show_hidden):
|
|
187
|
+
pm_data = [x for x in pm_data if not x.hidden]
|
|
188
|
+
|
|
189
|
+
if not pm_data:
|
|
190
|
+
await UniMessage.text(
|
|
191
|
+
f"插件 `{info.name}` 没有详细功能介绍哦",
|
|
192
|
+
).finish(reply_to=True)
|
|
193
|
+
|
|
194
|
+
r = await query_func_detail(pm_data, q_function.result)
|
|
195
|
+
if not r:
|
|
196
|
+
await UniMessage.text(
|
|
197
|
+
f"好像没有找到插件 `{info.name}` 的对应功能呢……",
|
|
198
|
+
).finish(reply_to=True)
|
|
199
|
+
func_index, func = r
|
|
200
|
+
m = await func_detail_templates.get(
|
|
201
|
+
func.template,
|
|
202
|
+
)(info, info_index, func, func_index, show_hidden)
|
|
203
|
+
await m.finish()
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
only_superuser_see_hidden: bool = False
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
config: ConfigModel = get_plugin_config(ConfigModel)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def version():
|
|
18
|
+
from . import __version__
|
|
19
|
+
|
|
20
|
+
return __version__
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from nonebot import get_loaded_plugins as _get_loaded_plugins
|
|
2
|
+
|
|
3
|
+
from .collect import collect_plugin_infos as _collect_plugin_infos
|
|
4
|
+
from .models import PMNPluginInfo as _PMNPluginInfoRaw
|
|
5
|
+
|
|
6
|
+
_infos: list[_PMNPluginInfoRaw] = []
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_infos() -> list[_PMNPluginInfoRaw]:
|
|
10
|
+
return _infos
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def refresh_infos() -> list[_PMNPluginInfoRaw]:
|
|
14
|
+
global _infos
|
|
15
|
+
_infos = await _collect_plugin_infos(_get_loaded_plugins())
|
|
16
|
+
return _infos
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import importlib
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
from importlib.metadata import Distribution, PackageNotFoundError, distribution
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from cookit.loguru import warning_suppress
|
|
10
|
+
from cookit.pyd import type_validate_python
|
|
11
|
+
from nonebot import logger
|
|
12
|
+
from nonebot.plugin import Plugin
|
|
13
|
+
|
|
14
|
+
from ..utils import normalize_plugin_name
|
|
15
|
+
from .mixin import chain_mixins, plugin_collect_mixins
|
|
16
|
+
from .models import PMNData, PMNPluginExtra, PMNPluginInfo
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def normalize_metadata_user(info: str, allow_multi: bool = False) -> str:
|
|
20
|
+
infos = info.split(",")
|
|
21
|
+
if not allow_multi:
|
|
22
|
+
infos = infos[:1]
|
|
23
|
+
return " & ".join(x.split("<")[0].strip().strip("'\"") for x in infos)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@lru_cache
|
|
27
|
+
def get_dist(module_name: str) -> Optional[Distribution]:
|
|
28
|
+
with warning_suppress(f"Unexpected error happened when getting info of package {module_name}"),\
|
|
29
|
+
suppress(PackageNotFoundError): # fmt: skip
|
|
30
|
+
return distribution(module_name)
|
|
31
|
+
if "." not in module_name:
|
|
32
|
+
return None
|
|
33
|
+
module_name = module_name.rsplit(".", 1)[0]
|
|
34
|
+
return get_dist(module_name)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@lru_cache
|
|
38
|
+
def get_version_attr(module_name: str) -> Optional[str]:
|
|
39
|
+
with warning_suppress(f"Unexpected error happened when importing {module_name}"),\
|
|
40
|
+
suppress(ImportError): # fmt: skip
|
|
41
|
+
m = importlib.import_module(module_name)
|
|
42
|
+
if ver := getattr(m, "__version__", None):
|
|
43
|
+
return ver
|
|
44
|
+
if "." not in module_name:
|
|
45
|
+
return None
|
|
46
|
+
module_name = module_name.rsplit(".", 1)[0]
|
|
47
|
+
return get_version_attr(module_name)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def get_info_from_plugin(plugin: Plugin) -> PMNPluginInfo:
|
|
51
|
+
meta = plugin.metadata
|
|
52
|
+
extra: Optional[PMNPluginExtra] = None
|
|
53
|
+
if meta:
|
|
54
|
+
with warning_suppress(f"Failed to parse plugin metadata of {plugin.id_}"):
|
|
55
|
+
extra = type_validate_python(PMNPluginExtra, meta.extra)
|
|
56
|
+
|
|
57
|
+
name = normalize_plugin_name(meta.name if meta else plugin.id_)
|
|
58
|
+
|
|
59
|
+
ver = extra.version if extra else None
|
|
60
|
+
if not ver:
|
|
61
|
+
ver = get_version_attr(plugin.module_name)
|
|
62
|
+
if not ver and (dist := get_dist(plugin.module_name)):
|
|
63
|
+
ver = dist.version
|
|
64
|
+
|
|
65
|
+
author = (
|
|
66
|
+
(" & ".join(extra.author) if isinstance(extra.author, list) else extra.author)
|
|
67
|
+
if extra
|
|
68
|
+
else None
|
|
69
|
+
)
|
|
70
|
+
if not author and (dist := get_dist(plugin.module_name)):
|
|
71
|
+
if author := dist.metadata.get("Author") or dist.metadata.get("Maintainer"):
|
|
72
|
+
author = normalize_metadata_user(author)
|
|
73
|
+
elif author := dist.metadata.get("Author-Email") or dist.metadata.get(
|
|
74
|
+
"Maintainer-Email",
|
|
75
|
+
):
|
|
76
|
+
author = normalize_metadata_user(author, allow_multi=True)
|
|
77
|
+
|
|
78
|
+
description = (
|
|
79
|
+
meta.description
|
|
80
|
+
if meta
|
|
81
|
+
else (
|
|
82
|
+
dist.metadata.get("Summary")
|
|
83
|
+
if (dist := get_dist(plugin.module_name))
|
|
84
|
+
else None
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
pmn = (extra.pmn if extra else None) or PMNData()
|
|
89
|
+
if ("hidden" not in pmn.model_fields_set) and meta and meta.type == "library":
|
|
90
|
+
pmn = PMNData(hidden=True)
|
|
91
|
+
|
|
92
|
+
logger.debug(f"Completed to get info of plugin {plugin.id_}")
|
|
93
|
+
return PMNPluginInfo(
|
|
94
|
+
plugin_id=plugin.id_,
|
|
95
|
+
name=name,
|
|
96
|
+
author=author,
|
|
97
|
+
version=ver,
|
|
98
|
+
description=description,
|
|
99
|
+
usage=meta.usage if meta else None,
|
|
100
|
+
pm_data=extra.menu_data if extra else None,
|
|
101
|
+
pmn=pmn,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def collect_plugin_infos(plugins: Iterable[Plugin]):
|
|
106
|
+
async def _get(p: Plugin):
|
|
107
|
+
with warning_suppress(f"Failed to get plugin info of {p.id_}"):
|
|
108
|
+
return await get_info_from_plugin(p)
|
|
109
|
+
|
|
110
|
+
infos = await asyncio.gather(
|
|
111
|
+
*(_get(plugin) for plugin in plugins),
|
|
112
|
+
)
|
|
113
|
+
infos = [x for x in infos if x]
|
|
114
|
+
|
|
115
|
+
async def final_mixin(infos: list[PMNPluginInfo]):
|
|
116
|
+
return infos
|
|
117
|
+
|
|
118
|
+
mixin_chain = chain_mixins(plugin_collect_mixins.data, final_mixin)
|
|
119
|
+
infos = await mixin_chain(infos)
|
|
120
|
+
|
|
121
|
+
infos.sort(key=lambda x: x.name_pinyin)
|
|
122
|
+
logger.success(f"Collected {len(infos)} plugin infos")
|
|
123
|
+
|
|
124
|
+
get_dist.cache_clear()
|
|
125
|
+
get_version_attr.cache_clear()
|
|
126
|
+
return infos
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from collections.abc import Coroutine, Sequence
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Callable, Generic, Optional, TypeVar
|
|
5
|
+
from typing_extensions import Concatenate, ParamSpec, TypeAlias
|
|
6
|
+
|
|
7
|
+
from cookit import DecoListCollector
|
|
8
|
+
from cookit.loguru.common import warning_suppress
|
|
9
|
+
from nonebot.matcher import MatcherSource
|
|
10
|
+
from nonebot.plugin.on import get_matcher_source
|
|
11
|
+
|
|
12
|
+
from .models import PMNPluginInfo
|
|
13
|
+
|
|
14
|
+
T = TypeVar("T")
|
|
15
|
+
K = TypeVar("K")
|
|
16
|
+
V = TypeVar("V")
|
|
17
|
+
|
|
18
|
+
P = ParamSpec("P")
|
|
19
|
+
|
|
20
|
+
Co: TypeAlias = Coroutine[Any, Any, T]
|
|
21
|
+
MixinFunc: TypeAlias = Callable[
|
|
22
|
+
Concatenate[
|
|
23
|
+
Callable[P, T],
|
|
24
|
+
P,
|
|
25
|
+
],
|
|
26
|
+
T,
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
PluginCollectMixinNext: TypeAlias = Callable[
|
|
30
|
+
[list[PMNPluginInfo]],
|
|
31
|
+
Co[list[PMNPluginInfo]],
|
|
32
|
+
]
|
|
33
|
+
PluginCollectMixin: TypeAlias = MixinFunc[
|
|
34
|
+
[list[PMNPluginInfo]],
|
|
35
|
+
Co[list[PMNPluginInfo]],
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
SelfMixinNext: TypeAlias = Callable[
|
|
39
|
+
[PMNPluginInfo],
|
|
40
|
+
Co[PMNPluginInfo],
|
|
41
|
+
]
|
|
42
|
+
SelfMixin: TypeAlias = MixinFunc[
|
|
43
|
+
[PMNPluginInfo],
|
|
44
|
+
Co[PMNPluginInfo],
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
PluginMixinNext: TypeAlias = Callable[
|
|
48
|
+
[list[PMNPluginInfo]],
|
|
49
|
+
Co[list[PMNPluginInfo]],
|
|
50
|
+
]
|
|
51
|
+
PluginMixin: TypeAlias = MixinFunc[
|
|
52
|
+
[list[PMNPluginInfo]],
|
|
53
|
+
Co[list[PMNPluginInfo]],
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
PluginDetailMixinNext: TypeAlias = Callable[
|
|
57
|
+
[PMNPluginInfo],
|
|
58
|
+
Co[PMNPluginInfo],
|
|
59
|
+
]
|
|
60
|
+
PluginDetailMixin: TypeAlias = MixinFunc[
|
|
61
|
+
[PMNPluginInfo],
|
|
62
|
+
Co[PMNPluginInfo],
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class MixinInfo(Generic[T]):
|
|
68
|
+
func: T
|
|
69
|
+
priority: int
|
|
70
|
+
source: Optional[MatcherSource]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class MixinCollector(DecoListCollector[MixinInfo[T]]):
|
|
74
|
+
def __call__( # pyright: ignore[reportIncompatibleMethodOverride]
|
|
75
|
+
self,
|
|
76
|
+
priority: int = 5,
|
|
77
|
+
_depth: int = 0,
|
|
78
|
+
_matcher_source: Optional[MatcherSource] = None,
|
|
79
|
+
):
|
|
80
|
+
def deco(func: T) -> T:
|
|
81
|
+
self.data.append(
|
|
82
|
+
MixinInfo(
|
|
83
|
+
func=func,
|
|
84
|
+
priority=priority,
|
|
85
|
+
source=get_matcher_source(_depth + 1),
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
self.data.sort(key=lambda x: x.priority)
|
|
89
|
+
return func
|
|
90
|
+
|
|
91
|
+
return deco
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class SelfMixinCollector(defaultdict[str, MixinCollector[T]]):
|
|
95
|
+
def __init__(self) -> None:
|
|
96
|
+
super().__init__(MixinCollector)
|
|
97
|
+
|
|
98
|
+
def __call__(
|
|
99
|
+
self,
|
|
100
|
+
priority: int = 1,
|
|
101
|
+
_depth: int = 0,
|
|
102
|
+
_matcher_source: Optional[MatcherSource] = None,
|
|
103
|
+
):
|
|
104
|
+
def deco(f: T) -> T:
|
|
105
|
+
s = _matcher_source or get_matcher_source()
|
|
106
|
+
if (not s) or not (pid := s.plugin_id):
|
|
107
|
+
raise ValueError("Self plugin not found")
|
|
108
|
+
self[pid](priority, _depth + 1, s)(f)
|
|
109
|
+
return f
|
|
110
|
+
|
|
111
|
+
return deco
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
plugin_collect_mixins: MixinCollector[PluginCollectMixin] = MixinCollector()
|
|
115
|
+
|
|
116
|
+
self_mixins: SelfMixinCollector[SelfMixin] = SelfMixinCollector()
|
|
117
|
+
self_detail_mixins: SelfMixinCollector[PluginDetailMixin] = SelfMixinCollector()
|
|
118
|
+
|
|
119
|
+
plugin_mixins = MixinCollector[PluginMixin]()
|
|
120
|
+
plugin_detail_mixins = MixinCollector[PluginDetailMixin]()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def format_source_warn_msg(info: MixinInfo) -> str:
|
|
124
|
+
if (not (s := info.source)) or not s.plugin_id:
|
|
125
|
+
return "Failed to run mixin from unknown source"
|
|
126
|
+
return (
|
|
127
|
+
f"Failed to run mixin from plugin {s.plugin_id}"
|
|
128
|
+
f" at module {s.module_name or 'unknown'}, line {s.lineno or 'unknown'}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def chain_mixins(
|
|
133
|
+
mixins: Sequence[MixinInfo[MixinFunc[P, Co[T]]]],
|
|
134
|
+
final_mixin: Callable[P, Co[T]],
|
|
135
|
+
) -> Callable[P, Co[T]]:
|
|
136
|
+
"""
|
|
137
|
+
将一系列中间件函数链接起来,形成一个调用链。
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
mixins: 中间件函数列表,每个函数接收下一个调用函数和其他参数。
|
|
141
|
+
final_mixin: 最终执行的函数,只接收其他参数。
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
链接后的函数,接收与 final_mixin 相同的参数。
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
# 如果没有中间件,直接返回最终函数
|
|
148
|
+
if not mixins:
|
|
149
|
+
return final_mixin
|
|
150
|
+
|
|
151
|
+
# 从后向前构建调用链
|
|
152
|
+
chain = final_mixin
|
|
153
|
+
|
|
154
|
+
# 使用函数工厂避免闭包中的变量绑定问题
|
|
155
|
+
def create_wrapper(
|
|
156
|
+
current_mixin: MixinInfo[MixinFunc[P, Co[T]]],
|
|
157
|
+
next_chain: Callable[P, Co[T]],
|
|
158
|
+
) -> Callable[P, Co[T]]:
|
|
159
|
+
async def wrapped(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
160
|
+
with warning_suppress(lambda _: format_source_warn_msg(current_mixin)):
|
|
161
|
+
return await current_mixin.func(next_chain, *args, **kwargs)
|
|
162
|
+
return await next_chain(*args, **kwargs)
|
|
163
|
+
|
|
164
|
+
return wrapped
|
|
165
|
+
|
|
166
|
+
# 从后向前应用每个中间件
|
|
167
|
+
for mixin in reversed(mixins):
|
|
168
|
+
chain = create_wrapper(mixin, chain)
|
|
169
|
+
|
|
170
|
+
return chain
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
async def resolve_main_mixin(infos: list[PMNPluginInfo]):
|
|
174
|
+
if not infos:
|
|
175
|
+
return infos
|
|
176
|
+
|
|
177
|
+
infos = infos.copy()
|
|
178
|
+
|
|
179
|
+
if plugin_mixins.data:
|
|
180
|
+
|
|
181
|
+
async def last_external_mixin(infos: list[PMNPluginInfo]):
|
|
182
|
+
return infos
|
|
183
|
+
|
|
184
|
+
external_mixin_chain = chain_mixins(
|
|
185
|
+
plugin_mixins.data,
|
|
186
|
+
last_external_mixin,
|
|
187
|
+
)
|
|
188
|
+
infos = await external_mixin_chain(infos)
|
|
189
|
+
|
|
190
|
+
for i in range(len(infos)):
|
|
191
|
+
x = infos[i]
|
|
192
|
+
if (not x.plugin_id) or (x.plugin_id not in self_mixins):
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
async def last_mixin(info: PMNPluginInfo):
|
|
196
|
+
return info
|
|
197
|
+
|
|
198
|
+
self_mixin_chain = chain_mixins(
|
|
199
|
+
self_mixins[x.plugin_id].data,
|
|
200
|
+
last_mixin,
|
|
201
|
+
)
|
|
202
|
+
infos[i] = await self_mixin_chain(x)
|
|
203
|
+
|
|
204
|
+
return infos
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
async def resolve_detail_mixin(info: PMNPluginInfo):
|
|
208
|
+
async def last_mixin(info: PMNPluginInfo):
|
|
209
|
+
return info
|
|
210
|
+
|
|
211
|
+
if plugin_detail_mixins.data:
|
|
212
|
+
mixin_chain = chain_mixins(
|
|
213
|
+
plugin_detail_mixins.data,
|
|
214
|
+
last_mixin,
|
|
215
|
+
)
|
|
216
|
+
info = await mixin_chain(info)
|
|
217
|
+
|
|
218
|
+
if info.plugin_id and info.plugin_id in self_detail_mixins:
|
|
219
|
+
mixin_chain = chain_mixins(
|
|
220
|
+
self_detail_mixins[info.plugin_id].data,
|
|
221
|
+
last_mixin,
|
|
222
|
+
)
|
|
223
|
+
info = await mixin_chain(info)
|
|
224
|
+
|
|
225
|
+
return info
|