nonebot-plugin-picstatus-re 2.3.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_picstatus_re/__init__.py +67 -0
- nonebot_plugin_picstatus_re/__main__.py +57 -0
- nonebot_plugin_picstatus_re/bg_provider.py +218 -0
- nonebot_plugin_picstatus_re/collectors/__init__.py +226 -0
- nonebot_plugin_picstatus_re/collectors/bot.py +93 -0
- nonebot_plugin_picstatus_re/collectors/cpu.py +63 -0
- nonebot_plugin_picstatus_re/collectors/disk.py +127 -0
- nonebot_plugin_picstatus_re/collectors/mem.py +30 -0
- nonebot_plugin_picstatus_re/collectors/misc.py +130 -0
- nonebot_plugin_picstatus_re/collectors/network.py +123 -0
- nonebot_plugin_picstatus_re/collectors/process.py +61 -0
- nonebot_plugin_picstatus_re/config.py +117 -0
- nonebot_plugin_picstatus_re/misc_statistics.py +153 -0
- nonebot_plugin_picstatus_re/res/assets/default_avatar.webp +0 -0
- nonebot_plugin_picstatus_re/res/assets/default_bg_0.webp +0 -0
- nonebot_plugin_picstatus_re/res/assets/default_bg_1.webp +0 -0
- nonebot_plugin_picstatus_re/res/assets/default_bg_2.webp +0 -0
- nonebot_plugin_picstatus_re/res/assets/default_bg_3.webp +0 -0
- nonebot_plugin_picstatus_re/templates/__init__.py +65 -0
- nonebot_plugin_picstatus_re/templates/default/__init__.py +166 -0
- nonebot_plugin_picstatus_re/templates/default/res/css/index.css +586 -0
- nonebot_plugin_picstatus_re/templates/default/res/templates/index.html.jinja +34 -0
- nonebot_plugin_picstatus_re/templates/default/res/templates/macros.html.jinja +236 -0
- nonebot_plugin_picstatus_re/templates/render.py +27 -0
- nonebot_plugin_picstatus_re/util.py +29 -0
- nonebot_plugin_picstatus_re-2.3.0.dist-info/METADATA +58 -0
- nonebot_plugin_picstatus_re-2.3.0.dist-info/RECORD +29 -0
- nonebot_plugin_picstatus_re-2.3.0.dist-info/WHEEL +4 -0
- nonebot_plugin_picstatus_re-2.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# ruff: noqa: E402
|
|
2
|
+
|
|
3
|
+
from nonebot import get_driver, require
|
|
4
|
+
from nonebot.plugin import PluginMetadata, inherit_supported_adapters
|
|
5
|
+
|
|
6
|
+
require("nonebot_plugin_apscheduler")
|
|
7
|
+
require("nonebot_plugin_alconna")
|
|
8
|
+
require("nonebot_plugin_uninfo")
|
|
9
|
+
require("nonebot_plugin_localstore")
|
|
10
|
+
|
|
11
|
+
from . import __main__ as __main__, misc_statistics as misc_statistics
|
|
12
|
+
from .bg_provider import bg_preloader
|
|
13
|
+
from .collectors import (
|
|
14
|
+
enable_collectors,
|
|
15
|
+
load_builtin_collectors,
|
|
16
|
+
registered_collectors,
|
|
17
|
+
)
|
|
18
|
+
from .config import ConfigModel, config
|
|
19
|
+
from .templates import load_builtin_templates, loaded_templates
|
|
20
|
+
|
|
21
|
+
driver = get_driver()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# lazy load builtin templates and collectors
|
|
25
|
+
@driver.on_startup
|
|
26
|
+
async def _():
|
|
27
|
+
if config.ps_template not in loaded_templates:
|
|
28
|
+
load_builtin_templates()
|
|
29
|
+
current_template = loaded_templates.get(config.ps_template)
|
|
30
|
+
if current_template is None:
|
|
31
|
+
raise ValueError(f"Template {config.ps_template} not found")
|
|
32
|
+
|
|
33
|
+
if (current_template.collectors is None) or any(
|
|
34
|
+
(x not in registered_collectors) for x in current_template.collectors
|
|
35
|
+
):
|
|
36
|
+
load_builtin_collectors()
|
|
37
|
+
|
|
38
|
+
collectors = (
|
|
39
|
+
set(registered_collectors)
|
|
40
|
+
if current_template.collectors is None
|
|
41
|
+
else current_template.collectors
|
|
42
|
+
)
|
|
43
|
+
await enable_collectors(*collectors)
|
|
44
|
+
|
|
45
|
+
bg_preloader.start_preload()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
usage = f"指令:{' / '.join(config.ps_command)}"
|
|
49
|
+
if config.ps_need_at:
|
|
50
|
+
usage += "\n注意:使用指令时需要@机器人"
|
|
51
|
+
if config.ps_only_su:
|
|
52
|
+
usage += "\n注意:仅SuperUser可以使用此指令"
|
|
53
|
+
|
|
54
|
+
__version__ = "2.3.0"
|
|
55
|
+
__plugin_meta__ = PluginMetadata(
|
|
56
|
+
name="PicStatus-Re",
|
|
57
|
+
description="以图片形式显示当前设备的运行状态",
|
|
58
|
+
usage=usage,
|
|
59
|
+
type="application",
|
|
60
|
+
homepage="https://github.com/yuexps/nonebot-plugin-picstatus-re",
|
|
61
|
+
config=ConfigModel,
|
|
62
|
+
supported_adapters=inherit_supported_adapters(
|
|
63
|
+
"nonebot_plugin_alconna",
|
|
64
|
+
"nonebot_plugin_uninfo",
|
|
65
|
+
),
|
|
66
|
+
extra={"License": "MIT", "Author": "yuexps"},
|
|
67
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from nonebot import logger, on_command
|
|
4
|
+
from nonebot.adapters import Bot as BaseBot, Event as BaseEvent, Message as BaseMessage
|
|
5
|
+
from nonebot.matcher import current_bot, current_event, current_matcher
|
|
6
|
+
from nonebot.params import CommandArg
|
|
7
|
+
from nonebot.permission import SUPERUSER
|
|
8
|
+
from nonebot.rule import Rule, to_me
|
|
9
|
+
from nonebot.typing import T_State
|
|
10
|
+
from nonebot_plugin_alconna.uniseg import UniMessage
|
|
11
|
+
|
|
12
|
+
from .bg_provider import bg_preloader
|
|
13
|
+
from .collectors import collect_all
|
|
14
|
+
from .config import config
|
|
15
|
+
from .misc_statistics import bot_avatar_cache, bot_info_cache, cache_bot_avatar
|
|
16
|
+
from .templates import render_current_template
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def check_empty_arg_rule(arg: BaseMessage = CommandArg()):
|
|
20
|
+
return not arg.extract_plain_text()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def trigger_rule():
|
|
24
|
+
rule = Rule(check_empty_arg_rule)
|
|
25
|
+
if config.ps_need_at:
|
|
26
|
+
rule &= to_me()
|
|
27
|
+
return rule
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_cmd, *_alias = config.ps_command
|
|
31
|
+
stat_matcher = on_command(
|
|
32
|
+
_cmd,
|
|
33
|
+
aliases=set(_alias),
|
|
34
|
+
rule=trigger_rule(),
|
|
35
|
+
permission=SUPERUSER if config.ps_only_su else None,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@stat_matcher.handle()
|
|
40
|
+
async def _(bot: BaseBot, event: BaseEvent, state: T_State):
|
|
41
|
+
if (
|
|
42
|
+
(bot.self_id not in bot_avatar_cache)
|
|
43
|
+
and (info := bot_info_cache.get(bot.self_id))
|
|
44
|
+
and info.avatar
|
|
45
|
+
):
|
|
46
|
+
await cache_bot_avatar(info.avatar, bot, event, state)
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
bg, collected = await asyncio.gather(bg_preloader.get(), collect_all())
|
|
50
|
+
ret = await render_current_template(collected=collected, bg=bg)
|
|
51
|
+
except Exception:
|
|
52
|
+
logger.exception("获取运行状态图失败")
|
|
53
|
+
await UniMessage("获取运行状态图片失败,请检查后台输出").send(
|
|
54
|
+
reply_to=config.ps_reply_target,
|
|
55
|
+
)
|
|
56
|
+
else:
|
|
57
|
+
await UniMessage.image(raw=ret).send(reply_to=config.ps_reply_target)
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import asyncio as aio
|
|
2
|
+
import mimetypes
|
|
3
|
+
import random
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from collections.abc import AsyncIterable
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import NamedTuple, TypeAlias
|
|
9
|
+
|
|
10
|
+
from cookit.common import race
|
|
11
|
+
from cookit.loguru import warning_suppress
|
|
12
|
+
from nonebot import get_driver, logger
|
|
13
|
+
|
|
14
|
+
from .config import BG_PRELOAD_CACHE_DIR, DEFAULT_BG_PATH, ASSETS_PATH, config
|
|
15
|
+
|
|
16
|
+
if sys.version_info >= (3, 11):
|
|
17
|
+
from asyncio.taskgroups import TaskGroup
|
|
18
|
+
else:
|
|
19
|
+
from taskgroup import TaskGroup
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BgBytesData(NamedTuple):
|
|
23
|
+
data: bytes | None
|
|
24
|
+
mime: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BgFileData(NamedTuple):
|
|
28
|
+
path: Path | None
|
|
29
|
+
mime: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
BgData: TypeAlias = BgBytesData | BgFileData
|
|
33
|
+
DEFAULT_MIME = "application/octet-stream"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_bg_files() -> list["Path"]:
|
|
37
|
+
if not config.ps_bg_local_path.exists():
|
|
38
|
+
logger.warning("Custom background path does not exist, fallback to default")
|
|
39
|
+
return [DEFAULT_BG_PATH]
|
|
40
|
+
if config.ps_bg_local_path.is_file():
|
|
41
|
+
return [config.ps_bg_local_path]
|
|
42
|
+
|
|
43
|
+
if config.ps_bg_local_path == ASSETS_PATH:
|
|
44
|
+
files = [x for x in config.ps_bg_local_path.glob("default_bg_*.webp") if x.is_file()]
|
|
45
|
+
else:
|
|
46
|
+
files = [
|
|
47
|
+
x for x in config.ps_bg_local_path.glob("*")
|
|
48
|
+
if x.is_file() and x.name != "default_avatar.webp"
|
|
49
|
+
]
|
|
50
|
+
if not files:
|
|
51
|
+
logger.warning("Custom background dir has no file in it, fallback to default")
|
|
52
|
+
return [DEFAULT_BG_PATH]
|
|
53
|
+
return files
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
BG_FILES = get_bg_files()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def refresh_bg_files():
|
|
60
|
+
global BG_FILES
|
|
61
|
+
BG_FILES = get_bg_files()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def local(num: int) -> AsyncIterable[BgData]:
|
|
65
|
+
files = random.sample(BG_FILES, num)
|
|
66
|
+
for x in files:
|
|
67
|
+
yield BgFileData(
|
|
68
|
+
x,
|
|
69
|
+
mimetypes.guess_type(x)[0] or DEFAULT_MIME,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def none(num: int) -> AsyncIterable[BgData]:
|
|
74
|
+
for _ in range(num):
|
|
75
|
+
yield BgBytesData(None, DEFAULT_MIME)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def fetch_bg(num: int) -> AsyncIterable[BgData]:
|
|
79
|
+
provider = none if config.ps_bg_provider == "none" else local
|
|
80
|
+
async for x in provider(num):
|
|
81
|
+
yield x
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def cache_bg(bg: BgBytesData):
|
|
85
|
+
if not bg.data:
|
|
86
|
+
return BgFileData(None, bg.mime)
|
|
87
|
+
BG_PRELOAD_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
path = BG_PRELOAD_CACHE_DIR / f"{time.time_ns()}.{bg.mime.split('/')[-1]}"
|
|
89
|
+
path.write_bytes(bg.data)
|
|
90
|
+
return BgFileData(path, bg.mime)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def read_cached_bg_file(bg: BgFileData) -> BgBytesData | None:
|
|
94
|
+
if not bg.path:
|
|
95
|
+
return BgBytesData(None, bg.mime)
|
|
96
|
+
with warning_suppress("Failed to read cached file"):
|
|
97
|
+
data = bg.path.read_bytes()
|
|
98
|
+
if bg.path.is_relative_to(BG_PRELOAD_CACHE_DIR):
|
|
99
|
+
with warning_suppress("Failed to unlink cached file"):
|
|
100
|
+
bg.path.unlink()
|
|
101
|
+
return BgBytesData(data, bg.mime)
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def get_one_fallback() -> BgBytesData:
|
|
106
|
+
with warning_suppress("Failed to get local bg file, fallback to none"):
|
|
107
|
+
async for x in local(1):
|
|
108
|
+
if bg := read_cached_bg_file(x):
|
|
109
|
+
return bg
|
|
110
|
+
logger.warning("Failed to read local bg file, fallback to none")
|
|
111
|
+
return BgBytesData(None, DEFAULT_MIME)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class BgPreloader:
|
|
115
|
+
def __init__(self, preload_count: int):
|
|
116
|
+
self.preload_count = preload_count
|
|
117
|
+
self.background_queue = aio.Queue[BgData]()
|
|
118
|
+
self.current_load_task_main: aio.Task | None = None
|
|
119
|
+
self.consumed_in_loading: bool = False
|
|
120
|
+
self.image_got_signal = aio.Event()
|
|
121
|
+
self.fire_tasks: set[aio.Task] = set()
|
|
122
|
+
|
|
123
|
+
async def preload_task(
|
|
124
|
+
self,
|
|
125
|
+
count: int,
|
|
126
|
+
fire: bool = False,
|
|
127
|
+
fire_done_signal: aio.Event | None = None,
|
|
128
|
+
):
|
|
129
|
+
logger.debug(f"Preload task started, will preload {count} images, {fire=}")
|
|
130
|
+
try:
|
|
131
|
+
async for x in fetch_bg(count):
|
|
132
|
+
logger.debug("Got one image")
|
|
133
|
+
if self.preload_count > 0 or (
|
|
134
|
+
fire_done_signal and fire_done_signal.is_set()
|
|
135
|
+
):
|
|
136
|
+
x = cache_bg(x) if isinstance(x, BgBytesData) else x
|
|
137
|
+
await self.background_queue.put(x)
|
|
138
|
+
self.image_got_signal.set()
|
|
139
|
+
self.image_got_signal.clear()
|
|
140
|
+
except Exception:
|
|
141
|
+
logger.exception("Unexpected error occurred in preload task")
|
|
142
|
+
else:
|
|
143
|
+
logger.debug("Preload task finished")
|
|
144
|
+
|
|
145
|
+
if fire:
|
|
146
|
+
return
|
|
147
|
+
if (
|
|
148
|
+
self.consumed_in_loading
|
|
149
|
+
or self.background_queue.qsize() < self.preload_count
|
|
150
|
+
):
|
|
151
|
+
self.consumed_in_loading = False
|
|
152
|
+
self.start_preload()
|
|
153
|
+
else:
|
|
154
|
+
self.current_load_task_main = None
|
|
155
|
+
|
|
156
|
+
def start_preload(self, force: bool = False):
|
|
157
|
+
count = self.preload_count - self.background_queue.qsize()
|
|
158
|
+
if count <= 0 and not force:
|
|
159
|
+
logger.debug(
|
|
160
|
+
"Current background queue size meets preload count, skip preload",
|
|
161
|
+
)
|
|
162
|
+
return
|
|
163
|
+
task = aio.create_task(self.preload_task(count))
|
|
164
|
+
self.current_load_task_main = task
|
|
165
|
+
|
|
166
|
+
def set_defer_preload(self):
|
|
167
|
+
if self.current_load_task_main:
|
|
168
|
+
logger.debug("Main preload task already running, set flag")
|
|
169
|
+
self.consumed_in_loading = True
|
|
170
|
+
else:
|
|
171
|
+
self.start_preload()
|
|
172
|
+
|
|
173
|
+
async def _get_on_fire(self) -> BgBytesData:
|
|
174
|
+
task_done_signal = aio.Event()
|
|
175
|
+
fire_task = aio.create_task(
|
|
176
|
+
self.preload_task(1, fire=True, fire_done_signal=task_done_signal),
|
|
177
|
+
)
|
|
178
|
+
fire_task.add_done_callback(lambda _: task_done_signal.set())
|
|
179
|
+
fire_task.add_done_callback(lambda _: self.fire_tasks.discard(fire_task))
|
|
180
|
+
self.fire_tasks.add(fire_task)
|
|
181
|
+
try:
|
|
182
|
+
await race(
|
|
183
|
+
task_done_signal.wait(),
|
|
184
|
+
aio.sleep(15),
|
|
185
|
+
)
|
|
186
|
+
finally:
|
|
187
|
+
task_done_signal.set()
|
|
188
|
+
|
|
189
|
+
if not self.background_queue.empty():
|
|
190
|
+
bg = await self.background_queue.get()
|
|
191
|
+
self.set_defer_preload()
|
|
192
|
+
if (not isinstance(bg, BgFileData)) or (bg := read_cached_bg_file(bg)):
|
|
193
|
+
return bg
|
|
194
|
+
|
|
195
|
+
logger.error("Unable to get an background image, falling back to local")
|
|
196
|
+
return await get_one_fallback()
|
|
197
|
+
|
|
198
|
+
async def get(self) -> BgBytesData:
|
|
199
|
+
self.set_defer_preload()
|
|
200
|
+
|
|
201
|
+
while not self.background_queue.empty():
|
|
202
|
+
bg = await self.background_queue.get()
|
|
203
|
+
self.set_defer_preload()
|
|
204
|
+
if (not isinstance(bg, BgFileData)) or (bg := read_cached_bg_file(bg)):
|
|
205
|
+
return bg
|
|
206
|
+
|
|
207
|
+
return await self._get_on_fire()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
bg_preloader = BgPreloader(config.ps_bg_preload_count)
|
|
211
|
+
|
|
212
|
+
driver = get_driver()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@driver.on_shutdown
|
|
216
|
+
async def _():
|
|
217
|
+
for t in bg_preloader.fire_tasks:
|
|
218
|
+
t.cancel()
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import importlib
|
|
3
|
+
import time
|
|
4
|
+
from abc import abstractmethod
|
|
5
|
+
from collections import deque
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from contextlib import suppress
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Generic, TypeVar
|
|
10
|
+
from typing_extensions import override
|
|
11
|
+
|
|
12
|
+
from nonebot import logger
|
|
13
|
+
from nonebot_plugin_apscheduler import scheduler
|
|
14
|
+
|
|
15
|
+
from ..config import config
|
|
16
|
+
|
|
17
|
+
T = TypeVar("T")
|
|
18
|
+
TI = TypeVar("TI")
|
|
19
|
+
TR = TypeVar("TR")
|
|
20
|
+
TC = TypeVar("TC", bound="Collector")
|
|
21
|
+
TCF = TypeVar("TCF", bound=Callable[[], Awaitable[Any]])
|
|
22
|
+
R = TypeVar("R")
|
|
23
|
+
|
|
24
|
+
Undefined = type("Undefined", (), {})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SkipCollectError(Exception):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Collector(Generic[TI, TR]):
|
|
32
|
+
@abstractmethod
|
|
33
|
+
async def _get(self) -> TI: ...
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
async def get(self) -> TR: ...
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class BaseNormalCollector(Collector[T, T], Generic[T]):
|
|
40
|
+
def __init__(self) -> None:
|
|
41
|
+
super().__init__()
|
|
42
|
+
|
|
43
|
+
@override
|
|
44
|
+
async def get(self) -> T:
|
|
45
|
+
return await self._get()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BaseFirstTimeCollector(Collector[T, T], Generic[T]):
|
|
49
|
+
def __init__(self) -> None:
|
|
50
|
+
super().__init__()
|
|
51
|
+
self._cached: T | Undefined = Undefined()
|
|
52
|
+
|
|
53
|
+
@override
|
|
54
|
+
async def get(self) -> T:
|
|
55
|
+
if not isinstance(self._cached, Undefined):
|
|
56
|
+
return self._cached
|
|
57
|
+
data = await self._get()
|
|
58
|
+
self._cached = data
|
|
59
|
+
return data
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class BasePeriodicCollector(Collector[T, deque[T]], Generic[T]):
|
|
63
|
+
def __init__(self, size: int = config.ps_default_collect_cache_size) -> None:
|
|
64
|
+
super().__init__()
|
|
65
|
+
self.data = deque(maxlen=size)
|
|
66
|
+
|
|
67
|
+
@override
|
|
68
|
+
async def get(self) -> deque[T]:
|
|
69
|
+
return self.data
|
|
70
|
+
|
|
71
|
+
async def collect(self):
|
|
72
|
+
try:
|
|
73
|
+
data = await self._get()
|
|
74
|
+
except SkipCollectError:
|
|
75
|
+
return
|
|
76
|
+
except Exception:
|
|
77
|
+
logger.exception("Error occurred while collecting data")
|
|
78
|
+
else:
|
|
79
|
+
self.data.append(data)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
registered_collectors: dict[str, type[Collector]] = {}
|
|
83
|
+
enabled_collectors: dict[str, Collector] = {}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def collector(name: str):
|
|
87
|
+
def deco(cls: type[TC]) -> type[TC]:
|
|
88
|
+
if name in registered_collectors:
|
|
89
|
+
raise ValueError(f"Collector {name} already exists")
|
|
90
|
+
registered_collectors[name] = cls
|
|
91
|
+
logger.debug(f"Registered collector {name}")
|
|
92
|
+
return cls
|
|
93
|
+
|
|
94
|
+
return deco
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _enable_collector(name: str):
|
|
98
|
+
if name not in registered_collectors:
|
|
99
|
+
raise ValueError(f"Collector {name} not found")
|
|
100
|
+
cls = registered_collectors[name]
|
|
101
|
+
if issubclass(cls, BasePeriodicCollector) and name in config.ps_collect_cache_size:
|
|
102
|
+
instance = cls(size=config.ps_collect_cache_size[name])
|
|
103
|
+
else:
|
|
104
|
+
instance = cls()
|
|
105
|
+
enabled_collectors[name] = instance
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def enable_collectors(*names: str):
|
|
109
|
+
for name in names:
|
|
110
|
+
_enable_collector(name)
|
|
111
|
+
await init_first_time_collectors()
|
|
112
|
+
await setup_periodic_collectors_update_job()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def functional_collector(cls: type[Collector], name: str | None = None):
|
|
116
|
+
def deco(func: TCF) -> TCF:
|
|
117
|
+
collector_name = name or func.__name__
|
|
118
|
+
if not collector_name:
|
|
119
|
+
raise ValueError("name must be provided")
|
|
120
|
+
|
|
121
|
+
class Collector(cls):
|
|
122
|
+
async def _get(self) -> Any:
|
|
123
|
+
return await func()
|
|
124
|
+
|
|
125
|
+
collector(collector_name)(Collector)
|
|
126
|
+
return func
|
|
127
|
+
|
|
128
|
+
return deco
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def normal_collector(name: str | None = None):
|
|
132
|
+
return functional_collector(BaseNormalCollector, name)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def first_time_collector(name: str | None = None):
|
|
136
|
+
return functional_collector(BaseFirstTimeCollector, name)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def periodic_collector(name: str | None = None):
|
|
140
|
+
return functional_collector(BasePeriodicCollector, name)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class BaseTimeBasedCounterCollector(Collector[R, Any], Generic[T, R]):
|
|
144
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
145
|
+
super().__init__(*args, **kwargs)
|
|
146
|
+
self.last_obj: Undefined | T = Undefined()
|
|
147
|
+
self.last_time: float = 0
|
|
148
|
+
self.normal_delay: float = 1
|
|
149
|
+
|
|
150
|
+
@abstractmethod
|
|
151
|
+
async def _calc(self, past: T, now: T, time_passed: float) -> R: ...
|
|
152
|
+
|
|
153
|
+
@abstractmethod
|
|
154
|
+
async def _get_obj(self) -> T: ...
|
|
155
|
+
|
|
156
|
+
@override
|
|
157
|
+
async def _get(self) -> R:
|
|
158
|
+
past = self.last_obj
|
|
159
|
+
past_time = self.last_time
|
|
160
|
+
time_now = time.time()
|
|
161
|
+
time_passed = time_now - past_time
|
|
162
|
+
|
|
163
|
+
self.last_time = time_now
|
|
164
|
+
self.last_obj = await self._get_obj()
|
|
165
|
+
if isinstance(past, Undefined):
|
|
166
|
+
raise SkipCollectError
|
|
167
|
+
return await self._calc(past, self.last_obj, time_passed)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class NormalTimeBasedCounterCollector(
|
|
171
|
+
BaseTimeBasedCounterCollector[T, R],
|
|
172
|
+
BaseNormalCollector[R],
|
|
173
|
+
Generic[T, R],
|
|
174
|
+
):
|
|
175
|
+
@override
|
|
176
|
+
async def get(self) -> R:
|
|
177
|
+
with suppress(SkipCollectError):
|
|
178
|
+
await self._get()
|
|
179
|
+
await asyncio.sleep(self.normal_delay)
|
|
180
|
+
return await self._get()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class PeriodicTimeBasedCounterCollector(
|
|
184
|
+
BaseTimeBasedCounterCollector[T, R],
|
|
185
|
+
BasePeriodicCollector[R],
|
|
186
|
+
Generic[T, R],
|
|
187
|
+
): ...
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def collect_all() -> dict[str, Any]:
|
|
191
|
+
async def get(name: str):
|
|
192
|
+
return name, await enabled_collectors[name].get()
|
|
193
|
+
|
|
194
|
+
res = await asyncio.gather(*(get(name) for name in enabled_collectors))
|
|
195
|
+
return dict(res)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def load_builtin_collectors():
|
|
199
|
+
for module in Path(__file__).parent.iterdir():
|
|
200
|
+
if not module.name.startswith("_"):
|
|
201
|
+
importlib.import_module(f".{module.stem}", __package__)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
async def init_first_time_collectors():
|
|
205
|
+
await asyncio.gather(
|
|
206
|
+
*(
|
|
207
|
+
x.get()
|
|
208
|
+
for x in enabled_collectors.values()
|
|
209
|
+
if isinstance(x, BaseFirstTimeCollector)
|
|
210
|
+
),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
async def setup_periodic_collectors_update_job():
|
|
215
|
+
collectors = [
|
|
216
|
+
x for x in enabled_collectors.values() if isinstance(x, BasePeriodicCollector)
|
|
217
|
+
]
|
|
218
|
+
if not collectors:
|
|
219
|
+
return
|
|
220
|
+
logger.debug("Setting up periodic collectors")
|
|
221
|
+
|
|
222
|
+
@scheduler.scheduled_job("interval", seconds=config.ps_collect_interval)
|
|
223
|
+
async def _do():
|
|
224
|
+
await asyncio.gather(*(x.collect() for x in collectors))
|
|
225
|
+
|
|
226
|
+
await _do()
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from nonebot import get_bots, logger
|
|
7
|
+
from nonebot.matcher import current_bot
|
|
8
|
+
|
|
9
|
+
from ..config import config
|
|
10
|
+
from ..misc_statistics import bot_connect_time, bot_info_cache, recv_num, send_num
|
|
11
|
+
from ..util import format_timedelta
|
|
12
|
+
from . import normal_collector
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from nonebot.adapters import Bot as BaseBot
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from nonebot.adapters.onebot.v11 import Bot as OBV11Bot
|
|
19
|
+
except ImportError:
|
|
20
|
+
OBV11Bot = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class BotStatus:
|
|
25
|
+
self_id: str
|
|
26
|
+
adapter: str
|
|
27
|
+
nick: str
|
|
28
|
+
bot_connected: str
|
|
29
|
+
msg_rec: str
|
|
30
|
+
msg_sent: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def get_ob11_msg_num(bot: "BaseBot") -> tuple[int | None, int | None]:
|
|
34
|
+
if not (config.ps_ob_v11_use_get_status and OBV11Bot and isinstance(bot, OBV11Bot)):
|
|
35
|
+
return None, None
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
bot_stat = (await bot.get_status()).get("stat")
|
|
39
|
+
except Exception as e:
|
|
40
|
+
logger.warning(
|
|
41
|
+
f"Error when getting bot status: {e.__class__.__name__}: {e}",
|
|
42
|
+
)
|
|
43
|
+
return None, None
|
|
44
|
+
if not bot_stat:
|
|
45
|
+
return None, None
|
|
46
|
+
|
|
47
|
+
msg_rec = bot_stat.get("message_received") or bot_stat.get(
|
|
48
|
+
"MessageReceived",
|
|
49
|
+
)
|
|
50
|
+
msg_sent = bot_stat.get("message_sent") or bot_stat.get("MessageSent")
|
|
51
|
+
return msg_rec, msg_sent
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def get_bot_status(bot: "BaseBot", now_time: datetime) -> BotStatus:
|
|
55
|
+
nick = (
|
|
56
|
+
((info := bot_info_cache[bot.self_id]).nick or info.name or info.id)
|
|
57
|
+
if (not config.ps_use_env_nick) and (bot.self_id in bot_info_cache)
|
|
58
|
+
else next(iter(config.nickname), None)
|
|
59
|
+
) or "Bot"
|
|
60
|
+
bot_connected = (
|
|
61
|
+
format_timedelta(now_time - t)
|
|
62
|
+
if (t := bot_connect_time.get(bot.self_id))
|
|
63
|
+
else "未知"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
msg_rec, msg_sent = await get_ob11_msg_num(bot)
|
|
67
|
+
if msg_rec is None:
|
|
68
|
+
msg_rec = recv_num.get(bot.self_id)
|
|
69
|
+
if msg_sent is None:
|
|
70
|
+
msg_sent = send_num.get(bot.self_id)
|
|
71
|
+
msg_rec = "未知" if (msg_rec is None) else str(msg_rec)
|
|
72
|
+
msg_sent = "未知" if (msg_sent is None) else str(msg_sent)
|
|
73
|
+
|
|
74
|
+
return BotStatus(
|
|
75
|
+
self_id=bot.self_id,
|
|
76
|
+
adapter=bot.adapter.get_name(),
|
|
77
|
+
nick=nick,
|
|
78
|
+
bot_connected=bot_connected,
|
|
79
|
+
msg_rec=msg_rec,
|
|
80
|
+
msg_sent=msg_sent,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@normal_collector()
|
|
85
|
+
async def bots() -> list[BotStatus]:
|
|
86
|
+
now_time = datetime.now().astimezone()
|
|
87
|
+
return (
|
|
88
|
+
[await get_bot_status(current_bot.get(), now_time)]
|
|
89
|
+
if config.ps_show_current_bot_only
|
|
90
|
+
else await asyncio.gather(
|
|
91
|
+
*(get_bot_status(bot, now_time) for bot in get_bots().values()),
|
|
92
|
+
)
|
|
93
|
+
)
|