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.
- nonebot_plugin_picstatus_re-2.3.0/.env.example +187 -0
- nonebot_plugin_picstatus_re-2.3.0/.github/workflows/pypi-publish.yml +34 -0
- nonebot_plugin_picstatus_re-2.3.0/.gitignore +9 -0
- nonebot_plugin_picstatus_re-2.3.0/LICENSE +21 -0
- nonebot_plugin_picstatus_re-2.3.0/PKG-INFO +58 -0
- nonebot_plugin_picstatus_re-2.3.0/README.md +33 -0
- nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/__init__.py +67 -0
- nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/__main__.py +57 -0
- nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/bg_provider.py +218 -0
- nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/collectors/__init__.py +226 -0
- nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/collectors/bot.py +93 -0
- nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/collectors/cpu.py +63 -0
- nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/collectors/disk.py +127 -0
- nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/collectors/mem.py +30 -0
- nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/collectors/misc.py +130 -0
- nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/collectors/network.py +123 -0
- nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/collectors/process.py +61 -0
- nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/config.py +117 -0
- nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/misc_statistics.py +153 -0
- nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/res/assets/default_avatar.webp +0 -0
- nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/res/assets/default_bg_0.webp +0 -0
- nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/res/assets/default_bg_1.webp +0 -0
- nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/res/assets/default_bg_2.webp +0 -0
- nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/res/assets/default_bg_3.webp +0 -0
- nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/templates/__init__.py +65 -0
- nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/templates/default/__init__.py +166 -0
- nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/templates/default/res/css/index.css +586 -0
- nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/templates/default/res/templates/index.html.jinja +34 -0
- nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/templates/default/res/templates/macros.html.jinja +236 -0
- nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/templates/render.py +27 -0
- nonebot_plugin_picstatus_re-2.3.0/nonebot_plugin_picstatus_re/util.py +29 -0
- 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,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()
|