nonebot-plugin-botui 0.1.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_botui-0.1.0/PKG-INFO +231 -0
- nonebot_plugin_botui-0.1.0/README.md +212 -0
- nonebot_plugin_botui-0.1.0/pyproject.toml +176 -0
- nonebot_plugin_botui-0.1.0/pyproject.toml.orig +175 -0
- nonebot_plugin_botui-0.1.0/src/nonebot_plugin_botui/__init__.py +382 -0
- nonebot_plugin_botui-0.1.0/src/nonebot_plugin_botui/api.py +1683 -0
- nonebot_plugin_botui-0.1.0/src/nonebot_plugin_botui/capture.py +570 -0
- nonebot_plugin_botui-0.1.0/src/nonebot_plugin_botui/config.py +170 -0
- nonebot_plugin_botui-0.1.0/src/nonebot_plugin_botui/ingest.py +216 -0
- nonebot_plugin_botui-0.1.0/src/nonebot_plugin_botui/media.py +438 -0
- nonebot_plugin_botui-0.1.0/src/nonebot_plugin_botui/mediastore.py +954 -0
- nonebot_plugin_botui-0.1.0/src/nonebot_plugin_botui/models.py +221 -0
- nonebot_plugin_botui-0.1.0/src/nonebot_plugin_botui/paths.py +36 -0
- nonebot_plugin_botui-0.1.0/src/nonebot_plugin_botui/segments.py +900 -0
- nonebot_plugin_botui-0.1.0/src/nonebot_plugin_botui/static/app.css +1774 -0
- nonebot_plugin_botui-0.1.0/src/nonebot_plugin_botui/static/app.js +3799 -0
- nonebot_plugin_botui-0.1.0/src/nonebot_plugin_botui/static/index.html +232 -0
- nonebot_plugin_botui-0.1.0/src/nonebot_plugin_botui/store.py +1271 -0
- nonebot_plugin_botui-0.1.0/src/nonebot_plugin_botui/webui.py +386 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: nonebot-plugin-botui
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 一个 QQ 风格的 WebUI 控制台:查看机器人收发的全部消息,并以机器人的身份发消息。
|
|
5
|
+
Author: Lonely-Sails
|
|
6
|
+
Requires-Dist: aiosqlite>=0.20.0,<1.0.0
|
|
7
|
+
Requires-Dist: fastapi>=0.100.0
|
|
8
|
+
Requires-Dist: httpx>=0.27.0,<1.0.0
|
|
9
|
+
Requires-Dist: nonebot-plugin-alconna>=0.60.0,<1.0.0
|
|
10
|
+
Requires-Dist: nonebot-plugin-apscheduler>=0.5.0,<1.0.0
|
|
11
|
+
Requires-Dist: nonebot-plugin-localstore>=0.7.0,<1.0.0
|
|
12
|
+
Requires-Dist: nonebot-plugin-uninfo>=0.10.0,<1.0.0
|
|
13
|
+
Requires-Dist: nonebot2>=2.3.0,<3.0.0
|
|
14
|
+
Requires-Python: >=3.10, <4.0
|
|
15
|
+
Project-URL: Homepage, https://github.com/Lonely-Sails/nonebot-plugin-botui
|
|
16
|
+
Project-URL: Issues, https://github.com/Lonely-Sails/nonebot-plugin-botui/issues
|
|
17
|
+
Project-URL: Repository, https://github.com/Lonely-Sails/nonebot-plugin-botui.git
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
<div align="center">
|
|
21
|
+
<a href="https://github.com/Lonely-Sails/nonebot-plugin-botui">
|
|
22
|
+
<img src="https://raw.githubusercontent.com/fllesser/nonebot-plugin-template/refs/heads/resource/.docs/NoneBotPlugin.svg" width="310" alt="logo"></a>
|
|
23
|
+
|
|
24
|
+
# NoneBot-Plugin-BotUI
|
|
25
|
+
|
|
26
|
+
<a href="https://github.com/Lonely-Sails/nonebot-plugin-botui"><img src="https://img.shields.io/badge/python-3.10%2B-3776AB?logo=python&logoColor=white" alt="python"></a>
|
|
27
|
+
<a href="https://github.com/Lonely-Sails/nonebot-plugin-botui"><img src="https://img.shields.io/badge/NoneBot2-2.3.0%2B-ff69b4" alt="nonebot2"></a>
|
|
28
|
+
<a href="https://github.com/Lonely-Sails/nonebot-plugin-botui/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="license"></a>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
给机器人配一个 **QQ 风格的 WebUI 控制台**:把机器人收发的全部消息记到本地 SQLite,在网页上像翻聊天记录一样查看,并以机器人的身份发言、回复、@ 和撤回。
|
|
32
|
+
|
|
33
|
+
## ✨ 功能特性
|
|
34
|
+
|
|
35
|
+
- **收发都记**:收到的消息由 `event_preprocessor` 钩子采集,发出的消息由 `Bot.on_calling_api` 钩子拦截 `send_msg` 一类的接口记录;图文混合消息按消息段完整保留
|
|
36
|
+
- **引用可读**:回复/引用消息会连同被引用者与原文一起保留,气泡上方显示「谁:引用内容」的引用块,点击可跳转到被引用的那条消息
|
|
37
|
+
- **文件接收与在线预览**:收到的文件显示为文件卡片(按扩展名着色图标 + 文件名 + 大小),点击即可在弹窗中在线预览 —— 文本类(代码、配置、日志、`txt`/`md`/`json` 等)直接渲染正文,图片、PDF 内联显示,音视频内联播放,其它类型可一键下载;文本预览默认只读取前 512 KB 并提示已截断,下载走**真流式代理**(支持 HTTP Range 断点续传),默认最大 100 MB(`BOTUI_FILE_MAX_BYTES`)。**文件名会自动还原**:像 QQ 官方机器人适配器只给一个下载直链、文件名恒为 `file.bin` 的情况,插件会从链接的 `fname`/`filename` 等参数或路径末段、以及上游响应头里取回真实文件名(含中文),并按真实后缀判断预览方式与下载名
|
|
38
|
+
- **合并转发展开**:合并转发消息可在气泡内原地折叠/展开,点开查看每个节点的发送者、时间与内容,优先使用记录里已有的内联节点,缺失时再调用适配器接口拉取
|
|
39
|
+
- **统一媒体库**:机器人收到的图片 / 文件 / 语音 / 音视频,以及 WebUI 里上传的附件,**都是同一种东西**——收到时异步下载入库,发送时正式入库,落在同一个目录、同一张 `blobs` 表、同一个取回地址 `/api/media/<id>`。消息段里存的就是这个地址,所以「收到的图链接失效打不开」与「自己发的图过期打不开」从此走同一条保命逻辑。媒体库有配额(默认 512 MB)与保留时间(未被引用的 3 天、被聊天记录引用的 7 天),超限时优先淘汰没人引用的资源;右上角「设置」面板可查看占用并清理(未引用 / 图片 / 文件 / 全部)。开关为 `BOTUI_MEDIA_ENABLED`
|
|
40
|
+
- **资源按内容(MD5)命名**:一个资源的 id 就是它内容的 MD5,落盘为 `blobs/<前两位>/<md5>`,消息里引用 `/api/media/<md5>`。内容相同的东西天然只有一份,不必再维护「同一链接只存一次」的去重表;文件名与 MIME 作为展示信息存在数据库里
|
|
41
|
+
- **元数据存数据库而不是散落的 JSON**:每个资源一个 `meta.json`、再配一个全局索引的做法,等于把状态摊在文件系统上——查询要遍历目录、并发要抢文件锁、崩溃会留下半个索引。媒体元数据现在直接建在消息已经用的那条 SQLite 连接上(`blobs` 表),与消息共享同一个事务与锁;磁盘上只按内容 MD5 落盘
|
|
42
|
+
- **上传附件先走系统临时目录**:WebUI 里选中的附件用 `tempfile` 暂存在系统临时目录(还没发送,不该占配额、也不该被内容去重),前端点「移除」即删;发送那一刻才按内容 MD5 正式进媒体库。因此「选完没发」的附件不会留在磁盘上赖着不走。清理由后台任务按 `BOTUI_MEDIA_TTL` 兜底,默认每 10 分钟跑一次(`BOTUI_MEDIA_CLEANUP_INTERVAL`)
|
|
43
|
+
- **聊天记录导出**:右键菜单可把当前会话(含时间、发送者与全部消息段)导出为 JSON 文件下载
|
|
44
|
+
- **跨平台**:不绑定具体适配器 —— 会话与用户信息来自 `nonebot-plugin-uninfo` 的 `Session`,消息解析与发送走 `nonebot-plugin-alconna` 的 `UniMessage` / `Target`
|
|
45
|
+
- **本地存储**:消息与会话落盘到插件数据目录的 `botui.sqlite3`(WAL 模式),媒体元数据存在同一库的 `blobs` 表里,介质文件放在同目录的 `blobs/`;支持记录条数上限与保留天数,插件定时自动清理
|
|
46
|
+
- **QQ 风格 WebUI**:左侧会话列表(搜索、头像、未读小红点、最后一条消息摘要)+ 右侧聊天记录(气泡、日期分隔、图片点击放大)
|
|
47
|
+
- **实时推送**:WebUI 通过 `/api/ws` 建立 WebSocket 长连接(`ws://` / `wss://`),服务端主动推送新消息、撤回与新会话,页面无需轮询;断线按 1s ~ 30s 指数退避自动重连,重连时带上事件序号补发漏掉的那段
|
|
48
|
+
- **新消息网页通知**:在右上角「设置」里开启后,收到新消息会弹出浏览器系统通知(可另开提示音,用 WebAudio 合成、无外部音频文件)。为了不打扰,**自己发的消息、以及正在看且窗口聚焦的会话**都不会提醒,只在该会话未打开、或页面切到后台 / 失焦时弹出;点通知可直接跳到对应会话。再次打开页面会自动校验授权状态,被撤销时会自动关掉
|
|
49
|
+
- **以机器人身份发言**:网页上直接发送文本、`@` 成员、回复某条消息,并可按配置撤回自己发出的消息;`@` 成员从「聊天里出现过的成员名册」中挑选,无需手动输入 QQ 号
|
|
50
|
+
- **发送图片与文件**:点输入框旁的 `+` 选图 / 选文件,也可以直接把文件**拖进页面**或从剪贴板**粘贴**(截图直接发)。图片按 `Image` 段发送、聊天里直接显示,其余按 `File` 段发送;选中的附件先以缩略图 / 文件名标签排在输入框上方,可逐个移除后再发送。选中的附件先暂存在系统临时目录(移除即删,不占媒体库配额),**发送时才按内容 MD5 正式入库**;单个附件默认上限 20 MB(`BOTUI_MEDIA_FILE_MAX_BYTES`)。发出的附件在记录里指向稳定的 `/api/media/<md5>` 地址,因此**聊天记录里自己发的图不会再变成坏图**
|
|
51
|
+
- **会话备注**:有些适配器拿不到真实的群名或用户昵称(只有一串号码,或名称随时变),可以给**会话、成员、机器人**分别设置备注。备注优先作为界面上的显示名(原始名称保留在 `raw_name` 里,会显示在会话头部),参与会话与成员搜索,且**不会被适配器后续上报的名称覆盖**;只影响展示,不写入聊天记录
|
|
52
|
+
- **多机器人切换**:右上角的下拉框会记录**所有连接过**的机器人,在线的标绿点、离线的置灰;每个机器人的会话、消息与搜索结果彼此隔离,切换后互不串台。给某个会话发消息时只会用**它自己的机器人**发送,该机器人不在线时宁可失败也不会冒用别的机器人
|
|
53
|
+
- **令牌鉴权 + 仅本机访问**:默认开启,访问令牌自动生成并保存在数据目录,默认只允许本机来源的请求
|
|
54
|
+
- **零额外端口**:路由直接挂到宿主项目已有的 FastAPI 应用上(`nonebot2[fastapi]`),不新起服务
|
|
55
|
+
- **不注册任何指令**:不占用命令名、不拦截聊天消息,访问地址只在启动时打印到控制台日志
|
|
56
|
+
|
|
57
|
+
## 📦 安装
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
nb plugin install nonebot-plugin-botui
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
或用 pip 安装:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install nonebot-plugin-botui
|
|
67
|
+
# 尚未发布到 PyPI 时,也可以直接从仓库安装:
|
|
68
|
+
pip install git+https://github.com/Lonely-Sails/nonebot-plugin-botui.git
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
安装后在 `pyproject.toml` 的 `[tool.nonebot]` 中加入:
|
|
72
|
+
|
|
73
|
+
```toml
|
|
74
|
+
plugins = ["nonebot_plugin_botui"]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
> WebUI 依赖宿主项目的 **FastAPI 驱动**:请安装 `nonebot2[fastapi]`(并在 `.env` 里配置 `DRIVER=~fastapi`),否则插件启动时会提示「未能拿到 FastAPI 应用」。
|
|
78
|
+
>
|
|
79
|
+
> 依赖:`nonebot2 >= 2.3.0`、`nonebot-plugin-alconna`、`nonebot-plugin-uninfo`、`nonebot-plugin-localstore`、`nonebot-plugin-apscheduler`、`fastapi`、`aiosqlite`、`httpx`。
|
|
80
|
+
|
|
81
|
+
## 🚀 快速开始
|
|
82
|
+
|
|
83
|
+
插件**不注册任何聊天指令**,不会占用命令名,也不会拦截用户消息。启动机器人后直接看控制台日志:
|
|
84
|
+
|
|
85
|
+
1. 启动日志里会给出带令牌的访问地址,以及数据存放位置:
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
[INFO] nonebot_plugin_botui | http://127.0.0.1:8080/botui/?token=xxxxxxxx
|
|
89
|
+
[INFO] nonebot_plugin_botui | 仅本机可访问;需要远程访问请设置 BOTUI_ALLOW_REMOTE=true
|
|
90
|
+
[INFO] nonebot_plugin_botui | 数据:/path/to/data/botui.sqlite3
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
每行都是一条独立日志(前缀由 NoneBot 的日志格式补上)。只有**偏离默认值**的开关才会额外列出(例如关掉了采集、关掉了鉴权);保持默认时不会啰嗦。
|
|
94
|
+
|
|
95
|
+
2. 打开那个链接即可。前端会自己把地址栏里的 `?token=` 抹掉,并保存到浏览器 `localStorage`,下次直接访问 `/botui/` 就行;换浏览器时在令牌输入框里粘贴令牌即可。
|
|
96
|
+
|
|
97
|
+
几个说明:
|
|
98
|
+
|
|
99
|
+
- 日志若显示**未能挂载 WebUI**,通常是驱动不对:WebUI 依赖宿主项目的 **FastAPI 驱动**,请安装 `nonebot2[fastapi]` 并在 `.env` 里配置 `DRIVER=~fastapi`。
|
|
100
|
+
- 令牌默认自动生成并写入插件数据目录下的 **`token.txt`**(目录由 `nonebot-plugin-localstore` 提供;文件权限为 `0600`)。想固定令牌就设置 `BOTUI_TOKEN`。
|
|
101
|
+
- 关闭令牌后地址里不会带 `?token=`,此时任何能访问该地址的人都能查看记录并冒充机器人发言,启动日志会明确警告。
|
|
102
|
+
|
|
103
|
+
## ⚙️ 配置
|
|
104
|
+
|
|
105
|
+
在 `.env` / `.env.prod` 中配置(环境变量前缀 `BOTUI_`,完整示例见 `.env.example`):
|
|
106
|
+
|
|
107
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
108
|
+
| --- | :-: | --- | --- |
|
|
109
|
+
| `BOTUI_ENABLED` | `bool` | `true` | 是否启用 WebUI;关闭后插件不采集也不挂载 |
|
|
110
|
+
| `BOTUI_ROUTE` | `str` | `/botui` | WebUI 挂载路径,不以 `/` 开头会自动补上 |
|
|
111
|
+
| `BOTUI_HOST` | `str` | `127.0.0.1` | 提示链接里使用的主机名(**只影响链接显示**,不改变安全策略) |
|
|
112
|
+
| `BOTUI_ALLOW_REMOTE` | `bool` | `false` | 是否允许非本机来源访问;开启前请确认令牌鉴权已启用(见「安全提示」) |
|
|
113
|
+
| `BOTUI_ALLOWED_HOSTS` | `tuple[str, ...]` | `()` | 允许的 `Host` 头白名单,**留空则不校验**;走域名(反向代理)访问时填上域名即启用,用于挡 DNS rebinding |
|
|
114
|
+
| `BOTUI_AUTH` | `bool` | `true` | 是否启用令牌鉴权(强烈建议保持开启) |
|
|
115
|
+
| `BOTUI_TOKEN` | `str` | 空 | 访问令牌;留空则自动生成并保存到数据目录的 `token.txt` |
|
|
116
|
+
| `BOTUI_CAPTURE_RECEIVED` | `bool` | `true` | 是否记录机器人收到的消息 |
|
|
117
|
+
| `BOTUI_CAPTURE_SENT` | `bool` | `true` | 是否记录机器人发出的消息 |
|
|
118
|
+
| `BOTUI_CAPTURE_SELF` | `bool` | `false` | 是否记录「自己发出的消息被适配器回传成事件」的情况;关闭可避免同一条消息被记两次 |
|
|
119
|
+
| `BOTUI_EXCLUDE_ADAPTERS` | `tuple[str, ...]` | `()` | 不记录这些适配器的消息,例如 `["OneBot V12"]` |
|
|
120
|
+
| `BOTUI_MAX_RECORDS` | `int` | `200000` | 消息记录上限,超出后自动删除最旧的记录;`0` 表示不限制 |
|
|
121
|
+
| `BOTUI_RETENTION_DAYS` | `int` | `0` | 消息保留天数,超期自动清理;`0` 表示不限制 |
|
|
122
|
+
| `BOTUI_RESOLVE_AT_NAME` | `bool` | `true` | 是否把 `@某人` 解析成群名片(会调用适配器接口并缓存) |
|
|
123
|
+
| `BOTUI_API_TIMEOUT` | `float` | `10.0` | 插件内部调用适配器接口(如获取群成员信息)的超时时间(秒) |
|
|
124
|
+
| `BOTUI_PAGE_SIZE` | `int` | `50` | 聊天记录每页条数,取值范围 `10 ~ 200` |
|
|
125
|
+
| `BOTUI_WRITE_ENABLED` | `bool` | `true` | 是否允许在 WebUI 中代替机器人发送消息;`false` 时网页进入只读模式 |
|
|
126
|
+
| `BOTUI_ALLOW_RECALL` | `bool` | `true` | 是否允许在 WebUI 中撤回消息 |
|
|
127
|
+
| `BOTUI_MEDIA_ALLOW_PRIVATE` | `bool` | `false` | 图片代理是否允许访问内网/环回地址;除非确实要用它代理内网图床,否则保持关闭(见「安全提示」) |
|
|
128
|
+
| `BOTUI_FILE_PREVIEW` | `bool` | `true` | 是否允许在网页上在线预览收到的文件(文本 / 图片 / PDF / 音视频) |
|
|
129
|
+
| `BOTUI_FILE_MAX_BYTES` | `int` | `104857600` | 文件下载代理允许的最大体积(字节,默认 100 MB);`0` 表示不限制 |
|
|
130
|
+
| `BOTUI_MEDIA_ENABLED` | `bool` | `true` | 是否启用媒体库(下载收到的媒体、接收上传的附件);关闭后链接只能实时代理,失效即打不开 |
|
|
131
|
+
| `BOTUI_MEDIA_MAX_BYTES` | `int` | `536870912` | 媒体库占用上限(字节,默认 512 MB),超出后自动淘汰最久未用的资源;`0` 表示不按体积限制 |
|
|
132
|
+
| `BOTUI_MEDIA_MAX_FILES` | `int` | `20000` | 媒体库文件数量上限;`0` 表示不按数量限制(资源按内容去重,条数通常远小于消息数) |
|
|
133
|
+
| `BOTUI_MEDIA_FILE_MAX_BYTES` | `int` | `20971520` | 单个资源进入媒体库的最大体积(字节,默认 20 MB),超过的只代理不落盘;`0` 表示不限制 |
|
|
134
|
+
| `BOTUI_MEDIA_TTL` | `float` | `259200.0` | 未被聊天记录引用的资源保留时长(秒,默认 3 天),到期自动回收;`0` 表示不按时间清理 |
|
|
135
|
+
| `BOTUI_MEDIA_RETENTION` | `float` | `604800.0` | 已被聊天记录引用的资源保留时长(秒,默认 7 天);`0` 表示不限制 |
|
|
136
|
+
| `BOTUI_MEDIA_FETCH_TIMEOUT` | `float` | `20.0` | 下载单个媒体资源入库的超时时间(秒) |
|
|
137
|
+
| `BOTUI_MEDIA_MAX_BYTES_PER_MESSAGE` | `int` | `20971520` | 单条消息最多下载多少字节入库(默认 20 MB),避免一条消息把媒体库刷满;`0` 表示不限制 |
|
|
138
|
+
| `BOTUI_MEDIA_CLEANUP_INTERVAL` | `int` | `10` | 媒体库自动清理的间隔(分钟);`0` 表示只保留每 6 小时一次的消息记录清理 |
|
|
139
|
+
| `BOTUI_SEND_INTERVAL` | `float` | `1.0` | 同一会话两次发送之间的最小间隔(秒),用于限流 |
|
|
140
|
+
| `BOTUI_DEBUG` | `bool` | `false` | 打印更详细的调试日志 |
|
|
141
|
+
|
|
142
|
+
### 数据存放位置
|
|
143
|
+
|
|
144
|
+
数据库与令牌的路径**完全交给 `nonebot-plugin-localstore`**,插件只读取它给出的目录,不再提供自己的目录配置项:
|
|
145
|
+
|
|
146
|
+
```dotenv
|
|
147
|
+
# 全局改(所有插件生效)
|
|
148
|
+
LOCALSTORE_DATA_DIR=/srv/nonebot/data
|
|
149
|
+
|
|
150
|
+
# 只改本插件(推荐,用插件 ID 作 key)
|
|
151
|
+
LOCALSTORE_PLUGIN_DATA_DIR={"nonebot_plugin_botui": "/srv/botui/data"}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
默认位置参考 localstore 的规则:`LOCALSTORE_USE_CWD=true` 时为 `<运行目录>/data/nonebot_plugin_botui/`,否则为系统数据目录下的 `nonebot2/nonebot_plugin_botui/`。可以用 `nb localstore` 查看实际路径。目录下会生成:
|
|
155
|
+
|
|
156
|
+
- `botui.sqlite3`:消息、会话**与媒体元数据**(`blobs` 表)数据库(WAL 模式)
|
|
157
|
+
- `token.txt`:自动生成的访问令牌(权限 `0600`)
|
|
158
|
+
- `blobs/`:媒体库的正文 —— 收到的图片/文件、已发出的附件都在这里,布局为 `blobs/<md5前两位>/<md5>`(正文名就是内容的 MD5)。整目录可删:删掉只是丢掉本地副本,不影响聊天记录本身,下一次访问会重新去上游链接取(前提是链接还没失效)
|
|
159
|
+
|
|
160
|
+
> 路径在插件导入时解析一次并存为常量。localstore 是靠**调用栈**反查「哪个插件在要目录」的,只有栈上存在插件自己的帧时才能查到(从 `__main__`、别名模块名或外部库回调里现取会报 `Cannot detect caller plugin`)。导入时解析最稳妥,之后全项目只读常量。
|
|
161
|
+
|
|
162
|
+
## 🎉 命令
|
|
163
|
+
|
|
164
|
+
**没有命令。** 插件不注册任何 matcher,不 `require` 任何前缀,也不拦截聊天消息 ——
|
|
165
|
+
访问地址只在启动时打印到控制台日志(见「快速开始」)。
|
|
166
|
+
|
|
167
|
+
这样设计是为了避免和你的其他插件抢命令名:早期的版本注册过 `帮助` / `状态` /
|
|
168
|
+
`链接` / `统计` / `清理` / `重置` 这些又短又通用的别名,很容易和别人撞车。
|
|
169
|
+
|
|
170
|
+
清理旧记录不需要手动触发:插件每 6 小时会按 `BOTUI_MAX_RECORDS` 与
|
|
171
|
+
`BOTUI_RETENTION_DAYS` 自动执行一次。
|
|
172
|
+
|
|
173
|
+
## 🖥️ 界面说明
|
|
174
|
+
|
|
175
|
+
- **主题**:默认深色,右上角按钮可切换浅色,选择保存在 `localStorage`(`botui_theme`)。
|
|
176
|
+
- **机器人切换**:右上角下拉框列出**所有连接过**的机器人(在线的标绿点、离线的置灰,显示头像、名称、`self_id` 与适配器)。始终恰好选中一个机器人:进来时优先选中上次选过的(记录在 `localStorage` 的 `botui_bot`),否则选中第一个在线的。切换机器人会清空当前会话与消息列表,只显示该机器人的会话与记录;连接状态由服务端推送实时更新,选择的机器人离线时会自动挑选下一个在线的。
|
|
177
|
+
- **设置面板**:右上角齿轮打开,包含两部分。**新消息通知** —— 开关网页通知、提示音;开启时会请求浏览器授权,被拒绝或浏览器不支持时开关置灰并显示原因(偏好记在 `localStorage` 的 `botui_notify`)。**本地缓存** —— 显示占用、资源数、图片/文件分类占用与本进程的命中统计,并提供「刷新 / 清理未引用 / 清理图片 / 清理文件 / 清空媒体库」几个操作。清空需要连点两次确认(第一次点会变成「确认清空?」)。媒体库未开启时缓存区域的按钮全部置灰。
|
|
178
|
+
- **会话记忆**:每个机器人最近打开的会话会记在 `localStorage`(`botui_chat`,形如 `{self_id: chat_key}`),下次打开页面或切回该机器人时自动打开那个会话;若已不存在(被清理或换了机器人)则照常停留在会话列表。
|
|
179
|
+
- **备注(别名)**:适配器拿不到真实群名或昵称时,可以自己给它们起个认得出的名字。**会话**:会话头部会话名旁的铅笔按钮,或在左侧会话列表上**右键 → 设置备注**;设置后会话头部会以备注为名、下方显示「原名 <适配器给的名称>」。**成员**:在输入框 `@` 菜单里**右键某个成员**,或在聊天记录里**右键某条消息 → 备注发言人**。**机器人**:右上角机器人切换器菜单里每行右侧的铅笔按钮。备注优先作为显示名,参与搜索,且不会被适配器上报的名称覆盖;清空后即恢复显示适配器给的名称。
|
|
180
|
+
- **令牌页**:开启鉴权时,若浏览器里没有令牌(或令牌失效返回 401),会先弹出输入框;令牌只存在当前浏览器的 `localStorage` 中,不会回传服务器保存。
|
|
181
|
+
- **会话列表**:按最后消息时间倒序,显示头像(拉不到图就用名称首字生成彩色圆)、名称、最后一条消息摘要(机器人发出的以「我: 」开头)、时间(今天为 `HH:MM`,昨天为「昨天」,更早为 `MM-DD` / `YYYY-MM-DD`)与未读数(超过 99 显示 `99+`,未打开的会话显示「新」+ 摘要)。
|
|
182
|
+
- **搜索**:顶部搜索框输入后出现「会话 / 消息」两个标签。**会话**标签即时过滤会话名、会话 ID(群号/QQ 号)与最后一条消息;**消息**标签会请求 `/api/search` 做正文全文检索,结果按时间倒序展示并高亮关键词,点一条即可跳到对应会话并定位到该消息(不在已加载的一页时会提示)。
|
|
183
|
+
- **聊天记录**:左右分栏气泡,连续消息紧凑排版,按天插入「今天 / 昨天 / 5月1日 / 2024年5月1日」分隔;切换会话时先显示骨架占位;图片消息点击放大(灯箱,点击任意处或按 `Esc` 关闭),`@`、回复、表情、文件、语音、视频、卡片、合并转发等消息段有对应样式;向上滚动自动加载更早的消息,单次会话最多在前端保留 1200 条。
|
|
184
|
+
- **文件与预览**:文件消息显示为文件卡片,点击在弹窗内预览 —— 文本类(代码/日志/配置等)直接显示正文并在超过 512 KB 时提示截断,图片与 PDF 内联显示,音视频提供内联播放器,其它类型给出下载按钮(弹窗右上角也可下载/另存)。语音、音频、视频消息也都有内联播放器;图片与媒体统一走 `/api/media/<id>`(媒体库副本),文件走 `/api/file` 流式代理(带令牌与 SSRF 校验)。**若资源已在媒体库里(响应头带 `X-BotUI-Cache: hit`)**,`/api/file`、`/api/preview` 会直接读本地副本,不再联网。预览开关为 `BOTUI_FILE_PREVIEW`。
|
|
185
|
+
- **合并转发**:合并转发消息显示为可折叠的「聊天记录」卡片,默认收起,点击标题栏原地展开成一条条小聊天记录(不弹窗),逐条显示发送者昵称、时间与内容。节点优先取自记录里的内联数据,只有在缺内联数据时才向适配器请求,且只在首次展开时拉取。
|
|
186
|
+
- **实时刷新**:页面通过 WebSocket(`/api/ws`)接收服务端推送的事件,新消息实时置顶并提示「N 条新消息」,新会话自动进入列表,撤回事件会把气泡移除。连接失败/断开时按 1s ~ 30s 指数退避重连,并在左下角显示连接状态(令牌无效用 `4401`、来源被拒用 `4403`,分别对应弹令牌框 / 提示只允许本机);重连会带上最后收到的事件序号,服务端从环形缓冲里补发漏掉的那段;序号出现较大空洞或长时间没收到任何推送时会自动重新同步一次。不在底部时新消息不打扰阅读,只提示「N 条新消息」。
|
|
187
|
+
- **发送 / 回复 / @**:底部输入框 `Enter` 发送、`Shift+Enter` 换行;`@` 按钮打开**成员选择菜单**(关键词过滤、显示头像与群主/管理员标签),只能从「这个群里记录到的成员」里选,不再要求手输 QQ 号,选中的成员以「@昵称」标签形式展示、可逐个移除;右键消息选「回复」会在输入框上方显示引用条(含被引用者与原文摘要),发送时带上回复段。
|
|
188
|
+
- **发送图片 / 文件**:输入框左侧的 `+` 按钮打开文件选择框(可多选),也可以把文件**拖进页面任意位置**(会显示拖拽提示层),或在输入框里直接**粘贴**剪贴板里的图片(截图 → `Ctrl/Cmd+V` → 发送)。选中的附件会先上传到服务端并以缩略图(图片)或「类别 + 文件名 + 大小」标签排在输入框上方,可逐个 `✕` 移除。发送时可以只发附件不发文字。上传进度反映在发送按钮上(显示「上传中」),上传中的附件不能发送。
|
|
189
|
+
- **右键菜单**:`复制文本`、`回复`、`备注发言人`(给该消息的发送者设置备注,仅收到的消息可用)、`撤回`(仅机器人自己发出且拿到消息 ID 的消息可用,且需要 `BOTUI_ALLOW_RECALL=true`;不支持撤回的适配器会在调用时返回失败原因)、`预览文件`(文件消息可用)、`导出聊天记录`(导出当前会话为 JSON)、`复制消息ID`。左侧会话列表上右键则是 `设置备注`。
|
|
190
|
+
- **只读模式**:`BOTUI_WRITE_ENABLED=false` 时输入框、发送按钮、`@` 按钮与 `+` 附件按钮均被禁用,底部提示「只读模式」;备注编辑同样会被拒绝(返回 403)。
|
|
191
|
+
- **窄屏**:宽度小于 768px 时变成单栏,点消息头部返回按钮回到会话列表;输入框字号会自动提到 16px,避免 iOS 聚焦时把整页放大;同时按 `viewport-fit=cover` 处理了刘海屏安全区。
|
|
192
|
+
|
|
193
|
+
## 🔐 安全提示
|
|
194
|
+
|
|
195
|
+
- **默认开启令牌鉴权**(`BOTUI_AUTH=true`)。令牌要么由 `BOTUI_TOKEN` 指定,要么自动生成 24 字节随机串写入数据目录的 `token.txt`(权限 `0600`)。请勿把令牌或 `token.txt` 泄露出去。
|
|
196
|
+
- **令牌优先走请求头**。普通接口一律用 `X-BotUI-Token`;地址栏里的 `?token=` 只对**浏览器无法带自定义头**的路径放行(`/media`、`/api/media`、`/api/file`、`/api/preview`、`/api/export` 与 WebSocket `/api/ws`),写接口一律不接受它 —— 这样令牌不容易被反向代理访问日志、浏览器历史或第三方子资源请求带出去。鉴权失败次数过多会短时返回 429。
|
|
197
|
+
- **默认只允许本机访问**。服务端会拒绝非本机来源的请求(返回 403)。判断来源用的是 `ipaddress` 的 loopback 判定,所以 `127.0.0.1`、`127.0.0.2`、`::1`、`::ffff:127.0.0.1`、`localhost` 都算本机。要开放给外部,需要显式设置 **`BOTUI_ALLOW_REMOTE=true`**。
|
|
198
|
+
- **`Host` 头白名单(挡 DNS rebinding,按需启用)**。`BOTUI_ALLOWED_HOSTS` **留空时不做任何校验**(默认),这样用主机名访问本机(如 `http://my-nas:8080`)不会被打回。走**域名**(反向代理)部署时把域名写进去即启用白名单:本机主机名(`localhost`、`127.0.0.1`、`[::1]`)始终允许,其余 `Host` 一律拒绝。这样即便攻击者用「先解析到自己的服务器、再改解析到 `127.0.0.1`」的手法让来源变成环回,也会因为 `Host` 不对而被拒。
|
|
199
|
+
- **响应头加固**。BotUI 路由下的响应都会带上 `X-Content-Type-Options: nosniff`、`X-Frame-Options: DENY`、`Referrer-Policy: no-referrer` 与 `frame-ancestors 'none'` 的 CSP。`nosniff` 尤其重要:`/api/file` 会按上游给的 `content-type` 内联返回文件,没有它时浏览器可能对内容做嗅探。此外 `/api/meta` 会返回 `robots_tag`,前端据此插入 `<meta name="robots" content="noindex, nofollow">`,避免被搜索引擎收录。
|
|
200
|
+
- **`BOTUI_HOST` 只管链接显示**。它决定启动日志里给出的主机名(比如你走反向代理,就填域名),**不会**改变来源限制。这样把链接改成局域网地址或域名都不会意外放开访问;反过来也不会因为填了域名就把自己锁在外面。
|
|
201
|
+
- 若同时开启 `BOTUI_ALLOW_REMOTE=true` 与 `BOTUI_AUTH=false`,启动时会打印一条醒目警告 —— 这是「任何人只要能访问该地址就能查看全部记录并冒充机器人」的状态。
|
|
202
|
+
- **链接里带的是单一密钥**。`?token=xxx` 就是全部凭据:谁能拿到链接,谁就能查看全部聊天记录并以机器人的身份发消息、撤回消息。地址只出现在**服务端控制台日志**里(不会发到任何聊天窗口),请自行保管好日志输出。
|
|
203
|
+
- **`BOTUI_AUTH=false` 意味着完全开放**(在来源限制之内):任何能访问该地址的人都能查看记录、以机器人身份发言和撤回;启动日志里也会明确提示「令牌鉴权已关闭」。
|
|
204
|
+
- **图片代理**:`/media` 需要令牌,且**只代理公网地址**。它会把目标域名解析一遍,只要解析结果里有环回、内网、链路本地(含 `169.254.169.254` 这类云元数据地址)等非公网地址就拒绝,重定向每一跳都会重新校验,因此不能拿它当跳板去读内网服务。如果你的图床确实在内网,才需要打开 `BOTUI_MEDIA_ALLOW_PRIVATE=true`。
|
|
205
|
+
- **媒体库落在磁盘上**:收到的媒体会被下载、上传的附件会直接存入 localstore 的插件数据目录下的 `blobs/`,所以磁盘占用会随收发的媒体增长。除按 `BOTUI_MEDIA_MAX_BYTES` / `BOTUI_MEDIA_MAX_FILES` 自动淘汰外,还可在 WebUI 的「设置」面板手动清理。若不想让它落盘,设 `BOTUI_MEDIA_ENABLED=false` 即可(此时仍可代理,只是链接失效后就打不开了)。
|
|
206
|
+
- **文件代理**:`/api/file`(流式下载/预览)与 `/api/preview`(文本预览)同样需要令牌,并复用与图片代理一致的 **SSRF 校验**与重定向逐跳校验。下载默认限制 100 MB(`BOTUI_FILE_MAX_BYTES`,`0` 表示不限制),文本预览最多读取前 512 KB。若你的文件确实在内网,同样需要 `BOTUI_MEDIA_ALLOW_PRIVATE=true`。
|
|
207
|
+
- **附件上传**:`POST /api/upload`(暂存到系统临时目录)需要令牌且仅在**可写模式**下可用(`BOTUI_WRITE_ENABLED=false` 时直接 403),取回/删除时 `id` 经严格校验,只允许本插件生成的 id,`../` 之类的路径穿越会被拒绝;文件名会剥离路径与控制字符后才展示。单个附件默认上限 20 MB(`BOTUI_MEDIA_FILE_MAX_BYTES`)。已发送的附件在**发送时**按内容 MD5 正式进媒体库(未发送的暂存文件随进程结束、移除或超时被删掉),因此**请确保磁盘空间与访问权限可控**;`BOTUI_MEDIA_ENABLED=false` 可以整体关闭该功能。发出的附件与收到的媒体存在同一份库、同一个 `/api/media/<md5>` 地址下。
|
|
208
|
+
- **需要公网访问时**:建议放在反向代理之后,加 HTTPS 与额外的访问控制,并保持令牌鉴权开启;连同 `BOTUI_WRITE_ENABLED=false` 一起使用还可以只读地公开记录。
|
|
209
|
+
- 注意反代与机器人在**同一台机器**上时,请求来源就是 `127.0.0.1`,来源限制会自然放行 —— 此时**鉴权完全依赖令牌**,请务必保持 `BOTUI_AUTH=true` 并确认反代不会把 `?token=` 记进日志。
|
|
210
|
+
- 若反代在另一台机器上,才需要 `BOTUI_ALLOW_REMOTE=true`。
|
|
211
|
+
|
|
212
|
+
## 🌐 跨平台说明
|
|
213
|
+
|
|
214
|
+
- 用户与场景信息统一由 `nonebot-plugin-uninfo` 的 `Session` 提供,发送目标由 `nonebot-plugin-alconna` 的 `Target` 构造,因此插件本身不绑定任何适配器;`supported_adapters` 直接继承自这两个插件。
|
|
215
|
+
- **多机器人**:每个机器人(`self_id`)的会话彼此独立 —— 会话 key 采用 `self_id:kind_id` 的形式内嵌机器人 ID,同名群在 A、B 两个机器人下就是两条不同的会话;每条消息都带上产生它的适配器名与 `self_id`。WebUI 的发送只会用**会话自己的机器人**,它不在线时返回 503 而不会改用别的机器人。
|
|
216
|
+
- 只要适配器支持对应的能力就能用:私聊/群聊识别、消息段解析、以及通过 alconna 的 exporter 发送消息。召回(撤回)需要该适配器在 alconna 里实现了对应的 exporter,不支持时网页会提示「适配器 xxx 不支持撤回」。
|
|
217
|
+
- `BOTUI_EXCLUDE_ADAPTERS` 可以按适配器名(如 `["OneBot V12"]`)排除某些平台的记录。
|
|
218
|
+
|
|
219
|
+
## 🧪 开发 / 测试
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
uv sync # 安装依赖
|
|
223
|
+
uv run poe test # 跑测试(pytest + 覆盖率,产出 coverage.xml / junit.xml)
|
|
224
|
+
uv run ruff check . # lint
|
|
225
|
+
uv run ruff format . # 格式化
|
|
226
|
+
uv run poe bump patch # 版本号变更(bump-my-version)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## 📄 协议
|
|
230
|
+
|
|
231
|
+
MIT © 2026 Lonely-Sails
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<a href="https://github.com/Lonely-Sails/nonebot-plugin-botui">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/fllesser/nonebot-plugin-template/refs/heads/resource/.docs/NoneBotPlugin.svg" width="310" alt="logo"></a>
|
|
4
|
+
|
|
5
|
+
# NoneBot-Plugin-BotUI
|
|
6
|
+
|
|
7
|
+
<a href="https://github.com/Lonely-Sails/nonebot-plugin-botui"><img src="https://img.shields.io/badge/python-3.10%2B-3776AB?logo=python&logoColor=white" alt="python"></a>
|
|
8
|
+
<a href="https://github.com/Lonely-Sails/nonebot-plugin-botui"><img src="https://img.shields.io/badge/NoneBot2-2.3.0%2B-ff69b4" alt="nonebot2"></a>
|
|
9
|
+
<a href="https://github.com/Lonely-Sails/nonebot-plugin-botui/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="license"></a>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
给机器人配一个 **QQ 风格的 WebUI 控制台**:把机器人收发的全部消息记到本地 SQLite,在网页上像翻聊天记录一样查看,并以机器人的身份发言、回复、@ 和撤回。
|
|
13
|
+
|
|
14
|
+
## ✨ 功能特性
|
|
15
|
+
|
|
16
|
+
- **收发都记**:收到的消息由 `event_preprocessor` 钩子采集,发出的消息由 `Bot.on_calling_api` 钩子拦截 `send_msg` 一类的接口记录;图文混合消息按消息段完整保留
|
|
17
|
+
- **引用可读**:回复/引用消息会连同被引用者与原文一起保留,气泡上方显示「谁:引用内容」的引用块,点击可跳转到被引用的那条消息
|
|
18
|
+
- **文件接收与在线预览**:收到的文件显示为文件卡片(按扩展名着色图标 + 文件名 + 大小),点击即可在弹窗中在线预览 —— 文本类(代码、配置、日志、`txt`/`md`/`json` 等)直接渲染正文,图片、PDF 内联显示,音视频内联播放,其它类型可一键下载;文本预览默认只读取前 512 KB 并提示已截断,下载走**真流式代理**(支持 HTTP Range 断点续传),默认最大 100 MB(`BOTUI_FILE_MAX_BYTES`)。**文件名会自动还原**:像 QQ 官方机器人适配器只给一个下载直链、文件名恒为 `file.bin` 的情况,插件会从链接的 `fname`/`filename` 等参数或路径末段、以及上游响应头里取回真实文件名(含中文),并按真实后缀判断预览方式与下载名
|
|
19
|
+
- **合并转发展开**:合并转发消息可在气泡内原地折叠/展开,点开查看每个节点的发送者、时间与内容,优先使用记录里已有的内联节点,缺失时再调用适配器接口拉取
|
|
20
|
+
- **统一媒体库**:机器人收到的图片 / 文件 / 语音 / 音视频,以及 WebUI 里上传的附件,**都是同一种东西**——收到时异步下载入库,发送时正式入库,落在同一个目录、同一张 `blobs` 表、同一个取回地址 `/api/media/<id>`。消息段里存的就是这个地址,所以「收到的图链接失效打不开」与「自己发的图过期打不开」从此走同一条保命逻辑。媒体库有配额(默认 512 MB)与保留时间(未被引用的 3 天、被聊天记录引用的 7 天),超限时优先淘汰没人引用的资源;右上角「设置」面板可查看占用并清理(未引用 / 图片 / 文件 / 全部)。开关为 `BOTUI_MEDIA_ENABLED`
|
|
21
|
+
- **资源按内容(MD5)命名**:一个资源的 id 就是它内容的 MD5,落盘为 `blobs/<前两位>/<md5>`,消息里引用 `/api/media/<md5>`。内容相同的东西天然只有一份,不必再维护「同一链接只存一次」的去重表;文件名与 MIME 作为展示信息存在数据库里
|
|
22
|
+
- **元数据存数据库而不是散落的 JSON**:每个资源一个 `meta.json`、再配一个全局索引的做法,等于把状态摊在文件系统上——查询要遍历目录、并发要抢文件锁、崩溃会留下半个索引。媒体元数据现在直接建在消息已经用的那条 SQLite 连接上(`blobs` 表),与消息共享同一个事务与锁;磁盘上只按内容 MD5 落盘
|
|
23
|
+
- **上传附件先走系统临时目录**:WebUI 里选中的附件用 `tempfile` 暂存在系统临时目录(还没发送,不该占配额、也不该被内容去重),前端点「移除」即删;发送那一刻才按内容 MD5 正式进媒体库。因此「选完没发」的附件不会留在磁盘上赖着不走。清理由后台任务按 `BOTUI_MEDIA_TTL` 兜底,默认每 10 分钟跑一次(`BOTUI_MEDIA_CLEANUP_INTERVAL`)
|
|
24
|
+
- **聊天记录导出**:右键菜单可把当前会话(含时间、发送者与全部消息段)导出为 JSON 文件下载
|
|
25
|
+
- **跨平台**:不绑定具体适配器 —— 会话与用户信息来自 `nonebot-plugin-uninfo` 的 `Session`,消息解析与发送走 `nonebot-plugin-alconna` 的 `UniMessage` / `Target`
|
|
26
|
+
- **本地存储**:消息与会话落盘到插件数据目录的 `botui.sqlite3`(WAL 模式),媒体元数据存在同一库的 `blobs` 表里,介质文件放在同目录的 `blobs/`;支持记录条数上限与保留天数,插件定时自动清理
|
|
27
|
+
- **QQ 风格 WebUI**:左侧会话列表(搜索、头像、未读小红点、最后一条消息摘要)+ 右侧聊天记录(气泡、日期分隔、图片点击放大)
|
|
28
|
+
- **实时推送**:WebUI 通过 `/api/ws` 建立 WebSocket 长连接(`ws://` / `wss://`),服务端主动推送新消息、撤回与新会话,页面无需轮询;断线按 1s ~ 30s 指数退避自动重连,重连时带上事件序号补发漏掉的那段
|
|
29
|
+
- **新消息网页通知**:在右上角「设置」里开启后,收到新消息会弹出浏览器系统通知(可另开提示音,用 WebAudio 合成、无外部音频文件)。为了不打扰,**自己发的消息、以及正在看且窗口聚焦的会话**都不会提醒,只在该会话未打开、或页面切到后台 / 失焦时弹出;点通知可直接跳到对应会话。再次打开页面会自动校验授权状态,被撤销时会自动关掉
|
|
30
|
+
- **以机器人身份发言**:网页上直接发送文本、`@` 成员、回复某条消息,并可按配置撤回自己发出的消息;`@` 成员从「聊天里出现过的成员名册」中挑选,无需手动输入 QQ 号
|
|
31
|
+
- **发送图片与文件**:点输入框旁的 `+` 选图 / 选文件,也可以直接把文件**拖进页面**或从剪贴板**粘贴**(截图直接发)。图片按 `Image` 段发送、聊天里直接显示,其余按 `File` 段发送;选中的附件先以缩略图 / 文件名标签排在输入框上方,可逐个移除后再发送。选中的附件先暂存在系统临时目录(移除即删,不占媒体库配额),**发送时才按内容 MD5 正式入库**;单个附件默认上限 20 MB(`BOTUI_MEDIA_FILE_MAX_BYTES`)。发出的附件在记录里指向稳定的 `/api/media/<md5>` 地址,因此**聊天记录里自己发的图不会再变成坏图**
|
|
32
|
+
- **会话备注**:有些适配器拿不到真实的群名或用户昵称(只有一串号码,或名称随时变),可以给**会话、成员、机器人**分别设置备注。备注优先作为界面上的显示名(原始名称保留在 `raw_name` 里,会显示在会话头部),参与会话与成员搜索,且**不会被适配器后续上报的名称覆盖**;只影响展示,不写入聊天记录
|
|
33
|
+
- **多机器人切换**:右上角的下拉框会记录**所有连接过**的机器人,在线的标绿点、离线的置灰;每个机器人的会话、消息与搜索结果彼此隔离,切换后互不串台。给某个会话发消息时只会用**它自己的机器人**发送,该机器人不在线时宁可失败也不会冒用别的机器人
|
|
34
|
+
- **令牌鉴权 + 仅本机访问**:默认开启,访问令牌自动生成并保存在数据目录,默认只允许本机来源的请求
|
|
35
|
+
- **零额外端口**:路由直接挂到宿主项目已有的 FastAPI 应用上(`nonebot2[fastapi]`),不新起服务
|
|
36
|
+
- **不注册任何指令**:不占用命令名、不拦截聊天消息,访问地址只在启动时打印到控制台日志
|
|
37
|
+
|
|
38
|
+
## 📦 安装
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
nb plugin install nonebot-plugin-botui
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
或用 pip 安装:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install nonebot-plugin-botui
|
|
48
|
+
# 尚未发布到 PyPI 时,也可以直接从仓库安装:
|
|
49
|
+
pip install git+https://github.com/Lonely-Sails/nonebot-plugin-botui.git
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
安装后在 `pyproject.toml` 的 `[tool.nonebot]` 中加入:
|
|
53
|
+
|
|
54
|
+
```toml
|
|
55
|
+
plugins = ["nonebot_plugin_botui"]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
> WebUI 依赖宿主项目的 **FastAPI 驱动**:请安装 `nonebot2[fastapi]`(并在 `.env` 里配置 `DRIVER=~fastapi`),否则插件启动时会提示「未能拿到 FastAPI 应用」。
|
|
59
|
+
>
|
|
60
|
+
> 依赖:`nonebot2 >= 2.3.0`、`nonebot-plugin-alconna`、`nonebot-plugin-uninfo`、`nonebot-plugin-localstore`、`nonebot-plugin-apscheduler`、`fastapi`、`aiosqlite`、`httpx`。
|
|
61
|
+
|
|
62
|
+
## 🚀 快速开始
|
|
63
|
+
|
|
64
|
+
插件**不注册任何聊天指令**,不会占用命令名,也不会拦截用户消息。启动机器人后直接看控制台日志:
|
|
65
|
+
|
|
66
|
+
1. 启动日志里会给出带令牌的访问地址,以及数据存放位置:
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
[INFO] nonebot_plugin_botui | http://127.0.0.1:8080/botui/?token=xxxxxxxx
|
|
70
|
+
[INFO] nonebot_plugin_botui | 仅本机可访问;需要远程访问请设置 BOTUI_ALLOW_REMOTE=true
|
|
71
|
+
[INFO] nonebot_plugin_botui | 数据:/path/to/data/botui.sqlite3
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
每行都是一条独立日志(前缀由 NoneBot 的日志格式补上)。只有**偏离默认值**的开关才会额外列出(例如关掉了采集、关掉了鉴权);保持默认时不会啰嗦。
|
|
75
|
+
|
|
76
|
+
2. 打开那个链接即可。前端会自己把地址栏里的 `?token=` 抹掉,并保存到浏览器 `localStorage`,下次直接访问 `/botui/` 就行;换浏览器时在令牌输入框里粘贴令牌即可。
|
|
77
|
+
|
|
78
|
+
几个说明:
|
|
79
|
+
|
|
80
|
+
- 日志若显示**未能挂载 WebUI**,通常是驱动不对:WebUI 依赖宿主项目的 **FastAPI 驱动**,请安装 `nonebot2[fastapi]` 并在 `.env` 里配置 `DRIVER=~fastapi`。
|
|
81
|
+
- 令牌默认自动生成并写入插件数据目录下的 **`token.txt`**(目录由 `nonebot-plugin-localstore` 提供;文件权限为 `0600`)。想固定令牌就设置 `BOTUI_TOKEN`。
|
|
82
|
+
- 关闭令牌后地址里不会带 `?token=`,此时任何能访问该地址的人都能查看记录并冒充机器人发言,启动日志会明确警告。
|
|
83
|
+
|
|
84
|
+
## ⚙️ 配置
|
|
85
|
+
|
|
86
|
+
在 `.env` / `.env.prod` 中配置(环境变量前缀 `BOTUI_`,完整示例见 `.env.example`):
|
|
87
|
+
|
|
88
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
89
|
+
| --- | :-: | --- | --- |
|
|
90
|
+
| `BOTUI_ENABLED` | `bool` | `true` | 是否启用 WebUI;关闭后插件不采集也不挂载 |
|
|
91
|
+
| `BOTUI_ROUTE` | `str` | `/botui` | WebUI 挂载路径,不以 `/` 开头会自动补上 |
|
|
92
|
+
| `BOTUI_HOST` | `str` | `127.0.0.1` | 提示链接里使用的主机名(**只影响链接显示**,不改变安全策略) |
|
|
93
|
+
| `BOTUI_ALLOW_REMOTE` | `bool` | `false` | 是否允许非本机来源访问;开启前请确认令牌鉴权已启用(见「安全提示」) |
|
|
94
|
+
| `BOTUI_ALLOWED_HOSTS` | `tuple[str, ...]` | `()` | 允许的 `Host` 头白名单,**留空则不校验**;走域名(反向代理)访问时填上域名即启用,用于挡 DNS rebinding |
|
|
95
|
+
| `BOTUI_AUTH` | `bool` | `true` | 是否启用令牌鉴权(强烈建议保持开启) |
|
|
96
|
+
| `BOTUI_TOKEN` | `str` | 空 | 访问令牌;留空则自动生成并保存到数据目录的 `token.txt` |
|
|
97
|
+
| `BOTUI_CAPTURE_RECEIVED` | `bool` | `true` | 是否记录机器人收到的消息 |
|
|
98
|
+
| `BOTUI_CAPTURE_SENT` | `bool` | `true` | 是否记录机器人发出的消息 |
|
|
99
|
+
| `BOTUI_CAPTURE_SELF` | `bool` | `false` | 是否记录「自己发出的消息被适配器回传成事件」的情况;关闭可避免同一条消息被记两次 |
|
|
100
|
+
| `BOTUI_EXCLUDE_ADAPTERS` | `tuple[str, ...]` | `()` | 不记录这些适配器的消息,例如 `["OneBot V12"]` |
|
|
101
|
+
| `BOTUI_MAX_RECORDS` | `int` | `200000` | 消息记录上限,超出后自动删除最旧的记录;`0` 表示不限制 |
|
|
102
|
+
| `BOTUI_RETENTION_DAYS` | `int` | `0` | 消息保留天数,超期自动清理;`0` 表示不限制 |
|
|
103
|
+
| `BOTUI_RESOLVE_AT_NAME` | `bool` | `true` | 是否把 `@某人` 解析成群名片(会调用适配器接口并缓存) |
|
|
104
|
+
| `BOTUI_API_TIMEOUT` | `float` | `10.0` | 插件内部调用适配器接口(如获取群成员信息)的超时时间(秒) |
|
|
105
|
+
| `BOTUI_PAGE_SIZE` | `int` | `50` | 聊天记录每页条数,取值范围 `10 ~ 200` |
|
|
106
|
+
| `BOTUI_WRITE_ENABLED` | `bool` | `true` | 是否允许在 WebUI 中代替机器人发送消息;`false` 时网页进入只读模式 |
|
|
107
|
+
| `BOTUI_ALLOW_RECALL` | `bool` | `true` | 是否允许在 WebUI 中撤回消息 |
|
|
108
|
+
| `BOTUI_MEDIA_ALLOW_PRIVATE` | `bool` | `false` | 图片代理是否允许访问内网/环回地址;除非确实要用它代理内网图床,否则保持关闭(见「安全提示」) |
|
|
109
|
+
| `BOTUI_FILE_PREVIEW` | `bool` | `true` | 是否允许在网页上在线预览收到的文件(文本 / 图片 / PDF / 音视频) |
|
|
110
|
+
| `BOTUI_FILE_MAX_BYTES` | `int` | `104857600` | 文件下载代理允许的最大体积(字节,默认 100 MB);`0` 表示不限制 |
|
|
111
|
+
| `BOTUI_MEDIA_ENABLED` | `bool` | `true` | 是否启用媒体库(下载收到的媒体、接收上传的附件);关闭后链接只能实时代理,失效即打不开 |
|
|
112
|
+
| `BOTUI_MEDIA_MAX_BYTES` | `int` | `536870912` | 媒体库占用上限(字节,默认 512 MB),超出后自动淘汰最久未用的资源;`0` 表示不按体积限制 |
|
|
113
|
+
| `BOTUI_MEDIA_MAX_FILES` | `int` | `20000` | 媒体库文件数量上限;`0` 表示不按数量限制(资源按内容去重,条数通常远小于消息数) |
|
|
114
|
+
| `BOTUI_MEDIA_FILE_MAX_BYTES` | `int` | `20971520` | 单个资源进入媒体库的最大体积(字节,默认 20 MB),超过的只代理不落盘;`0` 表示不限制 |
|
|
115
|
+
| `BOTUI_MEDIA_TTL` | `float` | `259200.0` | 未被聊天记录引用的资源保留时长(秒,默认 3 天),到期自动回收;`0` 表示不按时间清理 |
|
|
116
|
+
| `BOTUI_MEDIA_RETENTION` | `float` | `604800.0` | 已被聊天记录引用的资源保留时长(秒,默认 7 天);`0` 表示不限制 |
|
|
117
|
+
| `BOTUI_MEDIA_FETCH_TIMEOUT` | `float` | `20.0` | 下载单个媒体资源入库的超时时间(秒) |
|
|
118
|
+
| `BOTUI_MEDIA_MAX_BYTES_PER_MESSAGE` | `int` | `20971520` | 单条消息最多下载多少字节入库(默认 20 MB),避免一条消息把媒体库刷满;`0` 表示不限制 |
|
|
119
|
+
| `BOTUI_MEDIA_CLEANUP_INTERVAL` | `int` | `10` | 媒体库自动清理的间隔(分钟);`0` 表示只保留每 6 小时一次的消息记录清理 |
|
|
120
|
+
| `BOTUI_SEND_INTERVAL` | `float` | `1.0` | 同一会话两次发送之间的最小间隔(秒),用于限流 |
|
|
121
|
+
| `BOTUI_DEBUG` | `bool` | `false` | 打印更详细的调试日志 |
|
|
122
|
+
|
|
123
|
+
### 数据存放位置
|
|
124
|
+
|
|
125
|
+
数据库与令牌的路径**完全交给 `nonebot-plugin-localstore`**,插件只读取它给出的目录,不再提供自己的目录配置项:
|
|
126
|
+
|
|
127
|
+
```dotenv
|
|
128
|
+
# 全局改(所有插件生效)
|
|
129
|
+
LOCALSTORE_DATA_DIR=/srv/nonebot/data
|
|
130
|
+
|
|
131
|
+
# 只改本插件(推荐,用插件 ID 作 key)
|
|
132
|
+
LOCALSTORE_PLUGIN_DATA_DIR={"nonebot_plugin_botui": "/srv/botui/data"}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
默认位置参考 localstore 的规则:`LOCALSTORE_USE_CWD=true` 时为 `<运行目录>/data/nonebot_plugin_botui/`,否则为系统数据目录下的 `nonebot2/nonebot_plugin_botui/`。可以用 `nb localstore` 查看实际路径。目录下会生成:
|
|
136
|
+
|
|
137
|
+
- `botui.sqlite3`:消息、会话**与媒体元数据**(`blobs` 表)数据库(WAL 模式)
|
|
138
|
+
- `token.txt`:自动生成的访问令牌(权限 `0600`)
|
|
139
|
+
- `blobs/`:媒体库的正文 —— 收到的图片/文件、已发出的附件都在这里,布局为 `blobs/<md5前两位>/<md5>`(正文名就是内容的 MD5)。整目录可删:删掉只是丢掉本地副本,不影响聊天记录本身,下一次访问会重新去上游链接取(前提是链接还没失效)
|
|
140
|
+
|
|
141
|
+
> 路径在插件导入时解析一次并存为常量。localstore 是靠**调用栈**反查「哪个插件在要目录」的,只有栈上存在插件自己的帧时才能查到(从 `__main__`、别名模块名或外部库回调里现取会报 `Cannot detect caller plugin`)。导入时解析最稳妥,之后全项目只读常量。
|
|
142
|
+
|
|
143
|
+
## 🎉 命令
|
|
144
|
+
|
|
145
|
+
**没有命令。** 插件不注册任何 matcher,不 `require` 任何前缀,也不拦截聊天消息 ——
|
|
146
|
+
访问地址只在启动时打印到控制台日志(见「快速开始」)。
|
|
147
|
+
|
|
148
|
+
这样设计是为了避免和你的其他插件抢命令名:早期的版本注册过 `帮助` / `状态` /
|
|
149
|
+
`链接` / `统计` / `清理` / `重置` 这些又短又通用的别名,很容易和别人撞车。
|
|
150
|
+
|
|
151
|
+
清理旧记录不需要手动触发:插件每 6 小时会按 `BOTUI_MAX_RECORDS` 与
|
|
152
|
+
`BOTUI_RETENTION_DAYS` 自动执行一次。
|
|
153
|
+
|
|
154
|
+
## 🖥️ 界面说明
|
|
155
|
+
|
|
156
|
+
- **主题**:默认深色,右上角按钮可切换浅色,选择保存在 `localStorage`(`botui_theme`)。
|
|
157
|
+
- **机器人切换**:右上角下拉框列出**所有连接过**的机器人(在线的标绿点、离线的置灰,显示头像、名称、`self_id` 与适配器)。始终恰好选中一个机器人:进来时优先选中上次选过的(记录在 `localStorage` 的 `botui_bot`),否则选中第一个在线的。切换机器人会清空当前会话与消息列表,只显示该机器人的会话与记录;连接状态由服务端推送实时更新,选择的机器人离线时会自动挑选下一个在线的。
|
|
158
|
+
- **设置面板**:右上角齿轮打开,包含两部分。**新消息通知** —— 开关网页通知、提示音;开启时会请求浏览器授权,被拒绝或浏览器不支持时开关置灰并显示原因(偏好记在 `localStorage` 的 `botui_notify`)。**本地缓存** —— 显示占用、资源数、图片/文件分类占用与本进程的命中统计,并提供「刷新 / 清理未引用 / 清理图片 / 清理文件 / 清空媒体库」几个操作。清空需要连点两次确认(第一次点会变成「确认清空?」)。媒体库未开启时缓存区域的按钮全部置灰。
|
|
159
|
+
- **会话记忆**:每个机器人最近打开的会话会记在 `localStorage`(`botui_chat`,形如 `{self_id: chat_key}`),下次打开页面或切回该机器人时自动打开那个会话;若已不存在(被清理或换了机器人)则照常停留在会话列表。
|
|
160
|
+
- **备注(别名)**:适配器拿不到真实群名或昵称时,可以自己给它们起个认得出的名字。**会话**:会话头部会话名旁的铅笔按钮,或在左侧会话列表上**右键 → 设置备注**;设置后会话头部会以备注为名、下方显示「原名 <适配器给的名称>」。**成员**:在输入框 `@` 菜单里**右键某个成员**,或在聊天记录里**右键某条消息 → 备注发言人**。**机器人**:右上角机器人切换器菜单里每行右侧的铅笔按钮。备注优先作为显示名,参与搜索,且不会被适配器上报的名称覆盖;清空后即恢复显示适配器给的名称。
|
|
161
|
+
- **令牌页**:开启鉴权时,若浏览器里没有令牌(或令牌失效返回 401),会先弹出输入框;令牌只存在当前浏览器的 `localStorage` 中,不会回传服务器保存。
|
|
162
|
+
- **会话列表**:按最后消息时间倒序,显示头像(拉不到图就用名称首字生成彩色圆)、名称、最后一条消息摘要(机器人发出的以「我: 」开头)、时间(今天为 `HH:MM`,昨天为「昨天」,更早为 `MM-DD` / `YYYY-MM-DD`)与未读数(超过 99 显示 `99+`,未打开的会话显示「新」+ 摘要)。
|
|
163
|
+
- **搜索**:顶部搜索框输入后出现「会话 / 消息」两个标签。**会话**标签即时过滤会话名、会话 ID(群号/QQ 号)与最后一条消息;**消息**标签会请求 `/api/search` 做正文全文检索,结果按时间倒序展示并高亮关键词,点一条即可跳到对应会话并定位到该消息(不在已加载的一页时会提示)。
|
|
164
|
+
- **聊天记录**:左右分栏气泡,连续消息紧凑排版,按天插入「今天 / 昨天 / 5月1日 / 2024年5月1日」分隔;切换会话时先显示骨架占位;图片消息点击放大(灯箱,点击任意处或按 `Esc` 关闭),`@`、回复、表情、文件、语音、视频、卡片、合并转发等消息段有对应样式;向上滚动自动加载更早的消息,单次会话最多在前端保留 1200 条。
|
|
165
|
+
- **文件与预览**:文件消息显示为文件卡片,点击在弹窗内预览 —— 文本类(代码/日志/配置等)直接显示正文并在超过 512 KB 时提示截断,图片与 PDF 内联显示,音视频提供内联播放器,其它类型给出下载按钮(弹窗右上角也可下载/另存)。语音、音频、视频消息也都有内联播放器;图片与媒体统一走 `/api/media/<id>`(媒体库副本),文件走 `/api/file` 流式代理(带令牌与 SSRF 校验)。**若资源已在媒体库里(响应头带 `X-BotUI-Cache: hit`)**,`/api/file`、`/api/preview` 会直接读本地副本,不再联网。预览开关为 `BOTUI_FILE_PREVIEW`。
|
|
166
|
+
- **合并转发**:合并转发消息显示为可折叠的「聊天记录」卡片,默认收起,点击标题栏原地展开成一条条小聊天记录(不弹窗),逐条显示发送者昵称、时间与内容。节点优先取自记录里的内联数据,只有在缺内联数据时才向适配器请求,且只在首次展开时拉取。
|
|
167
|
+
- **实时刷新**:页面通过 WebSocket(`/api/ws`)接收服务端推送的事件,新消息实时置顶并提示「N 条新消息」,新会话自动进入列表,撤回事件会把气泡移除。连接失败/断开时按 1s ~ 30s 指数退避重连,并在左下角显示连接状态(令牌无效用 `4401`、来源被拒用 `4403`,分别对应弹令牌框 / 提示只允许本机);重连会带上最后收到的事件序号,服务端从环形缓冲里补发漏掉的那段;序号出现较大空洞或长时间没收到任何推送时会自动重新同步一次。不在底部时新消息不打扰阅读,只提示「N 条新消息」。
|
|
168
|
+
- **发送 / 回复 / @**:底部输入框 `Enter` 发送、`Shift+Enter` 换行;`@` 按钮打开**成员选择菜单**(关键词过滤、显示头像与群主/管理员标签),只能从「这个群里记录到的成员」里选,不再要求手输 QQ 号,选中的成员以「@昵称」标签形式展示、可逐个移除;右键消息选「回复」会在输入框上方显示引用条(含被引用者与原文摘要),发送时带上回复段。
|
|
169
|
+
- **发送图片 / 文件**:输入框左侧的 `+` 按钮打开文件选择框(可多选),也可以把文件**拖进页面任意位置**(会显示拖拽提示层),或在输入框里直接**粘贴**剪贴板里的图片(截图 → `Ctrl/Cmd+V` → 发送)。选中的附件会先上传到服务端并以缩略图(图片)或「类别 + 文件名 + 大小」标签排在输入框上方,可逐个 `✕` 移除。发送时可以只发附件不发文字。上传进度反映在发送按钮上(显示「上传中」),上传中的附件不能发送。
|
|
170
|
+
- **右键菜单**:`复制文本`、`回复`、`备注发言人`(给该消息的发送者设置备注,仅收到的消息可用)、`撤回`(仅机器人自己发出且拿到消息 ID 的消息可用,且需要 `BOTUI_ALLOW_RECALL=true`;不支持撤回的适配器会在调用时返回失败原因)、`预览文件`(文件消息可用)、`导出聊天记录`(导出当前会话为 JSON)、`复制消息ID`。左侧会话列表上右键则是 `设置备注`。
|
|
171
|
+
- **只读模式**:`BOTUI_WRITE_ENABLED=false` 时输入框、发送按钮、`@` 按钮与 `+` 附件按钮均被禁用,底部提示「只读模式」;备注编辑同样会被拒绝(返回 403)。
|
|
172
|
+
- **窄屏**:宽度小于 768px 时变成单栏,点消息头部返回按钮回到会话列表;输入框字号会自动提到 16px,避免 iOS 聚焦时把整页放大;同时按 `viewport-fit=cover` 处理了刘海屏安全区。
|
|
173
|
+
|
|
174
|
+
## 🔐 安全提示
|
|
175
|
+
|
|
176
|
+
- **默认开启令牌鉴权**(`BOTUI_AUTH=true`)。令牌要么由 `BOTUI_TOKEN` 指定,要么自动生成 24 字节随机串写入数据目录的 `token.txt`(权限 `0600`)。请勿把令牌或 `token.txt` 泄露出去。
|
|
177
|
+
- **令牌优先走请求头**。普通接口一律用 `X-BotUI-Token`;地址栏里的 `?token=` 只对**浏览器无法带自定义头**的路径放行(`/media`、`/api/media`、`/api/file`、`/api/preview`、`/api/export` 与 WebSocket `/api/ws`),写接口一律不接受它 —— 这样令牌不容易被反向代理访问日志、浏览器历史或第三方子资源请求带出去。鉴权失败次数过多会短时返回 429。
|
|
178
|
+
- **默认只允许本机访问**。服务端会拒绝非本机来源的请求(返回 403)。判断来源用的是 `ipaddress` 的 loopback 判定,所以 `127.0.0.1`、`127.0.0.2`、`::1`、`::ffff:127.0.0.1`、`localhost` 都算本机。要开放给外部,需要显式设置 **`BOTUI_ALLOW_REMOTE=true`**。
|
|
179
|
+
- **`Host` 头白名单(挡 DNS rebinding,按需启用)**。`BOTUI_ALLOWED_HOSTS` **留空时不做任何校验**(默认),这样用主机名访问本机(如 `http://my-nas:8080`)不会被打回。走**域名**(反向代理)部署时把域名写进去即启用白名单:本机主机名(`localhost`、`127.0.0.1`、`[::1]`)始终允许,其余 `Host` 一律拒绝。这样即便攻击者用「先解析到自己的服务器、再改解析到 `127.0.0.1`」的手法让来源变成环回,也会因为 `Host` 不对而被拒。
|
|
180
|
+
- **响应头加固**。BotUI 路由下的响应都会带上 `X-Content-Type-Options: nosniff`、`X-Frame-Options: DENY`、`Referrer-Policy: no-referrer` 与 `frame-ancestors 'none'` 的 CSP。`nosniff` 尤其重要:`/api/file` 会按上游给的 `content-type` 内联返回文件,没有它时浏览器可能对内容做嗅探。此外 `/api/meta` 会返回 `robots_tag`,前端据此插入 `<meta name="robots" content="noindex, nofollow">`,避免被搜索引擎收录。
|
|
181
|
+
- **`BOTUI_HOST` 只管链接显示**。它决定启动日志里给出的主机名(比如你走反向代理,就填域名),**不会**改变来源限制。这样把链接改成局域网地址或域名都不会意外放开访问;反过来也不会因为填了域名就把自己锁在外面。
|
|
182
|
+
- 若同时开启 `BOTUI_ALLOW_REMOTE=true` 与 `BOTUI_AUTH=false`,启动时会打印一条醒目警告 —— 这是「任何人只要能访问该地址就能查看全部记录并冒充机器人」的状态。
|
|
183
|
+
- **链接里带的是单一密钥**。`?token=xxx` 就是全部凭据:谁能拿到链接,谁就能查看全部聊天记录并以机器人的身份发消息、撤回消息。地址只出现在**服务端控制台日志**里(不会发到任何聊天窗口),请自行保管好日志输出。
|
|
184
|
+
- **`BOTUI_AUTH=false` 意味着完全开放**(在来源限制之内):任何能访问该地址的人都能查看记录、以机器人身份发言和撤回;启动日志里也会明确提示「令牌鉴权已关闭」。
|
|
185
|
+
- **图片代理**:`/media` 需要令牌,且**只代理公网地址**。它会把目标域名解析一遍,只要解析结果里有环回、内网、链路本地(含 `169.254.169.254` 这类云元数据地址)等非公网地址就拒绝,重定向每一跳都会重新校验,因此不能拿它当跳板去读内网服务。如果你的图床确实在内网,才需要打开 `BOTUI_MEDIA_ALLOW_PRIVATE=true`。
|
|
186
|
+
- **媒体库落在磁盘上**:收到的媒体会被下载、上传的附件会直接存入 localstore 的插件数据目录下的 `blobs/`,所以磁盘占用会随收发的媒体增长。除按 `BOTUI_MEDIA_MAX_BYTES` / `BOTUI_MEDIA_MAX_FILES` 自动淘汰外,还可在 WebUI 的「设置」面板手动清理。若不想让它落盘,设 `BOTUI_MEDIA_ENABLED=false` 即可(此时仍可代理,只是链接失效后就打不开了)。
|
|
187
|
+
- **文件代理**:`/api/file`(流式下载/预览)与 `/api/preview`(文本预览)同样需要令牌,并复用与图片代理一致的 **SSRF 校验**与重定向逐跳校验。下载默认限制 100 MB(`BOTUI_FILE_MAX_BYTES`,`0` 表示不限制),文本预览最多读取前 512 KB。若你的文件确实在内网,同样需要 `BOTUI_MEDIA_ALLOW_PRIVATE=true`。
|
|
188
|
+
- **附件上传**:`POST /api/upload`(暂存到系统临时目录)需要令牌且仅在**可写模式**下可用(`BOTUI_WRITE_ENABLED=false` 时直接 403),取回/删除时 `id` 经严格校验,只允许本插件生成的 id,`../` 之类的路径穿越会被拒绝;文件名会剥离路径与控制字符后才展示。单个附件默认上限 20 MB(`BOTUI_MEDIA_FILE_MAX_BYTES`)。已发送的附件在**发送时**按内容 MD5 正式进媒体库(未发送的暂存文件随进程结束、移除或超时被删掉),因此**请确保磁盘空间与访问权限可控**;`BOTUI_MEDIA_ENABLED=false` 可以整体关闭该功能。发出的附件与收到的媒体存在同一份库、同一个 `/api/media/<md5>` 地址下。
|
|
189
|
+
- **需要公网访问时**:建议放在反向代理之后,加 HTTPS 与额外的访问控制,并保持令牌鉴权开启;连同 `BOTUI_WRITE_ENABLED=false` 一起使用还可以只读地公开记录。
|
|
190
|
+
- 注意反代与机器人在**同一台机器**上时,请求来源就是 `127.0.0.1`,来源限制会自然放行 —— 此时**鉴权完全依赖令牌**,请务必保持 `BOTUI_AUTH=true` 并确认反代不会把 `?token=` 记进日志。
|
|
191
|
+
- 若反代在另一台机器上,才需要 `BOTUI_ALLOW_REMOTE=true`。
|
|
192
|
+
|
|
193
|
+
## 🌐 跨平台说明
|
|
194
|
+
|
|
195
|
+
- 用户与场景信息统一由 `nonebot-plugin-uninfo` 的 `Session` 提供,发送目标由 `nonebot-plugin-alconna` 的 `Target` 构造,因此插件本身不绑定任何适配器;`supported_adapters` 直接继承自这两个插件。
|
|
196
|
+
- **多机器人**:每个机器人(`self_id`)的会话彼此独立 —— 会话 key 采用 `self_id:kind_id` 的形式内嵌机器人 ID,同名群在 A、B 两个机器人下就是两条不同的会话;每条消息都带上产生它的适配器名与 `self_id`。WebUI 的发送只会用**会话自己的机器人**,它不在线时返回 503 而不会改用别的机器人。
|
|
197
|
+
- 只要适配器支持对应的能力就能用:私聊/群聊识别、消息段解析、以及通过 alconna 的 exporter 发送消息。召回(撤回)需要该适配器在 alconna 里实现了对应的 exporter,不支持时网页会提示「适配器 xxx 不支持撤回」。
|
|
198
|
+
- `BOTUI_EXCLUDE_ADAPTERS` 可以按适配器名(如 `["OneBot V12"]`)排除某些平台的记录。
|
|
199
|
+
|
|
200
|
+
## 🧪 开发 / 测试
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
uv sync # 安装依赖
|
|
204
|
+
uv run poe test # 跑测试(pytest + 覆盖率,产出 coverage.xml / junit.xml)
|
|
205
|
+
uv run ruff check . # lint
|
|
206
|
+
uv run ruff format . # 格式化
|
|
207
|
+
uv run poe bump patch # 版本号变更(bump-my-version)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## 📄 协议
|
|
211
|
+
|
|
212
|
+
MIT © 2026 Lonely-Sails
|