1
0
forked from bot/app

395 Commits

Author SHA1 Message Date
43dfc9a940 Add sign status for Lagrange.Core 2024-05-11 01:16:48 +08:00
7fbfafe2db Add sign status for Lagrange.Core 2024-05-11 01:00:13 +08:00
f22f8f772a Add sign status for Lagrange.Core 2024-05-11 00:34:11 +08:00
205b69e5cb Add sign status for Lagrange.Core 2024-05-11 00:16:38 +08:00
75a4d1fdcb Add sign status for Lagrange.Core 2024-05-10 20:43:16 +08:00
b3aa5c9e02 启用联合黑名单 2024-05-10 00:12:48 +08:00
38b496d800 Merge remote-tracking branch 'origin/main' 2024-05-09 21:38:45 +08:00
d2bb672f65 fix: main.py 2024-05-09 20:49:39 +08:00
6c267c6072 📝 更新调试模式配置 2024-05-09 16:37:50 +08:00
598260895c 📝 更新调试模式配置 2024-05-09 16:36:45 +08:00
e02dfdf5d6 更新NoneBot依赖版本 2024-05-09 16:25:47 +08:00
154b342057 New plugin Minecraft工具箱 2024-05-09 13:06:04 +08:00
b8375013a3 联合黑名单支持 2024-05-09 00:14:14 +08:00
5913528d32 Merge remote-tracking branch 'origin/main' 2024-05-09 00:09:44 +08:00
773137591f 联合黑名单支持 2024-05-09 00:09:18 +08:00
8fa0470187 联合黑名单支持 2024-05-08 23:40:16 +08:00
bf768b6cb5 update status.css 2024-05-07 08:48:37 +08:00
9b8c38cac0 docker支持 2024-05-01 02:39:28 +08:00
1f96372196 docker支持 2024-05-01 02:17:21 +08:00
148d671b5d 使用内存换速度 2024-04-30 15:23:12 +08:00
61d91ea0a9 🐛 状态卡片部分css样式异常 2024-04-29 17:05:55 +08:00
f9317802f4 Merge remote-tracking branch 'origin/main' 2024-04-29 16:59:03 +08:00
8809459f1b 🐛 轻雪天气获取空aqi值的异常错误 2024-04-29 16:58:52 +08:00
8f906f2d12 fix: 天气显示错误 2024-04-29 07:35:17 +08:00
269dd5ced2 fix: 轻雪天气星期几显示错误 2024-04-29 07:29:08 +08:00
c063c69dea 🐛 修复群聊启用在带参数不正确的问题 2024-04-28 18:52:22 +08:00
d43fe327c2 🔥 remove some of requirements.txt 2024-04-28 12:49:12 +08:00
d80c4a7c90 feat: pydantic v2 支持
fix: 自动更新后更新依赖
2024-04-27 21:38:37 +08:00
4d274df6b2 fix: weather timezone 2024-04-27 13:18:59 +08:00
431ebb59c2 轻雪天气更新 2024-04-27 11:45:31 +08:00
3713bf397c 轻雪天气更新 2024-04-27 11:42:58 +08:00
86c7b70e63 轻雪天气更新 2024-04-27 02:20:44 +08:00
9cfdd375ca 更新语言包至2024.4.26版本 2024-04-26 20:20:56 +08:00
c4e00e3402 更新语言包至2024.4.26版本 2024-04-26 20:20:53 +08:00
93e1a0ff77 全新资源包结构 2024-04-26 15:11:31 +08:00
f69844717f 新版状态页面 2024-04-26 15:06:21 +08:00
5100ca6c77 新版状态页面 2024-04-26 15:02:46 +08:00
857e58d635 : zap: 添加api请求超时限制 2024-04-26 12:39:43 +08:00
bb9b8a1ced : bug: 放宽依赖限制 2024-04-26 00:50:28 +08:00
b240d75552 : bug: 哈哈哈哈哈哈哈,markdown又寄了 2024-04-24 20:09:23 +08:00
0a1d96c434 : bug: 哈哈哈哈哈哈哈,markdown又活了 2024-04-24 16:33:58 +08:00
fe2ca8b05b 🐛 哈哈哈哈哈哈哈,markdown寄了,发图片 2024-04-24 15:20:32 +08:00
bfb5cf2cf0 🐛 api命令字符串错误输出 2024-04-24 15:12:26 +08:00
a6408a3397 📝 添加字体原有版权声明 2024-04-24 15:07:57 +08:00
5db29c7e2c 📝 添加字体原有版权声明 2024-04-24 00:44:45 +08:00
a38e2b887c 📝 添加字体原有版权声明 2024-04-24 00:23:05 +08:00
4a8ddaba2d 更新依赖 2024-04-23 23:10:58 +08:00
e1879bbebd 更新依赖 2024-04-23 22:59:30 +08:00
6b51b5fe9d Merge remote-tracking branch 'origin/main' 2024-04-23 21:49:54 +08:00
acfc70ea50 🐛 在读取数据库前进行迁移 fix #35 2024-04-23 21:49:16 +08:00
fb495d34d5 fix: typo 2024-04-23 07:45:38 +08:00
af038cb789 feat: 修改菜单样式 2024-04-23 07:42:56 +08:00
53bc6df30f 独立status插件... 2024-04-22 23:55:33 +08:00
ece71ca1e7 预渲染数据,发送更快 2024-04-22 21:08:08 +08:00
a0079da01b Merge remote-tracking branch 'origin/main' 2024-04-22 21:06:42 +08:00
bd5f6c5205 预渲染数据,发送更快 2024-04-22 21:05:35 +08:00
9687ddb842 docs: 资源包主页文档指引 2024-04-22 14:05:10 +08:00
ae19113141 Merge remote-tracking branch 'origin/main' 2024-04-21 14:15:34 +08:00
8b55156da9 新增api命令 2024-04-21 14:15:12 +08:00
34ba5ffde3 Merge pull request #34 from MoeSnowyFox/main
💩修复npm帮助部分文本错误
2024-04-21 01:10:08 +08:00
e948b9e94e 💩补充文本 2024-04-20 23:09:22 +08:00
e3ec25790f 💩 修复npm帮助界面输出错误 2024-04-20 23:08:07 +08:00
6ea3b2c1e2 Merge pull request #2 from snowykami/main
1
2024-04-20 23:01:58 +08:00
1a930dc604 轻雪API文档 2024-04-20 17:02:57 +08:00
e0982f3a24 新增安全模式启动 2024-04-20 12:47:42 +08:00
18d9ac3249 新增安全模式启动 2024-04-20 12:46:49 +08:00
7585a5473d 🐛 npm显示异常 2024-04-20 04:42:11 +08:00
4dd3b4aedc 📝 插件商店支持 2024-04-20 04:10:20 +08:00
5a9e8449cc 📝 add help command 2024-04-19 00:45:57 +08:00
62a2755ecf 🐛 fix typo update -> upsert 2024-04-19 00:31:49 +08:00
6e66c95487 Merge pull request #1 from snowykami/main
1
2024-04-18 23:35:25 +08:00
5c1170f6fb Merge pull request #33 from MoeSnowyFox/main
一些修改
2024-04-18 23:27:49 +08:00
dc83d6b469 Update npm.py 2024-04-18 22:41:02 +08:00
46715e17aa Update runtime.py 2024-04-18 22:40:18 +08:00
2e4013e948 Update runtime.py 2024-04-18 22:38:53 +08:00
b284e52203 Update runtime.py 2024-04-18 22:37:57 +08:00
391a183402 Update runtime.py 2024-04-18 22:36:16 +08:00
6722eeffa9 🐛 markdown暂时不可用 2024-04-18 18:15:11 +08:00
2cfd0de8e3 Update runtime.py 2024-04-18 00:24:02 +08:00
778bcf7623 Update npm.py 2024-04-18 00:23:29 +08:00
0fcde73178 Update rpm.py 2024-04-18 00:23:14 +08:00
8a0f25b5b0 Update zh-CN.lang 2024-04-18 00:22:36 +08:00
0e47e3c163 插件管理添加翻页按钮,支持从内存快速获取当前session状态 2024-04-17 20:45:44 +08:00
0e02af59ca 优先从内存读取用户信息 2024-04-17 17:45:32 +08:00
c4db4dc6a6 fix: 天气查询失败的问题 2024-04-15 20:46:09 +08:00
016fe3ef72 fix: 天气查询失败的问题 2024-04-15 20:43:39 +08:00
0d3361dc99 fix: 天气查询失败的问题 2024-04-15 20:21:50 +08:00
79d8063b5d feat: 轻雪天气实时天气功能已更新 2024-04-15 18:04:19 +08:00
7d0b9662f4 fix: update error 2024-04-15 06:38:06 +08:00
afbcad3a1c Merge remote-tracking branch 'origin/main' 2024-04-14 21:39:48 +08:00
15a329029d feat: 更清晰的目录结构,新的markdown构建 2024-04-14 21:39:27 +08:00
190e7ebdea fix: npm search 缺失 show_num 2024-04-13 13:45:57 +08:00
65dcf36fe7 Merge remote-tracking branch 'origin/main' 2024-04-12 13:07:30 +08:00
e2779bdfd7 feat: 对nb-cli的支持 2024-04-12 13:07:19 +08:00
87061fb5cb fix: Update core.py 2024-04-12 07:17:42 +08:00
50b851a2c4 fix: Update core.py 2024-04-12 07:16:16 +08:00
6a4c88a6ba feat: 自动更新功能 2024-04-12 01:15:05 +08:00
185b1d8a21 feat: 自动更新功能 2024-04-12 01:07:53 +08:00
7046c0d10e Merge remote-tracking branch 'origin/main' 2024-04-12 00:40:53 +08:00
83cd164a45 fix: 状态卡片百分比错误
feat: 群聊Bot开关,防止Bot乱窜
2024-04-12 00:40:41 +08:00
33dd2f104d fix: 状态卡片百分比错误
feat: 群聊Bot开关,防止Bot乱窜
2024-04-12 00:32:08 +08:00
58278fa735 feat: 同步天气api 2024-04-11 14:41:46 +08:00
f9e5742821 feat: 状态卡片模糊半径更改 2024-04-11 13:17:45 +08:00
d37442bc9d feat: 状态卡片模糊半径更改 2024-04-11 13:15:29 +08:00
79451ac24f docs: Update README.md 2024-04-11 08:38:29 +08:00
38f658edf9 feat: 重启时间显示 2024-04-10 23:47:10 +08:00
db0542279b docs: Update 2024-04-10 23:06:55 +08:00
23353a3673 feat: 提供插件更新功能 2024-04-10 12:18:45 +08:00
e0dc840197 feat: 提供插件更新功能 2024-04-10 12:16:40 +08:00
f2fda7f92e fix: stats -> status 2024-04-10 10:01:41 +08:00
cd0812af42 docs: Update basic_command.md 2024-04-10 09:39:38 +08:00
6f207a54aa docs: Update basic_command.md 2024-04-10 07:38:13 +08:00
aaebccf7ab fix: arguments index 2024-04-09 07:59:45 +08:00
655fb0999a docs: Update basic_command.md 2024-04-09 07:14:24 +08:00
c400eae7c8 fix: 插件安装提示找不到 2024-04-08 17:42:04 +08:00
3fba4c78dc feat: 字体 2024-04-08 17:31:03 +08:00
9346144f0e feat: 字体 2024-04-08 17:21:01 +08:00
edc86990a7 feat: 字体 2024-04-08 17:13:25 +08:00
83692ffd55 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	docs/.vuepress/theme.js
2024-04-08 17:02:24 +08:00
8e1ec22679 feat: 野兽先辈主题包
fix: 插件模块名问题
feat: `nps` -> `npm`
2024-04-08 17:01:55 +08:00
29867dd187 Merge pull request #32 from Liteyuki/main
fix: 移除注释
2024-04-08 11:20:30 +08:00
0f9b8fcca8 fix: 移除注释 2024-04-08 11:19:49 +08:00
195c98ddd2 fix: liteyuki_api.py -> ly_api.py 2024-04-08 10:04:31 +08:00
6e521497db docs: update logo 2024-04-07 07:42:16 +08:00
64ce2a2971 docs: Update fandq.md 2024-04-07 07:24:07 +08:00
2781c8bdfb docs: Update resource_pack.md 2024-04-07 04:16:53 +08:00
c45061a95a feat: 优化插件商店排版 2024-04-07 03:28:31 +08:00
16c1ba440c feat: 优化插件商店排版 2024-04-07 03:24:37 +08:00
e271059720 feat: 优化插件商店排版 2024-04-07 03:24:13 +08:00
9743868cce fix: npm对插件无法启用的bug
feat: 资源包的管理器
2024-04-07 00:35:53 +08:00
72742d805c docs: 资源包相关内容 2024-04-06 09:10:01 +08:00
08564b3ac6 feat: 新的资源包加载逻辑
feat: 主题商店支持
2024-04-06 08:50:25 +08:00
a76bc3de92 feat: 新的资源包加载逻辑
feat: 主题商店支持
2024-04-06 08:48:21 +08:00
be0b5e6de1 feat: 新的背景图
fix: 命令冲突
docs: 添加了命令头和nickname冲突的解释
2024-04-06 04:27:37 +08:00
bc856b4aa9 feat: 新的背景图
fix: 命令冲突
docs: 添加了命令头和nickname冲突的解释
2024-04-06 04:23:01 +08:00
468a534d85 fix: typo of config.yaml -> config.yml 2024-04-05 20:19:39 +08:00
9bfe173f92 fix: typo of config.yaml -> config.yml 2024-04-05 20:07:33 +08:00
26a15229cf fix: 内存计算误差 2024-04-05 10:26:46 +08:00
a9426ca48f feat: 系统版本显示 2024-04-05 09:44:32 +08:00
844f04d555 feat: 系统版本显示 2024-04-05 09:32:55 +08:00
731f07e062 fix: CPU频率显示位数超出范围
fix: 空驱动器显示在面板位置
2024-04-05 09:27:00 +08:00
1b3d82ebe2 feat: 新版本 2024-04-05 09:20:49 +08:00
b275a646ac feat: 新版本 2024-04-05 09:18:35 +08:00
90e059af32 feat: 新版本 2024-04-05 09:18:22 +08:00
321f19953d feat: 新版本 2024-04-05 09:15:57 +08:00
110b0cfc21 feat: 新版本 2024-04-05 09:14:26 +08:00
d9a32328b2 feat: 新版本 2024-04-05 08:55:24 +08:00
71faffaa44 feat: 更多的字体 2024-04-05 08:48:51 +08:00
edc0a16cad feat: 更多的字体 2024-04-05 07:14:35 +08:00
c5d2c040fe feat: 更多的字体 2024-04-05 07:02:18 +08:00
813f1c2ded Merge pull request #30 from MoeSnowyFox/main
fix: requirements.txt
2024-04-04 17:06:23 +08:00
b965d4d005 Update requirements.txt
谁家好心人写pip==24.0啊
2024-04-04 16:46:58 +08:00
d0c5385534 feat: 国产某聊天软件的图床支持 2024-04-03 14:59:40 +08:00
eaae8ceaad feat: 国产某聊天软件的图床支持 2024-04-03 14:59:00 +08:00
ad543dd738 feat: 国产某聊天软件的图床支持 2024-04-03 14:57:11 +08:00
7df870e65d feat: 国产某聊天软件的图床支持 2024-04-03 14:24:43 +08:00
263b28b2f9 feat: 国产某聊天软件的图床支持 2024-04-03 14:18:41 +08:00
4c60f09d94 Merge remote-tracking branch 'origin/main' 2024-04-03 14:10:23 +08:00
86e6397fa7 feat: 国产某聊天软件的图床支持 2024-04-03 14:10:10 +08:00
6c41a36d8e fix: nonebot未定义 2024-04-03 01:11:38 +08:00
955d9f6d62 feat: 优化了排版和渲染 2024-04-03 01:03:25 +08:00
14fb96fec2 feat:
- markdown发送失败后可以转为图片发送
- 轻雪图床支持
fix:
- 数据库删除时不提交
2024-04-02 20:32:28 +08:00
e7765a4513 fix: stats.html 2024-04-02 08:51:34 +08:00
7e302922c5 fix: 你写的垂直居中在我机子上没对齐 2024-04-02 08:01:19 +08:00
209d636919 feat: 愚人节快乐 2024-04-02 00:01:28 +08:00
55ea08cf11 feat: 愚人节快乐 2024-04-01 23:56:03 +08:00
e58e853445 feat: 愚人节快乐 2024-04-01 13:33:50 +08:00
e97bd0a50b feat: 愚人节快乐 2024-04-01 13:33:21 +08:00
31f266bf21 feat: 可以设置发送高清大图 2024-04-01 13:06:08 +08:00
b611ec1714 feat: 可以设置发送高清大图 2024-04-01 12:33:35 +08:00
4e549af1c9 feat: 可以设置发送高清大图 2024-04-01 12:30:13 +08:00
54cc57a2b2 feat: 可以设置发送高清大图 2024-04-01 12:29:04 +08:00
e43cb0ab07 feat: 愚人节快乐 2024-04-01 11:46:47 +08:00
c1ba64e7c3 feat: 愚人节快乐 2024-04-01 11:39:38 +08:00
19308ffc53 feat: 支持天气 2024-04-01 11:37:29 +08:00
6a03003d41 docs: Update basic_command.md 2024-04-01 07:35:34 +08:00
0e7e731080 Rename ISSUE_TEMPLATE_1.md to ISSUE_TEMPLATE.md 2024-04-01 03:15:43 +08:00
1993b46750 create: ISSUE_TEMPLATE_1.md 2024-04-01 03:14:11 +08:00
7ee18c4334 docs: Update config.md 2024-04-01 03:01:27 +08:00
00166e0ff3 Delete CNAME 2024-04-01 02:56:45 +08:00
b43a5827c9 docs: 修正连接路由错误 2024-04-01 01:04:16 +08:00
1619504059 docs: 修改描述 2024-03-31 19:23:15 +08:00
80a61a6eed fix: 讨论功能 2024-03-31 19:08:35 +08:00
7c551aecb2 fix: alconna响应器命令头别名配置错误的问题 2024-03-31 18:54:58 +08:00
3065122059 fix: alconna响应器命令头别名配置错误的问题 2024-03-31 18:54:04 +08:00
d3f3ee6dfa docs: 美化文档 2024-03-31 11:20:43 +08:00
d3fce1f145 docs: 美化文档 2024-03-31 11:15:32 +08:00
83468af6c7 docs: 美化文档 2024-03-31 11:14:04 +08:00
9365aec559 docs: 美化文档 2024-03-31 11:12:32 +08:00
9315af3dfd docs: 美化文档 2024-03-31 11:09:48 +08:00
41e389d690 docs: 美化文档 2024-03-31 10:59:17 +08:00
c80919ff1e docs: 添加了更多实现端建议 2024-03-31 09:22:42 +08:00
f446308e2a feat: 添加了对自定义命令前缀的支持 2024-03-31 09:06:02 +08:00
724e13180c feat: 添加了对自定义命令前缀的支持 2024-03-31 09:03:28 +08:00
2ad2bb4182 feat: 添加了对指令前缀的支持 2024-03-31 08:20:20 +08:00
76359ba83e fix: 取消了本地引用js 2024-03-31 07:25:29 +08:00
d8efa08d2f feat: 新增内容的多语言支持 2024-03-31 07:17:33 +08:00
b87e150e34 feat: 新增内容的多语言支持 2024-03-31 07:13:00 +08:00
9f3a451b6d feat: 新增内容的多语言支持 2024-03-31 07:11:59 +08:00
41ee427040 feat: 状态卡片 2024-03-31 06:32:04 +08:00
93569fcd99 feat: 状态卡片 2024-03-31 06:27:56 +08:00
807e552f8a feat: 状态卡片 2024-03-31 06:26:01 +08:00
84e2223dbb feat: 状态卡片 2024-03-31 06:23:23 +08:00
f9e61fd184 feat: 配置项目的热修改 2024-03-31 06:22:53 +08:00
c8851bd696 Merge remote-tracking branch 'origin/main' 2024-03-30 19:24:45 +08:00
9b2b0a7c7d feat: 配置项目的热修改 2024-03-30 19:24:09 +08:00
c15c604752 feat: 配置项目的热修改 2024-03-30 06:04:17 +08:00
65866488c6 feat: 添加了对菜单的初步支持 2024-03-29 17:44:48 +08:00
392376248d feat: 配置项目的热修改 2024-03-29 17:00:36 +08:00
d20699ee0f feat: 配置项目的热修改 2024-03-29 16:02:22 +08:00
a5f9247b32 Merge remote-tracking branch 'origin/main' 2024-03-29 15:12:30 +08:00
dd30b64004 feat: 配置项目的热修改 2024-03-29 15:12:14 +08:00
aed63c34c9 feat: 配置项目的热修改 2024-03-29 14:58:24 +08:00
9cf05fd8fd fix: 扫雷布局错误 2024-03-29 07:07:11 +08:00
6a49a70481 fix: 扫雷布局 2024-03-29 07:05:53 +08:00
205a28eb56 fix: index out of list 2024-03-29 07:03:09 +08:00
f23567194c fix: type has no TABLE_NAME attr 2024-03-29 00:05:46 +08:00
bb17d2949a feat: 扫雷游戏,测试 2024-03-28 23:53:38 +08:00
9e0b065566 docs: vuepress构建文档 2024-03-28 18:30:56 +08:00
6dffb0f581 docs: vuepress构建文档 2024-03-28 13:37:17 +08:00
2e37bd546b docs: vuepress构建文档 2024-03-28 13:27:40 +08:00
37b1346361 docs: vuepress构建文档 2024-03-28 12:05:03 +08:00
7cecfd1053 docs: vuepress构建文档 2024-03-28 11:12:55 +08:00
de75849cc3 docs: vuepress构建文档 2024-03-28 11:11:14 +08:00
edafffcbb5 docs: vuepress构建文档 2024-03-28 11:10:01 +08:00
ea35147938 docs: vuepress构建文档 2024-03-28 11:09:01 +08:00
86add474f4 docs: vuepress构建文档 2024-03-28 11:08:32 +08:00
6c02bfc783 docs: vuepress构建文档 2024-03-28 10:59:52 +08:00
dcae427c60 docs: vuepress构建文档 2024-03-28 10:51:22 +08:00
47f00c48c5 docs: vuepress构建文档 2024-03-28 10:50:19 +08:00
7db0617a5b docs: vuepress构建文档 2024-03-28 10:50:13 +08:00
19d79d356d docs: vuepress构建文档 2024-03-28 10:42:44 +08:00
57a302a71e docs: vuepress构建文档 2024-03-28 10:39:53 +08:00
bff728b7f1 docs: vuepress构建文档 2024-03-28 10:35:26 +08:00
1e0ebe0d30 docs: vuepress构建文档 2024-03-28 10:34:25 +08:00
42248dbc71 docs: vuepress构建文档 2024-03-28 10:33:13 +08:00
d0318c47d3 docs: vuepress构建文档 2024-03-28 10:31:52 +08:00
388946e56b docs: vuepress构建文档 2024-03-28 10:30:48 +08:00
b24489ae80 docs: vuepress构建文档 2024-03-28 10:28:34 +08:00
acb7a752de docs: vuepress构建文档 2024-03-28 10:26:19 +08:00
16b7347ca3 docs: vuepress构建文档 2024-03-28 10:24:12 +08:00
8763dfbe85 docs: vuepress构建文档 2024-03-28 10:22:45 +08:00
3a1e1b6b92 docs: vuepress构建文档 2024-03-28 10:20:50 +08:00
0b0e63f1d5 docs: vuepress构建文档 2024-03-28 10:19:39 +08:00
602636520e docs: vuepress构建文档 2024-03-28 10:18:53 +08:00
1f3231c0b4 docs: vuepress构建文档 2024-03-28 10:18:20 +08:00
f9ee8c0aed docs: vuepress构建文档 2024-03-28 10:17:04 +08:00
41b2b13442 docs: vuepress构建文档 2024-03-28 10:13:58 +08:00
58b756eb67 docs: vuepress构建文档 2024-03-28 10:11:28 +08:00
e18b9b5317 docs: vuepress构建文档 2024-03-28 10:07:09 +08:00
d602467b80 docs: vuepress构建文档 2024-03-28 10:05:40 +08:00
58ebf6efea docs: vuepress构建文档 2024-03-28 10:03:12 +08:00
7633ab444c docs: vuepress构建文档 2024-03-28 09:59:18 +08:00
e1a3e9a16c docs: vuepress构建文档 2024-03-28 09:57:44 +08:00
4ff78d0a6f docs: vuepress构建文档 2024-03-28 09:56:10 +08:00
7f7a66d639 docs: vuepress构建文档 2024-03-28 09:53:50 +08:00
1d268e6f97 docs: vuepress构建文档 2024-03-28 09:49:41 +08:00
97d936f9be docs: vuepress构建文档 2024-03-28 09:44:13 +08:00
f3e45c895d docs: vuepress构建文档 2024-03-28 09:38:46 +08:00
fafdbea96c docs: vuepress构建文档 2024-03-28 08:51:57 +08:00
70cabc2383 docs: vuepress构建文档 2024-03-28 08:39:36 +08:00
d44b5ea143 docs: vuepress构建文档 2024-03-28 08:33:38 +08:00
0ac374f5df docs: vuepress构建文档 2024-03-28 08:31:49 +08:00
a8f6a25369 docs: vuepress构建文档 2024-03-28 08:31:18 +08:00
1bbd1dd234 docs: vuepress构建文档 2024-03-28 08:26:44 +08:00
0880623930 perf: 优化插件搜索逻辑,避免重复关键词 2024-03-28 07:46:50 +08:00
8a8e9c62ab update: README.md 2024-03-28 07:21:11 +08:00
c5d850ac13 fix: typo of README.md 2024-03-28 07:04:20 +08:00
8eb693aee9 Create CNAME 2024-03-28 07:01:22 +08:00
570a46a840 docs: 使用vuepress重新构建文档 2024-03-27 23:51:35 +08:00
6e23542296 Merge remote-tracking branch 'origin/main' 2024-03-27 23:51:15 +08:00
f53aa6aa23 Delete docs directory 2024-03-27 23:47:15 +08:00
7baaaebef8 docs: 使用vuepress重新构建文档 2024-03-27 23:42:50 +08:00
630b6dc0ce docs: 使用vuepress重新构建文档 2024-03-27 23:37:14 +08:00
aaf4a752f7 docs: 使用vuepress重新构建文档 2024-03-27 23:36:18 +08:00
adcbc79c1a docs: 使用vuepress重新构建文档 2024-03-27 23:36:13 +08:00
b95c2b2e7e Create CNAME 2024-03-27 23:33:21 +08:00
58ab62c03b docs: 使用vuepress重新构建文档 2024-03-27 23:32:44 +08:00
7bd1b36ec9 docs: 使用vuepress重新构建文档 2024-03-27 23:28:57 +08:00
c7a2ebb4ea docs: 使用vuepress重新构建文档 2024-03-27 23:09:36 +08:00
75795a5b13 docs: 使用vuepress重新构建文档 2024-03-27 23:05:44 +08:00
505dfe3254 update: README.md 2024-03-27 13:34:00 +08:00
9594c6163f fix: lang 香港 -> 中國香港 2024-03-27 08:38:31 +08:00
f6ee13c263 feat: 忽略键盘中断类错误 2024-03-27 08:16:53 +08:00
f15bd985af feat: 忽略键盘中断类错误 2024-03-27 08:03:19 +08:00
e78a3cdab6 feat: 更新并重启 2024-03-27 08:02:05 +08:00
cf2ba6fe3b feat: 更新并重启 2024-03-27 07:59:18 +08:00
6edad57470 feat: 更新并重启 2024-03-27 07:58:14 +08:00
90216c1a60 feat: 更新并重启 2024-03-27 07:57:04 +08:00
1d352d1fce Merge remote-tracking branch 'origin/main' 2024-03-27 07:49:35 +08:00
0552cdfd05 fix: 个人信息显示隐藏属性的bug 2024-03-27 07:49:18 +08:00
980c8e6ee4 update: log.py 2024-03-27 07:13:42 +08:00
bf51f5a83b update: log.py 2024-03-27 07:10:52 +08:00
e22fadcf44 docs: 添加了命令手册 2024-03-27 00:15:12 +08:00
0bdd2d9b1d Update ys.lang 2024-03-27 00:12:42 +08:00
bd29df4f67 Update ys.lang 2024-03-27 00:10:48 +08:00
4b6226dfd7 feat: ys.lang 2024-03-27 00:09:47 +08:00
fca93a7c66 docs: 添加了命令手册 2024-03-27 00:05:51 +08:00
76ce54f68b docs: 优化排版 2024-03-26 23:37:12 +08:00
e5fd1ce9ae docs: 优化排版 2024-03-26 23:36:29 +08:00
3932366955 docs: 优化排版 2024-03-26 23:34:29 +08:00
423ba84908 fix: 数据库迁移报错"''" 2024-03-26 22:41:34 +08:00
5d7c201018 feat: 插件启用停用检测 2024-03-26 22:36:16 +08:00
ac234544a3 feat: 插件启用停用检测 2024-03-26 22:33:17 +08:00
58e603e1ad fix: 数据库支持 2024-03-26 21:33:40 +08:00
90c9ef31a1 Merge remote-tracking branch 'origin/main' 2024-03-26 17:15:20 +08:00
ecbe06a9e8 feat: 统一双引号 2024-03-26 17:14:41 +08:00
0e17762427 Update README.md 2024-03-26 14:32:13 +08:00
04fc9c3dd7 Merge remote-tracking branch 'origin/main' 2024-03-26 12:20:18 +08:00
eff60d8294 feat: 时区设定支持 2024-03-26 12:20:08 +08:00
ee55a9b9e9 Update README.md 2024-03-26 11:03:49 +08:00
365b2fcba2 Update README.md 2024-03-26 11:00:57 +08:00
475506273d Update README.md 2024-03-26 11:00:40 +08:00
e9d2a1fe86 Update README.md 2024-03-26 11:00:18 +08:00
c2e43fff7a feat: 新的orm框架 2024-03-26 10:01:26 +08:00
45c3bf3a5e Update README.md 2024-03-26 09:14:49 +08:00
d6c7c292d4 Update profile_manager.py 2024-03-26 08:39:30 +08:00
2eb60b3e1b Update profile_manager.py 2024-03-26 08:35:41 +08:00
d4a8aa1f87 Update profile_manager.py 2024-03-26 08:29:14 +08:00
615c7e6681 Create es.lang 2024-03-26 08:27:30 +08:00
23b13595b0 Create de.lang 2024-03-26 08:22:49 +08:00
e85292fdec Create ru.lang 2024-03-26 08:16:31 +08:00
573716e24e Create fr.lang 2024-03-26 08:15:17 +08:00
ede65d91a1 Create ko.lang 2024-03-26 08:12:28 +08:00
987b00d2ad feat: 当程序异常退出时给轻雪提交错误报告 2024-03-25 12:55:16 +08:00
172f45208f fix: require()插件错误 2024-03-25 00:33:23 +08:00
91b14d568c fix: require()插件错误 2024-03-24 23:09:59 +08:00
581aa7d6cc fix: require()插件错误 2024-03-24 23:07:08 +08:00
cc70a8ab2c fix: require()插件错误 2024-03-24 22:04:51 +08:00
4bfed64586 docs: 添加了部分内容 2024-03-24 21:45:43 +08:00
788bca7113 docs: 添加了部分内容 2024-03-24 21:21:57 +08:00
bb27eea0c2 docs: 添加了部分内容 2024-03-24 20:47:05 +08:00
d239a8a63d docs: 添加了部分内容 2024-03-24 20:41:40 +08:00
d87bd6c3b2 feat: 字体 2024-03-24 20:40:09 +08:00
d66fd31a4a feat: 字体 2024-03-24 20:24:00 +08:00
9d950a89ba feat: 自动向轻雪服务器上报错误信息 2024-03-24 20:17:03 +08:00
98634c1f4c feat: 自动向轻雪服务器上报错误信息 2024-03-24 19:58:42 +08:00
beb6f63199 feat: 自动向轻雪服务器上报错误信息 2024-03-24 19:57:27 +08:00
45f9afb73c feat: 自动向轻雪服务器上报错误信息 2024-03-24 19:15:49 +08:00
ccef8ca125 feat: 自动向轻雪服务器上报错误信息 2024-03-24 14:43:41 +08:00
5a929d3e99 fix: pydantic数据类型错误 2024-03-24 13:36:28 +08:00
c6f65d544f feat: log样式在不同等级下的支持 2024-03-24 10:26:40 +08:00
66e18a9c8d fix: 数据库迁移错误 2024-03-24 09:45:13 +08:00
81089523a1 Merge remote-tracking branch 'origin/main' 2024-03-24 09:44:37 +08:00
0e996c07df fix: 数据库迁移错误 2024-03-24 09:44:23 +08:00
fab5be70b3 fix: 插件列表显示错误问题 2024-03-24 09:43:34 +08:00
71bd9ab000 Create LICENSE 2024-03-24 03:11:16 +08:00
de0c073c26 fix: 插件列表显示错误问题 2024-03-23 19:55:12 +08:00
ca0bfe0181 feat: 添加了对菜单的初步支持 2024-03-22 14:13:02 +08:00
4f20089e57 Create zh-Kawaii.lang 2024-03-22 13:55:29 +08:00
500c68c6d5 Rename zh-WY to zh-WY.lang 2024-03-22 13:53:15 +08:00
a0e8bd36c9 Rename zh-HK to zh-HK.lang 2024-03-22 13:53:05 +08:00
96d7763c7d Create zh-HK 2024-03-22 13:52:49 +08:00
ca2c5b0911 feat: 添加了文言文支持 2024-03-22 13:51:22 +08:00
bcc5cb77ad feat: 添加了语言设置界面 2024-03-22 13:39:01 +08:00
1aacceecf0 feat: 添加了对菜单的初步支持 2024-03-22 12:44:56 +08:00
0bd415961c snowy 进行的更改 2024-03-22 12:41:38 +08:00
367b8a5e5d fix: 事件推送目标的逻辑错误 2024-03-22 10:24:38 +08:00
0b4217b592 fix: 事件推送目标的逻辑错误 2024-03-22 08:09:20 +08:00
5737b75d26 fix: 事件推送目标的逻辑错误 2024-03-22 08:08:48 +08:00
73b593ff98 feat: 添加插件启用和停用功能 2024-03-22 07:44:41 +08:00
2711d8844b fix: 插件列表显示错误问题 2024-03-21 14:52:02 +08:00
933979ceaa fix: 链接主页不显示 2024-03-21 13:02:08 +08:00
59506fcc76 fix: 修复了plugins.json报错的问题 2024-03-21 12:55:15 +08:00
a3d60fb435 feat: 添加了自动安装插件功能 2024-03-21 12:18:15 +08:00
e24c5c912e feat: 添加了自动安装插件功能 2024-03-21 12:10:24 +08:00
ca997f727a Create CNAME 2024-03-21 02:26:38 +08:00
c4b1cb15be Create ja.lang 2024-03-21 02:17:13 +08:00
49bf677da5 fix: 修复了plugins.json报错的问题 2024-03-21 02:04:51 +08:00
3657e6b93b fix: 修复了plugins.json报错的问题 2024-03-21 01:56:04 +08:00
db900a17d6 feat: 添加了对markdown的简单封装 2024-03-21 01:52:25 +08:00
d8a25c6ba5 feat: 添加了对markdown的简单封装 2024-03-21 01:47:30 +08:00
71cd7e6250 Create plugins.json 2024-03-21 01:46:22 +08:00
a1f99b74cf Create plugins.json 2024-03-21 01:43:22 +08:00
79f6d50e82 feat: 更新了插件商店部分功能 2024-03-21 01:20:18 +08:00
14d9f041ce feat: 对部分消息回复用markdown进行了重新排版 2024-03-20 22:30:52 +08:00
2b8cb2afb6 feat: 对部分消息回复用markdown进行了重新排版 2024-03-20 21:10:56 +08:00
8a1c981666 feat: 对部分消息回复用markdown进行了重新排版 2024-03-20 18:27:49 +08:00
fdefedf288 feat: 添加了对markdown的简单封装 2024-03-20 12:30:17 +08:00
e351465d97 feat: 添加了网页监控面板 2024-03-20 00:44:36 +08:00
ab5dc2200a feat: 添加了对markdown的简单封装 2024-03-19 22:43:55 +08:00
0bf56f79f1 feat: 添加了对markdown的简单封装 2024-03-19 22:06:30 +08:00
edf390ff43 feat: 添加了对markdown的简单封装 2024-03-19 21:56:31 +08:00
15c751b1c8 Changes 2024-03-19 20:38:25 +08:00
9585910623 feat: 添加了网页监控面板 2024-03-19 15:58:02 +08:00
d739c4cde6 feat: 添加了网页监控面板 2024-03-19 13:16:25 +08:00
3adc265876 feat: 添加了网页监控面板 2024-03-19 00:27:40 +08:00
51cb1a87b8 first commit 2024-03-18 18:21:56 +08:00
9e9f6e4ad6 update: r 2024-03-03 21:25:29 +08:00
d1795f0ca8 update: r 2024-03-03 21:22:49 +08:00
433ecf39ee update: Liteyuki ORM 2024-03-02 02:44:11 +08:00
e7c29c1597 update: Liteyuki ORM 2024-03-02 02:43:18 +08:00
8303514fa0 update: Liteyuki ORM 2024-03-01 23:24:36 +08:00
a3f63e383d Merge remote-tracking branch 'origin/main' 2024-03-01 00:08:08 +08:00
12231d08a8 update: Liteyuki ORM 2024-03-01 00:07:49 +08:00
247 changed files with 25758 additions and 185 deletions

