nonebot-plugin-picstatus-re 2.3.0__tar.gz

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 (32) hide show
  1. nonebot_plugin_picstatus_re-2.3.0/.env.example +187 -0
  2. nonebot_plugin_picstatus_re-2.3.0/.github/workflows/pypi-publish.yml +34 -0
  3. nonebot_plugin_picstatus_re-2.3.0/.gitignore +9 -0
  4. nonebot_plugin_picstatus_re-2.3.0/LICENSE +21 -0
  5. nonebot_plugin_picstatus_re-2.3.0/PKG-INFO +58 -0
  6. nonebot_plugin_picstatus_re-2.3.0/README.md +33 -0
  7. nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/__init__.py +67 -0
  8. nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/__main__.py +57 -0
  9. nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/bg_provider.py +218 -0
  10. nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/collectors/__init__.py +226 -0
  11. nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/collectors/bot.py +93 -0
  12. nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/collectors/cpu.py +63 -0
  13. nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/collectors/disk.py +127 -0
  14. nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/collectors/mem.py +30 -0
  15. nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/collectors/misc.py +130 -0
  16. nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/collectors/network.py +123 -0
  17. nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/collectors/process.py +61 -0
  18. nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/config.py +117 -0
  19. nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/misc_statistics.py +153 -0
  20. nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/res/assets/default_avatar.webp +0 -0
  21. nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/res/assets/default_bg_0.webp +0 -0
  22. nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/res/assets/default_bg_1.webp +0 -0
  23. nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/res/assets/default_bg_2.webp +0 -0
  24. nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/res/assets/default_bg_3.webp +0 -0
  25. nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/templates/__init__.py +65 -0
  26. nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/templates/default/__init__.py +166 -0
  27. nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/templates/default/res/css/index.css +586 -0
  28. nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/templates/default/res/templates/index.html.jinja +34 -0
  29. nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/templates/default/res/templates/macros.html.jinja +236 -0
  30. nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/templates/render.py +27 -0
  31. nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/util.py +29 -0
  32. nonebot_plugin_picstatus_re-2.3.0/pyproject.toml +34 -0
