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.
Files changed (29) hide show
  1. nonebot_plugin_picstatus_re/__init__.py +67 -0
  2. nonebot_plugin_picstatus_re/__main__.py +57 -0
  3. nonebot_plugin_picstatus_re/bg_provider.py +218 -0
  4. nonebot_plugin_picstatus_re/collectors/__init__.py +226 -0
  5. nonebot_plugin_picstatus_re/collectors/bot.py +93 -0
  6. nonebot_plugin_picstatus_re/collectors/cpu.py +63 -0
  7. nonebot_plugin_picstatus_re/collectors/disk.py +127 -0
  8. nonebot_plugin_picstatus_re/collectors/mem.py +30 -0
  9. nonebot_plugin_picstatus_re/collectors/misc.py +130 -0
  10. nonebot_plugin_picstatus_re/collectors/network.py +123 -0
  11. nonebot_plugin_picstatus_re/collectors/process.py +61 -0
  12. nonebot_plugin_picstatus_re/config.py +117 -0
  13. nonebot_plugin_picstatus_re/misc_statistics.py +153 -0
  14. nonebot_plugin_picstatus_re/res/assets/default_avatar.webp +0 -0
  15. nonebot_plugin_picstatus_re/res/assets/default_bg_0.webp +0 -0
  16. nonebot_plugin_picstatus_re/res/assets/default_bg_1.webp +0 -0
  17. nonebot_plugin_picstatus_re/res/assets/default_bg_2.webp +0 -0
  18. nonebot_plugin_picstatus_re/res/assets/default_bg_3.webp +0 -0
  19. nonebot_plugin_picstatus_re/templates/__init__.py +65 -0
  20. nonebot_plugin_picstatus_re/templates/default/__init__.py +166 -0
  21. nonebot_plugin_picstatus_re/templates/default/res/css/index.css +586 -0
  22. nonebot_plugin_picstatus_re/templates/default/res/templates/index.html.jinja +34 -0
  23. nonebot_plugin_picstatus_re/templates/default/res/templates/macros.html.jinja +236 -0
  24. nonebot_plugin_picstatus_re/templates/render.py +27 -0
  25. nonebot_plugin_picstatus_re/util.py +29 -0
  26. nonebot_plugin_picstatus_re-2.3.0.dist-info/METADATA +58 -0
  27. nonebot_plugin_picstatus_re-2.3.0.dist-info/RECORD +29 -0
  28. nonebot_plugin_picstatus_re-2.3.0.dist-info/WHEEL +4 -0
  29. 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
+ )