33
.github/workflows/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,33 @@
---
name: BUG 反馈
about: 使用轻雪时遇到了问题?
---
## 问题反馈
### **描述**
请详细描述一下你所遇到的bug
### **确保**
- [ ] 我已查阅所有issues没有相似或已被证实的内容
- [ ] 我已按照文档指引进行正确的操作,仍会复现该问题
### **预期效果**
你想要什么样的预期效果
### **实际效果**
实际上是怎么样的
### **运行环境**
- 系统及版本:
- Python环境
- commit哈希或版本
- 硬件信息(可选)
### **运行日志**
```
将相关日志粘贴到此处
```
### **严重等级**
致命|严重|警告|轻微

49
.github/workflows/deploy-docs.yml vendored Normal file
View File

@ -0,0 +1,49 @@
name: 部署文档
on:
push:
branches:
# 确保这是你正在使用的分支名称
- main
permissions:
contents: write
jobs:
deploy-gh-pages:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
# 如果你文档需要 Git 子模块,取消注释下一行
# submodules: true
- name: 安装 pnpm
uses: pnpm/action-setup@v2
with:
run_install: true
version: 8
- name: 设置 Node.js
run: |-
cd docs
pnpm install
- name: 构建文档
env:
NODE_OPTIONS: --max_old_space_size=8192
run: |-
cd docs
pnpm run docs:build
> .vuepress/dist/.nojekyll
- name: 部署文档
uses: JamesIves/github-pages-deploy-action@v4
with:
# 这是文档部署到的分支名称
branch: gh-pages
folder: docs/.vuepress/dist

45
.gitignore vendored
View File

@ -1,12 +1,37 @@
# idea
plugin/
# config
config.yml
# external plugins
.venv/
.idea/
.cache/
node_modules/
data/
db/
/resources/
__pycache__/
*.pyc
*.pyo
*.pyd
*.pyw
/plugins/
_config.yml
config.yml
config.example.yml
compile.bat
liteyuki/resources/templates/latest-debug.html
# vuepress
.github
pyproject.toml
# pyc/pyo
**/*.pyc
**/*.pyo
test.py
line_count.py
# nuitka
main.build/
main.dist/
main.exe
main.cmd
docs/.vuepress/.cache/
docs/.vuepress/.temp/
docs/.vuepress/dist/
prompt.txt
# js
**/echarts.js

1
CNAME
View File

@ -1 +0,0 @@
bot.liteyuki.icu

19
Dockerfile Normal file
View File

@ -0,0 +1,19 @@
FROM python:3.11-bullseye
ENV TZ Asia/Shanghai
COPY docker/sources.list /etc/apt/sources.list
RUN apt-get update && apt-get install -y git
WORKDIR /liteyukibot
COPY . /liteyukibot
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
RUN apt-get install -y libnss3 libnspr4 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libatspi2.0-0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libxkbcommon0 libasound2
EXPOSE 20216
CMD ["python", "main.py"]

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Snowykami
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

102
README.md
View File

