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.
Files changed (92) hide show
  1. nonebot_plugin_picmenu_next/__init__.py +33 -0
  2. nonebot_plugin_picmenu_next/__main__.py +203 -0
  3. nonebot_plugin_picmenu_next/config.py +20 -0
  4. nonebot_plugin_picmenu_next/data_source/__init__.py +16 -0
  5. nonebot_plugin_picmenu_next/data_source/collect.py +126 -0
  6. nonebot_plugin_picmenu_next/data_source/mixin.py +225 -0
  7. nonebot_plugin_picmenu_next/data_source/models.py +93 -0
  8. nonebot_plugin_picmenu_next/data_source/pinyin.py +51 -0
  9. nonebot_plugin_picmenu_next/ft_parser.py +222 -0
  10. nonebot_plugin_picmenu_next/res/gan_shen_me.jpg +0 -0
  11. nonebot_plugin_picmenu_next/templates/__init__.py +87 -0
  12. nonebot_plugin_picmenu_next/templates/default/__init__.py +126 -0
  13. nonebot_plugin_picmenu_next/templates/default/res/base.html.jinja +35 -0
  14. nonebot_plugin_picmenu_next/templates/default/res/css/base.css +230 -0
  15. nonebot_plugin_picmenu_next/templates/default/res/css/dark.css +11 -0
  16. nonebot_plugin_picmenu_next/templates/default/res/css/detail.css +16 -0
  17. nonebot_plugin_picmenu_next/templates/default/res/css/index.css +8 -0
  18. nonebot_plugin_picmenu_next/templates/default/res/detail.html.jinja +70 -0
  19. nonebot_plugin_picmenu_next/templates/default/res/index.html.jinja +25 -0
  20. nonebot_plugin_picmenu_next/templates/default/res/js/base.js +7 -0
  21. nonebot_plugin_picmenu_next/templates/default/res/js/tsconfig.json +8 -0
  22. nonebot_plugin_picmenu_next/templates/default/res/third-party/code-colorful.css +69 -0
  23. nonebot_plugin_picmenu_next/templates/default/res/third-party/code-github-dark.css +81 -0
  24. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_AMS-Regular.ttf +0 -0
  25. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_AMS-Regular.woff +0 -0
  26. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  27. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  28. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  29. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  30. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  31. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  32. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  33. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  34. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  35. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  36. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  37. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  38. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  39. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Main-Bold.ttf +0 -0
  40. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Main-Bold.woff +0 -0
  41. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Main-Bold.woff2 +0 -0
  42. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  43. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  44. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  45. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Main-Italic.ttf +0 -0
  46. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Main-Italic.woff +0 -0
  47. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Main-Italic.woff2 +0 -0
  48. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Main-Regular.ttf +0 -0
  49. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Main-Regular.woff +0 -0
  50. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Main-Regular.woff2 +0 -0
  51. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  52. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  53. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  54. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Math-Italic.ttf +0 -0
  55. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Math-Italic.woff +0 -0
  56. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Math-Italic.woff2 +0 -0
  57. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  58. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  59. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  60. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  61. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  62. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  63. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  64. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  65. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  66. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Script-Regular.ttf +0 -0
  67. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Script-Regular.woff +0 -0
  68. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Script-Regular.woff2 +0 -0
  69. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Size1-Regular.ttf +0 -0
  70. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Size1-Regular.woff +0 -0
  71. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  72. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Size2-Regular.ttf +0 -0
  73. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Size2-Regular.woff +0 -0
  74. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  75. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Size3-Regular.ttf +0 -0
  76. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Size3-Regular.woff +0 -0
  77. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  78. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Size4-Regular.ttf +0 -0
  79. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Size4-Regular.woff +0 -0
  80. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  81. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  82. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  83. nonebot_plugin_picmenu_next/templates/default/res/third-party/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  84. nonebot_plugin_picmenu_next/templates/default/res/third-party/katex.min.css +1 -0
  85. nonebot_plugin_picmenu_next/templates/default/res/third-party/katex.min.js +1 -0
  86. nonebot_plugin_picmenu_next/templates/pw_utils.py +82 -0
  87. nonebot_plugin_picmenu_next/utils.py +18 -0
  88. nonebot_plugin_picmenu_next-0.1.0.dist-info/METADATA +225 -0
  89. nonebot_plugin_picmenu_next-0.1.0.dist-info/RECORD +92 -0
  90. nonebot_plugin_picmenu_next-0.1.0.dist-info/WHEEL +4 -0
  91. nonebot_plugin_picmenu_next-0.1.0.dist-info/entry_points.txt +4 -0
  92. 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