From 35014e4048dcd8a97865c06332d4a9ddb7fafa38 Mon Sep 17 00:00:00 2001 From: HibiKier <45528451+HibiKier@users.noreply.github.com> Date: Wed, 25 Dec 2024 12:03:49 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84webui=E9=80=82=E9=85=8D=20(#1?= =?UTF-8?q?801)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ 使用Uninfo重构PlatformUtils基础方法 * 🩹 优化插件加载与模块格式转换逻辑 * 🚑 修复商店道具无法使用 * 🚑 修复道具无法正常使用 * 🔧 增加Bot状态管理及模块禁用功能 * 🎨 优化Web UI代码结构,修改target方法 * :rotating_light: auto fix by pre-commit hooks * 🎨 添加菜单API及优化异常处理 * 🐛 优化菜单API及模型结构,修复WebUi插件列表Api * :memo: 更新仓库readme * :rotating_light: add mdlint file * :memo: Add help chapter. * 🐛 修复优化AuthChecker逻辑 * 🐛 优化数据库API,移除冗余导入及修正SQL_DICT引用 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: BalconyJH --- .markdownlint.yaml | 4 + README.md | 830 +++++------------- resources/template/check/main.css | 2 +- zhenxun/builtin_plugins/__init__.py | 8 - .../builtin_plugins/hooks/_auth_checker.py | 4 +- zhenxun/builtin_plugins/init/init_plugin.py | 6 +- zhenxun/builtin_plugins/restart/__init__.py | 2 +- .../builtin_plugins/scheduler/chat_check.py | 3 +- zhenxun/builtin_plugins/shop/__init__.py | 4 +- .../superuser/broadcast/_data_source.py | 2 +- zhenxun/builtin_plugins/web_ui/__init__.py | 6 +- .../builtin_plugins/web_ui/api/__init__.py | 3 +- .../web_ui/api/menu/__init__.py | 26 + .../web_ui/api/menu/data_source.py | 64 ++ .../builtin_plugins/web_ui/api/menu/model.py | 21 + .../web_ui/api/tabs/dashboard/__init__.py | 149 +--- .../web_ui/api/tabs/dashboard/data_source.py | 169 +++- .../web_ui/api/tabs/database/__init__.py | 143 +-- .../web_ui/api/tabs/database/data_source.py | 90 ++ .../web_ui/api/tabs/main/__init__.py | 311 +++---- .../web_ui/api/tabs/main/data_source.py | 337 ++++++- .../web_ui/api/tabs/main/model.py | 46 +- .../web_ui/api/tabs/manage/__init__.py | 452 +++------- .../web_ui/api/tabs/manage/chat.py | 19 +- .../web_ui/api/tabs/manage/data_source.py | 274 ++++++ .../web_ui/api/tabs/manage/model.py | 2 +- .../web_ui/api/tabs/plugin_manage/__init__.py | 198 ++--- .../api/tabs/plugin_manage/data_source.py | 152 ++++ zhenxun/builtin_plugins/web_ui/base_model.py | 2 +- zhenxun/services/__init__.py | 8 + zhenxun/utils/common_utils.py | 30 + zhenxun/utils/platform.py | 326 +++---- 32 files changed, 1939 insertions(+), 1754 deletions(-) create mode 100644 .markdownlint.yaml create mode 100644 zhenxun/builtin_plugins/web_ui/api/menu/__init__.py create mode 100644 zhenxun/builtin_plugins/web_ui/api/menu/data_source.py create mode 100644 zhenxun/builtin_plugins/web_ui/api/menu/model.py create mode 100644 zhenxun/builtin_plugins/web_ui/api/tabs/database/data_source.py create mode 100644 zhenxun/builtin_plugins/web_ui/api/tabs/manage/data_source.py create mode 100644 zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/data_source.py diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 00000000..d2c1095d --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,4 @@ +MD013: false +MD024: # 重复标题 + siblings_only: true +MD033: false # 允许 html \ No newline at end of file diff --git a/README.md b/README.md index ad8f1491..59db485c 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,43 @@ +
- +zhenxun_bot
- -![python](https://img.shields.io/badge/python-v3.9%2B-blue) -![nonebot](https://img.shields.io/badge/nonebot-v2.1.3-yellow) -![onebot](https://img.shields.io/badge/onebot-v11-black) - + + license + + + python + + + nonebot + + + onebot + + + onebot + + + QQ + + + black + + + pyright + + + ruff +
-[![license](https://img.shields.io/badge/license-AGPL3.0-FE7D37)](https://github.com/HibiKier/zhenxun_bot/blob/main/LICENSE) -[![tencent-qq](https://img.shields.io/badge/%E7%BE%A4-是真寻酱哒-red?style=logo=tencent-qq)](https://jq.qq.com/?_wv=1027&k=u8PgBkMZ) +[![tencent-qq](https://img.shields.io/badge/%E7%BE%A4-是真寻酱哒-red?style=logo=tencent-qq)](https://qm.qq.com/q/mRNtLSl6uc) [![tencent-qq](https://img.shields.io/badge/%E7%BE%A4-真寻的技术群-c73e7e?style=logo=tencent-qq)](https://qm.qq.com/q/YYYt5rkMYc)
@@ -36,7 +58,7 @@ “真寻是[椛椛](https://github.com/FloatTech/ZeroBot-Plugin)的好朋友!” -:tada:喜欢真寻,于是真寻就来了!:tada: +🎉喜欢真寻,于是真寻就来了!🎉 本项目符合 [OneBot](https://github.com/howmanybots/onebot) 标准,可基于以下项目与机器人框架/平台进行交互 @@ -50,28 +72,27 @@
-![Star Trend](https://api.star-history.com/svg?repos=HibiKier/zhenxun_bot&type=Timeline) +Star Trend
-## 真寻觉得你需要帮助 +## 🤝 帮助页面 -
+
+点击展开查看图片 + zhenxun_help + html_help + help +
- - - - -
- -## 这是一份扩展 +## 📦 这是一份扩展 ### 1. 体验一下? 这是一个免费的,版本为 dev 的 zhenxun,你可以通过 [napcat](https://github.com/NapNeko/NapCatQQ) 或 [拉格朗日](https://github.com/LagrangeDev/Lagrange.Core) 以及 [matcha](https://github.com/A-kirami/matcha) 等直接连接用于体验与测试 (球球了测试君!) -``` +```text Url: ws://test.zhenxun.org:8080/onebot/v11/ws AccessToken: PUBLIC_ZHENXUN_TEST @@ -89,39 +110,23 @@ AccessToken: PUBLIC_ZHENXUN_TEST | [插件库](https://github.com/zhenxun-org/zhenxun_bot_plugins) | 插件 | [zhenxun-org](https://github.com/zhenxun-org) | 原 plugins 文件夹插件 | | [插件索引库](https://github.com/zhenxun-org/zhenxun_bot_plugins_index) | 插件 | [zhenxun-org](https://github.com/zhenxun-org) | 扩展插件索引库 | | [一键安装](https://github.com/soloxiaoye2022/zhenxun_bot-deploy) | 安装 | [soloxiaoye2022](https://github.com/soloxiaoye2022) | 第三方 | -| [WebUi](https://github.com/HibiKier/zhenxun_bot_webui) | 管理 | [hibikier](https://github.com/HibiKier) | 基于真寻 WebApi 的 webui 实现 | +| [WebUi](https://github.com/HibiKier/zhenxun_bot_webui) | 管理 | [hibikier](https://github.com/HibiKier) | 基于真寻 WebApi 的 webui 实现 [预览](#-webui界面展示) | | [安卓 app(WebUi)](https://github.com/YuS1aN/zhenxun_bot_android_ui) | 安装 | [YuS1aN](https://github.com/YuS1aN) | 第三方 | -
- WebUI 后台示例图 - -![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui00.png) -![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui01.png) -![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui02.png) -![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui03.png) -![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui04.png) -![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui05.png) -![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui06.png) -![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui07.png) - -
- -
- -## ~~来点优点?~~ 可爱难道还不够吗 +## 🥰 ~~来点优点?~~ 可爱难道还不够吗 - 实现了许多功能,且提供了大量功能管理命令,进行了多平台适配,兼容 nb2 商店插件 - 拥有完善可用的 webui - 通过 Config 配置项将所有插件配置统计保存至 config.yaml,利于统一用户修改 - 方便增删插件,原生 nonebot2 matcher,不需要额外修改,仅仅通过简单的配置属性就可以生成`帮助图片`和`帮助信息` - 提供了 cd,阻塞,每日次数等限制,仅仅通过简单的属性就可以生成一个限制,例如:`PluginCdBlock` 等 -- **..... 更多详细请通过[[传送门](https://hibikier.github.io/zhenxun_bot/)]查看文档!** +- **更多详细请通过 [传送门](https://hibikier.github.io/zhenxun_bot/) 查看文档!** -## 简单部署 +## 🛠️ 简单部署 -``` +```bash # 获取代码 git clone https://github.com/HibiKier/zhenxun_bot.git @@ -134,636 +139,154 @@ poetry install # 安装依赖 # 开始运行 poetry shell # 进入虚拟环境 -python bot.py - -# 首次后会在data目录下生成config.yaml文件 -# config.yaml用户配置插件 +python bot.py # 运行机器人 ``` -## 简单配置 +## 📝 简单配置 -``` -1.在.env.dev文件中 +> [!TIP] +> config.yaml 需要启动一次 Bot 后生成 - SUPERUSERS = [""] # 填写你的QQ +1.在 .env.dev 文件中填写你的机器人配置项 - PLATFORM_SUPERUSERS = ' - { - "qq": [""], # 在此处填写你的qq - "dodo": [], - "kaiheila": [], - "discord": [] - } - ' - # 此处填写你的数据库地址 - # 示例: "postgres://user:password@127.0.0.1:5432/database" - # 示例: "mysql://user:password@127.0.0.1:5432/database" - # 示例: "sqlite:data/db/zhenxun.db" 在data目录下建立db文件夹 - DB_URL = "" # 数据库地址 +2.在 configs/config.yaml 文件中修改你需要修改的插件配置项 +
+数据库地址(DB_URL)配置说明 -2.在configs/config.yaml文件中 # 该文件需要启动一次后生成 - * 修改插件配置项 +DB_URL 是基于 Tortoise ORM 的数据库连接字符串,用于指定项目所使用的数据库。以下是 DB_URL 的组成部分以及示例: -``` +格式为: ```<数据库类型>://<用户名>:<密码>@<主机>:<端口>/<数据库名>?<参数>``` -## 功能列表 +- 数据库类型:表示数据库类型,例如 postgres、mysql、sqlite 等。 +- 用户名:数据库的用户名,例如 root。 +- 密码:数据库的密码,例如 123456。 +- 主机:数据库的主机地址,例如 127.0.0.1(本地)或远程服务器 IP。 +- 端口:数据库的端口号,例如:PostgreSQL:5432, MySQL:3306 +- 数据库名:指定要使用的数据库名称,例如 zhenxun。 +- 参数(可选):用于传递额外的配置,例如字符集设置。 + +
+ +## 📋 功能列表 + +> [!NOTE] +> 真寻原 `plugins` 插件文件夹已迁移至 [插件仓库](https://github.com/zhenxun-org/zhenxun_bot_plugins) ,现在本体仅保留核心功能
内置功能 -**真寻原 `plugins` 插件文件夹已迁移至其他仓库,当前内置仅保留必要的功能** +### 🔧 基础功能 -### 基础功能 +- 昵称系统(群与群与私聊分开) +- 签到/我的签到/好感度排行/好感度总排行(影响色图概率和开箱次数,支持配置) +- 商店/我的金币/购买道具/使用道具/金币排行(完整的商店添加/购买/使用流程) +- 查看当前群欢迎消息 +- 个人信息查看(群组内权限,聊天频率等) +- 消息撤回 +- 功能统计可视化 +- 关于 +- 三种样式的帮助菜单 -- [x] 昵称系统(群与群与私聊分开) -- [x] 签到/我的签到/好感度排行/好感度总排行(影响色图概率和开箱次数,支持配置) -- [x] 商店/我的金币/购买道具/使用道具/金币排行(完整的商店添加/购买/使用流程) -- [x] 查看当前群欢迎消息 -- [x] 个人信息查看(群组内权限,聊天频率等) -- [x] 消息撤回 -- [x] 功能统计可视化 -- [x] 关于 -- [x] 三种样式的帮助菜单 +### 🛠️ 管理员功能 -### 管理员功能 +- 管理员帮助 +- 更新群组成员信息 +- 95%的群功能开关 +- 查看群内被动技能状态 +- 自定义群欢迎消息(是真寻的不是管家的!) +- ban/unban(支持设置 ban 时长)= 群组及用户的黑名单 +- 休息吧/醒来(群组内真寻状态) -- [x] 管理员帮助 -- [x] 更新群组成员信息 -- [x] 95%的群功能开关 -- [x] 查看群内被动技能状态 -- [x] 自定义群欢迎消息(是真寻的不是管家的!) -- [x] ban/unban(支持设置 ban 时长)= 群组及用户的黑名单 -- [x] 休息吧/醒来(群组内真寻状态) +### 🧑‍💼 超级用户功能 -### 超级用户功能 +- 超级用户帮助 +- 添加/删除权限(是真寻的管理员权限,不是群管理员) +- 群组管理,退群指令等 +- 广播 +- 自检(检查系统状态) +- 所有群组/所有好友 +- 退出指定群 +- 更新好友信息/更新群信息 +- 修改群权限 +- 检查更新 +- 重启 +- 添加/删除/查看群白名单 +- 功能开关(更多设置) +- 功能状态 +- 执行 SQL +- 重载配置 +- 清理临时数据 +- 增删群认证 +- 同意/拒绝好友/群聊请求 +- 添加/移除/更新插件/插件商店(plugins 库以及扩展库) +- WebUI API(对真寻前端的支持) -- [x] 超级用户帮助 -- [x] 添加/删除权限(是真寻的管理员权限,不是群管理员) -- [x] 群组管理,退群指令等 -- [x] 广播 -- [x] 自检(检查系统状态) -- [x] 所有群组/所有好友 -- [x] 退出指定群 -- [x] 更新好友信息/更新群信息 -- [x] 修改群权限 -- [x] 检查更新 -- [x] 重启 -- [x] 添加/删除/查看群白名单 -- [x] 功能开关(更多设置) -- [x] 功能状态 -- [x] 执行 SQL -- [x] 重载配置 -- [x] 清理临时数据 -- [x] 增删群认证 -- [x] 同意/拒绝好友/群聊请求 -- [x] 添加/移除/更新插件/插件商店(plugins 库以及扩展库) -- [x] WebUI API(对真寻前端的支持) +#### 🛡️ 超级用户的被动技能 -#### 超级用户的被动技能 +- 邀请入群提醒(别人邀请真寻入群,可配置自动同意) -- [x] 邀请入群提醒(别人邀请真寻入群,可配置自动同意) +- 添加好友提醒(别人添加真寻好友,可配置自动同意) -- [x] 添加好友提醒(别人添加真寻好友,可配置自动同意) +### 🤖 被动技能 -### 被动技能 +- 群早晚安 -- [x] 群早晚安 +### 👻 看不见的技能 -### 看不见的技能 - -- [x] 功能调用统计 -- [x] 聊天记录统计 -- [x] 检测恶意触发命令(将被最高权限 ban 掉 30 分钟,只有最高权限(9 级)可以进行 unban) -- [x] 自动同意好友/群组请求,加群请求将会提醒管理员,退群提示,加群欢迎等等 -- [x] 群聊时间检测(当群聊最后一人发言时间大于当前 48 小时后将关闭该群所有通知(即被动技能)) -- [x] 群管理员监控,自动为新晋管理员增加权限,为失去群管理员的用户删除权限 -- [x] 群权限系统 -- [x] 定时更新权限 -- [x] 自动配置重载 -- [x] 强制入群保护 -- [x] 自定备份(可配置) -- [x] 笨蛋检测(当使用功能名称当指令时真寻会跳出来狠狠嘲笑并帮助) - -### 更多插件 - -- [更多插件](https://github.com/zhenxun-org/zhenxun_bot_plugins) - -- [第三方插件索引库](https://github.com/zhenxun-org/zhenxun_bot_plugins_index) +- 功能调用统计 +- 聊天记录统计 +- 检测恶意触发命令(将被最高权限 ban 掉 30 分钟,只有最高权限(9 级)可以进行 unban) +- 自动同意好友/群组请求,加群请求将会提醒管理员,退群提示,加群欢迎等等 +- 群聊时间检测(当群聊最后一人发言时间大于当前 48 小时后将关闭该群所有通知(即被动技能)) +- 群管理员监控,自动为新晋管理员增加权限,为失去群管理员的用户删除权限 +- 群权限系统 +- 定时更新权限 +- 自动配置重载 +- 强制入群保护 +- 自定备份(可配置) +- 笨蛋检测(当使用功能名称当指令时真寻会跳出来狠狠嘲笑并帮助)
-## [爱发电](https://afdian.com/a/HibiKier) +## 💖 赞助
-爱发电 以及 感谢投喂 - +爱发电 + + + +
-### 感谢名单 +### 赞助名单 (可以告诉我你的 **github** 地址,我偷偷换掉 0v|) -[shenqi](https://afdian.net/u/fa923a8cfe3d11eba61752540025c377) -[A_Kyuu](https://afdian.net/u/b83954fc2c1211eba9eb52540025c377) -[疯狂混沌](https://afdian.net/u/789a2f9200cd11edb38352540025c377) -[投冥](https://afdian.net/a/144514mm) -[茶喵](https://afdian.net/u/fd22382eac4d11ecbfc652540025c377) -[AemokpaTNR](https://afdian.net/u/1169bb8c8a9611edb0c152540025c377) -[爱发电用户\_wrxn](https://afdian.net/u/4aa03d20db4311ecb1e752540025c377) -[qqw](https://afdian.net/u/b71db4e2cc3e11ebb76652540025c377) -[溫一壺月光下酒](https://afdian.net/u/ad667a5c650c11ed89bf52540025c377) -[伝木](https://afdian.net/u/246b80683f9511edba7552540025c377) -[阿奎](https://afdian.net/u/da41f72845d511ed930d52540025c377) -[醉梦尘逸](https://afdian.net/u/bc11d2683cd011ed99b552540025c377) -[Abc](https://afdian.net/u/870dc10a3cd311ed828852540025c377) -[本喵无敌哒](https://afdian.net/u/dffaa9005bc911ebb69b52540025c377) -[椎名冬羽](https://afdian.net/u/ca1ebd64395e11ed81b452540025c377) -[kaito](https://afdian.net/u/a055e20a498811eab1f052540025c377) -[笑柒 XIAO_Q7](https://afdian.net/u/4696db5c529111ec84ea52540025c377) -[请问一份爱多少钱](https://afdian.net/u/f57ef6602dbd11ed977f52540025c377) -[咸鱼鱼鱼鱼](https://afdian.net/u/8e39b9a400e011ed9f4a52540025c377) -[Kafka](https://afdian.net/u/41d66798ef6911ecbc5952540025c377) -[墨然](https://afdian.net/u/8aa5874a644d11eb8a6752540025c377) -[爱发电用户\_T9e4](https://afdian.net/u/2ad1bb82f3a711eca22852540025c377) -[笑柒 XIAO_Q7](https://afdian.net/u/4696db5c529111ec84ea52540025c377) -[noahzark](https://afdian.net/a/noahzark) -[腊条](https://afdian.net/u/f739c4d69eca11eba94b52540025c377) -[ze roller](https://afdian.net/u/0e599e96257211ed805152540025c377) -[爱发电用户\_4jrf](https://afdian.net/u/6b2cdcc817c611ed949152540025c377) -[爱发电用户\_TBsd](https://afdian.net/u/db638b60217911ed9efd52540025c377) -[烟寒若雨](https://afdian.net/u/067bd2161eec11eda62b52540025c377) -[ln](https://afdian.net/u/b51914ba1c6611ed8a4e52540025c377) -[爱发电用户\_b9S4](https://afdian.net/u/3d8f30581a2911edba6d52540025c377) -[爱发电用户\_c58s](https://afdian.net/u/a6ad8dda195e11ed9a4152540025c377) -[爱发电用户\_eNr9](https://afdian.net/u/05fdb41c0c9a11ed814952540025c377) -[MangataAkihi](https://github.com/Sakuracio) -[炀](https://afdian.net/u/69b76e9ec77b11ec874f52540025c377) -[爱发电用户\_Bc6j](https://afdian.net/u/8546be24f44111eca64052540025c377) -[大魔王](https://github.com/xipesoy) -[CopilotLaLaLa](https://github.com/CopilotLaLaLa) -[嘿小欧](https://afdian.net/u/daa4bec4f24911ec82e552540025c377) -[回忆的秋千](https://afdian.net/u/e315d9c6f14f11ecbeef52540025c377) -[十年くん](https://github.com/shinianj) -[哇](https://afdian.net/u/9b266244f23911eca19052540025c377) -[yajiwa](https://github.com/yajiwa) -[爆金币](https://afdian.net/u/0d78879ef23711ecb22452540025c377) -... +[shenqi](https://afdian.net/u/fa923a8cfe3d11eba61752540025c377) [A_Kyuu](https://afdian.net/u/b83954fc2c1211eba9eb52540025c377) [疯狂混沌](https://afdian.net/u/789a2f9200cd11edb38352540025c377) [投冥](https://afdian.net/a/144514mm) [茶喵](https://afdian.net/u/fd22382eac4d11ecbfc652540025c377) [AemokpaTNR](https://afdian.net/u/1169bb8c8a9611edb0c152540025c377) [爱发电用户\_wrxn](https://afdian.net/u/4aa03d20db4311ecb1e752540025c377) [qqw](https://afdian.net/u/b71db4e2cc3e11ebb76652540025c377) [溫一壺月光下酒](https://afdian.net/u/ad667a5c650c11ed89bf52540025c377) [伝木](https://afdian.net/u/246b80683f9511edba7552540025c377) [阿奎](https://afdian.net/u/da41f72845d511ed930d52540025c377) [醉梦尘逸](https://afdian.net/u/bc11d2683cd011ed99b552540025c377) [Abc](https://afdian.net/u/870dc10a3cd311ed828852540025c377) [本喵无敌哒](https://afdian.net/u/dffaa9005bc911ebb69b52540025c377) [椎名冬羽](https://afdian.net/u/ca1ebd64395e11ed81b452540025c377) [kaito](https://afdian.net/u/a055e20a498811eab1f052540025c377) [笑柒 XIAO_Q7](https://afdian.net/u/4696db5c529111ec84ea52540025c377) [请问一份爱多少钱](https://afdian.net/u/f57ef6602dbd11ed977f52540025c377) [咸鱼鱼鱼鱼](https://afdian.net/u/8e39b9a400e011ed9f4a52540025c377) [Kafka](https://afdian.net/u/41d66798ef6911ecbc5952540025c377) [墨然](https://afdian.net/u/8aa5874a644d11eb8a6752540025c377) [爱发电用户\_T9e4](https://afdian.net/u/2ad1bb82f3a711eca22852540025c377) [笑柒 XIAO_Q7](https://afdian.net/u/4696db5c529111ec84ea52540025c377) [noahzark](https://afdian.net/a/noahzark) [腊条](https://afdian.net/u/f739c4d69eca11eba94b52540025c377) [ze roller](https://afdian.net/u/0e599e96257211ed805152540025c377) [爱发电用户\_4jrf](https://afdian.net/u/6b2cdcc817c611ed949152540025c377) [爱发电用户\_TBsd](https://afdian.net/u/db638b60217911ed9efd52540025c377) [烟寒若雨](https://afdian.net/u/067bd2161eec11eda62b52540025c377) [ln](https://afdian.net/u/b51914ba1c6611ed8a4e52540025c377) [爱发电用户\_b9S4](https://afdian.net/u/3d8f30581a2911edba6d52540025c377) [爱发电用户\_c58s](https://afdian.net/u/a6ad8dda195e11ed9a4152540025c377) [爱发电用户\_eNr9](https://afdian.net/u/05fdb41c0c9a11ed814952540025c377) [MangataAkihi](https://github.com/Sakuracio) [炀](https://afdian.net/u/69b76e9ec77b11ec874f52540025c377) [爱发电用户\_Bc6j](https://afdian.net/u/8546be24f44111eca64052540025c377) [大魔王](https://github.com/xipesoy) [CopilotLaLaLa](https://github.com/CopilotLaLaLa) [嘿小欧](https://afdian.net/u/daa4bec4f24911ec82e552540025c377) [回忆的秋千](https://afdian.net/u/e315d9c6f14f11ecbeef52540025c377) [十年くん](https://github.com/shinianj) [哇](https://afdian.net/u/9b266244f23911eca19052540025c377) [yajiwa](https://github.com/yajiwa) [爆金币](https://afdian.net/u/0d78879ef23711ecb22452540025c377)... - +## 📜 贡献指南 - +> [!TIP] +> 发起 [issue](https://github.com/HibiKier/zhenxun_bot/issues/new/choose) 前,我们希望你能够阅读过或者了解 [提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md) +> +> - 善用[搜索引擎](https://www.google.com/) +> - 查阅 issue 中是否有类似问题,如果没有请按照模板发起 issue - - -
- -**..... 更多更新信息请查看文档** - -## Todo - -- [x] web 管理 - -## **特别感谢** +## 🌟 特别感谢 首席设计师:[酥酥/coldly-ss](https://github.com/coldly-ss) -## 感谢 +## 🙏 感谢 [botuniverse / onebot](https://github.com/botuniverse/onebot) :超棒的机器人协议 [Mrs4s / go-cqhttp](https://github.com/Mrs4s/go-cqhttp) :cqhttp 的 golang 实现,轻量、原生跨平台. @@ -778,3 +301,58 @@ python bot.py [Kyomotoi / AnimeThesaurus](https://github.com/Kyomotoi/AnimeThesaurus) :一个~~特二刺螈~~(文爱)的适用于任何 bot 的词库 [Ailitonia / omega-miya](https://github.com/Ailitonia/omega-miya) :基于 nonebot2 的 qq 机器人 [KimigaiiWuyi / GenshinUID](https://github.com/KimigaiiWuyi/GenshinUID) :一个基于 HoshinoBot/NoneBot2 的原神 UID 查询插件 + +## 📊 统计与活跃贡献者 + + + + + Performance Stats of HibiKier/zhenxun_bot - Last 28 days + + + + + + Active Contributors of HibiKier/zhenxun_bot - Last 28 days + + + +## 👨‍💻 开发者 + +感谢以下开发者对 绪山真寻 Bot 作出的贡献: + + + contributors + + +## 📸 WebUI界面展示 + +
+
+ webui00 +
+
+ webui01 +
+ +
+ webui02 +
+
+ webui03 +
+ +
+ webui04 +
+
+ webui05 +
+ +
+ webui06 +
+
+ webui07 +
+
diff --git a/resources/template/check/main.css b/resources/template/check/main.css index f6790712..645efa15 100644 --- a/resources/template/check/main.css +++ b/resources/template/check/main.css @@ -44,7 +44,7 @@ body { } .main { - height: 448px; + height: 444px; width: 335px; padding: 0 30px; position: relative; diff --git a/zhenxun/builtin_plugins/__init__.py b/zhenxun/builtin_plugins/__init__.py index bead7254..aaa8306d 100644 --- a/zhenxun/builtin_plugins/__init__.py +++ b/zhenxun/builtin_plugins/__init__.py @@ -2,20 +2,12 @@ from datetime import datetime import uuid import nonebot -from nonebot import require from nonebot.adapters import Bot from nonebot.drivers import Driver from tortoise import Tortoise from tortoise.exceptions import OperationalError import ujson as json -require("nonebot_plugin_apscheduler") -require("nonebot_plugin_alconna") -require("nonebot_plugin_session") -require("nonebot_plugin_userinfo") -require("nonebot_plugin_htmlrender") -# require("nonebot_plugin_uninfo") - from zhenxun.models.bot_connect_log import BotConnectLog from zhenxun.models.bot_console import BotConsole from zhenxun.models.goods_info import GoodsInfo diff --git a/zhenxun/builtin_plugins/hooks/_auth_checker.py b/zhenxun/builtin_plugins/hooks/_auth_checker.py index 7e2e179c..2276a8fa 100644 --- a/zhenxun/builtin_plugins/hooks/_auth_checker.py +++ b/zhenxun/builtin_plugins/hooks/_auth_checker.py @@ -387,7 +387,7 @@ class AuthChecker: session=session, ) raise IgnoredException("该群未开启此功能...") - if not plugin.status and plugin.block_type == BlockType.GROUP: + if plugin.block_type == BlockType.GROUP: """全局群组禁用""" try: if self.is_send_limit_message(plugin, sid) and not is_poke: @@ -410,7 +410,7 @@ class AuthChecker: raise IgnoredException("该插件在群组中已被禁用...") else: sid = user_id - if not plugin.status and plugin.block_type == BlockType.PRIVATE: + if plugin.block_type == BlockType.PRIVATE: """全局私聊禁用""" try: if self.is_send_limit_message(plugin, sid) and not is_poke: diff --git a/zhenxun/builtin_plugins/init/init_plugin.py b/zhenxun/builtin_plugins/init/init_plugin.py index ffcc8b4d..366df312 100644 --- a/zhenxun/builtin_plugins/init/init_plugin.py +++ b/zhenxun/builtin_plugins/init/init_plugin.py @@ -120,7 +120,7 @@ async def _(): if module_list := await PluginInfo.all().values("id", "module_path"): module2id = {m["module_path"]: m["id"] for m in module_list} for plugin in get_loaded_plugins(): - load_plugin.append(plugin.name) + load_plugin.append(plugin.module_name) await _handle_setting(plugin, plugin_list, limit_list, task_list) create_list = [] update_list = [] @@ -198,8 +198,8 @@ async def _(): 10, ) await data_migration() - await PluginInfo.filter(module__in=load_plugin).update(load_status=True) - await PluginInfo.filter(module__not_in=load_plugin).update(load_status=False) + await PluginInfo.filter(module_path__in=load_plugin).update(load_status=True) + await PluginInfo.filter(module_path__not_in=load_plugin).update(load_status=False) manager.init() if limit_list: for limit in limit_list: diff --git a/zhenxun/builtin_plugins/restart/__init__.py b/zhenxun/builtin_plugins/restart/__init__.py index 314404c3..b24a2345 100644 --- a/zhenxun/builtin_plugins/restart/__init__.py +++ b/zhenxun/builtin_plugins/restart/__init__.py @@ -89,7 +89,7 @@ async def _(bot: Bot): async with aiofiles.open(RESTART_MARK, encoding="utf8") as f: bot_id, user_id = (await f.read()).split() if bot := nonebot.get_bot(bot_id): - if target := PlatformUtils.get_target(bot, user_id): + if target := PlatformUtils.get_target(user_id=user_id): await MessageUtils.build_message( f"{BotConfig.self_nickname}已成功重启!" ).send(target, bot=bot) diff --git a/zhenxun/builtin_plugins/scheduler/chat_check.py b/zhenxun/builtin_plugins/scheduler/chat_check.py index 4c30ffe2..d7559665 100644 --- a/zhenxun/builtin_plugins/scheduler/chat_check.py +++ b/zhenxun/builtin_plugins/scheduler/chat_check.py @@ -37,8 +37,7 @@ async def _(): update_list = [] if modules := await TaskInfo.annotate().values_list("module", flat=True): for bot in nonebot.get_bots().values(): - group_list, _ = await PlatformUtils.get_group_list(bot) - group_list = [g for g in group_list if g.channel_id is None] + group_list, _ = await PlatformUtils.get_group_list(bot, True) for group in group_list: try: last_message = ( diff --git a/zhenxun/builtin_plugins/shop/__init__.py b/zhenxun/builtin_plugins/shop/__init__.py index 104ae88e..bc663e28 100644 --- a/zhenxun/builtin_plugins/shop/__init__.py +++ b/zhenxun/builtin_plugins/shop/__init__.py @@ -126,7 +126,9 @@ async def _(session: Uninfo, arparma: Arparma): async def _(session: Uninfo, arparma: Arparma, nickname: str = UserName()): logger.info("查看道具", arparma.header_result, session=session) if image := await ShopManage.my_props( - session.user.id, nickname, PlatformUtils.get_platform(session) + session.user.id, + nickname, + PlatformUtils.get_platform(session), ): await MessageUtils.build_message(image.pic2bytes()).finish(reply_to=True) return await MessageUtils.build_message("你的道具为空捏...").send(reply_to=True) diff --git a/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py b/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py index 195fc429..540eaeff 100644 --- a/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py +++ b/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py @@ -47,7 +47,7 @@ class BroadcastManage: group.group_id, ): target = PlatformUtils.get_target( - bot, None, group.channel_id or group.group_id + group_id=group.group_id, channel_id=group.channel_id ) if target: await MessageUtils.build_message(message_list).send( diff --git a/zhenxun/builtin_plugins/web_ui/__init__.py b/zhenxun/builtin_plugins/web_ui/__init__.py index bbea8708..afa325f9 100644 --- a/zhenxun/builtin_plugins/web_ui/__init__.py +++ b/zhenxun/builtin_plugins/web_ui/__init__.py @@ -13,6 +13,7 @@ from zhenxun.utils.enum import PluginType from .api.logs import router as ws_log_routes from .api.logs.log_manager import LOG_STORAGE +from .api.menu import router as menu_router from .api.tabs.dashboard import router as dashboard_router from .api.tabs.database import router as database_router from .api.tabs.main import router as main_router @@ -80,6 +81,7 @@ BaseApiRouter.include_router(manage_router) BaseApiRouter.include_router(database_router) BaseApiRouter.include_router(plugin_router) BaseApiRouter.include_router(system_router) +BaseApiRouter.include_router(menu_router) WsApiRouter = APIRouter(prefix="/zhenxun/socket") @@ -112,6 +114,6 @@ async def _(): app.include_router(BaseApiRouter) app.include_router(WsApiRouter) await init_public(app) - logger.info("API启动成功", "Web UI") + logger.info("API启动成功", "WebUi") except Exception as e: - logger.error("API启动失败", "Web UI", e=e) + logger.error("API启动失败", "WebUi", e=e) diff --git a/zhenxun/builtin_plugins/web_ui/api/__init__.py b/zhenxun/builtin_plugins/web_ui/api/__init__.py index de9b3798..3608647c 100644 --- a/zhenxun/builtin_plugins/web_ui/api/__init__.py +++ b/zhenxun/builtin_plugins/web_ui/api/__init__.py @@ -1 +1,2 @@ -from .tabs import * # noqa: F403 +from .menu import * # noqa: F403f +from .tabs import * # noqa: F403f diff --git a/zhenxun/builtin_plugins/web_ui/api/menu/__init__.py b/zhenxun/builtin_plugins/web_ui/api/menu/__init__.py new file mode 100644 index 00000000..aca8baef --- /dev/null +++ b/zhenxun/builtin_plugins/web_ui/api/menu/__init__.py @@ -0,0 +1,26 @@ +from fastapi import APIRouter +from fastapi.responses import JSONResponse + +from zhenxun.services.log import logger + +from ...base_model import Result +from ...utils import authentication +from .data_source import menu_manage +from .model import MenuData + +router = APIRouter(prefix="/menu") + + +@router.get( + "/get_menus", + dependencies=[authentication()], + response_model=Result[list[MenuData]], + response_class=JSONResponse, + deprecated="获取菜单列表", # type: ignore +) +async def _() -> Result[list[MenuData]]: + try: + return Result.ok(menu_manage.get_menus(), "拿到菜单了哦!") + except Exception as e: + logger.error(f"{router.prefix}/get_menus 调用错误", "WebUi", e=e) + return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") diff --git a/zhenxun/builtin_plugins/web_ui/api/menu/data_source.py b/zhenxun/builtin_plugins/web_ui/api/menu/data_source.py new file mode 100644 index 00000000..1f530a3c --- /dev/null +++ b/zhenxun/builtin_plugins/web_ui/api/menu/data_source.py @@ -0,0 +1,64 @@ +import ujson as json + +from zhenxun.configs.path_config import DATA_PATH +from zhenxun.services.log import logger + +from .model import MenuData, MenuItem + + +class MenuManage: + def __init__(self) -> None: + self.file = DATA_PATH / "web_ui" / "menu.json" + self.menu = [] + if self.file.exists(): + try: + self.menu = json.load(self.file.open(encoding="utf8")) + except Exception as e: + logger.warning("菜单文件损坏,已重新生成...", "WebUi", e=e) + if not self.menu: + self.menu = [ + MenuItem( + name="仪表盘", + module="dashboard", + router="/dashboard", + icon="dashboard", + default=True, + ), + MenuItem( + name="真寻控制台", + module="command", + router="/command", + icon="command", + ), + MenuItem( + name="插件列表", module="plugin", router="/plugin", icon="plugin" + ), + MenuItem( + name="插件商店", module="store", router="/store", icon="store" + ), + MenuItem( + name="好友/群组", module="manage", router="/manage", icon="user" + ), + MenuItem( + name="数据库管理", + module="database", + router="/database", + icon="database", + ), + MenuItem( + name="系统信息", module="system", router="/system", icon="system" + ), + ] + self.save() + + def get_menus(self): + return MenuData(menus=self.menu) + + def save(self): + self.file.parent.mkdir(parents=True, exist_ok=True) + temp = [menu.dict() for menu in self.menu] + with self.file.open("w", encoding="utf8") as f: + json.dump(temp, f, ensure_ascii=False, indent=4) + + +menu_manage = MenuManage() diff --git a/zhenxun/builtin_plugins/web_ui/api/menu/model.py b/zhenxun/builtin_plugins/web_ui/api/menu/model.py new file mode 100644 index 00000000..abd73a69 --- /dev/null +++ b/zhenxun/builtin_plugins/web_ui/api/menu/model.py @@ -0,0 +1,21 @@ +from pydantic import BaseModel + + +class MenuItem(BaseModel): + module: str + """模块名称""" + name: str + """菜单名称""" + router: str + """路由""" + icon: str + """图标""" + default: bool = False + """默认选中""" + + +class MenuData(BaseModel): + bot_type: str = "zhenxun" + """bot类型""" + menus: list[MenuItem] + """菜单列表""" diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/dashboard/__init__.py b/zhenxun/builtin_plugins/web_ui/api/tabs/dashboard/__init__.py index fba85be1..81b66185 100644 --- a/zhenxun/builtin_plugins/web_ui/api/tabs/dashboard/__init__.py +++ b/zhenxun/builtin_plugins/web_ui/api/tabs/dashboard/__init__.py @@ -1,20 +1,14 @@ -from datetime import datetime, timedelta - from fastapi import APIRouter from fastapi.responses import JSONResponse import nonebot from nonebot import require from nonebot.config import Config -from tortoise.expressions import RawSQL -from tortoise.functions import Count -from zhenxun.models.bot_connect_log import BotConnectLog -from zhenxun.models.chat_history import ChatHistory -from zhenxun.models.statistics import Statistics +from zhenxun.services.log import logger from ....base_model import BaseResultModel, QueryModel, Result from ....utils import authentication -from .data_source import BotManage +from .data_source import ApiDataSource from .model import AllChatAndCallCount, BotInfo, ChatCallMonthCount, QueryChatCallCount require("plugin_store") @@ -33,8 +27,9 @@ driver = nonebot.get_driver() ) async def _() -> Result[list[BotInfo]]: try: - return Result.ok(await BotManage.get_bot_list(), "拿到信息啦!") + return Result.ok(await ApiDataSource.get_bot_list(), "拿到信息啦!") except Exception as e: + logger.error(f"{router.prefix}/get_bot_list 调用错误", "WebUi", e=e) return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") @@ -46,29 +41,13 @@ async def _() -> Result[list[BotInfo]]: description="获取聊天/调用记录的全部和今日数量", ) async def _(bot_id: str | None = None) -> Result[QueryChatCallCount]: - now = datetime.now() - query = ChatHistory - if bot_id: - query = query.filter(bot_id=bot_id) - chat_all_count = await query.annotate().count() - chat_day_count = await query.filter( - create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute) - ).count() - query = Statistics - if bot_id: - query = query.filter(bot_id=bot_id) - call_all_count = await query.annotate().count() - call_day_count = await query.filter( - create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute) - ).count() - return Result.ok( - QueryChatCallCount( - chat_num=chat_all_count, - chat_day=chat_day_count, - call_num=call_all_count, - call_day=call_day_count, + try: + return Result.ok( + await ApiDataSource.get_chat_and_call_count(bot_id), "拿到信息啦!" ) - ) + except Exception as e: + logger.error(f"{router.prefix}/get_chat_and_call_count 调用错误", "WebUi", e=e) + return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") @router.get( @@ -79,41 +58,15 @@ async def _(bot_id: str | None = None) -> Result[QueryChatCallCount]: description="获取聊天/调用记录的全部数据次数", ) async def _(bot_id: str | None = None) -> Result[AllChatAndCallCount]: - now = datetime.now() - query = ChatHistory - if bot_id: - query = query.filter(bot_id=bot_id) - chat_week_count = await query.filter( - create_time__gte=now - timedelta(days=7, hours=now.hour, minutes=now.minute) - ).count() - chat_month_count = await query.filter( - create_time__gte=now - timedelta(days=30, hours=now.hour, minutes=now.minute) - ).count() - chat_year_count = await query.filter( - create_time__gte=now - timedelta(days=365, hours=now.hour, minutes=now.minute) - ).count() - query = Statistics - if bot_id: - query = query.filter(bot_id=bot_id) - call_week_count = await query.filter( - create_time__gte=now - timedelta(days=7, hours=now.hour, minutes=now.minute) - ).count() - call_month_count = await query.filter( - create_time__gte=now - timedelta(days=30, hours=now.hour, minutes=now.minute) - ).count() - call_year_count = await query.filter( - create_time__gte=now - timedelta(days=365, hours=now.hour, minutes=now.minute) - ).count() - return Result.ok( - AllChatAndCallCount( - chat_week=chat_week_count, - chat_month=chat_month_count, - chat_year=chat_year_count, - call_week=call_week_count, - call_month=call_month_count, - call_year=call_year_count, + try: + return Result.ok( + await ApiDataSource.get_all_chat_and_call_count(bot_id), "拿到信息啦!" ) - ) + except Exception as e: + logger.error( + f"{router.prefix}/get_all_chat_and_call_count 调用错误", "WebUi", e=e + ) + return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") @router.get( @@ -124,48 +77,13 @@ async def _(bot_id: str | None = None) -> Result[AllChatAndCallCount]: deprecated="获取聊天/调用记录的一个月数量", # type: ignore ) async def _(bot_id: str | None = None) -> Result[ChatCallMonthCount]: - now = datetime.now() - filter_date = now - timedelta(days=30, hours=now.hour, minutes=now.minute) - chat_query = ChatHistory - call_query = Statistics - if bot_id: - chat_query = chat_query.filter(bot_id=bot_id) - call_query = call_query.filter(bot_id=bot_id) - chat_date_list = ( - await chat_query.filter(create_time__gte=filter_date) - .annotate(date=RawSQL("DATE(create_time)"), count=Count("id")) - .group_by("date") - .values("date", "count") - ) - call_date_list = ( - await call_query.filter(create_time__gte=filter_date) - .annotate(date=RawSQL("DATE(create_time)"), count=Count("id")) - .group_by("date") - .values("date", "count") - ) - date_list = [] - chat_count_list = [] - call_count_list = [] - chat_date2cnt = {str(date["date"]): date["count"] for date in chat_date_list} - call_date2cnt = {str(date["date"]): date["count"] for date in call_date_list} - date = now.date() - for _ in range(30): - if str(date) in chat_date2cnt: - chat_count_list.append(chat_date2cnt[str(date)]) - else: - chat_count_list.append(0) - if str(date) in call_date2cnt: - call_count_list.append(call_date2cnt[str(date)]) - else: - call_count_list.append(0) - date_list.append(str(date)[5:]) - date -= timedelta(days=1) - chat_count_list.reverse() - call_count_list.reverse() - date_list.reverse() - return Result.ok( - ChatCallMonthCount(chat=chat_count_list, call=call_count_list, date=date_list) - ) + try: + return Result.ok( + await ApiDataSource.get_chat_and_call_month(bot_id), "拿到信息啦!" + ) + except Exception as e: + logger.error(f"{router.prefix}/get_chat_and_call_month 调用错误", "WebUi", e=e) + return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") @router.post( @@ -176,18 +94,11 @@ async def _(bot_id: str | None = None) -> Result[ChatCallMonthCount]: deprecated="获取Bot连接记录", # type: ignore ) async def _(query: QueryModel) -> Result[BaseResultModel]: - total = await BotConnectLog.all().count() - if total % query.size: - total += 1 - data = ( - await BotConnectLog.all() - .order_by("-id") - .offset((query.index - 1) * query.size) - .limit(query.size) - ) - for v in data: - v.connect_time = v.connect_time.replace(tzinfo=None).replace(microsecond=0) - return Result.ok(BaseResultModel(total=total, data=data)) + try: + return Result.ok(await ApiDataSource.get_connect_log(query), "拿到信息啦!") + except Exception as e: + logger.error(f"{router.prefix}/get_connect_log 调用错误", "WebUi", e=e) + return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") @router.get( diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/dashboard/data_source.py b/zhenxun/builtin_plugins/web_ui/api/tabs/dashboard/data_source.py index 02754a22..243dafc2 100644 --- a/zhenxun/builtin_plugins/web_ui/api/tabs/dashboard/data_source.py +++ b/zhenxun/builtin_plugins/web_ui/api/tabs/dashboard/data_source.py @@ -4,13 +4,17 @@ import time import nonebot from nonebot.adapters import Bot from nonebot.drivers import Driver +from tortoise.expressions import RawSQL +from tortoise.functions import Count +from zhenxun.models.bot_connect_log import BotConnectLog from zhenxun.models.chat_history import ChatHistory from zhenxun.models.statistics import Statistics from zhenxun.utils.platform import PlatformUtils +from ....base_model import BaseResultModel, QueryModel from ..main.data_source import bot_live -from .model import BotInfo +from .model import AllChatAndCallCount, BotInfo, ChatCallMonthCount, QueryChatCallCount driver: Driver = nonebot.get_driver() @@ -24,7 +28,7 @@ async def _(): CONNECT_TIME = int(time.time()) -class BotManage: +class ApiDataSource: @classmethod async def __build_bot_info(cls, bot: Bot) -> BotInfo: """构建Bot信息 @@ -47,8 +51,7 @@ class BotManage: bot_info = BotInfo( self_id=bot.self_id, nickname=nickname, ava_url=ava_url, platform=platform ) - group_list, _ = await PlatformUtils.get_group_list(bot) - group_list = [g for g in group_list if g.channel_id is None] + group_list, _ = await PlatformUtils.get_group_list(bot, True) friend_list, _ = await PlatformUtils.get_friend_list(bot) bot_info.group_count = len(group_list) bot_info.friend_count = len(friend_list) @@ -77,3 +80,161 @@ class BotManage: for _, bot in nonebot.get_bots().items(): bot_list.append(await cls.__build_bot_info(bot)) return bot_list + + @classmethod + async def get_chat_and_call_count(cls, bot_id: str | None) -> QueryChatCallCount: + """获取今日聊天和调用次数 + + 参数: + bot_id: bot id + + 返回: + QueryChatCallCount: 数据内容 + """ + now = datetime.now() + query = ChatHistory + if bot_id: + query = query.filter(bot_id=bot_id) + chat_all_count = await query.annotate().count() + chat_day_count = await query.filter( + create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute) + ).count() + query = Statistics + if bot_id: + query = query.filter(bot_id=bot_id) + call_all_count = await query.annotate().count() + call_day_count = await query.filter( + create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute) + ).count() + return QueryChatCallCount( + chat_num=chat_all_count, + chat_day=chat_day_count, + call_num=call_all_count, + call_day=call_day_count, + ) + + @classmethod + async def get_all_chat_and_call_count( + cls, bot_id: str | None + ) -> AllChatAndCallCount: + """获取全部聊天和调用记录 + + 参数: + bot_id: bot id + + 返回: + AllChatAndCallCount: 数据内容 + """ + now = datetime.now() + query = ChatHistory + if bot_id: + query = query.filter(bot_id=bot_id) + chat_week_count = await query.filter( + create_time__gte=now - timedelta(days=7, hours=now.hour, minutes=now.minute) + ).count() + chat_month_count = await query.filter( + create_time__gte=now + - timedelta(days=30, hours=now.hour, minutes=now.minute) + ).count() + chat_year_count = await query.filter( + create_time__gte=now + - timedelta(days=365, hours=now.hour, minutes=now.minute) + ).count() + query = Statistics + if bot_id: + query = query.filter(bot_id=bot_id) + call_week_count = await query.filter( + create_time__gte=now - timedelta(days=7, hours=now.hour, minutes=now.minute) + ).count() + call_month_count = await query.filter( + create_time__gte=now + - timedelta(days=30, hours=now.hour, minutes=now.minute) + ).count() + call_year_count = await query.filter( + create_time__gte=now + - timedelta(days=365, hours=now.hour, minutes=now.minute) + ).count() + return AllChatAndCallCount( + chat_week=chat_week_count, + chat_month=chat_month_count, + chat_year=chat_year_count, + call_week=call_week_count, + call_month=call_month_count, + call_year=call_year_count, + ) + + @classmethod + async def get_chat_and_call_month(cls, bot_id: str | None) -> ChatCallMonthCount: + """获取一个月内的调用/消息记录次数,并根据日期对数据填充0 + + 参数: + bot_id: bot id + + 返回: + ChatCallMonthCount: 数据内容 + """ + now = datetime.now() + filter_date = now - timedelta(days=30, hours=now.hour, minutes=now.minute) + chat_query = ChatHistory + call_query = Statistics + if bot_id: + chat_query = chat_query.filter(bot_id=bot_id) + call_query = call_query.filter(bot_id=bot_id) + chat_date_list = ( + await chat_query.filter(create_time__gte=filter_date) + .annotate(date=RawSQL("DATE(create_time)"), count=Count("id")) + .group_by("date") + .values("date", "count") + ) + call_date_list = ( + await call_query.filter(create_time__gte=filter_date) + .annotate(date=RawSQL("DATE(create_time)"), count=Count("id")) + .group_by("date") + .values("date", "count") + ) + date_list = [] + chat_count_list = [] + call_count_list = [] + chat_date2cnt = {str(date["date"]): date["count"] for date in chat_date_list} + call_date2cnt = {str(date["date"]): date["count"] for date in call_date_list} + date = now.date() + for _ in range(30): + if str(date) in chat_date2cnt: + chat_count_list.append(chat_date2cnt[str(date)]) + else: + chat_count_list.append(0) + if str(date) in call_date2cnt: + call_count_list.append(call_date2cnt[str(date)]) + else: + call_count_list.append(0) + date_list.append(str(date)[5:]) + date -= timedelta(days=1) + chat_count_list.reverse() + call_count_list.reverse() + date_list.reverse() + return ChatCallMonthCount( + chat=chat_count_list, call=call_count_list, date=date_list + ) + + @classmethod + async def get_connect_log(cls, query: QueryModel) -> BaseResultModel: + """获取bot连接日志 + + 参数: + query: 查询模型 + + 返回: + BaseResultModel: 数据内容 + """ + total = await BotConnectLog.all().count() + if total % query.size: + total += 1 + data = ( + await BotConnectLog.all() + .order_by("-id") + .offset((query.index - 1) * query.size) + .limit(query.size) + ) + for v in data: + v.connect_time = v.connect_time.replace(tzinfo=None).replace(microsecond=0) + return BaseResultModel(total=total, data=data) diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/database/__init__.py b/zhenxun/builtin_plugins/web_ui/api/tabs/database/__init__.py index efabfdfa..7b495c1c 100644 --- a/zhenxun/builtin_plugins/web_ui/api/tabs/database/__init__.py +++ b/zhenxun/builtin_plugins/web_ui/api/tabs/database/__init__.py @@ -3,14 +3,15 @@ from fastapi.responses import JSONResponse import nonebot from nonebot.drivers import Driver from tortoise import Tortoise -from tortoise.exceptions import OperationalError from zhenxun.configs.config import BotConfig from zhenxun.models.plugin_info import PluginInfo from zhenxun.models.task_info import TaskInfo +from zhenxun.services.log import logger from ....base_model import BaseResultModel, QueryModel, Result from ....utils import authentication +from .data_source import ApiDataSource, type2sql from .models.model import Column, SqlModel, SqlText from .models.sql_log import SqlLog @@ -20,52 +21,6 @@ router = APIRouter(prefix="/database") driver: Driver = nonebot.get_driver() -SQL_DICT = {} - - -SELECT_TABLE_MYSQL_SQL = """ -SELECT table_name AS name, table_comment AS `desc` -FROM information_schema.tables -WHERE table_schema = DATABASE(); -""" - -SELECT_TABLE_SQLITE_SQL = """ -SELECT name FROM sqlite_master WHERE type='table'; -""" - -SELECT_TABLE_PSQL_SQL = """ -select a.tablename as name,d.description as desc from pg_tables a - left join pg_class c on relname=tablename - left join pg_description d on oid=objoid and objsubid=0 where a.schemaname='public' -""" - -SELECT_TABLE_COLUMN_PSQL_SQL = """ -SELECT column_name, data_type, character_maximum_length as max_length, is_nullable -FROM information_schema.columns -WHERE table_name = '{}'; -""" - -SELECT_TABLE_COLUMN_MYSQL_SQL = """ -SHOW COLUMNS FROM {}; -""" - -SELECT_TABLE_COLUMN_SQLITE_SQL = """ -PRAGMA table_info({}); -""" - -type2sql = { - "mysql": SELECT_TABLE_MYSQL_SQL, - "sqlite": SELECT_TABLE_SQLITE_SQL, - "postgres": SELECT_TABLE_PSQL_SQL, -} - -type2sql_column = { - "mysql": SELECT_TABLE_COLUMN_MYSQL_SQL, - "sqlite": SELECT_TABLE_COLUMN_SQLITE_SQL, - "postgres": SELECT_TABLE_COLUMN_PSQL_SQL, -} - - @driver.on_startup async def _(): for plugin in nonebot.get_loaded_plugins(): @@ -73,7 +28,7 @@ async def _(): sql_list = [] if plugin.metadata and plugin.metadata.extra: sql_list = plugin.metadata.extra.get("sql_list") - if module in SQL_DICT: + if module in ApiDataSource.SQL_DICT: raise ValueError(f"{module} 常用SQL module 重复") if sql_list: SqlModel( @@ -81,15 +36,15 @@ async def _(): module=module, sql_list=sql_list, ) - SQL_DICT[module] = SqlModel - if SQL_DICT: - result = await PluginInfo.filter(module__in=SQL_DICT.keys()).values_list( - "module", "name" - ) + ApiDataSource.SQL_DICT[module] = SqlModel + if ApiDataSource.SQL_DICT: + result = await PluginInfo.filter( + module__in=ApiDataSource.SQL_DICT.keys() + ).values_list("module", "name") module2name = {r[0]: r[1] for r in result} - for s in SQL_DICT: - module = SQL_DICT[s].module - SQL_DICT[s].name = module2name.get(module, module) + for s in ApiDataSource.SQL_DICT: + module = ApiDataSource.SQL_DICT[s].module + ApiDataSource.SQL_DICT[s].name = module2name.get(module, module) @router.get( @@ -100,10 +55,14 @@ async def _(): description="获取数据库表", ) async def _() -> Result[list[dict]]: - db = Tortoise.get_connection("default") - sql_type = BotConfig.get_sql_type() - query = await db.execute_query_dict(type2sql[sql_type]) - return Result.ok(query) + try: + db = Tortoise.get_connection("default") + sql_type = BotConfig.get_sql_type() + query = await db.execute_query_dict(type2sql[sql_type]) + return Result.ok(query) + except Exception as e: + logger.error(f"{router.prefix}/get_table_list 调用错误", "WebUi", e=e) + return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") @router.get( @@ -114,34 +73,13 @@ async def _() -> Result[list[dict]]: description="获取表字段", ) async def _(table_name: str) -> Result[list[Column]]: - db = Tortoise.get_connection("default") - sql_type = BotConfig.get_sql_type() - sql = type2sql_column[sql_type] - query = await db.execute_query_dict(sql.format(table_name)) - result_list = [] - if sql_type == "sqlite": - result_list.extend( - Column( - column_name=result["name"], - data_type=result["type"], - max_length=-1, - is_nullable="YES" if result["notnull"] == 1 else "NO", - ) - for result in query + try: + return Result.ok( + await ApiDataSource.get_table_column(table_name), "拿到信息啦!" ) - elif sql_type == "mysql": - result_list.extend( - Column( - column_name=result["Field"], - data_type=result["Type"], - max_length=-1, - is_nullable=result["Null"], - ) - for result in query - ) - else: - result_list.extend(Column(**result) for result in query) - return Result.ok(result_list) + except Exception as e: + logger.error(f"{router.prefix}/get_table_column 调用错误", "WebUi", e=e) + return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") @router.post( @@ -163,7 +101,8 @@ async def _(sql: SqlText, request: Request) -> Result[list[dict]]: result = await TaskInfo.raw(sql.sql) await SqlLog.add(ip or "0.0.0.0", sql.sql, str(result)) return Result.ok(info="执行成功啦!") - except OperationalError as e: + except Exception as e: + logger.error(f"{router.prefix}/exec_sql 调用错误", "WebUi", e=e) await SqlLog.add(ip or "0.0.0.0", sql.sql, str(e), False) return Result.warning_(f"sql执行错误: {e}") @@ -176,16 +115,20 @@ async def _(sql: SqlText, request: Request) -> Result[list[dict]]: description="sql日志列表", ) async def _(query: QueryModel) -> Result[BaseResultModel]: - total = await SqlLog.all().count() - if total % query.size: - total += 1 - data = ( - await SqlLog.all() - .order_by("-id") - .offset((query.index - 1) * query.size) - .limit(query.size) - ) - return Result.ok(BaseResultModel(total=total, data=data)) + try: + total = await SqlLog.all().count() + if total % query.size: + total += 1 + data = ( + await SqlLog.all() + .order_by("-id") + .offset((query.index - 1) * query.size) + .limit(query.size) + ) + return Result.ok(BaseResultModel(total=total, data=data)) + except Exception as e: + logger.error(f"{router.prefix}/get_sql_log 调用错误", "WebUi", e=e) + return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") @router.get( @@ -197,5 +140,5 @@ async def _(query: QueryModel) -> Result[BaseResultModel]: ) async def _(plugin_name: str | None = None) -> Result[dict]: if plugin_name: - return Result.ok(SQL_DICT.get(plugin_name)) - return Result.ok(str(SQL_DICT)) + return Result.ok(ApiDataSource.SQL_DICT.get(plugin_name)) + return Result.ok(str(ApiDataSource.SQL_DICT)) diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/database/data_source.py b/zhenxun/builtin_plugins/web_ui/api/tabs/database/data_source.py new file mode 100644 index 00000000..9e937837 --- /dev/null +++ b/zhenxun/builtin_plugins/web_ui/api/tabs/database/data_source.py @@ -0,0 +1,90 @@ +from tortoise import Tortoise + +from zhenxun.configs.config import BotConfig + +from .models.model import Column + +SELECT_TABLE_MYSQL_SQL = """ +SELECT table_name AS name, table_comment AS `desc` +FROM information_schema.tables +WHERE table_schema = DATABASE(); +""" + +SELECT_TABLE_SQLITE_SQL = """ +SELECT name FROM sqlite_master WHERE type='table'; +""" + +SELECT_TABLE_PSQL_SQL = """ +select a.tablename as name,d.description as desc from pg_tables a + left join pg_class c on relname=tablename + left join pg_description d on oid=objoid and objsubid=0 where a.schemaname='public' +""" + +SELECT_TABLE_COLUMN_PSQL_SQL = """ +SELECT column_name, data_type, character_maximum_length as max_length, is_nullable +FROM information_schema.columns +WHERE table_name = '{}'; +""" + +SELECT_TABLE_COLUMN_MYSQL_SQL = """ +SHOW COLUMNS FROM {}; +""" + +SELECT_TABLE_COLUMN_SQLITE_SQL = """ +PRAGMA table_info({}); +""" + +type2sql = { + "mysql": SELECT_TABLE_MYSQL_SQL, + "sqlite": SELECT_TABLE_SQLITE_SQL, + "postgres": SELECT_TABLE_PSQL_SQL, +} + +type2sql_column = { + "mysql": SELECT_TABLE_COLUMN_MYSQL_SQL, + "sqlite": SELECT_TABLE_COLUMN_SQLITE_SQL, + "postgres": SELECT_TABLE_COLUMN_PSQL_SQL, +} + + +class ApiDataSource: + SQL_DICT = {} # noqa: RUF012 + + @classmethod + async def get_table_column(cls, table_name: str) -> list[Column]: + """获取表字段信息 + + 参数: + table_name: 表名 + + 返回: + list[Column]: 字段数据 + """ + db = Tortoise.get_connection("default") + sql_type = BotConfig.get_sql_type() + sql = type2sql_column[sql_type] + query = await db.execute_query_dict(sql.format(table_name)) + result_list = [] + if sql_type == "sqlite": + result_list.extend( + Column( + column_name=result["name"], + data_type=result["type"], + max_length=-1, + is_nullable="YES" if result["notnull"] == 1 else "NO", + ) + for result in query + ) + elif sql_type == "mysql": + result_list.extend( + Column( + column_name=result["Field"], + data_type=result["Type"], + max_length=-1, + is_nullable=result["Null"], + ) + for result in query + ) + else: + result_list.extend(Column(**result) for result in query) + return result_list diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/main/__init__.py b/zhenxun/builtin_plugins/web_ui/api/tabs/main/__init__.py index 91341cc5..36059101 100644 --- a/zhenxun/builtin_plugins/web_ui/api/tabs/main/__init__.py +++ b/zhenxun/builtin_plugins/web_ui/api/tabs/main/__init__.py @@ -1,7 +1,5 @@ import asyncio import contextlib -from datetime import datetime, timedelta -from pathlib import Path import time from fastapi import APIRouter @@ -9,28 +7,26 @@ from fastapi.responses import JSONResponse import nonebot from nonebot.config import Config from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState -from tortoise.functions import Count from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK -from zhenxun.models.bot_connect_log import BotConnectLog -from zhenxun.models.chat_history import ChatHistory -from zhenxun.models.group_console import GroupConsole -from zhenxun.models.plugin_info import PluginInfo -from zhenxun.models.statistics import Statistics +from zhenxun.models.bot_console import BotConsole from zhenxun.services.log import logger +from zhenxun.utils.common_utils import CommonUtils from zhenxun.utils.platform import PlatformUtils from ....base_model import Result -from ....config import AVA_URL, GROUP_AVA_URL, QueryDateType +from ....config import QueryDateType from ....utils import authentication, get_system_status -from .data_source import bot_live +from .data_source import ApiDataSource from .model import ( ActiveGroup, BaseInfo, + BotBlockModule, + BotManageUpdateParam, + BotStatusParam, HotPlugin, NonebotData, QueryCount, - TemplateBaseInfo, ) driver = nonebot.get_driver() @@ -56,64 +52,14 @@ async def _(bot_id: str | None = None) -> Result[list[BaseInfo]]: 返回: Result: 获取指定bot信息与bot列表 """ - global run_time - bot_list: list[TemplateBaseInfo] = [] - if bots := nonebot.get_bots(): - select_bot: BaseInfo - for _, bot in bots.items(): - login_info = await bot.get_login_info() - bot_list.append( - TemplateBaseInfo( - bot=bot, # type: ignore - self_id=bot.self_id, - nickname=login_info["nickname"], - ava_url=AVA_URL.format(bot.self_id), - ) - ) - # 获取指定qq号的bot信息,若无指定 则获取第一个 - if _bl := [b for b in bot_list if b.self_id == bot_id]: - select_bot = _bl[0] - else: - select_bot = bot_list[0] - select_bot.is_select = True - now = datetime.now() - # 今日累计接收消息 - select_bot.received_messages = await ChatHistory.filter( - bot_id=select_bot.self_id, - create_time__gte=now - timedelta(hours=now.hour), - ).count() - # 群聊数量 - select_bot.group_count = len(await select_bot.bot.get_group_list()) - # 好友数量 - select_bot.friend_count = len(await select_bot.bot.get_friend_list()) - for bot in bot_list: - bot.bot = None # type: ignore - # 插件加载数量 - select_bot.plugin_count = await PluginInfo.all().count() - fail_count = await PluginInfo.filter(load_status=False).count() - select_bot.fail_plugin_count = fail_count - select_bot.success_plugin_count = ( - select_bot.plugin_count - select_bot.fail_plugin_count - ) - # 连接时间 - select_bot.connect_time = bot_live.get(select_bot.self_id) or 0 - if select_bot.connect_time: - connect_date = datetime.fromtimestamp(select_bot.connect_time) - select_bot.connect_date = connect_date.strftime("%Y-%m-%d %H:%M:%S") - version_file = Path() / "__version__" - if version_file.exists(): - if text := version_file.open().read(): - if ver := text.replace("__version__: ", "").strip(): - select_bot.version = ver - day_call = await Statistics.filter( - create_time__gte=now - timedelta(hours=now.hour) - ).count() - select_bot.day_call = day_call - select_bot.connect_count = await BotConnectLog.filter( - bot_id=select_bot.self_id - ).count() - return Result.ok([BaseInfo(**e.dict()) for e in bot_list], "拿到信息啦!") - return Result.warning_("无Bot连接...") + try: + result = await ApiDataSource.get_base_info(bot_id) + if not result: + Result.warning_("无Bot连接...") + return Result.ok(result, "拿到信息啦!") + except Exception as e: + logger.error(f"{router.prefix}/get_base_info 调用错误", "WebUi", e=e) + return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") @router.get( @@ -124,32 +70,11 @@ async def _(bot_id: str | None = None) -> Result[list[BaseInfo]]: description="获取接收消息数量", ) async def _(bot_id: str | None = None) -> Result[QueryCount]: - now = datetime.now() - query = ChatHistory - if bot_id: - query = query.filter(bot_id=bot_id) - all_count = await query.annotate().count() - day_count = await query.filter( - create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute) - ).count() - week_count = await query.filter( - create_time__gte=now - timedelta(days=7, hours=now.hour, minutes=now.minute) - ).count() - month_count = await query.filter( - create_time__gte=now - timedelta(days=30, hours=now.hour, minutes=now.minute) - ).count() - year_count = await query.filter( - create_time__gte=now - timedelta(days=365, hours=now.hour, minutes=now.minute) - ).count() - return Result.ok( - QueryCount( - num=all_count, - day=day_count, - week=week_count, - month=month_count, - year=year_count, - ) - ) + try: + return Result.ok(await ApiDataSource.get_all_chat_count(bot_id), "拿到信息啦!") + except Exception as e: + logger.error(f"{router.prefix}/get_all_chat_count 调用错误", "WebUi", e=e) + return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") @router.get( @@ -160,32 +85,11 @@ async def _(bot_id: str | None = None) -> Result[QueryCount]: description="获取调用次数", ) async def _(bot_id: str | None = None) -> Result[QueryCount]: - now = datetime.now() - query = Statistics - if bot_id: - query = query.filter(bot_id=bot_id) - all_count = await query.annotate().count() - day_count = await query.filter( - create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute) - ).count() - week_count = await query.filter( - create_time__gte=now - timedelta(days=7, hours=now.hour, minutes=now.minute) - ).count() - month_count = await query.filter( - create_time__gte=now - timedelta(days=30, hours=now.hour, minutes=now.minute) - ).count() - year_count = await query.filter( - create_time__gte=now - timedelta(days=365, hours=now.hour, minutes=now.minute) - ).count() - return Result.ok( - QueryCount( - num=all_count, - day=day_count, - week=week_count, - month=month_count, - year=year_count, - ) - ) + try: + return Result.ok(await ApiDataSource.get_all_call_count(bot_id), "拿到信息啦!") + except Exception as e: + logger.error(f"{router.prefix}/get_all_call_count 调用错误", "WebUi", e=e) + return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") @router.get( @@ -196,19 +100,18 @@ async def _(bot_id: str | None = None) -> Result[QueryCount]: description="好友/群组数量", ) async def _(bot_id: str) -> Result[dict[str, int]]: - if bots := nonebot.get_bots(): - if bot_id not in bots: - return Result.warning_("指定Bot未连接...") - bot = bots[bot_id] - platform = PlatformUtils.get_platform(bot) - if platform == "qq": - data = { - "friend_count": len(await bot.get_friend_list()), - "group_count": len(await bot.get_group_list()), - } - return Result.ok(data) - return Result.warning_("暂不支持该平台...") - return Result.warning_("无Bot连接...") + try: + bot = nonebot.get_bot(bot_id) + data = { + "friend_count": len(await PlatformUtils.get_friend_list(bot)), + "group_count": len(await PlatformUtils.get_group_list(bot)), + } + return Result.ok(data, "拿到信息啦!") + except (ValueError, KeyError): + return Result.warning_("指定Bot未连接...") + except Exception as e: + logger.error(f"{router.prefix}/get_fg_count 调用错误", "WebUi", e=e) + return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") @router.get( @@ -219,6 +122,7 @@ async def _(bot_id: str) -> Result[dict[str, int]]: description="获取nb数据", ) async def _() -> Result[NonebotData]: + global run_time return Result.ok(NonebotData(config=driver.config, run_time=int(run_time))) @@ -241,6 +145,7 @@ async def _() -> Result[Config]: description="获取nb运行时间", ) async def _() -> Result[int]: + global run_time return Result.ok(int(run_time)) @@ -254,48 +159,13 @@ async def _() -> Result[int]: async def _( date_type: QueryDateType | None = None, bot_id: str | None = None ) -> Result[list[ActiveGroup]]: - query = ChatHistory - now = datetime.now() - if bot_id: - query = query.filter(bot_id=bot_id) - if date_type == QueryDateType.DAY: - query = query.filter(create_time__gte=now - timedelta(hours=now.hour)) - if date_type == QueryDateType.WEEK: - query = query.filter(create_time__gte=now - timedelta(days=7)) - if date_type == QueryDateType.MONTH: - query = query.filter(create_time__gte=now - timedelta(days=30)) - if date_type == QueryDateType.YEAR: - query = query.filter(create_time__gte=now - timedelta(days=365)) - data_list = ( - await query.annotate(count=Count("id")) - .filter(group_id__not_isnull=True) - .group_by("group_id") - .order_by("-count") - .limit(5) - .values_list("group_id", "count") - ) - id2name = {} - if data_list: - if info_list := await GroupConsole.filter( - group_id__in=[x[0] for x in data_list] - ).all(): - for group_info in info_list: - id2name[group_info.group_id] = group_info.group_name - active_group_list = [ - ActiveGroup( - group_id=data[0], - name=id2name.get(data[0]) or data[0], - chat_num=data[1], - ava_img=GROUP_AVA_URL.format(data[0], data[0]), + try: + return Result.ok( + await ApiDataSource.get_active_group(date_type, bot_id), "拿到信息啦!" ) - for data in data_list - ] - active_group_list = sorted( - active_group_list, key=lambda x: x.chat_num, reverse=True - ) - if len(active_group_list) > 5: - active_group_list = active_group_list[:5] - return Result.ok(active_group_list) + except Exception as e: + logger.error(f"{router.prefix}/get_active_group 调用错误", "WebUi", e=e) + return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") @router.get( @@ -308,37 +178,66 @@ async def _( async def _( date_type: QueryDateType | None = None, bot_id: str | None = None ) -> Result[list[HotPlugin]]: - query = Statistics - now = datetime.now() - if bot_id: - query = query.filter(bot_id=bot_id) - if date_type == QueryDateType.DAY: - query = query.filter(create_time__gte=now - timedelta(hours=now.hour)) - if date_type == QueryDateType.WEEK: - query = query.filter(create_time__gte=now - timedelta(days=7)) - if date_type == QueryDateType.MONTH: - query = query.filter(create_time__gte=now - timedelta(days=30)) - if date_type == QueryDateType.YEAR: - query = query.filter(create_time__gte=now - timedelta(days=365)) - data_list = ( - await query.annotate(count=Count("id")) - .group_by("plugin_name") - .order_by("-count") - .limit(5) - .values_list("plugin_name", "count") - ) - hot_plugin_list = [] - module_list = [x[0] for x in data_list] - plugins = await PluginInfo.filter(module__in=module_list).all() - module2name = {p.module: p.name for p in plugins} - for data in data_list: - module = data[0] - name = module2name.get(module) or module - hot_plugin_list.append(HotPlugin(module=module, name=name, count=data[1])) - hot_plugin_list = sorted(hot_plugin_list, key=lambda x: x.count, reverse=True) - if len(hot_plugin_list) > 5: - hot_plugin_list = hot_plugin_list[:5] - return Result.ok(hot_plugin_list) + try: + return Result.ok( + await ApiDataSource.get_hot_plugin(date_type, bot_id), "拿到信息啦!" + ) + except Exception as e: + logger.error(f"{router.prefix}/get_hot_plugin 调用错误", "WebUi", e=e) + return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") + + +@router.post( + "/change_bot_status", + dependencies=[authentication()], + response_model=Result, + response_class=JSONResponse, + description="修改bot全局开关", +) +async def _(param: BotStatusParam): + try: + await BotConsole.set_bot_status(param.status, param.bot_id) + return Result.ok(info="修改bot全局开关成功!") + except (ValueError, KeyError): + return Result.fail("Bot未初始化...") + + +@router.get( + "/get_bot_block_module", + dependencies=[authentication()], + response_model=Result[BotBlockModule], + response_class=JSONResponse, + description="获取bot层面的禁用模块", +) +async def _(bot_id: str) -> Result[BotBlockModule]: + try: + return Result.ok( + await ApiDataSource.get_bot_block_module(bot_id), "拿到信息啦!" + ) + except Exception as e: + logger.error(f"{router.prefix}/get_bot_block_module 调用错误", "WebUi", e=e) + return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") + + +@router.post( + "/update_bot_manage", + dependencies=[authentication()], + response_model=Result, + response_class=JSONResponse, + description="修改bot全局开关", +) +async def _(param: BotManageUpdateParam): + try: + bot_data = await BotConsole.get_or_none(bot_id=param.bot_id) + if not bot_data: + return Result.fail("Bot数据不存在...") + bot_data.block_plugins = CommonUtils.convert_module_format(param.block_plugins) + bot_data.block_tasks = CommonUtils.convert_module_format(param.block_tasks) + await bot_data.save(update_fields=["block_plugins", "block_tasks"]) + return Result.ok() + except Exception as e: + logger.error(f"{router.prefix}/update_bot_manage 调用错误", "WebUi", e=e) + return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") @ws_router.websocket("/system_status") diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/main/data_source.py b/zhenxun/builtin_plugins/web_ui/api/tabs/main/data_source.py index ca445016..79bbc5c0 100644 --- a/zhenxun/builtin_plugins/web_ui/api/tabs/main/data_source.py +++ b/zhenxun/builtin_plugins/web_ui/api/tabs/main/data_source.py @@ -1,8 +1,33 @@ +from datetime import datetime, timedelta +from pathlib import Path import time import nonebot -from nonebot.adapters.onebot.v11 import Bot +from nonebot.adapters import Bot from nonebot.drivers import Driver +from tortoise.functions import Count + +from zhenxun.models.bot_connect_log import BotConnectLog +from zhenxun.models.bot_console import BotConsole +from zhenxun.models.chat_history import ChatHistory +from zhenxun.models.group_console import GroupConsole +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.models.statistics import Statistics +from zhenxun.models.task_info import TaskInfo +from zhenxun.services.log import logger +from zhenxun.utils.common_utils import CommonUtils +from zhenxun.utils.enum import PluginType +from zhenxun.utils.platform import PlatformUtils + +from ....config import AVA_URL, GROUP_AVA_URL, QueryDateType +from .model import ( + ActiveGroup, + BaseInfo, + BotBlockModule, + HotPlugin, + QueryCount, + TemplateBaseInfo, +) driver: Driver = nonebot.get_driver() @@ -33,3 +58,313 @@ async def _(bot: Bot): @driver.on_bot_disconnect async def _(bot: Bot): bot_live.remove(bot.self_id) + + +class ApiDataSource: + @classmethod + async def __build_bot_info(cls, bot: Bot) -> TemplateBaseInfo: + """构建bot信息 + + 参数: + bot: bot实例 + + 返回: + TemplateBaseInfo: bot信息 + """ + login_info = None + try: + login_info = await bot.get_login_info() + except Exception as e: + logger.warning("调用接口get_login_info失败", "WebUi", e=e) + return TemplateBaseInfo( + bot=bot, + self_id=bot.self_id, + nickname=login_info["nickname"] if login_info else bot.self_id, + ava_url=AVA_URL.format(bot.self_id), + ) + + @classmethod + def __get_bot_version(cls) -> str: + """获取bot版本 + + 返回: + str | None: 版本 + """ + version_file = Path() / "__version__" + if version_file.exists(): + if text := version_file.open().read(): + return text.replace("__version__: ", "").strip() + return "unknown" + + @classmethod + async def __init_bot_base_data(cls, select_bot: TemplateBaseInfo): + """初始化bot的基础数据 + + 参数: + select_bot: bot + """ + now = datetime.now() + # 今日累计接收消息 + select_bot.received_messages = await ChatHistory.filter( + bot_id=select_bot.self_id, + create_time__gte=now - timedelta(hours=now.hour), + ).count() + # 群聊数量 + select_bot.group_count = len(await PlatformUtils.get_group_list(select_bot.bot)) + # 好友数量 + select_bot.friend_count = len( + await PlatformUtils.get_friend_list(select_bot.bot) + ) + select_bot.status = await BotConsole.get_bot_status(select_bot.self_id) + # 连接时间 + select_bot.connect_time = bot_live.get(select_bot.self_id) or 0 + if select_bot.connect_time: + connect_date = datetime.fromtimestamp(select_bot.connect_time) + select_bot.connect_date = connect_date.strftime("%Y-%m-%d %H:%M:%S") + select_bot.version = cls.__get_bot_version() + day_call = await Statistics.filter( + create_time__gte=now - timedelta(hours=now.hour) + ).count() + select_bot.day_call = day_call + select_bot.connect_count = await BotConnectLog.filter( + bot_id=select_bot.self_id + ).count() + + @classmethod + async def get_base_info(cls, bot_id: str | None) -> list[BaseInfo] | None: + """获取bot信息 + + 参数: + bot_id: bot id + + 返回: + list[BaseInfo] | None: bot列表 + """ + bots = nonebot.get_bots() + if not bots: + return None + select_bot: BaseInfo + bot_list = [await cls.__build_bot_info(bot) for _, bot in bots.items()] + # 获取指定qq号的bot信息,若无指定 则获取第一个 + if _bl := [b for b in bot_list if b.self_id == bot_id]: + select_bot = _bl[0] + else: + select_bot = bot_list[0] + await cls.__init_bot_base_data(select_bot) + for bot in bot_list: + bot.bot = None # type: ignore + select_bot.is_select = True + return [BaseInfo(**e.dict()) for e in bot_list] + + @classmethod + async def get_all_chat_count(cls, bot_id: str | None) -> QueryCount: + """获取年/月/周/日聊天次数 + + 参数: + bot_id: bot id + + 返回: + QueryCount: 数据内容 + """ + now = datetime.now() + query = ChatHistory + if bot_id: + query = query.filter(bot_id=bot_id) + all_count = await query.annotate().count() + day_count = await query.filter( + create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute) + ).count() + week_count = await query.filter( + create_time__gte=now - timedelta(days=7, hours=now.hour, minutes=now.minute) + ).count() + month_count = await query.filter( + create_time__gte=now + - timedelta(days=30, hours=now.hour, minutes=now.minute) + ).count() + year_count = await query.filter( + create_time__gte=now + - timedelta(days=365, hours=now.hour, minutes=now.minute) + ).count() + return QueryCount( + num=all_count, + day=day_count, + week=week_count, + month=month_count, + year=year_count, + ) + + @classmethod + async def get_all_call_count(cls, bot_id: str | None) -> QueryCount: + """获取年/月/周/日调用次数 + + 参数: + bot_id: bot id + + 返回: + QueryCount: 数据内容 + """ + now = datetime.now() + query = Statistics + if bot_id: + query = query.filter(bot_id=bot_id) + all_count = await query.annotate().count() + day_count = await query.filter( + create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute) + ).count() + week_count = await query.filter( + create_time__gte=now - timedelta(days=7, hours=now.hour, minutes=now.minute) + ).count() + month_count = await query.filter( + create_time__gte=now + - timedelta(days=30, hours=now.hour, minutes=now.minute) + ).count() + year_count = await query.filter( + create_time__gte=now + - timedelta(days=365, hours=now.hour, minutes=now.minute) + ).count() + return QueryCount( + num=all_count, + day=day_count, + week=week_count, + month=month_count, + year=year_count, + ) + + @classmethod + def __get_query( + cls, + base_query: type[ChatHistory | Statistics], + date_type: QueryDateType | None = None, + bot_id: str | None = None, + ): + """构建日期查询条件 + + 参数: + date_type: 日期类型. + bot_id: bot id. + """ + query = base_query + now = datetime.now() + if bot_id: + query = query.filter(bot_id=bot_id) + if date_type == QueryDateType.DAY: + query = query.filter(create_time__gte=now - timedelta(hours=now.hour)) + if date_type == QueryDateType.WEEK: + query = query.filter(create_time__gte=now - timedelta(days=7)) + if date_type == QueryDateType.MONTH: + query = query.filter(create_time__gte=now - timedelta(days=30)) + if date_type == QueryDateType.YEAR: + query = query.filter(create_time__gte=now - timedelta(days=365)) + return query + + @classmethod + async def get_active_group( + cls, date_type: QueryDateType | None = None, bot_id: str | None = None + ) -> list[ActiveGroup]: + """获取活跃群组 + + 参数: + date_type: 日期类型. + bot_id: bot id. + + 返回: + list[ActiveGroup]: 活跃群组列表 + """ + query = cls.__get_query(ChatHistory, date_type, bot_id) + data_list = ( + await query.annotate(count=Count("id")) + .filter(group_id__not_isnull=True) + .group_by("group_id") + .order_by("-count") + .limit(5) + .values_list("group_id", "count") + ) + id2name = {} + if data_list: + if info_list := await GroupConsole.filter( + group_id__in=[x[0] for x in data_list] + ).all(): + for group_info in info_list: + id2name[group_info.group_id] = group_info.group_name + active_group_list = [ + ActiveGroup( + group_id=data[0], + name=id2name.get(data[0]) or data[0], + chat_num=data[1], + ava_img=GROUP_AVA_URL.format(data[0], data[0]), + ) + for data in data_list + ] + active_group_list = sorted( + active_group_list, key=lambda x: x.chat_num, reverse=True + ) + if len(active_group_list) > 5: + active_group_list = active_group_list[:5] + return active_group_list + + @classmethod + async def get_hot_plugin( + cls, date_type: QueryDateType | None = None, bot_id: str | None = None + ) -> list[HotPlugin]: + """获取热门插件 + + 参数: + date_type: 日期类型. + bot_id: bot id. + + 返回: + list[HotPlugin]: 热门插件列表 + """ + query = cls.__get_query(Statistics, date_type, bot_id) + data_list = ( + await query.annotate(count=Count("id")) + .group_by("plugin_name") + .order_by("-count") + .limit(5) + .values_list("plugin_name", "count") + ) + hot_plugin_list = [] + module_list = [x[0] for x in data_list] + plugins = await PluginInfo.filter(module__in=module_list).all() + module2name = {p.module: p.name for p in plugins} + for data in data_list: + module = data[0] + name = module2name.get(module) or module + hot_plugin_list.append(HotPlugin(module=module, name=name, count=data[1])) + hot_plugin_list = sorted(hot_plugin_list, key=lambda x: x.count, reverse=True) + if len(hot_plugin_list) > 5: + hot_plugin_list = hot_plugin_list[:5] + return hot_plugin_list + + @classmethod + async def get_bot_block_module(cls, bot_id: str) -> BotBlockModule | None: + """获取bot层面的禁用模块 + + 参数: + bot_id: bot id + + 返回: + BotBlockModule | None: 数据内容 + """ + bot_data = await BotConsole.get_or_none(bot_id=bot_id) + if not bot_data: + return None + block_tasks = [] + block_plugins = [] + all_plugins = await PluginInfo.filter( + load_status=True, plugin_type=PluginType.NORMAL + ).values("module", "name") + all_task = await TaskInfo.annotate().values("module", "name") + if bot_data.block_tasks: + tasks = CommonUtils.convert_module_format(bot_data.block_tasks) + block_tasks = [t["module"] for t in all_task if t["module"] in tasks] + if bot_data.block_plugins: + plugins = CommonUtils.convert_module_format(bot_data.block_plugins) + block_plugins = [t["module"] for t in all_plugins if t["module"] in plugins] + return BotBlockModule( + bot_id=bot_id, + block_tasks=block_tasks, + block_plugins=block_plugins, + all_plugins=all_plugins, + all_tasks=all_task, + ) diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/main/model.py b/zhenxun/builtin_plugins/web_ui/api/tabs/main/model.py index 95a65515..a38be06f 100644 --- a/zhenxun/builtin_plugins/web_ui/api/tabs/main/model.py +++ b/zhenxun/builtin_plugins/web_ui/api/tabs/main/model.py @@ -1,8 +1,45 @@ +from typing import Any + from nonebot.adapters import Bot from nonebot.config import Config from pydantic import BaseModel +class BotManageUpdateParam(BaseModel): + """bot更新参数""" + + bot_id: str + """bot id""" + block_plugins: list[str] + """禁用插件""" + block_tasks: list[str] + """禁用被动""" + + +class BotStatusParam(BaseModel): + """bot状态参数""" + + bot_id: str + """bot id""" + status: bool + """状态""" + + +class BotBlockModule(BaseModel): + """bot禁用模块参数""" + + bot_id: str + """bot id""" + block_plugins: list[str] + """禁用插件""" + block_tasks: list[str] + """禁用被动""" + all_plugins: list[dict[str, Any]] + """所有插件""" + all_tasks: list[dict[str, Any]] + """所有被动""" + + class SystemStatus(BaseModel): """ 系统状态 @@ -36,13 +73,8 @@ class BaseInfo(BaseModel): """连接日期""" connect_count: int = 0 """连接次数""" - - plugin_count: int = 0 - """加载插件数量""" - success_plugin_count: int = 0 - """加载成功插件数量""" - fail_plugin_count: int = 0 - """加载失败插件数量""" + status: bool = False + """全局状态""" is_select: bool = False """当前选择""" diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/manage/__init__.py b/zhenxun/builtin_plugins/web_ui/api/tabs/manage/__init__.py index 12d3cbdb..87288465 100644 --- a/zhenxun/builtin_plugins/web_ui/api/tabs/manage/__init__.py +++ b/zhenxun/builtin_plugins/web_ui/api/tabs/manage/__init__.py @@ -2,16 +2,9 @@ from fastapi import APIRouter from fastapi.responses import JSONResponse import nonebot from nonebot.adapters.onebot.v11 import ActionFailed -from tortoise.functions import Count -from zhenxun.configs.config import BotConfig -from zhenxun.models.ban_console import BanConsole -from zhenxun.models.chat_history import ChatHistory from zhenxun.models.fg_request import FgRequest from zhenxun.models.group_console import GroupConsole -from zhenxun.models.plugin_info import PluginInfo -from zhenxun.models.statistics import Statistics -from zhenxun.models.task_info import TaskInfo from zhenxun.services.log import logger from zhenxun.utils.enum import RequestHandleType, RequestType from zhenxun.utils.exception import NotFoundError @@ -20,20 +13,17 @@ from zhenxun.utils.platform import PlatformUtils from ....base_model import Result from ....config import AVA_URL, GROUP_AVA_URL from ....utils import authentication +from .data_source import ApiDataSource from .model import ( ClearRequest, DeleteFriend, Friend, - FriendRequestResult, GroupDetail, - GroupRequestResult, GroupResult, HandleRequest, LeaveGroup, - Plugin, ReqResult, - SendMessage, - Task, + SendMessageParam, UpdateGroup, UserDetail, ) @@ -52,19 +42,21 @@ async def _(bot_id: str) -> Result: """ 获取群信息 """ - if not (bots := nonebot.get_bots()): - return Result.warning_("无Bot连接...") - if bot_id not in bots: - return Result.warning_("指定Bot未连接...") group_list_result = [] try: - group_list = await bots[bot_id].get_group_list() + bot = nonebot.get_bot(bot_id) + group_list, _ = await PlatformUtils.get_group_list(bot) for g in group_list: - gid = g["group_id"] - g["ava_url"] = GROUP_AVA_URL.format(gid, gid) - group_list_result.append(GroupResult(**g)) + ava_url = GROUP_AVA_URL.format(g.group_id, g.group_id) + group_list_result.append( + GroupResult( + group_id=g.group_id, group_name=g.group_name, ava_url=ava_url + ) + ) + except (ValueError, KeyError): + return Result.warning_("指定Bot未连接...") except Exception as e: - logger.error("调用API错误", "/get_group_list", e=e) + logger.error(f"{router.prefix}/get_group_list 调用错误", "WebUi", e=e) return Result.fail(f"{type(e)}: {e}") return Result.ok(group_list_result, "拿到了新鲜出炉的数据!") @@ -78,25 +70,11 @@ async def _(bot_id: str) -> Result: ) async def _(group: UpdateGroup) -> Result[str]: try: - group_id = group.group_id - if db_group := await GroupConsole.get_group(group_id): - task_list = await TaskInfo.all().values_list("module", flat=True) - db_group.level = group.level - db_group.status = group.status - if group.close_plugins: - group.close_plugins = [f"<{module}" for module in group.close_plugins] - db_group.block_plugin = ",".join(group.close_plugins) + "," - if group.task: - if block_task := [t for t in task_list if t not in group.task]: - block_task = [f"<{module}" for module in block_task] - db_group.block_task = ",".join(block_task) + "," # type: ignore - await db_group.save( - update_fields=["level", "status", "block_plugin", "block_task"] - ) + await ApiDataSource.update_group(group) + return Result.ok(info="已完成记录!") except Exception as e: - logger.error("调用API错误", "/get_group", e=e) - return Result.fail(f"{type(e)}: {e}") - return Result.ok(info="已完成记录!") + logger.error(f"{router.prefix}/update_group 调用错误", "WebUi", e=e) + return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") @router.get( @@ -110,24 +88,24 @@ async def _(bot_id: str) -> Result[list[Friend]]: """ 获取群信息 """ - if bots := nonebot.get_bots(): - if bot_id not in bots: - return Result.warning_("指定Bot未连接...") - try: - platform = PlatformUtils.get_platform(bots[bot_id]) - if platform != "qq": - return Result.warning_("该平台暂不支持该功能...") - friend_list = await bots[bot_id].get_friend_list() - for f in friend_list: - f["ava_url"] = AVA_URL.format(f["user_id"]) - return Result.ok( - [Friend(**f) for f in friend_list if str(f["user_id"]) != bot_id], - "拿到了新鲜出炉的数据!", + try: + bot = nonebot.get_bot(bot_id) + friend_list, _ = await PlatformUtils.get_friend_list(bot) + result_list = [] + for f in friend_list: + ava_url = AVA_URL.format(f.user_id) + result_list.append( + Friend(user_id=f.user_id, nickname=f.nickname, ava_url=ava_url) ) - except Exception as e: - logger.error("调用API错误", "/get_group_list", e=e) - return Result.fail(f"{type(e)}: {e}") - return Result.warning_("无Bot连接...") + return Result.ok( + result_list, + "拿到了新鲜出炉的数据!", + ) + except (ValueError, KeyError): + return Result.warning_("指定Bot未连接...") + except Exception as e: + logger.error("调用API错误", "/get_group_list", e=e) + return Result.fail(f"{type(e)}: {e}") @router.get( @@ -138,17 +116,21 @@ async def _(bot_id: str) -> Result[list[Friend]]: description="获取请求数量", ) async def _() -> Result[dict[str, int]]: - f_count = await FgRequest.filter( - request_type=RequestType.FRIEND, handle_type__isnull=True - ).count() - g_count = await FgRequest.filter( - request_type=RequestType.GROUP, handle_type__isnull=True - ).count() - data = { - "friend_count": f_count, - "group_count": g_count, - } - return Result.ok(data, f"{BotConfig.self_nickname}带来了最新的数据!") + try: + f_count = await FgRequest.filter( + request_type=RequestType.FRIEND, handle_type__isnull=True + ).count() + g_count = await FgRequest.filter( + request_type=RequestType.GROUP, handle_type__isnull=True + ).count() + data = { + "friend_count": f_count, + "group_count": g_count, + } + return Result.ok(data, "拿到了新鲜出炉的数据!") + except Exception as e: + logger.error("调用API错误", "/get_request_count", e=e) + return Result.fail(f"{type(e)}: {e}") @router.get( @@ -160,43 +142,10 @@ async def _() -> Result[dict[str, int]]: ) async def _() -> Result[ReqResult]: try: - req_result = ReqResult() - data_list = await FgRequest.filter(handle_type__isnull=True).all() - for req in data_list: - if req.request_type == RequestType.FRIEND: - req_result.friend.append( - FriendRequestResult( - oid=req.id, - bot_id=req.bot_id, - id=req.user_id, - flag=req.flag, - nickname=req.nickname, - comment=req.comment, - ava_url=AVA_URL.format(req.user_id), - type=str(req.request_type).lower(), - ) - ) - else: - req_result.group.append( - GroupRequestResult( - oid=req.id, - bot_id=req.bot_id, - id=req.user_id, - flag=req.flag, - nickname=req.nickname, - comment=req.comment, - ava_url=GROUP_AVA_URL.format(req.group_id, req.group_id), - type=str(req.request_type).lower(), - invite_group=req.group_id, - group_name=None, - ) - ) - req_result.friend.reverse() - req_result.group.reverse() + return Result.ok(await ApiDataSource.get_request_list(), "拿到信息啦!") except Exception as e: - logger.error("调用API错误", "/get_request", e=e) - return Result.fail(f"{type(e)}: {e}") - return Result.ok(req_result, f"{BotConfig.self_nickname}带来了最新的数据!") + logger.error(f"{router.prefix}/get_request_list 调用错误", "WebUi", e=e) + return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") @router.post( @@ -220,23 +169,21 @@ async def _(cr: ClearRequest) -> Result: response_class=JSONResponse, description="拒绝请求", ) -async def _(parma: HandleRequest) -> Result: +async def _(param: HandleRequest) -> Result: try: - if bots := nonebot.get_bots(): - bot_id = parma.bot_id - if bot_id not in nonebot.get_bots(): - return Result.warning_("指定Bot未连接...") - try: - await FgRequest.refused(bots[bot_id], parma.id) - except ActionFailed: - await FgRequest.expire(parma.id) - return Result.warning_("请求失败,可能该请求已失效或请求数据错误...") - except NotFoundError: - return Result.warning_("未找到此Id请求...") - return Result.ok(info="成功处理了请求!") - return Result.warning_("无Bot连接...") + bot = nonebot.get_bot(param.bot_id) + try: + await FgRequest.refused(bot, param.id) + except ActionFailed: + await FgRequest.expire(param.id) + return Result.warning_("请求失败,可能该请求已失效或请求数据错误...") + except NotFoundError: + return Result.warning_("未找到此Id请求...") + return Result.ok(info="成功处理了请求!") + except (ValueError, KeyError): + return Result.warning_("指定Bot未连接...") except Exception as e: - logger.error("调用API错误", "/refuse_request", e=e) + logger.error(f"{router.prefix}/refuse_request 调用错误", "WebUi", e=e) return Result.fail(f"{type(e)}: {e}") @@ -247,8 +194,8 @@ async def _(parma: HandleRequest) -> Result: response_class=JSONResponse, description="忽略请求", ) -async def _(parma: HandleRequest) -> Result: - await FgRequest.ignore(parma.id) +async def _(param: HandleRequest) -> Result: + await FgRequest.ignore(param.id) return Result.ok(info="成功处理了请求!") @@ -259,32 +206,30 @@ async def _(parma: HandleRequest) -> Result: response_class=JSONResponse, description="同意请求", ) -async def _(parma: HandleRequest) -> Result: +async def _(param: HandleRequest) -> Result: try: - if bots := nonebot.get_bots(): - bot_id = parma.bot_id - if bot_id not in nonebot.get_bots(): - return Result.warning_("指定Bot未连接...") - if not (req := await FgRequest.get_or_none(id=parma.id)): - return Result.warning_("未找到此Id请求...") - if req.request_type == RequestType.GROUP: - if group := await GroupConsole.get_group(group_id=req.group_id): - group.group_flag = 1 - await group.save(update_fields=["group_flag"]) - else: - await GroupConsole.update_or_create( - group_id=req.group_id, - defaults={"group_flag": 1}, - ) - try: - await FgRequest.approve(bots[bot_id], parma.id) - return Result.ok(info="成功处理了请求!") - except ActionFailed: - await FgRequest.expire(parma.id) - return Result.warning_("请求失败,可能该请求已失效或请求数据错误...") - return Result.warning_("无Bot连接...") + bot = nonebot.get_bot(param.bot_id) + if not (req := await FgRequest.get_or_none(id=param.id)): + return Result.warning_("未找到此Id请求...") + if req.request_type == RequestType.GROUP: + if group := await GroupConsole.get_group(group_id=req.group_id): + group.group_flag = 1 + await group.save(update_fields=["group_flag"]) + else: + await GroupConsole.update_or_create( + group_id=req.group_id, + defaults={"group_flag": 1}, + ) + try: + await FgRequest.approve(bot, param.id) + return Result.ok(info="成功处理了请求!") + except ActionFailed: + await FgRequest.expire(param.id) + return Result.warning_("请求失败,可能该请求已失效或请求数据错误...") + except (ValueError, KeyError): + return Result.warning_("指定Bot未连接...") except Exception as e: - logger.error("调用API错误", "/approve_request", e=e) + logger.error(f"{router.prefix}/approve_request 调用错误", "WebUi", e=e) return Result.fail(f"{type(e)}: {e}") @@ -297,19 +242,19 @@ async def _(parma: HandleRequest) -> Result: ) async def _(param: LeaveGroup) -> Result: try: - if bots := nonebot.get_bots(): - bot_id = param.bot_id - platform = PlatformUtils.get_platform(bots[bot_id]) - if platform != "qq": - return Result.warning_("该平台不支持退群操作...") - group_list = await bots[bot_id].get_group_list() - if param.group_id not in [str(g["group_id"]) for g in group_list]: - return Result.warning_("Bot未在该群聊中...") - await bots[bot_id].set_group_leave(group_id=param.group_id) - return Result.ok(info="成功处理了请求!") - return Result.warning_("无Bot连接...") + bot = nonebot.get_bot(param.bot_id) + platform = PlatformUtils.get_platform(bot) + if platform != "qq": + return Result.warning_("该平台不支持退群操作...") + group_list, _ = await PlatformUtils.get_group_list(bot) + if param.group_id not in [g.group_id for g in group_list]: + return Result.warning_("Bot未在该群聊中...") + await bot.set_group_leave(group_id=param.group_id) + return Result.ok(info="成功处理了请求!") + except (ValueError, KeyError): + return Result.warning_("指定Bot未连接...") except Exception as e: - logger.error("调用API错误", "/leave_group", e=e) + logger.error(f"{router.prefix}/leave_group 调用错误", "WebUi", e=e) return Result.fail(f"{type(e)}: {e}") @@ -322,19 +267,19 @@ async def _(param: LeaveGroup) -> Result: ) async def _(param: DeleteFriend) -> Result: try: - if bots := nonebot.get_bots(): - bot_id = param.bot_id - platform = PlatformUtils.get_platform(bots[bot_id]) - if platform != "qq": - return Result.warning_("该平台不支持删除好友操作...") - friend_list = await bots[bot_id].get_friend_list() - if param.user_id not in [str(g["user_id"]) for g in friend_list]: - return Result.warning_("Bot未有其好友...") - await bots[bot_id].delete_friend(user_id=param.user_id) - return Result.ok(info="成功处理了请求!") - return Result.warning_("Bot未连接...") + bot = nonebot.get_bot(param.bot_id) + platform = PlatformUtils.get_platform(bot) + if platform != "qq": + return Result.warning_("该平台不支持删除好友操作...") + friend_list, _ = await PlatformUtils.get_friend_list(bot) + if param.user_id not in [f.user_id for f in friend_list]: + return Result.warning_("Bot未有其好友...") + await bot.delete_friend(user_id=param.user_id) + return Result.ok(info="成功处理了请求!") + except (ValueError, KeyError): + return Result.warning_("指定Bot未连接...") except Exception as e: - logger.error("调用API错误", "/delete_friend", e=e) + logger.error(f"{router.prefix}/delete_friend 调用错误", "WebUi", e=e) return Result.fail(f"{type(e)}: {e}") @@ -346,43 +291,18 @@ async def _(param: DeleteFriend) -> Result: description="获取好友详情", ) async def _(bot_id: str, user_id: str) -> Result[UserDetail]: - if bots := nonebot.get_bots(): - if bot_id in bots: - if fd := [ - x - for x in await bots[bot_id].get_friend_list() - if str(x["user_id"]) == user_id - ]: - like_plugin_list = ( - await Statistics.filter(user_id=user_id) - .annotate(count=Count("id")) - .group_by("plugin_name") - .order_by("-count") - .limit(5) - .values_list("plugin_name", "count") - ) - like_plugin = {} - module_list = [x[0] for x in like_plugin_list] - plugins = await PluginInfo.filter(module__in=module_list).all() - module2name = {p.module: p.name for p in plugins} - for data in like_plugin_list: - name = module2name.get(data[0]) or data[0] - like_plugin[name] = data[1] - user = fd[0] - user_detail = UserDetail( - user_id=user_id, - ava_url=AVA_URL.format(user_id), - nickname=user["nickname"], - remark=user["remark"], - is_ban=await BanConsole.is_ban(user_id), - chat_count=await ChatHistory.filter(user_id=user_id).count(), - call_count=await Statistics.filter(user_id=user_id).count(), - like_plugin=like_plugin, - ) - return Result.ok(user_detail) - else: - return Result.warning_("未添加指定好友...") - return Result.warning_("无Bot连接...") + try: + result = await ApiDataSource.get_friend_detail(bot_id, user_id) + return ( + Result.ok(result, "拿到信息啦!") + if result + else Result.warning_("未找到该好友...") + ) + except (ValueError, KeyError): + return Result.warning_("指定Bot未连接...") + except Exception as e: + logger.error(f"{router.prefix}/get_friend_detail 调用错误", "WebUi", e=e) + return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") @router.get( @@ -392,90 +312,12 @@ async def _(bot_id: str, user_id: str) -> Result[UserDetail]: response_class=JSONResponse, description="获取群组详情", ) -async def _(bot_id: str, group_id: str) -> Result[GroupDetail]: - if not (bots := nonebot.get_bots()): - return Result.warning_("无Bot连接...") - if bot_id not in bots: - return Result.warning_("未添加指定群组...") - group = await GroupConsole.get_or_none(group_id=group_id) - if not group: - return Result.warning_("指定群组未被收录...") - like_plugin_list = ( - await Statistics.filter(group_id=group_id) - .annotate(count=Count("id")) - .group_by("plugin_name") - .order_by("-count") - .limit(5) - .values_list("plugin_name", "count") - ) - like_plugin = {} - plugins = await PluginInfo.get_plugins() - module2name = {p.module: p.name for p in plugins} - for data in like_plugin_list: - name = module2name.get(data[0]) or data[0] - like_plugin[name] = data[1] - close_plugins: list[Plugin] = [] - if group.block_plugin: - for module in group.block_plugin.replace("<", "").split(","): - if module: - plugin = Plugin( - module=module, - plugin_name=module, - is_super_block=False, - ) - plugin.plugin_name = module2name.get(module) or module - close_plugins.append(plugin) - exists_modules = [p.module for p in close_plugins] - if group.superuser_block_plugin: - for module in group.superuser_block_plugin.replace("<", "").split(","): - if module and module not in exists_modules: - plugin = Plugin( - module=module, - plugin_name=module, - is_super_block=True, - ) - plugin.plugin_name = module2name.get(module) or module - close_plugins.append(plugin) - all_task = await TaskInfo.annotate().values_list("module", "name") - task_module2name = {x[0]: x[1] for x in all_task} - task_list = [] - if group.block_task or group.superuser_block_plugin: - sbp = group.superuser_block_plugin.replace("<", "").split(",") - split_task = group.block_task.replace("<", "").split(",") - for task in all_task: - task_list.append( - Task( - name=task[0], - zh_name=task_module2name.get(task[0]) or task[0], - status=task[0] not in split_task and task[0] not in sbp, - is_super_block=task[0] in sbp, - ) - ) - else: - for task in all_task: - task_list.append( - Task( - name=task[0], - zh_name=task_module2name.get(task[0]) or task[0], - status=True, - is_super_block=False, - ) - ) - group_detail = GroupDetail( - group_id=group_id, - ava_url=GROUP_AVA_URL.format(group_id, group_id), - name=group.group_name, - member_count=group.member_count, - max_member_count=group.max_member_count, - chat_count=await ChatHistory.filter(group_id=group_id).count(), - call_count=await Statistics.filter(group_id=group_id).count(), - like_plugin=like_plugin, - level=group.level, - status=group.status, - close_plugins=close_plugins, - task=task_list, - ) - return Result.ok(group_detail) +async def _(group_id: str) -> Result[GroupDetail]: + try: + return Result.ok(await ApiDataSource.get_group_detail(group_id), "拿到信息啦!") + except Exception as e: + logger.error(f"{router.prefix}/get_group_detail 调用错误", "WebUi", e=e) + return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") @router.post( @@ -483,25 +325,17 @@ async def _(bot_id: str, group_id: str) -> Result[GroupDetail]: dependencies=[authentication()], response_model=Result, response_class=JSONResponse, - description="获取群组详情", + description="发送消息", ) -async def _(param: SendMessage) -> Result: - if not (bots := nonebot.get_bots()): - return Result.warning_("无Bot连接...") - if param.bot_id in bots: - platform = PlatformUtils.get_platform(bots[param.bot_id]) - if platform != "qq": - return Result.warning_("暂不支持该平台...") - try: - if param.user_id: - await bots[param.bot_id].send_private_msg( - user_id=str(param.user_id), message=param.message - ) - else: - await bots[param.bot_id].send_group_msg( - group_id=str(param.group_id), message=param.message - ) - except Exception as e: - return Result.fail(str(e)) +async def _(param: SendMessageParam) -> Result: + try: + bot = nonebot.get_bot(param.bot_id) + await PlatformUtils.send_message( + bot, param.user_id, param.group_id, param.message + ) return Result.ok("发送成功!") - return Result.warning_("指定Bot未连接...") + except (ValueError, KeyError): + return Result.warning_("指定Bot未连接...") + except Exception as e: + logger.error(f"{router.prefix}/send_message 调用错误", "WebUi", e=e) + return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/manage/chat.py b/zhenxun/builtin_plugins/web_ui/api/tabs/manage/chat.py index 164e260b..62b2f959 100644 --- a/zhenxun/builtin_plugins/web_ui/api/tabs/manage/chat.py +++ b/zhenxun/builtin_plugins/web_ui/api/tabs/manage/chat.py @@ -3,7 +3,7 @@ import nonebot from nonebot import on_message from nonebot.adapters.onebot.v11 import MessageEvent from nonebot_plugin_alconna import At, Hyper, Image, Text, UniMsg -from nonebot_plugin_session import EventSession +from nonebot_plugin_uninfo import Uninfo from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState from zhenxun.models.group_member_info import GroupInfoUser @@ -28,7 +28,7 @@ matcher = on_message(block=False, priority=1, rule=lambda: bool(ws_conn)) @driver.on_shutdown async def _(): - if ws_conn: + if ws_conn and ws_conn.client_state == WebSocketState.CONNECTED: await ws_conn.close() @@ -36,7 +36,7 @@ async def _(): async def _(websocket: WebSocket): global ws_conn await websocket.accept() - if not ws_conn: + if not ws_conn or ws_conn.client_state != WebSocketState.CONNECTED: ws_conn = websocket try: while websocket.client_state == WebSocketState.CONNECTED: @@ -80,25 +80,24 @@ async def message_handle( @matcher.handle() async def _( - message: UniMsg, event: MessageEvent, session: EventSession, uname: str = UserName() + message: UniMsg, event: MessageEvent, session: Uninfo, uname: str = UserName() ): global ws_conn, ID2NAME, ID_LIST - uid = session.id1 - if ws_conn and ws_conn.client_state == WebSocketState.CONNECTED and uid: + if ws_conn and ws_conn.client_state == WebSocketState.CONNECTED: msg_id = event.message_id if msg_id in ID_LIST: return ID_LIST.append(msg_id) if len(ID_LIST) > 50: ID_LIST = ID_LIST[40:] - gid = session.id3 or session.id2 + gid = session.group.id if session.group else None messages = await message_handle(message, gid) data = Message( - object_id=gid or uid, - user_id=uid, + object_id=gid or session.user.id, + user_id=session.user.id, group_id=gid, message=messages, name=uname, - ava_url=AVA_URL.format(uid), + ava_url=AVA_URL.format(session.user.id), ) await ws_conn.send_json(data.dict()) diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/manage/data_source.py b/zhenxun/builtin_plugins/web_ui/api/tabs/manage/data_source.py new file mode 100644 index 00000000..f573fb5b --- /dev/null +++ b/zhenxun/builtin_plugins/web_ui/api/tabs/manage/data_source.py @@ -0,0 +1,274 @@ +import nonebot +from tortoise.functions import Count + +from zhenxun.models.ban_console import BanConsole +from zhenxun.models.chat_history import ChatHistory +from zhenxun.models.fg_request import FgRequest +from zhenxun.models.group_console import GroupConsole +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.models.statistics import Statistics +from zhenxun.models.task_info import TaskInfo +from zhenxun.utils.common_utils import CommonUtils +from zhenxun.utils.enum import RequestType +from zhenxun.utils.platform import PlatformUtils + +from ....config import AVA_URL, GROUP_AVA_URL +from .model import ( + FriendRequestResult, + GroupDetail, + GroupRequestResult, + Plugin, + ReqResult, + Task, + UpdateGroup, + UserDetail, +) + + +class ApiDataSource: + @classmethod + async def update_group(cls, group: UpdateGroup): + """更新群组数据 + + 参数: + group: UpdateGroup + """ + db_group = await GroupConsole.get_group(group.group_id) or GroupConsole( + group_id=group.group_id + ) + task_list = await TaskInfo.all().values_list("module", flat=True) + db_group.level = group.level + db_group.status = group.status + if group.close_plugins: + db_group.block_plugin = CommonUtils.convert_module_format( + group.close_plugins + ) + else: + db_group.block_plugin = "" + if group.task: + if block_task := [t for t in task_list if t not in group.task]: + db_group.block_task = CommonUtils.convert_module_format(block_task) # type: ignore + else: + db_group.block_task = CommonUtils.convert_module_format(task_list) # type: ignore + await db_group.save() + + @classmethod + async def get_request_list(cls) -> ReqResult: + """获取好友与群组请求列表 + + 返回: + ReqResult: 数据内容 + """ + req_result = ReqResult() + data_list = await FgRequest.filter(handle_type__isnull=True).all() + for req in data_list: + if req.request_type == RequestType.FRIEND: + req_result.friend.append( + FriendRequestResult( + oid=req.id, + bot_id=req.bot_id, + id=req.user_id, + flag=req.flag, + nickname=req.nickname, + comment=req.comment, + ava_url=AVA_URL.format(req.user_id), + type=str(req.request_type).lower(), + ) + ) + else: + req_result.group.append( + GroupRequestResult( + oid=req.id, + bot_id=req.bot_id, + id=req.user_id, + flag=req.flag, + nickname=req.nickname, + comment=req.comment, + ava_url=GROUP_AVA_URL.format(req.group_id, req.group_id), + type=str(req.request_type).lower(), + invite_group=req.group_id, + group_name=None, + ) + ) + req_result.friend.reverse() + req_result.group.reverse() + return req_result + + @classmethod + async def get_friend_detail(cls, bot_id: str, user_id: str) -> UserDetail | None: + """获取好友详情 + + 参数: + bot_id: bot id + user_id: 用户id + + 返回: + UserDetail | None: 详情数据 + """ + bot = nonebot.get_bot(bot_id) + friend_list, _ = await PlatformUtils.get_friend_list(bot) + fd = [x for x in friend_list if x == user_id] + if not fd: + return None + like_plugin_list = ( + await Statistics.filter(user_id=user_id) + .annotate(count=Count("id")) + .group_by("plugin_name") + .order_by("-count") + .limit(5) + .values_list("plugin_name", "count") + ) + like_plugin = {} + module_list = [x[0] for x in like_plugin_list] + plugins = await PluginInfo.filter(module__in=module_list).all() + module2name = {p.module: p.name for p in plugins} + for data in like_plugin_list: + name = module2name.get(data[0]) or data[0] + like_plugin[name] = data[1] + user = fd[0] + return UserDetail( + user_id=user_id, + ava_url=AVA_URL.format(user_id), + nickname=user.user_name, + remark="", + is_ban=await BanConsole.is_ban(user_id), + chat_count=await ChatHistory.filter(user_id=user_id).count(), + call_count=await Statistics.filter(user_id=user_id).count(), + like_plugin=like_plugin, + ) + + @classmethod + async def __get_group_detail_like_plugin(cls, group_id: str) -> dict[str, int]: + """获取群组喜爱的插件 + + 参数: + group_id: 群组id + + 返回: + dict[str, int]: 插件与调用次数 + """ + like_plugin_list = ( + await Statistics.filter(group_id=group_id) + .annotate(count=Count("id")) + .group_by("plugin_name") + .order_by("-count") + .limit(5) + .values_list("plugin_name", "count") + ) + like_plugin = {} + plugins = await PluginInfo.get_plugins() + module2name = {p.module: p.name for p in plugins} + for data in like_plugin_list: + name = module2name.get(data[0]) or data[0] + like_plugin[name] = data[1] + return like_plugin + + @classmethod + async def __get_group_detail_disable_plugin( + cls, group: GroupConsole + ) -> list[Plugin]: + """获取群组禁用插件 + + 参数: + group: GroupConsole + + 返回: + list[Plugin]: 禁用插件数据列表 + """ + disable_plugins: list[Plugin] = [] + plugins = await PluginInfo.get_plugins() + module2name = {p.module: p.name for p in plugins} + if group.block_plugin: + for module in CommonUtils.convert_module_format(group.block_plugin): + if module: + plugin = Plugin( + module=module, + plugin_name=module, + is_super_block=False, + ) + plugin.plugin_name = module2name.get(module) or module + disable_plugins.append(plugin) + exists_modules = [p.module for p in disable_plugins] + if group.superuser_block_plugin: + for module in CommonUtils.convert_module_format( + group.superuser_block_plugin + ): + if module and module not in exists_modules: + plugin = Plugin( + module=module, + plugin_name=module, + is_super_block=True, + ) + plugin.plugin_name = module2name.get(module) or module + disable_plugins.append(plugin) + return disable_plugins + + @classmethod + async def __get_group_detail_task(cls, group: GroupConsole) -> list[Task]: + """获取群组被动技能状态 + + 参数: + group: GroupConsole + + 返回: + list[Task]: 群组被动列表 + """ + all_task = await TaskInfo.annotate().values_list("module", "name") + task_module2name = {x[0]: x[1] for x in all_task} + task_list = [] + if group.block_task or group.superuser_block_plugin: + sbp = CommonUtils.convert_module_format(group.superuser_block_task) + tasks = CommonUtils.convert_module_format(group.block_task) + task_list.extend( + Task( + name=task[0], + zh_name=task_module2name.get(task[0]) or task[0], + status=task[0] not in tasks and task[0] not in sbp, + is_super_block=task[0] in sbp, + ) + for task in all_task + ) + else: + task_list.extend( + Task( + name=task[0], + zh_name=task_module2name.get(task[0]) or task[0], + status=True, + is_super_block=False, + ) + for task in all_task + ) + return task_list + + @classmethod + async def get_group_detail(cls, group_id: str) -> GroupDetail | None: + """获取群组详情 + + 参数: + group_id: 群组id + + 返回: + GroupDetail | None: 群组详情数据 + """ + group = await GroupConsole.get_or_none(group_id=group_id) + if not group: + return None + like_plugin = await cls.__get_group_detail_like_plugin(group_id) + disable_plugins: list[Plugin] = await cls.__get_group_detail_disable_plugin( + group + ) + task_list = await cls.__get_group_detail_task(group) + return GroupDetail( + group_id=group_id, + ava_url=GROUP_AVA_URL.format(group_id, group_id), + name=group.group_name, + member_count=group.member_count, + max_member_count=group.max_member_count, + chat_count=await ChatHistory.filter(group_id=group_id).count(), + call_count=await Statistics.filter(group_id=group_id).count(), + like_plugin=like_plugin, + level=group.level, + status=group.status, + close_plugins=disable_plugins, + task=task_list, + ) diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/manage/model.py b/zhenxun/builtin_plugins/web_ui/api/tabs/manage/model.py index 60833870..9f5d9fd4 100644 --- a/zhenxun/builtin_plugins/web_ui/api/tabs/manage/model.py +++ b/zhenxun/builtin_plugins/web_ui/api/tabs/manage/model.py @@ -257,7 +257,7 @@ class Message(BaseModel): """用户头像""" -class SendMessage(BaseModel): +class SendMessageParam(BaseModel): """ 发送消息 """ diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/__init__.py b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/__init__.py index fb77ac2d..3e45ad03 100644 --- a/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/__init__.py +++ b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/__init__.py @@ -1,18 +1,14 @@ -import re - -import cattrs from fastapi import APIRouter, Query from fastapi.responses import JSONResponse -from zhenxun.configs.config import Config from zhenxun.models.plugin_info import PluginInfo as DbPluginInfo from zhenxun.services.log import logger from zhenxun.utils.enum import BlockType, PluginType from ....base_model import Result from ....utils import authentication +from .data_source import ApiDataSource from .model import ( - PluginConfig, PluginCount, PluginDetail, PluginInfo, @@ -34,31 +30,12 @@ async def _( plugin_type: list[PluginType] = Query(None), menu_type: str | None = None ) -> Result[list[PluginInfo]]: try: - plugin_list: list[PluginInfo] = [] - query = DbPluginInfo - if plugin_type: - query = query.filter(plugin_type__in=plugin_type, load_status=True) - if menu_type: - query = query.filter(menu_type=menu_type) - plugins = await query.all() - for plugin in plugins: - plugin_info = PluginInfo( - module=plugin.module, - plugin_name=plugin.name, - default_status=plugin.default_status, - limit_superuser=plugin.limit_superuser, - cost_gold=plugin.cost_gold, - menu_type=plugin.menu_type, - version=plugin.version or "0", - level=plugin.level, - status=plugin.status, - author=plugin.author, - ) - plugin_list.append(plugin_info) + return Result.ok( + await ApiDataSource.get_plugin_list(plugin_type, menu_type), "拿到信息啦!" + ) except Exception as e: - logger.error("调用API错误", "/get_plugins", e=e) - return Result.fail(f"{type(e)}: {e}") - return Result.ok(plugin_list, "拿到了新鲜出炉的数据!") + logger.error(f"{router.prefix}/get_plugin_list 调用错误", "WebUi", e=e) + return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") @router.get( @@ -69,21 +46,26 @@ async def _( deprecated="获取插件数量", # type: ignore ) async def _() -> Result[int]: - plugin_count = PluginCount() - plugin_count.normal = await DbPluginInfo.filter( - plugin_type=PluginType.NORMAL, load_status=True - ).count() - plugin_count.admin = await DbPluginInfo.filter( - plugin_type__in=[PluginType.ADMIN, PluginType.SUPER_AND_ADMIN], load_status=True - ).count() - plugin_count.superuser = await DbPluginInfo.filter( - plugin_type__in=[PluginType.SUPERUSER, PluginType.SUPER_AND_ADMIN], - load_status=True, - ).count() - plugin_count.other = await DbPluginInfo.filter( - plugin_type__in=[PluginType.HIDDEN, PluginType.DEPENDANT], load_status=True - ).count() - return Result.ok(plugin_count) + try: + plugin_count = PluginCount() + plugin_count.normal = await DbPluginInfo.filter( + plugin_type=PluginType.NORMAL, load_status=True + ).count() + plugin_count.admin = await DbPluginInfo.filter( + plugin_type__in=[PluginType.ADMIN, PluginType.SUPER_AND_ADMIN], + load_status=True, + ).count() + plugin_count.superuser = await DbPluginInfo.filter( + plugin_type__in=[PluginType.SUPERUSER, PluginType.SUPER_AND_ADMIN], + load_status=True, + ).count() + plugin_count.other = await DbPluginInfo.filter( + plugin_type__in=[PluginType.HIDDEN, PluginType.DEPENDANT], load_status=True + ).count() + return Result.ok(plugin_count, "拿到信息啦!") + except Exception as e: + logger.error(f"{router.prefix}/get_plugin_count 调用错误", "WebUi", e=e) + return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") @router.post( @@ -93,33 +75,15 @@ async def _() -> Result[int]: response_class=JSONResponse, description="更新插件参数", ) -async def _(plugin: UpdatePlugin) -> Result: +async def _(param: UpdatePlugin) -> Result: try: - db_plugin = await DbPluginInfo.get_or_none( - module=plugin.module, load_status=True - ) - if not db_plugin: - return Result.fail("插件不存在...") - db_plugin.default_status = plugin.default_status - db_plugin.limit_superuser = plugin.limit_superuser - db_plugin.cost_gold = plugin.cost_gold - db_plugin.level = plugin.level - db_plugin.menu_type = plugin.menu_type - db_plugin.block_type = plugin.block_type - db_plugin.status = plugin.block_type != BlockType.ALL - await db_plugin.save() - # 配置项 - if plugin.configs and (configs := Config.get(plugin.module)): - for key in plugin.configs: - if c := configs.configs.get(key): - value = plugin.configs[key] - if c.type and value is not None: - value = cattrs.structure(value, c.type) - Config.set_config(plugin.module, key, value) + await ApiDataSource.update_plugin(param) + return Result.ok(info="已经帮你写好啦!") + except (ValueError, KeyError): + return Result.fail("插件数据不存在...") except Exception as e: - logger.error("调用API错误", "/update_plugins", e=e) + logger.error(f"{router.prefix}/update_plugin 调用错误", "WebUi", e=e) return Result.fail(f"{type(e)}: {e}") - return Result.ok(info="已经帮你写好啦!") @router.post( @@ -130,17 +94,21 @@ async def _(plugin: UpdatePlugin) -> Result: description="开关插件", ) async def _(param: PluginSwitch) -> Result: - db_plugin = await DbPluginInfo.get_or_none(module=param.module, load_status=True) - if not db_plugin: - return Result.fail("插件不存在...") - if not param.status: - db_plugin.block_type = BlockType.ALL - db_plugin.status = False - else: - db_plugin.block_type = None - db_plugin.status = True - await db_plugin.save() - return Result.ok(info="成功改变了开关状态!") + try: + db_plugin = await DbPluginInfo.get_plugin(module=param.module) + if not db_plugin: + return Result.fail("插件不存在...") + if not param.status: + db_plugin.block_type = BlockType.ALL + db_plugin.status = False + else: + db_plugin.block_type = None + db_plugin.status = True + await db_plugin.save() + return Result.ok(info="成功改变了开关状态!") + except Exception as e: + logger.error(f"{router.prefix}/change_switch 调用错误", "WebUi", e=e) + return Result.fail(f"{type(e)}: {e}") @router.get( @@ -151,12 +119,20 @@ async def _(param: PluginSwitch) -> Result: description="获取插件类型", ) async def _() -> Result[list[str]]: - menu_type_list = [] - result = await DbPluginInfo.annotate().values_list("menu_type", flat=True) - for r in result: - if r not in menu_type_list and r: - menu_type_list.append(r) - return Result.ok(menu_type_list) + try: + menu_type_list = [] + result = ( + await DbPluginInfo.filter(load_status=True) + .annotate() + .values_list("menu_type", flat=True) + ) + for r in result: + if r not in menu_type_list and r: + menu_type_list.append(r) + return Result.ok(menu_type_list) + except Exception as e: + logger.error(f"{router.prefix}/get_plugin_menu_type 调用错误", "WebUi", e=e) + return Result.fail(f"{type(e)}: {e}") @router.get( @@ -167,46 +143,12 @@ async def _() -> Result[list[str]]: description="获取插件详情", ) async def _(module: str) -> Result[PluginDetail]: - db_plugin = await DbPluginInfo.get_or_none(module=module, load_status=True) - if not db_plugin: - return Result.fail("插件不存在...") - config_list = [] - if config := Config.get(module): - for cfg in config.configs: - type_str = "" - type_inner = None - if r := re.search(r"", str(config.configs[cfg].type)): - type_str = r[1] - elif r := re.search(r"typing\.(.*)\[(.*)\]", str(config.configs[cfg].type)): - type_str = r[1] - if type_str: - type_str = type_str.lower() - type_inner = r[2] - if type_inner: - type_inner = [x.strip() for x in type_inner.split(",")] - config_list.append( - PluginConfig( - module=module, - key=cfg, - value=config.configs[cfg].value, - help=config.configs[cfg].help, - default_value=config.configs[cfg].default_value, - type=type_str, - type_inner=type_inner, # type: ignore - ) - ) - plugin_info = PluginDetail( - module=module, - plugin_name=db_plugin.name, - default_status=db_plugin.default_status, - limit_superuser=db_plugin.limit_superuser, - cost_gold=db_plugin.cost_gold, - menu_type=db_plugin.menu_type, - version=db_plugin.version or "0", - level=db_plugin.level, - status=db_plugin.status, - author=db_plugin.author, - config_list=config_list, - block_type=db_plugin.block_type, - ) - return Result.ok(plugin_info) + try: + return Result.ok( + await ApiDataSource.get_plugin_detail(module), "已经帮你写好啦!" + ) + except (ValueError, KeyError): + return Result.fail("插件数据不存在...") + except Exception as e: + logger.error(f"{router.prefix}/get_plugin 调用错误", "WebUi", e=e) + return Result.fail(f"{type(e)}: {e}") diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/data_source.py b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/data_source.py new file mode 100644 index 00000000..ee0992d6 --- /dev/null +++ b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/data_source.py @@ -0,0 +1,152 @@ +import re + +import cattrs +from fastapi import Query + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import ConfigGroup +from zhenxun.models.plugin_info import PluginInfo as DbPluginInfo +from zhenxun.utils.enum import BlockType, PluginType + +from .model import PluginConfig, PluginDetail, PluginInfo, UpdatePlugin + + +class ApiDataSource: + @classmethod + async def get_plugin_list( + cls, plugin_type: list[PluginType] = Query(None), menu_type: str | None = None + ) -> list[PluginInfo]: + """获取插件列表 + + 参数: + plugin_type: 插件类型. + menu_type: 菜单类型. + + 返回: + list[PluginInfo]: 插件数据列表 + """ + plugin_list: list[PluginInfo] = [] + query = DbPluginInfo + if plugin_type: + query = query.filter(plugin_type__in=plugin_type, load_status=True) + if menu_type: + query = query.filter(menu_type=menu_type, load_status=True) + plugins = await query.all() + for plugin in plugins: + plugin_info = PluginInfo( + module=plugin.module, + plugin_name=plugin.name, + default_status=plugin.default_status, + limit_superuser=plugin.limit_superuser, + cost_gold=plugin.cost_gold, + menu_type=plugin.menu_type, + version=plugin.version or "0", + level=plugin.level, + status=plugin.status, + author=plugin.author, + ) + plugin_list.append(plugin_info) + return plugin_list + + @classmethod + async def update_plugin(cls, param: UpdatePlugin) -> DbPluginInfo: + """更新插件数据 + + 参数: + param: UpdatePlugin + + 返回: + DbPluginInfo | None: 插件数据 + """ + db_plugin = await DbPluginInfo.get_plugin(module=param.module) + if not db_plugin: + raise ValueError("插件不存在") + db_plugin.default_status = param.default_status + db_plugin.limit_superuser = param.limit_superuser + db_plugin.cost_gold = param.cost_gold + db_plugin.level = param.level + db_plugin.menu_type = param.menu_type + db_plugin.block_type = param.block_type + db_plugin.status = param.block_type != BlockType.ALL + await db_plugin.save() + # 配置项 + if param.configs and (configs := Config.get(param.module)): + for key in param.configs: + if c := configs.configs.get(key): + value = param.configs[key] + if c.type and value is not None: + value = cattrs.structure(value, c.type) + Config.set_config(param.module, key, value) + Config.save(save_simple_data=True) + return db_plugin + + @classmethod + def __build_plugin_config( + cls, module: str, cfg: str, config: ConfigGroup + ) -> PluginConfig: + """获取插件配置项 + + 参数: + module: 模块名 + cfg: cfg + config: ConfigGroup + + 返回: + lPluginConfig: 配置数据 + """ + type_str = "" + type_inner = None + if r := re.search(r"", str(config.configs[cfg].type)): + type_str = r[1] + elif r := re.search(r"typing\.(.*)\[(.*)\]", str(config.configs[cfg].type)): + type_str = r[1] + if type_str: + type_str = type_str.lower() + type_inner = r[2] + if type_inner: + type_inner = [x.strip() for x in type_inner.split(",")] + return PluginConfig( + module=module, + key=cfg, + value=config.configs[cfg].value, + help=config.configs[cfg].help, + default_value=config.configs[cfg].default_value, + type=type_str, + type_inner=type_inner, # type: ignore + ) + + @classmethod + async def get_plugin_detail(cls, module: str) -> PluginDetail: + """获取插件详情 + + 参数: + module: 模块名 + + 异常: + ValueError: 插件不存在 + + 返回: + PluginDetail: 插件详情数据 + """ + db_plugin = await DbPluginInfo.get_plugin(module=module) + if not db_plugin: + raise ValueError("插件不存在") + config_list = [] + if config := Config.get(module): + config_list.extend( + cls.__build_plugin_config(module, cfg, config) for cfg in config.configs + ) + return PluginDetail( + module=module, + plugin_name=db_plugin.name, + default_status=db_plugin.default_status, + limit_superuser=db_plugin.limit_superuser, + cost_gold=db_plugin.cost_gold, + menu_type=db_plugin.menu_type, + version=db_plugin.version or "0", + level=db_plugin.level, + status=db_plugin.status, + author=db_plugin.author, + config_list=config_list, + block_type=db_plugin.block_type, + ) diff --git a/zhenxun/builtin_plugins/web_ui/base_model.py b/zhenxun/builtin_plugins/web_ui/base_model.py index c9177fc9..52f07626 100644 --- a/zhenxun/builtin_plugins/web_ui/base_model.py +++ b/zhenxun/builtin_plugins/web_ui/base_model.py @@ -31,7 +31,7 @@ class Result(Generic[RT], BaseModel): """info""" warning: str | None = None """警告信息""" - data: RT = None + data: RT | None = None """返回数据""" @classmethod diff --git a/zhenxun/services/__init__.py b/zhenxun/services/__init__.py index e69de29b..5727da7d 100644 --- a/zhenxun/services/__init__.py +++ b/zhenxun/services/__init__.py @@ -0,0 +1,8 @@ +from nonebot import require + +require("nonebot_plugin_apscheduler") +require("nonebot_plugin_alconna") +require("nonebot_plugin_session") +require("nonebot_plugin_userinfo") +require("nonebot_plugin_htmlrender") +require("nonebot_plugin_uninfo") diff --git a/zhenxun/utils/common_utils.py b/zhenxun/utils/common_utils.py index 76445649..61259bd8 100644 --- a/zhenxun/utils/common_utils.py +++ b/zhenxun/utils/common_utils.py @@ -1,3 +1,5 @@ +from typing import overload + from nonebot.adapters import Bot from nonebot_plugin_uninfo import Session, SupportScope, Uninfo, get_interface @@ -62,6 +64,34 @@ class CommonUtils: return True return False + @staticmethod + def format(name: str) -> str: + return f"<{name}," + + @overload + @classmethod + def convert_module_format(cls, data: str) -> list[str]: ... + + @overload + @classmethod + def convert_module_format(cls, data: list[str]) -> str: ... + + @classmethod + def convert_module_format(cls, data: str | list[str]) -> str | list[str]: + """ + 在 ` UserData | None: """获取用户信息 参数: bot: Bot user_id: 用户id - group_id: 群组/频道id. + group_id: 群组id. + channel_id: 频道id. 返回: UserData | None: 用户数据 """ - if isinstance(bot, v11Bot): - if group_id: - if user := await bot.get_group_member_info( - group_id=int(group_id), user_id=int(user_id) - ): - return UserData( - name=user["nickname"], - card=user["card"], - user_id=user["user_id"], - group_id=user["group_id"], - role=user["role"], - join_time=user["join_time"], - ) - elif friend_list := await bot.get_friend_list(): - for f in friend_list: - if f["user_id"] == int(user_id): - return UserData( - name=f["nickname"], - card=f["remark"], - user_id=f["user_id"], - ) - if isinstance(bot, v12Bot): - if group_id: - if user := await bot.get_group_member_info( - group_id=group_id, user_id=user_id - ): - return UserData( - name=user["user_name"], - card=user["user_displayname"], - user_id=user["user_id"], - group_id=group_id, - ) - elif friend_list := await bot.get_friend_list(): - for f in friend_list: - if f["user_id"] == int(user_id): - return UserData( - name=f["user_name"], - card=f["user_remark"], - user_id=f["user_id"], - ) - if isinstance(bot, DodoBot) and group_id: - if user := await bot.get_member_info( - island_source_id=group_id, dodo_source_id=user_id - ): - return UserData( - name=user.nick_name, - card=user.personal_nick_name, - avatar_url=user.avatar_url, - user_id=user.dodo_source_id, - group_id=user.island_source_id, - join_time=int(user.join_time.timestamp()), + if interface := get_interface(bot): + member = None + user = None + if channel_id: + member = await interface.get_member( + SceneType.CHANNEL_TEXT, channel_id, user_id ) - if isinstance(bot, KaiheilaBot) and group_id: - if user := await bot.user_view(guild_id=group_id, user_id=user_id): - second = int(user.joined_at / 1000) if user.joined_at else None + if member: + user = member.user + elif group_id: + member = await interface.get_member(SceneType.GROUP, group_id, user_id) + if member: + user = member.user + else: + user = await interface.get_user(user_id) + if not user: + return None + if member: return UserData( - name=user.nickname or "", - avatar_url=user.avatar, - user_id=user_id, + name=user.name or "", + card=member.nick, + user_id=user.id, group_id=group_id, - join_time=second, + channel_id=channel_id, + role=member.role.id if member.role else None, + join_time=int(member.joined_at.timestamp()) + if member.joined_at + else None, + ) + else: + return UserData( + name=user.name or "", + user_id=user.id, + group_id=group_id, + channel_id=channel_id, ) return None @@ -337,7 +268,7 @@ class PlatformUtils: 返回: Receipt | None: 是否发送成功 """ - if target := cls.get_target(bot, user_id, group_id): + if target := cls.get_target(user_id=user_id, group_id=group_id): send_message = ( MessageUtils.build_message(message) if isinstance(message, str) @@ -361,7 +292,9 @@ class PlatformUtils: group_list, platform = await cls.get_group_list(bot) if group_list: db_group = await GroupConsole.all() - db_group_id = [(group.group_id, group.channel_id) for group in db_group] + db_group_id: list[tuple[str, str]] = [ + (group.group_id, group.channel_id) for group in db_group + ] for group in group_list: group.platform = platform if (group.group_id, group.channel_id) not in db_group_id: @@ -411,69 +344,43 @@ class PlatformUtils: return "unknown" @classmethod - async def get_group_list(cls, bot: Bot) -> tuple[list[GroupConsole], str]: + async def get_group_list( + cls, bot: Bot, only_group: bool = False + ) -> tuple[list[GroupConsole], str]: """获取群组列表 参数: bot: Bot + only_group: 是否只获取群组(不获取channel) 返回: tuple[list[GroupConsole], str]: 群组列表, 平台 """ - if isinstance(bot, v11Bot): - group_list = await bot.get_group_list() - return [ - GroupConsole( - group_id=str(g["group_id"]), - group_name=g["group_name"], - max_member_count=g["max_member_count"], - member_count=g["member_count"], - ) - for g in group_list - ], "qq" - if isinstance(bot, v12Bot): - group_list = await bot.get_group_list() - return [ - GroupConsole( - group_id=g.group_id, # type: ignore - user_name=g.group_name, # type: ignore - ) - for g in group_list - ], "qq" - if isinstance(bot, DodoBot): - island_list = await bot.get_island_list() - source_id_list = [ - (g.island_source_id, g.island_name) - for g in island_list - if g.island_source_id - ] - group_list = [] - for id, name in source_id_list: - channel_list = await bot.get_channel_list(island_source_id=id) - group_list.append(GroupConsole(group_id=id, group_name=name)) - group_list += [ + if interface := get_interface(bot): + platform = cls.get_platform(bot) + result_list = [] + scenes = await interface.get_scenes(SceneType.GROUP) + for scene in scenes: + group_id = scene.id + result_list.append( GroupConsole( - group_id=id, group_name=c.channel_name, channel_id=c.channel_id + group_id=scene.id, + group_name=scene.name, ) - for c in channel_list - ] - return group_list, "dodo" - if isinstance(bot, KaiheilaBot): - group_list = [] - guilds = await bot.guild_list() - if guilds.guilds: - for guild_id, name in [(g.id_, g.name) for g in guilds.guilds if g.id_]: - view = await bot.guild_view(guild_id=guild_id) - group_list.append(GroupConsole(group_id=guild_id, group_name=name)) - if view.channels: - group_list += [ - GroupConsole( - group_id=guild_id, group_name=c.name, channel_id=c.id_ + ) + if not only_group and platform != "qq": + if channel_list := await interface.get_scenes( + parent_scene_id=group_id + ): + for channel in channel_list: + result_list.append( + GroupConsole( + group_id=scene.id, + group_name=channel.name, + channel_id=channel.id, + ) ) - for c in view.channels - if c.type != 0 - ] - return group_list, "kaiheila" + return result_list, platform return [], "" @classmethod @@ -508,36 +415,17 @@ class PlatformUtils: 返回: list[FriendUser]: 好友列表 """ - if isinstance(bot, v11Bot): - friend_list = await bot.get_friend_list() + if interface := get_interface(bot): + user_list = await interface.get_users() return [ - FriendUser(user_id=str(f["user_id"]), user_name=f["nickname"]) - for f in friend_list - ], "qq" - if isinstance(bot, v12Bot): - friend_list = await bot.get_friend_list() - return [ - FriendUser( - user_id=f.user_id, # type: ignore - user_name=f.user_displayname or f.user_remark or f.user_name, # type: ignore - ) - for f in friend_list - ], "qq" - # if isinstance(bot, DodoBot): - # # TODO: dodo好友列表 - # pass - # if isinstance(bot, KaiheilaBot): - # # TODO: kaiheila好友列表 - # pass - # if isinstance(bot, DiscordBot): - # # TODO: discord好友列表 - # pass + FriendUser(user_id=u.id, user_name=u.name) for u in user_list + ], cls.get_platform(bot) return [], "" @classmethod def get_target( cls, - bot: Bot, + *, user_id: str | None = None, group_id: str | None = None, channel_id: str | None = None, @@ -554,16 +442,12 @@ class PlatformUtils: target: 对应平台Target """ target = None - if isinstance(bot, v11Bot | v12Bot): - if group_id: - target = Target(group_id) - elif user_id: - target = Target(user_id, private=True) - elif isinstance(bot, DodoBot | KaiheilaBot): - if group_id and channel_id: - target = Target(channel_id, parent_id=group_id, channel=True) - elif user_id: - target = Target(user_id, private=True) + if group_id and channel_id: + target = Target(channel_id, parent_id=group_id, channel=True) + elif group_id: + target = Target(group_id) + elif user_id: + target = Target(user_id, private=True) return target @@ -646,7 +530,9 @@ async def broadcast_group( ) continue target = PlatformUtils.get_target( - _bot, None, group.group_id, group.channel_id + user_id=None, + group_id=group.group_id, + channel_id=group.channel_id, ) if target: _used_group.append(key)