@ -1,80 +1,52 @@
<div align="center">
<img src="https://ks.liteyuki.icu:809/static/img/liteyuki_icon.png" style="width: 30%; margin-top:10%;" alt="a">
[//]: # (<img src="https://cdn.liteyuki.icu/static/img/logo.png" style="align-content: center; width: 50%; margin-top:10%;" alt="a">)
[![][banner]][lightyuki-link]
<h2><a href="https://bot.liteyuki.icu"> <span style="color: #a2d8f4">轻雪</span> <span style="color: #d0e9ff">6</span></a></h2>
<h4> <span style="color: #a2d8f4">✨ 轻量,高效,易于扩展✨</span></h4>
[![][OneBot]][onebot-link]
[![][Nonebot2]][nonebot-link]
[![][Liteyuki6.0]][lightyuki-link]
[![][Python3.10+]][python-link]
[![][Usage]][usage-link]
- 基于[Nonebot2](https://github.com/nonebot/nonebot2),有良好的生态支持
- 开箱即用,无需复杂配置
- 新的点击交互模式,拒绝手打指令
- 可视化插件管理包管理,支持一键安装插件
- 支持OneBot标准通信但不限于此
- 自定义主题支持,满足审美需求
- 国际化支持,支持多种语言
<h3>👇所有内容已迁移至👇</h3>
<h2><a href="https://bot.liteyuki.icu">轻雪主页</a></h2>
</div>
<div align=center>
<h2>
<font color="#d0e9ff">
轻雪
</font>
<font color="#a2d8f4">
6.0
</font>
</h2>
</div>
<div align=center><h4>轻量,高效,易于扩展</h4></div>
- 基于[Nonebot2]("https://github.com/nonebot/nonebot2"),有良好的生态支持
- 集成了上一代轻雪的优点和~~缺点~~
- 支持一切Onebot标准通信后续会支持更多的平台
### 感谢
- [Nonebot2](https://nonebot.dev)提供的框架支持
- [nonebot-plugin-htmlrender](https://github.com/kexue-z/nonebot-plugin-htmlrender)提供的渲染功能
- [nonebot-plugin-alconna](https://github.com/ArcletProject/nonebot-plugin-alconna)提供的命令解析功能
## 手动安装和部署
1. 安装`Git``Python3.10+`后,使用命令`git clone https://github.com/snowykami/LiteyukiBot` 克隆项目至本地。
一定要安装GitBot自带功能需要git支持
2. 切换到轻雪目录,使用`pip install -r requirements.txt`安装依赖
[OneBot]: https://img.shields.io/badge/OneBot-11/12-blue?style=for-the-badge
3. `python main.py`启动!
[Nonebot2]: https://img.shields.io/badge/Nonebot-2-red?style=for-the-badge
## 一键部署脚本(复制到本地保存执行)
[Liteyuki6.0]: https://img.shields.io/badge/Liteyuki-6.0-blue?style=for-the-badge
#### 提前部署好`Python3.10+`环境和`Git`环境
[Python3.10+]: https://img.shields.io/badge/Python-3.10+-blue?style=for-the-badge
#### Windows
[Usage]: https://img.shields.io/badge/文档-页面-blue?style=for-the-badge
```bash
chcp 65001
git clone https://github.com/snowykami/LiteyukiBot
cd LiteyukiBot
pip install -r requirements.txt
echo python3 main.py > start.bat
echo Install finished! Please run start.bat to start the bot!
```
[onebot-link]:https://onebot.dev/
#### Linux
[nonebot-link]:https://nonebot.dev/
```bash
git clone https://github.com/snowykami/LiteyukiBot
cd LiteyukiBot
pip install -r requirements.txt
echo python3 main.py > start.sh
chmod +x start.sh
echo Install finished! Please run start.sh to start the bot!
```
[lightyuki-link]:/
## 注意事项
[python-link]:https://www.python.org/
- 尽可能不要去动配置文件通过与bot交互进行配置即可若仍然想自定义配置请在`config.yml`中修改
[usage-link]:https://bot.liteyuki.icu/
- 首次启动会提醒用户注册超级用户
- Bot会自动检测新版本若出现新版本可用`git pull`命令更新
### Onebot实现端配置
| 字段 | 参考值 | 说明 |
|----|-------------------------------|-------------------------|
| 协议 | 反向WebSocket | 轻雪使用反向ws协议进行通信即轻雪作为服务端 |
| 地址 | ws://`host`:`port`/onebot/v11 | 地址取决于配置文件,默认为`20216`端口 |
### 推荐方案
1. 使用`Lagrange.Core``Lagrange.Core`支持多种协议
2. 云崽的`icqq-plugin``ws-plugin`进行通信
3. `Go-cqhttp`(目前已经半死不活了)
4. 人工实现的`Onebot`协议自己整一个WebSocket客户端看着QQ的消息然后给轻雪传输数据
请先自行查阅文档若有困难请联系相关开发者而不是Liteyuki的开发者
## 鸣谢
- html转图片使用的[kexue-z](https://github.com/kexue-z)的[nonebot-plugin-htmlrender](https://github.com/kexue-z/nonebot-plugin-htmlrender)插件的部分代码
- 重启方案用的[18870](https://github.com/18870)的[Nonebot-plugin-reboot](https://github.com/18870/nonebot-plugin-reboot)插件的部分代码
- Lagrange.Core的测试环境支持
[banner]: https://socialify.git.ci/snowykami/LiteyukiBot/image?description=1&forks=1&issues=1&Plus&pulls=1&stargazers=1&theme=Auto&logo=https%3A%2F%2Fcdn.liteyuki.icu%2Fstatic%2Fimg%2Flogo.png

View File

@ -1,4 +0,0 @@
{
"name": "Liteyuki Default",
"version": "1.0"
}

View File

@ -1,4 +0,0 @@
{
"name": "Liteyuki Language Pack",
"version": "1.0"
}

10
docker/sources.list Normal file
View File

@ -0,0 +1,10 @@
deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye main contrib non-free
# deb-src https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye main contrib non-free
deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-updates main contrib non-free
# deb-src https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-updates main contrib non-free
deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-backports main contrib non-free
# deb-src https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-backports main contrib non-free
deb https://mirrors.tuna.tsinghua.edu.cn/debian-security bullseye-security main contrib non-free
# deb-src https://mirrors.tuna.tsinghua.edu.cn/debian-security bullseye-security main contrib non-free

5
docs/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules/
./.vuepress/.cache/
./.vuepress/.temp/
./.vuepress/dist/

14
docs/.vuepress/client.js Normal file
View File

@ -0,0 +1,14 @@
import {defineClientConfig} from "vuepress/client";
import resourceStoreComp from "./components/res_store.vue";
import pluginStoreComp from "./components/plugin_store.vue";
//导入element-plus
import ElementPlus from 'element-plus';
export default defineClientConfig({
enhance: ({app, router, siteData}) => {
app.component("resourceStoreComp", resourceStoreComp);
app.component("pluginStoreComp", pluginStoreComp);
app.use(ElementPlus);
},
});

View File

@ -0,0 +1,126 @@
<template>
<div class="item-card">
<div class="item-name">{{ props.item.name }}</div>
<div class="item-description">{{ props.item.desc }}</div>
<div class="item-bar">
<!-- 三个可点击svg一个github一个下载一个可点击"https://github.com/{{ username }}.png?size=80"个人头像配上id-->
<a :href=props.item.homepage class="btn">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16">
<path fill="currentColor"
d="m7.775 3.275l1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0a.751.751 0 0 1 .018-1.042a.751.751 0 0 1 1.042-.018a1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018a.751.751 0 0 1-.018-1.042m-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018a.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0a.751.751 0 0 1-.018 1.042a.751.751 0 0 1-1.042.018a1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83"/>
</svg>
</a>
<!-- <button class="copy-btn btn"><div @click="copyToClipboard">安装</div></button> 点击后把安装命令写入剪贴板-->
<button class="btn copy-btn" @click="copyToClipboard">复制安装命令</button>
<div class="btn">
<a class="author-info" :href="`https://github.com/${props.item.author }`">
<img class="icon avatar" :src="`https://github.com/${ props.item.author }.png?size=80`" alt="">
<div class="author-name">{{ props.item.author }}</div>
</a>
</div>
<!-- 复制键复制安装命令npm install props.item.module_name-->
</div>
</div>
</template>
<script setup lang="ts">
import {defineProps, onMounted} from 'vue'
import Clipboard from 'clipboard'
// 复制安装命令按钮
// 构建复制成功和失败的提示
const props = defineProps({
item: Object
})
const copyToClipboard = () => {
const clipboard = new Clipboard('.copy-btn', {
text: () => `npm install ${props.item.module_name}`
})
clipboard.on('success', () => {
})
clipboard.on('error', () => {
})
}
// 复制到剪贴板的函数
</script>
<style scoped>
.item-card {
position: relative;
border-radius: 15px;
background-color: #00000011;
height: 160px;
padding: 16px;
margin: 10px;
box-sizing: border-box;
transition: background 0.3s ease;
}
.btn {
margin-right: 15px;
}
button {
background-color: #00000000;
border: none;
}
.copy-btn {
cursor: pointer;
color: #666;
}
.copy-btn:hover {
color: #111;
}
.item-name {
color: #111;
font-size: 20px;
margin-bottom: 10px;
}
.item-description {
color: #333;
font-size: 12px;
white-space: pre-wrap;
}
.icon {
width: 20px;
height: 20px;
color: $themeColor;
}
.author-info {
display: flex;
justify-content: left;
align-items: center;
}
.author-name {
font-size: 15px;
font-weight: normal;
}
.avatar {
border-radius: 50%;
margin: 0 10px;
}
.item-bar {
position: absolute;
bottom: 0;
height: 50px;
display: flex;
align-items: center;
box-sizing: border-box;
justify-content: space-between;
color: #00000055;
}
</style>

View File

@ -0,0 +1,39 @@
<script setup lang="ts">
import {ref} from 'vue'
import ItemCard from './plugin_item_card.vue'
// 插件商店Nonebot
let items = ref([])
fetch('https://registry.nonebot.dev/plugins.json')
.then(response => response.json())
.then(data => {
items.value = data
})
.catch(error => console.error(error))
</script>
<template>
<div>
<h1>插件商店</h1>
<p>所有内容来自<a href="https://nonebot.dev/store/plugins">NoneBot插件商店</a>在此仅作引用具体请访问NoneBot插件商店</p>
<div class="market">
<!-- 布局商品-->
<ItemCard v-for="item in items" :key="item.id" :item="item"/>
</div>
</div>
</template>
<style scoped>
h1 {
color: #00a6ff;
text-align: center;
font-weight: bold;
}
.market {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 10px;
}
</style>

View File

@ -0,0 +1,90 @@
<template>
<div class="item-card">
<div class="item-name">{{ props.item.name }}</div>
<div class="item-description">{{ props.item.description }}</div>
<div class="item-bar">
<!-- 三个可点击svg一个github一个下载一个可点击"https://github.com/{{ username }}.png?size=80"个人头像配上id-->
<a :href=props.item.link class="">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16">
<path fill="currentColor"
d="m7.775 3.275l1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0a.751.751 0 0 1 .018-1.042a.751.751 0 0 1 1.042-.018a1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018a.751.751 0 0 1-.018-1.042m-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018a.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0a.751.751 0 0 1-.018 1.042a.751.751 0 0 1-1.042.018a1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83"/>
</svg>
</a>
<div><a class="author-info" :href="`https://github.com/${props.item.author }`">
<img class="icon avatar" :src="`https://github.com/${ props.item.author }.png?size=80`" alt="">
<div class="author-name">{{ props.item.author }}</div>
</a>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {defineProps} from 'vue'
const props = defineProps({
item: Object
})
</script>
<style scoped>
.item-card {
position: relative;
border-radius: 15px;
background-color: #00000011;
height: 160px;
padding: 16px;
margin: 10px;
box-sizing: border-box;
transition: background 0.3s ease;
}
.item-card:hover {
border: 2px solid $themeColor;
}
.item-name {
color: $themeColor;
font-size: 20px;
margin-bottom: 10px;
}
.item-description {
color: #333;
font-size: 15px;
white-space: pre-wrap;
}
.icon {
width: 20px;
height: 20px;
color: $themeColor;
}
.author-info {
display: flex;
justify-content: left;
align-items: center;
}
.author-name {
font-size: 15px;
font-weight: normal;
}
.avatar {
border-radius: 50%;
margin: 0 10px;
}
.item-bar {
position: absolute;
bottom: 0;
height: 50px;
display: flex;
align-items: center;
box-sizing: border-box;
justify-content: space-between;
color: #00000055;
}
</style>

View File

@ -0,0 +1,38 @@
<script setup lang="ts">
import {ref} from 'vue'
import ItemCard from './res_item_card.vue'
// 从public/assets/resources.json加载插件
let items = ref([])
fetch('https://bot.liteyuki.icu/assets/resources.json')
.then(response => response.json())
.then(data => {
items.value = data
})
.catch(error => console.error(error))
</script>
<template>
<div>
<h1>主题/资源商店</h1>
<div class="market">
<!-- 布局商品-->
<ItemCard v-for="item in items" :key="item.id" :item="item" />
</div>
</div>
</template>
<style scoped>
h1 {
color: #00a6ff;
text-align: center;
font-weight: bold;
}
.market {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 10px;
}
</style>

30
docs/.vuepress/config.ts Normal file
View File

@ -0,0 +1,30 @@
import {defineUserConfig} from "vuepress";
import theme from "./theme.js";
import viteBundler from "@vuepress/bundler-vite";
export default defineUserConfig({
base: "/",
lang: "zh-CN",
title: "LiteyukiBot 轻雪机器人",
description: "LiteyukiBot | 轻雪机器人 | An OneBot Standard ChatBot | 一个OneBot标准的聊天机器人",
head: [
// 设置 favor.ico.vuepress/public 下
['link', {rel: 'icon', href: 'https://cdn.liteyuki.icu/favicon.ico'},],
['link', {rel: 'stylesheet', href: 'https://cdn.bootcdn.net/ajax/libs/firacode/6.2.0/fira_code.min.css'}],
[
"meta",
{
name: "viewport",
content: "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no",
},
],
],
theme,
// 和 PWA 一起启用
// shouldPrefetch: false,
});

20
docs/.vuepress/navbar.ts Normal file
View File

@ -0,0 +1,20 @@
import {navbar} from "vuepress-theme-hope";
export default navbar([
"/",
{
text: "项目部署",
link: "/deployment/",
prefix: "deployment/",
},
{
text: "使用手册",
link: "/usage/",
prefix: "usage/",
},
{
text: "商店",
link: "/store/resource",
prefix: "store/",
}
]);

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1024 1024"><path fill="#FDD7AD" d="M512 0 335.448 88.272l-70.616 35.312-70.624 35.312-176.552 88.28v529.648L512 1024l494.344-247.176V247.176z"/><path fill="#CBB292" d="m759.176 370.76-70.624 35.304-494.344-247.168 70.624-35.312zM512 494.344V1024L17.656 776.824V247.176z"/><path fill="#7F6E5D" d="M1006.344 247.168v529.656L512 1024V494.344l176.552-88.28v70.624l141.24-70.624v-70.616z"/><path fill="#7F5B53" d="M829.792 335.448v70.624L688.56 476.68v-70.624z"/><path fill="#CBB292" d="m829.792 335.448-70.624 35.312-494.344-247.176 70.624-35.312z"/><path fill="#2C3E50" d="m682.52 550.32 157.032-78.512a17.656 17.656 0 0 1 25.552 15.792v9.32a52.96 52.96 0 0 1-29.28 47.376L678.8 622.8a17.656 17.656 0 0 1-25.552-15.792v-9.312a52.96 52.96 0 0 1 29.28-47.376z"/></svg>

After

Width:  |  Height:  |  Size: 854 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024"><defs><linearGradient id="a" x1="522.593" x2="522.593" y1="-70.302" y2="-335.937" gradientUnits="userSpaceOnUse" spreadMethod="pad"><stop offset="0" stop-color="#fe5d5a" stop-opacity=".1"/><stop offset=".908" stop-color="#ef1220" stop-opacity=".5"/></linearGradient><linearGradient id="b" x1="107.12" x2="935.038" y1="-373.67" y2="-373.67" gradientUnits="userSpaceOnUse" spreadMethod="pad"><stop offset="0" stop-color="#ff5e59"/><stop offset="1" stop-color="#f01422"/></linearGradient><linearGradient id="c" x1="519.405" x2="519.405" y1="-195.547" y2="-726.816" gradientUnits="userSpaceOnUse" spreadMethod="pad"><stop offset="0" stop-color="#ffe2e2"/><stop offset=".888" stop-color="#ff8e8e"/></linearGradient><linearGradient id="d" x1="191.5" x2="483.9" y1="-564.9" y2="-564.9" gradientUnits="userSpaceOnUse" spreadMethod="pad"><stop offset="0" stop-color="#e92700" stop-opacity=".3"/><stop offset=".013" stop-color="#ef1220" stop-opacity=".2"/></linearGradient><linearGradient id="e" x1="403.502" x2="253.121" y1="-847.32" y2="-586.853" gradientUnits="userSpaceOnUse" spreadMethod="pad"><stop offset="0" stop-color="#ff5e59"/><stop offset=".201" stop-color="#f01422"/></linearGradient><linearGradient id="f" x1="330.485" x2="330.485" y1="-801.787" y2="-625.789" gradientUnits="userSpaceOnUse" spreadMethod="pad"><stop offset="0" stop-color="#ff5e59"/><stop offset=".201" stop-color="#f01422"/></linearGradient><linearGradient id="g" x1="397.351" x2="256.845" y1="-647.231" y2="-890.596" gradientUnits="userSpaceOnUse" spreadMethod="pad"><stop offset="0" stop-color="#ffa6a6"/><stop offset=".908" stop-color="#ff6b5d"/></linearGradient></defs><path fill="url(#a)" d="M501.2 662.3 327.6 763.8c-13.9 8.1-14.2 28.1-.5 36.7l179.1 97.7c10.9 5.9 24.1 5.9 34.9-.1l177-97.9c13.6-8.5 13.4-28.3-.3-36.5l-168.4-101c-14.8-9-33.3-9.1-48.2-.4Z"/><path fill="#f63037" d="m110.2 525.7-3.1 77.6 57.5 18.5L184 519.4Z"/><path fill="url(#b)" d="m476.6 363.5-328 154.6c-21 42.7-55.4 65.4-35.5 103.5 4.2 8 9.4 14.4 15.4 18.1l358.2 195.5c21.8 11.9 48.1 11.8 69.8-.2l354-195.8c27.2-16.9 34.8-90.3 7.3-106.8L573 364.1c-29.7-17.8-66.6-18-96.4-.6Z"/><path fill="url(#c)" d="M476.6 298.7 129.4 501.6c-27.8 16.3-28.4 56.3-1 73.3l358.2 195.5c21.8 11.9 48.1 11.8 69.8-.2l354-195.8c27.2-16.9 26.9-56.6-.6-73.1L573 299.3c-29.7-17.8-66.6-18-96.4-.6Z"/><path fill="#ff8989" fill-opacity=".31" d="m481.2 387.8 39.4 123.4c1.1 3.4 4 6 7.6 6.6l173.4 30.4-33-118.3c-.9-3.3-3.6-5.8-7-6.5l-180.4-35.6ZM327 499.2l40.4 101.1L496.7 525c2.5-1.5 3.7-4.5 2.7-7.3l-36-106.8-127.6 65c-8.6 4.3-12.4 14.4-8.8 23.3ZM523.8 540.5l-140.3 77.2L567.2 659c3.2.7 6.6.1 9.3-1.6l134.6-85-174.7-33.8c-4.3-1-8.7-.3-12.6 1.9Z"/><path fill="url(#d)" d="M483.9 406.1c0 35.46-65.46 64.2-146.2 64.2s-146.2-28.74-146.2-64.2c0-35.46 65.46-64.2 146.2-64.2s146.2 28.74 146.2 64.2Z"/><path fill="url(#e)" d="m254.2 188.4-123 83.1c-1.8 1.3-2.6 3.6-1.8 5.7l39.1 110.6c.6 1.7 2 2.9 3.8 3.2l221.8 40.5c1.3.3 2.7-.1 3.7-.8l131.7-93.6c1.9-1.4 2.6-3.9 1.7-6.1l-49.4-107c-.6-1.5-2.1-2.6-3.7-2.8l-220.3-33.5c-1.3-.2-2.6.1-3.6.7Z"/><path fill="url(#f)" d="m528.6 274.5 3 59.1-205 65.6-177.2-72.7-20-49.2 1.9-54.1Z"/><path fill="url(#g)" d="m250.6 138-112.3 76c-6 4.1-8.5 11.7-6.1 18.5l34.2 96.6c1.9 5.4 6.6 9.3 12.1 10.4l211 38.5c4.3.7 8.6-.2 12.1-2.7l120.5-85.5c6.3-4.4 8.4-12.7 5.3-19.7l-43.1-93.5c-2.2-4.9-6.8-8.3-12.1-9.1L262 135.6c-4-.7-8 .2-11.4 2.4Z"/><path fill="#fff" d="m419.8 252.8-79-11-29-57.7c-3.8-7.6-13.2-10.7-20.8-6.9-7.6 3.8-10.7 13.2-6.9 20.8l26.6 52.9-61.8 42.2c-7.1 4.8-8.9 14.5-4.1 21.5 3 4.4 7.9 6.8 12.8 6.8 3 0 6-.9 8.7-2.7l68-46.4 81.1 11.2c.7.1 1.4.1 2.1.1 7.6 0 14.3-5.6 15.3-13.4 1.4-8.4-4.5-16.2-13-17.4Z"/></svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>

After

Width:  |  Height:  |  Size: 963 B

View File

@ -0,0 +1 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 960 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1536 1024"><path fill="#1296db" d="M1425.067.256H110.933A110.933 110.933 0 0 0 0 110.848v723.627a110.933 110.933 0 0 0 110.933 110.933h1314.39c61.269 0 110.933-49.75 110.677-110.677V110.848A110.933 110.933 0 0 0 1425.067.256z" class="selected" data-spm-anchor-id="a313x.7781069.0.i4"/><path fill="#FFF" d="M664.747 723.797V435.883L517.12 620.373l-147.456-184.49v288l-148.053-67.158V221.781h147.626l147.627 184.576 147.541-184.576h147.627v565.76z"/><path d="M1024 0h426.667A85.333 85.333 0 0 1 1536 85.333v768a85.333 85.333 0 0 1-85.333 85.334H1024V0z" opacity=".1"/><path fill="#FFF" d="m1256.96 731.307-170.667-216.491h113.75V304.64h113.749v210.176h113.835z" opacity=".5"/></svg>

After

Width:  |  Height:  |  Size: 771 B

View File

@ -0,0 +1,39 @@
[
{
"name": "KawaiiStatus",
"author": "SnowyKami",
"description": "可爱的状态卡片仿照koishi的制作",
"link": "https://cdn.liteyuki.icu/static/lrp/KawaiiStatus.zip",
"icon": "https://cdn.jsdelivr.net/gh/SnowyKami/CDN/img/KawaiiStatus.png"
},
{
"name": "MiSans字体包",
"author": "SnowyKami",
"description": "小米官方字体MiSans",
"link": "https://cdn.liteyuki.icu/static/lrp/MiSansFonts.zip"
},
{
"name": "MapleMono字体包",
"author": "SnowyKami",
"description": "适用于字母的字体包",
"link": "https://cdn.liteyuki.icu/static/lrp/MapleMonoFonts.zip"
},
{
"name": "野兽先辈主题HomoTheme",
"author": "SnowyKami",
"description": "野兽先辈主题包114514",
"link": "https://cdn.liteyuki.icu/static/lrp/HomoTheme.zip"
},
{
"name": "示例包2",
"author": "SnowyKami",
"description": "A simple bot that shows the status of the bot and the server.",
"link": ""
},
{
"name": "示例包3",
"author": "SnowyKami",
"description": "A simple bot that shows the status of the bot and the server.",
"link": ""
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" class="icon" viewBox="0 0 3280.944 2800"><path fill="#41b883" d="M1645.332 601.004h375.675L1081.82 2238.478 142.636 601.004h718.477l220.708 379.704 216.013-379.704z"/><path fill="#41b883" d="M142.636 601.004l939.185 1637.474 939.186-1637.474h-375.675l-563.51 982.484-568.208-982.484z"/><path fill="#35495e" d="M513.188 601.004l568.207 987.23 563.511-987.23h-347.498l-216.013 379.704-220.708-379.704zM1607.792 1311.83l594.678 2.293 187.353-316.325-598.662 2.292zM2198.506 1909.57C2867.436 732.7 2939.502 605.426 2937.874 603.78c-.715-.723 45.303-1.314 102.262-1.314s103.562.428 103.562.951c0 .523-208.57 367.978-463.491 816.567L2216.715 2235.6l-102.1.596-102.102.596z"/><path fill="#41b883" d="M1680.563 2233.328c0-1.34 168.208-298.145 440.375-777.048a4135645.775 4135645.775 0 00337.619-594.19l146.13-257.25 170.746-.04 170.747-.04-5.536 9.741c-3.044 5.358-43.727 77.302-90.407 159.875-85.356 150.992-337.562 595.163-656.602 1156.373l-172 302.559-170.536.588c-93.795.322-170.536.069-170.536-.567z"/><path fill="#35495e" d="M1429.783 1625.351l594.679 2.292 187.353-316.324-598.662 2.292z"/><path fill="#41b883" d="M1524.207 1464.903l608.285 6.877 173.746-320.909h-619.072z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

25
docs/.vuepress/sidebar.ts Normal file
View File

@ -0,0 +1,25 @@
import {sidebar} from "vuepress-theme-hope";
export default sidebar({
"/": [
"",
{
text: "项目部署",
icon: "laptop-code",
prefix: "deployment/",
children: "structure",
},
{
text: "使用手册",
icon: "book",
prefix: "usage/",
children: "structure",
},
{
text: "商店",
icon: "store",
prefix: "store/",
children: "structure",
}
],
});

View File

@ -0,0 +1,7 @@
// you can change config here
$colors: #c0392b, #d35400, #f39c12, #27ae60, #16a085, #2980b9, #8e44ad, #2c3e50,
#7f8c8d !default;
body {
overflow-x: hidden;
}

View File

@ -0,0 +1,16 @@
// place your custom styles here
#main-title {
font-family: ColorTube, "Fira Code", serif;
color: #ff0000 !important; /* 你想要的颜色 */
line-height: 2;
}
@font-face {
font-family: ColorTube;
src: url("/assets/fonts/ColorTube.woff") format("woff")
}
code {
font-family: "Fira Code", monospace !important;
}

View File

@ -0,0 +1,2 @@
// you can change colors here
$theme-color: #00a6ff;

192
docs/.vuepress/theme.js Normal file
View File

@ -0,0 +1,192 @@
import {hopeTheme} from "vuepress-theme-hope";
import navbar from "./navbar.js";
import sidebar from "./sidebar.js";
export default hopeTheme({
hostname: "https://vuepress-theme-hope-docs-demo.netlify.app",
author: {
name: "远野千束",
url: "https://snowykami.me",
},
iconAssets: "fontawesome-with-brands",
logo: "https://cdn.liteyuki.icu/static/img/liteyuki_icon_640.png",
repo: "https://github.com/snowykami/LiteyukiBot",
docsDir: "docs",
// 导航栏
navbar,
// 侧边栏
sidebar,
// 页脚
footer: "LiteyukiBot",
displayFooter: true,
// 加密配置
encrypt: {
config: {
"/demo/encrypt.html": ["1234"],
},
},
// 多语言配置
metaLocales: {
editLink: "在 GitHub 上编辑此页",
},
// 如果想要实时查看任何改变,启用它。注: 这对更新性能有很大负面影响
// hotReload: true,
// 在这里配置主题提供的插件
plugins: {
search: true,
// search: true,
comment: {
provider: "Giscus",
repo: "snowykami/LiteyukiBot",
repoId: "R_kgDOHVNKpQ",
category: "Announcements",
categoryId: "DIC_kwDOHVNKpc4CeWxj",
},
components: {
components: ["Badge", "VPCard"],
},
// 此处开启了很多功能用于演示,你应仅保留用到的功能。
mdEnhance: {
alert: true,
align: true,
attrs: true,
codetabs: true,
footnote: true,
component: true,
demo: true,
figure: true,
imgLazyload: true,
imgSize: true,
include: true,
mark: true,
stylize: [
{
matcher: "Recommended",
replacer: ({tag}) => {
if (tag === "em")
return {
tag: "Badge",
attrs: {type: "tip"},
content: "Recommended",
};
},
},
],
sub: true,
sup: true,
tabs: true,
vPre: true,
// 在启用之前安装 chart.js
// chart: true,
// insert component easily
// 在启用之前安装 echarts
// echarts: true,
// 在启用之前安装 flowchart.ts
// flowchart: true,
// gfm requires mathjax-full to provide tex support
// gfm: true,
// 在启用之前安装 katex
// katex: true,
// 在启用之前安装 mathjax-full
// mathjax: true,
// 在启用之前安装 mermaid
// mermaid: true,
// playground: {
// presets: ["ts", "vue"],
// },
// 在启用之前安装 reveal.js
// revealJs: {
// plugins: ["highlight", "math", "search", "notes", "zoom"],
// },
// 在启用之前安装 @vue/repl
// vuePlayground: true,
// install sandpack-vue3 before enabling it
// sandpack: true,
},
// 如果你需要 PWA。安装 @vuepress/plugin-pwa 并取消下方注释
// pwa: {
// favicon: "/favicon.ico",
// cacheHTML: true,
// cachePic: true,
// appendBase: true,
// apple: {
// icon: "/assets/icon/apple-icon-152.png",
// statusBarColor: "black",
// },
// msTile: {
// image: "/assets/icon/ms-icon-144.png",
// color: "#ffffff",
// },
// manifest: {
// icons: [
// {
// src: "/assets/icon/chrome-mask-512.png",
// sizes: "512x512",
// purpose: "maskable",
// type: "image/png",
// },
// {
// src: "/assets/icon/chrome-mask-192.png",
// sizes: "192x192",
// purpose: "maskable",
// type: "image/png",
// },
// {
// src: "/assets/icon/chrome-512.png",
// sizes: "512x512",
// type: "image/png",
// },
// {
// src: "/assets/icon/chrome-192.png",
// sizes: "192x192",
// type: "image/png",
// },
// ],
// shortcuts: [
// {
// name: "Demo",
// short_name: "Demo",
// url: "/demo/",
// icons: [
// {
// src: "/assets/icon/guide-maskable.png",
// sizes: "192x192",
// purpose: "maskable",
// type: "image/png",
// },
// ],
// },
// ],
// },
// },
},
});

100
docs/README.md Normal file
View File

@ -0,0 +1,100 @@
---
home: true
icon: home
title: 首页
heroImage: https://cdn.liteyuki.icu/static/img/logo.png
bgImage:
bgImageDark:
bgImageStyle:
background-attachment: fixed
heroText: LiteyukiBot
tagline: 轻雪机器人一个以轻量和简洁为设计理念基于Nonebot2的OneBot标准聊天机器人
actions:
- text: 快速部署
icon: lightbulb
link: ./deployment/install.html
type: primary
- text: 使用手册
icon: book
link: ./usage/basic_command.html
highlights:
- header: 简洁至上
image: /assets/image/layout.svg
bgImage: https://theme-hope-assets.vuejs.press/bg/2-light.svg
bgImageDark: https://theme-hope-assets.vuejs.press/bg/2-dark.svg
bgImageStyle:
background-repeat: repeat
background-size: initial
features:
- title: 基于Nonebot2
icon: robot
details: 拥有良好的生态支持
link: https://nonebot.dev/
- title: 可视化插件管理
icon: plug
details: 使用<code>npm</code>,无需命令行操作即可安装/卸载插件
- title: 点击交互
icon: mouse-pointer
details: 新的点击交互模式,拒绝手打指令
- title: 主题支持
icon: paint-brush
details: 支持多种主题,可自定义资源包,满足你的审美需求
link: https://bot.liteyuki.icu/usage/resource_pack.html
- title: 国际化
icon: globe
details: 支持多种语言包括i18n部分语言和自行扩展的语言代码
link: https://baike.baidu.com/item/i18n/6771940
- title: 简易配置
icon: cog
details: 无需过多配置,开箱即用
link: https://bot.liteyuki.icu/deployment/config.html
- title: 低占用
icon: memory
details: 使用更少的依赖和资源
- title: OneBot标准
icon: link
details: 支持OneBotv11/12标准的四种通信协议
link: https://onebot.dev/
- title: Alconna命令解析
icon: link
details: 使用Alconna实现高效命令解析
link: https://github.com/nonebot/plugin-alconna
- title: 便捷更新
icon: cloud-download
details: 聊天窗口命令更新,无需手动下载
- title: 服务支持
icon: server
details: 内置轻雪API可自动收集错误提供图床服务
- title: 开源
icon: code
details: 项目遵循MIT协议开源欢迎各位的贡献
- header: 快速部署
image: /assets/image/box.svg
bgImage: https://theme-hope-assets.vuejs.press/bg/3-light.svg
bgImageDark: https://theme-hope-assets.vuejs.press/bg/3-dark.svg
highlights:
- title: 安装 Git 和 Python3.10+
- title: 使用 <code>git clone https://github.com/snowykami/LiteyukiBot</code> 以克隆项目至本地。
details: 如果无法连接到GitHub可以使用 <code>git clone https://gitee.com/snowykami/LiteyukiBot</code>
- title: 使用 <code>cd LiteyukiBot</code> 切换到项目目录。
- title: 使用 <code>pip install -r requirements.txt</code> 安装项目依赖。
details: 如果你有多个 Python 环境,请使用 <code>pythonx -m pip install -r requirements.txt</code>
- title: 使用 <code>python main.py</code> 启动项目。
copyright: © 2021-2024 SnowyKami All Rights Reserved
---

View File

@ -0,0 +1,8 @@
---
title: 项目部署
index: false
icon: laptop-code
category: 部署
---
<Catalog />

61
docs/deployment/config.md Normal file
View File

@ -0,0 +1,61 @@
---
title: 配置
icon: cog
order: 2
category: 使用指南
tag:
- 配置
- 部署
---
首次运行后生成`config.yml`,你可以修改配置项后重启轻雪,绝大多数情况下,你只需要修改`superusers``nickname`字段即可
## **基础配置项**
```yaml
command_start: [ "/", "" ] # 指令前缀,若没有""空命令头请开启alconna_use_command_start保证alconna解析正常
host: 127.0.0.1 # 监听地址默认为本机若要接收外部请求请填写0.0.0.0
port: 20216 # 绑定端口
nickname: [ "liteyuki" ] # 机器人昵称列表
superusers: [ "1919810" ] # 超级用户列表
```
## **其他配置**
以下为默认值,如需自定义请手动添加
```yaml
onebot_access_token: "" # 访问令牌,对公开放时建议设置
default_language: "zh-CN" # 默认语言
alconna_auto_completion: false # alconna是否自动补全指令默认false建议开启
# 开发者选项
log_level: "INFO" # 日志等级
log_icon: true # 是否显示日志等级图标(某些控制台字体不可用)
auto_report: true # 是否自动上报问题给轻雪服务器
auto_update: true # 是否自动更新轻雪每天4点检查更新
debug: false # 轻雪调试,开启后在调试时修改代码或资源会自动重载相应内容
safe_mode: false # 安全模式,开启后将不会加载任何第三方插件
# 其他Nonebot插件的配置项
custom_config_1: "custom_value1"
custom_config_2: "custom_value2"
...
```
> [!tip]
> 如果要使用dotenv配置文件请自行创建`.env.{ENVIRONMENT}`,并在`config.yml`中添加`environment:{ENVIRONMENT}`字段
## **OneBot实现端配置**
生产环境中推荐反向WebSocket
不同的实现端给出的字段可能不同,但是基本上都是一样的,这里给出一个参考值
| 字段 | 参考值 | 说明 |
|-------------|------------------------------------|----------------------------------|
| 协议 | 反向WebSocket | 推荐使用反向ws协议进行通信即轻雪作为服务端 |
| 地址 | ws://127.0.0.1:20216/onebot/v11/ws | 地址取决于配置文件,本机默认为`127.0.0.1:20216` |
| AccessToken | `""` | 如果你给轻雪配置了`AccessToken`,请在此填写相同的值 |
## **其他**
- 要使用其他通信方式请访问[OneBot Adapter](https://onebot.adapters.nonebot.dev/)获取详细信息
- 轻雪不局限于OneBot适配器你可以使用NoneBot2支持的任何适配器

58
docs/deployment/fandq.md Normal file
View File

@ -0,0 +1,58 @@
---
title: 答疑
icon: question
order: 3
category: 使用指南
tag:
- 配置
- 部署
---
## **常见问题**
- 设备上Python环境太乱了pip和python不对应怎么办
- 请使用`/path/to/python -m pip install -r requirements.txt`来安装依赖,
然后用`/path/to/python main.py`来启动Bot
其中`/path/to/python`是你要用来运行Bot的可执行文件
- 为什么我启动后机器人没有反应?
- 请检查配置文件的`command_start``superusers`,确认你有权限使用命令并按照正确的命令发送
- 确认命令头没有和`nickname{}`冲突,例如一个命令是`help`,但是`Bot`昵称有一个`help`那么将会被解析为nickname而不是命令
- 更新轻雪失败,报错`InvalidGitRepositoryError`
- 请正确安装`Git`,并使用克隆而非直接下载的方式部署轻雪
- 怎么登录聊天平台例如QQ
- 你有这个问题说明你不是很了解这个项目,本项目不负责实现登录功能,只负责处理和回应消息,登录功能由实现端(协议端)提供,
实现端本身不负责处理响应逻辑将消息按照OneBot标准处理好上报给轻雪
你需要使用Onebot标准的实现端来连接到轻雪并将消息上报给轻雪下面已经列出一些推荐的实现端
- `Playwright`安装失败
- 输入`playwright install`安装浏览器
- 有的插件安装后报错无法启动
- 请先查阅插件文档,确认插件必要配置项完好后,仍然出现问题,请联系插件作者或在安全模式`safe_mode: true`下启动轻雪,在安全模式下你可以使用`npm uninstall`卸载问题插件
- 其他问题
-
加入QQ群[775840726](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=SzmDYbfR6jY94o9KFNon7AwelRyI6M_u&authKey=ygeBdEmdFNyCWuNR4w0M1M8%2B5oDg7k%2FDfN0tzBkYcnbB%2FGHNnlVEnCIGbdftsnn7&noverify=0&group_code=775840726)
## **推荐方案(QQ)**
1. [Lagrange.OneBot](https://github.com/KonataDev/Lagrange.Core)基于NTQQ的OneBot实现目前Markdown消息支持Lagrange
2. [LLOneBot](https://github.com/LLOneBot/LLOneBot)NTQQ的OneBot插件需要安装NTQQ
3. [OpenShamrock](https://github.com/whitechi73/OpenShamrock)基于Lsposed的OneBot11实现
4. [TRSS-Yunzai](https://github.com/TimeRainStarSky/Yunzai),基于`node.js`,可使用`ws-plugin`进行通信
5. [go-cqhttp](https://github.com/Mrs4s/go-cqhttp)`go`语言实现的OneBot11实现端目前可用性较低
6. [Gensokyo](https://github.com/Hoshinonyaruko/Gensokyo),基于 OneBot QQ官方机器人Api Golang 原生实现,需要官方机器人权限
7. 人工实现的`Onebot`协议自己整一个WebSocket客户端看着QQ的消息然后给轻雪传输数据
## **推荐方案(Minecraft)**
1. [MinecraftOneBot](https://github.com/snowykami/MinecraftOnebot)我们专门为Minecraft开发的服务器Bot支持OneBotV11标准
使用其他项目连接请先自行查阅文档若有困难请联系对应开发者而不是Liteyuki的开发者
## **鸣谢**
- [Nonebot2](https://nonebot.dev)提供的框架支持
- [nonebot-plugin-htmlrender](https://github.com/kexue-z/nonebot-plugin-htmlrender/tree/master)提供的渲染功能
- [nonebot-plugin-alconna](https://github.com/ArcletProject/nonebot-plugin-alconna)提供的命令解析功能
- [MiSans](https://hyperos.mi.com/font/zh/)[MapleMono](https://gitee.com/mirrors/Maple-Mono)提供的字体,且遵守了相关字体开源协议

View File

@ -0,0 +1,50 @@
---
title: 安装
icon: download
order: 1
category: 使用指南
tag:
- 安装
---
## **开始安装**
### **常规方法**
1. 安装 [`Git`](https://git-scm.com/download/) 和 [`Python3.10+`](https://www.python.org/downloads/release/python-31010/) 环境
2. 克隆项目 `git clone https://github.com/snowykami/LiteyukiBot`
3. 进入轻雪目录 `cd LiteyukiBot`
4. 安装依赖 `pip install -r requirements.txt`
5. 启动 `python main.py`
### **使用Docker(测试中)**
1. 安装 [`Docker`](https://docs.docker.com/get-docker/)
2. 克隆项目 `git clone https://github.com/snowykami/LiteyukiBot`
3. 进入轻雪目录 `cd LiteyukiBot`
4. 构建镜像 `docker build -t liteyukibot .`
5. 启动容器 `docker run -p 20216:20216 -v $(pwd):/liteyukibot -v $(pwd)/.cache:/root/.cache liteyukibot`
> [!tip]
> Windows请使用项目绝对目录`/path/to/LiteyukiBot`代替`$(pwd)` <br>
> 若你修改了端口号请将`20216:20216`中的`20216`替换为你的端口号
## **设备要求**
- Windows系统版本最低`Windows10+`/`Windows Server 2019+`
- Linux系统要支持Python3.10+,推荐`Ubuntu 20.04+`(~~别用你那b CentOS~~)
- CPU: 至少`1vCPU`
- 内存: Bot无其他插件会占用`200~300MB`,其他插件占用视具体插件而定,建议`1GB`以上
- 硬盘: 至少`1GB`空间
> [!warning]
> 如果设备上有多个环境,请使用`path/to/python -m pip install -r requirements.txt`来安装依赖,`path/to/python`为你的Python可执行文件路径
> [!tip]
> 推荐使用虚拟环境来运行轻雪,以避免依赖冲突,你可以使用`python -m venv venv`来创建虚拟环境,然后使用`venv\Scripts\activate`来激活虚拟环境
> [!warning]
> 轻雪的更新功能依赖Git如果你没有安装Git你将无法使用更新功能
#### 其他问题请移步至[答疑](/deployment/fandq)
[//]: # (#### 想在Linux命令行中拥有更好的体验试试[TRSS_Liteyuki轻雪机器人管理脚本]&#40;https://timerainstarsky.github.io/TRSS_Liteyuki/&#41;该功能仅供参考不是LiteyukiBot官方提供的功能)

106
docs/foolsday.md Normal file
View File

@ -0,0 +1,106 @@
---
home: true
icon: home
title: 首页
heroImage: https://cdn.liteyuki.icu/static/img/logo.png
bgImage:
bgImageDark:
bgImageStyle:
background-attachment: fixed
heroText: HeavyyukiBot666 # LiteyukiBot 6
tagline: 重雪机器人一个以笨重和复杂为设计理念基于Koishi114514的OneBotv1919810标准聊天机器人可用于雪地清扫使用Typethon编写
#tagline: 轻雪机器人一个以轻量和简洁为设计理念基于Nonebot2的OneBot标准聊天机器人
actions:
- text: 快速结束 # 快速开始
icon: lightbulb
link: ./deployment/install.html
type: primary
- text: 奇怪的册子 # 使用手册
icon: book
link: ./usage/basic_command.html
#1. 安装 `Git` 和 `Python3.10+` 环境
#2. 克隆项目 `git clone https://github.com/snowykami/LiteyukiBot` (无法连接可以用`https://gitee.com/snowykami/LiteyukiBot`)
#3. 切换目录`cd LiteyukiBot`
#4. 安装依赖`pip install -r requirements.txt`(如果多个Python环境请指定后安装`pythonx -m pip install -r requirements.txt`)
#5. 启动`python main.py`
highlights:
- header: 简洁至下 # 简洁至上
image: /assets/image/layout.svg
bgImage: https://theme-hope-assets.vuejs.press/bg/2-light.svg
bgImageDark: https://theme-hope-assets.vuejs.press/bg/2-dark.svg
bgImageStyle:
background-repeat: repeat
background-size: initial
features:
- title: 基于Koishi.js233
icon: robot
details: 拥有良好的生态支持
link: https://nonebot.dev/
- title: 盲目插件管理
icon: plug
details: 基于nbshi使用<code>npmx和pip</code>,让你无法安装/卸载插件
- title: 点击无法交互
icon: mouse-pointer
details: 老的的点击交互模式,必须手打指令
- title: 猪蹄支持
icon: paint-brush
details: 支持多种主题,可自定义资源包,满足你的审美需求
- title: 非国际化
icon: globe
details: 支持多种语言包括i18n部分语言和自行扩展的语言代码
link: https://baike.baidu.com/item/i18n/6771940
- title: 超难配置
icon: cog
details: 无需过多配置,开箱即用
link: https://bot.liteyuki.icu/deployment/config.html
- title: 高占用
icon: memory
details: 使用更多的意义不明的依赖和资源
- title: 一个Bot标准
icon: link
details: 支持OneBotv11/12标准的四种通信协议
link: https://onebot.dev/
- title: Alconna
icon: link
details: 使用Alconna实现低效命令解析
link: https://github.com/nonebot/plugin-alconna
- title: 不准更新
icon: cloud-download
details: 要更新自己下新版本去
- title: 服务支持
icon: server
details: 内置轻雪API但随时可能跑路
- title: 闭源
icon: code
details: 要源代码自己逆向去
- header: 快速部署
image: /assets/image/box.svg
bgImage: https://theme-hope-assets.vuejs.press/bg/3-light.svg
bgImageDark: https://theme-hope-assets.vuejs.press/bg/3-dark.svg
highlights:
- title: 安装 Git 和 node.js+
- title: 使用 <code>git clone https://github.com/snowykami/LiteyukiBot</code> 以克隆项目至本地。
details: 如果无法连接到GitHub可以使用 <code>git clone https://gitee.com/snowykami/LiteyukiBot</code>
- title: 使用 <code>cd LiteyukiBot</code> 切换到项目目录。
- title: 使用 <code>npm install -r requirements.txt</code> 安装项目依赖。
details: 如果你有多个 node.js 环境,请使用 <code>pythonx -m npm install -r requirements.txt</code>
- title: 使用 <code>node main.py</code> 启动项目。
copyright: © 2021-2024 SnowyKami All Rights Reserved
---

26
docs/package.json Normal file
View File

@ -0,0 +1,26 @@
{
"name": "liteyuki",
"version": "2.0.0",
"description": "A project of vuepress-theme-hope",
"license": "MIT",
"type": "module",
"scripts": {
"docs:build": "vuepress-vite build .",
"docs:clean-dev": "vuepress-vite dev . --clean-cache",
"docs:dev": "vuepress-vite dev .",
"docs:update-package": "pnpm dlx vp-update"
},
"devDependencies": {
"@vuepress/bundler-vite": "2.0.0-rc.9",
"@vuepress/plugin-search": "2.0.0-rc.24",
"vue": "^3.4.21",
"vuepress": "2.0.0-rc.9",
"vuepress-plugin-search-pro": "2.0.0-rc.34",
"vuepress-theme-hope": "2.0.0-rc.32"
},
"dependencies": {
"clipboard": "^2.0.11",
"element-plus": "^2.7.0",
"element-ui": "^2.15.14"
}
}

3180
docs/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

5
docs/store/README.md Normal file
View File

@ -0,0 +1,5 @@
---
title: 资源商店
icon: store
index: false
---

8
docs/store/plugin.md Normal file
View File

@ -0,0 +1,8 @@
---
title: 插件商店
icon: plug
order: 2
category: 使用手册
---
<pluginStoreComp />

7
docs/store/resource.md Normal file
View File

@ -0,0 +1,7 @@
---
title: 资源商店
icon: box
order: 1
category: 使用手册
---
<resourceStoreComp />

15
docs/tsconfig.json Normal file
View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"allowSyntheticDefaultImports": true
},
"include": [
"./.vuepress/**/*.ts",
"./.vuepress/**/*.vue"
],
"exclude": [
"node_modules"
]
}

8
docs/usage/README.md Normal file
View File

@ -0,0 +1,8 @@
---
title: 使用手册
index: false
icon: laptop-code
category: 使用手册
---
<Catalog />

16
docs/usage/agreement.md Normal file
View File

@ -0,0 +1,16 @@
---
title: 用户协议
icon: user-secret
order: 3
category: 使用手册
---
1. 本项目遵循`MIT`协议,你可以自由使用,修改,分发,但是请保留原作者信息
2. 你可以选择开启`auto_report`(默认开启),轻雪会收集以下内容
- 运行环境的设备信息CPU内存系统信息Python信息
- 插件信息(不含插件数据)
- 部分异常信息,
- 会话负载信息(不含隐私部分)
以上内容仅用于项目的优化,不包含任何隐私信息,且通过安全的方式传输到轻雪的服务器,若你不希望提供这些信息,可以在配置文件中把`auto_report`设定为`false`
3. 本项目不会收集用户的任何隐私信息,但请注意甄别第三方插件的安全性
4. 使用此项目代表你已经同意以上协议

102
docs/usage/basic_command.md Normal file
View File

@ -0,0 +1,102 @@
---
title: 基础命令
icon: comment
order: 1
category: 使用手册
---
## 基础插件
### **轻雪 `liteyuki`**
```shell
# 仅超级用户
reload-liteyuki # 重载轻雪
update-liteyuki # 更新轻雪
liteecho # 查看当前bot
status # 查看统计信息和状态
config set <key> value # 添加配置项,若存在则会覆盖,输入值会被执行以转换为正确的类型,"10"和10是不一样的
config get [key] # 查询配置项不带key返回配置项列表推荐私聊使用
switch-image-mode # 在普通图片和Markdown大图之间切换该功能需要commit:505468b及以后的Lagrange.OneBot
/api api_name [args] # 调用机器人API例如/api get_group_member_list group_id=1234567空格用%20
# 仅超级用户,群聊仅群主、管理员、超级用户可用
group enable/disable [group_id] # 在群聊启用/停用机器人group_id仅超级用户可用
# 所有人可用
liteyuki-docs # 查看轻雪文档
```
命令别名
```shell
status 状态,
reload-liteyuki 重启轻雪,
update-liteyuki 更新轻雪,
reload-resources 重载资源,
config 配置 | set 设置 | get 查询,
switch-image-mode 切换图片模式,
liteyuki-docs 轻雪文档
group 群聊 | enable 启用 | disable 停用
```
### **插件/包管理器 `liteyuki_pacman`**
- 插件管理
```shell
# 仅超级用户
npm update # 更新插件商店索引
npm install <plugin_name> # 安装插件
npm uninstall <plugin_name> # 卸载插件
npm search <keywords...> # 通过关键词搜索插件
npm enable-global/disable-global <plugin_name> # 全局启用/停用插件
# 群聊仅群主、管理员、超级用户可用,私聊所有人可用
npm enable/disable <plugin_name> # 当前会话启用/停用插件
npm list [page] [num] # 列出所有插件 page为页数num为每页显示数量
# 所有人
help <plugin_name> # 查看插件帮助
```
- 资源包管理
```shell
# 仅超级用户
rpm list [page] [num] # 列出所有资源包 page为页数num为每页显示数量
rpm load <pack_name> # 加载资源包
rpm unload <pack_name> # 卸载资源包
rpm change <pack_name> # 修改优先级
rpm reload # 重载所有资源包
```
命令别名
```shell
npm 插件管理 | update 更新 | install 安装 | uninstall 卸载 | search 搜索
enable 启用 | disable 停用 | enable-global 全局启用 | disable-global 全局停用
rpm 资源包 | load 加载 | unload 卸载 | change 更改 | reload 重载 | list 列表
help 帮助
```
> [!warning]
> 受限于NoneBot2钩子函数的依赖注入参数插件停用只能阻断传入响应对于主动推送的插件不生效请阅读插件主页的说明。
### **用户管理`liteyuki_user`**
```shell
# 所有人可用
profile # 查看用户信息菜单
profile set <key> [value] # 设置用户信息或打开属性设置菜单
profile get <key> # 获取用户信息
```
命令别名
```shell
profile 个人信息 | set 设置 | get 查询
```
> [!tip]
> **参数**`<param>`为必填参数,`[option]`为可选参数。
>
> **命令别名**:配置了命令别名的命令可以使用别名代替原命令,例如`npm install ~`可以使用`插件 安装 ~`代替。

View File

@ -0,0 +1,29 @@
---
title: 功能命令
icon: comment
order: 2
category: 使用手册
---
## 功能插件命令
### **轻雪天气`liteyuki_weather`**
配置项
```yaml
weather_key: "" # 和风天气的天气key会自动判断key版本
```
命令
```shell
weather <keywords...> # 查询目标地实时天气,例如:"天气 北京 海淀", "weather Tokyo Shinjuku"
bind-city <keywords...> # 绑定查询城市,个人全局生效
```
命令别名
```shell
weather 天气, bind-city 绑定城市
```

45
docs/usage/lyapi.md Normal file
View File

@ -0,0 +1,45 @@
---
title: 轻雪API
icon: code
order: 4
category: 使用指南
tag:
- 配置
- 部署
---
## **轻雪API**
轻雪API是轻雪运行中部分服务的支持`go`语言编写,例如错误反馈,图床链接等,目前服务由轻雪服务器提供,用户无需额外部署
接口
- `url` `https://api.liteyuki.icu`
- `POST` `/register` 注册一个Bot
- 参数
- `name` `string` Bot名称
- `version` `string` Bot版本
- `version_id` `int` Bot版本ID
- `python` `string` Python版本
- `os` `string` 操作系统
- 返回
- `code` `int` 状态码
- `liteyuki_id` `string` 轻雪ID
- `POST` `/bug_report` 上报错误
- 参数
- `liteyuki_id` `string` 轻雪ID
- `content` `string` 错误信息
- `device_info` `string` 设备信息
- 返回
- `code` `int` 状态码
- `report_id` `string` 错误ID
- `POST` `/upload_image` 图床上传
- 参数
- `image` `file` 图片文件
- `liteyuki_id` `string` 轻雪ID,用于鉴权
- 返回
- `code` `int` 状态码
- `url` `string` 图床链接

View File

@ -0,0 +1,39 @@
---
title: 资源包
icon: paint-brush
order: 3
category: 使用手册
---
## 简介
资源包,亦可根据用途称为主题包、字体包、语言包等,它允许你一定程度上自定义轻雪的外观,并且不用修改源代码
- [资源/主题商店](/store/)提供了一些资源包供你选择,你也可以自己制作资源包
- 资源包的制作很简单,如果你接触过`Minecraft`的资源包,那么你能够很快就上手,仅需按照原有路径进行文件替换即可,讲起打包成一个新的资源包。
- 部分内容制作需要一点点前端基础,例如`html``css`
- 轻雪原版资源包请查看`LiteyukiBot/liteyuki/resources`,可以在此基础上进行修改
- 欢迎各位投稿资源包到轻雪资源商店
## 加载资源包
- 资源包通常是以`.zip`格式压缩的,只需要将其解压到根目录`resources`目录下即可,注意不要嵌套文件夹,正常的路径应该是这样的
```shell
main.py
resources
└─resource_pack_1
├─metadata.yml
├─templates
└───...
└─resource_pack_2
├─metadata.yml
└─...
```
- 你自己制作的资源包也应该遵循这个规则,并且应该在`metadata.yml`中填写一些信息
- 若没有`metadata.yml`文件,则该文件夹不会被识别为资源包
```yaml
name: "资源包名称"
version: "1.0.0"
description: "资源包描述"
# 你可以自定义一些信息,但请保证以上三个字段
...
```
- 资源包加载遵循一个优先级即后加载的资源包会覆盖前面的资源包例如你在A包中定义了一个`index.html`文件B包也定义了一个`index.html`文件那么加载B包后A包中的`index.html`文件会被覆盖
- 对于不同资源包的不同文件是可以相对引用的例如你在A中定义了`templates/index.html`在B中定义了`templates/style.css`可以在A的`index.html`中用`./style.css`相对路径引用B中的css

View File

@ -0,0 +1,35 @@
from nonebot.plugin import PluginMetadata
from .core import *
from .loader import *
from .dev_tools import *
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="轻雪核心插件",
description="轻雪主程序插件,包含了许多初始化的功能",
usage="",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable": False,
}
)
from ..utils.base.language import Language, get_default_lang_code
print("\033[34m" + r"""
__ ______ ________ ________ __ __ __ __ __ __ ______
/ | / |/ |/ |/ \ / |/ | / |/ | / |/ |
$$ | $$$$$$/ $$$$$$$$/ $$$$$$$$/ $$ \ /$$/ $$ | $$ |$$ | /$$/ $$$$$$/
$$ | $$ | $$ | $$ |__ $$ \/$$/ $$ | $$ |$$ |/$$/ $$ |
$$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |$$ $$< $$ |
$$ | $$ | $$ | $$$$$/ $$$$/ $$ | $$ |$$$$$ \ $$ |
$$ |_____ _$$ |_ $$ | $$ |_____ $$ | $$ \__$$ |$$ |$$ \ _$$ |_
$$ |/ $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |/ $$ |
$$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
""" + "\033[0m")
sys_lang = Language(get_default_lang_code())
nonebot.logger.info(sys_lang.get("main.current_language", LANG=sys_lang.get("language.name")))
# nonebot.logger.info(sys_lang.get("main.enable_webdash", URL=f"http://127.0.0.1:{config.get('port', 20216)}"))

View File

@ -0,0 +1,41 @@
import nonebot
from git import Repo
remote_urls = [
"https://github.com/snowykami/LiteyukiBot.git",
"https://gitee.com/snowykami/LiteyukiBot.git"
]
def detect_update() -> bool:
# 对每个远程仓库进行检查只要有一个仓库有更新就返回True
for remote_url in remote_urls:
repo = Repo(".")
repo.remotes.origin.set_url(remote_url)
repo.remotes.origin.fetch()
if repo.head.commit != repo.commit('origin/main'):
return True
def update_liteyuki() -> tuple[bool, str]:
"""更新轻雪
:return: 是否更新成功,更新变动"""
new_commit_detected = detect_update()
if new_commit_detected:
repo = Repo(".")
logs = ""
# 对每个远程仓库进行更新
for remote_url in remote_urls:
try:
logs += f"\nremote: {remote_url}"
repo.remotes.origin.set_url(remote_url)
repo.remotes.origin.pull()
diffs = repo.head.commit.diff("origin/main")
for diff in diffs.iter_change_type('M'):
logs += f"\n{diff.a_path}"
return True, logs
except:
continue
else:
return False, "Nothing Changed"

View File

@ -0,0 +1,324 @@
import base64
import time
from typing import Any
import nonebot
import pip
from nonebot import Bot, get_driver, require
from nonebot.adapters.onebot.v11 import Message, escape, unescape
from nonebot.exception import MockApiException
from nonebot.internal.matcher import Matcher
from nonebot.permission import SUPERUSER
from liteyuki.utils.base.config import get_config, load_from_yaml
from liteyuki.utils.base.data_manager import StoredConfig, TempConfig, common_db
from liteyuki.utils.base.language import get_user_lang
from liteyuki.utils.base.ly_typing import T_Bot, T_MessageEvent
from liteyuki.utils.message.message import MarkdownMessage as md, broadcast_to_superusers
from liteyuki.utils.base.reloader import Reloader
from .api import update_liteyuki
require("nonebot_plugin_alconna")
require("nonebot_plugin_apscheduler")
from nonebot_plugin_alconna import UniMessage, on_alconna, Alconna, Args, Subcommand, Arparma, MultiVar
from nonebot_plugin_apscheduler import scheduler
driver = get_driver()
markdown_image = common_db.first(StoredConfig(), default=StoredConfig()).config.get("markdown_image", False)
@on_alconna(
command=Alconna(
"liteecho",
Args["text", str, ""],
),
permission=SUPERUSER
).handle()
async def _(bot: T_Bot, matcher: Matcher, result: Arparma):
if result.main_args.get("text"):
await matcher.finish(Message(unescape(result.main_args.get("text"))))
else:
await matcher.finish(f"Hello, Liteyuki!\nBot {bot.self_id}")
@on_alconna(
aliases={"更新轻雪"},
command=Alconna(
"update-liteyuki"
),
permission=SUPERUSER
).handle()
async def _(bot: T_Bot, event: T_MessageEvent):
# 使用git pull更新
ulang = get_user_lang(str(event.user_id))
success, logs = update_liteyuki()
reply = "Liteyuki updated!\n"
reply += f"```\n{logs}\n```\n"
btn_restart = md.btn_cmd(ulang.get("liteyuki.restart_now"), "reload-liteyuki")
pip.main(["install", "-r", "requirements.txt"])
reply += f"{ulang.get('liteyuki.update_restart', RESTART=btn_restart)}"
await md.send_md(reply, bot, event=event, at_sender=False)
@on_alconna(
aliases={"重启轻雪"},
command=Alconna(
"reload-liteyuki"
),
permission=SUPERUSER
).handle()
async def _(matcher: Matcher, bot: T_Bot, event: T_MessageEvent):
await matcher.send("Liteyuki reloading")
temp_data = common_db.first(TempConfig(), default=TempConfig())
temp_data.data.update(
{
"reload" : True,
"reload_time" : time.time(),
"reload_bot_id" : bot.self_id,
"reload_session_type": event.message_type,
"reload_session_id" : event.group_id if event.message_type == "group" else event.user_id,
"delta_time" : 0
}
)
common_db.save(temp_data)
Reloader.reload(0)
@on_alconna(
aliases={"配置"},
command=Alconna(
"config",
Subcommand(
"set",
Args["key", str]["value", Any],
alias=["设置"],
),
Subcommand(
"get",
Args["key", str, None],
alias=["查询", "获取"]
),
Subcommand(
"remove",
Args["key", str],
alias=["删除"]
)
),
permission=SUPERUSER
).handle()
async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, matcher: Matcher):
ulang = get_user_lang(str(event.user_id))
stored_config: StoredConfig = common_db.first(StoredConfig(), default=StoredConfig())
if result.subcommands.get("set"):
key, value = result.subcommands.get("set").args.get("key"), result.subcommands.get("set").args.get("value")
try:
value = eval(value)
except:
pass
stored_config.config[key] = value
common_db.save(stored_config)
await matcher.finish(f"{ulang.get('liteyuki.config_set_success', KEY=key, VAL=value)}")
elif result.subcommands.get("get"):
key = result.subcommands.get("get").args.get("key")
file_config = load_from_yaml("config.yml")
reply = f"{ulang.get('liteyuki.current_config')}"
if key:
reply += f"```dotenv\n{key}={file_config.get(key, stored_config.config.get(key))}\n```"
else:
reply = f"{ulang.get('liteyuki.current_config')}"
reply += f"\n{ulang.get('liteyuki.static_config')}\n```dotenv"
for k, v in file_config.items():
reply += f"\n{k}={v}"
reply += "\n```"
if len(stored_config.config) > 0:
reply += f"\n{ulang.get('liteyuki.stored_config')}\n```dotenv"
for k, v in stored_config.config.items():
reply += f"\n{k}={v} {type(v)}"
reply += "\n```"
await md.send_md(reply, bot, event=event)
elif result.subcommands.get("remove"):
key = result.subcommands.get("remove").args.get("key")
if key in stored_config.config:
stored_config.config.pop(key)
common_db.save(stored_config)
await matcher.finish(f"{ulang.get('liteyuki.config_remove_success', KEY=key)}")
else:
await matcher.finish(f"{ulang.get('liteyuki.invalid_command', TEXT=key)}")
@on_alconna(
aliases={"切换图片模式"},
command=Alconna(
"switch-image-mode"
),
permission=SUPERUSER
).handle()
async def _(event: T_MessageEvent, matcher: Matcher):
global markdown_image
# 切换图片模式False以图片形式发送True以markdown形式发送
ulang = get_user_lang(str(event.user_id))
stored_config: StoredConfig = common_db.first(StoredConfig(), default=StoredConfig())
stored_config.config["markdown_image"] = not stored_config.config.get("markdown_image", False)
markdown_image = stored_config.config["markdown_image"]
common_db.save(stored_config)
await matcher.finish(ulang.get("liteyuki.image_mode_on" if stored_config.config["markdown_image"] else "liteyuki.image_mode_off"))
@on_alconna(
command=Alconna(
"liteyuki-docs",
),
aliases={"轻雪文档"},
).handle()
async def _(matcher: Matcher):
await matcher.finish("https://bot.liteyuki.icu/usage")
@on_alconna(
command=Alconna(
"/api",
Args["api", str]["args", MultiVar(str), ()],
),
permission=SUPERUSER
).handle()
async def _(result: Arparma, bot: T_Bot, event: T_MessageEvent, matcher: Matcher):
"""
调用API
Args:
result:
bot:
event:
Returns:
"""
api_name = result.main_args.get("api")
args: tuple[str] = result.main_args.get("args", ()) # 类似于url参数但每个参数间用空格分隔空格是%20
args_dict = {}
for arg in args:
key, value = arg.split("=", 1)
args_dict[key] = unescape(value.replace("%20", " "))
if api_name in need_user_id and "user_id" not in args_dict:
args_dict["user_id"] = str(event.user_id)
if api_name in need_group_id and "group_id" not in args_dict and event.message_type == "group":
args_dict["group_id"] = str(event.group_id)
if "message" in args_dict:
args_dict["message"] = Message(args_dict["message"])
try:
result = await bot.call_api(api_name, **args_dict)
except Exception as e:
result = str(e)
args_show = "\n".join("- %s: %s" % (k, v) for k, v in args_dict.items())
print(f"API: {api_name}\n\nArgs: \n{args_show}\n\nResult: {result}")
await matcher.finish(f"API: {api_name}\n\nArgs: \n{args_show}\n\nResult: {result}")
# system hook
@Bot.on_calling_api # 图片模式检测
async def test_for_md_image(bot: T_Bot, api: str, data: dict):
# 截获大图发送转换为markdown发送
if api in ["send_msg", "send_private_msg", "send_group_msg"] and markdown_image and data.get("user_id") != bot.self_id:
if api == "send_msg" and data.get("message_type") == "private" or api == "send_private_msg":
session_type = "private"
session_id = data.get("user_id")
elif api == "send_msg" and data.get("message_type") == "group" or api == "send_group_msg":
session_type = "group"
session_id = data.get("group_id")
else:
return
if len(data.get("message", [])) == 1 and data["message"][0].get("type") == "image":
file: str = data["message"][0].data.get("file")
# file:// http:// base64://
if file.startswith("http"):
result = await md.send_md(await md.image_async(file), bot, message_type=session_type, session_id=session_id)
elif file.startswith("file"):
file = file.replace("file://", "")
result = await md.send_image(open(file, "rb").read(), bot, message_type=session_type, session_id=session_id)
elif file.startswith("base64"):
file_bytes = base64.b64decode(file.replace("base64://", ""))
result = await md.send_image(file_bytes, bot, message_type=session_type, session_id=session_id)
else:
return
raise MockApiException(result=result)
@driver.on_startup
async def on_startup():
temp_data = common_db.first(TempConfig(), default=TempConfig())
# 储存重启信息
if temp_data.data.get("reload", False):
delta_time = time.time() - temp_data.data.get("reload_time", 0)
temp_data.data["delta_time"] = delta_time
common_db.save(temp_data) # 更新数据
@driver.on_shutdown
async def on_shutdown():
pass
@driver.on_bot_connect
async def _(bot: T_Bot):
temp_data = common_db.first(TempConfig(), default=TempConfig())
# 用于重启计时
if temp_data.data.get("reload", False):
temp_data.data["reload"] = False
reload_bot_id = temp_data.data.get("reload_bot_id", 0)
if reload_bot_id != bot.self_id:
return
reload_session_type = temp_data.data.get("reload_session_type", "private")
reload_session_id = temp_data.data.get("reload_session_id", 0)
delta_time = temp_data.data.get("delta_time", 0)
common_db.save(temp_data) # 更新数据
await bot.call_api(
"send_msg",
message_type=reload_session_type,
user_id=reload_session_id,
group_id=reload_session_id,
message="Liteyuki reloaded in %.2f s" % delta_time
)
# 每天4点更新
@scheduler.scheduled_job("cron", hour=4)
async def every_day_update():
if get_config("auto_update", default=True):
result, logs = update_liteyuki()
pip.main(["install", "-r", "requirements.txt"])
if result:
await broadcast_to_superusers(f"Liteyuki updated: ```\n{logs}\n```")
nonebot.logger.info(f"Liteyuki updated: {logs}")
Reloader.reload(5)
else:
nonebot.logger.info(logs)
# 安全的需要用户id的api
need_user_id = (
"send_private_msg",
"send_msg",
"set_group_card",
"set_group_special_title",
"get_stranger_info",
"get_group_member_info"
)
need_group_id = (
"send_group_msg",
"send_msg",
"set_group_card",
"set_group_name",
"set_group_special_title",
"get_group_member_info",
"get_group_member_list",
"get_group_honor_info"
)

View File

@ -0,0 +1,43 @@
import time
import nonebot
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from liteyuki.utils.base.config import get_config
from liteyuki.utils.base.reloader import Reloader
from liteyuki.utils.base.resource import load_resources
if get_config("debug", False):
nonebot.logger.info("Liteyuki Reload is enable, watching for file changes...")
class CodeModifiedHandler(FileSystemEventHandler):
def on_modified(self, event):
if "liteyuki/resources" not in event.src_path.replace("\\", "/"):
nonebot.logger.debug(f"{event.src_path} has been modified, reloading bot...")
Reloader.reload()
class ResourceModifiedHandler(FileSystemEventHandler):
def on_modified(self, event):
if not event.is_directory:
nonebot.logger.debug(f"{event.src_path} has been modified, reloading resource...")
load_resources()
code_modified_handler = CodeModifiedHandler()
resource_modified_handle = ResourceModifiedHandler()
observer = Observer()
observer.schedule(resource_modified_handle, path="liteyuki/resources", recursive=True)
observer.schedule(resource_modified_handle, path="resources", recursive=True)
observer.schedule(code_modified_handler, path="liteyuki", recursive=True)
observer.start()
# try:
# while True:
# time.sleep(1)
# except KeyboardInterrupt:
# observer.stop()
# observer.join()

View File

@ -0,0 +1,27 @@
import nonebot.plugin
from liteyuki.utils import init_log
from liteyuki.utils.base.config import get_config
from liteyuki.utils.base.data_manager import InstalledPlugin, plugin_db
from liteyuki.utils.base.resource import load_resources
from liteyuki.utils.message.tools import check_for_package
load_resources()
init_log()
nonebot.plugin.load_plugins("liteyuki/plugins")
# 从数据库读取已安装的插件
if not get_config("safe_mode", False):
# 安全模式下,不加载插件
installed_plugins: list[InstalledPlugin] = plugin_db.all(InstalledPlugin())
if installed_plugins:
for installed_plugin in installed_plugins:
if not check_for_package(installed_plugin.module_name):
nonebot.logger.error(f"{installed_plugin.module_name} not installed, but in loading database. please run `npm fixup` in chat to reinstall it.")
else:
nonebot.load_plugin(installed_plugin.module_name)
nonebot.plugin.load_plugins("plugins")
else:
nonebot.logger.info("Safe mode is on, no plugin loaded.")

View File

@ -0,0 +1,15 @@
from nonebot.plugin import PluginMetadata
from .rt_guide import *
__plugin_meta__ = PluginMetadata(
name="CRT生成工具",
description="一些CRT牌子生成器",
usage="我觉得你应该会用",
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable" : True,
"default_enable": True,
}
)

View File

@ -0,0 +1,575 @@
import os
import uuid
from typing import Tuple, Union, List
import nonebot
from PIL import Image, ImageFont, ImageDraw
default_color = (255, 255, 255, 255)
default_font = "resources/fonts/MiSans-Semibold.ttf"
def render_canvas_from_json(file: str, background: Image) -> "Canvas":
pass
class BasePanel:
def __init__(self,
uv_size: Tuple[Union[int, float], Union[int, float]] = (1.0, 1.0),
box_size: Tuple[Union[int, float], Union[int, float]] = (1.0, 1.0),
parent_point: Tuple[float, float] = (0.5, 0.5),
point: Tuple[float, float] = (0.5, 0.5)):
"""
:param uv_size: 底面板大小
:param box_size: 子(自身)面板大小
:param parent_point: 底面板锚点
:param point: 子(自身)面板锚点
"""
self.canvas: Canvas | None = None
self.uv_size = uv_size
self.box_size = box_size
self.parent_point = parent_point
self.point = point
self.parent: BasePanel | None = None
self.canvas_box: Tuple[float, float, float, float] = (0.0, 0.0, 1.0, 1.0)
# 此节点在父节点上的盒子
self.box = (
self.parent_point[0] - self.point[0] * self.box_size[0] / self.uv_size[0],
self.parent_point[1] - self.point[1] * self.box_size[1] / self.uv_size[1],
self.parent_point[0] + (1 - self.point[0]) * self.box_size[0] / self.uv_size[0],
self.parent_point[1] + (1 - self.point[1]) * self.box_size[1] / self.uv_size[1]
)
def load(self, only_calculate=False):
"""
将对象写入画布
此处仅作声明
由各子类重写
:return:
"""
self.actual_pos = self.canvas_box
def save_as(self, canvas_box, only_calculate=False):
"""
此函数执行时间较长,建议异步运行
:param only_calculate:
:param canvas_box 此节点在画布上的盒子,并不是在父节点上的盒子
:return:
"""
for name, child in self.__dict__.items():
# 此节点在画布上的盒子
if isinstance(child, BasePanel) and name not in ["canvas", "parent"]:
child.parent = self
if isinstance(self, Canvas):
child.canvas = self
else:
child.canvas = self.canvas
dxc = canvas_box[2] - canvas_box[0]
dyc = canvas_box[3] - canvas_box[1]
child.canvas_box = (
canvas_box[0] + dxc * child.box[0],
canvas_box[1] + dyc * child.box[1],
canvas_box[0] + dxc * child.box[2],
canvas_box[1] + dyc * child.box[3]
)
child.load(only_calculate)
child.save_as(child.canvas_box, only_calculate)
class Canvas(BasePanel):
def __init__(self, base_img: Image.Image):
self.base_img = base_img
self.canvas = self
super(Canvas, self).__init__()
self.draw_line_list = []
def export(self, file, alpha=False):
self.base_img = self.base_img.convert("RGBA")
self.save_as((0, 0, 1, 1))
draw = ImageDraw.Draw(self.base_img)
for line in self.draw_line_list:
draw.line(*line)
if not alpha:
self.base_img = self.base_img.convert("RGB")
self.base_img.save(file)
def delete(self):
os.remove(self.file)
def get_actual_box(self, path: str) -> Union[None, Tuple[float, float, float, float]]:
"""
获取控件实际相对大小
函数执行时间较长
:param path: 控件路径
:return:
"""
sub_obj = self
self.save_as((0, 0, 1, 1), True)
control_path = ""
for i, seq in enumerate(path.split(".")):
if seq not in sub_obj.__dict__:
raise KeyError(f"{control_path}中找不到控件:{seq}")
control_path += f".{seq}"
sub_obj = sub_obj.__dict__[seq]
return sub_obj.actual_pos
def get_actual_pixel_size(self, path: str) -> Union[None, Tuple[int, int]]:
"""
获取控件实际像素长宽
函数执行时间较长
:param path: 控件路径
:return:
"""
sub_obj = self
self.save_as((0, 0, 1, 1), True)
control_path = ""
for i, seq in enumerate(path.split(".")):
if seq not in sub_obj.__dict__:
raise KeyError(f"{control_path}中找不到控件:{seq}")
control_path += f".{seq}"
sub_obj = sub_obj.__dict__[seq]
dx = int(sub_obj.canvas.base_img.size[0] * (sub_obj.actual_pos[2] - sub_obj.actual_pos[0]))
dy = int(sub_obj.canvas.base_img.size[1] * (sub_obj.actual_pos[3] - sub_obj.actual_pos[1]))
return dx, dy
def get_actual_pixel_box(self, path: str) -> Union[None, Tuple[int, int, int, int]]:
"""
获取控件实际像素大小盒子
函数执行时间较长
:param path: 控件路径
:return:
"""
sub_obj = self
self.save_as((0, 0, 1, 1), True)
control_path = ""
for i, seq in enumerate(path.split(".")):
if seq not in sub_obj.__dict__:
raise KeyError(f"{control_path}中找不到控件:{seq}")
control_path += f".{seq}"
sub_obj = sub_obj.__dict__[seq]
x1 = int(sub_obj.canvas.base_img.size[0] * sub_obj.actual_pos[0])
y1 = int(sub_obj.canvas.base_img.size[1] * sub_obj.actual_pos[1])
x2 = int(sub_obj.canvas.base_img.size[2] * sub_obj.actual_pos[2])
y2 = int(sub_obj.canvas.base_img.size[3] * sub_obj.actual_pos[3])
return x1, y1, x2, y2
def get_parent_box(self, path: str) -> Union[None, Tuple[float, float, float, float]]:
"""
获取控件在父节点的大小
函数执行时间较长
:param path: 控件路径
:return:
"""
sub_obj = self.get_control_by_path(path)
on_parent_pos = (
(sub_obj.actual_pos[0] - sub_obj.parent.actual_pos[0]) / (sub_obj.parent.actual_pos[2] - sub_obj.parent.actual_pos[0]),
(sub_obj.actual_pos[1] - sub_obj.parent.actual_pos[1]) / (sub_obj.parent.actual_pos[3] - sub_obj.parent.actual_pos[1]),
(sub_obj.actual_pos[2] - sub_obj.parent.actual_pos[0]) / (sub_obj.parent.actual_pos[2] - sub_obj.parent.actual_pos[0]),
(sub_obj.actual_pos[3] - sub_obj.parent.actual_pos[1]) / (sub_obj.parent.actual_pos[3] - sub_obj.parent.actual_pos[1])
)
return on_parent_pos
def get_control_by_path(self, path: str) -> Union[BasePanel, "Img", "Rectangle", "Text"]:
sub_obj = self
self.save_as((0, 0, 1, 1), True)
control_path = ""
for i, seq in enumerate(path.split(".")):
if seq not in sub_obj.__dict__:
raise KeyError(f"{control_path}中找不到控件:{seq}")
control_path += f".{seq}"
sub_obj = sub_obj.__dict__[seq]
return sub_obj
def draw_line(self, path: str, p1: Tuple[float, float], p2: Tuple[float, float], color, width):
"""
画线
:param color:
:param width:
:param path:
:param p1:
:param p2:
:return:
"""
ac_pos = self.get_actual_box(path)
control = self.get_control_by_path(path)
dx = ac_pos[2] - ac_pos[0]
dy = ac_pos[3] - ac_pos[1]
xy_box = int((ac_pos[0] + dx * p1[0]) * control.canvas.base_img.size[0]), int((ac_pos[1] + dy * p1[1]) * control.canvas.base_img.size[1]), int(
(ac_pos[0] + dx * p2[0]) * control.canvas.base_img.size[0]), int((ac_pos[1] + dy * p2[1]) * control.canvas.base_img.size[1])
self.draw_line_list.append((xy_box, color, width))
class Panel(BasePanel):
def __init__(self, uv_size, box_size, parent_point, point):
super(Panel, self).__init__(uv_size, box_size, parent_point, point)
class TextSegment:
def __init__(self, text, **kwargs):
if not isinstance(text, str):
raise TypeError("请输入字符串")
self.text = text
self.color = kwargs.get("color", None)
self.font = kwargs.get("font", None)
@staticmethod
def text2text_segment_list(text: str):
"""
暂时没写好
:param text: %FFFFFFFF%1123%FFFFFFFF%21323
:return:
"""
pass
class Text(BasePanel):
def __init__(self, uv_size, box_size, parent_point, point, text: Union[str, list], font=default_font, color=(255, 255, 255, 255), vertical=False,
line_feed=False, force_size=False, fill=(0, 0, 0, 0), fillet=0, outline=(0, 0, 0, 0), outline_width=0, rectangle_side=0, font_size=None, dp: int = 5,
anchor: str = "la"):
"""
:param uv_size:
:param box_size:
:param parent_point:
:param point:
:param text: list[TextSegment] | str
:param font:
:param color:
:param vertical: 是否竖直
:param line_feed: 是否换行
:param force_size: 强制大小
:param dp: 字体大小递减精度
:param anchor : https://www.zhihu.com/question/474216280
:param fill: 底部填充颜色
:param fillet: 填充圆角
:param rectangle_side: 边框宽度
:param outline: 填充矩形边框颜色
:param outline_width: 填充矩形边框宽度
"""
self.actual_pos = None
self.outline_width = outline_width
self.outline = outline
self.fill = fill
self.fillet = fillet
self.font = font
self.text = text
self.color = color
self.force_size = force_size
self.vertical = vertical
self.line_feed = line_feed
self.dp = dp
self.font_size = font_size
self.rectangle_side = rectangle_side
self.anchor = anchor
super(Text, self).__init__(uv_size, box_size, parent_point, point)
def load(self, only_calculate=False):
"""限制区域像素大小"""
if isinstance(self.text, str):
self.text = [
TextSegment(text=self.text, color=self.color, font=self.font)
]
all_text = str()
for text in self.text:
all_text += text.text
limited_size = int((self.canvas_box[2] - self.canvas_box[0]) * self.canvas.base_img.size[0]), int((self.canvas_box[3] - self.canvas_box[1]) * self.canvas.base_img.size[1])
font_size = limited_size[1] if self.font_size is None else self.font_size
image_font = ImageFont.truetype(self.font, font_size)
actual_size = image_font.getsize(all_text)
while (actual_size[0] > limited_size[0] or actual_size[1] > limited_size[1]) and not self.force_size:
font_size -= self.dp
image_font = ImageFont.truetype(self.font, font_size)
actual_size = image_font.getsize(all_text)
draw = ImageDraw.Draw(self.canvas.base_img)
if isinstance(self.parent, Img) or isinstance(self.parent, Text):
self.parent.canvas_box = self.parent.actual_pos
dx0 = self.parent.canvas_box[2] - self.parent.canvas_box[0]
dy0 = self.parent.canvas_box[3] - self.parent.canvas_box[1]
dx1 = actual_size[0] / self.canvas.base_img.size[0]
dy1 = actual_size[1] / self.canvas.base_img.size[1]
start_point = [
int((self.parent.canvas_box[0] + dx0 * self.parent_point[0] - dx1 * self.point[0]) * self.canvas.base_img.size[0]),
int((self.parent.canvas_box[1] + dy0 * self.parent_point[1] - dy1 * self.point[1]) * self.canvas.base_img.size[1])
]
self.actual_pos = (
start_point[0] / self.canvas.base_img.size[0],
start_point[1] / self.canvas.base_img.size[1],
(start_point[0] + actual_size[0]) / self.canvas.base_img.size[0],
(start_point[1] + actual_size[1]) / self.canvas.base_img.size[1],
)
self.font_size = font_size
if not only_calculate:
for text_segment in self.text:
if text_segment.color is None:
text_segment.color = self.color
if text_segment.font is None:
text_segment.font = self.font
image_font = ImageFont.truetype(font=text_segment.font, size=font_size)
if self.fill[-1] > 0:
rectangle = Shape.rectangle(size=(actual_size[0] + 2 * self.rectangle_side, actual_size[1] + 2 * self.rectangle_side), fillet=self.fillet, fill=self.fill,
width=self.outline_width, outline=self.outline)
self.canvas.base_img.paste(im=rectangle, box=(start_point[0] - self.rectangle_side,
start_point[1] - self.rectangle_side,
start_point[0] + actual_size[0] + self.rectangle_side,
start_point[1] + actual_size[1] + self.rectangle_side),
mask=rectangle.split()[-1])
draw.text((start_point[0] - self.rectangle_side, start_point[1] - self.rectangle_side),
text_segment.text, text_segment.color, font=image_font, anchor=self.anchor)
text_width = image_font.getsize(text_segment.text)
start_point[0] += text_width[0]
class Img(BasePanel):
def __init__(self, uv_size, box_size, parent_point, point, img: Image.Image, keep_ratio=True):
self.img_base_img = img
self.keep_ratio = keep_ratio
super(Img, self).__init__(uv_size, box_size, parent_point, point)
def load(self, only_calculate=False):
self.preprocess()
self.img_base_img = self.img_base_img.convert("RGBA")
limited_size = int((self.canvas_box[2] - self.canvas_box[0]) * self.canvas.base_img.size[0]), \
int((self.canvas_box[3] - self.canvas_box[1]) * self.canvas.base_img.size[1])
if self.keep_ratio:
"""保持比例"""
actual_ratio = self.img_base_img.size[0] / self.img_base_img.size[1]
limited_ratio = limited_size[0] / limited_size[1]
if actual_ratio >= limited_ratio:
# 图片过长
self.img_base_img = self.img_base_img.resize(
(int(self.img_base_img.size[0] * limited_size[0] / self.img_base_img.size[0]),
int(self.img_base_img.size[1] * limited_size[0] / self.img_base_img.size[0]))
)
else:
self.img_base_img = self.img_base_img.resize(
(int(self.img_base_img.size[0] * limited_size[1] / self.img_base_img.size[1]),
int(self.img_base_img.size[1] * limited_size[1] / self.img_base_img.size[1]))
)
else:
"""不保持比例"""
self.img_base_img = self.img_base_img.resize(limited_size)
# 占比长度
if isinstance(self.parent, Img) or isinstance(self.parent, Text):
self.parent.canvas_box = self.parent.actual_pos
dx0 = self.parent.canvas_box[2] - self.parent.canvas_box[0]
dy0 = self.parent.canvas_box[3] - self.parent.canvas_box[1]
dx1 = self.img_base_img.size[0] / self.canvas.base_img.size[0]
dy1 = self.img_base_img.size[1] / self.canvas.base_img.size[1]
start_point = (
int((self.parent.canvas_box[0] + dx0 * self.parent_point[0] - dx1 * self.point[0]) * self.canvas.base_img.size[0]),
int((self.parent.canvas_box[1] + dy0 * self.parent_point[1] - dy1 * self.point[1]) * self.canvas.base_img.size[1])
)
alpha = self.img_base_img.split()[3]
self.actual_pos = (
start_point[0] / self.canvas.base_img.size[0],
start_point[1] / self.canvas.base_img.size[1],
(start_point[0] + self.img_base_img.size[0]) / self.canvas.base_img.size[0],
(start_point[1] + self.img_base_img.size[1]) / self.canvas.base_img.size[1],
)
if not only_calculate:
self.canvas.base_img.paste(self.img_base_img, start_point, alpha)
def preprocess(self):
pass
class Rectangle(Img):
def __init__(self, uv_size, box_size, parent_point, point, fillet: Union[int, float] = 0, img: Union[Image.Image] = None, keep_ratio=True,
color=default_color, outline_width=0, outline_color=default_color):
"""
圆角图
:param uv_size:
:param box_size:
:param parent_point:
:param point:
:param fillet: 圆角半径浮点或整数
:param img:
:param keep_ratio:
"""
self.fillet = fillet
self.color = color
self.outline_width = outline_width
self.outline_color = outline_color
super(Rectangle, self).__init__(uv_size, box_size, parent_point, point, img, keep_ratio)
def preprocess(self):
limited_size = (int(self.canvas.base_img.size[0] * (self.canvas_box[2] - self.canvas_box[0])),
int(self.canvas.base_img.size[1] * (self.canvas_box[3] - self.canvas_box[1])))
if not self.keep_ratio and self.img_base_img is not None and self.img_base_img.size[0] / self.img_base_img.size[1] != limited_size[0] / limited_size[1]:
self.img_base_img = self.img_base_img.resize(limited_size)
self.img_base_img = Shape.rectangle(size=limited_size, fillet=self.fillet, fill=self.color, width=self.outline_width, outline=self.outline_color)
class Color:
GREY = (128, 128, 128, 255)
RED = (255, 0, 0, 255)
GREEN = (0, 255, 0, 255)
BLUE = (0, 0, 255, 255)
YELLOW = (255, 255, 0, 255)
PURPLE = (255, 0, 255, 255)
CYAN = (0, 255, 255, 255)
WHITE = (255, 255, 255, 255)
BLACK = (0, 0, 0, 255)
@staticmethod
def hex2dec(colorHex: str) -> Tuple[int, int, int, int]:
"""
:param colorHex: FFFFFFFF ARGB-> (R, G, B, A)
:return:
"""
return int(colorHex[2:4], 16), int(colorHex[4:6], 16), int(colorHex[6:8], 16), int(colorHex[0:2], 16)
class Shape:
@staticmethod
def circular(radius: int, fill: tuple, width: int = 0, outline: tuple = Color.BLACK) -> Image.Image:
"""
:param radius: 半径(像素)
:param fill: 填充颜色
:param width: 轮廓粗细(像素)
:param outline: 轮廓颜色
:return: 圆形Image对象
"""
img = Image.new("RGBA", (radius * 2, radius * 2), color=radius)
draw = ImageDraw.Draw(img)
draw.ellipse(xy=(0, 0, radius * 2, radius * 2), fill=fill, outline=outline, width=width)
return img
@staticmethod
def rectangle(size: Tuple[int, int], fill: tuple, width: int = 0, outline: tuple = Color.BLACK, fillet: int = 0) -> Image.Image:
"""
:param fillet: 圆角半径(像素)
:param size: 长宽(像素)
:param fill: 填充颜色
:param width: 轮廓粗细(像素)
:param outline: 轮廓颜色
:return: 矩形Image对象
"""
img = Image.new("RGBA", size, color=fill)
draw = ImageDraw.Draw(img)
draw.rounded_rectangle(xy=(0, 0, size[0], size[1]), fill=fill, outline=outline, width=width, radius=fillet)
return img
@staticmethod
def ellipse(size: Tuple[int, int], fill: tuple, outline: int = 0, outline_color: tuple = Color.BLACK) -> Image.Image:
"""
:param size: 长宽(像素)
:param fill: 填充颜色
:param outline: 轮廓粗细(像素)
:param outline_color: 轮廓颜色
:return: 椭圆Image对象
"""
img = Image.new("RGBA", size, color=fill)
draw = ImageDraw.Draw(img)
draw.ellipse(xy=(0, 0, size[0], size[1]), fill=fill, outline=outline_color, width=outline)
return img
@staticmethod
def polygon(points: List[Tuple[int, int]], fill: tuple, outline: int, outline_color: tuple) -> Image.Image:
"""
:param points: 多边形顶点列表
:param fill: 填充颜色
:param outline: 轮廓粗细(像素)
:param outline_color: 轮廓颜色
:return: 多边形Image对象
"""
img = Image.new("RGBA", (max(points)[0], max(points)[1]), color=fill)
draw = ImageDraw.Draw(img)
draw.polygon(xy=points, fill=fill, outline=outline_color, width=outline)
return img
@staticmethod
def line(points: List[Tuple[int, int]], fill: tuple, width: int) -> Image:
"""
:param points: 线段顶点列表
:param fill: 填充颜色
:param width: 线段粗细(像素)
:return: 线段Image对象
"""
img = Image.new("RGBA", (max(points)[0], max(points)[1]), color=fill)
draw = ImageDraw.Draw(img)
draw.line(xy=points, fill=fill, width=width)
return img
class Utils:
@staticmethod
def central_clip_by_ratio(img: Image.Image, size: Tuple, use_cache=True):
"""
:param use_cache: 是否使用缓存,剪切过一次后默认生成缓存
:param img:
:param size: 仅为比例,满填充裁剪
:return:
"""
cache_file_path = str()
if use_cache:
filename_without_end = ".".join(os.path.basename(img.fp.name).split(".")[0:-1]) + f"_{size[0]}x{size[1]}" + ".png"
cache_file_path = os.path.join(".cache", filename_without_end)
if os.path.exists(cache_file_path):
nonebot.logger.info("本次使用缓存加载图片,不裁剪")
return Image.open(os.path.join(".cache", filename_without_end))
img_ratio = img.size[0] / img.size[1]
limited_ratio = size[0] / size[1]
if limited_ratio > img_ratio:
actual_size = (
img.size[0],
img.size[0] / size[0] * size[1]
)
box = (
0, (img.size[1] - actual_size[1]) // 2,
img.size[0], img.size[1] - (img.size[1] - actual_size[1]) // 2
)
else:
actual_size = (
img.size[1] / size[1] * size[0],
img.size[1],
)
box = (
(img.size[0] - actual_size[0]) // 2, 0,
img.size[0] - (img.size[0] - actual_size[0]) // 2, img.size[1]
)
img = img.crop(box).resize(size)
if use_cache:
img.save(cache_file_path)
return img
@staticmethod
def circular_clip(img: Image.Image):
"""
裁剪为alpha圆形
:param img:
:return:
"""
length = min(img.size)
alpha_cover = Image.new("RGBA", (length, length), color=(0, 0, 0, 0))
if img.size[0] > img.size[1]:
box = (
(img.size[0] - img[1]) // 2, 0,
(img.size[0] - img[1]) // 2 + img.size[1], img.size[1]
)
else:
box = (
0, (img.size[1] - img.size[0]) // 2,
img.size[0], (img.size[1] - img.size[0]) // 2 + img.size[0]
)
img = img.crop(box).resize((length, length))
draw = ImageDraw.Draw(alpha_cover)
draw.ellipse(xy=(0, 0, length, length), fill=(255, 255, 255, 255))
alpha = alpha_cover.split()[-1]
img.putalpha(alpha)
return img
@staticmethod
def open_img(path) -> Image.Image:
return Image.open(path, "RGBA")

View File

@ -0,0 +1,419 @@
import json
from typing import List, Any
from PIL import Image
from arclet.alconna import Alconna
from nb_cli import run_sync
from nonebot import on_command
from nonebot_plugin_alconna import on_alconna, Alconna, Subcommand, Args, MultiVar, Arparma, UniMessage
from pydantic import BaseModel
from .canvas import *
from ...utils.base.resource import get_path
resolution = 256
class Entrance(BaseModel):
identifier: str
size: tuple[int, int]
dest: List[str]
class Station(BaseModel):
identifier: str
chineseName: str
englishName: str
position: tuple[int, int]
class Line(BaseModel):
identifier: str
chineseName: str
englishName: str
color: Any
stations: List["Station"]
font_light = get_path("templates/fonts/MiSans/MiSans-Light.woff2")
font_bold = get_path("templates/fonts/MiSans/MiSans-Bold.woff2")
@run_sync
def generate_entrance_sign(name: str, aliases: List[str], lineInfo: List[Line], entranceIdentifier: str, ratio: tuple[int | float, int | float],
reso: int = resolution):
"""
Generates an entrance sign for the ride.
"""
width, height = ratio[0] * reso, ratio[1] * reso
baseCanvas = Canvas(Image.new("RGBA", (width, height), Color.WHITE))
# 加黑色图框
baseCanvas.outline = Img(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0, 0),
point=(0, 0),
img=Shape.rectangle(
size=(width, height),
fillet=0,
fill=(0, 0, 0, 0),
width=15,
outline=Color.BLACK
)
)
baseCanvas.contentPanel = Panel(
uv_size=(width, height),
box_size=(width - 28, height - 28),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
)
linePanelHeight = 0.7 * ratio[1]
linePanelWidth = linePanelHeight * 1.3
# 画线路面板部分
for i, line in enumerate(lineInfo):
linePanel = baseCanvas.contentPanel.__dict__[f"Line_{i}_Panel"] = Panel(
uv_size=ratio,
box_size=(linePanelWidth, linePanelHeight),
parent_point=(i * linePanelWidth / ratio[0], 1),
point=(0, 1),
)
linePanel.colorCube = Img(
uv_size=(1, 1),
box_size=(0.15, 1),
parent_point=(0.125, 1),
point=(0, 1),
img=Shape.rectangle(
size=(100, 100),
fillet=0,
fill=line.color,
),
keep_ratio=False
)
textPanel = linePanel.TextPanel = Panel(
uv_size=(1, 1),
box_size=(0.625, 1),
parent_point=(1, 1),
point=(1, 1)
)
# 中文线路名
textPanel.namePanel = Panel(
uv_size=(1, 1),
box_size=(1, 2 / 3),
parent_point=(0, 0),
point=(0, 0),
)
nameSize = baseCanvas.get_actual_pixel_size("contentPanel.Line_{}_Panel.TextPanel.namePanel".format(i))
textPanel.namePanel.text = Text(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
text=line.chineseName,
color=Color.BLACK,
font_size=int(nameSize[1] * 0.5),
force_size=True,
font=font_bold
)
# 英文线路名
textPanel.englishNamePanel = Panel(
uv_size=(1, 1),
box_size=(1, 1 / 3),
parent_point=(0, 1),
point=(0, 1),
)
englishNameSize = baseCanvas.get_actual_pixel_size("contentPanel.Line_{}_Panel.TextPanel.englishNamePanel".format(i))
textPanel.englishNamePanel.text = Text(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
text=line.englishName,
color=Color.BLACK,
font_size=int(englishNameSize[1] * 0.6),
force_size=True,
font=font_light
)
# 画名称部分
namePanel = baseCanvas.contentPanel.namePanel = Panel(
uv_size=(1, 1),
box_size=(1, 0.4),
parent_point=(0.5, 0),
point=(0.5, 0),
)
namePanel.text = Text(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
text=name,
color=Color.BLACK,
font_size=int(height * 0.3),
force_size=True,
font=font_bold
)
aliasesPanel = baseCanvas.contentPanel.aliasesPanel = Panel(
uv_size=(1, 1),
box_size=(1, 0.5),
parent_point=(0.5, 1),
point=(0.5, 1),
)
for j, alias in enumerate(aliases):
aliasesPanel.__dict__[alias] = Text(
uv_size=(1, 1),
box_size=(0.35, 0.5),
parent_point=(0.5, 0.5 * j),
point=(0.5, 0),
text=alias,
color=Color.BLACK,
font_size=int(height * 0.15),
font=font_light
)
# 画入口标识
entrancePanel = baseCanvas.contentPanel.entrancePanel = Panel(
uv_size=(1, 1),
box_size=(0.2, 1),
parent_point=(1, 0.5),
point=(1, 0.5),
)
# 中文文本
entrancePanel.namePanel = Panel(
uv_size=(1, 1),
box_size=(1, 0.5),
parent_point=(1, 0),
point=(1, 0),
)
entrancePanel.namePanel.text = Text(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0, 0.5),
point=(0, 0.5),
text=f"{entranceIdentifier}出入口",
color=Color.BLACK,
font_size=int(height * 0.2),
force_size=True,
font=font_bold
)
# 英文文本
entrancePanel.englishNamePanel = Panel(
uv_size=(1, 1),
box_size=(1, 0.5),
parent_point=(1, 1),
point=(1, 1),
)
entrancePanel.englishNamePanel.text = Text(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0, 0.5),
point=(0, 0.5),
text=f"Entrance {entranceIdentifier}",
color=Color.BLACK,
font_size=int(height * 0.15),
force_size=True,
font=font_light
)
return baseCanvas.base_img.tobytes()
crt_alc = on_alconna(
Alconna(
"crt",
Subcommand(
"entrance",
Args["name", str]["lines", str, ""]["entrance", int, 1], # /crt entrance 璧山&Bishan 1号线&Line1&#ff0000,27号线&Line1&#ff0000 1A
)
)
)
@crt_alc.assign("entrance")
async def _(result: Arparma):
args = result.subcommands.get("entrance").args
name = args["name"]
lines = args["lines"]
entrance = args["entrance"]
line_info = []
for line in lines.split(","):
line_args = line.split("&")
line_info.append(Line(
identifier=1,
chineseName=line_args[0],
englishName=line_args[1],
color=line_args[2],
stations=[]
))
img_bytes = await generate_entrance_sign(
name=name,
aliases=name.split("&"),
lineInfo=line_info,
entranceIdentifier=entrance,
ratio=(8, 1),
reso=256,
)
await crt_alc.finish(
UniMessage.image(raw=img_bytes)
)
def generate_platform_line_pic(line: Line, station: Station, ratio=None, reso: int = resolution):
"""
生成站台线路图
:param line: 线路对象
:param station: 本站点对象
:param ratio: 比例
:param reso: 分辨率1reso
:return: 两个方向的站牌
"""
if ratio is None:
ratio = [4, 1]
width, height = ratio[0] * reso, ratio[1] * reso
baseCanvas = Canvas(Image.new("RGBA", (width, height), Color.YELLOW))
# 加黑色图框
baseCanvas.linePanel = Panel(
uv_size=(1, 1),
box_size=(0.8, 0.15),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
)
# 直线块
baseCanvas.linePanel.recLine = Img(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
img=Shape.rectangle(
size=(10, 10),
fill=line.color,
),
keep_ratio=False
)
# 灰色直线块
baseCanvas.linePanel.recLineGrey = Img(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
img=Shape.rectangle(
size=(10, 10),
fill=Color.GREY,
),
keep_ratio=False
)
# 生成各站圆点
outline_width = 40
circleForward = Shape.circular(
radius=200,
fill=Color.WHITE,
width=outline_width,
outline=line.color,
)
circleThisPanel = Canvas(Image.new("RGBA", (200, 200), (0, 0, 0, 0)))
circleThisPanel.circleOuter = Img(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
img=Shape.circular(
radius=200,
fill=Color.WHITE,
width=outline_width,
outline=line.color,
),
)
circleThisPanel.circleOuter.circleInner = Img(
uv_size=(1, 1),
box_size=(0.7, 0.7),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
img=Shape.circular(
radius=200,
fill=line.color,
width=0,
outline=line.color,
),
)
circleThisPanel.export("a.png", alpha=True)
circleThis = circleThisPanel.base_img
circlePassed = Shape.circular(
radius=200,
fill=Color.WHITE,
width=outline_width,
outline=Color.GREY,
)
arrival = False
distance = 1 / (len(line.stations) - 1)
for i, sta in enumerate(line.stations):
box_size = (1.618, 1.618)
if sta.identifier == station.identifier:
arrival = True
baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img(
uv_size=(1, 1),
box_size=(1.8, 1.8),
parent_point=(distance * i, 0.5),
point=(0.5, 0.5),
img=circleThis,
keep_ratio=True
)
continue
if arrival:
# 后方站绘制
baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img(
uv_size=(1, 1),
box_size=box_size,
parent_point=(distance * i, 0.5),
point=(0.5, 0.5),
img=circleForward,
keep_ratio=True
)
else:
# 前方站绘制
baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img(
uv_size=(1, 1),
box_size=box_size,
parent_point=(distance * i, 0.5),
point=(0.5, 0.5),
img=circlePassed,
keep_ratio=True
)
return baseCanvas
def generate_platform_sign(name: str, aliases: List[str], lineInfo: List[Line], entranceIdentifier: str, ratio: tuple[int | float, int | float],
reso: int = resolution
):
pass
# def main():
# generate_entrance_sign(
# "璧山",
# aliases=["Bishan"],
# lineInfo=[
#
# Line(identifier="2", chineseName="1号线", englishName="Line 1", color=Color.RED, stations=[]),
# Line(identifier="3", chineseName="27号线", englishName="Line 27", color="#685bc7", stations=[]),
# Line(identifier="1", chineseName="璧铜线", englishName="BT Line", color="#685BC7", stations=[]),
# ],
# entranceIdentifier="1",
# ratio=(8, 1)
# )
#
#
# main()

View File

@ -0,0 +1,125 @@
import nonebot
from nonebot import on_message, require
from nonebot.plugin import PluginMetadata
from liteyuki.utils.base.data import Database, LiteModel
from liteyuki.utils.base.ly_typing import T_Bot, T_MessageEvent
from liteyuki.utils.message.message import MarkdownMessage as md
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import on_alconna
from arclet.alconna import Arparma, Alconna, Args, Option, Subcommand
class Node(LiteModel):
TABLE_NAME: str = "node"
bot_id: str = ""
session_type: str = ""
session_id: str = ""
def __str__(self):
return f"{self.bot_id}.{self.session_type}.{self.session_id}"
class Push(LiteModel):
TABLE_NAME: str = "push"
source: Node = Node()
target: Node = Node()
inde: int = 0
pushes_db = Database("data/pushes.ldb")
pushes_db.auto_migrate(Push(), Node())
alc = Alconna(
"lep",
Subcommand(
"add",
Args["source", str],
Args["target", str],
Option("bidirectional", Args["bidirectional", bool])
),
Subcommand(
"rm",
Args["index", int],
),
Subcommand(
"list",
)
)
add_push = on_alconna(alc)
@add_push.handle()
async def _(result: Arparma):
"""bot_id.session_type.session_id"""
if result.subcommands.get("add"):
source = result.subcommands["add"].args.get("source")
target = result.subcommands["add"].args.get("target")
if source and target:
source = source.split(".")
target = target.split(".")
push1 = Push(
source=Node(bot_id=source[0], session_type=source[1], session_id=source[2]),
target=Node(bot_id=target[0], session_type=target[1], session_id=target[2]),
inde=len(pushes_db.all(Push(), default=[]))
)
pushes_db.save(push1)
if result.subcommands["add"].args.get("bidirectional"):
push2 = Push(
source=Node(bot_id=target[0], session_type=target[1], session_id=target[2]),
target=Node(bot_id=source[0], session_type=source[1], session_id=source[2]),
inde=len(pushes_db.all(Push(), default=[]))
)
pushes_db.save(push2)
await add_push.finish("添加成功")
else:
await add_push.finish("参数缺失")
elif result.subcommands.get("rm"):
index = result.subcommands["rm"].args.get("index")
if index is not None:
try:
pushes_db.delete(Push(), "inde = ?", index)
await add_push.finish("删除成功")
except IndexError:
await add_push.finish("索引错误")
else:
await add_push.finish("参数缺失")
elif result.subcommands.get("list"):
await add_push.finish(
"\n".join([f"{push.inde} {push.source.bot_id}.{push.source.session_type}.{push.source.session_id} -> "
f"{push.target.bot_id}.{push.target.session_type}.{push.target.session_id}" for i, push in
enumerate(pushes_db.all(Push(), default=[]))]))
else:
await add_push.finish("参数错误")
@on_message(block=False).handle()
async def _(event: T_MessageEvent, bot: T_Bot):
for push in pushes_db.all(Push(), default=[]):
if str(push.source) == f"{bot.self_id}.{event.message_type}.{event.user_id if event.message_type == 'private' else event.group_id}":
bot2 = nonebot.get_bot(push.target.bot_id)
msg_formatted = ""
for line in str(event.message).split("\n"):
msg_formatted += f"**{line.strip()}**\n"
push_message = (
f"> From {event.sender.nickname}@{push.source.session_type}.{push.source.session_id}\n> Bot {bot.self_id}\n\n"
f"{msg_formatted}")
await md.send_md(push_message, bot2, message_type=push.target.session_type,
session_id=push.target.session_id)
return
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="轻雪事件推送",
description="事件推送插件支持单向和双向推送支持跨Bot推送",
usage="",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
}
)

View File

@ -0,0 +1,61 @@
from nonebot import on_command, require
from nonebot.adapters.onebot.v11 import MessageSegment
from nonebot.params import CommandArg
from nonebot.permission import SUPERUSER
from nonebot.plugin import PluginMetadata
from liteyuki.utils.base.ly_typing import T_Bot, T_MessageEvent, v11
from liteyuki.utils.message.message import MarkdownMessage as md, broadcast_to_superusers
from liteyuki.utils.message.html_tool import *
md_test = on_command("mdts", permission=SUPERUSER)
btn_test = on_command("btnts", permission=SUPERUSER)
latex_test = on_command("latex", permission=SUPERUSER)
placeholder = {
"&#91;": "[",
"&#93;": "]",
"&amp;": "&",
"&#44;": ",",
"\n" : r"\n",
"\"" : r'\\\"'
}
@md_test.handle()
async def _(bot: T_Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()):
await md.send_md(
str(arg),
bot,
message_type=event.message_type,
session_id=event.user_id if event.message_type == "private" else event.group_id
)
@btn_test.handle()
async def _(bot: T_Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()):
await md.send_btn(
str(arg),
bot,
message_type=event.message_type,
session_id=event.user_id if event.message_type == "private" else event.group_id
)
@latex_test.handle()
async def _(bot: T_Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()):
latex_text = f"$${str(arg)}$$"
img = await md_to_pic(latex_text)
await bot.send(event=event, message=MessageSegment.image(img))
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="轻雪Markdown测试",
description="用于测试Markdown的插件",
usage="",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
}
)

View File

@ -0,0 +1,14 @@
from nonebot.plugin import PluginMetadata
__plugin_meta__ = PluginMetadata(
name="Minecraft工具箱",
description="一些Minecraft相关工具箱",
usage="我觉得你应该会用",
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable" : True,
"default_enable": True,
}
)

View File

@ -0,0 +1,15 @@
from nonebot.plugin import PluginMetadata
from .minesweeper import *
__plugin_meta__ = PluginMetadata(
name="轻雪小游戏",
description="内置了一些小游戏",
usage="",
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
"toggleable" : True,
"default_enable" : True,
}
)

View File

@ -0,0 +1,169 @@
import random
from pydantic import BaseModel
from liteyuki.utils.message.message import MarkdownMessage as md
class Dot(BaseModel):
row: int
col: int
mask: bool = True
value: int = 0
flagged: bool = False
class Minesweeper:
# 0-8: number of mines around, 9: mine, -1: undefined
NUMS = "⓪①②③④⑤⑥⑦⑧🅑⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳"
MASK = "🅜"
FLAG = "🅕"
MINE = "🅑"
def __init__(self, rows, cols, num_mines, session_type, session_id):
assert rows > 0 and cols > 0 and 0 < num_mines < rows * cols
self.session_type = session_type
self.session_id = session_id
self.rows = rows
self.cols = cols
self.num_mines = num_mines
self.board: list[list[Dot]] = [[Dot(row=i, col=j) for j in range(cols)] for i in range(rows)]
self.is_first = True
def reveal(self, row, col) -> bool:
"""
展开
Args:
row:
col:
Returns:
游戏是否继续
"""
if self.is_first:
# 第一次展开,生成地雷
self.generate_board(self.board[row][col])
self.is_first = False
if self.board[row][col].value == 9:
self.board[row][col].mask = False
return False
if not self.board[row][col].mask:
return True
self.board[row][col].mask = False
if self.board[row][col].value == 0:
self.reveal_neighbors(row, col)
return True
def is_win(self) -> bool:
"""
是否胜利
Returns:
"""
for row in range(self.rows):
for col in range(self.cols):
if self.board[row][col].mask and self.board[row][col].value != 9:
return False
return True
def generate_board(self, first_dot: Dot):
"""
避开第一个点,生成地雷
Args:
first_dot: 第一个点
Returns:
"""
generate_count = 0
while generate_count < self.num_mines:
row = random.randint(0, self.rows - 1)
col = random.randint(0, self.cols - 1)
if self.board[row][col].value == 9 or (row, col) == (first_dot.row, first_dot.col):
continue
self.board[row][col] = Dot(row=row, col=col, mask=True, value=9)
generate_count += 1
for row in range(self.rows):
for col in range(self.cols):
if self.board[row][col].value != 9:
self.board[row][col].value = self.count_adjacent_mines(row, col)
def count_adjacent_mines(self, row, col):
"""
计算周围地雷数量
Args:
row:
col:
Returns:
"""
count = 0
for r in range(max(0, row - 1), min(self.rows, row + 2)):
for c in range(max(0, col - 1), min(self.cols, col + 2)):
if self.board[r][c].value == 9:
count += 1
return count
def reveal_neighbors(self, row, col):
"""
递归展开,使用深度优先搜索
Args:
row:
col:
Returns:
"""
for r in range(max(0, row - 1), min(self.rows, row + 2)):
for c in range(max(0, col - 1), min(self.cols, col + 2)):
if self.board[r][c].mask:
self.board[r][c].mask = False
if self.board[r][c].value == 0:
self.reveal_neighbors(r, c)
def mark(self, row, col) -> bool:
"""
标记
Args:
row:
col:
Returns:
是否标记成功,如果已经展开则无法标记
"""
if self.board[row][col].mask:
self.board[row][col].flagged = not self.board[row][col].flagged
return self.board[row][col].flagged
def board_markdown(self) -> str:
"""
打印地雷板
Returns:
"""
dis = " "
start = "> " if self.cols >= 10 else ""
text = start + self.NUMS[0] + dis*2
# 横向两个雷之间的间隔字符
# 生成横向索引
for i in range(self.cols):
text += f"{self.NUMS[i]}" + dis
text += "\n\n"
for i, row in enumerate(self.board):
text += start + f"{self.NUMS[i]}" + dis*2
print([d.value for d in row])
for dot in row:
if dot.mask and not dot.flagged:
text += md.btn_cmd(self.MASK, f"minesweeper reveal {dot.row} {dot.col}")
elif dot.flagged:
text += md.btn_cmd(self.FLAG, f"minesweeper mark {dot.row} {dot.col}")
else:
text += self.NUMS[dot.value]
text += dis
text += "\n"
btn_mark = md.btn_cmd("标记", f"minesweeper mark ", enter=False)
btn_end = md.btn_cmd("结束", "minesweeper end", enter=True)
text += f" {btn_mark} {btn_end}"
return text

View File

@ -0,0 +1,103 @@
from nonebot import require
from liteyuki.utils.base.ly_typing import T_Bot, T_MessageEvent
from liteyuki.utils.message.message import MarkdownMessage as md
require("nonebot_plugin_alconna")
from .game import Minesweeper
from nonebot_plugin_alconna import Alconna, on_alconna, Subcommand, Args, Arparma
minesweeper = on_alconna(
aliases={"扫雷"},
command=Alconna(
"minesweeper",
Subcommand(
"start",
Args["row", int, 8]["col", int, 8]["mines", int, 10],
alias=["开始"],
),
Subcommand(
"end",
alias=["结束"]
),
Subcommand(
"reveal",
Args["row", int]["col", int],
alias=["展开"]
),
Subcommand(
"mark",
Args["row", int]["col", int],
alias=["标记"]
),
),
)
minesweeper_cache: list[Minesweeper] = []
def get_minesweeper_cache(event: T_MessageEvent) -> Minesweeper | None:
for i in minesweeper_cache:
if i.session_type == event.message_type:
if i.session_id == event.user_id or i.session_id == event.group_id:
return i
return None
@minesweeper.handle()
async def _(event: T_MessageEvent, result: Arparma, bot: T_Bot):
game = get_minesweeper_cache(event)
if result.subcommands.get("start"):
if game:
await minesweeper.finish("当前会话不能同时进行多个扫雷游戏")
else:
try:
new_game = Minesweeper(
rows=result.subcommands["start"].args["row"],
cols=result.subcommands["start"].args["col"],
num_mines=result.subcommands["start"].args["mines"],
session_type=event.message_type,
session_id=event.user_id if event.message_type == "private" else event.group_id,
)
minesweeper_cache.append(new_game)
await minesweeper.send("游戏开始")
await md.send_md(new_game.board_markdown(), bot, event=event)
except AssertionError:
await minesweeper.finish("参数错误")
elif result.subcommands.get("end"):
if game:
minesweeper_cache.remove(game)
await minesweeper.finish("游戏结束")
else:
await minesweeper.finish("当前没有扫雷游戏")
elif result.subcommands.get("reveal"):
if not game:
await minesweeper.finish("当前没有扫雷游戏")
else:
row = result.subcommands["reveal"].args["row"]
col = result.subcommands["reveal"].args["col"]
if not (0 <= row < game.rows and 0 <= col < game.cols):
await minesweeper.finish("参数错误")
if not game.reveal(row, col):
minesweeper_cache.remove(game)
await md.send_md(game.board_markdown(), bot, event=event)
await minesweeper.finish("游戏结束")
await md.send_md(game.board_markdown(), bot, event=event)
if game.is_win():
minesweeper_cache.remove(game)
await minesweeper.finish("游戏胜利")
elif result.subcommands.get("mark"):
if not game:
await minesweeper.finish("当前没有扫雷游戏")
else:
row = result.subcommands["mark"].args["row"]
col = result.subcommands["mark"].args["col"]
if not (0 <= row < game.rows and 0 <= col < game.cols):
await minesweeper.finish("参数错误")
game.board[row][col].flagged = not game.board[row][col].flagged
await md.send_md(game.board_markdown(), bot, event=event)
else:
await minesweeper.finish("参数错误")

View File

@ -0,0 +1,22 @@
from nonebot.plugin import PluginMetadata
from .npm import *
from .rpm import *
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="轻雪包管理器",
description="本地插件管理和插件商店支持,资源包管理,支持启用/停用,安装/卸载插件",
usage=(
"npm list\n"
"npm enable/disable <plugin_name>\n"
"npm search <keywords...>\n"
"npm install/uninstall <plugin_name>\n"
),
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
"toggleable" : False,
"default_enable" : True,
}
)

View File

@ -0,0 +1,245 @@
import json
from typing import Optional
import aiofiles
import nonebot.plugin
from liteyuki.utils.base.data import LiteModel
from liteyuki.utils.base.data_manager import GlobalPlugin, Group, User, group_db, plugin_db, user_db
from liteyuki.utils.base.ly_typing import T_MessageEvent
__group_data = {} # 群数据缓存, {group_id: Group}
__user_data = {} # 用户数据缓存, {user_id: User}
__default_enable = {} # 插件默认启用状态缓存, {plugin_name: bool} static
__global_enable = {} # 插件全局启用状态缓存, {plugin_name: bool} dynamic
class PluginTag(LiteModel):
label: str
color: str = '#000000'
class StorePlugin(LiteModel):
name: str
desc: str
module_name: str # 插件商店中的模块名不等于本地的模块名,前者是文件夹名,后者是点分割模块名
project_link: str = ""
homepage: str = ""
author: str = ""
type: str | None = None
version: str | None = ""
time: str = ""
tags: list[PluginTag] = []
is_official: bool = False
def get_plugin_exist(plugin_name: str) -> bool:
"""
获取插件是否存在于加载列表
Args:
plugin_name:
Returns:
"""
for plugin in nonebot.plugin.get_loaded_plugins():
if plugin.name == plugin_name:
return True
return False
async def get_store_plugin(plugin_name: str) -> Optional[StorePlugin]:
"""
获取插件信息
Args:
plugin_name (str): 插件模块名
Returns:
Optional[StorePlugin]: 插件信息
"""
async with aiofiles.open("data/liteyuki/plugins.json", "r", encoding="utf-8") as f:
plugins: list[StorePlugin] = [StorePlugin(**pobj) for pobj in json.loads(await f.read())]
for plugin in plugins:
if plugin.module_name == plugin_name:
return plugin
return None
def get_plugin_default_enable(plugin_name: str) -> bool:
"""
获取插件默认启用状态,由插件定义,不存在则默认为启用,优先从缓存中获取
Args:
plugin_name (str): 插件模块名
Returns:
bool: 插件默认状态
"""
if plugin_name not in __default_enable:
plug = nonebot.plugin.get_plugin(plugin_name)
default_enable = (plug.metadata.extra.get("default_enable", True) if plug.metadata else True) if plug else True
__default_enable[plugin_name] = default_enable
return __default_enable[plugin_name]
def get_plugin_session_enable(event: T_MessageEvent, plugin_name: str) -> bool:
"""
获取插件当前会话启用状态
Args:
event: 会话事件
plugin_name (str): 插件模块名
Returns:
bool: 插件当前状态
"""
if event.message_type == "group":
group_id = str(event.group_id)
if group_id not in __group_data:
group: Group = group_db.first(Group(), "group_id = ?", group_id, default=Group(group_id=group_id))
__group_data[str(event.group_id)] = group
session = __group_data[group_id]
else:
# session: User = user_db.first(User(), "user_id = ?", event.user_id, default=User(user_id=str(event.user_id)))
user_id = str(event.user_id)
if user_id not in __user_data:
user: User = user_db.first(User(), "user_id = ?", user_id, default=User(user_id=user_id))
__user_data[user_id] = user
session = __user_data[user_id]
# 默认停用插件在启用列表内表示启用
# 默认停用插件不在启用列表内表示停用
# 默认启用插件在停用列表内表示停用
# 默认启用插件不在停用列表内表示启用
default_enable = get_plugin_default_enable(plugin_name)
if default_enable:
return plugin_name not in session.disabled_plugins
else:
return plugin_name in session.enabled_plugins
def set_plugin_session_enable(event: T_MessageEvent, plugin_name: str, enable: bool):
"""
设置插件会话启用状态,同时更新数据库和缓存
Args:
event:
plugin_name:
enable:
Returns:
"""
if event.message_type == "group":
session = group_db.first(Group(), "group_id = ?", str(event.group_id), default=Group(group_id=str(event.group_id)))
else:
session = user_db.first(User(), "user_id = ?", str(event.user_id), default=User(user_id=str(event.user_id)))
default_enable = get_plugin_default_enable(plugin_name)
if default_enable:
if enable:
session.disabled_plugins.remove(plugin_name)
else:
session.disabled_plugins.append(plugin_name)
else:
if enable:
session.enabled_plugins.append(plugin_name)
else:
session.enabled_plugins.remove(plugin_name)
if event.message_type == "group":
__group_data[str(event.group_id)] = session
print(session)
group_db.save(session)
else:
__user_data[str(event.user_id)] = session
user_db.save(session)
def get_plugin_global_enable(plugin_name: str) -> bool:
"""
获取插件全局启用状态, 优先从缓存中获取
Args:
plugin_name:
Returns:
"""
if plugin_name not in __global_enable:
plugin = plugin_db.first(
GlobalPlugin(),
"module_name = ?",
plugin_name,
default=GlobalPlugin(module_name=plugin_name, enabled=True))
__global_enable[plugin_name] = plugin.enabled
return __global_enable[plugin_name]
def set_plugin_global_enable(plugin_name: str, enable: bool):
"""
设置插件全局启用状态,同时更新数据库和缓存
Args:
plugin_name:
enable:
Returns:
"""
plugin = plugin_db.first(
GlobalPlugin(),
"module_name = ?",
plugin_name,
default=GlobalPlugin(module_name=plugin_name, enabled=True))
plugin.enabled = enable
plugin_db.save(plugin)
__global_enable[plugin_name] = enable
def get_plugin_can_be_toggle(plugin_name: str) -> bool:
"""
获取插件是否可以被启用/停用
Args:
plugin_name (str): 插件模块名
Returns:
bool: 插件是否可以被启用/停用
"""
plug = nonebot.plugin.get_plugin(plugin_name)
return plug.metadata.extra.get("toggleable", True) if plug and plug.metadata else True
def get_group_enable(group_id: str) -> bool:
"""
获取群组是否启用插机器人
Args:
group_id (str): 群组ID
Returns:
bool: 群组是否启用插件
"""
group_id = str(group_id)
if group_id not in __group_data:
group: Group = group_db.first(Group(), "group_id = ?", group_id, default=Group(group_id=group_id))
__group_data[group_id] = group
return __group_data[group_id].enable
def set_group_enable(group_id: str, enable: bool):
"""
设置群组是否启用插机器人
Args:
group_id (str): 群组ID
enable (bool): 是否启用
"""
group_id = str(group_id)
group: Group = group_db.first(Group(), "group_id = ?", group_id, default=Group(group_id=group_id))
group.enable = enable
__group_data[group_id] = group
group_db.save(group)

View File

@ -0,0 +1,641 @@
import os
import sys
import aiohttp
import nonebot.plugin
import pip
from io import StringIO
from arclet.alconna import MultiVar
from nonebot import Bot, require
from nonebot.exception import FinishedException, IgnoredException, MockApiException
from nonebot.internal.adapter import Event
from nonebot.internal.matcher import Matcher
from nonebot.message import run_preprocessor
from nonebot.permission import SUPERUSER
from nonebot.plugin import Plugin, PluginMetadata
from nonebot.utils import run_sync
from liteyuki.utils.base.data_manager import InstalledPlugin
from liteyuki.utils.base.language import get_user_lang
from liteyuki.utils.base.ly_typing import T_Bot
from liteyuki.utils.message.message import MarkdownMessage as md
from liteyuki.utils.message.markdown import MarkdownComponent as mdc, compile_md, escape_md
from liteyuki.utils.base.permission import GROUP_ADMIN, GROUP_OWNER
from liteyuki.utils.message.tools import clamp
from .common import *
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import on_alconna, Alconna, Args, Arparma, Subcommand
# const
enable_global = "enable-global"
disable_global = "disable-global"
enable = "enable"
disable = "disable"
@on_alconna(
aliases={"插件"},
command=Alconna(
"npm",
Subcommand(
"enable",
Args["plugin_name", str],
alias=["e", "启用"],
),
Subcommand(
"disable",
Args["plugin_name", str],
alias=["d", "停用"],
),
Subcommand(
enable_global,
Args["plugin_name", str],
alias=["eg", "全局启用"],
),
Subcommand(
disable_global,
Args["plugin_name", str],
alias=["dg", "全局停用"],
),
# 安装部分
Subcommand(
"update",
alias=["u", "更新"],
),
Subcommand(
"search",
Args["keywords", MultiVar(str)],
alias=["s", "搜索"],
),
Subcommand(
"install",
Args["plugin_name", str],
alias=["i", "安装"],
),
Subcommand(
"uninstall",
Args["plugin_name", str],
alias=["r", "rm", "卸载"],
),
Subcommand(
"list",
Args["page", int, 1]["num", int, 10],
alias=["ls", "列表"],
)
)
).handle()
async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
if not os.path.exists("data/liteyuki/plugins.json"):
await npm_update()
# 判断会话类型
ulang = get_user_lang(str(event.user_id))
plugin_name = result.args.get("plugin_name")
sc = result.subcommands # 获取子命令
perm_s = await SUPERUSER(bot, event) # 判断是否为超级用户
# 支持对自定义command_start的判断
if sc.get("enable") or result.subcommands.get("disable"):
toggle = result.subcommands.get("enable") is not None
plugin_exist = get_plugin_exist(plugin_name)
session_enable = get_plugin_session_enable(event, plugin_name) # 获取插件当前状态
can_be_toggled = get_plugin_can_be_toggle(plugin_name) # 获取插件是否可以被启用/停用
if not plugin_exist:
await npm.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name))
if not can_be_toggled:
await npm.finish(ulang.get("npm.plugin_cannot_be_toggled", NAME=plugin_name))
if session_enable == toggle:
await npm.finish(
ulang.get("npm.plugin_already", NAME=plugin_name, STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable")))
if event.message_type == "private":
session = user_db.first(User(), "user_id = ?", event.user_id, default=User(user_id=event.user_id))
else:
if await GROUP_ADMIN(bot, event) or await GROUP_OWNER(bot, event) or await SUPERUSER(bot, event):
session = group_db.first(Group(), "group_id = ?", event.group_id, default=Group(group_id=str(event.group_id)))
else:
raise FinishedException(ulang.get("Permission Denied"))
try:
set_plugin_session_enable(event, plugin_name, toggle)
except Exception as e:
nonebot.logger.error(e)
await npm.finish(
ulang.get(
"npm.toggle_failed",
NAME=plugin_name,
STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable"),
ERROR=str(e))
)
await npm.finish(
ulang.get(
"npm.toggle_success",
NAME=plugin_name,
STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable"))
)
elif sc.get(enable_global) or result.subcommands.get(disable_global) and await SUPERUSER(bot, event):
plugin_exist = get_plugin_exist(plugin_name)
toggle = result.subcommands.get(enable_global) is not None
can_be_toggled = get_plugin_can_be_toggle(plugin_name)
if not plugin_exist:
await npm.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name))
if not can_be_toggled:
await npm.finish(ulang.get("npm.plugin_cannot_be_toggled", NAME=plugin_name))
global_enable = get_plugin_global_enable(plugin_name)
if global_enable == toggle:
await npm.finish(
ulang.get("npm.plugin_already", NAME=plugin_name, STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable")))
try:
set_plugin_global_enable(plugin_name, toggle)
except Exception as e:
await npm.finish(
ulang.get(
"npm.toggle_failed",
NAME=plugin_name,
STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable"),
ERROR=str(e))
)
await npm.finish(
ulang.get(
"npm.toggle_success",
NAME=plugin_name,
STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable"))
)
elif sc.get("update") and perm_s:
r = await npm_update()
if r:
await npm.finish(ulang.get("npm.store_update_success"))
else:
await npm.finish(ulang.get("npm.store_update_failed"))
elif sc.get("search"):
keywords: list[str] = result.subcommands["search"].args.get("keywords")
rs = await npm_search(keywords)
max_show = 10
if len(rs):
reply = f"{ulang.get('npm.search_result')} | {ulang.get('npm.total', TOTAL=len(rs))}\n***"
for storePlugin in rs[:min(max_show, len(rs))]:
btn_install_or_update = md.btn_cmd(
ulang.get("npm.update") if get_plugin_exist(storePlugin.module_name) else ulang.get("npm.install"),
"npm install %s" % storePlugin.module_name
)
link_page = md.btn_link(ulang.get("npm.homepage"), storePlugin.homepage)
link_pypi = md.btn_link(ulang.get("npm.pypi"), storePlugin.homepage)
reply += (f"\n# **{storePlugin.name}**\n"
f"\n> **{storePlugin.desc}**\n"
f"\n> {ulang.get('npm.author')}: {storePlugin.author}"
f"\n> *{md.escape(storePlugin.module_name)}*"
f"\n> {btn_install_or_update} {link_page} {link_pypi}\n\n***\n")
if len(rs) > max_show:
reply += f"\n{ulang.get('npm.too_many_results', HIDE_NUM=len(rs) - max_show)}"
else:
reply = ulang.get("npm.search_no_result")
await md.send_md(reply, bot, event=event)
elif sc.get("install") and perm_s:
plugin_name: str = result.subcommands["install"].args.get("plugin_name")
store_plugin = await get_store_plugin(plugin_name)
await npm.send(ulang.get("npm.installing", NAME=plugin_name))
r, log = await npm_install(plugin_name)
log = log.replace("\\", "/")
if not store_plugin:
await npm.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name))
homepage_btn = md.btn_cmd(ulang.get("npm.homepage"), store_plugin.homepage)
if r:
r_load = nonebot.load_plugin(plugin_name) # 加载插件
installed_plugin = InstalledPlugin(module_name=plugin_name) # 构造插件信息模型
found_in_db_plugin = plugin_db.first(InstalledPlugin(), "module_name = ?", plugin_name) # 查询数据库中是否已经安装
if r_load:
if found_in_db_plugin is None:
plugin_db.save(installed_plugin)
info = md.escape(ulang.get("npm.install_success", NAME=store_plugin.name)) # markdown转义
await md.send_md(
f"{info}\n\n"
f"```\n{log}\n```",
bot,
event=event
)
else:
await npm.finish(ulang.get("npm.plugin_already_installed", NAME=store_plugin.name))
else:
info = ulang.get("npm.load_failed", NAME=plugin_name, HOMEPAGE=homepage_btn).replace("_", r"\\_")
await md.send_md(
f"{info}\n\n"
f"```\n{log}\n```\n",
bot,
event=event
)
else:
info = ulang.get("npm.install_failed", NAME=plugin_name, HOMEPAGE=homepage_btn).replace("_", r"\\_")
await md.send_md(
f"{info}\n\n"
f"```\n{log}\n```",
bot,
event=event
)
elif sc.get("uninstall") and perm_s:
plugin_name: str = result.subcommands["uninstall"].args.get("plugin_name") # type: ignore
found_installed_plugin: InstalledPlugin = plugin_db.first(InstalledPlugin(), "module_name = ?", plugin_name)
if found_installed_plugin:
plugin_db.delete(InstalledPlugin(), "module_name = ?", plugin_name)
reply = f"{ulang.get('npm.uninstall_success', NAME=found_installed_plugin.module_name)}"
await npm.finish(reply)
else:
await npm.finish(ulang.get("npm.plugin_not_installed", NAME=plugin_name))
elif sc.get("list"):
loaded_plugin_list = sorted(nonebot.get_loaded_plugins(), key=lambda x: x.name)
num_per_page = result.subcommands.get("list").args.get("num")
total = len(loaded_plugin_list) // num_per_page + (1 if len(loaded_plugin_list) % num_per_page else 0)
page = clamp(result.subcommands.get("list").args.get("page"), 1, total)
# 已加载插件 | 总计10 | 第1/3页
reply = (f"# {ulang.get('npm.loaded_plugins')} | "
f"{ulang.get('npm.total', TOTAL=len(nonebot.get_loaded_plugins()))} | "
f"{ulang.get('npm.page', PAGE=page, TOTAL=total)} \n***\n")
permission_oas = await GROUP_ADMIN(bot, event) or await GROUP_OWNER(bot, event) or await SUPERUSER(bot, event)
permission_s = await SUPERUSER(bot, event)
for storePlugin in loaded_plugin_list[(page - 1) * num_per_page: min(page * num_per_page, len(loaded_plugin_list))]:
# 检查是否有 metadata 属性
# 添加帮助按钮
btn_usage = md.btn_cmd(ulang.get("npm.usage"), f"help {storePlugin.name}", False)
store_plugin = await get_store_plugin(storePlugin.name)
session_enable = get_plugin_session_enable(event, storePlugin.name)
if store_plugin:
# btn_homepage = md.btn_link(ulang.get("npm.homepage"), store_plugin.homepage)
show_name = store_plugin.name
elif storePlugin.metadata:
# if storePlugin.metadata.extra.get("liteyuki"):
# btn_homepage = md.btn_link(ulang.get("npm.homepage"), "https://github.com/snowykami/LiteyukiBot")
# else:
# btn_homepage = ulang.get("npm.homepage")
show_name = storePlugin.metadata.name
else:
# btn_homepage = ulang.get("npm.homepage")
show_name = storePlugin.name
ulang.get("npm.no_description")
if storePlugin.metadata:
reply += f"\n**{md.escape(show_name)}**\n"
else:
reply += f"**{md.escape(show_name)}**\n"
reply += f"\n > {btn_usage}"
if permission_oas:
# 添加启用/停用插件按钮
cmd_toggle = f"npm {'disable' if session_enable else 'enable'} {storePlugin.name}"
text_toggle = ulang.get("npm.disable" if session_enable else "npm.enable")
can_be_toggle = get_plugin_can_be_toggle(storePlugin.name)
btn_toggle = text_toggle if not can_be_toggle else md.btn_cmd(text_toggle, cmd_toggle)
reply += f" {btn_toggle}"
if permission_s:
plugin_in_database = plugin_db.first(InstalledPlugin(), "module_name = ?", storePlugin.name)
# 添加移除插件和全局切换按钮
global_enable = get_plugin_global_enable(storePlugin.name)
btn_uninstall = (
md.btn_cmd(ulang.get("npm.uninstall"), f'npm uninstall {storePlugin.name}')) if plugin_in_database else ulang.get(
'npm.uninstall')
btn_toggle_global_text = ulang.get("npm.disable_global" if global_enable else "npm.enable_global")
cmd_toggle_global = f"npm {'disable' if global_enable else 'enable'}-global {storePlugin.name}"
btn_toggle_global = btn_toggle_global_text if not can_be_toggle else md.btn_cmd(btn_toggle_global_text, cmd_toggle_global)
reply += f" {btn_uninstall} {btn_toggle_global}"
reply += "\n\n***\n"
# 根据页数添加翻页按钮。第一页显示上一页文本而不是按钮,最后一页显示下一页文本而不是按钮
btn_prev = md.btn_cmd(ulang.get("npm.prev_page"), f"npm list {page - 1} {num_per_page}") if page > 1 else ulang.get("npm.prev_page")
btn_next = md.btn_cmd(ulang.get("npm.next_page"), f"npm list {page + 1} {num_per_page}") if page < total else ulang.get("npm.next_page")
reply += f"\n{btn_prev} {page}/{total} {btn_next}"
await md.send_md(reply, bot, event=event)
else:
if await SUPERUSER(bot, event):
btn_enable_global = md.btn_cmd(ulang.get("npm.enable_global"), "npm enable-global", False, False)
btn_disable_global = md.btn_cmd(ulang.get("npm.disable_global"), "npm disable-global", False, False)
btn_search = md.btn_cmd(ulang.get("npm.search"), "npm search ", False, False)
btn_uninstall_ = md.btn_cmd(ulang.get("npm.uninstall"), "npm uninstall ", False, False)
btn_install_ = md.btn_cmd(ulang.get("npm.install"), "npm install ", False, False)
btn_update = md.btn_cmd(ulang.get("npm.update_index"), "npm update", False, True)
btn_list = md.btn_cmd(ulang.get("npm.list_plugins"), "npm list ", False, False)
btn_disable = md.btn_cmd(ulang.get("npm.disable_session"), "npm disable ", False, False)
btn_enable = md.btn_cmd(ulang.get("npm.enable_session"), "npm enable ", False, False)
reply = (
f"\n# **{ulang.get('npm.help')}**"
f"\n{btn_update}"
f"\n\n>*{md.escape('npm update')}*\n"
f"\n{btn_install_}"
f"\n\n>*{md.escape('npm install <plugin_name')}*>\n"
f"\n{btn_uninstall_}"
f"\n\n>*{md.escape('npm uninstall <plugin_name')}*>\n"
f"\n{btn_search}"
f"\n\n>*{md.escape('npm search <keywords...')}*>\n"
f"\n{btn_disable_global}"
f"\n\n>*{md.escape('npm disable-global <plugin_name')}*>\n"
f"\n{btn_enable_global}"
f"\n\n>*{md.escape('npm enable-global <plugin_name')}*>\n"
f"\n{btn_disable}"
f"\n\n>*{md.escape('npm disable <plugin_name')}*>\n"
f"\n{btn_enable}"
f"\n\n>*{md.escape('npm enable <plugin_name')}*>\n"
f"\n{btn_list}"
f"\n\n>page为页数num为每页显示数量"
f"\n\n>*{md.escape('npm list [page] [num]')}*"
)
await md.send_md(reply, bot, event=event)
else:
btn_list = md.btn_cmd(ulang.get("npm.list_plugins"), "npm list ", False, False)
btn_disable = md.btn_cmd(ulang.get("npm.disable_session"), "npm disable ", False, False)
btn_enable = md.btn_cmd(ulang.get("npm.enable_session"), "npm enable ", False, False)
reply = (
f"\n# **{ulang.get('npm.help')}**"
f"\n{btn_disable}"
f"\n\n>*{md.escape('npm disable <plugin_name')}*>\n"
f"\n{btn_enable}"
f"\n\n>*{md.escape('npm enable <plugin_name')}*>\n"
f"\n{btn_list}"
f"\n\n>page为页数num为每页显示数量"
f"\n\n>*{md.escape('npm list [page] [num]')}*"
)
await md.send_md(reply, bot, event=event)
@on_alconna(
aliases={"群聊"},
command=Alconna(
"gm",
Subcommand(
enable,
Args["group_id", str, None],
alias=["e", "启用"],
),
Subcommand(
disable,
Args["group_id", str, None],
alias=["d", "停用"],
),
),
permission=SUPERUSER | GROUP_OWNER | GROUP_ADMIN
).handle()
async def _(bot: T_Bot, event: T_MessageEvent, gm: Matcher, result: Arparma):
ulang = get_user_lang(str(event.user_id))
to_enable = result.subcommands.get(enable) is not None
group_id = None
if await SUPERUSER(bot, event):
# 仅超级用户可以自定义群号
group_id = result.subcommands.get(enable, result.subcommands.get(disable)).args.get("group_id")
if group_id is None and event.message_type == "group":
group_id = str(event.group_id)
if group_id is None:
await gm.finish(ulang.get("liteyuki.invalid_command"), liteyuki_pass=True)
enabled = get_group_enable(group_id)
if enabled == to_enable:
await gm.finish(ulang.get("liteyuki.group_already", STATUS=ulang.get("npm.enable") if to_enable else ulang.get("npm.disable"), GROUP=group_id),
liteyuki_pass=True)
else:
set_group_enable(group_id, to_enable)
await gm.finish(
ulang.get("liteyuki.group_success", STATUS=ulang.get("npm.enable") if to_enable else ulang.get("npm.disable"), GROUP=group_id),
liteyuki_pass=True
)
@on_alconna(
aliases={"帮助"},
command=Alconna(
"help",
Args["plugin_name", str, None],
)
).handle()
async def _(result: Arparma, matcher: Matcher, event: T_MessageEvent, bot: T_Bot):
ulang = get_user_lang(str(event.user_id))
plugin_name = result.main_args.get("plugin_name")
if plugin_name:
searched_plugins = search_loaded_plugin(plugin_name)
if searched_plugins:
loaded_plugin = searched_plugins[0]
else:
await matcher.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name))
if loaded_plugin:
if loaded_plugin.metadata is None:
loaded_plugin.metadata = PluginMetadata(name=plugin_name, description="", usage="")
# 从商店获取详细信息
store_plugin = await get_store_plugin(plugin_name)
if loaded_plugin.metadata.extra.get("liteyuki"):
store_plugin = StorePlugin(
name=loaded_plugin.metadata.name,
desc=loaded_plugin.metadata.description,
author="SnowyKami",
module_name=plugin_name,
homepage="https://github.com/snowykami/LiteyukiBot"
)
elif store_plugin is None:
store_plugin = StorePlugin(
name=loaded_plugin.metadata.name,
desc=loaded_plugin.metadata.description,
author="",
module_name=plugin_name,
homepage=""
)
if store_plugin:
link = store_plugin.homepage
elif loaded_plugin.metadata.extra.get("liteyuki"):
link = "https://github.com/snowykami/LiteyukiBot"
else:
link = None
reply = [
mdc.heading(escape_md(store_plugin.name)),
mdc.quote(store_plugin.module_name),
mdc.quote(mdc.bold(ulang.get("npm.author")) + " " +
(mdc.link(store_plugin.author, f"https://github.com/{store_plugin.author}") if store_plugin.author else "Unknown")),
mdc.quote(mdc.bold(ulang.get("npm.description")) + " " + mdc.paragraph(max(loaded_plugin.metadata.description, store_plugin.desc))),
mdc.heading(ulang.get("npm.usage"), 2),
mdc.quote(escape_md(loaded_plugin.metadata.usage)),
mdc.link(ulang.get("npm.homepage"), link) if link else mdc.paragraph(ulang.get("npm.no_homepage"))
]
await md.send_md(compile_md(reply), bot, event=event)
else:
await matcher.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name))
else:
pass
# 传入事件阻断hook
@run_preprocessor
async def pre_handle(event: Event, matcher: Matcher):
plugin: Plugin = matcher.plugin
plugin_global_enable = get_plugin_global_enable(plugin.name)
if not plugin_global_enable:
raise IgnoredException("Plugin disabled globally")
if event.get_type() == "message":
plugin_session_enable = get_plugin_session_enable(event, plugin.name)
if not plugin_session_enable:
raise IgnoredException("Plugin disabled in session")
# 群聊开关阻断hook
@Bot.on_calling_api
async def block_disable_session(bot: Bot, api: str, args: dict):
if "group_id" in args and not args.get("liteyuki_pass", False):
group_id = args["group_id"]
if not get_group_enable(group_id):
nonebot.logger.debug(f"Group {group_id} disabled")
raise MockApiException(f"Group {group_id} disabled")
async def npm_update() -> bool:
"""
更新本地插件json缓存
Returns:
bool: 是否成功更新
"""
url_list = [
"https://registry.nonebot.dev/plugins.json",
]
for url in url_list:
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
if resp.status == 200:
async with aiofiles.open("data/liteyuki/plugins.json", "wb") as f:
data = await resp.read()
await f.write(data)
return True
return False
async def npm_search(keywords: list[str]) -> list[StorePlugin]:
"""
在本地缓存商店数据中搜索插件
Args:
keywords (list[str]): 关键词列表
Returns:
list[StorePlugin]: 插件列表
"""
plugin_blacklist = [
"nonebot_plugin_xiuxian_2",
"nonebot_plugin_htmlrender",
"nonebot_plugin_alconna",
]
results = []
async with aiofiles.open("data/liteyuki/plugins.json", "r", encoding="utf-8") as f:
plugins: list[StorePlugin] = [StorePlugin(**pobj) for pobj in json.loads(await f.read())]
for plugin in plugins:
if plugin.module_name in plugin_blacklist:
continue
plugin_text = ' '.join(
[
plugin.name,
plugin.desc,
plugin.author,
plugin.module_name,
' '.join([tag.label for tag in plugin.tags])
]
)
if all([keyword in plugin_text for keyword in keywords]):
results.append(plugin)
return results
@run_sync
def npm_install(plugin_package_name) -> tuple[bool, str]:
"""
异步安装插件使用pip安装
Args:
plugin_package_name:
Returns:
tuple[bool, str]: 是否成功,输出信息
"""
# 重定向标准输出
buffer = StringIO()
sys.stdout = buffer
sys.stderr = buffer
update = False
if get_plugin_exist(plugin_package_name):
update = True
mirrors = [
"https://pypi.tuna.tsinghua.edu.cn/simple", # 清华大学
"https://pypi.org/simple", # 官方源
]
# 使用pip安装包对每个镜像尝试一次成功后返回值
success = False
for mirror in mirrors:
try:
nonebot.logger.info(f"pip install try mirror: {mirror}")
if update:
result = pip.main(["install", "--upgrade", plugin_package_name, "-i", mirror])
else:
result = pip.main(["install", plugin_package_name, "-i", mirror])
success = result == 0
if success:
break
else:
nonebot.logger.warning(f"pip install failed, try next mirror.")
except Exception as e:
success = False
continue
sys.stdout = sys.__stdout__
sys.stderr = sys.__stderr__
return success, buffer.getvalue()
def search_loaded_plugin(keyword: str) -> list[Plugin]:
"""
搜索已加载插件
Args:
keyword (str): 关键词
Returns:
list[Plugin]: 插件列表
"""
if nonebot.get_plugin(keyword) is not None:
return [nonebot.get_plugin(keyword)]
else:
results = []
for plugin in nonebot.get_loaded_plugins():
if plugin.metadata is None:
plugin.metadata = PluginMetadata(name=plugin.name, description="", usage="")
if keyword in plugin.name + plugin.metadata.name + plugin.metadata.description:
results.append(plugin)
return results

View File

@ -0,0 +1,171 @@
# 轻雪资源包管理器
import os
import yaml
from nonebot import require
from nonebot.permission import SUPERUSER
from liteyuki.utils.base.language import get_user_lang
from liteyuki.utils.base.ly_typing import T_Bot, T_MessageEvent
from liteyuki.utils.message.message import MarkdownMessage as md
from liteyuki.utils.base.resource import (ResourceMetadata, add_resource_pack, change_priority, check_exist, check_status, get_loaded_resource_packs, get_resource_metadata, load_resources, remove_resource_pack)
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import Alconna, Args, on_alconna, Arparma, Subcommand
@on_alconna(
aliases={"资源包"},
command=Alconna(
"rpm",
Subcommand(
"list",
Args["page", int, 1]["num", int, 10],
alias=["ls", "列表", "列出"],
),
Subcommand(
"load",
Args["name", str],
alias=["安装"],
),
Subcommand(
"unload",
Args["name", str],
alias=["卸载"],
),
Subcommand(
"up",
Args["name", str],
alias=["上移"],
),
Subcommand(
"down",
Args["name", str],
alias=["下移"],
),
Subcommand(
"top",
Args["name", str],
alias=["置顶"],
),
Subcommand(
"reload",
alias=["重载"],
),
),
permission=SUPERUSER
).handle()
async def _(bot: T_Bot, event: T_MessageEvent, result: Arparma):
ulang = get_user_lang(str(event.user_id))
reply = ""
if result.subcommands.get("list"):
loaded_rps = get_loaded_resource_packs()
reply += f"{ulang.get('liteyuki.loaded_resources', NUM=len(loaded_rps))}\n"
for rp in loaded_rps:
btn_unload = md.btn_cmd(
ulang.get("npm.uninstall"),
f"rpm unload {rp.folder}"
)
btn_move_up = md.btn_cmd(
ulang.get("rpm.move_up"),
f"rpm up {rp.folder}"
)
btn_move_down = md.btn_cmd(
ulang.get("rpm.move_down"),
f"rpm down {rp.folder}"
)
btn_move_top = md.btn_cmd(
ulang.get("rpm.move_top"),
f"rpm top {rp.folder}"
)
# 添加新行
reply += (f"\n**{md.escape(rp.name)}**({md.escape(rp.folder)})\n\n"
f"> {btn_move_up} {btn_move_down} {btn_move_top} {btn_unload}\n\n***")
reply += f"\n\n{ulang.get('liteyuki.unloaded_resources')}\n"
loaded_folders = [rp.folder for rp in get_loaded_resource_packs()]
for folder in os.listdir("resources"):
if folder not in loaded_folders and os.path.exists(os.path.join("resources", folder, "metadata.yml")):
metadata = ResourceMetadata(
**yaml.load(
open(
os.path.join("resources", folder, "metadata.yml"),
encoding="utf-8"
),
Loader=yaml.FullLoader
)
)
metadata.folder = folder
metadata.path = os.path.join("resources", folder)
btn_load = md.btn_cmd(
ulang.get("npm.install"),
f"rpm load {metadata.folder}"
)
# 添加新行
reply += (f"\n**{md.escape(metadata.name)}**({md.escape(metadata.folder)})\n\n"
f"> {btn_load}\n\n***")
elif result.subcommands.get("load") or result.subcommands.get("unload"):
load = result.subcommands.get("load") is not None
rp_name = result.args.get("name")
r = False # 操作结果
if check_exist(rp_name):
if load != check_status(rp_name):
# 状态不同
if load:
r = add_resource_pack(rp_name)
else:
r = remove_resource_pack(rp_name)
rp_meta = get_resource_metadata(rp_name)
reply += ulang.get(
f"liteyuki.{'load' if load else 'unload'}_resource_{'success' if r else 'failed'}",
NAME=rp_meta.name
)
else:
# 重复操作
reply += ulang.get(f"liteyuki.resource_already_{'load' if load else 'unload'}ed", NAME=rp_name)
else:
reply += ulang.get("liteyuki.resource_not_found", NAME=rp_name)
if r:
btn_reload = md.btn_cmd(
ulang.get("liteyuki.reload_resources"),
f"rpm reload"
)
reply += "\n" + ulang.get("liteyuki.need_reload", BTN=btn_reload)
elif result.subcommands.get("up") or result.subcommands.get("down") or result.subcommands.get("top"):
rp_name = result.args.get("name")
if result.subcommands.get("up"):
delta = -1
elif result.subcommands.get("down"):
delta = 1
else:
delta = 0
if check_exist(rp_name):
if check_status(rp_name):
r = change_priority(rp_name, delta)
reply += ulang.get(f"liteyuki.change_priority_{'success' if r else 'failed'}", NAME=rp_name)
if r:
btn_reload = md.btn_cmd(
ulang.get("liteyuki.reload_resources"),
f"rpm reload"
)
reply += "\n" + ulang.get("liteyuki.need_reload", BTN=btn_reload)
else:
reply += ulang.get("liteyuki.resource_not_found", NAME=rp_name)
else:
reply += ulang.get("liteyuki.resource_not_found", NAME=rp_name)
elif result.subcommands.get("reload"):
load_resources()
reply = ulang.get(
"liteyuki.reload_resources_success",
NUM=len(get_loaded_resource_packs())
)
else:
btn_reload = md.btn_cmd(
ulang.get("liteyuki.reload_resources"),
f"rpm reload"
)
btn_list = md.btn_cmd(
ulang.get("liteyuki.list_resources"),
f"rpm list"
)
reply += f"{btn_list} \n {btn_reload}"
await md.send_md(reply, bot, event=event)

View File

@ -0,0 +1,24 @@
from nonebot.plugin import PluginMetadata
from .status import *
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="状态查看器",
description="",
usage=(
"MARKDOWN### 状态查看器\n"
"查看机器人的状态\n"
"### 用法\n"
"- `/status` 查看基本情况\n"
"- `/status memory` 查看内存使用情况\n"
"- `/status process` 查看进程情况\n"
),
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
"toggleable" : False,
"default_enable" : True,
}
)

