mirror of
https://github.com/TriM-Organization/LiteyukiBot-TriM.git
synced 2024-11-14 19:37:44 +08:00
🔀手动Merge轻雪主仓库a77f97f
This commit is contained in:
parent
4cc2ae61db
commit
f8b57bfe9a
53
.github/ISSUE_TEMPLATE/问题反馈.md
vendored
53
.github/ISSUE_TEMPLATE/问题反馈.md
vendored
@ -1,53 +0,0 @@
|
|||||||
---
|
|
||||||
name: 问题反馈
|
|
||||||
about: 反馈你在使用轻雪中遇到的问题
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 问题反馈
|
|
||||||
|
|
||||||
## **请确保**
|
|
||||||
|
|
||||||
- 已认真阅读[文档]("https://bot.liteyuki.icu"),该问题不是文档提及的或你自己操作不当造成的
|
|
||||||
- 你的问题是在最新版本的代码上测试的
|
|
||||||
- 请勿重复提交相同或类似的issue
|
|
||||||
|
|
||||||
|
|
||||||
## **描述问题**
|
|
||||||
|
|
||||||
请在此简单描述问题
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## **如何复现**
|
|
||||||
|
|
||||||
请阐述一下如何重现这个问题
|
|
||||||
### 预期
|
|
||||||
|
|
||||||
描述你期望发生的事情
|
|
||||||
|
|
||||||
### 实际
|
|
||||||
|
|
||||||
描述实际发生的事情
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## **日志或截图**
|
|
||||||
```
|
|
||||||
日志内容
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## **设备信息**
|
|
||||||
- **系统**: [例如 Ubuntu 22.04]
|
|
||||||
- **CPU**: [例如 Intel i7-7700K]
|
|
||||||
- **内存**: [例如 16GB]
|
|
||||||
- **Python**: [例如CPython 3.10.7]
|
|
||||||
|
|
||||||
|
|
||||||
**补充内容**
|
|
||||||
|
|
||||||
可选,推荐提供`pip freeze`的输出,以及其他相关信息,以及你的建议
|
|
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
@ -1,11 +0,0 @@
|
|||||||
# To get started with Dependabot version updates, you'll need to specify which
|
|
||||||
# package ecosystems to update and where the package manifests are located.
|
|
||||||
# Please see the documentation for all configuration options:
|
|
||||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
|
||||||
|
|
||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "" # See documentation for possible values
|
|
||||||
directory: "/" # Location of package manifests
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
44
.github/workflows/ISSUE_TEMPLATE.md
vendored
44
.github/workflows/ISSUE_TEMPLATE.md
vendored
@ -1,44 +0,0 @@
|
|||||||
# 问题反馈
|
|
||||||
|
|
||||||
## **请确保**
|
|
||||||
|
|
||||||
- 已认真阅读[文档]("https://bot.liteyuki.icu"),该问题不是文档提及的或你自己操作不当造成的
|
|
||||||
- 你的问题是在最新版本的代码上测试的
|
|
||||||
- 请勿重复提交相同或类似的issue
|
|
||||||
|
|
||||||
|
|
||||||
## **描述问题**
|
|
||||||
|
|
||||||
请在此简单描述问题
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## **如何复现**
|
|
||||||
|
|
||||||
请阐述一下如何重现这个问题
|
|
||||||
### 预期
|
|
||||||
|
|
||||||
描述你期望发生的事情
|
|
||||||
|
|
||||||
### 实际
|
|
||||||
|
|
||||||
描述实际发生的事情
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## **日志或截图**
|
|
||||||
```
|
|
||||||
日志内容
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## **设备信息**
|
|
||||||
- **系统**: [例如 Ubuntu 22.04]
|
|
||||||
- **CPU**: [例如 Intel i7-7700K]
|
|
||||||
- **内存**: [例如 16GB]
|
|
||||||
- **Python**: [例如CPython 3.10.7]
|
|
||||||
|
|
||||||
|
|
||||||
**补充内容**
|
|
||||||
|
|
||||||
可选,推荐提供`pip freeze`的输出,以及其他相关信息,以及你的建议
|
|
49
.github/workflows/deploy-docs.yml
vendored
49
.github/workflows/deploy-docs.yml
vendored
@ -1,49 +0,0 @@
|
|||||||
|
|
||||||
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
|
|
27
.gitignore
vendored
27
.gitignore
vendored
@ -14,6 +14,9 @@ __pycache__/
|
|||||||
*.pyd
|
*.pyd
|
||||||
*.pyw
|
*.pyw
|
||||||
/plugins/
|
/plugins/
|
||||||
|
|
||||||
|
#config
|
||||||
|
/config/
|
||||||
_config.yml
|
_config.yml
|
||||||
config.yml
|
config.yml
|
||||||
config.example.yml
|
config.example.yml
|
||||||
@ -29,11 +32,11 @@ src/nonebot_plugins/dislink_plugin_ccnd
|
|||||||
.github
|
.github
|
||||||
# pyproject.toml
|
# pyproject.toml
|
||||||
|
|
||||||
test.py
|
# mypy
|
||||||
line_count.py
|
|
||||||
mypy.ini
|
mypy.ini
|
||||||
|
|
||||||
# nuitka
|
# nuitka
|
||||||
|
compile.bat
|
||||||
main.build/
|
main.build/
|
||||||
main.dist/
|
main.dist/
|
||||||
main.exe
|
main.exe
|
||||||
@ -46,3 +49,23 @@ prompt.txt
|
|||||||
# js
|
# js
|
||||||
**/echarts.js
|
**/echarts.js
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build
|
||||||
|
dist
|
||||||
|
|
||||||
|
doc
|
||||||
|
|
||||||
|
mkdoc2.py
|
||||||
|
result.json
|
||||||
|
|
||||||
|
# litedoc
|
||||||
|
docs/zh/dev/api
|
||||||
|
docs/en/dev/api
|
||||||
|
mkdoc.bat
|
||||||
|
|
||||||
|
# vitepress
|
||||||
|
docs/.vitepress/dist/
|
||||||
|
docs/.vitepress/cache
|
||||||
|
docs/.vitepress/.temp
|
@ -1,4 +1,4 @@
|
|||||||
FROM python:3.11-bullseye
|
FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/python:3.10-slim-bullseye
|
||||||
|
|
||||||
ENV TZ Asia/Shanghai
|
ENV TZ Asia/Shanghai
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
[![][Python3.10+]][python-link]
|
[![][Python3.10+]][python-link]
|
||||||
[![][Usage]][usage-link]
|
[![][Usage]][usage-link]
|
||||||
|
|
||||||
- 基于[Nonebot2](https://github.com/nonebot/nonebot2),有良好的生态支持
|
- 原生支持与任意 `Python`Bot 框架互联,有良好的生态支持
|
||||||
- 开箱即用,无需复杂配置
|
- 开箱即用,无需复杂配置
|
||||||
- 集成包管理器,支持一键安装插件
|
- 集成包管理器,支持一键安装插件
|
||||||
- 支持 OneBot 标准通信但不限于此
|
- 支持 OneBot 标准通信但不限于此
|
||||||
@ -27,10 +27,7 @@
|
|||||||
|
|
||||||
### 感谢
|
### 感谢
|
||||||
|
|
||||||
- 感谢[NoneBot2](https://nonebot.dev)提供的框架支持
|
- 感谢所有贡献者们
|
||||||
- 感谢[nonebot-plugin-htmlrender](https://github.com/kexue-z/nonebot-plugin-htmlrender)提供的渲染功能
|
|
||||||
- 讨厌上述项目提供的**基于 Chromium 浏览器**的 HTML 渲染功能,就功能本身而言很好,但我讨厌一声不响在我电脑里装浏览器这个行为,虽然这并不妨碍我感谢之 ——金羿
|
|
||||||
- 感谢[nonebot-plugin-alconna](https://github.com/ArcletProject/nonebot-plugin-alconna)提供的命令解析功能
|
|
||||||
- 十分感谢[神羽 SnowyKami](https://github.com/snowykami)提供的技术指导和服务器资源
|
- 十分感谢[神羽 SnowyKami](https://github.com/snowykami)提供的技术指导和服务器资源
|
||||||
- 特别感谢[云裳工作室](https://doc.ysmcc.cn/doc/1/)提供的服务器挂载
|
- 特别感谢[云裳工作室](https://doc.ysmcc.cn/doc/1/)提供的服务器挂载
|
||||||
- 由衷感谢我在学习生活中遇到的所有朋友们,你们身为我生命中的一处景色,不断地推进我此生的进程。
|
- 由衷感谢我在学习生活中遇到的所有朋友们,你们身为我生命中的一处景色,不断地推进我此生的进程。
|
||||||
|
31
clean_pycache.py
Normal file
31
clean_pycache.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import shutil
|
||||||
|
import os
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.progress import track
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
with console.status("正在搜寻可清理之文件"):
|
||||||
|
egg_info: list = []
|
||||||
|
for file in os.listdir():
|
||||||
|
if file.endswith(".egg-info"):
|
||||||
|
egg_info.append(file)
|
||||||
|
console.print(file)
|
||||||
|
|
||||||
|
pycache: list = []
|
||||||
|
for dirpath, dirnames, filenames in os.walk("./"):
|
||||||
|
for dirname in dirnames:
|
||||||
|
if dirname == "__pycache__":
|
||||||
|
pycache.append(fn := os.path.join(dirpath, dirname))
|
||||||
|
console.print(fn)
|
||||||
|
for file in track(
|
||||||
|
["build", "dist", "logs", "log", *egg_info, *pycache], description="正在清理"
|
||||||
|
):
|
||||||
|
if os.path.isdir(file) and os.access(file, os.W_OK):
|
||||||
|
shutil.rmtree(file)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
@ -1,11 +1,12 @@
|
|||||||
from liteyuki.bot import (
|
from liteyuki.bot import (
|
||||||
LiteyukiBot,
|
LiteyukiBot,
|
||||||
get_bot
|
get_bot,
|
||||||
|
get_config,
|
||||||
|
get_config_with_compat
|
||||||
)
|
)
|
||||||
|
|
||||||
from liteyuki.comm import (
|
from liteyuki.comm import (
|
||||||
Channel,
|
Channel,
|
||||||
chan,
|
|
||||||
Event
|
Event
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -15,7 +16,27 @@ from liteyuki.plugin import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from liteyuki.log import (
|
from liteyuki.log import (
|
||||||
logger,
|
init_log,
|
||||||
init_log
|
logger
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"LiteyukiBot",
|
||||||
|
"get_bot",
|
||||||
|
"get_config",
|
||||||
|
"get_config_with_compat",
|
||||||
|
"Channel",
|
||||||
|
"Event",
|
||||||
|
"load_plugin",
|
||||||
|
"load_plugins",
|
||||||
|
"init_log",
|
||||||
|
"logger",
|
||||||
|
]
|
||||||
|
|
||||||
|
__version__ = "6.3.9" # 测试版本号
|
||||||
|
# 6.3.9
|
||||||
|
# 更改了on语法
|
||||||
|
|
||||||
|
# 6.3.8
|
||||||
|
# 1. 初步添加对聊天的支持
|
||||||
|
# 2. 优化了通道的性能
|
||||||
|
@ -1,103 +1,102 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import atexit
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from watchdog.events import FileSystemEventHandler
|
from liteyuki.bot.lifespan import LIFESPAN_FUNC, Lifespan, PROCESS_LIFESPAN_FUNC
|
||||||
from watchdog.observers import Observer
|
from liteyuki.comm.channel import get_channel
|
||||||
|
|
||||||
from liteyuki.bot.lifespan import LIFESPAN_FUNC, Lifespan
|
|
||||||
from liteyuki.core import IS_MAIN_PROCESS
|
|
||||||
from liteyuki.core.manager import ProcessManager
|
from liteyuki.core.manager import ProcessManager
|
||||||
from liteyuki.core.spawn_process import mb_run, nb_run
|
|
||||||
from liteyuki.log import init_log, logger
|
from liteyuki.log import init_log, logger
|
||||||
from liteyuki.plugin import load_plugins
|
from liteyuki.plugin import load_plugin
|
||||||
|
from liteyuki.utils import IS_MAIN_PROCESS
|
||||||
|
|
||||||
__all__ = ["LiteyukiBot", "get_bot"]
|
__all__ = [
|
||||||
|
"LiteyukiBot",
|
||||||
|
"get_bot",
|
||||||
|
"get_config",
|
||||||
|
"get_config_with_compat",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class LiteyukiBot:
|
class LiteyukiBot:
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, **kwargs) -> None:
|
||||||
|
"""
|
||||||
|
初始化轻雪实例
|
||||||
|
Args:
|
||||||
|
**kwargs: 配置
|
||||||
|
"""
|
||||||
|
"""常规操作"""
|
||||||
|
print_logo()
|
||||||
global _BOT_INSTANCE
|
global _BOT_INSTANCE
|
||||||
_BOT_INSTANCE = self # 引用
|
_BOT_INSTANCE = self # 引用
|
||||||
|
|
||||||
|
"""配置"""
|
||||||
self.config: dict[str, Any] = kwargs
|
self.config: dict[str, Any] = kwargs
|
||||||
|
|
||||||
|
"""初始化"""
|
||||||
self.init(**self.config) # 初始化
|
self.init(**self.config) # 初始化
|
||||||
|
logger.info("尹灵温 正在初始化…")
|
||||||
|
|
||||||
self.lifespan: Lifespan = Lifespan()
|
"""生命周期管理"""
|
||||||
|
self.lifespan = Lifespan()
|
||||||
|
self.process_manager: ProcessManager = ProcessManager(lifespan=self.lifespan)
|
||||||
|
|
||||||
self.process_manager: ProcessManager = ProcessManager(bot=self)
|
"""事件循环"""
|
||||||
self.loop = asyncio.new_event_loop()
|
self.loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(self.loop)
|
asyncio.set_event_loop(self.loop)
|
||||||
self.loop_thread = threading.Thread(target=self.loop.run_forever, daemon=True)
|
self.stop_event = threading.Event()
|
||||||
self.call_restart_count = 0
|
self.call_restart_count = 0
|
||||||
|
|
||||||
|
"""加载插件加载器"""
|
||||||
|
load_plugin("liteyuki.plugins.plugin_loader") # 加载轻雪插件
|
||||||
|
|
||||||
|
async def _run(self):
|
||||||
|
"""
|
||||||
|
启动逻辑
|
||||||
|
"""
|
||||||
|
await self.lifespan.before_start() # 启动前钩子
|
||||||
|
await self.lifespan.after_start() # 启动后钩子
|
||||||
|
await self.keep_alive()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
load_plugins("liteyuki/plugins") # 加载轻雪插件
|
"""
|
||||||
|
外部启动接口
|
||||||
|
"""
|
||||||
|
self.process_manager.start_all()
|
||||||
|
try:
|
||||||
|
asyncio.run(self._run())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.opt(colors=True).info("<y>尹灵温 关闭中…</y>")
|
||||||
|
self.stop()
|
||||||
|
logger.opt(colors=True).info("<y>尹灵温 已关停</y>")
|
||||||
|
|
||||||
self.loop_thread.start() # 启动事件循环
|
async def keep_alive(self):
|
||||||
asyncio.run(self.lifespan.before_start()) # 启动前钩子
|
"""
|
||||||
|
保持轻雪运行
|
||||||
self.process_manager.add_target("nonebot", nb_run, **self.config)
|
"""
|
||||||
self.process_manager.start("nonebot")
|
logger.info("尹灵温 持续运行中…")
|
||||||
|
try:
|
||||||
self.process_manager.add_target("melobot", mb_run, **self.config)
|
while not self.stop_event.is_set():
|
||||||
self.process_manager.start("melobot")
|
await asyncio.sleep(0.1)
|
||||||
|
except Exception:
|
||||||
asyncio.run(self.lifespan.after_start()) # 启动后钩子
|
logger.info("尹灵温 现退停…")
|
||||||
|
self.stop()
|
||||||
self.start_watcher() # 启动文件监视器
|
|
||||||
|
|
||||||
def start_watcher(self):
|
|
||||||
if self.config.get("debug", False):
|
|
||||||
|
|
||||||
src_directories = (
|
|
||||||
"liteyuki",
|
|
||||||
"src/liteyuki_main",
|
|
||||||
"src/liteyuki_plugins",
|
|
||||||
"src/nonebot_plugins",
|
|
||||||
"src/utils",
|
|
||||||
)
|
|
||||||
src_excludes_extensions = ("pyc",)
|
|
||||||
|
|
||||||
logger.debug("轻雪重载 已启用,正在加载文件修改监测……")
|
|
||||||
restart = self.restart_process
|
|
||||||
|
|
||||||
class CodeModifiedHandler(FileSystemEventHandler):
|
|
||||||
"""
|
|
||||||
Handler for code file changes
|
|
||||||
"""
|
|
||||||
|
|
||||||
def on_modified(self, event):
|
|
||||||
if (
|
|
||||||
event.src_path.endswith(src_excludes_extensions)
|
|
||||||
or event.is_directory
|
|
||||||
or "__pycache__" in event.src_path
|
|
||||||
):
|
|
||||||
return
|
|
||||||
logger.info(f"文件 {event.src_path} 已修改,机器人自动重启……")
|
|
||||||
restart()
|
|
||||||
|
|
||||||
code_modified_handler = CodeModifiedHandler()
|
|
||||||
|
|
||||||
observer = Observer()
|
|
||||||
for directory in src_directories:
|
|
||||||
observer.schedule(code_modified_handler, directory, recursive=True)
|
|
||||||
observer.start()
|
|
||||||
|
|
||||||
def restart(self, delay: int = 0):
|
def restart(self, delay: int = 0):
|
||||||
"""
|
"""
|
||||||
重启轻雪本体
|
重启轻雪本体
|
||||||
Returns:
|
Args:
|
||||||
|
delay ([`int`](https%3A//docs.python.org/3/library/functions.html#int), optional): 延迟重启时间. Defaults to 0.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.call_restart_count < 1:
|
if self.call_restart_count < 1:
|
||||||
executable = sys.executable
|
executable = sys.executable
|
||||||
args = sys.argv
|
args = sys.argv
|
||||||
logger.info("正在重启 尹灵温...")
|
logger.info("正在重启 尹灵温机器人框架")
|
||||||
time.sleep(delay)
|
time.sleep(delay)
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
cmd = "start"
|
cmd = "start"
|
||||||
@ -110,7 +109,9 @@ class LiteyukiBot:
|
|||||||
self.process_manager.terminate_all()
|
self.process_manager.terminate_all()
|
||||||
# 进程退出后重启
|
# 进程退出后重启
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=os.system, args=(f"{cmd} {executable} {' '.join(args)}",)
|
target=os.system,
|
||||||
|
args=(f"{cmd} {executable} {' '.join(args)}",),
|
||||||
|
daemon=True,
|
||||||
).start()
|
).start()
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
self.call_restart_count += 1
|
self.call_restart_count += 1
|
||||||
@ -119,44 +120,46 @@ class LiteyukiBot:
|
|||||||
"""
|
"""
|
||||||
停止轻雪
|
停止轻雪
|
||||||
Args:
|
Args:
|
||||||
name: 进程名称, 默认为None, 所有进程
|
name ([`Optional`](https%3A//docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https%3A//docs.python.org/3/library/stdtypes.html#str)]): 进程名. Defaults to None.
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
logger.info("Stopping LiteyukiBot...")
|
if name is not None:
|
||||||
|
chan_active = get_channel(f"{name}-active")
|
||||||
self.loop.create_task(self.lifespan.before_shutdown()) # 重启前钩子
|
chan_active.send(1)
|
||||||
self.loop.create_task(self.lifespan.before_shutdown()) # 停止前钩子
|
|
||||||
|
|
||||||
if name:
|
|
||||||
self.process_manager.terminate(name)
|
|
||||||
else:
|
else:
|
||||||
self.process_manager.terminate_all()
|
for process_name in self.process_manager.processes:
|
||||||
|
chan_active = get_channel(f"{process_name}-active")
|
||||||
|
chan_active.send(1)
|
||||||
|
|
||||||
def init(self, *args, **kwargs):
|
def init(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
初始化轻雪, 自动调用
|
初始化轻雪, 自动调用
|
||||||
Returns:
|
Args:
|
||||||
|
*args: 参数
|
||||||
|
**kwargs: 关键字参数
|
||||||
"""
|
"""
|
||||||
self.init_config()
|
|
||||||
self.init_logger()
|
self.init_logger()
|
||||||
|
|
||||||
def init_logger(self):
|
def init_logger(self):
|
||||||
# 修改nonebot的日志配置
|
"""
|
||||||
|
初始化日志
|
||||||
|
"""
|
||||||
init_log(config=self.config)
|
init_log(config=self.config)
|
||||||
|
|
||||||
def init_config(self):
|
def stop(self):
|
||||||
pass
|
"""
|
||||||
|
停止轻雪
|
||||||
|
"""
|
||||||
|
self.process_manager.terminate_all()
|
||||||
|
self.stop_event.set()
|
||||||
|
|
||||||
def on_before_start(self, func: LIFESPAN_FUNC):
|
def on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||||
"""
|
"""
|
||||||
注册启动前的函数
|
注册启动前的函数
|
||||||
Args:
|
Args:
|
||||||
func:
|
func ([`LIFESPAN_FUNC`](./lifespan#var-lifespan-func)): 生命周期函数
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
[`LIFESPAN_FUNC`](./lifespan#var-lifespan-func): 生命周期函数
|
||||||
"""
|
"""
|
||||||
return self.lifespan.on_before_start(func)
|
return self.lifespan.on_before_start(func)
|
||||||
|
|
||||||
@ -164,81 +167,128 @@ class LiteyukiBot:
|
|||||||
"""
|
"""
|
||||||
注册启动后的函数
|
注册启动后的函数
|
||||||
Args:
|
Args:
|
||||||
func:
|
func ([`LIFESPAN_FUNC`](./lifespan#var-lifespan-func)): 生命周期函数
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
[`LIFESPAN_FUNC`](./lifespan#var-lifespan-func): 生命周期函数
|
||||||
"""
|
"""
|
||||||
return self.lifespan.on_after_start(func)
|
return self.lifespan.on_after_start(func)
|
||||||
|
|
||||||
def on_before_shutdown(self, func: LIFESPAN_FUNC):
|
|
||||||
"""
|
|
||||||
注册停止前的函数,为子进程停止时调用
|
|
||||||
Args:
|
|
||||||
func:
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
return self.lifespan.on_before_shutdown(func)
|
|
||||||
|
|
||||||
def on_after_shutdown(self, func: LIFESPAN_FUNC):
|
def on_after_shutdown(self, func: LIFESPAN_FUNC):
|
||||||
"""
|
"""
|
||||||
注册停止后的函数:未实现
|
注册停止后的函数:未实现
|
||||||
Args:
|
Args:
|
||||||
func:
|
func ([`LIFESPAN_FUNC`](./lifespan#var-lifespan-func)): 生命周期函数
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
[`LIFESPAN_FUNC`](./lifespan#var-lifespan-func): 生命周期函数
|
||||||
"""
|
"""
|
||||||
return self.lifespan.on_after_shutdown(func)
|
return self.lifespan.on_after_shutdown(func)
|
||||||
|
|
||||||
def on_before_restart(self, func: LIFESPAN_FUNC):
|
def on_before_process_shutdown(self, func: PROCESS_LIFESPAN_FUNC):
|
||||||
"""
|
"""
|
||||||
注册重启前的函数,为子进程重启时调用
|
注册进程停止前的函数,为子进程停止时调用
|
||||||
Args:
|
Args:
|
||||||
func:
|
func ([`PROCESS_LIFESPAN_FUNC`](./lifespan#var-process-lifespan-func)): 生命周期函数
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
[`PROCESS_LIFESPAN_FUNC`](./lifespan#var-process-lifespan-func): 生命周期函数
|
||||||
|
"""
|
||||||
|
return self.lifespan.on_before_process_shutdown(func)
|
||||||
|
|
||||||
|
def on_before_process_restart(
|
||||||
|
self, func: PROCESS_LIFESPAN_FUNC
|
||||||
|
) -> PROCESS_LIFESPAN_FUNC:
|
||||||
|
"""
|
||||||
|
注册进程重启前的函数,为子进程重启时调用
|
||||||
|
Args:
|
||||||
|
func ([`PROCESS_LIFESPAN_FUNC`](./lifespan#var-process-lifespan-func)): 生命周期函数
|
||||||
|
Returns:
|
||||||
|
[`PROCESS_LIFESPAN_FUNC`](./lifespan#var-process-lifespan-func): 生命周期函数
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self.lifespan.on_before_restart(func)
|
return self.lifespan.on_before_process_restart(func)
|
||||||
|
|
||||||
def on_after_restart(self, func: LIFESPAN_FUNC):
|
def on_after_restart(self, func: LIFESPAN_FUNC):
|
||||||
"""
|
"""
|
||||||
注册重启后的函数:未实现
|
注册重启后的函数:未实现
|
||||||
Args:
|
Args:
|
||||||
func:
|
func ([`LIFESPAN_FUNC`](./lifespan#var-lifespan-func)): 生命周期函数
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
[`LIFESPAN_FUNC`](./lifespan#var-lifespan-func): 生命周期函数
|
||||||
"""
|
"""
|
||||||
return self.lifespan.on_after_restart(func)
|
return self.lifespan.on_after_restart(func)
|
||||||
|
|
||||||
def on_after_nonebot_init(self, func: LIFESPAN_FUNC):
|
|
||||||
"""
|
|
||||||
注册nonebot初始化后的函数
|
|
||||||
Args:
|
|
||||||
func:
|
|
||||||
|
|
||||||
Returns:
|
_BOT_INSTANCE: LiteyukiBot
|
||||||
|
|
||||||
"""
|
|
||||||
return self.lifespan.on_after_nonebot_init(func)
|
|
||||||
|
|
||||||
|
|
||||||
_BOT_INSTANCE: Optional[LiteyukiBot] = None
|
def get_bot() -> LiteyukiBot:
|
||||||
|
|
||||||
|
|
||||||
def get_bot() -> Optional[LiteyukiBot]:
|
|
||||||
"""
|
"""
|
||||||
获取轻雪实例
|
获取轻雪实例
|
||||||
Returns:
|
Returns:
|
||||||
LiteyukiBot: 当前的轻雪实例
|
[`LiteyukiBot`](#class-liteyukibot): 轻雪实例
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if IS_MAIN_PROCESS:
|
if IS_MAIN_PROCESS:
|
||||||
|
if _BOT_INSTANCE is None:
|
||||||
|
raise RuntimeError("尹灵温 实例未初始化")
|
||||||
return _BOT_INSTANCE
|
return _BOT_INSTANCE
|
||||||
else:
|
else:
|
||||||
# 从多进程上下文中获取
|
raise RuntimeError("无法在子进程中获取机器人实例")
|
||||||
pass
|
|
||||||
|
|
||||||
|
def get_config(key: str, default: Any = None) -> Any:
|
||||||
|
"""
|
||||||
|
获取配置
|
||||||
|
Args:
|
||||||
|
key ([`str`](https%3A//docs.python.org/3/library/stdtypes.html#str)): 配置键
|
||||||
|
default ([`Any`](https%3A//docs.python.org/3/library/functions.html#any), optional): 默认值. Defaults to None.
|
||||||
|
Returns:
|
||||||
|
[`Any`](https%3A//docs.python.org/3/library/functions.html#any): 配置值
|
||||||
|
"""
|
||||||
|
return get_bot().config.get(key, default)
|
||||||
|
|
||||||
|
|
||||||
|
def get_config_with_compat(
|
||||||
|
key: str, compat_keys: tuple[str], default: Any = None
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
获取配置,兼容旧版本
|
||||||
|
Args:
|
||||||
|
key ([`str`](https%3A//docs.python.org/3/library/stdtypes.html#str)): 配置键
|
||||||
|
compat_keys ([`tuple`](https%3A//docs.python.org/3/library/stdtypes.html#tuple)[`str`](https%3A//docs.python.org/3/library/stdtypes.html#str)): 兼容键
|
||||||
|
default ([`Any`](https%3A//docs.python.org/3/library/functions.html#any), optional): 默认值. Defaults to None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
[`Any`](https%3A//docs.python.org/3/library/functions.html#any): 配置值
|
||||||
|
"""
|
||||||
|
if key in get_bot().config:
|
||||||
|
return get_bot().config[key]
|
||||||
|
for compat_key in compat_keys:
|
||||||
|
if compat_key in get_bot().config:
|
||||||
|
logger.warning(f'配置键 "{compat_key}" 即将被 "{key}" 取代,请及时更新')
|
||||||
|
return get_bot().config[compat_key]
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def print_logo():
|
||||||
|
"""@litedoc-hide"""
|
||||||
|
print(
|
||||||
|
"\033[34m"
|
||||||
|
+ r"""
|
||||||
|
▅▅▅▅▅▅▅▅▅▅▅▅▅▅██ ▅▅▅▅▅▅▅▅▅▅▅▅▅▅██ ██ ▅▅▅▅▅▅▅▅▅▅█™
|
||||||
|
▛ ██ ██ ▛ ██ ███ ██ ██
|
||||||
|
██ ██ ███████████████ ██ ████████▅ ██
|
||||||
|
███████████████ ██ ███ ██ ██
|
||||||
|
██ ██ ▅██████████████▛ ██ ████████████
|
||||||
|
██ ██ ███ ███
|
||||||
|
████████████████ ██▅ ███ ██ ▅▅▅▅▅▅▅▅▅▅▅██
|
||||||
|
███ █ ▜███████ ██ ███ ██ ██ ██ ██
|
||||||
|
███ ███ █████▛ ██ ██ ██ ██ ██
|
||||||
|
███ ██ ███ █ ██ ██ ██ ██ ██
|
||||||
|
███ █████ ██████ ███ ██████████████
|
||||||
|
商业标记 版权所有 © 2024 金羿Eilles
|
||||||
|
机器软件 版权所有 © 2020-2024 神羽SnowyKami & 金羿Eilles\\
|
||||||
|
会同 LiteyukiStudio & 睿乐组织
|
||||||
|
保留所有权利
|
||||||
|
"""
|
||||||
|
+ "\033[0m"
|
||||||
|
)
|
||||||
|
@ -8,14 +8,19 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
|||||||
@File : lifespan.py
|
@File : lifespan.py
|
||||||
@Software: PyCharm
|
@Software: PyCharm
|
||||||
"""
|
"""
|
||||||
from typing import Any, Awaitable, Callable, TypeAlias
|
import asyncio
|
||||||
|
from typing import Any, Awaitable, Callable, TypeAlias, Sequence
|
||||||
|
|
||||||
from liteyuki.log import logger
|
from liteyuki.log import logger
|
||||||
from liteyuki.utils import is_coroutine_callable
|
from liteyuki.utils import is_coroutine_callable, async_wrapper
|
||||||
|
|
||||||
SYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Any]
|
SYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Any] # 同步生命周期函数
|
||||||
ASYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Awaitable[Any]]
|
ASYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Awaitable[Any]] # 异步生命周期函数
|
||||||
LIFESPAN_FUNC: TypeAlias = SYNC_LIFESPAN_FUNC | ASYNC_LIFESPAN_FUNC
|
LIFESPAN_FUNC: TypeAlias = SYNC_LIFESPAN_FUNC | ASYNC_LIFESPAN_FUNC # 生命周期函数
|
||||||
|
|
||||||
|
SYNC_PROCESS_LIFESPAN_FUNC: TypeAlias = Callable[[str], Any] # 同步进程生命周期函数
|
||||||
|
ASYNC_PROCESS_LIFESPAN_FUNC: TypeAlias = Callable[[str], Awaitable[Any]] # 异步进程生命周期函数
|
||||||
|
PROCESS_LIFESPAN_FUNC: TypeAlias = SYNC_PROCESS_LIFESPAN_FUNC | ASYNC_PROCESS_LIFESPAN_FUNC # 进程函数
|
||||||
|
|
||||||
|
|
||||||
class Lifespan:
|
class Lifespan:
|
||||||
@ -23,41 +28,35 @@ class Lifespan:
|
|||||||
"""
|
"""
|
||||||
轻雪生命周期管理,启动、停止、重启
|
轻雪生命周期管理,启动、停止、重启
|
||||||
"""
|
"""
|
||||||
|
self.life_flag: int = 0
|
||||||
self.life_flag: int = 0 # 0: 启动前,1: 启动后,2: 停止前,3: 停止后
|
|
||||||
|
|
||||||
self._before_start_funcs: list[LIFESPAN_FUNC] = []
|
self._before_start_funcs: list[LIFESPAN_FUNC] = []
|
||||||
self._after_start_funcs: list[LIFESPAN_FUNC] = []
|
self._after_start_funcs: list[LIFESPAN_FUNC] = []
|
||||||
|
|
||||||
self._before_shutdown_funcs: list[LIFESPAN_FUNC] = []
|
self._before_process_shutdown_funcs: list[PROCESS_LIFESPAN_FUNC] = []
|
||||||
self._after_shutdown_funcs: list[LIFESPAN_FUNC] = []
|
self._after_shutdown_funcs: list[LIFESPAN_FUNC] = []
|
||||||
|
|
||||||
self._before_restart_funcs: list[LIFESPAN_FUNC] = []
|
self._before_process_restart_funcs: list[PROCESS_LIFESPAN_FUNC] = []
|
||||||
self._after_restart_funcs: list[LIFESPAN_FUNC] = []
|
self._after_restart_funcs: list[LIFESPAN_FUNC] = []
|
||||||
|
|
||||||
self._after_nonebot_init_funcs: list[LIFESPAN_FUNC] = []
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def _run_funcs(funcs: list[LIFESPAN_FUNC]) -> None:
|
async def run_funcs(funcs: Sequence[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC], *args, **kwargs) -> None:
|
||||||
"""
|
"""
|
||||||
运行函数
|
并发运行异步函数
|
||||||
Args:
|
Args:
|
||||||
funcs:
|
funcs ([`Sequence`](https%3A//docs.python.org/3/library/typing.html#typing.Sequence)[[`ASYNC_LIFESPAN_FUNC`](#var-lifespan-func) | [`PROCESS_LIFESPAN_FUNC`](#var-process-lifespan-func)]): 函数列表
|
||||||
Returns:
|
Returns:
|
||||||
"""
|
"""
|
||||||
for func in funcs:
|
tasks = [func(*args, **kwargs) if is_coroutine_callable(func) else async_wrapper(func)(*args, **kwargs) for func in funcs]
|
||||||
if is_coroutine_callable(func):
|
await asyncio.gather(*tasks)
|
||||||
await func()
|
|
||||||
else:
|
|
||||||
func()
|
|
||||||
|
|
||||||
def on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
def on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||||
"""
|
"""
|
||||||
注册启动时的函数
|
注册启动时的函数
|
||||||
Args:
|
Args:
|
||||||
func:
|
func ([`LIFESPAN_FUNC`](#var-lifespan-func)): 生命周期函数
|
||||||
Returns:
|
Returns:
|
||||||
LIFESPAN_FUNC:
|
[`LIFESPAN_FUNC`](#var-lifespan-func): 生命周期函数
|
||||||
"""
|
"""
|
||||||
self._before_start_funcs.append(func)
|
self._before_start_funcs.append(func)
|
||||||
return func
|
return func
|
||||||
@ -66,124 +65,95 @@ class Lifespan:
|
|||||||
"""
|
"""
|
||||||
注册启动时的函数
|
注册启动时的函数
|
||||||
Args:
|
Args:
|
||||||
func:
|
func ([`LIFESPAN_FUNC`](#var-lifespan-func)): 生命周期函数
|
||||||
Returns:
|
Returns:
|
||||||
LIFESPAN_FUNC:
|
[`LIFESPAN_FUNC`](#var-lifespan-func): 生命周期函数
|
||||||
"""
|
"""
|
||||||
self._after_start_funcs.append(func)
|
self._after_start_funcs.append(func)
|
||||||
return func
|
return func
|
||||||
|
|
||||||
def on_before_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
def on_before_process_shutdown(self, func: PROCESS_LIFESPAN_FUNC) -> PROCESS_LIFESPAN_FUNC:
|
||||||
"""
|
"""
|
||||||
注册停止前的函数
|
注册进程停止前的函数
|
||||||
Args:
|
Args:
|
||||||
func:
|
func ([`PROCESS_LIFESPAN_FUNC`](#var-process-lifespan-func)): 进程生命周期函数
|
||||||
Returns:
|
Returns:
|
||||||
LIFESPAN_FUNC:
|
[`PROCESS_LIFESPAN_FUNC`](#var-process-lifespan-func): 进程生命周期函数
|
||||||
"""
|
"""
|
||||||
self._before_shutdown_funcs.append(func)
|
self._before_process_shutdown_funcs.append(func)
|
||||||
return func
|
return func
|
||||||
|
|
||||||
def on_after_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
def on_after_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||||
"""
|
"""
|
||||||
注册停止后的函数
|
注册停止后的函数
|
||||||
Args:
|
Args:
|
||||||
func:
|
func ([`LIFESPAN_FUNC`](#var-lifespan-func)): 生命周期函数
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
LIFESPAN_FUNC:
|
[`LIFESPAN_FUNC`](#var-lifespan-func): 生命周期函数
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self._after_shutdown_funcs.append(func)
|
self._after_shutdown_funcs.append(func)
|
||||||
return func
|
return func
|
||||||
|
|
||||||
def on_before_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
def on_before_process_restart(self, func: PROCESS_LIFESPAN_FUNC) -> PROCESS_LIFESPAN_FUNC:
|
||||||
"""
|
"""
|
||||||
注册重启时的函数
|
注册进程重启前的函数
|
||||||
Args:
|
Args:
|
||||||
func:
|
func ([`PROCESS_LIFESPAN_FUNC`](#var-process-lifespan-func)): 进程生命周期函数
|
||||||
Returns:
|
Returns:
|
||||||
LIFESPAN_FUNC:
|
[`PROCESS_LIFESPAN_FUNC`](#var-process-lifespan-func): 进程生命周期函数
|
||||||
"""
|
"""
|
||||||
self._before_restart_funcs.append(func)
|
self._before_process_restart_funcs.append(func)
|
||||||
return func
|
return func
|
||||||
|
|
||||||
def on_after_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
def on_after_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||||
"""
|
"""
|
||||||
注册重启后的函数
|
注册重启后的函数
|
||||||
Args:
|
Args:
|
||||||
func:
|
func ([`LIFESPAN_FUNC`](#var-lifespan-func)): 生命周期函数
|
||||||
Returns:
|
Returns:
|
||||||
LIFESPAN_FUNC:
|
[`LIFESPAN_FUNC`](#var-lifespan-func): 生命周期函数
|
||||||
"""
|
"""
|
||||||
self._after_restart_funcs.append(func)
|
self._after_restart_funcs.append(func)
|
||||||
return func
|
return func
|
||||||
|
|
||||||
def on_after_nonebot_init(self, func):
|
|
||||||
"""
|
|
||||||
注册 NoneBot 初始化后的函数
|
|
||||||
Args:
|
|
||||||
func:
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
self._after_nonebot_init_funcs.append(func)
|
|
||||||
return func
|
|
||||||
|
|
||||||
async def before_start(self) -> None:
|
async def before_start(self) -> None:
|
||||||
"""
|
"""
|
||||||
启动前
|
启动前钩子
|
||||||
Returns:
|
|
||||||
"""
|
"""
|
||||||
logger.debug("正在运行 before_start 之函数")
|
logger.debug("运行 before_start 函数")
|
||||||
await self._run_funcs(self._before_start_funcs)
|
await self.run_funcs(self._before_start_funcs)
|
||||||
|
|
||||||
async def after_start(self) -> None:
|
async def after_start(self) -> None:
|
||||||
"""
|
"""
|
||||||
启动后
|
启动后钩子
|
||||||
Returns:
|
|
||||||
"""
|
"""
|
||||||
logger.debug("正在运行 after_start 之函数")
|
logger.debug("运行 after_start 函数")
|
||||||
await self._run_funcs(self._after_start_funcs)
|
await self.run_funcs(self._after_start_funcs)
|
||||||
|
|
||||||
async def before_shutdown(self) -> None:
|
async def before_process_shutdown(self, *args, **kwargs) -> None:
|
||||||
"""
|
"""
|
||||||
停止前
|
停止前钩子
|
||||||
Returns:
|
|
||||||
"""
|
"""
|
||||||
logger.debug("正在运行 before_shutdown 之函数")
|
logger.debug("运行 before_shutdown 函数")
|
||||||
await self._run_funcs(self._before_shutdown_funcs)
|
await self.run_funcs(self._before_process_shutdown_funcs, *args, **kwargs)
|
||||||
|
|
||||||
async def after_shutdown(self) -> None:
|
async def after_shutdown(self) -> None:
|
||||||
"""
|
"""
|
||||||
停止后
|
停止后钩子 未实现
|
||||||
Returns:
|
|
||||||
"""
|
"""
|
||||||
logger.debug("正在运行 after_shutdown 之函数")
|
logger.debug("运行 after_shutdown 函数")
|
||||||
await self._run_funcs(self._after_shutdown_funcs)
|
await self.run_funcs(self._after_shutdown_funcs)
|
||||||
|
|
||||||
async def before_restart(self) -> None:
|
async def before_process_restart(self, *args, **kwargs) -> None:
|
||||||
"""
|
"""
|
||||||
重启前
|
重启前钩子
|
||||||
Returns:
|
|
||||||
"""
|
"""
|
||||||
logger.debug("正在运行 before_restart 之函数")
|
logger.debug("运行 before_restart 函数")
|
||||||
await self._run_funcs(self._before_restart_funcs)
|
await self.run_funcs(self._before_process_restart_funcs, *args, **kwargs)
|
||||||
|
|
||||||
async def after_restart(self) -> None:
|
async def after_restart(self) -> None:
|
||||||
"""
|
"""
|
||||||
重启后
|
重启后钩子 未实现
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
logger.debug("正在运行 after_restart 之函数")
|
logger.debug("运行 after_restart 函数")
|
||||||
await self._run_funcs(self._after_restart_funcs)
|
await self.run_funcs(self._after_restart_funcs)
|
||||||
|
|
||||||
async def after_nonebot_init(self) -> None:
|
|
||||||
"""
|
|
||||||
NoneBot 初始化后
|
|
||||||
Returns:
|
|
||||||
"""
|
|
||||||
logger.debug("正在运行 after_nonebot_init 之函数")
|
|
||||||
await self._run_funcs(self._after_nonebot_init_funcs)
|
|
||||||
|
@ -1,30 +1,38 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
|
||||||
|
|
||||||
@Time : 2024/7/26 下午10:36
|
|
||||||
@Author : snowykami
|
|
||||||
@Email : snowykami@outlook.com
|
|
||||||
@File : __init__.py
|
|
||||||
@Software: PyCharm
|
|
||||||
该模块用于轻雪主进程和Nonebot子进程之间的通信
|
该模块用于轻雪主进程和Nonebot子进程之间的通信
|
||||||
|
依赖关系
|
||||||
|
event -> _
|
||||||
|
storage -> channel_
|
||||||
|
rpc -> channel_, storage
|
||||||
"""
|
"""
|
||||||
from liteyuki.comm.channel import (
|
from liteyuki.comm.channel import (
|
||||||
Channel,
|
Channel,
|
||||||
chan,
|
|
||||||
get_channel,
|
get_channel,
|
||||||
set_channel,
|
set_channel,
|
||||||
set_channels,
|
set_channels,
|
||||||
get_channels
|
get_channels,
|
||||||
|
active_channel,
|
||||||
|
passive_channel
|
||||||
)
|
)
|
||||||
from liteyuki.comm.event import Event
|
from liteyuki.comm.event import Event
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Channel",
|
"Channel",
|
||||||
"chan",
|
|
||||||
"Event",
|
"Event",
|
||||||
"get_channel",
|
"get_channel",
|
||||||
"set_channel",
|
"set_channel",
|
||||||
"set_channels",
|
"set_channels",
|
||||||
"get_channels"
|
"get_channels",
|
||||||
|
"active_channel",
|
||||||
|
"passive_channel"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
from liteyuki.utils import IS_MAIN_PROCESS
|
||||||
|
|
||||||
|
# 第一次引用必定为赋值
|
||||||
|
_ref_count = 0
|
||||||
|
if not IS_MAIN_PROCESS:
|
||||||
|
if (active_channel is None or passive_channel is None) and _ref_count > 0:
|
||||||
|
raise RuntimeError("无法在子进程中初始化 Channel")
|
||||||
|
_ref_count += 1
|
||||||
|
@ -1,219 +1,305 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
|
||||||
|
|
||||||
@Time : 2024/7/26 下午11:21
|
|
||||||
@Author : snowykami
|
|
||||||
@Email : snowykami@outlook.com
|
|
||||||
@File : channel.py
|
|
||||||
@Software: PyCharm
|
|
||||||
|
|
||||||
本模块定义了一个通用的通道类,用于进程间通信
|
本模块定义了一个通用的通道类,用于进程间通信
|
||||||
"""
|
"""
|
||||||
import functools
|
import asyncio
|
||||||
import multiprocessing
|
|
||||||
import threading
|
|
||||||
from multiprocessing import Pipe
|
from multiprocessing import Pipe
|
||||||
from typing import Any, Optional, Callable, Awaitable, List, TypeAlias
|
from typing import Any, Callable, Coroutine, Generic, Optional, TypeAlias, TypeVar, get_args
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from liteyuki.utils import is_coroutine_callable, run_coroutine
|
from liteyuki.log import logger
|
||||||
|
from liteyuki.utils import IS_MAIN_PROCESS, is_coroutine_callable
|
||||||
|
|
||||||
SYNC_ON_RECEIVE_FUNC: TypeAlias = Callable[[Any], Any]
|
T = TypeVar("T")
|
||||||
ASYNC_ON_RECEIVE_FUNC: TypeAlias = Callable[[Any], Awaitable[Any]]
|
|
||||||
ON_RECEIVE_FUNC: TypeAlias = SYNC_ON_RECEIVE_FUNC | ASYNC_ON_RECEIVE_FUNC
|
|
||||||
|
|
||||||
SYNC_FILTER_FUNC: TypeAlias = Callable[[Any], bool]
|
SYNC_ON_RECEIVE_FUNC: TypeAlias = Callable[[T], Any] # 同步接收函数
|
||||||
ASYNC_FILTER_FUNC: TypeAlias = Callable[[Any], Awaitable[bool]]
|
ASYNC_ON_RECEIVE_FUNC: TypeAlias = Callable[[T], Coroutine[Any, Any, Any]] # 异步接收函数
|
||||||
FILTER_FUNC: TypeAlias = SYNC_FILTER_FUNC | ASYNC_FILTER_FUNC
|
ON_RECEIVE_FUNC: TypeAlias = SYNC_ON_RECEIVE_FUNC | ASYNC_ON_RECEIVE_FUNC # 接收函数
|
||||||
|
|
||||||
IS_MAIN_PROCESS = multiprocessing.current_process().name == "MainProcess"
|
SYNC_FILTER_FUNC: TypeAlias = Callable[[T], bool] # 同步过滤函数
|
||||||
|
ASYNC_FILTER_FUNC: TypeAlias = Callable[[T], Coroutine[Any, Any, bool]] # 异步过滤函数
|
||||||
|
FILTER_FUNC: TypeAlias = SYNC_FILTER_FUNC | ASYNC_FILTER_FUNC # 过滤函数
|
||||||
|
|
||||||
|
_func_id: int = 0
|
||||||
_channel: dict[str, "Channel"] = {}
|
_channel: dict[str, "Channel"] = {}
|
||||||
_callback_funcs: dict[str, ON_RECEIVE_FUNC] = {}
|
_callback_funcs: dict[int, ON_RECEIVE_FUNC] = {}
|
||||||
|
|
||||||
|
|
||||||
class Channel:
|
class Channel(Generic[T]):
|
||||||
"""
|
"""
|
||||||
通道类,用于进程间通信,进程内不可用,仅限主进程和子进程之间通信
|
通道类,可以在进程间和进程内通信,双向但同时只能有一个发送者和一个接收者
|
||||||
有两种接收工作方式,但是只能选择一种,主动接收和被动接收,主动接收使用 `receive` 方法,被动接收使用 `on_receive` 装饰器
|
有两种接收工作方式,但是只能选择一种,主动接收和被动接收,主动接收使用 `receive` 方法,被动接收使用 `on_receive` 装饰器
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, _id: str):
|
def __init__(self, name: str, type_check: Optional[bool] = None):
|
||||||
self.main_send_conn, self.sub_receive_conn = Pipe()
|
"""
|
||||||
self.sub_send_conn, self.main_receive_conn = Pipe()
|
初始化通道
|
||||||
self._closed = False
|
Args:
|
||||||
self._on_main_receive_funcs: list[str] = []
|
name: 通道ID
|
||||||
self._on_sub_receive_funcs: list[str] = []
|
type_check: 是否开启类型检查, 若为空,则传入泛型默认开启,否则默认关闭
|
||||||
self.name: str = _id
|
"""
|
||||||
|
|
||||||
self.is_main_receive_loop_running = False
|
self.conn_send, self.conn_recv = Pipe()
|
||||||
self.is_sub_receive_loop_running = False
|
self._conn_send_inner, self._conn_recv_inner = Pipe() # 内部通道,用于子进程通信
|
||||||
|
self._closed = False
|
||||||
|
self._on_main_receive_func_ids: list[int] = []
|
||||||
|
self._on_sub_receive_func_ids: list[int] = []
|
||||||
|
self.name: str = name
|
||||||
|
|
||||||
|
self.is_receive_loop_running = False
|
||||||
|
|
||||||
|
if type_check is None:
|
||||||
|
# 若传入泛型则默认开启类型检查
|
||||||
|
type_check = self._get_generic_type() is not None
|
||||||
|
|
||||||
|
elif type_check:
|
||||||
|
if self._get_generic_type() is None:
|
||||||
|
raise TypeError("Type hint 是强制类型检查之所必须")
|
||||||
|
self.type_check = type_check
|
||||||
|
if name in _channel:
|
||||||
|
raise ValueError(f"Channel {name} 已存在")
|
||||||
|
|
||||||
|
if IS_MAIN_PROCESS:
|
||||||
|
if name in _channel:
|
||||||
|
raise ValueError(f"Channel {name} 已存在")
|
||||||
|
_channel[name] = self
|
||||||
|
logger.debug(f"Channel {name} 已在主进程中初始化")
|
||||||
|
else:
|
||||||
|
logger.debug(f"Channel {name} 已初始化于子进程中,之后应于主进程中手动设置为妙")
|
||||||
|
|
||||||
|
def _get_generic_type(self) -> Optional[type]:
|
||||||
|
"""
|
||||||
|
获取通道传递泛型类型
|
||||||
|
Returns:
|
||||||
|
Optional[type]: 泛型类型
|
||||||
|
"""
|
||||||
|
if hasattr(self, '__orig_class__'):
|
||||||
|
return get_args(self.__orig_class__)[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _validate_structure(self, data: Any, structure: type) -> bool:
|
||||||
|
"""
|
||||||
|
验证数据结构
|
||||||
|
Args:
|
||||||
|
data: 数据
|
||||||
|
structure: 结构
|
||||||
|
Returns:
|
||||||
|
bool: 是否通过验证
|
||||||
|
"""
|
||||||
|
if isinstance(structure, type):
|
||||||
|
return isinstance(data, structure)
|
||||||
|
elif isinstance(structure, tuple):
|
||||||
|
if not isinstance(data, tuple) or len(data) != len(structure):
|
||||||
|
return False
|
||||||
|
return all(self._validate_structure(d, s) for d, s in zip(data, structure))
|
||||||
|
elif isinstance(structure, list):
|
||||||
|
if not isinstance(data, list):
|
||||||
|
return False
|
||||||
|
return all(self._validate_structure(d, structure[0]) for d in data)
|
||||||
|
elif isinstance(structure, dict):
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return False
|
||||||
|
return all(k in data and self._validate_structure(data[k], structure[k]) for k in structure)
|
||||||
|
return False
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Channel({self.name})"
|
return f"Channel({self.name})"
|
||||||
|
|
||||||
def send(self, data: Any):
|
def send(self, data: T):
|
||||||
"""
|
"""
|
||||||
发送数据
|
发送数据,发送函数为同步函数,没有异步的必要
|
||||||
Args:
|
Args:
|
||||||
data: 数据
|
data (T): 数据
|
||||||
"""
|
"""
|
||||||
if self._closed:
|
if self.type_check:
|
||||||
raise RuntimeError("无法发送至已关闭的通道中")
|
_type = self._get_generic_type()
|
||||||
if IS_MAIN_PROCESS:
|
if _type is not None and not self._validate_structure(data, _type):
|
||||||
print("主进程发送数据:", data)
|
raise TypeError(f"该数据必须为 {_type} 实例,而非 {type(data)}")
|
||||||
self.main_send_conn.send(data)
|
|
||||||
else:
|
|
||||||
print("子进程发送数据:", data)
|
|
||||||
self.sub_send_conn.send(data)
|
|
||||||
|
|
||||||
def receive(self) -> Any:
|
if self._closed:
|
||||||
|
raise RuntimeError("数据无法向已关闭的 Channel 中发送")
|
||||||
|
self.conn_send.send(data)
|
||||||
|
|
||||||
|
def receive(self) -> T:
|
||||||
"""
|
"""
|
||||||
接收数据
|
同步接收数据,会阻塞线程
|
||||||
Args:
|
Returns:
|
||||||
|
T: 数据
|
||||||
"""
|
"""
|
||||||
if self._closed:
|
if self._closed:
|
||||||
raise RuntimeError("无法从已关闭的通道中接收")
|
raise RuntimeError("无法在已关闭的 Channel 中接取数据")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
# 判断receiver是否为None或者receiver是否等于接收者,是则接收数据,否则不动数据
|
data = self.conn_recv.recv()
|
||||||
if IS_MAIN_PROCESS:
|
|
||||||
data = self.main_receive_conn.recv()
|
|
||||||
print("主进程接收数据:", data)
|
|
||||||
else:
|
|
||||||
data = self.sub_receive_conn.recv()
|
|
||||||
print("子进程接收数据:", data)
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def close(self):
|
async def async_receive(self) -> T:
|
||||||
"""
|
"""
|
||||||
关闭通道
|
异步接收数据,会挂起等待
|
||||||
|
Returns:
|
||||||
|
T: 数据
|
||||||
"""
|
"""
|
||||||
self._closed = True
|
loop = asyncio.get_running_loop()
|
||||||
self.sub_receive_conn.close()
|
data = await loop.run_in_executor(None, self.receive)
|
||||||
self.main_send_conn.close()
|
return data
|
||||||
self.sub_send_conn.close()
|
|
||||||
self.main_receive_conn.close()
|
|
||||||
|
|
||||||
def on_receive(self, filter_func: Optional[FILTER_FUNC] = None) -> Callable[[ON_RECEIVE_FUNC], ON_RECEIVE_FUNC]:
|
def on_receive(self, filter_func: Optional[FILTER_FUNC] = None) -> Callable[[Callable[[T], Any]], Callable[[T], Any]]:
|
||||||
"""
|
"""
|
||||||
接收数据并执行函数
|
接收数据并执行函数
|
||||||
Args:
|
Args:
|
||||||
filter_func: 过滤函数,为None则不过滤
|
filter_func ([`Optional`](https%3A//docs.python.org/3/library/typing.html#typing.Optional)[[`FILTER_FUNC`](#var-FILTER_FUNC)], optional): 过滤函数. Defaults to None.
|
||||||
Returns:
|
Returns:
|
||||||
装饰器,装饰一个函数在接收到数据后执行
|
Callable[[Callable[[T], Any]], Callable[[T], Any]]: 装饰器
|
||||||
"""
|
"""
|
||||||
if (not self.is_sub_receive_loop_running) and not IS_MAIN_PROCESS:
|
if not IS_MAIN_PROCESS:
|
||||||
threading.Thread(target=self._start_sub_receive_loop).start()
|
raise RuntimeError("on_receive 仅可用于主进程内")
|
||||||
|
|
||||||
if (not self.is_main_receive_loop_running) and IS_MAIN_PROCESS:
|
def decorator(func: Callable[[T], Any]) -> Callable[[T], Any]:
|
||||||
threading.Thread(target=self._start_main_receive_loop).start()
|
global _func_id
|
||||||
|
|
||||||
def decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC:
|
async def wrapper(data: T) -> Any:
|
||||||
async def wrapper(data: Any) -> Any:
|
|
||||||
if filter_func is not None:
|
if filter_func is not None:
|
||||||
if is_coroutine_callable(filter_func):
|
if is_coroutine_callable(filter_func):
|
||||||
if not await filter_func(data):
|
if not (await filter_func(data)): # type: ignore
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
if not filter_func(data):
|
if not filter_func(data):
|
||||||
return
|
return
|
||||||
return await func(data)
|
|
||||||
|
|
||||||
function_id = str(uuid4())
|
if is_coroutine_callable(func):
|
||||||
_callback_funcs[function_id] = wrapper
|
return await func(data)
|
||||||
|
else:
|
||||||
|
return func(data)
|
||||||
|
|
||||||
|
_callback_funcs[_func_id] = wrapper
|
||||||
if IS_MAIN_PROCESS:
|
if IS_MAIN_PROCESS:
|
||||||
self._on_main_receive_funcs.append(function_id)
|
self._on_main_receive_func_ids.append(_func_id)
|
||||||
else:
|
else:
|
||||||
self._on_sub_receive_funcs.append(function_id)
|
self._on_sub_receive_func_ids.append(_func_id)
|
||||||
|
_func_id += 1
|
||||||
return func
|
return func
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def _run_on_main_receive_funcs(self, data: Any):
|
async def _run_on_receive_funcs(self, data: Any):
|
||||||
"""
|
"""
|
||||||
运行接收函数
|
运行接收函数
|
||||||
Args:
|
Args:
|
||||||
data: 数据
|
data: 数据
|
||||||
"""
|
"""
|
||||||
for func_id in self._on_main_receive_funcs:
|
if IS_MAIN_PROCESS:
|
||||||
func = _callback_funcs[func_id]
|
[asyncio.create_task(_callback_funcs[func_id](data)) for func_id in self._on_main_receive_func_ids]
|
||||||
run_coroutine(func(data))
|
else:
|
||||||
|
[asyncio.create_task(_callback_funcs[func_id](data)) for func_id in self._on_sub_receive_func_ids]
|
||||||
def _run_on_sub_receive_funcs(self, data: Any):
|
|
||||||
"""
|
|
||||||
运行接收函数
|
|
||||||
Args:
|
|
||||||
data: 数据
|
|
||||||
"""
|
|
||||||
for func_id in self._on_sub_receive_funcs:
|
|
||||||
func = _callback_funcs[func_id]
|
|
||||||
run_coroutine(func(data))
|
|
||||||
|
|
||||||
def _start_main_receive_loop(self):
|
|
||||||
"""
|
|
||||||
开始接收数据
|
|
||||||
"""
|
|
||||||
self.is_main_receive_loop_running = True
|
|
||||||
while not self._closed:
|
|
||||||
data = self.main_receive_conn.recv()
|
|
||||||
self._run_on_main_receive_funcs(data)
|
|
||||||
|
|
||||||
def _start_sub_receive_loop(self):
|
|
||||||
"""
|
|
||||||
开始接收数据
|
|
||||||
"""
|
|
||||||
self.is_sub_receive_loop_running = True
|
|
||||||
while not self._closed:
|
|
||||||
data = self.sub_receive_conn.recv()
|
|
||||||
self._run_on_sub_receive_funcs(data)
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __next__(self) -> Any:
|
|
||||||
return self.receive()
|
|
||||||
|
|
||||||
|
|
||||||
"""默认通道实例,可直接从模块导入使用"""
|
"""子进程可用的主动和被动通道"""
|
||||||
chan = Channel("default")
|
active_channel: Channel = Channel(name="active_channel") # 主动通道
|
||||||
|
passive_channel: Channel = Channel(name="passive_channel") # 被动通道
|
||||||
|
publish_channel: Channel[tuple[str, dict[str, Any]]] = Channel(name="publish_channel") # 发布通道
|
||||||
|
"""通道传递通道,主进程创建单例,子进程初始化时实例化"""
|
||||||
|
channel_deliver_active_channel: Channel[Channel[Any]] # 主动通道传递通道
|
||||||
|
channel_deliver_passive_channel: Channel[tuple[str, dict[str, Any]]] # 被动通道传递通道
|
||||||
|
|
||||||
|
if IS_MAIN_PROCESS:
|
||||||
|
channel_deliver_active_channel = Channel(name="channel_deliver_active_channel") # 主动通道传递通道
|
||||||
|
channel_deliver_passive_channel = Channel(name="channel_deliver_passive_channel") # 被动通道传递通道
|
||||||
|
|
||||||
|
|
||||||
def set_channel(name: str, channel: Channel):
|
@channel_deliver_passive_channel.on_receive(filter_func=lambda data: data[0] == "set_channel")
|
||||||
|
def on_set_channel(data: tuple[str, dict[str, Any]]):
|
||||||
|
name, channel = data[1]["name"], data[1]["channel_"]
|
||||||
|
set_channel(name, channel)
|
||||||
|
|
||||||
|
|
||||||
|
@channel_deliver_passive_channel.on_receive(filter_func=lambda data: data[0] == "get_channel")
|
||||||
|
def on_get_channel(data: tuple[str, dict[str, Any]]):
|
||||||
|
name, recv_chan = data[1]["name"], data[1]["recv_chan"]
|
||||||
|
recv_chan.send(get_channel(name))
|
||||||
|
|
||||||
|
|
||||||
|
@channel_deliver_passive_channel.on_receive(filter_func=lambda data: data[0] == "get_channels")
|
||||||
|
def on_get_channels(data: tuple[str, dict[str, Any]]):
|
||||||
|
recv_chan = data[1]["recv_chan"]
|
||||||
|
recv_chan.send(get_channels())
|
||||||
|
|
||||||
|
|
||||||
|
def set_channel(name: str, channel: "Channel"):
|
||||||
"""
|
"""
|
||||||
设置通道实例
|
设置通道实例
|
||||||
Args:
|
Args:
|
||||||
name: 通道名称
|
name ([`str`](https%3A//docs.python.org/3/library/stdtypes.html#str)): 通道名称
|
||||||
channel: 通道实例
|
channel ([`Channel`](#class-channel-generic-t)): 通道实例
|
||||||
"""
|
"""
|
||||||
_channel[name] = channel
|
if not isinstance(channel, Channel):
|
||||||
|
raise TypeError(f"channel_ 必须为 Channel 实例,而非 {type(channel)}")
|
||||||
|
|
||||||
|
if IS_MAIN_PROCESS:
|
||||||
|
if name in _channel:
|
||||||
|
raise ValueError(f"Channel {name} 已存在")
|
||||||
|
_channel[name] = channel
|
||||||
|
else:
|
||||||
|
# 请求主进程设置通道
|
||||||
|
channel_deliver_passive_channel.send(
|
||||||
|
(
|
||||||
|
"set_channel", {
|
||||||
|
"name" : name,
|
||||||
|
"channel_": channel,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def set_channels(channels: dict[str, Channel]):
|
def set_channels(channels: dict[str, "Channel"]):
|
||||||
"""
|
"""
|
||||||
设置通道实例
|
设置通道实例
|
||||||
Args:
|
Args:
|
||||||
channels: 通道名称
|
channels ([`dict`](https%3A//docs.python.org/3/library/stdtypes.html#dict)[[`str`](https%3A//docs.python.org/3/library/stdtypes.html#str), [`Channel`](#class-channel-generic-t)]): 通道实例
|
||||||
"""
|
"""
|
||||||
for name, channel in channels.items():
|
for name, channel in channels.items():
|
||||||
_channel[name] = channel
|
set_channel(name, channel)
|
||||||
|
|
||||||
|
|
||||||
def get_channel(name: str) -> Optional[Channel]:
|
def get_channel(name: str) -> "Channel":
|
||||||
"""
|
"""
|
||||||
获取通道实例
|
获取通道实例
|
||||||
Args:
|
Args:
|
||||||
name: 通道名称
|
name ([`str`](https%3A//docs.python.org/3/library/stdtypes.html#str)): 通道名称
|
||||||
Returns:
|
Returns:
|
||||||
|
[`Channel`](#class-channel-generic-t): 通道实例
|
||||||
"""
|
"""
|
||||||
return _channel.get(name, None)
|
if IS_MAIN_PROCESS:
|
||||||
|
return _channel[name]
|
||||||
|
|
||||||
|
else:
|
||||||
|
recv_chan = Channel[Channel[Any]]("recv_chan")
|
||||||
|
channel_deliver_passive_channel.send(
|
||||||
|
(
|
||||||
|
"get_channel",
|
||||||
|
{
|
||||||
|
"name" : name,
|
||||||
|
"recv_chan": recv_chan
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return recv_chan.receive()
|
||||||
|
|
||||||
|
|
||||||
def get_channels() -> dict[str, Channel]:
|
def get_channels() -> dict[str, "Channel"]:
|
||||||
"""
|
"""
|
||||||
获取通道实例
|
获取通道实例们
|
||||||
Returns:
|
Returns:
|
||||||
|
[`dict`](https%3A//docs.python.org/3/library/stdtypes.html#dict)[[`str`](https%3A//docs.python.org/3/library/stdtypes.html#str), [`Channel`](#class-channel-generic-t)]: 通道实例
|
||||||
"""
|
"""
|
||||||
return _channel
|
if IS_MAIN_PROCESS:
|
||||||
|
return _channel
|
||||||
|
else:
|
||||||
|
recv_chan = Channel[dict[str, Channel[Any]]]("recv_chan")
|
||||||
|
channel_deliver_passive_channel.send(
|
||||||
|
(
|
||||||
|
"get_channels",
|
||||||
|
{
|
||||||
|
"recv_chan": recv_chan
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return recv_chan.receive()
|
||||||
|
@ -1,12 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
本模块用于轻雪主进程和子进程之间的通信的事件类
|
||||||
|
|
||||||
@Time : 2024/7/26 下午10:47
|
|
||||||
@Author : snowykami
|
|
||||||
@Email : snowykami@outlook.com
|
|
||||||
@File : event.py
|
|
||||||
@Software: PyCharm
|
|
||||||
"""
|
"""
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
26
liteyuki/comm/rpc.py
Normal file
26
liteyuki/comm/rpc.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
本模块用于实现RPC(基于IPC)通信
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import TypeAlias, Callable, Any
|
||||||
|
|
||||||
|
from liteyuki.comm.channel import Channel
|
||||||
|
|
||||||
|
ON_CALLING_FUNC: TypeAlias = Callable[[tuple, dict], Any]
|
||||||
|
|
||||||
|
|
||||||
|
class RPC:
|
||||||
|
"""
|
||||||
|
RPC类
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, on_calling: ON_CALLING_FUNC) -> None:
|
||||||
|
self.on_calling = on_calling
|
||||||
|
|
||||||
|
def call(self, args: tuple, kwargs: dict) -> Any:
|
||||||
|
"""
|
||||||
|
调用
|
||||||
|
"""
|
||||||
|
# 获取self.calling函数名
|
||||||
|
return self.on_calling(args, kwargs)
|
48
liteyuki/comm/socks_channel.py
Normal file
48
liteyuki/comm/socks_channel.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
基于socket的通道
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class SocksChannel:
|
||||||
|
"""
|
||||||
|
通道类,可以在进程间和进程内通信,双向但同时只能有一个发送者和一个接收者
|
||||||
|
有两种接收工作方式,但是只能选择一种,主动接收和被动接收,主动接收使用 `receive` 方法,被动接收使用 `on_receive` 装饰器
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name: str):
|
||||||
|
"""
|
||||||
|
初始化通道
|
||||||
|
Args:
|
||||||
|
name: 通道ID
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._name = name
|
||||||
|
self._conn_send = None
|
||||||
|
self._conn_recv = None
|
||||||
|
self._closed = False
|
||||||
|
|
||||||
|
def send(self, data):
|
||||||
|
"""
|
||||||
|
发送数据
|
||||||
|
Args:
|
||||||
|
data: 数据
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
def receive(self):
|
||||||
|
"""
|
||||||
|
接收数据
|
||||||
|
Returns:
|
||||||
|
data: 数据
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""
|
||||||
|
关闭通道
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
247
liteyuki/comm/storage.py
Normal file
247
liteyuki/comm/storage.py
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
共享内存模块。类似于redis,但是更加轻量级并且线程安全
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import threading
|
||||||
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
|
from liteyuki.comm import channel
|
||||||
|
from liteyuki.comm.channel import ASYNC_ON_RECEIVE_FUNC, Channel, ON_RECEIVE_FUNC
|
||||||
|
from liteyuki.utils import (
|
||||||
|
IS_MAIN_PROCESS,
|
||||||
|
is_coroutine_callable,
|
||||||
|
run_coroutine_in_thread,
|
||||||
|
)
|
||||||
|
|
||||||
|
if IS_MAIN_PROCESS:
|
||||||
|
_locks = {}
|
||||||
|
|
||||||
|
_on_main_subscriber_receive_funcs: dict[str, list[ASYNC_ON_RECEIVE_FUNC]] = {} # type: ignore
|
||||||
|
"""主进程订阅者接收函数"""
|
||||||
|
_on_sub_subscriber_receive_funcs: dict[str, list[ASYNC_ON_RECEIVE_FUNC]] = {} # type: ignore
|
||||||
|
"""子进程订阅者接收函数"""
|
||||||
|
|
||||||
|
|
||||||
|
def _get_lock(key) -> threading.Lock:
|
||||||
|
"""
|
||||||
|
获取锁
|
||||||
|
"""
|
||||||
|
if IS_MAIN_PROCESS:
|
||||||
|
if key not in _locks:
|
||||||
|
_locks[key] = threading.Lock()
|
||||||
|
return _locks[key]
|
||||||
|
else:
|
||||||
|
raise RuntimeError("无法在子进程中获取线程锁")
|
||||||
|
|
||||||
|
|
||||||
|
class KeyValueStore:
|
||||||
|
def __init__(self):
|
||||||
|
self._store = {}
|
||||||
|
self.active_chan = Channel[tuple[str, Optional[dict[str, Any]]]](
|
||||||
|
name="shared_memory-active"
|
||||||
|
)
|
||||||
|
self.passive_chan = Channel[tuple[str, Optional[dict[str, Any]]]](
|
||||||
|
name="shared_memory-passive"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.publish_channel = Channel[tuple[str, Any]](name="shared_memory-publish")
|
||||||
|
|
||||||
|
self.is_main_receive_loop_running = False
|
||||||
|
self.is_sub_receive_loop_running = False
|
||||||
|
|
||||||
|
def set(self, key: str, value: Any) -> None:
|
||||||
|
"""
|
||||||
|
设置键值对
|
||||||
|
Args:
|
||||||
|
key: 键
|
||||||
|
value: 值
|
||||||
|
|
||||||
|
"""
|
||||||
|
if IS_MAIN_PROCESS:
|
||||||
|
lock = _get_lock(key)
|
||||||
|
with lock:
|
||||||
|
self._store[key] = value
|
||||||
|
else:
|
||||||
|
# 向主进程发送请求拿取
|
||||||
|
self.passive_chan.send(("set", {"key": key, "value": value}))
|
||||||
|
|
||||||
|
def get(self, key: str, default: Optional[Any] = None) -> Optional[Any]:
|
||||||
|
"""
|
||||||
|
获取键值对
|
||||||
|
Args:
|
||||||
|
key: 键
|
||||||
|
default: 默认值
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Any: 值
|
||||||
|
"""
|
||||||
|
if IS_MAIN_PROCESS:
|
||||||
|
lock = _get_lock(key)
|
||||||
|
with lock:
|
||||||
|
return self._store.get(key, default)
|
||||||
|
else:
|
||||||
|
recv_chan = Channel[Optional[Any]]("recv_chan")
|
||||||
|
self.passive_chan.send(
|
||||||
|
("get", {"key": key, "default": default, "recv_chan": recv_chan})
|
||||||
|
)
|
||||||
|
return recv_chan.receive()
|
||||||
|
|
||||||
|
def delete(self, key: str, ignore_key_error: bool = True) -> None:
|
||||||
|
"""
|
||||||
|
删除键值对
|
||||||
|
Args:
|
||||||
|
key: 键
|
||||||
|
ignore_key_error: 是否忽略键不存在的错误
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
"""
|
||||||
|
if IS_MAIN_PROCESS:
|
||||||
|
lock = _get_lock(key)
|
||||||
|
with lock:
|
||||||
|
if key in self._store:
|
||||||
|
try:
|
||||||
|
del self._store[key]
|
||||||
|
del _locks[key]
|
||||||
|
except KeyError as e:
|
||||||
|
if not ignore_key_error:
|
||||||
|
raise e
|
||||||
|
else:
|
||||||
|
# 向主进程发送请求删除
|
||||||
|
self.passive_chan.send(("delete", {"key": key}))
|
||||||
|
|
||||||
|
def get_all(self) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
获取所有键值对
|
||||||
|
Returns:
|
||||||
|
dict[str, Any]: 键值对
|
||||||
|
"""
|
||||||
|
if IS_MAIN_PROCESS:
|
||||||
|
return self._store
|
||||||
|
else:
|
||||||
|
recv_chan = Channel[dict[str, Any]]("recv_chan")
|
||||||
|
self.passive_chan.send(("get_all", {"recv_chan": recv_chan}))
|
||||||
|
return recv_chan.receive()
|
||||||
|
|
||||||
|
def publish(self, channel_: str, data: Any) -> None:
|
||||||
|
"""
|
||||||
|
发布消息
|
||||||
|
Args:
|
||||||
|
channel_: 频道
|
||||||
|
data: 数据
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
"""
|
||||||
|
self.active_chan.send(("publish", {"channel": channel_, "data": data}))
|
||||||
|
|
||||||
|
def on_subscriber_receive(
|
||||||
|
self, channel_: str
|
||||||
|
) -> Callable[[ON_RECEIVE_FUNC], ON_RECEIVE_FUNC]:
|
||||||
|
"""
|
||||||
|
订阅者接收消息时的回调
|
||||||
|
Args:
|
||||||
|
channel_: 频道
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
装饰器
|
||||||
|
"""
|
||||||
|
if not IS_MAIN_PROCESS:
|
||||||
|
raise RuntimeError("无法订阅一子线程消息")
|
||||||
|
|
||||||
|
def decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC:
|
||||||
|
async def wrapper(data: Any):
|
||||||
|
if is_coroutine_callable(func):
|
||||||
|
await func(data)
|
||||||
|
else:
|
||||||
|
func(data)
|
||||||
|
|
||||||
|
if IS_MAIN_PROCESS:
|
||||||
|
if channel_ not in _on_main_subscriber_receive_funcs:
|
||||||
|
_on_main_subscriber_receive_funcs[channel_] = []
|
||||||
|
_on_main_subscriber_receive_funcs[channel_].append(wrapper)
|
||||||
|
else:
|
||||||
|
if channel_ not in _on_sub_subscriber_receive_funcs:
|
||||||
|
_on_sub_subscriber_receive_funcs[channel_] = []
|
||||||
|
_on_sub_subscriber_receive_funcs[channel_].append(wrapper)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def run_subscriber_receive_funcs(channel_: str, data: Any):
|
||||||
|
"""
|
||||||
|
运行订阅者接收函数
|
||||||
|
Args:
|
||||||
|
channel_: 频道
|
||||||
|
data: 数据
|
||||||
|
"""
|
||||||
|
[
|
||||||
|
asyncio.create_task(func(data))
|
||||||
|
for func in _on_main_subscriber_receive_funcs[channel_]
|
||||||
|
]
|
||||||
|
|
||||||
|
async def start_receive_loop(self):
|
||||||
|
"""
|
||||||
|
启动发布订阅接收器循环,在主进程中运行,若有子进程订阅则推送给子进程
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not IS_MAIN_PROCESS:
|
||||||
|
raise RuntimeError("无法在子进程中启用订阅接收器循环")
|
||||||
|
while True:
|
||||||
|
data = await self.active_chan.async_receive()
|
||||||
|
if data[0] == "publish":
|
||||||
|
# 运行主进程订阅函数
|
||||||
|
await self.run_subscriber_receive_funcs(
|
||||||
|
data[1]["channel"], data[1]["data"]
|
||||||
|
)
|
||||||
|
# 推送给子进程
|
||||||
|
self.publish_channel.send(data)
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalKeyValueStore:
|
||||||
|
_instance = None
|
||||||
|
_lock = threading.Lock()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_instance(cls):
|
||||||
|
if cls._instance is None:
|
||||||
|
with cls._lock:
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = KeyValueStore()
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
|
||||||
|
shared_memory: KeyValueStore = GlobalKeyValueStore.get_instance() # 共享内存对象
|
||||||
|
|
||||||
|
# 全局单例访问点
|
||||||
|
if IS_MAIN_PROCESS:
|
||||||
|
|
||||||
|
@shared_memory.passive_chan.on_receive(lambda d: d[0] == "get")
|
||||||
|
def on_get(data: tuple[str, dict[str, Any]]):
|
||||||
|
key = data[1]["key"]
|
||||||
|
default = data[1]["default"]
|
||||||
|
recv_chan = data[1]["recv_chan"]
|
||||||
|
recv_chan.send(shared_memory.get(key, default))
|
||||||
|
|
||||||
|
@shared_memory.passive_chan.on_receive(lambda d: d[0] == "set")
|
||||||
|
def on_set(data: tuple[str, dict[str, Any]]):
|
||||||
|
key = data[1]["key"]
|
||||||
|
value = data[1]["value"]
|
||||||
|
shared_memory.set(key, value)
|
||||||
|
|
||||||
|
@shared_memory.passive_chan.on_receive(lambda d: d[0] == "delete")
|
||||||
|
def on_delete(data: tuple[str, dict[str, Any]]):
|
||||||
|
key = data[1]["key"]
|
||||||
|
shared_memory.delete(key)
|
||||||
|
|
||||||
|
@shared_memory.passive_chan.on_receive(lambda d: d[0] == "get_all")
|
||||||
|
def on_get_all(data: tuple[str, dict[str, Any]]):
|
||||||
|
recv_chan = data[1]["recv_chan"]
|
||||||
|
recv_chan.send(shared_memory.get_all())
|
||||||
|
|
||||||
|
|
||||||
|
_ref_count = 0 # import 引用计数, 防止获取空指针
|
||||||
|
if not IS_MAIN_PROCESS:
|
||||||
|
if (shared_memory is None) and _ref_count > 1:
|
||||||
|
raise RuntimeError("共享内存未初始化")
|
||||||
|
_ref_count += 1
|
@ -1,49 +1,132 @@
|
|||||||
|
"""
|
||||||
|
该模块用于常用配置文件的加载
|
||||||
|
多配置文件编写原则:
|
||||||
|
1. 尽量不要冲突: 一个键不要多次出现
|
||||||
|
2. 分工明确: 每个配置文件给一个或一类服务提供配置
|
||||||
|
3. 扁平化编写: 配置文件尽量扁平化,不要出现过多的嵌套
|
||||||
|
4. 注意冲突时的优先级: 项目目录下的配置文件优先级高于config目录下的配置文件
|
||||||
|
5. 请不要将需要动态加载的内容写入配置文件,你应该使用其他储存方式
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from typing import List
|
import json
|
||||||
|
import copy
|
||||||
import nonebot
|
import toml
|
||||||
import yaml
|
import yaml
|
||||||
from pydantic import BaseModel
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from liteyuki.log import logger
|
||||||
|
|
||||||
|
_SUPPORTED_CONFIG_FORMATS = (".yaml", ".yml", ".json", ".toml")
|
||||||
|
|
||||||
|
|
||||||
config = {} # 主进程全局配置,确保加载后读取
|
def flat_config(config: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
扁平化配置文件
|
||||||
|
|
||||||
|
{a:{b:{c:1}}} -> {"a.b.c": 1}
|
||||||
|
Args:
|
||||||
|
config: 配置项目
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
扁平化后的配置文件,但也包含原有的键值对
|
||||||
|
"""
|
||||||
|
new_config = copy.deepcopy(config)
|
||||||
|
for key, value in config.items():
|
||||||
|
if isinstance(value, dict):
|
||||||
|
for k, v in flat_config(value).items():
|
||||||
|
new_config[f"{key}.{k}"] = v
|
||||||
|
return new_config
|
||||||
|
|
||||||
|
|
||||||
class SatoriNodeConfig(BaseModel):
|
def load_from_yaml(file_: str) -> dict[str, Any]:
|
||||||
host: str = ""
|
"""
|
||||||
port: str = "5500"
|
Load config from yaml file
|
||||||
path: str = ""
|
|
||||||
token: str = ""
|
"""
|
||||||
|
logger.debug("正在从 {} 中加载 YAML 配置".format(file_))
|
||||||
|
config = yaml.safe_load(open(file_, "r", encoding="utf-8"))
|
||||||
|
return flat_config(config if config is not None else {})
|
||||||
|
|
||||||
|
|
||||||
class SatoriConfig(BaseModel):
|
def load_from_json(file_: str) -> dict[str, Any]:
|
||||||
comment: str = "此皆正处于开发之中,切勿在生产环境中启用。"
|
"""
|
||||||
enable: bool = False
|
Load config from json file
|
||||||
hosts: List[SatoriNodeConfig] = [SatoriNodeConfig()]
|
"""
|
||||||
|
logger.debug("正在从 {} 中加载 JSON 配置".format(file_))
|
||||||
|
config = json.load(open(file_, "r", encoding="utf-8"))
|
||||||
|
return flat_config(config if config is not None else {})
|
||||||
|
|
||||||
|
|
||||||
class BasicConfig(BaseModel):
|
def load_from_toml(file_: str) -> dict[str, Any]:
|
||||||
host: str = "127.0.0.1"
|
"""
|
||||||
port: int = 20247
|
Load config from toml file
|
||||||
superusers: list[str] = []
|
"""
|
||||||
command_start: list[str] = ["/", ""]
|
logger.debug("正在从 {} 中加载 TOML 配置".format(file_))
|
||||||
nickname: list[str] = [f"灵温"]
|
config = toml.load(open(file_, "r", encoding="utf-8"))
|
||||||
satori: SatoriConfig = SatoriConfig()
|
return flat_config(config if config is not None else {})
|
||||||
data_path: str = "data/liteyuki"
|
|
||||||
|
|
||||||
|
|
||||||
def load_from_yaml(file: str) -> dict:
|
def load_from_files(*files: str, no_warning: bool = False) -> dict[str, Any]:
|
||||||
global config
|
"""
|
||||||
nonebot.logger.debug("Loading config from %s" % file)
|
从指定文件加载配置项,会自动识别文件格式
|
||||||
if not os.path.exists(file):
|
默认执行扁平化选项
|
||||||
nonebot.logger.warning(f"未找到配置文件 {file} ,已创建默认配置,请修改后重启。")
|
"""
|
||||||
with open(file, "w", encoding="utf-8") as f:
|
config = {}
|
||||||
yaml.dump(BasicConfig().dict(), f, default_flow_style=False)
|
for file in files:
|
||||||
|
if os.path.exists(file):
|
||||||
|
if file.endswith((".yaml", "yml")):
|
||||||
|
config.update(load_from_yaml(file))
|
||||||
|
elif file.endswith(".json"):
|
||||||
|
config.update(load_from_json(file))
|
||||||
|
elif file.endswith(".toml"):
|
||||||
|
config.update(load_from_toml(file))
|
||||||
|
else:
|
||||||
|
if not no_warning:
|
||||||
|
logger.warning(f"不支持配置文件 {file} 的类型")
|
||||||
|
else:
|
||||||
|
if not no_warning:
|
||||||
|
logger.warning(f"配置文件 {file} 未寻得")
|
||||||
|
return config
|
||||||
|
|
||||||
with open(file, "r", encoding="utf-8") as f:
|
|
||||||
conf = yaml.load(f, Loader=yaml.FullLoader)
|
def load_configs_from_dirs(
|
||||||
config = conf
|
*directories: str, no_waring: bool = False
|
||||||
if conf is None:
|
) -> dict[str, Any]:
|
||||||
nonebot.logger.warning(f"配置文件 {file} 为空,已创建默认配置,请修改后重启。")
|
"""
|
||||||
conf = BasicConfig().dict()
|
从目录下加载配置文件,不递归
|
||||||
return conf
|
按照读取文件的优先级反向覆盖
|
||||||
|
默认执行扁平化选项
|
||||||
|
"""
|
||||||
|
config = {}
|
||||||
|
for directory in directories:
|
||||||
|
if not os.path.exists(directory):
|
||||||
|
if not no_waring:
|
||||||
|
logger.warning(f"目录 {directory} 未寻得")
|
||||||
|
continue
|
||||||
|
for file in os.listdir(directory):
|
||||||
|
if file.endswith(_SUPPORTED_CONFIG_FORMATS):
|
||||||
|
config.update(
|
||||||
|
load_from_files(os.path.join(directory, file), no_warning=no_waring)
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def load_config_in_default(no_waring: bool = False) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
从一个标准的轻雪项目加载配置文件
|
||||||
|
项目目录下的config.*和config目录下的所有配置文件
|
||||||
|
项目目录下的配置文件优先
|
||||||
|
"""
|
||||||
|
config = load_configs_from_dirs("config", no_waring=no_waring)
|
||||||
|
config.update(
|
||||||
|
load_from_files(
|
||||||
|
"config.yaml",
|
||||||
|
"config.toml",
|
||||||
|
"config.json",
|
||||||
|
"config.yml",
|
||||||
|
no_warning=no_waring,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
@ -1,11 +1,2 @@
|
|||||||
import multiprocessing
|
|
||||||
|
|
||||||
from .spawn_process import *
|
|
||||||
from .manager import *
|
from .manager import *
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"IS_MAIN_PROCESS"
|
|
||||||
]
|
|
||||||
|
|
||||||
IS_MAIN_PROCESS = multiprocessing.current_process().name == "MainProcess"
|
|
||||||
|
|
||||||
|
@ -9,92 +9,175 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
|||||||
@Software: PyCharm
|
@Software: PyCharm
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import multiprocessing
|
||||||
import threading
|
import threading
|
||||||
from multiprocessing import Process
|
from multiprocessing import Process
|
||||||
from typing import TYPE_CHECKING
|
from typing import Any, Callable, TYPE_CHECKING, TypeAlias
|
||||||
|
|
||||||
from liteyuki.comm import Channel, get_channel, set_channels
|
|
||||||
from liteyuki.log import logger
|
from liteyuki.log import logger
|
||||||
|
from liteyuki.utils import IS_MAIN_PROCESS
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from liteyuki.bot import LiteyukiBot
|
from liteyuki.bot.lifespan import Lifespan
|
||||||
|
from liteyuki.comm.storage import KeyValueStore
|
||||||
|
|
||||||
|
from liteyuki.comm import Channel
|
||||||
|
|
||||||
|
if IS_MAIN_PROCESS:
|
||||||
|
from liteyuki.comm.channel import get_channel, publish_channel, get_channels
|
||||||
|
from liteyuki.comm.storage import shared_memory
|
||||||
|
from liteyuki.comm.channel import (
|
||||||
|
channel_deliver_active_channel,
|
||||||
|
channel_deliver_passive_channel,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
from liteyuki.comm import channel
|
||||||
|
from liteyuki.comm import storage
|
||||||
|
|
||||||
|
TARGET_FUNC: TypeAlias = Callable[..., Any]
|
||||||
TIMEOUT = 10
|
TIMEOUT = 10
|
||||||
|
|
||||||
__all__ = ["ProcessManager"]
|
__all__ = ["ProcessManager"]
|
||||||
|
multiprocessing.set_start_method("spawn", force=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelDeliver:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
active: Channel[Any],
|
||||||
|
passive: Channel[Any],
|
||||||
|
channel_deliver_active: Channel[Channel[Any]],
|
||||||
|
channel_deliver_passive: Channel[tuple[str, dict]],
|
||||||
|
publish: Channel[tuple[str, Any]],
|
||||||
|
):
|
||||||
|
self.active = active
|
||||||
|
self.passive = passive
|
||||||
|
self.channel_deliver_active = channel_deliver_active
|
||||||
|
self.channel_deliver_passive = channel_deliver_passive
|
||||||
|
self.publish = publish
|
||||||
|
|
||||||
|
|
||||||
|
# 函数处理一些跨进程通道的
|
||||||
|
def _delivery_channel_wrapper(
|
||||||
|
func: TARGET_FUNC, cd: ChannelDeliver, sm: "KeyValueStore", *args, **kwargs
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
子进程入口函数
|
||||||
|
处理一些操作
|
||||||
|
"""
|
||||||
|
# 给子进程设置通道
|
||||||
|
if IS_MAIN_PROCESS:
|
||||||
|
raise RuntimeError("函数仅可在子进程中被调用")
|
||||||
|
|
||||||
|
channel.active_channel = cd.active # 子进程主动通道
|
||||||
|
channel.passive_channel = cd.passive # 子进程被动通道
|
||||||
|
channel.channel_deliver_active_channel = (
|
||||||
|
cd.channel_deliver_active
|
||||||
|
) # 子进程通道传递主动通道
|
||||||
|
channel.channel_deliver_passive_channel = (
|
||||||
|
cd.channel_deliver_passive
|
||||||
|
) # 子进程通道传递被动通道
|
||||||
|
channel.publish_channel = cd.publish # 子进程发布通道
|
||||||
|
|
||||||
|
# 给子进程创建共享内存实例
|
||||||
|
|
||||||
|
storage.shared_memory = sm
|
||||||
|
|
||||||
|
func(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ProcessManager:
|
class ProcessManager:
|
||||||
"""
|
"""
|
||||||
在主进程中被调用
|
进程管理器
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, bot: "LiteyukiBot"):
|
def __init__(self, lifespan: "Lifespan"):
|
||||||
self.bot = bot
|
self.lifespan = lifespan
|
||||||
self.targets: dict[str, tuple[callable, tuple, dict]] = {}
|
self.targets: dict[str, tuple[Callable, tuple, dict]] = {}
|
||||||
self.processes: dict[str, Process] = {}
|
self.processes: dict[str, Process] = {}
|
||||||
|
|
||||||
set_channels(
|
def _run_process(self, name: str):
|
||||||
{
|
|
||||||
"nonebot-active": Channel(_id="nonebot-active"),
|
|
||||||
"melobot-active": Channel(_id="melobot-active"),
|
|
||||||
"nonebot-passive": Channel(_id="nonebot-passive"),
|
|
||||||
"melobot-passive": Channel(_id="melobot-passive"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def start(self, name: str, delay: int = 0):
|
|
||||||
"""
|
"""
|
||||||
开启后自动监控进程,并添加到进程字典中
|
开启后自动监控进程,并添加到进程字典中,会阻塞,请创建task
|
||||||
Args:
|
Args:
|
||||||
name:
|
name:
|
||||||
delay:
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if name not in self.targets:
|
if name not in self.targets:
|
||||||
raise KeyError(f"未有 Process {name} 之存在")
|
raise KeyError(f"Process {name} 未寻得")
|
||||||
|
|
||||||
def _start():
|
chan_active = get_channel(f"{name}-active")
|
||||||
should_exit = False
|
|
||||||
while not should_exit:
|
|
||||||
chan_active = get_channel(f"{name}-active")
|
|
||||||
chan_passive = get_channel(f"{name}-passive")
|
|
||||||
process = Process(
|
|
||||||
target=self.targets[name][0],
|
|
||||||
args=(chan_active, chan_passive, *self.targets[name][1]),
|
|
||||||
kwargs=self.targets[name][2],
|
|
||||||
)
|
|
||||||
self.processes[name] = process
|
|
||||||
process.start()
|
|
||||||
while not should_exit:
|
|
||||||
# 0退出 1重启
|
|
||||||
data = chan_active.receive()
|
|
||||||
if data == 1:
|
|
||||||
logger.info(f"重启 {name} 进程")
|
|
||||||
asyncio.run(self.bot.lifespan.before_shutdown())
|
|
||||||
asyncio.run(self.bot.lifespan.before_restart())
|
|
||||||
self.terminate(name)
|
|
||||||
break
|
|
||||||
|
|
||||||
elif data == 0:
|
def _start_process():
|
||||||
logger.info(f"关停 {name} 进程")
|
process = Process(
|
||||||
asyncio.run(self.bot.lifespan.before_shutdown())
|
target=self.targets[name][0],
|
||||||
should_exit = True
|
args=self.targets[name][1],
|
||||||
self.terminate(name)
|
kwargs=self.targets[name][2],
|
||||||
else:
|
daemon=True,
|
||||||
logger.warning("数据未知,省略:{}".format(data))
|
)
|
||||||
|
self.processes[name] = process
|
||||||
|
process.start()
|
||||||
|
|
||||||
if delay:
|
# 启动进程并监听信号
|
||||||
threading.Timer(delay, _start).start()
|
_start_process()
|
||||||
else:
|
while True:
|
||||||
threading.Thread(target=_start).start()
|
data = chan_active.receive()
|
||||||
|
if data == 0:
|
||||||
|
# 停止
|
||||||
|
logger.info(f"正在关停 Process {name}")
|
||||||
|
self.terminate(name)
|
||||||
|
break
|
||||||
|
elif data == 1:
|
||||||
|
# 重启
|
||||||
|
logger.info(f"正在重启 Process {name}")
|
||||||
|
self.terminate(name)
|
||||||
|
_start_process()
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
logger.warning("接收到未知信号数据 {} ,已忽略".format(data))
|
||||||
|
|
||||||
def add_target(self, name: str, target, *args, **kwargs):
|
def start_all(self):
|
||||||
self.targets[name] = (target, args, kwargs)
|
"""
|
||||||
|
对外启动方法,启动所有进程,创建asyncio task
|
||||||
|
"""
|
||||||
|
# [asyncio.create_task(self._run_process(name)) for name in self.targets]
|
||||||
|
|
||||||
def join(self):
|
for name in self.targets:
|
||||||
|
logger.debug(f"正在启动 Process {name}")
|
||||||
|
threading.Thread(
|
||||||
|
target=self._run_process, args=(name,), daemon=True
|
||||||
|
).start()
|
||||||
|
|
||||||
|
def add_target(self, name: str, target: TARGET_FUNC, args: tuple = (), kwargs=None):
|
||||||
|
"""
|
||||||
|
添加进程
|
||||||
|
Args:
|
||||||
|
name: 进程名,用于获取和唯一标识
|
||||||
|
target: 进程函数
|
||||||
|
args: 进程函数参数
|
||||||
|
kwargs: 进程函数关键字参数,通常会默认传入chan_active和chan_passive
|
||||||
|
"""
|
||||||
|
if kwargs is None:
|
||||||
|
kwargs = {}
|
||||||
|
chan_active: Channel = Channel(name=f"{name}-active")
|
||||||
|
chan_passive: Channel = Channel(name=f"{name}-passive")
|
||||||
|
|
||||||
|
channel_deliver = ChannelDeliver(
|
||||||
|
active=chan_active,
|
||||||
|
passive=chan_passive,
|
||||||
|
channel_deliver_active=channel_deliver_active_channel,
|
||||||
|
channel_deliver_passive=channel_deliver_passive_channel,
|
||||||
|
publish=publish_channel,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.targets[name] = (
|
||||||
|
_delivery_channel_wrapper,
|
||||||
|
(target, channel_deliver, shared_memory, *args),
|
||||||
|
kwargs,
|
||||||
|
)
|
||||||
|
# 主进程通道
|
||||||
|
|
||||||
|
def join_all(self):
|
||||||
for name, process in self.targets:
|
for name, process in self.targets:
|
||||||
process.join()
|
process.join()
|
||||||
|
|
||||||
@ -107,14 +190,29 @@ class ProcessManager:
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if name not in self.targets:
|
if name not in self.processes:
|
||||||
raise logger.warning(f"未有 Process {name} 之存在")
|
logger.warning(f"Process {name} 未寻得")
|
||||||
|
return
|
||||||
process = self.processes[name]
|
process = self.processes[name]
|
||||||
process.terminate()
|
process.terminate()
|
||||||
process.join(TIMEOUT)
|
process.join(TIMEOUT)
|
||||||
if process.is_alive():
|
if process.is_alive():
|
||||||
process.kill()
|
process.kill()
|
||||||
|
logger.success(f"Process {name} 已迫令终止")
|
||||||
|
|
||||||
def terminate_all(self):
|
def terminate_all(self):
|
||||||
for name in self.targets:
|
for name in self.targets:
|
||||||
self.terminate(name)
|
self.terminate(name)
|
||||||
|
|
||||||
|
def is_process_alive(self, name: str) -> bool:
|
||||||
|
"""
|
||||||
|
检查进程是否存活
|
||||||
|
Args:
|
||||||
|
name:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
if name not in self.targets:
|
||||||
|
logger.warning(f"Process {name} 未寻得")
|
||||||
|
return self.processes[name].is_alive()
|
||||||
|
@ -1,56 +0,0 @@
|
|||||||
from typing import Optional, TYPE_CHECKING
|
|
||||||
|
|
||||||
import nonebot
|
|
||||||
|
|
||||||
from liteyuki.core.nb import adapter_manager, driver_manager
|
|
||||||
from liteyuki.comm.channel import set_channel
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from liteyuki.comm.channel import Channel
|
|
||||||
|
|
||||||
timeout_limit: int = 20
|
|
||||||
|
|
||||||
"""导出对象,用于主进程与nonebot通信"""
|
|
||||||
_channels = {}
|
|
||||||
|
|
||||||
|
|
||||||
def nb_run(chan_active: "Channel", chan_passive: "Channel", *args, **kwargs):
|
|
||||||
"""
|
|
||||||
初始化NoneBot并运行在子进程
|
|
||||||
Args:
|
|
||||||
|
|
||||||
chan_active:
|
|
||||||
chan_passive:
|
|
||||||
**kwargs:
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
set_channel("nonebot-active", chan_active)
|
|
||||||
set_channel("nonebot-passive", chan_passive)
|
|
||||||
nonebot.init(**kwargs)
|
|
||||||
driver_manager.init(config=kwargs)
|
|
||||||
adapter_manager.init(kwargs)
|
|
||||||
adapter_manager.register()
|
|
||||||
nonebot.load_plugin("src.liteyuki_main")
|
|
||||||
nonebot.run()
|
|
||||||
|
|
||||||
|
|
||||||
def mb_run(chan_active: "Channel", chan_passive: "Channel", *args, **kwargs):
|
|
||||||
"""
|
|
||||||
初始化MeloBot并运行在子进程
|
|
||||||
Args:
|
|
||||||
chan_active
|
|
||||||
chan_passive
|
|
||||||
*args:
|
|
||||||
**kwargs:
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
set_channel("melobot-active", chan_active)
|
|
||||||
set_channel("melobot-passive", chan_passive)
|
|
||||||
|
|
||||||
# bot = MeloBot(__name__)
|
|
||||||
# bot.init(AbstractConnector(cd_time=0))
|
|
||||||
# bot.run()
|
|
4
liteyuki/dev/__init__.py
Normal file
4
liteyuki/dev/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
该模块用于存放一些开发工具
|
||||||
|
"""
|
90
liteyuki/dev/observer.py
Normal file
90
liteyuki/dev/observer.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
"""
|
||||||
|
此模块用于注册观察者函数,使用watchdog监控文件变化并重启bot
|
||||||
|
启用该模块需要在配置文件中设置`dev_mode`为True
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
from typing import Callable, TypeAlias
|
||||||
|
|
||||||
|
from watchdog.events import FileSystemEvent, FileSystemEventHandler
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
|
||||||
|
from liteyuki import get_bot, get_config_with_compat, logger
|
||||||
|
|
||||||
|
liteyuki_bot = get_bot()
|
||||||
|
|
||||||
|
CALLBACK_FUNC: TypeAlias = Callable[[FileSystemEvent], None] # 位置1为FileSystemEvent
|
||||||
|
FILTER_FUNC: TypeAlias = Callable[[FileSystemEvent], bool] # 位置1为FileSystemEvent
|
||||||
|
observer = Observer()
|
||||||
|
|
||||||
|
|
||||||
|
def debounce(wait):
|
||||||
|
"""
|
||||||
|
防抖函数
|
||||||
|
"""
|
||||||
|
def decorator(func):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
nonlocal last_call_time
|
||||||
|
current_time = time.time()
|
||||||
|
if (current_time - last_call_time) > wait:
|
||||||
|
last_call_time = current_time
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
last_call_time = None
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
if get_config_with_compat("liteyuki.reload", ("dev_mode",), False):
|
||||||
|
logger.debug("Liteyuki Reload 已启用,正在监视文件更新")
|
||||||
|
observer.start()
|
||||||
|
|
||||||
|
|
||||||
|
class CodeModifiedHandler(FileSystemEventHandler):
|
||||||
|
"""
|
||||||
|
Handler for code file changes
|
||||||
|
"""
|
||||||
|
|
||||||
|
@debounce(1)
|
||||||
|
def on_modified(self, event):
|
||||||
|
raise NotImplementedError("on_modified 函数在继承后必须实现")
|
||||||
|
|
||||||
|
def on_created(self, event):
|
||||||
|
self.on_modified(event)
|
||||||
|
|
||||||
|
def on_deleted(self, event):
|
||||||
|
self.on_modified(event)
|
||||||
|
|
||||||
|
def on_moved(self, event):
|
||||||
|
self.on_modified(event)
|
||||||
|
|
||||||
|
def on_any_event(self, event):
|
||||||
|
self.on_modified(event)
|
||||||
|
|
||||||
|
|
||||||
|
def on_file_system_event(directories: tuple[str], recursive: bool = True, event_filter: FILTER_FUNC = None) -> Callable[[CALLBACK_FUNC], CALLBACK_FUNC]:
|
||||||
|
"""
|
||||||
|
注册文件系统变化监听器
|
||||||
|
Args:
|
||||||
|
directories: 监听目录们
|
||||||
|
recursive: 是否递归监听子目录
|
||||||
|
event_filter: 事件过滤器, 返回True则执行回调函数
|
||||||
|
Returns:
|
||||||
|
装饰器,装饰一个函数在接收到数据后执行
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func: CALLBACK_FUNC) -> CALLBACK_FUNC:
|
||||||
|
def wrapper(event: FileSystemEvent):
|
||||||
|
|
||||||
|
if event_filter is not None and not event_filter(event):
|
||||||
|
return
|
||||||
|
func(event)
|
||||||
|
|
||||||
|
code_modified_handler = CodeModifiedHandler()
|
||||||
|
code_modified_handler.on_modified = wrapper
|
||||||
|
for directory in directories:
|
||||||
|
observer.schedule(code_modified_handler, directory, recursive=recursive)
|
||||||
|
|
||||||
|
return func
|
||||||
|
|
||||||
|
return decorator
|
28
liteyuki/dev/plugin.py
Normal file
28
liteyuki/dev/plugin.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||||
|
|
||||||
|
@Time : 2024/8/18 上午5:04
|
||||||
|
@Author : snowykami
|
||||||
|
@Email : snowykami@outlook.com
|
||||||
|
@File : plugin.py
|
||||||
|
@Software: PyCharm
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from liteyuki.bot import LiteyukiBot
|
||||||
|
from liteyuki.config import load_config_in_default
|
||||||
|
|
||||||
|
|
||||||
|
def run_plugins(*module_path: str | Path):
|
||||||
|
"""
|
||||||
|
运行插件,无需手动初始化bot
|
||||||
|
Args:
|
||||||
|
module_path: 插件路径,参考`liteyuki.load_plugin`的函数签名
|
||||||
|
"""
|
||||||
|
cfg = load_config_in_default()
|
||||||
|
plugins = cfg.get("liteyuki.plugins", [])
|
||||||
|
plugins.extend(module_path)
|
||||||
|
cfg["liteyuki.plugins"] = plugins
|
||||||
|
bot = LiteyukiBot(**cfg)
|
||||||
|
bot.run()
|
@ -9,22 +9,10 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
|||||||
@Software: PyCharm
|
@Software: PyCharm
|
||||||
"""
|
"""
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import loguru
|
import loguru
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
logger = loguru.logger
|
logger = loguru.logger
|
||||||
if TYPE_CHECKING:
|
|
||||||
# avoid sphinx autodoc resolve annotation failed
|
|
||||||
# because loguru module do not have `Logger` class actually
|
|
||||||
from loguru import Record
|
|
||||||
|
|
||||||
|
|
||||||
def default_filter(record: "Record"):
|
|
||||||
"""默认的日志过滤器,根据 `config.log_level` 配置改变日志等级。"""
|
|
||||||
log_level = record["extra"].get("nonebot_log_level", "INFO")
|
|
||||||
levelno = logger.level(log_level).no if isinstance(log_level, str) else log_level
|
|
||||||
return record["level"].no >= levelno
|
|
||||||
|
|
||||||
|
|
||||||
# DEBUG日志格式
|
# DEBUG日志格式
|
||||||
debug_format: str = (
|
debug_format: str = (
|
||||||
@ -50,35 +38,26 @@ def get_format(level: str) -> str:
|
|||||||
return default_format
|
return default_format
|
||||||
|
|
||||||
|
|
||||||
logger = loguru.logger.bind()
|
|
||||||
|
|
||||||
|
|
||||||
def init_log(config: dict):
|
def init_log(config: dict):
|
||||||
"""
|
"""
|
||||||
在语言加载完成后执行
|
在语言加载完成后执行
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
global logger
|
|
||||||
|
|
||||||
logger.remove()
|
logger.remove()
|
||||||
logger.add(
|
logger.add(
|
||||||
sys.stdout,
|
sys.stdout,
|
||||||
level=0,
|
level=0,
|
||||||
diagnose=False,
|
diagnose=False,
|
||||||
filter=default_filter,
|
|
||||||
format=get_format(config.get("log_level", "INFO")),
|
format=get_format(config.get("log_level", "INFO")),
|
||||||
)
|
)
|
||||||
show_icon = config.get("log_icon", True)
|
show_icon = config.get("log_icon", True)
|
||||||
|
logger.level("DEBUG", color="<blue>", icon=f"{'🐛' if show_icon else ''}试")
|
||||||
|
logger.level("INFO", color="<normal>", icon=f"{'ℹ️' if show_icon else ''}讯")
|
||||||
|
logger.level("SUCCESS", color="<green>", icon=f"{'✅' if show_icon else ''}警")
|
||||||
|
logger.level("WARNING", color="<yellow>", icon=f"{'⚠️' if show_icon else ''}误")
|
||||||
|
logger.level("ERROR", color="<red>", icon=f"{'⭕' if show_icon else ''}成")
|
||||||
|
|
||||||
# debug = lang.get("log.debug", default="==DEBUG")
|
|
||||||
# info = lang.get("log.info", default="===INFO")
|
init_log(config={})
|
||||||
# success = lang.get("log.success", default="SUCCESS")
|
|
||||||
# warning = lang.get("log.warning", default="WARNING")
|
|
||||||
# error = lang.get("log.error", default="==ERROR")
|
|
||||||
#
|
|
||||||
# logger.level("DEBUG", color="<blue>", icon=f"{'🐛' if show_icon else ''}{debug}")
|
|
||||||
# logger.level("INFO", color="<normal>", icon=f"{'ℹ️' if show_icon else ''}{info}")
|
|
||||||
# logger.level("SUCCESS", color="<green>", icon=f"{'✅' if show_icon else ''}{success}")
|
|
||||||
# logger.level("WARNING", color="<yellow>", icon=f"{'⚠️' if show_icon else ''}{warning}")
|
|
||||||
# logger.level("ERROR", color="<red>", icon=f"{'⭕' if show_icon else ''}{error}")
|
|
||||||
|
10
liteyuki/message/__init__.py
Normal file
10
liteyuki/message/__init__.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||||
|
|
||||||
|
@Time : 2024/8/19 下午10:44
|
||||||
|
@Author : snowykami
|
||||||
|
@Email : snowykami@outlook.com
|
||||||
|
@File : __init__.py.py
|
||||||
|
@Software: PyCharm
|
||||||
|
"""
|
88
liteyuki/message/event.py
Normal file
88
liteyuki/message/event.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||||
|
|
||||||
|
@Time : 2024/8/19 下午10:47
|
||||||
|
@Author : snowykami
|
||||||
|
@Email : snowykami@outlook.com
|
||||||
|
@File : event.py
|
||||||
|
@Software: PyCharm
|
||||||
|
"""
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from liteyuki import Channel
|
||||||
|
from liteyuki.comm.storage import shared_memory
|
||||||
|
|
||||||
|
|
||||||
|
class MessageEvent:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
|
||||||
|
bot_id: str,
|
||||||
|
message: list[dict[str, Any]] | str,
|
||||||
|
message_type: str,
|
||||||
|
raw_message: str,
|
||||||
|
session_id: str,
|
||||||
|
user_id: str,
|
||||||
|
session_type: str,
|
||||||
|
receive_channel: Optional[Channel["MessageEvent"]] = None,
|
||||||
|
data: Optional[dict[str, Any]] = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
轻雪抽象消息事件
|
||||||
|
Args:
|
||||||
|
|
||||||
|
bot_id: 机器人ID
|
||||||
|
message: 消息,消息段数组[{type: str, data: dict[str, Any]}]
|
||||||
|
raw_message: 原始消息(通常为纯文本的格式)
|
||||||
|
message_type: 消息类型(private, group, other)
|
||||||
|
|
||||||
|
session_id: 会话ID(私聊通常为用户ID,群聊通常为群ID)
|
||||||
|
session_type: 会话类型(private, group)
|
||||||
|
receive_channel: 接收频道(用于回复消息)
|
||||||
|
|
||||||
|
data: 附加数据
|
||||||
|
"""
|
||||||
|
|
||||||
|
if data is None:
|
||||||
|
data = {}
|
||||||
|
self.message_type = message_type
|
||||||
|
self.data = data
|
||||||
|
self.bot_id = bot_id
|
||||||
|
|
||||||
|
self.message = message
|
||||||
|
self.raw_message = raw_message
|
||||||
|
|
||||||
|
self.session_id = session_id
|
||||||
|
self.session_type = session_type
|
||||||
|
self.user_id = user_id
|
||||||
|
|
||||||
|
self.receive_channel = receive_channel
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return (f"Event(message_type={self.message_type}, data={self.data}, bot_id={self.bot_id}, "
|
||||||
|
f"session_id={self.session_id}, session_type={self.session_type})")
|
||||||
|
|
||||||
|
def reply(self, message: str | dict[str, Any]):
|
||||||
|
"""
|
||||||
|
回复消息
|
||||||
|
Args:
|
||||||
|
message:
|
||||||
|
Returns:
|
||||||
|
"""
|
||||||
|
reply_event = MessageEvent(
|
||||||
|
message_type=self.session_type,
|
||||||
|
message=message,
|
||||||
|
raw_message="",
|
||||||
|
data={
|
||||||
|
"message": message
|
||||||
|
},
|
||||||
|
bot_id=self.bot_id,
|
||||||
|
session_id=self.session_id,
|
||||||
|
user_id=self.user_id,
|
||||||
|
session_type=self.session_type,
|
||||||
|
receive_channel=None
|
||||||
|
)
|
||||||
|
# shared_memory.publish(self.receive_channel, reply_event)
|
||||||
|
if self.receive_channel:
|
||||||
|
self.receive_channel.send(reply_event)
|
63
liteyuki/message/matcher.py
Normal file
63
liteyuki/message/matcher.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||||
|
|
||||||
|
@Time : 2024/8/19 下午10:51
|
||||||
|
@Author : snowykami
|
||||||
|
@Email : snowykami@outlook.com
|
||||||
|
@File : matcher.py
|
||||||
|
@Software: PyCharm
|
||||||
|
"""
|
||||||
|
import traceback
|
||||||
|
from typing import Any, TypeAlias, Callable, Coroutine
|
||||||
|
|
||||||
|
from liteyuki.message.event import MessageEvent
|
||||||
|
from liteyuki.message.rule import Rule
|
||||||
|
|
||||||
|
EventHandler: TypeAlias = Callable[[MessageEvent], Coroutine[None, None, Any]]
|
||||||
|
|
||||||
|
|
||||||
|
class Matcher:
|
||||||
|
def __init__(self, rule: Rule, priority: int, block: bool):
|
||||||
|
"""
|
||||||
|
匹配器
|
||||||
|
Args:
|
||||||
|
rule: 规则
|
||||||
|
priority: 优先级 >= 0
|
||||||
|
block: 是否阻断后续优先级更低的匹配器
|
||||||
|
"""
|
||||||
|
self.rule = rule
|
||||||
|
self.priority = priority
|
||||||
|
self.block = block
|
||||||
|
self.handlers: list[EventHandler] = []
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Matcher(rule={self.rule}, priority={self.priority}, block={self.block})"
|
||||||
|
|
||||||
|
def handle(self) -> Callable[[EventHandler], EventHandler]:
|
||||||
|
"""
|
||||||
|
添加处理函数,装饰器
|
||||||
|
Returns:
|
||||||
|
装饰器 handler
|
||||||
|
"""
|
||||||
|
def decorator(handler: EventHandler) -> EventHandler:
|
||||||
|
self.handlers.append(handler)
|
||||||
|
return handler
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
async def run(self, event: MessageEvent) -> None:
|
||||||
|
"""
|
||||||
|
运行处理函数
|
||||||
|
Args:
|
||||||
|
event:
|
||||||
|
Returns:
|
||||||
|
"""
|
||||||
|
if not await self.rule(event):
|
||||||
|
return
|
||||||
|
|
||||||
|
for handler in self.handlers:
|
||||||
|
try:
|
||||||
|
await handler(event)
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
62
liteyuki/message/on.py
Normal file
62
liteyuki/message/on.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||||
|
|
||||||
|
@Time : 2024/8/19 下午10:52
|
||||||
|
@Author : snowykami
|
||||||
|
@Email : snowykami@outlook.com
|
||||||
|
@File : on.py
|
||||||
|
@Software: PyCharm
|
||||||
|
"""
|
||||||
|
|
||||||
|
from queue import Queue
|
||||||
|
|
||||||
|
from liteyuki.comm.storage import shared_memory
|
||||||
|
from liteyuki.log import logger
|
||||||
|
from liteyuki.message.event import MessageEvent
|
||||||
|
from liteyuki.message.matcher import Matcher
|
||||||
|
from liteyuki.message.rule import Rule, empty_rule
|
||||||
|
|
||||||
|
_matcher_list: list[Matcher] = []
|
||||||
|
_queue: Queue = Queue()
|
||||||
|
|
||||||
|
|
||||||
|
@shared_memory.on_subscriber_receive("event_to_liteyuki")
|
||||||
|
async def _(event: MessageEvent):
|
||||||
|
print("AA")
|
||||||
|
current_priority = -1
|
||||||
|
for i, matcher in enumerate(_matcher_list):
|
||||||
|
logger.info(f"为 Event {event} 运行 Matcher {matcher}")
|
||||||
|
await matcher.run(event)
|
||||||
|
# 同优先级不阻断,不同优先级阻断
|
||||||
|
if current_priority != matcher.priority:
|
||||||
|
current_priority = matcher.priority
|
||||||
|
if matcher.block:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
logger.info(f"无 Matcher 适配于 Event {event}")
|
||||||
|
print("BB")
|
||||||
|
|
||||||
|
|
||||||
|
def add_matcher(matcher: Matcher):
|
||||||
|
for i, m in enumerate(_matcher_list):
|
||||||
|
if m.priority < matcher.priority:
|
||||||
|
_matcher_list.insert(i, matcher)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
_matcher_list.append(matcher)
|
||||||
|
|
||||||
|
|
||||||
|
def on_message(rule: Rule = empty_rule, priority: int = 0, block: bool = False) -> Matcher:
|
||||||
|
matcher = Matcher(rule, priority, block)
|
||||||
|
# 按照优先级插入
|
||||||
|
add_matcher(matcher)
|
||||||
|
return matcher
|
||||||
|
|
||||||
|
|
||||||
|
def on_keywords(keywords: list[str], rule=empty_rule, priority: int = 0, block: bool = False) -> Matcher:
|
||||||
|
@Rule
|
||||||
|
async def on_keywords_rule(event: MessageEvent):
|
||||||
|
return any(keyword in event.raw_message for keyword in keywords)
|
||||||
|
|
||||||
|
return on_message(on_keywords_rule & rule, priority, block)
|
51
liteyuki/message/rule.py
Normal file
51
liteyuki/message/rule.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||||
|
|
||||||
|
@Time : 2024/8/19 下午10:55
|
||||||
|
@Author : snowykami
|
||||||
|
@Email : snowykami@outlook.com
|
||||||
|
@File : rule.py
|
||||||
|
@Software: PyCharm
|
||||||
|
"""
|
||||||
|
import inspect
|
||||||
|
from typing import Optional, TypeAlias, Callable, Coroutine
|
||||||
|
|
||||||
|
from liteyuki.message.event import MessageEvent
|
||||||
|
from liteyuki import get_config
|
||||||
|
|
||||||
|
_superusers: list[str] = get_config("liteyuki.superusers", [])
|
||||||
|
|
||||||
|
RuleHandlerFunc: TypeAlias = Callable[[MessageEvent], Coroutine[None, None, bool]]
|
||||||
|
"""规则函数签名"""
|
||||||
|
|
||||||
|
|
||||||
|
class Rule:
|
||||||
|
def __init__(self, handler: RuleHandlerFunc):
|
||||||
|
self.handler = handler
|
||||||
|
|
||||||
|
def __or__(self, other: "Rule") -> "Rule":
|
||||||
|
async def combined_handler(event: MessageEvent) -> bool:
|
||||||
|
return await self.handler(event) or await other.handler(event)
|
||||||
|
|
||||||
|
return Rule(combined_handler)
|
||||||
|
|
||||||
|
def __and__(self, other: "Rule") -> "Rule":
|
||||||
|
async def combined_handler(event: MessageEvent) -> bool:
|
||||||
|
return await self.handler(event) and await other.handler(event)
|
||||||
|
|
||||||
|
return Rule(combined_handler)
|
||||||
|
|
||||||
|
async def __call__(self, event: MessageEvent) -> bool:
|
||||||
|
if self.handler is None:
|
||||||
|
return True
|
||||||
|
return await self.handler(event)
|
||||||
|
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
async def empty_rule(event: MessageEvent) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
async def is_su_rule(event: MessageEvent) -> bool:
|
||||||
|
return str(event.user_id) in _superusers
|
@ -2,9 +2,9 @@
|
|||||||
"""
|
"""
|
||||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||||
|
|
||||||
@Time : 2024/8/10 下午5:18
|
@Time : 2024/8/19 下午10:47
|
||||||
@Author : snowykami
|
@Author : snowykami
|
||||||
@Email : snowykami@outlook.com
|
@Email : snowykami@outlook.com
|
||||||
@File : reloader_monitor.py
|
@File : session.py
|
||||||
@Software: PyCharm
|
@Software: PyCharm
|
||||||
"""
|
"""
|
357
liteyuki/mkdoc.py
Normal file
357
liteyuki/mkdoc.py
Normal file
@ -0,0 +1,357 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||||
|
|
||||||
|
@Time : 2024/8/19 上午6:23
|
||||||
|
@Author : snowykami
|
||||||
|
@Email : snowykami@outlook.com
|
||||||
|
@File : mkdoc.py
|
||||||
|
@Software: PyCharm
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from typing import Any
|
||||||
|
from enum import Enum
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
NO_TYPE_ANY = "Any"
|
||||||
|
NO_TYPE_HINT = "NoTypeHint"
|
||||||
|
|
||||||
|
|
||||||
|
class DefType(Enum):
|
||||||
|
FUNCTION = "function"
|
||||||
|
METHOD = "method"
|
||||||
|
STATIC_METHOD = "staticmethod"
|
||||||
|
CLASS_METHOD = "classmethod"
|
||||||
|
PROPERTY = "property"
|
||||||
|
|
||||||
|
|
||||||
|
class FunctionInfo(BaseModel):
|
||||||
|
name: str
|
||||||
|
args: list[tuple[str, str]]
|
||||||
|
return_type: str
|
||||||
|
docstring: str
|
||||||
|
source_code: str = ""
|
||||||
|
|
||||||
|
type: DefType
|
||||||
|
"""若为类中def,则有"""
|
||||||
|
is_async: bool
|
||||||
|
|
||||||
|
|
||||||
|
class AttributeInfo(BaseModel):
|
||||||
|
name: str
|
||||||
|
type: str
|
||||||
|
value: Any = None
|
||||||
|
docstring: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class ClassInfo(BaseModel):
|
||||||
|
name: str
|
||||||
|
docstring: str
|
||||||
|
methods: list[FunctionInfo]
|
||||||
|
attributes: list[AttributeInfo]
|
||||||
|
inherit: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleInfo(BaseModel):
|
||||||
|
module_path: str
|
||||||
|
"""点分割模块路径 例如 liteyuki.bot"""
|
||||||
|
|
||||||
|
functions: list[FunctionInfo]
|
||||||
|
classes: list[ClassInfo]
|
||||||
|
attributes: list[AttributeInfo]
|
||||||
|
docstring: str
|
||||||
|
|
||||||
|
|
||||||
|
def get_relative_path(base_path: str, target_path: str) -> str:
|
||||||
|
"""
|
||||||
|
获取相对路径
|
||||||
|
Args:
|
||||||
|
base_path: 基础路径
|
||||||
|
target_path: 目标路径
|
||||||
|
"""
|
||||||
|
return os.path.relpath(target_path, base_path)
|
||||||
|
|
||||||
|
|
||||||
|
def write_to_files(file_data: dict[str, str]):
|
||||||
|
"""
|
||||||
|
输出文件
|
||||||
|
Args:
|
||||||
|
file_data: 文件数据 相对路径
|
||||||
|
"""
|
||||||
|
|
||||||
|
for rp, data in file_data.items():
|
||||||
|
|
||||||
|
if not os.path.exists(os.path.dirname(rp)):
|
||||||
|
os.makedirs(os.path.dirname(rp))
|
||||||
|
with open(rp, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
|
||||||
|
def get_file_list(module_folder: str):
|
||||||
|
file_list = []
|
||||||
|
for root, dirs, files in os.walk(module_folder):
|
||||||
|
for file in files:
|
||||||
|
if file.endswith((".py", ".pyi")):
|
||||||
|
file_list.append(os.path.join(root, file))
|
||||||
|
return file_list
|
||||||
|
|
||||||
|
|
||||||
|
def get_module_info_normal(file_path: str, ignore_private: bool = True) -> ModuleInfo:
|
||||||
|
"""
|
||||||
|
获取函数和类
|
||||||
|
Args:
|
||||||
|
file_path: Python 文件路径
|
||||||
|
ignore_private: 忽略私有函数和类
|
||||||
|
Returns:
|
||||||
|
模块信息
|
||||||
|
"""
|
||||||
|
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as file:
|
||||||
|
file_content = file.read()
|
||||||
|
tree = ast.parse(file_content)
|
||||||
|
|
||||||
|
dot_sep_module_path = file_path.replace(os.sep, '.').replace(".py", "").replace(".pyi", "")
|
||||||
|
module_docstring = ast.get_docstring(tree)
|
||||||
|
|
||||||
|
module_info = ModuleInfo(
|
||||||
|
module_path=dot_sep_module_path,
|
||||||
|
functions=[],
|
||||||
|
classes=[],
|
||||||
|
attributes=[],
|
||||||
|
docstring=module_docstring if module_docstring else ""
|
||||||
|
)
|
||||||
|
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||||
|
# 模块函数 且不在类中 若ignore_private=True则忽略私有函数
|
||||||
|
if not any(isinstance(parent, ast.ClassDef) for parent in ast.iter_child_nodes(node)) and (not ignore_private or not node.name.startswith('_')):
|
||||||
|
|
||||||
|
# 判断第一个参数是否为self或cls,后期用其他办法优化
|
||||||
|
if node.args.args:
|
||||||
|
first_arg = node.args.args[0]
|
||||||
|
if first_arg.arg in ("self", "cls"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
function_docstring = ast.get_docstring(node)
|
||||||
|
|
||||||
|
func_info = FunctionInfo(
|
||||||
|
name=node.name,
|
||||||
|
args=[(arg.arg, ast.unparse(arg.annotation) if arg.annotation else NO_TYPE_ANY) for arg in node.args.args],
|
||||||
|
return_type=ast.unparse(node.returns) if node.returns else "None",
|
||||||
|
docstring=function_docstring if function_docstring else "",
|
||||||
|
type=DefType.FUNCTION,
|
||||||
|
is_async=isinstance(node, ast.AsyncFunctionDef),
|
||||||
|
source_code=ast.unparse(node)
|
||||||
|
)
|
||||||
|
module_info.functions.append(func_info)
|
||||||
|
|
||||||
|
elif isinstance(node, ast.ClassDef):
|
||||||
|
# 模块类
|
||||||
|
class_docstring = ast.get_docstring(node)
|
||||||
|
|
||||||
|
class_info = ClassInfo(
|
||||||
|
name=node.name,
|
||||||
|
docstring=class_docstring if class_docstring else "",
|
||||||
|
methods=[],
|
||||||
|
attributes=[],
|
||||||
|
inherit=[ast.unparse(base) for base in node.bases]
|
||||||
|
)
|
||||||
|
|
||||||
|
for class_node in node.body:
|
||||||
|
# methods [instance, static, class, property],保留__init__方法
|
||||||
|
if isinstance(class_node, ast.FunctionDef) and (not ignore_private or not class_node.name.startswith('_') or class_node.name == "__init__"):
|
||||||
|
method_docstring = ast.get_docstring(class_node)
|
||||||
|
def_type = DefType.METHOD
|
||||||
|
if class_node.decorator_list:
|
||||||
|
if any(isinstance(decorator, ast.Name) and decorator.id == "staticmethod" for decorator in class_node.decorator_list):
|
||||||
|
def_type = DefType.STATIC_METHOD
|
||||||
|
elif any(isinstance(decorator, ast.Name) and decorator.id == "classmethod" for decorator in class_node.decorator_list):
|
||||||
|
def_type = DefType.CLASS_METHOD
|
||||||
|
elif any(isinstance(decorator, ast.Name) and decorator.id == "property" for decorator in class_node.decorator_list):
|
||||||
|
def_type = DefType.PROPERTY
|
||||||
|
class_info.methods.append(FunctionInfo(
|
||||||
|
name=class_node.name,
|
||||||
|
args=[(arg.arg, ast.unparse(arg.annotation) if arg.annotation else NO_TYPE_ANY) for arg in class_node.args.args],
|
||||||
|
return_type=ast.unparse(class_node.returns) if class_node.returns else "None",
|
||||||
|
docstring=method_docstring if method_docstring else "",
|
||||||
|
type=def_type,
|
||||||
|
is_async=isinstance(class_node, ast.AsyncFunctionDef),
|
||||||
|
source_code=ast.unparse(class_node)
|
||||||
|
))
|
||||||
|
# attributes
|
||||||
|
elif isinstance(class_node, ast.Assign):
|
||||||
|
for target in class_node.targets:
|
||||||
|
if isinstance(target, ast.Name):
|
||||||
|
class_info.attributes.append(AttributeInfo(
|
||||||
|
name=target.id,
|
||||||
|
type=ast.unparse(class_node.value)
|
||||||
|
))
|
||||||
|
module_info.classes.append(class_info)
|
||||||
|
|
||||||
|
elif isinstance(node, ast.Assign):
|
||||||
|
# 检查是否在类或函数中
|
||||||
|
if not any(isinstance(parent, (ast.ClassDef, ast.FunctionDef)) for parent in ast.iter_child_nodes(node)):
|
||||||
|
# 模块属性变量
|
||||||
|
for target in node.targets:
|
||||||
|
if isinstance(target, ast.Name) and (not ignore_private or not target.id.startswith('_')):
|
||||||
|
attr_type = NO_TYPE_HINT
|
||||||
|
if isinstance(node.value, ast.AnnAssign) and node.value.annotation:
|
||||||
|
attr_type = ast.unparse(node.value.annotation)
|
||||||
|
module_info.attributes.append(AttributeInfo(
|
||||||
|
name=target.id,
|
||||||
|
type=attr_type,
|
||||||
|
value=ast.unparse(node.value) if node.value else None
|
||||||
|
))
|
||||||
|
|
||||||
|
return module_info
|
||||||
|
|
||||||
|
|
||||||
|
def generate_markdown(module_info: ModuleInfo, front_matter=None, lang: str = "zh-CN") -> str:
|
||||||
|
"""
|
||||||
|
生成模块的Markdown
|
||||||
|
你可在此自定义生成的Markdown格式
|
||||||
|
Args:
|
||||||
|
module_info: 模块信息
|
||||||
|
front_matter: 自定义选项title, index, icon, category
|
||||||
|
lang: 语言
|
||||||
|
Returns:
|
||||||
|
Markdown 字符串
|
||||||
|
"""
|
||||||
|
|
||||||
|
content = ""
|
||||||
|
|
||||||
|
front_matter = "---\n" + "\n".join([f"{k}: {v}" for k, v in front_matter.items()]) + "\n---\n\n"
|
||||||
|
|
||||||
|
content += front_matter
|
||||||
|
|
||||||
|
# 模块函数
|
||||||
|
for func in module_info.functions:
|
||||||
|
args_with_type = [f"{arg[0]}: {arg[1]}" if arg[1] else arg[0] for arg in func.args]
|
||||||
|
content += f"### ***{'async ' if func.is_async else ''}def*** `{func.name}({', '.join(args_with_type)}) -> {func.return_type}`\n\n"
|
||||||
|
|
||||||
|
func.docstring = func.docstring.replace("\n", "\n\n")
|
||||||
|
content += f"{func.docstring}\n\n"
|
||||||
|
|
||||||
|
# 函数源代码可展开区域
|
||||||
|
content += f"<details>\n<summary>源代码</summary>\n\n```python\n{func.source_code}\n```\n</details>\n\n"
|
||||||
|
|
||||||
|
# 类
|
||||||
|
for cls in module_info.classes:
|
||||||
|
if cls.inherit:
|
||||||
|
inherit = f"({', '.join(cls.inherit)})" if cls.inherit else ""
|
||||||
|
content += f"### ***class*** `{cls.name}{inherit}`\n\n"
|
||||||
|
else:
|
||||||
|
content += f"### ***class*** `{cls.name}`\n\n"
|
||||||
|
|
||||||
|
cls.docstring = cls.docstring.replace("\n", "\n\n")
|
||||||
|
content += f"{cls.docstring}\n\n"
|
||||||
|
for method in cls.methods:
|
||||||
|
# 类函数
|
||||||
|
|
||||||
|
if method.type != DefType.METHOD:
|
||||||
|
args_with_type = [f"{arg[0]}: {arg[1]}" if arg[1] else arg[0] for arg in method.args]
|
||||||
|
content += f"###   ***@{method.type.value}***\n"
|
||||||
|
else:
|
||||||
|
# self不加类型提示
|
||||||
|
args_with_type = [f"{arg[0]}: {arg[1]}" if arg[1] and arg[0] != "self" else arg[0] for arg in method.args]
|
||||||
|
content += f"###   ***{'async ' if method.is_async else ''}def*** `{method.name}({', '.join(args_with_type)}) -> {method.return_type}`\n\n"
|
||||||
|
|
||||||
|
method.docstring = method.docstring.replace("\n", "\n\n")
|
||||||
|
content += f" {method.docstring}\n\n"
|
||||||
|
# 函数源代码可展开区域
|
||||||
|
|
||||||
|
if lang == "zh-CN":
|
||||||
|
TEXT_SOURCE_CODE = "源代码"
|
||||||
|
else:
|
||||||
|
TEXT_SOURCE_CODE = "Source Code"
|
||||||
|
|
||||||
|
content += f"<details>\n<summary>{TEXT_SOURCE_CODE}</summary>\n\n```python\n{method.source_code}\n```\n</details>\n\n"
|
||||||
|
for attr in cls.attributes:
|
||||||
|
content += f"###   ***attr*** `{attr.name}: {attr.type}`\n\n"
|
||||||
|
|
||||||
|
# 模块属性
|
||||||
|
for attr in module_info.attributes:
|
||||||
|
if attr.type == NO_TYPE_HINT:
|
||||||
|
content += f"### ***var*** `{attr.name} = {attr.value}`\n\n"
|
||||||
|
else:
|
||||||
|
content += f"### ***var*** `{attr.name}: {attr.type} = {attr.value}`\n\n"
|
||||||
|
|
||||||
|
attr.docstring = attr.docstring.replace("\n", "\n\n")
|
||||||
|
content += f"{attr.docstring}\n\n"
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def generate_docs(module_folder: str, output_dir: str, with_top: bool = False, lang: str = "zh-CN", ignored_paths=None):
|
||||||
|
"""
|
||||||
|
生成文档
|
||||||
|
Args:
|
||||||
|
module_folder: 模块文件夹
|
||||||
|
output_dir: 输出文件夹
|
||||||
|
with_top: 是否包含顶层文件夹 False时例如docs/api/module_a, docs/api/module_b, True时例如docs/api/module/module_a.md, docs/api/module/module_b.md
|
||||||
|
ignored_paths: 忽略的路径
|
||||||
|
lang: 语言
|
||||||
|
"""
|
||||||
|
if ignored_paths is None:
|
||||||
|
ignored_paths = []
|
||||||
|
file_data: dict[str, str] = {} # 路径 -> 字串
|
||||||
|
|
||||||
|
file_list = get_file_list(module_folder)
|
||||||
|
|
||||||
|
# 清理输出目录
|
||||||
|
shutil.rmtree(output_dir, ignore_errors=True)
|
||||||
|
os.mkdir(output_dir)
|
||||||
|
|
||||||
|
replace_data = {
|
||||||
|
"__init__": "README",
|
||||||
|
".py" : ".md",
|
||||||
|
}
|
||||||
|
|
||||||
|
for pyfile_path in file_list:
|
||||||
|
if any(ignored_path.replace("\\", "/") in pyfile_path.replace("\\", "/") for ignored_path in ignored_paths):
|
||||||
|
continue
|
||||||
|
|
||||||
|
no_module_name_pyfile_path = get_relative_path(module_folder, pyfile_path) # 去头路径
|
||||||
|
|
||||||
|
# markdown相对路径
|
||||||
|
rel_md_path = pyfile_path if with_top else no_module_name_pyfile_path
|
||||||
|
for rk, rv in replace_data.items():
|
||||||
|
rel_md_path = rel_md_path.replace(rk, rv)
|
||||||
|
|
||||||
|
abs_md_path = os.path.join(output_dir, rel_md_path)
|
||||||
|
|
||||||
|
# 获取模块信息
|
||||||
|
module_info = get_module_info_normal(pyfile_path)
|
||||||
|
|
||||||
|
# 生成markdown
|
||||||
|
|
||||||
|
if "README" in abs_md_path:
|
||||||
|
front_matter = {
|
||||||
|
"title" : module_info.module_path.replace(".__init__", "").replace("_", "\\n"),
|
||||||
|
"index" : "true",
|
||||||
|
"icon" : "laptop-code",
|
||||||
|
"category": "API"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
front_matter = {
|
||||||
|
"title" : module_info.module_path.replace("_", "\\n"),
|
||||||
|
"order" : "1",
|
||||||
|
"icon" : "laptop-code",
|
||||||
|
"category": "API"
|
||||||
|
}
|
||||||
|
|
||||||
|
md_content = generate_markdown(module_info, front_matter)
|
||||||
|
print(f"Generate {pyfile_path} -> {abs_md_path}")
|
||||||
|
file_data[abs_md_path] = md_content
|
||||||
|
|
||||||
|
write_to_files(file_data)
|
||||||
|
|
||||||
|
|
||||||
|
# 入口脚本
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# 这里填入你的模块路径
|
||||||
|
generate_docs('liteyuki', 'docs/dev/api', with_top=False, ignored_paths=["liteyuki/plugins"], lang="zh-CN")
|
||||||
|
generate_docs('liteyuki', 'docs/en/dev/api', with_top=False, ignored_paths=["liteyuki/plugins"], lang="en")
|
@ -1,11 +1,12 @@
|
|||||||
from liteyuki.plugin.model import Plugin, PluginMetadata
|
from liteyuki.plugin.model import Plugin, PluginMetadata, PluginType
|
||||||
from liteyuki.plugin.load import load_plugin, load_plugins, _plugins
|
from liteyuki.plugin.load import load_plugin, load_plugins, _plugins
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"PluginMetadata",
|
"PluginMetadata",
|
||||||
"Plugin",
|
"Plugin",
|
||||||
"load_plugin",
|
"PluginType",
|
||||||
"load_plugins",
|
"load_plugin",
|
||||||
|
"load_plugins",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,14 +10,12 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
|||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
|
from importlib import import_module
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from nonebot import logger
|
from liteyuki.log import logger
|
||||||
|
from liteyuki.plugin.model import Plugin, PluginMetadata, PluginType
|
||||||
from liteyuki.plugin.model import Plugin, PluginMetadata
|
|
||||||
from importlib import import_module
|
|
||||||
|
|
||||||
from liteyuki.utils import path_to_module_name
|
from liteyuki.utils import path_to_module_name
|
||||||
|
|
||||||
_plugins: dict[str, Plugin] = {}
|
_plugins: dict[str, Plugin] = {}
|
||||||
@ -25,6 +23,7 @@ _plugins: dict[str, Plugin] = {}
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
"load_plugin",
|
"load_plugin",
|
||||||
"load_plugins",
|
"load_plugins",
|
||||||
|
"_plugins",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -35,45 +34,92 @@ def load_plugin(module_path: str | Path) -> Optional[Plugin]:
|
|||||||
module_path: 插件名称 `path.to.your.plugin`
|
module_path: 插件名称 `path.to.your.plugin`
|
||||||
或插件路径 `pathlib.Path(path/to/your/plugin)`
|
或插件路径 `pathlib.Path(path/to/your/plugin)`
|
||||||
"""
|
"""
|
||||||
module_path = path_to_module_name(Path(module_path)) if isinstance(module_path, Path) else module_path
|
module_path = (
|
||||||
|
path_to_module_name(Path(module_path))
|
||||||
|
if isinstance(module_path, Path)
|
||||||
|
else module_path
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
module = import_module(module_path)
|
module = import_module(module_path)
|
||||||
_plugins[module.__name__] = Plugin(
|
_plugins[module.__name__] = Plugin(
|
||||||
name=module.__name__,
|
name=module.__name__,
|
||||||
module=module,
|
module=module,
|
||||||
module_name=module_path,
|
module_name=module_path,
|
||||||
metadata=module.__dict__.get("__plugin_metadata__", None)
|
|
||||||
)
|
)
|
||||||
|
if module.__dict__.get("__plugin_metadata__", None):
|
||||||
|
metadata: "PluginMetadata" = module.__dict__["__plugin_metadata__"]
|
||||||
|
display_name = module.__name__.split(".")[-1]
|
||||||
|
elif module.__dict__.get("__liteyuki_plugin_meta__", None):
|
||||||
|
metadata: "PluginMetadata" = module.__dict__["__liteyuki_plugin_meta__"]
|
||||||
|
display_name = format_display_name(
|
||||||
|
f"{metadata.name}({module.__name__.split('.')[-1]})", metadata.type
|
||||||
|
)
|
||||||
|
elif module.__dict__.get("__plugin_meta__", None):
|
||||||
|
metadata: "PluginMetadata" = module.__dict__["__plugin_meta__"]
|
||||||
|
display_name = format_display_name(
|
||||||
|
f"{metadata.name}({module.__name__.split('.')[-1]})", metadata.type
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
|
||||||
|
logger.opt(colors=True).warning(
|
||||||
|
f'轻雪插件 "{module.__name__}" 的元信息未指定,将采用空的元信息'
|
||||||
|
)
|
||||||
|
|
||||||
|
metadata = PluginMetadata(
|
||||||
|
name=module.__name__,
|
||||||
|
)
|
||||||
|
display_name = module.__name__.split(".")[-1]
|
||||||
|
|
||||||
|
_plugins[module.__name__].metadata = metadata
|
||||||
|
|
||||||
logger.opt(colors=True).success(
|
logger.opt(colors=True).success(
|
||||||
f'成功加载 轻雪插件 "<y>{module.__name__.split(".")[-1]}</y>"'
|
f'成功加载轻雪插件 "{display_name}"'
|
||||||
)
|
)
|
||||||
return _plugins[module.__name__]
|
return _plugins[module.__name__]
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.opt(colors=True).success(
|
logger.opt(colors=True).success(
|
||||||
f'未能加载 轻雪插件 "<r>{module_path}</r>"'
|
f'无法加载轻雪插件 "<r>{module_path}</r>"'
|
||||||
)
|
)
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def load_plugins(*plugin_dir: str) -> set[Plugin]:
|
def load_plugins(*plugin_dir: str, ignore_warning: bool = True) -> set[Plugin]:
|
||||||
"""导入文件夹下多个插件
|
"""导入文件夹下多个插件
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
plugin_dir: 文件夹路径
|
plugin_dir: 文件夹路径
|
||||||
|
ignore_warning: 是否忽略警告,通常是目录不存在或目录为空
|
||||||
"""
|
"""
|
||||||
plugins = set()
|
plugins = set()
|
||||||
for dir_path in plugin_dir:
|
for dir_path in plugin_dir:
|
||||||
# 遍历每一个文件夹下的py文件和包含__init__.py的文件夹,不递归
|
# 遍历每一个文件夹下的py文件和包含__init__.py的文件夹,不递归
|
||||||
|
if not os.path.exists(dir_path):
|
||||||
|
if not ignore_warning:
|
||||||
|
logger.warning(f"插件目录 '{dir_path}' 不存在")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not os.listdir(dir_path):
|
||||||
|
if not ignore_warning:
|
||||||
|
logger.warning(f"插件目录 '{dir_path}' 为空")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not os.path.isdir(dir_path):
|
||||||
|
if not ignore_warning:
|
||||||
|
logger.warning(f"本应是插件目录的路径 '{dir_path}' 并非如此")
|
||||||
|
continue
|
||||||
|
|
||||||
for f in os.listdir(dir_path):
|
for f in os.listdir(dir_path):
|
||||||
path = Path(os.path.join(dir_path, f))
|
path = Path(os.path.join(dir_path, f))
|
||||||
|
|
||||||
module_name = None
|
module_name = None
|
||||||
if os.path.isfile(path) and f.endswith('.py') and f != '__init__.py':
|
if os.path.isfile(path) and f.endswith(".py") and f != "__init__.py":
|
||||||
module_name = f"{path_to_module_name(Path(dir_path))}.{f[:-3]}"
|
module_name = f"{path_to_module_name(Path(dir_path))}.{f[:-3]}"
|
||||||
|
|
||||||
elif os.path.isdir(path) and os.path.exists(os.path.join(path, '__init__.py')):
|
elif os.path.isdir(path) and os.path.exists(
|
||||||
|
os.path.join(path, "__init__.py")
|
||||||
|
):
|
||||||
module_name = path_to_module_name(path)
|
module_name = path_to_module_name(path)
|
||||||
|
|
||||||
if module_name:
|
if module_name:
|
||||||
@ -81,3 +127,27 @@ def load_plugins(*plugin_dir: str) -> set[Plugin]:
|
|||||||
if _plugins.get(module_name):
|
if _plugins.get(module_name):
|
||||||
plugins.add(_plugins[module_name])
|
plugins.add(_plugins[module_name])
|
||||||
return plugins
|
return plugins
|
||||||
|
|
||||||
|
|
||||||
|
def format_display_name(display_name: str, plugin_type: PluginType) -> str:
|
||||||
|
"""
|
||||||
|
设置插件名称颜色,根据不同类型插件设置颜色
|
||||||
|
Args:
|
||||||
|
display_name: 插件名称
|
||||||
|
plugin_type: 插件类型
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 设置后的插件名称 <y>name</y>
|
||||||
|
"""
|
||||||
|
color = "y"
|
||||||
|
match plugin_type:
|
||||||
|
case PluginType.APPLICATION:
|
||||||
|
color = "m"
|
||||||
|
case PluginType.TEST:
|
||||||
|
color = "g"
|
||||||
|
case PluginType.MODULE:
|
||||||
|
color = "e"
|
||||||
|
case PluginType.SERVICE:
|
||||||
|
color = "c"
|
||||||
|
|
||||||
|
return f"<{color}>{display_name} [{plugin_type.name}]</{color}>"
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
Copyright (C) 2020-2024 LiteyukiStudio. All rights reserved
|
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||||
|
|
||||||
版权所有 © 2020-2024 神羽SnowyKami & 金羿Eilles with LiteyukiStudio & TriM Org.
|
|
||||||
保留所有权利
|
|
||||||
|
|
||||||
@Time : 2024/7/23 下午11:59
|
@Time : 2024/7/23 下午11:59
|
||||||
@Author : snowykami
|
@Author : snowykami
|
||||||
|
@ -8,22 +8,61 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
|||||||
@File : model.py
|
@File : model.py
|
||||||
@Software: PyCharm
|
@Software: PyCharm
|
||||||
"""
|
"""
|
||||||
|
from enum import Enum
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class PluginType(Enum):
|
||||||
|
"""
|
||||||
|
插件类型枚举值
|
||||||
|
"""
|
||||||
|
APPLICATION = "application"
|
||||||
|
"""应用端:例如NoneBot"""
|
||||||
|
|
||||||
|
SERVICE = "service"
|
||||||
|
"""服务端:例如AI绘画后端"""
|
||||||
|
|
||||||
|
MODULE = "module"
|
||||||
|
"""模块:导出对象给其他插件使用"""
|
||||||
|
|
||||||
|
UNCLASSIFIED = "unclassified"
|
||||||
|
"""未分类:默认值"""
|
||||||
|
|
||||||
|
TEST = "test"
|
||||||
|
"""测试:测试插件"""
|
||||||
|
|
||||||
|
|
||||||
class PluginMetadata(BaseModel):
|
class PluginMetadata(BaseModel):
|
||||||
"""
|
"""
|
||||||
轻雪插件元数据,由插件编写者提供,name为必填项
|
轻雪插件元数据,由插件编写者提供,name为必填项
|
||||||
|
Attributes:
|
||||||
|
----------
|
||||||
|
|
||||||
|
name: str
|
||||||
|
插件名称
|
||||||
|
description: str
|
||||||
|
插件描述
|
||||||
|
usage: str
|
||||||
|
插件使用方法
|
||||||
|
type: str
|
||||||
|
插件类型
|
||||||
|
author: str
|
||||||
|
插件作者
|
||||||
|
homepage: str
|
||||||
|
插件主页
|
||||||
|
extra: dict[str, Any]
|
||||||
|
额外信息
|
||||||
"""
|
"""
|
||||||
name: str
|
name: str
|
||||||
description: str = ""
|
description: str = ""
|
||||||
usage: str = ""
|
usage: str = ""
|
||||||
type: str = ""
|
type: PluginType = PluginType.UNCLASSIFIED
|
||||||
|
author: str = ""
|
||||||
homepage: str = ""
|
homepage: str = ""
|
||||||
running_in_main: bool = True # 是否在主进程运行
|
extra: dict[str, Any] = {}
|
||||||
|
|
||||||
|
|
||||||
class Plugin(BaseModel):
|
class Plugin(BaseModel):
|
||||||
|
5
liteyuki/plugins/__init__.py
Normal file
5
liteyuki/plugins/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
此模块为内置插件文件夹,用于存放内置插件。
|
||||||
|
This module is the built-in plugin folder, used to store built-in plugins.
|
||||||
|
"""
|
@ -1,55 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
|
||||||
#
|
|
||||||
# @Time : 2024/7/22 上午11:25
|
|
||||||
# @Author : snowykami
|
|
||||||
# @Email : snowykami@outlook.com
|
|
||||||
# @File : asa.py
|
|
||||||
# @Software: PyCharm
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from liteyuki.plugin import PluginMetadata
|
|
||||||
from liteyuki import get_bot, logger
|
|
||||||
from liteyuki.comm.channel import get_channel
|
|
||||||
|
|
||||||
__plugin_meta__ = PluginMetadata(
|
|
||||||
name="lifespan_monitor",
|
|
||||||
)
|
|
||||||
|
|
||||||
bot = get_bot()
|
|
||||||
nbp_chan = get_channel("nonebot-passive")
|
|
||||||
mbp_chan = get_channel("melobot-passive")
|
|
||||||
|
|
||||||
|
|
||||||
@bot.on_before_start
|
|
||||||
def _():
|
|
||||||
logger.info("生命周期监控器:启动前")
|
|
||||||
|
|
||||||
|
|
||||||
@bot.on_before_shutdown
|
|
||||||
def _():
|
|
||||||
print(get_channel("main"))
|
|
||||||
logger.info("生命周期监控器:停止前")
|
|
||||||
|
|
||||||
|
|
||||||
@bot.on_before_restart
|
|
||||||
def _():
|
|
||||||
logger.info("生命周期监控器:重启前")
|
|
||||||
|
|
||||||
|
|
||||||
@bot.on_after_start
|
|
||||||
def _():
|
|
||||||
logger.info("生命周期监控器:启动后")
|
|
||||||
|
|
||||||
|
|
||||||
@bot.on_after_start
|
|
||||||
async def _():
|
|
||||||
logger.info("生命周期监控器:启动后")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# @mbp_chan.on_receive()
|
|
||||||
# @nbp_chan.on_receive()
|
|
||||||
# async def _(data):
|
|
||||||
# print("主进程收到数据", data)
|
|
19
liteyuki/plugins/liteecho.py
Normal file
19
liteyuki/plugins/liteecho.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||||
|
|
||||||
|
@Time : 2024/8/22 下午12:31
|
||||||
|
@Author : snowykami
|
||||||
|
@Email : snowykami@outlook.com
|
||||||
|
@File : liteecho.py
|
||||||
|
@Software: PyCharm
|
||||||
|
"""
|
||||||
|
|
||||||
|
from liteyuki.message.on import on_startswith
|
||||||
|
from liteyuki.message.event import MessageEvent
|
||||||
|
from liteyuki.message.rule import is_su_rule
|
||||||
|
|
||||||
|
|
||||||
|
@on_startswith(["ryounecho", "ryeco"], rule=is_su_rule).handle()
|
||||||
|
async def liteecho(event: MessageEvent):
|
||||||
|
event.reply(event.raw_message.strip()[8:].strip())
|
32
liteyuki/plugins/plugin_loader/__init__.py
Normal file
32
liteyuki/plugins/plugin_loader/__init__.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||||
|
|
||||||
|
@Time : 2024/8/11 下午10:02
|
||||||
|
@Author : snowykami
|
||||||
|
@Email : snowykami@outlook.com
|
||||||
|
@File : __init__.py.py
|
||||||
|
@Software: PyCharm
|
||||||
|
"""
|
||||||
|
from liteyuki import get_config, load_plugin
|
||||||
|
from liteyuki.plugin import PluginMetadata, load_plugins, PluginType
|
||||||
|
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
name="外部轻雪插件加载器",
|
||||||
|
description="插件加载器,用于加载轻雪原生插件",
|
||||||
|
type=PluginType.SERVICE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def default_plugins_loader():
|
||||||
|
"""
|
||||||
|
默认插件加载器,应在初始化时调用
|
||||||
|
"""
|
||||||
|
for plugin in get_config("liteyuki.plugins", []):
|
||||||
|
load_plugin(plugin)
|
||||||
|
|
||||||
|
for plugin_dir in get_config("liteyuki.plugin_dirs", ["src/liteyuki_plugins"]):
|
||||||
|
load_plugins(plugin_dir)
|
||||||
|
|
||||||
|
|
||||||
|
default_plugins_loader()
|
@ -4,11 +4,15 @@
|
|||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import inspect
|
import inspect
|
||||||
|
import multiprocessing
|
||||||
|
import threading
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Coroutine
|
from typing import Any, Callable, Coroutine
|
||||||
|
|
||||||
from liteyuki.log import logger
|
from liteyuki.log import logger
|
||||||
|
|
||||||
|
IS_MAIN_PROCESS = multiprocessing.current_process().name == "MainProcess"
|
||||||
|
|
||||||
|
|
||||||
def is_coroutine_callable(call: Callable[..., Any]) -> bool:
|
def is_coroutine_callable(call: Callable[..., Any]) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -39,7 +43,7 @@ def run_coroutine(*coro: Coroutine):
|
|||||||
# 检测是否有现有的事件循环
|
# 检测是否有现有的事件循环
|
||||||
|
|
||||||
try:
|
try:
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_running_loop()
|
||||||
if loop.is_running():
|
if loop.is_running():
|
||||||
# 如果事件循环正在运行,创建任务
|
# 如果事件循环正在运行,创建任务
|
||||||
for c in coro:
|
for c in coro:
|
||||||
@ -59,6 +63,18 @@ def run_coroutine(*coro: Coroutine):
|
|||||||
logger.error(f"协程异常:{e}")
|
logger.error(f"协程异常:{e}")
|
||||||
|
|
||||||
|
|
||||||
|
def run_coroutine_in_thread(*coro: Coroutine):
|
||||||
|
"""
|
||||||
|
在新线程中运行协程
|
||||||
|
Args:
|
||||||
|
coro:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
threading.Thread(target=run_coroutine, args=coro, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
def path_to_module_name(path: Path) -> str:
|
def path_to_module_name(path: Path) -> str:
|
||||||
"""
|
"""
|
||||||
转换路径为模块名
|
转换路径为模块名
|
||||||
@ -72,3 +88,19 @@ def path_to_module_name(path: Path) -> str:
|
|||||||
return ".".join(rel_path.parts[:-1])
|
return ".".join(rel_path.parts[:-1])
|
||||||
else:
|
else:
|
||||||
return ".".join(rel_path.parts[:-1] + (rel_path.stem,))
|
return ".".join(rel_path.parts[:-1] + (rel_path.stem,))
|
||||||
|
|
||||||
|
|
||||||
|
def async_wrapper(func: Callable[..., Any]) -> Callable[..., Coroutine]:
|
||||||
|
"""
|
||||||
|
异步包装器
|
||||||
|
Args:
|
||||||
|
func: Sync Callable
|
||||||
|
Returns:
|
||||||
|
Coroutine: Asynchronous Callable
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
wrapper.__signature__ = inspect.signature(func)
|
||||||
|
return wrapper
|
||||||
|
8
main.py
8
main.py
@ -1,6 +1,10 @@
|
|||||||
|
"""
|
||||||
|
启动脚本,会执行一些启动的操作,比如加载配置文件,初始化 bot 实例等。
|
||||||
|
"""
|
||||||
from liteyuki import LiteyukiBot
|
from liteyuki import LiteyukiBot
|
||||||
from liteyuki.config import load_from_yaml
|
from liteyuki.config import load_config_in_default
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
bot = LiteyukiBot(**load_from_yaml("config.yml"))
|
bot = LiteyukiBot(**load_config_in_default(no_waring=True))
|
||||||
bot.run()
|
bot.run()
|
||||||
|
@ -1,58 +1,44 @@
|
|||||||
# PEP 621 project metadata
|
# PEP 621 project metadata
|
||||||
# See https://www.python.org/dev/peps/pep-0621/
|
# See https://www.python.org/dev/peps/pep-0621/
|
||||||
# This file is for project use, but don`t use with nb-cli
|
# This file is liteyuki framework use only, don`t use it with applications or nb-cli.
|
||||||
# 此文件为项目所用,请不要和nb-cli一起使用以防被修改
|
# 此文件仅供 liteyuki 框架使用,请勿用于应用程序及nb-cli,请使用pip进行安装。
|
||||||
[tool.poetry]
|
[project]
|
||||||
name = "ryoun-trim"
|
name = "ryoun-trim"
|
||||||
version = "0"
|
dynamic = ["version"]
|
||||||
description = "based on liteyuki6"
|
description = "A lightweight bot process management framework and application."
|
||||||
authors = ["金羿Eilles"]
|
readme = "README.md"
|
||||||
license = "MIT & LSO"
|
requires-python = ">=3.10"
|
||||||
package-mode = false
|
authors = [
|
||||||
|
{ name = "Eilles", email = "EillesWan@outlook.com" },
|
||||||
|
{ name = "TriM-Org.", email = "TriM-Organization@hotmail.com" },
|
||||||
|
{ name = "snowykami", email = "snowykami@outlook.com" },
|
||||||
|
{ name = "LiteyukiStudio", email = "studio@liteyuki.icu" },
|
||||||
|
]
|
||||||
|
license = { text = "汉钰律许可协议 第一版" }
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
[tool.poetry.dependencies]
|
"loguru~=0.7.2",
|
||||||
python = "^3.10"
|
"pydantic==2.8.2",
|
||||||
aiofiles = "~23.2.1"
|
"PyYAML==6.0.2",
|
||||||
aiohttp = "~3.9.3"
|
"toml==0.10.2",
|
||||||
aiosqlite3 = "~0.3.0"
|
"watchdog==4.0.1",
|
||||||
colored = "~2.2.4"
|
"pdm-backend==2.3.3"
|
||||||
fastapi = "~0.110.0"
|
]
|
||||||
GitPython = "~3.1.42"
|
|
||||||
httpx = "~0.27.0"
|
|
||||||
importlib_metadata = "~7.0.2"
|
|
||||||
jieba = "~0.42.1"
|
|
||||||
loguru = "~0.7.2"
|
|
||||||
nb-cli = "~1.4.1"
|
|
||||||
nonebot-adapter-onebot = "~2.4.3"
|
|
||||||
nonebot-adapter-satori = "~0.11.5"
|
|
||||||
nonebot-plugin-alconna = "~0.46.3"
|
|
||||||
nonebot-plugin-apscheduler = "~0.4.0"
|
|
||||||
nonebot-plugin-htmlrender = "~0.3.1"
|
|
||||||
nonebot2 = { version = "~2.3.0", extras = ["fastapi", "httpx", "websockets"] }
|
|
||||||
numpy = "<2.0.0"
|
|
||||||
packaging = "~23.1"
|
|
||||||
psutil = "~5.9.8"
|
|
||||||
py-cpuinfo = "~9.0.0"
|
|
||||||
pydantic = "~2.7.0"
|
|
||||||
Pygments = "~2.17.2"
|
|
||||||
python-dotenv = "~1.0.1"
|
|
||||||
pytest = "~8.3.1"
|
|
||||||
pytz = "~2024.1"
|
|
||||||
PyYAML = "~6.0.1"
|
|
||||||
requests = "~2.31.0"
|
|
||||||
starlette = "~0.36.3"
|
|
||||||
watchdog = "~4.0.0"
|
|
||||||
|
|
||||||
|
|
||||||
[[tool.poetry.source]]
|
|
||||||
name = "tuna"
|
|
||||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
|
||||||
|
|
||||||
[tool.nonebot]
|
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
homepage = "https://bot.liteyuki.icu"
|
Homepage = "https://bot.liteyuki.icu"
|
||||||
repository = "https://gitee.com/TriM-Organization/LiteyukiBot-TriM"
|
Repository = "https://gitee.com/TriM-Organization/LiteyukiBot-TriM"
|
||||||
documentation = "https://bot.liteyuki.icu"
|
"Issue Tracker" = "https://github.com/LiteyukiStudio/LiteyukiBot/issues/new?assignees=&labels=&projects=&template=%E9%97%AE%E9%A2%98%E5%8F%8D%E9%A6%88.md&title="
|
||||||
|
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["pdm-backend"]
|
||||||
|
build-backend = "pdm.backend"
|
||||||
|
|
||||||
|
[tool.pdm.build]
|
||||||
|
includes = ["liteyuki/", "LICENSE", "README.md", "LICENSE.MD"]
|
||||||
|
excludes = ["tests/", "docs/", "src/"]
|
||||||
|
|
||||||
|
[tool.pdm.version]
|
||||||
|
source = "file"
|
||||||
|
path = "liteyuki/__init__.py"
|
@ -1,35 +1,34 @@
|
|||||||
aiohttp~=3.9.3
|
aiohttp>=3.9.3
|
||||||
aiofiles~=23.2.1
|
aiofiles>=23.2.1
|
||||||
colored~=2.2.4
|
colored>=2.2.4
|
||||||
GitPython~=3.1.42
|
GitPython>=3.1.43
|
||||||
httpx~=0.27.0
|
httpx>=0.27.0
|
||||||
nb-cli~=1.4.1
|
nonebot-plugin-htmlrender>=0.1.0
|
||||||
nonebot2[fastapi,httpx,websockets]~=2.3.0
|
nonebot2[fastapi,httpx,websockets]>=2.3.3
|
||||||
nonebot-plugin-htmlrender~=0.3.1
|
nonebot-adapter-onebot>=2.4.3
|
||||||
nonebot-adapter-onebot~=2.4.3
|
nonebot-plugin-alconna>=0.46.3
|
||||||
nonebot-plugin-alconna~=0.46.3
|
nonebot_plugin_apscheduler>=0.4.0
|
||||||
nonebot_plugin_apscheduler~=0.4.0
|
nonebot-adapter-satori>=0.11.5
|
||||||
nonebot-adapter-satori~=0.11.5
|
# pyppeteer>=2.0.0
|
||||||
|
markdown>=3.3.6
|
||||||
|
zhDateTime>=1.1.1
|
||||||
numpy<2.0.0
|
numpy<2.0.0
|
||||||
packaging~=23.1
|
packaging>=23.1
|
||||||
psutil~=5.9.8
|
psutil>=5.9.8
|
||||||
py-cpuinfo~=9.0.0
|
py-cpuinfo>=9.0.0
|
||||||
pydantic~=2.7.0
|
Pygments>=2.17.2
|
||||||
Pygments~=2.17.2
|
pyppeteer>=2.0.0
|
||||||
pytz~=2024.1
|
pytz>=2024.1
|
||||||
PyYAML~=6.0.1
|
PyYAML>=6.0.1
|
||||||
starlette~=0.36.3
|
pillow>=10.0.0
|
||||||
loguru~=0.7.2
|
toml>=0.10.2
|
||||||
importlib_metadata~=7.0.2
|
importlib_metadata>=7.0.2
|
||||||
requests~=2.31.0
|
watchdog>=4.0.0
|
||||||
watchdog~=4.0.0
|
jieba>=0.42.1
|
||||||
pillow~=10.0.0
|
python-dotenv>=1.0.1
|
||||||
jieba~=0.42.1
|
|
||||||
aiosqlite3~=0.3.0
|
|
||||||
fastapi~=0.110.0
|
|
||||||
python-dotenv~=1.0.1
|
|
||||||
nonebot_plugin_session
|
nonebot_plugin_session
|
||||||
pypinyin
|
pypinyin
|
||||||
zhDateTime>=1.0.3
|
|
||||||
Musicreater>=2.2.0
|
Musicreater>=2.2.0
|
||||||
librosa==0.10.1
|
librosa==0.10.1
|
||||||
|
TrimMCStruct
|
||||||
|
brotli
|
@ -2,7 +2,6 @@ from nonebot.plugin import PluginMetadata
|
|||||||
|
|
||||||
from .core import *
|
from .core import *
|
||||||
from .loader import *
|
from .loader import *
|
||||||
from .dev import *
|
|
||||||
|
|
||||||
__author__ = "snowykami"
|
__author__ = "snowykami"
|
||||||
__plugin_meta__ = PluginMetadata(
|
__plugin_meta__ = PluginMetadata(
|
||||||
@ -18,29 +17,6 @@ __plugin_meta__ = PluginMetadata(
|
|||||||
|
|
||||||
from ..utils.base.language import Language, get_default_lang_code
|
from ..utils.base.language import Language, get_default_lang_code
|
||||||
|
|
||||||
print(
|
|
||||||
"\033[34m"
|
|
||||||
+ r"""
|
|
||||||
▅▅▅▅▅▅▅▅▅▅▅▅▅▅██ ▅▅▅▅▅▅▅▅▅▅▅▅▅▅██ ██ ▅▅▅▅▅▅▅▅▅▅█™
|
|
||||||
▛ ██ ██ ▛ ██ ███ ██ ██
|
|
||||||
██ ██ ███████████████ ██ ████████▅ ██
|
|
||||||
███████████████ ██ ███ ██ ██
|
|
||||||
██ ██ ▅██████████████▛ ██ ████████████
|
|
||||||
██ ██ ███ ███
|
|
||||||
████████████████ ██▅ ███ ██ ▅▅▅▅▅▅▅▅▅▅▅██
|
|
||||||
███ █ ▜███████ ██ ███ ██ ██ ██ ██
|
|
||||||
███ ███ █████▛ ██ ██ ██ ██ ██
|
|
||||||
███ ██ ███ █ ██ ██ ██ ██ ██
|
|
||||||
███ █████ ██████ ███ ██████████████
|
|
||||||
商标标记 © 2024 金羿Eilles
|
|
||||||
版权所有 © 2020-2024 神羽SnowyKami & 金羿Eilles\\
|
|
||||||
with LiteyukiStudio & TriM Org.
|
|
||||||
保留所有权利
|
|
||||||
"""
|
|
||||||
+ "\033[0m"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
sys_lang = Language(get_default_lang_code())
|
sys_lang = Language(get_default_lang_code())
|
||||||
nonebot.logger.info(
|
nonebot.logger.info(
|
||||||
sys_lang.get("main.current_language", LANG=sys_lang.get("language.name"))
|
sys_lang.get("main.current_language", LANG=sys_lang.get("language.name"))
|
||||||
|
@ -1,44 +1,43 @@
|
|||||||
import base64
|
|
||||||
import time
|
import time
|
||||||
from typing import Any, AnyStr
|
from typing import AnyStr
|
||||||
|
|
||||||
|
import time
|
||||||
|
from typing import AnyStr
|
||||||
|
|
||||||
import nonebot
|
import nonebot
|
||||||
import pip
|
import pip
|
||||||
from nonebot import Bot, get_driver, require # type: ignore
|
from nonebot import get_driver, require
|
||||||
from nonebot.adapters import onebot, satori
|
from nonebot.adapters import onebot, satori
|
||||||
from nonebot.adapters.onebot.v11 import Message, escape, unescape
|
from nonebot.adapters.onebot.v11 import Message, unescape
|
||||||
from nonebot.exception import MockApiException
|
|
||||||
from nonebot.internal.matcher import Matcher
|
from nonebot.internal.matcher import Matcher
|
||||||
from nonebot.permission import SUPERUSER
|
from nonebot.permission import SUPERUSER
|
||||||
|
|
||||||
|
# from src.liteyuki.core import Reloader
|
||||||
from src.utils import event as event_utils, satori_utils
|
from src.utils import event as event_utils, satori_utils
|
||||||
from src.utils.base.config import get_config, load_from_yaml
|
from src.utils.base.config import get_config
|
||||||
from src.utils.base.data_manager import StoredConfig, TempConfig, common_db
|
from src.utils.base.data_manager import TempConfig, common_db
|
||||||
from src.utils.base.language import get_user_lang
|
from src.utils.base.language import get_user_lang
|
||||||
from src.utils.base.ly_typing import T_Bot, T_MessageEvent
|
from src.utils.base.ly_typing import T_Bot, T_MessageEvent
|
||||||
from src.utils.message.message import MarkdownMessage as md, broadcast_to_superusers
|
from src.utils.message.message import MarkdownMessage as md, broadcast_to_superusers
|
||||||
|
from .api import update_liteyuki # type: ignore
|
||||||
from .api import update_liteyuki
|
from ..utils.base import reload # type: ignore
|
||||||
from ..utils.base import reload
|
from ..utils.base.ly_function import get_function # type: ignore
|
||||||
from ..utils.base.ly_function import get_function
|
from ..utils.message.html_tool import md_to_pic
|
||||||
|
|
||||||
require("nonebot_plugin_alconna")
|
require("nonebot_plugin_alconna")
|
||||||
require("nonebot_plugin_apscheduler")
|
require("nonebot_plugin_apscheduler")
|
||||||
from nonebot_plugin_alconna import (
|
from nonebot_plugin_alconna import (
|
||||||
|
UniMessage,
|
||||||
on_alconna,
|
on_alconna,
|
||||||
Alconna,
|
Alconna,
|
||||||
Args,
|
Args,
|
||||||
Subcommand,
|
|
||||||
Arparma,
|
Arparma,
|
||||||
MultiVar,
|
MultiVar,
|
||||||
)
|
)
|
||||||
from nonebot_plugin_apscheduler import scheduler
|
from nonebot_plugin_apscheduler import scheduler
|
||||||
|
|
||||||
driver = get_driver()
|
|
||||||
|
|
||||||
markdown_image = common_db.where_one(StoredConfig(), default=StoredConfig()).config.get(
|
driver = get_driver()
|
||||||
"markdown_image", False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@on_alconna(
|
@on_alconna(
|
||||||
@ -50,8 +49,8 @@ markdown_image = common_db.where_one(StoredConfig(), default=StoredConfig()).con
|
|||||||
).handle()
|
).handle()
|
||||||
# Satori OK
|
# Satori OK
|
||||||
async def _(bot: T_Bot, matcher: Matcher, result: Arparma):
|
async def _(bot: T_Bot, matcher: Matcher, result: Arparma):
|
||||||
if result.main_args.get("text"):
|
if text := result.main_args.get("text"):
|
||||||
await matcher.finish(Message(unescape(result.main_args.get("text")))) # type: ignore
|
await matcher.finish(Message(unescape(text)))
|
||||||
else:
|
else:
|
||||||
await matcher.finish(f"君安!灵温向你问好~\n此机 {bot.self_id}")
|
await matcher.finish(f"君安!灵温向你问好~\n此机 {bot.self_id}")
|
||||||
|
|
||||||
@ -72,7 +71,7 @@ async def _(bot: T_Bot, matcher: Matcher, result: Arparma):
|
|||||||
aliases={"更新灵温"}, command=Alconna("update-ryoun"), permission=SUPERUSER
|
aliases={"更新灵温"}, command=Alconna("update-ryoun"), permission=SUPERUSER
|
||||||
).handle()
|
).handle()
|
||||||
# Satori OK
|
# Satori OK
|
||||||
async def _(bot: T_Bot, event: T_MessageEvent):
|
async def _(bot: T_Bot, event: T_MessageEvent, matcher: Matcher):
|
||||||
# 使用git pull更新
|
# 使用git pull更新
|
||||||
|
|
||||||
ulang = get_user_lang(
|
ulang = get_user_lang(
|
||||||
@ -84,7 +83,9 @@ async def _(bot: T_Bot, event: T_MessageEvent):
|
|||||||
btn_restart = md.btn_cmd(ulang.get("liteyuki.restart_now"), "reload-liteyuki")
|
btn_restart = md.btn_cmd(ulang.get("liteyuki.restart_now"), "reload-liteyuki")
|
||||||
pip.main(["install", "-r", "requirements.txt"])
|
pip.main(["install", "-r", "requirements.txt"])
|
||||||
reply += f"{ulang.get('liteyuki.update_restart', RESTART=btn_restart)}"
|
reply += f"{ulang.get('liteyuki.update_restart', RESTART=btn_restart)}"
|
||||||
await md.send_md(reply, bot, event=event, at_sender=False)
|
# await md.send_md(reply, bot)
|
||||||
|
img_bytes = await md_to_pic(reply)
|
||||||
|
await UniMessage.send(UniMessage.image(raw=img_bytes))
|
||||||
|
|
||||||
|
|
||||||
@on_alconna(
|
@on_alconna(
|
||||||
@ -115,108 +116,9 @@ async def _(matcher: Matcher, bot: T_Bot, event: T_MessageEvent):
|
|||||||
)
|
)
|
||||||
|
|
||||||
common_db.save(temp_data)
|
common_db.save(temp_data)
|
||||||
|
|
||||||
reload()
|
reload()
|
||||||
|
|
||||||
|
|
||||||
@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()
|
|
||||||
# Satori OK
|
|
||||||
async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, matcher: Matcher):
|
|
||||||
ulang = get_user_lang(str(event_utils.get_user_id(event)))
|
|
||||||
stored_config: StoredConfig = common_db.where_one(
|
|
||||||
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()
|
|
||||||
# Satori OK
|
|
||||||
async def _(event: T_MessageEvent, matcher: Matcher):
|
|
||||||
global markdown_image
|
|
||||||
# 切换图片模式,False以图片形式发送,True以markdown形式发送
|
|
||||||
ulang = get_user_lang(str(event_utils.get_user_id(event)))
|
|
||||||
stored_config: StoredConfig = common_db.where_one(
|
|
||||||
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()
|
|
||||||
# # Satori OK
|
|
||||||
# async def _(matcher: Matcher):
|
|
||||||
# await matcher.finish("https://bot.liteyuki.icu/usage")
|
|
||||||
|
|
||||||
|
|
||||||
@on_alconna(
|
@on_alconna(
|
||||||
command=Alconna(
|
command=Alconna(
|
||||||
@ -318,62 +220,6 @@ async def _(result: Arparma, bot: T_Bot, event: T_MessageEvent, matcher: Matcher
|
|||||||
await matcher.finish(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
|
@driver.on_startup
|
||||||
async def on_startup():
|
async def on_startup():
|
||||||
temp_data = common_db.where_one(TempConfig(), default=TempConfig())
|
temp_data = common_db.where_one(TempConfig(), default=TempConfig())
|
||||||
@ -426,6 +272,14 @@ async def _(bot: T_Bot):
|
|||||||
group_id=reload_session_id,
|
group_id=reload_session_id,
|
||||||
message=return_msg,
|
message=return_msg,
|
||||||
)
|
)
|
||||||
|
elif isinstance(bot, onebot.v12.Bot):
|
||||||
|
await bot.send_msg(
|
||||||
|
message_type=reload_session_type,
|
||||||
|
user_id=reload_session_id,
|
||||||
|
group_id=reload_session_id,
|
||||||
|
message=return_msg,
|
||||||
|
detail_type="group",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await bot.call_api(
|
await bot.call_api(
|
||||||
"send_msg",
|
"send_msg",
|
||||||
@ -445,7 +299,6 @@ async def every_day_update():
|
|||||||
if result:
|
if result:
|
||||||
await broadcast_to_superusers(f"灵温已更新:```\n{logs}\n```")
|
await broadcast_to_superusers(f"灵温已更新:```\n{logs}\n```")
|
||||||
nonebot.logger.info(f"灵温已更新:{logs}")
|
nonebot.logger.info(f"灵温已更新:{logs}")
|
||||||
# ProcessingManager.restart(3)
|
|
||||||
reload()
|
reload()
|
||||||
else:
|
else:
|
||||||
nonebot.logger.info(logs)
|
nonebot.logger.info(logs)
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
import nonebot
|
|
||||||
from watchdog.observers import Observer
|
|
||||||
from watchdog.events import FileSystemEventHandler
|
|
||||||
|
|
||||||
from liteyuki.bot import get_bot
|
|
||||||
from src.utils.base import reload
|
|
||||||
from src.utils.base.config import get_config
|
|
||||||
from src.utils.base.resource import load_resources
|
|
||||||
|
|
||||||
if get_config("debug", False):
|
|
||||||
|
|
||||||
liteyuki_bot = get_bot()
|
|
||||||
|
|
||||||
res_directories = (
|
|
||||||
"src/resources",
|
|
||||||
"resources",
|
|
||||||
)
|
|
||||||
|
|
||||||
class ResourceModifiedHandler(FileSystemEventHandler):
|
|
||||||
"""
|
|
||||||
Handler for resource file changes
|
|
||||||
"""
|
|
||||||
|
|
||||||
def on_modified(self, event):
|
|
||||||
nonebot.logger.info(f"资源 {event.src_path} 变更,重载资源包……")
|
|
||||||
load_resources()
|
|
||||||
|
|
||||||
resource_modified_handle = ResourceModifiedHandler()
|
|
||||||
|
|
||||||
observer = Observer()
|
|
||||||
for directory in res_directories:
|
|
||||||
observer.schedule(resource_modified_handle, directory, recursive=True)
|
|
||||||
observer.start()
|
|
@ -8,15 +8,10 @@ from src.utils.base.data_manager import InstalledPlugin, plugin_db
|
|||||||
from src.utils.base.resource import load_resources
|
from src.utils.base.resource import load_resources
|
||||||
from src.utils.message.tools import check_for_package
|
from src.utils.message.tools import check_for_package
|
||||||
|
|
||||||
from liteyuki import get_bot, chan
|
|
||||||
|
|
||||||
from nonebot_plugin_apscheduler import scheduler
|
|
||||||
|
|
||||||
load_resources()
|
load_resources()
|
||||||
init_log()
|
init_log()
|
||||||
|
|
||||||
driver = get_driver()
|
driver = get_driver()
|
||||||
liteyuki_bot = get_bot()
|
|
||||||
|
|
||||||
|
|
||||||
@driver.on_startup
|
@driver.on_startup
|
||||||
@ -32,7 +27,7 @@ async def load_plugins():
|
|||||||
for installed_plugin in installed_plugins:
|
for installed_plugin in installed_plugins:
|
||||||
if not check_for_package(installed_plugin.module_name):
|
if not check_for_package(installed_plugin.module_name):
|
||||||
nonebot.logger.error(
|
nonebot.logger.error(
|
||||||
f"插件 {installed_plugin.module_name} 在加载列表中但未安装。请使用超管账户对机器人发送 `npm fixup` 以重新安装。"
|
f"插件 {installed_plugin.module_name} 仍在加载列表中但未安装。"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
nonebot.load_plugin(installed_plugin.module_name)
|
nonebot.load_plugin(installed_plugin.module_name)
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
# 说明
|
|
||||||
|
|
||||||
此目录为**轻雪插件**目录,非其他插件目录。
|
|
24
src/liteyuki_plugins/hello_liteyuki.py
Normal file
24
src/liteyuki_plugins/hello_liteyuki.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||||
|
|
||||||
|
@Time : 2024/8/20 上午5:12
|
||||||
|
@Author : snowykami
|
||||||
|
@Email : snowykami@outlook.com
|
||||||
|
@File : liteyuki_reply.py
|
||||||
|
@Software: PyCharm
|
||||||
|
"""
|
||||||
|
from liteyuki.plugin import PluginMetadata, PluginType
|
||||||
|
from liteyuki.message.on import on_message
|
||||||
|
from liteyuki.message.event import MessageEvent
|
||||||
|
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
name="你好轻雪",
|
||||||
|
type=PluginType.APPLICATION
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@on_message().handle()
|
||||||
|
async def _(event: MessageEvent):
|
||||||
|
if str(event.raw_message) == "你好轻雪":
|
||||||
|
event.reply("你好呀")
|
42
src/liteyuki_plugins/lifespan_monitor.py
Normal file
42
src/liteyuki_plugins/lifespan_monitor.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||||
|
#
|
||||||
|
# @Time : 2024/7/22 上午11:25
|
||||||
|
# @Author : snowykami
|
||||||
|
# @Email : snowykami@outlook.com
|
||||||
|
# @File : asa.py
|
||||||
|
# @Software: PyCharm
|
||||||
|
import asyncio
|
||||||
|
import multiprocessing
|
||||||
|
|
||||||
|
from liteyuki.plugin import PluginMetadata, PluginType
|
||||||
|
from liteyuki import get_bot, logger
|
||||||
|
from liteyuki.comm.channel import get_channel
|
||||||
|
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
name="生命周期日志",
|
||||||
|
type=PluginType.SERVICE,
|
||||||
|
)
|
||||||
|
|
||||||
|
bot = get_bot()
|
||||||
|
|
||||||
|
|
||||||
|
@bot.on_before_start
|
||||||
|
def _():
|
||||||
|
logger.info("生命周期监控器:准备启动")
|
||||||
|
|
||||||
|
|
||||||
|
@bot.on_before_process_shutdown
|
||||||
|
def _(name="name"):
|
||||||
|
logger.info("生命周期监控器:准备停止")
|
||||||
|
|
||||||
|
|
||||||
|
@bot.on_before_process_restart
|
||||||
|
def _(name="name"):
|
||||||
|
logger.info("生命周期监控器:准备重启")
|
||||||
|
|
||||||
|
|
||||||
|
@bot.on_after_start
|
||||||
|
async def _():
|
||||||
|
logger.info("生命周期监控器:启动完成")
|
53
src/liteyuki_plugins/liteyukibot_plugin_nonebot/__init__.py
Normal file
53
src/liteyuki_plugins/liteyukibot_plugin_nonebot/__init__.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||||
|
|
||||||
|
@Time : 2024/8/11 下午5:24
|
||||||
|
@Author : snowykami
|
||||||
|
@Email : snowykami@outlook.com
|
||||||
|
@File : __init__.py.py
|
||||||
|
@Software: PyCharm
|
||||||
|
"""
|
||||||
|
|
||||||
|
import nonebot
|
||||||
|
from liteyuki.utils import IS_MAIN_PROCESS
|
||||||
|
from liteyuki.plugin import PluginMetadata, PluginType
|
||||||
|
from .nb_utils import adapter_manager, driver_manager # type: ignore
|
||||||
|
from liteyuki.log import logger
|
||||||
|
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
name="NoneBot2启动器",
|
||||||
|
type=PluginType.APPLICATION,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def nb_run(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
初始化NoneBot并运行在子进程
|
||||||
|
Args:
|
||||||
|
**kwargs:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
"""
|
||||||
|
# 给子进程传递通道对象
|
||||||
|
kwargs.update(kwargs.get("nonebot", {})) # nonebot配置优先
|
||||||
|
nonebot.init(**kwargs)
|
||||||
|
|
||||||
|
driver_manager.init(config=kwargs)
|
||||||
|
adapter_manager.init(kwargs)
|
||||||
|
adapter_manager.register()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# nonebot.load_plugin("nonebot-plugin-lnpm") # 尝试加载轻雪NoneBot插件加载器(Nonebot插件)
|
||||||
|
nonebot.load_plugin("src.liteyuki_main") # 尝试加载轻雪主插件(Nonebot插件)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
nonebot.run()
|
||||||
|
|
||||||
|
|
||||||
|
if IS_MAIN_PROCESS:
|
||||||
|
from liteyuki import get_bot
|
||||||
|
from .dev_reloader import *
|
||||||
|
|
||||||
|
liteyuki = get_bot()
|
||||||
|
liteyuki.process_manager.add_target(name="nonebot", target=nb_run, args=(), kwargs=liteyuki.config)
|
@ -0,0 +1,24 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
NoneBot 开发环境重载监视器
|
||||||
|
"""
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
from liteyuki.dev import observer
|
||||||
|
from liteyuki import get_bot, logger
|
||||||
|
from liteyuki.utils import IS_MAIN_PROCESS
|
||||||
|
from watchdog.events import FileSystemEvent
|
||||||
|
|
||||||
|
|
||||||
|
liteyuki = get_bot()
|
||||||
|
|
||||||
|
exclude_extensions = (".pyc", ".pyo")
|
||||||
|
|
||||||
|
|
||||||
|
@observer.on_file_system_event(
|
||||||
|
directories=("src/nonebot_plugins",),
|
||||||
|
event_filter=lambda event: not event.src_path.endswith(exclude_extensions) and ("__pycache__" not in event.src_path ) and os.path.isfile(event.src_path)
|
||||||
|
)
|
||||||
|
def restart_nonebot_process(event: FileSystemEvent):
|
||||||
|
logger.debug(f"文件 {event.src_path} 已更新,正在重载 nonebot")
|
||||||
|
liteyuki.restart_process("nonebot")
|
@ -7,14 +7,14 @@ from nonebot.adapters import satori
|
|||||||
|
|
||||||
def init(config: dict):
|
def init(config: dict):
|
||||||
if config.get("satori", None) is None:
|
if config.get("satori", None) is None:
|
||||||
nonebot.logger.info("未查见 Satori 的配置文档,将跳过 Satori 初始化")
|
nonebot.logger.info("未寻得 Satori 设定信息,跳过初始化")
|
||||||
return None
|
return None
|
||||||
satori_config = config.get("satori")
|
satori_config = config.get("satori")
|
||||||
if not satori_config.get("enable", False):
|
if not satori_config.get("enable", False):
|
||||||
nonebot.logger.info("未启用 Satori ,将跳过 Satori 初始化")
|
nonebot.logger.info("Satori 未启用,跳过初始化")
|
||||||
return None
|
return None
|
||||||
if os.getenv("SATORI_CLIENTS", None) is not None:
|
if os.getenv("SATORI_CLIENTS", None) is not None:
|
||||||
nonebot.logger.info("Satori 客户端已设入环境变量,跳过此步。")
|
nonebot.logger.info("Satori 客户端已在环境变量中配置,跳过初始化")
|
||||||
os.environ["SATORI_CLIENTS"] = json.dumps(satori_config.get("hosts", []), ensure_ascii=False)
|
os.environ["SATORI_CLIENTS"] = json.dumps(satori_config.get("hosts", []), ensure_ascii=False)
|
||||||
config['satori_clients'] = satori_config.get("hosts", [])
|
config['satori_clients'] = satori_config.get("hosts", [])
|
||||||
return
|
return
|
@ -9,12 +9,12 @@ from .defines import *
|
|||||||
def auto_set_env(config: dict):
|
def auto_set_env(config: dict):
|
||||||
dotenv.load_dotenv(".env")
|
dotenv.load_dotenv(".env")
|
||||||
if os.getenv("DRIVER", None) is not None:
|
if os.getenv("DRIVER", None) is not None:
|
||||||
nonebot.logger.info("Driver 已设入环境变量中,将跳过自动配置环节。")
|
nonebot.logger.info("Driver 已在环境变量中配置,跳过自动设定")
|
||||||
return
|
return
|
||||||
if config.get("satori", {'enable': False}).get("enable", False):
|
if config.get("satori", {'enable': False}).get("enable", False):
|
||||||
os.environ["DRIVER"] = get_driver_string(ASGI_DRIVER, HTTPX_DRIVER, WEBSOCKETS_DRIVER)
|
os.environ["DRIVER"] = get_driver_string(ASGI_DRIVER, HTTPX_DRIVER, WEBSOCKETS_DRIVER)
|
||||||
nonebot.logger.info("已启用 Satori,将 driver 设为 ASGI+HTTPX+WEBSOCKETS")
|
nonebot.logger.info("启用 Satori,已设定 Driver 为 ASGI+HTTPX+WEBSOCKETS")
|
||||||
else:
|
else:
|
||||||
os.environ["DRIVER"] = get_driver_string(ASGI_DRIVER)
|
os.environ["DRIVER"] = get_driver_string(ASGI_DRIVER)
|
||||||
nonebot.logger.info("已禁用 Satori,将 driver 设为 ASGI")
|
nonebot.logger.info("禁用 Satori,已设定 Driver 为 ASGI")
|
||||||
return
|
return
|
8
src/liteyuki_plugins/process_manager/__init__.py
Normal file
8
src/liteyuki_plugins/process_manager/__init__.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from liteyuki.plugin import PluginMetadata, PluginType
|
||||||
|
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
name="进程管理器",
|
||||||
|
author="snowykami",
|
||||||
|
description="进程管理器,用于管理子进程",
|
||||||
|
type=PluginType.SERVICE
|
||||||
|
)
|
@ -11,21 +11,22 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
|||||||
import json
|
import json
|
||||||
import os.path
|
import os.path
|
||||||
import platform
|
import platform
|
||||||
|
from aiohttp import ClientSession
|
||||||
import requests
|
|
||||||
from git import Repo
|
from git import Repo
|
||||||
from liteyuki.plugin import PluginMetadata
|
|
||||||
|
from liteyuki.plugin import PluginMetadata, PluginType
|
||||||
from liteyuki import get_bot, logger
|
from liteyuki import get_bot, logger
|
||||||
|
|
||||||
__plugin_meta__ = PluginMetadata(
|
__plugin_meta__ = PluginMetadata(
|
||||||
name="注册服务",
|
name="注册服务",
|
||||||
|
type=PluginType.SERVICE
|
||||||
)
|
)
|
||||||
|
|
||||||
liteyuki = get_bot()
|
liteyuki = get_bot()
|
||||||
commit_hash = Repo(".").head.commit.hexsha
|
commit_hash = Repo(".").head.commit.hexsha
|
||||||
|
|
||||||
|
|
||||||
def register_bot():
|
async def register_bot():
|
||||||
url = "https://api.liteyuki.icu/register"
|
url = "https://api.liteyuki.icu/register"
|
||||||
data = {
|
data = {
|
||||||
"name" : "尹灵温|轻雪-睿乐",
|
"name" : "尹灵温|轻雪-睿乐",
|
||||||
@ -36,17 +37,19 @@ def register_bot():
|
|||||||
"os" : f"{platform.system()} {platform.version()} {platform.machine()}"
|
"os" : f"{platform.system()} {platform.version()} {platform.machine()}"
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
logger.info("正在等待 Liteyuki 注册服务器……")
|
logger.info("正在等待 Liteyuki 注册服务器…")
|
||||||
resp = requests.post(url, json=data, timeout=(10, 15))
|
async with ClientSession() as session:
|
||||||
if resp.status_code == 200:
|
async with session.post(url, json=data, timeout=15) as resp:
|
||||||
data = resp.json()
|
if resp.status == 200:
|
||||||
if liteyuki_id := data.get("liteyuki_id"):
|
data = await resp.json()
|
||||||
with open("data/liteyuki/liteyuki.json", "wb") as f:
|
if liteyuki_id := data.get("liteyuki_id"):
|
||||||
f.write(json.dumps(data).encode("utf-8"))
|
with open("data/liteyuki/liteyuki.json", "wb") as f:
|
||||||
logger.success("成功将 {} 注册到 Liteyuki 服务器".format(liteyuki_id))
|
f.write(json.dumps(data).encode("utf-8"))
|
||||||
else:
|
logger.success("成功将 {} 注册到 Liteyuki 服务器".format(liteyuki_id))
|
||||||
raise ValueError(f"无法向 Liteyuki 服务器注册:{data}")
|
else:
|
||||||
|
raise ValueError(f"无法向 Liteyuki 服务器注册:{data}")
|
||||||
|
else:
|
||||||
|
raise ValueError(f"无法向 Liteyuki 服务器注册:{resp.status}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"虽然向 Liteyuki 服务器注册失败,但无关紧要:{e}")
|
logger.warning(f"虽然向 Liteyuki 服务器注册失败,但无关紧要:{e}")
|
||||||
|
|
||||||
@ -54,4 +57,6 @@ def register_bot():
|
|||||||
@liteyuki.on_before_start
|
@liteyuki.on_before_start
|
||||||
async def _():
|
async def _():
|
||||||
if not os.path.exists("data/liteyuki/liteyuki.json"):
|
if not os.path.exists("data/liteyuki/liteyuki.json"):
|
||||||
register_bot()
|
if not os.path.exists("data/liteyuki"):
|
||||||
|
os.makedirs("data/liteyuki")
|
||||||
|
await register_bot()
|
8
src/liteyuki_plugins/resource_loader/__init__.py
Normal file
8
src/liteyuki_plugins/resource_loader/__init__.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from liteyuki.plugin import PluginMetadata, PluginType
|
||||||
|
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
name="资源加载器",
|
||||||
|
author="snowykami",
|
||||||
|
description="进程管理器,用于管理子进程",
|
||||||
|
type=PluginType.SERVICE
|
||||||
|
)
|
19
src/liteyuki_plugins/scheduled_tasks/__init__.py
Normal file
19
src/liteyuki_plugins/scheduled_tasks/__init__.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||||
|
|
||||||
|
@Time : 2024/8/15 下午11:29
|
||||||
|
@Author : snowykami
|
||||||
|
@Email : snowykami@outlook.com
|
||||||
|
@File : __init__.py.py
|
||||||
|
@Software: PyCharm
|
||||||
|
"""
|
||||||
|
from liteyuki.plugin import PluginMetadata, PluginType
|
||||||
|
|
||||||
|
from .divided_by_lifespan import *
|
||||||
|
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
name="计划任务",
|
||||||
|
description="计划任务插件,一些杂项任务的计划执行。",
|
||||||
|
type=PluginType.SERVICE
|
||||||
|
)
|
@ -0,0 +1,11 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||||
|
|
||||||
|
@Time : 2024/8/15 下午11:32
|
||||||
|
@Author : snowykami
|
||||||
|
@Email : snowykami@outlook.com
|
||||||
|
@File : __init__.py
|
||||||
|
@Software: PyCharm
|
||||||
|
"""
|
||||||
|
from .after_start import *
|
@ -0,0 +1,25 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||||
|
|
||||||
|
@Time : 2024/8/15 下午11:32
|
||||||
|
@Author : snowykami
|
||||||
|
@Email : snowykami@outlook.com
|
||||||
|
@File : after_start.py
|
||||||
|
@Software: PyCharm
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
|
||||||
|
from liteyuki import get_bot
|
||||||
|
from liteyuki.comm.storage import shared_memory
|
||||||
|
|
||||||
|
liteyuki = get_bot()
|
||||||
|
|
||||||
|
|
||||||
|
@liteyuki.on_before_start
|
||||||
|
def save_startup_timestamp():
|
||||||
|
"""
|
||||||
|
储存启动的时间戳
|
||||||
|
"""
|
||||||
|
startup_timestamp = time.time()
|
||||||
|
shared_memory.set("startup_timestamp", startup_timestamp)
|
@ -1,27 +0,0 @@
|
|||||||
import multiprocessing
|
|
||||||
|
|
||||||
from nonebot.plugin import PluginMetadata
|
|
||||||
from liteyuki.comm import get_channel
|
|
||||||
from .rt_guide import *
|
|
||||||
from .crt_matchers 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,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# chan = get_channel("nonebot-passive")
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# @chan.on_receive()
|
|
||||||
# async def _(d):
|
|
||||||
# print("CRT子进程接收到数据:", d)
|
|
||||||
# chan.send("CRT子进程已接收到数据")
|
|
@ -1,575 +0,0 @@
|
|||||||
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")
|
|
@ -1,78 +0,0 @@
|
|||||||
from urllib.parse import quote
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
from nonebot import require
|
|
||||||
|
|
||||||
from src.utils.event import get_user_id
|
|
||||||
from src.utils.base.language import Language
|
|
||||||
from src.utils.base.ly_typing import T_MessageEvent
|
|
||||||
from src.utils.base.resource import get_path
|
|
||||||
from src.utils.message.html_tool import template2image
|
|
||||||
|
|
||||||
require("nonebot_plugin_alconna")
|
|
||||||
|
|
||||||
from nonebot_plugin_alconna import UniMessage, on_alconna, Alconna, Args, Subcommand, Arparma, Option
|
|
||||||
|
|
||||||
crt_cmd = on_alconna(
|
|
||||||
Alconna(
|
|
||||||
"crt",
|
|
||||||
Subcommand(
|
|
||||||
"route",
|
|
||||||
Args["start", str, "沙坪坝"]["end", str, "上新街"],
|
|
||||||
alias=("r",),
|
|
||||||
help_text="查询两地之间的地铁路线"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@crt_cmd.assign("route")
|
|
||||||
async def _(result: Arparma, event: T_MessageEvent):
|
|
||||||
# 获取语言
|
|
||||||
ulang = Language(get_user_id(event))
|
|
||||||
|
|
||||||
# 获取参数
|
|
||||||
# 你也别问我为什么要quote两次,问就是CRT官网的锅,只有这样才可以运行
|
|
||||||
start = quote(quote(result.other_args.get("start")))
|
|
||||||
end = quote(quote(result.other_args.get("end")))
|
|
||||||
|
|
||||||
# 判断参数语言
|
|
||||||
query_lang_code = ""
|
|
||||||
if start.isalpha() and end.isalpha():
|
|
||||||
query_lang_code = "Eng"
|
|
||||||
|
|
||||||
# 构造请求 URL
|
|
||||||
url = f"https://www.cqmetro.cn/Front/html/TakeLine!queryYs{query_lang_code}TakeLine.action?entity.startStaName={start}&entity.endStaName={end}"
|
|
||||||
|
|
||||||
# 请求数据
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.get(url) as resp:
|
|
||||||
result = await resp.json()
|
|
||||||
|
|
||||||
# 检查结果/无则终止
|
|
||||||
if not result.get("result"):
|
|
||||||
await crt_cmd.send(ulang.get("crt.no_result"))
|
|
||||||
return
|
|
||||||
|
|
||||||
# 模板传参定义
|
|
||||||
templates = {
|
|
||||||
"data" : {
|
|
||||||
"result": result["result"],
|
|
||||||
},
|
|
||||||
"localization": ulang.get_many(
|
|
||||||
"crt.station",
|
|
||||||
"crt.hour",
|
|
||||||
"crt.minute",
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
# 生成图片
|
|
||||||
image = await template2image(
|
|
||||||
template=get_path("templates/crt_route.html"),
|
|
||||||
templates=templates,
|
|
||||||
debug=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# 发送图片
|
|
||||||
await crt_cmd.send(UniMessage.image(raw=image))
|
|
@ -1,419 +0,0 @@
|
|||||||
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: 分辨率,1:reso
|
|
||||||
: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()
|
|
@ -1,15 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
||||||
)
|
|
@ -1,168 +0,0 @@
|
|||||||
import random
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from src.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
|
|
||||||
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
|
|
@ -1,103 +0,0 @@
|
|||||||
from nonebot import require
|
|
||||||
|
|
||||||
from src.utils.base.ly_typing import T_Bot, T_MessageEvent
|
|
||||||
from src.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("参数错误")
|
|
@ -14,17 +14,21 @@ from nonebot.permission import SUPERUSER
|
|||||||
from nonebot.plugin import Plugin, PluginMetadata
|
from nonebot.plugin import Plugin, PluginMetadata
|
||||||
from nonebot.utils import run_sync
|
from nonebot.utils import run_sync
|
||||||
|
|
||||||
|
|
||||||
from src.utils.base.data_manager import InstalledPlugin
|
from src.utils.base.data_manager import InstalledPlugin
|
||||||
from src.utils.base.language import get_user_lang
|
from src.utils.base.language import get_user_lang
|
||||||
from src.utils.base.ly_typing import T_Bot
|
from src.utils.base.ly_typing import T_Bot
|
||||||
from src.utils.message.message import MarkdownMessage as md
|
|
||||||
from src.utils.message.markdown import MarkdownComponent as mdc, compile_md, escape_md
|
|
||||||
from src.utils.base.permission import GROUP_ADMIN, GROUP_OWNER
|
from src.utils.base.permission import GROUP_ADMIN, GROUP_OWNER
|
||||||
from src.utils.message.tools import clamp
|
from src.utils.message.tools import clamp
|
||||||
|
from src.utils.message.message import MarkdownMessage as md
|
||||||
|
from src.utils.message.markdown import MarkdownComponent as mdc, compile_md, escape_md
|
||||||
|
from src.utils.message.html_tool import md_to_pic
|
||||||
from .common import *
|
from .common import *
|
||||||
|
|
||||||
|
|
||||||
require("nonebot_plugin_alconna")
|
require("nonebot_plugin_alconna")
|
||||||
from nonebot_plugin_alconna import (
|
from nonebot_plugin_alconna import (
|
||||||
|
UniMessage,
|
||||||
on_alconna,
|
on_alconna,
|
||||||
Alconna,
|
Alconna,
|
||||||
Args,
|
Args,
|
||||||
@ -147,7 +151,7 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
|
|||||||
session_id = event.group_id
|
session_id = event.group_id
|
||||||
new_event = event
|
new_event = event
|
||||||
else:
|
else:
|
||||||
raise FinishedException(ulang.get("Permission Denied"))
|
raise FinishedException(ulang.get("liteyuki.permission_denied"))
|
||||||
|
|
||||||
session_enable = get_plugin_session_enable(
|
session_enable = get_plugin_session_enable(
|
||||||
new_event, plugin_name
|
new_event, plugin_name
|
||||||
@ -292,7 +296,8 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
|
|||||||
reply += f"\n{ulang.get('npm.too_many_results', HIDE_NUM=len(rs) - max_show)}"
|
reply += f"\n{ulang.get('npm.too_many_results', HIDE_NUM=len(rs) - max_show)}"
|
||||||
else:
|
else:
|
||||||
reply = ulang.get("npm.search_no_result")
|
reply = ulang.get("npm.search_no_result")
|
||||||
await md.send_md(reply, bot, event=event)
|
img_bytes = await md_to_pic(reply)
|
||||||
|
await UniMessage.send(UniMessage.image(raw=img_bytes))
|
||||||
|
|
||||||
elif sc.get("install") and perm_s:
|
elif sc.get("install") and perm_s:
|
||||||
plugin_name: str = result.subcommands["install"].args.get("plugin_name")
|
plugin_name: str = result.subcommands["install"].args.get("plugin_name")
|
||||||
@ -320,7 +325,7 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
|
|||||||
info = md.escape(
|
info = md.escape(
|
||||||
ulang.get("npm.install_success", NAME=store_plugin.name)
|
ulang.get("npm.install_success", NAME=store_plugin.name)
|
||||||
) # markdown转义
|
) # markdown转义
|
||||||
await md.send_md(f"{info}\n\n" f"```\n{log}\n```", bot, event=event)
|
await npm.send(f"{info}\n\n" + f"\n{log}\n")
|
||||||
else:
|
else:
|
||||||
await npm.finish(
|
await npm.finish(
|
||||||
ulang.get(
|
ulang.get(
|
||||||
@ -331,12 +336,12 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
|
|||||||
info = ulang.get(
|
info = ulang.get(
|
||||||
"npm.load_failed", NAME=plugin_name, HOMEPAGE=homepage_btn
|
"npm.load_failed", NAME=plugin_name, HOMEPAGE=homepage_btn
|
||||||
).replace("_", r"\\_")
|
).replace("_", r"\\_")
|
||||||
await md.send_md(f"{info}\n\n" f"```\n{log}\n```\n", bot, event=event)
|
await npm.finish(f"{info}\n\n" f"```\n{log}\n```\n")
|
||||||
else:
|
else:
|
||||||
info = ulang.get(
|
info = ulang.get(
|
||||||
"npm.install_failed", NAME=plugin_name, HOMEPAGE=homepage_btn
|
"npm.install_failed", NAME=plugin_name, HOMEPAGE=homepage_btn
|
||||||
).replace("_", r"\\_")
|
).replace("_", r"\\_")
|
||||||
await md.send_md(f"{info}\n\n" f"```\n{log}\n```", bot, event=event)
|
await npm.send(f"{info}\n\n" f"```\n{log}\n```")
|
||||||
|
|
||||||
elif sc.get("uninstall") and perm_s:
|
elif sc.get("uninstall") and perm_s:
|
||||||
plugin_name: str = result.subcommands["uninstall"].args.get("plugin_name") # type: ignore
|
plugin_name: str = result.subcommands["uninstall"].args.get("plugin_name") # type: ignore
|
||||||
@ -464,7 +469,8 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
|
|||||||
else ulang.get("npm.next_page")
|
else ulang.get("npm.next_page")
|
||||||
)
|
)
|
||||||
reply += f"\n{btn_prev} {page}/{total} {btn_next}"
|
reply += f"\n{btn_prev} {page}/{total} {btn_next}"
|
||||||
await md.send_md(reply, bot, event=event)
|
img_bytes = await md_to_pic(reply)
|
||||||
|
await UniMessage.send(UniMessage.image(raw=img_bytes))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if await SUPERUSER(bot, event):
|
if await SUPERUSER(bot, event):
|
||||||
@ -517,7 +523,8 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
|
|||||||
f"\n\n>page为页数,num为每页显示数量"
|
f"\n\n>page为页数,num为每页显示数量"
|
||||||
f"\n\n>*{md.escape('npm list [page] [num]')}*"
|
f"\n\n>*{md.escape('npm list [page] [num]')}*"
|
||||||
)
|
)
|
||||||
await md.send_md(reply, bot, event=event)
|
img_bytes = await md_to_pic(reply)
|
||||||
|
await UniMessage.send(UniMessage.image(raw=img_bytes))
|
||||||
else:
|
else:
|
||||||
|
|
||||||
btn_list = md.btn_cmd(
|
btn_list = md.btn_cmd(
|
||||||
@ -539,7 +546,8 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
|
|||||||
f"\n\n>page为页数,num为每页显示数量"
|
f"\n\n>page为页数,num为每页显示数量"
|
||||||
f"\n\n>*{md.escape('npm list [page] [num]')}*"
|
f"\n\n>*{md.escape('npm list [page] [num]')}*"
|
||||||
)
|
)
|
||||||
await md.send_md(reply, bot, event=event)
|
img_bytes = await md_to_pic(reply)
|
||||||
|
await UniMessage.send(UniMessage.image(raw=img_bytes))
|
||||||
|
|
||||||
|
|
||||||
@on_alconna(
|
@on_alconna(
|
||||||
@ -554,7 +562,7 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
|
|||||||
Subcommand(
|
Subcommand(
|
||||||
disable,
|
disable,
|
||||||
Args["group_id", str, None],
|
Args["group_id", str, None],
|
||||||
alias=["d", "停用", "禁用"],
|
alias=["d", "停用"],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
permission=SUPERUSER | GROUP_OWNER | GROUP_ADMIN,
|
permission=SUPERUSER | GROUP_OWNER | GROUP_ADMIN,
|
||||||
@ -679,7 +687,7 @@ async def _(result: Arparma, matcher: Matcher, event: T_MessageEvent, bot: T_Bot
|
|||||||
else mdc.paragraph(ulang.get("npm.homepage"))
|
else mdc.paragraph(ulang.get("npm.homepage"))
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
await md.send_md(compile_md(reply), bot, event=event)
|
await matcher.finish(compile_md(reply))
|
||||||
else:
|
else:
|
||||||
await matcher.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name))
|
await matcher.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name))
|
||||||
else:
|
else:
|
||||||
|
@ -198,7 +198,7 @@ async def _(bot: T_Bot, event: T_MessageEvent, result: Arparma, matcher: Matcher
|
|||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
if send_as_md:
|
if send_as_md:
|
||||||
await md.send_md(reply, bot, event=event)
|
await matcher.send(reply)
|
||||||
else:
|
else:
|
||||||
if reply:
|
if reply:
|
||||||
await matcher.finish(reply)
|
await matcher.finish(reply)
|
||||||
|
@ -2,7 +2,6 @@ import nonebot
|
|||||||
|
|
||||||
from nonebot.message import event_preprocessor
|
from nonebot.message import event_preprocessor
|
||||||
|
|
||||||
# from nonebot_plugin_alconna.typings import Event
|
|
||||||
from src.utils.base.ly_typing import T_MessageEvent
|
from src.utils.base.ly_typing import T_MessageEvent
|
||||||
from src.utils import satori_utils
|
from src.utils import satori_utils
|
||||||
from nonebot.adapters import satori
|
from nonebot.adapters import satori
|
||||||
|
@ -1,163 +0,0 @@
|
|||||||
import datetime
|
|
||||||
import time
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
from nonebot import require
|
|
||||||
from nonebot.plugin import PluginMetadata
|
|
||||||
|
|
||||||
from src.utils.base.config import get_config
|
|
||||||
from src.utils.base.data import Database, LiteModel
|
|
||||||
from src.utils.base.resource import get_path
|
|
||||||
from src.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=(
|
|
||||||
"sign count 查看当前签名数\n"
|
|
||||||
"sign data 查看签名数变化\n"
|
|
||||||
"sign chart [limit] 查看签名数变化图表\n"
|
|
||||||
),
|
|
||||||
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 = "QPS 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.where_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)/(stamp*60))
|
|
||||||
reply += f"\n{name}: " + ", ".join([f"{i:.1f}" 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.where_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]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
img = await template2image(
|
|
||||||
template=get_path("templates/sign_status.html"),
|
|
||||||
templates={
|
|
||||||
"data": data
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return img
|
|
@ -61,6 +61,8 @@ async def get_stat_msg_image(
|
|||||||
condition_args.append(user_id)
|
condition_args.append(user_id)
|
||||||
|
|
||||||
msg_rows = msg_db.where_all(MessageEventModel(), condition, *condition_args)
|
msg_rows = msg_db.where_all(MessageEventModel(), condition, *condition_args)
|
||||||
|
if not msg_rows:
|
||||||
|
msg_rows = []
|
||||||
timestamps = []
|
timestamps = []
|
||||||
msg_count = []
|
msg_count = []
|
||||||
msg_rows.sort(key=lambda x: x.time)
|
msg_rows.sort(key=lambda x: x.time)
|
||||||
@ -157,8 +159,8 @@ async def get_stat_rank_image(
|
|||||||
templates = {
|
templates = {
|
||||||
"data": {
|
"data": {
|
||||||
"name": ulang.get("stat.rank")
|
"name": ulang.get("stat.rank")
|
||||||
+ f" 类别:{rank_type}"
|
+ f" Type {rank_type}"
|
||||||
+ f" 制约:{limit}",
|
+ f" Limit {limit}",
|
||||||
"ranking": ranking,
|
"ranking": ranking,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -96,8 +96,10 @@ async def _(result: Arparma, event: T_MessageEvent, bot: Bot):
|
|||||||
bot_id = result.other_args.get("bot_id")
|
bot_id = result.other_args.get("bot_id")
|
||||||
user_id = result.other_args.get("user_id")
|
user_id = result.other_args.get("user_id")
|
||||||
|
|
||||||
if group_id in ["current", "c"]:
|
if group_id in ["current", "c"] and hasattr(event, "group_id"):
|
||||||
group_id = str(event_utils.get_group_id(event))
|
group_id = str(event_utils.get_group_id(event))
|
||||||
|
else:
|
||||||
|
group_id = "all"
|
||||||
|
|
||||||
if group_id in ["all", "a"]:
|
if group_id in ["all", "a"]:
|
||||||
group_id = "all"
|
group_id = "all"
|
||||||
|
@ -7,7 +7,8 @@ from cpuinfo import cpuinfo
|
|||||||
from nonebot import require
|
from nonebot import require
|
||||||
from nonebot.adapters import satori
|
from nonebot.adapters import satori
|
||||||
|
|
||||||
from src.utils import __NAME__, __VERSION__
|
from src.utils import __NAME__
|
||||||
|
from liteyuki import __version__
|
||||||
from src.utils.base.config import get_config
|
from src.utils.base.config import get_config
|
||||||
from src.utils.base.data_manager import TempConfig, common_db
|
from src.utils.base.data_manager import TempConfig, common_db
|
||||||
from src.utils.base.language import Language
|
from src.utils.base.language import Language
|
||||||
@ -227,11 +228,19 @@ async def get_hardware_data() -> dict:
|
|||||||
pass
|
pass
|
||||||
swap = psutil.swap_memory()
|
swap = psutil.swap_memory()
|
||||||
cpu_brand_raw = cpuinfo.get_cpu_info().get("brand_raw", "未知处理器")
|
cpu_brand_raw = cpuinfo.get_cpu_info().get("brand_raw", "未知处理器")
|
||||||
if "AMD" in cpu_brand_raw:
|
if "amd" in cpu_brand_raw.lower():
|
||||||
brand = "AMD"
|
brand = "AMD"
|
||||||
elif "Intel" in cpu_brand_raw:
|
elif "intel" in cpu_brand_raw:
|
||||||
brand = "英特尔"
|
brand = "英特尔"
|
||||||
elif "Nvidia" in cpu_brand_raw:
|
elif "apple" in cpu_brand_raw.lower():
|
||||||
|
brand = "苹果"
|
||||||
|
elif "qualcomm" in cpu_brand_raw.lower():
|
||||||
|
brand = "高通"
|
||||||
|
elif "mediatek" in cpu_brand_raw.lower():
|
||||||
|
brand = "联发科"
|
||||||
|
elif "samsung" in cpu_brand_raw.lower():
|
||||||
|
brand = "三星"
|
||||||
|
elif "nvidia" in cpu_brand_raw.lower():
|
||||||
brand = "英伟达"
|
brand = "英伟达"
|
||||||
else:
|
else:
|
||||||
brand = "未知处理器"
|
brand = "未知处理器"
|
||||||
@ -262,7 +271,9 @@ async def get_hardware_data() -> dict:
|
|||||||
for disk in psutil.disk_partitions(all=True):
|
for disk in psutil.disk_partitions(all=True):
|
||||||
try:
|
try:
|
||||||
disk_usage = psutil.disk_usage(disk.mountpoint)
|
disk_usage = psutil.disk_usage(disk.mountpoint)
|
||||||
if disk_usage.total == 0:
|
if disk_usage.total == 0 or disk.mountpoint.startswith(
|
||||||
|
("/var", "/boot", "/run", "/proc", "/sys", "/dev", "/tmp", "/snap")
|
||||||
|
):
|
||||||
continue # 虚拟磁盘
|
continue # 虚拟磁盘
|
||||||
result["disk"].append(
|
result["disk"].append(
|
||||||
{
|
{
|
||||||
@ -283,7 +294,7 @@ async def get_liteyuki_data() -> dict:
|
|||||||
temp_data: TempConfig = common_db.where_one(TempConfig(), default=TempConfig())
|
temp_data: TempConfig = common_db.where_one(TempConfig(), default=TempConfig())
|
||||||
result = {
|
result = {
|
||||||
"name": list(get_config("nickname", [__NAME__]))[0],
|
"name": list(get_config("nickname", [__NAME__]))[0],
|
||||||
"version": f"{__VERSION__}{'-' + commit_hash[:7] if (commit_hash and len(commit_hash) > 8) else ''}",
|
"version": f"{__version__}{'-' + commit_hash[:7] if (commit_hash and len(commit_hash) > 8) else ''}",
|
||||||
"plugins": len(nonebot.get_loaded_plugins()),
|
"plugins": len(nonebot.get_loaded_plugins()),
|
||||||
"resources": len(get_loaded_resource_packs()),
|
"resources": len(get_loaded_resource_packs()),
|
||||||
"nonebot": f"{nonebot.__version__}",
|
"nonebot": f"{nonebot.__version__}",
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import httpx
|
|
||||||
import nonebot
|
import nonebot
|
||||||
from nonebot import require
|
from nonebot import require
|
||||||
from nonebot.exception import IgnoredException
|
from nonebot.exception import IgnoredException
|
||||||
|
@ -5,15 +5,24 @@ from nonebot import require
|
|||||||
|
|
||||||
from src.utils.base.data import LiteModel, Database
|
from src.utils.base.data import LiteModel, Database
|
||||||
from src.utils.base.data_manager import User, user_db, group_db
|
from src.utils.base.data_manager import User, user_db, group_db
|
||||||
from src.utils.base.language import Language, change_user_lang, get_all_lang, get_user_lang
|
from src.utils.base.language import (
|
||||||
|
Language,
|
||||||
|
change_user_lang,
|
||||||
|
get_all_lang,
|
||||||
|
get_user_lang,
|
||||||
|
)
|
||||||
from src.utils.base.ly_typing import T_Bot, T_MessageEvent
|
from src.utils.base.ly_typing import T_Bot, T_MessageEvent
|
||||||
from src.utils.message.message import MarkdownMessage as md
|
from src.utils.message.message import MarkdownMessage as md
|
||||||
|
|
||||||
|
# from src.utils.message.html_tool import md_to_pic
|
||||||
from .const import representative_timezones_list
|
from .const import representative_timezones_list
|
||||||
from src.utils import event as event_utils
|
from src.utils import event as event_utils
|
||||||
|
|
||||||
|
|
||||||
require("nonebot_plugin_alconna")
|
require("nonebot_plugin_alconna")
|
||||||
from nonebot_plugin_alconna import Alconna, Args, Arparma, Subcommand, on_alconna
|
from nonebot_plugin_alconna import Alconna, Args, Arparma, Subcommand, on_alconna
|
||||||
|
|
||||||
|
|
||||||
profile_alc = on_alconna(
|
profile_alc = on_alconna(
|
||||||
Alconna(
|
Alconna(
|
||||||
"profile",
|
"profile",
|
||||||
@ -28,7 +37,7 @@ profile_alc = on_alconna(
|
|||||||
alias=["g", "查询"],
|
alias=["g", "查询"],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
aliases={"用户信息"}
|
aliases={"用户信息"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -42,13 +51,21 @@ class Profile(LiteModel):
|
|||||||
|
|
||||||
@profile_alc.handle()
|
@profile_alc.handle()
|
||||||
async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
|
async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
|
||||||
user: User = user_db.where_one(User(), "user_id = ?", event_utils.get_user_id(event),
|
user: User = user_db.where_one(
|
||||||
default=User(user_id=str(event_utils.get_user_id(event))))
|
User(),
|
||||||
|
"user_id = ?",
|
||||||
|
event_utils.get_user_id(event),
|
||||||
|
default=User(user_id=str(event_utils.get_user_id(event))),
|
||||||
|
)
|
||||||
ulang = get_user_lang(str(event_utils.get_user_id(event)))
|
ulang = get_user_lang(str(event_utils.get_user_id(event)))
|
||||||
if result.subcommands.get("set"):
|
if result.subcommands.get("set"):
|
||||||
if result.subcommands["set"].args.get("value"):
|
if result.subcommands["set"].args.get("value"):
|
||||||
# 对合法性进行校验后设置
|
# 对合法性进行校验后设置
|
||||||
r = set_profile(result.args["key"], result.args["value"], str(event_utils.get_user_id(event)))
|
r = set_profile(
|
||||||
|
result.args["key"],
|
||||||
|
result.args["value"],
|
||||||
|
str(event_utils.get_user_id(event)),
|
||||||
|
)
|
||||||
if r:
|
if r:
|
||||||
user.profile[result.args["key"]] = result.args["value"]
|
user.profile[result.args["key"]] = result.args["value"]
|
||||||
user_db.save(user) # 数据库保存
|
user_db.save(user) # 数据库保存
|
||||||
@ -56,18 +73,28 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
|
|||||||
ulang.get(
|
ulang.get(
|
||||||
"user.profile.set_success",
|
"user.profile.set_success",
|
||||||
ATTR=ulang.get(f"user.profile.{result.args['key']}"),
|
ATTR=ulang.get(f"user.profile.{result.args['key']}"),
|
||||||
VALUE=result.args["value"]
|
VALUE=result.args["value"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await profile_alc.finish(ulang.get("user.profile.set_failed", ATTR=ulang.get(f"user.profile.{result.args['key']}")))
|
await profile_alc.finish(
|
||||||
|
ulang.get(
|
||||||
|
"user.profile.set_failed",
|
||||||
|
ATTR=ulang.get(f"user.profile.{result.args['key']}"),
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# 未输入值,尝试呼出菜单
|
# 未输入值,尝试呼出菜单
|
||||||
menu = get_profile_menu(result.args["key"], ulang)
|
menu = get_profile_menu(result.args["key"], ulang)
|
||||||
if menu:
|
if menu:
|
||||||
await md.send_md(menu, bot, event=event)
|
await md.send_md(menu, bot, event=event)
|
||||||
else:
|
else:
|
||||||
await profile_alc.finish(ulang.get("user.profile.input_value", ATTR=ulang.get(f"user.profile.{result.args['key']}")))
|
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"]
|
user.profile[result.args["key"]] = result.args["value"]
|
||||||
|
|
||||||
@ -92,11 +119,16 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
|
|||||||
continue
|
continue
|
||||||
val = profile.dict()[key]
|
val = profile.dict()[key]
|
||||||
key_text = ulang.get(f"user.profile.{key}")
|
key_text = ulang.get(f"user.profile.{key}")
|
||||||
btn_set = md.btn_cmd(ulang.get("user.profile.edit"), f"profile set {key}",
|
btn_set = md.btn_cmd(
|
||||||
enter=True if key in enter_attr else False)
|
ulang.get("user.profile.edit"),
|
||||||
reply += (f"\n**{key_text}** **{val}**\n"
|
f"profile set {key}",
|
||||||
f"\n> {ulang.get(f'user.profile.{key}.desc')}"
|
enter=True if key in enter_attr else False,
|
||||||
f"\n> {btn_set} \n\n***\n")
|
)
|
||||||
|
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)
|
await md.send_md(reply, bot, event=event)
|
||||||
|
|
||||||
|
|
||||||
@ -119,7 +151,9 @@ def get_profile_menu(key: str, ulang: Language) -> Optional[str]:
|
|||||||
reply = f"**{setting_name} {ulang.get('user.profile.settings')}**\n***\n"
|
reply = f"**{setting_name} {ulang.get('user.profile.settings')}**\n***\n"
|
||||||
if key == "lang":
|
if key == "lang":
|
||||||
for lang_code, lang_name in get_all_lang().items():
|
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}")
|
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"
|
reply += f"\n{btn_set_lang}\n***\n"
|
||||||
elif key == "timezone":
|
elif key == "timezone":
|
||||||
for tz in representative_timezones_list:
|
for tz in representative_timezones_list:
|
||||||
|
55
src/nonebot_plugins/to_liteyuki.py
Normal file
55
src/nonebot_plugins/to_liteyuki.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||||
|
|
||||||
|
@Time : 2024/8/20 上午5:10
|
||||||
|
@Author : snowykami
|
||||||
|
@Email : snowykami@outlook.com
|
||||||
|
@File : to_liteyuki.py
|
||||||
|
@Software: PyCharm
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from nonebot import Bot, get_bot, on_message, get_driver
|
||||||
|
from nonebot.plugin import PluginMetadata
|
||||||
|
from nonebot.adapters.onebot.v11 import MessageEvent, Bot
|
||||||
|
|
||||||
|
from liteyuki import Channel
|
||||||
|
from liteyuki.comm import get_channel
|
||||||
|
from liteyuki.comm.storage import shared_memory
|
||||||
|
from liteyuki.message.event import MessageEvent as LiteyukiMessageEvent
|
||||||
|
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
name="轻雪push",
|
||||||
|
description="把消息事件传递给轻雪框架进行处理",
|
||||||
|
usage="用户无需使用",
|
||||||
|
)
|
||||||
|
|
||||||
|
recv_channel = Channel[LiteyukiMessageEvent](name="event_to_nonebot")
|
||||||
|
|
||||||
|
|
||||||
|
# @on_message().handle()
|
||||||
|
# async def _(bot: Bot, event: MessageEvent):
|
||||||
|
# liteyuki_event = LiteyukiMessageEvent(
|
||||||
|
# message_type=event.message_type,
|
||||||
|
# message=event.dict()["message"],
|
||||||
|
# raw_message=event.raw_message,
|
||||||
|
# data=event.dict(),
|
||||||
|
# bot_id=bot.self_id,
|
||||||
|
# user_id=str(event.user_id),
|
||||||
|
# session_id=str(event.user_id if event.message_type == "private" else event.group_id),
|
||||||
|
# session_type=event.message_type,
|
||||||
|
# receive_channel=recv_channel,
|
||||||
|
# )
|
||||||
|
# shared_memory.publish("event_to_liteyuki", liteyuki_event)
|
||||||
|
|
||||||
|
|
||||||
|
# @get_driver().on_bot_connect
|
||||||
|
# async def _():
|
||||||
|
# while True:
|
||||||
|
# event = await recv_channel.async_receive()
|
||||||
|
# bot: Bot = get_bot(event.bot_id) # type: ignore
|
||||||
|
# if event.message_type == "private":
|
||||||
|
# await bot.send_private_msg(user_id=int(event.session_id), message=event.data["message"])
|
||||||
|
# elif event.message_type == "group":
|
||||||
|
# await bot.send_group_msg(group_id=int(event.session_id), message=event.data["message"])
|
@ -1,3 +0,0 @@
|
|||||||
name: Sign Status
|
|
||||||
description: for Lagrange
|
|
||||||
version: 2024.4.26
|
|
@ -1,4 +0,0 @@
|
|||||||
.sign-chart {
|
|
||||||
height: 400px;
|
|
||||||
background-color: rgba(255, 255, 255, 0.7);
|
|
||||||
}
|
|
@ -1,75 +0,0 @@
|
|||||||
// 数据类型声明
|
|
||||||
// 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]) / (60*(item["times"][index] - item["times"][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
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
<!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>
|
|
@ -1,10 +1,10 @@
|
|||||||
language.name=文言
|
language.name=文言
|
||||||
|
|
||||||
log.debug=试言
|
log.debug=试
|
||||||
log.info=讯文
|
log.info=讯
|
||||||
log.warning=警示
|
log.warning=警
|
||||||
log.error=查误
|
log.error=误
|
||||||
log.success=名成
|
log.success=成
|
||||||
|
|
||||||
liteyuki.restart=复启
|
liteyuki.restart=复启
|
||||||
liteyuki.restart_now=即复启
|
liteyuki.restart_now=即复启
|
||||||
|
@ -113,6 +113,7 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
.disk-title {
|
.disk-title {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
color: var(--main-text-color);
|
color: var(--main-text-color);
|
||||||
@ -126,6 +127,31 @@
|
|||||||
max-width: calc(100% - 40px);
|
max-width: calc(100% - 40px);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
.disk-name {
|
||||||
|
position: absolute;
|
||||||
|
color: var(--main-text-color);
|
||||||
|
font-size: 26px;
|
||||||
|
margin-left: 20px;
|
||||||
|
text-align: left;
|
||||||
|
white-space: normal; /* 允许换行 */
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
word-break: break-all; /* 允许在单词内换行 */
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disk-details {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
/* 调整位置以确保有足够的空间显示 */
|
||||||
|
color: var(--main-text-color);
|
||||||
|
font-size: 24px;
|
||||||
|
text-align: right;
|
||||||
|
white-space: nowrap;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
#motto-text {
|
#motto-text {
|
||||||
font-size: 42px;
|
font-size: 42px;
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
const data = JSON.parse(document.getElementById('data').innerText);
|
const data = JSON.parse(document.getElementById("data").innerText);
|
||||||
const bot_data = data['bot']; // 机器人数据
|
const bot_data = data["bot"]; // 机器人数据
|
||||||
const hardwareData = data['hardware']; // 硬件数据
|
const hardwareData = data["hardware"]; // 硬件数据
|
||||||
const liteyukiData = data['liteyuki']; // LiteYuki数据
|
const liteyukiData = data["liteyuki"]; // LiteYuki数据
|
||||||
const localData = data['localization']; // 本地化语言数据
|
const localData = data["localization"]; // 本地化语言数据
|
||||||
const motto_ = data['motto']; // 言论数据
|
const motto_ = data["motto"]; // 言论数据
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建CPU/内存/交换饼图
|
* 创建CPU/内存/交换饼图
|
||||||
@ -16,52 +16,54 @@ function createPieChartOption(title, data) {
|
|||||||
animation: false,
|
animation: false,
|
||||||
title: {
|
title: {
|
||||||
text: title,
|
text: title,
|
||||||
left: 'center',
|
left: "center",
|
||||||
top: 'center',
|
top: "center",
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: '#000',
|
color: "#000",
|
||||||
fontSize: 30,
|
fontSize: 30,
|
||||||
lineHeight: 36
|
lineHeight: 36,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
show: true,
|
show: true,
|
||||||
trigger: 'item',
|
trigger: "item",
|
||||||
backgroundColor: '#000',
|
backgroundColor: "#000",
|
||||||
},
|
},
|
||||||
color: data.length === 3 ? ['#053349', '#007ebd', "#00000044"] : ['#007ebd', '#00000044'],
|
color:
|
||||||
|
data.length === 3
|
||||||
|
? ["#053349", "#007ebd", "#00000044"]
|
||||||
|
: ["#007ebd", "#00000044"],
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
name: 'info',
|
name: "info",
|
||||||
type: 'pie',
|
type: "pie",
|
||||||
radius: ['80%', '100%'],
|
radius: ["80%", "100%"],
|
||||||
center: ['50%', '50%'],
|
center: ["50%", "50%"],
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
normal: {
|
normal: {
|
||||||
label: {
|
label: {
|
||||||
show: false
|
show: false,
|
||||||
},
|
},
|
||||||
labelLine: {
|
labelLine: {
|
||||||
show: false
|
show: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
emphasis: {
|
emphasis: {
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
textStyle: {
|
textStyle: {
|
||||||
fontSize: '50',
|
fontSize: "50",
|
||||||
fontWeight: 'bold'
|
fontWeight: "bold",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
data: data
|
data: data,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function convertSize(size, precision = 2, addUnit = true, suffix = " X字节") {
|
function convertSize(size, precision = 2, addUnit = true, suffix = " X字节") {
|
||||||
let isNegative = size < 0;
|
let isNegative = size < 0;
|
||||||
size = Math.abs(size);
|
size = Math.abs(size);
|
||||||
@ -81,7 +83,7 @@ function convertSize(size, precision = 2, addUnit = true, suffix = " X字节") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (addUnit) {
|
if (addUnit) {
|
||||||
return size.toFixed(precision) + suffix.replace('X', unit);
|
return size.toFixed(precision) + suffix.replace("X", unit);
|
||||||
} else {
|
} else {
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
@ -92,206 +94,260 @@ function convertSize(size, precision = 2, addUnit = true, suffix = " X字节") {
|
|||||||
* @param title
|
* @param title
|
||||||
* @param percent 数据
|
* @param percent 数据
|
||||||
*/
|
*/
|
||||||
function createBarChart(title, percent) {
|
function createBarChart(title, percent, name) {
|
||||||
// percent为百分比,最大值为100
|
// percent为百分比,最大值为100
|
||||||
let diskDiv = document.createElement('div')
|
let diskDiv = document.createElement("div");
|
||||||
diskDiv.setAttribute('class', 'disk-info')
|
diskDiv.setAttribute("class", "disk-info");
|
||||||
diskDiv.style.marginBottom = '20px'
|
diskDiv.style.marginBottom = "20px";
|
||||||
diskDiv.innerHTML = `
|
diskDiv.innerHTML = `
|
||||||
<div class="disk-title">${title}</div>
|
<div class="disk-title">${title}</div>
|
||||||
<div class="disk-usage" style="width: ${percent}%"></div>
|
<div class="disk-usage" style="width: ${percent}%"></div>
|
||||||
`
|
`;
|
||||||
|
updateDiskNameWidth(diskDiv);
|
||||||
|
|
||||||
return diskDiv
|
return diskDiv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新 .disk-name 宽度
|
||||||
|
function updateDiskNameWidth(diskInfoElement) {
|
||||||
|
let diskDetails = diskInfoElement.querySelector(".disk-details");
|
||||||
|
let diskName = diskInfoElement.querySelector(".disk-name");
|
||||||
|
let detailsWidth = diskDetails.offsetWidth;
|
||||||
|
let parentWidth = diskInfoElement.offsetWidth;
|
||||||
|
|
||||||
|
let nameMaxWidth = parentWidth - detailsWidth - 20 - 40;
|
||||||
|
diskName.style.maxWidth = `${nameMaxWidth}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function secondsToTextTime(seconds) {
|
function secondsToTextTime(seconds) {
|
||||||
let days = Math.floor(seconds / 86400)
|
let days = Math.floor(seconds / 86400);
|
||||||
let hours = Math.floor((seconds % 86400) / 3600)
|
let hours = Math.floor((seconds % 86400) / 3600);
|
||||||
let minutes = Math.floor((seconds % 3600) / 60)
|
let minutes = Math.floor((seconds % 3600) / 60);
|
||||||
let seconds_ = Math.floor(seconds % 60)
|
let seconds_ = Math.floor(seconds % 60);
|
||||||
return `${days}${localData['days']} ${hours}${localData['hours']} ${minutes}${localData['minutes']} ${seconds_}${localData['seconds']}`
|
return `${days}${localData["days"]} ${hours}${localData["hours"]} ${minutes}${localData["minutes"]} ${seconds_}${localData["seconds"]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主函数
|
// 主函数
|
||||||
function main() {
|
function main() {
|
||||||
// 添加机器人信息
|
// 添加机器人信息
|
||||||
bot_data['bots'].forEach(
|
bot_data["bots"].forEach((bot) => {
|
||||||
(bot) => {
|
let botInfoDiv = document.importNode(
|
||||||
let botInfoDiv = document.importNode(document.getElementById('bot-template').content, true) // 复制模板
|
document.getElementById("bot-template").content,
|
||||||
|
true
|
||||||
|
); // 复制模板
|
||||||
|
|
||||||
|
// 设置机器人信息
|
||||||
|
botInfoDiv.className = "info-box bot-info";
|
||||||
|
|
||||||
// 设置机器人信息
|
botInfoDiv.querySelector(".bot-icon-img").setAttribute("src", bot["icon"]);
|
||||||
botInfoDiv.className = 'info-box bot-info'
|
botInfoDiv.querySelector(".bot-name").innerText = bot["name"];
|
||||||
|
let tagArray = [
|
||||||
botInfoDiv.querySelector('.bot-icon-img').setAttribute('src', bot['icon'])
|
bot["protocol_name"],
|
||||||
botInfoDiv.querySelector('.bot-name').innerText = bot['name']
|
`${bot["app_name"]}`,
|
||||||
let tagArray = [
|
`${localData["groups"]}${bot["groups"]}`,
|
||||||
bot['protocol_name'],
|
`${localData["friends"]}${bot["friends"]}`,
|
||||||
`${bot['app_name']}`,
|
`${localData["message_sent"]}${bot["message_sent"]}`,
|
||||||
`${localData['groups']}${bot['groups']}`,
|
`${localData["message_received"]}${bot["message_received"]}`,
|
||||||
`${localData['friends']}${bot['friends']}`,
|
];
|
||||||
`${localData['message_sent']}${bot['message_sent']}`,
|
// 添加一些标签
|
||||||
`${localData['message_received']}${bot['message_received']}`,
|
tagArray.forEach((tag, index) => {
|
||||||
]
|
let tagSpan = document.createElement("span");
|
||||||
// 添加一些标签
|
tagSpan.className = "bot-tag";
|
||||||
tagArray.forEach(
|
tagSpan.innerText = tag;
|
||||||
(tag, index) => {
|
// 给最后一个标签不添加后缀
|
||||||
let tagSpan = document.createElement('span')
|
tagSpan.setAttribute("suffix", index === 0 || tag[0] == "\n" ? "0" : "1");
|
||||||
tagSpan.className = 'bot-tag'
|
botInfoDiv.querySelector(".bot-tags").appendChild(tagSpan);
|
||||||
tagSpan.innerText = tag
|
});
|
||||||
// 给最后一个标签不添加后缀
|
document.body.insertBefore(
|
||||||
tagSpan.setAttribute('suffix', (index === 0) || (tag[0] == '\n') ? '0' : '1')
|
botInfoDiv,
|
||||||
botInfoDiv.querySelector('.bot-tags').appendChild(tagSpan)
|
document.getElementById("hardware-info")
|
||||||
}
|
); // 插入对象
|
||||||
)
|
});
|
||||||
document.body.insertBefore(botInfoDiv, document.getElementById('hardware-info')) // 插入对象
|
|
||||||
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 添加轻雪信息
|
// 添加轻雪信息
|
||||||
let liteyukiInfoDiv = document.importNode(document.getElementById('bot-template').content, true) // 复制模板
|
let liteyukiInfoDiv = document.importNode(
|
||||||
liteyukiInfoDiv.className = 'info-box bot-info'
|
document.getElementById("bot-template").content,
|
||||||
liteyukiInfoDiv.querySelector('.bot-icon-img').setAttribute('src', './img/litetrimo.png')
|
true
|
||||||
liteyukiInfoDiv.querySelector('.bot-name').innerText = `${liteyukiData['name']} - 睿乐`
|
); // 复制模板
|
||||||
|
liteyukiInfoDiv.className = "info-box bot-info";
|
||||||
|
liteyukiInfoDiv
|
||||||
|
.querySelector(".bot-icon-img")
|
||||||
|
.setAttribute("src", "./img/litetrimo.png");
|
||||||
|
liteyukiInfoDiv.querySelector(
|
||||||
|
".bot-name"
|
||||||
|
).innerText = `${liteyukiData["name"]} - 睿乐`;
|
||||||
|
|
||||||
let tagArray = [
|
let tagArray = [
|
||||||
`灵温 ${liteyukiData['version']}`,
|
`灵温 ${liteyukiData["version"]}`,
|
||||||
`Nonebot ${liteyukiData['nonebot']}`,
|
`Nonebot ${liteyukiData["nonebot"]}`,
|
||||||
`${liteyukiData['python']}`,
|
`${liteyukiData["python"]}`,
|
||||||
liteyukiData['system'],
|
liteyukiData["system"],
|
||||||
`${localData['plugins']}${liteyukiData['plugins']}`,
|
`${localData["plugins"]}${liteyukiData["plugins"]}`,
|
||||||
`${localData['resources']}${liteyukiData['resources']}`,
|
`${localData["resources"]}${liteyukiData["resources"]}`,
|
||||||
`${localData['bots']}${liteyukiData['bots']}`,
|
`${localData["bots"]}${liteyukiData["bots"]}`,
|
||||||
`${localData['runtime']} ${secondsToTextTime(liteyukiData['runtime'])}`,
|
`${localData["runtime"]} ${secondsToTextTime(liteyukiData["runtime"])}`,
|
||||||
]
|
];
|
||||||
tagArray.forEach(
|
tagArray.forEach((tag, index) => {
|
||||||
(tag, index) => {
|
let tagSpan = document.createElement("span");
|
||||||
let tagSpan = document.createElement('span')
|
tagSpan.className = "bot-tag";
|
||||||
tagSpan.className = 'bot-tag'
|
tagSpan.innerText = tag;
|
||||||
tagSpan.innerText = tag
|
// 给最后一个标签不添加后缀
|
||||||
// 给最后一个标签不添加后缀
|
tagSpan.setAttribute("suffix", index === 0 || tag[0] == "\n" ? "0" : "1");
|
||||||
tagSpan.setAttribute('suffix', (index === 0) || (tag[0] == '\n') ? '0' : '1')
|
liteyukiInfoDiv.querySelector(".bot-tags").appendChild(tagSpan);
|
||||||
liteyukiInfoDiv.querySelector('.bot-tags').appendChild(tagSpan)
|
});
|
||||||
}
|
document.body.insertBefore(
|
||||||
)
|
liteyukiInfoDiv,
|
||||||
document.body.insertBefore(liteyukiInfoDiv, document.getElementById('hardware-info')) // 插入对象
|
document.getElementById("hardware-info")
|
||||||
|
); // 插入对象
|
||||||
|
|
||||||
// 添加硬件信息
|
// 添加硬件信息
|
||||||
const cpuData = hardwareData['cpu']
|
const cpuData = hardwareData["cpu"];
|
||||||
const memData = hardwareData['memory']
|
const memData = hardwareData["memory"];
|
||||||
const swapData = hardwareData['swap']
|
const swapData = hardwareData["swap"];
|
||||||
|
|
||||||
const cpuTagArray = [
|
const cpuTagArray = [
|
||||||
cpuData['name'],
|
cpuData["name"],
|
||||||
`${cpuData['cores']}${localData['cores']} ${cpuData['threads']}${localData['threads']}`,
|
`${cpuData["cores"]}${localData["cores"]} ${cpuData["threads"]}${localData["threads"]}`,
|
||||||
`${(cpuData['freq'] / 1000).toFixed(2)}吉赫兹`
|
`${(cpuData["freq"] / 1000).toFixed(2)}吉赫兹`,
|
||||||
]
|
];
|
||||||
|
|
||||||
const memTagArray = [
|
const memTagArray = [
|
||||||
`${localData['process']} ${convertSize(memData['usedProcess'])}`,
|
`${localData["process"]} ${convertSize(memData["usedProcess"])}`,
|
||||||
`${localData['used']} ${convertSize(memData['used'])}`,
|
`${localData["used"]} ${convertSize(memData["used"])}`,
|
||||||
`${localData['free']} ${convertSize(memData['free'])}`,
|
`${localData["free"]} ${convertSize(memData["free"])}`,
|
||||||
`${localData['total']} ${convertSize(memData['total'])}`
|
`${localData["total"]} ${convertSize(memData["total"])}`,
|
||||||
]
|
];
|
||||||
|
|
||||||
const swapTagArray = [
|
const swapTagArray = [
|
||||||
`${localData['used']} ${convertSize(swapData['used'])}`,
|
`${localData["used"]} ${convertSize(swapData["used"])}`,
|
||||||
`${localData['free']} ${convertSize(swapData['free'])}`,
|
`${localData["free"]} ${convertSize(swapData["free"])}`,
|
||||||
`${localData['total']} ${convertSize(swapData['total'])}`
|
`${localData["total"]} ${convertSize(swapData["total"])}`,
|
||||||
]
|
];
|
||||||
let cpuDeviceInfoDiv = document.importNode(document.getElementById('device-info').content, true)
|
let cpuDeviceInfoDiv = document.importNode(
|
||||||
let memDeviceInfoDiv = document.importNode(document.getElementById('device-info').content, true)
|
document.getElementById("device-info").content,
|
||||||
let swapDeviceInfoDiv = document.importNode(document.getElementById('device-info').content, true)
|
true
|
||||||
|
);
|
||||||
|
let memDeviceInfoDiv = document.importNode(
|
||||||
|
document.getElementById("device-info").content,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
let swapDeviceInfoDiv = document.importNode(
|
||||||
|
document.getElementById("device-info").content,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
cpuDeviceInfoDiv.querySelector('.device-info').setAttribute('id', 'cpu-info')
|
cpuDeviceInfoDiv.querySelector(".device-info").setAttribute("id", "cpu-info");
|
||||||
memDeviceInfoDiv.querySelector('.device-info').setAttribute('id', 'mem-info')
|
memDeviceInfoDiv.querySelector(".device-info").setAttribute("id", "mem-info");
|
||||||
swapDeviceInfoDiv.querySelector('.device-info').setAttribute('id', 'swap-info')
|
swapDeviceInfoDiv
|
||||||
cpuDeviceInfoDiv.querySelector('.device-chart').setAttribute('id', 'cpu-chart')
|
.querySelector(".device-info")
|
||||||
memDeviceInfoDiv.querySelector('.device-chart').setAttribute('id', 'mem-chart')
|
.setAttribute("id", "swap-info");
|
||||||
swapDeviceInfoDiv.querySelector('.device-chart').setAttribute('id', 'swap-chart')
|
cpuDeviceInfoDiv
|
||||||
|
.querySelector(".device-chart")
|
||||||
|
.setAttribute("id", "cpu-chart");
|
||||||
|
memDeviceInfoDiv
|
||||||
|
.querySelector(".device-chart")
|
||||||
|
.setAttribute("id", "mem-chart");
|
||||||
|
swapDeviceInfoDiv
|
||||||
|
.querySelector(".device-chart")
|
||||||
|
.setAttribute("id", "swap-chart");
|
||||||
|
|
||||||
let devices = {
|
let devices = {
|
||||||
'cpu': cpuDeviceInfoDiv,
|
cpu: cpuDeviceInfoDiv,
|
||||||
'mem': memDeviceInfoDiv,
|
mem: memDeviceInfoDiv,
|
||||||
'swap': swapDeviceInfoDiv
|
swap: swapDeviceInfoDiv,
|
||||||
}
|
};
|
||||||
// 遍历添加标签
|
// 遍历添加标签
|
||||||
for (let device in devices) {
|
for (let device in devices) {
|
||||||
let tagArray = []
|
let tagArray = [];
|
||||||
switch (device) {
|
switch (device) {
|
||||||
case 'cpu':
|
case "cpu":
|
||||||
tagArray = cpuTagArray
|
tagArray = cpuTagArray;
|
||||||
break
|
break;
|
||||||
case 'mem':
|
case "mem":
|
||||||
tagArray = memTagArray
|
tagArray = memTagArray;
|
||||||
break
|
break;
|
||||||
case 'swap':
|
case "swap":
|
||||||
tagArray = swapTagArray
|
tagArray = swapTagArray;
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
tagArray.forEach(
|
tagArray.forEach((tag, index) => {
|
||||||
(tag, index) => {
|
let tagDiv = document.createElement("div");
|
||||||
let tagDiv = document.createElement('div')
|
tagDiv.className = "device-tag";
|
||||||
tagDiv.className = 'device-tag'
|
tagDiv.innerText = tag;
|
||||||
tagDiv.innerText = tag
|
// 给最后一个标签不添加后缀
|
||||||
// 给最后一个标签不添加后缀
|
tagDiv.setAttribute("suffix", index === tagArray.length - 1 ? "0" : "1");
|
||||||
tagDiv.setAttribute('suffix', index === tagArray.length - 1 ? '0' : '1')
|
devices[device].querySelector(".device-tags").appendChild(tagDiv);
|
||||||
devices[device].querySelector('.device-tags').appendChild(tagDiv)
|
});
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 插入
|
// 插入
|
||||||
document.getElementById('hardware-info').appendChild(cpuDeviceInfoDiv)
|
document.getElementById("hardware-info").appendChild(cpuDeviceInfoDiv);
|
||||||
document.getElementById('hardware-info').appendChild(memDeviceInfoDiv)
|
document.getElementById("hardware-info").appendChild(memDeviceInfoDiv);
|
||||||
document.getElementById('hardware-info').appendChild(swapDeviceInfoDiv)
|
document.getElementById("hardware-info").appendChild(swapDeviceInfoDiv);
|
||||||
|
|
||||||
let cpuChart = echarts.init(document.getElementById('cpu-chart'))
|
let cpuChart = echarts.init(document.getElementById("cpu-chart"));
|
||||||
let memChart = echarts.init(document.getElementById('mem-chart'))
|
let memChart = echarts.init(document.getElementById("mem-chart"));
|
||||||
let swapChart = echarts.init(document.getElementById('swap-chart'))
|
let swapChart = echarts.init(document.getElementById("swap-chart"));
|
||||||
|
|
||||||
|
cpuChart.setOption(
|
||||||
|
createPieChartOption(
|
||||||
|
`${localData["cpu"]}\n${cpuData["percent"].toFixed(1)}%`,
|
||||||
|
[
|
||||||
|
{ name: "used", value: cpuData["percent"] },
|
||||||
|
{ name: "free", value: 100 - cpuData["percent"] },
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
cpuChart.setOption(createPieChartOption(`${localData['cpu']}\n${cpuData['percent'].toFixed(1)}%`, [
|
memChart.setOption(
|
||||||
{ name: 'used', value: cpuData['percent'] },
|
createPieChartOption(
|
||||||
{ name: 'free', value: 100 - cpuData['percent'] }
|
`${localData["memory"]}\n${memData["percent"].toFixed(1)}%`,
|
||||||
]))
|
[
|
||||||
|
{ name: "process", value: memData["usedProcess"] },
|
||||||
memChart.setOption(createPieChartOption(`${localData['memory']}\n${memData['percent'].toFixed(1)}%`, [
|
{ name: "used", value: memData["used"] - memData["usedProcess"] },
|
||||||
{ name: 'process', value: memData['usedProcess'] },
|
{ name: "free", value: memData["free"] },
|
||||||
{ name: 'used', value: memData['used'] - memData['usedProcess'] },
|
]
|
||||||
{ name: 'free', value: memData['free'] }
|
)
|
||||||
]))
|
);
|
||||||
|
|
||||||
|
|
||||||
swapChart.setOption(createPieChartOption(`${localData['swap']}\n${swapData['percent'].toFixed(1)}%`, [
|
|
||||||
{ name: 'used', value: swapData['used'] },
|
|
||||||
{ name: 'free', value: swapData['free'] }
|
|
||||||
]))
|
|
||||||
|
|
||||||
|
swapChart.setOption(
|
||||||
|
createPieChartOption(
|
||||||
|
`${localData["swap"]}\n${swapData["percent"].toFixed(1)}%`,
|
||||||
|
[
|
||||||
|
{ name: "used", value: swapData["used"] },
|
||||||
|
{ name: "free", value: swapData["free"] },
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// 磁盘信息
|
// 磁盘信息
|
||||||
const diskData = hardwareData['disk']
|
const diskData = hardwareData["disk"];
|
||||||
diskData.forEach(
|
diskData.forEach((disk) => {
|
||||||
(disk) => {
|
let diskTitle = `${localData['free']} ${convertSize(disk['free'])} ${localData['total']} ${convertSize(disk['total'])}`;
|
||||||
let diskTitle = `${disk['name']} ${localData['free']} ${convertSize(disk['free'])} ${localData['total']} ${convertSize(disk['total'])}`
|
let diskDiv = createBarChart(diskTitle, disk['percent'], disk['name']);
|
||||||
// 最后一个把margin-bottom去掉
|
// 最后一个把margin-bottom去掉
|
||||||
let diskDiv = createBarChart(diskTitle, disk['percent'])
|
if (disk === diskData[diskData.length - 1]) {
|
||||||
if (disk === diskData[diskData.length - 1]) {
|
diskDiv.style.marginBottom = "0";
|
||||||
diskDiv.style.marginBottom = '0'
|
}
|
||||||
}
|
document.getElementById('disk-info').appendChild(diskDiv);
|
||||||
document.getElementById('disk-info').appendChild(createBarChart(diskTitle, disk['percent']))
|
});
|
||||||
})
|
|
||||||
// 随机一言
|
// 随机一言
|
||||||
let mottoText = motto_['text']
|
let mottoText = motto_["text"];
|
||||||
let mottoFrom = motto_['source']
|
let mottoFrom = motto_["source"];
|
||||||
document.getElementById('motto-text').innerText = mottoText
|
document.getElementById("motto-text").innerText = mottoText;
|
||||||
document.getElementById('motto-from').innerText = mottoFrom
|
document.getElementById("motto-from").innerText = mottoFrom;
|
||||||
// 致谢
|
// 致谢
|
||||||
document.getElementById('addition-info').innerText = '感谢 锅炉 云裳工作室 提供服务器支持'
|
document.getElementById("addition-info").innerText =
|
||||||
|
"感谢 锅炉 云裳工作室 提供服务器支持";
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main();
|
||||||
|
/*
|
||||||
|
// 窗口大小改变监听器 -- Debug
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
const diskInfos = document.querySelectorAll('.disk-info');
|
||||||
|
diskInfos.forEach(diskInfo => {
|
||||||
|
updateDiskNameWidth(diskInfo);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
*/
|
@ -837,22 +837,6 @@
|
|||||||
"time": "2023-06-20T16:04:40.706727Z",
|
"time": "2023-06-20T16:04:40.706727Z",
|
||||||
"skip_test": false
|
"skip_test": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"module_name": "nonebot_plugin_htmlrender",
|
|
||||||
"project_link": "nonebot-plugin-htmlrender",
|
|
||||||
"name": "nonebot-plugin-htmlrender",
|
|
||||||
"desc": "通过浏览器渲染图片",
|
|
||||||
"author": "kexue-z",
|
|
||||||
"homepage": "https://github.com/kexue-z/nonebot-plugin-htmlrender",
|
|
||||||
"tags": [],
|
|
||||||
"is_official": false,
|
|
||||||
"type": "library",
|
|
||||||
"supported_adapters": null,
|
|
||||||
"valid": true,
|
|
||||||
"version": "0.3.1",
|
|
||||||
"time": "2024-03-14T08:47:15.010445Z",
|
|
||||||
"skip_test": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"module_name": "nonebot_plugin_admin",
|
"module_name": "nonebot_plugin_admin",
|
||||||
"project_link": "nonebot-plugin-admin",
|
"project_link": "nonebot-plugin-admin",
|
||||||
@ -8718,31 +8702,6 @@
|
|||||||
"time": "2023-07-14T10:32:08.006009Z",
|
"time": "2023-07-14T10:32:08.006009Z",
|
||||||
"skip_test": false
|
"skip_test": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"module_name": "nonebot_plugin_templates",
|
|
||||||
"project_link": "nonebot-plugin-templates",
|
|
||||||
"name": "templates_render",
|
|
||||||
"desc": "使用htmlrender和jinja2渲染,使用构建的menu,card或dict进行模板渲染",
|
|
||||||
"author": "canxin121",
|
|
||||||
"homepage": "https://github.com/canxin121/nonebot_plugin_templates",
|
|
||||||
"tags": [
|
|
||||||
{
|
|
||||||
"label": "模板渲染",
|
|
||||||
"color": "#eacd52"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "图片生成",
|
|
||||||
"color": "#adea52"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"is_official": false,
|
|
||||||
"type": "library",
|
|
||||||
"supported_adapters": null,
|
|
||||||
"valid": true,
|
|
||||||
"version": "0.1.6",
|
|
||||||
"time": "2023-08-24T09:56:33.184091Z",
|
|
||||||
"skip_test": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"module_name": "nonebot_plugin_pokesomeone",
|
"module_name": "nonebot_plugin_pokesomeone",
|
||||||
"project_link": "nonebot-plugin-pokesomeone",
|
"project_link": "nonebot-plugin-pokesomeone",
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
import json
|
|
||||||
import os.path
|
|
||||||
import platform
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
|
||||||
|
|
||||||
import nonebot
|
import nonebot
|
||||||
|
|
||||||
__NAME__ = "尹灵温|轻雪-睿乐"
|
__NAME__ = "尹灵温|轻雪-睿乐"
|
||||||
__VERSION__ = "6.3.4" # 60201
|
__VERSION__ = "6.3.9" # 60201
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
@ -15,36 +11,10 @@ from src.utils.base.config import load_from_yaml, config
|
|||||||
from src.utils.base.log import init_log
|
from src.utils.base.log import init_log
|
||||||
from git import Repo
|
from git import Repo
|
||||||
|
|
||||||
|
|
||||||
major, minor, patch = map(int, __VERSION__.split("."))
|
major, minor, patch = map(int, __VERSION__.split("."))
|
||||||
__VERSION_I__ = 99000000 + major * 10000 + minor * 100 + patch
|
__VERSION_I__ = 99000000 + major * 10000 + minor * 100 + patch
|
||||||
|
|
||||||
|
|
||||||
def register_bot():
|
|
||||||
url = "https://api.liteyuki.icu/register"
|
|
||||||
data = {
|
|
||||||
"name": __NAME__,
|
|
||||||
"version": __VERSION__,
|
|
||||||
"version_i": __VERSION_I__,
|
|
||||||
"python": f"{platform.python_implementation()} {platform.python_version()}",
|
|
||||||
"os": f"{platform.system()} {platform.version()} {platform.machine()}",
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
nonebot.logger.info("正在等待 Liteyuki 注册服务器…")
|
|
||||||
resp = requests.post(url, json=data, timeout=(10, 15))
|
|
||||||
if resp.status_code == 200:
|
|
||||||
data = resp.json()
|
|
||||||
if liteyuki_id := data.get("liteyuki_id"):
|
|
||||||
with open("data/liteyuki/liteyuki.json", "wb") as f:
|
|
||||||
f.write(json.dumps(data).encode("utf-8"))
|
|
||||||
nonebot.logger.success(f"成功将 {liteyuki_id} 注册到 Liteyuki 服务器")
|
|
||||||
else:
|
|
||||||
raise ValueError(f"无法向 Liteyuki 服务器注册:{data}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
nonebot.logger.warning(f"向 Liteyuki 服务器注册失败,但无所谓:{e}")
|
|
||||||
|
|
||||||
|
|
||||||
def init():
|
def init():
|
||||||
"""
|
"""
|
||||||
初始化
|
初始化
|
||||||
@ -64,25 +34,15 @@ def init():
|
|||||||
repo = Repo(".")
|
repo = Repo(".")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
nonebot.logger.error(
|
nonebot.logger.error(
|
||||||
f"无法读取 Git 仓库 {e},你是否是从仓库内下载的Zip文件?请使用git clone。"
|
f"无法读取 Git 仓库 `{e}`,你是否是从仓库直接下载的Zip文件?请使用git clone。"
|
||||||
)
|
)
|
||||||
|
|
||||||
# temp_data: TempConfig = common_db.where_one(TempConfig(), default=TempConfig())
|
# temp_data: TempConfig = common_db.where_one(TempConfig(), default=TempConfig())
|
||||||
# temp_data.data["start_time"] = time.time()
|
# temp_data.data["start_time"] = time.time()
|
||||||
# common_db.save(temp_data)
|
# common_db.save(temp_data)
|
||||||
|
|
||||||
# 在加载完成语言后再初始化日志
|
|
||||||
nonebot.logger.info("尹灵温 正在初始化…")
|
|
||||||
|
|
||||||
if not os.path.exists("data/liteyuki/liteyuki.json"):
|
|
||||||
register_bot()
|
|
||||||
|
|
||||||
if not os.path.exists("pyproject.toml"):
|
|
||||||
with open("pyproject.toml", "w", encoding="utf-8") as f:
|
|
||||||
f.write("[tool.nonebot]\n")
|
|
||||||
|
|
||||||
nonebot.logger.info(
|
nonebot.logger.info(
|
||||||
"正在 {} Python{}.{}.{} 上运行 尹灵温".format(
|
"正在 {} Python{}.{}.{} 上运行 尹灵温-NoneBot".format(
|
||||||
sys.executable,
|
sys.executable,
|
||||||
sys.version_info.major,
|
sys.version_info.major,
|
||||||
sys.version_info.minor,
|
sys.version_info.minor,
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import threading
|
import threading
|
||||||
|
|
||||||
from nonebot import logger
|
from liteyuki.comm.channel import active_channel
|
||||||
from liteyuki.comm.channel import get_channel
|
|
||||||
|
|
||||||
|
|
||||||
def reload(delay: float = 0.0, receiver: str = "nonebot"):
|
def reload(delay: float = 0.0, receiver: str = "nonebot"):
|
||||||
@ -14,13 +13,9 @@ def reload(delay: float = 0.0, receiver: str = "nonebot"):
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
chan = get_channel(receiver + "-active")
|
|
||||||
if chan is None:
|
|
||||||
logger.error(f"未见 Channel {receiver}-active 以至无法重载")
|
|
||||||
return
|
|
||||||
|
|
||||||
if delay > 0:
|
if delay > 0:
|
||||||
threading.Timer(delay, chan.send, args=(1,)).start()
|
threading.Timer(delay, active_channel.send, args=(1,)).start()
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
chan.send(1)
|
active_channel.send(1)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
import nonebot
|
import nonebot
|
||||||
@ -7,6 +8,7 @@ from pydantic import BaseModel
|
|||||||
|
|
||||||
from ..message.tools import random_hex_string
|
from ..message.tools import random_hex_string
|
||||||
|
|
||||||
|
|
||||||
config = {} # 全局配置,确保加载后读取
|
config = {} # 全局配置,确保加载后读取
|
||||||
|
|
||||||
|
|
||||||
@ -29,23 +31,37 @@ class BasicConfig(BaseModel):
|
|||||||
superusers: list[str] = []
|
superusers: list[str] = []
|
||||||
command_start: list[str] = ["/", ""]
|
command_start: list[str] = ["/", ""]
|
||||||
nickname: list[str] = [f"灵温-{random_hex_string(6)}"]
|
nickname: list[str] = [f"灵温-{random_hex_string(6)}"]
|
||||||
|
default_language: str = "zh-WY"
|
||||||
satori: SatoriConfig = SatoriConfig()
|
satori: SatoriConfig = SatoriConfig()
|
||||||
data_path: str = "data/liteyuki"
|
data_path: str = "data/liteyuki"
|
||||||
|
chromium_path: str = (
|
||||||
|
"/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome" # type: ignore
|
||||||
|
if platform.system() == "Darwin"
|
||||||
|
else (
|
||||||
|
"C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
|
||||||
|
if platform.system() == "Windows"
|
||||||
|
else "/usr/bin/chromium-browser"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def load_from_yaml(file: str) -> dict:
|
def load_from_yaml(file_: str) -> dict:
|
||||||
global config
|
global config
|
||||||
nonebot.logger.debug("Loading config from %s" % file)
|
nonebot.logger.debug("正在从 {} 中加载配置项".format(file_))
|
||||||
if not os.path.exists(file):
|
if not os.path.exists(file_):
|
||||||
nonebot.logger.warning(f"未找到配置文件 {file} ,已创建默认配置,请修改后重启。")
|
nonebot.logger.warning(
|
||||||
with open(file, "w", encoding="utf-8") as f:
|
f"未寻得配置文件 {file_} ,已以默认配置创建,请在重启后更改为你所需的内容。"
|
||||||
|
)
|
||||||
|
with open(file_, "w", encoding="utf-8") as f:
|
||||||
yaml.dump(BasicConfig().dict(), f, default_flow_style=False)
|
yaml.dump(BasicConfig().dict(), f, default_flow_style=False)
|
||||||
|
|
||||||
with open(file, "r", encoding="utf-8") as f:
|
with open(file_, "r", encoding="utf-8") as f:
|
||||||
conf = init_conf(yaml.load(f, Loader=yaml.FullLoader))
|
conf = init_conf(yaml.load(f, Loader=yaml.FullLoader))
|
||||||
config = conf
|
config = conf
|
||||||
if conf is None:
|
if conf is None:
|
||||||
nonebot.logger.warning(f"配置文件 {file} 为空,已创建默认配置,请修改后重启。")
|
nonebot.logger.warning(
|
||||||
|
f"配置文件 {file_} 为空,已以默认配置创建,请在重启后更改为你所需的内容。"
|
||||||
|
)
|
||||||
conf = BasicConfig().dict()
|
conf = BasicConfig().dict()
|
||||||
return conf
|
return conf
|
||||||
|
|
||||||
@ -75,7 +91,6 @@ def get_config(key: str, default=None):
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def init_conf(conf: dict) -> dict:
|
def init_conf(conf: dict) -> dict:
|
||||||
"""
|
"""
|
||||||
初始化配置文件,确保配置文件中的必要字段存在,且不会冲突
|
初始化配置文件,确保配置文件中的必要字段存在,且不会冲突
|
||||||
|
@ -30,7 +30,7 @@ class Database:
|
|||||||
os.makedirs(os.path.dirname(db_name))
|
os.makedirs(os.path.dirname(db_name))
|
||||||
|
|
||||||
self.db_name = db_name
|
self.db_name = db_name
|
||||||
self.conn = sqlite3.connect(db_name)
|
self.conn = sqlite3.connect(db_name, check_same_thread=False)
|
||||||
self.cursor = self.conn.cursor()
|
self.cursor = self.conn.cursor()
|
||||||
|
|
||||||
self._on_save_callbacks = []
|
self._on_save_callbacks = []
|
||||||
@ -94,7 +94,7 @@ class Database:
|
|||||||
f"数据库 Selecting {model.TABLE_NAME} WHERE {condition.replace('?', '%s') % args}"
|
f"数据库 Selecting {model.TABLE_NAME} WHERE {condition.replace('?', '%s') % args}"
|
||||||
)
|
)
|
||||||
if not table_name:
|
if not table_name:
|
||||||
raise ValueError(f"数据模型{model_type.__name__}未提供表名")
|
raise ValueError(f"数据模型 {model_type.__name__} 未提供表名")
|
||||||
|
|
||||||
# condition = f"WHERE {condition}"
|
# condition = f"WHERE {condition}"
|
||||||
# print(f"SELECT * FROM {table_name} {condition}", args)
|
# print(f"SELECT * FROM {table_name} {condition}", args)
|
||||||
@ -118,7 +118,7 @@ class Database:
|
|||||||
]
|
]
|
||||||
|
|
||||||
def save(self, *args: LiteModel):
|
def save(self, *args: LiteModel):
|
||||||
"""增/改操作
|
self.returns_ = """增/改操作
|
||||||
Args:
|
Args:
|
||||||
*args:
|
*args:
|
||||||
Returns:
|
Returns:
|
||||||
@ -126,7 +126,7 @@ class Database:
|
|||||||
table_list = [
|
table_list = [
|
||||||
item[0]
|
item[0]
|
||||||
for item in self.cursor.execute(
|
for item in self.cursor.execute(
|
||||||
"SELECT name FROM sqlite_master WHERE type='table'"
|
"SELECT name FROM sqlite_master WHERE type ='table'"
|
||||||
).fetchall()
|
).fetchall()
|
||||||
]
|
]
|
||||||
for model in args:
|
for model in args:
|
||||||
@ -158,7 +158,7 @@ class Database:
|
|||||||
new_obj[field] = value
|
new_obj[field] = value
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"数据模型{table_name}包含不支持的数据类型,字段:{field} 值:{value} 值类型:{type(value)}"
|
f"数据模型 {table_name} 包含不支持的数据类型,字段:{field} 值:{value} 值类型:{type(value)}"
|
||||||
)
|
)
|
||||||
if table_name:
|
if table_name:
|
||||||
fields, values = [], []
|
fields, values = [], []
|
||||||
@ -273,9 +273,9 @@ class Database:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
table_name = model.TABLE_NAME
|
table_name = model.TABLE_NAME
|
||||||
logger.debug(f"数据库 Deleting {model} WHERE {condition} {args}")
|
logger.debug(f"Deleting {model} WHERE {condition} {args}")
|
||||||
if not table_name:
|
if not table_name:
|
||||||
raise ValueError(f"数据模型{model.__class__.__name__}未提供表名")
|
raise ValueError(f"数据模型 {model.__class__.__name__} 未提供表名")
|
||||||
if model.id is not None:
|
if model.id is not None:
|
||||||
condition = f"id = {model.id}"
|
condition = f"id = {model.id}"
|
||||||
if not condition and not allow_empty:
|
if not condition and not allow_empty:
|
||||||
@ -297,7 +297,7 @@ class Database:
|
|||||||
"""
|
"""
|
||||||
for model in args:
|
for model in args:
|
||||||
if not model.TABLE_NAME:
|
if not model.TABLE_NAME:
|
||||||
raise ValueError(f"数据模型{type(model).__name__}未提供表名")
|
raise ValueError(f"数据模型 {type(model).__name__} 未提供表名")
|
||||||
|
|
||||||
# 若无则创建表
|
# 若无则创建表
|
||||||
self.cursor.execute(
|
self.cursor.execute(
|
||||||
|
@ -2,9 +2,8 @@ import os
|
|||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
from .data import Database, LiteModel, Database
|
from .data import Database, LiteModel
|
||||||
|
|
||||||
print("导入数据库模块")
|
|
||||||
DATA_PATH = "data/liteyuki"
|
DATA_PATH = "data/liteyuki"
|
||||||
user_db: Database = Database(os.path.join(DATA_PATH, "users.ldb"))
|
user_db: Database = Database(os.path.join(DATA_PATH, "users.ldb"))
|
||||||
group_db: Database = Database(os.path.join(DATA_PATH, "groups.ldb"))
|
group_db: Database = Database(os.path.join(DATA_PATH, "groups.ldb"))
|
||||||
@ -64,7 +63,7 @@ def auto_migrate():
|
|||||||
user_db.auto_migrate(User())
|
user_db.auto_migrate(User())
|
||||||
group_db.auto_migrate(Group())
|
group_db.auto_migrate(Group())
|
||||||
plugin_db.auto_migrate(InstalledPlugin(), GlobalPlugin())
|
plugin_db.auto_migrate(InstalledPlugin(), GlobalPlugin())
|
||||||
common_db.auto_migrate(GlobalPlugin(), StoredConfig(), TempConfig())
|
common_db.auto_migrate(GlobalPlugin(), TempConfig())
|
||||||
|
|
||||||
|
|
||||||
auto_migrate()
|
auto_migrate()
|
||||||
|
@ -1,92 +0,0 @@
|
|||||||
import json
|
|
||||||
import os.path
|
|
||||||
import platform
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
import nonebot
|
|
||||||
import psutil
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from .config import load_from_yaml
|
|
||||||
from .. import __NAME__, __VERSION_I__, __VERSION__
|
|
||||||
|
|
||||||
|
|
||||||
class LiteyukiAPI:
|
|
||||||
def __init__(self):
|
|
||||||
self.liteyuki_id = None
|
|
||||||
if os.path.exists("data/liteyuki/liteyuki.json"):
|
|
||||||
with open("data/liteyuki/liteyuki.json", "rb") as f:
|
|
||||||
self.data = json.loads(f.read())
|
|
||||||
self.liteyuki_id = self.data.get("liteyuki_id")
|
|
||||||
self.report = load_from_yaml("config.yml").get("auto_report", True)
|
|
||||||
|
|
||||||
if self.report:
|
|
||||||
nonebot.logger.info("已启用自动上报")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def device_info(self) -> dict:
|
|
||||||
"""
|
|
||||||
获取设备信息
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
"name": __NAME__,
|
|
||||||
"version": __VERSION__,
|
|
||||||
"version_i": __VERSION_I__,
|
|
||||||
"python": f"{platform.python_implementation()} {platform.python_version()}",
|
|
||||||
"os": f"{platform.system()} {platform.version()} {platform.machine()}",
|
|
||||||
"cpu": f"{psutil.cpu_count(logical=False)}c{psutil.cpu_count()}t{psutil.cpu_freq().current}MHz",
|
|
||||||
"memory_total": f"{psutil.virtual_memory().total / 1024 ** 3:.2f}吉字节",
|
|
||||||
"memory_used": f"{psutil.virtual_memory().used / 1024 ** 3:.2f}吉字节",
|
|
||||||
"memory_bot": f"{psutil.Process(os.getpid()).memory_info().rss / 1024 ** 2:.2f}兆字节",
|
|
||||||
"disk": f"{psutil.disk_usage('/').total / 1024 ** 3:.2f}吉字节",
|
|
||||||
}
|
|
||||||
|
|
||||||
def bug_report(self, content: str):
|
|
||||||
"""
|
|
||||||
提交bug报告
|
|
||||||
Args:
|
|
||||||
content:
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
if self.report:
|
|
||||||
nonebot.logger.warning(f"正在上报查误:{content}")
|
|
||||||
url = "https://api.liteyuki.icu/bug_report"
|
|
||||||
data = {
|
|
||||||
"liteyuki_id": self.liteyuki_id,
|
|
||||||
"content": content,
|
|
||||||
"device_info": self.device_info,
|
|
||||||
}
|
|
||||||
resp = requests.post(url, json=data)
|
|
||||||
if resp.status_code == 200:
|
|
||||||
nonebot.logger.success(
|
|
||||||
f"成功上报差误信息,报文ID为:{resp.json().get('report_id')}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
nonebot.logger.error(f"差误上报错误:{resp.text}")
|
|
||||||
else:
|
|
||||||
nonebot.logger.warning(f"已禁用自动上报:{content}")
|
|
||||||
|
|
||||||
def register(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def heartbeat_report(self):
|
|
||||||
"""
|
|
||||||
提交心跳,预留接口
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
url = "https://api.liteyuki.icu/heartbeat"
|
|
||||||
data = {
|
|
||||||
"liteyuki_id": self.liteyuki_id,
|
|
||||||
"version": __VERSION__,
|
|
||||||
}
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.post(url, json=data) as resp:
|
|
||||||
if resp.status == 200:
|
|
||||||
nonebot.logger.success("心跳成功送达。")
|
|
||||||
else:
|
|
||||||
nonebot.logger.error("休克:{}".format(await resp.text()))
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user