@@ -0,0 +1,187 @@
1
+ # ####################
2
+ # 以下配置项【均为可选】,请按需添加
3
+ # 除非特别说明,示例配置中的值均为插件该配置项的默认值
4
+ # ####################
5
+
6
+ # ====================
7
+ # 全局设置
8
+
9
+ # 用来测试访问网址时的代理(默认为空)
10
+ PROXY="http://127.0.0.1:7890"
11
+
12
+ # ====================
13
+ # 行为设置
14
+
15
+ # 要使用的图片模板
16
+ # 目前只有 default 可用
17
+ # 关于模板的特定配置请见最下方
18
+ PS_TEMPLATE=default
19
+
20
+ # 触发插件功能的指令列表
21
+ PS_COMMAND=["状态","status"]
22
+
23
+ # 是否只能由 SUPERUSER 触发指令
24
+ PS_ONLY_SU=False
25
+
26
+ # 触发指令是否需要 @Bot
27
+ PS_NEED_AT=False
28
+
29
+ # 是否回复目标用户
30
+ # 使用 QQ 官方机器人时需要关闭此项(官方尚未支持回复),否则可能导致报错!
31
+ PS_REPLY_TARGET=True
32
+
33
+ # 请求头像等其他 URL 时的超时时间(秒)
34
+ PS_REQ_TIMEOUT=10
35
+
36
+ # ====================
37
+ # 全局个性化设置
38
+
39
+ # 图片背景图来源
40
+ # 图片来源列表:
41
+ # - "local": 本地图片
42
+ # - "none": 无背景图
43
+ PS_BG_PROVIDER=local
44
+
45
+ # 背景图预载数量(最低可填 0)
46
+ PS_BG_PRELOAD_COUNT=2
47
+
48
+ # 本地背景图来源 ("local") 使用的图片文件 / 文件夹路径(默认为插件自带背景图)
49
+ # 如路径不存在,会 fallback 到插件自带默认背景图
50
+ PS_BG_LOCAL_PATH=
51
+
52
+ # 当获取 Bot 头像失败时使用的默认头像路径(默认为插件自带头像)
53
+ PS_DEFAULT_AVATAR=
54
+
55
+ # ====================
56
+ # 数据收集设置
57
+
58
+ # == 基础设置 ==
59
+
60
+ # PeriodicCollector 的调用间隔,单位秒
61
+ PS_COLLECT_INTERVAL=5
62
+
63
+ # PeriodicCollector 中 deque 的默认大小
64
+ PS_DEFAULT_COLLECT_CACHE_SIZE=1
65
+
66
+ # 设置特定 PeriodicCollector 中 deque 的大小,{ [name: string]: number },
67
+ PS_COLLECT_CACHE_SIZE={}
68
+
69
+ # == header ==
70
+
71
+ # 使用 .env 中配置的 NICKNAME 作为图片上的 Bot 昵称
72
+ PS_USE_ENV_NICK=False
73
+
74
+ # 仅显示当前 Bot
75
+ PS_SHOW_CURRENT_BOT_ONLY=False
76
+
77
+ # 是否对适配器为 OneBot V11 的 Bot 调用 get_status 获取收发消息数
78
+ PS_OB_V11_USE_GET_STATUS=True
79
+
80
+ # 是否使用 message_sent 事件(OneBot V11),或 user_id 为自身的消息事件统计发送消息数
81
+ # 为 False 时全局禁用,为 True 时全局启用,
82
+ # 为适配器名称列表(如 ["OneBot V11", "Telegram"])仅对指定的适配器启用
83
+ PS_COUNT_MESSAGE_SENT_EVENT=False
84
+
85
+ # 是否在 Bot 断开链接时清空收发消息计数
86
+ PS_DISCONNECT_RESET_COUNTER=True
87
+
88
+ # == disk ==
89
+
90
+ # 分区列表里忽略的盘符(挂载点)
91
+ # 使用正则表达式匹配
92
+ # 由于配置项使用JSON解析,所以需要使用双反斜杠转义,
93
+ # 如:"sda\\d" 解析为 sda\d(代表 sda<一位阿拉伯数字>);
94
+ # "C:\\\\Windows" 解析为 C:\\Windows(代表 C:\Windows)
95
+ PS_IGNORE_PARTS=[]
96
+
97
+ # 忽略获取容量状态失败的磁盘分区
98
+ PS_IGNORE_BAD_PARTS=False
99
+
100
+ # 是否排序分区列表(按照已用大小比例倒序)
101
+ PS_SORT_PARTS=True
102
+
103
+ # 是否反转分区列表排序
104
+ PS_SORT_PARTS_REVERSE=False
105
+
106
+ # 磁盘 IO 统计列表中忽略的磁盘名
107
+ # 使用正则表达式匹配(注意事项同上)
108
+ PS_IGNORE_DISK_IOS=["^(loop|zram)\\d*$"]
109
+
110
+ # 是否忽略 IO 都为 0B/s 的磁盘
111
+ PS_IGNORE_NO_IO_DISK=False
112
+
113
+ # 是否排序磁盘 IO 统计列表(按照读写速度总和倒序)
114
+ PS_SORT_DISK_IOS=True
115
+
116
+ # == network ==
117
+
118
+ # 网速列表中忽略的网络名称
119
+ # 使用正则表达式匹配(注意事项同上)
120
+ PS_IGNORE_NETS=[r"^lo(op)?\\d*$|^(Loopback|本地连接)"]
121
+
122
+ # 是否忽略上下行都为 0B/s 的网卡
123
+ PS_IGNORE_0B_NET=False
124
+
125
+ # 是否排序网速列表(按照上下行速度总和倒序)
126
+ PS_SORT_NETS=True
127
+
128
+ # 需要进行测试响应速度的网址列表
129
+ # 字段说明:
130
+ # - name: 显示名称
131
+ # - url: 测试网址
132
+ # - use_proxy: 是否使用插件配置中的代理访问(可不填,默认为 false)
133
+ PS_TEST_SITES='
134
+ [
135
+ {"name": "百度", "url": "https://www.baidu.com/"},
136
+ {"name": "谷歌", "url": "https://www.google.com/", "use_proxy": true}
137
+ ]
138
+ '
139
+
140
+ # 是否将测试网址的结果排序(按照响应时间正序)
141
+ PS_SORT_SITES=True
142
+
143
+ # 网址测试访问时的超时时间(秒)
144
+ PS_TEST_TIMEOUT=3
145
+
146
+ # == process ==
147
+
148
+ # 进程列表的最大项目数量
149
+ PS_PROC_LEN=5
150
+
151
+ # 要忽略的进程名
152
+ # 使用正则表达式匹配(注意事项同上)
153
+ PS_IGNORE_PROCS=[]
154
+
155
+ # 进程列表的排序方式
156
+ # 可选:cpu、mem
157
+ PS_PROC_SORT_BY=cpu
158
+
159
+ # 是否将进程 CPU 占用率显示为类似 Windows 任务管理器的百分比(最高 100%)
160
+ # 例:当你的 CPU 总共有 4 线程时,如果该进程吃满了两个线程,
161
+ # Linux 会显示为 200%(每个线程算 100%),而 Windows 会显示为 50%(总占用率算 100%)
162
+ PS_PROC_CPU_MAX_100P=False
163
+
164
+ # ====================
165
+ # default 模板特定配置
166
+
167
+ # 图片中渲染的组件列表及其排列顺序
168
+ # 默认启用全部组件
169
+ # 组件介绍:
170
+ # - "header": 已连接的 Bot 信息、NoneBot 运行时间、系统运行时间
171
+ # - "cpu_mem": CPU、MEM、SWAP 使用率圆环图
172
+ # - "disk": 分区占用情况、磁盘 IO 情况
173
+ # - "network": 网络 IO 情况、网络响应速度测试
174
+ # - "process": 进程 CPU、MEM 占用情况
175
+ # - "footer": NoneBot 与 PicStatus 版本、当前时间、Python 实现及版本、系统名称及架构
176
+ PS_DEFAULT_COMPONENTS=["header", "cpu_mem", "disk", "network", "process", "footer"]
177
+
178
+
179
+
180
+ # 输出的图片格式
181
+ # 可选:jpeg、png
182
+ PS_DEFAULT_PIC_FORMAT=jpeg
183
+
184
+ # 是否使用后台收集器
185
+ # 如使用,插件将会在后台以指定间隔获取部分服务器信息
186
+ # 如不使用,插件仅会在指令被调用时获取全部信息
187
+ PS_DEFAULT_USE_PERIODIC=True
@@ -0,0 +1,34 @@
1
+ name: Publish Python 🐍 distributions 📦 to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ build-n-publish:
10
+ name: Use uv to Build and publish Python 🐍 distributions 📦 to PyPI
11
+ runs-on: ubuntu-latest
12
+
13
+ permissions:
14
+ # IMPORTANT: this permission is mandatory for trusted publishing
15
+ id-token: write
16
+
17
+ steps:
18
+ - name: Checkout
19
+ uses: actions/checkout@master
20
+ with:
21
+ submodules: true
22
+
23
+ - name: Install uv
24
+ uses: astral-sh/setup-uv@v5
25
+
26
+ - name: 'Set up Python'
27
+ uses: actions/setup-python@v5
28
+ with:
29
+ python-version-file: 'pyproject.toml'
30
+
31
+ - name: Build and Publish distribution 📦 to PyPI
32
+ run: |
33
+ uv build
34
+ uv publish
@@ -0,0 +1,9 @@
1
+ /.vscode/
2
+ /.idea/
3
+ /.run/
4
+ /venv/
5
+ /testnb2/
6
+ .pdm-python
7
+ build/
8
+ __pycache__
9
+ /nonebot_plugin_picstatus/res/picstatus-debug.html
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 yuexps
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.4
2
+ Name: nonebot-plugin-picstatus-re
3
+ Version: 2.3.0
4
+ Summary: A NoneBot2 plugin generates a picture which shows the status of current device
5
+ Project-URL: homepage, https://github.com/yuexps/nonebot-plugin-picstatus-re
6
+ Author-email: yuexps <yuexps@qq.com>
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Requires-Python: <4.0,>=3.10
10
+ Requires-Dist: anyio>=4.10.0
11
+ Requires-Dist: cookit[jinja,loguru,nonebot-localstore,pydantic]>=0.13.3
12
+ Requires-Dist: httpx>=0.28.1
13
+ Requires-Dist: jinja2>=3.1.6
14
+ Requires-Dist: nonebot-plugin-alconna>=0.59.4
15
+ Requires-Dist: nonebot-plugin-apscheduler>=0.5.0
16
+ Requires-Dist: nonebot-plugin-htmlkit
17
+ Requires-Dist: nonebot-plugin-localstore>=0.7.4
18
+ Requires-Dist: nonebot-plugin-uninfo>=0.9.0
19
+ Requires-Dist: nonebot2>=2.4.3
20
+ Requires-Dist: psutil>=7.0.0
21
+ Requires-Dist: py-cpuinfo>=9.0.0
22
+ Requires-Dist: taskgroup>=0.2.2; python_version < '3.11'
23
+ Requires-Dist: yarl>=1.20.1
24
+ Description-Content-Type: text/markdown
25
+
26
+ <!-- markdownlint-disable MD033 MD036 MD041 -->
27
+
28
+ <div align="center">
29
+
30
+ # NoneBot-Plugin-PicStatus-Re
31
+
32
+ 以精美的卡片图片形式展示 NoneBot2 运行设备的系统状态(CPU、内存、磁盘、网速、进程等信息),采用 [nonebot/plugin-htmlkit](https://github.com/nonebot/plugin-htmlkit) 将 HTML 渲染为图片。
33
+
34
+ </div>
35
+
36
+ ## 安装
37
+
38
+ ```bash
39
+ # 使用 nb-cli 安装(推荐)
40
+ nb plugin install nonebot-plugin-picstatus-re
41
+
42
+ # 或使用 pip 安装
43
+ pip install nonebot-plugin-picstatus-re
44
+ ```
45
+ 使用包管理器安装时,需在项目的 `pyproject.toml` 中的 `[tool.nonebot]` 部分的 `plugins` 列表中手动追加 `"nonebot_plugin_picstatus_re"`。
46
+
47
+ ## 使用与配置
48
+
49
+ - **触发指令**:`状态` / `status`(可在配置中修改)
50
+ - **参数配置**:参数均在项目的 `.env.*` 文件中配置,完整说明请参考 **[.env.example](.env.example)**。
51
+
52
+ ## 鸣谢
53
+
54
+ - [nonebot-plugin-picstatus](https://github.com/lgc-NB2Dev/nonebot-plugin-picstatus) (原版插件)
55
+ - [nonebot/plugin-alconna](https://github.com/nonebot/plugin-alconna) (命令解析)
56
+ - [noneplugin/nonebot-plugin-userinfo](https://github.com/noneplugin/nonebot-plugin-userinfo) (用户信息获取)
57
+ - [nonebot/plugin-htmlkit](https://github.com/nonebot/plugin-htmlkit) (HTML 渲染)
58
+ - [LoliApi](https://www.loliapi.com/acg/pe/) (图片源)
@@ -0,0 +1,33 @@
1
+ <!-- markdownlint-disable MD033 MD036 MD041 -->
2
+
3
+ <div align="center">
4
+
5
+ # NoneBot-Plugin-PicStatus-Re
6
+
7
+ 以精美的卡片图片形式展示 NoneBot2 运行设备的系统状态(CPU、内存、磁盘、网速、进程等信息),采用 [nonebot/plugin-htmlkit](https://github.com/nonebot/plugin-htmlkit) 将 HTML 渲染为图片。
8
+
9
+ </div>
10
+
11
+ ## 安装
12
+
13
+ ```bash
14
+ # 使用 nb-cli 安装(推荐)
15
+ nb plugin install nonebot-plugin-picstatus-re
16
+
17
+ # 或使用 pip 安装
18
+ pip install nonebot-plugin-picstatus-re
19
+ ```
20
+ 使用包管理器安装时,需在项目的 `pyproject.toml` 中的 `[tool.nonebot]` 部分的 `plugins` 列表中手动追加 `"nonebot_plugin_picstatus_re"`。
21
+
22
+ ## 使用与配置
23
+
24
+ - **触发指令**:`状态` / `status`(可在配置中修改)
25
+ - **参数配置**:参数均在项目的 `.env.*` 文件中配置,完整说明请参考 **[.env.example](.env.example)**。
26
+
27
+ ## 鸣谢
28
+
29
+ - [nonebot-plugin-picstatus](https://github.com/lgc-NB2Dev/nonebot-plugin-picstatus) (原版插件)
30
+ - [nonebot/plugin-alconna](https://github.com/nonebot/plugin-alconna) (命令解析)
31
+ - [noneplugin/nonebot-plugin-userinfo](https://github.com/noneplugin/nonebot-plugin-userinfo) (用户信息获取)
32
+ - [nonebot/plugin-htmlkit](https://github.com/nonebot/plugin-htmlkit) (HTML 渲染)
33
+ - [LoliApi](https://www.loliapi.com/acg/pe/) (图片源)
@@ -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()