View File

@ -0,0 +1,257 @@
import platform
import time
import nonebot
import psutil
from cpuinfo import cpuinfo
from nonebot import require
from liteyuki.utils import __NAME__, __VERSION__
from liteyuki.utils.base.config import get_config
from liteyuki.utils.base.data_manager import TempConfig, common_db
from liteyuki.utils.base.language import Language
from liteyuki.utils.base.resource import get_loaded_resource_packs, get_path
from liteyuki.utils.message.html_tool import template2image
require("nonebot_plugin_apscheduler")
from nonebot_plugin_apscheduler import scheduler
protocol_names = {
0: "iPad",
1: "Android Phone",
2: "Android Watch",
3: "Mac",
5: "iPad",
6: "Android Pad",
}
"""
Universal Interface
data
- bot
- name: str
icon: str
id: int
protocol_name: str
groups: int
friends: int
message_sent: int
message_received: int
app_name: str
- hardware
- cpu
- percent: float
- name: str
- mem
- percent: float
- total: int
- used: int
- free: int
- swap
- percent: float
- total: int
- used: int
- free: int
- disk: list
- name: str
- percent: float
- total: int
"""
status_card_cache = {} # lang -> bytes
# 60s刷新一次
@scheduler.scheduled_job("cron", second="*/40")
async def refresh_status_card():
nonebot.logger.debug("Refreshing status card cache...")
global status_card_cache
bot_data = await get_bots_data()
hardware_data = await get_hardware_data()
liteyuki_data = await get_liteyuki_data()
for lang in status_card_cache.keys():
status_card_cache[lang] = await generate_status_card(
bot_data,
hardware_data,
liteyuki_data,
lang=lang,
use_cache=False
)
async def generate_status_card(bot: dict, hardware: dict, liteyuki: dict, lang="zh-CN", bot_id="0", use_cache=False) -> bytes:
if not use_cache:
return await template2image(
get_path("templates/status.html", abs_path=True),
{
"data": {
"bot" : bot,
"hardware" : hardware,
"liteyuki" : liteyuki,
"localization": get_local_data(lang)
}
},
debug=True
)
else:
if lang not in status_card_cache:
status_card_cache[lang] = await generate_status_card(bot, hardware, liteyuki, lang=lang, bot_id=bot_id)
return status_card_cache[lang]
def get_local_data(lang_code) -> dict:
lang = Language(lang_code)
return {
"friends" : lang.get("status.friends"),
"groups" : lang.get("status.groups"),
"plugins" : lang.get("status.plugins"),
"bots" : lang.get("status.bots"),
"message_sent" : lang.get("status.message_sent"),
"message_received": lang.get("status.message_received"),
"cpu" : lang.get("status.cpu"),
"memory" : lang.get("status.memory"),
"swap" : lang.get("status.swap"),
"disk" : lang.get("status.disk"),
"usage" : lang.get("status.usage"),
"total" : lang.get("status.total"),
"used" : lang.get("status.used"),
"free" : lang.get("status.free"),
"days" : lang.get("status.days"),
"hours" : lang.get("status.hours"),
"minutes" : lang.get("status.minutes"),
"seconds" : lang.get("status.seconds"),
"runtime" : lang.get("status.runtime"),
"threads" : lang.get("status.threads"),
"cores" : lang.get("status.cores"),
"process" : lang.get("status.process"),
"resources" : lang.get("status.resources"),
"description" : lang.get("status.description"),
}
async def get_bots_data(self_id: str = "0") -> dict:
"""获取当前所有机器人数据
Returns:
"""
result = {
"self_id": self_id,
"bots" : [],
}
for bot_id, bot in nonebot.get_bots().items():
groups = 0
friends = 0
status = {}
bot_name = bot_id
version_info = {}
try:
# API fetch
bot_name = (await bot.get_login_info())["nickname"]
groups = len(await bot.get_group_list())
friends = len(await bot.get_friend_list())
status = await bot.get_status()
version_info = await bot.get_version_info()
except Exception:
pass
statistics = status.get("stat", {})
app_name = version_info.get("app_name", "UnknownImplementation")
if app_name in ["Lagrange.OneBot", "LLOneBot", "Shamrock"]:
icon = f"https://q.qlogo.cn/g?b=qq&nk={bot_id}&s=640"
else:
icon = None
bot_data = {
"name" : bot_name,
"icon" : icon,
"id" : bot_id,
"protocol_name" : protocol_names.get(version_info.get("protocol_name"), "Online"),
"groups" : groups,
"friends" : friends,
"message_sent" : statistics.get("message_sent", 0),
"message_received": statistics.get("message_received", 0),
"app_name" : app_name
}
result["bots"].append(bot_data)
return result
async def get_hardware_data() -> dict:
mem = psutil.virtual_memory()
all_processes = psutil.Process().children(recursive=True)
all_processes.append(psutil.Process())
mem_used_process = 0
process_mem = {}
for process in all_processes:
try:
ps_name = process.name().replace(".exe", "")
if ps_name not in process_mem:
process_mem[ps_name] = 0
process_mem[ps_name] += process.memory_info().rss
mem_used_process += process.memory_info().rss
except Exception:
pass
swap = psutil.swap_memory()
cpu_brand_raw = cpuinfo.get_cpu_info().get("brand_raw", "Unknown")
if "AMD" in cpu_brand_raw:
brand = "AMD"
elif "Intel" in cpu_brand_raw:
brand = "Intel"
else:
brand = "Unknown"
result = {
"cpu" : {
"percent": psutil.cpu_percent(),
"name" : f"{brand} {cpuinfo.get_cpu_info().get('arch', 'Unknown')}",
"cores" : psutil.cpu_count(logical=False),
"threads": psutil.cpu_count(logical=True),
"freq" : psutil.cpu_freq().current # MHz
},
"memory": {
"percent" : mem.percent,
"total" : mem.total,
"used" : mem.used,
"free" : mem.free,
"usedProcess": mem_used_process,
},
"swap" : {
"percent": swap.percent,
"total" : swap.total,
"used" : swap.used,
"free" : swap.free
},
"disk" : [],
}
for disk in psutil.disk_partitions(all=True):
try:
disk_usage = psutil.disk_usage(disk.mountpoint)
if disk_usage.total == 0:
continue # 虚拟磁盘
result["disk"].append({
"name" : disk.mountpoint,
"percent": disk_usage.percent,
"total" : disk_usage.total,
"used" : disk_usage.used,
"free" : disk_usage.free
})
except:
pass
return result
async def get_liteyuki_data() -> dict:
temp_data: TempConfig = common_db.first(TempConfig(), default=TempConfig())
result = {
"name" : list(get_config("nickname", [__NAME__]))[0],
"version" : __VERSION__,
"plugins" : len(nonebot.get_loaded_plugins()),
"resources": len(get_loaded_resource_packs()),
"nonebot" : f"{nonebot.__version__}",
"python" : f"{platform.python_implementation()} {platform.python_version()}",
"system" : f"{platform.system()} {platform.release()}",
"runtime" : time.time() - temp_data.data.get("start_time", time.time()), # 运行时间秒数
"bots" : len(nonebot.get_bots())
}
return result

View File

@ -0,0 +1,52 @@
from nonebot import require
from liteyuki.utils.base.resource import get_path
from liteyuki.utils.message.html_tool import template2image
from liteyuki.utils.base.language import get_user_lang
from .api import *
from ...utils.base.ly_typing import T_Bot, T_MessageEvent
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import on_alconna, Alconna, Args, Subcommand, Arparma, UniMessage
status_alc = on_alconna(
aliases={"状态"},
command=Alconna(
"status",
Subcommand(
"memory",
alias={"mem", "m", "内存"},
),
Subcommand(
"process",
alias={"proc", "p", "进程"},
)
),
)
@status_alc.handle()
async def _(event: T_MessageEvent, bot: T_Bot):
ulang = get_user_lang(event.user_id)
if ulang.lang_code in status_card_cache:
image = status_card_cache[ulang.lang_code]
else:
image = await generate_status_card(
bot=await get_bots_data(),
hardware=await get_hardware_data(),
liteyuki=await get_liteyuki_data(),
lang=ulang.lang_code,
bot_id=bot.self_id,
use_cache=True
)
await status_alc.finish(UniMessage.image(raw=image))
@status_alc.assign("memory")
async def _():
print("memory")
@status_alc.assign("process")
async def _():
print("process")

View File

@ -0,0 +1,17 @@
from nonebot.plugin import PluginMetadata
from .api import *
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="联合黑名单(测试中...)",
description="",
usage="",
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
"toggleable" : True,
"default_enable" : True,
}
)

View File

@ -0,0 +1,60 @@
import datetime
import aiohttp
import httpx
import nonebot
from nonebot import require
from nonebot.exception import IgnoredException
from nonebot.message import event_preprocessor
from nonebot_plugin_alconna.typings import Event
require("nonebot_plugin_apscheduler")
from nonebot_plugin_apscheduler import scheduler
blacklist_data: dict[str, set[str]] = {}
blacklist: set[str] = set()
@scheduler.scheduled_job("interval", minutes=10, next_run_time=datetime.datetime.now())
async def update_blacklist():
await request_for_blacklist()
async def request_for_blacklist():
global blacklist
urls = [
"https://cdn.liteyuki.icu/static/ubl/"
]
platforms = [
"qq"
]
for plat in platforms:
for url in urls:
url += f"{plat}.txt"
async with aiohttp.ClientSession() as client:
resp = await client.get(url)
blacklist_data[plat] = set((await resp.text()).splitlines())
blacklist = get_uni_set()
nonebot.logger.info("blacklists updated")
def get_uni_set() -> set:
s = set()
for new_set in blacklist_data.values():
s.update(new_set)
return s
@event_preprocessor
async def pre_handle(event: Event):
try:
user_id = str(event.get_user_id())
except:
return
if user_id in get_uni_set():
raise IgnoredException("UserId in blacklist")

View File

@ -0,0 +1,16 @@
from nonebot.plugin import PluginMetadata
from .profile_manager import *
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="轻雪用户管理",
description="用户管理插件",
usage="",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable" : False,
"default_enable": True,
}
)

View File

@ -0,0 +1,23 @@
representative_timezones_list = [
"Etc/GMT+12", # 国际日期变更线西
"Pacific/Honolulu", # 夏威夷标准时间
"America/Anchorage", # 阿拉斯加标准时间
"America/Los_Angeles", # 美国太平洋标准时间
"America/Denver", # 美国山地标准时间
"America/Chicago", # 美国中部标准时间
"America/New_York", # 美国东部标准时间
"Europe/London", # 英国标准时间
"Europe/Paris", # 中欧标准时间
"Europe/Moscow", # 莫斯科标准时间
"Asia/Dubai", # 阿联酋标准时间
"Asia/Kolkata", # 印度标准时间
"Asia/Shanghai", # 中国标准时间
"Asia/Hong_Kong", # 中国香港标准时间
"Asia/Chongqing", # 中国重庆标准时间
"Asia/Macau", # 中国澳门标准时间
"Asia/Taipei", # 中国台湾标准时间
"Asia/Tokyo", # 日本标准时间
"Australia/Sydney", # 澳大利亚东部标准时间
"Pacific/Auckland" # 新西兰标准时间
]
representative_timezones_list.sort()

View File

@ -0,0 +1,148 @@
from typing import Optional
import pytz
from nonebot import require
from liteyuki.utils.base.data import LiteModel, Database
from liteyuki.utils.base.data_manager import User, user_db, group_db
from liteyuki.utils.base.language import Language, change_user_lang, get_all_lang, get_user_lang
from liteyuki.utils.base.ly_typing import T_Bot, T_MessageEvent
from liteyuki.utils.message.message import MarkdownMessage as md
from .const import representative_timezones_list
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import Alconna, Args, Arparma, Subcommand, on_alconna
profile_alc = on_alconna(
Alconna(
"profile",
Subcommand(
"set",
Args["key", str]["value", str, None],
alias=["s", "设置"],
),
Subcommand(
"get",
Args["key", str],
alias=["g", "查询"],
),
),
aliases={"用户信息"}
)
# json储存
class Profile(LiteModel):
lang: str = "zh-CN"
nickname: str = ""
timezone: str = "Asia/Shanghai"
location: str = ""
@profile_alc.handle()
async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
user: User = user_db.first(User(), "user_id = ?", event.user_id, default=User(user_id=str(event.user_id)))
ulang = get_user_lang(str(event.user_id))
if result.subcommands.get("set"):
if result.subcommands["set"].args.get("value"):
# 对合法性进行校验后设置
r = set_profile(result.args["key"], result.args["value"], str(event.user_id))
if r:
user.profile[result.args["key"]] = result.args["value"]
user_db.save(user) # 数据库保存
await profile_alc.finish(
ulang.get(
"user.profile.set_success",
ATTR=ulang.get(f"user.profile.{result.args['key']}"),
VALUE=result.args["value"]
)
)
else:
await profile_alc.finish(ulang.get("user.profile.set_failed", ATTR=ulang.get(f"user.profile.{result.args['key']}")))
else:
# 未输入值,尝试呼出菜单
menu = get_profile_menu(result.args["key"], ulang)
if menu:
await md.send_md(menu, bot, event=event)
else:
await profile_alc.finish(ulang.get("user.profile.input_value", ATTR=ulang.get(f"user.profile.{result.args['key']}")))
user.profile[result.args["key"]] = result.args["value"]
elif result.subcommands.get("get"):
if result.args["key"] in user.profile:
await profile_alc.finish(user.profile[result.args["key"]])
else:
await profile_alc.finish("无此键值")
else:
profile = Profile(**user.profile)
for k, v in user.profile.items():
profile.__setattr__(k, v)
reply = f"# {ulang.get('user.profile.info')}\n***\n"
hidden_attr = ["id", "TABLE_NAME"]
enter_attr = ["lang", "timezone"]
for key in sorted(profile.dict().keys()):
if key in hidden_attr:
continue
val = profile.dict()[key]
key_text = ulang.get(f"user.profile.{key}")
btn_set = md.btn_cmd(ulang.get("user.profile.edit"), f"profile set {key}",
enter=True if key in enter_attr else False)
reply += (f"\n**{key_text}** **{val}**\n"
f"\n> {ulang.get(f'user.profile.{key}.desc')}"
f"\n> {btn_set} \n\n***\n")
await md.send_md(reply, bot, event=event)
def get_profile_menu(key: str, ulang: Language) -> Optional[str]:
"""获取属性的markdown菜单
Args:
ulang: 用户语言
key: 属性键
Returns:
"""
setting_name = ulang.get(f"user.profile.{key}")
no_menu = ["id", "nickname", "location"]
if key in no_menu:
return None
reply = f"**{setting_name} {ulang.get('user.profile.settings')}**\n***\n"
if key == "lang":
for lang_code, lang_name in get_all_lang().items():
btn_set_lang = md.btn_cmd(f"{lang_name}({lang_code})", f"profile set {key} {lang_code}")
reply += f"\n{btn_set_lang}\n***\n"
elif key == "timezone":
for tz in representative_timezones_list:
btn_set_tz = md.btn_cmd(tz, f"profile set {key} {tz}")
reply += f"{btn_set_tz}\n***\n"
return reply
def set_profile(key: str, value: str, user_id: str) -> bool:
"""设置属性使用if分支对每一个合法性进行检查
Args:
user_id:
key:
value:
Returns:
是否成功设置输入合法性不通过返回False
"""
if key == "lang":
if value in get_all_lang():
change_user_lang(user_id, value)
return True
elif key == "timezone":
if value in pytz.all_timezones:
return True
elif key == "nickname":
return True

View File

@ -0,0 +1,27 @@
from nonebot.plugin import PluginMetadata
from nonebot import get_driver
from .qweather import *
__plugin_meta__ = PluginMetadata(
name="轻雪天气",
description="基于和风天气api的天气插件",
usage="",
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable" : True,
"default_enable": True,
}
)
from ...utils.base.data_manager import set_memory_data
driver = get_driver()
@driver.on_startup
async def _():
# 检查是否为开发者模式
is_dev = await check_key_dev(get_config("weather_key", ""))
set_memory_data("weather.is_dev", is_dev)

View File

@ -0,0 +1,171 @@
import aiohttp
from .qw_models import *
import httpx
from ...utils.base.data_manager import get_memory_data
from ...utils.base.language import Language
dev_url = "https://devapi.qweather.com/" # 开发HBa
com_url = "https://api.qweather.com/" # 正式环境
def get_qw_lang(lang: str) -> str:
if lang in ["zh-HK", "zh-TW"]:
return "zh-hant"
elif lang.startswith("zh"):
return "zh"
elif lang.startswith("en"):
return "en"
else:
return lang
async def check_key_dev(key: str) -> bool:
url = "https://api.qweather.com/v7/weather/now?"
params = {
"location": "101010100",
"key" : key,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return (resp.json()).get("code") != "200" # 查询不到付费数据为开发版
def get_local_data(ulang_code: str) -> dict:
"""
获取本地化数据
Args:
ulang_code:
Returns:
"""
ulang = Language(ulang_code)
return {
"monday" : ulang.get("weather.monday"),
"tuesday" : ulang.get("weather.tuesday"),
"wednesday": ulang.get("weather.wednesday"),
"thursday" : ulang.get("weather.thursday"),
"friday" : ulang.get("weather.friday"),
"saturday" : ulang.get("weather.saturday"),
"sunday" : ulang.get("weather.sunday"),
"today" : ulang.get("weather.today"),
"tomorrow" : ulang.get("weather.tomorrow"),
"day" : ulang.get("weather.day"),
"night" : ulang.get("weather.night"),
"no_aqi" : ulang.get("weather.no_aqi"),
}
async def city_lookup(
location: str,
key: str,
adm: str = "",
number: int = 20,
lang: str = "zh",
) -> CityLookup:
"""
通过关键字搜索城市信息
Args:
location:
key:
adm:
number:
lang: 可传入标准i18n语言代码如zh-CN、en-US等
Returns:
"""
url = "https://geoapi.qweather.com/v2/city/lookup?"
params = {
"location": location,
"adm" : adm,
"number" : number,
"key" : key,
"lang" : lang,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return CityLookup.parse_obj(resp.json())
async def get_weather_now(
key: str,
location: str,
lang: str = "zh",
unit: str = "m",
dev: bool = get_memory_data("is_dev", True),
) -> dict:
url_path = "v7/weather/now?"
url = dev_url + url_path if dev else com_url + url_path
params = {
"location": location,
"key" : key,
"lang" : lang,
"unit" : unit,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return resp.json()
async def get_weather_daily(
key: str,
location: str,
lang: str = "zh",
unit: str = "m",
dev: bool = get_memory_data("is_dev", True),
) -> dict:
url_path = "v7/weather/%dd?" % (7 if dev else 30)
url = dev_url + url_path if dev else com_url + url_path
params = {
"location": location,
"key" : key,
"lang" : lang,
"unit" : unit,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return resp.json()
async def get_weather_hourly(
key: str,
location: str,
lang: str = "zh",
unit: str = "m",
dev: bool = get_memory_data("is_dev", True),
) -> dict:
url_path = "v7/weather/%dh?" % (24 if dev else 168)
url = dev_url + url_path if dev else com_url + url_path
params = {
"location": location,
"key" : key,
"lang" : lang,
"unit" : unit,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return resp.json()
async def get_airquality(
key: str,
location: str,
lang: str,
pollutant: bool = False,
station: bool = False,
dev: bool = get_memory_data("is_dev", True),
) -> dict:
url_path = f"airquality/v1/now/{location}?"
url = dev_url + url_path if dev else com_url + url_path
params = {
"key" : key,
"lang" : lang,
"pollutant": pollutant,
"station" : station,
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return resp.json()

View File

@ -0,0 +1,62 @@
from liteyuki.utils.base.data import LiteModel
class Location(LiteModel):
name: str = ""
id: str = ""
lat: str = ""
lon: str = ""
adm2: str = ""
adm1: str = ""
country: str = ""
tz: str = ""
utcOffset: str = ""
isDst: str = ""
type: str = ""
rank: str = ""
fxLink: str = ""
sources: str = ""
license: str = ""
class CityLookup(LiteModel):
code: str = ""
location: list[Location] = [Location()]
class Now(LiteModel):
obsTime: str = ""
temp: str = ""
feelsLike: str = ""
icon: str = ""
text: str = ""
wind360: str = ""
windDir: str = ""
windScale: str = ""
windSpeed: str = ""
humidity: str = ""
precip: str = ""
pressure: str = ""
vis: str = ""
cloud: str = ""
dew: str = ""
sources: str = ""
license: str = ""
class WeatherNow(LiteModel):
code: str = ""
updateTime: str = ""
fxLink: str = ""
now: Now = Now()
class Daily(LiteModel):
pass
class WeatherDaily(LiteModel):
code: str = ""
updateTime: str = ""
fxLink: str = ""
daily: list[str] = []

View File

@ -0,0 +1,95 @@
from nonebot import require, on_endswith
from nonebot.adapters.onebot.v11 import MessageSegment
from nonebot.internal.matcher import Matcher
from liteyuki.utils.base.config import get_config
from liteyuki.utils.base.ly_typing import T_MessageEvent
from .qw_api import *
from liteyuki.utils.base.data_manager import User, user_db
from liteyuki.utils.base.language import Language, get_user_lang
from liteyuki.utils.base.resource import get_path
from liteyuki.utils.message.html_tool import template2image
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import on_alconna, Alconna, Args, MultiVar, Arparma
@on_alconna(
aliases={"天气"},
command=Alconna(
"weather",
Args["keywords", MultiVar(str), []],
),
).handle()
async def _(result: Arparma, event: T_MessageEvent, matcher: Matcher):
"""await alconna.send("weather", city)"""
kws = result.main_args.get("keywords")
image = await get_weather_now_card(matcher, event, kws)
await matcher.finish(MessageSegment.image(image))
@on_endswith(("天气", "weather")).handle()
async def _(event: T_MessageEvent, matcher: Matcher):
"""await alconna.send("weather", city)"""
kws = event.message.extract_plain_text()
image = await get_weather_now_card(matcher, event, [kws.replace("天气", "").replace("weather", "")], False)
await matcher.finish(MessageSegment.image(image))
async def get_weather_now_card(matcher: Matcher, event: T_MessageEvent, keyword: list[str], tip: bool = True):
ulang = get_user_lang(event.user_id)
qw_lang = get_qw_lang(ulang.lang_code)
key = get_config("weather_key")
is_dev = get_memory_data("weather.is_dev", True)
user: User = user_db.first(User(), "user_id = ?", event.user_id, default=User())
# params
unit = user.profile.get("unit", "m")
stored_location = user.profile.get("location", None)
if not key:
await matcher.finish(ulang.get("weather.no_key") if tip else None)
if keyword:
if len(keyword) >= 2:
adm = keyword[0]
city = keyword[-1]
else:
adm = ""
city = keyword[0]
city_info = await city_lookup(city, key, adm=adm, lang=qw_lang)
city_name = " ".join(keyword)
else:
if not stored_location:
await matcher.finish(ulang.get("liteyuki.invalid_command", TEXT="location") if tip else None)
city_info = await city_lookup(stored_location, key, lang=qw_lang)
city_name = stored_location
if city_info.code == "200":
location_data = city_info.location[0]
else:
await matcher.finish(ulang.get("weather.city_not_found", CITY=city_name) if tip else None)
weather_now = await get_weather_now(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev)
weather_daily = await get_weather_daily(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev)
weather_hourly = await get_weather_hourly(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev)
aqi = await get_airquality(key, location_data.id, lang=qw_lang, dev=is_dev)
image = await template2image(
template=get_path("templates/weather_now.html", abs_path=True),
templates={
"data": {
"params" : {
"unit": unit,
"lang": ulang.lang_code,
},
"weatherNow" : weather_now,
"weatherDaily" : weather_daily,
"weatherHourly": weather_hourly,
"aqi" : aqi,
"location" : location_data.dump(),
"localization" : get_local_data(ulang.lang_code)
}
},
debug=True,
wait=1
)
return image

View File

@ -0,0 +1,161 @@
import datetime
import time
import aiohttp
from nonebot import require
from nonebot.plugin import PluginMetadata
from liteyuki.utils.base.config import get_config
from liteyuki.utils.base.data import Database, LiteModel
from liteyuki.utils.base.resource import get_path
from liteyuki.utils.message.html_tool import template2image
require("nonebot_plugin_alconna")
require("nonebot_plugin_apscheduler")
from nonebot_plugin_apscheduler import scheduler
from nonebot_plugin_alconna import Alconna, AlconnaResult, CommandResult, Subcommand, UniMessage, on_alconna, Args
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="签名服务器状态",
description="适用于ntqq的签名状态查看",
usage="",
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable" : True,
"default_enable": True,
}
)
SIGN_COUNT_URLS: dict[str, str] = get_config("sign_count_urls", None)
SIGN_COUNT_DURATION = get_config("sign_count_duration", 10)
class SignCount(LiteModel):
TABLE_NAME: str = "sign_count"
time: float = 0.0
count: int = 0
sid: str = ""
sign_db = Database("data/liteyuki/ntqq_sign.ldb")
sign_db.auto_migrate(SignCount())
sign_status = on_alconna(Alconna(
"sign",
Subcommand(
"chart",
Args["limit", int, 10000]
),
Subcommand(
"count"
),
Subcommand(
"data"
)
))
cache_img: bytes = None
@sign_status.assign("count")
async def _():
reply = "Current sign count:"
for name, count in (await get_now_sign()).items():
reply += f"\n{name}: {count[1]}"
await sign_status.send(reply)
@sign_status.assign("data")
async def _():
query_stamp = [1, 5, 10, 15]
reply = "Count from last " + ", ".join([str(i) for i in query_stamp]) + "mins"
for name, url in SIGN_COUNT_URLS.items():
count_data = []
for stamp in query_stamp:
count_rows = sign_db.all(SignCount(), "sid = ? and time > ?", url, time.time() - 60 * stamp)
if len(count_rows) < 2:
count_data.append(-1)
else:
count_data.append(count_rows[-1].count - count_rows[0].count)
reply += f"\n{name}: " + ", ".join([str(i) for i in count_data])
await sign_status.send(reply)
@sign_status.assign("chart")
async def _(arp: CommandResult = AlconnaResult()):
limit = arp.result.subcommands.get("chart").args.get("limit")
if limit == 10000:
if cache_img:
await sign_status.send(UniMessage.image(raw=cache_img))
return
img = await generate_chart(limit)
await sign_status.send(UniMessage.image(raw=img))
@scheduler.scheduled_job("interval", seconds=SIGN_COUNT_DURATION, next_run_time=datetime.datetime.now())
async def update_sign_count():
global cache_img
if not SIGN_COUNT_URLS:
return
data = await get_now_sign()
for name, count in data.items():
await save_sign_count(count[0], count[1], SIGN_COUNT_URLS[name])
cache_img = await generate_chart(10000)
async def get_now_sign() -> dict[str, tuple[float, int]]:
"""
Get the sign count and the time of the latest sign
Returns:
tuple[float, int] | None: (time, count)
"""
data = {}
now = time.time()
async with aiohttp.ClientSession() as client:
for name, url in SIGN_COUNT_URLS.items():
async with client.get(url) as resp:
count = (await resp.json())["count"]
data[name] = (now, count)
return data
async def save_sign_count(timestamp: float, count: int, sid: str):
"""
Save the sign count to the database
Args:
sid: the sign id use url as the id
count:
timestamp (float): the time of the sign count (int): the count of the sign
"""
sign_db.save(SignCount(time=timestamp, count=count, sid=sid))
async def generate_chart(limit):
data = []
for name, url in SIGN_COUNT_URLS.items():
count_rows = sign_db.all(SignCount(), "sid = ? ORDER BY id DESC LIMIT ?", url, limit)
count_rows.reverse()
data.append(
{
"name" : name,
# "data": [[row.time, row.count] for row in count_rows]
"times" : [row.time for row in count_rows],
"counts": [row.count for row in count_rows]
}
)
print(len(count_rows))
img = await template2image(
template=get_path("templates/sign_status.html", debug=True),
templates={
"data": data
},
debug=True
)
return img

View File

@ -0,0 +1,3 @@
name: Sign Status
description: for Lagrange
version: 2024.4.26

View File

@ -0,0 +1,4 @@
.sign-chart {
height: 400px;
background-color: rgba(255, 255, 255, 0.7);
}

View File

@ -0,0 +1,75 @@
// 数据类型声明
// import * as echarts from 'echarts';
let data = JSON.parse(document.getElementById("data").innerText) // object
const signChartDivTemplate = document.importNode(document.getElementById("sign-chart-template").content, true)
data.forEach((item) => {
let signChartDiv = signChartDivTemplate.cloneNode(true)
let chartID = item["name"]
// 初始化ECharts实例
// 设置id
signChartDiv.querySelector(".sign-chart").id = chartID
document.body.appendChild(signChartDiv)
let signChart = echarts.init(document.getElementById(chartID))
let timeCount = []
item["counts"].forEach((count, index) => {
// 计算平均值index - 1的count + index的count + index + 1的count /3
if (index > 0) {
timeCount.push((item["counts"][index] - item["counts"][index - 1]))
}
})
console.log(timeCount)
signChart.setOption(
{
animation: false,
title: {
text: item["name"],
textStyle: {
color: '#000000' // 设置标题文本颜色为红色
}
},
xAxis: {
type: 'category',
data: item["times"].map(timestampToTime),
},
yAxis: [
{
type: 'value',
min: Math.min(...item["counts"]),
},
{
type: 'value',
min: Math.min(...timeCount),
}
],
series: [
{
data: item["counts"],
type: 'line',
yAxisIndex: 0
},
{
data: timeCount,
type: 'line',
yAxisIndex: 1
}
]
}
)
})
function timestampToTime(timestamp) {
let date = new Date(timestamp * 1000)
let Y = date.getFullYear() + '-'
let M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-'
let D = date.getDate() + ' '
let h = date.getHours() + ':'
let m = date.getMinutes() + ':'
let s = date.getSeconds()
return M + D + h + m + s
}

View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="zh" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<title>Liteyuki Status</title>
<link rel="stylesheet" href="./css/card.css">
<link rel="stylesheet" href="./css/fonts.css">
<link rel="stylesheet" href="./css/sign_status.css">
</head>
<body>
<template id="sign-chart-template">
<div class="info-box sign-chart">
</div>
</template>
<div class="data-storage" id="data">{{ data | tojson }}</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.5.0/echarts.min.js"></script>
<script src="./js/sign_status.js"></script>
<script src="./js/card.js"></script>
</body>

View File

@ -0,0 +1,12 @@
weather.monday=Mon
weather.tuesday=Tue
weather.wednesday=Wed
weather.thursday=Thu
weather.friday=Fri
weather.saturday=Sat
weather.sunday=Sun
weather.day=Day
weather.night=Night
weather.today=Today
weather.tomorrow=Tomorrow
weather.no_aqi=No AQI data

Some files were not shown because too many files have changed in this diff Show More